Skip to content

feat(taskctl): Phase 2 — Composer agent: spec validation and task decomposition #203

@randomm

Description

@randomm

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:

  1. Check for existing job: If a job already exists for issue 201 with status running or stopped, don't start a new one. Tell PM to use taskctl resume instead.

  2. 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
    }
  3. 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 at packages/opencode/src/session/async-tasks.ts to understand how enableAutoWakeup works.

  4. Fetch the GitHub issue: Use gh issue view <number> --json title,body to get the issue content. Parse the JSON output.

  5. Spawn the Composer agent: Call Session.createNext() + SessionPrompt.prompt() with the Composer agent. Pass the issue title and body as the prompt. Look at how packages/opencode/src/tool/task.ts spawns child sessions — follow the same pattern.

  6. 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
  7. Return to PM: The start command 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:

  1. Read the issue title and body carefully

  2. 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?" }
      ]
    }
  3. 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
        }
      ]
    }
  4. 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: or file: 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: or file: 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:

  1. Parse the JSON response from the Composer agent
  2. If status === "needs_clarification": return the questions to PM, do not create tasks
  3. 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 201 to monitor."

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 enableAutoWakeup is called with the correct session ID

Run bun test and bun run typecheck — both must pass.

Acceptance criteria

  • packages/opencode/src/agent/agent.ts has a composer agent defined with mode: "subagent" and hidden: 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-composer flag works for manually pre-created tasks
  • Duplicate job prevention: starting twice for same issue returns helpful message
  • bun test passes, bun run typecheck passes with 0 errors

What NOT to build yet

  • No Pulse (Phase 3)
  • No developer spawning (Phase 3)
  • No taskctl status, stop, resume commands (Phase 3)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions