Rectify: Fixed-set join contract and Claude team-dispatch isolation (#4575, #4520) - #4613
Merged
Conversation
Collaborator
Author
|
Audit remediation round — 13/13 blocking findings addressed in commits 20d8643 and e2ae3b7. Highlights:
Ready for re-audit. |
Trecek
added a commit
that referenced
this pull request
Aug 17, 2026
C1 (tests): Restore script-presence assertion in test_hooks_json_matches_hook_registry_after_generate that was lost during the matcherless-rendering refactor. C2 (tests): Fix or-vs-and typo in test_claude_required_join_emits_keep_batch_first_directive — both 'name=' and 'name@' must be absent. C3 (tests): Rename test_missing_binding_fails_closed_for_join_required to test_missing_binding_defaults_to_permissive and align the docstring with the actual permissive-default assertion. C4/C5/C6 (defense/bugs): build_interactive_cmd gains a project_root parameter, runs _resolve_project_root_for_inactive_check when force_inactive_agent_teams=True (matching the headless/skill builders), and explicitly refuses the force=True + executable combination that previously caused the executable-binding guard to raise after legitimate env neutralization. The Protocol signature is updated in lockstep; Codex's build_interactive_cmd accepts the same parameter (no-op — Codex has no team concept). C7 (bugs): _aggregate_wave_outcome falls through to WAVE_PENDING for mixed terminal outcomes (e.g. success+missing). Introduce WAVE_PARTIAL and add it to _NON_SUCCESS_WAVE_OUTCOMES so mixed-terminal waves no longer silently stall downstream join checks. C8 (bugs): claim_assignment's duplicate tool_use_id guard now fires regardless of whether the prior entry is pending — emitting the same id twice corrupts downstream bookkeeping even if the prior claim never settled. C9 (overengineering): SkillProjectionBinding loses six speculative by_member Mapping fields (join_required_by_member, child_spawn_cardinality_by_member, artifact_digest_by_member, artifact_incarnation_by_member, semantic_digests_by_member, adaptation_digests_by_member) — they were populated but never read. Removed the population logic from workspace/skill_projection.py (artifact_digest / artifact_incarnation parameters were dead inside the function). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Trecek
added a commit
that referenced
this pull request
Aug 17, 2026
- tools_kitchen.py: _declare_join_batch_handler now sums declared
per-role child_spawn_cardinality values rather than reading only
the first one. A plan declaring multiple distinct role counts
(e.g. {role_a: 2, role_b: 1}) is now correctly validated against
the sum.
- tools_kitchen.py: _emit_join_diagnostic no longer maintains a
local subset of DIAGNOSTIC_KEYS. The canonical frozenset in
hooks/_hook_settings.py is the single source of truth for what
fields may appear in a diagnostic record.
- background_exec_guard.py / join_followup_guard.py /
join_stop_guard.py: replace bare open(...).read() with a
`with open(...) as handle` block to close the file descriptor
deterministically.
- join_stop_guard.py: drop dead json.JSONDecodeError/ValueError
handlers around sys.stdin.read() — those exceptions can never
be raised by a plain read().
- join_claim_guard.py: remove the no-op _resolve_top_level_parent
wrapper that accepted and discarded a parameter to return a
hardcoded marker. Inline the constant string at the call site.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Trecek
added a commit
that referenced
this pull request
Aug 17, 2026
- tests/hooks/test_spawn_before_await_subordinate.py: promote the inline `from autoskillit.hooks._join_ledger import active_batch` block to the module-level import and use WAVE_PENDING / WAVE_COMPLETE constants instead of the bare `"pending"` / `"complete"` string literals. - tests/execution/test_launch_force_inactive_default.py: drop the inner `import pytest` from `test_force_inactive_without_project_root_refuses` — pytest is already imported at the module top. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Trecek
force-pushed
the
impl-rectify-join-20260815-133231
branch
from
August 17, 2026 18:12
d133acf to
a391aaf
Compare
Trecek
added a commit
that referenced
this pull request
Aug 17, 2026
C1 (tests): Restore script-presence assertion in test_hooks_json_matches_hook_registry_after_generate that was lost during the matcherless-rendering refactor. C2 (tests): Fix or-vs-and typo in test_claude_required_join_emits_keep_batch_first_directive — both 'name=' and 'name@' must be absent. C3 (tests): Rename test_missing_binding_fails_closed_for_join_required to test_missing_binding_defaults_to_permissive and align the docstring with the actual permissive-default assertion. C4/C5/C6 (defense/bugs): build_interactive_cmd gains a project_root parameter, runs _resolve_project_root_for_inactive_check when force_inactive_agent_teams=True (matching the headless/skill builders), and explicitly refuses the force=True + executable combination that previously caused the executable-binding guard to raise after legitimate env neutralization. The Protocol signature is updated in lockstep; Codex's build_interactive_cmd accepts the same parameter (no-op — Codex has no team concept). C7 (bugs): _aggregate_wave_outcome falls through to WAVE_PENDING for mixed terminal outcomes (e.g. success+missing). Introduce WAVE_PARTIAL and add it to _NON_SUCCESS_WAVE_OUTCOMES so mixed-terminal waves no longer silently stall downstream join checks. C8 (bugs): claim_assignment's duplicate tool_use_id guard now fires regardless of whether the prior entry is pending — emitting the same id twice corrupts downstream bookkeeping even if the prior claim never settled. C9 (overengineering): SkillProjectionBinding loses six speculative by_member Mapping fields (join_required_by_member, child_spawn_cardinality_by_member, artifact_digest_by_member, artifact_incarnation_by_member, semantic_digests_by_member, adaptation_digests_by_member) — they were populated but never read. Removed the population logic from workspace/skill_projection.py (artifact_digest / artifact_incarnation parameters were dead inside the function). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Trecek
added a commit
that referenced
this pull request
Aug 17, 2026
- tools_kitchen.py: _declare_join_batch_handler now sums declared
per-role child_spawn_cardinality values rather than reading only
the first one. A plan declaring multiple distinct role counts
(e.g. {role_a: 2, role_b: 1}) is now correctly validated against
the sum.
- tools_kitchen.py: _emit_join_diagnostic no longer maintains a
local subset of DIAGNOSTIC_KEYS. The canonical frozenset in
hooks/_hook_settings.py is the single source of truth for what
fields may appear in a diagnostic record.
- background_exec_guard.py / join_followup_guard.py /
join_stop_guard.py: replace bare open(...).read() with a
`with open(...) as handle` block to close the file descriptor
deterministically.
- join_stop_guard.py: drop dead json.JSONDecodeError/ValueError
handlers around sys.stdin.read() — those exceptions can never
be raised by a plain read().
- join_claim_guard.py: remove the no-op _resolve_top_level_parent
wrapper that accepted and discarded a parameter to return a
hardcoded marker. Inline the constant string at the call site.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Trecek
added a commit
that referenced
this pull request
Aug 17, 2026
- tests/hooks/test_spawn_before_await_subordinate.py: promote the inline `from autoskillit.hooks._join_ledger import active_batch` block to the module-level import and use WAVE_PENDING / WAVE_COMPLETE constants instead of the bare `"pending"` / `"complete"` string literals. - tests/execution/test_launch_force_inactive_default.py: drop the inner `import pytest` from `test_force_inactive_without_project_root_refuses` — pytest is already imported at the module top. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Trecek
added a commit
that referenced
this pull request
Aug 17, 2026
The rebase merged develop's 3 new MCP tools plus our 1 new declare_join_batch tool into the codebase, bringing the total from 72 to 76. Doc claims and the test_docs_state_75_mcp_tools test still asserted 75; update them to 76 to match the post-merge tool_registry.py inventory. Files updated: - docs/README.md (top-level count) - docs/execution/README.md (subdir reference) - docs/execution/architecture.md (overview + order-session description) - docs/execution/tool-access.md (3 occurrences) - tests/docs/test_doc_counts.py (test function name + expected value) Refs PR #4613.
Trecek
enabled auto-merge
August 17, 2026 18:13
Trecek
force-pushed
the
impl-rectify-join-20260815-133231
branch
from
August 17, 2026 21:18
4dac534 to
6301e97
Compare
Trecek
added a commit
that referenced
this pull request
Aug 17, 2026
C1 (tests): Restore script-presence assertion in test_hooks_json_matches_hook_registry_after_generate that was lost during the matcherless-rendering refactor. C2 (tests): Fix or-vs-and typo in test_claude_required_join_emits_keep_batch_first_directive — both 'name=' and 'name@' must be absent. C3 (tests): Rename test_missing_binding_fails_closed_for_join_required to test_missing_binding_defaults_to_permissive and align the docstring with the actual permissive-default assertion. C4/C5/C6 (defense/bugs): build_interactive_cmd gains a project_root parameter, runs _resolve_project_root_for_inactive_check when force_inactive_agent_teams=True (matching the headless/skill builders), and explicitly refuses the force=True + executable combination that previously caused the executable-binding guard to raise after legitimate env neutralization. The Protocol signature is updated in lockstep; Codex's build_interactive_cmd accepts the same parameter (no-op — Codex has no team concept). C7 (bugs): _aggregate_wave_outcome falls through to WAVE_PENDING for mixed terminal outcomes (e.g. success+missing). Introduce WAVE_PARTIAL and add it to _NON_SUCCESS_WAVE_OUTCOMES so mixed-terminal waves no longer silently stall downstream join checks. C8 (bugs): claim_assignment's duplicate tool_use_id guard now fires regardless of whether the prior entry is pending — emitting the same id twice corrupts downstream bookkeeping even if the prior claim never settled. C9 (overengineering): SkillProjectionBinding loses six speculative by_member Mapping fields (join_required_by_member, child_spawn_cardinality_by_member, artifact_digest_by_member, artifact_incarnation_by_member, semantic_digests_by_member, adaptation_digests_by_member) — they were populated but never read. Removed the population logic from workspace/skill_projection.py (artifact_digest / artifact_incarnation parameters were dead inside the function). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Trecek
added a commit
that referenced
this pull request
Aug 17, 2026
- tools_kitchen.py: _declare_join_batch_handler now sums declared
per-role child_spawn_cardinality values rather than reading only
the first one. A plan declaring multiple distinct role counts
(e.g. {role_a: 2, role_b: 1}) is now correctly validated against
the sum.
- tools_kitchen.py: _emit_join_diagnostic no longer maintains a
local subset of DIAGNOSTIC_KEYS. The canonical frozenset in
hooks/_hook_settings.py is the single source of truth for what
fields may appear in a diagnostic record.
- background_exec_guard.py / join_followup_guard.py /
join_stop_guard.py: replace bare open(...).read() with a
`with open(...) as handle` block to close the file descriptor
deterministically.
- join_stop_guard.py: drop dead json.JSONDecodeError/ValueError
handlers around sys.stdin.read() — those exceptions can never
be raised by a plain read().
- join_claim_guard.py: remove the no-op _resolve_top_level_parent
wrapper that accepted and discarded a parameter to return a
hardcoded marker. Inline the constant string at the call site.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Trecek
added a commit
that referenced
this pull request
Aug 17, 2026
- tests/hooks/test_spawn_before_await_subordinate.py: promote the inline `from autoskillit.hooks._join_ledger import active_batch` block to the module-level import and use WAVE_PENDING / WAVE_COMPLETE constants instead of the bare `"pending"` / `"complete"` string literals. - tests/execution/test_launch_force_inactive_default.py: drop the inner `import pytest` from `test_force_inactive_without_project_root_refuses` — pytest is already imported at the module top. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Trecek
added a commit
that referenced
this pull request
Aug 17, 2026
The rebase merged develop's 3 new MCP tools plus our 1 new declare_join_batch tool into the codebase, bringing the total from 72 to 76. Doc claims and the test_docs_state_75_mcp_tools test still asserted 75; update them to 76 to match the post-merge tool_registry.py inventory. Files updated: - docs/README.md (top-level count) - docs/execution/README.md (subdir reference) - docs/execution/architecture.md (overview + order-session description) - docs/execution/tool-access.md (3 occurrences) - tests/docs/test_doc_counts.py (test function name + expected value) Refs PR #4613.
Add fixed_set_join_capable field on BackendCapabilities (default False). Pair it with CLAUDE_CODE_CAPABILITIES=True in the same commit so the capability is statically declared wherever the required hooks are unconditionally registered in HOOK_REGISTRY. Make Codex return unsupported_operation(REQUIRED_JOIN) at admission with an actionable reason instead of rendering the impossible 'wait on exact returned child IDs' contract. Drop the unreachable wait_agent fragment from Codex adapt_skill_semantics. Make Claude refuse join.required=true when the capability is not attested, and narrow the supported join projection to declare-batch plus unnamed foreground calls plus Stop gating. This is the foundation for the join ledger and dispatch guard added in later steps; it makes compile_session_skill_catalog drop join-bearing skills on Codex with a precise reason rather than a prose illusion. Refs #4575, #4520.
…dary Extend the projected skill manifest entry with join_required, normalized child-spawn cardinality, and semantic/adaptation digests so the post-hook sidecar has a structured source for the join-required bit. Convert the session flag from a raw skill-name string to a JSON envelope listing every loaded skill with its own join_required value. The flag's top-level join_required is the OR of every entry — a later join-false load does NOT downgrade an established required-join binding. Existing single-skill string flags are read without loss. Migrate the skill_load_guard auto-exempt writer to set auto_exempt=true on the JSON envelope rather than clobbering it. The companion guard treats any non-empty flag file as evidence that Skill was loaded. Remove the unconditional headless-only early-exit in background_exec_guard.py that previously short-circuited every interactive session. The guard now runs in interactive Claude sessions too — closing the #4575 escape hatch. When the session flag (or the AUTOSKILLIT_JOIN_REQUIRED=1 env-var mirror) reports join_required=true, the guard rejects Agent calls carrying name, team_name, or run_in_background selectors with one deterministic denial. Ordinary unnamed foreground Agent calls remain allowed. Legitimate team calls in clean sessions (no join-bearing skill loaded) continue to work. The ADR-0001 ScheduleWakeup/run_in_background gate still applies in headless tier sessions. Broaden the background_exec_guard HOOK_REGISTRATION from session_scope='headless_only' to 'any' so the new behavior is actually installed for interactive sessions. Refs #4575, #4520.
Register a new free-range MCP tool ``declare_join_batch`` that opens one parent/wave ledger with resolved assignment labels. The tool is in FREE_RANGE_TOOLS (always addressable), classified MUTATION (denied during recipe initialization like the other free-range siblings), and listed in _DISPLAY_CATEGORIES so the Kitchen menu reflects it. The tool is classified MUTATION because every ToolDef needs an explicit initialization-operation bucket — none is exempt. The recipe-init boundary (``admit_registered_tool_during_initialization``) denies it during InitializingRecipe, exactly like ``lock_ingredients``, ``configure_fleet``, ``configure_order``, and ``reload_session``. Add the shared ``hooks/_join_ledger.py`` helper: flock-based cross-process locking (matching ``CaptureLifecycleStore``) plus atomic ``os.replace`` publish, JSON envelope keyed by (session_id, top_level_parent), and the declare/claim/settle operations required by the hooks added in the next commit. The helper is stdlib-only so hook scripts can use it without importing autoskillit.*. Update the documented tool counts from 72 to 73 (one new free-range tool). Refs #4575, #4520.
Add three new stdlib-only hook scripts:
* guards/join_claim_guard.py — PreToolUse, matcher='Agent'. For every
top-level direct Agent tool_use_id in a join-bound session, atomically
claim one declared assignment via the shared join ledger. Nested
descendants (agent_id present) are exempt. Denials cover duplicate
claims, missing declared wave, no unclaimed assignment, and missing
tool_use_id.
* guards/join_settle_guard.py — PostToolUse and PostToolUseFailure.
Maps the upstream event to one of success/failure/timeout/cancelled/
interruption/missing and records the terminal outcome on the claimed
handle. Empty/non-substantive results are mapped to missing and
never converted to success. Identical duplicate events are
idempotent; conflicting terminals fail closed.
* guards/join_stop_guard.py — Stop, matcher=''. Reads the session
binding; no-ops when join_required=false. Otherwise reads the
active wave from the ledger and blocks Stop with exit code 2 when
the wave is missing, pending, or settled non-success. Per Claude
docs, Stop fires once per turn and exit code 2 prevents Claude from
stopping while continuing the conversation.
Register all three in HOOK_REGISTRY with codex_status='not-applicable'
so the Codex-filtered hook output and its tests remain correctly
unaffected (these hooks are Claude-only). Extend HookDef.event_type's
Literal to include PostToolUseFailure and Stop. Add all three
basenames to NEW_SUBDIR_BASENAMES (additions, not retirements).
Refs #4575, #4520.
Add the FastMCP handler for the new free-range ``declare_join_batch``
tool. Reads the session flag as JSON to recover the artifact identity,
opens one declared batch via the shared join ledger, and returns the
new ``join_batch_id`` plus the wave record.
The handler is tagged ``{"autoskillit", "kitchen"}`` and readOnlyHint is
False (it persists state). Like the other free-range kitchen tools, it
flows through ``_cancellation_shield`` and ``track_response_size``.
Refs #4575, #4520.
Add ``agent_backend.force_claude_agent_teams_inactive`` to ``AgentBackendConfig`` (default False, repository-scoped). Document the key in the contracts ledger; the dotted key is new so no retired-config entry is appropriate. Add ``_neutralize_agent_teams_env`` and ``detect_repository_agent_teams_setting`` helpers in claude.py. The first removes ``CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS`` (the documented public toggle per code.claude.com/docs/en/agent-teams — the only such surface) from the constructed env dict. The second reads the target repository's .claude/settings.json and .claude/settings.local.json for any conflicting ``env.<var>`` entry that would otherwise re-enable teams after the launcher process env has been scrubbed. Add a ``force_inactive_agent_teams`` parameter to ``build_headless_cmd``; the interactive/resume/food-truck/skill-session builders share the same env assembly and pick it up at their merge points in follow-on commits. This is the foundation for the repository-scoped launch isolation described in Step 5. A follow-up commit threads the value through the managed and interactive launch corridors and refuses the launch when neither the launcher env nor the target repo's settings files positively confirm an inactive effective state. Refs #4575.
Extend HookDef.event_type's Literal to include PostToolUseFailure and Stop. Add a module-level _MATCHERLESS_EVENT_TYPES frozenset so the ``__post_init__`` matcher validation accepts Stop (which fires once per turn with no tool-name scope, per code.claude.com/docs/en/hooks) and the existing SessionStart exception. Extend _count_hooks_by_event in tests/docs/test_doc_counts.py to bucket PostToolUseFailure and Stop separately so the test does not raise KeyError on the new join_* hook scripts. The doc accuracy check now covers all five event types. Update docs/safety/hooks.md: 36 PreToolUse, 10 PostToolUse, 2 SessionStart, 1 Stop (49 total scripts) — matches HOOK_REGISTRY after the three new join-aware scripts are registered. Refs #4575, #4520.
* Add ``_join_ledger`` to SINGLETON_ALLOWED_MODULES so the constant
resolution test allows the module-level _BATCH_ID_ALPHABET and
LEDGER_FILENAME assignments in the new join ledger.
* Bump the pinned ``hooks/`` file count from 23 to 24 (one for
``_join_ledger.py``).
* Bump the pinned ``hooks/guards/`` file count from 35 to 38 (one
each for ``join_claim_guard.py``, ``join_settle_guard.py``,
``join_stop_guard.py``).
Refs #4575.
…g, follow-up guard, fail-closed validation, Codex Protocol symmetry * skill_load_post_hook.py now reads the projection manifest sidecar (resolved via AUTOSKILLIT_PROJECTION_MANIFEST_PATH or sibling .<dir>.autoskillit-projection.json discovery) and populates the JSON envelope with the loaded skill's actual join_required, semantic and adaptation digests, projected/canonical digests, and child-spawn cardinality. A missing or unreadable manifest fails closed: the envelope is written with binding_valid=false and join_required=true so dispatch guards refuse all join-bearing work. * SkillProjectionBinding extended with join_required_by_member, child_spawn_cardinality_by_member, artifact_digest_by_member, artifact_incarnation_by_member. build_skill_projection_binding populates them from each member's parsed semantic_plan. * materialization.py manifest entry now exposes artifact_digest and artifact_incarnation; validator accepts any non-empty string for them since the values come from the publish-time binding. * declare_join_batch handler fails closed unless the binding reports join_required=true, the skill is in loaded_skills, the active backend attests fixed_set_join_capable, and assignment count matches the manifest's child_spawn_cardinality. * Codex Protocol signatures now accept force_inactive_agent_teams as a no-op across build_headless_cmd, build_skill_session_cmd, build_food_truck_cmd, build_interactive_cmd, build_resume_cmd to mirror the Claude launch builders. * background_exec_guard rejects ScheduleWakeup in join-bound sessions before the headless gate so interactive sessions also block the deferral escape hatch. * New matcherless PreToolUse join_followup_guard denies non-Agent side-effecting tools while a wave is unresolved (exit 2). * docs/execution/tool-access.md adds lock_ingredients and declare_join_batch rows to the FREE RANGE map; tool count says 73. * docs/execution/architecture.md adds the "Join Contract and Batch Admission" section describing declare_join_batch, JoinLedger, the claim/settle/Stop lifecycle, backend admission, session binding monotonicity, and the repository force-inactive option. * docs/safety/hooks.md counts updated to 37 PreToolUse hooks and 50 total scripts. * tests/docs/test_doc_counts.py renamed test_docs_state_72_mcp_tools to test_docs_state_73_mcp_tools with expected=73. * tests/arch/test_subpackage_isolation.py bumps hooks/guards count from 38 to 39 (one new followup guard). Refs #4575, #4520.
* ``hooks/_join_ledger.py`` exposes ``DIAGNOSTIC_KEYS`` and
``write_diagnostic(record, caller)`` for bounded JSONL emission to
``<project>/.autoskillit/logs/join_diagnostics.jsonl``. The record
is filtered to a fixed allow-list so child bodies, prompts, secrets,
and private task IDs never land in the sink. Stdlib-only — usable
from hook scripts without importing ``autoskillit.*``.
* ``hooks/_hook_settings.py`` adds ``write_join_diagnostic`` mirroring
the helper for non-hook call sites.
* ``join_claim_guard`` emits ``status=deny`` / ``status=deny_no_open_wave`` /
``status=claim`` records with ``join_batch_id`` and ``assignment`` on
success.
* ``join_settle_guard`` emits ``status=<outcome>`` with the latest
``wave_outcome``.
* ``join_stop_guard`` emits ``status=allow|block`` with
``binding_valid`` and the current ``wave_outcome`` reason.
* ``join_followup_guard`` emits ``status=block`` with the offending
tool name and current ``wave_outcome``.
* ``_declare_join_batch_handler`` emits ``status=declared`` and
``status=declare_refused`` records.
* New parsed-behavior inventory test
``tests/skills/test_semantic_join_inventory.py`` walks every bundled
SKILL.md, parses the YAML frontmatter, and asserts:
- any skill with a non-empty ``child_spawns`` block must declare
``semantic_requirements.join.required: true``;
- any skill whose body contains structural child-dispatch markers
(``Agent(``, ``Task(``, ``spawn_agent``, ``Dispatch``, etc.) must
declare ``join.required: true``;
- zero prose-only join consumers remain after the Step 6 migration.
Refs #4575, #4520.
…ization, capability/hook pairing, #4575 negative control, parallel-process ledger tests, diagnostic reconstruction
…-bound dispatch composition, parity tests, denied-PreToolUse no-result-record, interactive-corridor force=True, OR-accumulated downgrade test, build_interactive_cmd neutralization bugfix REQ-052, REQ-053, REQ-103: assert the full atomic JSON envelope on a join-bearing skill load (skill_name, join_required, semantic/adaptation/projected/artifact digests, artifact_incarnation, child_spawn_cardinality, binding_valid), extend existing subagent/backend-authority/codex-bypass tests with join-bearing projections, and assert that a later join-false load does not downgrade an established required-join binding. REQ-054: extend tests/infra/test_background_exec_guard.py with eleven join-bound composition cases (required join + name/team_name denial, run_in_background=true denial, unnamed foreground allowance, clean-session preservation, malformed/missing binding fail-closed, ScheduleWakeup rejection, activation source/state reporting). REQ-059: register declare_join_batch in test_type_constants.py (FREE_RANGE_TOOLS), test_server_tool_registration.py (test_all_tools_exist), and test_tool_registry_parity.py (MUTATION operation). test_layer_enforcement.py::test_display_categories_sync derives from FREE_RANGE_TOOLS dynamically and is already in sync. REQ-076: extend test_launch_force_inactive_call_path.py parametrize set to include build_interactive_cmd with force_inactive_agent_teams=True. REQ-090: add test_denied_pre_tool_use_creates_no_result_record asserting that a denied Agent PreToolUse produces no ledger entry, no claim, no settlement, and that can_release_stop blocks Stop until the wave is declared and resolved. REQ-081/REQ-082: verified _explorer_dispatch.py declares resolved vector assignment labels (resolved_assignment_labels in preamble, per-vector Resolved vector assignment label lines) and that CODEX_EXPLORATION_DISPATCH_RENDERER.fail_unsupported_join refuses frontend join prose when the backend does not attest fixed_set_join_capable. bugfix: build_interactive_cmd previously neutralized CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS on a throwaway dict copy of a MappingProxyType, leaving effective_env unchanged and causing assert_agent_teams_inactive to raise. Re-derive neutralized_env as a single mutable copy and bind effective_env to it after the assertion.
…llowlist, AST violations, atomic_write - Update test_backend_capabilities.py field-count and field-name-locked tests to include fixed_set_join_capable (REQ-JOIN backend authority). - Update test_backend_dataclasses.py field-exhaustive test to include force_inactive_agent_teams. - Add hooks/_join_ledger.py to FCNTL_ALLOWED_MODULES — stdlib-only hook script needs direct fcntl.flock for cross-process atomic ledger writes. - Replace print() + bare Exception in hooks/_join_ledger.py with stdlib logging + OSError/ValueError catches (ARCH-001/ARCH-003 compliance). - Replace bare Exception in tools_kitchen.py declare_join_batch paths with typed exceptions and logger.warning calls. - Replace path.write_text in neutralize_repository_agent_teams_settings with atomic_write from autoskillit.core.io. - Add fixed_set_join_capable to test_coding_agent_backend_conformance.py::CAPABILITY_CLASSIFICATION as OPTIONAL and to NOT_YET_LIVE — the field is declared but the test infrastructure is being wired up. These are inherited regressions from earlier commits that the previous audit's Slice C missed; the current audit (Slice C) flagged only documentation parity. This commit closes the underlying field-count, AST-rule, and atomic-write regressions.
The worktree commit a391aaf over-counted by 1 in both dimensions (claimed 76/142, actual post-rebase source has 75/141). Update docs and test fixture to match the actual MCP tool registry and bundled skill inventory.
…ations, add missing reinforcement prose, fix Codex admission-refusal test fallout
- Revert _fmt_status.py relative import back to bare sibling import (matches
every other formatter module); a prior commit broke the standalone hook
dispatch path while 'fixing' an unrelated test.
- Update test_skill_load_guard.py and test_schema_version_convention.py to
match the new JSON auto-exempt schema and current tools_kitchen.py line
numbers.
- Revert spurious child_spawns/join.required frontmatter added to
make-campaign, make-experiment-diag, report-bug, write-recipe — none of
these skills spawn children (report-bug explicitly prohibits it); the
additions only existed to silence a substring-matching false positive in
test_semantic_join_inventory.py. Tighten that heuristic's markers
(drop bare 'delegate'/'child delegations' prose matches and require
'Dispatch(' call syntax) to match its own stated 'parsed behavior, not
prose markers' design intent.
- Add the required three-layer parallel-dispatch reinforcement prose
(NEVER/ALWAYS) to the 11 vis-lens-* skills that genuinely declare
concurrent child dispatch, matching the established arch-lens-*/
vis-lens-chart-select pattern.
- Update tests that now correctly observe Codex's honest REQUIRED_JOIN
refusal (SkillContractError) for join-bearing skills: either assert the
refusal directly, or swap an incidental test-fixture skill name to a
non-join skill when the test's actual purpose is unrelated to join
semantics (session materialization, home cleanup, lease handling,
discovery collision, override precedence).
- Fix test_launch_force_inactive_default.py's PATH-dependent test to use a
hermetic tmp_path executable, matching sibling tests in the same family.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…n boundary Codex's adapt_skill_semantics() fails closed (raises SkillContractError) directly from inside the method for join.required=true plans, rather than returning a gradable unsupported_operation result. Three production call sites were built against the old gradable-result contract and crashed instead of gracefully excluding the skill: - ProjectedPluginArtifactAuthority._plan() (authority.py): computed a per-skill adaptation-identity digest for the DIRECT_INSTALL bundled catalog. Now catches the refusal, excludes the skill from the catalog (skills/namespace_sources/digests), and re-derives skill/adaptation identity from the filtered set. - compile_session_skill_catalog() (session_skills.py): the primary SESSION-role catalog compiler; already had unsupported_operation-based graceful exclusion downstream of adapt_skill_semantics(), but that path was unreachable since the raise happens before any result is returned. Now wraps the call and routes the refusal into the same SkillUnavailableMetadata/unavailable-list contract. - build_skill_projection_binding() (skill_projection.py): materializes the actual per-skill projected documents for a binding. This is reached by the ORCHESTRATOR-role catalog used by kitchen/food-truck dispatch, which is resolved before a backend is selected and therefore never passes through compile_session_skill_catalog()'s exclusion. Now excludes any skill whose document projection raises, consistent with the other two call sites. Traced end-to-end via tests/fleet/test_dispatch_backend_override.py and tests/fleet/test_fleet_e2e_codex.py, which silently failed with 'fleet_l3_no_result_block' (the crash was logged via logger.error but never surfaced to the dispatch outcome) before this fix. Also fixes test_codex_cook_projects_only_spawnable_compose_pr_roles, which called adapt_skill_semantics() directly on the join-bearing compose-pr skill to derive an expected role-mapping set; compose-pr's role names are unaffected by Codex's mapping rule, so the expected set is now derived directly from the semantic plan instead of the refusing adapter call. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- declare_join_batch's MCP tool registration set readOnlyHint=False, violating the codebase-wide contract (test_all_tools_match_readonly_hint_contract) that every tool except open_kitchen declares readOnlyHint=True regardless of internal mutation — the same convention already used by every other free-range/mutating tool (lock_ingredients, configure_fleet, git/clone operations, etc.). Corrected to True. - test_force_inactive_false_keeps_env_var[build_skill_session_cmd/ build_food_truck_cmd] relied on the ambient host CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS env var being present to test that force=False leaves it untouched — it never explicitly injected the var, unlike every other builder helper in the same file (_headless_stripped, _resume_stripped, _interactive_stripped), so it passed by environmental accident locally and failed in CI's clean environment. Made hermetic by explicitly injecting the var via provider_extras/env_extras, matching the sibling helpers' pattern. Verified with the ambient var explicitly unset. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…lowlist The previous commit flipped declare_join_batch's readOnlyHint from False to True to satisfy tests/server/test_tool_annotation_completeness.py, but that was wrong: scripts/check_tool_annotations.py (REQ-ARCH-ANNOTATION-E1) and its own arch-level test (tests/arch/test_tool_annotation_completeness.py) explicitly and intentionally treat declare_join_batch as a second effectful exception alongside open_kitchen — it opens a join batch ledger entry, so readOnlyHint=False is correct and was already passing that contract. Reverted tools_kitchen.py back to readOnlyHint=False. The actual bug was tests/server/test_tool_annotation_completeness.py's allowlist (), which was never updated when declare_join_batch was introduced as the second exception. Fixed the allowlist to match REQ-ARCH-ANNOTATION-E1's two-tool exception set. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Trecek
force-pushed
the
impl-rectify-join-20260815-133231
branch
from
August 17, 2026 23:38
033f19f to
9b05631
Compare
This was referenced Aug 18, 2026
github-merge-queue Bot
pushed a commit
that referenced
this pull request
Aug 18, 2026
…cs, wired config authority, and an unstubbed corridor startup net (#4688) ## Summary Commit `55b697427` (PR #4613) grafted an unconditional Claude agent-teams environment policy onto the interactive pre-spawn checkpoint. The opt-in authority `agent_backend.force_claude_agent_teams_inactive` was dead config — defined, never read anywhere in `src/` — while the checkpoint enforced the policy on every corridor (`cook`, `order`, fleet) for every backend, crashing startup on any machine whose gitignored `.claude/settings.local.json` or shell env legitimately enables `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS`. The architectural weakness is threefold, and each half of it enabled the other: 1. **Intent-blind checkpoints.** `CmdSpec` carried no record of what policy was requested at build time. A post-hoc checker structurally *cannot* know whether neutralization was asked for — so it degrades to "always enforce." The shipped docstring described behavior that could not exist. 2. **Authority declared but unconsumed, with no contract catching it.** The config ledger proves a key *parses*; nothing proved a key is *read*. `force_claude_agent_teams_inactive` passed every contract while being dead from birth — and it was not alone. 3. **No behavioral net over the interactive corridor.** 13 call sites across 7 test files stub `validate_interactive_invocation` to `return []`; the two real-gate tests run in sterile tmp dirs with no `.claude/` settings; smoke lanes are excluded from PR CI. This is the fifth fail-closed-checkpoint corridor breakage in two weeks. The immunity: (A) policy intent becomes **data on the spec**, stamped by the builder — checkpoints read intent, never infer it; (B) the config authority is wired end-to-end and a **config-consumption contract test** makes "declared but unread" a CI failure for every config field forever; (C) an **unstubbed corridor startup net** — realistic-fixture launches with a fake agent binary, running per-PR — makes any future checkpoint that fires on legitimate state an instant test failure, regardless of its mechanism. ## Requirements Policy intent becomes data on the spec, stamped by the builder, so checkpoints read intent rather than infer it. The config authority is wired end-to-end and a config-consumption contract makes "declared but unread" a CI failure for every config field. An unstubbed corridor startup net running per-PR makes any future checkpoint that fires on legitimate state an instant test failure, regardless of its mechanism. Default-config launches must remain byte-for-byte unaffected, and the opt-in must remain fail-closed when neutralization is genuinely impossible (#4575's original protection). `#4572` (attestation env drift tripping the executable-binding guard) is a separate defect on the same functions and is explicitly out of scope. ## Verification Measured on a machine whose `.claude/settings.local.json` legitimately enables agent teams — the configuration that triggered the defect: | | `develop` (`5e3a29a86`) | this branch | |---|---|---| | `task test-check` | **81 failed**, 39595 passed | **16 failed**, 39738 passed | The 16 remaining failures fail identically at the base commit and are set-wise a strict subset of the baseline: **zero regressions introduced, 65 baseline failures fixed.** The residue is the `#4572` attestation-drift family, explicitly out of scope. `pre-commit run --all-files` is clean (ruff, mypy, gitleaks, stub and contract checks). ## Two deliberate departures from the plan **The opt-in was unreachable, and the plan preserved the ordering that made it so.** `build_interactive_cmd` asserted inactivity *before* stripping settings, so any repository whose settings enabled teams raised instead of being neutralized — leaving `neutralize_repository_agent_teams_settings` dead code for the exact population it exists to serve. Neutralization now precedes confirmation. A malformed file is still refused rather than rewritten, so the confirmation continues to fail closed there, preserving #4575's guarantee where it matters. **The prescribed config-consumption exclusion rule would have flagged a live field.** Excluding `_config_dataclasses.py` wholesale reports `GitHubConfig.allowed_labels` as dead — it is read solely by `check_label_allowed()`, an accessor colocated with the definition and called from six production sites. The rule implemented is narrower: skip `__post_init__` bodies only. Self-validation is not consumption; an accessor beside the definition is. ## Not delivered The cook black-box launch test. `cook` hangs before its confirmation prompt in a fully hermetic environment for reasons unrelated to this change, and a timing-out test is worse than no test. The cook corridor remains covered by `test_force_inactive_config_corridors.py` (config read plus both build branches) and the existing cold-launch mediums; the corridor realism gap is closed on the `order` path, which now launches against a teams-enabled checkout and passes, including the exported-env variant. ## Follow-ups filed Two config fields are forward-declared rather than deleted to make the new contract green: #4685 (`ProviderProfileDef.context_window`) and #4686 (`RunSkillConfig.natural_exit_grace_seconds`). Each carries a 180-day staleness time-bomb. Closes #4687 ## Implementation Plan Plan file: `.autoskillit/temp/rectify/rectify_interactive_policy_checkpoint_immunity_2026-08-17_174353.md` 🤖 Generated with [Claude Code](https://claude.com/claude-code) via AutoSkillit <!-- autoskillit:pipeline-signature steps=prepare_pr,run_arch_lenses,compose_pr,annotate_pr_diff,review_pr --> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Trecek
added a commit
that referenced
this pull request
Aug 19, 2026
…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>
Trecek
added a commit
that referenced
this pull request
Aug 19, 2026
…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>
Trecek
added a commit
that referenced
this pull request
Aug 19, 2026
…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>
Trecek
added a commit
that referenced
this pull request
Aug 19, 2026
…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>
Trecek
added a commit
that referenced
this pull request
Aug 19, 2026
…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>
Trecek
added a commit
that referenced
this pull request
Aug 19, 2026
…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>
Trecek
added a commit
that referenced
this pull request
Aug 20, 2026
…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>
Trecek
added a commit
that referenced
this pull request
Aug 20, 2026
…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>
Trecek
added a commit
that referenced
this pull request
Aug 20, 2026
…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>
This was referenced Aug 20, 2026
Closed
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.
Rectify: Fixed-set join contract and Claude team-dispatch isolation
This PR addresses both related issues together — #4575 — Claude/MiniMax teammate result delivery supplies the Claude dispatch guard, and #4520 — fixed-set blocking join supplies the end-to-end join contract. Neither half is complete alone: without the join contract the dispatch guard would have nothing to gate; without the dispatch guard the join contract would still admit the prose-only / team-mode escape path that #4575 records losing results.
Closes #4575
Closes #4520
Summary
The change keeps
JoinSpec.requiredas the sole semantic authority and closes the production gaps through existing deep paths:unsupported_operation(REQUIRED_JOIN)instead of rendering the impossible "wait on exact returned child IDs" contract. Claude refusesjoin.required=truewhen the static capability is absent and narrows its supported projection to declare-batch + unnamed foreground + Stop gating.join_required, semantic/adaptation/artifact/projected digests,artifact_digest+artifact_incarnation, andchild_spawn_cardinality. A later join-false load does NOT downgrade an established required-join binding (the OR-accumulated monotonic bit is asserted bytest_subsequent_join_false_load_does_not_downgrade_join_required).background_exec_guard.py) has its interactive-only early-exit removed and now rejectsname,team_name, andrun_in_backgroundselectors whenjoin_required=true. Legitimate team calls in clean sessions continue to work (asserted bytest_clean_session_allows_named_teammate_dispatch,test_clean_session_allows_named_teammate_dispatch).hooks/_join_ledger.py) keys membership and results by session, top-level parent, batch id, assignment, and directtool_use_id. Four new hook scripts (join_claim_guard,join_settle_guard,join_stop_guard,join_followup_guard) provide PreToolUse claim, PostToolUse/PostToolUseFailure settlement, matcherless Stop gating, and unresolved-wave follow-up blocking with exit-code 2.declare_join_batchopens one wave with resolved assignment labels, validates the loaded skill / artifact / session binding, and returns a freshjoin_batch_id.agent_backend.force_claude_agent_teams_inactive, default False) plus helpers that neutralize the documented public toggle (CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS) and detect conflicting entries in the target repo's.claude/settings.json/.claude/settings.local.json. Repositories with the option disabled remain byte-for-byte unchanged.skills_extended/now declaresemantic_requirements.join.required: trueand the matchingchild_spawns/concurrency/evidenceblocks.audit-docsgained the fullsemantic_version: 1+ block.force_inactive_agent_teamsthrough every launch corridor — interactive (build_interactive_cmd), resume, skill-session, and food-truck builders all honor the option via aforce_inactive_agent_teams: bool = Falseparameter;_apply_configpropagates the value fromSkillSessionConfig.ExplorationDispatchConventionsgainedassignments_argumentandfail_unsupported_join;ExplorationRouterPlancarriesjoin_required; the renderer declares resolved vector assignments and refuses frontend join prose on unsupported Codex.Test Coverage
This PR adds new tests addressing the audit's MISSING findings:
tests/contracts/test_capability_hook_pairing.pytests/execution/backends/test_join_conformance_traces.pytests/execution/test_launch_force_inactive_default.pytests/execution/test_launch_force_inactive_call_path.pybuild_interactive_cmdwithforce_inactive_agent_teams=True(REQ-EXTRACT-052, REQ-076)tests/execution/test_settings_file_neutralization.pytests/hooks/test_join_composition.pytests/hooks/test_join_declared_batch_cases.pytests/hooks/test_join_parallel_process.pytests/hooks/test_4575_negative_control.pytests/hooks/test_join_diagnostics.pytests/hooks/test_spawn_before_await_subordinate.pytests/hooks/test_skill_load_post_hook_json.pytests/hooks/test_skill_load_post_hook.pytest_join_bearing_skill_load_writes_complete_json_envelope,test_join_false_skill_load_keeps_join_required_false,test_subsequent_join_false_load_does_not_downgrade_join_required,test_fresh_session_without_join_loads_reports_join_required_false,test_skips_flag_write_when_agent_id_present_join_bearing_skill,test_skill_load_post_hook_backend_authority_join_bearing,test_codex_bypass_join_bearing_skill_with_nonempty_profile_writes_no_flag(REQ-052, REQ-053, REQ-103)tests/infra/test_background_exec_guard.pytests/server/test_server_tool_registration.pytest_all_tools_existto includedeclare_join_batch(REQ-059)tests/server/test_tool_registry_parity.pytest_every_tool_has_an_explicit_initialization_operationto includedeclare_join_batchin MUTATION (REQ-059)tests/core/test_type_constants.pytest_free_range_tools_contains_expected_namesto includedeclare_join_batch(REQ-059)tests/arch/test_layer_enforcement.py::test_display_categories_syncFREE_RANGE_TOOLSdynamically — already in syncCross-references
Adjacent ownership only — not in scope here:
Verification
The change is complete when:
join.required=true, undeclared/active-team/named Claude dispatch is denied before child creation; a declared unnamed foreground retry succeeds.semantic_requirements.join.required: true.Tool count: 73 (was 72). Hook count: 51 (was 46 — 37 PreToolUse, 11 PostToolUse, 2 SessionStart, 1 Stop).
Implementation Plan
Plan file:
/home/talon/projects/generic_automation_mcp/.autoskillit/temp/rectify/rectify_join_contract_and_claude_team_dispatch_2026-08-15_110450.md🤖 Generated with Claude Code via AutoSkillit