The Self-Evolution Flywheel — transparent, steerable, governed¶
The 24/7 propose-only Loop engine that mines the ecosystem corpus for improvements, distils them into reviewable specs, and develops the approved ones — made legible mid-flight (the operator can see what it is doing and why), steerable (the operator can reprioritize, pause, and veto), and governed (nothing lands without a human/Claude gate). Concepts: AU-KG.research.evolutionstate-live-surface-per / AU-KG.research.saturation-gauge-aggregates-four / AU-KG.research.close-distill-develop-seam / AU-OS.config.autonomous-spec-develop-off (flywheel transparency + review-veto) and AHE-3.71 / AU-AHE.harness.when-outcome-names-agent / AU-AHE.harness.callers-feed-back-per (the per-agent hardening loop), plus AU-KG.retrieval.assimilated-from-mragent / AU-KG.memory.generation-scoped-selective-reward (the memory substrate the loop reasons over).
This page is the operator-facing view of the flywheel. For the broader feedback-loop architecture it sits inside, read Self-Improving Reasoning Substrate (route → reason → measure → learn) and Failure-Driven Evolution (how production failures enter the loop as addressable gaps). This page does not re-derive those; it documents the transparency + steering + governance layer that was added on top, and the AHE-3.71/72/73 hardening cycle that makes "every exception the operator resolve hardens the system" literally true.
1. The flywheel: distill → develop → evolve¶
The whole flywheel is one propose-only cycle driven by
LoopController.run_one_cycle in
agent_utilities/knowledge_graph/research/loop_controller.py. A single controller
advances every active Loop (research / develop / skill) through one hot path —
there is no separate goal-runner or research-runner. The research path composes the
existing primitives into one cycle that makes the KG self-improving without
auto-merging anything:
intake → active Loops (research topics with no ADDRESSED_BY; AU-KG.research.these-properties-carry)
acquire → semantically related sources per topic (research/search)
resolve → ADDRESSES edges source→topic so the loop converges
reason → OWL/RDF reasoning over the ecosystem, harvest extrapolations (AU-KG.research.best-effort-lightweight-never)
distill → SpecDraft markdown into .specify/specs/kg-distilled/ (gated)
synth → a TeamSpec/AgentSpec proposal persisted to the KG
Every artifact a cycle emits is a DRAFT / proposal — spec markdown under
.specify/ and KG proposal nodes. No code execution, no PR merge, no edits outside
.specify/. The cycle is exposed on-demand via graph_loops / graph_orchestrate
(and the REST twin) and runs continuously via a throttled daemon tick / the unified
scheduler's self_evolution schedule.
The distill → develop seam that was broken (and the fix)¶
Historically the cycle's distill stage wrote SDD markdown into .specify/ and
stopped there. The develop / code path (change_publisher.governed_publish →
AU-AHE.harness.promotion-governance-validator–3.24) consumes promoted proposal nodes, not those .md files — so the two
tracks were disjoint: a distilled spec was a dead-end file nothing can develop.
LoopController._distill_specs now closes that seam (CONCEPT:AU-KG.research.close-distill-develop-seam). After
write_spec_drafts writes the markdown, each draft is persisted as a first-class,
queryable :SpecProposal node via
spec_proposals.persist_spec_proposal, linked DISTILLED_FROM its source concepts so
the why is traversable. The spec is then fed — only after approval — into the
existing promotion pipeline as the proposal that pipeline already consumes (no new
code-gen path). On the run side, LoopController._advance_develop detects a
spec-bound develop Loop (one carrying a spec_id) and routes it into
spec_proposals.develop_spec → governed_publish, where the merge_promotion
human-approval gate (OS-5.24) and the capability-ratchet regression gate (AU-AHE.evaluation.capability-benchmark-regression-ratchet)
stay on the path. So "distill specs → develop those specs" finally flows end to end,
governance intact.
flowchart LR
subgraph cycle["LoopController.run_one_cycle (propose-only)"]
intake["intake / acquire / resolve"] --> reason["reason (OWL/RDF, AU-KG.research.best-effort-lightweight-never)"]
reason --> distill["distill → SpecDraft .md"]
distill --> persist["persist_spec_proposal\n:SpecProposal (pending_review)\nDISTILLED_FROM concepts\n(AU-KG.research.close-distill-develop-seam)"]
end
persist --> gate{"spec-review\ncheckpoint\n(AU-OS.config.autonomous-spec-develop-off)"}
gate -- "approve" --> dev["develop_spec → governed_publish\n(merge_promotion gate +\ncapability ratchet)"]
gate -- "reject / veto" --> dead["rejected (terminal)"]
gate -- "edit" --> persist
dev --> branch["reviewable branch\n(never auto-merged)"]
Wave 6 — the unified Gap→SDD→Implement→Promote→Close spine (operator surface)¶
CONCEPT:AU-AHE.harness.canonical-gap-lifecycle. Before Wave 6 the platform had three
unrelated notions of "gap" (a transient failure_gap :Concept dict — see
Failure-Driven Evolution for that legacy shape — a
dead-end sdd_plan node, an in-memory SkillGap), plus a fourth (code-audit
findings), joined by 7+ disjoint id schemes. knowledge_graph/research/gaps.py
repurposes the existing KnowledgeGapNode model as the single gap
representation every discovery track now folds into:
flowchart LR
failure["failure_analyzer\n(production failures)"] -->|submit_gap| gap
research["plan_synthesis\n(research/OSS)"] -->|submit_gap| gap
skill["skill_evolver\n(skill coverage)"] -->|submit_gap| gap
audit["audit_gap_detector\n(code-correctness/security)"] -->|submit_gap| gap
manual["operator\n(graph_loops action=submit_gap)"] -->|submit_gap| gap
gap((":Gap\nopen")) -->|SPECIFIED_BY| spec[":SpecProposal\n(distill/review gate above)"]
spec -->|develop_spec| loop["develop-Loop\nRESOLVES the origin gap"]
loop -->|governed_publish| resolved["gap.status = resolved\n(the visible END)"]
Every hop is queryable and steerable through the SAME graph_loops MCP tool (+ REST
twin) the rest of this page documents — no separate gap-management surface:
graph_loops(action="gaps")— the open backlog, highest priority (lowestpriority_bucket, derived fromseverity) first.graph_loops(action="submit_gap", data_json={...})— file one by hand (an operator-triaged issue) through the exact samegaps.submit_gapthe four autonomous discovery tracks call — idempotent on the canonicalgap:<source>:<signature>id, and leased as a first-classWorkItemso it is schedulable like any other unit of work.graph_loops(action="gap", loop_id="<gap id>")— one gap plus its D6 provenance chain: theSpecProposalit wasSPECIFIED_BY(once distilled — see the seam above) and the develop-Loop thatRESOLVESit (once bound), so "which commit closed this gap" is a traversal, not an archaeology exercise.- The gap's derived develop-Loop is driven exactly like any other Loop
(
run/drive);spec_proposals.develop_specbinds aRESOLVESedge back to the origin gap, andgaps.resolve_gaps_for_loopflips it toresolvedwhen that Loop'sgoverned_publishlands — the samemerge_promotionhuman-approval gate and capability-ratchet regression gate as every other promoted change.
The read-only per-graph RBAC-ownership disposition pass (W2.8,
CONCEPT:AU-KG.audit.graph-ownership-disposition — a different, catalog-level
governance concern, not the Gap content lifecycle above) is reachable the same
way, on graph_governance: ownership_report runs the disposition pass;
ownership_apply previews (always dry-run) the UNAMBIGUOUS grant plan + its
rollback. The live mutating grant apply stays a separate, human-gated CLI
(scripts/apply_graph_ownership_grants.py --apply-unambiguous, HG-4) —
deliberately never reachable from MCP/REST.
2. Transparency: live EvolutionState + the saturation gauge¶
"The operator cannot steer what the operator cannot see." The observation plane every steering action hangs off. Concepts: AU-KG.research.evolutionstate-live-surface-per (live state + per-stage beacon), AU-KG.research.saturation-gauge-aggregates-four (saturation gauge). Module:
agent_utilities/knowledge_graph/research/evolution_state.py.
The per-stage progress beacon (AU-KG.research.evolutionstate-live-surface-per)¶
The deployed loop used to persist an EvolutionCycle audit node only at finalize —
so until a cycle finished it was opaque, and an operator can not see "what is it
mining / distilling / developing right now, and why". StageBeacon fixes that. One
beacon is created per run_one_cycle, and beacon.enter(stage) is called at every
stage boundary (_stage() in loop_controller.py calls it before running each stage),
upserting a single mutable :EvolutionBeacon node (id evolution:beacon). So a
mid-cycle graph_loops(action="state") reports exactly which loop and stage is live,
what it is acting on, and why (the focus_query / the open gaps it is mining). Every
beacon write is best-effort — instrumentation never aborts the cycle. read_beacon()
reads that one node back (an O(1) read, not a scan).
The aggregated read (AU-KG.research.evolutionstate-live-surface-per)¶
read_evolution_state(engine) is the operator's "make it transparent so I can steer"
surface. It folds into one legible read:
beacon— the live stage + rationale (fromread_beacon).velocity— the AU-AHE.sdd.recursive-improvement-instrumentation-aggregatingImprovementVelocityverdict (improving/steady/stalling/idle) fromimprovement_ledger.improvement_velocity: is the loop still improving, how fast, and is it emitting code or only prose?open_gaps— the recent per-cycleopen_gapstrend (_open_gaps_trendreads theEvolutionCycleaudit nodes' metadata).ingestion_coverage— the AU-OS.deployment.flagging-repos ingestion-coverage % (how much of this repository's corpus is ingested) viadeployment.doctor._check_ingestion_coverage.specs— the distilled-spec backlog counts + a pending sample (spec_proposals.specs_summary).saturation— the gauge below.steering— a built-in cheat-sheet of the exact tool calls to pause, reprioritize, review a spec, or acquire more corpus.
The saturation gauge (AU-KG.research.saturation-gauge-aggregates-four)¶
saturation_gauge(...) folds the signals that already exist into one 0..1
reading of "how mined-out is the current corpus":
| Component | Weight | Meaning |
|---|---|---|
velocity |
0.4 | AU-AHE.sdd.recursive-improvement-instrumentation-aggregating verdict: stalling→1.0, steady→0.5, improving/idle→0.0 |
coverage |
0.3 | AU-OS.deployment.flagging-repos ingestion coverage fraction (unknown ⇒ neutral 0.5) |
gaps |
0.3 | open_gaps trend: still shrinking fast ⇒ low; flat/rising ⇒ high |
gauge = 0.4·velocity + 0.3·coverage + 0.3·gaps. It is saturated when
gauge ≥ SATURATION_THRESHOLD (the module constant 0.66). The gauge is computed every
cycle in LoopController._finalize_metrics (the hot-loop path skips the coverage probe
for speed) and on every read_evolution_state read.
Crucially, when the gauge is high and velocity is stalling, it sets
request_more=True and emits a queryable :EvolutionSaturationSignal
(emit_saturation_signal) carrying a recommendation — but it never auto-fetches.
Acquiring more research / cloning more codebases is left as a steerable decision for
Claude/the human (enable discovery, run background research, or add to the breadth
corpus). This is the deliberate boundary: the loop tells the operator it is mined out; the operator
decide to feed it.
How it is surfaced (the "steerable" half)¶
Both surfaces dispatch into the same core (the Two surfaces by default contract):
- MCP:
graph_loops(action="state")→read_evolution_state;graph_loops(action="specs", status=…)→ the spec backlog;graph_loops(action="review", spec_id=…, decision=…)→ the veto gate. Registered inagent_utilities/mcp/tools/state_tools.py. - REST: the
/graph/loopstwin.
So the harness can query live EvolutionState, see the current stage and the saturation
reading, and steer — which is exactly what the AGENTS.md "Evolving / managing the
ecosystem → review-veto" directive asks of the orchestrator.
3. Governance: spec proposals + the review/veto gate (AU-KG.research.close-distill-develop-seam / AU-OS.config.autonomous-spec-develop-off)¶
The propose-and-hold checkpoint that keeps a 24/7 auto-coder safe. Module:
agent_utilities/knowledge_graph/research/spec_proposals.py.
A distilled :SpecProposal moves through a lifecycle, and pending_review is the
default landing state — review-first, not act-first:
pending_review ──approve──▶ approved ──develop──▶ developing ──▶ published
│ ▲ └──▶ reverted (ratchet regression)
│ └──edit (merge changes, hold again)
└──reject / veto ──▶ rejected (terminal)
review_spec(engine, spec_id, decision, …) is the explicit human/Claude decision point:
approve→ statusapproved, anActionDecisionaudit is recorded under thespec_promotionActionPolicy tier (_audit_spec_decision), and (by default) adevelopLoop is bound to the spec (_bind_develop_loopsubmitsloop:develop:<spec_id>and stampsspec_idon it) so it enters the develop pipeline and is visible + steerable ingraph_loops.reject/veto→ statusrejected— the veto terminal; it never develops.edit→ mergeedits(title/problem/approach/value/target_file) and keep it inpending_reviewfor another look.
The approved spec is shaped by _spec_to_proposal into exactly the dict the promotion
path already consumes, so an approved spec is a valid proposal — develop_spec
runs the existing code_synthesis → change_synthesis → validate_in_sandbox →
change_publisher → capability_ratchet path, which itself queues a reviewable branch
(never an auto-merge) behind the merge_promotion gate.
Propose-and-hold is the default; auto-advance is opt-in¶
The 24/7 loop only auto-advances a spec when two things are deliberately relaxed:
KG_LOOP_AUTO_DEVELOP(config.kg_loop_auto_develop, defaultFalse) — when on,_distill_specscallsauto_advance_specsafter persisting the drafts.- The
spec_promotionActionPolicy tier — which itself defaults toapproval_requiredindeploy/action-policy.default.yml({kind: spec_promotion, target: "*", tier: approval_required}).
auto_advance_specs consults the OS-5.24 ActionPolicy per pending spec; an allowed
verdict (an operator who relaxed that tier) approves + binds a develop Loop, otherwise
the spec stays pending_review and a human approval is queued. Acquisition is never
auto-run; this only governs develop. So a fresh spec sits in pending_review until a
human/Claude approves it — review-first by default, exactly as a 24/7 auto-coder needs.
4. The AHE-3.71/72/73 hardening loop — one exception → a durable hardening¶
The mechanism that makes "every exception the operator resolve hardens the system" real, end-to-end for one agent. Modules:
agent_utilities/harness/evolve_agent.py,agent_utilities/harness/program_optimization.py,agent_utilities/knowledge_graph/adaptation/feedback.py. Concepts: AHE-3.71 (gated apply + audit), AU-AHE.harness.when-outcome-names-agent (per-agent attribution), AU-AHE.harness.callers-feed-back-per (the orchestrator).
This converts the previously dormant/stubbed metric→optimize→harden substrate into one
real, gated metric → native optimize → hardened-prompt cycle for a single agent's
system prompt. It reuses the existing StructuredPrompt and reward machinery while the
engine jobs plane owns compilation and durable evidence.
AU-AHE.harness.when-outcome-names-agent — per-agent attribution¶
When a delegated run produces an outcome, FeedbackService.record_action_outcome(…,
agent_id=…) derives an opaque agent_ref and tags the resulting eval case with that
reference (see adaptation/feedback.py). The supplied identifier is not copied into
optimizer or evolution artifacts. Then:
FeedbackService.agent_eval_cases(agent_id)returns the eval-corpus slice attributed to that agent — its measured executions (expected vs the goal that was reached), which double as the training signal and the held-out scoring slice for its prompt.FeedbackService.build_agent_trainset(agent_id)exposes provider-neutralcontext/task/responserows plus their opaqueexample_refat the governed corpus boundary.
So an agent's own real outcomes — including the ones that failed — become the corpus that steers its prompt optimization. Attribution is by agent, not just by trace signature.
AU-AHE.harness.callers-feed-back-per — harden_agent_prompt, the orchestrator¶
EvolveAgent.harden_agent_prompt(agent_id, prompt_path, …) runs ONE closed cycle:
- Attribute — pool the agent's
action_outcomecases into a per-agent trainset + eval slice (AU-AHE.harness.when-outcome-names-agent). - Optimize —
run_program_optimizationon thesystem_prompttarget with that trainset. The native request and result contain opaque content, demonstration, evidence, and provenance references—not raw trace examples. - Build — validate a reference-only
ProgramCompiledState. The prompt blueprint and proposal persist this state, whileFeedbackService.resolve_program_demonstrationsmaterializes selected examples from the governed corpus only for the current execution. The renderer bounds and JSON-encodes each value inside a fixed data delimiter and tells the model never to treat instructions inside an example as policy. - Evaluate —
score_prompt_against_corpusscores the baseline against that ephemeral execution rendering. Resolved example text is discarded after evaluation. - Decide + apply —
should_promote(baseline, candidate, min_delta), then a gated apply.
It returns a PromptHardeningOutcome (status = applied | proposed | rejected |
no_data) — always with an audit trail.
AHE-3.71 — the gated apply + the audit/approve surface¶
EvolveAgent.apply_edits is no longer a stub. For a system_prompt edit carrying a
native reference-only compiled state it calls _apply_prompt_edit, which attaches that
state to its StructuredPrompt file via .save() and commits it — but only when
all three hold (CONCEPT:AU-AHE.harness.unified-promotion-gate — the unified evolution
matrix's governance-bypass closure; see §8 below):
- the candidate beat baseline (
edit.metadata["promote"]); - the
KG_AGENT_AUTO_APPLYgate is on (config.kg_agent_auto_apply, defaultFalse/ shadow) — this is a NECESSARY, no longer Enough, condition; and - the SAME unified
action_policy.decide()veto every other promotion vector passes —artifact_promotion.promote(..., PromotionCandidate(artifact_kind="prompt", ..., policy_kind="promote_prompt_version"))— whose shipped default tier (approval_required) means a benchmark win +KG_AGENT_AUTO_APPLY=trueALONE still does not apply; an operator must also relax that tier (or approve the queued request). Before this closure,KG_AGENT_AUTO_APPLYalone was enough — weaker governance than a skill-markdown promotion, which already went throughaction_policy.decide().
Off-gate (the default, or a queue_approval/deny verdict) the cycle is
propose-only: a queryable, git-diffable ProposedPromptChange audit record is
written under .specify/proposals/<id>.json (plus a best-effort KG node) carrying
opaque agent/component/evidence references, compiled metadata, the before/after metric,
and BOTH decisions (apply_status and the action_decision verdict). It never stores
raw trace examples, source-system identifiers, or a local path. The live prompt is
left untouched. EvolveAgent.approve_proposed_change(proposal_ref) is the steerable
reviewed apply path: a winning prompt can go live by review instead of by flipping the
global gate. Approval checks the proposal integrity reference, revalidates compiled
metadata, binds the opaque component reference to the in-memory registry, and rejects
workspace escapes. Background failure-cluster optimization is always review-only even
when the global auto-apply gate is enabled; only the metric-scored per-agent hardening
path can be marked automatically eligible.
The same boundary applies to round manifests. persist_manifest projects the operational
ChangeManifest into a durable schema containing opaque round/edit/component/task/evidence
references, scores, booleans, and validated compiled metadata. It omits local paths, raw
diffs, free-form trace text, and source identifiers; the filename uses a portable digest.
Deep-analysis output is bounded, must select a registered component type, cannot nominate
a path, and may reference only task IDs already present in the in-memory evidence corpus.
flowchart LR
fail["delegated run fails /\nungrounded / escalated"] --> outcome["record_action_outcome\n→ opaque agent/example refs"]
outcome --> attribute["agent_eval_cases +\nbuild_agent_trainset\n(governed store)"]
attribute --> optimize["run_program_optimization\nreference-only request/result"]
optimize --> build["validate ProgramCompiledState\n(no raw demonstrations)"]
build --> resolve["resolve demonstration refs\nephemerally for execution"]
resolve --> eval["score baseline vs execution render"]
eval --> promote{"should_promote?"}
promote -- "no" --> rejected["rejected (audit only)"]
promote -- "yes" --> gate{"KG_AGENT_AUTO_APPLY?"}
gate -- "off (default)" --> proposed["ProposedPromptChange\n(.specify/proposals/) — held\n→ approve_proposed_change()"]
gate -- "on" --> policy{"action_policy.decide\nkind=promote_prompt_version"}
policy -- "queue_approval / deny\n(shipped default)" --> proposed
policy -- "allow (operator-relaxed)" --> applied["StructuredPrompt.save() + commit"]
This is the loop AGENTS.md points at: when the operator resolve an exception, the fix (a hardened prompt — and, by the same spine, a fixed tool binding or a new skill) becomes a durable hardening of the system so it self-handles that case next time.
5. The memory substrate the loop reasons over (AU-KG.retrieval.assimilated-from-mragent / AU-KG.memory.generation-scoped-selective-reward)¶
The flywheel's reasoning quality depends on what it can recall and how trustworthy that recall is. Two assimilated primitives harden that substrate. (Note the renumbering: AU-KG.retrieval.assimilated-from-mragent is MRAgent active reconstruction; the selective-erasure primitive was originally tagged 2.275 and renumbered to AU-KG.memory.generation-scoped-selective-reward to avoid the collision — both IDs are confirmed against the code below.)
AU-KG.retrieval.assimilated-from-mragent — MRAgent active Cue-Tag-Content reconstruction¶
Module: agent_utilities/knowledge_graph/retrieval/active_reconstruction.py (assimilated
from MRAgent, arXiv:2606.06036). Instead of one-shot top-k retrieval, reconstruct()
walks a Cue → Tag → Content graph in an evidence-conditioned loop:
- Cue → Tag — enable candidate associative tags (relation types) on the current
cue frontier and keep only the
tag_top_kmost query-relevant (pruning the combinatorial neighbour blow-up a fixed n-hop expansion will incur). - (Cue, Tag) → Content — expand content only along the selected tags, dropping
neighbours below
relevance_floor. - Content → Cue — the best fresh content (
content_top_k) becomes the next cue frontier (reverse traversal),
so the walk progressively reconstructs a query-relevant subgraph and self-stop
(reusing the shared IterativeStopper, AU-KG.retrieval.adaptive-stopping-iterative-retrieval) once fresh evidence stops arriving. It is
dependency-injected (callers supply neighbor_fn and score_fn) with a
dependency-free lexical default (lexical_relevance — no torch) and engine-backed
adapters (engine_neighbor_fn, resolve_seeds). It is wired into the live path: the
entity_context "why" intent natively runs reconstruction (and falls back to the census
on no seed, so zero regression) — reachable via graph_explain action=explain
target='entity:why' and its REST twin. The ontology gains :ReconstructionTrajectory
(the analog of MRAgent's reconstructed memory state).
AU-KG.memory.generation-scoped-selective-reward — generation-scoped selective reward erasure¶
Module: agent_utilities/knowledge_graph/retrieval/capability_index.py (assimilated from
the Red Queen Gödel Machine, arXiv:2606.26294). The memory-maintenance quadrant forgot
learned reward only by age (decay_rewards, KG-2.4) or idle/max-age reapers — never
by provenance. So when an entity's generation changed (a capability redeployed, a
document re-ingested with materially new content, a model/embedding regime swapped), the
in-place CapabilityIndex.add() upsert replaced the vector but kept the stale reward
EMA — designate() kept ranking on evidence scored under a representation that no
longer existed. This is a non-stationary utility carried across a regime change.
Two surfaces fix it:
- Native, default-on on the live ingestion upsert path:
CapabilityIndex.add()auto-erases an id's stale reward when a re-embed's cosine distance exceeds_REWARD_REGEN_DISTANCE(the module constant0.25— one correct value, not a flag; bge-m3 minor-edit re-embeds stay well above 0.75 similarity, a material rewrite drops below). Content-stable re-adds keep their reward. - Explicit, provenance-scoped:
CapabilityIndex.selective_erase_rewards(ids)erases exactly the records whose source/evaluator/impl was superseded (order-independent), preserving every reward not inids, plus areward_erasuresobservability counter. Surfaced overgraph_feedback(MCP) andPOST /graph/feedback(REST) viaFeedbackService.record_correction(correction_type="selective_erasure").
The effect: the router re-climbs under the new regime from the neutral prior instead of being anchored by stale (possibly reward-hacked) utility forever. Unlike uniform time decay, this is targeted by provenance, not by age.
6. Config knobs¶
All evolution behavior follows Configuration discipline (a flag is a last resort). The few that exist:
| Setting (env alias) | Default | Effect |
|---|---|---|
KG_LOOP_DISTILL (config.kg_loop_distill) |
off | enable the distill stage (writes :SpecProposal drafts) |
KG_LOOP_BREADTH (config.kg_loop_breadth) |
— | ingest the OSS/repos/docs breadth corpus each cycle (idempotent) |
KG_LOOP_STANDARDIZE (config.kg_loop_standardize) |
— | enterprise standardization pass (needs a harvested estate) |
KG_LOOP_DISCOVER (config.kg_loop_discover) |
off | opt-in research discovery/intake (external calls) |
KG_LOOP_AUTO_DEVELOP (config.kg_loop_auto_develop) |
False |
let the 24/7 loop auto-advance approved specs through the spec_promotion gate (review-first otherwise) |
KG_AGENT_AUTO_APPLY (config.kg_agent_auto_apply) |
False (shadow) |
NECESSARY but not Enough: lets a hardened prompt that beat baseline be CONSIDERED for a live write; the promote_prompt_version ActionPolicy tier below must ALSO allow. Off ⇒ propose-only either way. |
spec_promotion ActionPolicy tier |
approval_required |
the governance tier on spec→develop promotion (deploy/action-policy.default.yml) |
promote_prompt_version / promote_tool_description_version / promote_mined_claim ActionPolicy tiers |
approval_required |
the SAME unified gate (§8) every promotion vector passes; KG_AGENT_AUTO_APPLY alone can no longer apply a prompt, and graph_ops_causal's materialize_claims alone can no longer promote a mined claim |
Non-flag tunables (module constants, intentionally not env vars):
SATURATION_THRESHOLD = 0.66(evolution_state.py) — the saturation cutoff._REWARD_REGEN_DISTANCE = 0.25(capability_index.py) — the re-embed distance that counts as a new generation and erases the stale reward.
7. Operating the flywheel¶
Watch it. Query live state — current stage + why, saturation, gaps trend, velocity, spec backlog:
graph_loops action=state # → read_evolution_state
graph_loops action=specs status=pending_review
graph_schedules action=list # the self_evolution schedule + run state
When state.saturation.request_more is true, the corpus looks mined out — the operator decide
to acquire more (enable KG_LOOP_DISCOVER, run graph_research action=background_research,
or clone more codebases into the breadth corpus). The loop will not auto-fetch.
Review / veto specs (the governance gate):
graph_loops action=review spec_id=<id> decision=approve # binds a develop Loop
graph_loops action=review spec_id=<id> decision=reject # veto (terminal)
graph_loops action=review spec_id=<id> decision=edit # merge edits, hold again
Approved specs develop through governed_publish, which queues a reviewable branch
behind the merge_promotion gate and the capability ratchet — never an auto-merge.
Steer the loop itself:
graph_loops action=prioritize loop_id=<id> priority=high
graph_schedules action=disable name=<evolution schedule> # pause
Close the hardening loop. After resolving a delegated-run exception, harden the
agent that failed: run EvolveAgent.harden_agent_prompt(agent_id, prompt_path). By
default the winning candidate is held as a ProposedPromptChange under
.specify/proposals/ — review the before/after metric and approve it with
approve_proposed_change(proposal_id) (or set KG_AGENT_AUTO_APPLY and relax the
shipped promote_prompt_version ActionPolicy tier to let beats-baseline candidates land
automatically — KG_AGENT_AUTO_APPLY alone is no longer enough, §8). Either way an
audit record and the manifest are written, so the hardening is never silent — and the
next run self-handles the case the operator just fixed.
8. The unified promotion gate + evolution matrix (CONCEPT:AU-AHE.harness.unified-promotion-gate)¶
Every artifact-mutation vector — skill markdown, prompt, mined claim, merged spec, routing policy — funnels through the SAME contract: a provenance-carrying reward, ONE comparison-then-governance gate, and a queryable version lineage. Modules:
agent_utilities/harness/reward_signal.py,agent_utilities/orchestration/ artifact_promotion.py,agent_utilities/models/knowledge_graph.py(ArtifactVersionNode),agent_utilities/knowledge_graph/research/evolution_state.py.
The contract¶
RewardSignal(valuein[0, 1],source,confidence,provenance_ref,reason) names the[0, 1]-clamped reward convention every producer in the codebase already converged on independently (Langfuse blend, skill benchmark score, capability EMA, ...) and adds WHO computed it.blend()generalizes the existing 2-operandlangfuse_signal.blend_rewardto N confidence-weighted signals.PromotionCandidate/promote()(orchestration/artifact_promotion.py) is the ONE entry point every optimizer's promotion boundary calls: (1)evaluate_promotion— the candidate-vs-incumbent comparison gate (skipped entirely whenincumbent_reward=None, the shape a comparison-less vector like a mined claim or a golden-loop proposal uses — gated on quality + governance instead of a held-out score), then (2) the SAMEaction_policy.decide(kind=...)veto every reserved-kind call site already used, then (3) a caller-legiblePromotionVerdict. It performs NO KG writes itself and never raises (a policy-consult failure fails closed).ArtifactVersionNode(models/knowledge_graph.py) generalizesSkillVersionNode's propose-only lifecycle contract to every vector:artifact_kind/artifact_id/status(proposal|active|rejected)/origin/reward/reward_source(theRewardSignalthat gated this version, e.g."internal_corpus")/task_count/notes/transfer_scores.SkillVersionNode/PromptVersionNodeare thin subclasses (additive fields, zero existing-caller breakage — the strangler pattern, not a parallel v2 type).- The matrix query —
artifact_evolution_summary(engine, artifact_kind=None, limit=200)(evolution_state.py) is the cross-vector read: scans everyArtifactVersionNodelabel, aggregates bystatusandkind, returns a drill-down sample. It is folded intoread_evolution_state()'sartifact_versionskey, so it is queryable from the SAME live surface every other steering read already uses —graph_loops action=state(MCP) and its REST twin — with zero new tool/route code.
Migration status (5 promotion pipelines)¶
Reserved kind |
Call site | On promote()? |
|---|---|---|
promote_skill_version |
skill_evolution.run_reflact_cycle |
Yes (pre-existing) |
merge_promotion |
auto_merge.GovernedAutoMerger._consult_action_policy |
Yes (pre-existing) |
promote_mined_claim |
ops_causal_tools._materialize_root_cause_claims (W2.7, closes B3) |
Yes |
promote_mined_claim |
loop_controller._run_insight_validation |
No — direct action_policy.decide() (already governed, not a bypass; see migration shape) |
route_policy_update |
loop_controller._run_trace_mining (via trace_pattern_miner) |
No — direct action_policy.decide() |
spec_promotion |
spec_proposals.review_spec / auto_advance_specs |
No — direct action_policy.decide() |
Two governance-bypass-closing vectors were also added directly onto the contract this
wave (not part of the original 5, born from closing the KG_AGENT_AUTO_APPLY bypass,
§4 above): promote_prompt_version (evolve_agent._apply_prompt_edit) and the reserved
(currently uncalled — no ComponentType.TOOL_DESCRIPTION edit exists yet, so it stays
unwired per Wire-First) promote_tool_description_version.
Migration shape for the 3 remaining direct-action_policy.decide() call sites —
mechanical, low-risk (same reserved kind, same shipped tier, same audit trail; only the
call-site plumbing changes):
- Wrap the vector's own quality/confidence signal as a
RewardSignal(e.g.CandidateInsight.confidence→RewardSignal(value=..., source="internal_corpus")). - Build a
PromotionCandidate(artifact_kind=<name>, artifact_id=<target>, candidate_ref=<target>, candidate_reward=<the RewardSignal>, incumbent_reward=None, policy_kind=<the existing reserved kind string>, source="loop_engine", reason=...)—incumbent_reward=Nonefor all three (none compares a candidate against an incumbent; each gates on quality + governance, the same shapepromote_mined_claimuses). - Replace the hand-built
ActionRequest+action_policy.decide(...)+ manualdecision.decision in ("allow", "allow_notify")check withverdict = promote(engine, candidate); verdict.decision; verdict.approved. - Keep every existing side effect (flywheel transitions,
ProposalPublication/ActionDecisionaudit writes) exactly where it is —promote()is a decision boundary, not a persistence layer; the caller still owns every write.
Not required this wave (the 2 pre-existing promote() call sites already exceed the
"migrate at least 2" bar); tracked here so the remaining 3 direct call sites move onto
the shared contract in a follow-up rather than accumulating a second pattern.