Skip to content

Egg/issue 2548/slice 1 - #2571

Merged
jwbron merged 10 commits into
egg/issue-2548/workfrom
egg/issue-2548/slice-1
May 8, 2026
Merged

Egg/issue 2548/slice 1#2571
jwbron merged 10 commits into
egg/issue-2548/workfrom
egg/issue-2548/slice-1

Conversation

@jwbron

@jwbron jwbron commented May 8, 2026

Copy link
Copy Markdown
Owner

No description provided.

egg and others added 10 commits May 7, 2026 19:02
slice-1 / task-1-1 + task-1-3 — the foundation slice for the context-PR
mechanism. Subsequent slices build the gateway primitive, the
orchestrator hook, and the slice-1 base rewiring on top of these
fields.

Schema 1.1 — extends ``PRMetadata`` with four optional fields:
- ``context_title`` / ``context_description`` — planner-emitted
  framing for the dedicated context PR (e.g. "Strategic plan for #N"
  vs the slice's "Implement …"). Both fall back to ``title`` /
  ``description`` when omitted.
- ``context_branch`` / ``context_pr_number`` — orchestrator-populated
  runtime values (the ``egg/<id>/context`` branch name and the GitHub
  PR number once the context PR has been opened). Planners must NOT
  emit these.

Bumps ``Contract.schemaVersion`` default from ``"1.0"`` to ``"1.1"``
and adds an ``after``-mode migration shim that promotes pre-1.1
contracts to 1.1 on load. The bump is purely additive — pre-1.1 JSON
loads cleanly with the new fields defaulting to ``None``.

Plan-parser plumbing — ``ParseResult`` grows ``pr_context_title`` /
``pr_context_description`` and a new ``extract_pr_context_metadata_from_yaml``
helper extracts the optional keys without breaking the existing
``extract_pr_metadata_from_yaml`` 5-tuple signature (and the
~10 callers + tests that unpack it).

Planner prompt — both planner-prompt sites in ``pipelines.py`` (the
plan-phase prompt under ``_build_phase_prompt`` and the
task_planner-role prompt under ``_build_agent_prompt``) gain the
``_PR_CONTEXT_GUIDANCE`` paragraph and the ``_PR_CONTEXT_YAML_EXAMPLE_LINES``
commented-out hints inside the ``pr:`` YAML block. Both helpers are
defined once next to ``_PR_DESCRIPTION_GUIDANCE`` so the two prompt
sites stay in sync when the guidance evolves.

Contract populator — ``_populate_contract_from_plan`` now copies
``result.pr_context_title`` / ``pr_context_description`` onto the new
PRMetadata it builds, and preserves any orchestrator-populated
``context_branch`` / ``context_pr_number`` across re-populates so a
later plan re-parse does not blow away runtime state set by slice-3's
hook.

Test impact: bumping the default ``schemaVersion`` to ``"1.1"`` causes
``tests/shared/egg_contracts/test_models.py::test_minimal_contract``
to fail on the literal ``"1.0"`` assertion. The fix-up belongs to
the tester role (task-1-2) along with the new ``PRMetadata.context_*``
round-trip coverage; coder boundaries forbid pushing test edits.
Lint (ruff format + check) and mypy delta are clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
slice-1 / task-1-2 — adversarial + regression coverage for the four new
optional ``PRMetadata.context_*`` fields and the ``schemaVersion``
1.0→1.1 promotion shim added by the coder in commit 75d8ca0.

Coverage:
* ``TestPRMetadataContextFields`` — defaults to None, full round-trip
  with all four fields populated, omitted-keys round-trip preserves
  None.
* ``TestPRMetadataContextPRNumberValidator`` — pins the ``ge=1``
  validator: 0/-1 are rejected at construct AND at setattr (under the
  shared ``EggContractBaseModel.validate_assignment=True`` from #2490);
  None and large positive ints accepted.
* ``TestPRMetadataSchemaVersionMigration`` — 1.0 payload loads with
  context defaults, dump→reload chain stays at 1.1, default is 1.1,
  legacy ``deferred_actions`` survive migration, and an unrecognized
  version (1.2 / 2.0) is NOT silently downgraded.
* ``TestPRMetadataContextEmptyStringSemantics`` — empty strings are
  accepted at the model layer so the orchestrator hook's
  ``context_title or title`` fallback works for both None and "".
* ``TestPlanParserContextFieldExtraction`` — covers task-1-3's
  ingestion path: ``extract_pr_context_metadata_from_yaml`` returns
  None pair for missing/None/absent inputs; collapses whitespace to
  None; warns on non-string ``context_title``; ``parse_plan`` threads
  the values onto ``ParseResult.pr_context_*``.

Also updates ``test_models.py::test_minimal_contract`` from the literal
``"1.0"`` schemaVersion assertion to ``"1.1"`` — the coder flagged this
as a known follow-up in commit 75d8ca0 (coder cannot push test edits
under the role boundary).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Blocking fix:
- _populate_contract_from_plan now also preserves PRMetadata.deferred_actions
  alongside context_branch / context_pr_number. The conditional-ACK gate at
  decisions.py:complete_phase writes deferred actions; the populator's
  start_phase=implement re-entry path was silently wiping them, erasing
  the merge-blocking Pre-merge Obligations handoff. Add a regression
  test in orchestrator/tests/test_short_flow_contract_population.py.

Non-blocking improvements:
- extract_pr_context_metadata_from_yaml now warns symmetrically on
  non-string context_description (mirrors the context_title branch),
  preventing silent str() coercion of structured planner values.
- Updated schemaVersion / _migrate_schema_version_to_1_1 docstrings to
  reflect that the bump fires at every load (mode="after"), not lazily on
  next save, and to acknowledge the migration is silent (no audit entry).
- Aligned TestPRMetadataContextEmptyStringSemantics docstring with reality
  (planner path collapses empty strings to None; only hand-edited or
  migrated payloads can produce a "" PRMetadata).
- New tests for the symmetric context_description warning.
Slice-1 lands the schema delta + planner-prompt update half of the
context-PR mechanism (#2548): `PRMetadata` grows four optional
`context_*` fields and `Contract.schemaVersion` defaults to `"1.1"`
with an additive `1.0 → 1.1` migration. The actual context-PR
mechanism (branch creation, PR opening, slice-1 base wiring) is
implemented in slices 3-4 and gets its own end-to-end documentation
pass in slice-5.

This commit updates the docs that reference contract examples and the
yaml-tasks `pr:` block so they reflect the slice-1-landed schema
state:

- `docs/templates/plan.md`: add optional `context_title` /
  `context_description` keys to the yaml-tasks `pr:` example as
  commented-out hints, plus a new prose blockquote explaining when
  planners may emit them and which sibling fields
  (`context_branch`, `context_pr_number`) are orchestrator-populated.
- `docs/architecture/sdlc-pipeline.md`: bump the example
  `schemaVersion` from `1.0` to `1.1` and add a "Schema 1.1 (#2548)"
  blockquote summarising the additive migration.
- `docs/guides/sdlc-pipeline.md`: same `schemaVersion` bump in the
  example JSON plus a short blockquote pointing readers at the
  migration semantics.

The PR-stack diagrams, BRC-history file naming, and slice-1-base
discussion in `docs/guides/concurrent-execution.md`,
`docs/architecture/orchestrator.md`, `docs/reference/orchestrator-cli.md`,
and `docs/guides/babysit-pr.md` remain untouched — those describe
behavior that does not yet exist on this branch and are slice-5's
responsibility once the mechanism is wired end-to-end.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
#2548)

Slice-1 (foundation) tester NACK: on the egg/issue-2548/work merge target,
slice-1's extract_pr_context_metadata_from_yaml + ParseResult.pr_context_*
plumbing stacks on top of #2527's validate_task_role_alignment additions,
pushing shared/egg_contracts/plan_parser.py to ~1,530 lines and breaching
the 1,500-line hard cap that scripts/check-file-sizes.py enforces. The
slice-1 branch alone is at 1,388 lines (clean), but the work-branch state
that the lint actually runs against is over.

Fix per reviewer_contract's forward-looking concern and tester's blocking
finding: add the file to scripts/file-size-allowlist.yaml under #2548 so
make lint passes during the slice-1 BRC. Decomposition is tracked under
the same issue and is the cheaper of the two unblock options for slice-1
(decomposing in-cycle would expand scope and risk slice-2/3/4's
dependency on the current plan_parser.py public surface).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…2548)

Both blockquotes now disclose that slice-1 lands only the schema fields
plus the planner-prompt advertisement — the orchestrator branch-creation
and PR-opening hooks land in #2548 slices 3-4. Until those slices merge
the four pr.context_* fields are forward-compatibly inert: a planner
emitting context_title / context_description has those values flow into
PRMetadata, but nothing acts on them yet, so reviewers and planners
reading the merged-but-pre-slice-3-4 docs see exactly what is and is
not wired today rather than reading the eventual contract as
present-tense.

Addresses non-blocking suggestion in PR #2555 review.
Adds 12 adversarial probes to the slice-1 test file as the tester role's
contribution to slice-1 task-1-2:

- model_dump_json round-trip preserves all four context_* fields
- model_dump_json preserves None as JSON null (no exclude_none drift)
- Combined phases:->slices: + schemaVersion 1.0->1.1 migration in one load
- YAML null (~) for context_title / context_description threads as None
- parse_plan markdown-only path (no yaml fence) yields None context fields
- list-typed context_title and context_description warn (mirrors int/dict)
- CRLF + mixed whitespace stripping for context strings
- context_pr_number accepts large ints (no implicit int32 ceiling)
- schemaVersion regex rejects "1.0-rc1" / "v1.0"
- 1.0 payload with explicit context fields still loads + bumps to 1.1
- non-dict pr: block short-circuits the context extractor (no AttributeError)

All 42 tests in tests/shared/egg_contracts/test_pr_metadata.py pass.
ruff check / format are clean. The wider lint and test suites are
verified separately.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…lowlist tracker

- Promote slice-1 forward-pointer blockquote from parenthetical to a
  follow-on bold-led paragraph in docs/architecture/sdlc-pipeline.md and
  docs/templates/plan.md so the schema-vs-orchestrator inertness is
  harder to miss on a quick skim.
- Reword test_extract_warns_on_list_typed_context_title docstring to
  cite the parser-layer isinstance(raw_title, str) guard instead of
  pydantic's str coercion machinery — pydantic is not in this code path.
- Cite #2569 (plan_parser.py decomposition tracker) from the
  scripts/file-size-allowlist.yaml entry comment instead of an
  uncited "follow-up".
* Per-slice implement-phase BRC history (#2548 slice-2)

Switches the implement-phase BRC writer to per-slice files
`<id>-implement-{slice_id}.{md,json}` (one per slice) and drops the
aggregate `<id>-implement.{md,json}` filename — hard switchover under
D4 (no aggregate file is produced).

Refactor:

- `_write_brc_history()` now partitions implement-phase BRC messages
  by `metadata['slice_id']` and writes one file per slice via the new
  `_write_brc_history_file()` helper. Refine, plan, and pr phases keep
  the aggregate `<id>-{phase}.{md,json}` filename. Implement messages
  without `slice_id` are dropped with a single aggregate WARNING — the
  partitioning is mandatory under D4.
- A new `_render_brc_history_markdown()` helper carries the
  byte-identical markdown rendering (idempotency invariant from #1714)
  shared between the aggregate and per-slice writers.
- Existing callers (`_rewrite_brc_history_for_pr`,
  `_persist_phase_brc_history`, the inline call in `_run_pipeline`)
  delegate to `_write_brc_history()` unchanged — partitioning is
  internal to the writer.
- The pipeline-identifier-scoped staging glob in
  `_commit_statefiles_to_worktree()` already picks up the new
  `<id>-implement-slice-<N>.{md,json}` filenames (prefix-anchored on
  the issue/pipeline id).

Test fixture updates (seeding `slice_id=slice-1` on existing
implement-phase fixtures and rewriting aggregate-file assertions to
the per-slice shape) are deferred to task-2-3 (tester role) — those
test paths are gateway-blocked for the coder role per the BRC file
boundaries. The tester will re-align fixtures and add net-new coverage
for the multi-slice writer, the missing-`slice_id` WARNING, and the
no-aggregate-file invariant in parallel.

Closes task-2-1 and task-2-2 in slice-2 of #2548.

* Validate slice_id against SLICE_ID_PATTERN before file write (#2548 sec fix)

Addresses reviewer_security NACK: `metadata['slice_id']` was
interpolated directly into the on-disk filename of the per-slice BRC
history file, with no validation against the canonical
``^slice-[0-9]+$`` shape. Any sandbox agent can post arbitrary
metadata via the generic message-send endpoint
(`orchestrator/routes/messages.py:202`), so a malicious
`metadata.slice_id = "../../etc/foo"` would have written under
`worktree/.egg-state/etc/foo.{md,json}` — escaping the intended
brc-history directory and clobbering arbitrary state files (contracts,
plan drafts, other slices' BRC files).

Fix: import ``SLICE_ID_PATTERN`` from ``slice_id_validation`` (the
shared allowlist that already gates every other gateway-facing seam
where slice_id is interpolated — signal handlers #2403, restart route
#2410, branch builders) and reject any message whose
``metadata['slice_id']`` is not a string fullmatching the canonical
pattern. Rejected messages fold into the same ``unattributed``
counter and the same single aggregate WARNING that handles
missing-slice_id messages.

This puts the new file-path call site on the same allowlist as every
other use of slice_id, satisfying the invariant called out in
``slice_id_validation.py``'s module docstring: *"a future caller that
forgets the upstream regex must not be able to smuggle path
separators or shell metacharacters into a tracker registry key, a
Job name, or a worktree id."* The brc-history file path is now the
fourth call site to honor it.

* Address holistic NACK: tag CONSENSUS_* with slice_id, preserve babysit_pr (#2548)

Addresses reviewer_code_holistic NACK on v2 with three blocking
findings:

1. **Cross-module synthetic-key audit**: producers (CONSENSUS_PROPOSE/
   ACK/NACK/RE_REVIEW/WITHDRAW handlers in `routes/signals.py`) did
   NOT attach `slice_id` to the message metadata they wrote — only
   `CONSENSUS_CONFIRMED` did, via `_slice_meta`. Under v2, that meant
   the implement-phase writer would drop nearly every real BRC
   message. Fix: extend each consensus signal handler to spread the
   same `_slice_meta = {"slice_id": slice_id} if slice_id is not None
   else {}` shape into the metadata of every CONSENSUS_* message it
   writes. The new asymmetry surfaces only at the writer (canonical
   slice_id is required there too — same regex used by every other
   gateway-facing seam) but the producer side now reliably tags every
   slice-scoped message.

2. **Babysit_pr regression**: babysit_pr pipelines have no slices and
   no message ever carries `slice_id`, so v2 dropped the entire BRC
   stream and produced no `pr-<N>-<sha>-implement.{md,json}` file
   (regression of the documented babysit_pr artifact in
   `skills/babysit-pr/SKILL.md`). Fix: the writer now auto-detects
   slice-aware vs aggregate mode by checking whether ANY message
   carries a canonical `slice_id`. If none do, the writer falls back
   to the aggregate `{identifier}-implement.{md,json}` filename —
   preserving babysit_pr semantics. If at least one does, partition
   per-slice and warn loudly about any unattributed siblings.

3. **Silent-fallback hunt**: the v2 drop branch logged a warning and
   silently produced no file. The new branch is no longer silent —
   when partition mode is engaged but some messages are unattributed,
   the warning includes the dropped count, the count of attributed
   messages, and a sample of message types that were dropped, so
   operators can diagnose tag asymmetry quickly. When NO messages
   have slice_id at all, the writer now falls through to the
   aggregate filename instead of dropping (see #2 above).

Also addresses reviewer_code_holistic non-blocking findings:

* `_build_brc_history_link_line()` now clusters per-slice implement
  files (`implement-slice-1`, `implement-slice-2`) at the canonical
  ``implement`` rank so the rendered link order matches the canonical
  phase order (refine → plan → implement[-slice-N] → pr).
* `_write_brc_history()` lead docstring rewritten to describe the
  per-slice / aggregate auto-detection and the babysit_pr fallback
  path.

* Address reviewer_code non-blocking notes: natural sort + tighter access (#2548)

Folds two non-blocking observations from reviewer_code's v2 NACK into
the v3 re-propose:

* `_write_brc_history()` partition loop: drop the `getattr(msg,
  "metadata", None) or {}` defensive guard. `Message.metadata` is a
  Pydantic `dict[str, Any]` field with `default_factory=dict`
  (`message_store.Message`), so it is always a dict at this point —
  the simpler `msg.metadata.get("slice_id")` is equivalent and
  removes a no-op `isinstance` branch.
* Iterate per-slice buckets in natural-sort order (integer suffix)
  rather than lexicographic. A 12-slice pipeline now writes its
  BRC files in `slice-1, slice-2, …, slice-12` order rather than
  the lexicographic `slice-1, slice-10, slice-11, slice-12, slice-2`.
  Every key has been SLICE_ID_PATTERN-validated by this point, so
  the integer parse is total.
* `_build_brc_history_link_line()` link rendering: per-slice files
  inside the implement cluster are now sorted by integer slice index
  too, so the PR-body legend reads `implement-slice-1,
  implement-slice-2, …, implement-slice-12` rather than the
  lexicographic order. Same total integer parse — the file glob
  could in theory produce a non-canonical name, so a malformed
  suffix sorts last within the cluster.

These were the non-blocking items in the reviewer_code v2 NACK that
sit cleanly alongside the v3 cross-module fix; folding them in one
commit avoids a follow-up cleanup churn.

* Per-slice implement-phase BRC history tests (#2548 slice-2)

Aligns the BRC-history test suite with the post-#2548 hard switchover
to per-slice implement-phase files (`<id>-implement-{slice_id}.{md,json}`)
and adds net-new coverage for the multi-slice writer, the missing-
`slice_id` WARNING path, and the no-aggregate-file invariant.

Existing tests:

- `test_brc_history.py` — `_make_brc_message` / `_make_brc_messages` now
  auto-stamp `metadata['slice_id']` for implement-phase fixtures so the
  hard-switchover writer keeps producing files. Aggregate-file path
  assertions (`42-implement.{md,json}`) are rewritten to the per-slice
  shape via a new `_implement_path()` helper. Link-line tests updated
  to use per-slice filenames in their stub writes.
- `test_brc_phase_propagation.py`, `test_diagnostic_logging_1633.py`,
  `test_pr_phase_brc_rewrite.py`, `test_conditional_ack.py` — same
  `slice_id` auto-stamping treatment for their own `_make_brc_message`
  helpers; aggregate-path assertions rewritten in lockstep.

New tests (`TestPerSliceImplementBrcHistory` and
`TestPerSliceImplementBrcHistoryRewriteForPr`):

- `test_writes_one_file_per_slice_no_aggregate` — N=2 slices yields two
  per-slice .md+.json pairs and zero aggregate files.
- `test_each_slice_file_contains_only_its_own_messages` — partitioning
  isolates buckets; cross-slice content leaks fail the test.
- `test_single_slice_still_uses_per_slice_filename` — N=1 still uses
  the per-slice naming (no special-case for the single-slice degenerate).
- `test_per_slice_file_carries_slice_label_in_header` — slice label is
  visible in the markdown header.
- `test_messages_without_slice_id_dropped_with_warning` — mix of
  attributed + unattributed: per-slice file written for the attributed
  set; unattributed messages dropped; a single warning carries the
  drop count.
- `test_all_messages_unattributed_no_files_no_aggregate` — when EVERY
  implement-phase message lacks a slice_id, no files are produced
  (no fall back to aggregate).
- `test_refine_phase_keeps_aggregate_filename`,
  `test_plan_phase_keeps_aggregate_filename`,
  `test_pr_phase_keeps_aggregate_filename` — regression: only implement
  partitions; refine/plan/pr keep the aggregate even when fixtures
  carry `slice_id` defensively.
- `test_partial_attribution_only_attributed_messages_get_files` —
  exact `dropped_count=1` accounting when one of three buckets is
  unattributed.
- `test_implement_messages_with_empty_slice_id_dropped` — empty-string
  slice_id is treated as missing (security-relevant: must NOT produce
  `42-implement-.md`).
- `test_three_slices_all_get_distinct_files` — N=3 sorted bucket walk;
  exercises deterministic order even on shuffled input.
- `test_idempotent_per_slice_write` — per-slice files are
  byte-identical across repeated writes (preserves #1714 invariant).
- `test_non_dict_metadata_is_treated_as_unattributed` — defensive
  guard around non-dict metadata produces a drop, not a crash.
- `test_rewrite_for_pr_emits_per_slice_implement_files` and
  `test_rewrite_for_pr_mixes_aggregate_refine_and_per_slice_implement`
  — the PR-phase safety-net rewrite (`_rewrite_brc_history_for_pr`)
  inherits the per-slice partitioning correctly; refine and implement
  shapes coexist in the same brc-history dir.

Closes task-2-3 in slice-2 of #2548.

* Adapt per-slice BRC history tests to v3 babysit fallback + SLICE_ID_PATTERN validation (#2548 slice-2)

Folds the v2→v3 coder behavior changes (commits beb2bae, 2a912c5,
70fe103) into the test plan:

- `test_all_messages_unattributed_no_files_no_aggregate` →
  `test_all_messages_unattributed_writes_aggregate_babysit_fallback`:
  v3 fixed reviewer_code_holistic finding #2 — when NO message in the
  store carries a canonical `slice_id`, the writer now falls back to
  the aggregate `<id>-implement.{md,json}` filename so non-slice
  pipelines (babysit_pr) keep producing the artifact documented in
  `skills/babysit-pr/SKILL.md`. Test was rewritten to assert this
  fallback path; complemented by `test_babysit_aggregate_fallback_contains_all_messages`
  which pins that the aggregate carries every BRC-eligible message.
- `test_non_dict_metadata_is_treated_as_unattributed` →
  `test_message_metadata_is_always_a_dict`: v3 dropped the defensive
  `getattr(msg, "metadata", None) or {}` guard now that
  `Message.metadata` is asserted as a Pydantic
  `dict[str, Any] = Field(default_factory=dict)` field. Test now pins
  the Pydantic invariant directly (default-factory yields {}, never
  None) so a future Pydantic-config change shows up here rather than
  as a runtime crash inside `_write_brc_history()`.
- `test_implement_messages_with_empty_slice_id_dropped` →
  `_treated_as_unattributed`: empty-string `slice_id` is now dropped
  via `SLICE_ID_PATTERN` validation rather than the falsy `if not
  slice_id`. Test mixes empty-slice_id with a canonical message so
  partition mode engages (otherwise we'd hit the babysit aggregate
  fallback) and asserts: (a) no `42-implement-.md` is ever produced
  (security: empty slice_id must not be interpolated into the per-slice
  stem), (b) the canonical slice-1 file IS produced, (c) drop warning
  carries `dropped_count=1`.

Net-new tests added to cover v3 behaviors:

- `test_invalid_slice_id_pattern_treated_as_unattributed`: defense-in-depth
  test for the new local SLICE_ID_PATTERN validation. Covers 9
  injection payloads (`../etc/passwd`, `slice-1/extra`, `phase-1`,
  `SLICE-1`, etc.) and asserts: (a) only the canonical slice-1 file
  exists, (b) no aggregate is written (partition mode is engaged), (c)
  no traversal — only the brc-history dir was created under
  `.egg-state/`, (d) drop warning's `dropped_count` matches the
  payload count exactly.
- `test_natural_sort_per_slice_iteration_order`: covers the v3
  reviewer_code non-blocking that switched bucket iteration from
  lexicographic to natural-sort by integer suffix. Patches
  `routes.pipelines.logger.info` to capture "Wrote BRC history file"
  calls and asserts the slice_id sequence is `slice-1, slice-2,
  slice-7, slice-11, slice-12` (not the lex order which would put
  slice-11 / slice-12 before slice-2).

72 tests in `test_brc_history.py` pass; 203 tests across the seven
BRC-history-related test files pass. `ruff check` clean.

* Address tester NACK: tag remaining 3 metadata sites with slice_id (#2548)

Closes the three remaining producer-side gaps the tester flagged on
v3:

1. `handle_producer_push_signal` auto-re-propose CONSENSUS_PROPOSE
   (lines 2110-2118): the auto-push re-propose path now spreads
   `**_slice_meta` into the metadata dict alongside the existing
   `auto_re_propose` / `trigger` / `commit_sha` / `version` /
   `changed_files` keys. Mirrors the manual re-propose path in
   `handle_consensus_propose_signal` patched in v3.
2. `handle_producer_push_signal` auto-re-propose CONSENSUS_RE_REVIEW
   notifications (lines 2125-2148): same fix — spread `**_slice_meta`
   into the per-reviewer notify metadata so the broadcast carries the
   partitioning key end-to-end.
3. `handle_consensus_resolve_obligation_signal` CONSENSUS_OBLIGATION_RESOLVED
   (lines 2017-2025): in-cycle conditional-ACK obligation resolution
   sits in BRC_HISTORY_TYPES and can fire during the implement phase
   with slice scope (typical case: tester satisfies a coder's
   conditional ACK on a per-slice review). The handler already extracts
   slice_id at line 1975 for tracker scoping; spread the same
   `**_slice_meta` shape into the OBLIGATION_RESOLVED message metadata
   so the audit trail lands in the per-slice BRC transcript.

After this commit every producer-side BRC message that can fire in
the implement phase carries `metadata.slice_id` for slice-scoped
callers (PROPOSE, RE_REVIEW manual, RE_REVIEW auto, ACK, NACK,
WITHDRAW, OBLIGATION_RESOLVED, CONFIRMED — already tagged before
slice-2). The remaining BRC_HISTORY_TYPES that don't currently tag
(HEARTBEAT, STATUS, NUDGE, HANDOFF, AGENT_FAILED) are non-blocking per
both the tester's and reviewer_code_holistic's review notes; they
fold cleanly into the partition-mode `unattributed` warning rather
than corrupting the per-slice transcript, and the consensus narrative
itself (the high-value review trail) lands in full.

* Fix checks: apply automated formatting fixes

* Address review feedback on per-slice BRC history (#2548)

Tag non-CONSENSUS BRC emitters with slice_id where their handler
already extracts it (HEARTBEAT in messages.py, excuse-producer STATUS
and ready-to-confirm STATUS in signals.py), so slice-scoped messages
land in the right per-slice transcript instead of the shared bucket.

Narrow `_write_brc_history` so the per-slice partitioning only drops
CONSENSUS_* messages without `metadata['slice_id']` (those remain a D4
contract violation). Non-CONSENSUS BRC types (HEARTBEAT, STATUS,
HANDOFF, AGENT_FAILED, NUDGE, OVERSEER_ALERT) without slice_id come
from emitters that do not uniformly carry slice scope (HealthMonitor
nudges, overseer respawn alerts, AGENT_FAILED broadcasts, CLI-routed
HANDOFF/NUDGE) and are now routed to a sibling
`{identifier}-implement-unattributed.{md,json}` file. The link-line
builder clusters that sibling at the implement rank after every
per-slice file. Update the writer docstring and inline comment to
match.

Drop the redundant local re-import of `SLICE_ID_PATTERN` inside
`_write_brc_history` (it's already imported at module top via the
sandbox/orchestrator dual-import pattern). Delete a dead/typo'd
disjunctive assertion in `test_idempotent_per_slice_write`.

Add tests covering: non-CONSENSUS unattributed routing to the sibling
file, the mixed CONSENSUS_*-dropped + non-CONSENSUS-routed split, and
HEARTBEAT/excuse-producer STATUS slice_id metadata round-trip on the
message bus.

* Address second-round review feedback on per-slice BRC history (#2548)

Fix the cross-module silent no-op flagged as blocking: brc_read_peer_artifact
now mirrors the writer's per-slice filename when EGG_SLICE_ID is set and
phase=='implement' (reads {identifier}-implement-{slice_id}.json), and by
default merges the cross-cutting unattributed sibling so reviewers see
their slice's CONSENSUS_* interleaved with OVERSEER_ALERT / AGENT_FAILED
context. Pipeline-level (non-slice) callers still read the aggregate file.

Non-blocking fixes:
- _emit_ready_to_confirm_nudges now has dedicated slice_id metadata tests
  (slice-scoped + pipeline-level pair, mirroring the excuse-producer pair).
- test_idempotent_per_slice_write extended to assert the unattributed
  sibling md/json are byte-identical across repeated writes.
- _render_brc_history_markdown special-cases slice_id=='unattributed':
  heading reads 'cross-cutting (unattributed)' and the metadata block uses
  Section: instead of Slice:, since unattributed is not a slice.
- docs/guides/concurrent-execution.md updated to show the per-slice link
  line shape and explain the unattributed sibling cluster.

* Address third-round non-blocking feedback on per-slice BRC history (#2548)

Sync the brc_read_peer_artifact handler's message_type filter
whitelist with the orchestrator-side BRC_HISTORY_TYPES emitter:

- Fix CONSENSUS_WITHDRAWN -> CONSENSUS_WITHDRAW typo (the writer
  emits CONSENSUS_WITHDRAW; the handler's whitelist rejected the
  correct name and accepted a string the writer never produces).
- Add CONSENSUS_OBLIGATION_RESOLVED, STATUS, HANDOFF, AGENT_FAILED,
  NUDGE, OVERSEER_ALERT, HEARTBEAT to the whitelist so reviewers can
  filter the implement-phase unattributed sibling on those types
  (e.g. message_type=[OVERSEER_ALERT] to scan cross-cutting alerts).
  The handler's docstring already invited these filters; the
  whitelist now matches.
- Update the schema description in tools/brc.py to match.
- Add test_filter_by_message_type_overseer_alert_in_unattributed
  exercising the OVERSEER_ALERT filter against an unattributed
  sibling fixture; locks in the writer/reader symmetry.

Also drop the underscore-prefixed _SLICE_ID_PATTERN private import
in brc_read_peer_artifact and use the existing public
resolve_slice_id({}) helper from _gateway -- same validation,
fewer reach-inside imports, consistent error message format with
the rest of the slice-aware tools.

* Add full-set drift guard locking BRC_HISTORY_TYPES writer/reader symmetry

Closes the fourth-round non-blocking review item on #2548 slice-2: the
existing single-type regression test pinned OVERSEER_ALERT only, so
adding a new entry to the writer-side BRC_HISTORY_TYPES without
updating the sandbox-side _BRC_HISTORY_TYPES would slip past the suite.

The new test in TestBrcHistoryTypesDriftGuard regex-extracts the
writer-side frozenset literal from orchestrator/routes/pipelines.py and
asserts it equals the handler-side whitelist. This preserves the
deliberate sandbox -> orchestrator package-boundary (the orchestrator
pulls fastapi) while still locking the contract: any future drift on
either side surfaces as a test failure with explicit handler-only /
writer-only diffs in the assertion message.

---------

Co-authored-by: egg <egg@example.com>
Co-authored-by: egg <egg@localhost>
Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>
@jwbron
jwbron merged commit 0742a18 into egg/issue-2548/work May 8, 2026
11 checks passed

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No agent-mode design concerns.

This slice is plumbing — schema 1.1 with four additive pr.context_* fields, per-slice BRC history partitioning in the orchestrator, and planner-prompt advertisement of two optional context-PR keys. None of it touches the agent-mode patterns the design guide flags:

  • The planner-prompt additions (_PR_CONTEXT_GUIDANCE, _PR_CONTEXT_YAML_EXAMPLE_LINES) inform the planner about optional keys and explicitly mark which fields the orchestrator owns at runtime — that's orientation, not constraint.
  • The YAML structured output is appropriate here: the contract is a machine-readable artifact consumed by orchestrator code paths that open PRs, so this is the "machine-readable output for genuine automation" exception, not human-facing output dressed up as JSON.
  • No direct Anthropic API calls, no httpx/requests to api.anthropic.com, no pinned claude-*-YYYYMMDD model identifiers, no post-processing pipelines parsing natural-language agent output.
  • The BRC history writer is pure orchestrator-side log routing — it partitions messages already in the bus into per-slice transcript files for reviewer ergonomics. No prompt-side coupling.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@james-in-a-box james-in-a-box Bot mentioned this pull request May 8, 2026

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Post-merge review (slice-1 of #2548)

Reviewed the full diff (3,555 additions / 186 deletions across 23 files) end-to-end. The slice landed cleanly: schema 1.0→1.1 migration is well-scoped and idempotent, the per-slice BRC history writer has thorough test coverage including path-traversal payloads, and the planner-prompt + ParseResult plumbing is forward-compatibly inert until slices 3-4 wire it up. A few observations worth tracking — none are blocking, but a couple deserve follow-up issues.

Notable: actual bug fix shipped under "follow-up"

sandbox/egg_agent_tools/handlers/brc.py previously had "CONSENSUS_WITHDRAWN" (with a trailing N) in _BRC_HISTORY_TYPES, but the real MessageType enum value is CONSENSUS_WITHDRAW (no N — see orchestrator/message_store.py:68). This meant brc_read_peer_artifact(message_type="CONSENSUS_WITHDRAW") would raise Unknown message_type(s), while CONSENSUS_WITHDRAWN was a no-op filter for messages that don't exist. The new whitelist fixes this. The new TestBrcHistoryTypesDriftGuard is a great regression guard, but a direct end-to-end test that actually filters by CONSENSUS_WITHDRAW and asserts a non-empty result would make the fix unmistakable in the test report. Worth adding.

CONSENSUS_BRC_TYPES is asymmetric and unguarded

pipelines.py:8105 introduces a hardcoded subset of BRC_HISTORY_TYPES that the writer treats as "must have slice_id or be dropped." The drift-guard test (TestBrcHistoryTypesDriftGuard) only locks writer↔reader symmetry on BRC_HISTORY_TYPES; nothing prevents a future PR from adding e.g. CONSENSUS_FOO to BRC_HISTORY_TYPES and forgetting CONSENSUS_BRC_TYPES, in which case the new type would silently route to unattributed_other instead of being the slice-attribution-required member it semantically is. Suggest adding a similar drift guard or, better, deriving CONSENSUS_BRC_TYPES programmatically (e.g. frozenset(t for t in BRC_HISTORY_TYPES if t.startswith("CONSENSUS_"))).

Transitional state: stale {id}-implement.md is never cleaned up

The hard switchover claims "no aggregate implement file is produced," but the writer never removes an existing 42-implement.md from a prior babysit_pr / pre-#2548 run on the same identifier. If a pipeline is restarted post-merge, an in-place .egg-state/brc-history/42-implement.md from an earlier run will persist alongside the new per-slice files; _build_brc_history_link_line will dutifully include both (the sort key correctly clusters them at rank["implement"] with idx=-1 then idx=N≥1). Tests cover the clean-on-disk case but not "stale aggregate already exists, then slice writer runs." Probably won't bite in greenfield use, but agents inheriting state across restarts may see surprising link lines. Consider an explicit cleanup pass when entering partition mode.

Slice-id canonicality ≠ slice-id legitimacy

The writer interpolates any metadata['slice_id'] matching ^slice-[0-9]+$ directly into the filename. There's no cross-check against contract.slices — a message with metadata['slice_id'] = "slice-99999" for a 3-slice contract would produce a 42-implement-slice-99999.md next to the legitimate three. The path is contained (good — that's what the regex guarantees), so this isn't a security issue, but since any role can post arbitrary metadata via routes/messages.py, a buggy or hostile sender can pollute brc-history with phantom-slice files. Logging an info/warning when an observed slice_id isn't in the loaded contract's slice set would surface this without adding false-rejects.

Handler re-sort runs unnecessarily on single-file reads

sandbox/egg_agent_tools/handlers/brc.py:1082if len(history_files) > 1: filtered.sort(...) always sorts when slice-scoped, even if the unattributed sibling doesn't exist on disk (because the path is added before the existence check at line 1042). Records from a single existing file are already in chronological order, so the sort is redundant. Either move the existence check to filter history_files before the sort decision, or condition on any_existed plus a tracking counter. Minor perf.

Documentation drift

sandbox/egg_agent_tools/handlers/brc.py:915 and :1015 reference "writer-side seam (orchestrator/routes/pipelines.py ~line 8406)" — the actual SLICE_ID_PATTERN.fullmatch(raw_slice_id) is on line 8417. Drifts every time pipelines.py shifts; consider replacing line numbers with a function/symbol reference (_write_brc_history).

Schema migration: confirmed safe under validate_assignment=True

I did want to flag a concern about _migrate_schema_version_to_1_1 mutating self.schemaVersion while EggContractBaseModel has validate_assignment=True (verified by test_pr_number_validator_re_runs_on_assignment). Walked through the flow:

  1. Pydantic v2 with validate_assignment=True re-runs mode="after" model_validators on field assignment.
  2. Inside the validator, self.schemaVersion = "1.1" triggers re-validation; the recursive call hits if self.schemaVersion == "1.0" → False, returns self, no further mutation.
  3. mode="wrap" validators (like _migrate_phases_to_slices) do not re-fire on field assignment, so _legacy_phases isn't re-stamped.

No infinite recursion. Worth a one-line # safe under validate_assignment=True note in the docstring for the next reader.

_populate_contract_from_plan preservation logic is correct

The preserved_branch / preserved_pr_number / preserved_deferred_actions capture-then-rebuild pattern at line 15216 reads cleanly. Important detail: when contract.pr is None initially, preserved_deferred_actions defaults to [] and context_branch/context_pr_number to None, which is the correct first-population behavior. The asymmetry (planner-emitted context_title/context_description refresh; orchestrator-populated fields preserve) is the right choice and is properly exercised by test_populate_contract_from_plan_preserves_deferred_actions.

Test coverage commendations

  • test_invalid_slice_id_pattern_treated_as_unattributed — exhaustive injection payloads (path traversal, separators, case mismatch, newline injection). Good defensive testing.
  • test_natural_sort_per_slice_iteration_order — pins iteration order against shuffled input; catches lexicographic regressions.
  • test_idempotent_per_slice_write — covers per-slice files AND the unattributed sibling under the #1714 idempotency contract.
  • The drift guard for writer↔reader BRC type symmetry is a solid pattern; suggest applying it to CONSENSUS_BRC_TYPES too (see above).

Summary

Foundational work is sound; the slice-1 acceptance criteria are well-met. None of the above is merge-blocking, but the CONSENSUS_BRC_TYPES drift risk and the stale-aggregate-cleanup edge case merit follow-up issues against #2548 before slices 3-4 land — both will become more visible once the orchestrator hooks start exercising real per-slice flows.

— Authored by egg

@james-in-a-box

james-in-a-box Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor

egg review completed. View run logs

1 previous review(s) hidden.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant