/** * Context loader: prints PRODUCT.md (and DESIGN.md if present) as one * markdown block on stdout, or exits with empty stdout when no PRODUCT.md * is found anywhere. The skill keys off "empty stdout" to branch into the * teach flow. * * Path resolution (first match wins): * 1. cwd, if PRODUCT.md or DESIGN.md is there * 2. .agents/context/ then docs/ * 3. $IMPECCABLE_CONTEXT_DIR (absolute or cwd-relative) — power-user * escape hatch, only consulted when defaults are empty * 4. cwd as a "nothing found" default * * `resolveContextDir()` and `loadContext()` are also exported for the * server-side scripts (live.mjs, live-server.mjs) that need the structured * shape rather than the markdown block. */ import fs from 'node:fs'; import path from 'node:path'; const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; const FALLBACK_DIRS = ['.agents/context', 'docs']; export function resolveContextDir(cwd = process.cwd()) { if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { return cwd; } for (const rel of FALLBACK_DIRS) { const candidate = path.resolve(cwd, rel); if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { return candidate; } } const envDir = process.env.IMPECCABLE_CONTEXT_DIR; if (envDir && envDir.trim()) { const trimmed = envDir.trim(); return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); } return cwd; } export function loadContext(cwd = process.cwd()) { const contextDir = resolveContextDir(cwd); const productPath = firstExisting(contextDir, PRODUCT_NAMES); const designPath = firstExisting(contextDir, DESIGN_NAMES); const product = productPath ? safeRead(productPath) : null; const design = designPath ? safeRead(designPath) : null; return { hasProduct: !!product, product, productPath: productPath ? path.relative(cwd, productPath) : null, hasDesign: !!design, design, designPath: designPath ? path.relative(cwd, designPath) : null, contextDir, }; } function firstExisting(dir, names) { for (const name of names) { const abs = path.join(dir, name); if (fs.existsSync(abs)) return abs; } return null; } function safeRead(p) { try { return fs.readFileSync(p, 'utf-8'); } catch { return null; } } /** * Pull the register (`brand` or `product`) out of PRODUCT.md by looking * for a `## Register` section and reading the first non-empty line that * follows it. Returns null when the file is legacy / register-less. */ function extractRegister(product) { if (!product) return null; const lines = product.split('\n'); for (let i = 0; i < lines.length; i++) { if (/^##\s+Register\b/i.test(lines[i].trim())) { for (let j = i + 1; j < lines.length; j++) { const next = lines[j].trim(); if (!next) continue; const word = next.toLowerCase(); if (word === 'brand' || word === 'product') return word; return null; } } } return null; } function cli() { const ctx = loadContext(process.cwd()); if (!ctx.hasProduct) { // Direct stdout message instead of relying on empty output as a signal // — cheap models miss the empty case more often than the explicit one. process.stdout.write( 'NO_PRODUCT_MD: This project has no PRODUCT.md yet. ' + 'Stop the current task, load reference/teach.md, and follow its ' + 'instructions to write PRODUCT.md before resuming.\n', ); process.exit(0); } const parts = [`# PRODUCT.md\n\n${ctx.product.trim()}`]; if (ctx.hasDesign) { parts.push(`# DESIGN.md\n\n${ctx.design.trim()}`); } const register = extractRegister(ctx.product); const next = register ? `NEXT STEP: This project's register is \`${register}\`. You MUST now read \`reference/${register}.md\` before producing any design output.` : `NEXT STEP: You MUST now read the matching register reference (\`reference/brand.md\` or \`reference/product.md\`) before producing any design output. Pick based on PRODUCT.md above.`; parts.push(next); process.stdout.write(parts.join('\n\n---\n\n') + '\n'); } const _running = process.argv[1]; if (_running?.endsWith('context.mjs') || _running?.endsWith('context.mjs/')) { cli(); }