From b3555b103d811787bf0920f153c26282fb214233 Mon Sep 17 00:00:00 2001 From: Jeremy McSpadden Date: Fri, 3 Jul 2026 11:06:45 -0500 Subject: [PATCH] no-mistakes(review): fix onboard fast root anchoring --- gsd-core/workflows/onboard.md | 47 ++++++++++++++++++++++------------ src/init-command-router.cts | 7 +++-- src/init.cts | 24 +++++++++++++++-- tests/onboard-command.test.cjs | 27 +++++++++++++++++++ 4 files changed, 85 insertions(+), 20 deletions(-) diff --git a/gsd-core/workflows/onboard.md b/gsd-core/workflows/onboard.md index be4ffd00c..11c640eba 100644 --- a/gsd-core/workflows/onboard.md +++ b/gsd-core/workflows/onboard.md @@ -23,25 +23,30 @@ Display banner: ``` Parse `$ARGUMENTS`: -- `--fast` sets `MAP_COMMAND` to `/gsd:map-codebase --fast`; otherwise `/gsd:map-codebase`. +- `--fast` sets `MAP_COMMAND` to `/gsd:map-codebase --fast`, passes `--fast` to the init projection, and accepts the default fast map subset (STACK, INTEGRATIONS, ARCHITECTURE, STRUCTURE) as sufficient onboarding context; otherwise `MAP_COMMAND` is `/gsd:map-codebase`. - `--text` sets `TEXT_MODE=true`. 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 "${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) +INIT_ONBOARD_FLAGS=() +if [[ " ${ARGUMENTS:-} " == *" --fast "* ]]; then INIT_ONBOARD_FLAGS=(--fast); fi +INIT=$(gsd_run --cwd "$_GSD_RUNTIME_ROOT" init onboard "${INIT_ONBOARD_FLAGS[@]}") if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi ``` 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`, +`is_brownfield`, `has_codebase_map`, `has_fast_codebase_map`, +`codebase_map_files_present`, `missing_codebase_map_files`, `missing_fast_codebase_map_files`, +`has_docs_candidates`, `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. +Set `CODEBASE_MAP_READY=has_fast_codebase_map` when `--fast` is present; otherwise set +`CODEBASE_MAP_READY=has_codebase_map`. **Text mode (`workflow.text_mode: true` in config or `--text` flag):** When `TEXT_MODE` is active, replace every `AskUserQuestion` call in this workflow with a plain-text numbered list and ask the user to type their choice number. This is required for non-Claude runtimes (OpenAI Codex, Gemini CLI, etc.) where `AskUserQuestion` is not available and would otherwise render as an inert code block. @@ -56,15 +61,15 @@ runs the lower-level refresh commands. ## 3. Codebase Mapping Gate -If `is_brownfield` is true and `has_codebase_map` is false: +If `is_brownfield` is true and `CODEBASE_MAP_READY` is false: - If `TEXT_MODE=true`, print: ```text -Existing code was detected, but the complete .planning/codebase/ map is missing. -Missing map files: {missing_codebase_map_files} +Existing code was detected, but the required .planning/codebase/ map is missing. +Missing map files: {--fast ? missing_fast_codebase_map_files : missing_codebase_map_files} -1. Map codebase first — run {MAP_COMMAND} to understand the repo before project setup (Recommended) +1. Map codebase first — run {MAP_COMMAND} from worktree root {git_worktree_root || _GSD_RUNTIME_ROOT} to understand the repo before project setup (Recommended) 2. Skip mapping — continue with weaker onboarding context Enter number: @@ -74,17 +79,19 @@ Stop and wait for the user's reply. - Otherwise use AskUserQuestion: - header: "Codebase" - - question: "Existing code was detected, but the complete .planning/codebase/ map is missing. Map it first?" + - question: "Existing code was detected, but the required .planning/codebase/ map is missing. Map it first?" - options: - - "Map codebase first" — Run `{MAP_COMMAND}` to understand the repo before project setup (Recommended) + - "Map codebase first" — Run `{MAP_COMMAND}` from worktree root `{git_worktree_root || _GSD_RUNTIME_ROOT}` to understand the repo before project setup (Recommended) - "Skip mapping" — Continue with weaker onboarding context If the user chooses mapping, do not nest the interactive map-codebase workflow. Print: ```text -Run this top-level command first, then rerun /gsd:onboard: +Run from worktree root {git_worktree_root || _GSD_RUNTIME_ROOT}: {MAP_COMMAND} + +Then rerun /gsd:onboard from the same worktree root. ``` Exit. @@ -96,6 +103,8 @@ If `is_brownfield` is false and `planning_exists` is false and `has_docs_candida ```text No existing code was detected. For a greenfield project, run: +Run from worktree root {git_worktree_root || _GSD_RUNTIME_ROOT}: + /gsd:new-project ``` @@ -110,7 +119,7 @@ If `has_docs_candidates` is true and `project_exists` is false: ```text Detected {doc_candidate_count} possible ADR/PRD/SPEC/RFC document(s). -1. Ingest docs first — run /gsd:ingest-docs to bootstrap planning from existing docs (Recommended) +1. Ingest docs first — run /gsd:ingest-docs from worktree root {git_worktree_root || _GSD_RUNTIME_ROOT} to bootstrap planning from existing docs (Recommended) 2. Skip docs ingest — continue to /gsd:new-project Enter number: @@ -122,15 +131,17 @@ Stop and wait for the user's reply. - header: "Docs" - question: "Detected {doc_candidate_count} possible ADR/PRD/SPEC/RFC document(s). Ingest them first?" - options: - - "Ingest docs first" — Run `/gsd:ingest-docs` to bootstrap planning from existing docs (Recommended) + - "Ingest docs first" — Run `/gsd:ingest-docs` from worktree root `{git_worktree_root || _GSD_RUNTIME_ROOT}` to bootstrap planning from existing docs (Recommended) - "Skip docs ingest" — Continue to `/gsd:new-project` If the user chooses ingest, do not nest the interactive ingest-docs workflow. Print: ```text -Run this top-level command first, then rerun /gsd:onboard: +Run from worktree root {git_worktree_root || _GSD_RUNTIME_ROOT}: /gsd:ingest-docs + +Then rerun /gsd:onboard from the same worktree root. ``` Exit. @@ -144,9 +155,11 @@ Print: ```text Codebase context is ready for project initialization. -Run this top-level command, then rerun /gsd:onboard: +Run from worktree root {git_worktree_root || _GSD_RUNTIME_ROOT}: /gsd:new-project + +Then rerun /gsd:onboard from the same worktree root. ``` Exit. @@ -162,10 +175,12 @@ Planning file status: - ROADMAP.md: {roadmap_exists ? "present" : "missing"} - STATE.md: {state_exists ? "present" : "missing"} -Run one of these top-level commands, then rerun /gsd:onboard: +Run one of these top-level commands from worktree root {git_worktree_root || _GSD_RUNTIME_ROOT}: /gsd:ingest-docs --mode merge /gsd:new-milestone + +Then rerun /gsd:onboard from the same worktree root. ``` Exit. Do not write `.planning/onboarding/SUMMARY.md` and do not print the onboarding diff --git a/src/init-command-router.cts b/src/init-command-router.cts index 29b84ea59..90e09e2e6 100644 --- a/src/init-command-router.cts +++ b/src/init-command-router.cts @@ -28,7 +28,7 @@ interface InitModule { cmdInitPlanPhase(cwd: string, phase: string | undefined, raw: boolean, opts: Record): void; cmdInitNewProject(cwd: string, raw: boolean): void; cmdInitNewMilestone(cwd: string, raw: boolean): void; - cmdInitOnboard(cwd: string, raw: boolean): void; + cmdInitOnboard(cwd: string, raw: boolean, opts?: Record): void; cmdInitQuick(cwd: string, name: string, raw: boolean): void; cmdInitIngestDocs(cwd: string, raw: boolean): void; cmdInitResume(cwd: string, raw: boolean): void; @@ -72,7 +72,10 @@ function routeInitCommand({ init, args, cwd, raw, error }: RouteInitCommandOptio }, 'new-project': () => init.cmdInitNewProject(cwd, raw), 'new-milestone': () => init.cmdInitNewMilestone(cwd, raw), - onboard: () => init.cmdInitOnboard(cwd, raw), + onboard: () => { + const namedArgs = parseNamedArgs(args, [], ['fast']); + init.cmdInitOnboard(cwd, raw, { fast: namedArgs['fast'] }); + }, quick: () => init.cmdInitQuick(cwd, args.slice(2).join(' '), raw), 'ingest-docs': () => init.cmdInitIngestDocs(cwd, raw), resume: () => init.cmdInitResume(cwd, raw), diff --git a/src/init.cts b/src/init.cts index b22c29d79..853e186cf 100644 --- a/src/init.cts +++ b/src/init.cts @@ -109,6 +109,10 @@ const REQUIRED_CODEBASE_MAP_FILES = [ 'INTEGRATIONS.md', 'CONCERNS.md', ]; +const FAST_CODEBASE_MAP_FILES = [ + 'STACK.md', 'INTEGRATIONS.md', 'ARCHITECTURE.md', 'STRUCTURE.md', +]; + const PLANNING_DOC_SEGMENTS = new Set([ 'adr', 'adrs', 'prd', 'prds', 'spec', 'specs', 'rfc', 'rfcs', ]); @@ -902,17 +906,29 @@ function cmdInitIngestDocs(cwd: string, raw: boolean): void { output(withProjectRoot(cwd, result), raw); } -function cmdInitOnboard(cwd: string, raw: boolean): void { +function cmdInitOnboard( + cwd: string, + raw: boolean, + options: Record = {}, +): void { const config = loadConfig(cwd); const codebaseMapFiles = listCodebaseMapFiles(cwd); const missingCodebaseMapFiles = REQUIRED_CODEBASE_MAP_FILES.filter( (file) => !codebaseMapFiles.includes(file), ); + const missingFastCodebaseMapFiles = FAST_CODEBASE_MAP_FILES.filter( + (file) => !codebaseMapFiles.includes(file), + ); const docCandidates = listPlanningDocCandidates(cwd); const hasCode = hasCodeFilesInternal(cwd); const hasPackageFile = hasPackageFileInternal(cwd); const isBrownfield = hasCode || hasPackageFile; const hasCodebaseMap = codebaseMapFiles.length === REQUIRED_CODEBASE_MAP_FILES.length; + const fastMode = options['fast'] === true; + const hasFastCodebaseMap = missingFastCodebaseMapFiles.length === 0; + const needsCodebaseMap = isBrownfield && ( + fastMode ? !hasFastCodebaseMap : !hasCodebaseMap + ); const result: Record = { commit_docs: config.commit_docs, @@ -929,11 +945,15 @@ function cmdInitOnboard(cwd: string, raw: boolean): void { has_existing_code: hasCode, has_package_file: hasPackageFile, is_brownfield: isBrownfield, - needs_codebase_map: isBrownfield && !hasCodebaseMap, + fast_mode: fastMode, + needs_codebase_map: needsCodebaseMap, has_codebase_map: hasCodebaseMap, + has_fast_codebase_map: hasFastCodebaseMap, codebase_dir_exists: fs.existsSync(path.join(planningRoot(cwd), 'codebase')), + fast_codebase_map_files_required: FAST_CODEBASE_MAP_FILES, codebase_map_files_present: codebaseMapFiles, missing_codebase_map_files: missingCodebaseMapFiles, + missing_fast_codebase_map_files: missingFastCodebaseMapFiles, has_docs_candidates: docCandidates.length > 0, doc_candidate_count: docCandidates.length, diff --git a/tests/onboard-command.test.cjs b/tests/onboard-command.test.cjs index e202cdd0c..cf1f6ddca 100644 --- a/tests/onboard-command.test.cjs +++ b/tests/onboard-command.test.cjs @@ -138,6 +138,29 @@ describe('init onboard public CLI projection', () => { assert.strictEqual(parsed.text_mode, true); }); + test('reports fast codebase map readiness for the default fast subset', () => { + fs.mkdirSync(path.join(tmpDir, '.planning', 'codebase'), { recursive: true }); + for (const name of ['STACK', 'INTEGRATIONS', 'ARCHITECTURE', 'STRUCTURE']) { + fs.writeFileSync(path.join(tmpDir, '.planning', 'codebase', `${name}.md`), `# ${name}\n`); + } + fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name":"fixture"}\n'); + + const result = runGsdTools(['init', 'onboard', '--fast', '--raw'], tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `init onboard should succeed: ${result.error}`); + + const parsed = JSON.parse(result.output); + assert.strictEqual(parsed.has_codebase_map, false); + assert.strictEqual(parsed.has_fast_codebase_map, true); + assert.strictEqual(parsed.needs_codebase_map, false); + assert.deepStrictEqual(parsed.fast_codebase_map_files_required, [ + 'STACK.md', + 'INTEGRATIONS.md', + 'ARCHITECTURE.md', + 'STRUCTURE.md', + ]); + assert.deepStrictEqual(parsed.missing_fast_codebase_map_files, []); + }); + test('reports missing requirements in otherwise existing planning', () => { fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# Project\n'); @@ -211,6 +234,10 @@ describe('/gsd:onboard command contract', () => { const content = fs.readFileSync(WF_PATH, 'utf8'); assert.ok(content.includes('init onboard'), 'workflow must use init onboard projection'); + assert.match(content, /gsd_run --cwd "\$_GSD_RUNTIME_ROOT" init onboard/, 'workflow must anchor init onboard at runtime root'); + assert.ok(content.includes('has_fast_codebase_map'), 'workflow must parse fast map readiness'); + assert.match(content, /CODEBASE_MAP_READY=.*has_fast_codebase_map/s, 'workflow must treat the fast subset as acceptable in fast mode'); + assert.ok(content.includes('Run from worktree root'), 'workflow handoffs must be anchored at the worktree root'); for (const runtimeHome of [ 'HERMES_HOME', 'CURSOR_CONFIG_DIR',