Skip to content

feat(cadence): shadow-evaluation mode for direct P1 vs P2 policy comparison (#875) - #881

Merged
github-actions[bot] merged 8 commits into
mainfrom
feat/issue-875-cadence-shadow-mode
May 21, 2026
Merged

github-actions[bot] merged 8 commits into
mainfrom
feat/issue-875-cadence-shadow-mode

Conversation

@robotrocketscience

Copy link
Copy Markdown
Owner

Summary

Implements #875 cadence shadow-evaluation mode. Lifts the single-policy
mutex that blocked direct P1 vs P2 comparison on identical workload —
the campaign's stated goal. Selected policy still drives live firing;
non-selected policies log decisions to a side-channel for offline
scoring.

Closes #875.

What ships

  • would_fire_p1 / would_fire_p2 — policy-agnostic predicates
    in src/aelfrice/cadence.py returning (bool, reason). Existing
    should_fire / should_fire_p2 re-expressed as policy-check +
    delegate. Behavior identical for live dispatch.
  • [cadence] shadow_mode_enabled TOML opt-in (default OFF) +
    AELFRICE_CADENCE_SHADOW_MODE_ENABLED env override. Resolver
    follows the standard env > kwarg > TOML > default precedence.
  • Per-session shadow log at
    .git/aelfrice/cadence_shadow/<session_id>.jsonl. One row per
    Stop-hook tick when enabled. Row schema:
    {
      "ts": "2026-05-20T23:59:00Z",
      "session_id": "...",
      "selected": "p2_ctx_threshold",
      "fired": true,
      "shadow": {
        "p1_every_k_turns":  {"would_fire": false, "reason": "fire_idx=8 mod k=15 = 8"},
        "p2_ctx_threshold":  {"would_fire": true,  "reason": "transcript bytes=405000 >= watermark=300000, phase-boundary"}
      }
    }
  • aelf cadence-score (hidden subcommand) — reads the on-disk
    shadow log and emits per-policy fire-rate + 2x2 P1/P2 agreement
    matrix. Flags: --project PATH (default cwd), --session SESSION_ID filter, --json for machine-readable output.

Design properties honored

Sequencing

Unblocks #876 (P3 turn-density-aware design). Per the #876 sequencing
decision: P3 designs become data-driven once shadow data accrues from
this PR.

Test plan

  • 16 would_fire_* purity cases (disabled / bad-config / divergent
    inputs / true / determinism replay / policy-agnostic verification
    / should_fire-delegation invariant).
  • 7 resolver cases (default-false, env-wins, kwarg-wins, env-
    beats-kwarg, unparseable-env-falls-through, TOML-true, wrong-
    type-ignored with stderr warning).
  • 7 shadow-log helper cases (path layout, no-disk-side-effect,
    JSON schema, determinism, extra-keys passthrough, append + two-
    row, fail-soft on unwritable parent).
  • 8 hook integration cases (disabled-default no-write, P1 no-
    fire row, P1 fire row, P2 selected logs both policies, policy=
    off logs fired=false even when P1 would have fired, two-tick
    append, cadence-disabled no-write, env override).
  • 14 scoring module cases (empty input, totals, timestamp range,
    per-policy counts, selected fire-rate, agreement matrix,
    session filter, malformed-decision skip, missing-dir tolerance,
    JSONL edge cases, JSON roundtrip).
  • 5 CLI surface cases (text report, --json roundtrip, --session
    filter, missing-project returns 2, empty-dir zero-row report).
  • Full repo suite: 4714 passed, 64 skipped, 75 xfailed — zero
    regressions on pre-existing tests.
  • aelf cadence-score --help smoke-tested via uv run aelf.

Commits

7 atomic commits per #ab96e9d3501b1c14 (atomic > batched):

  1. refactor(cadence): extract policy-agnostic would_fire_p1 / would_fire_p2
  2. feat(cadence): shadow_mode_enabled config + resolver
  3. feat(cadence): shadow-log path resolver + row formatter + appender
  4. feat(hook): wire Stop-hook shadow-evaluation logging
  5. feat(cadence): cadence_score module — agreement matrix + fire-rate
  6. feat(cli): aelf cadence-score subcommand for shadow-log reporting
  7. docs(changelog): unreleased entry for #875 cadence shadow-evaluation mode

…_p2 (#875)

Introduces would_fire_p1 and would_fire_p2 — pure predicates returning
(bool, reason) without checking config.policy. The existing
should_fire / should_fire_p2 are re-expressed as policy-check +
delegate, preserving behavior. This is the substrate for #875 shadow-
evaluation mode, which needs to evaluate every policy's would-fire
decision per Stop-hook tick regardless of which policy is selected.

Tests: 8 new would_fire_p1 cases + 9 new would_fire_p2 cases
(disabled / bad-config / divergent / true / determinism /
policy-agnostic / should_fire-delegation). Full cadence + hook-cadence
suite stays green (188 passed).
Adds [cadence] shadow_mode_enabled TOML key + AELFRICE_CADENCE_
SHADOW_MODE_ENABLED env, default OFF. Resolver follows the env >
kwarg > TOML > default precedence used by every other cadence knob.
Wired into CadenceConfig as a sixth field; load_cadence_config
reads it via _read_bool (same fail-soft pattern as `enabled`).

This is the opt-in gate for #875 shadow-evaluation mode. The
predicate-evaluation and log-write paths land in subsequent
commits; this one just gives them a flag to read.

Tests: 7 new cases — default-false, env-wins, kwarg-wins, env-beats-
kwarg, unparseable-env-falls-through, TOML-true, wrong-type-ignored
(falls back to default with stderr warning).
)

Three new public surfaces in src/aelfrice/cadence.py:

- shadow_log_path(project_aelfrice_dir, session_id) -> Path. Pure
  path computation; layout is <aelfrice-dir>/cadence_shadow/<sid>.jsonl,
  sibling of rebuild_logs/ and cadence_resume_cache.json.
- format_shadow_row(session_id, selected_policy, fired, shadow, now)
  -> str. Pure formatter; ts is supplied by caller (kept outside the
  formatter to preserve replay-ability in tests and scoring). Schema
  passes extra shadow keys through so P3 metrics can ride without a
  schema bump.
- append_shadow_row(log_path, row_line) -> None. Mkdir parent, append
  one line, swallow OSError fail-soft (Stop-hook hot-path).

CADENCE_SHADOW_DIRNAME = "cadence_shadow" exposed as a constant for
hook-side path derivation in the next commit.

Tests: 7 new cases — path layout, no-disk-side-effect on resolve,
JSON schema, determinism replay, extra-keys passthrough, append +
two-row concatenation, fail-soft on unwritable parent.
Hooks the shadow-log writer into _maybe_fire_cadence_checkpoint. On
every Stop tick where cadence is enabled, a new helper
_maybe_log_cadence_shadow_tick computes would_fire_p1 + would_fire_p2
against the same inputs the live dispatch reads, derives the
selected-policy `fired` boolean, and appends one row to
<aelfrice-dir>/cadence_shadow/<sid>.jsonl.

Gating: opt-in via [cadence] shadow_mode_enabled (default OFF). When
off, the helper returns on the first line at no measurable cost.
Cadence enabled=false short-circuits BEFORE shadow eval, so a stray
shadow flag on a disabled cadence doesn't write surprise disk
content. Fail-soft: any exception in the shadow path traces stderr
and returns; live dispatch is unaffected.

Tests: 8 new integration cases — disabled-default no-write, P1-no-
fire row schema (reason carries the diagnostic), P1-fire row, P2-
selected logs both policies, policy=off logs fired=false even when
P1 would have fired, two-tick append, cadence-disabled no-write,
env override (AELFRICE_CADENCE_SHADOW_MODE_ENABLED=1) flips it on
without TOML change.

Live-dispatch tests stay green (210 cadence + hook-cadence tests
pass total).
…er shadow logs (#875)

New module src/aelfrice/cadence_score.py with the scoring surface:

- iter_shadow_rows(shadow_dir) — generator over every JSONL line in
  the cadence_shadow dir; skips malformed lines + unreadable files.
- compute_summary(rows, session_filter=None) -> ShadowSummary —
  aggregates per-policy would_fire counts, selected-policy live fire
  counts, and a 2x2 P1 vs P2 agreement matrix.
- format_report(summary, as_json=False) -> str — human-readable
  block by default, machine-readable JSON with --json.
- resolve_shadow_dir(project_root) — derives the on-disk path.

Pure data processing; reads only the shadow log; no live cadence
state. Determinism (#605): same rows -> same report, byte-for-byte.

Tests: 14 cases covering empty input, totals, session filtering,
agreement-matrix counts, selected-policy fire-rate, malformed
decision skip, missing-dir tolerance, JSONL parsing edge cases
(bad-json line, list-not-dict), and JSON-output roundtrip.

The CLI subcommand wrapper lands in the next commit.
Hidden subcommand wrapping the cadence_score module. Flags:
  --project PATH       project root (default: cwd)
  --session SESSION_ID filter to one session_id
  --json               machine-readable JSON instead of text report

Reads <project>/.git/aelfrice/cadence_shadow/*.jsonl, aggregates,
prints. Exit codes: 0 (report emitted), 2 (project not a directory).

Registered in HIDDEN_SUBCOMMANDS in tests/test_slash_commands.py —
no slash file, since this is R&D tooling for the #749 campaign, not
a daily workflow verb. May graduate later if the surface stabilises.

Tests: 5 new CLI-surface cases — text report, --json roundtrip,
--session filter aggregates only matching rows, missing-project
returns 2, empty-dir emits zero-row report cleanly.
…mode

Documents the new surfaces: would_fire_* policy-agnostic predicates,
[cadence] shadow_mode_enabled TOML/env opt-in, the on-disk shadow
log layout, the aelf cadence-score CLI subcommand, and the rationale
(unblocks #749 campaign comparison without longitudinal flip-and-
rebake confound).
@robotrocketscience robotrocketscience added the author-Setr PR coordination mutex label May 21, 2026

@sourcery-ai sourcery-ai 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.

Sorry @robotrocketscience, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented May 21, 2026 •

Copy link
Copy Markdown

Warning

Rate limit exceeded

@robotrocketscience has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 12 minutes and 16 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a3c368f8-9e81-49b7-a599-ed2c4a1102c8

📥 Commits

Reviewing files that changed from the base of the PR and between 884ca5a and 310bbb3.

📒 Files selected for processing (10)
  • CHANGELOG/v3.md
  • src/aelfrice/cadence.py
  • src/aelfrice/cadence_score.py
  • src/aelfrice/cli.py
  • src/aelfrice/hook.py
  • tests/test_cadence.py
  • tests/test_cadence_p2.py
  • tests/test_cadence_score.py
  • tests/test_hook_stop_cadence_shadow.py
  • tests/test_slash_commands.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-875-cadence-shadow-mode

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 and usage tips.

@robotrocketscience robotrocketscience added the attn:review Needs review (PR open, awaiting reviewer) label May 21, 2026
@github-actions

github-actions Bot commented May 21, 2026 •

Copy link
Copy Markdown

PR-size soft cap

This PR is over the advisory size threshold:

  • 1761 changed lines (limit: 200)
  • 10 changed files (limit: 3)

Bigger PRs collide with more open work, which under the parallel-session workflow tends to produce repeated attn:merge-conflict cycles (see #602). When practical, split into smaller PRs that each touch a focused surface.

This is advisory only — nothing is blocked. If the size is intentional (large refactor, module removal, generated code), apply the size:override label and this comment will be removed on the next push.

Comment thread tests/test_cadence_score.py Fixed
@robotrocketscience robotrocketscience added the ready-to-merge Trigger merge-train: FF main to this PR's head label May 21, 2026
@github-actions

Copy link
Copy Markdown

merge-train: blocked

1 review thread(s) are unresolved on these files: tests/test_cadence_score.py. Resolve them on the PR (click 'Resolve conversation' on each) and re-add the label.

The ready-to-merge label has been removed. Address the issue above and re-add the label when you're ready for another attempt.

@github-actions github-actions Bot removed the ready-to-merge Trigger merge-train: FF main to this PR's head label May 21, 2026
@robotrocketscience robotrocketscience added the ready-to-merge Trigger merge-train: FF main to this PR's head label May 21, 2026
@github-actions
github-actions Bot merged commit 310bbb3 into main May 21, 2026
28 of 29 checks passed
@github-actions github-actions Bot removed the ready-to-merge Trigger merge-train: FF main to this PR's head label May 21, 2026
@github-actions

Copy link
Copy Markdown

merge-train: merged 310bbb3 → main via FF push.

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

Labels

attn:review Needs review (PR open, awaiting reviewer) author-Setr PR coordination mutex

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(cadence): shadow-evaluation mode — log non-selected policy fire-decisions for direct comparison (#749)

2 participants