From ea8976d16ef53e4fb3193deb8f758a89f8260263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=82CHES?= Date: Wed, 25 Mar 2026 00:04:24 -0600 Subject: [PATCH] feat(gsd): add `/gsd rethink` command for conversational project reorganization (#2459) Collects a snapshot of all milestones (status, dependencies, slice progress, queue order) and dispatches a prompt that turns Claude into a reorganization assistant. Supports reordering, parking, unparking, discarding, adding milestones, and updating dependencies through conversation. Co-authored-by: Claude Opus 4.6 (1M context) --- .../extensions/gsd/commands/catalog.ts | 3 +- .../extensions/gsd/commands/handlers/core.ts | 1 + .../extensions/gsd/commands/handlers/ops.ts | 5 + .../extensions/gsd/prompts/rethink.md | 78 +++++++++ src/resources/extensions/gsd/rethink.ts | 154 ++++++++++++++++++ 5 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 src/resources/extensions/gsd/prompts/rethink.md create mode 100644 src/resources/extensions/gsd/rethink.ts diff --git a/src/resources/extensions/gsd/commands/catalog.ts b/src/resources/extensions/gsd/commands/catalog.ts index 2c8d1224a..8045c85be 100644 --- a/src/resources/extensions/gsd/commands/catalog.ts +++ b/src/resources/extensions/gsd/commands/catalog.ts @@ -15,7 +15,7 @@ export interface GsdCommandDefinition { type CompletionMap = Record; export const GSD_COMMAND_DESCRIPTION = - "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|queue|quick|discuss|capture|triage|dispatch|history|undo|undo-task|reset-slice|rate|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|logs|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|park|unpark|init|setup|inspect|extensions|update|fast|mcp"; + "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|queue|quick|discuss|capture|triage|dispatch|history|undo|undo-task|reset-slice|rate|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|logs|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|park|unpark|init|setup|inspect|extensions|update|fast|mcp|rethink"; export const TOP_LEVEL_SUBCOMMANDS: readonly GsdCommandDefinition[] = [ { cmd: "help", desc: "Categorized command reference with descriptions" }, @@ -69,6 +69,7 @@ export const TOP_LEVEL_SUBCOMMANDS: readonly GsdCommandDefinition[] = [ { cmd: "extensions", desc: "Manage extensions (list, enable, disable, info)" }, { cmd: "fast", desc: "Toggle OpenAI service tier (on/off/flex/status)" }, { cmd: "mcp", desc: "MCP server status and connectivity check (status, check )" }, + { cmd: "rethink", desc: "Conversational project reorganization — reorder, park, discard, add milestones" }, { cmd: "workflow", desc: "Custom workflow lifecycle (new, run, list, validate, pause, resume)" }, ]; diff --git a/src/resources/extensions/gsd/commands/handlers/core.ts b/src/resources/extensions/gsd/commands/handlers/core.ts index c37def77c..c915f0486 100644 --- a/src/resources/extensions/gsd/commands/handlers/core.ts +++ b/src/resources/extensions/gsd/commands/handlers/core.ts @@ -36,6 +36,7 @@ export function showHelp(ctx: ExtensionCommandContext): void { " /gsd triage Classify and route pending captures", " /gsd skip Prevent a unit from auto-mode dispatch", " /gsd undo Revert last completed unit [--force]", + " /gsd rethink Conversational project reorganization — reorder, park, discard, add milestones", " /gsd park [id] Park a milestone — skip without deleting [reason]", " /gsd unpark [id] Reactivate a parked milestone", "", diff --git a/src/resources/extensions/gsd/commands/handlers/ops.ts b/src/resources/extensions/gsd/commands/handlers/ops.ts index d632a2ad9..a1996dfef 100644 --- a/src/resources/extensions/gsd/commands/handlers/ops.ts +++ b/src/resources/extensions/gsd/commands/handlers/ops.ts @@ -201,5 +201,10 @@ Examples: await handleExtensions(trimmed.replace(/^extensions\s*/, "").trim(), ctx); return true; } + if (trimmed === "rethink") { + const { handleRethink } = await import("../../rethink.js"); + await handleRethink(trimmed, ctx, pi); + return true; + } return false; } diff --git a/src/resources/extensions/gsd/prompts/rethink.md b/src/resources/extensions/gsd/prompts/rethink.md new file mode 100644 index 000000000..b79484726 --- /dev/null +++ b/src/resources/extensions/gsd/prompts/rethink.md @@ -0,0 +1,78 @@ +You are a project reorganization assistant for a GSD (Get Shit Done) project. The user wants to rethink their milestone plan — reorder priorities, remove work that's no longer needed, add new milestones, or restructure dependencies. + +## Current Milestone Landscape + +{{rethinkData}} + +## Detailed Milestone Context + +{{existingMilestonesContext}} + +## Your Role + +1. Present the current milestone order as a clear numbered list with status indicators (e.g. ✅ complete, ▶ active, ⏳ pending, ⏸ parked) +2. Ask: **"What would you like to change?"** +3. Execute changes conversationally, confirming destructive operations before proceeding + +## Supported Operations + +### Reorder milestones +Change execution order of pending/active milestones. Write `.gsd/QUEUE-ORDER.json`: +```json +{ "order": ["M003", "M001", "M002"], "updatedAt": "" } +``` +Only include non-complete milestone IDs. Validate dependency constraints before saving. + +### Park a milestone +Temporarily shelve a milestone (reversible). Create a `{ID}-PARKED.md` file in the milestone directory: +```markdown +--- +parked_at: +reason: "" +--- + +# {ID} — Parked + +> +``` +**Bias toward parking over discarding** when a milestone has any completed slices or tasks. + +### Unpark a milestone +Remove the `{ID}-PARKED.md` file from the milestone directory to reactivate it. + +### Discard a milestone +**Permanently** delete a milestone directory and prune it from QUEUE-ORDER.json. **Always confirm with the user before discarding.** Warn explicitly if the milestone has completed work. + +### Add a new milestone +Use the `gsd_milestone_generate_id` tool to get the next ID, then write a `{ID}-CONTEXT.md` file in `.gsd/milestones/{ID}/` with scope, goals, and success criteria. Update QUEUE-ORDER.json to place it at the desired position. + +### Update dependencies +Edit `depends_on` in the YAML frontmatter of a milestone's `{ID}-CONTEXT.md` file. For example: +```yaml +depends_on: [M001, M003] +``` + +## Dependency Validation Rules + +Before applying any reorder, verify: +- A milestone **cannot** be scheduled before any milestone in its `depends_on` list (would_block) +- Circular dependencies are forbidden +- Dependencies on non-existent milestones are invalid (missing_dep) +- Completed milestones always satisfy dependencies regardless of position + +If a proposed order would violate constraints, explain the issue and suggest alternatives (e.g. removing the dependency, reordering differently, or parking the blocker). + +## After Each Change + +1. Execute the change (write/delete files, update QUEUE-ORDER.json) +2. Show the updated milestone order +3. Note if the active milestone changed as a result +4. Ask if there's anything else to adjust + +## Important Constraints + +- Do NOT modify completed milestones — they're done +- Do NOT park completed milestones — it would corrupt dependency satisfaction +- Park is preferred over discard when a milestone has any completed work +- Always persist queue order changes to `.gsd/QUEUE-ORDER.json` +- After changes, run `git add .gsd/ && git commit -m "docs: rethink milestone order"` to persist diff --git a/src/resources/extensions/gsd/rethink.ts b/src/resources/extensions/gsd/rethink.ts new file mode 100644 index 000000000..a6f049b77 --- /dev/null +++ b/src/resources/extensions/gsd/rethink.ts @@ -0,0 +1,154 @@ +/** + * GSD Rethink — Conversational project reorganization. + * + * Collects a snapshot of all milestones (status, dependencies, slice progress, + * queue order) and dispatches a prompt that turns Claude into a reorganization + * assistant. Claude can then reorder, park, unpark, discard, or add milestones + * through conversation. + */ + +import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import { existsSync } from "node:fs"; + +import { isAutoActive } from "./auto.js"; +import { deriveState } from "./state.js"; +import { gsdRoot } from "./paths.js"; +import { findMilestoneIds } from "./milestone-ids.js"; +import { loadQueueOrder, validateQueueOrder } from "./queue-order.js"; +import { isParked, getParkedReason } from "./milestone-actions.js"; +import { getMilestoneSlices, isDbAvailable } from "./gsd-db.js"; +import { buildExistingMilestonesContext } from "./guided-flow-queue.js"; +import { loadPrompt } from "./prompt-loader.js"; + +// ─── Entry Point ────────────────────────────────────────────────────────────── + +export async function handleRethink( + _args: string, + ctx: ExtensionCommandContext, + pi: ExtensionAPI, +): Promise { + if (isAutoActive()) { + ctx.ui.notify("Cannot rethink while auto-mode is active. Stop auto-mode first.", "error"); + return; + } + + const basePath = process.cwd(); + const root = gsdRoot(basePath); + if (!existsSync(root)) { + ctx.ui.notify("No GSD project found. Run /gsd init first.", "warning"); + return; + } + + ctx.ui.notify("Building project snapshot for rethink...", "info"); + + const state = await deriveState(basePath); + const milestoneIds = findMilestoneIds(basePath); + + if (milestoneIds.length === 0) { + ctx.ui.notify("No milestones exist yet. Nothing to rethink.", "warning"); + return; + } + + const queueOrder = loadQueueOrder(basePath); + const rethinkData = buildRethinkData(basePath, milestoneIds, state, queueOrder); + const existingMilestonesContext = await buildExistingMilestonesContext(basePath, milestoneIds, state); + + const content = loadPrompt("rethink", { + rethinkData, + existingMilestonesContext, + }); + + pi.sendMessage( + { customType: "gsd-rethink", content, display: false }, + { triggerTurn: true }, + ); +} + +// ─── Data Builder ───────────────────────────────────────────────────────────── + +function buildRethinkData( + basePath: string, + milestoneIds: string[], + state: Awaited>, + queueOrder: string[] | null, +): string { + const lines: string[] = []; + const dbAvailable = isDbAvailable(); + + // ── Summary stats ─────────────────────────────────────────────────── + const counts = { complete: 0, active: 0, pending: 0, parked: 0 }; + for (const entry of state.registry) { + if (entry.status in counts) counts[entry.status as keyof typeof counts]++; + } + + lines.push("### Summary"); + lines.push(`${counts.complete} complete, ${counts.active} active, ${counts.pending} pending, ${counts.parked} parked — ${milestoneIds.length} total`); + lines.push(`Queue order source: ${queueOrder ? "explicit QUEUE-ORDER.json" : "default numeric (by ID)"}`); + if (state.activeMilestone) { + lines.push(`Active milestone: ${state.activeMilestone}`); + } + lines.push(""); + + // ── Milestone table ───────────────────────────────────────────────── + lines.push("### Execution Order"); + lines.push(""); + lines.push("| # | ID | Title | Status | Dependencies | Slices |"); + lines.push("|---|-----|-------|--------|--------------|--------|"); + + for (let i = 0; i < milestoneIds.length; i++) { + const mid = milestoneIds[i]; + const entry = state.registry.find(m => m.id === mid); + const title = entry?.title ?? mid; + const status = entry?.status ?? "unknown"; + const deps = entry?.dependsOn?.length ? entry.dependsOn.join(", ") : "—"; + + let sliceInfo = "—"; + if (dbAvailable && status !== "complete") { + const slices = getMilestoneSlices(mid); + if (slices.length > 0) { + const done = slices.filter(s => s.status === "complete").length; + sliceInfo = `${done}/${slices.length} complete`; + } + } + + // Add parked reason if applicable + let statusDisplay = status; + if (status === "parked") { + const reason = getParkedReason(basePath, mid); + if (reason) statusDisplay = `parked (${reason})`; + } + + lines.push(`| ${i + 1} | ${mid} | ${title} | ${statusDisplay} | ${deps} | ${sliceInfo} |`); + } + + // ── Dependency validation ─────────────────────────────────────────── + const pendingIds = milestoneIds.filter(mid => { + const entry = state.registry.find(m => m.id === mid); + return entry?.status !== "complete"; + }); + + const completedIds = new Set( + state.registry.filter(m => m.status === "complete").map(m => m.id), + ); + + const depsMap = new Map(); + for (const entry of state.registry) { + if (entry.dependsOn?.length) { + depsMap.set(entry.id, entry.dependsOn); + } + } + + if (pendingIds.length > 0 && depsMap.size > 0) { + const validation = validateQueueOrder(pendingIds, depsMap, completedIds); + + if (validation.violations.length > 0) { + lines.push(""); + lines.push("### Dependency Issues"); + for (const v of validation.violations) { + lines.push(`- **${v.type}**: ${v.message}`); + } + } + } + + return lines.join("\n"); +}