ADR-037 β Runtime is read-only toward Specialist assets; improvement loop proposes, humans approve
Status: Accepted (2026-07-09; P4.2 shipped β proposal flow live, legacy direct-ingest removed)
Date: 2026-07-07
Closes: OQ-205 (agent skill write-back at M2)
Refs: PRD prd-specialist-value-output Q1/T0.4, ADR-033 L6, ADR-008 (KB architecture)
Related issue: #227
Thesis: the product's core claim is human-vetted quality. Any path where AI output rewrites the assets that govern future AI output β Soul, Skills, or KB β without a human in the loop is a self-amplifying feedback channel that undercuts that claim. The runtime is therefore read-only toward Specialist assets; the improvement loop may only propose, and a human (AM/Expert) approval is what applies.
Contextβ
OQ-205 asked whether a Specialist agent should ever write persona content (SOUL.md, skills) back to storage after Expert correction, with Option A ("read-only forever") as the proposed default. While that question sat open, one write-back path already shipped and contradicts it:
api/src/haystack/jobs/correction-refinement.processor.ts takes every unprocessed Expert correction, has an LLM categorise it (applies_to, key_learning, write_to_kb), and β when the LLM says write_to_kb: true β directly ingests a generated markdown document into the org's KB (HaystackIngestionService.upload), or appends to an existing correction doc. No human sees the generated KB entry before it starts influencing retrieval for live drafts.
Why this violates the default:
- No quality gate at the exact point quality matters. The Expert reviewed the reply; nobody reviews the generalised lesson the LLM extracted from it. A mis-generalised lesson ("always promise refunds within 24h") silently steers every future draft for that org.
- Prompt-injection surface. The categorising LLM reads client-authored message content (#3190 hardened the prompt, but the write decision is still model-made). A poisoned inbound message steering
write_to_kbis a KB-poisoning primitive. - Unauditable provenance. KB docs created this way look like
admin_uploadsource; operators cannot distinguish human-curated knowledge from AI-self-written knowledge.
Decisionβ
- OQ-205 Option A β the runtime never writes Soul/Skill/KB assets. Specialist assets (SOUL.md, skills, KB documents, prompt templates) are operator-managed. No agent or in-process AI pipeline mutates them as a side effect of handling traffic.
- The improvement loop outputs proposals only. Expert corrections, edit-ratio signals, and any future learning pipeline produce proposals; a human (AM or Expert) approval step is what applies a proposal to a Specialist asset. Approval carries an audit trail.
- Transition (expand-contract, complete): the legacy correctionβKB auto-ingest in
correction-refinement.processor.tswas gated behind the feature flagcorrection_auto_ingest_enabled(default OFF) during the expand phase. That flag AND the entirecorrection-refinement.processor.tsdirect-ingest pipeline are now removed β P4.2a (LoopProposalProducerService) + P4.2b (approval UI) shipped, and P4.2c executed this contract step. P4.2a's producer consumes the unprocessed backlog (corrections.processed_at IS NULL) as human-approvedkb_golden_answerDRAFT proposals; its backlog query is deliberately flag-independent, so nothing is lost.
Consequencesβ
- Corrections accumulate unprocessed until P4.2. Accepted: the rows are durable (
corrections.processed_at IS NULLis the queue), the few-shot path (correction_fewshot_enabled, ADR-033 P1) reads corrections directly and is unaffected, and the alternative β continuing to self-apply β is worse than a delay. - Emergency path exists but is loud. An org that demonstrably needs auto-ingest before P4.2 can have the flag flipped ON per-org via the Ops feature-flag surface; every change requires a reason and lands in
feature_flag_audit, so exceptions are explicit and reviewable. - KB stops growing from corrections by default. AMs/Experts who relied on the automatic refinement docs must curate KB entries manually until P4.2.
- Gate short-circuits before the categorisation LLM call β flag OFF also spends zero LLM budget on corrections.
Alternatives consideredβ
- Option B β PR-promotion only (agent proposes a patch, human approves in the platform UI). This is the end-state (Decision 2 / P4.2); rejected as the immediate stopgap because building the proposal + approval surface is P4.2-sized work, while the flag gate ships now and loses nothing.
- Option C β audited agent write-back (agent writes in place; every write versioned and logged). Rejected: audit-after-the-fact is not a quality gate. The write has already influenced live drafts by the time a human reads the log β at the exact moment quality is the product's core claim, review must be before effect, not after.
Verificationβ
api/test/loop-proposal-producer.spec.tsβLoopProposalProducerService.runOnceconsumes thecorrections.processed_at IS NULLbacklog askb_golden_answerDRAFT proposals and marks each correction processed, independent of any feature flag (the human-approved proposal flow that replaces the legacy direct-ingest). Includes the migratedcorrections.processed_at columnschema guarantee (nullable, no default) the producer's backlog selector depends on.api/src/feature-flags/feature-flag.service.spec.tsβcorrection_auto_ingest_enabledis no longer inFLAG_KEYS(the flag and thecorrection-refinement.processor.tsdirect-ingest pipeline it gated were removed in P4.2c).