@reneturcios/greenlight 0.15.1 → 0.16.1
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/README.md +11 -6
- package/dist/behavior.d.ts +63 -0
- package/dist/behavior.js +178 -0
- package/dist/behavior.js.map +1 -0
- package/dist/cli.js +73 -3
- package/dist/cli.js.map +1 -1
- package/dist/friction.d.ts +8 -1
- package/dist/friction.js +23 -1
- package/dist/friction.js.map +1 -1
- package/dist/github.js +7 -2
- package/dist/github.js.map +1 -1
- package/dist/help.js +21 -3
- package/dist/help.js.map +1 -1
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/wave.d.ts +81 -0
- package/dist/wave.js +258 -0
- package/dist/wave.js.map +1 -0
- package/package.json +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -25,11 +25,11 @@ Three layers, from decide to solve to coordinate:
|
|
|
25
25
|
|
|
26
26
|
| Layer | Question it answers | Flagship commands |
|
|
27
27
|
|---|---|---|
|
|
28
|
-
| **Decide** | Is this issue mine to take? Is this repo worth my time? | `check`, `scan`, `friction`, `xray`, `preflight` |
|
|
28
|
+
| **Decide** | Is this issue mine to take? Is this repo worth my time? | `check`, `scan`, `friction`, `xray`, `wave`, `preflight` |
|
|
29
29
|
| **Solve** | Will my PR pass? What broke? What do I do next? | `ci-prep`, `ci`, `review`, `brief`, `verify`, `autopilot`, `babysit`, `evidence` |
|
|
30
30
|
| **Coordinate** | Who is working on what, and who resolves conflicts? | `claim` (+ Claim Protocol), `guard`, `radar`, `digest`, `passport` |
|
|
31
31
|
|
|
32
|
-
* **Decide.** `check` reads an issue, its comments, linked and mentioned PRs (timeline plus search plus comment references), and the repo's rules, then answers in plain words: GO, RISKY, TAKEN, GATED, or STALE. `scan` ranks a repo's open issues so you pick a winnable one first. `friction` scores a repo 0 to 100 on how hard it really is to get a PR merged, with every gate quoting the file its evidence came from. `xray` (Timeline X-Ray) deep-verifies candidates against their issue timelines before you write a line, catching hidden in-flight PRs including reporter-owned fix PRs; `scan --deep` demotes anything taken before it shows you the list.
|
|
32
|
+
* **Decide.** `check` reads an issue, its comments, linked and mentioned PRs (timeline plus search plus comment references), and the repo's rules, then answers in plain words: GO, RISKY, TAKEN, GATED, or STALE. `scan` ranks a repo's open issues so you pick a winnable one first. `friction` scores a repo 0 to 100 on how hard it really is to get a PR merged, with every gate quoting the file its evidence came from. `xray` (Timeline X-Ray) deep-verifies candidates against their issue timelines before you write a line, catching hidden in-flight PRs including reporter-owned fix PRs; `scan --deep` demotes anything taken before it shows you the list. `wave` turns the Agent Policy Index into a build queue in one command: winnable open repos, x-rayed starter issues, behavioral gates excluded.
|
|
33
33
|
* **Solve.** `ci-prep` runs a repo's own pull-request checks locally before you push (tests, lint, typecheck, plus conventions like changesets, commit format, DCO), so avoidable CI failures never appear in front of a maintainer. `ci` triages red PRs into pre-existing vs likely-yours vs external-blocker against base-branch history. `review` classifies every review thread as likely-real, likely-noise, or needs-decision and drafts your replies. `verify` reports the honest red-green test delta of your change. `autopilot` verifies bot findings against the code at head and fixes the mechanical items (DCO surgery, conflict re-merges, formatting) itself. `brief` assembles a verified one-screen brief for any PR: change shape by area and risk, CI triaged against base-branch history, findings classified real vs false-positive with evidence, test evidence present or explicitly marked absent, merge blockers, and a one-line look-at-this-first. `babysit` watches your open PRs and reports one prioritized action list.
|
|
34
34
|
* **Coordinate.** `claim` posts a protocol claim (expiring lease, public queue, heartbeats; full spec in [`protocol/SPEC.md`](protocol/SPEC.md)), with a hosted app client option via `--via-api`. `guard` analyzes an incoming PR for a maintainer: competing fix, prior claims, missing artifacts, first-time contributor. `digest` reports a repo's PR races, claimed-by-other conflicts, stale claims, and assignment-gate conflicts. `radar` scans the whole open-PR queue and clusters competing or duplicate PRs with evidence per cluster and a suggested keep, labeled a heuristic, never a verdict. `passport` renders a contributor's merged work plus claim record.
|
|
35
35
|
|
|
@@ -38,7 +38,7 @@ Every verdict and every report cites its evidence: the quoted policy line, the c
|
|
|
38
38
|
## Who it is for
|
|
39
39
|
|
|
40
40
|
* **Contributors (human).** Run `check` before you spend an evening on an issue. Run `scan` when you are picking what to work on. Run `ci-prep` before you push.
|
|
41
|
-
* **AI agents and coding tools.** One command (`init --agent claude-code`, also cursor, copilot, windsurf, cline) installs the MCP config plus the one behavior rule: check Greenlight before opening a PR. Agents waste the most tokens fixing already-taken issues;
|
|
41
|
+
* **AI agents and coding tools.** One command (`init --agent claude-code`, also cursor, copilot, windsurf, cline) installs the MCP config plus the one behavior rule: check Greenlight before opening a PR. Agents waste the most tokens fixing already-taken issues; 28 MCP tools make the whole engine available mid-run.
|
|
42
42
|
* **Maintainers.** `guard` on each incoming PR, `radar` across the whole open queue, `digest` for the weekly triage, and the roadmap below for the control room: stale-claim sweeps, policy enforcement, AI-PR filtering.
|
|
43
43
|
|
|
44
44
|
## Proof, stated plainly
|
|
@@ -103,7 +103,7 @@ One command sets it up:
|
|
|
103
103
|
npx @reneturcios/greenlight init --agent claude-code
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
(also `cursor`, `copilot`, `windsurf`, `cline`; repeat `--agent` for more than one). Init copies the MCP config into your project and installs the workflow instruction snippet that teaches your agent the one behavior rule: **check Greenlight before it opens a PR.** Prefer doing it by hand? The [`integrations/`](integrations/README.md) drop-in pack has ready-to-copy MCP configs plus the same snippets. Then verify with a "list your MCP tools" prompt; the greenlight server carries
|
|
106
|
+
(also `cursor`, `copilot`, `windsurf`, `cline`; repeat `--agent` for more than one). Init copies the MCP config into your project and installs the workflow instruction snippet that teaches your agent the one behavior rule: **check Greenlight before it opens a PR.** Prefer doing it by hand? The [`integrations/`](integrations/README.md) drop-in pack has ready-to-copy MCP configs plus the same snippets. Then verify with a "list your MCP tools" prompt; the greenlight server carries 28 tools (see the list below).
|
|
107
107
|
|
|
108
108
|
## CLI
|
|
109
109
|
|
|
@@ -283,12 +283,13 @@ The same engine is exposed over stdio for coding agents (Claude, Cursor, Cline).
|
|
|
283
283
|
}
|
|
284
284
|
```
|
|
285
285
|
|
|
286
|
-
Tools (
|
|
286
|
+
Tools (28):
|
|
287
287
|
|
|
288
288
|
* `check_issue` - verdict for one issue URL
|
|
289
289
|
* `scan_repo` - rank a repo's open issues, GO first (label-tagged starter issues, falling back to recent open unassigned issues; `deep: true` timeline-x-rays the candidates first)
|
|
290
290
|
* `xray_issues` - Timeline X-Ray: deep-verify candidate issues against their timelines (hidden in-flight PRs, reporter-owned fix PRs, comment claims); GO / RISKY / TAKEN per issue with PR evidence
|
|
291
291
|
* `repo_friction` - friction score for a repo
|
|
292
|
+
* `wave_queue` - build queue in one call: filters the Agent Policy Index for winnable open repos, x-rays starter issues (CLEAR / RISKY / TAKEN), excludes behavioral gates, skips repos where you already have an open PR
|
|
292
293
|
* `claim_issue` - verdict-gated claim comment (dry run unless `execute: true`)
|
|
293
294
|
* `prep_issue` - fork and branch check plus a local PREP.md checklist with the repo's learned conventions (dry run unless `execute: true`)
|
|
294
295
|
* `open_pr` - DCO-aware PR opening from a fork branch (dry run unless `execute: true`)
|
|
@@ -332,7 +333,11 @@ The verdict engine is deterministic (no LLM) and checks, in order:
|
|
|
332
333
|
11. An open PR referenced in a non-author comment whose title/body mentions the issue number: downgrades an otherwise-GO verdict to **RISKY** with the `comment-mentioned-pr` flag ("may already cover part of this issue"). Neither can produce **TAKEN**; only closing PRs do that. Fix-announcement comments ("/attempt #N" commands or "Submitted fix for ... in <PR url>" narratives, often linking PRs in sibling repos) get the same downgrade under their own reason quoting the PR URL, with the `attempt-linked-pr` flag, even when the PR never names the issue. `/attempt` comments also register as strong claims.
|
|
333
334
|
12. **Partially-addressed**: merged PRs that reference the issue number (comment-mentioned or linked), or maintainer comments signaling part of the work already shipped ("Half addressed by #905, leaving this open for half B", "Shipped in v3.1.0" next to the issue reference). A plain **GO** hides the scoping work this needs, so the flag `partially-addressed` is added, an otherwise-GO verdict is capped at **RISKY** with a reason like "part of this issue already shipped (PR #905, merged); scope your work to what remains". Never **TAKEN**.
|
|
334
335
|
|
|
335
|
-
Friction score starts at 100 and subtracts gate weights (assignment -25, vouch -35, collaborators-only -50, paused unsolicited PRs -40, external PRs auto-closed -50, discussion-first -10, claim-first -5, CLA/DCO -5 each), adjusts for external PR merge rate and review/response times when computable, and adds up to +10 for open good-first-issues. Unknown signals stay null and are skipped, never guessed. See `src/friction.ts` for the exact formula.
|
|
336
|
+
Friction score starts at 100 and subtracts gate weights (assignment -25, vouch -35, collaborators-only -50, paused unsolicited PRs -40, external PRs auto-closed -50, discussion-first -10, claim-first -5, CLA/DCO -5 each, detected behavioral gate -20), adjusts for external PR merge rate and review/response times when computable, and adds up to +10 for open good-first-issues. Unknown signals stay null and are skipped, never guessed. See `src/friction.ts` for the exact formula.
|
|
337
|
+
|
|
338
|
+
A behavioral gate is not a policy file: it is read from what maintainers do. `friction` walks the closed-PR list until it has the 15 most recent closed-unmerged PRs, then reads maintainer-side comments for AI-rejection evidence (AI attribution plus rejection language, explicit phrases like "AI slop" or an invoked "AI policy", with welcoming overrides like "AI-assisted PRs are welcome" suppressing the signal). Two independent PRs mark the repo as behaviorally gated; a single weak signal is carried as a warning, while a single explicit rejection phrase detects on its own. `friction --json` reports it as `behavioral_gate` (`detected`, `strength`, `threshold`, `evidence` with PR numbers, quotes, and URLs, `notes`), so index pipelines like the rebaseline can reclassify repos whose written policy is silent but whose practice is hostile. API cost is bounded at 18 calls: up to 3 list calls to collect 15 closed-unmerged PRs plus one comment page per PR; skip it with `--no-behavioral`.
|
|
339
|
+
|
|
340
|
+
`wave` turns the Agent Policy Index into a build queue in one command: pure-open repos with outsider merge share, star band, and starter issues are x-rayed with the existing xray engine (CLEAR / RISKY / TAKEN with competing PRs named), repos with detected behavioral gates are excluded, repos where you already have an open PR are skipped, and the run stops with marked partial results when the core API budget falls under the floor (default 800) or search throttling hits. Human and `--json` output; every stage reports the API calls it used.
|
|
336
341
|
|
|
337
342
|
### Agent-policy gates and triage holds
|
|
338
343
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export interface BehavioralClosedPR {
|
|
2
|
+
number: number;
|
|
3
|
+
url: string;
|
|
4
|
+
author: string;
|
|
5
|
+
closedAt: string;
|
|
6
|
+
}
|
|
7
|
+
export interface BehavioralMaintainerComment {
|
|
8
|
+
pr: number;
|
|
9
|
+
author: string;
|
|
10
|
+
association: string;
|
|
11
|
+
body: string;
|
|
12
|
+
url: string;
|
|
13
|
+
}
|
|
14
|
+
export interface BehavioralSignal {
|
|
15
|
+
pr: number;
|
|
16
|
+
author: string;
|
|
17
|
+
/** Truncated quote (240 chars) that fired the matcher. */
|
|
18
|
+
quote: string;
|
|
19
|
+
url: string;
|
|
20
|
+
/** True for explicit phrases like "AI slop"; weaker signals can still count toward strength. */
|
|
21
|
+
strong: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface BehavioralGate {
|
|
24
|
+
/** True when strength >= threshold: the maintainer rejects AI-built PRs in practice. */
|
|
25
|
+
detected: boolean;
|
|
26
|
+
/** Distinct closed-unmerged PRs with at least one matching maintainer comment. */
|
|
27
|
+
strength: number;
|
|
28
|
+
/** Detection needs strength >= threshold. Always 2; a lone PR is a warning, not a policy. */
|
|
29
|
+
threshold: number;
|
|
30
|
+
evidence: BehavioralSignal[];
|
|
31
|
+
/** Transparency: PRs scanned, maintainer comments seen, why nothing fired. */
|
|
32
|
+
notes: string[];
|
|
33
|
+
note: string;
|
|
34
|
+
}
|
|
35
|
+
/** Maximum closed-unmerged PRs examined per repo. Keeps the pass bounded. */
|
|
36
|
+
export declare const BEHAVIORAL_MAX_PRS = 15;
|
|
37
|
+
/**
|
|
38
|
+
* API cost contract: 1-3 calls to walk the closed-PR list (stops early
|
|
39
|
+
* once 15 unmerged PRs are found) plus at most one comment page per
|
|
40
|
+
* closed-unmerged PR.
|
|
41
|
+
*/
|
|
42
|
+
export declare const BEHAVIORAL_MAX_API_CALLS: number;
|
|
43
|
+
/** Pure: does one comment body read as maintainer AI-rejection evidence? */
|
|
44
|
+
export declare function matchAiRejection(body: string): {
|
|
45
|
+
matched: boolean;
|
|
46
|
+
strong: boolean;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Pure core: fold maintainer comments on closed-unmerged PRs into a gate
|
|
50
|
+
* verdict. The live fetcher below builds these two inputs; tests drive
|
|
51
|
+
* this function with fixtures, so the threshold logic never depends on
|
|
52
|
+
* the network. Comments whose association is not maintainer-side are
|
|
53
|
+
* ignored here too, so the pure contract is complete.
|
|
54
|
+
*/
|
|
55
|
+
export declare function detectBehavioralGateFromData(prs: BehavioralClosedPR[], comments: BehavioralMaintainerComment[]): BehavioralGate;
|
|
56
|
+
/**
|
|
57
|
+
* Live pass: walk the closed-PR list (newest first) until 15
|
|
58
|
+
* closed-unmerged PRs are collected, fetch comments on those PRs, and run
|
|
59
|
+
* the pure core. Returns the gate verdict; throws on API failure (the
|
|
60
|
+
* friction pipeline treats a failure as "signal unavailable", never as
|
|
61
|
+
* detected).
|
|
62
|
+
*/
|
|
63
|
+
export declare function detectBehavioralGate(owner: string, repo: string): Promise<BehavioralGate>;
|
package/dist/behavior.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behavioral gate detection (v0.16.0).
|
|
3
|
+
*
|
|
4
|
+
* Field data (2026-10-19, PR wave): spotDL/spotify-downloader is classed
|
|
5
|
+
* "open" in the Agent Policy Index (no policy file bans agents), yet its
|
|
6
|
+
* maintainer rejects AI-built contributions in practice: issue #2371 drew
|
|
7
|
+
* three PRs, all closed unmerged, with PR #2494 closed on a comment
|
|
8
|
+
* calling the submission "AI slop". Policy-file gate detection cannot see
|
|
9
|
+
* this shape, so the index misleads contributors into burning builds. This
|
|
10
|
+
* module closes that gap with a bounded, deterministic pass over recent
|
|
11
|
+
* closed-unmerged PRs.
|
|
12
|
+
*
|
|
13
|
+
* Design (deliberately conservative; read before changing):
|
|
14
|
+
* - Scope: the 15 most recent CLOSED-UNMERGED PRs. The closed-PR list is
|
|
15
|
+
* walked (newest first, up to 3 pages of 30) until 15 unmerged PRs are
|
|
16
|
+
* collected, because merged PRs carry no closure evidence and an
|
|
17
|
+
* active repo's unmerged closures can sit pages deep. Comment fetches
|
|
18
|
+
* (first page, per_page=100) run only for those unmerged PRs, so the
|
|
19
|
+
* total cost is 1-3 list calls + up to 15 comment calls (18 max).
|
|
20
|
+
* - Maintainer-side only: comment author_association must be OWNER,
|
|
21
|
+
* MEMBER, or COLLABORATOR, and the comment author must not be the PR
|
|
22
|
+
* author (a maintainer closing their own PR is not evidence against
|
|
23
|
+
* outsiders).
|
|
24
|
+
* - A comment counts as AI-rejection evidence when it contains an
|
|
25
|
+
* AI-attribution term AND a rejection/closure term (or matches one of
|
|
26
|
+
* the explicit strong phrases below). Welcoming overrides
|
|
27
|
+
* ("AI-assisted PRs are welcome") suppress the signal so open repos
|
|
28
|
+
* do not false-positive.
|
|
29
|
+
* - Detection threshold: strength >= 2, where strength is the number of
|
|
30
|
+
* DISTINCT closed-unmerged PRs carrying at least one matching
|
|
31
|
+
* maintainer comment. A single weak signal is carried in evidence
|
|
32
|
+
* with detected=false: a warning, not a behavioral policy. A single
|
|
33
|
+
* STRONG signal (an explicit phrase like "AI slop" or an invoked "AI
|
|
34
|
+
* policy" on closure) detects on its own: the phrase is unambiguous
|
|
35
|
+
* maintainer intent, and corroboration would only delay the warning.
|
|
36
|
+
*
|
|
37
|
+
* This is behavior inference from maintainer actions, not policy text:
|
|
38
|
+
* verdicts should treat a detected behavioral gate as a hard reason to
|
|
39
|
+
* skip the repo, the same way agents_banned is treated.
|
|
40
|
+
*/
|
|
41
|
+
import { apiGet, apiCallsUsed } from "./github.js";
|
|
42
|
+
/** Maximum closed-unmerged PRs examined per repo. Keeps the pass bounded. */
|
|
43
|
+
export const BEHAVIORAL_MAX_PRS = 15;
|
|
44
|
+
/**
|
|
45
|
+
* API cost contract: 1-3 calls to walk the closed-PR list (stops early
|
|
46
|
+
* once 15 unmerged PRs are found) plus at most one comment page per
|
|
47
|
+
* closed-unmerged PR.
|
|
48
|
+
*/
|
|
49
|
+
export const BEHAVIORAL_MAX_API_CALLS = 3 + BEHAVIORAL_MAX_PRS;
|
|
50
|
+
const AI_TERMS = /\b(ai|a\.i\.|llm|llms|chatgpt|copilot|Muse|gemini|gpt-?4|gpt-?3|generated|slop)\b/i;
|
|
51
|
+
const REJECTION_TERMS = /\b(clos(?:e|ed|ing|ure)?|not\s+accept(?:ing|s)?|won['’]t|will\s+not|do\s+not|don['’]t|reject(?:ed|s|ion)?|declin(?:e|ed)|low[\s-]?effort|slop|refus(?:e|ed|ing)|spam|not\s+welcome|polic(?:y|ies))\b/i;
|
|
52
|
+
/** Explicit phrases: unmistakable even without the two-term rule. */
|
|
53
|
+
const STRONG_PHRASES = [
|
|
54
|
+
/\bai\s+slop\b/i,
|
|
55
|
+
/\bno\s+ai[-\s]?(prs?|contributions?|code|submissions?)\b/i,
|
|
56
|
+
/\bwe\s+(do\s+not|don['’]t|won['’]t)\s+accept\s+ai/i,
|
|
57
|
+
/\bno\s+(ai|llm)[-\s]?generated\b/i,
|
|
58
|
+
/\bai[-\s]?(llm\s+)?polic(y|ies)\b/i,
|
|
59
|
+
];
|
|
60
|
+
/** Welcoming language suppresses the signal: "AI-assisted PRs are welcome" must never fire. */
|
|
61
|
+
const WELCOMING = /\bwelcom(e|ing)\b[^.!?]{0,80}\b(ai|ai[-\s]assisted|contributions?|contributors?|prs?)\b/i;
|
|
62
|
+
/** Pure: does one comment body read as maintainer AI-rejection evidence? */
|
|
63
|
+
export function matchAiRejection(body) {
|
|
64
|
+
const text = body.replace(/\s+/g, " ");
|
|
65
|
+
if (WELCOMING.test(text))
|
|
66
|
+
return { matched: false, strong: false };
|
|
67
|
+
for (const p of STRONG_PHRASES) {
|
|
68
|
+
if (p.test(text))
|
|
69
|
+
return { matched: true, strong: true };
|
|
70
|
+
}
|
|
71
|
+
if (AI_TERMS.test(text) && REJECTION_TERMS.test(text)) {
|
|
72
|
+
return { matched: true, strong: /\bslop\b/i.test(text) };
|
|
73
|
+
}
|
|
74
|
+
return { matched: false, strong: false };
|
|
75
|
+
}
|
|
76
|
+
function truncateQuote(body, limit = 240) {
|
|
77
|
+
const flat = body.replace(/\s+/g, " ").trim();
|
|
78
|
+
return flat.length > limit ? flat.slice(0, limit - 1) + "\u2026" : flat;
|
|
79
|
+
}
|
|
80
|
+
const MAINTAINER_ASSOCIATIONS = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
|
|
81
|
+
/**
|
|
82
|
+
* Pure core: fold maintainer comments on closed-unmerged PRs into a gate
|
|
83
|
+
* verdict. The live fetcher below builds these two inputs; tests drive
|
|
84
|
+
* this function with fixtures, so the threshold logic never depends on
|
|
85
|
+
* the network. Comments whose association is not maintainer-side are
|
|
86
|
+
* ignored here too, so the pure contract is complete.
|
|
87
|
+
*/
|
|
88
|
+
export function detectBehavioralGateFromData(prs, comments) {
|
|
89
|
+
const prByNumber = new Map(prs.map((p) => [p.number, p]));
|
|
90
|
+
const evidence = [];
|
|
91
|
+
const prsWithSignal = new Set();
|
|
92
|
+
const notes = [];
|
|
93
|
+
notes.push(`scanned ${prs.length} closed-unmerged PRs, ${comments.length} maintainer comments on them`);
|
|
94
|
+
for (const c of comments) {
|
|
95
|
+
if (!MAINTAINER_ASSOCIATIONS.has(String(c.association ?? "")))
|
|
96
|
+
continue;
|
|
97
|
+
const pr = prByNumber.get(c.pr);
|
|
98
|
+
if (!pr)
|
|
99
|
+
continue;
|
|
100
|
+
// A maintainer closing their own PR is not evidence against outsiders.
|
|
101
|
+
if (c.author.toLowerCase() === pr.author.toLowerCase())
|
|
102
|
+
continue;
|
|
103
|
+
const { matched, strong } = matchAiRejection(c.body);
|
|
104
|
+
if (!matched)
|
|
105
|
+
continue;
|
|
106
|
+
evidence.push({
|
|
107
|
+
pr: c.pr,
|
|
108
|
+
author: c.author,
|
|
109
|
+
quote: truncateQuote(c.body),
|
|
110
|
+
url: c.url,
|
|
111
|
+
strong,
|
|
112
|
+
});
|
|
113
|
+
prsWithSignal.add(c.pr);
|
|
114
|
+
}
|
|
115
|
+
const strength = prsWithSignal.size;
|
|
116
|
+
const threshold = 2;
|
|
117
|
+
const hasStrong = evidence.some((e) => e.strong);
|
|
118
|
+
// Two independent PRs make it a behavioral policy; one explicit
|
|
119
|
+
// strong phrase (e.g. "AI slop", an invoked "AI policy" on closure)
|
|
120
|
+
// is unambiguous maintainer intent and detects on its own.
|
|
121
|
+
const detected = strength >= threshold || (strength >= 1 && hasStrong);
|
|
122
|
+
const note = detected
|
|
123
|
+
? `maintainer rejects AI-built PRs in practice: ${strength} closed-unmerged PR${strength === 1 ? "" : "s"} carr${strength === 1 ? "ies" : "y"} AI-rejection comments${hasStrong ? ", including an explicit rejection phrase" : ""}`
|
|
124
|
+
: strength === 0
|
|
125
|
+
? "no maintainer AI-rejection evidence found on recent closed-unmerged PRs"
|
|
126
|
+
: `only ${strength} closed-unmerged PR with AI-rejection evidence and no explicit phrase (threshold ${threshold}): warning, not a behavioral gate`;
|
|
127
|
+
if (!detected && evidence.length > 0) {
|
|
128
|
+
notes.push("signals exist but stay below the detection threshold");
|
|
129
|
+
}
|
|
130
|
+
return { detected, strength, threshold, evidence, notes, note };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Live pass: walk the closed-PR list (newest first) until 15
|
|
134
|
+
* closed-unmerged PRs are collected, fetch comments on those PRs, and run
|
|
135
|
+
* the pure core. Returns the gate verdict; throws on API failure (the
|
|
136
|
+
* friction pipeline treats a failure as "signal unavailable", never as
|
|
137
|
+
* detected).
|
|
138
|
+
*/
|
|
139
|
+
export async function detectBehavioralGate(owner, repo) {
|
|
140
|
+
const callsBefore = apiCallsUsed();
|
|
141
|
+
const closedUnmerged = [];
|
|
142
|
+
for (let page = 1; page <= 3 && closedUnmerged.length < BEHAVIORAL_MAX_PRS; page++) {
|
|
143
|
+
const items = (await apiGet(`/repos/${owner}/${repo}/pulls?state=closed&sort=updated&direction=desc&per_page=30&page=${page}`));
|
|
144
|
+
if (!items || items.length === 0)
|
|
145
|
+
break;
|
|
146
|
+
for (const p of items) {
|
|
147
|
+
if (closedUnmerged.length >= BEHAVIORAL_MAX_PRS)
|
|
148
|
+
break;
|
|
149
|
+
if (p.state === "closed" && !p.merged_at) {
|
|
150
|
+
closedUnmerged.push({
|
|
151
|
+
number: p.number,
|
|
152
|
+
url: p.html_url,
|
|
153
|
+
author: String(p.user?.login ?? ""),
|
|
154
|
+
closedAt: String(p.closed_at ?? ""),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (items.length < 30)
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
const comments = [];
|
|
162
|
+
for (const pr of closedUnmerged) {
|
|
163
|
+
const list = (await apiGet(`/repos/${owner}/${repo}/issues/${pr.number}/comments?per_page=100`));
|
|
164
|
+
for (const c of list ?? []) {
|
|
165
|
+
comments.push({
|
|
166
|
+
pr: pr.number,
|
|
167
|
+
author: String(c.user?.login ?? ""),
|
|
168
|
+
association: String(c.author_association ?? ""),
|
|
169
|
+
body: String(c.body ?? ""),
|
|
170
|
+
url: c.html_url,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const gate = detectBehavioralGateFromData(closedUnmerged, comments);
|
|
175
|
+
gate.notes.push(`API calls used: ${apiCallsUsed() - callsBefore}`);
|
|
176
|
+
return gate;
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=behavior.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"behavior.js","sourceRoot":"","sources":["../src/behavior.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAwCnD,6EAA6E;AAC7E,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AACrC;;;;GAIG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,GAAG,kBAAkB,CAAC;AAE/D,MAAM,QAAQ,GACZ,oFAAoF,CAAC;AACvF,MAAM,eAAe,GACnB,uMAAuM,CAAC;AAC1M,qEAAqE;AACrE,MAAM,cAAc,GAAG;IACrB,gBAAgB;IAChB,2DAA2D;IAC3D,oDAAoD;IACpD,mCAAmC;IACnC,oCAAoC;CACrC,CAAC;AACF,+FAA+F;AAC/F,MAAM,SAAS,GACb,0FAA0F,CAAC;AAE7F,4EAA4E;AAC5E,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACvC,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IACnE,KAAK,MAAM,CAAC,IAAI,cAAc,EAAE,CAAC;QAC/B,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAC3D,CAAC;IACD,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACtD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IAC3D,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;AAC3C,CAAC;AAED,SAAS,aAAa,CAAC,IAAY,EAAE,KAAK,GAAG,GAAG;IAC9C,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAC9C,OAAO,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC;AAC1E,CAAC;AAED,MAAM,uBAAuB,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,cAAc,CAAC,CAAC,CAAC;AAE7E;;;;;;GAMG;AACH,MAAM,UAAU,4BAA4B,CAC1C,GAAyB,EACzB,QAAuC;IAEvC,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1D,MAAM,QAAQ,GAAuB,EAAE,CAAC;IACxC,MAAM,aAAa,GAAG,IAAI,GAAG,EAAU,CAAC;IACxC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,CAAC,IAAI,CACR,WAAW,GAAG,CAAC,MAAM,yBAAyB,QAAQ,CAAC,MAAM,8BAA8B,CAC5F,CAAC;IAEF,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACzB,IAAI,CAAC,uBAAuB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC;YAAE,SAAS;QACxE,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAChC,IAAI,CAAC,EAAE;YAAE,SAAS;QAClB,uEAAuE;QACvE,IAAI,CAAC,CAAC,MAAM,CAAC,WAAW,EAAE,KAAK,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE;YAAE,SAAS;QACjE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,gBAAgB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACrD,IAAI,CAAC,OAAO;YAAE,SAAS;QACvB,QAAQ,CAAC,IAAI,CAAC;YACZ,EAAE,EAAE,CAAC,CAAC,EAAE;YACR,MAAM,EAAE,CAAC,CAAC,MAAM;YAChB,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC;YAC5B,GAAG,EAAE,CAAC,CAAC,GAAG;YACV,MAAM;SACP,CAAC,CAAC;QACH,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAC1B,CAAC;IAED,MAAM,QAAQ,GAAG,aAAa,CAAC,IAAI,CAAC;IACpC,MAAM,SAAS,GAAG,CAAC,CAAC;IACpB,MAAM,SAAS,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IACjD,gEAAgE;IAChE,oEAAoE;IACpE,2DAA2D;IAC3D,MAAM,QAAQ,GAAG,QAAQ,IAAI,SAAS,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,SAAS,CAAC,CAAC;IACvE,MAAM,IAAI,GAAG,QAAQ;QACnB,CAAC,CAAC,gDAAgD,QAAQ,sBAAsB,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,QAAQ,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,yBAAyB,SAAS,CAAC,CAAC,CAAC,0CAA0C,CAAC,CAAC,CAAC,EAAE,EAAE;QACnO,CAAC,CAAC,QAAQ,KAAK,CAAC;YACd,CAAC,CAAC,yEAAyE;YAC3E,CAAC,CAAC,QAAQ,QAAQ,oFAAoF,SAAS,mCAAmC,CAAC;IACvJ,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrC,KAAK,CAAC,IAAI,CAAC,sDAAsD,CAAC,CAAC;IACrE,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AAClE,CAAC;AAoBD;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,KAAa,EACb,IAAY;IAEZ,MAAM,WAAW,GAAG,YAAY,EAAE,CAAC;IACnC,MAAM,cAAc,GAAyB,EAAE,CAAC;IAChD,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,IAAI,CAAC,IAAI,cAAc,CAAC,MAAM,GAAG,kBAAkB,EAAE,IAAI,EAAE,EAAE,CAAC;QACnF,MAAM,KAAK,GAAG,CAAC,MAAM,MAAM,CACzB,UAAU,KAAK,IAAI,IAAI,oEAAoE,IAAI,EAAE,CAClG,CAAiB,CAAC;QACnB,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,MAAM;QACxC,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YACtB,IAAI,cAAc,CAAC,MAAM,IAAI,kBAAkB;gBAAE,MAAM;YACvD,IAAI,CAAC,CAAC,KAAK,KAAK,QAAQ,IAAI,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC;gBACzC,cAAc,CAAC,IAAI,CAAC;oBAClB,MAAM,EAAE,CAAC,CAAC,MAAM;oBAChB,GAAG,EAAE,CAAC,CAAC,QAAQ;oBACf,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;oBACnC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,SAAS,IAAI,EAAE,CAAC;iBACpC,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,GAAG,EAAE;YAAE,MAAM;IAC/B,CAAC;IAED,MAAM,QAAQ,GAAkC,EAAE,CAAC;IACnD,KAAK,MAAM,EAAE,IAAI,cAAc,EAAE,CAAC;QAChC,MAAM,IAAI,GAAG,CAAC,MAAM,MAAM,CACxB,UAAU,KAAK,IAAI,IAAI,WAAW,EAAE,CAAC,MAAM,wBAAwB,CACpE,CAAsB,CAAC;QACxB,KAAK,MAAM,CAAC,IAAI,IAAI,IAAI,EAAE,EAAE,CAAC;YAC3B,QAAQ,CAAC,IAAI,CAAC;gBACZ,EAAE,EAAE,EAAE,CAAC,MAAM;gBACb,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;gBACnC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,kBAAkB,IAAI,EAAE,CAAC;gBAC/C,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;gBAC1B,GAAG,EAAE,CAAC,CAAC,QAAQ;aAChB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,MAAM,IAAI,GAAG,4BAA4B,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC;IACpE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,mBAAmB,YAAY,EAAE,GAAG,WAAW,EAAE,CAAC,CAAC;IACnE,OAAO,IAAI,CAAC;AACd,CAAC"}
|
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* greenlight check <issue-url> [--json] [--gates-file path]
|
|
6
6
|
* greenlight scan <owner/repo> [--json] [--no-fallback] [--gates-file path]
|
|
7
|
+
* greenlight wave [--index url|path] [--repos N] [--min-share N] [--stars lo,hi] [--issues-per-repo N] [--min-budget N] [--json]
|
|
7
8
|
* greenlight mcp
|
|
8
9
|
* greenlight friction <owner/repo> [--json]
|
|
9
10
|
* greenlight ci <pr-url> [--json]
|
|
@@ -55,6 +56,7 @@ import { briefPR } from "./brief.js";
|
|
|
55
56
|
import { babysit, defaultStatePath, defaultAccountAuthor } from "./babysit.js";
|
|
56
57
|
import { runAutopilot, defaultAutopilotStatePath } from "./autopilot.js";
|
|
57
58
|
import { runSync, formatSyncHuman, syncJson } from "./sync.js";
|
|
59
|
+
import { runWave, liveWaveDeps, loadPolicyIndex, formatWaveHuman, POLICY_INDEX_URL, DEFAULT_WAVE_FILTERS, } from "./wave.js";
|
|
58
60
|
import { runPortfolio, formatPortfolioHuman, portfolioJson } from "./portfolio.js";
|
|
59
61
|
import { runShipcheck, formatShipcheckHuman } from "./shipcheck.js";
|
|
60
62
|
import { runPreflight, formatPreflightHuman } from "./preflight.js";
|
|
@@ -382,12 +384,16 @@ async function cmdXray(repoRef, args) {
|
|
|
382
384
|
console.log(` flags: ${r.flags.join(", ")}`);
|
|
383
385
|
}
|
|
384
386
|
}
|
|
385
|
-
async function cmdFriction(repoRef, asJson) {
|
|
387
|
+
async function cmdFriction(repoRef, asJson, args = []) {
|
|
386
388
|
const { owner, repo } = parseRepoRef(repoRef);
|
|
387
389
|
// v0.9.3: one shared pipeline (fetchFrictionSummary) for the CLI, the
|
|
388
390
|
// MCP repo_friction tool, and preflight, so gates come from the full
|
|
389
391
|
// policy source set everywhere instead of a single CONTRIBUTING read.
|
|
390
|
-
|
|
392
|
+
// v0.16.0: the behavioral gate pass (maintainer actions, not policy
|
|
393
|
+
// text) is part of the report; --no-behavioral skips its API cost.
|
|
394
|
+
const report = await fetchFrictionSummary(owner, repo, {
|
|
395
|
+
behavioral: !args.includes("--no-behavioral"),
|
|
396
|
+
});
|
|
391
397
|
if (asJson) {
|
|
392
398
|
console.log(JSON.stringify(report, null, 2));
|
|
393
399
|
return;
|
|
@@ -397,12 +403,74 @@ async function cmdFriction(repoRef, asJson) {
|
|
|
397
403
|
console.log(` - ${reason}`);
|
|
398
404
|
if (report.reasons.length === 0)
|
|
399
405
|
console.log(" - no gates or penalties detected");
|
|
406
|
+
if (report.behavioral_gate?.detected) {
|
|
407
|
+
const n = report.behavioral_gate.strength;
|
|
408
|
+
console.log(` BEHAVIORAL GATE: maintainer rejects AI-built PRs in practice (${n} signal${n === 1 ? "" : "s"})`);
|
|
409
|
+
for (const ev of report.behavioral_gate.evidence) {
|
|
410
|
+
console.log(` - PR #${ev.pr} by @${ev.author}: "${ev.quote}" (${ev.url})`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
else if (report.behavioral_gate && report.behavioral_gate.evidence.length > 0) {
|
|
414
|
+
console.log(` behavioral note: ${report.behavioral_gate.note}`);
|
|
415
|
+
}
|
|
400
416
|
const starterCount = report.signals.openGoodFirstIssueCount ?? null;
|
|
401
417
|
if (starterCount !== null)
|
|
402
418
|
console.log(` open starter issues: ${starterCount}`);
|
|
403
419
|
if (report.mergeVelocity)
|
|
404
420
|
console.log(` merge velocity: ${describeMergeVelocity(report.mergeVelocity)}`);
|
|
405
421
|
}
|
|
422
|
+
/**
|
|
423
|
+
* v0.16.0: turn the Agent Policy Index into a build queue in one command.
|
|
424
|
+
* Filters pure-open repos, x-rays starter issues with the existing xray
|
|
425
|
+
* engine, excludes behavioral gates, and skips repos where you already
|
|
426
|
+
* have an open PR. Stops with partial results under the budget floor.
|
|
427
|
+
*/
|
|
428
|
+
async function cmdWave(args) {
|
|
429
|
+
const numFlag = (name) => {
|
|
430
|
+
const v = flagValue(args, name);
|
|
431
|
+
if (v === undefined)
|
|
432
|
+
return undefined;
|
|
433
|
+
const n = Number(v);
|
|
434
|
+
return Number.isFinite(n) ? n : undefined;
|
|
435
|
+
};
|
|
436
|
+
const starsRaw = flagValue(args, "--stars");
|
|
437
|
+
let minStars = DEFAULT_WAVE_FILTERS.minStars;
|
|
438
|
+
let maxStars = DEFAULT_WAVE_FILTERS.maxStars;
|
|
439
|
+
if (starsRaw) {
|
|
440
|
+
const [lo, hi] = starsRaw.split(",").map(Number);
|
|
441
|
+
if (Number.isFinite(lo) && Number.isFinite(hi) && lo <= hi) {
|
|
442
|
+
minStars = lo;
|
|
443
|
+
maxStars = hi;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const filters = {
|
|
447
|
+
minShare: numFlag("--min-share") ?? DEFAULT_WAVE_FILTERS.minShare,
|
|
448
|
+
minStars,
|
|
449
|
+
maxStars,
|
|
450
|
+
maxRepos: numFlag("--repos") ?? DEFAULT_WAVE_FILTERS.maxRepos,
|
|
451
|
+
issuesPerRepo: numFlag("--issues-per-repo") ?? DEFAULT_WAVE_FILTERS.issuesPerRepo,
|
|
452
|
+
minBudget: numFlag("--min-budget") ?? DEFAULT_WAVE_FILTERS.minBudget,
|
|
453
|
+
};
|
|
454
|
+
const indexSource = flagValue(args, "--index") ?? POLICY_INDEX_URL;
|
|
455
|
+
const asJson = args.includes("--json");
|
|
456
|
+
let index;
|
|
457
|
+
try {
|
|
458
|
+
index = await loadPolicyIndex(indexSource);
|
|
459
|
+
}
|
|
460
|
+
catch (err) {
|
|
461
|
+
console.error(`Error: could not load the policy index from ${indexSource}: ${err instanceof Error ? err.message : String(err)}`);
|
|
462
|
+
process.exitCode = 1;
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const result = await runWave(index, filters, liveWaveDeps());
|
|
466
|
+
if (asJson) {
|
|
467
|
+
console.log(JSON.stringify(result, null, 2));
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
console.log(formatWaveHuman(result));
|
|
471
|
+
if (result.partial)
|
|
472
|
+
process.exitCode = 2;
|
|
473
|
+
}
|
|
406
474
|
function flagValue(args, name) {
|
|
407
475
|
const i = args.indexOf(name);
|
|
408
476
|
return i >= 0 && i + 1 < args.length ? args[i + 1] : undefined;
|
|
@@ -1451,7 +1519,9 @@ async function main() {
|
|
|
1451
1519
|
if (command === "xray" && target)
|
|
1452
1520
|
return await cmdXray(target, args);
|
|
1453
1521
|
if (command === "friction" && target)
|
|
1454
|
-
return await cmdFriction(target, args.includes("--json"));
|
|
1522
|
+
return await cmdFriction(target, args.includes("--json"), args);
|
|
1523
|
+
if (command === "wave")
|
|
1524
|
+
return await cmdWave(args);
|
|
1455
1525
|
if (command === "ci" && target)
|
|
1456
1526
|
return await cmdCi(target, args.includes("--json"));
|
|
1457
1527
|
if (command === "ci-prep" && target)
|