feat(pipeline): Director-Critic quality gate before generation#41
Conversation
Extract loadDirectorSystemPrompt() helper for DRY prompt loading. Add reviseDirectorScore() that takes a DirectorScore + CritiqueResult and generates a revised plan. Guards nullable revision_instructions by falling back to the weaknesses array.
Generate → evaluate → revise loop (max 2 rounds, threshold >= 7). Tracks highest-scoring revision. Wraps evaluate() in try/catch for graceful degradation. Moves score.json write, onStageComplete, and dry-run guard after the loop. Uses revised score for cost estimation. Removes stale "revision coming in a future release" message from step-6 critic.
Add revisionCost and revisionRounds to CostBreakdown. Format revision cost as a separate line in cost estimates. Add type: "revision" handler to worker onProgress for meta.revisionHistory persistence.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@claude review |
|
Claude finished @tsensei's task in 3m 5s —— View job Code Review: Director-Critic Quality Gate
Overall this is a well-structured feature. The loop logic is correct, the graceful-degradation path works, and the test coverage is solid. A few things worth addressing before merge: Moderate Issues1.
const result = await llm.generate({ ... });
const validated = DirectorScore.parse(result.data); // throws → caught by caller
return { data: validated, usage: result.usage };The revision attempt is then swallowed by the 2. Test isolation: The test helper is an exact copy of the loop in The fix is to extract the loop into an exported function, e.g.: // orchestrator.ts or a new revision-loop.ts
export async function runRevisionLoop(opts: { ... }): Promise<{ ... }>That function can then be imported directly by both the orchestrator and the test, eliminating the copy. Fix this → 3. Last revision is produced but never scored ( When MAX_REVISION_ROUNDS=2 and both rounds return low scores, the loop produces a second revised plan but never evaluates it — the loop exits because // Round 1: evaluate(round1Score) → 6, revise → round2Score ← never evaluated
// After loop: score = bestScore = round1Score ← silent fallbackAdding a final Minor Issues4. Revision cost estimate may undercount ( The revision cost formula is: const revisionCost = revisionRounds * (callCost(TOKEN_ESTIMATES.critic) + callCost(TOKEN_ESTIMATES.creativeDirector));
5. Archetype constraint in revision is prompt-only ( "Keep the same archetype."If the LLM ignores this, // After validation
if (validated.archetype !== originalScore.archetype) {
validated.archetype = originalScore.archetype;
}Correctness Checks (All Pass)
VerdictLogically correct and well-tested for the happy path and edge cases. Issues 1 and 2 are the most actionable before merge — the missing retry and the copy-paste test isolation both represent real maintenance risk. Issues 3-5 are informational and can go in TODOS.md if not addressed now. |
…test TypeScript strict mode treats array[i % 8] as T | undefined. Add non-null assertion since the modulo guarantees a valid index.
…or context - Force archetype back to original in reviseDirectorScore if LLM changes it - Track evaluationsCompleted separately from revisionRoundsCompleted so cost display includes the gate evaluation (not just revision rounds) - Clarify catch comment: covers both critic and revision failures
…pproximation reviseDirectorScore now retries up to 2 times on schema validation failure, matching the pattern in generateDirectorScore. Previously a transient parse error would waste the entire revision round with nothing to show for it. Add comment noting that revision cost estimate slightly undercounts because revise prompts include the original score JSON (~1-3K extra tokens).
Response to code review findingsIssue 1 (no retry in Issue 2 (test duplication / Issue 3 (last revision never scored): Design decision, acknowledged in the design doc. Adding a final Issue 4 (cost undercount comment): Fixed in 7e23b6d. Added comment documenting the approximation. Issue 5 (archetype drift): Already fixed in be8e582 (before this review ran). |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously, the last revision was generated but never scored, wasting the LLM call. Now a final evaluate() runs after the loop if the last revision hasn't been scored yet. The highest-scoring version across all evaluations (including the final one) is used.
|
@claude review |
|
Update on Issue 3 (last revision never scored): On reflection, the original rationale was wrong. If we pay for a revision LLM call but never evaluate the result, that's a wasted call. Fixed in 4af706b. The loop now runs a final |
|
Claude finished @tsensei's task in 2m 43s —— View job Code Review: Director-Critic Quality Gate (Re-review)
All five issues from the first review have been resolved. Three new minor observations below. Issues from First Review — Status
New Findings (Minor)1. Retry count inconsistency: The PR response to issue 1 says "matching the pattern in 2. // orchestrator.ts
const costBreakdown = estimateCost(..., evaluationsCompleted); // passed here
// cost-estimator.ts
const revisionCost = revisionRounds * (callCost(critic) + callCost(director)); // named this
Using 3. Type cast mutation for archetype enforcement is slightly awkward ( if (validated.archetype !== originalScore.archetype) {
(validated as { archetype: string }).archetype = originalScore.archetype;
}This mutates const data = validated.archetype !== originalScore.archetype
? { ...validated, archetype: originalScore.archetype }
: validated;
return { data, usage: totalUsage };No behavioral difference — purely a style preference. Not a blocker. Correctness Checks (All Pass)
VerdictThis PR is in excellent shape. The logic is correct, the edge cases are covered, and the graceful-degradation path works at every level (evaluate failure, revise failure, parse error). The three new findings are all minor/cosmetic and don't affect correctness. Ready to merge. |
…timate Previously, evaluationsCompleted was passed as a single count and the cost formula charged (critic + director) per unit. This overcounted when the gate evaluated but didn't revise (score >= 7 first try): charged for a director call that never happened. Now estimateCost takes separate gateEvaluations and revisionRounds params. Cost formula: evaluations * critic_cost + revisions * director_cost.
Response to re-review findingsFinding 1 (retry count 2 vs 3): Intentional. Finding 2 (evaluationsCompleted semantic mismatch): Valid bug, fixed in c478868. Finding 3 (type cast mutation): Style preference, keeping as-is. The cast only fires when archetype differs (rare path). The spread alternative allocates a new object on every call even on the happy path (the conditional would need to wrap the return, not just the assignment). Neither is better; current code is more explicit about the mutation being intentional. |
Summary
Moves the Critic agent from a post-mortem at step 6 to a pre-generation quality gate inside the Director step. Every DirectorScore is now evaluated and revised before any expensive TTS, image, video, or music generation begins.
Quality gate loop:
revision_needed, the Director revises the plan using the Critic's feedbackCost tracking:
CostBreakdownnow includesrevisionCostandrevisionRoundsfieldsWorker persistence:
meta.jsonincludesrevisionHistoryarray with per-round critic scorestype: "revision"events during the director stageTest Coverage
Tests: 338 → 349 (+11 new)
creative-director.test.ts: 3 tests forreviseDirectorScore()(happy path, critique in prompt, nullable fallback)orchestrator-revision.test.ts: 8 tests for the revision loop (skip on high score, single revision, max rounds, best-score tracking, progress events, error handling, LLM usage accumulation)cost-estimator.test.ts: 1 test for revision cost fieldPre-Landing Review
No issues found. Pass 1 (Critical): no SQL, no race conditions, no trust boundary violations. Pass 2 (Informational): clean.
Plan Completion
Design doc:
tsensei-main-design-20260409-124145.md(APPROVED)TODOS
Documentation
CLAUDE.md: updated test count from 338 to 349docs/content/docs/pipeline/critic.mdx: rewrote intro to describe dual role (quality gate + post-mortem), replaced "coming in a future release" with revision loop documentationdocs/content/docs/pipeline/overview.mdx: updated pipeline diagram to show Director-Critic revision loop, updated dry-run descriptiondocs/content/docs/contributing/architecture.mdx: fixed critic agent input description ("DirectorScore + topic" not "rendered video + score")Test plan
pnpm start "topic" --dry-run --yesto verify revision messages appear🤖 Generated with Claude Code