@event4u/agent-config 14.13.0 → 14.14.0

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.
Files changed (101) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +116 -387
  5. package/dist/agent-src/commands/analyze/inbox.md +36 -0
  6. package/dist/agent-src/rules/code-comment-discipline.md +6 -0
  7. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +6 -6
  8. package/dist/agent-src/scripts/guarded_baseline.ts +22 -7
  9. package/dist/agent-src/scripts/stubs_due.ts +118 -2
  10. package/dist/agent-src/scripts/update_roadmap_progress.ts +8 -8
  11. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  12. package/dist/cli-delegate/cmd_conformance.js +38 -1
  13. package/dist/discovery/deprecation-report.md +1 -1
  14. package/dist/discovery/discovery-manifest.json +4 -4
  15. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  16. package/dist/discovery/discovery-manifest.summary.md +1 -1
  17. package/dist/discovery/dormancy-report.md +1 -1
  18. package/dist/discovery/orphan-report.md +1 -1
  19. package/dist/discovery/packs.json +2 -2
  20. package/dist/discovery/trust-report.md +1 -1
  21. package/dist/discovery/workspaces.json +2 -2
  22. package/dist/hooks/dispatch.js +1438 -711
  23. package/dist/mcp/registry-manifest.json +2 -2
  24. package/dist/mcp/server.json +2 -2
  25. package/dist/mcp/server.mjs +112 -17
  26. package/docs/guidelines/agent-infra/guarded-baseline.md +1 -1
  27. package/docs/guidelines/code-clarity.md +18 -0
  28. package/package.json +1 -1
  29. package/src/agent-src/scripts/archive_completed_roadmaps.ts +6 -6
  30. package/src/agent-src/scripts/guarded_baseline.ts +22 -7
  31. package/src/agent-src/scripts/stubs_due.ts +118 -2
  32. package/src/agent-src/scripts/update_roadmap_progress.ts +8 -8
  33. package/src/config/estate-count-budget.json +20 -1
  34. package/src/config/gate-coverage.yml +150 -0
  35. package/src/config/gate-violation-baselines.json +8 -3
  36. package/src/config/preamble-payload-budget.json +19 -11
  37. package/src/config/rule-stub-ceilings.json +14 -7
  38. package/src/scripts/_dispatch.bash +0 -4
  39. package/src/scripts/_lib/activation_ladder.ts +47 -3
  40. package/src/scripts/_lib/activation_receipt_producer.ts +341 -0
  41. package/src/scripts/_lib/adr_frontmatter.ts +26 -0
  42. package/src/scripts/_lib/candidate_pair_delta.ts +349 -0
  43. package/src/scripts/_lib/carrier_status.ts +118 -0
  44. package/src/scripts/_lib/cascade_stage_enumeration.ts +92 -0
  45. package/src/scripts/_lib/corpus_manifest.ts +726 -0
  46. package/src/scripts/_lib/delivery_arm_experiment.ts +18 -2
  47. package/src/scripts/_lib/estate_carrier_cases.ts +72 -0
  48. package/src/scripts/_lib/evaluation_cascade.ts +452 -0
  49. package/src/scripts/_lib/evolution_roi.ts +562 -0
  50. package/src/scripts/_lib/harness_evolution_guards.ts +10 -0
  51. package/src/scripts/_lib/lexical_shortlist.ts +215 -0
  52. package/src/scripts/_lib/llm_candidate_proposer.ts +461 -0
  53. package/src/scripts/_lib/llm_proposer_transport.ts +153 -0
  54. package/src/scripts/_lib/pathology_archive.ts +393 -0
  55. package/src/scripts/_lib/promotion_capability.ts +315 -0
  56. package/src/scripts/_lib/promotion_evidence.ts +414 -0
  57. package/src/scripts/_lib/promotion_review.ts +294 -0
  58. package/src/scripts/_lib/prompt_shape.ts +92 -0
  59. package/src/scripts/_lib/release_highlights.ts +70 -1
  60. package/src/scripts/_lib/release_material.ts +125 -2
  61. package/src/scripts/_lib/routing_corpus.ts +307 -0
  62. package/src/scripts/_lib/routing_index_input.ts +104 -0
  63. package/src/scripts/_lib/rule_injection.ts +22 -1
  64. package/src/scripts/_lib/semantic_noop.ts +167 -0
  65. package/src/scripts/activation_receipt.ts +258 -0
  66. package/src/scripts/ai_council/cross_exam.ts +128 -0
  67. package/src/scripts/ai_council/explain_route.ts +157 -0
  68. package/src/scripts/ai_council/information_gain.ts +293 -0
  69. package/src/scripts/ai_council/leakage_corpus.ts +385 -0
  70. package/src/scripts/ai_council/leakage_patterns.ts +453 -0
  71. package/src/scripts/ai_council/minority_retention.ts +171 -0
  72. package/src/scripts/ai_council/recouncil_savings.ts +299 -0
  73. package/src/scripts/ai_council/replay_route.ts +223 -0
  74. package/src/scripts/ai_council/reviewer_assignment.ts +279 -0
  75. package/src/scripts/ai_council/routing_training_row.ts +197 -0
  76. package/src/scripts/ai_council/synthesis_strategy.ts +159 -0
  77. package/src/scripts/check_adr_frontmatter.ts +4 -9
  78. package/src/scripts/check_estate_count.ts +205 -24
  79. package/src/scripts/check_release_highlights.ts +96 -13
  80. package/src/scripts/check_roadmap_trackable.ts +17 -2
  81. package/src/scripts/corpus_manifest.ts +235 -0
  82. package/src/scripts/evolution_lab.ts +181 -8
  83. package/src/scripts/hook_manifest.json +1 -1
  84. package/src/scripts/hook_manifest.yaml +28 -6
  85. package/src/scripts/hooks/comment_discipline_hook.ts +146 -0
  86. package/src/scripts/hooks/concern_registry.ts +2 -0
  87. package/src/scripts/hooks/turn_end_gate_hook.ts +9 -1
  88. package/src/scripts/language_mirror_hook.ts +14 -2
  89. package/src/scripts/lint_carrier_integrity.ts +820 -0
  90. package/src/scripts/lint_code_comments.ts +643 -0
  91. package/src/scripts/lint_plan_risk_register.ts +39 -19
  92. package/src/scripts/lint_promotion_paths.ts +779 -0
  93. package/src/scripts/llm_propose.ts +256 -0
  94. package/src/scripts/measure_delivery_sets.ts +361 -0
  95. package/src/scripts/measure_routing_signal.ts +307 -0
  96. package/src/scripts/model_rule_injection.ts +19 -3
  97. package/src/scripts/probe_council_retention.ts +495 -0
  98. package/src/scripts/release.ts +21 -37
  99. package/src/scripts/release_drill.ts +143 -0
  100. package/src/scripts/release_env.ts +35 -0
  101. package/src/scripts/release_publication.ts +84 -0
@@ -3,13 +3,13 @@
3
3
  "artefact_count": 623,
4
4
  "scanner_version": "de0a22b4ac4c"
5
5
  },
6
- "generated_at": "2026-08-31",
6
+ "generated_at": "2026-09-02",
7
7
  "package": {
8
8
  "description": "Governed skills, rules and commands for AI coding tools (Claude Code, Cursor, Windsurf, Copilot) \u2014 every claim machine-checked, including the counts in these badges.",
9
9
  "homepage": "https://event4u-app.github.io/agent-config",
10
10
  "name": "@event4u/agent-config",
11
11
  "repository": "https://github.com/event4u-app/agent-config",
12
- "version": "14.13.0"
12
+ "version": "14.14.0"
13
13
  },
14
14
  "registries": [
15
15
  {
@@ -9,12 +9,12 @@
9
9
  "transport": {
10
10
  "type": "stdio"
11
11
  },
12
- "version": "14.13.0"
12
+ "version": "14.14.0"
13
13
  }
14
14
  ],
15
15
  "repository": {
16
16
  "source": "github",
17
17
  "url": "https://github.com/event4u-app/agent-config"
18
18
  },
19
- "version": "14.13.0"
19
+ "version": "14.14.0"
20
20
  }
@@ -18969,6 +18969,11 @@ var init_dashboard_mode = __esm({
18969
18969
  import * as fs23 from "node:fs";
18970
18970
  import * as path22 from "node:path";
18971
18971
  import { fileURLToPath as fileURLToPath17, pathToFileURL as pathToFileURL9 } from "node:url";
18972
+ function is_blocked_quickwin(design_validated, capability_gap, blocker_class2) {
18973
+ if (design_validated === null || design_validated === "") return false;
18974
+ if (capability_gap !== "none") return false;
18975
+ return QUICKWIN_BLOCKER_CLASSES.includes(blocker_class2 ?? "");
18976
+ }
18972
18977
  function _isDir8(p) {
18973
18978
  try {
18974
18979
  return fs23.statSync(p).isDirectory();
@@ -19063,9 +19068,27 @@ function _parseIso(s) {
19063
19068
  return Number.isNaN(d.getTime()) ? null : d;
19064
19069
  }
19065
19070
  function scan(root, today) {
19066
- return scan_dir(path22.join(root, STUB_DIR), today);
19071
+ return scan_dir(path22.join(root, STUB_DIR), today, release_dates(root));
19072
+ }
19073
+ function release_dates(root) {
19074
+ const out = [];
19075
+ for (const rel of CHANGELOG_ERAS) {
19076
+ let text;
19077
+ try {
19078
+ text = fs23.readFileSync(path22.join(root, rel), "utf8");
19079
+ } catch {
19080
+ continue;
19081
+ }
19082
+ for (const m of text.matchAll(RELEASE_HEADING)) {
19083
+ if (m[1] !== void 0) out.push(m[1]);
19084
+ }
19085
+ }
19086
+ return out;
19067
19087
  }
19068
- function scan_dir(base, today) {
19088
+ function releases_since(opened, dates) {
19089
+ return dates.filter((d) => d > opened).length;
19090
+ }
19091
+ function scan_dir(base, today, releaseDates = []) {
19069
19092
  if (!_isDir8(base)) return [];
19070
19093
  const now = _parseIso(today);
19071
19094
  const out = [];
@@ -19086,6 +19109,10 @@ function scan_dir(base, today) {
19086
19109
  const review_by = _scalar(front, "review_by");
19087
19110
  const reviewed_at = _scalar(front, "reviewed_at");
19088
19111
  const probeVal = _scalar(front, "probe");
19112
+ const design_validated = _scalar(front, "design_validated");
19113
+ const capability_gap = _scalar(front, "capability_gap");
19114
+ const blocker_class2 = _scalar(front, "blocker_class");
19115
+ const blocker_opened = _scalar(front, "blocker_opened");
19089
19116
  const shape = text.includes(TRANSFER_MARKER) ? "transfer" : "orgmode";
19090
19117
  let overdue = false;
19091
19118
  let days_overdue = null;
@@ -19105,7 +19132,13 @@ function scan_dir(base, today) {
19105
19132
  has_probe_heading: /^#{1,4} *(Probe|Promotion)/m.test(text),
19106
19133
  owner_decisions: count_owner_decisions(text),
19107
19134
  overdue,
19108
- days_overdue
19135
+ days_overdue,
19136
+ design_validated,
19137
+ capability_gap,
19138
+ blocker_class: blocker_class2,
19139
+ blocker_opened,
19140
+ releases_since_blocked: blocker_opened === null ? null : releases_since(blocker_opened, releaseDates),
19141
+ blocked_quickwin: is_blocked_quickwin(design_validated, capability_gap, blocker_class2)
19109
19142
  });
19110
19143
  }
19111
19144
  return out;
@@ -19115,6 +19148,7 @@ function counts(records) {
19115
19148
  overdue: records.filter((r) => r.overdue).length,
19116
19149
  owner_decisions: records.reduce((s, r) => s + r.owner_decisions, 0),
19117
19150
  missing_review_by: records.filter((r) => r.review_by === null).length,
19151
+ blocked_quickwin: records.filter((r) => r.blocked_quickwin).length,
19118
19152
  total: records.length
19119
19153
  };
19120
19154
  }
@@ -19178,6 +19212,20 @@ function main9(argv = process.argv.slice(2)) {
19178
19212
  process.stdout.write(":\n");
19179
19213
  for (const r of withDecisions) {
19180
19214
  process.stdout.write(` ${r.file} (${r.owner_decisions})
19215
+ `);
19216
+ }
19217
+ }
19218
+ const quickwins = records.filter((r) => r.blocked_quickwin);
19219
+ if (quickwins.length > 0) {
19220
+ process.stdout.write(
19221
+ `
19222
+ \u{1F513} ${quickwins.length} BLOCKED QUICK-WIN(S) \u2014 design validated, no capability gap,
19223
+ held by an estate or budget decision rather than a product one:
19224
+ `
19225
+ );
19226
+ for (const r of quickwins) {
19227
+ const since = r.releases_since_blocked === null ? "no `blocker_opened:`" : `${String(r.releases_since_blocked)} release(s) since ${r.blocker_opened}`;
19228
+ process.stdout.write(` ${r.file} (${r.blocker_class}, ${since})
19181
19229
  `);
19182
19230
  }
19183
19231
  }
@@ -19227,7 +19275,7 @@ function headerFragment(stubsDir, today) {
19227
19275
  }
19228
19276
  return ` \xB7 **${overdue}** overdue stub${overdue !== 1 ? "s" : ""}, **${decisions}** owner decision${decisions !== 1 ? "s" : ""} \u2192 \`agent-config stubs:due\``;
19229
19277
  }
19230
- var STUB_DIR, CADENCE_DAYS, TRANSFER_MARKER, _HERE6;
19278
+ var STUB_DIR, CADENCE_DAYS, TRANSFER_MARKER, _HERE6, QUICKWIN_BLOCKER_CLASSES, CHANGELOG_ERAS, RELEASE_HEADING;
19231
19279
  var init_stubs_due = __esm({
19232
19280
  "src/agent-src/scripts/stubs_due.ts"() {
19233
19281
  "use strict";
@@ -19235,6 +19283,9 @@ var init_stubs_due = __esm({
19235
19283
  CADENCE_DAYS = { transfer: 30, orgmode: 120 };
19236
19284
  TRANSFER_MARKER = "drain-run transfer";
19237
19285
  _HERE6 = path22.resolve(fileURLToPath17(import.meta.url));
19286
+ QUICKWIN_BLOCKER_CLASSES = ["estate", "budget"];
19287
+ CHANGELOG_ERAS = ["CHANGELOG.md", "docs/archive/CHANGELOG-pre-14.12.0.md"];
19288
+ RELEASE_HEADING = /^## \[\d+\.\d+\.\d+\][^\n]*\((\d{4}-\d{2}-\d{2})\)\s*$/gm;
19238
19289
  if (_isCliEntry9() || process.argv[1] === _HERE6) {
19239
19290
  process.exitCode = main9();
19240
19291
  }
@@ -19348,15 +19399,21 @@ function guardedBaselineStaleness(items, repo_root2) {
19348
19399
  if (trigger === "") {
19349
19400
  continue;
19350
19401
  }
19351
- for (const token of trigger.split(/[\s,]+/).filter((t) => t !== "")) {
19352
- if (!token.includes("/")) {
19353
- unverifiable.push(`line ${it.line} (${it.label}): recheck_when \`${token}\``);
19354
- } else if (fs24.existsSync(path23.join(repo_root2, token))) {
19402
+ const tokens = trigger.split(/[\s,]+/).filter((t) => t !== "");
19403
+ const paths = tokens.filter((t) => t.includes("/"));
19404
+ for (const token of paths) {
19405
+ if (fs24.existsSync(path23.join(repo_root2, token))) {
19355
19406
  stale.push(
19356
19407
  `line ${it.line} (${it.label}): recheck_when \`${token}\` now exists \u2014 re-verify against the real mechanism`
19357
19408
  );
19358
19409
  }
19359
19410
  }
19411
+ if (paths.length > 0) {
19412
+ continue;
19413
+ }
19414
+ for (const token of tokens) {
19415
+ unverifiable.push(`line ${it.line} (${it.label}): recheck_when \`${token}\``);
19416
+ }
19360
19417
  }
19361
19418
  return { stale, unverifiable };
19362
19419
  }
@@ -19435,8 +19492,8 @@ __export(update_roadmap_progress_exports, {
19435
19492
  collect: () => collect,
19436
19493
  collect_bundles: () => collect_bundles,
19437
19494
  count_checkboxes: () => count_checkboxes,
19438
- is_draft: () => is_draft,
19439
19495
  is_roadmap_candidate: () => is_roadmap_candidate,
19496
+ is_unscheduled: () => is_unscheduled,
19440
19497
  main: () => main10,
19441
19498
  merge_gated_pending: () => merge_gated_pending,
19442
19499
  parse_blockers: () => parse_blockers,
@@ -19492,8 +19549,8 @@ function _stripQuotes2(s) {
19492
19549
  out = out.replace(/^'+/, "").replace(/'+$/, "");
19493
19550
  return out;
19494
19551
  }
19495
- function is_draft(fm) {
19496
- return DRAFT_VALUES.has((fm["status"] ?? "").toLowerCase());
19552
+ function is_unscheduled(fm) {
19553
+ return UNSCHEDULED_VALUES.has((fm["status"] ?? "").toLowerCase());
19497
19554
  }
19498
19555
  function is_roadmap_name(name) {
19499
19556
  if (EXCLUDE_NAMES.has(name)) {
@@ -19785,7 +19842,7 @@ function collect(roadmap_root) {
19785
19842
  continue;
19786
19843
  }
19787
19844
  const text = fs25.readFileSync(p, { encoding: "utf-8" });
19788
- if (is_draft(parse_frontmatter2(text))) {
19845
+ if (is_unscheduled(parse_frontmatter2(text))) {
19789
19846
  continue;
19790
19847
  }
19791
19848
  const stats = parse_roadmap(p, roadmap_root);
@@ -19815,7 +19872,7 @@ function collect_parked(roadmap_root) {
19815
19872
  } catch {
19816
19873
  continue;
19817
19874
  }
19818
- if (is_draft(parse_frontmatter2(text))) {
19875
+ if (is_unscheduled(parse_frontmatter2(text))) {
19819
19876
  continue;
19820
19877
  }
19821
19878
  const cell = resume_cell(text);
@@ -20270,7 +20327,7 @@ function _isCliEntry10() {
20270
20327
  return false;
20271
20328
  }
20272
20329
  }
20273
- var _HERE7, _require3, CHECKBOX_RE, PHASE_RE, TITLE_RE, EXCLUDE_NAMES, EXCLUDE_PREFIXES, EXCLUDE_DIRS, FRONTMATTER_RE, DRAFT_VALUES, MERGE_GATED_RE, PR_NUM_RE, PhaseStats, RoadmapStats, FENCED_CODE_RE, BLOCKERS_SECTION_RE, BLOCKER_HEADING_RE, NEXT_H2_RE, LEGACY_BLOCKED_UNTIL_RE, BLOCKER_FIELD_RE, RESUME_STATEMENT, RESUME_LOOSE, ArgparseExit, _PROG5;
20330
+ var _HERE7, _require3, CHECKBOX_RE, PHASE_RE, TITLE_RE, EXCLUDE_NAMES, EXCLUDE_PREFIXES, EXCLUDE_DIRS, FRONTMATTER_RE, UNSCHEDULED_VALUES, MERGE_GATED_RE, PR_NUM_RE, PhaseStats, RoadmapStats, FENCED_CODE_RE, BLOCKERS_SECTION_RE, BLOCKER_HEADING_RE, NEXT_H2_RE, LEGACY_BLOCKED_UNTIL_RE, BLOCKER_FIELD_RE, RESUME_STATEMENT, RESUME_LOOSE, ArgparseExit, _PROG5;
20274
20331
  var init_update_roadmap_progress = __esm({
20275
20332
  "src/agent-src/scripts/update_roadmap_progress.ts"() {
20276
20333
  "use strict";
@@ -20292,7 +20349,7 @@ var init_update_roadmap_progress = __esm({
20292
20349
  EXCLUDE_PREFIXES = ["open-questions"];
20293
20350
  EXCLUDE_DIRS = /* @__PURE__ */ new Set(["archive", "skipped", "stubs", "later"]);
20294
20351
  FRONTMATTER_RE = /^---\n([\s\S]*?)\n---[ \t\n\r\f\v]*\n/;
20295
- DRAFT_VALUES = /* @__PURE__ */ new Set(["draft"]);
20352
+ UNSCHEDULED_VALUES = /* @__PURE__ */ new Set(["draft", "carrier"]);
20296
20353
  MERGE_GATED_RE = /merge-gated/i;
20297
20354
  PR_NUM_RE = /pr[ \t\n\r\f\v]*[=#:]?[ \t\n\r\f\v]*#?[ \t\n\r\f\v]*(\d+)/gi;
20298
20355
  PhaseStats = class {
@@ -20432,6 +20489,7 @@ __export(archive_completed_roadmaps_exports, {
20432
20489
  _git_mv: () => _git_mv,
20433
20490
  _inbound_ref_rewrite: () => _inbound_ref_rewrite,
20434
20491
  _is_frozen_record: () => _is_frozen_record,
20492
+ _relatesTo: () => _relatesTo,
20435
20493
  _repo_root: () => _repo_root,
20436
20494
  archive_completed: () => archive_completed,
20437
20495
  deferralProblems: () => deferralProblems,
@@ -36552,11 +36610,48 @@ function humanAuthoredLead(text) {
36552
36610
  }
36553
36611
  return kept.join("\n");
36554
36612
  }
36555
- var DOCUMENT_HEAD;
36613
+ function stripInjectedRegions(text) {
36614
+ const lines = text.split("\n");
36615
+ const kept = [];
36616
+ for (let i = 0; i < lines.length; i++) {
36617
+ const line = lines[i] ?? "";
36618
+ const open = REGION_OPEN.exec(line);
36619
+ if (open === null) {
36620
+ kept.push(line);
36621
+ continue;
36622
+ }
36623
+ const close = new RegExp(`^\\s*</${open[1] ?? ""}>\\s*$`);
36624
+ let end = -1;
36625
+ for (let j = i + 1; j < lines.length; j++) {
36626
+ if (close.test(lines[j] ?? "")) {
36627
+ end = j;
36628
+ break;
36629
+ }
36630
+ }
36631
+ if (end === -1) {
36632
+ kept.push(line);
36633
+ continue;
36634
+ }
36635
+ if (kept.length > 0 && REGION_NOTE.test(kept[kept.length - 1] ?? "")) {
36636
+ kept.pop();
36637
+ }
36638
+ i = end;
36639
+ while (i + 1 < lines.length && (lines[i + 1] ?? "").trim() !== "") {
36640
+ if (!REGION_NOTE.test(lines[i + 1] ?? "")) {
36641
+ break;
36642
+ }
36643
+ i++;
36644
+ }
36645
+ }
36646
+ return kept.join("\n");
36647
+ }
36648
+ var DOCUMENT_HEAD, REGION_OPEN, REGION_NOTE;
36556
36649
  var init_prompt_shape = __esm({
36557
36650
  "src/scripts/_lib/prompt_shape.ts"() {
36558
36651
  "use strict";
36559
36652
  DOCUMENT_HEAD = /^\s*(#{1,6}\s|-{3,}\s*$|={3,}\s*$|<[A-Za-z][\w-]*>\s*$)/;
36653
+ REGION_OPEN = /^\s*<([A-Za-z][\w-]*)>\s*$/;
36654
+ REGION_NOTE = /\b(content (above|below) is from|treat (it|this|that|the (content|text|above|following)) as data)\b/i;
36560
36655
  }
36561
36656
  });
36562
36657
 
@@ -36599,7 +36694,7 @@ function instructionText(prompt) {
36599
36694
  return kept.join("\n");
36600
36695
  }
36601
36696
  function classify(prompt) {
36602
- const text = instructionText(prompt);
36697
+ const text = instructionText(stripInjectedRegions(prompt));
36603
36698
  const lead = humanAuthoredLead(text);
36604
36699
  const leadVerdict = _score2(lead);
36605
36700
  if (leadVerdict.language !== "und") {
@@ -34,6 +34,6 @@ Tooling, all of it in `guarded_baseline.ts` and enforced by both consumers:
34
34
  | Reported **separately**: a `## 🛡️ Guarded baselines` dashboard section plus a per-step stderr line | `update_roadmap_progress` |
35
35
  | **Rejected** — exit 1, on both `--check` and a plain regen — when `red_proof` is absent, when `category` is absent or illegal, when there is no evidence block, or when the annotation sits on anything but `- [ ]` | `update_roadmap_progress` |
36
36
  | Treated as **incomplete**: archival refused and the reason named | `archive_completed_roadmaps` |
37
- | Marked **stale** once a path-shaped `recheck_when` trigger resolves in the tree; a bare symbol trigger is reported as not machine-checkable rather than as not-stale | `guardedBaselineStaleness` |
37
+ | Marked **stale** once a path-shaped `recheck_when` trigger resolves in the tree. A trigger carrying **no** path token at all is reported as not machine-checkable rather than as not-stale; a trigger carrying at least one path token is decided by that path and is **not** reported, even when a companion symbol token sits beside it — a checked trigger that looks unchecked is the mirror of the failure the report exists to prevent (measured 2026-09-01: 3 of 4 reported lines were already decidable) | `guardedBaselineStaleness` |
38
38
 
39
39
  Only verification against the real mechanism permits `[x]`. **A baseline that has not demonstrably gone RED is an ordinary open item** and must not carry the annotation at all — the sabotage-and-restore proof is the entry price, not a nice-to-have.
@@ -315,6 +315,24 @@ Behavioral baseline: `tests/code-comments/eval-fixtures.md`
315
315
  `ccd-untouched-preservation`, `ccd-explicit-teaching-override`); the decidable
316
316
  criteria are proven by `tests/scripts/code_comment_fixtures.test.ts`.
317
317
 
318
+ ### Evidence belongs in the roadmap that asked for it
319
+
320
+ Measured 2026-09-02 in a consumer repository: **509 German comment lines** and
321
+ **21 report- or provenance-shaped comments** across **41 of 45** changed source
322
+ files, the worst a 101-line colour-analysis report inside a CSS file. The cause
323
+ was a roadmap requiring proof for every checked step: the run read that as
324
+ *evidence should be documented* and wrote the measurements into the source too,
325
+ where the roadmap already carried them.
326
+
327
+ The provenance of a value is not a WHY, and a roadmap cannot license a comment
328
+ the rule forbids.
329
+
330
+ Two enforcers carry this because neither rule could fire on a write:
331
+ `code-comment-discipline` matches prompt keywords, and `language-and-tone`'s
332
+ English-comment clause named a `.md`-only validator. `lint_code_comments`
333
+ refuses on the diff; the `comment-discipline` concern reports on the text as
334
+ written. Neither replaces the discipline.
335
+
318
336
  ## See also
319
337
 
320
338
  - Language-specific anchors that link to this guideline:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@event4u/agent-config",
3
3
  "mcpName": "io.github.event4u-app/agent-config",
4
- "version": "14.13.0",
4
+ "version": "14.14.0",
5
5
  "description": "Governed skills, rules and commands for AI coding tools (Claude Code, Cursor, Windsurf, Copilot) — every claim machine-checked, including the counts in these badges.",
6
6
  "license": "MIT",
7
7
  "private": false,
@@ -12,8 +12,8 @@
12
12
  * `urp.collect()` completion criterion, and exit code 0. snake_case kept.
13
13
  *
14
14
  * A roadmap that has reached `count_open == 0` and `count_deferred == 0` is
15
- * **complete**. This sweep moves it to `agents/roadmaps/archive/`, rewrites
16
- * inbound references (`agents/roadmaps/<x>.md` → `agents/roadmaps/archive/<x>.md`)
15
+ * **complete**. This sweep moves it to `agents/roadmaps/archive/`, rewrites code-comment-allow provenance-comment -- the sweep's own operand
16
+ * inbound references (`agents/roadmaps/<x>.md` → `agents/roadmaps/archive/<x>.md`) code-comment-allow provenance-comment -- the rewrite this sweep performs
17
17
  * across tracked files so links never break, and regenerates the dashboard.
18
18
  *
19
19
  * It replaces the old **merge-gate** (keep one item open + a manual post-merge
@@ -136,7 +136,7 @@ function _branch_touched_paths(root: string, base: string): Set<string> | null {
136
136
  *
137
137
  * Two classes, and the second is why this predicate exists at all:
138
138
  *
139
- * - `agents/evidence/**` — a committed record of what was true, or of what a
139
+ * - `agents/evidence/**` — a committed record of what was true, or of what a code-comment-allow provenance-comment -- the predicate's operand set
140
140
  * reviewer was shown, at one point in time. Retroactively editing it makes it
141
141
  * claim something that did not happen, which is the failure
142
142
  * `evaluator-independence` exists to prevent ("the prompt ships with the
@@ -146,7 +146,7 @@ function _branch_touched_paths(root: string, base: string): Set<string> | null {
146
146
  * AND makes `git apply` target a path the diff never created.
147
147
  *
148
148
  * Measured 2026-08-11: archiving one roadmap rewrote four frozen artifacts
149
- * under `agents/evidence/reviews/`, including the `diff.patch` headers of a
149
+ * under `agents/evidence/reviews/`, including the `diff.patch` headers of a code-comment-allow provenance-comment -- names what the predicate protects
150
150
  * recorded review input. Nothing caught it — the sweep reported it as
151
151
  * "4 ref(s) migrated", which reads like success.
152
152
  */
@@ -346,7 +346,7 @@ function _escapeRe(v: string): string {
346
346
  * bare `- <name>` item inside the `relates:` block, which is a declaration the
347
347
  * author made deliberately. A mention anywhere else in the file does not count.
348
348
  */
349
- function _relatesTo(text: string, slug: string): boolean {
349
+ export function _relatesTo(text: string, slug: string): boolean {
350
350
  const lines = text.split('\n');
351
351
  const start = lines.findIndex((l) => /^relates:/.test(l));
352
352
  if (start === -1) return false;
@@ -654,7 +654,7 @@ function _regen_dashboard(root: string, dry_run: boolean): void {
654
654
  /**
655
655
  * Rebuild the archive index after a move, when the builder is present.
656
656
  *
657
- * The sweep is the one operation that changes what `agents/roadmaps/archive/`
657
+ * The sweep is the one operation that changes what `agents/roadmaps/archive/` code-comment-allow provenance-comment -- the directory this function writes
658
658
  * contains, so it is the one operation that can leave the generated index
659
659
  * stale — and the drift gate on that index runs in the same PR the sweep runs
660
660
  * in. Regenerating here keeps the two from disagreeing by construction rather
@@ -226,9 +226,18 @@ export function guardedBaselineProblems(items: readonly GuardedBaselineItem[]):
226
226
  *
227
227
  * HONEST SCOPE. Only a PATH trigger is machine-checkable: a token containing `/`
228
228
  * is resolved against the repo root and stale iff it exists. A bare symbol name
229
- * cannot be decided from a path, so it is returned as `unverifiable` rather than
230
- * silently reading as "not stale" — an unchecked trigger that looks checked is
231
- * the failure this whole state exists to avoid.
229
+ * cannot be decided from a path, so a record carrying ONLY symbol tokens is
230
+ * returned as `unverifiable` rather than silently reading as "not stale" — an
231
+ * unchecked trigger that looks checked is the failure this whole state exists
232
+ * to avoid.
233
+ *
234
+ * A record whose trigger carries AT LEAST ONE path token is decided by that
235
+ * path and is NOT reported as unverifiable, even when a companion symbol token
236
+ * sits beside it. Reporting it would be the mirror of the failure above: a
237
+ * checked trigger that looks unchecked. Measured 2026-09-01 — three of the four
238
+ * lines the dashboard printed as "not machine-checkable" carried a path token
239
+ * and were therefore already decidable, which is noise that trains a reader to
240
+ * skip the section where the one genuinely undecidable trigger also lives.
232
241
  */
233
242
  export function guardedBaselineStaleness(
234
243
  items: readonly GuardedBaselineItem[],
@@ -241,16 +250,22 @@ export function guardedBaselineStaleness(
241
250
  if (trigger === '') {
242
251
  continue;
243
252
  }
244
- for (const token of trigger.split(/[\s,]+/).filter((t) => t !== '')) {
245
- if (!token.includes('/')) {
246
- unverifiable.push(`line ${it.line} (${it.label}): recheck_when \`${token}\``);
247
- } else if (fs.existsSync(path.join(repo_root, token))) {
253
+ const tokens = trigger.split(/[\s,]+/).filter((t) => t !== '');
254
+ const paths = tokens.filter((t) => t.includes('/'));
255
+ for (const token of paths) {
256
+ if (fs.existsSync(path.join(repo_root, token))) {
248
257
  stale.push(
249
258
  `line ${it.line} (${it.label}): recheck_when \`${token}\` now exists — ` +
250
259
  're-verify against the real mechanism',
251
260
  );
252
261
  }
253
262
  }
263
+ if (paths.length > 0) {
264
+ continue;
265
+ }
266
+ for (const token of tokens) {
267
+ unverifiable.push(`line ${it.line} (${it.label}): recheck_when \`${token}\``);
268
+ }
254
269
  }
255
270
  return { stale, unverifiable };
256
271
  }
@@ -66,6 +66,48 @@ export interface StubRecord {
66
66
  owner_decisions: number;
67
67
  overdue: boolean;
68
68
  days_overdue: number | null;
69
+ /** Frontmatter `design_validated:` — a citation, or null when absent. */
70
+ design_validated: string | null;
71
+ /** Frontmatter `capability_gap:` — `none` means the stub declares no gap. */
72
+ capability_gap: string | null;
73
+ /** Frontmatter `blocker_class:` — `estate` | `budget` | `product`. */
74
+ blocker_class: string | null;
75
+ /** Frontmatter `blocker_opened:` — ISO date the hold began, or null. */
76
+ blocker_opened: string | null;
77
+ /** Releases published since `blocker_opened`, or null when it is absent. */
78
+ releases_since_blocked: number | null;
79
+ /** All three membership conditions hold. See {@link BLOCKED_QUICKWIN_FIELDS}. */
80
+ blocked_quickwin: boolean;
81
+ }
82
+
83
+ /**
84
+ * The three frontmatter keys that decide fourth-bucket membership.
85
+ *
86
+ * Read as SCALARS, never as substrings of the body. A substring match over
87
+ * prose is what put a validated fix and a waiting preference into one bucket
88
+ * in the first place; doing it with three substrings instead of one would be
89
+ * worse, not better. A stub missing any of the three is not a member and falls
90
+ * through to the OWNER bucket exactly as before — absence is never a member.
91
+ */
92
+ export const BLOCKED_QUICKWIN_FIELDS = ['design_validated', 'capability_gap', 'blocker_class'] as const;
93
+
94
+ /** Blocker classes that make a stub a BLOCKED QUICK-WIN rather than a product wait. */
95
+ export const QUICKWIN_BLOCKER_CLASSES = ['estate', 'budget'] as const;
96
+
97
+ /**
98
+ * All three conditions, read from frontmatter only.
99
+ *
100
+ * A recorded design validation, an explicit `capability_gap: none`, and an open
101
+ * blocker that is an estate or budget decision rather than a product one.
102
+ */
103
+ export function is_blocked_quickwin(
104
+ design_validated: string | null,
105
+ capability_gap: string | null,
106
+ blocker_class: string | null,
107
+ ): boolean {
108
+ if (design_validated === null || design_validated === '') return false;
109
+ if (capability_gap !== 'none') return false;
110
+ return (QUICKWIN_BLOCKER_CLASSES as readonly string[]).includes(blocker_class ?? '');
69
111
  }
70
112
 
71
113
  function _isDir(p: string): boolean {
@@ -188,7 +230,48 @@ function _parseIso(s: string): Date | null {
188
230
 
189
231
  /** Read every stub and classify it. `today` is injectable so tests are stable. */
190
232
  export function scan(root: string, today: string): StubRecord[] {
191
- return scan_dir(path.join(root, STUB_DIR), today);
233
+ return scan_dir(path.join(root, STUB_DIR), today, release_dates(root));
234
+ }
235
+
236
+ /**
237
+ * Changelog files that carry released sections, newest era first.
238
+ *
239
+ * BOTH are read, and that is the whole point of the pair. The era split moved
240
+ * older releases into the archive, so a count taken over the current file alone
241
+ * silently reports zero for anything before the split — measured: 14.11.0 lives
242
+ * only in the archive, and grepping `CHANGELOG.md` for it returns nothing.
243
+ */
244
+ export const CHANGELOG_ERAS = ['CHANGELOG.md', 'docs/archive/CHANGELOG-pre-14.12.0.md'] as const;
245
+
246
+ /** `## [x.y.z](…) (YYYY-MM-DD)` — a released section heading with its date. */
247
+ const RELEASE_HEADING = /^## \[\d+\.\d+\.\d+\][^\n]*\((\d{4}-\d{2}-\d{2})\)\s*$/gm;
248
+
249
+ /** Every released section's date across both eras, unsorted. */
250
+ export function release_dates(root: string): string[] {
251
+ const out: string[] = [];
252
+ for (const rel of CHANGELOG_ERAS) {
253
+ let text: string;
254
+ try {
255
+ text = fs.readFileSync(path.join(root, rel), 'utf8');
256
+ } catch {
257
+ continue;
258
+ }
259
+ for (const m of text.matchAll(RELEASE_HEADING)) {
260
+ if (m[1] !== undefined) out.push(m[1]);
261
+ }
262
+ }
263
+ return out;
264
+ }
265
+
266
+ /**
267
+ * Releases published strictly AFTER the day the hold began.
268
+ *
269
+ * Strictly after, because a release cut on the same day cannot have carried a
270
+ * fix the hold was blocking — counting it would inflate the figure the
271
+ * council's falsifier is read against.
272
+ */
273
+ export function releases_since(opened: string, dates: readonly string[]): number {
274
+ return dates.filter((d) => d > opened).length;
192
275
  }
193
276
 
194
277
  /**
@@ -198,7 +281,11 @@ export function scan(root: string, today: string): StubRecord[] {
198
281
  * immediately re-join — a fragile round trip past exactly the directory both
199
282
  * callers care about.
200
283
  */
201
- export function scan_dir(base: string, today: string): StubRecord[] {
284
+ export function scan_dir(
285
+ base: string,
286
+ today: string,
287
+ releaseDates: readonly string[] = [],
288
+ ): StubRecord[] {
202
289
  if (!_isDir(base)) return [];
203
290
  const now = _parseIso(today);
204
291
  const out: StubRecord[] = [];
@@ -222,6 +309,10 @@ export function scan_dir(base: string, today: string): StubRecord[] {
222
309
  const review_by = _scalar(front, 'review_by');
223
310
  const reviewed_at = _scalar(front, 'reviewed_at');
224
311
  const probeVal = _scalar(front, 'probe');
312
+ const design_validated = _scalar(front, 'design_validated');
313
+ const capability_gap = _scalar(front, 'capability_gap');
314
+ const blocker_class = _scalar(front, 'blocker_class');
315
+ const blocker_opened = _scalar(front, 'blocker_opened');
225
316
  const shape: StubShape = text.includes(TRANSFER_MARKER) ? 'transfer' : 'orgmode';
226
317
  let overdue = false;
227
318
  let days_overdue: number | null = null;
@@ -242,6 +333,13 @@ export function scan_dir(base: string, today: string): StubRecord[] {
242
333
  owner_decisions: count_owner_decisions(text),
243
334
  overdue,
244
335
  days_overdue,
336
+ design_validated,
337
+ capability_gap,
338
+ blocker_class,
339
+ blocker_opened,
340
+ releases_since_blocked:
341
+ blocker_opened === null ? null : releases_since(blocker_opened, releaseDates),
342
+ blocked_quickwin: is_blocked_quickwin(design_validated, capability_gap, blocker_class),
245
343
  });
246
344
  }
247
345
  return out;
@@ -251,6 +349,8 @@ export interface Counts {
251
349
  overdue: number;
252
350
  owner_decisions: number;
253
351
  missing_review_by: number;
352
+ /** ADDITIVE 2026-09-01. Existing keys are unchanged in name, type and meaning. */
353
+ blocked_quickwin: number;
254
354
  total: number;
255
355
  }
256
356
 
@@ -259,6 +359,7 @@ export function counts(records: StubRecord[]): Counts {
259
359
  overdue: records.filter((r) => r.overdue).length,
260
360
  owner_decisions: records.reduce((s, r) => s + r.owner_decisions, 0),
261
361
  missing_review_by: records.filter((r) => r.review_by === null).length,
362
+ blocked_quickwin: records.filter((r) => r.blocked_quickwin).length,
262
363
  total: records.length,
263
364
  };
264
365
  }
@@ -328,6 +429,21 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
328
429
  }
329
430
  }
330
431
 
432
+ const quickwins = records.filter((r) => r.blocked_quickwin);
433
+ if (quickwins.length > 0) {
434
+ process.stdout.write(
435
+ `\n 🔓 ${quickwins.length} BLOCKED QUICK-WIN(S) — design validated, no capability gap,\n` +
436
+ ' held by an estate or budget decision rather than a product one:\n',
437
+ );
438
+ for (const r of quickwins) {
439
+ const since =
440
+ r.releases_since_blocked === null
441
+ ? 'no `blocker_opened:`'
442
+ : `${String(r.releases_since_blocked)} release(s) since ${r.blocker_opened}`;
443
+ process.stdout.write(` ${r.file} (${r.blocker_class}, ${since})\n`);
444
+ }
445
+ }
446
+
331
447
  const noProbe = records.filter((r) => !r.probe_none && !r.has_probe_heading);
332
448
  if (noProbe.length > 0) {
333
449
  process.stdout.write(