@reneturcios/greenlight 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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; 27 MCP tools make the whole engine available mid-run.
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 27 tools (see the list below).
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 (27):
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>;
@@ -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":"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"}
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
- const report = await fetchFrictionSummary(owner, repo);
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)