-
Notifications
You must be signed in to change notification settings - Fork 0
Description
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_progresstasks, show minutes elapsed sincepipeline.last_activity - For
closedtasks, show the commit hash from the close reason or a comment [attempt 2/3]shown whenpipeline.attempt > 1
taskctl stop <jobId>
Gracefully stops a running pipeline. Does NOT destroy work:
- Read the job record
- If job is not
running: return "Job is not running" - Write
{ stopping: true }to the job file (Pulse reads this on next tick and initiates graceful stop — see Phase 3a) - Return to PM: "Stop signal sent. Pipeline will finish in-flight work and halt. Use
taskctl statusto 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:
- 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
- Check job status: if
running, refuse (Pulse may already be running). Ifstoppedorfailedor no lock found: proceed - Update job: set
status: "running",stopping: false - Call
enableAutoWakeup(ctx.sessionID)to re-register PM's current session for notifications (session ID may have changed since the originalstart) - Start Pulse (same as in
taskctl startfrom Phase 3a) - 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:
- Check that task is in a state where override makes sense (
failed,in_progress,review,blocked_on_conflict) - Kill any active developer/adversarial session for this task
- Remove the worktree
- Set task status to
closed,close_reasonto"skipped by PM",pipeline.stageto"done" - Add comment:
"Skipped by PM override" - Tasks that
depends_onthis task are now unblocked (Pulse will pick them up on next tick) - Return: "Task skipped. Dependent tasks are now unblocked."
taskctl override <taskId> --commit-as-is
Commit the current worktree state despite adversarial issues:
- Check that a worktree exists for this task
- Spawn @ops to commit the worktree to the feature branch (same as the normal commit path)
- If commit succeeds: close task with
close_reason: "committed as-is by PM override", remove worktree - If commit fails: return the error to PM
- 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:
- Kill any active session for this task
- Remove existing worktree (delete the directory)
- Create a fresh worktree from the latest state of the feature branch
- Reset:
pipeline.attempt = 1,pipeline.adversarial_verdict = null,pipeline.stage = "idle" - Set task status to
open(Pulse scheduler will pick it up on next tick) - Add comment:
"Retried by PM. Fresh worktree created from latest ${branch}." - 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:
statusrenders correctly for jobs in different states (all tasks open, some closed, some failed)stopsetsjob.stopping = trueand nothing elseresumehandles zombie lock, callsenableAutoWakeup, starts Pulseoverride --skipcloses task, clears worktree, unblocks dependentsoverride --commit-as-iscalls @ops commit and closes on successretryresets 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> --skipcloses task, removes worktree, unblocks dependents -
taskctl override <taskId> --commit-as-iscommits worktree via @ops and closes task -
taskctl retry <taskId>resets task toopenwith fresh worktree - All commands return descriptive errors for invalid states or missing IDs
-
bun testpasses,bun run typecheckpasses with 0 errors