Files
msd-core/sdk/src/prompt-sanitizer.ts
Lex Christopherson 377a6d2c6e fix(sdk): use installed agent/workflow prompts instead of stripped-down bundled copies
The SDK bundled its own agents and workflows at ~17% the size of the real
ones, missing critical instructions like file naming conventions, scope
reduction rules, discovery protocols, and TDD integration. This caused
the planner to create a single PLAN.md instead of properly named
per-plan files (01-01-PLAN.md, 01-02-PLAN.md), breaking wave-based
parallel execution.

- Invert load priority: installed GSD agents/workflows first, SDK
  bundled as last-resort fallback
- Replace @-reference stripping with resolution (read + inline content)
- Use full agent definitions instead of extracting only the <role> block
- Delete sdk/prompts/agents/ and sdk/prompts/workflows/ (13 files)
- Delete headless-prompts.test.ts (validated deleted files)
- Thread projectDir through sanitizePrompt for @-reference resolution

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 14:08:05 -06:00

117 lines
4.3 KiB
TypeScript

/**
* Prompt sanitizer — resolves @-file references and strips interactive CLI
* patterns from GSD-1 prompts so they're safe for headless SDK use.
*
* @-file references (e.g., @~/.claude/get-shit-done/references/foo.md) are
* resolved by reading the file and inlining the content. This preserves the
* critical instructions that the real agent prompts depend on.
*
* Patterns removed (interactive-only, not useful headless):
* - /gsd-... skill commands (can't invoke skills in Agent SDK)
* - AskUserQuestion(...) calls
* - STOP directives in interactive contexts
* - SlashCommand() calls
* - 'wait for user' / 'ask the user' instructions
*/
import { readFileSync } from 'node:fs';
import { homedir } from 'node:os';
// ─── @-reference resolution ──────────────────────────────────────────────────
/**
* Matches @-file references in prompt text. Handles:
* - @~/.claude/get-shit-done/references/foo.md
* - @~/.claude/get-shit-done/workflows/bar.md
* - @.planning/PROJECT.md (project-relative)
*
* Only resolves references that start a line or follow whitespace,
* not email addresses or @ mentions in prose.
*/
const AT_REFERENCE_PATTERN = /^(\s*)@(~\/[^\s]+|\.planning\/[^\s]+)/gm;
/**
* Resolve @-file references by reading the file and inlining the content.
* References that can't be resolved (file not found) are removed silently.
*
* @param input - Prompt text with @-references
* @param projectDir - Project directory for resolving relative paths
* @returns Prompt with @-references replaced by file contents
*/
export function resolveAtReferences(input: string, projectDir?: string): string {
if (!input) return input;
return input.replace(AT_REFERENCE_PATTERN, (_match, indent: string, refPath: string) => {
const resolvedPath = refPath.startsWith('~/')
? refPath.replace('~/', `${homedir()}/`)
: projectDir
? `${projectDir}/${refPath}`
: refPath;
try {
const content = readFileSync(resolvedPath, 'utf-8').trim();
return `${indent}${content}`;
} catch {
// File not found — remove the reference silently
return '';
}
});
}
// ─── Interactive pattern stripping ───────────────────────────────────────────
/**
* Patterns that are interactive-only and should be stripped for headless use.
* Note: @~/... file references are NOT stripped — they're resolved above.
*/
const LINE_PATTERNS: RegExp[] = [
// @file:path/to/something references (explicit @file: directive, not @~/...)
/^.*@file:\S+.*$/gm,
// /gsd-command references — entire line containing a skill command
/^.*\/gsd[:-]\S+.*$/gm,
// AskUserQuestion(...) calls — entire line
/^.*AskUserQuestion\s*\(.*$/gm,
// SlashCommand() calls — entire line
/^.*SlashCommand\s*\(.*$/gm,
// STOP directives — lines that are primarily "STOP" instructions
/^.*\bSTOP\b(?:\s+(?:and\s+)?(?:wait|ask|here|now)).*$/gm,
/^\s*STOP\s*[.!]?\s*$/gm,
// 'wait for user' / 'ask the user' instruction lines
/^.*\bwait\s+for\s+(?:the\s+)?user\b.*$/gim,
/^.*\bask\s+the\s+user\b.*$/gim,
];
// ─── Public API ──────────────────────────────────────────────────────────────
/**
* Sanitize a prompt for headless SDK use:
* 1. Resolve @-file references (inline the content)
* 2. Strip interactive-only patterns
*
* @param input - Raw prompt string from agent/workflow files
* @param projectDir - Project directory for resolving relative @-references
* @returns Cleaned prompt ready for Agent SDK use
*/
export function sanitizePrompt(input: string, projectDir?: string): string {
if (!input) return input;
// Step 1: Resolve @-file references to inline content
let result = resolveAtReferences(input, projectDir);
// Step 2: Strip interactive-only patterns
for (const pattern of LINE_PATTERNS) {
pattern.lastIndex = 0;
result = result.replace(pattern, '');
}
// Collapse runs of 3+ blank lines down to 2 (preserve paragraph breaks)
result = result.replace(/\n{3,}/g, '\n\n');
return result.trim();
}