feat(plan-phase): --research-phase flag + scrub stale slash-command refs (#3042, #3044) (#3045)

* feat(plan-phase): --research-phase flag absorbs deleted /gsd-research-phase + scrub stale refs (#3042, #3044)

#3042 (orphaned research-phase): /gsd-research-phase had a workflow file
but no slash-command stub. Rather than restore the orphan, the research-
only capability is now a flag on /gsd-plan-phase:

  /gsd-plan-phase --research-phase <N>

When set, the workflow scopes to phase N, runs the research step (Section
5 of the existing plan-phase workflow), then early-exits before the
planner/plan-checker/verifier chain.

Per RCA against the deleted standalone, the flag adds two modifiers to
fully cover the original surface (Option B from the RCA discussion):

- --view : print existing RESEARCH.md to stdout, no spawn. Cheapest mode
  for the correction-without-replanning loop the issue reporter
  explicitly called out. Errors with a clear hint if RESEARCH.md is
  missing.
- --research : reuse the existing "force re-research" semantics. In
  research-only mode this skips the existing-RESEARCH.md prompt and
  re-spawns unconditionally.
- Neither flag, RESEARCH.md exists : prompt update/view/skip. Mirrors
  the deleted standalone's existing-artifact menu (#3042 RCA).

#3044 (stale slash-command refs): scrubbed five deleted commands from
all user-facing surfaces, including English docs, 4 localized doc sets
(ja-JP, ko-KR, zh-CN, pt-BR), workflows, templates, and references.

  /gsd-check-todos          → /gsd-capture --list
  /gsd-new-workspace        → /gsd-workspace --new
  /gsd-status               → /gsd-progress
  /gsd-plan-milestone-gaps  → table rows / orphan sections removed
                              (PR #3038 only scrubbed workflows/agent;
                              missed the docs surfaces this PR covers)
  /gsd-research-phase       → /gsd-plan-phase --research-phase

Includes a fix to docs/issue-driven-orchestration.md (PR #3036)
which itself referenced /gsd-new-workspace 4 times — self-correction.

Removed:
- get-shit-done/workflows/research-phase.md (orphan, capability
  absorbed into --research-phase flag)

Tests:
- tests/bug-3042-3044-research-flag-and-stale-refs.test.cjs — 46
  structural-IR tests across both bugs:
  - argument-hint advertises --research-phase + --view
  - workflow parses --research-phase, sets RESEARCH_ONLY,
    early-exits before planner
  - --view prints RESEARCH.md without spawning
  - --research forces refresh in research-only mode
  - existing-RESEARCH.md prompt path with update/view/skip
  - workflows/research-phase.md is removed
  - 5 deleted slash-commands absent from 17 English user-facing
    surfaces + 16 localized doc surfaces (4 locales × 4 docs each)
  - replacement command tokens present where deleted ones lived

6950/6950 full suite pass. Lints clean.

Closes #3042
Closes #3044

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: address all 8 CR findings on PR #3045

Major (3):
- get-shit-done/workflows/plan-phase.md:344 — added explicit early-exit
  guard at Section 5.1: "Skip if RESEARCH_ONLY=true". Without it, an LLM
  could fall through "use existing, skip to step 6" → planner spawn,
  violating the research-only contract. The guard makes the early-exit
  unreachable from any non-research-only branch.
- get-shit-done/references/continuation-format.md (3 examples) +
  zh-CN/.../continuation-format.md (3 examples) — pointed to
  `/gsd-plan-phase --research-phase` but docs/COMMANDS.md didn't
  document the flag. Added a full --research-phase + --view + --research
  modifier section to the /gsd-plan-phase flag table in COMMANDS.md so
  the canonical reference matches the continuation examples.

Minor (5):
- docs/FEATURES.md:1632 — `/gsd-plan-phase --research-phase` →
  `/gsd-plan-phase --research-phase <N>` (include required arg).
- get-shit-done/templates/README.md:46 — NN-VALIDATION.md producer
  reverted from `/gsd-plan-phase --research-phase` (Nyquist) to plain
  `/gsd-plan-phase` (Nyquist). VALIDATION.md is created during normal
  Nyquist flow, not research-only mode — the bulk replacement was
  wrong for that line.
- get-shit-done/workflows/help.md:89 — signature line was missing
  `--research`; added it alongside `--research-phase` and `--view`.
- tests/bug-3042-3044-...:197 — promptHasView/promptHasSkip were
  tautological (matched anywhere in 1700-line workflow). Tightened
  to a proximity check anchored on "RESEARCH.md already exists" prompt
  header within a 600-char window. Updated workflow to emit that
  literal phrase.
- tests/feat-2840-...:95 — workspace assertion used `/gsd-workspace`
  but the documented replacement is `/gsd-workspace --new`. Tightened
  to require both tokens (in 3 places: requiredCommands list, regex
  in conceptPairs, error message).

6950/6950 full suite pass. Lint clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-05-02 23:12:50 -04:00
committed by GitHub
parent dca12242b5
commit 1e6737cd8e
39 changed files with 502 additions and 203 deletions

View File

@@ -0,0 +1,333 @@
// allow-test-rule: prose-driven workflow files. The .includes() / regex
// hits below build a typed record of (a) presence of slash-command tokens
// in the parsed argument-hint frontmatter, (b) the literal flag tokens
// the workflow's bash parsing block consults, and (c) absence of deleted
// slash-command tokens across user-facing surfaces. These workflow files
// ARE the implementation — there is no "real" code seam to assert against.
// The IR-build hits are not the assertion surface; assertions run on the
// resulting boolean / Set membership.
/**
* Bug #3042 + #3044: research-only flag for /gsd-plan-phase + scrub of
* 4 stale slash-command references across user-facing surfaces.
*
* #3042 (orphaned research-phase): the slash command /gsd-research-phase
* never had a stub registered. Per the maintainer decision, the
* capability moves to a flag on /gsd-plan-phase rather than restoring
* a separate command. Invocation:
*
* /gsd-plan-phase --research-phase <N>
*
* When --research-phase is present, plan-phase scopes to phase N,
* runs only the research step, and exits before spawning the planner /
* plan-checker / verifier chain.
*
* #3044 (stale slash-command refs in user-facing docs): four commands
* appear in workflow / template / doc surfaces without being
* registered:
*
* /gsd-check-todos → /gsd-capture --list
* /gsd-new-workspace → /gsd-workspace --new
* /gsd-plan-milestone-gaps → inline gap planning (#3038 partial scrub)
* /gsd-status → /gsd-progress
* /gsd-research-phase → /gsd-plan-phase --research-phase
*
* Tests assert (a) the flag is wired, (b) every deleted/never-registered
* slash-command token is absent from user-facing surfaces, (c) the
* orphaned workflows/research-phase.md is removed.
*/
'use strict';
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.join(__dirname, '..');
function read(rel) {
return fs.readFileSync(path.join(ROOT, rel), 'utf-8');
}
function exists(rel) {
return fs.existsSync(path.join(ROOT, rel));
}
// ─── Slash-command token extractor (typed-IR helper) ────────────────────────
/**
* Returns the Set<string> of `/gsd-*` slash-command tokens emitted by a
* markdown surface. Strips trailing arg tokens so the identity is the
* command name only.
*/
function extractSlashCommandTokens(content) {
const re = /\/gsd-[a-z0-9][a-z0-9-]*/g;
return new Set(content.match(re) || []);
}
// ─── #3042: --research-phase flag wired into /gsd-plan-phase ────────────────
describe('bug #3042: /gsd-plan-phase --research-phase flag absorbs the standalone research command', () => {
test('commands/gsd/plan-phase.md argument-hint advertises --research-phase', () => {
const content = read('commands/gsd/plan-phase.md');
// Frontmatter argument-hint is the structural place users discover
// the flag. Parse the line that starts with "argument-hint:" and
// assert the flag token is present.
const m = content.match(/^argument-hint:\s*"([^"]+)"/m);
assert.ok(m, 'plan-phase.md must declare an argument-hint frontmatter field');
assert.ok(
m[1].includes('--research-phase'),
`argument-hint must include "--research-phase"; got: ${m[1]}`
);
});
test('plan-phase.md frontmatter description still advertises plan capability (no semantics drift)', () => {
const content = read('commands/gsd/plan-phase.md');
const m = content.match(/^description:\s*(.+)$/m);
assert.ok(m, 'plan-phase.md must have a description field');
// The description should still describe planning — the flag is
// additive, not a renamed command.
assert.ok(
/plan/i.test(m[1]),
`description should still mention planning; got: ${m[1]}`
);
});
test('workflows/plan-phase.md parses --research-phase and sets a research-only mode', () => {
const content = read('get-shit-done/workflows/plan-phase.md');
// The arg-parsing section of the workflow must mention the new flag
// by name. This is the structural seam the LLM follows.
assert.ok(
content.includes('--research-phase'),
'plan-phase.md workflow must reference the --research-phase flag in its argument-parsing section'
);
});
test('workflows/plan-phase.md skips planner/verifier when in research-only mode', () => {
const content = read('get-shit-done/workflows/plan-phase.md');
// Look for explicit early-exit prose so the LLM knows to stop after
// research. We accept any of: "research-only", "research only mode",
// "skip if --research-phase", "RESEARCH_ONLY", "exit after research".
const patterns = [
/research[ -]only/i,
/RESEARCH_ONLY/,
/skip if[^\n]*--research-phase/i,
/exit (?:after|when)[^\n]*research/i,
];
const hits = patterns.filter((re) => re.test(content));
assert.ok(
hits.length > 0,
`plan-phase workflow must contain explicit early-exit prose for --research-phase mode; ` +
`none of [research-only, RESEARCH_ONLY, "skip if --research-phase", "exit after research"] matched`
);
});
test('orphaned workflows/research-phase.md is removed', () => {
assert.equal(
exists('get-shit-done/workflows/research-phase.md'),
false,
'workflows/research-phase.md must be removed; the capability now lives on /gsd-plan-phase --research-phase'
);
});
test('argument-hint advertises --view as a research-only modifier', () => {
const content = read('commands/gsd/plan-phase.md');
const m = content.match(/^argument-hint:\s*"([^"]+)"/m);
assert.ok(m, 'plan-phase.md must declare an argument-hint frontmatter field');
assert.ok(
m[1].includes('--view'),
`argument-hint must include --view (research-only view-only mode); got: ${m[1]}`
);
});
test('workflow handles --view by printing existing RESEARCH.md without spawning', () => {
const content = read('get-shit-done/workflows/plan-phase.md');
// The workflow must reference the --view flag as a no-spawn mode
// for research-only invocations. We accept any of: "view-only",
// "VIEW_ONLY", "skip if --view", "no spawn" alongside --view.
assert.ok(
/--view/.test(content),
'plan-phase workflow must reference the --view flag'
);
const viewModePatterns = [
/view[ -]only/i,
/VIEW_ONLY/,
/no[ -]spawn/i,
/print[^\n]*RESEARCH\.md/i,
/display[^\n]*RESEARCH\.md/i,
];
const hits = viewModePatterns.filter((re) => re.test(content));
assert.ok(
hits.length > 0,
`plan-phase workflow must explain that --view prints existing RESEARCH.md without spawning; ` +
`expected one of [view-only, VIEW_ONLY, no-spawn, "print/display RESEARCH.md"]`
);
});
test('workflow uses --research as the force-refresh signal in research-only mode', () => {
const content = read('get-shit-done/workflows/plan-phase.md');
// The plan-phase workflow already had a --research flag with
// "force re-research" semantics. In research-only mode, that flag
// must short-circuit the "RESEARCH.md exists, what do you want to
// do?" prompt and unconditionally re-spawn. Assert the workflow
// documents the combined semantics.
const forceRefreshPatterns = [
/--research[^\n]*force[^\n]*refresh/i,
/--research[^\n]*re[ -]?research/i,
/force[ -]?refresh[^\n]*--research/i,
];
const hits = forceRefreshPatterns.filter((re) => re.test(content));
assert.ok(
hits.length > 0,
`plan-phase workflow must document that --research forces re-research (skip the "exists" prompt) when used with --research-phase`
);
});
test('workflow has an existing-RESEARCH.md prompt path (update/view/skip) within proximity', () => {
const content = read('get-shit-done/workflows/plan-phase.md');
// CR #3045 finding: the previous version of this test asserted
// `update`, `view`, `skip` appeared anywhere in the file, which was
// tautological — those words occur all over the workflow for
// unrelated reasons (--skip-research, --view flag declarations,
// etc.). Tighten to a proximity check: all three choice tokens
// must occur in a window of ~400 chars surrounding "RESEARCH.md
// already exists" / "Update — re-spawn" / equivalent prompt prose,
// proving the prompt section is genuinely present.
const idx = content.indexOf('RESEARCH.md already exists');
assert.ok(
idx >= 0,
'plan-phase workflow must contain the literal "RESEARCH.md already exists" prompt header in the research-only existing-artifact section'
);
const window = content.slice(idx, idx + 600);
const hasUpdate = /\b(?:update|refresh|re-spawn)\b/i.test(window);
const hasView = /\bview\b/i.test(window);
const hasSkip = /\bskip\b/i.test(window);
assert.ok(
hasUpdate && hasView && hasSkip,
`prompt section near "RESEARCH.md already exists" must mention all three choices (update/refresh/re-spawn, view, skip); ` +
`got update=${hasUpdate} view=${hasView} skip=${hasSkip}`
);
});
});
// ─── #3044: every deleted slash-command absent from user-facing surfaces ────
const DELETED_COMMANDS = [
'/gsd-check-todos', // → /gsd-capture --list
'/gsd-new-workspace', // → /gsd-workspace --new
'/gsd-status', // → /gsd-progress
'/gsd-plan-milestone-gaps', // → inline gap planning
'/gsd-research-phase', // → /gsd-plan-phase --research-phase
];
// Surfaces a user reads, browses, or follows routing prose from. Each
// must not emit any of the deleted slash-command tokens. Localized doc
// sets are included so the rename actually reaches every reader.
const USER_FACING_SURFACES = [
// Top-level repo
'README.md',
// Primary docs
'docs/USER-GUIDE.md',
'docs/FEATURES.md',
'docs/INVENTORY.md',
'docs/COMMANDS.md',
'docs/issue-driven-orchestration.md',
// Workflow surfaces that emit user-typed slash commands
'get-shit-done/workflows/check-todos.md',
'get-shit-done/workflows/add-todo.md',
'get-shit-done/workflows/resume-project.md',
'get-shit-done/workflows/progress.md',
'get-shit-done/workflows/code-review.md',
'get-shit-done/workflows/new-workspace.md',
'get-shit-done/workflows/list-workspaces.md',
'get-shit-done/workflows/transition.md',
'get-shit-done/workflows/discovery-phase.md',
// Reference + template surfaces
'get-shit-done/references/continuation-format.md',
'get-shit-done/templates/state.md',
'get-shit-done/templates/discovery.md',
'get-shit-done/templates/README.md',
];
describe('bug #3044: deleted slash-commands scrubbed from user-facing surfaces', () => {
for (const rel of USER_FACING_SURFACES) {
test(`${rel}: parsed slash-command token set excludes every deleted command`, () => {
if (!exists(rel)) {
// Some surfaces may not exist in every repo state (e.g. when the
// workflow file is itself removed in this PR). Skip with a
// structural note rather than a hard failure — the deletion is
// verified by extractSlashCommandTokens against the surfaces
// that DO exist.
return;
}
const tokens = extractSlashCommandTokens(read(rel));
for (const cmd of DELETED_COMMANDS) {
assert.equal(
tokens.has(cmd),
false,
`${rel}: parsed slash-command token set still contains deleted "${cmd}"`
);
}
});
}
});
// ─── Localized doc sets must also be scrubbed ───────────────────────────────
const LOCALES = ['ja-JP', 'ko-KR', 'zh-CN', 'pt-BR'];
const LOCALIZED_DOCS = [
'README.md',
'USER-GUIDE.md',
'FEATURES.md',
'COMMANDS.md',
];
describe('bug #3044: localized doc sets also scrubbed', () => {
for (const locale of LOCALES) {
for (const doc of LOCALIZED_DOCS) {
const rel = path.posix.join('docs', locale, doc);
test(`${rel}: parsed slash-command token set excludes every deleted command`, () => {
if (!exists(rel)) return; // some locales may not have every doc
const tokens = extractSlashCommandTokens(read(rel));
for (const cmd of DELETED_COMMANDS) {
assert.equal(
tokens.has(cmd),
false,
`${rel}: parsed slash-command token set still contains deleted "${cmd}"`
);
}
});
}
}
});
// ─── Replacement commands are documented ────────────────────────────────────
describe('replacement commands appear where the deleted ones used to live', () => {
test('docs/issue-driven-orchestration.md uses /gsd-workspace --new (not /gsd-new-workspace)', () => {
const content = read('docs/issue-driven-orchestration.md');
const tokens = extractSlashCommandTokens(content);
assert.equal(
tokens.has('/gsd-workspace'),
true,
'issue-driven-orchestration.md must reference /gsd-workspace as the workspace command'
);
// The "--new" flag must appear within 60 chars of /gsd-workspace —
// proves the flag belongs to that command rather than appearing
// somewhere unrelated.
const proximityRe = /\/gsd-workspace[^\n]{0,60}--new/;
assert.ok(
proximityRe.test(content),
'issue-driven-orchestration.md must document the "/gsd-workspace … --new" form'
);
});
test('workflows/code-review.md error path points at /gsd-progress (not /gsd-status)', () => {
const content = read('get-shit-done/workflows/code-review.md');
const tokens = extractSlashCommandTokens(content);
assert.equal(
tokens.has('/gsd-progress'),
true,
'code-review.md error path must route the user at /gsd-progress'
);
});
});

View File

@@ -91,7 +91,7 @@ function parseGuide() {
// Track which referenced commands appear at least once anywhere in the
// guide. This prevents drift if /gsd-* command names are renamed.
const requiredCommands = [
'/gsd-new-workspace',
'/gsd-workspace --new',
'/gsd-manager',
'/gsd-autonomous',
'/gsd-discuss-phase',
@@ -114,7 +114,7 @@ function parseGuide() {
statemd: /STATE\.md/.test(conceptMappingSection),
contextmd: /CONTEXT\.md/.test(conceptMappingSection),
planmd: /PLAN\.md/.test(conceptMappingSection),
workspaceCommand: /\/gsd-new-workspace/.test(conceptMappingSection),
workspaceCommand: /\/gsd-workspace\s+--new/.test(conceptMappingSection),
executionCommand:
/\/gsd-manager/.test(conceptMappingSection) ||
/\/gsd-autonomous/.test(conceptMappingSection),
@@ -202,7 +202,7 @@ describe('issue-driven-orchestration guide (#2840)', () => {
statemd: 'STATE.md must appear in the concept mapping',
contextmd: 'CONTEXT.md must appear in the concept mapping',
planmd: 'PLAN.md must appear in the concept mapping',
workspaceCommand: '/gsd-new-workspace must appear in the concept mapping',
workspaceCommand: '/gsd-workspace --new must appear in the concept mapping',
executionCommand:
'/gsd-manager or /gsd-autonomous must appear in the concept mapping',
verifyCommand: '/gsd-verify-work must appear in the concept mapping',