Rectify: Broker Provisioning Catch-All + Cook-Loop Opt-In Regression — Architectural Immunity (#4684) - #4696
Merged
Trecek merged 43 commits intoAug 20, 2026
Conversation
Trecek
force-pushed
the
impl-rectify-broker-provisioning-20260817-200349
branch
2 times, most recently
from
August 19, 2026 17:14
e38438f to
af44b0e
Compare
Replace the single opaque `except Exception: return "exploration_provisioning_failed"` catch-all in enable_exploration (and the three sibling catch-alls in tools_exploration.py, plus tools_evidence_reader.py's two) with typed branches keyed off the new ExplorationFailureCode(StrEnum) registry (mirrors FleetErrorCode/FLEET_ERROR_CODES). - Add ExplorationFailureCode enum (_type_enums.py) and EXPLORATION_FAILURE_CODES frozenset registry (_type_constants_registries.py), re-exported through core/__init__.pyi. - Add five named exception classes nested on OwnerBoundExplorationContextStore (TrustedRootMismatch, ServiceNotConfigured, SnapshotStale, StoreClosed, CapacityExceeded) and convert bind_session_scoped's six raw raises to use them, preserving each existing message string and exception base type. - enable_exploration now wraps its bind_session_scoped/enable_components calls to distinguish BindSessionScopedFailed/EnableComponentsFailed from the five named store exceptions, with a final catch-all producing UNEXPECTED_INTERNAL_ERROR instead of the old opaque code (preserving the "Never raises" contract). - tools_evidence_reader.py's two catch-alls get their own evidence_reader_unexpected_internal_error code, distinct from the misleading "broker_unavailable" label that conflated any bug with an actually-down broker. - New tests/server/test_enable_exploration_failure_codes.py: one test per precondition code (12 total), replacing the old catch-all-enshrining assertion in test_enable_exploration.py. - New tests/contracts/test_exploration_failure_code_registry.py: AST scan rejecting any unregistered "code" string literal in tools_exploration.py. Part of the #4684 broker-provisioning-catch-all-and-cook-loop-opt-in- regression rectify plan (step 2.1 of 13). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…g-field-has-consumer (#4684) Move the interactive content-policy checkpoint from an unconditional gate inside assert_interactive_ordering/validate_interactive_invocation to an opt-in flag carried on CmdSpec itself, mirroring the existing force_inactive_agent_teams builder-kwarg precedent. Wires the previously-dead AgentBackendConfig.force_claude_agent_teams_inactive config field into the cook CLI so it finally has a production consumer. - CmdSpec gains force_inactive_agent_teams: bool = False; all 16 constructor sites across claude.py, codex.py, _cmd_builder.py, launch_resolution.py, and _type_launch.py's launch-record (de)serializer now pass it explicitly. - assert_interactive_ordering (_headless_helpers.py) and validate_interactive_invocation (claude.py) both short-circuit when spec.force_inactive_agent_teams is False — the policy no longer fires for every interactive launch regardless of caller intent. - _session_cook.py resolves force_inactive from config.agent_backend.force_claude_agent_teams_inactive and threads it into both the probe-required and fallback build paths. - prepare_interactive_launch (_session_launch.py) fixes two propagation gaps: (1) its first build_interactive_cmd call never forwarded project_root, so force_inactive_agent_teams=True unconditionally raised "requires project_root"; (2) the second (executable-bound) call cannot itself carry force_inactive_agent_teams=True (build_interactive_cmd's own guard forbids combining it with executable=), so the resulting spec never carried the caller's intent — fixed via dataclasses.replace() after the call returns. - build_interactive_cmd's env-drift check ("interactive environment changed after executable binding") now excludes CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS from the comparison: when force_inactive_agent_teams=True neutralizes that var on the first (probe) call, the second (executable-bound) call cannot re-neutralize it (guarded), so a real ambient env value would otherwise always trip the drift check as a false positive. The var's own state is already validated by assert_agent_teams_inactive on whichever call requested force_inactive. - New tests/contracts/test_cmd_spec_force_inactive_field.py: AST-walks every CmdSpec(...) site in src/autoskillit/, asserting each passes the kwarg. - New tests/contracts/test_config_field_has_consumer.py: generalizes the inert-tracked:#NNNN discipline (tests/AGENTS.md) to config dataclass fields, scoped to AgentBackendConfig (the dataclass this plan wires). - New tests/cli/test_cook_settings_local_agent_teams.py: end-to-end cook() against a populated .claude/settings.local.json without stubbing validate_interactive_invocation — the exact composition PR #4613 broke. This test class of failure (72 pre-existing failures across test_fleet_dispatch.py, test_session_launch.py, test_interactive_cold_ launch_medium.py, test_reload_loop.py, test_terminal.py, and others) is now resolved now that the policy is opt-in instead of unconditional. Part of the #4684 broker-provisioning-catch-all-and-cook-loop-opt-in- regression rectify plan (steps 2.2+2.3 of 13). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…4684) Removes the double-gate where both assert_interactive_ordering (_headless_helpers.py) and backend.validate_interactive_invocation (claude.py) independently called _interactive_invocation_environment_policy — fixing one gate silently left the other still unconditionally firing. - assert_interactive_ordering is now cmd-ordering-only; the content-policy check lives solely behind backend.validate_interactive_invocation, which is already the sole call site (defined in the same module as the policy function it wraps). - The raw/non-managed branch of _run_interactive_session (used by ad-hoc fleet and campaign interactive sessions — cli/fleet/_fleet_session.py:89,199) never threads force_inactive_agent_teams (out of this plan's scope, same as _session_cook.py's wiring), so the policy check was always a no-op there; validate_interactive_invocation is deliberately NOT added to this branch, since for Codex it also enforces an unrelated, stricter contract (CODEX_HOME/SQLite-home env var matching) that only a managed session home satisfies — adding it would break real fleet/campaign Codex sessions, which never have one. - New scripts/check_single_enforcement_point.py: a bespoke AST call-site equivalence-class resolver (import-alias resolution, pure-return-wrapper equivalence classes, backend-specific exemption list) — no external prior art in this codebase for this shape of check. Wired as a new pre-commit hook. - New tests/contracts/test_single_enforcement_point.py: unit-verifies the resolver mechanism against synthetic fixtures (single/double call sites, import alias forms, wrapper equivalence classes, backend exemption accept/reject) plus an integration check against the real codebase. Part of the #4684 broker-provisioning-catch-all-and-cook-loop-opt-in- regression rectify plan (step 2.4 of 13). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…oration authority (#4684) - Extract _ReopenedLaunchAuthority, _safe_submit_failure_reason, and _ExplorationLaunchAuthorityStore out of exploration_context.py into a new sibling module exploration_context_durable.py, to stay under the 1100-line ceiling while adding new durable-authority machinery. One-way dependency (durable -> exploration_context); no circular import. - Add OwnerBoundExplorationContextStore.lease_for_capability() accessor so callers can resolve the in-memory lease minted for a just-bound capability. - Add bind_session_scoped_durable() free function in the new module: binds the in-memory capability via bind_session_scoped(), then writes an HMAC-signed 0600 durable authority file recording the same capability, mirroring bind_launch's durability guarantee for the session-scoped path. - Wire tools_exploration.py's enable_exploration to call bind_session_scoped_durable(...) with a per-session authority_home under ctx.temp_dir, instead of the bare in-memory bind_session_scoped(). - Fix grant/revoke asymmetry: enable_exploration granted FastMCP tag visibility via ctx.enable_components() but only ever revoked the in-memory lease via store.cleanup_session() on failure paths — never the tag itself. Add await ctx.disable_components(tags={"exploration"}) to the finally block so a later failure after a successful grant always revokes the tag. - Register both durable authority writers (bind_launch, bind_session_scoped_durable) in DURABLE_ARTIFACT_WRITERS (core/types/_type_constants.py). - Add tests/contracts/test_exploration_grant_revoke_symmetry.py: an AST check that every ctx.enable_components(...) call site in tools_exploration.py has a reachable ctx.disable_components(...) in the same function, plus a membership check that both durable writers are registered. Deliberately does NOT add pipeline/exploration_context*.py to test_durable_artifact_writers_guard.py's _SCOPED_MODULES — that guard's docstring explicitly rejects codebase-wide scope expansion beyond hook artifacts; the membership check above is the plan's actual specified home for this assertion. - Update tests/server/test_enable_exploration.py and test_enable_exploration_failure_codes.py: ctx fixtures now set disable_components = AsyncMock() and assert it is awaited with tags={"exploration"} on both the cancellation path and the enable_components-failure path. - Bump tests/arch/test_subpackage_isolation.py's pipeline file-count exemption from 18 to 19 for the new sibling module. test_check: 37859 passed, 613 skipped, 27 xfailed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…4684) - Add AgentBackendConfig.auto_provision_exploration: bool = False. When True and the boot-time/request-time session type is not in EXPLORER_INELIGIBLE_SESSION_TYPES, the exploration tag is pre-applied alongside the existing kitchen/plan-review reveals, mirroring force_claude_agent_teams_inactive's opt-in-on-the-artifact pattern. - Wire the gate into all three reveal call sites, each using whichever visibility primitive it already uses: - open_kitchen's ctx.enable_components branch (notification-capable backends): await ctx.enable_components(tags={exploration}). - open_kitchen's _use_global_enable/mcp.enable branch (backends without tools/list_changed support, e.g. Codex): mcp.enable(tags={exploration}). - _pre_reveal_kitchen (boot-time pre-reveal for non-notification backends): _mcp.enable(tags={exploration}). The HMAC capability lease minted by enable_exploration remains the sole authorization boundary in all cases — this only auto-provisions the weaker, ergonomic visibility gate. - Register auto_provision_exploration as a CONFIG_DEFAULT_INGREDIENTS entry (ingredient_defaults.py) with a defaults.yaml documented default and a lexicographically-inserted tests/contracts/config_key_ledger.txt entry (before agent_backend.backend). - Add KitchenStatusResult.broker_authority: str (server/tools/_types.py) and its runtime construction in tools_status.py's kitchen_status: reports ineligible_session_type for ORCHESTRATOR/FLEET sessions, else no_session_bound — kitchen_status has no per-request session/token context to determine a specific bound capability, so this reports session- type eligibility (the diagnostic the issue's motivation actually asks for: learn *before* the downstream zero-tool subagent refusal, not just after). Add the field to the pretty_output_hook formatter (_fmt_status.py's _fmt_kitchen_status + _FMT_KITCHEN_STATUS_RENDERED). - New tests/server/test_kitchen_status_broker_field.py (Step 1.4): the 3-case precondition matrix from the plan (skill -> no_session_bound; orchestrator/fleet -> ineligible_session_type). - New open_kitchen/_pre_reveal_kitchen auto-provision tests in test_tools_kitchen_visibility.py (both branches) and test_lifespan_skill_boot.py (boot-time pre-reveal, opted-in and opted-out cases) — not explicitly named in the plan's Step 1 list but added per AGENTS.md's 'add tests for new features' discipline; the boot-time tests isolate the new auto_provision_exploration flag from the pre-existing, separate 'exploration' feature gate (both must be on for the tag to actually surface — _pre_reveal_kitchen's unconditional feature-suppression pass would otherwise immediately undo the enable). - Fix-forward from the first test_check round: _fmt_status.py's formatter coverage registry, tests/infra/test_schema_version_convention.py's hardcoded (file, line) JSON-write-site allowlist (four call sites shifted by my new lines), and tests/server/_helpers.py's _PATCHED_DEFAULTS fixture (needed the new ingredient key for parity with CONFIG_DEFAULT_INGREDIENTS). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…lders (#4684) Replace tests/execution/test_launch_force_inactive_call_path.py's 5 hand-written build_*_cmd probes with a reflective discovery test in test_launch_force_inactive_call_path_reflective.py. The hand-written list silently passed when a sixth builder was added to ClaudeCodeBackend without the force_inactive_agent_teams parameter — nothing enumerated the actual builder surface, mirroring the reflective-enumeration pattern in tests/arch/test_backend_protocol_completeness.py. - BUILDER_METHOD_NAME_REGEX = r"^build_(?!inspector).*_cmd$" discovers build_food_truck_cmd, build_headless_cmd, build_interactive_cmd, build_resume_cmd, build_skill_session_cmd via dir(backend) — the same 5 the old hand-written list covered. Naturally excludes build_inspector_cmd (not launch-wired, per plan 2.2) and build_cmd (the pipeline-internal skill-command dispatch entry point, which forwards the field through but does not expose it as a caller parameter) without a hardcoded name list — neither matches build_<non-empty-middle>_cmd. - test_every_discovered_builder_accepts_force_inactive_agent_teams: the new signature-presence check the plan specifies (1.10). - Preserved the original 5 probes' actual behavioral assertions (env var actually stripped when force=True, left alone when force=False) rather than discarding them for the plan's literal signature-only pseudocode — removing real behavioral coverage would be a regression. Keyed them into a _BEHAVIORAL_PROBES registry plus a completeness test (test_every_discovered_builder_has_a_behavioral_probe) so a newly discovered builder without a wired probe fails loudly instead of being silently skipped. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…4684) Before this change, no SKILL.md or agent definition mentioned enable_exploration anywhere — rg -rln "enable_exploration" src/autoskillit/skills/ src/autoskillit/skills_extended/ src/autoskillit/agents/ returned zero files. A skill author who adds an <!-- autoskillit:exploration-vector id="..." --> marker shipped with no instruction that a preflight enable_exploration call is required. - Add a '> **Preflight:**' blockquote sentence (mentioning enable_exploration within 200 characters of the anchor) to all 49 skills_extended/*/SKILL.md files currently carrying the exploration-vector marker: 13 arch-lens-*, 18 exp-lens-*, 12 vis-lens-*, plus investigate, audit-docs, planner-analyze, planner-elaborate-phase, planner-extract-domain, scope. Inserted as the first paragraph after each file's H1 title heading. - New tests/skills/test_exploration_vector_preflight.py: enumerates every SKILL.md under skills/ and skills_extended/ containing the marker (mirrors test_explorer_conformance_preamble.py's enumeration shape) and asserts the structured preflight block — anchored by literal '> **Preflight:**' (case-insensitive) with enable_exploration within 200 characters. A loose, unrelated mention elsewhere in the file does not satisfy the contract. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…4684) Three consecutive rectify-adjacent changes to the same function/module (PR #4503 -> #4512 -> #4613/#4684) shipped with no CI signal that the underlying plan touched multiple import layers at once — PR #4613's plan touched >=3 layers across 141 files with no gate to flag it. - New tests/arch/test_rectify_blast_radius_guard.py: scans 'git diff --name-only HEAD~1 -- .autoskillit/temp/rectify/*.md' (not a static directory listing — .autoskillit/temp/ is gitignored per .autoskillit/.gitignore:1, so a directory scan would pass vacuously in every fresh CI clone with zero chance of ever firing) for any rectify plan committed in the most recent commit, counts distinct IL-0..IL-3 import-layer tokens referenced in it, and classifies: <=3 layers OK, 4-5 layers emits a pytest warning (explicit reviewer check), >5 layers is a hard test failure (decomposition required). The counting/ classification logic is pinned by direct unit tests independent of git state; the integration test passes vacuously in this worktree (the driving plan for this very implementation lives outside version control, per the same gitignore rule) — documented as the accepted, structural limitation of a git-diff-based scan versus a directory-listing scan that could never fire at all. - Extend skills_extended/rectify/SKILL.md's plan-split rule: split per layer when >3 distinct import layers are touched, even under the pre-existing 500-line threshold; >5 layers must be decomposed before implementation begins. Line count and layer count are independent triggers. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… namespace (#4684) - tests/server/test_lifespan_skill_boot.py: rewrite both _pre_reveal_kitchen auto-provision tests (added in Rectify 2.6) to assert on the mocked autoskillit.server.mcp singleton's .enable call history instead of the real cumulative mcp.list_tools() output. submit_exploration_query et al. also carry the pre-existing 'kitchen' tag; FastMCP's last-match-wins transform stack means _mcp.enable(tags={'kitchen'}) alone can make them visible whenever nothing *later* in the sequence disables 'exploration' specifically (e.g. when the separate 'exploration' feature flag is on) — a real-list_tools assertion was therefore passing or failing for reasons unrelated to the auto_provision_exploration opt-in itself. The call-recording assertion (mirroring the pattern already used for open_kitchen's two branches in test_tools_kitchen_visibility.py) tests exactly what Rectify 2.6 added: whether the new code calls enable(tags={'exploration'}), independent of the tag-union interaction with 'kitchen'. - src/autoskillit/skills_extended/rectify/SKILL.md: namespace the Rectify 2.9 plan-split-rule addition's skill cross-reference (/implement-worktree-no-merge -> /autoskillit:implement-worktree-no-merge) per tests/workspace/test_skills.py's cross-reference namespacing contract. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ocal.json (#4684) - Extend tests/cli/_cook_launch_helpers.py's arrange_cook with two new optional parameters, both additive/opt-in (existing call sites unchanged): - settings_content: dict | None — writes .claude/settings.local.json into the tmp_path project before cook runs. Refactored test_cook_settings_local_agent_teams.py (delivered in Rectify 2.2/2.3) to use it instead of its own duplicate write, removing the redundancy. - project_dir_override: Path | None — uses a caller-supplied project directory (e.g. the real repository root) instead of creating an empty tmp_path/project. Caller owns creating/populating that directory; the helper will not mkdir() or write settings_content into it, since doing so on a real, pre-existing directory would be destructive. - New tests/contracts/test_cli_cook_validator_stub_guard.py: an AST guard that fails when a tests/cli/ test both invokes cli.cook(...) and monkeypatches the *real* ClaudeCodeBackend.validate_interactive_invocation (3-arg class-target or 2-arg dotted-path form) — the exact composition that hid PR #4613's regression. Deliberately narrower than 'any validate_interactive_invocation stub': stubbing the real CodexBackend (a distinct, legitimately no-op-for-this-policy implementation — Codex has no agent-teams concept, out of scope per the plan's Step 6) or defining a standalone fake backend double (neither is 'hiding' any real production behavior) does not flag. A fully generic rule would immediately false-positive on 4+ pre-existing, legitimate Codex-focused tests. Five synthetic self-tests exercise both the two violation shapes and the three allowed shapes; a sixth confirms zero real violations exist today. - New tests/cli/test_cook_real_root_smoke.py (opt-in live gate, Step 1.13): gated by AUTOSKILLIT_COOK_REAL_ROOT_SMOKE=1, drives cli.cook() against this repository's own project root and its real .claude/settings.local.json (backed up and restored around each sub-case; restoration failure fails the test loudly rather than silently leaving the file test-modified). Asserts cli.cook() completes without error when force_inactive is False, and raises when CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 is active and force_inactive is True — the exact composition PR #4613 broke. A single test function (not two) so 'exactly one non-skipped test' holds. - New Taskfile.yml task test-smoke-cook-real-root, mirroring test-smoke-claude-explorer-live-gate's preconditions (claude installed + isolated auth present) and post-validation shape (JUnit non-skipped-count check), simplified to a stdout ValueError-absence check in place of the explorer gate's evidence-JSONL correlation check (this gate has no analogous evidence stream to validate). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… env-parity registry (#4684) Rectify 2.10's new Taskfile.yml task-smoke-cook-real-root sets AUTOSKILLIT_COOK_REAL_ROOT_SMOKE in its env: block, which test_every_taskfile_override_is_registered requires be registered in TEST_HARNESS_ENV_OVERRIDES (or the non-parity allowlist) — the double-bind pincer that prevents an unregistered Taskfile override from silently masking a failure class. Registered with the same shape as the sibling AUTOSKILLIT_CLAUDE_EXPLORER_LIVE_GATE/AUTOSKILLIT_WEB_AGENT_LIVE_GATE entries: opt-in-only, no parity fixture needed since ordinary test tasks never set it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…her extension (#4684) Step 1.14 — tests/server/test_claude_explorer_live_gate_envelope_code.py (new file, does not modify the existing live gate): - The existing test_claude_explorer_live_gate.py verifies only that the parent Claude session replied LIVE_OK; it never verifies enable_exploration itself succeeded. A future broker regression that swallows a precondition failure into a typed code (or the unexpected_internal_error catch-all — the exact bug class #4684 fixes) could still produce a LIVE_OK reply via some other retry path, uncaught by the existing gate. - Imports the existing gate's repository/plugin/MCP-config setup helpers by name rather than duplicating or refactoring them, so the already-verified live gate is untouched. - Instruments tools_exploration._failure(code) — the single module-level helper every except-branch in enable_exploration (including the final catch-all) routes through — using the same 'reassign a module-level name, called via bare-name lookup from the target function's body' pattern the existing gate already uses successfully for consume_exploration_request_record. (Reassigning the already-@mcp.tool() -decorated enable_exploration function itself was considered and rejected: the running MCP framework captures a direct reference to it at registration time, not a re-lookupable module attribute, so a later reassignment would not actually intercept dispatched calls.) Recording every _failure(code) invocation is a more direct test of 'the happy path was taken, not the catch-all' than trying to capture the inline-built success envelope: zero recorded failure codes plus a LIVE_OK reply is exactly the property this gate needs to hold. Step 1.3 — tests/hooks/test_exploration_request_identity_guard.py (extended, not replaced): - Add the four real production runtime tool names (mcp__plugin_autoskillit_autoskillit__*) to the existing parametrized matrix — every prior case used a synthesized name; none was the actual shape the plugin emits, despite the matcher regex being purpose-built for it. Verifies end-to-end through subprocess.run against the real shape. - Add a new AUTOSKILLIT_AGENT_BACKEND x AUTOSKILLIT_HEADLESS env-var matrix test (3 x 4 = 12 combinations) pinning that both checks in the guard script use exact equality (== "codex", == "1"), not truthiness — "true" is a truthy string in many languages' conventions but must NOT trigger the skip since it doesn't equal "1". Extracted _run's subprocess invocation into a new _run_with_env(payload, env) so the matrix test can set exact string env values (including "") that _run's bool-only headless parameter can't express, without changing _run's own signature or any of its existing call sites. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fix E / AC5) Issue #4684 AC5 requires a fallback agent for when enable_exploration returns ineligible_session_type or exploration_store_unavailable. The codebase's agent discipline is specialist-only (agents/AGENTS.md; docs/execution/explorer-agents.md mandates specialist terminal leaves; tests/server/test_tools_agents.py:434-479 assert packless agents must not appear in AGENT_PACK_REGISTRY) — registering general-purpose.md would contradict it. Tool restrictions come from frontmatter (tools:), not prompt content, so a restricted specialist is the architecturally consistent fix. - New src/autoskillit/agents/pluginless-explorer.md: a third terminal-leaf explorer specialist, tools: [Read, Grep, Glob], packless (subagent_type-only per agents/AGENTS.md:56-59 — only step 1 of the Adding Agents procedure is required). Body mirrors semantic-code-navigator.md's shape (Role + Verdict section) adapted for the restricted tool surface; includes the '## Verdict' structured-output marker every agents/*.md file requires per test_tools_agents.py's generic frontmatter/structured-output/ tool-body-reference checks (verified against all three directly). Deliberately has no codex: frontmatter block (Claude-only fallback) and is NOT added to BUNDLED_EXPLORER_ROLES — that set drives Codex's per-child terminal-binding machinery (explorer_projection.py, server/_explorer_projection.py's strict-equality discovery check) for the two broker-bound MCP-tool explorers; pluginless-explorer has an unrelated tool surface and is not part of that binding contract. - agents/AGENTS.md: document the new specialist alongside the existing prose sentence for semantic-code-navigator/repository-impact-profiler (the same 'packless terminal-leaf explorer' documentation pattern, not the general packless-agents enumerated list). - New tests/server/test_fallback_specialist_agent_registered.py (Step 1.15): asserts the specialist exists with the correct restricted tool surface, that no general-purpose.md was created, that it stays out of BUNDLED_EXPLORER_ROLES, and reflective discovery via an agents/ directory walk (mirrors test_launch_force_inactive_call_path_reflective.py's pattern) finds it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…-explorer (#4684) Rectify 2.12 registered a new agent (pluginless-explorer.md), discovered by load_bundled_agent_definitions() alongside the other 22 bundled agents. test_skill_child_roles_have_bounded_tools_and_usage_descriptions had a hardcoded len(definitions) == 22 assertion; bump to 23. No _SKILL_CHILD_ROLE_EXPECTATIONS entry is needed — pluginless-explorer is a terminal explorer specialist (like semantic-code-navigator and repository-impact-profiler, neither of which has an entry either), not a skill-child-role in that dict's sense. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Issue #4684 AC6 requires 'No new failure mode is introduced for the unaffected skills (regression test on each).' A hardcoded count assertion breaks on any new SKILL.md addition; a KNOWN_UNAFFECTED_SKILL_IDS registry follows the codebase's retirement-registry discipline instead (modelled on RETIRED_SKILL_NAMES). - Add KNOWN_UNAFFECTED_SKILL_IDS: frozenset[str] to core/types/_type_constants.py, immediately after RETIRED_SKILL_NAMES. Populated from a live walk of skills/ and skills_extended/ (92 skills) — a skill is UNAFFECTED iff its SKILL.md carries no '<!-- autoskillit:exploration-vector id="..." -->' marker. Deliberately excludes exp-lens-*/vis-lens-* (dormant markers, not yet wired via for_each) despite the source investigation's own narrative prose calling them 'Unaffected' — the registry's own contract test asserts registered skills carry zero markers, so a marker-bearing skill (dormant or not) cannot be included; a dormant marker could be wired up in a future PR without this registry's knowledge, which is exactly the drift the AC6 regression test exists to catch. Plan location deviation: the plan specified src/autoskillit/skills/_unaffected_skill_registry.py, but skills/ is a namespace package holding only SKILL.md content today (no __init__.py, no other .py files) — adding importable code there means every import creates a skills/__pycache__/ directory, which scripts/check_doc_counts.py's naive 'every directory under skills/ is a skill' counter then miscounts as an extra skill (141 -> 142, reproduced locally and confirmed via a real pre-commit failure before relocating). core/types/_type_constants.py is the architecturally consistent home: it's where the actual RETIRED_SKILL_NAMES/AGENT_PACK_REGISTRY/ DURABLE_ARTIFACT_WRITERS precedents this plan cites all already live. Exported via core/__init__.pyi (alphabetically positioned stub line). - New tests/contracts/test_unaffected_skill_registry.py: _discover_unaffected_skills() applies the same negative predicate live against the current skills/ and skills_extended/ trees. test_unaffected_skill_set_is_stable catches both directions of drift (new UNAFFECTED skill not registered; registered skill no longer UNAFFECTED). test_no_new_broker_coupling_in_unaffected_skills is parametrized per registered skill and asserts zero exploration-vector markers AND zero enable_exploration references (the indirect-coupling failure mode AC6 guards against). A sanity test pins the predicate against one known-affected (arch-lens-c4-container) and one known-unaffected (open-kitchen) skill so a broken predicate can't pass vacuously. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ol counts (#4684) - tests/arch/test_subpackage_structure.py::test_type_constants_split_completeness: 167 -> 168 total __all__ symbols across the _type_constants* split modules, for Rectify 2.13's new KNOWN_UNAFFECTED_SKILL_IDS export. - tests/workspace/test_agent_definition_rendering.py:: test_all_builtin_only_agents_rendered_byte_identical: bundled_definitions 22 -> 23 and originals (built-in-tool-only agents, no mcp__ tools) 19 -> 20 for Rectify 2.12's pluginless-explorer.md (tools: [Read, Grep, Glob] — no mcp__ tools, so it counts as built-in-only). A sibling to the tests/core/test_agent_definition.py fix already applied after 2.12 — this one was missed in that pass since I moved straight to 2.13 without a fresh full test_check round in between; caught by this round's full run. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…every dataclass
Independent audit found the contract test scoped to AgentBackendConfig
only, contradicting the plan's explicit spec ('walk every field of every
@DataClass in config/_config_dataclasses.py'). Widen _ENFORCED_DATACLASSES
to reflectively enumerate every dataclass directly defined in
_config_dataclasses.py.
Widening surfaced two real methodological gaps in the detector itself:
- dataclasses.fields() vs raw __dataclass_fields__: the latter includes
ClassVar pseudo-fields (RunSkillConfig._EXIT_GRACE_BUFFER_MS), producing
a false violation. Switched to dataclasses.fields().
- Fields consumed only via a method defined on their own dataclass
(GitHubConfig.allowed_labels, read inside check_label_allowed/
check_labels_allowed, called from 6 real tools_* sites) were invisible
to the direct '.field_name' grep. Added a two-hop detector: a method
(excluding __post_init__, which is auto-invoked regardless of whether
the field's value ever reaches real behavior) that reads self.field_name
and is itself called externally counts as consumption.
Widening also surfaced two genuine pre-existing orphans, unrelated to
- RunSkillConfig.natural_exit_grace_seconds — validated in __post_init__
but never threaded into execution/process/__init__.py's same-named
parameter at either real call site.
- ProviderProfileDef.context_window — validated in __post_init__ but
server/_guards.py's _profile_to_env (the sole translator of profile
fields into effective behavior) never reads it.
Both annotated inert-tracked:#4693 (tracking issue opened for this
discovery) rather than wired here, to avoid scope creep into unrelated
config debt. The escape hatch is now also exercised against a real field
(natural_exit_grace_seconds), not only a synthetic fixture, per the plan's
own requirement.
Refs #4684.
…_exploration.py siblings
Independent audit found submit_exploration_query/get_exploration_page/
resume_exploration_context still on their original opaque
'except Exception: return _failure(_FAILURE_BROKER_UNAVAILABLE)'
catch-all — only enable_exploration was refactored in Phase 2.1, despite
the plan explicitly listing all four functions ('refactor :407-478 and
the three sibling catch-alls at :299, :350, :397').
Their failure surface is narrower than enable_exploration's (no
bind_session_scoped/enable_components calls), so the typed-branch shape
differs: a new StoreUnavailable marker exception (mirroring
BindSessionScopedFailed/EnableComponentsFailed) replaces the bare
'raise RuntimeError("exploration context store is unavailable")' at the
two sites that raise it (submit_exploration_query,
_fetch_page_from_launch_environment, shared by the other two siblings).
Each function now distinguishes:
- StoreUnavailable -> BROKER_UNAVAILABLE (a named, specific condition —
no exploration context store configured for this session)
- any other exception -> UNEXPECTED_INTERNAL_ERROR (the new generic
fallback, same code enable_exploration already uses; logged via
log.warning(exc_info=True), never re-raised, preserving the
'Never raises' contract)
Both codes already existed in ExplorationFailureCode — no enum change
needed. Added regression tests for both branches across all 3 siblings.
Refs #4684.
…discipline in tests/contracts/AGENTS.md
Independent audit found this file byte-identical to develop despite the
plan explicitly listing it under Phase 2.3's file list ('new section
documenting the discipline'). Added a section mirroring the sibling
run_skill Parameter-Role Ledgers doc in tests/AGENTS.md, documenting the
liveness predicate (direct reader, indirect method-reader, or
inert-tracked:#NNNN), why it re-derives on every run instead of using a
frozen ledger, and the two pre-existing orphans REQ-007's widening
surfaced.
Refs #4684.
…cement gap as explicit out-of-scope Independent audit confirmed the exact trap the plan's own Step 2.4 warned against: assert_interactive_ordering's inline policy call was removed without an equivalent call reaching the raw/non-managed _run_interactive_session branch (the sole launch path for ad-hoc fleet/campaign interactive sessions), leaving only an explanatory code comment rather than the plan's specified remediation. Investigated both remediation options the audit offered: (a) add a Claude-backend-guarded enforcement call here, or (b) document the gap explicitly in the plan's Out-of-Scope section. Chose (b), after confirming (a) is far larger and more redundant than it first appears: _run_interactive_session has NO force_inactive_agent_teams parameter at all in this plan's scope — neither branch ever threads it, so a guarded call alone would still be a no-op; the real fix requires threading the flag through _run_interactive_session's signature and both branches, plus _launch_cook_session and its callers. develop's tip already ships exactly this (0b1b412 / #4688, 'Interactive Policy Checkpoint Immunity'), independently, via a differently-shaped architecture (a single inline call inside assert_interactive_ordering, gated on spec.force_inactive_agent_teams, rather than this plan's Step 2.4 single-enforcement-point consolidation into backend.validate_interactive_invocation). Hand-duplicating that fix here would produce two competing, partial implementations of the same corridor that must be reconciled against develop anyway. Updated the plan's own §6 Out-of-Scope section (main repo, not this worktree — plans live outside the implementation worktree) with the full investigation trail, and tightened the code comment here to reference it. The Codex-incompatibility rationale for not calling the generic validate_interactive_invocation Protocol method is unchanged and still applies regardless of which remediation path closes this gap. Refs #4684, #4688.
…efactors Post-rebase reconciliation of semantic conflicts that survived the textual merge (no conflict markers, but wrong against the new integration base): - _type_launch.py: drop the duplicate "force_inactive_agent_teams" key from the launch-record command payload. Both sides added it independently and git merged them cleanly into one dict; ruff F601 caught the duplicate. develop's placement (after process_idle_timeout_ms) is kept. - test_subpackage_structure.py: bump the hardcoded _type_constants symbol count 168 -> 169. develop added CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT_ENV_VAR (+1) while this branch added EXPLORATION_FAILURE_CODES and KNOWN_UNAFFECTED_SKILL_IDS (+2); neither side's count absorbed the other's. - test_cook_settings_local_agent_teams.py / test_cook_real_root_smoke.py: rebase the expected outcome onto #4688's remediation strategy. develop now neutralizes a conflicting env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS settings entry before the inactivity assertion rather than refusing the launch, so cook no longer raises for that case. Both tests keep their purpose — the real, unstubbed policy running against a real settings file — and now assert the file is actually rewritten and the launch env is agent-teams-free, plus that the opt-in leaves the repository untouched when off. - tests/contracts/AGENTS.md: follow develop's rename of force_claude_agent_teams_inactive -> force_inactive_agent_teams. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…al AST-parse assertions it never used test_escape_hatch_and_reader_detection_on_synthetic_fields claimed to exercise _field_has_consumer/_field_is_inert_tracked against synthetic fields but only ever asserted _INERT_TRACKED_RE against a hand-typed comment_block string; the synthetic_source/ast.parse/isinstance lines exercised nothing the assertions depended on. Real escape-hatch/consumer-detection coverage already lives in test_natural_exit_grace_seconds_is_inert_tracked_against_a_real_field and test_allowed_labels_is_consumed_indirectly_via_a_dataclass_method.
…ec call-site scan _cmd_spec_call_sites() smuggled file provenance through node._autoskillit_file with a silent "?" fallback if ever missing. Return (Path, ast.Call) tuples instead, matching the established multi-file ast.walk idiom already used by test_maintenance_install_argv_contract.py and test_no_backend_name_bypass.py.
…tive launch branch The 24-line comment claimed #4688 (commit 0b1b412) "already shipped on develop... threading the flag through both branches on its own single-inline-call architecture." Verified via git show that commit never touched this file; its actual change removed an inline env-policy call elsewhere, converging on the single-enforcement-point design this branch already uses — the opposite of what the comment described. Replaced with a concise, durably-true summary and dropped the plan-section/date citations.
…ng-fragmentation leak _safe_submit_failure_reason replaced sensitive_values in declaration order. authority_path frequently contains cwd/repository_root as a literal substring, and shorter fields (session_id) can be substrings of longer ones (source_identity). Redacting a shorter value first fragments a later, longer value's match, causing that replacement to silently no-op and leaving cleartext fragments next to a "[redacted]" token. Sorting by descending length before redacting eliminates the fragmentation window.
… invariant assert is stripped under python -O/PYTHONOPTIMIZE, and lease is None is reachable in a genuine race (a concurrent bind_session_scoped for the same session_id can evict this thread's just-minted capability via _discard_session_locked before lease_for_capability runs). Stripping the assert would let a None lease flow into lease.snapshot_digest/expires_at below as an unguarded AttributeError. Matches this module's existing plain-builtin-exception idiom (cf. ValueError in _ExplorationLaunchAuthorityStore.write).
bind_session_scoped_durable mints and registers a lease in-memory via store.bind_session_scoped, then writes the durable authority record. If the write raises (ValueError on a bad authority_home, or OSError from the file write/chmod), the lease was already committed with no compensating cleanup_session call on this path, orphaning it until TTL expiry. Wrap the write in try/except and call store.cleanup_session(session_id) before re-raising, mirroring the compensating-cleanup pattern already used at the tools_exploration.py call site for the enable_components failure case.
…ndependently Three related findings on the same finally block: (1) an unguarded cleanup_session/disable_components failure could mask an in-flight exception; (2) cleanup_session ran before disable_components with no isolation, so a cleanup_session raise skipped disable_components entirely, defeating the "partial-success enable_components never leaves the tag visible without a live lease" invariant the block exists to guarantee; (3) disable_components had no defensive wrapping while enable_components above it does. Wrapping each call in its own try/except-and-log makes both attempted unconditionally and resolves all three.
…e failure codes _FAILURE_INVALID_REQUEST, _FAILURE_CONTEXT_UNAVAILABLE, and _FAILURE_BROKER_UNAVAILABLE are module-level-aliased because each is referenced from multiple functions; UNEXPECTED_INTERNAL_ERROR is referenced from all 4 mcp.tool() handlers yet was the one holdout referenced directly, breaking the file's own "alias iff referenced from >1 call site" convention. Single-use codes are left unaliased, unchanged.
…cate The auto-provision eligibility check (ctx.config.agent_backend.auto_provision_exploration and session_type not in EXPLORER_INELIGIBLE_SESSION_TYPES) was duplicated verbatim in _session_boots.py's _pre_reveal_kitchen and _open_kitchen.py's open_kitchen, both introduced in the same commit. Extract a pure predicate function next to EXPLORER_INELIGIBLE_SESSION_TYPES in pipeline/exploration_context.py, re-export it from pipeline/__init__.py, and use it at both call sites.
…mismatch bind_session_scoped raised TrustedRootMismatch for a malformed source_identity as well as for an actual repository_root mismatch, so both distinct precondition failures produced the same trusted_root_mismatch diagnostic. Add a dedicated InvalidSourceIdentity(ValueError) exception and INVALID_SOURCE_IDENTITY failure code, and wire it through enable_exploration's re-raise tuple and except clause following the same one-code-per-precondition pattern already used for the other 5 exceptions. EXPLORATION_FAILURE_CODES is auto-derived as frozenset(ExplorationFailureCode), so the new code self-registers with no separate registry file to update.
The rebase onto upstream/develop (which now includes #4665's _command_classification.py decomposition and #4694's ambient-env ground-truth surface) shifted line numbers referenced by two line-number-keyed registries in tests/_ambient_env_surface.py, and made one explicit env-scrub call redundant: - FORWARDING_SITES: execution/backends/codex.py sites shifted 514->516, 648->651, 767->771, 839->844. - DYNAMIC_READ_EXEMPTIONS: server/_lifespan/_session_boots.py 455->468; server/tools/tools_evidence_reader.py 170->174. - tests/cli/test_cook_real_root_smoke.py:107's explicit monkeypatch.delenv("CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS") is now redundant with #4694's autouse _scrub_ambient_env fixture (the var has disposition="scrub" in AMBIENT_ENV_DISPOSITIONS); removed per tests/arch/test_no_adhoc_env_workarounds.py's guard. Verified: all shifted line numbers checked against actual on-disk positions; tests/contracts/test_ambient_env_surface.py (26), tests/arch/test_no_adhoc_env_workarounds.py (3), and tests/cli/test_cook_real_root_smoke.py (1 skipped, opt-in gated) all pass locally. pre-commit run --all-files clean. The remaining CI failure (test_sigterm_writes_scenario_json, rc=-15) is byte-identical to develop and untouched by this branch's history — passes locally in isolation; treated as pre-existing CI flakiness, not a regression from this PR. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…4699) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
kitchen_status's broker_authority literal, two agent doc descriptions, and the parametrized test all hand-typed "ineligible_session_type" (word order reversed) instead of the registered ExplorationFailureCode.SESSION_TYPE_INELIGIBLE = "session_type_ineligible" value that enable_exploration actually returns for this condition. tools_status.py now references the enum's .value directly so the two vocabularies cannot drift again. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
When module == SRC_ROOT.name (a bare `import autoskillit` with no submodule), the function fell through to "" + ".py" = ".py", which never matches any real key in the parsed relpath dict (the root package file is keyed "__init__.py"). Return "__init__.py" directly for this case instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ontext_durable - Module docstring claimed "zero imports from exploration_context"; the module actually has one TYPE_CHECKING-only annotation import. Reworded to state zero *runtime* imports, which is the actually-true claim. - bind_session_scoped_durable's docstring claimed authority_home is resolved via a server/_factory.py session_authority_home accessor that does not exist anywhere in the codebase (confirmed by full-repo grep). Reworded to describe what the sole caller actually does. - _is_capability_shape duplicates OwnerBoundExplorationContextStore._is_capability_shape in exploration_context.py (intentional, to avoid an import cycle — same rationale already documented for the neighboring _MAX_CAPABILITY_LENGTH constant). Added the same duplication-acknowledgment comment here so both copies are flagged for coordinated updates. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…able_exploration Both wrap arbitrary underlying failures from store.bind_session_scoped and ctx.enable_components (raise ... from exc), but their except clauses returned an opaque failure code with no server-side trace, unlike the final except Exception branch which already logs with exc_info=True. For a genuine unexpected failure inside either call, nothing was recorded server-side. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rride arrange_cook()'s settings_content write was nested inside the branch that only runs when project_dir_override is None. Passing both together silently dropped settings_content with no error, even though the caller's intent was clearly to seed a settings file — a footgun for future callers extending the real-root live gate. Raise ValueError instead of silently no-op'ing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_cmd_spec_call_sites() only matched bare-name CmdSpec(...) calls (ast.Name), silently skipping a qualified/attribute-path construction site (e.g. module.CmdSpec(...)), which would never be flagged for omitting force_inactive_agent_teams= — defeating the contract's stated guarantee. Mirrors the dual ast.Name/ast.Attribute handling already used by _stubs_real_claude_backend_validator in test_cli_cook_validator_stub_guard.py for the analogous problem. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… test _field_source_comment()/_field_is_inert_tracked() looked up a field's preceding doc-comment by field_name alone, scanning the whole file and returning the FIRST 4-space-indented match — unlike _has_indirect_method_reader()/_class_node(), which correctly scope to the dataclass under test. If two enforced dataclasses shared a field name and only the earlier-in-file one carried an inert-tracked:#NNNN annotation, the later class's genuinely orphaned field would be misattributed that annotation and silently pass the contract. Scope both functions to cls's ast.ClassDef line range via the existing _class_node() helper. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ceholder Defined but never consulted by the scan predicate (test_every_code_ literal_is_a_registered_exploration_failure_code() does not reference it). Its own comment admitted it exists purely for a hypothetical future envelope shape — exactly the "declared but unread" anti-pattern this same PR's test_config_field_has_consumer.py argues is dangerous because it makes reviewers believe a gate exists when none does. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ration The notification-backend auto_provision_exploration path has both an opt-in and opt-out test, but the _use_global_enable branch (mcp.enable path) only had an opt-in test — an asymmetric coverage gap in the very feature (#4684 Fix D) this PR adds. Mirrors test_open_kitchen_skips_exploration_for_notification_backend_without_opt_in. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…y edit Adding ExplorationFailureCode to tools_status.py's import block (prior commit) shifted the file's grandfathered json.dumps(mcp_data) write site from line 579 to line 580. Update the line-number-pinned allowlist entry in test_current_json_write_sites_match_allowlist to match. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Trecek
force-pushed
the
impl-rectify-broker-provisioning-20260817-200349
branch
from
August 20, 2026 00:35
10e93ec to
e5a52a9
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the rectify plan for issue #4684 (broker provisioning catch-all swallow + cook-loop opt-in regression) via "architectural immunity by construction": typed exploration-broker failure codes, opt-in
force_inactive_agent_teamsonCmdSpec, config-field-has-consumer discipline, single-enforcement-point AST guard, capability-tied grant/revoke symmetry, broker auto-provisioning, skill preflight docs, blast-radius gating, CLI composition tests, live-gate hardening, apluginless-explorerfallback specialist, and theKNOWN_UNAFFECTED_SKILL_IDSregistry (13 numbered sub-plans, "Rectify 2.1"–"2.13").An independent
/audit-implpass then found 4 real gaps against the plan's own text, all fixed here as 4 additional commits:test_config_field_has_consumer.pywas scoped toAgentBackendConfigonly, contradicting the plan's "every dataclass" spec. Widened reflectively to all 31 dataclasses inconfig/_config_dataclasses.py; fixed two detector bugs the widening surfaced (ClassVar pseudo-fields, method-mediated consumption); two genuine pre-existing orphans found and tracked via Orphaned config fields: RunSkillConfig.natural_exit_grace_seconds, ProviderProfileDef.context_window have no production consumer #4693 rather than wired (out of Exploration broker provisioning fails silently — enable_exploration returns exploration_provisioning_failed and broker-only subagents spawn with zero tools #4684's scope).tools_exploration.pystill had the original opaque catch-all — onlyenable_explorationwas refactored. Refactored all 3 to typed failure codes.tests/contracts/AGENTS.mdwas never updated to document the new discipline. Added.force_inactive_agent_teamsthrough_run_interactive_session's signature and both its branches, which is outside this plan's actual scope (the cook-loop opt-in regression specifically). Documented explicitly in the plan's own §6 Out-of-Scope section instead of duplicating work.develophas moved significantly since this branch's fork point (5e3a29a86). Commit0b1b412c0(#4688, "Interactive Policy Checkpoint Immunity") independently re-solved much of the same architectural ground as this plan's Phases 2.2–2.4 and 2.7, via a different design (a single inline call insideassert_interactive_orderinggated onspec.force_inactive_agent_teams, vs. this plan's single-enforcement-point consolidation intobackend.validate_interactive_invocation).13 files are touched by both branches, including a direct conflict: this branch deletes
tests/execution/test_launch_force_inactive_call_path.py(replaced by a reflective test), while #4688 extends the same file. Reconciliation is expected to require either (a) a manual rebase favoring #4688's already-tested implementation on the shared files, retaining only this branch's exploration-broker-specific work (Phases 2.1, 2.5, 2.6, 2.8–2.13, which #4688 doesn't touch), or (b) a fresh, smaller re-plan scoped to just that non-overlapping work.Verification
task test-check: 38096 passed, 613 skipped, 27 xfailed, 0 failed (on this branch's own base, before reconciling withdevelop's current tip).Implementation Plan
Plan file:
.autoskillit/temp/rectify/rectify_broker_provisioning_and_cook_opt_in_immunity_2026-08-17_235000.md🤖 Generated with Claude Code via AutoSkillit
Co-authored-by: Claude Sonnet 5 noreply@anthropic.com