@rtorcato/repo-tooling 3.37.1 → 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.1",
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,26 @@ 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.
267
+
268
+ **Resolve `HUMAN_USER` too — the person work is handed back to.** Needs no
269
+ config: on a personal repo the owner *is* the person. On an organisation repo
270
+ `.owner.login` is the org, which is not a human, so it resolves to empty and
271
+ every handoff below assigns nobody rather than something meaningless.
272
+
273
+ ```bash
274
+ HUMAN_USER=$(gh api "repos/$OWNER_REPO" --jq 'if .owner.type == "User" then .owner.login else "" end')
275
+ ```
276
+
277
+ Later uses are `${HUMAN_USER:+--add-assignee} ${HUMAN_USER:+"$HUMAN_USER"}`, the same shape as
278
+ `AGENT_USER`. **A `gh … edit` whose every expansion is empty has no flags and
279
+ errors — skip the call entirely in that case** rather than letting it fail the
280
+ tick.
263
281
 
264
282
  **The point is that assignee answers "whose turn is it", which no label does
265
283
  well.** Today an issue an agent is mid-way through and an issue nobody has
@@ -274,9 +292,11 @@ half the story:
274
292
  | PR passed both reviews, waiting to merge | the human |
275
293
  | `ai-blocked`, declined, or held | the human |
276
294
 
277
- `@me` cannot express this: it resolves to whichever token is running, and the
278
- agents authenticate as the owner, so `@me` is *always* the human. That is why
279
- this is a separate name rather than a reuse.
295
+ `@me` cannot express either end: it resolves to whichever token is running, and
296
+ the identity check above *requires* that token to be `AGENT_USER` whenever an
297
+ agent account is declared — so `@me` is the agent precisely where the last two
298
+ rows want the human (#606). Both are therefore named explicitly, and `@me`
299
+ appears nowhere in this skill.
280
300
 
281
301
  Note the web UI's assignee picker can show a stale list that omits a
282
302
  freshly-added collaborator; `repos/{repo}/assignees` is the authority.
@@ -474,7 +494,11 @@ Three things the gate does **not** change:
474
494
  - **`ai-notes` still blocks an unattended merge.** A reviewer who passed but left
475
495
  something to read means a human reads it.
476
496
  - **Order is still load-bearing.** If `autoMergeRequest != null` the merge can beat
477
- 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.)
478
502
 
479
503
  Be plain about the residual risk: even gated, this lands code on `main` unattended,
480
504
  and the only quality signal is two reviewers that — per the limits above — see the
@@ -538,9 +562,9 @@ makes `merge-ready` assert more than the `ai-ok-*` pair ever did: reviews passed
538
562
  *and* GitHub will accept the merge.
539
563
 
540
564
  ```bash
541
- gh pr edit <N> --add-assignee @me --add-label merge-ready \
565
+ gh pr edit <N> ${HUMAN_USER:+--add-assignee} ${HUMAN_USER:+"$HUMAN_USER"} --add-label merge-ready \
542
566
  --remove-label ai-review --remove-label ai-ok-code --remove-label ai-ok-sec \
543
- ${AGENT_USER:+--remove-assignee "$AGENT_USER"}
567
+ ${AGENT_USER:+--remove-assignee} ${AGENT_USER:+"$AGENT_USER"}
544
568
  ```
545
569
 
546
570
  **`merge-ready` replaces the pass pair — it does not join it.** A handed-off PR
@@ -633,7 +657,12 @@ the label landed, in no *Assigned to you* view at all. A legacy sweep, cheap to
633
657
  keep and self-retiring once the last one is handled:
634
658
 
635
659
  ```bash
636
- gh pr edit <N> --add-assignee @me ${AGENT_USER:+--remove-assignee "$AGENT_USER"}
660
+ # Both empty (org repo, no agentUser) would leave `gh pr edit <N>` with no flags,
661
+ # which errors — so guard the call rather than trusting the reader to skip it.
662
+ if [ -n "$HUMAN_USER" ] || [ -n "$AGENT_USER" ]; then
663
+ gh pr edit <N> ${HUMAN_USER:+--add-assignee} ${HUMAN_USER:+"$HUMAN_USER"} \
664
+ ${AGENT_USER:+--remove-assignee} ${AGENT_USER:+"$AGENT_USER"}
665
+ fi
637
666
  ```
638
667
 
639
668
  Count it as `rev`. Idempotent, so it also picks up ones an earlier tick stranded.
@@ -777,10 +806,10 @@ Only then:
777
806
  REMOVED=1 # every removal in this pass sets this
778
807
  git -C "$ROOT" worktree remove --force "$WT_DIR" # the path found above, not a rebuilt one
779
808
  git -C "$ROOT" branch -D "$BRANCH" 2>/dev/null
780
- 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
781
810
  # Still OPEN means the PR said only `Refs #N`; a `Closes #N` issue is already closed.
782
- if [ "$(gh issue view <N> --json state -q .state)" = OPEN ]; then
783
- gh issue edit <N> --add-assignee @me
811
+ if [ -n "$HUMAN_USER" ] && [ "$(gh issue view <N> --json state -q .state)" = OPEN ]; then
812
+ gh issue edit <N> --add-assignee "$HUMAN_USER"
784
813
  fi
785
814
  ```
786
815
 
@@ -811,7 +840,7 @@ work must never be reaped out from under itself.
811
840
 
812
841
  | Stalled | Condition | Do |
813
842
  |---|---|---|
814
- | 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 --add-assignee @me ${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`) |
815
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 |
816
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 |
817
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`) |
@@ -962,6 +991,13 @@ whose pass-label is missing — `code-reviewer` if no `ai-ok-code`,
962
991
  carries `ai-reviewing-sec`. Both can run concurrently; launch them in a single
963
992
  message.
964
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
+
965
1001
  **Before spawning either, check whether it already posted.** A missing verdict
966
1002
  label does not mean the review is missing: on #497 both reviewers posted
967
1003
  complete reviews and then went idle, labelling nothing. Every review comment
@@ -1044,8 +1080,8 @@ intended; one that died after posting is now recovered instead of duplicated.
1044
1080
  issue. Apply the label immediately before the spawn, not after:
1045
1081
 
1046
1082
  ```bash
1047
- gh pr edit <N> --add-label ai-reviewing-code ${AGENT_USER:+--add-assignee "$AGENT_USER"} # then spawn code-reviewer
1048
- 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
1049
1085
  ```
1050
1086
 
1051
1087
  Assigning `AGENT_USER` on the claim is idempotent — both arms adding the same
@@ -1218,10 +1254,10 @@ and a blank line — naming what each round changed and why the reviewer kept ob
1218
1254
  then:
1219
1255
 
1220
1256
  ```bash
1221
- gh issue edit <M> --add-label ai-blocked --remove-label ai-wip --add-assignee @me \
1222
- ${AGENT_USER:+--remove-assignee "$AGENT_USER"}
1223
- gh pr edit <N> --add-assignee @me --remove-label ai-review \
1224
- ${AGENT_USER:+--remove-assignee "$AGENT_USER"}
1257
+ gh issue edit <M> --add-label ai-blocked --remove-label ai-wip \
1258
+ ${HUMAN_USER:+--add-assignee} ${HUMAN_USER:+"$HUMAN_USER"} ${AGENT_USER:+--remove-assignee} ${AGENT_USER:+"$AGENT_USER"}
1259
+ gh pr edit <N> --remove-label ai-review \
1260
+ ${HUMAN_USER:+--add-assignee} ${HUMAN_USER:+"$HUMAN_USER"} ${AGENT_USER:+--remove-assignee} ${AGENT_USER:+"$AGENT_USER"}
1225
1261
  ```
1226
1262
 
1227
1263
  Leave the worktree and PR in place for the human; a ping-pong stall is the case where
@@ -1231,7 +1267,7 @@ Otherwise **claim first, then spawn** — same shape as the reviewer claims abov
1231
1267
  and for the same reason. Apply the label immediately before the spawn, not after:
1232
1268
 
1233
1269
  ```bash
1234
- 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
1235
1271
  ```
1236
1272
 
1237
1273
  A fix round runs longer than a 15-minute tick — on #565, `ai-changes` at 17:35 and
@@ -1278,7 +1314,6 @@ gh api "repos/$OWNER_REPO/issues?labels=ai-ready&state=open" \
1278
1314
  | select([.labels[].name] | index("ai-wip") == null)
1279
1315
  | select([.labels[].name] | index("ai-blocked") == null)
1280
1316
  | select([.labels[].name] | index("holding") == null)
1281
- | select([.labels[].name] | index("ai-suggested") == null)
1282
1317
  | select(.author_association=="OWNER" or .author_association=="MEMBER" or .author_association=="COLLABORATOR")
1283
1318
  | {number, title, body}'
1284
1319
  ```
@@ -1293,9 +1328,11 @@ the first place, but then mislabelling it costs nothing. Unlike `ai-blocked` (an
1293
1328
  agent tried and got stuck), `holding` says *no agent should ever start*, and it
1294
1329
  shows up in the issue list so a human triaging does not re-litigate it either.
1295
1330
 
1296
- `ai-suggested` is excluded for a harder reason: it is an agent's own suggestion,
1297
- so picking one up would let the loop feed itself work — promoting one is a human
1298
- 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).
1299
1336
 
1300
1337
  **Declining an issue is a visible act — comment, never just skip.** Whenever an
1301
1338
  agent decides an issue should *not* go to the pipeline — triaging which issues to
@@ -1383,7 +1420,7 @@ concurrent tick can't double-pick:
1383
1420
 
1384
1421
  ```bash
1385
1422
  gh issue edit <N> --add-label ai-wip --remove-label ai-ready \
1386
- ${AGENT_USER:+--add-assignee "$AGENT_USER"}
1423
+ ${AGENT_USER:+--add-assignee} ${AGENT_USER:+"$AGENT_USER"}
1387
1424
  ```
1388
1425
 
1389
1426
  Assigning here is what makes the issue list honest: from this moment an agent
@@ -1432,7 +1469,7 @@ done)
1432
1469
  **Iterate line by line — never `for d in $DIRS`.** Your shell may be zsh, which
1433
1470
  does not word-split an unquoted expansion: `$DIRS` arrives as *one* word with
1434
1471
  embedded newlines, `[ -d ]` fails against that nonsense path, and the loop links
1435
- **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
1436
1473
  silent — the `pnpm install` fallback is gated on `$DIRS` being *empty*, which it
1437
1474
  is not, so the worktree gets neither links nor an install, and the implementer
1438
1475
  meets `Cannot find module` on its first test run, reading as the issue's fault
@@ -1567,8 +1604,9 @@ Then spawn a background implementer agent:
1567
1604
  > If you cannot finish, hand it back so a human can see it:
1568
1605
  >
1569
1606
  > ```bash
1570
- > gh issue edit <N> --add-label ai-blocked --remove-label ai-wip --add-assignee @me \
1571
- > <the orchestrator substitutes `--remove-assignee <AGENT_USER>` here, or nothing>
1607
+ > gh issue edit <N> --add-label ai-blocked --remove-label ai-wip \
1608
+ > <the orchestrator substitutes `--add-assignee <HUMAN_USER>` and
1609
+ > `--remove-assignee <AGENT_USER>` here, either or both possibly nothing>
1572
1610
  > ```
1573
1611
  >
1574
1612
  > Handing back means the issue stops being the agent's: the human must end up the
@@ -1600,7 +1638,8 @@ worktree in this pass. Never fall back to `EnterWorktree`.
1600
1638
  Never skip this pass, **including on an idle tick**. An unobservable loop is
1601
1639
  indistinguishable from a dead one.
1602
1640
 
1603
- 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).
1604
1643
  Middle dot separated, zero segments omitted, stall counts first with a `⚠`:
1605
1644
 
1606
1645
  | State | `SUMMARY` |
@@ -1613,11 +1652,19 @@ Middle dot separated, zero segments omitted, stall counts first with a `⚠`:
1613
1652
  Then diff against last tick and decide whether to notify:
1614
1653
 
1615
1654
  ```bash
1616
- STATUS=".claude/ai-loop-status"
1655
+ STATUS="$ROOT/.claude/ai-loop-status" # absolute — a pinned tick's cwd is a worktree
1617
1656
  PREV=$(head -1 "$STATUS" 2>/dev/null)
1618
- 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, -)
1619
1662
  ```
1620
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
+
1621
1668
  - **`SUMMARY` != `PREV`** → notify, and `IDLE=0`.
1622
1669
  - **`SUMMARY` == `idle`** → `IDLE=$((IDLE+1))`; notify **only when `IDLE` is
1623
1670
  exactly 4** (≈1h quiet), with `idle 1h — no ai-ready issues`. Exactly, not
@@ -1626,20 +1673,25 @@ IDLE=$(sed -n 2p "$STATUS" 2>/dev/null || echo 0)
1626
1673
 
1627
1674
  One notification per tick, maximum — the summary already says everything.
1628
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
+
1629
1686
  ```bash
1630
- 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
1631
1689
  ```
1632
1690
 
1633
- `osascript` is macOS-only, and the `|| true` is what makes shipping it portable:
1634
- elsewhere the tick still completes and only loses the desktop toast. On Linux
1635
- swap in `notify-send "ai-issue-loop" "$SUMMARY"` behind the same `|| true`. The
1636
- statusline file below is plain text and works anywhere.
1637
-
1638
- When `SUMMARY` carries a `⚠` (anything `blocked`, `ci-red`, or `rebuild`), append
1639
- `sound name "Basso"` so a stall is audibly different from routine progress.
1691
+ The statusline file below is plain text and works anywhere.
1640
1692
 
1641
1693
  Write the file **last** — summary, idle counter, and the sorted `ai-suggested`
1642
- numbers the digest rule above compares against:
1694
+ numbers the digest rule below compares against:
1643
1695
 
1644
1696
  ```bash
1645
1697
  printf '%s\n%s\n%s\n' "$SUMMARY" "$IDLE" "$SUGGESTED" > "$STATUS"
@@ -1658,12 +1710,11 @@ cleaned up, sent to review, picked up, blocked. Nothing else; this repeats every
1658
1710
  which ones need reading before they are merged — that is the one place the notes
1659
1711
  reach a human who is not already looking at GitHub.
1660
1712
 
1661
- **End with the triage digest** — the open `ai-suggested` queue, one line per
1662
- issue, straight from `gh issue list --label ai-suggested --state open --json
1663
- number,title`. No new state, no extra prose: a list scanned in one glance is what
1664
- makes a human promote or close something. Skip the digest when the queue is empty
1665
- or unchanged since the last tick (compare against a third line in `$STATUS`: the
1666
- 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).
1667
1718
 
1668
1719
  **The digest is a deadline, not an archive** — Pass 2 closes any item untouched
1669
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