diff --git a/.github/scripts/close-issues-on-dev.sh b/.github/scripts/close-issues-on-dev.sh new file mode 100755 index 00000000..cfd511a1 --- /dev/null +++ b/.github/scripts/close-issues-on-dev.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# close-issues-on-dev.sh — close the issues named by pull requests that merged into a branch other than +# dev, once their merge commit is part of dev (see .github/workflows/close-issues-on-dev.yml). +# +# DRY_RUN=1 .github/scripts/close-issues-on-dev.sh # list, close nothing +# +# Needs: gh (authenticated; GH_TOKEN in CI), a checkout with full history, and origin/dev or HEAD on dev. +# LOOKBACK: how many recently merged pull requests to consider (default 300). +set -euo pipefail + +DRY_RUN="${DRY_RUN:-0}" +LOOKBACK="${LOOKBACK:-300}" +REPO="${GITHUB_REPOSITORY:-$(gh repo view --json nameWithOwner -q .nameWithOwner)}" +DEV_REF="${DEV_REF:-HEAD}" +[ "$DEV_REF" = HEAD ] && [ "$(git rev-parse --abbrev-ref HEAD)" != dev ] && DEV_REF=origin/dev + +closed=0; pending=0 +while IFS=$'\t' read -r pr base merge body64; do + [ -n "$merge" ] || continue + [ "$base" = dev ] && continue # GitHub closes these itself + if ! git merge-base --is-ancestor "$merge" "$DEV_REF" 2>/dev/null; then + pending=$((pending + 1)); continue # merged, but not on dev yet + fi + body=$(printf '%s' "$body64" | base64 --decode) + for n in $(printf '%s\n' "$body" | grep -oiE '\b(close[sd]?|fix(e[sd])?|resolve[sd]?)[[:space:]]*:?[[:space:]]+#[0-9]+' \ + | grep -oE '[0-9]+$' | sort -un); do + state=$(gh issue view "$n" --repo "$REPO" --json state -q .state 2>/dev/null || echo MISSING) + [ "$state" = OPEN ] || continue + if [ "$DRY_RUN" = 1 ]; then + echo "would close #$n (pull request #$pr into $base, ${merge:0:9} is on dev)" + else + gh issue close "$n" --repo "$REPO" \ + --comment "Closed by #$pr, which merged into \`$base\` and reached \`dev\` in ${merge:0:9}." + echo "closed #$n (pull request #$pr into $base)" + fi + closed=$((closed + 1)) + done +done < <(gh pr list --repo "$REPO" --state merged --limit "$LOOKBACK" \ + --json number,baseRefName,mergeCommit,body \ + -q '.[] | [.number, .baseRefName, (.mergeCommit.oid // ""), (.body // "" | @base64)] | @tsv') + +verb=closed; [ "$DRY_RUN" = 1 ] && verb="to close" +echo "issues $verb: $closed; merged pull requests not on dev yet: $pending" diff --git a/.github/workflows/close-issues-on-dev.yml b/.github/workflows/close-issues-on-dev.yml new file mode 100644 index 00000000..e491bed8 --- /dev/null +++ b/.github/workflows/close-issues-on-dev.yml @@ -0,0 +1,44 @@ +# ───────────────────────────────────────────────────────────────────────────── +# A pull request that merges into a release or integration branch (0.2.1, a +# perf branch) never closes the issues it names: GitHub acts on "Closes #N" only +# when the merge lands on the default branch. This closes them when that work +# reaches `dev`. +# +# On every push to dev it looks at pull requests merged into any other branch, +# keeps those whose merge commit is now part of dev, and closes each open issue +# their description closes ("Closes", "Fixes", "Resolves" #N), with a comment +# naming the pull request. "Part of #N" and "Refs #N" are not closing words, so +# umbrella issues stay open. +# ───────────────────────────────────────────────────────────────────────────── +name: close-issues-on-dev + +on: + push: + branches: [dev] + workflow_dispatch: + inputs: + dry_run: + description: list what would be closed, close nothing + type: boolean + default: true + +permissions: + contents: read + issues: write + pull-requests: read + +jobs: + close: + name: close issues whose pull request reached dev + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: close the issues + env: + GH_TOKEN: ${{ github.token }} + DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run && '1' || '0' }} + run: .github/scripts/close-issues-on-dev.sh diff --git a/README.md b/README.md index ba0d1d45..afb5f4ac 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

AxiomCode Graph

- A god's-eye view of your codebase for AI agents. Stop grepping. Ensure correctness and completeness for any task your AI agent performs. + Know what each change breaks in your codebase. Stop grepping. Ensure correctness and completeness for any task your AI agent performs. Ship less buggy code.

@@ -127,7 +127,7 @@ established relationships that could lead to false positives. AxiomCode Graph is two parts. The **engine** (`@axiomcode/code-graph` on npm) parses a repository and builds its graph; it also provides the `axiomcode` command and an MCP server. The **plugin** (`plugins/axiomcode/`) is the -agent-facing frontend: a skill, four MCP tools, and hooks. Install the engine first. +agent-facing frontend: a skill, five MCP tools, and hooks. Install the engine first. Requirements: **Node ≥ 22.13** and **Python 3** (`python3`, or `python` / `py` on Windows). On Windows, also [Git for Windows](https://git-scm.com/download/win): the CLI runs under its bash. The engine ships as a prebuilt @@ -139,7 +139,7 @@ your platform. No Soufflé and no compiler are needed, with one exception: > `sudo apt install build-essential python3`. The full `node:*` Docker images already include them. Before every release, the exact packages that ship are installed without Soufflé and run end to end (every CLI verb, -five languages) on macOS arm64 and x64, Linux x64 and arm64, and Windows x64. Check an install with +six language engines, including HTML/CSS) on macOS arm64 and x64, Linux x64 and arm64, and Windows x64. Check an install with `axiomcode --version`. ### Installation @@ -259,7 +259,7 @@ looked up again in the graph before you see it; the `verified:` line is that che ### Support for agents -Every agent below gets the four MCP tools and the skill; the hooks, which keep the graph current and report what an +Every agent below gets the five MCP tools and the skill; the hooks, which keep the graph current and report what an edit breaks, run where the last column says so. No hook annotates the agent's own reads and searches. | Agent | Hooks | diff --git a/tests/mcp_docs.py b/tests/mcp_docs.py index 90092481..42a40d23 100644 --- a/tests/mcp_docs.py +++ b/tests/mcp_docs.py @@ -26,7 +26,7 @@ [os.path.join(ROOT, 'plugins', 'axiomcode', 'AGENTS.md')] + \ sorted(glob.glob(os.path.join(ROOT, 'plugins', 'axiomcode', 'rules', '*.mdc'))) -VERB = r'(find|impact|path|tests)' +VERB = r'(context|impact|link|path|tests)' # a tool named: a call `impact(`, the Claude Code name mcp__plugin_axiomcode_axiomcode__impact, MCP `impact`, or a command # in backticks (`axiomcode impact x`); `path:line: …` is an answer's shape, not the tool MENTION = re.compile(r'`(?:axiomcode )?' + VERB + r'(?:[ \t][^`\n]*)?`|\bmcp__plugin_axiomcode_axiomcode__' + VERB + r'\b' @@ -130,7 +130,7 @@ def controls(tools): got = len(mismatches(documented(text), fake, 'control')) if got != want: bad.append(f"control {text!r}: {got} mismatch(es), want {want}") - if set(tools) != {'impact', 'path', 'tests'}: + if set(tools) != {'context', 'impact', 'link', 'path', 'tests'}: bad.append(f"the server listed {sorted(tools)}; the MCP tools were not read") return bad