Egg/issue 2548/slice 1 - #2571
Conversation
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.
…/egg-exec-20260508-042216-3164/work
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>
There was a problem hiding this comment.
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/requeststo api.anthropic.com, no pinnedclaude-*-YYYYMMDDmodel 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
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
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:1082 — if 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:
- Pydantic v2 with
validate_assignment=Truere-runsmode="after"model_validators on field assignment. - Inside the validator,
self.schemaVersion = "1.1"triggers re-validation; the recursive call hitsif self.schemaVersion == "1.0"→ False, returnsself, no further mutation. mode="wrap"validators (like_migrate_phases_to_slices) do not re-fire on field assignment, so_legacy_phasesisn'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_TYPEStoo (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
|
egg review completed. View run logs 1 previous review(s) hidden. |
No description provided.