Tasks: Subagent Definitions & Multi-Agent Orchestration — User Story 3¶
Spec: 029-subagent-orchestration
User Story: US3 — Define Subagent with Custom System Prompt (Priority: P1)
Input: Design documents from /docs/product-specs/029-subagent-orchestration/
Prerequisites: spec.md, plan.md, research.md, data-model.md, contracts/subagent-spec.schema.json, quickstart.md
Scope: This file delivers ONLY US3. The Phase 2 (Foundational) tasks here are the minimum SubagentSpec/ClaudeConfig.agents scaffolding US3 needs to be testable; US1 and US2 reuse them but are out of scope here.
Tests: Included. SC-004 explicitly mandates validation-error tests at config-load time; without them, US3's acceptance scenarios cannot be verified.
Format: [ID] [P?] [Story] Description¶
- [P]: Can run in parallel (different files, no dependencies)
- [Story]:
[US3]only on User Story 3 tasks. Setup / Foundational / Polish carry no story tag. - All paths absolute from repo root
/Users/justinbarias/Documents/Git/python/agentlab/.
Phase 1: Setup (Shared Infrastructure)¶
Purpose: Confirm the working tree and tooling baseline before any model edits.
- T001 Verify branch is
feature/029-claude-subagentsand working tree is clean; runmake install-devif.venv/is missing sopytest,mypy,ruff, andblackare available. - T002 [P] Confirm
claude-agent-sdk==0.1.44is installed in.venv/(it providesAgentDefinition); runpython -c "from claude_agent_sdk.types import AgentDefinition; print(AgentDefinition)"to fail-fast if missing.
Phase 2: Foundational (Blocking Prerequisites — shared with US1/US2)¶
Purpose: Stand up the SubagentSpec model skeleton, the ClaudeConfig.agents field with empty-map normalization, the description validator, and the legacy claude.subagents migration validator so US3's prompt-sourcing rules have a model to attach to AND so SC-005 holds regardless of which story ships first. These tasks are NOT US3-unique — US1 and US2 build on the same scaffolding — and so are tagged as foundational, not [US3].
TDD-ordered: foundational tests (T003a, T003b, T003c) are authored FIRST and verified FAILING before implementation tasks (T004–T009).
CRITICAL: No US3 (Phase 3) work can begin until this phase is complete.
Foundational tests (write FIRST — must FAIL before T004–T009)¶
- T003a [P] Add
test_subagent_description_required_non_emptyintests/unit/models/test_claude_config.pyassertingSubagentSpec(description="", prompt="x")andSubagentSpec(description=" ", prompt="x")both raiseValidationError("subagent requires description"). Verify it FAILS today (model not yet added). (FR-004, SC-004) - T003b [P] Add
test_legacy_subagents_block_rejectedintests/unit/models/test_claude_config.pyassertingClaudeConfig(**{"subagents": {"enabled": True}})raisesValidationErrorwhose message contains"claude.subagents is no longer supported"and references bothclaude.agentsandexecution.parallel_test_cases. Verify it FAILS. Shared with US1.T002 — first owner wins. (FR-011, SC-005) - T003c [P] Add
test_claude_config_agents_empty_map_normalized_to_noneintests/unit/models/test_claude_config.pyassertingClaudeConfig(agents={}).agents is None. Verify it FAILS. Shared with US1.T003. (FR-010, research §5)
Foundational implementation¶
- T004 Delete the existing
SubagentConfigmodel and theClaudeConfig.subagentsfield insrc/holodeck/models/claude_config.py(lines 63-70 and 115-118 perplan.md). Also remove theSubagentConfigimport intests/unit/models/test_claude_config.pyand any test cases that reference it (lines ~158-196, 248-255, and thesubagents=...line intest_with_all_fieldsaround line 269). Shared with US1.T005/T011. - T005 Add a
SubagentSpec(BaseModel)skeleton insrc/holodeck/models/claude_config.pywithmodel_config = ConfigDict(extra="forbid")and the five fields perdata-model.md§1:description: str,prompt: str | None = None,prompt_file: str | None = None,tools: list[str] | None = None,model: Literal["sonnet", "opus", "haiku", "inherit"] | None = None. No prompt/prompt_file validators yet — those land in Phase 3. Shared with US1.T006 — first owner wins. (FR-002, FR-008) - T006 Add the
descriptionnon-empty-after-strip@model_validator(mode="after")onSubagentSpecinsrc/holodeck/models/claude_config.pythat raisesValueError("subagent requires description")whendescription.strip() == "". Confirm T003a now PASSES. (FR-004, SC-004) - T007 Add
agents: dict[str, SubagentSpec] | None = Field(default=None, ...)toClaudeConfiginsrc/holodeck/models/claude_config.py(description perdata-model.md§2). Add a@model_validator(mode="after")that normalizesagents == {}→agents = None. Confirm T003c now PASSES. Shared with US1.T007. (FR-001, FR-010, research §5) - T008 Add a
@model_validator(mode="before")onClaudeConfiginsrc/holodeck/models/claude_config.pythat detects a legacysubagentskey in the input dict and raisesValueErrorwith the exact message from data-model.md §2 rule 2:"claude.subagents is no longer supported; remove this block. Subagent forwarding is gated solely by the presence of claude.agents. To cap HoloDeck-side test concurrency, set execution.parallel_test_cases instead.". Themode="before"runs ahead ofextra="forbid"so the targeted error wins. Confirm T003b now PASSES. Shared with US1.T008. (FR-011, SC-005)
Checkpoint: SubagentSpec exists with description validator, ClaudeConfig.agents exists with empty-map normalization, the legacy subagents block is rejected with the documented friendly error, and all three foundational tests are green. US3 prompt-validation work can now begin.
Phase 3: User Story 3 — Define Subagent with Custom System Prompt (Priority: P1)¶
Goal: Allow users to give each subagent a distinct system prompt, sourced either inline (prompt) or from a file (prompt_file) resolved relative to the agent.yaml directory. Validation errors fire at config-load time.
Independent Test: Load three minimal agent.yaml fixtures (inline prompt, prompt_file, neither) through holodeck.config.loader. Verify:
1. Inline prompt → SubagentSpec.prompt == "You are a financial analyst.", prompt_file is None.
2. prompt_file: ./prompts/analyst.md → SubagentSpec.prompt contains the file's contents, prompt_file is None.
3. Neither → ValidationError("subagent requires either prompt or prompt_file") at load time.
Tests for User Story 3 ⚠️ WRITE FIRST — must FAIL before T017–T020¶
All test tasks add cases to
tests/unit/models/test_claude_config.py(single file). They share that file, so NOT marked[P]. The fixture-creation task T009 is[P]because it touches a different path.
- T009 [P] [US3] Create test prompt fixture at
tests/unit/models/fixtures/subagent_prompts/analyst.mdcontaining the literal textYou are a data analyst. Analyze data only — do not write code.(used by T012 and T015). Confirm parent directories exist; createtests/unit/models/fixtures/andtests/unit/models/fixtures/subagent_prompts/if missing. - T010 [US3] Add
test_subagent_inline_prompt_loadstotests/unit/models/test_claude_config.py— constructs aSubagentSpec(description="x", prompt="You are a financial analyst."), asserts.prompt == "You are a financial analyst."and.prompt_file is None. (US3 acceptance scenario 1; FR-002) - T011 [US3] Add
test_subagent_inline_prompt_empty_after_strip_rejectedtotests/unit/models/test_claude_config.py—SubagentSpec(description="x", prompt=" ")raisesValidationError. (data-model.md §1 rule "Non-empty after strip"; SC-004) - T012 [US3] Add
test_subagent_prompt_file_inlinedtotests/unit/models/test_claude_config.py— setsagent_base_dirContextVar(fromsrc/holodeck/config/context.py) totests/unit/models/fixtures/subagent_prompts/, constructsSubagentSpec(description="x", prompt_file="./analyst.md"), asserts.promptmatches the fixture's contents and.prompt_file is None. Useagent_base_dir.set(...)with a token andagent_base_dir.reset(token)in atry/finally(mirror the pattern insrc/holodeck/config/schema.py:104-106). (US3 acceptance scenario 2; FR-005) - T013 [US3] Add
test_subagent_prompt_file_resolves_relative_to_agent_base_dirtotests/unit/models/test_claude_config.py— same as T012 but uses a nested path (subdir/analyst.md) under a temp directory created viatmp_path, and verifies absolute paths are also accepted (path resolution doesn't mangle absolute paths). (FR-005; quickstart §5 row "prompt_file resolves relative") - T014 [US3] Add
test_subagent_prompt_file_not_found_rejectedtotests/unit/models/test_claude_config.py—SubagentSpec(description="x", prompt_file="./does-not-exist.md")raisesValidationErrorwhose message contains"prompt_file not found"and the offending path. (Edge case: nonexistent prompt_file; SC-004; quickstart §5) - T015 [US3] Add
test_subagent_prompt_and_prompt_file_mutually_exclusivetotests/unit/models/test_claude_config.py—SubagentSpec(description="x", prompt="hi", prompt_file="./analyst.md")raisesValidationError("prompt and prompt_file are mutually exclusive")(exact message per quickstart §5). (FR-006; SC-004) - T016 [US3] Add
test_subagent_neither_prompt_nor_prompt_file_rejectedtotests/unit/models/test_claude_config.py—SubagentSpec(description="x")raisesValidationError("subagent requires either prompt or prompt_file")(exact message per quickstart §5). (FR-006; US3 acceptance scenario 3; SC-004)
Run
pytest tests/unit/models/test_claude_config.py -n auto -vand confirm T010–T016 all FAIL with the expected error shapes before proceeding to implementation.
Implementation for User Story 3¶
All implementation tasks edit
src/holodeck/models/claude_config.py. Single file, sequential.
- T017 [US3] In
src/holodeck/models/claude_config.py, add a@model_validator(mode="after")onSubagentSpecthat raisesValueError("prompt and prompt_file are mutually exclusive")when bothself.promptandself.prompt_fileare non-None(truthy after theNonecheck; explicit empty strings are caught separately by T018/T019). (FR-006; satisfies T015) - T018 [US3] In the same
@model_validator(mode="after")(or a sibling validator in the same file), raiseValueError("subagent requires either prompt or prompt_file")when both areNone. Order this check after the mutual-exclusion check so the messages don't conflict. (FR-006; satisfies T016; US3 acceptance scenario 3) - T019 [US3] In
src/holodeck/models/claude_config.py, extend theSubagentSpec@model_validator(mode="after")to handleprompt_fileresolution. Mirror the canonical pattern atsrc/holodeck/config/schema.py:101-112:
from pathlib import Path
from holodeck.config.context import agent_base_dir # ContextVar[str | None]
if self.prompt_file is not None:
base_dir_value = agent_base_dir.get()
base_dir = Path.cwd() if base_dir_value is None else Path(base_dir_value)
path = Path(self.prompt_file)
if not path.is_absolute():
path = base_dir / path
if not path.exists():
raise ValueError(f"prompt_file not found: {path}")
self.prompt = path.read_text(encoding="utf-8")
self.prompt_file = None
Use model_validator(mode="after") so the assignments stick (Pydantic v2 returns self). Note the ContextVar is ContextVar[str | None] (not Path | None), so coerce only after the None check. (FR-005, FR-006; satisfies T012, T013, T014; data-model.md §1 rule 3; research.md §2)
- [ ] T020 [US3] In src/holodeck/models/claude_config.py, after prompt_file inlining, add a non-empty-after-strip check on the resulting self.prompt: raise ValueError("subagent prompt must be non-empty") when self.prompt is not None and self.prompt.strip() == "". This catches both inline empty prompts and prompt_file paths that point at an empty file. (data-model.md §1 invariant "always non-empty after construction"; satisfies T011)
Run
pytest tests/unit/models/test_claude_config.py -n auto -vand confirm T010–T016 all PASS.
Checkpoint: US3 is fully functional and testable independently. A subagent's prompt/prompt_file is correctly sourced and validated at config-load time, with the exact error messages spec'd in quickstart.md §5.
Phase 4: Polish & Cross-Cutting¶
- T021 Run
make formatandmake lintfrom the repo root; confirmsrc/holodeck/models/claude_config.pyandtests/unit/models/test_claude_config.pypass Black (88 cols) + Ruff with no warnings. - T022 Run
make type-checkfrom the repo root; confirmsrc/holodeck/models/claude_config.pypasses MyPy strict (especially theagent_base_dir.get()fallback path and thePathimport). - T023 [P] Run the full unit test suite —
pytest tests/unit/ -n auto -v— to confirm no regressions in adjacentClaudeConfigtests, in particular that_warn_effort_with_extended_thinkingstill fires. - T024 Validate
quickstart.md§5 row "prompt_file: ./does-not-exist.md" by writing a 5-line ad-hoc YAML fixture and loading it throughholodeck.config.loader.load_agent_config()from a Python REPL; confirm theValidationErrormatches the documented message verbatim. (SC-004 end-to-end smoke check)
Dependencies & Execution Order¶
Phase Dependencies¶
- Phase 1 (Setup): No dependencies — start immediately.
- Phase 2 (Foundational, TDD): Depends on Phase 1. BLOCKS Phase 3. Tests T003a/T003b/T003c authored FIRST and verified FAILING before T004–T008 implementations.
- Phase 3 (US3): Depends on Phase 2 completion. Within Phase 3, tests T009–T016 must be written and FAIL before implementation T017–T020.
- Phase 4 (Polish): Depends on Phase 3.
Task-Level Dependencies (within US3)¶
- T009 (fixture file) → T012, T013, T015 (tests that read the fixture).
- T010–T016 (failing tests) → T017–T020 (implementations that make them pass).
- T017 (mutual-exclusion check) → T018 (at-least-one check; ordered second so the error messages don't collide).
- T018 → T019 (prompt_file resolution; runs only if at least one of prompt/prompt_file is set).
- T019 → T020 (non-empty post-strip check; runs after
prompt_filehas been inlined intoprompt).
Foundational → US3 Dependencies¶
- T005 (SubagentSpec skeleton) → all of T010–T020.
- T006 (description validator) → T010–T016 (tests construct
SubagentSpec(description="x", ...)so the description rule must already permit non-empty strings). - T004 (delete old
SubagentConfig) → T005 (add new model) → T007 (addagentsfield) → T008 (add legacy migration validator) — strict but safe ordering since they all edit the same file. - T003a/T003b/T003c (foundational failing tests) → T006/T008/T007 (the implementations that turn each green).
Cross-File Dependencies a Developer Should Know¶
src/holodeck/config/context.pydefinesagent_base_dir: ContextVar[str | None](line 22 — notestr, notPath). T019 imports it and coerces withPath(...)only after the None check; do not add a new ContextVar.src/holodeck/config/loader.py(lines 775-783) setsagent_base_dirfor the duration of YAML loading. US3 does NOT modify the loader — the existing wiring is reused.src/holodeck/config/schema.py(lines 104-106) is the canonical example ofagent_base_dir.get()usage with a fallback for the unset case. T019's resolution logic must match that pattern.src/holodeck/lib/backends/claude_backend.pybuild_options()is out of scope for US3 — translation toAgentDefinitionbelongs to US1. US3 only guarantees thatSubagentSpec.promptis a non-empty string ready for downstream translation.- T004 deletes
SubagentConfig. Existing tests intests/unit/models/test_claude_config.py(lines 158-196, 248-255 per plan.md) reference it and are removed in the same task. Shared with US1.T005/T011 — coordinate ownership at PR time.
Parallel Execution Example: User Story 3¶
The vast majority of US3 tasks edit one file (tests/unit/models/test_claude_config.py or src/holodeck/models/claude_config.py) and are sequential. Foundational tests T003a/T003b/T003c are [P]. In Phase 3, only T009 is genuinely parallel.
# Phase 1 setup — independent commands:
make install-dev # T001
python -c "from claude_agent_sdk.types import AgentDefinition; print(AgentDefinition)" # T002
# Phase 3 — only T009 is parallelizable; the test- and impl-edits all share one file:
mkdir -p tests/unit/models/fixtures/subagent_prompts # T009 setup
# (then add the file's contents)
# After tests T010–T016 are written and confirmed failing, run the full file in parallel:
pytest tests/unit/models/test_claude_config.py -n auto -v # verify FAIL → implement → verify PASS
Implementation Strategy¶
Slice-First (US3 only — this file)¶
- Complete Phase 1 (Setup) — environment confirmed.
- Complete Phase 2 (Foundational, TDD) — author T003a/T003b/T003c first (failing); land T004 → T005 → T006 → T007 → T008. Each implementation turns the matching foundational test green. STOP if there is friction here: T004's deletion of
SubagentConfigwill break unrelated tests intest_claude_config.py; the deletion sweeps those legacy cases at the same time, but coordinate with US1 if a parallel branch is open. - Complete Phase 3 tests (T009–T016) — all FAIL.
- Complete Phase 3 implementation (T017–T020) — all PASS.
- Complete Phase 4 polish — format/lint/type-check/regression suite green.
- STOP and VALIDATE: Run
pytest tests/unit/models/test_claude_config.py -n auto -v -k subagentand confirm all foundational + US3 tests pass. Runmake ciand confirm no regressions.
Parallel Team Strategy (US3 + US1 + US2)¶
Phase 2 is the shared foundation. One developer should land Phases 1+2 alone, then US1, US2, and US3 can split:
- Developer A: US1 (build_options translation, schema, sample agent.yaml).
- Developer B: US2 (tools-list typo warnings, MCP scoping tests).
- Developer C (this file): US3 (prompt validation rules + fixture).
US3 has no runtime dependency on US1 or US2 once Phase 2 is in place — the model validators it adds are self-contained.
Notes¶
- Every task line traces to at least one of: FR-002, FR-004, FR-005, FR-006, US3 acceptance scenario 1/2/3, an edge case in spec.md §"Edge Cases", or SC-004.
[P]is used sparingly and only when the task touches a different file from sibling tasks.- US3 does not touch
claude_backend.py,agent.schema.json, or any sample YAML — those changes belong to US1. - The exact validation error messages in T012, T013, T014 are copied verbatim from
quickstart.md§5 so users see the documented strings. - After Phase 3,
SubagentSpec.prompt_fileis alwaysNonepost-construction (data-model.md §"Invariants"). Downstream code (US1'sbuild_options()) can readspec.promptdirectly without re-resolving paths. - Do NOT add a JSON schema fragment in this slice —
contracts/subagent-spec.schema.jsonis applied toschemas/agent.schema.jsonby US1.