/**
* strategic-rules.js — Situational rules that fire based on session STATE
*
* Unlike file-level rules, these trigger on behavioral patterns:
* - stuck-loop: 5+ turns without approval
* - high-tool-rate: many tools, little progress
* - reset-and-re-read: multiple corrections in a row
* - unfamiliar-territory: editing a module with few prior sessions
* - context-saturation: long session getting tired
* - repeating-failed-approach: similar actions after recent correction
*
* Consumer: attention-router loads these and injects relevant ones into prompts.
*/
const fs = require('fs')
const STATE_FILE = '/tmp/claude-session-score.json'
/**
* Read live session state.
*/
function readSessionState () {
try { return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')) }
catch { return null }
}
/**
* All strategic rules. Each has:
* id: stable identifier
* condition(state): returns true if rule should fire
* message: one-line guidance
* severity: low / med / high (affects ranking in token budget)
*/
const STRATEGIC_RULES = [
{
id: 'stuck-loop',
condition: (state) => {
if (!state) return false
// Use PROBLEM-turns (resets on approval/proceed/new-problem/command).
const problemTurns = state._problemTurns != null
? state._problemTurns
: ((state.turns || 0) - (state._lastApprovalTurn || 0))
// Must be stuck AND have had a RECENT correction (not session-total).
// _recentCorrections is maintained as last 5 correction texts.
const recentCorrections = (state._recentCorrections || []).length
return problemTurns >= 5 && recentCorrections >= 1
},
message: '5+ turns on this problem with a recent correction. STOP and re-read the plan. Something is wrong.',
severity: 'high'
},
{
id: 'reset-and-re-read',
condition: (state) => {
if (!state) return false
// 2+ corrections in last 3 turns
const recent = (state._recentCorrections || []).slice(-3)
return recent.length >= 2
},
message: 'Multiple corrections in a row. Stop. Re-read the user prompt and the file completely before acting.',
severity: 'high'
},
{
id: 'high-tool-rate',
condition: (state) => {
if (!state) return false
const ratio = state.turns > 0 ? (state.toolCalls || 0) / state.turns : 0
return ratio > 15 // 15+ tools per turn = fumbling
},
message: 'High tool-call rate per turn. You may be fumbling — pause and plan the next step.',
severity: 'med'
},
{
id: 'unresolved-loop-active',
condition: (state) => {
return state && state._loopActive === true
},
message: 'Active correction loop detected. The usual-approach is failing. Try a DIFFERENT angle.',
severity: 'high'
},
{
id: 'context-saturation',
condition: (state) => {
if (!state) return false
// Fire only when session is long AND has few completions relative to length.
// Use approvals count (always present) as the completion signal.
// Many approvals in a long session = successful long work, not saturation.
const turns = state.turns || 0
const approvals = state.approvals || 0
if (turns < 80) return false
// Expected completion rate: at least 1 approval per 20 turns
return approvals < Math.floor(turns / 20)
},
message: 'Long session with few completions relative to length. Context may be saturated. Consider wrapping up.',
severity: 'med'
},
{
id: 'declining-trajectory',
condition: (state) => {
return state && state.trajectory === 'declining'
},
message: 'Session trajectory is declining. Last turns worse than earlier. Regroup.',
severity: 'med'
},
{
id: 'low-approval-rate',
condition: (state) => {
if (!state || state.turns < 10) return false
const approvalRate = state.turns > 0 ? (state.approvals || 0) / state.turns : 0
const correctionRate = state.turns > 0 ? (state.corrections || 0) / state.turns : 0
return correctionRate > approvalRate * 2 && (state.corrections || 0) >= 3
},
message: 'Corrections significantly outpacing approvals. The direction isn\'t landing. Re-examine assumptions.',
severity: 'med'
}
]
/**
* Evaluate all rules against current session state.
* Returns array of { id, message, severity } for rules that fire.
*/
function evaluateRules (state) {
if (!state) return []
const firing = []
for (const rule of STRATEGIC_RULES) {
try {
if (rule.condition(state)) {
firing.push({ id: rule.id, message: rule.message, severity: rule.severity })
}
} catch {}
}
// Sort by severity: high first
const sevOrder = { high: 0, med: 1, low: 2 }
firing.sort((a, b) => (sevOrder[a.severity] ?? 3) - (sevOrder[b.severity] ?? 3))
return firing
}
/**
* Check which strategic rules fire right now based on live session state.
*/
function evaluateNow () {
const state = readSessionState()
return evaluateRules(state)
}
// CLI — check live session and print firing rules
if (require.main === module) {
const firing = evaluateNow()
if (firing.length === 0) {
console.log('No strategic rules firing. Session is on track.')
} else {
console.log('Strategic rules firing:')
for (const r of firing) {
const marker = r.severity === 'high' ? '‼ ' : r.severity === 'med' ? '⚠ ' : '· '
console.log(`${marker}[${r.id}] ${r.message}`)
}
}
}
module.exports = { STRATEGIC_RULES, evaluateRules, evaluateNow }