@event4u/agent-config 16.3.0 → 16.5.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 (532) 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 +178 -78
  5. package/README.md +2 -2
  6. package/agents/templates/.ai-council.yml.example +26 -8
  7. package/dist/agent-src/commands/analyze/inbox.md +10 -7
  8. package/dist/agent-src/commands/bug/investigate.md +1 -1
  9. package/dist/agent-src/commands/commit/in-chunks.md +40 -31
  10. package/dist/agent-src/commands/commit.md +72 -33
  11. package/dist/agent-src/commands/condense.md +2 -8
  12. package/dist/agent-src/commands/council/debate.md +11 -7
  13. package/dist/agent-src/commands/council/default.md +10 -9
  14. package/dist/agent-src/commands/council.md +1 -1
  15. package/dist/agent-src/commands/design.md +1 -1
  16. package/dist/agent-src/commands/estimate-ticket.md +1 -1
  17. package/dist/agent-src/commands/feature/explore.md +3 -3
  18. package/dist/agent-src/commands/feature/plan.md +1 -1
  19. package/dist/agent-src/commands/fix/ci.md +17 -3
  20. package/dist/agent-src/commands/fix/commit-messages.md +48 -23
  21. package/dist/agent-src/commands/fix/pr-comments.md +3 -6
  22. package/dist/agent-src/commands/implement-ticket.md +3 -3
  23. package/dist/agent-src/commands/memory/add.md +4 -16
  24. package/dist/agent-src/commands/memory/load.md +5 -6
  25. package/dist/agent-src/commands/memory/promote.md +3 -16
  26. package/dist/agent-src/commands/mission/upgrade.md +3 -2
  27. package/dist/agent-src/commands/optimize/agents-dir.md +2 -2
  28. package/dist/agent-src/commands/optimize/deep.md +4 -2
  29. package/dist/agent-src/commands/optimize/rtk.md +4 -26
  30. package/dist/agent-src/commands/pr/create/description-only.md +18 -15
  31. package/dist/agent-src/commands/pr/create.md +42 -34
  32. package/dist/agent-src/commands/pr/merge.md +136 -9
  33. package/dist/agent-src/commands/prepare-for-review.md +13 -11
  34. package/dist/agent-src/commands/project/analyze.md +7 -35
  35. package/dist/agent-src/commands/refine-ticket.md +1 -1
  36. package/dist/agent-src/commands/review/changes.md +14 -19
  37. package/dist/agent-src/commands/roadmap/next.md +10 -1
  38. package/dist/agent-src/commands/roadmap/process-full.md +52 -38
  39. package/dist/agent-src/commands/roadmap/resolve-blockers.md +64 -14
  40. package/dist/agent-src/commands/worktree/create.md +7 -3
  41. package/dist/agent-src/commands/worktree/verify.md +2 -1
  42. package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +5 -3
  43. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +7 -9
  44. package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +8 -6
  45. package/dist/agent-src/contexts/contracts/frugality-charter.md +8 -5
  46. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +1 -1
  47. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +0 -1
  48. package/dist/agent-src/contexts/execution/evidence-discipline.md +2 -2
  49. package/dist/agent-src/contexts/execution/non-interactive-contract.md +5 -8
  50. package/dist/agent-src/contexts/execution/rdp-gate.md +4 -4
  51. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +9 -8
  52. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +67 -52
  53. package/dist/agent-src/contexts/execution/subagent-routing.md +8 -8
  54. package/dist/agent-src/contexts/execution/subagent-steering.md +6 -4
  55. package/dist/agent-src/contexts/execution/terminal-states.md +2 -2
  56. package/dist/agent-src/contexts/execution/toolchain-resolver.md +4 -10
  57. package/dist/agent-src/contexts/subagent-configuration.md +1 -1
  58. package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +39 -0
  59. package/dist/agent-src/guidelines/agent-infra/context-hygiene-mechanics.md +1 -1
  60. package/dist/agent-src/guidelines/agent-infra/cross-source-consistency-mechanics.md +8 -10
  61. package/dist/agent-src/guidelines/agent-infra/frontier-reasoning-operating-profile.md +3 -2
  62. package/dist/agent-src/guidelines/agent-infra/layered-settings.md +13 -1
  63. package/dist/agent-src/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  64. package/dist/agent-src/guidelines/agent-infra/self-improvement-pipeline.md +10 -3
  65. package/dist/agent-src/guidelines/agent-infra/size-and-scope.md +36 -0
  66. package/dist/agent-src/guidelines/design-antipatterns.md +1 -1
  67. package/dist/agent-src/guidelines/php/git.md +15 -2
  68. package/dist/agent-src/rules/active-remediation.md +0 -1
  69. package/dist/agent-src/rules/analysis-skill-routing.md +0 -1
  70. package/dist/agent-src/rules/architecture.md +0 -1
  71. package/dist/agent-src/rules/artifact-drafting-protocol.md +0 -1
  72. package/dist/agent-src/rules/artifact-engagement-recording.md +0 -1
  73. package/dist/agent-src/rules/augment-edit-discipline.md +0 -1
  74. package/dist/agent-src/rules/autonomous-execution.md +0 -1
  75. package/dist/agent-src/rules/brand-consistency.md +0 -1
  76. package/dist/agent-src/rules/brand-source-of-truth.md +0 -1
  77. package/dist/agent-src/rules/broken-access-control.md +0 -1
  78. package/dist/agent-src/rules/cli-output-handling.md +0 -1
  79. package/dist/agent-src/rules/code-comment-discipline.md +0 -1
  80. package/dist/agent-src/rules/code-provenance.md +0 -1
  81. package/dist/agent-src/rules/command-suggestion-policy.md +0 -1
  82. package/dist/agent-src/rules/commit-conventions.md +1 -2
  83. package/dist/agent-src/rules/communication-through-line.md +1 -2
  84. package/dist/agent-src/rules/content-quoting-floor.md +1 -2
  85. package/dist/agent-src/rules/context-hygiene.md +5 -5
  86. package/dist/agent-src/rules/copilot-routing.md +1 -2
  87. package/dist/agent-src/rules/council-availability.md +4 -4
  88. package/dist/agent-src/rules/cross-source-consistency.md +4 -6
  89. package/dist/agent-src/rules/decision-revisit-gate.md +2 -3
  90. package/dist/agent-src/rules/delegation-policy.md +2 -3
  91. package/dist/agent-src/rules/design-fidelity.md +0 -1
  92. package/dist/agent-src/rules/design-review-after-ui-write.md +3 -3
  93. package/dist/agent-src/rules/devcontainer-routing.md +1 -2
  94. package/dist/agent-src/rules/doc-screenshot-hygiene.md +0 -1
  95. package/dist/agent-src/rules/docker-commands.md +0 -1
  96. package/dist/agent-src/rules/domain-adoption-policy.md +0 -1
  97. package/dist/agent-src/rules/domain-safety-disclaimer.md +0 -1
  98. package/dist/agent-src/rules/domain-safety-pii.md +0 -1
  99. package/dist/agent-src/rules/domain-safety-retention.md +0 -1
  100. package/dist/agent-src/rules/downstream-changes.md +0 -1
  101. package/dist/agent-src/rules/engineering-safety-floor.md +0 -1
  102. package/dist/agent-src/rules/external-code-graph-interop.md +0 -1
  103. package/dist/agent-src/rules/external-reference-deep-dive.md +0 -1
  104. package/dist/agent-src/rules/fast-path-marker-visibility.md +1 -2
  105. package/dist/agent-src/rules/finance-safety-floor.md +0 -1
  106. package/dist/agent-src/rules/fix-what-you-see.md +6 -32
  107. package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +0 -1
  108. package/dist/agent-src/rules/git-history-discipline.md +1 -2
  109. package/dist/agent-src/rules/guidelines.md +0 -1
  110. package/dist/agent-src/rules/history-discipline.md +0 -1
  111. package/dist/agent-src/rules/icon-consistency.md +0 -1
  112. package/dist/agent-src/rules/image-likeness-and-rights.md +0 -1
  113. package/dist/agent-src/rules/improve-before-implement.md +0 -1
  114. package/dist/agent-src/rules/invite-challenge.md +0 -1
  115. package/dist/agent-src/rules/laravel-routing.md +1 -2
  116. package/dist/agent-src/rules/laravel-translations.md +0 -1
  117. package/dist/agent-src/rules/legal-safety-floor.md +0 -1
  118. package/dist/agent-src/rules/lethal-trifecta-guard.md +0 -1
  119. package/dist/agent-src/rules/linked-projects-onboarding-gate.md +0 -1
  120. package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +0 -1
  121. package/dist/agent-src/rules/markdown-safe-codeblocks.md +0 -1
  122. package/dist/agent-src/rules/media-governance-routing.md +1 -2
  123. package/dist/agent-src/rules/media-sync-ground-truth.md +0 -1
  124. package/dist/agent-src/rules/minimal-safe-diff.md +0 -1
  125. package/dist/agent-src/rules/missing-skill-recovery.md +0 -1
  126. package/dist/agent-src/rules/missing-tool-handling.md +0 -1
  127. package/dist/agent-src/rules/model-recommendation.md +0 -1
  128. package/dist/agent-src/rules/neighbour-precedence.md +64 -0
  129. package/dist/agent-src/rules/no-attribution-footers.md +0 -1
  130. package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +0 -1
  131. package/dist/agent-src/rules/no-pr-progress-comments.md +0 -1
  132. package/dist/agent-src/rules/no-roadmap-references.md +0 -1
  133. package/dist/agent-src/rules/notes-first-reasoning.md +0 -1
  134. package/dist/agent-src/rules/onboarding-gate.md +0 -1
  135. package/dist/agent-src/rules/output-discipline.md +0 -1
  136. package/dist/agent-src/rules/package-ci-checks.md +0 -1
  137. package/dist/agent-src/rules/persona-governance.md +2 -3
  138. package/dist/agent-src/rules/php-coding.md +0 -1
  139. package/dist/agent-src/rules/prefer-enums-over-literals.md +0 -1
  140. package/dist/agent-src/rules/preservation-guard.md +0 -1
  141. package/dist/agent-src/rules/provider-lifecycle-discipline.md +2 -3
  142. package/dist/agent-src/rules/question-not-instruction.md +0 -1
  143. package/dist/agent-src/rules/reviewer-awareness.md +0 -1
  144. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +0 -1
  145. package/dist/agent-src/rules/roadmap-progress-sync.md +3 -6
  146. package/dist/agent-src/rules/role-mode-adherence.md +0 -1
  147. package/dist/agent-src/rules/rule-type-governance.md +0 -1
  148. package/dist/agent-src/rules/runtime-safety.md +0 -1
  149. package/dist/agent-src/rules/scale-discipline.md +0 -1
  150. package/dist/agent-src/rules/secret-vcs-guard.md +0 -1
  151. package/dist/agent-src/rules/security-sensitive-stop.md +0 -1
  152. package/dist/agent-src/rules/self-repair-loop.md +0 -1
  153. package/dist/agent-src/rules/senior-engineering-discipline.md +0 -1
  154. package/dist/agent-src/rules/session-canary.md +0 -1
  155. package/dist/agent-src/rules/settings-ask-protocol.md +5 -6
  156. package/dist/agent-src/rules/size-enforcement.md +0 -1
  157. package/dist/agent-src/rules/skill-improvement-trigger.md +2 -3
  158. package/dist/agent-src/rules/skill-quality.md +0 -1
  159. package/dist/agent-src/rules/slash-command-routing-policy.md +0 -1
  160. package/dist/agent-src/rules/source-confidentiality.md +0 -1
  161. package/dist/agent-src/rules/source-discovery-gate.md +0 -1
  162. package/dist/agent-src/rules/source-of-truth.md +0 -1
  163. package/dist/agent-src/rules/spreadsheet-source-quality.md +1 -2
  164. package/dist/agent-src/rules/strategy-safety-floor.md +0 -1
  165. package/dist/agent-src/rules/symfony-routing.md +1 -2
  166. package/dist/agent-src/rules/think-before-action.md +0 -1
  167. package/dist/agent-src/rules/token-budget-discipline.md +14 -48
  168. package/dist/agent-src/rules/token-efficiency.md +0 -1
  169. package/dist/agent-src/rules/token-optimizer-maintenance.md +0 -1
  170. package/dist/agent-src/rules/tool-safety.md +0 -1
  171. package/dist/agent-src/rules/ui-audit-gate.md +1 -2
  172. package/dist/agent-src/rules/untrusted-input-defense.md +0 -1
  173. package/dist/agent-src/rules/upstream-proposal.md +0 -1
  174. package/dist/agent-src/rules/user-interaction.md +0 -1
  175. package/dist/agent-src/rules/user-interrupt-priority.md +0 -1
  176. package/dist/agent-src/scripts/gate_budget.ts +47 -23
  177. package/dist/agent-src/scripts/gate_execute.ts +12 -4
  178. package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +2 -0
  179. package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
  180. package/dist/agent-src/skills/ai-council/SKILL.md +7 -4
  181. package/dist/agent-src/skills/ai-council/references/cost-and-redaction.md +21 -11
  182. package/dist/agent-src/skills/ai-council/references/procedure.md +12 -6
  183. package/dist/agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -1
  184. package/dist/agent-src/skills/bug-analyzer/SKILL.md +1 -1
  185. package/dist/agent-src/skills/canvas-design/SKILL.md +4 -2
  186. package/dist/agent-src/skills/command-routing/SKILL.md +3 -4
  187. package/dist/agent-src/skills/conventional-commits-writing/SKILL.md +127 -139
  188. package/dist/agent-src/skills/corpus-grounding/ATTRIBUTION.md +39 -0
  189. package/dist/agent-src/skills/corpus-grounding/SKILL.md +8 -2
  190. package/dist/agent-src/skills/database/data/manifest.json +2 -2
  191. package/dist/agent-src/skills/database/data/query-tuning.csv +13 -13
  192. package/dist/agent-src/skills/design-intelligence/references/integration-mapping.md +13 -10
  193. package/dist/agent-src/skills/design-system-capture/SKILL.md +0 -3
  194. package/dist/agent-src/skills/estimate-ticket/SKILL.md +1 -1
  195. package/dist/agent-src/skills/fe-design/references/design-patterns.md +49 -2
  196. package/dist/agent-src/skills/feature-planning/SKILL.md +1 -1
  197. package/dist/agent-src/skills/frontend-render-security/SKILL.md +9 -0
  198. package/dist/agent-src/skills/git-workflow/SKILL.md +44 -44
  199. package/dist/agent-src/skills/git-workflow/references/branch-update.md +312 -0
  200. package/dist/agent-src/skills/git-workflow/references/commit-subject.md +205 -0
  201. package/dist/agent-src/skills/iconography/SKILL.md +4 -2
  202. package/dist/agent-src/skills/image-provider-routing/SKILL.md +16 -5
  203. package/dist/agent-src/skills/jira-integration/SKILL.md +3 -3
  204. package/dist/agent-src/skills/logo-generation/SKILL.md +12 -8
  205. package/dist/agent-src/skills/merge-conflicts/SKILL.md +10 -4
  206. package/dist/agent-src/skills/po-discovery/SKILL.md +1 -1
  207. package/dist/agent-src/skills/refine-ticket/SKILL.md +4 -4
  208. package/dist/agent-src/skills/roadmap-writing/SKILL.md +2 -2
  209. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +4 -2
  210. package/dist/agent-src/skills/source-discovery/SKILL.md +1 -1
  211. package/dist/agent-src/skills/svg-authoring/SKILL.md +318 -0
  212. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +3 -1
  213. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +1 -1
  214. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +1 -1
  215. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +2 -2
  216. package/dist/agent-src/skills/wireframe/SKILL.md +5 -2
  217. package/dist/agent-src/templates/AGENTS.md +1 -1
  218. package/dist/agent-src/templates/agent-settings.md +46 -129
  219. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +3 -3
  220. package/dist/agent-src/templates/command.md +2 -2
  221. package/dist/agent-src/templates/roadmaps.md +6 -1
  222. package/dist/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +0 -6
  223. package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +11 -7
  224. package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.ts +3 -2
  225. package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.ts +2 -1
  226. package/dist/agent-src/templates/scripts/work_engine/hooks/settings.ts +0 -6
  227. package/dist/agent-src/templates/scripts/work_engine/scoring/decision_engine.ts +9 -12
  228. package/dist/agent-src/templates/scripts/work_engine/scoring/memory_visibility.ts +2 -1
  229. package/dist/agent-src/templates/skill.md +1 -1
  230. package/dist/cli/commands/uiRender.js +201 -4
  231. package/dist/cli/commands/uiRenderAsset.js +1308 -0
  232. package/dist/cli/commands/uiRenderAssetCompare.js +752 -0
  233. package/dist/cli/commands/uiRenderAssetGrid.js +265 -0
  234. package/dist/cli/commands/uiRenderAssetGrounds.js +323 -0
  235. package/dist/cli/commands/uiRenderAssetIsolation.js +85 -0
  236. package/dist/cli/commands/uiRenderAssetTypes.js +145 -0
  237. package/dist/cli/main.js +47 -1
  238. package/dist/cli/registry.js +2 -1
  239. package/dist/cli-delegate/check_update_banner.js +195 -119
  240. package/dist/cli-delegate/{chunk-S4UMZNES.js → chunk-4DBG5K7M.js} +5 -5
  241. package/dist/cli-delegate/{chunk-TRI6DQZ6.js → chunk-5GPULQBE.js} +60 -42
  242. package/dist/cli-delegate/{chunk-JQVYKIZA.js → chunk-6SRJTYJS.js} +33 -177
  243. package/dist/cli-delegate/chunk-6V5FMA3P.js +186 -0
  244. package/dist/cli-delegate/chunk-6YQC2HLF.js +415 -0
  245. package/dist/cli-delegate/{chunk-E4XIZDIB.js → chunk-7HKRE54K.js} +1 -64
  246. package/dist/cli-delegate/{chunk-7HQ52DHU.js → chunk-AL3RZ22D.js} +1 -1
  247. package/dist/cli-delegate/{chunk-DBH4AHDK.js → chunk-ED7KCMLB.js} +9 -11
  248. package/dist/cli-delegate/{chunk-NXEILF2K.js → chunk-F3QDG2GT.js} +481 -72
  249. package/dist/cli-delegate/{chunk-CQWX336L.js → chunk-G5VVIZRC.js} +3 -3
  250. package/dist/cli-delegate/{chunk-JY2I2DXV.js → chunk-H76PHZBI.js} +137 -60
  251. package/dist/cli-delegate/chunk-IAYTIIQW.js +67 -0
  252. package/dist/cli-delegate/{chunk-LIYTLH6O.js → chunk-KXXWJTDO.js} +5 -10
  253. package/dist/cli-delegate/{chunk-T7L2QK3K.js → chunk-LQOCRFWT.js} +1 -1
  254. package/dist/cli-delegate/{chunk-S4YWNRKO.js → chunk-MDPPKQCQ.js} +34 -1
  255. package/dist/cli-delegate/chunk-OAFSJBHH.js +272 -0
  256. package/dist/cli-delegate/{chunk-GKE7ZVFA.js → chunk-PWX7CKMD.js} +1 -1
  257. package/dist/cli-delegate/{chunk-NDJLNC6Q.js → chunk-T6BP34WI.js} +1 -0
  258. package/dist/cli-delegate/{chunk-RQNNW7IF.js → chunk-T7XSJSUQ.js} +433 -632
  259. package/dist/cli-delegate/{chunk-NQ7WT43P.js → chunk-TMWYUOK4.js} +75 -12
  260. package/dist/cli-delegate/{chunk-EEHDXSYD.js → chunk-WQWXGIWB.js} +12 -273
  261. package/dist/cli-delegate/{chunk-QLDHIPWR.js → chunk-XQTPXKFA.js} +1 -1
  262. package/dist/cli-delegate/cmd_analyze_session.js +223 -43
  263. package/dist/cli-delegate/cmd_brand_status.js +3 -2
  264. package/dist/cli-delegate/cmd_conformance.js +489 -238
  265. package/dist/cli-delegate/cmd_converge.js +3 -3
  266. package/dist/cli-delegate/cmd_doctor.js +15 -12
  267. package/dist/cli-delegate/cmd_doctor_neighbours.js +11 -4
  268. package/dist/cli-delegate/cmd_explain.js +43 -19
  269. package/dist/cli-delegate/cmd_export.js +10 -10
  270. package/dist/cli-delegate/cmd_git_convention.js +1851 -0
  271. package/dist/cli-delegate/cmd_handoff.js +40 -21
  272. package/dist/cli-delegate/cmd_mcp_available.js +3 -2
  273. package/dist/cli-delegate/cmd_migrate.js +3 -3
  274. package/dist/cli-delegate/cmd_packs_active.js +4 -3
  275. package/dist/cli-delegate/cmd_prune.js +10 -10
  276. package/dist/cli-delegate/cmd_route_audit.js +7 -7
  277. package/dist/cli-delegate/cmd_route_explain.js +5 -5
  278. package/dist/cli-delegate/cmd_session_recycle.js +9 -7
  279. package/dist/cli-delegate/cmd_settings_check.js +54 -7
  280. package/dist/cli-delegate/cmd_settings_get.js +18 -8
  281. package/dist/cli-delegate/cmd_settings_migrate.js +10 -10
  282. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  283. package/dist/cli-delegate/cmd_sync.js +10 -10
  284. package/dist/cli-delegate/cmd_uninstall.js +10 -10
  285. package/dist/cli-delegate/cmd_update.js +7 -7
  286. package/dist/cli-delegate/cmd_upgrade.js +7 -7
  287. package/dist/cli-delegate/cmd_validate.js +10 -10
  288. package/dist/cli-delegate/cmd_versions.js +5 -5
  289. package/dist/cli-delegate/gate_budget.js +1 -1
  290. package/dist/cli-delegate/gate_execute.js +2 -2
  291. package/dist/cli-delegate/roadmap_gates.js +2 -2
  292. package/dist/discovery/deprecation-report.md +1 -1
  293. package/dist/discovery/discovery-manifest.json +171 -107
  294. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  295. package/dist/discovery/discovery-manifest.summary.md +7 -7
  296. package/dist/discovery/dormancy-report.md +1 -1
  297. package/dist/discovery/orphan-report.md +1 -1
  298. package/dist/discovery/packs.json +41 -22
  299. package/dist/discovery/trust-report.md +3 -3
  300. package/dist/discovery/workspaces.json +16 -11
  301. package/dist/hooks/dispatch.js +2520 -1924
  302. package/dist/hooks/dispatch.sha256 +1 -1
  303. package/dist/hooks/svg_rules.chunk.cjs +1626 -0
  304. package/dist/hooks/svg_rules.chunk.cjs.sha256 +1 -0
  305. package/dist/install/install.mjs +462 -431
  306. package/dist/install/installThinLayer.js +40 -12
  307. package/dist/install/installedRuleLinks.js +105 -19
  308. package/dist/mcp/awesome-mcp-servers.row.md +1 -1
  309. package/dist/mcp/mcp-cloudflare-catalogue.json +2 -2
  310. package/dist/mcp/registry-manifest.json +5 -5
  311. package/dist/mcp/server.json +2 -2
  312. package/dist/mcp/server.mjs +3063 -2160
  313. package/dist/router.json +1 -1
  314. package/dist/scripts/_lib/agent_settings.js +15 -120
  315. package/dist/scripts/_lib/compile_time_toggles.js +12 -21
  316. package/dist/scripts/_lib/git_convention.js +232 -0
  317. package/dist/scripts/_lib/global_deploy_inventory.js +730 -0
  318. package/dist/scripts/_lib/installed_layer.js +344 -18
  319. package/dist/scripts/_lib/package_root.js +68 -0
  320. package/dist/scripts/_lib/rule_law_section.js +1 -1
  321. package/dist/scripts/_lib/scan_scope.js +105 -0
  322. package/dist/scripts/_lib/settings_removed_keys.js +168 -0
  323. package/dist/scripts/_lib/svg_dom.js +284 -0
  324. package/dist/scripts/_lib/svg_geometry.js +477 -0
  325. package/dist/scripts/_lib/svg_motion.js +179 -0
  326. package/dist/scripts/_lib/svg_rules.js +1337 -0
  327. package/dist/scripts/_lib/svg_urls.js +67 -0
  328. package/dist/scripts/_lib/thin_rules.js +80 -3
  329. package/dist/scripts/lint_svg_asset.js +324 -0
  330. package/dist/server/app.js +2 -1
  331. package/dist/server/gitKeysGate.js +105 -0
  332. package/dist/server/routes/settings.js +67 -8
  333. package/dist/server/routes/wizard.js +4 -0
  334. package/dist/server/schemas/settings.js +23 -81
  335. package/dist/shared/skillRanking.js +30 -4
  336. package/dist/ui/assets/{index-Cwq4d0hX.js → index-C3djEJah.js} +2 -2
  337. package/dist/ui/index.html +1 -1
  338. package/docs/MIGRATION.md +72 -1
  339. package/docs/contracts/settings-classes.md +35 -65
  340. package/docs/guidelines/agent-infra/active-remediation-mechanics.md +39 -0
  341. package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +1 -1
  342. package/docs/guidelines/agent-infra/cross-source-consistency-mechanics.md +8 -10
  343. package/docs/guidelines/agent-infra/frontier-reasoning-operating-profile.md +3 -2
  344. package/docs/guidelines/agent-infra/layered-settings.md +13 -1
  345. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  346. package/docs/guidelines/agent-infra/self-improvement-pipeline.md +10 -3
  347. package/docs/guidelines/agent-infra/size-and-scope.md +36 -0
  348. package/docs/guidelines/design-antipatterns.md +1 -1
  349. package/docs/guidelines/php/git.md +15 -2
  350. package/package.json +4 -3
  351. package/src/agent-src/scripts/gate_budget.ts +47 -23
  352. package/src/agent-src/scripts/gate_execute.ts +12 -4
  353. package/src/agent-src/templates/AGENTS.md +1 -1
  354. package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +0 -6
  355. package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +11 -7
  356. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.ts +3 -2
  357. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.ts +2 -1
  358. package/src/agent-src/templates/scripts/work_engine/hooks/settings.ts +0 -6
  359. package/src/agent-src/templates/scripts/work_engine/scoring/decision_engine.ts +9 -12
  360. package/src/agent-src/templates/scripts/work_engine/scoring/memory_visibility.ts +2 -1
  361. package/src/config/agent-settings.template.yml +79 -312
  362. package/src/config/agents-paths.yml +16 -0
  363. package/src/config/assurance-capability-registry.json +7 -3
  364. package/src/config/ci-local-parity.yml +3 -0
  365. package/src/config/continuity-surface.json +3 -3
  366. package/src/config/discovery/packs.yml +17 -1
  367. package/src/config/evaluator-budgets.json +19 -6
  368. package/src/config/gate-coverage.yml +65 -23
  369. package/src/config/gate-violation-baselines.json +56 -16
  370. package/src/config/gitignore-block.txt +19 -0
  371. package/src/config/hook-bundle-budget.json +1 -1
  372. package/src/config/release-gate-locality.yml +2 -1
  373. package/src/config/routing-coverage-seed.json +55 -6
  374. package/src/config/rule-law-ceilings.json +12 -6
  375. package/src/config/rule-obligations.json +833 -0
  376. package/src/install/installThinLayer.ts +76 -10
  377. package/src/install/installedRuleLinks.ts +105 -17
  378. package/src/scripts/_cli/cmd_analyze_session.ts +9 -15
  379. package/src/scripts/_cli/cmd_conformance.ts +13 -2
  380. package/src/scripts/_cli/cmd_doctor.ts +10 -10
  381. package/src/scripts/_cli/cmd_doctor_neighbours.ts +133 -0
  382. package/src/scripts/_cli/cmd_explain.ts +72 -14
  383. package/src/scripts/_cli/cmd_git_convention.ts +637 -0
  384. package/src/scripts/_cli/cmd_settings_check.ts +66 -8
  385. package/src/scripts/_cli/cmd_settings_get.ts +18 -3
  386. package/src/scripts/_cli/doctor_execution.ts +72 -1
  387. package/src/scripts/_dispatch.bash +37 -2
  388. package/src/scripts/_lib/agent_settings.ts +15 -124
  389. package/src/scripts/_lib/blocked_by_marker.ts +1 -1
  390. package/src/scripts/_lib/compile_time_toggles.ts +12 -21
  391. package/src/scripts/_lib/dispatch_import_closure.ts +163 -0
  392. package/src/scripts/_lib/git_base_ref.ts +92 -0
  393. package/src/scripts/_lib/git_convention.ts +291 -0
  394. package/src/scripts/_lib/git_convention_carrier.ts +389 -0
  395. package/src/scripts/_lib/git_convention_grammar.ts +246 -0
  396. package/src/scripts/_lib/git_convention_measure.ts +344 -0
  397. package/src/scripts/_lib/global_deploy_inventory.ts +74 -0
  398. package/src/scripts/_lib/graph_feeder_record.ts +236 -13
  399. package/src/scripts/_lib/installed_layer.ts +448 -23
  400. package/src/scripts/_lib/knowledge_global.ts +14 -1
  401. package/src/scripts/_lib/module_reach.ts +456 -0
  402. package/src/scripts/_lib/neighbour_contradictions.ts +236 -0
  403. package/src/scripts/_lib/neighbour_tool_use.ts +8 -10
  404. package/src/scripts/_lib/ratification_artifact.ts +220 -1
  405. package/src/scripts/_lib/ratification_subject.ts +92 -0
  406. package/src/scripts/_lib/release_findings_ingest.ts +15 -7
  407. package/src/scripts/_lib/release_highlights.ts +5 -0
  408. package/src/scripts/_lib/rule_law_section.ts +1 -1
  409. package/src/scripts/_lib/rule_obligations.ts +205 -0
  410. package/src/scripts/_lib/self_repair.ts +1 -1
  411. package/src/scripts/_lib/settings_removed_keys.ts +173 -0
  412. package/src/scripts/_lib/svg_dom.ts +318 -0
  413. package/src/scripts/_lib/svg_geometry.ts +480 -0
  414. package/src/scripts/_lib/svg_motion.ts +208 -0
  415. package/src/scripts/_lib/svg_rules.ts +1453 -0
  416. package/src/scripts/_lib/svg_urls.ts +72 -0
  417. package/src/scripts/_lib/test_red_state.ts +5 -0
  418. package/src/scripts/_lib/thin_rules.ts +83 -3
  419. package/src/scripts/_lib/touched_file_quality.ts +142 -6
  420. package/src/scripts/_lib/verification_command.ts +59 -0
  421. package/src/scripts/_lib/verify_clause.ts +27 -17
  422. package/src/scripts/_lib/worker_budget.ts +1 -1
  423. package/src/scripts/adr/regenerate_index.ts +16 -1
  424. package/src/scripts/ai_council/config.ts +7 -7
  425. package/src/scripts/ai_council/modification_review.ts +100 -0
  426. package/src/scripts/ai_council/modification_review_questions.ts +90 -0
  427. package/src/scripts/ai_council/orchestrator.ts +5 -5
  428. package/src/scripts/ai_council/spend_gate.ts +18 -7
  429. package/src/scripts/ai_team/config.ts +11 -16
  430. package/src/scripts/audit_user_type_axis.ts +20 -8
  431. package/src/scripts/bench_ab_clone.ts +1 -2
  432. package/src/scripts/bench_concern_spawn_path.ts +129 -0
  433. package/src/scripts/bench_graph_feeder_latency.ts +584 -0
  434. package/src/scripts/check_agent_artifact_location.ts +36 -0
  435. package/src/scripts/check_branch_freshness.ts +147 -24
  436. package/src/scripts/check_enforcement_coverage.ts +130 -0
  437. package/src/scripts/check_evaluator_budgets.ts +4 -0
  438. package/src/scripts/check_finding_dispositions.ts +88 -25
  439. package/src/scripts/check_gate_coverage.ts +4 -3
  440. package/src/scripts/check_gate_reachability.ts +48 -1
  441. package/src/scripts/check_generator_sync.ts +48 -0
  442. package/src/scripts/check_hook_bundle_composition.ts +107 -0
  443. package/src/scripts/check_kernel_edit_ratified.ts +86 -6
  444. package/src/scripts/check_pack_size.ts +1 -1
  445. package/src/scripts/check_release_manifest_content.ts +189 -0
  446. package/src/scripts/check_standing_rule_delivery.ts +96 -0
  447. package/src/scripts/check_suppression_hygiene.ts +2 -5
  448. package/src/scripts/ci_settle.ts +42 -5
  449. package/src/scripts/classify_merge_risk.ts +320 -0
  450. package/src/scripts/closure_scan.ts +91 -1
  451. package/src/scripts/command_suggester/settings.ts +8 -108
  452. package/src/scripts/command_suggester/types.ts +4 -5
  453. package/src/scripts/consumer_matrix.ts +102 -0
  454. package/src/scripts/context_hygiene_hook.ts +13 -23
  455. package/src/scripts/cost/preflight.mjs +12 -1
  456. package/src/scripts/council_cli.ts +7 -7
  457. package/src/scripts/dispatch_r2_reviewer.ts +24 -11
  458. package/src/scripts/explain_run.ts +8 -13
  459. package/src/scripts/generate_git_convention_grammar.ts +127 -0
  460. package/src/scripts/generate_host_cost_table.ts +20 -3
  461. package/src/scripts/handoff_context_hook.ts +86 -37
  462. package/src/scripts/hook_manifest.json +1 -1
  463. package/src/scripts/hook_manifest.yaml +25 -6
  464. package/src/scripts/hooks/block_config_weakening.ts +48 -5
  465. package/src/scripts/hooks/block_plumbing_writes.ts +8 -4
  466. package/src/scripts/hooks/block_speaking_inbox_dir.ts +16 -1
  467. package/src/scripts/hooks/concern_registry.ts +2 -0
  468. package/src/scripts/hooks/delegation_nudge_hook.ts +2 -3
  469. package/src/scripts/hooks/design_pass_hook.ts +157 -3
  470. package/src/scripts/hooks/evidence_independence.ts +12 -4
  471. package/src/scripts/hooks/mcp_usage_observation_hook.ts +121 -0
  472. package/src/scripts/hooks/push_settle_hook.ts +4 -3
  473. package/src/scripts/hooks/rule_inject_hook.ts +44 -3
  474. package/src/scripts/hooks/state_io.ts +8 -4
  475. package/src/scripts/hooks/svg_obligation.ts +497 -0
  476. package/src/scripts/hooks/svg_rules_loader.ts +133 -0
  477. package/src/scripts/hooks/telemetry_usage_hook.ts +7 -46
  478. package/src/scripts/hooks/turn_end_gate_hook.ts +6 -3
  479. package/src/scripts/install-hooks.sh +42 -14
  480. package/src/scripts/install.ts +7 -31
  481. package/src/scripts/lint_blocker_citations.ts +198 -0
  482. package/src/scripts/lint_consolidation_lineage.ts +6 -3
  483. package/src/scripts/lint_design_quality.ts +60 -12
  484. package/src/scripts/lint_evidence_artifacts.ts +9 -0
  485. package/src/scripts/lint_hook_concern_budget.ts +10 -21
  486. package/src/scripts/lint_motion_authority_drift.ts +27 -0
  487. package/src/scripts/lint_pack_boundaries.ts +406 -28
  488. package/src/scripts/lint_roadmap_blockers.ts +282 -29
  489. package/src/scripts/lint_roadmap_complexity.ts +4 -2
  490. package/src/scripts/lint_rule_law_section.ts +151 -21
  491. package/src/scripts/lint_rule_skill_pack_reach.ts +17 -0
  492. package/src/scripts/lint_settings_classes.ts +1 -1
  493. package/src/scripts/lint_svg_asset.ts +365 -0
  494. package/src/scripts/lint_ticked_unmet_criteria.ts +165 -0
  495. package/src/scripts/lint_workflow_security.ts +12 -1
  496. package/src/scripts/measure_skill_ranker_baseline.ts +5 -0
  497. package/src/scripts/model_rule_injection.ts +3 -3
  498. package/src/scripts/onboarding_gate_hook.ts +7 -6
  499. package/src/scripts/print_required_checks.ts +15 -3
  500. package/src/scripts/probe_host_compliance.ts +33 -5
  501. package/src/scripts/probe_turnaround.ts +183 -3
  502. package/src/scripts/ratification_header.ts +77 -0
  503. package/src/scripts/refusal_envelope.ts +138 -0
  504. package/src/scripts/release_publication.ts +29 -3
  505. package/src/scripts/report_dispatch_import_closure.ts +76 -0
  506. package/src/scripts/report_installed_rule_links.ts +118 -1
  507. package/src/scripts/report_module_reach.ts +212 -0
  508. package/src/scripts/report_obligation_mechanism.ts +16 -18
  509. package/src/scripts/report_parked_blockers.ts +222 -0
  510. package/src/scripts/report_required_checks_drift.ts +196 -0
  511. package/src/scripts/report_skill_ranker_confusion.ts +103 -2
  512. package/src/scripts/report_standing_payload_by_host.ts +1 -1
  513. package/src/scripts/routing_doctor.ts +2 -2
  514. package/src/scripts/schemas/agent-settings.schema.json +17 -31
  515. package/src/scripts/schemas/command.schema.json +1 -1
  516. package/src/scripts/schemas/skill.schema.json +1 -1
  517. package/src/scripts/self_review_gate.ts +39 -2
  518. package/src/scripts/skill_tools/score_skill_relevance.ts +28 -19
  519. package/src/scripts/sync_pr_branch.ts +403 -132
  520. package/src/scripts/trigger_eval_grandfather.json +0 -16
  521. package/src/server/schemas/settings.ts +33 -177
  522. package/src/shared/skillRanking.ts +53 -4
  523. package/dist/cli-delegate/chunk-NYX3L2PZ.js +0 -99
  524. package/src/scripts/_lib/config_chain.ts +0 -477
  525. package/src/scripts/_lib/conformance_report.ts +0 -403
  526. package/src/scripts/_lib/eval_discrimination.ts +0 -95
  527. package/src/scripts/_lib/evaluator_promotion.ts +0 -225
  528. package/src/scripts/_lib/file_slicer.ts +0 -117
  529. package/src/scripts/_lib/legacy_boundary_map.ts +0 -228
  530. package/src/scripts/_lib/minimality_tiebreak.ts +0 -168
  531. package/src/scripts/_lib/overbuild_lens_contract.ts +0 -196
  532. package/src/scripts/_lib/role_split.ts +0 -338
@@ -0,0 +1,389 @@
1
+ /**
2
+ * The committed git convention: `.git-convention.yml` at the repository root
3
+ * (ADR-283), layered over the developer settings files.
4
+ *
5
+ * The developer files are gitignored, so a convention declared only there
6
+ * reaches the checkout it was written in and no worktree, fresh clone or CI
7
+ * run. The carrier is tracked, and where it is read from is the authority:
8
+ *
9
+ * - `update_strategy` at the resolved TARGET commit — the explicit `--base`
10
+ * when given, else the default branch — as the server reports it. The
11
+ * strategy that judges a pull request must not come from that pull request,
12
+ * the same ruling `branch_convergence.ts` records; a value on the branch is a
13
+ * candidate and is never adopted.
14
+ * - `commit_format` and `branch_pattern` at `HEAD`, at the repository root,
15
+ * because they shape the branch's own commits and names.
16
+ *
17
+ * In both cases the carrier is the highest-precedence layer, over every
18
+ * developer layer including the gitignored local one (the owner's D8), and a
19
+ * carrier that does not set a key leaves it to the developer layers read at
20
+ * the repository root, never per subdirectory. A target commit that cannot be
21
+ * resolved is `unresolvable`, never the default.
22
+ *
23
+ * The target is never inferred from a forge. A caller acting on a pull request
24
+ * already knows its base and passes it as `--base`; the default branch is the
25
+ * answer only for an operation that is not about a pull request. Inferring the
26
+ * base downstream took seven review rounds without converging, and a wrong
27
+ * inference reads a valid carrier from the wrong commit and exits 0.
28
+ */
29
+ import { spawnSync } from 'node:child_process';
30
+ import * as fs from 'node:fs';
31
+ import * as path from 'node:path';
32
+
33
+ import { find_project_root } from './agent_settings.js';
34
+ import { parseBaseRef, splitResolvedRef } from './git_base_ref.js';
35
+ import {
36
+ GIT_CONVENTION_KEYS,
37
+ checkoutSource,
38
+ conventionReading,
39
+ parseLayerText,
40
+ readGitConventionKey,
41
+ type GitConventionKey,
42
+ type GitConventionLayer,
43
+ type GitConventionReading,
44
+ } from './git_convention.js';
45
+
46
+ /** The tracked repository-root file a team declares its git convention in. */
47
+ export const CARRIER_PATH = '.git-convention.yml';
48
+
49
+ /** Sized for a ref lookup (`ls-remote`), which transfers no objects. */
50
+ export const NETWORK_TIMEOUT_MS = 8_000;
51
+
52
+ /**
53
+ * The target-commit fetch transfers objects, and the target is routinely a
54
+ * commit the server has advanced past the local fetch, so a large repository
55
+ * needs longer than a ref lookup does. Running out of time here makes the
56
+ * reading `unresolvable` and stops a sync, so the bound is generous.
57
+ */
58
+ export const CARRIER_FETCH_TIMEOUT_MS = 60_000;
59
+
60
+ export interface GitRunResult {
61
+ ok: boolean;
62
+ out: string;
63
+ err: string;
64
+ timedOut: boolean;
65
+ }
66
+
67
+ export type GitRunner = (cmd: string, args: readonly string[], cwd: string, timeoutMs: number) => GitRunResult;
68
+
69
+ export const runGit: GitRunner = (cmd, args, cwd, timeoutMs) => {
70
+ const r = spawnSync(cmd, [...args], { cwd, encoding: 'utf-8', timeout: timeoutMs, maxBuffer: 32 * 1024 * 1024 });
71
+ const code = (r.error as NodeJS.ErrnoException | undefined)?.code;
72
+ return { ok: r.status === 0, out: r.stdout ?? '', err: (r.stderr ?? '').trim(), timedOut: code === 'ETIMEDOUT' };
73
+ };
74
+
75
+ function sh(cmd: string, args: readonly string[], cwd: string): { ok: boolean; out: string; err: string } {
76
+ return runGit(cmd, args, cwd, NETWORK_TIMEOUT_MS);
77
+ }
78
+
79
+ /** The git questions target resolution asks, injected so no network is needed in tests. */
80
+ export interface TargetDeps {
81
+ /** The default branch as a remote-tracking ref (`origin/main`), or null. */
82
+ readonly defaultBranch: () => string | null;
83
+ /** The SHA the server reports for a ref right now, or null. */
84
+ readonly remoteSha: (ref: string) => string | null;
85
+ /** The configured remote names; without it only `origin/` marks a remote. */
86
+ readonly remotes?: () => readonly string[];
87
+ /** The default branch another remote's server names (`upstream/main`), or null. */
88
+ readonly defaultBranchOf?: (remote: string) => string | null;
89
+ }
90
+
91
+ export type TargetReason = 'explicit-base-override' | 'repository-default-branch';
92
+
93
+ export interface TargetRef {
94
+ readonly ref: string;
95
+ readonly reason: TargetReason;
96
+ }
97
+
98
+ /**
99
+ * The ref a branch is judged against: `--base`, else the default branch.
100
+ * `defaultRef` is passed by a caller that already asked for it.
101
+ */
102
+ export function resolveTarget(deps: TargetDeps, override: string | null, defaultRef?: string | null): TargetRef | null {
103
+ // A bare name is the branch on origin: the local branch of that name is a
104
+ // clone-time copy nothing keeps current.
105
+ const given = override === null ? null : parseBaseRef(override, deps.remotes?.() ?? []);
106
+ if (given !== null) return { ref: given.ref, reason: 'explicit-base-override' };
107
+ const def = defaultRef === undefined ? deps.defaultBranch() : defaultRef;
108
+ return def === null ? null : { ref: def, reason: 'repository-default-branch' };
109
+ }
110
+
111
+ /**
112
+ * The default branch the SERVER reports, from `git ls-remote --symref origin HEAD`.
113
+ *
114
+ * Pure and exported so the parse is testable without a network round trip, and
115
+ * because it closes a measured defect rather than a hypothetical one. Until
116
+ * 2026-09-03 the default branch was read from `refs/remotes/origin/HEAD` alone —
117
+ * a clone-time ref that is simply absent in some checkouts. Measured in a
118
+ * worktree of this repository the same day: the local form returned null, so
119
+ * `sync_pr_branch` refused with "no open PR and no origin/HEAD" while
120
+ * `check_branch_freshness`, three lines later in the same documented sequence,
121
+ * resolved `origin/main` from the server symref and passed. Mirrors
122
+ * `check_branch_freshness.ts:223`.
123
+ */
124
+ export function parseSymrefDefault(lsRemoteOut: string): string | null {
125
+ for (const line of lsRemoteOut.split('\n')) {
126
+ const t = line.trim();
127
+ if (!t.startsWith('ref:')) continue;
128
+ const name = t.slice('ref:'.length).trim().split(/\s+/)[0] ?? '';
129
+ if (!name.startsWith('refs/heads/')) continue;
130
+ const short = name.slice('refs/heads/'.length).trim();
131
+ if (short !== '') return `origin/${short}`;
132
+ }
133
+ return null;
134
+ }
135
+
136
+ /**
137
+ * The SHA of exactly `refs/heads/<branch>` in `git ls-remote` output.
138
+ *
139
+ * A ls-remote pattern matches every ref whose trailing path components equal
140
+ * it and the output is sorted by refname, so `backport/main` is listed before
141
+ * `main`: the first line is not the branch that was asked for. A SHA-256
142
+ * repository reports 64 hex digits, a SHA-1 one 40.
143
+ */
144
+ export function parseExactHeadSha(lsRemoteOut: string, branch: string): string | null {
145
+ const want = `refs/heads/${branch}`;
146
+ for (const line of lsRemoteOut.split('\n')) {
147
+ const [sha, name] = line.trim().split(/\s+/);
148
+ if (name === want && sha !== undefined && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(sha)) return sha;
149
+ }
150
+ return null;
151
+ }
152
+
153
+ export function makeTargetDeps(repo: string, run: GitRunner = runGit): TargetDeps {
154
+ const ask = (cmd: string, args: readonly string[]): GitRunResult => run(cmd, args, repo, NETWORK_TIMEOUT_MS);
155
+ return {
156
+ // The server first, the local ref second: `refs/remotes/origin/HEAD` is
157
+ // not set in every checkout (see `parseSymrefDefault`).
158
+ defaultBranch: (): string | null => {
159
+ const remote = ask('git', ['ls-remote', '--symref', '--', 'origin', 'HEAD']);
160
+ const fromServer = remote.ok ? parseSymrefDefault(remote.out) : null;
161
+ if (fromServer !== null) return fromServer;
162
+ const head = ask('git', ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']);
163
+ return head.ok && head.out.trim() !== '' ? head.out.trim() : null;
164
+ },
165
+ remoteSha: (ref: string): string | null => {
166
+ const { remote, branch } = splitResolvedRef(ref);
167
+ const out = ask('git', ['ls-remote', '--', remote, `refs/heads/${branch}`]);
168
+ return out.ok ? parseExactHeadSha(out.out, branch) : null;
169
+ },
170
+ remotes: (): readonly string[] => {
171
+ const out = ask('git', ['remote']);
172
+ return out.ok ? out.out.split('\n').map((r) => r.trim()).filter((r) => r !== '') : [];
173
+ },
174
+ defaultBranchOf: (remote: string): string | null => {
175
+ const out = ask('git', ['ls-remote', '--symref', '--', remote, 'HEAD']);
176
+ const named = out.ok ? parseSymrefDefault(out.out) : null;
177
+ return named === null ? null : `${remote}/${named.slice('origin/'.length)}`;
178
+ },
179
+ };
180
+ }
181
+
182
+ /**
183
+ * The same questions, each asked of git at most once. One run reads the
184
+ * strategy and then syncs; without this each step asked again, paying the
185
+ * network twice and able to judge the strategy against one commit and sync
186
+ * against another when the target moves in between.
187
+ */
188
+ export function memoTargetDeps<T extends TargetDeps>(deps: T): T {
189
+ const cache = new Map<string, unknown>();
190
+ const once = <V>(key: string, ask: () => V): V => {
191
+ if (!cache.has(key)) cache.set(key, ask());
192
+ return cache.get(key) as V;
193
+ };
194
+ return {
195
+ ...deps,
196
+ defaultBranch: (): string | null => once('default', () => deps.defaultBranch()),
197
+ remoteSha: (ref: string): string | null => once(`sha ${ref}`, () => deps.remoteSha(ref)),
198
+ ...(deps.remotes === undefined ? {} : { remotes: (): readonly string[] => once('remotes', () => (deps.remotes as () => readonly string[])()) }),
199
+ ...(deps.defaultBranchOf === undefined
200
+ ? {}
201
+ : { defaultBranchOf: (remote: string): string | null => once(`default ${remote}`, () => (deps.defaultBranchOf as (r: string) => string | null)(remote)) }),
202
+ };
203
+ }
204
+
205
+ /**
206
+ * `absent` (the commit has no carrier) and `no-commit` (the commit itself is
207
+ * unknown) are different facts; `timedOut` marks a commit the fetch ran out of
208
+ * time on, which says nothing about whether it exists.
209
+ */
210
+ export type CommitBlob = { kind: 'absent' } | { kind: 'present'; text: string } | { kind: 'no-commit'; timedOut?: boolean };
211
+
212
+ /**
213
+ * The carrier as one commit holds it. A commit the server reported but this
214
+ * clone has not fetched yet is fetched by its ref once; an answer of "the file
215
+ * does not exist" is only ever given for a commit that is present.
216
+ */
217
+ export function carrierBlobAt(repo: string, sha: string, fetchRef: string | null, run: GitRunner = runGit): CommitBlob {
218
+ const local = (args: readonly string[]): GitRunResult => run('git', args, repo, NETWORK_TIMEOUT_MS);
219
+ const has = (): boolean => local(['cat-file', '-e', `${sha}^{commit}`]).ok;
220
+ if (!has()) {
221
+ const from = fetchRef === null ? null : splitResolvedRef(fetchRef);
222
+ const fetched = from === null ? null : run('git', ['fetch', '-q', '--', from.remote, from.branch], repo, CARRIER_FETCH_TIMEOUT_MS);
223
+ if (fetched?.timedOut === true) return { kind: 'no-commit', timedOut: true };
224
+ if (!has()) return { kind: 'no-commit' };
225
+ }
226
+ const listed = local(['ls-tree', '--name-only', sha, '--', CARRIER_PATH]);
227
+ if (!listed.ok) return { kind: 'no-commit' };
228
+ if (listed.out.trim() === '') return { kind: 'absent' };
229
+ const blob = local(['cat-file', '-p', `${sha}:${CARRIER_PATH}`]);
230
+ return blob.ok ? { kind: 'present', text: blob.out } : { kind: 'no-commit' };
231
+ }
232
+
233
+ /** A filename a carrier is sometimes given that nothing reads. */
234
+ export const IGNORED_CARRIER_PATH = '.git-convention.yaml';
235
+
236
+ /** The warning every reader prints when `root` holds a carrier under the name nothing reads, or null. */
237
+ export function ignoredCarrierWarning(root: string): string | null {
238
+ return fs.existsSync(path.join(root, IGNORED_CARRIER_PATH))
239
+ ? `${IGNORED_CARRIER_PATH} is ignored — the team declaration is read only from ${CARRIER_PATH}; rename it to ${CARRIER_PATH}`
240
+ : null;
241
+ }
242
+
243
+ /**
244
+ * The carrier holds its keys under `git:`, as the settings files do. Keys set at
245
+ * the top level are read by nothing, and reading them as absent would apply the
246
+ * default the file was written to replace.
247
+ */
248
+ function _parseCarrier(text: string): Pick<GitConventionLayer, 'parsed' | 'data' | 'why' | 'shapeError'> {
249
+ const parsed = parseLayerText(text);
250
+ if (parsed.parsed !== 'valid') return parsed;
251
+ const top = Object.keys(parsed.data as Record<string, unknown>).filter((k) => (GIT_CONVENTION_KEYS as readonly string[]).includes(k));
252
+ if (top.length === 0) return parsed;
253
+ const listed = top.map((k) => `\`${k}\``).join(', ');
254
+ return { ...parsed, shapeError: `${listed} set at the top level; the carrier reads its keys under \`git:\` (\`git:\` on one line, then \` ${top[0]}: <value>\` indented)` };
255
+ }
256
+
257
+ function _carrierLayer(label: string, blob: Exclude<CommitBlob, { kind: 'no-commit' }>): GitConventionLayer {
258
+ if (blob.kind === 'absent') return { path: label, carries: true, parsed: 'absent', data: null };
259
+ return { path: label, carries: true, ..._parseCarrier(blob.text) };
260
+ }
261
+
262
+ /** Read failures map as `git_convention.ts` maps them for the developer files. */
263
+ function _workingTreeLayer(root: string): GitConventionLayer {
264
+ const p = path.join(root, CARRIER_PATH);
265
+ let text: string;
266
+ try {
267
+ if (!fs.statSync(p).isFile()) return { path: p, carries: true, parsed: 'absent', data: null };
268
+ text = fs.readFileSync(p, 'utf-8');
269
+ } catch (err) {
270
+ const parsed = (err as NodeJS.ErrnoException).code === 'ENOENT' ? 'absent' : 'malformed';
271
+ return { path: p, carries: true, parsed, data: null };
272
+ }
273
+ return { path: p, carries: true, ..._parseCarrier(text) };
274
+ }
275
+
276
+ /** The repository root, and whether it is a git repository at all. */
277
+ export function conventionRoot(cwd: string): { root: string; git: boolean } {
278
+ const top = sh('git', ['rev-parse', '--show-toplevel'], cwd);
279
+ if (top.ok && top.out.trim() !== '') return { root: top.out.trim(), git: true };
280
+ return { root: find_project_root(cwd) ?? cwd, git: false };
281
+ }
282
+
283
+ export interface ConventionTarget extends TargetRef {
284
+ readonly sha: string | null;
285
+ }
286
+
287
+ export interface CommittedConvention {
288
+ readonly root: string;
289
+ /** The value in force per key. */
290
+ readonly readings: Partial<Record<GitConventionKey, GitConventionReading>>;
291
+ /** The commit the carrier was read at per key; null outside a git repository. */
292
+ readonly readAt: Partial<Record<GitConventionKey, string | null>>;
293
+ /** What `update_strategy` was judged against; null when no target was asked for. */
294
+ readonly target: ConventionTarget | null;
295
+ /** The checkout's own value per key, only where it differs from the value in force. */
296
+ readonly candidates: Partial<Record<GitConventionKey, GitConventionReading>>;
297
+ }
298
+
299
+ export interface CommittedOptions {
300
+ readonly override?: string | null;
301
+ readonly deps?: TargetDeps;
302
+ readonly keys?: readonly GitConventionKey[];
303
+ /** Runs the git calls that read the carrier at a commit. */
304
+ readonly run?: GitRunner;
305
+ /** `false` reads only a commit already in this checkout; a missing one is `unresolvable`, never fetched. */
306
+ readonly fetch?: boolean;
307
+ }
308
+
309
+ function _short(sha: string): string {
310
+ return sha.slice(0, 12);
311
+ }
312
+
313
+ /** The git convention in force for the repository that contains `cwd`. */
314
+ export function readCommittedConvention(cwd: string, options: CommittedOptions = {}): CommittedConvention {
315
+ const keys = options.keys ?? GIT_CONVENTION_KEYS;
316
+ const { root, git } = conventionRoot(cwd);
317
+ const developer = checkoutSource(root).layers();
318
+ const readings: Partial<Record<GitConventionKey, GitConventionReading>> = {};
319
+ const readAt: Partial<Record<GitConventionKey, string | null>> = {};
320
+ const candidates: Partial<Record<GitConventionKey, GitConventionReading>> = {};
321
+ let target: ConventionTarget | null = null;
322
+
323
+ // Outside a git repository there is no committed layer to read.
324
+ if (!git) {
325
+ for (const key of keys) {
326
+ readings[key] = readGitConventionKey(key, { layers: () => developer });
327
+ readAt[key] = null;
328
+ }
329
+ return { root, readings, readAt, target, candidates };
330
+ }
331
+
332
+ const headSha = sh('git', ['rev-parse', '--verify', '-q', 'HEAD'], root).out.trim();
333
+ const headLabel = `${CARRIER_PATH} at ${headSha === '' ? 'HEAD' : _short(headSha)}`;
334
+ let headBlob: CommitBlob | null = null;
335
+ const atHead = (): CommitBlob => {
336
+ if (headBlob === null) headBlob = headSha === '' ? { kind: 'absent' } : carrierBlobAt(root, headSha, null, options.run);
337
+ return headBlob;
338
+ };
339
+
340
+ for (const key of keys) {
341
+ if (key !== 'update_strategy') {
342
+ const blob = atHead();
343
+ readings[key] = blob.kind === 'no-commit'
344
+ ? conventionReading(key, 'unresolvable', null, headLabel, `the carrier at HEAD (${_short(headSha)}) could not be read`)
345
+ : readGitConventionKey(key, { layers: () => [...developer, _carrierLayer(headLabel, blob)] });
346
+ readAt[key] = headSha === '' ? null : headSha;
347
+ continue;
348
+ }
349
+ const deps = options.deps ?? makeTargetDeps(root);
350
+ const ref = resolveTarget(deps, options.override ?? null);
351
+ const sha = ref === null ? null : deps.remoteSha(ref.ref);
352
+ target = ref === null ? null : { ...ref, sha };
353
+ readAt[key] = sha;
354
+ if (ref === null || sha === null) {
355
+ readings[key] = conventionReading(
356
+ key,
357
+ 'unresolvable',
358
+ null,
359
+ CARRIER_PATH,
360
+ ref === null
361
+ ? 'no --base was given and origin names no default branch, so the target commit is unknown'
362
+ : `the server reports no commit for ${ref.ref} (no such branch on ${splitResolvedRef(ref.ref).remote}, no such remote, or it is unreachable)`,
363
+ );
364
+ continue;
365
+ }
366
+ const blob = carrierBlobAt(root, sha, options.fetch === false ? null : ref.ref, options.run);
367
+ if (blob.kind === 'no-commit') {
368
+ readings[key] = conventionReading(
369
+ key,
370
+ 'unresolvable',
371
+ null,
372
+ `${CARRIER_PATH} at ${_short(sha)}`,
373
+ blob.timedOut === true
374
+ ? `fetching commit ${_short(sha)} of ${ref.ref} timed out after ${CARRIER_FETCH_TIMEOUT_MS / 1000} s`
375
+ : `commit ${_short(sha)} of ${ref.ref} could not be fetched`,
376
+ );
377
+ continue;
378
+ }
379
+ readings[key] = readGitConventionKey(key, { layers: () => [...developer, _carrierLayer(`${CARRIER_PATH} at ${_short(sha)}`, blob)] });
380
+ }
381
+
382
+ const tree = _workingTreeLayer(root);
383
+ for (const key of keys) {
384
+ const inForce = readings[key] as GitConventionReading;
385
+ const local = readGitConventionKey(key, { layers: () => [...developer, tree] });
386
+ if (local.value !== inForce.value || local.state !== inForce.state) candidates[key] = local;
387
+ }
388
+ return { root, readings, readAt, target, candidates };
389
+ }
@@ -0,0 +1,246 @@
1
+ /**
2
+ * The ticket grammar, the commit-subject grammars and the branch renderer.
3
+ *
4
+ * These were restated as regexes across several commands and checked by a model
5
+ * reading them, which let a ticket inside a compound scope pass and left an
6
+ * approved measured family with no validator at all. Pure: no I/O, so the CLI,
7
+ * the reference generator and the tests read the same values.
8
+ */
9
+
10
+ export const TICKET_GRAMMAR = '[A-Z][A-Z0-9]+-[0-9]+';
11
+
12
+ /**
13
+ * The grammar as a whole token: no letter or digit right before the key and no
14
+ * digit right after the number, so `fooDEV-12` holds no ticket. A version-like
15
+ * key (`HTTP2-1`, `V2-3`) is still ticket-shaped under the grammar; a card's
16
+ * `ticket_keys` is what tells it apart.
17
+ */
18
+ export const TICKET_TOKEN = `(?<![A-Za-z0-9])${TICKET_GRAMMAR}(?![0-9])`;
19
+
20
+ /** Prefixes of standard names that share the ticket shape (`UTF-8`, `CVE-2026-1234`). */
21
+ export const TICKET_DENYLIST = ['UTF', 'ISO', 'SHA', 'RFC', 'CVE', 'CWE', 'GHSA'] as const;
22
+
23
+ /**
24
+ * Protocol and language names that branch names carry with a major version
25
+ * (`HTTP-2`, `PHP-8`). Without a card such a key with a one-digit number is
26
+ * `version-like`, not a ticket; a card that lists the key keeps it a ticket.
27
+ * Two or more digits read as a ticket number, which is how the two are told apart.
28
+ */
29
+ export const VERSION_LIKE_KEYS = ['HTTP', 'HTTPS', 'TLS', 'SSL', 'OAUTH', 'PHP', 'PYTHON', 'JAVA', 'JDK', 'NODE', 'ES', 'HTML', 'CSS'] as const;
30
+
31
+ export type TicketStatus = 'ticket' | 'standard-name' | 'version-like' | 'unknown-key';
32
+
33
+ export interface TicketCandidate {
34
+ token: string;
35
+ key: string;
36
+ status: TicketStatus;
37
+ }
38
+
39
+ /** The ticket token in either case, scanned only when a card names the keys. */
40
+ const ANY_CASE_TOKEN = `(?<![A-Za-z0-9])[A-Za-z][A-Za-z0-9]+-[0-9]+(?![0-9])`;
41
+
42
+ /**
43
+ * Every ticket-shaped token in `text`, in order. With `keys`, a candidate whose
44
+ * key is not listed is `unknown-key`, and a key written in another case is
45
+ * matched against the list and returned upper-cased; a lower-case token whose
46
+ * key is not listed is not a candidate. Without `keys` only upper-case tokens
47
+ * count, and the grammar, the denylist and the version-like names decide.
48
+ */
49
+ export function ticketCandidates(text: string, keys?: readonly string[] | null): TicketCandidate[] {
50
+ const card = keys !== undefined && keys !== null && keys.length > 0 ? keys : null;
51
+ const out: TicketCandidate[] = [];
52
+ for (const m of text.matchAll(new RegExp(card === null ? TICKET_TOKEN : ANY_CASE_TOKEN, 'g'))) {
53
+ const dash = m[0].indexOf('-');
54
+ const written = m[0].slice(0, dash);
55
+ const key = written.toUpperCase();
56
+ const number = m[0].slice(dash + 1);
57
+ if (written !== key && !card?.includes(key)) continue;
58
+ let status: TicketStatus = 'ticket';
59
+ if ((TICKET_DENYLIST as readonly string[]).includes(key)) status = 'standard-name';
60
+ else if (card !== null) status = card.includes(key) ? 'ticket' : 'unknown-key';
61
+ else if (number.length === 1 && (VERSION_LIKE_KEYS as readonly string[]).includes(key)) status = 'version-like';
62
+ out.push({ token: `${key}-${number}`, key, status });
63
+ }
64
+ return out;
65
+ }
66
+
67
+ export function firstTicket(text: string, keys?: readonly string[] | null): string | null {
68
+ return ticketCandidates(text, keys).find((c) => c.status === 'ticket')?.token ?? null;
69
+ }
70
+
71
+ const KEY = /^[A-Z][A-Z0-9]+$/;
72
+
73
+ /**
74
+ * The approved project keys from a convention card's `ticket_keys` line, or
75
+ * from that line's value alone. A card without the line yields none.
76
+ */
77
+ export function parseTicketKeys(text: string): string[] {
78
+ const line = /^\s*ticket_keys\s*:(.*)$/m.exec(text)?.[1];
79
+ const value = line ?? (text.includes(':') ? '' : text);
80
+ return value
81
+ .replace(/[[\]"']/g, ' ')
82
+ .split(/[\s,]+/)
83
+ .filter((k) => KEY.test(k));
84
+ }
85
+
86
+ /** The keys a commitlint config's `issuePrefixes` names: offered for the card, never written by code. */
87
+ export function commitlintIssuePrefixes(configText: string): string[] {
88
+ const list = /["']?issuePrefixes["']?\s*:\s*\[([^\]]*)\]/.exec(configText)?.[1] ?? '';
89
+ return [...list.matchAll(/["']([^"']*)["']/g)].map((m) => (m[1] as string).replace(/-$/, '')).filter((k) => KEY.test(k));
90
+ }
91
+
92
+ export const COMMIT_TYPES = ['feat', 'fix', 'chore', 'docs', 'refactor', 'test', 'perf', 'style', 'build', 'ci', 'revert'] as const;
93
+
94
+ export type CommitFormat = 'ticket-scope' | 'ticket-conventional';
95
+ export type SubjectFamily = 'conventional' | 'ticket-conventional' | 'ticket-prefix' | 'gitmoji' | 'imperative-plain';
96
+
97
+ const TYPES = COMMIT_TYPES.join('|');
98
+
99
+ /** The `git.commit_format` grammars, in the syntax a JavaScript or PCRE engine reads. */
100
+ export const FORMAT_GRAMMAR: Readonly<Record<CommitFormat, string>> = {
101
+ 'ticket-scope': `^(${TYPES})(\\([^)]+\\))?!?: .+`,
102
+ 'ticket-conventional': `^(${TICKET_GRAMMAR} )?(${TYPES})(\\([^)]+\\))?!?: .+`,
103
+ };
104
+
105
+ /**
106
+ * The measured families as POSIX extended regular expressions, matched in
107
+ * order, first hit wins; anything else is `other`.
108
+ */
109
+ export const FAMILY_ERE: ReadonlyArray<readonly [SubjectFamily, string]> = [
110
+ ['conventional', '^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\\([^)]+\\))?!?: '],
111
+ ['ticket-conventional', `^${TICKET_GRAMMAR} (build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\\([^)]+\\))?!?: `],
112
+ ['ticket-prefix', `^\\[${TICKET_GRAMMAR}\\][: ]|^${TICKET_GRAMMAR}[: ]`],
113
+ // `[:ascii:]` is not a POSIX class (GNU and BSD grep reject it); a leading
114
+ // character outside printable ASCII, the controls and the spaces is the same set.
115
+ ['gitmoji', '^:[a-z0-9_+-]+:[[:space:]]|^[^ -~[:cntrl:][:space:]]'],
116
+ ['imperative-plain', '^[A-Z][a-z]+[[:space:]].*[^.]$'],
117
+ ];
118
+
119
+ /** The two POSIX bracket expressions the families use, and nothing else, translated for a JavaScript engine. */
120
+ function _js(ere: string): RegExp {
121
+ return new RegExp(ere.replaceAll('[^ -~[:cntrl:][:space:]]', '[^\\x00-\\x7F\\s]').replaceAll('[[:space:]]', '\\s'));
122
+ }
123
+
124
+ const FAMILY_JS: ReadonlyArray<readonly [SubjectFamily, RegExp]> = FAMILY_ERE.map(([f, ere]) => [f, _js(ere)] as const);
125
+
126
+ export function classifySubject(subject: string): SubjectFamily | 'other' {
127
+ return FAMILY_JS.find(([, re]) => re.test(subject))?.[0] ?? 'other';
128
+ }
129
+
130
+ export type SubjectRule = { format: CommitFormat } | { family: SubjectFamily };
131
+ export type SubjectVerdict = { ok: true } | { ok: false; rule: string };
132
+
133
+ const SCOPE = /^[^(:]*\(([^)]*)\)/;
134
+
135
+ /** Grammars where the ticket leads the subject, so a ticket in the scope is a second, misplaced copy. */
136
+ function _ticketLeads(rule: SubjectRule): boolean {
137
+ return 'format' in rule ? rule.format === 'ticket-conventional' : rule.family === 'ticket-conventional';
138
+ }
139
+
140
+ export function ruleName(rule: SubjectRule): string {
141
+ return 'format' in rule ? `git.commit_format: ${rule.format}` : `approved family ${rule.family}`;
142
+ }
143
+
144
+ /** Subjects git writes itself (`commit --fixup`, `--squash`, `--fixup=amend:`, `revert`), valid under every format and every approved family. */
145
+ export const GIT_OWN_SUBJECT = '^((fixup|squash|amend)! .+|Revert ".+")$';
146
+
147
+ /**
148
+ * A family's form without its ticket, for the families whose ticket is a
149
+ * leading part (owner decision 2026-10-07): the ticket is optional, as under
150
+ * the `ticket-conventional` setting value, and when present it leads.
151
+ */
152
+ export const TICKETLESS_FORM: Readonly<Partial<Record<SubjectFamily, string>>> = {
153
+ 'ticket-prefix': '^\\S.*',
154
+ 'ticket-conventional': FAMILY_ERE[0]?.[1] ?? '',
155
+ };
156
+
157
+ export function checkSubject(subject: string, rule: SubjectRule): SubjectVerdict {
158
+ if (new RegExp(GIT_OWN_SUBJECT).test(subject)) return { ok: true };
159
+ const grammar = 'format' in rule ? new RegExp(FORMAT_GRAMMAR[rule.format]) : (FAMILY_JS.find(([f]) => f === rule.family)?.[1] as RegExp);
160
+ const ticketless = 'family' in rule ? TICKETLESS_FORM[rule.family] : undefined;
161
+ if (!grammar.test(subject) && ticketless !== undefined) {
162
+ const family = (rule as { family: SubjectFamily }).family;
163
+ if (!_js(ticketless).test(subject)) {
164
+ return { ok: false, rule: `${ruleName(rule)} — does not match ${FAMILY_ERE.find(([f]) => f === family)?.[1] ?? ''}, nor its form without a ticket ${ticketless}` };
165
+ }
166
+ const stray = ticketCandidates(subject).find((c) => c.status === 'ticket');
167
+ if (stray !== undefined) {
168
+ return { ok: false, rule: `${ruleName(rule)} — the ticket \`${stray.token}\` stands outside the leading position; it leads the subject or is left out` };
169
+ }
170
+ return { ok: true };
171
+ }
172
+ if (!grammar.test(subject)) {
173
+ return { ok: false, rule: `${ruleName(rule)} — does not match ${'format' in rule ? FORMAT_GRAMMAR[rule.format] : (FAMILY_ERE.find(([f]) => f === rule.family)?.[1] ?? '')}` };
174
+ }
175
+ if (!_ticketLeads(rule)) return { ok: true };
176
+ const lead = /^(\S+) /.exec(subject)?.[1] ?? '';
177
+ const leading = ticketCandidates(lead).find((c) => c.token === lead);
178
+ if (leading !== undefined && leading.status === 'standard-name') {
179
+ return { ok: false, rule: `${ruleName(rule)} — \`${lead}\` is a standard name, not a ticket` };
180
+ }
181
+ const scope = SCOPE.exec(lead === '' || leading === undefined ? subject : subject.slice(lead.length + 1))?.[1] ?? '';
182
+ const inScope = ticketCandidates(scope).find((c) => c.status === 'ticket');
183
+ if (inScope !== undefined) {
184
+ return { ok: false, rule: `${ruleName(rule)} — the ticket \`${inScope.token}\` stands inside the scope; it leads the subject, never the scope` };
185
+ }
186
+ return { ok: true };
187
+ }
188
+
189
+ export interface BranchInput {
190
+ type?: string | null;
191
+ ticket?: string | null;
192
+ slug: string;
193
+ }
194
+
195
+ export type BranchRender = { ok: true; name: string } | { ok: false; reason: string };
196
+
197
+ const VALUE = /^[A-Za-z0-9._-]+$/;
198
+ const SEPARATORS = '-/_.';
199
+
200
+ /**
201
+ * `pattern` with its placeholders filled. An empty placeholder drops together
202
+ * with the separator after it, or the one before it when it is last; a ticket
203
+ * the pattern has no slot for prefixes the slug. Literal characters of the
204
+ * pattern are never rewritten, and a value outside `[A-Za-z0-9._-]` is refused
205
+ * rather than repaired, because the name reaches shell commands.
206
+ */
207
+ export function renderBranch(pattern: string, input: BranchInput): BranchRender {
208
+ const type = input.type ?? '';
209
+ const ticket = input.ticket ?? '';
210
+ let slug = input.slug;
211
+ if (slug === '' || !VALUE.test(slug)) return { ok: false, reason: `slug ${JSON.stringify(slug)} is empty or outside [A-Za-z0-9._-]` };
212
+ if (type !== '' && !VALUE.test(type)) return { ok: false, reason: `type ${JSON.stringify(type)} is outside [A-Za-z0-9._-]` };
213
+ if (ticket !== '' && !new RegExp(`^${TICKET_GRAMMAR}$`).test(ticket)) {
214
+ return { ok: false, reason: `ticket ${JSON.stringify(ticket)} does not match ${TICKET_GRAMMAR}` };
215
+ }
216
+ if (ticket !== '' && !pattern.includes('{ticket}') && slug !== ticket && !slug.startsWith(`${ticket}-`)) slug = `${ticket}-${slug}`;
217
+
218
+ const values: Record<string, string> = { type, ticket, slug };
219
+ const parts = pattern.split(/(\{(?:type|ticket|slug)\})/).filter((p) => p !== '');
220
+ const out: string[] = [];
221
+ for (let i = 0; i < parts.length; i++) {
222
+ const part = parts[i] as string;
223
+ const ph = /^\{(type|ticket|slug)\}$/.exec(part);
224
+ if (ph === null) {
225
+ out.push(part);
226
+ continue;
227
+ }
228
+ const value = values[ph[1] as string] as string;
229
+ if (value !== '') {
230
+ out.push(value);
231
+ continue;
232
+ }
233
+ const next = parts[i + 1];
234
+ if (next !== undefined && !next.startsWith('{') && SEPARATORS.includes(next[0] as string)) {
235
+ parts[i + 1] = next.slice(1);
236
+ } else if (next === undefined && out.length > 0) {
237
+ const prev = out[out.length - 1] as string;
238
+ if (SEPARATORS.includes(prev.slice(-1))) out[out.length - 1] = prev.slice(0, -1);
239
+ }
240
+ }
241
+ const name = out.join('');
242
+ if (name === '' || name.startsWith('/') || name.endsWith('/') || name.includes('//') || name.includes('..')) {
243
+ return { ok: false, reason: `the rendered name ${JSON.stringify(name)} is not a branch name` };
244
+ }
245
+ return { ok: true, name };
246
+ }