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>
117 lines
4.3 KiB
TypeScript
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();
|
|
}
|