no-mistakes(review): Format onboard handoffs by runtime

This commit is contained in:
Jeremy McSpadden
2026-07-03 17:08:29 -05:00
committed by Codesmith
parent a845a7d231
commit 2f2d33aea7
3 changed files with 86 additions and 30 deletions

View File

@@ -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.

View File

@@ -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,

View File

@@ -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');
});
});