"use strict"; /** * Init — Compound init commands for workflow bootstrapping * * ADR-457 build-at-publish: the hand-written bin/lib/init.cjs collapsed to * a TypeScript source of truth, compiled by tsc to a gitignored .cjs at the * same require() path. Behaviour preserved byte-for-behaviour; only types are added. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; const node_fs_1 = __importDefault(require("node:fs")); const node_path_1 = __importDefault(require("node:path")); const node_os_1 = __importDefault(require("node:os")); const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs"); // eslint-disable-next-line @typescript-eslint/no-require-imports -- io.cjs is an export= CommonJS module const io = require("./io.cjs"); // eslint-disable-next-line @typescript-eslint/no-require-imports -- config-loader.cjs is an export= CommonJS module const configLoader = require("./config-loader.cjs"); // eslint-disable-next-line @typescript-eslint/no-require-imports -- model-resolver.cjs is an export= CommonJS module const modelResolver = require("./model-resolver.cjs"); // eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-locator.cjs is an export= CommonJS module const phaseLocator = require("./phase-locator.cjs"); // eslint-disable-next-line @typescript-eslint/no-require-imports -- roadmap-parser.cjs is an export= CommonJS module const roadmapParser = require("./roadmap-parser.cjs"); // eslint-disable-next-line @typescript-eslint/no-require-imports -- core-utils.cjs is an export= CommonJS module const coreUtils = require("./core-utils.cjs"); // eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module const phaseId = require("./phase-id.cjs"); // eslint-disable-next-line @typescript-eslint/no-require-imports -- worktree-safety.cjs is an export= CommonJS module const worktreeSafety = require("./worktree-safety.cjs"); // eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module const planningWorkspace = require("./planning-workspace.cjs"); const secrets_cjs_1 = require("./secrets.cjs"); // eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-scan.cjs is an export= CommonJS module const scanPhasePlans = require("./plan-scan.cjs"); const state_document_cjs_1 = require("./state-document.cjs"); const runtime_slash_cjs_1 = require("./runtime-slash.cjs"); // eslint-disable-next-line @typescript-eslint/no-require-imports -- commands.cjs is an export= CommonJS module const commandsMod = require("./commands.cjs"); const security_cjs_1 = require("./security.cjs"); const runtime_homes_cjs_1 = require("./runtime-homes.cjs"); // eslint-disable-next-line @typescript-eslint/no-require-imports -- frontmatter.cjs is an export= CommonJS module const frontmatterMod = require("./frontmatter.cjs"); // eslint-disable-next-line @typescript-eslint/no-require-imports -- agent-install-check.cjs is an export= CommonJS module const agentInstallCheck = require("./agent-install-check.cjs"); const { checkAgentsInstalled } = agentInstallCheck; // eslint-disable-next-line @typescript-eslint/no-require-imports -- git-base-branch.cjs is an export= CommonJS module const gitBaseBranch = require("./git-base-branch.cjs"); const { gitWorktreeInfoInternal } = gitBaseBranch; const { output, error } = io; const { loadConfig } = configLoader; const { resolveModelInternal, resolveGranularityInternal, assertValidGranularityOverride } = modelResolver; const { findPhaseInternal } = phaseLocator; const { getRoadmapPhaseInternal, getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, } = roadmapParser; const { pathExistsInternal, generateSlugInternal, toPosixPath } = coreUtils; const { normalizePhaseName, phaseTokenMatches } = phaseId; const { pruneOrphanedWorktrees } = worktreeSafety; const { planningPaths, planningDir, planningRoot, findContextMdIn, } = planningWorkspace; const { determinePhaseStatus } = commandsMod; const { extractFrontmatter } = frontmatterMod; // Unused but imported for structural parity void stripShippedMilestones; // Accept all bold/colon variants of the Requirements header (#2769) const REQUIREMENTS_HEADER_RE = /^\*\*Requirements:?\*\*[^\S\n]*:?[^\S\n]*([^\n]*)$/m; function listPhaseSummaryFiles(phaseDir) { return scanPhasePlans(phaseDir)['summaryFiles']; } function listPhasePlanFiles(phaseDir) { return scanPhasePlans(phaseDir)['planFiles']; } function getLatestCompletedMilestone(cwd) { const milestonesPath = node_path_1.default.join(planningRoot(cwd), 'MILESTONES.md'); const content = (0, shell_command_projection_cjs_1.platformReadSync)(milestonesPath); if (content === null) return null; const match = content.match(/^##\s+(v[\d.]+)\s+(.+?)\s+\(Shipped:/m); if (!match) return null; return { version: match[1], name: match[2].trim(), }; } function withProjectRoot(cwd, result) { result['project_root'] = cwd; const activeRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd); const agentStatus = checkAgentsInstalled(activeRuntime); result['agents_installed'] = agentStatus.agents_installed; result['missing_agents'] = agentStatus.missing_agents; result['agents_dir'] = agentStatus.agents_dir; result['agent_runtime'] = agentStatus.agent_runtime; const config = loadConfig(cwd); if (config.response_language) { result['response_language'] = config.response_language; } if (config.project_code) { result['project_code'] = config.project_code; } const projectMdPath = node_path_1.default.join(planningDir(cwd), 'PROJECT.md'); const content = (0, shell_command_projection_cjs_1.platformReadSync)(projectMdPath); if (content) { const h1Match = content.match(/^#\s+(.+)$/m); if (h1Match) { result['project_title'] = h1Match[1].trim(); } } return result; } function getInitGitState(cwd) { const info = gitWorktreeInfoInternal(cwd); const worktreeRoot = info['worktreeRoot']; const normalizeForCompare = (p) => { if (typeof p !== 'string' || p.length === 0) return null; let resolved; try { resolved = node_fs_1.default.realpathSync.native(p); } catch { resolved = node_path_1.default.resolve(p); } resolved = node_path_1.default.resolve(resolved); if (process.platform === 'win32') { return resolved.replace(/\//g, '\\').toLowerCase(); } return resolved; }; let inNestedSubdir = false; if (info['inside']) { let resolvedByGitPrefix = false; try { const prefixResult = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '--show-prefix'], { cwd, timeout: 5000 }); if (prefixResult['exitCode'] === 0) { const prefix = (typeof prefixResult['stdout'] === 'string' ? prefixResult['stdout'] : '').trim().replace(/\\/g, '/'); inNestedSubdir = prefix.length > 0 && prefix !== '.' && prefix !== './'; resolvedByGitPrefix = true; } } catch { /* intentionally empty */ } if (!resolvedByGitPrefix) { const rootNorm = normalizeForCompare(worktreeRoot); const cwdNorm = normalizeForCompare(cwd); if (rootNorm && cwdNorm) { if (rootNorm === cwdNorm) { inNestedSubdir = false; } else { const rel = node_path_1.default.relative(rootNorm, cwdNorm); const relNorm = process.platform === 'win32' ? rel.replace(/\//g, '\\') : rel; inNestedSubdir = relNorm !== '' && relNorm !== '.' && !relNorm.startsWith('..') && !node_path_1.default.isAbsolute(relNorm); } } else { inNestedSubdir = worktreeRoot !== null; } } } if (inNestedSubdir && typeof worktreeRoot === 'string') { const toComparableRaw = (p) => p.replace(/\\/g, '/').replace(/\/+$/g, '').toLowerCase(); if (toComparableRaw(worktreeRoot) === toComparableRaw(String(cwd))) { inNestedSubdir = false; } } return { has_git: info['inside'], git_worktree_root: worktreeRoot, in_nested_subdir: inNestedSubdir, }; } function cmdInitExecutePhase(cwd, phase, raw, options = {}) { if (!phase) { error('phase required for init execute-phase'); } const config = loadConfig(cwd); let phaseInfo = findPhaseInternal(cwd, phase); const milestone = getMilestoneInfo(cwd); const roadmapPhase = getRoadmapPhaseInternal(cwd, phase); if (phaseInfo?.['archived'] && roadmapPhase?.['found']) { phaseInfo = null; } if (!phaseInfo && roadmapPhase?.['found']) { const phaseName = roadmapPhase['phase_name']; phaseInfo = { found: true, directory: null, phase_number: roadmapPhase['phase_number'], phase_name: phaseName, phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null, plans: [], summaries: [], incomplete_plans: [], has_research: false, has_context: false, has_verification: false, has_reviews: false, }; } const reqMatch = roadmapPhase?.['section']?.match(REQUIREMENTS_HEADER_RE); const reqExtracted = reqMatch ? reqMatch[1].replace(/[\[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean).join(', ') : null; const phase_req_ids = reqExtracted && reqExtracted !== 'TBD' ? reqExtracted : null; const wf = (config.workflow ?? {}); const result = { executor_model: resolveModelInternal(cwd, 'gsd-executor'), verifier_model: resolveModelInternal(cwd, 'gsd-verifier'), tdd_mode: options['tdd'] || Boolean(wf['tdd_mode']) || false, commit_docs: config.commit_docs, sub_repos: config.sub_repos, 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, verifier_enabled: config.verifier, phase_found: !!phaseInfo, phase_dir: phaseInfo?.['directory'] || null, phase_number: phaseInfo?.['phase_number'] || null, phase_name: phaseInfo?.['phase_name'] || null, phase_slug: phaseInfo?.['phase_slug'] || null, phase_req_ids, plans: phaseInfo?.['plans'] || [], summaries: phaseInfo?.['summaries'] || [], incomplete_plans: phaseInfo?.['incomplete_plans'] || [], plan_count: phaseInfo?.['plans']?.length || 0, incomplete_count: phaseInfo?.['incomplete_plans']?.length || 0, branch_name: config.branching_strategy === 'phase' && phaseInfo ? config.phase_branch_template .replace('{project}', config.project_code || '') .replace('{phase}', normalizePhaseName(phaseInfo['phase_number'])) .replace('{slug}', phaseInfo['phase_slug'] || 'phase') : config.branching_strategy === 'milestone' ? config.milestone_branch_template .replace('{milestone}', milestone['version']) .replace('{slug}', generateSlugInternal(milestone['name']) || 'milestone') : null, milestone_version: milestone['version'], milestone_name: milestone['name'], milestone_slug: generateSlugInternal(milestone['name']), state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')), roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')), config_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'config.json')), state_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'STATE.md'))), roadmap_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'ROADMAP.md'))), config_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'config.json'))), }; if (options['validate']) { try { const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md'); const stateContent = (0, shell_command_projection_cjs_1.platformReadSync)(statePath); if (stateContent !== null) { result['state_validation_ran'] = true; const stateWarnings = []; if (phaseInfo?.['directory'] && node_fs_1.default.existsSync(node_path_1.default.join(cwd, phaseInfo['directory']))) { const diskPlans = listPhasePlanFiles(node_path_1.default.join(cwd, phaseInfo['directory'])).length; const totalPlansRaw = (0, state_document_cjs_1.stateExtractField)(stateContent, 'Total Plans in Phase'); const totalPlansInPhase = totalPlansRaw ? parseInt(totalPlansRaw, 10) : null; if (totalPlansInPhase !== null && diskPlans !== totalPlansInPhase) { stateWarnings.push(`Plan count mismatch: STATE.md says ${totalPlansInPhase}, disk has ${diskPlans}`); } } result['state_warnings'] = stateWarnings; } } catch { /* intentionally empty */ } } output(withProjectRoot(cwd, result), raw); } function cmdInitPlanPhase(cwd, phase, raw, options = {}) { if (!phase) { error('phase required for init plan-phase'); } const config = loadConfig(cwd); let phaseInfo = findPhaseInternal(cwd, phase); const roadmapPhase = getRoadmapPhaseInternal(cwd, phase); if (phaseInfo?.['archived'] && roadmapPhase?.['found']) { phaseInfo = null; } if (!phaseInfo && roadmapPhase?.['found']) { const phaseName = roadmapPhase['phase_name']; phaseInfo = { found: true, directory: null, phase_number: roadmapPhase['phase_number'], phase_name: phaseName, phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null, plans: [], summaries: [], incomplete_plans: [], has_research: false, has_context: false, has_verification: false, has_reviews: false, }; } const reqMatch = roadmapPhase?.['section']?.match(REQUIREMENTS_HEADER_RE); const reqExtracted = reqMatch ? reqMatch[1].replace(/[\[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean).join(', ') : null; const phase_req_ids = reqExtracted && reqExtracted !== 'TBD' ? reqExtracted : null; const phaseDirPlan = phaseInfo?.['directory'] || null; const phaseNumberPlan = phaseInfo?.['phase_number'] || null; const phaseNamePlan = phaseInfo?.['phase_name'] || null; const rawProjectCodePlan = config.project_code || ''; let expectedPhaseDirPlan = null; if (!phaseDirPlan && phaseNumberPlan && phaseNamePlan) { const paddedNum = normalizePhaseName(phaseNumberPlan); const slug = (generateSlugInternal(phaseNamePlan) || '').substring(0, 60); if (slug) { const prefix = rawProjectCodePlan ? `${rawProjectCodePlan}-` : ''; const dirName = `${prefix}${paddedNum}-${slug}`; expectedPhaseDirPlan = toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningPaths(cwd).phases, dirName))); } } const granularityOverride = options['granularity']; assertValidGranularityOverride(granularityOverride, error); const granularity = resolveGranularityInternal(cwd, 'planning', granularityOverride || undefined); const wf = (config.workflow ?? {}); const result = { researcher_model: resolveModelInternal(cwd, 'gsd-phase-researcher'), planner_model: resolveModelInternal(cwd, 'gsd-planner'), checker_model: resolveModelInternal(cwd, 'gsd-plan-checker'), tdd_mode: options['tdd'] || Boolean(wf['tdd_mode']) || false, granularity, research_enabled: wf['research'], plan_checker_enabled: config.plan_checker, nyquist_validation_enabled: wf['nyquist_validation'], commit_docs: config.commit_docs, text_mode: config.text_mode, auto_advance: !!(config.auto_advance), auto_chain_active: !!(config._auto_chain_active), mode: config.mode || 'interactive', phase_found: !!phaseInfo, phase_dir: phaseDirPlan, expected_phase_dir: expectedPhaseDirPlan, phase_number: phaseNumberPlan, phase_name: phaseNamePlan, phase_slug: phaseInfo?.['phase_slug'] || null, padded_phase: phaseNumberPlan ? normalizePhaseName(phaseNumberPlan) : null, phase_req_ids, phase_status: phaseDirPlan ? determinePhaseStatus(phaseInfo?.['plans']?.length || 0, phaseInfo?.['summaries']?.length || 0, node_path_1.default.join(cwd, phaseDirPlan), 'Pending') : 'Pending', has_research: phaseInfo?.['has_research'] || false, has_context: phaseInfo?.['has_context'] || false, has_reviews: phaseInfo?.['has_reviews'] || false, has_plans: (phaseInfo?.['plans']?.length || 0) > 0, plan_count: phaseInfo?.['plans']?.length || 0, planning_exists: node_fs_1.default.existsSync(planningDir(cwd)), roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')), state_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'STATE.md'))), roadmap_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'ROADMAP.md'))), requirements_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md'))), patterns_path: null, }; if (phaseInfo?.['directory']) { const phaseDirFull = node_path_1.default.join(cwd, phaseInfo['directory']); try { const files = node_fs_1.default.readdirSync(phaseDirFull); const contextFile = findContextMdIn(phaseDirFull); if (contextFile) { result['context_path'] = toPosixPath(node_path_1.default.join(phaseInfo['directory'], contextFile)); } const researchFile = files.find((f) => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'); if (researchFile) { result['research_path'] = toPosixPath(node_path_1.default.join(phaseInfo['directory'], researchFile)); } const verificationFile = files.find((f) => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md'); if (verificationFile) { result['verification_path'] = toPosixPath(node_path_1.default.join(phaseInfo['directory'], verificationFile)); } const uatFile = files.find((f) => f.endsWith('-UAT.md') || f === 'UAT.md'); if (uatFile) { result['uat_path'] = toPosixPath(node_path_1.default.join(phaseInfo['directory'], uatFile)); } const reviewsFile = files.find((f) => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md'); if (reviewsFile) { result['reviews_path'] = toPosixPath(node_path_1.default.join(phaseInfo['directory'], reviewsFile)); } const patternsFile = files.find((f) => f.endsWith('-PATTERNS.md') || f === 'PATTERNS.md'); if (patternsFile) { result['patterns_path'] = toPosixPath(node_path_1.default.join(phaseInfo['directory'], patternsFile)); } } catch { /* intentionally empty */ } } if (options['validate']) { try { const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md'); const stateContent = (0, shell_command_projection_cjs_1.platformReadSync)(statePath); if (stateContent !== null) { const stateWarnings = []; result['state_validation_ran'] = true; const totalPlansRaw = (0, state_document_cjs_1.stateExtractField)(stateContent, 'Total Plans in Phase'); const totalPlansInPhase = totalPlansRaw ? parseInt(totalPlansRaw, 10) : null; if (totalPlansInPhase !== null && phaseInfo && totalPlansInPhase !== (phaseInfo['plans']?.length || 0)) { stateWarnings.push(`Plan count mismatch: STATE.md says ${totalPlansInPhase}, disk has ${phaseInfo['plans']?.length || 0}`); } result['state_warnings'] = stateWarnings; } } catch { /* intentionally empty */ } } output(withProjectRoot(cwd, result), raw); } function cmdInitNewProject(cwd, raw) { const config = loadConfig(cwd); const homedir = node_os_1.default.homedir(); const braveKeyFile = node_path_1.default.join(homedir, '.gsd', 'brave_api_key'); const hasBraveSearch = !!(process.env['BRAVE_API_KEY'] || node_fs_1.default.existsSync(braveKeyFile)); const firecrawlKeyFile = node_path_1.default.join(homedir, '.gsd', 'firecrawl_api_key'); const hasFirecrawl = !!(process.env['FIRECRAWL_API_KEY'] || node_fs_1.default.existsSync(firecrawlKeyFile)); const exaKeyFile = node_path_1.default.join(homedir, '.gsd', 'exa_api_key'); const hasExaSearch = !!(process.env['EXA_API_KEY'] || node_fs_1.default.existsSync(exaKeyFile)); let hasCode = false; let hasPackageFile = false; try { const codeExtensions = new Set([ '.ts', '.js', '.py', '.go', '.rs', '.swift', '.java', '.kt', '.kts', '.c', '.cpp', '.h', '.cs', '.rb', '.php', '.dart', '.m', '.mm', '.scala', '.groovy', '.lua', '.r', '.R', '.zig', '.ex', '.exs', '.clj', ]); const skipDirs = new Set([ 'node_modules', '.git', '.planning', '.claude', '.codex', '__pycache__', 'target', 'dist', 'build', ]); function findCodeFiles(dir, depth) { if (depth > 3) return false; let entries; try { entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true }); } catch { return false; } for (const entry of entries) { if (entry.isFile() && codeExtensions.has(node_path_1.default.extname(entry.name))) return true; if (entry.isDirectory() && !skipDirs.has(entry.name)) { if (findCodeFiles(node_path_1.default.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') || pathExistsInternal(cwd, 'Cargo.toml') || pathExistsInternal(cwd, 'go.mod') || pathExistsInternal(cwd, 'Package.swift') || pathExistsInternal(cwd, 'build.gradle') || pathExistsInternal(cwd, 'build.gradle.kts') || pathExistsInternal(cwd, 'pom.xml') || pathExistsInternal(cwd, 'Gemfile') || pathExistsInternal(cwd, 'composer.json') || pathExistsInternal(cwd, 'pubspec.yaml') || pathExistsInternal(cwd, 'CMakeLists.txt') || pathExistsInternal(cwd, 'Makefile') || pathExistsInternal(cwd, 'build.zig') || pathExistsInternal(cwd, 'mix.exs') || pathExistsInternal(cwd, 'project.clj'); const result = { researcher_model: resolveModelInternal(cwd, 'gsd-project-researcher'), synthesizer_model: resolveModelInternal(cwd, 'gsd-research-synthesizer'), roadmapper_model: resolveModelInternal(cwd, 'gsd-roadmapper'), commit_docs: config.commit_docs, project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'), has_codebase_map: pathExistsInternal(cwd, '.planning/codebase'), planning_exists: pathExistsInternal(cwd, '.planning'), has_existing_code: hasCode, has_package_file: hasPackageFile, is_brownfield: hasCode || hasPackageFile, needs_codebase_map: (hasCode || hasPackageFile) && !pathExistsInternal(cwd, '.planning/codebase'), ...getInitGitState(cwd), brave_search_available: hasBraveSearch, firecrawl_available: hasFirecrawl, exa_search_available: hasExaSearch, project_path: '.planning/PROJECT.md', }; output(withProjectRoot(cwd, result), raw); } function cmdInitNewMilestone(cwd, raw) { const config = loadConfig(cwd); const milestone = getMilestoneInfo(cwd); const latestCompleted = getLatestCompletedMilestone(cwd); const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases'); let phaseDirCount = 0; try { if (node_fs_1.default.existsSync(phasesDir)) { const isDirInMilestone = getMilestonePhaseFilter(cwd); phaseDirCount = node_fs_1.default .readdirSync(phasesDir, { withFileTypes: true }) .filter((entry) => entry.isDirectory() && isDirInMilestone(entry.name)) .length; } } catch { /* intentionally empty */ } const wf = (config.workflow ?? {}); const result = { researcher_model: resolveModelInternal(cwd, 'gsd-project-researcher'), synthesizer_model: resolveModelInternal(cwd, 'gsd-research-synthesizer'), roadmapper_model: resolveModelInternal(cwd, 'gsd-roadmapper'), commit_docs: config.commit_docs, research_enabled: wf['research'], 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 ? toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningRoot(cwd), 'milestones', `${latestCompleted.version}-phases`))) : null, project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'), roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')), state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')), project_path: '.planning/PROJECT.md', roadmap_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'ROADMAP.md'))), state_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'STATE.md'))), }; output(withProjectRoot(cwd, result), raw); } function cmdInitQuick(cwd, description, raw) { const config = loadConfig(cwd); const now = new Date(); const slug = description ? generateSlugInternal(description)?.substring(0, 40) : null; const yy = String(now.getFullYear()).slice(-2); const mm = String(now.getMonth() + 1).padStart(2, '0'); const dd = String(now.getDate()).padStart(2, '0'); const dateStr = yy + mm + dd; const secondsSinceMidnight = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds(); 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 = { planner_model: resolveModelInternal(cwd, 'gsd-planner'), executor_model: resolveModelInternal(cwd, 'gsd-executor'), checker_model: resolveModelInternal(cwd, 'gsd-plan-checker'), verifier_model: resolveModelInternal(cwd, 'gsd-verifier'), commit_docs: config.commit_docs, branch_name: quickBranchName, quick_id: quickId, slug: slug, description: description || null, date: now.toISOString().split('T')[0], timestamp: now.toISOString(), quick_dir: '.planning/quick', task_dir: slug ? `.planning/quick/${quickId}-${slug}` : null, roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')), planning_exists: node_fs_1.default.existsSync(planningRoot(cwd)), }; output(withProjectRoot(cwd, result), raw); } function cmdInitIngestDocs(cwd, raw) { const config = loadConfig(cwd); const result = { project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'), planning_exists: node_fs_1.default.existsSync(planningRoot(cwd)), ...getInitGitState(cwd), project_path: '.planning/PROJECT.md', commit_docs: config.commit_docs, }; output(withProjectRoot(cwd, result), raw); } function cmdInitResume(cwd, raw) { const config = loadConfig(cwd); let interruptedAgentId = null; const agentIdRaw = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(planningRoot(cwd), 'current-agent-id.txt')); if (agentIdRaw !== null) interruptedAgentId = agentIdRaw.trim(); const result = { state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')), roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')), project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'), planning_exists: node_fs_1.default.existsSync(planningRoot(cwd)), state_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'STATE.md'))), roadmap_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'ROADMAP.md'))), project_path: '.planning/PROJECT.md', has_interrupted_agent: !!interruptedAgentId, interrupted_agent_id: interruptedAgentId, commit_docs: config.commit_docs, }; output(withProjectRoot(cwd, result), raw); } function cmdInitVerifyWork(cwd, phase, raw) { if (!phase) { error('phase required for init verify-work'); } const config = loadConfig(cwd); let phaseInfo = findPhaseInternal(cwd, phase); if (phaseInfo?.['archived']) { const roadmapPhase = getRoadmapPhaseInternal(cwd, phase); if (roadmapPhase?.['found']) { phaseInfo = null; } } if (!phaseInfo) { const roadmapPhase = getRoadmapPhaseInternal(cwd, phase); if (roadmapPhase?.['found']) { const phaseName = roadmapPhase['phase_name']; phaseInfo = { found: true, directory: null, phase_number: roadmapPhase['phase_number'], phase_name: phaseName, phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null, plans: [], summaries: [], incomplete_plans: [], has_research: false, has_context: false, has_verification: false, }; } } const result = { planner_model: resolveModelInternal(cwd, 'gsd-planner'), checker_model: resolveModelInternal(cwd, 'gsd-plan-checker'), commit_docs: config.commit_docs, phase_found: !!phaseInfo, phase_dir: phaseInfo?.['directory'] || null, phase_number: phaseInfo?.['phase_number'] || null, phase_name: phaseInfo?.['phase_name'] || null, has_verification: phaseInfo?.['has_verification'] || false, }; output(withProjectRoot(cwd, result), raw); } function cmdInitPhaseOp(cwd, phase, raw) { const config = loadConfig(cwd); let phaseInfo = findPhaseInternal(cwd, phase); if (phaseInfo?.['archived']) { const roadmapPhase = getRoadmapPhaseInternal(cwd, phase); if (roadmapPhase?.['found']) { const phaseName = roadmapPhase['phase_name']; phaseInfo = { found: true, directory: null, phase_number: roadmapPhase['phase_number'], phase_name: phaseName, phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null, plans: [], summaries: [], incomplete_plans: [], has_research: false, has_context: false, has_verification: false, }; } } if (!phaseInfo) { const roadmapPhase = getRoadmapPhaseInternal(cwd, phase); if (roadmapPhase?.['found']) { const phaseName = roadmapPhase['phase_name']; phaseInfo = { found: true, directory: null, phase_number: roadmapPhase['phase_number'], phase_name: phaseName, phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null, plans: [], summaries: [], incomplete_plans: [], has_research: false, has_context: false, has_verification: false, }; } } const phaseDir = phaseInfo?.['directory'] || null; const phaseNumber = phaseInfo?.['phase_number'] || null; const phaseName = phaseInfo?.['phase_name'] || null; const rawProjectCode = config.project_code || ''; let expectedPhaseDir = null; if (!phaseDir && phaseNumber && phaseName) { const paddedNum = normalizePhaseName(phaseNumber); const slug = (generateSlugInternal(phaseName) || '').substring(0, 60); if (slug) { const prefix = rawProjectCode ? `${rawProjectCode}-` : ''; const dirName = `${prefix}${paddedNum}-${slug}`; expectedPhaseDir = toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningPaths(cwd).phases, dirName))); } } const result = { commit_docs: config.commit_docs, brave_search: typeof config.brave_search === 'string' ? (0, secrets_cjs_1.maskIfSecret)('brave_search', config.brave_search) : config.brave_search, firecrawl: typeof config.firecrawl === 'string' ? (0, secrets_cjs_1.maskIfSecret)('firecrawl', config.firecrawl) : config.firecrawl, exa_search: typeof config.exa_search === 'string' ? (0, secrets_cjs_1.maskIfSecret)('exa_search', config.exa_search) : config.exa_search, phase_found: !!phaseInfo, phase_dir: phaseDir, expected_phase_dir: expectedPhaseDir, phase_number: phaseNumber, phase_name: phaseName, phase_slug: phaseInfo?.['phase_slug'] || null, padded_phase: phaseNumber ? normalizePhaseName(phaseNumber) : null, has_research: phaseInfo?.['has_research'] || false, has_context: phaseInfo?.['has_context'] || false, has_plans: (phaseInfo?.['plans']?.length || 0) > 0, has_verification: phaseInfo?.['has_verification'] || false, has_reviews: phaseInfo?.['has_reviews'] || false, plan_count: phaseInfo?.['plans']?.length || 0, roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')), planning_exists: node_fs_1.default.existsSync(planningDir(cwd)), state_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'STATE.md'))), roadmap_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'ROADMAP.md'))), requirements_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md'))), }; if (phaseInfo?.['directory']) { const phaseDirFull = node_path_1.default.join(cwd, phaseInfo['directory']); try { const files = node_fs_1.default.readdirSync(phaseDirFull); const contextFile = findContextMdIn(phaseDirFull); if (contextFile) { result['context_path'] = toPosixPath(node_path_1.default.join(phaseInfo['directory'], contextFile)); } const researchFile = files.find((f) => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'); if (researchFile) { result['research_path'] = toPosixPath(node_path_1.default.join(phaseInfo['directory'], researchFile)); } const verificationFile = files.find((f) => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md'); if (verificationFile) { result['verification_path'] = toPosixPath(node_path_1.default.join(phaseInfo['directory'], verificationFile)); } const uatFile = files.find((f) => f.endsWith('-UAT.md') || f === 'UAT.md'); if (uatFile) { result['uat_path'] = toPosixPath(node_path_1.default.join(phaseInfo['directory'], uatFile)); } const reviewsFile = files.find((f) => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md'); if (reviewsFile) { result['reviews_path'] = toPosixPath(node_path_1.default.join(phaseInfo['directory'], reviewsFile)); } } catch { /* intentionally empty */ } } output(withProjectRoot(cwd, result), raw); } function cmdInitTodos(cwd, area, raw) { const config = loadConfig(cwd); const now = new Date(); const pendingDir = node_path_1.default.join(planningDir(cwd), 'todos', 'pending'); let count = 0; const todos = []; try { const files = node_fs_1.default.readdirSync(pendingDir).filter((f) => f.endsWith('.md')); for (const file of files) { const content = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(pendingDir, file)); if (content === null) continue; try { const createdMatch = content.match(/^created:\s*(.+)$/m); const titleMatch = content.match(/^title:\s*(.+)$/m); const areaMatch = content.match(/^area:\s*(.+)$/m); const todoArea = areaMatch ? areaMatch[1].trim() : 'general'; if (area && todoArea !== area) continue; count++; todos.push({ file, created: createdMatch ? createdMatch[1].trim() : 'unknown', title: titleMatch ? titleMatch[1].trim() : 'Untitled', area: todoArea, path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'todos', 'pending', file))), }); } catch { /* intentionally empty */ } } } catch { /* intentionally empty */ } const result = { commit_docs: config.commit_docs, date: now.toISOString().split('T')[0], timestamp: now.toISOString(), todo_count: count, todos, area_filter: area || null, pending_dir: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'todos', 'pending'))), completed_dir: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'todos', 'completed'))), planning_exists: node_fs_1.default.existsSync(planningDir(cwd)), todos_dir_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'todos')), pending_dir_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'todos', 'pending')), }; output(withProjectRoot(cwd, result), raw); } function cmdInitMilestoneOp(cwd, raw) { const config = loadConfig(cwd); const milestone = getMilestoneInfo(cwd); let phaseCount = 0; let completedPhases = 0; const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases'); const roadmapPhaseNumbers = []; try { const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md'); const roadmapRaw = node_fs_1.default.readFileSync(roadmapPath, 'utf-8'); const currentSection = extractCurrentMilestone(roadmapRaw, cwd); const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi; let m; while ((m = phasePattern.exec(currentSection)) !== null) { roadmapPhaseNumbers.push(m[1]); } } catch { /* intentionally empty */ } const canonicalizePhase = (tok) => { const m = tok.match(/^(\d+)([A-Z]?(?:\.\d+)*)$/); return m ? String(parseInt(m[1], 10)) + m[2] : tok; }; const diskPhaseDirs = new Map(); try { const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true }); for (const e of entries) { if (!e.isDirectory()) continue; const m = e.name.match(/^(\d+[A-Z]?(?:\.\d+)*)/); if (!m) continue; diskPhaseDirs.set(canonicalizePhase(m[1]), e.name); } } catch { /* intentionally empty */ } if (roadmapPhaseNumbers.length > 0) { phaseCount = roadmapPhaseNumbers.length; for (const num of roadmapPhaseNumbers) { const dirName = diskPhaseDirs.get(canonicalizePhase(num)); if (!dirName) continue; try { const hasSummary = listPhaseSummaryFiles(node_path_1.default.join(phasesDir, dirName)).length > 0; if (hasSummary) completedPhases++; } catch { /* intentionally empty */ } } } else { try { const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true }); const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name); phaseCount = dirs.length; for (const dir of dirs) { try { const hasSummary = listPhaseSummaryFiles(node_path_1.default.join(phasesDir, dir)).length > 0; if (hasSummary) completedPhases++; } catch { /* intentionally empty */ } } } catch { /* intentionally empty */ } } const archiveDir = node_path_1.default.join(planningRoot(cwd), 'archive'); let archivedMilestones = []; try { archivedMilestones = node_fs_1.default .readdirSync(archiveDir, { withFileTypes: true }) .filter((e) => e.isDirectory()) .map((e) => e.name); } catch { /* intentionally empty */ } const result = { commit_docs: config.commit_docs, milestone_version: milestone['version'], milestone_name: milestone['name'], milestone_slug: generateSlugInternal(milestone['name']), phase_count: phaseCount, completed_phases: completedPhases, all_phases_complete: phaseCount > 0 && phaseCount === completedPhases, archived_milestones: archivedMilestones, archive_count: archivedMilestones.length, project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'), roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')), state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')), archive_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningRoot(cwd), 'archive')), phases_dir_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'phases')), }; output(withProjectRoot(cwd, result), raw); } function cmdInitMapCodebase(cwd, raw) { const config = loadConfig(cwd); const now = new Date(); const codebaseDir = node_path_1.default.join(planningRoot(cwd), 'codebase'); let existingMaps = []; try { existingMaps = node_fs_1.default.readdirSync(codebaseDir).filter((f) => f.endsWith('.md')); } catch { /* intentionally empty */ } const result = { mapper_model: resolveModelInternal(cwd, 'gsd-codebase-mapper'), commit_docs: config.commit_docs, search_gitignored: config.search_gitignored, parallelization: config.parallelization, subagent_timeout: config.subagent_timeout, date: now.toISOString().split('T')[0], timestamp: now.toISOString(), codebase_dir: '.planning/codebase', existing_maps: existingMaps, has_maps: existingMaps.length > 0, planning_exists: pathExistsInternal(cwd, '.planning'), codebase_dir_exists: pathExistsInternal(cwd, '.planning/codebase'), }; output(withProjectRoot(cwd, result), raw); } function cmdInitManager(cwd, raw) { const config = loadConfig(cwd); const milestone = getMilestoneInfo(cwd); const _slashRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd); const paths = planningPaths(cwd); if (!node_fs_1.default.existsSync(paths.roadmap)) { error(`No ROADMAP.md found. Run ${(0, runtime_slash_cjs_1.formatGsdSlash)('new-milestone', _slashRuntime)} first.`); } if (!node_fs_1.default.existsSync(paths.state)) { error(`No STATE.md found. Run ${(0, runtime_slash_cjs_1.formatGsdSlash)('new-milestone', _slashRuntime)} first.`); } const rawContent = node_fs_1.default.readFileSync(paths.roadmap, 'utf-8'); const content = extractCurrentMilestone(rawContent, cwd); const phasesDir = paths.phases; const isDirInMilestone = getMilestonePhaseFilter(cwd); const _phaseDirEntries = (() => { try { return node_fs_1.default .readdirSync(phasesDir, { withFileTypes: true }) .filter((e) => e.isDirectory()) .map((e) => e.name); } catch { return []; } })(); const _checkboxStates = new Map(); const _cbPattern = /-\s*\[(x| )\]\s*.*Phase\s+(\d+[A-Z]?(?:\.\d+)*)[:\s]/gi; let _cbMatch; while ((_cbMatch = _cbPattern.exec(content)) !== null) { _checkboxStates.set(_cbMatch[2], _cbMatch[1].toLowerCase() === 'x'); } 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[\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 dirs = _phaseDirEntries.filter(isDirInMilestone); const dirMatch = dirs.find((d) => phaseTokenMatches(d, normalized)); if (dirMatch) { const fullDir = node_path_1.default.join(phasesDir, dirMatch); const phaseFiles = node_fs_1.default.readdirSync(fullDir); planCount = listPhasePlanFiles(fullDir).length; summaryCount = listPhaseSummaryFiles(fullDir).length; hasContext = findContextMdIn(fullDir) !== null; 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'; const nowMs = Date.now(); let newestMtime = 0; for (const f of phaseFiles) { try { const stat = node_fs_1.default.statSync(node_path_1.default.join(fullDir, f)); if (stat.mtimeMs > newestMtime) newestMtime = stat.mtimeMs; } catch { /* intentionally empty */ } } if (newestMtime > 0) { lastActivity = new Date(newestMtime).toISOString(); isActive = nowMs - newestMtime < 300000; } } } catch { /* intentionally empty */ } const roadmapComplete = _checkboxStates.get(phaseNum) || 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, }); } const MAX_NAME_WIDTH = 20; for (const phase of phases) { const name = phase['name']; if (name.length > MAX_NAME_WIDTH) { phase['display_name'] = name.slice(0, MAX_NAME_WIDTH - 1) + '…'; } else { phase['display_name'] = name; } } const completedNums = new Set(phases.filter((p) => p['disk_status'] === 'complete').map((p) => p['number'])); const _allCompletedPattern = /-\s*\[x\]\s*.*Phase\s+(\d+[A-Z]?(?:\.\d+)*)[:\s]/gi; let _allMatch; while ((_allMatch = _allCompletedPattern.exec(rawContent)) !== null) { completedNums.add(_allMatch[1]); } 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); } for (const phase of phases) { if (!phase['depends_on'] || /^none$/i.test(phase['depends_on'].trim())) { phase['deps_satisfied'] = true; } else { const depNums = phase['depends_on'].match(/\d+(?:\.\d+)*/g) || []; phase['deps_satisfied'] = depNums.every((n) => completedNums.has(n)); phase['dep_phases'] = depNums; } } for (const phase of phases) { phase['deps_display'] = phase['dep_phases'] && phase['dep_phases'].length > 0 ? phase['dep_phases'].join(',') : '—'; } for (const phase of phases) { phase['is_next_to_discuss'] = (phase['disk_status'] === 'empty' || phase['disk_status'] === 'no_directory') && phase['deps_satisfied']; } let waitingSignal = null; try { const waitingPath = node_path_1.default.join(cwd, '.planning', 'WAITING.json'); const waitingRaw = (0, shell_command_projection_cjs_1.platformReadSync)(waitingPath); if (waitingRaw !== null) { waitingSignal = JSON.parse(waitingRaw); } } catch { /* intentionally empty */ } const recommendedActions = []; for (const phase of phases) { if (phase['disk_status'] === 'complete') continue; if (/^999(?:\.|$)/.test(phase['number'])) continue; if (phase['disk_status'] === 'planned' && phase['deps_satisfied']) { recommendedActions.push({ phase: phase['number'], phase_name: phase['name'], action: 'execute', reason: `${phase['plan_count']} plans ready, dependencies met`, command: `${(0, runtime_slash_cjs_1.formatGsdSlash)('execute-phase', _slashRuntime)} ${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: `${(0, runtime_slash_cjs_1.formatGsdSlash)('plan-phase', _slashRuntime)} ${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: `${(0, runtime_slash_cjs_1.formatGsdSlash)('discuss-phase', _slashRuntime)} ${phase['number']}`, }); } } 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) { return activeExecuting.every((active) => !hasDepRelationship(action['phase'], active['number'])); } if (action['action'] === 'plan' && activePlanning.length > 0) { return activePlanning.every((active) => !hasDepRelationship(action['phase'], active['number'])); } return true; }); const nonBacklogPhases = phases.filter((p) => !/^999(?:\.|$)/.test(p['number'])); const completedCount = nonBacklogPhases.filter((p) => p['disk_status'] === 'complete').length; const sanitizeFlags = (rawVal) => { const val = typeof rawVal === 'string' ? rawVal : ''; if (!val) return ''; const tokens = val.split(/\s+/).filter(Boolean); const safe = tokens.every((t) => /^--[a-zA-Z0-9][-a-zA-Z0-9]*$/.test(t) || /^[a-zA-Z0-9][-a-zA-Z0-9_.]*$/.test(t)); if (!safe) { process.stderr.write(`gsd-tools: warning: manager.flags contains invalid tokens, ignoring: ${val}\n`); return ''; } return val; }; const mgr = config.manager; const mgrFlags = mgr?.['flags']; const managerFlags = { discuss: sanitizeFlags(mgrFlags?.['discuss']), plan: sanitizeFlags(mgrFlags?.['plan']), execute: sanitizeFlags(mgrFlags?.['execute']), }; 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 === nonBacklogPhases.length && nonBacklogPhases.length > 0, project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'), roadmap_exists: true, state_exists: true, manager_flags: managerFlags, }; output(withProjectRoot(cwd, result), raw); } function cmdInitProgress(cwd, raw) { try { pruneOrphanedWorktrees(cwd); } catch { /* intentionally empty */ } const config = loadConfig(cwd); const milestone = getMilestoneInfo(cwd); const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases'); const phases = []; let currentPhase = null; let nextPhase = null; const roadmapPhaseNums = new Set(); const roadmapPhaseNames = new Map(); const roadmapCheckboxStates = new Map(); try { const roadmapContent = extractCurrentMilestone(node_fs_1.default.readFileSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md'), 'utf-8'), cwd); const headingPattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi; let hm; while ((hm = headingPattern.exec(roadmapContent)) !== null) { roadmapPhaseNums.add(hm[1]); roadmapPhaseNames.set(hm[1], hm[2].replace(/\(INSERTED\)/i, '').trim()); } const cbPattern = /-\s*\[(x| )\]\s*.*Phase\s+(\d+[A-Z]?(?:\.\d+)*)[:\s]/gi; let cbm; while ((cbm = cbPattern.exec(roadmapContent)) !== null) { roadmapCheckboxStates.set(cbm[2], cbm[1].toLowerCase() === 'x'); } } catch { /* intentionally empty */ } const isDirInMilestone = getMilestonePhaseFilter(cwd); const seenPhaseNums = new Set(); try { const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true }); const dirs = entries .filter((e) => e.isDirectory()) .map((e) => e.name) .filter(isDirInMilestone) .sort((a, b) => { const pa = a.match(/^(\d+[A-Z]?(?:\.\d+)*)/i); const pb = b.match(/^(\d+[A-Z]?(?:\.\d+)*)/i); if (!pa || !pb) return a.localeCompare(b); return parseInt(pa[1], 10) - parseInt(pb[1], 10); }); for (const dir of dirs) { const dirMatch = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i); const phaseNumber = dirMatch ? dirMatch[1] : dir; const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null; seenPhaseNums.add(phaseNumber.replace(/^0+/, '') || '0'); const phasePath = node_path_1.default.join(phasesDir, dir); const phaseFiles = node_fs_1.default.readdirSync(phasePath); const plans = listPhasePlanFiles(phasePath); const summaries = listPhaseSummaryFiles(phasePath); const hasResearch = phaseFiles.some((f) => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'); const status = summaries.length >= plans.length && plans.length > 0 ? 'complete' : plans.length > 0 ? 'in_progress' : hasResearch ? 'researched' : 'pending'; const phaseInfo = { number: phaseNumber, name: phaseName, directory: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'phases', dir))), status, plan_count: plans.length, summary_count: summaries.length, has_research: hasResearch, }; phases.push(phaseInfo); if (!currentPhase && (status === 'in_progress' || status === 'researched')) { currentPhase = phaseInfo; } if (!nextPhase && status === 'pending') { nextPhase = phaseInfo; } } } catch { /* intentionally empty */ } for (const [num, name] of roadmapPhaseNames) { const stripped = num.replace(/^0+/, '') || '0'; if (!seenPhaseNums.has(stripped)) { const checkboxComplete = roadmapCheckboxStates.get(num) === true || roadmapCheckboxStates.get(stripped) === true; const status = checkboxComplete ? 'complete' : 'not_started'; const phaseInfo = { number: num, name: name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''), directory: null, status, plan_count: 0, summary_count: 0, has_research: false, }; phases.push(phaseInfo); if (!nextPhase && !currentPhase && status !== 'complete') { nextPhase = phaseInfo; } } } phases.sort((a, b) => parseInt(a['number'], 10) - parseInt(b['number'], 10)); let pausedAt = null; const state = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(planningDir(cwd), 'STATE.md')); if (state !== null) { const pauseMatch = state.match(/\*\*Paused At:\*\*\s*(.+)/); if (pauseMatch) pausedAt = pauseMatch[1].trim(); } const result = { executor_model: resolveModelInternal(cwd, 'gsd-executor'), planner_model: resolveModelInternal(cwd, 'gsd-planner'), commit_docs: config.commit_docs, milestone_version: milestone['version'], milestone_name: milestone['name'], phases, phase_count: phases.length, completed_count: phases.filter((p) => p['status'] === 'complete').length, in_progress_count: phases.filter((p) => p['status'] === 'in_progress').length, current_phase: currentPhase, next_phase: nextPhase, paused_at: pausedAt, has_work_in_progress: !!currentPhase, project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'), roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')), state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')), state_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'STATE.md'))), roadmap_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'ROADMAP.md'))), project_path: '.planning/PROJECT.md', config_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'config.json'))), }; output(withProjectRoot(cwd, result), raw); } function detectChildRepos(dir) { const repos = []; let entries; try { entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true }); } catch { return repos; } for (const entry of entries) { if (!entry.isDirectory()) continue; if (entry.name.startsWith('.')) continue; const fullPath = node_path_1.default.join(dir, entry.name); const gitDir = node_path_1.default.join(fullPath, '.git'); if (node_fs_1.default.existsSync(gitDir)) { const statusResult = (0, shell_command_projection_cjs_1.execGit)(['status', '--porcelain'], { cwd: fullPath, timeout: 5000, }); const hasUncommitted = statusResult['exitCode'] === 0 && statusResult['stdout'].length > 0; repos.push({ name: entry.name, path: fullPath, has_uncommitted: hasUncommitted }); } } return repos; } function cmdInitNewWorkspace(cwd, raw) { const homedir = process.env['HOME'] || node_os_1.default.homedir(); const defaultBase = node_path_1.default.join(homedir, 'gsd-workspaces'); const childRepos = detectChildRepos(cwd); const gitVersion = (0, shell_command_projection_cjs_1.execGit)(['--version'], { timeout: 5000 }); const worktreeAvailable = gitVersion['exitCode'] === 0; const result = { default_workspace_base: defaultBase, child_repos: childRepos, child_repo_count: childRepos.length, worktree_available: worktreeAvailable, is_git_repo: pathExistsInternal(cwd, '.git'), cwd_repo_name: node_path_1.default.basename(cwd), }; output(withProjectRoot(cwd, result), raw); } function cmdInitListWorkspaces(cwd, raw) { const homedir = process.env['HOME'] || node_os_1.default.homedir(); const defaultBase = node_path_1.default.join(homedir, 'gsd-workspaces'); const workspaces = []; if (node_fs_1.default.existsSync(defaultBase)) { let entries; try { entries = node_fs_1.default.readdirSync(defaultBase, { withFileTypes: true }); } catch { entries = []; } for (const entry of entries) { if (!entry.isDirectory()) continue; const wsPath = node_path_1.default.join(defaultBase, entry.name); const manifestPath = node_path_1.default.join(wsPath, 'WORKSPACE.md'); if (!node_fs_1.default.existsSync(manifestPath)) continue; let repoCount = 0; let hasProject = false; let strategy = 'unknown'; const manifest = (0, shell_command_projection_cjs_1.platformReadSync)(manifestPath); if (manifest !== null) { const strategyMatch = manifest.match(/^Strategy:\s*(.+)$/m); if (strategyMatch) strategy = strategyMatch[1].trim(); const tableRows = manifest .split('\n') .filter((l) => l.match(/^\|\s*\w/) && !l.includes('Repo') && !l.includes('---')); repoCount = tableRows.length; } hasProject = node_fs_1.default.existsSync(node_path_1.default.join(wsPath, '.planning', 'PROJECT.md')); workspaces.push({ name: entry.name, path: wsPath, repo_count: repoCount, strategy, has_project: hasProject, }); } } const result = { workspace_base: defaultBase, workspaces, workspace_count: workspaces.length, }; output(result, raw); } function cmdInitRemoveWorkspace(cwd, name, raw) { const homedir = process.env['HOME'] || node_os_1.default.homedir(); const defaultBase = node_path_1.default.join(homedir, 'gsd-workspaces'); if (!name) { error('workspace name required for init remove-workspace'); } const wsPath = node_path_1.default.join(defaultBase, name); const manifestPath = node_path_1.default.join(wsPath, 'WORKSPACE.md'); if (!node_fs_1.default.existsSync(wsPath)) { error(`Workspace not found: ${wsPath}`); } const repos = []; let strategy = 'unknown'; const manifestContent = (0, shell_command_projection_cjs_1.platformReadSync)(manifestPath); if (manifestContent !== null) { try { const manifest = manifestContent; const strategyMatch = manifest.match(/^Strategy:\s*(.+)$/m); if (strategyMatch) strategy = strategyMatch[1].trim(); const lines = manifest.split('\n'); for (const line of lines) { const lineMatch = line.match(/^\|\s*(\S+)\s*\|\s*(\S+)\s*\|\s*(\S+)\s*\|\s*(\S+)\s*\|$/); if (lineMatch && lineMatch[1] !== 'Repo' && !lineMatch[1].includes('---')) { repos.push({ name: lineMatch[1], source: lineMatch[2], branch: lineMatch[3], strategy: lineMatch[4], }); } } } catch { /* best-effort */ } } const dirtyRepos = []; for (const repo of repos) { const repoPath = node_path_1.default.join(wsPath, repo.name); if (!node_fs_1.default.existsSync(repoPath)) continue; const statusResult = (0, shell_command_projection_cjs_1.execGit)(['status', '--porcelain'], { cwd: repoPath, timeout: 5000, }); if (statusResult['exitCode'] === 0 && statusResult['stdout'].length > 0) { dirtyRepos.push(repo.name); } } const result = { workspace_name: name, workspace_path: wsPath, has_manifest: node_fs_1.default.existsSync(manifestPath), strategy, repos, repo_count: repos.length, dirty_repos: dirtyRepos, has_dirty_repos: dirtyRepos.length > 0, }; output(result, raw); } function buildAgentSkillsBlock(config, agentType, projectRoot) { const runtime = (config && config['runtime']) || 'claude'; const globalSkillsBase = (0, runtime_homes_cjs_1.getGlobalSkillsBase)(runtime); if (!config || !config['agent_skills'] || !agentType) return ''; let skillPaths = config['agent_skills'][agentType]; if (!skillPaths) return ''; if (typeof skillPaths === 'string') skillPaths = [skillPaths]; if (!Array.isArray(skillPaths) || skillPaths.length === 0) return ''; // Hoist trusted roots computation before the loop: loadTrustedGlobalRoots does // realpathSync I/O and should run at most once per call, not once per failing skill. // It returns [] cheaply when no roots are configured, so the realpath cost only // occurs when the caller has actually set trusted_global_roots. const trustedGlobalRoots = (0, security_cjs_1.loadTrustedGlobalRoots)(config); // Each entry is either a filesystem include ({ kind: 'include', ref, display }) or a // Skill-tool directive ({ kind: 'directive', name }) for plugin-provided namespaced skills. const validEntries = []; for (const skillPath of skillPaths) { if (typeof skillPath !== 'string') continue; if (skillPath.startsWith('global:')) { const skillName = skillPath.slice(7); if (!skillName) { process.stderr.write(`[agent-skills] WARNING: "global:" prefix with empty skill name — skipping\n`); continue; } // Accept: one or more [A-Za-z0-9_-]+ segments joined by single colons. // Rejects: empty segments (::), leading/trailing colon, dots, slashes, backslashes. if (!/^[A-Za-z0-9_-]+(:[A-Za-z0-9_-]+)*$/.test(skillName)) { process.stderr.write(`[agent-skills] WARNING: Invalid global skill name "${skillName}" — skipping\n`); continue; } const isNamespaced = skillName.includes(':'); if (isNamespaced) { // Plugin-provided namespaced skill: no filesystem path exists locally. if (runtime === 'claude') { // Emit a natural-language Skill-tool directive (not a @-include). validEntries.push({ kind: 'directive', name: skillName }); } else { process.stderr.write(`[agent-skills] WARNING: Plugin-namespaced skill "global:${skillName}" requires a Skill-tool-capable runtime (claude) — skipping on runtime "${runtime}"\n`); } continue; } // Non-namespaced bare name: attempt filesystem resolution as before. if (globalSkillsBase === null) { process.stderr.write(`[agent-skills] WARNING: Runtime "${runtime}" does not use a skills directory — "global:${skillName}" is not supported on this runtime\n`); continue; } const globalSkillDir = (0, runtime_homes_cjs_1.getGlobalSkillDir)(runtime, skillName); const globalSkillMd = node_path_1.default.join(globalSkillDir, 'SKILL.md'); const displayPath = (0, runtime_homes_cjs_1.getGlobalSkillDisplayPath)(runtime, skillName); if (!node_fs_1.default.existsSync(globalSkillMd)) { process.stderr.write(`[agent-skills] WARNING: Global skill not found at "${displayPath}/SKILL.md" — skipping\n`); continue; } const pathCheck = (0, security_cjs_1.validatePath)(globalSkillMd, globalSkillsBase, { allowAbsolute: true }); if (!pathCheck['safe']) { const acceptedViaTrustedRoot = trustedGlobalRoots.some((root) => { const rootCheck = (0, security_cjs_1.validatePath)(globalSkillMd, root, { allowAbsolute: true }); return Boolean(rootCheck['safe']); }); if (!acceptedViaTrustedRoot) { process.stderr.write(`[agent-skills] WARNING: Global skill "${skillName}" failed path check (symlink escape?) — skipping\n`); continue; } process.stderr.write(`[agent-skills] NOTE: Global skill "${skillName}" accepted via trusted_global_roots (resolves outside the default skills dir)\n`); } validEntries.push({ kind: 'include', ref: `${globalSkillDir}/SKILL.md`, display: displayPath }); continue; } const pathCheck = (0, security_cjs_1.validatePath)(skillPath, projectRoot); if (!pathCheck['safe']) { process.stderr.write(`[agent-skills] WARNING: Skipping unsafe path "${skillPath}": ${pathCheck['error']}\n`); continue; } const skillMdPath = node_path_1.default.join(projectRoot, skillPath, 'SKILL.md'); if (!node_fs_1.default.existsSync(skillMdPath)) { process.stderr.write(`[agent-skills] WARNING: Skill not found at "${skillPath}/SKILL.md" — skipping\n`); continue; } validEntries.push({ kind: 'include', ref: `${skillPath}/SKILL.md`, display: skillPath }); } if (validEntries.length === 0) return ''; const lines = validEntries.map((entry) => { if (entry.kind === 'directive') { return `- Load the \`${entry.name}\` skill via the Skill tool before proceeding (plugin-provided).`; } return `- @${entry.ref}`; }).join('\n'); return `\nRead these user-configured skills:\n${lines}\n`; } function cmdAgentSkills(cwd, agentType, raw, jsonMode) { if (!agentType) { output('', raw, ''); return; } const config = loadConfig(cwd); const block = buildAgentSkillsBlock(config, agentType, cwd); if (jsonMode) { const skillPaths = (config && config.agent_skills && config.agent_skills[agentType]) || []; const normalizedPaths = Array.isArray(skillPaths) ? skillPaths : skillPaths ? [skillPaths] : []; output({ agent_type: agentType, block: block || '', skills_count: normalizedPaths.length }, raw); return; } if (block) { process.stdout.write(block); } process.exit(0); } function buildSkillManifest(cwd, skillsDir = null) { const canonicalRoots = skillsDir ? [ { root: node_path_1.default.resolve(skillsDir), path: node_path_1.default.resolve(skillsDir), scope: 'custom', present: node_fs_1.default.existsSync(skillsDir), kind: 'skills', }, ] : [ { root: '.claude/skills', path: node_path_1.default.join(cwd, '.claude', 'skills'), scope: 'project', kind: 'skills', }, { root: '.agents/skills', path: node_path_1.default.join(cwd, '.agents', 'skills'), scope: 'project', kind: 'skills', }, { root: '.cursor/skills', path: node_path_1.default.join(cwd, '.cursor', 'skills'), scope: 'project', kind: 'skills', }, { root: '.github/skills', path: node_path_1.default.join(cwd, '.github', 'skills'), scope: 'project', kind: 'skills', }, { root: '.codex/skills', path: node_path_1.default.join(cwd, '.codex', 'skills'), scope: 'project', kind: 'skills', }, { root: '~/.claude/skills', path: (0, runtime_homes_cjs_1.getGlobalSkillsBase)('claude'), scope: 'global', kind: 'skills', }, { root: '~/.codex/skills', path: (0, runtime_homes_cjs_1.getGlobalSkillsBase)('codex'), scope: 'global', kind: 'skills', }, { root: '.claude/gsd-core/skills', path: node_path_1.default.join(node_os_1.default.homedir(), '.claude', 'gsd-core', 'skills'), scope: 'import-only', kind: 'skills', deprecated: true, }, { root: '.claude/commands/gsd', path: node_path_1.default.join(node_os_1.default.homedir(), '.claude', 'commands', 'gsd'), scope: 'legacy-commands', kind: 'commands', deprecated: true, }, ]; const skills = []; const roots = []; let legacyClaudeCommandsInstalled = false; for (const rootInfo of canonicalRoots) { const rootPath = rootInfo.path; const rootSummary = { root: rootInfo.root, path: rootPath, scope: rootInfo.scope, present: node_fs_1.default.existsSync(rootPath), deprecated: !!rootInfo.deprecated, }; if (!rootSummary.present) { roots.push(rootSummary); continue; } if (rootInfo.kind === 'commands') { let entries = []; try { entries = node_fs_1.default.readdirSync(rootPath, { withFileTypes: true }); } catch { roots.push(rootSummary); continue; } const commandFiles = entries.filter((entry) => entry.isFile() && entry.name.endsWith('.md')); rootSummary.command_count = commandFiles.length; if (rootSummary.command_count > 0) legacyClaudeCommandsInstalled = true; roots.push(rootSummary); continue; } let entries; try { entries = node_fs_1.default.readdirSync(rootPath, { withFileTypes: true }); } catch { roots.push(rootSummary); continue; } // Track skill names seen within this root to deduplicate dual-routed concretes // (e.g. spec-phase nested under both gsd-ns-workflow and gsd-ns-manage). const seenNamesInRoot = new Set(); function pushSkillEntry( // relPath must use forward slashes on all platforms (manifest paths are // posix-style for cross-platform stability; flat entries use template // literals that always produce '/'; nested entries are joined below // with explicit '/' separators rather than path.join). relPath, content) { const frontmatter = extractFrontmatter(content); const dirPart = relPath.replace(/\/SKILL\.md$/, ''); const stem = dirPart.includes('/') ? dirPart.split('/').pop() : dirPart; const name = frontmatter['name'] || stem; if (seenNamesInRoot.has(name)) return false; // dedupe dual-routed concretes seenNamesInRoot.add(name); const description = frontmatter['description'] || ''; const triggers = []; const bodyMatch = content.match(/^---[\s\S]*?---\s*\n([\s\S]*)$/); if (bodyMatch) { const body = bodyMatch[1]; const triggerLines = body.match(/^TRIGGER\s+when:\s*(.+)$/gmi); if (triggerLines) { for (const line of triggerLines) { const m = line.match(/^TRIGGER\s+when:\s*(.+)$/i); if (m) triggers.push(m[1].trim()); } } } skills.push({ name, description, triggers, path: dirPart, file_path: relPath, root: rootInfo.root, scope: rootInfo.scope, installed: rootInfo.scope !== 'import-only', deprecated: !!rootInfo.deprecated, }); return true; } let skillCount = 0; for (const entry of entries) { if (!entry.isDirectory()) continue; const skillMdPath = node_path_1.default.join(rootPath, entry.name, 'SKILL.md'); const content = (0, shell_command_projection_cjs_1.platformReadSync)(skillMdPath); if (content !== null) { if (pushSkillEntry(`${entry.name}/SKILL.md`, content)) skillCount++; } // Nested layout: /skills//SKILL.md // Used by cline, qwen, hermes, augment, trae, antigravity (#69 nested=true). // Descend exactly one level into /skills/ — no deeper recursion. // Scope to gsd-ns-* routers only: never vacuum up an unrelated user skill // that happens to have its own `skills/` subdirectory. if (!entry.name.startsWith('gsd-ns-')) continue; const nestedSkillsDir = node_path_1.default.join(rootPath, entry.name, 'skills'); let nestedEntries = []; try { nestedEntries = node_fs_1.default.readdirSync(nestedSkillsDir, { withFileTypes: true }); } catch { // No skills/ subdir — flat layout or unreadable; nothing to do. nestedEntries = []; } for (const nested of nestedEntries) { if (!nested.isDirectory()) continue; const nestedSkillMd = node_path_1.default.join(nestedSkillsDir, nested.name, 'SKILL.md'); const nestedContent = (0, shell_command_projection_cjs_1.platformReadSync)(nestedSkillMd); if (nestedContent === null) continue; // Use forward-slash separator explicitly so manifest paths are posix-style // on all platforms, matching the flat-layout behaviour above. const relPath = `${entry.name}/skills/${nested.name}/SKILL.md`; if (pushSkillEntry(relPath, nestedContent)) skillCount++; } } rootSummary.skill_count = skillCount; roots.push(rootSummary); } skills.sort((a, b) => { const rootCmp = a.root.localeCompare(b.root); return rootCmp !== 0 ? rootCmp : a.name.localeCompare(b.name); }); const gsdSkillsInstalled = skills.some((skill) => skill.name.startsWith('gsd-')); return { skills, roots, installation: { gsd_skills_installed: gsdSkillsInstalled, legacy_claude_commands_installed: legacyClaudeCommandsInstalled, }, counts: { skills: skills.length, roots: roots.length, }, }; } function cmdSkillManifest(cwd, args, raw) { const skillsDirIdx = args.indexOf('--skills-dir'); const skillsDir = skillsDirIdx >= 0 && args[skillsDirIdx + 1] ? args[skillsDirIdx + 1] : null; const manifest = buildSkillManifest(cwd, skillsDir); if (args.includes('--write')) { const planDir = node_path_1.default.join(cwd, '.planning'); if (node_fs_1.default.existsSync(planDir)) { const manifestPath = node_path_1.default.join(planDir, 'skill-manifest.json'); (0, shell_command_projection_cjs_1.platformWriteSync)(manifestPath, JSON.stringify(manifest, null, 2)); } } output(manifest, raw); } module.exports = { cmdInitExecutePhase, cmdInitPlanPhase, cmdInitNewProject, cmdInitNewMilestone, cmdInitQuick, cmdInitIngestDocs, cmdInitResume, cmdInitVerifyWork, cmdInitPhaseOp, cmdInitTodos, cmdInitMilestoneOp, cmdInitMapCodebase, cmdInitProgress, cmdInitManager, cmdInitNewWorkspace, cmdInitListWorkspaces, cmdInitRemoveWorkspace, detectChildRepos, buildAgentSkillsBlock, cmdAgentSkills, buildSkillManifest, cmdSkillManifest, };