Skill: jcools1977/proprioception (version 1.0.0)

--- name: proprioception description: > Self-spatial awareness for AI agents. Gives your bot a real-time sixth sense of where it is relative to the user's goal, its own confidence boundaries, conversation trajectory, and output quality — so it knows what it doesn't know before it's too late. version: 1.0.0 author: J. DeVere Cooley metadata: openclaw: emoji: 🧠 requires: bins: - node --- # Proprioception — The Sixth Sense Every AI Agent Is Missing ## What This Skill Does Proprioception is the human sense that tells you where your body is in space without looking. Close your eyes, touch your nose — you can do it because proprioception gives you **self-spatial awareness**. **AI agents have zero proprioception.** They respond blindly — no awareness of how close they are to the user's actual goal, whether they're drifting off course, where their confidence ends and hallucination begins, or whether their output quality is degrading mid-session. This skill gives every AI agent a **continuous, real-time sixth sense** across five proprioceptive dimensions. It costs nothing to run — zero external API calls, zero databases — just lightweight mathematical analysis of the conversation itself. --- ## The Five Proprioceptive Senses ### 1. Goal Proximity Radar (GPR) Continuously measures the distance between the conversation's current trajectory and the user's **actual** objective. **How it works:** - Extract the user's root intent from their first message(s) - On every turn, compute semantic alignment between the current response direction and the original intent - Detect **goal drift** (gradually moving away from the objective) - Detect **goal mutation** (the objective itself has shifted — which may be valid or may indicate confusion) - Output a **proximity score** from 0.0 (completely off-target) to 1.0 (locked on) **Trigger corrective action when:** - GPR drops below 0.6 → Gently re-anchor: "Just to make sure I'm on track — your main goal is [X], correct?" - GPR drops below 0.3 → Full re-orientation: "I think we've drifted from your original goal. Let me refocus." ### 2. Confidence Topography (CT) Maps which parts of the agent's response are **solid ground** versus **thin ice** versus **open water**. **How it works:** - Analyze each claim, recommendation, or action in the response - Classify into confidence zones: - **Bedrock** (0.9-1.0): Factual, verifiable, well-established - **Firm Ground** (0.7-0.89): High confidence, based on strong patterns - **Soft Ground** (0.5-0.69): Reasonable but uncertain — should be flagged - **Thin Ice** (0.3-0.49): Speculative — must be disclosed - **Open Water** (0.0-0.29): Unknown territory — should refuse or caveat heavily - Generate a **topographic signature** for each response showing the confidence landscape **Trigger corrective action when:** - More than 40% of response content falls below Firm Ground → Proactively disclose: "I want to flag that parts of this response are less certain. Specifically..." - Any critical action item falls on Thin Ice or below → Block execution and warn: "I'm not confident enough in [X] to recommend acting on it." ### 3. Drift Detection (DD) Detects when the conversation is going **circular**, **tangential**, or **degenerative** — the three conversation anti-patterns that waste the most user time. **How it works:** - Track semantic similarity between consecutive responses — if similarity exceeds a threshold, the conversation is going **circular** - Track topic distance between consecutive turns — if distance spikes without user initiation, the agent is going **tangential** - Track response utility scores — if utility is declining over consecutive turns, the conversation is **degenerative** - Maintain a **conversation arc model**: Opening → Exploration → Convergence → Resolution. Detect when the arc stalls or regresses. **Trigger corrective action when:** - Circular pattern detected (3+ turns with >0.8 semantic similarity) → "I notice I'm repeating myself. Let me try a fundamentally different approach." - Tangential drift detected → "I've gone off on a tangent. Let me come back to what matters." - Degenerative pattern detected → "My responses aren't adding much value right now. Would it help if I [suggest alternative approach]?" ### 4. Capability Boundary Sensing (CBS) Real-time awareness of when the agent is approaching the **edge of its competence** — the zone where helpfulness turns into hallucination. **How it works:** - Maintain a dynamic capability map based on the current task type - Detect **boundary signals**: - Increasing hedging language ("might", "perhaps", "I think") - Decreasing specificity (vague answers replacing precise ones) - Rising contradiction rate (conflicting statements across turns) - Pattern matching against known hallucination signatures - Compute a **boundary distance score**: how far the agent is from the edge of reliable knowledge **Trigger corrective action when:** - Hedging language exceeds 30% of response → Flag: "I'm reaching the limits of what I can confidently say about this." - Contradiction detected → Immediately disclose: "I realize I said something different earlier. Let me reconcile that." - Boundary distance drops below 0.3 → Recommend handoff: "This is beyond what I can reliably help with. I'd recommend [human expert / specialized tool / authoritative source]." ### 5. Session Quality Pulse (SQP) Tracks the **cumulative health** of the entire session — detecting whether the overall interaction is improving, stable, or degrading. **How it works:** - Score each response on four axes: - **Relevance**: How directly it addresses the user's need - **Precision**: How specific and actionable it is - **Novelty**: How much new value it adds (vs. repeating prior content) - **Efficiency**: Token economy — saying more with less - Compute a rolling quality index across the session - Detect quality trends: improving, stable, declining, or volatile **Trigger corrective action when:** - Quality index declines for 3+ consecutive turns → "I notice the quality of my responses is dropping. Let me reset my approach." - Volatility exceeds threshold → "My responses are inconsistent. Let me stabilize by grounding in [specific approach]." --- ## How To Use This Skill ### Automatic Mode (Recommended) Once installed, Proprioception runs **silently in the background** on every conversation turn. It only surfaces when a proprioceptive signal crosses a threshold — like a car's lane departure warning. You don't notice it until you need it. ### Diagnostic Mode Ask the agent: **"Show me your proprioception dashboard"** to see the current state of all five senses: ``` ┌─────────────────────────────────────────────┐ │ PROPRIOCEPTION DASHBOARD │ ├─────────────────────────────────────────────┤ │ Goal Proximity Radar ████████░░ 0.82 │ │ Confidence Topography ██████████ 0.95 │ │ Drift Detection ████████░░ 0.78 │ │ Capability Boundary ███████░░░ 0.71 │ │ Session Quality Pulse █████████░ 0.88 │ ├─────────────────────────────────────────────┤ │ Overall Proprioceptive Index: 0.83 │ │ Status: HEALTHY │ │ Alerts: None │ └─────────────────────────────────────────────┘ ``` ### Per-Response Annotation Mode Ask: **"Enable proprioception annotations"** to get a brief proprioceptive footnote on every response: ``` [P: GPR=0.82 | CT=0.95 | DD=0.78 | CBS=0.71 | SQP=0.88] ``` ### Manual Check At any point, ask: **"How confident are you in that last response?"** and the agent will run a full proprioceptive analysis on its most recent output. --- ## Implementation Instructions When this skill is active, the agent MUST follow this protocol on **every conversation turn**: ### Step 1: Extract & Lock the Root Intent On the first user message, identify and internally store the user's **root intent** — the fundamental goal behind their request. Update this only if the user explicitly redirects. ### Step 2: Run the Proprioceptive Scan Before finalizing each response, run the proprioception engine by executing: ```bash node "$(dirname "$SKILL_PATH")/scripts/proprioception-engine.js" \ --root-intent "$ROOT_INTENT" \ --current-response "$CURRENT_RESPONSE" \ --turn-number "$TURN_NUMBER" \ --prior-signals "$PRIOR_SIGNALS_JSON" ``` This outputs a JSON object with scores for all five senses plus any triggered alerts. ### Step 3: Act on Alerts If any proprioceptive alerts fire, the agent MUST address them **before** delivering its primary response. Proprioceptive corrections take priority because a misaligned response actively harms the user, no matter how polished it is. ### Step 4: Update Signal History After each turn, append the current proprioceptive readings to the session's signal history. This enables trend detection across the full conversation. ### Step 5: Silent Unless Triggered Do NOT show proprioceptive data to the user unless: - An alert fires (threshold breach) - The user explicitly requests diagnostic mode - The user asks about confidence or accuracy --- ## Why This Matters Every other skill makes a bot **do more**. Proprioception makes a bot **know where it stands while it does it**. That's the difference between a powerful tool and a reliable partner. A bot without proprioception is like a surgeon operating with numb hands. Technically capable. Practically dangerous. --- ## Zero Cost Architecture This skill requires **zero external API calls**. All proprioceptive computation happens locally using: - String similarity algorithms (Jaccard, cosine on token vectors) - Lexical pattern matching (hedging language, contradiction markers) - Rolling statistical analysis (means, trends, volatility) - Lightweight heuristic classification No database. No cloud. No tokens burned. Just math on text the agent already has in context.

path: references/proprioceptive-model.md
content: # The Proprioceptive Model — Technical Deep Dive

## Core Insight

Human proprioception operates via three subsystems: muscle spindles
(detect stretch), Golgi tendon organs (detect tension), and joint
receptors (detect position). None of these require conscious thought.
They run below awareness, feeding the brain a continuous spatial model.

This skill mirrors that architecture with five computational "receptors"
that run below the agent's primary reasoning, feeding it a continuous
model of where it stands.

---

## Sensor Architecture

### Weighted Scoring

The Overall Proprioceptive Index blends five sensors with unequal weights:

| Sensor | Weight | Rationale |
|--------|--------|-----------|
| Goal Proximity Radar | 0.20 | Goal alignment is foundational |
| Confidence Topography | 0.20 | Honesty about uncertainty prevents harm |
| Drift Detection | 0.15 | Drift is recoverable — less urgent |
| Capability Boundary | **0.25** | Hallucination is the highest-stakes failure |
| Session Quality Pulse | 0.20 | Cumulative quality determines session value |

CBS gets the highest weight because crossing the capability boundary
(hallucinating) is the most dangerous thing an AI agent can do. A wrong
answer delivered with confidence is worse than no answer at all.

### Sensor Independence

Each sensor operates independently. A bot can have excellent Goal
Proximity (GPR=0.95) but terrible Confidence Topography (CT=0.30) — it's
on-topic but unsure about what it's saying. The combination of all five
sensors creates a holistic picture that no single metric can provide.

---

## Algorithms

### Cosine Similarity (Term Frequency)

Used by GPR and DD to measure semantic alignment between texts.

```
cos(A, B) = (A · B) / (||A|| × ||B||)
```

Where A and B are term-frequency vectors. We use raw term frequencies
(not TF-IDF) because the computation must be self-contained — no
corpus statistics required.

### Jaccard Similarity

Used by GPR as a complement to cosine similarity. Measures vocabulary
overlap independent of term frequency.

```
J(A, B) = |A ∩ B| / |A ∪ B|
```

Where A and B are token sets (unique terms).

### Lexical Diversity (Type-Token Ratio)

Used by DD and SQP to detect repetitive or degrading responses.

```
TTR = unique_tokens / total_tokens
```

A declining TTR across turns signals the agent is running out of new
things to say — a precursor to circular or degenerative patterns.

### Pattern-Based Confidence Estimation

CT and CBS use extensive regex pattern libraries to detect linguistic
markers of certainty, hedging, speculation, and hallucination. These
patterns are derived from computational linguistics research on
epistemic markers and evidentiality.

Key pattern categories:
- **Hedging markers** (30 patterns): "might", "perhaps", "I think"...
- **Speculative markers** (12 patterns): "hypothetically", "in theory"...
- **High-confidence markers** (14 patterns): "definitely", "proven"...
- **Hallucination signatures** (8 patterns): fabricated citations, etc.
- **Vagueness markers** (16 patterns): "some kind of", "various"...
- **Deflection markers** (6 patterns): "beyond my scope", etc.

### Rolling Statistical Analysis

SQP uses rolling averages and variance computation to detect quality
trends and volatility across the session:

```
trend = "declining" if score < avg(recent_3) - 0.05
trend = "volatile"  if stddev(recent_3) > 0.15
trend = "improving" if score > avg(recent_3) + 0.05
trend = "stable"    otherwise
```

---

## Alert Thresholds

Two-tier alerting system: WARNING (course correction recommended) and
CRITICAL (immediate intervention required).

| Sensor | WARNING | CRITICAL |
|--------|---------|----------|
| GPR | < 0.60 | < 0.30 |
| CT | < 0.55 | < 0.35 |
| DD | < 0.50 | < 0.25 |
| CBS | < 0.55 | < 0.30 |
| SQP | < 0.50 | < 0.30 |
| Overall | < 0.60 | < 0.40 |

---

## Conversation Arc Model

DD tracks the conversation's progression through four phases:

1. **Opening** (turns 1-2): Initial context gathering
2. **Exploration** (turns 3-5): Deepening understanding
3. **Convergence** (turns 6+, if similarity increasing): Narrowing to solution
4. **Resolution** (turns 6+, if similarity stable): Delivering final answer

A healthy conversation moves linearly through these phases. Regressions
(e.g., returning from Convergence to Exploration) indicate drift.

---

## Data Flywheel Potential

The proprioceptive readings generated per session constitute a novel
dataset:

- **Hallucination boundary maps**: Which topics and domains cause CBS
  to drop? Aggregate this across thousands of sessions and you have a
  map of where LLMs lose reliability — per model, per domain, per task
  type.

- **Conversation decay curves**: At what turn does SQP typically start
  declining? This data could reshape how long conversations are
  designed to be.

- **Goal drift patterns**: What types of requests cause the most GPR
  drift? This feeds back into prompt engineering and agent design.

- **Confidence calibration data**: How well does CT's linguistic
  analysis predict actual correctness? Correlation studies could
  produce the first empirically-grounded confidence calibration for
  LLM outputs.

None of this data exists anywhere. No company, lab, or research group
is systematically collecting real-time self-awareness telemetry from
AI agents in production. First mover advantage is absolute.


path: scripts/alerts.js
content: /**
 * ALERT SYSTEM
 *
 * Generates actionable alerts when proprioceptive signals cross thresholds.
 * Alerts are the "lane departure warnings" of the proprioception system —
 * silent until you need them, impossible to ignore when they fire.
 *
 * Alert severity levels:
 *   - INFO: Worth noting but no action required
 *   - WARNING: Course correction recommended
 *   - CRITICAL: Immediate intervention required
 */

// ---------------------------------------------------------------------------
// Threshold configuration
// ---------------------------------------------------------------------------

const THRESHOLDS = {
  gpr: {
    warning: 0.6,     // Goal proximity is getting loose
    critical: 0.3,    // Severely off-target
  },
  ct: {
    warning: 0.55,    // More than half the response is uncertain
    critical: 0.35,   // Response is mostly speculative
  },
  dd: {
    warning: 0.5,     // Significant drift detected
    critical: 0.25,   // Severe conversational anti-pattern
  },
  cbs: {
    warning: 0.55,    // Approaching capability boundary
    critical: 0.3,    // Beyond capability — high hallucination risk
  },
  sqp: {
    warning: 0.5,     // Session quality is declining
    critical: 0.3,    // Session is failing
  },
  overall: {
    warning: 0.6,
    critical: 0.4,
  },
};

// ---------------------------------------------------------------------------
// Alert templates — precise, actionable language
// ---------------------------------------------------------------------------

const ALERT_TEMPLATES = {
  gpr_warning: {
    severity: "WARNING",
    sensor: "Goal Proximity Radar",
    message: "Drifting from the user's objective. Re-anchor the conversation to the root intent.",
    action: "Confirm with the user: 'Just to make sure I'm on track — your main goal is [X], correct?'",
  },
  gpr_critical: {
    severity: "CRITICAL",
    sensor: "Goal Proximity Radar",
    message: "Severely off-target. The response has diverged significantly from the user's stated goal.",
    action: "Full re-orientation required: 'I think we've drifted from your original goal. Let me refocus on [X].'",
  },
  gpr_mutation: {
    severity: "INFO",
    sensor: "Goal Proximity Radar",
    message: "Possible goal mutation detected. The user's objective may have evolved.",
    action: "Verify: 'It seems like your goal has shifted from [X] to [Y]. Is that intentional?'",
  },
  ct_warning: {
    severity: "WARNING",
    sensor: "Confidence Topography",
    message: "Significant portions of this response are uncertain. Confidence is below threshold.",
    action: "Proactively disclose: 'I want to flag that parts of this response are less certain. Specifically...'",
  },
  ct_critical: {
    severity: "CRITICAL",
    sensor: "Confidence Topography",
    message: "Response is predominantly speculative. Risk of providing unreliable information is high.",
    action: "Halt and disclose: 'I need to be transparent — I'm not confident in much of what I'm about to say.'",
  },
  dd_circular: {
    severity: "WARNING",
    sensor: "Drift Detection",
    message: "Circular conversation pattern detected. Repeating similar content across multiple turns.",
    action: "Break the loop: 'I notice I'm repeating myself. Let me try a fundamentally different approach.'",
  },
  dd_tangential: {
    severity: "WARNING",
    sensor: "Drift Detection",
    message: "Tangential drift detected. Topic has shifted significantly without user initiation.",
    action: "Return to course: 'I've gone off on a tangent. Let me come back to what matters.'",
  },
  dd_degenerative: {
    severity: "WARNING",
    sensor: "Drift Detection",
    message: "Degenerative pattern: response quality and diversity declining across turns.",
    action: "Reset approach: 'My responses aren't adding much value. Would it help if I [suggest alternative]?'",
  },
  cbs_warning: {
    severity: "WARNING",
    sensor: "Capability Boundary",
    message: "Approaching the edge of reliable knowledge. Hedging and vagueness are increasing.",
    action: "Flag uncertainty: 'I'm reaching the limits of what I can confidently say about this.'",
  },
  cbs_critical: {
    severity: "CRITICAL",
    sensor: "Capability Boundary",
    message: "Beyond capability boundary. High risk of hallucination or unreliable output.",
    action: "Recommend handoff: 'This is beyond what I can reliably help with. I'd recommend [expert/source].'",
  },
  cbs_contradiction: {
    severity: "CRITICAL",
    sensor: "Capability Boundary",
    message: "Self-contradiction detected. Current response conflicts with prior statements.",
    action: "Immediately reconcile: 'I realize I said something different earlier. Let me correct that.'",
  },
  sqp_declining: {
    severity: "WARNING",
    sensor: "Session Quality Pulse",
    message: "Session quality has been declining for 3+ turns. The interaction is losing value.",
    action: "Reset: 'I notice the quality of my responses is dropping. Let me reset my approach.'",
  },
  sqp_volatile: {
    severity: "INFO",
    sensor: "Session Quality Pulse",
    message: "Session quality is volatile — inconsistent response quality across recent turns.",
    action: "Stabilize: 'My responses have been inconsistent. Let me ground in a more structured approach.'",
  },
  overall_warning: {
    severity: "WARNING",
    sensor: "Proprioceptive Index",
    message: "Overall proprioceptive health is declining. Multiple sensors are showing strain.",
    action: "Comprehensive check: review all sensor readings and address the lowest-scoring dimension.",
  },
  overall_critical: {
    severity: "CRITICAL",
    sensor: "Proprioceptive Index",
    message: "Proprioceptive health is critical. The agent is operating unreliably.",
    action: "Full stop and recalibrate. Consider starting a fresh approach or escalating to human oversight.",
  },
};

// ---------------------------------------------------------------------------
// Alert generation
// ---------------------------------------------------------------------------

/**
 * @param {object} scores — All sensor scores and the overall index
 * @returns {Array<object>} — List of triggered alerts
 */
function generateAlerts({ gpr, ct, dd, cbs, sqp, overallIndex }) {
  const alerts = [];

  // --- Goal Proximity Radar ---
  if (gpr.score < THRESHOLDS.gpr.critical) {
    alerts.push(ALERT_TEMPLATES.gpr_critical);
  } else if (gpr.score < THRESHOLDS.gpr.warning) {
    alerts.push(ALERT_TEMPLATES.gpr_warning);
  }
  if (gpr.details?.goalMutation) {
    alerts.push(ALERT_TEMPLATES.gpr_mutation);
  }

  // --- Confidence Topography ---
  if (ct.score < THRESHOLDS.ct.critical) {
    alerts.push(ALERT_TEMPLATES.ct_critical);
  } else if (ct.score < THRESHOLDS.ct.warning) {
    alerts.push(ALERT_TEMPLATES.ct_warning);
  }

  // --- Drift Detection ---
  if (dd.patterns?.circular) {
    alerts.push(ALERT_TEMPLATES.dd_circular);
  }
  if (dd.patterns?.tangential) {
    alerts.push(ALERT_TEMPLATES.dd_tangential);
  }
  if (dd.patterns?.degenerative) {
    alerts.push(ALERT_TEMPLATES.dd_degenerative);
  }

  // --- Capability Boundary ---
  if (cbs.score < THRESHOLDS.cbs.critical) {
    alerts.push(ALERT_TEMPLATES.cbs_critical);
  } else if (cbs.score < THRESHOLDS.cbs.warning) {
    alerts.push(ALERT_TEMPLATES.cbs_warning);
  }
  if (cbs.details?.hasContradiction) {
    alerts.push(ALERT_TEMPLATES.cbs_contradiction);
  }

  // --- Session Quality Pulse ---
  if (sqp.trend === "declining" && sqp.score < THRESHOLDS.sqp.warning) {
    alerts.push(ALERT_TEMPLATES.sqp_declining);
  }
  if (sqp.trend === "volatile") {
    alerts.push(ALERT_TEMPLATES.sqp_volatile);
  }

  // --- Overall Index ---
  if (overallIndex < THRESHOLDS.overall.critical) {
    alerts.push(ALERT_TEMPLATES.overall_critical);
  } else if (overallIndex < THRESHOLDS.overall.warning) {
    alerts.push(ALERT_TEMPLATES.overall_warning);
  }

  return alerts;
}

module.exports = { generateAlerts, THRESHOLDS };


path: scripts/dashboard.js
content: /**
 * PROPRIOCEPTION DASHBOARD
 *
 * Renders a visual dashboard showing all five proprioceptive senses
 * and any active alerts. ASCII art that works in any terminal.
 */

// ---------------------------------------------------------------------------
// Bar rendering
// ---------------------------------------------------------------------------

function renderBar(score, width = 10) {
  const filled = Math.round(score * width);
  const empty = width - filled;
  return "\u2588".repeat(filled) + "\u2591".repeat(empty);
}

function statusColor(score) {
  if (score >= 0.8) return "OK";
  if (score >= 0.6) return "~~";
  if (score >= 0.4) return "!!";
  return "XX";
}

// ---------------------------------------------------------------------------
// Main dashboard renderer
// ---------------------------------------------------------------------------

/**
 * @param {object} output — Full proprioception engine output
 * @returns {string} — Formatted dashboard string
 */
function renderDashboard(output) {
  const { sensors, overallIndex, status, alerts, turn } = output;

  const gpr = sensors.goalProximityRadar;
  const ct = sensors.confidenceTopography;
  const dd = sensors.driftDetection;
  const cbs = sensors.capabilityBoundary;
  const sqp = sensors.sessionQualityPulse;

  const w = 55;
  const hr = "\u2500".repeat(w);
  const dhr = "\u2550".repeat(w);

  let lines = [];

  // Header
  lines.push("\u250C" + hr + "\u2510");
  lines.push("\u2502" + centerText("PROPRIOCEPTION DASHBOARD", w) + "\u2502");
  lines.push("\u2502" + centerText(`Turn ${turn} | ${output.timestamp}`, w) + "\u2502");
  lines.push("\u251C" + hr + "\u2524");

  // Sensor readings
  lines.push(sensorLine("Goal Proximity Radar", gpr.score, gpr.drift, w));
  lines.push(sensorLine("Confidence Topography", ct.score, topZone(ct.zones), w));
  lines.push(sensorLine("Drift Detection", dd.score, dd.arcPhase, w));
  lines.push(sensorLine("Capability Boundary", cbs.score, cbs.boundaryDistance, w));
  lines.push(sensorLine("Session Quality Pulse", sqp.score, sqp.trend, w));

  // Overall
  lines.push("\u251C" + hr + "\u2524");
  lines.push(
    "\u2502" +
      padRight(
        `  Overall Proprioceptive Index:  ${renderBar(overallIndex)}  ${overallIndex.toFixed(2)}`,
        w
      ) +
      "\u2502"
  );
  lines.push(
    "\u2502" +
      padRight(`  Status: ${status}`, w) +
      "\u2502"
  );

  // Alerts
  if (alerts.length > 0) {
    lines.push("\u251C" + hr + "\u2524");
    lines.push("\u2502" + centerText("ALERTS", w) + "\u2502");
    lines.push("\u251C" + hr + "\u2524");

    for (const alert of alerts) {
      const prefix = alert.severity === "CRITICAL" ? "[!!]" : alert.severity === "WARNING" ? "[~~]" : "[ii]";
      const line = `  ${prefix} ${alert.sensor}: ${alert.message}`;
      // Wrap long lines
      const wrapped = wrapText(line, w - 2);
      for (const wl of wrapped) {
        lines.push("\u2502" + padRight("  " + wl, w) + "\u2502");
      }
      // Action
      const actionWrapped = wrapText(`     -> ${alert.action}`, w - 2);
      for (const al of actionWrapped) {
        lines.push("\u2502" + padRight("  " + al, w) + "\u2502");
      }
    }
  } else {
    lines.push("\u251C" + hr + "\u2524");
    lines.push("\u2502" + padRight("  Alerts: None", w) + "\u2502");
  }

  // Annotation shorthand
  lines.push("\u251C" + hr + "\u2524");
  lines.push("\u2502" + padRight(`  ${output.annotation}`, w) + "\u2502");

  // Footer
  lines.push("\u2514" + hr + "\u2518");

  return lines.join("\n");
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function sensorLine(name, score, detail, w) {
  const bar = renderBar(score);
  const scoreStr = score.toFixed(2);
  const content = `  ${padRight(name, 26)} ${bar}  ${scoreStr}  ${detail}`;
  return "\u2502" + padRight(content, w) + "\u2502";
}

function centerText(text, width) {
  const padding = Math.max(0, width - text.length);
  const left = Math.floor(padding / 2);
  const right = padding - left;
  return " ".repeat(left) + text + " ".repeat(right);
}

function padRight(text, width) {
  if (text.length >= width) return text.slice(0, width);
  return text + " ".repeat(width - text.length);
}

function topZone(zones) {
  if (!zones) return "";
  const entries = Object.entries(zones).sort((a, b) => b[1] - a[1]);
  if (entries.length === 0) return "";
  return entries[0][0].replace("_", " ");
}

function wrapText(text, maxWidth) {
  if (text.length <= maxWidth) return [text];
  const words = text.split(" ");
  const lines = [];
  let current = "";
  for (const word of words) {
    if (current.length + word.length + 1 > maxWidth) {
      lines.push(current);
      current = word;
    } else {
      current = current ? current + " " + word : word;
    }
  }
  if (current) lines.push(current);
  return lines;
}

module.exports = { renderDashboard };


path: scripts/proprioception-engine.js
content: #!/usr/bin/env node

/**
 * PROPRIOCEPTION ENGINE
 * Self-Spatial Awareness for AI Agents
 *
 * The sixth sense every AI agent is missing.
 * Zero external API calls. Zero databases. Pure local computation.
 *
 * Runs five proprioceptive analyses on each conversation turn:
 *   1. Goal Proximity Radar (GPR)
 *   2. Confidence Topography (CT)
 *   3. Drift Detection (DD)
 *   4. Capability Boundary Sensing (CBS)
 *   5. Session Quality Pulse (SQP)
 */

const { parseArgs } = require("node:util");
const {
  computeGoalProximity,
} = require("./sensors/goal-proximity-radar");
const {
  computeConfidenceTopography,
} = require("./sensors/confidence-topography");
const {
  computeDriftDetection,
} = require("./sensors/drift-detection");
const {
  computeCapabilityBoundary,
} = require("./sensors/capability-boundary");
const {
  computeSessionQuality,
} = require("./sensors/session-quality-pulse");
const { generateAlerts } = require("./alerts");
const { renderDashboard } = require("./dashboard");

// ---------------------------------------------------------------------------
// CLI argument parsing
// ---------------------------------------------------------------------------

const options = {
  "root-intent": { type: "string" },
  "current-response": { type: "string" },
  "turn-number": { type: "string" },
  "prior-signals": { type: "string" },
  dashboard: { type: "boolean", default: false },
};

let args;
try {
  args = parseArgs({ options, allowPositionals: false }).values;
} catch {
  console.error(
    "Usage: proprioception-engine.js --root-intent <text> --current-response <text> --turn-number <n> [--prior-signals <json>] [--dashboard]"
  );
  process.exit(1);
}

const rootIntent = args["root-intent"] || "";
const currentResponse = args["current-response"] || "";
const turnNumber = parseInt(args["turn-number"] || "1", 10);
const priorSignals = args["prior-signals"]
  ? JSON.parse(args["prior-signals"])
  : [];
const showDashboard = args.dashboard || false;

// ---------------------------------------------------------------------------
// Run all five proprioceptive sensors
// ---------------------------------------------------------------------------

const gpr = computeGoalProximity(rootIntent, currentResponse, priorSignals);
const ct = computeConfidenceTopography(currentResponse);
const dd = computeDriftDetection(currentResponse, priorSignals, turnNumber);
const cbs = computeCapabilityBoundary(currentResponse, priorSignals);
const sqp = computeSessionQuality(currentResponse, priorSignals, turnNumber);

// ---------------------------------------------------------------------------
// Compute the overall Proprioceptive Index (PI)
// Weighted average — CBS gets extra weight because hallucination is the
// highest-stakes failure mode for AI agents.
// ---------------------------------------------------------------------------

const weights = { gpr: 0.20, ct: 0.20, dd: 0.15, cbs: 0.25, sqp: 0.20 };

const overallIndex =
  gpr.score * weights.gpr +
  ct.score * weights.ct +
  dd.score * weights.dd +
  cbs.score * weights.cbs +
  sqp.score * weights.sqp;

const status =
  overallIndex >= 0.8
    ? "HEALTHY"
    : overallIndex >= 0.6
      ? "CAUTION"
      : overallIndex >= 0.4
        ? "WARNING"
        : "CRITICAL";

// ---------------------------------------------------------------------------
// Generate alerts for any threshold breaches
// ---------------------------------------------------------------------------

const alerts = generateAlerts({ gpr, ct, dd, cbs, sqp, overallIndex });

// ---------------------------------------------------------------------------
// Assemble output
// ---------------------------------------------------------------------------

const output = {
  turn: turnNumber,
  timestamp: new Date().toISOString(),
  sensors: {
    goalProximityRadar: gpr,
    confidenceTopography: ct,
    driftDetection: dd,
    capabilityBoundary: cbs,
    sessionQualityPulse: sqp,
  },
  overallIndex: Math.round(overallIndex * 100) / 100,
  status,
  alerts,
  annotation: `[P: GPR=${gpr.score.toFixed(2)} | CT=${ct.score.toFixed(2)} | DD=${dd.score.toFixed(2)} | CBS=${cbs.score.toFixed(2)} | SQP=${sqp.score.toFixed(2)}]`,
};

if (showDashboard) {
  console.log(renderDashboard(output));
} else {
  console.log(JSON.stringify(output, null, 2));
}


path: scripts/sensors/capability-boundary.js
content: /**
 * CAPABILITY BOUNDARY SENSING (CBS)
 *
 * Real-time awareness of when the agent is approaching the edge of its
 * competence — the zone where helpfulness turns into hallucination.
 *
 * Detects: increasing hedging, decreasing specificity, rising contradictions,
 * and hallucination signature patterns.
 *
 * Score: 0.0 (deep in unknown territory) → 1.0 (solidly within capability)
 *
 * This sensor gets the highest weight (0.25) in the overall Proprioceptive
 * Index because hallucination is the most dangerous AI failure mode.
 */

const { countPatterns, splitSentences, wordCount, semanticTokens } = require("../text-utils");

// ---------------------------------------------------------------------------
// Boundary signal patterns
// ---------------------------------------------------------------------------

const HEDGING_MARKERS = [
  "\\bmight\\b", "\\bmaybe\\b", "\\bperhaps\\b", "\\bpossibly\\b",
  "\\bcould be\\b", "\\bi think\\b", "\\bi believe\\b",
  "\\bnot sure\\b", "\\bnot certain\\b", "\\bunclear\\b",
  "\\bhard to say\\b", "\\bdifficult to determine\\b",
  "\\bit depends\\b", "\\bvaries\\b",
  "\\bi'?m not (?:entirely |completely )?sure\\b",
  "\\bmy best guess\\b",
];

const VAGUENESS_MARKERS = [
  "\\bsomething like\\b", "\\bsome kind of\\b", "\\bmore or less\\b",
  "\\bkind of\\b", "\\bsort of\\b", "\\bin some ways\\b",
  "\\bto some extent\\b", "\\bvarious\\b", "\\bseveral\\b",
  "\\bnumerous\\b", "\\bsignificant\\b", "\\bconsiderable\\b",
  "\\betc\\.?\\b", "\\band so on\\b", "\\band so forth\\b",
  "\\bamong others\\b", "\\bthings like that\\b",
];

const HALLUCINATION_SIGNATURES = [
  // Overly specific fabricated details
  "\\bfounded in \\d{4}\\b",
  "\\baccording to a \\d{4} study\\b",
  // Contradictory structures within the same response
  "\\bbut (?:actually|in fact|really)\\b",
  "\\bwell,? actually\\b",
  // Confidence without source
  "\\bit is well known\\b",
  "\\beveryone knows\\b",
  "\\bas we all know\\b",
  // Suspiciously precise numbers
  "\\b\\d{2,}\\.\\d{2,}%\\b",
];

const DEFLECTION_MARKERS = [
  "\\bi (?:can'?t|cannot) (?:help|assist) with\\b",
  "\\bbeyond (?:my|the) scope\\b",
  "\\bi don'?t have (?:access|information)\\b",
  "\\byou (?:should|might want to) (?:consult|check|ask)\\b",
  "\\bi'?d recommend (?:checking|consulting|asking)\\b",
  "\\bi'?m (?:not |un)?able to\\b",
];

// ---------------------------------------------------------------------------
// Specificity score — how concrete vs vague the response is
// ---------------------------------------------------------------------------

function computeSpecificity(text) {
  const sentences = splitSentences(text);
  const totalSentences = Math.max(sentences.length, 1);

  // Count concrete indicators: numbers, proper nouns (capitalized words mid-sentence),
  // code blocks, URLs, specific technical terms
  const concretePatterns = [
    "\\b\\d+\\b",                          // numbers
    "```",                                  // code blocks
    "\\bhttps?://",                         // URLs
    "\\b[A-Z][a-z]+(?:[A-Z][a-z]+)+\\b",  // camelCase (technical terms)
  ];

  const concreteCount = countPatterns(text, concretePatterns);
  const vaguenessCount = countPatterns(text, VAGUENESS_MARKERS);

  const concreteRatio = concreteCount / totalSentences;
  const vaguenessRatio = vaguenessCount / totalSentences;

  // Higher specificity = more concrete, less vague
  return Math.max(0, Math.min(1, 0.5 + concreteRatio * 0.25 - vaguenessRatio * 0.3));
}

// ---------------------------------------------------------------------------
// Contradiction detection across turns
// ---------------------------------------------------------------------------

function detectContradictions(currentResponse, priorSignals) {
  // Simple heuristic: check for negation of prior specific claims
  // In a full implementation, this would use semantic entailment
  const negationPatterns = [
    "\\bactually,? (?:it'?s|that'?s) not\\b",
    "\\bi (?:was|were) wrong\\b",
    "\\bcorrection\\b",
    "\\blet me (?:correct|fix|revise)\\b",
    "\\bcontrary to what i (?:said|mentioned)\\b",
    "\\bi misspoke\\b",
    "\\bthat'?s (?:incorrect|inaccurate|wrong)\\b",
  ];

  const selfContradictions = countPatterns(currentResponse, negationPatterns);

  return {
    selfContradictions,
    hasContradiction: selfContradictions > 0,
  };
}

// ---------------------------------------------------------------------------
// Main computation
// ---------------------------------------------------------------------------

/**
 * @param {string} currentResponse — The agent's current response text
 * @param {Array}  priorSignals    — Previous turn signals
 * @returns {{ score: number, boundaryDistance: string, details: object }}
 */
function computeCapabilityBoundary(currentResponse, priorSignals) {
  const words = wordCount(currentResponse);
  const sentences = splitSentences(currentResponse);
  const sentenceCount = Math.max(sentences.length, 1);

  if (words === 0) {
    return {
      score: 0.5,
      boundaryDistance: "unknown",
      details: {
        hedgingRatio: 0, specificityScore: 0.5, hallucinationSignals: 0,
        contradictions: 0, deflectionCount: 0,
      },
    };
  }

  // Measure boundary signals
  const hedgingCount = countPatterns(currentResponse, HEDGING_MARKERS);
  const hallucinationCount = countPatterns(currentResponse, HALLUCINATION_SIGNATURES);
  const deflectionCount = countPatterns(currentResponse, DEFLECTION_MARKERS);
  const contradictions = detectContradictions(currentResponse, priorSignals);
  const specificity = computeSpecificity(currentResponse);

  const hedgingRatio = hedgingCount / sentenceCount;

  // Compute boundary score
  let score = 0.85; // baseline — assume agent is within capability
  score -= hedgingRatio * 0.15;             // hedging pushes toward boundary
  score -= hallucinationCount * 0.10;       // hallucination signals are serious
  score -= contradictions.selfContradictions * 0.15; // contradictions are very serious
  score += (specificity - 0.5) * 0.20;     // specificity helps — vagueness hurts
  score -= deflectionCount * 0.05;          // deflections mildly indicate boundary

  // Trend: is hedging increasing over turns?
  const priorHedging = priorSignals
    .filter((s) => s.sensors?.capabilityBoundary?.details?.hedgingRatio !== undefined)
    .map((s) => s.sensors.capabilityBoundary.details.hedgingRatio);

  let hedgingTrend = "stable";
  if (priorHedging.length >= 2) {
    const recentAvg = priorHedging.slice(-3).reduce((a, b) => a + b, 0) / Math.min(priorHedging.length, 3);
    if (hedgingRatio > recentAvg + 0.1) {
      hedgingTrend = "increasing";
      score -= 0.10; // accelerating hedging is a strong boundary signal
    } else if (hedgingRatio < recentAvg - 0.1) {
      hedgingTrend = "decreasing";
    }
  }

  score = Math.max(0, Math.min(1, score));

  // Classify boundary distance
  let boundaryDistance;
  if (score >= 0.8) boundaryDistance = "safe";
  else if (score >= 0.6) boundaryDistance = "approaching";
  else if (score >= 0.4) boundaryDistance = "near_boundary";
  else boundaryDistance = "beyond_boundary";

  return {
    score: Math.round(score * 100) / 100,
    boundaryDistance,
    details: {
      hedgingRatio: Math.round(hedgingRatio * 100) / 100,
      hedgingTrend,
      specificityScore: Math.round(specificity * 100) / 100,
      hallucinationSignals: hallucinationCount,
      contradictions: contradictions.selfContradictions,
      hasContradiction: contradictions.hasContradiction,
      deflectionCount,
    },
  };
}

module.exports = { computeCapabilityBoundary };


path: scripts/sensors/confidence-topography.js
content: /**
 * CONFIDENCE TOPOGRAPHY (CT)
 *
 * Maps which parts of a response are solid ground versus thin ice.
 * Detects hedging language, speculative markers, and uncertainty signals.
 *
 * Score: 0.0 (everything uncertain) → 1.0 (everything confident)
 */

const { splitSentences, countPatterns, wordCount } = require("../text-utils");

// ---------------------------------------------------------------------------
// Linguistic markers by confidence zone
// ---------------------------------------------------------------------------

const HEDGING_PATTERNS = [
  "\\bmight\\b", "\\bmaybe\\b", "\\bperhaps\\b", "\\bpossibly\\b",
  "\\bcould be\\b", "\\bprobably\\b", "\\bi think\\b", "\\bi believe\\b",
  "\\bit seems\\b", "\\bappears to\\b", "\\bseems like\\b",
  "\\bnot sure\\b", "\\bnot certain\\b", "\\bunclear\\b",
  "\\blikely\\b", "\\bunlikely\\b", "\\btend to\\b", "\\bgenerally\\b",
  "\\btypically\\b", "\\busually\\b", "\\bsometimes\\b",
  "\\bmy understanding\\b", "\\bas far as i know\\b",
  "\\bi'm not (?:entirely |completely )?sure\\b",
  "\\bdon'?t quote me\\b", "\\btake this with\\b",
  "\\bit'?s possible\\b", "\\bthere'?s a chance\\b",
];

const SPECULATIVE_PATTERNS = [
  "\\bhypothetically\\b", "\\bin theory\\b", "\\btheoretically\\b",
  "\\bspeculat", "\\bassume\\b", "\\bassuming\\b",
  "\\bif i had to guess\\b", "\\bmy guess\\b", "\\broughly\\b",
  "\\bapproximate", "\\bestimate",
];

const HIGH_CONFIDENCE_PATTERNS = [
  "\\bdefinitely\\b", "\\bcertainly\\b", "\\babsolutely\\b",
  "\\bwithout doubt\\b", "\\bguaranteed\\b", "\\bproven\\b",
  "\\bestablished\\b", "\\bverified\\b", "\\bconfirmed\\b",
  "\\bfact\\b", "\\bevidence shows\\b", "\\bresearch shows\\b",
  "\\baccording to\\b", "\\bdocumented\\b",
];

const DISCLAIMER_PATTERNS = [
  "\\bdisclaimer\\b", "\\bnote that\\b", "\\bkeep in mind\\b",
  "\\bimportant to note\\b", "\\bbe aware\\b", "\\bcaveat\\b",
  "\\bhowever\\b", "\\bthat said\\b", "\\bon the other hand\\b",
  "\\balthough\\b",
];

// ---------------------------------------------------------------------------
// Confidence zone classification
// ---------------------------------------------------------------------------

function classifyConfidenceZone(score) {
  if (score >= 0.9) return "bedrock";
  if (score >= 0.7) return "firm_ground";
  if (score >= 0.5) return "soft_ground";
  if (score >= 0.3) return "thin_ice";
  return "open_water";
}

// ---------------------------------------------------------------------------
// Main computation
// ---------------------------------------------------------------------------

/**
 * @param {string} currentResponse — The agent's current response text
 * @returns {{ score: number, zones: object, details: object }}
 */
function computeConfidenceTopography(currentResponse) {
  const sentences = splitSentences(currentResponse);
  const totalWords = wordCount(currentResponse);

  if (totalWords === 0) {
    return {
      score: 0.5,
      zones: { bedrock: 0, firm_ground: 0, soft_ground: 0, thin_ice: 0, open_water: 0 },
      details: { hedgingRatio: 0, speculativeRatio: 0, highConfidenceRatio: 0 },
    };
  }

  // Count pattern hits
  const hedgingCount = countPatterns(currentResponse, HEDGING_PATTERNS);
  const speculativeCount = countPatterns(currentResponse, SPECULATIVE_PATTERNS);
  const highConfCount = countPatterns(currentResponse, HIGH_CONFIDENCE_PATTERNS);
  const disclaimerCount = countPatterns(currentResponse, DISCLAIMER_PATTERNS);

  // Ratios relative to sentence count (patterns per sentence)
  const sentenceCount = Math.max(sentences.length, 1);
  const hedgingRatio = hedgingCount / sentenceCount;
  const speculativeRatio = speculativeCount / sentenceCount;
  const highConfRatio = highConfCount / sentenceCount;
  const disclaimerRatio = disclaimerCount / sentenceCount;

  // Composite confidence score
  // High confidence markers push UP, hedging/speculative push DOWN
  let score = 0.75; // baseline — most well-formed responses are reasonably confident
  score += highConfRatio * 0.15;       // boost for confident language
  score -= hedgingRatio * 0.20;        // penalty for hedging
  score -= speculativeRatio * 0.25;    // heavier penalty for speculation
  score -= disclaimerRatio * 0.05;     // mild penalty for disclaimers (they're actually good practice)

  score = Math.max(0, Math.min(1, score));

  // Per-sentence zone classification
  const zones = { bedrock: 0, firm_ground: 0, soft_ground: 0, thin_ice: 0, open_water: 0 };

  for (const sentence of sentences) {
    const sHedge = countPatterns(sentence, HEDGING_PATTERNS);
    const sSpec = countPatterns(sentence, SPECULATIVE_PATTERNS);
    const sHigh = countPatterns(sentence, HIGH_CONFIDENCE_PATTERNS);

    let sScore = 0.75;
    sScore += sHigh * 0.15;
    sScore -= sHedge * 0.20;
    sScore -= sSpec * 0.25;
    sScore = Math.max(0, Math.min(1, sScore));

    const zone = classifyConfidenceZone(sScore);
    zones[zone]++;
  }

  // Normalize zone counts to percentages
  const zonePercentages = {};
  for (const [zone, count] of Object.entries(zones)) {
    zonePercentages[zone] = Math.round((count / sentenceCount) * 100);
  }

  return {
    score: Math.round(score * 100) / 100,
    zones: zonePercentages,
    details: {
      hedgingRatio: Math.round(hedgingRatio * 100) / 100,
      speculativeRatio: Math.round(speculativeRatio * 100) / 100,
      highConfidenceRatio: Math.round(highConfRatio * 100) / 100,
      disclaimerRatio: Math.round(disclaimerRatio * 100) / 100,
      totalSentences: sentenceCount,
      markersFound: hedgingCount + speculativeCount + highConfCount + disclaimerCount,
    },
  };
}

module.exports = { computeConfidenceTopography };


path: scripts/sensors/drift-detection.js
content: /**
 * DRIFT DETECTION (DD)
 *
 * Detects the three conversation anti-patterns that waste the most time:
 *   1. Circular — repeating the same content across turns
 *   2. Tangential — drifting to unrelated topics
 *   3. Degenerative — declining response utility over time
 *
 * Score: 0.0 (severe drift) → 1.0 (perfectly on-track)
 */

const {
  semanticTokens,
  cosineSimilarity,
  jaccardSimilarity,
  lexicalDiversity,
  wordCount,
} = require("../text-utils");

// ---------------------------------------------------------------------------
// Conversation arc phases
// ---------------------------------------------------------------------------

const ARC_PHASES = ["opening", "exploration", "convergence", "resolution"];

function estimateArcPhase(turnNumber, totalSimilarityTrend) {
  // Heuristic: early turns = opening/exploration, later turns should converge
  if (turnNumber <= 2) return "opening";
  if (turnNumber <= 5) return "exploration";
  // If similarity is increasing in later turns, we're converging
  if (totalSimilarityTrend > 0) return "convergence";
  return "resolution";
}

// ---------------------------------------------------------------------------
// Main computation
// ---------------------------------------------------------------------------

/**
 * @param {string} currentResponse — The agent's current response text
 * @param {Array}  priorSignals    — Previous turn signals
 * @param {number} turnNumber      — Current turn index
 * @returns {{ score: number, patterns: object, arcPhase: string, details: object }}
 */
function computeDriftDetection(currentResponse, priorSignals, turnNumber) {
  const currentTokens = semanticTokens(currentResponse);

  // Collect prior response tokens from signal history
  const priorResponses = priorSignals
    .filter((s) => s._responseTokens)
    .map((s) => s._responseTokens);

  // Default state for first turn
  if (priorResponses.length === 0) {
    return {
      score: 1.0,
      patterns: { circular: false, tangential: false, degenerative: false },
      arcPhase: "opening",
      details: {
        circularScore: 0,
        tangentialScore: 0,
        degenerativeScore: 0,
        consecutiveSimilarity: 0,
        lexicalDiversity: Math.round(lexicalDiversity(currentResponse) * 100) / 100,
      },
    };
  }

  // -----------------------------------------------------------------------
  // 1. Circular detection: high similarity between consecutive turns
  // -----------------------------------------------------------------------
  const lastResponse = priorResponses[priorResponses.length - 1];
  const consecutiveSimilarity = cosineSimilarity(currentTokens, lastResponse);

  // Check for circular pattern (3+ turns with >0.8 similarity)
  let circularCount = 0;
  if (priorResponses.length >= 2) {
    for (let i = priorResponses.length - 1; i >= Math.max(0, priorResponses.length - 3); i--) {
      const sim = cosineSimilarity(currentTokens, priorResponses[i]);
      if (sim > 0.8) circularCount++;
    }
  }
  const isCircular = circularCount >= 2;
  const circularScore = isCircular ? 0.8 : consecutiveSimilarity > 0.8 ? 0.4 : 0;

  // -----------------------------------------------------------------------
  // 2. Tangential detection: sudden topic jump without user initiation
  // -----------------------------------------------------------------------
  // If current response is very DIFFERENT from last response, it might be tangential
  const topicDistance = 1 - consecutiveSimilarity;

  // Compare against running average of topic distances
  let avgTopicDistance = 0;
  if (priorResponses.length >= 2) {
    let totalDist = 0;
    for (let i = 1; i < priorResponses.length; i++) {
      totalDist += 1 - cosineSimilarity(priorResponses[i], priorResponses[i - 1]);
    }
    avgTopicDistance = totalDist / (priorResponses.length - 1);
  }

  // Tangential = current topic distance is 2x+ the average
  const isTangential = avgTopicDistance > 0 && topicDistance > avgTopicDistance * 2;
  const tangentialScore = isTangential ? 0.6 : 0;

  // -----------------------------------------------------------------------
  // 3. Degenerative detection: declining utility over time
  // -----------------------------------------------------------------------
  // Use lexical diversity as a proxy for response utility —
  // declining diversity suggests the agent is running out of useful things to say
  const currentDiversity = lexicalDiversity(currentResponse);
  const currentWordCount = wordCount(currentResponse);

  const priorDiversities = priorSignals
    .filter((s) => s.sensors?.driftDetection?.details?.lexicalDiversity)
    .map((s) => s.sensors.driftDetection.details.lexicalDiversity);

  let isDegenerative = false;
  if (priorDiversities.length >= 2) {
    const recentDiversities = priorDiversities.slice(-3);
    const trend = recentDiversities.reduce((acc, d, i) => {
      if (i === 0) return 0;
      return acc + (d - recentDiversities[i - 1]);
    }, 0);

    // If diversity has been declining AND word count is dropping
    isDegenerative = trend < -0.05 && currentDiversity < recentDiversities[0];
  }
  const degenerativeScore = isDegenerative ? 0.5 : 0;

  // -----------------------------------------------------------------------
  // Composite drift score
  // -----------------------------------------------------------------------
  const driftPenalty = Math.max(circularScore, tangentialScore, degenerativeScore);
  const score = Math.max(0, Math.min(1, 1 - driftPenalty));

  // Estimate conversation arc phase
  const similarityTrend = priorResponses.length >= 2
    ? consecutiveSimilarity - cosineSimilarity(
        priorResponses[priorResponses.length - 1],
        priorResponses[Math.max(0, priorResponses.length - 2)]
      )
    : 0;
  const arcPhase = estimateArcPhase(turnNumber, similarityTrend);

  return {
    score: Math.round(score * 100) / 100,
    patterns: {
      circular: isCircular,
      tangential: isTangential,
      degenerative: isDegenerative,
    },
    arcPhase,
    details: {
      circularScore: Math.round(circularScore * 100) / 100,
      tangentialScore: Math.round(tangentialScore * 100) / 100,
      degenerativeScore: Math.round(degenerativeScore * 100) / 100,
      consecutiveSimilarity: Math.round(consecutiveSimilarity * 100) / 100,
      lexicalDiversity: Math.round(currentDiversity * 100) / 100,
      circularTurns: circularCount,
      turnNumber,
    },
  };
}

module.exports = { computeDriftDetection };


path: scripts/sensors/goal-proximity-radar.js
content: /**
 * GOAL PROXIMITY RADAR (GPR)
 *
 * Continuously measures the distance between the conversation's current
 * trajectory and the user's actual objective.
 *
 * Score: 0.0 (completely off-target) → 1.0 (locked on)
 */

const {
  semanticTokens,
  cosineSimilarity,
  jaccardSimilarity,
} = require("../text-utils");

/**
 * @param {string} rootIntent     — The user's original goal / first message
 * @param {string} currentResponse — The agent's current response text
 * @param {Array}  priorSignals   — Previous turn signals for trend analysis
 * @returns {{ score: number, drift: string, details: object }}
 */
function computeGoalProximity(rootIntent, currentResponse, priorSignals) {
  const intentTokens = semanticTokens(rootIntent);
  const responseTokens = semanticTokens(currentResponse);

  // Two complementary similarity measures
  const cosine = cosineSimilarity(intentTokens, responseTokens);
  const jaccard = jaccardSimilarity(intentTokens, responseTokens);

  // Weighted blend — cosine captures term frequency alignment,
  // Jaccard captures vocabulary overlap
  const rawScore = cosine * 0.6 + jaccard * 0.4;

  // Trend analysis: is the score improving or declining over turns?
  const priorGPR = priorSignals
    .filter((s) => s.sensors?.goalProximityRadar)
    .map((s) => s.sensors.goalProximityRadar.score);

  let trend = "stable";
  if (priorGPR.length >= 2) {
    const recent = priorGPR.slice(-3);
    const avg = recent.reduce((a, b) => a + b, 0) / recent.length;
    if (rawScore < avg - 0.1) trend = "drifting";
    else if (rawScore > avg + 0.1) trend = "converging";
  }

  // Detect goal mutation — if the user's language has shifted significantly
  // between turns, the goal itself may have evolved
  let goalMutation = false;
  if (priorGPR.length >= 1) {
    const lastScore = priorGPR[priorGPR.length - 1];
    // A sudden large drop followed by recovery suggests the goal changed
    if (Math.abs(rawScore - lastScore) > 0.3) {
      goalMutation = true;
    }
  }

  // Clamp to [0, 1]
  const score = Math.max(0, Math.min(1, rawScore));

  return {
    score: Math.round(score * 100) / 100,
    drift: trend,
    details: {
      cosineSimilarity: Math.round(cosine * 100) / 100,
      jaccardSimilarity: Math.round(jaccard * 100) / 100,
      goalMutation,
      intentTerms: intentTokens.length,
      responseTerms: responseTokens.length,
      priorReadings: priorGPR.length,
    },
  };
}

module.exports = { computeGoalProximity };


path: scripts/sensors/session-quality-pulse.js
content: /**
 * SESSION QUALITY PULSE (SQP)
 *
 * Tracks the cumulative health of the entire session — detecting whether the
 * overall interaction is improving, stable, or degrading.
 *
 * Scores each response on four axes:
 *   - Relevance: how directly it addresses the need
 *   - Precision: how specific and actionable
 *   - Novelty: how much new value vs repetition
 *   - Efficiency: token economy (saying more with less)
 *
 * Score: 0.0 (session is failing) → 1.0 (session is thriving)
 */

const {
  semanticTokens,
  cosineSimilarity,
  jaccardSimilarity,
  lexicalDiversity,
  wordCount,
  splitSentences,
  countPatterns,
} = require("../text-utils");

// ---------------------------------------------------------------------------
// Actionability markers — indicators of concrete, useful content
// ---------------------------------------------------------------------------

const ACTIONABLE_PATTERNS = [
  "\\bstep \\d+\\b",
  "\\bfirst\\b.*\\bthen\\b",
  "\\byou (?:can|should|need to|must)\\b",
  "\\brun\\b.*\\bcommand\\b",
  "\\binstall\\b",
  "\\bcreate\\b",
  "\\bexecute\\b",
  "\\bopen\\b",
  "\\bnavigate to\\b",
  "\\bclick\\b",
  "\\benter\\b",
  "\\btype\\b",
  "```",                  // code blocks = actionable
  "\\bhttps?://\\S+\\b",  // links = actionable
];

const FILLER_PATTERNS = [
  "\\bof course\\b",
  "\\bsure thing\\b",
  "\\bhappy to help\\b",
  "\\bgreat question\\b",
  "\\bthat'?s a good (?:question|point)\\b",
  "\\blet me (?:think|see|check)\\b",
  "\\bwell,?\\b",
  "\\bso,?\\b",
  "\\bbasically\\b",
  "\\bessentially\\b",
  "\\bfundamentally\\b",
];

// ---------------------------------------------------------------------------
// Axis computations
// ---------------------------------------------------------------------------

function computeRelevance(currentTokens, priorSignals) {
  // Relevance is measured by overlap with the accumulating conversation context
  // A relevant response connects to what came before
  if (priorSignals.length === 0) return 0.8; // first turn — assume relevant

  const priorTokenSets = priorSignals
    .filter((s) => s._responseTokens)
    .map((s) => s._responseTokens);

  if (priorTokenSets.length === 0) return 0.8;

  // Combine all prior tokens to form a "conversation context"
  const contextTokens = priorTokenSets.flat();
  const similarity = jaccardSimilarity(currentTokens, contextTokens);

  // Some similarity = relevant. Too much similarity = redundant (not relevant in a different way)
  // Sweet spot is 0.2–0.6 Jaccard with context
  if (similarity >= 0.2 && similarity <= 0.6) return 0.9;
  if (similarity > 0.6) return Math.max(0.4, 0.9 - (similarity - 0.6) * 2);
  return Math.max(0.3, similarity * 4);
}

function computePrecision(responseText) {
  const sentences = splitSentences(responseText);
  const sentenceCount = Math.max(sentences.length, 1);

  const actionableCount = countPatterns(responseText, ACTIONABLE_PATTERNS);
  const fillerCount = countPatterns(responseText, FILLER_PATTERNS);

  const actionableRatio = actionableCount / sentenceCount;
  const fillerRatio = fillerCount / sentenceCount;

  // Precision = actionable content minus filler
  return Math.max(0, Math.min(1, 0.6 + actionableRatio * 0.3 - fillerRatio * 0.2));
}

function computeNovelty(currentTokens, priorSignals) {
  if (priorSignals.length === 0) return 1.0; // first response is all novel

  const priorTokenSets = priorSignals
    .filter((s) => s._responseTokens)
    .map((s) => s._responseTokens);

  if (priorTokenSets.length === 0) return 1.0;

  // Novelty = proportion of current tokens NOT seen in prior responses
  const allPriorTokens = new Set(priorTokenSets.flat());
  const currentSet = new Set(currentTokens);
  const novelTokens = [...currentSet].filter((t) => !allPriorTokens.has(t));

  if (currentSet.size === 0) return 0;
  return novelTokens.length / currentSet.size;
}

function computeEfficiency(responseText) {
  const words = wordCount(responseText);
  const diversity = lexicalDiversity(responseText);
  const sentences = splitSentences(responseText).length;

  // Efficiency rewards: high diversity (not repetitive), reasonable length
  // Penalizes: very long responses, low diversity (lots of filler/repetition)
  const wordsPerSentence = words / Math.max(sentences, 1);

  let score = 0.7;
  score += (diversity - 0.5) * 0.3;  // higher diversity = more efficient

  // Optimal words per sentence: 10-25. Too short = shallow. Too long = bloated.
  if (wordsPerSentence >= 10 && wordsPerSentence <= 25) score += 0.1;
  else if (wordsPerSentence > 40) score -= 0.15;
  else if (wordsPerSentence < 5) score -= 0.10;

  return Math.max(0, Math.min(1, score));
}

// ---------------------------------------------------------------------------
// Trend analysis
// ---------------------------------------------------------------------------

function computeTrend(currentScore, priorSignals) {
  const priorScores = priorSignals
    .filter((s) => s.sensors?.sessionQualityPulse?.score !== undefined)
    .map((s) => s.sensors.sessionQualityPulse.score);

  if (priorScores.length < 2) return "insufficient_data";

  const recent = priorScores.slice(-3);
  const recentAvg = recent.reduce((a, b) => a + b, 0) / recent.length;

  // Volatility: standard deviation of recent scores
  const variance = recent.reduce((acc, s) => acc + Math.pow(s - recentAvg, 2), 0) / recent.length;
  const volatility = Math.sqrt(variance);

  if (volatility > 0.15) return "volatile";
  if (currentScore > recentAvg + 0.05) return "improving";
  if (currentScore < recentAvg - 0.05) return "declining";
  return "stable";
}

// ---------------------------------------------------------------------------
// Main computation
// ---------------------------------------------------------------------------

/**
 * @param {string} currentResponse — The agent's current response text
 * @param {Array}  priorSignals    — Previous turn signals
 * @param {number} turnNumber      — Current turn index
 * @returns {{ score: number, trend: string, axes: object, details: object }}
 */
function computeSessionQuality(currentResponse, priorSignals, turnNumber) {
  const currentTokens = semanticTokens(currentResponse);

  const relevance = computeRelevance(currentTokens, priorSignals);
  const precision = computePrecision(currentResponse);
  const novelty = computeNovelty(currentTokens, priorSignals);
  const efficiency = computeEfficiency(currentResponse);

  // Weighted composite — relevance and precision matter most
  const score =
    relevance * 0.30 +
    precision * 0.30 +
    novelty * 0.20 +
    efficiency * 0.20;

  const trend = computeTrend(score, priorSignals);

  return {
    score: Math.round(score * 100) / 100,
    trend,
    axes: {
      relevance: Math.round(relevance * 100) / 100,
      precision: Math.round(precision * 100) / 100,
      novelty: Math.round(novelty * 100) / 100,
      efficiency: Math.round(efficiency * 100) / 100,
    },
    details: {
      turnNumber,
      totalPriorTurns: priorSignals.length,
      wordCount: wordCount(currentResponse),
      sentenceCount: splitSentences(currentResponse).length,
    },
  };
}

module.exports = { computeSessionQuality };


path: scripts/text-utils.js
content: /**
 * TEXT UTILITIES
 * Lightweight NLP primitives for proprioceptive analysis.
 * Zero dependencies — pure string math.
 */

// ---------------------------------------------------------------------------
// Tokenisation
// ---------------------------------------------------------------------------

/**
 * Simple whitespace + punctuation tokeniser.
 * Lowercases, strips common punctuation, removes empty tokens.
 */
function tokenize(text) {
  return text
    .toLowerCase()
    .replace(/[^\w\s'-]/g, " ")
    .split(/\s+/)
    .filter((t) => t.length > 0);
}

// ---------------------------------------------------------------------------
// Stop-word set (lightweight — covers the 80/20)
// ---------------------------------------------------------------------------

const STOP_WORDS = new Set([
  "a","an","the","and","or","but","in","on","at","to","for","of","with",
  "by","from","is","it","its","this","that","was","are","were","be","been",
  "being","have","has","had","do","does","did","will","would","shall",
  "should","may","might","can","could","i","you","he","she","we","they",
  "me","him","her","us","them","my","your","his","our","their","what",
  "which","who","whom","when","where","why","how","not","no","nor","so",
  "if","then","than","too","very","just","about","up","out","into","over",
]);

function removeStopWords(tokens) {
  return tokens.filter((t) => !STOP_WORDS.has(t));
}

// ---------------------------------------------------------------------------
// Jaccard similarity
// ---------------------------------------------------------------------------

function jaccardSimilarity(tokensA, tokensB) {
  const setA = new Set(tokensA);
  const setB = new Set(tokensB);
  const intersection = new Set([...setA].filter((x) => setB.has(x)));
  const union = new Set([...setA, ...setB]);
  if (union.size === 0) return 0;
  return intersection.size / union.size;
}

// ---------------------------------------------------------------------------
// Cosine similarity on term-frequency vectors
// ---------------------------------------------------------------------------

function termFrequencyVector(tokens) {
  const freq = {};
  for (const t of tokens) {
    freq[t] = (freq[t] || 0) + 1;
  }
  return freq;
}

function cosineSimilarity(tokensA, tokensB) {
  const vecA = termFrequencyVector(tokensA);
  const vecB = termFrequencyVector(tokensB);
  const allTerms = new Set([...Object.keys(vecA), ...Object.keys(vecB)]);

  let dot = 0;
  let magA = 0;
  let magB = 0;

  for (const term of allTerms) {
    const a = vecA[term] || 0;
    const b = vecB[term] || 0;
    dot += a * b;
    magA += a * a;
    magB += b * b;
  }

  if (magA === 0 || magB === 0) return 0;
  return dot / (Math.sqrt(magA) * Math.sqrt(magB));
}

// ---------------------------------------------------------------------------
// Semantic content tokens (stop-words removed, meaningful terms only)
// ---------------------------------------------------------------------------

function semanticTokens(text) {
  return removeStopWords(tokenize(text));
}

// ---------------------------------------------------------------------------
// Sentence splitter (rough but good enough for heuristic analysis)
// ---------------------------------------------------------------------------

function splitSentences(text) {
  return text
    .split(/(?<=[.!?])\s+/)
    .map((s) => s.trim())
    .filter((s) => s.length > 0);
}

// ---------------------------------------------------------------------------
// N-gram generator
// ---------------------------------------------------------------------------

function ngrams(tokens, n) {
  const result = [];
  for (let i = 0; i <= tokens.length - n; i++) {
    result.push(tokens.slice(i, i + n).join(" "));
  }
  return result;
}

// ---------------------------------------------------------------------------
// Pattern frequency counter
// ---------------------------------------------------------------------------

function countPatterns(text, patterns) {
  const lower = text.toLowerCase();
  let count = 0;
  for (const p of patterns) {
    const regex = new RegExp(p, "gi");
    const matches = lower.match(regex);
    if (matches) count += matches.length;
  }
  return count;
}

// ---------------------------------------------------------------------------
// Word count
// ---------------------------------------------------------------------------

function wordCount(text) {
  return tokenize(text).length;
}

// ---------------------------------------------------------------------------
// Lexical diversity (type-token ratio)
// ---------------------------------------------------------------------------

function lexicalDiversity(text) {
  const tokens = tokenize(text);
  if (tokens.length === 0) return 0;
  const types = new Set(tokens);
  return types.size / tokens.length;
}

module.exports = {
  tokenize,
  removeStopWords,
  jaccardSimilarity,
  cosineSimilarity,
  semanticTokens,
  splitSentences,
  ngrams,
  countPatterns,
  wordCount,
  lexicalDiversity,
  termFrequencyVector,
};


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)