Checkpoint 1 · Enrichment Pipeline → Multi-Agent Workflow

Should we refactor enrichment into a Workflow?

An ENR feasibility evaluation — where the Workflow harness helps, where it fights the pipeline's constraints, and what to build instead.

ENR Role · 2026-06-27 Feasibility evaluation enrich.ts · cohort-enrich · queue daemon Decision needed from Frank
Checkpoints Latest →
Verdict first

The one-line answer

Lead with the conclusion; the evidence follows.

No to refactoring the per-word generation pipeline (Pass 0–3, enrich.ts, the daemon) into a Workflow. It fights every load-bearing constraint the pipeline was built around.

Yes to a net-new Workflow-driven QA / hallucination-audit layer that runs after enrichment. That is where reasoning fan-out earns its keep — and building it also fixes the known-broken promotion gate that is blocking enriched → production today.

Don't rewrite the engine. Bolt the Workflow onto the part that actually needs reasoning fan-out.

Fit at a glance

Five surfaces — which fit a Workflow and which don't

Each row is one area of the pipeline. Scroll for the full "why" behind each.
Per-word generation passes (Pass 0–3, enrich.ts)
No fit. Serial by mandate (global rate-gap timer), ~90% Gemini not Claude, temp-0.0 locked for determinism, schema-strict, durable-queue-stateful. Fan-out would instantly hit the Gemini spend cap.
Recommendation: do not touch
~
Cohort outer loop (cohort-enrich.ts / run-enrich-cohort.ts)
Marginal. A Workflow expresses the loop cleanly, but must run at concurrency 1 (Gemini-bound) — the fan-out benefit evaporates. The daemon already provides failure isolation, resume, and atomic cross-session durability that the Workflow does not replace.
Recommendation: leave as daemon
QA / pedagogy-review layer — currently broken
Strong fit. Reviewing an enriched sense for quality and hallucinations is reasoning + judgment = Claude-shaped. Naturally parallel across senses. Runs on the Claude rate bucket, separate from Gemini. This is also the piece blocking all programmatic enriched → production promotion right now.
Recommendation: build this
Sense-validity / hallucination prune (PLAN-139)
Strong fit. Scoring each sense on source breadth + adversarial web-check for borderline cases is a multi-modal-sweep + adversarial-verify shape. Known ghost senses (jadi=Capricorn, ini=rice, bangun=wergeld — all Kaikki-only, zero living usage) are the textbook target for a fan-out of independent verifiers. One-off corpus job.
Recommendation: fold into QA Workflow
Pass 3 judge panels — already the right pattern
Already done correctly. The recovery panel runs gemini-2.5-flash + SEA-LION concurrently — a within-enrich micro-fanout across distinct rate-limit buckets. That is the correct pattern. No refactor needed here.
Status: already implemented
Context

What "Workflow" actually means here

So the evaluation is grounded, not abstract. Two facts drive everything below.

The Workflow harness is a deterministic JS script whose agent() primitive spawns Claude subagents (Opus / Sonnet / Haiku) with tools. Its headline powers: parallel() / pipeline() for concurrent subagents, real control flow (loops, conditionals, budget loops), and per-run resume via a journal.

Two facts that drive every collision below:

The existing pipeline

How enrich.ts actually runs

Grounded in the code and the cardinal. These five facts explain why the refactor does not fit.
Serial by mandate
enrich.ts:809–813 · run-enrich-cohort.ts:143
Code anchor GEMINI_RATE_LIMIT_MS / SEA_LION_RATE_LIMIT_MS — a global rate-gap timer enforced between every LLM call. The cohort runner is a for loop with execSync per word and sleep(7000) between words. Promise.all appears only for DB grounding reads (enrich.ts:1248), never for LLM calls.
Multi-vendor — ~90% not Claude
5 LLM channels; Claude is the exception
Channel map Gemini 2.5 Flash (Pass 1.1/1.3/1.5/2 + judges), Gemini 3.1 Flash Lite (Pass 3 gen), SEA-LION + Sahabat (Pass 3 recovery), and the claude CLI subprocess only in two narrow conditional spots: Pass 1.2 conflict arbiter and Pass 3 low-aid retry. Claude is the exception, not the engine.
Engineered for determinism
Pass 1.1 sense partition is temp 0.0 locked for byte-identical partitions. Conditional fields use strict JSON schemas with object-wrappers. This is structured-output generation — the opposite of open-ended agentic reasoning.
Durable cross-session state via the queue
t_enrich_queue — atomic claim (FOR UPDATE SKIP LOCKED), attempt budgets, heartbeat, stale-row recovery, and two-stage version stamping (v3.6-transient → v3.6) for crash-resume. A Workflow would have to re-implement all of this — net new surface area replacing something that already works.
Parallelism already extracted — and it's cross-bucket only
The offset pipeline overlaps Word N's Pass 3 with Word N+1's Pass 1 — precisely because they hit different rate-limit buckets (gemini-2.5-flash vs gemini-3.1-flash-lite vs claude CLI). That deliberate cross-bucket overlap is the only safe parallelism available. Fan-out within the same bucket would hit the spend cap.
Why the refactor fails

Five head-on collisions with the per-word refactor

Each is a load-bearing constraint a Workflow would have to remove. That removal is the problem.
1
Rate limits forbid the headline feature
Load-bearing rule feedback_api_rate_limits: never run parallel Gemini workers — hit the spending cap 2026-04-01. The limit is per-API-account, not per-process. Fanning out 16 subagents each calling gemini-2.5-flash = 16× the request rate against one bucket = instant spend-cap hit. The pipeline's serial rate-gap timer is exactly the mechanism a fan-out would remove.
2
Wrong LLM — agent() spawns Claude, not Gemini
Pass 1.3 combined fill: 60K-char grounding bundle, strict JSON schema, temp 0.1, Gemini. You cannot map that onto a Claude agent. The only way an agent participates is Bash('npx tsx enrich.ts …') — a thin shell around the daemon's job with added Claude cost and nondeterminism. Strictly worse than execSync.
3
Agents add the variance these passes were engineered to remove
Temp-0.0 lock-in and schema-strict output are the opposite of what a reasoning agent adds. Agents are valuable where judgment helps. These passes are designed to eliminate judgment variance. Wrapping them in a Workflow adds the problem back.
4
Workflow resume is per-run, same-session — not a cross-session queue
Workflow resume does not replace the durable job queue (atomic claim, attempts, heartbeat, transient-version crash-resume across sessions). A Workflow rewrite would have to re-implement all of it — net new surface area replacing a working system.
5
Trips two explicit load-bearing rules
A Workflow looping agent()enrich.ts over a cohort is structurally shell loops over enrich scripts — banned by feedback_use_queue_not_loops. And routing Claude reasoning into the generation path makes enrichment Claude-dependent, which deserves an explicit channel-decision record against feedback_anthropic_sdk_requires_frank_approval.
The honest yes

Where a Workflow genuinely fits

Three surfaces — A is marginal, B and C are where it earns its keep.
A
Cohort outer loop — marginal, probably not worth it
pipeline() over a word-list with per-item failure isolation and a budget-stop is a cleaner expression than the current execSync + sleep loop. But every Gemini-bound stage must run at concurrency 1 — the fan-out benefit evaporates — and the daemon provides cross-session durability (atomic claim, heartbeat, stale-row recovery) that the Workflow lacks. A throwaway Workflow for a one-off bounded cohort where durability doesn't matter is defensible but low value.
Recommendation: skip — leave as daemon
B
QA / pedagogy-review revival — recommended
pedagogy-review.ts is broken today — it reads tables that no longer exist (dropped t080/t090) and writes to a dropped table (t075_sense_qa) — so no word can be programmatically promoted enriched → production. Reviewing an enriched sense for quality and hallucinations is reasoning + judgment = Claude-shaped. Naturally parallel across senses (each is independent). Runs on the Claude rate bucket — separate from Gemini generation, so fan-out is safe and cheap. Workflow adversarial-verify and completeness-critic patterns are the right shape: fan out N skeptic reviewers per sense, flag hallucinations, gate promotion. Three wins in one build: (1) uses the Workflow where it is strong, (2) fixes a known-broken piece, (3) unblocks the production promotion path.
Recommendation: build this
C
Sense-validity / hallucination prune — strong fit, fold into Option B
PLAN-139's plan — score each sense on source breadth + adversarial web-check for borderline senses — is exactly a multi-modal-sweep + adversarial-verify Workflow. Known ghost senses (jadi=Capricorn, ini=rice, bangun=wergeld — all Kaikki-only, zero living usage) are the textbook target for a fan-out of independent web-checking verifiers. This is a one-off corpus batch, not a recurring generation step — fold it into the same Workflow harness as Option B.
Recommendation: fold into QA Workflow harness
D
Pass 3 judge panels — already the correct micro-fanout
The recovery panel already runs gemini-2.5-flash + SEA-LION concurrently — a within-enrich micro-fanout across distinct rate-limit buckets. This is already the right pattern. No refactor needed.
Status: already implemented correctly
Recommendation

What to do — four steps

If Frank approves, the natural next artifact is a /doc plan for the QA-as-Workflow layer — not a rewrite of enrich.ts.
1
Do NOT touch the per-word generation passes or the daemon. They are correctly serial, correctly multi-vendor, and correctly durable. A Workflow makes each of those properties worse.
2
DO scope a Workflow-driven QA / verification layer that runs after enrichment. Fan out Claude reviewers across enriched senses, adversarially verify for hallucinations, and gate the enriched → production promotion that is currently blocked by the broken QA writer.
3
Fold the PLAN-139 validity prune into the same Workflow harness as a one-off multi-modal-sweep + adversarial-verify job.
4
Pin concurrency to respect rate buckets. Claude-reviewer fan-out (Claude bucket) stays separate from any Gemini call (Gemini bucket). Never fan out same-bucket calls.
Cost and risk

Three things to keep in mind

Not blockers — but each deserves a note before the plan is written.
!
Channel separation is a feature, but confirm volume at the 300-language horizon
QA reviewers on the Claude channel bill the Claude subscription, not the Gemini API — the right separation, sidesteps the Gemini spend-cap risk entirely. But sanity-check the volume when the corpus reaches 300+ languages. QA is back-office and sampled (not per-user), so the blast radius is bounded — but confirm before the plan is written.
!
SDK gate is a note, not a block — but log the channel decision
The Workflow agent channel is the Claude Code harness, not @anthropic-ai/sdk in pipeline code, so feedback_anthropic_sdk_requires_frank_approval is not strictly triggered. But making enrichment QA Claude-dependent is a channel decision worth recording explicitly in the plan frontmatter.
!
Don't wrap the daemon in a Workflow for orchestration aesthetics
The temptation is to apply Workflow patterns to the daemon because they look cleaner. Resist it: the daemon's durability (cross-session queue, atomic claim, heartbeat) is load-bearing and the Workflow does not replace it. "Cleaner expression" is not sufficient justification.
Assumptions

What this evaluation rests on

Each tagged with a verification grade. Flag any you believe are wrong or stale.
A1
The pipeline enforces serial LLM execution via a global rate-gap timer
V1 (live code). Read enrich.ts:809–813 (rateGapMs), run-enrich-cohort.ts:143–204 (for-loop + sleep(7000) + failure isolation), enrich.ts:1248 (Promise.all only for DB grounding reads, never LLM calls).
A2
~90% of LLM calls are Gemini / SEA-LION / Sahabat; Claude only in two conditional spots
V3 (doc pointer). ENRICHMENT-PIPELINE-FLOW.md § 4.2 channel table + § 4.8 pinned model-per-pass map (cardinal, last_verified 2026-06-12). Not independently re-run this session.
A3
The 2026-04-01 spend-cap incident from parallel Gemini workers is real; limit is per-API-account
V3 (load-bearing rule). CLAUDE.md feedback_api_rate_limits + SessionStart hook. Incident not re-verified against billing logs this session.
A4
pedagogy-review.ts QA writer is broken and blocks all enriched → production promotion
V3 (doc pointer). Cardinal § 4.7 + § 10.2. Not run this session.
A5
Workflow agent() spawns Claude subagents (not arbitrary LLMs); concurrency cap ≈ min(16, cores−2)
V2 (tool contract). Workflow tool contract in this session's system prompt.
A6
PLAN-139 sense-validity prune is decided but not yet fully implemented
V3 (doc pointer). Cardinal § 4.2a "Gating model superseded" note. Live validity_score population not queried this session.
Decisions ledger

Explicit calls and inferred ones

Green = explicit (stated). Amber = implied (inferred from context). The misread-catcher — flag anything that looks wrong.
1
Frank's question: should the enrichment process be refactored into a dynamic multi-agent Workflow?
Scope: enrich.ts, the multi-pass/multi-channel LLM flow, cohort-enrich.ts, and the queue daemon.
Stated by Frank
2
Per-word generation pipeline and daemon: do NOT refactor into a Workflow
The evaluation's main conclusion. Rate limits, wrong LLM, determinism requirement, cross-session durable state, and two load-bearing rules all point the same direction.
Stated in evaluation — Frank to confirm
3
QA / verification layer: scope a net-new Workflow-driven build after enrichment
The recommended next step. Fixes the broken promotion gate, uses the Workflow where it is strong, and folds PLAN-139 in as a one-off pass.
Waiting on Frank
4
If Frank approves, the next artifact is a /doc plan for the QA-as-Workflow layer
Implied by the recommendation. Not a rewrite of enrich.ts — a new plan for a new layer built alongside the existing pipeline.
Inferred from what you said
5
The daemon and t_enrich_queue are not touched by this work
Implied across all three surfaces: the daemon's cross-session durability is load-bearing and the Workflow does not replace it. The QA layer runs after the queue, not inside it.
Inferred from evaluation
6
Making enrichment QA Claude-dependent should be recorded as a channel decision
Implied by the risk note on the SDK gate. Not a blocking decision — just needs to appear in the plan frontmatter so it is auditable.
Inferred from what you said
Worth your eye

Three things that need a direct call

🔴
pedagogy-review.ts is broken today — no word can reach production. The QA writer reads tables that no longer exist (dropped t080/t090) and writes to a dropped table (t075_sense_qa). This is not a future risk; it is the current state. A QA Workflow is the fix — and it is also where the Workflow earns its keep.
🟡
Depending on Claude for QA is a channel decision worth logging. The Workflow harness is not @anthropic-ai/sdk in pipeline code, so the hard gate does not trigger. But this is a meaningful change in the enrichment stack's dependency profile — the plan frontmatter should capture it explicitly so it's auditable.
👻
Known ghost senses need a prune sweep — a Workflow is the right harness. jadi=Capricorn, ini=rice, bangun=wergeld are all Kaikki-only with zero living usage — the classic Kaikki over-inclusion pattern. PLAN-139 scoped the fix. If the QA layer is green-lit, this one-off batch folds in cleanly as a second pass in the same Workflow.