Merge branch 'open-gsd:next' into kimi-runtime-support

This commit is contained in:
Viktorplus
2026-06-07 00:53:20 +02:00
committed by GitHub
15 changed files with 245 additions and 51 deletions

View File

@@ -0,0 +1,5 @@
---
type: Changed
pr: 746
---
**`/gsd:plan-phase` and `/gsd:execute-phase` no longer eagerly load MVP-only guidance on non-MVP runs** — the MVP planner rules, user-story template, Walking-Skeleton template, and MVP+TDD halt-report reference are now Read lazily by the planner/executor only when MVP / Walking-Skeleton / MVP+TDD mode is active, in both the workflow files and the `gsd-planner`/`gsd-executor` agent definitions, instead of being `@`-imported into every run. Behaviour is unchanged; non-MVP planning/execution simply carries less context. (#720)

1
.gitignore vendored
View File

@@ -71,6 +71,7 @@ build/
/gsd-core/bin/lib/package-legitimacy.cjs
/gsd-core/bin/lib/semver-compare.cjs
/gsd-core/bin/lib/config-types.cjs
/gsd-core/bin/lib/cli-exit.cjs
/gsd-core/bin/lib/code-review-flags.cjs
/gsd-core/bin/lib/context-utilization.cjs
/gsd-core/bin/lib/artifacts.cjs

View File

@@ -385,7 +385,7 @@ If RED or GREEN gate commits are missing, add a warning to SUMMARY.md under a `#
## MVP+TDD Gate
**When the orchestrator passes both `MVP_MODE=true` and `TDD_MODE=true`:** Before running the implementation step of any task with `tdd="true"`, run the runtime gate from `@~/.claude/gsd-core/references/execute-mvp-tdd.md`. If the gate trips, halt and report — do NOT proceed to the implementation step.
**When the orchestrator passes both `MVP_MODE=true` and `TDD_MODE=true`:** Before running the implementation step of any task with `tdd="true"`, run the runtime gate from `~/.claude/gsd-core/references/execute-mvp-tdd.md` (Read it). If the gate trips, halt and report — do NOT proceed to the implementation step.
**Halt-and-report protocol:**

View File

@@ -309,7 +309,7 @@ Exceptions where `tdd="true"` is not needed: `type="checkpoint:*"` tasks, config
## MVP Mode Detection
**When `MVP_MODE` is enabled (passed by the plan-phase orchestrator):** Decompose tasks as **vertical feature slices**, not horizontal layers. Required reading: `@~/.claude/gsd-core/references/planner-mvp-mode.md` (loaded conditionally by the orchestrator).
**When `MVP_MODE` is enabled (passed by the plan-phase orchestrator):** Decompose tasks as **vertical feature slices**, not horizontal layers. Required reading: Read `~/.claude/gsd-core/references/planner-mvp-mode.md` for the vertical-slice rules (lazy — only on MVP runs).
**Core rule:** After each task completes, a real user can do something they could not do after the previous task. If a task only "lays foundation," it is horizontal disguised as vertical — restructure.
@@ -323,7 +323,7 @@ Exceptions where `tdd="true"` is not needed: `type="checkpoint:*"` tasks, config
**As a** [user role], **I want to** [capability], **so that** [outcome].
```
Format rules from `@~/.claude/gsd-core/references/user-story-template.md`:
Format rules (Read `~/.claude/gsd-core/references/user-story-template.md`):
- All three slots required. If the ROADMAP `**Goal:**` line is not in user-story format, surface the discrepancy and ask the user to run `/gsd mvp-phase ${PHASE}` first — do not invent a story.
- Bold the three keywords (`**As a**`, `**I want to**`, `**so that**`) when emitting to PLAN.md. The ROADMAP form does not use bolded keywords; the PLAN form does.
2. First task: failing end-to-end test for the happy path.
@@ -332,7 +332,7 @@ Exceptions where `tdd="true"` is not needed: `type="checkpoint:*"` tasks, config
**Mode is all-or-nothing per phase** (PRD decision Q1). Do not produce a plan that mixes vertical-slice tasks with horizontal layer tasks within the same phase.
**Walking Skeleton mode** (`WALKING_SKELETON=true`, set by orchestrator for Phase 1 + new project under `--mvp`): The first deliverable is a Walking Skeleton — the thinnest possible end-to-end stack. In addition to `PLAN.md`, produce `SKELETON.md` using the template at `@~/.claude/gsd-core/references/skeleton-template.md`. `SKELETON.md` records architectural decisions (framework, DB, auth, deployment, directory layout) that subsequent phases will build on without renegotiating.
**Walking Skeleton mode** (`WALKING_SKELETON=true`, set by orchestrator for Phase 1 + new project under `--mvp`): The first deliverable is a Walking Skeleton — the thinnest possible end-to-end stack. In addition to `PLAN.md`, produce `SKELETON.md` using the template at `~/.claude/gsd-core/references/skeleton-template.md` (Read it now). `SKELETON.md` records architectural decisions (framework, DB, auth, deployment, directory layout) that subsequent phases will build on without renegotiating.
**Compatibility with TDD detection:** When both `MVP_MODE=true` and `workflow.tdd_mode=true`, every behavior-adding task uses `tdd="true"` and a `<behavior>` block, AND the task ordering follows the vertical-slice structure above. The first task is always a failing end-to-end test.

View File

@@ -194,6 +194,16 @@ parent dispatches, modes/ holds per-flag behavior (`power.md`, `all.md`,
checkpoint.json schemas that are read only when the corresponding output
file is being written.
`workflows/plan-phase.md`, `workflows/execute-phase.md`, and the
`gsd-planner` / `gsd-executor` agent definitions apply the same discipline
to their MVP-only reference bodies — `planner-mvp-mode.md`,
`user-story-template.md`, `skeleton-template.md`, and `execute-mvp-tdd.md`
are referenced for the planner/executor to Read only on MVP,
Walking-Skeleton, or MVP+TDD paths, rather than eagerly `@`-imported, so
non-MVP runs do not pay their context cost (guards against the "`@`-import
behind a conditional still loads eagerly" leak; see #720). The dedicated
`mvp-phase` workflow keeps its eager imports, since it is always MVP.
### Agents (`agents/*.md`)
Specialized agent definitions with frontmatter specifying:

View File

@@ -1,5 +1,5 @@
{
"generated": "2026-06-05",
"generated": "2026-06-06",
"families": {
"agents": [
"gsd-advisor-researcher",
@@ -272,6 +272,7 @@
"audit.cjs",
"check-command-router.cjs",
"cjs-command-router-adapter.cjs",
"cli-exit.cjs",
"clock.cjs",
"clusters.cjs",
"code-review-flags.cjs",

View File

@@ -370,7 +370,7 @@ The `gsd-planner` agent is decomposed into a core agent plus reference modules t
---
## CLI Modules (85 shipped)
## CLI Modules (86 shipped)
Full listing: `gsd-core/bin/lib/*.cjs`.
@@ -382,6 +382,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
| `artifacts.cjs` | Canonical artifact registry — known `.planning/` root file names; used by `gsd-health` W019 lint |
| `audit.cjs` | Audit dispatch, audit open sessions, audit storage helpers |
| `check-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools check` |
| `cli-exit.cjs` | `ExitError` class and `runMain()` helper — CLI entrypoints throw `ExitError` instead of calling `process.exit()`; `runMain()` translates the outcome into `process.exitCode` so output flushes cleanly |
| `cjs-command-router-adapter.cjs` | Shared compatibility adapter for manifest-backed CJS command-family routers |
| `clock.cjs` | Injectable clock seam (now/sleep) for deterministic lock testing |
| `clusters.cjs` | Skill cluster definitions for the runtime surface module (ADR-0011 Phase 2) |

View File

@@ -35,6 +35,7 @@ export default tseslint.config(
'**/*.generated.cjs',
// ADR-457: tsc-generated runtime artifact — lint the src/*.cts source, not the emitted .cjs.
'gsd-core/bin/lib/semver-compare.cjs',
'gsd-core/bin/lib/cli-exit.cjs',
'gsd-core/bin/lib/code-review-flags.cjs',
'gsd-core/bin/lib/context-utilization.cjs',
'gsd-core/bin/lib/artifacts.cjs',
@@ -174,7 +175,7 @@ export default tseslint.config(
'no-useless-escape': 'warn',
'no-unsafe-finally': 'warn',
// eslint-plugin-n rules
'n/no-process-exit': 'warn',
'n/no-process-exit': 'error',
// Local rules — warn for now; flip to error after cleanup phases
'local/no-source-grep': 'warn',
},

View File

@@ -21,6 +21,7 @@
*/
const { execNpm } = require('./lib/shell-command-projection.cjs');
const { runMain } = require('./lib/cli-exit.cjs');
// Sourced from the single Package Identity seam (#498), not re-typed. The seam
// bakes the value from package.json at build time, so it is a code constant —
@@ -98,9 +99,9 @@ function main() {
} else {
process.stderr.write(`check-latest-version: ${r.reason}: ${r.detail}\n`);
}
process.exit(r.ok ? 0 : 1);
return r.ok ? 0 : 1;
}
if (require.main === module) main();
if (require.main === module) runMain(main);
module.exports = { checkLatestVersion, CHECK_REASON, PACKAGE_NAME };

View File

@@ -170,6 +170,7 @@
const fs = require('fs');
const path = require('path');
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
const core = require('./lib/core.cjs');
const { error, ERROR_REASON } = core;
// Resolve findProjectRoot lazily at call time rather than binding it at module
@@ -1527,33 +1528,26 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
// ── Validate required args ─────────────────────────────────────────
if (!budgetStr) {
process.stderr.write('Error: --budget <N> is required\n');
process.exit(1);
throw new ExitError(1, 'Error: --budget <N> is required');
}
const budget = parseInt(budgetStr, 10);
if (!Number.isFinite(budget) || budget <= 0) {
process.stderr.write('Error: --budget must be a positive integer\n');
process.exit(1);
throw new ExitError(1, 'Error: --budget must be a positive integer');
}
if (!instructionsFile) {
process.stderr.write('Error: --instructions-file <path> is required\n');
process.exit(1);
throw new ExitError(1, 'Error: --instructions-file <path> is required');
}
if (!roadmapFile) {
process.stderr.write('Error: --roadmap-file <path> is required\n');
process.exit(1);
throw new ExitError(1, 'Error: --roadmap-file <path> is required');
}
if (planFiles.length === 0) {
process.stderr.write('Error: at least one --plan-file <path> is required\n');
process.exit(1);
throw new ExitError(1, 'Error: at least one --plan-file <path> is required');
}
if (!outputPromptFile) {
process.stderr.write('Error: --output-prompt <path> is required\n');
process.exit(1);
throw new ExitError(1, 'Error: --output-prompt <path> is required');
}
if (!outputMetadataFile) {
process.stderr.write('Error: --output-metadata <path> is required\n');
process.exit(1);
throw new ExitError(1, 'Error: --output-metadata <path> is required');
}
// ── Validate and read required files ──────────────────────────────
@@ -1563,11 +1557,9 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
return await fs.promises.readFile(resolved, 'utf8');
} catch (err) {
if (err && err.code === 'ENOENT') {
process.stderr.write(`Error: file not found for ${flagName}: ${resolved}\n`);
process.exit(1);
throw new ExitError(1, `Error: file not found for ${flagName}: ${resolved}`);
}
process.stderr.write(`Error: cannot read file for ${flagName}: ${resolved}\n`);
process.exit(1);
throw new ExitError(1, `Error: cannot read file for ${flagName}: ${resolved}`);
}
}
@@ -1578,8 +1570,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
return await fs.promises.readFile(resolved, 'utf8');
} catch (err) {
if (err && err.code === 'ENOENT') return null;
process.stderr.write(`Error: cannot read optional file: ${resolved}\n`);
process.exit(1);
throw new ExitError(1, `Error: cannot read optional file: ${resolved}`);
}
}
@@ -1592,11 +1583,9 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
return { file: path.basename(p), content };
} catch (err) {
if (err && err.code === 'ENOENT') {
process.stderr.write(`Error: plan file not found: ${resolved}\n`);
process.exit(1);
throw new ExitError(1, `Error: plan file not found: ${resolved}`);
}
process.stderr.write(`Error: cannot read plan file: ${resolved}\n`);
process.exit(1);
throw new ExitError(1, `Error: cannot read plan file: ${resolved}`);
}
}));
@@ -1625,7 +1614,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
await fs.promises.writeFile(path.resolve(outputPromptFile), prompt);
if (metadata.hardFailed) {
process.exit(2);
throw new ExitError(2);
}
break;
}
@@ -1895,4 +1884,4 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
}
}
main();
runMain(main);

View File

@@ -32,6 +32,7 @@
const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
const SIGNIFICANT_MIN_CHARS = 12;
const GSD_HOOK_VERSION_LINE_RE = /^(?:\/\/|#)\s*gsd-hook-version:\s*\S+\s*$/i;
@@ -48,10 +49,9 @@ function parseArgs(argv) {
process.stdout.write(
'usage: verify-reapply-patches.cjs --patches-dir <path> --config-dir <path> [--pristine-dir <path>] [--json]\n',
);
process.exit(0);
throw new ExitError(0);
} else {
process.stderr.write(`unknown argument: ${arg}\n`);
process.exit(2);
throw new ExitError(2, `unknown argument: ${arg}`);
}
}
return opts;
@@ -281,16 +281,13 @@ function verifyFile({ relPath, patchesDir, configDir, pristineDir, pristineHashe
function main() {
const opts = parseArgs(process.argv.slice(2));
if (!opts.patchesDir || !opts.configDir) {
process.stderr.write('--patches-dir and --config-dir are required\n');
process.exit(2);
throw new ExitError(2, '--patches-dir and --config-dir are required');
}
if (!fs.existsSync(opts.patchesDir)) {
process.stderr.write(`patches dir not found: ${opts.patchesDir}\n`);
process.exit(2);
throw new ExitError(2, `patches dir not found: ${opts.patchesDir}`);
}
if (!fs.existsSync(opts.configDir)) {
process.stderr.write(`config dir not found: ${opts.configDir}\n`);
process.exit(2);
throw new ExitError(2, `config dir not found: ${opts.configDir}`);
}
const files = walk(opts.patchesDir).filter((f) => !f.endsWith('backup-meta.json'));
@@ -342,11 +339,11 @@ function main() {
}
}
process.exit(failures.length > 0 ? 1 : 0);
return failures.length > 0 ? 1 : 0;
}
if (require.main === module) {
main();
runMain(main);
}
module.exports = { computeUserAddedLines, isSignificantLine, verifyFile, walk, REASON, readPristineHashes, sha256 };

View File

@@ -188,7 +188,7 @@ if [ "$MVP_MODE" = "true" ] && [ "$TDD_MODE" = "true" ]; then
fi
fi
```
Pure doc-only / config-only / test-only tasks return `is_behavior_adding=false` and are exempt. See `execute-mvp-tdd.md` for the halt report format.
Pure doc-only / config-only / test-only tasks return `is_behavior_adding=false` and are exempt. When the gate trips, Read `~/.claude/gsd-core/references/execute-mvp-tdd.md` for the exact halt report format.
</step>
<step name="check_blocking_antipatterns" priority="first">

View File

@@ -143,7 +143,7 @@ fi
```
When `WALKING_SKELETON=true`:
- Planner is instructed to produce `SKELETON.md` in the phase directory alongside `PLAN.md`. The template lives at `@~/.claude/gsd-core/references/skeleton-template.md`.
- Planner is instructed to produce `SKELETON.md` in the phase directory alongside `PLAN.md`. The template lives at `~/.claude/gsd-core/references/skeleton-template.md` — the planner reads it when producing SKELETON.md (lazy; not loaded on non-skeleton runs).
- The plan must scaffold project + routing + one real DB read/write + one real UI interaction + dev deployment — the thinnest possible end-to-end working slice.
**Interaction with `--prd <filepath>`.** `--mvp` and `--prd` compose. The PRD express path (Step 3.5) creates `CONTEXT.md` from the PRD file and continues to research; the Walking Skeleton gate fires independently from the conditions above. When both are active on Phase 1 of a new project, the planner receives `WALKING_SKELETON=true` and PRD-derived context simultaneously — the PRD informs *what the skeleton should prove*. No precedence is needed; the two signals are orthogonal. See [`references/mvp-concepts.md`](../references/mvp-concepts.md) for the broader interaction map.
@@ -933,12 +933,12 @@ Each TDD plan gets one feature with RED/GREEN/REFACTOR gate sequence.
</tdd_mode_active>
` : ''}
**MVP_MODE:** ${MVP_MODE} (when true, follow vertical-slice rules from `@~/.claude/gsd-core/references/planner-mvp-mode.md`; when false, ignore MVP guidance entirely.)
**WALKING_SKELETON:** ${WALKING_SKELETON} (when true, the first deliverable must be a Walking Skeleton — produce SKELETON.md alongside PLAN.md.)
**MVP_MODE:** ${MVP_MODE} (when true, follow vertical-slice rules from `~/.claude/gsd-core/references/planner-mvp-mode.md`; when false, ignore MVP guidance entirely.)
**WALKING_SKELETON:** ${WALKING_SKELETON} (when true, the first deliverable must be a Walking Skeleton — Read the template at `~/.claude/gsd-core/references/skeleton-template.md` and produce SKELETON.md alongside PLAN.md.)
${MVP_MODE === 'true' ? `
<mvp_mode_active>
**MVP Mode is ENABLED.** Follow vertical-slice planning rules from @~/.claude/gsd-core/references/planner-mvp-mode.md. Each plan must deliver a complete vertical slice — thin end-to-end functionality rather than horizontal layers.
**MVP Mode is ENABLED.** Read `~/.claude/gsd-core/references/planner-mvp-mode.md` now and follow its vertical-slice planning rules. Each plan must deliver a complete vertical slice — thin end-to-end functionality rather than horizontal layers.
</mvp_mode_active>
` : ''}
</planning_context>

41
src/cli-exit.cts Normal file
View File

@@ -0,0 +1,41 @@
/**
* Error carrying a process exit code. CLI logic throws this instead of calling
* process.exit() (banned by n/no-process-exit); runMain() translates it into
* process.exitCode at the entrypoint.
*/
class ExitError extends Error {
code: number;
hasUserMessage: boolean;
constructor(code = 1, message?: string) {
super(message === undefined ? `process exit ${code}` : message);
this.name = 'ExitError';
this.code = code;
this.hasUserMessage = message !== undefined;
}
}
/**
* Run a CLI main and translate its outcome into process.exitCode (never
* process.exit, so n/no-process-exit stays satisfied; output flushes and
* process.on('exit') cleanup still fires). main may be sync or async:
* number return -> process.exitCode = it
* thrown ExitError -> process.exitCode = err.code (+ stderr err.message if hasUserMessage && code!=0)
* other throw -> stderr stack + process.exitCode = 1
*/
function runMain(main: () => number | void | Promise<number | void>): void {
Promise.resolve()
.then(() => main())
.then((code) => { if (typeof code === 'number') process.exitCode = code; })
.catch((err: unknown) => {
if (err instanceof ExitError) {
if (err.hasUserMessage && err.code !== 0) process.stderr.write(`${err.message}\n`);
process.exitCode = err.code;
return;
}
const e = err as Error;
process.stderr.write(`${e && e.stack ? e.stack : String(err)}\n`);
process.exitCode = 1;
});
}
export = { ExitError, runMain };

View File

@@ -402,3 +402,150 @@ describe('SIZE: discuss-phase progressive disclosure (issue #2551)', () => {
);
});
});
const AGENTS_DIR = path.join(__dirname, '..', 'agents');
describe('workflow progressive disclosure — MVP bodies lazy-loaded (#720)', () => {
// MVP-only reference bodies (planner-mvp-mode.md, skeleton-template.md,
// execute-mvp-tdd.md) must NOT be eagerly @-imported at the top level of the
// always-loaded workflow files or agent definitions. An @-prefixed path is
// expanded into context the moment the file loads — regardless of whether
// MVP_MODE is true — inflating every session's token cost. Use a plain
// backtick path or a conditional "Read ..." instruction instead. See issue #720.
test('plan-phase.md does not eagerly @-import planner-mvp-mode.md', () => {
const planPhaseContent = fs.readFileSync(path.join(WORKFLOWS_DIR, 'plan-phase.md'), 'utf-8');
assert.ok(
!/@[~./\w-]*planner-mvp-mode\.md/.test(planPhaseContent),
'plan-phase.md contains an eager @-import of planner-mvp-mode.md — ' +
'this loads the MVP body into context for every session, even when MVP_MODE is false. ' +
'Replace with a conditional Read instruction or a plain backtick path. See #720.'
);
});
test('plan-phase.md does not eagerly @-import skeleton-template.md', () => {
const planPhaseContent = fs.readFileSync(path.join(WORKFLOWS_DIR, 'plan-phase.md'), 'utf-8');
assert.ok(
!/@[~./\w-]*skeleton-template\.md/.test(planPhaseContent),
'plan-phase.md contains an eager @-import of skeleton-template.md — ' +
'this loads the template into context on every plan-phase invocation. ' +
'Replace with a conditional Read instruction or a plain backtick path. See #720.'
);
});
test('plan-phase.md still references both MVP bodies (lazy reference preserved)', () => {
const planPhaseContent = fs.readFileSync(path.join(WORKFLOWS_DIR, 'plan-phase.md'), 'utf-8');
assert.ok(
/planner-mvp-mode\.md/.test(planPhaseContent) && /skeleton-template\.md/.test(planPhaseContent),
'plan-phase.md must still reference planner-mvp-mode.md and skeleton-template.md ' +
'(as lazy backtick paths or Read instructions) so agents know where to find them. ' +
'Do not delete the references — only remove the leading @ sigil. See #720.'
);
});
test('plan-phase.md does not list MVP bodies in <required_reading>', () => {
const planPhaseContent = fs.readFileSync(path.join(WORKFLOWS_DIR, 'plan-phase.md'), 'utf-8');
const requiredReadingMatch = planPhaseContent.match(/<required_reading>([\s\S]*?)<\/required_reading>/);
if (requiredReadingMatch) {
const block = requiredReadingMatch[1];
assert.ok(
!/planner-mvp-mode\.md/.test(block),
'planner-mvp-mode.md must NOT appear in plan-phase.md <required_reading> — ' +
'that block is always loaded regardless of MVP_MODE. See #720.'
);
assert.ok(
!/skeleton-template\.md/.test(block),
'skeleton-template.md must NOT appear in plan-phase.md <required_reading> — ' +
'that block is always loaded regardless of MVP_MODE. See #720.'
);
}
});
test('execute-phase.md does not eagerly @-import execute-mvp-tdd.md', () => {
const executePhaseContent = fs.readFileSync(path.join(WORKFLOWS_DIR, 'execute-phase.md'), 'utf-8');
assert.ok(
!/@[~./\w-]*execute-mvp-tdd\.md/.test(executePhaseContent),
'execute-phase.md contains an eager @-import of execute-mvp-tdd.md — ' +
'this loads the MVP TDD body into context for every session. ' +
'Replace with a conditional Read instruction or a plain backtick path. See #720.'
);
});
test('execute-phase.md still references execute-mvp-tdd.md (lazy reference preserved)', () => {
const executePhaseContent = fs.readFileSync(path.join(WORKFLOWS_DIR, 'execute-phase.md'), 'utf-8');
assert.ok(
/execute-mvp-tdd\.md/.test(executePhaseContent),
'execute-phase.md must still reference execute-mvp-tdd.md (as a lazy backtick path ' +
'or Read instruction) so agents know where to find it. ' +
'Do not delete the reference — only ensure there is no leading @ sigil. See #720.'
);
});
test('gsd-planner.md does not eagerly @-import planner-mvp-mode.md', () => {
const content = fs.readFileSync(path.join(AGENTS_DIR, 'gsd-planner.md'), 'utf-8');
assert.ok(
!/@[~./\w-]*planner-mvp-mode\.md/.test(content),
'gsd-planner.md contains an eager @-import of planner-mvp-mode.md — ' +
'this loads the MVP body into context for every session, even when MVP_MODE is false. ' +
'Replace with a conditional Read instruction or a plain backtick path. See #720.'
);
});
test('gsd-planner.md does not eagerly @-import skeleton-template.md', () => {
const content = fs.readFileSync(path.join(AGENTS_DIR, 'gsd-planner.md'), 'utf-8');
assert.ok(
!/@[~./\w-]*skeleton-template\.md/.test(content),
'gsd-planner.md contains an eager @-import of skeleton-template.md — ' +
'this loads the template into context on every planner invocation. ' +
'Replace with a conditional Read instruction or a plain backtick path. See #720.'
);
});
test('gsd-planner.md does not eagerly @-import user-story-template.md', () => {
const content = fs.readFileSync(path.join(AGENTS_DIR, 'gsd-planner.md'), 'utf-8');
assert.ok(
!/@[~./\w-]*user-story-template\.md/.test(content),
'gsd-planner.md contains an eager @-import of user-story-template.md — ' +
'this loads the template into context on every planner invocation. ' +
'Replace with a conditional Read instruction or a plain backtick path. See #720.'
);
});
test('gsd-planner.md still references the three MVP bodies', () => {
const content = fs.readFileSync(path.join(AGENTS_DIR, 'gsd-planner.md'), 'utf-8');
assert.ok(
/planner-mvp-mode\.md/.test(content),
'gsd-planner.md must still reference planner-mvp-mode.md (as a lazy path or Read instruction). ' +
'Do not delete the reference — only remove the leading @ sigil. See #720.'
);
assert.ok(
/skeleton-template\.md/.test(content),
'gsd-planner.md must still reference skeleton-template.md (as a lazy path or Read instruction). ' +
'Do not delete the reference — only remove the leading @ sigil. See #720.'
);
assert.ok(
/user-story-template\.md/.test(content),
'gsd-planner.md must still reference user-story-template.md (as a lazy path or Read instruction). ' +
'Do not delete the reference — only remove the leading @ sigil. See #720.'
);
});
test('gsd-executor.md does not eagerly @-import execute-mvp-tdd.md', () => {
const content = fs.readFileSync(path.join(AGENTS_DIR, 'gsd-executor.md'), 'utf-8');
assert.ok(
!/@[~./\w-]*execute-mvp-tdd\.md/.test(content),
'gsd-executor.md contains an eager @-import of execute-mvp-tdd.md — ' +
'this loads the MVP TDD body into context for every session. ' +
'Replace with a conditional Read instruction or a plain backtick path. See #720.'
);
});
test('gsd-executor.md still references execute-mvp-tdd.md', () => {
const content = fs.readFileSync(path.join(AGENTS_DIR, 'gsd-executor.md'), 'utf-8');
assert.ok(
/execute-mvp-tdd\.md/.test(content),
'gsd-executor.md must still reference execute-mvp-tdd.md (as a lazy path or Read instruction). ' +
'Do not delete the reference — only remove the leading @ sigil. See #720.'
);
});
});