From 2f2d33aea7067a7f90614279c9bf7affa85554aa Mon Sep 17 00:00:00 2001 From: Jeremy McSpadden Date: Fri, 3 Jul 2026 17:08:29 -0500 Subject: [PATCH] no-mistakes(review): Format onboard handoffs by runtime --- gsd-core/workflows/onboard.md | 31 ++++++++++++----------- src/onboard-projection.cts | 39 ++++++++++++++++++++++++---- tests/onboard-command.test.cjs | 46 ++++++++++++++++++++++++++-------- 3 files changed, 86 insertions(+), 30 deletions(-) diff --git a/gsd-core/workflows/onboard.md b/gsd-core/workflows/onboard.md index 5d6d83187..321649fc0 100644 --- a/gsd-core/workflows/onboard.md +++ b/gsd-core/workflows/onboard.md @@ -24,6 +24,7 @@ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi Parse JSON fields from `INIT`: - `next_action.kind`, `next_action.command`, `next_action.reason`, `next_action.missing`, `next_action.summary_path` +- `handoff_commands.ingest_docs`, `handoff_commands.manager`, `handoff_commands.new_project`, `handoff_commands.onboard` - `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` @@ -69,12 +70,12 @@ Run from worktree root {ONBOARDING_ROOT}: {next_action.command} -Then rerun /gsd:onboard from the same worktree root. +Then rerun {handoff_commands.onboard} from the same worktree root. ``` Exit. If the user skips mapping: -- If `planning_exists && (!project_exists || !requirements_exists || !roadmap_exists || !state_exists)`, route the skip to the partial planning guard instead: +- If `(project_exists || requirements_exists || roadmap_exists || state_exists) && (!project_exists || !requirements_exists || !roadmap_exists || !state_exists)`, route the skip to the partial planning guard instead: ```text Skipping codebase mapping may give downstream steps weaker context, but project planning exists and is incomplete. @@ -84,7 +85,7 @@ REQUIREMENTS.md: {requirements_exists ? "present" : "missing"} ROADMAP.md: {roadmap_exists ? "present" : "missing"} STATE.md: {state_exists ? "present" : "missing"} -Run the appropriate lower-level command to fill the missing planning artifact(s), then rerun /gsd:onboard. +Run the appropriate lower-level command to fill the missing planning artifact(s), then rerun {handoff_commands.onboard}. ``` Exit. @@ -92,11 +93,11 @@ Exit. - If `has_docs_candidates && !project_exists`, route the skip to docs ingest instead: ```text -Skipping codebase mapping may give downstream steps weaker context, but existing ADR/PRD/SPEC/RFC documents should still be ingested before /gsd:new-project. +Skipping codebase mapping may give downstream steps weaker context, but existing ADR/PRD/SPEC/RFC documents should still be ingested before {handoff_commands.new_project}. Run from worktree root {ONBOARDING_ROOT}: -/gsd:ingest-docs +{handoff_commands.ingest_docs} ``` Exit. @@ -104,11 +105,11 @@ Exit. - Otherwise print: ```text -Skipping codebase mapping may give /gsd:new-project weaker context. +Skipping codebase mapping may give {handoff_commands.new_project} weaker context. Run from worktree root {ONBOARDING_ROOT}: -/gsd:new-project +{handoff_commands.new_project} ``` Exit. @@ -124,7 +125,7 @@ If `next_action.kind == "ingest-docs"`: Detected {doc_candidate_count} possible ADR/PRD/SPEC/RFC document(s). 1. Ingest docs first — run {next_action.command} from worktree root {ONBOARDING_ROOT} (Recommended) -2. Skip docs ingest — continue to /gsd:new-project +2. Skip docs ingest — continue to {handoff_commands.new_project} Enter number: ``` @@ -134,7 +135,7 @@ Enter number: - question: "Detected {doc_candidate_count} possible ADR/PRD/SPEC/RFC document(s). Ingest them first?" - options: - "Ingest docs first" — Run `{next_action.command}` from worktree root `{ONBOARDING_ROOT}` (Recommended) - - "Skip docs ingest" — Continue to `/gsd:new-project` + - "Skip docs ingest" — Continue to `{handoff_commands.new_project}` If the user chooses ingest, print: @@ -143,17 +144,17 @@ Run from worktree root {ONBOARDING_ROOT}: {next_action.command} -Then rerun /gsd:onboard from the same worktree root. +Then rerun {handoff_commands.onboard} from the same worktree root. ``` Exit. If the user skips docs ingest, print: ```text -Skipping docs ingest may omit existing ADR/PRD/SPEC/RFC context from /gsd:new-project. +Skipping docs ingest may omit existing ADR/PRD/SPEC/RFC context from {handoff_commands.new_project}. Run from worktree root {ONBOARDING_ROOT}: -/gsd:new-project +{handoff_commands.new_project} ``` Exit. @@ -169,7 +170,7 @@ Run from worktree root {ONBOARDING_ROOT}: {next_action.command} -Then rerun /gsd:onboard from the same worktree root. +Then rerun {handoff_commands.onboard} from the same worktree root. ``` Exit. @@ -200,7 +201,7 @@ REQUIREMENTS.md: {requirements_exists ? "present" : "missing"} ROADMAP.md: {roadmap_exists ? "present" : "missing"} STATE.md: {state_exists ? "present" : "missing"} -Run the appropriate lower-level command to fill the missing planning artifact(s), then rerun /gsd:onboard. +Run the appropriate lower-level command to fill the missing planning artifact(s), then rerun {handoff_commands.onboard}. ``` Exit. @@ -262,7 +263,7 @@ Onboarding status: - Codebase map: {codebase_map_final_status} - Onboarding summary: present -Next recommended command: /gsd:manager +Next recommended command: {handoff_commands.manager} ``` Do not run implementation execution or shipping from onboarding. diff --git a/src/onboard-projection.cts b/src/onboard-projection.cts index c2501a210..b327ded57 100644 --- a/src/onboard-projection.cts +++ b/src/onboard-projection.cts @@ -5,6 +5,7 @@ import path from 'node:path'; 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'); +import { formatGsdSlash, resolveRuntime } from './runtime-slash.cjs'; const { pathExistsInternal, toPosixPath } = coreUtils; const { planningDir, planningRoot } = planningWorkspace; @@ -42,6 +43,15 @@ const PLANNING_DOC_SEGMENTS = new Set([ type MapReadiness = 'none' | 'fast' | 'complete'; +interface OnboardHandoffCommands { + ingest_docs: string; + manager: string; + map_codebase: string; + map_codebase_fast: string; + new_project: string; + onboard: string; +} + type OnboardNextAction = | { kind: 'map-codebase'; command: string; reason: string } | { kind: 'ingest-docs'; command: string; reason: string } @@ -68,6 +78,8 @@ interface OnboardProjection { state_exists: boolean; config_exists: boolean; + handoff_commands: OnboardHandoffCommands; + has_existing_code: boolean; has_package_file: boolean; is_brownfield: boolean; @@ -237,11 +249,12 @@ function nextAction(params: { onboardingSummaryExists: boolean; hasPlanningArtifacts: boolean; missingPlanningFiles: string[]; + handoffCommands: OnboardHandoffCommands; }): OnboardNextAction { if (params.isBrownfield && params.needsOnboardCodebaseMap) { return { kind: 'map-codebase', - command: params.fastMode ? '/gsd:map-codebase --fast' : '/gsd:map-codebase', + command: params.fastMode ? params.handoffCommands.map_codebase_fast : params.handoffCommands.map_codebase, reason: 'Existing code was detected, but the required .planning/codebase/ map is missing.', }; } @@ -249,7 +262,7 @@ function nextAction(params: { if (!params.projectExists && params.fastMode && params.mapReadiness === 'fast') { return { kind: 'complete-map-before-new-project', - command: '/gsd:map-codebase', + command: params.handoffCommands.map_codebase, reason: 'The fast map is enough for lightweight onboarding, but project setup still requires the complete codebase map.', }; } @@ -265,7 +278,7 @@ function nextAction(params: { if (params.hasDocsCandidates && !params.projectExists) { return { kind: 'ingest-docs', - command: '/gsd:ingest-docs', + command: params.handoffCommands.ingest_docs, reason: 'Detected existing ADR/PRD/SPEC/RFC document(s) before project setup.', }; } @@ -273,7 +286,7 @@ function nextAction(params: { if (!params.isBrownfield && !params.projectExists && !params.hasDocsCandidates) { return { kind: 'new-project', - command: '/gsd:new-project', + command: params.handoffCommands.new_project, reason: 'No existing code or planning docs were detected.', }; } @@ -281,7 +294,7 @@ function nextAction(params: { if (!params.projectExists) { return { kind: 'new-project', - command: '/gsd:new-project', + command: params.handoffCommands.new_project, reason: 'Codebase context is ready for project initialization.', }; } @@ -300,7 +313,20 @@ function nextAction(params: { }; } +function buildHandoffCommands(cwd: string): OnboardHandoffCommands { + const runtime = resolveRuntime(cwd); + return { + ingest_docs: formatGsdSlash('ingest-docs', runtime) as string, + manager: formatGsdSlash('manager', runtime) as string, + map_codebase: formatGsdSlash('map-codebase', runtime) as string, + map_codebase_fast: formatGsdSlash('map-codebase --fast', runtime) as string, + new_project: formatGsdSlash('new-project', runtime) as string, + onboard: formatGsdSlash('onboard', runtime) as string, + }; +} + function buildOnboardProjection(cwd: string, options: BuildOnboardProjectionOptions): OnboardProjection { + const handoffCommands = buildHandoffCommands(cwd); const codebaseMapFiles = listCodebaseMapFiles(cwd); const missingCodebaseMapFiles = REQUIRED_CODEBASE_MAP_FILES.filter( (file) => !codebaseMapFiles.includes(file), @@ -342,6 +368,8 @@ function buildOnboardProjection(cwd: string, options: BuildOnboardProjectionOpti state_exists: stateExists, config_exists: fs.existsSync(path.join(planningDir(cwd), 'config.json')), + handoff_commands: handoffCommands, + has_existing_code: hasCode, has_package_file: hasPackageFile, is_brownfield: isBrownfield, @@ -357,6 +385,7 @@ function buildOnboardProjection(cwd: string, options: BuildOnboardProjectionOpti onboardingSummaryExists, hasPlanningArtifacts, missingPlanningFiles, + handoffCommands, }), needs_codebase_map: needsCodebaseMap, needs_fast_codebase_map: needsFastCodebaseMap, diff --git a/tests/onboard-command.test.cjs b/tests/onboard-command.test.cjs index b3ab7a64b..5809f9b32 100644 --- a/tests/onboard-command.test.cjs +++ b/tests/onboard-command.test.cjs @@ -163,7 +163,7 @@ describe('init onboard public CLI projection', () => { assert.strictEqual(parsed.needs_codebase_map, true); assert.strictEqual(parsed.needs_fast_codebase_map, false); assert.strictEqual(parsed.next_action.kind, 'complete-map-before-new-project'); - assert.strictEqual(parsed.next_action.command, '/gsd:map-codebase'); + 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', @@ -188,7 +188,7 @@ describe('init onboard public CLI projection', () => { const parsed = JSON.parse(result.output); assert.strictEqual(parsed.next_action.kind, 'complete-map-before-new-project'); - assert.strictEqual(parsed.next_action.command, '/gsd:map-codebase'); + assert.strictEqual(parsed.next_action.command, '/gsd-map-codebase'); }); test('routes planning artifacts without PROJECT.md to partial planning', () => { @@ -212,7 +212,7 @@ describe('init onboard public CLI projection', () => { 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', + command: '/gsd-map-codebase', reason: 'Existing code was detected, but the required .planning/codebase/ map is missing.', }); @@ -224,7 +224,7 @@ describe('init onboard public CLI projection', () => { 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', + command: '/gsd-ingest-docs', reason: 'Detected existing ADR/PRD/SPEC/RFC document(s) before project setup.', }); @@ -234,7 +234,7 @@ describe('init onboard public CLI projection', () => { 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', + command: '/gsd-new-project', reason: 'No existing code or planning docs were detected.', }); @@ -318,6 +318,29 @@ describe('init onboard public CLI projection', () => { assert.strictEqual(parsed.needs_codebase_map, true); }); + + test('formats onboard handoff commands for the resolved runtime', () => { + fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name":"fixture"}\n'); + + const result = runGsdTools(['init', 'onboard', '--raw'], tmpDir, { + HOME: tmpDir, + GSD_RUNTIME: 'codex', + }); + assert.ok(result.success, `init onboard should succeed: ${result.error}`); + + const parsed = JSON.parse(result.output); + assert.strictEqual(parsed.next_action.kind, 'map-codebase'); + assert.strictEqual(parsed.next_action.command, '$gsd-map-codebase'); + assert.deepStrictEqual(parsed.handoff_commands, { + map_codebase: '$gsd-map-codebase', + map_codebase_fast: '$gsd-map-codebase --fast', + ingest_docs: '$gsd-ingest-docs', + manager: '$gsd-manager', + new_project: '$gsd-new-project', + onboard: '$gsd-onboard', + }); + }); + test('dotted query init.onboard matches direct init onboard', () => { fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name":"fixture"}\n'); @@ -376,7 +399,7 @@ describe('/gsd:onboard command contract', () => { 'skip mapping must hand off explicitly instead of falling through to summary creation', ); const partialPlanningSkipGuard = - 'If `planning_exists && (!project_exists || !requirements_exists || !roadmap_exists || !state_exists)`, route the skip to the partial planning guard instead:'; + 'If `(project_exists || requirements_exists || roadmap_exists || state_exists) && (!project_exists || !requirements_exists || !roadmap_exists || !state_exists)`, route the skip to the partial planning guard instead:'; const docsIngestSkipGuard = 'If `has_docs_candidates && !project_exists`, route the skip to docs ingest instead:'; assert.ok( @@ -392,11 +415,11 @@ describe('/gsd:onboard command contract', () => { 'skip mapping must check partial planning before docs ingest', ); assert.ok( - content.includes('/gsd:ingest-docs'), - 'skip mapping docs-preserving handoff must name the docs ingest command', + content.includes('{handoff_commands.ingest_docs}'), + 'skip mapping docs-preserving handoff must use the projected docs ingest command', ); assert.ok( - content.includes('Skipping codebase mapping may give /gsd:new-project weaker context.'), + content.includes('Skipping codebase mapping may give {handoff_commands.new_project} weaker context.'), 'skip mapping must warn about weaker context', ); assert.ok( @@ -404,12 +427,15 @@ describe('/gsd:onboard command contract', () => { 'skip docs ingest must hand off explicitly instead of falling through to summary creation', ); assert.ok( - content.includes('Skipping docs ingest may omit existing ADR/PRD/SPEC/RFC context from /gsd:new-project.'), + content.includes('Skipping docs ingest may omit existing ADR/PRD/SPEC/RFC context from {handoff_commands.new_project}.'), 'skip docs ingest must warn about omitted docs context', ); 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'); + assert.ok(content.includes('Next recommended command: {handoff_commands.manager}')); + assert.ok(!content.includes('/gsd:ingest-docs'), 'skip handoffs must not hard-code legacy ingest command'); + assert.ok(!content.includes('/gsd:new-project'), 'skip handoffs must not hard-code legacy new-project command'); }); });