docs: add issue-driven orchestration guide (#2840) (#3036)

* docs: add issue-driven orchestration guide (#2840)

Adds docs/issue-driven-orchestration.md — a recipe for driving GSD from a
GitHub / Linear / Jira issue using existing primitives. Maps Symphony-style
orchestration concepts onto GSD commands without vendoring code, adding a
daemon, or introducing tracker integration.

Concept mapping covers:
- WORKFLOW.md → ROADMAP.md / STATE.md / phase CONTEXT.md / phase PLAN.md
- isolated agent workspace → /gsd-new-workspace --strategy worktree
- agent dispatch → /gsd-manager (interactive), /gsd-autonomous (unattended)
- per-phase steps → /gsd-discuss-phase → /gsd-plan-phase → /gsd-execute-phase
- proof-of-work → /gsd-verify-work (UAT.md persists across /clear)
- adversarial review → /gsd-review (cross-AI peer review)
- human merge gate → /gsd-ship
- follow-up capture → /gsd-note, /gsd-plant-seed, /gsd-new-milestone

End-to-end flow walks through 7 numbered steps from picking the tracker
issue to capturing follow-ups. Safety boundaries (isolated worktrees,
explicit human review, no automatic public posting, verification before
ship) and non-goals (no vendoring, no daemon, no mandatory tracker, no
gate bypass, no command-surface expansion) are spelled out explicitly so
the doc cannot drift into "let's just add one more flag".

Cross-linked from docs/README.md (Documentation Index) and
docs/USER-GUIDE.md (Table of Contents preamble).

Tests: tests/feat-2840-issue-driven-orchestration-guide.test.cjs — 9
structural-IR tests parse the guide into a typed record and assert on
flags (commandsPresent, conceptPairs, nonGoalFlags, safetyFlags,
numberedSteps). Fence-language MD040 check enforced. Cross-link
presence enforced. No raw-text assertions on prose.

6890/6890 tests pass. Lint:tests clean (allow-test-rule comment justifies
the doc-shape parser per scripts/lint-no-source-grep.cjs escape hatch).
Lint:changeset clean.

Closes #2840

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

* fix(test): guard USER-GUIDE.md existsSync before read (CR #3036)

CR Minor: cross-linked-from-USER-GUIDE.md test called fs.readFileSync
directly without first asserting fs.existsSync, asymmetric with the
README.md test above. A missing USER-GUIDE.md would throw ENOENT instead
of producing a meaningful assertion message. Mirror the null-guard
pattern.

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 16:57:42 -04:00
committed by GitHub
parent 95d2bc20f8
commit 117b3ec009
5 changed files with 514 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
---
type: Added
pr: 2840
---
**`docs/issue-driven-orchestration.md` — recipe for driving GSD from a tracker issue** — new guide that maps Symphony-style orchestration concepts (workflow, isolated agent workspace, proof-of-work, human review gate, follow-up capture) onto existing GSD primitives (`/gsd-new-workspace`, `/gsd-manager`, `/gsd-autonomous`, `/gsd-verify-work`, `/gsd-review`, `/gsd-ship`, `STATE.md`, phase artifacts). Documentation only — no new commands, no daemon, no tracker integration.

View File

@@ -15,6 +15,7 @@ Language versions: [English](README.md) · [Português (pt-BR)](pt-BR/README.md)
| [CLI Tools Reference](CLI-TOOLS.md) | Contributors, agent authors | `gsd-tools.cjs` programmatic API for workflows and agents |
| [Agent Reference](AGENTS.md) | Contributors, advanced users | Role cards for primary agents — roles, tools, spawn patterns (the `agents/` filesystem is authoritative) |
| [User Guide](USER-GUIDE.md) | All users | Workflow walkthroughs, troubleshooting, and recovery |
| [Issue-Driven Orchestration](issue-driven-orchestration.md) | All users | Recipe for driving GSD from a tracker issue (GitHub / Linear / Jira) using existing primitives — no new commands or daemon |
| [Context Monitor](context-monitor.md) | All users | Context window monitoring hook architecture |
| [Discuss Mode](workflow-discuss-mode.md) | All users | Assumptions vs interview mode for discuss-phase |

View File

@@ -18,6 +18,11 @@ A detailed reference for workflows, troubleshooting, and configuration. For quic
- [Troubleshooting](#troubleshooting)
- [Recovery Quick Reference](#recovery-quick-reference)
For driving GSD directly from a GitHub / Linear / Jira issue, see the
[Issue-Driven Orchestration guide](issue-driven-orchestration.md) — a
recipe that maps tracker issues onto the workspace → discuss → plan →
execute → verify → review → ship loop using existing GSD primitives.
---
## End-to-End Walkthrough

View File

@@ -0,0 +1,183 @@
# Issue-Driven Orchestration with GSD
**Status:** stable workflow guide
**Audience:** developers who track work in GitHub Issues, Linear, Jira, or
similar issue trackers and want to drive AI-assisted implementation
through GSD's existing primitives.
## What this guide is
A recipe for combining commands GSD already ships into an issue-tracker
→ workspace → plan/execute → verify/review → PR loop. It is documentation
only. No new commands, no daemon, no tracker integration — every command
referenced below already exists in GSD today.
The shape is inspired by OpenAI's open-source [Symphony orchestration
reference](https://openai.com/index/open-source-codex-orchestration-symphony/)
([repository](https://github.com/openai/symphony)). GSD does not vendor or
wrap Symphony. The orchestration *concepts* map cleanly onto primitives
GSD already exposes; this guide just spells the mapping out so you can
adopt the pattern without writing glue code or bypassing GSD's safety
gates.
## Why this exists
GSD has the building blocks for issue-driven AI development —
`/gsd-new-workspace`, `/gsd-manager`, `/gsd-autonomous`, `/gsd-verify-work`,
`/gsd-review`, `/gsd-ship`, plus `STATE.md` and the phase artifact suite
— but no guide that walks through how to drive them from a single tracker
issue without writing custom orchestration scripts. Without that guide
the failure modes are:
- Underuse: developers run discuss/plan/execute manually and never reach
for `/gsd-manager` or `/gsd-autonomous` even when their work pattern
fits.
- Workaround scripts: developers wire ad-hoc shell loops between their
tracker and `claude` invocations, bypassing `STATE.md`, the phase
manifest, and the verification gates.
This guide makes the canonical loop discoverable.
## Concept mapping
Each row maps a Symphony-style orchestration concept to the GSD primitive
that already serves it. Use this table as a translation key when reading
Symphony docs, blog posts, or third-party orchestration write-ups.
| Symphony concept | GSD primitive |
|---|---|
| `WORKFLOW.md` (top-level intent) | `ROADMAP.md` (project intent), `STATE.md` (live status), phase `CONTEXT.md` (per-phase scope), phase `PLAN.md` (executable steps) |
| One isolated agent workspace per task | `/gsd-new-workspace --strategy worktree` |
| Agent dispatch and concurrency | `/gsd-manager` (interactive dashboard), `/gsd-autonomous` (unattended) |
| Per-phase plan and discuss steps | `/gsd-discuss-phase` → `/gsd-plan-phase` → `/gsd-execute-phase` |
| Proof-of-work / test evidence | `/gsd-verify-work` (UAT.md persisted across `/clear`) |
| Adversarial review | `/gsd-review` (cross-AI peer review of plans) |
| Human merge gate | `/gsd-ship` (creates PR, optional code review, prepares merge) |
| Follow-up capture | `/gsd-note`, `/gsd-plant-seed`, `/gsd-new-milestone`, or a manually opened tracker issue |
| Concurrency control | Manager / background-agent semantics (no always-on poller) |
The mapping is one-way: GSD owns the safety gates (verification, human
review, explicit confirmation for follow-up creation). Symphony's
"continuous orchestration" framing is intentionally not adopted — see
[Non-goals](#non-goals).
## End-to-end flow
The canonical issue → PR loop, written so it can run from a single
tracker issue end-to-end. Replace bracketed placeholders before running.
1. **Pick the tracker issue.** Choose one issue from your tracker (GitHub,
Linear, etc.) that is well-scoped enough for autonomous implementation
— bounded scope, observable acceptance criteria, no upstream
dependencies that block execution.
2. **Map to a GSD phase.** If the issue maps onto an existing phase in
`ROADMAP.md`, select it. If not, run `/gsd-new-milestone` (for a new
milestone of related issues) or open a phase via `/gsd-add-phase` /
`/gsd-insert-phase`. Capture the tracker issue URL in the phase's
`CONTEXT.md` so traceability survives compaction.
3. **Create an isolated workspace.** Run
`/gsd-new-workspace --strategy worktree <slug>` to spin up a git
worktree with an independent `.planning/` directory. The worktree is
the safety boundary: any exploration, partial commits, or aborted
plans stay outside `main`.
4. **Run discuss → plan → execute through GSD.** From inside the
workspace, run `/gsd-discuss-phase` to clarify ambiguities,
`/gsd-plan-phase` to produce `PLAN.md`, and either `/gsd-manager`
(interactive dashboard) or `/gsd-execute-phase` / `/gsd-autonomous`
(unattended) to implement. Avoid driving raw `claude` invocations
from outside GSD — that bypasses `STATE.md` updates and the phase
manifest.
5. **Demand proof-of-work.** Run `/gsd-verify-work` to walk the user
through UAT against the phase's acceptance criteria. Tests,
screenshots, log captures, and config diffs are all recorded in
`UAT.md`, which persists across `/clear` and feeds gaps into
`/gsd-plan-phase --gaps` when verification surfaces missed scope.
6. **Pass through the review and ship gates.** Run `/gsd-review` to get
adversarial peer review of the plan from independent AI CLIs (catches
blind spots model-by-model), then `/gsd-ship` to open the PR with a
rich body assembled from the planning artifacts. Both gates require a
human decision before anything reaches the remote.
7. **Capture follow-up work explicitly.** Use `/gsd-note` for inline
notes, `/gsd-plant-seed` for ideas worth a future phase, or
`/gsd-new-milestone` for a coherent group of follow-ups. Creating a
tracker issue from a discovered follow-up requires explicit user
confirmation — GSD does not post to remote trackers automatically.
When the PR merges, the loop closes. Auto-close keywords in the PR body
(`Closes #NNN` / `Fixes #NNN`) close the tracker issue at merge time.
## Safety boundaries
The loop is safe because four invariants hold by construction:
- **Isolated worktrees.** Every issue runs in a `/gsd-new-workspace`
worktree, so partial work, aborted plans, and exploratory commits
never touch `main`. `gsd-local-patches/` is the recovery surface if a
worktree's hand-edits need to come back across an update.
- **Explicit human review.** `/gsd-review` and `/gsd-ship` both stop for
human approval. There is no auto-merge and no auto-PR-from-execution
path. If you want to remove the human gate for a specific repository,
that is your branch-protection / merge-queue policy decision, not
something GSD opts into for you.
- **No automatic public posting.** GSD never opens, comments on, or
closes a tracker issue without an explicit user-initiated command.
Follow-up capture defaults to local artifacts (notes, seeds,
milestones); pushing back to the tracker is a separate manual step.
- **Verification before ship.** `/gsd-verify-work`'s UAT.md must record
evidence before `/gsd-ship` is run. The recommended discipline is to
treat `verification_failed` as a blocker even when the implementation
looks correct — the failure usually surfaces a missed acceptance
criterion, not a flaky test.
If any of these invariants is bypassed (e.g. running `claude` directly
against the worktree, skipping `/gsd-verify-work`, or scripting issue
creation through the tracker API without user confirmation), the
guarantees of this guide do not apply.
## Non-goals
This guide deliberately does **not** propose any of the following. They
are listed here so future contributors don't re-litigate them in code
review:
- **No vendoring or copying Symphony code.** GSD reuses its own
primitives. The mapping above is conceptual; no Symphony-derived
source ships in this repo.
- **No long-running daemon.** GSD does not poll GitHub or Linear. The
manager and autonomous workflows handle concurrency through
background-agent semantics, not a daemon.
- **No mandatory tracker dependency.** The loop works without any
tracker integration. The "tracker issue" step is a *human input* —
the URL goes into `CONTEXT.md`. GSD has no opinion about which
tracker you use, or whether you use one at all.
- **No bypass of verification, review, or human decision gates.** Even
when running `/gsd-autonomous`, the verification and review gates
still fire. The "autonomous" label refers to phase-to-phase
progression, not to skipping human approval.
- **No expansion of the default skill / command surface.** Every
command referenced in this guide already exists. This guide is a
documentation surface, not a feature surface.
## Possible future follow-up
If maintainer experience with this loop justifies it, a separate
approved-enhancement could later add a *minimal* tracker bridge:
- Importing one GitHub or Linear issue into a GSD workspace / phase.
- Exporting `UAT.md` evidence as a comment on the source issue.
- Generating follow-up tracker issues from `/gsd-plant-seed` output.
Each of those would be its own enhancement proposal because each adds
integration surface and ongoing maintenance burden. They are out of
scope for this guide.
## Related
- [docs/USER-GUIDE.md](USER-GUIDE.md) — task-oriented walkthroughs of
individual commands referenced above.
- [docs/COMMANDS.md](COMMANDS.md) — full reference for `/gsd-*`
commands.
- [docs/FEATURES.md](FEATURES.md) — feature-level capability matrix
(workspaces, manager, autonomous, verify, review, ship).
- [docs/ARCHITECTURE.md](ARCHITECTURE.md) — phase-artifact lifecycle
and `STATE.md` mechanics.

View File

@@ -0,0 +1,320 @@
/**
* 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-new-workspace',
'/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-new-workspace/.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-new-workspace 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'
);
});
});