diff --git a/.changeset/fix-3381-init-verify-work-ws.md b/.changeset/fix-3381-init-verify-work-ws.md new file mode 100644 index 000000000..1f5e5909b --- /dev/null +++ b/.changeset/fix-3381-init-verify-work-ws.md @@ -0,0 +1,6 @@ +--- +type: Fixed +pr: 3386 +--- + +**`/gsd-verify-work --ws ` now resolves workstream phases through the SDK** — `init.verify-work`, MVP-mode lookup, and phase-goal lookup all receive the selected workstream instead of falling back to root `.planning/`. diff --git a/commands/gsd/verify-work.md b/commands/gsd/verify-work.md index 2e5dd2883..04ebe3f2a 100644 --- a/commands/gsd/verify-work.md +++ b/commands/gsd/verify-work.md @@ -1,7 +1,7 @@ --- name: gsd:verify-work description: Validate built features through conversational UAT -argument-hint: "[phase number, e.g., '4']" +argument-hint: "[phase number, e.g., '4'] [--ws ]" allowed-tools: - Read - Bash diff --git a/get-shit-done/workflows/verify-work.md b/get-shit-done/workflows/verify-work.md index fe7c63a6c..ede27ea52 100644 --- a/get-shit-done/workflows/verify-work.md +++ b/get-shit-done/workflows/verify-work.md @@ -30,7 +30,11 @@ No Pass/Fail buttons. No severity questions. Just: "Here's what should happen. D If $ARGUMENTS contains a phase number, load context: ```bash -INIT=$(gsd-sdk query init.verify-work "${PHASE_ARG}") +GSD_WS="" +echo "$ARGUMENTS" | grep -qE -- '--ws[[:space:]]+[^[:space:]]+' && GSD_WS=$(echo "$ARGUMENTS" | grep -oE -- '--ws[[:space:]]+[^[:space:]]+') +PHASE_ARG=$(echo "$ARGUMENTS" | sed -E 's/--ws[[:space:]]+[^[:space:]]+//g' | xargs) + +INIT=$(gsd-sdk query init.verify-work "${PHASE_ARG}" ${GSD_WS}) if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi AGENT_SKILLS_PLANNER=$(gsd-sdk query agent-skills gsd-planner) AGENT_SKILLS_CHECKER=$(gsd-sdk query agent-skills gsd-plan-checker) @@ -42,7 +46,7 @@ Parse JSON for: `planner_model`, `checker_model`, `commit_docs`, `phase_found`, # MVP mode detection via the centralized phase.mvp-mode resolver. # verify-work has no --mvp CLI flag (mode is inherited from the planned phase), # so we omit --cli-flag — the verb falls through roadmap → config → false. -MVP_MODE=$(gsd-sdk query phase.mvp-mode "${phase_number}" --pick active) +MVP_MODE=$(gsd-sdk query phase.mvp-mode "${phase_number}" ${GSD_WS} --pick active) ``` @@ -154,7 +158,7 @@ When `MVP_MODE=false` (mode is null, absent, or the phase has no `**Mode:**` lin **User-story format guard.** When `MVP_MODE=true`, also verify the phase's goal is in User Story format via the centralized validator: ```bash -PHASE_GOAL=$(gsd-sdk query roadmap.get-phase "${phase_number}" --pick goal) +PHASE_GOAL=$(gsd-sdk query roadmap.get-phase "${phase_number}" ${GSD_WS} --pick goal) USER_STORY_VALID=$(gsd-sdk query user-story.validate --story "$PHASE_GOAL" --pick valid) if [ "$USER_STORY_VALID" != "true" ]; then echo "Phase ${phase_number} has '**Mode:** mvp' in ROADMAP.md but the **Goal:** is not in user-story format." diff --git a/sdk/src/query/init.test.ts b/sdk/src/query/init.test.ts index 11e8f077f..94bdb5738 100644 --- a/sdk/src/query/init.test.ts +++ b/sdk/src/query/init.test.ts @@ -448,6 +448,30 @@ describe('initVerifyWork', () => { expect(data.project_root).toBe(tmpDir); }); + it('resolves workstream-scoped phases when workstream is provided', async () => { + const wsDir = join(tmpDir, '.planning', 'workstreams', 'delivery'); + await mkdir(join(wsDir, 'phases', '32-shipment-creation-tracking-numbers-print-forms'), { recursive: true }); + await writeFile(join(wsDir, 'ROADMAP.md'), [ + '# Roadmap', + '', + '## v1.0: Delivery', + '', + '### Phase 32: Shipment Creation Tracking Numbers Print Forms', + '', + '**Goal:** Ship orders.', + '', + ].join('\n')); + + const result = await initVerifyWork(['32'], tmpDir, 'delivery'); + const data = result.data as Record; + + expect(data.phase_found).toBe(true); + expect(data.phase_number).toBe('32'); + expect(data.phase_dir).toBe( + '.planning/workstreams/delivery/phases/32-shipment-creation-tracking-numbers-print-forms', + ); + }); + it('returns error when phase arg missing', async () => { const result = await initVerifyWork([], tmpDir); const data = result.data as Record; diff --git a/sdk/src/query/init.ts b/sdk/src/query/init.ts index 6071754ca..c89bdd81d 100644 --- a/sdk/src/query/init.ts +++ b/sdk/src/query/init.ts @@ -184,14 +184,15 @@ async function getPhaseInfoWithFallback( async function getPhaseInfoForVerifyWork( phase: string, projectDir: string, + workstream?: string, ): Promise<{ phaseInfo: Record | null }> { - const phaseResult = await findPhase([phase], projectDir); + const phaseResult = await findPhase([phase], projectDir, workstream); let phaseInfo = phaseResult.data as Record | null; if (phaseInfo && phaseInfo.found === false) { phaseInfo = null; } - const roadmapResult = await roadmapGetPhase([phase], projectDir); + const roadmapResult = await roadmapGetPhase([phase], projectDir, workstream); const roadmapPhase = roadmapResult.data as Record | null; if (phaseInfo?.archived && roadmapPhase?.found) { @@ -205,9 +206,7 @@ async function getPhaseInfoForVerifyWork( directory: null, phase_number: roadmapPhase.phase_number, phase_name: phaseName, - phase_slug: phaseName - ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') - : null, + phase_slug: phaseName ? generateSlugInternal(phaseName) : null, plans: [], summaries: [], incomplete_plans: [], @@ -623,14 +622,14 @@ export const initResume: QueryHandler = async (_args, projectDir) => { * Init handler for verify-work workflow. * Port of cmdInitVerifyWork from init.cjs lines 538-586. */ -export const initVerifyWork: QueryHandler = async (args, projectDir) => { +export const initVerifyWork: QueryHandler = async (args, projectDir, workstream) => { const phase = args[0]; if (!phase) { return { data: { error: 'phase required for init verify-work' } }; } - const config = await loadConfig(projectDir); - const { phaseInfo } = await getPhaseInfoForVerifyWork(phase, projectDir); + const config = await loadConfig(projectDir, workstream); + const { phaseInfo } = await getPhaseInfoForVerifyWork(phase, projectDir, workstream); const configExists = existsSync(join(projectDir, '.planning', 'config.json')); const [plannerModel, checkerModel] = configExists diff --git a/tests/bug-3381-verify-work-workstream.test.cjs b/tests/bug-3381-verify-work-workstream.test.cjs new file mode 100644 index 000000000..03986ca5c --- /dev/null +++ b/tests/bug-3381-verify-work-workstream.test.cjs @@ -0,0 +1,49 @@ +// allow-test-rule: source-text-is-the-product — verify-work.md is a runtime workflow contract. + +'use strict'; + +const { test, describe } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +describe('bug #3381: verify-work forwards workstream context', () => { + test('workflow forwards ${GSD_WS} to workstream-sensitive SDK queries', () => { + const workflow = fs.readFileSync( + path.join(__dirname, '..', 'get-shit-done', 'workflows', 'verify-work.md'), + 'utf8', + ); + + assert.match(workflow, /GSD_WS=""/, 'verify-work must initialize GSD_WS'); + assert.match( + workflow, + /grep -qE -- '--ws\[\[:space:\]\]\+\[\^\[:space:\]\]\+'/, + 'verify-work must detect --ws in $ARGUMENTS', + ); + assert.match( + workflow, + /grep -oE -- '--ws\[\[:space:\]\]\+\[\^\[:space:\]\]\+'/, + 'verify-work must extract the --ws flag pair from $ARGUMENTS', + ); + assert.match( + workflow, + /PHASE_ARG=\$\(echo "\$ARGUMENTS" \| sed -E 's\/--ws\[\[:space:\]\]\+\[\^\[:space:\]\]\+\/\/g' \| xargs\)/, + 'verify-work must derive PHASE_ARG after removing --ws', + ); + assert.match( + workflow, + /gsd-sdk query init\.verify-work "\$\{PHASE_ARG\}" \$\{GSD_WS\}/, + 'init.verify-work must receive GSD_WS so phase_dir resolves in workstreams', + ); + assert.match( + workflow, + /gsd-sdk query phase\.mvp-mode "\$\{phase_number\}" \$\{GSD_WS\} --pick active/, + 'phase.mvp-mode must receive GSD_WS so roadmap mode is workstream-scoped', + ); + assert.match( + workflow, + /gsd-sdk query roadmap\.get-phase "\$\{phase_number\}" \$\{GSD_WS\} --pick goal/, + 'roadmap.get-phase must receive GSD_WS so goals are workstream-scoped', + ); + }); +});