@rtorcato/repo-tooling 3.37.2 → 3.38.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtorcato/repo-tooling",
3
- "version": "3.37.2",
3
+ "version": "3.38.0",
4
4
  "description": "One CLI to scaffold, audit and fix your repo's whole toolchain — linting, tests, commits, releases & CI.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -147,7 +147,7 @@ this block matches it, so the two cannot diverge.
147
147
  Also once per repo, keep the status file out of git:
148
148
 
149
149
  ```bash
150
- grep -qxF '.claude/ai-loop-status' .gitignore || echo '.claude/ai-loop-status' >> .gitignore
150
+ grep -qxF '.claude/ai-loop-status' "$ROOT/.gitignore" || echo '.claude/ai-loop-status' >> "$ROOT/.gitignore"
151
151
  ```
152
152
 
153
153
  ```
@@ -258,8 +258,12 @@ reviewable, and carried forward by `fix lockfile`:
258
258
  { "rules": { "aiLoop": { "agentUser": "your-bot-account" } } }
259
259
  ```
260
260
 
261
- Every later use is `${AGENT_USER:+--add-assignee "$AGENT_USER"}`, which expands
262
- to nothing when it is empty — so there is one code path, not two.
261
+ Every later use is `${AGENT_USER:+--add-assignee} ${AGENT_USER:+"$AGENT_USER"}`, which expands
262
+ to nothing when it is empty — so there is one code path, not two. **Keep the flag
263
+ and the value in separate expansions.** The one-expansion form
264
+ `${AGENT_USER:+--add-assignee "$AGENT_USER"}` (#624) word-splits in bash but not
265
+ in zsh, where `gh` receives `--add-assignee bot` as a single argument and
266
+ rejects it.
263
267
 
264
268
  **Resolve `HUMAN_USER` too — the person work is handed back to.** Needs no
265
269
  config: on a personal repo the owner *is* the person. On an organisation repo
@@ -270,7 +274,7 @@ every handoff below assigns nobody rather than something meaningless.
270
274
  HUMAN_USER=$(gh api "repos/$OWNER_REPO" --jq 'if .owner.type == "User" then .owner.login else "" end')
271
275
  ```
272
276
 
273
- Later uses are `${HUMAN_USER:+--add-assignee "$HUMAN_USER"}`, the same shape as
277
+ Later uses are `${HUMAN_USER:+--add-assignee} ${HUMAN_USER:+"$HUMAN_USER"}`, the same shape as
274
278
  `AGENT_USER`. **A `gh … edit` whose every expansion is empty has no flags and
275
279
  errors — skip the call entirely in that case** rather than letting it fail the
276
280
  tick.
@@ -490,7 +494,11 @@ Three things the gate does **not** change:
490
494
  - **`ai-notes` still blocks an unattended merge.** A reviewer who passed but left
491
495
  something to read means a human reads it.
492
496
  - **Order is still load-bearing.** If `autoMergeRequest != null` the merge can beat
493
- the review, so Pass 0's disarm step applies unchanged.
497
+ the review. Nowhere but this arm does the loop let an issue PR auto-merge, and
498
+ only after both verdicts, so one found already armed without both `ai-ok-*`
499
+ labels was armed by someone else — run `gh pr merge <N> --disable-auto` before anything
500
+ else touches it. (#605 removed the Pass 0 disarm step this line used to point
501
+ at, along with the Dependabot arm it served.)
494
502
 
495
503
  Be plain about the residual risk: even gated, this lands code on `main` unattended,
496
504
  and the only quality signal is two reviewers that — per the limits above — see the
@@ -554,9 +562,9 @@ makes `merge-ready` assert more than the `ai-ok-*` pair ever did: reviews passed
554
562
  *and* GitHub will accept the merge.
555
563
 
556
564
  ```bash
557
- gh pr edit <N> ${HUMAN_USER:+--add-assignee "$HUMAN_USER"} --add-label merge-ready \
565
+ gh pr edit <N> ${HUMAN_USER:+--add-assignee} ${HUMAN_USER:+"$HUMAN_USER"} --add-label merge-ready \
558
566
  --remove-label ai-review --remove-label ai-ok-code --remove-label ai-ok-sec \
559
- ${AGENT_USER:+--remove-assignee "$AGENT_USER"}
567
+ ${AGENT_USER:+--remove-assignee} ${AGENT_USER:+"$AGENT_USER"}
560
568
  ```
561
569
 
562
570
  **`merge-ready` replaces the pass pair — it does not join it.** A handed-off PR
@@ -652,8 +660,8 @@ keep and self-retiring once the last one is handled:
652
660
  # Both empty (org repo, no agentUser) would leave `gh pr edit <N>` with no flags,
653
661
  # which errors — so guard the call rather than trusting the reader to skip it.
654
662
  if [ -n "$HUMAN_USER" ] || [ -n "$AGENT_USER" ]; then
655
- gh pr edit <N> ${HUMAN_USER:+--add-assignee "$HUMAN_USER"} \
656
- ${AGENT_USER:+--remove-assignee "$AGENT_USER"}
663
+ gh pr edit <N> ${HUMAN_USER:+--add-assignee} ${HUMAN_USER:+"$HUMAN_USER"} \
664
+ ${AGENT_USER:+--remove-assignee} ${AGENT_USER:+"$AGENT_USER"}
657
665
  fi
658
666
  ```
659
667
 
@@ -798,7 +806,7 @@ Only then:
798
806
  REMOVED=1 # every removal in this pass sets this
799
807
  git -C "$ROOT" worktree remove --force "$WT_DIR" # the path found above, not a rebuilt one
800
808
  git -C "$ROOT" branch -D "$BRANCH" 2>/dev/null
801
- gh issue edit <N> --remove-label ai-wip ${AGENT_USER:+--remove-assignee "$AGENT_USER"} 2>/dev/null
809
+ gh issue edit <N> --remove-label ai-wip ${AGENT_USER:+--remove-assignee} ${AGENT_USER:+"$AGENT_USER"} 2>/dev/null
802
810
  # Still OPEN means the PR said only `Refs #N`; a `Closes #N` issue is already closed.
803
811
  if [ -n "$HUMAN_USER" ] && [ "$(gh issue view <N> --json state -q .state)" = OPEN ]; then
804
812
  gh issue edit <N> --add-assignee "$HUMAN_USER"
@@ -832,7 +840,7 @@ work must never be reaped out from under itself.
832
840
 
833
841
  | Stalled | Condition | Do |
834
842
  |---|---|---|
835
- | Implementer died | issue `ai-wip` ≥45min, **and no PR exists** for `ai-<N>-<slug>` | `gh issue edit <N> --add-label ai-blocked --remove-label ai-wip ${HUMAN_USER:+--add-assignee "$HUMAN_USER"} ${AGENT_USER:+--remove-assignee "$AGENT_USER"}`, comment, remove the worktree (and set `REMOVED=1`) |
843
+ | Implementer died | issue `ai-wip` ≥45min, **and no PR exists** for `ai-<N>-<slug>` | `gh issue edit <N> --add-label ai-blocked --remove-label ai-wip ${HUMAN_USER:+--add-assignee} ${HUMAN_USER:+"$HUMAN_USER"} ${AGENT_USER:+--remove-assignee} ${AGENT_USER:+"$AGENT_USER"}`, comment, remove the worktree (and set `REMOVED=1`) |
836
844
  | Reviewer died | PR `ai-reviewing-code` (or `ai-reviewing-sec`) ≥45min with no matching `ai-ok-*` and no `ai-changes` | `gh pr edit <N> --remove-label <the claim that stalled>` — drop **that** label, not a fixed one; a stalled `ai-reviewing-sec` cleared as `ai-reviewing-code` leaves the dead claim in place and the reviewer never re-spawns. Dropping the claim is what lets Pass 3 re-spawn it, and they're cheap and diff-scoped. If that claim has been applied ≥3 times, `ai-blocked` instead |
837
845
  | Fix implementer died | PR `ai-fixing` ≥45min and still `ai-changes` — it never got as far as relabelling to `ai-review` | `gh pr edit <N> --remove-label ai-fixing`, which is what lets Pass 3 dispatch the round again. If `ai-fixing` has been applied ≥3 times, `ai-blocked` on the linked issue instead — a round that dies every time is not one more spawn away from working. Leave the worktree: it holds whatever the dead implementer committed |
838
846
  | Orphan worktree | `"$WT_ROOT"/ai-<N>-*` whose issue is not `ai-wip` and has no open PR | remove the worktree and branch (and set `REMOVED=1`) |
@@ -983,6 +991,13 @@ whose pass-label is missing — `code-reviewer` if no `ai-ok-code`,
983
991
  carries `ai-reviewing-sec`. Both can run concurrently; launch them in a single
984
992
  message.
985
993
 
994
+ **`code-reviewer` and `security-expert` name the two *arms*, not agent types this
995
+ package ships.** Spawn each with that `subagent_type` when your Agent tool lists
996
+ it; otherwise spawn `general-purpose`, which always exists. The prompt template
997
+ below carries the whole review lens and the verdict protocol, so a named agent
998
+ only adds its own system prompt on top. Never skip a review because the named
999
+ type is missing (#611).
1000
+
986
1001
  **Before spawning either, check whether it already posted.** A missing verdict
987
1002
  label does not mean the review is missing: on #497 both reviewers posted
988
1003
  complete reviews and then went idle, labelling nothing. Every review comment
@@ -1065,8 +1080,8 @@ intended; one that died after posting is now recovered instead of duplicated.
1065
1080
  issue. Apply the label immediately before the spawn, not after:
1066
1081
 
1067
1082
  ```bash
1068
- gh pr edit <N> --add-label ai-reviewing-code ${AGENT_USER:+--add-assignee "$AGENT_USER"} # then spawn code-reviewer
1069
- gh pr edit <N> --add-label ai-reviewing-sec ${AGENT_USER:+--add-assignee "$AGENT_USER"} # then spawn security-expert
1083
+ gh pr edit <N> --add-label ai-reviewing-code ${AGENT_USER:+--add-assignee} ${AGENT_USER:+"$AGENT_USER"} # then spawn code-reviewer
1084
+ gh pr edit <N> --add-label ai-reviewing-sec ${AGENT_USER:+--add-assignee} ${AGENT_USER:+"$AGENT_USER"} # then spawn security-expert
1070
1085
  ```
1071
1086
 
1072
1087
  Assigning `AGENT_USER` on the claim is idempotent — both arms adding the same
@@ -1240,9 +1255,9 @@ then:
1240
1255
 
1241
1256
  ```bash
1242
1257
  gh issue edit <M> --add-label ai-blocked --remove-label ai-wip \
1243
- ${HUMAN_USER:+--add-assignee "$HUMAN_USER"} ${AGENT_USER:+--remove-assignee "$AGENT_USER"}
1258
+ ${HUMAN_USER:+--add-assignee} ${HUMAN_USER:+"$HUMAN_USER"} ${AGENT_USER:+--remove-assignee} ${AGENT_USER:+"$AGENT_USER"}
1244
1259
  gh pr edit <N> --remove-label ai-review \
1245
- ${HUMAN_USER:+--add-assignee "$HUMAN_USER"} ${AGENT_USER:+--remove-assignee "$AGENT_USER"}
1260
+ ${HUMAN_USER:+--add-assignee} ${HUMAN_USER:+"$HUMAN_USER"} ${AGENT_USER:+--remove-assignee} ${AGENT_USER:+"$AGENT_USER"}
1246
1261
  ```
1247
1262
 
1248
1263
  Leave the worktree and PR in place for the human; a ping-pong stall is the case where
@@ -1252,7 +1267,7 @@ Otherwise **claim first, then spawn** — same shape as the reviewer claims abov
1252
1267
  and for the same reason. Apply the label immediately before the spawn, not after:
1253
1268
 
1254
1269
  ```bash
1255
- gh pr edit <N> --add-label ai-fixing ${AGENT_USER:+--add-assignee "$AGENT_USER"} # then spawn the implementer
1270
+ gh pr edit <N> --add-label ai-fixing ${AGENT_USER:+--add-assignee} ${AGENT_USER:+"$AGENT_USER"} # then spawn the implementer
1256
1271
  ```
1257
1272
 
1258
1273
  A fix round runs longer than a 15-minute tick — on #565, `ai-changes` at 17:35 and
@@ -1299,7 +1314,6 @@ gh api "repos/$OWNER_REPO/issues?labels=ai-ready&state=open" \
1299
1314
  | select([.labels[].name] | index("ai-wip") == null)
1300
1315
  | select([.labels[].name] | index("ai-blocked") == null)
1301
1316
  | select([.labels[].name] | index("holding") == null)
1302
- | select([.labels[].name] | index("ai-suggested") == null)
1303
1317
  | select(.author_association=="OWNER" or .author_association=="MEMBER" or .author_association=="COLLABORATOR")
1304
1318
  | {number, title, body}'
1305
1319
  ```
@@ -1314,9 +1328,11 @@ the first place, but then mislabelling it costs nothing. Unlike `ai-blocked` (an
1314
1328
  agent tried and got stuck), `holding` says *no agent should ever start*, and it
1315
1329
  shows up in the issue list so a human triaging does not re-litigate it either.
1316
1330
 
1317
- `ai-suggested` is excluded for a harder reason: it is an agent's own suggestion,
1318
- so picking one up would let the loop feed itself work — promoting one is a human
1319
- act, which is what makes that label a triage queue rather than a backlog.
1331
+ `ai-suggested` is deliberately *not* filtered. An agent's own suggestion carries
1332
+ only `ai-suggested`, so it never matches `labels=ai-ready` — the loop cannot feed
1333
+ itself work. Promoting one is a human adding `ai-ready`, and the item keeps
1334
+ `ai-suggested` (Pass 2 relies on that), so excluding the label here would strand
1335
+ every promoted issue in the queue forever (#608).
1320
1336
 
1321
1337
  **Declining an issue is a visible act — comment, never just skip.** Whenever an
1322
1338
  agent decides an issue should *not* go to the pipeline — triaging which issues to
@@ -1404,7 +1420,7 @@ concurrent tick can't double-pick:
1404
1420
 
1405
1421
  ```bash
1406
1422
  gh issue edit <N> --add-label ai-wip --remove-label ai-ready \
1407
- ${AGENT_USER:+--add-assignee "$AGENT_USER"}
1423
+ ${AGENT_USER:+--add-assignee} ${AGENT_USER:+"$AGENT_USER"}
1408
1424
  ```
1409
1425
 
1410
1426
  Assigning here is what makes the issue list honest: from this moment an agent
@@ -1453,7 +1469,7 @@ done)
1453
1469
  **Iterate line by line — never `for d in $DIRS`.** Your shell may be zsh, which
1454
1470
  does not word-split an unquoted expansion: `$DIRS` arrives as *one* word with
1455
1471
  embedded newlines, `[ -d ]` fails against that nonsense path, and the loop links
1456
- **nothing** (#585). Same class as the Pass 2 glob hazard below, and just as
1472
+ **nothing** (#585). Same class as the Pass 2 glob hazard above, and just as
1457
1473
  silent — the `pnpm install` fallback is gated on `$DIRS` being *empty*, which it
1458
1474
  is not, so the worktree gets neither links nor an install, and the implementer
1459
1475
  meets `Cannot find module` on its first test run, reading as the issue's fault
@@ -1622,7 +1638,8 @@ worktree in this pass. Never fall back to `EnterWorktree`.
1622
1638
  Never skip this pass, **including on an idle tick**. An unobservable loop is
1623
1639
  indistinguishable from a dead one.
1624
1640
 
1625
- Compose `SUMMARY` from what Passes 1–4 already counted — no extra `gh` calls.
1641
+ Compose `SUMMARY` from what Passes 1–4 already counted — no extra `gh` calls
1642
+ (the triage digest's one `gh issue list` below is the only exception).
1626
1643
  Middle dot separated, zero segments omitted, stall counts first with a `⚠`:
1627
1644
 
1628
1645
  | State | `SUMMARY` |
@@ -1635,11 +1652,19 @@ Middle dot separated, zero segments omitted, stall counts first with a `⚠`:
1635
1652
  Then diff against last tick and decide whether to notify:
1636
1653
 
1637
1654
  ```bash
1638
- STATUS=".claude/ai-loop-status"
1655
+ STATUS="$ROOT/.claude/ai-loop-status" # absolute — a pinned tick's cwd is a worktree
1639
1656
  PREV=$(head -1 "$STATUS" 2>/dev/null)
1640
- IDLE=$(sed -n 2p "$STATUS" 2>/dev/null || echo 0)
1657
+ IDLE=$(sed -n 2p "$STATUS" 2>/dev/null); IDLE=${IDLE:-0}
1658
+ PREV_SUGGESTED=$(sed -n 3p "$STATUS" 2>/dev/null)
1659
+ DIGEST=$(gh issue list -R "$OWNER_REPO" --label ai-suggested --state open --limit 100 \
1660
+ --json number,title --jq 'sort_by(.number) | .[] | "#\(.number) \(.title)"')
1661
+ SUGGESTED=$(printf '%s\n' "$DIGEST" | grep -o '^#[0-9]*' | tr -d '#' | paste -sd, -)
1641
1662
  ```
1642
1663
 
1664
+ `IDLE=${IDLE:-0}` rather than `|| echo 0`: `sed` on a file shorter than two
1665
+ lines exits 0 with no output, so the `||` branch never fires and `IDLE+1` would
1666
+ run on an empty string.
1667
+
1643
1668
  - **`SUMMARY` != `PREV`** → notify, and `IDLE=0`.
1644
1669
  - **`SUMMARY` == `idle`** → `IDLE=$((IDLE+1))`; notify **only when `IDLE` is
1645
1670
  exactly 4** (≈1h quiet), with `idle 1h — no ai-ready issues`. Exactly, not
@@ -1648,20 +1673,25 @@ IDLE=$(sed -n 2p "$STATUS" 2>/dev/null || echo 0)
1648
1673
 
1649
1674
  One notification per tick, maximum — the summary already says everything.
1650
1675
 
1676
+ Send it with the **`PushNotification`** tool — `message`: `"$OWNER_REPO: $SUMMARY"`
1677
+ (one line, under 200 characters, `⚠` segments first so a truncated phone banner
1678
+ still leads with the stall). It works on every platform, reaches the phone when
1679
+ Remote Control is connected, and skips itself when the user is already at the
1680
+ terminal — so a tick the user is watching costs no toast. A "not sent" result is
1681
+ normal; never retry it.
1682
+
1683
+ Only when the tool is not available in this session, fall back to a desktop toast
1684
+ that cannot fail the tick:
1685
+
1651
1686
  ```bash
1652
- osascript -e "display notification \"$SUMMARY\" with title \"ai-issue-loop\" subtitle \"$OWNER_REPO\"" 2>/dev/null || true
1687
+ osascript -e "display notification \"$SUMMARY\" with title \"ai-issue-loop\" subtitle \"$OWNER_REPO\"" 2>/dev/null \
1688
+ || notify-send "ai-issue-loop" "$OWNER_REPO: $SUMMARY" 2>/dev/null || true
1653
1689
  ```
1654
1690
 
1655
- `osascript` is macOS-only, and the `|| true` is what makes shipping it portable:
1656
- elsewhere the tick still completes and only loses the desktop toast. On Linux
1657
- swap in `notify-send "ai-issue-loop" "$SUMMARY"` behind the same `|| true`. The
1658
- statusline file below is plain text and works anywhere.
1659
-
1660
- When `SUMMARY` carries a `⚠` (anything `blocked`, `ci-red`, or `rebuild`), append
1661
- `sound name "Basso"` so a stall is audibly different from routine progress.
1691
+ The statusline file below is plain text and works anywhere.
1662
1692
 
1663
1693
  Write the file **last** — summary, idle counter, and the sorted `ai-suggested`
1664
- numbers the digest rule above compares against:
1694
+ numbers the digest rule below compares against:
1665
1695
 
1666
1696
  ```bash
1667
1697
  printf '%s\n%s\n%s\n' "$SUMMARY" "$IDLE" "$SUGGESTED" > "$STATUS"
@@ -1680,12 +1710,11 @@ cleaned up, sent to review, picked up, blocked. Nothing else; this repeats every
1680
1710
  which ones need reading before they are merged — that is the one place the notes
1681
1711
  reach a human who is not already looking at GitHub.
1682
1712
 
1683
- **End with the triage digest** — the open `ai-suggested` queue, one line per
1684
- issue, straight from `gh issue list --label ai-suggested --state open --json
1685
- number,title`. No new state, no extra prose: a list scanned in one glance is what
1686
- makes a human promote or close something. Skip the digest when the queue is empty
1687
- or unchanged since the last tick (compare against a third line in `$STATUS`: the
1688
- sorted issue numbers).
1713
+ **End with the triage digest** — print `$DIGEST` (the open `ai-suggested`
1714
+ queue, one line per issue, fetched above). No new state, no extra prose: a list
1715
+ scanned in one glance is what makes a human promote or close something. Skip the
1716
+ digest when `$SUGGESTED` is empty or equals `$PREV_SUGGESTED` (line 3 of
1717
+ `$STATUS` from the last tick).
1689
1718
 
1690
1719
  **The digest is a deadline, not an archive** — Pass 2 closes any item untouched
1691
1720
  for 30 days, so anything listed here that nobody engages with will expire on its
@@ -55,6 +55,8 @@ git -C "$ROOT" fetch --prune
55
55
  # ai-issue-loop skill's Pass 0 for why this is repo config rather than an env var.
56
56
  AGENT_USER="${AI_LOOP_AGENT:-$(jq -r '.rules.aiLoop.agentUser // .aiLoop.agentUser // empty' "$ROOT/.repo-tooling.json" 2>/dev/null)}"
57
57
  [ -n "$AGENT_USER" ] && { gh api "repos/$R/assignees/$AGENT_USER" --silent 2>/dev/null || AGENT_USER=""; }
58
+ # The human a given-up issue is handed back to — the repo owner, when that is a user.
59
+ HUMAN_USER=$(gh api "repos/$R" --jq 'if .owner.type == "User" then .owner.login else "" end')
58
60
  ```
59
61
 
60
62
  `R` comes from the working directory's remote and is the only repo touched —
@@ -79,7 +81,6 @@ gh api "repos/$R/issues?labels=ai-ready&state=open" \
79
81
  | select([.labels[].name] | index("ai-wip") == null)
80
82
  | select([.labels[].name] | index("ai-blocked") == null)
81
83
  | select([.labels[].name] | index("holding") == null)
82
- | select([.labels[].name] | index("ai-suggested") == null)
83
84
  | select(.author_association=="OWNER" or .author_association=="MEMBER" or .author_association=="COLLABORATOR")
84
85
  | {number, title, body}'
85
86
  ```
@@ -121,7 +122,7 @@ carrying both re-enters the queue the instant `ai-wip` clears):
121
122
  ```bash
122
123
  for n in <numbers>; do
123
124
  gh issue edit -R "$R" $n --add-label ai-wip --remove-label ai-ready \
124
- ${AGENT_USER:+--add-assignee "$AGENT_USER"}
125
+ ${AGENT_USER:+--add-assignee} ${AGENT_USER:+"$AGENT_USER"}
125
126
  SLUG="ai-$n-<3-4 kebab words from the title>"
126
127
  mkdir -p "$WT_ROOT"
127
128
  git -C "$ROOT" worktree add "$WT_ROOT/$SLUG" -b "$SLUG" origin/main
@@ -143,11 +144,17 @@ and why.
143
144
  Call `Workflow` with the script below, passing the selected issues as `args`:
144
145
 
145
146
  ```
146
- Workflow({args: {repo: R, agentUser: AGENT_USER, issues: [{number, title, slug, worktree}, …]}, script: …})
147
+ Workflow({args: {repo: R, agentUser: AGENT_USER, humanUser: HUMAN_USER, namedReviewers, issues: [{number, title, slug, worktree}, …]}, script: …})
147
148
  ```
148
149
 
149
- Pass `agentUser` as the empty string when `AGENT_USER` is unset — the script
150
- tests it, so an empty value simply drops every assign.
150
+ Pass `namedReviewers: true` only when **both** `code-reviewer` and
151
+ `security-expert` appear in your Agent tool's list of agent types. They are not
152
+ shipped by this package, and a Workflow `agentType` that does not exist fails the
153
+ spawn. Otherwise pass `false`, and the reviewers run as `general-purpose` with the
154
+ same prompt, which carries the whole lens and verdict protocol (#611).
155
+
156
+ Pass `agentUser` / `humanUser` as the empty string when unset — the script
157
+ tests each, so an empty value simply drops that assign.
151
158
 
152
159
  ```js
153
160
  export const meta = {
@@ -211,8 +218,9 @@ const results = await pipeline(
211
218
 
212
219
  Give up early rather than grinding: if a build or test command hangs or fails
213
220
  twice the same way, stop. If you cannot finish, \`gh issue edit ${i.number}
214
- --add-label ai-blocked --remove-label ai-wip\`, comment why (🤖 header first),
215
- leave the worktree in place, and return pr: null.`,
221
+ --add-label ai-blocked --remove-label ai-wip${args.humanUser ? ` --add-assignee ${args.humanUser}` : ''}${args.agentUser ? ` --remove-assignee ${args.agentUser}` : ''}\`,
222
+ comment why (🤖 header first), leave the worktree in place, and return pr: null.
223
+ Handing back means the human ends up the only assignee.`,
216
224
  { label: `impl:#${i.number}`, phase: 'Implement', schema: PR }
217
225
  ),
218
226
 
@@ -251,7 +259,7 @@ Then apply exactly one verdict label, clearing your claim in the same command:
251
259
  \`gh pr edit ${r.pr} --add-label ai-changes --remove-label ai-review --remove-label ${v.claim}\`
252
260
  Plus \`--add-label ai-notes\` if and only if your section is not Nothing.
253
261
  A question only a human can answer → pass + ai-notes, never ai-changes.`,
254
- { label: `${v.type}:#${i.number}`, phase: 'Review', schema: VERDICT, agentType: v.type }
262
+ { label: `${v.type}:#${i.number}`, phase: 'Review', schema: VERDICT, agentType: args.namedReviewers ? v.type : 'general-purpose' }
255
263
  )))
256
264
  )
257
265
 
@@ -268,7 +276,8 @@ Notes on the script, so it doesn't get "tidied" into breakage:
268
276
  - **No `EnterWorktree` anywhere** — `{path}` is rejected for sibling worktrees
269
277
  and `{name}` relocates the orchestrator's own session. Implementers work via
270
278
  `git -C` and absolute paths.
271
- - Reviewers use `agentType` so they get their real system prompts, and post the
279
+ - Reviewers use `agentType` (the named type when installed, else
280
+ `general-purpose`) so they get their real system prompts, and post the
272
281
  same verdict markers the loop's Pass 3 reads — so a later tick adopts their
273
282
  verdicts instead of re-reviewing.
274
283