OverviewFilesCommitsIssuesDocsReleasesPeers

knowledge-ratchet / atomspace / strategic-rules.js

160 lines · javascript ref:
/**
 * 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 }