Skip to content

feat(taskctl): Phase 3c — PM-facing commands: status, stop, resume, override, retry, inspect #206

@randomm

Description

@randomm

Context

Part of the taskctl epic (#201). Depends on Phase 3b (#205).

What you're building: The set of taskctl commands that PM uses to monitor and control a running pipeline. These are the human-facing interface — PM needs these to check progress, handle escalations, and manage unexpected situations.

Background reading

Read lievo/plan-v2.md sections: "PM Interaction Model" and "What PM No Longer Does".

Commands to implement

taskctl status <issueNumber>

Shows a live dashboard for the job associated with a GitHub issue. Example output:

Job #201: "Feature X" — 3/5 tasks complete
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ add-auth-middleware        closed  (commit a1b2c3)
✅ add-route-guards           closed  (commit b2c3d4)
🔨 implement-user-profile-api in_progress  14m active  [attempt 2/3]
⏳ add-profile-ui             open    (blocked on implement-user-profile-api)
⏳ write-integration-tests    open
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Pulse: healthy  |  1 agent active  |  maxWorkers: 3

Implementation notes:

  • Read the job record and all tasks for that issue from the store (no LLM involved — pure data)
  • "Pulse: healthy" means the lock file exists and the PID in it is still running. "Pulse: stopped" or "Pulse: not running" otherwise.
  • For blocked tasks, show which task(s) they're blocked on
  • For in_progress tasks, show minutes elapsed since pipeline.last_activity
  • For closed tasks, show the commit hash from the close reason or a comment
  • [attempt 2/3] shown when pipeline.attempt > 1

taskctl stop <jobId>

Gracefully stops a running pipeline. Does NOT destroy work:

  1. Read the job record
  2. If job is not running: return "Job is not running"
  3. Write { stopping: true } to the job file (Pulse reads this on next tick and initiates graceful stop — see Phase 3a)
  4. Return to PM: "Stop signal sent. Pipeline will finish in-flight work and halt. Use taskctl status to monitor."

The actual stopping happens in Pulse (already implemented in Phase 3a). This command just sets the flag.

taskctl resume <jobId>

Resumes a stopped or crashed pipeline:

  1. Check the lock file:
    • If lock exists and PID is alive: "Pipeline is already running"
    • If lock exists and PID is dead (zombie): delete lock file, proceed
    • If no lock: proceed
  2. Check job status: if running, refuse (Pulse may already be running). If stopped or failed or no lock found: proceed
  3. Update job: set status: "running", stopping: false
  4. Call enableAutoWakeup(ctx.sessionID) to re-register PM's current session for notifications (session ID may have changed since the original start)
  5. Start Pulse (same as in taskctl start from Phase 3a)
  6. Return: "Pipeline resumed. N tasks remaining."

taskctl inspect <taskId>

Shows the full history of a task — useful when PM needs to understand why it failed:

Task: implement-user-profile-api
Status: failed (3 adversarial cycles)
Branch: feature/issue-201
Worktree: ~/.local/share/opencode/worktree/proj-abc/implement-user-profile-api/

Pipeline history:
  [developing → reviewing]  attempt 1  2026-02-18T10:00:00Z
  [reviewing → developing]  attempt 2  adversarial: ISSUES_FOUND — "Missing null check at src/api.ts:42"
  [developing → reviewing]  attempt 2  2026-02-18T10:15:00Z
  [reviewing → developing]  attempt 3  adversarial: CRITICAL_ISSUES_FOUND — "Auth bypass possible"
  [developing → reviewing]  attempt 3  2026-02-18T10:32:00Z
  [failed]                  attempt 3  adversarial: CRITICAL_ISSUES_FOUND — "Auth bypass still present"

Last adversarial verdict:
  CRITICAL_ISSUES_FOUND
  Issues:
    - src/api/profile.ts:89: Missing authentication middleware (severity: CRITICAL)
      Fix: Add auth middleware before route handler

Comments (5 total):
  [developer] Implementation complete: added profile endpoint with CRUD operations
  [adversarial] Verdict: ISSUES_FOUND — see pipeline history
  ...

Implementation: read the full task JSON from the store and format it nicely. No LLM.

taskctl override <taskId> --skip

Skip a task entirely without committing any work:

  1. Check that task is in a state where override makes sense (failed, in_progress, review, blocked_on_conflict)
  2. Kill any active developer/adversarial session for this task
  3. Remove the worktree
  4. Set task status to closed, close_reason to "skipped by PM", pipeline.stage to "done"
  5. Add comment: "Skipped by PM override"
  6. Tasks that depends_on this task are now unblocked (Pulse will pick them up on next tick)
  7. Return: "Task skipped. Dependent tasks are now unblocked."

taskctl override <taskId> --commit-as-is

Commit the current worktree state despite adversarial issues:

  1. Check that a worktree exists for this task
  2. Spawn @ops to commit the worktree to the feature branch (same as the normal commit path)
  3. If commit succeeds: close task with close_reason: "committed as-is by PM override", remove worktree
  4. If commit fails: return the error to PM
  5. Return: "Committed current worktree state. PM takes responsibility for quality on this task."

taskctl retry <taskId>

Reset a failed or stuck task for a fresh attempt:

  1. Kill any active session for this task
  2. Remove existing worktree (delete the directory)
  3. Create a fresh worktree from the latest state of the feature branch
  4. Reset: pipeline.attempt = 1, pipeline.adversarial_verdict = null, pipeline.stage = "idle"
  5. Set task status to open (Pulse scheduler will pick it up on next tick)
  6. Add comment: "Retried by PM. Fresh worktree created from latest ${branch}."
  7. Return: "Task reset. Pulse will reschedule it on next tick."

Error handling

Every command should handle the case where:

  • The job or task ID doesn't exist → "No job/task found with ID X"
  • The job exists but has no running Pulse → note this clearly in the output
  • The store is in an unexpected state → descriptive error, never silent failure

Testing

packages/opencode/test/tasks/commands.test.ts

Test each command with mocked store state:

  • status renders correctly for jobs in different states (all tasks open, some closed, some failed)
  • stop sets job.stopping = true and nothing else
  • resume handles zombie lock, calls enableAutoWakeup, starts Pulse
  • override --skip closes task, clears worktree, unblocks dependents
  • override --commit-as-is calls @ops commit and closes on success
  • retry resets task fields and sets status to open

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

Acceptance criteria

  • taskctl status <issueNumber> shows formatted job dashboard with task states, attempt counts, block reasons
  • Pulse health shown in status (checks lock file + PID liveness)
  • taskctl stop <jobId> sets stopping flag; does not destroy work
  • taskctl resume <jobId> handles zombie locks, re-registers PM session, restarts Pulse
  • taskctl inspect <taskId> shows full pipeline history and all adversarial verdicts
  • taskctl override <taskId> --skip closes task, removes worktree, unblocks dependents
  • taskctl override <taskId> --commit-as-is commits worktree via @ops and closes task
  • taskctl retry <taskId> resets task to open with fresh worktree
  • All commands return descriptive errors for invalid states or missing IDs
  • bun test passes, bun run typecheck passes with 0 errors

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