Spaces:
Runtime error
Runtime error
Aryan Mishra
feat: initial commit - Multilingual ABSA project setup with 6 phases, 36 requirements
a6b96c2 | ; | |
| /** | |
| * 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 `<agent_skills>\nRead these user-configured skills:\n${lines}\n</agent_skills>`; | |
| } | |
| 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: <entry>/skills/<stem>/SKILL.md | |
| // Used by cline, qwen, hermes, augment, trae, antigravity (#69 nested=true). | |
| // Descend exactly one level into <entry>/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, | |
| }; | |