Repository navigation
Bundled implementation recipe rejects make-plan dispatch with output_dir='.' #5256
Description
Problem
The bundled implementation recipe cannot reach its planning worker: the plan
step passes output_dir: . to /autoskillit:make-plan, whose declared write scope
is {{AUTOSKILLIT_TEMP}}/make-plan/. The shared run_skill scope check correctly
rejects the repository root before the planner session can start.
Live evidence
- Run: https://github.com/TalonT-Org/AutoSkillit/actions/runs/37854525992
- Tested commit:
017b3f182e9ca81d8cecfc6539d01721dd8632d3 - Scenario: the implementation E2E fixture introduced by E2E test: implementation recipe on a seeded sandbox issue #5233 / PR Add seeded sandbox implementation E2E validation #5255.
- Sandbox seed: https://github.com/TalonT-Org/autoskillit-e2e-sandbox/issues/1
- Docker setup, sandbox installation, and baseline tests passed.
- The terminal envelope reports
run_skill output_dir is outside the skill's declared write scope. - The terminal summary identifies
make-planandoutput_dir '.'; execution stopped before implementation or PR creation.
Source evidence
The recipe, make-plan declaration, and shared containment helper are unchanged
between PR #5255's base df70657d0e18f7c1bd590666b49cc1c9d31ad626 and its tested
head. This is a pre-existing production dispatch mismatch surfaced by the new
E2E harness, not a recipe regression introduced by the harness PR.
- implementation.yaml at the tested commit:
steps.plan.with.output_diris.. - make-plan/SKILL.md at the tested commit:
write_pathsis bounded to the planner temp directory. tools_execution._run_skill_session._extend_closure_write_scopechecks the requested directory withbounded_scope_contains.
Deterministic Docker reproduction
An archive of the exact tested source was streamed into an existing local image.
The disposable container had a read-only root filesystem, no network or host
mounts, one CPU, a 1 GB memory/swap limit, and bounded temporary filesystems.
It parsed the actual recipe and skill declaration and called the actual shared
containment checker. No model session, test suite, host installation, or host
settings were involved.
{
"configured_output_dir": ".",
"configured_output_dir_allowed": false,
"candidate_output_dir": ".autoskillit/temp/make-plan/",
"candidate_output_dir_allowed": true,
"peak_python_rss_kb": 56272
}Artifacts: /home/talon/projects/generic_automation_mcp/.autoskillit/temp/prepare-issue/docker-scope-repro/.
Self-contained reproduction for another investigator
Prerequisites: Docker, Git, and a checkout containing commit
017b3f182e9ca81d8cecfc6539d01721dd8632d3. No MiniMax key, sandbox token, running
MCP server, or host Python environment is needed for this deterministic probe.
The original existing image was autoskillit-test:345e37f8007b68913b51cdf72132e327183dc9ed
(image ID 3ba87731aa0a). It supplied Python and dependencies; the code under test
was the streamed archive of 017b3f182, not the older package inside the image.
That image is local, so the following builds the repository's user image if an
investigator does not already have a suitable AutoSkillit image. Building requires
network access; the subsequent probe does not.
From the repository checkout:
REPRO_DIR=.autoskillit/temp/planning-scope-37854525992
REPRO_SHA=017b3f182e9ca81d8cecfc6539d01721dd8632d3
mkdir -p "$REPRO_DIR/build-context"
git archive "$REPRO_SHA" | tar -xf - -C "$REPRO_DIR/build-context"
docker build --target user --tag autoskillit-scope-repro:017b3f182 \
--file "$REPRO_DIR/build-context/scripts/docker/Dockerfile" \
"$REPRO_DIR/build-context"Save this complete script as $REPRO_DIR/probe.py:
import json
import os
import resource
from pathlib import Path
import yaml
from autoskillit.hooks._write_scope import bounded_scope_contains, decode_write_scope
source = Path('/work/src/autoskillit')
recipe = yaml.safe_load((source / 'recipes/implementation.yaml').read_text())
frontmatter = yaml.safe_load(
(source / 'skills_extended/make-plan/SKILL.md').read_text().split('---', 2)[1]
)
scope = decode_write_scope(frontmatter['write_paths'])
output_dir = recipe['steps']['plan']['with']['output_dir']
project_dir = '/work/sandbox'
Path(project_dir).mkdir()
os.chdir(project_dir)
configured_allowed = bounded_scope_contains(scope, output_dir, project_dir)
candidate_allowed = bounded_scope_contains(
scope, '.autoskillit/temp/make-plan/', project_dir
)
assert output_dir == '.'
assert configured_allowed is False
assert candidate_allowed is True
print(json.dumps({
'source_commit': '017b3f182e9ca81d8cecfc6539d01721dd8632d3',
'declared_write_paths': frontmatter['write_paths'],
'configured_output_dir': output_dir,
'configured_output_dir_allowed': configured_allowed,
'candidate_output_dir_allowed': candidate_allowed,
'peak_python_rss_kb': resource.getrusage(resource.RUSAGE_SELF).ru_maxrss,
}, indent=2))Then archive the exact source and stream it into a disposable container:
git archive "$REPRO_SHA" src/autoskillit --output="$REPRO_DIR/source.tar"
tar -rf "$REPRO_DIR/source.tar" -C "$REPRO_DIR" probe.py
docker run --rm -i --network=none --memory=1g --memory-swap=1g \
--cpus=1 --pids-limit=128 --read-only --cap-drop=ALL \
--security-opt=no-new-privileges \
--tmpfs /work:rw,size=128m,mode=1777 \
--tmpfs /tmp:rw,size=32m,mode=1777 \
--workdir /work --entrypoint /bin/sh autoskillit-scope-repro:017b3f182 \
-c 'set -eu
tar -xf - -C /work
if [ -x /opt/uv-tools/autoskillit/bin/python ]; then
probe_python=/opt/uv-tools/autoskillit/bin/python
else
probe_python=/home/autoskillit/.local/share/uv/tools/autoskillit/bin/python
fi
PYTHONPATH=/work/src "$probe_python" -B /work/probe.py' \
< "$REPRO_DIR/source.tar"Expected: exit 0, configured_output_dir_allowed=false, and
candidate_output_dir_allowed=true. Peak memory can vary. This demonstrates the
dispatch contract mismatch; it does not execute a planner session or establish
that the entire implementation recipe succeeds after changing the argument.
The portable interpreter-selection form above was also run against the existing
autoskillit-user:local image (ID 3438dcd579eb) with the same exact source archive,
resource limits, and container project cwd. It exited 0 with the same rejection
and admission results; recorded peak Python RSS was 56552 KiB. This validates the
user-image interpreter path and the streamed-source reproduction. The optional
fresh image build command was not rerun locally; it mirrors the user-target build
from the successful setup phase of the linked CI run.
Retrieve the live failure artifacts
With GitHub access to the public run:
gh run view 37854525992 --repo TalonT-Org/AutoSkillit --log-failed
gh run download 37854525992 --repo TalonT-Org/AutoSkillit \
--name e2e-implementation-37854525992-1 \
--dir "$REPRO_DIR/live-artifacts"Inspect out/envelope.json, out/result.json,
out/baseline-install.command.json, and out/baseline-test.command.json.
Artifacts may expire according to Actions retention; the quoted terminal error,
source revision, and self-contained probe remain usable after expiration.
Repair direction
Correct the recipe's planner output-directory selection while preserving plan
captures and write-scope enforcement. Do not widen the planner's write scope to
the repository root. Check other actual planner dispatches for the same mismatch
and add a focused recipe-to-skill boundary regression test.
Verify planning admission in isolated Docker, then rerun the full implementation
scenario on MiniMax twice against the same sandbox seed before claiming successful
commissioning or a measured full-pipeline concurrency peak.
Related issues and deduplication
Distinct from #4376, which concerns an implementation worker's clone cwd and a
misleading output-dir diagnostic. Here the explicit recipe output directory itself
is outside the planner's declared boundary. The reporter crash is already tracked
in #5249 and is not part of this issue.
Investigation findings (2026-10-09)
How this was checked:
- Investigated on
developatdf70657d0. Paths and line numbers refer to that commit. - Three adversarial validators reviewed the draft. Every correction was re-checked against source before it was applied.
- Full report (local):
.autoskillit/temp/investigate/investigation_make_plan_output_dir_write_scope_2026-10-09_091407.md.
Root cause
The checks on run_skill output_dir disagree about BOUNDED skills, meaning skills whose write_paths is a list of temp directories.
1. The runtime check is correct.
_extend_closure_write_scope(src/autoskillit/server/tools/tools_execution/_run_skill_session.py:598-630) rejects an explicitoutput_diroutside a BOUNDED skill's declared scope..resolves to the worktree root (_resolve_dispatch_paths,:147-182), and that root would become the child session's write-guard prefix. That is a widening, which ADR 0015 (docs/decisions/0015-write-containment.md:9,26-27) rules out.- Every backend and every resume reaches this check through
_prepare_owned_dispatch_session(:364). Nothing normalizes.first.
2. The static rule skill-write-path-recipe-alignment has drifted.
_eligible_skill_path(src/autoskillit/recipe/rules/rules_skill_write_path_alignment.py:99-121) returns early for.,${{ context.work_dir }}and slash-only values.- Its boundary check,
_declared_boundary_error(:81-96), uses the samebounded_scope_containsas the runtime but never sees those values. - The pre-filter was written in
2d878e78b(Rectify: SKILL.md Write Path / Recipe output_dir Cross-Validation Immunity #3536) for the rule's original iteration-scoping check, where skipping.is correct.a328ae397(fix: contain interactive writes and verify installations #5114) later added the boundary check behind it, and the boundary check inherited the exemption.
3. The rule write-skill-requires-source-output-dir and its twin test have a stale premise.
- The rule is at
src/autoskillit/recipe/rules/rules_contracts.py:712-761(ERROR); the test istests/recipe/test_planner_contracts.py:43. - Both require
output_diron every skill withwrite_behavioralwaysorconditional. The rule's rationale is "the write guard defaults to a temp-dir prefix that blocks source-file edits". - The rule suggests
${{ context.work_dir }}, which the runtime rejects for BOUNDED skills. 0dddf08b4([FIX] Rectify: Write Guard Source-Edit Blocks & Push-Without-Rebase Immunity #3167) created this rule. In the same commit it addedoutput_dir: '.'to the write-behavior steps, make-plan'splanstep included, to satisfy it.
How it went live:
a328ae397(fix: contain interactive writes and verify installations #5114, 2026-09-21) gated the runtime check onroot_boundary is not None. make-plan had nowrite_paths, so.passed.- fix: contain interactive writes and verify installations #5114 swept the one affected BOUNDED skill of the time, changing compose-pr from
.to'{{AUTOSKILLIT_TEMP}}/compose-pr'. f1badd4ae(feat: compose skill write scopes by union with typed write_paths declarations #5211, 2026-09-28, fixes Cook sessions cannot compose skills: write boundary intersects across loaded skills, and composable sub-skills like prepare-issue are unusable #5154) did three things:- It took 20 SKILL.md files from no
write_pathsto BOUNDED. 12 of them are bundled, including make-plan, prepare-pr, prepare-research-pr and compose-research-pr. - It made the runtime check depend on the scope kind.
- It changed no recipes.
- It took 20 SKILL.md files from no
- Since that commit, every bundled make-plan dispatch fails at admission.
Scope: 17 tracked dispatch sites, not one
All of these pass output_dir: . to a BOUNDED skill. All are under src/autoskillit/recipes/. The inventory was recomputed independently three times, and removing the static pre-filter in memory produces exactly these findings.
| Skill | Sites |
|---|---|
| make-plan (10) | implementation.yaml:437 (plan); implementation-groups.yaml:241 (plan), :397 (gap_replan); remediation.yaml:1478 (make_plan); merge-prs.yaml:451, :738, :929, :1557; research.yaml:532 (plan_phase); research-implement.yaml:190 (plan_phase) |
| prepare-pr (3) | implementation.yaml:1334; implementation-groups.yaml:1166; remediation.yaml:1660 |
| prepare-research-pr (2) | research.yaml:1139; research-review.yaml:104 |
| compose-research-pr (2) | research.yaml:1214; research-review.yaml:169 |
- Fixing only the plan step is not enough. Fixing only
implementation.yaml:437moves the implementation pipeline's failure toprepare_pr(:1334). - A local recipe has 3 more sites. The git-excluded
.autoskillit/recipes/remediation-fable.yamlhas them at:935(make-plan),:1110(prepare-pr) and:1163(compose-pr).- A PR cannot change that file.
- Migration notes skip recipes without
autoskillit_version(src/autoskillit/migration/loader.py:70-77).
- Other
.targets are fine. Every other step passing.targets an UNRESTRICTED skill (implement-worktree-no-merge, resolve-failures and the like), which is correct.
Why tests missed it
- The static rule's test cannot fail on this input.
test_rule_silent_on_fixed_bundled_recipes(tests/recipe/test_rules_skill_write_path_alignment.py:286-302) asserts zero findings, and the pre-filter guarantees zero findings for exactly the failing input. - Runtime unit tests never pair
.with a BOUNDED skill.tests/server/test_tools_execution_write_prefix.py:367-404covers outside, inside, absent and unbounded cases.- Generic fixtures declare
write_paths: inherit(tests/conftest.py:821-840). - The real make-plan dispatch test passes no
output_dir(tests/server/test_tools_execution_routing.py:270-289).
- Recipe-driven admission tests use only in-scope values. They exist (
tool_ctx_ready_recipe, defaultresearch/scope), but none drives an affected step. - Two checks require the rejected value. The write-skill rule and
test_write_skill_steps_have_output_dirboth require anoutput_dirthat the runtime rejects. - No end-to-end coverage. develop had no end-to-end implementation scenario before PR Add seeded sandbox implementation E2E validation #5255.
Recommended repair
1. Make all the checks agree. Land this in the same part as item 2: either half alone fails task test-local-gate.
- Runtime:
- Keep the current semantics. Do not special-case
.and do not widen any scope. - Make the error name the skill, the requested
output_dirand the declared directories (_run_skill_session.py:622-624).
- Keep the current semantics. Do not special-case
- Alignment rule:
- Remove the pre-filter in front of the boundary check.
- Explicitly treat
${{ context.work_dir }}and slash-only values as the worktree root. Removing the pre-filter alone does not catch them:_static_base_prefixreduces them to"", and_declared_boundary_error:82-83then returns early. - Keep the iteration-scoping check's own treatment of
..
write-skill-requires-source-output-dirandtest_write_skill_steps_have_output_dir:- Make both aware of the write-scope kind, and require
output_dironly for UNRESTRICTED write-behavior skills. - Drop the suggestion that the runtime rejects.
- If
rules_contracts.pyimportsautoskillit.hooks._write_scope, add exemptions intests/arch/test_import_paths.py:131-136andtests/arch/test_layer_enforcement.py:1173-1197.
- Make both aware of the write-scope kind, and require
2. Drop output_dir: . from the 17 sites.
- run_skill's default directory (
resolve_skill_temp_dir,src/autoskillit/core/io/io.py:137-147) is<cwd>/.autoskillit/temp/<skill>, exactly each skill's declared scope. - What I verified stays the same:
- Plan captures come from output tokens, not
output_dir. - Plans still land in
make-plan/, so the implement gate'sallowed_plan_dirscheck passes. - Attestation admits an omitted value (
src/autoskillit/recipe/_binding.py:632-636). - Clone-guard policy and write evidence are unchanged.
- Plan captures come from output tokens, not
- What changes: the write-watch snapshot shrinks from the whole worktree to the skill's directory, which is relevant to Broad run_skill output watches let unrelated temp mutations satisfy report-skill progress #4258.
- Precedent: the arch-lens and analyze-pipeline-health steps already dispatch BOUNDED skills without
output_dir. - Alternative not chosen: an explicit
'{{AUTOSKILLIT_TEMP}}/<skill>', following the fix: contain interactive writes and verify installations #5114 compose-pr precedent.- The runtime admits it.
- But
loop-iterated-step-requires-iteration-scoped-output(ERROR) fires on the 9 tracked steps that sit in routing cycles: 8 make-plan and 1 prepare_pr. - Iteration-scoped subdirectories would be cosmetic. These skills ignore
output_dir, and make-plan must write directly intomake-plan/for the implement gate.
- Regenerate the compiled recipe JSON (
task compile-recipes), the contract cards and the 5 diagram hash lines.
3. Regression tests:
- Runtime:
.with a BOUNDED skill is rejected. - Alignment rule:
.and${{ context.work_dir }}produce a finding with BOUNDED skills and none with UNRESTRICTED skills. - The write-skill rule and its test exempt BOUNDED skills.
- Parametrize
tool_ctx_ready_recipeindirectly with an affected step, for example("implementation", "plan"), so the realrun_skilladmission path is exercised. - Add a contract test over the tracked bundled recipes, with a positive control.
4. Then run the commissioning plan above: an isolated Docker admission check, then two MiniMax runs of the PR #5255 scenario.
Related, out of scope here
vis_apply(research.yaml:281-316,research-design.yaml) needs its own issue. This is from reading the code; it has not been run.- It passes
'{{AUTOSKILLIT_TEMP}}/run-vis-lenses'tovis-lens-{slug}skills that may only write tovis-lens-<slug>/. vis_synthesizereadscapture_dir: run-vis-lenses(synthesize-vis-plan/SKILL.md:108), while the lenses write to their own directories.- The lens
write_pathspredate feat: compose skill write scopes by union with typed write_paths declarations #5211, so this has likely been rejected since fix: contain interactive writes and verify installations #5114. - The static rule cannot see it because the skill name is templated.
- It passes
- implement-worktree-no-merge write-scope gate rejects a clone cwd, and its error misdirects to output_dir #4376 is confirmed distinct. It concerns the UNRESTRICTED worktree gate in
_prepare_owned_write_scope. Broad run_skill output watches let unrelated temp mutations satisfy report-skill progress #4258 is adjacent. No open PR fixes this issue. - Narrowing never reaches the write guard. An in-scope
output_diris validated but does not narrow the write-guard prefixes, because closure extension re-adds the skill's own declared directory (src/autoskillit/workspace/session_skills/_manager.py:194puts the root in the closure). That bears on the ADR 0015 wording, not on this fix. - This is a recurring pattern. There have been 8 single-layer
output_dir/write-guard changes since May: Fix write guard blocking dry-walkthrough from editing plan in-place #2922, [FIX] Rectify: Recipe output_dir Template Bypass #2986, [FIX] Rectify: Write Guard Source-Edit Blocks & Push-Without-Rebase Immunity #3167, [FIX] Remove push_reachable gate from write-skill-requires-source-output-dir rule #3205, Rectify: SKILL.md Write Path / Recipe output_dir Cross-Validation Immunity #3536, Rectify: Temp-Only Writes False Success — Write-Scope Enforcement & Evidence Path-Awareness #4240, fix: contain interactive writes and verify installations #5114 and feat: compose skill write scopes by union with typed write_paths declarations #5211. A/autoskillit:rectifyfollow-up should target drift between the static and runtime checks. It should also require a sweep of call sites whenever a skipped check becomes active, as fix: contain interactive writes and verify installations #5114 did and feat: compose skill write scopes by union with typed write_paths declarations #5211 did not. - Open question: projects with a custom
workspace.temp_dir. The default directory hard-codes.autoskillit/temp. Not traced.
Scope update (2026-10-09): vis_apply is fixed under this issue
The investigation findings above listed vis_apply as "Related, out of scope here" and proposed a separate issue. It is now in scope: this issue tracks both defects, and they ship in one PR.
Why it belongs here:
- Same defect class. A recipe step passes an
output_diroutside a BOUNDED skill's declaredwrite_paths. - Confirmed at runtime.
- The real
_resolve_dispatch_paths+_extend_closure_write_scoperejectvis-lens-always-on,vis-lens-methodology-normsandvis-lens-chart-selectgivenoutput_dir='.autoskillit/temp/run-vis-lenses'. The error isrun_skill output_dir is outside the skill's declared write scope. - All three are admitted when
output_diris omitted. _apply_step_tuning_fallbackre-injects the recipe value, so an orchestrator cannot avoid the rejection.
- The real
- Synthesis reads the wrong place.
vis_synthesizereads{{AUTOSKILLIT_TEMP}}/run-vis-lenses, which no skill writes. Each lens writes{{AUTOSKILLIT_TEMP}}/vis-lens-<slug>/vis_spec_*.md, and the capturedvis_lens_output_pathsis never consumed. - Small and coupled.
- The fix is about 25–35 hand-written lines, in recipes this fix already edits and regenerates.
- The new bundled-dispatch admission test fails on any templated-skill step that passes an
output_dir. Withvis_applyfixed here, that test needs no allowlist.
Sites:
src/autoskillit/recipes/research.yaml:vis_applyoutput_dir(:290) andvis_synthesizecapture_dir(:313).src/autoskillit/recipes/research-design.yaml:vis_applyoutput_dir(:228) andvis_synthesizepositional capture argument (:241).
Repair:
- Drop
output_dirfrom bothvis_applysteps. Each lens then gets its default floor, which equals its declared directory. - Pass the lens output paths to synthesis.
vis_synthesizeconsumes${{ context.vis_lens_output_paths }}through a newsynthesize-vis-planinput,lens_output_paths. It is a comma-separated string and replacescapture_dir, following the existingcompose-research-prall_diagram_pathsprecedent.research-designalso declaresoptional_context_refs: [vis_lens_output_paths], because itsvis_applyroutes failures to synthesis. Synthesis already handles zero figures.
- Update the affected docs and tests. That means
synthesize-vis-plan/SKILL.md,tests/contracts/test_synthesize_vis_plan_contracts.pyand the phoropter docs. - Leave the generic synthesizers alone. The
phoropter-null-synthesisandphoropter-priority-synthesiscontracts keepcapture_dir. No bundled recipe dispatches them.
Regression coverage:
- A bundled-recipe data-flow test asserts that
vis_synthesizeconsumesvis_apply's captured paths. - The bundled-dispatch admission sweep fails on any templated-skill
output_dir. - A
vis-lens-always-onpositive control covers the pre-fix value.
Rectify plan (local): .autoskillit/temp/rectify/rectify_bounded_output_dir_write_target_authority_2026-10-09_150011_combined.md.
Activity