@erclx/aitk 0.60.0 → 0.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aitk",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "0.60.0",
4
+ "version": "0.61.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -37,6 +37,8 @@ The roadmap is optional and this skill does not require it. It carries why a seq
37
37
 
38
38
  A compaction is a moment this skill cannot detect, so the human asks for each side of it and this skill reads the matching runbook when they do. On a request to write the handoff or save the session, read `${CLAUDE_SKILL_DIR}/references/orchestrator-handoff.md` and follow it. It writes `.claude/tasks/session.md` with the state of play, the decisions taken under delegated authority, the mistakes worth not repeating, and the standing cautions. On a request to resume after a compaction, read `${CLAUDE_SKILL_DIR}/references/orchestrator-resume.md`, which reads that file back with the board and the groundwork behind the live work. Write nothing to the handoff that the board, a task file, or a groundwork folder already carries.
39
39
 
40
+ The review trigger takes the same shape. `references/orchestrator-poll.md` holds the loop prompt and the condition under which the poll runs, and `scripts/poll.sh` is what the prompt invokes. Nothing starts or stops that loop on its own, so the condition holds only while a session applies it.
41
+
40
42
  That routing lives in this body and this skill is user-invoked, so a session that has dropped the body routes nothing and the request lands as ordinary conversation. Approaching a compaction is when a long session is likeliest to have dropped it, which is the same moment the handoff exists for. Re-invoke `/aitk:claude-orchestrate` first whenever the session has run long or the ask goes unanswered. The two runbooks sit at `references/orchestrator-handoff.md` and `references/orchestrator-resume.md` inside this skill's own folder, so a person who knows their plugin root opens either one directly and follows it without this skill loaded at all.
41
43
 
42
44
  ## Output
@@ -96,7 +98,7 @@ Write no shape for a correction. A correction is a sentence, and a format for ad
96
98
  3. Decide parallelism and merge order. Note which plans touch a shared wiring seam so their PRs merge in sequence, not at once.
97
99
  4. Verify the plan against the tree. Reading it is not enough, since a plan goes stale from whatever merged after it was written. Grep for each construct it names and count the sites against the count it claims. Check that every phase label it cites is still open. Open each file it describes rather than trusting its account of the contents. Correct the plan before handing it over.
98
100
  5. Hand off. The human opens a worker worktree with `claude-worktree` and runs `claude-autoship` against the plan. The orchestrator does not spawn workers.
99
- 6. Review the PR. When a worker opens a PR, run `claude-pr-review` to post findings to it. This is the deep, independent pass. The worker's autoship self-review was only the green gate.
101
+ 6. Review the PR. When a worker opens a PR, run `claude-pr-review` to post findings to it. This is the deep, independent pass. The worker's autoship self-review was only the green gate. Learning that a PR moved is the mechanical half, so read `${CLAUDE_SKILL_DIR}/references/orchestrator-poll.md` and start the poll it carries on the first dispatch rather than checking the board by hand. It routes a moved or answered pull request straight to a re-review and reports an opened one without acting, which keeps every first pass a batched judgment this session triggers.
100
102
  7. Close the loop. After the worker runs `claude-address-review`, re-review if needed, then the human merges. Tell the trailing worker to rebase when its branch shares a seam with the merged one.
101
103
 
102
104
  ## Boundaries
@@ -0,0 +1,44 @@
1
+ ---
2
+ title: Orchestrator poll runbook
3
+ description: The review trigger, the condition under which it runs, and how to read what it reports
4
+ ---
5
+
6
+ Run the orchestrator's review trigger. The poll reports pull request movement and the session acts on what it reports. It reads only, and it never starts a first-pass review.
7
+
8
+ `${CLAUDE_SKILL_DIR}/scripts/poll.sh` is the script. It needs `gh` authenticated against the remote and `jq` on the path, and it reads the base branch from `origin/HEAD` rather than assuming a name.
9
+
10
+ ## When to run it
11
+
12
+ Start the poll on a dispatch and stop it when the last pull request merges with nothing else out. An open pull request or a dispatched worker is the condition, and both resolve from the board plus `gh pr list` without asking the operator. A release pull request alone does not qualify, since its sweep carries no findings.
13
+
14
+ Nothing enforces this. No hook starts the poll and no check stops it, so the condition holds only while a session applies it. Left always-on it fires into an empty board through every gap between a dispatch and its push, which leaves stopping it to the operator.
15
+
16
+ The poll is session-scoped and dies with the session that started it. Restart it after a compaction, and take the prompt from this file rather than from a transcript, since a running loop holds whatever wording it was started with and a correction here does not reach it.
17
+
18
+ ## The prompt
19
+
20
+ Resolve `${CLAUDE_SKILL_DIR}/scripts/poll.sh` to an absolute path and paste that in place of `<POLL_SCRIPT>` below. The variable expands while this runbook renders and not in a `/loop` turn, which arrives as a standalone prompt, so a block carrying the variable reaches the session as a literal string and the run improvises a substitute.
21
+
22
+ ```plaintext
23
+ /loop 3m Poll GitHub for pull request movement by running <POLL_SCRIPT>, then act on what it reports.
24
+
25
+ - MOVED or RESPONSE on a pull request I have already reviewed: run the aitk:claude-pr-review skill on it immediately, narrow pass. Re-reviews read prior..head and gain nothing from waiting.
26
+ - OPENED, or a pull request with no prior review pass: report it and stop. First passes wait for the operator, because reading several together is what surfaces cross-PR findings.
27
+ - SEEN: report it and stop. A pass already covers that head, whether it arrived out of band or before the poll first saw the pull request, so no review follows.
28
+ - CONFLICT: report it and stop. The branch owner rebases, not this session.
29
+ - GONE: report it, then sweep the board by invoking the aitk:claude-orchestrate skill and following its queue-refill sweep.
30
+ - A line starting `poll:`: report it verbatim and treat that pull request as unread this run. It is a failed query, not a state.
31
+ - Nothing changed: say exactly "No movement." and nothing else.
32
+
33
+ Never start a first-pass review on your own.
34
+ ```
35
+
36
+ ## Reading the output
37
+
38
+ Every classification line names a pull request and a state. A line starting `poll:` is not a classification. It means a query failed and the script declined to guess, so that pull request keeps its last known state and is neither reported as moved nor swept as merged. Treat it as unread and let the next run classify it.
39
+
40
+ The script exits non-zero and classifies nothing when the open pull request list itself fails to load. That case would otherwise report every tracked pull request as merged, so the baseline is left untouched and the run says so.
41
+
42
+ The baseline lives at `.claude/.tmp/pr-poll/baseline.txt` under the main worktree root and is per-machine. A first run against a board already in flight reports each open pull request once before it settles.
43
+
44
+ The four review headings the script matches are written by `claude-pr-review` and `claude-address-review`. A project that posts its reviews under different headings edits the two jq filters in the script to match, or every pull request reads as never reviewed.
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env bash
2
+ # Reports pull request movement since the last run. Reads only.
3
+ #
4
+ # The shebang is load-bearing. An earlier version ran under the operator's zsh,
5
+ # where an unquoted parameter expansion does not word-split, so `set -- $line`
6
+ # left every field but the first empty and every head compared unequal. The
7
+ # poll reported movement that had not happened, which is the one failure that
8
+ # makes a detection tool worth less than no tool.
9
+ set -e
10
+ set -o pipefail
11
+
12
+ # The baseline is per-machine mutable state, so it stays in gitignored scratch
13
+ # even though the script is tracked. Resolving the main worktree root rather
14
+ # than this file's own folder keeps a poll started from a linked worktree
15
+ # reading the baseline a poll started from main wrote.
16
+ MAIN_ROOT="$(git worktree list --porcelain 2>/dev/null | awk '/^worktree /{print $2; exit}')"
17
+ if [ -z "$MAIN_ROOT" ]; then
18
+ echo "poll: not a git repository, so nothing is classified" >&2
19
+ exit 1
20
+ fi
21
+ STATE_DIR="$MAIN_ROOT/.claude/.tmp/pr-poll"
22
+ mkdir -p "$STATE_DIR"
23
+ STATE="$STATE_DIR/baseline.txt"
24
+ touch "$STATE"
25
+
26
+ # The base branch is read rather than assumed, since this ships to projects that
27
+ # do not all call it `main`. A wrong base is not a visible failure: merge-tree
28
+ # reports every pull request as conflicted against a ref that does not resolve.
29
+ BASE_REF="$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null || true)"
30
+ if [ -z "$BASE_REF" ]; then
31
+ BASE_REF="origin/$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name 2>/dev/null || echo main)"
32
+ fi
33
+ BASE_BRANCH="${BASE_REF#origin/}"
34
+
35
+ # These four strings are owned elsewhere and pinned here. `claude-pr-review`
36
+ # writes `## Review` and `## Review closed`, and `claude-address-review` writes
37
+ # `## Review response` and `## Rebase`. All three surfaces ship separately, so a
38
+ # heading added in either skill breaks a test here that no check reaches across.
39
+ #
40
+ # Both families match on the first line alone so the two tests stay symmetric.
41
+ # The reply family carries `## Rebase` because a run sent straight to the rebase
42
+ # step posts under a heading deliberately kept outside the `## Review` family.
43
+ # Widening one family without the other is what left the reply test narrow, so
44
+ # a fifth heading is added here beside its sibling.
45
+ JQ_LAST_REVIEWED_HEAD='
46
+ [ .reviews[]
47
+ | select((.body // "") | split("\n")[0] | rtrimstr("\r")
48
+ | . == "## Review" or . == "## Review closed")
49
+ ] | last | .commit.oid // empty
50
+ '
51
+ JQ_REPLY_COUNT='
52
+ [ .comments[]
53
+ | select((.body // "") | split("\n")[0] | rtrimstr("\r")
54
+ | . == "## Review response" or . == "## Rebase")
55
+ ] | length
56
+ '
57
+
58
+ # A pull request this run could not read keeps the line it had, so the GONE
59
+ # sweep below does not read the gap as a merge and the baseline does not lose
60
+ # the head it already knew. Saying so on stderr is the point: a silent skip is
61
+ # how the failure this script was fixed for went unnoticed for a session.
62
+ carry_forward() {
63
+ local n=$1 reason=$2 old
64
+ old=$(grep "^$n " "$STATE" || true)
65
+ if [ -n "$old" ]; then
66
+ echo "$old"
67
+ echo "poll: #$n $reason, so it keeps its last known state and goes unclassified" >&2
68
+ else
69
+ echo "poll: #$n $reason, and it has no last known state, so it goes unclassified" >&2
70
+ fi
71
+ }
72
+
73
+ snapshot() {
74
+ local numbers n payload head prior resp merges
75
+ git fetch -q origin "$BASE_BRANCH" 2>/dev/null || true
76
+
77
+ # A failed list reaches the caller as no open pull requests, and that reports
78
+ # every tracked one as GONE. It is a louder wrong answer than the one this
79
+ # script was fixed for, so the run aborts rather than classify on it.
80
+ if ! numbers=$(gh pr list --state open --json number --jq '.[].number' 2>/dev/null); then
81
+ echo "poll: the open pull request list could not be read" >&2
82
+ return 1
83
+ fi
84
+
85
+ for n in $numbers; do
86
+ # A query that failed and a pull request with genuinely no reviews both
87
+ # arrive as an empty result. Reading the first as the second reported a
88
+ # reviewed pull request as never reviewed, which routes the re-review to
89
+ # the first-pass branch, where it reports and stops, so the movement sits
90
+ # unreviewed until a person notices. One query per pull request gives that
91
+ # failure a single place to surface.
92
+ if ! payload=$(gh pr view "$n" --json headRefOid,reviews,comments 2>/dev/null); then
93
+ carry_forward "$n" "could not be read"
94
+ continue
95
+ fi
96
+
97
+ head=$(jq -r '.headRefOid // empty' <<<"$payload")
98
+ if [ -z "$head" ]; then
99
+ carry_forward "$n" "returned no head"
100
+ continue
101
+ fi
102
+
103
+ prior=$(jq -r "$JQ_LAST_REVIEWED_HEAD" <<<"$payload")
104
+ resp=$(jq -r "$JQ_REPLY_COUNT" <<<"$payload")
105
+
106
+ # `gh pr view --json mergeable` reports UNKNOWN until GitHub finishes
107
+ # computing it, which is exactly when a poll asks. merge-tree answers
108
+ # locally against the base this machine has, so it never returns UNKNOWN.
109
+ git fetch -q origin "pull/$n/head" 2>/dev/null || true
110
+ if ! git cat-file -e "${head}^{commit}" 2>/dev/null ||
111
+ ! git rev-parse --verify -q "$BASE_REF" >/dev/null; then
112
+ # merge-tree exits non-zero on a ref it cannot resolve as well as on a
113
+ # real conflict, and the two are indistinguishable from its status. Both
114
+ # sides are checked, because an unresolvable base reports every pull
115
+ # request as conflicted rather than one, which is the louder half.
116
+ merges=unknown
117
+ elif git merge-tree --write-tree "$BASE_REF" "$head" >/dev/null 2>&1; then
118
+ merges=clean
119
+ else
120
+ merges=conflict
121
+ fi
122
+
123
+ echo "$n $head ${prior:-none} $resp $merges"
124
+ done
125
+ }
126
+
127
+ # The reason is printed by whichever branch failed, since this one cannot tell
128
+ # a list query from a parse and naming either would misattribute the other.
129
+ if ! NEW=$(snapshot); then
130
+ echo "poll: nothing is classified this run and the baseline is unchanged" >&2
131
+ exit 1
132
+ fi
133
+ CHANGED=0
134
+
135
+ while read -r n head prior resp merges; do
136
+ [ -z "$n" ] && continue
137
+ old=$(grep "^$n " "$STATE" || true)
138
+ if [ -z "$old" ]; then
139
+ # A pull request first seen here may already carry a pass, when it opened
140
+ # and was reviewed between two runs. Reporting it as new invites a first
141
+ # pass the thread already has.
142
+ if [ "$prior" = "$head" ]; then
143
+ echo "SEEN #$n at ${head:0:7}, first sighting, already covered by a pass"
144
+ elif [ "$merges" = unknown ]; then
145
+ # `unknown` means no merge was attempted, so it is left off the line
146
+ # rather than printed where a verdict belongs.
147
+ echo "OPENED #$n at ${head:0:7}"
148
+ else
149
+ echo "OPENED #$n at ${head:0:7}, $merges against $BASE_BRANCH"
150
+ fi
151
+ CHANGED=1
152
+ continue
153
+ fi
154
+ old_head=$(echo "$old" | cut -d' ' -f2)
155
+ old_resp=$(echo "$old" | cut -d' ' -f4)
156
+ old_merges=$(echo "$old" | cut -d' ' -f5)
157
+
158
+ # A conflict arrives from the base moving, not from the branch, so it is
159
+ # reported on the transition rather than only when the head changes.
160
+ if [ "$merges" = conflict ] && [ "$old_merges" != conflict ]; then
161
+ echo "CONFLICT #$n no longer merges into $BASE_BRANCH"
162
+ CHANGED=1
163
+ fi
164
+
165
+ if [ "$head" != "$old_head" ]; then
166
+ if [ "$prior" = "none" ]; then
167
+ echo "MOVED #$n -> ${head:0:7}, never reviewed"
168
+ elif [ "$prior" = "$head" ]; then
169
+ # An out-of-band pass reviewed this head before the poll saw it move, so
170
+ # the range is empty because it is covered rather than because it broke.
171
+ # Without this the force-push branch below claims a rewrite that never
172
+ # happened, which is the class of false report the shebang note names.
173
+ echo "SEEN #$n -> ${head:0:7}, already covered by the last pass"
174
+ else
175
+ # The range needs both commits local, and a force-push leaves the prior
176
+ # one unreachable. Count what resolves and say nothing when it does not,
177
+ # rather than reporting a zero that reads as no new work.
178
+ git fetch -q origin "pull/$n/head" 2>/dev/null || true
179
+ since=$(git log --oneline "${prior}..${head}" 2>/dev/null | wc -l | tr -d ' ' || true)
180
+ if [ -n "$since" ] && [ "$since" != "0" ]; then
181
+ echo "MOVED #$n -> ${head:0:7}, $since commit(s) since your last pass"
182
+ else
183
+ echo "MOVED #$n -> ${head:0:7}, range unresolved, likely force-pushed"
184
+ fi
185
+ fi
186
+ CHANGED=1
187
+ elif [ "$resp" -gt "$old_resp" ]; then
188
+ echo "RESPONSE #$n answered with no new commit"
189
+ CHANGED=1
190
+ fi
191
+ done <<<"$NEW"
192
+
193
+ while read -r n _rest; do
194
+ [ -z "$n" ] && continue
195
+ echo "$NEW" | grep -q "^$n " || {
196
+ echo "GONE #$n merged or closed"
197
+ CHANGED=1
198
+ }
199
+ done <"$STATE"
200
+
201
+ [ "$CHANGED" -eq 0 ] && echo "No movement."
202
+ echo "$NEW" >"$STATE"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/aitk",
3
3
  "type": "module",
4
- "version": "0.60.0",
4
+ "version": "0.61.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -27,9 +27,9 @@
27
27
  },
28
28
  "scripts": {
29
29
  "check:spell": "cspell '**' '.*/**' '.*' --no-progress --color --show-context",
30
- "format": "prettier --write --log-level warn --ignore-path .gitignore --ignore-path .prettierignore . && shfmt --write --indent 2 scripts/ tooling/",
31
- "check:format": "prettier --check --log-level warn --ignore-path .gitignore --ignore-path .prettierignore . && shfmt --diff --indent 2 scripts/ tooling/",
32
- "check:shell": "find scripts tooling .claude/hooks -name '*.sh' -exec shellcheck --severity=warning {} +",
30
+ "format": "prettier --write --log-level warn --ignore-path .gitignore --ignore-path .prettierignore . && shfmt --write --indent 2 scripts/ tooling/ claude/",
31
+ "check:format": "prettier --check --log-level warn --ignore-path .gitignore --ignore-path .prettierignore . && shfmt --diff --indent 2 scripts/ tooling/ claude/",
32
+ "check:shell": "find scripts tooling claude .claude/hooks -name '*.sh' -exec shellcheck --severity=warning {} +",
33
33
  "check:types": "tsc --noEmit",
34
34
  "check:install": "./scripts/core/install-check.sh",
35
35
  "test": "bun --bun vitest run",