Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .autoducks/.installed.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
"schemaVersion": 1,
"source_repo": "deepducks/autoducks",
"channel": "stable",
"ref": "v0.5.2",
"sha": "b062c9a0ab871d9ee5dbb0bce1a430b27dbc36ee",
"version": "0.5.2",
"installed_at": "2026-08-02T20:42:10Z",
"installed_by": "manual",
"ref": "4ac745e52ee271fff7ef8b677cfb875c5572ae44",
"sha": "4ac745e52ee271fff7ef8b677cfb875c5572ae44",
"version": "0.5.8",
"installed_at": "2026-08-04T00:06:29Z",
"installed_by": "autoducks-update.yml#30864501655",
"previous": {
"ref": "3746c6b5f5e70ca1fa3526978784420e7e6a7081",
"sha": "3746c6b5f5e70ca1fa3526978784420e7e6a7081",
"version": "0.1.0"
"ref": "v0.5.2",
"sha": "b062c9a0ab871d9ee5dbb0bce1a430b27dbc36ee",
"version": "0.5.2"
}
}
46 changes: 46 additions & 0 deletions .autoducks/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,51 @@
# Changelog

## [0.5.8] - 2026-08-03

### Fixed
- fix(agent): narrow the custom-agent lane's base-ref claim to what it delivers (#1179)

## [0.5.7] - 2026-08-03

### Fixed
- fix(agent): Read belongs in the tool floor too (#1176)

## [0.5.6] - 2026-08-03

### Fixed
- fix(agent): a tool floor the definition cannot replace away (#1174)

## [0.5.5] - 2026-08-03

### Fixed
- fix(feedback): a max_turns retry hint for the agent lane (#1172)

## [0.5.4] - 2026-08-03

### Fixed
- fix(agent): honour surface: both, and stop double-posting refusals (#1170)

## [0.5.3] - 2026-08-02

### Security
- The custom-agent lane now reads agent definitions — and the `custom_agents`
config keys that grant them tools — from the base branch, never from the
checked-out tree. On a pull request the checkout is `refs/pull/N/head`, and a
definition body becomes the agent's prompt, so the previous behaviour could
execute unreviewed content with the repository's token. This applies to both
discovery and prompt assembly. (#1168)

### Changed
- **Behaviour change for `/agent`:** a definition that exists only on a pull
request is no longer discovered, so an agent cannot be tried from the pull
request that introduces it. Merge the definition to the default branch first,
then use it. A `/agent` run on a pull request still works and still takes that
pull request as its context; only the definition comes from elsewhere. (#1168)
- Removed the machinery this replaces: the `verified` descriptor field, the tool
clamp and its `unverified_tools` floor, the unverified-definition refusal, and
the `custom_agents.allow_unverified` opt-in. None of these were part of a
release, so no configuration needs migrating. (#1168)

## [0.5.2] - 2026-08-02

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion .autoducks/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.5.2
0.5.8
35 changes: 23 additions & 12 deletions .autoducks/agents/agent/defaults.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
{
"tools": [
"Read", "Write", "Edit", "Glob", "Grep", "WebFetch", "WebSearch",
"Read",
"Write",
"Edit",
"Glob",
"Grep",
"WebFetch",
"WebSearch",
"Bash(read-only)",
"Bash(git log:*)", "Bash(git show:*)", "Bash(git diff:*)", "Bash(git status:*)",
"Bash(git blame:*)", "Bash(git rev-parse:*)", "Bash(git branch --list:*)",
"Bash(gh issue view:*)", "Bash(gh issue list:*)", "Bash(gh pr view:*)",
"Bash(gh pr diff:*)", "Bash(gh pr list:*)", "Bash(gh issue comment:*)"
"Bash(git log:*)",
"Bash(git show:*)",
"Bash(git diff:*)",
"Bash(git status:*)",
"Bash(git blame:*)",
"Bash(git rev-parse:*)",
"Bash(git branch --list:*)",
"Bash(gh issue view:*)",
"Bash(gh issue list:*)",
"Bash(gh pr view:*)",
"Bash(gh pr diff:*)",
"Bash(gh pr list:*)",
"Bash(gh issue comment:*)"
],
"unverified_tools": [
"Read", "Glob", "Grep",
"Bash(read-only)",
"Bash(git log:*)", "Bash(git show:*)", "Bash(git diff:*)", "Bash(git status:*)",
"Bash(git blame:*)", "Bash(git rev-parse:*)", "Bash(git branch --list:*)",
"Bash(gh issue view:*)", "Bash(gh issue list:*)", "Bash(gh pr view:*)",
"Bash(gh pr diff:*)", "Bash(gh pr list:*)"
"required_tools": [
"Read",
"Write"
],
"max_turns": 30,
"labels": []
Expand Down
104 changes: 89 additions & 15 deletions .autoducks/agents/agent/pre.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,14 @@ status_comment::start "$ISSUE_NUM"
# via the shared pre-failed marker + skip=true (no LLM call, post.sh no-ops).
refuse() {
local message="$1"
its::comment_issue "$ISSUE_NUM" "$message" || true
# The reason goes in the status comment only. Posting it as a standalone
# comment as well put identical text on the issue twice for every refusal:
# once from its::comment_issue, once as the status comment's failure body.
react_to_comment "${COMMENT_ID:-}" "confused"
status_comment::fail "$ISSUE_NUM" "$message" 2>/dev/null || true
if ! status_comment::fail "$ISSUE_NUM" "$message" 2>/dev/null; then
# Nothing to edit — do not let a status-comment failure swallow the reason.
its::comment_issue "$ISSUE_NUM" "$message" || true
fi
progress_labels::abort "$ISSUE_NUM" "Agent:running" 2>/dev/null || true
touch "$AUTODUCKS_PRE_FAILED_MARKER"
[[ -n "${GITHUB_OUTPUT:-}" ]] && echo "skip=true" >> "$GITHUB_OUTPUT"
Expand All @@ -45,10 +50,22 @@ fi
# ── Refusal #2: custom agents disabled repo-wide (never opens a definition) ─
AGENT_REPO_ROOT="${GITHUB_WORKSPACE:-$(pwd)}"
AGENT_LIVE_CONFIG="${AUTODUCKS_CONFIG:-$AGENT_REPO_ROOT/.autoducks/autoducks.json}"
CUSTOM_AGENTS_ENABLED="true"
if [[ -f "$AGENT_LIVE_CONFIG" ]]; then
CUSTOM_AGENTS_ENABLED="$(jq -r 'if .custom_agents.enabled == false then "false" else "true" end' "$AGENT_LIVE_CONFIG" 2>/dev/null || echo true)"
fi

# Config, like the definitions themselves, is read from the base branch —
# never the checked-out tree. `enabled` is the repo owner's kill switch, so a
# contributor must not be able to flip it back on in the same change that
# uses the lane. See discover-agents.sh for the full reasoning.
agent_base_config() {
if [[ -n "${AUTODUCKS_BASE_REF:-}" ]]; then
git -C "$AGENT_REPO_ROOT" show "$AUTODUCKS_BASE_REF:.autoducks/autoducks.json" 2>/dev/null || echo '{}'
elif [[ -f "$AGENT_LIVE_CONFIG" ]]; then
cat "$AGENT_LIVE_CONFIG"
else
echo '{}'
fi
}

CUSTOM_AGENTS_ENABLED="$(agent_base_config | jq -r 'if .custom_agents.enabled == false then "false" else "true" end' 2>/dev/null || echo true)"
if [[ "$CUSTOM_AGENTS_ENABLED" == "false" ]]; then
refuse "🚫 Custom agents are disabled for this repository."
fi
Expand Down Expand Up @@ -97,6 +114,11 @@ if [[ "$GET_RC" -eq 4 || -z "$DESCRIPTOR_JSON" ]]; then

No custom agent named \`${AGENT_NAME}\` was found. Run \`$(autoducks_command_for agent) <name>\` naming one of the agents below.

Definitions are read from the default branch, so one that exists only in your
working tree, only on a feature branch, or under a \`.gitignore\`d path (\`.claude/\`
often is) will not appear here. Commit and merge it first — including the
pull request that introduces it, which cannot run its own agent.

$(build_catalog_comment)"
fi

Expand All @@ -122,13 +144,20 @@ fi
# ── Refusal #4: surface mismatch (issue vs pr) ──────────────────────────
CURRENT_SURFACE="issue"
[[ "${IS_PR:-false}" == "true" ]] && CURRENT_SURFACE="pr"
if [[ "$DESC_SURFACE" != "$CURRENT_SURFACE" ]]; then
if [[ "$DESC_SURFACE" == "pr" ]]; then
# `both` means both, so it never mismatches. Comparing for equality alone
# refused it on every surface, and the message then reported it as
# `surface: issue` — the else branch only distinguished `pr` — so an agent
# declared `both`, invoked on an issue, was told it can only run on an issue.
case "$DESC_SURFACE" in
both) : ;;
"$CURRENT_SURFACE") : ;;
pr)
refuse "🚫 \`${AGENT_NAME}\` is declared \`surface: pr\` and can only run from a pull request — re-run \`$(autoducks_command_for agent) ${AGENT_NAME}\` on the pull request instead."
else
;;
*)
refuse "🚫 \`${AGENT_NAME}\` is declared \`surface: issue\` and can only run from an issue — re-run \`$(autoducks_command_for agent) ${AGENT_NAME}\` on the issue instead."
fi
fi
;;
esac

# ── Tool resolution: discover-agents.sh already applied levels 1+2
# (custom_agents.agents.<name>.tools beats frontmatter tools outright, no
Expand All @@ -138,6 +167,29 @@ fi
# union of this lane's defaults.json with the repo-wide .defaults.tools). ──
TOOLS_CSV="$(jq -r '.tools_effective // [] | join(",")' <<<"$DESCRIPTOR_JSON")"

# Whatever the definition asks for, the lane's own output contract still has
# to be satisfiable. The wrapper prompt requires the agent to write
# /tmp/agent-response.md, and a definition that declares `tools` REPLACES the
# lane default outright — so `tools: [WebSearch]` produced an agent that was
# ordered to write a file with no tool that can write, burned its whole turn
# budget on denied calls, and failed as `scope-missing`, blaming the
# definition for "not stating an output contract".
#
# The same applies to Read: the wrapper's `## Input` section lists the
# materialized context files and tells the agent to read them, so a definition
# without Read answers blind. That failed quietly rather than loudly — the
# agent produced a plausible answer and only mentioned in passing that it
# could not read the request, which it misdiagnosed as sandboxing.
#
# So required_tools is unioned in, always. It is deliberately not part of
# defaults.json's `tools`: that list is a *default* a definition may replace,
# while this one is the floor the lane needs to function at all.
REQUIRED_TOOLS_JSON="$(jq -c '.required_tools // []' "$AUTODUCKS_PINNED_ROOT/.autoducks/agents/agent/defaults.json" 2>/dev/null || echo '[]')"
if [[ -n "$TOOLS_CSV" && "$REQUIRED_TOOLS_JSON" != "[]" ]]; then
TOOLS_CSV="$(jq -rn --argjson req "$REQUIRED_TOOLS_JSON" --arg csv "$TOOLS_CSV" \
'($csv | split(",")) + $req | unique_by(.) | join(",")')"
fi

DESC_MODEL="$(jq -r '.model // empty' <<<"$DESCRIPTOR_JSON")"
DESC_EFFORT="$(jq -r '.effort // empty' <<<"$DESCRIPTOR_JSON")"
DESC_MAX_TURNS="$(jq -r '.max_turns // empty' <<<"$DESCRIPTOR_JSON")"
Expand All @@ -152,10 +204,32 @@ if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
} >> "$GITHUB_OUTPUT"
fi

# ── Read the inherited definition body (the descriptor carries only
# body_bytes, not the text — discover-agents.sh scans the live tree, so the
# body is read from there too, not the pinned snapshot). ────────────────
DEFINITION_FILE="$AGENT_REPO_ROOT/$DESC_SOURCE"
# ── Read the inherited definition body ─────────────────────────────────
# From AUTODUCKS_BASE_REF, the same source discover-agents.sh enumerated it
# from. This is the load-bearing read of the whole lane: the body below
# becomes the agent's prompt, so reading it from the checked-out tree would
# mean executing content from refs/pull/N/head — exactly what discovering
# from the base ref exists to prevent. Discovery and prompt assembly must
# never disagree about which tree a definition came from.
DEFINITION_FILE="$(mktemp)"
# Separate from the ERR trap above, which reports the failure rather than
# cleaning up. Harmless to skip on an ephemeral runner, but discover-agents.sh
# traps its own temp dir and this script should not be the odd one out.
trap 'rm -f "$DEFINITION_FILE"' EXIT
if [[ -n "${AUTODUCKS_BASE_REF:-}" ]]; then
if ! git -C "$AGENT_REPO_ROOT" show "$AUTODUCKS_BASE_REF:$DESC_SOURCE" > "$DEFINITION_FILE" 2>/dev/null; then
refuse "🚫 \`${AGENT_NAME}\` could not be read from the base branch (\`${DESC_SOURCE}\`). Custom agents only run definitions that are merged."
fi
else
# No `|| true`: an unreadable definition here used to leave the file empty
# and let the run continue into a real LLM call with an empty `## Role`.
# Unreachable from the shipped workflow, which always sets AUTODUCKS_BASE_REF,
# but reachable from local and test invocations — and a silent empty role is
# the worst of the available failures. Refuse, matching the branch above.
if ! cat "$AGENT_REPO_ROOT/$DESC_SOURCE" > "$DEFINITION_FILE" 2>/dev/null; then
refuse "🚫 \`${AGENT_NAME}\` could not be read from \`${DESC_SOURCE}\`."
fi
fi

# extract_body FILE — strip a leading `---`-delimited frontmatter block if
# present, same detection discover-agents.sh's own parse_definition uses
Expand Down
Loading
Loading