Detection Algorithm

A technical specification of the Behavioral Signal Aggregator (BSA) powering asshole-gc. Version 1.0. Last updated 2026-03-23.


Overview

asshole-gc uses a multi-pass Behavioral Signal Aggregator (BSA) to evaluate subjects across 14 weighted dimensions. The pipeline has four stages:

  1. Signal collection — passive observation, no active probing
  2. Dimension scoring — 14 weighted behavioral axes, rolling 72-hour window
  3. Consecutive day counting — the core threshold mechanism
  4. False positive suppression — context normalization before flagging

A subject is only flagged when all four conditions are met: threshold exceeded, confidence above minimum, false-positive score below ceiling, and at least one manual review pass (the --dry-run step).

Important: The algorithm is deliberately conservative. It is designed to surface subjects you already know about, not to surprise you. If the output surprises you, run --dry-run and review the evidence before proceeding.

Signal Collection

Signals are collected passively across the subject's observable behavioral surface. The system does not intercept private communications or require instrumentation. Observable surface includes:

Signals are timestamped and windowed. The rolling window is 72 hours by default, configurable via --window. Each signal has a weight, a context tag, and a decay function.

Signal structure
struct Signal {
    timestamp:   DateTime,
    dimension:   Dimension,   // which of the 14 axes
    weight:      f32,         // 0.0 – 1.0
    context:     Context,     // WorkStress | Personal | Deadline | Normal
    raw_text:    Option<String>,
}

14 Behavioral Dimensions

Each dimension is scored 0.0 (clean) to 1.0 (severe). Weights reflect how reliably each dimension predicts chronic vs. situational behavior.

# Dimension Weight Description
1 Condescension 0.90 Unsolicited correction, diminishing tone, "well, actually" density
2 Bad faith 0.90 Misrepresenting positions, strawmanning, goal-post movement
3 Weaponized incompetence 0.85 Strategic inability to perform tasks to avoid responsibility
4 Credit theft 0.85 Claiming others' work, omitting contributors, rewriting history
5 Chronic negativity 0.70 Consistent pessimism, complaint without constructive follow-through
6 Selective empathy 0.75 Empathy for peers/superiors, contempt for subordinates or strangers
7 Commitment failure 0.65 Repeated failure to follow through; no-show without acknowledgment
8 Boundary violation 0.88 Ignoring stated limits; re-asking after clear no
9 DARVO pattern 0.92 Deny, Attack, Reverse Victim and Offender when confronted
10 Energy drain coefficient 0.60 Subjective exhaustion reported by proximate observers after interactions
11 Escalation tendency 0.72 Disproportionate responses; minor friction → major conflict
12 Rules asymmetry 0.68 Standards applied to others but not self; double standards
13 Apology quality 0.65 Non-apology apologies ("sorry you feel that way"); conditional remorse
14 Pattern persistence 0.95 Recurrence after acknowledged correction. The defining signal.

BSA Scoring

The BSA computes a weighted aggregate across all 14 dimensions, normalized by the context weight of each signal. Context weights reduce scores during known stress periods (deadlines, personal crises) to avoid false positives from situational behavior.

Scoring pseudocode
fn bsa_score(signals: &[Signal]) -> f32 {
    let windowed = signals.filter(|s| s.within_window(72.hours()));

    let raw_score: f32 = windowed
        .group_by(|s| s.dimension)
        .map(|(dim, sigs)| {
            let dim_score = sigs.iter()
                .map(|s| s.weight * context_factor(s.context))
                .sum::<f32>()
                .min(1.0);
            dim_score * dim.weight
        })
        .sum();

    // Normalize to [0, 1]
    raw_score / DIMENSION_WEIGHT_SUM
}

Context factors:

ContextFactorNotes
Normal1.00Full weight
Work deadline0.70Reduced — stress artifacts
Personal crisis0.40Significantly reduced
Bereavement0.10Almost excluded

Consecutive Day Counter

The consecutive day counter is the core threshold mechanism. A "bad day" is defined as any 24-hour period in which the subject's BSA score exceeds 0.55 (configurable via --day-threshold).

The counter increments on bad days and resets on genuinely good days. A good day requires a score below 0.30 — not just below threshold. This prevents gaming via minimal compliance.

Design note: The reset threshold asymmetry (flag at 0.55, reset at 0.30) is intentional. It prevents the "one good day buys a week of bad behavior" pattern. To reset the counter, you have to actually be decent, not merely below-flagging.

Default threshold: 3 consecutive bad days. This was chosen because:


False Positive Suppression

The FP suppression layer runs after BSA scoring and before flagging. It applies three filters:

  1. Observer bias correction — if the observer reporting signals has a known conflict with the subject, dimension weights are reduced by 20%.
  2. Mirroring check — if the subject's flagged behaviors closely mirror behaviors exhibited by the reporting observer, confidence is reduced. Some people bring out the worst in each other.
  3. Single-source guard — signals from only one observer require a higher threshold (4 consecutive days instead of 3) before flagging.

The FP rate with --dry-run review is estimated at <2%. Without review, it is estimated at ~8%. Always use --dry-run.


Grace Period Logic

When --grace-period is passed, the subject receives a single clear notification before collection is executed. The grace period is 7 days by default (--grace-days N).

During the grace period, the counter continues running. If the subject demonstrates genuine improvement (score drops below reset threshold for 3+ consecutive days), the flag is cleared and they are moved to Status::Monitoring instead.

Warning: Grace periods can be gamed. The system tracks behavior during the grace period separately. A subject who improves only during the grace period and regresses afterward will have their grace eligibility revoked for 90 days.

Worked Examples

Example A: Dave (Engineering)

7 consecutive bad days · confidence 94% · Priority: HIGH
DimensionScoreKey signals
Condescension0.9123 unsolicited corrections in 7 days
DARVO pattern0.883 incidents of reverting blame on confrontation
Pattern persistence0.95Same behaviors for 14+ months
Credit theft0.722 documented omissions in shared presentations

Verdict: Flag. No false positive indicators. Grace period optional.

Example B: Casey (Slack)

3 consecutive bad days · confidence 41% · Status: MONITOR
DimensionScoreKey signals
Chronic negativity0.68High complaint volume this week
Escalation tendency0.42One overreaction in a tense meeting

Verdict: Monitor. Context: product launch week. Score likely situational. Check in 7 days.