← back to The Ai Factory

src/stages/critic.js

58 lines

// Stage 6 — Manager critic. Single Claude CLI subprocess call (Steve's Max sub).
// Reads the scaffolded file, returns a JSON review.

const { claudeCli } = require('../llm');

const CRITIC_PROMPT = (artifactType, content, spec) => `You are reviewing a draft Claude Code ${artifactType} file produced by an automated pipeline. Be a strict reviewer.

The artifact spec was:
${JSON.stringify(spec, null, 2)}

The draft file content:
---BEGIN FILE---
${content}
---END FILE---

Evaluate against Anthropic's guidelines for Claude Code ${artifactType}s:
- YAML frontmatter is valid (name, description${artifactType === 'subagent' ? ', tools, model' : ''})
- description is specific enough to trigger the right way (not too vague, not too narrow)
- ${artifactType === 'subagent' ? 'tools list is the minimum needed (no over-granting)' : 'WHEN to use is unambiguous'}
- Body sections are present and useful
- No hallucinated file paths, env vars, or commands
- No prose padding; every line earns its place

Return ONLY a JSON object, no prose, no fences:
{
  "approved": <true|false>,
  "score": <0-100>,
  "issues": [<short string per concrete problem>],
  "fix_instructions": [<short string per actionable revision the next stage should apply>]
}`;

async function runCritic({ artifactType, content, spec }) {
  const raw = await claudeCli({
    prompt: CRITIC_PROMPT(artifactType, content, spec),
    timeoutMs: 180000
  });

  // Claude CLI may wrap JSON in fences or add prose; pull out the first {...} block.
  const match = raw.match(/\{[\s\S]*\}/);
  if (!match) {
    return { approved: false, score: 0, issues: ['critic returned no JSON'], fix_instructions: [], raw_excerpt: raw.slice(0, 300) };
  }
  let review;
  try { review = JSON.parse(match[0]); }
  catch (e) {
    return { approved: false, score: 0, issues: [`critic JSON parse failed: ${e.message}`], fix_instructions: [], raw_excerpt: match[0].slice(0, 300) };
  }

  return {
    approved: !!review.approved,
    score: Number(review.score) || 0,
    issues: Array.isArray(review.issues) ? review.issues : [],
    fix_instructions: Array.isArray(review.fix_instructions) ? review.fix_instructions : []
  };
}

module.exports = { runCritic };