@codedrifters/configulator 0.0.404 → 0.0.405
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.d.mts +121 -1
- package/lib/index.d.ts +122 -2
- package/lib/index.js +704 -200
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +698 -200
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
package/lib/index.mjs
CHANGED
|
@@ -13300,188 +13300,6 @@ function buildMeetingAnalysisBundle(tier = AGENT_MODEL.BALANCED) {
|
|
|
13300
13300
|
}
|
|
13301
13301
|
var meetingAnalysisBundle = buildMeetingAnalysisBundle();
|
|
13302
13302
|
|
|
13303
|
-
// src/agent/bundles/run-ratio.ts
|
|
13304
|
-
var DEFAULT_DISPATCH_TO_HOUSEKEEPING_RATIO = 4;
|
|
13305
|
-
var DEFAULT_STATE_FILE_PATH = ".state/orchestrator-runs.json";
|
|
13306
|
-
var DEFAULT_DISPATCH_MODEL = "opus";
|
|
13307
|
-
var DEFAULT_HOUSEKEEPING_MODEL = "sonnet";
|
|
13308
|
-
function resolveRunRatio(config) {
|
|
13309
|
-
const ratio = config?.ratio ?? DEFAULT_DISPATCH_TO_HOUSEKEEPING_RATIO;
|
|
13310
|
-
assertValidRatio(ratio);
|
|
13311
|
-
const stateFilePath = config?.stateFilePath ?? DEFAULT_STATE_FILE_PATH;
|
|
13312
|
-
assertValidStateFilePath(stateFilePath);
|
|
13313
|
-
return {
|
|
13314
|
-
enabled: config?.enabled ?? true,
|
|
13315
|
-
ratio,
|
|
13316
|
-
stateFilePath,
|
|
13317
|
-
dispatchModel: config?.dispatchModel ?? DEFAULT_DISPATCH_MODEL,
|
|
13318
|
-
housekeepingModel: config?.housekeepingModel ?? DEFAULT_HOUSEKEEPING_MODEL
|
|
13319
|
-
};
|
|
13320
|
-
}
|
|
13321
|
-
function validateRunRatioConfig(config) {
|
|
13322
|
-
return resolveRunRatio(config);
|
|
13323
|
-
}
|
|
13324
|
-
function classifyRun(runCounter, ratio) {
|
|
13325
|
-
if (!ratio.enabled) {
|
|
13326
|
-
return "dispatch";
|
|
13327
|
-
}
|
|
13328
|
-
const cycle = ratio.ratio + 1;
|
|
13329
|
-
return runCounter > 0 && runCounter % cycle === 0 ? "housekeeping" : "dispatch";
|
|
13330
|
-
}
|
|
13331
|
-
function renderRunRatioSection(ratio) {
|
|
13332
|
-
const lines = [
|
|
13333
|
-
"## Run ratio (dispatch vs housekeeping)",
|
|
13334
|
-
"",
|
|
13335
|
-
"The orchestrator keeps a **persistent run counter** and interleaves",
|
|
13336
|
-
"dispatch runs (pick the next ready issue, recommend a worker) with",
|
|
13337
|
-
"**housekeeping runs** (batch PR review + maintenance scan) on a",
|
|
13338
|
-
"configurable ratio. This mirrors openhi's `DISPATCHER.md` contract:",
|
|
13339
|
-
"multiple dispatch runs feed the worker queue, then one batched",
|
|
13340
|
-
"housekeeping run flushes review backlog and runs maintenance",
|
|
13341
|
-
"triage so the pipeline never drifts.",
|
|
13342
|
-
""
|
|
13343
|
-
];
|
|
13344
|
-
if (!ratio.enabled) {
|
|
13345
|
-
lines.push(
|
|
13346
|
-
"**The run ratio is disabled for this project.** Every orchestrator",
|
|
13347
|
-
"run executes the full dispatch pipeline; PR review and maintenance",
|
|
13348
|
-
"remain manual invocations. Enable the ratio via",
|
|
13349
|
-
"`AgentConfigOptions.runRatio.enabled = true` once the operator is",
|
|
13350
|
-
"comfortable with the counter-backed cadence.",
|
|
13351
|
-
""
|
|
13352
|
-
);
|
|
13353
|
-
return lines.join("\n");
|
|
13354
|
-
}
|
|
13355
|
-
const cycle = ratio.ratio + 1;
|
|
13356
|
-
lines.push(
|
|
13357
|
-
"### Cadence",
|
|
13358
|
-
"",
|
|
13359
|
-
`The cycle length is **${cycle}** runs:`,
|
|
13360
|
-
"",
|
|
13361
|
-
`- Runs 1 through ${ratio.ratio} execute the **dispatch** pipeline`,
|
|
13362
|
-
` (recommended model: \`${ratio.dispatchModel}\`).`,
|
|
13363
|
-
`- Run ${cycle} executes the **housekeeping** pipeline`,
|
|
13364
|
-
` (recommended model: \`${ratio.housekeepingModel}\`).`,
|
|
13365
|
-
`- The counter wraps \u2014 run ${cycle + 1} is a dispatch run again, run ${cycle * 2} is the next housekeeping run, and so on.`,
|
|
13366
|
-
"",
|
|
13367
|
-
"The orchestrator increments the counter **once per invocation** at",
|
|
13368
|
-
"the top of the run, before any pipeline phase executes. The",
|
|
13369
|
-
"pre-increment value is never observed \u2014 the tick always returns",
|
|
13370
|
-
"the post-increment counter and the classified run type in a single",
|
|
13371
|
-
"atomic update.",
|
|
13372
|
-
"",
|
|
13373
|
-
"### State file",
|
|
13374
|
-
"",
|
|
13375
|
-
`The run counter persists at \`${ratio.stateFilePath}\`. The file is`,
|
|
13376
|
-
"plain JSON with a single `run_counter` integer field:",
|
|
13377
|
-
"",
|
|
13378
|
-
"```json",
|
|
13379
|
-
'{ "run_counter": 42 }',
|
|
13380
|
-
"```",
|
|
13381
|
-
"",
|
|
13382
|
-
"The state file is **gitignored** in consumer repos (it is local to",
|
|
13383
|
-
"each operator's machine). On a first run, or if the file is missing",
|
|
13384
|
-
"or corrupt, the orchestrator recreates it with `run_counter: 1`.",
|
|
13385
|
-
"",
|
|
13386
|
-
"### Dispatch-run pipeline",
|
|
13387
|
-
"",
|
|
13388
|
-
"1. Phase A \u2014 startup (fetch + checkout default branch).",
|
|
13389
|
-
"2. Phase C \u2014 triage unblock (resolve `Depends on:` chains).",
|
|
13390
|
-
"3. Phase E \u2014 queue scan (pick the top `PICK` line, run the scope",
|
|
13391
|
-
" gate, emit `NEXT_WORK_ITEM`).",
|
|
13392
|
-
"4. Phase F \u2014 cleanup.",
|
|
13393
|
-
"",
|
|
13394
|
-
"### Housekeeping-run pipeline",
|
|
13395
|
-
"",
|
|
13396
|
-
"1. Phase A \u2014 startup.",
|
|
13397
|
-
"2. Phase B \u2014 batch PR review across every eligible open PR.",
|
|
13398
|
-
"3. Phase D \u2014 maintenance scan (stale detection, orphaned branches,",
|
|
13399
|
-
" needs-attention summary).",
|
|
13400
|
-
"4. Phase F \u2014 cleanup.",
|
|
13401
|
-
"",
|
|
13402
|
-
"### Model recommendations",
|
|
13403
|
-
"",
|
|
13404
|
-
`Dispatch runs should use \`${ratio.dispatchModel}\` \u2014 the routing`,
|
|
13405
|
-
"logic, scope gate, and funnel-tier sort benefit from the stronger",
|
|
13406
|
-
"reasoning model. Housekeeping runs are mechanical (read CI status,",
|
|
13407
|
-
"toggle labels, post canned comments) and should use",
|
|
13408
|
-
`\`${ratio.housekeepingModel}\` so the batched PR review and`,
|
|
13409
|
-
"maintenance scan cost less per invocation.",
|
|
13410
|
-
"",
|
|
13411
|
-
"These strings are **informational** \u2014 they surface in the",
|
|
13412
|
-
"orchestrator's rendered rule content so operators know which model",
|
|
13413
|
-
"to run each session against. Configulator does not inject them as",
|
|
13414
|
-
"`model:` frontmatter on the sub-agent definition; the operator (or",
|
|
13415
|
-
"a scheduled task) picks the model at invocation time."
|
|
13416
|
-
);
|
|
13417
|
-
return lines.join("\n");
|
|
13418
|
-
}
|
|
13419
|
-
function renderRunRatioShellHelpers(ratio) {
|
|
13420
|
-
const cycle = ratio.ratio + 1;
|
|
13421
|
-
return [
|
|
13422
|
-
"# Increment the orchestrator run counter and classify the run.",
|
|
13423
|
-
"# Reads the state file (creating it on first run or corruption),",
|
|
13424
|
-
"# increments the counter, writes back atomically, and echoes",
|
|
13425
|
-
"# `run=<n> type=<dispatch|housekeeping>` on stdout.",
|
|
13426
|
-
"#",
|
|
13427
|
-
"# Uses the cycle length (ratio + 1) hard-coded from the resolved",
|
|
13428
|
-
"# RunRatioConfig so the shell helper matches the rendered rule",
|
|
13429
|
-
"# content byte-for-byte.",
|
|
13430
|
-
"run_counter_tick() {",
|
|
13431
|
-
' local state_file="$ORCHESTRATOR_STATE_FILE"',
|
|
13432
|
-
" local state_dir",
|
|
13433
|
-
' state_dir=$(dirname "$state_file")',
|
|
13434
|
-
' mkdir -p "$state_dir" 2>/dev/null || true',
|
|
13435
|
-
"",
|
|
13436
|
-
" local current=0",
|
|
13437
|
-
' if [ -f "$state_file" ]; then',
|
|
13438
|
-
" # jq returns empty string on parse failure; guard against it.",
|
|
13439
|
-
` current=$(jq -r '.run_counter // 0' "$state_file" 2>/dev/null || echo 0)`,
|
|
13440
|
-
' case "$current" in',
|
|
13441
|
-
" ''|*[!0-9]*) current=0 ;;",
|
|
13442
|
-
" esac",
|
|
13443
|
-
" fi",
|
|
13444
|
-
"",
|
|
13445
|
-
" local next=$((current + 1))",
|
|
13446
|
-
"",
|
|
13447
|
-
' local tmp_file="${state_file}.tmp.$$"',
|
|
13448
|
-
` printf '{ "run_counter": %d }\\n' "$next" > "$tmp_file"`,
|
|
13449
|
-
' mv "$tmp_file" "$state_file"',
|
|
13450
|
-
"",
|
|
13451
|
-
" local run_type=dispatch",
|
|
13452
|
-
` if [ $((next % ${cycle})) -eq 0 ]; then`,
|
|
13453
|
-
" run_type=housekeeping",
|
|
13454
|
-
" fi",
|
|
13455
|
-
` printf 'run=%d type=%s\\n' "$next" "$run_type"`,
|
|
13456
|
-
"}"
|
|
13457
|
-
].join("\n");
|
|
13458
|
-
}
|
|
13459
|
-
function assertValidRatio(ratio) {
|
|
13460
|
-
if (!Number.isInteger(ratio)) {
|
|
13461
|
-
throw new Error(
|
|
13462
|
-
`RunRatioConfig.ratio must be a positive integer; got ${ratio}`
|
|
13463
|
-
);
|
|
13464
|
-
}
|
|
13465
|
-
if (ratio < 1) {
|
|
13466
|
-
throw new Error(
|
|
13467
|
-
`RunRatioConfig.ratio must be a positive integer; got ${ratio}`
|
|
13468
|
-
);
|
|
13469
|
-
}
|
|
13470
|
-
}
|
|
13471
|
-
function assertValidStateFilePath(stateFilePath) {
|
|
13472
|
-
const trimmed = stateFilePath.trim();
|
|
13473
|
-
if (trimmed.length === 0) {
|
|
13474
|
-
throw new Error(
|
|
13475
|
-
"RunRatioConfig.stateFilePath must be a non-empty string relative to the repo root"
|
|
13476
|
-
);
|
|
13477
|
-
}
|
|
13478
|
-
if (trimmed.startsWith("/")) {
|
|
13479
|
-
throw new Error(
|
|
13480
|
-
`RunRatioConfig.stateFilePath must be relative to the repo root (no leading '/'); got ${stateFilePath}`
|
|
13481
|
-
);
|
|
13482
|
-
}
|
|
13483
|
-
}
|
|
13484
|
-
|
|
13485
13303
|
// src/agent/bundles/bundle-ownership.ts
|
|
13486
13304
|
var BUNDLE_OWNERSHIP = {
|
|
13487
13305
|
agenda: {
|
|
@@ -13625,6 +13443,255 @@ var BUNDLE_OWNERSHIP = {
|
|
|
13625
13443
|
downstreamIssueKinds: true
|
|
13626
13444
|
}
|
|
13627
13445
|
};
|
|
13446
|
+
var CONVENTIONAL_COMMIT_TYPE_LABELS = [
|
|
13447
|
+
"type:chore",
|
|
13448
|
+
"type:docs",
|
|
13449
|
+
"type:feat",
|
|
13450
|
+
"type:fix",
|
|
13451
|
+
"type:hotfix",
|
|
13452
|
+
"type:refactor",
|
|
13453
|
+
"type:release"
|
|
13454
|
+
];
|
|
13455
|
+
var PHASE_LABEL_TYPE_MAP = buildPhaseLabelTypeMap();
|
|
13456
|
+
function typeLabelForPhaseLabel(phaseLabel) {
|
|
13457
|
+
const exact = PHASE_LABEL_TYPE_MAP[phaseLabel];
|
|
13458
|
+
if (exact !== void 0 && !phaseLabel.endsWith(":")) {
|
|
13459
|
+
return exact;
|
|
13460
|
+
}
|
|
13461
|
+
let bestMatcher;
|
|
13462
|
+
for (const matcher of Object.keys(PHASE_LABEL_TYPE_MAP)) {
|
|
13463
|
+
if (!matcher.endsWith(":")) {
|
|
13464
|
+
continue;
|
|
13465
|
+
}
|
|
13466
|
+
if (!phaseLabel.startsWith(matcher)) {
|
|
13467
|
+
continue;
|
|
13468
|
+
}
|
|
13469
|
+
if (bestMatcher === void 0 || matcher.length > bestMatcher.length) {
|
|
13470
|
+
bestMatcher = matcher;
|
|
13471
|
+
}
|
|
13472
|
+
}
|
|
13473
|
+
return bestMatcher === void 0 ? void 0 : PHASE_LABEL_TYPE_MAP[bestMatcher];
|
|
13474
|
+
}
|
|
13475
|
+
function resolveTypeLabelForLabels(labels) {
|
|
13476
|
+
const phaseLabels = [];
|
|
13477
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
13478
|
+
for (const label of labels) {
|
|
13479
|
+
const typeLabel = typeLabelForPhaseLabel(label);
|
|
13480
|
+
if (typeLabel === void 0) {
|
|
13481
|
+
continue;
|
|
13482
|
+
}
|
|
13483
|
+
phaseLabels.push(label);
|
|
13484
|
+
candidates.add(typeLabel);
|
|
13485
|
+
}
|
|
13486
|
+
const candidateTypeLabels = Array.from(candidates).sort();
|
|
13487
|
+
if (candidateTypeLabels.length === 0) {
|
|
13488
|
+
return { outcome: "none", candidateTypeLabels: [], phaseLabels: [] };
|
|
13489
|
+
}
|
|
13490
|
+
if (candidateTypeLabels.length === 1) {
|
|
13491
|
+
return {
|
|
13492
|
+
outcome: "match",
|
|
13493
|
+
typeLabel: candidateTypeLabels[0],
|
|
13494
|
+
candidateTypeLabels,
|
|
13495
|
+
phaseLabels
|
|
13496
|
+
};
|
|
13497
|
+
}
|
|
13498
|
+
return { outcome: "ambiguous", candidateTypeLabels, phaseLabels };
|
|
13499
|
+
}
|
|
13500
|
+
function renderPhaseTypeInvariantSection(excludeBundles = []) {
|
|
13501
|
+
const rows = Object.keys(PHASE_LABEL_TYPE_MAP).filter(
|
|
13502
|
+
(matcher) => !isPhaseLabelMatcherOwnedByExcluded(matcher, excludeBundles)
|
|
13503
|
+
).sort().map((matcher) => {
|
|
13504
|
+
const display = matcher.endsWith(":") ? `${matcher}*` : matcher;
|
|
13505
|
+
const kind = matcher.endsWith(":") ? "prefix" : "exact";
|
|
13506
|
+
return `| \`${display}\` | ${kind} | \`${PHASE_LABEL_TYPE_MAP[matcher]}\` |`;
|
|
13507
|
+
});
|
|
13508
|
+
return [
|
|
13509
|
+
"## Phase-label \u2192 `type:<bundle>` invariant",
|
|
13510
|
+
"",
|
|
13511
|
+
"Every phased-pipeline bundle pairs its `<bundle>:<phase>` labels",
|
|
13512
|
+
"with exactly one `type:<bundle>` label. That type label is the",
|
|
13513
|
+
"**dedup + dispatch signal**: agents de-duplicate downstream work",
|
|
13514
|
+
'with `gh issue list --label "type:<bundle>"`, and Phase E derives',
|
|
13515
|
+
"its funnel-tier sort key from the issue's `type:*` label. An issue",
|
|
13516
|
+
"that carries the phase label but a conventional-commit `type:*`",
|
|
13517
|
+
"(`type:feat`, `type:docs`, \u2026, stamped from its title prefix by the",
|
|
13518
|
+
"generic create-issue workflow) is invisible to that dedup query and",
|
|
13519
|
+
"mis-tiers in dispatch.",
|
|
13520
|
+
"",
|
|
13521
|
+
"The pairing below is derived from the canonical bundle-ownership",
|
|
13522
|
+
"map \u2014 the same source of truth that generates the label registry.",
|
|
13523
|
+
"Matchers ending in `*` match by prefix; the rest match exactly, and",
|
|
13524
|
+
"an exact match always beats a prefix match.",
|
|
13525
|
+
"",
|
|
13526
|
+
"| Phase label | Match | Required type label |",
|
|
13527
|
+
"|-------------|-------|---------------------|",
|
|
13528
|
+
...rows,
|
|
13529
|
+
"",
|
|
13530
|
+
"### Enforcement",
|
|
13531
|
+
"",
|
|
13532
|
+
"The Phase D maintenance sweep runs the invariant in auto-correct",
|
|
13533
|
+
"mode and emits one summary line:",
|
|
13534
|
+
"",
|
|
13535
|
+
"```",
|
|
13536
|
+
"LABEL_INVARIANT_DONE mode=fix checked=<N> ok=<O> violations=<V> corrected=<C> flagged=<F>",
|
|
13537
|
+
"```",
|
|
13538
|
+
"",
|
|
13539
|
+
"A **report-only** audit is available for repos that prefer",
|
|
13540
|
+
"report-then-fix over auto-correct \u2014 same sweep, no mutations:",
|
|
13541
|
+
"",
|
|
13542
|
+
"```bash",
|
|
13543
|
+
".claude/procedures/check-blocked.sh label-invariant # report only",
|
|
13544
|
+
".claude/procedures/check-blocked.sh label-invariant --fix # apply",
|
|
13545
|
+
"```",
|
|
13546
|
+
"",
|
|
13547
|
+
"### Correction policy",
|
|
13548
|
+
"",
|
|
13549
|
+
"An issue must carry **exactly one** `type:*` label. Merely *adding*",
|
|
13550
|
+
"the required type label to an issue that already carries a",
|
|
13551
|
+
"conventional-commit one would leave two, making the funnel-tier sort",
|
|
13552
|
+
"key (which reads the **first** `type:*` label) non-deterministic and",
|
|
13553
|
+
"violating the label conventions. The correction therefore",
|
|
13554
|
+
"**replaces**, in a single atomic `gh issue edit`, so the issue is",
|
|
13555
|
+
"never observable carrying two `type:*` labels:",
|
|
13556
|
+
"",
|
|
13557
|
+
"| Issue state | Action |",
|
|
13558
|
+
"|-------------|--------|",
|
|
13559
|
+
"| Required type label present, nothing else | none \u2014 compliant, left untouched |",
|
|
13560
|
+
"| No `type:*` label at all | **add** the required label |",
|
|
13561
|
+
"| Only conventional-commit `type:*` label(s) | **replace** \u2014 remove them, add the required label |",
|
|
13562
|
+
"| Required label present **plus** a stray conventional-commit one | **remove** the stray, leaving exactly one |",
|
|
13563
|
+
"| A `type:*` label owned by a **different** bundle | **flag** `status:needs-attention` \u2014 never guessed at |",
|
|
13564
|
+
"| Phase labels imply **two or more** type labels | **flag** `status:needs-attention` \u2014 never auto-corrected |",
|
|
13565
|
+
"",
|
|
13566
|
+
"Only a conventional-commit `type:*` label is ever removed. A",
|
|
13567
|
+
"`type:*` label owned by another bundle carries real routing",
|
|
13568
|
+
"information, so the sweep refuses to guess and hands the issue to a",
|
|
13569
|
+
"human instead. Ambiguity is narrower than it looks: all three",
|
|
13570
|
+
"requirements bundles declare `type:requirement`, so every `req:*`",
|
|
13571
|
+
"phase label resolves to the same type label \u2014 genuine ambiguity",
|
|
13572
|
+
"needs phase labels from two bundles with *different* type labels.",
|
|
13573
|
+
"",
|
|
13574
|
+
"`status:needs-attention` is applied **additively** \u2014 the base",
|
|
13575
|
+
"`status:*` label always stays, per the additive-flag rule. Flagging",
|
|
13576
|
+
"is idempotent: an issue already carrying the flag is reported but",
|
|
13577
|
+
"not re-flagged."
|
|
13578
|
+
].join("\n");
|
|
13579
|
+
}
|
|
13580
|
+
function isPhaseLabelMatcherOwnedByExcluded(matcher, excludeBundles) {
|
|
13581
|
+
if (excludeBundles.length === 0) {
|
|
13582
|
+
return false;
|
|
13583
|
+
}
|
|
13584
|
+
let owned = false;
|
|
13585
|
+
for (const [bundleName, ownership] of Object.entries(BUNDLE_OWNERSHIP)) {
|
|
13586
|
+
if (!ownership.phaseLabelPrefixes.includes(matcher)) {
|
|
13587
|
+
continue;
|
|
13588
|
+
}
|
|
13589
|
+
owned = true;
|
|
13590
|
+
if (!excludeBundles.includes(bundleName)) {
|
|
13591
|
+
return false;
|
|
13592
|
+
}
|
|
13593
|
+
}
|
|
13594
|
+
return owned;
|
|
13595
|
+
}
|
|
13596
|
+
function renderPhaseTypeInvariantShellHelpers() {
|
|
13597
|
+
const matchers = Object.keys(PHASE_LABEL_TYPE_MAP);
|
|
13598
|
+
const exactMatchers = matchers.filter((m) => !m.endsWith(":")).sort();
|
|
13599
|
+
const prefixMatchers = matchers.filter((m) => m.endsWith(":")).sort((a, b) => b.length - a.length || a.localeCompare(b));
|
|
13600
|
+
const lines = [
|
|
13601
|
+
"# Resolve ONE phase label to the `type:<bundle>` label its owning",
|
|
13602
|
+
"# bundle declares. Echoes the label (with the `type:` prefix) or",
|
|
13603
|
+
"# nothing when no bundle owns it. Exact-match branches come first,",
|
|
13604
|
+
"# so `req:write` (requirements-writer) beats the `req:` prefix",
|
|
13605
|
+
"# (requirements-analyst). Generated from BUNDLE_OWNERSHIP \u2014 do not",
|
|
13606
|
+
"# hand-edit.",
|
|
13607
|
+
"phase_label_type_of() {",
|
|
13608
|
+
' case "${1:-}" in'
|
|
13609
|
+
];
|
|
13610
|
+
for (const matcher of exactMatchers) {
|
|
13611
|
+
lines.push(` ${matcher}) echo "${PHASE_LABEL_TYPE_MAP[matcher]}" ;;`);
|
|
13612
|
+
}
|
|
13613
|
+
for (const matcher of prefixMatchers) {
|
|
13614
|
+
lines.push(` ${matcher}*) echo "${PHASE_LABEL_TYPE_MAP[matcher]}" ;;`);
|
|
13615
|
+
}
|
|
13616
|
+
lines.push(
|
|
13617
|
+
" *) : ;;",
|
|
13618
|
+
" esac",
|
|
13619
|
+
"}",
|
|
13620
|
+
"",
|
|
13621
|
+
"# Return 0 when the argument is a conventional-commit `type:*`",
|
|
13622
|
+
"# label \u2014 the ONLY type labels the invariant correction may remove.",
|
|
13623
|
+
"# A `type:*` label owned by another bundle is never removed; the",
|
|
13624
|
+
"# issue is flagged for human triage instead.",
|
|
13625
|
+
"is_conventional_type_label() {",
|
|
13626
|
+
` case "\${1:-}" in`,
|
|
13627
|
+
` ${CONVENTIONAL_COMMIT_TYPE_LABELS.join("|")}) return 0 ;;`,
|
|
13628
|
+
" *) return 1 ;;",
|
|
13629
|
+
" esac",
|
|
13630
|
+
"}",
|
|
13631
|
+
"",
|
|
13632
|
+
"# Resolve an issue's FULL label list (one label per line on stdin)",
|
|
13633
|
+
"# to the type label the phase-label invariant requires. Emits",
|
|
13634
|
+
"# KEY=VALUE assignments the caller parses:",
|
|
13635
|
+
"# OUTCOME=none \u2014 no recognised phase label; invariant N/A",
|
|
13636
|
+
"# OUTCOME=match \u2014 plus TYPE_LABEL=<type:bundle>",
|
|
13637
|
+
"# OUTCOME=ambiguous \u2014 plus CANDIDATE_TYPE_LABELS=<space-separated>",
|
|
13638
|
+
"# PHASE_LABELS=<space-separated recognised phase labels>",
|
|
13639
|
+
"phase_type_of() {",
|
|
13640
|
+
" local label resolved",
|
|
13641
|
+
' local matched_types=""',
|
|
13642
|
+
' local matched_phase=""',
|
|
13643
|
+
" while IFS= read -r label; do",
|
|
13644
|
+
' [ -z "$label" ] && continue',
|
|
13645
|
+
' resolved=$(phase_label_type_of "$label")',
|
|
13646
|
+
' [ -z "$resolved" ] && continue',
|
|
13647
|
+
' matched_phase="${matched_phase}${label} "',
|
|
13648
|
+
' case " ${matched_types} " in',
|
|
13649
|
+
' *" ${resolved} "*) ;;',
|
|
13650
|
+
' *) matched_types="${matched_types}${resolved} " ;;',
|
|
13651
|
+
" esac",
|
|
13652
|
+
" done",
|
|
13653
|
+
" local count=0",
|
|
13654
|
+
" for resolved in $matched_types; do",
|
|
13655
|
+
" count=$((count + 1))",
|
|
13656
|
+
" done",
|
|
13657
|
+
' if [ "$count" -eq 0 ]; then',
|
|
13658
|
+
' echo "OUTCOME=none"',
|
|
13659
|
+
' elif [ "$count" -eq 1 ]; then',
|
|
13660
|
+
' echo "OUTCOME=match"',
|
|
13661
|
+
' echo "TYPE_LABEL=${matched_types% }"',
|
|
13662
|
+
" else",
|
|
13663
|
+
' echo "OUTCOME=ambiguous"',
|
|
13664
|
+
' echo "CANDIDATE_TYPE_LABELS=${matched_types% }"',
|
|
13665
|
+
" fi",
|
|
13666
|
+
' echo "PHASE_LABELS=${matched_phase% }"',
|
|
13667
|
+
"}"
|
|
13668
|
+
);
|
|
13669
|
+
return lines.join("\n");
|
|
13670
|
+
}
|
|
13671
|
+
function buildPhaseLabelTypeMap() {
|
|
13672
|
+
const map = {};
|
|
13673
|
+
for (const [bundleName, ownership] of Object.entries(BUNDLE_OWNERSHIP)) {
|
|
13674
|
+
if (ownership.phaseLabelPrefixes.length === 0) {
|
|
13675
|
+
continue;
|
|
13676
|
+
}
|
|
13677
|
+
if (ownership.typeLabels.length !== 1) {
|
|
13678
|
+
throw new Error(
|
|
13679
|
+
`BUNDLE_OWNERSHIP["${bundleName}"] declares ${ownership.phaseLabelPrefixes.length} phase-label matcher(s) but ${ownership.typeLabels.length} type label(s); a phase label must pair with exactly one type label`
|
|
13680
|
+
);
|
|
13681
|
+
}
|
|
13682
|
+
const typeLabel = `type:${ownership.typeLabels[0]}`;
|
|
13683
|
+
for (const matcher of ownership.phaseLabelPrefixes) {
|
|
13684
|
+
const existing = map[matcher];
|
|
13685
|
+
if (existing !== void 0 && existing !== typeLabel) {
|
|
13686
|
+
throw new Error(
|
|
13687
|
+
`Phase-label matcher "${matcher}" resolves to both "${existing}" and "${typeLabel}"; a matcher must imply exactly one type label`
|
|
13688
|
+
);
|
|
13689
|
+
}
|
|
13690
|
+
map[matcher] = typeLabel;
|
|
13691
|
+
}
|
|
13692
|
+
}
|
|
13693
|
+
return map;
|
|
13694
|
+
}
|
|
13628
13695
|
function isTypeLabelOwnedByExcluded(typeLabel, excludedBundles) {
|
|
13629
13696
|
if (excludedBundles.length === 0) {
|
|
13630
13697
|
return false;
|
|
@@ -13714,6 +13781,188 @@ function findOwnersOfTypeLabel(typeLabel) {
|
|
|
13714
13781
|
return owners;
|
|
13715
13782
|
}
|
|
13716
13783
|
|
|
13784
|
+
// src/agent/bundles/run-ratio.ts
|
|
13785
|
+
var DEFAULT_DISPATCH_TO_HOUSEKEEPING_RATIO = 4;
|
|
13786
|
+
var DEFAULT_STATE_FILE_PATH = ".state/orchestrator-runs.json";
|
|
13787
|
+
var DEFAULT_DISPATCH_MODEL = "opus";
|
|
13788
|
+
var DEFAULT_HOUSEKEEPING_MODEL = "sonnet";
|
|
13789
|
+
function resolveRunRatio(config) {
|
|
13790
|
+
const ratio = config?.ratio ?? DEFAULT_DISPATCH_TO_HOUSEKEEPING_RATIO;
|
|
13791
|
+
assertValidRatio(ratio);
|
|
13792
|
+
const stateFilePath = config?.stateFilePath ?? DEFAULT_STATE_FILE_PATH;
|
|
13793
|
+
assertValidStateFilePath(stateFilePath);
|
|
13794
|
+
return {
|
|
13795
|
+
enabled: config?.enabled ?? true,
|
|
13796
|
+
ratio,
|
|
13797
|
+
stateFilePath,
|
|
13798
|
+
dispatchModel: config?.dispatchModel ?? DEFAULT_DISPATCH_MODEL,
|
|
13799
|
+
housekeepingModel: config?.housekeepingModel ?? DEFAULT_HOUSEKEEPING_MODEL
|
|
13800
|
+
};
|
|
13801
|
+
}
|
|
13802
|
+
function validateRunRatioConfig(config) {
|
|
13803
|
+
return resolveRunRatio(config);
|
|
13804
|
+
}
|
|
13805
|
+
function classifyRun(runCounter, ratio) {
|
|
13806
|
+
if (!ratio.enabled) {
|
|
13807
|
+
return "dispatch";
|
|
13808
|
+
}
|
|
13809
|
+
const cycle = ratio.ratio + 1;
|
|
13810
|
+
return runCounter > 0 && runCounter % cycle === 0 ? "housekeeping" : "dispatch";
|
|
13811
|
+
}
|
|
13812
|
+
function renderRunRatioSection(ratio) {
|
|
13813
|
+
const lines = [
|
|
13814
|
+
"## Run ratio (dispatch vs housekeeping)",
|
|
13815
|
+
"",
|
|
13816
|
+
"The orchestrator keeps a **persistent run counter** and interleaves",
|
|
13817
|
+
"dispatch runs (pick the next ready issue, recommend a worker) with",
|
|
13818
|
+
"**housekeeping runs** (batch PR review + maintenance scan) on a",
|
|
13819
|
+
"configurable ratio. This mirrors openhi's `DISPATCHER.md` contract:",
|
|
13820
|
+
"multiple dispatch runs feed the worker queue, then one batched",
|
|
13821
|
+
"housekeeping run flushes review backlog and runs maintenance",
|
|
13822
|
+
"triage so the pipeline never drifts.",
|
|
13823
|
+
""
|
|
13824
|
+
];
|
|
13825
|
+
if (!ratio.enabled) {
|
|
13826
|
+
lines.push(
|
|
13827
|
+
"**The run ratio is disabled for this project.** Every orchestrator",
|
|
13828
|
+
"run executes the full dispatch pipeline; PR review and maintenance",
|
|
13829
|
+
"remain manual invocations. Enable the ratio via",
|
|
13830
|
+
"`AgentConfigOptions.runRatio.enabled = true` once the operator is",
|
|
13831
|
+
"comfortable with the counter-backed cadence.",
|
|
13832
|
+
""
|
|
13833
|
+
);
|
|
13834
|
+
return lines.join("\n");
|
|
13835
|
+
}
|
|
13836
|
+
const cycle = ratio.ratio + 1;
|
|
13837
|
+
lines.push(
|
|
13838
|
+
"### Cadence",
|
|
13839
|
+
"",
|
|
13840
|
+
`The cycle length is **${cycle}** runs:`,
|
|
13841
|
+
"",
|
|
13842
|
+
`- Runs 1 through ${ratio.ratio} execute the **dispatch** pipeline`,
|
|
13843
|
+
` (recommended model: \`${ratio.dispatchModel}\`).`,
|
|
13844
|
+
`- Run ${cycle} executes the **housekeeping** pipeline`,
|
|
13845
|
+
` (recommended model: \`${ratio.housekeepingModel}\`).`,
|
|
13846
|
+
`- The counter wraps \u2014 run ${cycle + 1} is a dispatch run again, run ${cycle * 2} is the next housekeeping run, and so on.`,
|
|
13847
|
+
"",
|
|
13848
|
+
"The orchestrator increments the counter **once per invocation** at",
|
|
13849
|
+
"the top of the run, before any pipeline phase executes. The",
|
|
13850
|
+
"pre-increment value is never observed \u2014 the tick always returns",
|
|
13851
|
+
"the post-increment counter and the classified run type in a single",
|
|
13852
|
+
"atomic update.",
|
|
13853
|
+
"",
|
|
13854
|
+
"### State file",
|
|
13855
|
+
"",
|
|
13856
|
+
`The run counter persists at \`${ratio.stateFilePath}\`. The file is`,
|
|
13857
|
+
"plain JSON with a single `run_counter` integer field:",
|
|
13858
|
+
"",
|
|
13859
|
+
"```json",
|
|
13860
|
+
'{ "run_counter": 42 }',
|
|
13861
|
+
"```",
|
|
13862
|
+
"",
|
|
13863
|
+
"The state file is **gitignored** in consumer repos (it is local to",
|
|
13864
|
+
"each operator's machine). On a first run, or if the file is missing",
|
|
13865
|
+
"or corrupt, the orchestrator recreates it with `run_counter: 1`.",
|
|
13866
|
+
"",
|
|
13867
|
+
"### Dispatch-run pipeline",
|
|
13868
|
+
"",
|
|
13869
|
+
"1. Phase A \u2014 startup (fetch + checkout default branch).",
|
|
13870
|
+
"2. Phase C \u2014 triage unblock (resolve `Depends on:` chains).",
|
|
13871
|
+
"3. Phase E \u2014 queue scan (pick the top `PICK` line, run the scope",
|
|
13872
|
+
" gate, emit `NEXT_WORK_ITEM`).",
|
|
13873
|
+
"4. Phase F \u2014 cleanup.",
|
|
13874
|
+
"",
|
|
13875
|
+
"### Housekeeping-run pipeline",
|
|
13876
|
+
"",
|
|
13877
|
+
"1. Phase A \u2014 startup.",
|
|
13878
|
+
"2. Phase B \u2014 batch PR review across every eligible open PR.",
|
|
13879
|
+
"3. Phase D \u2014 maintenance scan (stale detection, orphaned branches,",
|
|
13880
|
+
" needs-attention summary).",
|
|
13881
|
+
"4. Phase F \u2014 cleanup.",
|
|
13882
|
+
"",
|
|
13883
|
+
"### Model recommendations",
|
|
13884
|
+
"",
|
|
13885
|
+
`Dispatch runs should use \`${ratio.dispatchModel}\` \u2014 the routing`,
|
|
13886
|
+
"logic, scope gate, and funnel-tier sort benefit from the stronger",
|
|
13887
|
+
"reasoning model. Housekeeping runs are mechanical (read CI status,",
|
|
13888
|
+
"toggle labels, post canned comments) and should use",
|
|
13889
|
+
`\`${ratio.housekeepingModel}\` so the batched PR review and`,
|
|
13890
|
+
"maintenance scan cost less per invocation.",
|
|
13891
|
+
"",
|
|
13892
|
+
"These strings are **informational** \u2014 they surface in the",
|
|
13893
|
+
"orchestrator's rendered rule content so operators know which model",
|
|
13894
|
+
"to run each session against. Configulator does not inject them as",
|
|
13895
|
+
"`model:` frontmatter on the sub-agent definition; the operator (or",
|
|
13896
|
+
"a scheduled task) picks the model at invocation time."
|
|
13897
|
+
);
|
|
13898
|
+
return lines.join("\n");
|
|
13899
|
+
}
|
|
13900
|
+
function renderRunRatioShellHelpers(ratio) {
|
|
13901
|
+
const cycle = ratio.ratio + 1;
|
|
13902
|
+
return [
|
|
13903
|
+
"# Increment the orchestrator run counter and classify the run.",
|
|
13904
|
+
"# Reads the state file (creating it on first run or corruption),",
|
|
13905
|
+
"# increments the counter, writes back atomically, and echoes",
|
|
13906
|
+
"# `run=<n> type=<dispatch|housekeeping>` on stdout.",
|
|
13907
|
+
"#",
|
|
13908
|
+
"# Uses the cycle length (ratio + 1) hard-coded from the resolved",
|
|
13909
|
+
"# RunRatioConfig so the shell helper matches the rendered rule",
|
|
13910
|
+
"# content byte-for-byte.",
|
|
13911
|
+
"run_counter_tick() {",
|
|
13912
|
+
' local state_file="$ORCHESTRATOR_STATE_FILE"',
|
|
13913
|
+
" local state_dir",
|
|
13914
|
+
' state_dir=$(dirname "$state_file")',
|
|
13915
|
+
' mkdir -p "$state_dir" 2>/dev/null || true',
|
|
13916
|
+
"",
|
|
13917
|
+
" local current=0",
|
|
13918
|
+
' if [ -f "$state_file" ]; then',
|
|
13919
|
+
" # jq returns empty string on parse failure; guard against it.",
|
|
13920
|
+
` current=$(jq -r '.run_counter // 0' "$state_file" 2>/dev/null || echo 0)`,
|
|
13921
|
+
' case "$current" in',
|
|
13922
|
+
" ''|*[!0-9]*) current=0 ;;",
|
|
13923
|
+
" esac",
|
|
13924
|
+
" fi",
|
|
13925
|
+
"",
|
|
13926
|
+
" local next=$((current + 1))",
|
|
13927
|
+
"",
|
|
13928
|
+
' local tmp_file="${state_file}.tmp.$$"',
|
|
13929
|
+
` printf '{ "run_counter": %d }\\n' "$next" > "$tmp_file"`,
|
|
13930
|
+
' mv "$tmp_file" "$state_file"',
|
|
13931
|
+
"",
|
|
13932
|
+
" local run_type=dispatch",
|
|
13933
|
+
` if [ $((next % ${cycle})) -eq 0 ]; then`,
|
|
13934
|
+
" run_type=housekeeping",
|
|
13935
|
+
" fi",
|
|
13936
|
+
` printf 'run=%d type=%s\\n' "$next" "$run_type"`,
|
|
13937
|
+
"}"
|
|
13938
|
+
].join("\n");
|
|
13939
|
+
}
|
|
13940
|
+
function assertValidRatio(ratio) {
|
|
13941
|
+
if (!Number.isInteger(ratio)) {
|
|
13942
|
+
throw new Error(
|
|
13943
|
+
`RunRatioConfig.ratio must be a positive integer; got ${ratio}`
|
|
13944
|
+
);
|
|
13945
|
+
}
|
|
13946
|
+
if (ratio < 1) {
|
|
13947
|
+
throw new Error(
|
|
13948
|
+
`RunRatioConfig.ratio must be a positive integer; got ${ratio}`
|
|
13949
|
+
);
|
|
13950
|
+
}
|
|
13951
|
+
}
|
|
13952
|
+
function assertValidStateFilePath(stateFilePath) {
|
|
13953
|
+
const trimmed = stateFilePath.trim();
|
|
13954
|
+
if (trimmed.length === 0) {
|
|
13955
|
+
throw new Error(
|
|
13956
|
+
"RunRatioConfig.stateFilePath must be a non-empty string relative to the repo root"
|
|
13957
|
+
);
|
|
13958
|
+
}
|
|
13959
|
+
if (trimmed.startsWith("/")) {
|
|
13960
|
+
throw new Error(
|
|
13961
|
+
`RunRatioConfig.stateFilePath must be relative to the repo root (no leading '/'); got ${stateFilePath}`
|
|
13962
|
+
);
|
|
13963
|
+
}
|
|
13964
|
+
}
|
|
13965
|
+
|
|
13717
13966
|
// src/agent/bundles/scheduled-tasks.ts
|
|
13718
13967
|
var SCHEDULED_TASK_MODEL_VALUES = ["opus", "sonnet", "haiku"];
|
|
13719
13968
|
var SCHEDULED_TASK_KIND_VALUES = ["issue-worker", "pipeline"];
|
|
@@ -15669,6 +15918,7 @@ function buildCheckBlockedScript(tiers, scopeGate, _runRatio) {
|
|
|
15669
15918
|
"# .claude/procedures/check-blocked.sh maintenance",
|
|
15670
15919
|
"# .claude/procedures/check-blocked.sh prs",
|
|
15671
15920
|
"# .claude/procedures/check-blocked.sh scope <issue-number>",
|
|
15921
|
+
"# .claude/procedures/check-blocked.sh label-invariant [--fix]",
|
|
15672
15922
|
"",
|
|
15673
15923
|
"set -uo pipefail",
|
|
15674
15924
|
"",
|
|
@@ -15718,6 +15968,8 @@ function buildCheckBlockedScript(tiers, scopeGate, _runRatio) {
|
|
|
15718
15968
|
"",
|
|
15719
15969
|
scopeHelperIndented,
|
|
15720
15970
|
"",
|
|
15971
|
+
renderPhaseTypeInvariantShellHelpers(),
|
|
15972
|
+
"",
|
|
15721
15973
|
...renderDelegationActiveSignalsHelper(),
|
|
15722
15974
|
"",
|
|
15723
15975
|
"# \u2500\u2500 subcommands \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
@@ -16353,16 +16605,43 @@ function buildCheckBlockedScript(tiers, scopeGate, _runRatio) {
|
|
|
16353
16605
|
" esac",
|
|
16354
16606
|
' done <<< "$lease_output"',
|
|
16355
16607
|
"",
|
|
16608
|
+
" # \u2500\u2500 phase-label \u2192 type:<bundle> invariant (MUTATING) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
16609
|
+
" # Run the label-invariant sweep in --fix mode and surface its",
|
|
16610
|
+
" # per-issue lines for log visibility. cmd_label_invariant applies",
|
|
16611
|
+
" # the label edits itself (a single atomic remove+add per issue, so",
|
|
16612
|
+
" # an issue is never observable carrying two type:* labels) and",
|
|
16613
|
+
" # flags the cases it must not guess at. Its terminal",
|
|
16614
|
+
" # LABEL_INVARIANT_DONE line is captured and re-emitted alongside",
|
|
16615
|
+
" # MAINTENANCE_DONE so the orchestrator reads every summary from",
|
|
16616
|
+
" # one maintenance invocation.",
|
|
16617
|
+
" local label_output",
|
|
16618
|
+
" label_output=$(cmd_label_invariant --fix)",
|
|
16619
|
+
' local label_summary="LABEL_INVARIANT_DONE mode=fix checked=0 ok=0 violations=0 corrected=0 flagged=0"',
|
|
16620
|
+
"",
|
|
16621
|
+
" while IFS= read -r line; do",
|
|
16622
|
+
' [[ -z "$line" ]] && continue',
|
|
16623
|
+
' case "$line" in',
|
|
16624
|
+
" 'LABEL_INVARIANT_DONE '*)",
|
|
16625
|
+
' label_summary="$line"',
|
|
16626
|
+
" ;;",
|
|
16627
|
+
" *)",
|
|
16628
|
+
' echo "$line"',
|
|
16629
|
+
" ;;",
|
|
16630
|
+
" esac",
|
|
16631
|
+
' done <<< "$label_output"',
|
|
16632
|
+
"",
|
|
16356
16633
|
" # \u2500\u2500 needs-attention total \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
16357
16634
|
" local needs_attention_total",
|
|
16358
16635
|
' needs_attention_total=$(gh issue list --label "status:needs-attention" --state open \\',
|
|
16359
16636
|
" --json number --limit 100 2>/dev/null | jq 'length' 2>/dev/null || echo 0)",
|
|
16360
16637
|
" needs_attention_total=${needs_attention_total:-0}",
|
|
16361
16638
|
"",
|
|
16362
|
-
" #
|
|
16363
|
-
" # MAINTENANCE_DONE line
|
|
16639
|
+
" # Three summary lines consumed by the orchestrator: the",
|
|
16640
|
+
" # issue/orphan MAINTENANCE_DONE line, the PR-lease LEASE_RECONCILE",
|
|
16641
|
+
" # line, and the LABEL_INVARIANT_DONE line.",
|
|
16364
16642
|
' echo "MAINTENANCE_DONE flagged_stale=${flagged_stale_count} flagged_blocked=${flagged_blocked_count} orphan_branches=${orphan_branches_count} orphan_prs=${orphan_prs_count} needs_attention_total=${needs_attention_total}"',
|
|
16365
16643
|
' echo "$lease_summary"',
|
|
16644
|
+
' echo "$label_summary"',
|
|
16366
16645
|
"}",
|
|
16367
16646
|
"",
|
|
16368
16647
|
"cmd_prs() {",
|
|
@@ -16428,6 +16707,196 @@ function buildCheckBlockedScript(tiers, scopeGate, _runRatio) {
|
|
|
16428
16707
|
' done <<< "$eligible"',
|
|
16429
16708
|
"}",
|
|
16430
16709
|
"",
|
|
16710
|
+
"cmd_label_invariant() {",
|
|
16711
|
+
" # Phase-label \u2192 type:<bundle> invariant sweep.",
|
|
16712
|
+
" #",
|
|
16713
|
+
" # Every phased-pipeline bundle pairs its <bundle>:<phase> labels",
|
|
16714
|
+
" # with exactly one type:<bundle> label. An agent that reconstructs",
|
|
16715
|
+
" # a gh issue create call from prose can stamp a conventional-commit",
|
|
16716
|
+
" # type label derived from the title prefix instead, producing an",
|
|
16717
|
+
" # issue that carries the phase label but the WRONG type label \u2014",
|
|
16718
|
+
" # invisible to the --label type:<bundle> duplicate-check idiom and",
|
|
16719
|
+
" # mis-tiered by the funnel-tier sort in cmd_eligible.",
|
|
16720
|
+
" #",
|
|
16721
|
+
" # Default mode is REPORT-ONLY (the consumer-runnable audit); pass",
|
|
16722
|
+
" # --fix to apply corrections. cmd_maintenance runs the --fix mode",
|
|
16723
|
+
" # as part of the Phase D sweep.",
|
|
16724
|
+
" #",
|
|
16725
|
+
" # CORRECTION POLICY. An issue must carry EXACTLY ONE type:* label",
|
|
16726
|
+
" # (cmd_eligible derives its funnel-tier sort key from the FIRST",
|
|
16727
|
+
" # one, and the label conventions mandate exactly one), so the",
|
|
16728
|
+
" # correction REPLACES rather than merely adds:",
|
|
16729
|
+
" #",
|
|
16730
|
+
" # no type:* at all \u2192 add the implied type label",
|
|
16731
|
+
" # only conventional-commit \u2192 remove them, add the implied",
|
|
16732
|
+
" # type labels present label (single atomic edit)",
|
|
16733
|
+
" # implied label already there \u2192 remove the stray conventional",
|
|
16734
|
+
" # alongside a conventional one label, leaving exactly one",
|
|
16735
|
+
" # a type:* owned by ANOTHER \u2192 FLAG status:needs-attention;",
|
|
16736
|
+
" # bundle never guess which to drop",
|
|
16737
|
+
" # phase labels imply 2+ types \u2192 FLAG status:needs-attention",
|
|
16738
|
+
" #",
|
|
16739
|
+
" # Only a conventional-commit type label is ever removed \u2014 the set",
|
|
16740
|
+
" # is_conventional_type_label() recognises. A type:* label owned by",
|
|
16741
|
+
" # a different bundle is a genuine conflict a human must resolve.",
|
|
16742
|
+
" # status:needs-attention is applied ADDITIVELY; the base status:*",
|
|
16743
|
+
" # label always stays (see the additive-flag rule).",
|
|
16744
|
+
" local apply=0",
|
|
16745
|
+
' if [[ "${1:-}" == "--fix" ]]; then',
|
|
16746
|
+
" apply=1",
|
|
16747
|
+
" fi",
|
|
16748
|
+
' local mode="report"',
|
|
16749
|
+
' [[ "$apply" -eq 1 ]] && mode="fix"',
|
|
16750
|
+
"",
|
|
16751
|
+
" local issues",
|
|
16752
|
+
" issues=$(gh issue list --state open --json number,labels \\",
|
|
16753
|
+
' --limit 1000 2>/dev/null || echo "[]")',
|
|
16754
|
+
"",
|
|
16755
|
+
" # Filter jq-side and emit a TWO-field record. The label list is",
|
|
16756
|
+
" # last AND guaranteed non-empty by the select, so no field can",
|
|
16757
|
+
" # collapse under IFS=tab and shift the record left (#884). The",
|
|
16758
|
+
" # title is deliberately not threaded through \u2014 every output line",
|
|
16759
|
+
" # keys off the issue number.",
|
|
16760
|
+
" local issue_data",
|
|
16761
|
+
` issue_data=$(echo "$issues" | jq -r '`,
|
|
16762
|
+
" .[] |",
|
|
16763
|
+
" (.labels | map(.name)) as $names |",
|
|
16764
|
+
" select($names | length > 0) |",
|
|
16765
|
+
' "\\(.number)\\t\\($names | join(","))"',
|
|
16766
|
+
" ' 2>/dev/null)",
|
|
16767
|
+
"",
|
|
16768
|
+
" local checked_count=0",
|
|
16769
|
+
" local ok_count=0",
|
|
16770
|
+
" local corrected_count=0",
|
|
16771
|
+
" local flagged_count=0",
|
|
16772
|
+
"",
|
|
16773
|
+
" while IFS=$'\\t' read -r num labels_csv; do",
|
|
16774
|
+
' [[ -z "$num" ]] && continue',
|
|
16775
|
+
"",
|
|
16776
|
+
" local labels_nl",
|
|
16777
|
+
` labels_nl=$(printf '%s' "$labels_csv" | tr ',' '\\n')`,
|
|
16778
|
+
"",
|
|
16779
|
+
" local resolution assignment",
|
|
16780
|
+
" local outcome='' type_label='' phase_labels='' candidates=''",
|
|
16781
|
+
` resolution=$(printf '%s\\n' "$labels_nl" | phase_type_of)`,
|
|
16782
|
+
" while IFS= read -r assignment; do",
|
|
16783
|
+
' case "$assignment" in',
|
|
16784
|
+
" OUTCOME=*) outcome=${assignment#OUTCOME=} ;;",
|
|
16785
|
+
" TYPE_LABEL=*) type_label=${assignment#TYPE_LABEL=} ;;",
|
|
16786
|
+
" CANDIDATE_TYPE_LABELS=*) candidates=${assignment#CANDIDATE_TYPE_LABELS=} ;;",
|
|
16787
|
+
" PHASE_LABELS=*) phase_labels=${assignment#PHASE_LABELS=} ;;",
|
|
16788
|
+
" esac",
|
|
16789
|
+
' done <<< "$resolution"',
|
|
16790
|
+
"",
|
|
16791
|
+
" # No recognised phase label \u2014 the invariant does not apply.",
|
|
16792
|
+
" # Consumer-specific `foo:bar` labels are deliberately not policed.",
|
|
16793
|
+
' [[ "$outcome" == "none" ]] && continue',
|
|
16794
|
+
" checked_count=$((checked_count + 1))",
|
|
16795
|
+
"",
|
|
16796
|
+
" # Idempotent flagging: read the existing flag off the labels we",
|
|
16797
|
+
" # already fetched rather than spending another API call.",
|
|
16798
|
+
" local already_flagged=0",
|
|
16799
|
+
' case ",${labels_csv}," in',
|
|
16800
|
+
' *",status:needs-attention,"*) already_flagged=1 ;;',
|
|
16801
|
+
" esac",
|
|
16802
|
+
"",
|
|
16803
|
+
' if [[ "$outcome" == "ambiguous" ]]; then',
|
|
16804
|
+
' echo "LABEL_AMBIGUOUS #${num} phase=\\"${phase_labels}\\" candidates=\\"${candidates}\\""',
|
|
16805
|
+
" flagged_count=$((flagged_count + 1))",
|
|
16806
|
+
' if [[ "$apply" -eq 1 && "$already_flagged" -eq 0 ]]; then',
|
|
16807
|
+
' if gh issue edit "$num" --add-label "status:needs-attention" >/dev/null 2>&1; then',
|
|
16808
|
+
' gh issue comment "$num" \\',
|
|
16809
|
+
' --body "Label invariant: phase label(s) ${phase_labels} imply more than one bundle type label (${candidates}). Flagged for human triage \u2014 an ambiguous pairing is never auto-corrected." >/dev/null 2>&1 || true',
|
|
16810
|
+
' echo "LABEL_FLAGGED #${num} \u2014 added status:needs-attention (ambiguous)"',
|
|
16811
|
+
" else",
|
|
16812
|
+
' echo "LABEL_FLAG_FAILED #${num} \u2014 could not add status:needs-attention (ambiguous)"',
|
|
16813
|
+
" fi",
|
|
16814
|
+
" fi",
|
|
16815
|
+
" continue",
|
|
16816
|
+
" fi",
|
|
16817
|
+
"",
|
|
16818
|
+
" # Partition the issue's existing type:* labels against the",
|
|
16819
|
+
" # implied one.",
|
|
16820
|
+
" local label",
|
|
16821
|
+
" local has_expected=0",
|
|
16822
|
+
" local conv_types=''",
|
|
16823
|
+
" local foreign_types=''",
|
|
16824
|
+
" while IFS= read -r label; do",
|
|
16825
|
+
' [ -z "$label" ] && continue',
|
|
16826
|
+
' case "$label" in',
|
|
16827
|
+
" type:*) ;;",
|
|
16828
|
+
" *) continue ;;",
|
|
16829
|
+
" esac",
|
|
16830
|
+
' if [ "$label" = "$type_label" ]; then',
|
|
16831
|
+
" has_expected=1",
|
|
16832
|
+
" continue",
|
|
16833
|
+
" fi",
|
|
16834
|
+
' if is_conventional_type_label "$label"; then',
|
|
16835
|
+
' conv_types="${conv_types}${label} "',
|
|
16836
|
+
" else",
|
|
16837
|
+
' foreign_types="${foreign_types}${label} "',
|
|
16838
|
+
" fi",
|
|
16839
|
+
' done <<< "$labels_nl"',
|
|
16840
|
+
"",
|
|
16841
|
+
' if [[ "$has_expected" -eq 1 && -z "${conv_types}${foreign_types}" ]]; then',
|
|
16842
|
+
' echo "LABEL_OK #${num} type=${type_label}"',
|
|
16843
|
+
" ok_count=$((ok_count + 1))",
|
|
16844
|
+
" continue",
|
|
16845
|
+
" fi",
|
|
16846
|
+
"",
|
|
16847
|
+
" # A type:* label owned by a DIFFERENT bundle is a conflict the",
|
|
16848
|
+
" # sweep must not guess at: removing it would destroy routing",
|
|
16849
|
+
" # information, and adding alongside it would leave two bundle",
|
|
16850
|
+
" # type labels and a non-deterministic funnel tier.",
|
|
16851
|
+
' if [[ -n "$foreign_types" ]]; then',
|
|
16852
|
+
' echo "LABEL_CONFLICT #${num} phase=\\"${phase_labels}\\" expected=${type_label} foreign=\\"${foreign_types% }\\""',
|
|
16853
|
+
" flagged_count=$((flagged_count + 1))",
|
|
16854
|
+
' if [[ "$apply" -eq 1 && "$already_flagged" -eq 0 ]]; then',
|
|
16855
|
+
' if gh issue edit "$num" --add-label "status:needs-attention" >/dev/null 2>&1; then',
|
|
16856
|
+
' gh issue comment "$num" \\',
|
|
16857
|
+
' --body "Label invariant: phase label(s) ${phase_labels} require ${type_label}, but this issue carries ${foreign_types% }, which is owned by another bundle. Flagged for human triage \u2014 the sweep only removes conventional-commit type labels." >/dev/null 2>&1 || true',
|
|
16858
|
+
' echo "LABEL_FLAGGED #${num} \u2014 added status:needs-attention (conflicting bundle type)"',
|
|
16859
|
+
" else",
|
|
16860
|
+
' echo "LABEL_FLAG_FAILED #${num} \u2014 could not add status:needs-attention (conflict)"',
|
|
16861
|
+
" fi",
|
|
16862
|
+
" fi",
|
|
16863
|
+
" continue",
|
|
16864
|
+
" fi",
|
|
16865
|
+
"",
|
|
16866
|
+
" # Correctable: add the implied label and/or drop the",
|
|
16867
|
+
" # conventional-commit label(s) shadowing it, in ONE edit so the",
|
|
16868
|
+
" # issue is never observable carrying two type:* labels.",
|
|
16869
|
+
' local removing="${conv_types% }"',
|
|
16870
|
+
" local adding=''",
|
|
16871
|
+
' [[ "$has_expected" -eq 0 ]] && adding="$type_label"',
|
|
16872
|
+
' echo "LABEL_VIOLATION #${num} phase=\\"${phase_labels}\\" expected=${type_label} add=\\"${adding}\\" remove=\\"${removing}\\""',
|
|
16873
|
+
' if [[ "$apply" -eq 0 ]]; then',
|
|
16874
|
+
" continue",
|
|
16875
|
+
" fi",
|
|
16876
|
+
"",
|
|
16877
|
+
" local -a edit_args=()",
|
|
16878
|
+
" local stray",
|
|
16879
|
+
" for stray in $removing; do",
|
|
16880
|
+
' edit_args+=(--remove-label "$stray")',
|
|
16881
|
+
" done",
|
|
16882
|
+
' [[ -n "$adding" ]] && edit_args+=(--add-label "$adding")',
|
|
16883
|
+
' if [[ "${#edit_args[@]}" -eq 0 ]]; then',
|
|
16884
|
+
" continue",
|
|
16885
|
+
" fi",
|
|
16886
|
+
' if gh issue edit "$num" "${edit_args[@]}" >/dev/null 2>&1; then',
|
|
16887
|
+
' gh issue comment "$num" \\',
|
|
16888
|
+
' --body "Label invariant: phase label(s) ${phase_labels} require ${type_label}. Corrected \u2014 added: ${adding:-(none)}; removed: ${removing:-(none)}." >/dev/null 2>&1 || true',
|
|
16889
|
+
' echo "LABEL_CORRECTED #${num} added=\\"${adding}\\" removed=\\"${removing}\\""',
|
|
16890
|
+
" corrected_count=$((corrected_count + 1))",
|
|
16891
|
+
" else",
|
|
16892
|
+
' echo "LABEL_CORRECT_FAILED #${num} \u2014 label edit failed"',
|
|
16893
|
+
" fi",
|
|
16894
|
+
' done <<< "$issue_data"',
|
|
16895
|
+
"",
|
|
16896
|
+
" local violations_count=$((checked_count - ok_count))",
|
|
16897
|
+
' echo "LABEL_INVARIANT_DONE mode=${mode} checked=${checked_count} ok=${ok_count} violations=${violations_count} corrected=${corrected_count} flagged=${flagged_count}"',
|
|
16898
|
+
"}",
|
|
16899
|
+
"",
|
|
16431
16900
|
"cmd_scope() {",
|
|
16432
16901
|
' local issue_num="${1:-}"',
|
|
16433
16902
|
' if [[ -z "$issue_num" ]]; then',
|
|
@@ -16514,8 +16983,9 @@ function buildCheckBlockedScript(tiers, scopeGate, _runRatio) {
|
|
|
16514
16983
|
' maintenance) shift; cmd_maintenance "$@" ;;',
|
|
16515
16984
|
' prs) shift; cmd_prs "$@" ;;',
|
|
16516
16985
|
' scope) shift; cmd_scope "$@" ;;',
|
|
16986
|
+
' label-invariant) shift; cmd_label_invariant "$@" ;;',
|
|
16517
16987
|
" help|*)",
|
|
16518
|
-
' echo "Usage: check-blocked.sh <unblock|eligible|stale|orphaned|lease-reconcile|maintenance|prs|scope>"',
|
|
16988
|
+
' echo "Usage: check-blocked.sh <unblock|eligible|stale|orphaned|lease-reconcile|maintenance|prs|scope|label-invariant>"',
|
|
16519
16989
|
" exit 1",
|
|
16520
16990
|
" ;;",
|
|
16521
16991
|
"esac"
|
|
@@ -16524,7 +16994,7 @@ function buildCheckBlockedScript(tiers, scopeGate, _runRatio) {
|
|
|
16524
16994
|
function buildCheckBlockedProcedure(tiers, scopeGate = resolveScopeGate(), runRatio = resolveRunRatio()) {
|
|
16525
16995
|
return {
|
|
16526
16996
|
name: "check-blocked.sh",
|
|
16527
|
-
description: "Token-efficient issue triage script with subcommands: eligible, unblock, stale, orphaned, lease-reconcile, maintenance, prs, scope. Sorts eligible issues by priority desc \u2192 funnel tier asc \u2192 issue number asc, excluding issues carrying `status:needs-attention` or `status:deferred`; the scope subcommand classifies a single issue against the scope-gate thresholds; the unblock subcommand applies the `status:blocked` \u2192 `status:ready` label flip itself, posts the canned unblock comment, skips human-parked `status:deferred` candidates with a `SKIP_DEFERRED` line, and emits a single `TRIAGE_DONE unblocked=N still_blocked=M deferred_skipped=K` summary line; the lease-reconcile subcommand auto-reconciles stuck `review:fixing` PR leases \u2014 re-applying `review:needs-worker` on an orphaned lease (fix-list stale, no worker report) so the Phase B1 drain retries, and removing `review:needs-worker` on a consumed-but-uncleared wedge (branch HEAD advanced past the fix-list, marker still set, no worker report) so the reviewer confirm pass can merge, always posting an audit-trail note comment and emitting a single `LEASE_RECONCILE orphaned=N consumed_uncleared=M` summary line; the maintenance subcommand flags stale issues with `status:needs-attention` (never auto-resets to `status:ready`), counts orphan branches/PRs, folds in the lease-reconcile sweep, and emits a `MAINTENANCE_DONE flagged_stale=N flagged_blocked=M orphan_branches=A orphan_prs=B needs_attention_total=T` summary line followed by the `LEASE_RECONCILE` line.",
|
|
16997
|
+
description: "Token-efficient issue triage script with subcommands: eligible, unblock, stale, orphaned, lease-reconcile, maintenance, prs, scope, label-invariant. Sorts eligible issues by priority desc \u2192 funnel tier asc \u2192 issue number asc, excluding issues carrying `status:needs-attention` or `status:deferred`; the scope subcommand classifies a single issue against the scope-gate thresholds; the unblock subcommand applies the `status:blocked` \u2192 `status:ready` label flip itself, posts the canned unblock comment, skips human-parked `status:deferred` candidates with a `SKIP_DEFERRED` line, and emits a single `TRIAGE_DONE unblocked=N still_blocked=M deferred_skipped=K` summary line; the lease-reconcile subcommand auto-reconciles stuck `review:fixing` PR leases \u2014 re-applying `review:needs-worker` on an orphaned lease (fix-list stale, no worker report) so the Phase B1 drain retries, and removing `review:needs-worker` on a consumed-but-uncleared wedge (branch HEAD advanced past the fix-list, marker still set, no worker report) so the reviewer confirm pass can merge, always posting an audit-trail note comment and emitting a single `LEASE_RECONCILE orphaned=N consumed_uncleared=M` summary line; the maintenance subcommand flags stale issues with `status:needs-attention` (never auto-resets to `status:ready`), counts orphan branches/PRs, folds in the lease-reconcile sweep and the label-invariant auto-correction, and emits a `MAINTENANCE_DONE flagged_stale=N flagged_blocked=M orphan_branches=A orphan_prs=B needs_attention_total=T` summary line followed by the `LEASE_RECONCILE` and `LABEL_INVARIANT_DONE` lines; the label-invariant subcommand enforces the phase-label \u2192 `type:<bundle>` pairing derived from the canonical `BUNDLE_OWNERSHIP` map \u2014 report-only by default, auto-correcting with `--fix` (replacing a shadowing conventional-commit `type:*` rather than merely adding, so the issue keeps exactly one `type:*`) and flagging `status:needs-attention` when the pairing is ambiguous or collides with another bundle's type label, emitting a single `LABEL_INVARIANT_DONE mode=M checked=N ok=O violations=V corrected=C flagged=F` summary line.",
|
|
16528
16998
|
content: buildCheckBlockedScript(tiers, scopeGate, runRatio)
|
|
16529
16999
|
};
|
|
16530
17000
|
}
|
|
@@ -17240,32 +17710,36 @@ var orchestratorSubAgent = {
|
|
|
17240
17710
|
"## Phase D: Maintenance",
|
|
17241
17711
|
"",
|
|
17242
17712
|
"Run the bundled `check-blocked.sh maintenance` procedure and read",
|
|
17243
|
-
"**only** the
|
|
17244
|
-
"`LEASE_RECONCILE`. The procedure folds
|
|
17245
|
-
"orphan-detection, needs-attention summary,
|
|
17246
|
-
"auto-reconcile
|
|
17247
|
-
"into a single sweep \u2014
|
|
17248
|
-
"
|
|
17249
|
-
"
|
|
17250
|
-
"
|
|
17251
|
-
"
|
|
17713
|
+
"**only** the three summary lines it emits \u2014 `MAINTENANCE_DONE`,",
|
|
17714
|
+
"`LEASE_RECONCILE`, and `LABEL_INVARIANT_DONE`. The procedure folds",
|
|
17715
|
+
"the stale-detection, orphan-detection, needs-attention summary,",
|
|
17716
|
+
"PR-lease auto-reconcile, and phase-label invariant sweep that",
|
|
17717
|
+
"earlier revisions split across D1, D2, and D3 into a single sweep \u2014",
|
|
17718
|
+
"applying the `status:needs-attention` label and posting the canned",
|
|
17719
|
+
"flag comment for each stale / stale-blocked issue itself,",
|
|
17720
|
+
"reconciling stuck `review:fixing` PR leases itself, and correcting",
|
|
17721
|
+
"mislabeled pipeline issues itself, mirroring the discipline of",
|
|
17722
|
+
"Phase B (`pr-sweep.sh`) and Phase C (`check-blocked.sh unblock`).",
|
|
17252
17723
|
"",
|
|
17253
17724
|
"```bash",
|
|
17254
17725
|
".claude/procedures/check-blocked.sh maintenance",
|
|
17255
17726
|
"```",
|
|
17256
17727
|
"",
|
|
17257
|
-
"The script emits
|
|
17728
|
+
"The script emits three summary lines in this shape:",
|
|
17258
17729
|
"",
|
|
17259
17730
|
"```",
|
|
17260
17731
|
"MAINTENANCE_DONE flagged_stale=<N> flagged_blocked=<M> orphan_branches=<A> orphan_prs=<B> needs_attention_total=<T>",
|
|
17261
17732
|
"LEASE_RECONCILE orphaned=<N> consumed_uncleared=<M>",
|
|
17733
|
+
"LABEL_INVARIANT_DONE mode=fix checked=<N> ok=<O> violations=<V> corrected=<C> flagged=<F>",
|
|
17262
17734
|
"```",
|
|
17263
17735
|
"",
|
|
17264
17736
|
"Per-issue / per-orphan / per-PR informational lines (`FLAGGED_STALE",
|
|
17265
17737
|
"#N`, `FLAGGED_BLOCKED #N`, `STALE #N \u2014 \u2026`, `STALE_BLOCKED #N \u2014 \u2026`,",
|
|
17266
17738
|
"`ORPHAN_BRANCH \u2026`, `ORPHAN_PR #N \u2014 \u2026`, `FLAG_FAILED #N \u2014 \u2026`,",
|
|
17267
17739
|
"`LEASE_ORPHANED PR #N \u2014 \u2026`, `LEASE_CONSUMED_UNCLEARED PR #N \u2014 \u2026`,",
|
|
17268
|
-
"`LEASE_RECONCILE_FAILED PR #N \u2014 \u2026`
|
|
17740
|
+
"`LEASE_RECONCILE_FAILED PR #N \u2014 \u2026`, `LABEL_VIOLATION #N \u2014 \u2026`,",
|
|
17741
|
+
"`LABEL_CORRECTED #N \u2014 \u2026`, `LABEL_AMBIGUOUS #N \u2014 \u2026`,",
|
|
17742
|
+
"`LABEL_CONFLICT #N \u2014 \u2026`) are emitted for log visibility",
|
|
17269
17743
|
"but are **not** load-bearing for the orchestrator \u2014 partial failures",
|
|
17270
17744
|
"(one bad `gh` call) do not abort the sweep, they are simply omitted",
|
|
17271
17745
|
"from the counters.",
|
|
@@ -17305,9 +17779,24 @@ var orchestratorSubAgent = {
|
|
|
17305
17779
|
"is never silently force-released. The `review:fixing` lease itself",
|
|
17306
17780
|
"is always left for the reviewer confirm pass to release.",
|
|
17307
17781
|
"",
|
|
17308
|
-
"
|
|
17309
|
-
"
|
|
17310
|
-
"
|
|
17782
|
+
"**Phase-label invariant (`LABEL_INVARIANT_DONE`).** The sweep also",
|
|
17783
|
+
"enforces the phase-label \u2192 `type:<bundle>` pairing on every open",
|
|
17784
|
+
"issue, so a pipeline issue filed with a conventional-commit",
|
|
17785
|
+
"`type:*` (derived from its title prefix) cannot stay invisible to",
|
|
17786
|
+
'the `--label "type:<bundle>"` duplicate-check idiom or mis-tier in',
|
|
17787
|
+
"Phase E's funnel-tier sort. The script mutates the labels itself \u2014",
|
|
17788
|
+
"the orchestrator only reads the summary line. See the **Phase-label",
|
|
17789
|
+
"\u2192 `type:<bundle>` invariant** section in `CLAUDE.md` for the full",
|
|
17790
|
+
"correction policy; in short, a shadowing conventional-commit type",
|
|
17791
|
+
"label is **replaced** (never merely supplemented, so the issue",
|
|
17792
|
+
"keeps exactly one `type:*`), and an ambiguous or cross-bundle",
|
|
17793
|
+
"collision is **flagged** `status:needs-attention` rather than",
|
|
17794
|
+
"guessed at.",
|
|
17795
|
+
"",
|
|
17796
|
+
"Log all three summary lines and continue to Phase E regardless of",
|
|
17797
|
+
"outcomes \u2014 a non-zero `flagged_*`, `orphan_*`, `orphaned`,",
|
|
17798
|
+
"`consumed_uncleared`, `corrected`, or `flagged` count is",
|
|
17799
|
+
"informational, not a failure.",
|
|
17311
17800
|
"",
|
|
17312
17801
|
"## Phase E: Queue Scan",
|
|
17313
17802
|
"",
|
|
@@ -18309,6 +18798,7 @@ var ORCHESTRATOR_CONVENTIONS_PREAMBLE = [
|
|
|
18309
18798
|
"- Stale thresholds: 72h for in-progress, 168h for blocked",
|
|
18310
18799
|
"- Flagged issues get `status:needs-attention` \u2014 they are not auto-reset",
|
|
18311
18800
|
"- **Phase D auto-reconciles stuck PR leases.** The maintenance sweep's `check-blocked.sh maintenance` invocation folds in a PR-lease reconcile that unwedges `review:fixing` PRs the Phase B1 drain and the worker hand-off left stuck, emitting a `LEASE_RECONCILE orphaned=<N> consumed_uncleared=<M>` line alongside `MAINTENANCE_DONE`. An **orphaned lease** (`review:fixing` without `review:needs-worker`, fix-list older than the 72h stale threshold, no worker-report newer than the fix-list) is **re-armed** \u2014 the sweep re-applies `review:needs-worker` so the Phase B1 drain retries the delegation, leaving the lease untouched. A **consumed-but-uncleared wedge** (both labels present, branch HEAD newer than the fix-list, no worker-report \u2014 the worker pushed but skipped the hand-off) is **auto-reconciled** \u2014 the sweep removes `review:needs-worker` so the reviewer confirm pass can merge. Both paths post an audit-trail note comment; a lease is never silently force-released, and the `review:fixing` lease itself is always left for the reviewer confirm pass to release.",
|
|
18801
|
+
"- **Phase D enforces the phase-label \u2192 `type:<bundle>` invariant.** The same maintenance sweep folds in a label-invariant pass that auto-corrects open issues carrying a pipeline phase label without the matching `type:<bundle>` label, emitting a `LABEL_INVARIANT_DONE mode=fix checked=<N> ok=<O> violations=<V> corrected=<C> flagged=<F>` line alongside `MAINTENANCE_DONE`. The correction **replaces** a shadowing conventional-commit `type:*` rather than merely adding the bundle type, so the issue keeps exactly one `type:*` and Phase E's funnel-tier sort key stays deterministic. Ambiguous pairings and collisions with another bundle's `type:*` label are **flagged** `status:needs-attention` (additively) instead of guessed at. See the **Phase-label \u2192 `type:<bundle>` invariant** section below for the matcher table and the full correction policy.",
|
|
18312
18802
|
"",
|
|
18313
18803
|
"## Depth-0 invocation requirement",
|
|
18314
18804
|
"",
|
|
@@ -18324,6 +18814,8 @@ function buildOrchestratorConventionsContent(tiers, scopeGate = resolveScopeGate
|
|
|
18324
18814
|
"",
|
|
18325
18815
|
renderScopeGateSection(scopeGate, excludeBundles),
|
|
18326
18816
|
"",
|
|
18817
|
+
renderPhaseTypeInvariantSection(excludeBundles),
|
|
18818
|
+
"",
|
|
18327
18819
|
renderScheduledTasksSection(scheduledTasks),
|
|
18328
18820
|
"",
|
|
18329
18821
|
renderUnblockDependentsSection(unblockDependents)
|
|
@@ -40688,6 +41180,7 @@ export {
|
|
|
40688
41180
|
CDK_WATCH_DEFAULTS_BY_STAGE,
|
|
40689
41181
|
CLAUDE_RULE_TARGET,
|
|
40690
41182
|
COMPLETE_JOB_ID,
|
|
41183
|
+
CONVENTIONAL_COMMIT_TYPE_LABELS,
|
|
40691
41184
|
CdkCli,
|
|
40692
41185
|
DEFAULT_AC_THRESHOLDS,
|
|
40693
41186
|
DEFAULT_AGENT_PATHS,
|
|
@@ -40759,6 +41252,7 @@ export {
|
|
|
40759
41252
|
MONOREPO_LAYOUT,
|
|
40760
41253
|
MonorepoProject,
|
|
40761
41254
|
Nvmrc,
|
|
41255
|
+
PHASE_LABEL_TYPE_MAP,
|
|
40762
41256
|
PROD_DEPLOY_NAME,
|
|
40763
41257
|
PROGRESS_FILES_FORMAT_VALUES,
|
|
40764
41258
|
PnpmWorkspace,
|
|
@@ -40919,6 +41413,8 @@ export {
|
|
|
40919
41413
|
renderIssueTemplatesStarterPage,
|
|
40920
41414
|
renderMeetingTypesSection,
|
|
40921
41415
|
renderNextRequirementIdProcedure,
|
|
41416
|
+
renderPhaseTypeInvariantSection,
|
|
41417
|
+
renderPhaseTypeInvariantShellHelpers,
|
|
40922
41418
|
renderPriorityRulesSection,
|
|
40923
41419
|
renderProgressFileName,
|
|
40924
41420
|
renderProgressFilePath,
|
|
@@ -40970,6 +41466,7 @@ export {
|
|
|
40970
41466
|
resolveSkillEvals,
|
|
40971
41467
|
resolveTemplateVariables,
|
|
40972
41468
|
resolveTemporalFraming,
|
|
41469
|
+
resolveTypeLabelForLabels,
|
|
40973
41470
|
resolveTypeScriptProjectOutdir,
|
|
40974
41471
|
resolveUnblockDependents,
|
|
40975
41472
|
runScan,
|
|
@@ -40979,6 +41476,7 @@ export {
|
|
|
40979
41476
|
stripToolArtifactTagsProcedure,
|
|
40980
41477
|
tsdocRecordToFindings,
|
|
40981
41478
|
turborepoBundle,
|
|
41479
|
+
typeLabelForPhaseLabel,
|
|
40982
41480
|
typescriptBundle,
|
|
40983
41481
|
upstreamConfigulatorDocsBundle,
|
|
40984
41482
|
validateAgentTierConfig,
|