@erclx/aitk 0.96.0 → 0.97.1

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.96.0",
4
+ "version": "0.97.1",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -109,7 +109,7 @@ Write no shape for a correction. A correction is a sentence, and a format for ad
109
109
  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.
110
110
  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.
111
111
  - 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. That runbook holds the routing, and a summary of it here is a second source that drifts from it.
112
- 7. Dispatch the handback. A pass posting a critical or should-fix finding tells the session holding that branch to run `claude-address-review`, rather than waiting for a person to relay it. Re-review when the answer lands, then the human merges. Tell the trailing worker to rebase when its branch shares a seam with the merged one.
112
+ 7. Dispatch the handback. A pass posting a finding at any severity tells the session holding that branch to run `claude-address-review`, rather than waiting for a person to relay it. Read that off the counts on the summary line rather than off the heading, since a minors-only pass closes the heading and still owes a dispatch. Re-review when the answer lands, then the human merges. Tell the trailing worker to rebase when its branch shares a seam with the merged one.
113
113
  - Send only above that floor. A minor posts under the same heading and is visibility alone, so a rule keyed on any open finding sends a worker to act on a note
114
114
  - Resolve the target from a fresh session listing at the moment of sending, never from a mapping written down earlier, since names rotate as sessions end and one recorded earlier in a session has failed inside the hour
115
115
  - Open the message with the worktree and branch the sender believes the reader holds, asking to be corrected, whenever that mapping is inferred rather than confirmed
@@ -34,12 +34,13 @@ Poll GitHub for pull request movement by running <POLL_SCRIPT>, then act on what
34
34
  - 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.
35
35
  - OPENED, or a pull request with no prior review pass: run the aitk:claude-pr-review skill on it. A draft counts, since every pull request here opens as one and skipping drafts skips everything.
36
36
  - 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.
37
+ - STALLED: read the last pass and report what it carried. The pass has sat open for hours with nothing following it, so a worker mid-task is already ruled out and the dispatch either never went out or the session holding it is gone. Confirm and re-send it under the dispatch rule below. A pass carrying only minors is also under the wrong heading, so say so, and dispatch it either way. Do not re-run a review to correct the heading, since a pass on an unchanged head with no response behind it stops by design.
37
38
  - CONFLICT: report it and stop. The branch owner rebases, not this session.
38
39
  - GONE: report it, then sweep the board by invoking the aitk:claude-orchestrate skill and following its queue-refill sweep.
39
40
  - A line starting `poll:`: report it verbatim and treat that pull request as unread this run. It is a failed query, not a state.
40
41
  - Nothing changed: say exactly "No movement." and nothing else.
41
42
 
42
- After any pass that posts a critical or should-fix finding, tell the session holding that branch to run the aitk:claude-address-review skill. Resolve the target by running `aitk sessions list --branch <branch> --json` at that moment, which scopes the match to this repository, then route on how many sessions it returned. Zero: report the invocation for me and dispatch nobody. Exactly one, with the confidence field reading "confirmed": address that name directly. Any other count, any other confidence, or a command that is missing or refuses: fall back to picking from a session listing, open by naming the worktree and branch you believe the reader holds, and ask to be corrected. Two sessions can hold one branch, so read the count rather than the first row. A pass carrying only minor findings dispatches nobody.
43
+ After any pass that posts a finding, at any severity, tell the session holding that branch to run the aitk:claude-address-review skill. Resolve the target by running `aitk sessions list --branch <branch> --json` at that moment, which scopes the match to this repository, then route on how many sessions it returned. Zero: report the invocation for me and dispatch nobody. Exactly one, with the confidence field reading "confirmed": address that name directly. Any other count, any other confidence, or a command that is missing or refuses: fall back to picking from a session listing, open by naming the worktree and branch you believe the reader holds, and ask to be corrected. Two sessions can hold one branch, so read the count rather than the first row. The heading does not decide this: a minors-only pass posts the closed heading and still owes a dispatch, so read the counts on the summary line rather than the heading above them.
43
44
  ```
44
45
 
45
46
  ## Reading the output
@@ -50,4 +51,8 @@ The script exits non-zero and classifies nothing when the open pull request list
50
51
 
51
52
  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.
52
53
 
53
- 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.
54
+ 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 three jq filters in the script to match, or every pull request reads as never reviewed.
55
+
56
+ `STALLED` is the one state the script derives from a heading rather than from a commit or a count, since `claude-pr-review` posts the open heading only for a pass carrying a critical or a should-fix. The heading alone cannot carry it, because an open pass means a dispatch was owed and made, so the ordinary healthy thread is a worker still working. The age of that pass is the third test: the state fires when the open pass covers the head, nothing has followed it, and it is older than the `STALE_AFTER` seconds set at the top of the script. It reports once per entry and fires again after any commit or reply resets the thread. A project whose workers run longer than the default two hours raises that number.
57
+
58
+ What the state does not reach is a dispatch owed on a minor. The dispatch fires on a finding at any severity and the heading closes on a blocking one, so a minors-only pass owes a worker under a closed heading, and this state reads the open heading alone. It therefore catches a stalled blocking finding and never a stalled minor. That is a narrowing rather than a defect, and closing it would mean reading the counts off the summary line, which pins a second string this script does not own.
@@ -55,6 +55,44 @@ JQ_REPLY_COUNT='
55
55
  ] | length
56
56
  '
57
57
 
58
+ # `claude-pr-review` posts `## Review` only for a pass carrying a critical or a
59
+ # should-fix, so the heading of the last review is what says whether anything
60
+ # blocks the merge. Taking it as well as the commit is what separates a thread
61
+ # waiting on a worker from one a pass left open owing no dispatch. A project
62
+ # editing the two filters above for its own headings edits this one with them.
63
+ #
64
+ # The age of that review comes out of the same selection, because the heading
65
+ # alone cannot separate the two. Under the rule above an open heading means a
66
+ # dispatch was owed and made, so the ordinary healthy thread is a worker still
67
+ # working and every one of them would be reported minutes after the pass posted.
68
+ # jq computes the elapsed seconds itself, since `date -d` is GNU-only and this
69
+ # script runs wherever the plugin is installed. A review carrying no stamp reads
70
+ # as age zero and classifies nothing, which is the same answer the carry-forward
71
+ # path gives a pull request this run could not read.
72
+ JQ_LAST_REVIEW_STATE='
73
+ [ .reviews[]
74
+ | select((.body // "") | split("\n")[0] | rtrimstr("\r")
75
+ | . == "## Review" or . == "## Review closed")
76
+ ] | last
77
+ | if . == null then "none 0"
78
+ else ((.body | split("\n")[0] | rtrimstr("\r")
79
+ | if . == "## Review" then "open" else "closed" end)
80
+ + " "
81
+ + (if .submittedAt == null then "0"
82
+ else ((now - (.submittedAt | fromdateiso8601)) | floor | tostring)
83
+ end))
84
+ end
85
+ '
86
+
87
+ # An open pass this old has nobody on it. The cycle it has to clear is a review
88
+ # landing and a worker pushing a follow-up, measured between ten and thirty
89
+ # minutes across a day of runs on 2026-08-14, so two hours sits about four times
90
+ # past the slowest observed and still well inside a working session. Shorter
91
+ # starts catching the slow healthy case this threshold exists to exclude, and
92
+ # longer lets a dead dispatch sit past the point a person would have noticed.
93
+ # A project whose workers run longer than that raises it here.
94
+ STALE_AFTER=7200
95
+
58
96
  # A pull request this run could not read keeps the line it had, so the GONE
59
97
  # sweep below does not read the gap as a merge and the baseline does not lose
60
98
  # the head it already knew. Saying so on stderr is the point: a silent skip is
@@ -63,7 +101,14 @@ carry_forward() {
63
101
  local n=$1 reason=$2 old
64
102
  old=$(grep "^$n " "$STATE" || true)
65
103
  if [ -n "$old" ]; then
66
- echo "$old"
104
+ # Every other classification compares a field against itself on a carried
105
+ # line, so it fires nothing. STALLED reads a heading that is carried too, so
106
+ # the field is blanked to a value no branch matches. Echoing it intact would
107
+ # classify a pull request this run never reached while stderr below says the
108
+ # opposite, and one successful read restores it a run later. An already
109
+ # reported thread keeps its marker, or an unreadable run would let the same
110
+ # thread be reported a second time.
111
+ echo "$old" | awk '{ if ($6 != "reported") $6 = "carried"; print }'
67
112
  echo "poll: #$n $reason, so it keeps its last known state and goes unclassified" >&2
68
113
  else
69
114
  echo "poll: #$n $reason, and it has no last known state, so it goes unclassified" >&2
@@ -71,7 +116,7 @@ carry_forward() {
71
116
  }
72
117
 
73
118
  snapshot() {
74
- local numbers n payload head prior resp merges
119
+ local numbers n payload head prior resp merges review_state
75
120
  git fetch -q origin "$BASE_BRANCH" 2>/dev/null || true
76
121
 
77
122
  # A failed list reaches the caller as no open pull requests, and that reports
@@ -102,6 +147,9 @@ snapshot() {
102
147
 
103
148
  prior=$(jq -r "$JQ_LAST_REVIEWED_HEAD" <<<"$payload")
104
149
  resp=$(jq -r "$JQ_REPLY_COUNT" <<<"$payload")
150
+ # Two space-separated fields, so the line below carries them as its own
151
+ # sixth and seventh rather than needing a split.
152
+ review_state=$(jq -r "$JQ_LAST_REVIEW_STATE" <<<"$payload")
105
153
 
106
154
  # `gh pr view --json mergeable` reports UNKNOWN until GitHub finishes
107
155
  # computing it, which is exactly when a poll asks. merge-tree answers
@@ -120,7 +168,7 @@ snapshot() {
120
168
  merges=conflict
121
169
  fi
122
170
 
123
- echo "$n $head ${prior:-none} $resp $merges"
171
+ echo "$n $head ${prior:-none} $resp $merges $review_state"
124
172
  done
125
173
  }
126
174
 
@@ -131,9 +179,15 @@ if ! NEW=$(snapshot); then
131
179
  exit 1
132
180
  fi
133
181
  CHANGED=0
182
+ # The baseline is written from here rather than from the snapshot, because the
183
+ # heading field is the one column a classification rewrites. STALLED reports a
184
+ # standing condition rather than a transition, so without a written marker it
185
+ # would fire on every later run and the board would never read "No movement."
186
+ FINAL=""
134
187
 
135
- while read -r n head prior resp merges; do
188
+ while read -r n head prior resp merges heading age; do
136
189
  [ -z "$n" ] && continue
190
+ state=$heading
137
191
  old=$(grep "^$n " "$STATE" || true)
138
192
  if [ -z "$old" ]; then
139
193
  # A pull request first seen here may already carry a pass, when it opened
@@ -149,11 +203,17 @@ while read -r n head prior resp merges; do
149
203
  echo "OPENED #$n at ${head:0:7}, $merges against $BASE_BRANCH"
150
204
  fi
151
205
  CHANGED=1
206
+ FINAL+="$n $head $prior $resp $merges $state $age"$'\n'
152
207
  continue
153
208
  fi
154
209
  old_head=$(echo "$old" | cut -d' ' -f2)
155
210
  old_resp=$(echo "$old" | cut -d' ' -f4)
156
211
  old_merges=$(echo "$old" | cut -d' ' -f5)
212
+ # Only the marker is read off this field, so a baseline written before it
213
+ # existed yields empty and reads as not yet reported, which is what it is. The
214
+ # age decides the rest, so the first run after an upgrade needs no history and
215
+ # classifies a thread already past the threshold rather than waiting a run.
216
+ old_heading=$(echo "$old" | cut -d' ' -f6)
157
217
 
158
218
  # A conflict arrives from the base moving, not from the branch, so it is
159
219
  # reported on the transition rather than only when the head changes.
@@ -187,7 +247,26 @@ while read -r n head prior resp merges; do
187
247
  elif [ "$resp" -gt "$old_resp" ]; then
188
248
  echo "RESPONSE #$n answered with no new commit"
189
249
  CHANGED=1
250
+ elif [ "$heading" = open ] && [ "$old_heading" != reported ] &&
251
+ [ "$prior" = "$head" ] && [ "$age" -ge "$STALE_AFTER" ]; then
252
+ # Three things at once: the last pass is open, it covers the head so no
253
+ # commit followed it, and the two branches above found no reply either. The
254
+ # open heading is posted only for a critical or should-fix, so a pass owing
255
+ # no dispatch left it here. Any two of these describe an ordinary review
256
+ # waiting on a worker, which is why the age carries the third: without it
257
+ # every dispatched worker is reported minutes into the work it was sent to
258
+ # do, and a signal firing on the healthy path is one an operator learns to
259
+ # skip. The report still asks for a confirmation rather than asserting.
260
+ echo "STALLED #$n open at ${head:0:7}, no commit or reply in $((age / 3600))h"
261
+ state=reported
262
+ CHANGED=1
263
+ elif [ "$heading" = open ] && [ "$old_heading" = reported ]; then
264
+ # Nothing above fired, so the thread has not moved since it was reported.
265
+ # Carry the marker rather than the freshly derived heading, or the state
266
+ # re-enters next run and STALLED oscillates instead of reporting once.
267
+ state=reported
190
268
  fi
269
+ FINAL+="$n $head $prior $resp $merges $state"$'\n'
191
270
  done <<<"$NEW"
192
271
 
193
272
  while read -r n _rest; do
@@ -199,4 +278,4 @@ while read -r n _rest; do
199
278
  done <"$STATE"
200
279
 
201
280
  [ "$CHANGED" -eq 0 ] && echo "No movement."
202
- echo "$NEW" >"$STATE"
281
+ printf '%s' "$FINAL" >"$STATE"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: claude-pr-review
3
- description: Reviews an open pull request from an independent session and posts findings as a review comment on the PR. Posts a first pass against the whole change and every later pass against only the commits added since, under `## Review` while anything is open and `## Review closed` once nothing is. Reads project docs and the roadmap for cross-feature context a self-review lacks. Use when asked to "review the PR", "review this feature's PR", "post a PR review", "re-review the PR", "close out the review", "confirm the findings are fixed", or acting as the orchestrator reviewing a worker's PR. Do NOT use to review local uncommitted changes. That is `claude-review`.
3
+ description: Reviews an open pull request from an independent session and posts findings as a review comment on the PR. Posts a first pass against the whole change and every later pass against only the commits added since, under `## Review` while a critical or should-fix is open and `## Review closed` once nothing blocks the merge. Reads project docs and the roadmap for cross-feature context a self-review lacks. Use when asked to "review the PR", "review this feature's PR", "post a PR review", "re-review the PR", "close out the review", "confirm the findings are fixed", or acting as the orchestrator reviewing a worker's PR. Do NOT use to review local uncommitted changes. That is `claude-review`.
4
4
  ---
5
5
 
6
6
  # Claude PR review
@@ -13,10 +13,10 @@ findings to the PR, so the vantage is independent and the output is durable.
13
13
  It posts at least twice over a pull request's life. A first pass opens the
14
14
  review against the whole change, and every later pass reads only the commits
15
15
  added since. The heading reports state rather than pass number: a pass carrying
16
- findings takes `## Review`, and `## Review closed` is reserved for a pass
17
- carrying none, so the most recent comment's heading reports where the review
18
- stands. Every pass is this skill, and which one it is gets detected from the
19
- thread rather than named by the caller.
16
+ a critical or a should-fix takes `## Review`, and `## Review closed` covers
17
+ every other pass, so the most recent comment's heading reports whether anything
18
+ blocks the merge. Every pass is this skill, and which one it is gets detected
19
+ from the thread rather than named by the caller.
20
20
 
21
21
  ## Guards
22
22
 
@@ -85,9 +85,9 @@ Review the diff and files for the same axes as `claude-review` (bugs, edge cases
85
85
 
86
86
  Apply the high-signal filter: flag only what will cause incorrect behavior, break a documented rule, or mislead a downstream feature. If uncertain, do not flag.
87
87
 
88
- A later pass applies the same axes to the delta, and adds one check the first pass cannot make: did each prior finding land, and did the fix regress anything it touched. Findings of its own are normal findings, stated at the same severity and counted the same way. That count is what Step 4 reads to pick the heading, so a pass raising one of its own is not a close-out.
88
+ A later pass applies the same axes to the delta, and adds one check the first pass cannot make: did each prior finding land, and did the fix regress anything it touched. Findings of its own are normal findings, stated at the same severity and counted the same way. That count is what Step 4 reads to pick the heading, so a pass raising a critical or a should-fix of its own is not a close-out.
89
89
 
90
- A prior finding can also be settled by argument rather than by a fix. A reply naming the plan question that already declined it, or a constraint this session could not see, withdraws the finding or moves its grade. State that outcome in the body under the finding it changes, naming the fact that produced it, whether the argument arrived on the thread or through the channel that carried the dispatch. Dropping the finding from this body instead leaves a reader unable to tell a withdrawal from an oversight, and the reasoning goes with the session that heard it. A withdrawal removes the finding from the count, so a pass that withdrew every open item is a close-out. Write the withdrawal and its cause into that body rather than taking the short close-out line Step 4 supplies, which reports prior findings addressed and would credit a fix nobody made.
90
+ A prior finding can also be settled by argument rather than by a fix. A reply naming the plan question that already declined it, or a constraint this session could not see, withdraws the finding or moves its grade. State that outcome in the body under the finding it changes, naming the fact that produced it, whether the argument arrived on the thread or through the channel that carried the dispatch. Dropping the finding from this body instead leaves a reader unable to tell a withdrawal from an oversight, and the reasoning goes with the session that heard it. A withdrawal removes the finding from the count, so a pass that withdrew every critical and should-fix is a close-out. Write the withdrawal and its cause into that body rather than taking the short close-out line Step 4 supplies, which reports prior findings addressed and would credit a fix nobody made.
91
91
 
92
92
  Use severity: `critical` (blocks merge), `should-fix` (fix before merge), `minor` (visibility only).
93
93
 
@@ -144,9 +144,15 @@ Re-reviewed `<short-sha>`, N commits since the prior pass. X critical, Y should-
144
144
  🤖 Reviewed by Claude Code
145
145
  ```
146
146
 
147
- The heading reports whether anything is open and the summary line reports which pass this is. Post under `## Review` whenever the pass carries a finding at any severity, whether it is the first pass or the fourth. Reserve `## Review closed` for a pass carrying none, which makes it the marker worth scanning for rather than a label on a kind of pass. A pull request thread then reads as `## Review`, the worker's answer under `## Review response` from `claude-address-review`, another `## Review` while anything is still open, and `## Review closed` when nothing is.
147
+ The heading reports whether anything blocks the merge and the summary line reports which pass this is. Post under `## Review` whenever the pass carries a critical or a should-fix, whether it is the first pass or the fourth. Post `## Review closed` on every other pass, a pass carrying only minors included, and list those minors under it as follow-ups. A pull request thread then reads as `## Review`, the worker's answer under `## Review response` from `claude-address-review`, another `## Review` while a critical or should-fix is still open, and `## Review closed` when neither is.
148
148
 
149
- Read the state off the most recent review comment rather than off the presence of a closed one. A close-out does not close the pull request, so a commit pushed after it gets its own pass, and that pass reopens the review under `## Review` when it raises anything.
149
+ The heading and the dispatch answer two different questions, so a close-out does not mean the pass owes nobody. The heading says whether anything blocks the merge and the dispatch says whether any work is owed, and a pass carrying only minors closes the heading while still sending the session holding the branch. Keying the dispatch on the grade too was measured wrong: across 8 findings on one archived pass, 3 were posted as minor and 2 of those were defects a worker fixed rather than recorded, so a floor at should-fix loses real fixes to a grade that runs low.
150
+
151
+ The cost is that a clean thread no longer reads off the heading alone, since a close-out may carry minors. Take the merge decision from the heading and the counts from the summary line under it, which is where they already sit.
152
+
153
+ A minor the dispatched worker declines is what needs a surface that survives the merge, rather than every minor, since one that gets fixed on the branch needs no durable record. Write a declined minor into the `## Findings` section of the task the branch closes, which is where the queue-refill sweep already routes a finding that changes another task. A declined finding left on the thread alone is lost the moment the pull request merges.
154
+
155
+ Read the state off the most recent review comment rather than off the presence of a closed one. A close-out does not close the pull request, so a commit pushed after it gets its own pass, and that pass reopens the review under `## Review` when it raises a critical or a should-fix.
150
156
 
151
157
  Both of this skill's headings anchor as a section distinct from human threads. Do not invent one beyond those two and the `## Review response` a sibling owns, and do not append the PR number, which GitHub already renders above the comment.
152
158
 
@@ -166,11 +172,25 @@ Before posting, run the scan in `.claude/standards/publish.md` against the body,
166
172
  gh pr review <number> --comment --body-file .claude/.tmp/pr-review/body-<number>-<short-sha>.md
167
173
  ```
168
174
 
169
- A pass with no findings takes `## Review closed` and a short body, with the footer line included either way. On a first pass, post `✅ No blocking findings. Reviewed against project docs and roadmap.` On a later pass, post `✅ Prior findings addressed. Re-reviewed <short-sha>, N commits since the prior pass.`
175
+ A pass carrying nothing at all takes `## Review closed` and a short body, with the footer line included either way. On a first pass, post `✅ No blocking findings. Reviewed against project docs and roadmap.` On a later pass, post `✅ Prior findings addressed. Re-reviewed <short-sha>, N commits since the prior pass.`
176
+
177
+ A pass carrying only minors takes the same heading and the full shape rather than either short line, since the minors have to be readable and neither line reports them. Keep whichever scope sentence the pass owes on the summary line:
178
+
179
+ ```markdown
180
+ ## Review closed
181
+
182
+ 0 critical, 0 should-fix, Z minor. Nothing blocks the merge. Reviewed against project docs and roadmap.
183
+
184
+ **`path/to/file.ext`**
185
+
186
+ - **minor**: finding, and the fix it wants.
187
+
188
+ 🤖 Reviewed by Claude Code
189
+ ```
170
190
 
171
- A pass that closed by withdrawing a finding rather than by reading its fix takes neither line, per the withdrawal rule in Step 3. Both claim a fix landed, and the second names it, so posting either over a withdrawal credits work nobody did on the one comment a reader treats as the verdict. Write the withdrawal and the fact that settled it in place of the canned line, keeping the heading and the footer.
191
+ A pass that closed by withdrawing a finding rather than by reading its fix takes neither line, per the withdrawal rule in Step 3. Both claim a fix landed, and the second names it, so posting either over a withdrawal credits work nobody did on the one comment a reader treats as the verdict. Write the withdrawal and the fact that settled it in place of the canned line, keeping the heading and the footer.
172
192
 
173
- Post that one even when there is nothing to report. A review left with no closing comment reads as one nobody answered.
193
+ Post a close-out even when there is nothing to report. A review left with no closing comment reads as one nobody answered.
174
194
 
175
195
  ## Step 5: output
176
196
 
@@ -73,7 +73,7 @@ When features are independent, run them in parallel instead of sequentially. Use
73
73
  - Ship each worktree separately with `aitk:git-ship`
74
74
  - For full autonomy per worktree, invoke `aitk:claude-autoship` instead of the manual chain. Approve the plan, walk away, come back to draft PRs.
75
75
 
76
- To run several worktrees as a coordinated flow rather than ad hoc, assert the orchestrator role in one warm session with `aitk:claude-orchestrate`. It owns the roadmap, handing a needed draft or resequence to a worker that runs `aitk:claude-roadmap` in its branch, plans each feature, refills the ready queue so a free worker never waits, and reviews each worker's PR with `aitk:claude-pr-review`, then tells the session holding that branch to run `aitk:claude-address-review` whenever the pass posted a critical or should-fix finding. The human launches workers and merges. See [operating model](operating-model.md) for the full loop.
76
+ To run several worktrees as a coordinated flow rather than ad hoc, assert the orchestrator role in one warm session with `aitk:claude-orchestrate`. It owns the roadmap, handing a needed draft or resequence to a worker that runs `aitk:claude-roadmap` in its branch, plans each feature, refills the ready queue so a free worker never waits, and reviews each worker's PR with `aitk:claude-pr-review`, then tells the session holding that branch to run `aitk:claude-address-review` whenever the pass posted a finding at any severity, read off the counts on the summary line rather than off the heading above them. The human launches workers and merges. See [operating model](operating-model.md) for the full loop.
77
77
 
78
78
  Roadmap ownership holds while a scope exists to sequence. Once the MVP list in `.claude/REQUIREMENTS.md` has shipped, later work arrives as discrete items and the orchestrator reads `.claude/tasks/priority.md` for execution order instead.
79
79
 
@@ -40,8 +40,8 @@ One feature travels this path end to end.
40
40
  2. Orchestrator plans the next feature with `claude-feature`, writing a plan to `.claude/plans/`. Planning stays in the warm session because good planning is cross-feature. It needs the contract other features consume and the shared wiring seam. A cold session would re-derive or guess.
41
41
  3. The human opens a worker worktree with `claude-worktree` and runs `claude-autoship` against the plan. The worker builds, self-checks, opens a PR, and stops at the PR boundary.
42
42
  4. Orchestrator reviews the PR with `claude-pr-review` and posts findings to it.
43
- 5. Orchestrator tells the session holding that branch to run `claude-address-review` once the pass posted a critical or should-fix finding, resolving the target from a session listing taken at that moment and reporting the invocation for the human when no live session holds it. The worker addresses the findings, rebases onto `origin/main` when a sibling landed first and left the branch unable to merge, then pushes a follow-up. A pass carrying only minor findings dispatches nobody and goes straight to step 7, since a minor is visibility alone and leaves nothing for a worker to act on.
44
- 6. Orchestrator closes the review out with `claude-pr-review` again. The second pass reads only the commits the follow-up added, or the worker's response alone when the follow-up added none, and posts under `## Review closed` when it finds nothing open or under `## Review` when it does, so a reader learns the state from the heading. Repeat from step 5 until the review closes or leaves only minor findings open.
43
+ 5. Orchestrator tells the session holding that branch to run `claude-address-review` once the pass posted a finding at any severity, resolving the target from a session listing taken at that moment and reporting the invocation for the human when no live session holds it. The worker addresses the findings, rebases onto `origin/main` when a sibling landed first and left the branch unable to merge, then pushes a follow-up. A pass carrying only minor findings dispatches too, since the grade runs low often enough that a floor at should-fix loses fixes a worker would have made.
44
+ 6. Orchestrator closes the review out with `claude-pr-review` again. The second pass reads only the commits the follow-up added, or the worker's response alone when the follow-up added none, and posts under `## Review` when it finds a critical or a should-fix and under `## Review closed` otherwise, so a reader learns the merge decision from the heading and the counts from the line under it. Repeat from step 5 until a pass closes the review.
45
45
  7. The human reads the result and merges. The orchestrator tells any trailing worker whose branch shares a seam with the merged one to run `claude-address-review`, which rebases whether or not the review left anything open.
46
46
 
47
47
  There is no loop construct here. Each worker is a single build that halts at the
@@ -102,9 +102,14 @@ closed clean and then went stale still rebases when the skill is invoked. The
102
102
  re-read costs a full pass rather than a delta, since the prior reviewed commit no
103
103
  longer reaches the head, and `claude-pr-review` detects that itself.
104
104
 
105
- The heading carries the state rather than the pass number. A pass with a finding
106
- takes `## Review` and a pass with none takes `## Review closed`, so a thread can
107
- be scanned for what is still open without opening a comment. The feedback
105
+ The heading carries the state rather than the pass number. A pass carrying a
106
+ critical or a should-fix takes `## Review` and every other pass takes
107
+ `## Review closed`, so a thread can be scanned for what still blocks a merge
108
+ without opening a comment. The heading and the dispatch answer two questions
109
+ rather than one: a minors-only pass closes the heading and still sends a worker,
110
+ so the dispatch reads the counts on the summary line rather than the heading
111
+ above them. A minor the worker declines goes to the findings of the task the
112
+ branch closes, since a thread does not survive the merge. The feedback
108
113
  becomes a durable artifact both sessions read, survives a session ending, and
109
114
  anchors to the change. That removes the copy-paste that otherwise routes review
110
115
  through the human between two sessions.
@@ -0,0 +1,26 @@
1
+ ---
2
+ description: Require a route capture against a running preview after a page or route surface changes
3
+ paths:
4
+ - '**/routes/**/*.{tsx,jsx,vue,svelte,astro}'
5
+ - '**/pages/**/*.{tsx,jsx,vue,svelte,astro}'
6
+ - '**/app/**/page.{tsx,jsx}'
7
+ ---
8
+
9
+ # Surface capture standards
10
+
11
+ ## When to capture
12
+
13
+ - Run `bun run screenshot` after changing what a route renders.
14
+ - Capture against a running preview server. Do not capture against a dev server.
15
+ - Capture every theme the route ships. Do not capture the default theme alone.
16
+
17
+ ## What a capture covers
18
+
19
+ - Capture the full page at the viewport its case declares. Do not capture a component in isolation.
20
+ - Add a case to the capture record when adding a route.
21
+ - Remove a route's case in the change that removes the route.
22
+
23
+ ## Sharing a capture
24
+
25
+ - Attach a capture to the pull request by hand when a reviewer needs to see it.
26
+ - Do not commit a capture. Do not remove the capture folder from `.gitignore`.
@@ -1,2 +1,2 @@
1
1
  extends = "node"
2
- rules = ["210-astro", "350-security-web", "400-ui", "410-a11y", "430-ux-completeness"]
2
+ rules = ["210-astro", "350-security-web", "400-ui", "410-a11y", "430-ux-completeness", "440-surface-capture"]
@@ -1,2 +1,2 @@
1
1
  extends = "node"
2
- rules = ["200-react", "230-nextjs", "250-tailwind", "300-testing-ts", "310-zod", "350-security-web", "400-ui", "410-a11y", "420-forms", "430-ux-completeness"]
2
+ rules = ["200-react", "230-nextjs", "250-tailwind", "300-testing-ts", "310-zod", "350-security-web", "400-ui", "410-a11y", "420-forms", "430-ux-completeness", "440-surface-capture"]
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/aitk",
3
3
  "type": "module",
4
- "version": "0.96.0",
4
+ "version": "0.97.1",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -4,6 +4,8 @@
4
4
 
5
5
  The base layer covers every project the toolkit scaffolds, whatever language sits on top. It ships formatting, spelling, shell linting, conventional commits, git hooks, CI, and three maintenance scripts. Every other stack extends it, so a decision made here is one every stack inherits.
6
6
 
7
+ A repository with several language roots layers this stack once at the root and skips it per subtree, since git honors only one `core.hooksPath` and a re-dropped husky silently breaks. `docs/target-projects.md` carries the recipe.
8
+
7
9
  ## What ships as golden configs
8
10
 
9
11
  Golden config files live in `tooling/base/configs/` and are copied into the target on `aitk tooling sync base .`. They are the source of truth. The reference covers rationale and tradeoffs. Configs show the concrete setup.
@@ -1,41 +1,53 @@
1
1
  import type { Page } from '@playwright/test'
2
-
3
- const ROUTES = [{ name: 'home', path: '/', width: 1280, height: 800 }]
4
-
5
- type State = { name: string; setup?: (page: Page) => Promise<void> }
6
- const STATES: State[] = [
7
- { name: 'default' },
8
- { name: 'dark', setup: async (p) => p.emulateMedia({ colorScheme: 'dark' }) },
9
- ]
10
-
11
2
  import { chromium } from '@playwright/test'
12
3
  import { mkdir } from 'fs/promises'
13
4
  import path from 'path'
14
5
 
6
+ interface CaptureCase {
7
+ section: string
8
+ theme: string
9
+ route: string
10
+ width: number
11
+ height: number
12
+ setup?: (page: Page) => Promise<void>
13
+ }
14
+
15
+ const CASES: CaptureCase[] = [
16
+ { section: 'home', theme: 'default', route: '/', width: 1280, height: 800 },
17
+ {
18
+ section: 'home',
19
+ theme: 'dark',
20
+ route: '/',
21
+ width: 1280,
22
+ height: 800,
23
+ setup: (page) => page.emulateMedia({ colorScheme: 'dark' }),
24
+ },
25
+ ]
26
+
15
27
  const BASE_URL = process.env.SCREENSHOT_BASE_URL ?? 'http://localhost:4173'
16
28
  const OUT_DIR = 'screenshots'
17
29
 
18
30
  const browser = await chromium.launch()
19
- await mkdir(OUT_DIR, { recursive: true })
20
31
 
21
- for (const route of ROUTES) {
22
- for (const state of STATES) {
23
- const ctx = await browser.newContext({
24
- viewport: { width: route.width, height: route.height },
25
- })
26
- const page = await ctx.newPage()
32
+ for (const captureCase of CASES) {
33
+ const context = await browser.newContext({
34
+ viewport: { width: captureCase.width, height: captureCase.height },
35
+ })
36
+ const page = await context.newPage()
37
+
38
+ if (captureCase.setup) await captureCase.setup(page)
27
39
 
28
- if (state.setup) await state.setup(page)
40
+ await page.goto(`${BASE_URL}${captureCase.route}`)
41
+ await page.waitForLoadState('networkidle')
29
42
 
30
- await page.goto(`${BASE_URL}${route.path}`)
31
- await page.waitForLoadState('networkidle')
43
+ const sectionDir = path.join(OUT_DIR, captureCase.section)
44
+ await mkdir(sectionDir, { recursive: true })
32
45
 
33
- const file = path.join(OUT_DIR, `${route.name}-${state.name}.png`)
34
- await page.screenshot({ path: file, fullPage: true })
35
- console.log(`captured ${file}`)
46
+ const file = path.join(sectionDir, `${captureCase.theme}.png`)
47
+ await page.screenshot({ path: file, fullPage: true })
48
+ console.log(`captured ${file}`)
36
49
 
37
- await ctx.close()
38
- }
50
+ await context.close()
39
51
  }
40
52
 
41
53
  await browser.close()
@@ -12,7 +12,7 @@ Golden config files live in `tooling/web/configs/` and are copied into the targe
12
12
 
13
13
  - `eslint.config.js`: flat config with `@eslint/js`, `typescript-eslint`, React hooks, import sort, check-file, vitest rules scoped to test files, `eslint-config-prettier` last.
14
14
  - `src/test/setup.ts`: `@testing-library/jest-dom` import, `cleanup` after each test.
15
- - `e2e/screenshot.ts`: capture template. `ROUTES` and `STATES` consts at the top carry one route in a default and a dark state, and the loop below them writes `screenshots/<name>-<state>.png`. Per-project routes and states extend the two consts.
15
+ - `e2e/screenshot.ts`: capture template. A single `CASES` record at the top carries one entry per output file, each naming a section, a theme, a route, and its own viewport, and the loop below writes `screenshots/<section>/<theme>.png`. Per-project cases extend the one record. A route's themes sit together under its section folder, so the filename carries the theme alone.
16
16
  - `.vscode/extensions.json` and `.vscode/settings.json`: editor wiring for ESLint, Tailwind, Playwright, Vitest.
17
17
  - `.github/workflows/verify.yml`: `static-checks`, `unit-tests`, `build-verify`, and `e2e-tests` jobs.
18
18
  - `scripts/verify.sh`: extends base verify with typecheck, lint, unit tests, and build in the full order.
@@ -102,7 +102,9 @@ Append rows:
102
102
  | `bun run test:e2e` | Run Playwright E2E tests. |
103
103
  | `bun run screenshot` | Build, preview, then capture screenshots. |
104
104
 
105
- `aitk tooling verify <stack>` is the only automated caller of `bun run screenshot`, running it for any stack whose `package.json` declares the script and asserting that PNG files land under `screenshots/`. No ship chain captures a screenshot, so the output path the seed writes is a contract that one verifier reads rather than a default a ship step depends on.
105
+ `aitk tooling verify <stack>` is the only automated caller of `bun run screenshot`, running it for any stack whose `package.json` declares the script and asserting that PNG files land under `screenshots/`. It counts them with a recursive find carrying no depth limit, so the section folders the seed writes satisfy the assertion without a change to it. Do not flatten the layout to protect that check. No ship chain captures a screenshot, so the output path the seed writes is a contract that one verifier reads rather than a default a ship step depends on.
106
+
107
+ `governance/rules/ui/440-surface-capture.md` is what asks a session to run the capture after a route changes. It fires on route and page files rather than on every component, so a shared component changing every screen fires nothing and the operator runs the capture by hand.
106
108
 
107
109
  ## Verify script
108
110