Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions scripts/scaffold.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ def _build_template_context(
tags = metadata.tags or []
has_llm_judge = (submission_dir / "tests" / "llm_judge.py").is_file()

# .mcp.json can be at root or inside supportive/ (legacy location)
has_mcp_json = (
(submission_dir / ".mcp.json").is_file()
or (submission_dir / "supportive" / ".mcp.json").is_file()
)

base_context = {
"submission_name": metadata.name,
"persona": metadata.persona or "general",
Expand All @@ -62,6 +68,7 @@ def _build_template_context(
"has_supportive": (submission_dir / "supportive").is_dir(),
"has_scripts": (submission_dir / "scripts").is_dir(),
"has_claude_md": (submission_dir / "CLAUDE.md").is_file(),
"has_mcp_json": has_mcp_json,
"has_llm_judge": has_llm_judge,
# These were formerly ad-hoc dict reads from raw metadata; they are
# not SubmissionMetadata fields (extra="forbid" rejects them), so the
Expand Down Expand Up @@ -135,13 +142,20 @@ def _copy_submission_files(

_copy_supportive(submission_dir, build_context_dir, variant)

# Treatment-only assets: CLAUDE.md, scripts/, docs, and skills are
# Treatment-only assets: CLAUDE.md, .mcp.json, scripts/, docs, and skills are
# skill knowledge that only the treatment variant should receive.
if variant == "treatment":
claude_md = submission_dir / "CLAUDE.md"
if claude_md.is_file():
shutil.copy2(claude_md, build_context_dir / "CLAUDE.md")

# .mcp.json: check root first, fall back to supportive/ (legacy location)
mcp_json = submission_dir / ".mcp.json"
if not mcp_json.is_file():
mcp_json = submission_dir / "supportive" / ".mcp.json"
if mcp_json.is_file():
shutil.copy2(mcp_json, build_context_dir / ".mcp.json")

scripts_src = submission_dir / "scripts"
if scripts_src.is_dir():
shutil.copytree(
Expand Down Expand Up @@ -210,11 +224,12 @@ def scaffold_submission(
strategy,
)
# Control is a vanilla agent: no supportive files, no scripts,
# no CLAUDE.md. Only instruction.md and tests are shared.
# no CLAUDE.md, no .mcp.json. Only instruction.md and tests are shared.
if variant == "control":
context["has_supportive"] = False
context["has_scripts"] = False
context["has_claude_md"] = False
context["has_mcp_json"] = False
rendered = _render_templates(jinja_env, context)

target_dir.mkdir(parents=True, exist_ok=True)
Expand Down
3 changes: 3 additions & 0 deletions templates/Dockerfile.j2
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ COPY tests/ /tests/
{% if has_claude_md %}
COPY CLAUDE.md /workspace/CLAUDE.md
{% endif %}
{% if has_mcp_json %}
COPY .mcp.json /workspace/.mcp.json
{% endif %}
{% if has_supportive %}
COPY supportive/ /workspace/supportive/
{% endif %}
Expand Down
77 changes: 77 additions & 0 deletions tests/test_scaffold.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,83 @@ def test_no_supportive_in_dockerfile_when_absent(self, valid_submission: Path, t
content = (treatment / "environment" / "Dockerfile").read_text()
assert "COPY supportive/" not in content

def test_mcp_json_at_root_copied_only_for_treatment(self, tmp_path: Path):
""".mcp.json at submission root is copied only for treatment."""
sub = tmp_path / "mcp-skill"
sub.mkdir()
(sub / "instruction.md").write_text("Do it.\n")
(sub / "skills").mkdir()
(sub / "skills" / "SKILL.md").write_text("# Skill\n")
(sub / "tests").mkdir()
(sub / "tests" / "test_outputs.py").write_text("def test(): pass\n")
(sub / ".mcp.json").write_text('{"mcpServers": {}}\n')
(sub / "metadata.yaml").write_text(yaml.dump({"name": "mcp-skill"}))

output = tmp_path / "output"
treatment, control = scaffold_submission(sub, output, TEMPLATES_DIR)
assert (treatment / "environment" / ".mcp.json").is_file()
assert not (control / "environment" / ".mcp.json").exists()

def test_mcp_json_in_supportive_copied_only_for_treatment(self, tmp_path: Path):
""".mcp.json in supportive/ (legacy location) is copied only for treatment."""
sub = tmp_path / "mcp-skill-legacy"
sub.mkdir()
(sub / "instruction.md").write_text("Do it.\n")
(sub / "skills").mkdir()
(sub / "skills" / "SKILL.md").write_text("# Skill\n")
(sub / "tests").mkdir()
(sub / "tests" / "test_outputs.py").write_text("def test(): pass\n")
supportive = sub / "supportive"
supportive.mkdir()
(supportive / ".mcp.json").write_text('{"mcpServers": {}}\n')
(sub / "metadata.yaml").write_text(yaml.dump({"name": "mcp-skill-legacy"}))

output = tmp_path / "output"
treatment, control = scaffold_submission(sub, output, TEMPLATES_DIR)
assert (treatment / "environment" / ".mcp.json").is_file()
assert not (control / "environment" / ".mcp.json").exists()

def test_dockerfile_includes_mcp_json_for_treatment(self, tmp_path: Path):
"""Dockerfile includes COPY .mcp.json when present."""
sub = tmp_path / "mcp-skill"
sub.mkdir()
(sub / "instruction.md").write_text("Do it.\n")
(sub / "skills").mkdir()
(sub / "skills" / "SKILL.md").write_text("# Skill\n")
(sub / "tests").mkdir()
(sub / "tests" / "test_outputs.py").write_text("def test(): pass\n")
(sub / ".mcp.json").write_text('{"mcpServers": {}}\n')
(sub / "metadata.yaml").write_text(yaml.dump({"name": "mcp-skill"}))

output = tmp_path / "output"
treatment, _ = scaffold_submission(sub, output, TEMPLATES_DIR)
content = (treatment / "environment" / "Dockerfile").read_text()
assert "COPY .mcp.json /workspace/.mcp.json" in content

def test_dockerfile_excludes_mcp_json_for_control(self, tmp_path: Path):
"""Dockerfile excludes .mcp.json for control variant."""
sub = tmp_path / "mcp-skill"
sub.mkdir()
(sub / "instruction.md").write_text("Do it.\n")
(sub / "skills").mkdir()
(sub / "skills" / "SKILL.md").write_text("# Skill\n")
(sub / "tests").mkdir()
(sub / "tests" / "test_outputs.py").write_text("def test(): pass\n")
(sub / ".mcp.json").write_text('{"mcpServers": {}}\n')
(sub / "metadata.yaml").write_text(yaml.dump({"name": "mcp-skill"}))

output = tmp_path / "output"
_, control = scaffold_submission(sub, output, TEMPLATES_DIR)
content = (control / "environment" / "Dockerfile").read_text()
assert "COPY .mcp.json" not in content

def test_no_mcp_json_in_dockerfile_when_absent(self, valid_submission: Path, tmp_path: Path):
"""Dockerfile excludes .mcp.json when not present in submission."""
output = tmp_path / "output"
treatment, _ = scaffold_submission(valid_submission, output, TEMPLATES_DIR)
content = (treatment / "environment" / "Dockerfile").read_text()
assert "COPY .mcp.json" not in content

def test_test_sh_includes_llm_judge_when_present(self, full_submission: Path, tmp_path: Path):
output = tmp_path / "output"
treatment, _ = scaffold_submission(full_submission, output, TEMPLATES_DIR)
Expand Down
Loading