@codedrifters/configulator 0.0.328 → 0.0.330

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.js CHANGED
@@ -237,6 +237,7 @@ __export(index_exports, {
237
237
  DEFAULT_ISSUE_TEMPLATES_REQUIRE_REFERENCE: () => DEFAULT_ISSUE_TEMPLATES_REQUIRE_REFERENCE,
238
238
  DEFAULT_OFF_PEAK_CRON_EXAMPLE: () => DEFAULT_OFF_PEAK_CRON_EXAMPLE,
239
239
  DEFAULT_PARTIAL_UNBLOCK_COMMENT_TEMPLATE: () => DEFAULT_PARTIAL_UNBLOCK_COMMENT_TEMPLATE,
240
+ DEFAULT_PATHS_EXEMPT_FROM_SIZE: () => DEFAULT_PATHS_EXEMPT_FROM_SIZE,
240
241
  DEFAULT_PRIORITY_LABELS: () => DEFAULT_PRIORITY_LABELS,
241
242
  DEFAULT_PRODUCT_CONTEXT_PATH: () => DEFAULT_PRODUCT_CONTEXT_PATH,
242
243
  DEFAULT_PROGRESS_FILES_ENABLED: () => DEFAULT_PROGRESS_FILES_ENABLED,
@@ -339,6 +340,7 @@ __export(index_exports, {
339
340
  buildMeetingAnalysisBundle: () => buildMeetingAnalysisBundle,
340
341
  buildOrchestratorConventionsContent: () => buildOrchestratorConventionsContent,
341
342
  buildPeopleProfileBundle: () => buildPeopleProfileBundle,
343
+ buildPrReviewBundle: () => buildPrReviewBundle,
342
344
  buildRegulatoryResearchBundle: () => buildRegulatoryResearchBundle,
343
345
  buildReport: () => buildReport,
344
346
  buildRequirementsAnalystBundle: () => buildRequirementsAnalystBundle,
@@ -448,7 +450,6 @@ __export(index_exports, {
448
450
  renderSkillEvalsRuleContent: () => renderSkillEvalsRuleContent,
449
451
  renderSkillEvalsRunnerScript: () => renderSkillEvalsRunnerScript,
450
452
  renderSourceTierExamples: () => renderSourceTierExamples,
451
- renderStubIndexConventionRuleContent: () => renderStubIndexConventionRuleContent,
452
453
  renderTemporalFramingCheckerScript: () => renderTemporalFramingCheckerScript,
453
454
  renderTemporalFramingRuleContent: () => renderTemporalFramingRuleContent,
454
455
  renderUnblockDependentsScript: () => renderUnblockDependentsScript,
@@ -469,6 +470,7 @@ __export(index_exports, {
469
470
  resolveOrchestratorAssets: () => resolveOrchestratorAssets,
470
471
  resolveOutdirFromPackageName: () => resolveOutdirFromPackageName,
471
472
  resolveOverrideForLabels: () => resolveOverrideForLabels,
473
+ resolvePrReviewPolicy: () => resolvePrReviewPolicy,
472
474
  resolveProgressFiles: () => resolveProgressFiles,
473
475
  resolveReactViteSiteProjectOutdir: () => resolveReactViteSiteProjectOutdir,
474
476
  resolveRunRatio: () => resolveRunRatio,
@@ -492,6 +494,7 @@ __export(index_exports, {
492
494
  validateIssueDefaultsConfig: () => validateIssueDefaultsConfig,
493
495
  validateIssueTemplatesConfig: () => validateIssueTemplatesConfig,
494
496
  validateMonorepoLayout: () => validateMonorepoLayout,
497
+ validatePrReviewPolicyConfig: () => validatePrReviewPolicyConfig,
495
498
  validateProgressFilesConfig: () => validateProgressFilesConfig,
496
499
  validateRunRatioConfig: () => validateRunRatioConfig,
497
500
  validateScheduledTasksConfig: () => validateScheduledTasksConfig,
@@ -919,7 +922,8 @@ var agendaAnalystSubAgent = {
919
922
  " or cut content \u2014 do not ship a math-broken agenda.",
920
923
  "",
921
924
  "7. **Create the folder `index.md` if missing.** Populate it",
922
- " following the `stub-index-convention` rule:",
925
+ " following the section-index-page contract in the",
926
+ " `shared-editing-safety` rule:",
923
927
  " - Frontmatter `title` (short sidebar label) and `description`",
924
928
  " (one line).",
925
929
  " - A 2\u20134 sentence summary of the meeting's objective and desired",
@@ -1492,11 +1496,7 @@ var awsCdkBundle = {
1492
1496
  // src/agent/bundles/issue-templates.ts
1493
1497
  var DEFAULT_ISSUE_TEMPLATES_ENABLED = true;
1494
1498
  var DEFAULT_ISSUE_TEMPLATES_PATH = "docs/src/content/docs/agents/issue-templates.md";
1495
- var DEFAULT_ISSUE_TEMPLATES_BUNDLE_PATH_PATTERNS = [
1496
- "packages/@codedrifters/configulator/src/agent/bundles/**/*.ts",
1497
- ".claude/agents/**/*.md",
1498
- ".claude/skills/**/*.md"
1499
- ];
1499
+ var DEFAULT_ISSUE_TEMPLATES_BUNDLE_PATH_PATTERNS = [".claude/agents/**/*.md", ".claude/skills/**/*.md"];
1500
1500
  var DEFAULT_ISSUE_TEMPLATES_EMIT_CHECKER = false;
1501
1501
  var DEFAULT_ISSUE_TEMPLATES_EMIT_STARTER = false;
1502
1502
  var DEFAULT_ISSUE_TEMPLATES_REQUIRE_REFERENCE = true;
@@ -2585,6 +2585,55 @@ function renderSharedEditingRuleContent(se) {
2585
2585
  }
2586
2586
  }
2587
2587
  lines.push(
2588
+ "## Defer Shared-Index Commit to Final Pre-Push Step",
2589
+ "",
2590
+ "Even with the single-entry, deterministic-sort discipline above,",
2591
+ "two sessions that **prepare** their row inserts at the same time",
2592
+ "still race on push: whichever session pushes second sees the",
2593
+ "first session's commit on the remote and has to rebase its own",
2594
+ "commit on top, regenerating the same insert position calculation",
2595
+ "against a now-changed file. Repeated rebases multiply the chance",
2596
+ "of a mis-merge that silently drops a row.",
2597
+ "",
2598
+ "Sessions that produce both content and a shared-index row insert",
2599
+ "shrink this race window by deferring the index commit to the",
2600
+ "**final pre-push step** \u2014 after every content commit has landed",
2601
+ "locally, and immediately before `git push`:",
2602
+ "",
2603
+ "1. **Commit content first.** Write and commit every non-index",
2604
+ " change that the session produces (profile body, transcript",
2605
+ " extraction, requirement document, etc.) in its own focused",
2606
+ " commit or commits.",
2607
+ "2. **Rebase against the remote default branch** before touching",
2608
+ " the shared index:",
2609
+ "",
2610
+ " ```bash",
2611
+ " git fetch origin",
2612
+ ` git pull --${se.conflictStrategy} origin <default-branch>`,
2613
+ " ```",
2614
+ "",
2615
+ "3. **Re-read the shared index** from the now-up-to-date working",
2616
+ " tree. Another session may have appended a row while this",
2617
+ " session's content commits were in flight.",
2618
+ "4. **Re-compute the insert position** in declared sort order",
2619
+ " against the freshly-read rows. Do not assume the position",
2620
+ " computed earlier in the session is still correct.",
2621
+ "5. **Insert the row and commit the index edit on its own** \u2014",
2622
+ " one focused commit whose only file is the shared index. Run",
2623
+ " the commit-path verification (above) against that commit",
2624
+ " before continuing.",
2625
+ "6. **Push immediately.** The shorter the wall-clock gap between",
2626
+ " the rebase / re-read in step 2 and the push, the smaller the",
2627
+ " window in which another session can land a competing row.",
2628
+ "",
2629
+ "Per-agent workflows that touch a shared index file should call",
2630
+ "out this defer-to-final-commit sequence explicitly \u2014 see the",
2631
+ "`meeting-analyst` and `software-profile-analyst` sub-agent",
2632
+ "prompts for the canonical wording. The pattern is mechanical",
2633
+ "(rebase, re-read, re-compute, focused commit, push) rather than",
2634
+ "editorial, so the same recipe applies to every shared-index",
2635
+ "row-producing agent regardless of what content surrounds it.",
2636
+ "",
2588
2637
  "## Merge-Conflict Resolution",
2589
2638
  "",
2590
2639
  "When `git push` reports a conflict on a shared index file (two",
@@ -2616,7 +2665,66 @@ function renderSharedEditingRuleContent(se) {
2616
2665
  "conflicts; rewriting or reordering existing rows almost always",
2617
2666
  "does. If a shared index needs a structural change (column",
2618
2667
  "added, sort key changed), file a dedicated issue for the change",
2619
- "rather than bundling it into a content-contributing PR."
2668
+ "rather than bundling it into a content-contributing PR.",
2669
+ "",
2670
+ "## Section Index Page Shape",
2671
+ "",
2672
+ "When any agent creates or updates an `index.md` (or `README.md`)",
2673
+ "in a docs subdirectory under a Starlight content root, the file's",
2674
+ "body must include:",
2675
+ "",
2676
+ "1. **A 1\u20132 paragraph summary** of the section's purpose and how",
2677
+ " it fits into the larger research, requirements, or capability",
2678
+ " area. Readers landing on the page should understand what's in",
2679
+ " the section without falling back to the sidebar.",
2680
+ "",
2681
+ "2. **A grouped, linked listing of the directory's children**",
2682
+ " (table or bullet list). When a natural taxonomy exists (e.g.",
2683
+ " organizations grouped by sub-segment, regulations grouped by",
2684
+ " jurisdiction, capabilities grouped by tier), use it. Otherwise",
2685
+ " sort alphabetically by title. Every listing entry must link",
2686
+ " to the child page.",
2687
+ "",
2688
+ "3. **No body `# Heading`.** Starlight renders the frontmatter",
2689
+ " `title:` as the page H1 automatically \u2014 a body H1 produces a",
2690
+ " duplicate. The same no-body-H1 contract that applies to skill",
2691
+ " templates and inline agent templates also applies to every",
2692
+ " index page emitted by an agent.",
2693
+ "",
2694
+ "### When this applies",
2695
+ "",
2696
+ "The convention applies to every `index.md` / `README.md` file",
2697
+ "covered by the shared-index path globs listed above:",
2698
+ "",
2699
+ "- `docs/src/content/docs/**/index.md`",
2700
+ "- `docs/src/content/docs/**/README.md`",
2701
+ "",
2702
+ "Any agent that scaffolds a new directory under a Starlight",
2703
+ "content root MUST populate the directory's index page following",
2704
+ "this contract \u2014 a one-sentence stub is not acceptable.",
2705
+ "",
2706
+ "### Reference shapes",
2707
+ "",
2708
+ "Existing rich indexes are the canonical reference shape. Examples:",
2709
+ "",
2710
+ "- A `<MEETINGS_ROOT>/<YYYY-MM-DD>-<slug>/index.md` populated by",
2711
+ " the `agenda-analyst` bundle: frontmatter `title` /",
2712
+ " `description`, a 2\u20134 sentence summary, and a `## Documents`",
2713
+ " section listing every page in the folder.",
2714
+ "- A regulations scope index that opens with two paragraphs of",
2715
+ " context and groups every linked regulation under a",
2716
+ " `## Regulations` table by jurisdiction.",
2717
+ "- A standards-organizations index that groups every linked",
2718
+ " organization under a `## Organizations` heading by role",
2719
+ " (governance, working group, implementer).",
2720
+ "",
2721
+ "### Out of scope",
2722
+ "",
2723
+ "- Auto-generated child listings via Astro/Starlight components.",
2724
+ " Agents emit hand-curated tables or bullet lists; the rule",
2725
+ " defines the shape, not the rendering technology.",
2726
+ "- Backfilling pre-existing stub indexes is a downstream-consumer",
2727
+ " cleanup task, not a configulator change."
2620
2728
  );
2621
2729
  return lines.join("\n");
2622
2730
  }
@@ -2632,9 +2740,14 @@ function renderSharedEditingBundleHook(se, bundleLabel) {
2632
2740
  "the latest default branch before editing, insert exactly one row",
2633
2741
  "in deterministic sort position, commit the index edit in its own",
2634
2742
  "focused commit, and verify the row is present in the commit",
2635
- "before pushing. See the `shared-editing-safety` rule for the",
2636
- "full protocol, the list of files covered, and the",
2637
- "merge-conflict resolution recipe."
2743
+ "before pushing. **Defer the index commit to the final pre-push",
2744
+ "step** \u2014 land every content commit first, then rebase against",
2745
+ "the remote default branch, re-read the index, re-compute the",
2746
+ "insert position, write the row, commit the index on its own,",
2747
+ "and push immediately. See the `shared-editing-safety` rule for",
2748
+ "the full protocol, the list of files covered, the",
2749
+ "defer-to-final-commit sequence, and the merge-conflict",
2750
+ "resolution recipe."
2638
2751
  ].join("\n");
2639
2752
  }
2640
2753
  function renderSharedEditingHelperScript(_se) {
@@ -3166,71 +3279,6 @@ function assertValidProductContextPath(value) {
3166
3279
  }
3167
3280
  }
3168
3281
 
3169
- // src/agent/bundles/stub-index-convention.ts
3170
- function renderStubIndexConventionRuleContent() {
3171
- return [
3172
- "# Section Index Pages",
3173
- "",
3174
- "When any agent creates or updates an `index.md` (or `README.md`)",
3175
- "in a docs subdirectory under a Starlight content root, the file's",
3176
- "body must include:",
3177
- "",
3178
- "1. **A 1\u20132 paragraph summary** of the section's purpose and how",
3179
- " it fits into the larger research, requirements, or capability",
3180
- " area. Readers landing on the page should understand what's in",
3181
- " the section without falling back to the sidebar.",
3182
- "",
3183
- "2. **A grouped, linked listing of the directory's children**",
3184
- " (table or bullet list). When a natural taxonomy exists (e.g.",
3185
- " organizations grouped by sub-segment, regulations grouped by",
3186
- " jurisdiction, capabilities grouped by tier), use it. Otherwise",
3187
- " sort alphabetically by title. Every listing entry must link",
3188
- " to the child page.",
3189
- "",
3190
- "3. **No body `# Heading`.** Starlight renders the frontmatter",
3191
- " `title:` as the page H1 automatically \u2014 a body H1 produces a",
3192
- " duplicate. The same no-body-H1 contract that applies to skill",
3193
- " templates and inline agent templates also applies to every",
3194
- " index page emitted by an agent.",
3195
- "",
3196
- "## When this applies",
3197
- "",
3198
- "The convention applies to every `index.md` / `README.md` file",
3199
- "covered by the shared-index path globs documented in the",
3200
- "`shared-editing-safety` rule:",
3201
- "",
3202
- "- `docs/src/content/docs/**/index.md`",
3203
- "- `docs/src/content/docs/**/README.md`",
3204
- "",
3205
- "Any agent that scaffolds a new directory under a Starlight",
3206
- "content root MUST populate the directory's index page following",
3207
- "this contract \u2014 a one-sentence stub is not acceptable.",
3208
- "",
3209
- "## Reference shapes",
3210
- "",
3211
- "Existing rich indexes are the canonical reference shape. Examples:",
3212
- "",
3213
- "- A `<MEETINGS_ROOT>/<YYYY-MM-DD>-<slug>/index.md` populated by",
3214
- " the `agenda-analyst` bundle: frontmatter `title` /",
3215
- " `description`, a 2\u20134 sentence summary, and a `## Documents`",
3216
- " section listing every page in the folder.",
3217
- "- A regulations scope index that opens with two paragraphs of",
3218
- " context and groups every linked regulation under a",
3219
- " `## Regulations` table by jurisdiction.",
3220
- "- A standards-organizations index that groups every linked",
3221
- " organization under a `## Organizations` heading by role",
3222
- " (governance, working group, implementer).",
3223
- "",
3224
- "## Out of scope",
3225
- "",
3226
- "- Auto-generated child listings via Astro/Starlight components.",
3227
- " Agents emit hand-curated tables or bullet lists; the rule",
3228
- " defines the shape, not the rendering technology.",
3229
- "- Backfilling pre-existing stub indexes is a downstream-consumer",
3230
- " cleanup task, not a configulator change."
3231
- ].join("\n");
3232
- }
3233
-
3234
3282
  // src/agent/bundles/temporal-framing.ts
3235
3283
  var DEFAULT_TEMPORAL_FRAMING_ENABLED = true;
3236
3284
  var DEFAULT_TEMPORAL_FRAMING_PATHS = [
@@ -4399,6 +4447,12 @@ function buildBaseBundle(paths = DEFAULT_AGENT_PATHS) {
4399
4447
  {
4400
4448
  name: "issue-label-conventions",
4401
4449
  description: "Priority and status label taxonomy, defaults, inference rules, and blocking rules for agent-created or updated issues",
4450
+ // ALWAYS scope: every agent (and the user) needs the label
4451
+ // taxonomy whenever an issue gets created or updated, which
4452
+ // happens from any context — not only when editing agent /
4453
+ // skill / bundle source. Consumers that want to narrow the
4454
+ // load can override via `agentConfig.additionalRulePaths`
4455
+ // or `excludeRules`.
4402
4456
  scope: AGENT_RULE_SCOPE.ALWAYS,
4403
4457
  content: [
4404
4458
  "# Issue Label Conventions",
@@ -4764,7 +4818,13 @@ function buildBaseBundle(paths = DEFAULT_AGENT_PATHS) {
4764
4818
  {
4765
4819
  name: "progress-file-convention",
4766
4820
  description: "Progress-file schema and write rules, partial-resume protocol, stale-branch decision tree, and [BLOCKED] comment format that let phased agents survive crashes without losing work.",
4767
- scope: AGENT_RULE_SCOPE.ALWAYS,
4821
+ scope: AGENT_RULE_SCOPE.FILE_PATTERN,
4822
+ // Bundle defaults exclude paths into configulator's own source
4823
+ // (only meaningful when configulator is a workspace package,
4824
+ // i.e. in codedrifters/packages itself). That repo restores
4825
+ // them via `agentConfig.additionalRulePaths`; other consumers
4826
+ // get a clean default.
4827
+ filePatterns: [".claude/agents/*.md", ".claude/procedures/**"],
4768
4828
  content: renderProgressFilesRuleContent(resolveProgressFiles()),
4769
4829
  platforms: {
4770
4830
  cursor: { exclude: true }
@@ -4774,27 +4834,26 @@ function buildBaseBundle(paths = DEFAULT_AGENT_PATHS) {
4774
4834
  {
4775
4835
  name: "shared-editing-safety",
4776
4836
  description: "Shared-editing safety: single-entry deterministic-sort inserts on index files, commit-path verification, and the merge-conflict resolution recipe that keeps concurrent agent sessions from dropping each other's rows on shared registries and feature matrices.",
4777
- scope: AGENT_RULE_SCOPE.ALWAYS,
4837
+ scope: AGENT_RULE_SCOPE.FILE_PATTERN,
4838
+ filePatterns: DEFAULT_SHARED_INDEX_PATHS,
4778
4839
  content: renderSharedEditingRuleContent(resolveSharedEditing()),
4779
4840
  platforms: {
4780
4841
  cursor: { exclude: true }
4781
4842
  },
4782
4843
  tags: ["workflow"]
4783
4844
  },
4784
- {
4785
- name: "stub-index-convention",
4786
- description: "Section-index convention: every agent-emitted `index.md` / `README.md` under a Starlight docs root carries a contextual summary plus a grouped, linked listing of the directory's children \u2014 and no body `# Heading` (Starlight renders the frontmatter `title:` as the page H1).",
4787
- scope: AGENT_RULE_SCOPE.ALWAYS,
4788
- content: renderStubIndexConventionRuleContent(),
4789
- platforms: {
4790
- cursor: { exclude: true }
4791
- },
4792
- tags: ["workflow"]
4793
- },
4794
4845
  {
4795
4846
  name: "temporal-framing-convention",
4796
4847
  description: "Temporal-framing convention: every agent-authored time-sensitive factual claim (ownership, leadership tenure, regulatory status, litigation, dated metrics) carries an inline `as of [YYYY-MM-DD]` or `as of [Month YYYY]` qualifier on first occurrence so refresh agents have a mechanical signal for which claims to re-verify.",
4797
- scope: AGENT_RULE_SCOPE.ALWAYS,
4848
+ scope: AGENT_RULE_SCOPE.FILE_PATTERN,
4849
+ filePatterns: [
4850
+ "docs/src/content/docs/profiles/**/*.md",
4851
+ "docs/src/content/docs/industry-research/**/*.md",
4852
+ "docs/src/content/docs/software-research/**/*.md",
4853
+ "docs/src/content/docs/regulatory-research/**/*.md",
4854
+ "docs/src/content/docs/standards-research/**/*.md",
4855
+ "docs/src/content/docs/customer-research/**/*.md"
4856
+ ],
4798
4857
  content: renderTemporalFramingRuleContent(resolveTemporalFraming()),
4799
4858
  platforms: {
4800
4859
  cursor: { exclude: true }
@@ -4804,7 +4863,10 @@ function buildBaseBundle(paths = DEFAULT_AGENT_PATHS) {
4804
4863
  {
4805
4864
  name: "skill-evals",
4806
4865
  description: "Skill eval harness contract: declarative prompt/expected-output regression suites per skill at `<skillsRoot>/<skill-name>/evals/evals.json`, parameterised by a shared product-context fixture so the same eval shape works across every configulator-consuming project without forking fixtures.",
4807
- scope: AGENT_RULE_SCOPE.ALWAYS,
4866
+ scope: AGENT_RULE_SCOPE.FILE_PATTERN,
4867
+ // Bundle defaults exclude paths into configulator's own
4868
+ // source — see comment on progress-file-convention above.
4869
+ filePatterns: [".claude/skills/**"],
4808
4870
  content: renderSkillEvalsRuleContent(resolveSkillEvals()),
4809
4871
  platforms: {
4810
4872
  cursor: { exclude: true }
@@ -4814,7 +4876,14 @@ function buildBaseBundle(paths = DEFAULT_AGENT_PATHS) {
4814
4876
  {
4815
4877
  name: "issue-templates-convention",
4816
4878
  description: `Issue-templates convention: a single hand-authored reference page under \`${paths.docsRoot}/agents/issue-templates.md\` that carries one canonical \`gh issue create\` recipe per downstream phase label, and the reference-don't-inline rule that keeps bundle rules and agent prompts citing that page instead of duplicating full template invocations.`,
4817
- scope: AGENT_RULE_SCOPE.ALWAYS,
4879
+ scope: AGENT_RULE_SCOPE.FILE_PATTERN,
4880
+ // Bundle defaults exclude paths into configulator's own
4881
+ // source — see comment on progress-file-convention above.
4882
+ filePatterns: [
4883
+ ".claude/skills/**/SKILL.md",
4884
+ ".claude/agents/*.md",
4885
+ `${paths.docsRoot}/agents/issue-templates.md`
4886
+ ],
4818
4887
  content: renderIssueTemplatesRuleContent(resolveIssueTemplates()),
4819
4888
  platforms: {
4820
4889
  cursor: { exclude: true }
@@ -6137,8 +6206,8 @@ function buildBusinessModelsAnalystSubAgent(paths) {
6137
6206
  "",
6138
6207
  "7. **Create the segment index** at",
6139
6208
  " `<BUSINESS_MODELS_ROOT>/<industry>/segments/<SEGMENT_SLUG>/index.md`",
6140
- " if it does not already exist. Follow the",
6141
- " `stub-index-convention` rule: a 1\u20132 paragraph summary of the",
6209
+ " if it does not already exist. Follow the section-index-page",
6210
+ " contract in the `shared-editing-safety` rule: a 1\u20132 paragraph summary of the",
6142
6211
  " segment plus a linked listing of every page in the folder",
6143
6212
  " (`./business-model.md` and any value-stream / capability pages",
6144
6213
  " that land later). No body `# Heading` \u2014 the frontmatter",
@@ -10095,6 +10164,395 @@ var setIssueTypeProcedure = {
10095
10164
  `echo "$result" | jq -c '.data.updateIssueIssueType.issue'`
10096
10165
  ].join("\n")
10097
10166
  };
10167
+ var cleanMergedBranchesProcedure = {
10168
+ name: "clean-merged-branches.sh",
10169
+ 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.",
10170
+ content: [
10171
+ "#!/usr/bin/env bash",
10172
+ "# clean-merged-branches.sh \u2014 Analyse local branches against a base.",
10173
+ "#",
10174
+ "# Reports each local branch (other than HEAD and the base branch) as",
10175
+ "# MERGED, UNMERGED, EMPTY, or SKIP_WORKTREE. MERGED means every file the",
10176
+ "# branch added or modified now matches the base \u2014 so the branch content",
10177
+ "# is fully on the base even when the merge was a squash.",
10178
+ "#",
10179
+ "# This procedure is analysis-only. It never runs `git branch -D`. The",
10180
+ "# /clean-merged-branches slash-command skill wraps it with the deletion",
10181
+ "# prompt; the procedure itself is safe to invoke non-interactively from",
10182
+ "# any agent.",
10183
+ "#",
10184
+ "# Usage:",
10185
+ "# .claude/procedures/clean-merged-branches.sh # default base: main",
10186
+ "# .claude/procedures/clean-merged-branches.sh --base develop",
10187
+ "#",
10188
+ "# Output (one line per branch, stable format for grepping):",
10189
+ "# MERGED <branch> files=<n>",
10190
+ "# UNMERGED <branch> files=<n> differs=<n>",
10191
+ "# EMPTY <branch>",
10192
+ "# SKIP_WORKTREE <branch> worktree=<path>",
10193
+ "#",
10194
+ "# Exit codes:",
10195
+ "# 0 \u2014 analysis completed (whether or not any MERGED branches were found)",
10196
+ "# 2 \u2014 argument error",
10197
+ "# 3 \u2014 required command not on PATH",
10198
+ "# 4 \u2014 not a git repository",
10199
+ "",
10200
+ "set -uo pipefail",
10201
+ "",
10202
+ "# \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",
10203
+ "",
10204
+ "err() {",
10205
+ ' printf "clean-merged-branches.sh: %s\\n" "$*" >&2',
10206
+ "}",
10207
+ "",
10208
+ "usage() {",
10209
+ " cat >&2 <<'USAGE'",
10210
+ "Usage: clean-merged-branches.sh [--base <name>]",
10211
+ "",
10212
+ " --base <name> Base branch to compare against (default: main; falls",
10213
+ " back to whatever origin/HEAD points at if main is",
10214
+ " not present locally).",
10215
+ "",
10216
+ "Reports MERGED / UNMERGED / EMPTY / SKIP_WORKTREE for every local",
10217
+ "branch other than HEAD and the base branch. Does not delete anything.",
10218
+ "USAGE",
10219
+ "}",
10220
+ "",
10221
+ "# \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",
10222
+ "",
10223
+ 'base=""',
10224
+ "while [[ $# -gt 0 ]]; do",
10225
+ ' case "$1" in',
10226
+ " --base)",
10227
+ ' if [[ $# -lt 2 || -z "${2:-}" ]]; then',
10228
+ ' err "--base requires a value"',
10229
+ " usage",
10230
+ " exit 2",
10231
+ " fi",
10232
+ ' base="$2"',
10233
+ " shift 2",
10234
+ " ;;",
10235
+ " -h|--help)",
10236
+ " usage",
10237
+ " exit 0",
10238
+ " ;;",
10239
+ " *)",
10240
+ ' err "unknown argument: $1"',
10241
+ " usage",
10242
+ " exit 2",
10243
+ " ;;",
10244
+ " esac",
10245
+ "done",
10246
+ "",
10247
+ "# \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",
10248
+ "",
10249
+ "for cmd in git; do",
10250
+ ' if ! command -v "$cmd" >/dev/null 2>&1; then',
10251
+ ' err "required command not found on PATH: $cmd"',
10252
+ " exit 3",
10253
+ " fi",
10254
+ "done",
10255
+ "",
10256
+ "# \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",
10257
+ "",
10258
+ "if ! git rev-parse --git-dir >/dev/null 2>&1; then",
10259
+ ' err "not inside a git repository"',
10260
+ " exit 4",
10261
+ "fi",
10262
+ "",
10263
+ "# Default base = main if it exists locally, otherwise whatever",
10264
+ "# origin/HEAD points at (e.g. master, trunk, develop).",
10265
+ 'if [[ -z "$base" ]]; then',
10266
+ ' if git show-ref --verify --quiet "refs/heads/main"; then',
10267
+ ' base="main"',
10268
+ " else",
10269
+ " origin_head=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null || true)",
10270
+ ' if [[ -n "$origin_head" ]]; then',
10271
+ ' base="${origin_head#origin/}"',
10272
+ " else",
10273
+ ' base="main"',
10274
+ " fi",
10275
+ " fi",
10276
+ "fi",
10277
+ "",
10278
+ "# Verify the chosen base actually exists as a local branch.",
10279
+ 'if ! git show-ref --verify --quiet "refs/heads/$base"; then',
10280
+ ` err "base branch '$base' does not exist locally"`,
10281
+ " exit 4",
10282
+ "fi",
10283
+ "",
10284
+ "# Refresh remote-tracking refs and prune deleted upstream branches",
10285
+ "# so the gone-upstream signal is fresh (best effort \u2014 never fatal).",
10286
+ "git fetch --prune origin >/dev/null 2>&1 || true",
10287
+ "",
10288
+ 'current=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")',
10289
+ "",
10290
+ "# \u2500\u2500 worktree map (branch -> worktree path, excluding the current) \u2500\u2500\u2500",
10291
+ "",
10292
+ "# Use git worktree list --porcelain to detect any branch checked out in",
10293
+ "# a separate worktree. We must skip those because `git branch -D` would",
10294
+ "# refuse to delete a branch that is checked out elsewhere.",
10295
+ "#",
10296
+ "# We use parallel arrays (not an associative array) to stay compatible",
10297
+ "# with bash 3 \u2014 macOS still ships bash 3.2 as /bin/bash, and the script",
10298
+ "# is invoked via #!/usr/bin/env bash which picks whichever bash is",
10299
+ "# first on PATH. Lookups are O(n) but n is tiny (worktree count).",
10300
+ "worktree_branches=()",
10301
+ "worktree_paths=()",
10302
+ 'cwd_top=$(git rev-parse --show-toplevel 2>/dev/null || echo "")',
10303
+ 'wt_path=""',
10304
+ "while IFS= read -r line; do",
10305
+ ' case "$line" in',
10306
+ " worktree\\ *)",
10307
+ ' wt_path="${line#worktree }"',
10308
+ " ;;",
10309
+ " branch\\ *)",
10310
+ ' wt_branch="${line#branch refs/heads/}"',
10311
+ " # Skip the worktree that matches the current top-level \u2014 that one",
10312
+ " # is the *current* checkout; skipping HEAD is handled separately.",
10313
+ ' if [[ -n "$wt_path" && "$wt_path" != "$cwd_top" ]]; then',
10314
+ ' worktree_branches+=("$wt_branch")',
10315
+ ' worktree_paths+=("$wt_path")',
10316
+ " fi",
10317
+ ' wt_path=""',
10318
+ " ;;",
10319
+ ' "")',
10320
+ ' wt_path=""',
10321
+ " ;;",
10322
+ " esac",
10323
+ "done < <(git worktree list --porcelain 2>/dev/null)",
10324
+ "",
10325
+ "# Echo the worktree path for the given branch, or empty string if the",
10326
+ "# branch is not checked out in another worktree.",
10327
+ "lookup_worktree() {",
10328
+ ' local needle="$1"',
10329
+ " local i",
10330
+ ' for i in "${!worktree_branches[@]}"; do',
10331
+ ' if [[ "${worktree_branches[$i]}" == "$needle" ]]; then',
10332
+ ` printf '%s\\n' "\${worktree_paths[$i]}"`,
10333
+ " return 0",
10334
+ " fi",
10335
+ " done",
10336
+ " return 0",
10337
+ "}",
10338
+ "",
10339
+ "# \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",
10340
+ "",
10341
+ 'echo "Analysing local branches against ${base}\u2026"',
10342
+ "echo",
10343
+ "",
10344
+ "while IFS= read -r branch; do",
10345
+ ' [[ -z "$branch" ]] && continue',
10346
+ ' [[ "$branch" == "$base" ]] && continue',
10347
+ ' [[ "$branch" == "$current" ]] && continue',
10348
+ "",
10349
+ ' wt=$(lookup_worktree "$branch")',
10350
+ ' if [[ -n "$wt" ]]; then',
10351
+ ` printf 'SKIP_WORKTREE %s worktree=%s\\n' "$branch" "$wt"`,
10352
+ " continue",
10353
+ " fi",
10354
+ "",
10355
+ ' mb=$(git merge-base "$base" "$branch" 2>/dev/null || true)',
10356
+ ' if [[ -z "$mb" ]]; then',
10357
+ " # No common ancestor with base \u2014 treat as UNMERGED (don't risk it).",
10358
+ ` printf 'UNMERGED %s files=? differs=?\\n' "$branch"`,
10359
+ " continue",
10360
+ " fi",
10361
+ "",
10362
+ ' files=$(git diff --name-only "$mb" "$branch" 2>/dev/null || true)',
10363
+ ' if [[ -z "$files" ]]; then',
10364
+ ` printf 'EMPTY %s\\n' "$branch"`,
10365
+ " continue",
10366
+ " fi",
10367
+ "",
10368
+ " # Count how many of those files still differ from the base today.",
10369
+ " # We use NUL-delimited git output to survive filenames with spaces.",
10370
+ " diff_count=0",
10371
+ " while IFS= read -r -d '' f; do",
10372
+ " diff_count=$((diff_count + 1))",
10373
+ ' done < <(git diff -z --name-only "$branch" "$base" -- $files 2>/dev/null)',
10374
+ "",
10375
+ ` total=$(echo "$files" | wc -l | tr -d ' ')`,
10376
+ "",
10377
+ ' if [[ "$diff_count" -eq 0 ]]; then',
10378
+ ` printf 'MERGED %s files=%s\\n' "$branch" "$total"`,
10379
+ " else",
10380
+ ` printf 'UNMERGED %s files=%s differs=%s\\n' "$branch" "$total" "$diff_count"`,
10381
+ " fi",
10382
+ "done < <(git branch --format='%(refname:short)')",
10383
+ "",
10384
+ "exit 0"
10385
+ ].join("\n")
10386
+ };
10387
+ var cleanMergedBranchesSkill = {
10388
+ name: "clean-merged-branches",
10389
+ 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.",
10390
+ disableModelInvocation: true,
10391
+ userInvocable: true,
10392
+ platforms: { cursor: { exclude: true } },
10393
+ instructions: [
10394
+ "# Clean Merged Branches",
10395
+ "",
10396
+ "Identify local branches whose content is fully on the base branch \u2014",
10397
+ "including branches that were squash-merged (where `git branch -d`",
10398
+ "refuses because the branch commits are not ancestors of the base) \u2014",
10399
+ "report them to the user, and force-delete them after explicit",
10400
+ "confirmation.",
10401
+ "",
10402
+ "## Usage",
10403
+ "",
10404
+ "```",
10405
+ "/clean-merged-branches # default base: main",
10406
+ "/clean-merged-branches --base develop # custom base branch",
10407
+ "```",
10408
+ "",
10409
+ "### Flags",
10410
+ "",
10411
+ "- **`--base <name>`** \u2014 base branch to compare against. Defaults to",
10412
+ " `main`; falls back to whatever `origin/HEAD` points at if `main`",
10413
+ " does not exist locally.",
10414
+ "",
10415
+ "## What This Skill Does",
10416
+ "",
10417
+ "1. **Analyse.** Runs `.claude/procedures/clean-merged-branches.sh`",
10418
+ " (passing through any `--base` flag). The procedure walks every",
10419
+ " local branch and prints one stable log line per branch:",
10420
+ " - `MERGED <branch> files=<n>` \u2014 every file the branch added",
10421
+ " or modified now matches the base. Safe to delete.",
10422
+ " - `UNMERGED <branch> files=<n> differs=<n>` \u2014 branch content",
10423
+ " still differs from the base. Do NOT delete.",
10424
+ " - `EMPTY <branch>` \u2014 branch has no file changes vs. its",
10425
+ " merge-base. Safe to delete.",
10426
+ " - `SKIP_WORKTREE <branch> worktree=<path>` \u2014 branch is checked",
10427
+ " out in another worktree. Skipped (cannot be deleted while it's",
10428
+ " in use).",
10429
+ "",
10430
+ "2. **Confirm.** If the report turns up at least one `MERGED` (or",
10431
+ " `EMPTY`) branch, print the exact deletion list and a single",
10432
+ " `[y/N]` prompt. Empty input defaults to **no** and aborts.",
10433
+ "",
10434
+ "3. **Delete.** On explicit `y` / `Y`, run `git branch -D <branch>`",
10435
+ " for each confirmed branch and emit one `DELETED <branch>` line",
10436
+ " per success. On any other answer (including empty input),",
10437
+ " abort with `Aborted. No branches deleted.` and exit cleanly.",
10438
+ "",
10439
+ "If the report finds zero deletable branches, exit cleanly without",
10440
+ "prompting \u2014 there is nothing to confirm.",
10441
+ "",
10442
+ "## Behaviour Guarantees",
10443
+ "",
10444
+ "- **Always skip the current branch and the base branch.** The",
10445
+ " procedure enforces this; the skill never needs to filter again.",
10446
+ "- **Never delete without an explicit `y`.** The default on empty",
10447
+ " input is no. The skill never runs `git branch -D` until after",
10448
+ " the prompt returns `y` or `Y`.",
10449
+ "- **Content equality is the proof.** A branch classifies as",
10450
+ " `MERGED` when every file it added or modified matches the base",
10451
+ " today \u2014 handles squash merges, file deletions, and renames.",
10452
+ " The `[origin/<branch>: gone]` indicator is a useful secondary",
10453
+ " signal but is NOT the deletion gate.",
10454
+ "- **Safe across worktrees.** Branches checked out in another",
10455
+ " worktree are logged as `SKIP_WORKTREE` and excluded from the",
10456
+ " deletion list.",
10457
+ "",
10458
+ "## Output",
10459
+ "",
10460
+ "The procedure's per-branch log lines first, then (if any deletable",
10461
+ "branches exist) the confirmation prompt, then one `DELETED <branch>`",
10462
+ "line per successful deletion or `Aborted. No branches deleted.`",
10463
+ "on abort.",
10464
+ "",
10465
+ "## Implementation Recipe",
10466
+ "",
10467
+ "```bash",
10468
+ "# 1. Run the analysis-only procedure and capture its output.",
10469
+ 'report=$(.claude/procedures/clean-merged-branches.sh "$@")',
10470
+ 'printf "%s\\n" "$report"',
10471
+ "",
10472
+ "# 2. Extract the MERGED + EMPTY branch names from the report.",
10473
+ "mergeable=()",
10474
+ "while IFS= read -r line; do",
10475
+ ' case "$line" in',
10476
+ ' "MERGED "*|"EMPTY "*)',
10477
+ " # The branch name is the second whitespace-delimited token.",
10478
+ ` branch=$(echo "$line" | awk '{print $2}')`,
10479
+ ' [[ -n "$branch" ]] && mergeable+=("$branch")',
10480
+ " ;;",
10481
+ " esac",
10482
+ 'done <<< "$report"',
10483
+ "",
10484
+ "# 3. If nothing to delete, exit cleanly without prompting.",
10485
+ "if [[ ${#mergeable[@]} -eq 0 ]]; then",
10486
+ " echo",
10487
+ ' echo "No merged branches to delete."',
10488
+ " exit 0",
10489
+ "fi",
10490
+ "",
10491
+ "# 4. Show the list and prompt the user once.",
10492
+ "echo",
10493
+ 'echo "The following ${#mergeable[@]} branches are safe to delete:"',
10494
+ `printf ' %s\\n' "\${mergeable[@]}"`,
10495
+ "echo",
10496
+ 'read -r -p "Force-delete all ${#mergeable[@]} with git branch -D? [y/N] " answer',
10497
+ "",
10498
+ "# 5. On explicit y/Y only, delete each branch.",
10499
+ 'if [[ "$answer" == "y" || "$answer" == "Y" ]]; then',
10500
+ ' for b in "${mergeable[@]}"; do',
10501
+ ' if git branch -D "$b" >/dev/null 2>&1; then',
10502
+ ' echo "DELETED $b"',
10503
+ " else",
10504
+ ' echo "DELETE_FAILED $b" >&2',
10505
+ " fi",
10506
+ " done",
10507
+ "else",
10508
+ ' echo "Aborted. No branches deleted."',
10509
+ "fi",
10510
+ "```",
10511
+ "",
10512
+ "## Composability",
10513
+ "",
10514
+ "The procedure (`.claude/procedures/clean-merged-branches.sh`) is",
10515
+ "analysis-only and safe to invoke from any agent. The skill is the",
10516
+ "interactive entry point \u2014 use it when a human is at the keyboard.",
10517
+ "Background workers (orchestrator, maintenance-audit) should call",
10518
+ "the procedure directly and report its output without acting on it."
10519
+ ].join("\n"),
10520
+ referenceFiles: [
10521
+ {
10522
+ path: "evals/evals.json",
10523
+ content: JSON.stringify(
10524
+ {
10525
+ skill_name: "clean-merged-branches",
10526
+ evals: [
10527
+ {
10528
+ id: 1,
10529
+ prompt: "/clean-merged-branches",
10530
+ 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.",
10531
+ files: [],
10532
+ product_context_refs: []
10533
+ },
10534
+ {
10535
+ id: 2,
10536
+ prompt: "/clean-merged-branches --base develop",
10537
+ 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.",
10538
+ files: [],
10539
+ product_context_refs: []
10540
+ },
10541
+ {
10542
+ id: 3,
10543
+ 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.",
10544
+ 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.",
10545
+ files: [],
10546
+ product_context_refs: []
10547
+ }
10548
+ ]
10549
+ },
10550
+ null,
10551
+ 2
10552
+ )
10553
+ }
10554
+ ]
10555
+ };
10098
10556
  var githubWorkflowBundle = {
10099
10557
  name: "github-workflow",
10100
10558
  description: "GitHub issue and PR workflow automation patterns",
@@ -10116,6 +10574,10 @@ var githubWorkflowBundle = {
10116
10574
  {
10117
10575
  name: "create-issue-workflow",
10118
10576
  description: "Automated workflow for creating a new GitHub issue",
10577
+ // ALWAYS scope: users invoke "create an issue" from any
10578
+ // context, not only when editing agent / skill / bundle source.
10579
+ // Consumers that want to narrow the load can override via
10580
+ // `agentConfig.additionalRulePaths` or `excludeRules`.
10119
10581
  scope: AGENT_RULE_SCOPE.ALWAYS,
10120
10582
  content: [
10121
10583
  "# Create Issue Workflow",
@@ -10242,9 +10704,36 @@ var githubWorkflowBundle = {
10242
10704
  "- Delegate merge to the `pr-reviewer` sub-agent \u2014 do not merge manually and do not enable auto-merge directly"
10243
10705
  ].join("\n"),
10244
10706
  tags: ["workflow"]
10707
+ },
10708
+ {
10709
+ name: "branch-cleanup",
10710
+ 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).",
10711
+ scope: AGENT_RULE_SCOPE.ALWAYS,
10712
+ content: [
10713
+ "# Branch Cleanup",
10714
+ "",
10715
+ "Local branches accumulate after every merged PR. In squash-merge",
10716
+ "repositories `git branch -d` refuses to delete them because the",
10717
+ "commit hash on the base differs, even when the branch content is",
10718
+ "fully merged. The `github-workflow` bundle ships two affordances",
10719
+ "that use content-equality (not commit-graph reachability) to",
10720
+ "identify branches safe to force-delete:",
10721
+ "",
10722
+ "- `/clean-merged-branches` \u2014 interactive slash-command skill that",
10723
+ " classifies every local branch, prompts for confirmation, then",
10724
+ " runs `git branch -D` on the confirmed list. See",
10725
+ " `.claude/skills/clean-merged-branches/SKILL.md` for usage,",
10726
+ " output format, and the squash-merge verification algorithm.",
10727
+ "- `.claude/procedures/clean-merged-branches.sh` \u2014 analysis-only",
10728
+ " procedure for non-interactive agent use (orchestrator,",
10729
+ " maintenance-audit). NEVER deletes \u2014 only reports `MERGED` /",
10730
+ " `UNMERGED` / `EMPTY` / `SKIP_WORKTREE` lines."
10731
+ ].join("\n"),
10732
+ tags: ["workflow"]
10245
10733
  }
10246
10734
  ],
10247
- procedures: [setIssueTypeProcedure]
10735
+ skills: [cleanMergedBranchesSkill],
10736
+ procedures: [setIssueTypeProcedure, cleanMergedBranchesProcedure]
10248
10737
  };
10249
10738
 
10250
10739
  // src/agent/bundles/industry-discovery.ts
@@ -11881,12 +12370,14 @@ function buildMeetingAnalystSubAgent(tier) {
11881
12370
  " Interest (signal threshold still applies); capture customer",
11882
12371
  " pain points as candidate BR (not FR).",
11883
12372
  "",
11884
- "5. **Maintain the `insights/` tree index.** Ensure",
11885
- " `<meetingsRoot>/insights/index.md` and",
12373
+ "5. **Plan the `insights/` tree index update \u2014 do not commit it",
12374
+ " yet.** Ensure `<meetingsRoot>/insights/index.md` and",
11886
12375
  " `<meetingsRoot>/insights/{type}/index.md` exist (create them",
11887
- " following the `section-index` convention if missing) and append",
11888
- " a row for the current meeting's basename if one is not already",
11889
- " present.",
12376
+ " following the `section-index` convention if missing). Note",
12377
+ " the basename row this phase will eventually append, but",
12378
+ " **defer** the row insert and its commit to step 8 below so",
12379
+ " the shared-index commit can land in the smallest possible",
12380
+ " window before push.",
11890
12381
  "",
11891
12382
  "6. **Create downstream phase issues** using `gh issue create`:",
11892
12383
  " - Always create a `meeting:notes` issue (blocked on this extract issue)",
@@ -11897,7 +12388,35 @@ function buildMeetingAnalystSubAgent(tier) {
11897
12388
  " - Always create a `meeting:link` issue \u2014 blocked on the draft issue if one",
11898
12389
  " was created, otherwise blocked on the notes issue",
11899
12390
  "",
11900
- "7. Commit, push, and close the extract issue.",
12391
+ "7. **Commit the extraction content first.** Stage and commit",
12392
+ " the `insights/{type}/<basename>.md` file (and any newly",
12393
+ " created `insights/index.md` / `insights/{type}/index.md`",
12394
+ " stub pages from step 5 that do not yet exist on the remote)",
12395
+ " in a single focused commit. **Do not push yet.**",
12396
+ "",
12397
+ "8. **Defer the shared-index row insert to a final pre-push",
12398
+ " commit.** Per the `shared-editing-safety` rule's",
12399
+ " **Defer Shared-Index Commit to Final Pre-Push Step**",
12400
+ " subsection, the index update for an existing",
12401
+ " `insights/{type}/index.md` is a **shared-index row insert**",
12402
+ " that races other parallel meeting sessions writing rows into",
12403
+ " the same partition file. Apply the deferred sequence:",
12404
+ "",
12405
+ " ```bash",
12406
+ " git fetch origin",
12407
+ " git pull --rebase origin <default-branch>",
12408
+ " ```",
12409
+ "",
12410
+ " Re-read `insights/{type}/index.md` from the now-up-to-date",
12411
+ " working tree, re-compute the alphabetical insert position",
12412
+ " for the current meeting's basename row (another session may",
12413
+ " have appended a sibling row in the meantime), insert exactly",
12414
+ " one row, and commit the index edit in its **own focused",
12415
+ " commit** whose only file is the shared index. Run the",
12416
+ " commit-path verification step (`git show HEAD:<index-path>`",
12417
+ " + grep count) against that commit before pushing.",
12418
+ "",
12419
+ "9. **Push and close.** Push the branch and close the extract issue.",
11901
12420
  "",
11902
12421
  "---",
11903
12422
  "",
@@ -11957,13 +12476,41 @@ function buildMeetingAnalystSubAgent(tier) {
11957
12476
  " - Action Items (table: who, what, when)",
11958
12477
  " - Open Questions",
11959
12478
  " - Follow-up items",
11960
- "4. **Maintain the `notes/` tree index.** Ensure",
11961
- " `<meetingsRoot>/notes/index.md` and",
12479
+ "4. **Plan the `notes/` tree index update \u2014 do not commit it",
12480
+ " yet.** Ensure `<meetingsRoot>/notes/index.md` and",
11962
12481
  " `<meetingsRoot>/notes/{type}/index.md` exist (create them",
11963
- " following the `section-index` convention if missing) and append",
11964
- " a row for the current meeting's basename if one is not already",
11965
- " present.",
11966
- "5. Commit, push, and close the notes issue.",
12482
+ " following the `section-index` convention if missing). Note",
12483
+ " the basename row this phase will eventually append, but",
12484
+ " **defer** the row insert and its commit to step 6 below so",
12485
+ " the shared-index commit can land in the smallest possible",
12486
+ " window before push.",
12487
+ "5. **Commit the notes content first.** Stage and commit the",
12488
+ " `notes/{type}/<basename>.md` file (and any newly created",
12489
+ " `notes/index.md` / `notes/{type}/index.md` stub pages from",
12490
+ " step 4 that do not yet exist on the remote) in a single",
12491
+ " focused commit. **Do not push yet.**",
12492
+ "6. **Defer the shared-index row insert to a final pre-push",
12493
+ " commit.** Per the `shared-editing-safety` rule's",
12494
+ " **Defer Shared-Index Commit to Final Pre-Push Step**",
12495
+ " subsection, the index update for an existing",
12496
+ " `notes/{type}/index.md` is a **shared-index row insert**",
12497
+ " that races other parallel meeting sessions writing rows",
12498
+ " into the same partition file. Apply the deferred sequence:",
12499
+ "",
12500
+ " ```bash",
12501
+ " git fetch origin",
12502
+ " git pull --rebase origin <default-branch>",
12503
+ " ```",
12504
+ "",
12505
+ " Re-read `notes/{type}/index.md` from the now-up-to-date",
12506
+ " working tree, re-compute the alphabetical insert position",
12507
+ " for the current meeting's basename row (another session may",
12508
+ " have appended a sibling row in the meantime), insert exactly",
12509
+ " one row, and commit the index edit in its **own focused",
12510
+ " commit** whose only file is the shared index. Run the",
12511
+ " commit-path verification step against that commit before",
12512
+ " pushing.",
12513
+ "7. Push and close the notes issue.",
11967
12514
  "",
11968
12515
  "---",
11969
12516
  "",
@@ -15605,12 +16152,25 @@ var issueWorkerSubAgent = {
15605
16152
  " `file` (and optional `line`). Track `comment_id` per item so you can",
15606
16153
  " report which items were handled and which (if any) failed.",
15607
16154
  "",
15608
- " **Synthetic rebase items.** A `comment_id` of",
15609
- " `synthetic:rebase-behind-main` is the reviewer's signal that the",
15610
- " PR's head branch is BEHIND the default branch with merge conflicts",
15611
- " that `gh pr update-branch` could not resolve. For this item only,",
15612
- " the work is **not** an editorial change \u2014 it is a rebase plus",
15613
- " conflict resolution. Run the following sequence:",
16155
+ " **Synthetic rebase items.** Two `comment_id` values flag the",
16156
+ " reviewer's signal that the PR's head branch is BEHIND the default",
16157
+ " branch with merge conflicts that `gh pr update-branch` could not",
16158
+ " resolve. For either item the work is **not** an editorial change",
16159
+ " \u2014 it is a rebase plus conflict resolution. The two ids select",
16160
+ " different recipes:",
16161
+ "",
16162
+ " - `synthetic:rebase-behind-main` \u2014 generic conflict. Resolve",
16163
+ " each conflicting file by hand (read both sides, reconcile,",
16164
+ " stage), then `git rebase --continue` until the rebase",
16165
+ " completes.",
16166
+ " - `synthetic:rebase-shared-index` \u2014 every conflicting file is a",
16167
+ " row-insert race on a shared-index file (registry / index /",
16168
+ " feature-matrix under a Starlight content root). Apply the",
16169
+ " explicit re-insert recipe carried in the item's `instruction`",
16170
+ " field \u2014 do **not** hand-merge.",
16171
+ "",
16172
+ " **For `synthetic:rebase-behind-main`.** Run the following",
16173
+ " sequence:",
15614
16174
  "",
15615
16175
  " ```bash",
15616
16176
  " git fetch origin",
@@ -15634,6 +16194,75 @@ var issueWorkerSubAgent = {
15634
16194
  " history. Push with a regular non-force `git push origin <branch>`",
15635
16195
  " and report the rebased head SHA as the worker's commit.",
15636
16196
  "",
16197
+ " **For `synthetic:rebase-shared-index`.** Apply the typed",
16198
+ " re-insert recipe step-by-step. Read the recipe verbatim from the",
16199
+ " item's `instruction` field; the steps below summarise the",
16200
+ " contract the reviewer encodes and the precondition guards you",
16201
+ " must enforce:",
16202
+ "",
16203
+ " 1. **Pull and rebase** onto the default branch:",
16204
+ "",
16205
+ " ```bash",
16206
+ " git fetch origin",
16207
+ " git pull --rebase origin {{repository.defaultBranch}}",
16208
+ " ```",
16209
+ "",
16210
+ " 2. **For each conflicting file**, inspect the conflict markers",
16211
+ " against the shared-index glob set and the precondition",
16212
+ " guards. The shared-index globs (sourced from the",
16213
+ " `shared-editing-safety` rule) are:",
16214
+ "",
16215
+ ...DEFAULT_SHARED_INDEX_PATHS.map((p) => ` - \`${p}\``),
16216
+ "",
16217
+ " **Precondition guards.** Before re-inserting, confirm every",
16218
+ " `<<<<<<<` / `=======` / `>>>>>>>` hunk in the file touches",
16219
+ " only data rows (lines starting with `| ` that are not the",
16220
+ " table header or `|---|---|` separator). If any hunk touches",
16221
+ " the frontmatter (lines between the opening / closing `---`",
16222
+ " fences), the page H1, surrounding prose paragraphs, the",
16223
+ " table header row, or the separator row, **stop**, run",
16224
+ " `git rebase --abort`, and record the item as `failed` with",
16225
+ " the structured marker `BLOCKED <reason>` (e.g.",
16226
+ " `BLOCKED conflict touches table header in <path>`). Do not",
16227
+ " hand-merge \u2014 the typed recipe applies only to pure",
16228
+ " row-insert races.",
16229
+ "",
16230
+ " When the guards pass, read the rebased version of the file",
16231
+ " (it now contains the other PR's row), extract this PR's row",
16232
+ " from the `<<<<<<<` side of the conflict markers, re-insert",
16233
+ " that row in declared sort order using the file's documented",
16234
+ " sort key (alphabetical on the first column by default), and",
16235
+ " stage the file:",
16236
+ "",
16237
+ " ```bash",
16238
+ " git add <path>",
16239
+ " ```",
16240
+ "",
16241
+ " 3. **Run the commit-path verification** for each re-inserted",
16242
+ " row. The marker must appear exactly once in the staged file",
16243
+ " (zero means missing, more than one means duplicated by a",
16244
+ " mis-merge):",
16245
+ "",
16246
+ " ```bash",
16247
+ ' count=$(git show :<path> | grep -Fc "<row-unique-marker>")',
16248
+ ' [ "$count" = "1" ] || { echo "BLOCKED verification failed for <path>"; exit 1; }',
16249
+ " ```",
16250
+ "",
16251
+ " 4. **Continue the rebase and push** with a non-force push once",
16252
+ " every file is staged and verified:",
16253
+ "",
16254
+ " ```bash",
16255
+ " git rebase --continue",
16256
+ " git push origin <branch>",
16257
+ " ```",
16258
+ "",
16259
+ " On any `BLOCKED` precondition failure, exit non-zero with the",
16260
+ " `BLOCKED <reason>` line, run `git rebase --abort`, record the",
16261
+ " item as `failed`, and proceed to the report step. The reviewer",
16262
+ " will see the `failed` outcome on the next pass and fall through",
16263
+ " to the human-required hand-off via `review:awaiting-human`. Do",
16264
+ " not force-push under any circumstance.",
16265
+ "",
15637
16266
  "4. When complete, prepare a short structured report (PR number, commit",
15638
16267
  " SHAs you will push, items handled by `comment_id`, items that failed",
15639
16268
  " to apply) \u2014 you will return this after Phase 6.",
@@ -15697,11 +16326,11 @@ var issueWorkerSubAgent = {
15697
16326
  "",
15698
16327
  "**Synthetic-rebase items skip the `fix(review)` commit.** When the",
15699
16328
  "fix-list contained a `comment_id` of `synthetic:rebase-behind-main`",
15700
- "and you completed the rebase in Phase 4, the rebased commit history",
15701
- "is itself the deliverable \u2014 there is no per-item editorial change to",
15702
- "wrap in a `fix(review)` commit. Skip `git add` / `git commit` /",
15703
- "`git pull --rebase` for that item and push the rebased branch",
15704
- "directly:",
16329
+ "or `synthetic:rebase-shared-index` and you completed the rebase in",
16330
+ "Phase 4, the rebased commit history is itself the deliverable \u2014",
16331
+ "there is no per-item editorial change to wrap in a `fix(review)`",
16332
+ "commit. Skip `git add` / `git commit` / `git pull --rebase` for",
16333
+ "that item and push the rebased branch directly:",
15705
16334
  "",
15706
16335
  "```bash",
15707
16336
  "git push origin <branch-name>",
@@ -15979,7 +16608,15 @@ var orchestratorBundle = {
15979
16608
  {
15980
16609
  name: "orchestrator-conventions",
15981
16610
  description: "Guidelines for orchestrator agent behavior and pipeline management, including the funnel-tier dispatch sort, scope gate, and per-agent scheduled-task layout",
15982
- scope: AGENT_RULE_SCOPE.ALWAYS,
16611
+ scope: AGENT_RULE_SCOPE.FILE_PATTERN,
16612
+ // Bundle defaults exclude paths into configulator's own source
16613
+ // (only meaningful when configulator is a workspace package).
16614
+ // codedrifters/packages restores them via
16615
+ // `agentConfig.additionalRulePaths`.
16616
+ filePatterns: [
16617
+ ".claude/agents/orchestrator.md",
16618
+ ".claude/scheduled-tasks/**"
16619
+ ],
15983
16620
  content: buildOrchestratorConventionsContent(
15984
16621
  DEFAULT_AGENT_TIERS,
15985
16622
  resolveScopeGate(),
@@ -15988,7 +16625,6 @@ var orchestratorBundle = {
15988
16625
  resolveUnblockDependents()
15989
16626
  ),
15990
16627
  platforms: {
15991
- claude: { target: "claude-md" },
15992
16628
  cursor: { exclude: true }
15993
16629
  },
15994
16630
  tags: ["workflow"]
@@ -17112,6 +17748,32 @@ var pnpmBundle = {
17112
17748
  ]
17113
17749
  };
17114
17750
 
17751
+ // src/agent/bundles/pr-review-policy.ts
17752
+ var DEFAULT_PATHS_EXEMPT_FROM_SIZE = [
17753
+ "docs/**"
17754
+ ];
17755
+ function resolvePrReviewPolicy(config) {
17756
+ const pathsExemptFromSize = config?.autoMerge?.pathsExemptFromSize ?? DEFAULT_PATHS_EXEMPT_FROM_SIZE;
17757
+ assertValidPathsExemptFromSize(pathsExemptFromSize);
17758
+ return {
17759
+ autoMerge: {
17760
+ pathsExemptFromSize: [...pathsExemptFromSize]
17761
+ }
17762
+ };
17763
+ }
17764
+ function validatePrReviewPolicyConfig(config) {
17765
+ return resolvePrReviewPolicy(config);
17766
+ }
17767
+ function assertValidPathsExemptFromSize(paths) {
17768
+ for (const path8 of paths) {
17769
+ if (typeof path8 !== "string" || path8.trim().length === 0) {
17770
+ throw new Error(
17771
+ "prReviewPolicy.autoMerge.pathsExemptFromSize entries must be non-empty strings"
17772
+ );
17773
+ }
17774
+ }
17775
+ }
17776
+
17115
17777
  // src/agent/bundles/pr-review.ts
17116
17778
  var prReviewerSubAgent = {
17117
17779
  name: "pr-reviewer",
@@ -17302,11 +17964,16 @@ var prReviewerSubAgent = {
17302
17964
  "### Step 2: Evaluate in precedence order",
17303
17965
  "",
17304
17966
  "Walk the following checks in order. The **first match wins** and fixes",
17305
- "the mode; record the triggering condition as the `reason`. Mixed-match",
17306
- "PRs (signals from both sides) resolve conservatively to",
17307
- "`human-required` \u2014 force-auto only wins when it is the single highest",
17308
- "match and no later human-required signal changes the outcome under",
17309
- "step 2c.",
17967
+ "the mode; record the triggering condition as the `reason` **and**",
17968
+ "record the numeric **`matched_rule`** index (one of `2`, `3`, `4`,",
17969
+ "`5`, or `6` for any human-required match \u2014 see the rule numbers",
17970
+ "below) so later phases can branch on which precedence rule fired.",
17971
+ "When rule 1 (force-auto label) or rule 7 (default) fixes the mode,",
17972
+ "record `matched_rule: null` \u2014 only the human-required rules carry an",
17973
+ "actionable index. Mixed-match PRs (signals from both sides) resolve",
17974
+ "conservatively to `human-required` \u2014 force-auto only wins when it is",
17975
+ "the single highest match and no later human-required signal changes",
17976
+ "the outcome under step 2c.",
17310
17977
  "",
17311
17978
  "1. **Force-auto label** \u2014 if the PR carries any label listed under",
17312
17979
  " `auto-merge.labels-that-force-auto` (e.g. `review:auto-ok`), set",
@@ -17323,9 +17990,18 @@ var prReviewerSubAgent = {
17323
17990
  " (fetched in Phase 2) matches any entry in",
17324
17991
  " `human-required.issue-types` (case-insensitive), set",
17325
17992
  " `mode = human-required`.",
17326
- "6. **Size thresholds** \u2014 if the PR exceeds either threshold under",
17327
- " `human-required.size` (`files` count or `insertions` count), set",
17328
- " `mode = human-required`.",
17993
+ "6. **Size thresholds** \u2014 first read",
17994
+ " `auto-merge.paths-exempt-from-size` from the policy (defaults to",
17995
+ " the documented carve-out list). For every file in the PR diff,",
17996
+ " evaluate whether the file path matches at least one glob in the",
17997
+ " carve-out list. If **every** changed path matches the carve-out,",
17998
+ " **skip rule #6** entirely and continue to rule #7 \u2014 the PR is",
17999
+ " exempt from the size threshold regardless of its `files` or",
18000
+ " `insertions` count. Otherwise (any changed path falls outside",
18001
+ " the carve-out list), apply the size check: if the PR exceeds",
18002
+ " either threshold under `human-required.size` (`files` count or",
18003
+ " `insertions` count), set `mode = human-required` and record the",
18004
+ " triggered axis (files vs. insertions) as the reason.",
17329
18005
  "7. **Default** \u2014 if no rule above matched, apply the `default` field",
17330
18006
  " from the policy (typically `auto-merge`).",
17331
18007
  "",
@@ -17339,14 +18015,23 @@ var prReviewerSubAgent = {
17339
18015
  "",
17340
18016
  "### Step 3: Record the decision",
17341
18017
  "",
17342
- "Persist the evaluated mode and reason for later phases so Phase 4 and",
17343
- "any downstream summary writer can cite it:",
18018
+ "Persist the evaluated mode, the reason, and the matched-rule index",
18019
+ "for later phases so Phase 4 and any downstream summary writer can",
18020
+ "cite them:",
17344
18021
  "",
17345
18022
  "```",
17346
18023
  "Review mode: <auto-merge | human-required>",
17347
18024
  "Reason: <short explanation \u2014 label name, path+glob, issue type, size threshold, default>",
18025
+ "Matched rule: <2 | 3 | 4 | 5 | 6 | null>",
17348
18026
  "```",
17349
18027
  "",
18028
+ "`Matched rule` is the numeric index of the precedence rule that",
18029
+ "fixed the mode in Step 2. It is populated only for human-required",
18030
+ "matches (rules 2\u20136) \u2014 force-auto (rule 1) and default (rule 7)",
18031
+ "record `null`. Phase 4's `gh pr update-branch` gate consults this",
18032
+ "field to decide whether the size-only carve-out (rule 6) allows the",
18033
+ "bot to keep a human-required branch fresh against the default branch.",
18034
+ "",
17350
18035
  "Phases 3 (acceptance-criteria comparison) and CI verification run",
17351
18036
  "unchanged regardless of mode. Only the terminal action in Phase 4",
17352
18037
  "branches on the decided mode.",
@@ -17798,9 +18483,42 @@ var prReviewerSubAgent = {
17798
18483
  "gh pr view <pr-number> --json mergeStateStatus --jq '.mergeStateStatus'",
17799
18484
  "```",
17800
18485
  "",
17801
- "When the value is `BEHIND`, attempt to bring the head branch current with",
17802
- "the default branch via `gh pr update-branch` (default merge strategy \u2014",
17803
- "**never** `--rebase`, which would rewrite commits on a published branch):",
18486
+ "Before running `gh pr update-branch`, evaluate the **eligibility",
18487
+ "gate** below. The step runs when `mergeStateStatus == BEHIND` **AND**",
18488
+ "either of the following holds:",
18489
+ "",
18490
+ "- The review mode decided in Phase 2.75 is `auto-merge`, **or**",
18491
+ "- The review mode is `human-required` **and** `matched_rule == 6`",
18492
+ " (size threshold is the sole trigger that fixed the mode).",
18493
+ "",
18494
+ "All other `human-required` matches (rules 2\u20135) continue to block",
18495
+ "`update-branch`. Concretely, **never** run `gh pr update-branch`",
18496
+ "when the mode is `human-required` and any of the following fired:",
18497
+ "",
18498
+ "- rule 2 (`review:human-required` label),",
18499
+ "- rule 3 (any `labels-that-force-human` label such as `priority:critical`),",
18500
+ "- rule 4 (`human-required.paths` glob match), or",
18501
+ "- rule 5 (`human-required.issue-types` match).",
18502
+ "",
18503
+ "The rationale: rule 6 fires on the **volume** of the diff alone \u2014",
18504
+ "there is nothing about the changed paths or labels that suggests a",
18505
+ "human reviewer has explicit ownership of the branch's lifecycle.",
18506
+ "Pushing the default branch into a size-tripped human-required PR",
18507
+ "keeps it from sitting stale while the human is still drafting",
18508
+ "their review. By contrast, each of rules 2\u20135 signals a human",
18509
+ "reviewer who owns the branch's lifecycle; silently pushing main",
18510
+ "into those PRs expands the diff under review without their consent.",
18511
+ "",
18512
+ "When the gate **denies** `update-branch` (`human-required` mode and",
18513
+ "`matched_rule` in 2\u20135), record",
18514
+ "`Branch updated: not eligible (human-required by rule <N>)` and skip",
18515
+ "the rest of this sub-section. The human reviewer keeps branch-",
18516
+ "lifecycle ownership.",
18517
+ "",
18518
+ "When the gate **permits** `update-branch`, attempt to bring the head",
18519
+ "branch current with the default branch via `gh pr update-branch`",
18520
+ "(default merge strategy \u2014 **never** `--rebase`, which would rewrite",
18521
+ "commits on a published branch):",
17804
18522
  "",
17805
18523
  "```bash",
17806
18524
  "gh pr update-branch <pr-number>",
@@ -17809,8 +18527,9 @@ var prReviewerSubAgent = {
17809
18527
  "Branch on the outcome:",
17810
18528
  "",
17811
18529
  "- **Success** \u2014 record `Branch updated: yes` for the per-PR report and",
17812
- " stop. Auto-merge will fire when required checks pass on the new head",
17813
- " SHA. Do **not** poll for the merge here \u2014 Phase 5 owns polling.",
18530
+ " stop. Auto-merge (when enabled) will fire when required checks pass",
18531
+ " on the new head SHA. Do **not** poll for the merge here \u2014 Phase 5",
18532
+ " owns polling.",
17814
18533
  "- **Failure for reasons other than a merge conflict** (permission",
17815
18534
  " denied, branch protection refusing the merge commit, transient",
17816
18535
  " network error) \u2014 record `Branch updated: failed (<reason>)`, post a",
@@ -17824,13 +18543,6 @@ var prReviewerSubAgent = {
17824
18543
  "an `update-branch` attempt \u2014 every other state either has nothing to do",
17825
18544
  "or is already gated on a different signal that Phase 5 picks up.",
17826
18545
  "",
17827
- "Never run `gh pr update-branch` on a PR whose review mode is",
17828
- "`human-required`. Pushing main into a human-required PR expands the",
17829
- "scope of the diff the human is reviewing without their consent. The",
17830
- "`update-branch` step lives **only** under the `Mode auto-merge` branch",
17831
- "of this phase \u2014 `human-required` skips straight to its hand-off block",
17832
- "below.",
17833
- "",
17834
18546
  "##### Conflict-resolution delegation (BEHIND + conflicts)",
17835
18547
  "",
17836
18548
  "When `gh pr update-branch <pr-number>` fails because the merge would",
@@ -17844,9 +18556,13 @@ var prReviewerSubAgent = {
17844
18556
  "fall through to the fallback at the end of this sub-section instead.",
17845
18557
  "",
17846
18558
  "1. **Review mode is `auto-merge`.** Never delegate conflict",
17847
- " resolution on `human-required` PRs \u2014 pushing main resolutions into",
17848
- " them expands the diff the human is reviewing. (This is the same",
17849
- " reason the `update-branch` step itself is auto-merge-only.)",
18559
+ " resolution on `human-required` PRs \u2014 pushing worker-resolved",
18560
+ " merge content into them expands the diff under review without",
18561
+ " the human reviewer's consent. (Unlike the `update-branch` step",
18562
+ " above, which permits a size-only `human-required` carve-out, the",
18563
+ " conflict-resolution delegation flow is auto-merge-only across",
18564
+ " the board: a worker rebase push is a stronger branch mutation",
18565
+ " than the merge-commit `gh pr update-branch` performs.)",
17850
18566
  "2. **Delegation invocation guard permits the hand-off** \u2014 the PR",
17851
18567
  " carries the `origin:issue-worker` label, **or** the reviewer was",
17852
18568
  " invoked with `--allow-human-author`. The same guard used for the",
@@ -17871,8 +18587,53 @@ var prReviewerSubAgent = {
17871
18587
  " and conflicts there should be resolved by re-running synth, not by",
17872
18588
  " merging the conflict markers.",
17873
18589
  "",
17874
- "When every guard above passes, hand off to `issue-worker` with a",
17875
- "**single synthetic fix-list item** that describes the rebase work:",
18590
+ "When every guard above passes, **classify each conflicting file**",
18591
+ "before composing the fix-list. The classification picks one of two",
18592
+ "typed recipes \u2014 a precise `shared-index` resolver when every",
18593
+ "conflict is a row-insert race on a registry / index / feature-matrix",
18594
+ "file, or the generic rebase recipe in every other case.",
18595
+ "",
18596
+ "**Classification step.** For each conflicting path reported by the",
18597
+ "failed `update-branch`, decide whether the file is `shared-index` or",
18598
+ "`generic` against these criteria (the shared-index glob set comes",
18599
+ "from the `shared-editing-safety` rule \u2014 see the bundle's",
18600
+ "`shared-editing.ts` for the canonical constant):",
18601
+ "",
18602
+ "- `shared-index` \u2014 the path matches one of the shared-editing glob",
18603
+ " patterns:",
18604
+ ...DEFAULT_SHARED_INDEX_PATHS.map((p) => ` - \`${p}\``),
18605
+ " **AND** the conflict diff is bounded to row insertions only.",
18606
+ " Inspect the merge conflict diff for the file: every `<<<<<<<` /",
18607
+ " `=======` / `>>>>>>>` hunk must touch only data rows (lines that",
18608
+ " start with `| ` and are not the table header or the `|---|---|`",
18609
+ " separator). The conflict must **not** touch:",
18610
+ " - The frontmatter block (lines between the opening and",
18611
+ " closing `---` fences at the top of the file).",
18612
+ " - The page H1 or any surrounding prose paragraphs.",
18613
+ " - The table header row (the first `| Column | ... |` row of",
18614
+ " any table).",
18615
+ " - The separator row (`|---|---|...|`).",
18616
+ " If the conflict hunks touch any of the above, classify the file",
18617
+ " as `generic` \u2014 the mechanical row-insert recipe cannot safely",
18618
+ " reconcile structural edits.",
18619
+ "- `generic` \u2014 anything else (path outside the shared-editing glob",
18620
+ " set, **or** conflict hunks touch the frontmatter / header /",
18621
+ " separator / surrounding prose).",
18622
+ "",
18623
+ "**Branching emit step.**",
18624
+ "",
18625
+ "- **Every conflicting file is `shared-index`** \u2014 emit the typed",
18626
+ ' fix-list item with `comment_id: "synthetic:rebase-shared-index"`',
18627
+ " carrying the explicit re-insert recipe (see step 3 below). The",
18628
+ " worker mechanically re-inserts each row in declared sort order",
18629
+ " against the rebased file.",
18630
+ "- **Any conflicting file is `generic`** \u2014 emit the existing generic",
18631
+ ' fix-list item with `comment_id: "synthetic:rebase-behind-main"`',
18632
+ " (see step 3 below). The worker performs a hand-merge of the",
18633
+ " conflict markers.",
18634
+ "",
18635
+ "Hand off to `issue-worker` with the chosen single synthetic",
18636
+ "fix-list item:",
17876
18637
  "",
17877
18638
  "1. **Disable auto-merge** so a fast CI pass cannot land the PR",
17878
18639
  " mid-delegation (idempotent \u2014 safe no-op when auto-merge was never",
@@ -17891,9 +18652,18 @@ var prReviewerSubAgent = {
17891
18652
  " ```",
17892
18653
  "",
17893
18654
  "3. **Post a fix-list comment** containing exactly one synthetic item",
17894
- " describing the rebase. The `comment_id` is the literal string",
17895
- " `synthetic:rebase-behind-main` so the worker recognises the",
17896
- " special case and the next reviewer pass can identify the item:",
18655
+ " describing the rebase. The `comment_id` field selects the typed",
18656
+ " recipe:",
18657
+ "",
18658
+ " - `synthetic:rebase-behind-main` \u2014 generic conflict; the worker",
18659
+ " resolves conflicting hunks by hand.",
18660
+ " - `synthetic:rebase-shared-index` \u2014 every conflict is a pure",
18661
+ " row-insert race on a shared-index file; the worker re-inserts",
18662
+ " each row in declared sort order against the rebased file.",
18663
+ "",
18664
+ " The next reviewer pass identifies the item by its `comment_id`.",
18665
+ "",
18666
+ " **Generic shape (`synthetic:rebase-behind-main`):**",
17897
18667
  "",
17898
18668
  " ```markdown",
17899
18669
  " ## Reviewer: fix list for @issue-worker",
@@ -17912,6 +18682,27 @@ var prReviewerSubAgent = {
17912
18682
  " ```",
17913
18683
  " ```",
17914
18684
  "",
18685
+ " **Typed shape (`synthetic:rebase-shared-index`).** The",
18686
+ " `instruction` field carries the full re-insert recipe \u2014 the",
18687
+ " worker reads it imperatively, so spell every step out:",
18688
+ "",
18689
+ " ```markdown",
18690
+ " ## Reviewer: fix list for @issue-worker",
18691
+ "",
18692
+ " - [ ] @reviewer \u2014 rebase onto origin/<default-branch> and re-insert shared-index rows in: <space-separated list of conflicting files>",
18693
+ "",
18694
+ " ```json fix-list",
18695
+ " {",
18696
+ ' "pr": <pr-number>,',
18697
+ ' "branch": "<head-ref-name>",',
18698
+ ' "generated_at": "<ISO-8601 timestamp>",',
18699
+ ' "items": [',
18700
+ ' {"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."}',
18701
+ " ]",
18702
+ " }",
18703
+ " ```",
18704
+ " ```",
18705
+ "",
17915
18706
  "4. **Invoke `issue-worker` in feedback mode** with the same prompt",
17916
18707
  " shape used by the in-scope-fix flow: include the literal phrase",
17917
18708
  " `feedback mode: PR #<n>` plus the repository identifier",
@@ -17956,7 +18747,16 @@ var prReviewerSubAgent = {
17956
18747
  " gh pr edit <pr-number> --add-label 'review:awaiting-human'",
17957
18748
  " ```",
17958
18749
  "",
17959
- "2. Exit cleanly after the acceptance-criteria check completes and any",
18750
+ "2. **If `matched_rule == 6`** (size threshold was the sole trigger),",
18751
+ " run the `Update the branch when `mergeStateStatus` is `BEHIND``",
18752
+ " step from the `Mode auto-merge` branch above before exiting. The",
18753
+ " eligibility gate documented in that sub-section explicitly permits",
18754
+ " `gh pr update-branch` on size-only human-required PRs so the bot",
18755
+ " keeps the branch fresh against the default branch while the human",
18756
+ " reviews. Skip this sub-step for `matched_rule` in 2\u20135 \u2014 the gate",
18757
+ " denies `update-branch` there and the human owns branch-lifecycle.",
18758
+ "",
18759
+ "3. Exit cleanly after the acceptance-criteria check completes and any",
17960
18760
  " summary comment the reviewer posts. Proceed to Phase 5 only if a",
17961
18761
  " merge occurred \u2014 in `human-required` mode the reviewer stops at",
17962
18762
  " the hand-off and does not poll for merge.",
@@ -18201,13 +19001,20 @@ var prReviewerSubAgent = {
18201
19001
  " AC-drift pushback, any failed-fix pushback, and any human-required",
18202
19002
  " hand-off all keep auto-merge disabled until the human resolves",
18203
19003
  " the underlying state.",
18204
- "15. **Never run `gh pr update-branch` on a `human-required` PR.**",
18205
- " Pushing main into a human-required PR expands the scope of the",
18206
- " diff the human is reviewing without their consent. The",
18207
- " `update-branch` step is gated to the `Mode auto-merge` branch of",
18208
- " Phase 4 only. The same restriction applies to delegating",
19004
+ "15. **Restrict `gh pr update-branch` on `human-required` PRs.** The",
19005
+ " `update-branch` step is permitted when the review mode is",
19006
+ " `auto-merge`, **or** when the mode is `human-required` **and**",
19007
+ " Phase 2.75's `matched_rule == 6` (size threshold was the sole",
19008
+ " trigger). All other human-required matches (rule 2 force-human",
19009
+ " label, rule 3 listed force-human label, rule 4 path glob, rule 5",
19010
+ " issue type) continue to block `update-branch`: pushing main into",
19011
+ " those PRs expands the diff under review without the human",
19012
+ " reviewer's consent. The same restriction applies to delegating",
18209
19013
  " conflict resolution to `issue-worker`: never delegate a rebase",
18210
- " on a `human-required` PR.",
19014
+ " on a `human-required` PR regardless of `matched_rule` \u2014 the",
19015
+ " typed-recipe delegation flow stays auto-merge-only because a",
19016
+ " worker push to a human-required branch is a stronger mutation",
19017
+ " than the merge-commit `gh pr update-branch` performs.",
18211
19018
  "16. **Never delegate conflict resolution involving generated or",
18212
19019
  " projen-managed files.** When `gh pr update-branch` fails on a",
18213
19020
  " BEHIND PR with conflicts and any conflicting path is a lockfile,",
@@ -18375,364 +19182,442 @@ var reviewPrsSkill = {
18375
19182
  "comment on that PR and continue with the next."
18376
19183
  ].join("\n")
18377
19184
  };
18378
- var prReviewBundle = {
18379
- name: "pr-review",
18380
- description: "Pull request review workflow: verifies PRs against their linked issues' acceptance criteria and orchestrates squash-merge, single or looped over all eligible PRs",
18381
- // Default-apply: the PR review workflow is safe to include everywhere,
18382
- // and keeping review/merge policy centralised in the pr-reviewer agent
18383
- // means consumers get consistent behaviour out of the box. Consumers can
18384
- // still exclude it explicitly via `excludeBundles` if desired.
18385
- appliesWhen: () => true,
18386
- rules: [
18387
- {
18388
- name: "pr-review-policy",
18389
- description: "Declarative policy that tells the pr-reviewer which PRs may auto-merge and which must wait for a human reviewer",
18390
- scope: AGENT_RULE_SCOPE.ALWAYS,
18391
- content: [
18392
- "# PR Review Policy",
18393
- "",
18394
- "The `pr-reviewer` sub-agent evaluates every PR it reviews against the",
18395
- "policy below and routes the PR into one of two modes:",
18396
- "",
18397
- "- **`auto-merge`** \u2014 the reviewer may enable squash auto-merge once",
18398
- " all acceptance criteria are met and CI is green.",
18399
- "- **`human-required`** \u2014 the reviewer runs the full AC/CI check but",
18400
- " never calls `gh pr merge --auto`. It applies the",
18401
- " `review:awaiting-human` label and hands off to a human reviewer.",
18402
- "",
18403
- "## Policy",
18404
- "",
18405
- "```yaml",
18406
- "version: 1",
18407
- "default: auto-merge",
18408
- "",
18409
- "human-required:",
18410
- " paths:",
18411
- ' - "docs/src/content/docs/requirements/architectural-decisions/**"',
18412
- ' - "docs/src/content/docs/project-context.md"',
18413
- ' - ".github/workflows/**"',
18414
- ' - ".github/CODEOWNERS"',
18415
- ' - ".projenrc.ts"',
18416
- ' - "projenrc/**"',
18417
- ' - "CLAUDE.md"',
18418
- ' - ".claude/**"',
18419
- ' - "packages/**/package.json"',
18420
- " issue-types:",
18421
- " - release",
18422
- " - hotfix",
18423
- " size:",
18424
- " files: 10",
18425
- " insertions: 500",
18426
- " labels-that-force-human:",
18427
- ' - "review:human-required"',
18428
- ' - "priority:critical"',
18429
- "",
18430
- "auto-merge:",
18431
- " labels-that-force-auto:",
18432
- ' - "review:auto-ok"',
18433
- "```",
18434
- "",
18435
- "## Precedence",
18436
- "",
18437
- "The reviewer walks the following checks in order. The **first match**",
18438
- "fixes the mode; any mixed-match PR (signals from both sides) resolves",
18439
- "conservatively to `human-required` \u2014 `auto-merge` only wins when the",
18440
- "force-auto label is the single top-priority match.",
18441
- "",
18442
- "1. **`auto-merge.labels-that-force-auto`** \u2014 if the PR carries any of",
18443
- " these labels (e.g. `review:auto-ok`), the mode is `auto-merge`",
18444
- " outright. This is the only escape hatch from the conservative",
18445
- " default; it requires a maintainer to apply the label explicitly.",
18446
- "2. **`review:human-required` label** \u2014 reserved force-human label;",
18447
- " if present (and no force-auto label beat it in step 1), the mode",
18448
- " is `human-required`.",
18449
- "3. **`human-required.labels-that-force-human`** \u2014 any listed label on",
18450
- " the PR (e.g. `priority:critical`) forces `human-required`.",
18451
- "4. **`human-required.paths`** \u2014 if any file in the PR diff matches",
18452
- " any glob here, the mode is `human-required`. Matching uses",
18453
- " standard glob semantics (`**` for recursive directories,",
18454
- " `*` for a single path segment).",
18455
- "5. **`human-required.issue-types`** \u2014 if the linked issue's GitHub",
18456
- " issue type matches any entry (case-insensitive), the mode is",
18457
- " `human-required`.",
18458
- "6. **`human-required.size`** \u2014 if the PR exceeds either the `files`",
18459
- " count or the `insertions` count, the mode is `human-required`.",
18460
- "7. **`default`** \u2014 applied only when no rule above matched",
18461
- " (normally `auto-merge`).",
18462
- "",
18463
- "The `pr-reviewer` sub-agent records the decided mode and the triggering",
18464
- "reason in its Phase 2.75 output so downstream phases and any sticky",
18465
- "summary can cite the specific rule that applied."
18466
- ].join("\n"),
18467
- tags: ["policy", "review"]
18468
- },
18469
- {
18470
- name: "pr-review-workflow",
18471
- description: "Describes the /review-pr and /review-prs skills and their delegation to the pr-reviewer sub-agent",
18472
- scope: AGENT_RULE_SCOPE.ALWAYS,
18473
- content: [
18474
- "# PR Review Workflow",
18475
- "",
18476
- "Two skills are available, both backed by the same `pr-reviewer`",
18477
- "sub-agent:",
18478
- "",
18479
- "- **`/review-pr <pr-number>`** \u2014 review a single targeted PR.",
18480
- "- **`/review-prs`** \u2014 loop over every eligible open PR in the",
18481
- " repository and review each one in turn.",
18482
- "",
18483
- "The `pr-reviewer` sub-agent:",
18484
- "",
18485
- "1. Runs a pre-flight eligibility filter (mergeable, CI not failing,",
18486
- " has a linked issue). Ineligible PRs get a short comment and are",
18487
- " skipped.",
18488
- "2. Fetches the PR, its diff, CI status, and the linked issue",
18489
- "3. **Evaluates the PR Review Policy** (see the `PR Review Policy`",
18490
- " section above) to decide whether the PR is `auto-merge` or",
18491
- " `human-required`, and records the triggering reason",
18492
- "4. Builds a checklist from the issue's acceptance criteria",
18493
- "5. Verifies the diff satisfies each criterion and that CI is green",
18494
- "6. **Enables squash auto-merge** (with `--delete-branch`) when all",
18495
- " checks pass **and** the review mode is `auto-merge`",
18496
- "7. **Applies `review:awaiting-human`** and hands off to a human",
18497
- " reviewer when the review mode is `human-required` (no auto-merge,",
18498
- " even if every acceptance criterion is met)",
18499
- "8. **Comments with grouped findings** when any check fails (plain",
18500
- " `gh pr comment`, not a formal `--request-changes` review)",
18501
- "9. After a successful merge, verifies the linked issue is closed",
18502
- " and closes it explicitly if the merge commit did not",
18503
- "10. Cleans up the local branch after merge",
18504
- "",
18505
- "The reviewer **never** implements code and **never** pushes commits",
18506
- "to a PR's branch \u2014 it only reviews, decides, and orchestrates merge",
18507
- "or comment. In loop mode, a failed review for one PR never stops",
18508
- "the loop; the reviewer comments and moves on. See the `pr-reviewer`",
18509
- "agent definition for the full phase-by-phase contract."
18510
- ].join("\n"),
18511
- platforms: {
18512
- cursor: { exclude: true }
19185
+ function buildPrReviewBundle(policy = resolvePrReviewPolicy()) {
19186
+ return {
19187
+ name: "pr-review",
19188
+ description: "Pull request review workflow: verifies PRs against their linked issues' acceptance criteria and orchestrates squash-merge, single or looped over all eligible PRs",
19189
+ // Default-apply: the PR review workflow is safe to include everywhere,
19190
+ // and keeping review/merge policy centralised in the pr-reviewer agent
19191
+ // means consumers get consistent behaviour out of the box. Consumers can
19192
+ // still exclude it explicitly via `excludeBundles` if desired.
19193
+ appliesWhen: () => true,
19194
+ rules: [
19195
+ {
19196
+ name: "pr-review-policy",
19197
+ description: "Declarative policy that tells the pr-reviewer which PRs may auto-merge and which must wait for a human reviewer",
19198
+ scope: AGENT_RULE_SCOPE.FILE_PATTERN,
19199
+ // Bundle defaults exclude paths into configulator's own source
19200
+ // (only meaningful when configulator is a workspace package).
19201
+ // codedrifters/packages restores them via
19202
+ // `agentConfig.additionalRulePaths`.
19203
+ filePatterns: [
19204
+ ".claude/agents/pr-reviewer.md",
19205
+ ".claude/skills/review-pr/**"
19206
+ ],
19207
+ content: [
19208
+ "# PR Review Policy",
19209
+ "",
19210
+ "The `pr-reviewer` sub-agent evaluates every PR it reviews against the",
19211
+ "policy below and routes the PR into one of two modes:",
19212
+ "",
19213
+ "- **`auto-merge`** \u2014 the reviewer may enable squash auto-merge once",
19214
+ " all acceptance criteria are met and CI is green.",
19215
+ "- **`human-required`** \u2014 the reviewer runs the full AC/CI check but",
19216
+ " never calls `gh pr merge --auto`. It applies the",
19217
+ " `review:awaiting-human` label and hands off to a human reviewer.",
19218
+ "",
19219
+ "## Policy",
19220
+ "",
19221
+ "```yaml",
19222
+ "version: 1",
19223
+ "default: auto-merge",
19224
+ "",
19225
+ "human-required:",
19226
+ " paths:",
19227
+ ' - "docs/src/content/docs/requirements/architectural-decisions/**"',
19228
+ ' - "docs/src/content/docs/project-context.md"',
19229
+ ' - ".github/workflows/**"',
19230
+ ' - ".github/CODEOWNERS"',
19231
+ ' - ".projenrc.ts"',
19232
+ ' - "projenrc/**"',
19233
+ ' - "CLAUDE.md"',
19234
+ ' - ".claude/**"',
19235
+ ' - "packages/**/package.json"',
19236
+ " issue-types:",
19237
+ " - release",
19238
+ " - hotfix",
19239
+ " size:",
19240
+ " files: 10",
19241
+ " insertions: 500",
19242
+ " labels-that-force-human:",
19243
+ ' - "review:human-required"',
19244
+ ' - "priority:critical"',
19245
+ "",
19246
+ "auto-merge:",
19247
+ " labels-that-force-auto:",
19248
+ ' - "review:auto-ok"',
19249
+ " paths-exempt-from-size:",
19250
+ ...renderPathsExemptFromSizeYaml(
19251
+ policy.autoMerge.pathsExemptFromSize
19252
+ ),
19253
+ "```",
19254
+ "",
19255
+ "## Precedence",
19256
+ "",
19257
+ "The reviewer walks the following checks in order. The **first match**",
19258
+ "fixes the mode; any mixed-match PR (signals from both sides) resolves",
19259
+ "conservatively to `human-required` \u2014 `auto-merge` only wins when the",
19260
+ "force-auto label is the single top-priority match.",
19261
+ "",
19262
+ "1. **`auto-merge.labels-that-force-auto`** \u2014 if the PR carries any of",
19263
+ " these labels (e.g. `review:auto-ok`), the mode is `auto-merge`",
19264
+ " outright. This is the only escape hatch from the conservative",
19265
+ " default; it requires a maintainer to apply the label explicitly.",
19266
+ "2. **`review:human-required` label** \u2014 reserved force-human label;",
19267
+ " if present (and no force-auto label beat it in step 1), the mode",
19268
+ " is `human-required`.",
19269
+ "3. **`human-required.labels-that-force-human`** \u2014 any listed label on",
19270
+ " the PR (e.g. `priority:critical`) forces `human-required`.",
19271
+ "4. **`human-required.paths`** \u2014 if any file in the PR diff matches",
19272
+ " any glob here, the mode is `human-required`. Matching uses",
19273
+ " standard glob semantics (`**` for recursive directories,",
19274
+ " `*` for a single path segment).",
19275
+ "5. **`human-required.issue-types`** \u2014 if the linked issue's GitHub",
19276
+ " issue type matches any entry (case-insensitive), the mode is",
19277
+ " `human-required`.",
19278
+ "6. **`human-required.size`** \u2014 first read",
19279
+ " `auto-merge.paths-exempt-from-size` from the policy block above.",
19280
+ " For every file in the PR diff, evaluate whether the file path",
19281
+ " matches at least one glob in that carve-out list. If **every**",
19282
+ " changed path matches the carve-out, **skip rule #6** entirely",
19283
+ " and continue to rule #7 \u2014 the PR is exempt from the size",
19284
+ " threshold regardless of its `files` or `insertions` count.",
19285
+ " Otherwise (any changed path falls outside the carve-out list),",
19286
+ " apply the size check: if the PR exceeds either the `files`",
19287
+ " count or the `insertions` count, the mode is `human-required`.",
19288
+ "7. **`default`** \u2014 applied only when no rule above matched",
19289
+ " (normally `auto-merge`).",
19290
+ "",
19291
+ "The `auto-merge.paths-exempt-from-size` carve-out exists so",
19292
+ "**doc-only PRs** that routinely exceed the 500-insertion size",
19293
+ "threshold (large migrations, bulk additions, refresh passes)",
19294
+ "are not forced into `human-required` mode for a reason that does",
19295
+ "not reflect production risk. The default carve-out exempts the",
19296
+ "entire `docs/**` tree \u2014 every consumer of configulator places its",
19297
+ "Starlight docs site there. A PR mixing docs and code still falls",
19298
+ "into `human-required` at rule #6 because the non-docs path fails",
19299
+ "the carve-out check, so the rule only relaxes the threshold for",
19300
+ "PRs whose **every** changed path is doc-only.",
19301
+ "",
19302
+ "The `pr-reviewer` sub-agent records the decided mode, the",
19303
+ "triggering reason, and the numeric **matched-rule index** (2\u20136",
19304
+ "for human-required matches; `null` for rule 1 force-auto or",
19305
+ "rule 7 default) in its Phase 2.75 output. Downstream phases and",
19306
+ "the sticky summary cite the specific rule that applied.",
19307
+ "",
19308
+ "### Rule-#6 carve-out for `gh pr update-branch`",
19309
+ "",
19310
+ "The reviewer's BEHIND-branch refresh step (`gh pr update-branch`)",
19311
+ "is normally restricted to `auto-merge` PRs because pushing the",
19312
+ "default branch into a `human-required` PR expands the diff the",
19313
+ "human is reviewing without their consent. A narrow exception",
19314
+ "applies when rule #6 (size threshold) is the **sole** trigger",
19315
+ "for `human-required` mode: the bot may still run `gh pr",
19316
+ "update-branch` so a code-heavy size-tripped PR does not sit",
19317
+ "BEHIND while the human drafts their review.",
19318
+ "",
19319
+ "The exception is keyed on the matched-rule index recorded in",
19320
+ "Phase 2.75. All other `human-required` triggers \u2014 rule 2",
19321
+ "(`review:human-required` label), rule 3 (any",
19322
+ "`labels-that-force-human` label such as `priority:critical`),",
19323
+ "rule 4 (`human-required.paths` glob match), and rule 5",
19324
+ "(`human-required.issue-types` match) \u2014 continue to block",
19325
+ "`update-branch` because each one signals a human reviewer who",
19326
+ "has explicit ownership of the branch's lifecycle.",
19327
+ "",
19328
+ "This carve-out is largely belt-and-suspenders given the doc-only",
19329
+ "size carve-out above. Doc-only PRs that trip rule #6 now route",
19330
+ "directly to `auto-merge`, so the rule-#6 `update-branch` carve-",
19331
+ "out only kicks in for **code-heavy** PRs that legitimately trip",
19332
+ "rule #6 (mixed-content diffs whose non-doc paths fail the",
19333
+ "`paths-exempt-from-size` check, or consumers that disable the",
19334
+ "doc-only carve-out entirely)."
19335
+ ].join("\n"),
19336
+ tags: ["policy", "review"]
18513
19337
  },
18514
- tags: ["workflow"]
18515
- },
18516
- {
18517
- name: "pr-review-feedback-protocol",
18518
- 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.",
18519
- scope: AGENT_RULE_SCOPE.ALWAYS,
18520
- content: [
18521
- "# PR Review Feedback Protocol",
18522
- "",
18523
- "## Human-in-the-Loop Feedback Protocol",
18524
- "",
18525
- "The PR review pipeline is a **human-in-the-loop feedback loop**.",
18526
- "Reviewers (human or agent) leave comments on the PR; the",
18527
- "`pr-reviewer` sub-agent classifies each comment, reacts to it,",
18528
- "delegates in-scope fixes to `issue-worker`, and updates a single",
18529
- "sticky `## Reviewer notes` comment that is the canonical record of",
18530
- "PR state. The sections below document the conventions humans need",
18531
- "to read and drive that loop.",
18532
- "",
18533
- "### Trigger Model: Human-Triggered, Single-Pass",
18534
- "",
18535
- "Each reviewer pass runs exactly once and does not self-chain. A",
18536
- "human re-invokes `/review-pr <n>` (or `/review-prs`) whenever the",
18537
- "PR state changes enough to warrant another look \u2014 a new comment,",
18538
- "a new commit, a resolved pushback, a label flip. The reviewer",
18539
- "never reschedules itself and never loops back after handing off to",
18540
- "`issue-worker`: the worker's run is the terminal step of that",
18541
- "pass, and a human must re-invoke the reviewer to see the follow-up",
18542
- "reactions and the auto-merge re-enablement decision.",
18543
- "",
18544
- "This keeps the loop cheap to reason about: every agent action is",
18545
- "traceable to a specific human invocation, and there is no",
18546
- "background automation to pause or cancel.",
18547
- "",
18548
- "### Reaction State Machine",
18549
- "",
18550
- "The reviewer signals its disposition toward each human comment via",
18551
- "GitHub reactions on that comment. Five reactions carry meaning in",
18552
- "this workflow; every other reaction is ignored.",
18553
- "",
18554
- "| Reaction | Meaning | Terminal? |",
18555
- "|----------|---------|-----------|",
18556
- "| `eyes` | Seen by reviewer; no terminal decision yet. Queued for processing on this or a later pass. | No |",
18557
- "| `+1` | Reviewer accepted the comment's request; a fix has been queued or has already landed. | **Yes** |",
18558
- "| `rocket` | The accepted fix has landed on the branch. The reviewer's reply cites the commit SHA that applied it. | **Yes** |",
18559
- "| `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 |",
18560
- "| `-1` | Declined as out-of-scope. A separate tracking issue was created; the reviewer's reply links to it. | **Yes** |",
18561
- "",
18562
- "Terminal reactions (`+1`, `rocket`, `-1`) are applied **only after**",
18563
- "the corresponding action has truly completed \u2014 the fix accepted,",
18564
- "the commit landed on the branch, or the out-of-scope tracking",
18565
- "issue created and linked in a reply. The reviewer never applies a",
18566
- "terminal reaction pre-emptively.",
18567
- "",
18568
- "A comment carrying only `eyes` or `thinking_face` from the",
18569
- "reviewer is **non-terminal** and will be re-evaluated on the next",
18570
- "pass. A comment carrying any terminal reaction authored by the",
18571
- "reviewer is dropped from future classification.",
18572
- "",
18573
- "GitHub's reactions API uses `confused` as the content string for",
18574
- "the `thinking_face` reaction (`content=confused` when POSTing).",
18575
- "",
18576
- "### Resolving a Pushback",
18577
- "",
18578
- "When the reviewer pushes back on a comment with `thinking_face`,",
18579
- "auto-merge is blocked until the dispute is resolved. Humans have",
18580
- "three ways to clear a pushback:",
18581
- "",
18582
- "1. **Withdraw the comment.** Delete the comment, or edit out the",
18583
- " disputed request, then re-invoke `/review-pr <n>`. The reviewer",
18584
- " drops the withdrawn item from its queue on the next pass.",
18585
- "2. **Reply with clarification.** Post a reply on the same thread",
18586
- " that addresses the reviewer's objection (cite the acceptance",
18587
- " criterion you meant, supply the missing context, or concede the",
18588
- " point). Re-invoke `/review-pr <n>` \u2014 the reviewer re-classifies",
18589
- " the thread and may promote `thinking_face` to `+1` if the",
18590
- " clarification satisfies it.",
18591
- "3. **Force through with `review:auto-ok`.** Apply the",
18592
- " `review:auto-ok` label to the PR as an explicit maintainer",
18593
- " override. The reviewer will log the override in the sticky",
18594
- " `## Reviewer notes` comment and proceed with auto-merge even",
18595
- " though the dispute was never resolved by reply or withdrawal.",
18596
- "",
18597
- "### Fix-List Comment Format",
18598
- "",
18599
- "When Phase 4 delegates in-scope fixes to `issue-worker`, it posts",
18600
- "a single PR-level comment whose body carries both a human-readable",
18601
- "checkbox summary and a fenced ```json fix-list``` block. The JSON",
18602
- "block is the authoritative payload the worker parses; the",
18603
- "checkbox list is for humans reading the PR.",
18604
- "",
18605
- "```markdown",
18606
- "## Reviewer: fix list for @issue-worker",
18607
- "",
18608
- "- [ ] @<author> \u2014 <instruction summary> (<file>:<line>)",
18609
- "",
18610
- "```json fix-list",
18611
- "{",
18612
- ' "pr": <pr-number>,',
18613
- ' "branch": "<head-ref-name>",',
18614
- ' "generated_at": "<ISO-8601 timestamp>",',
18615
- ' "items": [',
18616
- ' {"comment_id": "<id>", "author": "<login>", "file": "<path>", "line": <n>, "instruction": "<imperative instruction>"}',
18617
- " ]",
18618
- "}",
18619
- "```",
18620
- "```",
18621
- "",
18622
- "Each `items[]` entry corresponds to one in-scope comment the",
18623
- "reviewer queued on this pass. The `comment_id` is preserved",
18624
- "exactly as returned by the GitHub API so that `issue-worker` can",
18625
- "report per-item outcomes and the reviewer can apply `rocket` or",
18626
- "`thinking_face` to the correct source comment on the next pass.",
18627
- "",
18628
- "### Sticky `## Reviewer notes` Comment",
18629
- "",
18630
- "Every PR has **one** canonical reviewer-notes comment. The",
18631
- "reviewer creates it on the first pass, then **edits it in place**",
18632
- "on every subsequent pass via",
18633
- "`gh api .../issues/comments/<id> -X PATCH`. It is never",
18634
- "duplicated and never replaced by a fresh per-pass summary.",
18635
- "",
18636
- "This sticky comment is the **single human-facing source of truth**",
18637
- "for the PR's current state. Humans scanning the PR should read",
18638
- "the sticky first, before scrolling back through individual threads.",
18639
- "It carries, at a minimum:",
18640
- "",
18641
- "- **Mode** \u2014 `auto-merge` or `human-required`, with the Phase 2.75",
18642
- " reason that chose that mode.",
18643
- "- **AC status** \u2014 met, partial, or missing, with evidence links",
18644
- " to files or tests.",
18645
- "- **CI status** \u2014 green, pending, or red.",
18646
- "- **Outstanding** \u2014 comments still carrying a non-terminal",
18647
- " reviewer reaction (`eyes`, open `thinking_face`).",
18648
- "- **Pushbacks** \u2014 every unresolved `thinking_face` the reviewer",
18649
- " has left, with the reason captured in its pushback reply.",
18650
- "- **Last pass** \u2014 the ISO 8601 timestamp of the most recent run.",
18651
- "",
18652
- "The sticky is updated on every pass \u2014 including passes that ended",
18653
- "in a pushback-gated skip, a `NEEDS_CHANGES` findings comment, or",
18654
- "a `human-required` hand-off \u2014 so it never goes stale while the",
18655
- "reviewer is actively processing the PR.",
18656
- "",
18657
- "### Label Glossary",
18658
- "",
18659
- "Five review-workflow labels drive the feedback loop. Consumers",
18660
- "that adopt this workflow are responsible for creating them in",
18661
- "their own repos (the same way they create `priority:*` and",
18662
- "`status:*` labels).",
18663
- "",
18664
- "| Label | Purpose |",
18665
- "|-------|---------|",
18666
- "| `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`. |",
18667
- "| `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. |",
18668
- "| `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. |",
18669
- "| `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). |",
18670
- "| `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. |",
18671
- "",
18672
- "### Reviewing Human-Authored PRs: the `--allow-human-author` Flag",
18673
- "",
18674
- "By default the reviewer only **delegates** in-scope fixes on",
18675
- "bot-authored PRs \u2014 those carrying the `origin:issue-worker`",
18676
- "label. Running `/review-pr <n>` or `/review-prs` on a",
18677
- "human-authored PR still produces a full review (reactions,",
18678
- "replies, sticky summary, and auto-merge when the policy allows",
18679
- "it) but skips the delegation hand-off to `issue-worker` \u2014 the",
18680
- "human author is expected to apply the fixes themselves.",
18681
- "",
18682
- "Pass `--allow-human-author` to opt into delegation on",
18683
- "human-authored PRs for a single invocation:",
18684
- "",
18685
- "```",
18686
- "/review-pr <pr-number> --allow-human-author",
18687
- "/review-prs --allow-human-author",
18688
- "```",
18689
- "",
18690
- "The flag does **not** persist across invocations. Subsequent",
18691
- "invocations return to the bot-only default and require the flag",
18692
- "to be re-supplied if delegation on a human-authored PR is desired",
18693
- "again."
18694
- ].join("\n"),
18695
- platforms: {
18696
- cursor: { exclude: true }
19338
+ {
19339
+ name: "pr-review-workflow",
19340
+ description: "Describes the /review-pr and /review-prs skills and their delegation to the pr-reviewer sub-agent",
19341
+ scope: AGENT_RULE_SCOPE.ALWAYS,
19342
+ content: [
19343
+ "# PR Review Workflow",
19344
+ "",
19345
+ "Two skills are available, both backed by the same `pr-reviewer`",
19346
+ "sub-agent:",
19347
+ "",
19348
+ "- **`/review-pr <pr-number>`** \u2014 review a single targeted PR.",
19349
+ "- **`/review-prs`** \u2014 loop over every eligible open PR in the",
19350
+ " repository and review each one in turn.",
19351
+ "",
19352
+ "The `pr-reviewer` sub-agent:",
19353
+ "",
19354
+ "1. Runs a pre-flight eligibility filter (mergeable, CI not failing,",
19355
+ " has a linked issue). Ineligible PRs get a short comment and are",
19356
+ " skipped.",
19357
+ "2. Fetches the PR, its diff, CI status, and the linked issue",
19358
+ "3. **Evaluates the PR Review Policy** (see the `PR Review Policy`",
19359
+ " section above) to decide whether the PR is `auto-merge` or",
19360
+ " `human-required`, and records the triggering reason",
19361
+ "4. Builds a checklist from the issue's acceptance criteria",
19362
+ "5. Verifies the diff satisfies each criterion and that CI is green",
19363
+ "6. **Enables squash auto-merge** (with `--delete-branch`) when all",
19364
+ " checks pass **and** the review mode is `auto-merge`",
19365
+ "7. **Applies `review:awaiting-human`** and hands off to a human",
19366
+ " reviewer when the review mode is `human-required` (no auto-merge,",
19367
+ " even if every acceptance criterion is met)",
19368
+ "8. **Comments with grouped findings** when any check fails (plain",
19369
+ " `gh pr comment`, not a formal `--request-changes` review)",
19370
+ "9. After a successful merge, verifies the linked issue is closed",
19371
+ " and closes it explicitly if the merge commit did not",
19372
+ "10. Cleans up the local branch after merge",
19373
+ "",
19374
+ "The reviewer **never** implements code and **never** pushes commits",
19375
+ "to a PR's branch \u2014 it only reviews, decides, and orchestrates merge",
19376
+ "or comment. In loop mode, a failed review for one PR never stops",
19377
+ "the loop; the reviewer comments and moves on. See the `pr-reviewer`",
19378
+ "agent definition for the full phase-by-phase contract."
19379
+ ].join("\n"),
19380
+ platforms: {
19381
+ cursor: { exclude: true }
19382
+ },
19383
+ tags: ["workflow"]
18697
19384
  },
18698
- tags: ["workflow"]
18699
- }
18700
- ],
18701
- skills: [reviewPrSkill, reviewPrsSkill],
18702
- subAgents: [prReviewerSubAgent],
18703
- labels: [
18704
- {
18705
- name: "type:pr-review",
18706
- color: "5319E7",
18707
- description: "PR review tasks"
18708
- },
18709
- {
18710
- name: "origin:issue-worker",
18711
- color: "5319E7",
18712
- description: "PR opened by the issue-worker agent"
18713
- },
18714
- {
18715
- name: "review:auto-ok",
18716
- color: "0E8A16",
18717
- description: "Force auto-merge regardless of policy"
18718
- },
18719
- {
18720
- name: "review:human-required",
18721
- color: "D93F0B",
18722
- description: "Force human review regardless of policy"
18723
- },
18724
- {
18725
- name: "review:awaiting-human",
18726
- color: "FBCA04",
18727
- description: "Reviewer handed off; awaiting human merge decision"
18728
- },
18729
- {
18730
- name: "review:fixing",
18731
- color: "D4C5F9",
18732
- description: "Short-lived lease while issue-worker applies feedback fixes"
18733
- }
18734
- ]
18735
- };
19385
+ {
19386
+ name: "pr-review-feedback-protocol",
19387
+ 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.",
19388
+ scope: AGENT_RULE_SCOPE.FILE_PATTERN,
19389
+ // Bundle defaults exclude paths into configulator's own source
19390
+ // (only meaningful when configulator is a workspace package).
19391
+ // codedrifters/packages restores them via
19392
+ // `agentConfig.additionalRulePaths`.
19393
+ filePatterns: [
19394
+ ".claude/agents/pr-reviewer.md",
19395
+ ".claude/agents/issue-worker.md"
19396
+ ],
19397
+ content: [
19398
+ "# PR Review Feedback Protocol",
19399
+ "",
19400
+ "## Human-in-the-Loop Feedback Protocol",
19401
+ "",
19402
+ "The PR review pipeline is a **human-in-the-loop feedback loop**.",
19403
+ "Reviewers (human or agent) leave comments on the PR; the",
19404
+ "`pr-reviewer` sub-agent classifies each comment, reacts to it,",
19405
+ "delegates in-scope fixes to `issue-worker`, and updates a single",
19406
+ "sticky `## Reviewer notes` comment that is the canonical record of",
19407
+ "PR state. The sections below document the conventions humans need",
19408
+ "to read and drive that loop.",
19409
+ "",
19410
+ "### Trigger Model: Human-Triggered, Single-Pass",
19411
+ "",
19412
+ "Each reviewer pass runs exactly once and does not self-chain. A",
19413
+ "human re-invokes `/review-pr <n>` (or `/review-prs`) whenever the",
19414
+ "PR state changes enough to warrant another look \u2014 a new comment,",
19415
+ "a new commit, a resolved pushback, a label flip. The reviewer",
19416
+ "never reschedules itself and never loops back after handing off to",
19417
+ "`issue-worker`: the worker's run is the terminal step of that",
19418
+ "pass, and a human must re-invoke the reviewer to see the follow-up",
19419
+ "reactions and the auto-merge re-enablement decision.",
19420
+ "",
19421
+ "This keeps the loop cheap to reason about: every agent action is",
19422
+ "traceable to a specific human invocation, and there is no",
19423
+ "background automation to pause or cancel.",
19424
+ "",
19425
+ "### Reaction State Machine",
19426
+ "",
19427
+ "The reviewer signals its disposition toward each human comment via",
19428
+ "GitHub reactions on that comment. Five reactions carry meaning in",
19429
+ "this workflow; every other reaction is ignored.",
19430
+ "",
19431
+ "| Reaction | Meaning | Terminal? |",
19432
+ "|----------|---------|-----------|",
19433
+ "| `eyes` | Seen by reviewer; no terminal decision yet. Queued for processing on this or a later pass. | No |",
19434
+ "| `+1` | Reviewer accepted the comment's request; a fix has been queued or has already landed. | **Yes** |",
19435
+ "| `rocket` | The accepted fix has landed on the branch. The reviewer's reply cites the commit SHA that applied it. | **Yes** |",
19436
+ "| `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 |",
19437
+ "| `-1` | Declined as out-of-scope. A separate tracking issue was created; the reviewer's reply links to it. | **Yes** |",
19438
+ "",
19439
+ "Terminal reactions (`+1`, `rocket`, `-1`) are applied **only after**",
19440
+ "the corresponding action has truly completed \u2014 the fix accepted,",
19441
+ "the commit landed on the branch, or the out-of-scope tracking",
19442
+ "issue created and linked in a reply. The reviewer never applies a",
19443
+ "terminal reaction pre-emptively.",
19444
+ "",
19445
+ "A comment carrying only `eyes` or `thinking_face` from the",
19446
+ "reviewer is **non-terminal** and will be re-evaluated on the next",
19447
+ "pass. A comment carrying any terminal reaction authored by the",
19448
+ "reviewer is dropped from future classification.",
19449
+ "",
19450
+ "GitHub's reactions API uses `confused` as the content string for",
19451
+ "the `thinking_face` reaction (`content=confused` when POSTing).",
19452
+ "",
19453
+ "### Resolving a Pushback",
19454
+ "",
19455
+ "When the reviewer pushes back on a comment with `thinking_face`,",
19456
+ "auto-merge is blocked until the dispute is resolved. Humans have",
19457
+ "three ways to clear a pushback:",
19458
+ "",
19459
+ "1. **Withdraw the comment.** Delete the comment, or edit out the",
19460
+ " disputed request, then re-invoke `/review-pr <n>`. The reviewer",
19461
+ " drops the withdrawn item from its queue on the next pass.",
19462
+ "2. **Reply with clarification.** Post a reply on the same thread",
19463
+ " that addresses the reviewer's objection (cite the acceptance",
19464
+ " criterion you meant, supply the missing context, or concede the",
19465
+ " point). Re-invoke `/review-pr <n>` \u2014 the reviewer re-classifies",
19466
+ " the thread and may promote `thinking_face` to `+1` if the",
19467
+ " clarification satisfies it.",
19468
+ "3. **Force through with `review:auto-ok`.** Apply the",
19469
+ " `review:auto-ok` label to the PR as an explicit maintainer",
19470
+ " override. The reviewer will log the override in the sticky",
19471
+ " `## Reviewer notes` comment and proceed with auto-merge even",
19472
+ " though the dispute was never resolved by reply or withdrawal.",
19473
+ "",
19474
+ "### Fix-List Comment Format",
19475
+ "",
19476
+ "When Phase 4 delegates in-scope fixes to `issue-worker`, it posts",
19477
+ "a single PR-level comment whose body carries both a human-readable",
19478
+ "checkbox summary and a fenced ```json fix-list``` block. The JSON",
19479
+ "block is the authoritative payload the worker parses; the",
19480
+ "checkbox list is for humans reading the PR.",
19481
+ "",
19482
+ "```markdown",
19483
+ "## Reviewer: fix list for @issue-worker",
19484
+ "",
19485
+ "- [ ] @<author> \u2014 <instruction summary> (<file>:<line>)",
19486
+ "",
19487
+ "```json fix-list",
19488
+ "{",
19489
+ ' "pr": <pr-number>,',
19490
+ ' "branch": "<head-ref-name>",',
19491
+ ' "generated_at": "<ISO-8601 timestamp>",',
19492
+ ' "items": [',
19493
+ ' {"comment_id": "<id>", "author": "<login>", "file": "<path>", "line": <n>, "instruction": "<imperative instruction>"}',
19494
+ " ]",
19495
+ "}",
19496
+ "```",
19497
+ "```",
19498
+ "",
19499
+ "Each `items[]` entry corresponds to one in-scope comment the",
19500
+ "reviewer queued on this pass. The `comment_id` is preserved",
19501
+ "exactly as returned by the GitHub API so that `issue-worker` can",
19502
+ "report per-item outcomes and the reviewer can apply `rocket` or",
19503
+ "`thinking_face` to the correct source comment on the next pass.",
19504
+ "",
19505
+ "### Sticky `## Reviewer notes` Comment",
19506
+ "",
19507
+ "Every PR has **one** canonical reviewer-notes comment. The",
19508
+ "reviewer creates it on the first pass, then **edits it in place**",
19509
+ "on every subsequent pass via",
19510
+ "`gh api .../issues/comments/<id> -X PATCH`. It is never",
19511
+ "duplicated and never replaced by a fresh per-pass summary.",
19512
+ "",
19513
+ "This sticky comment is the **single human-facing source of truth**",
19514
+ "for the PR's current state. Humans scanning the PR should read",
19515
+ "the sticky first, before scrolling back through individual threads.",
19516
+ "It carries, at a minimum:",
19517
+ "",
19518
+ "- **Mode** \u2014 `auto-merge` or `human-required`, with the Phase 2.75",
19519
+ " reason that chose that mode.",
19520
+ "- **AC status** \u2014 met, partial, or missing, with evidence links",
19521
+ " to files or tests.",
19522
+ "- **CI status** \u2014 green, pending, or red.",
19523
+ "- **Outstanding** \u2014 comments still carrying a non-terminal",
19524
+ " reviewer reaction (`eyes`, open `thinking_face`).",
19525
+ "- **Pushbacks** \u2014 every unresolved `thinking_face` the reviewer",
19526
+ " has left, with the reason captured in its pushback reply.",
19527
+ "- **Last pass** \u2014 the ISO 8601 timestamp of the most recent run.",
19528
+ "",
19529
+ "The sticky is updated on every pass \u2014 including passes that ended",
19530
+ "in a pushback-gated skip, a `NEEDS_CHANGES` findings comment, or",
19531
+ "a `human-required` hand-off \u2014 so it never goes stale while the",
19532
+ "reviewer is actively processing the PR.",
19533
+ "",
19534
+ "### Label Glossary",
19535
+ "",
19536
+ "Five review-workflow labels drive the feedback loop. Consumers",
19537
+ "that adopt this workflow are responsible for creating them in",
19538
+ "their own repos (the same way they create `priority:*` and",
19539
+ "`status:*` labels).",
19540
+ "",
19541
+ "| Label | Purpose |",
19542
+ "|-------|---------|",
19543
+ "| `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`. |",
19544
+ "| `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. |",
19545
+ "| `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. |",
19546
+ "| `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). |",
19547
+ "| `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. |",
19548
+ "",
19549
+ "### Reviewing Human-Authored PRs: the `--allow-human-author` Flag",
19550
+ "",
19551
+ "By default the reviewer only **delegates** in-scope fixes on",
19552
+ "bot-authored PRs \u2014 those carrying the `origin:issue-worker`",
19553
+ "label. Running `/review-pr <n>` or `/review-prs` on a",
19554
+ "human-authored PR still produces a full review (reactions,",
19555
+ "replies, sticky summary, and auto-merge when the policy allows",
19556
+ "it) but skips the delegation hand-off to `issue-worker` \u2014 the",
19557
+ "human author is expected to apply the fixes themselves.",
19558
+ "",
19559
+ "Pass `--allow-human-author` to opt into delegation on",
19560
+ "human-authored PRs for a single invocation:",
19561
+ "",
19562
+ "```",
19563
+ "/review-pr <pr-number> --allow-human-author",
19564
+ "/review-prs --allow-human-author",
19565
+ "```",
19566
+ "",
19567
+ "The flag does **not** persist across invocations. Subsequent",
19568
+ "invocations return to the bot-only default and require the flag",
19569
+ "to be re-supplied if delegation on a human-authored PR is desired",
19570
+ "again."
19571
+ ].join("\n"),
19572
+ platforms: {
19573
+ cursor: { exclude: true }
19574
+ },
19575
+ tags: ["workflow"]
19576
+ }
19577
+ ],
19578
+ skills: [reviewPrSkill, reviewPrsSkill],
19579
+ subAgents: [prReviewerSubAgent],
19580
+ labels: [
19581
+ {
19582
+ name: "type:pr-review",
19583
+ color: "5319E7",
19584
+ description: "PR review tasks"
19585
+ },
19586
+ {
19587
+ name: "origin:issue-worker",
19588
+ color: "5319E7",
19589
+ description: "PR opened by the issue-worker agent"
19590
+ },
19591
+ {
19592
+ name: "review:auto-ok",
19593
+ color: "0E8A16",
19594
+ description: "Force auto-merge regardless of policy"
19595
+ },
19596
+ {
19597
+ name: "review:human-required",
19598
+ color: "D93F0B",
19599
+ description: "Force human review regardless of policy"
19600
+ },
19601
+ {
19602
+ name: "review:awaiting-human",
19603
+ color: "FBCA04",
19604
+ description: "Reviewer handed off; awaiting human merge decision"
19605
+ },
19606
+ {
19607
+ name: "review:fixing",
19608
+ color: "D4C5F9",
19609
+ description: "Short-lived lease while issue-worker applies feedback fixes"
19610
+ }
19611
+ ]
19612
+ };
19613
+ }
19614
+ function renderPathsExemptFromSizeYaml(paths) {
19615
+ if (paths.length === 0) {
19616
+ return [" []"];
19617
+ }
19618
+ return paths.map((path8) => ` - "${path8}"`);
19619
+ }
19620
+ var prReviewBundle = buildPrReviewBundle();
18736
19621
 
18737
19622
  // src/agent/bundles/projen.ts
18738
19623
  var projenBundle = {
@@ -18743,6 +19628,10 @@ var projenBundle = {
18743
19628
  {
18744
19629
  name: "development-commands",
18745
19630
  description: "Projen development commands for building, testing, linting, and validating changes",
19631
+ // ALWAYS scope: developers ask "how do I build/test/lint?" from
19632
+ // anywhere in the repo, not only when editing projen config.
19633
+ // Consumers that want to narrow the load can override via
19634
+ // `agentConfig.additionalRulePaths` or `excludeRules`.
18746
19635
  scope: AGENT_RULE_SCOPE.ALWAYS,
18747
19636
  content: [
18748
19637
  "# Development Commands",
@@ -19451,7 +20340,8 @@ function buildRegulatoryResearchAnalystSubAgent(paths, issueDefaults) {
19451
20340
  "7. **Create or update the scope index page** at",
19452
20341
  " `<SCOPE_INDEX_PAGE>` so downstream regulations link back to a",
19453
20342
  " coherent landing page for the scope. Follow the",
19454
- " `stub-index-convention` rule: the body must carry a 1\u20132",
20343
+ " section-index-page contract in the `shared-editing-safety` rule:",
20344
+ " the body must carry a 1\u20132",
19455
20345
  " paragraph contextual summary plus a grouped, linked listing of",
19456
20346
  " every regulation in the scope (e.g. by jurisdiction). No body",
19457
20347
  " `# Heading` \u2014 the frontmatter `title:` already renders as the",
@@ -25795,8 +26685,39 @@ function buildSoftwareProfileAnalystSubAgent(paths, issueDefaults, tier) {
25795
26685
  " every profile is mapped back to the capability model regardless",
25796
26686
  " of whether any adjacent products were surfaced.",
25797
26687
  "",
25798
- "7. **Commit and push** the matrix file (and any profile updates).",
25799
- " Close the matrix issue.",
26688
+ "7. **Commit any profile updates first \u2014 do not touch the",
26689
+ " matrix yet.** If step 4 surfaced edits to the per-product",
26690
+ " profile file (e.g. notes added to `## Risks / Open",
26691
+ " Questions` about column drift), stage and commit those",
26692
+ " profile changes in a focused commit. **Do not push yet,**",
26693
+ " and **do not** include the matrix file in this commit.",
26694
+ "",
26695
+ "8. **Defer the feature-matrix row insert to a final pre-push",
26696
+ " commit.** Per the `shared-editing-safety` rule's",
26697
+ " **Defer Shared-Index Commit to Final Pre-Push Step**",
26698
+ " subsection, the feature matrix is the canonical example of",
26699
+ " a **shared index file** \u2014 multiple software-profile",
26700
+ " sessions racing to append rows for different products is",
26701
+ " the exact contention this rule exists to mitigate. Apply",
26702
+ " the deferred sequence:",
26703
+ "",
26704
+ " ```bash",
26705
+ " git fetch origin",
26706
+ " git pull --rebase origin <default-branch>",
26707
+ " ```",
26708
+ "",
26709
+ " Re-read `<MATRIX_FILE>` from the now-up-to-date working",
26710
+ " tree, re-compute the deterministic insert position for the",
26711
+ " current product's rows (another session may have appended",
26712
+ " rows for a different product, or extended the segment",
26713
+ " columns, in the meantime), insert the rows in sort order,",
26714
+ " and commit the matrix edit in its **own focused commit**",
26715
+ " whose only file is `<MATRIX_FILE>`. Run the commit-path",
26716
+ " verification step (`git show HEAD:<MATRIX_FILE>` + grep",
26717
+ " count on the product slug as the unique marker) against",
26718
+ " that commit before pushing.",
26719
+ "",
26720
+ "9. **Push and close.** Push the branch and close the matrix issue.",
25800
26721
  "",
25801
26722
  "---",
25802
26723
  "",
@@ -26905,7 +27826,8 @@ function buildStandardsResearchAnalystSubAgent(paths, issueDefaults) {
26905
27826
  " pages.",
26906
27827
  "",
26907
27828
  "4. **Write the overview page** at `<OVERVIEW_PAGE>`. Follow",
26908
- " the `stub-index-convention` rule: the body opens with a",
27829
+ " the section-index-page contract in the",
27830
+ " `shared-editing-safety` rule: the body opens with a",
26909
27831
  " 1\u20132 paragraph contextual summary of the standard and its",
26910
27832
  " versions, and the version pages, modules, and extensions",
26911
27833
  " sections below double as the grouped, linked listing of",
@@ -28738,6 +29660,62 @@ function bundleNameForWorkflowRule(ruleName) {
28738
29660
  );
28739
29661
  return entry?.bundle;
28740
29662
  }
29663
+ function buildConventionsRegistryRule(rules) {
29664
+ const entries = [];
29665
+ for (const rule of rules) {
29666
+ if (rule.scope !== AGENT_RULE_SCOPE.FILE_PATTERN) {
29667
+ continue;
29668
+ }
29669
+ if (rule.platforms?.claude?.exclude === true) {
29670
+ continue;
29671
+ }
29672
+ if (!rule.filePatterns || rule.filePatterns.length === 0) {
29673
+ continue;
29674
+ }
29675
+ entries.push(rule);
29676
+ }
29677
+ if (entries.length === 0) {
29678
+ return void 0;
29679
+ }
29680
+ entries.sort((a, b) => a.name.localeCompare(b.name));
29681
+ const lines = [
29682
+ "# Conventions Registry",
29683
+ "",
29684
+ "Each row below is a `FILE_PATTERN`-scoped convention rule that",
29685
+ "auto-loads on demand when the agent's current file matches one",
29686
+ "of the listed paths. Full rule bodies live under",
29687
+ "`.claude/rules/<slug>.md` so they only consume context when",
29688
+ "they apply. Consult the matching rule before doing work in any",
29689
+ "of the listed surfaces.",
29690
+ "",
29691
+ "| Rule | Auto-loads when editing | Purpose |",
29692
+ "| --- | --- | --- |"
29693
+ ];
29694
+ const MAX_PATTERNS_SHOWN = 3;
29695
+ for (const rule of entries) {
29696
+ const ruleLink = `[\`${rule.name}\`](.claude/rules/${rule.name}.md)`;
29697
+ const patterns = rule.filePatterns ?? [];
29698
+ let patternCell;
29699
+ if (patterns.length <= MAX_PATTERNS_SHOWN) {
29700
+ patternCell = patterns.map((p) => `\`${p}\``).join(", ");
29701
+ } else {
29702
+ const shown = patterns.slice(0, MAX_PATTERNS_SHOWN).map((p) => `\`${p}\``).join(", ");
29703
+ patternCell = `${shown}, \u2026 ([full list](.claude/rules/${rule.name}.md))`;
29704
+ }
29705
+ const purposeCell = rule.description.replace(/\s+/g, " ").replace(/\|/g, "\\|").trim();
29706
+ lines.push(`| ${ruleLink} | ${patternCell} | ${purposeCell} |`);
29707
+ }
29708
+ return {
29709
+ name: "conventions-registry",
29710
+ description: "Routing table for every FILE_PATTERN-scoped convention rule. Lists each rule's slug, the file patterns that auto-trigger it, and a one-line purpose so agents whose current file does not match any rule's paths: can still discover the full convention set.",
29711
+ scope: AGENT_RULE_SCOPE.ALWAYS,
29712
+ content: lines.join("\n"),
29713
+ platforms: {
29714
+ cursor: { exclude: true }
29715
+ },
29716
+ tags: ["workflow"]
29717
+ };
29718
+ }
28741
29719
  function buildAgentRegistryRule(bundles, paths) {
28742
29720
  const activeNames = new Set(bundles.map((b) => b.name));
28743
29721
  const rows = AGENT_REGISTRY_ENTRIES.filter(
@@ -29153,7 +30131,7 @@ function renderPriorityRulesSection(rules) {
29153
30131
  }
29154
30132
 
29155
30133
  // src/agent/bundles/index.ts
29156
- function buildBuiltInBundles(paths = DEFAULT_AGENT_PATHS, issueDefaults = DEFAULT_RESOLVED_ISSUE_DEFAULTS, defaultAgentTier = AGENT_MODEL.BALANCED, bundleAgentTiers = /* @__PURE__ */ new Map()) {
30134
+ function buildBuiltInBundles(paths = DEFAULT_AGENT_PATHS, issueDefaults = DEFAULT_RESOLVED_ISSUE_DEFAULTS, defaultAgentTier = AGENT_MODEL.BALANCED, bundleAgentTiers = /* @__PURE__ */ new Map(), prReviewPolicy = resolvePrReviewPolicy()) {
29157
30135
  const tierFor = (bundle) => bundleAgentTiers.get(bundle) ?? defaultAgentTier;
29158
30136
  return [
29159
30137
  buildBaseBundle(paths),
@@ -29170,7 +30148,7 @@ function buildBuiltInBundles(paths = DEFAULT_AGENT_PATHS, issueDefaults = DEFAUL
29170
30148
  buildMeetingAnalysisBundle(tierFor("meeting-analysis")),
29171
30149
  agendaBundle,
29172
30150
  orchestratorBundle,
29173
- prReviewBundle,
30151
+ buildPrReviewBundle(prReviewPolicy),
29174
30152
  buildRequirementsAnalystBundle(paths, issueDefaults),
29175
30153
  buildRequirementsWriterBundle(paths, issueDefaults),
29176
30154
  buildRequirementsReviewerBundle(paths, issueDefaults),
@@ -29234,210 +30212,10 @@ function prefix(rel, entry) {
29234
30212
  return path.posix.join(rel, entry);
29235
30213
  }
29236
30214
 
29237
- // src/agent/renderers/cursor-renderer.ts
30215
+ // src/agent/renderers/claude-renderer.ts
29238
30216
  var import_projen6 = require("projen");
29239
30217
  var import_textfile2 = require("projen/lib/textfile");
29240
- var GENERATED_MARKER = "# ~~ Generated by @codedrifters/configulator. Edits welcome \u2014 please contribute improvements back. ~~";
29241
- var CursorRenderer = class _CursorRenderer {
29242
- /**
29243
- * Render all Cursor configuration files.
29244
- */
29245
- static render(component, rules, skills, subAgents, mcpServers, settings) {
29246
- _CursorRenderer.renderRules(component, rules);
29247
- _CursorRenderer.renderSkills(component, skills);
29248
- _CursorRenderer.renderSubAgents(component, subAgents);
29249
- _CursorRenderer.renderMcpServers(component, mcpServers);
29250
- _CursorRenderer.renderHooks(component, settings);
29251
- _CursorRenderer.renderIgnoreFiles(component, settings);
29252
- }
29253
- static renderRules(component, rules) {
29254
- for (const rule of rules) {
29255
- if (rule.platforms?.cursor?.exclude) continue;
29256
- const lines = [];
29257
- const description = rule.platforms?.cursor?.description ?? rule.description;
29258
- const isAlways = rule.scope === AGENT_RULE_SCOPE.ALWAYS;
29259
- lines.push("---");
29260
- lines.push(`description: "${description}"`);
29261
- lines.push(`alwaysApply: ${isAlways}`);
29262
- if (!isAlways && rule.filePatterns && rule.filePatterns.length > 0) {
29263
- lines.push(`path: ${JSON.stringify([...rule.filePatterns])}`);
29264
- }
29265
- lines.push("---");
29266
- lines.push("");
29267
- lines.push(...rule.content.split("\n"));
29268
- new import_textfile2.TextFile(component, `.cursor/rules/${rule.name}.mdc`, { lines });
29269
- }
29270
- }
29271
- static renderSkills(component, skills) {
29272
- for (const skill of skills) {
29273
- if (skill.platforms?.cursor?.exclude) continue;
29274
- const lines = [];
29275
- lines.push("---");
29276
- lines.push(`name: "${skill.name}"`);
29277
- lines.push(`description: "${skill.description}"`);
29278
- if (skill.disableModelInvocation) {
29279
- lines.push(`disable-model-invocation: true`);
29280
- }
29281
- if (skill.userInvocable === false) {
29282
- lines.push(`user-invocable: false`);
29283
- }
29284
- if (skill.context) {
29285
- lines.push(`context: "${skill.context}"`);
29286
- }
29287
- if (skill.agent) {
29288
- lines.push(`agent: "${skill.agent}"`);
29289
- }
29290
- if (skill.shell) {
29291
- lines.push(`shell: "${skill.shell}"`);
29292
- }
29293
- if (skill.allowedTools && skill.allowedTools.length > 0) {
29294
- lines.push(`allowed-tools:`);
29295
- for (const tool of skill.allowedTools) {
29296
- lines.push(` - "${tool}"`);
29297
- }
29298
- }
29299
- lines.push("---");
29300
- lines.push("");
29301
- lines.push(...skill.instructions.split("\n"));
29302
- new import_textfile2.TextFile(component, `.cursor/skills/${skill.name}/SKILL.md`, {
29303
- lines
29304
- });
29305
- if (skill.referenceFiles && skill.referenceFiles.length > 0) {
29306
- for (const file of skill.referenceFiles) {
29307
- new import_textfile2.TextFile(component, `.cursor/skills/${skill.name}/${file.path}`, {
29308
- lines: file.content.split("\n")
29309
- });
29310
- }
29311
- }
29312
- }
29313
- }
29314
- static renderSubAgents(component, subAgents) {
29315
- for (const agent of subAgents) {
29316
- if (agent.platforms?.cursor?.exclude) continue;
29317
- const lines = [];
29318
- lines.push("---");
29319
- lines.push(`name: ${agent.name}`);
29320
- lines.push(`description: >-`);
29321
- lines.push(` ${agent.description}`);
29322
- if (agent.platforms?.cursor?.readonly) {
29323
- lines.push(`readonly: true`);
29324
- }
29325
- if (agent.platforms?.cursor?.isBackground) {
29326
- lines.push(`is_background: true`);
29327
- }
29328
- lines.push("---");
29329
- lines.push("");
29330
- lines.push(...agent.prompt.split("\n"));
29331
- new import_textfile2.TextFile(component, `.cursor/agents/${agent.name}.md`, { lines });
29332
- }
29333
- }
29334
- static renderMcpServers(component, mcpServers) {
29335
- const serverNames = Object.keys(mcpServers);
29336
- if (serverNames.length === 0) return;
29337
- const obj = { mcpServers: {} };
29338
- const servers = obj.mcpServers;
29339
- for (const [name, config] of Object.entries(mcpServers)) {
29340
- const server = {};
29341
- if (config.transport) server.transport = config.transport;
29342
- if (config.command) server.command = config.command;
29343
- if (config.args) server.args = [...config.args];
29344
- if (config.url) server.url = config.url;
29345
- if (config.headers && Object.keys(config.headers).length > 0) {
29346
- server.headers = { ...config.headers };
29347
- }
29348
- if (config.env) server.env = { ...config.env };
29349
- servers[name] = server;
29350
- }
29351
- new import_projen6.JsonFile(component, ".cursor/mcp.json", { obj });
29352
- }
29353
- static renderHooks(component, settings) {
29354
- if (!settings?.hooks) return;
29355
- const hooks = {};
29356
- const hookEntries = settings.hooks;
29357
- for (const [event, actions] of Object.entries(hookEntries)) {
29358
- if (actions && actions.length > 0) {
29359
- hooks[event] = actions.map((h) => ({
29360
- command: h.command
29361
- }));
29362
- }
29363
- }
29364
- if (Object.keys(hooks).length === 0) return;
29365
- new import_projen6.JsonFile(component, ".cursor/hooks.json", {
29366
- obj: { version: 1, hooks }
29367
- });
29368
- }
29369
- static renderIgnoreFiles(component, settings) {
29370
- if (settings?.ignorePatterns && settings.ignorePatterns.length > 0) {
29371
- new import_textfile2.TextFile(component, ".cursorignore", {
29372
- lines: [GENERATED_MARKER, "", ...settings.ignorePatterns]
29373
- });
29374
- }
29375
- if (settings?.indexingIgnorePatterns && settings.indexingIgnorePatterns.length > 0) {
29376
- new import_textfile2.TextFile(component, ".cursorindexingignore", {
29377
- lines: [GENERATED_MARKER, "", ...settings.indexingIgnorePatterns]
29378
- });
29379
- }
29380
- }
29381
- };
29382
-
29383
- // src/agent/template-resolver.ts
29384
- var FALLBACKS = {
29385
- "repository.owner": "<owner>",
29386
- "repository.name": "<repo>",
29387
- "repository.defaultBranch": "main",
29388
- "organization.name": "<organization>",
29389
- "organization.githubOrg": "<org>",
29390
- "githubProject.name": "<project-name>",
29391
- "githubProject.number": "<project-number>",
29392
- "githubProject.nodeId": "<project-node-id>",
29393
- docsPath: "<docs-path>",
29394
- // The monorepo-layout seed block is additive: when absent, the
29395
- // seeded `project-context.md` template reads cleanly without it.
29396
- // Fall back to an empty string so no placeholder text leaks into
29397
- // rendered prompts for repos that predate the layout contract.
29398
- monorepoLayoutSeedBlock: ""
29399
- };
29400
- var TEMPLATE_RE = /\{\{(\w+(?:\.\w+)*)\}\}/g;
29401
- function getNestedValue(obj, path8) {
29402
- const parts = path8.split(".");
29403
- let current = obj;
29404
- for (const part of parts) {
29405
- if (current == null || typeof current !== "object") {
29406
- return void 0;
29407
- }
29408
- current = current[part];
29409
- }
29410
- if (current == null) {
29411
- return void 0;
29412
- }
29413
- return String(current);
29414
- }
29415
- function resolveTemplateVariables(template, metadata) {
29416
- if (!TEMPLATE_RE.test(template)) {
29417
- return { resolved: template, unresolvedKeys: [] };
29418
- }
29419
- const unresolvedKeys = [];
29420
- TEMPLATE_RE.lastIndex = 0;
29421
- const resolved = template.replace(TEMPLATE_RE, (_match, key) => {
29422
- if (metadata) {
29423
- const value = getNestedValue(
29424
- metadata,
29425
- key
29426
- );
29427
- if (value !== void 0) {
29428
- return value;
29429
- }
29430
- }
29431
- unresolvedKeys.push(key);
29432
- return FALLBACKS[key] ?? `<${key}>`;
29433
- });
29434
- return { resolved, unresolvedKeys };
29435
- }
29436
-
29437
- // src/agent/renderers/claude-renderer.ts
29438
- var import_projen7 = require("projen");
29439
- var import_textfile3 = require("projen/lib/textfile");
29440
- var GENERATED_MARKER2 = "<!-- ~~ Generated by @codedrifters/configulator. Edits welcome \u2014 please contribute improvements back. ~~ -->";
30218
+ var GENERATED_MARKER = "<!-- ~~ Generated by @codedrifters/configulator. Edits welcome \u2014 please contribute improvements back. ~~ -->";
29441
30219
  var ClaudeRenderer = class _ClaudeRenderer {
29442
30220
  /**
29443
30221
  * Render all Claude Code configuration files.
@@ -29462,12 +30240,12 @@ var ClaudeRenderer = class _ClaudeRenderer {
29462
30240
  return target === CLAUDE_RULE_TARGET.CLAUDE_MD;
29463
30241
  });
29464
30242
  if (claudeMdRules.length === 0) return;
29465
- const lines = [GENERATED_MARKER2, ""];
30243
+ const lines = [GENERATED_MARKER, ""];
29466
30244
  for (let i = 0; i < claudeMdRules.length; i++) {
29467
30245
  if (i > 0) lines.push("", "---", "");
29468
30246
  lines.push(...claudeMdRules[i].content.split("\n"));
29469
30247
  }
29470
- new import_textfile3.TextFile(component, "CLAUDE.md", { lines });
30248
+ new import_textfile2.TextFile(component, "CLAUDE.md", { lines });
29471
30249
  }
29472
30250
  static renderScopedRules(component, rules) {
29473
30251
  const scopedRules = rules.filter((r) => {
@@ -29487,7 +30265,7 @@ var ClaudeRenderer = class _ClaudeRenderer {
29487
30265
  lines.push("");
29488
30266
  }
29489
30267
  lines.push(...rule.content.split("\n"));
29490
- new import_textfile3.TextFile(component, `.claude/rules/${rule.name}.md`, { lines });
30268
+ new import_textfile2.TextFile(component, `.claude/rules/${rule.name}.md`, { lines });
29491
30269
  }
29492
30270
  }
29493
30271
  static renderSettings(component, mcpServers, settings) {
@@ -29612,7 +30390,7 @@ var ClaudeRenderer = class _ClaudeRenderer {
29612
30390
  hasContent = true;
29613
30391
  }
29614
30392
  if (!hasContent) return;
29615
- new import_projen7.JsonFile(component, ".claude/settings.json", { obj });
30393
+ new import_projen6.JsonFile(component, ".claude/settings.json", { obj });
29616
30394
  }
29617
30395
  static buildSandboxObj(sandbox) {
29618
30396
  const obj = {};
@@ -29699,12 +30477,12 @@ var ClaudeRenderer = class _ClaudeRenderer {
29699
30477
  lines.push("---");
29700
30478
  lines.push("");
29701
30479
  lines.push(...skill.instructions.split("\n"));
29702
- new import_textfile3.TextFile(component, `.claude/skills/${skill.name}/SKILL.md`, {
30480
+ new import_textfile2.TextFile(component, `.claude/skills/${skill.name}/SKILL.md`, {
29703
30481
  lines
29704
30482
  });
29705
30483
  if (skill.referenceFiles && skill.referenceFiles.length > 0) {
29706
30484
  for (const file of skill.referenceFiles) {
29707
- new import_textfile3.TextFile(component, `.claude/skills/${skill.name}/${file.path}`, {
30485
+ new import_textfile2.TextFile(component, `.claude/skills/${skill.name}/${file.path}`, {
29708
30486
  lines: file.content.split("\n")
29709
30487
  });
29710
30488
  }
@@ -29762,7 +30540,7 @@ var ClaudeRenderer = class _ClaudeRenderer {
29762
30540
  lines.push("---");
29763
30541
  lines.push("");
29764
30542
  lines.push(...agent.prompt.split("\n"));
29765
- new import_textfile3.TextFile(component, `.claude/agents/${agent.name}.md`, { lines });
30543
+ new import_textfile2.TextFile(component, `.claude/agents/${agent.name}.md`, { lines });
29766
30544
  }
29767
30545
  }
29768
30546
  static buildMcpServerObj(config) {
@@ -29779,7 +30557,7 @@ var ClaudeRenderer = class _ClaudeRenderer {
29779
30557
  }
29780
30558
  static renderProcedures(component, procedures) {
29781
30559
  for (const proc of procedures) {
29782
- new import_textfile3.TextFile(component, `.claude/procedures/${proc.name}`, {
30560
+ new import_textfile2.TextFile(component, `.claude/procedures/${proc.name}`, {
29783
30561
  lines: proc.content.split("\n"),
29784
30562
  executable: true
29785
30563
  });
@@ -29806,7 +30584,7 @@ var ClaudeRenderer = class _ClaudeRenderer {
29806
30584
  lines.push("---");
29807
30585
  lines.push("");
29808
30586
  lines.push(...command.content.split("\n"));
29809
- new import_textfile3.TextFile(component, `.claude/commands/${command.name}.md`, { lines });
30587
+ new import_textfile2.TextFile(component, `.claude/commands/${command.name}.md`, { lines });
29810
30588
  }
29811
30589
  }
29812
30590
  /**
@@ -29818,6 +30596,206 @@ var ClaudeRenderer = class _ClaudeRenderer {
29818
30596
  }
29819
30597
  };
29820
30598
 
30599
+ // src/agent/renderers/cursor-renderer.ts
30600
+ var import_projen7 = require("projen");
30601
+ var import_textfile3 = require("projen/lib/textfile");
30602
+ var GENERATED_MARKER2 = "# ~~ Generated by @codedrifters/configulator. Edits welcome \u2014 please contribute improvements back. ~~";
30603
+ var CursorRenderer = class _CursorRenderer {
30604
+ /**
30605
+ * Render all Cursor configuration files.
30606
+ */
30607
+ static render(component, rules, skills, subAgents, mcpServers, settings) {
30608
+ _CursorRenderer.renderRules(component, rules);
30609
+ _CursorRenderer.renderSkills(component, skills);
30610
+ _CursorRenderer.renderSubAgents(component, subAgents);
30611
+ _CursorRenderer.renderMcpServers(component, mcpServers);
30612
+ _CursorRenderer.renderHooks(component, settings);
30613
+ _CursorRenderer.renderIgnoreFiles(component, settings);
30614
+ }
30615
+ static renderRules(component, rules) {
30616
+ for (const rule of rules) {
30617
+ if (rule.platforms?.cursor?.exclude) continue;
30618
+ const lines = [];
30619
+ const description = rule.platforms?.cursor?.description ?? rule.description;
30620
+ const isAlways = rule.scope === AGENT_RULE_SCOPE.ALWAYS;
30621
+ lines.push("---");
30622
+ lines.push(`description: "${description}"`);
30623
+ lines.push(`alwaysApply: ${isAlways}`);
30624
+ if (!isAlways && rule.filePatterns && rule.filePatterns.length > 0) {
30625
+ lines.push(`path: ${JSON.stringify([...rule.filePatterns])}`);
30626
+ }
30627
+ lines.push("---");
30628
+ lines.push("");
30629
+ lines.push(...rule.content.split("\n"));
30630
+ new import_textfile3.TextFile(component, `.cursor/rules/${rule.name}.mdc`, { lines });
30631
+ }
30632
+ }
30633
+ static renderSkills(component, skills) {
30634
+ for (const skill of skills) {
30635
+ if (skill.platforms?.cursor?.exclude) continue;
30636
+ const lines = [];
30637
+ lines.push("---");
30638
+ lines.push(`name: "${skill.name}"`);
30639
+ lines.push(`description: "${skill.description}"`);
30640
+ if (skill.disableModelInvocation) {
30641
+ lines.push(`disable-model-invocation: true`);
30642
+ }
30643
+ if (skill.userInvocable === false) {
30644
+ lines.push(`user-invocable: false`);
30645
+ }
30646
+ if (skill.context) {
30647
+ lines.push(`context: "${skill.context}"`);
30648
+ }
30649
+ if (skill.agent) {
30650
+ lines.push(`agent: "${skill.agent}"`);
30651
+ }
30652
+ if (skill.shell) {
30653
+ lines.push(`shell: "${skill.shell}"`);
30654
+ }
30655
+ if (skill.allowedTools && skill.allowedTools.length > 0) {
30656
+ lines.push(`allowed-tools:`);
30657
+ for (const tool of skill.allowedTools) {
30658
+ lines.push(` - "${tool}"`);
30659
+ }
30660
+ }
30661
+ lines.push("---");
30662
+ lines.push("");
30663
+ lines.push(...skill.instructions.split("\n"));
30664
+ new import_textfile3.TextFile(component, `.cursor/skills/${skill.name}/SKILL.md`, {
30665
+ lines
30666
+ });
30667
+ if (skill.referenceFiles && skill.referenceFiles.length > 0) {
30668
+ for (const file of skill.referenceFiles) {
30669
+ new import_textfile3.TextFile(component, `.cursor/skills/${skill.name}/${file.path}`, {
30670
+ lines: file.content.split("\n")
30671
+ });
30672
+ }
30673
+ }
30674
+ }
30675
+ }
30676
+ static renderSubAgents(component, subAgents) {
30677
+ for (const agent of subAgents) {
30678
+ if (agent.platforms?.cursor?.exclude) continue;
30679
+ const lines = [];
30680
+ lines.push("---");
30681
+ lines.push(`name: ${agent.name}`);
30682
+ lines.push(`description: >-`);
30683
+ lines.push(` ${agent.description}`);
30684
+ if (agent.platforms?.cursor?.readonly) {
30685
+ lines.push(`readonly: true`);
30686
+ }
30687
+ if (agent.platforms?.cursor?.isBackground) {
30688
+ lines.push(`is_background: true`);
30689
+ }
30690
+ lines.push("---");
30691
+ lines.push("");
30692
+ lines.push(...agent.prompt.split("\n"));
30693
+ new import_textfile3.TextFile(component, `.cursor/agents/${agent.name}.md`, { lines });
30694
+ }
30695
+ }
30696
+ static renderMcpServers(component, mcpServers) {
30697
+ const serverNames = Object.keys(mcpServers);
30698
+ if (serverNames.length === 0) return;
30699
+ const obj = { mcpServers: {} };
30700
+ const servers = obj.mcpServers;
30701
+ for (const [name, config] of Object.entries(mcpServers)) {
30702
+ const server = {};
30703
+ if (config.transport) server.transport = config.transport;
30704
+ if (config.command) server.command = config.command;
30705
+ if (config.args) server.args = [...config.args];
30706
+ if (config.url) server.url = config.url;
30707
+ if (config.headers && Object.keys(config.headers).length > 0) {
30708
+ server.headers = { ...config.headers };
30709
+ }
30710
+ if (config.env) server.env = { ...config.env };
30711
+ servers[name] = server;
30712
+ }
30713
+ new import_projen7.JsonFile(component, ".cursor/mcp.json", { obj });
30714
+ }
30715
+ static renderHooks(component, settings) {
30716
+ if (!settings?.hooks) return;
30717
+ const hooks = {};
30718
+ const hookEntries = settings.hooks;
30719
+ for (const [event, actions] of Object.entries(hookEntries)) {
30720
+ if (actions && actions.length > 0) {
30721
+ hooks[event] = actions.map((h) => ({
30722
+ command: h.command
30723
+ }));
30724
+ }
30725
+ }
30726
+ if (Object.keys(hooks).length === 0) return;
30727
+ new import_projen7.JsonFile(component, ".cursor/hooks.json", {
30728
+ obj: { version: 1, hooks }
30729
+ });
30730
+ }
30731
+ static renderIgnoreFiles(component, settings) {
30732
+ if (settings?.ignorePatterns && settings.ignorePatterns.length > 0) {
30733
+ new import_textfile3.TextFile(component, ".cursorignore", {
30734
+ lines: [GENERATED_MARKER2, "", ...settings.ignorePatterns]
30735
+ });
30736
+ }
30737
+ if (settings?.indexingIgnorePatterns && settings.indexingIgnorePatterns.length > 0) {
30738
+ new import_textfile3.TextFile(component, ".cursorindexingignore", {
30739
+ lines: [GENERATED_MARKER2, "", ...settings.indexingIgnorePatterns]
30740
+ });
30741
+ }
30742
+ }
30743
+ };
30744
+
30745
+ // src/agent/template-resolver.ts
30746
+ var FALLBACKS = {
30747
+ "repository.owner": "<owner>",
30748
+ "repository.name": "<repo>",
30749
+ "repository.defaultBranch": "main",
30750
+ "organization.name": "<organization>",
30751
+ "organization.githubOrg": "<org>",
30752
+ "githubProject.name": "<project-name>",
30753
+ "githubProject.number": "<project-number>",
30754
+ "githubProject.nodeId": "<project-node-id>",
30755
+ docsPath: "<docs-path>",
30756
+ // The monorepo-layout seed block is additive: when absent, the
30757
+ // seeded `project-context.md` template reads cleanly without it.
30758
+ // Fall back to an empty string so no placeholder text leaks into
30759
+ // rendered prompts for repos that predate the layout contract.
30760
+ monorepoLayoutSeedBlock: ""
30761
+ };
30762
+ var TEMPLATE_RE = /\{\{(\w+(?:\.\w+)*)\}\}/g;
30763
+ function getNestedValue(obj, path8) {
30764
+ const parts = path8.split(".");
30765
+ let current = obj;
30766
+ for (const part of parts) {
30767
+ if (current == null || typeof current !== "object") {
30768
+ return void 0;
30769
+ }
30770
+ current = current[part];
30771
+ }
30772
+ if (current == null) {
30773
+ return void 0;
30774
+ }
30775
+ return String(current);
30776
+ }
30777
+ function resolveTemplateVariables(template, metadata) {
30778
+ if (!TEMPLATE_RE.test(template)) {
30779
+ return { resolved: template, unresolvedKeys: [] };
30780
+ }
30781
+ const unresolvedKeys = [];
30782
+ TEMPLATE_RE.lastIndex = 0;
30783
+ const resolved = template.replace(TEMPLATE_RE, (_match, key) => {
30784
+ if (metadata) {
30785
+ const value = getNestedValue(
30786
+ metadata,
30787
+ key
30788
+ );
30789
+ if (value !== void 0) {
30790
+ return value;
30791
+ }
30792
+ }
30793
+ unresolvedKeys.push(key);
30794
+ return FALLBACKS[key] ?? `<${key}>`;
30795
+ });
30796
+ return { resolved, unresolvedKeys };
30797
+ }
30798
+
29821
30799
  // src/agent/renderers/codex-renderer.ts
29822
30800
  var CodexRenderer = class {
29823
30801
  static render(_component, _rules, _skills, _subAgents) {
@@ -30034,6 +31012,7 @@ var SHARED_EDITING_BUNDLE_HOOKS = [
30034
31012
  ["customer-profile-workflow", "customer-profile"],
30035
31013
  ["industry-discovery-workflow", "industry-discovery"],
30036
31014
  ["meeting-agenda-workflow", "agenda"],
31015
+ ["meeting-processing-workflow", "meeting-analyst"],
30037
31016
  ["people-profile-workflow", "people-profile"],
30038
31017
  ["regulatory-research-workflow", "regulatory-research"],
30039
31018
  ["requirements-reviewer-workflow", "requirements-reviewer"],
@@ -30223,7 +31202,8 @@ var AgentConfig = class _AgentConfig extends import_projen8.Component {
30223
31202
  this.resolvedPaths,
30224
31203
  resolveIssueDefaults(this.options.issueDefaults),
30225
31204
  resolveDefaultAgentTier(this.options),
30226
- resolveBundleAgentTiers(this.options)
31205
+ resolveBundleAgentTiers(this.options),
31206
+ resolvePrReviewPolicy(this.options.prReviewPolicy)
30227
31207
  );
30228
31208
  }
30229
31209
  return this.cachedBundles;
@@ -30265,6 +31245,7 @@ var AgentConfig = class _AgentConfig extends import_projen8.Component {
30265
31245
  super.preSynthesize();
30266
31246
  validateAgentTierConfig(this.options.tiers);
30267
31247
  validateScopeGateConfig(this.options.scopeGate);
31248
+ validatePrReviewPolicyConfig(this.options.prReviewPolicy);
30268
31249
  const resolvedRunRatio = resolveRunRatio(this.options.runRatio);
30269
31250
  if (resolvedRunRatio.enabled) {
30270
31251
  this.project.gitignore.addPatterns(`/${resolvedRunRatio.stateFilePath}`);
@@ -30457,6 +31438,20 @@ ${extra}`
30457
31438
  }
30458
31439
  }
30459
31440
  }
31441
+ if (this.options.additionalRulePaths) {
31442
+ for (const [name, extraPaths] of Object.entries(
31443
+ this.options.additionalRulePaths
31444
+ )) {
31445
+ if (extraPaths.length === 0) continue;
31446
+ const existing = ruleMap.get(name);
31447
+ if (!existing) continue;
31448
+ if (existing.scope !== AGENT_RULE_SCOPE.FILE_PATTERN) continue;
31449
+ ruleMap.set(name, {
31450
+ ...existing,
31451
+ filePatterns: [...existing.filePatterns ?? [], ...extraPaths]
31452
+ });
31453
+ }
31454
+ }
30460
31455
  if (this.options.priorityRules && this.options.priorityRules.length > 0) {
30461
31456
  const issueLabelRule = ruleMap.get("issue-label-conventions");
30462
31457
  if (issueLabelRule) {
@@ -30663,9 +31658,6 @@ ${hook}`
30663
31658
  }
30664
31659
  }
30665
31660
  }
30666
- if (!hasAnyDocsEmittingBundle(excludedBundleNames)) {
30667
- ruleMap.delete("stub-index-convention");
30668
- }
30669
31661
  if (injectBundleHooks && resolvedIssueTemplatesForRules.enabled && hasDownstreamBundles) {
30670
31662
  for (const [ruleName, label] of ISSUE_TEMPLATES_BUNDLE_HOOKS) {
30671
31663
  const existing = ruleMap.get(ruleName);
@@ -30782,6 +31774,12 @@ ${meetingsSection}`;
30782
31774
  });
30783
31775
  }
30784
31776
  }
31777
+ const conventionsRegistryRule = buildConventionsRegistryRule(
31778
+ ruleMap.values()
31779
+ );
31780
+ if (conventionsRegistryRule) {
31781
+ ruleMap.set(conventionsRegistryRule.name, conventionsRegistryRule);
31782
+ }
30785
31783
  return [...ruleMap.values()].sort((a, b) => {
30786
31784
  if (a.name === "project-overview") return -1;
30787
31785
  if (b.name === "project-overview") return 1;
@@ -36791,6 +37789,7 @@ export const collections = {
36791
37789
  DEFAULT_ISSUE_TEMPLATES_REQUIRE_REFERENCE,
36792
37790
  DEFAULT_OFF_PEAK_CRON_EXAMPLE,
36793
37791
  DEFAULT_PARTIAL_UNBLOCK_COMMENT_TEMPLATE,
37792
+ DEFAULT_PATHS_EXEMPT_FROM_SIZE,
36794
37793
  DEFAULT_PRIORITY_LABELS,
36795
37794
  DEFAULT_PRODUCT_CONTEXT_PATH,
36796
37795
  DEFAULT_PROGRESS_FILES_ENABLED,
@@ -36893,6 +37892,7 @@ export const collections = {
36893
37892
  buildMeetingAnalysisBundle,
36894
37893
  buildOrchestratorConventionsContent,
36895
37894
  buildPeopleProfileBundle,
37895
+ buildPrReviewBundle,
36896
37896
  buildRegulatoryResearchBundle,
36897
37897
  buildReport,
36898
37898
  buildRequirementsAnalystBundle,
@@ -37002,7 +38002,6 @@ export const collections = {
37002
38002
  renderSkillEvalsRuleContent,
37003
38003
  renderSkillEvalsRunnerScript,
37004
38004
  renderSourceTierExamples,
37005
- renderStubIndexConventionRuleContent,
37006
38005
  renderTemporalFramingCheckerScript,
37007
38006
  renderTemporalFramingRuleContent,
37008
38007
  renderUnblockDependentsScript,
@@ -37023,6 +38022,7 @@ export const collections = {
37023
38022
  resolveOrchestratorAssets,
37024
38023
  resolveOutdirFromPackageName,
37025
38024
  resolveOverrideForLabels,
38025
+ resolvePrReviewPolicy,
37026
38026
  resolveProgressFiles,
37027
38027
  resolveReactViteSiteProjectOutdir,
37028
38028
  resolveRunRatio,
@@ -37046,6 +38046,7 @@ export const collections = {
37046
38046
  validateIssueDefaultsConfig,
37047
38047
  validateIssueTemplatesConfig,
37048
38048
  validateMonorepoLayout,
38049
+ validatePrReviewPolicyConfig,
37049
38050
  validateProgressFilesConfig,
37050
38051
  validateRunRatioConfig,
37051
38052
  validateScheduledTasksConfig,