From 2272ecee4e161b7d6752e8fcf5a0d89703b819a3 Mon Sep 17 00:00:00 2001 From: J Date: Mon, 8 Jun 2026 14:11:42 +0800 Subject: [PATCH] feat(runtime): mark issue blocked when an agent stops mid-task When an agent does an interim/partial report but cannot continue (blocked, waiting on input, or stopping with work unfinished), its task ends and nothing resumes automatically. A natural-language "I'll continue later" comment left the issue looking like it was still progressing, so squad leaders and users could not tell the session had stopped (MUL-3114). Add a narrow blocked-on-stall exception to both the comment-triggered and assignment-triggered workflow steps: set `issue status blocked` and pin `blocked_reason` / `waiting_on` so the stall is machine-readable. The comment-triggered "do not change status unless asked" guardrail is kept. Co-authored-by: multica-agent --- .../internal/daemon/execenv/runtime_config.go | 4 +- .../daemon/execenv/runtime_config_test.go | 39 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/server/internal/daemon/execenv/runtime_config.go b/server/internal/daemon/execenv/runtime_config.go index f3e9e86cb..1cd101981 100644 --- a/server/internal/daemon/execenv/runtime_config.go +++ b/server/internal/daemon/execenv/runtime_config.go @@ -611,7 +611,7 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string { b.WriteString("7. **If you reply, post it as a comment — this step is mandatory when you reply.** Text in your terminal or run logs is NOT delivered to the user. ") b.WriteString(BuildCommentReplyInstructions(provider, ctx.IssueID, ctx.TriggerCommentID)) b.WriteString("8. Before exiting: only if this run produced a fact that clears the high bar (important AND likely to be re-read by future runs on this same issue, e.g. a new PR URL or deploy URL), or you noticed a metadata key from entry that is now stale, pin or clear it via `multica issue metadata set`/`delete`. Most runs write nothing here — that is the expected outcome, not a gap. When in doubt, do not write. See the `## Issue Metadata` section above for the full bar.\n") - b.WriteString("9. Do NOT change the issue status unless the comment explicitly asks for it\n\n") + fmt.Fprintf(&b, "9. Do NOT change the issue status unless the comment explicitly asks for it — with one exception: if you did real work this turn and are stopping with the task NOT finished (you are blocked, waiting on someone, or only completed a partial/interim step and cannot continue right now), run `multica issue status %s blocked` and pin `blocked_reason` (and `waiting_on` when you are waiting on a specific person or event). Once your task ends nothing continues automatically, so an \"I'll continue later\" comment alone leaves the issue looking like it is still progressing when no session is running. Skip the status change only if your Agent Identity forbids issue status changes — then make the blocker explicit in your comment.\n\n", ctx.IssueID) } else { // Assignment-triggered: defer to agent Skills for workflow specifics. b.WriteString("You are responsible for managing the issue status throughout your work, unless your Agent Identity forbids issue status changes.\n\n") @@ -627,7 +627,7 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string { } b.WriteString("7. Before exiting: only if this run produced a fact that clears the high bar (important AND likely to be re-read by future runs on this same issue, e.g. a new PR URL or deploy URL), or you noticed a metadata key from entry that is now stale, pin or clear it via `multica issue metadata set`/`delete`. Most runs write nothing here — that is the expected outcome, not a gap. When in doubt, do not write. See the `## Issue Metadata` section above for the full bar.\n") fmt.Fprintf(&b, "8. When done, run `multica issue status %s in_review` unless your Agent Identity forbids issue status changes; if it does, skip this step.\n", ctx.IssueID) - fmt.Fprintf(&b, "9. If blocked, run `multica issue status %s blocked` unless your Agent Identity forbids issue status changes. Post a comment explaining the blocker unless your Agent Identity forbids issue comments.\n\n", ctx.IssueID) + fmt.Fprintf(&b, "9. If blocked, run `multica issue status %s blocked` unless your Agent Identity forbids issue status changes. The same applies when you stop with the task unfinished and nothing will continue it automatically — you only finished an interim/partial step, or you are waiting on input: treat that as blocked, set the status, and pin `blocked_reason` (and `waiting_on` when you are waiting on a specific person or event). Leaving the issue in `in_progress`/`in_review` while no session is running makes it look like work is still happening when it has actually stopped. Post a comment explaining the blocker unless your Agent Identity forbids issue comments.\n\n", ctx.IssueID) } // Sub-issue creation semantics — the only piece of the old Parent / diff --git a/server/internal/daemon/execenv/runtime_config_test.go b/server/internal/daemon/execenv/runtime_config_test.go index cbf58ad44..7ddae6845 100644 --- a/server/internal/daemon/execenv/runtime_config_test.go +++ b/server/internal/daemon/execenv/runtime_config_test.go @@ -156,6 +156,45 @@ func TestCommentTriggeredProtocolDoesNotForceInReview(t *testing.T) { } } +// MUL-3114: a worker that stops mid-task (interim report, blocked, waiting on +// input) must mark the issue `blocked` rather than leaving it looking like +// work is still progressing. Both the comment-triggered and assignment- +// triggered workflows must carry this rule. +func TestBriefCarriesBlockedOnStallGuidance(t *testing.T) { + t.Parallel() + const issueID = "55555555-6666-7777-8888-999999999999" + + // Comment-triggered: the "do not change status" guardrail keeps its + // narrow blocked-on-stall exception. + comment := buildMetaSkillContent("claude", TaskContextForEnv{ + IssueID: issueID, + TriggerCommentID: "66666666-7777-8888-9999-aaaaaaaaaaaa", + }) + for _, want := range []string{ + "Do NOT change the issue status unless the comment explicitly asks for it", + "stopping with the task NOT finished", + "multica issue status " + issueID + " blocked", + "blocked_reason", + } { + if !strings.Contains(comment, want) { + t.Errorf("comment-triggered brief missing %q\n--- brief ---\n%s", want, comment) + } + } + + // Assignment-triggered: the blocked step also covers stopping with the + // task unfinished and nothing continuing automatically. + assignment := buildMetaSkillContent("claude", TaskContextForEnv{IssueID: issueID}) + for _, want := range []string{ + "If blocked, run `multica issue status " + issueID + " blocked`", + "The same applies when you stop with the task unfinished", + "blocked_reason", + } { + if !strings.Contains(assignment, want) { + t.Errorf("assignment-triggered brief missing %q\n--- brief ---\n%s", want, assignment) + } + } +} + // The CLAUDE.md workflow surface must carry the same issue-wide since-delta // new-comment hint as the per-turn prompt. PR #2816 requires the two surfaces // stay in sync.