Skip to content

Tasks run marks tasks as completed when the agent session ends without producing code #144

Closed

Description

@alexvinicius

Summary

compozy tasks run <slug> --ide <runtime> rewrites every dispatched task's frontmatter to status: completed even when the agent never started work, because the assembled task prompt is structured as descriptive context rather than as an executable instruction. The agent enters a "ready when you are" standby state, the session ends cleanly, the runner mistakes the clean exit for success, and MarkTaskCompleted rewrites the file. Subsequent invocations of compozy tasks run then skip the task entirely (correct default with --include-completed=false), so work that was never performed becomes invisible.

Root Cause

The prompt assembled by buildPRDTaskPrompt (internal/core/prompt/prd.go) is delivered to the ACP client as the first user turn. Its structure today is:

# Implementation Task: <name>

## Task Context           (descriptive)
<required_skills>...      (XML guidelines)
<critical>...             (rules, conditional language)
## Workflow Memory        (descriptive)
## Task Specification     (the actual task body)
## Task Files             (paths + more rules)

Every section is either a fact ("Task file: …"), a rule ("Use installed cy-execute-task…"), or a specification ("# Task 42: …"). There is no imperative directive telling the agent to begin work now, and no explicit revocation of the agent's default consent-seeking behavior.

When Claude Code opens an ACP session, the runtime emits a model-handoff line ("Model set to default (Opus 4.7 1M)") before the first user turn is processed. The agent then receives the prompt above, finds no "do this now" instruction, and defaults to its safe-by-default greeting:

"Ready when you are — let me know if you want me to start on task_01."

— exactly the message visible in the screenshots below. The agent never invokes cy-execute-task, never reads any file, never produces any diff. The session ends cleanly. Then in internal/core/run/internal/acpshared/session_exec.go:

func handleSessionCompletion(...) jobAttemptResult {
    if sessionErr == nil {
        return jobAttemptResult{Status: attemptStatusSuccess, ExitCode: 0}
    }
    ...
}

A clean session end produces AttemptStatusSuccess (internal/core/run/internal/runshared/types.go:59-61). runner.go:84 unconditionally invokes afterJobSuccess, which in PRD-tasks mode delegates to afterTaskJobSuccess (internal/core/run/executor/review_hooks.go:39-93). That hook calls MarkTaskCompleted (internal/core/tasks/store.go:83-116):

rewritten, err := frontmatter.RewriteStringField(string(content), "status", "completed")

There is no verification at any layer that the agent:

  • modified or created any file under the workspace,
  • emitted a structured "task complete" signal,
  • referenced or addressed the task it was dispatched to handle.

So the bug compounds in two layers:

  1. Dispatch layer (root cause): the prompt is non-actionable, the agent stays idle.
  2. Completion layer (symptom amplifier): the runner treats sessionErr == nil as proof the task is done, regardless of whether anything actually happened.

Reproduction

  1. In a workspace with a .compozy/tasks/<slug>/ directory containing one or more task_NN.md files with status: pending, run:
compozy tasks run <slug> --ide claude
  1. Observe that the dispatched session opens with the agent in standby mode (transcript shows "Ready when you are — let me know if you want me to start on task_NN") and ends without any tool use.

  2. Inspect the workspace:

git status                                       # clean — no files changed
grep '^status:' .compozy/tasks/<slug>/task_*.md  # all show status: completed

The screenshots attached below show this state: the CLI cockpit shows two dispatched tasks (task_01, task_05) marked SUCCESS, while the right-hand pane shows the agent's actual response was a polite request for confirmation — never an attempt to do work. git status is clean, no source files exist, but the runner has already rewritten the task frontmatters.

Image Image

Proposed Solution

Two-layer fix, with the prompt fix as the primary repair and the diff-check as defense in depth.

Primary fix — prompt kickoff directive (root cause)

Prepend an unambiguous imperative section to buildPRDTaskPrompt that:

  • Names the specific task and points at the cy-execute-task skill.
  • Explicitly forbids asking the user for confirmation ("the user has already authorized execution by invoking compozy tasks run").
  • Forbids the "summarize the plan back" pattern that also surfaces as no-op behavior.

Mirror this with a closing directive at the very end of the prompt so the imperative is also the most recent context when the agent generates its first turn (recency effect).

Secondary fix — workspace diff-check (defense in depth)

Gate MarkTaskCompleted in afterTaskJobSuccess on a positive signal that the agent actually performed work. Capture a git status --porcelain snapshot before dispatching the job and again after; if the snapshots are equal and the capture path is supported, treat the attempt as a no-op: do not mark the task completed and emit a distinct task.file_skipped event so the no-op is visible in the run journal.

This catches the residual cases where the prompt fix alone wouldn't help — e.g., transport-level oddities, agent early exit, agent explicit refusal, or any future prompt regressions.

Future option (not in scope here)

Extend the ACP protocol with an explicit "task complete" assertion the agent must emit before the runner accepts success. Sessions that end without it stay pending. This is the most robust signal but requires upstream protocol work; the two layers above cover the reported case without it.

Notes

  • The filter in internal/core/tasks/walker.go:13-25 is correct: it skips IsTaskCompleted entries when --include-completed=false. The bug is upstream of that, in how tasks become completed in the first place.
  • --dry-run and --max-retries do not mitigate the issue. There is no current CLI flag that prevents the false-completion rewrite.
  • After encountering this, the user must reset the affected frontmatters manually (or revert the task files via VCS) before the runner will dispatch the same tasks again.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions