diff --git a/.gitignore b/.gitignore
index 9562ecd17..c44176af8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -192,6 +192,7 @@ build/
/gsd-core/bin/lib/eval.cjs
/gsd-core/bin/lib/eval-command-router.cjs
/gsd-core/bin/lib/init-command-router.cjs
+/gsd-core/bin/lib/onboard-projection.cjs
/gsd-core/bin/lib/agent-command-router.cjs
/gsd-core/bin/lib/agent-install-check.cjs
/gsd-core/bin/lib/task-command-router.cjs
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 580c90822..a000dd984 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -162,6 +162,7 @@ export default tseslint.config(
'gsd-core/bin/lib/eval.cjs',
'gsd-core/bin/lib/eval-command-router.cjs',
'gsd-core/bin/lib/init-command-router.cjs',
+ 'gsd-core/bin/lib/onboard-projection.cjs',
'gsd-core/bin/lib/agent-command-router.cjs',
'gsd-core/bin/lib/agent-install-check.cjs',
'gsd-core/bin/lib/task-command-router.cjs',
diff --git a/gsd-core/references/gsd-run-resolver.md b/gsd-core/references/gsd-run-resolver.md
new file mode 100644
index 000000000..dd584b755
--- /dev/null
+++ b/gsd-core/references/gsd-run-resolver.md
@@ -0,0 +1,8 @@
+# Standard `gsd_run` Resolver
+
+Use this bootstrap block when a workflow must call `gsd-tools` from the active worktree/runtime.
+Keep this resolver centralized; workflows should reference this file instead of copying the shell probe.
+
+```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
+```
diff --git a/gsd-core/workflows/onboard.md b/gsd-core/workflows/onboard.md
index 6dc5a1f97..d955e3048 100644
--- a/gsd-core/workflows/onboard.md
+++ b/gsd-core/workflows/onboard.md
@@ -1,360 +1,219 @@
-
-Guide existing-codebase onboarding by sequencing the already-owned GSD primitives:
-`/gsd:map-codebase`, `/gsd:ingest-docs`, and `/gsd:new-project`. The workflow is
-idempotent: it confirms existing artifacts, refuses to overwrite planning data silently,
-and stops with the exact next top-level command whenever a nested interactive workflow
-would be unsafe.
-
+# /gsd:onboard Workflow
-
-Read all files referenced by the invoking prompt's execution_context before starting.
-
+One-command onboarding for an existing or unknown repo. This workflow is a thin
+renderer around `init onboard`; deterministic routing lives in the CLI projection.
-
+@~/.claude/gsd-core/references/gsd-run-resolver.md
-## 1. Initialize
-
-Display banner:
-
-```
-━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- GSD ► ONBOARDING
-━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-```
+## 1. Render the Onboarding Projection
Parse `$ARGUMENTS`:
-- `--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`.
+- `--fast` passes `--fast` to `init onboard`. Fast mode accepts the fast map for lightweight onboarding only; `next_action` still decides whether complete map work is required before project setup.
+- `--text` forces text-mode choices for runtimes without `AskUserQuestion`.
-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
-```
-
-Run init from parsed prompt args, not shell `$ARGUMENTS`: `--fast` => `INIT=$(gsd_run --cwd "$_GSD_RUNTIME_ROOT" init onboard --fast)`; default omits `--fast`.
+Run the standard `gsd_run` resolver from the reference above, then run the projection from the runtime root:
```bash
+# If --fast was parsed from $ARGUMENTS:
+INIT=$(gsd_run --cwd "$_GSD_RUNTIME_ROOT" init onboard --fast --raw)
+# Otherwise:
+INIT=$(gsd_run --cwd "$_GSD_RUNTIME_ROOT" init onboard --raw)
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`, `fast_mode`, `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`.
+Parse JSON fields from `INIT`:
-Set `FAST_MODE=fast_mode` from INIT.
-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_MODE` is true; otherwise set
-`CODEBASE_MAP_READY=has_codebase_map`.
+- `next_action.kind`, `next_action.command`, `next_action.reason`, `next_action.missing`, `next_action.summary_path`
+- `map_readiness`, `codebase_map_summary_status`, `codebase_map_final_status`
+- `planning_exists`, `project_exists`, `requirements_exists`, `roadmap_exists`, `state_exists`
+- `is_brownfield`, `fast_mode`, `has_codebase_map`, `has_fast_codebase_map`
+- `missing_codebase_map_files`, `missing_fast_codebase_map_files`
+- `has_docs_candidates`, `doc_candidate_count`, `onboarding_summary_exists`
+- `commit_docs`, `text_mode`, `has_git`, `git_worktree_root`, `in_nested_subdir`
-**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.
+Set:
+- `TEXT_MODE=true` if `--text` is present or `text_mode` is true.
+- `ONBOARDING_ROOT={git_worktree_root || _GSD_RUNTIME_ROOT}`.
-## 2. Git and Existing Planning Safety
+If `has_git` and `in_nested_subdir` are true, warn that onboarding artifacts belong to the outer worktree at `git_worktree_root`. Do not run `git init`.
-If `has_git` is true and `in_nested_subdir` is true, warn that onboarding artifacts will
-belong to the outer worktree at `git_worktree_root`. Do not run `git init` here.
+## 2. Execute `next_action`
-If `planning_exists` is true, do not overwrite PROJECT/ROADMAP/STATE. Onboarding is
-idempotent and must only confirm or summarize existing files unless the user explicitly
-runs the lower-level refresh commands.
+### `map-codebase`
-## 3. Codebase Mapping Gate
-
-If `is_brownfield` is true and `CODEBASE_MAP_READY` is false:
+If `next_action.kind == "map-codebase"`:
- If `TEXT_MODE=true`, print:
```text
-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}
+{next_action.reason}
+Missing map files: {fast_mode ? missing_fast_codebase_map_files : missing_codebase_map_files}
-1. Map codebase first — run {MAP_COMMAND} from worktree root {git_worktree_root || _GSD_RUNTIME_ROOT} to understand the repo before project setup (Recommended)
+1. Map codebase first — run {next_action.command} from worktree root {ONBOARDING_ROOT} (Recommended)
2. Skip mapping — continue with weaker onboarding context
Enter number:
```
-Stop and wait for the user's reply.
-
- Otherwise use AskUserQuestion:
- header: "Codebase"
- - question: "Existing code was detected, but the required .planning/codebase/ map is missing. Map it first?"
+ - question: "{next_action.reason} Map it first?"
- options:
- - "Map codebase first" — Run `{MAP_COMMAND}` from worktree root `{git_worktree_root || _GSD_RUNTIME_ROOT}` to understand the repo before project setup (Recommended)
+ - "Map codebase first" — Run `{next_action.command}` from worktree root `{ONBOARDING_ROOT}` (Recommended)
- "Skip mapping" — Continue with weaker onboarding context
-If the user chooses mapping, do not nest the interactive map-codebase workflow. Print:
+If the user chooses mapping, do not nest the interactive workflow. Print:
```text
-Run from worktree root {git_worktree_root || _GSD_RUNTIME_ROOT}:
+Run from worktree root {ONBOARDING_ROOT}:
-{MAP_COMMAND}
+{next_action.command}
Then rerun /gsd:onboard from the same worktree root.
```
-Exit.
+Exit. If the user skips mapping, continue with a warning.
-If the user skips mapping, continue with a warning.
+### `ingest-docs`
-If `is_brownfield` is false and `planning_exists` is false and `has_docs_candidates` is false, print:
-
-```text
-No existing code was detected. For a greenfield project, run:
-
-Run from worktree root {git_worktree_root || _GSD_RUNTIME_ROOT}:
-
-/gsd:new-project
-```
-
-Exit.
-
-## 4. Existing Docs Gate
-
-If `has_docs_candidates` is true and `project_exists` is false:
+If `next_action.kind == "ingest-docs"`:
- If `TEXT_MODE=true`, print:
```text
+{next_action.reason}
Detected {doc_candidate_count} possible ADR/PRD/SPEC/RFC document(s).
-1. Ingest docs first — run /gsd:ingest-docs from worktree root {git_worktree_root || _GSD_RUNTIME_ROOT} to bootstrap planning from existing docs (Recommended)
+1. Ingest docs first — run {next_action.command} from worktree root {ONBOARDING_ROOT} (Recommended)
2. Skip docs ingest — continue to /gsd:new-project
Enter number:
```
-Stop and wait for the user's reply.
-
- Otherwise use AskUserQuestion:
- 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` from worktree root `{git_worktree_root || _GSD_RUNTIME_ROOT}` to bootstrap planning from existing docs (Recommended)
+ - "Ingest docs first" — Run `{next_action.command}` from worktree root `{ONBOARDING_ROOT}` (Recommended)
- "Skip docs ingest" — Continue to `/gsd:new-project`
-If the user chooses ingest, do not nest the interactive ingest-docs workflow. Print:
+If the user chooses ingest, print:
```text
-Run from worktree root {git_worktree_root || _GSD_RUNTIME_ROOT}:
+Run from worktree root {ONBOARDING_ROOT}:
-/gsd:ingest-docs
+{next_action.command}
+
+Then rerun /gsd:onboard from the same worktree root.
+```
+
+Exit. If the user skips docs ingest, continue to the new-project handoff below.
+
+### `complete-map-before-new-project`
+
+If `next_action.kind == "complete-map-before-new-project"`, print:
+
+```text
+{next_action.reason}
+
+Run from worktree root {ONBOARDING_ROOT}:
+
+{next_action.command}
Then rerun /gsd:onboard from the same worktree root.
```
Exit.
-## 5. Project Initialization Gate
+### `new-project`
-If `project_exists` is false and `fast_mode && has_fast_codebase_map && !has_codebase_map` is true:
-
-Print:
+If `next_action.kind == "new-project"`, print:
```text
-Fast codebase context is accepted for onboarding, but /gsd:new-project still requires the complete codebase map.
+{next_action.reason}
-Run from worktree root {git_worktree_root || _GSD_RUNTIME_ROOT}:
+Run from worktree root {ONBOARDING_ROOT}:
-/gsd:map-codebase
-
-Then rerun /gsd:onboard from the same worktree root.
+{next_action.command}
```
Exit.
-If `project_exists` is false:
+### `partial-planning`
-Print:
+If `next_action.kind == "partial-planning"`, print:
```text
-Codebase context is ready for project initialization.
+Project planning exists but is incomplete.
-Run from worktree root {git_worktree_root || _GSD_RUNTIME_ROOT}:
+Missing files: {next_action.missing}
+REQUIREMENTS.md: {requirements_exists ? "present" : "missing"}
+ROADMAP.md: {roadmap_exists ? "present" : "missing"}
+STATE.md: {state_exists ? "present" : "missing"}
-/gsd:new-project
-
-Then rerun /gsd:onboard from the same worktree root.
+Run the appropriate lower-level command to fill the missing planning artifact(s), then rerun /gsd:onboard.
```
Exit.
-If `project_exists` is true and either `requirements_exists`, `roadmap_exists`, or
-`state_exists` is false, print:
+### `ready`
-```text
-Existing PROJECT.md was found, but planning is incomplete.
+If `next_action.kind == "ready"`, print the final status section and exit.
-Planning file status:
-- REQUIREMENTS.md: {requirements_exists ? "present" : "missing"}
-- ROADMAP.md: {roadmap_exists ? "present" : "missing"}
-- STATE.md: {state_exists ? "present" : "missing"}
+### `write-summary`
-Run one of these top-level commands from worktree root {git_worktree_root || _GSD_RUNTIME_ROOT}:
+If `next_action.kind == "write-summary"`, continue to summary creation.
-/gsd:ingest-docs --mode merge
-/gsd:new-milestone
+## 3. Create Onboarding Summary
-Then rerun /gsd:onboard from the same worktree root.
-```
+Create `{ONBOARDING_ROOT}/{next_action.summary_path}`. Do not overwrite an existing summary; the projection should only route here when the summary is missing.
-Exit. Do not write `.planning/onboarding/SUMMARY.md` and do not print the onboarding
-complete status for partial planning.
-
-If `project_exists`, `requirements_exists`, `roadmap_exists`, and `state_exists` are all
-true, continue.
-
-## 6. Write Onboarding Summary
-
-Resolve `ONBOARDING_ROOT={git_worktree_root || _GSD_RUNTIME_ROOT}` before this step.
-Use `ONBOARDING_ROOT` for every filesystem write and commit in this step so nested-cwd
-invocations do not create `subdir/.planning` artifacts.
-
-Create `{ONBOARDING_ROOT}/.planning/onboarding/SUMMARY.md` only after PROJECT.md exists.
-If it already exists, update it only after confirmation; do not overwrite silently.
-
-Before summary keep/update choices, derive map status values safely:
-
-- If `has_codebase_map` is true:
- - `CODEBASE_MAP_SUMMARY_STATUS=.planning/codebase/ (complete)`
- - `CODEBASE_MAP_FINAL_STATUS=(complete)`
-- Else if `fast_mode && has_fast_codebase_map` is true:
- - `CODEBASE_MAP_SUMMARY_STATUS=.planning/codebase/ (fast/partial-but-accepted codebase map)`
- - `CODEBASE_MAP_FINAL_STATUS=(fast/partial-but-accepted; required fast files present)`
-- Else:
- - `CODEBASE_MAP_SUMMARY_STATUS=.planning/codebase/ (incomplete or skipped)`
- - `CODEBASE_MAP_FINAL_STATUS=(incomplete or skipped; missing: {missing_codebase_map_files})`
-
-If `onboarding_summary_exists` is true:
-
-- If `TEXT_MODE=true`, print:
-
-```text
-Onboarding summary already exists at .planning/onboarding/SUMMARY.md.
-
-1. Update summary — regenerate from current artifact status (Recommended)
-2. Keep existing summary — skip writing
-
-Enter number:
-```
-
-Stop and wait for the user's reply.
-
-- Otherwise use AskUserQuestion:
- - header: "Onboarding Summary"
- - question: "Onboarding summary already exists. Update it from current artifact status?"
- - options:
- - "Update summary" — Regenerate `.planning/onboarding/SUMMARY.md` (Recommended)
- - "Keep existing summary" — Skip writing
-
-If the user chooses to keep the existing summary, skip to Step 7 without writing.
-
-If `onboarding_summary_exists` is false or the user confirms an update, write the summary
-using the template below.
-
-Summary contents:
+Summary template:
```markdown
# Onboarding Summary
-**Generated:** {YYYY-MM-DD}
-**Status:** Ready for GSD workflow
+## Project State
+- PROJECT.md: {project_exists ? "present" : "missing"}
+- REQUIREMENTS.md: {requirements_exists ? "present" : "missing"}
+- ROADMAP.md: {roadmap_exists ? "present" : "missing"}
+- STATE.md: {state_exists ? "present" : "missing"}
-## Artifact Status
+## Codebase Context
+- Brownfield repo: {is_brownfield ? "yes" : "no"}
+- Map readiness: {map_readiness}
+- Codebase map: {codebase_map_summary_status}
+- Fast map available: {has_fast_codebase_map ? "yes" : "no"}
-- Project: .planning/PROJECT.md
-- Requirements: .planning/REQUIREMENTS.md
-- Roadmap: .planning/ROADMAP.md
-- State: .planning/STATE.md
-- Codebase map: {CODEBASE_MAP_SUMMARY_STATUS}
-
-## Codebase Map
-
-{if has_codebase_map}
-Confirmed files:
-- STACK.md
-- ARCHITECTURE.md
-- STRUCTURE.md
-- CONVENTIONS.md
-- TESTING.md
-- INTEGRATIONS.md
-- CONCERNS.md
-{else if fast_mode && has_fast_codebase_map}
-Accepted fast map files:
-- STACK.md
-- INTEGRATIONS.md
-- ARCHITECTURE.md
-- STRUCTURE.md
-Context strength: fast/partial-but-accepted codebase map; not all seven map files are present.
-{else}
-Present files: {codebase_map_files_present}
-Missing files: {missing_codebase_map_files}
-Context strength: weaker because codebase mapping was skipped or is incomplete.
-{endif}
-
-## Existing Docs
-
-Detected planning docs: {doc_candidate_count}
+## Docs Context
+- Existing ADR/PRD/SPEC/RFC candidates: {has_docs_candidates ? doc_candidate_count : 0}
## Recommended Next Step
-
-/gsd:discuss-phase 1
+- {next_action.reason}
```
-Commit the summary if `commit_docs` is true:
+If `commit_docs` is true, commit only the summary path from the onboarding root:
```bash
gsd_run --cwd "$ONBOARDING_ROOT" query commit "docs: create onboarding summary" --files .planning/onboarding/SUMMARY.md
```
-## 7. Final Status
+Continue to final status.
+
+## 4. Final Status
Print:
-```
-━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- GSD ► ONBOARDING COMPLETE ✓
-━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+```text
+Onboarding status:
+- PROJECT.md: {project_exists ? "present" : "missing"}
+- REQUIREMENTS.md: {requirements_exists ? "present" : "missing"}
+- ROADMAP.md: {roadmap_exists ? "present" : "missing"}
+- STATE.md: {state_exists ? "present" : "missing"}
+- Codebase map: {codebase_map_final_status}
+- Onboarding summary: present
-Created / confirmed / status:
-- .planning/codebase/ {CODEBASE_MAP_FINAL_STATUS}
-- .planning/PROJECT.md
-- .planning/REQUIREMENTS.md {requirements_exists ? "(present)" : "(missing; onboarding incomplete)"}
-- .planning/ROADMAP.md
-- .planning/STATE.md
-- .planning/onboarding/SUMMARY.md
-
-───────────────────────────────────────────────────────────────
-
-## ▶ Next Up
-
-**Discuss Phase 1** — capture implementation decisions before planning.
-
-`/gsd:discuss-phase 1`
-
-───────────────────────────────────────────────────────────────
-
-**Also available:**
-- `/gsd:map-codebase` — refresh the codebase map after significant changes
-- `/gsd:ingest-docs --mode merge` — merge new ADR/PRD/SPEC docs into planning
-- `/gsd:progress` — inspect current workflow state
-
-───────────────────────────────────────────────────────────────
+Next recommended command: /gsd:manager
```
-
-
-
-- [ ] Existing codebase detected before project initialization
-- [ ] Missing codebase map routes to `/gsd:map-codebase` or `/gsd:map-codebase --fast`
-- [ ] Existing planning docs route to `/gsd:ingest-docs`
-- [ ] Missing project setup routes to `/gsd:new-project`
-- [ ] Existing `.planning/` files are not overwritten silently
-- [ ] Text mode stops and waits at numbered-list gates
-- [ ] `.planning/onboarding/SUMMARY.md` is created only after PROJECT.md exists
-- [ ] User sees next command
-
+Do not run implementation execution or shipping from onboarding.
diff --git a/src/init.cts b/src/init.cts
index 853e186cf..5c781ba25 100644
--- a/src/init.cts
+++ b/src/init.cts
@@ -51,6 +51,15 @@ const { checkAgentsInstalled } = agentInstallCheck;
import gitBaseBranch = require('./git-base-branch.cjs');
const { gitWorktreeInfoInternal } = gitBaseBranch;
import { makeResolution } from './resolution.cjs';
+// eslint-disable-next-line @typescript-eslint/no-require-imports -- onboard-projection.cjs is an export= CommonJS module
+import onboardProjection = require('./onboard-projection.cjs');
+const {
+ REQUIRED_CODEBASE_MAP_FILES,
+ buildOnboardProjection,
+ hasCodeFilesInternal,
+ hasPackageFileInternal,
+ listCodebaseMapFiles,
+} = onboardProjection;
const { output, error } = io;
const { loadConfig, loadConfigResolved } = configLoader;
@@ -86,130 +95,6 @@ void stripShippedMilestones;
// Accept all bold/colon variants of the Requirements header (#2769)
const REQUIREMENTS_HEADER_RE = /^\*\*Requirements:?\*\*[^\S\n]*:?[^\S\n]*([^\n]*)$/m;
-const CODE_EXTENSIONS = new Set([
- '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs', '.swift', '.java',
- '.kt', '.kts', '.c', '.cpp', '.cc', '.h', '.hpp', '.cs', '.rb', '.php', '.dart',
- '.m', '.mm', '.scala', '.groovy', '.lua', '.r', '.R', '.zig', '.ex', '.exs', '.clj',
-]);
-
-const CODE_SCAN_SKIP_DIRS = new Set([
- 'node_modules', '.git', '.planning', '.claude', '.codex', '__pycache__', 'target',
- 'dist', 'build', '.next', '.nuxt', '.svelte-kit', 'coverage', 'vendor', '.venv', 'venv',
-]);
-
-const PACKAGE_FILES = [
- 'package.json', 'requirements.txt', 'pyproject.toml', 'Cargo.toml', 'go.mod',
- 'Package.swift', 'build.gradle', 'build.gradle.kts', 'pom.xml', 'Gemfile',
- 'composer.json', 'pubspec.yaml', 'CMakeLists.txt', 'Makefile', 'build.zig',
- 'mix.exs', 'project.clj',
-];
-
-const REQUIRED_CODEBASE_MAP_FILES = [
- 'STACK.md', 'ARCHITECTURE.md', 'STRUCTURE.md', 'CONVENTIONS.md', 'TESTING.md',
- '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',
-]);
-
-function hasCodeFilesInternal(dir: string, depth = 0): boolean {
- if (depth > 3) return false;
- let entries: fs.Dirent[];
- try {
- entries = fs.readdirSync(dir, { withFileTypes: true });
- } catch {
- return false;
- }
-
- for (const entry of entries) {
- if (entry.isFile() && CODE_EXTENSIONS.has(path.extname(entry.name))) return true;
- if (entry.isDirectory() && !CODE_SCAN_SKIP_DIRS.has(entry.name)) {
- if (hasCodeFilesInternal(path.join(dir, entry.name), depth + 1)) return true;
- }
- }
-
- return false;
-}
-
-function hasPackageFileInternal(cwd: string): boolean {
- return PACKAGE_FILES.some((file) => pathExistsInternal(cwd, file));
-}
-
-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[];
- try {
- entries = fs.readdirSync(dir, { withFileTypes: true });
- } catch {
- return;
- }
-
- for (const entry of entries) {
- const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
- if (entry.isDirectory()) {
- if (!CODE_SCAN_SKIP_DIRS.has(entry.name)) {
- visit(path.join(dir, entry.name), rel, depth + 1);
- }
- continue;
- }
-
- 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);
- }
-
- return [...candidates].sort();
-}
-
-function listCodebaseMapFiles(cwd: string): string[] {
- const codebaseDir = path.join(planningRoot(cwd), 'codebase');
- if (!fs.existsSync(codebaseDir)) return [];
- return REQUIRED_CODEBASE_MAP_FILES.filter((file) =>
- fs.existsSync(path.join(codebaseDir, file)),
- );
-}
-
function listPhaseSummaryFiles(phaseDir: string): string[] {
return (scanPhasePlans(phaseDir) as unknown as Record)['summaryFiles'];
}
@@ -912,65 +797,13 @@ function cmdInitOnboard(
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,
- text_mode:
- !!config.text_mode || !!((config.workflow ?? {}) as Record)['text_mode'],
-
- project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
- planning_exists: fs.existsSync(planningRoot(cwd)),
- requirements_exists: fs.existsSync(path.join(planningDir(cwd), 'REQUIREMENTS.md')),
- roadmap_exists: fs.existsSync(path.join(planningDir(cwd), 'ROADMAP.md')),
- state_exists: fs.existsSync(path.join(planningDir(cwd), 'STATE.md')),
- config_exists: fs.existsSync(path.join(planningDir(cwd), 'config.json')),
-
- has_existing_code: hasCode,
- has_package_file: hasPackageFile,
- is_brownfield: isBrownfield,
- 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,
- doc_candidates: docCandidates,
-
- onboarding_summary_exists: pathExistsInternal(cwd, '.planning/onboarding/SUMMARY.md'),
- onboarding_summary_path: '.planning/onboarding/SUMMARY.md',
-
- project_path: '.planning/PROJECT.md',
- requirements_path: toPosixPath(
- path.relative(cwd, path.join(planningDir(cwd), 'REQUIREMENTS.md')),
- ),
- roadmap_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'ROADMAP.md'))),
- state_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'STATE.md'))),
- codebase_dir: toPosixPath(path.relative(cwd, path.join(planningRoot(cwd), 'codebase'))),
- onboarding_dir: toPosixPath(path.relative(cwd, path.join(planningRoot(cwd), 'onboarding'))),
-
+ const workflowConfig = (config.workflow ?? {}) as Record;
+ const result = {
+ ...buildOnboardProjection(cwd, {
+ commitDocs: !!config.commit_docs,
+ fast: options['fast'] === true,
+ textMode: !!config.text_mode || !!workflowConfig['text_mode'],
+ }),
...getInitGitState(cwd),
};
diff --git a/src/onboard-projection.cts b/src/onboard-projection.cts
new file mode 100644
index 000000000..26ba11305
--- /dev/null
+++ b/src/onboard-projection.cts
@@ -0,0 +1,383 @@
+import fs from 'node:fs';
+import path from 'node:path';
+
+// eslint-disable-next-line @typescript-eslint/no-require-imports -- core-utils.cjs is an export= CommonJS module
+import coreUtils = require('./core-utils.cjs');
+// eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
+import planningWorkspace = require('./planning-workspace.cjs');
+
+const { pathExistsInternal, toPosixPath } = coreUtils;
+const { planningDir, planningRoot } = planningWorkspace;
+
+const CODE_EXTENSIONS = new Set([
+ '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs', '.swift', '.java',
+ '.kt', '.kts', '.c', '.cpp', '.cc', '.h', '.hpp', '.cs', '.rb', '.php', '.dart',
+ '.m', '.mm', '.scala', '.groovy', '.lua', '.r', '.R', '.zig', '.ex', '.exs', '.clj',
+]);
+
+const CODE_SCAN_SKIP_DIRS = new Set([
+ 'node_modules', '.git', '.planning', '.claude', '.codex', '__pycache__', 'target',
+ 'dist', 'build', '.next', '.nuxt', '.svelte-kit', 'coverage', 'vendor', '.venv', 'venv',
+]);
+
+const PACKAGE_FILES = [
+ 'package.json', 'requirements.txt', 'pyproject.toml', 'Cargo.toml', 'go.mod',
+ 'Package.swift', 'build.gradle', 'build.gradle.kts', 'pom.xml', 'Gemfile',
+ 'composer.json', 'pubspec.yaml', 'CMakeLists.txt', 'Makefile', 'build.zig',
+ 'mix.exs', 'project.clj',
+];
+
+const REQUIRED_CODEBASE_MAP_FILES = [
+ 'STACK.md', 'ARCHITECTURE.md', 'STRUCTURE.md', 'CONVENTIONS.md', 'TESTING.md',
+ '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',
+]);
+
+type MapReadiness = 'none' | 'fast' | 'complete';
+
+type OnboardNextAction =
+ | { kind: 'map-codebase'; command: string; reason: string }
+ | { kind: 'ingest-docs'; command: string; reason: string }
+ | { kind: 'new-project'; command: string; reason: string }
+ | { kind: 'complete-map-before-new-project'; command: string; reason: string }
+ | { kind: 'partial-planning'; missing: string[]; reason: string }
+ | { kind: 'write-summary'; summary_path: string; reason: string }
+ | { kind: 'ready'; reason: string };
+
+interface BuildOnboardProjectionOptions {
+ commitDocs: boolean;
+ fast: boolean;
+ textMode: boolean;
+}
+
+interface OnboardProjection {
+ commit_docs: boolean;
+ text_mode: boolean;
+
+ project_exists: boolean;
+ planning_exists: boolean;
+ requirements_exists: boolean;
+ roadmap_exists: boolean;
+ state_exists: boolean;
+ config_exists: boolean;
+
+ has_existing_code: boolean;
+ has_package_file: boolean;
+ is_brownfield: boolean;
+ fast_mode: boolean;
+ map_readiness: MapReadiness;
+ next_action: OnboardNextAction;
+ needs_codebase_map: boolean;
+ has_codebase_map: boolean;
+ has_fast_codebase_map: boolean;
+ codebase_dir_exists: boolean;
+ fast_codebase_map_files_required: string[];
+ codebase_map_files_present: string[];
+ missing_codebase_map_files: string[];
+ missing_fast_codebase_map_files: string[];
+ codebase_map_summary_status: string;
+ codebase_map_final_status: string;
+
+ has_docs_candidates: boolean;
+ doc_candidate_count: number;
+ doc_candidates: string[];
+
+ onboarding_summary_exists: boolean;
+ onboarding_summary_path: string;
+
+ project_path: string;
+ requirements_path: string;
+ roadmap_path: string;
+ state_path: string;
+ codebase_dir: string;
+ onboarding_dir: string;
+}
+
+function hasCodeFilesInternal(dir: string, depth = 0): boolean {
+ if (depth > 3) return false;
+ let entries: fs.Dirent[];
+ try {
+ entries = fs.readdirSync(dir, { withFileTypes: true });
+ } catch {
+ return false;
+ }
+
+ for (const entry of entries) {
+ if (entry.isFile() && CODE_EXTENSIONS.has(path.extname(entry.name))) return true;
+ if (entry.isDirectory() && !CODE_SCAN_SKIP_DIRS.has(entry.name)) {
+ if (hasCodeFilesInternal(path.join(dir, entry.name), depth + 1)) return true;
+ }
+ }
+
+ return false;
+}
+
+function hasPackageFileInternal(cwd: string): boolean {
+ return PACKAGE_FILES.some((file) => pathExistsInternal(cwd, file));
+}
+
+function listPlanningDocCandidates(cwd: string): string[] {
+ const roots = ['docs', 'adr', 'adrs', 'prd', 'prds', 'spec', 'specs', 'rfc', 'rfcs'];
+ const candidates = new Set();
+
+ function 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'
+ );
+ }
+
+ function addCandidate(rel: string, name: string): void {
+ if (name.toLowerCase().endsWith('.md') && isPlanningDocCandidate(rel, name)) {
+ candidates.add(toPosixPath(rel));
+ }
+ }
+
+ function visit(dir: string, relDir: string, depth: number): void {
+ if (depth > 3) return;
+ let entries: fs.Dirent[];
+ try {
+ entries = fs.readdirSync(dir, { withFileTypes: true });
+ } catch {
+ return;
+ }
+
+ for (const entry of entries) {
+ const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
+ if (entry.isDirectory()) {
+ if (!CODE_SCAN_SKIP_DIRS.has(entry.name)) {
+ visit(path.join(dir, entry.name), rel, depth + 1);
+ }
+ continue;
+ }
+
+ 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);
+ }
+
+ return [...candidates].sort();
+}
+
+function listCodebaseMapFiles(cwd: string): string[] {
+ const codebaseDir = path.join(planningRoot(cwd), 'codebase');
+ if (!fs.existsSync(codebaseDir)) return [];
+ return REQUIRED_CODEBASE_MAP_FILES.filter((file) =>
+ fs.existsSync(path.join(codebaseDir, file)),
+ );
+}
+
+function getMapReadiness(hasCompleteMap: boolean, hasFastMap: boolean): MapReadiness {
+ if (hasCompleteMap) return 'complete';
+ if (hasFastMap) return 'fast';
+ return 'none';
+}
+
+function mapSummaryStatus(mapReadiness: MapReadiness): string {
+ if (mapReadiness === 'complete') return '.planning/codebase/ (complete codebase map)';
+ if (mapReadiness === 'fast') return '.planning/codebase/ (fast/partial codebase map; complete map still required for project setup)';
+ return 'missing';
+}
+
+function mapFinalStatus(mapReadiness: MapReadiness): string {
+ if (mapReadiness === 'complete') return 'complete';
+ if (mapReadiness === 'fast') return 'fast/partial; complete map still required for project setup';
+ return 'missing';
+}
+
+function planningMissing(requirementsExists: boolean, roadmapExists: boolean, stateExists: boolean): string[] {
+ const missing: string[] = [];
+ if (!requirementsExists) missing.push('REQUIREMENTS.md');
+ if (!roadmapExists) missing.push('ROADMAP.md');
+ if (!stateExists) missing.push('STATE.md');
+ return missing;
+}
+
+function nextAction(params: {
+ fastMode: boolean;
+ isBrownfield: boolean;
+ needsCodebaseMap: boolean;
+ hasDocsCandidates: boolean;
+ projectExists: boolean;
+ mapReadiness: MapReadiness;
+ onboardingSummaryExists: boolean;
+ missingPlanningFiles: string[];
+}): OnboardNextAction {
+ if (params.isBrownfield && params.needsCodebaseMap) {
+ return {
+ kind: 'map-codebase',
+ command: params.fastMode ? '/gsd:map-codebase --fast' : '/gsd:map-codebase',
+ reason: 'Existing code was detected, but the required .planning/codebase/ map is missing.',
+ };
+ }
+
+ if (params.hasDocsCandidates && !params.projectExists) {
+ return {
+ kind: 'ingest-docs',
+ command: '/gsd:ingest-docs',
+ reason: 'Detected existing ADR/PRD/SPEC/RFC document(s) before project setup.',
+ };
+ }
+
+ if (!params.isBrownfield && !params.projectExists && !params.hasDocsCandidates) {
+ return {
+ kind: 'new-project',
+ command: '/gsd:new-project',
+ reason: 'No existing code or planning docs were detected.',
+ };
+ }
+
+ if (!params.projectExists && params.fastMode && params.mapReadiness === 'fast') {
+ return {
+ kind: 'complete-map-before-new-project',
+ command: '/gsd:map-codebase',
+ reason: 'The fast map is enough for lightweight onboarding, but project setup still requires the complete codebase map.',
+ };
+ }
+
+ if (!params.projectExists) {
+ return {
+ kind: 'new-project',
+ command: '/gsd:new-project',
+ reason: 'Codebase context is ready for project initialization.',
+ };
+ }
+
+ if (params.missingPlanningFiles.length > 0) {
+ return {
+ kind: 'partial-planning',
+ missing: params.missingPlanningFiles,
+ reason: 'Project planning exists but required planning files are missing.',
+ };
+ }
+
+ if (!params.onboardingSummaryExists) {
+ return {
+ kind: 'write-summary',
+ summary_path: '.planning/onboarding/SUMMARY.md',
+ reason: 'Onboarding summary is missing.',
+ };
+ }
+
+ return {
+ kind: 'ready',
+ reason: 'Onboarding summary already exists.',
+ };
+}
+
+function buildOnboardProjection(cwd: string, options: BuildOnboardProjectionOptions): OnboardProjection {
+ 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 hasFastCodebaseMap = missingFastCodebaseMapFiles.length === 0;
+ const mapReadinessValue = getMapReadiness(hasCodebaseMap, hasFastCodebaseMap);
+ const needsCodebaseMap = isBrownfield && (
+ options.fast ? !hasFastCodebaseMap : !hasCodebaseMap
+ );
+ const projectExists = pathExistsInternal(cwd, '.planning/PROJECT.md');
+ const requirementsExists = fs.existsSync(path.join(planningDir(cwd), 'REQUIREMENTS.md'));
+ const roadmapExists = fs.existsSync(path.join(planningDir(cwd), 'ROADMAP.md'));
+ const stateExists = fs.existsSync(path.join(planningDir(cwd), 'STATE.md'));
+ const onboardingSummaryExists = pathExistsInternal(cwd, '.planning/onboarding/SUMMARY.md');
+ const missingPlanningFiles = planningMissing(requirementsExists, roadmapExists, stateExists);
+
+ return {
+ commit_docs: options.commitDocs,
+ text_mode: options.textMode,
+
+ project_exists: projectExists,
+ planning_exists: fs.existsSync(planningRoot(cwd)),
+ requirements_exists: requirementsExists,
+ roadmap_exists: roadmapExists,
+ state_exists: stateExists,
+ config_exists: fs.existsSync(path.join(planningDir(cwd), 'config.json')),
+
+ has_existing_code: hasCode,
+ has_package_file: hasPackageFile,
+ is_brownfield: isBrownfield,
+ fast_mode: options.fast,
+ map_readiness: mapReadinessValue,
+ next_action: nextAction({
+ fastMode: options.fast,
+ isBrownfield,
+ needsCodebaseMap,
+ hasDocsCandidates: docCandidates.length > 0,
+ projectExists,
+ mapReadiness: mapReadinessValue,
+ onboardingSummaryExists,
+ missingPlanningFiles,
+ }),
+ 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,
+ codebase_map_summary_status: mapSummaryStatus(mapReadinessValue),
+ codebase_map_final_status: mapFinalStatus(mapReadinessValue),
+
+ has_docs_candidates: docCandidates.length > 0,
+ doc_candidate_count: docCandidates.length,
+ doc_candidates: docCandidates,
+
+ onboarding_summary_exists: onboardingSummaryExists,
+ onboarding_summary_path: '.planning/onboarding/SUMMARY.md',
+
+ project_path: '.planning/PROJECT.md',
+ requirements_path: toPosixPath(
+ path.relative(cwd, path.join(planningDir(cwd), 'REQUIREMENTS.md')),
+ ),
+ roadmap_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'ROADMAP.md'))),
+ state_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'STATE.md'))),
+ codebase_dir: toPosixPath(path.relative(cwd, path.join(planningRoot(cwd), 'codebase'))),
+ onboarding_dir: toPosixPath(path.relative(cwd, path.join(planningRoot(cwd), 'onboarding'))),
+ };
+}
+
+export = {
+ REQUIRED_CODEBASE_MAP_FILES,
+ FAST_CODEBASE_MAP_FILES,
+ buildOnboardProjection,
+ hasCodeFilesInternal,
+ hasPackageFileInternal,
+ listCodebaseMapFiles,
+ listPlanningDocCandidates,
+};
diff --git a/tests/onboard-command.test.cjs b/tests/onboard-command.test.cjs
index 2201b3730..30bb98716 100644
--- a/tests/onboard-command.test.cjs
+++ b/tests/onboard-command.test.cjs
@@ -138,7 +138,7 @@ describe('init onboard public CLI projection', () => {
assert.strictEqual(parsed.text_mode, true);
});
- test('reports fast codebase map readiness for the default fast subset', () => {
+ test('reports fast codebase map readiness and a complete-map handoff before new-project', () => {
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`);
@@ -149,9 +149,13 @@ describe('init onboard public CLI projection', () => {
assert.ok(result.success, `init onboard should succeed: ${result.error}`);
const parsed = JSON.parse(result.output);
+ assert.strictEqual(parsed.map_readiness, 'fast');
assert.strictEqual(parsed.has_codebase_map, false);
assert.strictEqual(parsed.has_fast_codebase_map, true);
assert.strictEqual(parsed.needs_codebase_map, false);
+ assert.strictEqual(parsed.next_action.kind, 'complete-map-before-new-project');
+ assert.strictEqual(parsed.next_action.command, '/gsd:map-codebase');
+ assert.match(parsed.next_action.reason, /complete codebase map/i);
assert.deepStrictEqual(parsed.fast_codebase_map_files_required, [
'STACK.md',
'INTEGRATIONS.md',
@@ -161,6 +165,74 @@ describe('init onboard public CLI projection', () => {
assert.deepStrictEqual(parsed.missing_fast_codebase_map_files, []);
});
+ test('projects the next action for code, docs, greenfield, partial planning, and summary states', () => {
+ fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true });
+ fs.writeFileSync(path.join(tmpDir, 'src', 'server.ts'), 'export const server = true;\n');
+ let result = runGsdTools('init onboard --raw', tmpDir, { HOME: tmpDir });
+ assert.ok(result.success, `init onboard should succeed: ${result.error}`);
+ assert.deepStrictEqual(JSON.parse(result.output).next_action, {
+ kind: 'map-codebase',
+ command: '/gsd:map-codebase',
+ reason: 'Existing code was detected, but the required .planning/codebase/ map is missing.',
+ });
+
+ cleanup(tmpDir);
+ tmpDir = createFixture({ planning: false, projectDoc: false });
+ fs.mkdirSync(path.join(tmpDir, 'docs', 'adr'), { recursive: true });
+ fs.writeFileSync(path.join(tmpDir, 'docs', 'adr', '0001-runtime.md'), '# ADR: Runtime\n');
+ result = runGsdTools('init onboard --raw', tmpDir, { HOME: tmpDir });
+ assert.ok(result.success, `init onboard should succeed: ${result.error}`);
+ assert.deepStrictEqual(JSON.parse(result.output).next_action, {
+ kind: 'ingest-docs',
+ command: '/gsd:ingest-docs',
+ reason: 'Detected existing ADR/PRD/SPEC/RFC document(s) before project setup.',
+ });
+
+ cleanup(tmpDir);
+ tmpDir = createFixture({ planning: false, projectDoc: false });
+ result = runGsdTools('init onboard --raw', tmpDir, { HOME: tmpDir });
+ assert.ok(result.success, `init onboard should succeed: ${result.error}`);
+ assert.deepStrictEqual(JSON.parse(result.output).next_action, {
+ kind: 'new-project',
+ command: '/gsd:new-project',
+ reason: 'No existing code or planning docs were detected.',
+ });
+
+ cleanup(tmpDir);
+ tmpDir = createFixture({ planning: false, projectDoc: false });
+ fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
+ fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# Project\n');
+ fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), '# Roadmap\n');
+ fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '# State\n');
+ result = runGsdTools('init onboard --raw', tmpDir, { HOME: tmpDir });
+ assert.ok(result.success, `init onboard should succeed: ${result.error}`);
+ const partial = JSON.parse(result.output);
+ assert.strictEqual(partial.next_action.kind, 'partial-planning');
+ assert.deepStrictEqual(partial.next_action.missing, ['REQUIREMENTS.md']);
+
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'codebase'), { recursive: true });
+ for (const name of ['STACK', 'ARCHITECTURE', 'STRUCTURE', 'CONVENTIONS', 'TESTING', 'INTEGRATIONS', 'CONCERNS']) {
+ fs.writeFileSync(path.join(tmpDir, '.planning', 'codebase', `${name}.md`), `# ${name}\n`);
+ }
+ fs.writeFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), '# Requirements\n');
+ result = runGsdTools('init onboard --raw', tmpDir, { HOME: tmpDir });
+ assert.ok(result.success, `init onboard should succeed: ${result.error}`);
+ assert.deepStrictEqual(JSON.parse(result.output).next_action, {
+ kind: 'write-summary',
+ summary_path: '.planning/onboarding/SUMMARY.md',
+ reason: 'Onboarding summary is missing.',
+ });
+
+ fs.mkdirSync(path.join(tmpDir, '.planning', 'onboarding'), { recursive: true });
+ fs.writeFileSync(path.join(tmpDir, '.planning', 'onboarding', 'SUMMARY.md'), '# Onboarding Summary\n');
+ result = runGsdTools('init onboard --raw', tmpDir, { HOME: tmpDir });
+ assert.ok(result.success, `init onboard should succeed: ${result.error}`);
+ assert.deepStrictEqual(JSON.parse(result.output).next_action, {
+ kind: 'ready',
+ reason: 'Onboarding summary already exists.',
+ });
+ });
+
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');
@@ -230,101 +302,37 @@ describe('/gsd:onboard command contract', () => {
assert.ok(content.includes('@~/.claude/gsd-core/references/gate-prompts.md'));
});
- test('workflow routes through existing primitives and protects existing planning', () => {
+ test('workflow renders the init projection without owning the route state machine', () => {
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('@~/.claude/gsd-core/references/gsd-run-resolver.md'));
assert.ok(
- !content.includes('ARGUMENTS:-'),
- 'workflow must not depend on an unexported shell ARGUMENTS variable',
+ !content.includes('_GSD_SHIM_NAME="gsd-tools.cjs"'),
+ 'workflow must reference the shared resolver instead of inlining it',
);
- assert.match(
- content,
- /Run init from parsed prompt args, not shell `\$ARGUMENTS`/,
- 'workflow must forward fast mode from parsed prompt arguments into init onboard',
- );
- assert.match(
- content,
- /gsd_run --cwd "\$_GSD_RUNTIME_ROOT" init onboard --fast/,
- 'workflow must provide a literal fast init command',
- );
- 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',
- 'CODEX_HOME',
- 'GEMINI_CONFIG_DIR',
- 'WINDSURF_CONFIG_DIR',
- 'OPENCODE_CONFIG_DIR',
+ assert.match(content, /init onboard --fast --raw/);
+ assert.match(content, /init onboard --raw/);
+ assert.match(content, /next_action\.kind/);
+ assert.match(content, /map_readiness/);
+ assert.match(content, /codebase_map_summary_status/);
+ assert.match(content, /ONBOARDING_ROOT=\{git_worktree_root \|\| _GSD_RUNTIME_ROOT\}/);
+
+ for (const action of [
+ 'map-codebase',
+ 'ingest-docs',
+ 'complete-map-before-new-project',
+ 'new-project',
+ 'partial-planning',
+ 'write-summary',
+ 'ready',
]) {
- assert.ok(content.includes(runtimeHome), `workflow resolver must include ${runtimeHome}`);
+ assert.ok(content.includes(`next_action.kind == "${action}"`), `workflow must render ${action}`);
}
- 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(
- content,
- /If `has_docs_candidates` is true and `project_exists` is false:/,
- 'workflow must offer docs ingest before project setup even when codebase mapping already created .planning',
- );
- assert.match(
- content,
- /If `is_brownfield` is false and `planning_exists` is false and `has_docs_candidates` is false, print:/,
- 'workflow must not send doc-only repos to new-project before the docs ingest gate',
- );
- assert.ok(content.includes('new-project'), 'workflow must route to new-project');
- assert.ok(content.includes('.planning/onboarding/SUMMARY.md'), 'workflow must create onboarding summary');
- assert.match(
- content,
- /ONBOARDING_ROOT=\{git_worktree_root \|\| _GSD_RUNTIME_ROOT\}/,
- 'workflow must resolve a single root for summary writes',
- );
- assert.match(
- content,
- /Create `\{ONBOARDING_ROOT\}\/\.planning\/onboarding\/SUMMARY\.md`/,
- 'workflow must anchor summary writes at onboarding root',
- );
- assert.match(
- content,
- /gsd_run --cwd "\$ONBOARDING_ROOT" query commit "docs: create onboarding summary" --files \.planning\/onboarding\/SUMMARY\.md/,
- 'workflow must commit the root-relative summary path from onboarding root',
- );
- assert.match(
- content,
- /CODEBASE_MAP_SUMMARY_STATUS=\.planning\/codebase\/ \(fast\/partial-but-accepted codebase map\)/,
- 'workflow summary must label fast accepted maps without implying a complete map',
- );
- assert.match(
- content,
- /CODEBASE_MAP_FINAL_STATUS=\(fast\/partial-but-accepted; required fast files present\)/,
- 'final status must distinguish complete maps from accepted fast maps',
- );
- assert.match(
- content,
- /If `project_exists` is false and `fast_mode && has_fast_codebase_map && !has_codebase_map` is true:/,
- 'workflow must not hand fast-partial maps directly to new-project',
- );
- assert.match(
- content,
- /Fast codebase context is accepted for onboarding, but \/gsd:new-project still requires the complete codebase map\./,
- 'workflow must explain why fast-partial onboarding stops before new-project',
- );
- const deriveMapStatusIndex = content.indexOf('Before summary keep/update choices, derive map status values safely:');
- const keepExistingSummaryIndex = content.indexOf('If `onboarding_summary_exists` is true:');
- assert.ok(deriveMapStatusIndex >= 0, 'workflow must derive map status values');
- assert.ok(keepExistingSummaryIndex >= 0, 'workflow must define the existing-summary branch');
- assert.ok(
- deriveMapStatusIndex < keepExistingSummaryIndex,
- 'workflow must derive final map status before a keep-existing-summary path can skip to final status',
- );
- assert.match(content, /overwrite|idempotent|do not overwrite/i, 'workflow must protect existing planning');
- assert.ok(content.includes('requirements_exists'), 'workflow must parse requirements existence');
- assert.match(content, /REQUIREMENTS\.md: \{requirements_exists \? "present" : "missing"\}/, 'workflow must report missing requirements in partial planning');
- assert.match(content, /If `project_exists` is true and .*`requirements_exists`.* is false/s, 'workflow must gate partial planning on missing requirements');
+
+ assert.ok(content.includes('AskUserQuestion'), 'workflow must still support interactive choices');
assert.ok(content.includes('--text'), 'workflow must document text-mode fallback');
+ assert.match(content, /do not overwrite/i, 'workflow must protect existing summary/planning');
+ assert.match(content, /query commit "docs: create onboarding summary" --files \.planning\/onboarding\/SUMMARY\.md/);
assert.ok(!content.includes('execute-phase'), 'onboarding must not execute implementation phases');
assert.ok(!content.includes('gsd:ship'), 'onboarding must not ship work');
});