* 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>
321 lines
12 KiB
JavaScript
321 lines
12 KiB
JavaScript
/**
|
|
* Tests for docs/issue-driven-orchestration.md (#2840).
|
|
*
|
|
* Structural-IR assertions per CONTRIBUTING.md "Prohibited: Raw Text Matching
|
|
* on Test Outputs": parse the guide into a typed record and assert on
|
|
* semantic flags, not regex on prose. The guide is rebuildable as long as
|
|
* the structural invariants survive — section-level rewording is fine.
|
|
*
|
|
* Acceptance criteria from issue #2840:
|
|
* - One guide explaining issue-driven orchestration using existing GSD
|
|
* commands.
|
|
* - Concrete end-to-end issue → workspace → plan/execute → verify/review
|
|
* → PR flow.
|
|
* - Explicitly documents safety boundaries: isolated worktrees, explicit
|
|
* human review, no automatic public posting by default.
|
|
* - Adds no runtime dependencies / no new command, daemon, or tracker
|
|
* integration. (Test-enforced via concept-mapping audit.)
|
|
*/
|
|
|
|
// allow-test-rule: structural-IR parser for a docs guide. The .includes()
|
|
// calls below build a typed record (commandsPresent flags, conceptPairs
|
|
// flags, nonGoalFlags, safetyFlags); assertions run on those booleans, not
|
|
// on raw text. This is the documented escape hatch in
|
|
// scripts/lint-no-source-grep.cjs for doc-shape tests.
|
|
|
|
'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 GUIDE_PATH = path.join(__dirname, '..', 'docs', 'issue-driven-orchestration.md');
|
|
|
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Extract a section starting at a given heading. Returns the body up to (but
|
|
* not including) the next heading at the same or shallower depth, or null if
|
|
* the heading isn't found.
|
|
*/
|
|
function extractSection(content, heading) {
|
|
const lines = content.split('\n');
|
|
const headingRe = new RegExp(`^(#+)\\s+${heading.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}\\s*$`);
|
|
let start = -1;
|
|
let depth = 0;
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const m = lines[i].match(headingRe);
|
|
if (m) {
|
|
start = i + 1;
|
|
depth = m[1].length;
|
|
break;
|
|
}
|
|
}
|
|
if (start < 0) return null;
|
|
let end = lines.length;
|
|
for (let i = start; i < lines.length; i++) {
|
|
const m = lines[i].match(/^(#+)\s+/);
|
|
if (m && m[1].length <= depth) {
|
|
end = i;
|
|
break;
|
|
}
|
|
}
|
|
return lines.slice(start, end).join('\n');
|
|
}
|
|
|
|
/**
|
|
* Parse the guide into a typed record. Returns null when the guide is
|
|
* missing so the file-presence test can name the actual problem instead of
|
|
* cascading TypeErrors.
|
|
*/
|
|
function parseGuide() {
|
|
if (!fs.existsSync(GUIDE_PATH)) return null;
|
|
const content = fs.readFileSync(GUIDE_PATH, 'utf8');
|
|
// Strip inline emphasis but NOT underscores (snake_case identifiers like
|
|
// gsd-new-workspace, .planning/, etc. must survive).
|
|
const stripped = content.replace(/\*{1,3}|~{2}/g, '');
|
|
|
|
// Concept-mapping table: rows that pair a Symphony-style concept with a
|
|
// GSD primitive. Test asserts on presence of each required pair, not on
|
|
// exact prose ordering.
|
|
const conceptMappingSection = extractSection(content, 'Concept mapping');
|
|
const endToEndSection = extractSection(content, 'End-to-end flow') ||
|
|
extractSection(content, 'End-to-end issue → PR flow') ||
|
|
extractSection(content, 'End-to-end orchestration loop');
|
|
const safetySection = extractSection(content, 'Safety boundaries') ||
|
|
extractSection(content, 'Safety');
|
|
const nonGoalsSection = extractSection(content, 'Non-goals') ||
|
|
extractSection(content, 'What this guide does not do');
|
|
|
|
// Track which referenced commands appear at least once anywhere in the
|
|
// guide. This prevents drift if /gsd-* command names are renamed.
|
|
const requiredCommands = [
|
|
'/gsd-workspace --new',
|
|
'/gsd-manager',
|
|
'/gsd-autonomous',
|
|
'/gsd-discuss-phase',
|
|
'/gsd-plan-phase',
|
|
'/gsd-execute-phase',
|
|
'/gsd-verify-work',
|
|
'/gsd-review',
|
|
'/gsd-ship',
|
|
];
|
|
const commandsPresent = Object.fromEntries(
|
|
requiredCommands.map((c) => [c, content.includes(c)])
|
|
);
|
|
|
|
// Concept-mapping invariants — keys are concept slugs, values are the
|
|
// GSD primitive that must appear in the same paragraph/row of the
|
|
// concept-mapping section.
|
|
const conceptPairs = conceptMappingSection
|
|
? {
|
|
roadmap: /ROADMAP\.md/.test(conceptMappingSection),
|
|
statemd: /STATE\.md/.test(conceptMappingSection),
|
|
contextmd: /CONTEXT\.md/.test(conceptMappingSection),
|
|
planmd: /PLAN\.md/.test(conceptMappingSection),
|
|
workspaceCommand: /\/gsd-workspace\s+--new/.test(conceptMappingSection),
|
|
executionCommand:
|
|
/\/gsd-manager/.test(conceptMappingSection) ||
|
|
/\/gsd-autonomous/.test(conceptMappingSection),
|
|
verifyCommand: /\/gsd-verify-work/.test(conceptMappingSection),
|
|
reviewCommand: /\/gsd-review/.test(conceptMappingSection),
|
|
shipCommand: /\/gsd-ship/.test(conceptMappingSection),
|
|
}
|
|
: null;
|
|
|
|
// Non-goals required by the issue: must explicitly disclaim all four.
|
|
const nonGoalFlags = nonGoalsSection
|
|
? {
|
|
noVendoring: /vendor|copy/i.test(nonGoalsSection),
|
|
noDaemon: /daemon|polling/i.test(nonGoalsSection),
|
|
noTrackerDependency: /tracker.*depend|mandatory.*track/i.test(nonGoalsSection),
|
|
noBypassReview: /bypass|review|verification|human.*decision|human gate/i.test(nonGoalsSection),
|
|
}
|
|
: null;
|
|
|
|
// Safety boundaries — required disclaimers about how the loop stays safe.
|
|
const safetyFlags = safetySection
|
|
? {
|
|
isolatedWorktrees: /worktree|isolated/i.test(safetySection),
|
|
explicitReview: /review|human.*gate|human.*approval/i.test(safetySection),
|
|
noAutoPosting: /not.*automatic|no.*auto|explicit.*confirm|user.*confirm|human.*confirm/i.test(safetySection),
|
|
}
|
|
: null;
|
|
|
|
// End-to-end flow must enumerate at least the seven step sequence the
|
|
// acceptance criteria call out. We assert on numbered list items so the
|
|
// narrative can be reworded freely.
|
|
const numberedSteps = endToEndSection
|
|
? (endToEndSection.match(/^\s*\d+\.\s+/gm) || []).length
|
|
: 0;
|
|
|
|
// Strip markdown emphasis when checking for snake_case-sensitive content
|
|
// in section bodies (per the markdown-aware matching pattern).
|
|
const strippedConceptMapping = conceptMappingSection
|
|
? conceptMappingSection.replace(/\*{1,3}|~{2}/g, '')
|
|
: null;
|
|
|
|
return {
|
|
raw: content,
|
|
stripped,
|
|
conceptMappingSection,
|
|
strippedConceptMapping,
|
|
endToEndSection,
|
|
safetySection,
|
|
nonGoalsSection,
|
|
commandsPresent,
|
|
conceptPairs,
|
|
nonGoalFlags,
|
|
safetyFlags,
|
|
numberedSteps,
|
|
};
|
|
}
|
|
|
|
// ─── Tests ──────────────────────────────────────────────────────────────────
|
|
|
|
describe('issue-driven-orchestration guide (#2840)', () => {
|
|
test('docs/issue-driven-orchestration.md exists', () => {
|
|
assert.ok(
|
|
fs.existsSync(GUIDE_PATH),
|
|
`Guide must live at docs/issue-driven-orchestration.md per #2840`
|
|
);
|
|
});
|
|
|
|
test('every required GSD command is referenced at least once', () => {
|
|
const ir = parseGuide();
|
|
assert.ok(ir, 'parseGuide returned null — guide is missing');
|
|
for (const [cmd, present] of Object.entries(ir.commandsPresent)) {
|
|
assert.ok(present, `guide must reference ${cmd}`);
|
|
}
|
|
});
|
|
|
|
test('concept mapping section exists and pairs Symphony concepts with GSD primitives', () => {
|
|
const ir = parseGuide();
|
|
assert.ok(ir, 'guide must be present');
|
|
assert.ok(
|
|
ir.conceptMappingSection,
|
|
'guide must contain a "Concept mapping" section'
|
|
);
|
|
const expected = {
|
|
roadmap: 'ROADMAP.md must appear in the concept mapping',
|
|
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-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',
|
|
reviewCommand: '/gsd-review must appear in the concept mapping',
|
|
shipCommand: '/gsd-ship must appear in the concept mapping',
|
|
};
|
|
for (const [flag, msg] of Object.entries(expected)) {
|
|
assert.equal(ir.conceptPairs[flag], true, msg);
|
|
}
|
|
});
|
|
|
|
test('safety boundaries section names isolation, review, and non-auto-posting', () => {
|
|
const ir = parseGuide();
|
|
assert.ok(ir, 'guide must be present');
|
|
assert.ok(
|
|
ir.safetySection,
|
|
'guide must contain a "Safety boundaries" or "Safety" section'
|
|
);
|
|
assert.equal(
|
|
ir.safetyFlags.isolatedWorktrees,
|
|
true,
|
|
'safety section must mention isolated worktrees'
|
|
);
|
|
assert.equal(
|
|
ir.safetyFlags.explicitReview,
|
|
true,
|
|
'safety section must require explicit human review'
|
|
);
|
|
assert.equal(
|
|
ir.safetyFlags.noAutoPosting,
|
|
true,
|
|
'safety section must disclaim automatic public posting'
|
|
);
|
|
});
|
|
|
|
test('non-goals section disclaims vendoring, daemon, tracker dependency, and gate-bypass', () => {
|
|
const ir = parseGuide();
|
|
assert.ok(ir, 'guide must be present');
|
|
assert.ok(
|
|
ir.nonGoalsSection,
|
|
'guide must contain a "Non-goals" section'
|
|
);
|
|
const expected = {
|
|
noVendoring: 'must disclaim copying/vendoring Symphony',
|
|
noDaemon: 'must disclaim a long-running daemon',
|
|
noTrackerDependency: 'must disclaim mandatory tracker dependency',
|
|
noBypassReview: 'must disclaim bypassing review/verification gates',
|
|
};
|
|
for (const [flag, msg] of Object.entries(expected)) {
|
|
assert.equal(ir.nonGoalFlags[flag], true, msg);
|
|
}
|
|
});
|
|
|
|
test('end-to-end flow enumerates at least 7 numbered steps (per acceptance criteria)', () => {
|
|
const ir = parseGuide();
|
|
assert.ok(ir, 'guide must be present');
|
|
assert.ok(
|
|
ir.endToEndSection,
|
|
'guide must contain an "End-to-end flow" (or equivalent) section'
|
|
);
|
|
assert.ok(
|
|
ir.numberedSteps >= 7,
|
|
`end-to-end section must enumerate ≥7 numbered steps; found ${ir.numberedSteps}`
|
|
);
|
|
});
|
|
|
|
test('every fenced code block has a language tag (markdownlint MD040)', () => {
|
|
const ir = parseGuide();
|
|
assert.ok(ir, 'guide must be present');
|
|
// Pair fence opens; flag any opener with no language tag.
|
|
const fences = ir.raw.match(/^```.*$/gm) || [];
|
|
const openers = [];
|
|
for (let i = 0; i < fences.length; i++) {
|
|
// Even index = opener, odd = closer. An opener with empty trailing
|
|
// text is MD040.
|
|
if (i % 2 === 0) openers.push(fences[i]);
|
|
}
|
|
const bare = openers.filter((f) => /^```\s*$/.test(f));
|
|
assert.equal(
|
|
bare.length,
|
|
0,
|
|
`MD040: ${bare.length} fenced block(s) lack a language tag`
|
|
);
|
|
});
|
|
|
|
test('cross-linked from docs/README.md', () => {
|
|
const readme = path.join(__dirname, '..', 'docs', 'README.md');
|
|
if (!fs.existsSync(readme)) {
|
|
// docs/README.md is the discovery surface. Without a cross-link, the
|
|
// guide is invisible to users browsing docs/.
|
|
return; // tolerate absence; test below ensures FEATURES.md anchor.
|
|
}
|
|
const txt = fs.readFileSync(readme, 'utf8');
|
|
assert.ok(
|
|
/issue-driven-orchestration/.test(txt),
|
|
'docs/README.md must link to the new guide'
|
|
);
|
|
});
|
|
|
|
test('cross-linked from docs/USER-GUIDE.md', () => {
|
|
const guide = path.join(__dirname, '..', 'docs', 'USER-GUIDE.md');
|
|
// Mirror the null-guard pattern from the README test above: a missing
|
|
// file must produce a meaningful assertion message, not a cryptic
|
|
// ENOENT stack trace. (CR #3036.)
|
|
assert.ok(
|
|
fs.existsSync(guide),
|
|
'docs/USER-GUIDE.md must exist for cross-link validation'
|
|
);
|
|
const txt = fs.readFileSync(guide, 'utf8');
|
|
assert.ok(
|
|
/issue-driven-orchestration/.test(txt),
|
|
'docs/USER-GUIDE.md must link to the new guide'
|
|
);
|
|
});
|
|
});
|