* fix: Created 10 headless prompt files (5 workflows + 5 agents) in sdk/p… - "sdk/prompts/workflows/execute-plan.md" - "sdk/prompts/workflows/research-phase.md" - "sdk/prompts/workflows/plan-phase.md" - "sdk/prompts/workflows/verify-phase.md" - "sdk/prompts/workflows/discuss-phase.md" - "sdk/prompts/agents/gsd-executor.md" - "sdk/prompts/agents/gsd-phase-researcher.md" - "sdk/prompts/agents/gsd-planner.md" GSD-Task: S01/T02 * feat: Created prompt-sanitizer.ts, wired headless prompt loading into P… - "sdk/src/prompt-sanitizer.ts" - "sdk/src/phase-prompt.ts" - "sdk/src/gsd-tools.ts" - "sdk/src/gsd-tools.test.ts" - "sdk/src/phase-runner-types.test.ts" GSD-Task: S01/T01 * test: Added 111 unit tests covering sanitizePrompt(), headless prompt l… - "sdk/src/prompt-sanitizer.test.ts" - "sdk/src/headless-prompts.test.ts" - "sdk/src/phase-prompt.test.ts" GSD-Task: S01/T03 * feat: Wired sdkPromptsDir preference and sanitizePrompt into InitRunner… - "sdk/src/init-runner.ts" - "sdk/package.json" GSD-Task: S02/T01 * feat: add --init flag to auto command for single-command PRD-to-execution gsd-sdk auto --init @path/to/prd.md now bootstraps the project (init) then immediately runs the autonomous phase execution loop. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: add remaining headless prompt files and templates Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: Extended init-runner.test.ts with 7 sdkPromptsDir preference and… - "sdk/src/init-runner.test.ts" GSD-Task: S02/T03 --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
160 lines
5.9 KiB
TypeScript
160 lines
5.9 KiB
TypeScript
/**
|
|
* Contract test: all headless prompt files in sdk/prompts/ must contain
|
|
* zero instances of blocked interactive patterns.
|
|
*
|
|
* This prevents regression — any new prompt file or edit that reintroduces
|
|
* interactive mechanics will fail this test.
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import { readFile } from 'node:fs/promises';
|
|
import { join, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { readdirSync } from 'node:fs';
|
|
|
|
// ─── Paths ───────────────────────────────────────────────────────────────────
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const promptsDir = join(__dirname, '..', 'prompts');
|
|
const workflowsDir = join(promptsDir, 'workflows');
|
|
const agentsDir = join(promptsDir, 'agents');
|
|
|
|
// ─── Blocked patterns ────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Patterns that MUST NOT appear in headless prompts.
|
|
* Each entry: [label for reporting, regex].
|
|
*/
|
|
const BLOCKED_PATTERNS: Array<[string, RegExp]> = [
|
|
['AskUserQuestion', /AskUserQuestion\s*\(/],
|
|
['SlashCommand', /SlashCommand\s*\(/],
|
|
['/gsd: command', /\/gsd:\S+/],
|
|
['@file: reference', /@file:\S+/],
|
|
['STOP + wait directive', /\bSTOP\b\s+(?:and\s+)?(?:wait|ask)/i],
|
|
['bare STOP directive', /^\s*STOP\s*[.!]?\s*$/m],
|
|
['wait for user', /\bwait\s+for\s+(?:the\s+)?user\b/i],
|
|
['ask the user', /\bask\s+the\s+user\b/i],
|
|
];
|
|
|
|
// ─── Expected files ──────────────────────────────────────────────────────────
|
|
|
|
const EXPECTED_WORKFLOWS = [
|
|
'execute-plan.md',
|
|
'research-phase.md',
|
|
'plan-phase.md',
|
|
'verify-phase.md',
|
|
'discuss-phase.md',
|
|
];
|
|
|
|
const EXPECTED_AGENTS = [
|
|
'gsd-executor.md',
|
|
'gsd-phase-researcher.md',
|
|
'gsd-planner.md',
|
|
'gsd-verifier.md',
|
|
'gsd-plan-checker.md',
|
|
'gsd-project-researcher.md',
|
|
'gsd-research-synthesizer.md',
|
|
'gsd-roadmapper.md',
|
|
];
|
|
|
|
const templatesDir = join(promptsDir, 'templates');
|
|
const researchTemplatesDir = join(templatesDir, 'research-project');
|
|
|
|
const EXPECTED_TEMPLATES = [
|
|
'project.md',
|
|
'requirements.md',
|
|
'roadmap.md',
|
|
'state.md',
|
|
];
|
|
|
|
const EXPECTED_RESEARCH_TEMPLATES = [
|
|
'ARCHITECTURE.md',
|
|
'FEATURES.md',
|
|
'PITFALLS.md',
|
|
'STACK.md',
|
|
'SUMMARY.md',
|
|
];
|
|
|
|
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
|
|
describe('headless prompt contract', () => {
|
|
describe('file inventory', () => {
|
|
it('has all expected workflow files', () => {
|
|
const actual = readdirSync(workflowsDir).sort();
|
|
expect(actual).toEqual(EXPECTED_WORKFLOWS.sort());
|
|
});
|
|
|
|
it('has all expected agent files', () => {
|
|
const actual = readdirSync(agentsDir).sort();
|
|
expect(actual).toEqual(EXPECTED_AGENTS.sort());
|
|
});
|
|
});
|
|
|
|
describe('zero interactive patterns in workflow prompts', () => {
|
|
for (const filename of EXPECTED_WORKFLOWS) {
|
|
describe(filename, () => {
|
|
for (const [label, pattern] of BLOCKED_PATTERNS) {
|
|
it(`contains no ${label}`, async () => {
|
|
const content = await readFile(join(workflowsDir, filename), 'utf-8');
|
|
const matches = content.match(new RegExp(pattern.source, pattern.flags + 'g'));
|
|
expect(matches, `Found ${label} in ${filename}: ${matches?.join(', ')}`).toBeNull();
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
describe('zero interactive patterns in agent prompts', () => {
|
|
for (const filename of EXPECTED_AGENTS) {
|
|
describe(filename, () => {
|
|
for (const [label, pattern] of BLOCKED_PATTERNS) {
|
|
it(`contains no ${label}`, async () => {
|
|
const content = await readFile(join(agentsDir, filename), 'utf-8');
|
|
const matches = content.match(new RegExp(pattern.source, pattern.flags + 'g'));
|
|
expect(matches, `Found ${label} in ${filename}: ${matches?.join(', ')}`).toBeNull();
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
describe('template file inventory', () => {
|
|
it('has all expected top-level template files', () => {
|
|
const actual = readdirSync(templatesDir).filter(f => f.endsWith('.md')).sort();
|
|
expect(actual).toEqual(EXPECTED_TEMPLATES.sort());
|
|
});
|
|
|
|
it('has all expected research-project template files', () => {
|
|
const actual = readdirSync(researchTemplatesDir).sort();
|
|
expect(actual).toEqual(EXPECTED_RESEARCH_TEMPLATES.sort());
|
|
});
|
|
});
|
|
|
|
describe('zero interactive patterns in template prompts', () => {
|
|
for (const filename of EXPECTED_TEMPLATES) {
|
|
describe(filename, () => {
|
|
for (const [label, pattern] of BLOCKED_PATTERNS) {
|
|
it(`contains no ${label}`, async () => {
|
|
const content = await readFile(join(templatesDir, filename), 'utf-8');
|
|
const matches = content.match(new RegExp(pattern.source, pattern.flags + 'g'));
|
|
expect(matches, `Found ${label} in ${filename}: ${matches?.join(', ')}`).toBeNull();
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
describe('zero interactive patterns in research-project templates', () => {
|
|
for (const filename of EXPECTED_RESEARCH_TEMPLATES) {
|
|
describe(filename, () => {
|
|
for (const [label, pattern] of BLOCKED_PATTERNS) {
|
|
it(`contains no ${label}`, async () => {
|
|
const content = await readFile(join(researchTemplatesDir, filename), 'utf-8');
|
|
const matches = content.match(new RegExp(pattern.source, pattern.flags + 'g'));
|
|
expect(matches, `Found ${label} in ${filename}: ${matches?.join(', ')}`).toBeNull();
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
});
|