Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions commands/gsd/manager.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
name: gsd:manager
description: Interactive command center for managing multiple phases from one terminal
allowed-tools:
- Read
- Write
- Bash
- Glob
- Grep
- AskUserQuestion
- Task
---
<objective>
Single-terminal command center for managing a milestone. Shows a dashboard of all phases with visual status indicators, recommends optimal next actions, and dispatches work — discuss runs inline, plan/execute run as background agents.

Designed for power users who want to parallelize work across phases from one terminal: discuss a phase while another plans or executes in the background.

**Creates/Updates:**
- No files created directly — dispatches to existing GSD commands via Skill() and background Task agents.
- Reads `.planning/STATE.md`, `.planning/ROADMAP.md`, phase directories for status.

**After:** User exits when done managing, or all phases complete and milestone lifecycle is suggested.
</objective>

<execution_context>
@~/.claude/get-shit-done/workflows/manager.md
@~/.claude/get-shit-done/references/ui-brand.md
</execution_context>

<context>
No arguments required. Requires an active milestone with ROADMAP.md and STATE.md.

Project context, phase list, dependencies, and recommendations are resolved inside the workflow using `gsd-tools.cjs init manager`. No upfront context loading needed.
</context>

<process>
Execute the manager workflow from @~/.claude/get-shit-done/workflows/manager.md end-to-end.
Maintain the dashboard refresh loop until the user exits or all phases complete.
</process>
5 changes: 4 additions & 1 deletion get-shit-done/bin/gsd-tools.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,9 @@ async function main() {
case 'progress':
init.cmdInitProgress(cwd, raw);
break;
case 'manager':
init.cmdInitManager(cwd, raw);
break;
case 'new-workspace':
init.cmdInitNewWorkspace(cwd, raw);
break;
Expand All @@ -661,7 +664,7 @@ async function main() {
init.cmdInitRemoveWorkspace(cwd, args[2], raw);
break;
default:
error(`Unknown init workflow: ${workflow}\nAvailable: execute-phase, plan-phase, new-project, new-milestone, quick, resume, verify-work, phase-op, todos, milestone-op, map-codebase, progress, new-workspace, list-workspaces, remove-workspace`);
error(`Unknown init workflow: ${workflow}\nAvailable: execute-phase, plan-phase, new-project, new-milestone, quick, resume, verify-work, phase-op, todos, milestone-op, map-codebase, progress, manager, new-workspace, list-workspaces, remove-workspace`);
}
break;
}
Expand Down
250 changes: 249 additions & 1 deletion get-shit-done/bin/lib/init.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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, extractCurrentMilestone, normalizePhaseName, toPosixPath, output, error } = require('./core.cjs');
const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, normalizePhaseName, planningPaths, toPosixPath, output, error } = require('./core.cjs');

function getLatestCompletedMilestone(cwd) {
const milestonesPath = path.join(cwd, '.planning', 'MILESTONES.md');
Expand Down Expand Up @@ -761,6 +761,253 @@ function cmdInitMapCodebase(cwd, raw) {
output(withProjectRoot(cwd, result), raw);
}

function cmdInitManager(cwd, raw) {
const config = loadConfig(cwd);
const milestone = getMilestoneInfo(cwd);

// Use planningPaths for forward-compatibility with workstream scoping (#1268)
const paths = planningPaths(cwd);

// Validate prerequisites
if (!fs.existsSync(paths.roadmap)) {
error('No ROADMAP.md found. Run /gsd:new-milestone first.');
}
if (!fs.existsSync(paths.state)) {
error('No STATE.md found. Run /gsd:new-milestone first.');
}
const rawContent = fs.readFileSync(paths.roadmap, 'utf-8');
const content = extractCurrentMilestone(rawContent, cwd);
const phasesDir = paths.phases;
const isDirInMilestone = getMilestonePhaseFilter(cwd);

const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
const phases = [];
let match;

while ((match = phasePattern.exec(content)) !== null) {
const phaseNum = match[1];
const phaseName = match[2].replace(/\(INSERTED\)/i, '').trim();

const sectionStart = match.index;
const restOfContent = content.slice(sectionStart);
const nextHeader = restOfContent.match(/\n#{2,4}\s+Phase\s+\d/i);
const sectionEnd = nextHeader ? sectionStart + nextHeader.index : content.length;
const section = content.slice(sectionStart, sectionEnd);

const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
const goal = goalMatch ? goalMatch[1].trim() : null;

const dependsMatch = section.match(/\*\*Depends on(?::\*\*|\*\*:)\s*([^\n]+)/i);
const depends_on = dependsMatch ? dependsMatch[1].trim() : null;

const normalized = normalizePhaseName(phaseNum);
let diskStatus = 'no_directory';
let planCount = 0;
let summaryCount = 0;
let hasContext = false;
let hasResearch = false;
let lastActivity = null;
let isActive = false;

try {
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).filter(isDirInMilestone);
const dirMatch = dirs.find(d => d.startsWith(normalized + '-') || d === normalized);

if (dirMatch) {
const fullDir = path.join(phasesDir, dirMatch);
const phaseFiles = fs.readdirSync(fullDir);
planCount = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
summaryCount = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
hasContext = phaseFiles.some(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');

if (summaryCount >= planCount && planCount > 0) diskStatus = 'complete';
else if (summaryCount > 0) diskStatus = 'partial';
else if (planCount > 0) diskStatus = 'planned';
else if (hasResearch) diskStatus = 'researched';
else if (hasContext) diskStatus = 'discussed';
else diskStatus = 'empty';

// Activity detection: check most recent file mtime
const now = Date.now();
let newestMtime = 0;
for (const f of phaseFiles) {
try {
const stat = fs.statSync(path.join(fullDir, f));
if (stat.mtimeMs > newestMtime) newestMtime = stat.mtimeMs;
} catch { /* intentionally empty */ }
}
if (newestMtime > 0) {
lastActivity = new Date(newestMtime).toISOString();
isActive = (now - newestMtime) < 300000; // 5 minutes
}
}
} catch { /* intentionally empty */ }

// Check ROADMAP checkbox status
const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${phaseNum.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[:\\s]`, 'i');
const checkboxMatch = content.match(checkboxPattern);
const roadmapComplete = checkboxMatch ? checkboxMatch[1] === 'x' : false;
if (roadmapComplete && diskStatus !== 'complete') {
diskStatus = 'complete';
}

phases.push({
number: phaseNum,
name: phaseName,
goal,
depends_on,
disk_status: diskStatus,
has_context: hasContext,
has_research: hasResearch,
plan_count: planCount,
summary_count: summaryCount,
roadmap_complete: roadmapComplete,
last_activity: lastActivity,
is_active: isActive,
});
}

// Compute display names: truncate to keep table aligned
const MAX_NAME_WIDTH = 20;
for (const phase of phases) {
if (phase.name.length > MAX_NAME_WIDTH) {
phase.display_name = phase.name.slice(0, MAX_NAME_WIDTH - 1) + '…';
} else {
phase.display_name = phase.name;
}
}

// Dependency satisfaction: check if all depends_on phases are complete
const completedNums = new Set(phases.filter(p => p.disk_status === 'complete').map(p => p.number));
for (const phase of phases) {
if (!phase.depends_on || /^none$/i.test(phase.depends_on.trim())) {
phase.deps_satisfied = true;
} else {
// Parse "Phase 1, Phase 3" or "1, 3" formats
const depNums = phase.depends_on.match(/\d+(?:\.\d+)*/g) || [];
phase.deps_satisfied = depNums.every(n => completedNums.has(n));
phase.dep_phases = depNums;
}
}

// Compact dependency display for dashboard
for (const phase of phases) {
phase.deps_display = (phase.dep_phases && phase.dep_phases.length > 0)
? phase.dep_phases.join(',')
: '—';
}

// Sliding window: discuss is sequential — only the first undiscussed phase is available
let foundNextToDiscuss = false;
for (const phase of phases) {
if (!foundNextToDiscuss && (phase.disk_status === 'empty' || phase.disk_status === 'no_directory')) {
phase.is_next_to_discuss = true;
foundNextToDiscuss = true;
} else {
phase.is_next_to_discuss = false;
}
}

// Check for WAITING.json signal
let waitingSignal = null;
try {
const waitingPath = path.join(cwd, '.planning', 'WAITING.json');
if (fs.existsSync(waitingPath)) {
waitingSignal = JSON.parse(fs.readFileSync(waitingPath, 'utf-8'));
}
} catch { /* intentionally empty */ }

// Compute recommended actions (execute > plan > discuss)
const recommendedActions = [];
for (const phase of phases) {
if (phase.disk_status === 'complete') continue;

if (phase.disk_status === 'planned' && phase.deps_satisfied) {
recommendedActions.push({
phase: phase.number,
phase_name: phase.name,
action: 'execute',
reason: `${phase.plan_count} plans ready, dependencies met`,
command: `/gsd:execute-phase ${phase.number}`,
});
} else if (phase.disk_status === 'discussed' || phase.disk_status === 'researched') {
recommendedActions.push({
phase: phase.number,
phase_name: phase.name,
action: 'plan',
reason: 'Context gathered, ready for planning',
command: `/gsd:plan-phase ${phase.number}`,
});
} else if ((phase.disk_status === 'empty' || phase.disk_status === 'no_directory') && phase.is_next_to_discuss) {
recommendedActions.push({
phase: phase.number,
phase_name: phase.name,
action: 'discuss',
reason: 'Unblocked, ready to gather context',
command: `/gsd:discuss-phase ${phase.number}`,
});
}
}

// Filter recommendations: no parallel execute/plan unless phases are independent
// Two phases are "independent" if neither depends on the other (directly or transitively)
const phaseMap = new Map(phases.map(p => [p.number, p]));

function reaches(from, to, visited = new Set()) {
if (visited.has(from)) return false;
visited.add(from);
const p = phaseMap.get(from);
if (!p || !p.dep_phases || p.dep_phases.length === 0) return false;
if (p.dep_phases.includes(to)) return true;
return p.dep_phases.some(dep => reaches(dep, to, visited));
}

function hasDepRelationship(numA, numB) {
return reaches(numA, numB) || reaches(numB, numA);
}

// Detect phases with active work (file modified in last 5 min)
const activeExecuting = phases.filter(p =>
p.disk_status === 'partial' ||
(p.disk_status === 'planned' && p.is_active)
);
const activePlanning = phases.filter(p =>
p.is_active && (p.disk_status === 'discussed' || p.disk_status === 'researched')
);

const filteredActions = recommendedActions.filter(action => {
if (action.action === 'execute' && activeExecuting.length > 0) {
// Only allow if independent of ALL actively-executing phases
return activeExecuting.every(active => !hasDepRelationship(action.phase, active.number));
}
if (action.action === 'plan' && activePlanning.length > 0) {
// Only allow if independent of ALL actively-planning phases
return activePlanning.every(active => !hasDepRelationship(action.phase, active.number));
}
return true;
});

const completedCount = phases.filter(p => p.disk_status === 'complete').length;
const result = {
milestone_version: milestone.version,
milestone_name: milestone.name,
phases,
phase_count: phases.length,
completed_count: completedCount,
in_progress_count: phases.filter(p => ['partial', 'planned', 'discussed', 'researched'].includes(p.disk_status)).length,
recommended_actions: filteredActions,
waiting_signal: waitingSignal,
all_complete: completedCount === phases.length && phases.length > 0,
project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
roadmap_exists: true,
state_exists: true,
};

output(withProjectRoot(cwd, result), raw);
}

function cmdInitProgress(cwd, raw) {
const config = loadConfig(cwd);
const milestone = getMilestoneInfo(cwd);
Expand Down Expand Up @@ -1080,6 +1327,7 @@ module.exports = {
cmdInitMilestoneOp,
cmdInitMapCodebase,
cmdInitProgress,
cmdInitManager,
cmdInitNewWorkspace,
cmdInitListWorkspaces,
cmdInitRemoveWorkspace,
Expand Down
Loading