diff --git a/gsd-core/workflows/onboard.md b/gsd-core/workflows/onboard.md index 2c8bfd135..80fac011c 100644 --- a/gsd-core/workflows/onboard.md +++ b/gsd-core/workflows/onboard.md @@ -29,7 +29,7 @@ Parse `$ARGUMENTS`: Run the init projection: ```bash -_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found. Run: npx -y @opengsd/gsd-core@latest --local" >&2; exit 1; fi +_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi INIT=$(gsd_run init onboard) if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi ``` @@ -38,7 +38,7 @@ Parse JSON fields: `planning_exists`, `project_exists`, `requirements_exists`, `roadmap_exists`, `state_exists`, `has_existing_code`, `has_package_file`, `is_brownfield`, `has_codebase_map`, `codebase_map_files_present`, `missing_codebase_map_files`, `has_docs_candidates`, -`doc_candidate_count`, `onboarding_summary_exists`, `text_mode`, `agents_installed`, +`doc_candidate_count`, `onboarding_summary_exists`, `text_mode`, `commit_docs`, `agents_installed`, `missing_agents`, `has_git`, `git_worktree_root`, `in_nested_subdir`. Set `TEXT_MODE=true` if `--text` is present OR `text_mode` from INIT is true. diff --git a/src/init.cts b/src/init.cts index 472de1ab7..a82bae89d 100644 --- a/src/init.cts +++ b/src/init.cts @@ -140,6 +140,24 @@ function listPlanningDocCandidates(cwd: string): string[] { const roots = ['docs', 'adr', 'adrs', 'prd', 'prds', 'spec', 'specs', 'rfc', 'rfcs']; const candidates = new Set(); + const isPlanningDocCandidate = (rel: string, name: string): boolean => { + const upperName = name.toUpperCase(); + const relLower = rel.toLowerCase(); + const pathSegments = relLower.split('/'); + return ( + /(^|[-_ ])(ADR|PRD|SPEC|RFC)([-_ ]|\.)/i.test(name) || + /^\d{4}[-_].+\.md$/i.test(name) || + pathSegments.some((segment) => PLANNING_DOC_SEGMENTS.has(segment)) || + upperName === 'REQUIREMENTS.MD' + ); + }; + + const addCandidate = (rel: string, name: string): void => { + if (name.toLowerCase().endsWith('.md') && isPlanningDocCandidate(rel, name)) { + candidates.add(toPosixPath(rel)); + } + }; + const visit = (dir: string, relDir: string, depth: number): void => { if (depth > 3) return; let entries: fs.Dirent[]; @@ -158,21 +176,20 @@ function listPlanningDocCandidates(cwd: string): string[] { continue; } - if (!entry.isFile() || !entry.name.toLowerCase().endsWith('.md')) continue; - const upperName = entry.name.toUpperCase(); - const relLower = rel.toLowerCase(); - const pathSegments = relLower.split('/'); - if ( - /(^|[-_ ])(ADR|PRD|SPEC|RFC)([-_ ]|\.)/i.test(entry.name) || - /^\d{4}[-_].+\.md$/i.test(entry.name) || - pathSegments.some((segment) => PLANNING_DOC_SEGMENTS.has(segment)) || - upperName === 'REQUIREMENTS.md' - ) { - candidates.add(toPosixPath(rel)); - } + if (entry.isFile()) addCandidate(rel, entry.name); } }; + let rootEntries: fs.Dirent[] = []; + try { + rootEntries = fs.readdirSync(cwd, { withFileTypes: true }); + } catch { + rootEntries = []; + } + for (const entry of rootEntries) { + if (entry.isFile()) addCandidate(entry.name, entry.name); + } + for (const root of roots) { const full = path.join(cwd, root); if (fs.existsSync(full)) visit(full, root, 0); diff --git a/tests/onboard-command.test.cjs b/tests/onboard-command.test.cjs index aac86784d..ccaa85c84 100644 --- a/tests/onboard-command.test.cjs +++ b/tests/onboard-command.test.cjs @@ -71,6 +71,32 @@ describe('init onboard public CLI projection', () => { ]); }); + test('detects root-level planning docs without broad repo scan', () => { + fs.writeFileSync(path.join(tmpDir, 'PRD.md'), '# Product Requirements\n'); + fs.writeFileSync(path.join(tmpDir, 'SPEC.md'), '# Specification\n'); + fs.writeFileSync(path.join(tmpDir, 'RFC.md'), '# Request for Comments\n'); + fs.writeFileSync(path.join(tmpDir, 'ADR.md'), '# Architecture Decision\n'); + fs.writeFileSync(path.join(tmpDir, 'REQUIREMENTS.md'), '# Requirements\n'); + fs.writeFileSync(path.join(tmpDir, '0001-decision.md'), '# Decision\n'); + fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true }); + fs.writeFileSync(path.join(tmpDir, 'src', 'PRD.md'), '# Nested Product Requirements\n'); + + const result = runGsdTools('init onboard --raw', tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `init onboard should succeed: ${result.error}`); + + const parsed = JSON.parse(result.output); + assert.strictEqual(parsed.has_docs_candidates, true); + assert.strictEqual(parsed.doc_candidate_count, 6); + assert.deepStrictEqual(parsed.doc_candidates, [ + '0001-decision.md', + 'ADR.md', + 'PRD.md', + 'REQUIREMENTS.md', + 'RFC.md', + 'SPEC.md', + ]); + }); + test('reports complete codebase map and onboarding summary in existing planning', () => { fs.mkdirSync(path.join(tmpDir, '.planning', 'codebase'), { recursive: true }); for (const name of ['STACK', 'ARCHITECTURE', 'STRUCTURE', 'CONVENTIONS', 'TESTING', 'INTEGRATIONS', 'CONCERNS']) { @@ -185,6 +211,17 @@ describe('/gsd:onboard command contract', () => { const content = fs.readFileSync(WF_PATH, 'utf8'); assert.ok(content.includes('init onboard'), 'workflow must use init onboard projection'); + for (const runtimeHome of [ + 'HERMES_HOME', + 'CURSOR_CONFIG_DIR', + 'CODEX_HOME', + 'GEMINI_CONFIG_DIR', + 'WINDSURF_CONFIG_DIR', + 'OPENCODE_CONFIG_DIR', + ]) { + assert.ok(content.includes(runtimeHome), `workflow resolver must include ${runtimeHome}`); + } + assert.match(content, /Parse JSON fields:.*`commit_docs`/s, 'workflow must parse commit_docs before using it'); assert.ok(content.includes('map-codebase'), 'workflow must route to map-codebase'); assert.ok(content.includes('ingest-docs'), 'workflow must route to ingest-docs'); assert.match(