@event4u/agent-config 16.3.0 → 16.4.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 (486) 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 +251 -0
  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/estimate-ticket.md +1 -1
  16. package/dist/agent-src/commands/feature/explore.md +3 -3
  17. package/dist/agent-src/commands/feature/plan.md +1 -1
  18. package/dist/agent-src/commands/fix/ci.md +17 -3
  19. package/dist/agent-src/commands/fix/commit-messages.md +48 -23
  20. package/dist/agent-src/commands/fix/pr-comments.md +3 -6
  21. package/dist/agent-src/commands/implement-ticket.md +3 -3
  22. package/dist/agent-src/commands/memory/add.md +4 -16
  23. package/dist/agent-src/commands/memory/load.md +5 -6
  24. package/dist/agent-src/commands/memory/promote.md +3 -16
  25. package/dist/agent-src/commands/mission/upgrade.md +3 -2
  26. package/dist/agent-src/commands/optimize/agents-dir.md +2 -2
  27. package/dist/agent-src/commands/optimize/deep.md +4 -2
  28. package/dist/agent-src/commands/optimize/rtk.md +4 -26
  29. package/dist/agent-src/commands/pr/create/description-only.md +18 -15
  30. package/dist/agent-src/commands/pr/create.md +42 -34
  31. package/dist/agent-src/commands/pr/merge.md +136 -9
  32. package/dist/agent-src/commands/prepare-for-review.md +13 -11
  33. package/dist/agent-src/commands/project/analyze.md +7 -35
  34. package/dist/agent-src/commands/refine-ticket.md +1 -1
  35. package/dist/agent-src/commands/review/changes.md +14 -19
  36. package/dist/agent-src/commands/roadmap/next.md +10 -1
  37. package/dist/agent-src/commands/roadmap/process-full.md +52 -38
  38. package/dist/agent-src/commands/roadmap/resolve-blockers.md +64 -14
  39. package/dist/agent-src/commands/worktree/create.md +7 -3
  40. package/dist/agent-src/commands/worktree/verify.md +2 -1
  41. package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +5 -3
  42. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +7 -9
  43. package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +8 -6
  44. package/dist/agent-src/contexts/contracts/frugality-charter.md +8 -5
  45. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +1 -1
  46. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +0 -1
  47. package/dist/agent-src/contexts/execution/evidence-discipline.md +2 -2
  48. package/dist/agent-src/contexts/execution/non-interactive-contract.md +5 -8
  49. package/dist/agent-src/contexts/execution/rdp-gate.md +4 -4
  50. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +9 -8
  51. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +67 -52
  52. package/dist/agent-src/contexts/execution/subagent-routing.md +8 -8
  53. package/dist/agent-src/contexts/execution/subagent-steering.md +6 -4
  54. package/dist/agent-src/contexts/execution/terminal-states.md +2 -2
  55. package/dist/agent-src/contexts/execution/toolchain-resolver.md +4 -10
  56. package/dist/agent-src/contexts/subagent-configuration.md +1 -1
  57. package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +39 -0
  58. package/dist/agent-src/guidelines/agent-infra/context-hygiene-mechanics.md +1 -1
  59. package/dist/agent-src/guidelines/agent-infra/cross-source-consistency-mechanics.md +8 -10
  60. package/dist/agent-src/guidelines/agent-infra/frontier-reasoning-operating-profile.md +3 -2
  61. package/dist/agent-src/guidelines/agent-infra/layered-settings.md +13 -1
  62. package/dist/agent-src/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  63. package/dist/agent-src/guidelines/agent-infra/self-improvement-pipeline.md +10 -3
  64. package/dist/agent-src/guidelines/agent-infra/size-and-scope.md +36 -0
  65. package/dist/agent-src/guidelines/php/git.md +15 -2
  66. package/dist/agent-src/rules/active-remediation.md +0 -1
  67. package/dist/agent-src/rules/analysis-skill-routing.md +0 -1
  68. package/dist/agent-src/rules/architecture.md +0 -1
  69. package/dist/agent-src/rules/artifact-drafting-protocol.md +0 -1
  70. package/dist/agent-src/rules/artifact-engagement-recording.md +0 -1
  71. package/dist/agent-src/rules/augment-edit-discipline.md +0 -1
  72. package/dist/agent-src/rules/autonomous-execution.md +0 -1
  73. package/dist/agent-src/rules/brand-consistency.md +0 -1
  74. package/dist/agent-src/rules/brand-source-of-truth.md +0 -1
  75. package/dist/agent-src/rules/broken-access-control.md +0 -1
  76. package/dist/agent-src/rules/cli-output-handling.md +0 -1
  77. package/dist/agent-src/rules/code-comment-discipline.md +0 -1
  78. package/dist/agent-src/rules/code-provenance.md +0 -1
  79. package/dist/agent-src/rules/command-suggestion-policy.md +0 -1
  80. package/dist/agent-src/rules/commit-conventions.md +1 -2
  81. package/dist/agent-src/rules/communication-through-line.md +1 -2
  82. package/dist/agent-src/rules/content-quoting-floor.md +1 -2
  83. package/dist/agent-src/rules/context-hygiene.md +5 -5
  84. package/dist/agent-src/rules/copilot-routing.md +1 -2
  85. package/dist/agent-src/rules/council-availability.md +4 -4
  86. package/dist/agent-src/rules/cross-source-consistency.md +4 -6
  87. package/dist/agent-src/rules/decision-revisit-gate.md +2 -3
  88. package/dist/agent-src/rules/delegation-policy.md +2 -3
  89. package/dist/agent-src/rules/design-fidelity.md +0 -1
  90. package/dist/agent-src/rules/design-review-after-ui-write.md +3 -3
  91. package/dist/agent-src/rules/devcontainer-routing.md +1 -2
  92. package/dist/agent-src/rules/doc-screenshot-hygiene.md +0 -1
  93. package/dist/agent-src/rules/docker-commands.md +0 -1
  94. package/dist/agent-src/rules/domain-adoption-policy.md +0 -1
  95. package/dist/agent-src/rules/domain-safety-disclaimer.md +0 -1
  96. package/dist/agent-src/rules/domain-safety-pii.md +0 -1
  97. package/dist/agent-src/rules/domain-safety-retention.md +0 -1
  98. package/dist/agent-src/rules/downstream-changes.md +0 -1
  99. package/dist/agent-src/rules/engineering-safety-floor.md +0 -1
  100. package/dist/agent-src/rules/external-code-graph-interop.md +0 -1
  101. package/dist/agent-src/rules/external-reference-deep-dive.md +0 -1
  102. package/dist/agent-src/rules/fast-path-marker-visibility.md +1 -2
  103. package/dist/agent-src/rules/finance-safety-floor.md +0 -1
  104. package/dist/agent-src/rules/fix-what-you-see.md +6 -32
  105. package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +0 -1
  106. package/dist/agent-src/rules/git-history-discipline.md +1 -2
  107. package/dist/agent-src/rules/guidelines.md +0 -1
  108. package/dist/agent-src/rules/history-discipline.md +0 -1
  109. package/dist/agent-src/rules/icon-consistency.md +0 -1
  110. package/dist/agent-src/rules/image-likeness-and-rights.md +0 -1
  111. package/dist/agent-src/rules/improve-before-implement.md +0 -1
  112. package/dist/agent-src/rules/invite-challenge.md +0 -1
  113. package/dist/agent-src/rules/laravel-routing.md +1 -2
  114. package/dist/agent-src/rules/laravel-translations.md +0 -1
  115. package/dist/agent-src/rules/legal-safety-floor.md +0 -1
  116. package/dist/agent-src/rules/lethal-trifecta-guard.md +0 -1
  117. package/dist/agent-src/rules/linked-projects-onboarding-gate.md +0 -1
  118. package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +0 -1
  119. package/dist/agent-src/rules/markdown-safe-codeblocks.md +0 -1
  120. package/dist/agent-src/rules/media-governance-routing.md +1 -2
  121. package/dist/agent-src/rules/media-sync-ground-truth.md +0 -1
  122. package/dist/agent-src/rules/minimal-safe-diff.md +0 -1
  123. package/dist/agent-src/rules/missing-skill-recovery.md +0 -1
  124. package/dist/agent-src/rules/missing-tool-handling.md +0 -1
  125. package/dist/agent-src/rules/model-recommendation.md +0 -1
  126. package/dist/agent-src/rules/neighbour-precedence.md +64 -0
  127. package/dist/agent-src/rules/no-attribution-footers.md +0 -1
  128. package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +0 -1
  129. package/dist/agent-src/rules/no-pr-progress-comments.md +0 -1
  130. package/dist/agent-src/rules/no-roadmap-references.md +0 -1
  131. package/dist/agent-src/rules/notes-first-reasoning.md +0 -1
  132. package/dist/agent-src/rules/onboarding-gate.md +0 -1
  133. package/dist/agent-src/rules/output-discipline.md +0 -1
  134. package/dist/agent-src/rules/package-ci-checks.md +0 -1
  135. package/dist/agent-src/rules/persona-governance.md +2 -3
  136. package/dist/agent-src/rules/php-coding.md +0 -1
  137. package/dist/agent-src/rules/prefer-enums-over-literals.md +0 -1
  138. package/dist/agent-src/rules/preservation-guard.md +0 -1
  139. package/dist/agent-src/rules/provider-lifecycle-discipline.md +2 -3
  140. package/dist/agent-src/rules/question-not-instruction.md +0 -1
  141. package/dist/agent-src/rules/reviewer-awareness.md +0 -1
  142. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +0 -1
  143. package/dist/agent-src/rules/roadmap-progress-sync.md +3 -6
  144. package/dist/agent-src/rules/role-mode-adherence.md +0 -1
  145. package/dist/agent-src/rules/rule-type-governance.md +0 -1
  146. package/dist/agent-src/rules/runtime-safety.md +0 -1
  147. package/dist/agent-src/rules/scale-discipline.md +0 -1
  148. package/dist/agent-src/rules/secret-vcs-guard.md +0 -1
  149. package/dist/agent-src/rules/security-sensitive-stop.md +0 -1
  150. package/dist/agent-src/rules/self-repair-loop.md +0 -1
  151. package/dist/agent-src/rules/senior-engineering-discipline.md +0 -1
  152. package/dist/agent-src/rules/session-canary.md +0 -1
  153. package/dist/agent-src/rules/settings-ask-protocol.md +5 -6
  154. package/dist/agent-src/rules/size-enforcement.md +0 -1
  155. package/dist/agent-src/rules/skill-improvement-trigger.md +2 -3
  156. package/dist/agent-src/rules/skill-quality.md +0 -1
  157. package/dist/agent-src/rules/slash-command-routing-policy.md +0 -1
  158. package/dist/agent-src/rules/source-confidentiality.md +0 -1
  159. package/dist/agent-src/rules/source-discovery-gate.md +0 -1
  160. package/dist/agent-src/rules/source-of-truth.md +0 -1
  161. package/dist/agent-src/rules/spreadsheet-source-quality.md +1 -2
  162. package/dist/agent-src/rules/strategy-safety-floor.md +0 -1
  163. package/dist/agent-src/rules/symfony-routing.md +1 -2
  164. package/dist/agent-src/rules/think-before-action.md +0 -1
  165. package/dist/agent-src/rules/token-budget-discipline.md +12 -46
  166. package/dist/agent-src/rules/token-efficiency.md +0 -1
  167. package/dist/agent-src/rules/token-optimizer-maintenance.md +0 -1
  168. package/dist/agent-src/rules/tool-safety.md +0 -1
  169. package/dist/agent-src/rules/ui-audit-gate.md +1 -2
  170. package/dist/agent-src/rules/untrusted-input-defense.md +0 -1
  171. package/dist/agent-src/rules/upstream-proposal.md +0 -1
  172. package/dist/agent-src/rules/user-interaction.md +0 -1
  173. package/dist/agent-src/rules/user-interrupt-priority.md +0 -1
  174. package/dist/agent-src/scripts/gate_budget.ts +47 -23
  175. package/dist/agent-src/scripts/gate_execute.ts +12 -4
  176. package/dist/agent-src/skills/ai-council/SKILL.md +7 -4
  177. package/dist/agent-src/skills/ai-council/references/cost-and-redaction.md +21 -11
  178. package/dist/agent-src/skills/ai-council/references/procedure.md +12 -6
  179. package/dist/agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -1
  180. package/dist/agent-src/skills/bug-analyzer/SKILL.md +1 -1
  181. package/dist/agent-src/skills/command-routing/SKILL.md +3 -4
  182. package/dist/agent-src/skills/conventional-commits-writing/SKILL.md +127 -139
  183. package/dist/agent-src/skills/corpus-grounding/ATTRIBUTION.md +39 -0
  184. package/dist/agent-src/skills/corpus-grounding/SKILL.md +8 -2
  185. package/dist/agent-src/skills/database/data/manifest.json +2 -2
  186. package/dist/agent-src/skills/database/data/query-tuning.csv +13 -13
  187. package/dist/agent-src/skills/design-system-capture/SKILL.md +0 -3
  188. package/dist/agent-src/skills/estimate-ticket/SKILL.md +1 -1
  189. package/dist/agent-src/skills/feature-planning/SKILL.md +1 -1
  190. package/dist/agent-src/skills/git-workflow/SKILL.md +44 -44
  191. package/dist/agent-src/skills/git-workflow/references/branch-update.md +312 -0
  192. package/dist/agent-src/skills/git-workflow/references/commit-subject.md +205 -0
  193. package/dist/agent-src/skills/jira-integration/SKILL.md +3 -3
  194. package/dist/agent-src/skills/merge-conflicts/SKILL.md +10 -4
  195. package/dist/agent-src/skills/po-discovery/SKILL.md +1 -1
  196. package/dist/agent-src/skills/refine-ticket/SKILL.md +4 -4
  197. package/dist/agent-src/skills/roadmap-writing/SKILL.md +2 -2
  198. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +4 -2
  199. package/dist/agent-src/skills/source-discovery/SKILL.md +1 -1
  200. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +3 -1
  201. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +1 -1
  202. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +1 -1
  203. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +2 -2
  204. package/dist/agent-src/templates/AGENTS.md +1 -1
  205. package/dist/agent-src/templates/agent-settings.md +46 -129
  206. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +3 -3
  207. package/dist/agent-src/templates/command.md +2 -2
  208. package/dist/agent-src/templates/roadmaps.md +6 -1
  209. package/dist/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +0 -6
  210. package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +11 -7
  211. package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.ts +3 -2
  212. package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.ts +2 -1
  213. package/dist/agent-src/templates/scripts/work_engine/hooks/settings.ts +0 -6
  214. package/dist/agent-src/templates/scripts/work_engine/scoring/decision_engine.ts +9 -12
  215. package/dist/agent-src/templates/scripts/work_engine/scoring/memory_visibility.ts +2 -1
  216. package/dist/agent-src/templates/skill.md +1 -1
  217. package/dist/cli/registry.js +1 -0
  218. package/dist/cli-delegate/check_update_banner.js +195 -119
  219. package/dist/cli-delegate/{chunk-TRI6DQZ6.js → chunk-5GPULQBE.js} +60 -42
  220. package/dist/cli-delegate/{chunk-JQVYKIZA.js → chunk-6SRJTYJS.js} +33 -177
  221. package/dist/cli-delegate/chunk-6V5FMA3P.js +186 -0
  222. package/dist/cli-delegate/chunk-6YQC2HLF.js +415 -0
  223. package/dist/cli-delegate/{chunk-E4XIZDIB.js → chunk-7HKRE54K.js} +1 -64
  224. package/dist/cli-delegate/{chunk-NQ7WT43P.js → chunk-AED2VQGM.js} +65 -11
  225. package/dist/cli-delegate/{chunk-7HQ52DHU.js → chunk-AL3RZ22D.js} +1 -1
  226. package/dist/cli-delegate/{chunk-DBH4AHDK.js → chunk-ED7KCMLB.js} +9 -11
  227. package/dist/cli-delegate/{chunk-NXEILF2K.js → chunk-F3QDG2GT.js} +481 -72
  228. package/dist/cli-delegate/{chunk-CQWX336L.js → chunk-G5VVIZRC.js} +3 -3
  229. package/dist/cli-delegate/{chunk-JY2I2DXV.js → chunk-H76PHZBI.js} +137 -60
  230. package/dist/cli-delegate/chunk-IAYTIIQW.js +67 -0
  231. package/dist/cli-delegate/{chunk-LIYTLH6O.js → chunk-KXXWJTDO.js} +5 -10
  232. package/dist/cli-delegate/{chunk-T7L2QK3K.js → chunk-LQOCRFWT.js} +1 -1
  233. package/dist/cli-delegate/{chunk-S4YWNRKO.js → chunk-MDPPKQCQ.js} +34 -1
  234. package/dist/cli-delegate/{chunk-S4UMZNES.js → chunk-MSUL5NYP.js} +5 -5
  235. package/dist/cli-delegate/chunk-OAFSJBHH.js +272 -0
  236. package/dist/cli-delegate/{chunk-GKE7ZVFA.js → chunk-PWX7CKMD.js} +1 -1
  237. package/dist/cli-delegate/{chunk-NDJLNC6Q.js → chunk-T6BP34WI.js} +1 -0
  238. package/dist/cli-delegate/{chunk-RQNNW7IF.js → chunk-T7XSJSUQ.js} +433 -632
  239. package/dist/cli-delegate/{chunk-EEHDXSYD.js → chunk-WQWXGIWB.js} +12 -273
  240. package/dist/cli-delegate/{chunk-QLDHIPWR.js → chunk-XQTPXKFA.js} +1 -1
  241. package/dist/cli-delegate/cmd_analyze_session.js +223 -43
  242. package/dist/cli-delegate/cmd_brand_status.js +3 -2
  243. package/dist/cli-delegate/cmd_conformance.js +105 -35
  244. package/dist/cli-delegate/cmd_converge.js +3 -3
  245. package/dist/cli-delegate/cmd_doctor.js +15 -12
  246. package/dist/cli-delegate/cmd_doctor_neighbours.js +11 -4
  247. package/dist/cli-delegate/cmd_explain.js +43 -19
  248. package/dist/cli-delegate/cmd_export.js +10 -10
  249. package/dist/cli-delegate/cmd_git_convention.js +1851 -0
  250. package/dist/cli-delegate/cmd_handoff.js +40 -21
  251. package/dist/cli-delegate/cmd_mcp_available.js +3 -2
  252. package/dist/cli-delegate/cmd_migrate.js +3 -3
  253. package/dist/cli-delegate/cmd_packs_active.js +4 -3
  254. package/dist/cli-delegate/cmd_prune.js +10 -10
  255. package/dist/cli-delegate/cmd_route_audit.js +7 -7
  256. package/dist/cli-delegate/cmd_route_explain.js +5 -5
  257. package/dist/cli-delegate/cmd_session_recycle.js +9 -7
  258. package/dist/cli-delegate/cmd_settings_check.js +54 -7
  259. package/dist/cli-delegate/cmd_settings_get.js +18 -8
  260. package/dist/cli-delegate/cmd_settings_migrate.js +10 -10
  261. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  262. package/dist/cli-delegate/cmd_sync.js +10 -10
  263. package/dist/cli-delegate/cmd_uninstall.js +10 -10
  264. package/dist/cli-delegate/cmd_update.js +7 -7
  265. package/dist/cli-delegate/cmd_upgrade.js +7 -7
  266. package/dist/cli-delegate/cmd_validate.js +10 -10
  267. package/dist/cli-delegate/cmd_versions.js +5 -5
  268. package/dist/cli-delegate/gate_budget.js +1 -1
  269. package/dist/cli-delegate/gate_execute.js +2 -2
  270. package/dist/cli-delegate/roadmap_gates.js +2 -2
  271. package/dist/discovery/deprecation-report.md +1 -1
  272. package/dist/discovery/discovery-manifest.json +129 -90
  273. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  274. package/dist/discovery/discovery-manifest.summary.md +3 -3
  275. package/dist/discovery/dormancy-report.md +1 -1
  276. package/dist/discovery/orphan-report.md +1 -1
  277. package/dist/discovery/packs.json +24 -9
  278. package/dist/discovery/trust-report.md +3 -3
  279. package/dist/discovery/workspaces.json +5 -4
  280. package/dist/hooks/dispatch.js +2151 -1820
  281. package/dist/hooks/dispatch.sha256 +1 -1
  282. package/dist/install/install.mjs +462 -431
  283. package/dist/install/installThinLayer.js +40 -12
  284. package/dist/install/installedRuleLinks.js +105 -19
  285. package/dist/mcp/awesome-mcp-servers.row.md +1 -1
  286. package/dist/mcp/mcp-cloudflare-catalogue.json +2 -2
  287. package/dist/mcp/registry-manifest.json +5 -5
  288. package/dist/mcp/server.json +2 -2
  289. package/dist/mcp/server.mjs +2702 -1995
  290. package/dist/router.json +1 -1
  291. package/dist/scripts/_lib/agent_settings.js +15 -120
  292. package/dist/scripts/_lib/compile_time_toggles.js +12 -21
  293. package/dist/scripts/_lib/git_convention.js +232 -0
  294. package/dist/scripts/_lib/global_deploy_inventory.js +730 -0
  295. package/dist/scripts/_lib/installed_layer.js +344 -18
  296. package/dist/scripts/_lib/package_root.js +68 -0
  297. package/dist/scripts/_lib/rule_law_section.js +1 -1
  298. package/dist/scripts/_lib/settings_removed_keys.js +168 -0
  299. package/dist/scripts/_lib/thin_rules.js +80 -3
  300. package/dist/server/app.js +2 -1
  301. package/dist/server/gitKeysGate.js +105 -0
  302. package/dist/server/routes/settings.js +67 -8
  303. package/dist/server/routes/wizard.js +4 -0
  304. package/dist/server/schemas/settings.js +23 -81
  305. package/dist/shared/skillRanking.js +30 -4
  306. package/dist/ui/assets/{index-Cwq4d0hX.js → index-C3djEJah.js} +2 -2
  307. package/dist/ui/index.html +1 -1
  308. package/docs/MIGRATION.md +72 -1
  309. package/docs/contracts/settings-classes.md +35 -65
  310. package/docs/guidelines/agent-infra/active-remediation-mechanics.md +39 -0
  311. package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +1 -1
  312. package/docs/guidelines/agent-infra/cross-source-consistency-mechanics.md +8 -10
  313. package/docs/guidelines/agent-infra/frontier-reasoning-operating-profile.md +3 -2
  314. package/docs/guidelines/agent-infra/layered-settings.md +13 -1
  315. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  316. package/docs/guidelines/agent-infra/self-improvement-pipeline.md +10 -3
  317. package/docs/guidelines/agent-infra/size-and-scope.md +36 -0
  318. package/docs/guidelines/php/git.md +15 -2
  319. package/package.json +1 -1
  320. package/src/agent-src/scripts/gate_budget.ts +47 -23
  321. package/src/agent-src/scripts/gate_execute.ts +12 -4
  322. package/src/agent-src/templates/AGENTS.md +1 -1
  323. package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +0 -6
  324. package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +11 -7
  325. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.ts +3 -2
  326. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.ts +2 -1
  327. package/src/agent-src/templates/scripts/work_engine/hooks/settings.ts +0 -6
  328. package/src/agent-src/templates/scripts/work_engine/scoring/decision_engine.ts +9 -12
  329. package/src/agent-src/templates/scripts/work_engine/scoring/memory_visibility.ts +2 -1
  330. package/src/config/agent-settings.template.yml +79 -312
  331. package/src/config/agents-paths.yml +16 -0
  332. package/src/config/assurance-capability-registry.json +7 -3
  333. package/src/config/ci-local-parity.yml +3 -0
  334. package/src/config/continuity-surface.json +3 -3
  335. package/src/config/discovery/packs.yml +17 -1
  336. package/src/config/evaluator-budgets.json +19 -6
  337. package/src/config/gate-coverage.yml +65 -23
  338. package/src/config/gate-violation-baselines.json +53 -13
  339. package/src/config/gitignore-block.txt +19 -0
  340. package/src/config/hook-bundle-budget.json +1 -1
  341. package/src/config/release-gate-locality.yml +2 -1
  342. package/src/config/routing-coverage-seed.json +48 -6
  343. package/src/config/rule-law-ceilings.json +12 -6
  344. package/src/config/rule-obligations.json +833 -0
  345. package/src/install/installThinLayer.ts +76 -10
  346. package/src/install/installedRuleLinks.ts +105 -17
  347. package/src/scripts/_cli/cmd_analyze_session.ts +9 -15
  348. package/src/scripts/_cli/cmd_conformance.ts +13 -2
  349. package/src/scripts/_cli/cmd_doctor.ts +10 -10
  350. package/src/scripts/_cli/cmd_doctor_neighbours.ts +133 -0
  351. package/src/scripts/_cli/cmd_explain.ts +72 -14
  352. package/src/scripts/_cli/cmd_git_convention.ts +637 -0
  353. package/src/scripts/_cli/cmd_settings_check.ts +66 -8
  354. package/src/scripts/_cli/cmd_settings_get.ts +18 -3
  355. package/src/scripts/_cli/doctor_execution.ts +72 -1
  356. package/src/scripts/_dispatch.bash +37 -2
  357. package/src/scripts/_lib/agent_settings.ts +15 -124
  358. package/src/scripts/_lib/blocked_by_marker.ts +1 -1
  359. package/src/scripts/_lib/compile_time_toggles.ts +12 -21
  360. package/src/scripts/_lib/dispatch_import_closure.ts +163 -0
  361. package/src/scripts/_lib/git_base_ref.ts +92 -0
  362. package/src/scripts/_lib/git_convention.ts +291 -0
  363. package/src/scripts/_lib/git_convention_carrier.ts +389 -0
  364. package/src/scripts/_lib/git_convention_grammar.ts +246 -0
  365. package/src/scripts/_lib/git_convention_measure.ts +344 -0
  366. package/src/scripts/_lib/global_deploy_inventory.ts +74 -0
  367. package/src/scripts/_lib/graph_feeder_record.ts +236 -13
  368. package/src/scripts/_lib/installed_layer.ts +448 -23
  369. package/src/scripts/_lib/knowledge_global.ts +14 -1
  370. package/src/scripts/_lib/module_reach.ts +456 -0
  371. package/src/scripts/_lib/neighbour_contradictions.ts +236 -0
  372. package/src/scripts/_lib/neighbour_tool_use.ts +8 -10
  373. package/src/scripts/_lib/ratification_artifact.ts +220 -1
  374. package/src/scripts/_lib/ratification_subject.ts +92 -0
  375. package/src/scripts/_lib/release_findings_ingest.ts +15 -7
  376. package/src/scripts/_lib/release_highlights.ts +5 -0
  377. package/src/scripts/_lib/rule_law_section.ts +1 -1
  378. package/src/scripts/_lib/rule_obligations.ts +205 -0
  379. package/src/scripts/_lib/self_repair.ts +1 -1
  380. package/src/scripts/_lib/settings_removed_keys.ts +173 -0
  381. package/src/scripts/_lib/test_red_state.ts +5 -0
  382. package/src/scripts/_lib/thin_rules.ts +83 -3
  383. package/src/scripts/_lib/touched_file_quality.ts +142 -6
  384. package/src/scripts/_lib/worker_budget.ts +1 -1
  385. package/src/scripts/adr/regenerate_index.ts +16 -1
  386. package/src/scripts/ai_council/config.ts +7 -7
  387. package/src/scripts/ai_council/modification_review.ts +100 -0
  388. package/src/scripts/ai_council/modification_review_questions.ts +90 -0
  389. package/src/scripts/ai_council/orchestrator.ts +5 -5
  390. package/src/scripts/ai_council/spend_gate.ts +18 -7
  391. package/src/scripts/ai_team/config.ts +11 -16
  392. package/src/scripts/audit_user_type_axis.ts +20 -8
  393. package/src/scripts/bench_ab_clone.ts +1 -2
  394. package/src/scripts/bench_concern_spawn_path.ts +129 -0
  395. package/src/scripts/bench_graph_feeder_latency.ts +584 -0
  396. package/src/scripts/check_agent_artifact_location.ts +36 -0
  397. package/src/scripts/check_branch_freshness.ts +147 -24
  398. package/src/scripts/check_enforcement_coverage.ts +130 -0
  399. package/src/scripts/check_evaluator_budgets.ts +4 -0
  400. package/src/scripts/check_finding_dispositions.ts +88 -25
  401. package/src/scripts/check_gate_coverage.ts +4 -3
  402. package/src/scripts/check_gate_reachability.ts +48 -1
  403. package/src/scripts/check_generator_sync.ts +48 -0
  404. package/src/scripts/check_hook_bundle_composition.ts +107 -0
  405. package/src/scripts/check_kernel_edit_ratified.ts +86 -6
  406. package/src/scripts/check_pack_size.ts +1 -1
  407. package/src/scripts/check_release_manifest_content.ts +189 -0
  408. package/src/scripts/check_standing_rule_delivery.ts +96 -0
  409. package/src/scripts/check_suppression_hygiene.ts +2 -5
  410. package/src/scripts/ci_settle.ts +42 -5
  411. package/src/scripts/classify_merge_risk.ts +320 -0
  412. package/src/scripts/closure_scan.ts +91 -1
  413. package/src/scripts/command_suggester/settings.ts +8 -108
  414. package/src/scripts/command_suggester/types.ts +4 -5
  415. package/src/scripts/consumer_matrix.ts +102 -0
  416. package/src/scripts/context_hygiene_hook.ts +13 -23
  417. package/src/scripts/cost/preflight.mjs +12 -1
  418. package/src/scripts/council_cli.ts +7 -7
  419. package/src/scripts/dispatch_r2_reviewer.ts +24 -11
  420. package/src/scripts/explain_run.ts +8 -13
  421. package/src/scripts/generate_git_convention_grammar.ts +127 -0
  422. package/src/scripts/generate_host_cost_table.ts +20 -3
  423. package/src/scripts/handoff_context_hook.ts +86 -37
  424. package/src/scripts/hook_manifest.json +1 -1
  425. package/src/scripts/hook_manifest.yaml +25 -6
  426. package/src/scripts/hooks/block_config_weakening.ts +48 -5
  427. package/src/scripts/hooks/block_plumbing_writes.ts +8 -4
  428. package/src/scripts/hooks/concern_registry.ts +2 -0
  429. package/src/scripts/hooks/delegation_nudge_hook.ts +2 -3
  430. package/src/scripts/hooks/evidence_independence.ts +12 -4
  431. package/src/scripts/hooks/mcp_usage_observation_hook.ts +121 -0
  432. package/src/scripts/hooks/push_settle_hook.ts +4 -3
  433. package/src/scripts/hooks/rule_inject_hook.ts +44 -3
  434. package/src/scripts/hooks/state_io.ts +8 -4
  435. package/src/scripts/hooks/telemetry_usage_hook.ts +7 -46
  436. package/src/scripts/hooks/turn_end_gate_hook.ts +1 -0
  437. package/src/scripts/install-hooks.sh +42 -14
  438. package/src/scripts/install.ts +7 -31
  439. package/src/scripts/lint_blocker_citations.ts +198 -0
  440. package/src/scripts/lint_evidence_artifacts.ts +9 -0
  441. package/src/scripts/lint_hook_concern_budget.ts +10 -21
  442. package/src/scripts/lint_pack_boundaries.ts +406 -28
  443. package/src/scripts/lint_roadmap_blockers.ts +282 -29
  444. package/src/scripts/lint_roadmap_complexity.ts +4 -2
  445. package/src/scripts/lint_rule_law_section.ts +151 -21
  446. package/src/scripts/lint_rule_skill_pack_reach.ts +17 -0
  447. package/src/scripts/lint_settings_classes.ts +1 -1
  448. package/src/scripts/lint_ticked_unmet_criteria.ts +165 -0
  449. package/src/scripts/lint_workflow_security.ts +12 -1
  450. package/src/scripts/measure_skill_ranker_baseline.ts +5 -0
  451. package/src/scripts/model_rule_injection.ts +3 -3
  452. package/src/scripts/onboarding_gate_hook.ts +7 -6
  453. package/src/scripts/print_required_checks.ts +15 -3
  454. package/src/scripts/probe_host_compliance.ts +33 -5
  455. package/src/scripts/probe_turnaround.ts +183 -3
  456. package/src/scripts/ratification_header.ts +77 -0
  457. package/src/scripts/refusal_envelope.ts +138 -0
  458. package/src/scripts/release_publication.ts +29 -3
  459. package/src/scripts/report_dispatch_import_closure.ts +76 -0
  460. package/src/scripts/report_installed_rule_links.ts +118 -1
  461. package/src/scripts/report_module_reach.ts +212 -0
  462. package/src/scripts/report_obligation_mechanism.ts +16 -18
  463. package/src/scripts/report_parked_blockers.ts +222 -0
  464. package/src/scripts/report_required_checks_drift.ts +196 -0
  465. package/src/scripts/report_skill_ranker_confusion.ts +103 -2
  466. package/src/scripts/report_standing_payload_by_host.ts +1 -1
  467. package/src/scripts/routing_doctor.ts +2 -2
  468. package/src/scripts/schemas/agent-settings.schema.json +17 -31
  469. package/src/scripts/schemas/command.schema.json +1 -1
  470. package/src/scripts/schemas/skill.schema.json +1 -1
  471. package/src/scripts/self_review_gate.ts +39 -2
  472. package/src/scripts/skill_tools/score_skill_relevance.ts +28 -19
  473. package/src/scripts/sync_pr_branch.ts +403 -132
  474. package/src/scripts/trigger_eval_grandfather.json +0 -16
  475. package/src/server/schemas/settings.ts +33 -177
  476. package/src/shared/skillRanking.ts +53 -4
  477. package/dist/cli-delegate/chunk-NYX3L2PZ.js +0 -99
  478. package/src/scripts/_lib/config_chain.ts +0 -477
  479. package/src/scripts/_lib/conformance_report.ts +0 -403
  480. package/src/scripts/_lib/eval_discrimination.ts +0 -95
  481. package/src/scripts/_lib/evaluator_promotion.ts +0 -225
  482. package/src/scripts/_lib/file_slicer.ts +0 -117
  483. package/src/scripts/_lib/legacy_boundary_map.ts +0 -228
  484. package/src/scripts/_lib/minimality_tiebreak.ts +0 -168
  485. package/src/scripts/_lib/overbuild_lens_contract.ts +0 -196
  486. package/src/scripts/_lib/role_split.ts +0 -338
@@ -26,8 +26,36 @@
26
26
  * conflict-count reduction and must not be banked as drawdown (AI council
27
27
  * 2026-08-21, both seats; roadmap road-to-merge-hotspot-drawdown).
28
28
  *
29
- * Exit codes: 0 = already current, or merged cleanly · 1 = conflict, or the base
30
- * could not be resolved · 2 = internal error. `scanned:` on every path.
29
+ * The base is the explicit `--base`, else the default branch. Nothing here asks
30
+ * a forge which pull request the branch belongs to: a caller acting on a pull
31
+ * request passes its base, and without `--base` the run is about the default
32
+ * branch, which is right only for a branch that targets it.
33
+ *
34
+ * Exit codes: 0 = already current, or merged cleanly, or `unverified` (the
35
+ * target resolved but its commit could not be fetched, or a base ref's own
36
+ * fetch failed; nothing was touched) ·
37
+ * 1 = conflict, git refusing the merge before it starts (a dirty tree, an
38
+ * untracked file in the way — git's own line is printed), a merge that stopped
39
+ * part-way (MERGE_HEAD set, or the merge half-applied in the index — the line
40
+ * names `git merge --abort` or `git reset --merge`), or the base could
41
+ * not be resolved (no `--base` and no default
42
+ * branch, a `--base` the server does not know, no origin, or origin unreachable
43
+ * at the ref lookup) · 2 = internal error · 3 = behind, and
44
+ * `git.update_strategy` is not `merge`, so the merge was refused (reason code
45
+ * `TARGET_POLICY_STALE` when the branch is current with its non-default target
46
+ * and that target is itself behind the default branch the policy adds — the
47
+ * target's update, not this branch's; a branch also behind the target gets the
48
+ * ordinary behind line first, naming the target's lag as a note) · 4 =
49
+ * `git.update_strategy` cannot be read (a settings file or a committed
50
+ * `.git-convention.yml` that does not parse, a value outside the schema, or a
51
+ * value only a user-global file carries), so nothing was checked or merged; the line carries a stable reason
52
+ * code (`git-convention-malformed` / `-invalid` / `-discarded` /
53
+ * `-unresolvable`) and the file.
54
+ * `scanned:` on every path.
55
+ *
56
+ * The merge has its own timeout, 300 s, since it runs hooks and writes the work
57
+ * tree; `AGENT_CONFIG_SYNC_MERGE_TIMEOUT_MS` overrides it. Every other git call
58
+ * stays on the 8 s network bound.
31
59
  */
32
60
 
33
61
  import * as path from 'node:path';
@@ -40,9 +68,25 @@ import {
40
68
  loadPolicyAtSha,
41
69
  type ShaFileReader,
42
70
  } from './_lib/branch_convergence.js';
71
+ import { checkoutSource, describeRefusal, isRefusal, readGitConventionKey, type GitConventionReading } from './_lib/git_convention.js';
72
+ import {
73
+ conventionRoot,
74
+ ignoredCarrierWarning,
75
+ makeTargetDeps,
76
+ memoTargetDeps,
77
+ parseSymrefDefault,
78
+ readCommittedConvention,
79
+ resolveTarget,
80
+ type ConventionTarget,
81
+ type TargetDeps,
82
+ } from './_lib/git_convention_carrier.js';
83
+ import { baseValueError, splitResolvedRef } from './_lib/git_base_ref.js';
43
84
  import { reportScanned } from './_lib/scan_scope.js';
44
85
 
86
+ export { parseSymrefDefault };
87
+
45
88
  const NETWORK_TIMEOUT_MS = 8_000;
89
+ const MERGE_TIMEOUT_MS = 300_000;
46
90
 
47
91
  /** Paths that are GENERATED — a conflict here is regenerated, never hand-merged. */
48
92
  const GENERATED = [
@@ -165,14 +209,28 @@ const GENERATED_PATTERNS: readonly { readonly re: RegExp; readonly why: string }
165
209
  */
166
210
  const REMEASURED = ['src/config/gate-violation-baselines.json'];
167
211
 
168
- function sh(cmd: string, args: readonly string[], cwd: string): { ok: boolean; out: string; err: string } {
212
+ function sh(
213
+ cmd: string,
214
+ args: readonly string[],
215
+ cwd: string,
216
+ timeout: number = NETWORK_TIMEOUT_MS,
217
+ ): { ok: boolean; out: string; err: string; killed: boolean } {
169
218
  const r = spawnSync(cmd, [...args], {
170
219
  cwd,
171
220
  encoding: 'utf-8',
172
- timeout: NETWORK_TIMEOUT_MS,
221
+ timeout,
173
222
  maxBuffer: 32 * 1024 * 1024,
174
223
  });
175
- return { ok: r.status === 0, out: r.stdout ?? '', err: (r.stderr ?? '').trim() };
224
+ return { ok: r.status === 0, out: r.stdout ?? '', err: (r.stderr ?? '').trim(), killed: r.signal !== null };
225
+ }
226
+
227
+ /**
228
+ * A merge runs hooks and writes the work tree, so the network timeout is the
229
+ * wrong bound for it: killed mid-flight it leaves MERGE_HEAD behind.
230
+ */
231
+ function mergeTimeoutMs(): number {
232
+ const n = Number(process.env['AGENT_CONFIG_SYNC_MERGE_TIMEOUT_MS']);
233
+ return Number.isInteger(n) && n > 0 ? n : MERGE_TIMEOUT_MS;
176
234
  }
177
235
 
178
236
  /** True when `rel` is a generated artefact rather than an authored one. */
@@ -232,6 +290,28 @@ export interface Plan {
232
290
  remeasured: string[];
233
291
  authored: string[];
234
292
  scanned: number;
293
+ /** Base refs the branch is behind — set by a dry run, so a caller can decide without parsing `message`. */
294
+ behind?: number;
295
+ /**
296
+ * Set when the policy added the default branch and the TARGET itself is
297
+ * behind it: updating the branch onto the target cannot make it current.
298
+ * `branchBehind` is how far the branch is behind the target itself; only at
299
+ * 0 is the target's update the one a reader needs, not this branch's.
300
+ */
301
+ targetStale?: { target: string; defaultRef: string; behind: number; branchBehind: number };
302
+ /** The base-set summary and the per-ref behind list a dry run found, so a caller can word its own line. */
303
+ summary?: string;
304
+ behindDetail?: string;
305
+ }
306
+
307
+ /** The refusal for a branch that is behind under a strategy other than `merge`. */
308
+ export function behindRefusalMessage(plan: Plan, strategy: string): string {
309
+ const t = plan.targetStale;
310
+ return (
311
+ `⚠️ sync_pr_branch: refused — the branch is behind and git.update_strategy is \`${strategy}\`; this script only merges. ` +
312
+ `Rebase on request instead (git-workflow references/branch-update.md). ${plan.summary ?? ''}. Behind: ${plan.behindDetail ?? '?'}.` +
313
+ (t === undefined ? '' : ` The target ${t.target} is itself ${String(t.behind)} commit(s) behind ${t.defaultRef}; once this branch is current with it, the target needs updating too.`)
314
+ );
235
315
  }
236
316
 
237
317
  /**
@@ -297,15 +377,14 @@ export function renderConflictReport(plan: Plan): string {
297
377
  /**
298
378
  * Why the base is a SET now, and what each entry means.
299
379
  *
300
- * The old resolution was an EXCLUSIVE chain — `--base`, then the open PR's
301
- * base, then `origin/HEAD` — so a PR targeting a release line or a stacked
380
+ * The old resolution was an EXCLUSIVE chain — `--base`, then `origin/HEAD` —
381
+ * so a PR targeting a release line or a stacked
302
382
  * parent was kept current with its target and arbitrarily stale against the
303
383
  * default branch. Whether the default belongs in the set is a policy question
304
384
  * decided per target and read from the TARGET's own commit; see
305
385
  * `_lib/branch_convergence.ts` for the decision and the trust boundary.
306
386
  */
307
387
  export type BaseReason =
308
- | 'pull-request-target'
309
388
  | 'explicit-base-override'
310
389
  | 'repository-default-branch'
311
390
  | 'branch-convergence-policy:include-default';
@@ -327,7 +406,7 @@ export interface ResolveBaseResult {
327
406
  readonly policyStatus: 'applied' | 'not-required' | 'disabled';
328
407
  }
329
408
 
330
- /** Neither an open PR nor a default branch answered. Distinct from a missing policy. */
409
+ /** Neither `--base` nor a default branch answered. Distinct from a missing policy. */
331
410
  export class UnresolvableBase extends Error {
332
411
  constructor(detail: string) {
333
412
  super(`unresolvable — ${detail}`);
@@ -339,66 +418,17 @@ export class UnresolvableBase extends Error {
339
418
  * The git questions base resolution asks, as data.
340
419
  *
341
420
  * Injected rather than called inline so the twelve council fixtures can be
342
- * exercised without a network, a forge, or a real release line — this
421
+ * exercised without a network or a real release line — this
343
422
  * repository has none, so a live fixture could only ever cover the
344
423
  * default-target path.
345
424
  */
346
- export interface BaseDeps {
347
- readonly currentBranch: () => string;
348
- /** The open PR's `baseRefName`, bare (`release/1.x`), or null. */
349
- readonly prBase: (branch: string) => string | null;
350
- /** The default branch as a remote-tracking ref (`origin/main`), or null. */
351
- readonly defaultBranch: () => string | null;
352
- /** The SHA the server reports for a ref right now, or null. */
353
- readonly remoteSha: (ref: string) => string | null;
425
+ export interface BaseDeps extends TargetDeps {
354
426
  readonly readAtSha: ShaFileReader;
355
427
  }
356
428
 
357
429
  export function makeGitDeps(repo: string): BaseDeps {
358
430
  return {
359
- currentBranch: (): string => sh('git', ['rev-parse', '--abbrev-ref', 'HEAD'], repo).out.trim(),
360
- prBase: (branch: string): string | null => {
361
- if (branch === '' || branch === 'HEAD') return null;
362
- // The forge knows the REAL base, which matters for a stacked or
363
- // release-line PR: measuring against the repo default would compare
364
- // against a branch this PR never merges into.
365
- const pr = sh('gh', ['pr', 'list', '--head', branch, '--state', 'open', '--json', 'baseRefName', '--limit', '1'], repo);
366
- if (!pr.ok) return null;
367
- try {
368
- const rows = JSON.parse(pr.out || '[]') as Array<{ baseRefName?: string }>;
369
- const b = rows[0]?.baseRefName;
370
- return typeof b === 'string' && b !== '' ? b : null;
371
- } catch {
372
- return null;
373
- }
374
- },
375
- // ASK THE SERVER first, and only then fall back to the local ref.
376
- //
377
- // The local-only form was this module's whole answer until the default
378
- // branch became load-bearing here: the policy's `include` needs a ref to
379
- // add, and the target-is-default identity check needs one to compare
380
- // against. Measured in this worktree on 2026-09-03 — `refs/remotes/
381
- // origin/HEAD` is not set, so the local form returned null and
382
- // `sync_pr_branch` reported "no open PR and no origin/HEAD" while
383
- // `check_branch_freshness`, standing three lines away in the same
384
- // sequence, resolved `origin/main` from the server symref and passed.
385
- // Two resolvers disagreeing about the base is the defect this roadmap
386
- // exists to close, one layer down. Mirrors
387
- // `check_branch_freshness.ts:223` `serverDefaultBase`.
388
- defaultBranch: (): string | null => {
389
- const remote = sh('git', ['ls-remote', '--symref', 'origin', 'HEAD'], repo);
390
- const fromServer = remote.ok ? parseSymrefDefault(remote.out) : null;
391
- if (fromServer !== null) return fromServer;
392
- const head = sh('git', ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], repo);
393
- return head.ok && head.out.trim() !== '' ? head.out.trim() : null;
394
- },
395
- remoteSha: (ref: string): string | null => {
396
- const bare = ref.replace(/^origin\//, '');
397
- const out = sh('git', ['ls-remote', '--heads', 'origin', bare], repo);
398
- if (!out.ok) return null;
399
- const sha = out.out.trim().split(/\s+/)[0];
400
- return sha !== undefined && /^[0-9a-f]{40}$/.test(sha) ? sha : null;
401
- },
431
+ ...makeTargetDeps(repo),
402
432
  // `git show <sha>:<path>` and nothing else. There is no filesystem read
403
433
  // here by construction: a policy the PR head carries must not be able to
404
434
  // change the criteria the PR is judged against.
@@ -409,36 +439,15 @@ export function makeGitDeps(repo: string): BaseDeps {
409
439
  };
410
440
  }
411
441
 
412
- /**
413
- * The default branch the SERVER reports, from `git ls-remote --symref origin HEAD`.
414
- *
415
- * Pure and exported so the parse is testable without a network round trip, and
416
- * because it closes a measured defect rather than a hypothetical one. Until
417
- * 2026-09-03 this module read the default branch from `refs/remotes/origin/HEAD`
418
- * alone — a clone-time ref that is simply absent in some checkouts. Measured in
419
- * a worktree of this repository the same day: the local form returned null, so
420
- * `sync_pr_branch` refused with "no open PR and no origin/HEAD" while
421
- * `check_branch_freshness`, three lines later in the same documented sequence,
422
- * resolved `origin/main` from the server symref and passed. Two resolvers
423
- * disagreeing about the base is this roadmap's own subject one layer down, and
424
- * the default branch stopped being cosmetic here the moment the policy's
425
- * `include` needed a ref to add. Mirrors `check_branch_freshness.ts:223`.
426
- */
427
- export function parseSymrefDefault(lsRemoteOut: string): string | null {
428
- for (const line of lsRemoteOut.split('\n')) {
429
- const t = line.trim();
430
- if (!t.startsWith('ref:')) continue;
431
- const name = t.slice('ref:'.length).trim().split(/\s+/)[0] ?? '';
432
- if (!name.startsWith('refs/heads/')) continue;
433
- const short = name.slice('refs/heads/'.length).trim();
434
- if (short !== '') return `origin/${short}`;
435
- }
436
- return null;
437
- }
438
-
439
442
  /** Strip the remote prefix so a policy key is the branch name a human writes. */
440
443
  function bareName(ref: string): string {
441
- return ref.replace(/^origin\//, '');
444
+ return splitResolvedRef(ref).branch;
445
+ }
446
+
447
+ function sameRemoteBranch(a: string, b: string): boolean {
448
+ const x = splitResolvedRef(a);
449
+ const y = splitResolvedRef(b);
450
+ return x.remote === y.remote && x.branch === y.branch;
442
451
  }
443
452
 
444
453
  /**
@@ -453,27 +462,24 @@ function bareName(ref: string): string {
453
462
  export function resolveBase(repo: string, override: string | null, deps: BaseDeps = makeGitDeps(repo)): ResolveBaseResult {
454
463
  const defaultRef = deps.defaultBranch();
455
464
 
456
- let target: BaseEntry;
457
- if (override !== null && override.trim() !== '') {
458
- target = { ref: override.trim(), reason: 'explicit-base-override' };
459
- } else {
460
- const pr = deps.prBase(deps.currentBranch());
461
- if (pr !== null) {
462
- target = { ref: `origin/${pr}`, reason: 'pull-request-target' };
463
- } else if (defaultRef !== null) {
464
- target = { ref: defaultRef, reason: 'repository-default-branch' };
465
- } else {
466
- throw new UnresolvableBase('no open PR and no origin/HEAD');
467
- }
465
+ const target = resolveTarget(deps, override, defaultRef);
466
+ if (target === null) {
467
+ throw new UnresolvableBase('no --base and no origin/HEAD');
468
468
  }
469
469
 
470
- // A PR targeting the default branch needs no entry — identity by ref name,
471
- // or by the SHA both names resolve to.
472
- const sameName = defaultRef !== null && bareName(target.ref) === bareName(defaultRef);
470
+ // A PR targeting the default branch needs no entry — identity by remote and
471
+ // branch name, or by the SHA both names resolve to. The branch name alone is
472
+ // not identity: `upstream/main` can sit on a different commit than `origin/main`.
473
+ const sameName = defaultRef !== null && sameRemoteBranch(target.ref, defaultRef);
473
474
  const targetSha = deps.remoteSha(target.ref);
474
475
  const defaultSha = defaultRef === null ? null : deps.remoteSha(defaultRef);
475
476
  const sameSha = targetSha !== null && defaultSha !== null && targetSha === defaultSha;
476
- if (sameName || sameSha) {
477
+ // A fork layout: the pull request targets the default branch of the
478
+ // remote it names (`upstream/main`), not a release line of origin.
479
+ const targetRemote = splitResolvedRef(target.ref).remote;
480
+ const ownDefault = sameName || sameSha || (defaultRef !== null && splitResolvedRef(defaultRef).remote === targetRemote) ? null : (deps.defaultBranchOf?.(targetRemote) ?? null);
481
+ const ownName = ownDefault !== null && sameRemoteBranch(target.ref, ownDefault);
482
+ if (sameName || sameSha || ownName) {
477
483
  return { entries: [target], policyStatus: 'not-required' };
478
484
  }
479
485
 
@@ -527,8 +533,6 @@ export function describeReason(reason: BaseReason): string {
527
533
  switch (reason) {
528
534
  case 'explicit-base-override':
529
535
  return 'given by --base';
530
- case 'pull-request-target':
531
- return 'the open PR base';
532
536
  case 'repository-default-branch':
533
537
  return 'the repo default branch';
534
538
  case 'branch-convergence-policy:include-default':
@@ -569,6 +573,8 @@ export interface IntegrationAttempt {
569
573
 
570
574
  export interface IntegrationOutcome {
571
575
  readonly ok: boolean;
576
+ /** Set when `checkPin` refused the pinned set: nothing was merged. */
577
+ readonly stopped?: boolean;
572
578
  readonly attempts: readonly IntegrationAttempt[];
573
579
  readonly conflicted: readonly string[];
574
580
  readonly message: string;
@@ -577,7 +583,10 @@ export interface IntegrationOutcome {
577
583
  /** The git operations the retry loop needs, injected so a moving base is testable. */
578
584
  export interface IntegrateOps {
579
585
  readonly remoteSha: (ref: string) => string | null;
580
- readonly merge: (ref: string) => { ok: boolean; conflicted: string[] };
586
+ /** Merges `sha`, the pinned commit of `ref`; `error` is a refusal that is not a conflict. */
587
+ readonly merge: (ref: string, sha: string | null) => { ok: boolean; conflicted: string[]; error?: string };
588
+ /** Asked once, before the first merge, with the pinned OIDs; a message stops the run. */
589
+ readonly checkPin?: (pinned: readonly { ref: string; before: string | null }[]) => string | null;
581
590
  }
582
591
 
583
592
  function renderAttempts(attempts: readonly IntegrationAttempt[]): string {
@@ -600,10 +609,15 @@ export function integrateWithPinnedBase(refs: readonly string[], ops: IntegrateO
600
609
  const attempts: IntegrationAttempt[] = [];
601
610
  for (let n = 1; n <= MAX_BASE_ATTEMPTS; n++) {
602
611
  const pinned = refs.map((ref) => ({ ref, before: ops.remoteSha(ref) }));
612
+ // Only before the first merge: once one has landed, a "nothing was
613
+ // merged" stop would misreport the tree, so a later move is a base move.
614
+ const stop = n === 1 ? (ops.checkPin?.(pinned) ?? null) : null;
615
+ if (stop !== null) return { ok: false, stopped: true, attempts, conflicted: [], message: stop };
603
616
  let conflicted: string[] = [];
604
617
  let clean = true;
605
618
  for (const q of pinned) {
606
- const m = ops.merge(q.ref);
619
+ const m = ops.merge(q.ref, q.before);
620
+ if (!m.ok && m.error !== undefined) return { ok: false, attempts, conflicted: [], message: m.error };
607
621
  if (!m.ok) {
608
622
  clean = false;
609
623
  conflicted = m.conflicted;
@@ -639,6 +653,69 @@ export function integrateWithPinnedBase(refs: readonly string[], ops: IntegrateO
639
653
  };
640
654
  }
641
655
 
656
+ /**
657
+ * Merge exactly the commit the pin and the policy check were about.
658
+ *
659
+ * The run fetched once, before the pin, so the local tracking ref may name an
660
+ * older commit, or a newer one when the policy re-read fetched. It is fetched
661
+ * again when it differs from the pin, and the pinned SHA itself is merged when
662
+ * the ref still names another commit. A merge that leaves HEAD without the
663
+ * pinned commit is a refusal, never a success.
664
+ */
665
+ function mergePinned(repo: string, ref: string, sha: string | null): { ok: boolean; conflicted: string[]; error?: string } {
666
+ const tracking = (): string => sh('git', ['rev-parse', '--verify', '-q', `${ref}^{commit}`], repo).out.trim();
667
+ let target = ref;
668
+ if (sha !== null && tracking() !== sha) {
669
+ const { remote, branch } = splitResolvedRef(ref);
670
+ sh('git', ['fetch', '--', remote, `+refs/heads/${branch}:refs/remotes/${remote}/${branch}`], repo);
671
+ if (tracking() !== sha) {
672
+ if (!sh('git', ['cat-file', '-e', `${sha}^{commit}`], repo).ok) {
673
+ return { ok: false, conflicted: [], error: `the pinned commit ${sha.slice(0, 12)} of ${ref} could not be fetched — nothing was merged for it.` };
674
+ }
675
+ target = sha;
676
+ }
677
+ }
678
+ const indexWasClean = sh('git', ['diff', '--cached', '--quiet'], repo).ok;
679
+ const m = sh('git', ['merge', '--no-edit', target], repo, mergeTimeoutMs());
680
+ if (!m.ok) {
681
+ // A merge git refuses before it starts (local changes it would
682
+ // overwrite, an untracked file in the way) leaves no unmerged path.
683
+ const conflicted = sh('git', ['diff', '--name-only', '--diff-filter=U'], repo).out.split('\n').filter((p) => p.trim() !== '');
684
+ if (conflicted.length > 0) return { ok: false, conflicted };
685
+ // A staged difference that was not there before came from this merge —
686
+ // a killed git leaves one with no MERGE_HEAD to abort. Staged work the
687
+ // user had is not a half-applied merge, and resetting it would lose it.
688
+ const how = m.killed ? `was stopped after ${String(mergeTimeoutMs() / 1000)} s` : 'did not complete';
689
+ if (sh('git', ['rev-parse', '-q', '--verify', 'MERGE_HEAD'], repo).ok) {
690
+ return {
691
+ ok: false,
692
+ conflicted: [],
693
+ error:
694
+ `the merge of ${ref} ${how} and left a merge in progress (MERGE_HEAD is set) — ` +
695
+ 'finish it with `git commit` or discard it with `git merge --abort`.',
696
+ };
697
+ }
698
+ if (indexWasClean && !sh('git', ['diff', '--cached', '--quiet'], repo).ok) {
699
+ return {
700
+ ok: false,
701
+ conflicted: [],
702
+ error:
703
+ `the merge of ${ref} ${how} and left it half-applied in the index (no MERGE_HEAD) — ` +
704
+ 'discard it with `git reset --merge`.',
705
+ };
706
+ }
707
+ if (m.killed) {
708
+ return { ok: false, conflicted: [], error: `the merge of ${ref} was stopped after ${String(mergeTimeoutMs() / 1000)} s before it changed anything — nothing was merged for it.` };
709
+ }
710
+ const why = (m.err || m.out).trim().split('\n').join(' ');
711
+ return { ok: false, conflicted: [], error: `git refused to merge ${ref}: ${why || 'no reason given'} — nothing was merged for it.` };
712
+ }
713
+ if (sha !== null && !sh('git', ['merge-base', '--is-ancestor', sha, 'HEAD'], repo).ok) {
714
+ return { ok: false, conflicted: [], error: `the merge of ${ref} left HEAD without its pinned commit ${sha.slice(0, 12)}.` };
715
+ }
716
+ return { ok: true, conflicted: [] };
717
+ }
718
+
642
719
  /** The regeneration operations Phase 3 needs, injected so no real conflict is required. */
643
720
  export interface RegenOps {
644
721
  readonly regenerate: () => { ok: boolean; err: string };
@@ -731,10 +808,19 @@ function gitRegenOps(repo: string): RegenOps {
731
808
  };
732
809
  }
733
810
 
734
- export function sync(repo: string, baseOverride: string | null, dryRun: boolean, autoResolve = false): Plan {
811
+ /**
812
+ * What the merge path needs beyond the base set. `live` answers the last-moment
813
+ * pin and is never memoised — a memoised pin could not see the target move.
814
+ */
815
+ export interface SyncOptions {
816
+ readonly live?: TargetDeps;
817
+ readonly checkPin?: IntegrateOps['checkPin'];
818
+ }
819
+
820
+ export function sync(repo: string, baseOverride: string | null, dryRun: boolean, autoResolve = false, deps?: BaseDeps, opts: SyncOptions = {}): Plan {
735
821
  let resolved: ResolveBaseResult;
736
822
  try {
737
- resolved = resolveBase(repo, baseOverride);
823
+ resolved = resolveBase(repo, baseOverride, deps ?? makeGitDeps(repo));
738
824
  } catch (exc) {
739
825
  // A missing policy, an unresolvable target SHA and an unresolvable base
740
826
  // are all REFUSALS carrying their own typed message. None of them
@@ -751,13 +837,14 @@ export function sync(repo: string, baseOverride: string | null, dryRun: boolean,
751
837
  const summary = renderBaseSummary(resolved);
752
838
  const order = integrationOrder(resolved);
753
839
 
754
- const fetched = sh('git', ['fetch', 'origin', '--prune'], repo);
755
- if (!fetched.ok) {
840
+ const remotes = [...new Set(['origin', ...order.map((ref) => splitResolvedRef(ref).remote)])];
841
+ const failed = remotes.map((remote) => ({ remote, ...sh('git', ['fetch', '--prune', '--', remote], repo) })).find((f) => !f.ok);
842
+ if (failed !== undefined) {
756
843
  // Unreachable remote is not "already current" — saying so is the whole
757
844
  // point, since a silent pass here reproduces the staleness this closes.
758
845
  return {
759
846
  exit: 0,
760
- message: `unverified — could not fetch origin (${fetched.err.split('\n')[0] ?? '?'}). Base freshness NOT checked.`,
847
+ message: `unverified — could not fetch ${failed.remote} (${failed.err.split('\n')[0] ?? '?'}). Base freshness NOT checked.`,
761
848
  generated: [],
762
849
  remeasured: [],
763
850
  authored: [],
@@ -765,10 +852,49 @@ export function sync(repo: string, baseOverride: string | null, dryRun: boolean,
765
852
  };
766
853
  }
767
854
 
768
- const behindEach = order.map((ref) => ({
769
- ref,
770
- behind: Number(sh('git', ['rev-list', '--count', `HEAD..${ref}`], repo).out.trim() || '0'),
771
- }));
855
+ // A single-branch clone's refspec never moves a base tracking ref fetched
856
+ // once by hand, so the remote-wide fetch above can leave it stale and the
857
+ // count below would read "already current". Each ref is fetched by name.
858
+ // A failed one is unverified like the remote-wide fetch: the ref still names
859
+ // whatever it named before, and counting it would read a stale "current".
860
+ for (const ref of order) {
861
+ const { remote, branch } = splitResolvedRef(ref);
862
+ const f = sh('git', ['fetch', '-q', '--', remote, `+refs/heads/${branch}:refs/remotes/${remote}/${branch}`], repo);
863
+ if (!f.ok) {
864
+ return {
865
+ exit: 0,
866
+ message: `unverified — could not fetch ${ref} (${f.err.split('\n')[0] ?? '?'}). Base freshness NOT checked.`,
867
+ generated: [],
868
+ remeasured: [],
869
+ authored: [],
870
+ scanned: 0,
871
+ };
872
+ }
873
+ }
874
+
875
+ // A count git could not take is not a count of 0: a ref this checkout never
876
+ // fetched (a single-branch clone) would otherwise read as "already current".
877
+ const countBehind = (from: string, ref: string): number | null => {
878
+ const r = sh('git', ['rev-list', '--count', `${from}..${ref}`], repo);
879
+ const n = Number(r.out.trim());
880
+ return r.ok && r.out.trim() !== '' && Number.isInteger(n) ? n : null;
881
+ };
882
+ const uncountable = (ref: string): Plan => ({
883
+ exit: 1,
884
+ message:
885
+ `cannot count commits behind ${ref} — it is not in this checkout (a single-branch clone fetches only its own branch). ` +
886
+ `Fetch it: git fetch -- ${splitResolvedRef(ref).remote} +refs/heads/${bareName(ref)}:refs/remotes/${splitResolvedRef(ref).remote}/${bareName(ref)}`,
887
+ generated: [],
888
+ remeasured: [],
889
+ authored: [],
890
+ scanned: 0,
891
+ });
892
+ const behindEach: { ref: string; behind: number }[] = [];
893
+ for (const ref of order) {
894
+ const behind = countBehind('HEAD', ref);
895
+ if (behind === null) return uncountable(ref);
896
+ behindEach.push({ ref, behind });
897
+ }
772
898
  // De-duplicated by ancestry: a ref already contained in HEAD is 0 behind and
773
899
  // is not merged again, which is what keeps a target that already contains
774
900
  // the default from being merged twice.
@@ -778,28 +904,33 @@ export function sync(repo: string, baseOverride: string | null, dryRun: boolean,
778
904
  }
779
905
  if (dryRun) {
780
906
  const detail = stale.map((b) => `${b.ref} (${String(b.behind)} behind)`).join(', ');
907
+ const added = resolved.entries.find((e) => e.reason === 'branch-convergence-policy:include-default');
908
+ const target = resolved.entries[0]?.ref ?? '';
909
+ const targetBehind = added === undefined ? 0 : countBehind(target, added.ref);
910
+ if (targetBehind === null) return uncountable(added?.ref ?? target);
781
911
  return {
912
+ ...(targetBehind > 0 && added !== undefined
913
+ ? { targetStale: { target, defaultRef: added.ref, behind: targetBehind, branchBehind: behindEach.find((b) => b.ref === target)?.behind ?? 0 } }
914
+ : {}),
782
915
  exit: 0,
783
916
  message: `${summary}. Behind: ${detail} — would merge in that order. Dry run, nothing changed.`,
917
+ summary,
918
+ behindDetail: detail,
784
919
  generated: [],
785
920
  remeasured: [],
786
921
  authored: [],
787
922
  scanned: order.length,
923
+ behind: stale.length,
788
924
  };
789
925
  }
790
926
 
927
+ const live = opts.live ?? makeGitDeps(repo);
791
928
  const outcome = integrateWithPinnedBase(
792
929
  stale.map((b) => b.ref),
793
930
  {
794
- remoteSha: (ref: string): string | null => makeGitDeps(repo).remoteSha(ref),
795
- merge: (ref: string): { ok: boolean; conflicted: string[] } => {
796
- const m = sh('git', ['merge', ref, '--no-edit'], repo);
797
- if (m.ok) return { ok: true, conflicted: [] };
798
- return {
799
- ok: false,
800
- conflicted: sh('git', ['diff', '--name-only', '--diff-filter=U'], repo).out.split('\n'),
801
- };
802
- },
931
+ remoteSha: (ref: string): string | null => live.remoteSha(ref),
932
+ ...(opts.checkPin === undefined ? {} : { checkPin: opts.checkPin }),
933
+ merge: (ref: string, sha: string | null) => mergePinned(repo, ref, sha),
803
934
  },
804
935
  );
805
936
 
@@ -816,8 +947,8 @@ export function sync(repo: string, baseOverride: string | null, dryRun: boolean,
816
947
  };
817
948
  }
818
949
  if (outcome.conflicted.length === 0) {
819
- // The base kept moving. No conflict to classify; the evidence IS the
820
- // per-attempt OID list already in the message.
950
+ // The base kept moving, or moved onto a commit carrying another
951
+ // strategy. No conflict to classify; the message carries the OIDs.
821
952
  return { exit: 1, message: `${summary}. ${outcome.message}`, generated: [], remeasured: [], authored: [], scanned: order.length };
822
953
  }
823
954
 
@@ -845,7 +976,82 @@ export function sync(repo: string, baseOverride: string | null, dryRun: boolean,
845
976
  };
846
977
  }
847
978
 
848
- export function main(argv?: readonly string[]): number {
979
+ /**
980
+ * `git.update_strategy` in force for the repository: the committed
981
+ * `.git-convention.yml` at the target commit, over the developer layers
982
+ * (ADR-283). Absent everywhere reads as `merge`, the behaviour before the key
983
+ * existed; a file that does not parse, a typo, a user-global-only value and a
984
+ * target commit that cannot be resolved are refusals here.
985
+ */
986
+ export function updateStrategy(repo: string, base: string | null = null, deps?: TargetDeps): GitConventionReading {
987
+ return readStrategy(repo, base, deps).reading;
988
+ }
989
+
990
+ function readStrategy(repo: string, base: string | null, deps?: TargetDeps): { reading: GitConventionReading; baseResolved: boolean; target: ConventionTarget | null } {
991
+ const read = readCommittedConvention(repo, { override: base, keys: ['update_strategy'], ...(deps ? { deps } : {}) });
992
+ return { reading: read.readings.update_strategy as GitConventionReading, baseResolved: read.target !== null && read.target.sha !== null, target: read.target };
993
+ }
994
+
995
+ export interface StrategyGate {
996
+ /** `null` — the strategy is readable, go on; otherwise the exit to return. */
997
+ exit: 0 | 1 | 4 | null;
998
+ line: string;
999
+ reading: GitConventionReading;
1000
+ /** The ref and commit the strategy was read at, when `strategyGate` resolved one. */
1001
+ target?: ConventionTarget | null;
1002
+ }
1003
+
1004
+ /**
1005
+ * The one place that maps an unreadable `git.update_strategy` to an exit.
1006
+ *
1007
+ * A developer file that is itself a refusal, and a carrier at the target that
1008
+ * does not parse or is not accepted, are exit 4. A target that resolves to no
1009
+ * commit — no `--base` and no default branch, a ref the server does not know,
1010
+ * no origin, origin unreachable at the ref lookup — is the base failure, exit
1011
+ * 1. A target the server named whose commit could not be fetched is
1012
+ * `unverified`: exit 0 with a warning, and nothing is touched.
1013
+ */
1014
+ export function strategyExit(reading: GitConventionReading, baseResolved: boolean, developer: GitConventionReading): StrategyGate {
1015
+ if (!isRefusal(reading.state)) return { exit: null, line: '', reading };
1016
+ const refused: StrategyGate = { exit: 4, line: `❌ sync_pr_branch: refused — ${describeRefusal(reading)}. Nothing was checked or merged.`, reading };
1017
+ if (reading.state !== 'unresolvable' || isRefusal(developer.state)) return refused;
1018
+ const why = reading.detail ?? 'the target commit is unknown';
1019
+ if (!baseResolved) return { exit: 1, line: `❌ sync_pr_branch: base could not be resolved — ${why}. Nothing was checked or merged.`, reading };
1020
+ return { exit: 0, line: `⚠️ sync_pr_branch: unverified — ${why}. Base freshness NOT checked, nothing was merged.`, reading };
1021
+ }
1022
+
1023
+ export function strategyGate(repo: string, base: string | null, targetDeps: BaseDeps): StrategyGate {
1024
+ const { reading, baseResolved, target } = readStrategy(repo, base, targetDeps);
1025
+ return { ...strategyExit(reading, baseResolved, readGitConventionKey('update_strategy', checkoutSource(conventionRoot(repo).root))), target };
1026
+ }
1027
+
1028
+ /** The strategy a reading puts in force, or its refusing state: what a plan is made with. */
1029
+ function strategyInForce(r: GitConventionReading): string {
1030
+ return isRefusal(r.state) ? `unreadable (${r.state})` : (r.value ?? 'merge');
1031
+ }
1032
+
1033
+ /**
1034
+ * The stop for a target that moved between the strategy read and the pin.
1035
+ *
1036
+ * The commit being integrated is governed by the policy it carries, so the
1037
+ * carrier is read again at the pinned commit through the same reader. The same
1038
+ * strategy in force there goes on against the new commit; a different one, or
1039
+ * one that cannot be read, stops before anything is merged — the plan was made
1040
+ * under a policy the integrated commit no longer carries.
1041
+ */
1042
+ export function policyMovedStop(from: string, to: string, planned: GitConventionReading, pinned: GitConventionReading): string | null {
1043
+ const was = strategyInForce(planned);
1044
+ const now = strategyInForce(pinned);
1045
+ if (was === now) return null;
1046
+ const why = isRefusal(pinned.state) ? ` (${describeRefusal(pinned)})` : '';
1047
+ return (
1048
+ `the target moved from ${from.slice(0, 12)} to ${to.slice(0, 12)} and its git.update_strategy changed from ${was} to ${now}${why} — ` +
1049
+ 'nothing was merged; run again so the plan is made under the strategy the new commit carries.'
1050
+ );
1051
+ }
1052
+
1053
+ /** `deps` answers the target questions; tests inject it, a run asks git. */
1054
+ export function main(argv?: readonly string[], deps?: BaseDeps): number {
849
1055
  const args = argv ?? process.argv.slice(2);
850
1056
  let repo = process.cwd();
851
1057
  let base: string | null = null;
@@ -873,6 +1079,12 @@ export function main(argv?: readonly string[]): number {
873
1079
  reportScanned({ gate: 'sync_pr_branch', scanned: 0, units: 'base ref(s)', roots: ['origin'], allowEmpty: 'argument error' });
874
1080
  return 1;
875
1081
  }
1082
+ const blank = baseValueError(v);
1083
+ if (blank !== null) {
1084
+ process.stderr.write(`❌ sync_pr_branch: ${blank}\n`);
1085
+ reportScanned({ gate: 'sync_pr_branch', scanned: 0, units: 'base ref(s)', roots: ['origin'], allowEmpty: 'argument error' });
1086
+ return 2;
1087
+ }
876
1088
  base = v;
877
1089
  } else if (a === '--dry-run') {
878
1090
  dryRun = true;
@@ -884,7 +1096,18 @@ export function main(argv?: readonly string[]): number {
884
1096
  process.stdout.write(
885
1097
  'usage: sync_pr_branch [--repo PATH] [--base REF] [--dry-run] [--auto-resolve-generated] [--quiet]\n' +
886
1098
  ' Merges the PR base into the current branch so the PR does not go stale.\n' +
887
- ' Resolves the base from the open PR when there is one. A conflict is\n' +
1099
+ ' Under a git.update_strategy other than merge it only checks: a current\n' +
1100
+ ' branch exits 0, a behind one exits 3 and is never merged (--dry-run and\n' +
1101
+ ' --auto-resolve-generated have nothing to change there). A strategy that\n' +
1102
+ ' cannot be read (unparsable file, typo, user-global-only) exits 4.\n' +
1103
+ ' The base is --base; without it the default branch, so a PR into any other\n' +
1104
+ ' base must pass --base <its base>; a branch name and refs/heads/<name> mean\n' +
1105
+ ' origin/<name>, and <remote>/<name> is used as given. A blank --base\n' +
1106
+ ' is a usage error, exit 2.\n' +
1107
+ ' A base that cannot be resolved or counted exits 1; one whose commit\n' +
1108
+ ' cannot be fetched is unverified, exit 0. A target that moves before the\n' +
1109
+ ' merge is read again at the new commit; another strategy there exits 1,\n' +
1110
+ ' nothing merged. A conflict is\n' +
888
1111
  ' reported and never auto-resolved; generated and authored conflicts are\n' +
889
1112
  ' listed separately because only the first has one correct resolution;\n' +
890
1113
  ' measured ratchet baselines are a third class, re-measured not merged.\n' +
@@ -903,9 +1126,52 @@ export function main(argv?: readonly string[]): number {
903
1126
  }
904
1127
  }
905
1128
 
1129
+ // This script only merges. Under any other strategy a merge of the base is
1130
+ // the commit the setting excludes, and a rebase needs the user's request per
1131
+ // git-history-discipline — so the branch is only CHECKED (dry run), and a
1132
+ // branch that is behind is refused rather than merged. A current branch
1133
+ // passes, so an automated pre-push sync stays green when nothing is to do.
1134
+ const ignored = ignoredCarrierWarning(conventionRoot(repo).root);
1135
+ if (ignored !== null) process.stdout.write(`⚠️ sync_pr_branch: ${ignored}\n`);
1136
+ const live = deps ?? makeGitDeps(repo);
1137
+ const targetDeps = memoTargetDeps(live);
906
1138
  let plan: Plan;
907
1139
  try {
908
- plan = sync(repo, base, dryRun, autoResolve);
1140
+ const gate = strategyGate(repo, base, targetDeps);
1141
+ if (gate.exit !== null) {
1142
+ process.stdout.write(`${gate.line}\n`);
1143
+ const why = gate.exit === 0 ? 'target commit not fetched — stated above' : gate.exit === 1 ? 'base unresolvable — stated above' : 'git.update_strategy unreadable';
1144
+ reportScanned({ gate: 'sync_pr_branch', scanned: 0, units: 'base ref(s)', roots: ['origin'], allowEmpty: why });
1145
+ return gate.exit;
1146
+ }
1147
+ const strategy = gate.reading.value ?? 'merge';
1148
+ if (strategy !== 'merge') {
1149
+ plan = sync(repo, base, true, false, targetDeps);
1150
+ const t = plan.targetStale;
1151
+ if (plan.exit === 0 && t !== undefined && t.branchBehind === 0) {
1152
+ process.stdout.write(
1153
+ `⚠️ sync_pr_branch: refused — TARGET_POLICY_STALE: the target ${t.target} is itself ${String(t.behind)} commit(s) behind ${t.defaultRef}, ` +
1154
+ `which the branch-convergence policy requires; updating this branch onto the target cannot make it current. ` +
1155
+ `The target needs updating first; this branch has nothing to do until then.\n`,
1156
+ );
1157
+ reportScanned({ gate: 'sync_pr_branch', scanned: plan.scanned, units: 'base ref(s)', roots: ['origin'] });
1158
+ return 3;
1159
+ }
1160
+ if (plan.exit === 0 && (plan.behind ?? 0) > 0) {
1161
+ process.stdout.write(`${behindRefusalMessage(plan, strategy)}\n`);
1162
+ reportScanned({ gate: 'sync_pr_branch', scanned: plan.scanned, units: 'base ref(s)', roots: ['origin'] });
1163
+ return 3;
1164
+ }
1165
+ } else {
1166
+ const read = gate.target ?? null;
1167
+ const checkPin = (pinned: readonly { ref: string; before: string | null }[]): string | null => {
1168
+ const now = read === null ? null : (pinned.find((p) => p.ref === read.ref)?.before ?? null);
1169
+ if (read === null || read.sha === null || now === null || now === read.sha) return null;
1170
+ const again = strategyGate(repo, base, { ...targetDeps, remoteSha: () => now });
1171
+ return policyMovedStop(read.sha, now, gate.reading, again.reading);
1172
+ };
1173
+ plan = sync(repo, base, dryRun, autoResolve, targetDeps, { live, checkPin });
1174
+ }
909
1175
  } catch (exc) {
910
1176
  reportScanned({ gate: 'sync_pr_branch', scanned: 0, units: 'base ref(s)', roots: ['origin'], allowEmpty: 'internal error' });
911
1177
  process.stderr.write(`❌ sync_pr_branch: internal error: ${exc instanceof Error ? exc.message : String(exc)}\n`);
@@ -936,9 +1202,14 @@ export function main(argv?: readonly string[]): number {
936
1202
  return plan.exit;
937
1203
  }
938
1204
 
1205
+ // Inlined into the `git:convention` delegate bundle this module shares that
1206
+ // bundle's `import.meta.url`, so the URL comparison below would run `main` at
1207
+ // import time with the verb's argv. Defined by the esbuild builds only.
1208
+ declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
939
1209
  const _HERE = fileURLToPath(import.meta.url);
940
1210
  function _isCliEntry(): boolean {
941
1211
  if (process.argv[1] === undefined) return false;
1212
+ if (typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__) return path.basename(process.argv[1], '.js') === 'sync_pr_branch';
942
1213
  return import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href || process.argv[1] === _HERE;
943
1214
  }
944
1215
  if (_isCliEntry()) {