fix: honor workstream in verify-work init (#3386)

* fix: honor workstream in verify-work init

* fix: define verify-work phase arg
This commit is contained in:
Tom Boucher
2026-05-10 20:25:42 -04:00
committed by GitHub
parent 570dddd1cd
commit 574d1f448c
6 changed files with 94 additions and 12 deletions

View File

@@ -0,0 +1,6 @@
---
type: Fixed
pr: 3386
---
**`/gsd-verify-work --ws <name>` 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/`.

View File

@@ -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 <name>]"
allowed-tools:
- Read
- Bash

View File

@@ -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)
```
</step>
@@ -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."

View File

@@ -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<string, unknown>;
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<string, unknown>;

View File

@@ -184,14 +184,15 @@ async function getPhaseInfoWithFallback(
async function getPhaseInfoForVerifyWork(
phase: string,
projectDir: string,
workstream?: string,
): Promise<{ phaseInfo: Record<string, unknown> | null }> {
const phaseResult = await findPhase([phase], projectDir);
const phaseResult = await findPhase([phase], projectDir, workstream);
let phaseInfo = phaseResult.data as Record<string, unknown> | 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<string, unknown> | 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

View File

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