mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-02 19:58:46 +00:00
Preserve exact review and predecessor evidence through author continuation
This commit is contained in:
parent
d30cfbe1f5
commit
d15050c4c2
30 changed files with 523 additions and 77 deletions
|
|
@ -1,11 +1,11 @@
|
|||
# Mutation Attribution Rule
|
||||
|
||||
This chapter binds mutation evidence, staging and interpreter resolution so a reviewed commit does not claim another actor’s work. Attribution informs review; it is not exclusive ownership.
|
||||
Mutation evidence, staging and interpreter resolution keep commits from claiming another actor’s work. Attribution informs review; it is not exclusive ownership.
|
||||
|
||||
- Capture a `system_repo` baseline when a queued root starts and terminal candidates at outcome derivation. Pre-existing dirt, stale/missing baselines and failed scans are ambiguity for reviewing LLMs, not structural outcome vetoes. Keep this evidence in the task result; add no lease/holder service, second ledger or runtime writer keyword scanner.
|
||||
- The acceptance packet reads mutation evidence from the canonical results root (`budget_drive_root` first), the same root the writer and the outcome consumer use.
|
||||
- Git staging is attribution-based: `paths=None` means the clean-at-baseline candidate set plus explicitly adopted predecessor candidates; an explicit list must be its subset, and empty never means `git add -A`. Other pre-existing user dirt stays excluded evidence. Whole-tree staging belongs only to typed managed update/release transactions and the typed external patch-capture transaction; contexts without a captured baseline keep the legacy staging contract.
|
||||
- A later independent root may adopt in-place changes from its host-validated predecessor source. Keep original dirt and separate `predecessor_adoption` in the same evidence. Require terminal quiescence, the same repository, exact terminal content fingerprints and unchanged per-path base. Unrelated landed paths/dirt remain separate; missing or size-only fingerprints prove no transfer. Adoption grants attribution, never review approval, an automatic task or a reset of the old wallet.
|
||||
- A later independent root may adopt in-place changes through its admission/queue-validated predecessor source. Retain original dirt and separate `predecessor_adoption` evidence. Require terminal quiescence, same repo, exact content/Git-mode fingerprints and unchanged per-path base. Read config/index once per surface; honor `core.filemode` and indexed symlinks without index writes. Missing, size-only or unknown regular-file modes prove no transfer. Other landed work/dirt stays separate; absent legacy modes never block unrelated commits. Adoption grants attribution only, no review approval, automatic task or wallet reset.
|
||||
- Resolve unversioned Python only for `run_command`, `run_script`, `start_service` and run-kind `verify_and_record`, once BEFORE the shell guard, so guard and handler receive identical argv. Resolve bare Node for the same four surfaces but once AFTER the dispatch gates: the node health check executes an argv[0]-steered candidate, and probing before the gates would run a planted PATH shim for a request the fences would refuse. Never rewrite explicit paths, versioned interpreters, shell bodies or remote execution; never install a dependency in response to `ModuleNotFoundError`; with no usable runtime the argv runs as written and fails honestly (a rewritten absolute shebang is a disclosed residual).
|
||||
- Skill Review ordinals and provenance stay in `review_job.json` and the append-only `review_history.jsonl`: allocate under the lifecycle lock, consume a round only after actual start, write one terminal row per `job_id`, and compute legacy ordinals at read time without rewriting history.
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
AST-derived inventory of compatibility facades, regenerated by `python scripts/regenerate_inventories.py`. Do not edit. A facade row is any runtime module whose top-level `from <population module> import ...` statements carry the `noqa: F401` re-export marker — the codebase's declared "this binding exists for its binding, not for this module's own use" convention (reference FACADE_CONSUMERS method). Leaf domains come from `ouroboros/domains.toml`; a leaf outside the facade's domain is marked ✗ (that edge also appears in the manifest's pinned direction matrix). `tests/test_generated_inventories.py` pins byte-identity, so any re-export surface change must regenerate this file.
|
||||
|
||||
- facade modules: **58**; marked re-export bindings: **2273**; cross-domain facade→leaf pairs: **131**
|
||||
- facade modules: **58**; marked re-export bindings: **2272**; cross-domain facade→leaf pairs: **131**
|
||||
|
||||
| facade | domain | bindings | leaves |
|
||||
|---|---|---:|---|
|
||||
|
|
@ -19,7 +19,7 @@ AST-derived inventory of compatibility facades, regenerated by `python scripts/r
|
|||
| `ouroboros/headless.py` | D17 | 45 | `ouroboros/contracts/task_constraint.py` (1 ✗D19)<br>`ouroboros/headless_status.py` (11)<br>`ouroboros/workspace_patch_capture.py` (20)<br>`ouroboros/workspace_patch_rules.py` (13) |
|
||||
| `ouroboros/llm.py` | D02 | 92 | `ouroboros/anthropic_native_custody.py` (7)<br>`ouroboros/context_budget.py` (2 ✗D03)<br>`ouroboros/llm_anthropic.py` (2)<br>`ouroboros/llm_attempt.py` (22)<br>`ouroboros/llm_capability_policy.py` (5)<br>`ouroboros/llm_fallback.py` (1)<br>`ouroboros/llm_gigachat.py` (1)<br>`ouroboros/llm_local.py` (7)<br>`ouroboros/llm_messages.py` (1)<br>`ouroboros/llm_openai_compatible.py` (5)<br>`ouroboros/llm_pricing.py` (4)<br>`ouroboros/llm_routing.py` (3)<br>`ouroboros/openrouter_attribution.py` (1)<br>`ouroboros/provider_models.py` (7)<br>`ouroboros/request_wire_recovery.py` (7)<br>`ouroboros/transport_custody.py` (1)<br>`ouroboros/usage_accounting.py` (14 ✗D16)<br>`ouroboros/utils.py` (2 ✗D18) |
|
||||
| `ouroboros/loop.py` | D01 | 257 | `ouroboros/config.py` (6 ✗D12)<br>`ouroboros/context.py` (1 ✗D03)<br>`ouroboros/context_budget.py` (1 ✗D03)<br>`ouroboros/context_compaction.py` (2 ✗D03)<br>`ouroboros/context_fit.py` (2 ✗D03)<br>`ouroboros/deadline_utils.py` (2)<br>`ouroboros/llm.py` (3 ✗D02)<br>`ouroboros/loop_acceptance.py` (21)<br>`ouroboros/loop_acceptance_review.py` (29)<br>`ouroboros/loop_budget.py` (13)<br>`ouroboros/loop_delivery.py` (29)<br>`ouroboros/loop_forced_finalization.py` (22)<br>`ouroboros/loop_llm_call.py` (6)<br>`ouroboros/loop_messages.py` (10)<br>`ouroboros/loop_model_call.py` (19)<br>`ouroboros/loop_nudges.py` (16)<br>`ouroboros/loop_round_limits.py` (16)<br>`ouroboros/loop_tool_execution.py` (5)<br>`ouroboros/loop_transport.py` (10)<br>`ouroboros/nanny_pacing.py` (4 ✗D07)<br>`ouroboros/observability.py` (1 ✗D16)<br>`ouroboros/outcomes.py` (17)<br>`ouroboros/pricing.py` (1 ✗D02)<br>`ouroboros/review_cycles.py` (1 ✗D06)<br>`ouroboros/task_finalization.py` (3)<br>`ouroboros/task_pacing.py` (1)<br>`ouroboros/tool_policy.py` (4 ✗D04)<br>`ouroboros/usage_accounting.py` (5 ✗D16)<br>`ouroboros/utils.py` (3 ✗D18)<br>`supervisor/owner_stop.py` (4 ✗D09) |
|
||||
| `ouroboros/loop_acceptance.py` | D01 | 5 | `ouroboros/acceptance_settlement.py` (1)<br>`ouroboros/loop_messages.py` (4) |
|
||||
| `ouroboros/loop_acceptance.py` | D01 | 4 | `ouroboros/loop_messages.py` (4) |
|
||||
| `ouroboros/loop_llm_call.py` | D01 | 2 | `ouroboros/config.py` (2 ✗D12) |
|
||||
| `ouroboros/loop_nudges.py` | D01 | 1 | `ouroboros/skill_readiness.py` (1 ✗D14) |
|
||||
| `ouroboros/loop_round_limits.py` | D01 | 5 | `supervisor/owner_stop.py` (5 ✗D09) |
|
||||
|
|
|
|||
|
|
@ -473,8 +473,7 @@ class OuroborosAgent:
|
|||
from ouroboros.mutation_attribution import capture_mutation_baseline
|
||||
|
||||
predecessor = (task.get("predecessor_authority") or {}).get("source")
|
||||
if (not isinstance(predecessor, dict)
|
||||
or predecessor.get("task_id") != task.get("predecessor_task_id")):
|
||||
if not isinstance(predecessor, dict):
|
||||
predecessor = None
|
||||
capture_mutation_baseline(
|
||||
pathlib.Path(
|
||||
|
|
|
|||
|
|
@ -597,9 +597,7 @@ ACCEPTANCE_DECISION_REASONS = (
|
|||
"author_finish",
|
||||
"author_stop",
|
||||
"review_outcome_received",
|
||||
# The pacing/wallet reason two branches below already STAMP (`pass_reason ==
|
||||
# REASON_REVIEW_CYCLES_EXHAUSTED`); it was missing from the closed set, so a
|
||||
# spent shared cap shipped a reason no reader could validate.
|
||||
# An explicit author stop can retain the wallet's exhausted-cycle reason.
|
||||
REASON_REVIEW_CYCLES_EXHAUSTED,
|
||||
# A-material (2026-08-30): the resubmit carried no changed candidate and no new
|
||||
# obligation disposition, so the recorded verdict was replayed for free.
|
||||
|
|
@ -682,13 +680,11 @@ def merge_agent_acceptance_stance(trace: Dict[str, Any], decision: dict, ctx: An
|
|||
trace["acceptance_decision"] = merged
|
||||
|
||||
|
||||
from ouroboros.acceptance_settlement import expose_acceptance_feedback # noqa: E402,F401 - existing import surface
|
||||
|
||||
|
||||
def _collect_acceptance_obligations(llm_trace: Dict[str, Any], result: Any) -> None:
|
||||
"""Typed PER-TASK obligations from critical contributing findings (v6.54.4).
|
||||
|
||||
Required+blocking path only. Each critical finding WITH a concrete
|
||||
Blocking path after review eligibility. Each critical finding WITH a concrete
|
||||
recommendation becomes one open obligation in llm_trace (never the durable
|
||||
commit review_state — a separate SSOT). Clean finalization asks for an
|
||||
agent disposition per obligation (v6.54.0); time/pass gates and the
|
||||
|
|
|
|||
|
|
@ -711,7 +711,8 @@ def _finish_advisory_author(ctx: _TaskAcceptanceContext) -> bool:
|
|||
author = build_author_disposition(
|
||||
disposition=disposition, rationale=str(stance.get("agent_rationale") or ""),
|
||||
subject_hash=ctx.review_binding["binding_hash"],
|
||||
reviewer_signal=str((feedback or {}).get("aggregate_signal") or ""), enforcement=_loop().get_review_enforcement(),
|
||||
reviewer_signal=str((feedback or {}).get("aggregate_signal") or ""),
|
||||
enforcement="blocking" if review_enforcement_blocks(_loop().get_review_enforcement()) else "advisory",
|
||||
)
|
||||
author["action"] = action
|
||||
from ouroboros.task_results import project_task_acceptance_review_capacity
|
||||
|
|
@ -907,7 +908,7 @@ def _apply_task_acceptance_result(
|
|||
if capsule and open_obligations:
|
||||
_loop()._set_acceptance_decision(ctx.llm_trace, {
|
||||
"status": ACCEPTANCE_FINALIZED_UNACCEPTED,
|
||||
"reason": pass_reason if pass_reason == REASON_REVIEW_CYCLES_EXHAUSTED else "open_obligations",
|
||||
"reason": "open_obligations",
|
||||
"source": "task_acceptance_review",
|
||||
"rationale": (
|
||||
f"Improvement gates exhausted ({pass_reason or 'passes spent'}) with "
|
||||
|
|
@ -931,7 +932,6 @@ def _apply_task_acceptance_result(
|
|||
_loop()._set_acceptance_decision(ctx.llm_trace, {
|
||||
"status": ACCEPTANCE_FINALIZED_UNACCEPTED,
|
||||
"reason": (
|
||||
pass_reason if pass_reason == REASON_REVIEW_CYCLES_EXHAUSTED else
|
||||
"improvement_window_closed"
|
||||
if (not ctx.passes_done and pass_reason)
|
||||
else "capsule_spent"
|
||||
|
|
|
|||
|
|
@ -487,7 +487,7 @@ def _dispatch_round_model(
|
|||
binding = (waiter.register_reprepare(role, lambda kwargs: _reprepare_waiting_main(ctx, kwargs))
|
||||
if waiter is not None else contextlib.nullcontext())
|
||||
previous_call = ctx.accumulated_usage.get("_last_llm_call_meta")
|
||||
from ouroboros.loop_acceptance import expose_acceptance_feedback
|
||||
from ouroboros.acceptance_settlement import expose_acceptance_feedback
|
||||
|
||||
observe_feedback = lambda sent: expose_acceptance_feedback(
|
||||
getattr(ctx.tools._ctx, "_execution_trace", {}), sent, str(ctx.task_id))
|
||||
|
|
|
|||
|
|
@ -15,8 +15,10 @@ from __future__ import annotations
|
|||
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import stat
|
||||
import subprocess
|
||||
from typing import Any, Iterable, Mapping, Sequence
|
||||
|
||||
|
|
@ -159,6 +161,62 @@ def _path_fingerprint(path: pathlib.Path) -> dict[str, Any]:
|
|||
return {"kind": "other", "size": int(stat.st_size)}
|
||||
|
||||
|
||||
def _git_path_fingerprints(root: pathlib.Path, paths: Iterable[str]) -> dict[str, dict[str, Any]]:
|
||||
"""Capture content and ordinary Git staging modes with one config/index read.
|
||||
|
||||
Only regular files need an additional mode fact: symlink targets and absence
|
||||
already describe their Git type. Missing mode evidence remains unknown.
|
||||
"""
|
||||
fingerprints = {path: _path_fingerprint(root / path) for path in paths}
|
||||
files = {path: row for path, row in fingerprints.items() if row.get("kind") == "file"}
|
||||
if not files:
|
||||
return fingerprints
|
||||
for row in files.values():
|
||||
row["git_mode"] = None
|
||||
try:
|
||||
config = subprocess.run(
|
||||
["git", "config", "--type=bool", "--null", "--get-regexp", r"^core\.(filemode|symlinks)$"],
|
||||
cwd=str(root), capture_output=True, text=True, check=False,
|
||||
)
|
||||
if config.returncode not in (0, 1):
|
||||
raise RuntimeError("Git mode configuration unavailable")
|
||||
options = dict(row.split("\n", 1) for row in config.stdout.split("\0") if row)
|
||||
modes, unmerged = {}, set()
|
||||
for entry in _run_git(root, "ls-files", "--stage", "-z").split("\0"):
|
||||
if not entry:
|
||||
continue
|
||||
metadata, path = entry.split("\t", 1)
|
||||
mode, _oid, stage = metadata.split()
|
||||
if path in files:
|
||||
if stage == "0":
|
||||
modes[path] = mode
|
||||
else:
|
||||
unmerged.add(path)
|
||||
for path, row in files.items():
|
||||
if path in unmerged:
|
||||
continue
|
||||
indexed = modes.get(path)
|
||||
if options.get("core.symlinks", "true") == "false" and indexed == "120000":
|
||||
row["git_mode"] = indexed
|
||||
elif options.get("core.filemode", "true") == "false":
|
||||
row["git_mode"] = indexed if indexed in {"100644", "100755"} else "100644"
|
||||
else:
|
||||
row["git_mode"] = "100755" if (root / path).lstat().st_mode & stat.S_IXUSR else "100644"
|
||||
except (OSError, RuntimeError, ValueError):
|
||||
logging.getLogger(__name__).debug("Git fingerprint mode unavailable", exc_info=True)
|
||||
return fingerprints
|
||||
|
||||
|
||||
def _foreign_fingerprint_matches(previous: Any, current: Any) -> bool:
|
||||
"""Unknown legacy mode must not globally block already-excluded foreign WIP."""
|
||||
if not isinstance(previous, dict) or not isinstance(current, dict):
|
||||
return False
|
||||
if previous.get("git_mode") is not None and current.get("git_mode") is not None:
|
||||
return previous == current
|
||||
return ({key: value for key, value in previous.items() if key != "git_mode"}
|
||||
== {key: value for key, value in current.items() if key != "git_mode"})
|
||||
|
||||
|
||||
def _normalize_known_paths(root: pathlib.Path, values: Iterable[Any]) -> list[str]:
|
||||
normalized: list[str] = []
|
||||
for value in values:
|
||||
|
|
@ -220,9 +278,7 @@ def _capture_surface(surface: Mapping[str, Any]) -> dict[str, Any]:
|
|||
"base_commit": _run_git(root, "rev-parse", "HEAD").strip(),
|
||||
"base_tree": _run_git(root, "rev-parse", "HEAD^{tree}").strip(),
|
||||
"dirty_paths": dirty_paths,
|
||||
"dirty_fingerprints": {
|
||||
path: _path_fingerprint(root / path) for path in dirty_paths
|
||||
},
|
||||
"dirty_fingerprints": _git_path_fingerprints(root, dirty_paths),
|
||||
}
|
||||
return row
|
||||
row["known_path_fingerprints"] = {
|
||||
|
|
@ -382,6 +438,8 @@ def _predecessor_git_changes(
|
|||
if (path not in changed_base and path in (retained.get("candidates") or [])
|
||||
and isinstance(fingerprint, dict)
|
||||
and (fingerprint.get("sha256") or fingerprint.get("kind") in {"missing", "symlink"})
|
||||
and (fingerprint.get("kind") != "file"
|
||||
or fingerprint.get("git_mode") in {"100644", "100755", "120000"})
|
||||
and fingerprint == (git.get("dirty_fingerprints") or {}).get(path)):
|
||||
adoption["paths"].append(path)
|
||||
return adoption
|
||||
|
|
@ -566,9 +624,10 @@ def attributed_git_candidates(
|
|||
excluded = sorted(path for path in changed if path in dirty)
|
||||
candidates = sorted(path for path in changed if path not in dirty)
|
||||
# A pre-existing dirty path is excluded evidence either way; it becomes a
|
||||
# blocker only when its content actually CHANGED during the observed window
|
||||
# blocker only when known content/mode CHANGED during the observed window
|
||||
# (unchanged owner WIP merely persisting must not wedge the task's commits).
|
||||
if any(_path_fingerprint(root / path) != fingerprints.get(path) for path in excluded):
|
||||
current_fingerprints = _git_path_fingerprints(root, excluded)
|
||||
if any(not _foreign_fingerprint_matches(fingerprints.get(path), current_fingerprints[path]) for path in excluded):
|
||||
blockers.append("preexisting_dirty_changed")
|
||||
effect_state = str(evidence.get("effect_state") or "")
|
||||
if effect_state not in _OBSERVED_EFFECT_STATES:
|
||||
|
|
@ -683,8 +742,13 @@ def record_terminal_mutation_candidates(
|
|||
dirty = _foreign_dirty_paths(git)
|
||||
fingerprints = git.get("dirty_fingerprints") or {}
|
||||
excluded = sorted(path for path in changed if path in dirty)
|
||||
try:
|
||||
current_fingerprints = _git_path_fingerprints(root, changed)
|
||||
except OSError:
|
||||
current_fingerprints = {}
|
||||
blockers.append("candidate_fingerprint_unavailable")
|
||||
if any(
|
||||
_path_fingerprint(root / path) != fingerprints.get(path)
|
||||
not _foreign_fingerprint_matches(fingerprints.get(path), current_fingerprints.get(path))
|
||||
for path in excluded
|
||||
):
|
||||
blockers.append("preexisting_dirty_changed")
|
||||
|
|
@ -697,11 +761,7 @@ def record_terminal_mutation_candidates(
|
|||
if isinstance(flag_row, dict) and str(flag_row.get("flag") or "")
|
||||
)
|
||||
candidates = sorted(path for path in changed if path not in dirty)
|
||||
candidate_fingerprints = {}
|
||||
try:
|
||||
candidate_fingerprints = {path: _path_fingerprint(root / path) for path in candidates}
|
||||
except OSError:
|
||||
blockers.append("candidate_fingerprint_unavailable")
|
||||
candidate_fingerprints = {path: current_fingerprints[path] for path in candidates if path in current_fingerprints}
|
||||
row.update({
|
||||
"candidates": candidates,
|
||||
"candidate_fingerprints": candidate_fingerprints,
|
||||
|
|
@ -845,9 +905,7 @@ def advance_mutation_baseline(
|
|||
"base_commit": _run_git(root, "rev-parse", "HEAD").strip(),
|
||||
"base_tree": _run_git(root, "rev-parse", "HEAD^{tree}").strip(),
|
||||
"dirty_paths": still_dirty,
|
||||
"dirty_fingerprints": {
|
||||
path: _path_fingerprint(root / path) for path in still_dirty
|
||||
},
|
||||
"dirty_fingerprints": _git_path_fingerprints(root, still_dirty),
|
||||
**({"predecessor_adoption": {
|
||||
**row["git"]["predecessor_adoption"],
|
||||
"paths": sorted(set(row["git"]["predecessor_adoption"].get("paths") or []) & set(still_dirty)),
|
||||
|
|
|
|||
|
|
@ -722,7 +722,7 @@ TASK_CAUSE_PHRASES = {
|
|||
# accepted decision with a sentence here still states its cause.
|
||||
"previous_revision_accepted": "The reviewers approved the earlier version of this answer; it changed before they finished.",
|
||||
"author_stop": "Main stopped with unfinished work; no review approval was granted.",
|
||||
"review_outcome_received": "The review could not start; Main received the recorded reason.",
|
||||
"review_outcome_received": "Main received the review outcome or recorded limitation.",
|
||||
"author_finish": "The answer was delivered on Main's own judgement; the reviewers had not signed it off.",
|
||||
"review_degraded": "No reviewer verdict was established for this answer.",
|
||||
"infra_failure": "A review infrastructure failure prevented a settled verdict.",
|
||||
|
|
|
|||
|
|
@ -1035,7 +1035,7 @@ def _settle_review_attempt(
|
|||
if entry.released_early: # plan review's event route: progress line + the settled-wave frame
|
||||
from ouroboros.tools.plan_review_collect import announce_released_settlement
|
||||
|
||||
announce_released_settlement(usage_ctx, request=request, task_id=task_id, slot=slot, actor=actor,
|
||||
announce_released_settlement(usage_ctx, request=request, task_id=task_id, actor=actor,
|
||||
settled_wave=dict(released_wave.get("slots") or {}), roster_size=int(released_wave.get("total") or 0))
|
||||
if getattr(request, "surface", "") == "task_acceptance" and (released_wave or quorum_wave):
|
||||
from ouroboros.acceptance_settlement import announce_acceptance_settlement
|
||||
|
|
|
|||
|
|
@ -210,8 +210,8 @@ def reconcile_pending_acceptance_runs(
|
|||
log.warning("acceptance run %s could not be reconciled: %s",
|
||||
str(run.get("panel_id") or "")[:16], exc)
|
||||
continue
|
||||
# Keep the paid operation's request; only its producer facts advance.
|
||||
run.update({key: value for key, value in vars(result).items() if key != "request"})
|
||||
# Keep the host panel identity and paid request; only producer facts advance.
|
||||
run.update({key: value for key, value in vars(result).items() if key not in {"request", "panel_id"}})
|
||||
advanced += not acceptance_run_pending(run)
|
||||
return advanced
|
||||
|
||||
|
|
|
|||
|
|
@ -145,7 +145,14 @@ def build_review_status_payload(projection: Dict[str, Any], *, next_step: str, i
|
|||
}
|
||||
if selected_attempt is not None and selected_attempt.phase in {"review_only", "late_wait"}:
|
||||
from ouroboros.config import get_review_enforcement
|
||||
if get_review_enforcement() == "advisory":
|
||||
from ouroboros.review_records import review_outcome_received
|
||||
|
||||
received = review_outcome_received(
|
||||
[*selected_attempt.triad_raw_results, selected_attempt.scope_raw_result],
|
||||
findings=[*selected_attempt.critical_findings, *selected_attempt.advisory_findings],
|
||||
terminal=selected_attempt.phase == "review_only" and selected_attempt.status == "reviewed",
|
||||
)
|
||||
if get_review_enforcement() == "advisory" and received:
|
||||
payload["review_reference"] = {"surface": "commit", **{key: getattr(selected_attempt, key) for key in
|
||||
("repo_key", "task_id", "tool_name", "attempt", "pre_review_fingerprint")}}
|
||||
payload["next_step"] = ("Read the returned findings. You may revise and request another permitted review, stop, or call "
|
||||
|
|
|
|||
|
|
@ -446,8 +446,8 @@ def retired_setting_keys_notice(dropped: tuple[str, ...], *, reviewer_slots: tup
|
|||
elif state == "invalid":
|
||||
panel = (
|
||||
"NO reviewer panel: that setting is malformed, so reviews are refused "
|
||||
"(commit review blocks; under Advisory enforcement it warns and commits "
|
||||
"unreviewed) until it is repaired on the Settings page — %s" % parse_error)
|
||||
"(Blocking prevents committing; Advisory returns the failure for an explicit "
|
||||
"author decision) until it is repaired on the Settings page — %s" % parse_error)
|
||||
else:
|
||||
panel = "the SHIPPED default reviewer panel until that setting is authored (Settings page)"
|
||||
clauses.append(
|
||||
|
|
|
|||
|
|
@ -1090,7 +1090,7 @@ def _acceptance_rails_line_inner(
|
|||
"(deadline/budget rails bind)"
|
||||
)
|
||||
else:
|
||||
passes_part = f"review passes: {int(passes_done)}/{int(cap)}"
|
||||
passes_part = f"author passes: {int(passes_done)}/{int(cap)}"
|
||||
# v6.74.4 freeze directive (count axis): the pass launched at
|
||||
# cap-1 is the last one improvement_pass_allowed will admit, so
|
||||
# say so. cap==0 never feeds a capsule back; skip the clause, and
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ log = logging.getLogger(__name__)
|
|||
|
||||
|
||||
def announce_released_settlement(
|
||||
usage_ctx: Any, *, request: Any, task_id: str, slot: Any, actor: Any,
|
||||
usage_ctx: Any, *, request: Any, task_id: str, actor: Any,
|
||||
settled_wave: Dict[str, str], roster_size: int = 0,
|
||||
) -> None:
|
||||
"""Attach late evidence and announce whole-wave settlement through its mailbox.
|
||||
|
|
|
|||
|
|
@ -1333,7 +1333,7 @@ def _dispatch_unified_review(ctx: ToolContext, commit_message: str, prepared: di
|
|||
)
|
||||
return _handle_review_block_or_warning(
|
||||
ctx, blocking_review, blocked_msg,
|
||||
"Review enforcement=Advisory: review infrastructure failure did not block commit. ",
|
||||
"Review enforcement=Advisory: review infrastructure failed; an explicit author decision is required. ",
|
||||
)
|
||||
|
||||
if "error" in result:
|
||||
|
|
@ -1347,7 +1347,7 @@ def _dispatch_unified_review(ctx: ToolContext, commit_message: str, prepared: di
|
|||
)
|
||||
return _handle_review_block_or_warning(
|
||||
ctx, blocking_review, blocked_msg,
|
||||
"Review enforcement=Advisory: review service error did not block commit. ",
|
||||
"Review enforcement=Advisory: review service failed; an explicit author decision is required. ",
|
||||
)
|
||||
|
||||
model_results = result.get("results", [])
|
||||
|
|
@ -1359,7 +1359,7 @@ def _dispatch_unified_review(ctx: ToolContext, commit_message: str, prepared: di
|
|||
"model — commit cannot proceed without a successful review.")
|
||||
return _handle_review_block_or_warning(
|
||||
ctx, blocking_review, blocked_msg,
|
||||
"Review enforcement=Advisory: review returned no model results; commit proceeding anyway. ")
|
||||
"Review enforcement=Advisory: no model results were received; an explicit author decision is required. ")
|
||||
|
||||
critical_fails, advisory_warns, errored_models, _triad_raw = _collect_review_findings(ctx, model_results)
|
||||
models_total = len(model_results)
|
||||
|
|
@ -1381,7 +1381,7 @@ def _dispatch_unified_review(ctx: ToolContext, commit_message: str, prepared: di
|
|||
f"{', '.join(pending_models)}. Retry the same commit to reconcile them without a blind paid resend.")
|
||||
pending_block = _handle_review_block_or_warning(
|
||||
ctx, blocking_review, blocked_msg,
|
||||
"Review enforcement=Advisory: pending review work did not block commit. ",
|
||||
"Review enforcement=Advisory: review is pending; collect its outcome before choosing an author continuation. ",
|
||||
)
|
||||
if pending_block is not None:
|
||||
return pending_block
|
||||
|
|
@ -1401,7 +1401,7 @@ def _dispatch_unified_review(ctx: ToolContext, commit_message: str, prepared: di
|
|||
)
|
||||
return _handle_review_block_or_warning(
|
||||
ctx, blocking_review, blocked_msg,
|
||||
"Review enforcement=Advisory: review quorum failure did not block commit. ",
|
||||
"Review enforcement=Advisory: review quorum was not met; an explicit author decision is required. ",
|
||||
)
|
||||
|
||||
if models_total < 2:
|
||||
|
|
@ -1440,14 +1440,10 @@ def _dispatch_unified_review(ctx: ToolContext, commit_message: str, prepared: di
|
|||
ctx,
|
||||
("Cyber Pro: critical review findings do not prohibit action."
|
||||
if not review_enforcement_blocks("blocking") else
|
||||
"Review enforcement=Advisory: critical review findings did not block commit."),
|
||||
"Review enforcement=Advisory: critical findings require an explicit author decision before committing."),
|
||||
)
|
||||
for finding in getattr(ctx, "_last_review_critical_findings", []) or []:
|
||||
_append_review_warning(ctx, finding)
|
||||
for warning in getattr(ctx, "_last_review_advisory_findings", []) or []:
|
||||
_append_review_warning(ctx, warning)
|
||||
if errored_note:
|
||||
_append_review_warning(ctx, errored_note)
|
||||
|
||||
if not critical_fails:
|
||||
# All clear: reset iteration state. With critical findings present
|
||||
|
|
|
|||
|
|
@ -291,3 +291,41 @@ def test_legacy_settled_review_replays_its_proven_subject_without_key_error(actu
|
|||
assert h.trace["review_runs"][-1]["actors"] == old_actors
|
||||
assert h.trace["acceptance_decision"]["reason"] != "infra_failure"
|
||||
assert "KeyError" not in json.dumps(h.trace.get("acceptance_decision") or {})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("runtime,enforcement", [("pro", "advisory"), ("cyber_pro", "advisory"), ("cyber_pro", "blocking")])
|
||||
@pytest.mark.parametrize("explicit", [False, True])
|
||||
def test_task_author_record_preserves_configured_and_effective_authority(actual_acceptance, monkeypatch, runtime, enforcement, explicit):
|
||||
import inspect
|
||||
from ouroboros import config
|
||||
from ouroboros.acceptance_settlement import expose_acceptance_feedback
|
||||
from ouroboros.outcomes import _objective_axis
|
||||
from ouroboros.project_dialogue import outcome_phase
|
||||
|
||||
h = actual_acceptance
|
||||
# Obtain a real independent outcome in ordinary Advisory first.
|
||||
h.run("Initial answer")
|
||||
original = copy.deepcopy(h.trace["review_runs"][-1])
|
||||
expose_acceptance_feedback(h.trace, inspect.getclosurevars(h.run).nonlocals["messages"], "task")
|
||||
if explicit or runtime == "pro":
|
||||
h.annotate()
|
||||
config.reset_runtime_mode_baseline_for_tests()
|
||||
config.initialize_runtime_mode_baseline(runtime)
|
||||
monkeypatch.setenv("OUROBOROS_REVIEW_ENFORCEMENT", enforcement)
|
||||
try:
|
||||
h.run("Verified revised answer")
|
||||
decision = h.trace["acceptance_decision"]
|
||||
assert decision["enforcement"] == enforcement
|
||||
assert decision["author_disposition"]["enforcement"] == "advisory"
|
||||
assert decision["reason"] == "author_finish"
|
||||
assert h.trace["review_runs"][0]["actors"] == original["actors"]
|
||||
review = {"status": "fail", "acceptance_decision": decision}
|
||||
objective = _objective_axis(review)
|
||||
row = {"status": "completed", "outcome_axes": {
|
||||
"objective": objective, "review": review, "execution": {"status": "ok"}}}
|
||||
assert objective["status"] == "pass"
|
||||
assert outcome_phase(row, {}) == "done"
|
||||
row["outcome_axes"]["execution"]["status"] = "failed"
|
||||
assert outcome_phase(row, {}) == "error"
|
||||
finally:
|
||||
config.reset_runtime_mode_baseline_for_tests()
|
||||
|
|
|
|||
183
tests/test_git_mode_attribution.py
Normal file
183
tests/test_git_mode_attribution.py
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
"""Exact predecessor custody follows ordinary Git modes, not raw permissions."""
|
||||
import copy
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from ouroboros import mutation_attribution as attribution
|
||||
from ouroboros.task_results import load_task_result, write_task_result
|
||||
from tests.test_mutation_attribution import _git, _repo
|
||||
from tests.test_predecessor_mutation_handoff import _previous, _source, _start
|
||||
|
||||
|
||||
@pytest.mark.parametrize("filemode", [True, False])
|
||||
def test_foreign_git_mode_is_not_adopted(tmp_path, filemode):
|
||||
if filemode and os.name == "nt":
|
||||
pytest.skip("filesystem executable-bit probe; index-mode case covers Windows")
|
||||
root, data = _previous(tmp_path)
|
||||
_git(root, "config", "core.filemode", str(filemode).lower())
|
||||
if filemode:
|
||||
(root / "clean.txt").chmod(0o755)
|
||||
else:
|
||||
_git(root, "update-index", "--chmod=+x", "clean.txt")
|
||||
index = (root / ".git/index").read_bytes()
|
||||
_start(data, root, "successor", _source())
|
||||
selected, evidence, error = attribution.resolve_attributed_git_paths(data, "successor", root, None)
|
||||
assert selected == ["new.txt"] and not error
|
||||
assert evidence["excluded_preexisting_dirty"] == ["clean.txt", "dirty.txt"]
|
||||
assert (root / ".git/index").read_bytes() == index, "observation must preserve staged work"
|
||||
if filemode:
|
||||
_git(root, "add", "--", *selected)
|
||||
assert "mode change" not in _git(root, "diff", "--cached", "--summary")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("filemode", [True, False])
|
||||
def test_predecessor_executable_correction_keeps_its_mode(tmp_path, filemode):
|
||||
if filemode and os.name == "nt":
|
||||
pytest.skip("filesystem executable-bit probe; index-mode case covers Windows")
|
||||
root, data = _previous(tmp_path)
|
||||
_git(root, "config", "core.filemode", str(filemode).lower())
|
||||
if filemode:
|
||||
(root / "clean.txt").chmod(0o755)
|
||||
else:
|
||||
_git(root, "update-index", "--chmod=+x", "clean.txt")
|
||||
terminal = attribution.record_terminal_mutation_candidates(data, "previous")
|
||||
assert terminal["terminal_candidate_snapshot"]["surfaces"][0]["candidate_fingerprints"]["clean.txt"]["git_mode"] == "100755"
|
||||
_start(data, root, "successor", _source())
|
||||
selected, _, error = attribution.resolve_attributed_git_paths(data, "successor", root, None)
|
||||
assert selected == ["clean.txt", "new.txt"] and not error
|
||||
_git(root, "add", "--", *selected)
|
||||
assert "mode change 100644 => 100755 clean.txt" in _git(root, "diff", "--cached", "--summary")
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="requires meaningful POSIX chmod")
|
||||
@pytest.mark.parametrize("filemode,mode", [(True, 0o654), (True, 0o645), (False, 0o755)])
|
||||
def test_git_ignored_permission_changes_preserve_handoff(tmp_path, filemode, mode):
|
||||
root, data = _previous(tmp_path)
|
||||
_git(root, "config", "core.filemode", str(filemode).lower())
|
||||
(root / "clean.txt").chmod(mode)
|
||||
_start(data, root, "successor", _source())
|
||||
selected, _, error = attribution.resolve_attributed_git_paths(data, "successor", root, None)
|
||||
assert selected == ["clean.txt", "new.txt"] and not error
|
||||
_git(root, "add", "--", *selected)
|
||||
assert "mode change" not in _git(root, "diff", "--cached", "--summary")
|
||||
|
||||
|
||||
def test_git_mode_capture_is_batched_and_read_only(tmp_path, monkeypatch):
|
||||
root = _repo(tmp_path)
|
||||
_git(root, "config", "core.filemode", "false")
|
||||
paths = ["clean.txt", "dirty.txt"] + [f"new-{i}.txt" for i in range(20)]
|
||||
for path in paths[2:]:
|
||||
(root / path).write_text("new\n", encoding="utf-8")
|
||||
index_before = hashlib.sha256((root / ".git/index").read_bytes()).hexdigest()
|
||||
calls, run = [], attribution.subprocess.run
|
||||
|
||||
def observe(argv, **kwargs):
|
||||
calls.append(argv)
|
||||
return run(argv, **kwargs)
|
||||
|
||||
monkeypatch.setattr(attribution.subprocess, "run", observe)
|
||||
fingerprints = attribution._git_path_fingerprints(root, paths)
|
||||
assert len(calls) == 2, "one config and one index read, independent of path count"
|
||||
assert all(row["sha256"] and row["git_mode"] == "100644" for row in fingerprints.values())
|
||||
assert hashlib.sha256((root / ".git/index").read_bytes()).hexdigest() == index_before
|
||||
|
||||
|
||||
def test_indexed_symlink_mode_matches_git_on_a_regular_file(tmp_path):
|
||||
root = _repo(tmp_path)
|
||||
_git(root, "config", "core.symlinks", "false")
|
||||
link = root / "link"
|
||||
link.write_text("clean.txt", encoding="utf-8")
|
||||
blob = _git(root, "hash-object", "-w", "link")
|
||||
_git(root, "update-index", "--add", "--cacheinfo", f"120000,{blob},link")
|
||||
link.write_text("dirty.txt", encoding="utf-8")
|
||||
fingerprint = attribution._git_path_fingerprints(root, ["link"])["link"]
|
||||
assert fingerprint["kind"] == "file" and fingerprint["git_mode"] == "120000"
|
||||
_git(root, "add", "--", "link")
|
||||
assert _git(root, "ls-files", "--stage", "link").startswith("120000 ")
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="requires meaningful POSIX chmod")
|
||||
def test_predecessor_executable_addition_is_retained(tmp_path):
|
||||
root, data = _previous(tmp_path)
|
||||
_git(root, "config", "core.filemode", "true")
|
||||
(root / "new.txt").chmod(0o744)
|
||||
attribution.record_terminal_mutation_candidates(data, "previous")
|
||||
_start(data, root, "successor", _source())
|
||||
selected, _, error = attribution.resolve_attributed_git_paths(data, "successor", root, None)
|
||||
assert selected == ["clean.txt", "new.txt"] and not error
|
||||
_git(root, "add", "--", *selected)
|
||||
assert "create mode 100755 new.txt" in _git(root, "diff", "--cached", "--summary")
|
||||
|
||||
|
||||
def test_new_files_deletions_and_symlinks_keep_exact_handoff(tmp_path):
|
||||
root, data = _previous(tmp_path)
|
||||
(root / "clean.txt").unlink()
|
||||
try:
|
||||
(root / "link").symlink_to("new.txt")
|
||||
except OSError:
|
||||
pytest.skip("native symlink creation unavailable")
|
||||
attribution.record_terminal_mutation_candidates(data, "previous")
|
||||
_start(data, root, "successor", _source())
|
||||
selected, _, error = attribution.resolve_attributed_git_paths(data, "successor", root, None)
|
||||
assert selected == ["clean.txt", "link", "new.txt"] and not error
|
||||
_git(root, "add", "--", *selected)
|
||||
summary = _git(root, "diff", "--cached", "--summary")
|
||||
assert "delete mode 100644 clean.txt" in summary
|
||||
assert "create mode 120000 link" in summary and "create mode 100644 new.txt" in summary
|
||||
|
||||
|
||||
def test_legacy_mode_is_not_invented_for_exact_handoff(tmp_path):
|
||||
root, data = _previous(tmp_path)
|
||||
old = copy.deepcopy(load_task_result(data, "previous")["mutation_evidence"])
|
||||
for row in old["terminal_candidate_snapshot"]["surfaces"][0]["candidate_fingerprints"].values():
|
||||
row.pop("git_mode", None)
|
||||
write_task_result(data, "previous", "completed", mutation_evidence=old)
|
||||
_start(data, root, "successor", _source())
|
||||
assert attribution.attributed_git_candidates(data, "successor", root)["candidates"] == []
|
||||
assert load_task_result(data, "previous")["mutation_evidence"] == old
|
||||
|
||||
|
||||
def test_legacy_excluded_wip_does_not_block_other_paths_or_epoch_advance(tmp_path):
|
||||
root, data = _previous(tmp_path)
|
||||
evidence = _start(data, root, "independent")
|
||||
for row in evidence["baseline"]["surfaces"][0]["git"]["dirty_fingerprints"].values():
|
||||
row.pop("git_mode", None)
|
||||
write_task_result(data, "independent", "running", mutation_evidence=evidence)
|
||||
(root / "mine.txt").write_text("independent work\n", encoding="utf-8")
|
||||
selected, _, error = attribution.resolve_attributed_git_paths(data, "independent", root, None)
|
||||
assert selected == ["mine.txt"] and not error
|
||||
_git(root, "add", "--", *selected)
|
||||
_git(root, "commit", "-qm", "fixture independent commit")
|
||||
advanced = attribution.advance_mutation_baseline(data, "independent", root)
|
||||
dirty = advanced["baseline"]["surfaces"][0]["git"]["dirty_fingerprints"]
|
||||
assert all(row["git_mode"] == "100644" for row in dirty.values())
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="requires meaningful POSIX chmod")
|
||||
def test_foreign_mode_change_is_visible_at_current_and_terminal_comparison(tmp_path):
|
||||
root, data = _previous(tmp_path)
|
||||
_git(root, "config", "core.filemode", "true")
|
||||
_start(data, root, "independent")
|
||||
(root / "dirty.txt").chmod(0o755)
|
||||
assert "preexisting_dirty_changed" in attribution.attributed_git_candidates(data, "independent", root)["blockers"]
|
||||
terminal = attribution.record_terminal_mutation_candidates(data, "independent")
|
||||
assert "preexisting_dirty_changed" in terminal["terminal_candidate_snapshot"]["surfaces"][0]["blockers"]
|
||||
|
||||
|
||||
def test_unknown_mode_cannot_authorize_a_regular_file_transfer(tmp_path, monkeypatch):
|
||||
root, data = _previous(tmp_path)
|
||||
run_git = attribution._run_git
|
||||
|
||||
def unreadable_index(path, *args):
|
||||
if args[0] == "ls-files":
|
||||
raise RuntimeError("index unavailable")
|
||||
return run_git(path, *args)
|
||||
|
||||
monkeypatch.setattr(attribution, "_run_git", unreadable_index)
|
||||
terminal = attribution.record_terminal_mutation_candidates(data, "previous")
|
||||
assert terminal["terminal_candidate_snapshot"]["surfaces"][0]["candidate_fingerprints"]["clean.txt"]["git_mode"] is None
|
||||
monkeypatch.setattr(attribution, "_run_git", run_git)
|
||||
_start(data, root, "successor", _source())
|
||||
assert attribution.attributed_git_candidates(data, "successor", root)["candidates"] == []
|
||||
|
|
@ -178,7 +178,7 @@ class TestReviewEnforcementModes:
|
|||
result = review._run_unified_review(ctx, "test commit", repo_dir=ctx.repo_dir)
|
||||
assert result is None
|
||||
assert any(
|
||||
isinstance(w, str) and "critical review findings did not block commit" in w.lower()
|
||||
isinstance(w, str) and "critical findings require an explicit author decision" in w.lower()
|
||||
for w in ctx._review_advisory
|
||||
)
|
||||
assert any(
|
||||
|
|
@ -195,13 +195,18 @@ class TestReviewEnforcementModes:
|
|||
self._mock_staged(monkeypatch, review, changed_files="x.py")
|
||||
monkeypatch.setenv("OUROBOROS_REVIEW_ENFORCEMENT", "advisory")
|
||||
ctx._review_advisory = ["prior deterministic/preflight warning"]
|
||||
monkeypatch.setattr(review, "_handle_multi_model_review", lambda *a, **kw: self._fake_result(
|
||||
response = json.loads(self._fake_result(
|
||||
'[{"item":"contract","verdict":"FAIL","severity":"critical","reason":"material original finding"}]',
|
||||
'[{"item":"style","verdict":"FAIL","severity":"advisory","reason":"minor original finding"}]'))
|
||||
response["results"].append({"model": "failed-critic", "error": "Transport unavailable"})
|
||||
monkeypatch.setattr(review, "_handle_multi_model_review", lambda *a, **kw: json.dumps(response))
|
||||
assert review._run_unified_review(ctx, "candidate", repo_dir=ctx.repo_dir) is None
|
||||
saved = json.dumps(ctx._review_advisory)
|
||||
assert "prior deterministic/preflight warning" in saved
|
||||
assert "material original finding" in saved and "minor original finding" in saved
|
||||
assert saved.count("material original finding") == 1
|
||||
assert saved.count("minor original finding") == 1
|
||||
assert saved.count("prior deterministic/preflight warning") == 1
|
||||
assert saved.count("Note: 1 of 3 review models") == 1
|
||||
|
||||
@pytest.mark.parametrize("failure", ["nonzero_rc", "non_utf8_rc"])
|
||||
def test_uncapturable_staged_diff_blocks_instead_of_reviewing_a_placeholder(
|
||||
|
|
|
|||
|
|
@ -81,6 +81,8 @@ _ROUTE_ENV_NAMES = (
|
|||
"OUROBOROS_OBSERVABILITY_KEEP_RAW",
|
||||
"OUROBOROS_MODEL_ACCOUNTS",
|
||||
"OUROBOROS_MODEL_CONTEXT_WINDOWS",
|
||||
"OUROBOROS_PROCESSING_PREFERENCE",
|
||||
"OUROBOROS_MODEL_PROCESSING_PREFERENCES",
|
||||
)
|
||||
|
||||
# Class-level caches LLMClient uses as process-global memory. Reset per case.
|
||||
|
|
@ -714,6 +716,23 @@ def test_llm_provider_route_matches_golden(case):
|
|||
)
|
||||
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", ["", "main"])
|
||||
def test_processing_environment_isolated_but_explicit_case_options_apply(monkeypatch, role):
|
||||
case = next(case for case in _CASES if case["id"] == "openai.dispatch.happy_path")
|
||||
spec = copy.deepcopy(case["spec"])
|
||||
if role:
|
||||
spec["call"]["kwargs"]["model_role"] = role
|
||||
monkeypatch.setenv("OUROBOROS_PROCESSING_PREFERENCE", "fast")
|
||||
monkeypatch.setenv("OUROBOROS_MODEL_PROCESSING_PREFERENCES", '{"main":"economy"}')
|
||||
assert _observe(spec) == case["expected"]
|
||||
key = "OUROBOROS_MODEL_PROCESSING_PREFERENCES" if role else "OUROBOROS_PROCESSING_PREFERENCE"
|
||||
spec.setdefault("env", {})[key] = '{"main":"standard"}' if role else "standard"
|
||||
explicit = _observe(spec)
|
||||
assert explicit["sends"][0]["payload"]["service_tier"] == "default"
|
||||
assert explicit["returned"]["usage"]["processing"]["requested"] == "standard"
|
||||
|
||||
|
||||
def test_golden_covers_every_declared_provider_lane():
|
||||
"""Coverage floor: dropping a lane's fixtures must fail, not go unnoticed."""
|
||||
covered = {case["id"].split(".", 1)[0] for case in _CASES}
|
||||
|
|
|
|||
|
|
@ -108,7 +108,8 @@ def test_every_host_acceptance_writer_emits_a_canonical_status_and_typed_reason(
|
|||
# cannot escape the guard by living in (or moving to) a leaf.
|
||||
loop_file = pathlib.Path(loop_mod.__file__)
|
||||
src = []
|
||||
for path in [loop_file, *sorted(loop_file.parent.glob("loop_*.py"))]:
|
||||
for path in [loop_file, *sorted(loop_file.parent.glob("loop_*.py")),
|
||||
loop_file.parent / "acceptance_settlement.py"]:
|
||||
src.extend(path.read_text(encoding="utf-8").splitlines())
|
||||
starts = [
|
||||
i for i, line in enumerate(src)
|
||||
|
|
@ -116,7 +117,7 @@ def test_every_host_acceptance_writer_emits_a_canonical_status_and_typed_reason(
|
|||
]
|
||||
# Include the separate infrastructure-outcome handback; it requests an
|
||||
# author response without manufacturing a critic capsule or reviewer PASS.
|
||||
assert len(starts) == 21, f"writer inventory changed: {len(starts)} call sites"
|
||||
assert len(starts) == 22, f"writer inventory changed: {len(starts)} call sites"
|
||||
allowed_status = {
|
||||
"ACCEPTANCE_ACCEPTED", "ACCEPTANCE_REVISION_REQUESTED",
|
||||
"ACCEPTANCE_FINALIZED_UNACCEPTED",
|
||||
|
|
@ -148,7 +149,7 @@ def test_every_host_acceptance_writer_emits_a_canonical_status_and_typed_reason(
|
|||
seen_expression_reasons += 1
|
||||
assert reason_names[name] in ACCEPTANCE_DECISION_REASONS, name
|
||||
# The widened regex really does catch expression-valued reasons: the two
|
||||
# `pass_reason if ... == REASON_REVIEW_CYCLES_EXHAUSTED` branches and the
|
||||
# explicit author-stop REASON_REVIEW_CYCLES_EXHAUSTED branches and the
|
||||
# A-material `REASON_IDENTICAL_ACCEPTANCE_REFUSED` writer.
|
||||
assert seen_expression_reasons >= 3, seen_expression_reasons
|
||||
|
||||
|
|
|
|||
|
|
@ -473,7 +473,7 @@ def test_required_blocking_unbounded_loop_collapses_to_zero_under_hurry(tmp_path
|
|||
rails = tp_mod.acceptance_rails_line(
|
||||
snapshot, effective, 0, None, required_blocking=True,
|
||||
)
|
||||
assert "review passes: 0/0" in rails
|
||||
assert "author passes: 0/0" in rails
|
||||
assert "no local count cap" not in rails
|
||||
# An unlatched ctx passes the profile through UNCHANGED (identity).
|
||||
unlatched = _acceptance_ctx(tmp_path, latched=False)
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ def test_a_b_late_a_preserves_authority_and_complete_source(tmp_path):
|
|||
before = state(tmp_path)
|
||||
actor, text = complete(tmp_path, a, req, slots[0])
|
||||
ctx = SimpleNamespace(drive_root=tmp_path, task_id=TASK, emit_progress_fn=lambda text: None)
|
||||
collect.announce_released_settlement(ctx, request=req, task_id=TASK, slot=slots[0],
|
||||
collect.announce_released_settlement(ctx, request=req, task_id=TASK,
|
||||
actor=actor, settled_wave={})
|
||||
after = state(tmp_path)
|
||||
old = tr.plan_review_wave(after, A)
|
||||
|
|
|
|||
|
|
@ -134,15 +134,16 @@ def test_real_startup_passes_validated_predecessor_to_baseline(tmp_path):
|
|||
|
||||
root, data = _previous(tmp_path)
|
||||
task = {"id": "successor", "root_task_id": "successor", "budget_drive_root": str(data),
|
||||
"predecessor_task_id": "previous", "predecessor_authority_source": _source()}
|
||||
"predecessor_authority_source": _source()}
|
||||
assert not validate_task_authority_sources(data, task)
|
||||
write_task_result(data, "successor", "running")
|
||||
agent = SimpleNamespace(env=SimpleNamespace(repo_dir=root, drive_root=data, budget_drive_root=str(data)))
|
||||
OuroborosAgent._capture_mutation_baseline(agent, task, {})
|
||||
assert attributed_git_candidates(data, "successor", root)["candidates"] == ["clean.txt", "new.txt"]
|
||||
# Merely carrying old context is not an explicit selection by this task.
|
||||
unselected = {**task, "id": "unselected", "root_task_id": "unselected"}
|
||||
unselected.pop("predecessor_task_id")
|
||||
unselected = {"id": "unselected", "root_task_id": "unselected", "budget_drive_root": str(data),
|
||||
"metadata": {"project_last_task_result": {"task_id": "previous"}}}
|
||||
write_task_result(data, "unselected", "running")
|
||||
assert not validate_task_authority_sources(data, unselected)
|
||||
OuroborosAgent._capture_mutation_baseline(agent, unselected, {})
|
||||
assert attributed_git_candidates(data, "unselected", root)["candidates"] == []
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ def test_post_review_finish_handles_revised_answer_without_another_panel(monkeyp
|
|||
)
|
||||
assert run("initial answer") is True
|
||||
critic_hash = trace["review_runs"][-1]["binding_hash"]
|
||||
from ouroboros.loop_acceptance import expose_acceptance_feedback
|
||||
from ouroboros.acceptance_settlement import expose_acceptance_feedback
|
||||
expose_acceptance_feedback(trace, messages, "author-root")
|
||||
trace["tool_calls"].append({"tool": "task_acceptance_review", "args": {}})
|
||||
merge_agent_acceptance_stance(trace, {"disposition": "partial", "explicit_finish": change != "ordinary_evidence", "rationale": "I fixed the material issue."}, tools_ctx)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ from types import SimpleNamespace
|
|||
import pytest
|
||||
|
||||
from ouroboros import loop, review_substrate, task_pacing
|
||||
from ouroboros.loop_acceptance import expose_acceptance_feedback, merge_agent_acceptance_stance
|
||||
from ouroboros.acceptance_settlement import expose_acceptance_feedback
|
||||
from ouroboros.loop_acceptance import merge_agent_acceptance_stance
|
||||
from ouroboros.loop_acceptance_review import _apply_task_acceptance_result
|
||||
from ouroboros.review_records import ReviewRunResult
|
||||
from ouroboros.task_results import effective_task_acceptance_review_cycles
|
||||
|
|
|
|||
94
tests/test_review_wave_identity.py
Normal file
94
tests/test_review_wave_identity.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
"""A collected host review keeps one identity across durable publications."""
|
||||
import copy
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
|
||||
|
||||
def test_one_operation_through_pending_and_settled_publications(tmp_path, monkeypatch):
|
||||
from ouroboros import review_custody, review_dispatch, review_projection
|
||||
from ouroboros.loop_acceptance_review import _record_host_acceptance_run, acceptance_run_pending
|
||||
from ouroboros.review_records import ReviewRequest, ReviewSlot
|
||||
from ouroboros.review_substrate import run_review_request
|
||||
from ouroboros.task_results import load_task_result, merge_review_projection
|
||||
from tests.test_loop_acceptance_gate import _seed_acceptance_root
|
||||
|
||||
entered, release, settled = threading.Event(), threading.Event(), threading.Event()
|
||||
calls, stamps = [], []
|
||||
real_settle = review_custody._settle_review_attempt
|
||||
|
||||
def settle(*args, **kwargs):
|
||||
try:
|
||||
return real_settle(*args, **kwargs)
|
||||
finally:
|
||||
settled.set()
|
||||
|
||||
monkeypatch.setattr(review_custody, '_settle_review_attempt', settle)
|
||||
|
||||
class HeldModel:
|
||||
def chat(self, **kwargs):
|
||||
# A recording LLM bypasses the real API accounting boundary, so
|
||||
# explicitly invoke its already-bound dispatch stamp there.
|
||||
review_dispatch.invoke_bound_api_review_paid_stamp()
|
||||
calls.append(kwargs)
|
||||
entered.set()
|
||||
assert release.wait(10)
|
||||
return {'content': json.dumps({'verdict': 'FAIL', 'summary': 'Actual retained criticism',
|
||||
'findings': []})}, {'prompt_tokens': 5, 'completion_tokens': 2}
|
||||
|
||||
ctx = SimpleNamespace(task_id='panel-audit', task_attempt=1, drive_root=tmp_path,
|
||||
budget_drive_root=tmp_path, task_metadata={}, pending_events=[], event_queue=None)
|
||||
_seed_acceptance_root(tmp_path, ctx.task_id, ctx)
|
||||
ctx._review_paid_stamp = review_dispatch.ReviewPaidStamp(lambda: stamps.append('physical'))
|
||||
request = ReviewRequest(surface='task_acceptance', task_id=ctx.task_id, task_attempt=1,
|
||||
goal='Audit publication identity', subject='The same complete answer',
|
||||
evidence={'requirement': 'same'}, retry_key='panel-audit:paid-one', drain_deadline=time.monotonic())
|
||||
slot = ReviewSlot(slot_id='one', model='model/original', effort='high', timeout_sec=20)
|
||||
trace = {'review_runs': []}
|
||||
snapshots = []
|
||||
try:
|
||||
first = run_review_request(request, slots=[slot], drive_root=tmp_path, usage_ctx=ctx, llm=HeldModel())
|
||||
assert entered.wait(5) and acceptance_run_pending(first)
|
||||
binding = review_projection.build_review_binding(candidate=request.subject, evidence=request.evidence,
|
||||
fence_token_or_state='same fence')
|
||||
binding['paid_identity'] = 'paid-one'
|
||||
owner = SimpleNamespace(tools=SimpleNamespace(_ctx=ctx), llm_trace=trace, review_binding=binding)
|
||||
run = _record_host_acceptance_run(owner, first)
|
||||
|
||||
def publish():
|
||||
review_projection.publish_acceptance_checkpoint(ctx, trace)
|
||||
saved = load_task_result(tmp_path, ctx.task_id)
|
||||
snapshots.append(copy.deepcopy(saved['review_projection']))
|
||||
return saved
|
||||
|
||||
publish()
|
||||
advanced = review_dispatch.reconcile_pending_acceptance_runs(trace, drive_root=tmp_path, usage_ctx=ctx)
|
||||
assert advanced == 0 and acceptance_run_pending(run)
|
||||
publish()
|
||||
release.set()
|
||||
assert settled.wait(5)
|
||||
advanced = review_dispatch.reconcile_pending_acceptance_runs(trace, drive_root=tmp_path, usage_ctx=ctx)
|
||||
assert advanced == 1 and not acceptance_run_pending(run)
|
||||
saved = publish()
|
||||
assert len(calls) == len(stamps) == 1
|
||||
assert len(trace['review_runs']) == 1
|
||||
rows = saved['review_projection']['panels']
|
||||
assert len(rows) == 1
|
||||
assert [len(p["panels"]) for p in snapshots] == [1, 1, 1]
|
||||
operations = {actor['operation_id'] for panel in rows for actor in panel['actors']}
|
||||
assert len(operations) == 1 and run['actors'][0]['operation_id'] in operations
|
||||
assert run['actors'][0]['parsed']['verdict'] == 'FAIL'
|
||||
assert rows[-1]['publication_revision'] == 3
|
||||
assert rows[0]['panel_id'] == binding['panel_id']
|
||||
# Neither another logical record at the same binding (host error,
|
||||
# rebuttal/new wave) nor another task attempt is collapsed.
|
||||
second = {**rows[0], 'panel_index': 1, 'publication_revision': 4}
|
||||
next_attempt = {**rows[0], 'task_attempt': 2, 'publication_revision': 1}
|
||||
merged = merge_review_projection(saved['review_projection'], {'panels': [second, next_attempt]})
|
||||
assert len(merged['panels']) == 3
|
||||
finally:
|
||||
release.set()
|
||||
assert settled.wait(5)
|
||||
|
|
@ -393,7 +393,7 @@ def test_capsule_leads_with_verdict_blocker_rails_and_three_moves():
|
|||
result = _fail_result_with([_finding()])
|
||||
capsule = build_improvement_capsule(
|
||||
result,
|
||||
rails_line="money: $1.00 spent; time: 10 min left; review passes: 1 done",
|
||||
rails_line="money: $1.00 spent; time: 10 min left; author passes: 1 done",
|
||||
open_obligations=[{"id": "ob-1"}, {"id": "ob-2"}],
|
||||
)
|
||||
# The note the model READS says the assessment in words: a ledger token in
|
||||
|
|
@ -725,7 +725,7 @@ def test_rails_final_pass_freeze_directive_workspace():
|
|||
# rides EVERY workspace rails line (commit triad r1, sol: a deadline/cost
|
||||
# rail can end the loop between capsules), and never a non-workspace one.
|
||||
line = _rails(5, cap=6, workspace=True)
|
||||
assert "review passes: 5/6" in line
|
||||
assert "author passes: 5/6" in line
|
||||
assert "FINAL improvement pass, no further passes will run" in line
|
||||
assert "working tree as it stands" in line and "VERIFIED state" in line
|
||||
# Non-workspace: factual finality only, no tree directive.
|
||||
|
|
@ -738,15 +738,15 @@ def test_rails_freeze_directive_absent_off_final_and_edge_caps(monkeypatch):
|
|||
# Non-final pass: no FINAL marker, but the workspace tree directive is
|
||||
# always present for workspace deliveries.
|
||||
mid = _rails(3, cap=6, workspace=True)
|
||||
assert "FINAL" not in mid and "review passes: 3/6" in mid
|
||||
assert "FINAL" not in mid and "author passes: 3/6" in mid
|
||||
assert "working tree as it stands" in mid
|
||||
assert "working tree" not in _rails(3, cap=6, workspace=False)
|
||||
# cap == 0 never feeds a capsule back — no misleading FINAL rail.
|
||||
zero = _rails(0, cap=0, workspace=True)
|
||||
assert "review passes: 0/0" in zero and "FINAL" not in zero
|
||||
assert "author passes: 0/0" in zero and "FINAL" not in zero
|
||||
# Passes already exhausted (supersede-reset re-review): not a launch.
|
||||
spent = _rails(6, cap=6, workspace=True)
|
||||
assert "review passes: 6/6" in spent and "FINAL" not in spent
|
||||
assert "author passes: 6/6" in spent and "FINAL" not in spent
|
||||
# No local cap (required+blocking with the shared review-cycle cap set to
|
||||
# unlimited — D10/D20: otherwise the shared cap binds) — no count-axis clause.
|
||||
monkeypatch.setenv("OUROBOROS_REVIEW_MAX_CYCLES", "unlimited")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"""Real two-task Git continuation: saved Blocking correction earns fresh authority."""
|
||||
from types import SimpleNamespace
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
|
@ -14,7 +15,49 @@ from ouroboros.tools import git
|
|||
from ouroboros.tools.registry import ToolContext
|
||||
from ouroboros.tools.scope_review import ScopeReviewResult
|
||||
from tests.test_mutation_attribution import _git, _repo
|
||||
from tests.test_predecessor_mutation_handoff import _source
|
||||
|
||||
|
||||
def _promote_retained_correction(root, data, monkeypatch):
|
||||
from ouroboros.server_routing_context import _task_result_ground_truth
|
||||
from ouroboros.tools import control_routing
|
||||
from supervisor import queue, workers
|
||||
from supervisor.events import _handle_promote_chat_to_task
|
||||
|
||||
pending, running, pool = [], {}, {0: SimpleNamespace()}
|
||||
for module in (queue, workers):
|
||||
monkeypatch.setattr(module, "DRIVE_ROOT", data)
|
||||
monkeypatch.setattr(module, "PENDING", pending)
|
||||
monkeypatch.setattr(module, "RUNNING", running)
|
||||
monkeypatch.setattr(workers, "WORKERS", pool)
|
||||
monkeypatch.setattr(workers, "_WORKER_POOL_DISABLED_REASON", "")
|
||||
monkeypatch.setattr(queue, "QUEUE_SNAPSHOT_PATH", data / "state/queue_snapshot.json")
|
||||
for name in ("ADMISSION_RESERVATIONS", "ACCEPTANCE_FENCES", "BUDGET_ROOT_FENCES"):
|
||||
monkeypatch.setattr(queue, name, {})
|
||||
monkeypatch.setattr(queue, "QUEUE_SEQ_COUNTER_REF", {"value": 0})
|
||||
supervisor = SimpleNamespace(
|
||||
DRIVE_ROOT=data, WORKERS=pool, PENDING=pending, RUNNING=running, bridge=None,
|
||||
enqueue_task=queue.enqueue_task, persist_queue_snapshot=queue.persist_queue_snapshot,
|
||||
load_state=lambda: {"owner_chat_id": 1}, append_jsonl=lambda *_a, **_kw: None,
|
||||
)
|
||||
|
||||
def deliver(_ctx, event):
|
||||
assert event["predecessor_task_id"] == "first"
|
||||
return "test_event_bus", _handle_promote_chat_to_task(event, supervisor)
|
||||
|
||||
monkeypatch.setattr(control_routing, "_emit_and_wait_for_routing", deliver)
|
||||
router = ToolContext(repo_dir=root, drive_root=data, task_id="decision",
|
||||
current_chat_id=1, task_metadata={"main_routing_manifest": {
|
||||
"final_results": [_task_result_ground_truth(load_task_result(data, "first"))]}})
|
||||
response = control_routing._promote_chat_to_task(
|
||||
router, "Finish the retained correction", predecessor_task_id="first", workspace="none")
|
||||
assert "durably scheduled" in response, response
|
||||
assert len(pending) == 1 and "predecessor_task_id" not in pending[0]
|
||||
snapshot = json.loads(queue.QUEUE_SNAPSHOT_PATH.read_text(encoding="utf-8"))
|
||||
assert snapshot["pending"][0]["task"]["predecessor_authority_source"] == pending[0]["predecessor_authority_source"]
|
||||
pending.clear()
|
||||
assert queue.restore_pending_from_snapshot() == 1
|
||||
assert "predecessor_task_id" not in pending[0]
|
||||
return pending[0]
|
||||
|
||||
|
||||
def test_second_task_reviews_and_commits_only_explicitly_selected_correction(tmp_path, monkeypatch):
|
||||
|
|
@ -89,14 +132,15 @@ def test_second_task_reviews_and_commits_only_explicitly_selected_correction(tmp
|
|||
|
||||
# Independent admission explicitly selects the retained predecessor. It does
|
||||
# not copy its paid wallet or critic authority, and does not reset first.
|
||||
task = {"id": "second", "root_task_id": "second", "budget_drive_root": str(data),
|
||||
"predecessor_task_id": "first", "predecessor_authority_source": _source("first")}
|
||||
assert not validate_task_authority_sources(data, task)
|
||||
write_task_result(data, "second", "running")
|
||||
task = _promote_retained_correction(root, data, monkeypatch)
|
||||
second_id = task["id"]
|
||||
assert second_id != "first" and task["root_task_id"] == second_id
|
||||
agent = SimpleNamespace(env=SimpleNamespace(repo_dir=root, drive_root=data, budget_drive_root=str(data)))
|
||||
assert not validate_task_authority_sources(agent.env, task)
|
||||
write_task_result(data, second_id, "running")
|
||||
OuroborosAgent._capture_mutation_baseline(agent, task, {})
|
||||
second = context("second")
|
||||
candidates = attributed_git_candidates(data, "second", root)
|
||||
second = context(second_id)
|
||||
candidates = attributed_git_candidates(data, second_id, root)
|
||||
assert candidates["candidates"] == ["clean.txt", "new.txt"]
|
||||
assert candidates["excluded_preexisting_dirty"] == ["dirty.txt"]
|
||||
completed = git._repo_commit_push(second, "Commit retained correction after fresh review", skip_advisory_review=True)
|
||||
|
|
@ -106,11 +150,11 @@ def test_second_task_reviews_and_commits_only_explicitly_selected_correction(tmp
|
|||
assert _git(root, "show", "HEAD:dirty.txt") == "base"
|
||||
assert (root / "dirty.txt").read_text(encoding="utf-8") == "unrelated owner WIP\n"
|
||||
assert _git(root, "diff", "--name-only") == "dirty.txt"
|
||||
assert [row["task"] for row in reviews] == ["first", "second"]
|
||||
assert [row["task"] for row in reviews] == ["first", second_id]
|
||||
assert reviews[0]["fingerprint"] != reviews[1]["fingerprint"]
|
||||
attempts = load_state(data).attempts
|
||||
assert {task_id: sum(row.paid for row in attempts if row.root_task_id == task_id)
|
||||
for task_id in ("first", "second")} == {"first": 1, "second": 1}
|
||||
for task_id in ("first", second_id)} == {"first": 1, second_id: 1}
|
||||
assert attempts[-1].status == "succeeded" and not attempts[-1].author_disposition
|
||||
assert load_task_result(data, "first") == first_result
|
||||
assert len(publications) == 1 and all(row["exit"] == 0 for row in checks)
|
||||
|
|
|
|||
|
|
@ -51,6 +51,10 @@ def test_commit_finish_requires_received_outcome(candidate, monkeypatch, basis):
|
|||
if basis == "explicit_prior":
|
||||
reference = json.loads(prior.split('\n', 1)[1])['review_reference']
|
||||
row = load_state(ctx.drive_root).attempts[-1]
|
||||
from ouroboros.tools.claude_advisory_review import _handle_review_status
|
||||
projected = json.loads(_handle_review_status(ctx))
|
||||
assert ("author_disposition" in projected["next_step"]) is (basis == "custody_lost")
|
||||
assert ("review_reference" in projected) is (basis == "custody_lost")
|
||||
assert row.status == 'reviewing' and row.late_result_pending
|
||||
assert not row.critical_findings
|
||||
second = git._repo_commit_push(ctx, 'Fix amount', review_reference=reference,
|
||||
|
|
|
|||
|
|
@ -416,7 +416,7 @@ export function taskStoppedWithSummary(evt) {
|
|||
const TASK_CAUSE_PHRASES = {
|
||||
previous_revision_accepted: "The reviewers approved the earlier version of this answer; it changed before they finished.",
|
||||
author_stop: "Main stopped with unfinished work; no review approval was granted.",
|
||||
review_outcome_received: "The review could not start; Main received the recorded reason.",
|
||||
review_outcome_received: "Main received the review outcome or recorded limitation.",
|
||||
author_finish: "The answer was delivered on Main's own judgement; the reviewers had not signed it off.",
|
||||
review_degraded: "No reviewer verdict was established for this answer.",
|
||||
infra_failure: "A review infrastructure failure prevented a settled verdict.",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue