mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-03 12:18:39 +00:00
* C0(bug3): SSOT canonical_objective_fingerprint + hoist _PLAN_REVIEW_SUFFIX
Leaf module ouroboros/evolution_fingerprint.py with the single objective-repeat
fingerprint (strip plan-review suffix, normalize, sha256[:16]) that all BUG3 layers
key off. Hoist the inline plan-review suffix from post_task_evolution into the shared
_PLAN_REVIEW_SUFFIX constant (byte-identical) so the append and the strip cannot drift.
Zero behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* C1(bug3): stamp per-cycle objective_fp into the evolution transaction
begin_evolution_transaction captures canonical_objective_fingerprint(campaign['objective'])
at cycle START into tx['objective_fp']. This is the SSOT per-cycle key the repeat counter
(C2) increments on and the absorb-clear pops; taken at cycle start because campaign['objective']
can be overwritten by a later promotion before the outcome is recorded. Sole production caller
is queue.py enqueue path, so every cycle (idle + post-task) is stamped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* C2(bug3): per-fingerprint objective-repeat counter + pause gate
Load-bearing fix. A cumulative per-fingerprint counter on the campaign dict (keyed on the
SSOT tx['objective_fp']) increments on every non-absorbing cycle and is cleared only on a
genuine absorb of that fingerprint. Unlike the consecutive-failures breaker (reset by ANY
non-failing cycle), this catches a blocked objective re-proposed NON-consecutively.
- helpers _bump/_clear_objective_repeat_count (setdefault-tolerant, skip empty fp)
- task-done outcome chain: bump on no_op/abandoned, clear on absorbed, waiting_for_restart pass
- ALL THREE durable absorb sites clear, and the durable abandoned / verified-no-commit sites
bump (catches commit-but-never-absorbs): agent_startup_checks boot-reconcile + restart-verify
- pause-only gate in enqueue_evolution_task_if_needed (after the 3-fail breaker, before the
next start) keyed on the SAME canonical_objective_fingerprint(campaign['objective']),
OBJECTIVE_REPEAT_CAP=3, distinct owner escalation, no objective rewrite
- replay test of /tmp/b3 e09bb702: counts[apikey] reaches 3 at cycle 10 (was capped at 2)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* C3(bug3) Layer B: detach + dropped-set on no_op/abandoned (never mark done)
A no_op/abandoned objective stays status=open in the backlog (the work is genuinely
unsolved); we only stop FEEDING it back to the evolution chooser. The bump/clear helpers now
also maintain campaign['dropped_objective_fps'] (Layer A input); the task-done abandoned
branch now pops post_task_backlog_id like no_op (it previously leaked the link into a later
absorbed cycle). close_backlog_items(done) stays exclusively at the absorb sites.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* C4(bug3) Layer A: feed CLOSED/DROPPED + active objective into the chooser prompt
The missing input that let a CLOSED objective be re-suggested. Adds two prompt sections and a
do-not-re-propose rule. _closed_objectives_digest reads the STRUCTURED ledger
(evolution_checkpoints.jsonl: cycle_outcome in {absorbed,abandoned,no_op} OR
outcome_axes.objective/review.outcome_tier=='blocked_with_evidence') — NOT patterns.md prose —
deduped by the SSOT fingerprint. P5-compatible: enriches context + a soft rule; the LLM still
owns the decision (the hard backstop is Layer C).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* G0(bug1/2): detached-HEAD -B fix + GIT_LOST_WORKTREE diagnostic (+cross-bug)
Applies the fix-proposal git.py.patch: on a detached HEAD, _repo_commit_push uses
`git checkout -B ctx.branch_dev HEAD` (preserve the in-flight commit) instead of a plain
checkout that orphans it; a clean tree right after that reconciliation returns a distinct
GIT_LOST_WORKTREE diagnostic instead of the misleading generic GIT_NO_CHANGES.
Cross-bug fix (council blocker): the diagnostic token now carries the _FAILED marker and
avoids the '⚠️ GIT_ERROR' prefix (which loop_tool_execution carves out to is_error=False), so
a lost-worktree cycle classifies as infra_failed and INCREMENTS evolution_consecutive_failures
instead of resetting it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* G0 §6(bug1-class): git_ops checkout -b -> -B + check=True (no silent swallow)
Same detached/branchless-HEAD class as BUG1, different surface (boot/UI reconcile). The
branch-absent path used `checkout -b` with check=False, which swallowed a 'branch exists'
error and proceeded with HEAD possibly detached/wrong; -B force-creates at HEAD and check=True
raises a real failure. (The post-checkout branch assert and the restart-verify branch assert
are documented as defense-in-depth follow-ups — they need test-aware wiring to not bypass the
git-command mocks.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* G0(a): integration test of the real _repo_commit_push detached-HEAD path
Exercises the actual ouroboros.tools.git code (not just the pure-git fix_verification.sh logic):
detached HEAD -> 'git checkout -B ouroboros HEAD' (never the orphaning plain checkout) and
came_from_detached_checkout=True into the stage cycle; on-branch -> plain checkout, no force-move.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Andrew <andgri200@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Ouroboros <ouroboros@local.mac>
108 lines
4.7 KiB
Python
108 lines
4.7 KiB
Python
"""C2 (BUG 3): the per-fingerprint cumulative objective-repeat counter.
|
|
|
|
The decisive property the prior consecutive-streak design FAILED: an objective that recurs
|
|
NON-consecutively (interleaved with other no_op work) must still accumulate to the pause
|
|
threshold. Replays the /tmp/b3 campaign e09bb702 sequence.
|
|
"""
|
|
from ouroboros.evolution_fingerprint import canonical_objective_fingerprint as fp
|
|
from supervisor.evolution_lifecycle import (
|
|
_bump_objective_repeat_count,
|
|
_clear_objective_repeat_count,
|
|
)
|
|
|
|
# Representative per-cycle objectives from campaign e09bb702.
|
|
APIKEY = "Add a pre-flight ANTHROPIC_API_KEY check at the start of commit_reviewed"
|
|
REGMAP = "Add a registration-map doc comment in registry.py"
|
|
REGISTRY = "Add a blocked_objective registry to the evolution cycle entry point"
|
|
DEVMD = "Create an Adding a New Tool checklist section in DEVELOPMENT.md"
|
|
|
|
|
|
def _tx(objective):
|
|
"""A transaction shaped like begin_evolution_transaction's output (only objective_fp matters)."""
|
|
return {"objective_fp": fp(objective)}
|
|
|
|
|
|
def _counts(campaign):
|
|
return campaign.get("objective_repeat_counts") or {}
|
|
|
|
|
|
def _dropped(campaign):
|
|
return campaign.get("dropped_objective_fps") or []
|
|
|
|
|
|
def test_replay_tmp_b3_reaches_threshold_3_on_nonconsecutive_recurrence():
|
|
"""The API-KEY objective recurs at cyc 5,7,10,11 with DIFFERENT objectives at 6,8,9.
|
|
A consecutive-streak counter peaks at 2; the cumulative map must reach 3 at cyc 10."""
|
|
c = {} # a campaign with no counter field yet (setdefault must tolerate it)
|
|
|
|
# cyc4 ABSORBED (before the loop window): clearing an absent fp is a safe no-op
|
|
_clear_objective_repeat_count(c, _tx(APIKEY))
|
|
# cyc5 no_op API-KEY
|
|
_bump_objective_repeat_count(c, _tx(APIKEY))
|
|
assert _counts(c)[fp(APIKEY)] == 1
|
|
# cyc6 no_op REG-MAP (interleaved — must NOT reset the API-KEY tally)
|
|
_bump_objective_repeat_count(c, _tx(REGMAP))
|
|
# cyc7 no_op API-KEY
|
|
_bump_objective_repeat_count(c, _tx(APIKEY))
|
|
assert _counts(c)[fp(APIKEY)] == 2
|
|
# cyc8 no_op REGISTRY, cyc9 no_op DEVMD (interleaved)
|
|
_bump_objective_repeat_count(c, _tx(REGISTRY))
|
|
_bump_objective_repeat_count(c, _tx(DEVMD))
|
|
# cyc10 no_op API-KEY -> reaches 3, the gate threshold, BEFORE cyc11 is spent
|
|
_bump_objective_repeat_count(c, _tx(APIKEY))
|
|
assert _counts(c)[fp(APIKEY)] == 3, "cumulative counter must catch the non-consecutive loop"
|
|
# the interleaved objectives each sit at 1 and never interfere
|
|
assert _counts(c)[fp(REGMAP)] == 1
|
|
assert _counts(c)[fp(REGISTRY)] == 1
|
|
assert _counts(c)[fp(DEVMD)] == 1
|
|
|
|
|
|
def test_absorb_clears_only_the_absorbed_fingerprint():
|
|
c = {}
|
|
_bump_objective_repeat_count(c, _tx(APIKEY))
|
|
_bump_objective_repeat_count(c, _tx(APIKEY))
|
|
_bump_objective_repeat_count(c, _tx(REGMAP))
|
|
_clear_objective_repeat_count(c, _tx(APIKEY)) # genuine absorb on API-KEY
|
|
assert fp(APIKEY) not in _counts(c) # its tally is gone
|
|
assert _counts(c)[fp(REGMAP)] == 1 # the other objective is untouched
|
|
|
|
|
|
def test_success_immunity_a_different_objective_does_not_clear_the_loop():
|
|
"""A non-failing/absorbed cycle on a DIFFERENT objective must NOT reset the looping one."""
|
|
c = {}
|
|
_bump_objective_repeat_count(c, _tx(APIKEY))
|
|
_bump_objective_repeat_count(c, _tx(APIKEY))
|
|
_clear_objective_repeat_count(c, _tx(REGMAP)) # some other objective absorbs
|
|
assert _counts(c)[fp(APIKEY)] == 2 # the loop tally survives
|
|
|
|
|
|
def test_setdefault_tolerates_a_campaign_persisted_before_the_field_existed():
|
|
c = {"status": "active", "objective": APIKEY} # no objective_repeat_counts key
|
|
_bump_objective_repeat_count(c, _tx(APIKEY)) # must not KeyError
|
|
assert _counts(c)[fp(APIKEY)] == 1
|
|
|
|
|
|
def test_empty_fingerprint_is_never_bucketed():
|
|
c = {}
|
|
_bump_objective_repeat_count(c, {"objective_fp": ""}) # tx-less / empty objective
|
|
_bump_objective_repeat_count(c, {}) # missing key entirely
|
|
assert _counts(c) == {}
|
|
assert _dropped(c) == []
|
|
|
|
|
|
def test_layer_b_bump_records_dropped_fp_deduped():
|
|
c = {}
|
|
_bump_objective_repeat_count(c, _tx(APIKEY))
|
|
_bump_objective_repeat_count(c, _tx(APIKEY)) # again -> still a single entry
|
|
_bump_objective_repeat_count(c, _tx(REGMAP))
|
|
assert _dropped(c).count(fp(APIKEY)) == 1 # deduped, not appended twice
|
|
assert fp(REGMAP) in _dropped(c)
|
|
|
|
|
|
def test_layer_b_absorb_undrops_only_the_absorbed_fp():
|
|
c = {}
|
|
_bump_objective_repeat_count(c, _tx(APIKEY))
|
|
_bump_objective_repeat_count(c, _tx(REGMAP))
|
|
_clear_objective_repeat_count(c, _tx(APIKEY)) # API-KEY landed -> no longer do-not-re-propose
|
|
assert fp(APIKEY) not in _dropped(c)
|
|
assert fp(REGMAP) in _dropped(c) # the other stays dropped
|