@codedrifters/configulator 0.0.327 → 0.0.329

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/lib/index.mjs CHANGED
@@ -2252,6 +2252,55 @@ function renderSharedEditingRuleContent(se) {
2252
2252
  }
2253
2253
  }
2254
2254
  lines.push(
2255
+ "## Defer Shared-Index Commit to Final Pre-Push Step",
2256
+ "",
2257
+ "Even with the single-entry, deterministic-sort discipline above,",
2258
+ "two sessions that **prepare** their row inserts at the same time",
2259
+ "still race on push: whichever session pushes second sees the",
2260
+ "first session's commit on the remote and has to rebase its own",
2261
+ "commit on top, regenerating the same insert position calculation",
2262
+ "against a now-changed file. Repeated rebases multiply the chance",
2263
+ "of a mis-merge that silently drops a row.",
2264
+ "",
2265
+ "Sessions that produce both content and a shared-index row insert",
2266
+ "shrink this race window by deferring the index commit to the",
2267
+ "**final pre-push step** \u2014 after every content commit has landed",
2268
+ "locally, and immediately before `git push`:",
2269
+ "",
2270
+ "1. **Commit content first.** Write and commit every non-index",
2271
+ " change that the session produces (profile body, transcript",
2272
+ " extraction, requirement document, etc.) in its own focused",
2273
+ " commit or commits.",
2274
+ "2. **Rebase against the remote default branch** before touching",
2275
+ " the shared index:",
2276
+ "",
2277
+ " ```bash",
2278
+ " git fetch origin",
2279
+ ` git pull --${se.conflictStrategy} origin <default-branch>`,
2280
+ " ```",
2281
+ "",
2282
+ "3. **Re-read the shared index** from the now-up-to-date working",
2283
+ " tree. Another session may have appended a row while this",
2284
+ " session's content commits were in flight.",
2285
+ "4. **Re-compute the insert position** in declared sort order",
2286
+ " against the freshly-read rows. Do not assume the position",
2287
+ " computed earlier in the session is still correct.",
2288
+ "5. **Insert the row and commit the index edit on its own** \u2014",
2289
+ " one focused commit whose only file is the shared index. Run",
2290
+ " the commit-path verification (above) against that commit",
2291
+ " before continuing.",
2292
+ "6. **Push immediately.** The shorter the wall-clock gap between",
2293
+ " the rebase / re-read in step 2 and the push, the smaller the",
2294
+ " window in which another session can land a competing row.",
2295
+ "",
2296
+ "Per-agent workflows that touch a shared index file should call",
2297
+ "out this defer-to-final-commit sequence explicitly \u2014 see the",
2298
+ "`meeting-analyst` and `software-profile-analyst` sub-agent",
2299
+ "prompts for the canonical wording. The pattern is mechanical",
2300
+ "(rebase, re-read, re-compute, focused commit, push) rather than",
2301
+ "editorial, so the same recipe applies to every shared-index",
2302
+ "row-producing agent regardless of what content surrounds it.",
2303
+ "",
2255
2304
  "## Merge-Conflict Resolution",
2256
2305
  "",
2257
2306
  "When `git push` reports a conflict on a shared index file (two",
@@ -2299,9 +2348,14 @@ function renderSharedEditingBundleHook(se, bundleLabel) {
2299
2348
  "the latest default branch before editing, insert exactly one row",
2300
2349
  "in deterministic sort position, commit the index edit in its own",
2301
2350
  "focused commit, and verify the row is present in the commit",
2302
- "before pushing. See the `shared-editing-safety` rule for the",
2303
- "full protocol, the list of files covered, and the",
2304
- "merge-conflict resolution recipe."
2351
+ "before pushing. **Defer the index commit to the final pre-push",
2352
+ "step** \u2014 land every content commit first, then rebase against",
2353
+ "the remote default branch, re-read the index, re-compute the",
2354
+ "insert position, write the row, commit the index on its own,",
2355
+ "and push immediately. See the `shared-editing-safety` rule for",
2356
+ "the full protocol, the list of files covered, the",
2357
+ "defer-to-final-commit sequence, and the merge-conflict",
2358
+ "resolution recipe."
2305
2359
  ].join("\n");
2306
2360
  }
2307
2361
  function renderSharedEditingHelperScript(_se) {
@@ -9762,6 +9816,395 @@ var setIssueTypeProcedure = {
9762
9816
  `echo "$result" | jq -c '.data.updateIssueIssueType.issue'`
9763
9817
  ].join("\n")
9764
9818
  };
9819
+ var cleanMergedBranchesProcedure = {
9820
+ name: "clean-merged-branches.sh",
9821
+ description: "Analyse local branches and classify each as MERGED, UNMERGED, EMPTY, or SKIP_WORKTREE against a base branch (default: main). Analysis-only \u2014 never deletes branches. Handles squash merges via content equality.",
9822
+ content: [
9823
+ "#!/usr/bin/env bash",
9824
+ "# clean-merged-branches.sh \u2014 Analyse local branches against a base.",
9825
+ "#",
9826
+ "# Reports each local branch (other than HEAD and the base branch) as",
9827
+ "# MERGED, UNMERGED, EMPTY, or SKIP_WORKTREE. MERGED means every file the",
9828
+ "# branch added or modified now matches the base \u2014 so the branch content",
9829
+ "# is fully on the base even when the merge was a squash.",
9830
+ "#",
9831
+ "# This procedure is analysis-only. It never runs `git branch -D`. The",
9832
+ "# /clean-merged-branches slash-command skill wraps it with the deletion",
9833
+ "# prompt; the procedure itself is safe to invoke non-interactively from",
9834
+ "# any agent.",
9835
+ "#",
9836
+ "# Usage:",
9837
+ "# .claude/procedures/clean-merged-branches.sh # default base: main",
9838
+ "# .claude/procedures/clean-merged-branches.sh --base develop",
9839
+ "#",
9840
+ "# Output (one line per branch, stable format for grepping):",
9841
+ "# MERGED <branch> files=<n>",
9842
+ "# UNMERGED <branch> files=<n> differs=<n>",
9843
+ "# EMPTY <branch>",
9844
+ "# SKIP_WORKTREE <branch> worktree=<path>",
9845
+ "#",
9846
+ "# Exit codes:",
9847
+ "# 0 \u2014 analysis completed (whether or not any MERGED branches were found)",
9848
+ "# 2 \u2014 argument error",
9849
+ "# 3 \u2014 required command not on PATH",
9850
+ "# 4 \u2014 not a git repository",
9851
+ "",
9852
+ "set -uo pipefail",
9853
+ "",
9854
+ "# \u2500\u2500 helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
9855
+ "",
9856
+ "err() {",
9857
+ ' printf "clean-merged-branches.sh: %s\\n" "$*" >&2',
9858
+ "}",
9859
+ "",
9860
+ "usage() {",
9861
+ " cat >&2 <<'USAGE'",
9862
+ "Usage: clean-merged-branches.sh [--base <name>]",
9863
+ "",
9864
+ " --base <name> Base branch to compare against (default: main; falls",
9865
+ " back to whatever origin/HEAD points at if main is",
9866
+ " not present locally).",
9867
+ "",
9868
+ "Reports MERGED / UNMERGED / EMPTY / SKIP_WORKTREE for every local",
9869
+ "branch other than HEAD and the base branch. Does not delete anything.",
9870
+ "USAGE",
9871
+ "}",
9872
+ "",
9873
+ "# \u2500\u2500 argument parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
9874
+ "",
9875
+ 'base=""',
9876
+ "while [[ $# -gt 0 ]]; do",
9877
+ ' case "$1" in',
9878
+ " --base)",
9879
+ ' if [[ $# -lt 2 || -z "${2:-}" ]]; then',
9880
+ ' err "--base requires a value"',
9881
+ " usage",
9882
+ " exit 2",
9883
+ " fi",
9884
+ ' base="$2"',
9885
+ " shift 2",
9886
+ " ;;",
9887
+ " -h|--help)",
9888
+ " usage",
9889
+ " exit 0",
9890
+ " ;;",
9891
+ " *)",
9892
+ ' err "unknown argument: $1"',
9893
+ " usage",
9894
+ " exit 2",
9895
+ " ;;",
9896
+ " esac",
9897
+ "done",
9898
+ "",
9899
+ "# \u2500\u2500 dependency checks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
9900
+ "",
9901
+ "for cmd in git; do",
9902
+ ' if ! command -v "$cmd" >/dev/null 2>&1; then',
9903
+ ' err "required command not found on PATH: $cmd"',
9904
+ " exit 3",
9905
+ " fi",
9906
+ "done",
9907
+ "",
9908
+ "# \u2500\u2500 repo + base resolution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
9909
+ "",
9910
+ "if ! git rev-parse --git-dir >/dev/null 2>&1; then",
9911
+ ' err "not inside a git repository"',
9912
+ " exit 4",
9913
+ "fi",
9914
+ "",
9915
+ "# Default base = main if it exists locally, otherwise whatever",
9916
+ "# origin/HEAD points at (e.g. master, trunk, develop).",
9917
+ 'if [[ -z "$base" ]]; then',
9918
+ ' if git show-ref --verify --quiet "refs/heads/main"; then',
9919
+ ' base="main"',
9920
+ " else",
9921
+ " origin_head=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null || true)",
9922
+ ' if [[ -n "$origin_head" ]]; then',
9923
+ ' base="${origin_head#origin/}"',
9924
+ " else",
9925
+ ' base="main"',
9926
+ " fi",
9927
+ " fi",
9928
+ "fi",
9929
+ "",
9930
+ "# Verify the chosen base actually exists as a local branch.",
9931
+ 'if ! git show-ref --verify --quiet "refs/heads/$base"; then',
9932
+ ` err "base branch '$base' does not exist locally"`,
9933
+ " exit 4",
9934
+ "fi",
9935
+ "",
9936
+ "# Refresh remote-tracking refs and prune deleted upstream branches",
9937
+ "# so the gone-upstream signal is fresh (best effort \u2014 never fatal).",
9938
+ "git fetch --prune origin >/dev/null 2>&1 || true",
9939
+ "",
9940
+ 'current=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")',
9941
+ "",
9942
+ "# \u2500\u2500 worktree map (branch -> worktree path, excluding the current) \u2500\u2500\u2500",
9943
+ "",
9944
+ "# Use git worktree list --porcelain to detect any branch checked out in",
9945
+ "# a separate worktree. We must skip those because `git branch -D` would",
9946
+ "# refuse to delete a branch that is checked out elsewhere.",
9947
+ "#",
9948
+ "# We use parallel arrays (not an associative array) to stay compatible",
9949
+ "# with bash 3 \u2014 macOS still ships bash 3.2 as /bin/bash, and the script",
9950
+ "# is invoked via #!/usr/bin/env bash which picks whichever bash is",
9951
+ "# first on PATH. Lookups are O(n) but n is tiny (worktree count).",
9952
+ "worktree_branches=()",
9953
+ "worktree_paths=()",
9954
+ 'cwd_top=$(git rev-parse --show-toplevel 2>/dev/null || echo "")',
9955
+ 'wt_path=""',
9956
+ "while IFS= read -r line; do",
9957
+ ' case "$line" in',
9958
+ " worktree\\ *)",
9959
+ ' wt_path="${line#worktree }"',
9960
+ " ;;",
9961
+ " branch\\ *)",
9962
+ ' wt_branch="${line#branch refs/heads/}"',
9963
+ " # Skip the worktree that matches the current top-level \u2014 that one",
9964
+ " # is the *current* checkout; skipping HEAD is handled separately.",
9965
+ ' if [[ -n "$wt_path" && "$wt_path" != "$cwd_top" ]]; then',
9966
+ ' worktree_branches+=("$wt_branch")',
9967
+ ' worktree_paths+=("$wt_path")',
9968
+ " fi",
9969
+ ' wt_path=""',
9970
+ " ;;",
9971
+ ' "")',
9972
+ ' wt_path=""',
9973
+ " ;;",
9974
+ " esac",
9975
+ "done < <(git worktree list --porcelain 2>/dev/null)",
9976
+ "",
9977
+ "# Echo the worktree path for the given branch, or empty string if the",
9978
+ "# branch is not checked out in another worktree.",
9979
+ "lookup_worktree() {",
9980
+ ' local needle="$1"',
9981
+ " local i",
9982
+ ' for i in "${!worktree_branches[@]}"; do',
9983
+ ' if [[ "${worktree_branches[$i]}" == "$needle" ]]; then',
9984
+ ` printf '%s\\n' "\${worktree_paths[$i]}"`,
9985
+ " return 0",
9986
+ " fi",
9987
+ " done",
9988
+ " return 0",
9989
+ "}",
9990
+ "",
9991
+ "# \u2500\u2500 walk local branches \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
9992
+ "",
9993
+ 'echo "Analysing local branches against ${base}\u2026"',
9994
+ "echo",
9995
+ "",
9996
+ "while IFS= read -r branch; do",
9997
+ ' [[ -z "$branch" ]] && continue',
9998
+ ' [[ "$branch" == "$base" ]] && continue',
9999
+ ' [[ "$branch" == "$current" ]] && continue',
10000
+ "",
10001
+ ' wt=$(lookup_worktree "$branch")',
10002
+ ' if [[ -n "$wt" ]]; then',
10003
+ ` printf 'SKIP_WORKTREE %s worktree=%s\\n' "$branch" "$wt"`,
10004
+ " continue",
10005
+ " fi",
10006
+ "",
10007
+ ' mb=$(git merge-base "$base" "$branch" 2>/dev/null || true)',
10008
+ ' if [[ -z "$mb" ]]; then',
10009
+ " # No common ancestor with base \u2014 treat as UNMERGED (don't risk it).",
10010
+ ` printf 'UNMERGED %s files=? differs=?\\n' "$branch"`,
10011
+ " continue",
10012
+ " fi",
10013
+ "",
10014
+ ' files=$(git diff --name-only "$mb" "$branch" 2>/dev/null || true)',
10015
+ ' if [[ -z "$files" ]]; then',
10016
+ ` printf 'EMPTY %s\\n' "$branch"`,
10017
+ " continue",
10018
+ " fi",
10019
+ "",
10020
+ " # Count how many of those files still differ from the base today.",
10021
+ " # We use NUL-delimited git output to survive filenames with spaces.",
10022
+ " diff_count=0",
10023
+ " while IFS= read -r -d '' f; do",
10024
+ " diff_count=$((diff_count + 1))",
10025
+ ' done < <(git diff -z --name-only "$branch" "$base" -- $files 2>/dev/null)',
10026
+ "",
10027
+ ` total=$(echo "$files" | wc -l | tr -d ' ')`,
10028
+ "",
10029
+ ' if [[ "$diff_count" -eq 0 ]]; then',
10030
+ ` printf 'MERGED %s files=%s\\n' "$branch" "$total"`,
10031
+ " else",
10032
+ ` printf 'UNMERGED %s files=%s differs=%s\\n' "$branch" "$total" "$diff_count"`,
10033
+ " fi",
10034
+ "done < <(git branch --format='%(refname:short)')",
10035
+ "",
10036
+ "exit 0"
10037
+ ].join("\n")
10038
+ };
10039
+ var cleanMergedBranchesSkill = {
10040
+ name: "clean-merged-branches",
10041
+ description: "Report every local branch as MERGED or UNMERGED against the base (handles squash merges via content equality), then prompt the user to force-delete the MERGED list. Defaults base to main; --base overrides. Skips current branch, base branch, and any branch checked out in another worktree.",
10042
+ disableModelInvocation: true,
10043
+ userInvocable: true,
10044
+ platforms: { cursor: { exclude: true } },
10045
+ instructions: [
10046
+ "# Clean Merged Branches",
10047
+ "",
10048
+ "Identify local branches whose content is fully on the base branch \u2014",
10049
+ "including branches that were squash-merged (where `git branch -d`",
10050
+ "refuses because the branch commits are not ancestors of the base) \u2014",
10051
+ "report them to the user, and force-delete them after explicit",
10052
+ "confirmation.",
10053
+ "",
10054
+ "## Usage",
10055
+ "",
10056
+ "```",
10057
+ "/clean-merged-branches # default base: main",
10058
+ "/clean-merged-branches --base develop # custom base branch",
10059
+ "```",
10060
+ "",
10061
+ "### Flags",
10062
+ "",
10063
+ "- **`--base <name>`** \u2014 base branch to compare against. Defaults to",
10064
+ " `main`; falls back to whatever `origin/HEAD` points at if `main`",
10065
+ " does not exist locally.",
10066
+ "",
10067
+ "## What This Skill Does",
10068
+ "",
10069
+ "1. **Analyse.** Runs `.claude/procedures/clean-merged-branches.sh`",
10070
+ " (passing through any `--base` flag). The procedure walks every",
10071
+ " local branch and prints one stable log line per branch:",
10072
+ " - `MERGED <branch> files=<n>` \u2014 every file the branch added",
10073
+ " or modified now matches the base. Safe to delete.",
10074
+ " - `UNMERGED <branch> files=<n> differs=<n>` \u2014 branch content",
10075
+ " still differs from the base. Do NOT delete.",
10076
+ " - `EMPTY <branch>` \u2014 branch has no file changes vs. its",
10077
+ " merge-base. Safe to delete.",
10078
+ " - `SKIP_WORKTREE <branch> worktree=<path>` \u2014 branch is checked",
10079
+ " out in another worktree. Skipped (cannot be deleted while it's",
10080
+ " in use).",
10081
+ "",
10082
+ "2. **Confirm.** If the report turns up at least one `MERGED` (or",
10083
+ " `EMPTY`) branch, print the exact deletion list and a single",
10084
+ " `[y/N]` prompt. Empty input defaults to **no** and aborts.",
10085
+ "",
10086
+ "3. **Delete.** On explicit `y` / `Y`, run `git branch -D <branch>`",
10087
+ " for each confirmed branch and emit one `DELETED <branch>` line",
10088
+ " per success. On any other answer (including empty input),",
10089
+ " abort with `Aborted. No branches deleted.` and exit cleanly.",
10090
+ "",
10091
+ "If the report finds zero deletable branches, exit cleanly without",
10092
+ "prompting \u2014 there is nothing to confirm.",
10093
+ "",
10094
+ "## Behaviour Guarantees",
10095
+ "",
10096
+ "- **Always skip the current branch and the base branch.** The",
10097
+ " procedure enforces this; the skill never needs to filter again.",
10098
+ "- **Never delete without an explicit `y`.** The default on empty",
10099
+ " input is no. The skill never runs `git branch -D` until after",
10100
+ " the prompt returns `y` or `Y`.",
10101
+ "- **Content equality is the proof.** A branch classifies as",
10102
+ " `MERGED` when every file it added or modified matches the base",
10103
+ " today \u2014 handles squash merges, file deletions, and renames.",
10104
+ " The `[origin/<branch>: gone]` indicator is a useful secondary",
10105
+ " signal but is NOT the deletion gate.",
10106
+ "- **Safe across worktrees.** Branches checked out in another",
10107
+ " worktree are logged as `SKIP_WORKTREE` and excluded from the",
10108
+ " deletion list.",
10109
+ "",
10110
+ "## Output",
10111
+ "",
10112
+ "The procedure's per-branch log lines first, then (if any deletable",
10113
+ "branches exist) the confirmation prompt, then one `DELETED <branch>`",
10114
+ "line per successful deletion or `Aborted. No branches deleted.`",
10115
+ "on abort.",
10116
+ "",
10117
+ "## Implementation Recipe",
10118
+ "",
10119
+ "```bash",
10120
+ "# 1. Run the analysis-only procedure and capture its output.",
10121
+ 'report=$(.claude/procedures/clean-merged-branches.sh "$@")',
10122
+ 'printf "%s\\n" "$report"',
10123
+ "",
10124
+ "# 2. Extract the MERGED + EMPTY branch names from the report.",
10125
+ "mergeable=()",
10126
+ "while IFS= read -r line; do",
10127
+ ' case "$line" in',
10128
+ ' "MERGED "*|"EMPTY "*)',
10129
+ " # The branch name is the second whitespace-delimited token.",
10130
+ ` branch=$(echo "$line" | awk '{print $2}')`,
10131
+ ' [[ -n "$branch" ]] && mergeable+=("$branch")',
10132
+ " ;;",
10133
+ " esac",
10134
+ 'done <<< "$report"',
10135
+ "",
10136
+ "# 3. If nothing to delete, exit cleanly without prompting.",
10137
+ "if [[ ${#mergeable[@]} -eq 0 ]]; then",
10138
+ " echo",
10139
+ ' echo "No merged branches to delete."',
10140
+ " exit 0",
10141
+ "fi",
10142
+ "",
10143
+ "# 4. Show the list and prompt the user once.",
10144
+ "echo",
10145
+ 'echo "The following ${#mergeable[@]} branches are safe to delete:"',
10146
+ `printf ' %s\\n' "\${mergeable[@]}"`,
10147
+ "echo",
10148
+ 'read -r -p "Force-delete all ${#mergeable[@]} with git branch -D? [y/N] " answer',
10149
+ "",
10150
+ "# 5. On explicit y/Y only, delete each branch.",
10151
+ 'if [[ "$answer" == "y" || "$answer" == "Y" ]]; then',
10152
+ ' for b in "${mergeable[@]}"; do',
10153
+ ' if git branch -D "$b" >/dev/null 2>&1; then',
10154
+ ' echo "DELETED $b"',
10155
+ " else",
10156
+ ' echo "DELETE_FAILED $b" >&2',
10157
+ " fi",
10158
+ " done",
10159
+ "else",
10160
+ ' echo "Aborted. No branches deleted."',
10161
+ "fi",
10162
+ "```",
10163
+ "",
10164
+ "## Composability",
10165
+ "",
10166
+ "The procedure (`.claude/procedures/clean-merged-branches.sh`) is",
10167
+ "analysis-only and safe to invoke from any agent. The skill is the",
10168
+ "interactive entry point \u2014 use it when a human is at the keyboard.",
10169
+ "Background workers (orchestrator, maintenance-audit) should call",
10170
+ "the procedure directly and report its output without acting on it."
10171
+ ].join("\n"),
10172
+ referenceFiles: [
10173
+ {
10174
+ path: "evals/evals.json",
10175
+ content: JSON.stringify(
10176
+ {
10177
+ skill_name: "clean-merged-branches",
10178
+ evals: [
10179
+ {
10180
+ id: 1,
10181
+ prompt: "/clean-merged-branches",
10182
+ expected_output: "The skill runs the analysis-only `clean-merged-branches.sh` procedure against the current checkout's base branch (default: `main`). Each local branch other than HEAD and `main` is reported on one stable line as `MERGED`, `UNMERGED`, `EMPTY`, or `SKIP_WORKTREE` with a file count. When at least one branch classifies as `MERGED` or `EMPTY`, the skill prints the exact deletion list and prompts `Force-delete all <n> with git branch -D? [y/N]`. On explicit `y` / `Y` it force-deletes each branch with `git branch -D` and emits one `DELETED <branch>` line per success. Empty input or any other answer aborts with `Aborted. No branches deleted.` and no branches are deleted. When zero branches are deletable, the skill exits cleanly without prompting.",
10183
+ files: [],
10184
+ product_context_refs: []
10185
+ },
10186
+ {
10187
+ id: 2,
10188
+ prompt: "/clean-merged-branches --base develop",
10189
+ expected_output: "The skill forwards `--base develop` to the procedure, which compares every local branch (other than HEAD and `develop`) against `develop` instead of `main`. Branches whose content matches `develop` classify as `MERGED`; others as `UNMERGED`. The base-branch override is also reflected in the deletion prompt (`...with git branch -D?`), and the skill never deletes `develop` itself or the current HEAD. If `develop` does not exist as a local branch, the procedure exits non-zero with a clear diagnostic and the skill aborts without prompting.",
10190
+ files: [],
10191
+ product_context_refs: []
10192
+ },
10193
+ {
10194
+ id: 3,
10195
+ prompt: "/clean-merged-branches \u2014 I have a branch called feat/old-feature that I checked out in a sibling worktree under /tmp/work. Confirm it's skipped.",
10196
+ expected_output: "The procedure detects `feat/old-feature` via `git worktree list --porcelain` and reports `SKIP_WORKTREE feat/old-feature worktree=/tmp/work` instead of classifying it. The branch is excluded from the deletion list shown at the confirmation prompt. Even if the user answers `y`, the skill never runs `git branch -D feat/old-feature` because the branch is not in the mergeable list.",
10197
+ files: [],
10198
+ product_context_refs: []
10199
+ }
10200
+ ]
10201
+ },
10202
+ null,
10203
+ 2
10204
+ )
10205
+ }
10206
+ ]
10207
+ };
9765
10208
  var githubWorkflowBundle = {
9766
10209
  name: "github-workflow",
9767
10210
  description: "GitHub issue and PR workflow automation patterns",
@@ -9909,9 +10352,36 @@ var githubWorkflowBundle = {
9909
10352
  "- Delegate merge to the `pr-reviewer` sub-agent \u2014 do not merge manually and do not enable auto-merge directly"
9910
10353
  ].join("\n"),
9911
10354
  tags: ["workflow"]
10355
+ },
10356
+ {
10357
+ name: "branch-cleanup",
10358
+ description: "Local-branch hygiene helpers shipped with the github-workflow bundle, including the /clean-merged-branches skill for safely force-deleting branches whose content has already merged into the base (handles squash merges).",
10359
+ scope: AGENT_RULE_SCOPE.ALWAYS,
10360
+ content: [
10361
+ "# Branch Cleanup",
10362
+ "",
10363
+ "Local branches accumulate after every merged PR. In squash-merge",
10364
+ "repositories `git branch -d` refuses to delete them because the",
10365
+ "commit hash on the base differs, even when the branch content is",
10366
+ "fully merged. The `github-workflow` bundle ships two affordances",
10367
+ "that use content-equality (not commit-graph reachability) to",
10368
+ "identify branches safe to force-delete:",
10369
+ "",
10370
+ "- `/clean-merged-branches` \u2014 interactive slash-command skill that",
10371
+ " classifies every local branch, prompts for confirmation, then",
10372
+ " runs `git branch -D` on the confirmed list. See",
10373
+ " `.claude/skills/clean-merged-branches/SKILL.md` for usage,",
10374
+ " output format, and the squash-merge verification algorithm.",
10375
+ "- `.claude/procedures/clean-merged-branches.sh` \u2014 analysis-only",
10376
+ " procedure for non-interactive agent use (orchestrator,",
10377
+ " maintenance-audit). NEVER deletes \u2014 only reports `MERGED` /",
10378
+ " `UNMERGED` / `EMPTY` / `SKIP_WORKTREE` lines."
10379
+ ].join("\n"),
10380
+ tags: ["workflow"]
9912
10381
  }
9913
10382
  ],
9914
- procedures: [setIssueTypeProcedure]
10383
+ skills: [cleanMergedBranchesSkill],
10384
+ procedures: [setIssueTypeProcedure, cleanMergedBranchesProcedure]
9915
10385
  };
9916
10386
 
9917
10387
  // src/agent/bundles/industry-discovery.ts
@@ -11548,12 +12018,14 @@ function buildMeetingAnalystSubAgent(tier) {
11548
12018
  " Interest (signal threshold still applies); capture customer",
11549
12019
  " pain points as candidate BR (not FR).",
11550
12020
  "",
11551
- "5. **Maintain the `insights/` tree index.** Ensure",
11552
- " `<meetingsRoot>/insights/index.md` and",
12021
+ "5. **Plan the `insights/` tree index update \u2014 do not commit it",
12022
+ " yet.** Ensure `<meetingsRoot>/insights/index.md` and",
11553
12023
  " `<meetingsRoot>/insights/{type}/index.md` exist (create them",
11554
- " following the `section-index` convention if missing) and append",
11555
- " a row for the current meeting's basename if one is not already",
11556
- " present.",
12024
+ " following the `section-index` convention if missing). Note",
12025
+ " the basename row this phase will eventually append, but",
12026
+ " **defer** the row insert and its commit to step 8 below so",
12027
+ " the shared-index commit can land in the smallest possible",
12028
+ " window before push.",
11557
12029
  "",
11558
12030
  "6. **Create downstream phase issues** using `gh issue create`:",
11559
12031
  " - Always create a `meeting:notes` issue (blocked on this extract issue)",
@@ -11564,7 +12036,35 @@ function buildMeetingAnalystSubAgent(tier) {
11564
12036
  " - Always create a `meeting:link` issue \u2014 blocked on the draft issue if one",
11565
12037
  " was created, otherwise blocked on the notes issue",
11566
12038
  "",
11567
- "7. Commit, push, and close the extract issue.",
12039
+ "7. **Commit the extraction content first.** Stage and commit",
12040
+ " the `insights/{type}/<basename>.md` file (and any newly",
12041
+ " created `insights/index.md` / `insights/{type}/index.md`",
12042
+ " stub pages from step 5 that do not yet exist on the remote)",
12043
+ " in a single focused commit. **Do not push yet.**",
12044
+ "",
12045
+ "8. **Defer the shared-index row insert to a final pre-push",
12046
+ " commit.** Per the `shared-editing-safety` rule's",
12047
+ " **Defer Shared-Index Commit to Final Pre-Push Step**",
12048
+ " subsection, the index update for an existing",
12049
+ " `insights/{type}/index.md` is a **shared-index row insert**",
12050
+ " that races other parallel meeting sessions writing rows into",
12051
+ " the same partition file. Apply the deferred sequence:",
12052
+ "",
12053
+ " ```bash",
12054
+ " git fetch origin",
12055
+ " git pull --rebase origin <default-branch>",
12056
+ " ```",
12057
+ "",
12058
+ " Re-read `insights/{type}/index.md` from the now-up-to-date",
12059
+ " working tree, re-compute the alphabetical insert position",
12060
+ " for the current meeting's basename row (another session may",
12061
+ " have appended a sibling row in the meantime), insert exactly",
12062
+ " one row, and commit the index edit in its **own focused",
12063
+ " commit** whose only file is the shared index. Run the",
12064
+ " commit-path verification step (`git show HEAD:<index-path>`",
12065
+ " + grep count) against that commit before pushing.",
12066
+ "",
12067
+ "9. **Push and close.** Push the branch and close the extract issue.",
11568
12068
  "",
11569
12069
  "---",
11570
12070
  "",
@@ -11624,13 +12124,41 @@ function buildMeetingAnalystSubAgent(tier) {
11624
12124
  " - Action Items (table: who, what, when)",
11625
12125
  " - Open Questions",
11626
12126
  " - Follow-up items",
11627
- "4. **Maintain the `notes/` tree index.** Ensure",
11628
- " `<meetingsRoot>/notes/index.md` and",
12127
+ "4. **Plan the `notes/` tree index update \u2014 do not commit it",
12128
+ " yet.** Ensure `<meetingsRoot>/notes/index.md` and",
11629
12129
  " `<meetingsRoot>/notes/{type}/index.md` exist (create them",
11630
- " following the `section-index` convention if missing) and append",
11631
- " a row for the current meeting's basename if one is not already",
11632
- " present.",
11633
- "5. Commit, push, and close the notes issue.",
12130
+ " following the `section-index` convention if missing). Note",
12131
+ " the basename row this phase will eventually append, but",
12132
+ " **defer** the row insert and its commit to step 6 below so",
12133
+ " the shared-index commit can land in the smallest possible",
12134
+ " window before push.",
12135
+ "5. **Commit the notes content first.** Stage and commit the",
12136
+ " `notes/{type}/<basename>.md` file (and any newly created",
12137
+ " `notes/index.md` / `notes/{type}/index.md` stub pages from",
12138
+ " step 4 that do not yet exist on the remote) in a single",
12139
+ " focused commit. **Do not push yet.**",
12140
+ "6. **Defer the shared-index row insert to a final pre-push",
12141
+ " commit.** Per the `shared-editing-safety` rule's",
12142
+ " **Defer Shared-Index Commit to Final Pre-Push Step**",
12143
+ " subsection, the index update for an existing",
12144
+ " `notes/{type}/index.md` is a **shared-index row insert**",
12145
+ " that races other parallel meeting sessions writing rows",
12146
+ " into the same partition file. Apply the deferred sequence:",
12147
+ "",
12148
+ " ```bash",
12149
+ " git fetch origin",
12150
+ " git pull --rebase origin <default-branch>",
12151
+ " ```",
12152
+ "",
12153
+ " Re-read `notes/{type}/index.md` from the now-up-to-date",
12154
+ " working tree, re-compute the alphabetical insert position",
12155
+ " for the current meeting's basename row (another session may",
12156
+ " have appended a sibling row in the meantime), insert exactly",
12157
+ " one row, and commit the index edit in its **own focused",
12158
+ " commit** whose only file is the shared index. Run the",
12159
+ " commit-path verification step against that commit before",
12160
+ " pushing.",
12161
+ "7. Push and close the notes issue.",
11634
12162
  "",
11635
12163
  "---",
11636
12164
  "",
@@ -15272,12 +15800,25 @@ var issueWorkerSubAgent = {
15272
15800
  " `file` (and optional `line`). Track `comment_id` per item so you can",
15273
15801
  " report which items were handled and which (if any) failed.",
15274
15802
  "",
15275
- " **Synthetic rebase items.** A `comment_id` of",
15276
- " `synthetic:rebase-behind-main` is the reviewer's signal that the",
15277
- " PR's head branch is BEHIND the default branch with merge conflicts",
15278
- " that `gh pr update-branch` could not resolve. For this item only,",
15279
- " the work is **not** an editorial change \u2014 it is a rebase plus",
15280
- " conflict resolution. Run the following sequence:",
15803
+ " **Synthetic rebase items.** Two `comment_id` values flag the",
15804
+ " reviewer's signal that the PR's head branch is BEHIND the default",
15805
+ " branch with merge conflicts that `gh pr update-branch` could not",
15806
+ " resolve. For either item the work is **not** an editorial change",
15807
+ " \u2014 it is a rebase plus conflict resolution. The two ids select",
15808
+ " different recipes:",
15809
+ "",
15810
+ " - `synthetic:rebase-behind-main` \u2014 generic conflict. Resolve",
15811
+ " each conflicting file by hand (read both sides, reconcile,",
15812
+ " stage), then `git rebase --continue` until the rebase",
15813
+ " completes.",
15814
+ " - `synthetic:rebase-shared-index` \u2014 every conflicting file is a",
15815
+ " row-insert race on a shared-index file (registry / index /",
15816
+ " feature-matrix under a Starlight content root). Apply the",
15817
+ " explicit re-insert recipe carried in the item's `instruction`",
15818
+ " field \u2014 do **not** hand-merge.",
15819
+ "",
15820
+ " **For `synthetic:rebase-behind-main`.** Run the following",
15821
+ " sequence:",
15281
15822
  "",
15282
15823
  " ```bash",
15283
15824
  " git fetch origin",
@@ -15301,6 +15842,75 @@ var issueWorkerSubAgent = {
15301
15842
  " history. Push with a regular non-force `git push origin <branch>`",
15302
15843
  " and report the rebased head SHA as the worker's commit.",
15303
15844
  "",
15845
+ " **For `synthetic:rebase-shared-index`.** Apply the typed",
15846
+ " re-insert recipe step-by-step. Read the recipe verbatim from the",
15847
+ " item's `instruction` field; the steps below summarise the",
15848
+ " contract the reviewer encodes and the precondition guards you",
15849
+ " must enforce:",
15850
+ "",
15851
+ " 1. **Pull and rebase** onto the default branch:",
15852
+ "",
15853
+ " ```bash",
15854
+ " git fetch origin",
15855
+ " git pull --rebase origin {{repository.defaultBranch}}",
15856
+ " ```",
15857
+ "",
15858
+ " 2. **For each conflicting file**, inspect the conflict markers",
15859
+ " against the shared-index glob set and the precondition",
15860
+ " guards. The shared-index globs (sourced from the",
15861
+ " `shared-editing-safety` rule) are:",
15862
+ "",
15863
+ ...DEFAULT_SHARED_INDEX_PATHS.map((p) => ` - \`${p}\``),
15864
+ "",
15865
+ " **Precondition guards.** Before re-inserting, confirm every",
15866
+ " `<<<<<<<` / `=======` / `>>>>>>>` hunk in the file touches",
15867
+ " only data rows (lines starting with `| ` that are not the",
15868
+ " table header or `|---|---|` separator). If any hunk touches",
15869
+ " the frontmatter (lines between the opening / closing `---`",
15870
+ " fences), the page H1, surrounding prose paragraphs, the",
15871
+ " table header row, or the separator row, **stop**, run",
15872
+ " `git rebase --abort`, and record the item as `failed` with",
15873
+ " the structured marker `BLOCKED <reason>` (e.g.",
15874
+ " `BLOCKED conflict touches table header in <path>`). Do not",
15875
+ " hand-merge \u2014 the typed recipe applies only to pure",
15876
+ " row-insert races.",
15877
+ "",
15878
+ " When the guards pass, read the rebased version of the file",
15879
+ " (it now contains the other PR's row), extract this PR's row",
15880
+ " from the `<<<<<<<` side of the conflict markers, re-insert",
15881
+ " that row in declared sort order using the file's documented",
15882
+ " sort key (alphabetical on the first column by default), and",
15883
+ " stage the file:",
15884
+ "",
15885
+ " ```bash",
15886
+ " git add <path>",
15887
+ " ```",
15888
+ "",
15889
+ " 3. **Run the commit-path verification** for each re-inserted",
15890
+ " row. The marker must appear exactly once in the staged file",
15891
+ " (zero means missing, more than one means duplicated by a",
15892
+ " mis-merge):",
15893
+ "",
15894
+ " ```bash",
15895
+ ' count=$(git show :<path> | grep -Fc "<row-unique-marker>")',
15896
+ ' [ "$count" = "1" ] || { echo "BLOCKED verification failed for <path>"; exit 1; }',
15897
+ " ```",
15898
+ "",
15899
+ " 4. **Continue the rebase and push** with a non-force push once",
15900
+ " every file is staged and verified:",
15901
+ "",
15902
+ " ```bash",
15903
+ " git rebase --continue",
15904
+ " git push origin <branch>",
15905
+ " ```",
15906
+ "",
15907
+ " On any `BLOCKED` precondition failure, exit non-zero with the",
15908
+ " `BLOCKED <reason>` line, run `git rebase --abort`, record the",
15909
+ " item as `failed`, and proceed to the report step. The reviewer",
15910
+ " will see the `failed` outcome on the next pass and fall through",
15911
+ " to the human-required hand-off via `review:awaiting-human`. Do",
15912
+ " not force-push under any circumstance.",
15913
+ "",
15304
15914
  "4. When complete, prepare a short structured report (PR number, commit",
15305
15915
  " SHAs you will push, items handled by `comment_id`, items that failed",
15306
15916
  " to apply) \u2014 you will return this after Phase 6.",
@@ -15364,11 +15974,11 @@ var issueWorkerSubAgent = {
15364
15974
  "",
15365
15975
  "**Synthetic-rebase items skip the `fix(review)` commit.** When the",
15366
15976
  "fix-list contained a `comment_id` of `synthetic:rebase-behind-main`",
15367
- "and you completed the rebase in Phase 4, the rebased commit history",
15368
- "is itself the deliverable \u2014 there is no per-item editorial change to",
15369
- "wrap in a `fix(review)` commit. Skip `git add` / `git commit` /",
15370
- "`git pull --rebase` for that item and push the rebased branch",
15371
- "directly:",
15977
+ "or `synthetic:rebase-shared-index` and you completed the rebase in",
15978
+ "Phase 4, the rebased commit history is itself the deliverable \u2014",
15979
+ "there is no per-item editorial change to wrap in a `fix(review)`",
15980
+ "commit. Skip `git add` / `git commit` / `git pull --rebase` for",
15981
+ "that item and push the rebased branch directly:",
15372
15982
  "",
15373
15983
  "```bash",
15374
15984
  "git push origin <branch-name>",
@@ -16779,6 +17389,32 @@ var pnpmBundle = {
16779
17389
  ]
16780
17390
  };
16781
17391
 
17392
+ // src/agent/bundles/pr-review-policy.ts
17393
+ var DEFAULT_PATHS_EXEMPT_FROM_SIZE = [
17394
+ "docs/**"
17395
+ ];
17396
+ function resolvePrReviewPolicy(config) {
17397
+ const pathsExemptFromSize = config?.autoMerge?.pathsExemptFromSize ?? DEFAULT_PATHS_EXEMPT_FROM_SIZE;
17398
+ assertValidPathsExemptFromSize(pathsExemptFromSize);
17399
+ return {
17400
+ autoMerge: {
17401
+ pathsExemptFromSize: [...pathsExemptFromSize]
17402
+ }
17403
+ };
17404
+ }
17405
+ function validatePrReviewPolicyConfig(config) {
17406
+ return resolvePrReviewPolicy(config);
17407
+ }
17408
+ function assertValidPathsExemptFromSize(paths) {
17409
+ for (const path8 of paths) {
17410
+ if (typeof path8 !== "string" || path8.trim().length === 0) {
17411
+ throw new Error(
17412
+ "prReviewPolicy.autoMerge.pathsExemptFromSize entries must be non-empty strings"
17413
+ );
17414
+ }
17415
+ }
17416
+ }
17417
+
16782
17418
  // src/agent/bundles/pr-review.ts
16783
17419
  var prReviewerSubAgent = {
16784
17420
  name: "pr-reviewer",
@@ -16969,11 +17605,16 @@ var prReviewerSubAgent = {
16969
17605
  "### Step 2: Evaluate in precedence order",
16970
17606
  "",
16971
17607
  "Walk the following checks in order. The **first match wins** and fixes",
16972
- "the mode; record the triggering condition as the `reason`. Mixed-match",
16973
- "PRs (signals from both sides) resolve conservatively to",
16974
- "`human-required` \u2014 force-auto only wins when it is the single highest",
16975
- "match and no later human-required signal changes the outcome under",
16976
- "step 2c.",
17608
+ "the mode; record the triggering condition as the `reason` **and**",
17609
+ "record the numeric **`matched_rule`** index (one of `2`, `3`, `4`,",
17610
+ "`5`, or `6` for any human-required match \u2014 see the rule numbers",
17611
+ "below) so later phases can branch on which precedence rule fired.",
17612
+ "When rule 1 (force-auto label) or rule 7 (default) fixes the mode,",
17613
+ "record `matched_rule: null` \u2014 only the human-required rules carry an",
17614
+ "actionable index. Mixed-match PRs (signals from both sides) resolve",
17615
+ "conservatively to `human-required` \u2014 force-auto only wins when it is",
17616
+ "the single highest match and no later human-required signal changes",
17617
+ "the outcome under step 2c.",
16977
17618
  "",
16978
17619
  "1. **Force-auto label** \u2014 if the PR carries any label listed under",
16979
17620
  " `auto-merge.labels-that-force-auto` (e.g. `review:auto-ok`), set",
@@ -16990,9 +17631,18 @@ var prReviewerSubAgent = {
16990
17631
  " (fetched in Phase 2) matches any entry in",
16991
17632
  " `human-required.issue-types` (case-insensitive), set",
16992
17633
  " `mode = human-required`.",
16993
- "6. **Size thresholds** \u2014 if the PR exceeds either threshold under",
16994
- " `human-required.size` (`files` count or `insertions` count), set",
16995
- " `mode = human-required`.",
17634
+ "6. **Size thresholds** \u2014 first read",
17635
+ " `auto-merge.paths-exempt-from-size` from the policy (defaults to",
17636
+ " the documented carve-out list). For every file in the PR diff,",
17637
+ " evaluate whether the file path matches at least one glob in the",
17638
+ " carve-out list. If **every** changed path matches the carve-out,",
17639
+ " **skip rule #6** entirely and continue to rule #7 \u2014 the PR is",
17640
+ " exempt from the size threshold regardless of its `files` or",
17641
+ " `insertions` count. Otherwise (any changed path falls outside",
17642
+ " the carve-out list), apply the size check: if the PR exceeds",
17643
+ " either threshold under `human-required.size` (`files` count or",
17644
+ " `insertions` count), set `mode = human-required` and record the",
17645
+ " triggered axis (files vs. insertions) as the reason.",
16996
17646
  "7. **Default** \u2014 if no rule above matched, apply the `default` field",
16997
17647
  " from the policy (typically `auto-merge`).",
16998
17648
  "",
@@ -17006,14 +17656,23 @@ var prReviewerSubAgent = {
17006
17656
  "",
17007
17657
  "### Step 3: Record the decision",
17008
17658
  "",
17009
- "Persist the evaluated mode and reason for later phases so Phase 4 and",
17010
- "any downstream summary writer can cite it:",
17659
+ "Persist the evaluated mode, the reason, and the matched-rule index",
17660
+ "for later phases so Phase 4 and any downstream summary writer can",
17661
+ "cite them:",
17011
17662
  "",
17012
17663
  "```",
17013
17664
  "Review mode: <auto-merge | human-required>",
17014
17665
  "Reason: <short explanation \u2014 label name, path+glob, issue type, size threshold, default>",
17666
+ "Matched rule: <2 | 3 | 4 | 5 | 6 | null>",
17015
17667
  "```",
17016
17668
  "",
17669
+ "`Matched rule` is the numeric index of the precedence rule that",
17670
+ "fixed the mode in Step 2. It is populated only for human-required",
17671
+ "matches (rules 2\u20136) \u2014 force-auto (rule 1) and default (rule 7)",
17672
+ "record `null`. Phase 4's `gh pr update-branch` gate consults this",
17673
+ "field to decide whether the size-only carve-out (rule 6) allows the",
17674
+ "bot to keep a human-required branch fresh against the default branch.",
17675
+ "",
17017
17676
  "Phases 3 (acceptance-criteria comparison) and CI verification run",
17018
17677
  "unchanged regardless of mode. Only the terminal action in Phase 4",
17019
17678
  "branches on the decided mode.",
@@ -17465,9 +18124,42 @@ var prReviewerSubAgent = {
17465
18124
  "gh pr view <pr-number> --json mergeStateStatus --jq '.mergeStateStatus'",
17466
18125
  "```",
17467
18126
  "",
17468
- "When the value is `BEHIND`, attempt to bring the head branch current with",
17469
- "the default branch via `gh pr update-branch` (default merge strategy \u2014",
17470
- "**never** `--rebase`, which would rewrite commits on a published branch):",
18127
+ "Before running `gh pr update-branch`, evaluate the **eligibility",
18128
+ "gate** below. The step runs when `mergeStateStatus == BEHIND` **AND**",
18129
+ "either of the following holds:",
18130
+ "",
18131
+ "- The review mode decided in Phase 2.75 is `auto-merge`, **or**",
18132
+ "- The review mode is `human-required` **and** `matched_rule == 6`",
18133
+ " (size threshold is the sole trigger that fixed the mode).",
18134
+ "",
18135
+ "All other `human-required` matches (rules 2\u20135) continue to block",
18136
+ "`update-branch`. Concretely, **never** run `gh pr update-branch`",
18137
+ "when the mode is `human-required` and any of the following fired:",
18138
+ "",
18139
+ "- rule 2 (`review:human-required` label),",
18140
+ "- rule 3 (any `labels-that-force-human` label such as `priority:critical`),",
18141
+ "- rule 4 (`human-required.paths` glob match), or",
18142
+ "- rule 5 (`human-required.issue-types` match).",
18143
+ "",
18144
+ "The rationale: rule 6 fires on the **volume** of the diff alone \u2014",
18145
+ "there is nothing about the changed paths or labels that suggests a",
18146
+ "human reviewer has explicit ownership of the branch's lifecycle.",
18147
+ "Pushing the default branch into a size-tripped human-required PR",
18148
+ "keeps it from sitting stale while the human is still drafting",
18149
+ "their review. By contrast, each of rules 2\u20135 signals a human",
18150
+ "reviewer who owns the branch's lifecycle; silently pushing main",
18151
+ "into those PRs expands the diff under review without their consent.",
18152
+ "",
18153
+ "When the gate **denies** `update-branch` (`human-required` mode and",
18154
+ "`matched_rule` in 2\u20135), record",
18155
+ "`Branch updated: not eligible (human-required by rule <N>)` and skip",
18156
+ "the rest of this sub-section. The human reviewer keeps branch-",
18157
+ "lifecycle ownership.",
18158
+ "",
18159
+ "When the gate **permits** `update-branch`, attempt to bring the head",
18160
+ "branch current with the default branch via `gh pr update-branch`",
18161
+ "(default merge strategy \u2014 **never** `--rebase`, which would rewrite",
18162
+ "commits on a published branch):",
17471
18163
  "",
17472
18164
  "```bash",
17473
18165
  "gh pr update-branch <pr-number>",
@@ -17476,8 +18168,9 @@ var prReviewerSubAgent = {
17476
18168
  "Branch on the outcome:",
17477
18169
  "",
17478
18170
  "- **Success** \u2014 record `Branch updated: yes` for the per-PR report and",
17479
- " stop. Auto-merge will fire when required checks pass on the new head",
17480
- " SHA. Do **not** poll for the merge here \u2014 Phase 5 owns polling.",
18171
+ " stop. Auto-merge (when enabled) will fire when required checks pass",
18172
+ " on the new head SHA. Do **not** poll for the merge here \u2014 Phase 5",
18173
+ " owns polling.",
17481
18174
  "- **Failure for reasons other than a merge conflict** (permission",
17482
18175
  " denied, branch protection refusing the merge commit, transient",
17483
18176
  " network error) \u2014 record `Branch updated: failed (<reason>)`, post a",
@@ -17491,13 +18184,6 @@ var prReviewerSubAgent = {
17491
18184
  "an `update-branch` attempt \u2014 every other state either has nothing to do",
17492
18185
  "or is already gated on a different signal that Phase 5 picks up.",
17493
18186
  "",
17494
- "Never run `gh pr update-branch` on a PR whose review mode is",
17495
- "`human-required`. Pushing main into a human-required PR expands the",
17496
- "scope of the diff the human is reviewing without their consent. The",
17497
- "`update-branch` step lives **only** under the `Mode auto-merge` branch",
17498
- "of this phase \u2014 `human-required` skips straight to its hand-off block",
17499
- "below.",
17500
- "",
17501
18187
  "##### Conflict-resolution delegation (BEHIND + conflicts)",
17502
18188
  "",
17503
18189
  "When `gh pr update-branch <pr-number>` fails because the merge would",
@@ -17511,9 +18197,13 @@ var prReviewerSubAgent = {
17511
18197
  "fall through to the fallback at the end of this sub-section instead.",
17512
18198
  "",
17513
18199
  "1. **Review mode is `auto-merge`.** Never delegate conflict",
17514
- " resolution on `human-required` PRs \u2014 pushing main resolutions into",
17515
- " them expands the diff the human is reviewing. (This is the same",
17516
- " reason the `update-branch` step itself is auto-merge-only.)",
18200
+ " resolution on `human-required` PRs \u2014 pushing worker-resolved",
18201
+ " merge content into them expands the diff under review without",
18202
+ " the human reviewer's consent. (Unlike the `update-branch` step",
18203
+ " above, which permits a size-only `human-required` carve-out, the",
18204
+ " conflict-resolution delegation flow is auto-merge-only across",
18205
+ " the board: a worker rebase push is a stronger branch mutation",
18206
+ " than the merge-commit `gh pr update-branch` performs.)",
17517
18207
  "2. **Delegation invocation guard permits the hand-off** \u2014 the PR",
17518
18208
  " carries the `origin:issue-worker` label, **or** the reviewer was",
17519
18209
  " invoked with `--allow-human-author`. The same guard used for the",
@@ -17538,8 +18228,53 @@ var prReviewerSubAgent = {
17538
18228
  " and conflicts there should be resolved by re-running synth, not by",
17539
18229
  " merging the conflict markers.",
17540
18230
  "",
17541
- "When every guard above passes, hand off to `issue-worker` with a",
17542
- "**single synthetic fix-list item** that describes the rebase work:",
18231
+ "When every guard above passes, **classify each conflicting file**",
18232
+ "before composing the fix-list. The classification picks one of two",
18233
+ "typed recipes \u2014 a precise `shared-index` resolver when every",
18234
+ "conflict is a row-insert race on a registry / index / feature-matrix",
18235
+ "file, or the generic rebase recipe in every other case.",
18236
+ "",
18237
+ "**Classification step.** For each conflicting path reported by the",
18238
+ "failed `update-branch`, decide whether the file is `shared-index` or",
18239
+ "`generic` against these criteria (the shared-index glob set comes",
18240
+ "from the `shared-editing-safety` rule \u2014 see the bundle's",
18241
+ "`shared-editing.ts` for the canonical constant):",
18242
+ "",
18243
+ "- `shared-index` \u2014 the path matches one of the shared-editing glob",
18244
+ " patterns:",
18245
+ ...DEFAULT_SHARED_INDEX_PATHS.map((p) => ` - \`${p}\``),
18246
+ " **AND** the conflict diff is bounded to row insertions only.",
18247
+ " Inspect the merge conflict diff for the file: every `<<<<<<<` /",
18248
+ " `=======` / `>>>>>>>` hunk must touch only data rows (lines that",
18249
+ " start with `| ` and are not the table header or the `|---|---|`",
18250
+ " separator). The conflict must **not** touch:",
18251
+ " - The frontmatter block (lines between the opening and",
18252
+ " closing `---` fences at the top of the file).",
18253
+ " - The page H1 or any surrounding prose paragraphs.",
18254
+ " - The table header row (the first `| Column | ... |` row of",
18255
+ " any table).",
18256
+ " - The separator row (`|---|---|...|`).",
18257
+ " If the conflict hunks touch any of the above, classify the file",
18258
+ " as `generic` \u2014 the mechanical row-insert recipe cannot safely",
18259
+ " reconcile structural edits.",
18260
+ "- `generic` \u2014 anything else (path outside the shared-editing glob",
18261
+ " set, **or** conflict hunks touch the frontmatter / header /",
18262
+ " separator / surrounding prose).",
18263
+ "",
18264
+ "**Branching emit step.**",
18265
+ "",
18266
+ "- **Every conflicting file is `shared-index`** \u2014 emit the typed",
18267
+ ' fix-list item with `comment_id: "synthetic:rebase-shared-index"`',
18268
+ " carrying the explicit re-insert recipe (see step 3 below). The",
18269
+ " worker mechanically re-inserts each row in declared sort order",
18270
+ " against the rebased file.",
18271
+ "- **Any conflicting file is `generic`** \u2014 emit the existing generic",
18272
+ ' fix-list item with `comment_id: "synthetic:rebase-behind-main"`',
18273
+ " (see step 3 below). The worker performs a hand-merge of the",
18274
+ " conflict markers.",
18275
+ "",
18276
+ "Hand off to `issue-worker` with the chosen single synthetic",
18277
+ "fix-list item:",
17543
18278
  "",
17544
18279
  "1. **Disable auto-merge** so a fast CI pass cannot land the PR",
17545
18280
  " mid-delegation (idempotent \u2014 safe no-op when auto-merge was never",
@@ -17558,9 +18293,18 @@ var prReviewerSubAgent = {
17558
18293
  " ```",
17559
18294
  "",
17560
18295
  "3. **Post a fix-list comment** containing exactly one synthetic item",
17561
- " describing the rebase. The `comment_id` is the literal string",
17562
- " `synthetic:rebase-behind-main` so the worker recognises the",
17563
- " special case and the next reviewer pass can identify the item:",
18296
+ " describing the rebase. The `comment_id` field selects the typed",
18297
+ " recipe:",
18298
+ "",
18299
+ " - `synthetic:rebase-behind-main` \u2014 generic conflict; the worker",
18300
+ " resolves conflicting hunks by hand.",
18301
+ " - `synthetic:rebase-shared-index` \u2014 every conflict is a pure",
18302
+ " row-insert race on a shared-index file; the worker re-inserts",
18303
+ " each row in declared sort order against the rebased file.",
18304
+ "",
18305
+ " The next reviewer pass identifies the item by its `comment_id`.",
18306
+ "",
18307
+ " **Generic shape (`synthetic:rebase-behind-main`):**",
17564
18308
  "",
17565
18309
  " ```markdown",
17566
18310
  " ## Reviewer: fix list for @issue-worker",
@@ -17579,6 +18323,27 @@ var prReviewerSubAgent = {
17579
18323
  " ```",
17580
18324
  " ```",
17581
18325
  "",
18326
+ " **Typed shape (`synthetic:rebase-shared-index`).** The",
18327
+ " `instruction` field carries the full re-insert recipe \u2014 the",
18328
+ " worker reads it imperatively, so spell every step out:",
18329
+ "",
18330
+ " ```markdown",
18331
+ " ## Reviewer: fix list for @issue-worker",
18332
+ "",
18333
+ " - [ ] @reviewer \u2014 rebase onto origin/<default-branch> and re-insert shared-index rows in: <space-separated list of conflicting files>",
18334
+ "",
18335
+ " ```json fix-list",
18336
+ " {",
18337
+ ' "pr": <pr-number>,',
18338
+ ' "branch": "<head-ref-name>",',
18339
+ ' "generated_at": "<ISO-8601 timestamp>",',
18340
+ ' "items": [',
18341
+ ' {"comment_id": "synthetic:rebase-shared-index", "author": "reviewer", "file": "<first-conflicting-file>", "instruction": "Branch is BEHIND default-branch with shared-index row-insert conflicts only. Apply this recipe: (1) Fetch and rebase: `git fetch origin && git pull --rebase origin <default-branch>`. (2) For each conflicting shared-index file: read the rebased version (it now contains the other PR\'s row), extract this PR\'s row from the `<<<<<<<` side of the conflict markers, re-insert that row in declared sort order using the file\'s documented sort key (alphabetical on the first column by default), stage the file with `git add <path>`. (3) Run the commit-path verification for each row: `count=$(git show :<path> | grep -Fc <row-unique-marker>) && [ \\"$count\\" = \\"1\\" ] || exit 1`. (4) `git rebase --continue` and push with non-force `git push origin <branch>`. (5) If any conflict marker touches the frontmatter, table header row, `|---|---|` separator, or surrounding prose, abort the recipe and emit `BLOCKED <reason>` \u2014 the precondition guards from the reviewer\'s classification step were violated."}',
18342
+ " ]",
18343
+ " }",
18344
+ " ```",
18345
+ " ```",
18346
+ "",
17582
18347
  "4. **Invoke `issue-worker` in feedback mode** with the same prompt",
17583
18348
  " shape used by the in-scope-fix flow: include the literal phrase",
17584
18349
  " `feedback mode: PR #<n>` plus the repository identifier",
@@ -17623,7 +18388,16 @@ var prReviewerSubAgent = {
17623
18388
  " gh pr edit <pr-number> --add-label 'review:awaiting-human'",
17624
18389
  " ```",
17625
18390
  "",
17626
- "2. Exit cleanly after the acceptance-criteria check completes and any",
18391
+ "2. **If `matched_rule == 6`** (size threshold was the sole trigger),",
18392
+ " run the `Update the branch when `mergeStateStatus` is `BEHIND``",
18393
+ " step from the `Mode auto-merge` branch above before exiting. The",
18394
+ " eligibility gate documented in that sub-section explicitly permits",
18395
+ " `gh pr update-branch` on size-only human-required PRs so the bot",
18396
+ " keeps the branch fresh against the default branch while the human",
18397
+ " reviews. Skip this sub-step for `matched_rule` in 2\u20135 \u2014 the gate",
18398
+ " denies `update-branch` there and the human owns branch-lifecycle.",
18399
+ "",
18400
+ "3. Exit cleanly after the acceptance-criteria check completes and any",
17627
18401
  " summary comment the reviewer posts. Proceed to Phase 5 only if a",
17628
18402
  " merge occurred \u2014 in `human-required` mode the reviewer stops at",
17629
18403
  " the hand-off and does not poll for merge.",
@@ -17868,13 +18642,20 @@ var prReviewerSubAgent = {
17868
18642
  " AC-drift pushback, any failed-fix pushback, and any human-required",
17869
18643
  " hand-off all keep auto-merge disabled until the human resolves",
17870
18644
  " the underlying state.",
17871
- "15. **Never run `gh pr update-branch` on a `human-required` PR.**",
17872
- " Pushing main into a human-required PR expands the scope of the",
17873
- " diff the human is reviewing without their consent. The",
17874
- " `update-branch` step is gated to the `Mode auto-merge` branch of",
17875
- " Phase 4 only. The same restriction applies to delegating",
18645
+ "15. **Restrict `gh pr update-branch` on `human-required` PRs.** The",
18646
+ " `update-branch` step is permitted when the review mode is",
18647
+ " `auto-merge`, **or** when the mode is `human-required` **and**",
18648
+ " Phase 2.75's `matched_rule == 6` (size threshold was the sole",
18649
+ " trigger). All other human-required matches (rule 2 force-human",
18650
+ " label, rule 3 listed force-human label, rule 4 path glob, rule 5",
18651
+ " issue type) continue to block `update-branch`: pushing main into",
18652
+ " those PRs expands the diff under review without the human",
18653
+ " reviewer's consent. The same restriction applies to delegating",
17876
18654
  " conflict resolution to `issue-worker`: never delegate a rebase",
17877
- " on a `human-required` PR.",
18655
+ " on a `human-required` PR regardless of `matched_rule` \u2014 the",
18656
+ " typed-recipe delegation flow stays auto-merge-only because a",
18657
+ " worker push to a human-required branch is a stronger mutation",
18658
+ " than the merge-commit `gh pr update-branch` performs.",
17878
18659
  "16. **Never delegate conflict resolution involving generated or",
17879
18660
  " projen-managed files.** When `gh pr update-branch` fails on a",
17880
18661
  " BEHIND PR with conflicts and any conflicting path is a lockfile,",
@@ -18042,364 +18823,426 @@ var reviewPrsSkill = {
18042
18823
  "comment on that PR and continue with the next."
18043
18824
  ].join("\n")
18044
18825
  };
18045
- var prReviewBundle = {
18046
- name: "pr-review",
18047
- description: "Pull request review workflow: verifies PRs against their linked issues' acceptance criteria and orchestrates squash-merge, single or looped over all eligible PRs",
18048
- // Default-apply: the PR review workflow is safe to include everywhere,
18049
- // and keeping review/merge policy centralised in the pr-reviewer agent
18050
- // means consumers get consistent behaviour out of the box. Consumers can
18051
- // still exclude it explicitly via `excludeBundles` if desired.
18052
- appliesWhen: () => true,
18053
- rules: [
18054
- {
18055
- name: "pr-review-policy",
18056
- description: "Declarative policy that tells the pr-reviewer which PRs may auto-merge and which must wait for a human reviewer",
18057
- scope: AGENT_RULE_SCOPE.ALWAYS,
18058
- content: [
18059
- "# PR Review Policy",
18060
- "",
18061
- "The `pr-reviewer` sub-agent evaluates every PR it reviews against the",
18062
- "policy below and routes the PR into one of two modes:",
18063
- "",
18064
- "- **`auto-merge`** \u2014 the reviewer may enable squash auto-merge once",
18065
- " all acceptance criteria are met and CI is green.",
18066
- "- **`human-required`** \u2014 the reviewer runs the full AC/CI check but",
18067
- " never calls `gh pr merge --auto`. It applies the",
18068
- " `review:awaiting-human` label and hands off to a human reviewer.",
18069
- "",
18070
- "## Policy",
18071
- "",
18072
- "```yaml",
18073
- "version: 1",
18074
- "default: auto-merge",
18075
- "",
18076
- "human-required:",
18077
- " paths:",
18078
- ' - "docs/src/content/docs/requirements/architectural-decisions/**"',
18079
- ' - "docs/src/content/docs/project-context.md"',
18080
- ' - ".github/workflows/**"',
18081
- ' - ".github/CODEOWNERS"',
18082
- ' - ".projenrc.ts"',
18083
- ' - "projenrc/**"',
18084
- ' - "CLAUDE.md"',
18085
- ' - ".claude/**"',
18086
- ' - "packages/**/package.json"',
18087
- " issue-types:",
18088
- " - release",
18089
- " - hotfix",
18090
- " size:",
18091
- " files: 10",
18092
- " insertions: 500",
18093
- " labels-that-force-human:",
18094
- ' - "review:human-required"',
18095
- ' - "priority:critical"',
18096
- "",
18097
- "auto-merge:",
18098
- " labels-that-force-auto:",
18099
- ' - "review:auto-ok"',
18100
- "```",
18101
- "",
18102
- "## Precedence",
18103
- "",
18104
- "The reviewer walks the following checks in order. The **first match**",
18105
- "fixes the mode; any mixed-match PR (signals from both sides) resolves",
18106
- "conservatively to `human-required` \u2014 `auto-merge` only wins when the",
18107
- "force-auto label is the single top-priority match.",
18108
- "",
18109
- "1. **`auto-merge.labels-that-force-auto`** \u2014 if the PR carries any of",
18110
- " these labels (e.g. `review:auto-ok`), the mode is `auto-merge`",
18111
- " outright. This is the only escape hatch from the conservative",
18112
- " default; it requires a maintainer to apply the label explicitly.",
18113
- "2. **`review:human-required` label** \u2014 reserved force-human label;",
18114
- " if present (and no force-auto label beat it in step 1), the mode",
18115
- " is `human-required`.",
18116
- "3. **`human-required.labels-that-force-human`** \u2014 any listed label on",
18117
- " the PR (e.g. `priority:critical`) forces `human-required`.",
18118
- "4. **`human-required.paths`** \u2014 if any file in the PR diff matches",
18119
- " any glob here, the mode is `human-required`. Matching uses",
18120
- " standard glob semantics (`**` for recursive directories,",
18121
- " `*` for a single path segment).",
18122
- "5. **`human-required.issue-types`** \u2014 if the linked issue's GitHub",
18123
- " issue type matches any entry (case-insensitive), the mode is",
18124
- " `human-required`.",
18125
- "6. **`human-required.size`** \u2014 if the PR exceeds either the `files`",
18126
- " count or the `insertions` count, the mode is `human-required`.",
18127
- "7. **`default`** \u2014 applied only when no rule above matched",
18128
- " (normally `auto-merge`).",
18129
- "",
18130
- "The `pr-reviewer` sub-agent records the decided mode and the triggering",
18131
- "reason in its Phase 2.75 output so downstream phases and any sticky",
18132
- "summary can cite the specific rule that applied."
18133
- ].join("\n"),
18134
- tags: ["policy", "review"]
18135
- },
18136
- {
18137
- name: "pr-review-workflow",
18138
- description: "Describes the /review-pr and /review-prs skills and their delegation to the pr-reviewer sub-agent",
18139
- scope: AGENT_RULE_SCOPE.ALWAYS,
18140
- content: [
18141
- "# PR Review Workflow",
18142
- "",
18143
- "Two skills are available, both backed by the same `pr-reviewer`",
18144
- "sub-agent:",
18145
- "",
18146
- "- **`/review-pr <pr-number>`** \u2014 review a single targeted PR.",
18147
- "- **`/review-prs`** \u2014 loop over every eligible open PR in the",
18148
- " repository and review each one in turn.",
18149
- "",
18150
- "The `pr-reviewer` sub-agent:",
18151
- "",
18152
- "1. Runs a pre-flight eligibility filter (mergeable, CI not failing,",
18153
- " has a linked issue). Ineligible PRs get a short comment and are",
18154
- " skipped.",
18155
- "2. Fetches the PR, its diff, CI status, and the linked issue",
18156
- "3. **Evaluates the PR Review Policy** (see the `PR Review Policy`",
18157
- " section above) to decide whether the PR is `auto-merge` or",
18158
- " `human-required`, and records the triggering reason",
18159
- "4. Builds a checklist from the issue's acceptance criteria",
18160
- "5. Verifies the diff satisfies each criterion and that CI is green",
18161
- "6. **Enables squash auto-merge** (with `--delete-branch`) when all",
18162
- " checks pass **and** the review mode is `auto-merge`",
18163
- "7. **Applies `review:awaiting-human`** and hands off to a human",
18164
- " reviewer when the review mode is `human-required` (no auto-merge,",
18165
- " even if every acceptance criterion is met)",
18166
- "8. **Comments with grouped findings** when any check fails (plain",
18167
- " `gh pr comment`, not a formal `--request-changes` review)",
18168
- "9. After a successful merge, verifies the linked issue is closed",
18169
- " and closes it explicitly if the merge commit did not",
18170
- "10. Cleans up the local branch after merge",
18171
- "",
18172
- "The reviewer **never** implements code and **never** pushes commits",
18173
- "to a PR's branch \u2014 it only reviews, decides, and orchestrates merge",
18174
- "or comment. In loop mode, a failed review for one PR never stops",
18175
- "the loop; the reviewer comments and moves on. See the `pr-reviewer`",
18176
- "agent definition for the full phase-by-phase contract."
18177
- ].join("\n"),
18178
- platforms: {
18179
- cursor: { exclude: true }
18826
+ function buildPrReviewBundle(policy = resolvePrReviewPolicy()) {
18827
+ return {
18828
+ name: "pr-review",
18829
+ description: "Pull request review workflow: verifies PRs against their linked issues' acceptance criteria and orchestrates squash-merge, single or looped over all eligible PRs",
18830
+ // Default-apply: the PR review workflow is safe to include everywhere,
18831
+ // and keeping review/merge policy centralised in the pr-reviewer agent
18832
+ // means consumers get consistent behaviour out of the box. Consumers can
18833
+ // still exclude it explicitly via `excludeBundles` if desired.
18834
+ appliesWhen: () => true,
18835
+ rules: [
18836
+ {
18837
+ name: "pr-review-policy",
18838
+ description: "Declarative policy that tells the pr-reviewer which PRs may auto-merge and which must wait for a human reviewer",
18839
+ scope: AGENT_RULE_SCOPE.ALWAYS,
18840
+ content: [
18841
+ "# PR Review Policy",
18842
+ "",
18843
+ "The `pr-reviewer` sub-agent evaluates every PR it reviews against the",
18844
+ "policy below and routes the PR into one of two modes:",
18845
+ "",
18846
+ "- **`auto-merge`** \u2014 the reviewer may enable squash auto-merge once",
18847
+ " all acceptance criteria are met and CI is green.",
18848
+ "- **`human-required`** \u2014 the reviewer runs the full AC/CI check but",
18849
+ " never calls `gh pr merge --auto`. It applies the",
18850
+ " `review:awaiting-human` label and hands off to a human reviewer.",
18851
+ "",
18852
+ "## Policy",
18853
+ "",
18854
+ "```yaml",
18855
+ "version: 1",
18856
+ "default: auto-merge",
18857
+ "",
18858
+ "human-required:",
18859
+ " paths:",
18860
+ ' - "docs/src/content/docs/requirements/architectural-decisions/**"',
18861
+ ' - "docs/src/content/docs/project-context.md"',
18862
+ ' - ".github/workflows/**"',
18863
+ ' - ".github/CODEOWNERS"',
18864
+ ' - ".projenrc.ts"',
18865
+ ' - "projenrc/**"',
18866
+ ' - "CLAUDE.md"',
18867
+ ' - ".claude/**"',
18868
+ ' - "packages/**/package.json"',
18869
+ " issue-types:",
18870
+ " - release",
18871
+ " - hotfix",
18872
+ " size:",
18873
+ " files: 10",
18874
+ " insertions: 500",
18875
+ " labels-that-force-human:",
18876
+ ' - "review:human-required"',
18877
+ ' - "priority:critical"',
18878
+ "",
18879
+ "auto-merge:",
18880
+ " labels-that-force-auto:",
18881
+ ' - "review:auto-ok"',
18882
+ " paths-exempt-from-size:",
18883
+ ...renderPathsExemptFromSizeYaml(
18884
+ policy.autoMerge.pathsExemptFromSize
18885
+ ),
18886
+ "```",
18887
+ "",
18888
+ "## Precedence",
18889
+ "",
18890
+ "The reviewer walks the following checks in order. The **first match**",
18891
+ "fixes the mode; any mixed-match PR (signals from both sides) resolves",
18892
+ "conservatively to `human-required` \u2014 `auto-merge` only wins when the",
18893
+ "force-auto label is the single top-priority match.",
18894
+ "",
18895
+ "1. **`auto-merge.labels-that-force-auto`** \u2014 if the PR carries any of",
18896
+ " these labels (e.g. `review:auto-ok`), the mode is `auto-merge`",
18897
+ " outright. This is the only escape hatch from the conservative",
18898
+ " default; it requires a maintainer to apply the label explicitly.",
18899
+ "2. **`review:human-required` label** \u2014 reserved force-human label;",
18900
+ " if present (and no force-auto label beat it in step 1), the mode",
18901
+ " is `human-required`.",
18902
+ "3. **`human-required.labels-that-force-human`** \u2014 any listed label on",
18903
+ " the PR (e.g. `priority:critical`) forces `human-required`.",
18904
+ "4. **`human-required.paths`** \u2014 if any file in the PR diff matches",
18905
+ " any glob here, the mode is `human-required`. Matching uses",
18906
+ " standard glob semantics (`**` for recursive directories,",
18907
+ " `*` for a single path segment).",
18908
+ "5. **`human-required.issue-types`** \u2014 if the linked issue's GitHub",
18909
+ " issue type matches any entry (case-insensitive), the mode is",
18910
+ " `human-required`.",
18911
+ "6. **`human-required.size`** \u2014 first read",
18912
+ " `auto-merge.paths-exempt-from-size` from the policy block above.",
18913
+ " For every file in the PR diff, evaluate whether the file path",
18914
+ " matches at least one glob in that carve-out list. If **every**",
18915
+ " changed path matches the carve-out, **skip rule #6** entirely",
18916
+ " and continue to rule #7 \u2014 the PR is exempt from the size",
18917
+ " threshold regardless of its `files` or `insertions` count.",
18918
+ " Otherwise (any changed path falls outside the carve-out list),",
18919
+ " apply the size check: if the PR exceeds either the `files`",
18920
+ " count or the `insertions` count, the mode is `human-required`.",
18921
+ "7. **`default`** \u2014 applied only when no rule above matched",
18922
+ " (normally `auto-merge`).",
18923
+ "",
18924
+ "The `auto-merge.paths-exempt-from-size` carve-out exists so",
18925
+ "**doc-only PRs** that routinely exceed the 500-insertion size",
18926
+ "threshold (large migrations, bulk additions, refresh passes)",
18927
+ "are not forced into `human-required` mode for a reason that does",
18928
+ "not reflect production risk. The default carve-out exempts the",
18929
+ "entire `docs/**` tree \u2014 every consumer of configulator places its",
18930
+ "Starlight docs site there. A PR mixing docs and code still falls",
18931
+ "into `human-required` at rule #6 because the non-docs path fails",
18932
+ "the carve-out check, so the rule only relaxes the threshold for",
18933
+ "PRs whose **every** changed path is doc-only.",
18934
+ "",
18935
+ "The `pr-reviewer` sub-agent records the decided mode, the",
18936
+ "triggering reason, and the numeric **matched-rule index** (2\u20136",
18937
+ "for human-required matches; `null` for rule 1 force-auto or",
18938
+ "rule 7 default) in its Phase 2.75 output. Downstream phases and",
18939
+ "the sticky summary cite the specific rule that applied.",
18940
+ "",
18941
+ "### Rule-#6 carve-out for `gh pr update-branch`",
18942
+ "",
18943
+ "The reviewer's BEHIND-branch refresh step (`gh pr update-branch`)",
18944
+ "is normally restricted to `auto-merge` PRs because pushing the",
18945
+ "default branch into a `human-required` PR expands the diff the",
18946
+ "human is reviewing without their consent. A narrow exception",
18947
+ "applies when rule #6 (size threshold) is the **sole** trigger",
18948
+ "for `human-required` mode: the bot may still run `gh pr",
18949
+ "update-branch` so a code-heavy size-tripped PR does not sit",
18950
+ "BEHIND while the human drafts their review.",
18951
+ "",
18952
+ "The exception is keyed on the matched-rule index recorded in",
18953
+ "Phase 2.75. All other `human-required` triggers \u2014 rule 2",
18954
+ "(`review:human-required` label), rule 3 (any",
18955
+ "`labels-that-force-human` label such as `priority:critical`),",
18956
+ "rule 4 (`human-required.paths` glob match), and rule 5",
18957
+ "(`human-required.issue-types` match) \u2014 continue to block",
18958
+ "`update-branch` because each one signals a human reviewer who",
18959
+ "has explicit ownership of the branch's lifecycle.",
18960
+ "",
18961
+ "This carve-out is largely belt-and-suspenders given the doc-only",
18962
+ "size carve-out above. Doc-only PRs that trip rule #6 now route",
18963
+ "directly to `auto-merge`, so the rule-#6 `update-branch` carve-",
18964
+ "out only kicks in for **code-heavy** PRs that legitimately trip",
18965
+ "rule #6 (mixed-content diffs whose non-doc paths fail the",
18966
+ "`paths-exempt-from-size` check, or consumers that disable the",
18967
+ "doc-only carve-out entirely)."
18968
+ ].join("\n"),
18969
+ tags: ["policy", "review"]
18180
18970
  },
18181
- tags: ["workflow"]
18182
- },
18183
- {
18184
- name: "pr-review-feedback-protocol",
18185
- description: "Documents the human-in-the-loop feedback loop on PR review: reaction state machine, pushback resolution, fix-list comment format, sticky reviewer-notes comment, label glossary, and human-author opt-in flag.",
18186
- scope: AGENT_RULE_SCOPE.ALWAYS,
18187
- content: [
18188
- "# PR Review Feedback Protocol",
18189
- "",
18190
- "## Human-in-the-Loop Feedback Protocol",
18191
- "",
18192
- "The PR review pipeline is a **human-in-the-loop feedback loop**.",
18193
- "Reviewers (human or agent) leave comments on the PR; the",
18194
- "`pr-reviewer` sub-agent classifies each comment, reacts to it,",
18195
- "delegates in-scope fixes to `issue-worker`, and updates a single",
18196
- "sticky `## Reviewer notes` comment that is the canonical record of",
18197
- "PR state. The sections below document the conventions humans need",
18198
- "to read and drive that loop.",
18199
- "",
18200
- "### Trigger Model: Human-Triggered, Single-Pass",
18201
- "",
18202
- "Each reviewer pass runs exactly once and does not self-chain. A",
18203
- "human re-invokes `/review-pr <n>` (or `/review-prs`) whenever the",
18204
- "PR state changes enough to warrant another look \u2014 a new comment,",
18205
- "a new commit, a resolved pushback, a label flip. The reviewer",
18206
- "never reschedules itself and never loops back after handing off to",
18207
- "`issue-worker`: the worker's run is the terminal step of that",
18208
- "pass, and a human must re-invoke the reviewer to see the follow-up",
18209
- "reactions and the auto-merge re-enablement decision.",
18210
- "",
18211
- "This keeps the loop cheap to reason about: every agent action is",
18212
- "traceable to a specific human invocation, and there is no",
18213
- "background automation to pause or cancel.",
18214
- "",
18215
- "### Reaction State Machine",
18216
- "",
18217
- "The reviewer signals its disposition toward each human comment via",
18218
- "GitHub reactions on that comment. Five reactions carry meaning in",
18219
- "this workflow; every other reaction is ignored.",
18220
- "",
18221
- "| Reaction | Meaning | Terminal? |",
18222
- "|----------|---------|-----------|",
18223
- "| `eyes` | Seen by reviewer; no terminal decision yet. Queued for processing on this or a later pass. | No |",
18224
- "| `+1` | Reviewer accepted the comment's request; a fix has been queued or has already landed. | **Yes** |",
18225
- "| `rocket` | The accepted fix has landed on the branch. The reviewer's reply cites the commit SHA that applied it. | **Yes** |",
18226
- "| `thinking_face` | Reviewer pushback \u2014 the comment conflicts with an acceptance criterion, a CLAUDE.md convention, the project-context doc, or is ambiguous. **Blocks auto-merge** until resolved. | No |",
18227
- "| `-1` | Declined as out-of-scope. A separate tracking issue was created; the reviewer's reply links to it. | **Yes** |",
18228
- "",
18229
- "Terminal reactions (`+1`, `rocket`, `-1`) are applied **only after**",
18230
- "the corresponding action has truly completed \u2014 the fix accepted,",
18231
- "the commit landed on the branch, or the out-of-scope tracking",
18232
- "issue created and linked in a reply. The reviewer never applies a",
18233
- "terminal reaction pre-emptively.",
18234
- "",
18235
- "A comment carrying only `eyes` or `thinking_face` from the",
18236
- "reviewer is **non-terminal** and will be re-evaluated on the next",
18237
- "pass. A comment carrying any terminal reaction authored by the",
18238
- "reviewer is dropped from future classification.",
18239
- "",
18240
- "GitHub's reactions API uses `confused` as the content string for",
18241
- "the `thinking_face` reaction (`content=confused` when POSTing).",
18242
- "",
18243
- "### Resolving a Pushback",
18244
- "",
18245
- "When the reviewer pushes back on a comment with `thinking_face`,",
18246
- "auto-merge is blocked until the dispute is resolved. Humans have",
18247
- "three ways to clear a pushback:",
18248
- "",
18249
- "1. **Withdraw the comment.** Delete the comment, or edit out the",
18250
- " disputed request, then re-invoke `/review-pr <n>`. The reviewer",
18251
- " drops the withdrawn item from its queue on the next pass.",
18252
- "2. **Reply with clarification.** Post a reply on the same thread",
18253
- " that addresses the reviewer's objection (cite the acceptance",
18254
- " criterion you meant, supply the missing context, or concede the",
18255
- " point). Re-invoke `/review-pr <n>` \u2014 the reviewer re-classifies",
18256
- " the thread and may promote `thinking_face` to `+1` if the",
18257
- " clarification satisfies it.",
18258
- "3. **Force through with `review:auto-ok`.** Apply the",
18259
- " `review:auto-ok` label to the PR as an explicit maintainer",
18260
- " override. The reviewer will log the override in the sticky",
18261
- " `## Reviewer notes` comment and proceed with auto-merge even",
18262
- " though the dispute was never resolved by reply or withdrawal.",
18263
- "",
18264
- "### Fix-List Comment Format",
18265
- "",
18266
- "When Phase 4 delegates in-scope fixes to `issue-worker`, it posts",
18267
- "a single PR-level comment whose body carries both a human-readable",
18268
- "checkbox summary and a fenced ```json fix-list``` block. The JSON",
18269
- "block is the authoritative payload the worker parses; the",
18270
- "checkbox list is for humans reading the PR.",
18271
- "",
18272
- "```markdown",
18273
- "## Reviewer: fix list for @issue-worker",
18274
- "",
18275
- "- [ ] @<author> \u2014 <instruction summary> (<file>:<line>)",
18276
- "",
18277
- "```json fix-list",
18278
- "{",
18279
- ' "pr": <pr-number>,',
18280
- ' "branch": "<head-ref-name>",',
18281
- ' "generated_at": "<ISO-8601 timestamp>",',
18282
- ' "items": [',
18283
- ' {"comment_id": "<id>", "author": "<login>", "file": "<path>", "line": <n>, "instruction": "<imperative instruction>"}',
18284
- " ]",
18285
- "}",
18286
- "```",
18287
- "```",
18288
- "",
18289
- "Each `items[]` entry corresponds to one in-scope comment the",
18290
- "reviewer queued on this pass. The `comment_id` is preserved",
18291
- "exactly as returned by the GitHub API so that `issue-worker` can",
18292
- "report per-item outcomes and the reviewer can apply `rocket` or",
18293
- "`thinking_face` to the correct source comment on the next pass.",
18294
- "",
18295
- "### Sticky `## Reviewer notes` Comment",
18296
- "",
18297
- "Every PR has **one** canonical reviewer-notes comment. The",
18298
- "reviewer creates it on the first pass, then **edits it in place**",
18299
- "on every subsequent pass via",
18300
- "`gh api .../issues/comments/<id> -X PATCH`. It is never",
18301
- "duplicated and never replaced by a fresh per-pass summary.",
18302
- "",
18303
- "This sticky comment is the **single human-facing source of truth**",
18304
- "for the PR's current state. Humans scanning the PR should read",
18305
- "the sticky first, before scrolling back through individual threads.",
18306
- "It carries, at a minimum:",
18307
- "",
18308
- "- **Mode** \u2014 `auto-merge` or `human-required`, with the Phase 2.75",
18309
- " reason that chose that mode.",
18310
- "- **AC status** \u2014 met, partial, or missing, with evidence links",
18311
- " to files or tests.",
18312
- "- **CI status** \u2014 green, pending, or red.",
18313
- "- **Outstanding** \u2014 comments still carrying a non-terminal",
18314
- " reviewer reaction (`eyes`, open `thinking_face`).",
18315
- "- **Pushbacks** \u2014 every unresolved `thinking_face` the reviewer",
18316
- " has left, with the reason captured in its pushback reply.",
18317
- "- **Last pass** \u2014 the ISO 8601 timestamp of the most recent run.",
18318
- "",
18319
- "The sticky is updated on every pass \u2014 including passes that ended",
18320
- "in a pushback-gated skip, a `NEEDS_CHANGES` findings comment, or",
18321
- "a `human-required` hand-off \u2014 so it never goes stale while the",
18322
- "reviewer is actively processing the PR.",
18323
- "",
18324
- "### Label Glossary",
18325
- "",
18326
- "Five review-workflow labels drive the feedback loop. Consumers",
18327
- "that adopt this workflow are responsible for creating them in",
18328
- "their own repos (the same way they create `priority:*` and",
18329
- "`status:*` labels).",
18330
- "",
18331
- "| Label | Purpose |",
18332
- "|-------|---------|",
18333
- "| `origin:issue-worker` | PR was opened by the `issue-worker` agent. Eligible for auto-delegation of in-scope fixes. Human-authored PRs lack this label and will not trigger delegation unless the reviewer is invoked with `--allow-human-author`. |",
18334
- "| `review:human-required` | Force human review regardless of what the policy would otherwise decide. The reviewer never enables auto-merge on a PR carrying this label. |",
18335
- "| `review:auto-ok` | Force auto-merge regardless of what the policy would otherwise decide. **Also resolves outstanding `thinking_face` pushbacks** as an explicit maintainer override; the reviewer logs the override in the sticky summary. |",
18336
- "| `review:awaiting-human` | Set by the reviewer when it completes its work on a `human-required` PR and is handing off the final merge decision. Cleared by a human (or by `review:auto-ok` flipping the PR back to `auto-merge` mode). |",
18337
- "| `review:fixing` | Short-lived lease held by the reviewer while an `issue-worker` feedback-mode delegation is mid-run. Released automatically at the end of Phase 4 step (g). Contention on this label means a prior delegation crashed without releasing it and needs human investigation. |",
18338
- "",
18339
- "### Reviewing Human-Authored PRs: the `--allow-human-author` Flag",
18340
- "",
18341
- "By default the reviewer only **delegates** in-scope fixes on",
18342
- "bot-authored PRs \u2014 those carrying the `origin:issue-worker`",
18343
- "label. Running `/review-pr <n>` or `/review-prs` on a",
18344
- "human-authored PR still produces a full review (reactions,",
18345
- "replies, sticky summary, and auto-merge when the policy allows",
18346
- "it) but skips the delegation hand-off to `issue-worker` \u2014 the",
18347
- "human author is expected to apply the fixes themselves.",
18348
- "",
18349
- "Pass `--allow-human-author` to opt into delegation on",
18350
- "human-authored PRs for a single invocation:",
18351
- "",
18352
- "```",
18353
- "/review-pr <pr-number> --allow-human-author",
18354
- "/review-prs --allow-human-author",
18355
- "```",
18356
- "",
18357
- "The flag does **not** persist across invocations. Subsequent",
18358
- "invocations return to the bot-only default and require the flag",
18359
- "to be re-supplied if delegation on a human-authored PR is desired",
18360
- "again."
18361
- ].join("\n"),
18362
- platforms: {
18363
- cursor: { exclude: true }
18971
+ {
18972
+ name: "pr-review-workflow",
18973
+ description: "Describes the /review-pr and /review-prs skills and their delegation to the pr-reviewer sub-agent",
18974
+ scope: AGENT_RULE_SCOPE.ALWAYS,
18975
+ content: [
18976
+ "# PR Review Workflow",
18977
+ "",
18978
+ "Two skills are available, both backed by the same `pr-reviewer`",
18979
+ "sub-agent:",
18980
+ "",
18981
+ "- **`/review-pr <pr-number>`** \u2014 review a single targeted PR.",
18982
+ "- **`/review-prs`** \u2014 loop over every eligible open PR in the",
18983
+ " repository and review each one in turn.",
18984
+ "",
18985
+ "The `pr-reviewer` sub-agent:",
18986
+ "",
18987
+ "1. Runs a pre-flight eligibility filter (mergeable, CI not failing,",
18988
+ " has a linked issue). Ineligible PRs get a short comment and are",
18989
+ " skipped.",
18990
+ "2. Fetches the PR, its diff, CI status, and the linked issue",
18991
+ "3. **Evaluates the PR Review Policy** (see the `PR Review Policy`",
18992
+ " section above) to decide whether the PR is `auto-merge` or",
18993
+ " `human-required`, and records the triggering reason",
18994
+ "4. Builds a checklist from the issue's acceptance criteria",
18995
+ "5. Verifies the diff satisfies each criterion and that CI is green",
18996
+ "6. **Enables squash auto-merge** (with `--delete-branch`) when all",
18997
+ " checks pass **and** the review mode is `auto-merge`",
18998
+ "7. **Applies `review:awaiting-human`** and hands off to a human",
18999
+ " reviewer when the review mode is `human-required` (no auto-merge,",
19000
+ " even if every acceptance criterion is met)",
19001
+ "8. **Comments with grouped findings** when any check fails (plain",
19002
+ " `gh pr comment`, not a formal `--request-changes` review)",
19003
+ "9. After a successful merge, verifies the linked issue is closed",
19004
+ " and closes it explicitly if the merge commit did not",
19005
+ "10. Cleans up the local branch after merge",
19006
+ "",
19007
+ "The reviewer **never** implements code and **never** pushes commits",
19008
+ "to a PR's branch \u2014 it only reviews, decides, and orchestrates merge",
19009
+ "or comment. In loop mode, a failed review for one PR never stops",
19010
+ "the loop; the reviewer comments and moves on. See the `pr-reviewer`",
19011
+ "agent definition for the full phase-by-phase contract."
19012
+ ].join("\n"),
19013
+ platforms: {
19014
+ cursor: { exclude: true }
19015
+ },
19016
+ tags: ["workflow"]
18364
19017
  },
18365
- tags: ["workflow"]
18366
- }
18367
- ],
18368
- skills: [reviewPrSkill, reviewPrsSkill],
18369
- subAgents: [prReviewerSubAgent],
18370
- labels: [
18371
- {
18372
- name: "type:pr-review",
18373
- color: "5319E7",
18374
- description: "PR review tasks"
18375
- },
18376
- {
18377
- name: "origin:issue-worker",
18378
- color: "5319E7",
18379
- description: "PR opened by the issue-worker agent"
18380
- },
18381
- {
18382
- name: "review:auto-ok",
18383
- color: "0E8A16",
18384
- description: "Force auto-merge regardless of policy"
18385
- },
18386
- {
18387
- name: "review:human-required",
18388
- color: "D93F0B",
18389
- description: "Force human review regardless of policy"
18390
- },
18391
- {
18392
- name: "review:awaiting-human",
18393
- color: "FBCA04",
18394
- description: "Reviewer handed off; awaiting human merge decision"
18395
- },
18396
- {
18397
- name: "review:fixing",
18398
- color: "D4C5F9",
18399
- description: "Short-lived lease while issue-worker applies feedback fixes"
18400
- }
18401
- ]
18402
- };
19018
+ {
19019
+ name: "pr-review-feedback-protocol",
19020
+ description: "Documents the human-in-the-loop feedback loop on PR review: reaction state machine, pushback resolution, fix-list comment format, sticky reviewer-notes comment, label glossary, and human-author opt-in flag.",
19021
+ scope: AGENT_RULE_SCOPE.ALWAYS,
19022
+ content: [
19023
+ "# PR Review Feedback Protocol",
19024
+ "",
19025
+ "## Human-in-the-Loop Feedback Protocol",
19026
+ "",
19027
+ "The PR review pipeline is a **human-in-the-loop feedback loop**.",
19028
+ "Reviewers (human or agent) leave comments on the PR; the",
19029
+ "`pr-reviewer` sub-agent classifies each comment, reacts to it,",
19030
+ "delegates in-scope fixes to `issue-worker`, and updates a single",
19031
+ "sticky `## Reviewer notes` comment that is the canonical record of",
19032
+ "PR state. The sections below document the conventions humans need",
19033
+ "to read and drive that loop.",
19034
+ "",
19035
+ "### Trigger Model: Human-Triggered, Single-Pass",
19036
+ "",
19037
+ "Each reviewer pass runs exactly once and does not self-chain. A",
19038
+ "human re-invokes `/review-pr <n>` (or `/review-prs`) whenever the",
19039
+ "PR state changes enough to warrant another look \u2014 a new comment,",
19040
+ "a new commit, a resolved pushback, a label flip. The reviewer",
19041
+ "never reschedules itself and never loops back after handing off to",
19042
+ "`issue-worker`: the worker's run is the terminal step of that",
19043
+ "pass, and a human must re-invoke the reviewer to see the follow-up",
19044
+ "reactions and the auto-merge re-enablement decision.",
19045
+ "",
19046
+ "This keeps the loop cheap to reason about: every agent action is",
19047
+ "traceable to a specific human invocation, and there is no",
19048
+ "background automation to pause or cancel.",
19049
+ "",
19050
+ "### Reaction State Machine",
19051
+ "",
19052
+ "The reviewer signals its disposition toward each human comment via",
19053
+ "GitHub reactions on that comment. Five reactions carry meaning in",
19054
+ "this workflow; every other reaction is ignored.",
19055
+ "",
19056
+ "| Reaction | Meaning | Terminal? |",
19057
+ "|----------|---------|-----------|",
19058
+ "| `eyes` | Seen by reviewer; no terminal decision yet. Queued for processing on this or a later pass. | No |",
19059
+ "| `+1` | Reviewer accepted the comment's request; a fix has been queued or has already landed. | **Yes** |",
19060
+ "| `rocket` | The accepted fix has landed on the branch. The reviewer's reply cites the commit SHA that applied it. | **Yes** |",
19061
+ "| `thinking_face` | Reviewer pushback \u2014 the comment conflicts with an acceptance criterion, a CLAUDE.md convention, the project-context doc, or is ambiguous. **Blocks auto-merge** until resolved. | No |",
19062
+ "| `-1` | Declined as out-of-scope. A separate tracking issue was created; the reviewer's reply links to it. | **Yes** |",
19063
+ "",
19064
+ "Terminal reactions (`+1`, `rocket`, `-1`) are applied **only after**",
19065
+ "the corresponding action has truly completed \u2014 the fix accepted,",
19066
+ "the commit landed on the branch, or the out-of-scope tracking",
19067
+ "issue created and linked in a reply. The reviewer never applies a",
19068
+ "terminal reaction pre-emptively.",
19069
+ "",
19070
+ "A comment carrying only `eyes` or `thinking_face` from the",
19071
+ "reviewer is **non-terminal** and will be re-evaluated on the next",
19072
+ "pass. A comment carrying any terminal reaction authored by the",
19073
+ "reviewer is dropped from future classification.",
19074
+ "",
19075
+ "GitHub's reactions API uses `confused` as the content string for",
19076
+ "the `thinking_face` reaction (`content=confused` when POSTing).",
19077
+ "",
19078
+ "### Resolving a Pushback",
19079
+ "",
19080
+ "When the reviewer pushes back on a comment with `thinking_face`,",
19081
+ "auto-merge is blocked until the dispute is resolved. Humans have",
19082
+ "three ways to clear a pushback:",
19083
+ "",
19084
+ "1. **Withdraw the comment.** Delete the comment, or edit out the",
19085
+ " disputed request, then re-invoke `/review-pr <n>`. The reviewer",
19086
+ " drops the withdrawn item from its queue on the next pass.",
19087
+ "2. **Reply with clarification.** Post a reply on the same thread",
19088
+ " that addresses the reviewer's objection (cite the acceptance",
19089
+ " criterion you meant, supply the missing context, or concede the",
19090
+ " point). Re-invoke `/review-pr <n>` \u2014 the reviewer re-classifies",
19091
+ " the thread and may promote `thinking_face` to `+1` if the",
19092
+ " clarification satisfies it.",
19093
+ "3. **Force through with `review:auto-ok`.** Apply the",
19094
+ " `review:auto-ok` label to the PR as an explicit maintainer",
19095
+ " override. The reviewer will log the override in the sticky",
19096
+ " `## Reviewer notes` comment and proceed with auto-merge even",
19097
+ " though the dispute was never resolved by reply or withdrawal.",
19098
+ "",
19099
+ "### Fix-List Comment Format",
19100
+ "",
19101
+ "When Phase 4 delegates in-scope fixes to `issue-worker`, it posts",
19102
+ "a single PR-level comment whose body carries both a human-readable",
19103
+ "checkbox summary and a fenced ```json fix-list``` block. The JSON",
19104
+ "block is the authoritative payload the worker parses; the",
19105
+ "checkbox list is for humans reading the PR.",
19106
+ "",
19107
+ "```markdown",
19108
+ "## Reviewer: fix list for @issue-worker",
19109
+ "",
19110
+ "- [ ] @<author> \u2014 <instruction summary> (<file>:<line>)",
19111
+ "",
19112
+ "```json fix-list",
19113
+ "{",
19114
+ ' "pr": <pr-number>,',
19115
+ ' "branch": "<head-ref-name>",',
19116
+ ' "generated_at": "<ISO-8601 timestamp>",',
19117
+ ' "items": [',
19118
+ ' {"comment_id": "<id>", "author": "<login>", "file": "<path>", "line": <n>, "instruction": "<imperative instruction>"}',
19119
+ " ]",
19120
+ "}",
19121
+ "```",
19122
+ "```",
19123
+ "",
19124
+ "Each `items[]` entry corresponds to one in-scope comment the",
19125
+ "reviewer queued on this pass. The `comment_id` is preserved",
19126
+ "exactly as returned by the GitHub API so that `issue-worker` can",
19127
+ "report per-item outcomes and the reviewer can apply `rocket` or",
19128
+ "`thinking_face` to the correct source comment on the next pass.",
19129
+ "",
19130
+ "### Sticky `## Reviewer notes` Comment",
19131
+ "",
19132
+ "Every PR has **one** canonical reviewer-notes comment. The",
19133
+ "reviewer creates it on the first pass, then **edits it in place**",
19134
+ "on every subsequent pass via",
19135
+ "`gh api .../issues/comments/<id> -X PATCH`. It is never",
19136
+ "duplicated and never replaced by a fresh per-pass summary.",
19137
+ "",
19138
+ "This sticky comment is the **single human-facing source of truth**",
19139
+ "for the PR's current state. Humans scanning the PR should read",
19140
+ "the sticky first, before scrolling back through individual threads.",
19141
+ "It carries, at a minimum:",
19142
+ "",
19143
+ "- **Mode** \u2014 `auto-merge` or `human-required`, with the Phase 2.75",
19144
+ " reason that chose that mode.",
19145
+ "- **AC status** \u2014 met, partial, or missing, with evidence links",
19146
+ " to files or tests.",
19147
+ "- **CI status** \u2014 green, pending, or red.",
19148
+ "- **Outstanding** \u2014 comments still carrying a non-terminal",
19149
+ " reviewer reaction (`eyes`, open `thinking_face`).",
19150
+ "- **Pushbacks** \u2014 every unresolved `thinking_face` the reviewer",
19151
+ " has left, with the reason captured in its pushback reply.",
19152
+ "- **Last pass** \u2014 the ISO 8601 timestamp of the most recent run.",
19153
+ "",
19154
+ "The sticky is updated on every pass \u2014 including passes that ended",
19155
+ "in a pushback-gated skip, a `NEEDS_CHANGES` findings comment, or",
19156
+ "a `human-required` hand-off \u2014 so it never goes stale while the",
19157
+ "reviewer is actively processing the PR.",
19158
+ "",
19159
+ "### Label Glossary",
19160
+ "",
19161
+ "Five review-workflow labels drive the feedback loop. Consumers",
19162
+ "that adopt this workflow are responsible for creating them in",
19163
+ "their own repos (the same way they create `priority:*` and",
19164
+ "`status:*` labels).",
19165
+ "",
19166
+ "| Label | Purpose |",
19167
+ "|-------|---------|",
19168
+ "| `origin:issue-worker` | PR was opened by the `issue-worker` agent. Eligible for auto-delegation of in-scope fixes. Human-authored PRs lack this label and will not trigger delegation unless the reviewer is invoked with `--allow-human-author`. |",
19169
+ "| `review:human-required` | Force human review regardless of what the policy would otherwise decide. The reviewer never enables auto-merge on a PR carrying this label. |",
19170
+ "| `review:auto-ok` | Force auto-merge regardless of what the policy would otherwise decide. **Also resolves outstanding `thinking_face` pushbacks** as an explicit maintainer override; the reviewer logs the override in the sticky summary. |",
19171
+ "| `review:awaiting-human` | Set by the reviewer when it completes its work on a `human-required` PR and is handing off the final merge decision. Cleared by a human (or by `review:auto-ok` flipping the PR back to `auto-merge` mode). |",
19172
+ "| `review:fixing` | Short-lived lease held by the reviewer while an `issue-worker` feedback-mode delegation is mid-run. Released automatically at the end of Phase 4 step (g). Contention on this label means a prior delegation crashed without releasing it and needs human investigation. |",
19173
+ "",
19174
+ "### Reviewing Human-Authored PRs: the `--allow-human-author` Flag",
19175
+ "",
19176
+ "By default the reviewer only **delegates** in-scope fixes on",
19177
+ "bot-authored PRs \u2014 those carrying the `origin:issue-worker`",
19178
+ "label. Running `/review-pr <n>` or `/review-prs` on a",
19179
+ "human-authored PR still produces a full review (reactions,",
19180
+ "replies, sticky summary, and auto-merge when the policy allows",
19181
+ "it) but skips the delegation hand-off to `issue-worker` \u2014 the",
19182
+ "human author is expected to apply the fixes themselves.",
19183
+ "",
19184
+ "Pass `--allow-human-author` to opt into delegation on",
19185
+ "human-authored PRs for a single invocation:",
19186
+ "",
19187
+ "```",
19188
+ "/review-pr <pr-number> --allow-human-author",
19189
+ "/review-prs --allow-human-author",
19190
+ "```",
19191
+ "",
19192
+ "The flag does **not** persist across invocations. Subsequent",
19193
+ "invocations return to the bot-only default and require the flag",
19194
+ "to be re-supplied if delegation on a human-authored PR is desired",
19195
+ "again."
19196
+ ].join("\n"),
19197
+ platforms: {
19198
+ cursor: { exclude: true }
19199
+ },
19200
+ tags: ["workflow"]
19201
+ }
19202
+ ],
19203
+ skills: [reviewPrSkill, reviewPrsSkill],
19204
+ subAgents: [prReviewerSubAgent],
19205
+ labels: [
19206
+ {
19207
+ name: "type:pr-review",
19208
+ color: "5319E7",
19209
+ description: "PR review tasks"
19210
+ },
19211
+ {
19212
+ name: "origin:issue-worker",
19213
+ color: "5319E7",
19214
+ description: "PR opened by the issue-worker agent"
19215
+ },
19216
+ {
19217
+ name: "review:auto-ok",
19218
+ color: "0E8A16",
19219
+ description: "Force auto-merge regardless of policy"
19220
+ },
19221
+ {
19222
+ name: "review:human-required",
19223
+ color: "D93F0B",
19224
+ description: "Force human review regardless of policy"
19225
+ },
19226
+ {
19227
+ name: "review:awaiting-human",
19228
+ color: "FBCA04",
19229
+ description: "Reviewer handed off; awaiting human merge decision"
19230
+ },
19231
+ {
19232
+ name: "review:fixing",
19233
+ color: "D4C5F9",
19234
+ description: "Short-lived lease while issue-worker applies feedback fixes"
19235
+ }
19236
+ ]
19237
+ };
19238
+ }
19239
+ function renderPathsExemptFromSizeYaml(paths) {
19240
+ if (paths.length === 0) {
19241
+ return [" []"];
19242
+ }
19243
+ return paths.map((path8) => ` - "${path8}"`);
19244
+ }
19245
+ var prReviewBundle = buildPrReviewBundle();
18403
19246
 
18404
19247
  // src/agent/bundles/projen.ts
18405
19248
  var projenBundle = {
@@ -25462,8 +26305,39 @@ function buildSoftwareProfileAnalystSubAgent(paths, issueDefaults, tier) {
25462
26305
  " every profile is mapped back to the capability model regardless",
25463
26306
  " of whether any adjacent products were surfaced.",
25464
26307
  "",
25465
- "7. **Commit and push** the matrix file (and any profile updates).",
25466
- " Close the matrix issue.",
26308
+ "7. **Commit any profile updates first \u2014 do not touch the",
26309
+ " matrix yet.** If step 4 surfaced edits to the per-product",
26310
+ " profile file (e.g. notes added to `## Risks / Open",
26311
+ " Questions` about column drift), stage and commit those",
26312
+ " profile changes in a focused commit. **Do not push yet,**",
26313
+ " and **do not** include the matrix file in this commit.",
26314
+ "",
26315
+ "8. **Defer the feature-matrix row insert to a final pre-push",
26316
+ " commit.** Per the `shared-editing-safety` rule's",
26317
+ " **Defer Shared-Index Commit to Final Pre-Push Step**",
26318
+ " subsection, the feature matrix is the canonical example of",
26319
+ " a **shared index file** \u2014 multiple software-profile",
26320
+ " sessions racing to append rows for different products is",
26321
+ " the exact contention this rule exists to mitigate. Apply",
26322
+ " the deferred sequence:",
26323
+ "",
26324
+ " ```bash",
26325
+ " git fetch origin",
26326
+ " git pull --rebase origin <default-branch>",
26327
+ " ```",
26328
+ "",
26329
+ " Re-read `<MATRIX_FILE>` from the now-up-to-date working",
26330
+ " tree, re-compute the deterministic insert position for the",
26331
+ " current product's rows (another session may have appended",
26332
+ " rows for a different product, or extended the segment",
26333
+ " columns, in the meantime), insert the rows in sort order,",
26334
+ " and commit the matrix edit in its **own focused commit**",
26335
+ " whose only file is `<MATRIX_FILE>`. Run the commit-path",
26336
+ " verification step (`git show HEAD:<MATRIX_FILE>` + grep",
26337
+ " count on the product slug as the unique marker) against",
26338
+ " that commit before pushing.",
26339
+ "",
26340
+ "9. **Push and close.** Push the branch and close the matrix issue.",
25467
26341
  "",
25468
26342
  "---",
25469
26343
  "",
@@ -28035,7 +28909,7 @@ var VERSION = {
28035
28909
  /**
28036
28910
  * Version of Astro to pin for AstroProject scaffolding.
28037
28911
  */
28038
- ASTRO_VERSION: "6.3.6",
28912
+ ASTRO_VERSION: "6.3.7",
28039
28913
  /**
28040
28914
  * CDK CLI for workflows and command line operations.
28041
28915
  *
@@ -28047,7 +28921,7 @@ var VERSION = {
28047
28921
  *
28048
28922
  * CLI and lib are versioned separately, so this is the lib version.
28049
28923
  */
28050
- AWS_CDK_LIB_VERSION: "2.256.1",
28924
+ AWS_CDK_LIB_VERSION: "2.257.0",
28051
28925
  /**
28052
28926
  * Version of the AWS Constructs library to use.
28053
28927
  */
@@ -28064,11 +28938,11 @@ var VERSION = {
28064
28938
  /**
28065
28939
  * Version of PNPM to use in workflows at github actions.
28066
28940
  */
28067
- PNPM_VERSION: "11.2.1",
28941
+ PNPM_VERSION: "11.2.2",
28068
28942
  /**
28069
28943
  * Version of Projen to use.
28070
28944
  */
28071
- PROJEN_VERSION: "0.99.63",
28945
+ PROJEN_VERSION: "0.99.64",
28072
28946
  /**
28073
28947
  * Version of `actions/setup-node` to use in GitHub workflows.
28074
28948
  * Tracks the version projen currently emits (see node_modules/projen/lib/github/workflows.js).
@@ -28820,7 +29694,7 @@ function renderPriorityRulesSection(rules) {
28820
29694
  }
28821
29695
 
28822
29696
  // src/agent/bundles/index.ts
28823
- function buildBuiltInBundles(paths = DEFAULT_AGENT_PATHS, issueDefaults = DEFAULT_RESOLVED_ISSUE_DEFAULTS, defaultAgentTier = AGENT_MODEL.BALANCED, bundleAgentTiers = /* @__PURE__ */ new Map()) {
29697
+ function buildBuiltInBundles(paths = DEFAULT_AGENT_PATHS, issueDefaults = DEFAULT_RESOLVED_ISSUE_DEFAULTS, defaultAgentTier = AGENT_MODEL.BALANCED, bundleAgentTiers = /* @__PURE__ */ new Map(), prReviewPolicy = resolvePrReviewPolicy()) {
28824
29698
  const tierFor = (bundle) => bundleAgentTiers.get(bundle) ?? defaultAgentTier;
28825
29699
  return [
28826
29700
  buildBaseBundle(paths),
@@ -28837,7 +29711,7 @@ function buildBuiltInBundles(paths = DEFAULT_AGENT_PATHS, issueDefaults = DEFAUL
28837
29711
  buildMeetingAnalysisBundle(tierFor("meeting-analysis")),
28838
29712
  agendaBundle,
28839
29713
  orchestratorBundle,
28840
- prReviewBundle,
29714
+ buildPrReviewBundle(prReviewPolicy),
28841
29715
  buildRequirementsAnalystBundle(paths, issueDefaults),
28842
29716
  buildRequirementsWriterBundle(paths, issueDefaults),
28843
29717
  buildRequirementsReviewerBundle(paths, issueDefaults),
@@ -28901,210 +29775,10 @@ function prefix(rel, entry) {
28901
29775
  return path.posix.join(rel, entry);
28902
29776
  }
28903
29777
 
28904
- // src/agent/renderers/cursor-renderer.ts
29778
+ // src/agent/renderers/claude-renderer.ts
28905
29779
  import { JsonFile as JsonFile2 } from "projen";
28906
29780
  import { TextFile as TextFile2 } from "projen/lib/textfile";
28907
- var GENERATED_MARKER = "# ~~ Generated by @codedrifters/configulator. Edits welcome \u2014 please contribute improvements back. ~~";
28908
- var CursorRenderer = class _CursorRenderer {
28909
- /**
28910
- * Render all Cursor configuration files.
28911
- */
28912
- static render(component, rules, skills, subAgents, mcpServers, settings) {
28913
- _CursorRenderer.renderRules(component, rules);
28914
- _CursorRenderer.renderSkills(component, skills);
28915
- _CursorRenderer.renderSubAgents(component, subAgents);
28916
- _CursorRenderer.renderMcpServers(component, mcpServers);
28917
- _CursorRenderer.renderHooks(component, settings);
28918
- _CursorRenderer.renderIgnoreFiles(component, settings);
28919
- }
28920
- static renderRules(component, rules) {
28921
- for (const rule of rules) {
28922
- if (rule.platforms?.cursor?.exclude) continue;
28923
- const lines = [];
28924
- const description = rule.platforms?.cursor?.description ?? rule.description;
28925
- const isAlways = rule.scope === AGENT_RULE_SCOPE.ALWAYS;
28926
- lines.push("---");
28927
- lines.push(`description: "${description}"`);
28928
- lines.push(`alwaysApply: ${isAlways}`);
28929
- if (!isAlways && rule.filePatterns && rule.filePatterns.length > 0) {
28930
- lines.push(`path: ${JSON.stringify([...rule.filePatterns])}`);
28931
- }
28932
- lines.push("---");
28933
- lines.push("");
28934
- lines.push(...rule.content.split("\n"));
28935
- new TextFile2(component, `.cursor/rules/${rule.name}.mdc`, { lines });
28936
- }
28937
- }
28938
- static renderSkills(component, skills) {
28939
- for (const skill of skills) {
28940
- if (skill.platforms?.cursor?.exclude) continue;
28941
- const lines = [];
28942
- lines.push("---");
28943
- lines.push(`name: "${skill.name}"`);
28944
- lines.push(`description: "${skill.description}"`);
28945
- if (skill.disableModelInvocation) {
28946
- lines.push(`disable-model-invocation: true`);
28947
- }
28948
- if (skill.userInvocable === false) {
28949
- lines.push(`user-invocable: false`);
28950
- }
28951
- if (skill.context) {
28952
- lines.push(`context: "${skill.context}"`);
28953
- }
28954
- if (skill.agent) {
28955
- lines.push(`agent: "${skill.agent}"`);
28956
- }
28957
- if (skill.shell) {
28958
- lines.push(`shell: "${skill.shell}"`);
28959
- }
28960
- if (skill.allowedTools && skill.allowedTools.length > 0) {
28961
- lines.push(`allowed-tools:`);
28962
- for (const tool of skill.allowedTools) {
28963
- lines.push(` - "${tool}"`);
28964
- }
28965
- }
28966
- lines.push("---");
28967
- lines.push("");
28968
- lines.push(...skill.instructions.split("\n"));
28969
- new TextFile2(component, `.cursor/skills/${skill.name}/SKILL.md`, {
28970
- lines
28971
- });
28972
- if (skill.referenceFiles && skill.referenceFiles.length > 0) {
28973
- for (const file of skill.referenceFiles) {
28974
- new TextFile2(component, `.cursor/skills/${skill.name}/${file.path}`, {
28975
- lines: file.content.split("\n")
28976
- });
28977
- }
28978
- }
28979
- }
28980
- }
28981
- static renderSubAgents(component, subAgents) {
28982
- for (const agent of subAgents) {
28983
- if (agent.platforms?.cursor?.exclude) continue;
28984
- const lines = [];
28985
- lines.push("---");
28986
- lines.push(`name: ${agent.name}`);
28987
- lines.push(`description: >-`);
28988
- lines.push(` ${agent.description}`);
28989
- if (agent.platforms?.cursor?.readonly) {
28990
- lines.push(`readonly: true`);
28991
- }
28992
- if (agent.platforms?.cursor?.isBackground) {
28993
- lines.push(`is_background: true`);
28994
- }
28995
- lines.push("---");
28996
- lines.push("");
28997
- lines.push(...agent.prompt.split("\n"));
28998
- new TextFile2(component, `.cursor/agents/${agent.name}.md`, { lines });
28999
- }
29000
- }
29001
- static renderMcpServers(component, mcpServers) {
29002
- const serverNames = Object.keys(mcpServers);
29003
- if (serverNames.length === 0) return;
29004
- const obj = { mcpServers: {} };
29005
- const servers = obj.mcpServers;
29006
- for (const [name, config] of Object.entries(mcpServers)) {
29007
- const server = {};
29008
- if (config.transport) server.transport = config.transport;
29009
- if (config.command) server.command = config.command;
29010
- if (config.args) server.args = [...config.args];
29011
- if (config.url) server.url = config.url;
29012
- if (config.headers && Object.keys(config.headers).length > 0) {
29013
- server.headers = { ...config.headers };
29014
- }
29015
- if (config.env) server.env = { ...config.env };
29016
- servers[name] = server;
29017
- }
29018
- new JsonFile2(component, ".cursor/mcp.json", { obj });
29019
- }
29020
- static renderHooks(component, settings) {
29021
- if (!settings?.hooks) return;
29022
- const hooks = {};
29023
- const hookEntries = settings.hooks;
29024
- for (const [event, actions] of Object.entries(hookEntries)) {
29025
- if (actions && actions.length > 0) {
29026
- hooks[event] = actions.map((h) => ({
29027
- command: h.command
29028
- }));
29029
- }
29030
- }
29031
- if (Object.keys(hooks).length === 0) return;
29032
- new JsonFile2(component, ".cursor/hooks.json", {
29033
- obj: { version: 1, hooks }
29034
- });
29035
- }
29036
- static renderIgnoreFiles(component, settings) {
29037
- if (settings?.ignorePatterns && settings.ignorePatterns.length > 0) {
29038
- new TextFile2(component, ".cursorignore", {
29039
- lines: [GENERATED_MARKER, "", ...settings.ignorePatterns]
29040
- });
29041
- }
29042
- if (settings?.indexingIgnorePatterns && settings.indexingIgnorePatterns.length > 0) {
29043
- new TextFile2(component, ".cursorindexingignore", {
29044
- lines: [GENERATED_MARKER, "", ...settings.indexingIgnorePatterns]
29045
- });
29046
- }
29047
- }
29048
- };
29049
-
29050
- // src/agent/template-resolver.ts
29051
- var FALLBACKS = {
29052
- "repository.owner": "<owner>",
29053
- "repository.name": "<repo>",
29054
- "repository.defaultBranch": "main",
29055
- "organization.name": "<organization>",
29056
- "organization.githubOrg": "<org>",
29057
- "githubProject.name": "<project-name>",
29058
- "githubProject.number": "<project-number>",
29059
- "githubProject.nodeId": "<project-node-id>",
29060
- docsPath: "<docs-path>",
29061
- // The monorepo-layout seed block is additive: when absent, the
29062
- // seeded `project-context.md` template reads cleanly without it.
29063
- // Fall back to an empty string so no placeholder text leaks into
29064
- // rendered prompts for repos that predate the layout contract.
29065
- monorepoLayoutSeedBlock: ""
29066
- };
29067
- var TEMPLATE_RE = /\{\{(\w+(?:\.\w+)*)\}\}/g;
29068
- function getNestedValue(obj, path8) {
29069
- const parts = path8.split(".");
29070
- let current = obj;
29071
- for (const part of parts) {
29072
- if (current == null || typeof current !== "object") {
29073
- return void 0;
29074
- }
29075
- current = current[part];
29076
- }
29077
- if (current == null) {
29078
- return void 0;
29079
- }
29080
- return String(current);
29081
- }
29082
- function resolveTemplateVariables(template, metadata) {
29083
- if (!TEMPLATE_RE.test(template)) {
29084
- return { resolved: template, unresolvedKeys: [] };
29085
- }
29086
- const unresolvedKeys = [];
29087
- TEMPLATE_RE.lastIndex = 0;
29088
- const resolved = template.replace(TEMPLATE_RE, (_match, key) => {
29089
- if (metadata) {
29090
- const value = getNestedValue(
29091
- metadata,
29092
- key
29093
- );
29094
- if (value !== void 0) {
29095
- return value;
29096
- }
29097
- }
29098
- unresolvedKeys.push(key);
29099
- return FALLBACKS[key] ?? `<${key}>`;
29100
- });
29101
- return { resolved, unresolvedKeys };
29102
- }
29103
-
29104
- // src/agent/renderers/claude-renderer.ts
29105
- import { JsonFile as JsonFile3 } from "projen";
29106
- import { TextFile as TextFile3 } from "projen/lib/textfile";
29107
- var GENERATED_MARKER2 = "<!-- ~~ Generated by @codedrifters/configulator. Edits welcome \u2014 please contribute improvements back. ~~ -->";
29781
+ var GENERATED_MARKER = "<!-- ~~ Generated by @codedrifters/configulator. Edits welcome \u2014 please contribute improvements back. ~~ -->";
29108
29782
  var ClaudeRenderer = class _ClaudeRenderer {
29109
29783
  /**
29110
29784
  * Render all Claude Code configuration files.
@@ -29129,12 +29803,12 @@ var ClaudeRenderer = class _ClaudeRenderer {
29129
29803
  return target === CLAUDE_RULE_TARGET.CLAUDE_MD;
29130
29804
  });
29131
29805
  if (claudeMdRules.length === 0) return;
29132
- const lines = [GENERATED_MARKER2, ""];
29806
+ const lines = [GENERATED_MARKER, ""];
29133
29807
  for (let i = 0; i < claudeMdRules.length; i++) {
29134
29808
  if (i > 0) lines.push("", "---", "");
29135
29809
  lines.push(...claudeMdRules[i].content.split("\n"));
29136
29810
  }
29137
- new TextFile3(component, "CLAUDE.md", { lines });
29811
+ new TextFile2(component, "CLAUDE.md", { lines });
29138
29812
  }
29139
29813
  static renderScopedRules(component, rules) {
29140
29814
  const scopedRules = rules.filter((r) => {
@@ -29154,7 +29828,7 @@ var ClaudeRenderer = class _ClaudeRenderer {
29154
29828
  lines.push("");
29155
29829
  }
29156
29830
  lines.push(...rule.content.split("\n"));
29157
- new TextFile3(component, `.claude/rules/${rule.name}.md`, { lines });
29831
+ new TextFile2(component, `.claude/rules/${rule.name}.md`, { lines });
29158
29832
  }
29159
29833
  }
29160
29834
  static renderSettings(component, mcpServers, settings) {
@@ -29279,7 +29953,7 @@ var ClaudeRenderer = class _ClaudeRenderer {
29279
29953
  hasContent = true;
29280
29954
  }
29281
29955
  if (!hasContent) return;
29282
- new JsonFile3(component, ".claude/settings.json", { obj });
29956
+ new JsonFile2(component, ".claude/settings.json", { obj });
29283
29957
  }
29284
29958
  static buildSandboxObj(sandbox) {
29285
29959
  const obj = {};
@@ -29366,12 +30040,12 @@ var ClaudeRenderer = class _ClaudeRenderer {
29366
30040
  lines.push("---");
29367
30041
  lines.push("");
29368
30042
  lines.push(...skill.instructions.split("\n"));
29369
- new TextFile3(component, `.claude/skills/${skill.name}/SKILL.md`, {
30043
+ new TextFile2(component, `.claude/skills/${skill.name}/SKILL.md`, {
29370
30044
  lines
29371
30045
  });
29372
30046
  if (skill.referenceFiles && skill.referenceFiles.length > 0) {
29373
30047
  for (const file of skill.referenceFiles) {
29374
- new TextFile3(component, `.claude/skills/${skill.name}/${file.path}`, {
30048
+ new TextFile2(component, `.claude/skills/${skill.name}/${file.path}`, {
29375
30049
  lines: file.content.split("\n")
29376
30050
  });
29377
30051
  }
@@ -29429,7 +30103,7 @@ var ClaudeRenderer = class _ClaudeRenderer {
29429
30103
  lines.push("---");
29430
30104
  lines.push("");
29431
30105
  lines.push(...agent.prompt.split("\n"));
29432
- new TextFile3(component, `.claude/agents/${agent.name}.md`, { lines });
30106
+ new TextFile2(component, `.claude/agents/${agent.name}.md`, { lines });
29433
30107
  }
29434
30108
  }
29435
30109
  static buildMcpServerObj(config) {
@@ -29446,7 +30120,7 @@ var ClaudeRenderer = class _ClaudeRenderer {
29446
30120
  }
29447
30121
  static renderProcedures(component, procedures) {
29448
30122
  for (const proc of procedures) {
29449
- new TextFile3(component, `.claude/procedures/${proc.name}`, {
30123
+ new TextFile2(component, `.claude/procedures/${proc.name}`, {
29450
30124
  lines: proc.content.split("\n"),
29451
30125
  executable: true
29452
30126
  });
@@ -29473,7 +30147,7 @@ var ClaudeRenderer = class _ClaudeRenderer {
29473
30147
  lines.push("---");
29474
30148
  lines.push("");
29475
30149
  lines.push(...command.content.split("\n"));
29476
- new TextFile3(component, `.claude/commands/${command.name}.md`, { lines });
30150
+ new TextFile2(component, `.claude/commands/${command.name}.md`, { lines });
29477
30151
  }
29478
30152
  }
29479
30153
  /**
@@ -29485,6 +30159,206 @@ var ClaudeRenderer = class _ClaudeRenderer {
29485
30159
  }
29486
30160
  };
29487
30161
 
30162
+ // src/agent/renderers/cursor-renderer.ts
30163
+ import { JsonFile as JsonFile3 } from "projen";
30164
+ import { TextFile as TextFile3 } from "projen/lib/textfile";
30165
+ var GENERATED_MARKER2 = "# ~~ Generated by @codedrifters/configulator. Edits welcome \u2014 please contribute improvements back. ~~";
30166
+ var CursorRenderer = class _CursorRenderer {
30167
+ /**
30168
+ * Render all Cursor configuration files.
30169
+ */
30170
+ static render(component, rules, skills, subAgents, mcpServers, settings) {
30171
+ _CursorRenderer.renderRules(component, rules);
30172
+ _CursorRenderer.renderSkills(component, skills);
30173
+ _CursorRenderer.renderSubAgents(component, subAgents);
30174
+ _CursorRenderer.renderMcpServers(component, mcpServers);
30175
+ _CursorRenderer.renderHooks(component, settings);
30176
+ _CursorRenderer.renderIgnoreFiles(component, settings);
30177
+ }
30178
+ static renderRules(component, rules) {
30179
+ for (const rule of rules) {
30180
+ if (rule.platforms?.cursor?.exclude) continue;
30181
+ const lines = [];
30182
+ const description = rule.platforms?.cursor?.description ?? rule.description;
30183
+ const isAlways = rule.scope === AGENT_RULE_SCOPE.ALWAYS;
30184
+ lines.push("---");
30185
+ lines.push(`description: "${description}"`);
30186
+ lines.push(`alwaysApply: ${isAlways}`);
30187
+ if (!isAlways && rule.filePatterns && rule.filePatterns.length > 0) {
30188
+ lines.push(`path: ${JSON.stringify([...rule.filePatterns])}`);
30189
+ }
30190
+ lines.push("---");
30191
+ lines.push("");
30192
+ lines.push(...rule.content.split("\n"));
30193
+ new TextFile3(component, `.cursor/rules/${rule.name}.mdc`, { lines });
30194
+ }
30195
+ }
30196
+ static renderSkills(component, skills) {
30197
+ for (const skill of skills) {
30198
+ if (skill.platforms?.cursor?.exclude) continue;
30199
+ const lines = [];
30200
+ lines.push("---");
30201
+ lines.push(`name: "${skill.name}"`);
30202
+ lines.push(`description: "${skill.description}"`);
30203
+ if (skill.disableModelInvocation) {
30204
+ lines.push(`disable-model-invocation: true`);
30205
+ }
30206
+ if (skill.userInvocable === false) {
30207
+ lines.push(`user-invocable: false`);
30208
+ }
30209
+ if (skill.context) {
30210
+ lines.push(`context: "${skill.context}"`);
30211
+ }
30212
+ if (skill.agent) {
30213
+ lines.push(`agent: "${skill.agent}"`);
30214
+ }
30215
+ if (skill.shell) {
30216
+ lines.push(`shell: "${skill.shell}"`);
30217
+ }
30218
+ if (skill.allowedTools && skill.allowedTools.length > 0) {
30219
+ lines.push(`allowed-tools:`);
30220
+ for (const tool of skill.allowedTools) {
30221
+ lines.push(` - "${tool}"`);
30222
+ }
30223
+ }
30224
+ lines.push("---");
30225
+ lines.push("");
30226
+ lines.push(...skill.instructions.split("\n"));
30227
+ new TextFile3(component, `.cursor/skills/${skill.name}/SKILL.md`, {
30228
+ lines
30229
+ });
30230
+ if (skill.referenceFiles && skill.referenceFiles.length > 0) {
30231
+ for (const file of skill.referenceFiles) {
30232
+ new TextFile3(component, `.cursor/skills/${skill.name}/${file.path}`, {
30233
+ lines: file.content.split("\n")
30234
+ });
30235
+ }
30236
+ }
30237
+ }
30238
+ }
30239
+ static renderSubAgents(component, subAgents) {
30240
+ for (const agent of subAgents) {
30241
+ if (agent.platforms?.cursor?.exclude) continue;
30242
+ const lines = [];
30243
+ lines.push("---");
30244
+ lines.push(`name: ${agent.name}`);
30245
+ lines.push(`description: >-`);
30246
+ lines.push(` ${agent.description}`);
30247
+ if (agent.platforms?.cursor?.readonly) {
30248
+ lines.push(`readonly: true`);
30249
+ }
30250
+ if (agent.platforms?.cursor?.isBackground) {
30251
+ lines.push(`is_background: true`);
30252
+ }
30253
+ lines.push("---");
30254
+ lines.push("");
30255
+ lines.push(...agent.prompt.split("\n"));
30256
+ new TextFile3(component, `.cursor/agents/${agent.name}.md`, { lines });
30257
+ }
30258
+ }
30259
+ static renderMcpServers(component, mcpServers) {
30260
+ const serverNames = Object.keys(mcpServers);
30261
+ if (serverNames.length === 0) return;
30262
+ const obj = { mcpServers: {} };
30263
+ const servers = obj.mcpServers;
30264
+ for (const [name, config] of Object.entries(mcpServers)) {
30265
+ const server = {};
30266
+ if (config.transport) server.transport = config.transport;
30267
+ if (config.command) server.command = config.command;
30268
+ if (config.args) server.args = [...config.args];
30269
+ if (config.url) server.url = config.url;
30270
+ if (config.headers && Object.keys(config.headers).length > 0) {
30271
+ server.headers = { ...config.headers };
30272
+ }
30273
+ if (config.env) server.env = { ...config.env };
30274
+ servers[name] = server;
30275
+ }
30276
+ new JsonFile3(component, ".cursor/mcp.json", { obj });
30277
+ }
30278
+ static renderHooks(component, settings) {
30279
+ if (!settings?.hooks) return;
30280
+ const hooks = {};
30281
+ const hookEntries = settings.hooks;
30282
+ for (const [event, actions] of Object.entries(hookEntries)) {
30283
+ if (actions && actions.length > 0) {
30284
+ hooks[event] = actions.map((h) => ({
30285
+ command: h.command
30286
+ }));
30287
+ }
30288
+ }
30289
+ if (Object.keys(hooks).length === 0) return;
30290
+ new JsonFile3(component, ".cursor/hooks.json", {
30291
+ obj: { version: 1, hooks }
30292
+ });
30293
+ }
30294
+ static renderIgnoreFiles(component, settings) {
30295
+ if (settings?.ignorePatterns && settings.ignorePatterns.length > 0) {
30296
+ new TextFile3(component, ".cursorignore", {
30297
+ lines: [GENERATED_MARKER2, "", ...settings.ignorePatterns]
30298
+ });
30299
+ }
30300
+ if (settings?.indexingIgnorePatterns && settings.indexingIgnorePatterns.length > 0) {
30301
+ new TextFile3(component, ".cursorindexingignore", {
30302
+ lines: [GENERATED_MARKER2, "", ...settings.indexingIgnorePatterns]
30303
+ });
30304
+ }
30305
+ }
30306
+ };
30307
+
30308
+ // src/agent/template-resolver.ts
30309
+ var FALLBACKS = {
30310
+ "repository.owner": "<owner>",
30311
+ "repository.name": "<repo>",
30312
+ "repository.defaultBranch": "main",
30313
+ "organization.name": "<organization>",
30314
+ "organization.githubOrg": "<org>",
30315
+ "githubProject.name": "<project-name>",
30316
+ "githubProject.number": "<project-number>",
30317
+ "githubProject.nodeId": "<project-node-id>",
30318
+ docsPath: "<docs-path>",
30319
+ // The monorepo-layout seed block is additive: when absent, the
30320
+ // seeded `project-context.md` template reads cleanly without it.
30321
+ // Fall back to an empty string so no placeholder text leaks into
30322
+ // rendered prompts for repos that predate the layout contract.
30323
+ monorepoLayoutSeedBlock: ""
30324
+ };
30325
+ var TEMPLATE_RE = /\{\{(\w+(?:\.\w+)*)\}\}/g;
30326
+ function getNestedValue(obj, path8) {
30327
+ const parts = path8.split(".");
30328
+ let current = obj;
30329
+ for (const part of parts) {
30330
+ if (current == null || typeof current !== "object") {
30331
+ return void 0;
30332
+ }
30333
+ current = current[part];
30334
+ }
30335
+ if (current == null) {
30336
+ return void 0;
30337
+ }
30338
+ return String(current);
30339
+ }
30340
+ function resolveTemplateVariables(template, metadata) {
30341
+ if (!TEMPLATE_RE.test(template)) {
30342
+ return { resolved: template, unresolvedKeys: [] };
30343
+ }
30344
+ const unresolvedKeys = [];
30345
+ TEMPLATE_RE.lastIndex = 0;
30346
+ const resolved = template.replace(TEMPLATE_RE, (_match, key) => {
30347
+ if (metadata) {
30348
+ const value = getNestedValue(
30349
+ metadata,
30350
+ key
30351
+ );
30352
+ if (value !== void 0) {
30353
+ return value;
30354
+ }
30355
+ }
30356
+ unresolvedKeys.push(key);
30357
+ return FALLBACKS[key] ?? `<${key}>`;
30358
+ });
30359
+ return { resolved, unresolvedKeys };
30360
+ }
30361
+
29488
30362
  // src/agent/renderers/codex-renderer.ts
29489
30363
  var CodexRenderer = class {
29490
30364
  static render(_component, _rules, _skills, _subAgents) {
@@ -29701,6 +30575,7 @@ var SHARED_EDITING_BUNDLE_HOOKS = [
29701
30575
  ["customer-profile-workflow", "customer-profile"],
29702
30576
  ["industry-discovery-workflow", "industry-discovery"],
29703
30577
  ["meeting-agenda-workflow", "agenda"],
30578
+ ["meeting-processing-workflow", "meeting-analyst"],
29704
30579
  ["people-profile-workflow", "people-profile"],
29705
30580
  ["regulatory-research-workflow", "regulatory-research"],
29706
30581
  ["requirements-reviewer-workflow", "requirements-reviewer"],
@@ -29890,7 +30765,8 @@ var AgentConfig = class _AgentConfig extends Component8 {
29890
30765
  this.resolvedPaths,
29891
30766
  resolveIssueDefaults(this.options.issueDefaults),
29892
30767
  resolveDefaultAgentTier(this.options),
29893
- resolveBundleAgentTiers(this.options)
30768
+ resolveBundleAgentTiers(this.options),
30769
+ resolvePrReviewPolicy(this.options.prReviewPolicy)
29894
30770
  );
29895
30771
  }
29896
30772
  return this.cachedBundles;
@@ -29932,6 +30808,7 @@ var AgentConfig = class _AgentConfig extends Component8 {
29932
30808
  super.preSynthesize();
29933
30809
  validateAgentTierConfig(this.options.tiers);
29934
30810
  validateScopeGateConfig(this.options.scopeGate);
30811
+ validatePrReviewPolicyConfig(this.options.prReviewPolicy);
29935
30812
  const resolvedRunRatio = resolveRunRatio(this.options.runRatio);
29936
30813
  if (resolvedRunRatio.enabled) {
29937
30814
  this.project.gitignore.addPatterns(`/${resolvedRunRatio.stateFilePath}`);
@@ -36470,6 +37347,7 @@ export {
36470
37347
  DEFAULT_ISSUE_TEMPLATES_REQUIRE_REFERENCE,
36471
37348
  DEFAULT_OFF_PEAK_CRON_EXAMPLE,
36472
37349
  DEFAULT_PARTIAL_UNBLOCK_COMMENT_TEMPLATE,
37350
+ DEFAULT_PATHS_EXEMPT_FROM_SIZE,
36473
37351
  DEFAULT_PRIORITY_LABELS,
36474
37352
  DEFAULT_PRODUCT_CONTEXT_PATH,
36475
37353
  DEFAULT_PROGRESS_FILES_ENABLED,
@@ -36572,6 +37450,7 @@ export {
36572
37450
  buildMeetingAnalysisBundle,
36573
37451
  buildOrchestratorConventionsContent,
36574
37452
  buildPeopleProfileBundle,
37453
+ buildPrReviewBundle,
36575
37454
  buildRegulatoryResearchBundle,
36576
37455
  buildReport,
36577
37456
  buildRequirementsAnalystBundle,
@@ -36702,6 +37581,7 @@ export {
36702
37581
  resolveOrchestratorAssets,
36703
37582
  resolveOutdirFromPackageName,
36704
37583
  resolveOverrideForLabels,
37584
+ resolvePrReviewPolicy,
36705
37585
  resolveProgressFiles,
36706
37586
  resolveReactViteSiteProjectOutdir,
36707
37587
  resolveRunRatio,
@@ -36725,6 +37605,7 @@ export {
36725
37605
  validateIssueDefaultsConfig,
36726
37606
  validateIssueTemplatesConfig,
36727
37607
  validateMonorepoLayout,
37608
+ validatePrReviewPolicyConfig,
36728
37609
  validateProgressFilesConfig,
36729
37610
  validateRunRatioConfig,
36730
37611
  validateScheduledTasksConfig,