@codedrifters/configulator 0.0.406 → 0.0.407

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
@@ -277,6 +277,7 @@ __export(index_exports, {
277
277
  DOCS_SYNC_AUDIT_SCHEMA_VERSION: () => DOCS_SYNC_AUDIT_SCHEMA_VERSION,
278
278
  GITHUB_ISSUE_TYPES: () => GITHUB_ISSUE_TYPES,
279
279
  GITHUB_ISSUE_TYPE_BY_TITLE_PREFIX: () => GITHUB_ISSUE_TYPE_BY_TITLE_PREFIX,
280
+ ISSUE_TEMPLATES_GENERATED_SUFFIX: () => ISSUE_TEMPLATES_GENERATED_SUFFIX,
280
281
  JsiiFaker: () => JsiiFaker,
281
282
  LAYOUT_ENFORCEMENT: () => LAYOUT_ENFORCEMENT,
282
283
  LAYOUT_ROOT_BY_PROJECT_TYPE: () => LAYOUT_ROOT_BY_PROJECT_TYPE,
@@ -372,6 +373,7 @@ __export(index_exports, {
372
373
  checkLinksProcedure: () => checkLinksProcedure,
373
374
  classifyIssueScope: () => classifyIssueScope,
374
375
  classifyRun: () => classifyRun,
376
+ collectIssueTemplateRecipeStubs: () => collectIssueTemplateRecipeStubs,
375
377
  companyProfileBundle: () => companyProfileBundle,
376
378
  compileFencedSamples: () => compileFencedSamples,
377
379
  createApiDiffCheck: () => createApiDiffCheck,
@@ -397,6 +399,8 @@ __export(index_exports, {
397
399
  isScheduledTaskOwnedByExcluded: () => isScheduledTaskOwnedByExcluded,
398
400
  isSuppressedWorkflowRule: () => isSuppressedWorkflowRule,
399
401
  isTypeLabelOwnedByExcluded: () => isTypeLabelOwnedByExcluded,
402
+ issueTemplatesChildGlob: () => issueTemplatesChildGlob,
403
+ issueTemplatesGeneratedPath: () => issueTemplatesGeneratedPath,
400
404
  jestBundle: () => jestBundle,
401
405
  labelsForPhase: () => labelsForPhase,
402
406
  maintenanceAuditBundle: () => maintenanceAuditBundle,
@@ -447,8 +451,10 @@ __export(index_exports, {
447
451
  renderFocusSection: () => renderFocusSection,
448
452
  renderGithubIssueTypeSection: () => renderGithubIssueTypeSection,
449
453
  renderGithubIssueTypeSectionLines: () => renderGithubIssueTypeSectionLines,
454
+ renderIssueTemplateLabelsCheckerScript: () => renderIssueTemplateLabelsCheckerScript,
450
455
  renderIssueTemplatesBundleHook: () => renderIssueTemplatesBundleHook,
451
456
  renderIssueTemplatesCheckerScript: () => renderIssueTemplatesCheckerScript,
457
+ renderIssueTemplatesGeneratedPage: () => renderIssueTemplatesGeneratedPage,
452
458
  renderIssueTemplatesRuleContent: () => renderIssueTemplatesRuleContent,
453
459
  renderIssueTemplatesStarterPage: () => renderIssueTemplatesStarterPage,
454
460
  renderIssueTypeAssignmentBlanket: () => renderIssueTypeAssignmentBlanket,
@@ -2199,12 +2205,111 @@ var awsCdkBundle = {
2199
2205
  ]
2200
2206
  };
2201
2207
 
2208
+ // src/agent/bundles/issue-defaults.ts
2209
+ var VALID_STATUS_VALUES = [
2210
+ "ready",
2211
+ "blocked",
2212
+ "in-progress",
2213
+ "ready-for-review",
2214
+ "needs-attention",
2215
+ "done",
2216
+ "deferred"
2217
+ ];
2218
+ var VALID_PRIORITY_VALUES = [
2219
+ "critical",
2220
+ "high",
2221
+ "medium",
2222
+ "low",
2223
+ "trivial"
2224
+ ];
2225
+ var DEFAULT_ISSUE_STATUS = "ready";
2226
+ var DEFAULT_ISSUE_PRIORITY = "medium";
2227
+ var DEFAULT_RESOLVED_ISSUE_DEFAULTS = {
2228
+ defaults: {
2229
+ status: DEFAULT_ISSUE_STATUS,
2230
+ priority: DEFAULT_ISSUE_PRIORITY
2231
+ },
2232
+ overrides: {}
2233
+ };
2234
+ function resolveIssueDefaults(config) {
2235
+ if (config === void 0) {
2236
+ return DEFAULT_RESOLVED_ISSUE_DEFAULTS;
2237
+ }
2238
+ const overrides = {};
2239
+ for (const [phaseLabel, override] of Object.entries(config)) {
2240
+ assertValidPhaseLabel(phaseLabel);
2241
+ assertValidOverride(phaseLabel, override);
2242
+ overrides[phaseLabel] = {
2243
+ status: override.status ?? DEFAULT_ISSUE_STATUS,
2244
+ priority: override.priority ?? DEFAULT_ISSUE_PRIORITY
2245
+ };
2246
+ }
2247
+ return {
2248
+ defaults: {
2249
+ status: DEFAULT_ISSUE_STATUS,
2250
+ priority: DEFAULT_ISSUE_PRIORITY
2251
+ },
2252
+ overrides
2253
+ };
2254
+ }
2255
+ function validateIssueDefaultsConfig(config) {
2256
+ return resolveIssueDefaults(config);
2257
+ }
2258
+ function labelsForPhase(resolved, phaseLabel) {
2259
+ return resolved.overrides[phaseLabel] ?? resolved.defaults;
2260
+ }
2261
+ function assertValidPhaseLabel(phaseLabel) {
2262
+ if (typeof phaseLabel !== "string" || phaseLabel.trim() === "") {
2263
+ throw new Error(
2264
+ "AgentConfigOptions.issueDefaults: phase-label keys must be non-empty strings (e.g. `people:research`)."
2265
+ );
2266
+ }
2267
+ }
2268
+ function assertValidOverride(phaseLabel, override) {
2269
+ if (override === null || typeof override !== "object" || Array.isArray(override)) {
2270
+ throw new Error(
2271
+ `AgentConfigOptions.issueDefaults["${phaseLabel}"] must be an object with optional \`status\` and \`priority\` fields.`
2272
+ );
2273
+ }
2274
+ const { status, priority } = override;
2275
+ if (status === void 0 && priority === void 0) {
2276
+ throw new Error(
2277
+ `AgentConfigOptions.issueDefaults["${phaseLabel}"] must declare at least one of \`status\` or \`priority\`. Empty entries are rejected because they are almost always a typo on the field name.`
2278
+ );
2279
+ }
2280
+ if (status !== void 0 && !VALID_STATUS_VALUES.includes(status)) {
2281
+ throw new Error(
2282
+ `AgentConfigOptions.issueDefaults["${phaseLabel}"].status="${status}" is not a recognised status value. Allowed values: ${VALID_STATUS_VALUES.join(", ")}.`
2283
+ );
2284
+ }
2285
+ if (priority !== void 0 && !VALID_PRIORITY_VALUES.includes(priority)) {
2286
+ throw new Error(
2287
+ `AgentConfigOptions.issueDefaults["${phaseLabel}"].priority="${priority}" is not a recognised priority value. Allowed values: ${VALID_PRIORITY_VALUES.join(", ")}.`
2288
+ );
2289
+ }
2290
+ }
2291
+
2202
2292
  // src/agent/bundles/issue-templates.ts
2203
2293
  var DEFAULT_ISSUE_TEMPLATES_ENABLED = true;
2204
2294
  var DEFAULT_ISSUE_TEMPLATES_PATH = "docs/src/content/docs/agents/issue-templates.md";
2205
2295
  var DEFAULT_ISSUE_TEMPLATES_BUNDLE_PATH_PATTERNS = [".claude/agents/**/*.md", ".claude/skills/**/*.md"];
2206
2296
  var DEFAULT_ISSUE_TEMPLATES_EMIT_CHECKER = false;
2207
2297
  var DEFAULT_ISSUE_TEMPLATES_EMIT_STARTER = false;
2298
+ var ISSUE_TEMPLATES_GENERATED_SUFFIX = "-generated";
2299
+ function issueTemplatesGeneratedPath(templatesPath) {
2300
+ const slash = templatesPath.lastIndexOf("/");
2301
+ const dot = templatesPath.lastIndexOf(".");
2302
+ if (dot > slash) {
2303
+ return `${templatesPath.slice(0, dot)}${ISSUE_TEMPLATES_GENERATED_SUFFIX}${templatesPath.slice(dot)}`;
2304
+ }
2305
+ return `${templatesPath}${ISSUE_TEMPLATES_GENERATED_SUFFIX}`;
2306
+ }
2307
+ function issueTemplatesChildGlob(templatesPath) {
2308
+ const slash = templatesPath.lastIndexOf("/");
2309
+ const dot = templatesPath.lastIndexOf(".");
2310
+ const stem = dot > slash ? templatesPath.slice(0, dot) : templatesPath;
2311
+ return `${stem}/*.md`;
2312
+ }
2208
2313
  var DEFAULT_ISSUE_TEMPLATES_REQUIRE_REFERENCE = true;
2209
2314
  function resolveIssueTemplates(config) {
2210
2315
  const templatesPath = config?.templatesPath ?? DEFAULT_ISSUE_TEMPLATES_PATH;
@@ -2288,7 +2393,40 @@ function renderIssueTemplatesRuleContent(it, hasDownstreamBundles = true) {
2288
2393
  "issue kind appends one section; retiring a kind removes one. The",
2289
2394
  "label taxonomy, body template shape, and placeholder conventions",
2290
2395
  "are shared across every section.",
2291
- "",
2396
+ ""
2397
+ ];
2398
+ if (it.emitStarterDoc) {
2399
+ lines.push(
2400
+ "## Scaffolded Starting Point",
2401
+ "",
2402
+ "The page is hand-authored, but it does not start empty. Two files",
2403
+ "ship together:",
2404
+ "",
2405
+ `1. \`${it.templatesPath}\` \u2014 **written once**, then yours. Titles,`,
2406
+ " bodies, and the callers list are hand-authored here and never",
2407
+ " overwritten.",
2408
+ `2. \`${issueTemplatesGeneratedPath(it.templatesPath)}\` \u2014`,
2409
+ " **regenerated on every synth**. One",
2410
+ " `## Template: <phase-label>` stub per phase label the active",
2411
+ " bundles contribute to the label registry, with the phase label,",
2412
+ " the `type:<bundle>` label its owning bundle declares, and the",
2413
+ " configured `status:*` / `priority:*` defaults pre-filled.",
2414
+ "",
2415
+ "Only the **label set** is generated. Title and body stay",
2416
+ "angle-bracket placeholders, so the scaffold answers *which labels*",
2417
+ "without dictating *what the issue says*. Copy a stub into the",
2418
+ "hand-authored page, flesh out its body, and leave the label block",
2419
+ "as generated.",
2420
+ "",
2421
+ "The split is load-bearing. A write-once scaffold alone freezes",
2422
+ "every consumer on whatever skeleton shipped the day they adopted",
2423
+ "the convention \u2014 a new phase label minted upstream never reaches",
2424
+ "them. Regenerating the label half fixes that without ever",
2425
+ "clobbering an authored body.",
2426
+ ""
2427
+ );
2428
+ }
2429
+ lines.push(
2292
2430
  "## Reference-Don't-Inline Rule",
2293
2431
  "",
2294
2432
  `Every bundle rule, agent prompt, or skill instruction that files a`,
@@ -2359,8 +2497,12 @@ function renderIssueTemplatesRuleContent(it, hasDownstreamBundles = true) {
2359
2497
  " placeholders (`<Organization Name>`, `<slug>`) that match",
2360
2498
  " existing templates. Consistency across sections lets callers",
2361
2499
  " substitute without re-reading the body every time.",
2500
+ "5. **Does the recipe assign a GitHub issue type?** `gh issue",
2501
+ " create` cannot set it and the `type:*` label does not either, so",
2502
+ " every section ends with the follow-up call. An issue that skips",
2503
+ " it stays untyped forever.",
2362
2504
  ""
2363
- ];
2505
+ );
2364
2506
  if (it.emitChecker) {
2365
2507
  lines.push(
2366
2508
  "## Automated Lint",
@@ -2391,6 +2533,33 @@ function renderIssueTemplatesRuleContent(it, hasDownstreamBundles = true) {
2391
2533
  "(which documents the conventional `gh issue create` call shape",
2392
2534
  "as taxonomy, not a downstream template). All other bundle",
2393
2535
  "files in the pattern set are in scope.",
2536
+ "",
2537
+ "### Recipe label correctness",
2538
+ "",
2539
+ "A companion lint,",
2540
+ "`.claude/procedures/check-issue-template-labels.sh`, checks the",
2541
+ "recipes themselves rather than where they live. It walks the",
2542
+ "templates page, its router-style child pages, and the generated",
2543
+ "label-set companion, and fails non-zero when a",
2544
+ "`## Template: <phase-label>` section:",
2545
+ "",
2546
+ "- never passes `--label <phase-label>` \u2014 the heading and the",
2547
+ " command disagree;",
2548
+ "- carries no `type:*` label, more than one, or one that is not",
2549
+ " the `type:<bundle>` label the phase label's owning bundle",
2550
+ " declares;",
2551
+ "- has no GitHub issue-type assignment step.",
2552
+ "",
2553
+ "```bash",
2554
+ ".claude/procedures/check-issue-template-labels.sh",
2555
+ "```",
2556
+ "",
2557
+ "The phase-label \u2192 type-label pairing is generated from the same",
2558
+ "bundle-ownership map that drives `.github/labels.yml` and the",
2559
+ "orchestrator's invariant sweep, so a hand-edit can never",
2560
+ "reintroduce a mismatch the registry has already ruled out.",
2561
+ "Sections whose heading matches no bundle-owned phase label are",
2562
+ "skipped \u2014 consumer-specific labels are deliberately not policed.",
2394
2563
  ""
2395
2564
  );
2396
2565
  }
@@ -2399,8 +2568,10 @@ function renderIssueTemplatesRuleContent(it, hasDownstreamBundles = true) {
2399
2568
  "",
2400
2569
  "The convention intentionally does not:",
2401
2570
  "",
2402
- "- Auto-generate the templates page \u2014 repo-specific phase labels",
2403
- " and body conventions are too varied for a one-size template.",
2571
+ "- Auto-generate recipe **bodies**. Only the label set is",
2572
+ " scaffolded, because labels are derivable from the bundle",
2573
+ " registry and bodies are not \u2014 repo-specific objectives,",
2574
+ " acceptance criteria, and callers lists stay hand-authored.",
2404
2575
  "- Prescribe a fixed body shape across templates. Each section",
2405
2576
  " may carry its own `## Acceptance Criteria`, `## Scope Size`,",
2406
2577
  " or `## Output Path` fields as the downstream agent requires;",
@@ -2431,7 +2602,8 @@ function renderIssueTemplatesBundleHook(it, bundleLabel) {
2431
2602
  "as an inline template vs. a prose mention."
2432
2603
  ].join("\n");
2433
2604
  }
2434
- function renderIssueTemplatesStarterPage(_it) {
2605
+ function renderIssueTemplatesStarterPage(it) {
2606
+ const generatedLink = `./${basename(issueTemplatesGeneratedPath(it.templatesPath))}`;
2435
2607
  return [
2436
2608
  "---",
2437
2609
  "title: Issue Templates",
@@ -2466,6 +2638,20 @@ function renderIssueTemplatesStarterPage(_it) {
2466
2638
  "bundle's rule \u2014 the issue-templates page is a recipe catalogue,",
2467
2639
  "not a duplicate-check layer.",
2468
2640
  "",
2641
+ "## Generated label sets",
2642
+ "",
2643
+ `[Generated Issue-Template Label Sets](${generatedLink}) carries one`,
2644
+ "correct-by-construction stub per phase label the active bundles",
2645
+ "emit \u2014 the phase label paired with the `type:<bundle>` label its",
2646
+ "owning bundle declares, plus the configured `status:*` /",
2647
+ "`priority:*` defaults. That page regenerates on every `projen` run,",
2648
+ "so it never goes stale as phase labels come and go.",
2649
+ "",
2650
+ "This page is the opposite: written once, then owned by you. Copy a",
2651
+ "stub across, flesh out its title and body here, and leave the label",
2652
+ "block exactly as generated \u2014 the label-consistency lint holds both",
2653
+ "copies to the same pairing.",
2654
+ "",
2469
2655
  "## Template: example-phase",
2470
2656
  "",
2471
2657
  "Replace this example with the real templates for this repo. Each",
@@ -2505,6 +2691,115 @@ function renderIssueTemplatesStarterPage(_it) {
2505
2691
  "```"
2506
2692
  ].join("\n");
2507
2693
  }
2694
+ function collectIssueTemplateRecipeStubs(bundles, issueDefaults = DEFAULT_RESOLVED_ISSUE_DEFAULTS) {
2695
+ const stubs = /* @__PURE__ */ new Map();
2696
+ for (const bundle of bundles) {
2697
+ for (const label of bundle.labels ?? []) {
2698
+ const typeLabel = typeLabelForPhaseLabel(label.name);
2699
+ if (typeLabel === void 0) {
2700
+ continue;
2701
+ }
2702
+ if (stubs.has(label.name)) {
2703
+ continue;
2704
+ }
2705
+ const defaults = labelsForPhase(issueDefaults, label.name);
2706
+ stubs.set(label.name, {
2707
+ phaseLabel: label.name,
2708
+ typeLabel,
2709
+ bundleName: bundle.name,
2710
+ description: label.description ?? "",
2711
+ status: defaults.status,
2712
+ priority: defaults.priority,
2713
+ issueType: githubIssueTypeForTitle(`${label.name}: placeholder`)
2714
+ });
2715
+ }
2716
+ }
2717
+ return [...stubs.values()].sort(
2718
+ (a, b) => a.phaseLabel.localeCompare(b.phaseLabel)
2719
+ );
2720
+ }
2721
+ function renderIssueTemplatesGeneratedPage(it, stubs) {
2722
+ const lines = [
2723
+ "---",
2724
+ "title: Generated Issue-Template Label Sets",
2725
+ "description: Auto-generated label-correct gh issue create stubs \u2014 one per phase label.",
2726
+ "---",
2727
+ "",
2728
+ "**Generated file \u2014 do not edit.** Every section below is",
2729
+ "regenerated from the active bundle set on each `projen` run; hand",
2730
+ "edits are overwritten. The hand-authored recipes live in",
2731
+ `[the issue-templates page](./${basename(it.templatesPath)}) \u2014 this`,
2732
+ "page exists so the **label half** of every recipe stays",
2733
+ "correct-by-construction as phase labels come and go.",
2734
+ "",
2735
+ "## How to use",
2736
+ "",
2737
+ "Each section carries the exact label set the phase-label invariant",
2738
+ "requires: the phase label itself, the `type:<bundle>` label its",
2739
+ "owning bundle declares, and the configured `status:*` / `priority:*`",
2740
+ "defaults for that phase. Title and body are placeholders \u2014 copy the",
2741
+ "stub into the hand-authored templates page and flesh out the body",
2742
+ "there, or run it as-is when no repo-specific body exists yet.",
2743
+ "",
2744
+ "Every stub ends with the **GitHub issue type** assignment. `gh issue",
2745
+ "create` cannot set the issue type and the `type:*` label does not",
2746
+ "set it either, so an issue that skips the follow-up call stays",
2747
+ "untyped forever. See **Assigning the GitHub issue type** in the",
2748
+ "`issue-conventions` rule for the no-helper fallback.",
2749
+ ""
2750
+ ];
2751
+ if (stubs.length === 0) {
2752
+ lines.push(
2753
+ "## No phase labels",
2754
+ "",
2755
+ "No active bundle contributes a phase label, so there is nothing to",
2756
+ "scaffold. Enable a phased-pipeline bundle (or drop it from",
2757
+ "`excludeBundles`) and re-run `projen`."
2758
+ );
2759
+ return lines.join("\n");
2760
+ }
2761
+ for (const stub of stubs) {
2762
+ lines.push(
2763
+ `## Template: ${stub.phaseLabel}`,
2764
+ "",
2765
+ ...stub.description === "" ? [] : [stub.description, ""],
2766
+ `**Owning bundle:** \`${stub.bundleName}\` \u2014 **required type label:**`,
2767
+ `\`${stub.typeLabel}\` (paired by the bundle-ownership map).`,
2768
+ "",
2769
+ "**Callers:** <name the bundles and agents that file this issue>",
2770
+ "",
2771
+ "```bash",
2772
+ "gh issue create \\",
2773
+ ` --title "${stub.phaseLabel}: <short description>" \\`,
2774
+ ` --label "${stub.typeLabel}" \\`,
2775
+ ` --label "${stub.phaseLabel}" \\`,
2776
+ ` --label "priority:${stub.priority}" \\`,
2777
+ ` --label "status:${stub.status}" \\`,
2778
+ ' --body "## Objective',
2779
+ "",
2780
+ "<1-3 sentences describing the work>.",
2781
+ "",
2782
+ "## Context",
2783
+ "",
2784
+ "- **Discovered in:** <source description> (#<parent-issue>)",
2785
+ "",
2786
+ "## Acceptance Criteria",
2787
+ "",
2788
+ "- [ ] <criterion 1>",
2789
+ '"',
2790
+ "```",
2791
+ "",
2792
+ `Then assign the GitHub issue type (\`${stub.issueType}\`):`,
2793
+ "",
2794
+ "```bash",
2795
+ `${SET_ISSUE_TYPE_HELPER_PATH} <issue-number> ${stub.issueType}`,
2796
+ "```",
2797
+ ""
2798
+ );
2799
+ }
2800
+ lines.pop();
2801
+ return lines.join("\n");
2802
+ }
2508
2803
  function renderIssueTemplatesCheckerScript(it) {
2509
2804
  const patternsLiteral = it.bundlePathPatterns.map((p) => ` ${JSON.stringify(p)}`).join("\n");
2510
2805
  return [
@@ -2693,6 +2988,157 @@ function renderIssueTemplatesCheckerScript(it) {
2693
2988
  "exit 0"
2694
2989
  ].join("\n");
2695
2990
  }
2991
+ function renderIssueTemplateLabelsCheckerScript(it) {
2992
+ const generatedPath = issueTemplatesGeneratedPath(it.templatesPath);
2993
+ const childGlob = issueTemplatesChildGlob(it.templatesPath);
2994
+ return [
2995
+ "#!/usr/bin/env bash",
2996
+ "# check-issue-template-labels.sh \u2014 Enforce recipe label correctness.",
2997
+ "#",
2998
+ "# Usage:",
2999
+ "# .claude/procedures/check-issue-template-labels.sh [<file>...]",
3000
+ "#",
3001
+ "# With no arguments the lint walks the templates page, its",
3002
+ "# router-style child pages, and the generated label-set companion.",
3003
+ "# Positional arguments override that default set.",
3004
+ "#",
3005
+ "# For every `## Template: <phase-label>` section it asserts the",
3006
+ "# recipe passes the heading's phase label, carries exactly one",
3007
+ "# `type:*` label matching the phase-label invariant, and assigns a",
3008
+ "# GitHub issue type. Sections whose heading matches no bundle-owned",
3009
+ "# phase label are skipped \u2014 unrecognised labels are consumer-",
3010
+ "# specific and deliberately not policed.",
3011
+ "#",
3012
+ "# The resolver below is generated from the canonical bundle-",
3013
+ "# ownership map \u2014 do not edit by hand; regenerate via",
3014
+ "# `pnpm exec projen`.",
3015
+ "",
3016
+ "set -uo pipefail",
3017
+ "",
3018
+ "err() {",
3019
+ ' printf "check-issue-template-labels.sh: %s\\n" "$*" >&2',
3020
+ "}",
3021
+ "",
3022
+ renderPhaseTypeInvariantShellHelpers(),
3023
+ "",
3024
+ `templates_path=${JSON.stringify(it.templatesPath)}`,
3025
+ `generated_path=${JSON.stringify(generatedPath)}`,
3026
+ `child_glob=${JSON.stringify(childGlob)}`,
3027
+ "",
3028
+ "# Emit one TAB-separated record per `## Template:` section:",
3029
+ "# <file>\\t<line>\\t<phase-label>\\t<comma-joined labels>\\t<0|1 typed>",
3030
+ "# Quote characters are folded to spaces up front so a recipe written",
3031
+ "# with single quotes, double quotes, or none at all parses the same.",
3032
+ "# Fenced blocks are tracked so the `## Objective` heading inside a",
3033
+ "# recipe's --body string never reads as the end of the section.",
3034
+ "scan_file() {",
3035
+ ' local file="$1"',
3036
+ ` tr '\\042\\047' ' ' < "$file" | awk -v FNAME="$file" '`,
3037
+ " function flush() {",
3038
+ ' if (phase != "") {',
3039
+ ' printf "%s\\t%d\\t%s\\t%s\\t%d\\n", FNAME, start, phase, labels, hastype;',
3040
+ " }",
3041
+ ' phase = ""; labels = ""; hastype = 0;',
3042
+ " }",
3043
+ " {",
3044
+ " if ($0 ~ /^[ \\t]*```/) {",
3045
+ " fence = 1 - fence;",
3046
+ ' } else if (fence == 0 && index($0, "## ") == 1) {',
3047
+ " flush();",
3048
+ ' if (index($0, "## Template: ") == 1) {',
3049
+ " phase = substr($0, 14);",
3050
+ ' gsub(/[ \\t`]/, "", phase);',
3051
+ " start = NR;",
3052
+ " }",
3053
+ " next;",
3054
+ " }",
3055
+ ' if (phase == "") next;',
3056
+ " if ($0 ~ /set-issue-type\\.sh/ || $0 ~ /updateIssueIssueType/) {",
3057
+ " hastype = 1;",
3058
+ " }",
3059
+ " line = $0;",
3060
+ " while (match(line, /--label[ \\t]+[^ \\t]+/)) {",
3061
+ " tok = substr(line, RSTART, RLENGTH);",
3062
+ " line = substr(line, RSTART + RLENGTH);",
3063
+ ' sub(/^--label[ \\t]+/, "", tok);',
3064
+ ' gsub(/[\\\\`]/, "", tok);',
3065
+ ' labels = labels tok ",";',
3066
+ " }",
3067
+ " }",
3068
+ " END { flush(); }",
3069
+ " '",
3070
+ "}",
3071
+ "",
3072
+ "# Collect the file list: positional args override the default set.",
3073
+ "files=()",
3074
+ "if [[ $# -gt 0 ]]; then",
3075
+ ' files=("$@")',
3076
+ "else",
3077
+ ' [[ -f "$templates_path" ]] && files+=("$templates_path")',
3078
+ ' [[ -f "$generated_path" ]] && files+=("$generated_path")',
3079
+ " for child in $child_glob; do",
3080
+ ' [[ -f "$child" ]] && files+=("$child")',
3081
+ " done",
3082
+ "fi",
3083
+ "",
3084
+ "if [[ ${#files[@]} -eq 0 ]]; then",
3085
+ " exit 0",
3086
+ "fi",
3087
+ "",
3088
+ "violations=()",
3089
+ "checked=0",
3090
+ "",
3091
+ 'for file in "${files[@]}"; do',
3092
+ ' [[ -f "$file" ]] || continue',
3093
+ " while IFS=$'\\t' read -r rec_file rec_line phase labels hastype; do",
3094
+ ' [[ -z "$phase" ]] && continue',
3095
+ ' required=$(phase_label_type_of "$phase")',
3096
+ ' [[ -z "$required" ]] && continue',
3097
+ " checked=$(( checked + 1 ))",
3098
+ ' case ",${labels}" in',
3099
+ ' *",${phase},"*) ;;',
3100
+ ` *) violations+=("$rec_file:$rec_line: recipe for '$phase' never passes --label $phase") ;;`,
3101
+ " esac",
3102
+ ' type_labels=""',
3103
+ " type_count=0",
3104
+ ' for entry in $(printf "%s" "$labels" | tr , " "); do',
3105
+ ' case "$entry" in',
3106
+ " type:*)",
3107
+ ' type_labels="${type_labels}${entry} "',
3108
+ " type_count=$(( type_count + 1 ))",
3109
+ " ;;",
3110
+ " esac",
3111
+ " done",
3112
+ ' if [[ "$type_count" -eq 0 ]]; then',
3113
+ ` violations+=("$rec_file:$rec_line: recipe for '$phase' carries no type:* label (expected $required)")`,
3114
+ ' elif [[ "$type_count" -gt 1 ]]; then',
3115
+ " violations+=(\"$rec_file:$rec_line: recipe for '$phase' carries ${type_count} type:* labels (${type_labels% }); exactly one is allowed\")",
3116
+ ' elif [[ "${type_labels% }" != "$required" ]]; then',
3117
+ ` violations+=("$rec_file:$rec_line: recipe for '$phase' carries \${type_labels% } but the phase label requires $required")`,
3118
+ " fi",
3119
+ ' if [[ "$hastype" != "1" ]]; then',
3120
+ ` violations+=("$rec_file:$rec_line: recipe for '$phase' has no GitHub issue-type assignment step")`,
3121
+ " fi",
3122
+ ' done < <(scan_file "$file")',
3123
+ "done",
3124
+ "",
3125
+ "if [[ ${#violations[@]} -gt 0 ]]; then",
3126
+ ' err "issue-template recipes disagree with the phase-label invariant:"',
3127
+ ' for v in "${violations[@]}"; do',
3128
+ ' err " - $v"',
3129
+ " done",
3130
+ ` err "each '## Template: <phase-label>' recipe must pass its own phase label, exactly one matching type:<bundle> label, and a set-issue-type.sh (or updateIssueIssueType) step"`,
3131
+ " exit 1",
3132
+ "fi",
3133
+ "",
3134
+ 'printf "check-issue-template-labels.sh: %d recipe(s) OK\\n" "$checked"',
3135
+ "exit 0"
3136
+ ].join("\n");
3137
+ }
3138
+ function basename(path8) {
3139
+ const slash = path8.lastIndexOf("/");
3140
+ return slash === -1 ? path8 : path8.slice(slash + 1);
3141
+ }
2696
3142
  function assertValidTemplatesPath(value) {
2697
3143
  if (typeof value !== "string" || value.trim().length === 0) {
2698
3144
  throw new Error(
@@ -5695,90 +6141,6 @@ function buildBaseBundle(paths = DEFAULT_AGENT_PATHS) {
5695
6141
  }
5696
6142
  var baseBundle = buildBaseBundle();
5697
6143
 
5698
- // src/agent/bundles/issue-defaults.ts
5699
- var VALID_STATUS_VALUES = [
5700
- "ready",
5701
- "blocked",
5702
- "in-progress",
5703
- "ready-for-review",
5704
- "needs-attention",
5705
- "done",
5706
- "deferred"
5707
- ];
5708
- var VALID_PRIORITY_VALUES = [
5709
- "critical",
5710
- "high",
5711
- "medium",
5712
- "low",
5713
- "trivial"
5714
- ];
5715
- var DEFAULT_ISSUE_STATUS = "ready";
5716
- var DEFAULT_ISSUE_PRIORITY = "medium";
5717
- var DEFAULT_RESOLVED_ISSUE_DEFAULTS = {
5718
- defaults: {
5719
- status: DEFAULT_ISSUE_STATUS,
5720
- priority: DEFAULT_ISSUE_PRIORITY
5721
- },
5722
- overrides: {}
5723
- };
5724
- function resolveIssueDefaults(config) {
5725
- if (config === void 0) {
5726
- return DEFAULT_RESOLVED_ISSUE_DEFAULTS;
5727
- }
5728
- const overrides = {};
5729
- for (const [phaseLabel, override] of Object.entries(config)) {
5730
- assertValidPhaseLabel(phaseLabel);
5731
- assertValidOverride(phaseLabel, override);
5732
- overrides[phaseLabel] = {
5733
- status: override.status ?? DEFAULT_ISSUE_STATUS,
5734
- priority: override.priority ?? DEFAULT_ISSUE_PRIORITY
5735
- };
5736
- }
5737
- return {
5738
- defaults: {
5739
- status: DEFAULT_ISSUE_STATUS,
5740
- priority: DEFAULT_ISSUE_PRIORITY
5741
- },
5742
- overrides
5743
- };
5744
- }
5745
- function validateIssueDefaultsConfig(config) {
5746
- return resolveIssueDefaults(config);
5747
- }
5748
- function labelsForPhase(resolved, phaseLabel) {
5749
- return resolved.overrides[phaseLabel] ?? resolved.defaults;
5750
- }
5751
- function assertValidPhaseLabel(phaseLabel) {
5752
- if (typeof phaseLabel !== "string" || phaseLabel.trim() === "") {
5753
- throw new Error(
5754
- "AgentConfigOptions.issueDefaults: phase-label keys must be non-empty strings (e.g. `people:research`)."
5755
- );
5756
- }
5757
- }
5758
- function assertValidOverride(phaseLabel, override) {
5759
- if (override === null || typeof override !== "object" || Array.isArray(override)) {
5760
- throw new Error(
5761
- `AgentConfigOptions.issueDefaults["${phaseLabel}"] must be an object with optional \`status\` and \`priority\` fields.`
5762
- );
5763
- }
5764
- const { status, priority } = override;
5765
- if (status === void 0 && priority === void 0) {
5766
- throw new Error(
5767
- `AgentConfigOptions.issueDefaults["${phaseLabel}"] must declare at least one of \`status\` or \`priority\`. Empty entries are rejected because they are almost always a typo on the field name.`
5768
- );
5769
- }
5770
- if (status !== void 0 && !VALID_STATUS_VALUES.includes(status)) {
5771
- throw new Error(
5772
- `AgentConfigOptions.issueDefaults["${phaseLabel}"].status="${status}" is not a recognised status value. Allowed values: ${VALID_STATUS_VALUES.join(", ")}.`
5773
- );
5774
- }
5775
- if (priority !== void 0 && !VALID_PRIORITY_VALUES.includes(priority)) {
5776
- throw new Error(
5777
- `AgentConfigOptions.issueDefaults["${phaseLabel}"].priority="${priority}" is not a recognised priority value. Allowed values: ${VALID_PRIORITY_VALUES.join(", ")}.`
5778
- );
5779
- }
5780
- }
5781
-
5782
6144
  // src/agent/bundles/bcm-writer.ts
5783
6145
  function buildBcmWriterSubAgent(paths, issueDefaults) {
5784
6146
  return {
@@ -33025,6 +33387,8 @@ var UPSTREAM_CONFIGULATOR_DOCS_DETAIL = [
33025
33387
  " --body $'## Summary\\n\\nThe `software-profile-analyst` Phase 2 prompt does not capture the vendor pricing tier (free / paid / enterprise), which downstream consumers need for segment matching.\\n\\n## Details\\n\\n- Add a `Pricing tier` field to the profile template at `docs/src/content/docs/agents/software-profile-analyst.md`\\n- Update the Phase 2 instructions to require the field before commit\\n- Add a regression test in `bundles.test.ts`'",
33026
33388
  "```",
33027
33389
  "",
33390
+ "The call above does **not** set the GitHub issue type \u2014 `gh issue create` cannot. Assign it immediately, in the same session, with the cross-repo GraphQL flow above (`feat:` title \u2192 **Feature**; every other prefix except `epic:` / `fix:` \u2192 **Task**). Skipping it leaves the upstream issue untyped, and nobody comes back for it later.",
33391
+ "",
33028
33392
  "After filing, reference the upstream issue from any local workaround so it can be reverted once the upstream fix lands:",
33029
33393
  "",
33030
33394
  "```typescript",
@@ -35201,11 +35565,34 @@ var AgentConfig = class _AgentConfig extends import_projen8.Component {
35201
35565
  ).split("\n"),
35202
35566
  executable: true
35203
35567
  });
35568
+ new import_projen8.TextFile(
35569
+ this,
35570
+ ".claude/procedures/check-issue-template-labels.sh",
35571
+ {
35572
+ lines: renderIssueTemplateLabelsCheckerScript(
35573
+ resolvedIssueTemplates
35574
+ ).split("\n"),
35575
+ executable: true
35576
+ }
35577
+ );
35204
35578
  }
35205
35579
  if (resolvedIssueTemplates.emitStarterDoc) {
35206
35580
  new import_projen8.SampleFile(this.project, resolvedIssueTemplates.templatesPath, {
35207
35581
  contents: renderIssueTemplatesStarterPage(resolvedIssueTemplates)
35208
35582
  });
35583
+ new import_projen8.TextFile(
35584
+ this,
35585
+ issueTemplatesGeneratedPath(resolvedIssueTemplates.templatesPath),
35586
+ {
35587
+ lines: renderIssueTemplatesGeneratedPage(
35588
+ resolvedIssueTemplates,
35589
+ collectIssueTemplateRecipeStubs(
35590
+ this.activeBundles,
35591
+ resolveIssueDefaults(this.options.issueDefaults)
35592
+ )
35593
+ ).split("\n")
35594
+ }
35595
+ );
35209
35596
  }
35210
35597
  }
35211
35598
  const resolvedTemporalFraming = validateTemporalFramingConfig(
@@ -41860,6 +42247,7 @@ export const collections = {
41860
42247
  DOCS_SYNC_AUDIT_SCHEMA_VERSION,
41861
42248
  GITHUB_ISSUE_TYPES,
41862
42249
  GITHUB_ISSUE_TYPE_BY_TITLE_PREFIX,
42250
+ ISSUE_TEMPLATES_GENERATED_SUFFIX,
41863
42251
  JsiiFaker,
41864
42252
  LAYOUT_ENFORCEMENT,
41865
42253
  LAYOUT_ROOT_BY_PROJECT_TYPE,
@@ -41955,6 +42343,7 @@ export const collections = {
41955
42343
  checkLinksProcedure,
41956
42344
  classifyIssueScope,
41957
42345
  classifyRun,
42346
+ collectIssueTemplateRecipeStubs,
41958
42347
  companyProfileBundle,
41959
42348
  compileFencedSamples,
41960
42349
  createApiDiffCheck,
@@ -41980,6 +42369,8 @@ export const collections = {
41980
42369
  isScheduledTaskOwnedByExcluded,
41981
42370
  isSuppressedWorkflowRule,
41982
42371
  isTypeLabelOwnedByExcluded,
42372
+ issueTemplatesChildGlob,
42373
+ issueTemplatesGeneratedPath,
41983
42374
  jestBundle,
41984
42375
  labelsForPhase,
41985
42376
  maintenanceAuditBundle,
@@ -42030,8 +42421,10 @@ export const collections = {
42030
42421
  renderFocusSection,
42031
42422
  renderGithubIssueTypeSection,
42032
42423
  renderGithubIssueTypeSectionLines,
42424
+ renderIssueTemplateLabelsCheckerScript,
42033
42425
  renderIssueTemplatesBundleHook,
42034
42426
  renderIssueTemplatesCheckerScript,
42427
+ renderIssueTemplatesGeneratedPage,
42035
42428
  renderIssueTemplatesRuleContent,
42036
42429
  renderIssueTemplatesStarterPage,
42037
42430
  renderIssueTypeAssignmentBlanket,