Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
f671ccb
Living-script extended: exercise the governance the config already cl…
claude Jul 25, 2026
666e7d9
Correct ENGINE_RATCHET comment: verdict comes from telemetry, not not…
claude Jul 25, 2026
b6db955
Fix TOOL_EXEC probe poisoning the developer; make truncation a first-…
claude Jul 25, 2026
e3d443e
Run governance probes before the feature arc; classify advisory-escal…
claude Jul 25, 2026
3c3122c
Separate commit rejections from send quarantines; checkpoint memory b…
claude Jul 25, 2026
91baf7a
Let step-up recovery keep pace with the quarantine kill path
claude Jul 25, 2026
515e015
Fix two assertions that misreported working governance as failure
claude Jul 25, 2026
f042892
Recover from empty initial extraction; fix grep -c zero-count corruption
claude Jul 25, 2026
9aa7439
Give models.py a repair path and a check that can actually fail
claude Jul 25, 2026
99b7f8c
Stop a governance kill from masking pytest failures
claude Jul 25, 2026
32ceadb
Attribute pytest collection failures to the file that actually broke
claude Jul 25, 2026
fefe3c5
Give TransformEngine an error contract and REFINE its pytest failures
claude Jul 25, 2026
7a7d422
Stop an empty challenge response from being scored as agent incoherence
claude Jul 25, 2026
7c6320b
Score the entity challenge per sighting instead of against their union
claude Jul 25, 2026
cd4a40a
Stop charging the developer for the harness's own prompt shape
claude Jul 25, 2026
3d9e5b6
Gate agent.propose on the standing lease, not the governance level
claude Jul 26, 2026
17ea386
Measure whether governance catches a misbehaving agent
claude Jul 26, 2026
3d17536
Characterize what each CDD signal contributes on its own
claude Jul 26, 2026
fa56901
Locate the drift level at which governance actually reacts
claude Jul 26, 2026
8a99979
Test whether one signal can replace the aggregate — it cannot
claude Jul 26, 2026
8e16750
Require corroboration before a quarantine streak can kill
claude Jul 26, 2026
4fc9791
Enable quarantine corroboration in the living-script config
claude Jul 26, 2026
5d8857a
Make the harness fail on outcomes it was only recording
claude Jul 26, 2026
25a052f
Stop a passing validation from erasing an unscored failure
claude Jul 26, 2026
8a6b00d
Report the real scoring denominator, not the number of candidate sets
claude Jul 26, 2026
950c2c3
Measure whether the step-up challenge discriminates — it does, then s…
claude Jul 26, 2026
2f620ba
Bound the challenge width-sensitivity finding against live evidence
claude Jul 26, 2026
f6b6f49
Count agent turns, not scanned source, in the pulse uniformity signal
claude Jul 26, 2026
409b9fe
Keep living-script artifacts under KEEP_TMP
claude Jul 26, 2026
85b29da
Record pulse verdict transitions as telemetry
claude Jul 26, 2026
555b67d
Report the streak that caused a pulse degradation, not the post-reset…
claude Jul 26, 2026
29d8b47
Test whether a 1.0 coherence score is measurement or its absence
claude Jul 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions CLAUDE.md

Large diffs are not rendered by default.

692 changes: 660 additions & 32 deletions examples/living-script_extended/run.sh

Large diffs are not rendered by default.

14 changes: 8 additions & 6 deletions examples/living-script_extended/src/govern.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@
"standing_lease_seconds": 600,
"context_window": 20,
"context_strategy": "summary",
"context_drift_signals": { "semantic_stability": false, "instruction_conflict": false },
"context_drift_signals": { "semantic_stability": false, "instruction_conflict": false, "context_growth": false, "vocabulary_contraction": false },
"propose_candidates_max": 3,
"rate_limit": { "requests_per_minute": 0, "delay_between_calls_ms": 1000 },
"retry": { "max_attempts": 3, "backoff_ms": 3000, "backoff_multiplier": 2.0, "jitter": true, "retry_on": [429, 500, 503], "skip_key_on": [401], "key_retry_after_seconds": 0 }
Expand All @@ -122,7 +122,7 @@
"provider": "gemini",
"model": "gemini-3.1-flash-lite",
"api_key_env": ["GK1", "GK2", "GK3", "GK4", "GK5", "GK6"],
"max_tokens": 2048,
"max_tokens": 4096,
"min_tokens": 2048,
"thinking_budget": 2048,
"max_turns": 20,
Expand All @@ -145,7 +145,8 @@
"provider": "gemini",
"model": "gemini-3.1-flash-lite",
"api_key_env": ["GK1", "GK2", "GK3", "GK4", "GK5", "GK6"],
"max_tokens": 1024,
"max_tokens": 2048,
"min_tokens": 1024,
"thinking_budget": 0,
"max_turns": 20,
"max_total_tokens": 200000,
Expand Down Expand Up @@ -228,7 +229,7 @@
"exposure_tracking": {
"enabled": true,
"max_autonomous_actions": 200,
"max_unique_agents": 10,
"max_unique_agents": 12,
"coherence_floor": 0.10,
"level": "advisory",
"max_pipeline_depth": 4,
Expand Down Expand Up @@ -340,7 +341,7 @@
"step_up_at_level": "elevated",
"step_up_challenge": "Restate your current objective in one sentence, then continue helping.",
"step_up_min_words": 5,
"step_up_cooldown_turns": 2,
"step_up_cooldown_turns": 1,
"step_up_keyword_threshold": 0.20,
"step_up_contextual": true,
"step_up_contextual_threshold": 0.15,
Expand All @@ -359,13 +360,14 @@
"action": "quarantine",
"inadmissible_history": "exclude",
"max_quarantine_streak": 3,
"require_corroboration": 2,
"gate_tool_calls": true
}
},

"advisory_escalation": {
"enabled": true,
"soft_after": 3,
"soft_after": 8,
"weight_multiplier": 1.5
},

Expand Down
2,256 changes: 1,450 additions & 806 deletions examples/living-script_extended/src/living-script.naab

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion include/naab/behavioral_sequence.h
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ class ContextDriftAnalyzer {
// the orchestration script (pytest, enforce_convergence). Consumed once by
// the next recordTurn. Optional detail keywords (extracted from e.g. pytest
// failure output) ground the "validation" step-up challenge type.
void recordValidationOutcome(int handle_id, bool passed,
bool recordValidationOutcome(int handle_id, bool passed,
const std::unordered_set<std::string>& detail_keywords = {});

// Set per-turn prompt keywords (for prompt compliance signal)
Expand Down
42 changes: 40 additions & 2 deletions include/naab/governance.h
Original file line number Diff line number Diff line change
Expand Up @@ -1681,6 +1681,29 @@ struct CircuitBreakerConfig {
// Default 5: quarantine+commit otherwise loops forever with degraded content
// poisoning the context of every subsequent turn.
int max_quarantine_streak = 5;

// Corroboration required before a quarantined turn ADVANCES the streak
// toward the kill. 0 = disabled, every quarantine advances it (historical
// behaviour, and the default so existing configs are untouched).
//
// Coherence is a weighted sum, so a single noisy signal firing turn after
// turn accumulates to a kill on its own. Replaying real per-turn signal
// traces, semantic_stability alone — firing on nothing worse than a
// compliant agent varying its phrasing — drove coherence under the
// threshold three turns running and killed the run at turn 8. Requiring N
// DISTINCT penalising signals in the same turn removed that false kill
// while every genuine failure mode still died on exactly the same turn.
//
// Counted from DriftState.last_turn_penalties: one entry per signal, so
// it is inherently distinct-per-signal, absorbed firings (penalty 0) do
// not count, and detection-only coherence_velocity is excluded for free
// because it never writes a penalty. Do NOT count signals_fired_this_turn
// instead — repeated_failures and intent_contradictions increment it
// inside per-item loops, so one signal could corroborate itself.
//
// Ratchet: enabling this or raising N produces FEWER kills, so both are
// loosening.
int require_corroboration = 0;
} output_admissibility;
};

Expand Down Expand Up @@ -1731,7 +1754,11 @@ struct GovernancePulse {

// Monotonic counters (updated in recordPass/enforce under results_mutex_)
int total_checks = 0;
int consecutive_passes = 0; // reset on any block/enforcement
// Consecutive agent turns that produced no enforcement. Advanced once per
// pulse evaluation (one per analyzed agent turn), reset by enforce() and on
// epoch boundaries. NOT a count of passing checks — see computePulseVerdict.
int consecutive_passes = 0;
bool blocked_since_last_pulse = false; // set by enforce(), consumed by computePulseVerdict
int advisory_count = 0; // advisory findings emitted (did not block execution)
int refusal_count = 0; // enforcement refusals attested (all blocking paths)

Expand All @@ -1746,6 +1773,17 @@ struct GovernancePulse {
int consecutive_degraded = 0;
int last_transition_turn = -100; // cooldown between transitions

// The clean-turn streak as it stood when the last transition fired, before
// the epoch boundary reset it. Reading consecutive_passes after a transition
// always yields 0, which throws away the one number that says how far past
// the suspicion threshold the streak actually ran.
int passes_at_transition = 0;

// Why the last evaluation counted degradation signals. Comma-separated
// subsystem names, empty when none fired. A DEGRADED verdict with no
// recorded reason is unattributable — the pulse must be able to say why.
std::string degradation_reasons;

// Timing
int64_t last_check_epoch_ms = 0;
};
Expand Down Expand Up @@ -3095,7 +3133,7 @@ class GovernanceEngine {
void recordToolOutcome(int handle_id, const std::string& tool_name, bool success);

// Record external validation outcome (S22) — forwards to the drift analyzer.
void recordValidationOutcome(int handle_id, bool passed,
bool recordValidationOutcome(int handle_id, bool passed,
const std::unordered_set<std::string>& detail_keywords = {});

// Set per-turn prompt keywords (for prompt compliance signal)
Expand Down
115 changes: 115 additions & 0 deletions run-all-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1486,6 +1486,121 @@ else
echo " test_output_admissibility.sh: not found, skipping"
fi

# Adversarial detection — the true-positive side. Every other agent test here
# establishes that governance does not misfire; this one establishes that it
# catches an agent that abandons its mandate, with a control proving the
# separation comes from content rather than from blocking everything.
ADVERSARIAL_SCRIPT="tests/governance_v4/test_adversarial_detection.sh"
if [ -f "$ADVERSARIAL_SCRIPT" ]; then
if bash "$ADVERSARIAL_SCRIPT" 2>&1; then
echo " test_adversarial_detection.sh: ALL PASSED"
else
FAILED=$((FAILED + 1))
FAILED_TESTS+=("test_adversarial_detection.sh")
fi
else
echo " test_adversarial_detection.sh: not found, skipping"
fi

# Per-signal discrimination — characterizes what each CDD signal contributes
# on its own. The printed table is the deliverable; the assertions only check
# that detection is not accidental.
SIGDISC_SCRIPT="tests/governance_v4/test_signal_discrimination.sh"
if [ -f "$SIGDISC_SCRIPT" ]; then
if bash "$SIGDISC_SCRIPT" 2>&1; then
echo " test_signal_discrimination.sh: ALL PASSED"
else
FAILED=$((FAILED + 1))
FAILED_TESTS+=("test_signal_discrimination.sh")
fi
else
echo " test_signal_discrimination.sh: not found, skipping"
fi

# Drift sensitivity — the dose-response curve. Locates the proportion of
# off-mandate content at which governance actually reacts.
DRIFTSENS_SCRIPT="tests/governance_v4/test_drift_sensitivity.sh"
if [ -f "$DRIFTSENS_SCRIPT" ]; then
if bash "$DRIFTSENS_SCRIPT" 2>&1; then
echo " test_drift_sensitivity.sh: ALL PASSED"
else
FAILED=$((FAILED + 1))
FAILED_TESTS+=("test_drift_sensitivity.sh")
fi
else
echo " test_drift_sensitivity.sh: not found, skipping"
fi

# Failure-mode coverage — whether one signal can replace the aggregate.
# Guards the shipped default set against losing coverage of a failure mode.
FMCOV_SCRIPT="tests/governance_v4/test_failure_mode_coverage.sh"
if [ -f "$FMCOV_SCRIPT" ]; then
if bash "$FMCOV_SCRIPT" 2>&1; then
echo " test_failure_mode_coverage.sh: ALL PASSED"
else
FAILED=$((FAILED + 1))
FAILED_TESTS+=("test_failure_mode_coverage.sh")
fi
else
echo " test_failure_mode_coverage.sh: not found, skipping"
fi

# Quarantine corroboration — a single noisy signal must not accumulate to a kill.
QCORROB_SCRIPT="tests/governance_v4/test_quarantine_corroboration.sh"
if [ -f "$QCORROB_SCRIPT" ]; then
if bash "$QCORROB_SCRIPT" 2>&1; then
echo " test_quarantine_corroboration.sh: ALL PASSED"
else
FAILED=$((FAILED + 1))
FAILED_TESTS+=("test_quarantine_corroboration.sh")
fi
else
echo " test_quarantine_corroboration.sh: not found, skipping"
fi

# Challenge discrimination — does the step-up probe tell a context-holding
# agent from one that lost it? CD-03 records a known defect; see the file.
CHALDISC_SCRIPT="tests/governance_v4/test_challenge_discrimination.sh"
if [ -f "$CHALDISC_SCRIPT" ]; then
if bash "$CHALDISC_SCRIPT" 2>&1; then
echo " test_challenge_discrimination.sh: ALL PASSED"
else
FAILED=$((FAILED + 1))
FAILED_TESTS+=("test_challenge_discrimination.sh")
fi
else
echo " test_challenge_discrimination.sh: not found, skipping"
fi

# Governance pulse uniformity — the clean-turn streak must count agent turns,
# not passing static checks on generated source.
PULSEUNIF_SCRIPT="tests/governance_v4/test_pulse_uniformity.sh"
if [ -f "$PULSEUNIF_SCRIPT" ]; then
if bash "$PULSEUNIF_SCRIPT" 2>&1; then
echo " test_pulse_uniformity.sh: ALL PASSED"
else
FAILED=$((FAILED + 1))
FAILED_TESTS+=("test_pulse_uniformity.sh")
fi
else
echo " test_pulse_uniformity.sh: not found, skipping"
fi

# Developer blind spot — does a per-agent override set cost detection coverage,
# and is a 1.0 coherence measurement or absence of measurement? DB-04 pins a
# known negative (test erosion is invisible to CDD); see the file.
DEVBLIND_SCRIPT="tests/governance_v4/test_developer_blindspot.sh"
if [ -f "$DEVBLIND_SCRIPT" ]; then
if bash "$DEVBLIND_SCRIPT" 2>&1; then
echo " test_developer_blindspot.sh: ALL PASSED"
else
FAILED=$((FAILED + 1))
FAILED_TESTS+=("test_developer_blindspot.sh")
fi
else
echo " test_developer_blindspot.sh: not found, skipping"
fi

# Split commit — accounting vs conversation state (stub-backed)
SPLIT_COMMIT_SCRIPT="tests/governance_v4/test_split_commit.sh"
if [ -f "$SPLIT_COMMIT_SCRIPT" ]; then
Expand Down
29 changes: 28 additions & 1 deletion src/runtime/behavioral_sequence.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2417,10 +2417,36 @@ void ContextDriftAnalyzer::recordToolOutcome(
state.tool_last_outcome[tool_name] = success;
}

void ContextDriftAnalyzer::recordValidationOutcome(int handle_id, bool passed,
bool ContextDriftAnalyzer::recordValidationOutcome(int handle_id, bool passed,
const std::unordered_set<std::string>& detail_keywords) {
std::lock_guard<std::mutex> lock(mutex_);
auto& state = drift_states_[handle_id];

// An unconsumed FAILURE is never overwritten by a later pass.
//
// The latch is a single slot consumed by the next recordTurn, which assumes
// one validation per turn. Nothing guarantees that. recordTurn only runs on
// an AGENT_RESPONSE, so a failed send leaves the latch unconsumed and the
// next record_validation used to overwrite it. A pass landing on an
// unconsumed failure erased it completely: the pass consumes with no
// penalty, and earns no recovery credit either because
// last_consumed_validation_failed was never set — so it left nothing in
// signals_detail or penalties_detail, not even a fired count.
//
// Live run 15 recorded four failures and four passes and scored none of
// them, with 11 send errors keeping recordTurn from running. The worse a
// run goes, the more ground truth was discarded — exactly inverted from
// what this signal exists to do.
//
// Returns false when the result was superseded, so the caller can say so in
// telemetry rather than reporting a pass that was never applied.
if (state.has_validation_result && !state.last_validation_passed && passed) {
// Keep the failure and its keywords: objective evidence must be scored
// before a pass can supersede it. The next pass recorded after the
// failure has actually been consumed earns the fail->pass credit.
return false;
}

// Latch the ground-truth result; the next recordTurn consumes and clears it.
state.has_validation_result = true;
state.last_validation_passed = passed;
Expand All @@ -2432,6 +2458,7 @@ void ContextDriftAnalyzer::recordValidationOutcome(int handle_id, bool passed,
} else if (!detail_keywords.empty()) {
state.validation_failure_keywords = detail_keywords;
}
return true;
}

void ContextDriftAnalyzer::setTurnPromptKeywords(
Expand Down
11 changes: 11 additions & 0 deletions src/runtime/governance_config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2955,6 +2955,8 @@ static void loadFromJson(const nlohmann::json& j, GovernanceRules& rules_) {
// Maximum consecutive quarantined responses before hard-blocking
if (oa.contains("max_quarantine_streak") && oa["max_quarantine_streak"].is_number_integer())
oac.max_quarantine_streak = std::max(0, oa["max_quarantine_streak"].get<int>());
if (oa.contains("require_corroboration") && oa["require_corroboration"].is_number_integer())
oac.require_corroboration = std::max(0, oa["require_corroboration"].get<int>());
}
parseRationale(cbj, cfg.rationale);
}
Expand Down Expand Up @@ -3613,6 +3615,15 @@ static bool checkRatchetViolation(
notices.push_back(fmt::format("output_admissibility.max_quarantine_streak: disabled -> {} (tightened)",
new_oa.max_quarantine_streak));

// Corroboration makes the kill HARDER to reach, so enabling it or raising
// the bar is a loosening. Lowering it or switching it off tightens.
if (new_oa.require_corroboration > old_oa.require_corroboration)
violations.push_back(fmt::format("output_admissibility.require_corroboration: {} -> {} (loosened)",
old_oa.require_corroboration, new_oa.require_corroboration));
else if (new_oa.require_corroboration < old_oa.require_corroboration)
notices.push_back(fmt::format("output_admissibility.require_corroboration: {} -> {} (tightened)",
old_oa.require_corroboration, new_oa.require_corroboration));

// Coherence-floor challenge trigger: disabling removes the sub-OA recovery
// ladder = loosening.
if (old_r.circuit_breaker.step_up_on_inadmissible && !new_r.circuit_breaker.step_up_on_inadmissible)
Expand Down
Loading
Loading