From 1155c7564e189022ade0506cfaec2a55c89bf02f Mon Sep 17 00:00:00 2001 From: lone Date: Mon, 16 Mar 2026 10:13:55 +0800 Subject: [PATCH 01/83] docs: add Chinese (zh-CN) documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add language switch link to root README - Translate README.md to docs/zh-CN/README.md - Translate USER-GUIDE.md to docs/zh-CN/USER-GUIDE.md - Translate all 13 reference documents to docs/zh-CN/references/ Key terminology mappings: - context engineering → 上下文工程 - spec-driven development → 规格驱动开发 - context rot → 上下文衰减 - phase → 阶段 - milestone → 里程碑 - roadmap → 路线图 Co-Authored-By: Claude Opus 4.6 --- README.md | 2 + docs/zh-CN/README.md | 707 ++++++++++++++++++ docs/zh-CN/USER-GUIDE.md | 492 ++++++++++++ docs/zh-CN/references/checkpoints.md | 450 +++++++++++ docs/zh-CN/references/continuation-format.md | 249 ++++++ .../references/decimal-phase-calculation.md | 65 ++ docs/zh-CN/references/git-integration.md | 248 ++++++ docs/zh-CN/references/git-planning-commit.md | 38 + .../references/model-profile-resolution.md | 34 + docs/zh-CN/references/model-profiles.md | 93 +++ .../references/phase-argument-parsing.md | 61 ++ docs/zh-CN/references/planning-config.md | 200 +++++ docs/zh-CN/references/questioning.md | 142 ++++ docs/zh-CN/references/tdd.md | 263 +++++++ docs/zh-CN/references/ui-brand.md | 158 ++++ .../zh-CN/references/verification-patterns.md | 612 +++++++++++++++ 16 files changed, 3814 insertions(+) create mode 100644 docs/zh-CN/README.md create mode 100644 docs/zh-CN/USER-GUIDE.md create mode 100644 docs/zh-CN/references/checkpoints.md create mode 100644 docs/zh-CN/references/continuation-format.md create mode 100644 docs/zh-CN/references/decimal-phase-calculation.md create mode 100644 docs/zh-CN/references/git-integration.md create mode 100644 docs/zh-CN/references/git-planning-commit.md create mode 100644 docs/zh-CN/references/model-profile-resolution.md create mode 100644 docs/zh-CN/references/model-profiles.md create mode 100644 docs/zh-CN/references/phase-argument-parsing.md create mode 100644 docs/zh-CN/references/planning-config.md create mode 100644 docs/zh-CN/references/questioning.md create mode 100644 docs/zh-CN/references/tdd.md create mode 100644 docs/zh-CN/references/ui-brand.md create mode 100644 docs/zh-CN/references/verification-patterns.md diff --git a/README.md b/README.md index 976250856..ca34dfb0a 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ **Solves context rot — the quality degradation that happens as Claude fills its context window.** +[**English**](README.md) | [**简体中文**](docs/zh-CN/README.md) + [![npm version](https://img.shields.io/npm/v/get-shit-done-cc?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/get-shit-done-cc) [![npm downloads](https://img.shields.io/npm/dm/get-shit-done-cc?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/get-shit-done-cc) [![Tests](https://img.shields.io/github/actions/workflow/status/glittercowboy/get-shit-done/test.yml?branch=main&style=for-the-badge&logo=github&label=Tests)](https://github.com/glittercowboy/get-shit-done/actions/workflows/test.yml) diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md new file mode 100644 index 000000000..150aa346e --- /dev/null +++ b/docs/zh-CN/README.md @@ -0,0 +1,707 @@ +
+ +# GET SHIT DONE + +**一个轻量级且强大的元提示、上下文工程和规格驱动开发系统,支持 Claude Code、OpenCode、Gemini CLI 和 Codex。** + +**解决上下文衰减 —— 即 Claude 填充上下文窗口时发生的质量退化问题。** + +[![npm version](https://img.shields.io/npm/v/get-shit-done-cc?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/get-shit-done-cc) +[![npm downloads](https://img.shields.io/npm/dm/get-shit-done-cc?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/get-shit-done-cc) +[![Tests](https://img.shields.io/github/actions/workflow/status/glittercowboy/get-shit-done/test.yml?branch=main&style=for-the-badge&logo=github&label=Tests)](https://github.com/glittercowboy/get-shit-done/actions/workflows/test.yml) +[![Discord](https://img.shields.io/badge/Discord-Join-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/gsd) +[![X (Twitter)](https://img.shields.io/badge/X-@gsd__foundation-000000?style=for-the-badge&logo=x&logoColor=white)](https://x.com/gsd_foundation) +[![$GSD Token](https://img.shields.io/badge/$GSD-Dexscreener-1C1C1C?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48Y2lyY2xlIGN4PSIxMiIgY3k9IjEyIiByPSIxMCIgZmlsbD0iIzAwRkYwMCIvPjwvc3ZnPg==&logoColor=00FF00)](https://dexscreener.com/solana/dwudwjvan7bzkw9zwlbyv6kspdlvhwzrqy6ebk8xzxkv) +[![GitHub stars](https://img.shields.io/github/stars/glittercowboy/get-shit-done?style=for-the-badge&logo=github&color=181717)](https://github.com/glittercowboy/get-shit-done) +[![License](https://img.shields.io/badge/license-MIT-blue?style=for-the-badge)](LICENSE) + +
+ +```bash +npx get-shit-done-cc@latest +``` + +**支持 Mac、Windows 和 Linux。** + +
+ +![GSD Install](../assets/terminal.svg) + +
+ +*"如果你清楚自己想要什么,它真的会帮你构建出来。不忽悠。"* + +*"我试过 SpecKit、OpenSpec 和 Taskmaster —— 这是我用过的效果最好的。"* + +*"这是我用过的 Claude Code 最强大的扩展。没有过度设计。真的就是把事情做完。"* + +
+ +**被 Amazon、Google、Shopify 和 Webflow 的工程师信赖使用。** + +[我为什么开发这个](#我为什么开发这个) · [工作原理](#工作原理) · [命令](#命令) · [为什么有效](#为什么有效) · [用户指南](USER-GUIDE.md) + +
+ +--- + +## 我为什么开发这个 + +我是一名独立开发者。我不写代码 —— Claude Code 写。 + +其他规格驱动开发工具确实存在,比如 BMAD、Speckit... 但它们似乎都把事情搞得比实际需要的复杂得多(冲刺会议、故事点、干系人同步、回顾、Jira 工作流),或者缺乏对你正在构建的东西的真正大局理解。我不是一个 50 人的软件公司。我不想搞企业级表演。我只是个想构建出好用的东西的创意人。 + +所以我开发了 GSD。复杂性在系统内部,不在你的工作流里。幕后是:上下文工程、XML 提示格式、子代理编排、状态管理。你看到的是:几个命令,用就完了。 + +系统给 Claude 提供了它完成工作**以及**验证工作所需的一切。我信任这个工作流。它就是做得好。 + +这就是它的本质。没有企业级角色扮演的废话。只是一个让 Claude Code 稳定可靠地构建酷东西的极其有效的系统。 + +— **TÂCHES** + +--- + +Vibecoding 名声不好。你描述想要什么,AI 生成代码,结果得到不一致的垃圾,规模一大就崩。 + +GSD 解决了这个问题。它是让 Claude Code 变得可靠的上下文工程层。描述你的想法,让系统提取它需要知道的一切,然后让 Claude Code 开始工作。 + +--- + +## 这个工具适合谁 + +想要描述需求然后正确构建出来的人 —— 不用假装自己在运营一个 50 人的工程组织。 + +--- + +## 快速开始 + +```bash +npx get-shit-done-cc@latest +``` + +安装程序会提示你选择: +1. **运行时** —— Claude Code、OpenCode、Gemini、Codex 或全部 +2. **位置** —— 全局(所有项目)或本地(仅当前项目) + +验证安装: +- Claude Code / Gemini: `/gsd:help` +- OpenCode: `/gsd-help` +- Codex: `$gsd-help` + +> [!NOTE] +> Codex 安装使用技能(`skills/gsd-*/SKILL.md`)而非自定义提示。 + +### 保持更新 + +GSD 快速迭代。定期更新: + +```bash +npx get-shit-done-cc@latest +``` + +
+非交互式安装(Docker、CI、脚本) + +```bash +# Claude Code +npx get-shit-done-cc --claude --global # 安装到 ~/.claude/ +npx get-shit-done-cc --claude --local # 安装到 ./.claude/ + +# OpenCode(开源,免费模型) +npx get-shit-done-cc --opencode --global # 安装到 ~/.config/opencode/ + +# Gemini CLI +npx get-shit-done-cc --gemini --global # 安装到 ~/.gemini/ + +# Codex(技能优先) +npx get-shit-done-cc --codex --global # 安装到 ~/.codex/ +npx get-shit-done-cc --codex --local # 安装到 ./.codex/ + +# 所有运行时 +npx get-shit-done-cc --all --global # 安装到所有目录 +``` + +使用 `--global`(`-g`)或 `--local`(`-l`)跳过位置提示。 +使用 `--claude`、`--opencode`、`--gemini`、`--codex` 或 `--all` 跳过运行时提示。 + +
+ +
+开发安装 + +克隆仓库并本地运行安装程序: + +```bash +git clone https://github.com/glittercowboy/get-shit-done.git +cd get-shit-done +node bin/install.js --claude --local +``` + +安装到 `./.claude/` 用于在贡献前测试修改。 + +
+ +### 推荐:跳过权限模式 + +GSD 设计为无摩擦自动化。运行 Claude Code 时使用: + +```bash +claude --dangerously-skip-permissions +``` + +> [!TIP] +> 这是 GSD 的预期使用方式 —— 停下来 50 次批准 `date` 和 `git commit` 会失去意义。 + +
+替代方案:细粒度权限 + +如果你不想使用那个标志,在项目的 `.claude/settings.json` 中添加: + +```json +{ + "permissions": { + "allow": [ + "Bash(date:*)", + "Bash(echo:*)", + "Bash(cat:*)", + "Bash(ls:*)", + "Bash(mkdir:*)", + "Bash(wc:*)", + "Bash(head:*)", + "Bash(tail:*)", + "Bash(sort:*)", + "Bash(grep:*)", + "Bash(tr:*)", + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(git status:*)", + "Bash(git log:*)", + "Bash(git diff:*)", + "Bash(git tag:*)" + ] + } +} +``` + +
+ +--- + +## 工作原理 + +> **已有代码?** 先运行 `/gsd:map-codebase`。它会生成并行代理分析你的技术栈、架构、约定和关注点。然后 `/gsd:new-project` 就了解你的代码库了 —— 问题聚焦在你正在**添加**什么,规划会自动加载你的模式。 + +### 1. 初始化项目 + +``` +/gsd:new-project +``` + +一条命令,一个流程。系统: + +1. **提问** —— 问到完全理解你的想法为止(目标、约束、技术偏好、边缘情况) +2. **研究** —— 生成并行代理调查领域(可选但推荐) +3. **需求** —— 提取哪些是 v1、v2 和范围外 +4. **路线图** —— 创建映射到需求的阶段 + +你批准路线图。现在准备好构建了。 + +**创建:** `PROJECT.md`、`REQUIREMENTS.md`、`ROADMAP.md`、`STATE.md`、`.planning/research/` + +--- + +### 2. 讨论阶段 + +``` +/gsd:discuss-phase 1 +``` + +**这是你塑造实现方式的地方。** + +你的路线图每个阶段有一两句话。这不足以按照**你**想象的方式构建东西。这一步在研究或规划之前捕获你的偏好。 + +系统分析阶段并根据正在构建的内容识别灰色区域: + +- **视觉功能** → 布局、密度、交互、空状态 +- **API/CLI** → 响应格式、标志、错误处理、详细程度 +- **内容系统** → 结构、语气、深度、流程 +- **组织任务** → 分组标准、命名、重复项、例外 + +对于你选择的每个领域,它会问到让你满意为止。输出 —— `CONTEXT.md` —— 直接输入接下来的两个步骤: + +1. **研究员读取它** —— 知道要调查什么模式("用户想要卡片布局" → 研究卡片组件库) +2. **规划者读取它** —— 知道哪些决策已锁定("无限滚动已决定" → 规划包含滚动处理) + +你在这里走得越深,系统构建的就越是你真正想要的。跳过它你会得到合理的默认值。使用它你会得到**你的**愿景。 + +**创建:** `{阶段号}-CONTEXT.md` + +--- + +### 3. 规划阶段 + +``` +/gsd:plan-phase 1 +``` + +系统: + +1. **研究** —— 调查如何实现这个阶段,由你的 CONTEXT.md 决策指导 +2. **规划** —— 创建 2-3 个带有 XML 结构的原子任务计划 +3. **验证** —— 根据需求检查计划,循环直到通过 + +每个计划足够小,可以在全新的上下文窗口中执行。没有退化,没有"我现在会更简洁"。 + +**创建:** `{阶段号}-RESEARCH.md`、`{阶段号}-{N}-PLAN.md` + +--- + +### 4. 执行阶段 + +``` +/gsd:execute-phase 1 +``` + +系统: + +1. **按波次运行计划** —— 可能的话并行,有依赖时顺序 +2. **每个计划全新上下文** —— 200k token 纯粹用于实现,零累积垃圾 +3. **每个任务提交** —— 每个任务都有自己的原子提交 +4. **根据目标验证** —— 检查代码库是否交付了阶段承诺的内容 + +离开,回来看到完成的工作和干净的 git 历史。 + +**波次执行工作原理:** + +计划根据依赖关系分组到"波次"。在每个波次内,计划并行运行。波次顺序执行。 + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ 阶段执行 │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ 波次 1 (并行) 波次 2 (并行) 波次 3 │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │ 计划 01 │ │ 计划 02 │ → │ 计划 03 │ │ 计划 04 │ → │ 计划 05 │ │ +│ │ │ │ │ │ │ │ │ │ │ │ +│ │ 用户 │ │ 产品 │ │ 订单 │ │ 购物车 │ │ 结账 │ │ +│ │ 模型 │ │ 模型 │ │ API │ │ API │ │ UI │ │ +│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ +│ │ │ ↑ ↑ ↑ │ +│ └───────────┴──────────────┴───────────┘ │ │ +│ 依赖关系: 计划 03 需要计划 01 │ │ +│ 计划 04 需要计划 02 │ │ +│ 计划 05 需要计划 03 + 04 │ │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +**为什么波次重要:** +- 独立计划 → 同一波次 → 并行运行 +- 依赖计划 → 后续波次 → 等待依赖 +- 文件冲突 → 顺序计划或同一计划 + +这就是为什么"垂直切片"(计划 01: 用户功能端到端)比"水平分层"(计划 01: 所有模型,计划 02: 所有 API)并行化更好。 + +**创建:** `{阶段号}-{N}-SUMMARY.md`、`{阶段号}-VERIFICATION.md` + +--- + +### 5. 验证工作 + +``` +/gsd:verify-work 1 +``` + +**这是你确认它真的有效的地方。** + +自动化验证检查代码存在和测试通过。但功能是否按你预期的方式**工作**?这是你使用它的机会。 + +系统: + +1. **提取可测试交付物** —— 你现在应该能做什么 +2. **逐个引导你** —— "你能用邮箱登录吗?" 是/否,或描述有什么问题 +3. **自动诊断失败** —— 生成调试代理找根本原因 +4. **创建已验证的修复计划** —— 准备立即重新执行 + +如果一切通过,继续。如果有东西坏了,不用手动调试 —— 只需再次运行 `/gsd:execute-phase`,使用它创建的修复计划。 + +**创建:** `{阶段号}-UAT.md`,如果发现问题则创建修复计划 + +--- + +### 6. 循环 → 完成 → 下一个里程碑 + +``` +/gsd:discuss-phase 2 +/gsd:plan-phase 2 +/gsd:execute-phase 2 +/gsd:verify-work 2 +... +/gsd:complete-milestone +/gsd:new-milestone +``` + +循环 **讨论 → 规划 → 执行 → 验证** 直到里程碑完成。 + +如果你想在讨论期间更快速地输入,使用 `/gsd:discuss-phase --batch` 一次回答一组小问题,而不是一个一个来。 + +每个阶段都会获得你的输入(讨论)、适当的研究(规划)、干净的执行(执行)和人工验证(验证)。上下文保持新鲜。质量保持高水平。 + +当所有阶段完成后,`/gsd:complete-milestone` 归档里程碑并标记发布。 + +然后 `/gsd:new-milestone` 开始下一个版本 —— 与 `new-project` 相同的流程,但针对你现有的代码库。你描述接下来想构建什么,系统研究领域,你界定需求范围,它创建新的路线图。每个里程碑是一个干净的周期:定义 → 构建 → 发布。 + +--- + +### 快速模式 + +``` +/gsd:quick +``` + +**用于不需要完整规划的临时任务。** + +快速模式给你 GSD 保证(原子提交、状态跟踪)和更快的路径: + +- **相同代理** —— 规划者 + 执行者,相同质量 +- **跳过可选步骤** —— 无研究、无计划检查器、无验证器 +- **独立跟踪** —— 存放在 `.planning/quick/`,不是阶段 + +用于:bug 修复、小功能、配置更改、一次性任务。 + +``` +/gsd:quick +> 你想做什么?"在设置中添加深色模式切换" +``` + +**创建:** `.planning/quick/001-add-dark-mode-toggle/PLAN.md`、`SUMMARY.md` + +--- + +## 为什么有效 + +### 上下文工程 + +Claude Code 非常强大,**如果你**给它需要的上下文。大多数人没有。 + +GSD 为你处理: + +| 文件 | 作用 | +|------|------| +| `PROJECT.md` | 项目愿景,始终加载 | +| `research/` | 生态知识(技术栈、功能、架构、陷阱) | +| `REQUIREMENTS.md` | 界定 v1/v2 需求及阶段可追溯性 | +| `ROADMAP.md` | 你要去哪里,完成了什么 | +| `STATE.md` | 决策、阻塞项、位置 —— 跨会话记忆 | +| `PLAN.md` | 带有 XML 结构和验证步骤的原子任务 | +| `SUMMARY.md` | 发生了什么,改了什么,提交到历史 | +| `todos/` | 为后续工作捕获的想法和任务 | + +基于 Claude 质量退化的位置设置大小限制。保持在限制内,获得一致的卓越。 + +### XML 提示格式 + +每个计划都是为 Claude 优化的结构化 XML: + +```xml + + 创建登录端点 + src/app/api/auth/login/route.ts + + 使用 jose 处理 JWT(不用 jsonwebtoken - CommonJS 问题)。 + 根据 users 表验证凭据。 + 成功时返回 httpOnly cookie。 + + curl -X POST localhost:3000/api/auth/login 返回 200 + Set-Cookie + 有效凭据返回 cookie,无效返回 401 + +``` + +精确的指令。不猜测。内置验证。 + +### 多代理编排 + +每个阶段使用相同模式:轻量编排器生成专门代理,收集结果,路由到下一步。 + +| 阶段 | 编排器做 | 代理做 | +|-------|------------------|-----------| +| 研究 | 协调,呈现发现 | 4 个并行研究员调查技术栈、功能、架构、陷阱 | +| 规划 | 验证,管理迭代 | 规划者创建计划,检查器验证,循环直到通过 | +| 执行 | 分组为波次,跟踪进度 | 执行者并行实现,每个有全新 200k 上下文 | +| 验证 | 呈现结果,路由下一步 | 验证器根据目标检查代码库,调试器诊断失败 | + +编排器从不做重活。它生成代理,等待,整合结果。 + +**结果:** 你可以运行整个阶段 —— 深度研究、多个计划创建和验证、跨并行执行者编写数千行代码、根据目标自动化验证 —— 你的主上下文窗口保持在 30-40%。工作在全新的子代理上下文中完成。你的会话保持快速和响应。 + +### 原子 Git 提交 + +每个任务在完成后立即获得自己的提交: + +```bash +abc123f docs(08-02): 完成用户注册计划 +def456g feat(08-02): 添加邮箱确认流程 +hij789k feat(08-02): 实现密码哈希 +lmn012o feat(08-02): 创建注册端点 +``` + +> [!NOTE] +> **好处:** Git bisect 找到确切的失败任务。每个任务独立可回滚。未来会话中 Claude 的清晰历史。AI 自动化工作流中更好的可观察性。 + +每个提交都是精确的、可追溯的、有意义的。 + +### 模块化设计 + +- 向当前里程碑添加阶段 +- 在阶段之间插入紧急工作 +- 完成里程碑并重新开始 +- 调整计划而不重建一切 + +你永远不会被锁定。系统会适应。 + +--- + +## 命令 + +### 核心工作流 + +| 命令 | 作用 | +|---------|--------------| +| `/gsd:new-project [--auto]` | 完整初始化:提问 → 研究 → 需求 → 路线图 | +| `/gsd:discuss-phase [N] [--auto]` | 在规划前捕获实现决策 | +| `/gsd:plan-phase [N] [--auto]` | 阶段的研究 + 规划 + 验证 | +| `/gsd:execute-phase ` | 在并行波次中执行所有计划,完成后验证 | +| `/gsd:verify-work [N]` | 手动用户验收测试 ¹ | +| `/gsd:audit-milestone` | 验证里程碑达到了其完成定义 | +| `/gsd:complete-milestone` | 归档里程碑,标记发布 | +| `/gsd:new-milestone [name]` | 开始下一个版本:提问 → 研究 → 需求 → 路线图 | + +### 导航 + +| 命令 | 作用 | +|---------|--------------| +| `/gsd:progress` | 我在哪?接下来做什么? | +| `/gsd:help` | 显示所有命令和使用指南 | +| `/gsd:update` | 更新 GSD 并预览变更日志 | +| `/gsd:join-discord` | 加入 GSD Discord 社区 | + +### 现有代码库 + +| 命令 | 作用 | +|---------|--------------| +| `/gsd:map-codebase` | 在 new-project 之前分析现有代码库 | + +### 阶段管理 + +| 命令 | 作用 | +|---------|--------------| +| `/gsd:add-phase` | 向路线图追加阶段 | +| `/gsd:insert-phase [N]` | 在阶段之间插入紧急工作 | +| `/gsd:remove-phase [N]` | 删除未来阶段,重新编号 | +| `/gsd:list-phase-assumptions [N]` | 规划前查看 Claude 的预期方法 | +| `/gsd:plan-milestone-gaps` | 创建阶段以填补审计发现的差距 | + +### 会话 + +| 命令 | 作用 | +|---------|--------------| +| `/gsd:pause-work` | 阶段中途停止时创建交接 | +| `/gsd:resume-work` | 从上次会话恢复 | + +### 工具 + +| 命令 | 作用 | +|---------|--------------| +| `/gsd:settings` | 配置模型配置文件和工作流代理 | +| `/gsd:set-profile ` | 切换模型配置文件(quality/balanced/budget) | +| `/gsd:add-todo [desc]` | 捕获想法留待后用 | +| `/gsd:check-todos` | 列出待处理事项 | +| `/gsd:debug [desc]` | 带持久状态的系统化调试 | +| `/gsd:quick [--full] [--discuss]` | 用 GSD 保证执行临时任务(`--full` 添加计划检查和验证,`--discuss` 先收集上下文) | +| `/gsd:health [--repair]` | 验证 `.planning/` 目录完整性,用 `--repair` 自动修复 | + +¹ 由 Reddit 用户 OracleGreyBeard 贡献 + +--- + +## 配置 + +GSD 在 `.planning/config.json` 中存储项目设置。在 `/gsd:new-project` 期间配置或稍后用 `/gsd:settings` 更新。完整配置模式、工作流开关、git 分支选项和每个代理的模型分解,请参阅[用户指南](USER-GUIDE.md#配置参考)。 + +### 核心设置 + +| 设置 | 选项 | 默认值 | 控制内容 | +|---------|---------|---------|------------------| +| `mode` | `yolo`, `interactive` | `interactive` | 自动批准 vs 每步确认 | +| `granularity` | `coarse`, `standard`, `fine` | `standard` | 阶段粒度 —— 范围切分多细(阶段 × 计划) | + +### 模型配置 + +控制每个代理使用哪个 Claude 模型。平衡质量和 token 消耗。 + +| 配置 | 规划 | 执行 | 验证 | +|---------|----------|-----------|--------------| +| `quality` | Opus | Opus | Sonnet | +| `balanced`(默认) | Opus | Sonnet | Sonnet | +| `budget` | Sonnet | Sonnet | Haiku | + +切换配置: +``` +/gsd:set-profile budget +``` + +或通过 `/gsd:settings` 配置。 + +### 工作流代理 + +这些在规划/执行期间生成额外代理。它们提高质量但增加 token 和时间。 + +| 设置 | 默认值 | 作用 | +|---------|---------|--------------| +| `workflow.research` | `true` | 每个阶段规划前研究领域 | +| `workflow.plan_check` | `true` | 执行前验证计划是否达到阶段目标 | +| `workflow.verifier` | `true` | 执行后确认必须项已交付 | +| `workflow.auto_advance` | `false` | 自动链式执行 讨论 → 规划 → 执行 | + +使用 `/gsd:settings` 切换这些,或每次调用时覆盖: +- `/gsd:plan-phase --skip-research` +- `/gsd:plan-phase --skip-verify` + +### 执行 + +| 设置 | 默认值 | 控制内容 | +|---------|---------|------------------| +| `parallelization.enabled` | `true` | 同时运行独立计划 | +| `planning.commit_docs` | `true` | 在 git 中跟踪 `.planning/` | + +### Git 分支 + +控制 GSD 在执行期间如何处理分支。 + +| 设置 | 选项 | 默认值 | 作用 | +|---------|---------|---------|--------------| +| `git.branching_strategy` | `none`, `phase`, `milestone` | `none` | 分支创建策略 | +| `git.phase_branch_template` | 字符串 | `gsd/phase-{phase}-{slug}` | 阶段分支模板 | +| `git.milestone_branch_template` | 字符串 | `gsd/{milestone}-{slug}` | 里程碑分支模板 | + +**策略:** +- **`none`** —— 提交到当前分支(默认 GSD 行为) +- **`phase`** —— 每个阶段创建一个分支,阶段完成时合并 +- **`milestone`** —— 为整个里程碑创建一个分支,完成时合并 + +在里程碑完成时,GSD 提供 squash 合并(推荐)或带历史合并。 + +--- + +## 安全 + +### 保护敏感文件 + +GSD 的代码库映射和分析命令读取文件以了解你的项目。**保护包含密钥的文件**,将它们添加到 Claude Code 的拒绝列表: + +1. 打开 Claude Code 设置(`.claude/settings.json` 或全局) +2. 将敏感文件模式添加到拒绝列表: + +```json +{ + "permissions": { + "deny": [ + "Read(.env)", + "Read(.env.*)", + "Read(**/secrets/*)", + "Read(**/*credential*)", + "Read(**/*.pem)", + "Read(**/*.key)" + ] + } +} +``` + +这完全阻止 Claude 读取这些文件,无论你运行什么命令。 + +> [!IMPORTANT] +> GSD 包含内置保护以防止提交密钥,但纵深防御是最佳实践。拒绝读取敏感文件作为第一道防线。 + +--- + +## 故障排除 + +**安装后找不到命令?** +- 重启运行时以重新加载命令/技能 +- 验证文件是否存在于 `~/.claude/commands/gsd/`(全局)或 `./.claude/commands/gsd/`(本地) +- 对于 Codex,验证技能是否存在于 `~/.codex/skills/gsd-*/SKILL.md`(全局)或 `./.codex/skills/gsd-*/SKILL.md`(本地) + +**命令没有按预期工作?** +- 运行 `/gsd:help` 验证安装 +- 重新运行 `npx get-shit-done-cc` 重新安装 + +**更新到最新版本?** +```bash +npx get-shit-done-cc@latest +``` + +**使用 Docker 或容器化环境?** + +如果用波浪号路径(`~/.claude/...`)读取文件失败,在安装前设置 `CLAUDE_CONFIG_DIR`: +```bash +CLAUDE_CONFIG_DIR=/home/youruser/.claude npx get-shit-done-cc --global +``` +这确保使用绝对路径而不是 `~`,后者在容器中可能无法正确展开。 + +### 卸载 + +完全删除 GSD: + +```bash +# 全局安装 +npx get-shit-done-cc --claude --global --uninstall +npx get-shit-done-cc --opencode --global --uninstall +npx get-shit-done-cc --codex --global --uninstall + +# 本地安装(当前项目) +npx get-shit-done-cc --claude --local --uninstall +npx get-shit-done-cc --opencode --local --uninstall +npx get-shit-done-cc --codex --local --uninstall +``` + +这删除所有 GSD 命令、代理、钩子和设置,同时保留你的其他配置。 + +--- + +## 社区移植 + +OpenCode、Gemini CLI 和 Codex 现在通过 `npx get-shit-done-cc` 原生支持。 + +这些社区移植开创了多运行时支持: + +| 项目 | 平台 | 描述 | +|---------|----------|-------------| +| [gsd-opencode](https://github.com/rokicool/gsd-opencode) | OpenCode | 原始 OpenCode 适配 | +| gsd-gemini (已归档) | Gemini CLI | 由 uberfuzzy 开发的原始 Gemini 适配 | + +--- + +## Star 历史 + + + + + + Star History Chart + + + +--- + +## 许可证 + +MIT 许可证。详见 [LICENSE](../LICENSE)。 + +--- + +
+ +**Claude Code 很强大。GSD 让它可靠。** + +
\ No newline at end of file diff --git a/docs/zh-CN/USER-GUIDE.md b/docs/zh-CN/USER-GUIDE.md new file mode 100644 index 000000000..66cae6a0a --- /dev/null +++ b/docs/zh-CN/USER-GUIDE.md @@ -0,0 +1,492 @@ +# GSD 用户指南 + +工作流、故障排除和配置的详细参考。快速入门设置请参阅 [README](README.md)。 + +--- + +## 目录 + +- [工作流图解](#工作流图解) +- [命令参考](#命令参考) +- [配置参考](#配置参考) +- [使用示例](#使用示例) +- [故障排除](#故障排除) +- [恢复快速参考](#恢复快速参考) + +--- + +## 工作流图解 + +### 完整项目生命周期 + +``` + ┌──────────────────────────────────────────────────┐ + │ 新建项目 │ + │ /gsd:new-project │ + │ 提问 -> 研究 -> 需求 -> 路线图 │ + └─────────────────────────┬────────────────────────┘ + │ + ┌──────────────▼─────────────┐ + │ 每个阶段: │ + │ │ + │ ┌────────────────────┐ │ + │ │ /gsd:discuss-phase │ │ <- 锁定偏好 + │ └──────────┬─────────┘ │ + │ │ │ + │ ┌──────────▼─────────┐ │ + │ │ /gsd:plan-phase │ │ <- 研究 + 规划 + 验证 + │ └──────────┬─────────┘ │ + │ │ │ + │ ┌──────────▼─────────┐ │ + │ │ /gsd:execute-phase │ │ <- 并行执行 + │ └──────────┬─────────┘ │ + │ │ │ + │ ┌──────────▼─────────┐ │ + │ │ /gsd:verify-work │ │ <- 手动 UAT + │ └──────────┬─────────┘ │ + │ │ │ + │ 下一阶段?────────────┘ + │ │ 否 + └─────────────┼──────────────┘ + │ + ┌───────────────▼──────────────┐ + │ /gsd:audit-milestone │ + │ /gsd:complete-milestone │ + └───────────────┬──────────────┘ + │ + 另一个里程碑? + │ │ + 是 否 -> 完成! + │ + ┌───────▼──────────────┐ + │ /gsd:new-milestone │ + └──────────────────────┘ +``` + +### 规划代理协调 + +``` + /gsd:plan-phase N + │ + ├── 阶段研究员 (x4 并行) + │ ├── 技术栈研究员 + │ ├── 功能研究员 + │ ├── 架构研究员 + │ └── 陷阱研究员 + │ │ + │ ┌──────▼──────┐ + │ │ RESEARCH.md │ + │ └──────┬──────┘ + │ │ + │ ┌──────▼──────┐ + │ │ 规划者 │ <- 读取 PROJECT.md, REQUIREMENTS.md, + │ │ │ CONTEXT.md, RESEARCH.md + │ └──────┬──────┘ + │ │ + │ ┌──────▼───────────┐ ┌────────┐ + │ │ 计划检查器 │────>│ 通过? │ + │ └──────────────────┘ └───┬────┘ + │ │ + │ 是 │ 否 + │ │ │ │ + │ │ └───┘ (循环,最多 3 次) + │ │ + │ ┌─────▼──────┐ + │ │ PLAN 文件 │ + │ └────────────┘ + └── 完成 +``` + +### 验证架构 (Nyquist 层) + +在 plan-phase 研究期间,GSD 现在在任何代码编写之前将自动化测试覆盖率映射到每个阶段需求。这确保当 Claude 的执行者提交任务时,反馈机制已经存在可以在几秒钟内验证它。 + +研究员检测你现有的测试基础设施,将每个需求映射到特定的测试命令,并识别在实现开始之前必须创建的任何测试脚手架(波次 0 任务)。 + +计划检查器将其强制作为第 8 个验证维度:缺少自动化验证命令的计划将不会被批准。 + +**输出:** `{阶段}-VALIDATION.md` —— 阶段的反馈契约。 + +**禁用:** 在 `/gsd:settings` 中设置 `workflow.nyquist_validation: false`,用于测试基础设施不是重点的快速原型阶段。 + +### 追溯验证 (`/gsd:validate-phase`) + +对于在 Nyquist 验证存在之前执行的阶段,或只有传统测试套件的现有代码库,追溯审计并填补覆盖缺口: + +``` + /gsd:validate-phase N + | + +-- 检测状态 (VALIDATION.md 存在? SUMMARY.md 存在?) + | + +-- 发现: 扫描实现,将需求映射到测试 + | + +-- 分析缺口: 哪些需求缺少自动化验证? + | + +-- 呈现缺口计划供审批 + | + +-- 生成审计器: 生成测试,运行,调试(最多 3 次尝试) + | + +-- 更新 VALIDATION.md + | + +-- COMPLIANT -> 所有需求都有自动化检查 + +-- PARTIAL -> 部分缺口升级为仅手动 +``` + +审计器从不修改实现代码 —— 只修改测试文件和 VALIDATION.md。如果测试发现实现 bug,它会标记为升级让你处理。 + +**何时使用:** 在启用了 Nyquist 之前规划的阶段执行后,或在 `/gsd:audit-milestone` 发现 Nyquist 合规缺口后。 + +### 执行波次协调 + +``` + /gsd:execute-phase N + │ + ├── 分析计划依赖 + │ + ├── 波次 1 (独立计划): + │ ├── 执行者 A (全新 200K 上下文) -> 提交 + │ └── 执行者 B (全新 200K 上下文) -> 提交 + │ + ├── 波次 2 (依赖波次 1): + │ └── 执行者 C (全新 200K 上下文) -> 提交 + │ + └── 验证器 + └── 根据阶段目标检查代码库 + │ + ├── 通过 -> VERIFICATION.md (成功) + └── 失败 -> 问题记录到 /gsd:verify-work +``` + +### 现有代码库工作流 + +``` + /gsd:map-codebase + │ + ├── 技术栈映射器 -> codebase/STACK.md + ├── 架构映射器 -> codebase/ARCHITECTURE.md + ├── 约定映射器 -> codebase/CONVENTIONS.md + └── 关注点映射器 -> codebase/CONCERNS.md + │ + ┌───────▼──────────┐ + │ /gsd:new-project │ <- 问题聚焦于你正在添加的内容 + └──────────────────┘ +``` + +--- + +## 命令参考 + +### 核心工作流 + +| 命令 | 用途 | 何时使用 | +|---------|---------|-------------| +| `/gsd:new-project` | 完整项目初始化:提问、研究、需求、路线图 | 新项目开始时 | +| `/gsd:new-project --auto @idea.md` | 从文档自动初始化 | 有现成的 PRD 或想法文档 | +| `/gsd:discuss-phase [N]` | 捕获实现决策 | 规划前,塑造构建方式 | +| `/gsd:plan-phase [N]` | 研究 + 规划 + 验证 | 执行阶段前 | +| `/gsd:execute-phase ` | 在并行波次中执行所有计划 | 规划完成后 | +| `/gsd:verify-work [N]` | 带自动诊断的手动 UAT | 执行完成后 | +| `/gsd:audit-milestone` | 验证里程碑达到其完成定义 | 完成里程碑前 | +| `/gsd:complete-milestone` | 归档里程碑,标记发布 | 所有阶段已验证 | +| `/gsd:new-milestone [name]` | 开始下一个版本周期 | 完成里程碑后 | + +### 导航 + +| 命令 | 用途 | 何时使用 | +|---------|---------|-------------| +| `/gsd:progress` | 显示状态和下一步 | 任何时候 -- "我在哪?" | +| `/gsd:resume-work` | 从上次会话恢复完整上下文 | 开始新会话 | +| `/gsd:pause-work` | 保存上下文交接 | 阶段中途停止 | +| `/gsd:help` | 显示所有命令 | 快速参考 | +| `/gsd:update` | 更新 GSD 并预览变更日志 | 检查新版本 | +| `/gsd:join-discord` | 打开 Discord 社区邀请 | 问题或社区 | + +### 阶段管理 + +| 命令 | 用途 | 何时使用 | +|---------|---------|-------------| +| `/gsd:add-phase` | 向路线图追加新阶段 | 初始规划后范围增长 | +| `/gsd:insert-phase [N]` | 插入紧急工作(小数编号) | 里程碑中途紧急修复 | +| `/gsd:remove-phase [N]` | 删除未来阶段并重新编号 | 移除某个功能 | +| `/gsd:list-phase-assumptions [N]` | 预览 Claude 的预期方法 | 规划前,验证方向 | +| `/gsd:plan-milestone-gaps` | 为审计缺口创建阶段 | 审计发现缺失项后 | +| `/gsd:research-phase [N]` | 仅深度生态研究 | 复杂或不熟悉的领域 | + +### 现有代码库和工具 + +| 命令 | 用途 | 何时使用 | +|---------|---------|-------------| +| `/gsd:map-codebase` | 分析现有代码库 | 在现有代码上运行 `/gsd:new-project` 之前 | +| `/gsd:quick` | 带 GSD 保证的临时任务 | Bug 修复、小功能、配置更改 | +| `/gsd:debug [desc]` | 带持久状态的系统化调试 | 出问题时 | +| `/gsd:add-todo [desc]` | 捕获想法留待后用 | 会话期间想到什么 | +| `/gsd:check-todos` | 列出待处理事项 | 查看捕获的想法 | +| `/gsd:settings` | 配置工作流开关和模型配置 | 更改模型、切换代理 | +| `/gsd:set-profile ` | 快速切换配置 | 更改成本/质量权衡 | +| `/gsd:reapply-patches` | 更新后恢复本地修改 | 如果你有本地编辑,在 `/gsd:update` 后 | + +--- + +## 配置参考 + +GSD 在 `.planning/config.json` 中存储项目设置。在 `/gsd:new-project` 期间配置或稍后用 `/gsd:settings` 更新。 + +### 完整 config.json 模式 + +```json +{ + "mode": "interactive", + "granularity": "standard", + "model_profile": "balanced", + "planning": { + "commit_docs": true, + "search_gitignored": false + }, + "workflow": { + "research": true, + "plan_check": true, + "verifier": true, + "nyquist_validation": true + }, + "git": { + "branching_strategy": "none", + "phase_branch_template": "gsd/phase-{phase}-{slug}", + "milestone_branch_template": "gsd/{milestone}-{slug}" + } +} +``` + +### 核心设置 + +| 设置 | 选项 | 默认值 | 控制内容 | +|---------|---------|---------|------------------| +| `mode` | `interactive`, `yolo` | `interactive` | `yolo` 自动批准决策;`interactive` 每步确认 | +| `granularity` | `coarse`, `standard`, `fine` | `standard` | 阶段粒度:范围切分多细(3-5、5-8 或 8-12 个阶段) | +| `model_profile` | `quality`, `balanced`, `budget` | `balanced` | 每个代理的模型层级(见下表) | + +### 规划设置 + +| 设置 | 选项 | 默认值 | 控制内容 | +|---------|---------|---------|------------------| +| `planning.commit_docs` | `true`, `false` | `true` | `.planning/` 文件是否提交到 git | +| `planning.search_gitignored` | `true`, `false` | `false` | 在广泛搜索中添加 `--no-ignore` 以包含 `.planning/` | + +> **注意:** 如果 `.planning/` 在 `.gitignore` 中,无论配置值如何,`commit_docs` 自动为 `false`。 + +### 工作流开关 + +| 设置 | 选项 | 默认值 | 控制内容 | +|---------|---------|---------|------------------| +| `workflow.research` | `true`, `false` | `true` | 规划前的领域调查 | +| `workflow.plan_check` | `true`, `false` | `true` | 计划验证循环(最多 3 次迭代) | +| `workflow.verifier` | `true`, `false` | `true` | 根据阶段目标的执行后验证 | +| `workflow.nyquist_validation` | `true`, `false` | `true` | plan-phase 期间的验证架构研究;第 8 个计划检查维度 | + +在熟悉的领域或需要节省 token 时禁用这些以加速阶段。 + +### Git 分支 + +| 设置 | 选项 | 默认值 | 控制内容 | +|---------|---------|---------|------------------| +| `git.branching_strategy` | `none`, `phase`, `milestone` | `none` | 何时以及如何创建分支 | +| `git.phase_branch_template` | 模板字符串 | `gsd/phase-{phase}-{slug}` | 阶段策略的分支名 | +| `git.milestone_branch_template` | 模板字符串 | `gsd/{milestone}-{slug}` | 里程碑策略的分支名 | + +**分支策略说明:** + +| 策略 | 创建分支 | 范围 | 适用于 | +|----------|---------------|-------|----------| +| `none` | 从不 | N/A | 独立开发、简单项目 | +| `phase` | 每次 `execute-phase` | 每个阶段一个分支 | 每阶段代码审查、细粒度回滚 | +| `milestone` | 第一次 `execute-phase` | 所有阶段共享一个分支 | 发布分支、每个版本一个 PR | + +**模板变量:** `{phase}` = 零填充数字(如 "03"),`{slug}` = 小写连字符名称,`{milestone}` = 版本(如 "v1.0")。 + +### 模型配置(每个代理分解) + +| 代理 | `quality` | `balanced` | `budget` | +|-------|-----------|------------|----------| +| gsd-planner | Opus | Opus | Sonnet | +| gsd-roadmapper | Opus | Sonnet | Sonnet | +| gsd-executor | Opus | Sonnet | Sonnet | +| gsd-phase-researcher | Opus | Sonnet | Haiku | +| gsd-project-researcher | Opus | Sonnet | Haiku | +| gsd-research-synthesizer | Sonnet | Sonnet | Haiku | +| gsd-debugger | Opus | Sonnet | Sonnet | +| gsd-codebase-mapper | Sonnet | Haiku | Haiku | +| gsd-verifier | Sonnet | Sonnet | Haiku | +| gsd-plan-checker | Sonnet | Sonnet | Haiku | +| gsd-integration-checker | Sonnet | Sonnet | Haiku | + +**配置理念:** +- **quality** —— 所有决策代理使用 Opus,只读验证使用 Sonnet。有配额可用且工作关键时使用。 +- **balanced** —— 仅规划(架构决策发生的地方)使用 Opus,其他全部使用 Sonnet。这是默认,有充分理由。 +- **budget** —— 编写代码的使用 Sonnet,研究和验证使用 Haiku。大量工作或不太关键的阶段使用。 + +--- + +## 使用示例 + +### 新项目(完整周期) + +```bash +claude --dangerously-skip-permissions +/gsd:new-project # 回答问题,配置,批准路线图 +/clear +/gsd:discuss-phase 1 # 锁定你的偏好 +/gsd:plan-phase 1 # 研究 + 规划 + 验证 +/gsd:execute-phase 1 # 并行执行 +/gsd:verify-work 1 # 手动 UAT +/clear +/gsd:discuss-phase 2 # 对每个阶段重复 +... +/gsd:audit-milestone # 检查所有内容已发布 +/gsd:complete-milestone # 归档,标记,完成 +``` + +### 从现有文档创建新项目 + +```bash +/gsd:new-project --auto @prd.md # 从你的文档自动运行研究/需求/路线图 +/clear +/gsd:discuss-phase 1 # 从这里开始正常流程 +``` + +### 现有代码库 + +```bash +/gsd:map-codebase # 分析现有内容(并行代理) +/gsd:new-project # 问题聚焦于你正在添加的内容 +# (从这里开始正常阶段工作流) +``` + +### 快速 Bug 修复 + +```bash +/gsd:quick +> "修复移动端 Safari 上登录按钮无响应的问题" +``` + +### 中断后恢复 + +```bash +/gsd:progress # 查看你停在哪和接下来做什么 +# 或 +/gsd:resume-work # 从上次会话完整恢复上下文 +``` + +### 准备发布 + +```bash +/gsd:audit-milestone # 检查需求覆盖率,检测存根 +/gsd:plan-milestone-gaps # 如果审计发现缺口,创建阶段来填补 +/gsd:complete-milestone # 归档,标记,完成 +``` + +### 速度与质量预设 + +| 场景 | 模式 | 粒度 | 配置 | 研究 | 计划检查 | 验证器 | +|----------|------|-------|---------|----------|------------|----------| +| 原型开发 | `yolo` | `coarse` | `budget` | 关 | 关 | 关 | +| 正常开发 | `interactive` | `standard` | `balanced` | 开 | 开 | 开 | +| 生产环境 | `interactive` | `fine` | `quality` | 开 | 开 | 开 | + +### 里程碑中途范围变更 + +```bash +/gsd:add-phase # 向路线图追加新阶段 +# 或 +/gsd:insert-phase 3 # 在阶段 3 和 4 之间插入紧急工作 +# 或 +/gsd:remove-phase 7 # 移除阶段 7 并重新编号 +``` + +--- + +## 故障排除 + +### "项目已初始化" + +你运行了 `/gsd:new-project` 但 `.planning/PROJECT.md` 已存在。这是安全检查。如果你想重新开始,先删除 `.planning/` 目录。 + +### 长会话期间上下文退化 + +在主要命令之间清除上下文窗口:Claude Code 中的 `/clear`。GSD 设计围绕全新上下文 —— 每个子代理获得干净的 200K 窗口。如果主会话质量下降,清除并使用 `/gsd:resume-work` 或 `/gsd:progress` 恢复状态。 + +### 计划看起来错误或不一致 + +在规划前运行 `/gsd:discuss-phase [N]`。大多数计划质量问题来自 Claude 做出了 `CONTEXT.md` 本可以防止的假设。你也可以运行 `/gsd:list-phase-assumptions [N]` 在提交计划前查看 Claude 打算做什么。 + +### 执行失败或产生存根 + +检查计划是否太雄心勃勃。计划最多应有 2-3 个任务。如果任务太大,它们超出了单个上下文窗口可以可靠产生的内容。用更小的范围重新规划。 + +### 忘记你在哪里 + +运行 `/gsd:progress`。它读取所有状态文件,准确告诉你位置和下一步。 + +### 执行后需要更改某些内容 + +不要重新运行 `/gsd:execute-phase`。使用 `/gsd:quick` 进行针对性修复,或用 `/gsd:verify-work` 通过 UAT 系统识别和修复问题。 + +### 模型成本太高 + +切换到 budget 配置:`/gsd:set-profile budget`。如果领域对你(或 Claude)熟悉,通过 `/gsd:settings` 禁用研究和计划检查代理。 + +### 处理敏感/私有项目 + +在 `/gsd:new-project` 期间或通过 `/gsd:settings` 设置 `commit_docs: false`。将 `.planning/` 添加到 `.gitignore`。规划工件保留在本地,从不接触 git。 + +### GSD 更新覆盖了我的本地更改 + +从 v1.17 开始,安装程序将本地修改的文件备份到 `gsd-local-patches/`。运行 `/gsd:reapply-patches` 将你的更改合并回来。 + +### 子代理似乎失败但工作已完成 + +存在 Claude Code 分类 bug 的已知解决方法。GSD 的编排器(execute-phase、quick)在报告失败前抽查实际输出。如果你看到失败消息但提交已创建,检查 `git log` —— 工作可能已成功。 + +--- + +## 恢复快速参考 + +| 问题 | 解决方案 | +|---------|----------| +| 丢失上下文 / 新会话 | `/gsd:resume-work` 或 `/gsd:progress` | +| 阶段出错 | `git revert` 阶段提交,然后重新规划 | +| 需要更改范围 | `/gsd:add-phase`、`/gsd:insert-phase` 或 `/gsd:remove-phase` | +| 里程碑审计发现缺口 | `/gsd:plan-milestone-gaps` | +| 出问题了 | `/gsd:debug "描述"` | +| 快速针对性修复 | `/gsd:quick` | +| 计划与你的愿景不符 | `/gsd:discuss-phase [N]` 然后重新规划 | +| 成本过高 | `/gsd:set-profile budget` 和 `/gsd:settings` 关闭代理 | +| 更新破坏了本地更改 | `/gsd:reapply-patches` | + +--- + +## 项目文件结构 + +供参考,这是 GSD 在你的项目中创建的内容: + +``` +.planning/ + PROJECT.md # 项目愿景和上下文(始终加载) + REQUIREMENTS.md # 界定 v1/v2 需求及 ID + ROADMAP.md # 带状态跟踪的阶段分解 + STATE.md # 决策、阻塞项、会话记忆 + config.json # 工作流配置 + MILESTONES.md # 已完成里程碑归档 + research/ # 来自 /gsd:new-project 的领域研究 + todos/ + pending/ # 等待处理的捕获想法 + done/ # 已完成的待办事项 + debug/ # 活跃调试会话 + resolved/ # 已归档的调试会话 + codebase/ # 现有代码库映射(来自 /gsd:map-codebase) + phases/ + XX-phase-name/ + XX-YY-PLAN.md # 原子执行计划 + XX-YY-SUMMARY.md # 执行结果和决策 + CONTEXT.md # 你的实现偏好 + RESEARCH.md # 生态研究发现 + VERIFICATION.md # 执行后验证结果 +``` \ No newline at end of file diff --git a/docs/zh-CN/references/checkpoints.md b/docs/zh-CN/references/checkpoints.md new file mode 100644 index 000000000..a41a22edb --- /dev/null +++ b/docs/zh-CN/references/checkpoints.md @@ -0,0 +1,450 @@ +# 检查点 + +计划自主执行。检查点用于规范化需要人工验证或决策的交互点。 + +**核心原则:** Claude 用 CLI/API 自动化一切。检查点用于验证和决策,而非手动工作。 + +**黄金法则:** +1. **如果 Claude 能运行,Claude 就运行** - 绝不让用户执行 CLI 命令、启动服务器或运行构建 +2. **Claude 设置验证环境** - 启动开发服务器、填充数据库、配置环境变量 +3. **用户只做需要人工判断的事** - 视觉检查、UX 评估、"这个感觉对吗?" +4. **密钥来自用户,自动化来自 Claude** - 询问 API 密钥,然后 Claude 通过 CLI 使用它们 +5. **自动模式绕过验证/决策检查点** — 当 config 中 `workflow._auto_chain_active` 或 `workflow.auto_advance` 为 true 时:human-verify 自动批准,decision 自动选择第一个选项,human-action 仍会停止(认证门控无法自动化) + +## 检查点类型 + +### checkpoint:human-verify(最常见 - 90%) + +**何时使用:** Claude 完成自动化工作,人工确认其正常工作。 + +**用于:** +- 视觉 UI 检查(布局、样式、响应式) +- 交互流程(点击向导、测试用户流程) +- 功能验证(功能按预期工作) +- 音频/视频播放质量 +- 动画流畅度 +- 无障碍测试 + +**结构:** +```xml + + [Claude 自动化并部署/构建的内容] + + [测试的确切步骤 - URL、命令、预期行为] + + [如何继续 - "approved"、"yes" 或描述问题] + +``` + +**示例:UI 组件(展示关键模式:Claude 在检查点之前启动服务器)** +```xml + + 构建响应式仪表板布局 + src/components/Dashboard.tsx, src/app/dashboard/page.tsx + 创建带侧边栏、标题和内容区域的仪表板。使用 Tailwind 响应式类处理移动端。 + npm run build 成功,无 TypeScript 错误 + 仪表板组件构建无错误 + + + + 启动开发服务器用于验证 + 在后台运行 `npm run dev`,等待 "ready" 消息,捕获端口 + curl http://localhost:3000 返回 200 + 开发服务器运行于 http://localhost:3000 + + + + 响应式仪表板布局 - 开发服务器运行于 http://localhost:3000 + + 访问 http://localhost:3000/dashboard 并验证: + 1. 桌面端 (>1024px): 左侧边栏,右侧内容,顶部标题 + 2. 平板端 (768px): 侧边栏折叠为汉堡菜单 + 3. 移动端 (375px): 单列布局,出现底部导航 + 4. 任何尺寸无布局偏移或水平滚动 + + 输入 "approved" 或描述布局问题 + +``` + +### checkpoint:decision(9%) + +**何时使用:** 人工必须做出影响实现方向的选择。 + +**用于:** +- 技术选型(哪个认证提供商、哪个数据库) +- 架构决策(monorepo 还是独立仓库) +- 设计选择(配色方案、布局方式) +- 功能优先级(构建哪个变体) +- 数据模型决策(模式结构) + +**结构:** +```xml + + [正在决策的内容] + [为什么这个决策重要] + + + + + [如何表明选择] + +``` + +**示例:认证提供商选择** +```xml + + 选择认证提供商 + + 应用需要用户认证。三个可靠选项各有权衡。 + + + + + + + 选择:supabase、clerk 或 nextauth + +``` + +### checkpoint:human-action(1% - 罕见) + +**何时使用:** 操作没有 CLI/API 且需要仅人工交互,或者 Claude 在自动化过程中遇到认证门控。 + +**仅用于:** +- **认证门控** - Claude 尝试了 CLI/API 但需要凭证(这不是失败) +- 邮箱验证链接(点击邮件) +- 短信两步验证码(手机验证) +- 人工账户审批(平台需要人工审核) +- 信用卡 3D Secure 流程(基于 Web 的支付授权) +- OAuth 应用审批(基于 Web 的审批) + +**不要用于预定的手动工作:** +- 部署(使用 CLI - 如需要则认证门控) +- 创建 webhooks/数据库(使用 API/CLI - 如需要则认证门控) +- 运行构建/测试(使用 Bash 工具) +- 创建文件(使用 Write 工具) + +**结构:** +```xml + + [人工必须做什么 - Claude 已完成所有可自动化的] + + [Claude 已自动化的内容] + [需要人工操作的一件事] + + [Claude 之后可以检查的内容] + [如何继续] + +``` + +**示例:认证门控(动态检查点)** +```xml + + 部署到 Vercel + .vercel/, vercel.json + 运行 `vercel --yes` 进行部署 + vercel ls 显示部署,curl 返回 200 + + + + + + 认证 Vercel CLI 以便我继续部署 + + 我尝试部署但收到认证错误。 + 运行:vercel login + 这将打开你的浏览器 - 完成认证流程。 + + vercel whoami 返回你的账户邮箱 + 认证完成后输入 "done" + + + + + + 重试 Vercel 部署 + 运行 `vercel --yes`(已认证) + vercel ls 显示部署,curl 返回 200 + +``` + +**关键区别:** 认证门控是 Claude 遇到认证错误时动态创建的。不是预定的 — Claude 先自动化,只有在被阻止时才请求凭证。 + +## 执行协议 + +当 Claude 遇到 `type="checkpoint:*"` 时: + +1. **立即停止** - 不继续下一个任务 +2. **清晰显示检查点** 使用下面的格式 +3. **等待用户响应** - 不幻想完成 +4. **如可能则验证** - 检查文件、运行测试、任何指定的内容 +5. **恢复执行** - 仅在确认后继续下一个任务 + +**对于 checkpoint:human-verify:** +``` +╔═══════════════════════════════════════════════════════╗ +║ CHECKPOINT: 需要验证 ║ +╚═══════════════════════════════════════════════════════╝ + +进度: 5/8 任务完成 +任务: 响应式仪表板布局 + +已构建: /dashboard 的响应式仪表板 + +如何验证: + 1. 访问: http://localhost:3000/dashboard + 2. 桌面端 (>1024px): 侧边栏可见,内容填充剩余空间 + 3. 平板端 (768px): 侧边栏折叠为图标 + 4. 移动端 (375px): 侧边栏隐藏,出现汉堡菜单 + +──────────────────────────────────────────────────────── +→ 你的操作: 输入 "approved" 或描述问题 +──────────────────────────────────────────────────────── +``` + +**对于 checkpoint:decision:** +``` +╔═══════════════════════════════════════════════════════╗ +║ CHECKPOINT: 需要决策 ║ +╚═══════════════════════════════════════════════════════╝ + +进度: 2/6 任务完成 +任务: 选择认证提供商 + +决策: 我们应该使用哪个认证提供商? + +上下文: 需要用户认证。三个选项各有权衡。 + +选项: + 1. supabase - 与我们的数据库内置集成,免费额度 + 优点: 行级安全集成,慷慨的免费额度 + 缺点: UI 定制性较差,生态锁定 + + 2. clerk - 最佳 DX,10k 用户后付费 + 优点: 精美的预构建 UI,优秀文档 + 缺点: 供应商锁定,规模化时价格问题 + + 3. nextauth - 自托管,最大控制权 + 优点: 免费,无供应商锁定,广泛采用 + 缺点: 更多设置工作,自行 DIY 安全更新 + +──────────────────────────────────────────────────────── +→ 你的操作: 选择 supabase、clerk 或 nextauth +──────────────────────────────────────────────────────── +``` + +## 认证门控 + +**认证门控 = Claude 尝试了 CLI/API,收到认证错误。** 不是失败 — 是需要人工输入来解除阻止的门控。 + +**模式:** Claude 尝试自动化 → 认证错误 → 创建 checkpoint:human-action → 用户认证 → Claude 重试 → 继续 + +**门控协议:** +1. 认识到这不是失败 - 缺少认证是正常的 +2. 停止当前任务 - 不要反复重试 +3. 动态创建 checkpoint:human-action +4. 提供确切的认证步骤 +5. 验证认证有效 +6. 重试原始任务 +7. 正常继续 + +**关键区别:** +- 预定的检查点:"我需要你做 X"(错误 - Claude 应该自动化) +- 认证门控:"我尝试自动化 X 但需要凭证"(正确 - 解除自动化阻止) + +## 自动化参考 + +**规则:** 如果有 CLI/API,Claude 就做。绝不让人工执行可自动化的工作。 + +### 服务 CLI 参考 + +| 服务 | CLI/API | 关键命令 | 认证门控 | +|------|---------|----------|----------| +| Vercel | `vercel` | `--yes`, `env add`, `--prod`, `ls` | `vercel login` | +| Railway | `railway` | `init`, `up`, `variables set` | `railway login` | +| Fly | `fly` | `launch`, `deploy`, `secrets set` | `fly auth login` | +| Stripe | `stripe` + API | `listen`, `trigger`, API 调用 | .env 中的 API key | +| Supabase | `supabase` | `init`, `link`, `db push`, `gen types` | `supabase login` | +| Upstash | `upstash` | `redis create`, `redis get` | `upstash auth login` | +| PlanetScale | `pscale` | `database create`, `branch create` | `pscale auth login` | +| GitHub | `gh` | `repo create`, `pr create`, `secret set` | `gh auth login` | +| Node | `npm`/`pnpm` | `install`, `run build`, `test`, `run dev` | N/A | +| Xcode | `xcodebuild` | `-project`, `-scheme`, `build`, `test` | N/A | +| Convex | `npx convex` | `dev`, `deploy`, `env set`, `env get` | `npx convex login` | + +### 环境变量自动化 + +**Env 文件:** 使用 Write/Edit 工具。绝不让用户手动创建 .env。 + +**通过 CLI 的仪表板环境变量:** + +| 平台 | CLI 命令 | 示例 | +|------|----------|------| +| Convex | `npx convex env set` | `npx convex env set OPENAI_API_KEY sk-...` | +| Vercel | `vercel env add` | `vercel env add STRIPE_KEY production` | +| Railway | `railway variables set` | `railway variables set API_KEY=value` | +| Fly | `fly secrets set` | `fly secrets set DATABASE_URL=...` | +| Supabase | `supabase secrets set` | `supabase secrets set MY_SECRET=value` | + +### 开发服务器自动化 + +| 框架 | 启动命令 | 就绪信号 | 默认 URL | +|------|----------|----------|----------| +| Next.js | `npm run dev` | "Ready in" 或 "started server" | http://localhost:3000 | +| Vite | `npm run dev` | "ready in" | http://localhost:5173 | +| Convex | `npx convex dev` | "Convex functions ready" | N/A(仅后端)| +| Express | `npm start` | "listening on port" | http://localhost:3000 | +| Django | `python manage.py runserver` | "Starting development server" | http://localhost:8000 | + +**服务器生命周期:** +```bash +# 后台运行,捕获 PID +npm run dev & +DEV_SERVER_PID=$! + +# 等待就绪(最多 30s) +timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; done' +``` + +**端口冲突:** 终止陈旧进程(`lsof -ti:3000 | xargs kill`)或使用备用端口(`--port 3001`)。 + +**服务器保持运行** 直到检查点结束。仅在计划完成、切换到生产环境或端口需要用于不同服务时终止。 + +### CLI 安装处理 + +| CLI | 自动安装? | 命令 | +|-----|------------|------| +| npm/pnpm/yarn | 否 - 询问用户 | 用户选择包管理器 | +| vercel | 是 | `npm i -g vercel` | +| gh (GitHub) | 是 | `brew install gh` (macOS) 或 `apt install gh` (Linux) | +| stripe | 是 | `npm i -g stripe` | +| supabase | 是 | `npm i -g supabase` | +| convex | 否 - 使用 npx | `npx convex`(无需安装)| +| fly | 是 | `brew install flyctl` 或 curl 安装器 | +| railway | 是 | `npm i -g @railway/cli` | + +**协议:** 尝试命令 → "command not found" → 可自动安装?→ 是:静默安装,重试 → 否:检查点请求用户安装。 + +## 检查点前自动化失败处理 + +| 失败 | 响应 | +|------|------| +| 服务器无法启动 | 检查错误,修复问题,重试(不进入检查点)| +| 端口被占用 | 终止陈旧进程或使用备用端口 | +| 缺少依赖 | 运行 `npm install`,重试 | +| 构建错误 | 先修复错误(是 bug,不是检查点问题)| +| 认证错误 | 创建认证门控检查点 | +| 网络超时 | 带退避重试,如果持续则检查点 | + +**绝不呈现验证环境损坏的检查点。** 如果 `curl localhost:3000` 失败,不要让用户"访问 localhost:3000"。 + +## 可自动化快速参考 + +| 操作 | 可自动化?| Claude 做?| +|------|------------|------------| +| 部署到 Vercel | 是 (`vercel`) | 是 | +| 创建 Stripe webhook | 是 (API) | 是 | +| 写入 .env 文件 | 是 (Write 工具) | 是 | +| 创建 Upstash DB | 是 (`upstash`) | 是 | +| 运行测试 | 是 (`npm test`) | 是 | +| 启动开发服务器 | 是 (`npm run dev`) | 是 | +| 添加环境变量到 Convex | 是 (`npx convex env set`) | 是 | +| 添加环境变量到 Vercel | 是 (`vercel env add`) | 是 | +| 填充数据库 | 是 (CLI/API) | 是 | +| 点击邮件验证链接 | 否 | 否 | +| 输入带 3DS 的信用卡 | 否 | 否 | +| 在浏览器中完成 OAuth | 否 | 否 | +| 视觉验证 UI 是否正确 | 否 | 否 | +| 测试交互式用户流程 | 否 | 否 | + +## 反模式 + +### ❌ 错误:让用户启动开发服务器 +```xml + + 仪表板组件 + + 1. 运行: npm run dev + 2. 访问: http://localhost:3000/dashboard + 3. 检查布局是否正确 + + +``` +**为什么错误:** Claude 可以运行 `npm run dev`。用户应该只访问 URL,不执行命令。 + +### ✅ 正确:Claude 启动服务器,用户访问 +```xml + + 启动开发服务器 + 在后台运行 `npm run dev` + curl localhost:3000 返回 200 + + + + http://localhost:3000/dashboard 的仪表板(服务器运行中) + + 访问 http://localhost:3000/dashboard 并验证: + 1. 布局匹配设计 + 2. 无控制台错误 + + +``` + +### ❌ 错误:让用户部署 / ✅ 正确:Claude 自动化 +```xml + + + 部署到 Vercel + 访问 vercel.com/new → 导入仓库 → 点击部署 → 复制 URL + + + + + 部署到 Vercel + 运行 `vercel --yes`。捕获 URL。 + vercel ls 显示部署,curl 返回 200 + + + + 已部署到 {url} + 访问 {url},检查首页加载 + 输入 "approved" + +``` + +## 摘要 + +检查点规范化人工介入点用于验证和决策,而非手动工作。 + +**黄金法则:** 如果 Claude 能自动化它,Claude 就必须自动化它。 + +**检查点优先级:** +1. **checkpoint:human-verify**(90%)- Claude 自动化一切,人工确认视觉/功能正确性 +2. **checkpoint:decision**(9%)- 人工做出架构/技术选择 +3. **checkpoint:human-action**(1%)- 真正无法避免的、没有 API/CLI 的手动步骤 + +**何时不用检查点:** +- Claude 可以编程验证的事情(测试、构建) +- 文件操作(Claude 可以读取文件) +- 代码正确性(测试和静态分析) +- 任何可通过 CLI/API 自动化的内容 \ No newline at end of file diff --git a/docs/zh-CN/references/continuation-format.md b/docs/zh-CN/references/continuation-format.md new file mode 100644 index 000000000..674b3e770 --- /dev/null +++ b/docs/zh-CN/references/continuation-format.md @@ -0,0 +1,249 @@ +# 续接格式 + +完成命令或工作流后展示下一步的标准格式。 + +## 核心结构 + +``` +--- + +## ▶ 下一步 + +**{标识符}: {名称}** — {单行描述} + +`{可复制粘贴的命令}` + +`/clear` 优先 → 全新上下文窗口 + +--- + +**也可选:** +- `{备选项 1}` — 描述 +- `{备选项 2}` — 描述 + +--- +``` + +## 格式规则 + +1. **始终展示它是什么** — 名称 + 描述,绝不仅仅是一个命令路径 +2. **从源文件拉取上下文** — ROADMAP.md 用于阶段,PLAN.md `` 用于计划 +3. **命令用内联代码** — 反引号,易于复制粘贴,渲染为可点击链接 +4. **`/clear` 说明** — 始终包含,保持简洁但解释原因 +5. **用"也可选"而非"其他选项"** — 听起来更像应用 +6. **视觉分隔符** — 上下用 `---` 使其突出 + +## 变体 + +### 执行下一个计划 + +``` +--- + +## ▶ 下一步 + +**02-03: 刷新令牌轮换** — 添加带滑动过期的 /api/auth/refresh + +`/gsd:execute-phase 2` + +`/clear` 优先 → 全新上下文窗口 + +--- + +**也可选:** +- 执行前审查计划 +- `/gsd:list-phase-assumptions 2` — 检查假设 + +--- +``` + +### 执行阶段中最后一个计划 + +添加注释说明这是最后一个计划以及接下来是什么: + +``` +--- + +## ▶ 下一步 + +**02-03: 刷新令牌轮换** — 添加带滑动过期的 /api/auth/refresh +阶段 2 的最后一个计划 + +`/gsd:execute-phase 2` + +`/clear` 优先 → 全新上下文窗口 + +--- + +**完成后:** +- 阶段 2 → 阶段 3 过渡 +- 下一步:**阶段 3: 核心功能** — 用户仪表板和设置 + +--- +``` + +### 规划阶段 + +``` +--- + +## ▶ 下一步 + +**阶段 2: 认证** — 带刷新令牌的 JWT 登录流程 + +`/gsd:plan-phase 2` + +`/clear` 优先 → 全新上下文窗口 + +--- + +**也可选:** +- `/gsd:discuss-phase 2` — 先收集上下文 +- `/gsd:research-phase 2` — 调查未知项 +- 审查路线图 + +--- +``` + +### 阶段完成,准备下一步 + +在下一步操作前显示完成状态: + +``` +--- + +## ✓ 阶段 2 完成 + +3/3 计划已执行 + +## ▶ 下一步 + +**阶段 3: 核心功能** — 用户仪表板、设置和数据导出 + +`/gsd:plan-phase 3` + +`/clear` 优先 → 全新上下文窗口 + +--- + +**也可选:** +- `/gsd:discuss-phase 3` — 先收集上下文 +- `/gsd:research-phase 3` — 调查未知项 +- 回顾阶段 2 构建的内容 + +--- +``` + +### 多个同等选项 + +当没有明确的主要操作时: + +``` +--- + +## ▶ 下一步 + +**阶段 3: 核心功能** — 用户仪表板、设置和数据导出 + +**直接规划:** `/gsd:plan-phase 3` + +**先讨论上下文:** `/gsd:discuss-phase 3` + +**研究未知项:** `/gsd:research-phase 3` + +`/clear` 优先 → 全新上下文窗口 + +--- +``` + +### 里程碑完成 + +``` +--- + +## 🎉 里程碑 v1.0 完成 + +全部 4 个阶段已发布 + +## ▶ 下一步 + +**开始 v1.1** — 提问 → 研究 → 需求 → 路线图 + +`/gsd:new-milestone` + +`/clear` 优先 → 全新上下文窗口 + +--- +``` + +## 拉取上下文 + +### 用于阶段(从 ROADMAP.md): + +```markdown +### 阶段 2: 认证 +**目标**: 带刷新令牌的 JWT 登录流程 +``` + +提取:`**阶段 2: 认证** — 带刷新令牌的 JWT 登录流程` + +### 用于计划(从 ROADMAP.md): + +```markdown +计划: +- [ ] 02-03: 添加刷新令牌轮换 +``` + +或从 PLAN.md ``: + +```xml + +添加带滑动过期窗口的刷新令牌轮换。 + +目的: 在不影响安全性的前提下延长会话生命周期。 + +``` + +提取:`**02-03: 刷新令牌轮换** — 添加带滑动过期的 /api/auth/refresh` + +## 反模式 + +### 不要:仅命令(无上下文) + +``` +## 继续 + +运行 `/clear`,然后粘贴: +/gsd:execute-phase 2 +``` + +用户不知道 02-03 是关于什么的。 + +### 不要:缺少 /clear 说明 + +``` +`/gsd:plan-phase 3` + +先运行 /clear。 +``` + +没有解释原因。用户可能跳过。 + +### 不要:"其他选项" 措辞 + +``` +其他选项: +- 审查路线图 +``` + +听起来像是事后补充。用"也可选:"替代。 + +### 不要:用围栏代码块展示命令 + +``` +``` +/gsd:plan-phase 3 +``` +``` + +模板内的围栏代码块会造成嵌套歧义。用内联反引号替代。 \ No newline at end of file diff --git a/docs/zh-CN/references/decimal-phase-calculation.md b/docs/zh-CN/references/decimal-phase-calculation.md new file mode 100644 index 000000000..d25f46870 --- /dev/null +++ b/docs/zh-CN/references/decimal-phase-calculation.md @@ -0,0 +1,65 @@ +# 小数阶段计算 + +为紧急插入计算下一个小数阶段编号。 + +## 使用 gsd-tools + +```bash +# 获取阶段 6 之后的下一个小数阶段 +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" phase next-decimal 6 +``` + +输出: +```json +{ + "found": true, + "base_phase": "06", + "next": "06.1", + "existing": [] +} +``` + +已有小数时: +```json +{ + "found": true, + "base_phase": "06", + "next": "06.3", + "existing": ["06.1", "06.2"] +} +``` + +## 提取值 + +```bash +DECIMAL_INFO=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" phase next-decimal "${AFTER_PHASE}") +DECIMAL_PHASE=$(printf '%s\n' "$DECIMAL_INFO" | jq -r '.next') +BASE_PHASE=$(printf '%s\n' "$DECIMAL_INFO" | jq -r '.base_phase') +``` + +或使用 --raw 标志: +```bash +DECIMAL_PHASE=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" phase next-decimal "${AFTER_PHASE}" --raw) +# 返回: 06.1 +``` + +## 示例 + +| 已有阶段 | 下一个阶段 | +|----------|------------| +| 仅 06 | 06.1 | +| 06, 06.1 | 06.2 | +| 06, 06.1, 06.2 | 06.3 | +| 06, 06.1, 06.3(有空缺)| 06.4 | + +## 目录命名 + +小数阶段目录使用完整的小数编号: + +```bash +SLUG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" generate-slug "$DESCRIPTION" --raw) +PHASE_DIR=".planning/phases/${DECIMAL_PHASE}-${SLUG}" +mkdir -p "$PHASE_DIR" +``` + +示例:`.planning/phases/06.1-fix-critical-auth-bug/` \ No newline at end of file diff --git a/docs/zh-CN/references/git-integration.md b/docs/zh-CN/references/git-integration.md new file mode 100644 index 000000000..8fb58d0a6 --- /dev/null +++ b/docs/zh-CN/references/git-integration.md @@ -0,0 +1,248 @@ + +GSD 框架的 Git 集成。 + + + + +**提交结果,而非过程。** + +git 日志应该读起来像是发布内容的变更日志,而不是规划活动的日记。 + + + + +| 事件 | 提交? | 原因 | +| ----------------------- | ------- | ------------------------------------------------ | +| BRIEF + ROADMAP 创建 | 是 | 项目初始化 | +| PLAN.md 创建 | 否 | 中间产物 - 与计划完成一起提交 | +| RESEARCH.md 创建 | 否 | 中间产物 | +| DISCOVERY.md 创建 | 否 | 中间产物 | +| **任务完成** | 是 | 原子工作单元(每个任务 1 个提交) | +| **计划完成** | 是 | 元数据提交(SUMMARY + STATE + ROADMAP) | +| 交接创建 | 是 | WIP 状态保留 | + + + + + +```bash +[ -d .git ] && echo "GIT_EXISTS" || echo "NO_GIT" +``` + +如果 NO_GIT:静默运行 `git init`。GSD 项目总是有自己的仓库。 + + + + + +## 项目初始化(brief + roadmap 一起) + +``` +docs: initialize [project-name] ([N] phases) + +[PROJECT.md 中的一句话描述] + +Phases: +1. [phase-name]: [goal] +2. [phase-name]: [goal] +3. [phase-name]: [goal] +``` + +提交内容: + +```bash +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: initialize [project-name] ([N] phases)" --files .planning/ +``` + + + + +## 任务完成(计划执行期间) + +每个任务在完成后立即获得自己的提交。 + +``` +{type}({phase}-{plan}): {task-name} + +- [关键变更 1] +- [关键变更 2] +- [关键变更 3] +``` + +**提交类型:** +- `feat` - 新功能/功能 +- `fix` - Bug 修复 +- `test` - 仅测试(TDD RED 阶段) +- `refactor` - 代码清理(TDD REFACTOR 阶段) +- `perf` - 性能改进 +- `chore` - 依赖、配置、工具 + +**示例:** + +```bash +# 标准任务 +git add src/api/auth.ts src/types/user.ts +git commit -m "feat(08-02): create user registration endpoint + +- POST /auth/register validates email and password +- Checks for duplicate users +- Returns JWT token on success +" + +# TDD 任务 - RED 阶段 +git add src/__tests__/jwt.test.ts +git commit -m "test(07-02): add failing test for JWT generation + +- Tests token contains user ID claim +- Tests token expires in 1 hour +- Tests signature verification +" + +# TDD 任务 - GREEN 阶段 +git add src/utils/jwt.ts +git commit -m "feat(07-02): implement JWT generation + +- Uses jose library for signing +- Includes user ID and expiry claims +- Signs with HS256 algorithm +" +``` + + + + +## 计划完成(所有任务完成后) + +所有任务提交后,最后一个元数据提交捕获计划完成。 + +``` +docs({phase}-{plan}): complete [plan-name] plan + +Tasks completed: [N]/[N] +- [Task 1 name] +- [Task 2 name] +- [Task 3 name] + +SUMMARY: .planning/phases/XX-name/{phase}-{plan}-SUMMARY.md +``` + +提交内容: + +```bash +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs({phase}-{plan}): complete [plan-name] plan" --files .planning/phases/XX-name/{phase}-{plan}-PLAN.md .planning/phases/XX-name/{phase}-{plan}-SUMMARY.md .planning/STATE.md .planning/ROADMAP.md +``` + +**注意:** 代码文件不包含 - 已按任务提交。 + + + + +## 交接(WIP) + +``` +wip: [phase-name] paused at task [X]/[Y] + +Current: [task name] +[如果阻塞:] Blocked: [reason] +``` + +提交内容: + +```bash +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "wip: [phase-name] paused at task [X]/[Y]" --files .planning/ +``` + + + + + + +**旧方法(每个计划提交):** +``` +a7f2d1 feat(checkout): Stripe payments with webhook verification +3e9c4b feat(products): catalog with search, filters, and pagination +8a1b2c feat(auth): JWT with refresh rotation using jose +5c3d7e feat(foundation): Next.js 15 + Prisma + Tailwind scaffold +2f4a8d docs: initialize ecommerce-app (5 phases) +``` + +**新方法(每个任务提交):** +``` +# Phase 04 - Checkout +1a2b3c docs(04-01): complete checkout flow plan +4d5e6f feat(04-01): add webhook signature verification +7g8h9i feat(04-01): implement payment session creation +0j1k2l feat(04-01): create checkout page component + +# Phase 03 - Products +3m4n5o docs(03-02): complete product listing plan +6p7q8r feat(03-02): add pagination controls +9s0t1u feat(03-02): implement search and filters +2v3w4x feat(03-01): create product catalog schema + +# Phase 02 - Auth +5y6z7a docs(02-02): complete token refresh plan +8b9c0d feat(02-02): implement refresh token rotation +1e2f3g test(02-02): add failing test for token refresh +4h5i6j docs(02-01): complete JWT setup plan +7k8l9m feat(02-01): add JWT generation and validation +0n1o2p chore(02-01): install jose library + +# Phase 01 - Foundation +3q4r5s docs(01-01): complete scaffold plan +6t7u8v feat(01-01): configure Tailwind and globals +9w0x1y feat(01-01): set up Prisma with database +2z3a4b feat(01-01): create Next.js 15 project + +# Initialization +5c6d7e docs: initialize ecommerce-app (5 phases) +``` + +每个计划产生 2-4 个提交(任务 + 元数据)。清晰、细粒度、可 bisect。 + + + + + +**仍不要提交(中间产物):** +- PLAN.md 创建(与计划完成一起提交) +- RESEARCH.md(中间产物) +- DISCOVERY.md(中间产物) +- 小的规划调整 +- "Fixed typo in roadmap" + +**要提交(结果):** +- 每个任务完成(feat/fix/test/refactor) +- 计划完成元数据(docs) +- 项目初始化(docs) + +**关键原则:** 提交可工作的代码和已发布的结果,而非规划过程。 + + + + + +## 为什么使用每任务提交? + +**AI 上下文工程:** +- Git 历史成为未来 Claude 会话的主要上下文源 +- `git log --grep="{phase}-{plan}"` 显示计划的所有工作 +- `git diff ^..` 显示每个任务的确切变更 +- 减少对解析 SUMMARY.md 的依赖 = 更多上下文用于实际工作 + +**失败恢复:** +- 任务 1 已提交 ✅,任务 2 失败 ❌ +- 下次会话中的 Claude:看到任务 1 完成,可以重试任务 2 +- 可以 `git reset --hard` 到最后一个成功的任务 + +**调试:** +- `git bisect` 找到确切的失败任务,而不仅仅是失败计划 +- `git blame` 将行追溯到特定任务上下文 +- 每个提交独立可回滚 + +**可观察性:** +- 独立开发者 + Claude 工作流受益于细粒度归因 +- 原子提交是 git 最佳实践 +- 当消费者是 Claude 而非人类时,"提交噪音"无关紧要 + + \ No newline at end of file diff --git a/docs/zh-CN/references/git-planning-commit.md b/docs/zh-CN/references/git-planning-commit.md new file mode 100644 index 000000000..fd128ba6c --- /dev/null +++ b/docs/zh-CN/references/git-planning-commit.md @@ -0,0 +1,38 @@ +# Git 规划提交 + +使用 gsd-tools CLI 提交规划工件,它会自动检查 `commit_docs` 配置和 gitignore 状态。 + +## 通过 CLI 提交 + +始终使用 `gsd-tools.cjs commit` 处理 `.planning/` 文件 — 它会自动处理 `commit_docs` 和 gitignore 检查: + +```bash +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs({scope}): {description}" --files .planning/STATE.md .planning/ROADMAP.md +``` + +如果 `commit_docs` 为 `false` 或 `.planning/` 被 gitignore,CLI 会返回 `skipped`(带原因)。无需手动条件检查。 + +## 修改上次提交 + +将 `.planning/` 文件变更合并到上次提交: + +```bash +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "" --files .planning/codebase/*.md --amend +``` + +## 提交消息模式 + +| 命令 | 范围 | 示例 | +|------|------|------| +| plan-phase | phase | `docs(phase-03): create authentication plans` | +| execute-phase | phase | `docs(phase-03): complete authentication phase` | +| new-milestone | milestone | `docs: start milestone v1.1` | +| remove-phase | chore | `chore: remove phase 17 (dashboard)` | +| insert-phase | phase | `docs: insert phase 16.1 (critical fix)` | +| add-phase | phase | `docs: add phase 07 (settings page)` | + +## 何时跳过 + +- config 中 `commit_docs: false` +- `.planning/` 被 gitignore +- 无变更可提交(用 `git status --porcelain .planning/` 检查) \ No newline at end of file diff --git a/docs/zh-CN/references/model-profile-resolution.md b/docs/zh-CN/references/model-profile-resolution.md new file mode 100644 index 000000000..777a9a4b0 --- /dev/null +++ b/docs/zh-CN/references/model-profile-resolution.md @@ -0,0 +1,34 @@ +# 模型配置解析 + +在编排开始时解析一次模型配置,然后在所有 Task 生成时使用。 + +## 解析模式 + +```bash +MODEL_PROFILE=$(cat .planning/config.json 2>/dev/null | grep -o '"model_profile"[[:space:]]*:[[:space:]]*"[^"]*"' | grep -o '"[^"]*"$' | tr -d '"' || echo "balanced") +``` + +默认值:未设置或缺少 config 时为 `balanced`。 + +## 查找表 + +@~/.claude/get-shit-done/references/model-profiles.md + +在表中查找已解析配置对应的代理。将 model 参数传递给 Task 调用: + +``` +Task( + prompt="...", + subagent_type="gsd-planner", + model="{resolved_model}" # "inherit"、"sonnet" 或 "haiku" +) +``` + +**注意:** Opus 级代理解析为 `"inherit"`(而非 `"opus"`)。这会使代理使用父会话的模型,避免与可能阻止特定 opus 版本的组织策略冲突。 + +## 使用方法 + +1. 在编排开始时解析一次 +2. 存储 profile 值 +3. 生成时在表中查找每个代理的模型 +4. 将 model 参数传递给每个 Task 调用(值:`"inherit"`、`"sonnet"`、`"haiku"`) \ No newline at end of file diff --git a/docs/zh-CN/references/model-profiles.md b/docs/zh-CN/references/model-profiles.md new file mode 100644 index 000000000..011fbba57 --- /dev/null +++ b/docs/zh-CN/references/model-profiles.md @@ -0,0 +1,93 @@ +# 模型配置 + +模型配置控制每个 GSD 代理使用哪个 Claude 模型。这允许平衡质量和 token 消耗。 + +## 配置定义 + +| 代理 | `quality` | `balanced` | `budget` | +|-------|-----------|------------|----------| +| gsd-planner | opus | opus | sonnet | +| gsd-roadmapper | opus | sonnet | sonnet | +| gsd-executor | opus | sonnet | sonnet | +| gsd-phase-researcher | opus | sonnet | haiku | +| gsd-project-researcher | opus | sonnet | haiku | +| gsd-research-synthesizer | sonnet | sonnet | haiku | +| gsd-debugger | opus | sonnet | sonnet | +| gsd-codebase-mapper | sonnet | haiku | haiku | +| gsd-verifier | sonnet | sonnet | haiku | +| gsd-plan-checker | sonnet | sonnet | haiku | +| gsd-integration-checker | sonnet | sonnet | haiku | +| gsd-nyquist-auditor | sonnet | sonnet | haiku | + +## 配置理念 + +**quality** - 最大推理能力 +- 所有决策代理使用 Opus +- 只读验证使用 Sonnet +- 适用场景:有配额可用、关键架构工作 + +**balanced**(默认)- 智能分配 +- 仅规划(架构决策发生的地方)使用 Opus +- 执行和研究使用 Sonnet(遵循明确指令) +- 验证使用 Sonnet(需要推理,不仅仅是模式匹配) +- 适用场景:正常开发、质量与成本的良好平衡 + +**budget** - 最小化 Opus 使用 +- 编写代码的使用 Sonnet +- 研究和验证使用 Haiku +- 适用场景:节省配额、大量工作、不太关键的阶段 + +## 解析逻辑 + +编排器在生成代理前解析模型: + +``` +1. 读取 .planning/config.json +2. 检查 model_overrides 是否有代理特定覆盖 +3. 如果没有覆盖,在配置表中查找代理 +4. 将 model 参数传递给 Task 调用 +``` + +## 单代理覆盖 + +覆盖特定代理而不更改整个配置: + +```json +{ + "model_profile": "balanced", + "model_overrides": { + "gsd-executor": "opus", + "gsd-planner": "haiku" + } +} +``` + +覆盖优先于配置。有效值:`opus`、`sonnet`、`haiku`。 + +## 切换配置 + +运行时:`/gsd:set-profile ` + +项目默认值:在 `.planning/config.json` 中设置: +```json +{ + "model_profile": "balanced" +} +``` + +## 设计理由 + +**为什么 gsd-planner 使用 Opus?** +规划涉及架构决策、目标分解和任务设计。这是模型质量影响最大的地方。 + +**为什么 gsd-executor 使用 Sonnet?** +执行者遵循明确的 PLAN.md 指令。计划已包含推理;执行只是实现。 + +**为什么 balanced 中验证器使用 Sonnet(而非 Haiku)?** +验证需要目标回溯推理 —— 检查代码是否**交付**了阶段承诺的内容,而不仅仅是模式匹配。Sonnet 处理得很好;Haiku 可能会遗漏细微的差距。 + +**为什么 gsd-codebase-mapper 使用 Haiku?** +只读探索和模式提取。不需要推理,只需从文件内容输出结构化结果。 + +**为什么用 `inherit` 而不是直接传递 `opus`?** +Claude Code 的 `"opus"` 别名映射到特定模型版本。组织可能阻止旧版 opus 而允许新版。GSD 为 opus 级代理返回 `"inherit"`,使其使用用户在会话中配置的任何 opus 版本。这避免了版本冲突和静默回退到 Sonnet。 \ No newline at end of file diff --git a/docs/zh-CN/references/phase-argument-parsing.md b/docs/zh-CN/references/phase-argument-parsing.md new file mode 100644 index 000000000..1434143bb --- /dev/null +++ b/docs/zh-CN/references/phase-argument-parsing.md @@ -0,0 +1,61 @@ +# 阶段参数解析 + +为操作阶段的命令解析和规范化阶段参数。 + +## 提取 + +从 `$ARGUMENTS` 中: +- 提取阶段编号(第一个数字参数) +- 提取标志(以 `--` 为前缀) +- 剩余文本为描述(用于 insert/add 命令) + +## 使用 gsd-tools + +`find-phase` 命令一步完成规范化和验证: + +```bash +PHASE_INFO=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" find-phase "${PHASE}") +``` + +返回 JSON 包含: +- `found`: true/false +- `directory`: 阶段目录的完整路径 +- `phase_number`: 规范化的编号(如 "06"、"06.1") +- `phase_name`: 名称部分(如 "foundation") +- `plans`: PLAN.md 文件数组 +- `summaries`: SUMMARY.md 文件数组 + +## 手动规范化(遗留) + +将整数阶段补零到 2 位。保留小数后缀。 + +```bash +# 规范化阶段编号 +if [[ "$PHASE" =~ ^[0-9]+$ ]]; then + # 整数: 8 → 08 + PHASE=$(printf "%02d" "$PHASE") +elif [[ "$PHASE" =~ ^([0-9]+)\.([0-9]+)$ ]]; then + # 小数: 2.1 → 02.1 + PHASE=$(printf "%02d.%s" "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}") +fi +``` + +## 验证 + +使用 `roadmap get-phase` 验证阶段存在: + +```bash +PHASE_CHECK=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" roadmap get-phase "${PHASE}") +if [ "$(printf '%s\n' "$PHASE_CHECK" | jq -r '.found')" = "false" ]; then + echo "ERROR: Phase ${PHASE} not found in roadmap" + exit 1 +fi +``` + +## 目录查找 + +使用 `find-phase` 进行目录查找: + +```bash +PHASE_DIR=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" find-phase "${PHASE}" --raw) +``` \ No newline at end of file diff --git a/docs/zh-CN/references/planning-config.md b/docs/zh-CN/references/planning-config.md new file mode 100644 index 000000000..075d2f85d --- /dev/null +++ b/docs/zh-CN/references/planning-config.md @@ -0,0 +1,200 @@ + + +`.planning/` 目录行为的配置选项。 + + +```json +"planning": { + "commit_docs": true, + "search_gitignored": false +}, +"git": { + "branching_strategy": "none", + "phase_branch_template": "gsd/phase-{phase}-{slug}", + "milestone_branch_template": "gsd/{milestone}-{slug}" +} +``` + +| 选项 | 默认值 | 描述 | +|--------|---------|-------------| +| `commit_docs` | `true` | 是否将规划工件提交到 git | +| `search_gitignored` | `false` | 在广泛 rg 搜索中添加 `--no-ignore` | +| `git.branching_strategy` | `"none"` | Git 分支策略:`"none"`、`"phase"` 或 `"milestone"` | +| `git.phase_branch_template` | `"gsd/phase-{phase}-{slug}"` | 阶段策略的分支模板 | +| `git.milestone_branch_template` | `"gsd/{milestone}-{slug}"` | 里程碑策略的分支模板 | + + + + +**当 `commit_docs: true`(默认):** +- 规划文件正常提交 +- SUMMARY.md、STATE.md、ROADMAP.md 在 git 中跟踪 +- 规划决策的完整历史保留 + +**当 `commit_docs: false`:** +- 跳过 `.planning/` 文件的所有 `git add`/`git commit` +- 用户必须将 `.planning/` 添加到 `.gitignore` +- 适用于:OSS 贡献、客户项目、保持规划私有 + +**使用 gsd-tools.cjs(推荐):** + +```bash +# 提交时自动检查 commit_docs + gitignore: +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: update state" --files .planning/STATE.md + +# 通过 state load 加载配置(返回 JSON): +INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state load) +if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi +# commit_docs 在 JSON 输出中可用 + +# 或使用包含 commit_docs 的 init 命令: +INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init execute-phase "1") +if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi +# commit_docs 包含在所有 init 命令输出中 +``` + +**自动检测:** 如果 `.planning/` 被 gitignore,无论 config.json 如何,`commit_docs` 自动为 `false`。这防止用户在 `.gitignore` 中有 `.planning/` 时出现 git 错误。 + +**通过 CLI 提交(自动处理检查):** + +```bash +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: update state" --files .planning/STATE.md +``` + +CLI 在内部检查 `commit_docs` 配置和 gitignore 状态 —— 无需手动条件判断。 + + + + + +**当 `search_gitignored: false`(默认):** +- 标准 rg 行为(尊重 .gitignore) +- 直接路径搜索有效:`rg "pattern" .planning/` 找到文件 +- 广泛搜索跳过 gitignored:`rg "pattern"` 跳过 `.planning/` + +**当 `search_gitignored: true`:** +- 在应该包含 `.planning/` 的广泛 rg 搜索中添加 `--no-ignore` +- 仅在搜索整个仓库并期望 `.planning/` 匹配时需要 + +**注意:** 大多数 GSD 操作使用直接文件读取或显式路径,无论 gitignore 状态如何都有效。 + + + + + +使用未提交模式: + +1. **设置配置:** + ```json + "planning": { + "commit_docs": false, + "search_gitignored": true + } + ``` + +2. **添加到 .gitignore:** + ``` + .planning/ + ``` + +3. **已存在的跟踪文件:** 如果 `.planning/` 之前被跟踪: + ```bash + git rm -r --cached .planning/ + git commit -m "chore: stop tracking planning docs" + ``` + +4. **分支合并:** 当使用 `branching_strategy: phase` 或 `milestone` 时,`complete-milestone` 工作流在 `commit_docs: false` 时自动从暂存区移除 `.planning/` 文件,然后才进行合并提交。 + + + + + +**分支策略:** + +| 策略 | 创建分支时机 | 分支范围 | 合并点 | +|----------|---------------------|--------------|-------------| +| `none` | 从不 | N/A | N/A | +| `phase` | `execute-phase` 开始时 | 单个阶段 | 阶段后用户手动合并 | +| `milestone` | 里程碑第一个 `execute-phase` | 整个里程碑 | `complete-milestone` 时 | + +**当 `git.branching_strategy: "none"`(默认):** +- 所有工作提交到当前分支 +- 标准 GSD 行为 + +**当 `git.branching_strategy: "phase"`:** +- `execute-phase` 在执行前创建/切换到分支 +- 分支名来自 `phase_branch_template`(如 `gsd/phase-03-authentication`) +- 所有计划提交到该分支 +- 阶段完成后用户手动合并分支 +- `complete-milestone` 提供合并所有阶段分支的选项 + +**当 `git.branching_strategy: "milestone"`:** +- 里程碑的第一个 `execute-phase` 创建里程碑分支 +- 分支名来自 `milestone_branch_template`(如 `gsd/v1.0-mvp`) +- 里程碑中所有阶段提交到同一分支 +- `complete-milestone` 提供将里程碑分支合并到 main 的选项 + +**模板变量:** + +| 变量 | 可用于 | 描述 | +|----------|--------------|-------------| +| `{phase}` | phase_branch_template | 零填充阶段号(如 "03") | +| `{slug}` | 两者 | 小写、连字符名称 | +| `{milestone}` | milestone_branch_template | 里程碑版本(如 "v1.0") | + +**检查配置:** + +使用 `init execute-phase` 返回所有配置为 JSON: +```bash +INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init execute-phase "1") +if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi +# JSON 输出包含:branching_strategy, phase_branch_template, milestone_branch_template +``` + +或使用 `state load` 获取配置值: +```bash +INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state load) +if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi +# 从 JSON 解析 branching_strategy, phase_branch_template, milestone_branch_template +``` + +**分支创建:** + +```bash +# 阶段策略 +if [ "$BRANCHING_STRATEGY" = "phase" ]; then + PHASE_SLUG=$(echo "$PHASE_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//') + BRANCH_NAME=$(echo "$PHASE_BRANCH_TEMPLATE" | sed "s/{phase}/$PADDED_PHASE/g" | sed "s/{slug}/$PHASE_SLUG/g") + git checkout -b "$BRANCH_NAME" 2>/dev/null || git checkout "$BRANCH_NAME" +fi + +# 里程碑策略 +if [ "$BRANCHING_STRATEGY" = "milestone" ]; then + MILESTONE_SLUG=$(echo "$MILESTONE_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//') + BRANCH_NAME=$(echo "$MILESTONE_BRANCH_TEMPLATE" | sed "s/{milestone}/$MILESTONE_VERSION/g" | sed "s/{slug}/$MILESTONE_SLUG/g") + git checkout -b "$BRANCH_NAME" 2>/dev/null || git checkout "$BRANCH_NAME" +fi +``` + +**complete-milestone 时的合并选项:** + +| 选项 | Git 命令 | 结果 | +|--------|-------------|--------| +| Squash 合并(推荐) | `git merge --squash` | 每个分支单个干净提交 | +| 带历史合并 | `git merge --no-ff` | 保留所有单独提交 | +| 不合并直接删除 | `git branch -D` | 丢弃分支工作 | +| 保留分支 | (无) | 后续手动处理 | + +推荐 Squash 合并 —— 保持 main 分支历史干净,同时在分支中保留完整开发历史(直到删除)。 + +**使用场景:** + +| 策略 | 最适合 | +|----------|----------| +| `none` | 独立开发、简单项目 | +| `phase` | 每阶段代码审查、细粒度回滚、团队协作 | +| `milestone` | 发布分支、预发布环境、每个版本一个 PR | + + + + \ No newline at end of file diff --git a/docs/zh-CN/references/questioning.md b/docs/zh-CN/references/questioning.md new file mode 100644 index 000000000..7f23dbe07 --- /dev/null +++ b/docs/zh-CN/references/questioning.md @@ -0,0 +1,142 @@ +# 提问指南 + +项目初始化是梦想提取,而非需求收集。你在帮助用户发现和表达他们想构建的内容。这不是合同谈判 —— 是协作思考。 + +## 理念 + +**你是思考伙伴,不是面试官。** + +用户通常有一个模糊的想法。你的工作是帮助他们将其锐化。问一些让他们思考"哦,我没想到那个"或"是的,这正是我的意思"的问题。 + +不要审问。协作。不要照本宣科。顺藤摸瓜。 + +## 目标 + +到提问结束时,你需要足够的清晰度来编写下游阶段可执行的 PROJECT.md: + +- **研究** 需要:研究什么领域、用户已知什么、存在哪些未知 +- **需求** 需要:足够清晰的愿景来界定 v1 功能 +- **路线图** 需要:足够清晰的愿景来分解为阶段、"完成"是什么样子 +- **plan-phase** 需要:可分解为任务的具体需求、实现选择的上下文 +- **execute-phase** 需要:可验证的成功标准、需求背后的"为什么" + +模糊的 PROJECT.md 会让每个下游阶段都在猜测。成本会叠加。 + +## 如何提问 + +**开放开始。** 让他们倾倒心理模型。不要用结构打断。 + +**跟随能量。** 无论他们强调什么,深入那个。什么让他们兴奋?什么问题引发了这一切? + +**挑战模糊。** 绝不接受模糊回答。"好"意味着什么?"用户"指谁?"简单"是怎么简单? + +**让抽象具体。**"带我走一遍使用这个。""那实际看起来是什么样?" + +**澄清歧义。**"你说 Z 时,是指 A 还是 B?""你提到了 X —— 跟我多说说。" + +**知道何时停止。** 当你理解他们想要什么、为什么想要、给谁用、完成是什么样 —— 提议继续。 + +## 问题类型 + +以此作为灵感,不是清单。选择与话题相关的。 + +**动机 —— 为什么存在:** +- "什么引发了这一切?" +- "你今天在做什么会被这个替代?" +- "如果这个存在,你会做什么?" + +**具体性 —— 它实际是什么:** +- "带我走一遍使用这个" +- "你说 X —— 那实际看起来是什么样?" +- "给我一个例子" + +**澄清 —— 他们什么意思:** +- "你说 Z 时,是指 A 还是 B?" +- "你提到了 X —— 跟我多说说那个" + +**成功 —— 你怎么知道它在工作:** +- "你怎么知道这个在工作?" +- "完成是什么样子?" + +## 使用 AskUserQuestion + +用 AskUserQuestion 帮助用户思考,通过呈现具体的选项供他们反应。 + +**好选项:** +- 他们可能意思的解读 +- 确认或否认的具体例子 +- 揭示优先级的具体选择 + +**坏选项:** +- 泛泛的类别("技术"、"业务"、"其他") +- 预设答案的引导性选项 +- 选项太多(2-4 个理想) +- 超过 12 个字符的标题(硬限制 —— 验证会拒绝) + +**示例 —— 模糊回答:** +用户说"它应该快" + +- header: "快" +- question: "快是指?" +- options: ["亚秒响应", "处理大数据集", "快速构建", "让我解释"] + +**示例 —— 跟随话题:** +用户提到"对当前工具感到沮丧" + +- header: "沮丧" +- question: "具体什么让你沮丧?" +- options: ["点击太多", "缺少功能", "不可靠", "让我解释"] + +**给用户的提示 —— 修改选项:** +想要稍微修改某个选项版本的用户可以选择"Other"并通过编号引用选项:`#1 但仅用于指关节` 或 `#2 禁用分页`。这避免重新输入完整选项文本。 + +## 自由格式规则 + +**当用户想自由解释时,停止使用 AskUserQuestion。** + +如果用户选择"Other"且他们的回应表明他们想用自己的话描述(如"让我描述一下"、"我来解释"、"别的"、或任何非选择/修改现有选项的开放式回复),你必须: + +1. **用纯文本问你的追问** — 不通过 AskUserQuestion +2. **等待他们在正常提示符下输入** +3. **仅在处理他们的自由格式回应后恢复 AskUserQuestion** + +同样适用于如果你包含一个表明自由格式的选项(如"让我解释"或"详细描述")且用户选择了它。 + +**错误:** 用户说"让我描述一下" → AskUserQuestion("什么功能?", ["功能 A", "功能 B", "详细描述"]) +**正确:** 用户说"让我描述一下" → "请讲 —— 你在想什么?" + +## 上下文清单 + +以此作为**背景清单**,而非对话结构。进行时在脑中检查这些。如果还有缺口,自然地穿插问题。 + +- [ ] 他们在构建什么(足够具体可以向陌生人解释) +- [ ] 为什么它需要存在(驱动它的问题或渴望) +- [ ] 给谁用的(即使只是他们自己) +- [ ] "完成"是什么样子(可观察的结果) + +四件事。如果他们主动提供更多,捕获它。 + +## 决策门控 + +当你能写出清晰的 PROJECT.md 时,提议继续: + +- header: "准备好了?" +- question: "我想我理解你想要什么了。准备创建 PROJECT.md 吗?" +- options: + - "创建 PROJECT.md" — 让我们继续 + - "继续探索" — 我想分享更多 / 再问我 + +如果"继续探索" —— 问他们想添加什么或识别缺口并自然探查。 + +循环直到选择"创建 PROJECT.md"。 + +## 反模式 + +- **走清单** — 不管他们说什么都按领域走 +- **套话问题** — "你的核心价值是什么?""什么超出范围?"不管上下文 +- **企业腔** — "你的成功标准是什么?""你的利益相关者是谁?" +- **审问** — 不基于回答构建就连续发问 +- **急于求成** — 最小化问题以开始"实际工作" +- **浅层接受** — 不探查就接受模糊回答 +- **过早约束** — 还不理解想法就问技术栈 +- **用户技能** — 绝不问用户的技术经验。Claude 来构建。 \ No newline at end of file diff --git a/docs/zh-CN/references/tdd.md b/docs/zh-CN/references/tdd.md new file mode 100644 index 000000000..247bf77d3 --- /dev/null +++ b/docs/zh-CN/references/tdd.md @@ -0,0 +1,263 @@ + +TDD 关乎设计质量,而非覆盖率指标。红-绿-重构循环迫使你在实现前思考行为,从而产生更清晰的接口和更可测试的代码。 + +**原则:** 如果在编写 `fn` 之前能用 `expect(fn(input)).toBe(output)` 描述行为,TDD 会改善结果。 + +**关键洞察:** TDD 工作本质上比标准任务更重 —— 它需要 2-3 个执行周期(RED → GREEN → REFACTOR),每个周期都涉及文件读取、测试运行和可能的调试。TDD 功能获得专门的计划,以确保整个周期内有完整的上下文可用。 + + + +## 何时 TDD 提高质量 + +**TDD 候选(创建 TDD 计划):** +- 有明确输入/输出的业务逻辑 +- 有请求/响应契约的 API 端点 +- 数据转换、解析、格式化 +- 验证规则和约束 +- 有可测试行为的算法 +- 状态机和工作流 +- 有清晰规格的工具函数 + +**跳过 TDD(使用带 `type="auto"` 任务的标准计划):** +- UI 布局、样式、视觉组件 +- 配置更改 +- 连接现有组件的胶水代码 +- 一次性脚本和迁移 +- 无业务逻辑的简单 CRUD +- 探索性原型 + +**启发式:** 能在编写 `fn` 之前写 `expect(fn(input)).toBe(output)` 吗? +→ 能:创建 TDD 计划 +→ 不能:使用标准计划,事后添加测试(如需要) + + + +## TDD 计划结构 + +每个 TDD 计划通过完整的 RED-GREEN-REFACTOR 循环实现**一个功能**。 + +```markdown +--- +phase: XX-name +plan: NN +type: tdd +--- + + +[什么功能以及为什么] +Purpose: [该功能 TDD 的设计收益] +Output: [可工作的、已测试的功能] + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@relevant/source/files.ts + + + + [功能名称] + [源文件, 测试文件] + + [可测试术语描述的预期行为] + Cases: 输入 → 预期输出 + + [测试通过后如何实现] + + + +[证明功能有效的测试命令] + + + +- 失败测试已编写并提交 +- 实现通过测试 +- 重构完成(如需要) +- 所有 2-3 个提交都存在 + + + +完成后,创建包含以下内容的 SUMMARY.md: +- RED: 编写了什么测试,为什么失败 +- GREEN: 什么实现让它通过 +- REFACTOR: 做了什么清理(如有) +- Commits: 生成的提交列表 + +``` + +**每个 TDD 计划一个功能。** 如果功能足够简单可以批量处理,那就足够简单可以跳过 TDD —— 使用标准计划,事后添加测试。 + + + +## 红-绿-重构循环 + +**RED - 编写失败测试:** +1. 按项目约定创建测试文件 +2. 编写描述预期行为的测试(来自 `` 元素) +3. 运行测试 - 必须**失败** +4. 如果测试通过:功能已存在或测试有误。调查。 +5. 提交:`test({phase}-{plan}): add failing test for [feature]` + +**GREEN - 实现使其通过:** +1. 编写使测试通过的最小代码 +2. 不耍小聪明,不优化 - 只让它工作 +3. 运行测试 - 必须**通过** +4. 提交:`feat({phase}-{plan}): implement [feature]` + +**REFACTOR(如需要):** +1. 如果存在明显的改进,清理实现 +2. 运行测试 - 必须**仍然通过** +3. 仅在做出更改时提交:`refactor({phase}-{plan}): clean up [feature]` + +**结果:** 每个 TDD 计划产生 2-3 个原子提交。 + + + +## 好测试 vs 坏测试 + +**测试行为,而非实现:** +- 好:"返回格式化的日期字符串" +- 坏:"用正确参数调用 formatDate 辅助函数" +- 测试应该能经受重构 + +**每个测试一个概念:** +- 好:分别为有效输入、空输入、畸形输入编写测试 +- 坏:用多个断言检查所有边缘情况的单个测试 + +**描述性名称:** +- 好:"should reject empty email"、"returns null for invalid ID" +- 坏:"test1"、"handles error"、"works correctly" + +**不包含实现细节:** +- 好:测试公共 API、可观察行为 +- 坏:Mock 内部实现、测试私有方法、断言内部状态 + + + +## 测试框架设置(如不存在) + +当执行 TDD 计划但没有配置测试框架时,作为 RED 阶段的一部分进行设置: + +**1. 检测项目类型:** +```bash +# JavaScript/TypeScript +if [ -f package.json ]; then echo "node"; fi + +# Python +if [ -f requirements.txt ] || [ -f pyproject.toml ]; then echo "python"; fi + +# Go +if [ -f go.mod ]; then echo "go"; fi + +# Rust +if [ -f Cargo.toml ]; then echo "rust"; fi +``` + +**2. 安装最小框架:** +| 项目 | 框架 | 安装 | +|---------|-----------|---------| +| Node.js | Jest | `npm install -D jest @types/jest ts-jest` | +| Node.js (Vite) | Vitest | `npm install -D vitest` | +| Python | pytest | `pip install pytest` | +| Go | testing | 内置 | +| Rust | cargo test | 内置 | + +**3. 按需创建配置:** +- Jest: 带 ts-jest preset 的 `jest.config.js` +- Vitest: 带测试全局变量的 `vitest.config.ts` +- pytest: `pytest.ini` 或 `pyproject.toml` 部分 + +**4. 验证设置:** +```bash +# 运行空测试套件 - 应该以 0 个测试通过 +npm test # Node +pytest # Python +go test ./... # Go +cargo test # Rust +``` + +**5. 创建第一个测试文件:** +遵循项目约定的测试位置: +- 源文件旁边的 `*.test.ts` / `*.spec.ts` +- `__tests__/` 目录 +- 根目录的 `tests/` 目录 + +框架设置是第一个 TDD 计划 RED 阶段的一次性成本。 + + + +## 错误处理 + +**测试在 RED 阶段没有失败:** +- 功能可能已存在 - 调查 +- 测试可能有误(没测试你以为的东西) +- 前进前修复 + +**测试在 GREEN 阶段没有通过:** +- 调试实现 +- 不要跳到重构 +- 持续迭代直到绿色 + +**测试在 REFACTOR 阶段失败:** +- 撤销重构 +- 提交过早 +- 用更小的步骤重构 + +**不相关的测试失败:** +- 停下来调查 +- 可能表明耦合问题 +- 前进前修复 + + + +## TDD 计划的提交模式 + +TDD 计划产生 2-3 个原子提交(每个阶段一个): + +``` +test(08-02): add failing test for email validation + +- Tests valid email formats accepted +- Tests invalid formats rejected +- Tests empty input handling + +feat(08-02): implement email validation + +- Regex pattern matches RFC 5322 +- Returns boolean for validity +- Handles edge cases (empty, null) + +refactor(08-02): extract regex to constant (optional) + +- Moved pattern to EMAIL_REGEX constant +- No behavior changes +- Tests still pass +``` + +**与标准计划对比:** +- 标准计划:每个任务 1 个提交,每个计划 2-4 个提交 +- TDD 计划:单个功能 2-3 个提交 + +两者遵循相同格式:`{type}({phase}-{plan}): {description}` + +**好处:** +- 每个提交独立可回滚 +- Git bisect 在提交级别工作 +- 显示 TDD 纪律的清晰历史 +- 与整体提交策略一致 + + + +## 上下文预算 + +TDD 计划目标 **~40% 上下文使用率**(低于标准计划的 ~50%)。 + +为什么更低: +- RED 阶段:编写测试、运行测试、可能调试为什么没有失败 +- GREEN 阶段:实现、运行测试、可能对失败进行迭代 +- REFACTOR 阶段:修改代码、运行测试、验证无回归 + +每个阶段涉及读取文件、运行命令、分析输出。来回往复本质上比线性任务执行更重。 + +单一功能聚焦确保整个周期保持完整质量。 + \ No newline at end of file diff --git a/docs/zh-CN/references/ui-brand.md b/docs/zh-CN/references/ui-brand.md new file mode 100644 index 000000000..f9828ae1b --- /dev/null +++ b/docs/zh-CN/references/ui-brand.md @@ -0,0 +1,158 @@ +# UI 品牌规范 + +面向用户的 GSD 输出的视觉模式。编排器通过 @ 引用此文件。 + +## 阶段横幅 + +用于主要工作流过渡。 + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + GSD ► {阶段名称} +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +**阶段名称(大写):** +- `QUESTIONING`(提问) +- `RESEARCHING`(研究) +- `DEFINING REQUIREMENTS`(定义需求) +- `CREATING ROADMAP`(创建路线图) +- `PLANNING PHASE {N}`(规划阶段 {N}) +- `EXECUTING WAVE {N}`(执行波次 {N}) +- `VERIFYING`(验证) +- `PHASE {N} COMPLETE ✓`(阶段 {N} 完成) +- `MILESTONE COMPLETE 🎉`(里程碑完成) + +--- + +## 检查点框 + +需要用户操作。62 字符宽度。 + +``` +╔══════════════════════════════════════════════════════════════╗ +║ CHECKPOINT: {类型} ║ +╚══════════════════════════════════════════════════════════════╝ + +{内容} + +────────────────────────────────────────────────────────────── +→ {操作提示} +────────────────────────────────────────────────────────────── +``` + +**类型:** +- `CHECKPOINT: 需要验证` → `→ 输入 "approved" 或描述问题` +- `CHECKPOINT: 需要决策` → `→ 选择: option-a / option-b` +- `CHECKPOINT: 需要操作` → `→ 完成后输入 "done"` + +--- + +## 状态符号 + +``` +✓ 完成 / 通过 / 已验证 +✗ 失败 / 缺失 / 阻塞 +◆ 进行中 +○ 待处理 +⚡ 自动批准 +⚠ 警告 +🎉 里程碑完成(仅在横幅中) +``` + +--- + +## 进度显示 + +**阶段/里程碑级别:** +``` +进度: ████████░░ 80% +``` + +**任务级别:** +``` +任务: 2/4 完成 +``` + +**计划级别:** +``` +计划: 3/5 完成 +``` + +--- + +## 生成指示器 + +``` +◆ 正在生成研究员... + +◆ 并行生成 4 个研究员... + → 技术栈研究 + → 功能研究 + → 架构研究 + → 陷阱研究 + +✓ 研究员完成: STACK.md 已写入 +``` + +--- + +## 下一步区块 + +始终在主要完成后。 + +``` +─────────────────────────────────────────────────────────────── + +## ▶ 下一步 + +**{标识符}: {名称}** — {单行描述} + +`{可复制粘贴的命令}` + +`/clear` 优先 → 全新上下文窗口 + +─────────────────────────────────────────────────────────────── + +**也可选:** +- `/gsd:alternative-1` — 描述 +- `/gsd:alternative-2` — 描述 + +─────────────────────────────────────────────────────────────── +``` + +--- + +## 错误框 + +``` +╔══════════════════════════════════════════════════════════════╗ +║ ERROR ║ +╚══════════════════════════════════════════════════════════════╝ + +{错误描述} + +**修复方法:** {解决步骤} +``` + +--- + +## 表格 + +``` +| 阶段 | 状态 | 计划 | 进度 | +|------|------|------|------| +| 1 | ✓ | 3/3 | 100% | +| 2 | ◆ | 1/4 | 25% | +| 3 | ○ | 0/2 | 0% | +``` + +--- + +## 反模式 + +- 变化的框/横幅宽度 +- 混合横幅样式(`===`、`---`、`***`) +- 横幅中缺少 `GSD ►` 前缀 +- 随机 emoji(`🚀`、`✨`、`💫`) +- 完成后缺少下一步区块 \ No newline at end of file diff --git a/docs/zh-CN/references/verification-patterns.md b/docs/zh-CN/references/verification-patterns.md new file mode 100644 index 000000000..cfeeb8dd0 --- /dev/null +++ b/docs/zh-CN/references/verification-patterns.md @@ -0,0 +1,612 @@ +# 验证模式 + +如何验证不同类型的工件是真实实现,而非存根或占位符。 + + +**存在 ≠ 实现** + +文件存在并不意味着功能有效。验证必须检查: +1. **存在** - 文件在预期路径 +2. **实质性** - 内容是真实实现,非占位符 +3. **已连接** - 已连接到系统的其他部分 +4. **功能性** - 调用时实际工作 + +级别 1-3 可以编程检查。级别 4 通常需要人工验证。 + + + + +## 通用存根模式 + +这些模式表明占位符代码,无论文件类型: + +**基于注释的存根:** +```bash +# 存根注释的 Grep 模式 +grep -E "(TODO|FIXME|XXX|HACK|PLACEHOLDER)" "$file" +grep -E "implement|add later|coming soon|will be" "$file" -i +grep -E "// \.\.\.|/\* \.\.\. \*/|# \.\.\." "$file" +``` + +**输出中的占位符文本:** +```bash +# UI 占位符模式 +grep -E "placeholder|lorem ipsum|coming soon|under construction" "$file" -i +grep -E "sample|example|test data|dummy" "$file" -i +grep -E "\[.*\]|<.*>|\{.*\}" "$file" # 模板括号未移除 +``` + +**空或琐碎实现:** +```bash +# 什么都不做的函数 +grep -E "return null|return undefined|return \{\}|return \[\]" "$file" +grep -E "pass$|\.\.\.|\bnothing\b" "$file" +grep -E "console\.(log|warn|error).*only" "$file" # 仅日志函数 +``` + +**预期动态但硬编码的值:** +```bash +# 硬编码 ID、计数或内容 +grep -E "id.*=.*['\"].*['\"]" "$file" # 硬编码字符串 ID +grep -E "count.*=.*\d+|length.*=.*\d+" "$file" # 硬编码计数 +grep -E "\\\$\d+\.\d{2}|\d+ items" "$file" # 硬编码显示值 +``` + + + + + +## React/Next.js 组件 + +**存在检查:** +```bash +# 文件存在且导出组件 +[ -f "$component_path" ] && grep -E "export (default |)function|export const.*=.*\(" "$component_path" +``` + +**实质性检查:** +```bash +# 返回实际 JSX,非占位符 +grep -E "return.*<" "$component_path" | grep -v "return.*null" | grep -v "placeholder" -i + +# 有有意义的内容(不仅仅是包装 div) +grep -E "<[A-Z][a-zA-Z]+|className=|onClick=|onChange=" "$component_path" + +# 使用 props 或 state(非静态) +grep -E "props\.|useState|useEffect|useContext|\{.*\}" "$component_path" +``` + +**React 特有的存根模式:** +```javascript +// 危险信号 - 这些是存根: +return
Component
+return
Placeholder
+return
{/* TODO */}
+return

Coming soon

+return null +return <> + +// 也是存根 - 空处理器: +onClick={() => {}} +onChange={() => console.log('clicked')} +onSubmit={(e) => e.preventDefault()} // 仅阻止默认,什么都不做 +``` + +**连接检查:** +```bash +# 组件导入它需要的东西 +grep -E "^import.*from" "$component_path" + +# Props 实际被使用(不仅仅是接收) +# 查找解构或 props.X 用法 +grep -E "\{ .* \}.*props|\bprops\.[a-zA-Z]+" "$component_path" + +# API 调用存在(对于数据获取组件) +grep -E "fetch\(|axios\.|useSWR|useQuery|getServerSideProps|getStaticProps" "$component_path" +``` + +**功能验证(需要人工):** +- 组件是否渲染可见内容? +- 交互元素是否响应点击? +- 数据是否加载并显示? +- 错误状态是否适当显示? + +
+ + + +## API 路由(Next.js App Router / Express 等) + +**存在检查:** +```bash +# 路由文件存在 +[ -f "$route_path" ] + +# 导出 HTTP 方法处理器(Next.js App Router) +grep -E "export (async )?(function|const) (GET|POST|PUT|PATCH|DELETE)" "$route_path" + +# 或 Express 风格处理器 +grep -E "\.(get|post|put|patch|delete)\(" "$route_path" +``` + +**实质性检查:** +```bash +# 有实际逻辑,不仅仅是 return 语句 +wc -l "$route_path" # 超过 10-15 行表明真实实现 + +# 与数据源交互 +grep -E "prisma\.|db\.|mongoose\.|sql|query|find|create|update|delete" "$route_path" -i + +# 有错误处理 +grep -E "try|catch|throw|error|Error" "$route_path" + +# 返回有意义的响应 +grep -E "Response\.json|res\.json|res\.send|return.*\{" "$route_path" | grep -v "message.*not implemented" -i +``` + +**API 路由特有的存根模式:** +```typescript +// 危险信号 - 这些是存根: +export async function POST() { + return Response.json({ message: "Not implemented" }) +} + +export async function GET() { + return Response.json([]) // 空 array 无数据库查询 +} + +export async function PUT() { + return new Response() // 空响应 +} + +// 仅控制台日志: +export async function POST(req) { + console.log(await req.json()) + return Response.json({ ok: true }) +} +``` + +**连接检查:** +```bash +# 导入数据库/服务客户端 +grep -E "^import.*prisma|^import.*db|^import.*client" "$route_path" + +# 实际使用请求体(对于 POST/PUT) +grep -E "req\.json\(\)|req\.body|request\.json\(\)" "$route_path" + +# 验证输入(不仅仅信任请求) +grep -E "schema\.parse|validate|zod|yup|joi" "$route_path" +``` + +**功能验证(人工或自动化):** +- GET 是否从数据库返回真实数据? +- POST 是否实际创建记录? +- 错误响应是否有正确的状态码? +- 认证检查是否实际执行? + + + + + +## 数据库模式(Prisma / Drizzle / SQL) + +**存在检查:** +```bash +# 模式文件存在 +[ -f "prisma/schema.prisma" ] || [ -f "drizzle/schema.ts" ] || [ -f "src/db/schema.sql" ] + +# 模型/表已定义 +grep -E "^model $model_name|CREATE TABLE $table_name|export const $table_name" "$schema_path" +``` + +**实质性检查:** +```bash +# 有预期字段(不仅仅是 id) +grep -A 20 "model $model_name" "$schema_path" | grep -E "^\s+\w+\s+\w+" + +# 有预期关系 +grep -E "@relation|REFERENCES|FOREIGN KEY" "$schema_path" + +# 有适当的字段类型(不全是 String) +grep -A 20 "model $model_name" "$schema_path" | grep -E "Int|DateTime|Boolean|Float|Decimal|Json" +``` + +**模式特有的存根模式:** +```prisma +// 危险信号 - 这些是存根: +model User { + id String @id + // TODO: add fields +} + +model Message { + id String @id + content String // 只有一个真实字段 +} + +// 缺少关键字段: +model Order { + id String @id + // 缺少: userId, items, total, status, createdAt +} +``` + +**连接检查:** +```bash +# 迁移存在且已应用 +ls prisma/migrations/ 2>/dev/null | wc -l # 应该 > 0 +npx prisma migrate status 2>/dev/null | grep -v "pending" + +# 客户端已生成 +[ -d "node_modules/.prisma/client" ] +``` + +**功能验证:** +```bash +# 可以查询表(自动化) +npx prisma db execute --stdin <<< "SELECT COUNT(*) FROM $table_name" +``` + + + + + +## 自定义 Hooks 和工具 + +**存在检查:** +```bash +# 文件存在且导出函数 +[ -f "$hook_path" ] && grep -E "export (default )?(function|const)" "$hook_path" +``` + +**实质性检查:** +```bash +# Hook 使用 React hooks(对于自定义 hooks) +grep -E "useState|useEffect|useCallback|useMemo|useRef|useContext" "$hook_path" + +# 有有意义的返回值 +grep -E "return \{|return \[" "$hook_path" + +# 超过琐碎长度 +[ $(wc -l < "$hook_path") -gt 10 ] +``` + +**Hooks 特有的存根模式:** +```typescript +// 危险信号 - 这些是存根: +export function useAuth() { + return { user: null, login: () => {}, logout: () => {} } +} + +export function useCart() { + const [items, setItems] = useState([]) + return { items, addItem: () => console.log('add'), removeItem: () => {} } +} + +// 硬编码返回: +export function useUser() { + return { name: "Test User", email: "test@example.com" } +} +``` + +**连接检查:** +```bash +# Hook 实际在某处被导入 +grep -r "import.*$hook_name" src/ --include="*.tsx" --include="*.ts" | grep -v "$hook_path" + +# Hook 实际被调用 +grep -r "$hook_name()" src/ --include="*.tsx" --include="*.ts" | grep -v "$hook_path" +``` + + + + + +## 环境变量和配置 + +**存在检查:** +```bash +# .env 文件存在 +[ -f ".env" ] || [ -f ".env.local" ] + +# 必需变量已定义 +grep -E "^$VAR_NAME=" .env .env.local 2>/dev/null +``` + +**实质性检查:** +```bash +# 变量有实际值(非占位符) +grep -E "^$VAR_NAME=.+" .env .env.local 2>/dev/null | grep -v "your-.*-here|xxx|placeholder|TODO" -i + +# 值对类型看起来有效: +# - URL 应以 http 开头 +# - 密钥应足够长 +# - 布尔值应为 true/false +``` + +**环境变量特有的存根模式:** +```bash +# 危险信号 - 这些是存根: +DATABASE_URL=your-database-url-here +STRIPE_SECRET_KEY=sk_test_xxx +API_KEY=placeholder +NEXT_PUBLIC_API_URL=http://localhost:3000 # 生产环境仍指向 localhost +``` + +**连接检查:** +```bash +# 变量实际在代码中使用 +grep -r "process\.env\.$VAR_NAME|env\.$VAR_NAME" src/ --include="*.ts" --include="*.tsx" + +# 变量在验证模式中(如果使用 zod 等验证 env) +grep -E "$VAR_NAME" src/env.ts src/env.mjs 2>/dev/null +``` + + + + + +## 连接验证模式 + +连接验证检查组件是否实际通信。这是大多数存根隐藏的地方。 + +### 模式:组件 → API + +**检查:** 组件是否实际调用 API? + +```bash +# 查找 fetch/axios 调用 +grep -E "fetch\(['\"].*$api_path|axios\.(get|post).*$api_path" "$component_path" + +# 验证未被注释掉 +grep -E "fetch\(|axios\." "$component_path" | grep -v "^.*//.*fetch" + +# 检查响应被使用 +grep -E "await.*fetch|\.then\(|setData|setState" "$component_path" +``` + +**危险信号:** +```typescript +// Fetch 存在但响应被忽略: +fetch('/api/messages') // 无 await,无 .then,无赋值 + +// Fetch 在注释中: +// fetch('/api/messages').then(r => r.json()).then(setMessages) + +// Fetch 到错误的端点: +fetch('/api/message') // 拼写错误 - 应该是 /api/messages +``` + +### 模式:API → 数据库 + +**检查:** API 路由是否实际查询数据库? + +```bash +# 查找数据库调用 +grep -E "prisma\.$model|db\.query|Model\.find" "$route_path" + +# 验证被 await +grep -E "await.*prisma|await.*db\." "$route_path" + +# 检查结果被返回 +grep -E "return.*json.*data|res\.json.*result" "$route_path" +``` + +**危险信号:** +```typescript +// 查询存在但结果未返回: +await prisma.message.findMany() +return Response.json({ ok: true }) // 返回静态值,非查询结果 + +// 查询未被 await: +const messages = prisma.message.findMany() // 缺少 await +return Response.json(messages) // 返回 Promise,非数据 +``` + +### 模式:表单 → 处理器 + +**检查:** 表单提交是否实际做些什么? + +```bash +# 查找 onSubmit 处理器 +grep -E "onSubmit=\{|handleSubmit" "$component_path" + +# 检查处理器有内容 +grep -A 10 "onSubmit.*=" "$component_path" | grep -E "fetch|axios|mutate|dispatch" + +# 验证不仅仅是 preventDefault +grep -A 5 "onSubmit" "$component_path" | grep -v "only.*preventDefault" -i +``` + +**危险信号:** +```typescript +// 处理器仅阻止默认: +onSubmit={(e) => e.preventDefault()} + +// 处理器仅日志: +const handleSubmit = (data) => { + console.log(data) +} + +// 处理器为空: +onSubmit={() => {}} +``` + +### 模式:状态 → 渲染 + +**检查:** 组件是否渲染状态,而非硬编码内容? + +```bash +# 查找 JSX 中的状态使用 +grep -E "\{.*messages.*\}|\{.*data.*\}|\{.*items.*\}" "$component_path" + +# 检查状态的 map/render +grep -E "\.map\(|\.filter\(|\.reduce\(" "$component_path" + +# 验证动态内容 +grep -E "\{[a-zA-Z_]+\." "$component_path" # 变量插值 +``` + +**危险信号:** +```tsx +// 硬编码而非状态: +return
+

Message 1

+

Message 2

+
+ +// 状态存在但未渲染: +const [messages, setMessages] = useState([]) +return
No messages
// 总是显示 "no messages" + +// 渲染错误的状态: +const [messages, setMessages] = useState([]) +return
{otherData.map(...)}
// 使用不同数据 +``` + +
+ + + +## 快速验证清单 + +对于每种工件类型,运行此清单: + +### 组件清单 +- [ ] 文件存在于预期路径 +- [ ] 导出函数/const 组件 +- [ ] 返回 JSX(非 null/空) +- [ ] 渲染中无占位符文本 +- [ ] 使用 props 或 state(非静态) +- [ ] 事件处理器有真实实现 +- [ ] 导入正确解析 +- [ ] 在应用某处被使用 + +### API 路由清单 +- [ ] 文件存在于预期路径 +- [ ] 导出 HTTP 方法处理器 +- [ ] 处理器超过 5 行 +- [ ] 查询数据库或服务 +- [ ] 返回有意义的响应(非空/占位符) +- [ ] 有错误处理 +- [ ] 验证输入 +- [ ] 从前端调用 + +### 模式清单 +- [ ] 模型/表已定义 +- [ ] 有所有预期字段 +- [ ] 字段有适当类型 +- [ ] 如需要关系已定义 +- [ ] 迁移存在且已应用 +- [ ] 客户端已生成 + +### Hook/工具清单 +- [ ] 文件存在于预期路径 +- [ ] 导出函数 +- [ ] 有有意义的实现(非空返回) +- [ ] 在应用某处被使用 +- [ ] 返回值被消费 + +### 连接清单 +- [ ] 组件 → API: fetch/axios 调用存在且使用响应 +- [ ] API → 数据库: 查询存在且结果返回 +- [ ] 表单 → 处理器: onSubmit 调用 API/mutation +- [ ] 状态 → 渲染: 状态变量出现在 JSX 中 + + + + + +## 自动化验证方法 + +对于验证子代理,使用此模式: + +```bash +# 1. 检查存在 +check_exists() { + [ -f "$1" ] && echo "EXISTS: $1" || echo "MISSING: $1" +} + +# 2. 检查存根模式 +check_stubs() { + local file="$1" + local stubs=$(grep -c -E "TODO|FIXME|placeholder|not implemented" "$file" 2>/dev/null || echo 0) + [ "$stubs" -gt 0 ] && echo "STUB_PATTERNS: $stubs in $file" +} + +# 3. 检查连接(组件调用 API) +check_wiring() { + local component="$1" + local api_path="$2" + grep -q "$api_path" "$component" && echo "WIRED: $component → $api_path" || echo "NOT_WIRED: $component → $api_path" +} + +# 4. 检查实质性(超过 N 行,有预期模式) +check_substantive() { + local file="$1" + local min_lines="$2" + local pattern="$3" + local lines=$(wc -l < "$file" 2>/dev/null || echo 0) + local has_pattern=$(grep -c -E "$pattern" "$file" 2>/dev/null || echo 0) + [ "$lines" -ge "$min_lines" ] && [ "$has_pattern" -gt 0 ] && echo "SUBSTANTIVE: $file" || echo "THIN: $file ($lines lines, $has_pattern matches)" +} +``` + +对每个必须有工件运行这些检查。汇总结果到 VERIFICATION.md。 + + + + + +## 何时需要人工验证 + +有些事情无法编程验证。标记这些需要人工测试: + +**始终人工:** +- 视觉外观(看起来对吗?) +- 用户流程完成(能实际做那件事吗?) +- 实时行为(WebSocket、SSE) +- 外部服务集成(Stripe、邮件发送) +- 错误消息清晰度(消息有帮助吗?) +- 性能感觉(感觉快吗?) + +**如不确定则人工:** +- grep 无法追踪的复杂连接 +- 依赖状态的动态行为 +- 边缘情况和错误状态 +- 移动端响应式 +- 无障碍性 + +**人工验证请求格式:** +```markdown +## 需要人工验证 + +### 1. 聊天消息发送 +**测试:** 输入消息并点击发送 +**预期:** 消息出现在列表中,输入框清空 +**检查:** 刷新后消息是否持久? + +### 2. 错误处理 +**测试:** 断开网络,尝试发送 +**预期:** 错误消息出现,消息未丢失 +**检查:** 重连后能重试吗? +``` + + + + + +## 检查点前自动化 + +关于自动化优先的检查点模式、服务器生命周期管理、CLI 安装处理和错误恢复协议,请参阅: + +**@~/.claude/get-shit-done/references/checkpoints.md** → `` 部分 + +关键原则: +- Claude 在呈现检查点**之前**设置验证环境 +- 用户从不运行 CLI 命令(仅访问 URL) +- 服务器生命周期:检查点前启动、处理端口冲突、持续运行 +- CLI 安装:安全处自动安装,否则检查点让用户选择 +- 错误处理:检查点前修复损坏环境,绝不呈现有失败设置的检查点 + + \ No newline at end of file From 63af9af0f48b664f94d2acd239fb1e395c302b95 Mon Sep 17 00:00:00 2001 From: Elliot Drel <2superfirebolt@gmail.com> Date: Mon, 16 Mar 2026 23:49:34 -0400 Subject: [PATCH 02/83] fix: track hook files in manifest for local patch detection (#769) Hook files (gsd-statusline.js, gsd-check-update.js, gsd-context-monitor.js) are installed during updates but were never included in gsd-file-manifest.json. This means saveLocalPatches() could not detect user modifications to hooks, causing them to be silently overwritten on update with no backup. Add hooks/gsd-*.js to writeManifest() so the existing local patch detection system automatically backs up modified hooks to gsd-local-patches/ before overwriting, matching the behavior already in place for workflows, commands, and agents. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/install.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/bin/install.js b/bin/install.js index cff9bb5f8..c2ee9aee3 100755 --- a/bin/install.js +++ b/bin/install.js @@ -2314,6 +2314,18 @@ function writeManifest(configDir, runtime = 'claude') { } } } + // Track hook files so saveLocalPatches() can detect user modifications + // Hooks are only installed for runtimes that use settings.json (not Codex/Copilot) + if (!isCodex && !isCopilot) { + const hooksDir = path.join(configDir, 'hooks'); + if (fs.existsSync(hooksDir)) { + for (const file of fs.readdirSync(hooksDir)) { + if (file.startsWith('gsd-') && file.endsWith('.js')) { + manifest.files['hooks/' + file] = fileHash(path.join(hooksDir, file)); + } + } + } + } fs.writeFileSync(path.join(configDir, MANIFEST_NAME), JSON.stringify(manifest, null, 2)); return manifest; From 10294128c908b258e5b624712f56c3cf73776f2f Mon Sep 17 00:00:00 2001 From: Medhat Galal Date: Tue, 17 Mar 2026 08:10:59 -0400 Subject: [PATCH 03/83] Fix duplicate Codex GSD agent blocks without marker --- bin/install.js | 17 ++++++++++++++--- tests/codex-config.test.cjs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/bin/install.js b/bin/install.js index cff9bb5f8..68a87e3f2 100755 --- a/bin/install.js +++ b/bin/install.js @@ -857,6 +857,10 @@ function generateCodexConfigBlock(agents) { return lines.join('\n'); } +function stripCodexGsdAgentSections(content) { + return content.replace(/^\[agents\.gsd-[^\]]+\]\n(?:(?!\[)[^\n]*\n?)*/gm, ''); +} + /** * Strip GSD sections from Codex config.toml content. * Returns cleaned content, or null if file would be empty. @@ -882,7 +886,7 @@ function stripGsdFromCodexConfig(content) { cleaned = cleaned.replace(/^default_mode_request_user_input\s*=\s*true\s*\n?/m, ''); // Remove [agents.gsd-*] sections (from header to next section or EOF) - cleaned = cleaned.replace(/^\[agents\.gsd-[^\]]+\]\n(?:(?!\[)[^\n]*\n?)*/gm, ''); + cleaned = stripCodexGsdAgentSections(cleaned); // Remove [features] section if now empty (only header, no keys before next section) cleaned = cleaned.replace(/^\[features\]\s*\n(?=\[|$)/m, ''); @@ -916,7 +920,7 @@ function mergeCodexConfig(configPath, gsdBlock) { let before = existing.substring(0, markerIndex).trimEnd(); if (before) { // Strip any GSD-managed sections that leaked above the marker from previous installs - before = before.replace(/^\[agents\.gsd-[^\]]+\]\n(?:(?!\[)[^\n]*\n?)*/gm, ''); + before = stripCodexGsdAgentSections(before); before = before.replace(/^\[agents\]\n(?:(?!\[)[^\n]*\n?)*/m, ''); before = before.replace(/\n{3,}/g, '\n\n').trimEnd(); @@ -929,7 +933,14 @@ function mergeCodexConfig(configPath, gsdBlock) { // Case 3: No marker — append GSD block let content = existing; - content = content.trimEnd() + '\n\n' + gsdBlock + '\n'; + content = stripCodexGsdAgentSections(content); + content = content.replace(/\n{3,}/g, '\n\n').trimEnd(); + + if (content) { + content = content + '\n\n' + gsdBlock + '\n'; + } else { + content = gsdBlock + '\n'; + } fs.writeFileSync(configPath, content); } diff --git a/tests/codex-config.test.cjs b/tests/codex-config.test.cjs index 4c2cd0aff..2ea3a91af 100644 --- a/tests/codex-config.test.cjs +++ b/tests/codex-config.test.cjs @@ -354,6 +354,36 @@ describe('mergeCodexConfig', () => { assert.ok(content.includes('[agents.gsd-executor]'), 'has agent'); }); + test('case 3 strips existing [agents.gsd-*] sections before appending fresh block', () => { + const configPath = path.join(tmpDir, 'config.toml'); + const existing = [ + '[model]', + 'name = "o3"', + '', + '[agents.custom-agent]', + 'description = "user agent"', + '', + '', + '[agents.gsd-executor]', + 'description = "old"', + 'config_file = "agents/gsd-executor.toml"', + '', + ].join('\n'); + fs.writeFileSync(configPath, existing); + + mergeCodexConfig(configPath, sampleBlock); + + const content = fs.readFileSync(configPath, 'utf8'); + const gsdAgentCount = (content.match(/^\[agents\.gsd-executor\]\s*$/gm) || []).length; + const markerCount = (content.match(new RegExp(GSD_CODEX_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length; + + assert.ok(content.includes('[model]'), 'preserves user content'); + assert.ok(content.includes('[agents.custom-agent]'), 'preserves non-GSD agent section'); + assert.strictEqual(gsdAgentCount, 1, 'keeps exactly one GSD agent section'); + assert.strictEqual(markerCount, 1, 'adds exactly one marker block'); + assert.ok(!/\n{3,}# GSD Agent Configuration/.test(content), 'does not leave extra blank lines before marker block'); + }); + test('idempotent: re-merge produces same result', () => { const configPath = path.join(tmpDir, 'config.toml'); mergeCodexConfig(configPath, sampleBlock); From 5a7d56e6c54e7a6d86b58ab10c6610d825e008cd Mon Sep 17 00:00:00 2001 From: CI Date: Tue, 17 Mar 2026 20:24:35 +0800 Subject: [PATCH 04/83] fix(codex): include required agent metadata in TOML --- bin/install.js | 12 ++++++++++-- tests/codex-config.test.cjs | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/bin/install.js b/bin/install.js index cff9bb5f8..f7e230b66 100755 --- a/bin/install.js +++ b/bin/install.js @@ -819,14 +819,22 @@ purpose: ${toSingleLine(description)} /** * Generate a per-agent .toml config file for Codex. - * Sets sandbox_mode and developer_instructions from the agent markdown body. + * Sets required agent metadata, sandbox_mode, and developer_instructions + * from the agent markdown content. */ function generateCodexAgentToml(agentName, agentContent) { const sandboxMode = CODEX_AGENT_SANDBOX[agentName] || 'read-only'; - const { body } = extractFrontmatterAndBody(agentContent); + const { frontmatter, body } = extractFrontmatterAndBody(agentContent); + const frontmatterText = frontmatter || ''; + const resolvedName = extractFrontmatterField(frontmatterText, 'name') || agentName; + const resolvedDescription = toSingleLine( + extractFrontmatterField(frontmatterText, 'description') || `GSD agent ${resolvedName}` + ); const instructions = body.trim(); const lines = [ + `name = ${JSON.stringify(resolvedName)}`, + `description = ${JSON.stringify(resolvedDescription)}`, `sandbox_mode = "${sandboxMode}"`, // Agent prompts contain raw backslashes in regexes and shell snippets. // TOML literal multiline strings preserve them without escape parsing. diff --git a/tests/codex-config.test.cjs b/tests/codex-config.test.cjs index 4c2cd0aff..42b3bb850 100644 --- a/tests/codex-config.test.cjs +++ b/tests/codex-config.test.cjs @@ -160,6 +160,19 @@ tools: Read, Grep, Glob assert.ok(result.includes("'''"), 'has closing literal triple quotes'); }); + test('includes required name and description fields', () => { + const result = generateCodexAgentToml('gsd-executor', sampleAgent); + assert.ok(result.includes('name = "gsd-executor"'), 'has name'); + assert.ok(result.includes('description = "Executes plans"'), 'has description'); + }); + + test('falls back to generated description when frontmatter is missing fields', () => { + const minimalAgent = `You are an unknown agent.`; + const result = generateCodexAgentToml('gsd-unknown', minimalAgent); + assert.ok(result.includes('name = "gsd-unknown"'), 'falls back to agent name'); + assert.ok(result.includes('description = "GSD agent gsd-unknown"'), 'falls back to synthetic description'); + }); + test('defaults unknown agents to read-only', () => { const result = generateCodexAgentToml('gsd-unknown', sampleAgent); assert.ok(result.includes('sandbox_mode = "read-only"'), 'defaults to read-only'); @@ -485,10 +498,13 @@ describe('installCodexConfig (integration)', () => { assert.ok(fs.existsSync(path.join(agentsDir, 'gsd-plan-checker.toml')), 'plan-checker .toml exists'); const executorToml = fs.readFileSync(path.join(agentsDir, 'gsd-executor.toml'), 'utf8'); + assert.ok(executorToml.includes('name = "gsd-executor"'), 'executor has name'); + assert.ok(executorToml.includes('description = "Executes GSD plans with atomic commits, deviation handling, checkpoint protocols, and state management. Spawned by execute-phase orchestrator or execute-plan command."'), 'executor has description'); assert.ok(executorToml.includes('sandbox_mode = "workspace-write"'), 'executor is workspace-write'); assert.ok(executorToml.includes('developer_instructions'), 'has developer_instructions'); const checkerToml = fs.readFileSync(path.join(agentsDir, 'gsd-plan-checker.toml'), 'utf8'); + assert.ok(checkerToml.includes('name = "gsd-plan-checker"'), 'plan-checker has name'); assert.ok(checkerToml.includes('sandbox_mode = "read-only"'), 'plan-checker is read-only'); }); }); From 52b2d390ccfc76729bfbe69a0a3c1389cbddafba Mon Sep 17 00:00:00 2001 From: Colin Date: Tue, 17 Mar 2026 11:00:24 -0400 Subject: [PATCH 05/83] feat(milestones): support safe phase-number resets --- docs/COMMANDS.md | 2 ++ get-shit-done/bin/lib/init.cjs | 32 ++++++++++++++++++++ get-shit-done/workflows/help.md | 2 ++ get-shit-done/workflows/new-milestone.md | 38 +++++++++++++++++++++--- tests/init.test.cjs | 29 ++++++++++++++++++ 5 files changed, 99 insertions(+), 4 deletions(-) diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 5f1c9c9fe..750fca646 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -183,6 +183,7 @@ Start next version cycle. | Argument | Required | Description | |----------|----------|-------------| | `name` | No | Milestone name | +| `--reset-phase-numbers` | No | Restart the new milestone at Phase 1 and archive old phase dirs before roadmapping | **Prerequisites:** Previous milestone completed **Produces:** Updated `PROJECT.md`, new `REQUIREMENTS.md`, new `ROADMAP.md` @@ -190,6 +191,7 @@ Start next version cycle. ```bash /gsd:new-milestone # Interactive /gsd:new-milestone "v2.0 Mobile" # Named milestone +/gsd:new-milestone --reset-phase-numbers "v2.0 Mobile" # Restart milestone numbering at 1 ``` --- diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index d29e533c8..fd8d48de1 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -7,6 +7,23 @@ const path = require('path'); const { execSync } = require('child_process'); const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, normalizePhaseName, toPosixPath, output, error } = require('./core.cjs'); +function getLatestCompletedMilestone(cwd) { + const milestonesPath = path.join(cwd, '.planning', 'MILESTONES.md'); + if (!fs.existsSync(milestonesPath)) return null; + + try { + const content = fs.readFileSync(milestonesPath, 'utf-8'); + const match = content.match(/^##\s+(v[\d.]+)\s+(.+?)\s+\(Shipped:/m); + if (!match) return null; + return { + version: match[1], + name: match[2].trim(), + }; + } catch { + return null; + } +} + function cmdInitExecutePhase(cwd, phase, raw) { if (!phase) { error('phase required for init execute-phase'); @@ -221,6 +238,17 @@ function cmdInitNewProject(cwd, raw) { function cmdInitNewMilestone(cwd, raw) { const config = loadConfig(cwd); const milestone = getMilestoneInfo(cwd); + const latestCompleted = getLatestCompletedMilestone(cwd); + const phasesDir = path.join(cwd, '.planning', 'phases'); + let phaseDirCount = 0; + + try { + if (fs.existsSync(phasesDir)) { + phaseDirCount = fs.readdirSync(phasesDir, { withFileTypes: true }) + .filter(entry => entry.isDirectory()) + .length; + } + } catch {} const result = { // Models @@ -235,6 +263,10 @@ function cmdInitNewMilestone(cwd, raw) { // Current milestone current_milestone: milestone.version, current_milestone_name: milestone.name, + latest_completed_milestone: latestCompleted?.version || null, + latest_completed_milestone_name: latestCompleted?.name || null, + phase_dir_count: phaseDirCount, + phase_archive_path: latestCompleted ? `.planning/milestones/${latestCompleted.version}-phases` : null, // File existence project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'), diff --git a/get-shit-done/workflows/help.md b/get-shit-done/workflows/help.md index 058d4a810..d1af5c4d6 100644 --- a/get-shit-done/workflows/help.md +++ b/get-shit-done/workflows/help.md @@ -192,10 +192,12 @@ Start a new milestone through unified flow. - Optional domain research (spawns 4 parallel researcher agents) - Requirements definition with scoping - Roadmap creation with phase breakdown +- Optional `--reset-phase-numbers` flag restarts numbering at Phase 1 and archives old phase dirs first for safety Mirrors `/gsd:new-project` flow for brownfield projects (existing PROJECT.md). Usage: `/gsd:new-milestone "v2.0 Features"` +Usage: `/gsd:new-milestone --reset-phase-numbers "v2.0 Features"` **`/gsd:complete-milestone `** Archive completed milestone and prepare for next version. diff --git a/get-shit-done/workflows/new-milestone.md b/get-shit-done/workflows/new-milestone.md index 59b894bbb..0b818719a 100644 --- a/get-shit-done/workflows/new-milestone.md +++ b/get-shit-done/workflows/new-milestone.md @@ -14,6 +14,12 @@ Read all files referenced by the invoking prompt's execution_context before star ## 1. Load Context +Parse `$ARGUMENTS` before doing anything else: +- `--reset-phase-numbers` flag → opt into restarting roadmap phase numbering at `1` +- remaining text → use as milestone name if present + +If the flag is absent, keep the current behavior of continuing phase numbering from the previous milestone. + - Read PROJECT.md (existing project, validated requirements, decisions) - Read MILESTONES.md (what shipped previously) - Read STATE.md (pending todos, blockers) @@ -82,7 +88,27 @@ INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init new-milestone) if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi ``` -Extract from init JSON: `researcher_model`, `synthesizer_model`, `roadmapper_model`, `commit_docs`, `research_enabled`, `current_milestone`, `project_exists`, `roadmap_exists`. +Extract from init JSON: `researcher_model`, `synthesizer_model`, `roadmapper_model`, `commit_docs`, `research_enabled`, `current_milestone`, `project_exists`, `roadmap_exists`, `latest_completed_milestone`, `phase_dir_count`, `phase_archive_path`. + +## 7.5 Reset-phase safety (only when `--reset-phase-numbers`) + +If `--reset-phase-numbers` is active: + +1. Set starting phase number to `1` for the upcoming roadmap. +2. If `phase_dir_count > 0`, archive the old phase directories before roadmapping so new `01-*` / `02-*` directories cannot collide with stale milestone directories. + +If `phase_dir_count > 0` and `phase_archive_path` is available: + +```bash +mkdir -p "${phase_archive_path}" +find .planning/phases -mindepth 1 -maxdepth 1 -type d -exec mv {} "${phase_archive_path}/" \; +``` + +Then verify `.planning/phases/` no longer contains old milestone directories before continuing. + +If `phase_dir_count > 0` but `phase_archive_path` is missing: +- Stop and explain that reset numbering is unsafe without a completed milestone archive target. +- Tell the user to complete/archive the previous milestone first, then rerun `/gsd:new-milestone --reset-phase-numbers`. ## 8. Research Decision @@ -270,7 +296,9 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: define milest ◆ Spawning roadmapper... ``` -**Starting phase number:** Read MILESTONES.md for last phase number. Continue from there (v1.0 ended at phase 5 → v1.1 starts at phase 6). +**Starting phase number:** +- If `--reset-phase-numbers` is active, start at **Phase 1** +- Otherwise, continue from the previous milestone's last phase number (v1.0 ended at phase 5 → v1.1 starts at phase 6) ``` Task(prompt=" @@ -286,7 +314,9 @@ Task(prompt=" Create roadmap for milestone v[X.Y]: -1. Start phase numbering from [N] +1. Respect the selected numbering mode: + - `--reset-phase-numbers` → start at Phase 1 + - default behavior → continue from the previous milestone's last phase number 2. Derive phases from THIS MILESTONE's requirements only 3. Map every requirement to exactly one phase 4. Derive 2-5 success criteria per phase (observable user behaviors) @@ -378,7 +408,7 @@ Also: `/gsd:plan-phase [N]` — skip discussion, plan directly - [ ] gsd-roadmapper spawned with phase numbering context - [ ] Roadmap files written immediately (not draft) - [ ] User feedback incorporated (if any) -- [ ] ROADMAP.md phases continue from previous milestone +- [ ] Phase numbering mode respected (continued or reset) - [ ] All commits made (if planning docs committed) - [ ] User knows next step: `/gsd:discuss-phase [N]` diff --git a/tests/init.test.cjs b/tests/init.test.cjs index 66a645d7d..5e67a7018 100644 --- a/tests/init.test.cjs +++ b/tests/init.test.cjs @@ -907,6 +907,35 @@ describe('cmdInitNewMilestone', () => { assert.strictEqual(output2.roadmap_exists, true); assert.strictEqual(output2.project_exists, true); }); + + test('reports latest completed milestone and archive target for reset flow', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'MILESTONES.md'), + '# Milestones\n\n## v1.2 Search Refresh (Shipped: 2026-02-18)\n\n---\n' + ); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06-refine-search'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '07-polish'), { recursive: true }); + + const result = runGsdTools('init new-milestone', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.latest_completed_milestone, 'v1.2'); + assert.strictEqual(output.latest_completed_milestone_name, 'Search Refresh'); + assert.strictEqual(output.phase_dir_count, 2); + assert.strictEqual(output.phase_archive_path, '.planning/milestones/v1.2-phases'); + }); + + test('reset flow metadata is null-safe when no milestones file exists', () => { + const result = runGsdTools('init new-milestone', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.latest_completed_milestone, null); + assert.strictEqual(output.latest_completed_milestone_name, null); + assert.strictEqual(output.phase_dir_count, 0); + assert.strictEqual(output.phase_archive_path, null); + }); }); // ───────────────────────────────────────────────────────────────────────────── From 6b6c73256d3cf833d5871b316a0cc6092687107b Mon Sep 17 00:00:00 2001 From: Colin Date: Tue, 17 Mar 2026 11:08:32 -0400 Subject: [PATCH 06/83] fix(health): preserve state on phase mismatch repair --- get-shit-done/bin/lib/verify.cjs | 8 ++++-- get-shit-done/workflows/health.md | 8 +++--- tests/verify-health.test.cjs | 44 ++++++++++++++++++------------- 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/get-shit-done/bin/lib/verify.cjs b/get-shit-done/bin/lib/verify.cjs index 9f8a08546..5994bc1e9 100644 --- a/get-shit-done/bin/lib/verify.cjs +++ b/get-shit-done/bin/lib/verify.cjs @@ -606,8 +606,12 @@ function cmdValidateHealth(cwd, options, raw) { if (!diskPhases.has(ref) && !diskPhases.has(normalizedRef) && !diskPhases.has(String(parseInt(ref, 10)))) { // Only warn if phases dir has any content (not just an empty project) if (diskPhases.size > 0) { - addIssue('warning', 'W002', `STATE.md references phase ${ref}, but only phases ${[...diskPhases].sort().join(', ')} exist`, 'Run /gsd:health --repair to regenerate STATE.md', true); - if (!repairs.includes('regenerateState')) repairs.push('regenerateState'); + addIssue( + 'warning', + 'W002', + `STATE.md references phase ${ref}, but only phases ${[...diskPhases].sort().join(', ')} exist`, + 'Review STATE.md manually before changing it; /gsd:health --repair will not overwrite an existing STATE.md for phase mismatches' + ); } } } diff --git a/get-shit-done/workflows/health.md b/get-shit-done/workflows/health.md index 54b7a1423..c2d76b70f 100644 --- a/get-shit-done/workflows/health.md +++ b/get-shit-done/workflows/health.md @@ -72,8 +72,8 @@ Errors: N | Warnings: N | Info: N ``` ## Warnings -- [W001] STATE.md references phase 5, but only phases 1-3 exist - Fix: Run /gsd:health --repair to regenerate +- [W002] STATE.md references phase 5, but only phases 1-3 exist + Fix: Review STATE.md manually before changing it; repair will not overwrite an existing STATE.md - [W005] Phase directory "1-setup" doesn't follow NN-name format Fix: Rename to match pattern (e.g., 01-setup) @@ -130,7 +130,7 @@ Report final status. | E004 | error | STATE.md not found | Yes | | E005 | error | config.json parse error | Yes | | W001 | warning | PROJECT.md missing required section | No | -| W002 | warning | STATE.md references invalid phase | Yes | +| W002 | warning | STATE.md references invalid phase | No | | W003 | warning | config.json not found | Yes | | W004 | warning | config.json invalid field value | No | | W005 | warning | Phase directory naming mismatch | No | @@ -148,7 +148,7 @@ Report final status. |--------|--------|------| | createConfig | Create config.json with defaults | None | | resetConfig | Delete + recreate config.json | Loses custom settings | -| regenerateState | Create STATE.md from ROADMAP structure | Loses session history | +| regenerateState | Create STATE.md from ROADMAP structure when it is missing | Loses session history | | addNyquistKey | Add workflow.nyquist_validation: true to config.json | None — matches existing default | **Not repairable (too risky):** diff --git a/tests/verify-health.test.cjs b/tests/verify-health.test.cjs index c06a8404a..f22dd3ad7 100644 --- a/tests/verify-health.test.cjs +++ b/tests/verify-health.test.cjs @@ -191,10 +191,9 @@ describe('validate health command', () => { assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); - assert.ok( - output.warnings.some(w => w.code === 'W002'), - `Expected W002 in warnings: ${JSON.stringify(output.warnings)}` - ); + const w002 = output.warnings.find(w => w.code === 'W002'); + assert.ok(w002, `Expected W002 in warnings: ${JSON.stringify(output.warnings)}`); + assert.strictEqual(w002.repairable, false, 'W002 should not be auto-repairable'); }); // ─── Check 5: config.json valid JSON + valid schema ─────────────────────── @@ -613,16 +612,13 @@ describe('validate health --repair command', () => { assert.ok(stateContent.includes('# Session State'), 'regenerated STATE.md should contain "# Session State"'); }); - test('backs up existing STATE.md before regenerating', () => { + test('does not rewrite existing STATE.md for invalid phase references', () => { writeValidConfigJson(tmpDir); const statePath = path.join(tmpDir, '.planning', 'STATE.md'); - const originalContent = '# Session State\n\nOriginal content here.\n'; - fs.writeFileSync(statePath, originalContent); - - // Make STATE.md reference a nonexistent phase so repair is triggered + const originalContent = '# Session State\n\nPhase 99 is current.\n'; fs.writeFileSync( statePath, - '# Session State\n\nPhase 99 is current.\n' + originalContent ); const result = runGsdTools('validate health --repair', tmpDir); @@ -630,19 +626,17 @@ describe('validate health --repair command', () => { const output = JSON.parse(result.output); assert.ok( - Array.isArray(output.repairs_performed), - `Expected repairs_performed: ${JSON.stringify(output)}` + !Array.isArray(output.repairs_performed) || !output.repairs_performed.some(r => r.action === 'regenerateState'), + `Did not expect regenerateState for W002: ${JSON.stringify(output)}` ); - // Verify a .bak- file exists alongside STATE.md + const stateContent = fs.readFileSync(statePath, 'utf-8'); + assert.strictEqual(stateContent, originalContent, 'existing STATE.md should be preserved'); + const planningDir = path.join(tmpDir, '.planning'); const planningFiles = fs.readdirSync(planningDir); const backupFile = planningFiles.find(f => f.startsWith('STATE.md.bak-')); - assert.ok(backupFile, `Expected a STATE.md.bak- file. Found files: ${planningFiles.join(', ')}`); - - // Verify backup contains the original content - const backupContent = fs.readFileSync(path.join(planningDir, backupFile), 'utf-8'); - assert.ok(backupContent.includes('Phase 99'), 'backup should contain the original STATE.md content'); + assert.strictEqual(backupFile, undefined, `Did not expect backup file for non-destructive repair. Found: ${planningFiles.join(', ')}`); }); test('adds nyquist_validation key to config.json via addNyquistKey repair', () => { @@ -688,4 +682,18 @@ describe('validate health --repair command', () => { `Expected repairable_count >= 2, got ${output.repairable_count}. Full output: ${JSON.stringify(output)}` ); }); + + test('phase mismatch warnings do not count as repairable issues', () => { + writeValidConfigJson(tmpDir); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + '# Session State\n\nPhase 99 is the current phase.\n' + ); + + const result = runGsdTools('validate health', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.repairable_count, 0, `Expected no repairable issues for W002: ${JSON.stringify(output)}`); + }); }); From 7156f02ed5c8d07962d68fbaf9d34276caa87bef Mon Sep 17 00:00:00 2001 From: 0Shard Date: Tue, 17 Mar 2026 17:18:48 +0200 Subject: [PATCH 07/83] fix: semver 3+ segment parsing and CRLF frontmatter corruption recovery - fix(core): getMilestoneInfo() version regex `\d+\.\d+` only matched 2-segment versions (v1.2). Changed to `\d+(?:\.\d+)+` to support 3+ segments (v1.2.1, v2.0.1). Same fix in roadmap.cjs milestone extraction pattern. - fix(state): stripFrontmatter() used `^---\n` (LF-only) which failed to strip CRLF frontmatter blocks. When STATE.md had dual frontmatter blocks from prior CRLF corruption, each writeStateMd() call preserved the stale block and prepended a new wrong one. Now handles CRLF and strips all stacked frontmatter blocks. - fix(frontmatter): extractFrontmatter() always used the first --- block. When dual blocks exist from corruption, the first is stale. Now uses the last block (most recent sync). --- get-shit-done/bin/lib/core.cjs | 10 ++++++---- get-shit-done/bin/lib/frontmatter.cjs | 6 +++++- get-shit-done/bin/lib/roadmap.cjs | 2 +- get-shit-done/bin/lib/state.cjs | 12 +++++++++++- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index 8c1b427f8..79bdb08c2 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -512,7 +512,8 @@ function getMilestoneInfo(cwd) { // First: check for list-format roadmaps using 🚧 (in-progress) marker // e.g. "- 🚧 **v2.1 Belgium** — Phases 24-28 (in progress)" - const inProgressMatch = roadmap.match(/🚧\s*\*\*v(\d+\.\d+)\s+([^*]+)\*\*/); + // e.g. "- 🚧 **v1.2.1 Tech Debt** — Phases 1-8 (in progress)" + const inProgressMatch = roadmap.match(/🚧\s*\*\*v(\d+(?:\.\d+)+)\s+([^*]+)\*\*/); if (inProgressMatch) { return { version: 'v' + inProgressMatch[1], @@ -523,15 +524,16 @@ function getMilestoneInfo(cwd) { // Second: heading-format roadmaps — strip shipped milestones in
blocks const cleaned = stripShippedMilestones(roadmap); // Extract version and name from the same ## heading for consistency - const headingMatch = cleaned.match(/## .*v(\d+\.\d+)[:\s]+([^\n(]+)/); + // Supports 2+ segment versions: v1.2, v1.2.1, v2.0.1, etc. + const headingMatch = cleaned.match(/## .*v(\d+(?:\.\d+)+)[:\s]+([^\n(]+)/); if (headingMatch) { return { version: 'v' + headingMatch[1], name: headingMatch[2].trim(), }; } - // Fallback: try bare version match - const versionMatch = cleaned.match(/v(\d+\.\d+)/); + // Fallback: try bare version match (greedy — capture longest version string) + const versionMatch = cleaned.match(/v(\d+(?:\.\d+)+)/); return { version: versionMatch ? versionMatch[0] : 'v1.0', name: 'milestone', diff --git a/get-shit-done/bin/lib/frontmatter.cjs b/get-shit-done/bin/lib/frontmatter.cjs index e5f500a68..d7bb698dd 100644 --- a/get-shit-done/bin/lib/frontmatter.cjs +++ b/get-shit-done/bin/lib/frontmatter.cjs @@ -10,7 +10,11 @@ const { safeReadFile, normalizeMd, output, error } = require('./core.cjs'); function extractFrontmatter(content) { const frontmatter = {}; - const match = content.match(/^---\r?\n([\s\S]+?)\r?\n---/); + // Find ALL frontmatter blocks at the start of the file. + // If multiple blocks exist (corruption from CRLF mismatch), use the LAST one + // since it represents the most recent state sync. + const allBlocks = [...content.matchAll(/(?:^|\n)\s*---\r?\n([\s\S]+?)\r?\n---/g)]; + const match = allBlocks.length > 0 ? allBlocks[allBlocks.length - 1] : null; if (!match) return frontmatter; const yaml = match[1]; diff --git a/get-shit-done/bin/lib/roadmap.cjs b/get-shit-done/bin/lib/roadmap.cjs index 3164b702c..466039284 100644 --- a/get-shit-done/bin/lib/roadmap.cjs +++ b/get-shit-done/bin/lib/roadmap.cjs @@ -181,7 +181,7 @@ function cmdRoadmapAnalyze(cwd, raw) { // Extract milestone info const milestones = []; - const milestonePattern = /##\s*(.*v(\d+\.\d+)[^(\n]*)/gi; + const milestonePattern = /##\s*(.*v(\d+(?:\.\d+)+)[^(\n]*)/gi; let mMatch; while ((mMatch = milestonePattern.exec(content)) !== null) { milestones.push({ diff --git a/get-shit-done/bin/lib/state.cjs b/get-shit-done/bin/lib/state.cjs index 40bf8d2cc..f96c73e19 100644 --- a/get-shit-done/bin/lib/state.cjs +++ b/get-shit-done/bin/lib/state.cjs @@ -664,7 +664,17 @@ function buildStateFrontmatter(bodyContent, cwd) { } function stripFrontmatter(content) { - return content.replace(/^---\n[\s\S]*?\n---\n*/, ''); + // Strip ALL frontmatter blocks at the start of the file. + // Handles CRLF line endings and multiple stacked blocks (corruption recovery). + // Greedy: keeps stripping ---...--- blocks separated by optional whitespace. + let result = content; + // eslint-disable-next-line no-constant-condition + while (true) { + const stripped = result.replace(/^\s*---\r?\n[\s\S]*?\r?\n---\s*/, ''); + if (stripped === result) break; + result = stripped; + } + return result; } function syncStateFrontmatter(content, cwd) { From f5167a5ca93902de6dc8503ba1572db55abe7667 Mon Sep 17 00:00:00 2001 From: Colin Date: Tue, 17 Mar 2026 11:41:44 -0400 Subject: [PATCH 08/83] feat(claude-md): add workflow enforcement guidance --- docs/COMMANDS.md | 2 +- get-shit-done/bin/lib/profile-output.cjs | 23 ++++++- get-shit-done/templates/claude-md.md | 23 ++++++- get-shit-done/workflows/new-project.md | 13 +++- tests/claude-md.test.cjs | 82 ++++++++++++++++++++++++ 5 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 tests/claude-md.test.cjs diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 5f1c9c9fe..a48670396 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -23,7 +23,7 @@ Initialize a new project with deep context gathering. | `--auto @file.md` | Auto-extract from document, skip interactive questions | **Prerequisites:** No existing `.planning/PROJECT.md` -**Produces:** `PROJECT.md`, `REQUIREMENTS.md`, `ROADMAP.md`, `STATE.md`, `config.json`, `research/` +**Produces:** `PROJECT.md`, `REQUIREMENTS.md`, `ROADMAP.md`, `STATE.md`, `config.json`, `research/`, `CLAUDE.md` ```bash /gsd:new-project # Interactive mode diff --git a/get-shit-done/bin/lib/profile-output.cjs b/get-shit-done/bin/lib/profile-output.cjs index ec6264f52..9d2add389 100644 --- a/get-shit-done/bin/lib/profile-output.cjs +++ b/get-shit-done/bin/lib/profile-output.cjs @@ -179,6 +179,17 @@ const CLAUDE_MD_FALLBACKS = { architecture: 'Architecture not yet mapped. Follow existing patterns found in the codebase.', }; +const CLAUDE_MD_WORKFLOW_ENFORCEMENT = [ + 'Before using Edit, Write, or other file-changing tools, start work through a GSD command so planning artifacts and execution context stay in sync.', + '', + 'Use these entry points:', + '- `/gsd:quick` for small fixes, doc updates, and ad-hoc tasks', + '- `/gsd:debug` for investigation and bug fixing', + '- `/gsd:execute-phase` for planned phase work', + '', + 'Do not make direct repo edits outside a GSD workflow unless the user explicitly asks to bypass it.', +].join('\n'); + const CLAUDE_MD_PROFILE_PLACEHOLDER = [ '', '## Developer Profile', @@ -356,6 +367,14 @@ function generateArchitectureSection(cwd) { return { content: summary, source: 'ARCHITECTURE.md', hasFallback: false }; } +function generateWorkflowSection() { + return { + content: CLAUDE_MD_WORKFLOW_ENFORCEMENT, + source: 'GSD defaults', + hasFallback: false, + }; +} + // ─── Commands ───────────────────────────────────────────────────────────────── function cmdWriteProfile(cwd, options, raw) { @@ -796,18 +815,20 @@ function cmdGenerateClaudeProfile(cwd, options, raw) { } function cmdGenerateClaudeMd(cwd, options, raw) { - const MANAGED_SECTIONS = ['project', 'stack', 'conventions', 'architecture']; + const MANAGED_SECTIONS = ['project', 'stack', 'conventions', 'architecture', 'workflow']; const generators = { project: generateProjectSection, stack: generateStackSection, conventions: generateConventionsSection, architecture: generateArchitectureSection, + workflow: generateWorkflowSection, }; const sectionHeadings = { project: '## Project', stack: '## Technology Stack', conventions: '## Conventions', architecture: '## Architecture', + workflow: '## GSD Workflow Enforcement', }; const generated = {}; diff --git a/get-shit-done/templates/claude-md.md b/get-shit-done/templates/claude-md.md index f19ec3f01..240146a28 100644 --- a/get-shit-done/templates/claude-md.md +++ b/get-shit-done/templates/claude-md.md @@ -2,8 +2,8 @@ Template for project-root `CLAUDE.md` — auto-generated by `gsd-tools generate-claude-md`. -Contains 5 marker-bounded sections. Each section is independently updatable. -The `generate-claude-md` subcommand manages 4 sections (project, stack, conventions, architecture). +Contains 6 marker-bounded sections. Each section is independently updatable. +The `generate-claude-md` subcommand manages 5 sections (project, stack, conventions, architecture, workflow enforcement). The profile section is managed exclusively by `generate-claude-profile`. --- @@ -66,6 +66,22 @@ Conventions not yet established. Will populate as patterns emerge during develop Architecture not yet mapped. Follow existing patterns found in the codebase. ``` +### Workflow Enforcement Section +``` + +## GSD Workflow Enforcement + +Before using Edit, Write, or other file-changing tools, start work through a GSD command so planning artifacts and execution context stay in sync. + +Use these entry points: +- `/gsd:quick` for small fixes, doc updates, and ad-hoc tasks +- `/gsd:debug` for investigation and bug fixing +- `/gsd:execute-phase` for planned phase work + +Do not make direct repo edits outside a GSD workflow unless the user explicitly asks to bypass it. + +``` + ### Profile Section (Placeholder Only) ``` @@ -88,7 +104,8 @@ CLAUDE.md file and no profile section exists yet. 2. **Stack** — Technology choices (what tools are used) 3. **Conventions** — Code patterns and rules (how code is written) 4. **Architecture** — System structure (how components fit together) -5. **Profile** — Developer behavioral preferences (how to interact) +5. **Workflow Enforcement** — Default GSD entry points for file-changing work +6. **Profile** — Developer behavioral preferences (how to interact) ## Marker Format diff --git a/get-shit-done/workflows/new-project.md b/get-shit-done/workflows/new-project.md index 7db0e6c6b..ed52d8b8e 100644 --- a/get-shit-done/workflows/new-project.md +++ b/get-shit-done/workflows/new-project.md @@ -1011,10 +1011,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:** + +```bash +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" generate-claude-md +``` + +This ensures new projects get the default GSD workflow-enforcement guidance and current project context in `CLAUDE.md`. + **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 +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 ``` ## 9. Done @@ -1035,6 +1043,7 @@ Present completion summary: | Research | `.planning/research/` | | Requirements | `.planning/REQUIREMENTS.md` | | Roadmap | `.planning/ROADMAP.md` | +| Project guide | `CLAUDE.md` | **[N] phases** | **[X] requirements** | Ready to build ✓ ``` @@ -1085,6 +1094,7 @@ Exit skill and invoke SlashCommand("/gsd:discuss-phase 1 --auto") - `.planning/REQUIREMENTS.md` - `.planning/ROADMAP.md` - `.planning/STATE.md` +- `CLAUDE.md` @@ -1106,6 +1116,7 @@ Exit skill and invoke SlashCommand("/gsd:discuss-phase 1 --auto") - [ ] ROADMAP.md created with phases, requirement mappings, success criteria - [ ] STATE.md initialized - [ ] REQUIREMENTS.md traceability updated +- [ ] CLAUDE.md generated with GSD workflow guidance - [ ] User knows next step is `/gsd:discuss-phase 1` **Atomic commits:** Each phase commits its artifacts immediately. If context is lost, artifacts persist. diff --git a/tests/claude-md.test.cjs b/tests/claude-md.test.cjs new file mode 100644 index 000000000..3443f7783 --- /dev/null +++ b/tests/claude-md.test.cjs @@ -0,0 +1,82 @@ +/** + * CLAUDE.md generation and new-project workflow tests + */ + +const { test, describe, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const path = require('path'); +const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs'); + +describe('generate-claude-md', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('creates CLAUDE.md with workflow enforcement section', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'PROJECT.md'), + '# Test Project\n\n## What This Is\n\nA small test project.\n' + ); + + const result = runGsdTools('generate-claude-md', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.action, 'created'); + assert.strictEqual(output.sections_total, 5); + assert.ok(output.sections_generated.includes('workflow')); + + const claudePath = path.join(tmpDir, 'CLAUDE.md'); + const content = fs.readFileSync(claudePath, 'utf-8'); + assert.ok(content.includes('## GSD Workflow Enforcement')); + assert.ok(content.includes('/gsd:quick')); + assert.ok(content.includes('/gsd:debug')); + assert.ok(content.includes('/gsd:execute-phase')); + assert.ok(content.includes('Do not make direct repo edits outside a GSD workflow')); + }); + + test('adds workflow enforcement section when updating an existing CLAUDE.md', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'PROJECT.md'), + '# Test Project\n\n## What This Is\n\nA small test project.\n' + ); + fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '## Local Notes\n\nKeep this intro.\n'); + + const result = runGsdTools('generate-claude-md', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.action, 'updated'); + + const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8'); + assert.ok(content.includes('## Local Notes')); + assert.ok(content.includes('## GSD Workflow Enforcement')); + }); +}); + +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', () => { + 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')); + }); + + test('new-project artifacts mention CLAUDE.md', () => { + 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`')); + assert.ok(commandsContent.includes('`CLAUDE.md`')); + }); +}); From 3a0c81133bbede93e7430ee45986b0d19b4845c9 Mon Sep 17 00:00:00 2001 From: Colin Date: Tue, 17 Mar 2026 12:04:02 -0400 Subject: [PATCH 09/83] feat(quick): add quick-task branch support --- docs/CONFIGURATION.md | 13 ++++++- docs/USER-GUIDE.md | 30 ++++++++++------ get-shit-done/bin/lib/config.cjs | 3 +- get-shit-done/bin/lib/core.cjs | 2 ++ get-shit-done/bin/lib/init.cjs | 8 +++++ get-shit-done/bin/lib/verify.cjs | 1 + get-shit-done/references/planning-config.md | 4 ++- get-shit-done/workflows/quick.md | 16 ++++++++- get-shit-done/workflows/settings.md | 4 ++- tests/init.test.cjs | 39 +++++++++++++++++++++ tests/quick-branching.test.cjs | 39 +++++++++++++++++++++ 11 files changed, 144 insertions(+), 15 deletions(-) create mode 100644 tests/quick-branching.test.cjs diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 3d1631752..a38e0fa92 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -42,7 +42,8 @@ GSD stores project settings in `.planning/config.json`. Created during `/gsd:new "git": { "branching_strategy": "none", "phase_branch_template": "gsd/phase-{phase}-{slug}", - "milestone_branch_template": "gsd/{milestone}-{slug}" + "milestone_branch_template": "gsd/{milestone}-{slug}", + "quick_branch_template": null }, "gates": { "confirm_project": true, @@ -142,6 +143,7 @@ To keep planning artifacts out of git: | `git.branching_strategy` | enum | `none` | `none`, `phase`, or `milestone` | | `git.phase_branch_template` | string | `gsd/phase-{phase}-{slug}` | Branch name template for phase strategy | | `git.milestone_branch_template` | string | `gsd/{milestone}-{slug}` | Branch name template for milestone strategy | +| `git.quick_branch_template` | string or null | `null` | Optional branch name template for `/gsd:quick` tasks | ### Strategy Comparison @@ -158,6 +160,15 @@ To keep planning artifacts out of git: | `{phase}` | `phase_branch_template` | `03` (zero-padded) | | `{slug}` | Both templates | `user-authentication` (lowercase, hyphenated) | | `{milestone}` | `milestone_branch_template` | `v1.0` | +| `{num}` / `{quick}` | `quick_branch_template` | `260317-abc` (quick task ID) | + +Example quick-task branching: + +```json +"git": { + "quick_branch_template": "gsd/quick-{num}-{slug}" +} +``` ### Merge Options at Milestone Completion diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index 5458cedc0..118ecfbef 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -349,12 +349,13 @@ GSD stores project settings in `.planning/config.json`. Configure during `/gsd:n "ui_phase": true, "ui_safety_gate": true }, - "git": { - "branching_strategy": "none", - "phase_branch_template": "gsd/phase-{phase}-{slug}", - "milestone_branch_template": "gsd/{milestone}-{slug}" - } -} + "git": { + "branching_strategy": "none", + "phase_branch_template": "gsd/phase-{phase}-{slug}", + "milestone_branch_template": "gsd/{milestone}-{slug}", + "quick_branch_template": null + } +} ``` ### Core Settings @@ -391,9 +392,10 @@ Disable these to speed up phases in familiar domains or when conserving tokens. | Setting | Options | Default | What it Controls | |---------|---------|---------|------------------| -| `git.branching_strategy` | `none`, `phase`, `milestone` | `none` | When and how branches are created | -| `git.phase_branch_template` | Template string | `gsd/phase-{phase}-{slug}` | Branch name for phase strategy | -| `git.milestone_branch_template` | Template string | `gsd/{milestone}-{slug}` | Branch name for milestone strategy | +| `git.branching_strategy` | `none`, `phase`, `milestone` | `none` | When and how branches are created | +| `git.phase_branch_template` | Template string | `gsd/phase-{phase}-{slug}` | Branch name for phase strategy | +| `git.milestone_branch_template` | Template string | `gsd/{milestone}-{slug}` | Branch name for milestone strategy | +| `git.quick_branch_template` | Template string or `null` | `null` | Optional branch name for `/gsd:quick` tasks | **Branching strategies explained:** @@ -403,7 +405,15 @@ Disable these to speed up phases in familiar domains or when conserving tokens. | `phase` | At each `execute-phase` | One phase per branch | Code review per phase, granular rollback | | `milestone` | At first `execute-phase` | All phases share one branch | Release branches, PR per version | -**Template variables:** `{phase}` = zero-padded number (e.g., "03"), `{slug}` = lowercase hyphenated name, `{milestone}` = version (e.g., "v1.0"). +**Template variables:** `{phase}` = zero-padded number (e.g., "03"), `{slug}` = lowercase hyphenated name, `{milestone}` = version (e.g., "v1.0"), `{num}` / `{quick}` = quick task ID (e.g., "260317-abc"). + +Example quick-task branching: + +```json +"git": { + "quick_branch_template": "gsd/quick-{num}-{slug}" +} +``` ### Model Profiles (Per-Agent Breakdown) diff --git a/get-shit-done/bin/lib/config.cjs b/get-shit-done/bin/lib/config.cjs index 1e0e65491..e79021c22 100644 --- a/get-shit-done/bin/lib/config.cjs +++ b/get-shit-done/bin/lib/config.cjs @@ -17,7 +17,7 @@ const VALID_CONFIG_KEYS = new Set([ 'workflow.research', 'workflow.plan_check', 'workflow.verifier', 'workflow.nyquist_validation', 'workflow.ui_phase', 'workflow.ui_safety_gate', 'workflow._auto_chain_active', - 'git.branching_strategy', 'git.phase_branch_template', 'git.milestone_branch_template', + 'git.branching_strategy', 'git.phase_branch_template', 'git.milestone_branch_template', 'git.quick_branch_template', 'planning.commit_docs', 'planning.search_gitignored', ]); @@ -91,6 +91,7 @@ function ensureConfigFile(cwd) { branching_strategy: 'none', phase_branch_template: 'gsd/phase-{phase}-{slug}', milestone_branch_template: 'gsd/{milestone}-{slug}', + quick_branch_template: null, workflow: { research: true, plan_check: true, diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index 8c1b427f8..f9b944426 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -58,6 +58,7 @@ function loadConfig(cwd) { branching_strategy: 'none', phase_branch_template: 'gsd/phase-{phase}-{slug}', milestone_branch_template: 'gsd/{milestone}-{slug}', + quick_branch_template: null, research: true, plan_checker: true, verifier: true, @@ -100,6 +101,7 @@ function loadConfig(cwd) { branching_strategy: get('branching_strategy', { section: 'git', field: 'branching_strategy' }) ?? defaults.branching_strategy, phase_branch_template: get('phase_branch_template', { section: 'git', field: 'phase_branch_template' }) ?? defaults.phase_branch_template, milestone_branch_template: get('milestone_branch_template', { section: 'git', field: 'milestone_branch_template' }) ?? defaults.milestone_branch_template, + quick_branch_template: get('quick_branch_template', { section: 'git', field: 'quick_branch_template' }) ?? defaults.quick_branch_template, research: get('research', { section: 'workflow', field: 'research' }) ?? defaults.research, plan_checker: get('plan_checker', { section: 'workflow', field: 'plan_check' }) ?? defaults.plan_checker, verifier: get('verifier', { section: 'workflow', field: 'verifier' }) ?? defaults.verifier, diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index d29e533c8..7cb82d5bb 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -267,6 +267,13 @@ function cmdInitQuick(cwd, description, raw) { const timeBlocks = Math.floor(secondsSinceMidnight / 2); const timeEncoded = timeBlocks.toString(36).padStart(3, '0'); const quickId = dateStr + '-' + timeEncoded; + const branchSlug = slug || 'quick'; + const quickBranchName = config.quick_branch_template + ? config.quick_branch_template + .replace('{num}', quickId) + .replace('{quick}', quickId) + .replace('{slug}', branchSlug) + : null; const result = { // Models @@ -277,6 +284,7 @@ function cmdInitQuick(cwd, description, raw) { // Config commit_docs: config.commit_docs, + branch_name: quickBranchName, // Quick task info quick_id: quickId, diff --git a/get-shit-done/bin/lib/verify.cjs b/get-shit-done/bin/lib/verify.cjs index 9f8a08546..efa6ecc06 100644 --- a/get-shit-done/bin/lib/verify.cjs +++ b/get-shit-done/bin/lib/verify.cjs @@ -746,6 +746,7 @@ function cmdValidateHealth(cwd, options, raw) { branching_strategy: 'none', phase_branch_template: 'gsd/phase-{phase}-{slug}', milestone_branch_template: 'gsd/{milestone}-{slug}', + quick_branch_template: null, workflow: { research: true, plan_check: true, diff --git a/get-shit-done/references/planning-config.md b/get-shit-done/references/planning-config.md index 3bb884377..f8276c761 100644 --- a/get-shit-done/references/planning-config.md +++ b/get-shit-done/references/planning-config.md @@ -11,7 +11,8 @@ Configuration options for `.planning/` directory behavior. "git": { "branching_strategy": "none", "phase_branch_template": "gsd/phase-{phase}-{slug}", - "milestone_branch_template": "gsd/{milestone}-{slug}" + "milestone_branch_template": "gsd/{milestone}-{slug}", + "quick_branch_template": null } ``` @@ -22,6 +23,7 @@ Configuration options for `.planning/` directory behavior. | `git.branching_strategy` | `"none"` | Git branching approach: `"none"`, `"phase"`, or `"milestone"` | | `git.phase_branch_template` | `"gsd/phase-{phase}-{slug}"` | Branch template for phase strategy | | `git.milestone_branch_template` | `"gsd/{milestone}-{slug}"` | Branch template for milestone strategy | +| `git.quick_branch_template` | `null` | Optional branch template for quick-task runs | diff --git a/get-shit-done/workflows/quick.md b/get-shit-done/workflows/quick.md index 4f21ebad0..7138dd031 100644 --- a/get-shit-done/workflows/quick.md +++ b/get-shit-done/workflows/quick.md @@ -111,7 +111,7 @@ INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init quick "$DESCRIP if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi ``` -Parse JSON for: `planner_model`, `executor_model`, `checker_model`, `verifier_model`, `commit_docs`, `quick_id`, `slug`, `date`, `timestamp`, `quick_dir`, `task_dir`, `roadmap_exists`, `planning_exists`. +Parse JSON for: `planner_model`, `executor_model`, `checker_model`, `verifier_model`, `commit_docs`, `branch_name`, `quick_id`, `slug`, `date`, `timestamp`, `quick_dir`, `task_dir`, `roadmap_exists`, `planning_exists`. **If `roadmap_exists` is false:** Error — Quick mode requires an active project with ROADMAP.md. Run `/gsd:new-project` first. @@ -119,6 +119,20 @@ Quick tasks can run mid-phase - validation only checks ROADMAP.md exists, not ph --- +**Step 2.5: Handle quick-task branching** + +**If `branch_name` is empty/null:** Skip and continue on the current branch. + +**If `branch_name` is set:** Check out the quick-task branch before any planning commits: + +```bash +git checkout -b "$branch_name" 2>/dev/null || git checkout "$branch_name" +``` + +All quick-task commits for this run stay on that branch. User handles merge/rebase afterward. + +--- + **Step 3: Create task directory** ```bash diff --git a/get-shit-done/workflows/settings.md b/get-shit-done/workflows/settings.md index 7fc344559..de6c21f93 100644 --- a/get-shit-done/workflows/settings.md +++ b/get-shit-done/workflows/settings.md @@ -157,7 +157,8 @@ Merge new settings into existing config.json: "ui_safety_gate": true/false }, "git": { - "branching_strategy": "none" | "phase" | "milestone" + "branching_strategy": "none" | "phase" | "milestone", + "quick_branch_template": }, "hooks": { "context_warnings": true/false @@ -200,6 +201,7 @@ Write `~/.gsd/defaults.json` with: "commit_docs": , "parallelization": , "branching_strategy": , + "quick_branch_template": , "workflow": { "research": , "plan_check": , diff --git a/tests/init.test.cjs b/tests/init.test.cjs index 66a645d7d..305c9ba74 100644 --- a/tests/init.test.cjs +++ b/tests/init.test.cjs @@ -679,6 +679,7 @@ describe('cmdInitQuick', () => { assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); + assert.strictEqual(output.branch_name, null); assert.strictEqual(output.slug, 'fix-login-bug'); assert.strictEqual(output.description, 'Fix login bug'); @@ -736,6 +737,44 @@ describe('cmdInitQuick', () => { const output = JSON.parse(result.output); assert.ok(output.slug.length <= 40, `Slug should be <= 40 chars, got ${output.slug.length}: "${output.slug}"`); }); + + test('returns quick branch name when quick_branch_template is configured', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'config.json'), + JSON.stringify({ + git: { + quick_branch_template: 'gsd/quick-{num}-{slug}', + }, + }, null, 2) + ); + + const result = runGsdTools('init quick "Fix login bug"', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.ok(output.branch_name, 'branch_name should be set'); + assert.ok(output.branch_name.startsWith('gsd/quick-')); + assert.ok(output.branch_name.endsWith('-fix-login-bug')); + assert.ok(output.branch_name.includes(output.quick_id), 'branch_name should include quick_id'); + }); + + test('uses fallback slug in quick branch name when description is omitted', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'config.json'), + JSON.stringify({ + git: { + quick_branch_template: 'gsd/quick-{quick}-{slug}', + }, + }, null, 2) + ); + + const result = runGsdTools('init quick', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.ok(output.branch_name, 'branch_name should be set'); + assert.ok(output.branch_name.endsWith('-quick'), `Expected fallback slug in branch name, got "${output.branch_name}"`); + }); }); // ───────────────────────────────────────────────────────────────────────────── diff --git a/tests/quick-branching.test.cjs b/tests/quick-branching.test.cjs new file mode 100644 index 000000000..5259ae199 --- /dev/null +++ b/tests/quick-branching.test.cjs @@ -0,0 +1,39 @@ +/** + * Quick task branching tests + * + * Validates that /gsd:quick exposes branch_name from init and that the + * workflow checks out a dedicated quick-task branch when configured. + */ + +const { test, describe } = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const path = require('path'); + +describe('quick workflow: branching support', () => { + const workflowPath = path.join(__dirname, '..', 'get-shit-done', 'workflows', 'quick.md'); + let content; + + test('workflow file exists', () => { + assert.ok(fs.existsSync(workflowPath), 'workflows/quick.md should exist'); + }); + + test('init parse list includes branch_name', () => { + content = fs.readFileSync(workflowPath, 'utf-8'); + assert.ok(content.includes('branch_name'), 'quick workflow should parse branch_name from init JSON'); + }); + + test('workflow includes quick-task branching step', () => { + content = fs.readFileSync(workflowPath, 'utf-8'); + assert.ok(content.includes('Step 2.5: Handle quick-task branching')); + assert.ok(content.includes('git checkout -b "$branch_name" 2>/dev/null || git checkout "$branch_name"')); + }); + + test('branching step runs before task directory creation', () => { + content = fs.readFileSync(workflowPath, 'utf-8'); + const branchingIndex = content.indexOf('Step 2.5: Handle quick-task branching'); + const createDirIndex = content.indexOf('Step 3: Create task directory'); + assert.ok(branchingIndex !== -1 && createDirIndex !== -1, 'workflow should contain both branching and directory steps'); + assert.ok(branchingIndex < createDirIndex, 'branching should happen before quick task directories and commits'); + }); +}); From 92e5c04e00d4a71c6cbe8505a30302fb6b5bec11 Mon Sep 17 00:00:00 2001 From: mitchelladam <17411882+mitchelladam@users.noreply.github.com> Date: Tue, 17 Mar 2026 20:20:19 +0000 Subject: [PATCH 10/83] feat: add Cursor CLI runtime support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Cursor as a fifth supported runtime alongside Claude Code, OpenCode, Gemini, and Codex. Cursor uses skills (~/.cursor/skills/gsd-*/SKILL.md) like Codex, with tool name mappings (Bash→Shell, Edit→StrReplace), subagent type conversion, and full Claude-to-Cursor content adaptation including project conventions (CLAUDE.md→.cursor/rules/, .claude/skills/ →.cursor/skills/) and brand references. Includes install, uninstall, interactive prompt, help text, manifest tracking, and .cjs/.js utility script conversion. Made-with: Cursor --- bin/install.js | 279 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 263 insertions(+), 16 deletions(-) diff --git a/bin/install.js b/bin/install.js index cff9bb5f8..ebbaa4c68 100755 --- a/bin/install.js +++ b/bin/install.js @@ -64,6 +64,7 @@ const hasGemini = args.includes('--gemini'); const hasCodex = args.includes('--codex'); const hasCopilot = args.includes('--copilot'); const hasAntigravity = args.includes('--antigravity'); +const hasCursor = args.includes('--cursor'); const hasBoth = args.includes('--both'); // Legacy flag, keeps working const hasAll = args.includes('--all'); const hasUninstall = args.includes('--uninstall') || args.includes('-u'); @@ -71,7 +72,7 @@ const hasUninstall = args.includes('--uninstall') || args.includes('-u'); // Runtime selection - can be set by flags or interactive prompt let selectedRuntimes = []; if (hasAll) { - selectedRuntimes = ['claude', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity']; + selectedRuntimes = ['claude', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor']; } else if (hasBoth) { selectedRuntimes = ['claude', 'opencode']; } else { @@ -81,6 +82,7 @@ if (hasAll) { if (hasCodex) selectedRuntimes.push('codex'); if (hasCopilot) selectedRuntimes.push('copilot'); if (hasAntigravity) selectedRuntimes.push('antigravity'); + if (hasCursor) selectedRuntimes.push('cursor'); } // WSL + Windows Node.js detection @@ -124,6 +126,7 @@ function getDirName(runtime) { if (runtime === 'gemini') return '.gemini'; if (runtime === 'codex') return '.codex'; if (runtime === 'antigravity') return '.agent'; + if (runtime === 'cursor') return '.cursor'; return '.claude'; } @@ -151,6 +154,7 @@ function getConfigDirFromHome(runtime, isGlobal) { if (!isGlobal) return "'.agent'"; return "'.gemini', 'antigravity'"; } + if (runtime === 'cursor') return "'.cursor'"; return "'.claude'"; } @@ -237,6 +241,18 @@ function getGlobalDir(runtime, explicitDir = null) { return path.join(os.homedir(), '.gemini', 'antigravity'); } + if (runtime === 'cursor') { + // Cursor: --config-dir > CURSOR_CONFIG_DIR > ~/.cursor + if (explicitDir) { + return expandTilde(explicitDir); + } + if (process.env.CURSOR_CONFIG_DIR) { + return expandTilde(process.env.CURSOR_CONFIG_DIR); + } + return path.join(os.homedir(), '.cursor'); + } + + // Claude Code: --config-dir > CLAUDE_CONFIG_DIR > ~/.claude if (explicitDir) { return expandTilde(explicitDir); @@ -257,7 +273,7 @@ const banner = '\n' + '\n' + ' Get Shit Done ' + dim + 'v' + pkg.version + reset + '\n' + ' A meta-prompting, context engineering and spec-driven\n' + - ' development system for Claude Code, OpenCode, Gemini, Codex, Copilot, and Antigravity by TÂCHES.\n'; + ' development system for Claude Code, OpenCode, Gemini, Codex, Copilot, Antigravity, and Cursor by TÂCHES.\n'; // Parse --config-dir argument function parseConfigDirArg() { @@ -295,7 +311,7 @@ if (hasUninstall) { // Show help if requested if (hasHelp) { - console.log(` ${yellow}Usage:${reset} npx get-shit-done-cc [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir ${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx get-shit-done-cc\n\n ${dim}# Install for Claude Code globally${reset}\n npx get-shit-done-cc --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx get-shit-done-cc --gemini --global\n\n ${dim}# Install for Codex globally${reset}\n npx get-shit-done-cc --codex --global\n\n ${dim}# Install for Copilot globally${reset}\n npx get-shit-done-cc --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx get-shit-done-cc --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx get-shit-done-cc --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx get-shit-done-cc --antigravity --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx get-shit-done-cc --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx get-shit-done-cc --codex --global --config-dir ~/.codex-work\n\n ${dim}# Install to current project only${reset}\n npx get-shit-done-cc --claude --local\n\n ${dim}# Uninstall GSD from Codex globally${reset}\n npx get-shit-done-cc --codex --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR environment variables.\n`); + console.log(` ${yellow}Usage:${reset} npx get-shit-done-cc [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir ${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx get-shit-done-cc\n\n ${dim}# Install for Claude Code globally${reset}\n npx get-shit-done-cc --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx get-shit-done-cc --gemini --global\n\n ${dim}# Install for Codex globally${reset}\n npx get-shit-done-cc --codex --global\n\n ${dim}# Install for Copilot globally${reset}\n npx get-shit-done-cc --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx get-shit-done-cc --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx get-shit-done-cc --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx get-shit-done-cc --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx get-shit-done-cc --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx get-shit-done-cc --cursor --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx get-shit-done-cc --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx get-shit-done-cc --codex --global --config-dir ~/.codex-work\n\n ${dim}# Install to current project only${reset}\n npx get-shit-done-cc --claude --local\n\n ${dim}# Uninstall GSD from Cursor globally${reset}\n npx get-shit-done-cc --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR environment variables.\n`); process.exit(0); } @@ -717,6 +733,123 @@ function extractFrontmatterField(frontmatter, fieldName) { return match[1].trim().replace(/^['"]|['"]$/g, ''); } +// Tool name mapping from Claude Code to Cursor CLI +const claudeToCursorTools = { + Bash: 'Shell', + Edit: 'StrReplace', + AskUserQuestion: null, // No direct equivalent — use conversational prompting + SlashCommand: null, // No equivalent — skills are auto-discovered +}; + +/** + * Convert a Claude Code tool name to Cursor CLI format + * @returns {string|null} Cursor tool name, or null if tool should be excluded + */ +function convertCursorToolName(claudeTool) { + if (claudeTool in claudeToCursorTools) { + return claudeToCursorTools[claudeTool]; + } + // MCP tools keep their format (Cursor supports MCP) + if (claudeTool.startsWith('mcp__')) { + return claudeTool; + } + // Most tools share the same name (Read, Write, Glob, Grep, Task, WebSearch, WebFetch, TodoWrite) + return claudeTool; +} + +function convertSlashCommandsToCursorSkillMentions(content) { + let converted = content.replace(/\/gsd:([a-z0-9-]+)/gi, (_, commandName) => { + return `gsd-${String(commandName).toLowerCase()}`; + }); + converted = converted.replace(/\/gsd-help\b/g, 'gsd-help'); + return converted; +} + +function convertClaudeToCursorMarkdown(content) { + let converted = convertSlashCommandsToCursorSkillMentions(content); + // Replace tool name references in body text + converted = converted.replace(/\bBash\(/g, 'Shell('); + converted = converted.replace(/\bEdit\(/g, 'StrReplace('); + converted = converted.replace(/\bAskUserQuestion\b/g, 'conversational prompting'); + // Replace subagent_type from Claude to Cursor format + converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"'); + converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}'); + // Replace project-level Claude conventions with Cursor equivalents + converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.cursor/rules/`'); + converted = converted.replace(/\.\/CLAUDE\.md/g, '.cursor/rules/'); + converted = converted.replace(/`CLAUDE\.md`/g, '`.cursor/rules/`'); + converted = converted.replace(/\bCLAUDE\.md\b/g, '.cursor/rules/'); + converted = converted.replace(/\.claude\/skills\//g, '.cursor/skills/'); + // Remove Claude Code-specific bug workarounds before brand replacement + converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, ''); + converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, ''); + // Replace "Claude Code" brand references with "Cursor" + converted = converted.replace(/\bClaude Code\b/g, 'Cursor'); + return converted; +} + +function getCursorSkillAdapterHeader(skillName) { + return ` +## A. Skill Invocation +- This skill is invoked when the user mentions \`${skillName}\` or describes a task matching this skill. +- Treat all user text after the skill mention as \`{{GSD_ARGS}}\`. +- If no arguments are present, treat \`{{GSD_ARGS}}\` as empty. + +## B. User Prompting +When the workflow needs user input, prompt the user conversationally: +- Present options as a numbered list in your response text +- Ask the user to reply with their choice +- For multi-select, ask for comma-separated numbers + +## C. Tool Usage +Use these Cursor tools when executing GSD workflows: +- \`Shell\` for running commands (terminal operations) +- \`StrReplace\` for editing existing files +- \`Read\`, \`Write\`, \`Glob\`, \`Grep\`, \`Task\`, \`WebSearch\`, \`WebFetch\`, \`TodoWrite\` as needed + +## D. Subagent Spawning +When the workflow needs to spawn a subagent: +- Use \`Task(subagent_type="generalPurpose", ...)\` +- The \`model\` parameter maps to Cursor's model options (e.g., "fast") +`; +} + +function convertClaudeCommandToCursorSkill(content, skillName) { + const converted = convertClaudeToCursorMarkdown(content); + const { frontmatter, body } = extractFrontmatterAndBody(converted); + let description = `Run GSD workflow ${skillName}.`; + if (frontmatter) { + const maybeDescription = extractFrontmatterField(frontmatter, 'description'); + if (maybeDescription) { + description = maybeDescription; + } + } + description = toSingleLine(description); + const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description; + const adapter = getCursorSkillAdapterHeader(skillName); + + return `---\nname: ${yamlQuote(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`; +} + +/** + * Convert Claude Code agent markdown to Cursor agent format. + * Strips frontmatter fields Cursor doesn't support (color, skills), + * converts tool references, and adds a role context header. + */ +function convertClaudeAgentToCursorAgent(content) { + let converted = convertClaudeToCursorMarkdown(content); + + const { frontmatter, body } = extractFrontmatterAndBody(converted); + if (!frontmatter) return converted; + + const name = extractFrontmatterField(frontmatter, 'name') || 'unknown'; + const description = extractFrontmatterField(frontmatter, 'description') || ''; + + const cleanFrontmatter = `---\nname: ${yamlQuote(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`; + + return `${cleanFrontmatter}\n${body}`; +} + function convertSlashCommandsToCodexSkillMentions(content) { let converted = content.replace(/\/gsd:([a-z0-9-]+)/gi, (_, commandName) => { return `$gsd-${String(commandName).toLowerCase()}`; @@ -1445,6 +1578,59 @@ function copyCommandsAsCodexSkills(srcDir, skillsDir, prefix, pathPrefix, runtim recurse(srcDir, prefix); } +function copyCommandsAsCursorSkills(srcDir, skillsDir, prefix, pathPrefix, runtime) { + if (!fs.existsSync(srcDir)) { + return; + } + + fs.mkdirSync(skillsDir, { recursive: true }); + + // Remove previous GSD Cursor skills to avoid stale command skills + const existing = fs.readdirSync(skillsDir, { withFileTypes: true }); + for (const entry of existing) { + if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) { + fs.rmSync(path.join(skillsDir, entry.name), { recursive: true }); + } + } + + function recurse(currentSrcDir, currentPrefix) { + const entries = fs.readdirSync(currentSrcDir, { withFileTypes: true }); + + for (const entry of entries) { + const srcPath = path.join(currentSrcDir, entry.name); + if (entry.isDirectory()) { + recurse(srcPath, `${currentPrefix}-${entry.name}`); + continue; + } + + if (!entry.name.endsWith('.md')) { + continue; + } + + const baseName = entry.name.replace('.md', ''); + const skillName = `${currentPrefix}-${baseName}`; + const skillDir = path.join(skillsDir, skillName); + fs.mkdirSync(skillDir, { recursive: true }); + + let content = fs.readFileSync(srcPath, 'utf8'); + const globalClaudeRegex = /~\/\.claude\//g; + const globalClaudeHomeRegex = /\$HOME\/\.claude\//g; + const localClaudeRegex = /\.\/\.claude\//g; + const cursorDirRegex = /~\/\.cursor\//g; + content = content.replace(globalClaudeRegex, pathPrefix); + content = content.replace(globalClaudeHomeRegex, pathPrefix); + content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`); + content = content.replace(cursorDirRegex, pathPrefix); + content = processAttribution(content, getCommitAttribution(runtime)); + content = convertClaudeCommandToCursorSkill(content, skillName); + + fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content); + } + } + + recurse(srcDir, prefix); +} + /** * Copy Claude commands as Copilot skills — one folder per skill with SKILL.md. * Applies CONV-01 (structure), CONV-02 (allowed-tools), CONV-06 (paths), CONV-07 (command names). @@ -1561,6 +1747,7 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand const isCodex = runtime === 'codex'; const isCopilot = runtime === 'copilot'; const isAntigravity = runtime === 'antigravity'; + const isCursor = runtime === 'cursor'; const dirName = getDirName(runtime); // Clean install: remove existing destination to prevent orphaned files @@ -1617,6 +1804,9 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand content = convertClaudeToAntigravityContent(content, isGlobal); content = processAttribution(content, getCommitAttribution(runtime)); fs.writeFileSync(destPath, content); + } else if (isCursor) { + content = convertClaudeToCursorMarkdown(content); + fs.writeFileSync(destPath, content); } else { fs.writeFileSync(destPath, content); } @@ -1630,6 +1820,14 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand let content = fs.readFileSync(srcPath, 'utf8'); content = convertClaudeToAntigravityContent(content, isGlobal); fs.writeFileSync(destPath, content); + } else if (isCursor && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) { + // For Cursor, also convert Claude references in JS/CJS utility scripts + let jsContent = fs.readFileSync(srcPath, 'utf8'); + jsContent = jsContent.replace(/\/gsd:([a-z0-9-]+)/gi, (_, cmd) => `gsd-${cmd.toLowerCase()}`); + jsContent = jsContent.replace(/\.claude\/skills\//g, '.cursor/skills/'); + jsContent = jsContent.replace(/CLAUDE\.md/g, '.cursor/rules/'); + jsContent = jsContent.replace(/\bClaude Code\b/g, 'Cursor'); + fs.writeFileSync(destPath, jsContent); } else { fs.copyFileSync(srcPath, destPath); } @@ -1722,6 +1920,7 @@ function uninstall(isGlobal, runtime = 'claude') { const isCodex = runtime === 'codex'; const isCopilot = runtime === 'copilot'; const isAntigravity = runtime === 'antigravity'; + const isCursor = runtime === 'cursor'; const dirName = getDirName(runtime); // Get the target directory based on runtime and install type @@ -1739,6 +1938,7 @@ function uninstall(isGlobal, runtime = 'claude') { if (runtime === 'codex') runtimeLabel = 'Codex'; if (runtime === 'copilot') runtimeLabel = 'Copilot'; if (runtime === 'antigravity') runtimeLabel = 'Antigravity'; + if (runtime === 'cursor') runtimeLabel = 'Cursor'; console.log(` Uninstalling GSD from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`); @@ -1765,8 +1965,8 @@ function uninstall(isGlobal, runtime = 'claude') { } console.log(` ${green}✓${reset} Removed GSD commands from command/`); } - } else if (isCodex) { - // Codex: remove skills/gsd-*/SKILL.md skill directories + } else if (isCodex || isCursor) { + // Codex/Cursor: remove skills/gsd-*/SKILL.md skill directories const skillsDir = path.join(targetDir, 'skills'); if (fs.existsSync(skillsDir)) { let skillCount = 0; @@ -1779,11 +1979,12 @@ function uninstall(isGlobal, runtime = 'claude') { } if (skillCount > 0) { removedCount++; - console.log(` ${green}✓${reset} Removed ${skillCount} Codex skills`); + console.log(` ${green}✓${reset} Removed ${skillCount} ${runtimeLabel} skills`); } } - // Codex: remove GSD agent .toml config files + // Codex-only: remove GSD agent .toml config files and config.toml sections + if (isCodex) { const codexAgentsDir = path.join(targetDir, 'agents'); if (fs.existsSync(codexAgentsDir)) { const tomlFiles = fs.readdirSync(codexAgentsDir); @@ -1816,6 +2017,7 @@ function uninstall(isGlobal, runtime = 'claude') { console.log(` ${green}✓${reset} Cleaned GSD sections from config.toml`); } } + } } else if (isCopilot) { // Copilot: remove skills/gsd-*/ directories (same layout as Codex skills) const skillsDir = path.join(targetDir, 'skills'); @@ -1866,6 +2068,23 @@ function uninstall(isGlobal, runtime = 'claude') { console.log(` ${green}✓${reset} Removed ${skillCount} Antigravity skills`); } } + } else if (isCursor) { + // Cursor: remove skills/gsd-*/ directories (same layout as Codex skills) + const skillsDir = path.join(targetDir, 'skills'); + if (fs.existsSync(skillsDir)) { + let skillCount = 0; + const entries = fs.readdirSync(skillsDir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory() && entry.name.startsWith('gsd-')) { + fs.rmSync(path.join(skillsDir, entry.name), { recursive: true }); + skillCount++; + } + } + if (skillCount > 0) { + removedCount++; + console.log(` ${green}✓${reset} Removed ${skillCount} Cursor skills`); + } + } } else { const gsdCommandsDir = path.join(targetDir, 'commands', 'gsd'); if (fs.existsSync(gsdCommandsDir)) { @@ -2274,6 +2493,7 @@ function writeManifest(configDir, runtime = 'claude') { const isCodex = runtime === 'codex'; const isCopilot = runtime === 'copilot'; const isAntigravity = runtime === 'antigravity'; + const isCursor = runtime === 'cursor'; const gsdDir = path.join(configDir, 'get-shit-done'); const commandsDir = path.join(configDir, 'commands', 'gsd'); const opencodeCommandDir = path.join(configDir, 'command'); @@ -2285,7 +2505,7 @@ function writeManifest(configDir, runtime = 'claude') { for (const [rel, hash] of Object.entries(gsdHashes)) { manifest.files['get-shit-done/' + rel] = hash; } - if (!isOpencode && !isCodex && !isCopilot && !isAntigravity && fs.existsSync(commandsDir)) { + if (!isOpencode && !isCodex && !isCopilot && !isAntigravity && !isCursor && fs.existsSync(commandsDir)) { const cmdHashes = generateManifest(commandsDir); for (const [rel, hash] of Object.entries(cmdHashes)) { manifest.files['commands/gsd/' + rel] = hash; @@ -2298,7 +2518,7 @@ function writeManifest(configDir, runtime = 'claude') { } } } - if ((isCodex || isCopilot || isAntigravity) && fs.existsSync(codexSkillsDir)) { + if ((isCodex || isCopilot || isAntigravity || isCursor) && fs.existsSync(codexSkillsDir)) { for (const skillName of listCodexSkillNames(codexSkillsDir)) { const skillRoot = path.join(codexSkillsDir, skillName); const skillHashes = generateManifest(skillRoot); @@ -2376,7 +2596,9 @@ function reportLocalPatches(configDir, runtime = 'claude') { ? '/gsd-reapply-patches' : runtime === 'codex' ? '$gsd-reapply-patches' - : '/gsd:reapply-patches'; + : runtime === 'cursor' + ? 'gsd-reapply-patches (mention the skill name)' + : '/gsd:reapply-patches'; console.log(''); console.log(' ' + yellow + 'Local patches detected' + reset + ' (from v' + meta.from_version + '):'); for (const f of meta.files) { @@ -2397,6 +2619,7 @@ function install(isGlobal, runtime = 'claude') { const isCodex = runtime === 'codex'; const isCopilot = runtime === 'copilot'; const isAntigravity = runtime === 'antigravity'; + const isCursor = runtime === 'cursor'; const dirName = getDirName(runtime); const src = path.join(__dirname, '..'); @@ -2421,6 +2644,7 @@ function install(isGlobal, runtime = 'claude') { if (isCodex) runtimeLabel = 'Codex'; if (isCopilot) runtimeLabel = 'Copilot'; if (isAntigravity) runtimeLabel = 'Antigravity'; + if (isCursor) runtimeLabel = 'Cursor'; console.log(` Installing for ${cyan}${runtimeLabel}${reset} to ${cyan}${locationLabel}${reset}\n`); @@ -2488,6 +2712,16 @@ function install(isGlobal, runtime = 'claude') { } else { failures.push('skills/gsd-*'); } + } else if (isCursor) { + const skillsDir = path.join(targetDir, 'skills'); + const gsdSrc = path.join(src, 'commands', 'gsd'); + copyCommandsAsCursorSkills(gsdSrc, skillsDir, 'gsd', pathPrefix, runtime); + const installedSkillNames = listCodexSkillNames(skillsDir); // reuse — same dir structure + if (installedSkillNames.length > 0) { + console.log(` ${green}✓${reset} Installed ${installedSkillNames.length} skills to skills/`); + } else { + failures.push('skills/gsd-*'); + } } else { // Claude Code & Gemini: nested structure in commands/ directory const commandsDir = path.join(targetDir, 'commands'); @@ -2552,6 +2786,8 @@ function install(isGlobal, runtime = 'claude') { content = convertClaudeAgentToCopilotAgent(content, isGlobal); } else if (isAntigravity) { content = convertClaudeAgentToAntigravityAgent(content, isGlobal); + } else if (isCursor) { + content = convertClaudeAgentToCursorAgent(content); } const destName = isCopilot ? entry.name.replace('.md', '.agent.md') : entry.name; fs.writeFileSync(path.join(agentsDest, destName), content); @@ -2585,7 +2821,7 @@ function install(isGlobal, runtime = 'claude') { failures.push('VERSION'); } - if (!isCodex && !isCopilot) { + if (!isCodex && !isCopilot && !isCursor) { // Write package.json to force CommonJS mode for GSD scripts // Prevents "require is not defined" errors when project has "type": "module" // Node.js walks up looking for package.json - this stops inheritance from project @@ -2705,6 +2941,11 @@ function install(isGlobal, runtime = 'claude') { return { settingsPath: null, settings: null, statuslineCommand: null, runtime }; } + if (isCursor) { + // Cursor uses skills — no config.toml, no settings.json hooks needed + return { settingsPath: null, settings: null, statuslineCommand: null, runtime }; + } + // Configure statusline and hooks in settings.json // Gemini and Antigravity use AfterTool instead of PostToolUse for post-tool hooks const postToolEvent = (runtime === 'gemini' || runtime === 'antigravity') ? 'AfterTool' : 'PostToolUse'; @@ -2788,8 +3029,9 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS const isOpencode = runtime === 'opencode'; const isCodex = runtime === 'codex'; const isCopilot = runtime === 'copilot'; + const isCursor = runtime === 'cursor'; - if (shouldInstallStatusline && !isOpencode && !isCodex && !isCopilot) { + if (shouldInstallStatusline && !isOpencode && !isCodex && !isCopilot && !isCursor) { settings.statusLine = { type: 'command', command: statuslineCommand @@ -2798,7 +3040,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS } // Write settings when runtime supports settings.json - if (!isCodex && !isCopilot) { + if (!isCodex && !isCopilot && !isCursor) { writeSettings(settingsPath, settings); } @@ -2813,12 +3055,14 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS if (runtime === 'codex') program = 'Codex'; if (runtime === 'copilot') program = 'Copilot'; if (runtime === 'antigravity') program = 'Antigravity'; + if (runtime === 'cursor') program = 'Cursor'; let command = '/gsd:new-project'; if (runtime === 'opencode') command = '/gsd-new-project'; if (runtime === 'codex') command = '$gsd-new-project'; if (runtime === 'copilot') command = '/gsd-new-project'; if (runtime === 'antigravity') command = '/gsd-new-project'; + if (runtime === 'cursor') command = 'gsd-new-project (mention the skill name)'; console.log(` ${green}Done!${reset} Open a blank directory in ${program} and run ${cyan}${command}${reset}. @@ -2902,15 +3146,18 @@ function promptRuntime(callback) { ${cyan}4${reset}) Codex ${dim}(~/.codex)${reset} ${cyan}5${reset}) Copilot ${dim}(~/.copilot)${reset} ${cyan}6${reset}) Antigravity ${dim}(~/.gemini/antigravity)${reset} - ${cyan}7${reset}) All + ${cyan}7${reset}) Cursor ${dim}(~/.cursor)${reset} + ${cyan}8${reset}) All `); rl.question(` Choice ${dim}[1]${reset}: `, (answer) => { answered = true; rl.close(); const choice = answer.trim() || '1'; - if (choice === '7') { - callback(['claude', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity']); + if (choice === '8') { + callback(['claude', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor']); + } else if (choice === '7') { + callback(['cursor']); } else if (choice === '6') { callback(['antigravity']); } else if (choice === '5') { From 8c1b224474ebe06ca685e32b788618b4629f1231 Mon Sep 17 00:00:00 2001 From: Matteo Michele Bianchini Date: Wed, 18 Mar 2026 09:57:33 +0000 Subject: [PATCH 11/83] fix: use ~/ instead of resolved absolute paths in global install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows, install.js resolves $HOME to the absolute path (e.g. C:/Users/matte/.claude/) in all workflow .md files. This breaks when ~/.claude is mounted into a Docker container where the path doesn't exist — Node interprets the Windows path as relative to CWD, producing paths like: /workspace/project/C:/Users/matte/.claude/get-shit-done/bin/gsd-tools.cjs For global installs, replace os.homedir() with ~ in pathPrefix so that paths like ~/.claude/get-shit-done/bin/gsd-tools.cjs work correctly across all environments. Local installs keep using resolved absolute paths since they may be outside $HOME. Co-Authored-By: Claude Opus 4.6 --- bin/install.js | 9 ++- tests/path-replacement.test.cjs | 100 ++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 tests/path-replacement.test.cjs diff --git a/bin/install.js b/bin/install.js index cff9bb5f8..c14fed0ec 100755 --- a/bin/install.js +++ b/bin/install.js @@ -2411,9 +2411,12 @@ function install(isGlobal, runtime = 'claude') { // Path prefix for file references in markdown content (e.g. gsd-tools.cjs). // Replaces $HOME/.claude/ or ~/.claude/ so the result is get-shit-done/bin/... - // Always use absolute path so: (1) local installs work when GSD is outside $HOME, - // (2) spawned subagents with empty $HOME still resolve the path (fixes #820). - const pathPrefix = `${path.resolve(targetDir).replace(/\\/g, '/')}/`; + // For global installs: use ~/ so paths work across environments (e.g. Docker + // containers mounting ~/.claude from a Windows host where os.homedir() differs). + // For local installs: use resolved absolute path (may be outside $HOME). + const pathPrefix = isGlobal + ? path.resolve(targetDir).replace(os.homedir(), '~').replace(/\\/g, '/') + '/' + : `${path.resolve(targetDir).replace(/\\/g, '/')}/`; let runtimeLabel = 'Claude Code'; if (isOpencode) runtimeLabel = 'OpenCode'; diff --git a/tests/path-replacement.test.cjs b/tests/path-replacement.test.cjs new file mode 100644 index 000000000..db925cc47 --- /dev/null +++ b/tests/path-replacement.test.cjs @@ -0,0 +1,100 @@ +/** + * GSD Tests - path replacement in install.js + * + * Verifies that global installs produce ~/ paths in .md files, + * never resolved absolute paths containing os.homedir(). + * Reproduces the bug where Windows installs write C:/Users/... + * paths that break in Docker containers. + */ + +const { test, describe } = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const repoRoot = path.join(__dirname, '..'); + +// Simulate the pathPrefix computation from install.js (global install) +function computePathPrefix(homedir, targetDir) { + return path.resolve(targetDir).replace(homedir, '~').replace(/\\/g, '/') + '/'; +} + +describe('pathPrefix computation', () => { + test('default Claude global install uses ~/', () => { + const homedir = os.homedir(); + const targetDir = path.join(homedir, '.claude'); + const prefix = computePathPrefix(homedir, targetDir); + assert.strictEqual(prefix, '~/.claude/'); + }); + + test('default Gemini global install uses ~/', () => { + const homedir = os.homedir(); + const targetDir = path.join(homedir, '.gemini'); + const prefix = computePathPrefix(homedir, targetDir); + assert.strictEqual(prefix, '~/.gemini/'); + }); + + test('custom config dir under home uses ~/', () => { + const homedir = os.homedir(); + const targetDir = path.join(homedir, '.config', 'claude'); + const prefix = computePathPrefix(homedir, targetDir); + assert.ok(prefix.startsWith('~/'), `Expected ~/ prefix, got: ${prefix}`); + assert.ok(!prefix.includes(homedir), `Should not contain homedir: ${homedir}`); + }); + + test('Windows-style paths produce ~/ not C:/', () => { + // On Windows, path.resolve returns the input unchanged when it's already absolute. + // Simulate the string operation directly (can't use path.resolve for Windows paths on Linux). + const winHomedir = 'C:\\Users\\matte'; + const winTargetDir = 'C:\\Users\\matte\\.claude'; + // This is what the fix does: targetDir.replace(homedir, '~').replace(/\\/g, '/') + '/' + const prefix = winTargetDir.replace(winHomedir, '~').replace(/\\/g, '/') + '/'; + assert.strictEqual(prefix, '~/.claude/'); + assert.ok(!prefix.includes('C:'), `Should not contain drive letter, got: ${prefix}`); + }); +}); + +describe('installed .md files contain no resolved absolute paths', () => { + const homedir = os.homedir(); + const targetDir = path.join(homedir, '.claude'); + const pathPrefix = computePathPrefix(homedir, targetDir); + const claudeDirRegex = /~\/\.claude\//g; + const claudeHomeRegex = /\$HOME\/\.claude\//g; + const normalizedHomedir = homedir.replace(/\\/g, '/'); + + // Collect all .md files from source directories + function collectMdFiles(dir) { + const results = []; + if (!fs.existsSync(dir)) return results; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + results.push(...collectMdFiles(fullPath)); + } else if (entry.name.endsWith('.md')) { + results.push(fullPath); + } + } + return results; + } + + const dirsToCheck = ['commands', 'get-shit-done', 'agents'].map(d => path.join(repoRoot, d)); + const mdFiles = dirsToCheck.flatMap(collectMdFiles); + + test('source .md files exist', () => { + assert.ok(mdFiles.length > 0, `Expected .md files, found ${mdFiles.length}`); + }); + + test('after replacement, no .md file contains os.homedir()', () => { + const failures = []; + for (const file of mdFiles) { + let content = fs.readFileSync(file, 'utf8'); + content = content.replace(claudeDirRegex, pathPrefix); + content = content.replace(claudeHomeRegex, pathPrefix); + if (content.includes(normalizedHomedir) && normalizedHomedir !== '~') { + failures.push(path.relative(repoRoot, file)); + } + } + assert.deepStrictEqual(failures, [], `Files with resolved absolute paths: ${failures.join(', ')}`); + }); +}); From 7eed3bc2dfe0df7ec1adf655ffce2fa5eb6163a1 Mon Sep 17 00:00:00 2001 From: Matteo Michele Bianchini Date: Wed, 18 Mar 2026 10:03:40 +0000 Subject: [PATCH 12/83] fix(workflows): add explicit TaskOutput instructions to map-codebase The workflow spawns 4 background agents but didn't tell Claude how to wait for them. Without explicit TaskOutput instructions, the orchestrator displays "Waiting for agents to complete..." indefinitely. Co-Authored-By: Claude Opus 4.6 --- get-shit-done/workflows/map-codebase.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/get-shit-done/workflows/map-codebase.md b/get-shit-done/workflows/map-codebase.md index 901b14511..0cfcd9c27 100644 --- a/get-shit-done/workflows/map-codebase.md +++ b/get-shit-done/workflows/map-codebase.md @@ -172,9 +172,19 @@ Continue to collect_confirmations. -Wait for all 4 agents to complete. +Wait for all 4 agents to complete using TaskOutput tool. -Read each agent's output file to collect confirmations. +**For each agent task_id returned by the Agent tool calls above:** +``` +TaskOutput tool: + task_id: "{task_id from Agent result}" + block: true + timeout: 300000 +``` + +Call TaskOutput for all 4 agents in parallel (single message with 4 TaskOutput calls). + +Once all TaskOutput calls return, read each agent's output file to collect confirmations. **Expected confirmation format from each agent:** ``` From 31a93e2da726964f0b09994b285034baaa58f1aa Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:55:07 -0400 Subject: [PATCH 13/83] docs: document inherit profile for non-Anthropic providers (#1036) (#1185) Users on OpenRouter or local models get unexpected API costs because GSD's default 'balanced' profile spawns specific Anthropic models for subagents. The 'inherit' profile exists but wasn't well-documented for this use case. Changes: - model-profiles.md: add 'Using Non-Anthropic Models' section explaining when and how to use inherit profile - model-profiles.md: update inherit description to mention OpenRouter and local models - settings.md: update Inherit option description to mention OpenRouter and local models (was only mentioning OpenCode) Closes #1036 --- get-shit-done/references/model-profiles.md | 18 ++++++++++++++++++ get-shit-done/workflows/settings.md | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/get-shit-done/references/model-profiles.md b/get-shit-done/references/model-profiles.md index e4d5f0649..97a92b75d 100644 --- a/get-shit-done/references/model-profiles.md +++ b/get-shit-done/references/model-profiles.md @@ -40,8 +40,26 @@ Model profiles control which Claude model each GSD agent uses. This allows balan **inherit** - Follow the current session model - All agents resolve to `inherit` - Best when you switch models interactively (for example OpenCode `/model`) +- **Required when using non-Anthropic providers** (OpenRouter, local models, etc.) — otherwise GSD may call Anthropic models directly, incurring unexpected costs - Use when: you want GSD to follow your currently selected runtime model +## Using Non-Anthropic Models (OpenRouter, Local, etc.) + +If you're using Claude Code with OpenRouter, a local model, or any non-Anthropic provider, set the `inherit` profile to prevent GSD from calling Anthropic models for subagents: + +```bash +# Via settings command +/gsd:settings +# → Select "Inherit" for model profile + +# Or manually in .planning/config.json +{ + "model_profile": "inherit" +} +``` + +Without `inherit`, GSD's default `balanced` profile spawns specific Anthropic models (`opus`, `sonnet`, `haiku`) for each agent type, which can result in additional API costs through your non-Anthropic provider. + ## Resolution Logic Orchestrators resolve model before spawning: diff --git a/get-shit-done/workflows/settings.md b/get-shit-done/workflows/settings.md index 7fc344559..b540cff61 100644 --- a/get-shit-done/workflows/settings.md +++ b/get-shit-done/workflows/settings.md @@ -49,7 +49,7 @@ AskUserQuestion([ { label: "Quality", description: "Opus everywhere except verification (highest cost)" }, { label: "Balanced (Recommended)", description: "Opus for planning, Sonnet for research/execution/verification" }, { label: "Budget", description: "Sonnet for writing, Haiku for research/verification (lowest cost)" }, - { label: "Inherit", description: "Use current session model for all agents (best for OpenCode /model)" } + { label: "Inherit", description: "Use current session model for all agents (best for OpenRouter, local models, or runtime model switching)" } ] }, { From 94b83759afb8b43681a21e1e2979fb465c4c2ade Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:55:21 -0400 Subject: [PATCH 14/83] fix: use arrays for RUNTIME_DIRS to fix zsh word-splitting (#1173) (#1183) The version detection script in update.md used a space-separated string for RUNTIME_DIRS and iterated with `for entry in $RUNTIME_DIRS`. This relies on word-splitting which works in bash but fails in zsh (zsh does not word-split unquoted variables by default), causing the entire string to be treated as one entry and detection to fall through to UNKNOWN. Fix: convert RUNTIME_DIRS and ORDERED_RUNTIME_DIRS from space-separated strings to proper arrays, and iterate with ${array[@]} syntax which works correctly in both bash and zsh. Closes #1173 --- get-shit-done/workflows/update.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/get-shit-done/workflows/update.md b/get-shit-done/workflows/update.md index 7d276eae4..fa910c927 100644 --- a/get-shit-done/workflows/update.md +++ b/get-shit-done/workflows/update.md @@ -20,8 +20,11 @@ First, derive `PREFERRED_RUNTIME` from the invoking prompt's `execution_context` Use `PREFERRED_RUNTIME` as the first runtime checked so `/gsd:update` targets the runtime that invoked it. ```bash -# Runtime candidates: ":" -RUNTIME_DIRS="claude:.claude opencode:.config/opencode opencode:.opencode gemini:.gemini codex:.codex" +# Runtime candidates: ":" stored as an array. +# Using an array instead of a space-separated string ensures correct +# iteration in both bash and zsh (zsh does not word-split unquoted +# variables by default). Fixes #1173. +RUNTIME_DIRS=( "claude:.claude" "opencode:.config/opencode" "opencode:.opencode" "gemini:.gemini" "codex:.codex" ) # PREFERRED_RUNTIME should be set from execution_context before running this block. # If not set, infer from runtime env vars; fallback to claude. @@ -40,23 +43,23 @@ if [ -z "$PREFERRED_RUNTIME" ]; then fi # Reorder entries so preferred runtime is checked first. -ORDERED_RUNTIME_DIRS="" -for entry in $RUNTIME_DIRS; do +ORDERED_RUNTIME_DIRS=() +for entry in "${RUNTIME_DIRS[@]}"; do runtime="${entry%%:*}" if [ "$runtime" = "$PREFERRED_RUNTIME" ]; then - ORDERED_RUNTIME_DIRS="$ORDERED_RUNTIME_DIRS $entry" + ORDERED_RUNTIME_DIRS+=( "$entry" ) fi done -for entry in $RUNTIME_DIRS; do +for entry in "${RUNTIME_DIRS[@]}"; do runtime="${entry%%:*}" if [ "$runtime" != "$PREFERRED_RUNTIME" ]; then - ORDERED_RUNTIME_DIRS="$ORDERED_RUNTIME_DIRS $entry" + ORDERED_RUNTIME_DIRS+=( "$entry" ) fi done # Check local first (takes priority only if valid and distinct from global) LOCAL_VERSION_FILE="" LOCAL_MARKER_FILE="" LOCAL_DIR="" LOCAL_RUNTIME="" -for entry in $ORDERED_RUNTIME_DIRS; do +for entry in "${ORDERED_RUNTIME_DIRS[@]}"; do runtime="${entry%%:*}" dir="${entry#*:}" if [ -f "./$dir/get-shit-done/VERSION" ] || [ -f "./$dir/get-shit-done/workflows/update.md" ]; then @@ -69,7 +72,7 @@ for entry in $ORDERED_RUNTIME_DIRS; do done GLOBAL_VERSION_FILE="" GLOBAL_MARKER_FILE="" GLOBAL_DIR="" GLOBAL_RUNTIME="" -for entry in $ORDERED_RUNTIME_DIRS; do +for entry in "${ORDERED_RUNTIME_DIRS[@]}"; do runtime="${entry%%:*}" dir="${entry#*:}" if [ -f "$HOME/$dir/get-shit-done/VERSION" ] || [ -f "$HOME/$dir/get-shit-done/workflows/update.md" ]; then From 9acfa4bffc67de82d796feeda63fd6bde2352e68 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:55:40 -0400 Subject: [PATCH 15/83] fix: add sequential fallback for map-codebase on runtimes without Task tool (#1174) (#1184) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Runtimes like Antigravity don't have a Task tool for spawning subagents. When the agent encounters Task() calls, it falls back to browser_subagent which is meant for web browsing, not code analysis — causing gsd-map-codebase to fail. This adds: 1. A detect_runtime_capabilities step before spawn_agents 2. An explicit warning to NEVER use browser_subagent for code analysis 3. A sequential_mapping fallback step that performs all 4 mapping passes inline using file system tools when Task is unavailable Closes #1174 --- get-shit-done/workflows/map-codebase.md | 54 ++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/get-shit-done/workflows/map-codebase.md b/get-shit-done/workflows/map-codebase.md index 901b14511..6398e5d8b 100644 --- a/get-shit-done/workflows/map-codebase.md +++ b/get-shit-done/workflows/map-codebase.md @@ -82,12 +82,25 @@ mkdir -p .planning/codebase Continue to spawn_agents. - + +Before spawning agents, detect whether the current runtime supports the `Task` tool for subagent delegation. + +**Runtimes with Task tool:** Claude Code, Cursor (native subagent support) +**Runtimes WITHOUT Task tool:** Antigravity, Gemini CLI, OpenCode, Codex, and others + +**How to detect:** Check if you have access to a `Task` tool. If you do NOT have a `Task` tool (or only have tools like `browser_subagent` which is for web browsing, NOT code analysis): + +→ **Skip `spawn_agents` and `collect_confirmations`** — go directly to `sequential_mapping` instead. + +**CRITICAL:** Never use `browser_subagent` or `Explore` as a substitute for `Task`. The `browser_subagent` tool is exclusively for web page interaction and will fail for codebase analysis. If `Task` is unavailable, perform the mapping sequentially in-context. + + + Spawn 4 parallel gsd-codebase-mapper agents. Use Task tool with `subagent_type="gsd-codebase-mapper"`, `model="{mapper_model}"`, and `run_in_background=true` for parallel execution. -**CRITICAL:** Use the dedicated `gsd-codebase-mapper` agent, NOT `Explore`. The mapper agent writes documents directly. +**CRITICAL:** Use the dedicated `gsd-codebase-mapper` agent, NOT `Explore` or `browser_subagent`. The mapper agent writes documents directly. **Agent 1: Tech Focus** @@ -195,6 +208,37 @@ If any agent failed, note the failure and continue with successful documents. Continue to verify_output. + +When the `Task` tool is unavailable, perform codebase mapping sequentially in the current context. This replaces `spawn_agents` and `collect_confirmations`. + +**IMPORTANT:** Do NOT use `browser_subagent`, `Explore`, or any browser-based tool. Use only file system tools (Read, Bash, Write, Grep, Glob, list_dir, view_file, grep_search, or equivalent tools available in your runtime). + +Perform all 4 mapping passes sequentially: + +**Pass 1: Tech Focus** +- Explore package.json/Cargo.toml/go.mod/requirements.txt, config files, dependency trees +- Write `.planning/codebase/STACK.md` — Languages, runtime, frameworks, dependencies, configuration +- Write `.planning/codebase/INTEGRATIONS.md` — External APIs, databases, auth providers, webhooks + +**Pass 2: Architecture Focus** +- Explore directory structure, entry points, module boundaries, data flow +- Write `.planning/codebase/ARCHITECTURE.md` — Pattern, layers, data flow, abstractions, entry points +- Write `.planning/codebase/STRUCTURE.md` — Directory layout, key locations, naming conventions + +**Pass 3: Quality Focus** +- Explore code style, error handling patterns, test files, CI config +- Write `.planning/codebase/CONVENTIONS.md` — Code style, naming, patterns, error handling +- Write `.planning/codebase/TESTING.md` — Framework, structure, mocking, coverage + +**Pass 4: Concerns Focus** +- Explore TODOs, known issues, fragile areas, security patterns +- Write `.planning/codebase/CONCERNS.md` — Tech debt, bugs, security, performance, fragile areas + +Use the same document templates as the `gsd-codebase-mapper` agent. Include actual file paths formatted with backticks. + +Continue to verify_output. + + Verify all documents created successfully: @@ -307,10 +351,10 @@ End workflow. - .planning/codebase/ directory created -- 4 parallel gsd-codebase-mapper agents spawned with run_in_background=true -- Agents write documents directly (orchestrator doesn't receive document contents) -- Read agent output files to collect confirmations +- If Task tool available: 4 parallel gsd-codebase-mapper agents spawned with run_in_background=true +- If Task tool NOT available: 4 sequential mapping passes performed inline (never using browser_subagent) - All 7 codebase documents exist +- No empty documents (each should have >20 lines) - Clear completion summary with line counts - User offered clear next steps in GSD style From 93dc3d134f0bd6a39f20916d873f06ec6075acd6 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:55:55 -0400 Subject: [PATCH 16/83] test: add coverage for model-profiles, template, profile-pipeline, profile-output (#1170) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New test files for 4 previously untested modules: model-profiles.test.cjs (15 tests): - MODEL_PROFILES data integrity (all agents, all profiles, valid aliases) - VALID_PROFILES list validation - getAgentToModelMapForProfile (balanced, budget, quality, agent count) - formatAgentToModelMapAsTable (header, separator, column alignment) template.test.cjs (11 tests): - template select: minimal/standard/complex heuristics, fallback - template fill: summary/plan/verification generation, --plan option - Error paths: existing file, unknown type, missing phase profile-pipeline.test.cjs (7 tests): - scan-sessions: empty dir, synthetic project, multi-session - extract-messages: user message extraction, meta/internal filtering - profile-questionnaire: structure validation profile-output.test.cjs (13 tests): - PROFILING_QUESTIONS data (fields, options, uniqueness) - CLAUDE_INSTRUCTIONS coverage (dimensions, instruction mapping) - write-profile: analysis JSON → USER-PROFILE.md - generate-claude-md: --auto generation, --force protection - generate-dev-preferences: analysis → preferences command Test count: 744 → 798 (+54 new tests, 0 failures) --- tests/model-profiles.test.cjs | 134 ++++++++++++++++++++++ tests/profile-output.test.cjs | 197 ++++++++++++++++++++++++++++++++ tests/profile-pipeline.test.cjs | 160 ++++++++++++++++++++++++++ tests/template.test.cjs | 186 ++++++++++++++++++++++++++++++ 4 files changed, 677 insertions(+) create mode 100644 tests/model-profiles.test.cjs create mode 100644 tests/profile-output.test.cjs create mode 100644 tests/profile-pipeline.test.cjs create mode 100644 tests/template.test.cjs diff --git a/tests/model-profiles.test.cjs b/tests/model-profiles.test.cjs new file mode 100644 index 000000000..55fd1cf00 --- /dev/null +++ b/tests/model-profiles.test.cjs @@ -0,0 +1,134 @@ +/** + * Model Profiles Tests + * + * Tests for MODEL_PROFILES data structure, VALID_PROFILES list, + * formatAgentToModelMapAsTable, and getAgentToModelMapForProfile. + */ + +const { test, describe } = require('node:test'); +const assert = require('node:assert'); + +const { + MODEL_PROFILES, + VALID_PROFILES, + formatAgentToModelMapAsTable, + getAgentToModelMapForProfile, +} = require('../get-shit-done/bin/lib/model-profiles.cjs'); + +// ─── MODEL_PROFILES data integrity ──────────────────────────────────────────── + +describe('MODEL_PROFILES', () => { + test('contains all expected GSD agents', () => { + const expectedAgents = [ + 'gsd-planner', 'gsd-roadmapper', 'gsd-executor', + 'gsd-phase-researcher', 'gsd-project-researcher', 'gsd-research-synthesizer', + 'gsd-debugger', 'gsd-codebase-mapper', 'gsd-verifier', + 'gsd-plan-checker', 'gsd-integration-checker', 'gsd-nyquist-auditor', + 'gsd-ui-researcher', 'gsd-ui-checker', 'gsd-ui-auditor', + ]; + for (const agent of expectedAgents) { + assert.ok(MODEL_PROFILES[agent], `Missing agent: ${agent}`); + } + }); + + test('every agent has quality, balanced, and budget profiles', () => { + for (const [agent, profiles] of Object.entries(MODEL_PROFILES)) { + assert.ok(profiles.quality, `${agent} missing quality profile`); + assert.ok(profiles.balanced, `${agent} missing balanced profile`); + assert.ok(profiles.budget, `${agent} missing budget profile`); + } + }); + + test('all profile values are valid model aliases', () => { + const validModels = ['opus', 'sonnet', 'haiku']; + for (const [agent, profiles] of Object.entries(MODEL_PROFILES)) { + for (const [profile, model] of Object.entries(profiles)) { + assert.ok( + validModels.includes(model), + `${agent}.${profile} has invalid model "${model}" — expected one of ${validModels.join(', ')}` + ); + } + } + }); + + test('quality profile never uses haiku', () => { + for (const [agent, profiles] of Object.entries(MODEL_PROFILES)) { + assert.notStrictEqual( + profiles.quality, 'haiku', + `${agent} quality profile should not use haiku` + ); + } + }); +}); + +// ─── VALID_PROFILES ─────────────────────────────────────────────────────────── + +describe('VALID_PROFILES', () => { + test('contains quality, balanced, and budget', () => { + assert.deepStrictEqual(VALID_PROFILES.sort(), ['balanced', 'budget', 'quality']); + }); + + test('is derived from MODEL_PROFILES keys', () => { + const fromData = Object.keys(MODEL_PROFILES['gsd-planner']); + assert.deepStrictEqual(VALID_PROFILES.sort(), fromData.sort()); + }); +}); + +// ─── getAgentToModelMapForProfile ───────────────────────────────────────────── + +describe('getAgentToModelMapForProfile', () => { + test('returns correct models for balanced profile', () => { + const map = getAgentToModelMapForProfile('balanced'); + assert.strictEqual(map['gsd-planner'], 'opus'); + assert.strictEqual(map['gsd-codebase-mapper'], 'haiku'); + assert.strictEqual(map['gsd-verifier'], 'sonnet'); + }); + + test('returns correct models for budget profile', () => { + const map = getAgentToModelMapForProfile('budget'); + assert.strictEqual(map['gsd-planner'], 'sonnet'); + assert.strictEqual(map['gsd-phase-researcher'], 'haiku'); + }); + + test('returns correct models for quality profile', () => { + const map = getAgentToModelMapForProfile('quality'); + assert.strictEqual(map['gsd-planner'], 'opus'); + assert.strictEqual(map['gsd-executor'], 'opus'); + }); + + test('returns all agents in the map', () => { + const map = getAgentToModelMapForProfile('balanced'); + const agentCount = Object.keys(MODEL_PROFILES).length; + assert.strictEqual(Object.keys(map).length, agentCount); + }); +}); + +// ─── formatAgentToModelMapAsTable ───────────────────────────────────────────── + +describe('formatAgentToModelMapAsTable', () => { + test('produces a table with header and separator', () => { + const map = { 'gsd-planner': 'opus', 'gsd-executor': 'sonnet' }; + const table = formatAgentToModelMapAsTable(map); + assert.ok(table.includes('Agent'), 'should have Agent header'); + assert.ok(table.includes('Model'), 'should have Model header'); + assert.ok(table.includes('─'), 'should have separator line'); + assert.ok(table.includes('gsd-planner'), 'should list agent'); + assert.ok(table.includes('opus'), 'should list model'); + }); + + test('pads columns correctly', () => { + const map = { 'a': 'opus', 'very-long-agent-name': 'haiku' }; + const table = formatAgentToModelMapAsTable(map); + const lines = table.split('\n').filter(l => l.trim()); + // Separator line uses ┼, data/header lines use │ + const dataLines = lines.filter(l => l.includes('│')); + const pipePositions = dataLines.map(l => l.indexOf('│')); + const unique = [...new Set(pipePositions)]; + assert.strictEqual(unique.length, 1, 'all data lines should align on │'); + }); + + test('handles empty map', () => { + const table = formatAgentToModelMapAsTable({}); + assert.ok(table.includes('Agent'), 'should still have header'); + }); +}); diff --git a/tests/profile-output.test.cjs b/tests/profile-output.test.cjs new file mode 100644 index 000000000..3001195f5 --- /dev/null +++ b/tests/profile-output.test.cjs @@ -0,0 +1,197 @@ +/** + * Profile Output Tests + * + * Tests for profile rendering commands and PROFILING_QUESTIONS data. + */ + +const { test, describe, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const path = require('path'); +const { runGsdTools, createTempProject, createTempGitProject, cleanup } = require('./helpers.cjs'); + +const { + PROFILING_QUESTIONS, + CLAUDE_INSTRUCTIONS, +} = require('../get-shit-done/bin/lib/profile-output.cjs'); + +// ─── PROFILING_QUESTIONS data ───────────────────────────────────────────────── + +describe('PROFILING_QUESTIONS', () => { + test('is a non-empty array', () => { + assert.ok(Array.isArray(PROFILING_QUESTIONS)); + assert.ok(PROFILING_QUESTIONS.length > 0); + }); + + test('each question has required fields', () => { + for (const q of PROFILING_QUESTIONS) { + assert.ok(q.dimension, `question missing dimension`); + assert.ok(q.header, `${q.dimension} missing header`); + assert.ok(q.question, `${q.dimension} missing question`); + assert.ok(Array.isArray(q.options), `${q.dimension} options should be array`); + assert.ok(q.options.length >= 2, `${q.dimension} should have at least 2 options`); + } + }); + + test('each option has label, value, and rating', () => { + for (const q of PROFILING_QUESTIONS) { + for (const opt of q.options) { + assert.ok(opt.label, `${q.dimension} option missing label`); + assert.ok(opt.value, `${q.dimension} option missing value`); + assert.ok(opt.rating, `${q.dimension} option missing rating`); + } + } + }); + + test('all dimension keys are unique', () => { + const dims = PROFILING_QUESTIONS.map(q => q.dimension); + const unique = [...new Set(dims)]; + assert.strictEqual(dims.length, unique.length); + }); +}); + +// ─── CLAUDE_INSTRUCTIONS ────────────────────────────────────────────────────── + +describe('CLAUDE_INSTRUCTIONS', () => { + test('is a non-empty object', () => { + assert.ok(typeof CLAUDE_INSTRUCTIONS === 'object'); + assert.ok(Object.keys(CLAUDE_INSTRUCTIONS).length > 0); + }); + + test('each dimension has at least one instruction', () => { + for (const [dim, instructions] of Object.entries(CLAUDE_INSTRUCTIONS)) { + assert.ok(typeof instructions === 'object', `${dim} should be an object`); + assert.ok(Object.keys(instructions).length > 0, `${dim} should have instructions`); + } + }); + + test('every PROFILING_QUESTIONS dimension has CLAUDE_INSTRUCTIONS', () => { + for (const q of PROFILING_QUESTIONS) { + assert.ok( + CLAUDE_INSTRUCTIONS[q.dimension], + `${q.dimension} has questions but no CLAUDE_INSTRUCTIONS` + ); + } + }); +}); + +// ─── write-profile command ──────────────────────────────────────────────────── + +describe('write-profile command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('writes USER-PROFILE.md from analysis JSON', () => { + const analysis = { + profile_version: '1.0', + dimensions: { + communication_style: { rating: 'terse-direct', confidence: 'HIGH' }, + decision_speed: { rating: 'fast-intuitive', confidence: 'MEDIUM' }, + explanation_depth: { rating: 'concise', confidence: 'HIGH' }, + debugging_approach: { rating: 'fix-first', confidence: 'LOW' }, + ux_philosophy: { rating: 'function-first', confidence: 'MEDIUM' }, + vendor_philosophy: { rating: 'pragmatic', confidence: 'HIGH' }, + frustration_triggers: { rating: 'over-explanation', confidence: 'LOW' }, + learning_style: { rating: 'hands-on', confidence: 'MEDIUM' }, + }, + }; + + const analysisPath = path.join(tmpDir, 'analysis.json'); + fs.writeFileSync(analysisPath, JSON.stringify(analysis)); + + const result = runGsdTools(['write-profile', '--input', analysisPath, '--raw'], tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.ok(out.profile_path, 'should return profile_path'); + assert.ok(out.dimensions_scored > 0, 'should have scored dimensions'); + }); + + test('errors when --input is missing', () => { + const result = runGsdTools('write-profile --raw', tmpDir); + assert.ok(!result.success, 'should fail without --input'); + assert.ok(result.error.includes('--input'), 'should mention --input'); + }); +}); + +// ─── generate-claude-md command ─────────────────────────────────────────────── + +describe('generate-claude-md command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempGitProject(); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'PROJECT.md'), + '# My Project\n\nA test project.\n\n## Tech Stack\n\n- Node.js\n- TypeScript\n' + ); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('generates CLAUDE.md with --auto flag', () => { + const outputPath = path.join(tmpDir, 'CLAUDE.md'); + const result = runGsdTools(['generate-claude-md', '--output', outputPath, '--auto', '--raw'], tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + + if (fs.existsSync(outputPath)) { + const content = fs.readFileSync(outputPath, 'utf-8'); + assert.ok(content.length > 0, 'should have content'); + } + }); + + test('does not overwrite existing CLAUDE.md without --force', () => { + const outputPath = path.join(tmpDir, 'CLAUDE.md'); + fs.writeFileSync(outputPath, '# Custom CLAUDE.md\n\nUser content.\n'); + + const result = runGsdTools(['generate-claude-md', '--output', outputPath, '--auto', '--raw'], tmpDir); + // Should merge, not overwrite + const content = fs.readFileSync(outputPath, 'utf-8'); + assert.ok(content.length > 0, 'should still have content'); + }); +}); + +// ─── generate-dev-preferences ───────────────────────────────────────────────── + +describe('generate-dev-preferences command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('errors when --analysis is missing', () => { + const result = runGsdTools('generate-dev-preferences --raw', tmpDir); + assert.ok(!result.success, 'should fail without --analysis'); + assert.ok(result.error.includes('--analysis'), 'should mention --analysis'); + }); + + test('generates preferences from analysis file', () => { + const analysis = { + profile_version: '1.0', + dimensions: { + communication_style: { rating: 'terse-direct', confidence: 'HIGH' }, + decision_speed: { rating: 'fast-intuitive', confidence: 'MEDIUM' }, + }, + }; + const analysisPath = path.join(tmpDir, 'analysis.json'); + fs.writeFileSync(analysisPath, JSON.stringify(analysis)); + + const result = runGsdTools(['generate-dev-preferences', '--analysis', analysisPath, '--raw'], tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.ok(out.command_path || out.command_name, 'should return command output'); + }); +}); diff --git a/tests/profile-pipeline.test.cjs b/tests/profile-pipeline.test.cjs new file mode 100644 index 000000000..e6e783412 --- /dev/null +++ b/tests/profile-pipeline.test.cjs @@ -0,0 +1,160 @@ +/** + * Profile Pipeline Tests + * + * Tests for session scanning, message extraction, and profile sampling. + * Uses synthetic session data in temp directories via --path override. + */ + +const { test, describe, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs'); + +// ─── scan-sessions ──────────────────────────────────────────────────────────── + +describe('scan-sessions command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-profile-test-')); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('returns empty array for empty sessions directory', () => { + const sessionsDir = path.join(tmpDir, 'projects'); + fs.mkdirSync(sessionsDir, { recursive: true }); + const result = runGsdTools(`scan-sessions --path ${sessionsDir} --raw`, tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.ok(Array.isArray(out), 'should return an array'); + assert.strictEqual(out.length, 0, 'should be empty'); + }); + + test('scans synthetic project directory', () => { + const sessionsDir = path.join(tmpDir, 'projects'); + const projectDir = path.join(sessionsDir, 'test-project-abc123'); + fs.mkdirSync(projectDir, { recursive: true }); + + // Create a synthetic session file + const sessionData = [ + JSON.stringify({ type: 'user', userType: 'external', message: { content: 'hello' }, timestamp: Date.now() }), + JSON.stringify({ type: 'assistant', message: { content: 'hi' }, timestamp: Date.now() }), + ].join('\n'); + fs.writeFileSync(path.join(projectDir, 'session-001.jsonl'), sessionData); + + const result = runGsdTools(`scan-sessions --path ${sessionsDir} --raw`, tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.ok(Array.isArray(out), 'should return array'); + assert.strictEqual(out.length, 1, 'should find 1 project'); + assert.strictEqual(out[0].sessionCount, 1, 'should have 1 session'); + }); + + test('reports multiple sessions and sizes', () => { + const sessionsDir = path.join(tmpDir, 'projects'); + const projectDir = path.join(sessionsDir, 'multi-session-project'); + fs.mkdirSync(projectDir, { recursive: true }); + + for (let i = 1; i <= 3; i++) { + const data = JSON.stringify({ type: 'user', userType: 'external', message: { content: `msg ${i}` }, timestamp: Date.now() }); + fs.writeFileSync(path.join(projectDir, `session-${i}.jsonl`), data + '\n'); + } + + const result = runGsdTools(`scan-sessions --path ${sessionsDir} --raw`, tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out[0].sessionCount, 3); + assert.ok(out[0].totalSize > 0, 'should have non-zero size'); + }); +}); + +// ─── extract-messages ───────────────────────────────────────────────────────── + +describe('extract-messages command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-profile-test-')); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('extracts user messages from synthetic session', () => { + const sessionsDir = path.join(tmpDir, 'projects'); + const projectDir = path.join(sessionsDir, 'my-project'); + fs.mkdirSync(projectDir, { recursive: true }); + + const messages = [ + { type: 'user', userType: 'external', message: { content: 'fix the login bug' }, timestamp: Date.now() }, + { type: 'assistant', message: { content: 'I will fix it.' }, timestamp: Date.now() }, + { type: 'user', userType: 'external', message: { content: 'add dark mode' }, timestamp: Date.now() }, + { type: 'user', userType: 'internal', isMeta: true, message: { content: ' JSON.stringify(m)).join('\n') + ); + + const result = runGsdTools(`extract-messages my-project --path ${sessionsDir} --raw`, tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.messages_extracted, 2, 'should extract 2 genuine user messages'); + assert.strictEqual(out.project, 'my-project'); + assert.ok(out.output_file, 'should have output file path'); + }); + + test('filters out meta and internal messages', () => { + const sessionsDir = path.join(tmpDir, 'projects'); + const projectDir = path.join(sessionsDir, 'filter-test'); + fs.mkdirSync(projectDir, { recursive: true }); + + const messages = [ + { type: 'user', userType: 'external', message: { content: 'real message' }, timestamp: Date.now() }, + { type: 'user', userType: 'internal', message: { content: 'internal msg' }, timestamp: Date.now() }, + { type: 'user', userType: 'external', isMeta: true, message: { content: 'meta msg' }, timestamp: Date.now() }, + { type: 'user', userType: 'external', message: { content: ' JSON.stringify(m)).join('\n') + ); + + const result = runGsdTools(`extract-messages filter-test --path ${sessionsDir} --raw`, tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.messages_extracted, 2, 'should only extract 2 genuine external messages'); + }); +}); + +// ─── profile-questionnaire ──────────────────────────────────────────────────── + +describe('profile-questionnaire command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('returns questionnaire structure', () => { + const result = runGsdTools('profile-questionnaire --raw', tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.ok(out.questions, 'should have questions array'); + assert.ok(out.questions.length > 0, 'should have at least one question'); + assert.ok(out.questions[0].dimension, 'each question should have a dimension'); + assert.ok(out.questions[0].options, 'each question should have options'); + }); +}); diff --git a/tests/template.test.cjs b/tests/template.test.cjs new file mode 100644 index 000000000..8d2ae3d51 --- /dev/null +++ b/tests/template.test.cjs @@ -0,0 +1,186 @@ +/** + * Template Tests + * + * Tests for cmdTemplateSelect (heuristic template selection) and + * cmdTemplateFill (summary, plan, verification template generation). + */ + +const { test, describe, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const path = require('path'); +const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs'); + +// ─── template select ────────────────────────────────────────────────────────── + +describe('template select command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + // Create a phase directory with a plan + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-setup'); + fs.mkdirSync(phaseDir, { recursive: true }); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('selects minimal template for simple plan', () => { + const planPath = path.join(tmpDir, '.planning', 'phases', '01-setup', '01-01-PLAN.md'); + fs.writeFileSync(planPath, [ + '# Plan', + '', + '### Task 1', + 'Do the thing.', + '', + 'File: `src/index.ts`', + ].join('\n')); + + const result = runGsdTools(`template select .planning/phases/01-setup/01-01-PLAN.md`, tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.type, 'minimal'); + assert.ok(out.template.includes('summary-minimal')); + }); + + test('selects standard template for moderate plan', () => { + const planPath = path.join(tmpDir, '.planning', 'phases', '01-setup', '01-01-PLAN.md'); + fs.writeFileSync(planPath, [ + '# Plan', + '', + '### Task 1', + 'Create `src/auth/login.ts`', + '', + '### Task 2', + 'Create `src/auth/register.ts`', + '', + '### Task 3', + 'Update `src/routes/index.ts`', + '', + 'Files: `src/auth/login.ts`, `src/auth/register.ts`, `src/routes/index.ts`, `src/middleware/auth.ts`', + ].join('\n')); + + const result = runGsdTools(`template select .planning/phases/01-setup/01-01-PLAN.md`, tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.type, 'standard'); + }); + + test('selects complex template for plan with decisions and many files', () => { + const planPath = path.join(tmpDir, '.planning', 'phases', '01-setup', '01-01-PLAN.md'); + const lines = ['# Plan', '']; + for (let i = 1; i <= 6; i++) { + lines.push(`### Task ${i}`, `Do task ${i}.`, ''); + } + lines.push('Made a decision about architecture.', 'Another decision here.'); + for (let i = 1; i <= 8; i++) { + lines.push(`File: \`src/module${i}/index.ts\``); + } + fs.writeFileSync(planPath, lines.join('\n')); + + const result = runGsdTools(`template select .planning/phases/01-setup/01-01-PLAN.md`, tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.type, 'complex'); + }); + + test('returns standard as fallback for nonexistent file', () => { + const result = runGsdTools(`template select .planning/phases/01-setup/nonexistent.md`, tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.type, 'standard'); + assert.ok(out.error, 'should include error message'); + }); +}); + +// ─── template fill ──────────────────────────────────────────────────────────── + +describe('template fill command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-setup'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + '## Roadmap\n\n### Phase 1: Setup\n**Goal:** Initial setup\n' + ); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('fills summary template', () => { + const result = runGsdTools('template fill summary --phase 1', tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.created, true); + assert.ok(out.path.includes('01-01-SUMMARY.md')); + + const content = fs.readFileSync(path.join(tmpDir, out.path), 'utf-8'); + assert.ok(content.includes('---'), 'should have frontmatter'); + assert.ok(content.includes('Phase 1'), 'should reference phase'); + assert.ok(content.includes('Accomplishments'), 'should have accomplishments section'); + }); + + test('fills plan template', () => { + const result = runGsdTools('template fill plan --phase 1', tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.created, true); + assert.ok(out.path.includes('01-01-PLAN.md')); + + const content = fs.readFileSync(path.join(tmpDir, out.path), 'utf-8'); + assert.ok(content.includes('---'), 'should have frontmatter'); + assert.ok(content.includes('Objective'), 'should have objective section'); + assert.ok(content.includes(''), 'should have task XML'); + }); + + test('fills verification template', () => { + const result = runGsdTools('template fill verification --phase 1', tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.created, true); + assert.ok(out.path.includes('01-VERIFICATION.md')); + + const content = fs.readFileSync(path.join(tmpDir, out.path), 'utf-8'); + assert.ok(content.includes('Observable Truths'), 'should have truths section'); + assert.ok(content.includes('Required Artifacts'), 'should have artifacts section'); + }); + + test('rejects existing file', () => { + // Create the file first + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-setup'); + fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), '# Existing'); + + const result = runGsdTools('template fill summary --phase 1', tmpDir); + assert.ok(result.success); // outputs JSON, doesn't crash + const out = JSON.parse(result.output); + assert.ok(out.error, 'should report error for existing file'); + assert.ok(out.error.includes('already exists')); + }); + + test('errors on unknown template type', () => { + const result = runGsdTools('template fill bogus --phase 1', tmpDir); + assert.ok(!result.success, 'should fail for unknown type'); + assert.ok(result.error.includes('Unknown template type')); + }); + + test('errors when phase not found', () => { + const result = runGsdTools('template fill summary --phase 99', tmpDir); + assert.ok(result.success); + const out = JSON.parse(result.output); + assert.ok(out.error, 'should report phase not found'); + }); + + test('respects --plan option for plan number', () => { + const result = runGsdTools('template fill plan --phase 1 --plan 03', tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.ok(out.path.includes('01-03-PLAN.md'), `Expected plan 03 in path, got ${out.path}`); + }); +}); From 1efc74af51df32cd3dcd1c15de6b9569562a1987 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:56:25 -0400 Subject: [PATCH 17/83] refactor(tests): consolidate runtime converters, remove duplicate tests, standardize helpers (#1169) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test consolidation: - Merged gemini-config.test.cjs + opencode-agent-conversion.test.cjs into runtime-converters.test.cjs (single file for small runtime converters) - Removed 11 duplicate tests from core.test.cjs: - 5 comparePhaseNum tests (authoritative copies in phase.test.cjs) - 6 normalizePhaseName tests (authoritative copies in phase.test.cjs) - Added note in core.test.cjs pointing to phase.test.cjs as canonical location Standardization: - core.test.cjs now uses createTempProject()/cleanup() from helpers.cjs instead of inline fs.mkdtempSync/fs.rmSync patterns File count: 21 → 19 test files Test count: 755 → 744 (11 duplicates removed, 0 coverage lost) --- tests/core.test.cjs | 101 ++++-------------- tests/gemini-config.test.cjs | 47 -------- ...n.test.cjs => runtime-converters.test.cjs} | 54 ++++++++-- 3 files changed, 69 insertions(+), 133 deletions(-) delete mode 100644 tests/gemini-config.test.cjs rename tests/{opencode-agent-conversion.test.cjs => runtime-converters.test.cjs} (66%) diff --git a/tests/core.test.cjs b/tests/core.test.cjs index b20328f32..2f9d796f0 100644 --- a/tests/core.test.cjs +++ b/tests/core.test.cjs @@ -10,6 +10,7 @@ const assert = require('node:assert'); const fs = require('fs'); const path = require('path'); const os = require('os'); +const { createTempProject, cleanup } = require('./helpers.cjs'); const { loadConfig, @@ -35,14 +36,13 @@ describe('loadConfig', () => { let originalCwd; beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); - fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); + tmpDir = createTempProject(); originalCwd = process.cwd(); }); afterEach(() => { process.chdir(originalCwd); - fs.rmSync(tmpDir, { recursive: true, force: true }); + cleanup(tmpDir); }); function writeConfig(obj) { @@ -129,12 +129,11 @@ describe('resolveModelInternal', () => { let tmpDir; beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); - fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); + tmpDir = createTempProject(); }); afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + cleanup(tmpDir); }); function writeConfig(obj) { @@ -276,62 +275,11 @@ describe('generateSlugInternal', () => { }); }); -// ─── normalizePhaseName ──────────────────────────────────────────────────────── - -describe('normalizePhaseName', () => { - test('pads single digit', () => { - assert.strictEqual(normalizePhaseName('1'), '01'); - }); - - test('preserves double digit', () => { - assert.strictEqual(normalizePhaseName('12'), '12'); - }); - - test('handles letter suffix', () => { - assert.strictEqual(normalizePhaseName('1A'), '01A'); - }); - - test('handles decimal phases', () => { - assert.strictEqual(normalizePhaseName('2.1'), '02.1'); - }); - - test('handles multi-level decimals', () => { - assert.strictEqual(normalizePhaseName('1.2.3'), '01.2.3'); - }); - - test('returns non-matching input unchanged', () => { - assert.strictEqual(normalizePhaseName('abc'), 'abc'); - }); -}); - -// ─── comparePhaseNum ─────────────────────────────────────────────────────────── - -describe('comparePhaseNum', () => { - test('sorts integer phases numerically', () => { - assert.ok(comparePhaseNum('1', '2') < 0); - assert.ok(comparePhaseNum('10', '2') > 0); - }); - - test('sorts letter suffixes', () => { - assert.ok(comparePhaseNum('12', '12A') < 0); - assert.ok(comparePhaseNum('12A', '12B') < 0); - }); - - test('sorts decimal phases', () => { - assert.ok(comparePhaseNum('2', '2.1') < 0); - assert.ok(comparePhaseNum('2.1', '2.2') < 0); - }); - - test('handles multi-level decimals', () => { - assert.ok(comparePhaseNum('1.1', '1.1.2') < 0); - assert.ok(comparePhaseNum('1.1.2', '1.2') < 0); - }); - - test('returns 0 for equal phases', () => { - assert.strictEqual(comparePhaseNum('1', '1'), 0); - assert.strictEqual(comparePhaseNum('2.1', '2.1'), 0); - }); -}); +// ─── normalizePhaseName / comparePhaseNum ────────────────────────────────────── +// NOTE: Comprehensive tests for normalizePhaseName and comparePhaseNum are in +// phase.test.cjs (which covers all edge cases: hybrid, letter-suffix, +// multi-level decimal, case-insensitive, directory-slug, and full sort order). +// Removed duplicates here to keep a single authoritative test location. // ─── safeReadFile ────────────────────────────────────────────────────────────── @@ -343,7 +291,7 @@ describe('safeReadFile', () => { }); afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + cleanup(tmpDir); }); test('reads existing file', () => { @@ -363,12 +311,11 @@ describe('pathExistsInternal', () => { let tmpDir; beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); - fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); + tmpDir = createTempProject(); }); afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + cleanup(tmpDir); }); test('returns true for existing path', () => { @@ -390,12 +337,11 @@ describe('getMilestoneInfo', () => { let tmpDir; beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); - fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); + tmpDir = createTempProject(); }); afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + cleanup(tmpDir); }); test('extracts version and name from roadmap', () => { @@ -502,7 +448,7 @@ describe('searchPhaseInDir', () => { }); afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + cleanup(tmpDir); }); test('finds phase directory by normalized prefix', () => { @@ -564,12 +510,11 @@ describe('findPhaseInternal', () => { let tmpDir; beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); - fs.mkdirSync(path.join(tmpDir, '.planning', 'phases'), { recursive: true }); + tmpDir = createTempProject(); }); afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + cleanup(tmpDir); }); test('finds phase in current phases directory', () => { @@ -605,12 +550,11 @@ describe('getRoadmapPhaseInternal', () => { let tmpDir; beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); - fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); + tmpDir = createTempProject(); }); afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + cleanup(tmpDir); }); // Bug: getRoadmapPhaseInternal was missing from module.exports @@ -687,12 +631,11 @@ describe('getMilestonePhaseFilter', () => { let tmpDir; beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); - fs.mkdirSync(path.join(tmpDir, '.planning', 'phases'), { recursive: true }); + tmpDir = createTempProject(); }); afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); + cleanup(tmpDir); }); test('filters directories to only current milestone phases', () => { diff --git a/tests/gemini-config.test.cjs b/tests/gemini-config.test.cjs deleted file mode 100644 index 794208427..000000000 --- a/tests/gemini-config.test.cjs +++ /dev/null @@ -1,47 +0,0 @@ -/** - * GSD Tools Tests - Gemini agent conversion - * - * Verifies Gemini-specific agent frontmatter conversion removes - * unsupported fields while preserving converted tools and body text. - */ - -process.env.GSD_TEST_MODE = '1'; - -const { test, describe } = require('node:test'); -const assert = require('node:assert'); - -const { convertClaudeToGeminiAgent } = require('../bin/install.js'); - -describe('convertClaudeToGeminiAgent', () => { - test('drops unsupported skills frontmatter while keeping converted tools', () => { - const input = `--- -name: gsd-codebase-mapper -description: Explores codebase and writes structured analysis documents. -tools: Read, Bash, Grep, Glob, Write -color: cyan -skills: - - gsd-mapper-workflow ---- - - -Use \${PHASE} in shell examples. -`; - - const result = convertClaudeToGeminiAgent(input); - const frontmatter = result.split('---')[1] || ''; - - assert.ok(frontmatter.includes('name: gsd-codebase-mapper'), 'keeps name'); - assert.ok(frontmatter.includes('description: Explores codebase and writes structured analysis documents.'), 'keeps description'); - assert.ok(frontmatter.includes('tools:'), 'adds Gemini tools array'); - assert.ok(frontmatter.includes(' - read_file'), 'maps Read -> read_file'); - assert.ok(frontmatter.includes(' - run_shell_command'), 'maps Bash -> run_shell_command'); - assert.ok(frontmatter.includes(' - search_file_content'), 'maps Grep -> search_file_content'); - assert.ok(frontmatter.includes(' - glob'), 'maps Glob -> glob'); - assert.ok(frontmatter.includes(' - write_file'), 'maps Write -> write_file'); - assert.ok(!frontmatter.includes('color:'), 'drops unsupported color field'); - assert.ok(!frontmatter.includes('skills:'), 'drops unsupported skills field'); - assert.ok(!frontmatter.includes('gsd-mapper-workflow'), 'drops skills list items'); - assert.ok(result.includes('$PHASE'), 'escapes ${PHASE} shell variable for Gemini'); - assert.ok(!result.includes('${PHASE}'), 'removes Gemini template-string pattern'); - }); -}); diff --git a/tests/opencode-agent-conversion.test.cjs b/tests/runtime-converters.test.cjs similarity index 66% rename from tests/opencode-agent-conversion.test.cjs rename to tests/runtime-converters.test.cjs index 6ba62b54b..6491d9376 100644 --- a/tests/opencode-agent-conversion.test.cjs +++ b/tests/runtime-converters.test.cjs @@ -1,19 +1,21 @@ /** - * OpenCode Agent Frontmatter Conversion Tests + * Runtime Converter Tests — OpenCode + Gemini * - * Validates that convertClaudeToOpencodeFrontmatter correctly converts - * agent frontmatter for OpenCode compatibility when isAgent: true. + * Tests for small runtime-specific conversion functions from install.js. + * Larger runtime test suites (Copilot, Codex, Antigravity) have their own files. * - * Bug: Without isAgent flag, the function strips name: (agents need it), - * keeps color:/skills:/tools: record (should strip), and doesn't add - * model: inherit / mode: subagent (required by OpenCode agents). + * OpenCode: convertClaudeToOpencodeFrontmatter (agent + command modes) + * Gemini: convertClaudeToGeminiAgent (frontmatter + tool mapping + body escaping) */ const { test, describe } = require('node:test'); const assert = require('node:assert'); process.env.GSD_TEST_MODE = '1'; -const { convertClaudeToOpencodeFrontmatter } = require('../bin/install.js'); +const { + convertClaudeToOpencodeFrontmatter, + convertClaudeToGeminiAgent, +} = require('../bin/install.js'); // Sample Claude agent frontmatter (matches actual GSD agent format) const SAMPLE_AGENT = `--- @@ -141,3 +143,41 @@ describe('OpenCode command conversion (isAgent: false, default)', () => { assert.ok(frontmatter.includes('description:'), 'description should be kept'); }); }); + +// ───────────────────────────────────────────────────────────────────────────── +// Gemini CLI agent conversion (merged from gemini-config.test.cjs) +// ───────────────────────────────────────────────────────────────────────────── + +describe('convertClaudeToGeminiAgent', () => { + test('drops unsupported skills frontmatter while keeping converted tools', () => { + const input = `--- +name: gsd-codebase-mapper +description: Explores codebase and writes structured analysis documents. +tools: Read, Bash, Grep, Glob, Write +color: cyan +skills: + - gsd-mapper-workflow +--- + + +Use \${PHASE} in shell examples. +`; + + const result = convertClaudeToGeminiAgent(input); + const frontmatter = result.split('---')[1] || ''; + + assert.ok(frontmatter.includes('name: gsd-codebase-mapper'), 'keeps name'); + assert.ok(frontmatter.includes('description: Explores codebase and writes structured analysis documents.'), 'keeps description'); + assert.ok(frontmatter.includes('tools:'), 'adds Gemini tools array'); + assert.ok(frontmatter.includes(' - read_file'), 'maps Read -> read_file'); + assert.ok(frontmatter.includes(' - run_shell_command'), 'maps Bash -> run_shell_command'); + assert.ok(frontmatter.includes(' - search_file_content'), 'maps Grep -> search_file_content'); + assert.ok(frontmatter.includes(' - glob'), 'maps Glob -> glob'); + assert.ok(frontmatter.includes(' - write_file'), 'maps Write -> write_file'); + assert.ok(!frontmatter.includes('color:'), 'drops unsupported color field'); + assert.ok(!frontmatter.includes('skills:'), 'drops unsupported skills field'); + assert.ok(!frontmatter.includes('gsd-mapper-workflow'), 'drops skills list items'); + assert.ok(result.includes('$PHASE'), 'escapes ${PHASE} shell variable for Gemini'); + assert.ok(!result.includes('${PHASE}'), 'removes Gemini template-string pattern'); + }); +}); From 41f8cd48ed2e8a0a9fb319dc7612af1c059f3a14 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:56:35 -0400 Subject: [PATCH 18/83] feat: add /gsd:next command for automatic workflow advancement (#927) (#1167) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a zero-friction command that detects the current project state and automatically invokes the next logical workflow step: - No phases → discuss first phase - Phase has no context → discuss - Phase has context but no plans → plan - Phase has plans but incomplete → execute - All plans complete → verify and complete phase - All phases complete → complete milestone - Paused → resume work No arguments needed — reads STATE.md, ROADMAP.md, and phase directories to determine progression. Designed for multi-project workflows. Closes #927 --- commands/gsd/next.md | 24 ++++++++ get-shit-done/workflows/next.md | 97 +++++++++++++++++++++++++++++++++ tests/copilot-install.test.cjs | 4 +- 3 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 commands/gsd/next.md create mode 100644 get-shit-done/workflows/next.md diff --git a/commands/gsd/next.md b/commands/gsd/next.md new file mode 100644 index 000000000..e7d81c747 --- /dev/null +++ b/commands/gsd/next.md @@ -0,0 +1,24 @@ +--- +name: gsd:next +description: Automatically advance to the next logical step in the GSD workflow +allowed-tools: + - Read + - Bash + - Grep + - Glob + - SlashCommand +--- + +Detect the current project state and automatically invoke the next logical GSD workflow step. +No arguments needed — reads STATE.md, ROADMAP.md, and phase directories to determine what comes next. + +Designed for rapid multi-project workflows where remembering which phase/step you're on is overhead. + + + +@~/.claude/get-shit-done/workflows/next.md + + + +Execute the next workflow from @~/.claude/get-shit-done/workflows/next.md end-to-end. + diff --git a/get-shit-done/workflows/next.md b/get-shit-done/workflows/next.md new file mode 100644 index 000000000..80e2f3622 --- /dev/null +++ b/get-shit-done/workflows/next.md @@ -0,0 +1,97 @@ + +Detect current project state and automatically advance to the next logical GSD workflow step. +Reads project state to determine: discuss → plan → execute → verify → complete progression. + + + +Read all files referenced by the invoking prompt's execution_context before starting. + + + + + +Read project state to determine current position: + +```bash +# Get state snapshot +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state json 2>/dev/null || echo "{}" +``` + +Also read: +- `.planning/STATE.md` — current phase, progress, plan counts +- `.planning/ROADMAP.md` — milestone structure and phase list + +Extract: +- `current_phase` — which phase is active +- `plan_of` / `plans_total` — plan execution progress +- `progress` — overall percentage +- `status` — active, paused, etc. + +If no `.planning/` directory exists: +``` +No GSD project detected. Run `/gsd:new-project` to get started. +``` +Exit. + + + +Apply routing rules based on state: + +**Route 1: No phases exist yet → discuss** +If ROADMAP has phases but no phase directories exist on disk: +→ Next action: `/gsd:discuss-phase ` + +**Route 2: Phase exists but has no CONTEXT.md or RESEARCH.md → discuss** +If the current phase directory exists but has neither CONTEXT.md nor RESEARCH.md: +→ Next action: `/gsd:discuss-phase ` + +**Route 3: Phase has context but no plans → plan** +If the current phase has CONTEXT.md (or RESEARCH.md) but no PLAN.md files: +→ Next action: `/gsd:plan-phase ` + +**Route 4: Phase has plans but incomplete summaries → execute** +If plans exist but not all have matching summaries: +→ Next action: `/gsd:execute-phase ` + +**Route 5: All plans have summaries → verify and complete** +If all plans in the current phase have summaries: +→ Next action: `/gsd:verify-work` then `/gsd:complete-phase` + +**Route 6: Phase complete, next phase exists → advance** +If the current phase is complete and the next phase exists in ROADMAP: +→ Next action: `/gsd:discuss-phase ` + +**Route 7: All phases complete → complete milestone** +If all phases are complete: +→ Next action: `/gsd:complete-milestone` + +**Route 8: Paused → resume** +If STATE.md shows paused_at: +→ Next action: `/gsd:resume-work` + + + +Display the determination: + +``` +## GSD Next + +**Current:** Phase [N] — [name] | [progress]% +**Status:** [status description] + +▶ **Next step:** `/gsd:[command] [args]` + [One-line explanation of why this is the next step] +``` + +Then immediately invoke the determined command via SlashCommand. +Do not ask for confirmation — the whole point of `/gsd:next` is zero-friction advancement. + + + + + +- [ ] Project state correctly detected +- [ ] Next action correctly determined from routing rules +- [ ] Command invoked immediately without user confirmation +- [ ] Clear status shown before invoking + diff --git a/tests/copilot-install.test.cjs b/tests/copilot-install.test.cjs index 158ea974e..88404019e 100644 --- a/tests/copilot-install.test.cjs +++ b/tests/copilot-install.test.cjs @@ -625,7 +625,7 @@ describe('copyCommandsAsCopilotSkills', () => { // Count gsd-* directories — should be 31 const dirs = fs.readdirSync(tempDir, { withFileTypes: true }) .filter(e => e.isDirectory() && e.name.startsWith('gsd-')); - assert.strictEqual(dirs.length, 39, `expected 39 skill folders, got ${dirs.length}`); + assert.strictEqual(dirs.length, 40, `expected 40 skill folders, got ${dirs.length}`); } finally { fs.rmSync(tempDir, { recursive: true }); } @@ -1119,7 +1119,7 @@ const { execFileSync } = require('child_process'); const crypto = require('crypto'); const INSTALL_PATH = path.join(__dirname, '..', 'bin', 'install.js'); -const EXPECTED_SKILLS = 39; +const EXPECTED_SKILLS = 40; const EXPECTED_AGENTS = 16; function runCopilotInstall(cwd) { From 8520424a62d9af3e311b91856d68b188fe7f773e Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:56:44 -0400 Subject: [PATCH 19/83] fix: replace curl with fetch() in verification examples for Windows compatibility (#899) (#1166) MSYS curl on Windows has SSL/TLS failures and path mangling issues. Replaced curl references in checkpoint and phase-prompt templates with Node.js fetch() which works cross-platform. Changes: - checkpoints.md: server readiness check uses fetch() instead of curl - checkpoints.md: added cross-platform note about curl vs fetch - checkpoints.md: verify tags use fetch instead of curl - phase-prompt.md: verify tags use fetch instead of curl Partially addresses #899 (patch 1 of 6) --- get-shit-done/references/checkpoints.md | 22 ++++++++++++---------- get-shit-done/templates/phase-prompt.md | 4 ++-- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/get-shit-done/references/checkpoints.md b/get-shit-done/references/checkpoints.md index 232817480..23f90e99c 100644 --- a/get-shit-done/references/checkpoints.md +++ b/get-shit-done/references/checkpoints.md @@ -50,7 +50,7 @@ Plans execute autonomously. Checkpoints formalize interaction points where human Start dev server for verification Run `npm run dev` in background, wait for "ready" message, capture port - curl http://localhost:3000 returns 200 + fetch http://localhost:3000 returns 200 Dev server running at http://localhost:3000 @@ -240,7 +240,7 @@ Plans execute autonomously. Checkpoints formalize interaction points where human Deploy to Vercel .vercel/, vercel.json Run `vercel --yes` to deploy - vercel ls shows deployment, curl returns 200 + vercel ls shows deployment, fetch returns 200 @@ -261,7 +261,7 @@ Plans execute autonomously. Checkpoints formalize interaction points where human Retry Vercel deployment Run `vercel --yes` (now authenticated) - vercel ls shows deployment, curl returns 200 + vercel ls shows deployment, fetch returns 200 ``` @@ -455,8 +455,8 @@ I'll verify: vercel whoami returns your account npm run dev & DEV_SERVER_PID=$! -# Wait for ready (max 30s) -timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; done' +# Wait for ready (max 30s) — uses fetch() for cross-platform compatibility +timeout 30 bash -c 'until node -e "fetch(\"http://localhost:3000\").then(r=>{process.exit(r.ok?0:1)}).catch(()=>process.exit(1))" 2>/dev/null; do sleep 1; done' ``` **Port conflicts:** Kill stale process (`lsof -ti:3000 | xargs kill`) or use alternate port (`--port 3001`). @@ -489,7 +489,9 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d | Auth error | Create auth gate checkpoint | | Network timeout | Retry with backoff, then checkpoint if persistent | -**Never present a checkpoint with broken verification environment.** If `curl localhost:3000` fails, don't ask user to "visit localhost:3000". +**Never present a checkpoint with broken verification environment.** If the local server isn't responding, don't ask user to "visit localhost:3000". + +> **Cross-platform note:** Use `node -e "fetch('http://localhost:3000').then(r=>console.log(r.status))"` instead of `curl` for health checks. `curl` is broken on Windows MSYS/Git Bash due to SSL/path mangling issues. ```xml @@ -502,7 +504,7 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d Fix server startup issue Investigate error, fix root cause, restart server - curl http://localhost:3000 returns 200 + fetch http://localhost:3000 returns 200 @@ -608,7 +610,7 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d Start dev server for auth testing Run `npm run dev` in background, wait for ready signal - curl http://localhost:3000 returns 200 + fetch http://localhost:3000 returns 200 Dev server running at http://localhost:3000 @@ -651,7 +653,7 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d Start dev server Run `npm run dev` in background - curl localhost:3000 returns 200 + fetch http://localhost:3000 returns 200 @@ -677,7 +679,7 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d Deploy to Vercel Run `vercel --yes`. Capture URL. - vercel ls shows deployment, curl returns 200 + vercel ls shows deployment, fetch returns 200 diff --git a/get-shit-done/templates/phase-prompt.md b/get-shit-done/templates/phase-prompt.md index 6d23160dd..b242dc15e 100644 --- a/get-shit-done/templates/phase-prompt.md +++ b/get-shit-done/templates/phase-prompt.md @@ -341,7 +341,7 @@ Output: User model, API endpoints, and UI components. Task 2: Create User API endpoints src/features/user/api.ts GET /users (list), GET /users/:id (single), POST /users (create). Use User type from model. - curl tests pass for all endpoints + fetch tests pass for all endpoints All CRUD operations work @@ -407,7 +407,7 @@ Output: Working dashboard component. Start dev server Run `npm run dev` in background, wait for ready - curl localhost:3000 returns 200 + fetch http://localhost:3000 returns 200 From 14c1dd845b993e612f7eb31d31ff720bcc18f744 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:56:51 -0400 Subject: [PATCH 20/83] fix(build): add syntax validation to hook build script (#1165) Prevents shipping hooks with JavaScript SyntaxError (like the duplicate const cwd declaration that caused PostToolUse errors for all users in v1.25.1). The build script now validates each hook file's syntax via vm.Script before copying to dist/. If any hook has a SyntaxError, the build fails with a clear error message and exits non-zero, blocking npm publish. Refs #1107, #1109, #1125, #1161 --- scripts/build-hooks.js | 43 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/scripts/build-hooks.js b/scripts/build-hooks.js index ffb60b0ff..dda263e4e 100644 --- a/scripts/build-hooks.js +++ b/scripts/build-hooks.js @@ -1,10 +1,14 @@ #!/usr/bin/env node /** * Copy GSD hooks to dist for installation. + * Validates JavaScript syntax before copying to prevent shipping broken hooks. + * See #1107, #1109, #1125, #1161 — a duplicate const declaration shipped + * in dist and caused PostToolUse hook errors for all users. */ const fs = require('fs'); const path = require('path'); +const vm = require('vm'); const HOOKS_DIR = path.join(__dirname, '..', 'hooks'); const DIST_DIR = path.join(HOOKS_DIR, 'dist'); @@ -16,13 +20,34 @@ const HOOKS_TO_COPY = [ 'gsd-statusline.js' ]; +/** + * Validate JavaScript syntax without executing the file. + * Catches SyntaxError (duplicate const, missing brackets, etc.) + * before the hook gets shipped to users. + */ +function validateSyntax(filePath) { + const content = fs.readFileSync(filePath, 'utf8'); + try { + // Use vm.compileFunction to check syntax without executing + new vm.Script(content, { filename: path.basename(filePath) }); + return null; // No error + } catch (e) { + if (e instanceof SyntaxError) { + return e.message; + } + throw e; + } +} + function build() { // Ensure dist directory exists if (!fs.existsSync(DIST_DIR)) { fs.mkdirSync(DIST_DIR, { recursive: true }); } - // Copy hooks to dist + let hasErrors = false; + + // Copy hooks to dist with syntax validation for (const hook of HOOKS_TO_COPY) { const src = path.join(HOOKS_DIR, hook); const dest = path.join(DIST_DIR, hook); @@ -32,9 +57,21 @@ function build() { continue; } - console.log(`Copying ${hook}...`); + // Validate syntax before copying + const syntaxError = validateSyntax(src); + if (syntaxError) { + console.error(`\x1b[31m✗ ${hook}: SyntaxError — ${syntaxError}\x1b[0m`); + hasErrors = true; + continue; + } + + console.log(`\x1b[32m✓\x1b[0m Copying ${hook}...`); fs.copyFileSync(src, dest); - console.log(` → ${dest}`); + } + + if (hasErrors) { + console.error('\n\x1b[31mBuild failed: fix syntax errors above before publishing.\x1b[0m'); + process.exit(1); } console.log('\nBuild complete.'); From e7198f419f89faa6d72f9ca4e35eb417d132d0ae Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:57:20 -0400 Subject: [PATCH 21/83] fix: hook version tracking, stale hook detection, stdin timeout, and session-report command (#1153, #1157, #1161, #1162) (#1163) * fix: hook version tracking, stale hook detection, and stdin timeout increase - Add gsd-hook-version header to all hook files for version tracking (#1153) - Install.js now stamps current version into hooks during installation - gsd-check-update.js detects stale hooks by comparing version headers - gsd-statusline.js shows warning when stale hooks are detected - Increase context monitor stdin timeout from 3s to 10s (#1162) - Set +x permission on hook files during installation (#1162) Fixes #1153, #1162, #1161 * feat: add /gsd:session-report command for post-session summary generation Adds a new command that generates SESSION_REPORT.md with: - Work performed summary (phases touched, commits, files changed) - Key outcomes and decisions made - Active blockers and open items - Estimated resource usage metrics Reports are written to .planning/reports/ with date-stamped filenames. Closes #1157 * test: update expected skill count from 39 to 40 for new session-report command --- bin/install.js | 4 + commands/gsd/session-report.md | 19 +++ get-shit-done/workflows/session-report.md | 146 ++++++++++++++++++++++ hooks/gsd-check-update.js | 34 ++++- hooks/gsd-context-monitor.js | 10 +- hooks/gsd-statusline.js | 4 + 6 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 commands/gsd/session-report.md create mode 100644 get-shit-done/workflows/session-report.md diff --git a/bin/install.js b/bin/install.js index cff9bb5f8..95b50d7e9 100755 --- a/bin/install.js +++ b/bin/install.js @@ -2606,10 +2606,14 @@ function install(isGlobal, runtime = 'claude') { if (fs.statSync(srcFile).isFile()) { const destFile = path.join(hooksDest, entry); // Template .js files to replace '.claude' with runtime-specific config dir + // and stamp the current GSD version into the hook version header if (entry.endsWith('.js')) { let content = fs.readFileSync(srcFile, 'utf8'); content = content.replace(/'\.claude'/g, configDirReplacement); + content = content.replace(/\{\{GSD_VERSION\}\}/g, pkg.version); fs.writeFileSync(destFile, content); + // Ensure hook files are executable (fixes #1162 — missing +x permission) + try { fs.chmodSync(destFile, 0o755); } catch (e) { /* Windows doesn't support chmod */ } } else { fs.copyFileSync(srcFile, destFile); } diff --git a/commands/gsd/session-report.md b/commands/gsd/session-report.md new file mode 100644 index 000000000..a0eb1d6ef --- /dev/null +++ b/commands/gsd/session-report.md @@ -0,0 +1,19 @@ +--- +name: gsd:session-report +description: Generate a session report with token usage estimates, work summary, and outcomes +allowed-tools: + - Read + - Bash + - Write +--- + +Generate a structured SESSION_REPORT.md document capturing session outcomes, work performed, and estimated resource usage. Provides a shareable artifact for post-session review. + + + +@~/.claude/get-shit-done/workflows/session-report.md + + + +Execute the session-report workflow from @~/.claude/get-shit-done/workflows/session-report.md end-to-end. + diff --git a/get-shit-done/workflows/session-report.md b/get-shit-done/workflows/session-report.md new file mode 100644 index 000000000..f336edc08 --- /dev/null +++ b/get-shit-done/workflows/session-report.md @@ -0,0 +1,146 @@ + +Generate a post-session summary document capturing work performed, outcomes achieved, and estimated resource usage. Writes SESSION_REPORT.md to .planning/reports/ for human review and stakeholder sharing. + + + +Read all files referenced by the invoking prompt's execution_context before starting. + + + + + +Collect session data from available sources: + +1. **STATE.md** — current phase, milestone, progress, blockers, decisions +2. **Git log** — commits made during this session (last 24h or since last report) +3. **Plan/Summary files** — plans executed, summaries written +4. **ROADMAP.md** — milestone context and phase goals + +```bash +# Get recent commits (last 24 hours) +git log --oneline --since="24 hours ago" --no-merges 2>/dev/null || echo "No recent commits" + +# Count files changed +git diff --stat HEAD~10 HEAD 2>/dev/null | tail -1 || echo "No diff available" +``` + +Read `.planning/STATE.md` to get: +- Current milestone and phase +- Progress percentage +- Active blockers +- Recent decisions + +Read `.planning/ROADMAP.md` to get milestone name and goals. + +Check for existing reports: +```bash +ls -la .planning/reports/SESSION_REPORT*.md 2>/dev/null || echo "No previous reports" +``` + + + +Estimate token usage from observable signals: + +- Count of tool calls is not directly available, so estimate from git activity and file operations +- Note: This is an **estimate** — exact token counts require API-level instrumentation not available to hooks + +Estimation heuristics: +- Each commit ≈ 1 plan cycle (research + plan + execute + verify) +- Each plan file ≈ 2,000-5,000 tokens of agent context +- Each summary file ≈ 1,000-2,000 tokens generated +- Subagent spawns multiply by ~1.5x per agent type used + + + +Create the report directory and file: + +```bash +mkdir -p .planning/reports +``` + +Write `.planning/reports/SESSION_REPORT.md` (or `.planning/reports/YYYYMMDD-session-report.md` if previous reports exist): + +```markdown +# GSD Session Report + +**Generated:** [timestamp] +**Project:** [from PROJECT.md title or directory name] +**Milestone:** [N] — [milestone name from ROADMAP.md] + +--- + +## Session Summary + +**Duration:** [estimated from first to last commit timestamp, or "Single session"] +**Phase Progress:** [from STATE.md] +**Plans Executed:** [count of summaries written this session] +**Commits Made:** [count from git log] + +## Work Performed + +### Phases Touched +[List phases worked on with brief description of what was done] + +### Key Outcomes +[Bullet list of concrete deliverables: files created, features implemented, bugs fixed] + +### Decisions Made +[From STATE.md decisions table, if any were added this session] + +## Files Changed + +[Summary of files modified, created, deleted — from git diff stat] + +## Blockers & Open Items + +[Active blockers from STATE.md] +[Any TODO items created during session] + +## Estimated Resource Usage + +| Metric | Estimate | +|--------|----------| +| Commits | [N] | +| Files changed | [N] | +| Plans executed | [N] | +| Subagents spawned | [estimated] | + +> **Note:** Token and cost estimates require API-level instrumentation. +> These metrics reflect observable session activity only. + +--- + +*Generated by `/gsd:session-report`* +``` + + + +Show the user: + +``` +## Session Report Generated + +📄 `.planning/reports/[filename].md` + +### Highlights +- **Commits:** [N] +- **Files changed:** [N] +- **Phase progress:** [X]% +- **Plans executed:** [N] +``` + +If this is the first report, mention: +``` +💡 Run `/gsd:session-report` at the end of each session to build a history of project activity. +``` + + + + + +- [ ] Session data gathered from STATE.md, git log, and plan files +- [ ] Report written to .planning/reports/ +- [ ] Report includes work summary, outcomes, and file changes +- [ ] Filename includes date to prevent overwrites +- [ ] Result summary displayed to user + diff --git a/hooks/gsd-check-update.js b/hooks/gsd-check-update.js index b9a6075ed..510302fb3 100755 --- a/hooks/gsd-check-update.js +++ b/hooks/gsd-check-update.js @@ -1,4 +1,5 @@ #!/usr/bin/env node +// gsd-hook-version: {{GSD_VERSION}} // Check for GSD updates in background, write result to cache // Called by SessionStart hook - runs once per session @@ -43,6 +44,7 @@ if (!fs.existsSync(cacheDir)) { // Run check in background (spawn background process, windowsHide prevents console flash) const child = spawn(process.execPath, ['-e', ` const fs = require('fs'); + const path = require('path'); const { execSync } = require('child_process'); const cacheFile = ${JSON.stringify(cacheFile)}; @@ -51,14 +53,43 @@ const child = spawn(process.execPath, ['-e', ` // Check project directory first (local install), then global let installed = '0.0.0'; + let configDir = ''; try { if (fs.existsSync(projectVersionFile)) { installed = fs.readFileSync(projectVersionFile, 'utf8').trim(); + configDir = path.dirname(path.dirname(projectVersionFile)); } else if (fs.existsSync(globalVersionFile)) { installed = fs.readFileSync(globalVersionFile, 'utf8').trim(); + configDir = path.dirname(path.dirname(globalVersionFile)); } } catch (e) {} + // Check for stale hooks — compare hook version headers against installed VERSION + let staleHooks = []; + if (configDir) { + const hooksDir = path.join(configDir, 'hooks'); + try { + if (fs.existsSync(hooksDir)) { + const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.js')); + for (const hookFile of hookFiles) { + try { + const content = fs.readFileSync(path.join(hooksDir, hookFile), 'utf8'); + const versionMatch = content.match(/\\/\\/ gsd-hook-version:\\s*(.+)/); + if (versionMatch) { + const hookVersion = versionMatch[1].trim(); + if (hookVersion !== installed && !hookVersion.includes('{{')) { + staleHooks.push({ file: hookFile, hookVersion, installedVersion: installed }); + } + } else { + // No version header at all — definitely stale (pre-version-tracking) + staleHooks.push({ file: hookFile, hookVersion: 'unknown', installedVersion: installed }); + } + } catch (e) {} + } + } + } catch (e) {} + } + let latest = null; try { latest = execSync('npm view get-shit-done-cc version', { encoding: 'utf8', timeout: 10000, windowsHide: true }).trim(); @@ -68,7 +99,8 @@ const child = spawn(process.execPath, ['-e', ` update_available: latest && installed !== latest, installed, latest: latest || 'unknown', - checked: Math.floor(Date.now() / 1000) + checked: Math.floor(Date.now() / 1000), + stale_hooks: staleHooks.length > 0 ? staleHooks : undefined }; fs.writeFileSync(cacheFile, JSON.stringify(result)); diff --git a/hooks/gsd-context-monitor.js b/hooks/gsd-context-monitor.js index d7a5eff06..ae1bbf9a3 100644 --- a/hooks/gsd-context-monitor.js +++ b/hooks/gsd-context-monitor.js @@ -1,4 +1,5 @@ #!/usr/bin/env node +// gsd-hook-version: {{GSD_VERSION}} // Context Monitor - PostToolUse/AfterTool hook (Gemini uses AfterTool) // Reads context metrics from the statusline bridge file and injects // warnings when context usage is high. This makes the AGENT aware of @@ -27,10 +28,11 @@ const STALE_SECONDS = 60; // ignore metrics older than 60s const DEBOUNCE_CALLS = 5; // min tool uses between warnings let input = ''; -// Timeout guard: if stdin doesn't close within 3s (e.g. pipe issues on -// Windows/Git Bash), exit silently instead of hanging until Claude Code -// kills the process and reports "hook error". See #775. -const stdinTimeout = setTimeout(() => process.exit(0), 3000); +// Timeout guard: if stdin doesn't close within 10s (e.g. pipe issues on +// Windows/Git Bash, or slow Claude Code piping during large outputs), +// exit silently instead of hanging until Claude Code kills the process +// and reports "hook error". See #775, #1162. +const stdinTimeout = setTimeout(() => process.exit(0), 10000); process.stdin.setEncoding('utf8'); process.stdin.on('data', chunk => input += chunk); process.stdin.on('end', () => { diff --git a/hooks/gsd-statusline.js b/hooks/gsd-statusline.js index d88ca4a2c..ae7025b99 100755 --- a/hooks/gsd-statusline.js +++ b/hooks/gsd-statusline.js @@ -1,4 +1,5 @@ #!/usr/bin/env node +// gsd-hook-version: {{GSD_VERSION}} // Claude Code Statusline - GSD Edition // Shows: model | current task | directory | context usage @@ -99,6 +100,9 @@ process.stdin.on('end', () => { if (cache.update_available) { gsdUpdate = '\x1b[33m⬆ /gsd:update\x1b[0m │ '; } + if (cache.stale_hooks && cache.stale_hooks.length > 0) { + gsdUpdate += '\x1b[31m⚠ stale hooks — run /gsd:update\x1b[0m │ '; + } } catch (e) {} } From 26d742c5485bcc38e0894b4552399751c50def58 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:57:31 -0400 Subject: [PATCH 22/83] fix(core): replace negative-heuristic stripShippedMilestones with positive milestone lookup (#1145) (#1146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit stripShippedMilestones() uses a negative heuristic: strip all
blocks, assume what remains is the current milestone. This breaks when agents accidentally wrap the current milestone in
for collapsibility — all downstream consumers then see an empty milestone. Observed failure: cmdPhaseComplete() returns is_last_phase: true and next_phase: null for non-final phases because the current milestone's phases were stripped along with shipped ones. Added extractCurrentMilestone(content, cwd) — a positive lookup that: 1. Reads the current milestone version from STATE.md frontmatter 2. Falls back to 🚧 in-progress marker in ROADMAP.md 3. Finds the section heading matching that version 4. Returns only that section's content 5. Falls back to stripShippedMilestones() if version can't be determined Updated 12 call sites across 6 files to use extractCurrentMilestone: - core.cjs: getRoadmapPhaseInternal(), getMilestonePhaseFilter() - phase.cjs: cmdPhaseAdd(), cmdPhaseInsert(), cmdPhaseComplete() (2 sites) - roadmap.cjs: cmdRoadmapGetPhase(), cmdRoadmapAnalyze() - commands.cjs: stats/progress display - verify.cjs: phase verification (2 sites) - init.cjs: project initialization Kept stripShippedMilestones() for: - getMilestoneInfo() — determines the version itself, can't use positive lookup - replaceInCurrentMilestone() — write operations, conservative boundary - extractCurrentMilestone() fallback — when no version available All 755 tests pass. Fixes #1145 --- get-shit-done/bin/lib/commands.cjs | 4 +- get-shit-done/bin/lib/core.cjs | 90 +++++++++++++++++++++++++++++- get-shit-done/bin/lib/init.cjs | 6 +- get-shit-done/bin/lib/phase.cjs | 10 ++-- get-shit-done/bin/lib/roadmap.cjs | 6 +- get-shit-done/bin/lib/verify.cjs | 6 +- 6 files changed, 104 insertions(+), 18 deletions(-) diff --git a/get-shit-done/bin/lib/commands.cjs b/get-shit-done/bin/lib/commands.cjs index 73decc8bd..d7109df19 100644 --- a/get-shit-done/bin/lib/commands.cjs +++ b/get-shit-done/bin/lib/commands.cjs @@ -4,7 +4,7 @@ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); -const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, stripShippedMilestones, toPosixPath, output, error, findPhaseInternal } = require('./core.cjs'); +const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, stripShippedMilestones, extractCurrentMilestone, toPosixPath, output, error, findPhaseInternal } = require('./core.cjs'); const { extractFrontmatter } = require('./frontmatter.cjs'); const { MODEL_PROFILES } = require('./model-profiles.cjs'); @@ -547,7 +547,7 @@ function cmdStats(cwd, format, raw) { let totalSummaries = 0; try { - const roadmapContent = stripShippedMilestones(fs.readFileSync(roadmapPath, 'utf-8')); + const roadmapContent = extractCurrentMilestone(fs.readFileSync(roadmapPath, 'utf-8'), cwd); const headingPattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi; let match; while ((match = headingPattern.exec(roadmapContent)) !== null) { diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index 8c1b427f8..16d6e2726 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -420,6 +420,91 @@ function stripShippedMilestones(content) { return content.replace(/
[\s\S]*?<\/details>/gi, ''); } +/** + * Extract the current milestone section from ROADMAP.md by positive lookup. + * + * Instead of stripping
blocks (negative heuristic that breaks if + * agents wrap the current milestone in
), this finds the section + * matching the current milestone version and returns only that content. + * + * Falls back to stripShippedMilestones() if: + * - cwd is not provided + * - STATE.md doesn't exist or has no milestone field + * - Version can't be found in ROADMAP.md + * + * @param {string} content - Full ROADMAP.md content + * @param {string} [cwd] - Working directory for reading STATE.md + * @returns {string} Content scoped to current milestone + */ +function extractCurrentMilestone(content, cwd) { + if (!cwd) return stripShippedMilestones(content); + + // 1. Get current milestone version from STATE.md frontmatter + let version = null; + try { + const statePath = path.join(cwd, '.planning', 'STATE.md'); + if (fs.existsSync(statePath)) { + const stateRaw = fs.readFileSync(statePath, 'utf-8'); + const milestoneMatch = stateRaw.match(/^milestone:\s*(.+)/m); + if (milestoneMatch) { + version = milestoneMatch[1].trim(); + } + } + } catch {} + + // 2. Fallback: derive version from getMilestoneInfo pattern in ROADMAP.md itself + if (!version) { + // Check for 🚧 in-progress marker + const inProgressMatch = content.match(/🚧\s*\*\*v(\d+\.\d+)\s/); + if (inProgressMatch) { + version = 'v' + inProgressMatch[1]; + } + } + + if (!version) return stripShippedMilestones(content); + + // 3. Find the section matching this version + // Match headings like: ## Roadmap v3.0: Name, ## v3.0 Name, etc. + const escapedVersion = escapeRegex(version); + const sectionPattern = new RegExp( + `(^#{1,3}\\s+.*${escapedVersion}[^\\n]*)`, + 'mi' + ); + const sectionMatch = content.match(sectionPattern); + + if (!sectionMatch) return stripShippedMilestones(content); + + const sectionStart = sectionMatch.index; + + // Find the end: next milestone heading at same or higher level, or EOF + // Milestone headings look like: ## v2.0, ## Roadmap v2.0, ## ✅ v1.0, etc. + const headingLevel = sectionMatch[1].match(/^(#{1,3})\s/)[1].length; + const restContent = content.slice(sectionStart + sectionMatch[0].length); + const nextMilestonePattern = new RegExp( + `^#{1,${headingLevel}}\\s+(?:.*v\\d+\\.\\d+|✅|📋|🚧)`, + 'mi' + ); + const nextMatch = restContent.match(nextMilestonePattern); + + let sectionEnd; + if (nextMatch) { + sectionEnd = sectionStart + sectionMatch[0].length + nextMatch.index; + } else { + sectionEnd = content.length; + } + + // Return everything before the current milestone section (non-milestone content + // like title, overview) plus the current milestone section + const beforeMilestones = content.slice(0, sectionStart); + const currentSection = content.slice(sectionStart, sectionEnd); + + // Also include any content before the first milestone heading (title, overview, etc.) + // but strip any
blocks in it (these are definitely shipped) + const preamble = beforeMilestones.replace(/
[\s\S]*?<\/details>/gi, ''); + + return preamble + currentSection; +} + /** * Replace a pattern only in the current milestone section of ROADMAP.md * (everything after the last
close tag). Used for write operations @@ -444,7 +529,7 @@ function getRoadmapPhaseInternal(cwd, phaseNum) { if (!fs.existsSync(roadmapPath)) return null; try { - const content = stripShippedMilestones(fs.readFileSync(roadmapPath, 'utf-8')); + const content = extractCurrentMilestone(fs.readFileSync(roadmapPath, 'utf-8'), cwd); const escapedPhase = escapeRegex(phaseNum.toString()); const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`, 'i'); const headerMatch = content.match(phasePattern); @@ -549,7 +634,7 @@ function getMilestoneInfo(cwd) { function getMilestonePhaseFilter(cwd) { const milestonePhaseNums = new Set(); try { - const roadmap = stripShippedMilestones(fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8')); + const roadmap = extractCurrentMilestone(fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8'), cwd); const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi; let m; while ((m = phasePattern.exec(roadmap)) !== null) { @@ -597,6 +682,7 @@ module.exports = { getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, + extractCurrentMilestone, replaceInCurrentMilestone, toPosixPath, }; diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index d29e533c8..9c9d35655 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -5,7 +5,7 @@ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); -const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, normalizePhaseName, toPosixPath, output, error } = require('./core.cjs'); +const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, normalizePhaseName, toPosixPath, output, error } = require('./core.cjs'); function cmdInitExecutePhase(cwd, phase, raw) { if (!phase) { @@ -633,8 +633,8 @@ function cmdInitProgress(cwd, raw) { const roadmapPhaseNums = new Set(); const roadmapPhaseNames = new Map(); try { - const roadmapContent = stripShippedMilestones( - fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8') + const roadmapContent = extractCurrentMilestone( + fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8'), cwd ); const headingPattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi; let hm; diff --git a/get-shit-done/bin/lib/phase.cjs b/get-shit-done/bin/lib/phase.cjs index d88be9459..6bf2e5cf5 100644 --- a/get-shit-done/bin/lib/phase.cjs +++ b/get-shit-done/bin/lib/phase.cjs @@ -4,7 +4,7 @@ const fs = require('fs'); const path = require('path'); -const { escapeRegex, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, stripShippedMilestones, replaceInCurrentMilestone, toPosixPath, output, error } = require('./core.cjs'); +const { escapeRegex, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, toPosixPath, output, error } = require('./core.cjs'); const { extractFrontmatter } = require('./frontmatter.cjs'); const { writeStateMd } = require('./state.cjs'); @@ -319,7 +319,7 @@ function cmdPhaseAdd(cwd, description, raw) { } const rawContent = fs.readFileSync(roadmapPath, 'utf-8'); - const content = stripShippedMilestones(rawContent); + const content = extractCurrentMilestone(rawContent, cwd); const slug = generateSlugInternal(description); // Find highest integer phase number (in current milestone only) @@ -376,7 +376,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) { } const rawContent = fs.readFileSync(roadmapPath, 'utf-8'); - const content = stripShippedMilestones(rawContent); + const content = extractCurrentMilestone(rawContent, cwd); const slug = generateSlugInternal(description); // Normalize input then strip leading zeros for flexible matching @@ -760,7 +760,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) { if (fs.existsSync(reqPath)) { // Extract the current phase section from roadmap (scoped to avoid cross-phase matching) const phaseEsc = escapeRegex(phaseNum); - const currentMilestoneRoadmap = stripShippedMilestones(roadmapContent); + const currentMilestoneRoadmap = extractCurrentMilestone(roadmapContent, cwd); const phaseSectionMatch = currentMilestoneRoadmap.match( new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEsc}[:\\s][\\s\\S]*?)(?=#{2,4}\\s*Phase\\s+|$)`, 'i') ); @@ -824,7 +824,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) { // for phases that are defined but not yet planned (no directory on disk) if (isLastPhase && fs.existsSync(roadmapPath)) { try { - const roadmapForPhases = stripShippedMilestones(fs.readFileSync(roadmapPath, 'utf-8')); + const roadmapForPhases = extractCurrentMilestone(fs.readFileSync(roadmapPath, 'utf-8'), cwd); const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi; let pm; while ((pm = phasePattern.exec(roadmapForPhases)) !== null) { diff --git a/get-shit-done/bin/lib/roadmap.cjs b/get-shit-done/bin/lib/roadmap.cjs index 3164b702c..a199a6ab8 100644 --- a/get-shit-done/bin/lib/roadmap.cjs +++ b/get-shit-done/bin/lib/roadmap.cjs @@ -4,7 +4,7 @@ const fs = require('fs'); const path = require('path'); -const { escapeRegex, normalizePhaseName, output, error, findPhaseInternal, stripShippedMilestones, replaceInCurrentMilestone } = require('./core.cjs'); +const { escapeRegex, normalizePhaseName, output, error, findPhaseInternal, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone } = require('./core.cjs'); function cmdRoadmapGetPhase(cwd, phaseNum, raw) { const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md'); @@ -15,7 +15,7 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) { } try { - const content = stripShippedMilestones(fs.readFileSync(roadmapPath, 'utf-8')); + const content = extractCurrentMilestone(fs.readFileSync(roadmapPath, 'utf-8'), cwd); // Escape special regex chars in phase number, handle decimal const escapedPhase = escapeRegex(phaseNum); @@ -99,7 +99,7 @@ function cmdRoadmapAnalyze(cwd, raw) { } const rawContent = fs.readFileSync(roadmapPath, 'utf-8'); - const content = stripShippedMilestones(rawContent); + const content = extractCurrentMilestone(rawContent, cwd); const phasesDir = path.join(cwd, '.planning', 'phases'); // Extract all phase headings: ## Phase N: Name or ### Phase N: Name diff --git a/get-shit-done/bin/lib/verify.cjs b/get-shit-done/bin/lib/verify.cjs index 9f8a08546..61fbc16c6 100644 --- a/get-shit-done/bin/lib/verify.cjs +++ b/get-shit-done/bin/lib/verify.cjs @@ -5,7 +5,7 @@ const fs = require('fs'); const path = require('path'); const os = require('os'); -const { safeReadFile, normalizePhaseName, execGit, findPhaseInternal, getMilestoneInfo, stripShippedMilestones, output, error } = require('./core.cjs'); +const { safeReadFile, normalizePhaseName, execGit, findPhaseInternal, getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone, output, error } = require('./core.cjs'); const { extractFrontmatter, parseMustHavesBlock } = require('./frontmatter.cjs'); const { writeStateMd } = require('./state.cjs'); @@ -409,7 +409,7 @@ function cmdValidateConsistency(cwd, raw) { } const roadmapContentRaw = fs.readFileSync(roadmapPath, 'utf-8'); - const roadmapContent = stripShippedMilestones(roadmapContentRaw); + const roadmapContent = extractCurrentMilestone(roadmapContentRaw, cwd); // Extract phases from ROADMAP (archived milestones already stripped) const roadmapPhases = new Set(); @@ -695,7 +695,7 @@ function cmdValidateHealth(cwd, options, raw) { // Inline subset of cmdValidateConsistency if (fs.existsSync(roadmapPath)) { const roadmapContentRaw = fs.readFileSync(roadmapPath, 'utf-8'); - const roadmapContent = stripShippedMilestones(roadmapContentRaw); + const roadmapContent = extractCurrentMilestone(roadmapContentRaw, cwd); const roadmapPhases = new Set(); const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi; let m; From c2c4301a98e83de60fe77449b7377e8b1454ab67 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:57:42 -0400 Subject: [PATCH 23/83] feat: model alias-to-full-ID resolution for Task API compatibility (#991) (#1141) Claude Code's Task tool sometimes doesn't resolve short aliases (opus, sonnet, haiku) and passes them directly to the API, causing 404s. Tasks then inherit the parent session's model instead of the configured one. Added: - MODEL_ALIAS_MAP in core.cjs mapping aliases to full model IDs - resolve_model_ids config option (default: false for backward compat) - resolveModelInternal() maps aliases when resolve_model_ids is true Usage: { "resolve_model_ids": true } This causes gsd-tools resolve-model to return 'claude-sonnet-4-5' instead of 'sonnet', which the Task tool passes to the API without needing alias resolution on Claude Code's side. The alias map is maintained per release. Users can also use model_overrides for full control. All 755 tests pass. Fixes #991 --- get-shit-done/bin/lib/core.cjs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index 16d6e2726..5038dc247 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -64,6 +64,7 @@ function loadConfig(cwd) { nyquist_validation: true, parallelization: true, brave_search: false, + resolve_model_ids: false, // when true, resolve aliases (opus/sonnet/haiku) to full model IDs }; try { @@ -106,6 +107,7 @@ function loadConfig(cwd) { nyquist_validation: get('nyquist_validation', { section: 'workflow', field: 'nyquist_validation' }) ?? defaults.nyquist_validation, parallelization, brave_search: get('brave_search') ?? defaults.brave_search, + resolve_model_ids: get('resolve_model_ids') ?? defaults.resolve_model_ids, model_overrides: parsed.model_overrides || null, }; } catch { @@ -557,6 +559,19 @@ function getRoadmapPhaseInternal(cwd, phaseNum) { } } +// ─── Model alias resolution ─────────────────────────────────────────────────── + +/** + * Map short model aliases to full model IDs. + * Updated each release to match current model versions. + * Users can override with model_overrides in config.json for custom/latest models. + */ +const MODEL_ALIAS_MAP = { + 'opus': 'claude-opus-4-0', + 'sonnet': 'claude-sonnet-4-5', + 'haiku': 'claude-haiku-3-5', +}; + function resolveModelInternal(cwd, agentType) { const config = loadConfig(cwd); @@ -571,7 +586,15 @@ function resolveModelInternal(cwd, agentType) { const agentModels = MODEL_PROFILES[agentType]; if (!agentModels) return 'sonnet'; if (profile === 'inherit') return 'inherit'; - return agentModels[profile] || agentModels['balanced'] || 'sonnet'; + const alias = agentModels[profile] || agentModels['balanced'] || 'sonnet'; + + // If resolve_model_ids is true, map alias to full model ID + // This prevents 404s when the Task tool passes aliases directly to the API + if (config.resolve_model_ids) { + return MODEL_ALIAS_MAP[alias] || alias; + } + + return alias; } // ─── Misc utilities ─────────────────────────────────────────────────────────── @@ -685,4 +708,5 @@ module.exports = { extractCurrentMilestone, replaceInCurrentMilestone, toPosixPath, + MODEL_ALIAS_MAP, }; From 309d8671723c3aa54edaba9cd35c4e99708f0a18 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:57:59 -0400 Subject: [PATCH 24/83] fix: prevent nested Skill calls that break AskUserQuestion (#1009) (#1140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When plan-phase invokes discuss-phase as a nested Skill call, AskUserQuestion calls auto-resolve with empty answers — the user never sees the question UI. This is a Claude Code runtime bug with nested subcontexts. Made the 'Run discuss-phase first' path explicitly exit the workflow with a display message instead of risking nested invocation: - Added explicit warning: do NOT invoke as nested Skill/Task - Show the command for user to run as top-level - Exit the plan-phase workflow immediately Fixes #1009 --- get-shit-done/workflows/plan-phase.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/get-shit-done/workflows/plan-phase.md b/get-shit-done/workflows/plan-phase.md index f7114c5a8..8ec87113a 100644 --- a/get-shit-done/workflows/plan-phase.md +++ b/get-shit-done/workflows/plan-phase.md @@ -170,7 +170,16 @@ Use AskUserQuestion: - "Run discuss-phase first" — Capture design decisions before planning If "Continue without context": Proceed to step 5. -If "Run discuss-phase first": Display `/gsd:discuss-phase {X}` and exit workflow. +If "Run discuss-phase first": + **IMPORTANT:** Do NOT invoke discuss-phase as a nested Skill/Task call — AskUserQuestion + does not work correctly in nested subcontexts (#1009). Instead, display the command + and exit so the user runs it as a top-level command: + ``` + Run this command first, then re-run /gsd:plan-phase {X}: + + /gsd:discuss-phase {X} + ``` + **Exit the plan-phase workflow. Do not continue.** ## 5. Handle Research From 6536214a3ace443b61aab27c271a83474cacce43 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:58:08 -0400 Subject: [PATCH 25/83] fix: add explicit agent type listings to prevent fallback after /clear (#949) (#1139) After /clear, Claude Code sometimes loses awareness of custom agent types and falls back to 'general-purpose'. This happens because the model doesn't re-read .claude/agents/ after context reset. Added sections to: - execute-phase.md: lists all 12 valid GSD agent types with descriptions - plan-phase.md: lists the 3 agent types used during planning The explicit listing in workflow instructions ensures the model always has an unambiguous reference to valid agent types, regardless of whether .claude/agents/ was re-read after /clear. Fixes #949 --- get-shit-done/workflows/execute-phase.md | 18 ++++++++++++++++++ get-shit-done/workflows/plan-phase.md | 7 +++++++ 2 files changed, 25 insertions(+) diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index af9027722..b522dba55 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -10,6 +10,24 @@ Orchestrator coordinates, not executes. Each subagent loads the full execute-pla Read STATE.md before any operation to load project context. + +These are the valid GSD subagent types registered in .claude/agents/ (or equivalent for your runtime). +Always use the exact name from this list — do not fall back to 'general-purpose' or other built-in types: + +- gsd-executor — Executes plan tasks, commits, creates SUMMARY.md +- gsd-verifier — Verifies phase completion, checks quality gates +- gsd-planner — Creates detailed plans from phase scope +- gsd-phase-researcher — Researches technical approaches for a phase +- gsd-plan-checker — Reviews plan quality before execution +- gsd-debugger — Diagnoses and fixes issues +- gsd-codebase-mapper — Maps project structure and dependencies +- gsd-integration-checker — Checks cross-phase integration +- gsd-nyquist-auditor — Validates verification coverage +- gsd-ui-researcher — Researches UI/UX approaches +- gsd-ui-checker — Reviews UI implementation quality +- gsd-ui-auditor — Audits UI against design requirements + + diff --git a/get-shit-done/workflows/plan-phase.md b/get-shit-done/workflows/plan-phase.md index 8ec87113a..62815be58 100644 --- a/get-shit-done/workflows/plan-phase.md +++ b/get-shit-done/workflows/plan-phase.md @@ -8,6 +8,13 @@ Read all files referenced by the invoking prompt's execution_context before star @~/.claude/get-shit-done/references/ui-brand.md + +Valid GSD subagent types (use exact names — do not fall back to 'general-purpose'): +- gsd-phase-researcher — Researches technical approaches for a phase +- gsd-planner — Creates detailed plans from phase scope +- gsd-plan-checker — Reviews plan quality before execution + + ## 1. Initialize From aa9cb7bcb6aca1453f80deb7a440897b88e1092e Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:58:15 -0400 Subject: [PATCH 26/83] feat: add Codex hooks support for SessionStart (#1020) (#1138) Codex v0.114.0 added experimental hooks with SessionStart and Stop events. GSD's Codex installer now configures hooks in config.toml: - Enables codex_hooks feature flag in [features] section - Adds SessionStart hook for GSD update checking (gsd-update-check.js) - Graceful fallback if config.toml write fails Hook format follows Codex's TOML convention: [[hooks]] event = "SessionStart" command = "node /path/to/gsd-update-check.js" PostToolUse hooks (context monitor) are not yet added since Codex only supports SessionStart and Stop events currently. Fixes #1020 --- bin/install.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/bin/install.js b/bin/install.js index 95b50d7e9..c79642a02 100755 --- a/bin/install.js +++ b/bin/install.js @@ -2693,6 +2693,36 @@ function install(isGlobal, runtime = 'claude') { const agentCount = installCodexConfig(targetDir, agentsSrc); console.log(` ${green}✓${reset} Generated config.toml with ${agentCount} agent roles`); console.log(` ${green}✓${reset} Generated ${agentCount} agent .toml config files`); + + // Add Codex hooks (SessionStart for update checking) — requires codex_hooks feature flag + const configPath = path.join(targetDir, 'config.toml'); + try { + let configContent = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf-8') : ''; + + // Enable hooks feature flag if not present + if (!configContent.includes('codex_hooks')) { + const featuresSection = '[features]\ncodex_hooks = true\n'; + if (configContent.includes('[features]')) { + configContent = configContent.replace(/\[features\]\n/, featuresSection); + } else { + configContent = featuresSection + '\n' + configContent; + } + } + + // Add SessionStart hook for update checking + const updateCheckScript = path.resolve(targetDir, 'get-shit-done', 'hooks', 'gsd-update-check.js').replace(/\\/g, '/'); + const hookBlock = `\n# GSD Hooks\n[[hooks]]\nevent = "SessionStart"\ncommand = "node ${updateCheckScript}"\n`; + + if (!configContent.includes('gsd-update-check')) { + configContent += hookBlock; + } + + fs.writeFileSync(configPath, configContent, 'utf-8'); + console.log(` ${green}✓${reset} Configured Codex hooks (SessionStart)`); + } catch (e) { + console.warn(` ${yellow}⚠${reset} Could not configure Codex hooks: ${e.message}`); + } + return { settingsPath: null, settings: null, statuslineCommand: null, runtime }; } From 665c948c221fc7b3b13f98c0ee7cdd6c19aba513 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:58:36 -0400 Subject: [PATCH 27/83] feat: MCP tool awareness for GSD subagents (#973) (#1137) GSD executor agents ignore MCP tools (e.g. jCodeMunch) even when CLAUDE.md explicitly instructs their use. Agents default to Grep/Glob because those are explicitly referenced in workflow patterns. Added MCP tool instructions to: - execute-phase.md: section in executor agent prompt telling agents to prefer MCP tools over Grep/Glob when available - execute-plan.md: Step 2 in execute section with MCP tool fallback guidance Agents now: 1. Check if CLAUDE.md references MCP tools 2. Prefer MCP tools for code navigation when accessible 3. Fall back to Grep/Glob if MCP tools are not available Fixes #973 --- get-shit-done/workflows/execute-phase.md | 7 +++++++ get-shit-done/workflows/execute-plan.md | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index b522dba55..f77f3f272 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -158,6 +158,13 @@ Execute each wave in sequence. Within a wave: parallel if `PARALLELIZATION=true` - .claude/skills/ or .agents/skills/ (Project skills, if either exists — list skills, read SKILL.md for each, follow relevant rules during implementation) + + If CLAUDE.md or project instructions reference MCP tools (e.g. jCodeMunch, context7, + or other MCP servers), prefer those tools over Grep/Glob for code navigation when available. + MCP tools often save significant tokens by providing structured code indexes. + Check tool availability first — if MCP tools are not accessible, fall back to Grep/Glob. + + - [ ] All tasks executed - [ ] Each task committed individually diff --git a/get-shit-done/workflows/execute-plan.md b/get-shit-done/workflows/execute-plan.md index d47b6a18f..5dd6f8fc0 100644 --- a/get-shit-done/workflows/execute-plan.md +++ b/get-shit-done/workflows/execute-plan.md @@ -135,7 +135,8 @@ If previous SUMMARY has unresolved "Issues Encountered" or "Next Phase Readiness Deviations are normal — handle via rules below. 1. Read @context files from prompt -2. Per task: +2. **MCP tools:** If CLAUDE.md or project instructions reference MCP tools (e.g. jCodeMunch for code navigation), prefer them over Grep/Glob when available. Fall back to Grep/Glob if MCP tools are not accessible. +3. Per task: - **MANDATORY read_first gate:** If the task has a `` field, you MUST read every listed file BEFORE making any edits. This is not optional. Do not skip files because you "already know" what's in them — read them. The read_first files establish ground truth for the task. - `type="auto"`: if `tdd="true"` → TDD execution. Implement with deviation rules + auth gates. Verify done criteria. Commit (see task_commit). Track hash for Summary. - `type="checkpoint:*"`: STOP → checkpoint_protocol → wait for user → continue only after confirmation. From 27e9bad203f4345e2f392d9b4068faa7ce12bffa Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:59:11 -0400 Subject: [PATCH 28/83] feat: interactive executor mode for pair-programming style execution (#963) (#1136) Adds --interactive flag to /gsd:execute-phase that changes execution from autonomous subagent delegation to sequential inline execution with user checkpoints between tasks. Interactive mode: - Executes plans sequentially inline (no subagent spawning) - Presents each plan to user: Execute, Review first, Skip, Stop - Pauses after each task for user intervention - Dramatically lower token usage (no subagent overhead) - Maintains full GSD planning/tracking structure Changes: - execute-phase.md: new check_interactive_mode step with full interactive flow - execute-phase command: documented --interactive flag in argument-hint and context Use cases: - Small phases (1-3 plans, no complex dependencies) - Bug fixes and verification gap closure - Learning GSD workflow - When user wants to pair-program with Claude under GSD structure Fixes #963 --- commands/gsd/execute-phase.md | 3 +- get-shit-done/workflows/execute-phase.md | 48 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/commands/gsd/execute-phase.md b/commands/gsd/execute-phase.md index 1a798471f..b0741db0c 100644 --- a/commands/gsd/execute-phase.md +++ b/commands/gsd/execute-phase.md @@ -1,7 +1,7 @@ --- name: gsd:execute-phase description: Execute all plans in a phase with wave-based parallelization -argument-hint: " [--gaps-only]" +argument-hint: " [--gaps-only] [--interactive]" allowed-tools: - Read - Write @@ -31,6 +31,7 @@ Phase: $ARGUMENTS **Flags:** - `--gaps-only` — Execute only gap closure plans (plans with `gap_closure: true` in frontmatter). Use after verify-work creates fix plans. +- `--interactive` — Execute plans sequentially inline (no subagents) with user checkpoints between tasks. Lower token usage, pair-programming style. Best for small phases, bug fixes, and verification gaps. Context files are resolved inside the workflow via `gsd-tools init execute-phase` and per-subagent `` blocks. diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index f77f3f272..9e36e30f5 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -55,6 +55,54 @@ fi ``` + +**Parse `--interactive` flag from $ARGUMENTS.** + +**If `--interactive` flag present:** Switch to interactive execution mode. + +Interactive mode executes plans sequentially **inline** (no subagent spawning) with user +checkpoints between tasks. The user can review, modify, or redirect work at any point. + +**Interactive execution flow:** + +1. Load plan inventory as normal (discover_and_group_plans) +2. For each plan (sequentially, ignoring wave grouping): + + a. **Present the plan to the user:** + ``` + ## Plan {plan_id}: {plan_name} + + Objective: {from plan file} + Tasks: {task_count} + + Options: + - Execute (proceed with all tasks) + - Review first (show task breakdown before starting) + - Skip (move to next plan) + - Stop (end execution, save progress) + ``` + + b. **If "Review first":** Read and display the full plan file. Ask again: Execute, Modify, Skip. + + c. **If "Execute":** Read and follow `~/.claude/get-shit-done/workflows/execute-plan.md` **inline** + (do NOT spawn a subagent). Execute tasks one at a time. + + d. **After each task:** Pause briefly. If the user intervenes (types anything), stop and address + their feedback before continuing. Otherwise proceed to next task. + + e. **After plan complete:** Show results, commit, create SUMMARY.md, then present next plan. + +3. After all plans: proceed to verification (same as normal mode). + +**Benefits of interactive mode:** +- No subagent overhead — dramatically lower token usage +- User catches mistakes early — saves costly verification cycles +- Maintains GSD's planning/tracking structure +- Best for: small phases, bug fixes, verification gaps, learning GSD + +**Skip to handle_branching step** (interactive plans execute inline after grouping). + + Check `branching_strategy` from init: From 7dd31e6d202cfde647b90b847033c65907f687c0 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:59:39 -0400 Subject: [PATCH 29/83] =?UTF-8?q?feat:=20signal=20file=20for=20decision=20?= =?UTF-8?q?points=20=E2=80=94=20WAITING.json=20(#1034)=20(#1133)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When GSD hits a blocking decision point (AskUserQuestion, next action prompt), external watchers have no way to detect it. Users monitoring multiple auto sessions must visually check each terminal. Added: - state signal-waiting: writes .planning/WAITING.json (or .gsd/WAITING.json) with type, question, options, timestamp, and phase info - state signal-resume: removes WAITING.json when user answers Signal file format: { status, type, question, options[], since, phase } External tools can watch for this file via fswatch, polling, or inotify. Complements the existing remote-questions extension (Slack/Discord). Fixes #1034 --- get-shit-done/bin/gsd-tools.cjs | 17 ++++++++++++ get-shit-done/bin/lib/state.cjs | 49 +++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index 16975e8f2..cdee42566 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -15,6 +15,8 @@ * state get [section] Get STATE.md content or section * state patch --field val ... Batch update STATE.md fields * state begin-phase --phase N --name S --plans C Update STATE.md for new phase start + * state signal-waiting --type T --question Q --options "A|B" --phase P Write WAITING.json signal + * state signal-resume Remove WAITING.json signal * resolve-model Get model for agent based on profile * find-phase Find phase directory by number * commit [--files f1 f2] Commit planning docs @@ -255,6 +257,21 @@ async function main() { plansIdx !== -1 ? parseInt(args[plansIdx + 1], 10) : null, raw ); + } else if (subcommand === 'signal-waiting') { + const typeIdx = args.indexOf('--type'); + const qIdx = args.indexOf('--question'); + const optIdx = args.indexOf('--options'); + const phaseIdx = args.indexOf('--phase'); + state.cmdSignalWaiting( + cwd, + typeIdx !== -1 ? args[typeIdx + 1] : null, + qIdx !== -1 ? args[qIdx + 1] : null, + optIdx !== -1 ? args[optIdx + 1] : null, + phaseIdx !== -1 ? args[phaseIdx + 1] : null, + raw + ); + } else if (subcommand === 'signal-resume') { + state.cmdSignalResume(cwd, raw); } else { state.cmdStateLoad(cwd, raw); } diff --git a/get-shit-done/bin/lib/state.cjs b/get-shit-done/bin/lib/state.cjs index 40bf8d2cc..78797694a 100644 --- a/get-shit-done/bin/lib/state.cjs +++ b/get-shit-done/bin/lib/state.cjs @@ -778,6 +778,53 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) { output({ updated, phase: phaseNumber, phase_name: phaseName || null, plan_count: planCount || null }, raw, updated.length > 0 ? 'true' : 'false'); } +/** + * Write a WAITING.json signal file when GSD hits a decision point. + * External watchers (fswatch, polling, orchestrators) can detect this. + * File is written to .planning/WAITING.json (or .gsd/WAITING.json if .gsd exists). + * Fixes #1034. + */ +function cmdSignalWaiting(cwd, type, question, options, phase, raw) { + const gsdDir = fs.existsSync(path.join(cwd, '.gsd')) ? path.join(cwd, '.gsd') : path.join(cwd, '.planning'); + const waitingPath = path.join(gsdDir, 'WAITING.json'); + + const signal = { + status: 'waiting', + type: type || 'decision_point', + question: question || null, + options: options ? options.split('|').map(o => o.trim()) : [], + since: new Date().toISOString(), + phase: phase || null, + }; + + try { + fs.mkdirSync(gsdDir, { recursive: true }); + fs.writeFileSync(waitingPath, JSON.stringify(signal, null, 2), 'utf-8'); + output({ signaled: true, path: waitingPath }, raw, 'true'); + } catch (e) { + output({ signaled: false, error: e.message }, raw, 'false'); + } +} + +/** + * Remove the WAITING.json signal file when user answers and agent resumes. + */ +function cmdSignalResume(cwd, raw) { + const paths = [ + path.join(cwd, '.gsd', 'WAITING.json'), + path.join(cwd, '.planning', 'WAITING.json'), + ]; + + let removed = false; + for (const p of paths) { + if (fs.existsSync(p)) { + try { fs.unlinkSync(p); removed = true; } catch {} + } + } + + output({ resumed: true, removed }, raw, removed ? 'true' : 'false'); +} + module.exports = { stateExtractField, stateReplaceField, @@ -796,4 +843,6 @@ module.exports = { cmdStateSnapshot, cmdStateJson, cmdStateBeginPhase, + cmdSignalWaiting, + cmdSignalResume, }; From e8dbc3031bbbc809d9071fa3fa0aa2b1db2086c3 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 12:00:07 -0400 Subject: [PATCH 30/83] fix: add runtime compatibility fallback for Copilot executor stuck issue (#1128) (#1131) Copilot's subagent spawning (Task API) may not properly return completion signals to the orchestrator, causing it to hang indefinitely waiting for agents that have already finished their work. Added section to execute-phase.md with: - Runtime-specific subagent spawning guidance (Claude Code, Copilot, others) - Fallback rule: if agent completes work but orchestrator doesn't get the signal, treat as success based on spot-checks (SUMMARY.md exists, commits present) - Sequential inline execution fallback for runtimes without reliable Task API Fixes #1128 --- get-shit-done/workflows/execute-phase.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index 9e36e30f5..243103b9d 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -6,6 +6,20 @@ Execute all plans in a phase using wave-based parallel execution. Orchestrator s Orchestrator coordinates, not executes. Each subagent loads the full execute-plan context. Orchestrator: discover plans → analyze deps → group waves → spawn agents → handle checkpoints → collect results. + +**Subagent spawning is runtime-specific:** +- **Claude Code:** Uses `Task(subagent_type="gsd-executor", ...)` — blocks until complete, returns result +- **Copilot:** Uses `@gsd-executor` agent reference — if subagent spawning hangs or fails to return, + fall back to **sequential inline execution**: read and follow execute-plan.md directly for each plan + instead of spawning parallel agents. This is slower but reliable. +- **Other runtimes (Gemini, Codex, OpenCode):** If Task/subagent API is unavailable, use sequential + inline execution as the fallback. + +**Fallback rule:** If a spawned agent completes its work (commits visible, SUMMARY.md exists) but +the orchestrator never receives the completion signal, treat it as successful based on spot-checks +and continue to the next wave/plan. + + Read STATE.md before any operation to load project context. From 7101ddcb9c179900ccbb350b3ffecf742611f8ae Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 12:00:24 -0400 Subject: [PATCH 31/83] fix: prevent PROJECT.md drift and fix phase completion counters (#956) (#1130) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gaps in the standard workflow cycle caused planning document drift: 1. PROJECT.md was never updated during discuss → plan → execute → verify. Only transition.md (optional) and complete-milestone evolved it. Added an 'update_project_md' step to execute-phase.md that evolves PROJECT.md after phase completion: moves requirements to Validated, updates Current State, bumps Last Updated timestamp. 2. cmdPhaseComplete() in phase.cjs advanced 'Current Phase' but never incremented 'Completed Phases' counter or recalculated 'percent'. Added counter increment and percentage recalculation based on completed/total phases ratio. Addresses the workflow-level gaps identified in #956: - PROJECT.md evolution in execute-phase (gap #2) - completed_phases counter not incremented (gap #1 table row 3) - percent never recalculated (gap #1 table row 4) Fixes #956 --- get-shit-done/bin/lib/phase.cjs | 28 ++++++++++++++++++++++++ get-shit-done/workflows/execute-phase.md | 22 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/get-shit-done/bin/lib/phase.cjs b/get-shit-done/bin/lib/phase.cjs index 6bf2e5cf5..5cb5eac4d 100644 --- a/get-shit-done/bin/lib/phase.cjs +++ b/get-shit-done/bin/lib/phase.cjs @@ -880,6 +880,34 @@ function cmdPhaseComplete(cwd, phaseNum, raw) { `$1Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}` ); + // Increment Completed Phases counter (#956) + const completedMatch = stateContent.match(/\*\*Completed Phases:\*\*\s*(\d+)/); + if (completedMatch) { + const newCompleted = parseInt(completedMatch[1], 10) + 1; + stateContent = stateContent.replace( + /(\*\*Completed Phases:\*\*\s*)\d+/, + `$1${newCompleted}` + ); + + // Recalculate percent based on completed / total (#956) + const totalMatch = stateContent.match(/\*\*Total Phases:\*\*\s*(\d+)/); + if (totalMatch) { + const totalPhases = parseInt(totalMatch[1], 10); + if (totalPhases > 0) { + const newPercent = Math.round((newCompleted / totalPhases) * 100); + stateContent = stateContent.replace( + /(\*\*Progress:\*\*\s*)\d+%/, + `$1${newPercent}%` + ); + // Also update percent field if it exists separately + stateContent = stateContent.replace( + /(percent:\s*)\d+/, + `$1${newPercent}` + ); + } + } + } + writeStateMd(statePath, stateContent, cwd); } diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index 243103b9d..d70966162 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -499,6 +499,28 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(phase-{X}): co ``` + +**Evolve PROJECT.md to reflect phase completion (prevents planning document drift — #956):** + +PROJECT.md tracks validated requirements, decisions, and current state. Without this step, +PROJECT.md falls behind silently over multiple phases. + +1. Read `.planning/PROJECT.md` +2. If the file exists and has a `## Validated Requirements` or `## Requirements` section: + - Move any requirements validated by this phase from Active → Validated + - Add a brief note: `Validated in Phase {X}: {Name}` +3. If the file has a `## Current State` or similar section: + - Update it to reflect this phase's completion (e.g., "Phase {X} complete — {one-liner}") +4. Update the `Last updated:` footer to today's date +5. Commit the change: + +```bash +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(phase-{X}): evolve PROJECT.md after phase completion" --files .planning/PROJECT.md +``` + +**Skip this step if** `.planning/PROJECT.md` does not exist. + + **Exception:** If `gaps_found`, the `verify_phase_goal` step already presents the gap-closure path (`/gsd:plan-phase {X} --gaps`). No additional routing needed — skip auto-advance. From 849aed665469c0cca5f20654bab47b762bcb13f6 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 12:00:35 -0400 Subject: [PATCH 32/83] fix: prevent agent from suggesting non-existent /gsd:transition command (#1081) (#1129) The agent was telling users to run '/gsd:transition' after phase completion, but this command does not exist. transition.md is an internal workflow invoked by execute-phase during auto-advance. Changes: - Add banner to transition.md declaring it is NOT a user command - Add explicit warning in execute-phase completion section that /gsd:transition does not exist - Add 'only suggest commands listed above' guard to prevent hallucination - Update resume-project.md to avoid ambiguous 'Transition' label - Replace 'ready for transition' with 'ready for next step' in execute-plan.md Fixes #1081 --- .release-monitor.sh | 51 +++++++++++++++++++++++ get-shit-done/workflows/execute-phase.md | 4 ++ get-shit-done/workflows/execute-plan.md | 2 +- get-shit-done/workflows/resume-project.md | 4 +- get-shit-done/workflows/transition.md | 16 +++++++ 5 files changed, 74 insertions(+), 3 deletions(-) create mode 100755 .release-monitor.sh diff --git a/.release-monitor.sh b/.release-monitor.sh new file mode 100755 index 000000000..200383398 --- /dev/null +++ b/.release-monitor.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Release monitor for gsd-build/get-shit-done +# Checks every 15 minutes, writes new release info to a signal file + +REPO="gsd-build/get-shit-done" +SIGNAL_FILE="/tmp/gsd-new-release.json" +STATE_FILE="/tmp/gsd-monitor-last-tag" +LOG_FILE="/tmp/gsd-monitor.log" + +# Initialize with current latest +echo "v1.25.1" > "$STATE_FILE" +rm -f "$SIGNAL_FILE" + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE" + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" +} + +log "Monitor started. Watching $REPO for releases newer than v1.25.1" +log "Checking every 15 minutes..." + +while true; do + sleep 900 # 15 minutes + + LAST_KNOWN=$(cat "$STATE_FILE" 2>/dev/null) + + # Get latest release tag + LATEST=$(gh release list -R "$REPO" --limit 1 2>/dev/null | awk '{print $1}') + + if [ -z "$LATEST" ]; then + log "WARNING: Failed to fetch releases (network issue?)" + continue + fi + + if [ "$LATEST" != "$LAST_KNOWN" ]; then + log "NEW RELEASE DETECTED: $LATEST (was: $LAST_KNOWN)" + + # Fetch release notes + RELEASE_BODY=$(gh release view "$LATEST" -R "$REPO" --json tagName,name,body 2>/dev/null) + + # Write signal file for the agent to pick up + echo "$RELEASE_BODY" > "$SIGNAL_FILE" + echo "$LATEST" > "$STATE_FILE" + + log "Signal file written to $SIGNAL_FILE" + # Exit so the agent can process it, then restart + exit 0 + else + log "No new release. Latest is still $LATEST" + fi +done diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index d70966162..90891516c 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -574,6 +574,8 @@ Read and follow `~/.claude/get-shit-done/workflows/transition.md`, passing throu **STOP. Do not auto-advance. Do not execute transition. Do not plan next phase. Present options to the user and wait.** +**IMPORTANT: There is NO `/gsd:transition` command. Never suggest it. The transition workflow is internal only.** + ``` ## ✓ Phase {X}: {Name} Complete @@ -582,6 +584,8 @@ Read and follow `~/.claude/get-shit-done/workflows/transition.md`, passing throu /gsd:plan-phase {next} — plan next phase /gsd:execute-phase {next} — execute next phase ``` + +Only suggest the commands listed above. Do not invent or hallucinate command names. diff --git a/get-shit-done/workflows/execute-plan.md b/get-shit-done/workflows/execute-plan.md index 5dd6f8fc0..e64dec825 100644 --- a/get-shit-done/workflows/execute-plan.md +++ b/get-shit-done/workflows/execute-plan.md @@ -371,7 +371,7 @@ One-liner SUBSTANTIVE: "JWT auth with refresh rotation using jose library" not " Include: duration, start/end times, task count, file count. -Next: more plans → "Ready for {next-plan}" | last → "Phase complete, ready for transition". +Next: more plans → "Ready for {next-plan}" | last → "Phase complete, ready for next step". diff --git a/get-shit-done/workflows/resume-project.md b/get-shit-done/workflows/resume-project.md index 00ce54df0..7ebdc2150 100644 --- a/get-shit-done/workflows/resume-project.md +++ b/get-shit-done/workflows/resume-project.md @@ -154,7 +154,7 @@ Based on project state, determine the most logical next action: → Option: Abandon and move on **If phase in progress, all plans complete:** -→ Primary: Transition to next phase +→ Primary: Advance to next phase (via internal transition workflow) → Option: Review completed work **If phase ready to plan:** @@ -242,7 +242,7 @@ Based on user selection, route to appropriate workflow: --- ``` -- **Transition** → ./transition.md +- **Advance to next phase** → ./transition.md (internal workflow, invoked inline — NOT a user command) - **Check todos** → Read .planning/todos/pending/, present summary - **Review alignment** → Read PROJECT.md, compare to current state - **Something else** → Ask what they need diff --git a/get-shit-done/workflows/transition.md b/get-shit-done/workflows/transition.md index 5e8927dfd..d3073cb6a 100644 --- a/get-shit-done/workflows/transition.md +++ b/get-shit-done/workflows/transition.md @@ -1,3 +1,19 @@ + + +**This is an INTERNAL workflow — NOT a user-facing command.** + +There is no `/gsd:transition` command. This workflow is invoked automatically by +`execute-phase` during auto-advance, or inline by the orchestrator after phase +verification. Users should never be told to run `/gsd:transition`. + +**Valid user commands for phase progression:** +- `/gsd:discuss-phase {N}` — discuss a phase before planning +- `/gsd:plan-phase {N}` — plan a phase +- `/gsd:execute-phase {N}` — execute a phase +- `/gsd:progress` — see roadmap progress + + + **Read these files NOW:** From a97e4c2c6fc3640c02148aeb43c5ece4b8df612f Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 12:01:08 -0400 Subject: [PATCH 33/83] feat: /gsd:ship command for PR creation from verified phase work (#829) (#1123) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: /gsd:ship command for PR creation from verified phase work (#829) New command that bridges local completion → merged PR, closing the plan → execute → verify → ship loop. Workflow (workflows/ship.md): 1. Preflight: verification passed, clean tree, correct branch, gh auth 2. Push branch to remote 3. Auto-generate rich PR body from planning artifacts: - Phase goal from ROADMAP.md - Changes from SUMMARY.md files - Requirements addressed (REQ-IDs) - Verification status - Key decisions 4. Create PR via gh CLI (supports --draft) 5. Optional code review request 6. Update STATE.md with shipping status Files: - commands/gsd/ship.md: New command entry point - get-shit-done/workflows/ship.md: Full workflow implementation - get-shit-done/workflows/help.md: Add ship to help output - docs/COMMANDS.md: Command reference - docs/FEATURES.md: Feature spec with REQ-SHIP-01 through 05 - docs/USER-GUIDE.md: Add to command table - CHANGELOG.md: Document new command Fixes #829 * fix(tests): update expected skill count from 39 to 40 for new ship command The Copilot install E2E tests hardcode the expected number of skill directories and manifest entries. Adding commands/gsd/ship.md increased the count from 39 to 40. --- CHANGELOG.md | 1 + commands/gsd/ship.md | 23 ++++ docs/COMMANDS.md | 26 ++++ docs/FEATURES.md | 19 +++ docs/USER-GUIDE.md | 37 +++--- get-shit-done/workflows/help.md | 14 ++ get-shit-done/workflows/ship.md | 228 ++++++++++++++++++++++++++++++++ 7 files changed, 330 insertions(+), 18 deletions(-) create mode 100644 commands/gsd/ship.md create mode 100644 get-shit-done/workflows/ship.md diff --git a/CHANGELOG.md b/CHANGELOG.md index b6f7d2a69..1eee1e152 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Pre-wave dependency check in `execute-phase`: verifies key-links from prior wave artifacts before spawning next wave - Cross-Plan Data Contracts (Dimension 9) in plan-checker: detects incompatible transformations between plans sharing data pipelines - Export-level spot check in `verify-phase`: catches dead stores that exist in wired files but are never called +- **`/gsd:ship` command** — Native PR creation workflow that bridges local completion → merged PR. Auto-generates rich PR body from planning artifacts (SUMMARY.md, VERIFICATION.md, REQUIREMENTS.md), pushes branch, creates PR via `gh`, optionally requests review, and updates STATE.md with shipping status (#829) ### Fixed - **Requirements `mark-complete` is now idempotent** — Re-marking already-completed requirements returns `already_complete` instead of `not_found` (#948) diff --git a/commands/gsd/ship.md b/commands/gsd/ship.md new file mode 100644 index 000000000..124695553 --- /dev/null +++ b/commands/gsd/ship.md @@ -0,0 +1,23 @@ +--- +name: gsd:ship +description: Create PR, run review, and prepare for merge after verification passes +argument-hint: "[phase number or milestone, e.g., '4' or 'v1.0']" +allowed-tools: + - Read + - Bash + - Grep + - Glob + - Write + - AskUserQuestion +--- + +Bridge local completion → merged PR. After /gsd:verify-work passes, ship the work: push branch, create PR with auto-generated body, optionally trigger review, and track the merge. + +Closes the plan → execute → verify → ship loop. + + + +@~/.claude/get-shit-done/workflows/ship.md + + +Execute the ship workflow from @~/.claude/get-shit-done/workflows/ship.md end-to-end. diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 5f1c9c9fe..9b7947737 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -132,6 +132,32 @@ User acceptance testing with auto-diagnosis. --- +### `/gsd:ship` + +Create PR from completed phase work with auto-generated body. + +| Argument | Required | Description | +|----------|----------|-------------| +| `N` | No | Phase number or milestone version (e.g., `4` or `v1.0`) | +| `--draft` | No | Create as draft PR | + +**Prerequisites:** Phase verified (`/gsd:verify-work` passed), `gh` CLI installed and authenticated +**Produces:** GitHub PR with rich body from planning artifacts, STATE.md updated + +```bash +/gsd:ship 4 # Ship phase 4 +/gsd:ship 4 --draft # Ship as draft PR +``` + +**PR body includes:** +- Phase goal from ROADMAP.md +- Changes summary from SUMMARY.md files +- Requirements addressed (REQ-IDs) +- Verification status +- Key decisions + +--- + ### `/gsd:ui-review` Retroactive 6-pillar visual audit of implemented frontend. diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 27afed54e..88c543a1f 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -261,6 +261,25 @@ --- +### 6.5. Ship + +**Command:** `/gsd:ship [N] [--draft]` + +**Purpose:** Bridge local completion → merged PR. After verification passes, push branch, create PR with auto-generated body from planning artifacts, optionally trigger review, and track in STATE.md. + +**Requirements:** +- REQ-SHIP-01: System MUST verify phase has passed verification before shipping +- REQ-SHIP-02: System MUST push branch and create PR via `gh` CLI +- REQ-SHIP-03: System MUST auto-generate PR body from SUMMARY.md, VERIFICATION.md, and REQUIREMENTS.md +- REQ-SHIP-04: System MUST update STATE.md with shipping status and PR number +- REQ-SHIP-05: System MUST support `--draft` flag for draft PRs + +**Prerequisites:** Phase verified, `gh` CLI installed and authenticated, work on feature branch + +**Produces:** GitHub PR with rich body, STATE.md updated + +--- + ### 7. UI Review **Command:** `/gsd:ui-review [N]` diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index 5458cedc0..d29455860 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -284,6 +284,7 @@ Controlled by `workflow.ui_safety_gate` config toggle. | `/gsd:plan-phase [N]` | Research + plan + verify | Before executing a phase | | `/gsd:execute-phase ` | Execute all plans in parallel waves | After planning is complete | | `/gsd:verify-work [N]` | Manual UAT with auto-diagnosis | After execution completes | +| `/gsd:ship [N]` | Create PR from verified work | After verification passes | | `/gsd:ui-review [N]` | Retroactive 6-pillar visual audit | After execution or verify-work (frontend projects) | | `/gsd:audit-milestone` | Verify milestone met its definition of done | Before completing milestone | | `/gsd:complete-milestone` | Archive milestone, tag release | All phases verified | @@ -363,7 +364,7 @@ GSD stores project settings in `.planning/config.json`. Configure during `/gsd:n |---------|---------|---------|------------------| | `mode` | `interactive`, `yolo` | `interactive` | `yolo` auto-approves decisions; `interactive` confirms at each step | | `granularity` | `coarse`, `standard`, `fine` | `standard` | Phase granularity: how finely scope is sliced (3-5, 5-8, or 8-12 phases) | -| `model_profile` | `quality`, `balanced`, `budget`, `inherit` | `balanced` | Model tier for each agent (see table below) | +| `model_profile` | `quality`, `balanced`, `budget`, `inherit` | `balanced` | Model tier for each agent (see table below) | ### Planning Settings @@ -407,25 +408,25 @@ Disable these to speed up phases in familiar domains or when conserving tokens. ### Model Profiles (Per-Agent Breakdown) -| Agent | `quality` | `balanced` | `budget` | `inherit` | -|-------|-----------|------------|----------|-----------| -| gsd-planner | Opus | Opus | Sonnet | Inherit | -| gsd-roadmapper | Opus | Sonnet | Sonnet | Inherit | -| gsd-executor | Opus | Sonnet | Sonnet | Inherit | -| gsd-phase-researcher | Opus | Sonnet | Haiku | Inherit | -| gsd-project-researcher | Opus | Sonnet | Haiku | Inherit | -| gsd-research-synthesizer | Sonnet | Sonnet | Haiku | Inherit | -| gsd-debugger | Opus | Sonnet | Sonnet | Inherit | -| gsd-codebase-mapper | Sonnet | Haiku | Haiku | Inherit | -| gsd-verifier | Sonnet | Sonnet | Haiku | Inherit | -| gsd-plan-checker | Sonnet | Sonnet | Haiku | Inherit | -| gsd-integration-checker | Sonnet | Sonnet | Haiku | Inherit | +| Agent | `quality` | `balanced` | `budget` | `inherit` | +|-------|-----------|------------|----------|-----------| +| gsd-planner | Opus | Opus | Sonnet | Inherit | +| gsd-roadmapper | Opus | Sonnet | Sonnet | Inherit | +| gsd-executor | Opus | Sonnet | Sonnet | Inherit | +| gsd-phase-researcher | Opus | Sonnet | Haiku | Inherit | +| gsd-project-researcher | Opus | Sonnet | Haiku | Inherit | +| gsd-research-synthesizer | Sonnet | Sonnet | Haiku | Inherit | +| gsd-debugger | Opus | Sonnet | Sonnet | Inherit | +| gsd-codebase-mapper | Sonnet | Haiku | Haiku | Inherit | +| gsd-verifier | Sonnet | Sonnet | Haiku | Inherit | +| gsd-plan-checker | Sonnet | Sonnet | Haiku | Inherit | +| gsd-integration-checker | Sonnet | Sonnet | Haiku | Inherit | **Profile philosophy:** -- **quality** -- Opus for all decision-making agents, Sonnet for read-only verification. Use when quota is available and the work is critical. -- **balanced** -- Opus only for planning (where architecture decisions happen), Sonnet for everything else. The default for good reason. -- **budget** -- Sonnet for anything that writes code, Haiku for research and verification. Use for high-volume work or less critical phases. -- **inherit** -- All agents use the current session model. Best when switching models dynamically (for example OpenCode `/model`). +- **quality** -- Opus for all decision-making agents, Sonnet for read-only verification. Use when quota is available and the work is critical. +- **balanced** -- Opus only for planning (where architecture decisions happen), Sonnet for everything else. The default for good reason. +- **budget** -- Sonnet for anything that writes code, Haiku for research and verification. Use for high-volume work or less critical phases. +- **inherit** -- All agents use the current session model. Best when switching models dynamically (for example OpenCode `/model`). --- diff --git a/get-shit-done/workflows/help.md b/get-shit-done/workflows/help.md index 058d4a810..e32ea0d25 100644 --- a/get-shit-done/workflows/help.md +++ b/get-shit-done/workflows/help.md @@ -308,6 +308,20 @@ Validate built features through conversational UAT. Usage: `/gsd:verify-work 3` +### Ship Work + +**`/gsd:ship [phase]`** +Create a PR from completed phase work with an auto-generated body. + +- Pushes branch to remote +- Creates PR with summary from SUMMARY.md, VERIFICATION.md, REQUIREMENTS.md +- Optionally requests code review +- Updates STATE.md with shipping status + +Prerequisites: Phase verified, `gh` CLI installed and authenticated. + +Usage: `/gsd:ship 4` or `/gsd:ship 4 --draft` + ### Milestone Auditing **`/gsd:audit-milestone [version]`** diff --git a/get-shit-done/workflows/ship.md b/get-shit-done/workflows/ship.md new file mode 100644 index 000000000..3c29de1ff --- /dev/null +++ b/get-shit-done/workflows/ship.md @@ -0,0 +1,228 @@ + +Create a pull request from completed phase/milestone work, generate a rich PR body from planning artifacts, optionally run code review, and prepare for merge. Closes the plan → execute → verify → ship loop. + + + +Read all files referenced by the invoking prompt's execution_context before starting. + + + + + +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`, `commit_docs`. + +Also load config for branching strategy: +```bash +CONFIG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state load) +``` + +Extract: `branching_strategy`, `branch_name`. + + + +Verify the work is ready to ship: + +1. **Verification passed?** + ```bash + VERIFICATION=$(cat ${PHASE_DIR}/*-VERIFICATION.md 2>/dev/null) + ``` + Check for `status: passed` or `status: human_needed` (with human approval). + If no VERIFICATION.md or status is `gaps_found`: warn and ask user to confirm. + +2. **Clean working tree?** + ```bash + git status --short + ``` + If uncommitted changes exist: ask user to commit or stash first. + +3. **On correct branch?** + ```bash + CURRENT_BRANCH=$(git branch --show-current) + ``` + If on `main`/`master`: warn — should be on a feature branch. + If branching_strategy is `none`: offer to create a branch now. + +4. **Remote configured?** + ```bash + git remote -v | head -2 + ``` + Detect `origin` remote. If no remote: error — can't create PR. + +5. **`gh` CLI available?** + ```bash + which gh && gh auth status 2>&1 + ``` + If `gh` not found or not authenticated: provide setup instructions and exit. + + + +Push the current branch to remote: + +```bash +git push origin ${CURRENT_BRANCH} 2>&1 +``` + +If push fails (e.g., no upstream): set upstream: +```bash +git push --set-upstream origin ${CURRENT_BRANCH} 2>&1 +``` + +Report: "Pushed `{branch}` to origin ({commit_count} commits ahead of main)" + + + +Auto-generate a rich PR body from planning artifacts: + +**1. Title:** +``` +Phase {phase_number}: {phase_name} +``` +Or for milestone: `Milestone {version}: {name}` + +**2. Summary section:** +Read ROADMAP.md for phase goal. Read VERIFICATION.md for verification status. + +```markdown +## Summary + +**Phase {N}: {Name}** +**Goal:** {goal from ROADMAP.md} +**Status:** Verified ✓ + +{One paragraph synthesized from SUMMARY.md files — what was built} +``` + +**3. Changes section:** +For each SUMMARY.md in the phase directory: +```markdown +## Changes + +### Plan {plan_id}: {plan_name} +{one_liner from SUMMARY.md frontmatter} + +**Key files:** +{key-files.created and key-files.modified from SUMMARY.md frontmatter} +``` + +**4. Requirements section:** +```markdown +## Requirements Addressed + +{REQ-IDs from plan frontmatter, linked to REQUIREMENTS.md descriptions} +``` + +**5. Testing section:** +```markdown +## Verification + +- [x] Automated verification: {pass/fail from VERIFICATION.md} +- {human verification items from VERIFICATION.md, if any} +``` + +**6. Decisions section:** +```markdown +## Key Decisions + +{Decisions from STATE.md accumulated context relevant to this phase} +``` + + + +Create the PR using the generated body: + +```bash +gh pr create \ + --title "Phase ${PHASE_NUMBER}: ${PHASE_NAME}" \ + --body "${PR_BODY}" \ + --base main +``` + +If `--draft` flag was passed: add `--draft`. + +Report: "PR #{number} created: {url}" + + + +Ask if user wants to trigger a code review: + +``` +AskUserQuestion: + question: "PR created. Run a code review before merge?" + options: + - label: "Skip review" + description: "PR is ready — merge when CI passes" + - label: "Self-review" + description: "I'll review the diff in the PR myself" + - label: "Request review" + description: "Request review from a teammate" +``` + +**If "Request review":** +```bash +gh pr edit ${PR_NUMBER} --add-reviewer "${REVIEWER}" +``` + +**If "Self-review":** +Report the PR URL and suggest: "Review the diff at {url}/files" + + + +Update STATE.md to reflect the shipping action: + +```bash +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state update "Last Activity" "$(date +%Y-%m-%d)" +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state update "Status" "Phase ${PHASE_NUMBER} shipped — PR #${PR_NUMBER}" +``` + +If `commit_docs` is true: +```bash +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(${padded_phase}): ship phase ${PHASE_NUMBER} — PR #${PR_NUMBER}" --files .planning/STATE.md +``` + + + +``` +─────────────────────────────────────────────────────────────── + +## ✓ Phase {X}: {Name} — Shipped + +PR: #{number} ({url}) +Branch: {branch} → main +Commits: {count} +Verification: ✓ Passed +Requirements: {N} REQ-IDs addressed + +Next steps: +- Review/approve PR +- Merge when CI passes +- /gsd:complete-milestone (if last phase in milestone) +- /gsd:progress (to see what's next) + +─────────────────────────────────────────────────────────────── +``` + + + + + +After shipping: + +- /gsd:complete-milestone — if all phases in milestone are done +- /gsd:progress — see overall project state +- /gsd:execute-phase {next} — continue to next phase + + + +- [ ] Preflight checks passed (verification, clean tree, branch, remote, gh) +- [ ] Branch pushed to remote +- [ ] PR created with rich auto-generated body +- [ ] STATE.md updated with shipping status +- [ ] User knows PR number and next steps + From f54f3df77685f9acc10bbfa56e6d5cdbfce6d7cb Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 12:01:25 -0400 Subject: [PATCH 34/83] feat: structured session handoff artifact for cross-session continuity (#940) (#1122) Enhance /gsd:pause-work to write .planning/HANDOFF.json alongside .continue-here.md. The JSON provides machine-readable state that /gsd:resume-work can parse for precise resumption. HANDOFF.json includes: - Task position (phase, plan, task number, status) - Completed and remaining tasks with commit hashes - Blockers with type classification (technical/human_action/external) - Human actions pending (API keys, approvals, manual testing) - Uncommitted files list - Context notes for mental model restoration Resume-work changes: - HANDOFF.json is primary resumption source (highest priority) - Surfaces blockers and human actions immediately on session start - Validates uncommitted files against git status - Deletes HANDOFF.json after successful resumption - Falls back to .continue-here.md if no JSON exists Also checks for placeholder content in SUMMARY.md files to catch false completions (frontmatter claims complete but body has TBD). Fixes #940 --- CHANGELOG.md | 3 ++ docs/FEATURES.md | 6 ++- docs/USER-GUIDE.md | 2 +- get-shit-done/workflows/pause-work.md | 64 +++++++++++++++++++++-- get-shit-done/workflows/resume-project.md | 22 +++++++- 5 files changed, 87 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1eee1e152..6f1947b3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed - **Requirements `mark-complete` is now idempotent** — Re-marking already-completed requirements returns `already_complete` instead of `not_found` (#948) +### Improved +- **Structured session handoff** — `/gsd:pause-work` now writes `.planning/HANDOFF.json` alongside `.continue-here.md`. JSON provides machine-readable state (task position, blockers, human actions pending, uncommitted files) that `/gsd:resume-work` parses for precise resumption instead of generic "what do you want to do?" (#940) + ## [1.25.0] - 2026-03-16 ### Added diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 88c543a1f..b706e257d 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -513,11 +513,13 @@ **Purpose:** Maintain project continuity across context resets and sessions. **Requirements:** -- REQ-SESSION-01: Pause MUST save current position and next steps to `continue-here.md` -- REQ-SESSION-02: Resume MUST restore full project context from state files +- REQ-SESSION-01: Pause MUST save current position and next steps to `continue-here.md` and structured `HANDOFF.json` +- REQ-SESSION-02: Resume MUST restore full project context from HANDOFF.json (preferred) or state files (fallback) - REQ-SESSION-03: Progress MUST show current position, next action, and overall completion - REQ-SESSION-04: Progress MUST read all state files (STATE.md, ROADMAP.md, phase directories) - REQ-SESSION-05: All session operations MUST work after `/clear` (context reset) +- REQ-SESSION-06: HANDOFF.json MUST include blockers, human actions pending, and in-progress task state +- REQ-SESSION-07: Resume MUST surface human actions and blockers immediately on session start --- diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index d29455860..4dcfca52b 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -296,7 +296,7 @@ Controlled by `workflow.ui_safety_gate` config toggle. |---------|---------|-------------| | `/gsd:progress` | Show status and next steps | Anytime -- "where am I?" | | `/gsd:resume-work` | Restore full context from last session | Starting a new session | -| `/gsd:pause-work` | Save context handoff | Stopping mid-phase | +| `/gsd:pause-work` | Save structured handoff (HANDOFF.json + continue-here.md) | Stopping mid-phase | | `/gsd:help` | Show all commands | Quick reference | | `/gsd:update` | Update GSD with changelog preview | Check for new versions | | `/gsd:join-discord` | Open Discord community invite | Questions or community | diff --git a/get-shit-done/workflows/pause-work.md b/get-shit-done/workflows/pause-work.md index f723ef81a..ccdba267d 100644 --- a/get-shit-done/workflows/pause-work.md +++ b/get-shit-done/workflows/pause-work.md @@ -1,5 +1,5 @@ -Create `.continue-here.md` handoff file to preserve complete work state across sessions. Enables seamless resumption with full context restoration. +Create structured `.planning/HANDOFF.json` and `.continue-here.md` handoff files to preserve complete work state across sessions. The JSON provides machine-readable state for `/gsd:resume-work`; the markdown provides human-readable context. @@ -27,10 +27,61 @@ If no active phase detected, ask user which phase they're pausing work on. 3. **Work remaining**: What's left in current plan/phase 4. **Decisions made**: Key decisions and rationale 5. **Blockers/issues**: Anything stuck -6. **Mental context**: The approach, next steps, "vibe" -7. **Files modified**: What's changed but not committed +6. **Human actions pending**: Things that need manual intervention (MCP setup, API keys, approvals, manual testing) +7. **Background processes**: Any running servers/watchers that were part of the workflow +8. **Files modified**: What's changed but not committed Ask user for clarifications if needed via conversational questions. + +**Also inspect SUMMARY.md files for false completions:** +```bash +# Check for placeholder content in existing summaries +grep -l "To be filled\|placeholder\|TBD" .planning/phases/*/*.md 2>/dev/null +``` +Report any summaries with placeholder content as incomplete items. + + + +**Write structured handoff to `.planning/HANDOFF.json`:** + +```bash +timestamp=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" current-timestamp full --raw) +``` + +```json +{ + "version": "1.0", + "timestamp": "{timestamp}", + "phase": "{phase_number}", + "phase_name": "{phase_name}", + "phase_dir": "{phase_dir}", + "plan": {current_plan_number}, + "task": {current_task_number}, + "total_tasks": {total_task_count}, + "status": "paused", + "completed_tasks": [ + {"id": 1, "name": "{task_name}", "status": "done", "commit": "{short_hash}"}, + {"id": 2, "name": "{task_name}", "status": "done", "commit": "{short_hash}"}, + {"id": 3, "name": "{task_name}", "status": "in_progress", "progress": "{what_done}"} + ], + "remaining_tasks": [ + {"id": 4, "name": "{task_name}", "status": "not_started"}, + {"id": 5, "name": "{task_name}", "status": "not_started"} + ], + "blockers": [ + {"description": "{blocker}", "type": "technical|human_action|external", "workaround": "{if any}"} + ], + "human_actions_pending": [ + {"action": "{what needs to be done}", "context": "{why}", "blocking": true} + ], + "decisions": [ + {"decision": "{what}", "rationale": "{why}", "phase": "{phase_number}"} + ], + "uncommitted_files": [], + "next_action": "{specific first action when resuming}", + "context_notes": "{mental state, approach, what you were thinking}" +} +``` @@ -92,19 +143,22 @@ timestamp=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" current-timesta ```bash -node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "wip: [phase-name] paused at task [X]/[Y]" --files .planning/phases/*/.continue-here.md +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "wip: [phase-name] paused at task [X]/[Y]" --files .planning/phases/*/.continue-here.md .planning/HANDOFF.json ``` ``` -✓ Handoff created: .planning/phases/[XX-name]/.continue-here.md +✓ Handoff created: + - .planning/HANDOFF.json (structured, machine-readable) + - .planning/phases/[XX-name]/.continue-here.md (human-readable) Current state: - Phase: [XX-name] - Task: [X] of [Y] - Status: [in_progress/blocked] +- Blockers: [count] ({human_actions_pending count} need human action) - Committed as WIP To resume: /gsd:resume-work diff --git a/get-shit-done/workflows/resume-project.md b/get-shit-done/workflows/resume-project.md index 7ebdc2150..a8dafcf2c 100644 --- a/get-shit-done/workflows/resume-project.md +++ b/get-shit-done/workflows/resume-project.md @@ -63,6 +63,9 @@ cat .planning/PROJECT.md Look for incomplete work that needs attention: ```bash +# Check for structured handoff (preferred — machine-readable) +cat .planning/HANDOFF.json 2>/dev/null + # Check for continue-here files (mid-plan resumption) ls .planning/phases/*/.continue-here*.md 2>/dev/null @@ -78,7 +81,18 @@ if [ "$has_interrupted_agent" = "true" ]; then fi ``` -**If .continue-here file exists:** +**If HANDOFF.json exists:** + +- This is the primary resumption source — structured data from `/gsd:pause-work` +- Parse `status`, `phase`, `plan`, `task`, `total_tasks`, `next_action` +- Check `blockers` and `human_actions_pending` — surface these immediately +- Check `completed_tasks` for `in_progress` items — these need attention first +- Validate `uncommitted_files` against `git status` — flag divergence +- Use `context_notes` to restore mental model +- Flag: "Found structured handoff — resuming from task {task}/{total_tasks}" +- **After successful resumption, delete HANDOFF.json** (it's a one-shot artifact) + +**If .continue-here file exists (fallback):** - This is a mid-plan resumption point - Read the file for specific resumption context @@ -145,8 +159,12 @@ Based on project state, determine the most logical next action: → Primary: Resume interrupted agent (Task tool with resume parameter) → Option: Start fresh (abandon agent work) +**If HANDOFF.json exists:** +→ Primary: Resume from structured handoff (highest priority — specific task/blocker context) +→ Option: Discard handoff and reassess from files + **If .continue-here file exists:** -→ Primary: Resume from checkpoint +→ Fallback: Resume from checkpoint → Option: Start fresh on current plan **If incomplete plan (PLAN without SUMMARY):** From 0f095ac3d0389925787124c12033bdfc3c9b7fca Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 12:02:02 -0400 Subject: [PATCH 35/83] feat: requirements coverage gate in plan-phase pipeline (#984) (#1121) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add step 13 (Requirements Coverage Gate) to plan-phase workflow. After plans pass the checker, verifies all phase requirements are covered by at least one plan before declaring planning complete. - Extracts REQ-IDs from plan frontmatter and compares against phase_req_ids from ROADMAP - Cross-checks CONTEXT.md features against plan objectives to detect silently dropped scope - Reports gaps with options: re-plan, defer, or proceed - Skips when phase_req_ids is null/TBD (no requirements mapped) Fixes #984 Co-authored-by: TÂCHES --- CHANGELOG.md | 1 + docs/FEATURES.md | 1 + get-shit-done/workflows/plan-phase.md | 55 ++++++++++++++++++++++++++- 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f1947b3f..0bc4b9986 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Pre-wave dependency check in `execute-phase`: verifies key-links from prior wave artifacts before spawning next wave - Cross-Plan Data Contracts (Dimension 9) in plan-checker: detects incompatible transformations between plans sharing data pipelines - Export-level spot check in `verify-phase`: catches dead stores that exist in wired files but are never called +- **Requirements coverage gate in plan-phase** — New step 13 verifies all phase requirements are covered by at least one plan before planning completes. Cross-checks REQ-IDs from ROADMAP against plan frontmatter and CONTEXT.md features against plan objectives (#984) - **`/gsd:ship` command** — Native PR creation workflow that bridges local completion → merged PR. Auto-generates rich PR body from planning artifacts (SUMMARY.md, VERIFICATION.md, REQUIREMENTS.md), pushes branch, creates PR via `gh`, optionally requests review, and updates STATE.md with shipping status (#829) ### Fixed diff --git a/docs/FEATURES.md b/docs/FEATURES.md index b706e257d..7ca07e297 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -171,6 +171,7 @@ - REQ-PLAN-06: System MUST support `--skip-research` flag to bypass research phase - REQ-PLAN-07: System MUST prompt user to run `/gsd:ui-phase` if frontend phase detected and no UI-SPEC.md exists (UI safety gate) - REQ-PLAN-08: System MUST include Nyquist validation mapping when `workflow.nyquist_validation` is enabled +- REQ-PLAN-09: System MUST verify all phase requirements are covered by at least one plan before planning completes (requirements coverage gate) **Produces:** | Artifact | Description | diff --git a/get-shit-done/workflows/plan-phase.md b/get-shit-done/workflows/plan-phase.md index 62815be58..2d3291533 100644 --- a/get-shit-done/workflows/plan-phase.md +++ b/get-shit-done/workflows/plan-phase.md @@ -587,11 +587,62 @@ Display: `Max iterations reached. {N} issues remain:` + issue list Offer: 1) Force proceed, 2) Provide guidance and retry, 3) Abandon -## 13. Present Final Status +## 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. + +**Skip if:** `phase_req_ids` is null or TBD (no requirements mapped to this phase). + +**Step 1: Extract requirement IDs claimed by plans** +```bash +# Collect all requirement IDs from plan frontmatter +PLAN_REQS=$(grep -h "requirements_addressed\|requirements:" ${PHASE_DIR}/*-PLAN.md 2>/dev/null | tr -d '[]' | tr ',' '\n' | sed 's/^[[:space:]]*//' | sort -u) +``` + +**Step 2: Compare against phase requirements from ROADMAP** + +For each REQ-ID in `phase_req_ids`: +- If REQ-ID appears in `PLAN_REQS` → covered ✓ +- If REQ-ID does NOT appear in any plan → uncovered ✗ + +**Step 3: Check CONTEXT.md features against plan objectives** + +Read CONTEXT.md `` section. Extract feature/capability names. Check each against plan `` blocks. Features not mentioned in any plan objective → potentially dropped. + +**Step 4: Report** + +If all requirements covered and no dropped features: +``` +✓ Requirements coverage: {N}/{N} REQ-IDs covered by plans +``` +→ Proceed to step 14. + +If gaps found: +``` +## ⚠ Requirements Coverage Gap + +{M} of {N} phase requirements are not assigned to any plan: + +| REQ-ID | Description | Plans | +|--------|-------------|-------| +| {id} | {from REQUIREMENTS.md} | None | + +{K} CONTEXT.md features not found in plan objectives: +- {feature_name} — described in CONTEXT.md but no plan covers it + +Options: +1. Re-plan to include missing requirements (recommended) +2. Move uncovered requirements to next phase +3. Proceed anyway — accept coverage gaps +``` + +Use AskUserQuestion to present the options. + +## 14. Present Final Status Route to `` OR `auto_advance` depending on flags/config. -## 14. Auto-Advance Check +## 15. Auto-Advance Check Check for auto-advance trigger: From a75c1d1f671af2732990bf040735b30426de4236 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 12:02:34 -0400 Subject: [PATCH 36/83] feat: cross-phase regression gate in execute-phase pipeline (#945) (#1120) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add regression_gate step between executor completion and verification in execute-phase workflow. Runs prior phases' test suites to catch cross-phase regressions before they compound. - Discovers prior VERIFICATION.md files and extracts test file paths - Detects project test runner (jest/vitest/cargo/pytest) - Reports pass/fail with options to fix, continue, or abort - Skips silently for first phase or when no prior tests exist Changes: - execute-phase.md: New regression_gate step - CHANGELOG.md: Document regression gate feature - docs/FEATURES.md: Add REQ-EXEC-09 Fixes #945 Co-authored-by: TÂCHES --- CHANGELOG.md | 1 + docs/FEATURES.md | 1 + get-shit-done/workflows/execute-phase.md | 61 ++++++++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bc4b9986..cb350eb5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Pre-wave dependency check in `execute-phase`: verifies key-links from prior wave artifacts before spawning next wave - Cross-Plan Data Contracts (Dimension 9) in plan-checker: detects incompatible transformations between plans sharing data pipelines - Export-level spot check in `verify-phase`: catches dead stores that exist in wired files but are never called +- **Cross-phase regression gate** — New `regression_gate` step in `execute-phase` runs prior phases' test suites after execution completes but before verification, catching regressions before they compound (#945) - **Requirements coverage gate in plan-phase** — New step 13 verifies all phase requirements are covered by at least one plan before planning completes. Cross-checks REQ-IDs from ROADMAP against plan frontmatter and CONTEXT.md features against plan objectives (#984) - **`/gsd:ship` command** — Native PR creation workflow that bridges local completion → merged PR. Auto-generates rich PR body from planning artifacts (SUMMARY.md, VERIFICATION.md, REQUIREMENTS.md), pushes branch, creates PR via `gh`, optionally requests review, and updates STATE.md with shipping status (#829) diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 7ca07e297..79e4a83d9 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -221,6 +221,7 @@ - REQ-EXEC-06: System MUST run post-execution verifier to check phase goals were met - REQ-EXEC-07: System MUST support git branching strategies (`none`, `phase`, `milestone`) - REQ-EXEC-08: System MUST invoke node repair operator on task verification failure (when enabled) +- REQ-EXEC-09: System MUST run prior phases' test suites before verification to catch cross-phase regressions **Produces:** | Artifact | Description | diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index 90891516c..e5c6afe4d 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -415,6 +415,67 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(phase-${PARENT ``` + +Run prior phases' test suites to catch cross-phase regressions BEFORE verification. + +**Skip if:** This is the first phase (no prior phases), or no prior VERIFICATION.md files exist. + +**Step 1: Discover prior phases' test files** +```bash +# Find all VERIFICATION.md files from prior phases in current milestone +PRIOR_VERIFICATIONS=$(find .planning/phases/ -name "*-VERIFICATION.md" ! -path "*${PHASE_NUMBER}*" 2>/dev/null) +``` + +**Step 2: Extract test file lists from prior verifications** + +For each VERIFICATION.md found, look for test file references: +- Lines containing `test`, `spec`, or `__tests__` paths +- The "Test Suite" or "Automated Checks" section +- File patterns from `key-files.created` in corresponding SUMMARY.md files that match `*.test.*` or `*.spec.*` + +Collect all unique test file paths into `REGRESSION_FILES`. + +**Step 3: Run regression tests (if any found)** + +```bash +# Detect test runner and run prior phase tests +if [ -f "package.json" ]; then + # Node.js — use project's test runner + npx jest ${REGRESSION_FILES} --passWithNoTests --no-coverage -q 2>&1 || npx vitest run ${REGRESSION_FILES} 2>&1 +elif [ -f "Cargo.toml" ]; then + cargo test 2>&1 +elif [ -f "requirements.txt" ] || [ -f "pyproject.toml" ]; then + python -m pytest ${REGRESSION_FILES} -q --tb=short 2>&1 +fi +``` + +**Step 4: Report results** + +If all tests pass: +``` +✓ Regression gate: {N} prior-phase test files passed — no regressions detected +``` +→ Proceed to verify_phase_goal + +If any tests fail: +``` +## ⚠ Cross-Phase Regression Detected + +Phase {X} execution may have broken functionality from prior phases. + +| Test File | Phase | Status | Detail | +|-----------|-------|--------|--------| +| {file} | {origin_phase} | FAILED | {first_failure_line} | + +Options: +1. Fix regressions before verification (recommended) +2. Continue to verification anyway (regressions will compound) +3. Abort phase — roll back and re-plan +``` + +Use AskUserQuestion to present the options. + + Verify phase achieved its GOAL, not just completed tasks. From 9bf78719b66b6868eb1a26d814160c58b350a3b0 Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Wed, 18 Mar 2026 10:08:49 -0600 Subject: [PATCH 37/83] docs: update changelog for v1.26.0 Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 50 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb350eb5a..43618437d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,21 +6,44 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +## [1.26.0] - 2026-03-18 + ### Added -- **`/gsd:profile-user` command** — Developer behavioral profiling from session analysis across 8 dimensions (communication, decisions, debugging, UX, vendor choices, frustrations, learning style, explanation depth). Generates `USER-PROFILE.md`, `/gsd:dev-preferences`, and `CLAUDE.md` profile section for personalized responses. Includes `--questionnaire` fallback and `--refresh` for re-analysis -- **Execution hardening** — Three quality improvements to the execution pipeline: - - Pre-wave dependency check in `execute-phase`: verifies key-links from prior wave artifacts before spawning next wave - - Cross-Plan Data Contracts (Dimension 9) in plan-checker: detects incompatible transformations between plans sharing data pipelines - - Export-level spot check in `verify-phase`: catches dead stores that exist in wired files but are never called -- **Cross-phase regression gate** — New `regression_gate` step in `execute-phase` runs prior phases' test suites after execution completes but before verification, catching regressions before they compound (#945) -- **Requirements coverage gate in plan-phase** — New step 13 verifies all phase requirements are covered by at least one plan before planning completes. Cross-checks REQ-IDs from ROADMAP against plan frontmatter and CONTEXT.md features against plan objectives (#984) -- **`/gsd:ship` command** — Native PR creation workflow that bridges local completion → merged PR. Auto-generates rich PR body from planning artifacts (SUMMARY.md, VERIFICATION.md, REQUIREMENTS.md), pushes branch, creates PR via `gh`, optionally requests review, and updates STATE.md with shipping status (#829) +- **Developer profiling pipeline** — `/gsd:profile-user` analyzes Claude Code session history to build behavioral profiles across 8 dimensions (communication, decisions, debugging, UX, vendor choices, frustrations, learning style, explanation depth). Generates `USER-PROFILE.md`, `/gsd:dev-preferences`, and `CLAUDE.md` profile section. Includes `--questionnaire` fallback and `--refresh` for re-analysis (#1084) +- **`/gsd:ship` command** — PR creation from verified phase work. Auto-generates rich PR body from planning artifacts, pushes branch, creates PR via `gh`, and updates STATE.md (#829) +- **`/gsd:next` command** — Automatic workflow advancement to the next logical step (#927) +- **Cross-phase regression gate** — Execute-phase runs prior phases' test suites after execution, catching regressions before they compound (#945) +- **Requirements coverage gate** — Plan-phase verifies all phase requirements are covered by at least one plan before proceeding (#984) +- **Structured session handoff artifact** — `/gsd:pause-work` writes `.planning/HANDOFF.json` for machine-readable cross-session continuity (#940) +- **WAITING.json signal file** — Machine-readable signal for decision points requiring user input (#1034) +- **Interactive executor mode** — Pair-programming style execution with step-by-step user involvement (#963) +- **MCP tool awareness** — GSD subagents can discover and use MCP server tools (#973) +- **Codex hooks support** — SessionStart hook support for Codex runtime (#1020) +- **Model alias-to-full-ID resolution** — Task API compatibility for model alias strings (#991) +- **Execution hardening** — Pre-wave dependency checks, cross-plan data contracts, and export-level spot checks (#1082) +- **Markdown normalization** — Generated markdown conforms to markdownlint standards (#1112) + +### Changed +- Test suite consolidated: runtime converters deduplicated, helpers standardized (#1169) +- Added test coverage for model-profiles, templates, profile-pipeline, profile-output (#1170) +- Documented `inherit` profile for non-Anthropic providers (#1036) ### Fixed -- **Requirements `mark-complete` is now idempotent** — Re-marking already-completed requirements returns `already_complete` instead of `not_found` (#948) - -### Improved -- **Structured session handoff** — `/gsd:pause-work` now writes `.planning/HANDOFF.json` alongside `.continue-here.md`. JSON provides machine-readable state (task position, blockers, human actions pending, uncommitted files) that `/gsd:resume-work` parses for precise resumption instead of generic "what do you want to do?" (#940) +- Agent suggests non-existent `/gsd:transition` — replaced with real commands (#1081, #1100) +- PROJECT.md drift and phase completion counter accuracy (#956) +- Copilot executor stuck issue — runtime compatibility fallback added (#1128) +- Explicit agent type listings prevent fallback after `/clear` (#949) +- Nested Skill calls breaking AskUserQuestion (#1009) +- Negative-heuristic `stripShippedMilestones` replaced with positive milestone lookup (#1145) +- Hook version tracking, stale hook detection, stdin timeout, session-report command (#1153, #1157, #1161, #1162) +- Hook build script syntax validation (#1165) +- Verification examples use `fetch()` instead of `curl` for Windows compatibility (#899) +- Sequential fallback for `map-codebase` on runtimes without Task tool (#1174) +- Zsh word-splitting fix for RUNTIME_DIRS arrays (#1173) +- CRLF frontmatter parsing, duplicate cwd crash, STATE.md phase transitions (#1105) +- Requirements `mark-complete` made idempotent (#948) +- Profile template paths, field names, and evidence key corrections (#1095) +- Duplicate variable declaration removed (#1101) ## [1.25.0] - 2026-03-16 @@ -1542,7 +1565,8 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - YOLO mode for autonomous execution - Interactive mode with checkpoints -[Unreleased]: https://github.com/glittercowboy/get-shit-done/compare/v1.25.0...HEAD +[Unreleased]: https://github.com/glittercowboy/get-shit-done/compare/v1.26.0...HEAD +[1.26.0]: https://github.com/glittercowboy/get-shit-done/releases/tag/v1.26.0 [1.25.0]: https://github.com/glittercowboy/get-shit-done/releases/tag/v1.25.0 [1.24.0]: https://github.com/glittercowboy/get-shit-done/releases/tag/v1.24.0 [1.23.0]: https://github.com/glittercowboy/get-shit-done/releases/tag/v1.23.0 From 641a4fc15af147c1ebdbfd2f8a8b382a072855c0 Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Wed, 18 Mar 2026 10:08:52 -0600 Subject: [PATCH 38/83] 1.26.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3ffa43783..2e1af42c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "get-shit-done-cc", - "version": "1.25.1", + "version": "1.26.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "get-shit-done-cc", - "version": "1.25.1", + "version": "1.26.0", "license": "MIT", "bin": { "get-shit-done-cc": "bin/install.js" diff --git a/package.json b/package.json index a03f6c366..5a11cfb12 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "get-shit-done-cc", - "version": "1.25.1", + "version": "1.26.0", "description": "A meta-prompting, context engineering and spec-driven development system for Claude Code, OpenCode, Gemini and Codex by TÂCHES.", "bin": { "get-shit-done-cc": "bin/install.js" From fc468adb4260dc76dc308e2aba969ff42f09c702 Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Wed, 18 Mar 2026 10:16:26 -0600 Subject: [PATCH 39/83] fix(tests): update copilot skill count assertions for ship and next commands Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/copilot-install.test.cjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/copilot-install.test.cjs b/tests/copilot-install.test.cjs index 88404019e..ea79e411d 100644 --- a/tests/copilot-install.test.cjs +++ b/tests/copilot-install.test.cjs @@ -625,7 +625,7 @@ describe('copyCommandsAsCopilotSkills', () => { // Count gsd-* directories — should be 31 const dirs = fs.readdirSync(tempDir, { withFileTypes: true }) .filter(e => e.isDirectory() && e.name.startsWith('gsd-')); - assert.strictEqual(dirs.length, 40, `expected 40 skill folders, got ${dirs.length}`); + assert.strictEqual(dirs.length, 42, `expected 42 skill folders, got ${dirs.length}`); } finally { fs.rmSync(tempDir, { recursive: true }); } @@ -1119,7 +1119,7 @@ const { execFileSync } = require('child_process'); const crypto = require('crypto'); const INSTALL_PATH = path.join(__dirname, '..', 'bin', 'install.js'); -const EXPECTED_SKILLS = 40; +const EXPECTED_SKILLS = 42; const EXPECTED_AGENTS = 16; function runCopilotInstall(cwd) { From 0f112abf55e71f0201d713361a24e9763694343e Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 17 Mar 2026 20:31:17 -0400 Subject: [PATCH 40/83] fix: remove model:inherit from OpenCode agent conversion (#1156) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenCode does not recognize 'model: inherit' as a valid model identifier — it throws ProviderModelNotFoundError when spawning any GSD subagent. Changes: - Remove 'model: inherit' injection for OpenCode agents in install.js - Strip 'model:' field entirely during OpenCode frontmatter conversion (OpenCode uses its configured default model when no model is specified) - Update tests to verify model: inherit is NOT added This fixes all 15 GSD agent definitions and the gsd-set-profile command for OpenCode users. Fixes #1156 --- bin/install.js | 11 ++++++++++- tests/runtime-converters.test.cjs | 6 ++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/bin/install.js b/bin/install.js index c79642a02..819e531e5 100755 --- a/bin/install.js +++ b/bin/install.js @@ -1228,6 +1228,13 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) { continue; } + // Strip model: field — OpenCode doesn't support Claude Code model aliases + // like 'haiku', 'sonnet', 'opus', or 'inherit'. Omitting lets OpenCode use + // its configured default model. See #1156. + if (trimmed.startsWith('model:')) { + continue; + } + // Convert color names to hex for opencode (commands only; agents strip color above) if (trimmed.startsWith('color:')) { const colorValue = trimmed.substring(6).trim().toLowerCase(); @@ -1264,8 +1271,10 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) { } // For agents: add required OpenCode agent fields + // Note: Do NOT add 'model: inherit' — OpenCode does not recognize the 'inherit' + // keyword and throws ProviderModelNotFoundError. Omitting model: lets OpenCode + // use its default model for subagents. See #1156. if (isAgent) { - newLines.push('model: inherit'); newLines.push('mode: subagent'); } diff --git a/tests/runtime-converters.test.cjs b/tests/runtime-converters.test.cjs index 6491d9376..5da337310 100644 --- a/tests/runtime-converters.test.cjs +++ b/tests/runtime-converters.test.cjs @@ -5,6 +5,8 @@ * Larger runtime test suites (Copilot, Codex, Antigravity) have their own files. * * OpenCode: convertClaudeToOpencodeFrontmatter (agent + command modes) + * model: inherit is NOT added (OpenCode doesn't support it — see #1156) + * but mode: subagent IS added (required by OpenCode agents). * Gemini: convertClaudeToGeminiAgent (frontmatter + tool mapping + body escaping) */ @@ -56,10 +58,10 @@ describe('OpenCode agent conversion (isAgent: true)', () => { assert.ok(frontmatter.includes('name: gsd-executor'), 'name: should be preserved for agents'); }); - test('adds model: inherit', () => { + test('does not add model: inherit (OpenCode does not support it)', () => { const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true }); const frontmatter = result.split('---')[1]; - assert.ok(frontmatter.includes('model: inherit'), 'model: inherit should be added'); + assert.ok(!frontmatter.includes('model: inherit'), 'model: inherit should NOT be added — OpenCode throws ProviderModelNotFoundError'); }); test('adds mode: subagent', () => { From e84999663cd955fb87ade79dc0065d5d1abf874d Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 16 Mar 2026 16:51:24 -0400 Subject: [PATCH 41/83] feat(discuss-phase): cross-reference pending todos against phase scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a cross_reference_todos step to discuss-phase that surfaces relevant backlog items before scope-setting decisions are made. Implementation: - New 'todo match-phase ' CLI command (commands.cjs) that scores pending todos against a phase's ROADMAP goal using three heuristics: keyword overlap, area match, and file path overlap - New cross_reference_todos step in discuss-phase.md between load_prior_context and scout_codebase - CONTEXT.md template gains 'Folded Todos' subsection in and 'Reviewed Todos (not folded)' subsection in Design: - No AI call for matching — pure keyword/area/file heuristics for speed - Silent skip when todo_count is 0 or no matches (no workflow slowdown) - Auto mode folds all todos with score >= 0.4 automatically - Scoring: keywords (up to 0.6), area match (0.3), file overlap (0.4) Tests: 5 new tests covering empty state, keyword matching, unrelated todo exclusion, area matching, and score sorting. Closes #1111 --- get-shit-done/bin/gsd-tools.cjs | 4 +- get-shit-done/bin/lib/commands.cjs | 127 ++++++++++++++++++++++- get-shit-done/workflows/discuss-phase.md | 52 ++++++++++ tests/commands.test.cjs | 92 ++++++++++++++++ 4 files changed, 273 insertions(+), 2 deletions(-) diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index cdee42566..558185ac5 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -534,8 +534,10 @@ async function main() { const subcommand = args[1]; if (subcommand === 'complete') { commands.cmdTodoComplete(cwd, args[2], raw); + } else if (subcommand === 'match-phase') { + commands.cmdTodoMatchPhase(cwd, args[2], raw); } else { - error('Unknown todo subcommand. Available: complete'); + error('Unknown todo subcommand. Available: complete, match-phase'); } break; } diff --git a/get-shit-done/bin/lib/commands.cjs b/get-shit-done/bin/lib/commands.cjs index d7109df19..ff13ad6cb 100644 --- a/get-shit-done/bin/lib/commands.cjs +++ b/get-shit-done/bin/lib/commands.cjs @@ -4,7 +4,7 @@ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); -const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, stripShippedMilestones, extractCurrentMilestone, toPosixPath, output, error, findPhaseInternal } = require('./core.cjs'); +const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, stripShippedMilestones, extractCurrentMilestone, toPosixPath, output, error, findPhaseInternal, getRoadmapPhaseInternal } = require('./core.cjs'); const { extractFrontmatter } = require('./frontmatter.cjs'); const { MODEL_PROFILES } = require('./model-profiles.cjs'); @@ -448,6 +448,130 @@ function cmdProgressRender(cwd, format, raw) { } } +/** + * Match pending todos against a phase's goal/name/requirements. + * Returns todos with relevance scores based on keyword, area, and file overlap. + * Used by discuss-phase to surface relevant todos before scope-setting. + */ +function cmdTodoMatchPhase(cwd, phase, raw) { + if (!phase) { error('phase required for todo match-phase'); } + + const pendingDir = path.join(cwd, '.planning', 'todos', 'pending'); + const todos = []; + + // Load pending todos + try { + const files = fs.readdirSync(pendingDir).filter(f => f.endsWith('.md')); + for (const file of files) { + try { + const content = fs.readFileSync(path.join(pendingDir, file), 'utf-8'); + const titleMatch = content.match(/^title:\s*(.+)$/m); + const areaMatch = content.match(/^area:\s*(.+)$/m); + const filesMatch = content.match(/^files:\s*(.+)$/m); + const body = content.replace(/^(title|area|files|created|priority):.*$/gm, '').trim(); + + todos.push({ + file, + title: titleMatch ? titleMatch[1].trim() : 'Untitled', + area: areaMatch ? areaMatch[1].trim() : 'general', + files: filesMatch ? filesMatch[1].trim().split(/[,\s]+/).filter(Boolean) : [], + body: body.slice(0, 200), // first 200 chars for context + }); + } catch {} + } + } catch {} + + if (todos.length === 0) { + output({ phase, matches: [], todo_count: 0 }, raw); + return; + } + + // Load phase goal/name from ROADMAP + const phaseInfo = getRoadmapPhaseInternal(cwd, phase); + const phaseName = phaseInfo ? (phaseInfo.phase_name || '') : ''; + const phaseGoal = phaseInfo ? (phaseInfo.goal || '') : ''; + const phaseSection = phaseInfo ? (phaseInfo.section || '') : ''; + + // Build keyword set from phase name + goal + section text + const phaseText = `${phaseName} ${phaseGoal} ${phaseSection}`.toLowerCase(); + const stopWords = new Set(['the', 'and', 'for', 'with', 'from', 'that', 'this', 'will', 'are', 'was', 'has', 'have', 'been', 'not', 'but', 'all', 'can', 'into', 'each', 'when', 'any', 'use', 'new']); + const phaseKeywords = new Set( + phaseText.split(/[\s\-_/.,;:()\[\]{}|]+/) + .map(w => w.replace(/[^a-z0-9]/g, '')) + .filter(w => w.length > 2 && !stopWords.has(w)) + ); + + // Find phase directory to get expected file paths + const phaseInfoDisk = findPhaseInternal(cwd, phase); + const phasePlans = []; + if (phaseInfoDisk && phaseInfoDisk.found) { + try { + const phaseDir = path.join(cwd, phaseInfoDisk.directory); + const planFiles = fs.readdirSync(phaseDir).filter(f => f.endsWith('-PLAN.md')); + for (const pf of planFiles) { + try { + const planContent = fs.readFileSync(path.join(phaseDir, pf), 'utf-8'); + const fmFiles = planContent.match(/files_modified:\s*\[([^\]]*)\]/); + if (fmFiles) { + phasePlans.push(...fmFiles[1].split(',').map(s => s.trim().replace(/['"]/g, '')).filter(Boolean)); + } + } catch {} + } + } catch {} + } + + // Score each todo for relevance + const matches = []; + for (const todo of todos) { + let score = 0; + const reasons = []; + + // Keyword match: todo title/body terms in phase text + const todoWords = `${todo.title} ${todo.body}`.toLowerCase() + .split(/[\s\-_/.,;:()\[\]{}|]+/) + .map(w => w.replace(/[^a-z0-9]/g, '')) + .filter(w => w.length > 2 && !stopWords.has(w)); + + const matchedKeywords = todoWords.filter(w => phaseKeywords.has(w)); + if (matchedKeywords.length > 0) { + score += Math.min(matchedKeywords.length * 0.2, 0.6); + reasons.push(`keywords: ${[...new Set(matchedKeywords)].slice(0, 5).join(', ')}`); + } + + // Area match: todo area appears in phase text + if (todo.area !== 'general' && phaseText.includes(todo.area.toLowerCase())) { + score += 0.3; + reasons.push(`area: ${todo.area}`); + } + + // File match: todo files overlap with phase plan files + if (todo.files.length > 0 && phasePlans.length > 0) { + const fileOverlap = todo.files.filter(f => + phasePlans.some(pf => pf.includes(f) || f.includes(pf)) + ); + if (fileOverlap.length > 0) { + score += 0.4; + reasons.push(`files: ${fileOverlap.slice(0, 3).join(', ')}`); + } + } + + if (score > 0) { + matches.push({ + file: todo.file, + title: todo.title, + area: todo.area, + score: Math.round(score * 100) / 100, + reasons, + }); + } + } + + // Sort by score descending + matches.sort((a, b) => b.score - a.score); + + output({ phase, matches, todo_count: todos.length }, raw); +} + function cmdTodoComplete(cwd, filename, raw) { if (!filename) { error('filename required for todo complete'); @@ -704,6 +828,7 @@ module.exports = { cmdWebsearch, cmdProgressRender, cmdTodoComplete, + cmdTodoMatchPhase, cmdScaffold, cmdStats, }; diff --git a/get-shit-done/workflows/discuss-phase.md b/get-shit-done/workflows/discuss-phase.md index d8f7a6f40..d21c1fe4a 100644 --- a/get-shit-done/workflows/discuss-phase.md +++ b/get-shit-done/workflows/discuss-phase.md @@ -242,6 +242,47 @@ Structure the extracted information: **If no prior context exists:** Continue without — this is expected for early phases. + +Check if any pending todos are relevant to this phase's scope. Surfaces backlog items that might otherwise be missed. + +**Load and match todos:** +```bash +TODO_MATCHES=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" todo match-phase "${PHASE_NUMBER}") +``` + +Parse JSON for: `todo_count`, `matches[]` (each with `file`, `title`, `area`, `score`, `reasons`). + +**If `todo_count` is 0 or `matches` is empty:** Skip silently — no workflow slowdown. + +**If matches found:** + +Present matched todos to the user. Show each match with its title, area, and why it matched: + +``` +📋 Found {N} pending todo(s) that may be relevant to Phase {X}: + +{For each match:} +- **{title}** (area: {area}, relevance: {score}) — matched on {reasons} +``` + +Use AskUserQuestion (multiSelect) asking which todos to fold into this phase's scope: + +``` +Which of these todos should be folded into Phase {X} scope? +(Select any that apply, or none to skip) +``` + +**For selected (folded) todos:** +- Store internally as `` for inclusion in CONTEXT.md `` section +- These become additional scope items that downstream agents (researcher, planner) will see + +**For unselected (reviewed but not folded) todos:** +- Store internally as `` for inclusion in CONTEXT.md `` section +- This prevents future phases from re-surfacing the same todos as "missed" + +**Auto mode (`--auto`):** Fold all todos with score >= 0.4 automatically. Log the selection. + + Lightweight scan of existing code to inform gray area identification and discussion. Uses ~10% context — acceptable for an interactive session. @@ -544,6 +585,11 @@ mkdir -p ".planning/phases/${padded_phase}-${phase_slug}" ### Claude's Discretion [Areas where user said "you decide" — note that Claude has flexibility here] +### Folded Todos +[If any todos were folded into scope from the cross_reference_todos step, list them here. +Each entry should include the todo title, original problem, and how it fits this phase's scope. +If no todos were folded: omit this subsection entirely.] + @@ -595,6 +641,12 @@ Every entry needs a full relative path — not just a name.] [Ideas that came up but belong in other phases. Don't lose them.] +### Reviewed Todos (not folded) +[If any todos were reviewed in cross_reference_todos but not folded into scope, +list them here so future phases know they were considered. +Each entry: todo title + reason it was deferred (out of scope, belongs in Phase Y, etc.) +If no reviewed-but-deferred todos: omit this subsection entirely.] + [If none: "None — discussion stayed within phase scope"] diff --git a/tests/commands.test.cjs b/tests/commands.test.cjs index b43d2827a..f84624e47 100644 --- a/tests/commands.test.cjs +++ b/tests/commands.test.cjs @@ -567,6 +567,98 @@ describe('todo complete command', () => { }); }); +// ───────────────────────────────────────────────────────────────────────────── +// todo match-phase command +// ───────────────────────────────────────────────────────────────────────────── + +describe('todo match-phase command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + afterEach(() => cleanup(tmpDir)); + + test('returns empty matches when no todos exist', () => { + const result = runGsdTools('todo match-phase 01', tmpDir); + assert.ok(result.success, 'should succeed'); + const output = JSON.parse(result.output); + assert.strictEqual(output.todo_count, 0); + assert.deepStrictEqual(output.matches, []); + }); + + test('matches todo by keyword overlap with phase name', () => { + const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending'); + fs.mkdirSync(pendingDir, { recursive: true }); + fs.writeFileSync(path.join(pendingDir, 'auth-todo.md'), + 'title: Add OAuth token refresh\narea: auth\ncreated: 2026-03-01\n\nNeed to handle token expiry for OAuth flows.'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), + '# Roadmap\n\n### Phase 01: Authentication and Session Management\n\n**Goal:** Implement OAuth login and session handling\n'); + + const result = runGsdTools('todo match-phase 01', tmpDir); + assert.ok(result.success, 'should succeed'); + const output = JSON.parse(result.output); + assert.strictEqual(output.todo_count, 1, 'should find 1 todo'); + assert.ok(output.matches.length > 0, 'should have matches'); + assert.strictEqual(output.matches[0].title, 'Add OAuth token refresh'); + assert.ok(output.matches[0].score > 0, 'score should be positive'); + assert.ok(output.matches[0].reasons.length > 0, 'should have reasons'); + }); + + test('does not match unrelated todo', () => { + const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending'); + fs.mkdirSync(pendingDir, { recursive: true }); + fs.writeFileSync(path.join(pendingDir, 'auth-todo.md'), + 'title: Add OAuth token refresh\narea: auth\ncreated: 2026-03-01\n\nOAuth token expiry.'); + fs.writeFileSync(path.join(pendingDir, 'unrelated-todo.md'), + 'title: Fix CSS grid layout in dashboard\narea: ui\ncreated: 2026-03-01\n\nGrid columns break on mobile.'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), + '# Roadmap\n\n### Phase 01: Authentication and Session Management\n\n**Goal:** Implement OAuth login and session handling\n'); + + const result = runGsdTools('todo match-phase 01', tmpDir); + assert.ok(result.success, 'should succeed'); + const output = JSON.parse(result.output); + const matchTitles = output.matches.map(m => m.title); + assert.ok(matchTitles.includes('Add OAuth token refresh'), 'auth todo should match'); + assert.ok(!matchTitles.includes('Fix CSS grid layout in dashboard'), 'unrelated todo should not match'); + }); + + test('matches todo by area overlap', () => { + const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending'); + fs.mkdirSync(pendingDir, { recursive: true }); + fs.writeFileSync(path.join(pendingDir, 'auth-todo.md'), + 'title: Add OAuth token refresh\narea: auth\ncreated: 2026-03-01\n\nOAuth token handling.'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), + '# Roadmap\n\n### Phase 01: Auth System\n\n**Goal:** Build auth module\n'); + + const result = runGsdTools('todo match-phase 01', tmpDir); + const output = JSON.parse(result.output); + const authMatch = output.matches.find(m => m.title === 'Add OAuth token refresh'); + assert.ok(authMatch, 'should find auth todo'); + const hasAreaReason = authMatch.reasons.some(r => r.startsWith('area:')); + assert.ok(hasAreaReason, 'should match on area'); + }); + + test('sorts matches by score descending', () => { + const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending'); + fs.mkdirSync(pendingDir, { recursive: true }); + fs.writeFileSync(path.join(pendingDir, 'weak-match.md'), + 'title: Check token format\narea: general\ncreated: 2026-03-01\n\nToken format validation.'); + fs.writeFileSync(path.join(pendingDir, 'strong-match.md'), + 'title: Session management authentication OAuth token handling\narea: auth\ncreated: 2026-03-01\n\nSession auth OAuth tokens.'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), + '# Roadmap\n\n### Phase 01: Authentication and Session Management\n\n**Goal:** Implement OAuth login, session handling, and token management\n'); + + const result = runGsdTools('todo match-phase 01', tmpDir); + const output = JSON.parse(result.output); + assert.ok(output.matches.length >= 2, 'should have multiple matches'); + for (let i = 1; i < output.matches.length; i++) { + assert.ok(output.matches[i - 1].score >= output.matches[i].score, + `match ${i-1} score (${output.matches[i-1].score}) should be >= match ${i} score (${output.matches[i].score})`); + } + }); +}); + // ───────────────────────────────────────────────────────────────────────────── // scaffold command // ───────────────────────────────────────────────────────────────────────────── From 6e612c54d7428ed4920d3a9fe579048afa18c001 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 16 Mar 2026 20:55:22 -0400 Subject: [PATCH 42/83] fix: embed evolution rules in generated PROJECT.md so agents see them at runtime (#1039) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The block in templates/project.md defined requirement lifecycle rules (validate, invalidate, add, log decisions) but these instructions only existed in the template — they never made it into the generated PROJECT.md that agents actually read during phase transitions. Changes: - new-project.md: Add ## Evolution section to generated PROJECT.md with the phase transition and milestone review checklists - new-milestone.md: Ensure ## Evolution section exists in PROJECT.md (backfills for projects created before this feature) - execute-phase.md: Add .planning/PROJECT.md to executor so executors have project context (core value, requirements, evolution) - templates/project.md: Add comment noting the block is implemented by transition.md and complete-milestone.md - docs/ARCHITECTURE.md, docs/FEATURES.md: Note evolution rules in PROJECT.md descriptions - CHANGELOG.md: Document the new Evolution section and executor context Fixes #1039 All 755 tests pass. --- docs/ARCHITECTURE.md | 2 +- docs/FEATURES.md | 2 +- get-shit-done/templates/project.md | 2 ++ get-shit-done/workflows/execute-phase.md | 1 + get-shit-done/workflows/new-milestone.md | 21 +++++++++++++++++++++ get-shit-done/workflows/new-project.md | 21 +++++++++++++++++++++ 6 files changed, 47 insertions(+), 2 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 0163b0cc0..f326c4f54 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -358,7 +358,7 @@ Equivalent paths for other runtimes: ``` .planning/ -├── PROJECT.md # Project vision, constraints, decisions +├── PROJECT.md # Project vision, constraints, decisions, evolution rules ├── REQUIREMENTS.md # Scoped requirements (v1/v2/out-of-scope) ├── ROADMAP.md # Phase breakdown with status tracking ├── STATE.md # Living memory: position, decisions, blockers, metrics diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 79e4a83d9..ad1e2402a 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -70,7 +70,7 @@ **Produces:** | Artifact | Description | |----------|-------------| -| `PROJECT.md` | Project vision, constraints, technical decisions | +| `PROJECT.md` | Project vision, constraints, technical decisions, evolution rules | | `REQUIREMENTS.md` | Scoped requirements with unique IDs (REQ-XX) | | `ROADMAP.md` | Phase breakdown with status tracking and requirement mapping | | `STATE.md` | Initial project state with position, decisions, metrics | diff --git a/get-shit-done/templates/project.md b/get-shit-done/templates/project.md index 8971f4528..37a986c70 100644 --- a/get-shit-done/templates/project.md +++ b/get-shit-done/templates/project.md @@ -127,6 +127,8 @@ Common types: Tech stack, Timeline, Budget, Dependencies, Compatibility, Perform PROJECT.md evolves throughout the project lifecycle. +These rules are embedded in the generated PROJECT.md (## Evolution section) +and implemented by workflows/transition.md and workflows/complete-milestone.md. **After each phase transition:** 1. Requirements invalidated? → Move to Out of Scope with reason diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index e5c6afe4d..4f778e7c2 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -214,6 +214,7 @@ Execute each wave in sequence. Within a wave: parallel if `PARALLELIZATION=true` Read these files at execution start using the Read tool: - {phase_dir}/{plan_file} (Plan) + - .planning/PROJECT.md (Project context — core value, requirements, evolution rules) - .planning/STATE.md (State) - .planning/config.json (Config, if exists) - ./CLAUDE.md (Project instructions, if exists — follow project-specific guidelines and coding conventions) diff --git a/get-shit-done/workflows/new-milestone.md b/get-shit-done/workflows/new-milestone.md index 59b894bbb..492108ef3 100644 --- a/get-shit-done/workflows/new-milestone.md +++ b/get-shit-done/workflows/new-milestone.md @@ -54,6 +54,27 @@ Add/update: Update Active requirements section and "Last updated" footer. +Ensure the `## Evolution` section exists in PROJECT.md. If missing (projects created before this feature), add it before the footer: + +```markdown +## Evolution + +This document evolves at phase transitions and milestone boundaries. + +**After each phase transition** (via `/gsd:transition`): +1. Requirements invalidated? → Move to Out of Scope with reason +2. Requirements validated? → Move to Validated with phase reference +3. New requirements emerged? → Add to Active +4. Decisions to log? → Add to Key Decisions +5. "What This Is" still accurate? → Update if drifted + +**After each milestone** (via `/gsd:complete-milestone`): +1. Full review of all sections +2. Core Value check — still the right priority? +3. Audit Out of Scope — reasons still valid? +4. Update Context with current state +``` + ## 5. Update STATE.md ```markdown diff --git a/get-shit-done/workflows/new-project.md b/get-shit-done/workflows/new-project.md index 7db0e6c6b..90597a369 100644 --- a/get-shit-done/workflows/new-project.md +++ b/get-shit-done/workflows/new-project.md @@ -335,6 +335,27 @@ Initialize with any decisions made during questioning: *Last updated: [date] after initialization* ``` +**Evolution section** (include at the end of PROJECT.md, before the footer): + +```markdown +## Evolution + +This document evolves at phase transitions and milestone boundaries. + +**After each phase transition** (via `/gsd:transition`): +1. Requirements invalidated? → Move to Out of Scope with reason +2. Requirements validated? → Move to Validated with phase reference +3. New requirements emerged? → Add to Active +4. Decisions to log? → Add to Key Decisions +5. "What This Is" still accurate? → Update if drifted + +**After each milestone** (via `/gsd:complete-milestone`): +1. Full review of all sections +2. Core Value check — still the right priority? +3. Audit Out of Scope — reasons still valid? +4. Update Context with current state +``` + Do not compress. Capture everything gathered. **Commit PROJECT.md:** From 0377951a04021e06eb55e3166466fd1d240e8076 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 17 Mar 2026 07:49:14 -0400 Subject: [PATCH 43/83] feat: context window size awareness for 1M+ models (#1086) GSD was designed around 200k context windows. With Opus 4.6 / Sonnet 4.6 shipping 1M context at standard pricing, GSD should adapt its strategies. Changes: - Add context_window config option (default: 200000, configurable to 1000000) - Expose context_window in init output so workflows can adapt behavior - Update execute-phase.md to note context-size-dependent strategies: - 200k: paths-only executor context, 10-15% orchestrator budget - 1M+: richer context passing, inline small phases, relaxed /clear - Context monitor already uses percentage-based thresholds (adaptive) Users can opt in via config.json: { "context_window": 1000000 } Addresses #1086 --- get-shit-done/bin/lib/core.cjs | 2 ++ get-shit-done/bin/lib/init.cjs | 1 + get-shit-done/workflows/execute-phase.md | 13 ++++++++++--- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index 5038dc247..d195bfdfd 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -65,6 +65,7 @@ function loadConfig(cwd) { parallelization: true, brave_search: false, resolve_model_ids: false, // when true, resolve aliases (opus/sonnet/haiku) to full model IDs + context_window: 200000, // default 200k; set to 1000000 for Opus/Sonnet 4.6 1M models }; try { @@ -108,6 +109,7 @@ function loadConfig(cwd) { parallelization, brave_search: get('brave_search') ?? defaults.brave_search, resolve_model_ids: get('resolve_model_ids') ?? defaults.resolve_model_ids, + context_window: get('context_window') ?? defaults.context_window, model_overrides: parsed.model_overrides || null, }; } catch { diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index 9c9d35655..3d92bdc6b 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -31,6 +31,7 @@ function cmdInitExecutePhase(cwd, phase, raw) { // Config flags commit_docs: config.commit_docs, parallelization: config.parallelization, + context_window: config.context_window, branching_strategy: config.branching_strategy, phase_branch_template: config.phase_branch_template, milestone_branch_template: config.milestone_branch_template, diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index e5c6afe4d..58ddb3a46 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -191,8 +191,9 @@ Execute each wave in sequence. Within a wave: parallel if `PARALLELIZATION=true` 2. **Spawn executor agents:** - Pass paths only — executors read files themselves with their fresh 200k context. - This keeps orchestrator context lean (~10-15%). + Pass paths only — executors read files themselves with their fresh context window. + For 200k models, this keeps orchestrator context lean (~10-15%). + For 1M+ models (Opus 4.6, Sonnet 4.6), richer context can be passed directly. ``` Task( @@ -652,7 +653,13 @@ Only suggest the commands listed above. Do not invent or hallucinate command nam -Orchestrator: ~10-15% context. Subagents: fresh 200k each. No polling (Task blocks). No context bleed. +Orchestrator: ~10-15% context for 200k windows, can use more for 1M+ windows. +Subagents: fresh context each (200k-1M depending on model). No polling (Task blocks). No context bleed. + +For 1M+ context models, consider: +- Passing richer context (code snippets, dependency outputs) directly to executors instead of just file paths +- Running small phases (≤3 plans, no dependencies) inline without subagent spawning overhead +- Relaxing /clear recommendations — context rot onset is much further out with 5x window From c7954d1ad70b492c31e952254d8e91ea8e30a21f Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 16 Mar 2026 21:27:06 -0400 Subject: [PATCH 44/83] refactor: deduplicate code, add planningPaths helper, annotate empty catches Code quality refactoring across 9 lib modules: 1. state.cjs: Remove duplicate stateExtractField() definition (lines 12 vs 184). Replace 3 inline regex escape patterns with escapeRegex() from core.cjs. Add getStatePath() local helper for the 15 repeated statePath declarations. Import planningPaths from core. 2. core.cjs: Add planningPaths(cwd) helper returning an object with all common .planning paths (state, roadmap, project, config, phases, requirements). Add planningDir(cwd) shorthand. Export both. 3. init.cjs: Replace Unix-only execSync('find . | grep | head') with cross-platform fs.readdirSync recursive walk for code file detection. Remove unused child_process import. Fixes Windows compatibility. 4. All lib modules: Annotate 48 empty catch blocks with /* intentionally empty */ to distinguish deliberate error swallowing from accidental missing handlers. Affected: commands.cjs (8), config.cjs (1), core.cjs (4), init.cjs (13), milestone.cjs (3), phase.cjs (6), roadmap.cjs (1), state.cjs (3), verify.cjs (9). All 755 tests pass. --- get-shit-done/bin/lib/commands.cjs | 16 +++++----- get-shit-done/bin/lib/config.cjs | 2 +- get-shit-done/bin/lib/core.cjs | 31 +++++++++++++++--- get-shit-done/bin/lib/init.cjs | 49 +++++++++++++++++------------ get-shit-done/bin/lib/milestone.cjs | 6 ++-- get-shit-done/bin/lib/phase.cjs | 12 +++---- get-shit-done/bin/lib/roadmap.cjs | 2 +- get-shit-done/bin/lib/state.cjs | 34 +++++++++----------- get-shit-done/bin/lib/verify.cjs | 18 +++++------ 9 files changed, 98 insertions(+), 72 deletions(-) diff --git a/get-shit-done/bin/lib/commands.cjs b/get-shit-done/bin/lib/commands.cjs index d7109df19..4dc913183 100644 --- a/get-shit-done/bin/lib/commands.cjs +++ b/get-shit-done/bin/lib/commands.cjs @@ -71,9 +71,9 @@ function cmdListTodos(cwd, area, raw) { area: todoArea, path: toPosixPath(path.join('.planning', 'todos', 'pending', file)), }); - } catch {} + } catch { /* intentionally empty */ } } - } catch {} + } catch { /* intentionally empty */ } const result = { count, todos }; output(result, raw, count.toString()); @@ -120,7 +120,7 @@ function cmdHistoryDigest(cwd, raw) { for (const dir of currentDirs) { allPhaseDirs.push({ name: dir, fullPath: path.join(phasesDir, dir), milestone: null }); } - } catch {} + } catch { /* intentionally empty */ } } if (allPhaseDirs.length === 0) { @@ -412,7 +412,7 @@ function cmdProgressRender(cwd, format, raw) { phases.push({ number: phaseNum, name: phaseName, plans, summaries, status }); } - } catch {} + } catch { /* intentionally empty */ } const percent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0; @@ -559,7 +559,7 @@ function cmdStats(cwd, format, raw) { status: 'Not Started', }); } - } catch {} + } catch { /* intentionally empty */ } try { const entries = fs.readdirSync(phasesDir, { withFileTypes: true }); @@ -595,7 +595,7 @@ function cmdStats(cwd, format, raw) { status, }); } - } catch {} + } catch { /* intentionally empty */ } const phases = [...phasesByNumber.values()].sort((a, b) => comparePhaseNum(a.number, b.number)); const completedPhases = phases.filter(p => p.status === 'Complete').length; @@ -613,7 +613,7 @@ function cmdStats(cwd, format, raw) { requirementsComplete = checked ? checked.length : 0; requirementsTotal = requirementsComplete + (unchecked ? unchecked.length : 0); } - } catch {} + } catch { /* intentionally empty */ } // Last activity from STATE.md let lastActivity = null; @@ -626,7 +626,7 @@ function cmdStats(cwd, format, raw) { || stateContent.match(/^Last activity:\s*(.+)$/im); if (activityMatch) lastActivity = activityMatch[1].trim(); } - } catch {} + } catch { /* intentionally empty */ } // Git stats let gitCommits = 0; diff --git a/get-shit-done/bin/lib/config.cjs b/get-shit-done/bin/lib/config.cjs index 1e0e65491..ef1466f61 100644 --- a/get-shit-done/bin/lib/config.cjs +++ b/get-shit-done/bin/lib/config.cjs @@ -76,7 +76,7 @@ function ensureConfigFile(cwd) { delete userDefaults.depth; try { fs.writeFileSync(globalDefaultsPath, JSON.stringify(userDefaults, null, 2), 'utf-8'); - } catch {} + } catch { /* intentionally empty */ } } } } catch (err) { diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index 5038dc247..847ea1a4f 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -76,7 +76,7 @@ function loadConfig(cwd) { const depthToGranularity = { quick: 'coarse', standard: 'standard', comprehensive: 'fine' }; parsed.granularity = depthToGranularity[parsed.depth] || parsed.depth; delete parsed.depth; - try { fs.writeFileSync(configPath, JSON.stringify(parsed, null, 2), 'utf-8'); } catch {} + try { fs.writeFileSync(configPath, JSON.stringify(parsed, null, 2), 'utf-8'); } catch { /* intentionally empty */ } } const get = (key, nested) => { @@ -250,6 +250,27 @@ function execGit(cwd, args) { }; } +// ─── Common path helpers ────────────────────────────────────────────────────── + +/** Get the .planning directory path */ +function planningDir(cwd) { + return path.join(cwd, '.planning'); +} + +/** Get common .planning file paths */ +function planningPaths(cwd) { + const base = path.join(cwd, '.planning'); + return { + planning: base, + state: path.join(base, 'STATE.md'), + roadmap: path.join(base, 'ROADMAP.md'), + project: path.join(base, 'PROJECT.md'), + config: path.join(base, 'config.json'), + phases: path.join(base, 'phases'), + requirements: path.join(base, 'REQUIREMENTS.md'), + }; +} + // ─── Phase utilities ────────────────────────────────────────────────────────── function escapeRegex(value) { @@ -370,7 +391,7 @@ function findPhaseInternal(cwd, phase) { return result; } } - } catch {} + } catch { /* intentionally empty */ } return null; } @@ -405,7 +426,7 @@ function getArchivedPhaseDirs(cwd) { }); } } - } catch {} + } catch { /* intentionally empty */ } return results; } @@ -663,7 +684,7 @@ function getMilestonePhaseFilter(cwd) { while ((m = phasePattern.exec(roadmap)) !== null) { milestonePhaseNums.add(m[1]); } - } catch {} + } catch { /* intentionally empty */ } if (milestonePhaseNums.size === 0) { const passAll = () => true; @@ -709,4 +730,6 @@ module.exports = { replaceInCurrentMilestone, toPosixPath, MODEL_ALIAS_MAP, + planningDir, + planningPaths, }; diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index 9c9d35655..25fab1581 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -153,7 +153,7 @@ function cmdInitPlanPhase(cwd, phase, raw) { if (uatFile) { result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile)); } - } catch {} + } catch { /* intentionally empty */ } } output(result, raw); @@ -167,17 +167,26 @@ function cmdInitNewProject(cwd, raw) { const braveKeyFile = path.join(homedir, '.gsd', 'brave_api_key'); const hasBraveSearch = !!(process.env.BRAVE_API_KEY || fs.existsSync(braveKeyFile)); - // Detect existing code + // Detect existing code (cross-platform — no Unix `find` dependency) let hasCode = false; let hasPackageFile = false; try { - const files = execSync('find . -maxdepth 3 \\( -name "*.ts" -o -name "*.js" -o -name "*.py" -o -name "*.go" -o -name "*.rs" -o -name "*.swift" -o -name "*.java" \\) 2>/dev/null | grep -v node_modules | grep -v .git | head -5', { - cwd, - encoding: 'utf-8', - stdio: ['pipe', 'pipe', 'pipe'], - }); - hasCode = files.trim().length > 0; - } catch {} + const codeExtensions = new Set(['.ts', '.js', '.py', '.go', '.rs', '.swift', '.java']); + const skipDirs = new Set(['node_modules', '.git', '.planning', '.claude', '__pycache__', 'target', 'dist', 'build']); + function findCodeFiles(dir, depth) { + if (depth > 3) return false; + let entries; + try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return false; } + for (const entry of entries) { + if (entry.isFile() && codeExtensions.has(path.extname(entry.name))) return true; + if (entry.isDirectory() && !skipDirs.has(entry.name)) { + if (findCodeFiles(path.join(dir, entry.name), depth + 1)) return true; + } + } + return false; + } + hasCode = findCodeFiles(cwd, 0); + } catch { /* intentionally empty — best-effort detection */ } hasPackageFile = pathExistsInternal(cwd, 'package.json') || pathExistsInternal(cwd, 'requirements.txt') || @@ -307,7 +316,7 @@ function cmdInitResume(cwd, raw) { let interruptedAgentId = null; try { interruptedAgentId = fs.readFileSync(path.join(cwd, '.planning', 'current-agent-id.txt'), 'utf-8').trim(); - } catch {} + } catch { /* intentionally empty */ } const result = { // File existence @@ -459,7 +468,7 @@ function cmdInitPhaseOp(cwd, phase, raw) { if (uatFile) { result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile)); } - } catch {} + } catch { /* intentionally empty */ } } output(result, raw); @@ -494,9 +503,9 @@ function cmdInitTodos(cwd, area, raw) { area: todoArea, path: '.planning/todos/pending/' + file, }); - } catch {} + } catch { /* intentionally empty */ } } - } catch {} + } catch { /* intentionally empty */ } const result = { // Config @@ -543,9 +552,9 @@ function cmdInitMilestoneOp(cwd, raw) { const phaseFiles = fs.readdirSync(path.join(phasesDir, dir)); const hasSummary = phaseFiles.some(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md'); if (hasSummary) completedPhases++; - } catch {} + } catch { /* intentionally empty */ } } - } catch {} + } catch { /* intentionally empty */ } // Check archive const archiveDir = path.join(cwd, '.planning', 'archive'); @@ -554,7 +563,7 @@ function cmdInitMilestoneOp(cwd, raw) { archivedMilestones = fs.readdirSync(archiveDir, { withFileTypes: true }) .filter(e => e.isDirectory()) .map(e => e.name); - } catch {} + } catch { /* intentionally empty */ } const result = { // Config @@ -593,7 +602,7 @@ function cmdInitMapCodebase(cwd, raw) { let existingMaps = []; try { existingMaps = fs.readdirSync(codebaseDir).filter(f => f.endsWith('.md')); - } catch {} + } catch { /* intentionally empty */ } const result = { // Models @@ -642,7 +651,7 @@ function cmdInitProgress(cwd, raw) { roadmapPhaseNums.add(hm[1]); roadmapPhaseNames.set(hm[1], hm[2].replace(/\(INSERTED\)/i, '').trim()); } - } catch {} + } catch { /* intentionally empty */ } const isDirInMilestone = getMilestonePhaseFilter(cwd); const seenPhaseNums = new Set(); @@ -695,7 +704,7 @@ function cmdInitProgress(cwd, raw) { nextPhase = phaseInfo; } } - } catch {} + } catch { /* intentionally empty */ } // Add phases defined in ROADMAP but not yet scaffolded to disk for (const [num, name] of roadmapPhaseNames) { @@ -726,7 +735,7 @@ function cmdInitProgress(cwd, raw) { const state = fs.readFileSync(path.join(cwd, '.planning', 'STATE.md'), 'utf-8'); const pauseMatch = state.match(/\*\*Paused At:\*\*\s*(.+)/); if (pauseMatch) pausedAt = pauseMatch[1].trim(); - } catch {} + } catch { /* intentionally empty */ } const result = { // Models diff --git a/get-shit-done/bin/lib/milestone.cjs b/get-shit-done/bin/lib/milestone.cjs index 6fd032798..ba56a0727 100644 --- a/get-shit-done/bin/lib/milestone.cjs +++ b/get-shit-done/bin/lib/milestone.cjs @@ -137,10 +137,10 @@ function cmdMilestoneComplete(cwd, version, options, raw) { // Count tasks const taskMatches = content.match(/##\s*Task\s*\d+/gi) || []; totalTasks += taskMatches.length; - } catch {} + } catch { /* intentionally empty */ } } } - } catch {} + } catch { /* intentionally empty */ } // Archive ROADMAP.md if (fs.existsSync(roadmapPath)) { @@ -220,7 +220,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) { archivedCount++; } phasesArchived = archivedCount > 0; - } catch {} + } catch { /* intentionally empty */ } } const result = { diff --git a/get-shit-done/bin/lib/phase.cjs b/get-shit-done/bin/lib/phase.cjs index 5cb5eac4d..dc2c52f99 100644 --- a/get-shit-done/bin/lib/phase.cjs +++ b/get-shit-done/bin/lib/phase.cjs @@ -401,7 +401,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) { const dm = dir.match(decimalPattern); if (dm) existingDecimals.push(parseInt(dm[1], 10)); } - } catch {} + } catch { /* intentionally empty */ } const nextDecimal = existingDecimals.length === 0 ? 1 : Math.max(...existingDecimals) + 1; const decimalPhase = `${normalizedBase}.${nextDecimal}`; @@ -470,7 +470,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) { const entries = fs.readdirSync(phasesDir, { withFileTypes: true }); const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b)); targetDir = dirs.find(d => d.startsWith(normalized + '-') || d === normalized); - } catch {} + } catch { /* intentionally empty */ } // Check for executed work (SUMMARY.md files) if (targetDir && !force) { @@ -538,7 +538,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) { } } } - } catch {} + } catch { /* intentionally empty */ } } else { // Integer removal: renumber all subsequent integer phases @@ -598,7 +598,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) { } } } - } catch {} + } catch { /* intentionally empty */ } } // Update ROADMAP.md @@ -818,7 +818,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) { } } } - } catch {} + } catch { /* intentionally empty */ } // Fallback: if filesystem found no next phase, check ROADMAP.md // for phases that are defined but not yet planned (no directory on disk) @@ -835,7 +835,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) { break; } } - } catch {} + } catch { /* intentionally empty */ } } // Update STATE.md diff --git a/get-shit-done/bin/lib/roadmap.cjs b/get-shit-done/bin/lib/roadmap.cjs index a199a6ab8..0e6d900e7 100644 --- a/get-shit-done/bin/lib/roadmap.cjs +++ b/get-shit-done/bin/lib/roadmap.cjs @@ -151,7 +151,7 @@ function cmdRoadmapAnalyze(cwd, raw) { else if (hasContext) diskStatus = 'discussed'; else diskStatus = 'empty'; } - } catch {} + } catch { /* intentionally empty */ } // Check ROADMAP checkbox status const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}[:\\s]`, 'i'); diff --git a/get-shit-done/bin/lib/state.cjs b/get-shit-done/bin/lib/state.cjs index 78797694a..9296aaa5b 100644 --- a/get-shit-done/bin/lib/state.cjs +++ b/get-shit-done/bin/lib/state.cjs @@ -4,9 +4,14 @@ const fs = require('fs'); const path = require('path'); -const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, normalizeMd, output, error } = require('./core.cjs'); +const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, normalizeMd, planningPaths, output, error } = require('./core.cjs'); const { extractFrontmatter, reconstructFrontmatter } = require('./frontmatter.cjs'); +/** Shorthand — every state command needs this path */ +function getStatePath(cwd) { + return planningPaths(cwd).state; +} + // Shared helper: extract a field value from STATE.md content. // Supports both **Field:** bold and plain Field: format. function stateExtractField(content, fieldName) { @@ -26,7 +31,7 @@ function cmdStateLoad(cwd, raw) { let stateRaw = ''; try { stateRaw = fs.readFileSync(path.join(planningDir, 'STATE.md'), 'utf-8'); - } catch {} + } catch { /* intentionally empty */ } const configExists = fs.existsSync(path.join(planningDir, 'config.json')); const roadmapExists = fs.existsSync(path.join(planningDir, 'ROADMAP.md')); @@ -75,7 +80,7 @@ function cmdStateGet(cwd, section, raw) { } // Try to find markdown section or field - const fieldEscaped = section.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const fieldEscaped = escapeRegex(section); // Check for **field:** value (bold format) const boldPattern = new RegExp(`\\*\\*${fieldEscaped}:\\*\\*\\s*(.*)`, 'i'); @@ -125,7 +130,7 @@ function cmdStatePatch(cwd, patches, raw) { const results = { updated: [], failed: [] }; for (const [field, value] of Object.entries(patches)) { - const fieldEscaped = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const fieldEscaped = escapeRegex(field); // Try **Field:** bold format first, then plain Field: format const boldPattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i'); const plainPattern = new RegExp(`(^${fieldEscaped}:\\s*)(.*)`, 'im'); @@ -159,7 +164,7 @@ function cmdStateUpdate(cwd, field, value) { const statePath = path.join(cwd, '.planning', 'STATE.md'); try { let content = fs.readFileSync(statePath, 'utf-8'); - const fieldEscaped = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const fieldEscaped = escapeRegex(field); // Try **Field:** bold format first, then plain Field: format const boldPattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i'); const plainPattern = new RegExp(`(^${fieldEscaped}:\\s*)(.*)`, 'im'); @@ -180,21 +185,10 @@ function cmdStateUpdate(cwd, field, value) { } // ─── State Progression Engine ──────────────────────────────────────────────── - -function stateExtractField(content, fieldName) { - const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - // Try **Field:** bold format first - const boldPattern = new RegExp(`\\*\\*${escaped}:\\*\\*\\s*(.+)`, 'i'); - const boldMatch = content.match(boldPattern); - if (boldMatch) return boldMatch[1].trim(); - // Fall back to plain Field: format - const plainPattern = new RegExp(`^${escaped}:\\s*(.+)`, 'im'); - const plainMatch = content.match(plainPattern); - return plainMatch ? plainMatch[1].trim() : null; -} +// stateExtractField is defined above (shared helper) — do not duplicate. function stateReplaceField(content, fieldName, newValue) { - const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const escaped = escapeRegex(fieldName); // Try **Field:** bold format first, then plain Field: format const boldPattern = new RegExp(`(\\*\\*${escaped}:\\*\\*\\s*)(.*)`, 'i'); if (boldPattern.test(content)) { @@ -576,7 +570,7 @@ function buildStateFrontmatter(bodyContent, cwd) { const info = getMilestoneInfo(cwd); milestone = info.version; milestoneName = info.name; - } catch {} + } catch { /* intentionally empty */ } } let totalPhases = totalPhasesRaw ? parseInt(totalPhasesRaw, 10) : null; @@ -611,7 +605,7 @@ function buildStateFrontmatter(bodyContent, cwd) { totalPlans = diskTotalPlans; completedPlans = diskTotalSummaries; } - } catch {} + } catch { /* intentionally empty */ } } let progressPercent = null; diff --git a/get-shit-done/bin/lib/verify.cjs b/get-shit-done/bin/lib/verify.cjs index 61fbc16c6..5b3a12c13 100644 --- a/get-shit-done/bin/lib/verify.cjs +++ b/get-shit-done/bin/lib/verify.cjs @@ -428,7 +428,7 @@ function cmdValidateConsistency(cwd, raw) { const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)/i); if (dm) diskPhases.add(dm[1]); } - } catch {} + } catch { /* intentionally empty */ } // Check: phases in ROADMAP but not on disk for (const p of roadmapPhases) { @@ -490,7 +490,7 @@ function cmdValidateConsistency(cwd, raw) { } } } - } catch {} + } catch { /* intentionally empty */ } // Check: frontmatter in plans has required fields try { @@ -510,7 +510,7 @@ function cmdValidateConsistency(cwd, raw) { } } } - } catch {} + } catch { /* intentionally empty */ } const passed = errors.length === 0; output({ passed, errors, warnings, warning_count: warnings.length }, raw, passed ? 'passed' : 'failed'); @@ -599,7 +599,7 @@ function cmdValidateHealth(cwd, options, raw) { if (m) diskPhases.add(m[1]); } } - } catch {} + } catch { /* intentionally empty */ } // Check for invalid references for (const ref of phaseRefs) { const normalizedRef = String(parseInt(ref, 10)).padStart(2, '0'); @@ -641,7 +641,7 @@ function cmdValidateHealth(cwd, options, raw) { addIssue('warning', 'W008', 'config.json: workflow.nyquist_validation absent (defaults to enabled but agents may skip)', 'Run /gsd:health --repair to add key', true); if (!repairs.includes('addNyquistKey')) repairs.push('addNyquistKey'); } - } catch {} + } catch { /* intentionally empty */ } } // ─── Check 6: Phase directory naming (NN-name format) ───────────────────── @@ -652,7 +652,7 @@ function cmdValidateHealth(cwd, options, raw) { addIssue('warning', 'W005', `Phase directory "${e.name}" doesn't follow NN-name format`, 'Rename to match pattern (e.g., 01-setup)'); } } - } catch {} + } catch { /* intentionally empty */ } // ─── Check 7: Orphaned plans (PLAN without SUMMARY) ─────────────────────── try { @@ -671,7 +671,7 @@ function cmdValidateHealth(cwd, options, raw) { } } } - } catch {} + } catch { /* intentionally empty */ } // ─── Check 7b: Nyquist VALIDATION.md consistency ──────────────────────── try { @@ -689,7 +689,7 @@ function cmdValidateHealth(cwd, options, raw) { } } } - } catch {} + } catch { /* intentionally empty */ } // ─── Check 8: Run existing consistency checks ───────────────────────────── // Inline subset of cmdValidateConsistency @@ -712,7 +712,7 @@ function cmdValidateHealth(cwd, options, raw) { if (dm) diskPhases.add(dm[1]); } } - } catch {} + } catch { /* intentionally empty */ } // Phases in ROADMAP but not on disk for (const p of roadmapPhases) { From 026a1b013e7026ed314583dafebcf42d9a3917b8 Mon Sep 17 00:00:00 2001 From: mitchelladam <17411882+mitchelladam@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:29:47 +0000 Subject: [PATCH 45/83] fix(cursor): write unquoted skill and subagent names Prevent Cursor from treating frontmatter quotes as part of skill/subagent identifiers by emitting plain name scalars, and add regression tests to lock the conversion behavior. Made-with: Cursor --- bin/install.js | 15 ++++++-- tests/cursor-conversion.test.cjs | 61 ++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 tests/cursor-conversion.test.cjs diff --git a/bin/install.js b/bin/install.js index ebbaa4c68..869821dd0 100755 --- a/bin/install.js +++ b/bin/install.js @@ -710,6 +710,14 @@ function yamlQuote(value) { return JSON.stringify(value); } +function yamlIdentifier(value) { + const text = String(value).trim(); + if (/^[A-Za-z0-9][A-Za-z0-9-]*$/.test(text)) { + return text; + } + return yamlQuote(text); +} + function extractFrontmatterAndBody(content) { if (!content.startsWith('---')) { return { frontmatter: null, body: content }; @@ -828,7 +836,7 @@ function convertClaudeCommandToCursorSkill(content, skillName) { const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description; const adapter = getCursorSkillAdapterHeader(skillName); - return `---\nname: ${yamlQuote(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`; + return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`; } /** @@ -845,7 +853,7 @@ function convertClaudeAgentToCursorAgent(content) { const name = extractFrontmatterField(frontmatter, 'name') || 'unknown'; const description = extractFrontmatterField(frontmatter, 'description') || ''; - const cleanFrontmatter = `---\nname: ${yamlQuote(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`; + const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`; return `${cleanFrontmatter}\n${body}`; } @@ -3257,7 +3265,10 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) { // Test-only exports — skip main logic when loaded as a module for testing if (process.env.GSD_TEST_MODE) { module.exports = { + yamlIdentifier, getCodexSkillAdapterHeader, + convertClaudeCommandToCursorSkill, + convertClaudeAgentToCursorAgent, convertClaudeToGeminiAgent, convertClaudeAgentToCodexAgent, generateCodexAgentToml, diff --git a/tests/cursor-conversion.test.cjs b/tests/cursor-conversion.test.cjs new file mode 100644 index 000000000..87652ab96 --- /dev/null +++ b/tests/cursor-conversion.test.cjs @@ -0,0 +1,61 @@ +/** + * Cursor conversion regression tests. + * + * Ensures Cursor frontmatter names are emitted as plain identifiers + * (without surrounding quotes), so Cursor does not treat quotes as + * literal parts of skill/subagent names. + */ + +process.env.GSD_TEST_MODE = '1'; + +const { describe, test } = require('node:test'); +const assert = require('node:assert'); + +const { + convertClaudeCommandToCursorSkill, + convertClaudeAgentToCursorAgent, +} = require('../bin/install.js'); + +describe('convertClaudeCommandToCursorSkill', () => { + test('writes unquoted Cursor skill name in frontmatter', () => { + const input = `--- +name: quick +description: Execute a quick task +--- + + +Test body + +`; + + const result = convertClaudeCommandToCursorSkill(input, 'gsd-quick'); + const nameMatch = result.match(/^name:\s*(.+)$/m); + + assert.ok(nameMatch, 'frontmatter contains name field'); + assert.equal(nameMatch[1], 'gsd-quick', 'skill name is plain scalar'); + assert.ok(!result.includes('name: "gsd-quick"'), 'quoted skill name is not emitted'); + }); +}); + +describe('convertClaudeAgentToCursorAgent', () => { + test('writes unquoted Cursor agent name in frontmatter', () => { + const input = `--- +name: gsd-planner +description: Planner agent +tools: Read, Write +color: green +--- + + +Planner body + +`; + + const result = convertClaudeAgentToCursorAgent(input); + const nameMatch = result.match(/^name:\s*(.+)$/m); + + assert.ok(nameMatch, 'frontmatter contains name field'); + assert.equal(nameMatch[1], 'gsd-planner', 'agent name is plain scalar'); + assert.ok(!result.includes('name: "gsd-planner"'), 'quoted agent name is not emitted'); + }); +}); From 69cfae2011ff1b3fdf31f468f9e6c891e0790f8a Mon Sep 17 00:00:00 2001 From: mitchelladam <17411882+mitchelladam@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:49:28 +0000 Subject: [PATCH 46/83] fix(cursor): preserve slash-prefixed gsd commands in converted markdown Keep `/gsd-*` command examples slash-prefixed during Cursor conversion while still normalizing legacy `gsd:` syntax, and add regression coverage for Next Up markdown output. Made-with: Cursor --- bin/install.js | 10 ++++------ tests/cursor-conversion.test.cjs | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/bin/install.js b/bin/install.js index fe30e5df2..ffc71fedf 100755 --- a/bin/install.js +++ b/bin/install.js @@ -766,11 +766,9 @@ function convertCursorToolName(claudeTool) { } function convertSlashCommandsToCursorSkillMentions(content) { - let converted = content.replace(/\/gsd:([a-z0-9-]+)/gi, (_, commandName) => { - return `gsd-${String(commandName).toLowerCase()}`; - }); - converted = converted.replace(/\/gsd-help\b/g, 'gsd-help'); - return converted; + // Keep leading "/" for slash commands; only normalize gsd: -> gsd-. + // This preserves rendered "next step" commands like "/gsd-execute-phase 17". + return content.replace(/gsd:/gi, 'gsd-'); } function convertClaudeToCursorMarkdown(content) { @@ -1831,7 +1829,7 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand } else if (isCursor && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) { // For Cursor, also convert Claude references in JS/CJS utility scripts let jsContent = fs.readFileSync(srcPath, 'utf8'); - jsContent = jsContent.replace(/\/gsd:([a-z0-9-]+)/gi, (_, cmd) => `gsd-${cmd.toLowerCase()}`); + jsContent = jsContent.replace(/gsd:/gi, 'gsd-'); jsContent = jsContent.replace(/\.claude\/skills\//g, '.cursor/skills/'); jsContent = jsContent.replace(/CLAUDE\.md/g, '.cursor/rules/'); jsContent = jsContent.replace(/\bClaude Code\b/g, 'Cursor'); diff --git a/tests/cursor-conversion.test.cjs b/tests/cursor-conversion.test.cjs index 87652ab96..2c7c79d52 100644 --- a/tests/cursor-conversion.test.cjs +++ b/tests/cursor-conversion.test.cjs @@ -35,6 +35,26 @@ Test body assert.equal(nameMatch[1], 'gsd-quick', 'skill name is plain scalar'); assert.ok(!result.includes('name: "gsd-quick"'), 'quoted skill name is not emitted'); }); + + test('preserves slash for slash commands in markdown body', () => { + const input = `--- +name: gsd:plan-phase +description: Plan a phase +--- + +Next: +/gsd:execute-phase 17 +/gsd-help +gsd:progress +`; + + const result = convertClaudeCommandToCursorSkill(input, 'gsd-plan-phase'); + + assert.ok(result.includes('/gsd-execute-phase 17'), 'slash command remains slash-prefixed'); + assert.ok(result.includes('/gsd-help'), 'existing slash command is preserved'); + assert.ok(result.includes('gsd-progress'), 'non-slash gsd: references still normalize'); + assert.ok(!result.includes('/gsd:execute-phase'), 'legacy colon command form is removed'); + }); }); describe('convertClaudeAgentToCursorAgent', () => { From a9be67f504b61bc0f91ef000839887c1083d154a Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 14:54:02 -0400 Subject: [PATCH 47/83] docs: comprehensive v1.26 release documentation update (#1187) Updates all docs to reflect v1.26.0 features and changes: README.md: - Add /gsd:ship and /gsd:next to command tables - Add /gsd:session-report to Session section - Update workflow to show ship step and auto-advance - Update inherit profile description for non-Anthropic providers docs/COMMANDS.md: - Add /gsd:next command reference with full state detection logic - Add /gsd:session-report command reference with report contents docs/FEATURES.md: - Add Auto-Advance (Next) feature (#14) - Add Cross-Phase Regression Gate feature (#20) - Add Requirements Coverage Gate feature (#21) - Add Session Reporting feature (#24) - Fix all section numbering (was broken with duplicates) - Update inherit profile to mention non-Anthropic providers - Renumber all 39 features consistently docs/USER-GUIDE.md: - Add /gsd:ship to workflow diagram - Add /gsd:next and /gsd:session-report to command tables - Add HANDOFF.json and reports/ to file structure - Add troubleshooting for non-Anthropic model providers - Add recovery entries for session-report and next - Update example workflow to include ship and session-report docs/CONFIGURATION.md: - Update inherit profile to mention non-Anthropic providers --- README.md | 19 ++++- docs/COMMANDS.md | 39 ++++++++++ docs/CONFIGURATION.md | 2 +- docs/FEATURES.md | 172 +++++++++++++++++++++++++++++++----------- docs/USER-GUIDE.md | 20 ++++- 5 files changed, 201 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index ddcee19a7..13e96875f 100644 --- a/README.md +++ b/README.md @@ -342,19 +342,26 @@ If everything passes, you move on. If something's broken, you don't manually deb --- -### 6. Repeat → Complete → Next Milestone +### 6. Repeat → Ship → Complete → Next Milestone ``` /gsd:discuss-phase 2 /gsd:plan-phase 2 /gsd:execute-phase 2 /gsd:verify-work 2 +/gsd:ship 2 # Create PR from verified work ... /gsd:complete-milestone /gsd:new-milestone ``` -Loop **discuss → plan → execute → verify** until milestone complete. +Or let GSD figure out the next step automatically: + +``` +/gsd:next # Auto-detect and run next step +``` + +Loop **discuss → plan → execute → verify → ship** until milestone complete. If you want faster intake during discussion, use `/gsd:discuss-phase --batch` to answer a small grouped set of questions at once instead of one-by-one. @@ -491,6 +498,8 @@ You're never locked in. The system adapts. | `/gsd:plan-phase [N] [--auto]` | Research + plan + verify for a phase | | `/gsd:execute-phase ` | Execute all plans in parallel waves, verify when complete | | `/gsd:verify-work [N]` | Manual user acceptance testing ¹ | +| `/gsd:ship [N] [--draft]` | Create PR from verified phase work with auto-generated body | +| `/gsd:next` | Automatically advance to the next logical workflow step | | `/gsd:audit-milestone` | Verify milestone achieved its definition of done | | `/gsd:complete-milestone` | Archive milestone, tag release | | `/gsd:new-milestone [name]` | Start next version: questions → research → requirements → roadmap | @@ -507,6 +516,7 @@ You're never locked in. The system adapts. | Command | What it does | |---------|--------------| | `/gsd:progress` | Where am I? What's next? | +| `/gsd:next` | Auto-detect state and run the next step | | `/gsd:help` | Show all commands and usage guide | | `/gsd:update` | Update GSD with changelog preview | | `/gsd:join-discord` | Join the GSD Discord community | @@ -531,8 +541,9 @@ You're never locked in. The system adapts. | Command | What it does | |---------|--------------| -| `/gsd:pause-work` | Create handoff when stopping mid-phase | +| `/gsd:pause-work` | Create handoff when stopping mid-phase (writes HANDOFF.json) | | `/gsd:resume-work` | Restore from last session | +| `/gsd:session-report` | Generate session summary with work performed and outcomes | ### Utilities @@ -581,7 +592,7 @@ Switch profiles: /gsd:set-profile budget ``` -Use `inherit` to follow the current runtime model selection (for example OpenCode `/model`). +Use `inherit` when using non-Anthropic providers (OpenRouter, local models) or to follow the current runtime model selection (e.g. OpenCode `/model`). Or configure via `/gsd:settings`. diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 9b7947737..1e2870342 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -132,6 +132,45 @@ User acceptance testing with auto-diagnosis. --- +### `/gsd:next` + +Automatically advance to the next logical workflow step. Reads project state and runs the appropriate command. + +**Prerequisites:** `.planning/` directory exists +**Behavior:** +- No project → suggests `/gsd:new-project` +- Phase needs discussion → runs `/gsd:discuss-phase` +- Phase needs planning → runs `/gsd:plan-phase` +- Phase needs execution → runs `/gsd:execute-phase` +- Phase needs verification → runs `/gsd:verify-work` +- All phases complete → suggests `/gsd:complete-milestone` + +```bash +/gsd:next # Auto-detect and run next step +``` + +--- + +### `/gsd:session-report` + +Generate a session report with work summary, outcomes, and estimated resource usage. + +**Prerequisites:** Active project with recent work +**Produces:** `.planning/reports/SESSION_REPORT.md` + +```bash +/gsd:session-report # Generate post-session summary +``` + +**Report includes:** +- Work performed (commits, plans executed, phases progressed) +- Outcomes and deliverables +- Blockers and decisions made +- Estimated token/cost usage +- Next steps recommendation + +--- + ### `/gsd:ship` Create PR from completed phase work with auto-generated body. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 3d1631752..bc750d3c4 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -246,7 +246,7 @@ Valid override values: `opus`, `sonnet`, `haiku`, `inherit` | `quality` | Opus for all decision-making, Sonnet for verification | Quota available, critical architecture work | | `balanced` | Opus for planning only, Sonnet for everything else | Normal development (default) | | `budget` | Sonnet for code-writing, Haiku for research/verification | High-volume work, less critical phases | -| `inherit` | All agents use current session model | Dynamic model switching (OpenCode `/model`) | +| `inherit` | All agents use current session model | Dynamic model switching, **non-Anthropic providers** (OpenRouter, local models) | --- diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 79e4a83d9..5ece01d3a 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -20,33 +20,38 @@ - [Quick Mode](#10-quick-mode) - [Autonomous Mode](#11-autonomous-mode) - [Freeform Routing](#12-freeform-routing) + - [Note Capture](#13-note-capture) + - [Auto-Advance (Next)](#14-auto-advance-next) - [Quality Assurance Features](#quality-assurance-features) - - [Nyquist Validation](#13-nyquist-validation) - - [Plan Checking](#14-plan-checking) - - [Post-Execution Verification](#15-post-execution-verification) - - [Node Repair](#16-node-repair) - - [Health Validation](#17-health-validation) + - [Nyquist Validation](#15-nyquist-validation) + - [Plan Checking](#16-plan-checking) + - [Post-Execution Verification](#17-post-execution-verification) + - [Node Repair](#18-node-repair) + - [Health Validation](#19-health-validation) + - [Cross-Phase Regression Gate](#20-cross-phase-regression-gate) + - [Requirements Coverage Gate](#21-requirements-coverage-gate) - [Context Engineering Features](#context-engineering-features) - - [Context Window Monitoring](#18-context-window-monitoring) - - [Session Management](#19-session-management) - - [Multi-Agent Orchestration](#20-multi-agent-orchestration) - - [Model Profiles](#21-model-profiles) + - [Context Window Monitoring](#22-context-window-monitoring) + - [Session Management](#23-session-management) + - [Session Reporting](#24-session-reporting) + - [Multi-Agent Orchestration](#25-multi-agent-orchestration) + - [Model Profiles](#26-model-profiles) - [Brownfield Features](#brownfield-features) - - [Codebase Mapping](#22-codebase-mapping) + - [Codebase Mapping](#27-codebase-mapping) - [Utility Features](#utility-features) - - [Debug System](#23-debug-system) - - [Todo Management](#24-todo-management) - - [Statistics Dashboard](#25-statistics-dashboard) - - [Update System](#26-update-system) - - [Settings Management](#27-settings-management) - - [Test Generation](#28-test-generation) + - [Debug System](#28-debug-system) + - [Todo Management](#29-todo-management) + - [Statistics Dashboard](#30-statistics-dashboard) + - [Update System](#31-update-system) + - [Settings Management](#32-settings-management) + - [Test Generation](#33-test-generation) - [Infrastructure Features](#infrastructure-features) - - [Git Integration](#29-git-integration) - - [CLI Tools](#30-cli-tools) - - [Multi-Runtime Support](#31-multi-runtime-support) - - [Hook System](#32-hook-system) - - [Developer Profiling](#33-developer-profiling) - - [Execution Hardening](#34-execution-hardening) + - [Git Integration](#34-git-integration) + - [CLI Tools](#35-cli-tools) + - [Multi-Runtime Support](#36-multi-runtime-support) + - [Hook System](#37-hook-system) + - [Developer Profiling](#38-developer-profiling) + - [Execution Hardening](#39-execution-hardening) --- @@ -408,9 +413,34 @@ --- +### 14. Auto-Advance (Next) + +**Command:** `/gsd:next` + +**Purpose:** Automatically detect current project state and advance to the next logical workflow step, eliminating the need to remember which phase/step you're on. + +**Requirements:** +- REQ-NEXT-01: System MUST read STATE.md, ROADMAP.md, and phase directories to determine current position +- REQ-NEXT-02: System MUST detect whether discuss, plan, execute, or verify is needed +- REQ-NEXT-03: System MUST invoke the correct command automatically +- REQ-NEXT-04: System MUST suggest `/gsd:new-project` if no project exists +- REQ-NEXT-05: System MUST suggest `/gsd:complete-milestone` when all phases are complete + +**State Detection Logic:** +| State | Action | +|-------|--------| +| No `.planning/` directory | Suggest `/gsd:new-project` | +| Phase has no CONTEXT.md | Run `/gsd:discuss-phase` | +| Phase has no PLAN.md files | Run `/gsd:plan-phase` | +| Phase has plans but no SUMMARY.md | Run `/gsd:execute-phase` | +| Phase executed but no VERIFICATION.md | Run `/gsd:verify-work` | +| All phases complete | Suggest `/gsd:complete-milestone` | + +--- + ## Quality Assurance Features -### 14. Nyquist Validation +### 15. Nyquist Validation **Purpose:** Map automated test coverage to phase requirements before any code is written. Named after the Nyquist sampling theorem — ensures a feedback signal exists for every requirement. @@ -433,7 +463,7 @@ --- -### 15. Plan Checking +### 16. Plan Checking **Purpose:** Goal-backward verification that plans will achieve phase objectives before execution. @@ -445,7 +475,7 @@ --- -### 16. Post-Execution Verification +### 17. Post-Execution Verification **Purpose:** Automated check that the codebase delivers what the phase promised. @@ -457,7 +487,7 @@ --- -### 17. Node Repair +### 18. Node Repair **Purpose:** Autonomous recovery when task verification fails during execution. @@ -471,7 +501,7 @@ --- -### 18. Health Validation +### 19. Health Validation **Command:** `/gsd:health [--repair]` @@ -486,9 +516,37 @@ --- +### 20. Cross-Phase Regression Gate + +**Purpose:** Prevent regressions from compounding across phases by running prior phases' test suites after execution. + +**Requirements:** +- REQ-REGR-01: System MUST run test suites from all completed prior phases after phase execution +- REQ-REGR-02: System MUST report any test failures as cross-phase regressions +- REQ-REGR-03: Regressions MUST be surfaced before post-execution verification +- REQ-REGR-04: System MUST identify which prior phase's tests were broken + +**When:** Runs automatically during `/gsd:execute-phase` before the verifier step. + +--- + +### 21. Requirements Coverage Gate + +**Purpose:** Ensure all phase requirements are covered by at least one plan before planning completes. + +**Requirements:** +- REQ-COVGATE-01: System MUST extract all requirement IDs assigned to the phase from ROADMAP.md +- REQ-COVGATE-02: System MUST verify each requirement appears in at least one PLAN.md +- REQ-COVGATE-03: Uncovered requirements MUST block planning completion +- REQ-COVGATE-04: System MUST report which specific requirements lack plan coverage + +**When:** Runs automatically at the end of `/gsd:plan-phase` after the plan checker loop. + +--- + ## Context Engineering Features -### 19. Context Window Monitoring +### 22. Context Window Monitoring **Purpose:** Prevent context rot by alerting both user and agent when context is running low. @@ -508,7 +566,7 @@ --- -### 20. Session Management +### 23. Session Management **Commands:** `/gsd:pause-work`, `/gsd:resume-work`, `/gsd:progress` @@ -525,7 +583,32 @@ --- -### 21. Multi-Agent Orchestration +### 24. Session Reporting + +**Command:** `/gsd:session-report` + +**Purpose:** Generate a structured post-session summary document capturing work performed, outcomes achieved, and estimated resource usage. + +**Requirements:** +- REQ-REPORT-01: System MUST gather data from STATE.md, git log, and plan/summary files +- REQ-REPORT-02: System MUST include commits made, plans executed, and phases progressed +- REQ-REPORT-03: System MUST estimate token usage and cost based on session activity +- REQ-REPORT-04: System MUST include active blockers and decisions made +- REQ-REPORT-05: System MUST recommend next steps + +**Produces:** `.planning/reports/SESSION_REPORT.md` + +**Report Sections:** +- Session overview (duration, milestone, phase) +- Work performed (commits, plans, phases) +- Outcomes and deliverables +- Blockers and decisions +- Resource estimates (tokens, cost) +- Next steps recommendation + +--- + +### 25. Multi-Agent Orchestration **Purpose:** Coordinate specialized agents with fresh context windows for each task. @@ -539,7 +622,7 @@ --- -### 22. Model Profiles +### 26. Model Profiles **Command:** `/gsd:set-profile ` @@ -550,6 +633,7 @@ - REQ-MODEL-02: Each profile MUST define model tier per agent (see profile table) - REQ-MODEL-03: Per-agent overrides MUST take precedence over profile - REQ-MODEL-04: `inherit` profile MUST defer to runtime's current model selection +- REQ-MODEL-04a: `inherit` profile MUST be used when running non-Anthropic providers (OpenRouter, local models) to avoid unexpected API costs - REQ-MODEL-05: Profile switch MUST be programmatic (script, not LLM-driven) - REQ-MODEL-06: Model resolution MUST happen once per orchestration, not per spawn @@ -574,7 +658,7 @@ ## Brownfield Features -### 23. Codebase Mapping +### 27. Codebase Mapping **Command:** `/gsd:map-codebase [area]` @@ -602,7 +686,7 @@ ## Utility Features -### 24. Debug System +### 28. Debug System **Command:** `/gsd:debug [description]` @@ -620,7 +704,7 @@ --- -### 25. Todo Management +### 29. Todo Management **Commands:** `/gsd:add-todo [desc]`, `/gsd:check-todos` @@ -634,7 +718,7 @@ --- -### 26. Statistics Dashboard +### 30. Statistics Dashboard **Command:** `/gsd:stats` @@ -648,7 +732,7 @@ --- -### 27. Update System +### 31. Update System **Command:** `/gsd:update` @@ -663,7 +747,7 @@ --- -### 28. Settings Management +### 32. Settings Management **Command:** `/gsd:settings` @@ -696,7 +780,7 @@ --- -### 29. Test Generation +### 33. Test Generation **Command:** `/gsd:add-tests [N]` @@ -711,7 +795,7 @@ ## Infrastructure Features -### 30. Git Integration +### 34. Git Integration **Purpose:** Atomic commits, branching strategies, and clean history management. @@ -737,7 +821,7 @@ fix(03-01): correct auth token expiry --- -### 31. CLI Tools +### 35. CLI Tools **Purpose:** Programmatic utilities for workflows and agents, replacing repetitive inline bash patterns. @@ -752,7 +836,7 @@ fix(03-01): correct auth token expiry --- -### 32. Multi-Runtime Support +### 36. Multi-Runtime Support **Purpose:** Run GSD across 6 different AI coding agent runtimes. @@ -775,7 +859,7 @@ fix(03-01): correct auth token expiry --- -### 33. Hook System +### 37. Hook System **Purpose:** Runtime event hooks for context monitoring, status display, and update checking. @@ -795,7 +879,7 @@ fix(03-01): correct auth token expiry Color coding: <50% green, <65% yellow, <80% orange, ≥80% red with skull emoji -### 33. Developer Profiling +### 38. Developer Profiling **Command:** `/gsd:profile-user [--questionnaire] [--refresh]` @@ -831,7 +915,7 @@ Color coding: <50% green, <65% yellow, <80% orange, ≥80% red with skull emoji - REQ-PROF-03: Questionnaire MUST be available as fallback when no session history exists - REQ-PROF-04: Generated artifacts MUST be discoverable by Claude Code (CLAUDE.md integration) -### 34. Execution Hardening +### 39. Execution Hardening **Purpose:** Three additive quality improvements to the execution pipeline that catch cross-plan failures before they cascade. diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index 4dcfca52b..36253a1fc 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -50,6 +50,10 @@ A detailed reference for workflows, troubleshooting, and configuration. For quic │ │ /gsd:verify-work │ │ <- Manual UAT │ └──────────┬─────────┘ │ │ │ │ + │ ┌──────────▼─────────┐ │ + │ │ /gsd:ship │ │ <- Create PR (optional) + │ └──────────┬─────────┘ │ + │ │ │ │ Next Phase?────────────┘ │ │ No └─────────────┼──────────────┘ @@ -285,6 +289,7 @@ Controlled by `workflow.ui_safety_gate` config toggle. | `/gsd:execute-phase ` | Execute all plans in parallel waves | After planning is complete | | `/gsd:verify-work [N]` | Manual UAT with auto-diagnosis | After execution completes | | `/gsd:ship [N]` | Create PR from verified work | After verification passes | +| `/gsd:next` | Auto-detect state and run next step | Anytime — "what should I do next?" | | `/gsd:ui-review [N]` | Retroactive 6-pillar visual audit | After execution or verify-work (frontend projects) | | `/gsd:audit-milestone` | Verify milestone met its definition of done | Before completing milestone | | `/gsd:complete-milestone` | Archive milestone, tag release | All phases verified | @@ -297,6 +302,7 @@ Controlled by `workflow.ui_safety_gate` config toggle. | `/gsd:progress` | Show status and next steps | Anytime -- "where am I?" | | `/gsd:resume-work` | Restore full context from last session | Starting a new session | | `/gsd:pause-work` | Save structured handoff (HANDOFF.json + continue-here.md) | Stopping mid-phase | +| `/gsd:session-report` | Generate session summary with work and outcomes | End of session, stakeholder sharing | | `/gsd:help` | Show all commands | Quick reference | | `/gsd:update` | Update GSD with changelog preview | Check for new versions | | `/gsd:join-discord` | Open Discord community invite | Questions or community | @@ -426,7 +432,7 @@ Disable these to speed up phases in familiar domains or when conserving tokens. - **quality** -- Opus for all decision-making agents, Sonnet for read-only verification. Use when quota is available and the work is critical. - **balanced** -- Opus only for planning (where architecture decisions happen), Sonnet for everything else. The default for good reason. - **budget** -- Sonnet for anything that writes code, Haiku for research and verification. Use for high-volume work or less critical phases. -- **inherit** -- All agents use the current session model. Best when switching models dynamically (for example OpenCode `/model`). +- **inherit** -- All agents use the current session model. Best when switching models dynamically (e.g. OpenCode `/model`), or **required** when using non-Anthropic providers (OpenRouter, local models) to avoid unexpected API costs. --- @@ -443,12 +449,14 @@ claude --dangerously-skip-permissions /gsd:plan-phase 1 # Research + plan + verify /gsd:execute-phase 1 # Parallel execution /gsd:verify-work 1 # Manual UAT +/gsd:ship 1 # Create PR from verified work /gsd:ui-review 1 # Visual audit (frontend phases) /clear -/gsd:discuss-phase 2 # Repeat for each phase +/gsd:next # Auto-detect and run next step ... /gsd:audit-milestone # Check everything shipped /gsd:complete-milestone # Archive, tag, done +/gsd:session-report # Generate session summary ``` ### New Project from Existing Document @@ -540,6 +548,10 @@ Do not re-run `/gsd:execute-phase`. Use `/gsd:quick` for targeted fixes, or `/gs Switch to budget profile: `/gsd:set-profile budget`. Disable research and plan-check agents via `/gsd:settings` if the domain is familiar to you (or to Claude). +### Using Non-Anthropic Models (OpenRouter, Local) + +If GSD subagents call Anthropic models and you're paying through OpenRouter or a local provider, switch to the `inherit` profile: `/gsd:set-profile inherit`. This makes all agents use your current session model instead of specific Anthropic models. See also `/gsd:settings` → Model Profile → Inherit. + ### Working on a Sensitive/Private Project Set `commit_docs: false` during `/gsd:new-project` or via `/gsd:settings`. Add `.planning/` to your `.gitignore`. Planning artifacts stay local and never touch git. @@ -567,6 +579,8 @@ A known workaround exists for a Claude Code classification bug. GSD's orchestrat | Plan doesn't match your vision | `/gsd:discuss-phase [N]` then re-plan | | Costs running high | `/gsd:set-profile budget` and `/gsd:settings` to toggle agents off | | Update broke local changes | `/gsd:reapply-patches` | +| Want session summary for stakeholder | `/gsd:session-report` | +| Don't know what step is next | `/gsd:next` | --- @@ -582,7 +596,9 @@ For reference, here is what GSD creates in your project: STATE.md # Decisions, blockers, session memory config.json # Workflow configuration MILESTONES.md # Completed milestone archive + HANDOFF.json # Structured session handoff (from /gsd:pause-work) research/ # Domain research from /gsd:new-project + reports/ # Session reports (from /gsd:session-report) todos/ pending/ # Captured ideas awaiting work done/ # Completed todos From 3e61c7da94bcdf465b4c2d7afc52346fb9532585 Mon Sep 17 00:00:00 2001 From: Eli Herman <50721369+eli-herman@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:49:27 -0500 Subject: [PATCH 48/83] feat(researcher): add Runtime State Inventory for rename/refactor phases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a Runtime State Inventory step that fires when a phase involves renaming, rebranding, refactoring, or migrating strings across a codebase. The core problem: grep audits find files. They do NOT find runtime state — ChromaDB collection names, Mem0 user_ids, n8n workflows in SQLite, Windows Task Scheduler descriptions, pm2 process names, SOPS key names, pip egg-info directories, etc. These survive a complete file-level rename and will break the system silently after the code is "done". Three additions: 1. Pre-Submission Checklist — adds a reminder item so the checklist gate catches skipped inventories before RESEARCH.md is committed 2. output_format — adds Runtime State Inventory table to the RESEARCH.md template so the section appears in every rename/refactor phase's output 3. execution_flow Step 2.5 — structured investigation protocol with the five categories (stored data, live service config, OS-registered state, secrets/env vars, build artifacts), explicit examples for each, and the canonical question that frames the whole exercise --- agents/gsd-phase-researcher.md | 39 ++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/agents/gsd-phase-researcher.md b/agents/gsd-phase-researcher.md index 7b897903c..1a767b9c8 100644 --- a/agents/gsd-phase-researcher.md +++ b/agents/gsd-phase-researcher.md @@ -194,6 +194,7 @@ Priority: Context7 > Official Docs > Official GitHub > Verified WebSearch > Unve - [ ] Publication dates checked (prefer recent/current) - [ ] Confidence levels assigned honestly - [ ] "What might I have missed?" review completed +- [ ] **If rename/refactor phase:** Runtime State Inventory completed — all 5 categories answered explicitly (not left blank) @@ -274,6 +275,20 @@ src/ **Key insight:** [why custom solutions are worse in this domain] +## Runtime State Inventory + +> Include this section for rename/refactor/migration phases only. Omit entirely for greenfield phases. + +| Category | Items Found | Action Required | +|----------|-------------|------------------| +| Stored data | [e.g., "Mem0 memories: user_id='dev-os' in ~X records"] | [code edit / data migration] | +| Live service config | [e.g., "25 n8n workflows in SQLite not exported to git"] | [API patch / manual] | +| OS-registered state | [e.g., "Windows Task Scheduler: 3 tasks with 'dev-os' in description"] | [re-register tasks] | +| Secrets/env vars | [e.g., "SOPS key 'webhook_auth_header' — code rename only, key unchanged"] | [none / update key] | +| Build artifacts | [e.g., "scripts/devos-cli/devos_cli.egg-info/ — stale after pyproject.toml rename"] | [reinstall package] | + +**Nothing found in category:** State explicitly ("None — verified by X"). + ## Common Pitfalls ### Pitfall 1: [Name] @@ -407,6 +422,26 @@ Based on phase description, identify what needs investigating: - **Pitfalls:** Common beginner mistakes, gotchas, rewrite-causing errors - **Don't Hand-Roll:** Existing solutions for deceptively complex problems +## Step 2.5: Runtime State Inventory (rename / refactor / migration phases only) + +**Trigger:** Any phase involving rename, rebrand, refactor, string replacement, or migration. + +A grep audit finds files. It does NOT find runtime state. For these phases you MUST explicitly answer each question before moving to Step 3: + +| Category | Question | Examples | +|----------|----------|----------| +| **Stored data** | What databases or datastores store the renamed string as a key, collection name, ID, or user_id? | ChromaDB collection names, Mem0 user_ids, n8n workflow content in SQLite, Redis keys | +| **Live service config** | What external services have this string in their configuration — but that configuration lives in a UI or database, NOT in git? | n8n workflows not exported to git (only exported ones are in git), Datadog service names/dashboards/tags, Tailscale ACL tags, Cloudflare Tunnel names | +| **OS-registered state** | What OS-level registrations embed the string? | Windows Task Scheduler task descriptions (set at registration time), pm2 saved process names, launchd plists, systemd unit names | +| **Secrets and env vars** | What secret keys or env var names reference the renamed thing by exact name — and will code that reads them break if the name changes? | SOPS key names, .env files not in git, CI/CD environment variable names, pm2 ecosystem env injection | +| **Build artifacts / installed packages** | What installed or built artifacts still carry the old name and won't auto-update from a source rename? | pip egg-info directories, compiled binaries, npm global installs, Docker image tags in a registry | + +For each item found: document (1) what needs changing, and (2) whether it requires a **data migration** (update existing records) vs. a **code edit** (change how new records are written). These are different tasks and must both appear in the plan. + +**The canonical question:** *After every file in the repo is updated, what runtime systems still have the old string cached, stored, or registered?* + +If the answer for a category is "nothing" — say so explicitly. Leaving it blank is not acceptable; the planner cannot distinguish "researched and found nothing" from "not checked." + ## Step 3: Execute Research Protocol For each domain: Context7 first → Official docs → WebSearch → Cross-verify. Document findings with confidence levels as you go. @@ -460,7 +495,7 @@ List missing test files, framework config, or shared fixtures needed before impl ## Phase Requirements | ID | Description | Research Support | -|----|-------------|-----------------| +|----|-------------|------------------| | {REQ-ID} | {from REQUIREMENTS.md} | {which research findings enable implementation} | ``` @@ -556,4 +591,4 @@ Quality indicators: - **Actionable:** Planner could create tasks based on this research - **Current:** Year included in searches, publication dates checked - + \ No newline at end of file From be302f02bb0d04ee013057d1f2f78067c24da380 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 17:07:33 -0400 Subject: [PATCH 49/83] feat: --analyze flag for discuss-phase trade-off analysis (#833) Adds --analyze flag to /gsd:discuss-phase that provides a trade-off analysis before each question (or question group in --batch mode). When active, each question is preceded by: - 2-3 options with pros/cons based on codebase context - A recommended approach with reasoning - Known pitfalls or constraints from prior phases Composable with existing flags: --batch --analyze gives grouped questions each with trade-off tables. Closes #833 --- commands/gsd/discuss-phase.md | 2 +- get-shit-done/workflows/discuss-phase.md | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/commands/gsd/discuss-phase.md b/commands/gsd/discuss-phase.md index b5c926021..75ebde603 100644 --- a/commands/gsd/discuss-phase.md +++ b/commands/gsd/discuss-phase.md @@ -1,7 +1,7 @@ --- name: gsd:discuss-phase description: Gather phase context through adaptive questioning before planning. Use --auto to skip interactive questions (Claude picks recommended defaults). -argument-hint: " [--auto]" +argument-hint: " [--auto] [--batch] [--analyze]" allowed-tools: - Read - Write diff --git a/get-shit-done/workflows/discuss-phase.md b/get-shit-done/workflows/discuss-phase.md index d8f7a6f40..89099fc13 100644 --- a/get-shit-done/workflows/discuss-phase.md +++ b/get-shit-done/workflows/discuss-phase.md @@ -406,6 +406,30 @@ For each selected area, conduct a focused discussion loop. **Batch mode support:** Parse optional `--batch` from `$ARGUMENTS`. - Accept `--batch`, `--batch=N`, or `--batch N` + +**Analyze mode support:** Parse optional `--analyze` from `$ARGUMENTS`. +When `--analyze` is active, before presenting each question (or question group in batch mode), provide a brief **trade-off analysis** for the decision: +- 2-3 options with pros/cons based on codebase context and common patterns +- A recommended approach with reasoning +- Known pitfalls or constraints from prior phases + +Example with `--analyze`: +``` +**Trade-off analysis: Authentication strategy** + +| Approach | Pros | Cons | +|----------|------|------| +| Session cookies | Simple, httpOnly prevents XSS | Requires CSRF protection, sticky sessions | +| JWT (stateless) | Scalable, no server state | Token size, revocation complexity | +| OAuth 2.0 + PKCE | Industry standard for SPAs | More setup, redirect flow UX | + +💡 Recommended: OAuth 2.0 + PKCE — your app has social login in requirements (REQ-04) and this aligns with the existing NextAuth setup in `src/lib/auth.ts`. + +How should users authenticate? +``` + +This gives the user context to make informed decisions without extra prompting. When `--analyze` is absent, present questions directly as before. +- Accept `--batch`, `--batch=N`, or `--batch N` - Default to 4 questions per batch when no number is provided - Clamp explicit sizes to 2-5 so a batch stays answerable - If `--batch` is absent, keep the existing one-question-at-a-time flow From 297adf842508ae5ebcc556d18cc81785def17a06 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 17:13:55 -0400 Subject: [PATCH 50/83] fix: add Windows troubleshooting for plan-phase freezes (#732) Windows users experience plan-phase freezes due to Claude Code stdio deadlocks with MCP servers. When a subagent hangs, the orchestrator blocks indefinitely with no timeout or error. Adds: - Windows troubleshooting section to plan-phase.md with: - Orphaned process cleanup (PowerShell commands) - Stale task directory cleanup (~/.claude/tasks/) - MCP server reduction advice - --skip-research fallback to reduce agent chain - Stale task directory detection to health.md (I002 diagnostic) - Reports count of stale dirs on --repair - Safe cleanup guidance Closes #732 --- get-shit-done/workflows/health.md | 22 ++++++++++++++++++++++ get-shit-done/workflows/plan-phase.md | 24 ++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/get-shit-done/workflows/health.md b/get-shit-done/workflows/health.md index 54b7a1423..0cb95fd1b 100644 --- a/get-shit-done/workflows/health.md +++ b/get-shit-done/workflows/health.md @@ -157,3 +157,25 @@ Report final status. - Orphaned plan cleanup + + +**Windows-specific:** Check for stale Claude Code task directories that accumulate on crash/freeze. +These are left behind when subagents are force-killed and consume disk space. + +When `--repair` is active, detect and clean up: + +```bash +# Check for stale task directories (older than 24 hours) +TASKS_DIR="$HOME/.claude/tasks" +if [ -d "$TASKS_DIR" ]; then + STALE_COUNT=$(find "$TASKS_DIR" -maxdepth 1 -type d -mtime +1 2>/dev/null | wc -l) + if [ "$STALE_COUNT" -gt 0 ]; then + echo "⚠️ Found $STALE_COUNT stale task directories in ~/.claude/tasks/" + echo " These are leftover from crashed subagent sessions." + echo " Run: rm -rf ~/.claude/tasks/* (safe — only affects dead sessions)" + fi +fi +``` + +Report as info diagnostic: `I002 | info | Stale subagent task directories found | Yes (--repair removes them)` + diff --git a/get-shit-done/workflows/plan-phase.md b/get-shit-done/workflows/plan-phase.md index 2d3291533..26697dc3e 100644 --- a/get-shit-done/workflows/plan-phase.md +++ b/get-shit-done/workflows/plan-phase.md @@ -737,6 +737,30 @@ Verification: {Passed | Passed with override | Skipped} ─────────────────────────────────────────────────────────────── + +**Windows users:** If plan-phase freezes during agent spawning (common on Windows due to +stdio deadlocks with MCP servers — see Claude Code issue anthropics/claude-code#28126): + +1. **Force-kill:** Close the terminal (Ctrl+C may not work) +2. **Clean up orphaned processes:** + ```powershell + # Kill orphaned node processes from stale MCP servers + Get-Process node -ErrorAction SilentlyContinue | Where-Object {$_.StartTime -lt (Get-Date).AddHours(-1)} | Stop-Process -Force + ``` +3. **Clean up stale task directories:** + ```powershell + # Remove stale subagent task dirs (Claude Code never cleans these on crash) + Remove-Item -Recurse -Force "$env:USERPROFILE\.claude\tasks\*" -ErrorAction SilentlyContinue + ``` +4. **Reduce MCP server count:** Temporarily disable non-essential MCP servers in settings.json +5. **Retry:** Restart Claude Code and run `/gsd:plan-phase` again + +If freezes persist, try `--skip-research` to reduce the agent chain from 3 to 2 agents: +``` +/gsd:plan-phase N --skip-research +``` + + - [ ] .planning/ directory validated - [ ] Phase validated against roadmap From a99caaeb59cf8ffc66bda9ff3c975fa0f62311e3 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 17:04:36 -0400 Subject: [PATCH 51/83] feat: /gsd:fast command for trivial inline tasks (#609) New lightweight command for tasks too small to justify planning overhead: typo fixes, config changes, forgotten commits, simple additions. - Runs inline in current context (no subagent spawn) - No PLAN.md or SUMMARY.md generated - Scope guard: redirects to /gsd:quick if task needs >3 file edits - Atomic commit with conventional commit format - Logs to STATE.md quick tasks table if present Complements /gsd:quick (which spawns planner+executor) for cases where the planning overhead exceeds the actual work. Closes #609 --- commands/gsd/fast.md | 30 +++++++++ get-shit-done/workflows/fast.md | 105 ++++++++++++++++++++++++++++++++ get-shit-done/workflows/help.md | 15 +++++ tests/copilot-install.test.cjs | 4 +- 4 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 commands/gsd/fast.md create mode 100644 get-shit-done/workflows/fast.md diff --git a/commands/gsd/fast.md b/commands/gsd/fast.md new file mode 100644 index 000000000..4121f9f75 --- /dev/null +++ b/commands/gsd/fast.md @@ -0,0 +1,30 @@ +--- +name: gsd:fast +description: Execute a trivial task inline — no subagents, no planning overhead +argument-hint: "[task description]" +allowed-tools: + - Read + - Write + - Edit + - Bash + - Grep + - Glob +--- + + +Execute a trivial task directly in the current context without spawning subagents +or generating PLAN.md files. For tasks too small to justify planning overhead: +typo fixes, config changes, small refactors, forgotten commits, simple additions. + +This is NOT a replacement for /gsd:quick — use /gsd:quick for anything that +needs research, multi-step planning, or verification. /gsd:fast is for tasks +you could describe in one sentence and execute in under 2 minutes. + + + +@~/.claude/get-shit-done/workflows/fast.md + + + +Execute the fast workflow from @~/.claude/get-shit-done/workflows/fast.md end-to-end. + diff --git a/get-shit-done/workflows/fast.md b/get-shit-done/workflows/fast.md new file mode 100644 index 000000000..729bcc32e --- /dev/null +++ b/get-shit-done/workflows/fast.md @@ -0,0 +1,105 @@ + +Execute a trivial task inline without subagent overhead. No PLAN.md, no Task spawning, +no research, no plan checking. Just: understand → do → commit → log. + +For tasks like: fix a typo, update a config value, add a missing import, rename a +variable, commit uncommitted work, add a .gitignore entry, bump a version number. + +Use /gsd:quick for anything that needs multi-step planning or research. + + + + + +Parse `$ARGUMENTS` for the task description. + +If empty, ask: +``` +What's the quick fix? (one sentence) +``` + +Store as `$TASK`. + + + +**Before doing anything, verify this is actually trivial.** + +A task is trivial if it can be completed in: +- ≤ 3 file edits +- ≤ 1 minute of work +- No new dependencies or architecture changes +- No research needed + +If the task seems non-trivial (multi-file refactor, new feature, needs research), +say: + +``` +This looks like it needs planning. Use /gsd:quick instead: + /gsd:quick "{task description}" +``` + +And stop. + + + +Do the work directly: + +1. Read the relevant file(s) +2. Make the change(s) +3. Verify the change works (run existing tests if applicable, or do a quick sanity check) + +**No PLAN.md.** Just do it. + + + +Commit the change atomically: + +```bash +git add -A +git commit -m "fix: {concise description of what changed}" +``` + +Use conventional commit format: `fix:`, `feat:`, `docs:`, `chore:`, `refactor:` as appropriate. + + + +If `.planning/STATE.md` exists, append to the "Quick Tasks Completed" table. +If the table doesn't exist, skip this step silently. + +```bash +# Check if STATE.md has quick tasks table +if grep -q "Quick Tasks Completed" .planning/STATE.md 2>/dev/null; then + # Append entry — workflow handles the format + echo "| $(date +%Y-%m-%d) | fast | $TASK | ✅ |" >> .planning/STATE.md +fi +``` + + + +Report completion: + +``` +✅ Done: {what was changed} + Commit: {short hash} + Files: {list of changed files} +``` + +No next-step suggestions. No workflow routing. Just done. + + + + + +- NEVER spawn a Task/subagent — this runs inline +- NEVER create PLAN.md or SUMMARY.md files +- NEVER run research or plan-checking +- If the task takes more than 3 file edits, STOP and redirect to /gsd:quick +- If you're unsure how to implement it, STOP and redirect to /gsd:quick + + + +- [ ] Task completed in current context (no subagents) +- [ ] Atomic git commit with conventional message +- [ ] STATE.md updated if it exists +- [ ] Total operation under 2 minutes wall time + diff --git a/get-shit-done/workflows/help.md b/get-shit-done/workflows/help.md index e32ea0d25..cdd60638e 100644 --- a/get-shit-done/workflows/help.md +++ b/get-shit-done/workflows/help.md @@ -151,6 +151,21 @@ Usage: `/gsd:quick` Usage: `/gsd:quick --research --full` Result: Creates `.planning/quick/NNN-slug/PLAN.md`, `.planning/quick/NNN-slug/SUMMARY.md` +--- + +**`/gsd:fast [description]`** +Execute a trivial task inline — no subagents, no planning files, no overhead. + +For tasks too small to justify planning: typo fixes, config changes, forgotten commits, simple additions. Runs in the current context, makes the change, commits, and logs to STATE.md. + +- No PLAN.md or SUMMARY.md created +- No subagent spawned (runs inline) +- ≤ 3 file edits — redirects to `/gsd:quick` if task is non-trivial +- Atomic commit with conventional message + +Usage: `/gsd:fast "fix the typo in README"` +Usage: `/gsd:fast "add .env to gitignore"` + ### Roadmap Management **`/gsd:add-phase `** diff --git a/tests/copilot-install.test.cjs b/tests/copilot-install.test.cjs index ea79e411d..b6510d0e7 100644 --- a/tests/copilot-install.test.cjs +++ b/tests/copilot-install.test.cjs @@ -625,7 +625,7 @@ describe('copyCommandsAsCopilotSkills', () => { // Count gsd-* directories — should be 31 const dirs = fs.readdirSync(tempDir, { withFileTypes: true }) .filter(e => e.isDirectory() && e.name.startsWith('gsd-')); - assert.strictEqual(dirs.length, 42, `expected 42 skill folders, got ${dirs.length}`); + assert.strictEqual(dirs.length, 43, `expected 43 skill folders, got ${dirs.length}`); } finally { fs.rmSync(tempDir, { recursive: true }); } @@ -1119,7 +1119,7 @@ const { execFileSync } = require('child_process'); const crypto = require('crypto'); const INSTALL_PATH = path.join(__dirname, '..', 'bin', 'install.js'); -const EXPECTED_SKILLS = 42; +const EXPECTED_SKILLS = 43; const EXPECTED_AGENTS = 16; function runCopilotInstall(cwd) { From 3bfc0f68455c9043cecd7db855542b898f478c2f Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 16 Mar 2026 20:31:45 -0400 Subject: [PATCH 52/83] fix: add --no-verify support for parallel executor commits (#1116) Parallel executor agents trigger pre-commit hooks on every commit, causing build lock contention (e.g., cargo lock fights in Rust projects) and 40+ minute delays from cascading retries. Changes: - Add --no-verify flag to gsd-tools commit command (commands.cjs) - Wire --no-verify through gsd-tools.cjs CLI parser - Update execute-phase.md to instruct parallel agents to use --no-verify - Add post-wave hook validation step so orchestrator runs hooks once - Update execute-plan.md pre-commit handling: parallel agents skip hooks, sequential agents still handle hooks normally - Add parallel agent note to git-integration.md reference Fixes #1116 --- get-shit-done/bin/gsd-tools.cjs | 5 +++-- get-shit-done/bin/lib/commands.cjs | 5 +++-- get-shit-done/references/git-integration.md | 4 ++++ get-shit-done/workflows/execute-phase.md | 20 +++++++++++++++++++- get-shit-done/workflows/execute-plan.md | 8 +++++--- 5 files changed, 34 insertions(+), 8 deletions(-) diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index cdee42566..02915d58b 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -19,7 +19,7 @@ * state signal-resume Remove WAITING.json signal * resolve-model Get model for agent based on profile * find-phase Find phase directory by number - * commit [--files f1 f2] Commit planning docs + * commit [--files f1 f2] [--no-verify] Commit planning docs * verify-summary Verify a SUMMARY.md file * generate-slug Convert text to URL-safe slug * current-timestamp [format] Get timestamp (full|date|filename) @@ -290,6 +290,7 @@ async function main() { case 'commit': { const amend = args.includes('--amend'); + const noVerify = args.includes('--no-verify'); const filesIndex = args.indexOf('--files'); // Collect all positional args between command name and first flag, // then join them — handles both quoted ("multi word msg") and @@ -298,7 +299,7 @@ async function main() { const messageArgs = args.slice(1, endIndex).filter(a => !a.startsWith('--')); const message = messageArgs.join(' ') || undefined; const files = filesIndex !== -1 ? args.slice(filesIndex + 1).filter(a => !a.startsWith('--')) : []; - commands.cmdCommit(cwd, message, files, raw, amend); + commands.cmdCommit(cwd, message, files, raw, amend, noVerify); break; } diff --git a/get-shit-done/bin/lib/commands.cjs b/get-shit-done/bin/lib/commands.cjs index d7109df19..b7416a1b3 100644 --- a/get-shit-done/bin/lib/commands.cjs +++ b/get-shit-done/bin/lib/commands.cjs @@ -214,7 +214,7 @@ function cmdResolveModel(cwd, agentType, raw) { output(result, raw, model); } -function cmdCommit(cwd, message, files, raw, amend) { +function cmdCommit(cwd, message, files, raw, amend, noVerify) { if (!message && !amend) { error('commit message required'); } @@ -241,8 +241,9 @@ function cmdCommit(cwd, message, files, raw, amend) { execGit(cwd, ['add', file]); } - // Commit + // Commit (--no-verify skips pre-commit hooks, used by parallel executor agents) const commitArgs = amend ? ['commit', '--amend', '--no-edit'] : ['commit', '-m', message]; + if (noVerify) commitArgs.push('--no-verify'); const commitResult = execGit(cwd, commitArgs); if (commitResult.exitCode !== 0) { if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) { diff --git a/get-shit-done/references/git-integration.md b/get-shit-done/references/git-integration.md index 1e0a9d1b4..d9bbecac2 100644 --- a/get-shit-done/references/git-integration.md +++ b/get-shit-done/references/git-integration.md @@ -61,6 +61,10 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: initialize [p Each task gets its own commit immediately after completion. +> **Parallel agents:** When running as a parallel executor (spawned by execute-phase), +> use `--no-verify` on all commits to avoid pre-commit hook lock contention. +> The orchestrator validates hooks once after all agents complete. + ``` {type}({phase}-{plan}): {task-name} diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index e5c6afe4d..e6e855398 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -204,6 +204,14 @@ Execute each wave in sequence. Within a wave: parallel if `PARALLELIZATION=true` Commit each task atomically. Create SUMMARY.md. Update STATE.md and ROADMAP.md. + + 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 + orchestrator validates hooks once after all agents complete. + For gsd-tools commits: add --no-verify flag. + For direct git commits: use git commit --no-verify -m "..." + + @~/.claude/get-shit-done/workflows/execute-plan.md @~/.claude/get-shit-done/templates/summary.md @@ -240,7 +248,17 @@ Execute each wave in sequence. Within a wave: parallel if `PARALLELIZATION=true` 3. **Wait for all agents in wave to complete.** -4. **Report completion — spot-check claims first:** +4. **Post-wave hook validation (parallel mode only):** + + When agents committed with `--no-verify`, run pre-commit hooks once after the wave: + ```bash + # Run project's pre-commit hooks on the current state + git diff --cached --quiet || git stash # stash any unstaged changes + git hook run pre-commit 2>&1 || echo "⚠ Pre-commit hooks failed — review before continuing" + ``` + If hooks fail: report the failure and ask "Fix hook issues now?" or "Continue to next wave?" + +5. **Report completion — spot-check claims first:** For each SUMMARY.md: - Verify first 2 files from `key-files.created` exist on disk diff --git a/get-shit-done/workflows/execute-plan.md b/get-shit-done/workflows/execute-plan.md index e64dec825..0e5d8e8a5 100644 --- a/get-shit-done/workflows/execute-plan.md +++ b/get-shit-done/workflows/execute-plan.md @@ -234,6 +234,10 @@ See `~/.claude/get-shit-done/references/tdd.md` for structure. Your commits may trigger pre-commit hooks. Auto-fix hooks handle themselves transparently — files get fixed and re-staged automatically. +**If running as a parallel executor agent (spawned by execute-phase):** +Use `--no-verify` on all commits. Pre-commit hooks cause build lock contention when multiple agents commit simultaneously (e.g., cargo lock fights in Rust projects). The orchestrator validates once after all agents complete. + +**If running as the sole executor (sequential mode):** If a commit is BLOCKED by a hook: 1. The `git commit` command fails with hook error output @@ -241,9 +245,7 @@ If a commit is BLOCKED by a hook: 3. Fix the issue (type error, lint violation, secret leak, etc.) 4. `git add` the fixed files 5. Retry the commit -6. Do NOT use `--no-verify` - -This is normal and expected. Budget 1-2 retry cycles per commit. +6. Budget 1-2 retry cycles per commit From 60f38cdb9eaeb11ee2b0ee7bce5015b0e418c21d Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 16 Mar 2026 20:37:58 -0400 Subject: [PATCH 53/83] fix: remove duplicate stateExtractField, cross-platform code detection, STATE.md file locking Code review fixes from codebase pattern analysis: 1. state.cjs: Remove duplicate stateExtractField() definition (lines 12 vs 184). The second definition shadowed the first with identical logic. Keeps the original that uses escapeRegex() from core.cjs. 2. init.cjs: Replace Unix-only 'find' pipe with cross-platform fs.readdirSync recursive walk for code file detection. The execSync('find ... | grep ...') command fails on Windows where these Unix utilities aren't available. Removes unused child_process import. 3. state.cjs: Add lockfile-based mutual exclusion to writeStateMd() to prevent parallel executor agents from overwriting each other's STATE.md changes. Uses O_EXCL atomic file creation for lock acquisition, stale lock detection (10s timeout), and spin-wait with jitter. Ensures data integrity during wave-based parallel execution where multiple agents update STATE.md concurrently. All 755 existing tests pass. --- get-shit-done/bin/lib/init.cjs | 25 +++++++++----- get-shit-done/bin/lib/state.cjs | 59 +++++++++++++++++++++++++-------- 2 files changed, 63 insertions(+), 21 deletions(-) diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index 9c9d35655..bcac25560 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -167,17 +167,26 @@ function cmdInitNewProject(cwd, raw) { const braveKeyFile = path.join(homedir, '.gsd', 'brave_api_key'); const hasBraveSearch = !!(process.env.BRAVE_API_KEY || fs.existsSync(braveKeyFile)); - // Detect existing code + // Detect existing code (cross-platform — no Unix `find` dependency) let hasCode = false; let hasPackageFile = false; try { - const files = execSync('find . -maxdepth 3 \\( -name "*.ts" -o -name "*.js" -o -name "*.py" -o -name "*.go" -o -name "*.rs" -o -name "*.swift" -o -name "*.java" \\) 2>/dev/null | grep -v node_modules | grep -v .git | head -5', { - cwd, - encoding: 'utf-8', - stdio: ['pipe', 'pipe', 'pipe'], - }); - hasCode = files.trim().length > 0; - } catch {} + const codeExtensions = new Set(['.ts', '.js', '.py', '.go', '.rs', '.swift', '.java']); + const skipDirs = new Set(['node_modules', '.git', '.planning', '.claude', '__pycache__', 'target', 'dist', 'build']); + function findCodeFiles(dir, depth) { + if (depth > 3) return false; + let entries; + try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return false; } + for (const entry of entries) { + if (entry.isFile() && codeExtensions.has(path.extname(entry.name))) return true; + if (entry.isDirectory() && !skipDirs.has(entry.name)) { + if (findCodeFiles(path.join(dir, entry.name), depth + 1)) return true; + } + } + return false; + } + hasCode = findCodeFiles(cwd, 0); + } catch { /* intentionally empty — best-effort detection */ } hasPackageFile = pathExistsInternal(cwd, 'package.json') || pathExistsInternal(cwd, 'requirements.txt') || diff --git a/get-shit-done/bin/lib/state.cjs b/get-shit-done/bin/lib/state.cjs index 78797694a..ca6d1f40f 100644 --- a/get-shit-done/bin/lib/state.cjs +++ b/get-shit-done/bin/lib/state.cjs @@ -180,18 +180,7 @@ function cmdStateUpdate(cwd, field, value) { } // ─── State Progression Engine ──────────────────────────────────────────────── - -function stateExtractField(content, fieldName) { - const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - // Try **Field:** bold format first - const boldPattern = new RegExp(`\\*\\*${escaped}:\\*\\*\\s*(.+)`, 'i'); - const boldMatch = content.match(boldPattern); - if (boldMatch) return boldMatch[1].trim(); - // Fall back to plain Field: format - const plainPattern = new RegExp(`^${escaped}:\\s*(.+)`, 'im'); - const plainMatch = content.match(plainPattern); - return plainMatch ? plainMatch[1].trim() : null; -} +// stateExtractField is defined above (shared helper) — do not duplicate. function stateReplaceField(content, fieldName, newValue) { const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); @@ -677,10 +666,54 @@ function syncStateFrontmatter(content, cwd) { /** * Write STATE.md with synchronized YAML frontmatter. * All STATE.md writes should use this instead of raw writeFileSync. + * Uses a simple lockfile to prevent parallel agents from overwriting + * each other's changes (race condition with read-modify-write cycle). */ function writeStateMd(statePath, content, cwd) { const synced = syncStateFrontmatter(content, cwd); - fs.writeFileSync(statePath, normalizeMd(synced), 'utf-8'); + const lockPath = statePath + '.lock'; + const maxRetries = 10; + const retryDelay = 200; // ms + + // Acquire lock (spin with backoff) + for (let i = 0; i < maxRetries; i++) { + try { + // O_EXCL fails if file already exists — atomic lock + const fd = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY); + fs.writeSync(fd, String(process.pid)); + fs.closeSync(fd); + break; + } catch (err) { + if (err.code === 'EEXIST') { + // Check for stale lock (> 10s old) + try { + const stat = fs.statSync(lockPath); + if (Date.now() - stat.mtimeMs > 10000) { + fs.unlinkSync(lockPath); + continue; // retry immediately after clearing stale lock + } + } catch { /* lock was released between check — retry */ } + + if (i === maxRetries - 1) { + // Last resort: write anyway rather than losing data + try { fs.unlinkSync(lockPath); } catch {} + break; + } + // Spin-wait with small jitter + const jitter = Math.floor(Math.random() * 50); + const start = Date.now(); + while (Date.now() - start < retryDelay + jitter) { /* busy wait */ } + continue; + } + break; // non-EEXIST error — proceed without lock + } + } + + try { + fs.writeFileSync(statePath, normalizeMd(synced), 'utf-8'); + } finally { + try { fs.unlinkSync(lockPath); } catch { /* lock already gone */ } + } } function cmdStateJson(cwd, raw) { From 214a621cb2e6242237546be03a21a776080fa37d Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 16 Mar 2026 20:48:02 -0400 Subject: [PATCH 54/83] docs: update changelog, architecture, CLI tools, config, features, and user guide for parallel execution fixes Documentation updates for #1116 fixes and code review findings: - CHANGELOG.md: Add --no-verify commit flag, post-wave hook validation, STATE.md file locking, duplicate function removal, cross-platform init - docs/ARCHITECTURE.md: Add 'Parallel Commit Safety' section explaining --no-verify strategy and STATE.md lockfile mechanism - docs/CLI-TOOLS.md: Document --no-verify flag on commit command with usage guidance - docs/CONFIGURATION.md: Add note about pre-commit hooks and parallel execution behavior under parallelization settings - docs/FEATURES.md: Add --no-verify to executor capabilities, add 'Parallel Safety' section - docs/USER-GUIDE.md: Add troubleshooting entries for parallel execution build lock errors and Windows EPERM crashes, update recovery table --- docs/ARCHITECTURE.md | 8 ++++++++ docs/CLI-TOOLS.md | 5 ++++- docs/CONFIGURATION.md | 2 ++ docs/FEATURES.md | 5 +++++ docs/USER-GUIDE.md | 16 ++++++++++++++++ 5 files changed, 35 insertions(+), 1 deletion(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 0163b0cc0..470df6326 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -247,6 +247,14 @@ Each executor gets: - Project context (PROJECT.md, STATE.md) - Phase context (CONTEXT.md, RESEARCH.md if available) +#### Parallel Commit Safety + +When multiple executors run within the same wave, two mechanisms prevent conflicts: + +1. **`--no-verify` commits** — Parallel agents skip pre-commit hooks (which can cause build lock contention, e.g., cargo lock fights in Rust projects). The orchestrator runs `git hook run pre-commit` once after each wave completes. + +2. **STATE.md file locking** — All `writeStateMd()` calls use lockfile-based mutual exclusion (`STATE.md.lock` with `O_EXCL` atomic creation). This prevents the read-modify-write race condition where two agents read STATE.md, modify different fields, and the last writer overwrites the other's changes. Includes stale lock detection (10s timeout) and spin-wait with jitter. + --- ## Data Flow diff --git a/docs/CLI-TOOLS.md b/docs/CLI-TOOLS.md index f0228496b..2621e7769 100644 --- a/docs/CLI-TOOLS.md +++ b/docs/CLI-TOOLS.md @@ -331,7 +331,10 @@ node gsd-tools.cjs progress [json|table|bar] node gsd-tools.cjs todo complete # Git commit with config checks -node gsd-tools.cjs commit [--files f1 f2] [--amend] +node gsd-tools.cjs commit [--files f1 f2] [--amend] [--no-verify] +``` + +> **`--no-verify`**: Skips pre-commit hooks. Used by parallel executor agents during wave-based execution to avoid build lock contention (e.g., cargo lock fights in Rust projects). The orchestrator runs hooks once after each wave completes. Do not use `--no-verify` during sequential execution — let hooks run normally. # Web search (requires Brave API key) node gsd-tools.cjs websearch [--limit N] [--freshness day|week|month] diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index bc750d3c4..ce991803b 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -133,6 +133,8 @@ To keep planning artifacts out of git: | `parallelization.max_concurrent_agents` | number | `3` | Maximum simultaneous agents | | `parallelization.min_plans_for_parallel` | number | `2` | Minimum plans to trigger parallel execution | +> **Pre-commit hooks and parallel execution**: When parallelization is enabled, executor agents commit with `--no-verify` to avoid build lock contention (e.g., cargo lock fights in Rust projects). The orchestrator validates hooks once after each wave completes. STATE.md writes are protected by file-level locking to prevent concurrent write corruption. If you need hooks to run per-commit, set `parallelization.enabled: false`. + --- ## Git Branching diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 5ece01d3a..b40484c3d 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -245,9 +245,14 @@ - Reads PLAN.md with full task instructions - Has access to PROJECT.md, STATE.md, CONTEXT.md, RESEARCH.md - Commits each task atomically with structured commit messages +- Uses `--no-verify` on commits during parallel execution to avoid build lock contention - Handles checkpoint types: `auto`, `checkpoint:human-verify`, `checkpoint:decision`, `checkpoint:human-action` - Reports deviations from plan in SUMMARY.md +**Parallel Safety:** +- **Pre-commit hooks**: Skipped by parallel agents (`--no-verify`), run once by orchestrator after each wave +- **STATE.md locking**: File-level lockfile prevents concurrent write corruption across agents + --- ### 6. Work Verification diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index 36253a1fc..948fa9899 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -564,6 +564,21 @@ Since v1.17, the installer backs up locally modified files to `gsd-local-patches A known workaround exists for a Claude Code classification bug. GSD's orchestrators (execute-phase, quick) spot-check actual output before reporting failure. If you see a failure message but commits were made, check `git log` -- the work may have succeeded. +### Parallel Execution Causes Build Lock Errors + +If you see pre-commit hook failures, cargo lock contention, or 30+ minute execution times during parallel wave execution, this is caused by multiple agents triggering build tools simultaneously. GSD handles this automatically since v1.26 — parallel agents use `--no-verify` on commits and the orchestrator runs hooks once after each wave. If you're on an older version, add this to your project's `CLAUDE.md`: + +```markdown +## Git Commit Rules for Agents +All subagent/executor commits MUST use `--no-verify`. +``` + +To disable parallel execution entirely: `/gsd:settings` → set `parallelization.enabled` to `false`. + +### Windows: Installation Crashes on Protected Directories + +If the installer crashes with `EPERM: operation not permitted, scandir` on Windows, this is caused by OS-protected directories (e.g., Chromium browser profiles). Fixed since v1.24 — update to the latest version. As a workaround, temporarily rename the problematic directory before running the installer. + --- ## Recovery Quick Reference @@ -581,6 +596,7 @@ A known workaround exists for a Claude Code classification bug. GSD's orchestrat | Update broke local changes | `/gsd:reapply-patches` | | Want session summary for stakeholder | `/gsd:session-report` | | Don't know what step is next | `/gsd:next` | +| Parallel execution build errors | Update GSD or set `parallelization.enabled: false` | --- From a6ba3e268e5fca4aa3112fa053451cbc88d31d5e Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 17:36:07 -0400 Subject: [PATCH 55/83] feat: PreToolUse workflow guard hook for rogue edit prevention (#678) (#1197) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New opt-in PreToolUse hook that warns when Claude edits files outside a GSD workflow context (no active /gsd: command or subagent). Soft guard — advises, does not block. The edit proceeds but Claude sees a reminder to use /gsd:fast or /gsd:quick for state tracking. Enable: set hooks.workflow_guard: true in .planning/config.json Default: disabled (false) Allows without warning: - .planning/ files (GSD state management) - Config files (.gitignore, .env, CLAUDE.md, settings.json) - Subagent contexts (executor, planner, etc.) Includes 3s stdin timeout guard and silent fail-safe. Closes #678 --- get-shit-done/workflows/settings.md | 3 +- hooks/gsd-workflow-guard.js | 93 +++++++++++++++++++++++++++++ scripts/build-hooks.js | 3 +- 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 hooks/gsd-workflow-guard.js diff --git a/get-shit-done/workflows/settings.md b/get-shit-done/workflows/settings.md index b540cff61..760056918 100644 --- a/get-shit-done/workflows/settings.md +++ b/get-shit-done/workflows/settings.md @@ -160,7 +160,8 @@ Merge new settings into existing config.json: "branching_strategy": "none" | "phase" | "milestone" }, "hooks": { - "context_warnings": true/false + "context_warnings": true/false, + "workflow_guard": true/false } } ``` diff --git a/hooks/gsd-workflow-guard.js b/hooks/gsd-workflow-guard.js new file mode 100644 index 000000000..d8075aaf6 --- /dev/null +++ b/hooks/gsd-workflow-guard.js @@ -0,0 +1,93 @@ +#!/usr/bin/env node +// GSD Workflow Guard — PreToolUse hook +// Detects when Claude attempts file edits outside a GSD workflow context +// (no active /gsd: command or Task subagent) and injects an advisory warning. +// +// This is a SOFT guard — it advises, not blocks. The edit still proceeds. +// The warning nudges Claude to use /gsd:quick or /gsd:fast instead of +// making direct edits that bypass state tracking. +// +// Enable via config: hooks.workflow_guard: true (default: false) +// Only triggers on Write/Edit tool calls to non-.planning/ files. + +const fs = require('fs'); +const path = require('path'); + +let input = ''; +const stdinTimeout = setTimeout(() => process.exit(0), 3000); +process.stdin.setEncoding('utf8'); +process.stdin.on('data', chunk => input += chunk); +process.stdin.on('end', () => { + clearTimeout(stdinTimeout); + try { + const data = JSON.parse(input); + const toolName = data.tool_name; + + // Only guard Write and Edit tool calls + if (toolName !== 'Write' && toolName !== 'Edit') { + process.exit(0); + } + + // Check if we're inside a GSD workflow (Task subagent or /gsd: command) + // Subagents have a session_id that differs from the parent + // and typically have a description field set by the orchestrator + if (data.tool_input?.is_subagent || data.session_type === 'task') { + process.exit(0); + } + + // Check the file being edited + const filePath = data.tool_input?.file_path || data.tool_input?.path || ''; + + // Allow edits to .planning/ files (GSD state management) + if (filePath.includes('.planning/') || filePath.includes('.planning\\')) { + process.exit(0); + } + + // Allow edits to common config/docs files that don't need GSD tracking + const allowedPatterns = [ + /\.gitignore$/, + /\.env/, + /CLAUDE\.md$/, + /AGENTS\.md$/, + /GEMINI\.md$/, + /settings\.json$/, + ]; + if (allowedPatterns.some(p => p.test(filePath))) { + process.exit(0); + } + + // Check if workflow guard is enabled + const cwd = data.cwd || process.cwd(); + const configPath = path.join(cwd, '.planning', 'config.json'); + if (fs.existsSync(configPath)) { + try { + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + if (!config.hooks?.workflow_guard) { + process.exit(0); // Guard disabled (default) + } + } catch (e) { + process.exit(0); + } + } else { + process.exit(0); // No GSD project — don't guard + } + + // If we get here: GSD project, guard enabled, file edit outside .planning/, + // not in a subagent context. Inject advisory warning. + const output = { + hookSpecificOutput: { + hookEventName: "PreToolUse", + additionalContext: `⚠️ WORKFLOW ADVISORY: You're editing ${path.basename(filePath)} directly without a GSD command. ` + + 'This edit will not be tracked in STATE.md or produce a SUMMARY.md. ' + + 'Consider using /gsd:fast for trivial fixes or /gsd:quick for larger changes ' + + 'to maintain project state tracking. ' + + 'If this is intentional (e.g., user explicitly asked for a direct edit), proceed normally.' + } + }; + + process.stdout.write(JSON.stringify(output)); + } catch (e) { + // Silent fail — never block tool execution + process.exit(0); + } +}); diff --git a/scripts/build-hooks.js b/scripts/build-hooks.js index dda263e4e..b1b8fa416 100644 --- a/scripts/build-hooks.js +++ b/scripts/build-hooks.js @@ -17,7 +17,8 @@ const DIST_DIR = path.join(HOOKS_DIR, 'dist'); const HOOKS_TO_COPY = [ 'gsd-check-update.js', 'gsd-context-monitor.js', - 'gsd-statusline.js' + 'gsd-statusline.js', + 'gsd-workflow-guard.js' ]; /** From 5fd384f3364ee4e967919a5a770e7f222a152448 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 19:20:58 -0400 Subject: [PATCH 56/83] fix: universal agent name replacement for non-Claude runtimes (#766) (#1195) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: universal agent name replacement for non-Claude runtimes (#766) Adds neutralizeAgentReferences() shared function that all non-Claude runtime converters call to replace Claude-specific references: - 'Claude' (standalone agent name) → 'the agent' - 'CLAUDE.md' → runtime-specific file (AGENTS.md, GEMINI.md, COPILOT.md) - Removes 'Do NOT load full AGENTS.md' (harmful for AGENTS.md runtimes) Preserves: 'Claude Code' (product), 'Claude Opus/Sonnet/Haiku' (models), 'claude-' prefixes (packages, CSS classes). Integrated into: OpenCode, Gemini, Copilot, Antigravity, and Codex converters. Claude Code converter unchanged (references are correct). Includes 7 new tests covering all replacement rules. Closes #766 * fix: use copilot-instructions.md instead of COPILOT.md for Copilot runtime Addresses review from Solvely-Colin: Copilot's actual instruction file is copilot-instructions.md, not COPILOT.md. The neutralizer was mapping CLAUDE.md -> COPILOT.md which would reference a non-existent file. - install.js: pass 'copilot-instructions.md' to neutralizeAgentReferences - runtime-converters.test.cjs: update test to validate correct filename --- bin/install.js | 43 +++++++++++++++++++++++- tests/runtime-converters.test.cjs | 54 +++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/bin/install.js b/bin/install.js index 62792da60..93d909ed1 100755 --- a/bin/install.js +++ b/bin/install.js @@ -567,6 +567,8 @@ function convertClaudeToCopilotContent(content, isGlobal = false) { c = c.replace(/\.claude\//g, '.github/'); // CONV-07: Command name conversion (all gsd: references → gsd-) c = c.replace(/gsd:/g, 'gsd-'); + // Runtime-neutral agent name replacement (#766) + c = neutralizeAgentReferences(c, 'copilot-instructions.md'); return c; } @@ -657,6 +659,8 @@ function convertClaudeToAntigravityContent(content, isGlobal = false) { c = c.replace(/\.claude\//g, '.agent/'); // Command name conversion (all gsd: references → gsd-) c = c.replace(/gsd:/g, 'gsd-'); + // Runtime-neutral agent name replacement (#766) + c = neutralizeAgentReferences(c, 'GEMINI.md'); return c; } @@ -867,6 +871,8 @@ function convertSlashCommandsToCodexSkillMentions(content) { function convertClaudeToCodexMarkdown(content) { let converted = convertSlashCommandsToCodexSkillMentions(content); converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}'); + // Runtime-neutral agent name replacement (#766) + converted = neutralizeAgentReferences(converted, 'AGENTS.md'); return converted; } @@ -1175,6 +1181,36 @@ function installCodexConfig(targetDir, agentsSrc) { * Terminals don't support subscript — Gemini renders these as raw HTML. * Converts text to italic *(text)* for readable terminal output. */ +/** + * Runtime-neutral agent name and instruction file replacement. + * Used by ALL non-Claude runtime converters to avoid Claude-specific + * references in workflow prompts, agent definitions, and documentation. + * + * Replaces: + * - Standalone "Claude" (agent name) → "the agent" + * Preserves: "Claude Code" (product), "Claude Opus/Sonnet/Haiku" (models), + * "claude-" (prefixes), "CLAUDE.md" (handled separately) + * - "CLAUDE.md" → runtime-appropriate instruction file + * - "Do NOT load full AGENTS.md" → removed (harmful for AGENTS.md runtimes) + * + * @param {string} content - File content to neutralize + * @param {string} instructionFile - Runtime's instruction file ('AGENTS.md', 'GEMINI.md', etc.) + * @returns {string} Content with runtime-neutral references + */ +function neutralizeAgentReferences(content, instructionFile) { + let c = content; + // Replace standalone "Claude" (the agent) but preserve product/model names. + // Negative lookahead avoids: Claude Code, Claude Opus/Sonnet/Haiku, Claude native, Claude-based + c = c.replace(/\bClaude(?! Code| Opus| Sonnet| Haiku| native| based|-)\b(?!\.md)/g, 'the agent'); + // Replace CLAUDE.md with runtime-appropriate instruction file + if (instructionFile) { + c = c.replace(/CLAUDE\.md/g, instructionFile); + } + // Remove instructions that conflict with AGENTS.md-based runtimes + c = c.replace(/Do NOT load full `AGENTS\.md` files[^\n]*/g, ''); + return c; +} + function stripSubTags(content) { return content.replace(/(.*?)<\/sub>/g, '*($1)*'); } @@ -1279,7 +1315,9 @@ function convertClaudeToGeminiAgent(content) { // is equivalent bash and invisible to Gemini's /\$\{(\w+)\}/g regex. const escapedBody = body.replace(/\$\{(\w+)\}/g, '$$$1'); - return `---\n${newFrontmatter}\n---${stripSubTags(escapedBody)}`; + // Runtime-neutral agent name replacement (#766) + const neutralBody = neutralizeAgentReferences(escapedBody, 'GEMINI.md'); + return `---\n${newFrontmatter}\n---${stripSubTags(neutralBody)}`; } function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) { @@ -1295,6 +1333,8 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) { convertedContent = convertedContent.replace(/\$HOME\/\.claude\b/g, '$HOME/.config/opencode'); // Replace general-purpose subagent type with OpenCode's equivalent "general" convertedContent = convertedContent.replace(/subagent_type="general-purpose"/g, 'subagent_type="general"'); + // Runtime-neutral agent name replacement (#766) + convertedContent = neutralizeAgentReferences(convertedContent, 'AGENTS.md'); // Check if content has frontmatter if (!convertedContent.startsWith('---')) { @@ -3319,6 +3359,7 @@ if (process.env.GSD_TEST_MODE) { installCodexConfig, convertClaudeCommandToCodexSkill, convertClaudeToOpencodeFrontmatter, + neutralizeAgentReferences, GSD_CODEX_MARKER, CODEX_AGENT_SANDBOX, getDirName, diff --git a/tests/runtime-converters.test.cjs b/tests/runtime-converters.test.cjs index 5da337310..0dc33cd11 100644 --- a/tests/runtime-converters.test.cjs +++ b/tests/runtime-converters.test.cjs @@ -17,6 +17,7 @@ process.env.GSD_TEST_MODE = '1'; const { convertClaudeToOpencodeFrontmatter, convertClaudeToGeminiAgent, + neutralizeAgentReferences, } = require('../bin/install.js'); // Sample Claude agent frontmatter (matches actual GSD agent format) @@ -183,3 +184,56 @@ Use \${PHASE} in shell examples. assert.ok(!result.includes('${PHASE}'), 'removes Gemini template-string pattern'); }); }); + +// ─── neutralizeAgentReferences (#766) ───────────────────────────────────────── + +describe('neutralizeAgentReferences', () => { + test('replaces standalone Claude with "the agent"', () => { + const input = 'Claude handles these decisions. Claude should read the file.'; + const result = neutralizeAgentReferences(input, 'AGENTS.md'); + assert.ok(!result.includes('Claude handles'), 'standalone Claude replaced'); + assert.ok(result.includes('the agent handles'), 'replaced with "the agent"'); + }); + + test('preserves Claude Code (product name)', () => { + const input = 'This is a Claude Code bug. Use Claude Code settings.'; + const result = neutralizeAgentReferences(input, 'AGENTS.md'); + assert.ok(result.includes('Claude Code bug'), 'Claude Code preserved'); + assert.ok(result.includes('Claude Code settings'), 'Claude Code preserved'); + }); + + test('preserves Claude model names', () => { + const input = 'Use Claude Opus for planning. Claude Sonnet for execution. Claude Haiku for research.'; + const result = neutralizeAgentReferences(input, 'AGENTS.md'); + assert.ok(result.includes('Claude Opus'), 'Opus preserved'); + assert.ok(result.includes('Claude Sonnet'), 'Sonnet preserved'); + assert.ok(result.includes('Claude Haiku'), 'Haiku preserved'); + }); + + test('replaces CLAUDE.md with runtime instruction file', () => { + const input = 'Read CLAUDE.md for project instructions. Check ./CLAUDE.md if exists.'; + const result = neutralizeAgentReferences(input, 'AGENTS.md'); + assert.ok(result.includes('AGENTS.md'), 'CLAUDE.md -> AGENTS.md'); + assert.ok(!result.includes('CLAUDE.md'), 'no CLAUDE.md remains'); + }); + + test('uses different instruction file per runtime', () => { + const input = 'Read CLAUDE.md for instructions.'; + assert.ok(neutralizeAgentReferences(input, 'GEMINI.md').includes('GEMINI.md')); + assert.ok(neutralizeAgentReferences(input, 'copilot-instructions.md').includes('copilot-instructions.md')); + assert.ok(neutralizeAgentReferences(input, 'AGENTS.md').includes('AGENTS.md')); + }); + + test('removes AGENTS.md load-blocking instruction', () => { + const input = 'Do NOT load full `AGENTS.md` files — they contain agent definitions.'; + const result = neutralizeAgentReferences(input, 'AGENTS.md'); + assert.ok(!result.includes('Do NOT load full'), 'blocking instruction removed'); + }); + + test('preserves claude- prefixes (CSS classes, package names)', () => { + const input = 'The claude-ctx session and claude-code package.'; + const result = neutralizeAgentReferences(input, 'AGENTS.md'); + assert.ok(result.includes('claude-ctx'), 'claude- prefix preserved'); + assert.ok(result.includes('claude-code'), 'claude-code preserved'); + }); +}); From a4da2165231efcf3d2677bc673fad63c1c98cdcb Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 17 Mar 2026 08:01:12 -0400 Subject: [PATCH 57/83] feat: support ticket-based phase identifiers for team workflows (#1019) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Teams sharing a GSD repo collide on sequential phase numbers when multiple people create phases concurrently. This adds support for arbitrary string IDs (e.g. PROJ-42, AUTH-101) as phase identifiers. New config option: { "phase_naming": "custom" } Changes: - core.cjs: normalizePhaseName() handles non-numeric IDs gracefully - core.cjs: comparePhaseNum() falls back to string comparison for custom IDs - core.cjs: searchPhaseInDir() matches custom ID prefixes case-insensitively - core.cjs: getMilestonePhaseFilter() recognizes custom ID directory names - core.cjs: getRoadmapPhaseInternal() matches Phase headers with any word chars - core.cjs: loadConfig() adds phase_naming option (default: 'sequential') - phase.cjs: cmdPhaseAdd() accepts --id flag for custom phase IDs - verify.cjs: health check skips sequential numbering validation in custom mode - gsd-tools.cjs: phase add --id PROJ-42 "description" wired up Usage: # Sequential mode (default, backward compatible) gsd-tools phase add "User Authentication" # → Phase 3 # Custom mode (config: phase_naming: custom) gsd-tools phase add --id PROJ-42 "User Authentication" # Creates: .planning/phases/PROJ-42-user-authentication/ # ROADMAP: ### Phase PROJ-42: User Authentication All 755 existing tests pass. Fixes #1019 --- get-shit-done/bin/gsd-tools.cjs | 15 ++++++++-- get-shit-done/bin/lib/core.cjs | 47 ++++++++++++++++++++++++-------- get-shit-done/bin/lib/phase.cjs | 46 ++++++++++++++++++++----------- get-shit-done/bin/lib/verify.cjs | 21 ++++++++------ 4 files changed, 90 insertions(+), 39 deletions(-) diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index 633d921ff..bb24d0835 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -35,7 +35,7 @@ * * Phase Operations: * phase next-decimal Calculate next decimal phase number - * phase add Append new phase to roadmap + create dir + * phase add [--id ID] Append new phase to roadmap + create dir * phase insert Insert decimal phase after existing * phase remove [--force] Remove phase, renumber all subsequent * phase complete Mark phase done, update state + roadmap @@ -470,7 +470,18 @@ async function main() { if (subcommand === 'next-decimal') { phase.cmdPhaseNextDecimal(cwd, args[2], raw); } else if (subcommand === 'add') { - phase.cmdPhaseAdd(cwd, args.slice(2).join(' '), raw); + const idIdx = args.indexOf('--id'); + let customId = null; + const descArgs = []; + for (let i = 2; i < args.length; i++) { + if (args[i] === '--id' && i + 1 < args.length) { + customId = args[i + 1]; + i++; // skip value + } else { + descArgs.push(args[i]); + } + } + phase.cmdPhaseAdd(cwd, descArgs.join(' '), raw, customId); } else if (subcommand === 'insert') { phase.cmdPhaseInsert(cwd, args[2], args.slice(3).join(' '), raw); } else if (subcommand === 'remove') { diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index 66918b018..df71d8c97 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -66,6 +66,7 @@ function loadConfig(cwd) { brave_search: false, resolve_model_ids: false, // when true, resolve aliases (opus/sonnet/haiku) to full model IDs context_window: 200000, // default 200k; set to 1000000 for Opus/Sonnet 4.6 1M models + phase_naming: 'sequential', // 'sequential' (default, auto-increment) or 'custom' (arbitrary string IDs) }; try { @@ -110,6 +111,7 @@ function loadConfig(cwd) { brave_search: get('brave_search') ?? defaults.brave_search, resolve_model_ids: get('resolve_model_ids') ?? defaults.resolve_model_ids, context_window: get('context_window') ?? defaults.context_window, + phase_naming: get('phase_naming') ?? defaults.phase_naming, model_overrides: parsed.model_overrides || null, }; } catch { @@ -280,17 +282,23 @@ function escapeRegex(value) { } function normalizePhaseName(phase) { - const match = String(phase).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i); - if (!match) return phase; - const padded = match[1].padStart(2, '0'); - const letter = match[2] ? match[2].toUpperCase() : ''; - const decimal = match[3] || ''; - return padded + letter + decimal; + const str = String(phase); + // Standard numeric phases: 1, 01, 12A, 12.1 + const match = str.match(/^(\d+)([A-Z])?((?:\.\d+)*)/i); + if (match) { + const padded = match[1].padStart(2, '0'); + const letter = match[2] ? match[2].toUpperCase() : ''; + const decimal = match[3] || ''; + return padded + letter + decimal; + } + // Custom phase IDs (e.g. PROJ-42, AUTH-101): return as-is + return str; } function comparePhaseNum(a, b) { const pa = String(a).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i); const pb = String(b).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i); + // If either is non-numeric (custom ID), fall back to string comparison if (!pa || !pb) return String(a).localeCompare(String(b)); const intDiff = parseInt(pa[1], 10) - parseInt(pb[1], 10); if (intDiff !== 0) return intDiff; @@ -320,10 +328,19 @@ function searchPhaseInDir(baseDir, relBase, normalized) { try { const entries = fs.readdirSync(baseDir, { withFileTypes: true }); const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b)); - const match = dirs.find(d => d.startsWith(normalized)); + // Match: starts with normalized (numeric) OR contains normalized as prefix segment (custom ID) + const match = dirs.find(d => { + if (d.startsWith(normalized)) return true; + // For custom IDs like PROJ-42, match case-insensitively + if (d.toUpperCase().startsWith(normalized.toUpperCase())) return true; + return false; + }); if (!match) return null; - const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i); + // Extract phase number and name — supports both numeric (01-name) and custom (PROJ-42-name) + const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i) + || match.match(/^([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*)-(.+)/i) + || [null, match, null]; const phaseNumber = dirMatch ? dirMatch[1] : normalized; const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null; const phaseDir = path.join(baseDir, match); @@ -556,6 +573,7 @@ function getRoadmapPhaseInternal(cwd, phaseNum) { try { const content = extractCurrentMilestone(fs.readFileSync(roadmapPath, 'utf-8'), cwd); const escapedPhase = escapeRegex(phaseNum.toString()); + // Match both numeric (Phase 1:) and custom (Phase PROJ-42:) headers const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`, 'i'); const headerMatch = content.match(phasePattern); if (!headerMatch) return null; @@ -563,7 +581,7 @@ function getRoadmapPhaseInternal(cwd, phaseNum) { const phaseName = headerMatch[1].trim(); const headerIndex = headerMatch.index; const restOfContent = content.slice(headerIndex); - const nextHeaderMatch = restOfContent.match(/\n#{2,4}\s+Phase\s+\d/i); + const nextHeaderMatch = restOfContent.match(/\n#{2,4}\s+Phase\s+[\w]/i); const sectionEnd = nextHeaderMatch ? headerIndex + nextHeaderMatch.index : content.length; const section = content.slice(headerIndex, sectionEnd).trim(); @@ -681,7 +699,8 @@ function getMilestonePhaseFilter(cwd) { const milestonePhaseNums = new Set(); try { const roadmap = extractCurrentMilestone(fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8'), cwd); - const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi; + // Match both numeric phases (Phase 1:) and custom IDs (Phase PROJ-42:) + const phasePattern = /#{2,4}\s*Phase\s+([\w][\w.-]*)\s*:/gi; let m; while ((m = phasePattern.exec(roadmap)) !== null) { milestonePhaseNums.add(m[1]); @@ -699,9 +718,13 @@ function getMilestonePhaseFilter(cwd) { ); function isDirInMilestone(dirName) { + // Try numeric match first const m = dirName.match(/^0*(\d+[A-Za-z]?(?:\.\d+)*)/); - if (!m) return false; - return normalized.has(m[1].toLowerCase()); + if (m && normalized.has(m[1].toLowerCase())) return true; + // Try custom ID match (e.g. PROJ-42-description → PROJ-42) + const customMatch = dirName.match(/^([A-Za-z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)*)/); + if (customMatch && normalized.has(customMatch[1].toLowerCase())) return true; + return false; } isDirInMilestone.phaseCount = milestonePhaseNums.size; return isDirInMilestone; diff --git a/get-shit-done/bin/lib/phase.cjs b/get-shit-done/bin/lib/phase.cjs index dc2c52f99..4d737dc78 100644 --- a/get-shit-done/bin/lib/phase.cjs +++ b/get-shit-done/bin/lib/phase.cjs @@ -4,7 +4,7 @@ const fs = require('fs'); const path = require('path'); -const { escapeRegex, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, toPosixPath, output, error } = require('./core.cjs'); +const { escapeRegex, loadConfig, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, toPosixPath, output, error } = require('./core.cjs'); const { extractFrontmatter } = require('./frontmatter.cjs'); const { writeStateMd } = require('./state.cjs'); @@ -308,11 +308,12 @@ function cmdPhasePlanIndex(cwd, phase, raw) { output(result, raw); } -function cmdPhaseAdd(cwd, description, raw) { +function cmdPhaseAdd(cwd, description, raw, customId) { if (!description) { error('description required for phase add'); } + const config = loadConfig(cwd); const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md'); if (!fs.existsSync(roadmapPath)) { error('ROADMAP.md not found'); @@ -322,18 +323,29 @@ function cmdPhaseAdd(cwd, description, raw) { const content = extractCurrentMilestone(rawContent, cwd); const slug = generateSlugInternal(description); - // Find highest integer phase number (in current milestone only) - const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi; - let maxPhase = 0; - let m; - while ((m = phasePattern.exec(content)) !== null) { - const num = parseInt(m[1], 10); - if (num > maxPhase) maxPhase = num; + let newPhaseId; + let dirName; + + if (customId || config.phase_naming === 'custom') { + // Custom phase naming: use provided ID or generate from description + newPhaseId = customId || slug.toUpperCase().replace(/-/g, '-'); + if (!newPhaseId) error('--id required when phase_naming is "custom"'); + dirName = `${newPhaseId}-${slug}`; + } else { + // Sequential mode: find highest integer phase number (in current milestone only) + const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi; + let maxPhase = 0; + let m; + while ((m = phasePattern.exec(content)) !== null) { + const num = parseInt(m[1], 10); + if (num > maxPhase) maxPhase = num; + } + + newPhaseId = maxPhase + 1; + const paddedNum = String(newPhaseId).padStart(2, '0'); + dirName = `${paddedNum}-${slug}`; } - const newPhaseNum = maxPhase + 1; - const paddedNum = String(newPhaseNum).padStart(2, '0'); - const dirName = `${paddedNum}-${slug}`; const dirPath = path.join(cwd, '.planning', 'phases', dirName); // Create directory with .gitkeep so git tracks empty folders @@ -341,7 +353,8 @@ function cmdPhaseAdd(cwd, description, raw) { fs.writeFileSync(path.join(dirPath, '.gitkeep'), ''); // Build phase entry - const phaseEntry = `\n### Phase ${newPhaseNum}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${maxPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd:plan-phase ${newPhaseNum} to break down)\n`; + const dependsOn = config.phase_naming === 'custom' ? '' : `\n**Depends on:** Phase ${typeof newPhaseId === 'number' ? newPhaseId - 1 : 'TBD'}`; + const phaseEntry = `\n### Phase ${newPhaseId}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD${dependsOn}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd:plan-phase ${newPhaseId} to break down)\n`; // Find insertion point: before last "---" or at end let updatedContent; @@ -355,14 +368,15 @@ function cmdPhaseAdd(cwd, description, raw) { fs.writeFileSync(roadmapPath, updatedContent, 'utf-8'); const result = { - phase_number: newPhaseNum, - padded: paddedNum, + phase_number: typeof newPhaseId === 'number' ? newPhaseId : String(newPhaseId), + padded: typeof newPhaseId === 'number' ? String(newPhaseId).padStart(2, '0') : String(newPhaseId), name: description, slug, directory: `.planning/phases/${dirName}`, + naming_mode: config.phase_naming, }; - output(result, raw, paddedNum); + output(result, raw, result.padded); } function cmdPhaseInsert(cwd, afterPhase, description, raw) { diff --git a/get-shit-done/bin/lib/verify.cjs b/get-shit-done/bin/lib/verify.cjs index 5b3a12c13..c334eaa34 100644 --- a/get-shit-done/bin/lib/verify.cjs +++ b/get-shit-done/bin/lib/verify.cjs @@ -5,7 +5,7 @@ const fs = require('fs'); const path = require('path'); const os = require('os'); -const { safeReadFile, normalizePhaseName, execGit, findPhaseInternal, getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone, output, error } = require('./core.cjs'); +const { safeReadFile, loadConfig, normalizePhaseName, execGit, findPhaseInternal, getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone, output, error } = require('./core.cjs'); const { extractFrontmatter, parseMustHavesBlock } = require('./frontmatter.cjs'); const { writeStateMd } = require('./state.cjs'); @@ -445,15 +445,18 @@ function cmdValidateConsistency(cwd, raw) { } } - // Check: sequential phase numbers (integers only) - const integerPhases = [...diskPhases] - .filter(p => !p.includes('.')) - .map(p => parseInt(p, 10)) - .sort((a, b) => a - b); + // Check: sequential phase numbers (integers only, skip in custom naming mode) + const config = loadConfig(cwd); + if (config.phase_naming !== 'custom') { + const integerPhases = [...diskPhases] + .filter(p => !p.includes('.')) + .map(p => parseInt(p, 10)) + .sort((a, b) => a - b); - for (let i = 1; i < integerPhases.length; i++) { - if (integerPhases[i] !== integerPhases[i - 1] + 1) { - warnings.push(`Gap in phase numbering: ${integerPhases[i - 1]} → ${integerPhases[i]}`); + for (let i = 1; i < integerPhases.length; i++) { + if (integerPhases[i] !== integerPhases[i - 1] + 1) { + warnings.push(`Gap in phase numbering: ${integerPhases[i - 1]} → ${integerPhases[i]}`); + } } } From 42a2c15b3998c53654043f9510131558df071ad5 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 17:06:35 -0400 Subject: [PATCH 58/83] feat: research-before-questions config option (#1186) Adds workflow.research_questions config toggle (default: false) that enables web research before asking questions during /gsd:new-project and /gsd:discuss-phase. When enabled: - discuss-phase: searches best practices for each gray area before presenting questions, showing 2-3 bullet points of findings - new-project: researches the user's described domain before asking follow-up questions, weaving findings into the conversation Added to /gsd:settings as a toggle ('Research Qs' section). Closes #1186 --- get-shit-done/workflows/discuss-phase.md | 19 +++++++++++++++++++ get-shit-done/workflows/new-project.md | 7 +++++++ get-shit-done/workflows/settings.md | 12 +++++++++++- 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/get-shit-done/workflows/discuss-phase.md b/get-shit-done/workflows/discuss-phase.md index 75cd055c1..5182afce8 100644 --- a/get-shit-done/workflows/discuss-phase.md +++ b/get-shit-done/workflows/discuss-phase.md @@ -445,6 +445,25 @@ Continue to discuss_areas with selected areas. For each selected area, conduct a focused discussion loop. +**Research-before-questions mode:** Check if `research_questions` is enabled in config (from init context or `.planning/config.json`). When enabled, before presenting questions for each area: +1. Do a brief web search for best practices related to the area topic +2. Summarize the top findings in 2-3 bullet points +3. Present the research alongside the question so the user can make a more informed decision + +Example with research enabled: +``` +Let's talk about [Authentication Strategy]. + +📊 Best practices research: +• OAuth 2.0 + PKCE is the current standard for SPAs (replaces implicit flow) +• Session tokens with httpOnly cookies preferred over localStorage for XSS protection +• Consider passkey/WebAuthn support — adoption is accelerating in 2025-2026 + +With that context: How should users authenticate? +``` + +When disabled (default), skip the research and present questions directly as before. + **Batch mode support:** Parse optional `--batch` from `$ARGUMENTS`. - Accept `--batch`, `--batch=N`, or `--batch N` diff --git a/get-shit-done/workflows/new-project.md b/get-shit-done/workflows/new-project.md index 90597a369..4558c8b40 100644 --- a/get-shit-done/workflows/new-project.md +++ b/get-shit-done/workflows/new-project.md @@ -222,6 +222,13 @@ Ask inline (freeform, NOT AskUserQuestion): Wait for their response. This gives you the context needed to ask intelligent follow-up questions. +**Research-before-questions mode:** Check if `research_questions` is enabled in `.planning/config.json` (or the config from init context). When enabled, before asking follow-up questions about a topic area: +1. Do a brief web search for best practices related to what the user described +2. Mention key findings naturally as you ask questions (e.g., "Most projects like this use X — is that what you're thinking, or something different?") +3. This makes questions more informed without changing the conversational flow + +When disabled (default), ask questions directly as before. + **Follow the thread:** Based on what they said, ask follow-up questions that dig into their response. Use AskUserQuestion with options that probe what they mentioned — interpretations, clarifications, concrete examples. diff --git a/get-shit-done/workflows/settings.md b/get-shit-done/workflows/settings.md index 760056918..1c7ada565 100644 --- a/get-shit-done/workflows/settings.md +++ b/get-shit-done/workflows/settings.md @@ -135,6 +135,15 @@ AskUserQuestion([ { label: "Yes (Recommended)", description: "Warn when context usage exceeds 65%. Helps avoid losing work." }, { label: "No", description: "Disable warnings. Allows Claude to reach auto-compact naturally. Good for long unattended runs." } ] + }, + { + question: "Research best practices before asking questions? (web search during new-project and discuss-phase)", + header: "Research Qs", + multiSelect: false, + options: [ + { label: "No (Recommended)", description: "Ask questions directly. Faster, uses fewer tokens." }, + { label: "Yes", description: "Search web for best practices before each question group. More informed questions but uses more tokens." } + ] } ]) ``` @@ -161,7 +170,8 @@ Merge new settings into existing config.json: }, "hooks": { "context_warnings": true/false, - "workflow_guard": true/false + "workflow_guard": true/false, + "research_questions": true/false } } ``` From b1363966102b9afe6924e7f6deb8c66faf8c756b Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 17 Mar 2026 08:03:03 -0400 Subject: [PATCH 59/83] feat: add backlog parking lot and persistent context threads (#1005) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three new commands for managing ideas and cross-session context: /gsd:add-backlog Adds a 999.x numbered backlog item to ROADMAP.md. Creates phase directory immediately so /gsd:discuss-phase and /gsd:plan-phase work on them. No dependencies, no sequencing — pure parking lot. /gsd:review-backlog Lists all 999.x items, lets user promote to active milestone, keep, or remove. Promotion renumbers to next sequential phase with proper deps. /gsd:thread [name | description] Three modes: - No args: list all threads with status - Existing name: resume thread, load context - New description: create thread from current conversation context Threads live in .planning/threads/ as lightweight markdown files with Goal, Context, References, and Next Steps sections. Design: - Self-contained command files, no core changes needed - 999.x numbering keeps backlog out of active sequence - Threads are independent of phases — cross-session knowledge stores - Both features compose with existing GSD commands Fixes #1005 --- commands/gsd/add-backlog.md | 76 ++++++++++++++++++++ commands/gsd/review-backlog.md | 61 ++++++++++++++++ commands/gsd/thread.md | 127 +++++++++++++++++++++++++++++++++ 3 files changed, 264 insertions(+) create mode 100644 commands/gsd/add-backlog.md create mode 100644 commands/gsd/review-backlog.md create mode 100644 commands/gsd/thread.md diff --git a/commands/gsd/add-backlog.md b/commands/gsd/add-backlog.md new file mode 100644 index 000000000..a144fb975 --- /dev/null +++ b/commands/gsd/add-backlog.md @@ -0,0 +1,76 @@ +--- +name: gsd:add-backlog +description: Add an idea to the backlog parking lot (999.x numbering) +argument-hint: +allowed-tools: + - Read + - Write + - Bash +--- + + +Add a backlog item to the roadmap using 999.x numbering. Backlog items are +unsequenced ideas that aren't ready for active planning — they live outside +the normal phase sequence and accumulate context over time. + + + + +1. **Read ROADMAP.md** to find existing backlog entries: + ```bash + cat .planning/ROADMAP.md + ``` + +2. **Find next backlog number:** + ```bash + NEXT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" phase next-decimal 999 --raw) + ``` + If no 999.x phases exist, start at 999.1. + +3. **Create the phase directory:** + ```bash + SLUG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" generate-slug "$ARGUMENTS") + mkdir -p ".planning/phases/${NEXT}-${SLUG}" + touch ".planning/phases/${NEXT}-${SLUG}/.gitkeep" + ``` + +4. **Add to ROADMAP.md** under a `## Backlog` section. If the section doesn't exist, create it at the end: + + ```markdown + ## Backlog + + ### Phase {NEXT}: {description} (BACKLOG) + + **Goal:** [Captured for future planning] + **Requirements:** TBD + **Plans:** 0 plans + + Plans: + - [ ] TBD (promote with /gsd:review-backlog when ready) + ``` + +5. **Commit:** + ```bash + node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: add backlog item ${NEXT} — ${ARGUMENTS}" --files .planning/ROADMAP.md ".planning/phases/${NEXT}-${SLUG}/.gitkeep" + ``` + +6. **Report:** + ``` + ## 📋 Backlog Item Added + + Phase {NEXT}: {description} + Directory: .planning/phases/{NEXT}-{slug}/ + + This item lives in the backlog parking lot. + Use /gsd:discuss-phase {NEXT} to explore it further. + Use /gsd:review-backlog to promote items to active milestone. + ``` + + + + +- 999.x numbering keeps backlog items out of the active phase sequence +- Phase directories are created immediately, so /gsd:discuss-phase and /gsd:plan-phase work on them +- No `Depends on:` field — backlog items are unsequenced by definition +- Sparse numbering is fine (999.1, 999.3) — always uses next-decimal + diff --git a/commands/gsd/review-backlog.md b/commands/gsd/review-backlog.md new file mode 100644 index 000000000..91de5dd83 --- /dev/null +++ b/commands/gsd/review-backlog.md @@ -0,0 +1,61 @@ +--- +name: gsd:review-backlog +description: Review and promote backlog items to active milestone +allowed-tools: + - Read + - Write + - Bash +--- + + +Review all 999.x backlog items and optionally promote them into the active +milestone sequence or remove stale entries. + + + + +1. **List backlog items:** + ```bash + ls -d .planning/phases/999* 2>/dev/null || echo "No backlog items found" + ``` + +2. **Read ROADMAP.md** and extract all 999.x phase entries: + ```bash + cat .planning/ROADMAP.md + ``` + Show each backlog item with its description, any accumulated context (CONTEXT.md, RESEARCH.md), and creation date. + +3. **Present the list to the user** via AskUserQuestion: + - For each backlog item, show: phase number, description, accumulated artifacts + - Options per item: **Promote** (move to active), **Keep** (leave in backlog), **Remove** (delete) + +4. **For items to PROMOTE:** + - Find the next sequential phase number in the active milestone + - Rename the directory from `999.x-slug` to `{new_num}-slug`: + ```bash + NEW_NUM=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" phase add "${DESCRIPTION}" --raw) + ``` + - Move accumulated artifacts to the new phase directory + - Update ROADMAP.md: move the entry from `## Backlog` section to the active phase list + - Remove `(BACKLOG)` marker + - Add appropriate `**Depends on:**` field + +5. **For items to REMOVE:** + - Delete the phase directory + - Remove the entry from ROADMAP.md `## Backlog` section + +6. **Commit changes:** + ```bash + node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: review backlog — promoted N, removed M" --files .planning/ROADMAP.md + ``` + +7. **Report summary:** + ``` + ## 📋 Backlog Review Complete + + Promoted: {list of promoted items with new phase numbers} + Kept: {list of items remaining in backlog} + Removed: {list of deleted items} + ``` + + diff --git a/commands/gsd/thread.md b/commands/gsd/thread.md new file mode 100644 index 000000000..fe921184b --- /dev/null +++ b/commands/gsd/thread.md @@ -0,0 +1,127 @@ +--- +name: gsd:thread +description: Manage persistent context threads for cross-session work +argument-hint: [name | description] +allowed-tools: + - Read + - Write + - Bash +--- + + +Create, list, or resume persistent context threads. Threads are lightweight +cross-session knowledge stores for work that spans multiple sessions but +doesn't belong to any specific phase. + + + + +**Parse $ARGUMENTS to determine mode:** + + +**If no arguments or $ARGUMENTS is empty:** + +List all threads: +```bash +ls .planning/threads/*.md 2>/dev/null +``` + +For each thread, read the first few lines to show title and status: +``` +## Active Threads + +| Thread | Status | Last Updated | +|--------|--------|-------------| +| fix-deploy-key-auth | OPEN | 2026-03-15 | +| pasta-tcp-timeout | RESOLVED | 2026-03-12 | +| perf-investigation | IN PROGRESS | 2026-03-17 | +``` + +If no threads exist, show: +``` +No threads found. Create one with: /gsd:thread +``` + + + +**If $ARGUMENTS matches an existing thread name (file exists):** + +Resume the thread — load its context into the current session: +```bash +cat ".planning/threads/${THREAD_NAME}.md" +``` + +Display the thread content and ask what the user wants to work on next. +Update the thread's status to `IN PROGRESS` if it was `OPEN`. + + + +**If $ARGUMENTS is a new description (no matching thread file):** + +Create a new thread: + +1. Generate slug from description: + ```bash + SLUG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" generate-slug "$ARGUMENTS") + ``` + +2. Create the threads directory if needed: + ```bash + mkdir -p .planning/threads + ``` + +3. Write the thread file: + ```bash + cat > ".planning/threads/${SLUG}.md" << 'EOF' + # Thread: {description} + + ## Status: OPEN + + ## Goal + + {description} + + ## Context + + *Created from conversation on {today's date}.* + + ## References + + - *(add links, file paths, or issue numbers)* + + ## Next Steps + + - *(what the next session should do first)* + EOF + ``` + +4. If there's relevant context in the current conversation (code snippets, + error messages, investigation results), extract and add it to the Context + section. + +5. Commit: + ```bash + node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: create thread — ${ARGUMENTS}" --files ".planning/threads/${SLUG}.md" + ``` + +6. Report: + ``` + ## 🧵 Thread Created + + Thread: {slug} + File: .planning/threads/{slug}.md + + Resume anytime with: /gsd:thread {slug} + ``` + + + + + +- Threads are NOT phase-scoped — they exist independently of the roadmap +- Lighter weight than /gsd:pause-work — no phase state, no plan context +- The value is in Context and Next Steps — a cold-start session can pick up immediately +- Threads can be promoted to phases or backlog items when they mature: + /gsd:add-phase or /gsd:add-backlog with context from the thread +- Thread files live in .planning/threads/ — no collision with phases or other GSD structures + From 12e4cfe041421d0b340020107463334ec2f6774a Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 17 Mar 2026 08:14:11 -0400 Subject: [PATCH 60/83] fix: update Copilot skill count in tests for 3 new commands Tests expected 39 skill folders but got 42 after adding add-backlog, review-backlog, and thread commands. Updated both the hardcoded count and EXPECTED_SKILLS constant. --- tests/copilot-install.test.cjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/copilot-install.test.cjs b/tests/copilot-install.test.cjs index b6510d0e7..249d8a89a 100644 --- a/tests/copilot-install.test.cjs +++ b/tests/copilot-install.test.cjs @@ -625,7 +625,7 @@ describe('copyCommandsAsCopilotSkills', () => { // Count gsd-* directories — should be 31 const dirs = fs.readdirSync(tempDir, { withFileTypes: true }) .filter(e => e.isDirectory() && e.name.startsWith('gsd-')); - assert.strictEqual(dirs.length, 43, `expected 43 skill folders, got ${dirs.length}`); + assert.strictEqual(dirs.length, 46, `expected 46 skill folders, got ${dirs.length}`); } finally { fs.rmSync(tempDir, { recursive: true }); } @@ -1119,7 +1119,7 @@ const { execFileSync } = require('child_process'); const crypto = require('crypto'); const INSTALL_PATH = path.join(__dirname, '..', 'bin', 'install.js'); -const EXPECTED_SKILLS = 43; +const EXPECTED_SKILLS = 46; const EXPECTED_AGENTS = 16; function runCopilotInstall(cwd) { From 9ca03ec35e6e184d2c78eec05057daf78f2812a7 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 23:29:38 -0400 Subject: [PATCH 61/83] fix: filter stale hooks check to gsd-prefixed files only (#1200) gsd-check-update.js scanned ALL .js files in the hooks directory and flagged any without a gsd-hook-version header as stale. This incorrectly flagged user-created hooks (e.g. guard-edits-outside-project.js), producing a persistent 'stale hooks' warning that /gsd:update couldn't resolve. Fix: filter hookFiles to f.startsWith('gsd-') && f.endsWith('.js') since all GSD hooks follow the gsd-* naming convention. Includes regression test validating the filter excludes user hooks. Closes #1200 --- hooks/gsd-check-update.js | 2 +- tests/core.test.cjs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/hooks/gsd-check-update.js b/hooks/gsd-check-update.js index 510302fb3..9076ec038 100755 --- a/hooks/gsd-check-update.js +++ b/hooks/gsd-check-update.js @@ -70,7 +70,7 @@ const child = spawn(process.execPath, ['-e', ` const hooksDir = path.join(configDir, 'hooks'); try { if (fs.existsSync(hooksDir)) { - const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.js')); + const hookFiles = fs.readdirSync(hooksDir).filter(f => f.startsWith('gsd-') && f.endsWith('.js')); for (const hookFile of hookFiles) { try { const content = fs.readFileSync(path.join(hooksDir, hookFile), 'utf8'); diff --git a/tests/core.test.cjs b/tests/core.test.cjs index 2f9d796f0..56d658a53 100644 --- a/tests/core.test.cjs +++ b/tests/core.test.cjs @@ -867,3 +867,33 @@ describe('normalizeMd', () => { assert.ok(result.includes('\n\n- Decision 1'), 'list needs blank line before'); }); }); + +// ─── Stale hook filter regression (#1200) ───────────────────────────────────── + +describe('stale hook filter', () => { + test('filter should only match gsd-prefixed .js files', () => { + const files = [ + 'gsd-check-update.js', + 'gsd-context-monitor.js', + 'gsd-statusline.js', + 'gsd-workflow-guard.js', + 'guard-edits-outside-project.js', // user hook + 'my-custom-hook.js', // user hook + 'gsd-check-update.js.bak', // backup file + 'README.md', // non-js file + ]; + + const gsdFilter = f => f.startsWith('gsd-') && f.endsWith('.js'); + const filtered = files.filter(gsdFilter); + + assert.deepStrictEqual(filtered, [ + 'gsd-check-update.js', + 'gsd-context-monitor.js', + 'gsd-statusline.js', + 'gsd-workflow-guard.js', + ], 'should only include gsd-prefixed .js files'); + + assert.ok(!filtered.includes('guard-edits-outside-project.js'), 'must not include user hooks'); + assert.ok(!filtered.includes('my-custom-hook.js'), 'must not include non-gsd hooks'); + }); +}); From 862f6b91ba58693a0c97acead06fbc7e1b9719ac Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 23:32:05 -0400 Subject: [PATCH 62/83] fix: prevent Codex config.toml corruption from non-boolean [features] keys (#1202) When GSD installs codex_hooks = true under [features], any non-boolean keys already in that section (e.g. model = "gpt-5.4") cause Codex's TOML parser to fail with 'invalid type: string, expected a boolean'. Root cause: TOML sections extend until the next [section] header. If the user placed model/model_reasoning_effort under [features] (common since Codex's own config format encourages this), GSD's installer didn't detect or correct the structural issue. Fix: After injecting codex_hooks, scan the [features] section for non-boolean values and move them above [features] to the top level. This preserves the user's keys while keeping [features] clean for Codex's strict boolean parser. Includes 2 regression tests: - Detects non-boolean keys under [features] (model, model_reasoning_effort) - Confirms boolean keys (codex_hooks, multi_agent) are not flagged Closes #1202 --- bin/install.js | 30 +++++++++++++++++++++++++++--- tests/codex-config.test.cjs | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/bin/install.js b/bin/install.js index 93d909ed1..6558c070d 100755 --- a/bin/install.js +++ b/bin/install.js @@ -2992,11 +2992,35 @@ function install(isGlobal, runtime = 'claude') { // Enable hooks feature flag if not present if (!configContent.includes('codex_hooks')) { - const featuresSection = '[features]\ncodex_hooks = true\n'; if (configContent.includes('[features]')) { - configContent = configContent.replace(/\[features\]\n/, featuresSection); + // Insert codex_hooks = true right after the [features] header. + // Fixes #1202: previous approach could leave non-boolean keys (like + // model = "gpt-5.4") under [features], causing Codex TOML parse errors. + configContent = configContent.replace(/(\[features\]\n)/, '$1codex_hooks = true\n'); } else { - configContent = featuresSection + '\n' + configContent; + configContent = '[features]\ncodex_hooks = true\n\n' + configContent; + } + } + + // Safety check: detect non-boolean keys under [features] that would break Codex (#1202). + // Extract the [features] section content (between [features] and next [section] or EOF). + const featuresMatch = configContent.match(/\[features\]\n([\s\S]*?)(?=\n\[|$)/); + if (featuresMatch) { + const featuresBody = featuresMatch[1]; + const nonBooleanKeys = featuresBody.split('\n') + .filter(line => line.match(/^\s*\w+\s*=/) && !line.match(/=\s*(true|false)\s*(#.*)?$/)) + .map(line => line.trim()); + if (nonBooleanKeys.length > 0) { + // Move non-boolean keys above [features] to prevent TOML parse errors + let cleanedFeatures = featuresBody.split('\n') + .filter(line => !line.match(/^\s*\w+\s*=/) || line.match(/=\s*(true|false)\s*(#.*)?$/)) + .join('\n'); + const movedKeys = nonBooleanKeys.join('\n') + '\n'; + configContent = configContent.replace( + /\[features\]\n[\s\S]*?(?=\n\[|$)/, + movedKeys + '\n[features]\n' + cleanedFeatures.trim() + '\n' + ); + console.log(` ${yellow}⚠${reset} Moved ${nonBooleanKeys.length} non-feature key(s) out of [features] section to prevent TOML errors`); } } diff --git a/tests/codex-config.test.cjs b/tests/codex-config.test.cjs index 4c2cd0aff..e5303d2cb 100644 --- a/tests/codex-config.test.cjs +++ b/tests/codex-config.test.cjs @@ -492,3 +492,37 @@ describe('installCodexConfig (integration)', () => { assert.ok(checkerToml.includes('sandbox_mode = "read-only"'), 'plan-checker is read-only'); }); }); + +// ─── Codex config.toml [features] safety (#1202) ───────────────────────────── + +describe('codex features section safety', () => { + test('non-boolean keys under [features] are moved to top level', () => { + // Simulate the bug from #1202: model = "gpt-5.4" under [features] + // causes "invalid type: string, expected a boolean in features" + const configContent = `[features]\ncodex_hooks = true\n\nmodel = "gpt-5.4"\nmodel_reasoning_effort = "medium"\n\n[agents.gsd-executor]\ndescription = "test"\n`; + + const featuresMatch = configContent.match(/\[features\]\n([\s\S]*?)(?=\n\[|$)/); + assert.ok(featuresMatch, 'features section found'); + + const featuresBody = featuresMatch[1]; + const nonBooleanKeys = featuresBody.split('\n') + .filter(line => line.match(/^\s*\w+\s*=/) && !line.match(/=\s*(true|false)\s*(#.*)?$/)) + .map(line => line.trim()); + + assert.strictEqual(nonBooleanKeys.length, 2, 'should detect 2 non-boolean keys'); + assert.ok(nonBooleanKeys.includes('model = "gpt-5.4"'), 'detects model key'); + assert.ok(nonBooleanKeys.includes('model_reasoning_effort = "medium"'), 'detects model_reasoning_effort key'); + }); + + test('boolean keys under [features] are NOT flagged', () => { + const configContent = `[features]\ncodex_hooks = true\nmulti_agent = false\n`; + + const featuresMatch = configContent.match(/\[features\]\n([\s\S]*?)(?=\n\[|$)/); + const featuresBody = featuresMatch[1]; + const nonBooleanKeys = featuresBody.split('\n') + .filter(line => line.match(/^\s*\w+\s*=/) && !line.match(/=\s*(true|false)\s*(#.*)?$/)) + .map(line => line.trim()); + + assert.strictEqual(nonBooleanKeys.length, 0, 'no non-boolean keys in a clean config'); + }); +}); From 1d4deb0f8b7dceac8a58df9cf44560974a406b9f Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 23:38:36 -0400 Subject: [PATCH 63/83] =?UTF-8?q?refactor:=20adopt=20planningPaths()=20acr?= =?UTF-8?q?oss=204=20modules=20=E2=80=94=20eliminate=2034=20inline=20path?= =?UTF-8?q?=20constructions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consolidates repeated path.join(cwd, '.planning', ...) patterns into calls to the shared planningPaths() helper from core.cjs. Modules updated: - state.cjs: 16 → planningPaths() (2 remain for non-standard paths) - commands.cjs: 8 → planningPaths() (4 remain for todos dir) - milestone.cjs: 5 → planningPaths() (3 remain for archive/milestones) - roadmap.cjs: 4 → planningPaths() Benefits: - Single source of truth for .planning/ directory structure - Easier to change planning dir location in the future - Consistent path construction across the codebase - No behavioral changes — pure refactor 797/797 tests pass. --- get-shit-done/bin/lib/commands.cjs | 18 +++++++------- get-shit-done/bin/lib/milestone.cjs | 12 ++++----- get-shit-done/bin/lib/roadmap.cjs | 10 ++++---- get-shit-done/bin/lib/state.cjs | 38 ++++++++++++++--------------- tests/cursor-conversion.test.cjs | 4 +-- 5 files changed, 41 insertions(+), 41 deletions(-) diff --git a/get-shit-done/bin/lib/commands.cjs b/get-shit-done/bin/lib/commands.cjs index feeb7557f..7227ea5c8 100644 --- a/get-shit-done/bin/lib/commands.cjs +++ b/get-shit-done/bin/lib/commands.cjs @@ -4,7 +4,7 @@ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); -const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, stripShippedMilestones, extractCurrentMilestone, toPosixPath, output, error, findPhaseInternal, getRoadmapPhaseInternal } = require('./core.cjs'); +const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, stripShippedMilestones, extractCurrentMilestone, planningPaths, toPosixPath, output, error, findPhaseInternal, getRoadmapPhaseInternal } = require('./core.cjs'); const { extractFrontmatter } = require('./frontmatter.cjs'); const { MODEL_PROFILES } = require('./model-profiles.cjs'); @@ -98,7 +98,7 @@ function cmdVerifyPathExists(cwd, targetPath, raw) { } function cmdHistoryDigest(cwd, raw) { - const phasesDir = path.join(cwd, '.planning', 'phases'); + const phasesDir = planningPaths(cwd).phases; const digest = { phases: {}, decisions: [], tech_stack: new Set() }; // Collect all phase directories: archived + current @@ -382,8 +382,8 @@ async function cmdWebsearch(query, options, raw) { } function cmdProgressRender(cwd, format, raw) { - const phasesDir = path.join(cwd, '.planning', 'phases'); - const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md'); + const phasesDir = planningPaths(cwd).phases; + const roadmapPath = planningPaths(cwd).roadmap; const milestone = getMilestoneInfo(cwd); const phases = []; @@ -637,7 +637,7 @@ function cmdScaffold(cwd, type, options, raw) { } const slug = generateSlugInternal(name); const dirName = `${padded}-${slug}`; - const phasesParent = path.join(cwd, '.planning', 'phases'); + const phasesParent = planningPaths(cwd).phases; fs.mkdirSync(phasesParent, { recursive: true }); const dirPath = path.join(phasesParent, dirName); fs.mkdirSync(dirPath, { recursive: true }); @@ -659,10 +659,10 @@ function cmdScaffold(cwd, type, options, raw) { } function cmdStats(cwd, format, raw) { - const phasesDir = path.join(cwd, '.planning', 'phases'); - const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md'); - const reqPath = path.join(cwd, '.planning', 'REQUIREMENTS.md'); - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const phasesDir = planningPaths(cwd).phases; + const roadmapPath = planningPaths(cwd).roadmap; + const reqPath = planningPaths(cwd).requirements; + const statePath = planningPaths(cwd).state; const milestone = getMilestoneInfo(cwd); const isDirInMilestone = getMilestonePhaseFilter(cwd); diff --git a/get-shit-done/bin/lib/milestone.cjs b/get-shit-done/bin/lib/milestone.cjs index ba56a0727..63c5e15fb 100644 --- a/get-shit-done/bin/lib/milestone.cjs +++ b/get-shit-done/bin/lib/milestone.cjs @@ -4,7 +4,7 @@ const fs = require('fs'); const path = require('path'); -const { escapeRegex, getMilestonePhaseFilter, normalizeMd, output, error } = require('./core.cjs'); +const { escapeRegex, getMilestonePhaseFilter, normalizeMd, planningPaths, output, error } = require('./core.cjs'); const { extractFrontmatter } = require('./frontmatter.cjs'); const { writeStateMd } = require('./state.cjs'); @@ -25,7 +25,7 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) { error('no valid requirement IDs found'); } - const reqPath = path.join(cwd, '.planning', 'REQUIREMENTS.md'); + const reqPath = planningPaths(cwd).requirements; if (!fs.existsSync(reqPath)) { output({ updated: false, reason: 'REQUIREMENTS.md not found', ids: reqIds }, raw, 'no requirements file'); return; @@ -90,12 +90,12 @@ function cmdMilestoneComplete(cwd, version, options, raw) { error('version required for milestone complete (e.g., v1.0)'); } - const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md'); - const reqPath = path.join(cwd, '.planning', 'REQUIREMENTS.md'); - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const roadmapPath = planningPaths(cwd).roadmap; + const reqPath = planningPaths(cwd).requirements; + const statePath = planningPaths(cwd).state; const milestonesPath = path.join(cwd, '.planning', 'MILESTONES.md'); const archiveDir = path.join(cwd, '.planning', 'milestones'); - const phasesDir = path.join(cwd, '.planning', 'phases'); + const phasesDir = planningPaths(cwd).phases; const today = new Date().toISOString().split('T')[0]; const milestoneName = options.name || version; diff --git a/get-shit-done/bin/lib/roadmap.cjs b/get-shit-done/bin/lib/roadmap.cjs index 0e6d900e7..a2fe661a6 100644 --- a/get-shit-done/bin/lib/roadmap.cjs +++ b/get-shit-done/bin/lib/roadmap.cjs @@ -4,10 +4,10 @@ const fs = require('fs'); const path = require('path'); -const { escapeRegex, normalizePhaseName, output, error, findPhaseInternal, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone } = require('./core.cjs'); +const { escapeRegex, normalizePhaseName, planningPaths, output, error, findPhaseInternal, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone } = require('./core.cjs'); function cmdRoadmapGetPhase(cwd, phaseNum, raw) { - const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md'); + const roadmapPath = planningPaths(cwd).roadmap; if (!fs.existsSync(roadmapPath)) { output({ found: false, error: 'ROADMAP.md not found' }, raw, ''); @@ -91,7 +91,7 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) { } function cmdRoadmapAnalyze(cwd, raw) { - const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md'); + const roadmapPath = planningPaths(cwd).roadmap; if (!fs.existsSync(roadmapPath)) { output({ error: 'ROADMAP.md not found', milestones: [], phases: [], current_phase: null }, raw); @@ -100,7 +100,7 @@ function cmdRoadmapAnalyze(cwd, raw) { const rawContent = fs.readFileSync(roadmapPath, 'utf-8'); const content = extractCurrentMilestone(rawContent, cwd); - const phasesDir = path.join(cwd, '.planning', 'phases'); + const phasesDir = planningPaths(cwd).phases; // Extract all phase headings: ## Phase N: Name or ### Phase N: Name const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi; @@ -230,7 +230,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) { error('phase number required for roadmap update-plan-progress'); } - const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md'); + const roadmapPath = planningPaths(cwd).roadmap; const phaseInfo = findPhaseInternal(cwd, phaseNum); if (!phaseInfo) { diff --git a/get-shit-done/bin/lib/state.cjs b/get-shit-done/bin/lib/state.cjs index 08aa84862..3d75dfa96 100644 --- a/get-shit-done/bin/lib/state.cjs +++ b/get-shit-done/bin/lib/state.cjs @@ -26,15 +26,15 @@ function stateExtractField(content, fieldName) { function cmdStateLoad(cwd, raw) { const config = loadConfig(cwd); - const planningDir = path.join(cwd, '.planning'); + const planDir = planningPaths(cwd).planning; let stateRaw = ''; try { - stateRaw = fs.readFileSync(path.join(planningDir, 'STATE.md'), 'utf-8'); + stateRaw = fs.readFileSync(path.join(planDir, 'STATE.md'), 'utf-8'); } catch { /* intentionally empty */ } - const configExists = fs.existsSync(path.join(planningDir, 'config.json')); - const roadmapExists = fs.existsSync(path.join(planningDir, 'ROADMAP.md')); + const configExists = fs.existsSync(path.join(planDir, 'config.json')); + const roadmapExists = fs.existsSync(path.join(planDir, 'ROADMAP.md')); const stateExists = stateRaw.length > 0; const result = { @@ -70,7 +70,7 @@ function cmdStateLoad(cwd, raw) { } function cmdStateGet(cwd, section, raw) { - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const statePath = planningPaths(cwd).state; try { const content = fs.readFileSync(statePath, 'utf-8'); @@ -124,7 +124,7 @@ function readTextArgOrFile(cwd, value, filePath, label) { } function cmdStatePatch(cwd, patches, raw) { - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const statePath = planningPaths(cwd).state; try { let content = fs.readFileSync(statePath, 'utf-8'); const results = { updated: [], failed: [] }; @@ -161,7 +161,7 @@ function cmdStateUpdate(cwd, field, value) { error('field and value required for state update'); } - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const statePath = planningPaths(cwd).state; try { let content = fs.readFileSync(statePath, 'utf-8'); const fieldEscaped = escapeRegex(field); @@ -202,7 +202,7 @@ function stateReplaceField(content, fieldName, newValue) { } function cmdStateAdvancePlan(cwd, raw) { - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const statePath = planningPaths(cwd).state; if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; } let content = fs.readFileSync(statePath, 'utf-8'); @@ -231,7 +231,7 @@ function cmdStateAdvancePlan(cwd, raw) { } function cmdStateRecordMetric(cwd, options, raw) { - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const statePath = planningPaths(cwd).state; if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; } let content = fs.readFileSync(statePath, 'utf-8'); @@ -265,13 +265,13 @@ function cmdStateRecordMetric(cwd, options, raw) { } function cmdStateUpdateProgress(cwd, raw) { - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const statePath = planningPaths(cwd).state; if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; } let content = fs.readFileSync(statePath, 'utf-8'); // Count summaries across current milestone phases only - const phasesDir = path.join(cwd, '.planning', 'phases'); + const phasesDir = planningPaths(cwd).phases; let totalPlans = 0; let totalSummaries = 0; @@ -310,7 +310,7 @@ function cmdStateUpdateProgress(cwd, raw) { } function cmdStateAddDecision(cwd, options, raw) { - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const statePath = planningPaths(cwd).state; if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; } const { phase, summary, summary_file, rationale, rationale_file } = options; @@ -348,7 +348,7 @@ function cmdStateAddDecision(cwd, options, raw) { } function cmdStateAddBlocker(cwd, text, raw) { - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const statePath = planningPaths(cwd).state; if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; } const blockerOptions = typeof text === 'object' && text !== null ? text : { text }; let blockerText = null; @@ -381,7 +381,7 @@ function cmdStateAddBlocker(cwd, text, raw) { } function cmdStateResolveBlocker(cwd, text, raw) { - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const statePath = planningPaths(cwd).state; if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; } if (!text) { output({ error: 'text required' }, raw); return; } @@ -413,7 +413,7 @@ function cmdStateResolveBlocker(cwd, text, raw) { } function cmdStateRecordSession(cwd, options, raw) { - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const statePath = planningPaths(cwd).state; if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; } let content = fs.readFileSync(statePath, 'utf-8'); @@ -448,7 +448,7 @@ function cmdStateRecordSession(cwd, options, raw) { } function cmdStateSnapshot(cwd, raw) { - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const statePath = planningPaths(cwd).state; if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); @@ -580,7 +580,7 @@ function buildStateFrontmatter(bodyContent, cwd) { if (cwd) { try { - const phasesDir = path.join(cwd, '.planning', 'phases'); + const phasesDir = planningPaths(cwd).phases; if (fs.existsSync(phasesDir)) { const isDirInMilestone = getMilestonePhaseFilter(cwd); const phaseDirs = fs.readdirSync(phasesDir, { withFileTypes: true }) @@ -722,7 +722,7 @@ function writeStateMd(statePath, content, cwd) { } function cmdStateJson(cwd, raw) { - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const statePath = planningPaths(cwd).state; if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw, 'STATE.md not found'); return; @@ -748,7 +748,7 @@ function cmdStateJson(cwd, raw) { * Fixes: #1102 (plan counts), #1103 (status/last_activity), #1104 (body text). */ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) { - const statePath = path.join(cwd, '.planning', 'STATE.md'); + const statePath = planningPaths(cwd).state; if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; diff --git a/tests/cursor-conversion.test.cjs b/tests/cursor-conversion.test.cjs index 2c7c79d52..a6fe43844 100644 --- a/tests/cursor-conversion.test.cjs +++ b/tests/cursor-conversion.test.cjs @@ -32,7 +32,7 @@ Test body const nameMatch = result.match(/^name:\s*(.+)$/m); assert.ok(nameMatch, 'frontmatter contains name field'); - assert.equal(nameMatch[1], 'gsd-quick', 'skill name is plain scalar'); + assert.strictEqual(nameMatch[1], 'gsd-quick', 'skill name is plain scalar'); assert.ok(!result.includes('name: "gsd-quick"'), 'quoted skill name is not emitted'); }); @@ -75,7 +75,7 @@ Planner body const nameMatch = result.match(/^name:\s*(.+)$/m); assert.ok(nameMatch, 'frontmatter contains name field'); - assert.equal(nameMatch[1], 'gsd-planner', 'agent name is plain scalar'); + assert.strictEqual(nameMatch[1], 'gsd-planner', 'agent name is plain scalar'); assert.ok(!result.includes('name: "gsd-planner"'), 'quoted agent name is not emitted'); }); }); From f656dcbd6ff244a8ac7d5886da864c5f08514009 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 23:43:28 -0400 Subject: [PATCH 64/83] =?UTF-8?q?chore:=20update=20CI=20matrix=20to=20Node?= =?UTF-8?q?=2020,=2022,=2024=20=E2=80=94=20drop=20EOL=20Node=2018?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Node 18 reached EOL April 2025. Node 24 is the current LTS target. Changes: - CI matrix: [18, 20, 22] → [20, 22, 24] - package.json engines: >=16.7.0 → >=20.0.0 - Removed Node 18 conditional in CI (c8 coverage works on all 20+) - Simplified CI to single test:coverage step for all versions 797/797 tests pass on Node 24. --- .github/workflows/test.yml | 10 +--------- package.json | 2 +- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 94fb64bb4..efda73dff 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,7 +22,7 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest, macos-latest, windows-latest] - node-version: [18, 20, 22] + node-version: [20, 22, 24] steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -37,13 +37,5 @@ jobs: run: npm ci - name: Run tests with coverage - # c8 v11 requires Node 20+ (engines: ^20.0.0 || >=22.0.0). Node 18 EOL April 2025. - # Use bash on all platforms so shell glob expansion works on Windows. - if: matrix.node-version != 18 shell: bash run: npm run test:coverage - - - name: Run tests (Node 18, coverage not supported) - if: matrix.node-version == 18 - shell: bash - run: npm test diff --git a/package.json b/package.json index 5a11cfb12..5d31df501 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "url": "https://github.com/glittercowboy/get-shit-done/issues" }, "engines": { - "node": ">=16.7.0" + "node": ">=20.0.0" }, "devDependencies": { "c8": "^11.0.0", From c41a9f5908f8d95b03e0fbd87c218fae3735dd30 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 23:53:13 -0400 Subject: [PATCH 65/83] =?UTF-8?q?feat:=20community-requested=20commands=20?= =?UTF-8?q?=E2=80=94=20/gsd:review,=20/gsd:plant-seed,=20/gsd:pr-branch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three commands reimplemented from positively-received community PRs: 1. /gsd:review (#925) — Cross-AI peer review Invoke external AI CLIs (Gemini, Claude, Codex) to independently review phase plans. Produces REVIEWS.md with per-reviewer feedback and consensus summary. Feed back into planning via --reviews flag. Multiple users praised the adversarial review concept. 2. /gsd:plant-seed (#456) — Forward-looking idea capture Capture ideas with trigger conditions that auto-surface during /gsd:new-milestone. Seeds preserve WHY, WHEN to surface, and breadcrumbs to related code. Better than deferred items because triggers are checked, not forgotten. 3. /gsd:pr-branch (#470) — Clean PR branches Create a branch for pull requests by filtering out .planning/ commits. Classifies commits as code-only, planning-only, or mixed, then cherry-picks only code changes. Reviewers see clean diffs. All three are standalone command+workflow additions with no core code changes. Help workflow updated. Test skill counts updated. 797/797 tests pass. --- commands/gsd/plant-seed.md | 28 ++++ commands/gsd/pr-branch.md | 25 +++ commands/gsd/review.md | 37 +++++ get-shit-done/workflows/help.md | 34 ++++ get-shit-done/workflows/plant-seed.md | 169 +++++++++++++++++++ get-shit-done/workflows/pr-branch.md | 129 +++++++++++++++ get-shit-done/workflows/review.md | 228 ++++++++++++++++++++++++++ tests/copilot-install.test.cjs | 4 +- 8 files changed, 652 insertions(+), 2 deletions(-) create mode 100644 commands/gsd/plant-seed.md create mode 100644 commands/gsd/pr-branch.md create mode 100644 commands/gsd/review.md create mode 100644 get-shit-done/workflows/plant-seed.md create mode 100644 get-shit-done/workflows/pr-branch.md create mode 100644 get-shit-done/workflows/review.md diff --git a/commands/gsd/plant-seed.md b/commands/gsd/plant-seed.md new file mode 100644 index 000000000..95ab4efc8 --- /dev/null +++ b/commands/gsd/plant-seed.md @@ -0,0 +1,28 @@ +--- +name: gsd:plant-seed +description: Capture a forward-looking idea with trigger conditions — surfaces automatically at the right milestone +argument-hint: "[idea summary]" +allowed-tools: + - Read + - Write + - Edit + - Bash + - AskUserQuestion +--- + + +Capture an idea that's too big for now but should surface automatically when the right +milestone arrives. Seeds solve context rot: instead of a one-liner in Deferred that nobody +reads, a seed preserves the full WHY, WHEN to surface, and breadcrumbs to details. + +Creates: .planning/seeds/SEED-NNN-slug.md +Consumed by: /gsd:new-milestone (scans seeds and presents matches) + + + +@~/.claude/get-shit-done/workflows/plant-seed.md + + + +Execute the plant-seed workflow from @~/.claude/get-shit-done/workflows/plant-seed.md end-to-end. + diff --git a/commands/gsd/pr-branch.md b/commands/gsd/pr-branch.md new file mode 100644 index 000000000..6c2d7f8d9 --- /dev/null +++ b/commands/gsd/pr-branch.md @@ -0,0 +1,25 @@ +--- +name: gsd:pr-branch +description: Create a clean PR branch by filtering out .planning/ commits — ready for code review +argument-hint: "[target branch, default: main]" +allowed-tools: + - Bash + - Read + - AskUserQuestion +--- + + +Create a clean branch suitable for pull requests by filtering out .planning/ commits +from the current branch. Reviewers see only code changes, not GSD planning artifacts. + +This solves the problem of PR diffs being cluttered with PLAN.md, SUMMARY.md, STATE.md +changes that are irrelevant to code review. + + + +@~/.claude/get-shit-done/workflows/pr-branch.md + + + +Execute the pr-branch workflow from @~/.claude/get-shit-done/workflows/pr-branch.md end-to-end. + diff --git a/commands/gsd/review.md b/commands/gsd/review.md new file mode 100644 index 000000000..84d8171f6 --- /dev/null +++ b/commands/gsd/review.md @@ -0,0 +1,37 @@ +--- +name: gsd:review +description: Request cross-AI peer review of phase plans from external AI CLIs +argument-hint: "--phase N [--gemini] [--claude] [--codex] [--all]" +allowed-tools: + - Read + - Write + - Bash + - Glob + - Grep +--- + + +Invoke external AI CLIs (Gemini, Claude, Codex) to independently review phase plans. +Produces a structured REVIEWS.md with per-reviewer feedback that can be fed back into +planning via /gsd:plan-phase --reviews. + +**Flow:** Detect CLIs → Build review prompt → Invoke each CLI → Collect responses → Write REVIEWS.md + + + +@~/.claude/get-shit-done/workflows/review.md + + + +Phase number: extracted from $ARGUMENTS (required) + +**Flags:** +- `--gemini` — Include Gemini CLI review +- `--claude` — Include Claude CLI review (uses separate session) +- `--codex` — Include Codex CLI review +- `--all` — Include all available CLIs + + + +Execute the review workflow from @~/.claude/get-shit-done/workflows/review.md end-to-end. + diff --git a/get-shit-done/workflows/help.md b/get-shit-done/workflows/help.md index cdd60638e..76106e909 100644 --- a/get-shit-done/workflows/help.md +++ b/get-shit-done/workflows/help.md @@ -337,6 +337,40 @@ Prerequisites: Phase verified, `gh` CLI installed and authenticated. Usage: `/gsd:ship 4` or `/gsd:ship 4 --draft` +--- + +**`/gsd:review --phase N [--gemini] [--claude] [--codex] [--all]`** +Cross-AI peer review — invoke external AI CLIs to independently review phase plans. + +- Detects available CLIs (gemini, claude, codex) +- Each CLI reviews plans independently with the same structured prompt +- Produces REVIEWS.md with per-reviewer feedback and consensus summary +- Feed reviews back into planning: `/gsd:plan-phase N --reviews` + +Usage: `/gsd:review --phase 3 --all` + +--- + +**`/gsd:pr-branch [target]`** +Create a clean branch for pull requests by filtering out .planning/ commits. + +- Classifies commits: code-only (include), planning-only (exclude), mixed (include sans .planning/) +- Cherry-picks code commits onto a clean branch +- Reviewers see only code changes, no GSD artifacts + +Usage: `/gsd:pr-branch` or `/gsd:pr-branch main` + +--- + +**`/gsd:plant-seed [idea]`** +Capture a forward-looking idea with trigger conditions for automatic surfacing. + +- Seeds preserve WHY, WHEN to surface, and breadcrumbs to related code +- Auto-surfaces during `/gsd:new-milestone` when trigger conditions match +- Better than deferred items — triggers are checked, not forgotten + +Usage: `/gsd:plant-seed "add real-time notifications when we build the events system"` + ### Milestone Auditing **`/gsd:audit-milestone [version]`** diff --git a/get-shit-done/workflows/plant-seed.md b/get-shit-done/workflows/plant-seed.md new file mode 100644 index 000000000..918667cef --- /dev/null +++ b/get-shit-done/workflows/plant-seed.md @@ -0,0 +1,169 @@ + +Capture a forward-looking idea as a structured seed file with trigger conditions. +Seeds auto-surface during /gsd:new-milestone when trigger conditions match the +new milestone's scope. + +Seeds beat deferred items because they: +- Preserve WHY the idea matters (not just WHAT) +- Define WHEN to surface (trigger conditions, not manual scanning) +- Track breadcrumbs (code references, related decisions) +- Auto-present at the right time via new-milestone scan + + + + + +Parse `$ARGUMENTS` for the idea summary. + +If empty, ask: +``` +What's the idea? (one sentence) +``` + +Store as `$IDEA`. + + + +```bash +mkdir -p .planning/seeds +``` + + + +Ask focused questions to build a complete seed: + +``` +AskUserQuestion( + header: "Trigger", + question: "When should this idea surface? (e.g., 'when we add user accounts', 'next major version', 'when performance becomes a priority')", + options: [] // freeform +) +``` + +Store as `$TRIGGER`. + +``` +AskUserQuestion( + header: "Why", + question: "Why does this matter? What problem does it solve or what opportunity does it create?", + options: [] +) +``` + +Store as `$WHY`. + +``` +AskUserQuestion( + header: "Scope", + question: "How big is this? (rough estimate)", + options: [ + { label: "Small", description: "A few hours — could be a quick task" }, + { label: "Medium", description: "A phase or two — needs planning" }, + { label: "Large", description: "A full milestone — significant effort" } + ] +) +``` + +Store as `$SCOPE`. + + + +Search the codebase for relevant references: + +```bash +# Find files related to the idea keywords +grep -rl "$KEYWORD" --include="*.ts" --include="*.js" --include="*.md" . 2>/dev/null | head -10 +``` + +Also check: +- Current STATE.md for related decisions +- ROADMAP.md for related phases +- todos/ for related captured ideas + +Store relevant file paths as `$BREADCRUMBS`. + + + +```bash +# Find next seed number +EXISTING=$(ls .planning/seeds/SEED-*.md 2>/dev/null | wc -l) +NEXT=$((EXISTING + 1)) +PADDED=$(printf "%03d" $NEXT) +``` + +Generate slug from idea summary. + + + +Write `.planning/seeds/SEED-{PADDED}-{slug}.md`: + +```markdown +--- +id: SEED-{PADDED} +status: dormant +planted: {ISO date} +planted_during: {current milestone/phase from STATE.md} +trigger_when: {$TRIGGER} +scope: {$SCOPE} +--- + +# SEED-{PADDED}: {$IDEA} + +## Why This Matters + +{$WHY} + +## When to Surface + +**Trigger:** {$TRIGGER} + +This seed should be presented during `/gsd:new-milestone` when the milestone +scope matches any of these conditions: +- {trigger condition 1} +- {trigger condition 2} + +## Scope Estimate + +**{$SCOPE}** — {elaboration based on scope choice} + +## Breadcrumbs + +Related code and decisions found in the current codebase: + +{list of $BREADCRUMBS with file paths} + +## Notes + +{any additional context from the current session} +``` + + + +```bash +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: plant seed — {$IDEA}" --files .planning/seeds/SEED-{PADDED}-{slug}.md +``` + + + +``` +✅ Seed planted: SEED-{PADDED} + +"{$IDEA}" +Trigger: {$TRIGGER} +Scope: {$SCOPE} +File: .planning/seeds/SEED-{PADDED}-{slug}.md + +This seed will surface automatically when you run /gsd:new-milestone +and the milestone scope matches the trigger condition. +``` + + + + + +- [ ] Seed file created in .planning/seeds/ +- [ ] Frontmatter includes status, trigger, scope +- [ ] Breadcrumbs collected from codebase +- [ ] Committed to git +- [ ] User shown confirmation with trigger info + diff --git a/get-shit-done/workflows/pr-branch.md b/get-shit-done/workflows/pr-branch.md new file mode 100644 index 000000000..11697d473 --- /dev/null +++ b/get-shit-done/workflows/pr-branch.md @@ -0,0 +1,129 @@ + +Create a clean branch for pull requests by filtering out .planning/ commits. +The PR branch contains only code changes — reviewers don't see GSD artifacts +(PLAN.md, SUMMARY.md, STATE.md, CONTEXT.md, etc.). + +Uses git cherry-pick with path filtering to rebuild a clean history. + + + + + +Parse `$ARGUMENTS` for target branch (default: `main`). + +```bash +CURRENT_BRANCH=$(git branch --show-current) +TARGET=${1:-main} +``` + +Check preconditions: +- Must be on a feature branch (not main/master) +- Must have commits ahead of target + +```bash +AHEAD=$(git rev-list --count "$TARGET".."$CURRENT_BRANCH" 2>/dev/null) +if [ "$AHEAD" = "0" ]; then + echo "No commits ahead of $TARGET — nothing to filter." + exit 0 +fi +``` + +Display: +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + GSD ► PR BRANCH +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Branch: {CURRENT_BRANCH} +Target: {TARGET} +Commits: {AHEAD} ahead +``` + + + +Classify commits: + +```bash +# Get all commits ahead of target +git log --oneline "$TARGET".."$CURRENT_BRANCH" --no-merges +``` + +For each commit, check if it ONLY touches .planning/ files: + +```bash +# For each commit hash +FILES=$(git diff-tree --no-commit-id --name-only -r $HASH) +ALL_PLANNING=$(echo "$FILES" | grep -v "^\.planning/" | wc -l) +``` + +Classify: +- **Code commits**: Touch at least one non-.planning/ file → INCLUDE +- **Planning-only commits**: Touch only .planning/ files → EXCLUDE +- **Mixed commits**: Touch both → INCLUDE (planning changes come along) + +Display analysis: +``` +Commits to include: {N} (code changes) +Commits to exclude: {N} (planning-only) +Mixed commits: {N} (code + planning — included) +``` + + + +```bash +PR_BRANCH="${CURRENT_BRANCH}-pr" + +# Create PR branch from target +git checkout -b "$PR_BRANCH" "$TARGET" +``` + +Cherry-pick only code commits (in order): + +```bash +for HASH in $CODE_COMMITS; do + git cherry-pick "$HASH" --no-commit + # Remove any .planning/ files that came along in mixed commits + git rm -r --cached .planning/ 2>/dev/null || true + git commit -C "$HASH" +done +``` + +Return to original branch: +```bash +git checkout "$CURRENT_BRANCH" +``` + + + +```bash +# Verify no .planning/ files in PR branch +PLANNING_FILES=$(git diff --name-only "$TARGET".."$PR_BRANCH" | grep "^\.planning/" | wc -l) +TOTAL_FILES=$(git diff --name-only "$TARGET".."$PR_BRANCH" | wc -l) +PR_COMMITS=$(git rev-list --count "$TARGET".."$PR_BRANCH") +``` + +Display results: +``` +✅ PR branch created: {PR_BRANCH} + +Original: {AHEAD} commits, {ORIGINAL_FILES} files +PR branch: {PR_COMMITS} commits, {TOTAL_FILES} files +Planning files: {PLANNING_FILES} (should be 0) + +Next steps: + git push origin {PR_BRANCH} + gh pr create --base {TARGET} --head {PR_BRANCH} + +Or use /gsd:ship to create the PR automatically. +``` + + + + + +- [ ] PR branch created from target +- [ ] Planning-only commits excluded +- [ ] No .planning/ files in PR branch diff +- [ ] Commit messages preserved from original +- [ ] User shown next steps + diff --git a/get-shit-done/workflows/review.md b/get-shit-done/workflows/review.md new file mode 100644 index 000000000..99a13da95 --- /dev/null +++ b/get-shit-done/workflows/review.md @@ -0,0 +1,228 @@ + +Cross-AI peer review — invoke external AI CLIs to independently review phase plans. +Each CLI gets the same prompt (PROJECT.md context, phase plans, requirements) and +produces structured feedback. Results are combined into REVIEWS.md for the planner +to incorporate via --reviews flag. + +This implements adversarial review: different AI models catch different blind spots. +A plan that survives review from 2-3 independent AI systems is more robust. + + + + + +Check which AI CLIs are available on the system: + +```bash +# Check each CLI +command -v gemini >/dev/null 2>&1 && echo "gemini:available" || echo "gemini:missing" +command -v claude >/dev/null 2>&1 && echo "claude:available" || echo "claude:missing" +command -v codex >/dev/null 2>&1 && echo "codex:available" || echo "codex:missing" +``` + +Parse flags from `$ARGUMENTS`: +- `--gemini` → include Gemini +- `--claude` → include Claude +- `--codex` → include Codex +- `--all` → include all available +- No flags → include all available + +If no CLIs are available: +``` +No external AI CLIs found. Install at least one: +- gemini: https://github.com/google-gemini/gemini-cli +- codex: https://github.com/openai/codex +- claude: https://github.com/anthropics/claude-code + +Then run /gsd:review again. +``` +Exit. + +If only one CLI is the current runtime (e.g. running inside Claude), skip it for the review +to ensure independence. At least one DIFFERENT CLI must be available. + + + +Collect phase artifacts for the review prompt: + +```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 +``` + +Read from init: `phase_dir`, `phase_number`, `padded_phase`. + +Then read: +1. `.planning/PROJECT.md` (first 80 lines — project context) +2. Phase section from `.planning/ROADMAP.md` +3. All `*-PLAN.md` files in the phase directory +4. `*-CONTEXT.md` if present (user decisions) +5. `*-RESEARCH.md` if present (domain research) +6. `.planning/REQUIREMENTS.md` (requirements this phase addresses) + + + +Build a structured review prompt: + +```markdown +# Cross-AI Plan Review Request + +You are reviewing implementation plans for a software project phase. +Provide structured feedback on plan quality, completeness, and risks. + +## Project Context +{first 80 lines of PROJECT.md} + +## Phase {N}: {phase name} +### Roadmap Section +{roadmap phase section} + +### Requirements Addressed +{requirements for this phase} + +### User Decisions (CONTEXT.md) +{context if present} + +### Research Findings +{research if present} + +### Plans to Review +{all PLAN.md contents} + +## Review Instructions + +Analyze each plan and provide: + +1. **Summary** — One-paragraph assessment +2. **Strengths** — What's well-designed (bullet points) +3. **Concerns** — Potential issues, gaps, risks (bullet points with severity: HIGH/MEDIUM/LOW) +4. **Suggestions** — Specific improvements (bullet points) +5. **Risk Assessment** — Overall risk level (LOW/MEDIUM/HIGH) with justification + +Focus on: +- Missing edge cases or error handling +- Dependency ordering issues +- Scope creep or over-engineering +- Security considerations +- Performance implications +- Whether the plans actually achieve the phase goals + +Output your review in markdown format. +``` + +Write to a temp file: `/tmp/gsd-review-prompt-{phase}.md` + + + +For each selected CLI, invoke in sequence (not parallel — avoid rate limits): + +**Gemini:** +```bash +gemini -p "$(cat /tmp/gsd-review-prompt-{phase}.md)" 2>/dev/null > /tmp/gsd-review-gemini-{phase}.md +``` + +**Claude (separate session):** +```bash +claude -p "$(cat /tmp/gsd-review-prompt-{phase}.md)" --no-input 2>/dev/null > /tmp/gsd-review-claude-{phase}.md +``` + +**Codex:** +```bash +codex -p "$(cat /tmp/gsd-review-prompt-{phase}.md)" 2>/dev/null > /tmp/gsd-review-codex-{phase}.md +``` + +If a CLI fails, log the error and continue with remaining CLIs. + +Display progress: +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + GSD ► CROSS-AI REVIEW — Phase {N} +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +◆ Reviewing with {CLI}... done ✓ +◆ Reviewing with {CLI}... done ✓ +``` + + + +Combine all review responses into `{phase_dir}/{padded_phase}-REVIEWS.md`: + +```markdown +--- +phase: {N} +reviewers: [gemini, claude, codex] +reviewed_at: {ISO timestamp} +plans_reviewed: [{list of PLAN.md files}] +--- + +# Cross-AI Plan Review — Phase {N} + +## Gemini Review + +{gemini review content} + +--- + +## Claude Review + +{claude review content} + +--- + +## Codex Review + +{codex review content} + +--- + +## Consensus Summary + +{synthesize common concerns across all reviewers} + +### Agreed Strengths +{strengths mentioned by 2+ reviewers} + +### Agreed Concerns +{concerns raised by 2+ reviewers — highest priority} + +### Divergent Views +{where reviewers disagreed — worth investigating} +``` + +Commit: +```bash +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: cross-AI review for phase {N}" --files {phase_dir}/{padded_phase}-REVIEWS.md +``` + + + +Display summary: + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + GSD ► REVIEW COMPLETE +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Phase {N} reviewed by {count} AI systems. + +Consensus concerns: +{top 3 shared concerns} + +Full review: {padded_phase}-REVIEWS.md + +To incorporate feedback into planning: + /gsd:plan-phase {N} --reviews +``` + +Clean up temp files. + + + + + +- [ ] At least one external CLI invoked successfully +- [ ] REVIEWS.md written with structured feedback +- [ ] Consensus summary synthesized from multiple reviewers +- [ ] Temp files cleaned up +- [ ] User knows how to use feedback (/gsd:plan-phase --reviews) + diff --git a/tests/copilot-install.test.cjs b/tests/copilot-install.test.cjs index b6510d0e7..249d8a89a 100644 --- a/tests/copilot-install.test.cjs +++ b/tests/copilot-install.test.cjs @@ -625,7 +625,7 @@ describe('copyCommandsAsCopilotSkills', () => { // Count gsd-* directories — should be 31 const dirs = fs.readdirSync(tempDir, { withFileTypes: true }) .filter(e => e.isDirectory() && e.name.startsWith('gsd-')); - assert.strictEqual(dirs.length, 43, `expected 43 skill folders, got ${dirs.length}`); + assert.strictEqual(dirs.length, 46, `expected 46 skill folders, got ${dirs.length}`); } finally { fs.rmSync(tempDir, { recursive: true }); } @@ -1119,7 +1119,7 @@ const { execFileSync } = require('child_process'); const crypto = require('crypto'); const INSTALL_PATH = path.join(__dirname, '..', 'bin', 'install.js'); -const EXPECTED_SKILLS = 43; +const EXPECTED_SKILLS = 46; const EXPECTED_AGENTS = 16; function runCopilotInstall(cwd) { From 60a76ae06e00e30926fdc897e3b85aad13c071c9 Mon Sep 17 00:00:00 2001 From: jecanore Date: Wed, 18 Mar 2026 23:30:09 -0500 Subject: [PATCH 66/83] feat: add verification debt tracking and /gsd:audit-uat command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prevent silent loss of UAT/verification items when projects advance. Surfaces outstanding items across all prior phases so nothing is forgotten. New command: - /gsd:audit-uat — cross-phase audit with categorized report and test plan New capabilities: - Cross-phase health check in /gsd:progress (Step 1.6) - status: partial for incomplete UAT sessions - result: blocked with blocked_by tag for dependency-gated tests - human_needed items persisted as trackable HUMAN-UAT.md files - Phase completion and transition warnings for verification debt Files: 4 new, 14 modified (9 feature + 5 docs) Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 7 + commands/gsd/audit-uat.md | 24 ++ docs/ARCHITECTURE.md | 6 +- docs/CLI-TOOLS.md | 8 +- docs/COMMANDS.md | 13 + docs/FEATURES.md | 34 +++ get-shit-done/bin/gsd-tools.cjs | 9 + get-shit-done/bin/lib/phase.cjs | 23 ++ get-shit-done/bin/lib/uat.cjs | 189 +++++++++++++ get-shit-done/templates/UAT.md | 24 +- get-shit-done/workflows/audit-uat.md | 109 ++++++++ get-shit-done/workflows/execute-phase.md | 63 ++++- get-shit-done/workflows/help.md | 11 + get-shit-done/workflows/progress.md | 60 ++++- get-shit-done/workflows/transition.md | 24 ++ get-shit-done/workflows/verify-work.md | 41 ++- tests/copilot-install.test.cjs | 4 +- tests/uat.test.cjs | 326 +++++++++++++++++++++++ 18 files changed, 962 insertions(+), 13 deletions(-) create mode 100644 commands/gsd/audit-uat.md create mode 100644 get-shit-done/bin/lib/uat.cjs create mode 100644 get-shit-done/workflows/audit-uat.md create mode 100644 tests/uat.test.cjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 43618437d..adebc2e20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,13 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - **Model alias-to-full-ID resolution** — Task API compatibility for model alias strings (#991) - **Execution hardening** — Pre-wave dependency checks, cross-plan data contracts, and export-level spot checks (#1082) - **Markdown normalization** — Generated markdown conforms to markdownlint standards (#1112) +- **`/gsd:audit-uat` command** — Cross-phase audit of all outstanding UAT and verification items. Scans every phase for pending, skipped, blocked, and human_needed items. Cross-references against codebase to detect stale documentation. Produces prioritized human test plan grouped by testability +- **Verification debt tracking** — Five structural improvements to prevent silent loss of UAT/verification items when projects advance: + - Cross-phase health check in `/gsd:progress` (Step 1.6) surfaces outstanding items from ALL prior phases + - `status: partial` in UAT files distinguishes incomplete testing from completed sessions + - `result: blocked` with `blocked_by` tag for tests blocked by external dependencies (server, device, build, third-party) + - `human_needed` verification items now persist as HUMAN-UAT.md files (trackable across sessions) + - Phase completion and transition warnings surface verification debt non-blockingly ### Changed - Test suite consolidated: runtime converters deduplicated, helpers standardized (#1169) diff --git a/commands/gsd/audit-uat.md b/commands/gsd/audit-uat.md new file mode 100644 index 000000000..604e1be12 --- /dev/null +++ b/commands/gsd/audit-uat.md @@ -0,0 +1,24 @@ +--- +name: gsd:audit-uat +description: Cross-phase audit of all outstanding UAT and verification items +allowed-tools: + - Read + - Glob + - Grep + - Bash +--- + +Scan all phases for pending, skipped, blocked, and human_needed UAT items. Cross-reference against codebase to detect stale documentation. Produce prioritized human test plan. + + + +@~/.claude/get-shit-done/workflows/audit-uat.md + + + +Core planning files are loaded in-workflow via CLI. + +**Scope:** +Glob: .planning/phases/*/*-UAT.md +Glob: .planning/phases/*/*-VERIFICATION.md + diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3d8b4108a..f54ce8816 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -171,7 +171,7 @@ Runtime hooks that integrate with the host AI agent: ### CLI Tools (`get-shit-done/bin/`) -Node.js CLI utility (`gsd-tools.cjs`) with 11 domain modules: +Node.js CLI utility (`gsd-tools.cjs`) with 15 domain modules: | Module | Responsibility | |--------|---------------| @@ -342,8 +342,8 @@ UI-SPEC.md (per phase) ─────────────────── ├── commands/gsd/*.md # 37 slash commands ├── get-shit-done/ │ ├── bin/gsd-tools.cjs # CLI utility -│ ├── bin/lib/*.cjs # 11 domain modules -│ ├── workflows/*.md # 41 workflow definitions +│ ├── bin/lib/*.cjs # 15 domain modules +│ ├── workflows/*.md # 42 workflow definitions │ ├── references/*.md # 13 shared reference docs │ └── templates/ # Planning artifact templates ├── agents/*.md # 15 agent definitions diff --git a/docs/CLI-TOOLS.md b/docs/CLI-TOOLS.md index 2621e7769..2e1dcfb0f 100644 --- a/docs/CLI-TOOLS.md +++ b/docs/CLI-TOOLS.md @@ -9,7 +9,7 @@ `gsd-tools.cjs` is a Node.js CLI utility that replaces repetitive inline bash patterns across GSD's ~50 command, workflow, and agent files. It centralizes: config parsing, model resolution, phase lookup, git commits, summary verification, state management, and template operations. **Location:** `get-shit-done/bin/gsd-tools.cjs` -**Modules:** 11 domain modules in `get-shit-done/bin/lib/` +**Modules:** 15 domain modules in `get-shit-done/bin/lib/` **Usage:** ```bash @@ -330,6 +330,9 @@ node gsd-tools.cjs progress [json|table|bar] # Complete a todo node gsd-tools.cjs todo complete +# UAT audit — scan all phases for unresolved items +node gsd-tools.cjs audit-uat + # Git commit with config checks node gsd-tools.cjs commit [--files f1 f2] [--amend] [--no-verify] ``` @@ -358,3 +361,6 @@ node gsd-tools.cjs websearch [--limit N] [--freshness day|week|month] | Milestone | `lib/milestone.cjs` | Milestone archival, requirements marking | | Commands | `lib/commands.cjs` | Misc: slug, timestamp, todos, scaffold, stats, websearch | | Model Profiles | `lib/model-profiles.cjs` | Profile resolution table | +| UAT | `lib/uat.cjs` | Cross-phase UAT/verification audit | +| Profile Output | `lib/profile-output.cjs` | Developer profile formatting | +| Profile Pipeline | `lib/profile-pipeline.cjs` | Session analysis pipeline | diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 1e2870342..1e5ab4dd3 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -215,6 +215,19 @@ Retroactive 6-pillar visual audit of implemented frontend. --- +### `/gsd:audit-uat` + +Cross-phase audit of all outstanding UAT and verification items. + +**Prerequisites:** At least one phase has been executed with UAT or verification +**Produces:** Categorized audit report with human test plan + +```bash +/gsd:audit-uat +``` + +--- + ### `/gsd:audit-milestone` Verify milestone met its definition of done. diff --git a/docs/FEATURES.md b/docs/FEATURES.md index f62553412..cce53eb26 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -52,6 +52,7 @@ - [Hook System](#37-hook-system) - [Developer Profiling](#38-developer-profiling) - [Execution Hardening](#39-execution-hardening) + - [Verification Debt Tracking](#40-verification-debt-tracking) --- @@ -939,3 +940,36 @@ After Level 3 wiring verification passes, spot-check individual exports for actu - REQ-HARD-01: Pre-wave check MUST verify key-links from all prior wave artifacts before spawning next wave - REQ-HARD-02: Cross-plan contract check MUST detect incompatible data transformations between plans - REQ-HARD-03: Export spot-check MUST identify dead stores in wired files + +--- + +### 40. Verification Debt Tracking + +**Command:** `/gsd:audit-uat` + +**Purpose:** Prevent silent loss of UAT/verification items when projects advance past phases with outstanding tests. Surfaces verification debt across all prior phases so items are never forgotten. + +**Components:** + +**1. Cross-Phase Health Check** (progress.md Step 1.6) +Every `/gsd:progress` call scans ALL phases in the current milestone for outstanding items (pending, skipped, blocked, human_needed). Displays a non-blocking warning section with actionable links. + +**2. `status: partial`** (verify-work.md, UAT.md) +New UAT status that distinguishes between "session ended" and "all tests resolved". Prevents `status: complete` when tests are still pending, blocked, or skipped without reason. + +**3. `result: blocked` with `blocked_by` tag** (verify-work.md, UAT.md) +New test result type for tests blocked by external dependencies (server, physical device, release build, third-party services). Categorized separately from skipped tests. + +**4. HUMAN-UAT.md Persistence** (execute-phase.md) +When verification returns `human_needed`, items are persisted as a trackable HUMAN-UAT.md file with `status: partial`. Feeds into the cross-phase health check and audit systems. + +**5. Phase Completion Warnings** (phase.cjs, transition.md) +`phase complete` CLI returns verification debt warnings in its JSON output. Transition workflow surfaces outstanding items before confirmation. + +**Requirements:** +- REQ-DEBT-01: System MUST surface outstanding UAT/verification items from ALL prior phases in `/gsd:progress` +- REQ-DEBT-02: System MUST distinguish incomplete testing (partial) from completed testing (complete) +- REQ-DEBT-03: System MUST categorize blocked tests with `blocked_by` tags +- REQ-DEBT-04: System MUST persist human_needed verification items as trackable UAT files +- REQ-DEBT-05: System MUST warn (non-blocking) during phase completion and transition when verification debt exists +- REQ-DEBT-06: `/gsd:audit-uat` MUST scan all phases, categorize items by testability, and produce a human test plan diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index 633d921ff..c8df61a06 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -64,6 +64,9 @@ * Todos: * todo complete Move todo from pending to completed * + * UAT Audit: + * audit-uat Scan all phases for unresolved UAT/verification items + * * Scaffolding: * scaffold context --phase Create CONTEXT.md template * scaffold uat --phase Create UAT.md template @@ -525,6 +528,12 @@ async function main() { break; } + case 'audit-uat': { + const uat = require('./lib/uat.cjs'); + uat.cmdAuditUat(cwd, raw); + break; + } + case 'stats': { const subcommand = args[1] || 'json'; commands.cmdStats(cwd, subcommand, raw); diff --git a/get-shit-done/bin/lib/phase.cjs b/get-shit-done/bin/lib/phase.cjs index dc2c52f99..f01a59c55 100644 --- a/get-shit-done/bin/lib/phase.cjs +++ b/get-shit-done/bin/lib/phase.cjs @@ -721,6 +721,27 @@ function cmdPhaseComplete(cwd, phaseNum, raw) { const summaryCount = phaseInfo.summaries.length; let requirementsUpdated = false; + // Check for unresolved verification debt (non-blocking warnings) + const warnings = []; + try { + const phaseFullDir = path.join(cwd, phaseInfo.directory); + const phaseFiles = fs.readdirSync(phaseFullDir); + + for (const file of phaseFiles.filter(f => f.includes('-UAT') && f.endsWith('.md'))) { + const content = fs.readFileSync(path.join(phaseFullDir, file), 'utf-8'); + if (/result: pending/.test(content)) warnings.push(`${file}: has pending tests`); + if (/result: blocked/.test(content)) warnings.push(`${file}: has blocked tests`); + if (/status: partial/.test(content)) warnings.push(`${file}: testing incomplete (partial)`); + if (/status: diagnosed/.test(content)) warnings.push(`${file}: has diagnosed gaps`); + } + + for (const file of phaseFiles.filter(f => f.includes('-VERIFICATION') && f.endsWith('.md'))) { + const content = fs.readFileSync(path.join(phaseFullDir, file), 'utf-8'); + if (/status: human_needed/.test(content)) warnings.push(`${file}: needs human verification`); + if (/status: gaps_found/.test(content)) warnings.push(`${file}: has unresolved gaps`); + } + } catch {} + // Update ROADMAP.md: mark phase complete if (fs.existsSync(roadmapPath)) { let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8'); @@ -922,6 +943,8 @@ function cmdPhaseComplete(cwd, phaseNum, raw) { roadmap_updated: fs.existsSync(roadmapPath), state_updated: fs.existsSync(statePath), requirements_updated: requirementsUpdated, + warnings, + has_warnings: warnings.length > 0, }; output(result, raw); diff --git a/get-shit-done/bin/lib/uat.cjs b/get-shit-done/bin/lib/uat.cjs new file mode 100644 index 000000000..652fae16e --- /dev/null +++ b/get-shit-done/bin/lib/uat.cjs @@ -0,0 +1,189 @@ +/** + * UAT Audit — Cross-phase UAT/VERIFICATION scanner + * + * Reads all *-UAT.md and *-VERIFICATION.md files across all phases. + * Extracts non-passing items. Returns structured JSON for workflow consumption. + */ + +const fs = require('fs'); +const path = require('path'); +const { output, error, getMilestonePhaseFilter } = require('./core.cjs'); +const { extractFrontmatter } = require('./frontmatter.cjs'); + +function cmdAuditUat(cwd, raw) { + const phasesDir = path.join(cwd, '.planning', 'phases'); + if (!fs.existsSync(phasesDir)) { + error('No .planning/phases directory found'); + } + + const isDirInMilestone = getMilestonePhaseFilter(cwd); + const results = []; + + // Scan all phase directories + const dirs = fs.readdirSync(phasesDir, { withFileTypes: true }) + .filter(e => e.isDirectory()) + .map(e => e.name) + .filter(isDirInMilestone) + .sort(); + + for (const dir of dirs) { + const phaseMatch = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)/i); + const phaseNum = phaseMatch ? phaseMatch[1] : dir; + const phaseDir = path.join(phasesDir, dir); + const files = fs.readdirSync(phaseDir); + + // Process UAT files + for (const file of files.filter(f => f.includes('-UAT') && f.endsWith('.md'))) { + const content = fs.readFileSync(path.join(phaseDir, file), 'utf-8'); + const items = parseUatItems(content); + if (items.length > 0) { + results.push({ + phase: phaseNum, + phase_dir: dir, + file, + file_path: `.planning/phases/${dir}/${file}`, + type: 'uat', + status: (extractFrontmatter(content).status || 'unknown'), + items, + }); + } + } + + // Process VERIFICATION files + for (const file of files.filter(f => f.includes('-VERIFICATION') && f.endsWith('.md'))) { + const content = fs.readFileSync(path.join(phaseDir, file), 'utf-8'); + const status = extractFrontmatter(content).status || 'unknown'; + if (status === 'human_needed' || status === 'gaps_found') { + const items = parseVerificationItems(content, status); + if (items.length > 0) { + results.push({ + phase: phaseNum, + phase_dir: dir, + file, + file_path: `.planning/phases/${dir}/${file}`, + type: 'verification', + status, + items, + }); + } + } + } + } + + // Compute summary + const summary = { + total_files: results.length, + total_items: results.reduce((sum, r) => sum + r.items.length, 0), + by_category: {}, + by_phase: {}, + }; + + for (const r of results) { + if (!summary.by_phase[r.phase]) summary.by_phase[r.phase] = 0; + for (const item of r.items) { + summary.by_phase[r.phase]++; + const cat = item.category || 'unknown'; + summary.by_category[cat] = (summary.by_category[cat] || 0) + 1; + } + } + + output({ results, summary }, raw); +} + +function parseUatItems(content) { + const items = []; + // Match test blocks: ### N. Name\nexpected: ...\nresult: ...\n + const testPattern = /###\s*(\d+)\.\s*([^\n]+)\nexpected:\s*([^\n]+)\nresult:\s*(\w+)(?:\n(?:reported|reason|blocked_by):\s*[^\n]*)?/g; + let match; + while ((match = testPattern.exec(content)) !== null) { + const [, num, name, expected, result] = match; + if (result === 'pending' || result === 'skipped' || result === 'blocked') { + // Extract optional fields — limit to current test block (up to next ### or EOF) + const afterMatch = content.slice(match.index); + const nextHeading = afterMatch.indexOf('\n###', 1); + const blockText = nextHeading > 0 ? afterMatch.slice(0, nextHeading) : afterMatch; + const reasonMatch = blockText.match(/reason:\s*(.+)/); + const blockedByMatch = blockText.match(/blocked_by:\s*(.+)/); + + const item = { + test: parseInt(num, 10), + name: name.trim(), + expected: expected.trim(), + result, + category: categorizeItem(result, reasonMatch?.[1], blockedByMatch?.[1]), + }; + if (reasonMatch) item.reason = reasonMatch[1].trim(); + if (blockedByMatch) item.blocked_by = blockedByMatch[1].trim(); + items.push(item); + } + } + return items; +} + +function parseVerificationItems(content, status) { + const items = []; + if (status === 'human_needed') { + // Extract from human_verification section — look for numbered items or table rows + const hvSection = content.match(/##\s*Human Verification.*?\n([\s\S]*?)(?=\n##\s|\n---\s|$)/i); + if (hvSection) { + const lines = hvSection[1].split('\n'); + for (const line of lines) { + // Match table rows: | N | description | ... | + const tableMatch = line.match(/\|\s*(\d+)\s*\|\s*([^|]+)/); + // Match bullet items: - description + const bulletMatch = line.match(/^[-*]\s+(.+)/); + // Match numbered items: 1. description + const numberedMatch = line.match(/^(\d+)\.\s+(.+)/); + + if (tableMatch) { + items.push({ + test: parseInt(tableMatch[1], 10), + name: tableMatch[2].trim(), + result: 'human_needed', + category: 'human_uat', + }); + } else if (numberedMatch) { + items.push({ + test: parseInt(numberedMatch[1], 10), + name: numberedMatch[2].trim(), + result: 'human_needed', + category: 'human_uat', + }); + } else if (bulletMatch && bulletMatch[1].length > 10) { + items.push({ + name: bulletMatch[1].trim(), + result: 'human_needed', + category: 'human_uat', + }); + } + } + } + } + // gaps_found items are already handled by plan-phase --gaps pipeline + return items; +} + +function categorizeItem(result, reason, blockedBy) { + if (result === 'blocked' || blockedBy) { + if (blockedBy) { + if (/server/i.test(blockedBy)) return 'server_blocked'; + if (/device|physical/i.test(blockedBy)) return 'device_needed'; + if (/build|release|preview/i.test(blockedBy)) return 'build_needed'; + if (/third.party|twilio|stripe/i.test(blockedBy)) return 'third_party'; + } + return 'blocked'; + } + if (result === 'skipped') { + if (reason) { + if (/server|not running|not available/i.test(reason)) return 'server_blocked'; + if (/simulator|physical|device/i.test(reason)) return 'device_needed'; + if (/build|release|preview/i.test(reason)) return 'build_needed'; + } + return 'skipped_unresolved'; + } + if (result === 'pending') return 'pending'; + if (result === 'human_needed') return 'human_uat'; + return 'unknown'; +} + +module.exports = { cmdAuditUat }; diff --git a/get-shit-done/templates/UAT.md b/get-shit-done/templates/UAT.md index 5e2118ed2..fd2345a98 100644 --- a/get-shit-done/templates/UAT.md +++ b/get-shit-done/templates/UAT.md @@ -8,7 +8,7 @@ Template for `.planning/phases/XX-name/{phase_num}-UAT.md` — persistent UAT se ```markdown --- -status: testing | complete | diagnosed +status: testing | partial | complete | diagnosed phase: XX-name source: [list of SUMMARY.md files tested] started: [ISO timestamp] @@ -45,6 +45,12 @@ expected: [observable behavior] result: skipped reason: [why skipped] +### 5. [Test Name] +expected: [observable behavior] +result: blocked +blocked_by: server | physical-device | release-build | third-party | prior-phase +reason: [why blocked] + ... ## Summary @@ -54,6 +60,7 @@ passed: [N] issues: [N] pending: [N] skipped: [N] +blocked: [N] ## Gaps @@ -74,7 +81,7 @@ skipped: [N] **Frontmatter:** -- `status`: OVERWRITE - "testing" or "complete" +- `status`: OVERWRITE - "testing", "partial", or "complete" - `phase`: IMMUTABLE - set on creation - `source`: IMMUTABLE - SUMMARY files being tested - `started`: IMMUTABLE - set on creation @@ -87,9 +94,10 @@ skipped: [N] **Tests:** - Each test: OVERWRITE result field when user responds -- `result` values: [pending], pass, issue, skipped +- `result` values: [pending], pass, issue, skipped, blocked - If issue: add `reported` (verbatim) and `severity` (inferred) - If skipped: add `reason` if provided +- If blocked: add `blocked_by` (tag) and `reason` (if provided) **Summary:** - OVERWRITE counts after each response @@ -156,6 +164,16 @@ skipped: [N] - Commit file - Present summary with next steps +**Partial completion:** +- status → "partial" (if pending, blocked, or unresolved skipped tests remain) +- Current Test → "[testing paused — {N} items outstanding]" +- Commit file +- Present summary with outstanding items highlighted + +**Resuming partial session:** +- `/gsd:verify-work {phase}` picks up from first pending/blocked test +- When all items resolved, status advances to "complete" + **Resume after /clear:** 1. Read frontmatter → know phase and status 2. Read Current Test → know where we are diff --git a/get-shit-done/workflows/audit-uat.md b/get-shit-done/workflows/audit-uat.md new file mode 100644 index 000000000..13e95ce79 --- /dev/null +++ b/get-shit-done/workflows/audit-uat.md @@ -0,0 +1,109 @@ + +Cross-phase audit of all UAT and verification files. Finds every outstanding item (pending, skipped, blocked, human_needed), optionally verifies against the codebase to detect stale docs, and produces a prioritized human test plan. + + + + + +Run the CLI audit: + +```bash +AUDIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" audit-uat --raw) +``` + +Parse JSON for `results` array and `summary` object. + +If `summary.total_items` is 0: +``` +## All Clear + +No outstanding UAT or verification items found across all phases. +All tests are passing, resolved, or diagnosed with fix plans. +``` +Stop here. + + + +Group items by what's actionable NOW vs. what needs prerequisites: + +**Testable Now** (no external dependencies): +- `pending` — tests never run +- `human_uat` — human verification items +- `skipped_unresolved` — skipped without clear blocking reason + +**Needs Prerequisites:** +- `server_blocked` — needs external server running +- `device_needed` — needs physical device (not simulator) +- `build_needed` — needs release/preview build +- `third_party` — needs external service configuration + +For each item in "Testable Now", use Grep/Read to check if the underlying feature still exists in the codebase: +- If the test references a component/function that no longer exists → mark as `stale` +- If the test references code that has been significantly rewritten → mark as `needs_update` +- Otherwise → mark as `active` + + + +Present the audit report: + +``` +## UAT Audit Report + +**{total_items} outstanding items across {total_files} files in {phase_count} phases** + +### Testable Now ({count}) + +| # | Phase | Test | Description | Status | +|---|-------|------|-------------|--------| +| 1 | {phase} | {test_name} | {expected} | {active/stale/needs_update} | +... + +### Needs Prerequisites ({count}) + +| # | Phase | Test | Blocked By | Description | +|---|-------|------|------------|-------------| +| 1 | {phase} | {test_name} | {category} | {expected} | +... + +### Stale (can be closed) ({count}) + +| # | Phase | Test | Why Stale | +|---|-------|------|-----------| +| 1 | {phase} | {test_name} | {reason} | +... + +--- + +## Recommended Actions + +1. **Close stale items:** `/gsd:verify-work {phase}` — mark stale tests as resolved +2. **Run active tests:** Human UAT test plan below +3. **When prerequisites met:** Retest blocked items with `/gsd:verify-work {phase}` +``` + + + +Generate a human UAT test plan for "Testable Now" + "active" items only: + +Group by what can be tested together (same screen, same feature, same prerequisite): + +``` +## Human UAT Test Plan + +### Group 1: {category — e.g., "Billing Flow"} +Prerequisites: {what needs to be running/configured} + +1. **{Test name}** (Phase {N}) + - Navigate to: {where} + - Do: {action} + - Expected: {expected behavior} + +2. **{Test name}** (Phase {N}) + ... + +### Group 2: {category} +... +``` + + + diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index 552afb318..0d6c42704 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -525,6 +525,51 @@ grep "^status:" "$PHASE_DIR"/*-VERIFICATION.md | cut -d: -f2 | tr -d ' ' | `gaps_found` | Present gap summary, offer `/gsd:plan-phase {phase} --gaps` | **If human_needed:** + +**Step A: Persist human verification items as UAT file.** + +Create `{phase_dir}/{phase_num}-HUMAN-UAT.md` using UAT template format: + +```markdown +--- +status: partial +phase: {phase_num}-{phase_name} +source: [{phase_num}-VERIFICATION.md] +started: [now ISO] +updated: [now ISO] +--- + +## Current Test + +[awaiting human testing] + +## Tests + +{For each human_verification item from VERIFICATION.md:} + +### {N}. {item description} +expected: {expected behavior from VERIFICATION.md} +result: [pending] + +## Summary + +total: {count} +passed: 0 +issues: 0 +pending: {count} +skipped: 0 +blocked: 0 + +## Gaps +``` + +Commit the file: +```bash +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "test({phase_num}): persist human verification items as UAT" --files "{phase_dir}/{phase_num}-HUMAN-UAT.md" +``` + +**Step B: Present to user:** + ``` ## ✓ Phase {X}: {Name} — Human Verification Required @@ -532,9 +577,15 @@ All automated checks passed. {N} items need human testing: {From VERIFICATION.md human_verification section} +Items saved to `{phase_num}-HUMAN-UAT.md` — they will appear in `/gsd:progress` and `/gsd:audit-uat`. + "approved" → continue | Report issues → gap closure ``` +**If user says "approved":** Proceed to `update_roadmap`. The HUMAN-UAT.md file persists with `status: partial` and will surface in future progress checks until the user runs `/gsd:verify-work` on it. + +**If user reports issues:** Proceed to gap closure as currently implemented. + **If gaps_found:** ``` ## ⚠ Phase {X}: {Name} — Gaps Found @@ -572,8 +623,18 @@ The CLI handles: - Updating plan count to final - Advancing STATE.md to next phase - Updating REQUIREMENTS.md traceability +- Scanning for verification debt (returns `warnings` array) -Extract from result: `next_phase`, `next_phase_name`, `is_last_phase`. +Extract from result: `next_phase`, `next_phase_name`, `is_last_phase`, `warnings`, `has_warnings`. + +**If has_warnings is true:** +``` +## Phase {X} marked complete with {N} warnings: + +{list each warning} + +These items are tracked and will appear in `/gsd:progress` and `/gsd:audit-uat`. +``` ```bash node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(phase-{X}): complete phase execution" --files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md {phase_dir}/*-VERIFICATION.md diff --git a/get-shit-done/workflows/help.md b/get-shit-done/workflows/help.md index 76106e909..d002edf42 100644 --- a/get-shit-done/workflows/help.md +++ b/get-shit-done/workflows/help.md @@ -371,6 +371,17 @@ Capture a forward-looking idea with trigger conditions for automatic surfacing. Usage: `/gsd:plant-seed "add real-time notifications when we build the events system"` +--- + +**`/gsd:audit-uat`** +Cross-phase audit of all outstanding UAT and verification items. +- Scans every phase for pending, skipped, blocked, and human_needed items +- Cross-references against codebase to detect stale documentation +- Produces prioritized human test plan grouped by testability +- Use before starting a new milestone to clear verification debt + +Usage: `/gsd:audit-uat` + ### Milestone Auditing **`/gsd:audit-milestone [version]`** diff --git a/get-shit-done/workflows/progress.md b/get-shit-done/workflows/progress.md index 83bf5825b..c0ba54a65 100644 --- a/get-shit-done/workflows/progress.md +++ b/get-shit-done/workflows/progress.md @@ -150,17 +150,47 @@ State: "This phase has {X} plans, {Y} summaries." Check for UAT.md files with status "diagnosed" (has gaps needing fixes). ```bash -# Check for diagnosed UAT with gaps -grep -l "status: diagnosed" .planning/phases/[current-phase-dir]/*-UAT.md 2>/dev/null +# Check for diagnosed UAT with gaps or partial (incomplete) testing +grep -l "status: diagnosed\|status: partial" .planning/phases/[current-phase-dir]/*-UAT.md 2>/dev/null ``` Track: - `uat_with_gaps`: UAT.md files with status "diagnosed" (gaps need fixing) +- `uat_partial`: UAT.md files with status "partial" (incomplete testing) + +**Step 1.6: Cross-phase health check** + +Scan ALL phases in the current milestone for outstanding verification debt using the CLI (which respects milestone boundaries via `getMilestonePhaseFilter`): + +```bash +DEBT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" audit-uat --raw 2>/dev/null) +``` + +Parse JSON for `summary.total_items` and `summary.total_files`. + +Track: `outstanding_debt` — `summary.total_items` from the audit. + +**If outstanding_debt > 0:** Add a warning section to the progress report output (in the `report` step), placed between "## What's Next" and the route suggestion: + +```markdown +## Verification Debt ({N} files across prior phases) + +| Phase | File | Issue | +|-------|------|-------| +| {phase} | {filename} | {pending_count} pending, {skipped_count} skipped, {blocked_count} blocked | +| {phase} | {filename} | human_needed — {count} items | + +Review: `/gsd:audit-uat` — full cross-phase audit +Resume testing: `/gsd:verify-work {phase}` — retest specific phase +``` + +This is a WARNING, not a blocker — routing proceeds normally. The debt is visible so the user can make an informed choice. **Step 2: Route based on counts** | Condition | Meaning | Action | |-----------|---------|--------| +| uat_partial > 0 | UAT testing incomplete | Go to **Route E.2** | | uat_with_gaps > 0 | UAT gaps need fix plans | Go to **Route E** | | summaries < plans | Unexecuted plans exist | Go to **Route A** | | summaries = plans AND plans > 0 | Phase complete | Go to Step 3 | @@ -260,6 +290,32 @@ UAT.md exists with gaps (diagnosed issues). User needs to plan fixes. --- +**Route E.2: UAT testing incomplete (partial)** + +UAT.md exists with `status: partial` — testing session ended before all items resolved. + +``` +--- + +## Incomplete UAT Testing + +**{phase_num}-UAT.md** has {N} unresolved tests (pending, blocked, or skipped). + +`/gsd:verify-work {phase}` — resume testing from where you left off + +`/clear` first → fresh context window + +--- + +**Also available:** +- `/gsd:audit-uat` — full cross-phase UAT audit +- `/gsd:execute-phase {phase}` — execute phase plans + +--- +``` + +--- + **Step 3: Check milestone status (only when phase complete)** Read ROADMAP.md and identify: diff --git a/get-shit-done/workflows/transition.md b/get-shit-done/workflows/transition.md index d3073cb6a..dec8af17a 100644 --- a/get-shit-done/workflows/transition.md +++ b/get-shit-done/workflows/transition.md @@ -74,6 +74,30 @@ cat .planning/config.json 2>/dev/null +**Check for verification debt in this phase:** + +```bash +# Count outstanding items in current phase +OUTSTANDING="" +for f in .planning/phases/XX-current/*-UAT.md .planning/phases/XX-current/*-VERIFICATION.md; do + [ -f "$f" ] || continue + grep -q "result: pending\|result: blocked\|status: partial\|status: human_needed\|status: diagnosed" "$f" && OUTSTANDING="$OUTSTANDING\n$(basename $f)" +done +``` + +**If OUTSTANDING is not empty:** + +Append to the completion confirmation message (regardless of mode): + +``` +Outstanding verification items in this phase: +{list filenames} + +These will carry forward as debt. Review: `/gsd:audit-uat` +``` + +This does NOT block transition — it ensures the user sees the debt before confirming. + **If all plans complete:** diff --git a/get-shit-done/workflows/verify-work.md b/get-shit-done/workflows/verify-work.md index deed81d19..7cade7f32 100644 --- a/get-shit-done/workflows/verify-work.md +++ b/get-shit-done/workflows/verify-work.md @@ -231,6 +231,29 @@ result: skipped reason: [user's reason if provided] ``` +**If response indicates blocked:** +- "blocked", "can't test - server not running", "need physical device", "need release build" +- Or any response containing: "server", "blocked", "not running", "physical device", "release build" + +Infer blocked_by tag from response: +- Contains: server, not running, gateway, API → `server` +- Contains: physical, device, hardware, real phone → `physical-device` +- Contains: release, preview, build, EAS → `release-build` +- Contains: stripe, twilio, third-party, configure → `third-party` +- Contains: depends on, prior phase, prerequisite → `prior-phase` +- Default: `other` + +Update Tests section: +``` +### {N}. {name} +expected: {expected} +result: blocked +blocked_by: {inferred tag} +reason: "{verbatim user response}" +``` + +Note: Blocked tests do NOT go into the Gaps section (they aren't code issues — they're prerequisite gates). + **If response is anything else:** - Treat as issue description @@ -293,8 +316,24 @@ Proceed to `present_test`. **Complete testing and commit:** +**Determine final status:** + +Count results: +- `pending_count`: tests with `result: [pending]` +- `blocked_count`: tests with `result: blocked` +- `skipped_no_reason`: tests with `result: skipped` and no `reason` field + +``` +if pending_count > 0 OR blocked_count > 0 OR skipped_no_reason > 0: + status: partial + # Session ended but not all tests resolved +else: + status: complete + # All tests have a definitive result (pass, issue, or skipped-with-reason) +``` + Update frontmatter: -- status: complete +- status: {computed status} - updated: [now] Clear Current Test section: diff --git a/tests/copilot-install.test.cjs b/tests/copilot-install.test.cjs index 249d8a89a..f82ecdac7 100644 --- a/tests/copilot-install.test.cjs +++ b/tests/copilot-install.test.cjs @@ -625,7 +625,7 @@ describe('copyCommandsAsCopilotSkills', () => { // Count gsd-* directories — should be 31 const dirs = fs.readdirSync(tempDir, { withFileTypes: true }) .filter(e => e.isDirectory() && e.name.startsWith('gsd-')); - assert.strictEqual(dirs.length, 46, `expected 46 skill folders, got ${dirs.length}`); + assert.strictEqual(dirs.length, 47, `expected 47 skill folders, got ${dirs.length}`); } finally { fs.rmSync(tempDir, { recursive: true }); } @@ -1119,7 +1119,7 @@ const { execFileSync } = require('child_process'); const crypto = require('crypto'); const INSTALL_PATH = path.join(__dirname, '..', 'bin', 'install.js'); -const EXPECTED_SKILLS = 46; +const EXPECTED_SKILLS = 47; const EXPECTED_AGENTS = 16; function runCopilotInstall(cwd) { diff --git a/tests/uat.test.cjs b/tests/uat.test.cjs new file mode 100644 index 000000000..3c3fbc424 --- /dev/null +++ b/tests/uat.test.cjs @@ -0,0 +1,326 @@ +/** + * GSD Tools Tests - UAT Audit + */ + +const { test, describe, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const path = require('path'); +const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs'); + +describe('audit-uat command', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('returns empty results when no UAT files exist', () => { + // Create a phase directory with no UAT files + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true }); + fs.writeFileSync(path.join(tmpDir, '.planning', 'phases', '01-foundation', '.gitkeep'), ''); + + const result = runGsdTools('audit-uat --raw', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.deepStrictEqual(output.results, []); + assert.strictEqual(output.summary.total_items, 0); + assert.strictEqual(output.summary.total_files, 0); + }); + + test('detects UAT with pending items', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(phaseDir, { recursive: true }); + + fs.writeFileSync(path.join(phaseDir, '01-UAT.md'), `--- +status: testing +phase: 01-foundation +started: 2025-01-01T00:00:00Z +updated: 2025-01-01T00:00:00Z +--- + +## Tests + +### 1. Login Form +expected: Form displays with email and password fields +result: pass + +### 2. Submit Button +expected: Submitting shows loading state +result: pending +`); + + const result = runGsdTools('audit-uat --raw', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.summary.total_items, 1); + assert.strictEqual(output.results[0].phase, '01'); + assert.strictEqual(output.results[0].items[0].result, 'pending'); + assert.strictEqual(output.results[0].items[0].category, 'pending'); + assert.strictEqual(output.results[0].items[0].name, 'Submit Button'); + }); + + test('detects UAT with blocked items and categorizes blocked_by', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '02-api'); + fs.mkdirSync(phaseDir, { recursive: true }); + + fs.writeFileSync(path.join(phaseDir, '02-UAT.md'), `--- +status: partial +phase: 02-api +started: 2025-01-01T00:00:00Z +updated: 2025-01-01T00:00:00Z +--- + +## Tests + +### 1. API Health Check +expected: Returns 200 OK +result: blocked +blocked_by: server +reason: Server not running locally +`); + + const result = runGsdTools('audit-uat --raw', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.summary.total_items, 1); + assert.strictEqual(output.results[0].items[0].result, 'blocked'); + assert.strictEqual(output.results[0].items[0].category, 'server_blocked'); + assert.strictEqual(output.results[0].items[0].blocked_by, 'server'); + }); + + test('detects false completion (complete status with pending items)', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-ui'); + fs.mkdirSync(phaseDir, { recursive: true }); + + fs.writeFileSync(path.join(phaseDir, '03-UAT.md'), `--- +status: complete +phase: 03-ui +started: 2025-01-01T00:00:00Z +updated: 2025-01-01T00:00:00Z +--- + +## Tests + +### 1. Dashboard Layout +expected: Cards render in grid +result: pass + +### 2. Mobile Responsive +expected: Grid collapses to single column on mobile +result: pending +`); + + const result = runGsdTools('audit-uat --raw', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.summary.total_items, 1); + assert.strictEqual(output.results[0].status, 'complete'); + assert.strictEqual(output.results[0].items[0].result, 'pending'); + }); + + test('extracts human_needed items from VERIFICATION files', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '04-auth'); + fs.mkdirSync(phaseDir, { recursive: true }); + + fs.writeFileSync(path.join(phaseDir, '04-VERIFICATION.md'), `--- +status: human_needed +phase: 04-auth +--- + +## Automated Checks + +All passed. + +## Human Verification + +1. Test SSO login with Google account +2. Test password reset flow end-to-end +3. Verify MFA enrollment on new device +`); + + const result = runGsdTools('audit-uat --raw', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.summary.total_items, 3); + assert.strictEqual(output.results[0].type, 'verification'); + assert.strictEqual(output.results[0].status, 'human_needed'); + assert.strictEqual(output.results[0].items[0].category, 'human_uat'); + assert.strictEqual(output.results[0].items[0].name, 'Test SSO login with Google account'); + }); + + test('scans and aggregates across multiple phases', () => { + // Phase 1 with pending + const phase1 = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(phase1, { recursive: true }); + fs.writeFileSync(path.join(phase1, '01-UAT.md'), `--- +status: partial +phase: 01-foundation +started: 2025-01-01T00:00:00Z +updated: 2025-01-01T00:00:00Z +--- + +## Tests + +### 1. Test A +expected: Works +result: pending +`); + + // Phase 2 with blocked + const phase2 = path.join(tmpDir, '.planning', 'phases', '02-api'); + fs.mkdirSync(phase2, { recursive: true }); + fs.writeFileSync(path.join(phase2, '02-UAT.md'), `--- +status: partial +phase: 02-api +started: 2025-01-01T00:00:00Z +updated: 2025-01-01T00:00:00Z +--- + +## Tests + +### 1. Test B +expected: Responds +result: blocked +blocked_by: server + +### 2. Test C +expected: Returns data +result: skipped +reason: device not available +`); + + const result = runGsdTools('audit-uat --raw', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.summary.total_files, 2); + assert.strictEqual(output.summary.total_items, 3); + assert.strictEqual(output.summary.by_phase['01'], 1); + assert.strictEqual(output.summary.by_phase['02'], 2); + }); + + test('milestone scoping filters phases to current milestone', () => { + // Create a ROADMAP.md that only references Phase 2 + fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), `# Roadmap + +### Phase 2: API Layer +**Goal:** Build API +`); + + // Phase 1 (not in current milestone) with pending + const phase1 = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(phase1, { recursive: true }); + fs.writeFileSync(path.join(phase1, '01-UAT.md'), `--- +status: partial +phase: 01-foundation +started: 2025-01-01T00:00:00Z +updated: 2025-01-01T00:00:00Z +--- + +## Tests + +### 1. Old Test +expected: Old behavior +result: pending +`); + + // Phase 2 (in current milestone) with pending + const phase2 = path.join(tmpDir, '.planning', 'phases', '02-api'); + fs.mkdirSync(phase2, { recursive: true }); + fs.writeFileSync(path.join(phase2, '02-UAT.md'), `--- +status: partial +phase: 02-api +started: 2025-01-01T00:00:00Z +updated: 2025-01-01T00:00:00Z +--- + +## Tests + +### 1. New Test +expected: New behavior +result: pending +`); + + const result = runGsdTools('audit-uat --raw', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + // Only Phase 2 should be included (Phase 1 not in ROADMAP) + assert.strictEqual(output.summary.total_files, 1); + assert.strictEqual(output.results[0].phase, '02'); + }); + + test('summary by_category counts are correct', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '05-billing'); + fs.mkdirSync(phaseDir, { recursive: true }); + + fs.writeFileSync(path.join(phaseDir, '05-UAT.md'), `--- +status: partial +phase: 05-billing +started: 2025-01-01T00:00:00Z +updated: 2025-01-01T00:00:00Z +--- + +## Tests + +### 1. Payment Form +expected: Stripe elements load +result: pending + +### 2. Webhook Handler +expected: Processes payment events +result: blocked +blocked_by: third-party Stripe + +### 3. Invoice PDF +expected: Generates downloadable PDF +result: skipped +reason: needs release build + +### 4. Refund Flow +expected: Processes refund +result: pending +`); + + const result = runGsdTools('audit-uat --raw', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.summary.total_items, 4); + assert.strictEqual(output.summary.by_category.pending, 2); + assert.strictEqual(output.summary.by_category.third_party, 1); + assert.strictEqual(output.summary.by_category.build_needed, 1); + }); + + test('ignores VERIFICATION files without human_needed or gaps_found status', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(phaseDir, { recursive: true }); + + fs.writeFileSync(path.join(phaseDir, '01-VERIFICATION.md'), `--- +status: passed +phase: 01-foundation +--- + +## Results + +All checks passed. +`); + + const result = runGsdTools('audit-uat --raw', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.summary.total_items, 0); + assert.strictEqual(output.summary.total_files, 0); + }); +}); From 459f7f3b645b82a287a91bc4dfc8ef30a8ce517a Mon Sep 17 00:00:00 2001 From: j2h4u <39818683+j2h4u@users.noreply.github.com> Date: Wed, 4 Mar 2026 03:12:53 +0500 Subject: [PATCH 67/83] fix(roadmap): mark individual plan checkboxes when summaries exist `cmdRoadmapUpdatePlanProgress` only marked phase-level checkboxes (e.g. `- [ ] Phase 50: Build`) but skipped plan-level entries (e.g. `- [ ] 50-01-PLAN.md`). Now iterates phase summaries and marks matching plan checkboxes as complete. Co-Authored-By: Claude Opus 4.6 --- get-shit-done/bin/lib/roadmap.cjs | 12 +++++++++++ tests/roadmap.test.cjs | 36 +++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/get-shit-done/bin/lib/roadmap.cjs b/get-shit-done/bin/lib/roadmap.cjs index a2fe661a6..6dd3554c6 100644 --- a/get-shit-done/bin/lib/roadmap.cjs +++ b/get-shit-done/bin/lib/roadmap.cjs @@ -287,6 +287,18 @@ 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:") + 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})`, + 'i' + ); + roadmapContent = roadmapContent.replace(planCheckboxPattern, '$1x$2'); + } + fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8'); output({ diff --git a/tests/roadmap.test.cjs b/tests/roadmap.test.cjs index ea5e2f538..7f41055a3 100644 --- a/tests/roadmap.test.cjs +++ b/tests/roadmap.test.cjs @@ -756,6 +756,42 @@ describe('roadmap update-plan-progress command', () => { assert.strictEqual(output.updated, false, 'should not update'); assert.ok(output.reason.includes('ROADMAP.md not found'), 'reason should mention missing ROADMAP.md'); }); + + test('marks completed plan checkboxes', () => { + const roadmapContent = `# Roadmap + +- [ ] Phase 50: Build + - [ ] 50-01-PLAN.md + - [ ] 50-02-PLAN.md + +### Phase 50: Build +**Goal:** Build stuff +**Plans:** 2 plans + +## Progress + +| Phase | Plans Complete | Status | Completed | +|-------|---------------|--------|-----------| +| 50. Build | 0/2 | Planned | | +`; + fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), roadmapContent); + + const p50 = path.join(tmpDir, '.planning', 'phases', '50-build'); + fs.mkdirSync(p50, { recursive: true }); + fs.writeFileSync(path.join(p50, '50-01-PLAN.md'), '# Plan 1'); + fs.writeFileSync(path.join(p50, '50-02-PLAN.md'), '# Plan 2'); + // Only plan 1 has a summary (completed) + fs.writeFileSync(path.join(p50, '50-01-SUMMARY.md'), '# Summary 1'); + + const result = runGsdTools('roadmap update-plan-progress 50', 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] 50-01-PLAN.md') || roadmap.includes('[x] 50-01'), + 'completed plan checkbox should be marked'); + assert.ok(roadmap.includes('[ ] 50-02-PLAN.md') || roadmap.includes('[ ] 50-02'), + 'incomplete plan checkbox should remain unchecked'); + }); }); // ───────────────────────────────────────────────────────────────────────────── From b34bf532efd869be7dabe51258ab58422c7da07e Mon Sep 17 00:00:00 2001 From: j2h4u <39818683+j2h4u@users.noreply.github.com> Date: Wed, 4 Mar 2026 03:04:50 +0500 Subject: [PATCH 68/83] fix(milestone): read task count from **Tasks:** field in summary body MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The summary template writes task count as `**Tasks:** N` in the Performance section, but `cmdMilestoneComplete` only counted `## Task N` markdown headers — which don't exist in that format. Now checks three patterns in order: `**Tasks:** N` field (primary), ` --- get-shit-done/bin/lib/milestone.cjs | 13 ++++++++++--- tests/milestone.test.cjs | 24 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/get-shit-done/bin/lib/milestone.cjs b/get-shit-done/bin/lib/milestone.cjs index 63c5e15fb..af473e59e 100644 --- a/get-shit-done/bin/lib/milestone.cjs +++ b/get-shit-done/bin/lib/milestone.cjs @@ -134,9 +134,16 @@ function cmdMilestoneComplete(cwd, version, options, raw) { if (fm['one-liner']) { accomplishments.push(fm['one-liner']); } - // Count tasks - const taskMatches = content.match(/##\s*Task\s*\d+/gi) || []; - totalTasks += taskMatches.length; + // Count tasks: prefer **Tasks:** N from Performance section, + // then ]/gi) || []; + const mdTaskMatches = content.match(/##\s*Task\s*\d+/gi) || []; + totalTasks += xmlTaskMatches.length || mdTaskMatches.length; + } } catch { /* intentionally empty */ } } } diff --git a/tests/milestone.test.cjs b/tests/milestone.test.cjs index fc00a37cd..7dcaee60d 100644 --- a/tests/milestone.test.cjs +++ b/tests/milestone.test.cjs @@ -424,6 +424,30 @@ describe('milestone complete command', () => { assert.strictEqual(output.phases, 2, 'should count only phases 456 and 457'); }); + test('counts tasks from **Tasks:** N in summary body', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap v1.0\n\n### Phase 1: Foundation\n**Goal:** Setup\n` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# State\n\n**Status:** In progress\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-SUMMARY.md'), + `---\none-liner: Built the foundation\n---\n\n# Phase 1: Foundation Summary\n\n**Built the foundation**\n\n## Performance\n\n- **Duration:** 28 min\n- **Tasks:** 7\n- **Files modified:** 12\n` + ); + + const result = runGsdTools('milestone complete v1.0 --name MVP', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.tasks, 7, 'should count tasks from **Tasks:** N field'); + }); + test('handles empty phases directory', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'), From 8a6cdf5f25f156e065e46c3c9f65747f1da9c242 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Thu, 19 Mar 2026 09:15:06 -0400 Subject: [PATCH 69/83] fix(execute-phase): add Copilot sequential fallback and spot-check completion detection (#1128) - Default Copilot runtime to sequential inline execution instead of unreliable subagent spawning - Add spot-check fallback in step 3 (SUMMARY.md exists + git commits found) so orchestrator never blocks indefinitely on missing completion signals - Add runtime detection guidance in init step to force sequential mode under Copilot - Add regression test verifying execute-phase contains Copilot fallback and spot-check documentation --- get-shit-done/workflows/execute-phase.md | 41 +++++++++++++++++++++--- tests/agent-frontmatter.test.cjs | 14 ++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index 552afb318..fdbc20e7d 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -9,15 +9,18 @@ Orchestrator coordinates, not executes. Each subagent loads the full execute-pla **Subagent spawning is runtime-specific:** - **Claude Code:** Uses `Task(subagent_type="gsd-executor", ...)` — blocks until complete, returns result -- **Copilot:** Uses `@gsd-executor` agent reference — if subagent spawning hangs or fails to return, - fall back to **sequential inline execution**: read and follow execute-plan.md directly for each plan - instead of spawning parallel agents. This is slower but reliable. +- **Copilot:** Subagent spawning does not reliably return completion signals. **Default to + sequential inline execution**: read and follow execute-plan.md directly for each plan + instead of spawning parallel agents. Only attempt parallel spawning if the user + explicitly requests it — and in that case, rely on the spot-check fallback in step 3 + to detect completion. - **Other runtimes (Gemini, Codex, OpenCode):** If Task/subagent API is unavailable, use sequential inline execution as the fallback. **Fallback rule:** If a spawned agent completes its work (commits visible, SUMMARY.md exists) but the orchestrator never receives the completion signal, treat it as successful based on spot-checks -and continue to the next wave/plan. +and continue to the next wave/plan. Never block indefinitely waiting for a signal — always verify +via filesystem and git state. @@ -60,6 +63,14 @@ Parse JSON for: `executor_model`, `verifier_model`, `commit_docs`, `parallelizat When `parallelization` is false, plans within a wave execute sequentially. +**Runtime detection for Copilot:** +Check if the current runtime is Copilot by testing for the `@gsd-executor` agent pattern +or absence of the `Task()` subagent API. If running under Copilot, force sequential inline +execution regardless of the `parallelization` setting — Copilot's subagent completion +signals are unreliable (see ``). Set `COPILOT_SEQUENTIAL=true` +internally and skip the `execute_waves` step in favor of `check_interactive_mode`'s +inline path for each plan. + **REQUIRED — Sync chain flag with intent.** If user invoked manually (no `--auto`), clear the ephemeral chain flag from any previous interrupted `--auto` chain. This prevents stale `_auto_chain_active: true` from causing unwanted auto-advance. This does NOT touch `workflow.auto_advance` (the user's persistent settings preference). You MUST execute this bash block before any config reads: ```bash # REQUIRED: prevents stale auto-chain from previous --auto runs @@ -250,6 +261,28 @@ Execute each wave in sequence. Within a wave: parallel if `PARALLELIZATION=true` 3. **Wait for all agents in wave to complete.** + **Completion signal fallback (Copilot and runtimes where Task() may not return):** + + If a spawned agent does not return a completion signal but appears to have finished + its work, do NOT block indefinitely. Instead, verify completion via spot-checks: + + ```bash + # For each plan in this wave, check if the executor finished: + SUMMARY_EXISTS=$(test -f "{phase_dir}/{plan_number}-{plan_padded}-SUMMARY.md" && echo "true" || echo "false") + COMMITS_FOUND=$(git log --oneline --all --grep="{phase_number}-{plan_padded}" --since="1 hour ago" | head -1) + ``` + + **If SUMMARY.md exists AND commits are found:** The agent completed successfully — + treat as done and proceed to step 4. Log: `"✓ {Plan ID} completed (verified via spot-check — completion signal not received)"` + + **If SUMMARY.md does NOT exist after a reasonable wait:** The agent may still be + running or may have failed silently. Check `git log --oneline -5` for recent + activity. If commits are still appearing, wait longer. If no activity, report + the plan as failed and route to the failure handler in step 5. + + **This fallback applies automatically to all runtimes.** Claude Code's Task() normally + returns synchronously, but the fallback ensures resilience if it doesn't. + 4. **Post-wave hook validation (parallel mode only):** When agents committed with `--no-verify`, run pre-commit hooks once after the wave: diff --git a/tests/agent-frontmatter.test.cjs b/tests/agent-frontmatter.test.cjs index a5aea5264..5308f4fad 100644 --- a/tests/agent-frontmatter.test.cjs +++ b/tests/agent-frontmatter.test.cjs @@ -151,6 +151,20 @@ describe('SPAWN: spawn type consistency', () => { 'diagnose-issues should spawn gsd-debugger, not general-purpose' ); }); + + test('execute-phase has Copilot sequential fallback in runtime_compatibility', () => { + const content = fs.readFileSync( + path.join(WORKFLOWS_DIR, 'execute-phase.md'), 'utf-8' + ); + assert.ok( + content.includes('sequential inline execution'), + 'execute-phase must document sequential inline execution as Copilot fallback' + ); + assert.ok( + content.includes('spot-check'), + 'execute-phase must have spot-check fallback for completion detection' + ); + }); }); // ─── Required Frontmatter Fields ───────────────────────────────────────────── From 0487151142c62d7d148d8f1cf015b892dc491deb Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Thu, 19 Mar 2026 09:19:01 -0400 Subject: [PATCH 70/83] fix(workflows): add text_mode config for Claude Code remote session compatibility (#1214) - Add workflow.text_mode config option (default: false) that replaces AskUserQuestion TUI menus with plain-text numbered lists - Document --text flag and config-set workflow.text_mode true as the fix for /rc remote sessions where the Claude App cannot forward TUI menu selections - Update discuss-phase.md with text mode parsing and answer_validation fallback documentation - Add text_mode to loadConfig defaults and VALID_CONFIG_KEYS - Add regression tests for config-set and loadConfig --- get-shit-done/bin/lib/config.cjs | 1 + get-shit-done/bin/lib/core.cjs | 2 ++ get-shit-done/workflows/discuss-phase.md | 19 +++++++++++++++++++ get-shit-done/workflows/settings.md | 3 +++ tests/config.test.cjs | 10 ++++++++++ tests/core.test.cjs | 1 + 6 files changed, 36 insertions(+) diff --git a/get-shit-done/bin/lib/config.cjs b/get-shit-done/bin/lib/config.cjs index ef1466f61..e40e0d84d 100644 --- a/get-shit-done/bin/lib/config.cjs +++ b/get-shit-done/bin/lib/config.cjs @@ -16,6 +16,7 @@ const VALID_CONFIG_KEYS = new Set([ 'search_gitignored', 'brave_search', 'workflow.research', 'workflow.plan_check', 'workflow.verifier', 'workflow.nyquist_validation', 'workflow.ui_phase', 'workflow.ui_safety_gate', + 'workflow.text_mode', 'workflow._auto_chain_active', 'git.branching_strategy', 'git.phase_branch_template', 'git.milestone_branch_template', 'planning.commit_docs', 'planning.search_gitignored', diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index 66918b018..453fe6e8a 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -64,6 +64,7 @@ function loadConfig(cwd) { nyquist_validation: true, parallelization: true, brave_search: false, + text_mode: false, // when true, use plain-text numbered lists instead of AskUserQuestion menus resolve_model_ids: false, // when true, resolve aliases (opus/sonnet/haiku) to full model IDs context_window: 200000, // default 200k; set to 1000000 for Opus/Sonnet 4.6 1M models }; @@ -108,6 +109,7 @@ function loadConfig(cwd) { nyquist_validation: get('nyquist_validation', { section: 'workflow', field: 'nyquist_validation' }) ?? defaults.nyquist_validation, parallelization, brave_search: get('brave_search') ?? defaults.brave_search, + text_mode: get('text_mode', { section: 'workflow', field: 'text_mode' }) ?? defaults.text_mode, resolve_model_ids: get('resolve_model_ids') ?? defaults.resolve_model_ids, context_window: get('context_window') ?? defaults.context_window, model_overrides: parsed.model_overrides || null, diff --git a/get-shit-done/workflows/discuss-phase.md b/get-shit-done/workflows/discuss-phase.md index 5182afce8..b0da511c3 100644 --- a/get-shit-done/workflows/discuss-phase.md +++ b/get-shit-done/workflows/discuss-phase.md @@ -110,6 +110,18 @@ Phase: "API documentation" 1. Retry the question once with the same parameters 2. If still empty, present the options as a plain-text numbered list and ask the user to type their choice number Never proceed with an empty answer. + +**Text mode (`workflow.text_mode: true` in config or `--text` flag):** +When text mode is active, **do not use AskUserQuestion at all**. Instead, present every +question as 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 the Claude App +cannot forward TUI menu selections back to the host. + +Enable text mode: +- Per-session: pass `--text` flag to any command (e.g., `/gsd:discuss-phase --text`) +- Per-project: `gsd-tools config-set workflow.text_mode true` + +Text mode applies to ALL workflows in the session, not just discuss-phase. @@ -464,6 +476,13 @@ With that context: How should users authenticate? When disabled (default), skip the research and present questions directly as before. +**Text mode support:** Parse optional `--text` from `$ARGUMENTS`. +- Accept `--text` flag OR read `workflow.text_mode` from config (from init context) +- When active, replace ALL `AskUserQuestion` calls with plain-text numbered lists +- User types a number to select, or types free text for "Other" +- This is required for Claude Code remote sessions (`/rc` mode) where TUI menus + don't work through the Claude App + **Batch mode support:** Parse optional `--batch` from `$ARGUMENTS`. - Accept `--batch`, `--batch=N`, or `--batch N` diff --git a/get-shit-done/workflows/settings.md b/get-shit-done/workflows/settings.md index 1c7ada565..13cd8399a 100644 --- a/get-shit-done/workflows/settings.md +++ b/get-shit-done/workflows/settings.md @@ -172,6 +172,9 @@ Merge new settings into existing config.json: "context_warnings": true/false, "workflow_guard": true/false, "research_questions": true/false + }, + "workflow": { + "text_mode": true/false // Use plain-text questions instead of TUI menus (for /rc remote sessions) } } ``` diff --git a/tests/config.test.cjs b/tests/config.test.cjs index 789753153..33fb9d44a 100644 --- a/tests/config.test.cjs +++ b/tests/config.test.cjs @@ -287,6 +287,16 @@ describe('config-set command', () => { ); }); + test('sets workflow.text_mode for remote session support', () => { + writeConfig(tmpDir, {}); + + const result = runGsdTools('config-set workflow.text_mode true', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const config = readConfig(tmpDir); + assert.strictEqual(config.workflow.text_mode, true); + }); + test('errors when no key path provided', () => { const result = runGsdTools('config-set', tmpDir); assert.strictEqual(result.success, false); diff --git a/tests/core.test.cjs b/tests/core.test.cjs index 56d658a53..3a3861ea1 100644 --- a/tests/core.test.cjs +++ b/tests/core.test.cjs @@ -61,6 +61,7 @@ describe('loadConfig', () => { assert.strictEqual(config.brave_search, false); assert.strictEqual(config.parallelization, true); assert.strictEqual(config.nyquist_validation, true); + assert.strictEqual(config.text_mode, false); }); test('reads model_profile from config.json', () => { From 342ca5929dd609d95a2aaf9057a3b8e3b73adfa1 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Thu, 19 Mar 2026 12:25:47 -0400 Subject: [PATCH 71/83] fix(tests): update expected Copilot skill count from 47 to 50 Three new commands were added (add-backlog, review-backlog, thread) but the Copilot install test counts were not updated. --- tests/copilot-install.test.cjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/copilot-install.test.cjs b/tests/copilot-install.test.cjs index f82ecdac7..ee162d4c4 100644 --- a/tests/copilot-install.test.cjs +++ b/tests/copilot-install.test.cjs @@ -625,7 +625,7 @@ describe('copyCommandsAsCopilotSkills', () => { // Count gsd-* directories — should be 31 const dirs = fs.readdirSync(tempDir, { withFileTypes: true }) .filter(e => e.isDirectory() && e.name.startsWith('gsd-')); - assert.strictEqual(dirs.length, 47, `expected 47 skill folders, got ${dirs.length}`); + assert.strictEqual(dirs.length, 50, `expected 50 skill folders, got ${dirs.length}`); } finally { fs.rmSync(tempDir, { recursive: true }); } @@ -1119,7 +1119,7 @@ const { execFileSync } = require('child_process'); const crypto = require('crypto'); const INSTALL_PATH = path.join(__dirname, '..', 'bin', 'install.js'); -const EXPECTED_SKILLS = 47; +const EXPECTED_SKILLS = 50; const EXPECTED_AGENTS = 16; function runCopilotInstall(cwd) { From 61d82dc3c0d81d367ab1377b4a47fbe9845def99 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Thu, 19 Mar 2026 09:21:01 -0400 Subject: [PATCH 72/83] feat(discuss-phase): auto-generate DISCUSSION-LOG.md for audit trail (#1209) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Generate {phase_num}-DISCUSSION-LOG.md alongside CONTEXT.md during discuss-phase sessions - Log captures all options presented per gray area (not just the selected one), user's choice, notes, Claude's discretion items, and deferred ideas - File is explicitly marked as audit-only — not for agent consumption - Add discussion-log.md template with format specification - Track Q&A data accumulation instruction in discuss_areas step - Commit discussion log alongside CONTEXT.md in same git commit - Add regression tests for workflow reference and template existence --- get-shit-done/templates/discussion-log.md | 63 +++++++++++++++++++++++ get-shit-done/workflows/discuss-phase.md | 60 ++++++++++++++++++++- tests/agent-frontmatter.test.cjs | 31 +++++++++++ 3 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 get-shit-done/templates/discussion-log.md diff --git a/get-shit-done/templates/discussion-log.md b/get-shit-done/templates/discussion-log.md new file mode 100644 index 000000000..37cd86692 --- /dev/null +++ b/get-shit-done/templates/discussion-log.md @@ -0,0 +1,63 @@ +# Discussion Log Template + +Template for `.planning/phases/XX-name/{phase_num}-DISCUSSION-LOG.md` — audit trail of discuss-phase Q&A sessions. + +**Purpose:** Software audit trail for decision-making. Captures all options considered, not just the selected one. Separate from CONTEXT.md which is the implementation artifact consumed by downstream agents. + +**NOT for LLM consumption.** This file should never be referenced in `` blocks or agent prompts. + +## Format + +```markdown +# Phase [X]: [Name] - Discussion Log + +> **Audit trail only.** Do not use as input to planning, research, or execution agents. +> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered. + +**Date:** [ISO date] +**Phase:** [phase number]-[phase name] +**Areas discussed:** [comma-separated list] + +--- + +## [Area 1 Name] + +| Option | Description | Selected | +|--------|-------------|----------| +| [Option 1] | [Brief description] | | +| [Option 2] | [Brief description] | ✓ | +| [Option 3] | [Brief description] | | + +**User's choice:** [Selected option or verbatim free-text response] +**Notes:** [Any clarifications or rationale provided during discussion] + +--- + +## [Area 2 Name] + +... + +--- + +## Claude's Discretion + +[Areas delegated to Claude's judgment — list what was deferred and why] + +## Deferred Ideas + +[Ideas mentioned but not in scope for this phase] + +--- + +*Phase: XX-name* +*Discussion log generated: [date]* +``` + +## Rules + +- Generated automatically at end of every discuss-phase session +- Includes ALL options considered, not just the selected one +- Includes user's freeform notes and clarifications +- Clearly marked as audit-only, not an implementation artifact +- Does NOT interfere with CONTEXT.md generation or downstream agent behavior +- Committed alongside CONTEXT.md in the same git commit diff --git a/get-shit-done/workflows/discuss-phase.md b/get-shit-done/workflows/discuss-phase.md index b0da511c3..be37d214f 100644 --- a/get-shit-done/workflows/discuss-phase.md +++ b/get-shit-done/workflows/discuss-phase.md @@ -603,11 +603,23 @@ Back to [current area]: [return to current question]" ``` Track deferred ideas internally. + +**Track discussion log data internally:** +For each question asked, accumulate: +- Area name +- All options presented (label + description) +- Which option the user selected (or their free-text response) +- Any follow-up notes or clarifications the user provided +This data is used to generate DISCUSSION-LOG.md in the `write_context` step. Create CONTEXT.md capturing decisions made. +**Also generate DISCUSSION-LOG.md** — a full audit trail of the discuss-phase Q&A. +This file is for human reference only (software audits, compliance reviews). It is NOT +consumed by downstream agents (researcher, planner, executor). + **Find or create phase directory:** Use values from init: `phase_dir`, `phase_slug`, `padded_phase`. @@ -762,10 +774,54 @@ Created: .planning/phases/${PADDED_PHASE}-${SLUG}/${PADDED_PHASE}-CONTEXT.md -Commit phase context (uses `commit_docs` from init internally): +**Write DISCUSSION-LOG.md before committing:** + +**File location:** `${phase_dir}/${padded_phase}-DISCUSSION-LOG.md` + +```markdown +# Phase [X]: [Name] - Discussion Log + +> **Audit trail only.** Do not use as input to planning, research, or execution agents. +> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered. + +**Date:** [ISO date] +**Phase:** [phase number]-[phase name] +**Areas discussed:** [comma-separated list] + +--- + +[For each gray area discussed:] + +## [Area Name] + +| Option | Description | Selected | +|--------|-------------|----------| +| [Option 1] | [Description from AskUserQuestion] | | +| [Option 2] | [Description] | ✓ | +| [Option 3] | [Description] | | + +**User's choice:** [Selected option or free-text response] +**Notes:** [Any clarifications, follow-up context, or rationale the user provided] + +--- + +[Repeat for each area] + +## Claude's Discretion + +[List areas where user said "you decide" or deferred to Claude] + +## Deferred Ideas + +[Ideas mentioned during discussion that were noted for future phases] +``` + +Write file. + +Commit phase context and discussion log: ```bash -node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(${padded_phase}): capture phase context" --files "${phase_dir}/${padded_phase}-CONTEXT.md" +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(${padded_phase}): capture phase context" --files "${phase_dir}/${padded_phase}-CONTEXT.md" "${phase_dir}/${padded_phase}-DISCUSSION-LOG.md" ``` Confirm: "Committed: docs(${padded_phase}): capture phase context" diff --git a/tests/agent-frontmatter.test.cjs b/tests/agent-frontmatter.test.cjs index 5308f4fad..a5b4f6412 100644 --- a/tests/agent-frontmatter.test.cjs +++ b/tests/agent-frontmatter.test.cjs @@ -181,3 +181,34 @@ describe('AGENT: required frontmatter fields', () => { }); } }); + +// ─── Discussion Log ────────────────────────────────────────────────────────── + +describe('DISCUSS: discussion log generation', () => { + test('discuss-phase workflow references DISCUSSION-LOG.md generation', () => { + const content = fs.readFileSync( + path.join(WORKFLOWS_DIR, 'discuss-phase.md'), 'utf-8' + ); + assert.ok( + content.includes('DISCUSSION-LOG.md'), + 'discuss-phase must reference DISCUSSION-LOG.md generation' + ); + assert.ok( + content.includes('Audit trail only'), + 'discuss-phase must mark discussion log as audit-only' + ); + }); + + test('discussion-log template exists', () => { + const templatePath = path.join(__dirname, '..', 'get-shit-done', 'templates', 'discussion-log.md'); + assert.ok( + fs.existsSync(templatePath), + 'discussion-log.md template must exist' + ); + const content = fs.readFileSync(templatePath, 'utf-8'); + assert.ok( + content.includes('Do not use as input to planning'), + 'template must contain audit-only notice' + ); + }); +}); From 85a65fd384885baf96f8ba498762f327d169d9a7 Mon Sep 17 00:00:00 2001 From: j2h4u <39818683+j2h4u@users.noreply.github.com> Date: Thu, 5 Mar 2026 02:58:49 +0500 Subject: [PATCH 73/83] fix(state): parse compound Plan field in advance-plan command `cmdStateAdvancePlan` expected separate `Current Plan` and `Total Plans in Phase` fields, but the current STATE.md template uses a single compound field: `Plan: X of Y in current phase`. Now tries legacy separate fields first, then falls back to parsing the compound format. Preserves the compound format when writing back (replaces only the plan number). Also handles `Last activity` (lowercase) field name from current template. Co-Authored-By: Claude Opus 4.6 --- get-shit-done/bin/lib/state.cjs | 44 +++++++++++++++++++++++++++------ tests/state.test.cjs | 39 +++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 7 deletions(-) diff --git a/get-shit-done/bin/lib/state.cjs b/get-shit-done/bin/lib/state.cjs index 0d7387855..3a91006b9 100644 --- a/get-shit-done/bin/lib/state.cjs +++ b/get-shit-done/bin/lib/state.cjs @@ -206,25 +206,55 @@ function cmdStateAdvancePlan(cwd, raw) { if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; } let content = fs.readFileSync(statePath, 'utf-8'); - const currentPlan = parseInt(stateExtractField(content, 'Current Plan'), 10); - const totalPlans = parseInt(stateExtractField(content, 'Total Plans in Phase'), 10); const today = new Date().toISOString().split('T')[0]; + // Try legacy separate fields first, then compound "Plan: X of Y" format + const legacyPlan = stateExtractField(content, 'Current Plan'); + const legacyTotal = stateExtractField(content, 'Total Plans in Phase'); + const planField = stateExtractField(content, 'Plan'); + + let currentPlan, totalPlans; + let useCompoundFormat = false; + + if (legacyPlan && legacyTotal) { + currentPlan = parseInt(legacyPlan, 10); + totalPlans = parseInt(legacyTotal, 10); + } else if (planField) { + // Compound format: "2 of 6 in current phase" or "2 of 6" + currentPlan = parseInt(planField, 10); + const ofMatch = planField.match(/of\s+(\d+)/); + totalPlans = ofMatch ? parseInt(ofMatch[1], 10) : NaN; + useCompoundFormat = true; + } + if (isNaN(currentPlan) || isNaN(totalPlans)) { output({ error: 'Cannot parse Current Plan or Total Plans in Phase from STATE.md' }, raw); return; } + const replaceField = (c, primary, fallback, value) => { + let r = stateReplaceField(c, primary, value); + if (r) return r; + if (fallback) { r = stateReplaceField(c, fallback, value); if (r) return r; } + return c; + }; + if (currentPlan >= totalPlans) { - content = stateReplaceField(content, 'Status', 'Phase complete — ready for verification') || content; - content = stateReplaceField(content, 'Last Activity', today) || content; + content = replaceField(content, 'Status', null, 'Phase complete — ready for verification'); + content = replaceField(content, 'Last Activity', 'Last activity', today); writeStateMd(statePath, content, cwd); output({ advanced: false, reason: 'last_plan', current_plan: currentPlan, total_plans: totalPlans, status: 'ready_for_verification' }, raw, 'false'); } else { const newPlan = currentPlan + 1; - content = stateReplaceField(content, 'Current Plan', String(newPlan)) || content; - content = stateReplaceField(content, 'Status', 'Ready to execute') || content; - content = stateReplaceField(content, 'Last Activity', today) || content; + if (useCompoundFormat) { + // Preserve compound format: "X of Y in current phase" → replace X only + const newPlanValue = planField.replace(/^\d+/, String(newPlan)); + content = stateReplaceField(content, 'Plan', newPlanValue) || content; + } else { + content = stateReplaceField(content, 'Current Plan', String(newPlan)) || content; + } + content = replaceField(content, 'Status', null, 'Ready to execute'); + content = replaceField(content, 'Last Activity', 'Last activity', today); writeStateMd(statePath, content, cwd); output({ advanced: true, previous_plan: currentPlan, current_plan: newPlan, total_plans: totalPlans }, raw, 'true'); } diff --git a/tests/state.test.cjs b/tests/state.test.cjs index 14ea38982..fe3b84670 100644 --- a/tests/state.test.cjs +++ b/tests/state.test.cjs @@ -892,6 +892,45 @@ describe('cmdStateAdvancePlan (state advance-plan)', () => { assert.ok(output.error !== undefined, 'output should have error field'); assert.ok(output.error.toLowerCase().includes('cannot parse'), 'error should mention Cannot parse'); }); + + test('advances plan in compound "Plan: X of Y" format', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# Project State\n\nPlan: 2 of 5 in current phase\nStatus: In progress\nLast activity: 2025-01-01\n` + ); + + const result = runGsdTools('state advance-plan', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.advanced, true, 'advanced should be true'); + assert.strictEqual(output.previous_plan, 2); + assert.strictEqual(output.current_plan, 3); + assert.strictEqual(output.total_plans, 5); + + const updated = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); + assert.ok(updated.includes('Plan: 3 of 5 in current phase'), + 'should preserve compound format with updated plan number'); + assert.ok(updated.includes('Status: Ready to execute'), + 'Status should be updated'); + }); + + test('marks phase complete on last plan in compound format', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# Project State\n\nPlan: 3 of 3 in current phase\nStatus: In progress\nLast activity: 2025-01-01\n` + ); + + const result = runGsdTools('state advance-plan', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.advanced, false); + assert.strictEqual(output.reason, 'last_plan'); + + const updated = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); + assert.ok(updated.includes('Phase complete'), 'Status should contain Phase complete'); + }); }); describe('cmdStateRecordMetric (state record-metric)', () => { From 104a39e573b35469b4b13f9290f01114c4e65df9 Mon Sep 17 00:00:00 2001 From: j2h4u <39818683+j2h4u@users.noreply.github.com> Date: Wed, 4 Mar 2026 03:10:49 +0500 Subject: [PATCH 74/83] fix(lifecycle): extract one-liner from summary body when not in frontmatter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The summary template puts the one-liner as a `**bold**` line after the `# Phase N` heading, but `cmdSummaryExtract` and `cmdMilestoneComplete` only checked frontmatter `one-liner` field — which is often empty. Adds `extractOneLinerFromBody()` to core.cjs as a fallback that parses the first `**...**` line after the heading. Co-Authored-By: Claude Opus 4.6 --- get-shit-done/bin/lib/commands.cjs | 4 ++-- get-shit-done/bin/lib/core.cjs | 18 +++++++++++++++++ get-shit-done/bin/lib/milestone.cjs | 7 ++++--- tests/commands.test.cjs | 31 +++++++++++++++++++++++++++++ tests/milestone.test.cjs | 28 ++++++++++++++++++++++++++ 5 files changed, 83 insertions(+), 5 deletions(-) diff --git a/get-shit-done/bin/lib/commands.cjs b/get-shit-done/bin/lib/commands.cjs index 7227ea5c8..10696d0b1 100644 --- a/get-shit-done/bin/lib/commands.cjs +++ b/get-shit-done/bin/lib/commands.cjs @@ -4,7 +4,7 @@ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); -const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, stripShippedMilestones, extractCurrentMilestone, planningPaths, toPosixPath, output, error, findPhaseInternal, getRoadmapPhaseInternal } = require('./core.cjs'); +const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, stripShippedMilestones, extractCurrentMilestone, planningPaths, toPosixPath, output, error, findPhaseInternal, extractOneLinerFromBody, getRoadmapPhaseInternal } = require('./core.cjs'); const { extractFrontmatter } = require('./frontmatter.cjs'); const { MODEL_PROFILES } = require('./model-profiles.cjs'); @@ -296,7 +296,7 @@ function cmdSummaryExtract(cwd, summaryPath, fields, raw) { // Build full result const fullResult = { path: summaryPath, - one_liner: fm['one-liner'] || null, + one_liner: fm['one-liner'] || extractOneLinerFromBody(content) || null, key_files: fm['key-files'] || [], tech_added: (fm['tech-stack'] && fm['tech-stack'].added) || [], patterns: fm['patterns-established'] || [], diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index f39ac5bd2..807f883d7 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -642,6 +642,23 @@ function resolveModelInternal(cwd, agentType) { return alias; } +// ─── Summary body helpers ───────────────────────────────────────────────── + +/** + * Extract a one-liner from the summary body when it's not in frontmatter. + * The summary template defines one-liner as a bold markdown line after the heading: + * # Phase X: Name Summary + * **[substantive one-liner text]** + */ +function extractOneLinerFromBody(content) { + if (!content) return null; + // Strip frontmatter first + const body = content.replace(/^---\n[\s\S]*?\n---\n*/, ''); + // Find the first **...** line after a # heading + const match = body.match(/^#[^\n]*\n+\*\*([^*]+)\*\*/m); + return match ? match[1].trim() : null; +} + // ─── Misc utilities ─────────────────────────────────────────────────────────── function pathExistsInternal(cwd, targetPath) { @@ -760,6 +777,7 @@ module.exports = { extractCurrentMilestone, replaceInCurrentMilestone, toPosixPath, + extractOneLinerFromBody, MODEL_ALIAS_MAP, planningDir, planningPaths, diff --git a/get-shit-done/bin/lib/milestone.cjs b/get-shit-done/bin/lib/milestone.cjs index af473e59e..b105b9f2d 100644 --- a/get-shit-done/bin/lib/milestone.cjs +++ b/get-shit-done/bin/lib/milestone.cjs @@ -4,7 +4,7 @@ const fs = require('fs'); const path = require('path'); -const { escapeRegex, getMilestonePhaseFilter, normalizeMd, planningPaths, output, error } = require('./core.cjs'); +const { escapeRegex, getMilestonePhaseFilter, extractOneLinerFromBody, normalizeMd, planningPaths, output, error } = require('./core.cjs'); const { extractFrontmatter } = require('./frontmatter.cjs'); const { writeStateMd } = require('./state.cjs'); @@ -131,8 +131,9 @@ function cmdMilestoneComplete(cwd, version, options, raw) { try { const content = fs.readFileSync(path.join(phasesDir, dir, s), 'utf-8'); const fm = extractFrontmatter(content); - if (fm['one-liner']) { - accomplishments.push(fm['one-liner']); + const oneLiner = fm['one-liner'] || extractOneLinerFromBody(content); + if (oneLiner) { + accomplishments.push(oneLiner); } // Count tasks: prefer **Tasks:** N from Performance section, // then { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(phaseDir, { recursive: true }); + + fs.writeFileSync( + path.join(phaseDir, '01-01-SUMMARY.md'), + `--- +phase: "01" +key-files: + - src/lib/db.ts +--- + +# Phase 1: Foundation Summary + +**JWT auth with refresh rotation using jose library** + +## Performance + +- **Duration:** 28 min +- **Tasks:** 5 +` + ); + + const result = runGsdTools('summary-extract .planning/phases/01-foundation/01-01-SUMMARY.md', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.one_liner, 'JWT auth with refresh rotation using jose library', + 'one-liner should be extracted from body **bold** line'); + }); + test('handles missing frontmatter fields gracefully', () => { const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation'); fs.mkdirSync(phaseDir, { recursive: true }); diff --git a/tests/milestone.test.cjs b/tests/milestone.test.cjs index 7dcaee60d..77b3c8376 100644 --- a/tests/milestone.test.cjs +++ b/tests/milestone.test.cjs @@ -448,6 +448,34 @@ describe('milestone complete command', () => { assert.strictEqual(output.tasks, 7, 'should count tasks from **Tasks:** N field'); }); + test('extracts one-liner from body when not in frontmatter', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap v1.0\n\n### Phase 1: Foundation\n**Goal:** Setup\n` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# State\n\n**Status:** In progress\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 }); + // No one-liner in frontmatter, but present in body as bold line + fs.writeFileSync( + path.join(p1, '01-01-SUMMARY.md'), + `---\nphase: "01"\n---\n\n# Phase 1: Foundation Summary\n\n**JWT auth with refresh rotation using jose library**\n\n## Performance\n` + ); + + const result = runGsdTools('milestone complete v1.0 --name MVP', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.ok( + output.accomplishments.includes('JWT auth with refresh rotation using jose library'), + 'should extract one-liner from body bold line' + ); + }); + test('handles empty phases directory', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'), From e3bc614eb7947ea36de99d8a5f13433e93e01829 Mon Sep 17 00:00:00 2001 From: j2h4u <39818683+j2h4u@users.noreply.github.com> Date: Wed, 4 Mar 2026 03:14:44 +0500 Subject: [PATCH 75/83] fix(roadmap): handle 5-column progress tables with Milestone column The regex-based table parser captured a fixed number of columns, so 5-column tables (Phase | Milestone | Plans | Status | Completed) had the Milestone column eaten and Status/Date written to wrong cells. Replaced regex with cell-based `split('|')` parsing that detects column count (4 or 5) and updates the correct cells by index. Affects both `cmdRoadmapUpdatePlanProgress` and `cmdPhaseComplete`. Co-Authored-By: Claude Opus 4.6 --- get-shit-done/bin/lib/phase.cjs | 25 ++++++++++++------- get-shit-done/bin/lib/roadmap.cjs | 27 ++++++++++++++------- tests/phase.test.cjs | 40 +++++++++++++++++++++++++++++++ tests/roadmap.test.cjs | 32 +++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 16 deletions(-) diff --git a/get-shit-done/bin/lib/phase.cjs b/get-shit-done/bin/lib/phase.cjs index d3b7d98b7..097175e15 100644 --- a/get-shit-done/bin/lib/phase.cjs +++ b/get-shit-done/bin/lib/phase.cjs @@ -767,16 +767,25 @@ function cmdPhaseComplete(cwd, phaseNum, raw) { ); roadmapContent = replaceInCurrentMilestone(roadmapContent, checkboxPattern, `$1x$2 (completed ${today})`); - // Progress table: update Status to Complete, add date + // Progress table: update Status to Complete, add date (handles 4 or 5 column tables) const phaseEscaped = escapeRegex(phaseNum); - const tablePattern = new RegExp( - `(\\|\\s*${phaseEscaped}\\.?\\s[^|]*\\|[^|]*\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)`, - 'i' - ); - roadmapContent = replaceInCurrentMilestone( - roadmapContent, tablePattern, - `$1 Complete $2 ${today} $3` + const tableRowPattern = new RegExp( + `^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`, + 'im' ); + roadmapContent = roadmapContent.replace(tableRowPattern, (fullRow) => { + const cells = fullRow.split('|').slice(1, -1); + if (cells.length === 5) { + // 5-col: Phase | Milestone | Plans | Status | Completed + cells[3] = ' Complete '; + cells[4] = ` ${today} `; + } else if (cells.length === 4) { + // 4-col: Phase | Plans | Status | Completed + cells[2] = ' Complete '; + cells[3] = ` ${today} `; + } + return '|' + cells.join('|') + '|'; + }); // Update plan count in phase section const planCountPattern = new RegExp( diff --git a/get-shit-done/bin/lib/roadmap.cjs b/get-shit-done/bin/lib/roadmap.cjs index fc4c7010c..693baf4f5 100644 --- a/get-shit-done/bin/lib/roadmap.cjs +++ b/get-shit-done/bin/lib/roadmap.cjs @@ -257,16 +257,27 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) { let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8'); const phaseEscaped = escapeRegex(phaseNum); - // Progress table row: update Plans column (summaries/plans) and Status column - const tablePattern = new RegExp( - `(\\|\\s*${phaseEscaped}\\.?\\s[^|]*\\|)[^|]*(\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)`, - 'i' + // Progress table row: update Plans/Status/Date columns (handles 4 or 5 column tables) + const tableRowPattern = new RegExp( + `^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`, + 'im' ); const dateField = isComplete ? ` ${today} ` : ' '; - roadmapContent = replaceInCurrentMilestone( - roadmapContent, tablePattern, - `$1 ${summaryCount}/${planCount} $2 ${status.padEnd(11)}$3${dateField}$4` - ); + roadmapContent = roadmapContent.replace(tableRowPattern, (fullRow) => { + const cells = fullRow.split('|').slice(1, -1); // drop leading/trailing empty from split + if (cells.length === 5) { + // 5-col: Phase | Milestone | Plans | Status | Completed + cells[2] = ` ${summaryCount}/${planCount} `; + cells[3] = ` ${status.padEnd(11)}`; + cells[4] = dateField; + } else if (cells.length === 4) { + // 4-col: Phase | Plans | Status | Completed + cells[1] = ` ${summaryCount}/${planCount} `; + cells[2] = ` ${status.padEnd(11)}`; + cells[3] = dateField; + } + return '|' + cells.join('|') + '|'; + }); // Update plan count in phase detail section const planCountPattern = new RegExp( diff --git a/tests/phase.test.cjs b/tests/phase.test.cjs index 76a21a68e..a4df0743c 100644 --- a/tests/phase.test.cjs +++ b/tests/phase.test.cjs @@ -1468,6 +1468,46 @@ describe('phase complete command', () => { const req = fs.readFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), 'utf-8'); assert.ok(req.includes('- [ ] **AMT-01**'), 'AMT-01 should remain unchanged'); }); + + test('preserves Milestone column in 5-column progress table', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap + +- [ ] Phase 1: Foundation + +### Phase 1: Foundation +**Goal:** Setup +**Plans:** 1 plans + +## Progress + +| Phase | Milestone | Plans Complete | Status | Completed | +|-------|-----------|----------------|--------|-----------| +| 1. Foundation | v1.0 | 0/1 | Planned | | +` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# State\n\n**Current Phase:** 01\n**Status:** In progress\n**Current Plan:** 01-01\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'); + + 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'); + const rowMatch = roadmap.match(/^\|[^\n]*1\. Foundation[^\n]*$/m); + assert.ok(rowMatch, 'table row should exist'); + const cells = rowMatch[0].split('|').slice(1, -1).map(c => c.trim()); + assert.strictEqual(cells.length, 5, 'should have 5 columns'); + assert.strictEqual(cells[1], 'v1.0', 'Milestone column should be preserved'); + assert.ok(cells[3].includes('Complete'), 'Status column should be Complete'); + }); }); // ───────────────────────────────────────────────────────────────────────────── diff --git a/tests/roadmap.test.cjs b/tests/roadmap.test.cjs index 7f41055a3..f043e40f9 100644 --- a/tests/roadmap.test.cjs +++ b/tests/roadmap.test.cjs @@ -792,6 +792,38 @@ describe('roadmap update-plan-progress command', () => { assert.ok(roadmap.includes('[ ] 50-02-PLAN.md') || roadmap.includes('[ ] 50-02'), 'incomplete plan checkbox should remain unchecked'); }); + + test('preserves Milestone column in 5-column progress table', () => { + const roadmapContent = `# Roadmap + +### Phase 50: Build +**Goal:** Build stuff +**Plans:** 1 plans + +## Progress + +| Phase | Milestone | Plans Complete | Status | Completed | +|-------|-----------|----------------|--------|-----------| +| 50. Build | v2.0 | 0/1 | Planned | | +`; + fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), roadmapContent); + + const p50 = path.join(tmpDir, '.planning', 'phases', '50-build'); + fs.mkdirSync(p50, { recursive: true }); + fs.writeFileSync(path.join(p50, '50-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(p50, '50-01-SUMMARY.md'), '# Summary'); + + const result = runGsdTools('roadmap update-plan-progress 50', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8'); + const rowMatch = roadmap.match(/^\|[^\n]*50\. Build[^\n]*$/m); + assert.ok(rowMatch, 'table row should exist'); + const cells = rowMatch[0].split('|').slice(1, -1).map(c => c.trim()); + assert.strictEqual(cells.length, 5, 'should have 5 columns'); + assert.strictEqual(cells[1], 'v2.0', 'Milestone column should be preserved'); + assert.ok(cells[3].includes('Complete'), 'Status column should show Complete'); + }); }); // ───────────────────────────────────────────────────────────────────────────── From 6dc8caa0d5232ef453f4ee1d5bb7d8e9ea1fd709 Mon Sep 17 00:00:00 2001 From: Tony Park Date: Fri, 20 Mar 2026 01:51:33 +0900 Subject: [PATCH 76/83] fix(commit): stage file deletions when passed non-existent paths When check-todos moves a file from pending/ to done/, the commit only staged the new destination. The deletion of the source was never staged because `git add` on a non-existent path is a no-op. Now we detect missing files and use `git rm --cached` to stage the deletion. Closes #1228 Co-Authored-By: Claude Opus 4.6 (1M context) --- get-shit-done/bin/lib/commands.cjs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/get-shit-done/bin/lib/commands.cjs b/get-shit-done/bin/lib/commands.cjs index 7227ea5c8..48620962b 100644 --- a/get-shit-done/bin/lib/commands.cjs +++ b/get-shit-done/bin/lib/commands.cjs @@ -238,7 +238,13 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify) { // Stage files const filesToStage = files && files.length > 0 ? files : ['.planning/']; for (const file of filesToStage) { - execGit(cwd, ['add', file]); + const fullPath = path.join(cwd, file); + if (!fs.existsSync(fullPath)) { + // File was deleted/moved — stage the deletion + execGit(cwd, ['rm', '--cached', '--ignore-unmatch', file]); + } else { + execGit(cwd, ['add', file]); + } } // Commit (--no-verify skips pre-commit hooks, used by parallel executor agents) From 0afffb15f5c9b32ec198c81d4369071954128f37 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Thu, 19 Mar 2026 09:23:39 -0400 Subject: [PATCH 77/83] feat(core): worktree-aware .planning/ resolution and file locking (#1215) - Add resolveWorktreeRoot() that detects linked worktrees via git rev-parse --git-common-dir and resolves to the main worktree where .planning/ lives - Add withPlanningLock() file-based locking mechanism to prevent concurrent worktrees from corrupting shared planning files - Wire worktree root resolution into gsd-tools.cjs main entry point - Add regression tests for resolveWorktreeRoot (non-git, normal repo) and withPlanningLock (normal execution, error cleanup, stale lock recovery) --- get-shit-done/bin/gsd-tools.cjs | 8 ++++ get-shit-done/bin/lib/core.cjs | 80 +++++++++++++++++++++++++++++++++ tests/core.test.cjs | 77 +++++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+) diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index b05a98995..7842fb6d8 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -173,6 +173,14 @@ async function main() { error(`Invalid --cwd: ${cwd}`); } + // Resolve worktree root: in a linked worktree, .planning/ lives in the main worktree + const { resolveWorktreeRoot } = require('./lib/core.cjs'); + const worktreeRoot = resolveWorktreeRoot(cwd); + if (worktreeRoot !== cwd) { + // Only override cwd for planning-related commands — keep original cwd for git operations + cwd = worktreeRoot; + } + const rawIndex = args.indexOf('--raw'); const raw = rawIndex !== -1; if (rawIndex !== -1) args.splice(rawIndex, 1); diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index 807f883d7..43b81bc20 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -260,6 +260,84 @@ function execGit(cwd, args) { // ─── Common path helpers ────────────────────────────────────────────────────── +/** + * Resolve the main worktree root when running inside a git worktree. + * In a linked worktree, .planning/ lives in the main worktree, not in the linked one. + * Returns the main worktree path, or cwd if not in a worktree. + */ +function resolveWorktreeRoot(cwd) { + // Check if we're in a linked worktree + const gitDir = execGit(cwd, ['rev-parse', '--git-dir']); + const commonDir = execGit(cwd, ['rev-parse', '--git-common-dir']); + + if (gitDir.exitCode !== 0 || commonDir.exitCode !== 0) return cwd; + + // In a linked worktree, .git is a file pointing to .git/worktrees/ + // and git-common-dir points to the main repo's .git directory + const gitDirResolved = path.resolve(cwd, gitDir.stdout); + const commonDirResolved = path.resolve(cwd, commonDir.stdout); + + if (gitDirResolved !== commonDirResolved) { + // We're in a linked worktree — resolve main worktree root + // The common dir is the main repo's .git, so its parent is the main worktree root + return path.dirname(commonDirResolved); + } + + return cwd; +} + +/** + * Acquire a file-based lock for .planning/ writes. + * Prevents concurrent worktrees from corrupting shared planning files. + * Lock is auto-released after the callback completes. + */ +function withPlanningLock(cwd, fn) { + const lockPath = path.join(planningDir(cwd), '.lock'); + const lockTimeout = 10000; // 10 seconds + const retryDelay = 100; + const start = Date.now(); + + // Ensure .planning/ exists + try { fs.mkdirSync(planningDir(cwd), { recursive: true }); } catch { /* ok */ } + + while (Date.now() - start < lockTimeout) { + try { + // Atomic create — fails if file exists + fs.writeFileSync(lockPath, JSON.stringify({ + pid: process.pid, + cwd, + acquired: new Date().toISOString(), + }), { flag: 'wx' }); + + // Lock acquired — run the function + try { + return fn(); + } finally { + try { fs.unlinkSync(lockPath); } catch { /* already released */ } + } + } catch (err) { + if (err.code === 'EEXIST') { + // Lock exists — check if stale (>30s old) + try { + const stat = fs.statSync(lockPath); + if (Date.now() - stat.mtimeMs > 30000) { + fs.unlinkSync(lockPath); + continue; // retry + } + } catch { continue; } + + // Wait and retry + spawnSync('sleep', ['0.1'], { stdio: 'ignore' }); + continue; + } + throw err; + } + } + // Timeout — force acquire (stale lock recovery) + try { fs.unlinkSync(lockPath); } catch { /* ok */ } + return fn(); +} + /** Get the .planning directory path */ function planningDir(cwd) { return path.join(cwd, '.planning'); @@ -778,6 +856,8 @@ module.exports = { replaceInCurrentMilestone, toPosixPath, extractOneLinerFromBody, + resolveWorktreeRoot, + withPlanningLock, MODEL_ALIAS_MAP, planningDir, planningPaths, diff --git a/tests/core.test.cjs b/tests/core.test.cjs index 3a3861ea1..3ed18ef01 100644 --- a/tests/core.test.cjs +++ b/tests/core.test.cjs @@ -898,3 +898,80 @@ describe('stale hook filter', () => { assert.ok(!filtered.includes('my-custom-hook.js'), 'must not include non-gsd hooks'); }); }); + +// ─── resolveWorktreeRoot ───────────────────────────────────────────────────── + +describe('resolveWorktreeRoot', () => { + const { resolveWorktreeRoot } = require('../get-shit-done/bin/lib/core.cjs'); + + test('returns cwd when not in a git repo', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-wt-test-')); + try { + assert.strictEqual(resolveWorktreeRoot(tmpDir), tmpDir); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + test('returns cwd in a normal git repo (not a worktree)', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-wt-test-')); + try { + const { execSync } = require('child_process'); + execSync('git init', { cwd: tmpDir, stdio: 'pipe' }); + assert.strictEqual(resolveWorktreeRoot(tmpDir), tmpDir); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); +}); + +// ─── withPlanningLock ──────────────────────────────────────────────────────── + +describe('withPlanningLock', () => { + const { withPlanningLock, planningDir } = require('../get-shit-done/bin/lib/core.cjs'); + + test('executes function and returns result', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-lock-test-')); + fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); + try { + const result = withPlanningLock(tmpDir, () => 42); + assert.strictEqual(result, 42); + // Lock file should be cleaned up + assert.ok(!fs.existsSync(path.join(planningDir(tmpDir), '.lock'))); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + test('cleans up lock file even on error', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-lock-test-')); + fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); + try { + assert.throws(() => { + withPlanningLock(tmpDir, () => { throw new Error('test'); }); + }, /test/); + assert.ok(!fs.existsSync(path.join(planningDir(tmpDir), '.lock'))); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + test('recovers from stale lock (>30s old)', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-lock-test-')); + const planDir = path.join(tmpDir, '.planning'); + fs.mkdirSync(planDir, { recursive: true }); + const lockPath = path.join(planDir, '.lock'); + try { + // Create a stale lock + fs.writeFileSync(lockPath, '{"pid":99999}'); + // Backdate the lock file by 31 seconds + const staleTime = new Date(Date.now() - 31000); + fs.utimesSync(lockPath, staleTime, staleTime); + + const result = withPlanningLock(tmpDir, () => 'recovered'); + assert.strictEqual(result, 'recovered'); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); +}); From 3e2c85e6fd1d8d71ac02cfe2b6226b63ebf63151 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Thu, 19 Mar 2026 15:05:20 -0400 Subject: [PATCH 78/83] refactor: consolidate STATE.md field helpers, fix command injection in isGitIgnored MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactoring: - Extract stateReplaceFieldWithFallback() to state.cjs as single source of truth for the try-primary-then-fallback pattern that was duplicated inline across phase.cjs, milestone.cjs, and state.cjs - Replace all inline bold-only regex patterns in cmdPhaseComplete with shared stateReplaceField/stateExtractField helpers — now supports both **Bold:** and plain Field: STATE.md formats (fixes the same bug as #924) - Replace inline bold-only regex patterns in cmdMilestoneComplete with shared helpers - Replace inline bold-only regex for Completed Phases/Total Phases/Progress counters in cmdPhaseComplete with stateExtractField/stateReplaceField - Replace inline bold-only Total Phases regex in cmdPhaseRemove with shared helpers Security: - Fix command injection surface in isGitIgnored (core.cjs): replace execSync with string concatenation with execFileSync using array arguments — prevents shell interpretation of special characters in file paths Tests (7 new): - 5 tests for stateReplaceFieldWithFallback: primary field, fallback, neither, preference, and plain format - 1 regression test: phase complete with plain-format STATE.md fields - 1 regression test: milestone complete with plain-format STATE.md fields 854 tests pass (was 847). No behavioral regressions. --- get-shit-done/bin/lib/core.cjs | 6 ++- get-shit-done/bin/lib/milestone.cjs | 22 +++----- get-shit-done/bin/lib/phase.cjs | 84 +++++++++++++---------------- get-shit-done/bin/lib/state.cjs | 32 +++++++---- tests/milestone.test.cjs | 17 ++++++ tests/phase.test.cjs | 25 +++++++++ tests/state.test.cjs | 41 +++++++++++++- 7 files changed, 152 insertions(+), 75 deletions(-) diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index 43b81bc20..c4c7ac707 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -4,7 +4,7 @@ const fs = require('fs'); const path = require('path'); -const { execSync, spawnSync } = require('child_process'); +const { execSync, execFileSync, spawnSync } = require('child_process'); const { MODEL_PROFILES } = require('./model-profiles.cjs'); // ─── Path helpers ──────────────────────────────────────────────────────────── @@ -131,7 +131,9 @@ function isGitIgnored(cwd, targetPath) { // Without it, git check-ignore returns "not ignored" for tracked files even when // .gitignore explicitly lists them — a common source of confusion when .planning/ // was committed before being added to .gitignore. - execSync('git check-ignore -q --no-index -- ' + targetPath.replace(/[^a-zA-Z0-9._\-/]/g, ''), { + // Use execFileSync (array args) to prevent shell interpretation of special characters + // in file paths — avoids command injection via crafted path names. + execFileSync('git', ['check-ignore', '-q', '--no-index', '--', targetPath], { cwd, stdio: 'pipe', }); diff --git a/get-shit-done/bin/lib/milestone.cjs b/get-shit-done/bin/lib/milestone.cjs index b105b9f2d..a86584d2f 100644 --- a/get-shit-done/bin/lib/milestone.cjs +++ b/get-shit-done/bin/lib/milestone.cjs @@ -6,7 +6,7 @@ const fs = require('fs'); const path = require('path'); const { escapeRegex, getMilestonePhaseFilter, extractOneLinerFromBody, normalizeMd, planningPaths, output, error } = require('./core.cjs'); const { extractFrontmatter } = require('./frontmatter.cjs'); -const { writeStateMd } = require('./state.cjs'); +const { writeStateMd, stateReplaceFieldWithFallback } = require('./state.cjs'); function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) { if (!reqIdsRaw || reqIdsRaw.length === 0) { @@ -194,21 +194,15 @@ function cmdMilestoneComplete(cwd, version, options, raw) { fs.writeFileSync(milestonesPath, normalizeMd(`# Milestones\n\n${milestoneEntry}`), 'utf-8'); } - // Update STATE.md + // Update STATE.md — use shared helpers that handle both **bold:** and plain Field: formats if (fs.existsSync(statePath)) { let stateContent = fs.readFileSync(statePath, 'utf-8'); - stateContent = stateContent.replace( - /(\*\*Status:\*\*\s*).*/, - `$1${version} milestone complete` - ); - stateContent = stateContent.replace( - /(\*\*Last Activity:\*\*\s*).*/, - `$1${today}` - ); - stateContent = stateContent.replace( - /(\*\*Last Activity Description:\*\*\s*).*/, - `$1${version} milestone completed and archived` - ); + + stateContent = stateReplaceFieldWithFallback(stateContent, 'Status', null, `${version} milestone complete`); + stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity', 'Last activity', today); + stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity Description', null, + `${version} milestone completed and archived`); + writeStateMd(statePath, stateContent, cwd); } diff --git a/get-shit-done/bin/lib/phase.cjs b/get-shit-done/bin/lib/phase.cjs index 097175e15..5b01f2bbd 100644 --- a/get-shit-done/bin/lib/phase.cjs +++ b/get-shit-done/bin/lib/phase.cjs @@ -6,7 +6,7 @@ const fs = require('fs'); const path = require('path'); const { escapeRegex, loadConfig, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, toPosixPath, output, error } = require('./core.cjs'); const { extractFrontmatter } = require('./frontmatter.cjs'); -const { writeStateMd } = require('./state.cjs'); +const { writeStateMd, stateExtractField, stateReplaceField, stateReplaceFieldWithFallback } = require('./state.cjs'); function cmdPhasesList(cwd, options, raw) { const phasesDir = path.join(cwd, '.planning', 'phases'); @@ -685,12 +685,11 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) { const statePath = path.join(cwd, '.planning', 'STATE.md'); if (fs.existsSync(statePath)) { let stateContent = fs.readFileSync(statePath, 'utf-8'); - // Update "Total Phases" field - const totalPattern = /(\*\*Total Phases:\*\*\s*)(\d+)/; - const totalMatch = stateContent.match(totalPattern); - if (totalMatch) { - const oldTotal = parseInt(totalMatch[2], 10); - stateContent = stateContent.replace(totalPattern, `$1${oldTotal - 1}`); + // Update "Total Phases" field — supports both bold and plain formats + const totalRaw = stateExtractField(stateContent, 'Total Phases'); + if (totalRaw) { + const oldTotal = parseInt(totalRaw, 10); + stateContent = stateReplaceField(stateContent, 'Total Phases', String(oldTotal - 1)) || stateContent; } // Update "Phase: X of Y" pattern const ofPattern = /(\bof\s+)(\d+)(\s*(?:\(|phases?))/i; @@ -882,67 +881,58 @@ function cmdPhaseComplete(cwd, phaseNum, raw) { } catch { /* intentionally empty */ } } - // Update STATE.md + // Update STATE.md — use shared helpers that handle both **bold:** and plain Field: formats if (fs.existsSync(statePath)) { let stateContent = fs.readFileSync(statePath, 'utf-8'); - // Update Current Phase - stateContent = stateContent.replace( - /(\*\*Current Phase:\*\*\s*).*/, - `$1${nextPhaseNum || phaseNum}` - ); + // Update Current Phase — preserve "X of Y (Name)" compound format + const phaseValue = nextPhaseNum || phaseNum; + const existingPhaseField = stateExtractField(stateContent, 'Current Phase') + || stateExtractField(stateContent, 'Phase'); + let newPhaseValue = String(phaseValue); + if (existingPhaseField) { + const totalMatch = existingPhaseField.match(/of\s+(\d+)/); + const nameMatch = existingPhaseField.match(/\(([^)]+)\)/); + if (totalMatch) { + const total = totalMatch[1]; + const nameStr = nextPhaseName ? ` (${nextPhaseName.replace(/-/g, ' ')})` : (nameMatch ? ` (${nameMatch[1]})` : ''); + newPhaseValue = `${phaseValue} of ${total}${nameStr}`; + } + } + stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase', 'Phase', newPhaseValue); // Update Current Phase Name if (nextPhaseName) { - stateContent = stateContent.replace( - /(\*\*Current Phase Name:\*\*\s*).*/, - `$1${nextPhaseName.replace(/-/g, ' ')}` - ); + stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase Name', null, nextPhaseName.replace(/-/g, ' ')); } // Update Status - stateContent = stateContent.replace( - /(\*\*Status:\*\*\s*).*/, - `$1${isLastPhase ? 'Milestone complete' : 'Ready to plan'}` - ); + stateContent = stateReplaceFieldWithFallback(stateContent, 'Status', null, + isLastPhase ? 'Milestone complete' : 'Ready to plan'); // Update Current Plan - stateContent = stateContent.replace( - /(\*\*Current Plan:\*\*\s*).*/, - `$1Not started` - ); + stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Plan', 'Plan', 'Not started'); // Update Last Activity - stateContent = stateContent.replace( - /(\*\*Last Activity:\*\*\s*).*/, - `$1${today}` - ); + stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity', 'Last activity', today); // Update Last Activity Description - stateContent = stateContent.replace( - /(\*\*Last Activity Description:\*\*\s*).*/, - `$1Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}` - ); + stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity Description', null, + `Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`); // Increment Completed Phases counter (#956) - const completedMatch = stateContent.match(/\*\*Completed Phases:\*\*\s*(\d+)/); - if (completedMatch) { - const newCompleted = parseInt(completedMatch[1], 10) + 1; - stateContent = stateContent.replace( - /(\*\*Completed Phases:\*\*\s*)\d+/, - `$1${newCompleted}` - ); + const completedRaw = stateExtractField(stateContent, 'Completed Phases'); + if (completedRaw) { + const newCompleted = parseInt(completedRaw, 10) + 1; + stateContent = stateReplaceField(stateContent, 'Completed Phases', String(newCompleted)) || stateContent; // Recalculate percent based on completed / total (#956) - const totalMatch = stateContent.match(/\*\*Total Phases:\*\*\s*(\d+)/); - if (totalMatch) { - const totalPhases = parseInt(totalMatch[1], 10); + const totalRaw = stateExtractField(stateContent, 'Total Phases'); + if (totalRaw) { + const totalPhases = parseInt(totalRaw, 10); if (totalPhases > 0) { const newPercent = Math.round((newCompleted / totalPhases) * 100); - stateContent = stateContent.replace( - /(\*\*Progress:\*\*\s*)\d+%/, - `$1${newPercent}%` - ); + stateContent = stateReplaceField(stateContent, 'Progress', `${newPercent}%`) || stateContent; // Also update percent field if it exists separately stateContent = stateContent.replace( /(percent:\s*)\d+/, diff --git a/get-shit-done/bin/lib/state.cjs b/get-shit-done/bin/lib/state.cjs index 3a91006b9..a01aafd66 100644 --- a/get-shit-done/bin/lib/state.cjs +++ b/get-shit-done/bin/lib/state.cjs @@ -201,6 +201,22 @@ function stateReplaceField(content, fieldName, newValue) { return null; } +/** + * Replace a STATE.md field with fallback field name support. + * Tries `primary` first, then `fallback` (if provided), returns content unchanged + * if neither matches. This consolidates the replaceWithFallback pattern that was + * previously duplicated inline across phase.cjs, milestone.cjs, and state.cjs. + */ +function stateReplaceFieldWithFallback(content, primary, fallback, value) { + let result = stateReplaceField(content, primary, value); + if (result) return result; + if (fallback) { + result = stateReplaceField(content, fallback, value); + if (result) return result; + } + return content; +} + function cmdStateAdvancePlan(cwd, raw) { const statePath = planningPaths(cwd).state; if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; } @@ -232,16 +248,9 @@ function cmdStateAdvancePlan(cwd, raw) { return; } - const replaceField = (c, primary, fallback, value) => { - let r = stateReplaceField(c, primary, value); - if (r) return r; - if (fallback) { r = stateReplaceField(c, fallback, value); if (r) return r; } - return c; - }; - if (currentPlan >= totalPlans) { - content = replaceField(content, 'Status', null, 'Phase complete — ready for verification'); - content = replaceField(content, 'Last Activity', 'Last activity', today); + content = stateReplaceFieldWithFallback(content, 'Status', null, 'Phase complete — ready for verification'); + content = stateReplaceFieldWithFallback(content, 'Last Activity', 'Last activity', today); writeStateMd(statePath, content, cwd); output({ advanced: false, reason: 'last_plan', current_plan: currentPlan, total_plans: totalPlans, status: 'ready_for_verification' }, raw, 'false'); } else { @@ -253,8 +262,8 @@ function cmdStateAdvancePlan(cwd, raw) { } else { content = stateReplaceField(content, 'Current Plan', String(newPlan)) || content; } - content = replaceField(content, 'Status', null, 'Ready to execute'); - content = replaceField(content, 'Last Activity', 'Last activity', today); + content = stateReplaceFieldWithFallback(content, 'Status', null, 'Ready to execute'); + content = stateReplaceFieldWithFallback(content, 'Last Activity', 'Last activity', today); writeStateMd(statePath, content, cwd); output({ advanced: true, previous_plan: currentPlan, current_plan: newPlan, total_plans: totalPlans }, raw, 'true'); } @@ -906,6 +915,7 @@ function cmdSignalResume(cwd, raw) { module.exports = { stateExtractField, stateReplaceField, + stateReplaceFieldWithFallback, writeStateMd, cmdStateLoad, cmdStateGet, diff --git a/tests/milestone.test.cjs b/tests/milestone.test.cjs index 77b3c8376..c3319d10c 100644 --- a/tests/milestone.test.cjs +++ b/tests/milestone.test.cjs @@ -476,6 +476,23 @@ describe('milestone complete command', () => { ); }); + test('updates STATE.md with plain format fields', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap v1.0\n` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# State\n\nStatus: In progress\nLast Activity: 2025-01-01\nLast Activity Description: Working\n` + ); + + const result = runGsdTools('milestone complete v1.0 --name Test', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); + assert.ok(state.includes('v1.0 milestone complete'), 'plain Status field should be updated'); + }); + test('handles empty phases directory', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'), diff --git a/tests/phase.test.cjs b/tests/phase.test.cjs index a4df0743c..bbc0c004e 100644 --- a/tests/phase.test.cjs +++ b/tests/phase.test.cjs @@ -1508,6 +1508,31 @@ describe('phase complete command', () => { assert.strictEqual(cells[1], 'v1.0', 'Milestone column should be preserved'); assert.ok(cells[3].includes('Complete'), 'Status column should be Complete'); }); + + test('updates STATE.md with plain format fields (no bold)', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap\n\n### Phase 1: Only\n**Goal:** Test\n` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# State\n\nPhase: 1 of 1 (Only)\nStatus: In progress\nPlan: 01-01\nLast Activity: 2025-01-01\nLast Activity Description: Working\n` + ); + + const p1 = path.join(tmpDir, '.planning', 'phases', '01-only'); + 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'); + + const result = runGsdTools('phase complete 1', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); + assert.ok(state.includes('Milestone complete'), 'plain Status field should be updated'); + assert.ok(state.includes('Not started'), 'plain Plan field should be updated'); + // Verify compound format preserved + assert.ok(state.match(/Phase:.*of\s+1/), 'should preserve "of N" in compound Phase format'); + }); }); // ───────────────────────────────────────────────────────────────────────────── diff --git a/tests/state.test.cjs b/tests/state.test.cjs index fe3b84670..7f86f25fc 100644 --- a/tests/state.test.cjs +++ b/tests/state.test.cjs @@ -506,7 +506,7 @@ describe('STATE.md frontmatter sync', () => { // stateExtractField and stateReplaceField helpers // ───────────────────────────────────────────────────────────────────────────── -const { stateExtractField, stateReplaceField } = require('../get-shit-done/bin/lib/state.cjs'); +const { stateExtractField, stateReplaceField, stateReplaceFieldWithFallback } = require('../get-shit-done/bin/lib/state.cjs'); describe('stateExtractField and stateReplaceField helpers', () => { // stateExtractField tests @@ -585,6 +585,45 @@ describe('stateExtractField and stateReplaceField helpers', () => { }); }); +// ───────────────────────────────────────────────────────────────────────────── +// stateReplaceFieldWithFallback — consolidated fallback helper +// ───────────────────────────────────────────────────────────────────────────── + +describe('stateReplaceFieldWithFallback', () => { + test('replaces primary field when present', () => { + const content = '# State\n\n**Status:** Old\n'; + const result = stateReplaceFieldWithFallback(content, 'Status', null, 'New'); + assert.ok(result.includes('**Status:** New')); + }); + + test('falls back to secondary field when primary not found', () => { + const content = '# State\n\nLast activity: 2024-01-01\n'; + const result = stateReplaceFieldWithFallback(content, 'Last Activity', 'Last activity', '2025-03-19'); + assert.ok(result.includes('Last activity: 2025-03-19'), 'should update fallback field'); + }); + + test('returns content unchanged when neither field matches', () => { + const content = '# State\n\n**Phase:** 3\n'; + const result = stateReplaceFieldWithFallback(content, 'Status', 'state', 'New'); + assert.strictEqual(result, content, 'content should be unchanged'); + }); + + test('prefers primary over fallback when both exist', () => { + const content = '# State\n\n**Status:** Old\nStatus: Also old\n'; + const result = stateReplaceFieldWithFallback(content, 'Status', 'Status', 'New'); + // Bold format is tried first by stateReplaceField + assert.ok(result.includes('**Status:** New'), 'should replace bold (primary) format'); + }); + + test('works with plain format fields', () => { + const content = '# State\n\nPhase: 1 of 3 (Foundation)\nStatus: In progress\nPlan: 01-01\n'; + let updated = stateReplaceFieldWithFallback(content, 'Status', null, 'Complete'); + assert.ok(updated.includes('Status: Complete'), 'should update plain Status'); + updated = stateReplaceFieldWithFallback(updated, 'Current Plan', 'Plan', 'Not started'); + assert.ok(updated.includes('Plan: Not started'), 'should fall back to Plan field'); + }); +}); + // ───────────────────────────────────────────────────────────────────────────── // cmdStateLoad, cmdStateGet, cmdStatePatch, cmdStateUpdate CLI tests // ───────────────────────────────────────────────────────────────────────────── From 21081dc821c4883c0501a510bf177bf0be32a89b Mon Sep 17 00:00:00 2001 From: Srinivas Koduri Date: Wed, 18 Mar 2026 19:35:22 -0700 Subject: [PATCH 79/83] feat: multi-repo workspace support with auto-detection and project root resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for workspaces with multiple independent git repositories. When configured, GSD routes commits to the correct sub-repo and ensures .planning/ stays at the project root. Core features: - detectSubRepos(): scans child directories for .git to discover repos - findProjectRoot(): walks up from CWD to find the project root that owns .planning/, preventing orphaned .planning/ in sub-repos - loadConfig auto-syncs sub_repos when repos are added or removed - Migrates legacy "multiRepo: true" to sub_repos array automatically - All init commands include project_root in output - cmdCommitToSubrepo: groups files by sub-repo prefix, commits independently Zero impact on single-repo workflows — sub_repos defaults to empty array. Co-Authored-By: Claude Opus 4.6 (1M context) --- agents/gsd-executor.md | 10 +- get-shit-done/bin/gsd-tools.cjs | 16 +- get-shit-done/bin/lib/commands.cjs | 65 ++++++ get-shit-done/bin/lib/core.cjs | 122 +++++++++++ get-shit-done/bin/lib/init.cjs | 34 +-- get-shit-done/references/git-integration.md | 43 ++++ get-shit-done/templates/config.json | 3 +- get-shit-done/workflows/execute-plan.md | 16 +- get-shit-done/workflows/new-project.md | 33 +++ tests/core.test.cjs | 227 ++++++++++++++++++++ tests/init.test.cjs | 64 ++++++ 11 files changed, 616 insertions(+), 17 deletions(-) diff --git a/agents/gsd-executor.md b/agents/gsd-executor.md index 03f604248..fa3f8c5e1 100644 --- a/agents/gsd-executor.md +++ b/agents/gsd-executor.md @@ -47,7 +47,7 @@ INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init execute-phase " if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi ``` -Extract from init JSON: `executor_model`, `commit_docs`, `phase_dir`, `plans`, `incomplete_plans`. +Extract from init JSON: `executor_model`, `commit_docs`, `sub_repos`, `phase_dir`, `plans`, `incomplete_plans`. Also read STATE.md for position, decisions, blockers: ```bash @@ -328,6 +328,14 @@ git add src/types/user.ts | `chore` | Config, tooling, dependencies | **4. Commit:** + +**If `sub_repos` is configured (non-empty array from init context):** Use `commit-to-subrepo` to route files to their correct sub-repo: +```bash +node ~/.claude/get-shit-done/bin/gsd-tools.cjs commit-to-subrepo "{type}({phase}-{plan}): {concise task description}" --files file1 file2 ... +``` +Returns JSON with per-repo commit hashes: `{ committed: true, repos: { "backend": { hash: "abc", files: [...] }, ... } }`. Record all hashes for SUMMARY. + +**Otherwise (standard single-repo):** ```bash git commit -m "{type}({phase}-{plan}): {concise task description} diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index 7842fb6d8..a7cd70377 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -20,6 +20,7 @@ * resolve-model Get model for agent based on profile * find-phase Find phase directory by number * commit [--files f1 f2] [--no-verify] Commit planning docs + * commit-to-subrepo --files f1 f2 Route commits to sub-repos * verify-summary Verify a SUMMARY.md file * generate-slug Convert text to URL-safe slug * current-timestamp [format] Get timestamp (full|date|filename) @@ -134,7 +135,7 @@ const fs = require('fs'); const path = require('path'); -const { error } = require('./lib/core.cjs'); +const { error, findProjectRoot } = require('./lib/core.cjs'); const state = require('./lib/state.cjs'); const phase = require('./lib/phase.cjs'); const roadmap = require('./lib/roadmap.cjs'); @@ -177,10 +178,13 @@ async function main() { const { resolveWorktreeRoot } = require('./lib/core.cjs'); const worktreeRoot = resolveWorktreeRoot(cwd); if (worktreeRoot !== cwd) { - // Only override cwd for planning-related commands — keep original cwd for git operations cwd = worktreeRoot; } + // Multi-repo guard: if CWD is inside a sub-repo, walk up to the project root + // so .planning/ is read/written at the correct level. + cwd = findProjectRoot(cwd); + const rawIndex = args.indexOf('--raw'); const raw = rawIndex !== -1; if (rawIndex !== -1) args.splice(rawIndex, 1); @@ -314,6 +318,14 @@ async function main() { break; } + case 'commit-to-subrepo': { + const message = args[1]; + const filesIndex = args.indexOf('--files'); + const files = filesIndex !== -1 ? args.slice(filesIndex + 1).filter(a => !a.startsWith('--')) : []; + commands.cmdCommitToSubrepo(cwd, message, files, raw); + break; + } + case 'verify-summary': { const summaryPath = args[1]; const countIndex = args.indexOf('--check-count'); diff --git a/get-shit-done/bin/lib/commands.cjs b/get-shit-done/bin/lib/commands.cjs index a43a0e822..27461c393 100644 --- a/get-shit-done/bin/lib/commands.cjs +++ b/get-shit-done/bin/lib/commands.cjs @@ -269,6 +269,70 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify) { output(result, raw, hash || 'committed'); } +function cmdCommitToSubrepo(cwd, message, files, raw) { + if (!message) { + error('commit message required'); + } + + const config = loadConfig(cwd); + const subRepos = config.sub_repos; + + if (!subRepos || subRepos.length === 0) { + error('no sub_repos configured in .planning/config.json'); + } + + if (!files || files.length === 0) { + error('--files required for commit-to-subrepo'); + } + + // Group files by sub-repo prefix + const grouped = {}; + const unmatched = []; + for (const file of files) { + const match = subRepos.find(repo => file.startsWith(repo + '/')); + if (match) { + if (!grouped[match]) grouped[match] = []; + grouped[match].push(file); + } else { + unmatched.push(file); + } + } + + const repos = {}; + for (const [repo, repoFiles] of Object.entries(grouped)) { + const repoCwd = path.join(cwd, repo); + + // Stage files (strip sub-repo prefix for paths relative to that repo) + for (const file of repoFiles) { + const relativePath = file.slice(repo.length + 1); + execGit(repoCwd, ['add', relativePath]); + } + + // Commit + const commitResult = execGit(repoCwd, ['commit', '-m', message]); + if (commitResult.exitCode !== 0) { + if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) { + repos[repo] = { committed: false, hash: null, files: repoFiles, reason: 'nothing_to_commit' }; + continue; + } + repos[repo] = { committed: false, hash: null, files: repoFiles, reason: 'error', error: commitResult.stderr }; + continue; + } + + // Get hash + const hashResult = execGit(repoCwd, ['rev-parse', '--short', 'HEAD']); + const hash = hashResult.exitCode === 0 ? hashResult.stdout : null; + repos[repo] = { committed: true, hash, files: repoFiles }; + } + + const result = { + committed: Object.values(repos).some(r => r.committed), + repos, + unmatched: unmatched.length > 0 ? unmatched : undefined, + }; + output(result, raw, Object.entries(repos).map(([r, v]) => `${r}:${v.hash || 'skip'}`).join(' ')); +} + function cmdSummaryExtract(cwd, summaryPath, fields, raw) { if (!summaryPath) { error('summary-path required for summary-extract'); @@ -831,6 +895,7 @@ module.exports = { cmdHistoryDigest, cmdResolveModel, cmdCommit, + cmdCommitToSubrepo, cmdSummaryExtract, cmdWebsearch, cmdProgressRender, diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index c4c7ac707..3101f02e5 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -14,6 +14,91 @@ function toPosixPath(p) { return p.split(path.sep).join('/'); } +/** + * Scan immediate child directories for separate git repos. + * Returns a sorted array of directory names that have their own `.git`. + * Excludes hidden directories and node_modules. + */ +function detectSubRepos(cwd) { + const results = []; + try { + const entries = fs.readdirSync(cwd, { withFileTypes: true }); + for (const entry of entries) { + if (!entry.isDirectory()) continue; + if (entry.name.startsWith('.') || entry.name === 'node_modules') continue; + const gitPath = path.join(cwd, entry.name, '.git'); + try { + if (fs.existsSync(gitPath)) { + results.push(entry.name); + } + } catch {} + } + } catch {} + return results.sort(); +} + +/** + * Walk up from `startDir` to find the project root that owns `.planning/`. + * + * In multi-repo workspaces, Claude may open inside a sub-repo (e.g. `backend/`) + * instead of the project root. This function prevents `.planning/` from being + * created inside the sub-repo by locating the nearest ancestor that already has + * a `.planning/` directory. + * + * Detection strategy (checked in order for each ancestor): + * 1. Parent has `.planning/config.json` with `sub_repos` listing this directory + * 2. Parent has `.planning/config.json` with `multiRepo: true` (legacy format) + * 3. Parent has `.planning/` and current dir has its own `.git` (heuristic) + * + * Returns `startDir` unchanged when no ancestor `.planning/` is found (first-run + * or single-repo projects). + */ +function findProjectRoot(startDir) { + const resolved = path.resolve(startDir); + const root = path.parse(resolved).root; + const homedir = require('os').homedir(); + const startHasGit = fs.existsSync(path.join(resolved, '.git')); + + let dir = resolved; + while (dir !== root) { + const parent = path.dirname(dir); + if (parent === dir) break; // filesystem root + if (parent === homedir) break; // never go above home + + const parentPlanning = path.join(parent, '.planning'); + if (fs.existsSync(parentPlanning) && fs.statSync(parentPlanning).isDirectory()) { + const configPath = path.join(parentPlanning, 'config.json'); + try { + const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + const subRepos = config.sub_repos || config.planning?.sub_repos || []; + + // Check explicit sub_repos list + if (Array.isArray(subRepos) && subRepos.length > 0) { + const relPath = path.relative(parent, resolved); + const topSegment = relPath.split(path.sep)[0]; + if (subRepos.includes(topSegment)) { + return parent; + } + } + + // Check legacy multiRepo flag + if (config.multiRepo === true && startHasGit) { + return parent; + } + } catch { + // config.json missing or malformed — fall back to .git heuristic + } + + // Heuristic: parent has .planning/ and startDir has its own .git + if (startHasGit) { + return parent; + } + } + dir = parent; + } + return startDir; +} + // ─── Output helpers ─────────────────────────────────────────────────────────── function output(result, raw, rawValue) { @@ -66,6 +151,7 @@ function loadConfig(cwd) { parallelization: true, brave_search: false, text_mode: false, // when true, use plain-text numbered lists instead of AskUserQuestion menus + sub_repos: [], resolve_model_ids: false, // when true, resolve aliases (opus/sonnet/haiku) to full model IDs context_window: 200000, // default 200k; set to 1000000 for Opus/Sonnet 4.6 1M models phase_naming: 'sequential', // 'sequential' (default, auto-increment) or 'custom' (arbitrary string IDs) @@ -83,6 +169,39 @@ function loadConfig(cwd) { try { fs.writeFileSync(configPath, JSON.stringify(parsed, null, 2), 'utf-8'); } catch { /* intentionally empty */ } } + // Auto-detect and sync sub_repos: scan for child directories with .git + let configDirty = false; + + // Migrate legacy "multiRepo: true" boolean → sub_repos array + if (parsed.multiRepo === true && !parsed.sub_repos && !parsed.planning?.sub_repos) { + const detected = detectSubRepos(cwd); + if (detected.length > 0) { + parsed.sub_repos = detected; + if (!parsed.planning) parsed.planning = {}; + parsed.planning.commit_docs = false; + delete parsed.multiRepo; + configDirty = true; + } + } + + // Keep sub_repos in sync with actual filesystem + const currentSubRepos = parsed.sub_repos || parsed.planning?.sub_repos || []; + if (Array.isArray(currentSubRepos) && currentSubRepos.length > 0) { + const detected = detectSubRepos(cwd); + if (detected.length > 0) { + const sorted = [...currentSubRepos].sort(); + if (JSON.stringify(sorted) !== JSON.stringify(detected)) { + parsed.sub_repos = detected; + configDirty = true; + } + } + } + + // Persist sub_repos changes (migration or sync) + if (configDirty) { + try { fs.writeFileSync(configPath, JSON.stringify(parsed, null, 2), 'utf-8'); } catch {} + } + const get = (key, nested) => { if (parsed[key] !== undefined) return parsed[key]; if (nested && parsed[nested.section] && parsed[nested.section][nested.field] !== undefined) { @@ -113,6 +232,7 @@ function loadConfig(cwd) { parallelization, brave_search: get('brave_search') ?? defaults.brave_search, text_mode: get('text_mode', { section: 'workflow', field: 'text_mode' }) ?? defaults.text_mode, + sub_repos: get('sub_repos', { section: 'planning', field: 'sub_repos' }) ?? defaults.sub_repos, resolve_model_ids: get('resolve_model_ids') ?? defaults.resolve_model_ids, context_window: get('context_window') ?? defaults.context_window, phase_naming: get('phase_naming') ?? defaults.phase_naming, @@ -860,6 +980,8 @@ module.exports = { extractOneLinerFromBody, resolveWorktreeRoot, withPlanningLock, + findProjectRoot, + detectSubRepos, MODEL_ALIAS_MAP, planningDir, planningPaths, diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index 3f21237a3..b19d3d1d8 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -24,6 +24,16 @@ function getLatestCompletedMilestone(cwd) { } } +/** + * Inject `project_root` into an init result object. + * Workflows use this to prefix `.planning/` paths correctly when Claude's CWD + * differs from the project root (e.g., inside a sub-repo). + */ +function withProjectRoot(cwd, result) { + result.project_root = cwd; + return result; +} + function cmdInitExecutePhase(cwd, phase, raw) { if (!phase) { error('phase required for init execute-phase'); @@ -95,7 +105,7 @@ function cmdInitExecutePhase(cwd, phase, raw) { config_path: '.planning/config.json', }; - output(result, raw); + output(withProjectRoot(cwd, result), raw); } function cmdInitPlanPhase(cwd, phase, raw) { @@ -174,7 +184,7 @@ function cmdInitPlanPhase(cwd, phase, raw) { } catch { /* intentionally empty */ } } - output(result, raw); + output(withProjectRoot(cwd, result), raw); } function cmdInitNewProject(cwd, raw) { @@ -242,7 +252,7 @@ function cmdInitNewProject(cwd, raw) { project_path: '.planning/PROJECT.md', }; - output(result, raw); + output(withProjectRoot(cwd, result), raw); } function cmdInitNewMilestone(cwd, raw) { @@ -289,7 +299,7 @@ function cmdInitNewMilestone(cwd, raw) { state_path: '.planning/STATE.md', }; - output(result, raw); + output(withProjectRoot(cwd, result), raw); } function cmdInitQuick(cwd, description, raw) { @@ -347,7 +357,7 @@ function cmdInitQuick(cwd, description, raw) { }; - output(result, raw); + output(withProjectRoot(cwd, result), raw); } function cmdInitResume(cwd, raw) { @@ -379,7 +389,7 @@ function cmdInitResume(cwd, raw) { commit_docs: config.commit_docs, }; - output(result, raw); + output(withProjectRoot(cwd, result), raw); } function cmdInitVerifyWork(cwd, phase, raw) { @@ -408,7 +418,7 @@ function cmdInitVerifyWork(cwd, phase, raw) { has_verification: phaseInfo?.has_verification || false, }; - output(result, raw); + output(withProjectRoot(cwd, result), raw); } function cmdInitPhaseOp(cwd, phase, raw) { @@ -512,7 +522,7 @@ function cmdInitPhaseOp(cwd, phase, raw) { } catch { /* intentionally empty */ } } - output(result, raw); + output(withProjectRoot(cwd, result), raw); } function cmdInitTodos(cwd, area, raw) { @@ -571,7 +581,7 @@ function cmdInitTodos(cwd, area, raw) { pending_dir_exists: pathExistsInternal(cwd, '.planning/todos/pending'), }; - output(result, raw); + output(withProjectRoot(cwd, result), raw); } function cmdInitMilestoneOp(cwd, raw) { @@ -632,7 +642,7 @@ function cmdInitMilestoneOp(cwd, raw) { phases_dir_exists: pathExistsInternal(cwd, '.planning/phases'), }; - output(result, raw); + output(withProjectRoot(cwd, result), raw); } function cmdInitMapCodebase(cwd, raw) { @@ -666,7 +676,7 @@ function cmdInitMapCodebase(cwd, raw) { codebase_dir_exists: pathExistsInternal(cwd, '.planning/codebase'), }; - output(result, raw); + output(withProjectRoot(cwd, result), raw); } function cmdInitProgress(cwd, raw) { @@ -813,7 +823,7 @@ function cmdInitProgress(cwd, raw) { config_path: '.planning/config.json', }; - output(result, raw); + output(withProjectRoot(cwd, result), raw); } module.exports = { diff --git a/get-shit-done/references/git-integration.md b/get-shit-done/references/git-integration.md index d9bbecac2..2b6a9b733 100644 --- a/get-shit-done/references/git-integration.md +++ b/get-shit-done/references/git-integration.md @@ -250,3 +250,46 @@ Each plan produces 2-4 commits (tasks + metadata). Clear, granular, bisectable. - "Commit noise" irrelevant when consumer is Claude, not humans + + + +## Multi-Repo Workspace Support (sub_repos) + +For workspaces with separate git repos (e.g., `backend/`, `frontend/`, `shared/`), GSD routes commits to each repo independently. + +### Configuration + +In `.planning/config.json`, list sub-repo directories under `planning.sub_repos`: + +```json +{ + "planning": { + "commit_docs": false, + "sub_repos": ["backend", "frontend", "shared"] + } +} +``` + +Set `commit_docs: false` so planning docs stay local and are not committed to any sub-repo. + +### How It Works + +1. **Auto-detection:** During `/gsd:new-project`, directories with their own `.git` folder are detected and offered for selection as sub-repos. +2. **File grouping:** Code files are grouped by their sub-repo prefix (e.g., `backend/src/api/users.ts` belongs to the `backend/` repo). +3. **Independent commits:** Each sub-repo receives its own atomic commit via `gsd-tools.cjs commit-to-subrepo`. File paths are made relative to the sub-repo root before staging. +4. **Planning stays local:** The `.planning/` directory is not committed; it acts as cross-repo coordination. + +### Commit Routing + +Instead of the standard `commit` command, use `commit-to-subrepo` when `sub_repos` is configured: + +```bash +node ~/.claude/get-shit-done/bin/gsd-tools.cjs commit-to-subrepo "feat(02-01): add user API" \ + --files backend/src/api/users.ts backend/src/types/user.ts frontend/src/components/UserForm.tsx +``` + +This stages `src/api/users.ts` and `src/types/user.ts` in the `backend/` repo, and `src/components/UserForm.tsx` in the `frontend/` repo, then commits each independently with the same message. + +Files that don't match any configured sub-repo are reported as unmatched. + + diff --git a/get-shit-done/templates/config.json b/get-shit-done/templates/config.json index 462e63628..6b8b46064 100644 --- a/get-shit-done/templates/config.json +++ b/get-shit-done/templates/config.json @@ -10,7 +10,8 @@ }, "planning": { "commit_docs": true, - "search_gitignored": false + "search_gitignored": false, + "sub_repos": [] }, "parallelization": { "enabled": true, diff --git a/get-shit-done/workflows/execute-plan.md b/get-shit-done/workflows/execute-plan.md index 0e5d8e8a5..437e8302a 100644 --- a/get-shit-done/workflows/execute-plan.md +++ b/get-shit-done/workflows/execute-plan.md @@ -19,7 +19,7 @@ INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init execute-phase " if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi ``` -Extract from init JSON: `executor_model`, `commit_docs`, `phase_dir`, `phase_number`, `plans`, `summaries`, `incomplete_plans`, `state_path`, `config_path`. +Extract from init JSON: `executor_model`, `commit_docs`, `sub_repos`, `phase_dir`, `phase_number`, `plans`, `summaries`, `incomplete_plans`, `state_path`, `config_path`. If `.planning/` missing: error. @@ -276,6 +276,20 @@ git add src/types/user.ts **4. Format:** `{type}({phase}-{plan}): {description}` with bullet points for key changes. + +**Sub-repos mode:** If `sub_repos` is configured (non-empty array from init context), use `commit-to-subrepo` instead of standard git commit. This routes files to their correct sub-repo based on path prefix. + +```bash +node ~/.claude/get-shit-done/bin/gsd-tools.cjs commit-to-subrepo "{type}({phase}-{plan}): {description}" --files file1 file2 ... +``` + +The command groups files by sub-repo prefix and commits atomically to each. Returns JSON: `{ committed: true, repos: { "backend": { hash: "abc", files: [...] }, ... } }`. + +Record hashes from each repo in the response for SUMMARY tracking. + +**If `sub_repos` is empty or not set:** Use standard git commit flow below. + + **5. Record hash:** ```bash TASK_COMMIT=$(git rev-parse --short HEAD) diff --git a/get-shit-done/workflows/new-project.md b/get-shit-done/workflows/new-project.md index b5876e989..b5cb42482 100644 --- a/get-shit-done/workflows/new-project.md +++ b/get-shit-done/workflows/new-project.md @@ -528,6 +528,39 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "chore: add project **Note:** Run `/gsd:settings` anytime to update these preferences. +## 5.1. Sub-Repo Detection + +**Detect multi-repo workspace:** + +Check for directories with their own `.git` folders (separate repos within the workspace): + +```bash +find . -maxdepth 2 -type d -name ".git" -not -path "./.git" +``` + +**If sub-repos found:** + +Strip the `/.git` suffix and `./` prefix to get directory names (e.g., `./backend/.git` → `backend`). + +Use AskUserQuestion: +- header: "Multi-Repo Workspace" +- question: "I detected separate git repos in this workspace. Which directories contain code that GSD should commit to?" +- multiSelect: true +- options: one option per detected directory + - "[directory name]" — Separate git repo + +**If user selects one or more directories:** +- Set `planning.sub_repos` in config.json to the selected directory names array (e.g., `["backend", "frontend"]`) +- Auto-set `planning.commit_docs` to `false` (planning docs stay local in multi-repo workspaces) +- Add `.planning/` to `.gitignore` if not already present + +Update the config file: +```bash +node ~/.claude/get-shit-done/bin/gsd-tools.cjs commit "chore: configure multi-repo workspace" --files .planning/config.json +``` + +**If no sub-repos found or user selects none:** Continue with no changes to config. + ## 5.5. Resolve Model Profile Use models from init: `researcher_model`, `synthesizer_model`, `roadmapper_model`. diff --git a/tests/core.test.cjs b/tests/core.test.cjs index 3ed18ef01..eca98f1f4 100644 --- a/tests/core.test.cjs +++ b/tests/core.test.cjs @@ -27,6 +27,8 @@ const { getRoadmapPhaseInternal, searchPhaseInDir, findPhaseInternal, + findProjectRoot, + detectSubRepos, } = require('../get-shit-done/bin/lib/core.cjs'); // ─── loadConfig ──────────────────────────────────────────────────────────────── @@ -975,3 +977,228 @@ describe('withPlanningLock', () => { } }); }); + +// ─── detectSubRepos ────────────────────────────────────────────────────────── + +describe('detectSubRepos', () => { + let projectRoot; + + beforeEach(() => { + projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-detect-test-')); + }); + + afterEach(() => { + fs.rmSync(projectRoot, { recursive: true, force: true }); + }); + + test('returns empty array when no child directories have .git', () => { + fs.mkdirSync(path.join(projectRoot, 'src')); + fs.mkdirSync(path.join(projectRoot, 'lib')); + assert.deepStrictEqual(detectSubRepos(projectRoot), []); + }); + + test('detects directories with .git', () => { + fs.mkdirSync(path.join(projectRoot, 'backend', '.git'), { recursive: true }); + fs.mkdirSync(path.join(projectRoot, 'frontend', '.git'), { recursive: true }); + fs.mkdirSync(path.join(projectRoot, 'scripts')); // no .git + assert.deepStrictEqual(detectSubRepos(projectRoot), ['backend', 'frontend']); + }); + + test('returns sorted results', () => { + fs.mkdirSync(path.join(projectRoot, 'zeta', '.git'), { recursive: true }); + fs.mkdirSync(path.join(projectRoot, 'alpha', '.git'), { recursive: true }); + fs.mkdirSync(path.join(projectRoot, 'mid', '.git'), { recursive: true }); + assert.deepStrictEqual(detectSubRepos(projectRoot), ['alpha', 'mid', 'zeta']); + }); + + test('skips hidden directories', () => { + fs.mkdirSync(path.join(projectRoot, '.hidden', '.git'), { recursive: true }); + fs.mkdirSync(path.join(projectRoot, 'visible', '.git'), { recursive: true }); + assert.deepStrictEqual(detectSubRepos(projectRoot), ['visible']); + }); + + test('skips node_modules', () => { + fs.mkdirSync(path.join(projectRoot, 'node_modules', '.git'), { recursive: true }); + fs.mkdirSync(path.join(projectRoot, 'app', '.git'), { recursive: true }); + assert.deepStrictEqual(detectSubRepos(projectRoot), ['app']); + }); +}); + +// ─── loadConfig sub_repos auto-sync ────────────────────────────────────────── + +describe('loadConfig sub_repos auto-sync', () => { + let projectRoot; + + beforeEach(() => { + projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-sync-test-')); + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + }); + + afterEach(() => { + fs.rmSync(projectRoot, { recursive: true, force: true }); + }); + + test('migrates multiRepo: true to sub_repos array', () => { + // Create config with legacy multiRepo flag + fs.writeFileSync( + path.join(projectRoot, '.planning', 'config.json'), + JSON.stringify({ multiRepo: true, model_profile: 'quality' }) + ); + // Create sub-repos + fs.mkdirSync(path.join(projectRoot, 'backend', '.git'), { recursive: true }); + fs.mkdirSync(path.join(projectRoot, 'frontend', '.git'), { recursive: true }); + + const config = loadConfig(projectRoot); + assert.deepStrictEqual(config.sub_repos, ['backend', 'frontend']); + assert.strictEqual(config.commit_docs, false); + + // Verify config was persisted + const saved = JSON.parse(fs.readFileSync(path.join(projectRoot, '.planning', 'config.json'), 'utf-8')); + assert.deepStrictEqual(saved.sub_repos, ['backend', 'frontend']); + assert.strictEqual(saved.multiRepo, undefined, 'multiRepo should be removed'); + }); + + test('adds newly detected repos to sub_repos', () => { + fs.mkdirSync(path.join(projectRoot, 'backend', '.git'), { recursive: true }); + fs.writeFileSync( + path.join(projectRoot, '.planning', 'config.json'), + JSON.stringify({ sub_repos: ['backend'] }) + ); + + // Add a new repo + fs.mkdirSync(path.join(projectRoot, 'frontend', '.git'), { recursive: true }); + + const config = loadConfig(projectRoot); + assert.deepStrictEqual(config.sub_repos, ['backend', 'frontend']); + }); + + test('removes repos that no longer have .git', () => { + fs.mkdirSync(path.join(projectRoot, 'backend', '.git'), { recursive: true }); + fs.writeFileSync( + path.join(projectRoot, '.planning', 'config.json'), + JSON.stringify({ sub_repos: ['backend', 'old-repo'] }) + ); + + const config = loadConfig(projectRoot); + assert.deepStrictEqual(config.sub_repos, ['backend']); + }); + + test('does not sync when sub_repos is empty and no repos detected', () => { + fs.writeFileSync( + path.join(projectRoot, '.planning', 'config.json'), + JSON.stringify({ sub_repos: [] }) + ); + + const config = loadConfig(projectRoot); + assert.deepStrictEqual(config.sub_repos, []); + }); +}); + +// ─── findProjectRoot ───────────────────────────────────────────────────────── + +describe('findProjectRoot', () => { + let projectRoot; + + beforeEach(() => { + projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-root-test-')); + }); + + afterEach(() => { + fs.rmSync(projectRoot, { recursive: true, force: true }); + }); + + test('returns startDir when no .planning/ exists anywhere', () => { + const subDir = path.join(projectRoot, 'backend'); + fs.mkdirSync(subDir); + assert.strictEqual(findProjectRoot(subDir), subDir); + }); + + test('returns startDir when .planning/ is in startDir itself', () => { + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + assert.strictEqual(findProjectRoot(projectRoot), projectRoot); + }); + + test('walks up to parent with .planning/ and sub_repos config listing this dir', () => { + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + fs.writeFileSync( + path.join(projectRoot, '.planning', 'config.json'), + JSON.stringify({ sub_repos: ['backend', 'frontend'] }) + ); + + const backendDir = path.join(projectRoot, 'backend'); + fs.mkdirSync(backendDir); + + assert.strictEqual(findProjectRoot(backendDir), projectRoot); + }); + + test('walks up from nested sub-repo subdirectory', () => { + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + fs.writeFileSync( + path.join(projectRoot, '.planning', 'config.json'), + JSON.stringify({ sub_repos: ['backend', 'frontend'] }) + ); + + const deepDir = path.join(projectRoot, 'backend', 'src', 'services'); + fs.mkdirSync(deepDir, { recursive: true }); + + assert.strictEqual(findProjectRoot(deepDir), projectRoot); + }); + + test('walks up via legacy multiRepo flag', () => { + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + fs.writeFileSync( + path.join(projectRoot, '.planning', 'config.json'), + JSON.stringify({ multiRepo: true }) + ); + + const backendDir = path.join(projectRoot, 'backend'); + fs.mkdirSync(path.join(backendDir, '.git'), { recursive: true }); + + assert.strictEqual(findProjectRoot(backendDir), projectRoot); + }); + + test('walks up via .git heuristic when no config exists', () => { + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + // No config.json at all + + const backendDir = path.join(projectRoot, 'backend'); + fs.mkdirSync(path.join(backendDir, '.git'), { recursive: true }); + + assert.strictEqual(findProjectRoot(backendDir), projectRoot); + }); + + test('does not walk up for dirs without .git when no sub_repos config', () => { + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + + const scriptsDir = path.join(projectRoot, 'scripts'); + fs.mkdirSync(scriptsDir); + + assert.strictEqual(findProjectRoot(scriptsDir), scriptsDir); + }); + + test('handles planning.sub_repos nested config format', () => { + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + fs.writeFileSync( + path.join(projectRoot, '.planning', 'config.json'), + JSON.stringify({ planning: { sub_repos: ['backend'] } }) + ); + + const backendDir = path.join(projectRoot, 'backend'); + fs.mkdirSync(backendDir); + + assert.strictEqual(findProjectRoot(backendDir), projectRoot); + }); + + test('returns startDir when sub_repos is empty and no .git', () => { + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + fs.writeFileSync( + path.join(projectRoot, '.planning', 'config.json'), + JSON.stringify({ sub_repos: [] }) + ); + + const backendDir = path.join(projectRoot, 'backend'); + fs.mkdirSync(backendDir); + + assert.strictEqual(findProjectRoot(backendDir), backendDir); + }); +}); diff --git a/tests/init.test.cjs b/tests/init.test.cjs index d36762860..740fca823 100644 --- a/tests/init.test.cjs +++ b/tests/init.test.cjs @@ -977,6 +977,70 @@ describe('cmdInitNewMilestone', () => { }); }); +// ───────────────────────────────────────────────────────────────────────────── +// findProjectRoot integration — gsd-tools resolves project root from sub-repo +// ───────────────────────────────────────────────────────────────────────────── + +describe('findProjectRoot integration via --cwd', () => { + let projectRoot; + + beforeEach(() => { + projectRoot = createTempProject(); + // Add ROADMAP.md so init quick doesn't error + fs.writeFileSync( + path.join(projectRoot, '.planning', 'ROADMAP.md'), + '# Roadmap\n\n## Phase 1: Foundation\n**Goal:** Setup\n' + ); + // Write sub_repos config + fs.writeFileSync( + path.join(projectRoot, '.planning', 'config.json'), + JSON.stringify({ sub_repos: ['backend', 'frontend'] }) + ); + // Create sub-repo directory + fs.mkdirSync(path.join(projectRoot, 'backend')); + }); + + afterEach(() => { + cleanup(projectRoot); + }); + + test('init quick from sub-repo CWD returns project_root pointing to parent', () => { + const backendDir = path.join(projectRoot, 'backend'); + const result = runGsdTools(['init', 'quick', 'test task', '--cwd', backendDir]); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.ok('project_root' in output, 'Should have project_root'); + assert.strictEqual(output.project_root, projectRoot, 'project_root should be the parent, not the sub-repo'); + assert.ok(output.roadmap_exists, 'Should find ROADMAP.md at project root'); + }); + + test('init quick from project root returns project_root as-is', () => { + const result = runGsdTools(['init', 'quick', 'test task', '--cwd', projectRoot]); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.project_root, projectRoot); + }); + + test('state load from sub-repo CWD reads project root config', () => { + // Write STATE.md at project root + fs.writeFileSync( + path.join(projectRoot, '.planning', 'STATE.md'), + '---\ncurrent_phase: 1\nphase_name: Foundation\n---\n# State\n' + ); + + const backendDir = path.join(projectRoot, 'backend'); + const result = runGsdTools(['state', '--cwd', backendDir]); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + // Should find config from project root, not from backend/ + assert.deepStrictEqual(output.config.sub_repos, ['backend', 'frontend'], + 'Should read sub_repos from project root config'); + }); +}); + // ───────────────────────────────────────────────────────────────────────────── // roadmap analyze command // ───────────────────────────────────────────────────────────────────────────── From d0aae7b63c5b2e239acf59b0be649b8b8bea8901 Mon Sep 17 00:00:00 2001 From: Srinivas Koduri Date: Wed, 18 Mar 2026 20:06:49 -0700 Subject: [PATCH 80/83] =?UTF-8?q?fix:=20address=20PR=20review=20=E2=80=94?= =?UTF-8?q?=20nested=20path=20resolution,=20sub=5Frepos=20in=20init,=20dep?= =?UTF-8?q?th-1=20detection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - findProjectRoot: use isInsideGitRepo() to walk up and find .git in ancestor dirs, fixing nested paths like backend/src/modules/ - Add sub_repos to cmdInitExecutePhase output so execute-plan.md and gsd-executor.md can route commits correctly - Align new-project.md sub-repo detection to maxdepth 1 matching detectSubRepos() behavior - Add 3 nested path tests for .git heuristic, sub_repos, and multiRepo Co-Authored-By: Claude Opus 4.6 (1M context) --- get-shit-done/bin/lib/core.cjs | 19 ++++++++--- get-shit-done/bin/lib/init.cjs | 1 + get-shit-done/workflows/new-project.md | 4 +-- tests/core.test.cjs | 47 ++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 6 deletions(-) diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index 3101f02e5..edcf95e3c 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -57,7 +57,18 @@ function findProjectRoot(startDir) { const resolved = path.resolve(startDir); const root = path.parse(resolved).root; const homedir = require('os').homedir(); - const startHasGit = fs.existsSync(path.join(resolved, '.git')); + + // Check if startDir or any of its ancestors (up to but not including a + // candidate project root) contains a .git directory. This handles both + // `backend/` (direct sub-repo) and `backend/src/modules/` (nested inside). + function isInsideGitRepo(candidateParent) { + let d = resolved; + while (d !== candidateParent && d !== root) { + if (fs.existsSync(path.join(d, '.git'))) return true; + d = path.dirname(d); + } + return false; + } let dir = resolved; while (dir !== root) { @@ -82,15 +93,15 @@ function findProjectRoot(startDir) { } // Check legacy multiRepo flag - if (config.multiRepo === true && startHasGit) { + if (config.multiRepo === true && isInsideGitRepo(parent)) { return parent; } } catch { // config.json missing or malformed — fall back to .git heuristic } - // Heuristic: parent has .planning/ and startDir has its own .git - if (startHasGit) { + // Heuristic: parent has .planning/ and we're inside a git repo + if (isInsideGitRepo(parent)) { return parent; } } diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index b19d3d1d8..6083dd908 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -57,6 +57,7 @@ function cmdInitExecutePhase(cwd, phase, raw) { // Config flags commit_docs: config.commit_docs, + sub_repos: config.sub_repos, parallelization: config.parallelization, context_window: config.context_window, branching_strategy: config.branching_strategy, diff --git a/get-shit-done/workflows/new-project.md b/get-shit-done/workflows/new-project.md index b5cb42482..9f1bd89d2 100644 --- a/get-shit-done/workflows/new-project.md +++ b/get-shit-done/workflows/new-project.md @@ -535,12 +535,12 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "chore: add project Check for directories with their own `.git` folders (separate repos within the workspace): ```bash -find . -maxdepth 2 -type d -name ".git" -not -path "./.git" +find . -maxdepth 1 -type d -not -name ".*" -not -name "node_modules" -exec test -d "{}/.git" \; -print ``` **If sub-repos found:** -Strip the `/.git` suffix and `./` prefix to get directory names (e.g., `./backend/.git` → `backend`). +Strip the `./` prefix to get directory names (e.g., `./backend` → `backend`). Use AskUserQuestion: - header: "Multi-Repo Workspace" diff --git a/tests/core.test.cjs b/tests/core.test.cjs index eca98f1f4..77fb46956 100644 --- a/tests/core.test.cjs +++ b/tests/core.test.cjs @@ -1167,6 +1167,53 @@ describe('findProjectRoot', () => { assert.strictEqual(findProjectRoot(backendDir), projectRoot); }); + test('walks up from nested path inside sub-repo via .git heuristic', () => { + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + + // Sub-repo with .git at its root + const backendDir = path.join(projectRoot, 'backend'); + fs.mkdirSync(path.join(backendDir, '.git'), { recursive: true }); + + // Nested path deep inside the sub-repo + const nestedDir = path.join(backendDir, 'src', 'modules', 'auth'); + fs.mkdirSync(nestedDir, { recursive: true }); + + // isInsideGitRepo walks up and finds backend/.git + assert.strictEqual(findProjectRoot(nestedDir), projectRoot); + }); + + test('walks up from nested path inside sub-repo via sub_repos config', () => { + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + fs.writeFileSync( + path.join(projectRoot, '.planning', 'config.json'), + JSON.stringify({ sub_repos: ['backend'] }) + ); + + // Nested path deep inside the sub-repo + const nestedDir = path.join(projectRoot, 'backend', 'src', 'modules'); + fs.mkdirSync(nestedDir, { recursive: true }); + + // With sub_repos config, it checks topSegment of relative path + assert.strictEqual(findProjectRoot(nestedDir), projectRoot); + }); + + test('walks up from nested path via legacy multiRepo flag', () => { + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + fs.writeFileSync( + path.join(projectRoot, '.planning', 'config.json'), + JSON.stringify({ multiRepo: true }) + ); + + const backendDir = path.join(projectRoot, 'backend'); + fs.mkdirSync(path.join(backendDir, '.git'), { recursive: true }); + + // Nested inside sub-repo — isInsideGitRepo walks up and finds backend/.git + const nestedDir = path.join(backendDir, 'src'); + fs.mkdirSync(nestedDir, { recursive: true }); + + assert.strictEqual(findProjectRoot(nestedDir), projectRoot); + }); + test('does not walk up for dirs without .git when no sub_repos config', () => { fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); From 99b239dbaf6df1d1c69488675684c8763193d8e7 Mon Sep 17 00:00:00 2001 From: Srinivas Koduri Date: Wed, 18 Mar 2026 22:45:11 -0700 Subject: [PATCH 81/83] fix(executor): record per-repo commit hashes in multi-repo mode The hash recording step used `git rev-parse --short HEAD` which fails when the project root is not a git repo (multi-repo workspaces). Update the protocol to extract hashes from commit-to-subrepo JSON output and record all sub-repo hashes in the SUMMARY. Co-Authored-By: Claude Opus 4.6 (1M context) --- agents/gsd-executor.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/agents/gsd-executor.md b/agents/gsd-executor.md index fa3f8c5e1..9b818becd 100644 --- a/agents/gsd-executor.md +++ b/agents/gsd-executor.md @@ -344,7 +344,9 @@ git commit -m "{type}({phase}-{plan}): {concise task description} " ``` -**5. Record hash:** `TASK_COMMIT=$(git rev-parse --short HEAD)` — track for SUMMARY. +**5. Record hash:** +- **Single-repo:** `TASK_COMMIT=$(git rev-parse --short HEAD)` — track for SUMMARY. +- **Multi-repo (sub_repos):** Extract hashes from `commit-to-subrepo` JSON output (`repos.{name}.hash`). Record all hashes for SUMMARY (e.g., `backend@abc1234, frontend@def5678`). **6. Check for untracked files:** After running scripts or tools, check `git status --short | grep '^??'`. For any new untracked files: commit if intentional, add to `.gitignore` if generated/runtime output. Never leave generated files untracked. From fd0d5464842d5266be56bb0c85a25c048310e7cb Mon Sep 17 00:00:00 2001 From: Srinivas Koduri Date: Wed, 18 Mar 2026 22:45:19 -0700 Subject: [PATCH 82/83] fix(new-project): remove invalid commit step for multi-repo config The workflow set commit_docs to false for multi-repo workspaces then immediately ran gsd-tools commit on config.json, which would be skipped. Replace with a note that config changes are local-only. Co-Authored-By: Claude Opus 4.6 (1M context) --- get-shit-done/workflows/new-project.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/get-shit-done/workflows/new-project.md b/get-shit-done/workflows/new-project.md index 9f1bd89d2..5b5d6ad5a 100644 --- a/get-shit-done/workflows/new-project.md +++ b/get-shit-done/workflows/new-project.md @@ -554,10 +554,7 @@ Use AskUserQuestion: - Auto-set `planning.commit_docs` to `false` (planning docs stay local in multi-repo workspaces) - Add `.planning/` to `.gitignore` if not already present -Update the config file: -```bash -node ~/.claude/get-shit-done/bin/gsd-tools.cjs commit "chore: configure multi-repo workspace" --files .planning/config.json -``` +Config changes are saved locally — no commit needed since `commit_docs` is `false` in multi-repo mode. **If no sub-repos found or user selects none:** Continue with no changes to config. From 32c6d880bf2db6f2f9e92ac20b6c00ad7306eeb6 Mon Sep 17 00:00:00 2001 From: Srinivas Koduri Date: Thu, 19 Mar 2026 10:05:50 -0700 Subject: [PATCH 83/83] =?UTF-8?q?fix:=20address=20maintainer=20review=20?= =?UTF-8?q?=E2=80=94=20gate=20findProjectRoot,=20warn=20on=20unmatched=20f?= =?UTF-8?q?iles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Gate findProjectRoot to commands that access .planning/ — skip for pure-utility commands (generate-slug, current-timestamp, template, frontmatter, verify-path-exists, verify-summary) to avoid unnecessary filesystem traversal on every invocation. 2. Warn to stderr when commit-to-subrepo encounters files that don't match any configured sub-repo prefix. 3. Document that loadConfig auto-syncs sub_repos with the filesystem, so config.json may be rewritten when repos are added or removed. Co-Authored-By: Claude Opus 4.6 (1M context) --- get-shit-done/bin/gsd-tools.cjs | 15 +++++++++++---- get-shit-done/bin/lib/commands.cjs | 4 ++++ get-shit-done/references/git-integration.md | 2 +- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index a7cd70377..4ee194ec1 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -181,10 +181,6 @@ async function main() { cwd = worktreeRoot; } - // Multi-repo guard: if CWD is inside a sub-repo, walk up to the project root - // so .planning/ is read/written at the correct level. - cwd = findProjectRoot(cwd); - const rawIndex = args.indexOf('--raw'); const raw = rawIndex !== -1; if (rawIndex !== -1) args.splice(rawIndex, 1); @@ -195,6 +191,17 @@ async function main() { error('Usage: gsd-tools [args] [--raw] [--cwd ]\nCommands: state, resolve-model, find-phase, commit, verify-summary, verify, frontmatter, template, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, init'); } + // Multi-repo guard: resolve project root for commands that read/write .planning/. + // Skip for pure-utility commands that don't touch .planning/ to avoid unnecessary + // filesystem traversal on every invocation. + const SKIP_ROOT_RESOLUTION = new Set([ + 'generate-slug', 'current-timestamp', 'verify-path-exists', + 'verify-summary', 'template', 'frontmatter', + ]); + if (!SKIP_ROOT_RESOLUTION.has(command)) { + cwd = findProjectRoot(cwd); + } + switch (command) { case 'state': { const subcommand = args[1]; diff --git a/get-shit-done/bin/lib/commands.cjs b/get-shit-done/bin/lib/commands.cjs index 27461c393..f0d95a3a0 100644 --- a/get-shit-done/bin/lib/commands.cjs +++ b/get-shit-done/bin/lib/commands.cjs @@ -298,6 +298,10 @@ function cmdCommitToSubrepo(cwd, message, files, raw) { } } + if (unmatched.length > 0) { + process.stderr.write(`Warning: ${unmatched.length} file(s) did not match any sub-repo prefix: ${unmatched.join(', ')}\n`); + } + const repos = {}; for (const [repo, repoFiles] of Object.entries(grouped)) { const repoCwd = path.join(cwd, repo); diff --git a/get-shit-done/references/git-integration.md b/get-shit-done/references/git-integration.md index 2b6a9b733..ef530533d 100644 --- a/get-shit-done/references/git-integration.md +++ b/get-shit-done/references/git-integration.md @@ -274,7 +274,7 @@ Set `commit_docs: false` so planning docs stay local and are not committed to an ### How It Works -1. **Auto-detection:** During `/gsd:new-project`, directories with their own `.git` folder are detected and offered for selection as sub-repos. +1. **Auto-detection:** During `/gsd:new-project`, directories with their own `.git` folder are detected and offered for selection as sub-repos. On subsequent runs, `loadConfig` auto-syncs the `sub_repos` list with the filesystem — adding newly created repos and removing deleted ones. This means `config.json` may be rewritten automatically when repos change on disk. 2. **File grouping:** Code files are grouped by their sub-repo prefix (e.g., `backend/src/api/users.ts` belongs to the `backend/` repo). 3. **Independent commits:** Each sub-repo receives its own atomic commit via `gsd-tools.cjs commit-to-subrepo`. File paths are made relative to the sub-repo root before staging. 4. **Planning stays local:** The `.planning/` directory is not committed; it acts as cross-repo coordination.