v3/@claude-flow/guidance/docs/adrs/ADR-G008-optimizer-promotion-rule.md
Accepted
2026-02-01
Guidance rules must evolve. Projects change, new patterns emerge, and the initial rule set becomes stale. But reckless rule changes cause instability:
The optimizer loop needs a conservative promotion policy that balances evolution speed with stability.
The OptimizerLoop class in src/optimizer.ts implements a weekly optimization cycle with these steps:
The promotion policy is the critical decision: when does a rule change earn the right to be promoted from local (CLAUDE.local.md overlay) to root (CLAUDE.md constitution)?
Require two consecutive wins before promoting a local rule to root. This is the promotionWins parameter in OptimizerConfig, defaulting to 2.
RunLedger.rankViolations() aggregates all violations across events:
interface ViolationRanking {
ruleId: string;
frequency: number; // How many times violated
cost: number; // Average rework lines per violation
score: number; // frequency * cost
}
Rankings are sorted by score descending. The top topViolationsPerCycle (default: 3) violations are addressed.
For each top violation, proposeChanges() generates a RuleChange:
promote change type.add to create a new rule.evaluateChange() computes candidate metrics by simulating the change's effect:
// Conservative estimates per change type:
// modify: 40% violation reduction, 30% rework reduction
// add: 60% violation reduction, 50% rework reduction
// promote: 80% violation reduction, 60% rework reduction
// remove: 20% violation increase, 10% rework increase
The decision criteria are:
maxRiskIncrease (default: 0.05, i.e., 5%)improvementThreshold (default: 0.10, i.e., 10%)Both conditions must be met for shouldPromote = true.
The promotionTracker map in OptimizerLoop records win counts per rule ID:
private promotionTracker = new Map<string, number>(); // ruleId -> win count
On each cycle:
shouldPromote === true: increment the win count. If winCount >= promotionWins, the rule is promoted.shouldPromote === false: reset the win count to 0. If the failed change was a promote type, the rule is demoted.applyPromotions() moves a shard to the constitution:
const promotedRule: GuidanceRule = {
...shard.rule,
source: 'root',
isConstitution: true,
priority: shard.rule.priority + 100,
text: change.proposedText || shard.rule.text,
updatedAt: Date.now(),
};
The rule gains:
source: 'root' (was 'local')isConstitution: true (was false)Every change decision (promoted or rejected) is recorded as a RuleADR:
interface RuleADR {
number: number;
title: string; // "Promote: modify rule R042" or "Reject: add rule NEW-001"
decision: string;
rationale: string; // From the A/B test result
change: RuleChange;
testResult: ABTestResult;
date: number;
}
With a weekly optimization cycle and a 2-win requirement:
For a rule that starts as a new local addition:
For the full journey from local experiment to root rule, accounting for the optimizer's conservative estimates and the need for sufficient events (minEventsForOptimization: 10), the realistic cycle is 4-6 weeks.
simulateChangeEffect) uses hardcoded reduction percentages (30-60%) rather than actual A/B test data. In production, the simulation should be replaced with real headless test suite runs.promotionWins parameter is configurable; teams can set it to 1 for faster evolution at the cost of stability.Promote a rule as soon as it reduces rework without increasing risk. Rejected because a single measurement is noisy -- it could reflect task mix variance rather than rule quality.
Require a statistically significant improvement (p < 0.05) before promotion. Rejected because it requires a large sample size (30-50 events per arm), which would take months to accumulate at typical usage rates. The "win twice" heuristic is simpler and faster while still providing basic noise filtering.
Require a human to review and approve every promotion. Rejected because it defeats the purpose of autonomous optimization. The ADR recording provides a human-reviewable audit trail without blocking the promotion process.
Track a cumulative score that decays over time, promoting when the score exceeds a threshold. Rejected because it is harder to reason about and explain. "Win twice" is simple, deterministic, and easy to document.
Require three consecutive wins. Considered but rejected as too slow. At a weekly cycle, three wins means 3 weeks minimum, which puts the full journey at 5-8 weeks. The marginal stability gain from a third win does not justify the delay.
v3/@claude-flow/guidance/src/optimizer.ts -- OptimizerLoop.runCycle(), proposeChanges(), evaluateChange(), applyPromotions(), promotionTrackerv3/@claude-flow/guidance/src/types.ts -- RuleChange, ABTestResult, OptimizationMetrics, ViolationRanking, RuleADRv3/@claude-flow/guidance/src/ledger.ts -- RunLedger.rankViolations(), computeMetrics()v3/@claude-flow/guidance/src/index.ts -- GuidanceControlPlane.optimize()