@event4u/agent-config 14.6.0 → 14.8.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 (258) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +255 -145
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/analyze/inbox.md +264 -10
  5. package/dist/agent-src/commands/analyze.md +1 -1
  6. package/dist/agent-src/commands/evals/git-pr-merge.json +13 -0
  7. package/dist/agent-src/commands/evals/roadmap.json +17 -1
  8. package/dist/agent-src/commands/pr/create.md +14 -2
  9. package/dist/agent-src/commands/pr/merge.md +379 -0
  10. package/dist/agent-src/commands/roadmap/process-full.md +272 -32
  11. package/dist/agent-src/commands/roadmap.md +1 -1
  12. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +46 -36
  13. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +37 -2
  14. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +95 -2
  15. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +289 -106
  16. package/dist/agent-src/rules/artifact-drafting-protocol.md +5 -1
  17. package/dist/agent-src/rules/context-hygiene.md +2 -0
  18. package/dist/agent-src/rules/decision-revisit-gate.md +67 -32
  19. package/dist/agent-src/rules/fix-what-you-see.md +108 -0
  20. package/dist/agent-src/rules/no-roadmap-references.md +0 -5
  21. package/dist/agent-src/rules/recurring-criticism.md +110 -0
  22. package/dist/agent-src/rules/roadmap-progress-sync.md +20 -1
  23. package/dist/agent-src/rules/rule-type-governance.md +0 -2
  24. package/dist/agent-src/rules/self-repair-loop.md +17 -0
  25. package/dist/agent-src/rules/skill-quality.md +0 -2
  26. package/dist/agent-src/rules/source-confidentiality.md +37 -7
  27. package/dist/agent-src/scripts/archival_sweep.ts +117 -0
  28. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +26 -7
  29. package/dist/agent-src/scripts/dashboard_mode.ts +212 -0
  30. package/dist/agent-src/scripts/gate_budget.ts +282 -0
  31. package/dist/agent-src/scripts/gate_execute.ts +72 -31
  32. package/dist/agent-src/scripts/roadmap_gates.ts +74 -3
  33. package/dist/agent-src/scripts/update_roadmap_progress.ts +296 -53
  34. package/dist/agent-src/skills/adr-create/SKILL.md +124 -5
  35. package/dist/agent-src/skills/ai-council/SKILL.md +48 -807
  36. package/dist/agent-src/skills/ai-council/references/advanced-modes.md +320 -0
  37. package/dist/agent-src/skills/ai-council/references/cost-and-redaction.md +99 -0
  38. package/dist/agent-src/skills/ai-council/references/execution-modes.md +162 -0
  39. package/dist/agent-src/skills/ai-council/references/output-and-synthesis.md +125 -0
  40. package/dist/agent-src/skills/ai-council/references/procedure.md +143 -0
  41. package/dist/agent-src/skills/decision-record/SKILL.md +61 -3
  42. package/dist/agent-src/skills/decision-review/SKILL.md +65 -0
  43. package/dist/agent-src/skills/merge-conflicts/SKILL.md +26 -0
  44. package/dist/agent-src/skills/quality-tools/SKILL.md +34 -326
  45. package/dist/agent-src/skills/quality-tools/references/js-ts-tools.md +113 -0
  46. package/dist/agent-src/skills/quality-tools/references/php-tools.md +222 -0
  47. package/dist/agent-src/skills/roadmap-management/SKILL.md +15 -409
  48. package/dist/agent-src/skills/roadmap-management/references/archival.md +226 -0
  49. package/dist/agent-src/skills/roadmap-management/references/authoring.md +107 -0
  50. package/dist/agent-src/skills/roadmap-management/references/dashboard.md +47 -0
  51. package/dist/agent-src/skills/roadmap-management/references/lifecycle.md +58 -0
  52. package/dist/agent-src/skills/security/SKILL.md +8 -0
  53. package/dist/agent-src/skills/security-audit/SKILL.md +12 -1
  54. package/dist/agent-src/skills/skill-writing/SKILL.md +34 -570
  55. package/dist/agent-src/skills/skill-writing/references/procedure.md +275 -0
  56. package/dist/agent-src/skills/skill-writing/references/read-only-scripts.md +18 -0
  57. package/dist/agent-src/skills/skill-writing/references/section-patterns.md +298 -0
  58. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  59. package/dist/agent-src/templates/roadmaps.md +65 -4
  60. package/dist/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
  61. package/dist/agent-src/templates/scripts/telemetry/remote.ts +160 -7
  62. package/dist/agent-src/templates/scripts/telemetry/transport.ts +169 -0
  63. package/dist/cli/registry.js +2 -1
  64. package/dist/cli/registry.js.map +1 -1
  65. package/dist/cli-delegate/chunk-2GNWGMAL.js +172 -0
  66. package/dist/cli-delegate/{chunk-P6LQDIWK.js → chunk-7PRE6ZV2.js} +338 -0
  67. package/dist/cli-delegate/{chunk-BKO2AZ7Z.js → chunk-B2ERU2VA.js} +1 -1
  68. package/dist/cli-delegate/chunk-E4XIZDIB.js +402 -0
  69. package/dist/cli-delegate/{chunk-3UCJWMMP.js → chunk-EEJUMCDZ.js} +16 -16
  70. package/dist/cli-delegate/{chunk-NXFWFAYV.js → chunk-G23AT6GD.js} +1 -0
  71. package/dist/cli-delegate/{chunk-XURJ4AQC.js → chunk-H4VIVD4C.js} +385 -284
  72. package/dist/cli-delegate/chunk-I3EIN2YG.js +36 -0
  73. package/dist/cli-delegate/{chunk-RLN73PR5.js → chunk-LPPWO5CE.js} +12 -8
  74. package/dist/cli-delegate/chunk-MWRD5YOH.js +100 -0
  75. package/dist/cli-delegate/{chunk-UEZ7D35A.js → chunk-N7CDDGI3.js} +4 -23
  76. package/dist/cli-delegate/{chunk-63WSP6YZ.js → chunk-PMEKGDED.js} +74 -167
  77. package/dist/cli-delegate/{chunk-GRRPEPRN.js → chunk-T7L2QK3K.js} +24 -5
  78. package/dist/cli-delegate/{chunk-EVTY7ISR.js → chunk-YM4GZDJG.js} +9 -1
  79. package/dist/cli-delegate/cmd_adr_effective.js +1162 -0
  80. package/dist/cli-delegate/cmd_conformance.js +466 -716
  81. package/dist/cli-delegate/cmd_converge.js +3 -3
  82. package/dist/cli-delegate/cmd_doctor.js +10 -10
  83. package/dist/cli-delegate/cmd_explain.js +1 -1
  84. package/dist/cli-delegate/cmd_export.js +10 -9
  85. package/dist/cli-delegate/cmd_handoff.js +10 -9
  86. package/dist/cli-delegate/cmd_memory_get.js +2 -1
  87. package/dist/cli-delegate/cmd_migrate.js +2 -2
  88. package/dist/cli-delegate/cmd_packs_active.js +1 -1
  89. package/dist/cli-delegate/cmd_preflight.js +5 -5
  90. package/dist/cli-delegate/cmd_prune.js +11 -10
  91. package/dist/cli-delegate/cmd_route_audit.js +6 -5
  92. package/dist/cli-delegate/cmd_route_explain.js +3 -3
  93. package/dist/cli-delegate/cmd_session_recycle.js +840 -30
  94. package/dist/cli-delegate/cmd_settings_get.js +3 -3
  95. package/dist/cli-delegate/cmd_settings_migrate.js +10 -9
  96. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  97. package/dist/cli-delegate/cmd_sync.js +11 -10
  98. package/dist/cli-delegate/cmd_uninstall.js +13 -11
  99. package/dist/cli-delegate/cmd_update.js +4 -4
  100. package/dist/cli-delegate/cmd_upgrade.js +4 -4
  101. package/dist/cli-delegate/cmd_validate.js +11 -10
  102. package/dist/cli-delegate/cmd_versions.js +3 -3
  103. package/dist/discovery/deprecation-report.md +1 -1
  104. package/dist/discovery/discovery-manifest.json +111 -37
  105. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  106. package/dist/discovery/discovery-manifest.summary.md +5 -5
  107. package/dist/discovery/dormancy-report.md +1 -1
  108. package/dist/discovery/orphan-report.md +1 -1
  109. package/dist/discovery/packs.json +15 -12
  110. package/dist/discovery/trust-report.md +3 -3
  111. package/dist/discovery/workspaces.json +11 -8
  112. package/dist/hooks/dispatch.js +3165 -2192
  113. package/dist/install/claudePathsPlan.js +187 -0
  114. package/dist/install/claudePathsPlan.js.map +1 -0
  115. package/dist/install/globalRuleLayers.js +169 -0
  116. package/dist/install/globalRuleLayers.js.map +1 -0
  117. package/dist/install/hostLayerFingerprint.js +136 -0
  118. package/dist/install/hostLayerFingerprint.js.map +1 -0
  119. package/dist/install/install.mjs +553 -425
  120. package/dist/install/partitionEligibility.js +432 -0
  121. package/dist/install/partitionEligibility.js.map +1 -0
  122. package/dist/install/ruleLayerPartition.js +157 -0
  123. package/dist/install/ruleLayerPartition.js.map +1 -0
  124. package/dist/mcp/registry-manifest.json +3 -3
  125. package/dist/mcp/server.json +2 -2
  126. package/dist/mcp/server.mjs +1863 -1234
  127. package/dist/router.json +1 -1
  128. package/dist/scripts/_lib/agent_src.js +41 -0
  129. package/dist/scripts/_lib/agent_src.js.map +1 -1
  130. package/dist/scripts/_lib/install_layout.js +83 -0
  131. package/dist/scripts/_lib/install_layout.js.map +1 -0
  132. package/dist/scripts/_lib/installed_lock.js +444 -0
  133. package/dist/scripts/_lib/installed_lock.js.map +1 -0
  134. package/dist/scripts/_lib/prune_empty_dirs.js +85 -0
  135. package/dist/scripts/_lib/prune_empty_dirs.js.map +1 -0
  136. package/dist/scripts/condense.js +63 -251
  137. package/dist/scripts/condense.js.map +1 -1
  138. package/dist/server/schemas/settings.js +4 -0
  139. package/dist/server/schemas/settings.js.map +1 -1
  140. package/docs/contracts/settings-classes.md +12 -6
  141. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +8 -3
  142. package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +45 -1
  143. package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +26 -0
  144. package/docs/guidelines/agent-infra/false-green.md +47 -1
  145. package/docs/guidelines/agent-infra/memory-access.md +17 -0
  146. package/docs/guidelines/agent-infra/recurring-criticism-mechanics.md +96 -0
  147. package/package.json +1 -1
  148. package/src/agent-src/scripts/archival_sweep.ts +117 -0
  149. package/src/agent-src/scripts/archive_completed_roadmaps.ts +26 -7
  150. package/src/agent-src/scripts/dashboard_mode.ts +212 -0
  151. package/src/agent-src/scripts/gate_budget.ts +282 -0
  152. package/src/agent-src/scripts/gate_execute.ts +72 -31
  153. package/src/agent-src/scripts/roadmap_gates.ts +74 -3
  154. package/src/agent-src/scripts/update_roadmap_progress.ts +296 -53
  155. package/src/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
  156. package/src/agent-src/templates/scripts/telemetry/remote.ts +160 -7
  157. package/src/agent-src/templates/scripts/telemetry/transport.ts +169 -0
  158. package/src/config/agent-settings.template.yml +27 -2
  159. package/src/config/ci-local-parity.yml +39 -0
  160. package/src/config/estate-count-budget.json +612 -3
  161. package/src/config/evaluator-budgets.json +3 -3
  162. package/src/config/gate-coverage.yml +220 -12
  163. package/src/config/gate-violation-baselines.json +73 -63
  164. package/src/config/gitignore-block.txt +15 -2
  165. package/src/config/hook-latency-budget.json +5 -0
  166. package/src/config/rule-activation-census.json +14 -6
  167. package/src/config/rule-stub-ceilings.json +34 -7
  168. package/src/install/claudePathsPlan.ts +196 -0
  169. package/src/install/globalRuleLayers.ts +197 -0
  170. package/src/install/hostLayerFingerprint.ts +145 -0
  171. package/src/install/partitionEligibility.ts +502 -0
  172. package/src/install/ruleLayerPartition.ts +212 -0
  173. package/src/scripts/_cli/cmd_adr_effective.ts +484 -0
  174. package/src/scripts/_cli/cmd_session_recycle.ts +9 -1
  175. package/src/scripts/_dispatch.bash +17 -1
  176. package/src/scripts/_lib/ac_heading.ts +30 -0
  177. package/src/scripts/_lib/adr_frontmatter.test.ts +365 -0
  178. package/src/scripts/_lib/adr_frontmatter.ts +459 -0
  179. package/src/scripts/_lib/agent_src.ts +42 -0
  180. package/src/scripts/_lib/bench_ab_pinned_repo.ts +162 -0
  181. package/src/scripts/_lib/bench_ab_workspace.ts +10 -1
  182. package/src/scripts/_lib/envelope_grounding.ts +49 -9
  183. package/src/scripts/_lib/installed_lock.ts +50 -5
  184. package/src/scripts/_lib/layer_overlap_notice.ts +11 -2
  185. package/src/scripts/_lib/md_section.ts +129 -0
  186. package/src/scripts/_lib/prune_empty_dirs.ts +80 -0
  187. package/src/scripts/_lib/scope_guard.sh +26 -3
  188. package/src/scripts/_lib/self_repair.ts +19 -0
  189. package/src/scripts/_lib/self_repair_class_b.ts +171 -0
  190. package/src/scripts/adr/evidence_census.ts +899 -0
  191. package/src/scripts/adr/regenerate_index.ts +12 -32
  192. package/src/scripts/adr_cite_check.ts +540 -39
  193. package/src/scripts/ai_council/cli_help.ts +55 -0
  194. package/src/scripts/audit_adr_coverage.ts +103 -17
  195. package/src/scripts/before_complete_hook.ts +153 -28
  196. package/src/scripts/bench_ab_v2_complexity.ts +27 -7
  197. package/src/scripts/bench_hook_latency.ts +204 -0
  198. package/src/scripts/build_archive_index.ts +34 -0
  199. package/src/scripts/check_adr_frontmatter.ts +574 -39
  200. package/src/scripts/check_hook_bundle_content.ts +219 -0
  201. package/src/scripts/check_new_adr_evidence.ts +529 -0
  202. package/src/scripts/check_references.ts +78 -0
  203. package/src/scripts/check_release_published.ts +32 -11
  204. package/src/scripts/check_rule_layer_partition.ts +531 -0
  205. package/src/scripts/check_single_delivery.ts +194 -17
  206. package/src/scripts/check_standing_rule_delivery.ts +8 -2
  207. package/src/scripts/ci_settle.ts +170 -0
  208. package/src/scripts/condense.ts +79 -259
  209. package/src/scripts/council_cli.ts +3 -3
  210. package/src/scripts/dispatch_r2_reviewer.ts +6 -14
  211. package/src/scripts/external_sources_denylist.json +4 -1
  212. package/src/scripts/hook_manifest.json +1 -1
  213. package/src/scripts/hook_manifest.yaml +77 -14
  214. package/src/scripts/hooks/block_no_verify.ts +61 -10
  215. package/src/scripts/hooks/block_unauthorized_git.ts +21 -1
  216. package/src/scripts/hooks/concern_registry.ts +4 -0
  217. package/src/scripts/hooks/dispatch_hook.ts +154 -187
  218. package/src/scripts/hooks/dispatch_issues.ts +38 -19
  219. package/src/scripts/hooks/fallback_yaml.ts +66 -0
  220. package/src/scripts/hooks/host_semantics.ts +21 -0
  221. package/src/scripts/hooks/injection_budget.ts +13 -3
  222. package/src/scripts/hooks/py_json_dumps.ts +103 -0
  223. package/src/scripts/hooks/rtk_wrap_hook.ts +8 -3
  224. package/src/scripts/hooks/run_continuation_hook.ts +8 -5
  225. package/src/scripts/hooks/ship_diff_volume_hook.ts +81 -15
  226. package/src/scripts/hooks/source_first_gate_hook.ts +464 -0
  227. package/src/scripts/hooks/state_io.ts +870 -21
  228. package/src/scripts/hooks/stdin_failure_policy.ts +96 -0
  229. package/src/scripts/hooks/subagent_ledger_hook.ts +44 -15
  230. package/src/scripts/hooks/telemetry_flush_hook.ts +139 -0
  231. package/src/scripts/hooks/telemetry_self_repair.ts +128 -0
  232. package/src/scripts/hooks/telemetry_usage_hook.ts +22 -4
  233. package/src/scripts/hooks/turn_end_gate_hook.ts +81 -15
  234. package/src/scripts/injection_scan_hook.ts +137 -5
  235. package/src/scripts/install.ts +16 -11
  236. package/src/scripts/language_mirror_hook.ts +561 -25
  237. package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
  238. package/src/scripts/lint_hook_manifest.ts +49 -0
  239. package/src/scripts/lint_plan_risk_register.ts +4 -3
  240. package/src/scripts/lint_provenance_vocabulary.ts +472 -4
  241. package/src/scripts/lint_skill_router_head.ts +4 -4
  242. package/src/scripts/mcp_server/tools.ts +2 -2
  243. package/src/scripts/new_roadmap.ts +136 -0
  244. package/src/scripts/pack_mcp_content.ts +20 -5
  245. package/src/scripts/release.ts +37 -38
  246. package/src/scripts/report_layer_overlap.ts +6 -0
  247. package/src/scripts/routing_doctor.ts +5 -1
  248. package/src/scripts/self_repair_hook.ts +37 -5
  249. package/src/scripts/session_register_hook.ts +11 -0
  250. package/src/scripts/skill_usage_collect.ts +11 -1
  251. package/src/scripts/skill_usage_report.ts +278 -8
  252. package/src/scripts/sync_pr_branch.ts +170 -20
  253. package/src/server/schemas/settings.ts +8 -0
  254. package/dist/cli-delegate/chunk-EJITJBJX.js +0 -172
  255. package/dist/cli-delegate/chunk-PZWRPRWP.js +0 -38
  256. package/dist/cli-delegate/{chunk-PPQO4NGR.js → chunk-6RJA2F5A.js} +3 -3
  257. package/dist/cli-delegate/{chunk-RYNADUFG.js → chunk-LWTYTX6F.js} +0 -0
  258. package/dist/cli-delegate/{chunk-FBKL6JWT.js → chunk-MMSGVZKV.js} +3 -3
@@ -251,7 +251,22 @@ function _stripChars(s: string, chars: string): string {
251
251
  // module-level CLI self-invoke into its bundle. Re-exported here because four
252
252
  // in-repo consumers import it from `condense.js`.
253
253
  export { rule_in_scope } from '../install/ruleInScope.js';
254
+ // Re-exported for in-repo consumers; definitions moved beside emit_host_rules_cli.ts (Phase 5).
255
+ export {
256
+ CLAUDE_PATHS_PATTERN_BUDGET,
257
+ _claude_paths_plan,
258
+ _escape_claude_bracket,
259
+ _expanded_pattern_count,
260
+ _has_non_path_trigger,
261
+ _is_unresolved_placeholder,
262
+ derive_trigger_globs,
263
+ type ClaudePathsPlan,
264
+ } from '../install/claudePathsPlan.js';
254
265
  import { rule_in_scope } from '../install/ruleInScope.js';
266
+ import { pruneEmptyDirs } from './_lib/prune_empty_dirs.js';
267
+ import { commandsWithheld, partitionActive, personaPartition, setPartitionAnnounce } from '../install/partitionEligibility.js'; // ADR-236
268
+ import { dedupableRules, partitionRulesForDir } from '../install/ruleLayerPartition.js'; // ADR-236, per-host evidence
269
+ import { _claude_paths_plan, derive_trigger_globs } from '../install/claudePathsPlan.js';
255
270
 
256
271
  const _HERE = path.dirname(fileURLToPath(import.meta.url)); // <repo>/src/scripts
257
272
  const _DEFAULT_PROJECT_ROOT = path.resolve(_HERE, '..', '..');
@@ -304,6 +319,7 @@ function _deriveState(root: string): ModuleState {
304
319
  }
305
320
 
306
321
  export const MODULE_STATE: ModuleState = _deriveState(_DEFAULT_PROJECT_ROOT);
322
+ setPartitionAnnounce(success); // ADR-236: the mode line must be visible at the default level
307
323
 
308
324
  /**
309
325
  * Test seam — reassign one or more module-state fields, mirroring the pytest
@@ -425,15 +441,6 @@ function _read_augment_rules_use_symlinks(): boolean {
425
441
  return false;
426
442
  }
427
443
 
428
- /**
429
- * Per-tool user-scope rule directory, for the scope-dedup below. Only tools
430
- * whose host reads a user-scope rules directory can have a redundant twin;
431
- * everything else has nothing to de-duplicate against.
432
- */
433
- const USER_SCOPE_RULE_DIRS: Readonly<Record<string, string>> = {
434
- '.claude/rules': path.join('.claude', 'rules'),
435
- };
436
-
437
444
  function _read_projection_scope_dedup(): boolean {
438
445
  const data = load_agent_settings({ project_path: MODULE_STATE.SETTINGS_FILE });
439
446
  const projection = data['projection'];
@@ -450,60 +457,6 @@ function _read_projection_scope_dedup(): boolean {
450
457
  return false;
451
458
  }
452
459
 
453
- /**
454
- * Rules whose user-scope twin is BYTE-IDENTICAL to the source, i.e. safe to
455
- * skip at project scope: the host still loads the same text, once instead of
456
- * twice, so nothing the model sees changes.
457
- *
458
- * Byte-identity is the whole safety argument and is not negotiable. Keying on
459
- * the filename alone would silently let a stale globally-installed copy win
460
- * whenever the two scopes hold different versions — which is the NORMAL state
461
- * while developing the package (measured: 110/110 shared filenames differing in
462
- * bytes). This is deliberately not the thin-projection mechanism: no rule
463
- * becomes trigger-gated, so the quality floor that disabled thin projection
464
- * does not apply here.
465
- */
466
- function _dedupable_rules(tool_dir: string, rules: readonly string[], userHome: string): Set<string> {
467
- const relative = USER_SCOPE_RULE_DIRS[tool_dir];
468
- if (relative === undefined) {
469
- return new Set();
470
- }
471
- // A hostile or simply absent $HOME must make the dedup inert, not
472
- // adventurous. In a container `$HOME` is often unset (so `homedir()` can
473
- // resolve to `/`) or world-writable, and this function decides which rules
474
- // to STOP emitting — reading an unexpected tree there is how a projection
475
- // silently loses a rule. Council review of PR #1055 raised exactly this.
476
- let userDirStat: fs.Stats;
477
- const userDir = path.join(userHome, relative);
478
- try {
479
- userDirStat = fs.statSync(userDir);
480
- } catch {
481
- return new Set();
482
- }
483
- if (!userDirStat.isDirectory()) {
484
- return new Set();
485
- }
486
- // World-writable user scope: anyone on the box could plant a byte-identical
487
- // twin and thereby delete a rule from the project projection. Refuse.
488
- if ((userDirStat.mode & 0o002) !== 0) {
489
- _print(` ⚠️ ${tool_dir}: user-scope rules dir is world-writable — scope-dedup skipped`);
490
- return new Set();
491
- }
492
- const skip = new Set<string>();
493
- for (const rule of rules) {
494
- const twin = path.join(userDir, rule);
495
- const source = path.join(MODULE_STATE.RULES_SOURCE, rule);
496
- try {
497
- if (!_isFile(twin) || !_isFile(source)) continue;
498
- if (fs.readFileSync(twin).equals(fs.readFileSync(source))) {
499
- skip.add(rule);
500
- }
501
- } catch {
502
- // An unreadable twin is simply not de-duplicable — emit the project copy.
503
- }
504
- }
505
- return skip;
506
- }
507
460
 
508
461
  function _lean_projection_mode(): string {
509
462
  const data = load_agent_settings({ project_path: MODULE_STATE.SETTINGS_FILE });
@@ -1122,6 +1075,18 @@ function _scoped_rule_basenames(): string[] {
1122
1075
  .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
1123
1076
  }
1124
1077
 
1078
+ /** Adapter over {@link partitionRulesForDir}; that module carries the reasoning. */
1079
+ export function partition_rules_for_dir(
1080
+ tool_dir: string,
1081
+ rules: readonly string[],
1082
+ user_home: string = process.env['HOME'] ?? os.homedir(),
1083
+ ): string[] {
1084
+ return partitionRulesForDir({
1085
+ toolDir: tool_dir, rules, userHome: user_home, announce: _print,
1086
+ projectRoot: MODULE_STATE.PROJECT_ROOT, rulesSource: MODULE_STATE.RULES_SOURCE,
1087
+ });
1088
+ }
1089
+
1125
1090
  /**
1126
1091
  * The emit plan of {@link generate_rule_symlinks}: active tool dir → the exact
1127
1092
  * rule basenames it projects there.
@@ -1146,8 +1111,12 @@ export function projected_rule_trees(
1146
1111
  const dedup_on = _read_projection_scope_dedup();
1147
1112
  const out: Record<string, string[]> = {};
1148
1113
  for (const tool_dir of Object.keys(_filter_tool_dirs(TOOL_DIRS))) {
1149
- const skip = dedup_on ? _dedupable_rules(tool_dir, rules, user_home) : new Set<string>();
1150
- out[tool_dir] = rules.filter((r) => !skip.has(r));
1114
+ const skip = dedup_on
1115
+ ? dedupableRules({ toolDir: tool_dir, rules, userHome: user_home,
1116
+ rulesSource: MODULE_STATE.RULES_SOURCE, announce: _print })
1117
+ : new Set<string>();
1118
+ const kept = rules.filter((r) => !skip.has(r));
1119
+ out[tool_dir] = partition_rules_for_dir(tool_dir, kept, user_home);
1151
1120
  }
1152
1121
  return out;
1153
1122
  }
@@ -1266,7 +1235,10 @@ export function generate_rule_symlinks(): number {
1266
1235
  }
1267
1236
 
1268
1237
  export function generate_windsurfrules(): number {
1269
- const rules = _scoped_rule_basenames();
1238
+ // Windsurf's LEGACY single-file surface — same host, so the same evidence, keyed
1239
+ // on `.windsurf/rules`. Without this line removing the per-run filter regressed
1240
+ // it 13 → 113: the per-run filter had been covering it by accident.
1241
+ const rules = partition_rules_for_dir('.windsurf/rules', _scoped_rule_basenames());
1270
1242
  const parts = ['# Auto-generated from dist/agent-src/rules/ — do not edit directly\n'];
1271
1243
  for (const rule of rules) {
1272
1244
  const p = path.join(MODULE_STATE.RULES_SOURCE, rule);
@@ -1322,32 +1294,6 @@ function _yaml_scalar(value: string): string {
1322
1294
  return JSON.stringify(value);
1323
1295
  }
1324
1296
 
1325
- /**
1326
- * Derive host-native activation globs from a rule's `triggers:` frontmatter
1327
- * (road-to-request-scoped-rule-load Phase 2). `file_pattern` triggers map
1328
- * verbatim; `path_prefix` triggers map to `<prefix>**`. Keyword / phrase /
1329
- * command triggers produce no glob — those rules stay
1330
- * description-activated (Cursor Agent-Requested / Windsurf model_decision).
1331
- */
1332
- export function derive_trigger_globs(meta: Record<string, unknown>): string[] {
1333
- const triggers = meta['triggers'];
1334
- if (!Array.isArray(triggers)) {
1335
- return [];
1336
- }
1337
- const globs: string[] = [];
1338
- for (const t of triggers) {
1339
- if (t === null || typeof t !== 'object' || Array.isArray(t)) continue;
1340
- const obj = t as Record<string, unknown>;
1341
- if (typeof obj['file_pattern'] === 'string' && obj['file_pattern']) {
1342
- globs.push(obj['file_pattern']);
1343
- } else if (typeof obj['path_prefix'] === 'string' && obj['path_prefix']) {
1344
- const prefix = obj['path_prefix'] as string;
1345
- globs.push(prefix.endsWith('/') ? `${prefix}**` : `${prefix}**`);
1346
- }
1347
- }
1348
- // De-dup, keep declaration order (deterministic output).
1349
- return [...new Set(globs)];
1350
- }
1351
1297
 
1352
1298
  export function _emit_cursor_mdc(source: string, target: string): void {
1353
1299
  const [meta, body] = _parse_frontmatter(_readText(source));
@@ -1390,151 +1336,8 @@ export function _emit_windsurf_rule(source: string, target: string): void {
1390
1336
  _writeText(target, lines.join('\n'));
1391
1337
  }
1392
1338
 
1393
- /**
1394
- * A brace group the host expands, versus an agent-config placeholder that only
1395
- * LOOKS like one.
1396
- *
1397
- * `path_prefix: "{module_root}/"` in `roadmap-ci-steps-policy` is resolved by
1398
- * this suite from `modules.root_paths`. Emitted verbatim into a host `paths:`
1399
- * list it is a brace group with no comma, which the host expands to the literal
1400
- * string `{module_root}` — a pattern that matches no file. The rule would then
1401
- * reach the model **never** instead of on-demand, and nothing would say so: the
1402
- * probed contract records this exact degradation as silent
1403
- * (`claude-code-rules-dir-contract.md` § constraints).
1404
- *
1405
- * Dropping the pattern is fail-safe in the one direction that matters. A rule
1406
- * left with no pattern at all carries no `paths:` key and loads
1407
- * unconditionally — the status quo, not a regression. A rule carrying a
1408
- * no-op pattern is invisible.
1409
- *
1410
- * The discriminator is the comma, not a placeholder allowlist: `{ts,tsx}` is a
1411
- * real alternation, `{module_root}` cannot be one.
1412
- */
1413
- export function _is_unresolved_placeholder(pattern: string): boolean {
1414
- for (const m of pattern.matchAll(/\{([^}]*)\}/g)) {
1415
- if (!(m[1] as string).includes(',')) {
1416
- return true;
1417
- }
1418
- }
1419
- return false;
1420
- }
1421
-
1422
- /**
1423
- * `[` opens a bracket expression for the host. An unparseable one makes that
1424
- * pattern match nothing while the rule's other patterns keep working — so a
1425
- * literal `[` in a trigger is escaped rather than trusted.
1426
- */
1427
- export function _escape_claude_bracket(pattern: string): string {
1428
- return pattern.replace(/\[/g, '\\[');
1429
- }
1430
-
1431
- /**
1432
- * Expanded-pattern cost of one glob: the product of its brace alternations.
1433
- * The host budgets a rule's whole `paths:` list at 1,000 expanded patterns and
1434
- * 4 MiB, and a list over budget is used UNEXPANDED — braces then match
1435
- * literally, i.e. nothing. Counting keeps the emitter under the cliff instead
1436
- * of discovering it as a silent no-op.
1437
- */
1438
- export function _expanded_pattern_count(pattern: string): number {
1439
- let n = 1;
1440
- for (const m of pattern.matchAll(/\{([^}]*)\}/g)) {
1441
- n *= (m[1] as string).split(',').length;
1442
- }
1443
- return n;
1444
- }
1445
1339
 
1446
- /** Host-documented budget for one rule's whole `paths:` list. */
1447
- export const CLAUDE_PATHS_PATTERN_BUDGET = 1000;
1448
1340
 
1449
- export interface ClaudePathsPlan {
1450
- /** Patterns to emit under `paths:`, in declaration order. */
1451
- globs: string[];
1452
- /** Patterns deliberately not emitted, with the reason a reader can act on. */
1453
- dropped: { pattern: string; reason: 'unresolved-placeholder' | 'over-budget' | 'mixed-triggers' }[];
1454
- }
1455
-
1456
- /**
1457
- * Does this rule declare any trigger that is NOT path-shaped?
1458
- *
1459
- * Load-bearing for `_claude_paths_plan` below, because one `triggers:` list
1460
- * means two different things on two hosts. Cursor and Windsurf treat globs as
1461
- * ADDITIVE — a rule with empty globs still reaches the agent through its
1462
- * description (see `_emit_cursor_mdc`'s own comment). Claude Code treats
1463
- * `paths:` as EXCLUSIVE: with it the rule loads on a path match; without it the
1464
- * rule loads unconditionally. The emitter fed both semantics from one list, so
1465
- * a rule that declared keywords *and* one path glob silently became
1466
- * path-gated-only on Claude.
1467
- */
1468
- export function _has_non_path_trigger(meta: Record<string, unknown>): boolean {
1469
- const triggers = meta['triggers'];
1470
- if (!Array.isArray(triggers)) return false;
1471
- for (const t of triggers) {
1472
- if (t === null || typeof t !== 'object' || Array.isArray(t)) continue;
1473
- const obj = t as Record<string, unknown>;
1474
- if (typeof obj['keyword'] === 'string' && obj['keyword']) return true;
1475
- if (typeof obj['phrase'] === 'string' && obj['phrase']) return true;
1476
- }
1477
- return false;
1478
- }
1479
-
1480
- /**
1481
- * Derive the host's `paths:` list for one rule, with every drop recorded.
1482
- *
1483
- * Kernel / always-apply rules get an EMPTY list on purpose: absent `paths:` is
1484
- * how this host says "load unconditionally", which is exactly what an Iron-Law
1485
- * rule needs. Scoping one would be a correctness regression dressed as a byte
1486
- * saving — and the same contract notes path-scoped rules are not re-injected
1487
- * after `/compact`, so an obligation that must survive compaction cannot be
1488
- * path-scoped at all.
1489
- */
1490
- export function _claude_paths_plan(meta: Record<string, unknown>): ClaudePathsPlan {
1491
- const always_apply = Boolean(meta['alwaysApply'] || meta['type'] === 'always');
1492
- if (always_apply) {
1493
- return { globs: [], dropped: [] };
1494
- }
1495
- const globs: string[] = [];
1496
- const dropped: ClaudePathsPlan['dropped'] = [];
1497
-
1498
- // A rule that ALSO declares keyword / phrase triggers gets no `paths:` at
1499
- // all, and therefore keeps loading unconditionally on Claude Code.
1500
- //
1501
- // Emitting `paths:` here would narrow the rule to a path match and discard
1502
- // every keyword the author wrote, because this host reads the list as the
1503
- // whole gate rather than as one way in. Measured before this guard: 25 of
1504
- // 110 projected rules carried `paths:` and 19 of them also carried keyword
1505
- // triggers — `design-fidelity` lost 21 of them, in exactly the pasted-
1506
- // screenshot and capability-URL handovers its own routing section names as
1507
- // the classes it exists to catch. Nothing in the tree recorded a decision
1508
- // to make that trade, which is why it is treated as a defect.
1509
- //
1510
- // The drop is REPORTED rather than silent, so a rule whose obligation
1511
- // really is path-bound is visible as a candidate for removing its keyword
1512
- // triggers — the narrowing then becomes an authoring decision instead of an
1513
- // emitter side effect. Cursor and Windsurf are untouched: globs are
1514
- // additive there, so the same rule keeps both routes.
1515
- if (_has_non_path_trigger(meta)) {
1516
- for (const raw of derive_trigger_globs(meta)) {
1517
- dropped.push({ pattern: raw, reason: 'mixed-triggers' });
1518
- }
1519
- return { globs, dropped };
1520
- }
1521
-
1522
- let spent = 0;
1523
- for (const raw of derive_trigger_globs(meta)) {
1524
- if (_is_unresolved_placeholder(raw)) {
1525
- dropped.push({ pattern: raw, reason: 'unresolved-placeholder' });
1526
- continue;
1527
- }
1528
- const cost = _expanded_pattern_count(raw);
1529
- if (spent + cost > CLAUDE_PATHS_PATTERN_BUDGET) {
1530
- dropped.push({ pattern: raw, reason: 'over-budget' });
1531
- continue;
1532
- }
1533
- spent += cost;
1534
- globs.push(_escape_claude_bracket(raw));
1535
- }
1536
- return { globs, dropped };
1537
- }
1538
1341
 
1539
1342
  /**
1540
1343
  * Emit a Claude Code rule carrying the host's OWN activation key.
@@ -1605,12 +1408,16 @@ function _clean_modern_dir(target_dir: string, valid_names: ReadonlySet<string>)
1605
1408
  export function generate_cursor_mdc_rules(): number {
1606
1409
  const scope = _read_rule_workspaces();
1607
1410
  const pack_scope = _read_rule_packs();
1608
- const rules = _rglobSorted(MODULE_STATE.RULES_SOURCE, '*.md')
1411
+ let rules = _rglobSorted(MODULE_STATE.RULES_SOURCE, '*.md')
1609
1412
  .filter((p) => _isFile(p))
1610
1413
  .filter((p) => rule_in_scope(p, scope, pack_scope))
1611
1414
  // ADR-004 manual rules are reference-only — never auto-injected into a
1612
1415
  // per-tool tree. Same predicate as generate_rule_symlinks.
1613
1416
  .filter((p) => !_is_manual_rule(p));
1417
+ // ADR-236 per-host partition (src/install/ruleLayerPartition.ts). Narrowing the
1418
+ // list is a REMOVAL, because `_clean_modern_dir` below sweeps to `valid`.
1419
+ const kept = new Set(partition_rules_for_dir('.cursor/rules', rules.map((r) => path.basename(r))));
1420
+ rules = rules.filter((r) => kept.has(path.basename(r)));
1614
1421
  const stems = rules.map((r) => path.basename(r, '.md'));
1615
1422
  const valid = new Set([
1616
1423
  ...stems.map((s) => `${s}.mdc`),
@@ -1627,12 +1434,15 @@ export function generate_cursor_mdc_rules(): number {
1627
1434
  export function generate_windsurf_modern_rules(): number {
1628
1435
  const scope = _read_rule_workspaces();
1629
1436
  const pack_scope = _read_rule_packs();
1630
- const rules = _rglobSorted(MODULE_STATE.RULES_SOURCE, '*.md')
1437
+ let rules = _rglobSorted(MODULE_STATE.RULES_SOURCE, '*.md')
1631
1438
  .filter((p) => _isFile(p))
1632
1439
  .filter((p) => rule_in_scope(p, scope, pack_scope))
1633
1440
  // ADR-004 manual rules are reference-only — never auto-injected into a
1634
1441
  // per-tool tree. Same predicate as generate_rule_symlinks.
1635
1442
  .filter((p) => !_is_manual_rule(p));
1443
+ // ADR-236 per-host partition — same shape as the cursor emitter above.
1444
+ const kept = new Set(partition_rules_for_dir('.windsurf/rules', rules.map((r) => path.basename(r))));
1445
+ rules = rules.filter((r) => kept.has(path.basename(r)));
1636
1446
  const valid = new Set(rules.map((r) => path.basename(r)));
1637
1447
  _clean_modern_dir(_WINDSURF_RULES_DIR(), valid);
1638
1448
  for (const rule of rules) {
@@ -1823,6 +1633,7 @@ function _render_native_model_md(src_md: string, tier: string): string {
1823
1633
  }
1824
1634
 
1825
1635
  export function generate_claude_skills(active_skill_names: ReadonlySet<string> | null = null): number {
1636
+ if (partitionActive(MODULE_STATE.PROJECT_ROOT)) active_skill_names = new Set<string>();
1826
1637
  if (!_exists(MODULE_STATE.SKILLS_SOURCE)) {
1827
1638
  process.stderr.write(' ⚠️ dist/agent-src/skills/ not found — skipping skills\n');
1828
1639
  return 0;
@@ -1836,7 +1647,9 @@ export function generate_claude_skills(active_skill_names: ReadonlySet<string> |
1836
1647
  skills = skills.filter((s) => active_skill_names.has(s));
1837
1648
  }
1838
1649
  const skill_set = new Set(skills);
1839
- const command_slugs = new Set([...iterCommands()].map(([, slug]) => slug));
1650
+ const command_slugs = partitionActive(MODULE_STATE.PROJECT_ROOT)
1651
+ ? new Set<string>()
1652
+ : new Set([...iterCommands()].map(([, slug]) => slug));
1840
1653
 
1841
1654
  _mkdirp(MODULE_STATE.CLAUDE_SKILLS_DIR);
1842
1655
  const auto = _read_model_auto_switch() === 'auto';
@@ -1932,18 +1745,16 @@ function _nested_command_subpath(source_file: string): string | null {
1932
1745
  /**
1933
1746
  * Project-scope `.claude/commands/<cluster>/<sub>.md` — the colon form.
1934
1747
  *
1935
- * Before this, the package repo's own `.claude/` carried NO commands
1936
- * directory: every command reached Claude Code as a hyphen-named skill
1937
- * wrapper in `.claude/skills/`, while the colon form existed only in the
1938
- * user-global tree written by `install.ts`. Every clustered command was
1939
- * therefore listed TWICE for anyone with a global install
1940
- * (`roadmap-process-full` AND `roadmap:process-full`), costing 4,214 GPT tok
1941
- * of always-loaded catalog for zero added reach — measured 2026-08-02.
1748
+ * Before this, the package repo's own `.claude/` carried NO commands directory:
1749
+ * every command reached Claude Code as a hyphen-named skill wrapper in
1750
+ * `.claude/skills/`, while the colon form existed only in the user-global tree
1751
+ * written by `install.ts`. Every clustered command was therefore listed TWICE
1752
+ * for anyone with a global install (`roadmap-process-full` AND
1753
+ * `roadmap:process-full`), costing 4,214 GPT tok of catalog — measured 2026-08-02.
1942
1754
  *
1943
1755
  * Emitting the nested form here lets `generate_claude_commands` stop wrapping
1944
1756
  * the clustered commands: one listing, same reachability, and reachability no
1945
- * longer depends on a global deploy having run. Claude Code dedupes project
1946
- * and user scope by name, so the two copies of `/cluster:sub` collapse.
1757
+ * longer depends on a global deploy. Dedup AND precedence: `commandsWithheld`.
1947
1758
  *
1948
1759
  * ADR-003 (colon canonical for clusters) and ADR-044 (flat commands stay
1949
1760
  * hyphenated) both hold: flat commands are untouched and keep their wrapper.
@@ -1951,6 +1762,8 @@ function _nested_command_subpath(source_file: string): string | null {
1951
1762
  export function generate_claude_project_commands(
1952
1763
  active_command_slugs: ReadonlySet<string> | null = null,
1953
1764
  ): number {
1765
+ if (commandsWithheld(MODULE_STATE.PROJECT_ROOT)) active_command_slugs = new Set<string>();
1766
+ // Returns before the stale sweep — invariant, see `commandsWithheld`.
1954
1767
  if (!_isDir(path.join(MODULE_STATE.PROJECT_ROOT, 'src', 'domains'))) {
1955
1768
  return 0;
1956
1769
  }
@@ -1967,8 +1780,7 @@ export function generate_claude_project_commands(
1967
1780
 
1968
1781
  const target_root = _CLAUDE_COMMANDS_DIR();
1969
1782
  const valid = new Set(nested.map(([, sub]) => `${sub}.md`));
1970
- // Sweep stale links first: a renamed or deleted command must not linger as
1971
- // a dangling `/cluster:sub`.
1783
+ // Sweep stale links first — a renamed, deleted or WITHHELD command must not linger.
1972
1784
  if (_exists(target_root)) {
1973
1785
  for (const item of _rglobSorted(target_root, '*.md')) {
1974
1786
  const rel = _relativeToPosix(item, target_root);
@@ -1976,6 +1788,7 @@ export function generate_claude_project_commands(
1976
1788
  fs.unlinkSync(item);
1977
1789
  }
1978
1790
  }
1791
+ pruneEmptyDirs(target_root); // else the gate counts 40 empty cluster dirs as 40 commands
1979
1792
  }
1980
1793
 
1981
1794
  let count = 0;
@@ -1997,6 +1810,7 @@ export function generate_claude_project_commands(
1997
1810
  }
1998
1811
 
1999
1812
  export function generate_claude_commands(active_command_slugs: ReadonlySet<string> | null = null): number {
1813
+ if (partitionActive(MODULE_STATE.PROJECT_ROOT)) active_command_slugs = new Set<string>();
2000
1814
  if (!_isDir(path.join(MODULE_STATE.PROJECT_ROOT, 'src', 'domains'))) {
2001
1815
  process.stderr.write(' ⚠️ src/domains/ not found — skipping commands\n');
2002
1816
  return 0;
@@ -2447,7 +2261,7 @@ export function generate_persona_symlinks(): number {
2447
2261
  .map((p) => path.basename(p))
2448
2262
  .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
2449
2263
  void personas;
2450
- const list = direct;
2264
+ const partition = personaPartition(MODULE_STATE.PROJECT_ROOT, direct);
2451
2265
  const tool_dirs = _filter_tool_dirs(PERSONA_TOOL_DIRS);
2452
2266
  let total = 0;
2453
2267
  for (const [tool_dir, rel_prefix] of Object.entries(tool_dirs)) {
@@ -2455,11 +2269,11 @@ export function generate_persona_symlinks(): number {
2455
2269
  _mkdirp(target_dir);
2456
2270
  for (const item of _iterdirSorted(target_dir)) {
2457
2271
  const name = path.basename(item);
2458
- if (_isSymlink(item) && !list.includes(name) && name !== 'README.md') {
2272
+ if (_isSymlink(item) && !partition.listFor(tool_dir).includes(name) && name !== 'README.md') {
2459
2273
  fs.unlinkSync(item);
2460
2274
  }
2461
2275
  }
2462
- for (const persona of list) {
2276
+ for (const persona of partition.listFor(tool_dir)) {
2463
2277
  const link = path.join(target_dir, persona);
2464
2278
  const target = path.join(rel_prefix, persona);
2465
2279
  if (_existsOrSymlink(link)) {
@@ -2469,9 +2283,8 @@ export function generate_persona_symlinks(): number {
2469
2283
  total += 1;
2470
2284
  }
2471
2285
  }
2472
- info(
2473
- ` ✅ Created ${total} persona symlinks across ${Object.keys(tool_dirs).length} tool directories (${list.length} personas each)`,
2474
- );
2286
+ const per_dir = Object.keys(tool_dirs).map((d) => `${d}=${partition.countFor(d)}`);
2287
+ info(` ✅ Created ${total} persona symlinks — ${per_dir.join(' · ')}${partition.note}`);
2475
2288
  return total;
2476
2289
  }
2477
2290
 
@@ -2671,10 +2484,17 @@ export function project_to_augment(): void {
2671
2484
  const current = new Set<string>();
2672
2485
  let written = 0;
2673
2486
  if (_exists(src_rules)) {
2674
- const rules = _iterdirSorted(src_rules)
2675
- .filter((p) => p.endsWith('.md') && _isFile(p))
2676
- .map((p) => path.basename(p))
2677
- .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
2487
+ // ADR-236 per-host partition, ahead of the `use_symlinks` branch so both
2488
+ // writers narrow; the `existing` minus `current` sweep below makes it a
2489
+ // removal. This emitter never had the scope or ADR-004 manual filters
2490
+ // either — why its count is 15 and the symlink trees' 13. Pre-existing.
2491
+ const rules = partition_rules_for_dir(
2492
+ '.augment/rules',
2493
+ _iterdirSorted(src_rules)
2494
+ .filter((p) => p.endsWith('.md') && _isFile(p))
2495
+ .map((p) => path.basename(p))
2496
+ .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)),
2497
+ );
2678
2498
  for (const rule of rules) {
2679
2499
  const target = path.join(dst_rules, rule);
2680
2500
  if (_isSymlink(target) || _existsOrSymlink(target)) {
@@ -63,6 +63,7 @@ import {
63
63
  plan_advisor_swap,
64
64
  } from './ai_council/advisors.js';
65
65
  import { format_install_hints } from './ai_council/cli_hints.js';
66
+ import { renderSubHelp } from './ai_council/cli_help.js';
66
67
  import {
67
68
  type AdvisorConfig,
68
69
  type CouncilConfig,
@@ -2507,6 +2508,7 @@ function cmd_run(
2507
2508
  const [extra_calls, extra_usd] = _consensus_cost_delta(ai_cfg, question.mode, estimates, billable.length);
2508
2509
  const [ch_calls, ch_usd] = _chairman_cost_delta(ai_cfg, estimates);
2509
2510
  const [pr_extra_calls, pr_extra_usd] = _peer_review_cost_delta(ai_cfg, args, estimates, billable.length);
2511
+ if (!args.confirm) _stdout('council:run · DRY PASS — estimate only, no seat is contacted. Add --confirm to run.\n');
2510
2512
  _stdout(
2511
2513
  `council:run · mode=${question.mode} · members=${members.length} ` +
2512
2514
  `(billable=${billable.length})\n`,
@@ -3728,10 +3730,8 @@ function _parseArgs(argv: string[]): Args {
3728
3730
  const rest = argv.slice(1);
3729
3731
  const usage = _usageFor(first);
3730
3732
  const { positionals, opts, requiredOpts } = _specsFor(first);
3731
-
3732
- // Sub-help.
3733
3733
  if (rest.includes('-h') || rest.includes('--help')) {
3734
- _stdout(usage);
3734
+ _stdout(renderSubHelp(first, usage, { positionals, opts, requiredOpts }));
3735
3735
  process.exitCode = 0;
3736
3736
  throw new _ArgExit();
3737
3737
  }
@@ -51,6 +51,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
51
51
  // (validator → dispatcher for the scope hash, dispatcher → validator for
52
52
  // relevance) is safe: neither module calls the other at module-evaluation
53
53
  // time, and each CLI entry guard only fires for its own argv[1].
54
+ import { isAcceptanceCriteriaHeading } from './_lib/ac_heading.js';
54
55
  import { gitEnv } from './_lib/git_env.js';
55
56
  import { completionReviewDisabled } from './_lib/planning_settings.js';
56
57
  import { artifactRelevance } from './check_completion_review.js';
@@ -492,20 +493,11 @@ export function extractAcceptanceCriteria(roadmapText: string): string {
492
493
  const lines = roadmapText.split('\n');
493
494
  let start = -1;
494
495
  for (let i = 0; i < lines.length; i++) {
495
- // Case-insensitive, and NOT anchored at the end of the line.
496
- //
497
- // Case first: the tree carries both `## Acceptance Criteria` and
498
- // `## Acceptance criteria`; a case-sensitive match silently extracted
499
- // nothing for the latter (found by the zcs-close R2 review, 2026-08-09).
500
- //
501
- // The end-anchor was the same defect wearing a different mask, and it was
502
- // LIVE: `\s*$` rejected a heading carrying a qualifier, so
503
- // `## Acceptance criteria (per phase, on promotion to ready)` and
504
- // `## Acceptance criteria (anti-dump — the review-s own rule)` extracted
505
- // nothing. Two roadmaps, measured 2026-08-18 — and they had been counted
506
- // as "declares no criteria" by the very census that motivated this fix,
507
- // which is how a matcher tuned to its own measurement hides its misses.
508
- if (/^##\s+acceptance criteria\b/i.test(lines[i] as string)) {
496
+ // Case-insensitive and NOT end-anchored — both of those were live misses
497
+ // here (case: zcs-close R2 review 2026-08-09; the `(per phase, …)` qualifier:
498
+ // measured 2026-08-18). The predicate now lives in `_lib/ac_heading`, which
499
+ // records all three instances of the defect and why it is shared.
500
+ if (isAcceptanceCriteriaHeading(lines[i] as string)) {
509
501
  start = i;
510
502
  break;
511
503
  }
@@ -62,7 +62,10 @@
62
62
  "\\bagent-reach\\b",
63
63
  "panniantong",
64
64
  "virgiliojr94",
65
- "book-to-skill"
65
+ "book-to-skill",
66
+ "project-codeguard",
67
+ "\\bcodeguard\\b",
68
+ "\\bcosai\\b"
66
69
  ],
67
70
  "skip_paths": [
68
71
  "src/scripts/check_no_external_sources.py",