Skip to content

Commit 7555994

Browse files
authored
[FIX] Fix Codex Investigate Report Sandbox (#4216)
## Summary Codex remediation runs were launching the `investigate` skill in a read-only sandbox even though that skill must write a markdown report and emit an `investigation_path` token. The fix updates the investigate skill contract to declare mandatory write behavior, gives the remediation recipe a scoped investigate output directory, refreshes the compiled recipe and diagram hash, and adds focused regression coverage for the server-side `run_skill` launch parameters. ## Requirements Codex-backed remediation runs can reach the first `run_skill` step (`investigate`) but fail before the recipe can continue because the child Codex session is read-only and cannot write the mandatory investigation report artifact under `.autoskillit/temp/investigate/`. The child can complete useful analysis and emit `%%ORDER_UP`, but `run_skill` adjudicates the step as `adjudicated_failure` because no report file or other write evidence exists. This blocks the pipeline before implementation begins. The remediation recipe's `investigate` step must be able to produce its required investigation artifact when run under the Codex backend, or the recipe must route that step to a backend/provider/sandbox mode capable of writing the artifact. If a skill contract requires artifact writes, dispatch/admission should reject unsupported Codex read-only execution before launching a zero-progress child. Closes #4215 ## Implementation Plan Plan file: `/home/talon/projects/generic_automation_mcp/.autoskillit/temp/codex-loop/issue-4215-narrow-implementation-plan.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 -->
1 parent a51c6d6 commit 7555994

8 files changed

Lines changed: 52 additions & 8 deletions

File tree

src/autoskillit/recipe/skill_contracts.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,10 @@ skills:
244244
- name: investigation_path
245245
type: file_path
246246
expected_output_patterns:
247-
- "investigation_path[ \\t]*=[ \\t]*/.+"
247+
- "investigation_path[ \\t]*=[ \\t]*/.+/investigate/.+\\.md"
248248
pattern_examples:
249-
- "investigation_path = /tmp/investigation.md\n%%ORDER_UP%%"
250-
read_only: true
249+
- "investigation_path = /tmp/project/run/investigate/investigation_bug_2026-07-08.md\n%%ORDER_UP%%"
250+
write_behavior: always
251251
make-groups:
252252
inputs:
253253
- name: source_doc

src/autoskillit/recipes/diagrams/remediation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!-- autoskillit-recipe-hash: sha256:c8205806500c54e98e1dd94e2f09f106c0b1603fcc60e2958ab70788c59a39c9 -->
1+
<!-- autoskillit-recipe-hash: sha256:0c8594b80bc296fe525502e13896523cdc864ce3e475526dd9991040eb657382 -->
22
<!-- autoskillit-diagram-format: v7 -->
33
## remediation
44
Investigate, rectify, implement, and merge a bug fix with CI and PR gates.

src/autoskillit/recipes/remediation.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@
225225
"with": {
226226
"skill_command": "/autoskillit:investigate ${{ '--depth deep ' if inputs.depth == 'deep' else '' }}${{ inputs.task }}",
227227
"cwd": "${{ context.work_dir }}",
228+
"output_dir": "{{AUTOSKILLIT_TEMP}}/investigate",
228229
"step_name": "investigate",
229230
"issue_url": "${{ inputs.issue_url }}",
230231
"issue_title": "${{ context.issue_title }}"

src/autoskillit/recipes/remediation.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ steps:
235235
skill_command: /autoskillit:investigate ${{ '--depth deep ' if inputs.depth
236236
== 'deep' else '' }}${{ inputs.task }}
237237
cwd: ${{ context.work_dir }}
238+
output_dir: '{{AUTOSKILLIT_TEMP}}/investigate'
238239
step_name: investigate
239240
issue_url: ${{ inputs.issue_url }}
240241
issue_title: ${{ context.issue_title }}

src/autoskillit/skills_extended/investigate/SKILL.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,14 @@ tool **before** beginning any analysis. Use the returned `content` field as the
9696
- Check for similar existing patterns in codebase
9797
- Ensure approaches, solutions, and fixes are the appropriate long-term solutions with proper architecture
9898

99+
## Context Limit Behavior
100+
101+
If context pressure interrupts the investigation before the final report token is emitted,
102+
resume from the partial notes and continue toward the same mandatory report. A successful
103+
invocation must still write exactly one markdown report under `{{AUTOSKILLIT_TEMP}}/investigate/`
104+
and emit `investigation_path = {absolute_path_to_investigation_report_file}` as the final
105+
plain-text line. Do not emit the token until the report exists.
106+
99107
## Standard Mode Workflow (Steps 1–4)
100108

101109
**Path-existence guard:** Before issuing a `Read` call on a path that is not guaranteed to
@@ -375,7 +383,7 @@ Spawn one adversarial subagent via `Agent(model="sonnet")` whose role is to disc
375383

376384
**If counterevidence is found:** Return to D3 for one additional deepening batch focused on reconciling the contradiction.
377385

378-
**If no counterevidence is found:** The hypothesis stands. Proceed to D5.
386+
**When the challenge finds no counterevidence:** The hypothesis stands. Proceed to D5.
379387

380388
### Step D5: Solution Convergence
381389

tests/recipe/test_contract_strength.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"dry-walkthrough",
2424
"file-audit-issues",
2525
"generate-report",
26+
"investigate",
2627
"make-campaign",
2728
"make-groups",
2829
"phoropter-null-synthesis",

tests/recipe/test_contracts.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -739,12 +739,17 @@ def test_write_behavior_conditional_loaded() -> None:
739739
assert any("conflict_report_path" in p for p in contract.write_expected_when)
740740

741741

742-
def test_write_behavior_defaults_to_none() -> None:
743-
"""investigate has no write_behavior — defaults to None."""
742+
def test_investigate_declares_report_write_contract() -> None:
743+
"""investigate writes a report artifact and must not run read-only."""
744744
manifest = load_bundled_manifest()
745745
contract = get_skill_contract("investigate", manifest)
746746
assert contract is not None
747-
assert contract.write_behavior is None
747+
assert contract.write_behavior == "always"
748+
assert contract.read_only is False
749+
assert contract.expected_output_patterns
750+
assert any(
751+
"/investigate/" in p and p.endswith(".md\n%%ORDER_UP%%") for p in contract.pattern_examples
752+
)
748753

749754

750755
ALWAYS_WRITE_SKILLS = {
@@ -762,6 +767,7 @@ def test_write_behavior_defaults_to_none() -> None:
762767
"implement-experiment",
763768
"implement-worktree",
764769
"implement-worktree-no-merge",
770+
"investigate",
765771
"make-campaign",
766772
"make-groups",
767773
"phoropter-null-synthesis",

tests/server/test_tools_execution_write_prefix.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,30 @@ async def test_allowed_write_prefix_uses_fallback_without_output_dir(
4545
expected = str(tmp_path / ".autoskillit" / "temp" / "test") + "/"
4646
assert executor.calls[0].allowed_write_prefix == expected
4747
assert executor.calls[0].allowed_write_prefixes == (expected,)
48+
49+
50+
@pytest.mark.anyio
51+
async def test_investigate_contract_runs_writable_with_report_watch_dir(
52+
tool_ctx_kitchen_open, monkeypatch, tmp_path
53+
) -> None:
54+
"""investigate must not be launched as a read-only skill because it writes a report."""
55+
from pathlib import Path
56+
57+
from tests.fakes import InMemoryHeadlessExecutor
58+
59+
executor = InMemoryHeadlessExecutor()
60+
tool_ctx_kitchen_open.executor = executor
61+
monkeypatch.setattr("autoskillit.server._ctx", tool_ctx_kitchen_open)
62+
63+
await run_skill("/autoskillit:investigate regression", str(tmp_path))
64+
65+
assert len(executor.calls) == 1
66+
call = executor.calls[0]
67+
assert call.readonly_skill is False
68+
assert call.write_behavior is not None
69+
assert call.write_behavior.mode == "always"
70+
assert call.expected_output_patterns
71+
72+
report_dir = tmp_path / ".autoskillit" / "temp" / "investigate"
73+
assert Path(report_dir) in call.write_watch_dirs
74+
assert call.allowed_write_prefix == str(report_dir) + "/"

0 commit comments

Comments
 (0)