Platform Note: State files use the platform's native directory:
.claude/(Claude Code),.codex/(Codex CLI), or.cursor/(Cursor IDE). Shared metrics live in.orchestrator/metrics/(v2) with fallback to<state-dir>/metrics/for pre-v2.0 legacy data. Seeskills/_shared/platform-tools.md.
Evolve Skill
Phase 0: Bootstrap Gate
Read skills/_shared/bootstrap-gate.md and execute the gate check. If the gate is CLOSED, invoke skills/bootstrap/SKILL.md and wait for completion before proceeding. If the gate is OPEN, continue to Phase 1.
Phase 1: Config & Data Loading
1.1 Read Session Config
Read and parse Session Config per skills/_shared/config-reading.md. Store result as $CONFIG.
1.2 Check Persistence
Extract persistence from $CONFIG. If persistence is false, abort with message:
"Learnings require persistence to be enabled in Session Config. Add
persistence: trueto your Session Config block (CLAUDE.md for Claude Code, AGENTS.md for Codex CLI)."
1.3 Determine Mode
Read mode from $ARGUMENTS:
- If empty or not provided, default to
analyze - Valid modes:
analyze,review,list,dialectic - If invalid mode provided, report error and list valid modes
1.4 Load Data
Lazy-create defensive (#185): If .orchestrator/metrics/learnings.jsonl does not exist (pre-#185 repo or bootstrap skipped), create an empty file and emit an info log — do NOT hard-fail:
LEARNINGS_FILE=".orchestrator/metrics/learnings.jsonl"
if [[ ! -f "$LEARNINGS_FILE" ]]; then
mkdir -p "$(dirname "$LEARNINGS_FILE")"
: > "$LEARNINGS_FILE"
echo "info(#185): auto-created $LEARNINGS_FILE (was missing)" >&2
fi
This defensive step is idempotent and cheap — it ensures /evolve analyze|review|list never fails because of a missing artifact file.
- Read
.orchestrator/metrics/sessions.jsonl(session history). If it does not exist, check<state-dir>/metrics/sessions.jsonlas a legacy fallback (where<state-dir>is.claude/,.codex/, or.cursor/per platform). If neither exists, warn: "No session history found. Run at least one session first." - Read
.orchestrator/metrics/learnings.jsonlif it exists. If not found, check<state-dir>/metrics/learnings.jsonlas a legacy fallback. - Count existing learnings, note any where
expires_at< current date (expired)
Phase 2: Mode Dispatch
Route based on mode:
analyze→ Phase 3review→ Phase 4list→ Phase 5dialectic→ Phase 6
Phase 3: Analyze Mode (default)
Extract learnings from session history.
Vault Integration: If
vault-integration.enabledistruein Session Config, confirmed learnings are mirrored to the configured Obsidian vault after the atomic write (Step 3.5, step 9). Seedocs/session-config-reference.mdfor thevault-integrationconfig block.
Step 3.1: Read Session Data
- Read all entries from
.orchestrator/metrics/sessions.jsonl(or<state-dir>/metrics/sessions.jsonlif the v2 path does not exist — see Phase 1.4 fallback) - Parse each JSONL line as JSON
- Sort by
completed_atdescending (most recent first) - If no sessions found, abort: "No session data available. Complete at least one session before running evolve."
Step 3.1b: Read Extra Sources (#638)
When evolve.extra-sources is configured in Session Config (default [] ⇒ this step is a no-op), /evolve consumes OUT-OF-BAND domain measurement sidecars to surface domain-regression learnings.
READ-ONLY contract: /evolve NEVER runs the domain measurement. The measurement (e.g. an eval-learn regression harness) runs elsewhere and writes a sidecar JSON; this step only READS that sidecar's output. Never shell out to produce the sidecar from here.
For each configured extra-sources entry {path, kind, learning-type}:
- Read the sidecar at
path(parser-validated as repo-relative, with absolute paths and..escape segments dropped before this step, then resolved against the repo root). If the file is missing or unreadable, skip with a WARN (evolve: extra-source not found: <path>) — do not abort the whole run. - Schema-gate the sidecar against the
kind's expected shape. Forkind: regression-flagsthe schema is{ flags: [ { metric, baseline, recent, delta } ] }. If the parsed JSON does not match (missingflagsarray, or a flag missing a required field), skip with a WARN (evolve: extra-source <path> failed regression-flags schema gate) — never guess at a different shape. - Emit one
domain-regressionlearning candidate per flag that is PERSISTENT — i.e. the samemetricregressed across ≥2 consecutive sessions (cross-reference prior sessions' sidecar reads or the existing learnings store for the samesubject). A one-off flag is noise; only a persistent regression earns a candidate.type:learning-typefrom the entry (registered enum valuedomain-regression)subject: the flag'smetricinsight: a human-readable regression statement (e.g. "metric<metric>regressed: baseline → recent (delta ) persisting across ≥2 sessions")evidence:baseline → recent(the concrete data points from the sidecar)confidence/expires_at: derived via the existing confidence + decay infrastructure (Step 3.5), exactly as for the built-in learning types.domain-regressioncarries a 60-day TTL (LEARNING_TTL_DAYS).
- Candidates flow into the SAME Step 3.4 AskUserQuestion confirmation + Step 3.5 write path as the built-in learning types — there is no separate write path.
Step 3.2: Pattern Extraction
For each of the 9 built-in analyzer learning types, apply these heuristics:
1. fragile-file (type: fragile-file)
- Look at wave data: if the same file appears in 3+ waves'
files_changedwithin a session, it is fragile - Cross-session: if a file appears in 3+ different sessions'
files_changed, flag it - Subject = file path (relative to project root)
2. effective-sizing (type: effective-sizing)
- Compare
total_agentsandtotal_wavesacross session types - Calculate average agents per wave for each session type
- Subject = canonical identifier like
deep-session-sizingorfeature-session-sizing - Insight = "Deep sessions average X agents across Y waves" or "Feature sessions work well with X agents/wave"
- Over-delivery ratio aggregation (#730/H4, #794.7): compute the MEDIAN of
waves[].over_delivery_ratioacross the last ~5sessions.jsonlrecords of the samesession_type, filtered to waves whoseroleis notDiscovery/Finalizationand which carry the field (skip records lacking the field — pre-#730; also skip Discovery/Finalization waves, whose planned set is empty by design). This exclusion clause is intentionally identical toskills/session-plan/SKILL.mdStep 0.5 "Over-delivery sizing" — keep the two wordings in sync on edit. Fold the median into this candidate'sinsight/evidencefields — e.g.evidence:"median_over_delivery_ratio: 1.4 (n=12 waves, session_type=deep)"— sosession-planStep 0.5 can read the ratio from theeffective-sizinglearning first, falling back to its own directsessions.jsonlscan only when no such learning exists.
3. recurring-issue (type: recurring-issue)
- Look at
agent_summary— iffailedorpartial> 0 across multiple sessions, flag - Check wave
qualityfields — repeated failures indicate recurring issues - Subject = issue pattern identifier (e.g., "test-failures-in-wave-execution", "lint-regressions")
4. scope-guidance (type: scope-guidance)
- Cross-reference
effectiveness.planned_issuesvseffectiveness.completion_rate - Skip sessions that lack the
effectivenessfield (early sessions may not have it) - If completion_rate is consistently 1.0 with N issues, note "N issues per session works well"
- If completion_rate < 0.7, note "scope was too large"
- Subject =
optimal-scope-per-session-type
5. deviation-pattern (type: deviation-pattern)
Ownership Reference: See
skills/_shared/state-ownership.md. evolve has read-only access to STATE.md.
- Read
<state-dir>/STATE.mdif it exists and check## Deviationssection - Cross-reference with session duration vs planned waves
- Subject = pattern name (e.g., "scope-creep-in-feature-sessions", "underestimated-complexity")
6. stagnation-class-frequency (type: stagnation-class-frequency)
- Read
stagnation_eventsfrom the most recent 5 sessions insessions.jsonl(skip sessions lacking the field — they predate #84). - For each
(file, error_class)pair appearing in ≥2 sessions, extract a candidate:- Subject =
<file>:<error_class>(e.g.,skills/wave-executor/wave-loop.md:edit-format-friction) - Insight = "File has stagnation in recent sessions — candidate for pre-edit grounding (#85)."
- Evidence = " sessions with stagnation_events for this file/class"
- Subject =
- These learnings feed #85 (pre-edit grounding injection) when it ships — high-frequency pairs trigger grounding.
7. hardware-pattern (type: hardware-pattern)
v3.1.0 / Sub-Epic #160 (C2, issue #171). Keyed on
host_classrather than project — surfaces hardware-bound problems that affect the user across every repo on the same machine. Complements the project-keyed types above.
- Read
.orchestrator/metrics/events.jsonl(session + wave events) and the registrysweep.logat~/.config/session-orchestrator/sessions/sweep.log. Both are optional — missing files produce no candidates. - Invoke
scripts/lib/hardware-pattern-detector.mjs→detectHardwarePatterns({events, sweepLogEntries, thresholds}). Thresholds come from Session Configresource-thresholdswhen present, falling back toDEFAULT_THRESHOLDS. - Five detection signals (aggregated per
(signal, host_class)pair, ≥2 occurrences required):- oom-kill —
orchestrator.session.stoppedwithexit_code: 137or OOM-marker inerror - heartbeat-gap — registry sweep-log entries with
gap_minutesaboveresource-thresholds.zombie-threshold-min - concurrent-session-pressure — session-start events with
peer_count ≥ concurrent-sessions-warn - disk-full — events whose
errormatchesENOSPC/ "no space left" - thermal-throttle — events whose
resource_snapshot.cpu_load_pctcrossescpu-load-max-pct
- oom-kill —
- Each candidate is piped through
candidateToLearning()→validateLearning(). Defaultscopeisprivate(in-repo only). To promote topublic, the user runsnpm run share:hw-learnings -- --promote(C3 export). This anonymizes eachprivatehardware-pattern entry, validates via the privacy contract, and appends apublictwin tolearnings.jsonl(original preserved). Use--dry-runto preview without writing. - Subject convention:
<signal>::<host_class>(e.g.,oom-kill::macos-arm64-m3pro). The::separator avoids colliding with project-keyed subjects. - Confidence starts at 0.5 like other learning types, but decay is slower in practice: hardware stays the same longer than code. This is an emergent property of the existing expire-after-N-days policy applied to a mostly-stable
host_class— no special-casing needed. - Presentation in step 3.5 (see below): render hardware-patterns in a dedicated section titled
## Hardware Patterns (keyed on host_class)after the project-keyed patterns. This makes the source of the learning obvious to the user at confirmation time.
8. autopilot-effectiveness (type: autopilot-effectiveness)
v3.2 Autopilot / Sub-Epic #271 (issue #298). Compares manual vs. autopilot session outcomes per mode (housekeeping, feature, deep) so the loop can learn whether walk-away runs preserve quality. Complements the project-keyed and hardware-keyed types above.
- Read
.orchestrator/metrics/autopilot.jsonl(one record per autopilot loop run) and.orchestrator/metrics/sessions.jsonl(manual + autopilot session outcomes). Both are optional — missing files produce no candidates. - Invoke
scripts/lib/evolve/autopilot-effectiveness.mjs→analyze(autopilotRuns, sessions). The module pairs records bymodeand compares completion-rate, carryover-rate, kill-switch frequency, and quality-gate pass-rate between the two populations. - Data-gating contract: the analyzer requires ≥20 paired manual+autopilot runs per mode before emitting any candidates. Below that threshold the function returns
[](empty input contract) — evolve simply skips this type for that mode and reports nothing. This prevents premature conclusions from small samples (#297 calibration depends on the same threshold). - Subject convention:
<mode>-manual-vs-autopilot(e.g.,housekeeping-manual-vs-autopilot,feature-manual-vs-autopilot,deep-manual-vs-autopilot). One subject per mode that crosses threshold. - Insight = "Autopilot sessions complete at % vs. manual % (Δ pp across N pairs)" or analogous carryover/kill-switch framing when those signals dominate.
- Confidence starts at 0.5 like other learning types; lifecycle ±0.15 / -0.20 via the existing dedupe-and-update infrastructure in Step 3.3 — no special-casing.
- Each candidate is piped through
candidateToLearning()→validateLearning()exactly like the other types. Defaultscopeisprivate(autopilot RUN data is per-host until the user opts in to share). (refs #298)
9. autonomy-verdict (type: autonomy-verdict)
Dispatcher Autonomy / P3.5 (issue #683). Synthesizes per-repo or per-scope autonomy readiness from autopilot run outcomes plus advisory skill-judge signals. Complements
autopilot-effectiveness: type 8 asks whether autopilot preserves quality by mode; this type asks whether a repo/scope is ready for more dispatcher autonomy.
- Read
.orchestrator/metrics/autopilot.jsonl,.orchestrator/metrics/sessions.jsonl, and.orchestrator/metrics/skill-judgments.jsonl. All are optional — missing files produce no candidates. - Invoke
scripts/lib/evolve/autonomy-verdict.mjs→analyze(autopilotRuns, sessions, skillJudgments, { repo | scope }). The analyzer reuses the type-8 mode rollups and combines them with counted skill-judgeapplied/completedsignals. - Data-gating contract: the analyzer requires ≥1 autopilot run and ≥1 canonical advisory skill-judge judgment (
schema_version: 1,event: "judged",advisory: true) before emitting a candidate. Below that threshold it returns[]so/evolve analyzestays quiet during cold-start. - Subject convention:
<repo-or-scope>-autonomy-readiness(e.g.,session-orchestrator-autonomy-readiness). - Insight frames the readiness verdict (
ready,watch, ornot-ready), the combined score, and the signal counts. Evidence includes the normalized scope, verdict, autopilot summary, and skill-judge summary. - Confidence is derived in the analyzer from signal volume, judge confidence, and score separation, then flows through the existing dedupe-and-update infrastructure in Step 3.3. Default
scopeisprivatebecause autopilot and skill-judge data are host/session-local. (refs #683)
Step 3.2b: Zero Patterns Check
If no patterns were extracted across all built-in analyzers and configured extra sources, report: "No patterns found in session history. This can happen with very few sessions or sessions that lack detailed wave/agent data." and skip to end (do not proceed to AskUserQuestion).
Step 3.3: Deduplicate Against Existing Learnings
For each extracted pattern, check if a learning with same type + subject already exists in learnings.jsonl:
- If exists: propose confidence update (+0.15 if confirmed by new evidence, -0.2 if contradicted)
- If new: propose as new learning with confidence 0.5
Step 3.4: Present Findings via AskUserQuestion
Present extracted patterns to the user for confirmation. Use AskUserQuestion with multiSelect: true:
On Codex CLI where AskUserQuestion is unavailable, present as a numbered Markdown list.
AskUserQuestion({
questions: [{
question: "Which learnings should be saved?\n\nExtracted patterns from session history:",
header: "Evolve — Confirm Learnings",
options: [
{
label: "[type] subject",
description: "insight | evidence: ... | confidence: 0.5 (new) or +0.15 (update)"
},
...
{
label: "Skip all",
description: "Do not save any learnings this time"
}
],
multiSelect: true
}]
})
If user selects "Skip all" or selects nothing, abort gracefully: "No learnings saved."
Step 3.5: Write Confirmed Learnings
For confirmed learnings, use atomic rewrite strategy:
Read ALL existing lines from
.orchestrator/metrics/learnings.jsonl(if exists) into memory. If not found, check<state-dir>/metrics/learnings.jsonlas a legacy fallback. If legacy data is found, it will be migrated to the v2 path on write (step 8).Apply confidence updates for confirmed existing learnings:
- Increment confidence by +0.15
- Cap at 1.0
- Reset
expires_atusingderiveExpiresAt(now, type)unless the candidate supplies a more specific expiry
Apply confidence decrements for contradicted learnings (-0.2) — do NOT reset
expires_atfor contradicted learnings (let them decay naturally)Append new learnings with the canonical schema_version:1 shape — every field is required (#303):
schema_version: 1 (integer, ALWAYS — never omit)id: UUID v4 string generated vianode -e "const {randomUUID}=require('crypto');process.stdout.write(randomUUID())"oruuidgen | tr '[:upper:]' '[:lower:]'. MUST be a non-empty UUID string. Never omit — missingidcauses 100% mirror-skip (#303).type: one offragile-file,effective-sizing,recurring-issue,scope-guidance,deviation-pattern,stagnation-class-frequency,hardware-pattern,autopilot-effectiveness,autonomy-verdict,domain-regression(#638 — only when sourced fromevolve.extra-sources, see Step 3.1b)subject: the pattern subjectinsight: human-readable description of the pattern. MUST beinsight— do NOT usedescriptionorrecommendation(legacy alias keys that vault-mirror cannot read; see #303).evidence: specific data points that support the patternconfidence: use the candidate's derived confidence when supplied (e.g.,autonomy-verdict); otherwise 0.5 for new learningssource_session: non-empty kebab-slug string identifying the session from which the pattern was extracted (e.g.main-2026-04-27-1942). MUST be a string — never an object, array, number, or null. If multiple sessions contributed, use the earliest. If unknown, use"unknown"(the string). Never passString(<object>)— that yields"[object Object]"and breaks the YAML mirror downstream (#307). Optional pre-write validation:jq -e 'select(.source_session | type == "string" and length > 2)'.created_at: current ISO 8601 dateexpires_at: preserve the candidate's derived expiry when supplied; otherwise derive fromLEARNING_TTL_DAYS[type]viaderiveExpiresAt()(falling back to the schema default) rather than hard-coding a 30-day horizon
Verify write: Read back the first line of the written file to confirm valid JSON. If read-back fails or is not valid JSON, report error to user.
Prune: remove entries where
expires_at< current date ORconfidence<= 0.0Consolidate duplicates (NULL-SUBJECT SAFE): if same
type+subjectappears more than once ANDsubjectis a non-empty string, keep the entry with highest confidence. Entries with null/empty/missingsubjectare NEVER collapsed — each is keyed by its uniqueidand always preserved. (Fix for issue #284: empty-subject dedupe collapse.)Write entire result back to
.orchestrator/metrics/learnings.jsonlwith>(atomic rewrite, NOT append>>)Vault mirror (conditional): Check
$CONFIG."vault-integration".enabledvia jq. If the field is missing orfalse, skip this step entirely — skill behavior is unchanged.If
enabledistrue:a. Check
$CONFIG."vault-integration".mode. Ifmodeisoff, skip the mirror invocation (treat as disabled). Ifmodeis absent, default towarn.b. Resolve the vault directory: use
$CONFIG."vault-integration"."vault-dir"if non-null, otherwise fall back to the$VAULT_DIRenvironment variable. If neither is set, emit a warning and skip.c. Invoke the mirror script. Derive a synthetic
EVOLVE_SESSION_IDso the vault-mirror auto-commit phase (#31) produces a traceable commit subject (chore(vault): mirror evolve-<date> — N learnings + 0 sessions). Pass--vault-namewhenvault-integration.vault-nameis set in Session Config:EVOLVE_SESSION_ID="evolve-$(date -u +%Y-%m-%d-%H%M)" EVOLVE_VAULT_NAME=$(echo "$CONFIG" | jq -r '."vault-integration"."vault-name" // empty') node "$PLUGIN_ROOT/scripts/vault-mirror.mjs" \ --vault-dir "<vault-dir>" \ --source .orchestrator/metrics/learnings.jsonl \ --kind learning \ --session-id "$EVOLVE_SESSION_ID" \ ${EVOLVE_VAULT_NAME:+--vault-name "$EVOLVE_VAULT_NAME"}d. Handle the exit code according to
mode:warn(default): on non-zero exit, surface a warning in evolve output (e.g. "Warning: vault mirror failed — learnings saved locally but not mirrored.") but do NOT fail the skill.strict: on non-zero exit, fail the skill immediately and report the error to the user.
e. On success (exit 0), report: "Mirrored N learnings to
<vault-dir>/40-learnings/."
Report: "Saved N new learnings, updated M existing. Total active: K."
Step 3.6: C2 Auto-Repair Feeder (opt-in — #647)
Default OFF (advisory-only). With no
skill-evolution:block in Session Config, this step surfaces repair candidates as ADVICE only — it applies nothing and opens no MR. This mirrors the opt-in precedent ofslopcheck(#520) andverification-auto-fix(#521): the engine is dark unless explicitly enabled.
After confirmed learnings are written (Step 3.5), the actionable subset can feed the C2 tiered auto-repair engine (Epic #643 / issue #647). This is a pointer section — the modules own the logic; do not duplicate it here.
skill-evolution: is a DISTINCT sibling of the pre-existing evolve: block. evolve: (extra-sources) tunes learning EXTRACTION (Step 3.1b); skill-evolution: tunes repair AUTONOMY. They are parsed by different modules and never share keys — do not conflate them. The skill-evolution: block is parsed by scripts/lib/config/skill-evolution.mjs (_parseSkillEvolution) and surfaced at $CONFIG['skill-evolution'] (wired in scripts/lib/config.mjs). Shape: { autonomy: 'off'|'advisory'|'autonomous-gated', 'evidence-floor': number, judge: boolean }, default autonomy: 'off'. Do NOT add skill-evolution: as a column-0 key to any consolidated Session Config parity block — it is a standalone top-level block (claude-md-drift-check Check-6 enforces parity only on the ## Session Config keys).
Candidate intake. Pass the post-Step-3.5 learnings (and, when available, the claude-md-drift-check result) to extractCandidates({ learnings, driftResult, evidenceFloor: $CONFIG['skill-evolution']['evidence-floor'], now }) from scripts/lib/skill-evolution/candidate-intake.mjs. It is a pure transform — only actionable, non-expired learnings whose confidence ≥ evidence-floor AND whose insight is prescriptive AND resolves to a repo-relative path become RepairCandidates.
Gate per artifact type. Each candidate's target_path is classified by classifyTarget(target_path, { repoRoot }) from scripts/lib/skill-evolution/blast-radius-classifier.mjs (the heart of the design; path-traversal-safe, fail-closed):
| Target type | Gate | Posture |
|---|---|---|
plugin-skill (skills/…) |
none | always-mr — never autonomous |
local-skill (.claude/skills/…) |
none | always-mr — never autonomous |
local-config (ROOT CLAUDE.md / AGENTS.md Session Config) |
config-validation | autonomous-gated |
| anything else | none | always-mr (fail-closed) |
Only ROOT-instruction Session Config edits are eligible for autonomous apply, and only when ALL of: runConfigValidationGate({ repoRoot }) (scripts/lib/skill-evolution/config-validation-gate.mjs) is GREEN (parse-config + config-schema + claude-md-drift-check) AND evidence ≥ evidence-floor AND autonomy: autonomous-gated. Skill repairs are MR-only by construction.
Invocation contract (this foundation slice = ADVISORY surfacing). The single orchestrator that ties intake → classify → gate → route → stamp together is runRepairEngine({ repoRoot, config, learnings, driftResult, dryRun }) from scripts/lib/skill-evolution/engine.mjs — it returns { outcomes, summary } and applies the full gate-per-artifact-type decision matrix internally (autonomy: off ⇒ every outcome is advisory-only). In the default/advisory posture, /evolve SURFACES candidates and their classification only — it does not apply or open MRs. Apply is gated on the config-validation gate above; MR-opening (openRepairMr({ candidate, diff, repoRoot, dryRun }) from scripts/lib/skill-evolution/mr-opener.mjs) is gated on autonomy != off. Candidate de-dup / processed_at lifecycle is owned by scripts/lib/skill-evolution/idempotency.mjs. When autonomy: off (default), report the surfaced candidates as advice and stop.
Phase 4: Review Mode
Interactive management of existing learnings.
Step 4.1: Load Learnings
- Read
.orchestrator/metrics/learnings.jsonl. If not found, check<state-dir>/metrics/learnings.jsonlas a legacy fallback. - If neither exists or both are empty: "No learnings found. Run
/evolve analyzefirst." - Parse each line as JSON
Step 4.2: Display Learnings
Present a formatted table grouped by type. Include the Effective column — the recency-decayed surfacing score (#670) — so stale high-confidence entries are visible as decay candidates next to their static confidence:
## Active Learnings
| # | Type | Subject | Confidence | Effective | Expires | Insight |
|---|------|---------|------------|-----------|---------|---------|
| 1 | fragile-file | src/lib/auth.ts | 0.80 | 0.78 | 2026-07-05 | Changed in 4 of last 5 sessions |
| 2 | effective-sizing | feature-session-sizing | 0.65 | 0.61 | 2026-06-20 | Feature sessions work well with 3 agents/wave |
| ... | ... | ... | ... | ... | ... | ... |
Summary: N active learnings (M high confidence, K expiring soon)
Effective (decayed) score — #670. Retrieval/surfacing ranks by an
effectiveScore = max(confidence × 0.5^(ageDays / halfLifeDays), confidence × floorFactor)blend, NOT raw confidence.ageDaysderives fromlast_reinforced/last_accessed/updated_atwhen present, elsecreated_at. So a stale high-confidence learning ranks below a fresh mid-confidence one, while thefloorFactor(default 0.1) guarantees a durable learning never collapses to ~0. Tuned under the existingevolve:Session Config block (decay-enabled: true,decay-half-life-days: 90,decay-floor-factor: 0.1— all conservative defaults; setdecay-enabled: falseto restore pure-confidence ordering). Implemented inscripts/lib/learnings/surface.mjs(effectiveScore+surfaceTopN). The confidence FILTER (> 0.3) is unchanged — decay re-ranks survivors, it does not change eligibility.
Step 4.3: Interactive Management
Use AskUserQuestion with options:
On Codex CLI where AskUserQuestion is unavailable, present as a numbered Markdown list.
AskUserQuestion({
questions: [{
question: "What would you like to do with your learnings?",
header: "Evolve — Review",
options: [
{ label: "Boost confidence", description: "Select learnings to boost (+0.15)" },
{ label: "Reduce confidence", description: "Select learnings to reduce (-0.2)" },
{ label: "Delete specific learnings", description: "Select learnings to remove" },
{ label: "Extend expiry", description: "Reset expires_at by learning-expiry-days from now" },
{ label: "Done — no changes", description: "Exit without changes" }
]
}]
})
If user selects "Boost confidence", "Reduce confidence", "Delete specific learnings", or "Extend expiry", present a follow-up AskUserQuestion with multiSelect: true listing all learnings by # | type | subject so the user can select which ones to modify.
On Codex CLI where AskUserQuestion is unavailable, present as a numbered Markdown list.
Step 4.4: Apply Changes
Use the same atomic rewrite strategy as Phase 3, Step 3.5:
- Read all lines from
learnings.jsonl - Apply the selected operation to selected learnings:
- Boost: +0.15 confidence (cap 1.0), reset expires_at to +
learning-expiry-days - Reduce: -0.2 confidence
- Delete: remove selected entries
- Extend: reset expires_at to current date +
learning-expiry-days
- Boost: +0.15 confidence (cap 1.0), reset expires_at to +
- Prune entries where
expires_at< current date ORconfidence<= 0.0 - Consolidate duplicates (same
type+ non-emptysubject): keep highest confidence. Null-subject entries are preserved individually (keyed byid). See SKILL.md #284 fix note. - Write entire result back with
>(atomic rewrite)
Report: "Updated N learnings. Total active: K."
Phase 5: List Mode
Simple read-only display.
Step 5.1: Load and Display
- Read
.orchestrator/metrics/learnings.jsonl. If not found, check<state-dir>/metrics/learnings.jsonlas a legacy fallback. - If neither exists: "No learnings yet. Run
/evolve analyzeto extract patterns from session history." - Parse each line as JSON
Step 5.2: Formatted Output
Display a formatted table grouped by type:
## Active Learnings
### fragile-file
| Subject | Confidence | Expires | Insight |
|---------|------------|---------|---------|
| ... | ... | ... | ... |
### effective-sizing
| Subject | Confidence | Expires | Insight |
|---------|------------|---------|---------|
| ... | ... | ... | ... |
(repeat for each type that has entries)
Step 5.3: Summary
Display summary line:
N active learnings (M high confidence, K expiring soon)
- High confidence = confidence > 0.7
- Expiring soon = expires_at within 14 days of current date
Phase 6: Dialectic Mode
Single-pass LLM derivation of USER.md + AGENT.md (peer cards from #503) updates from current learnings + sessions + steering files. Dry-run-default per #506 EARS contract.
Step 6.0: Argument Parsing
Parse $ARGUMENTS for trailing flags after the dialectic keyword:
| Flag | Default | Behavior |
|---|---|---|
--apply |
false |
Write diff to USER.md/AGENT.md via merger.mjs; without it = dry-run |
--dry-run |
true |
Explicit dry-run (default); mutually exclusive with --apply |
--model <name> |
from Session Config dialectic.model (default haiku) |
Override LLM |
--budget-tokens <N> |
from Session Config dialectic.budget-tokens (default 8000) |
Token budget |
Mutex check: --apply + --dry-run together = error "flags mutually exclusive".
Step 6.1: Pre-checks
- Bootstrap gate (Phase 0) — already executed
- Persistence check (Phase 1.2) — already executed
- Cadence check: if invoked via session-end Phase 3.6.7 auto-trigger, the trigger has already pre-checked cadence. For manual invocation, skip cadence — manual always runs.
Step 6.2: Data Load
Read all 4 input sources via runDialecticDeriver() from scripts/dialectic-deriver.mjs (see W2 I1):
- Top-N learnings from
.orchestrator/metrics/learnings.jsonl(default 50, sorted by confidence DESC) - Last-K sessions from
.orchestrator/metrics/sessions.jsonl(default 10, sorted by completed_at DESC) - Peer cards via
readPeerCards(repoRoot)fromscripts/lib/peer-cards/reader.mjs— returns{user, agent}or null - Project steering files (CLAUDE.md / AGENTS.md Session Config block + narratives)
Graceful degradation: any null/empty source is acceptable. If ALL inputs empty → return {status: 'empty-input'}.
Step 6.3: Dispatch the Deriver Agent
Construct a dispatchAgent function that uses the harness Agent tool to invoke the dialectic-deriver agent (see agents/dialectic-deriver.md):
const dispatchAgent = async ({ model, prompt, maxTokens }) => {
// Coordinator uses Agent tool with subagent_type: "session-orchestrator:dialectic-deriver"
// and the model parameter to invoke the right tier
const result = await Agent({
description: "Dialectic-deriver LLM pass",
subagent_type: "session-orchestrator:dialectic-deriver",
model,
prompt,
});
return { text: result.text, usage: result.usage ?? { input_tokens: 0, output_tokens: 0 } };
};
> **Why `maxTokens` is not passed to Agent():** the Claude Code harness `Agent()` tool does not currently accept a `max_tokens` parameter. Output-token budget is therefore enforced via prompt text (see line 414 in `skills/session-end/SKILL.md`: "with budget ${budget-tokens} input + 4000 output tokens"). The dispatchAgent contract declares `maxTokens` as the canonical interface; the evolve skill destructures it for forward-compat but routes enforcement through the prompt body. When the harness adds a max_tokens hint, this dispatchAgent becomes the single update point.
const result = await runDialecticDeriver({
dispatchAgent,
repoRoot: process.cwd(),
model: argv.model ?? config.dialectic?.model ?? 'haiku',
budget: { input: argv['budget-tokens'] ?? config.dialectic?.['budget-tokens'] ?? 8000, output: 4000 },
dryRun: !argv.apply,
allowEmptying: argv['allow-emptying'] ?? false,
});
Step 6.4: Diff Output & Apply Gate
- If dry-run (default): present diff inline; write to
.orchestrator/dialectic-pending.md(atomic tmp+rename); EXIT. Suggestion: "Re-run with/evolve --dialectic --applyto apply." - If
--apply: callmergePeerCard(existingBody, managedUpdates)fromscripts/lib/peer-cards/merger.mjsfor each card target, thenwritePeerCard(repoRoot, 'user', mergedUserCard)andwritePeerCard(repoRoot, 'agent', mergedAgentCard)fromscripts/lib/peer-cards/writer.mjs. Update theupdated:frontmatter. - Report:
Dialectic-derived: M deltas to USER.md, N deltas to AGENT.md. Dry-run | Applied. Tokens: in=<X> out=<Y>.
Step 6.5: Error Handling
status: 'unknown-model'→ fail with clear error (already thrown by validateModel)status: 'budget-exceeded'→ emit{status:'budget-exceeded', used:N, budget:M}, do NOT truncatestatus: 'would-empty-card'→ warn + require--allow-emptyingflagstatus: 'empty-input'→ exit clean with message "dialectic: skipped (no input)"- subagent crash → log ⚠, exit cleanly (do NOT write to
.orchestrator/dialectic-pending.md)
Cross-reference: PRD #506 AC1-AC4 + EARS gates. Vault Integration: dialectic does NOT mirror to vault (#506 scope — peer cards are repo-local by design; vault mirror is for cross-repo sessions/learnings).
Critical Rules
- NEVER modify
learnings.jsonlwithout reading it first — race condition prevention - NEVER skip the deduplication check — duplicates degrade the intelligence system
- NEVER write learnings without user confirmation — always present via AskUserQuestion first (on Codex CLI where AskUserQuestion is unavailable, present as a numbered Markdown list)
- ALWAYS use uuid-v4 for new learning IDs (generate via
uuidgenor equivalent bash command) - ALWAYS preserve a candidate-supplied
expires_at; otherwise derive it fromLEARNING_TTL_DAYS[type]viaderiveExpiresAt()rather than hard-codinglearning-expiry-days - ALWAYS present findings to user before writing — no silent writes
- ALWAYS use atomic rewrite (read all, modify, write all with
>) — never append with>> - ALWAYS cap confidence at 1.0 — never exceed
Anti-Patterns
- DO NOT write learnings without user confirmation — always present via AskUserQuestion first (on Codex CLI where AskUserQuestion is unavailable, present as a numbered Markdown list)
- DO NOT append to
learnings.jsonl— always use atomic rewrite (read all, modify, write all) - DO NOT create duplicate learnings — always check type + subject match first
- DO NOT set confidence above 1.0 or forget to cap it
- DO NOT fabricate patterns — only extract from actual session data with verifiable evidence
- DO NOT skip the pruning step — expired and zero-confidence entries must be removed on every write
Source: hashgraph-online/awesome-codex-plugins → plugins/Kanevry/session-orchestrator/skills/evolve/SKILL.md