Skip to content

Add orchestration drift guardrail - #15

Open
Pro777 wants to merge 6 commits into
developfrom
codex/orchestration-drift-guardrail
Open

Add orchestration drift guardrail#15
Pro777 wants to merge 6 commits into
developfrom
codex/orchestration-drift-guardrail

Conversation

@Pro777

@Pro777 Pro777 commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Summary\n- add a narrow orchestration telemetry contract to Talos snapshots/evaluations\n- detect when the main thread drifts into worker implementation after subagents were requested\n- surface the guardrail in evaluation output and CLI explain output\n\n## Testing\n- ./.venv/bin/pytest\n

Summary by CodeRabbit

  • New Features

    • Added snapshot-based CLI workflows for evaluating, explaining, and checking status from a snapshot file.
    • Expanded public output to include structured evaluation details and readable reasons.
    • Added support for configurable persisted state and policy files.
  • Bug Fixes

    • Improved handling of missing or invalid saved status data.
    • Added safer persistence behavior with better fallback handling when writes fail.
  • Documentation

    • Updated usage, persistence, testing, and overview docs to reflect the new CLI and snapshot-based workflow.

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds Talos data models (TalosPolicy, TalosSnapshot, TalosState, TalosEvaluation), a new runtime module for snapshot evaluation and JSON/JSONL persistence (cycles, status, history, policy), refactors scorer.py to pure policy-driven scoring, adds CLI evaluate/status/explain subcommands, expands package exports, and updates tests and documentation accordingly.

Changes

Snapshot Evaluation Feature

Layer / File(s) Summary
Data model definitions
talos/models.py, tests/test_models.py
Adds TalosPolicy, TalosSnapshot, TalosState, TalosEvaluation dataclasses with from_dict/to_dict methods and type validation, plus round-trip and validation tests.
Scorer refactor to pure arithmetic
talos/scorer.py, tests/test_scorer.py
compute_talos_level now accepts an optional TalosPolicy and uses policy-driven thresholds; persistence helpers removed; tests verify policy-driven threshold behavior.
Runtime persistence and evaluation
talos/runtime.py, tests/test_runtime.py, tests/test_cycles.py
New module resolves file paths, atomically persists state/status/history/policy, detects orchestration guardrails, computes explain_level reasons, and implements evaluate_snapshot plus backward-compatible load_cycles/save_cycles wrappers; extensive tests cover persistence, evaluation, and failure handling.
CLI evaluate/status/explain commands
talos/cli.py, tests/test_cli.py
Adds logging, a _print_json helper, and evaluate, status, explain subcommands that load snapshots, evaluate, persist results, and report errors; tests validate output, persistence side effects, and error paths.
Exports and documentation
talos/__init__.py, README.md, TESTING.md, docs/OVERVIEW.md
Expands public re-exports for new models/runtime helpers; updates README/TESTING/OVERVIEW to document the snapshot CLI workflow and the four persisted state files with their environment variable overrides.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CLI
  participant Runtime
  participant Scorer
  participant FileSystem

  User->>CLI: talos evaluate --snapshot snapshot.json
  CLI->>Runtime: read_snapshot(path)
  CLI->>Runtime: load_state(), load_policy()
  CLI->>Runtime: evaluate_snapshot(snapshot, prior_state, policy)
  Runtime->>Scorer: compute_talos_level(metrics, policy)
  Scorer-->>Runtime: level
  Runtime->>Runtime: detect_guardrails(snapshot)
  Runtime->>Runtime: explain_level(snapshot, level, guardrails)
  Runtime-->>CLI: TalosEvaluation
  CLI->>Runtime: save_state(), save_status(), append_history()
  Runtime->>FileSystem: write cycles.json, status.json, history.jsonl
  CLI-->>User: print evaluation.to_dict()

  User->>CLI: talos status
  CLI->>FileSystem: read status.json
  CLI-->>User: print status JSON

  User->>CLI: talos explain --snapshot snapshot.json
  CLI->>Runtime: evaluate_snapshot(snapshot)
  Runtime-->>CLI: TalosEvaluation.reasons
  CLI-->>User: print reasons
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

  • Spitfire-Cowboy/talos#1: Original talos/scorer.py/talos/__init__.py persistence-scoring API that this PR refactors into talos/runtime.py with policy-driven compute_talos_level.
  • Spitfire-Cowboy/talos#14: Introduces the same snapshot-driven CLI workflow (evaluate/status/explain) and README/test updates using TalosSnapshot/evaluate_snapshot.
  • Spitfire-Cowboy/talos#13: Overlapping work on talos/models.py and talos/runtime.py structured evaluation contract and persistence.

Poem

Hop, hop, through fields of JSON light,
I carry snapshots through the night,
Cycles saved, and history penned,
Policies loaded, reasons append,
A rabbit cheers this tidy state—
🐇 evaluate, explain, and celebrate!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding an orchestration drift guardrail.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/orchestration-drift-guardrail
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch codex/orchestration-drift-guardrail

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Pro777 Pro777 added the ready-for-review Ready for review label Jun 30, 2026
@codecov

codecov Bot commented Jun 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.28326% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
talos/runtime.py 96.63% 3 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
talos/models.py (2)

16-21: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse the dataclass defaults inside TalosPolicy.from_dict.

TalosPolicy.from_dict() duplicates 0.8 and 2, while talos/scorer.py derives DEFAULT_POLICY from TalosPolicy(). If these field defaults change, TalosPolicy() and TalosPolicy.from_dict({}) will silently diverge and produce different scoring depending on construction path.

Suggested change
     `@classmethod`
     def from_dict(cls, payload: Dict[str, Any]) -> "TalosPolicy":
+        defaults = cls()
         return cls(
-            friction_ratio=float(payload.get("friction_ratio", 0.8)),
-            write_block_cycles=int(payload.get("write_block_cycles", 2)),
+            friction_ratio=float(payload.get("friction_ratio", defaults.friction_ratio)),
+            write_block_cycles=int(payload.get("write_block_cycles", defaults.write_block_cycles)),
         )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@talos/models.py` around lines 16 - 21, `TalosPolicy.from_dict` is hardcoding
default values that duplicate the dataclass field defaults, which can drift from
the defaults used by `TalosPolicy()` and `DEFAULT_POLICY` in `talos/scorer.py`.
Update `TalosPolicy.from_dict` to reuse the class/dataclass defaults for
`friction_ratio` and `write_block_cycles` instead of literal values, so empty
payloads build the same policy as direct construction. Use the `TalosPolicy`
class and its `from_dict` method as the lookup points for the fix.

88-106: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add TalosEvaluation.from_dict() to complete the model contract.

This is the only Talos model that cannot round-trip through the typed API, even though runtime persists evaluations as structured JSON. That asymmetry pushes any future reader of status.json or history.jsonl back to raw dict parsing instead of the shared model contract.

Suggested change
 `@dataclass`
 class TalosEvaluation:
     """Structured output contract for a Talos evaluation cycle."""
@@
     source: str = "manual"
     timestamp: str = ""
 
+    `@classmethod`
+    def from_dict(cls, payload: Dict[str, Any]) -> "TalosEvaluation":
+        return cls(
+            level=int(payload["level"]),
+            previous_level=int(payload["previous_level"]),
+            cycles_at_level=int(payload["cycles_at_level"]),
+            wip_total=int(payload["wip_total"]),
+            global_max=int(payload["global_max"]),
+            backlog_total=int(payload["backlog_total"]),
+            backlog_delta=int(payload["backlog_delta"]),
+            at_cap_projects=list(payload.get("at_cap_projects", [])),
+            reasons=list(payload.get("reasons", [])),
+            guardrails=list(payload.get("guardrails", [])),
+            source=str(payload.get("source", "manual")),
+            timestamp=str(payload.get("timestamp", "")),
+        )
+
     def to_dict(self) -> Dict[str, Any]:
         return asdict(self)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@talos/models.py` around lines 88 - 106, Add a TalosEvaluation.from_dict()
constructor to complete the model contract so persisted evaluation JSON can
round-trip through the typed API. Implement it as the inverse of
TalosEvaluation.to_dict(), using the TalosEvaluation dataclass fields to build
an instance from a dict while preserving defaults like guardrails, source, and
timestamp when missing. Keep the parsing logic alongside to_dict() in
TalosEvaluation so future readers of status.json or history.jsonl can use the
shared model instead of raw dict handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@talos/cli.py`:
- Around line 72-73: The evaluate/explain flow is letting snapshot loading
failures escape as raw tracebacks instead of CLI usage errors. Update the
command handlers that call read_snapshot(), especially the evaluate and explain
paths that use evaluate_snapshot() and any similar snapshot loading logic, to
catch file/JSON/schema/coercion exceptions and route them through
parser.error(...) like the status command. Keep the existing behavior for valid
snapshots, but ensure bad input is reported consistently via the CLI parser.
- Around line 75-84: The persistence check in evaluate is incorrectly using
TALOS_CYCLES_FILE/TALOS_STATUS_FILE/TALOS_HISTORY_FILE existence to infer write
success, so stale files can hide failed overwrites. Update the talos/runtime.py
helpers save_state, save_status, and append_history to either raise on write
failure or return explicit success flags, then change talos/cli.py evaluate to
derive persistence_failed and the exit code from those helper results instead of
probing file existence after the writes.

In `@talos/runtime.py`:
- Around line 51-52: The drift guard in the orchestration check is firing too
early because `intent` and `main_thread_role` are being used as the primary
trigger. Update the logic around the drift check to require a real subagent
handoff by keying off `subagents_requested` and `subagents_spawned`, and use
`intent`/`main_thread_role` only as supporting context. Make sure the same
gating is applied consistently in the related drift handling paths in
`talos.runtime` so a single-thread run cannot report that subagents were
requested unless they actually were.
- Around line 157-158: The zero-cap early return in the explain path skips the
later guardrails handling, so `evaluation.reasons` can miss a message that is
present in `evaluation.guardrails`. Update the relevant flow in `talos.runtime`
(the branch that checks `snapshot.global_max <= 0`, and the similar guardrails
logic later in the same path) so the same zero-cap message is appended to
reasons before returning, or otherwise ensure the guardrails append runs for
this edge case without changing the clean-level behavior.
- Around line 156-164: Keep explain_level() consistent with
compute_talos_level() by using the same friction threshold calculation instead
of flooring it with int(...). Update the threshold check in runtime.py so the
comparison against snapshot.wip_total uses the same float-based global_max *
active_policy.friction_ratio logic as the scorer, while preserving the early
return for snapshot.global_max <= 0. This will keep the reasons generated by
explain_level() aligned with compute_talos_level() and the CLI explain output.

---

Nitpick comments:
In `@talos/models.py`:
- Around line 16-21: `TalosPolicy.from_dict` is hardcoding default values that
duplicate the dataclass field defaults, which can drift from the defaults used
by `TalosPolicy()` and `DEFAULT_POLICY` in `talos/scorer.py`. Update
`TalosPolicy.from_dict` to reuse the class/dataclass defaults for
`friction_ratio` and `write_block_cycles` instead of literal values, so empty
payloads build the same policy as direct construction. Use the `TalosPolicy`
class and its `from_dict` method as the lookup points for the fix.
- Around line 88-106: Add a TalosEvaluation.from_dict() constructor to complete
the model contract so persisted evaluation JSON can round-trip through the typed
API. Implement it as the inverse of TalosEvaluation.to_dict(), using the
TalosEvaluation dataclass fields to build an instance from a dict while
preserving defaults like guardrails, source, and timestamp when missing. Keep
the parsing logic alongside to_dict() in TalosEvaluation so future readers of
status.json or history.jsonl can use the shared model instead of raw dict
handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ef3b87ba-ef50-46c0-896f-f77d0ca58689

📥 Commits

Reviewing files that changed from the base of the PR and between 89110b9 and a96063f.

📒 Files selected for processing (13)
  • README.md
  • TESTING.md
  • docs/OVERVIEW.md
  • talos/__init__.py
  • talos/cli.py
  • talos/models.py
  • talos/runtime.py
  • talos/scorer.py
  • tests/test_cli.py
  • tests/test_cycles.py
  • tests/test_models.py
  • tests/test_runtime.py
  • tests/test_scorer.py

Comment thread talos/cli.py
Comment on lines +72 to +73
snapshot = read_snapshot(args.snapshot)
evaluation = evaluate_snapshot(snapshot, prior_state=load_state(), policy=load_policy())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle invalid snapshots as CLI errors.

read_snapshot() can fail on missing files, invalid JSON, or schema coercion errors from TalosSnapshot.from_dict(), but evaluate and explain currently surface those as raw tracebacks. These paths should go through parser.error(...), like status, so bad user input fails predictably.

Also applies to: 97-98

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@talos/cli.py` around lines 72 - 73, The evaluate/explain flow is letting
snapshot loading failures escape as raw tracebacks instead of CLI usage errors.
Update the command handlers that call read_snapshot(), especially the evaluate
and explain paths that use evaluate_snapshot() and any similar snapshot loading
logic, to catch file/JSON/schema/coercion exceptions and route them through
parser.error(...) like the status command. Keep the existing behavior for valid
snapshots, but ensure bad input is reported consistently via the CLI parser.

Comment thread talos/cli.py
Comment on lines +75 to +84
save_state(level=next_state.level, count=next_state.count, last_backlog=next_state.last_backlog)
save_status(evaluation)
append_history(evaluation)
persistence_failed = not (
TALOS_CYCLES_FILE.exists() and TALOS_STATUS_FILE.exists() and TALOS_HISTORY_FILE.exists()
)
_print_json(evaluation.to_dict())
if persistence_failed:
logger.error("Talos evaluation completed but one or more persistence writes failed")
return 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Don't infer write success from file existence.

save_state, save_status, and append_history each swallow their own write exceptions in talos/runtime.py, so this only catches the “file was never created” case. If an older cycles.json / status.json / history.jsonl already exists, a failed overwrite still leaves .exists() true and evaluate returns 0 with stale persisted state/status. Have the runtime helpers raise or return success flags, then derive the exit code from those results instead of a post-write existence probe.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@talos/cli.py` around lines 75 - 84, The persistence check in evaluate is
incorrectly using TALOS_CYCLES_FILE/TALOS_STATUS_FILE/TALOS_HISTORY_FILE
existence to infer write success, so stale files can hide failed overwrites.
Update the talos/runtime.py helpers save_state, save_status, and append_history
to either raise on write failure or return explicit success flags, then change
talos/cli.py evaluate to derive persistence_failed and the exit code from those
helper results instead of probing file existence after the writes.

Comment thread talos/runtime.py
Comment on lines +51 to +52
intent = str(orchestration.get("intent", orchestration.get("mode", ""))).strip().lower()
main_thread_role = str(orchestration.get("main_thread_role", "")).strip().lower()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Require a real subagent handoff before flagging drift.

intent / main_thread_role alone currently trigger the guardrail, so a single-thread run can emit "subagents were requested" even when no subagent was ever requested or spawned. Gate this on subagents_requested / subagents_spawned and treat role/intent as supporting context instead of the trigger.

Also applies to: 63-71

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@talos/runtime.py` around lines 51 - 52, The drift guard in the orchestration
check is firing too early because `intent` and `main_thread_role` are being used
as the primary trigger. Update the logic around the drift check to require a
real subagent handoff by keying off `subagents_requested` and
`subagents_spawned`, and use `intent`/`main_thread_role` only as supporting
context. Make sure the same gating is applied consistently in the related drift
handling paths in `talos.runtime` so a single-thread run cannot report that
subagents were requested unless they actually were.

Comment thread talos/runtime.py
Comment on lines +156 to +164
ratio_threshold = int(snapshot.global_max * active_policy.friction_ratio) if snapshot.global_max > 0 else 0
if snapshot.global_max <= 0:
return ["global_max<=0 so Talos stayed at clean level 0"]
if snapshot.wip_total >= snapshot.global_max:
reasons.append("wip_total reached or exceeded global_max")
if snapshot.at_cap_projects:
reasons.append("one or more projects are already at cap")
if snapshot.wip_total >= ratio_threshold and snapshot.wip_total < snapshot.global_max:
reasons.append("wip_total crossed the friction threshold")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep explain_level() aligned with compute_talos_level().

compute_talos_level() compares against global_max * friction_ratio as a float, but this path floors that threshold with int(...). For inputs like global_max=3, friction_ratio=0.8, wip_total=2, the level stays 0 while the reason says the friction threshold was crossed. That makes evaluate_snapshot.reasons and CLI explain disagree with the scorer.

Suggested fix
-    ratio_threshold = int(snapshot.global_max * active_policy.friction_ratio) if snapshot.global_max > 0 else 0
     if snapshot.global_max <= 0:
         return ["global_max<=0 so Talos stayed at clean level 0"]
@@
-    if snapshot.wip_total >= ratio_threshold and snapshot.wip_total < snapshot.global_max:
+    if (
+        snapshot.wip_total >= snapshot.global_max * active_policy.friction_ratio
+        and snapshot.wip_total < snapshot.global_max
+    ):
         reasons.append("wip_total crossed the friction threshold")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ratio_threshold = int(snapshot.global_max * active_policy.friction_ratio) if snapshot.global_max > 0 else 0
if snapshot.global_max <= 0:
return ["global_max<=0 so Talos stayed at clean level 0"]
if snapshot.wip_total >= snapshot.global_max:
reasons.append("wip_total reached or exceeded global_max")
if snapshot.at_cap_projects:
reasons.append("one or more projects are already at cap")
if snapshot.wip_total >= ratio_threshold and snapshot.wip_total < snapshot.global_max:
reasons.append("wip_total crossed the friction threshold")
if snapshot.global_max <= 0:
return ["global_max<=0 so Talos stayed at clean level 0"]
if snapshot.wip_total >= snapshot.global_max:
reasons.append("wip_total reached or exceeded global_max")
if snapshot.at_cap_projects:
reasons.append("one or more projects are already at cap")
if (
snapshot.wip_total >= snapshot.global_max * active_policy.friction_ratio
and snapshot.wip_total < snapshot.global_max
):
reasons.append("wip_total crossed the friction threshold")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@talos/runtime.py` around lines 156 - 164, Keep explain_level() consistent
with compute_talos_level() by using the same friction threshold calculation
instead of flooring it with int(...). Update the threshold check in runtime.py
so the comparison against snapshot.wip_total uses the same float-based
global_max * active_policy.friction_ratio logic as the scorer, while preserving
the early return for snapshot.global_max <= 0. This will keep the reasons
generated by explain_level() aligned with compute_talos_level() and the CLI
explain output.

Comment thread talos/runtime.py
Comment on lines +157 to +158
if snapshot.global_max <= 0:
return ["global_max<=0 so Talos stayed at clean level 0"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Don't drop guardrails in the zero-cap explain path.

The early return for global_max <= 0 bypasses the later guardrails append, so an orchestration-drift snapshot can populate evaluation.guardrails but omit the same message from evaluation.reasons. That edge case breaks the new explain/output surfacing contract.

Also applies to: 171-173

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@talos/runtime.py` around lines 157 - 158, The zero-cap early return in the
explain path skips the later guardrails handling, so `evaluation.reasons` can
miss a message that is present in `evaluation.guardrails`. Update the relevant
flow in `talos.runtime` (the branch that checks `snapshot.global_max <= 0`, and
the similar guardrails logic later in the same path) so the same zero-cap
message is appended to reasons before returning, or otherwise ensure the
guardrails append runs for this edge case without changing the clean-level
behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-review Ready for review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant