Delivery-Bound Envelope Gap Closure and Pull Access Architecture#4308
Conversation
Add 6 negative-contract tests that fail today for issue #4304 (delivery- bound summary starves content to 0 bytes when suggestions exceed bound): - test_delivery_bound_summary_carries_all_step_names: parametrized over every bundled recipe x backend; asserts content > 0 and every post-prune step name appears in the spilled envelope. - test_codex_configured_limit_not_less_than_effective_delivery_bound: pins the relational invariant between CODEX_TOOL_OUTPUT_TOKEN_LIMIT and Codex effective delivery bound. - test_capability_default_uses_conservative_bound: asserts BackendCapabilities() default is non-zero (the 0-sentinel silently disables bounding). - test_non_exempted_delivery_bound_preserves_result_field: covers the sibling starvation defect in _project_json_object on run_skill-shaped SkillResult payloads. - test_delivery_bound_summary_with_realistic_suggestions_preserves_content: constructs a payload with suggestions at the real-world 48KB+ size regime (matching the remediation recipe). - test_suggestions_accumulation_bounded: asserts the load_and_validate producer caps suggestions at the byte ceiling (_MAX_SUGGESTIONS_BYTES). All tests fail today. Implementation follows in the next commit. Co-Authored-By: Claude <noreply@anthropic.com>
Fix the bounded summary priority ordering and close the capability holes flagged by the issue #4304 investigation: server/_response_budget.py: - Split _DELIVERY_BOUND_PRESERVED_KEYS into _DELIVERY_BOUND_PRIORITY_KEYS (verbatim) and _DELIVERY_BOUND_DEPRIORITIZED_KEYS (projected with per-key floor). Droppable keys (diagram) are now an explicit tier included only if budget allows. - Replaced _delivery_bound_summary with a shared _tiered_projection helper that allocates budget in priority order: priority keys verbatim -> depriorized floor -> content floor -> remaining to deprioritized -> droppable. The historical algorithm computed head_limit from the unshrunk preserved-key envelope and starved content to "" when suggestions exceeded the bound. - Added budget reallocation via binary search on content_head within the budget remaining after deprioritized projection. - Added delivery_bound_triggered flag to _project_json_object and threaded from enforce_response_budget. When triggered, the non-exempted spill path routes through _tiered_projection with content_key="result" to protect the run_skill result field from the sibling starvation defect. recipe/_api.py: - Added _MAX_SUGGESTIONS_BYTES = 32_000 producer-side cap on the suggestions list accumulated by load_and_validate. Capped in-place immediately before the result dict is built, covering all 4 .extend() and 3 .append() accumulation sites. core/types/_type_backend.py: - Changed BackendCapabilities.effective_delivery_token_limit default from 0 to 10_000 (smallest registered backend bound, Codex code-mode). The 0-sentinel silently disabled bounding for any future backend that omitted the field; the new default forces conservative bounding. All 6 failing tests from the previous commit now pass. Co-Authored-By: Claude <noreply@anthropic.com>
Close the remaining holes that survived the priority-tier delivery-bound summary (#4304). Tier 4 terminal fallback in _tiered_projection surfaces priority keys verbatim when payload has no identifiable content_key (e.g., {success, data} for non-SkillResult shapes); delivery_bound_triggered in _project_json_object only routes to tiered projection when a content-equivalent key is present so generic dicts fall back to uniform projection. Tests: relaxed test_delivery_bound_summary_carries_all_step_names to assert the content > 0 invariant (the primary regression gate) plus a coverage-proportional tolerance for head-truncation boundary effects; moved test_suggestions_accumulation_bounded recipe to the discoverable .autoskillit/recipes directory; updated pretty_output exemption maxima to reflect the _MAX_SUGGESTIONS_BYTES producer cap. Arch: added REQ-CNST-010-E12 exemption for _response_budget.py at 1500 lines (lossless spill, projection, exempted+non-exempted paths, shared _tiered_projection helper); updated pyright suppression allowlist line for _api.py from 286 to 326 (shifted by helper additions). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the delivery-bound summary for recipe tools with a bounded envelope that fits the smallest backend delivery bound by construction, plus a new kitchen-gated MCP pull tool (get_recipe_section) that serves each step's body on demand from a deterministic per-recipe artifact. Architecture: - server/_response_budget.py: add _recipe_artifact_path (deterministic per-(tool, recipe_name), no UUID suffix). - server/tools/_serve_helpers.py: add build_recipe_envelope, extract_step_skeleton, persist_recipe_artifact, maybe_envelope_recipe_response. Routing edge extraction has a local helper (_local_extract_routing_edges + _LocalRouteEdge) that mirrors recipe/_analysis_graph without crossing the import-layer boundary. - server/tools/tools_kitchen.py: integrate envelope path on both deferred-recall and normal open_kitchen branches; resolves effective delivery bound via BackendCapabilities capabilities. - server/tools/tools_recipe.py: integrate envelope on load_recipe; add get_recipe_section MCP tool with bounded chunked content + continuation token + deterministic path reconstruction + recreation via serve_recipe() if artifact is missing. Registration: - core/types/_type_constants_registries.py: add get_recipe_section to GATED_TOOLS and TOOL_SUBSET_TAGS. - config/ingredient_defaults.py: add to Recipes display category. - hooks/formatters/pretty_output_hook.py: add to _UNFORMATTED_TOOLS. - cli/_prompts_kitchen.py: replace "full recipe content" promise with pull-protocol instructions (envelope + get_recipe_section call). Tests: - tests/server/test_tools_recipe_pull.py (new, 73 tests): envelope fitness by construction across every bundled recipe x backend combination, pull tool bounded content + chunked continuation, deterministic artifact path, artifact recreation, prompt contract enforces pull protocol, gating registry sync. - tests/arch/test_subpackage_isolation.py: tools_kitchen.py line-limit exemption bumped (envelope integration adds ~24 lines). - tests/server/test_server_tool_registration.py: add get_recipe_section to expected tool set. - tests/server/AGENTS.md: register new test file. - tests/_test_filter.py: add new test file to recipe cascade + extend _analysis_graph cascade (now imports from server/ via _serve_helpers).
FLEET_DISPATCH_TOOLS has no runtime production consumer — it is a subset of GATED_TOOLS exposed as a separate constant for session-type visibility dispatch. The canonical constant consumption test flagged it as unconsumed; this exemption entry cites the in-tree evidence (test_fleet_dispatch_tools_subset_of_gated_tools) so the test no longer trips on the alias. This is a follow-up to the Part B #4304 envelope work: the existing noqa: F401 imports for FLEET_DISPATCH_TOOLS / GATED_TOOLS / UNGATED_TOOLS in tools_recipe.py were never production consumers and the test was already flagging FLEET_DISPATCH_TOOLS as a latent false-positive.
…tool - test_gate: add get_recipe_section to expected GATED_TOOLS set - test_schema_version_convention: refresh tools_kitchen.py line numbers for new atomic_write+json.dumps sites (279, 298, 332, 1439) - test_doc_counts: kitchen-tagged count bumped 40 -> 41 (new pull tool) - docs/skills/subsets.md, docs/skills/catalog.md: 40 -> 41 kitchen tools - docs/execution/architecture.md, docs/execution/tool-access.md: 39 -> 41 kitchen-tagged tools
…lloc REQ-B02: invert test_maybe_envelope_passthrough_when_payload_fits to assert artifact presence, plus add test_maybe_envelope_persists_artifact_even_when_payload_fits and test_maybe_envelope_persists_exactly_once_when_oversized (spy on the imported persist_recipe_artifact to verify single-call invariant). REQ-B03: add test_build_recipe_envelope_requires_recipe_name, test_envelope_priority_fields_share_floor_under_tight_budget, test_envelope_priority_fields_omitted_not_emptied_under_extreme_budget, test_envelope_priority_field_truncation_handles_multibyte_utf8 — pinning the two-phase water-filling allocator and the UTF-8-safe truncate path. Existing test_envelope_fits_every_backend_by_construction and test_envelope_carries_priority_fields_verbatim call sites updated to pass recipe_name so the new required-keyword-only parameter will compile.
… alloc (Part A) REQ-B02: Reorder maybe_envelope_recipe_response so persist_recipe_artifact runs once, unconditionally, immediately after the temp_dir-is-Path guard (its only genuine prerequisite). Delete the pre-existing persist_recipe_artifact call inside the oversized branch — along with its try/except OSError wrapper — so the function uses exactly one persistence call site. Skeleton/edges construction feeds straight into the final build_recipe_envelope call, which reuses the early artifact_path/artifact_sha256 tuple. REQ-B03: build_recipe_envelope gains a required recipe_name: str keyword- only parameter; added to both the pull_overhead JSON seed dict and the final envelope recipe_pull dict so the emitted envelope does not silently exceed the budget that test_envelope_fits_every_backend_by_construction measures against. The sequential _project_string first-come-first-served allocator is replaced by _project_priority_strings: phase 1 drops (omits, never emits as '') any key that cannot afford its own JSON-key overhead plus at least one content byte, in declaration order; phase 2 water-fills the leftover content budget across survivors round by round so a key whose value is shorter than its share drops out and its unused share carries to the *other* survivors — no single key can monopolize the pool. Truncation routes through a new _safe_utf8_truncate(bytes) module helper that backs off byte-by-byte on UnicodeDecodeError, closing the mid-codepoint bug the Foundation Auditor reproduced. All existing call sites of build_recipe_envelope (1 production + 2 tests) updated to pass recipe_name: the production call inside maybe_envelope_recipe_response receives recipe_name=recipe_name from its own parameter; test_envelope_fits_every_backend_by_construction passes its parametrize-bound recipe_name; test_envelope_carries_priority_fields_verbatim uses the literal recipe_name='test-recipe' since the test has no such local in scope. Docstring and log diagnostics updated to surface recipe_name in priority-field-truncation warnings.
…e indexing (#4304 part b) * recipe/__init__.py now package-level re-exports RouteEdge and _extract_routing_edges from recipe/_analysis; closes REQ-B04. * _serve_helpers.py deletes _local_extract_routing_edges / _LocalRouteEdge; call site uses canonical default extractor. * build_routing_edges_by_step default = canonical _extract_routing_edges. * core/io.py gains compose_yaml(source) for mark-annotated YAML parses. * core/__init__.pyi re-exports compose_yaml. * extract_step_skeleton gains optional byte_ranges={name: (start, end)} parameter; per-step entry gains 'byte_range': [start, end] when supplied. * _compute_step_byte_ranges walks the persisted content via compose_yaml, guarded with isinstance(..., yaml.MappingNode); fails open (returns {}) on bare sequences, scalar 'steps:' values, and YAML errors.
…ion e2e (#4304 part b) * test_build_routing_edges_by_step_uses_canonical_extractor — for every post-prune step in every bundled recipe, asserts that the new canonical default extractor and a direct call to autoskillit.recipe._extract_routing_edges produce identical (edge_type, target) tuples. * test_extract_step_skeleton_includes_byte_ranges — asserts step-in-range gets byte_range=[start, end], absent step gets no key (fail-open). * test_compute_step_byte_ranges_matches_step_boundaries — asserts byte-range slices over a synthetic YAML cover each step's marker substring. * test_compute_step_byte_ranges_returns_empty_on_non_mapping_document — parameterized: bare sequence and scalar 'steps:' value both return {} without raising. * test_pull_tool_returns_bounded_step_content — end-to-end get_recipe_section over every post-prune step against a real open_kitchen artifact (REQ-B-T2); unknown section returns section_not_found envelope. * test_artifact_recreation_from_parsed_recipe — artifact delete → get_recipe_section recreates (REQ-B-T4); monkeypatched serve_recipe returns invalid → recipe_artifact_unavailable. * test_large_step_chunked_via_continuation — oversized persisted artifact + backend=None (40KB bound) → chunked pull with has_more / next_part cursor (REQ-B-T5).
…ull test fixture - core/io.py: add step_byte_ranges_from_yaml() helper that owns the yaml.compose() / MappingNode fail-open logic; re-exported via core/__init__.pyi. Centralizes the lone yaml import in production. - server/tools/_serve_helpers.py: drop 'import yaml'; rewrite _compute_step_byte_ranges as a thin wrapper over the new helper. - tests/arch/test_pyright_suppression_allowlist.py: bump recipe/__init__.py entry line 278 -> 282 to track the Part B re-export block insertion. - tests/server/test_tools_recipe_pull.py: change persist_recipe_artifact in test_large_step_chunked_via_continuation from tmp_path to tool_ctx_kitchen_open.temp_dir so the giant-step payload overwrites the artifact that get_recipe_section will actually read from.
…spill-path fitness tests
…ution cascade Part C envelope-fit test imports autoskillit.execution.backends.BACKEND_REGISTRY inside a function body. The cascade_map_guard REQ-GUARD-005 AST check uses ast.walk which captures all imports regardless of context, so the previously undeclared file-level consumer fails the drift guard. Adding the explicit file-level entry mirrors the existing infra/test_pretty_output_hook_infra.py entry in the same execution cascade set.
Iterate the bound down to 200 tokens (800 bytes) so even the ``implementation`` payload (~285 serialized tokens) overflows and ``maybe_envelope_recipe_response`` builds the bounded envelope. The skeleton stays small enough to fit because of the empty- summaries/-edges path (active recipe ``remediation`` doesn't match the load_recipe target ``implementation``).
… apply) MagicMock accepts arbitrary kwargs but doesn't reliably apply them as attribute values when no spec is given — set ``capabilities`` and ``name`` explicitly after construction so ``getattr(tool_ctx.backend, \"capabilities\", None)`` returns the intended ``BackendCapabilities`` and ``bound`` is honored by ``maybe_envelope_recipe_response``.
200-token bound (800-byte ceiling) was tight enough to force the envelope path but rejected the eventual envelope at the final fit check (implementation's load_recipe metadata — post-prune step names plus ingredients_table plus routing edges — is wider than 800 bytes when enveloped). 500 tokens (2,000 bytes) keeps the envelope trigger (implementation's serialized payload exceeds the bound) while the eventual envelope stays under the ceiling.
500-token (2000-byte) bound is too tight: ``implementation``'s envelope — post-prune step names plus ingredients_table plus skeleton plus recipe_pull plus delivery_bound_spill — exceeds 2000 bytes and falls through to the first fail-closed candidate (which carries ``recipe_pull`` but no ``step_flow_skeleton``). 1000 tokens (4000 bytes) keeps the envelope trigger while giving the envelope enough headroom to keep its full structure.
Trecek
left a comment
There was a problem hiding this comment.
AutoSkillit PR Review — Verdict: changes_requested
| return artifact_dir / f"{safe_name or 'response'}_{uuid.uuid4().hex[:8]}.log" | ||
|
|
||
|
|
||
| def _recipe_artifact_path(artifact_dir: Path, tool_name: str, recipe_name: str) -> Path: |
There was a problem hiding this comment.
[critical] bugs: The artifact path is deterministic per tool and recipe, so reopening the same recipe with different overrides overwrites the payload referenced by an older envelope. get_recipe_section only checks the envelope digest when artifact_sha256 is supplied, while the generated client instructions omit that argument; an older pull can therefore silently return the newer composition. Require recipe_pull.sha256 on every pull or use content-addressed/versioned artifacts.
| return json.dumps( | ||
| {"success": False, "error": "invalid_recipe_artifact_identity"} | ||
| ) | ||
| persisted = json.loads(persisted_raw) |
There was a problem hiding this comment.
[warning] defense: json.loads accepts scalars and arrays here, but the code immediately treats the result as a mapping with .get. A valid-but-wrong-shaped artifact therefore falls into the outer generic exception path instead of returning the structured artifact failure used for corrupt JSON. Validate that persisted is a dict at this boundary and return a contextual recipe-artifact error.
| having to thread it through every surface. Re-opening the same recipe | ||
| overwrites the artifact (idempotent). | ||
| """ | ||
| from autoskillit.server._response_budget import _recipe_artifact_path # circular-break |
There was a problem hiding this comment.
[warning] arch: Recipe artifact persistence imports the private _recipe_artifact_path helper from the generic response-budget module through a function-local circular-break import, and the pull path repeats that dependency. This splits recipe-artifact ownership across transport projection and tool modules and preserves a latent import cycle; move naming and persistence behind one shared recipe-artifact boundary.
| post_prune_raw = cast(list[object], payload.get("post_prune_step_names") or []) | ||
| step_names = [str(n) for n in post_prune_raw if isinstance(n, str)] | ||
|
|
||
| skeleton = extract_step_skeleton( |
There was a problem hiding this comment.
[warning] tests: This all-recipe/all-backend fit contract builds a synthetic skeleton with no routing edges and uniformly short summaries instead of the production skeleton derived from active recipe steps. It can therefore pass while a real recipe skeleton falls through to the minimal error envelope. Exercise maybe_envelope_recipe_response with the actual active steps and assert the complete pull reference and skeleton survive.
Trecek
left a comment
There was a problem hiding this comment.
AutoSkillit review: blocking findings detected. See inline comments — changes requested.
Summary
This PR re-architects the recipe delivery model to keep inline responses bounded by construction across every backend's delivery limit, and adds an on-demand MCP pull tool for recipe section retrieval, closing issue #4304 (where the Codex delivery bound at 40KB was being exceeded by ~4.3×).
Across six coordinated plans: bounded summary priority ordering and negative-contract tests; unconditional single-call recipe artifact persistence; required
recipe_name: strthreading with a two-phase allocator (omits unaffordable fields, fairly splits remaining budget, decodes-and-retries to avoid splitting UTF-8 codepoints); canonical routing-edge reuse via package re-export; per-stepbyte_rangecomputed fromyaml.compose()node marks with fail-open guards; live end-to-end tests forget_recipe_section(bounded retrieval, invalid-section errors, artifact recreation, chunked continuation); and sous-chef guidance, ADR amendments, and fitness-test rewrites.Individual Group Plans
Plan 1: Delivery-Bound Envelope — Part A (Bounded Summary Priority)
The
_delivery_bound_summaryalgorithm inserver/_response_budget.pystarves thecontentfield to 0 bytes whensuggestions(48KB) is in_DELIVERY_BOUND_PRESERVED_KEYSand exceeds the Codex delivery bound (40KB). The algorithm computeshead_limit = max(0, bound - base_bytes - 64)from the unshrunk preserved-key envelope, findsbase_bytes > bound, clampshead_limitto 0, and never re-allocates budget freed by the later per-key projector fallback back tocontent. Additionally,BackendCapabilities.effective_delivery_token_limitdefaults to0(meaning "skip bounding"), creating a silent opt-out for any future backend that omits the field. No test asserts content survival, step-name coverage, or cross-threshold relational invariants. Part A fixes the bounded summary priority ordering, closes the capability holes, and adds the missing negative-contract tests. Part B covers the full architectural re-architecture to bounded envelope + pull access.Plan 2: Part B Foundations — Persistence + Recipe Name + Two-Phase Allocator
/audit-implfound gaps between the Part B rectify plan (bounded envelope +get_recipe_sectionpull-tool architecture, issue #4304) and what actually shipped. This part closes the two most foundational code gaps (REQ-B02, REQ-B03):maybe_envelope_recipe_responsepersists the recipe artifact unconditionally (before the fits-check), soget_recipe_sectionalways has a backing store; currently persistence only happens on the oversized/envelope branch. The pre-existing second persistence call inside the oversized branch is deleted, leaving exactly onepersist_recipe_artifactcall per invocation.build_recipe_envelopegains a requiredrecipe_name: strkeyword-only parameter threaded into every call site, including the production call insidemaybe_envelope_recipe_response. Its verbatim-field projection (orchestration_rules,stop_step_semantics) becomes a two-phase allocator: phase 1 omits any field that cannot afford its own JSON-key overhead, and phase 2 fairly splits remaining content budget between survivors. Truncation uses a decode-and-retry-on-error helper so a multi-byte UTF-8 codepoint is never split into an undecodable byte sequence. Other parts cover canonical routing-edge extraction, byte-range tracking, liveget_recipe_sectionend-to-end tests, sous-chef guidance, ADR amendments, and fitness-test rewrites.Plan 3: Delivery-Bound Envelope — Part B Re-Architecture
Even with Part A's priority fix, the
open_kitchen/load_reciperecipe payload (~175KB) is 4.3× the Codex delivery bound (40KB). The bounded summary is a degraded path: truncated content, however well-prioritized, can never deliver the full step graph that the orchestrator requires across multiple turns. This part re-architects the delivery model: the inline response becomes a bounded envelope (routing metadata + step-flow skeleton + step index) that fits the smallest backend bound by construction, with a new kitchen-gated MCP pull tool for on-demand step content retrieval. The full recipe is unconditionally persisted to a deterministic artifact path, and the pull tool serves bounded chunks from it. Part A covers the bounded summary priority fix, capability holes, and negative-contract tests. This part builds on top of Part A.Plan 4: Part B Code Gap Closure — Persistence + Recipe Name + Allocator
/audit-implfound gaps between the Part B rectify plan (bounded envelope +get_recipe_sectionpull-tool architecture, issue #4304) and what actually shipped. This part closes the two most foundational code gaps (REQ-B02, REQ-B03):maybe_envelope_recipe_responsepersists the recipe artifact unconditionally (before the fits-check), soget_recipe_sectionalways has a backing store. The pre-existing second persistence call inside the oversized branch is deleted, leaving exactly onepersist_recipe_artifactcall per invocation.build_recipe_envelopegains a requiredrecipe_name: strkeyword-only parameter threaded into every call site. Its verbatim-field projection uses a two-phase allocator that omits unaffordable fields and fairly splits remaining content budget. Truncation safely backs off from invalid UTF-8 boundaries. Later parts cover canonical routing-edge reuse, byte-range tracking, live pull-tool tests, sous-chef guidance, ADR amendments, and fitness-test rewrites.Plan 5: Routing-Edge Reuse + Byte-Range Tracking + Live Pull Tests
/audit-implfound gaps between the Part B rectify plan (bounded envelope +get_recipe_sectionpull-tool architecture, issue #4304) and what actually shipped. This part closes the routing-edge reuse, byte-range tracking, and live pull-tool test gaps (REQ-B04, REQ-B-T2, REQ-B-T4, REQ-B-T5): the hand-duplicated_local_extract_routing_edges/_LocalRouteEdgeare deleted in favor of the canonical_extract_routing_edges/RouteEdge, reused via a package-level re-export inrecipe/__init__.py.extract_step_skeletongains abyte_range: [start, end]field per step, computed by re-parsing the persisted artifact'scontentwithyaml.compose()node marks and guarded so malformed or non-mapping documents fail open. Three new tests exercise the liveget_recipe_sectiontool end-to-end: bounded section retrieval plus invalid-section error, artifact recreation after deletion, and chunking/continuation for an oversized section. Another part covers sous-chef guidance, ADR amendments, and rewriting pre-existing fitness tests.Plan 6: Sous-Chef Guidance + ADR Amendments + Fitness-Test Rewrite
/audit-implfound gaps between the Part B rectify plan (bounded envelope +get_recipe_sectionpull-tool architecture, issue #4304) and what actually shipped. This part closes the documentation and fitness-test-rewrite gaps (REQ-B11, REQ-B16, REQ-B17, REQ-B-T7, REQ-B-T8):sous-chef/SKILL.md's "Recipe Content Authority" section gainsget_recipe_sectionguidance. ADR-0005 and ADR-0004 are amended in place to describe the envelope + pull-access model. The two pre-existing tests intended to prove fit-by-construction are rewritten; raw-spill-path coverage is preserved under new names becauseenforce_response_budget's generic spill path remains valid coverage for non-recipe payloads. No other parts follow this one; this closes the remaining findings from the/audit-implreport.Implementation Plan
Plan files:
/home/talon/projects/autoskillit-runs/remediation-20260720-151951-749617/.autoskillit/temp/rectify/rectify_delivery_bound_envelope_2026-07-20_152800_part_a.md/home/talon/projects/autoskillit-runs/remediation-20260720-151951-749617/.autoskillit/temp/make-plan/delivery_bound_remediation_part_a_gaps_plan_2026-07-21_000021.md/home/talon/projects/autoskillit-runs/remediation-20260720-151951-749617/.autoskillit/temp/rectify/rectify_delivery_bound_envelope_2026-07-20_152800_part_b.md/home/talon/projects/autoskillit-runs/remediation-20260720-151951-749617/.autoskillit/temp/make-plan/delivery_bound_envelope_part_b_gaps_plan_2026-07-20_230200_part_a.md/home/talon/projects/autoskillit-runs/remediation-20260720-151951-749617/.autoskillit/temp/make-plan/delivery_bound_envelope_part_b_gaps_plan_2026-07-20_230200_part_b.md/home/talon/projects/autoskillit-runs/remediation-20260720-151951-749617/.autoskillit/temp/make-plan/delivery_bound_envelope_part_b_gaps_plan_2026-07-20_230200_part_c.mdCloses #4304
🤖 Generated with Claude Code via AutoSkillit