@codedrifters/configulator 0.0.407 → 0.0.409

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
@@ -223,6 +223,7 @@ __export(index_exports, {
223
223
  DEFAULT_API_EXTRACTOR_REPORT_FILENAME: () => DEFAULT_API_EXTRACTOR_REPORT_FILENAME,
224
224
  DEFAULT_API_EXTRACTOR_REPORT_FOLDER: () => DEFAULT_API_EXTRACTOR_REPORT_FOLDER,
225
225
  DEFAULT_AUDIT_REPORT_DIR: () => DEFAULT_AUDIT_REPORT_DIR,
226
+ DEFAULT_BASE_CONVENTIONS: () => DEFAULT_BASE_CONVENTIONS,
226
227
  DEFAULT_BUILD_POLICY: () => DEFAULT_BUILD_POLICY,
227
228
  DEFAULT_BUNDLE_OVERRIDES: () => DEFAULT_BUNDLE_OVERRIDES,
228
229
  DEFAULT_DECOMPOSITION_TEMPLATE: () => DEFAULT_DECOMPOSITION_TEMPLATE,
@@ -239,6 +240,7 @@ __export(index_exports, {
239
240
  DEFAULT_ISSUE_TEMPLATES_PATH: () => DEFAULT_ISSUE_TEMPLATES_PATH,
240
241
  DEFAULT_ISSUE_TEMPLATES_REQUIRE_REFERENCE: () => DEFAULT_ISSUE_TEMPLATES_REQUIRE_REFERENCE,
241
242
  DEFAULT_OFF_PEAK_CRON_EXAMPLE: () => DEFAULT_OFF_PEAK_CRON_EXAMPLE,
243
+ DEFAULT_ORCHESTRATOR_CONVENTIONS: () => DEFAULT_ORCHESTRATOR_CONVENTIONS,
242
244
  DEFAULT_PARTIAL_UNBLOCK_COMMENT_TEMPLATE: () => DEFAULT_PARTIAL_UNBLOCK_COMMENT_TEMPLATE,
243
245
  DEFAULT_PATHS_EXEMPT_FROM_SIZE: () => DEFAULT_PATHS_EXEMPT_FROM_SIZE,
244
246
  DEFAULT_PRIORITY_LABELS: () => DEFAULT_PRIORITY_LABELS,
@@ -251,6 +253,7 @@ __export(index_exports, {
251
253
  DEFAULT_REQUIREMENT_CATEGORY_DIRS: () => DEFAULT_REQUIREMENT_CATEGORY_DIRS,
252
254
  DEFAULT_REQUIRE_PRODUCT_CONTEXT: () => DEFAULT_REQUIRE_PRODUCT_CONTEXT,
253
255
  DEFAULT_RESOLVED_ISSUE_DEFAULTS: () => DEFAULT_RESOLVED_ISSUE_DEFAULTS,
256
+ DEFAULT_RULE_CONVENTIONS: () => DEFAULT_RULE_CONVENTIONS,
254
257
  DEFAULT_SAMPLE_COMPILER_OPTIONS: () => DEFAULT_SAMPLE_COMPILER_OPTIONS,
255
258
  DEFAULT_SCHEDULED_TASKS_ROOT: () => DEFAULT_SCHEDULED_TASKS_ROOT,
256
259
  DEFAULT_SCHEDULED_TASK_ENTRIES: () => DEFAULT_SCHEDULED_TASK_ENTRIES,
@@ -354,6 +357,7 @@ __export(index_exports, {
354
357
  buildIndustryDiscoveryBundle: () => buildIndustryDiscoveryBundle,
355
358
  buildMaintenanceAuditBundle: () => buildMaintenanceAuditBundle,
356
359
  buildMeetingAnalysisBundle: () => buildMeetingAnalysisBundle,
360
+ buildOrchestratorBundle: () => buildOrchestratorBundle,
357
361
  buildOrchestratorConventionsContent: () => buildOrchestratorConventionsContent,
358
362
  buildPeopleProfileBundle: () => buildPeopleProfileBundle,
359
363
  buildPrReviewBundle: () => buildPrReviewBundle,
@@ -369,6 +373,7 @@ __export(index_exports, {
369
373
  buildUnblockDependentsProcedure: () => buildUnblockDependentsProcedure,
370
374
  bundleNameForWorkflowRule: () => bundleNameForWorkflowRule,
371
375
  businessModelsBundle: () => businessModelsBundle,
376
+ checkClosingKeywordsProcedure: () => checkClosingKeywordsProcedure,
372
377
  checkDocSamplesProcedure: () => checkDocSamplesProcedure,
373
378
  checkLinksProcedure: () => checkLinksProcedure,
374
379
  classifyIssueScope: () => classifyIssueScope,
@@ -443,6 +448,7 @@ __export(index_exports, {
443
448
  renderCdkRollback: () => renderCdkRollback,
444
449
  renderCdkSynth: () => renderCdkSynth,
445
450
  renderCdkWatch: () => renderCdkWatch,
451
+ renderCheckClosingKeywordsScript: () => renderCheckClosingKeywordsScript,
446
452
  renderCheckDocSamplesProcedure: () => renderCheckDocSamplesProcedure,
447
453
  renderCheckLinksProcedure: () => renderCheckLinksProcedure,
448
454
  renderCustomDocSectionBlock: () => renderCustomDocSectionBlock,
@@ -5402,7 +5408,15 @@ var createRuleSkill = {
5402
5408
  "- Use `ruleExtensions` to add project-specific content to existing bundle rules instead of replacing them"
5403
5409
  ].join("\n")
5404
5410
  };
5405
- function buildBaseBundle(paths = DEFAULT_AGENT_PATHS) {
5411
+ var DEFAULT_BASE_CONVENTIONS = {
5412
+ progressFiles: resolveProgressFiles(),
5413
+ sharedEditing: resolveSharedEditing(),
5414
+ temporalFraming: resolveTemporalFraming(),
5415
+ skillEvals: resolveSkillEvals(),
5416
+ issueTemplates: resolveIssueTemplates(),
5417
+ hasDownstreamIssueKindBundles: true
5418
+ };
5419
+ function buildBaseBundle(paths = DEFAULT_AGENT_PATHS, conventions = DEFAULT_BASE_CONVENTIONS) {
5406
5420
  return {
5407
5421
  name: "base",
5408
5422
  description: "Core rules: project overview, interaction style, and general coding conventions",
@@ -6071,7 +6085,7 @@ function buildBaseBundle(paths = DEFAULT_AGENT_PATHS) {
6071
6085
  // them via `agentConfig.additionalRulePaths`; other consumers
6072
6086
  // get a clean default.
6073
6087
  filePatterns: [".claude/agents/*.md", ".claude/procedures/**"],
6074
- content: renderProgressFilesRuleContent(resolveProgressFiles()),
6088
+ content: renderProgressFilesRuleContent(conventions.progressFiles),
6075
6089
  platforms: {
6076
6090
  cursor: { exclude: true }
6077
6091
  },
@@ -6082,7 +6096,7 @@ function buildBaseBundle(paths = DEFAULT_AGENT_PATHS) {
6082
6096
  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.",
6083
6097
  scope: AGENT_RULE_SCOPE.FILE_PATTERN,
6084
6098
  filePatterns: DEFAULT_SHARED_INDEX_PATHS,
6085
- content: renderSharedEditingRuleContent(resolveSharedEditing()),
6099
+ content: renderSharedEditingRuleContent(conventions.sharedEditing),
6086
6100
  platforms: {
6087
6101
  cursor: { exclude: true }
6088
6102
  },
@@ -6100,7 +6114,7 @@ function buildBaseBundle(paths = DEFAULT_AGENT_PATHS) {
6100
6114
  "docs/src/content/docs/standards-research/**/*.md",
6101
6115
  "docs/src/content/docs/customer-research/**/*.md"
6102
6116
  ],
6103
- content: renderTemporalFramingRuleContent(resolveTemporalFraming()),
6117
+ content: renderTemporalFramingRuleContent(conventions.temporalFraming),
6104
6118
  platforms: {
6105
6119
  cursor: { exclude: true }
6106
6120
  },
@@ -6113,7 +6127,7 @@ function buildBaseBundle(paths = DEFAULT_AGENT_PATHS) {
6113
6127
  // Bundle defaults exclude paths into configulator's own
6114
6128
  // source — see comment on progress-file-convention above.
6115
6129
  filePatterns: [".claude/skills/**"],
6116
- content: renderSkillEvalsRuleContent(resolveSkillEvals()),
6130
+ content: renderSkillEvalsRuleContent(conventions.skillEvals),
6117
6131
  platforms: {
6118
6132
  cursor: { exclude: true }
6119
6133
  },
@@ -6130,7 +6144,10 @@ function buildBaseBundle(paths = DEFAULT_AGENT_PATHS) {
6130
6144
  ".claude/agents/*.md",
6131
6145
  `${paths.docsRoot}/agents/issue-templates.md`
6132
6146
  ],
6133
- content: renderIssueTemplatesRuleContent(resolveIssueTemplates()),
6147
+ content: renderIssueTemplatesRuleContent(
6148
+ conventions.issueTemplates,
6149
+ conventions.hasDownstreamIssueKindBundles
6150
+ ),
6134
6151
  platforms: {
6135
6152
  cursor: { exclude: true }
6136
6153
  },
@@ -11742,6 +11759,278 @@ var docsSyncBundle = buildDocsSyncBundle();
11742
11759
 
11743
11760
  // src/agent/bundles/github-workflow.ts
11744
11761
  var import_github = require("projen/lib/github");
11762
+
11763
+ // src/agent/bundles/closing-keywords.ts
11764
+ function renderCheckClosingKeywordsScript() {
11765
+ return [
11766
+ "#!/usr/bin/env bash",
11767
+ "# check-closing-keywords.sh \u2014 Detect PR bodies that strand issues.",
11768
+ "#",
11769
+ "# GitHub honours exactly ONE issue reference per closing keyword. A",
11770
+ "# body that writes `Closes #593, #594, #595` closes #593 and leaves",
11771
+ "# #594 and #595 open forever. The only correct form is one keyword",
11772
+ "# per issue, each on its own line:",
11773
+ "#",
11774
+ "# Closes #593",
11775
+ "# Closes #594",
11776
+ "#",
11777
+ "# Usage:",
11778
+ "# .claude/procedures/check-closing-keywords.sh <pr-number>",
11779
+ "# .claude/procedures/check-closing-keywords.sh --body-file <path>",
11780
+ "#",
11781
+ "# Output (one stable line per reference, then a summary line):",
11782
+ "# CLOSES #<n> line=<L>",
11783
+ "# A reference GitHub WILL honour (the first one after a keyword).",
11784
+ "# STRANDED #<n> line=<L> keyword=<kw> first=#<n>",
11785
+ "# A reference chained onto an earlier one. GitHub will NOT",
11786
+ "# close it \u2014 the issue is stranded.",
11787
+ "# CLOSING_KEYWORDS_OK closes=<K>",
11788
+ "# CLOSING_KEYWORDS_MALFORMED closes=<K> stranded=<S>",
11789
+ "# NO_CLOSING_KEYWORD",
11790
+ "#",
11791
+ "# Exit codes:",
11792
+ "# 0 \u2014 at least one closing keyword, every one well-formed",
11793
+ "# 1 \u2014 at least one stranded reference (malformed body)",
11794
+ "# 2 \u2014 argument error",
11795
+ "# 3 \u2014 required command not on PATH",
11796
+ "# 4 \u2014 could not read the PR body",
11797
+ "# 5 \u2014 no closing keyword found at all",
11798
+ "",
11799
+ "set -uo pipefail",
11800
+ "",
11801
+ "# \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",
11802
+ "",
11803
+ "err() {",
11804
+ ' printf "check-closing-keywords.sh: %s\\n" "$*" >&2',
11805
+ "}",
11806
+ "",
11807
+ "usage() {",
11808
+ " cat >&2 <<'USAGE'",
11809
+ "Usage: check-closing-keywords.sh <pr-number>",
11810
+ " check-closing-keywords.sh --body-file <path>",
11811
+ "",
11812
+ " <pr-number> Positive integer PR number; the body is read",
11813
+ " via `gh pr view <n> --json body`.",
11814
+ " --body-file <path> Read the body from a file instead. Use this to",
11815
+ " check a drafted body BEFORE `gh pr create`.",
11816
+ "",
11817
+ "Emits one CLOSES / STRANDED line per issue reference, then a summary.",
11818
+ "Exits 1 when any reference is stranded by a multi-issue keyword.",
11819
+ "USAGE",
11820
+ "}",
11821
+ "",
11822
+ "# \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",
11823
+ "",
11824
+ 'mode=""',
11825
+ 'arg=""',
11826
+ "",
11827
+ "while [[ $# -gt 0 ]]; do",
11828
+ ' case "$1" in',
11829
+ " --body-file)",
11830
+ ' if [[ $# -lt 2 || -z "${2:-}" ]]; then',
11831
+ ' err "--body-file requires a value"',
11832
+ " usage",
11833
+ " exit 2",
11834
+ " fi",
11835
+ ' if [[ -n "$mode" ]]; then',
11836
+ ' err "specify either a PR number or --body-file, not both"',
11837
+ " exit 2",
11838
+ " fi",
11839
+ ' mode="file"',
11840
+ ' arg="$2"',
11841
+ " shift 2",
11842
+ " ;;",
11843
+ " -h|--help)",
11844
+ " usage",
11845
+ " exit 0",
11846
+ " ;;",
11847
+ " -*)",
11848
+ ' err "unknown option: $1"',
11849
+ " usage",
11850
+ " exit 2",
11851
+ " ;;",
11852
+ " *)",
11853
+ ' if [[ -n "$mode" ]]; then',
11854
+ ' err "unexpected extra argument: $1"',
11855
+ " usage",
11856
+ " exit 2",
11857
+ " fi",
11858
+ ' mode="pr"',
11859
+ ' arg="$1"',
11860
+ " shift",
11861
+ " ;;",
11862
+ " esac",
11863
+ "done",
11864
+ "",
11865
+ 'if [[ -z "$mode" ]]; then',
11866
+ ' err "expected a PR number or --body-file <path>"',
11867
+ " usage",
11868
+ " exit 2",
11869
+ "fi",
11870
+ "",
11871
+ "# \u2500\u2500 read the body \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",
11872
+ "",
11873
+ 'body=""',
11874
+ "",
11875
+ 'if [[ "$mode" == "file" ]]; then',
11876
+ ' if [[ ! -r "$arg" ]]; then',
11877
+ ' err "body file not readable: $arg"',
11878
+ " exit 4",
11879
+ " fi",
11880
+ ' body=$(cat "$arg")',
11881
+ "else",
11882
+ ' if ! [[ "$arg" =~ ^[1-9][0-9]*$ ]]; then',
11883
+ ` err "PR number must be a positive integer (got: '$arg')"`,
11884
+ " exit 2",
11885
+ " fi",
11886
+ " if ! command -v gh >/dev/null 2>&1; then",
11887
+ ' err "required command not found on PATH: gh"',
11888
+ " exit 3",
11889
+ " fi",
11890
+ ` if ! body=$(gh pr view "$arg" --json body -q '.body' 2>/dev/null); then`,
11891
+ ' err "could not read the body of PR #${arg} via gh"',
11892
+ " exit 4",
11893
+ " fi",
11894
+ "fi",
11895
+ "",
11896
+ "# \u2500\u2500 parsing contract \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",
11897
+ "#",
11898
+ "# Regexes live in single-quoted variables so backticks and other shell",
11899
+ "# metacharacters inside them are never expanded by [[ ]].",
11900
+ "#",
11901
+ "# kw_re matches a closing keyword plus its FIRST issue reference. The",
11902
+ "# leading (^|[^[:alnum:]_]) is a word boundary so `prefixes #1` is not",
11903
+ "# read as `fixes #1`.",
11904
+ "#",
11905
+ "# sep_re / ws_re walk the tail AFTER that first reference, matching",
11906
+ "# only references CHAINED directly onto it by a separator a human",
11907
+ "# would mistake for a list: `,` `/` `&` `+` `and` `, and`, or",
11908
+ "# bare whitespace. Anything else \u2014 a period, a dash, a word, an",
11909
+ "# opening paren \u2014 terminates the chain. That is what keeps prose like",
11910
+ "# `Closes #893. See #607 and #860 for context.` from false-positiving:",
11911
+ "# the `.` after #893 ends the chain before `#607` is ever considered.",
11912
+ "",
11913
+ "fence_re='^[[:space:]]{0,3}(```+|~~~+)'",
11914
+ "quote_re='^[[:space:]]*>'",
11915
+ "kw_re='(^|[^[:alnum:]_])(closes|closed|close|fixes|fixed|fix|resolves|resolved|resolve)[[:space:]]*:?[[:space:]]*#([0-9]+)(.*)$'",
11916
+ "sep_re='^[[:space:]]*(,[[:space:]]*and|,|/|&|[+]|and)[[:space:]]*#([0-9]+)(.*)$'",
11917
+ "ws_re='^[[:space:]]+#([0-9]+)(.*)$'",
11918
+ "",
11919
+ "# Keywords are case-insensitive on GitHub, so the scan is too.",
11920
+ "shopt -s nocasematch",
11921
+ "",
11922
+ "# A single backtick, held in a variable so the code-span stripper",
11923
+ "# never has to quote one inline.",
11924
+ "bt='`'",
11925
+ "",
11926
+ "# Remove inline code spans (`...`) from a line. A PR body that",
11927
+ "# DISCUSSES the malformed form \u2014 writing `Closes #593, #594` inside",
11928
+ "# backticks \u2014 must not trip the detector.",
11929
+ "strip_code_spans() {",
11930
+ ' local s="$1"',
11931
+ ' local out=""',
11932
+ ' while [[ "$s" == *"$bt"*"$bt"* ]]; do',
11933
+ ' out="$out${s%%$bt*}"',
11934
+ ' s="${s#*$bt}"',
11935
+ ' s="${s#*$bt}"',
11936
+ " done",
11937
+ ` printf '%s%s' "$out" "$s"`,
11938
+ "}",
11939
+ "",
11940
+ "# \u2500\u2500 scan \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\u2500\u2500\u2500",
11941
+ "",
11942
+ "line_no=0",
11943
+ "in_fence=0",
11944
+ 'fence_marker=""',
11945
+ "closes_count=0",
11946
+ "stranded_count=0",
11947
+ "",
11948
+ 'while IFS= read -r raw || [[ -n "$raw" ]]; do',
11949
+ " line_no=$((line_no + 1))",
11950
+ " # gh returns CRLF bodies; strip the CR so end-of-line matching works.",
11951
+ ` raw="\${raw//$'\\r'/}"`,
11952
+ "",
11953
+ " # Fenced code blocks are quoted content, never the PR's own intent.",
11954
+ ' if [[ "$raw" =~ $fence_re ]]; then',
11955
+ ' marker="${BASH_REMATCH[1]:0:1}"',
11956
+ " if [[ $in_fence -eq 0 ]]; then",
11957
+ " in_fence=1",
11958
+ ' fence_marker="$marker"',
11959
+ ' elif [[ "$marker" == "$fence_marker" ]]; then',
11960
+ " in_fence=0",
11961
+ ' fence_marker=""',
11962
+ " fi",
11963
+ " continue",
11964
+ " fi",
11965
+ " if [[ $in_fence -eq 1 ]]; then",
11966
+ " continue",
11967
+ " fi",
11968
+ "",
11969
+ " # Blockquoted text is someone else's words being cited.",
11970
+ ' if [[ "$raw" =~ $quote_re ]]; then',
11971
+ " continue",
11972
+ " fi",
11973
+ "",
11974
+ ' line=$(strip_code_spans "$raw")',
11975
+ "",
11976
+ ' rest="$line"',
11977
+ ' while [[ "$rest" =~ $kw_re ]]; do',
11978
+ ' kw="${BASH_REMATCH[2]}"',
11979
+ ' first="${BASH_REMATCH[3]}"',
11980
+ ' tail="${BASH_REMATCH[4]}"',
11981
+ "",
11982
+ ` printf 'CLOSES #%s line=%s\\n' "$first" "$line_no"`,
11983
+ " closes_count=$((closes_count + 1))",
11984
+ "",
11985
+ " # Walk every reference chained onto this keyword's first one.",
11986
+ ' t="$tail"',
11987
+ " while :; do",
11988
+ ' if [[ "$t" =~ $sep_re ]]; then',
11989
+ ' extra="${BASH_REMATCH[2]}"',
11990
+ ' t="${BASH_REMATCH[3]}"',
11991
+ ' elif [[ "$t" =~ $ws_re ]]; then',
11992
+ ' extra="${BASH_REMATCH[1]}"',
11993
+ ' t="${BASH_REMATCH[2]}"',
11994
+ " else",
11995
+ " break",
11996
+ " fi",
11997
+ " printf 'STRANDED #%s line=%s keyword=%s first=#%s\\n' \\",
11998
+ ' "$extra" "$line_no" "$kw" "$first"',
11999
+ " stranded_count=$((stranded_count + 1))",
12000
+ " done",
12001
+ "",
12002
+ ' rest="$t"',
12003
+ " done",
12004
+ 'done <<< "$body"',
12005
+ "",
12006
+ "# \u2500\u2500 summary \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",
12007
+ "",
12008
+ "if [[ $closes_count -eq 0 ]]; then",
12009
+ ' echo "NO_CLOSING_KEYWORD"',
12010
+ " exit 5",
12011
+ "fi",
12012
+ "",
12013
+ "if [[ $stranded_count -gt 0 ]]; then",
12014
+ " printf 'CLOSING_KEYWORDS_MALFORMED closes=%s stranded=%s\\n' \\",
12015
+ ' "$closes_count" "$stranded_count"',
12016
+ ' err "GitHub honours only ONE issue reference per closing keyword."',
12017
+ ' err "Give every issue its own keyword, one per line:"',
12018
+ ' err " Closes #123"',
12019
+ ' err " Closes #456"',
12020
+ " exit 1",
12021
+ "fi",
12022
+ "",
12023
+ `printf 'CLOSING_KEYWORDS_OK closes=%s\\n' "$closes_count"`,
12024
+ "exit 0"
12025
+ ].join("\n");
12026
+ }
12027
+ var checkClosingKeywordsProcedure = {
12028
+ name: "check-closing-keywords.sh",
12029
+ description: "Parse a PR body (by PR number or --body-file) and report every closing-keyword issue reference as CLOSES (GitHub will honour it) or STRANDED (chained onto an earlier reference and silently ignored). Exits 1 when any reference is stranded. Skips fenced code blocks, blockquotes, and inline code spans.",
12030
+ content: renderCheckClosingKeywordsScript()
12031
+ };
12032
+
12033
+ // src/agent/bundles/github-workflow.ts
11745
12034
  var setIssueTypeProcedure = {
11746
12035
  name: "set-issue-type.sh",
11747
12036
  description: "Assign a GitHub issue type (Feature / Task / Epic / Bug / etc.) via the updateIssueIssueType GraphQL mutation in a single step",
@@ -12416,8 +12705,24 @@ function buildGithubWorkflowBundle(buildPolicy = DEFAULT_BUILD_POLICY) {
12416
12705
  "5. **Push the branch** to origin: `git push -u origin <branch>`",
12417
12706
  "6. **Create the PR** using `gh pr create`:",
12418
12707
  " - **Title**: use a conventional commit style title (e.g., `feat(scope): short description`)",
12419
- " - **Body**: include `Closes #<issue-number>` (derived from the branch name) and a brief summary of changes",
12420
- "7. **Delegate review and merge to the `pr-reviewer` sub-agent.** After the PR is created, invoke the `/review-pr <pr-number>` skill (or otherwise hand the new PR number to the `pr-reviewer` sub-agent). The reviewer verifies the diff against the linked issue's acceptance criteria and enables squash auto-merge when all checks pass. Do **not** run `gh pr merge --auto` yourself \u2014 review/merge policy lives solely in the `pr-reviewer` agent.",
12708
+ " - **Body**: include one `Closes #<issue-number>` line per issue the PR closes (derived from the branch name and any additional issues in scope) and a brief summary of changes. See **Closing Keywords** below \u2014 never list several issues after a single keyword.",
12709
+ "7. **Verify the closing keywords** with `.claude/procedures/check-closing-keywords.sh --body-file <path>` on the body you are about to post, then `.claude/procedures/check-closing-keywords.sh <pr-number>` on the body that landed. Exit `0` means every referenced issue will auto-close; exit `1` prints a `STRANDED #<n>` line per issue GitHub would silently leave open \u2014 fix the body with `gh pr edit <pr-number> --body-file <path>` and re-run until it exits `0`.",
12710
+ "8. **Delegate review and merge to the `pr-reviewer` sub-agent.** After the PR is created, invoke the `/review-pr <pr-number>` skill (or otherwise hand the new PR number to the `pr-reviewer` sub-agent). The reviewer verifies the diff against the linked issue's acceptance criteria and enables squash auto-merge when all checks pass. Do **not** run `gh pr merge --auto` yourself \u2014 review/merge policy lives solely in the `pr-reviewer` agent.",
12711
+ "",
12712
+ "### Closing Keywords",
12713
+ "",
12714
+ "**GitHub honours exactly ONE issue reference per closing keyword.** `Closes #593, #594, #595` closes **#593 only** \u2014 #594 and #595 stay open forever with no warning anywhere. `Closes #821 / #837` and `Closes #821 and #837` strand the second issue the same way. Give every issue its own keyword, one per line:",
12715
+ "",
12716
+ "```markdown",
12717
+ "Closes #593 <- correct: repeat the keyword, one issue each",
12718
+ "Closes #594",
12719
+ "",
12720
+ "Closes #593, #594 <- WRONG: closes #593, strands #594",
12721
+ "Closes #821 / #837 <- WRONG: closes #821, strands #837",
12722
+ "Closes #821 and #837 <- WRONG: closes #821, strands #837",
12723
+ "```",
12724
+ "",
12725
+ "`Fixes` and `Resolves` behave identically \u2014 the rule is the keyword-to-issue ratio, not the verb. Non-closing references (`Refs #849`, `Related to #607`) are unaffected. `.claude/procedures/check-closing-keywords.sh` is the mechanical check; it skips fenced code blocks, blockquotes, and inline code spans, so a body that *discusses* the malformed form does not trip it.",
12421
12726
  "",
12422
12727
  "### PR Body Template",
12423
12728
  "",
@@ -12434,10 +12739,18 @@ function buildGithubWorkflowBundle(buildPolicy = DEFAULT_BUILD_POLICY) {
12434
12739
  "- [ ] Relevant changes have been reviewed",
12435
12740
  "```",
12436
12741
  "",
12742
+ "When the PR closes more than one issue, the closing block expands to one keyword per issue \u2014 it never becomes a list:",
12743
+ "",
12744
+ "```markdown",
12745
+ "Closes #<first-issue-number>",
12746
+ "Closes #<second-issue-number>",
12747
+ "```",
12748
+ "",
12437
12749
  "### Important",
12438
12750
  "",
12439
12751
  "- Always derive the issue number from the branch name (e.g., `feat/42-add-login` \u2192 `#42`)",
12440
12752
  "- Use conventional commit format for the PR title",
12753
+ "- **One closing keyword per issue, each on its own line.** Never `Closes #A, #B` \u2014 GitHub closes only `#A`. Run `.claude/procedures/check-closing-keywords.sh` before and after `gh pr create`; a non-zero exit means the PR would strand an issue",
12441
12754
  "- Delegate merge to the `pr-reviewer` sub-agent \u2014 do not merge manually and do not enable auto-merge directly"
12442
12755
  ].join("\n"),
12443
12756
  tags: ["workflow"]
@@ -12470,7 +12783,11 @@ function buildGithubWorkflowBundle(buildPolicy = DEFAULT_BUILD_POLICY) {
12470
12783
  }
12471
12784
  ],
12472
12785
  skills: [cleanMergedBranchesSkill],
12473
- procedures: [setIssueTypeProcedure, cleanMergedBranchesProcedure]
12786
+ procedures: [
12787
+ setIssueTypeProcedure,
12788
+ cleanMergedBranchesProcedure,
12789
+ checkClosingKeywordsProcedure
12790
+ ]
12474
12791
  };
12475
12792
  }
12476
12793
  var githubWorkflowBundle = buildGithubWorkflowBundle();
@@ -19558,6 +19875,27 @@ var issueWorkerSubAgent = {
19558
19875
  'Closes #<issue-number>"',
19559
19876
  "```",
19560
19877
  "",
19878
+ "**One closing keyword per issue, each on its own line.** GitHub honours",
19879
+ "only one issue reference per keyword, so `Closes #A, #B` closes `#A` and",
19880
+ "leaves `#B` open forever \u2014 the same is true of `Closes #A / #B` and",
19881
+ "`Closes #A and #B`. A PR that closes several issues repeats the keyword:",
19882
+ "",
19883
+ "```",
19884
+ "Closes #<first-issue-number>",
19885
+ "Closes #<second-issue-number>",
19886
+ "```",
19887
+ "",
19888
+ "Verify it mechanically before moving on \u2014 this exits non-zero and names",
19889
+ "every issue the body would strand:",
19890
+ "",
19891
+ "```bash",
19892
+ ".claude/procedures/check-closing-keywords.sh <pr-number>",
19893
+ "```",
19894
+ "",
19895
+ "If it reports `CLOSING_KEYWORDS_MALFORMED`, fix the body with",
19896
+ "`gh pr edit <pr-number> --body-file <path>` and re-run until it exits",
19897
+ "`0`. See the **Closing Keywords** section of the `pr-workflow` rule.",
19898
+ "",
19561
19899
  "Do **not** enable auto-merge yourself. The `pr-reviewer` sub-agent owns",
19562
19900
  "review and merge for every PR \u2014 it verifies the diff against the issue's",
19563
19901
  "acceptance criteria and enables squash auto-merge only when all checks",
@@ -19831,37 +20169,7 @@ var scanCommand = {
19831
20169
  ""
19832
20170
  ].join("\n")
19833
20171
  };
19834
- var orchestratorBundle = {
19835
- name: "orchestrator",
19836
- description: "Pipeline orchestrator agent for issue triage, PR review, and queue management",
19837
- // Always included by default
19838
- appliesWhen: () => true,
19839
- rules: [
19840
- {
19841
- name: "orchestrator-conventions",
19842
- description: "Guidelines for orchestrator agent behavior and pipeline management, including the funnel-tier dispatch sort, scope gate, and per-agent scheduled-task layout",
19843
- scope: AGENT_RULE_SCOPE.FILE_PATTERN,
19844
- // Bundle defaults exclude paths into configulator's own source
19845
- // (only meaningful when configulator is a workspace package).
19846
- // codedrifters/packages restores them via
19847
- // `agentConfig.additionalRulePaths`.
19848
- filePatterns: [
19849
- ".claude/agents/orchestrator.md",
19850
- ".claude/scheduled-tasks/**"
19851
- ],
19852
- content: buildOrchestratorConventionsContent(
19853
- DEFAULT_AGENT_TIERS,
19854
- resolveScopeGate(),
19855
- resolveRunRatio(),
19856
- resolveScheduledTasks(),
19857
- resolveUnblockDependents()
19858
- ),
19859
- platforms: {
19860
- cursor: { exclude: true }
19861
- },
19862
- tags: ["workflow"]
19863
- }
19864
- ],
20172
+ var ORCHESTRATOR_BUNDLE_STATIC = {
19865
20173
  subAgents: [orchestratorSubAgent, issueWorkerSubAgent],
19866
20174
  procedures: [
19867
20175
  checkBlockedProcedure,
@@ -19877,6 +20185,51 @@ var orchestratorBundle = {
19877
20185
  ]
19878
20186
  }
19879
20187
  };
20188
+ var DEFAULT_ORCHESTRATOR_CONVENTIONS = {
20189
+ tiers: DEFAULT_AGENT_TIERS,
20190
+ scopeGate: resolveScopeGate(),
20191
+ runRatio: resolveRunRatio(),
20192
+ scheduledTasks: resolveScheduledTasks(),
20193
+ unblockDependents: resolveUnblockDependents(),
20194
+ excludeBundles: []
20195
+ };
20196
+ function buildOrchestratorBundle(conventions = DEFAULT_ORCHESTRATOR_CONVENTIONS) {
20197
+ return {
20198
+ name: "orchestrator",
20199
+ description: "Pipeline orchestrator agent for issue triage, PR review, and queue management",
20200
+ // Always included by default
20201
+ appliesWhen: () => true,
20202
+ rules: [
20203
+ {
20204
+ name: "orchestrator-conventions",
20205
+ description: "Guidelines for orchestrator agent behavior and pipeline management, including the funnel-tier dispatch sort, scope gate, and per-agent scheduled-task layout",
20206
+ scope: AGENT_RULE_SCOPE.FILE_PATTERN,
20207
+ // Bundle defaults exclude paths into configulator's own source
20208
+ // (only meaningful when configulator is a workspace package).
20209
+ // codedrifters/packages restores them via
20210
+ // `agentConfig.additionalRulePaths`.
20211
+ filePatterns: [
20212
+ ".claude/agents/orchestrator.md",
20213
+ ".claude/scheduled-tasks/**"
20214
+ ],
20215
+ content: buildOrchestratorConventionsContent(
20216
+ conventions.tiers,
20217
+ conventions.scopeGate,
20218
+ conventions.runRatio,
20219
+ conventions.scheduledTasks,
20220
+ conventions.unblockDependents,
20221
+ conventions.excludeBundles
20222
+ ),
20223
+ platforms: {
20224
+ cursor: { exclude: true }
20225
+ },
20226
+ tags: ["workflow"]
20227
+ }
20228
+ ],
20229
+ ...ORCHESTRATOR_BUNDLE_STATIC
20230
+ };
20231
+ }
20232
+ var orchestratorBundle = buildOrchestratorBundle();
19880
20233
 
19881
20234
  // src/agent/bundles/people-profile.ts
19882
20235
  function buildPeopleProfileAnalystSubAgent(paths, issueDefaults, tier) {
@@ -21222,8 +21575,32 @@ var prReviewerSubAgent = {
21222
21575
  " resolve CI via the **CI Verification** section's Actions-runs",
21223
21576
  " fallback before deciding eligibility \u2014 do **not** treat the 403 as",
21224
21577
  " either a pass or an automatic disqualification.",
21225
- "3. The PR body contains a linked issue via one of the closing keywords:",
21226
- " `Closes #N`, `Fixes #N`, or `Resolves #N` (case-insensitive).",
21578
+ "3. The PR body contains at least one linked issue via a closing",
21579
+ " keyword (`Closes #N`, `Fixes #N`, `Resolves #N`, case-insensitive).",
21580
+ " Resolve this mechanically \u2014 do **not** eyeball the body:",
21581
+ "",
21582
+ "```bash",
21583
+ ".claude/procedures/check-closing-keywords.sh <pr-number>",
21584
+ "```",
21585
+ "",
21586
+ " The procedure is the single source of truth for closing-keyword",
21587
+ " parsing in this pipeline. It emits one `CLOSES #<n> line=<L>` line",
21588
+ " per reference GitHub will honour, one",
21589
+ " `STRANDED #<n> line=<L> keyword=<kw> first=#<n>` line per reference",
21590
+ " GitHub will silently ignore, then a summary line. It skips fenced",
21591
+ " code blocks, blockquotes, and inline code spans, so a body that",
21592
+ " merely discusses closing keywords does not confuse it.",
21593
+ "",
21594
+ " - Exit `5` / `NO_CLOSING_KEYWORD` \u2014 check (3) **fails**; the PR has",
21595
+ " no linked issue.",
21596
+ " - Exit `0` / `CLOSING_KEYWORDS_OK` \u2014 check (3) passes. Record the",
21597
+ " `CLOSES` set as the **linked-issue set** for the rest of the pass.",
21598
+ " - Exit `1` / `CLOSING_KEYWORDS_MALFORMED` \u2014 check (3) **passes**",
21599
+ " (there is a linked issue), but the body would strand every",
21600
+ " `STRANDED` issue. This is **not** an eligibility rejection \u2014 the",
21601
+ " code is not at fault. Record both the `CLOSES` and `STRANDED`",
21602
+ " sets as the linked-issue set and carry the malformed finding into",
21603
+ " Phase 4 and Phase 5.",
21227
21604
  "",
21228
21605
  "If the **CI** check (2) or the **linked-issue** check (3) fails, post",
21229
21606
  "a short comment explaining the reason and stop. Do not proceed to",
@@ -21291,16 +21668,29 @@ var prReviewerSubAgent = {
21291
21668
  "gh pr checks <pr-number>",
21292
21669
  "```",
21293
21670
  "",
21294
- "Extract the linked issue number from the PR body using the closing keywords",
21295
- "(`Closes #N`, `Fixes #N`, or `Resolves #N`) identified in Phase 1.5.",
21671
+ "Use the **linked-issue set** already computed in Phase 1.5 \u2014 the union",
21672
+ "of the `CLOSES` and `STRANDED` issue numbers the",
21673
+ "`check-closing-keywords.sh` run reported. Do not re-derive it by",
21674
+ "reading the body yourself.",
21675
+ "",
21676
+ "Most PRs link exactly one issue, and every phase below reads naturally",
21677
+ "in the singular. When the set has more than one member, treat **every**",
21678
+ "member as a linked issue: fetch each one, union their acceptance",
21679
+ "criteria in Phase 3, and close each one in Phase 5. The **primary**",
21680
+ "linked issue \u2014 the one cited in the sticky summary and the structured",
21681
+ "report \u2014 is the first `CLOSES` entry.",
21296
21682
  "",
21297
- "Then fetch the linked issue, including its `issueType` so Phase 2.75 can",
21298
- "evaluate the issue-type portion of the review policy:",
21683
+ "Then fetch each linked issue, including its `issueType` so Phase 2.75",
21684
+ "can evaluate the issue-type portion of the review policy:",
21299
21685
  "",
21300
21686
  "```bash",
21301
21687
  "gh issue view <issue-number> --json number,title,body,labels,state,issueType",
21302
21688
  "```",
21303
21689
  "",
21690
+ "When several issues are linked, Phase 2.75's issue-type rule matches if",
21691
+ "**any** linked issue's type is in `human-required.issue-types` \u2014 the",
21692
+ "conservative reading, consistent with the policy's mixed-signal rule.",
21693
+ "",
21304
21694
  "## Phase 2.5: Gather Comments",
21305
21695
  "",
21306
21696
  "Before deciding review mode, collect every human comment on the PR so",
@@ -21598,6 +21988,14 @@ var prReviewerSubAgent = {
21598
21988
  "",
21599
21989
  "- **Convention compliance** \u2014 PR title uses a conventional commit prefix,",
21600
21990
  " body includes a closing keyword, branch name follows project conventions",
21991
+ "- **Closing-keyword integrity** \u2014 the Phase 1.5",
21992
+ " `check-closing-keywords.sh` run exited `0`. A",
21993
+ " `CLOSING_KEYWORDS_MALFORMED` result is a **Suggested** finding (not",
21994
+ " blocking): the diff is unaffected, and Phase 5 closes the stranded",
21995
+ " issues deterministically, so gating the merge on it would wedge a",
21996
+ " correct PR over a body typo. Record every `STRANDED #<n>` in the",
21997
+ " findings and in the sticky summary so the malformed body is visible",
21998
+ " rather than silent.",
21601
21999
  "- **Test coverage** \u2014 new or changed behavior has tests",
21602
22000
  "- **CI status** \u2014 resolve CI via the **CI Verification** section",
21603
22001
  " (primary check-runs read, with the Actions-runs fallback on a",
@@ -21960,7 +22358,38 @@ var prReviewerSubAgent = {
21960
22358
  "",
21961
22359
  "### If all acceptance criteria are met and CI is green",
21962
22360
  "",
21963
- "Branch on the **review mode** decided in Phase 2.75:",
22361
+ "#### Pre-merge closing-keyword check",
22362
+ "",
22363
+ "Run this **before** branching on the review mode \u2014 it applies in both",
22364
+ "modes and is the last cheap moment to catch a body that would strand",
22365
+ "issues on merge:",
22366
+ "",
22367
+ "```bash",
22368
+ ".claude/procedures/check-closing-keywords.sh <pr-number>",
22369
+ "```",
22370
+ "",
22371
+ "Re-run it here rather than reusing the Phase 1.5 result: the body may",
22372
+ "have been edited since, and the merge is about to make the answer",
22373
+ "permanent.",
22374
+ "",
22375
+ "- **Exit `0`** \u2014 nothing to do; every linked issue will auto-close.",
22376
+ "- **Exit `1`** \u2014 the body strands at least one issue. Do **not** block",
22377
+ " the merge; post one comment naming the stranded issues and the",
22378
+ " corrected block, then continue. Phase 5 closes the survivors",
22379
+ " explicitly, so the malformed body costs a comment, not a wedged PR.",
22380
+ "",
22381
+ "```bash",
22382
+ "gh pr comment <pr-number> --body 'GitHub honours only one issue reference per closing keyword, so this body would close #<first> and silently leave #<stranded\u2026> open. Give each issue its own keyword, one per line:",
22383
+ "",
22384
+ "Closes #<first>",
22385
+ "Closes #<stranded>",
22386
+ "",
22387
+ "Merging anyway \u2014 Phase 5 will close the stranded issues explicitly.'",
22388
+ "```",
22389
+ "",
22390
+ "Carry the full stranded set into Phase 5 verbatim.",
22391
+ "",
22392
+ "Then branch on the **review mode** decided in Phase 2.75:",
21964
22393
  "",
21965
22394
  "#### Mode `auto-merge`",
21966
22395
  "",
@@ -22148,6 +22577,7 @@ var prReviewerSubAgent = {
22148
22577
  "**Outstanding:** <list of comments still open with their classification and author>",
22149
22578
  "**Pushbacks:** <list of disputes with reasons>",
22150
22579
  "**Label reconciliation:** <consistent | corrections applied this pass>",
22580
+ "**Closing keywords:** ok | malformed \u2014 strands #<n>[, #<n>\u2026]",
22151
22581
  "**Last pass:** <ISO timestamp>",
22152
22582
  "```",
22153
22583
  "",
@@ -22172,6 +22602,11 @@ var prReviewerSubAgent = {
22172
22602
  " the corrections applied this pass (the detected violation combo,",
22173
22603
  " the label(s) removed, and the resulting self-consistent set).",
22174
22604
  " Every reconciliation correction is recorded here for auditability.",
22605
+ "- **Closing keywords** \u2014 the",
22606
+ " `.claude/procedures/check-closing-keywords.sh` verdict: `ok`, or",
22607
+ " `malformed` plus every `STRANDED #<n>` the body would leave open.",
22608
+ " Recorded on every pass so a body that strands issues is visible to a",
22609
+ " human before the merge, not only after Phase 5 repairs it.",
22175
22610
  "- **Last pass** \u2014 the ISO 8601 timestamp of this run.",
22176
22611
  "",
22177
22612
  "### Step 3: Create or edit in place",
@@ -22556,42 +22991,64 @@ var prReviewerSubAgent = {
22556
22991
  "gh pr view <pr-number> --json state --jq '.state'",
22557
22992
  "```",
22558
22993
  "",
22559
- "- **`MERGED`** \u2014 clean up locally **and** verify the linked issue closed:",
22994
+ "- **`MERGED`** \u2014 clean up locally **and** verify **every** linked issue",
22995
+ " closed:",
22560
22996
  " ```bash",
22561
22997
  " git checkout {{repository.defaultBranch}}",
22562
22998
  " git pull origin {{repository.defaultBranch}}",
22563
22999
  " git fetch --prune origin",
22564
23000
  " git branch -d <branch-name> 2>/dev/null || git branch -D <branch-name> 2>/dev/null || true",
22565
23001
  " ```",
22566
- " Transition the linked issue to `status:done` (replaces whichever of",
22567
- " `status:ready-for-review` or `status:in-progress` it was carrying):",
23002
+ "",
23003
+ " **Post-merge closure verification.** Iterate the **full linked-issue",
23004
+ " set** from Phase 1.5 \u2014 the union of the `CLOSES` and `STRANDED`",
23005
+ " numbers `check-closing-keywords.sh` reported \u2014 not just the primary",
23006
+ " issue. This loop is the structural guard against the stranded-issue",
23007
+ " defect class: GitHub honours only one issue reference per closing",
23008
+ " keyword, so a body written as `Closes #A, #B` closes `#A` and leaves",
23009
+ " `#B` open forever with no signal anywhere. Because the reviewer",
23010
+ " already knows the full set, every survivor is closed here",
23011
+ " deterministically instead of being discovered months later by an",
23012
+ " audit.",
23013
+ "",
23014
+ " For **each** issue in the set, in order:",
23015
+ "",
23016
+ " 1. Transition it to `status:done` (replaces whichever of",
23017
+ " `status:ready-for-review` or `status:in-progress` it was carrying):",
22568
23018
  " ```bash",
22569
23019
  " gh issue edit <issue-number> \\",
22570
23020
  ' --remove-label "status:ready-for-review" \\',
22571
23021
  ' --remove-label "status:in-progress" \\',
22572
23022
  ' --add-label "status:done"',
22573
23023
  " ```",
22574
- " Then check the linked issue state:",
23024
+ " 2. Check its state:",
22575
23025
  " ```bash",
22576
23026
  " gh issue view <issue-number> --json state --jq '.state'",
22577
23027
  " ```",
22578
- " If the issue is **not** `CLOSED`, close it explicitly (this covers",
22579
- " malformed or missing closing keywords on the merge commit):",
23028
+ " 3. If the issue is **not** `CLOSED`, close it explicitly (this covers",
23029
+ " both a missing closing keyword and an issue stranded by a",
23030
+ " multi-issue keyword):",
22580
23031
  " ```bash",
22581
23032
  " gh issue close <issue-number> --reason completed",
22582
23033
  " gh issue comment <issue-number> --body 'PR #<pr-number> merged. Closing issue.'",
22583
23034
  " ```",
22584
- " Once `status:done` is applied (either via the edit above or the",
22585
- " explicit close path), run the targeted unblock sweep so any",
22586
- " dependents waiting on this issue flip from `status:blocked` to",
22587
- " `status:ready` immediately:",
23035
+ " 4. Once `status:done` is applied (either via the edit above or the",
23036
+ " explicit close path), run the targeted unblock sweep so any",
23037
+ " dependents waiting on this issue flip from `status:blocked` to",
23038
+ " `status:ready` immediately:",
22588
23039
  " ```bash",
22589
23040
  " .claude/procedures/unblock-dependents.sh <issue-number>",
22590
23041
  " ```",
23042
+ "",
22591
23043
  " Log the sweep's output (one line per processed dependent:",
22592
23044
  " `UNBLOCKED #<n>` / `STILL_BLOCKED #<n>` / `NO_DEPENDENTS #<m>`)",
22593
23045
  " in the PR review summary. See the **Agent-driven unblocking**",
22594
23046
  " section in `CLAUDE.md` for the full contract.",
23047
+ "",
23048
+ " Report one `Issue #<n>: <closed-by-github | closed-explicitly>` line",
23049
+ " per member of the set. Any issue that needed the explicit close was",
23050
+ " stranded by the PR body \u2014 say so in the summary so the authoring",
23051
+ " defect is visible, not just repaired.",
22595
23052
  "- **`CLOSED` (not merged)** \u2014 leave the branch in place; report the closure.",
22596
23053
  "- **Still `OPEN`** \u2014 auto-merge is pending; report and stop without deleting.",
22597
23054
  "",
@@ -22604,6 +23061,8 @@ var prReviewerSubAgent = {
22604
23061
  "```",
22605
23062
  "PR #<number> \u2014 <title>",
22606
23063
  "Linked issue: #<issue-number>",
23064
+ "Linked issue set: #<n> [, #<n>\u2026] (from check-closing-keywords.sh)",
23065
+ "Closing keywords: <ok | malformed: strands #<n>[, #<n>\u2026]>",
22607
23066
  "Review mode: <auto-merge | human-required>",
22608
23067
  "Reason: <short explanation from Phase 2.75>",
22609
23068
  "Verdict: AUTO_MERGE_ENABLED | AWAITING_HUMAN | DELEGATED_TO_WORKER | NEEDS_CHANGES | INELIGIBLE | BLOCKED",
@@ -22644,6 +23103,14 @@ var prReviewerSubAgent = {
22644
23103
  " PR in turn.",
22645
23104
  "2. **Never merge without a linked issue.** If the PR body has no",
22646
23105
  " `Closes #N` / `Fixes #N` / `Resolves #N`, comment and stop.",
23106
+ "2a. **Never merge without verifying every linked issue closes.** Resolve",
23107
+ " the linked-issue set with",
23108
+ " `.claude/procedures/check-closing-keywords.sh <pr-number>`, never by",
23109
+ " eyeballing the body. GitHub honours only one issue reference per",
23110
+ " closing keyword, so a `STRANDED` line means that issue will stay",
23111
+ " open after the merge unless Phase 5 closes it. Phase 5 must iterate",
23112
+ " the **whole** set \u2014 `CLOSES` plus `STRANDED` \u2014 and explicitly close",
23113
+ " every issue GitHub left open.",
22647
23114
  "3. **Never merge with failing CI.** Even if every criterion is met,",
22648
23115
  " block on red checks. Resolve CI status via the **CI",
22649
23116
  " Verification** section: read check-runs first, and only on a",
@@ -34350,10 +34817,14 @@ function renderPriorityRulesSection(rules) {
34350
34817
  }
34351
34818
 
34352
34819
  // src/agent/bundles/index.ts
34353
- function buildBuiltInBundles(paths = DEFAULT_AGENT_PATHS, issueDefaults = DEFAULT_RESOLVED_ISSUE_DEFAULTS, defaultAgentTier = AGENT_MODEL.BALANCED, bundleAgentTiers = /* @__PURE__ */ new Map(), prReviewPolicy = resolvePrReviewPolicy(), buildPolicy = DEFAULT_BUILD_POLICY) {
34820
+ var DEFAULT_RULE_CONVENTIONS = {
34821
+ base: DEFAULT_BASE_CONVENTIONS,
34822
+ orchestrator: DEFAULT_ORCHESTRATOR_CONVENTIONS
34823
+ };
34824
+ function buildBuiltInBundles(paths = DEFAULT_AGENT_PATHS, issueDefaults = DEFAULT_RESOLVED_ISSUE_DEFAULTS, defaultAgentTier = AGENT_MODEL.BALANCED, bundleAgentTiers = /* @__PURE__ */ new Map(), prReviewPolicy = resolvePrReviewPolicy(), buildPolicy = DEFAULT_BUILD_POLICY, conventions = DEFAULT_RULE_CONVENTIONS) {
34354
34825
  const tierFor = (bundle) => bundleAgentTiers.get(bundle) ?? defaultAgentTier;
34355
34826
  return [
34356
- buildBaseBundle(paths),
34827
+ buildBaseBundle(paths, conventions.base),
34357
34828
  upstreamConfigulatorDocsBundle,
34358
34829
  typescriptBundle,
34359
34830
  vitestBundle,
@@ -34366,7 +34837,7 @@ function buildBuiltInBundles(paths = DEFAULT_AGENT_PATHS, issueDefaults = DEFAUL
34366
34837
  slackBundle,
34367
34838
  buildMeetingAnalysisBundle(tierFor("meeting-analysis")),
34368
34839
  agendaBundle,
34369
- orchestratorBundle,
34840
+ buildOrchestratorBundle(conventions.orchestrator),
34370
34841
  buildPrReviewBundle(prReviewPolicy),
34371
34842
  buildRequirementsAnalystBundle(paths, issueDefaults),
34372
34843
  buildRequirementsWriterBundle(paths, issueDefaults),
@@ -35454,6 +35925,13 @@ var AgentConfig = class _AgentConfig extends import_projen8.Component {
35454
35925
  * credential requirement when a remote cache actually exists. The
35455
35926
  * getter is lazy by design — `TurboRepo` must already be attached
35456
35927
  * to the project when the bundles are first read.
35928
+ *
35929
+ * Every **config-driven convention rule** is likewise resolved here
35930
+ * and seeded into its owning bundle, so the rule enters the rule map
35931
+ * already carrying the consumer's settings. Rewriting those rules
35932
+ * after the map was assembled — the previous approach — silently
35933
+ * discarded any `ruleExtensions` append or same-name `rules`
35934
+ * override that had already been merged in.
35457
35935
  */
35458
35936
  get pathAwareBundles() {
35459
35937
  if (!this.cachedBundles) {
@@ -35463,11 +35941,52 @@ var AgentConfig = class _AgentConfig extends import_projen8.Component {
35463
35941
  resolveDefaultAgentTier(this.options),
35464
35942
  resolveBundleAgentTiers(this.options),
35465
35943
  resolvePrReviewPolicy(this.options.prReviewPolicy),
35466
- resolveBuildPolicy(this.project)
35944
+ resolveBuildPolicy(this.project),
35945
+ this.resolvedRuleConventions
35467
35946
  );
35468
35947
  }
35469
35948
  return this.cachedBundles;
35470
35949
  }
35950
+ /**
35951
+ * Resolved settings for every config-driven convention rule, derived
35952
+ * from this project's options. Consumed by `buildBuiltInBundles` so
35953
+ * the `base` and `orchestrator` bundles seed final rule content.
35954
+ *
35955
+ * `excludeBundles` feeds two of these: the orchestrator's rendered
35956
+ * tier table / scope-gate overrides / scheduled-tasks registry drop
35957
+ * rows owned by excluded bundles, and the issue-templates rule falls
35958
+ * back to its disabled stub once every downstream-issue-kind bundle
35959
+ * has been excluded.
35960
+ */
35961
+ get resolvedRuleConventions() {
35962
+ const excludeBundles = this.options.excludeBundles ?? [];
35963
+ const orchestratorAssets = resolveOrchestratorAssets(
35964
+ this.options.tiers,
35965
+ this.options.scopeGate,
35966
+ this.options.runRatio,
35967
+ this.options.scheduledTasks,
35968
+ this.options.unblockDependents,
35969
+ excludeBundles
35970
+ );
35971
+ return {
35972
+ base: {
35973
+ progressFiles: resolveProgressFiles(this.options.progressFiles),
35974
+ sharedEditing: resolveSharedEditing(this.options.sharedEditing),
35975
+ temporalFraming: resolveTemporalFraming(this.options.temporalFraming),
35976
+ skillEvals: resolveSkillEvals(this.options.skillEvals),
35977
+ issueTemplates: resolveIssueTemplates(this.options.issueTemplates),
35978
+ hasDownstreamIssueKindBundles: hasAnyDownstreamIssueKindBundle(excludeBundles)
35979
+ },
35980
+ orchestrator: {
35981
+ tiers: orchestratorAssets.tiers,
35982
+ scopeGate: orchestratorAssets.scopeGate,
35983
+ runRatio: orchestratorAssets.runRatio,
35984
+ scheduledTasks: orchestratorAssets.scheduledTasks,
35985
+ unblockDependents: orchestratorAssets.unblockDependents,
35986
+ excludeBundles
35987
+ }
35988
+ };
35989
+ }
35471
35990
  /**
35472
35991
  * Returns the bundles that are active for this project: auto-detected
35473
35992
  * bundles (when `autoDetectBundles !== false`) plus force-included
@@ -35782,43 +36301,10 @@ ${section}`
35782
36301
  }
35783
36302
  }
35784
36303
  const excludedBundleNames = this.options.excludeBundles ?? [];
35785
- if (this.options.tiers || this.options.scopeGate || this.options.runRatio || this.options.scheduledTasks || this.options.unblockDependents || excludedBundleNames.length > 0) {
35786
- const orchestratorRule = ruleMap.get("orchestrator-conventions");
35787
- if (orchestratorRule) {
35788
- const { conventionsContent } = resolveOrchestratorAssets(
35789
- this.options.tiers,
35790
- this.options.scopeGate,
35791
- this.options.runRatio,
35792
- this.options.scheduledTasks,
35793
- this.options.unblockDependents,
35794
- excludedBundleNames
35795
- );
35796
- if (conventionsContent !== orchestratorRule.content) {
35797
- ruleMap.set("orchestrator-conventions", {
35798
- ...orchestratorRule,
35799
- content: conventionsContent
35800
- });
35801
- }
35802
- }
35803
- }
35804
36304
  const injectBundleHooks = this.options.claudeMd?.injectBundleHooks ?? true;
35805
36305
  const resolvedProgressFiles = resolveProgressFiles(
35806
36306
  this.options.progressFiles
35807
36307
  );
35808
- if (this.options.progressFiles) {
35809
- const progressRule = ruleMap.get("progress-file-convention");
35810
- if (progressRule) {
35811
- const progressContent = renderProgressFilesRuleContent(
35812
- resolvedProgressFiles
35813
- );
35814
- if (progressContent !== progressRule.content) {
35815
- ruleMap.set("progress-file-convention", {
35816
- ...progressRule,
35817
- content: progressContent
35818
- });
35819
- }
35820
- }
35821
- }
35822
36308
  if (injectBundleHooks && resolvedProgressFiles.enabled) {
35823
36309
  for (const [ruleName, label] of PROGRESS_FILE_BUNDLE_HOOKS) {
35824
36310
  const existing = ruleMap.get(ruleName);
@@ -35845,20 +36331,6 @@ ${hook}`
35845
36331
  const resolvedSharedEditingForRules = resolveSharedEditing(
35846
36332
  this.options.sharedEditing
35847
36333
  );
35848
- if (this.options.sharedEditing) {
35849
- const sharedEditingRule = ruleMap.get("shared-editing-safety");
35850
- if (sharedEditingRule) {
35851
- const sharedEditingContent = renderSharedEditingRuleContent(
35852
- resolvedSharedEditingForRules
35853
- );
35854
- if (sharedEditingContent !== sharedEditingRule.content) {
35855
- ruleMap.set("shared-editing-safety", {
35856
- ...sharedEditingRule,
35857
- content: sharedEditingContent
35858
- });
35859
- }
35860
- }
35861
- }
35862
36334
  if (injectBundleHooks && resolvedSharedEditingForRules.enabled) {
35863
36335
  for (const [ruleName, label] of SHARED_EDITING_BUNDLE_HOOKS) {
35864
36336
  const existing = ruleMap.get(ruleName);
@@ -35885,20 +36357,6 @@ ${hook}`
35885
36357
  const resolvedSkillEvalsForRules = resolveSkillEvals(
35886
36358
  this.options.skillEvals
35887
36359
  );
35888
- if (this.options.skillEvals) {
35889
- const skillEvalsRule = ruleMap.get("skill-evals");
35890
- if (skillEvalsRule) {
35891
- const skillEvalsContent = renderSkillEvalsRuleContent(
35892
- resolvedSkillEvalsForRules
35893
- );
35894
- if (skillEvalsContent !== skillEvalsRule.content) {
35895
- ruleMap.set("skill-evals", {
35896
- ...skillEvalsRule,
35897
- content: skillEvalsContent
35898
- });
35899
- }
35900
- }
35901
- }
35902
36360
  if (injectBundleHooks && resolvedSkillEvalsForRules.enabled) {
35903
36361
  for (const [ruleName, label] of SKILL_EVALS_BUNDLE_HOOKS) {
35904
36362
  const existing = ruleMap.get(ruleName);
@@ -35926,21 +36384,6 @@ ${hook}`
35926
36384
  this.options.issueTemplates
35927
36385
  );
35928
36386
  const hasDownstreamBundles = hasAnyDownstreamIssueKindBundle(excludedBundleNames);
35929
- if (this.options.issueTemplates || !hasDownstreamBundles) {
35930
- const issueTemplatesRule = ruleMap.get("issue-templates-convention");
35931
- if (issueTemplatesRule) {
35932
- const issueTemplatesContent = renderIssueTemplatesRuleContent(
35933
- resolvedIssueTemplatesForRules,
35934
- hasDownstreamBundles
35935
- );
35936
- if (issueTemplatesContent !== issueTemplatesRule.content) {
35937
- ruleMap.set("issue-templates-convention", {
35938
- ...issueTemplatesRule,
35939
- content: issueTemplatesContent
35940
- });
35941
- }
35942
- }
35943
- }
35944
36387
  if (injectBundleHooks && resolvedIssueTemplatesForRules.enabled && hasDownstreamBundles) {
35945
36388
  for (const [ruleName, label] of ISSUE_TEMPLATES_BUNDLE_HOOKS) {
35946
36389
  const existing = ruleMap.get(ruleName);
@@ -35964,23 +36407,6 @@ ${hook}`
35964
36407
  });
35965
36408
  }
35966
36409
  }
35967
- const resolvedTemporalFramingForRules = resolveTemporalFraming(
35968
- this.options.temporalFraming
35969
- );
35970
- if (this.options.temporalFraming) {
35971
- const temporalFramingRule = ruleMap.get("temporal-framing-convention");
35972
- if (temporalFramingRule) {
35973
- const temporalFramingContent = renderTemporalFramingRuleContent(
35974
- resolvedTemporalFramingForRules
35975
- );
35976
- if (temporalFramingContent !== temporalFramingRule.content) {
35977
- ruleMap.set("temporal-framing-convention", {
35978
- ...temporalFramingRule,
35979
- content: temporalFramingContent
35980
- });
35981
- }
35982
- }
35983
- }
35984
36410
  const tierExamples = this.options.features?.sourceTierExamples;
35985
36411
  if (_AgentConfig.hasActiveTierExamples(tierExamples)) {
35986
36412
  const sourceRule = ruleMap.get("source-quality-verification");
@@ -42193,6 +42619,7 @@ export const collections = {
42193
42619
  DEFAULT_API_EXTRACTOR_REPORT_FILENAME,
42194
42620
  DEFAULT_API_EXTRACTOR_REPORT_FOLDER,
42195
42621
  DEFAULT_AUDIT_REPORT_DIR,
42622
+ DEFAULT_BASE_CONVENTIONS,
42196
42623
  DEFAULT_BUILD_POLICY,
42197
42624
  DEFAULT_BUNDLE_OVERRIDES,
42198
42625
  DEFAULT_DECOMPOSITION_TEMPLATE,
@@ -42209,6 +42636,7 @@ export const collections = {
42209
42636
  DEFAULT_ISSUE_TEMPLATES_PATH,
42210
42637
  DEFAULT_ISSUE_TEMPLATES_REQUIRE_REFERENCE,
42211
42638
  DEFAULT_OFF_PEAK_CRON_EXAMPLE,
42639
+ DEFAULT_ORCHESTRATOR_CONVENTIONS,
42212
42640
  DEFAULT_PARTIAL_UNBLOCK_COMMENT_TEMPLATE,
42213
42641
  DEFAULT_PATHS_EXEMPT_FROM_SIZE,
42214
42642
  DEFAULT_PRIORITY_LABELS,
@@ -42221,6 +42649,7 @@ export const collections = {
42221
42649
  DEFAULT_REQUIREMENT_CATEGORY_DIRS,
42222
42650
  DEFAULT_REQUIRE_PRODUCT_CONTEXT,
42223
42651
  DEFAULT_RESOLVED_ISSUE_DEFAULTS,
42652
+ DEFAULT_RULE_CONVENTIONS,
42224
42653
  DEFAULT_SAMPLE_COMPILER_OPTIONS,
42225
42654
  DEFAULT_SCHEDULED_TASKS_ROOT,
42226
42655
  DEFAULT_SCHEDULED_TASK_ENTRIES,
@@ -42324,6 +42753,7 @@ export const collections = {
42324
42753
  buildIndustryDiscoveryBundle,
42325
42754
  buildMaintenanceAuditBundle,
42326
42755
  buildMeetingAnalysisBundle,
42756
+ buildOrchestratorBundle,
42327
42757
  buildOrchestratorConventionsContent,
42328
42758
  buildPeopleProfileBundle,
42329
42759
  buildPrReviewBundle,
@@ -42339,6 +42769,7 @@ export const collections = {
42339
42769
  buildUnblockDependentsProcedure,
42340
42770
  bundleNameForWorkflowRule,
42341
42771
  businessModelsBundle,
42772
+ checkClosingKeywordsProcedure,
42342
42773
  checkDocSamplesProcedure,
42343
42774
  checkLinksProcedure,
42344
42775
  classifyIssueScope,
@@ -42413,6 +42844,7 @@ export const collections = {
42413
42844
  renderCdkRollback,
42414
42845
  renderCdkSynth,
42415
42846
  renderCdkWatch,
42847
+ renderCheckClosingKeywordsScript,
42416
42848
  renderCheckDocSamplesProcedure,
42417
42849
  renderCheckLinksProcedure,
42418
42850
  renderCustomDocSectionBlock,