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
48 changes: 48 additions & 0 deletions .github/skill-tests/test_merge.sh
Original file line number Diff line number Diff line change
Expand Up @@ -274,4 +274,52 @@ test_detect_allowed_methods_requires_repo_slug() {
assert_contains "$_CAPTURED" "REPO_SLUG not set" "should report missing REPO_SLUG"
}

test_format_merge_commit_with_headline() {
_source_merge
assert_eq "$(format_merge_commit abcdef1234567 "Add thing")" "abcdef1 Add thing" "should shorten oid and append headline"
}

test_format_merge_commit_null_headline() {
_source_merge
assert_eq "$(format_merge_commit abcdef1234567 null)" "abcdef1" "should drop a null headline"
assert_eq "$(format_merge_commit abcdef1234567 "")" "abcdef1" "should drop an empty headline"
}

test_wait_for_checks_skipped_with_flag() {
_source_merge
WAIT_CHECKS=false
capture wait_for_checks
assert_eq "$_CAPTURED_EXIT" "0" "should skip the gate when WAIT_CHECKS=false"
}

test_wait_for_checks_refuses_failing() {
_source_merge
WAIT_CHECKS=true
PR_NUMBER=1
count_required_checks() { [[ "$1" == "fail" ]] && echo 2 || echo 0; }
capture wait_for_checks
assert_eq "$_CAPTURED_EXIT" "5" "should exit 5 on failing required checks"
assert_contains "$_CAPTURED" "failing required check" "should report failing checks"
}

test_wait_for_checks_times_out_on_pending() {
_source_merge
WAIT_CHECKS=true
PR_NUMBER=1
CHECK_TIMEOUT=0
count_required_checks() { [[ "$1" == "pending" ]] && echo 1 || echo 0; }
capture wait_for_checks
assert_eq "$_CAPTURED_EXIT" "5" "should exit 5 when checks stay pending"
assert_contains "$_CAPTURED" "pending required check" "should report pending checks"
}

test_wait_for_checks_passes_when_clear() {
_source_merge
WAIT_CHECKS=true
PR_NUMBER=1
count_required_checks() { echo 0; }
capture wait_for_checks
assert_eq "$_CAPTURED_EXIT" "0" "should pass when no checks fail or pend"
}

run_tests
4 changes: 2 additions & 2 deletions agents/skills/cross-agent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Scan for existing skills in all locations:
1. `.agents/skills/*/SKILL.md` (already canonical)
2. `.claude/commands/*.md` (Claude flat commands)
3. `.claude/skills/*/SKILL.md` (Claude directory skills)
Also check for a lowercase `claude.md` — Claude Code requires `CLAUDE.md` (uppercase) for discovery. Use `ls -1 | grep '^claude\.md$'` to detect the wrong casing (a plain `test -f` is unreliable on case-insensitive filesystems).
Also check for a lowercase `claude.md` — Claude Code requires `CLAUDE.md` (uppercase) for discovery. Use `command ls -1 | grep '^claude\.md$'` to detect the wrong casing (a plain `test -f` is unreliable on case-insensitive filesystems).

Classify the repo:

Expand Down Expand Up @@ -301,4 +301,4 @@ Present a final summary:
- **CLAUDE.md has significant content**: Merge into AGENTS.md before symlinking. Show the user what was merged.
- **Mixed state**: Some skills in .agents/skills/, some in .claude/commands/. Migrate only the ones not already canonical.
- **Skill name collision**: If a skill exists in both .claude/commands/ and .agents/skills/ with different content, warn the user and skip. Do not overwrite canonical skills.
- **Lowercase `claude.md`**: Claude Code requires uppercase `CLAUDE.md`. If `claude.md` exists, rename it (`git mv` if tracked, `mv` otherwise) before proceeding with Step 7. Use `ls -1 | grep '^claude\.md$'` for detection — `test -f` is unreliable on case-insensitive filesystems (macOS default).
- **Lowercase `claude.md`**: Claude Code requires uppercase `CLAUDE.md`. If `claude.md` exists, rename it (`git mv` if tracked, `mv` otherwise) before proceeding with Step 7. Use `command ls -1 | grep '^claude\.md$'` for detection — `test -f` is unreliable on case-insensitive filesystems (macOS default).
2 changes: 1 addition & 1 deletion agents/skills/dream/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ The only escape hatch is `--dry-run` for previewing what dream *would* do.

- Argus KB available: !`command -v argus 2>/dev/null | head -1`
- Recent changes since last dream run: !`tail -100 ~/.dots/sys/kb-changes/changes.jsonl 2>/dev/null | head -100`
- Last dream run: !`ls -t ~/.dots/sys/dream-runs 2>/dev/null | head -1`
- Last dream run: !`command ls -t ~/.dots/sys/dream-runs 2>/dev/null | head -1`
- Today's date: !`date +%Y-%m-%d`

## MCP tool naming
Expand Down
4 changes: 2 additions & 2 deletions agents/skills/equip/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ If no arguments are provided, ask the user what to analyze.
## Context

- Project root: !`pwd`
- Existing skills: !`ls agents/skills/ 2>/dev/null | head -40`
- Custom agents: !`ls agents/custom/ 2>/dev/null | head -20`
- Existing skills: !`command ls agents/skills/ 2>/dev/null | head -40`
- Custom agents: !`command ls agents/custom/ 2>/dev/null | head -20`
- Skill descriptions: !`grep -r "^description:" agents/skills/*/SKILL.md 2>/dev/null | head -40`
- Agent descriptions: !`grep "^description:" agents/custom/*.md 2>/dev/null | head -20`
- Project type: !`find . -maxdepth 1 \( -name go.mod -o -name Gemfile -o -name package.json -o -name Cargo.toml -o -name pyproject.toml \) 2>/dev/null | head -3`
Expand Down
4 changes: 2 additions & 2 deletions agents/skills/handoff/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Generate a structured prompt capturing the current conversation context so it ca
## Context

- Calling task ID: !`echo $ARGUS_TASK_ID 2>/dev/null | head -1`
- Argus projects: !`ls -1 ~/.argus/worktrees/ 2>/dev/null | head -50`
- Argus projects: !`command ls -1 ~/.argus/worktrees/ 2>/dev/null | head -50`
- Repo root: !`git rev-parse --show-toplevel 2>/dev/null | head -1`
- Branch: !`git branch --show-current`
- Base ref: !`git branch -r 2>/dev/null | grep -oE 'origin/(main|master)' | head -1`
Expand All @@ -28,7 +28,7 @@ Generate a structured prompt capturing the current conversation context so it ca
- Changed files vs main: !`git diff --name-only origin/main..HEAD 2>/dev/null | head -30`
- Changed files vs master: !`git diff --name-only origin/master..HEAD 2>/dev/null | head -30`
- KB usage restriction in this repo: !`grep -liE 'never use argus-kb|argus-kb.*sandbox|sandboxed away from external systems' CLAUDE.md AGENTS.md 2>/dev/null | head -3`
- Repo-local context/ directory present: !`ls -d context 2>/dev/null | head -1`
- Repo-local context/ directory present: !`command ls -d context 2>/dev/null | head -1`

## Instructions

Expand Down
2 changes: 1 addition & 1 deletion agents/skills/improve/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Run `/improve` at the end of any session where:

- Current repo: !`git rev-parse --show-toplevel 2>/dev/null | head -1`
- Skills directory: !`find agents/skills -maxdepth 2 -name SKILL.md 2>/dev/null | head -30`
- Context directory exists: !`ls context/knowledge/index.md 2>/dev/null | head -1`
- Context directory exists: !`command ls context/knowledge/index.md 2>/dev/null | head -1`
- Knowledge base index: !`cat context/knowledge/index.md 2>/dev/null | head -30`
- Context directory structure: !`find context -type f 2>/dev/null | head -20`
- Voice profile: !`find . -maxdepth 3 -name 'voice-profile.md' -o -name 'VOICE.md' -o -name 'voice.md' 2>/dev/null | head -5`
Expand Down
7 changes: 5 additions & 2 deletions agents/skills/merge/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,11 @@ Resolve the script path — use the first that exists:
2. `agents/skills/merge/scripts/merge.sh` (repo-relative, for development/workspaces)

```
bash <script-path> [--method <squash|merge|rebase>] [--keep-branch] "<title>" "<body>"
bash <script-path> [--method <squash|merge|rebase>] [--keep-branch] [--no-wait-checks] "<title>" "<body>"
```

The script waits for pending required checks (default 1200s, override with the MERGE_CHECK_TIMEOUT env var in seconds) and refuses to merge when a required check is failing. Pass `--no-wait-checks` only when the user explicitly wants to skip that gate.

#### Choosing a merge method

The script defaults to **squash** (collapses all commits into one). Pick a different method when squash would lose intentional structure:
Expand All @@ -97,7 +99,7 @@ After a successful **squash** merge, the script auto-switches the current worktr
- `--method merge` / `--method rebase` — auto-switch is **off** by default. Both methods preserve the original commits, so the feature branch may still be useful for stacked PRs or chained follow-on work.
- `--keep-branch` — opt out of auto-switch on squash. The worktree stays on the feature branch and the script prints a divergence warning instead. Use this for stacked-PR workflows that squash but still need to keep the branch checked out (rare).

If auto-switch fails (e.g., uncommitted changes in the worktree, or `master` is checked out in another worktree), the script falls back to a loud warning rather than aborting — the merge itself already succeeded.
If the default branch is checked out in another worktree, the script falls back to `git checkout --detach <remote>/<default>`, leaving this worktree on a detached HEAD at the new tip (create a new branch before committing). If that also fails (e.g., uncommitted changes), it prints a loud warning listing both options — `git checkout --detach <remote>/<default>`, or `git reset --hard <remote>/<default>` to reuse the branch name (discards the branch commits, so only after the merge landed) — rather than aborting, since the merge itself already succeeded.

#### Review-required repos: automatic `--admin` escalation

Expand Down Expand Up @@ -135,4 +137,5 @@ On a repo where you hold admin, `/merge` lands the PR through the review gate it
- If the admin error mentions pending/required checks (or the PR still has checks running), tell the user: "PR is blocked by a still-pending required status check, which prevented even the admin merge. Wait for checks to finish, then re-run /merge." Re-running once checks are green typically lands it.
- If the admin error actually indicates missing permissions (e.g. "must have admin rights", "not authorized"), tell the user: "PR requires an approving review and I lack admin on this repo to bypass it. Ask a reviewer to approve, then re-run /merge."
- Otherwise, surface the literal admin error to the user verbatim and suggest the matching next step.
- **Exit 5 (required checks)** — A required check is failing, or still pending after the wait timeout. For failing checks, report which ones (`gh pr checks <number>`) and fix them first. For a timeout, tell the user the checks are still running and to re-run /merge once they finish (or raise MERGE_CHECK_TIMEOUT).
- **Exit 1** — Report the error from stderr.
90 changes: 76 additions & 14 deletions agents/skills/merge/scripts/merge.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ set -euo pipefail

# merge.sh — Merge current branch to the default branch via GitHub PR
#
# Usage: merge.sh [--skip-rebase] [--method <squash|merge|rebase>] [--squash|--merge|--rebase] [--keep-branch] "<title>" "<body>"
# Usage: merge.sh [--skip-rebase] [--no-wait-checks] [--method <squash|merge|rebase>] [--squash|--merge|--rebase] [--keep-branch] "<title>" "<body>"
#
# Without an explicit method, merge.sh probes the repo's allowed methods and
# tries them in preference order (squash → rebase → merge). Pass --method (or
Expand All @@ -22,6 +22,10 @@ set -euo pipefail
# 2 — rebase conflict (needs manual resolution)
# 3 — no commits to merge
# 4 — PR blocked (review required / changes requested) and auto-merge unavailable
# 5 — required checks failing, or still pending after the wait timeout
#
# Before merging, required checks are awaited (default 1200s, override with
# MERGE_CHECK_TIMEOUT=<seconds>). Pass --no-wait-checks to skip the gate.

COAUTHOR="Co-Authored-By: Claude <noreply@anthropic.com>"

Expand All @@ -44,6 +48,9 @@ PR_REVIEW_DECISION=""
LAST_GH_ERR=""
MERGE_METHOD_FLAG="" # input flag (squash|merge|rebase|""); narrows ALLOWED_METHODS when set
KEEP_BRANCH=false # when true, restore checkout to feature branch after squash merge
WAIT_CHECKS=true # when false, skip the required-checks gate
CHECK_TIMEOUT="${MERGE_CHECK_TIMEOUT:-1200}"
CHECK_POLL_INTERVAL="${MERGE_CHECK_POLL_INTERVAL:-15}"

# --- Helpers ---

Expand Down Expand Up @@ -182,6 +189,42 @@ check_pr_state() {
fi
}

# Count required checks in a gh bucket (pending|fail|cancel). Prints 0 when the
# PR has no required checks. A failed query also prints 0 (the gate fails open
# rather than blocking merges on gh outages) but warns so the skip is visible.
count_required_checks() {
local bucket="$1" count
if ! count=$(gh pr checks "$PR_NUMBER" --repo "$REPO_SLUG" --required --json bucket \
--jq "[.[] | select(.bucket == \"${bucket}\")] | length" 2>/dev/null); then
# gh exits non-zero with no output when no required checks are configured
count=0
info "Could not read required checks for PR #${PR_NUMBER} (none configured, or gh error); treating as 0 ${bucket}" >&2
fi
[[ "$count" =~ ^[0-9]+$ ]] || count=0
echo "$count"
}

# Refuse on failing required checks; wait (up to CHECK_TIMEOUT) on pending ones.
wait_for_checks() {
[[ "$WAIT_CHECKS" == true ]] || return 0

local waited=0 failing pending
while true; do
failing=$(( $(count_required_checks fail) + $(count_required_checks cancel) ))
[[ "$failing" -gt 0 ]] && die 5 "PR #${PR_NUMBER} has ${failing} failing required check(s) — fix them before merging (or pass --no-wait-checks to skip this gate)"

pending=$(count_required_checks pending)
[[ "$pending" -eq 0 ]] && return 0

if [[ "$waited" -ge "$CHECK_TIMEOUT" ]]; then
die 5 "PR #${PR_NUMBER} still has ${pending} pending required check(s) after ${CHECK_TIMEOUT}s — wait for them and re-run /merge"
fi
info "Waiting on ${pending} pending required check(s) (~${waited}s of ${CHECK_TIMEOUT}s; gh call time not counted)..."
sleep "$CHECK_POLL_INTERVAL"
waited=$((waited + CHECK_POLL_INTERVAL))
done
}

detect_allowed_methods() {
[[ -z "$REPO_SLUG" ]] && die 1 "REPO_SLUG not set — detect_default_branch must run first"

Expand Down Expand Up @@ -334,12 +377,22 @@ fetch_merge_commit() {
return 0 # no merge commit yet for auto-merge
fi

local commit_line
commit_line=$(gh pr view "$PR_NUMBER" --repo "$REPO_SLUG" --json mergeCommit \
--jq '.mergeCommit | "\(.oid[0:7]) \(.messageHeadline)"' 2>/dev/null || echo "")
if [[ -n "$commit_line" ]]; then
MERGE_COMMIT="$commit_line"
fi
# gh's mergeCommit JSON carries only the oid for PRs (messageHeadline comes
# back null), so fetch the headline from the commits API.
local oid headline
oid=$(gh pr view "$PR_NUMBER" --repo "$REPO_SLUG" --json mergeCommit \
--jq '.mergeCommit.oid // empty' 2>/dev/null || echo "")
[[ -z "$oid" ]] && return 0
headline=$(gh api "repos/${REPO_SLUG}/commits/${oid}" \
--jq '.commit.message | split("\n")[0]' 2>/dev/null || echo "")
MERGE_COMMIT=$(format_merge_commit "$oid" "$headline")
}

# Format "<short-oid> <headline>"; omit the headline when it is empty or "null".
format_merge_commit() {
local oid="$1" headline="${2:-}"
[[ "$headline" == "null" ]] && headline=""
echo "${oid:0:7}${headline:+ $headline}"
}

update_local_master() {
Expand Down Expand Up @@ -383,13 +436,20 @@ update_local_master() {
git -C "$worktree_path" pull "$TARGET" "$DEFAULT_BRANCH" 2>/dev/null || true
fi
if [[ "$auto_switch" == true ]]; then
info "NOTE: could not auto-switch this worktree to ${DEFAULT_BRANCH} — still on '${BRANCH}' (now divergent)."
if [[ -n "$worktree_path" ]]; then
info " ${DEFAULT_BRANCH} is checked out in worktree '${worktree_path}'."
# Fall back to a detached HEAD at the new remote tip: it leaves the
# divergent feature branch without needing the default branch's name.
if git fetch "$TARGET" "$DEFAULT_BRANCH" 2>/dev/null \
&& git checkout --detach "${TARGET}/${DEFAULT_BRANCH}" 2>/dev/null; then
info "Worktree now detached at ${TARGET}/${DEFAULT_BRANCH}${worktree_path:+ (${DEFAULT_BRANCH} is checked out in worktree '${worktree_path}')}; '${BRANCH}' was squashed and is now divergent."
info " Run 'git checkout -b <new-branch>' before committing new work."
else
info " Run 'git fetch ${TARGET} ${DEFAULT_BRANCH}' to create a local ref, then"
info "NOTE: could not auto-switch this worktree to ${DEFAULT_BRANCH} — still on '${BRANCH}' (now divergent)."
if [[ -n "$worktree_path" ]]; then
info " ${DEFAULT_BRANCH} is checked out in worktree '${worktree_path}'."
fi
info " Options: 'git checkout --detach ${TARGET}/${DEFAULT_BRANCH}' (leave the branch, keep it as-is),"
info " or 'git reset --hard ${TARGET}/${DEFAULT_BRANCH}' (discards '${BRANCH}' commits, reuses the branch name; only safe after the merge landed)."
fi
info " 'git checkout ${DEFAULT_BRANCH}' or 'git reset --hard ${TARGET}/${DEFAULT_BRANCH}' before continuing."
fi
fi
}
Expand Down Expand Up @@ -429,12 +489,13 @@ main() {
while [[ "${1:-}" == --* ]]; do
case "$1" in
--skip-rebase) skip_rebase=true; shift ;;
--no-wait-checks) WAIT_CHECKS=false; shift ;;
--squash) MERGE_METHOD_FLAG="squash"; shift ;;
--merge) MERGE_METHOD_FLAG="merge"; shift ;;
--rebase) MERGE_METHOD_FLAG="rebase"; shift ;;
--method) MERGE_METHOD_FLAG="${2:?--method requires squash|merge|rebase}"; shift 2 ;;
--keep-branch) KEEP_BRANCH=true; shift ;;
*) die 1 "Unknown flag: $1 — usage: merge.sh [--skip-rebase] [--method <squash|merge|rebase>] [--squash|--merge|--rebase] [--keep-branch] \"<title>\" \"<body>\"" ;;
*) die 1 "Unknown flag: $1 — usage: merge.sh [--skip-rebase] [--no-wait-checks] [--method <squash|merge|rebase>] [--squash|--merge|--rebase] [--keep-branch] \"<title>\" \"<body>\"" ;;
esac
done

Expand All @@ -443,7 +504,7 @@ main() {
*) die 1 "Invalid --method '${MERGE_METHOD_FLAG}' — must be squash, merge, or rebase" ;;
esac

local title="${1:?Usage: merge.sh [--skip-rebase] [--method <squash|merge|rebase>] \"<title>\" \"<body>\"}"
local title="${1:?Usage: merge.sh [--skip-rebase] [--no-wait-checks] [--keep-branch] [--method <squash|merge|rebase>] \"<title>\" \"<body>\"}"
local body="${2:-}"

# Append co-author line
Expand Down Expand Up @@ -478,6 +539,7 @@ ${COAUTHOR}"
info "Method override: using only ${MERGE_METHOD_FLAG} (skipping probe-based fallback)"
ALLOWED_METHODS=("$MERGE_METHOD_FLAG")
fi
wait_for_checks
do_merge "$title" "$body"
fetch_merge_commit # must precede update_local_master (uses GitHub API, not local state)
update_local_master
Expand Down
2 changes: 1 addition & 1 deletion agents/skills/plan/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Read a PRD, tech spike, or feature spec and produce a concrete implementation pl
- Recent commits: !`git log --oneline -10 2>/dev/null | head -10`
- Prior plans: !`find .context/plans -name "*.md" 2>/dev/null | head -10`
- Prior spikes: !`find context/spikes -name "*.md" 2>/dev/null | head -10`
- Phase artifacts: !`ls -t .context/phases/*.md 2>/dev/null | head -10`
- Phase artifacts: !`command ls -t .context/phases/*.md 2>/dev/null | head -10`

## Phase Protocol

Expand Down
Loading
Loading