@cursor/july 0.1.55 → 0.1.62

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 (265) hide show
  1. package/dist/bin/agent-serve.js +30 -23
  2. package/dist/channels/github/github-channel.d.ts +13 -1
  3. package/dist/channels/github/github-channel.d.ts.map +1 -1
  4. package/dist/channels/github/github-channel.js +81 -16
  5. package/dist/channels/github/types.d.ts +29 -1
  6. package/dist/channels/github/types.d.ts.map +1 -1
  7. package/dist/channels/origin/cursor-account.d.ts +22 -0
  8. package/dist/channels/origin/cursor-account.d.ts.map +1 -0
  9. package/dist/channels/origin/cursor-account.js +35 -0
  10. package/dist/channels/origin/events.d.ts +10 -0
  11. package/dist/channels/origin/events.d.ts.map +1 -0
  12. package/dist/channels/origin/events.js +27 -0
  13. package/dist/channels/origin/index.d.ts +29 -0
  14. package/dist/channels/origin/index.d.ts.map +1 -0
  15. package/dist/channels/origin/index.js +28 -0
  16. package/dist/channels/origin/origin-channel.d.ts +68 -0
  17. package/dist/channels/origin/origin-channel.d.ts.map +1 -0
  18. package/dist/channels/origin/origin-channel.js +567 -0
  19. package/dist/channels/origin/origin-webhook.d.ts +38 -0
  20. package/dist/channels/origin/origin-webhook.d.ts.map +1 -0
  21. package/dist/channels/origin/origin-webhook.js +151 -0
  22. package/dist/channels/origin/types.d.ts +152 -0
  23. package/dist/channels/origin/types.d.ts.map +1 -0
  24. package/dist/channels/origin/types.js +1 -0
  25. package/dist/channels/slack/doctor.d.ts +17 -1
  26. package/dist/channels/slack/doctor.d.ts.map +1 -1
  27. package/dist/channels/slack/doctor.js +186 -10
  28. package/dist/docs/404.html +2 -2
  29. package/dist/docs/ab.html +4 -4
  30. package/dist/docs/assets/{app.CVQxhV85.js → app.B-biogkN.js} +1 -1
  31. package/dist/docs/assets/chunks/@localSearchIndexroot.5ozYNf74.js +1 -0
  32. package/dist/docs/assets/chunks/{VPLocalSearchBox.QAPzJjql.js → VPLocalSearchBox.DZqKtFZS.js} +1 -1
  33. package/dist/docs/assets/chunks/{theme.CSxTCOFu.js → theme.BwC5zP2r.js} +2 -2
  34. package/dist/docs/assets/{example-agents_fsd.md.DPz9ezO4.js → example-agents_fsd.md.ZeGEpAw_.js} +2 -2
  35. package/dist/docs/assets/{example-agents_fsd.md.DPz9ezO4.lean.js → example-agents_fsd.md.ZeGEpAw_.lean.js} +1 -1
  36. package/dist/docs/assets/{example-agents_security-reviewer.md.Dkf1gyo6.js → example-agents_security-reviewer.md.DBL1TwtP.js} +2 -2
  37. package/dist/docs/assets/{example-agents_security-reviewer.md.Dkf1gyo6.lean.js → example-agents_security-reviewer.md.DBL1TwtP.lean.js} +1 -1
  38. package/dist/docs/assets/guides_convert-automation.md.BhMzTkE5.js +12 -0
  39. package/dist/docs/assets/guides_convert-automation.md.BhMzTkE5.lean.js +1 -0
  40. package/dist/docs/assets/{guides_github.md.B8QKtIpw.js → guides_github.md.CLLQJKRB.js} +4 -4
  41. package/dist/docs/assets/{guides_github.md.B8QKtIpw.lean.js → guides_github.md.CLLQJKRB.lean.js} +1 -1
  42. package/dist/docs/assets/index.md.DhRHS_-L.js +5 -0
  43. package/dist/docs/assets/{index.md.COeoSkcF.lean.js → index.md.DhRHS_-L.lean.js} +1 -1
  44. package/dist/docs/assets/{reference_cli.md.D_CCfG8R.js → reference_cli.md.Cl14Jfo3.js} +8 -2
  45. package/dist/docs/assets/{reference_cli.md.D_CCfG8R.lean.js → reference_cli.md.Cl14Jfo3.lean.js} +1 -1
  46. package/dist/docs/assets/scaffolding-agents.md.CrWSA0Ko.js +1 -0
  47. package/dist/docs/assets/{scaffolding-agents.md.mkc3B_ZW.lean.js → scaffolding-agents.md.CrWSA0Ko.lean.js} +1 -1
  48. package/dist/docs/assets/templates_agentic-owners.md.BkTLORaU.js +14 -0
  49. package/dist/docs/assets/templates_agentic-owners.md.BkTLORaU.lean.js +1 -0
  50. package/dist/docs/assets/templates_demo.md.Bgd6MBaZ.js +4 -0
  51. package/dist/docs/assets/templates_demo.md.Bgd6MBaZ.lean.js +1 -0
  52. package/dist/docs/assets/templates_pr-autofixer.md.DcmoeUNZ.js +8 -0
  53. package/dist/docs/assets/templates_pr-autofixer.md.DcmoeUNZ.lean.js +1 -0
  54. package/dist/docs/assets/templates_security-reviewer.md.C0yIUaYs.js +2 -0
  55. package/dist/docs/assets/templates_security-reviewer.md.C0yIUaYs.lean.js +1 -0
  56. package/dist/docs/building-with-agents.html +4 -4
  57. package/dist/docs/concepts.html +5 -5
  58. package/dist/docs/deployment.html +4 -4
  59. package/dist/docs/evals.html +4 -4
  60. package/dist/docs/example-agents/approval-buddy.html +4 -4
  61. package/dist/docs/example-agents/benny.html +4 -4
  62. package/dist/docs/example-agents/bugbot.html +4 -4
  63. package/dist/docs/example-agents/codebase-wiki.html +4 -4
  64. package/dist/docs/example-agents/codeowners-review.html +4 -4
  65. package/dist/docs/example-agents/concierge.html +4 -4
  66. package/dist/docs/example-agents/fsd.html +5 -5
  67. package/dist/docs/example-agents/index.html +4 -4
  68. package/dist/docs/example-agents/knowledge-base.html +4 -4
  69. package/dist/docs/example-agents/oncall.html +4 -4
  70. package/dist/docs/example-agents/security-reviewer.html +5 -5
  71. package/dist/docs/example-agents/slack-agent.html +4 -4
  72. package/dist/docs/example-agents/weather-agent.html +4 -4
  73. package/dist/docs/guides/agent-to-agent.html +4 -4
  74. package/dist/docs/guides/cloud-runtime.html +4 -4
  75. package/dist/docs/guides/convert-automation.html +36 -0
  76. package/dist/docs/guides/github.html +7 -7
  77. package/dist/docs/guides/human-in-the-loop.html +4 -4
  78. package/dist/docs/guides/mcp-oauth.html +4 -4
  79. package/dist/docs/guides/opentelemetry.html +4 -4
  80. package/dist/docs/guides/slack.html +4 -4
  81. package/dist/docs/guides/webhooks.html +4 -4
  82. package/dist/docs/hashmap.json +1 -1
  83. package/dist/docs/hillclimbing.html +4 -4
  84. package/dist/docs/index.html +6 -6
  85. package/dist/docs/quickstart.html +4 -4
  86. package/dist/docs/reference/agent-config.html +4 -4
  87. package/dist/docs/reference/artifacts.html +4 -4
  88. package/dist/docs/reference/channels.html +4 -4
  89. package/dist/docs/reference/cli.html +12 -6
  90. package/dist/docs/reference/connections.html +4 -4
  91. package/dist/docs/reference/hooks.html +4 -4
  92. package/dist/docs/reference/http-api.html +4 -4
  93. package/dist/docs/reference/instructions.html +4 -4
  94. package/dist/docs/reference/playground.html +4 -4
  95. package/dist/docs/reference/project-layout.html +4 -4
  96. package/dist/docs/reference/prompt.html +4 -4
  97. package/dist/docs/reference/schedules.html +4 -4
  98. package/dist/docs/reference/sessions.html +4 -4
  99. package/dist/docs/reference/skills.html +4 -4
  100. package/dist/docs/reference/subagents.html +4 -4
  101. package/dist/docs/reference/tools.html +4 -4
  102. package/dist/docs/scaffolding-agents.html +5 -5
  103. package/dist/docs/storage.html +4 -4
  104. package/dist/docs/templates/agentic-owners.html +38 -0
  105. package/dist/docs/templates/demo.html +28 -0
  106. package/dist/docs/templates/pr-autofixer.html +32 -0
  107. package/dist/docs/templates/security-reviewer.html +26 -0
  108. package/dist/docs/templates/triage.html +5 -5
  109. package/dist/docs/troubleshooting.html +4 -4
  110. package/dist/index.d.ts +3 -3
  111. package/dist/index.d.ts.map +1 -1
  112. package/dist/index.js +2 -2
  113. package/dist/internal/cli-ax.d.ts +2 -0
  114. package/dist/internal/cli-ax.d.ts.map +1 -1
  115. package/dist/internal/cli-ax.js +20 -0
  116. package/dist/internal/cli-slack.d.ts +2 -0
  117. package/dist/internal/cli-slack.d.ts.map +1 -1
  118. package/dist/internal/cli-slack.js +11 -2
  119. package/dist/internal/cursor-event-relay.d.ts +64 -12
  120. package/dist/internal/cursor-event-relay.d.ts.map +1 -1
  121. package/dist/internal/cursor-event-relay.js +138 -26
  122. package/dist/internal/discovery.d.ts.map +1 -1
  123. package/dist/internal/discovery.js +11 -2
  124. package/dist/internal/distribution.d.ts.map +1 -1
  125. package/dist/internal/distribution.js +1 -0
  126. package/dist/internal/handleAgentServeTrigger.d.ts +33 -6
  127. package/dist/internal/handleAgentServeTrigger.d.ts.map +1 -1
  128. package/dist/internal/handleAgentServeTrigger.js +97 -19
  129. package/dist/internal/init-interview.d.ts +67 -0
  130. package/dist/internal/init-interview.d.ts.map +1 -0
  131. package/dist/internal/init-interview.js +300 -0
  132. package/dist/internal/init-project.d.ts +4 -0
  133. package/dist/internal/init-project.d.ts.map +1 -1
  134. package/dist/internal/init-project.js +40 -16
  135. package/dist/internal/mcp-endpoint.d.ts +7 -0
  136. package/dist/internal/mcp-endpoint.d.ts.map +1 -1
  137. package/dist/internal/mcp-endpoint.js +7 -0
  138. package/dist/internal/mcp-oauth.d.ts +0 -4
  139. package/dist/internal/mcp-oauth.d.ts.map +1 -1
  140. package/dist/internal/mcp-oauth.js +0 -4
  141. package/dist/internal/server.d.ts.map +1 -1
  142. package/dist/internal/server.js +160 -58
  143. package/dist/internal/session-engine.d.ts.map +1 -1
  144. package/dist/internal/session-engine.js +20 -21
  145. package/dist/internal/session-links.d.ts +14 -0
  146. package/dist/internal/session-links.d.ts.map +1 -0
  147. package/dist/internal/session-links.js +69 -0
  148. package/dist/internal/sessions-client.d.ts.map +1 -1
  149. package/dist/internal/sessions-client.js +1 -0
  150. package/dist/playground/assets/index-B4rcOgeP.css +1 -0
  151. package/dist/playground/assets/{index-0oqXUkrR.js → index-DXcY42Jv.js} +42 -42
  152. package/dist/playground/index.html +2 -2
  153. package/dist/types.d.ts +19 -12
  154. package/dist/types.d.ts.map +1 -1
  155. package/docs/README.md +15 -0
  156. package/docs/example-agents/fsd.md +3 -0
  157. package/docs/example-agents/security-reviewer.md +3 -0
  158. package/docs/guides/convert-automation.md +176 -0
  159. package/docs/guides/github.md +6 -2
  160. package/docs/reference/cli.md +34 -10
  161. package/docs/scaffolding-agents.md +3 -0
  162. package/docs/templates/agentic-owners.md +97 -0
  163. package/docs/templates/demo.md +84 -0
  164. package/docs/templates/pr-autofixer.md +136 -0
  165. package/docs/templates/security-reviewer.md +89 -0
  166. package/package.json +9 -1
  167. package/skills/github/SKILL.md +6 -6
  168. package/src/bin/agent-serve.ts +13 -6
  169. package/src/channels/github/github-channel.ts +113 -20
  170. package/src/channels/github/types.ts +33 -0
  171. package/src/channels/origin/cursor-account.ts +64 -0
  172. package/src/channels/origin/events.ts +41 -0
  173. package/src/channels/origin/index.ts +29 -0
  174. package/src/channels/origin/origin-channel.ts +784 -0
  175. package/src/channels/origin/origin-webhook.ts +202 -0
  176. package/src/channels/origin/types.ts +186 -0
  177. package/src/channels/slack/doctor.ts +218 -5
  178. package/src/index.ts +3 -1
  179. package/src/internal/cli-ax.ts +23 -0
  180. package/src/internal/cli-slack.ts +11 -1
  181. package/src/internal/cursor-event-relay.ts +201 -32
  182. package/src/internal/discovery.ts +15 -5
  183. package/src/internal/distribution.ts +1 -0
  184. package/src/internal/handleAgentServeTrigger.ts +129 -19
  185. package/src/internal/init-interview.ts +384 -0
  186. package/src/internal/init-project.ts +70 -15
  187. package/src/internal/mcp-endpoint.ts +7 -0
  188. package/src/internal/mcp-oauth.ts +0 -4
  189. package/src/internal/server.ts +190 -60
  190. package/src/internal/session-engine.ts +5 -21
  191. package/src/internal/session-links.ts +88 -0
  192. package/src/internal/sessions-client.ts +1 -0
  193. package/src/types.ts +19 -12
  194. package/templates/agentic-owners/README.md +73 -0
  195. package/templates/agentic-owners/agent/agent.ts +15 -0
  196. package/templates/agentic-owners/agent/channels/github.ts +85 -0
  197. package/templates/agentic-owners/agent/instructions.md +34 -0
  198. package/templates/agentic-owners/agent/lib/config.ts +13 -0
  199. package/templates/agentic-owners/agent/lib/github.ts +241 -0
  200. package/templates/agentic-owners/agent/lib/owners/README.md +50 -0
  201. package/templates/agentic-owners/agent/lib/owners/constants.ts +22 -0
  202. package/templates/agentic-owners/agent/lib/owners/decision.ts +485 -0
  203. package/templates/agentic-owners/agent/lib/owners/handles.ts +54 -0
  204. package/templates/agentic-owners/agent/lib/owners/match.ts +177 -0
  205. package/templates/agentic-owners/agent/lib/owners/matching.ts +95 -0
  206. package/templates/agentic-owners/agent/lib/owners/parse.ts +589 -0
  207. package/templates/agentic-owners/agent/lib/owners/policyMode.ts +65 -0
  208. package/templates/agentic-owners/agent/lib/owners/prompt.ts +66 -0
  209. package/templates/agentic-owners/agent/lib/owners/prompts/base-review.md +105 -0
  210. package/templates/agentic-owners/agent/lib/owners/types.ts +45 -0
  211. package/templates/agentic-owners/agent/lib/review.ts +777 -0
  212. package/templates/agentic-owners/agent/tools/record_review.ts +69 -0
  213. package/templates/agentic-owners/evals/evals.config.ts +5 -0
  214. package/templates/agentic-owners/evals/owners.eval.ts +116 -0
  215. package/templates/agentic-owners/package.json +20 -0
  216. package/templates/agentic-owners/tsconfig.json +12 -0
  217. package/templates/demo/README.md +33 -0
  218. package/templates/demo/agent/agent.ts +15 -0
  219. package/templates/demo/agent/channels/github.ts +50 -0
  220. package/templates/demo/agent/channels/queue.ts +30 -0
  221. package/templates/demo/agent/instructions.md +8 -0
  222. package/templates/demo/agent/lib/collect.ts +78 -0
  223. package/templates/demo/agent/tools/record_demo.ts +40 -0
  224. package/templates/demo/evals/demo.eval.ts +33 -0
  225. package/templates/demo/evals/evals.config.ts +5 -0
  226. package/templates/demo/package.json +18 -0
  227. package/templates/demo/tsconfig.json +12 -0
  228. package/templates/pr-autofixer/README.md +44 -0
  229. package/templates/pr-autofixer/agent/agent.ts +22 -0
  230. package/templates/pr-autofixer/agent/channels/drive.ts +40 -0
  231. package/templates/pr-autofixer/agent/channels/github.ts +167 -0
  232. package/templates/pr-autofixer/agent/channels/slack.ts +17 -0
  233. package/templates/pr-autofixer/agent/hooks/bind-agent.ts +19 -0
  234. package/templates/pr-autofixer/agent/instructions.md +83 -0
  235. package/templates/pr-autofixer/agent/lib/affinity.ts +33 -0
  236. package/templates/pr-autofixer/agent/lib/drive-schemas.ts +15 -0
  237. package/templates/pr-autofixer/agent/lib/drive.ts +79 -0
  238. package/templates/pr-autofixer/agent/lib/merge-watch.ts +123 -0
  239. package/templates/pr-autofixer/agent/lib/pr-state.ts +54 -0
  240. package/templates/pr-autofixer/agent/lib/repos.ts +19 -0
  241. package/templates/pr-autofixer/agent/lib/start-drive.ts +122 -0
  242. package/templates/pr-autofixer/agent/lib/webhook-buffer.ts +225 -0
  243. package/templates/pr-autofixer/agent/lib/webhook-routing.ts +231 -0
  244. package/templates/pr-autofixer/agent/storage.ts +7 -0
  245. package/templates/pr-autofixer/agent/tools/drive_pr.ts +22 -0
  246. package/templates/pr-autofixer/evals/coordinator.eval.ts +16 -0
  247. package/templates/pr-autofixer/evals/evals.config.ts +5 -0
  248. package/templates/pr-autofixer/init.json +25 -0
  249. package/templates/pr-autofixer/package.json +18 -0
  250. package/templates/pr-autofixer/tsconfig.json +12 -0
  251. package/templates/security-reviewer/README.md +31 -0
  252. package/templates/security-reviewer/agent/agent.ts +15 -0
  253. package/templates/security-reviewer/agent/channels/github.ts +46 -0
  254. package/templates/security-reviewer/agent/instructions.md +22 -0
  255. package/templates/security-reviewer/agent/lib/repos.ts +5 -0
  256. package/templates/security-reviewer/agent/tools/post_findings.ts +72 -0
  257. package/templates/security-reviewer/evals/evals.config.ts +5 -0
  258. package/templates/security-reviewer/evals/review.eval.ts +53 -0
  259. package/templates/security-reviewer/init.json +25 -0
  260. package/templates/security-reviewer/package.json +18 -0
  261. package/templates/security-reviewer/tsconfig.json +12 -0
  262. package/dist/docs/assets/chunks/@localSearchIndexroot.BFpWSo6D.js +0 -1
  263. package/dist/docs/assets/index.md.COeoSkcF.js +0 -5
  264. package/dist/docs/assets/scaffolding-agents.md.mkc3B_ZW.js +0 -1
  265. package/dist/playground/assets/index-tHCjJxv4.css +0 -1
@@ -0,0 +1,485 @@
1
+ // Vendored from the Agentic Owners policy core; see README.md in this
2
+ // directory for the deliberate divergences.
3
+ //
4
+ // The agentic-owners **approval decision** — the deterministic policy that
5
+ // turns the matched `.agentic-owners` rules plus the agent's risk tier into a
6
+ // concrete verdict for the PR: self-approve, or request reviewers (never
7
+ // `CHANGES_REQUESTED`).
8
+ //
9
+ // Rules (closest-match policy is already applied upstream in match.ts):
10
+ // 1. A rule with `always_approve: true` is satisfied at every risk tier.
11
+ // 2. Low risk (`very_low` / `low`) → approve, UNLESS a matched policy is a hard
12
+ // gate (`human_review_required: true`) that the author cannot self-review.
13
+ // 3. Medium+ risk (`medium` / `medium_high` / `high`) → request review from the
14
+ // owners and do NOT approve, UNLESS the author's `self_review` authority
15
+ // covers EVERY review-capable matched rule (each one sets
16
+ // `self_review: true` and lists the author as a `user:` owner) — then
17
+ // approve (including when a covered rule is also a hard gate).
18
+ // 4. Medium+ risk without full self_review coverage → request reviewers
19
+ // (from the agent's recommended set), excluding the author themselves.
20
+ // 5. Host fail-closes downgrade a would-be approve to requesting reviewers:
21
+ // a governance touch always; incomplete evidence (empty / truncated diff,
22
+ // capped file list) unless the author's self_review coverage vouches for a
23
+ // truncation.
24
+ //
25
+ // Settled invariants: a hard `human_review_required` gate blocks auto-approve
26
+ // for non-owners (and blocks low-risk auto-approve entirely), but an owner may
27
+ // clear it when that same rule sets `self_review: true`. An unknown/unparsed
28
+ // risk never auto-approves (fail-safe to requesting reviewers).
29
+
30
+ import { ruleIsHardGate, ruleIsNotifyOnly } from "./policyMode.js";
31
+ import type { AgenticOwnersPathRule } from "./types.js";
32
+
33
+ export type ApprovalAction = "approve" | "request_reviewers";
34
+
35
+ export type ApprovalRiskBucket = "low" | "medium_plus" | "unknown";
36
+
37
+ /** Canonical model risk tiers, after boundary normalization. */
38
+ export type NormalizedApprovalRisk =
39
+ | "very_low"
40
+ | "low"
41
+ | "medium"
42
+ | "medium_high"
43
+ | "high";
44
+
45
+ /** Normalize and validate a model risk tier at its boundary. */
46
+ export function normalizeApprovalRisk(
47
+ riskLevel: string | undefined
48
+ ): NormalizedApprovalRisk | undefined {
49
+ const normalized = riskLevel?.trim().toLowerCase();
50
+ switch (normalized) {
51
+ case "very_low":
52
+ case "low":
53
+ case "medium":
54
+ case "medium_high":
55
+ case "high":
56
+ return normalized;
57
+ default:
58
+ return undefined;
59
+ }
60
+ }
61
+
62
+ /** Bucket the agent's risk level for the approval decision. */
63
+ export function approvalRiskBucket(
64
+ riskLevel: string | undefined
65
+ ): ApprovalRiskBucket {
66
+ switch (normalizeApprovalRisk(riskLevel)) {
67
+ case "very_low":
68
+ case "low":
69
+ return "low";
70
+ case "medium":
71
+ case "medium_high":
72
+ case "high":
73
+ return "medium_plus";
74
+ default:
75
+ return "unknown";
76
+ }
77
+ }
78
+
79
+ /**
80
+ * The resolved PR author, used to evaluate the `self_review` carve-out.
81
+ * Team membership is never resolved (repo-scoped credentials cannot read
82
+ * org teams), so ownership checks match `user:` owners only — team-owned
83
+ * rules stay fail-closed for the author.
84
+ */
85
+ export type ApprovalAuthor = {
86
+ /** PR author GitHub login (no `@`), when resolvable. */
87
+ readonly handle?: string;
88
+ };
89
+
90
+ /**
91
+ * Host-observed quality of the evidence behind the model's risk verdict.
92
+ * Any gap fails closed inside {@link decideApproval}: a verdict formed over
93
+ * partial evidence never auto-approves (see the carve-outs there).
94
+ */
95
+ export type ApprovalEvidence = {
96
+ /** No reviewable file carried a usable patch (no evidence at all). */
97
+ readonly diffEmpty: boolean;
98
+ /**
99
+ * The model saw only part of the diff: the fetch char budget cut it, or
100
+ * some file carried no patch (binary / oversized).
101
+ */
102
+ readonly diffTruncated: boolean;
103
+ /**
104
+ * `listFiles` returned fewer paths than the PR reports (GitHub caps the
105
+ * listing at 3000 files). Omitted paths never entered policy matching,
106
+ * so neither the matched rules nor self_review coverage are sound.
107
+ */
108
+ readonly filesTruncated: boolean;
109
+ };
110
+
111
+ export type ApprovalDecision = {
112
+ readonly action: ApprovalAction;
113
+ /** True only when `action === "approve"`. */
114
+ readonly approve: boolean;
115
+ /** True when any matched rule is a hard `human_review_required` gate. */
116
+ readonly humanReviewRequired: boolean;
117
+ /** True when an owner-author + `self_review` lifted the no-approve default. */
118
+ readonly selfReviewApplied: boolean;
119
+ readonly riskBucket: ApprovalRiskBucket;
120
+ /** Reviewers to request when `action === "request_reviewers"` (else `[]`). */
121
+ readonly requestReviewers: readonly string[];
122
+ /** One-line, human-readable rationale for the check / Slack / comment. */
123
+ readonly reason: string;
124
+ };
125
+
126
+ /** Cap matching the prompt's "max 2 reviewers" rule. */
127
+ export const MAX_REQUESTED_REVIEWERS = 2;
128
+
129
+ function normalizeHandle(handle: string): string {
130
+ return handle.trim().replace(/^@/, "").toLowerCase();
131
+ }
132
+
133
+ /**
134
+ * Whether `author` is a listed `user:` owner of `rule` (handle match,
135
+ * case-insensitive). Returns false when the author identity is unknown, and
136
+ * always false for team-only ownership — membership is never resolved, so
137
+ * team-owned rules fail closed for the author.
138
+ */
139
+ export function authorOwnsRule(
140
+ rule: AgenticOwnersPathRule,
141
+ author: ApprovalAuthor | undefined
142
+ ): boolean {
143
+ if (author?.handle === undefined || author.handle.trim() === "") {
144
+ return false;
145
+ }
146
+ const handle = normalizeHandle(author.handle);
147
+ return rule.users.some((user) => normalizeHandle(user) === handle);
148
+ }
149
+
150
+ /**
151
+ * The first matched rule (closest-match order is preserved) for which the author
152
+ * is an owner AND `self_review` is enabled — i.e. the rule that authorizes the
153
+ * author to approve their own change. Undefined when none applies.
154
+ */
155
+ function selfReviewRuleForAuthor(
156
+ matchedRules: readonly AgenticOwnersPathRule[],
157
+ author: ApprovalAuthor | undefined
158
+ ): AgenticOwnersPathRule | undefined {
159
+ return matchedRules.find(
160
+ (rule) => rule.selfReview && authorOwnsRule(rule, author)
161
+ );
162
+ }
163
+
164
+ /**
165
+ * Whether the author's `self_review` authority covers EVERY matched rule that
166
+ * could require review: each review-capable rule (not `always_approve`, not
167
+ * notify-only) sets `self_review: true` AND lists the author as an owner —
168
+ * with at least one such rule present.
169
+ *
170
+ * This is the signal both self_review carve-outs inside {@link decideApproval}
171
+ * consult. The medium+ lift uses it because `self_review` clears *that rule*:
172
+ * one owned rule must not stamp a PR whose other matched rules still require
173
+ * review. The unverified-evidence fail-close uses it for its owner-vouching
174
+ * carve-out: the author could approve their own change under every matched
175
+ * policy at any tier, so truncation of the model's evidence must not hold the
176
+ * PR for a non-author owner who does not exist.
177
+ *
178
+ * Fail-tight by construction: no matched rules → false; only `always_approve`
179
+ * / notify-only matches → false (no review requirement to satisfy); ANY
180
+ * review-capable rule without `self_review`, or not owned by this author
181
+ * (unknown identity included) → false.
182
+ */
183
+ export function authorSelfReviewCoversMatchedRules(
184
+ matchedRules: readonly AgenticOwnersPathRule[],
185
+ author: ApprovalAuthor | undefined
186
+ ): boolean {
187
+ const reviewCapableRules = matchedRules.filter(
188
+ (rule) => !rule.alwaysApprove && !ruleIsNotifyOnly(rule)
189
+ );
190
+ return (
191
+ reviewCapableRules.length > 0 &&
192
+ reviewCapableRules.every(
193
+ (rule) => rule.selfReview && authorOwnsRule(rule, author)
194
+ )
195
+ );
196
+ }
197
+
198
+ /**
199
+ * The reviewer set to request when not approving: the agent's recommended
200
+ * reviewers, with the author removed (you cannot review your own PR), deduped
201
+ * and capped at {@link MAX_REQUESTED_REVIEWERS}.
202
+ */
203
+ function reviewersToRequest({
204
+ recommendedReviewers,
205
+ author,
206
+ }: {
207
+ recommendedReviewers: readonly string[];
208
+ author: ApprovalAuthor | undefined;
209
+ }): string[] {
210
+ const authorHandle =
211
+ author?.handle !== undefined && author.handle.trim() !== ""
212
+ ? normalizeHandle(author.handle)
213
+ : undefined;
214
+ const seen = new Set<string>();
215
+ const out: string[] = [];
216
+ for (const raw of recommendedReviewers) {
217
+ const reviewer = raw.trim();
218
+ if (reviewer === "") {
219
+ continue;
220
+ }
221
+ // Drop the author when they appear as a bare-user handle; team handles
222
+ // (org/team) are kept even if the author is on that team — the request goes
223
+ // to the team, not the individual.
224
+ if (
225
+ authorHandle !== undefined &&
226
+ !reviewer.includes("/") &&
227
+ normalizeHandle(reviewer) === authorHandle
228
+ ) {
229
+ continue;
230
+ }
231
+ const key = reviewer.toLowerCase();
232
+ if (seen.has(key)) {
233
+ continue;
234
+ }
235
+ seen.add(key);
236
+ out.push(reviewer);
237
+ if (out.length >= MAX_REQUESTED_REVIEWERS) {
238
+ break;
239
+ }
240
+ }
241
+ return out;
242
+ }
243
+
244
+ /**
245
+ * The approve reason when no matched rule requires review: every match is
246
+ * `always_approve`, notify-only, or a mix — worded to match which it was.
247
+ */
248
+ function reasonForNoBlockingOwners(
249
+ matchedRules: readonly AgenticOwnersPathRule[]
250
+ ): string {
251
+ const anyNotifyOnly = matchedRules.some((rule) => ruleIsNotifyOnly(rule));
252
+ if (!anyNotifyOnly) {
253
+ return "all matched policies set always_approve";
254
+ }
255
+ const anyAlwaysApprove = matchedRules.some((rule) => rule.alwaysApprove);
256
+ return anyAlwaysApprove
257
+ ? "matched policies are notify-only or always_approve (no blocking owners)"
258
+ : "all matched policies are notify-only (no blocking owners)";
259
+ }
260
+
261
+ /**
262
+ * Compute the approval verdict from the matched policy + agent risk + author,
263
+ * then apply the host fail-closes over it: a governance touch or unverified
264
+ * evidence downgrades a would-be approve to requesting reviewers, so the
265
+ * caller only ever writes the returned decision.
266
+ *
267
+ * Pure and dependency-free so it is exhaustively unit-testable; the IO (author
268
+ * resolution, evidence observation, actuating the decision) lives in the
269
+ * callers.
270
+ */
271
+ export function decideApproval(input: {
272
+ matchedRules: readonly AgenticOwnersPathRule[];
273
+ riskLevel: string | undefined;
274
+ author?: ApprovalAuthor;
275
+ recommendedReviewers?: readonly string[];
276
+ /** Host-observed evidence quality; omitted means complete evidence. */
277
+ evidence?: ApprovalEvidence;
278
+ /**
279
+ * True when the PR touches ownership-governance paths (the policy files
280
+ * this agent enforces, CODEOWNERS). The model's tier never carries
281
+ * approval authority over its own review routing — and self_review does
282
+ * not clear it (an owner must not self-approve a rewrite of their own
283
+ * gates).
284
+ */
285
+ governanceTouch?: boolean;
286
+ }): ApprovalDecision {
287
+ const { matchedRules, riskLevel, author } = input;
288
+ const reviewRequiredRules = matchedRules.filter(
289
+ (rule) => !rule.alwaysApprove && !ruleIsNotifyOnly(rule)
290
+ );
291
+ const riskBucket = approvalRiskBucket(riskLevel);
292
+ const humanReviewRequired = reviewRequiredRules.some((rule) =>
293
+ ruleIsHardGate(rule)
294
+ );
295
+ const requestReviewers = reviewersToRequest({
296
+ recommendedReviewers: input.recommendedReviewers ?? [],
297
+ author,
298
+ });
299
+
300
+ const requestVerdict = (reason: string): ApprovalDecision => ({
301
+ action: "request_reviewers",
302
+ approve: false,
303
+ humanReviewRequired,
304
+ selfReviewApplied: false,
305
+ riskBucket,
306
+ requestReviewers,
307
+ reason,
308
+ });
309
+
310
+ const approveVerdict = (
311
+ reason: string,
312
+ selfReviewApplied: boolean
313
+ ): ApprovalDecision => ({
314
+ action: "approve",
315
+ approve: true,
316
+ humanReviewRequired,
317
+ selfReviewApplied,
318
+ riskBucket,
319
+ requestReviewers: [],
320
+ reason,
321
+ });
322
+
323
+ const decision = baseApprovalVerdict({
324
+ matchedRules,
325
+ reviewRequiredRules,
326
+ riskBucket,
327
+ humanReviewRequired,
328
+ author,
329
+ requestVerdict,
330
+ approveVerdict,
331
+ });
332
+ if (!decision.approve) {
333
+ return decision;
334
+ }
335
+
336
+ // Host fail-closes over a would-be approve. Each downgrades to requesting
337
+ // reviewers (never CHANGES_REQUESTED) while keeping the base attribution.
338
+ const hold = (reason: string): ApprovalDecision => ({
339
+ ...decision,
340
+ action: "request_reviewers",
341
+ approve: false,
342
+ requestReviewers,
343
+ reason,
344
+ });
345
+
346
+ if (input.governanceTouch === true) {
347
+ return hold(
348
+ "the change edits ownership governance — auto-approve disabled (fail closed)"
349
+ );
350
+ }
351
+
352
+ const evidence = input.evidence;
353
+ if (evidence === undefined) {
354
+ return decision;
355
+ }
356
+ if (evidence.filesTruncated) {
357
+ // Matching itself ran over a partial file list, so self_review coverage
358
+ // is unknowable — nothing clears this hold.
359
+ return hold(
360
+ "the changed-file list is incomplete (GitHub caps listed files) — auto-approve disabled (fail closed)"
361
+ );
362
+ }
363
+ // An empty or truncated diff never auto-approves — including the
364
+ // no-blocking-owners branch — except when the author's own self_review
365
+ // authority covers every matched rule (truncation only; an empty patch
366
+ // carries no evidence at all).
367
+ const selfReviewClearsEvidence =
368
+ authorSelfReviewCoversMatchedRules(matchedRules, author) &&
369
+ !evidence.diffEmpty;
370
+ if ((evidence.diffEmpty || evidence.diffTruncated) && !selfReviewClearsEvidence) {
371
+ return hold(
372
+ evidence.diffEmpty
373
+ ? "no usable patch hunks — auto-approve disabled (fail closed)"
374
+ : "diff truncated at the fetch budget — auto-approve disabled (fail closed)"
375
+ );
376
+ }
377
+ return decision;
378
+ }
379
+
380
+ /** The policy-only verdict, before the host fail-closes are applied. */
381
+ function baseApprovalVerdict(input: {
382
+ matchedRules: readonly AgenticOwnersPathRule[];
383
+ reviewRequiredRules: readonly AgenticOwnersPathRule[];
384
+ riskBucket: ApprovalRiskBucket;
385
+ humanReviewRequired: boolean;
386
+ author: ApprovalAuthor | undefined;
387
+ requestVerdict: (reason: string) => ApprovalDecision;
388
+ approveVerdict: (
389
+ reason: string,
390
+ selfReviewApplied: boolean
391
+ ) => ApprovalDecision;
392
+ }): ApprovalDecision {
393
+ const {
394
+ matchedRules,
395
+ reviewRequiredRules,
396
+ riskBucket,
397
+ humanReviewRequired,
398
+ author,
399
+ requestVerdict,
400
+ approveVerdict,
401
+ } = input;
402
+
403
+ if (matchedRules.length > 0 && reviewRequiredRules.length === 0) {
404
+ return approveVerdict(reasonForNoBlockingOwners(matchedRules), false);
405
+ }
406
+
407
+ // A hard human-review gate blocks unless every hard-gate rule is cleared by
408
+ // owner self_review on that same rule. Low-risk auto-approve never clears a
409
+ // hard gate without self_review.
410
+ const blockingHardGate = reviewRequiredRules.find(
411
+ (rule) =>
412
+ ruleIsHardGate(rule) && !(rule.selfReview && authorOwnsRule(rule, author))
413
+ );
414
+ if (blockingHardGate !== undefined) {
415
+ return requestVerdict("a matched policy requires human review");
416
+ }
417
+
418
+ // Low risk → approve (hard gates above already filtered; remaining
419
+ // hard+self_review rules are cleared by the owner carve-out).
420
+ if (riskBucket === "low") {
421
+ const selfReviewRule = selfReviewRuleForAuthor(reviewRequiredRules, author);
422
+ if (selfReviewRule !== undefined && humanReviewRequired) {
423
+ return approveVerdict(
424
+ `low risk, owner self_review (${selfReviewRule.sourcePath})`,
425
+ true
426
+ );
427
+ }
428
+ return approveVerdict("low risk, advisory policy", false);
429
+ }
430
+
431
+ // Medium+ → request reviewers, unless the author's self_review authority
432
+ // covers EVERY review-capable matched rule. self_review clears *that
433
+ // rule*: one owned rule must not stamp a PR whose other matched rules
434
+ // still require review.
435
+ if (riskBucket === "medium_plus") {
436
+ if (authorSelfReviewCoversMatchedRules(matchedRules, author)) {
437
+ return approveVerdict(
438
+ "medium+ risk, owner self_review covers every matched policy",
439
+ true
440
+ );
441
+ }
442
+ return requestVerdict("medium+ risk, no auto-approve");
443
+ }
444
+
445
+ // (Settled) Unknown / unparsed risk never auto-approves.
446
+ return requestVerdict("risk could not be determined (fail-safe hold)");
447
+ }
448
+
449
+ /** A recommended reviewer split into the user / team buckets GitHub requires. */
450
+ export type SplitReviewers = {
451
+ /** Bare user logins (no `@`). */
452
+ users: string[];
453
+ /** Team slugs (the trailing segment of an `org/team` handle). */
454
+ teams: string[];
455
+ };
456
+
457
+ /**
458
+ * Split the decision's recommended reviewers into GitHub's `reviewers` (users)
459
+ * and `team_reviewers` (team slugs) buckets. A handle with a `/` is a team
460
+ * (`org/team`); GitHub's request API wants only the trailing slug, not the org.
461
+ * Dedupes case-insensitively while preserving order.
462
+ */
463
+ export function splitReviewers(reviewers: readonly string[]): SplitReviewers {
464
+ const users: string[] = [];
465
+ const teams: string[] = [];
466
+ const seenUsers = new Set<string>();
467
+ const seenTeams = new Set<string>();
468
+ for (const raw of reviewers) {
469
+ const handle = raw.trim().replace(/^@/, "");
470
+ if (handle === "") {
471
+ continue;
472
+ }
473
+ if (handle.includes("/")) {
474
+ const slug = handle.slice(handle.lastIndexOf("/") + 1);
475
+ if (slug !== "" && !seenTeams.has(slug.toLowerCase())) {
476
+ seenTeams.add(slug.toLowerCase());
477
+ teams.push(slug);
478
+ }
479
+ } else if (!seenUsers.has(handle.toLowerCase())) {
480
+ seenUsers.add(handle.toLowerCase());
481
+ users.push(handle);
482
+ }
483
+ }
484
+ return { users, teams };
485
+ }
@@ -0,0 +1,54 @@
1
+ // Vendored from the Agentic Owners policy core; see README.md in this
2
+ // directory. The source's `reviewersConventionHandle` (a Canon-specific
3
+ // `<org>/<slug>-reviewers` convention) and its Graphite mention-sentinel
4
+ // repair (a Graphite-PR-page round-trip artifact this template never sees)
5
+ // are dropped: this template resolves bare team slugs against the watched
6
+ // repository's owner.
7
+
8
+ /**
9
+ * Validate and normalize a reviewer handle before it enters a candidate list, a
10
+ * model prompt, or a GitHub review-request API. Returns `@login` for a user,
11
+ * `org/team` for a team, or `undefined` for a shape GitHub would reject (so
12
+ * the caller drops it).
13
+ *
14
+ * Repairs the malformed shapes seen in the wild rather than emitting them: a
15
+ * duplicated leading org (`anysphere/anysphere/x` → `anysphere/x`, gen21
16
+ * shadow review) is collapsed, and a stray leading `@` / trailing dot / mixed
17
+ * case is normalized. A team handle with more than one meaningful `/` segment
18
+ * that is not a simple org duplication is rejected.
19
+ */
20
+ export function sanitizeReviewerHandle(raw: string): string | undefined {
21
+ const trimmed = raw.trim();
22
+ if (trimmed === "") {
23
+ return undefined;
24
+ }
25
+ if (trimmed.includes("/")) {
26
+ const segments = trimmed
27
+ .replace(/^@/, "")
28
+ .replace(/\.+$/, "")
29
+ .toLowerCase()
30
+ .split("/")
31
+ .map((segment) => segment.trim())
32
+ .filter((segment) => segment !== "");
33
+ // A GitHub team handle is always exactly `org/team`. Collapse a duplicated
34
+ // leading org (the shape a double-prefix bug produces) before validating.
35
+ while (segments.length > 2 && segments[0] === segments[1]) {
36
+ segments.shift();
37
+ }
38
+ if (segments.length !== 2) {
39
+ return undefined;
40
+ }
41
+ const [org, team] = segments;
42
+ if (
43
+ org === undefined ||
44
+ team === undefined ||
45
+ !/^[a-z0-9][a-z0-9-]*$/.test(org) ||
46
+ !/^[a-z0-9][a-z0-9._-]*$/.test(team)
47
+ ) {
48
+ return undefined;
49
+ }
50
+ return `${org}/${team}`;
51
+ }
52
+ const login = trimmed.replace(/^@/, "").toLowerCase();
53
+ return /^[a-z0-9][a-z0-9-]*$/.test(login) ? `@${login}` : undefined;
54
+ }