Skip to content

Control loop dead-ends in two moves when the evaluated head is not the checked-out worktree #397

Closed

Description

@pengfei-threemoonslab

Summary

When the evaluated head is not the commit checked out in the worktree, verify --preview routes to detect against that worktree. Because the worktree does not contain the changed files, detect cannot name the project, returns control_state: human_review_required with next_action.command: null, and the loop ends. Two commands, both authorized by allowed_next_commands, and there is no forward path for either actor.

The remedy — check out the head — is derivable from what preview already knows and is never stated.

Reproduction

google/adk-samples#1745, worktree on main, CLI 0.16.0b7, contract 24.

Step 1

agents-shipgate verify --preview --json --workspace <repo> \
  --base 765b4d1fbdcdd397fc48c622c22d757d8c87bafe --head pr-1745
"control": {
  "state": "agent_action_required",
  "next_action": {
    "kind": "discover",
    "command": "shipgate detect --workspace <repo> --json",
    "why": "The project this change belongs to could not be established (the evaluated head 'pr-1745' is not the commit this worktree has checked out, and project markers are read from the worktree that init would run against) …"
  },
  "allowed_next_commands": ["shipgate detect --workspace <repo> --json"]
}

Step 2 — the only allowed command:

"control_state": "human_review_required",
"next_actor": "human",
"next_action": {"actor": "human", "kind": "review", "command": null},
"verify_required": false

The diagnosis in step 1 is correct and sharp. The routing that follows from it is not: detect is pointed at a tree that structurally cannot contain python/agents/smart_closer/, because that directory exists only on the PR branch.

Checking out pr-1745 and re-running the identical step-1 command clears the state immediately.

Secondary defect in the same path

detect's escalation hands the human a JSON selector inside a shell command, and no runnable command:

"next_action": "Review: Agents were found in 25 separate projects; this workspace is not one manifest's scope. Run `init --workspace <agent_project_candidates[].path> --write` for the project you are changing.",
"next_actions": [{"kind": "review", "command": null, …}]

init already does this correctly — its next_actions[] carry concrete per-candidate commands with executable and args arrays. detect should adopt the same shape.

Why this matters

allowed_next_commands promises a deterministic loop. A loop that terminates in two moves on a legitimate PR — with command: null for the human it escalates to — is worse than no loop, because the contract implied determinism.

Suggested direction

  1. When _head_is_current_worktree is false, say so as the actionable instruction ("check out <head>, or re-run with --head omitted"), rather than routing to a discovery command that reads the wrong tree.
  2. Give detect's unresolved-scope escalation the same concrete per-candidate commands init already emits.

Related: #327, #369 (structured argv on control contracts).


Found during a first-time-user adoption walk of adk-samples#1745 on 2026-08-20.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Queued; valuable but not blockingarea:agent-modeAgent-mode contract: control envelope, next actions, command stringsbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions