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) <noreply@anthropic.com>
This commit is contained in:
parent
109f8e4461
commit
ea8976d16e
5 changed files with 240 additions and 1 deletions
|
|
@ -15,7 +15,7 @@ export interface GsdCommandDefinition {
|
|||
type CompletionMap = Record<string, readonly GsdCommandDefinition[]>;
|
||||
|
||||
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 <server>)" },
|
||||
{ cmd: "rethink", desc: "Conversational project reorganization — reorder, park, discard, add milestones" },
|
||||
{ cmd: "workflow", desc: "Custom workflow lifecycle (new, run, list, validate, pause, resume)" },
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export function showHelp(ctx: ExtensionCommandContext): void {
|
|||
" /gsd triage Classify and route pending captures",
|
||||
" /gsd skip <unit> 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",
|
||||
"",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
78
src/resources/extensions/gsd/prompts/rethink.md
Normal file
78
src/resources/extensions/gsd/prompts/rethink.md
Normal file
|
|
@ -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": "<ISO timestamp>" }
|
||||
```
|
||||
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: <ISO timestamp>
|
||||
reason: "<reason>"
|
||||
---
|
||||
|
||||
# {ID} — Parked
|
||||
|
||||
> <reason>
|
||||
```
|
||||
**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
|
||||
154
src/resources/extensions/gsd/rethink.ts
Normal file
154
src/resources/extensions/gsd/rethink.ts
Normal file
|
|
@ -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<void> {
|
||||
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<ReturnType<typeof deriveState>>,
|
||||
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<string, string[]>();
|
||||
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");
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue