@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.
- package/dist/bin/agent-serve.js +30 -23
- package/dist/channels/github/github-channel.d.ts +13 -1
- package/dist/channels/github/github-channel.d.ts.map +1 -1
- package/dist/channels/github/github-channel.js +81 -16
- package/dist/channels/github/types.d.ts +29 -1
- package/dist/channels/github/types.d.ts.map +1 -1
- package/dist/channels/origin/cursor-account.d.ts +22 -0
- package/dist/channels/origin/cursor-account.d.ts.map +1 -0
- package/dist/channels/origin/cursor-account.js +35 -0
- package/dist/channels/origin/events.d.ts +10 -0
- package/dist/channels/origin/events.d.ts.map +1 -0
- package/dist/channels/origin/events.js +27 -0
- package/dist/channels/origin/index.d.ts +29 -0
- package/dist/channels/origin/index.d.ts.map +1 -0
- package/dist/channels/origin/index.js +28 -0
- package/dist/channels/origin/origin-channel.d.ts +68 -0
- package/dist/channels/origin/origin-channel.d.ts.map +1 -0
- package/dist/channels/origin/origin-channel.js +567 -0
- package/dist/channels/origin/origin-webhook.d.ts +38 -0
- package/dist/channels/origin/origin-webhook.d.ts.map +1 -0
- package/dist/channels/origin/origin-webhook.js +151 -0
- package/dist/channels/origin/types.d.ts +152 -0
- package/dist/channels/origin/types.d.ts.map +1 -0
- package/dist/channels/origin/types.js +1 -0
- package/dist/channels/slack/doctor.d.ts +17 -1
- package/dist/channels/slack/doctor.d.ts.map +1 -1
- package/dist/channels/slack/doctor.js +186 -10
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +4 -4
- package/dist/docs/assets/{app.CVQxhV85.js → app.B-biogkN.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.5ozYNf74.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.QAPzJjql.js → VPLocalSearchBox.DZqKtFZS.js} +1 -1
- package/dist/docs/assets/chunks/{theme.CSxTCOFu.js → theme.BwC5zP2r.js} +2 -2
- package/dist/docs/assets/{example-agents_fsd.md.DPz9ezO4.js → example-agents_fsd.md.ZeGEpAw_.js} +2 -2
- package/dist/docs/assets/{example-agents_fsd.md.DPz9ezO4.lean.js → example-agents_fsd.md.ZeGEpAw_.lean.js} +1 -1
- package/dist/docs/assets/{example-agents_security-reviewer.md.Dkf1gyo6.js → example-agents_security-reviewer.md.DBL1TwtP.js} +2 -2
- package/dist/docs/assets/{example-agents_security-reviewer.md.Dkf1gyo6.lean.js → example-agents_security-reviewer.md.DBL1TwtP.lean.js} +1 -1
- package/dist/docs/assets/guides_convert-automation.md.BhMzTkE5.js +12 -0
- package/dist/docs/assets/guides_convert-automation.md.BhMzTkE5.lean.js +1 -0
- package/dist/docs/assets/{guides_github.md.B8QKtIpw.js → guides_github.md.CLLQJKRB.js} +4 -4
- package/dist/docs/assets/{guides_github.md.B8QKtIpw.lean.js → guides_github.md.CLLQJKRB.lean.js} +1 -1
- package/dist/docs/assets/index.md.DhRHS_-L.js +5 -0
- package/dist/docs/assets/{index.md.COeoSkcF.lean.js → index.md.DhRHS_-L.lean.js} +1 -1
- package/dist/docs/assets/{reference_cli.md.D_CCfG8R.js → reference_cli.md.Cl14Jfo3.js} +8 -2
- package/dist/docs/assets/{reference_cli.md.D_CCfG8R.lean.js → reference_cli.md.Cl14Jfo3.lean.js} +1 -1
- package/dist/docs/assets/scaffolding-agents.md.CrWSA0Ko.js +1 -0
- package/dist/docs/assets/{scaffolding-agents.md.mkc3B_ZW.lean.js → scaffolding-agents.md.CrWSA0Ko.lean.js} +1 -1
- package/dist/docs/assets/templates_agentic-owners.md.BkTLORaU.js +14 -0
- package/dist/docs/assets/templates_agentic-owners.md.BkTLORaU.lean.js +1 -0
- package/dist/docs/assets/templates_demo.md.Bgd6MBaZ.js +4 -0
- package/dist/docs/assets/templates_demo.md.Bgd6MBaZ.lean.js +1 -0
- package/dist/docs/assets/templates_pr-autofixer.md.DcmoeUNZ.js +8 -0
- package/dist/docs/assets/templates_pr-autofixer.md.DcmoeUNZ.lean.js +1 -0
- package/dist/docs/assets/templates_security-reviewer.md.C0yIUaYs.js +2 -0
- package/dist/docs/assets/templates_security-reviewer.md.C0yIUaYs.lean.js +1 -0
- package/dist/docs/building-with-agents.html +4 -4
- package/dist/docs/concepts.html +5 -5
- package/dist/docs/deployment.html +4 -4
- package/dist/docs/evals.html +4 -4
- package/dist/docs/example-agents/approval-buddy.html +4 -4
- package/dist/docs/example-agents/benny.html +4 -4
- package/dist/docs/example-agents/bugbot.html +4 -4
- package/dist/docs/example-agents/codebase-wiki.html +4 -4
- package/dist/docs/example-agents/codeowners-review.html +4 -4
- package/dist/docs/example-agents/concierge.html +4 -4
- package/dist/docs/example-agents/fsd.html +5 -5
- package/dist/docs/example-agents/index.html +4 -4
- package/dist/docs/example-agents/knowledge-base.html +4 -4
- package/dist/docs/example-agents/oncall.html +4 -4
- package/dist/docs/example-agents/security-reviewer.html +5 -5
- package/dist/docs/example-agents/slack-agent.html +4 -4
- package/dist/docs/example-agents/weather-agent.html +4 -4
- package/dist/docs/guides/agent-to-agent.html +4 -4
- package/dist/docs/guides/cloud-runtime.html +4 -4
- package/dist/docs/guides/convert-automation.html +36 -0
- package/dist/docs/guides/github.html +7 -7
- package/dist/docs/guides/human-in-the-loop.html +4 -4
- package/dist/docs/guides/mcp-oauth.html +4 -4
- package/dist/docs/guides/opentelemetry.html +4 -4
- package/dist/docs/guides/slack.html +4 -4
- package/dist/docs/guides/webhooks.html +4 -4
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +4 -4
- package/dist/docs/index.html +6 -6
- package/dist/docs/quickstart.html +4 -4
- package/dist/docs/reference/agent-config.html +4 -4
- package/dist/docs/reference/artifacts.html +4 -4
- package/dist/docs/reference/channels.html +4 -4
- package/dist/docs/reference/cli.html +12 -6
- package/dist/docs/reference/connections.html +4 -4
- package/dist/docs/reference/hooks.html +4 -4
- package/dist/docs/reference/http-api.html +4 -4
- package/dist/docs/reference/instructions.html +4 -4
- package/dist/docs/reference/playground.html +4 -4
- package/dist/docs/reference/project-layout.html +4 -4
- package/dist/docs/reference/prompt.html +4 -4
- package/dist/docs/reference/schedules.html +4 -4
- package/dist/docs/reference/sessions.html +4 -4
- package/dist/docs/reference/skills.html +4 -4
- package/dist/docs/reference/subagents.html +4 -4
- package/dist/docs/reference/tools.html +4 -4
- package/dist/docs/scaffolding-agents.html +5 -5
- package/dist/docs/storage.html +4 -4
- package/dist/docs/templates/agentic-owners.html +38 -0
- package/dist/docs/templates/demo.html +28 -0
- package/dist/docs/templates/pr-autofixer.html +32 -0
- package/dist/docs/templates/security-reviewer.html +26 -0
- package/dist/docs/templates/triage.html +5 -5
- package/dist/docs/troubleshooting.html +4 -4
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/internal/cli-ax.d.ts +2 -0
- package/dist/internal/cli-ax.d.ts.map +1 -1
- package/dist/internal/cli-ax.js +20 -0
- package/dist/internal/cli-slack.d.ts +2 -0
- package/dist/internal/cli-slack.d.ts.map +1 -1
- package/dist/internal/cli-slack.js +11 -2
- package/dist/internal/cursor-event-relay.d.ts +64 -12
- package/dist/internal/cursor-event-relay.d.ts.map +1 -1
- package/dist/internal/cursor-event-relay.js +138 -26
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +11 -2
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +1 -0
- package/dist/internal/handleAgentServeTrigger.d.ts +33 -6
- package/dist/internal/handleAgentServeTrigger.d.ts.map +1 -1
- package/dist/internal/handleAgentServeTrigger.js +97 -19
- package/dist/internal/init-interview.d.ts +67 -0
- package/dist/internal/init-interview.d.ts.map +1 -0
- package/dist/internal/init-interview.js +300 -0
- package/dist/internal/init-project.d.ts +4 -0
- package/dist/internal/init-project.d.ts.map +1 -1
- package/dist/internal/init-project.js +40 -16
- package/dist/internal/mcp-endpoint.d.ts +7 -0
- package/dist/internal/mcp-endpoint.d.ts.map +1 -1
- package/dist/internal/mcp-endpoint.js +7 -0
- package/dist/internal/mcp-oauth.d.ts +0 -4
- package/dist/internal/mcp-oauth.d.ts.map +1 -1
- package/dist/internal/mcp-oauth.js +0 -4
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +160 -58
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +20 -21
- package/dist/internal/session-links.d.ts +14 -0
- package/dist/internal/session-links.d.ts.map +1 -0
- package/dist/internal/session-links.js +69 -0
- package/dist/internal/sessions-client.d.ts.map +1 -1
- package/dist/internal/sessions-client.js +1 -0
- package/dist/playground/assets/index-B4rcOgeP.css +1 -0
- package/dist/playground/assets/{index-0oqXUkrR.js → index-DXcY42Jv.js} +42 -42
- package/dist/playground/index.html +2 -2
- package/dist/types.d.ts +19 -12
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +15 -0
- package/docs/example-agents/fsd.md +3 -0
- package/docs/example-agents/security-reviewer.md +3 -0
- package/docs/guides/convert-automation.md +176 -0
- package/docs/guides/github.md +6 -2
- package/docs/reference/cli.md +34 -10
- package/docs/scaffolding-agents.md +3 -0
- package/docs/templates/agentic-owners.md +97 -0
- package/docs/templates/demo.md +84 -0
- package/docs/templates/pr-autofixer.md +136 -0
- package/docs/templates/security-reviewer.md +89 -0
- package/package.json +9 -1
- package/skills/github/SKILL.md +6 -6
- package/src/bin/agent-serve.ts +13 -6
- package/src/channels/github/github-channel.ts +113 -20
- package/src/channels/github/types.ts +33 -0
- package/src/channels/origin/cursor-account.ts +64 -0
- package/src/channels/origin/events.ts +41 -0
- package/src/channels/origin/index.ts +29 -0
- package/src/channels/origin/origin-channel.ts +784 -0
- package/src/channels/origin/origin-webhook.ts +202 -0
- package/src/channels/origin/types.ts +186 -0
- package/src/channels/slack/doctor.ts +218 -5
- package/src/index.ts +3 -1
- package/src/internal/cli-ax.ts +23 -0
- package/src/internal/cli-slack.ts +11 -1
- package/src/internal/cursor-event-relay.ts +201 -32
- package/src/internal/discovery.ts +15 -5
- package/src/internal/distribution.ts +1 -0
- package/src/internal/handleAgentServeTrigger.ts +129 -19
- package/src/internal/init-interview.ts +384 -0
- package/src/internal/init-project.ts +70 -15
- package/src/internal/mcp-endpoint.ts +7 -0
- package/src/internal/mcp-oauth.ts +0 -4
- package/src/internal/server.ts +190 -60
- package/src/internal/session-engine.ts +5 -21
- package/src/internal/session-links.ts +88 -0
- package/src/internal/sessions-client.ts +1 -0
- package/src/types.ts +19 -12
- package/templates/agentic-owners/README.md +73 -0
- package/templates/agentic-owners/agent/agent.ts +15 -0
- package/templates/agentic-owners/agent/channels/github.ts +85 -0
- package/templates/agentic-owners/agent/instructions.md +34 -0
- package/templates/agentic-owners/agent/lib/config.ts +13 -0
- package/templates/agentic-owners/agent/lib/github.ts +241 -0
- package/templates/agentic-owners/agent/lib/owners/README.md +50 -0
- package/templates/agentic-owners/agent/lib/owners/constants.ts +22 -0
- package/templates/agentic-owners/agent/lib/owners/decision.ts +485 -0
- package/templates/agentic-owners/agent/lib/owners/handles.ts +54 -0
- package/templates/agentic-owners/agent/lib/owners/match.ts +177 -0
- package/templates/agentic-owners/agent/lib/owners/matching.ts +95 -0
- package/templates/agentic-owners/agent/lib/owners/parse.ts +589 -0
- package/templates/agentic-owners/agent/lib/owners/policyMode.ts +65 -0
- package/templates/agentic-owners/agent/lib/owners/prompt.ts +66 -0
- package/templates/agentic-owners/agent/lib/owners/prompts/base-review.md +105 -0
- package/templates/agentic-owners/agent/lib/owners/types.ts +45 -0
- package/templates/agentic-owners/agent/lib/review.ts +777 -0
- package/templates/agentic-owners/agent/tools/record_review.ts +69 -0
- package/templates/agentic-owners/evals/evals.config.ts +5 -0
- package/templates/agentic-owners/evals/owners.eval.ts +116 -0
- package/templates/agentic-owners/package.json +20 -0
- package/templates/agentic-owners/tsconfig.json +12 -0
- package/templates/demo/README.md +33 -0
- package/templates/demo/agent/agent.ts +15 -0
- package/templates/demo/agent/channels/github.ts +50 -0
- package/templates/demo/agent/channels/queue.ts +30 -0
- package/templates/demo/agent/instructions.md +8 -0
- package/templates/demo/agent/lib/collect.ts +78 -0
- package/templates/demo/agent/tools/record_demo.ts +40 -0
- package/templates/demo/evals/demo.eval.ts +33 -0
- package/templates/demo/evals/evals.config.ts +5 -0
- package/templates/demo/package.json +18 -0
- package/templates/demo/tsconfig.json +12 -0
- package/templates/pr-autofixer/README.md +44 -0
- package/templates/pr-autofixer/agent/agent.ts +22 -0
- package/templates/pr-autofixer/agent/channels/drive.ts +40 -0
- package/templates/pr-autofixer/agent/channels/github.ts +167 -0
- package/templates/pr-autofixer/agent/channels/slack.ts +17 -0
- package/templates/pr-autofixer/agent/hooks/bind-agent.ts +19 -0
- package/templates/pr-autofixer/agent/instructions.md +83 -0
- package/templates/pr-autofixer/agent/lib/affinity.ts +33 -0
- package/templates/pr-autofixer/agent/lib/drive-schemas.ts +15 -0
- package/templates/pr-autofixer/agent/lib/drive.ts +79 -0
- package/templates/pr-autofixer/agent/lib/merge-watch.ts +123 -0
- package/templates/pr-autofixer/agent/lib/pr-state.ts +54 -0
- package/templates/pr-autofixer/agent/lib/repos.ts +19 -0
- package/templates/pr-autofixer/agent/lib/start-drive.ts +122 -0
- package/templates/pr-autofixer/agent/lib/webhook-buffer.ts +225 -0
- package/templates/pr-autofixer/agent/lib/webhook-routing.ts +231 -0
- package/templates/pr-autofixer/agent/storage.ts +7 -0
- package/templates/pr-autofixer/agent/tools/drive_pr.ts +22 -0
- package/templates/pr-autofixer/evals/coordinator.eval.ts +16 -0
- package/templates/pr-autofixer/evals/evals.config.ts +5 -0
- package/templates/pr-autofixer/init.json +25 -0
- package/templates/pr-autofixer/package.json +18 -0
- package/templates/pr-autofixer/tsconfig.json +12 -0
- package/templates/security-reviewer/README.md +31 -0
- package/templates/security-reviewer/agent/agent.ts +15 -0
- package/templates/security-reviewer/agent/channels/github.ts +46 -0
- package/templates/security-reviewer/agent/instructions.md +22 -0
- package/templates/security-reviewer/agent/lib/repos.ts +5 -0
- package/templates/security-reviewer/agent/tools/post_findings.ts +72 -0
- package/templates/security-reviewer/evals/evals.config.ts +5 -0
- package/templates/security-reviewer/evals/review.eval.ts +53 -0
- package/templates/security-reviewer/init.json +25 -0
- package/templates/security-reviewer/package.json +18 -0
- package/templates/security-reviewer/tsconfig.json +12 -0
- package/dist/docs/assets/chunks/@localSearchIndexroot.BFpWSo6D.js +0 -1
- package/dist/docs/assets/index.md.COeoSkcF.js +0 -5
- package/dist/docs/assets/scaffolding-agents.md.mkc3B_ZW.js +0 -1
- 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
|
+
}
|