-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Context
Part of the taskctl epic (#201). Depends on Phase 1 (#202) being complete first.
What you're building: The Composer — a one-shot LLM agent that reads a GitHub issue (or a freeform spec string), decides whether the spec is clear enough to work from, and if so decomposes it into a properly structured dependency graph of tasks written to the store. You're also wiring up taskctl start <issueNumber> as the command PM uses to kick everything off.
The Composer does NOT write code. It only creates tasks.
Background reading
Read lievo/plan-v2.md sections: "Composer" under The Six Agents, and "Phase 2" under Implementation Phases.
Also read how existing agents are defined programmatically — look at packages/opencode/src/agent/agent.ts around lines 77-202 where native agents like explore, build, and plan are defined. The Composer follows the same pattern: it's an agent with mode: "subagent" and a system prompt string.
Files to create/modify
packages/opencode/src/tasks/composer.ts ← Composer orchestration logic
packages/opencode/src/agent/agent.ts ← add Composer agent definition
packages/opencode/src/tasks/tool.ts ← add "start" command (modify existing)
How taskctl start works
When PM calls taskctl start 201:
-
Check for existing job: If a job already exists for issue 201 with status
runningorstopped, don't start a new one. Tell PM to usetaskctl resumeinstead. -
Create a Job record in the store:
{ id: `job-201-${Date.now()}`, parent_issue: 201, status: "running", created_at: new Date().toISOString(), stopping: false, pulse_pid: null, // Pulse not started yet in this phase max_workers: 3, pm_session_id: ctx.sessionID // the PM's current session }
-
Call enableAutoWakeup: Call
enableAutoWakeup(ctx.sessionID)so that PM's session will receive push notifications from the pipeline. This is how PM gets notified when tasks complete — it's the same mechanism used by the async task system today. Look atpackages/opencode/src/session/async-tasks.tsto understand howenableAutoWakeupworks. -
Fetch the GitHub issue: Use
gh issue view <number> --json title,bodyto get the issue content. Parse the JSON output. -
Spawn the Composer agent: Call
Session.createNext()+SessionPrompt.prompt()with the Composer agent. Pass the issue title and body as the prompt. Look at howpackages/opencode/src/tool/task.tsspawns child sessions — follow the same pattern. -
Process Composer output: Composer returns either:
- A clarification request (spec unclear) → return the questions to PM, mark job
failed - A list of tasks → write them all to the store, return "Job started: N tasks queued" to PM
- A clarification request (spec unclear) → return the questions to PM, mark job
-
Return to PM: The
startcommand should return immediately with a summary. Pulse will be started in Phase 3 — for now, just return the task list and tell PM "Pipeline execution will begin in Phase 3".
The Composer agent definition
In packages/opencode/src/agent/agent.ts, add a new agent following the exact same pattern as the existing native agents. Key fields:
{
name: "composer",
mode: "subagent", // can be spawned by tools, not user-selectable
hidden: true, // doesn't appear in the agent picker UI
model: "...", // use the configured default model
prompt: `...` // system prompt — see below
}Composer system prompt
The Composer's system prompt should instruct it to:
-
Read the issue title and body carefully
-
If the spec is missing acceptance criteria OR is too vague to decompose into concrete tasks, respond with this exact JSON and nothing else:
{ "status": "needs_clarification", "questions": [ { "id": 1, "question": "What specific behaviour should change?" } ] } -
Otherwise, respond with this exact JSON:
{ "status": "ready", "tasks": [ { "title": "Add OAuth2 config schema", "description": "Add zod schema for OAuth2 config to src/config/config.ts", "acceptance_criteria": "Schema validates clientId, clientSecret, redirectUri. Tests pass.", "task_type": "implementation", "labels": ["module:config", "file:src/config/config.ts"], "depends_on": [], "priority": 0 } ] } -
Rules for good task decomposition:
- Each task should be completable by one developer agent in a single session
- Every task MUST have
acceptance_criteria— without this, adversarial review has nothing to check against - Every task MUST have at least one label with
module:orfile:prefix — this enables conflict detection - Dependencies should be ordered: tasks that others depend on come first (lower priority number = higher priority)
- Tasks that don't share any
module:orfile:labels can run in parallel - Do not create tasks for work that isn't explicitly required by the issue
The system prompt should also tell the Composer to validate the graph before responding — check that no task's depends_on creates a cycle. If a cycle exists, fix it before outputting.
Processing the Composer's response
In composer.ts:
- Parse the JSON response from the Composer agent
- If
status === "needs_clarification": return the questions to PM, do not create tasks - If
status === "ready":- Run
validateGraph()from Phase 1 on the proposed tasks as an extra safety check - If validation returns errors (cycles, etc.): escalate to PM with the error details
- If validation passes: write all tasks to the store using the store from Phase 1
- Generate human-readable slugs for task IDs from their titles (e.g. "Add OAuth2 config schema" → "add-oauth2-config-schema"). If slug collides with existing task, append
-2,-3, etc. - Return to PM: "Job job-201-xxx started: 3 tasks queued. Use
taskctl status 201to monitor."
- Run
taskctl start 201 --skip-composer
Add an optional flag to bypass Composer and go straight to Pulse (which in this phase means just writing an empty job and returning). This is useful for cases where PM has already manually created tasks in Phase 1 and just wants to start the pipeline. The tasks must already exist in the store for the given issue — if none exist, return an error.
Testing
Write tests in packages/opencode/test/tasks/composer.test.ts. Because the Composer involves spawning an LLM agent, test the orchestration logic (not the LLM itself):
- Mock the Composer's response and verify that
status: "needs_clarification"returns questions to PM - Mock a
status: "ready"response with a valid task graph — verify tasks are written to store - Mock a
status: "ready"response with a circular dependency — verify the error is caught before writing - Verify job record is created in store on
taskctl start - Verify
enableAutoWakeupis called with the correct session ID
Run bun test and bun run typecheck — both must pass.
Acceptance criteria
-
packages/opencode/src/agent/agent.tshas acomposeragent defined withmode: "subagent"andhidden: true -
taskctl start <issueNumber>fetches the GitHub issue and spawns Composer - Composer returns structured JSON — ambiguous spec returns questions, clear spec returns task list
- Tasks are written to the store with human-readable slug IDs
- Graph is validated before writing — circular dependencies produce an error to PM, not corrupt store data
-
enableAutoWakeup(pmSessionId)is called when a job starts - Job record is written to store with correct
pm_session_id -
--skip-composerflag works for manually pre-created tasks - Duplicate job prevention: starting twice for same issue returns helpful message
-
bun testpasses,bun run typecheckpasses with 0 errors
What NOT to build yet
- No Pulse (Phase 3)
- No developer spawning (Phase 3)
- No
taskctl status,stop,resumecommands (Phase 3)