@reneturcios/greenlight 0.17.1 → 0.19.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 +20 -8
- package/dist/behavior.d.ts +13 -3
- package/dist/behavior.js +41 -10
- package/dist/behavior.js.map +1 -1
- package/dist/bounty.d.ts +8 -0
- package/dist/bounty.js +15 -0
- package/dist/bounty.js.map +1 -1
- package/dist/cli.js +131 -8
- package/dist/cli.js.map +1 -1
- package/dist/friction-index.js +13 -4
- package/dist/friction-index.js.map +1 -1
- package/dist/friction.d.ts +17 -2
- package/dist/friction.js +67 -19
- package/dist/friction.js.map +1 -1
- package/dist/github.d.ts +131 -2
- package/dist/github.js +235 -42
- package/dist/github.js.map +1 -1
- package/dist/help.js +19 -6
- package/dist/help.js.map +1 -1
- package/dist/index.js +16 -1
- package/dist/index.js.map +1 -1
- package/dist/merge-velocity.js +5 -3
- package/dist/merge-velocity.js.map +1 -1
- package/dist/types.d.ts +52 -3
- package/dist/verdict.d.ts +16 -1
- package/dist/verdict.js +36 -2
- package/dist/verdict.js.map +1 -1
- package/package.json +1 -1
- package/server.json +3 -3
package/README.md
CHANGED
|
@@ -38,12 +38,12 @@ 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; 32 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
|
|
45
45
|
|
|
46
|
-
*
|
|
46
|
+
* 1079 tests, all passing, including a fixture corpus pinned to real campaign cases (a taken issue, a weak PR mention, claim-first gates, scoped claims, half-shipped issues, stale claims, paused-PR workflows).
|
|
47
47
|
* Dogfooded on real campaigns: the toolkit has authored 80+ pull requests across dozens of public open-source repos. Every feature exists because a real contribution died without it.
|
|
48
48
|
* Distributed everywhere the audience already looks: npm (`@reneturcios/greenlight`), the MCP Registry, and GHCR (`ghcr.io/cloudcorprecords/greenlight`), plus Docker and direct MCP stdio.
|
|
49
49
|
* Two living datasets ship with it: the Contribution Friction Index (`data/friction-index.json`) and the Agent Policy Index (505 repos, 127 gated, 19 agent bans, published at `docs/policy.html`), both built from real campaign field work, not scraped guesses.
|
|
@@ -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 32 tools (see the list below).
|
|
107
107
|
|
|
108
108
|
## CLI
|
|
109
109
|
|
|
@@ -133,6 +133,13 @@ npx @reneturcios/greenlight scan PrefectHQ/fastmcp
|
|
|
133
133
|
npx @reneturcios/greenlight xray usebruno/bruno --issues 8149,9319
|
|
134
134
|
npx @reneturcios/greenlight scan usebruno/bruno --deep
|
|
135
135
|
|
|
136
|
+
# Scans stop on a wall-time budget (default 600s; override with --max-time <seconds>).
|
|
137
|
+
# A scan that hits the budget returns the results gathered so far, labeled partial:
|
|
138
|
+
# --json gains partial: true, partial_reason ("scan time budget 600s reached; evaluated
|
|
139
|
+
# K of M candidates"), evaluated, and candidates; the human output prints the results
|
|
140
|
+
# plus a "Partial scan:" line naming the budget. Partial scans still exit 0.
|
|
141
|
+
npx @reneturcios/greenlight scan PrefectHQ/prefect --max-time 300
|
|
142
|
+
|
|
136
143
|
# Friction score for a repo
|
|
137
144
|
npx @reneturcios/greenlight friction triggerdotdev/trigger.dev
|
|
138
145
|
|
|
@@ -283,7 +290,7 @@ The same engine is exposed over stdio for coding agents (Claude, Cursor, Cline).
|
|
|
283
290
|
}
|
|
284
291
|
```
|
|
285
292
|
|
|
286
|
-
Tools (
|
|
293
|
+
Tools (32):
|
|
287
294
|
|
|
288
295
|
* `check_issue` - verdict for one issue URL
|
|
289
296
|
* `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)
|
|
@@ -293,6 +300,7 @@ Tools (31):
|
|
|
293
300
|
* `verify` - honest base-vs-head test delta for a checked-out repo (red-first, never raw counts)
|
|
294
301
|
* `shipcheck` - release-readiness: GitHub release, npm version, registry record, and GHCR tag, GO / NO-GO per surface
|
|
295
302
|
* `passport` - contributor passport for a GitHub login: ledger profile, merged PR count, honor ratio
|
|
303
|
+
* `bounty_check` - bounty-liveness classification for one issue: live platform, unverifiable claim, or dead platform
|
|
296
304
|
* `claim_issue` - verdict-gated claim comment (dry run unless `execute: true`)
|
|
297
305
|
* `prep_issue` - fork and branch check plus a local PREP.md checklist with the repo's learned conventions (dry run unless `execute: true`)
|
|
298
306
|
* `open_pr` - DCO-aware PR opening from a fork branch (dry run unless `execute: true`)
|
|
@@ -328,7 +336,7 @@ The verdict engine is deterministic (no LLM) and checks, in order:
|
|
|
328
336
|
3. An open linked PR that **closes** the issue: **TAKEN**. A PR that merely mentions the issue number (like pdfcn PR #91, which closes #70 but lists sibling issues in its body) only adds a `weak-pr-mention` flag and downgrades an otherwise-GO verdict to **RISKY**. Marked a duplicate of an issue closed as completed: **RISKY** with the `duplicate-of-closed` flag (the work already shipped; a still-open duplicate target only adds a `duplicate-of-open` warning with its URL).
|
|
329
337
|
4. Two or more strong claimants: **TAKEN** (claim race).
|
|
330
338
|
5. One strong fresh whole-issue claim (under 45 days): **RISKY** with the `claimed` flag. A fresh claim scoped to part of the issue ("taking the remaining fallback ask"): **RISKY** with `claimed` + `partial-claim` flags and a reason naming the claimer and the scope, never TAKEN.
|
|
331
|
-
6. One strong claim older than 45 days with no closing PR: **RISKY** with the `stale_claim` flag. The claimant probably walked away, but nobody knows for sure. Stale scoped claims get the same treatment.
|
|
339
|
+
6. One strong claim older than 45 days with no closing PR: **RISKY** with the `stale_claim` flag. The claimant probably walked away, but nobody knows for sure. Stale scoped claims get the same treatment. Claims older than 180 days with no closing PR are *decayed* instead: disclosed with the `stale-signal` flag and a down-weighting reason, but never blocking on their own (two ancient claims no longer form a claim race, and a single ancient claim no longer downgrades a clean issue).
|
|
332
340
|
7. A hard gate (assignment, vouch, collaborators-only, paused unsolicited PRs, external PRs auto-closed by workflow) with nobody assigned: **GATED**. The last two come from CONTRIBUTING language ("paused unsolicited pull requests") and a scan of `.github/workflows` for close-external-PR automation.
|
|
333
341
|
8. No activity for 180+ days: **STALE**.
|
|
334
342
|
9. A soft claim-first gate (the repo asks contributors to comment before starting, like pdfcn): never **GATED**; it caps an otherwise-GO verdict at **RISKY** with a note, the same downgrade shape as a weak PR mention.
|
|
@@ -336,9 +344,13 @@ The verdict engine is deterministic (no LLM) and checks, in order:
|
|
|
336
344
|
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.
|
|
337
345
|
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**.
|
|
338
346
|
|
|
339
|
-
Friction score starts at 100 and subtracts gate weights (assignment -25, vouch -35, collaborators-only -50, paused unsolicited PRs -40, scoped pause -20, 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.
|
|
347
|
+
Friction score starts at 100 and subtracts gate weights (assignment -25, vouch -35, collaborators-only -50, paused unsolicited PRs -40, scoped pause -20, 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. When the classic starter labels read exactly 0, friction reports a display-only candidate estimate (open, unassigned issues under a wider starter label set, e.g. "beginner friendly", "easy", "first-timers-only") next to the label count; the estimate is never scored and never merged into the label count. See `src/friction.ts` for the exact formula.
|
|
348
|
+
|
|
349
|
+
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 within a 12-month recency bound (older closures are excluded, with a "stale signal" note when the newest closure itself is older than the bound), 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 the top-level `behavioral_gate` field (`detected`, `strength`, `threshold`, `evidence` with PR numbers, quotes, and URLs, `notes`); that field is the canonical location, and `signals.behavioralGate` is kept as an identical mirror for index-pipeline compatibility. Index pipelines like the rebaseline read the canonical field to reclassify repos whose written policy is silent but whose practice is hostile. API cost is bounded at 48 calls: up to 3 list calls to collect 15 closed-unmerged PRs plus up to 3 evidence pages per PR (issue comments, reviews, review comments); skip it with `--no-behavioral`.
|
|
350
|
+
|
|
351
|
+
`friction --lite` is the canonical lightweight friction computation: policy gates ONLY. It fetches the policy sources (CONTRIBUTING variants, AGENTS.md, CLAUDE.md, and AI_POLICY.md at the root, under `.github/`, and under `docs/`, plus the org-level AI_POLICY.md), parses gates from them, applies the collaborators-only push from `detectRepoPolicy` (no API calls today), and scores with the exact documented formula: start at 100, add the gate weights with the same GATE_WEIGHTS, clamp to [0, 100]. It skips the behavioral gate pass, merge velocity (and with it the external PR merge-rate term), first-review and issue-response timing, and the good-first-issue bonus: all outcome signals stay null and are skipped, never guessed, `signals.note` reads `"lite"`, and the report carries `lite: true`. The `gates` and `score` fields have the same shape as full friction, so the Agent Policy Index scanner can adopt `--lite` as the single friction-lite implementation instead of maintaining its own.
|
|
340
352
|
|
|
341
|
-
|
|
353
|
+
Every friction report (lite and full) also carries `policy_provenance`: one entry per probed policy source saying whether it was read, was absent, or failed to fetch, so "no policy file" is never again confused with "policy fetch failed" (previously both looked identical). In human output this renders as one summary line, e.g. `policy sources: 2 read (CONTRIBUTING.md, AGENTS.md), 3 absent, 0 fetch errors`.
|
|
342
354
|
|
|
343
355
|
`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. Candidates rank by winnability by default (outsider merge share, then friction, then stars); `--sort stars` restores the old order.
|
|
344
356
|
|
|
@@ -346,7 +358,7 @@ A scoped intake pause ("not accepting pull requests that add new formatting opti
|
|
|
346
358
|
|
|
347
359
|
Autopilot keeps a standing branch-health board: per-PR mergeable, mergeable_state, base SHA, and behind-count persist in its state between runs, render in the report output, and ride along in the `autopilot_report` MCP tool (`branch_health`, `branch_board`).
|
|
348
360
|
|
|
349
|
-
|
|
361
|
+
Four more CLI commands are agent-callable MCP tools: `verify` (honest base-vs-head test delta), `shipcheck` (release, npm, registry, and GHCR readiness), `passport` (claim-ledger contributor profile), and `bounty` (bounty-liveness for one issue).
|
|
350
362
|
|
|
351
363
|
### Agent-policy gates and triage holds
|
|
352
364
|
|
package/dist/behavior.d.ts
CHANGED
|
@@ -4,12 +4,19 @@ export interface BehavioralClosedPR {
|
|
|
4
4
|
author: string;
|
|
5
5
|
closedAt: string;
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* One maintainer-authored text blob on a closed-unmerged PR.
|
|
9
|
+
* v0.18.0 (item 5): maintainer rejections often live in review bodies
|
|
10
|
+
* and line comments, not issue comments, so all three surfaces feed the
|
|
11
|
+
* same matcher. `source` defaults to "comment" for older callers.
|
|
12
|
+
*/
|
|
7
13
|
export interface BehavioralMaintainerComment {
|
|
8
14
|
pr: number;
|
|
9
15
|
author: string;
|
|
10
16
|
association: string;
|
|
11
17
|
body: string;
|
|
12
18
|
url: string;
|
|
19
|
+
source?: "comment" | "review" | "review_comment";
|
|
13
20
|
}
|
|
14
21
|
export interface BehavioralSignal {
|
|
15
22
|
pr: number;
|
|
@@ -19,6 +26,8 @@ export interface BehavioralSignal {
|
|
|
19
26
|
url: string;
|
|
20
27
|
/** True for explicit phrases like "AI slop"; weaker signals can still count toward strength. */
|
|
21
28
|
strong: boolean;
|
|
29
|
+
/** Which surface carried the evidence: "comment" | "review" | "review_comment". */
|
|
30
|
+
source?: "comment" | "review" | "review_comment";
|
|
22
31
|
}
|
|
23
32
|
export interface BehavioralGate {
|
|
24
33
|
/** True when strength >= threshold: the maintainer rejects AI-built PRs in practice. */
|
|
@@ -53,9 +62,10 @@ export declare function partitionByRecency(prs: BehavioralClosedPR[], nowMs: num
|
|
|
53
62
|
newestClosedAt: string | null;
|
|
54
63
|
};
|
|
55
64
|
/**
|
|
56
|
-
* API cost contract: 1-3 calls to walk the closed-PR
|
|
57
|
-
* once 15 unmerged PRs are found) plus
|
|
58
|
-
* closed-unmerged PR
|
|
65
|
+
* API cost contract (v0.18.0, item 5): 1-3 calls to walk the closed-PR
|
|
66
|
+
* list (stops early once 15 unmerged PRs are found) plus up to three
|
|
67
|
+
* evidence pages per closed-unmerged PR (issue comments, reviews,
|
|
68
|
+
* review comments).
|
|
59
69
|
*/
|
|
60
70
|
export declare const BEHAVIORAL_MAX_API_CALLS: number;
|
|
61
71
|
/** Pure: does one comment body read as maintainer AI-rejection evidence? */
|
package/dist/behavior.js
CHANGED
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
* itself is older than the bound). The closed-PR list is
|
|
18
18
|
* walked (newest first, up to 3 pages of 30) until 15 unmerged PRs are
|
|
19
19
|
* collected, because merged PRs carry no closure evidence and an
|
|
20
|
-
* active repo's unmerged closures can sit pages deep.
|
|
21
|
-
* (first page, per_page=100) run only for those unmerged PRs,
|
|
22
|
-
*
|
|
20
|
+
* active repo's unmerged closures can sit pages deep. Evidence fetches
|
|
21
|
+
* (first page, per_page=100) run only for those unmerged PRs, on all
|
|
22
|
+
* three surfaces (issue comments, reviews, review comments), so the
|
|
23
|
+
* total cost is 1-3 list calls + up to 3 pages x 15 PRs (48 max).
|
|
23
24
|
* - Maintainer-side only: comment author_association must be OWNER,
|
|
24
25
|
* MEMBER, or COLLABORATOR, and the comment author must not be the PR
|
|
25
26
|
* author (a maintainer closing their own PR is not evidence against
|
|
@@ -80,11 +81,12 @@ export function partitionByRecency(prs, nowMs, months = BEHAVIORAL_RECENCY_MONTH
|
|
|
80
81
|
return { fresh, staleCount, newestClosedAt };
|
|
81
82
|
}
|
|
82
83
|
/**
|
|
83
|
-
* API cost contract: 1-3 calls to walk the closed-PR
|
|
84
|
-
* once 15 unmerged PRs are found) plus
|
|
85
|
-
* closed-unmerged PR
|
|
84
|
+
* API cost contract (v0.18.0, item 5): 1-3 calls to walk the closed-PR
|
|
85
|
+
* list (stops early once 15 unmerged PRs are found) plus up to three
|
|
86
|
+
* evidence pages per closed-unmerged PR (issue comments, reviews,
|
|
87
|
+
* review comments).
|
|
86
88
|
*/
|
|
87
|
-
export const BEHAVIORAL_MAX_API_CALLS = 3 + BEHAVIORAL_MAX_PRS;
|
|
89
|
+
export const BEHAVIORAL_MAX_API_CALLS = 3 + 3 * BEHAVIORAL_MAX_PRS;
|
|
88
90
|
const AI_TERMS = /\b(ai|a\.i\.|llm|llms|chatgpt|copilot|Muse|gemini|gpt-?4|gpt-?3|generated|slop)\b/i;
|
|
89
91
|
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;
|
|
90
92
|
/** Explicit phrases: unmistakable even without the two-term rule. */
|
|
@@ -128,7 +130,7 @@ export function detectBehavioralGateFromData(prs, comments) {
|
|
|
128
130
|
const evidence = [];
|
|
129
131
|
const prsWithSignal = new Set();
|
|
130
132
|
const notes = [];
|
|
131
|
-
notes.push(`scanned ${prs.length} closed-unmerged PRs, ${comments.length} maintainer
|
|
133
|
+
notes.push(`scanned ${prs.length} closed-unmerged PRs, ${comments.length} maintainer-authored texts on them (issue comments, reviews, review comments)`);
|
|
132
134
|
for (const c of comments) {
|
|
133
135
|
if (!MAINTAINER_ASSOCIATIONS.has(String(c.association ?? "")))
|
|
134
136
|
continue;
|
|
@@ -147,6 +149,7 @@ export function detectBehavioralGateFromData(prs, comments) {
|
|
|
147
149
|
quote: truncateQuote(c.body),
|
|
148
150
|
url: c.url,
|
|
149
151
|
strong,
|
|
152
|
+
source: c.source ?? "comment",
|
|
150
153
|
});
|
|
151
154
|
prsWithSignal.add(c.pr);
|
|
152
155
|
}
|
|
@@ -200,16 +203,44 @@ export async function detectBehavioralGate(owner, repo, opts = {}) {
|
|
|
200
203
|
// v0.17.1: drop stale closures before paying comment calls for them.
|
|
201
204
|
const recency = partitionByRecency(closedUnmerged, nowMs);
|
|
202
205
|
const live = recency.fresh;
|
|
206
|
+
// v0.18.0 (item 5): maintainer rejections live on three surfaces.
|
|
207
|
+
// One bounded page each: issue comments, reviews, review comments.
|
|
203
208
|
const comments = [];
|
|
204
209
|
for (const pr of live) {
|
|
205
|
-
const
|
|
206
|
-
for (const c of
|
|
210
|
+
const issueComments = (await apiGet(`/repos/${owner}/${repo}/issues/${pr.number}/comments?per_page=100`));
|
|
211
|
+
for (const c of issueComments ?? []) {
|
|
207
212
|
comments.push({
|
|
208
213
|
pr: pr.number,
|
|
209
214
|
author: String(c.user?.login ?? ""),
|
|
210
215
|
association: String(c.author_association ?? ""),
|
|
211
216
|
body: String(c.body ?? ""),
|
|
212
217
|
url: c.html_url,
|
|
218
|
+
source: "comment",
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
const reviews = (await apiGet(`/repos/${owner}/${repo}/pulls/${pr.number}/reviews?per_page=100`));
|
|
222
|
+
for (const r of reviews ?? []) {
|
|
223
|
+
const body = String(r.body ?? "").trim();
|
|
224
|
+
if (!body)
|
|
225
|
+
continue; // bare APPROVE/REQUEST_CHANGES votes carry no text
|
|
226
|
+
comments.push({
|
|
227
|
+
pr: pr.number,
|
|
228
|
+
author: String(r.user?.login ?? ""),
|
|
229
|
+
association: String(r.author_association ?? ""),
|
|
230
|
+
body,
|
|
231
|
+
url: r.html_url,
|
|
232
|
+
source: "review",
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
const reviewComments = (await apiGet(`/repos/${owner}/${repo}/pulls/${pr.number}/comments?per_page=100`));
|
|
236
|
+
for (const c of reviewComments ?? []) {
|
|
237
|
+
comments.push({
|
|
238
|
+
pr: pr.number,
|
|
239
|
+
author: String(c.user?.login ?? ""),
|
|
240
|
+
association: String(c.author_association ?? ""),
|
|
241
|
+
body: String(c.body ?? ""),
|
|
242
|
+
url: c.html_url,
|
|
243
|
+
source: "review_comment",
|
|
213
244
|
});
|
|
214
245
|
}
|
|
215
246
|
}
|
package/dist/behavior.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"behavior.js","sourceRoot":"","sources":["../src/behavior.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"behavior.js","sourceRoot":"","sources":["../src/behavior.ts"],"names":[],"mappings":"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"}
|
package/dist/bounty.d.ts
CHANGED
|
@@ -19,6 +19,14 @@ export interface BountySignal {
|
|
|
19
19
|
/** Evidence string for the flag, or "" when flag is null. */
|
|
20
20
|
evidence: string;
|
|
21
21
|
}
|
|
22
|
+
/** Live: fetch the issue and classify its bounty advertising (v0.18.0, item 23). */
|
|
23
|
+
export declare function detectBountyForIssue(owner: string, repo: string, number: number): Promise<{
|
|
24
|
+
issueUrl: string;
|
|
25
|
+
repo: string;
|
|
26
|
+
labels: string[];
|
|
27
|
+
title: string;
|
|
28
|
+
signal: BountySignal | null;
|
|
29
|
+
}>;
|
|
22
30
|
export declare function detectBounty(input: {
|
|
23
31
|
labels: string[];
|
|
24
32
|
title: string;
|
package/dist/bounty.js
CHANGED
|
@@ -11,6 +11,21 @@
|
|
|
11
11
|
const DEAD_PLATFORMS = {
|
|
12
12
|
algora: "algora.io 404s and its public API returns zero items (dead since 2025)",
|
|
13
13
|
};
|
|
14
|
+
import { apiGet, resolveCanonicalRepo } from "./github.js";
|
|
15
|
+
/** Live: fetch the issue and classify its bounty advertising (v0.18.0, item 23). */
|
|
16
|
+
export async function detectBountyForIssue(owner, repo, number) {
|
|
17
|
+
const canon = await resolveCanonicalRepo(owner, repo);
|
|
18
|
+
const data = (await apiGet(`/repos/${canon.owner}/${canon.repo}/issues/${number}`));
|
|
19
|
+
const labels = (data.labels ?? []).map((l) => String(l.name ?? ""));
|
|
20
|
+
const title = String(data.title ?? "");
|
|
21
|
+
return {
|
|
22
|
+
issueUrl: String(data.html_url ?? ""),
|
|
23
|
+
repo: canon.fullName,
|
|
24
|
+
labels,
|
|
25
|
+
title,
|
|
26
|
+
signal: detectBounty({ labels, title, body: String(data.body ?? "") }),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
14
29
|
const PLATFORM_RE = /(algora|opire|issuehunt|polar|gitcoin|oss[-.]?hunter|bountysource)/i;
|
|
15
30
|
function normalizePlatform(name) {
|
|
16
31
|
return name.toLowerCase().replace(/[-.]/g, "");
|
package/dist/bounty.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bounty.js","sourceRoot":"","sources":["../src/bounty.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAeH,MAAM,cAAc,GAA2B;IAC7C,MAAM,EAAE,wEAAwE;CACjF,CAAC;AAEF,MAAM,WAAW,GAAG,qEAAqE,CAAC;AAE1F,SAAS,iBAAiB,CAAC,IAAY;IACrC,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AACjD,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAI5B;IACC,MAAM,IAAI,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzE,MAAM,aAAa,GACjB,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC;QACxB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;QACf,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrE,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpC,IAAI,CAAC,aAAa;QAAE,OAAO,IAAI,CAAC;IAEhC,MAAM,EAAE,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClC,MAAM,QAAQ,GAAG,EAAE,CAAC,CAAC,CAAC,iBAAiB,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC3D,MAAM,EAAE,GAAG,oCAAoC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3D,MAAM,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IAE7C,IAAI,QAAQ,IAAI,cAAc,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzC,OAAO;YACL,QAAQ;YACR,MAAM;YACN,IAAI,EAAE,sBAAsB;YAC5B,QAAQ,EAAE,iBAAiB,QAAQ,qBAAqB,cAAc,CAAC,QAAQ,CAAC,EAAE;SACnF,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO;YACL,QAAQ,EAAE,SAAS;YACnB,MAAM;YACN,IAAI,EAAE,mBAAmB;YACzB,QAAQ,EAAE,sEAAsE;SACjF,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;AACxD,CAAC"}
|
|
1
|
+
{"version":3,"file":"bounty.js","sourceRoot":"","sources":["../src/bounty.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* Greenlight CLI.
|
|
4
4
|
*
|
|
5
5
|
* greenlight check <issue-url> [--json] [--gates-file path]
|
|
6
|
-
* greenlight scan <owner/repo> [--json] [--no-fallback] [--gates-file path]
|
|
6
|
+
* greenlight scan <owner/repo> [--json] [--no-fallback] [--gates-file path] [--deep] [--max-time seconds]
|
|
7
7
|
* greenlight wave [--index url|path] [--repos N] [--min-share N] [--stars lo,hi] [--issues-per-repo N] [--min-budget N] [--json]
|
|
8
8
|
* greenlight mcp
|
|
9
|
-
* greenlight friction <owner/repo> [--json]
|
|
9
|
+
* greenlight friction <owner/repo> [--json] [--lite] [--no-behavioral]
|
|
10
10
|
* greenlight ci <pr-url> [--json]
|
|
11
11
|
* greenlight review <pr-url> [--json]
|
|
12
12
|
* greenlight claim <issue-url> [--yes] [--force] [--json]
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* greenlight ci-prep <path> [--base <ref>] [--json] [--act]
|
|
18
18
|
* greenlight babysit [--prs urls] [--mine --repos o/r] [--account] [--claims-only] [--json]
|
|
19
19
|
* greenlight autopilot [--author login] [--prs urls] [--max-prs N] [--apply] [--max-fixes N] [--json]
|
|
20
|
+
* greenlight bounty <issue-url> [--json]
|
|
20
21
|
* greenlight sync [--author login] [--tracker csv|json] [--stale-days N] [--write] [--json]
|
|
21
22
|
* greenlight portfolio [--author login] [--json]
|
|
22
23
|
* greenlight init [--agent <tool>...] [--dry-run] [--force]
|
|
@@ -28,7 +29,7 @@
|
|
|
28
29
|
* The claim/prep/pr commands default to a DRY RUN: nothing is posted or
|
|
29
30
|
* created without --yes, and writes require GITHUB_TOKEN (or GH_TOKEN).
|
|
30
31
|
*/
|
|
31
|
-
import { parseIssueUrl, buildIssueSnapshot, scanCandidates, RateLimitError, fetchComments, apiCallsUsed, } from "./github.js";
|
|
32
|
+
import { parseIssueUrl, buildIssueSnapshot, scanCandidates, RateLimitError, fetchComments, apiCallsUsed, resolveCanonicalRepo, scanBudgetExceeded, DEFAULT_SCAN_MAX_TIME_SEC, } from "./github.js";
|
|
32
33
|
import { xrayIssue, xrayIssues, sortXrayResults, applyXrayDemotion } from "./xray.js";
|
|
33
34
|
import { CLAIM_MARKER } from "./protocol.js";
|
|
34
35
|
import { llmEnabled, llmModelId } from "./llm-claims.js";
|
|
@@ -64,6 +65,7 @@ import { detectBehavioralGate } from "./behavior.js";
|
|
|
64
65
|
import { viaApiClaim, viaApiRelease, viaApiHeartbeat, deviceFlowLogin, browserLoginFlow, defaultTokenStore, } from "./claim-api.js";
|
|
65
66
|
import { buildEvidencePack } from "./evidence.js";
|
|
66
67
|
import { buildPassport } from "./passport.js";
|
|
68
|
+
import { detectBountyForIssue } from "./bounty.js";
|
|
67
69
|
import { renderLabelsDryRun } from "./guard.js";
|
|
68
70
|
import { writeFileSync } from "node:fs";
|
|
69
71
|
import { createRequire } from "node:module";
|
|
@@ -151,7 +153,14 @@ function loadExternalGates(args) {
|
|
|
151
153
|
}
|
|
152
154
|
}
|
|
153
155
|
async function cmdCheck(issueUrl, asJson, args) {
|
|
154
|
-
const
|
|
156
|
+
const parsed = parseIssueUrl(issueUrl);
|
|
157
|
+
// v0.18.0 (item 15): resolve renames before any name-based fetch.
|
|
158
|
+
const canon = await resolveCanonicalRepo(parsed.owner, parsed.repo);
|
|
159
|
+
const { owner, repo } = canon;
|
|
160
|
+
const number = parsed.number;
|
|
161
|
+
const renameNote = canon.renamed
|
|
162
|
+
? `repo renamed: ${parsed.owner}/${parsed.repo} -> ${canon.fullName}`
|
|
163
|
+
: null;
|
|
155
164
|
// The check pipeline merges the legacy verdict with the Claim Protocol
|
|
156
165
|
// state: a protocol claim verdict overrides legacy claim inference, but
|
|
157
166
|
// never gates or cross-referenced PR evidence (see mergeProtocolVerdict).
|
|
@@ -194,9 +203,12 @@ async function cmdCheck(issueUrl, asJson, args) {
|
|
|
194
203
|
llm_claims: merged.llm_claims ?? [],
|
|
195
204
|
llm_status: merged.llm_status ?? null,
|
|
196
205
|
suggestion,
|
|
206
|
+
renamedFrom: renameNote ? `${parsed.owner}/${parsed.repo}` : null,
|
|
197
207
|
}, null, 2));
|
|
198
208
|
return;
|
|
199
209
|
}
|
|
210
|
+
if (renameNote)
|
|
211
|
+
console.log(renameNote);
|
|
200
212
|
printVerdict(snapshot, result, false);
|
|
201
213
|
for (const line of merged.protocolLines)
|
|
202
214
|
console.log(` ${line}`);
|
|
@@ -247,7 +259,23 @@ async function cmdStatus(issueUrl, asJson) {
|
|
|
247
259
|
}
|
|
248
260
|
}
|
|
249
261
|
async function cmdScan(repoRef, asJson, args) {
|
|
250
|
-
|
|
262
|
+
// v0.19.0 (item 17): wall-time bound (--max-time <seconds>, default
|
|
263
|
+
// DEFAULT_SCAN_MAX_TIME_SEC). Mega-backlog scans ran about 25 minutes;
|
|
264
|
+
// when the budget runs out the scan stops with marked partial results
|
|
265
|
+
// instead of running open-ended.
|
|
266
|
+
const startedAt = Date.now();
|
|
267
|
+
const maxTimeRaw = flagValue(args, "--max-time");
|
|
268
|
+
const maxTimeSec = maxTimeRaw !== undefined
|
|
269
|
+
? Math.max(1, Math.trunc(Number(maxTimeRaw)) || DEFAULT_SCAN_MAX_TIME_SEC)
|
|
270
|
+
: DEFAULT_SCAN_MAX_TIME_SEC;
|
|
271
|
+
const parsed = parseRepoRef(repoRef);
|
|
272
|
+
// v0.18.0 (item 15): the old name 422s the candidate search; resolve
|
|
273
|
+
// first and say so in the output.
|
|
274
|
+
const canon = await resolveCanonicalRepo(parsed.owner, parsed.repo);
|
|
275
|
+
const { owner, repo } = canon;
|
|
276
|
+
const scanRenameNote = canon.renamed ? `repo renamed: ${parsed.owner}/${parsed.repo} -> ${canon.fullName}` : null;
|
|
277
|
+
if (scanRenameNote && !asJson)
|
|
278
|
+
console.log(scanRenameNote);
|
|
251
279
|
const { issues, fallbackUsed } = await scanCandidates(owner, repo, {
|
|
252
280
|
fallback: !args.includes("--no-fallback"),
|
|
253
281
|
limit: 10,
|
|
@@ -275,7 +303,13 @@ async function cmdScan(repoRef, asJson, args) {
|
|
|
275
303
|
// check pipeline's LLM layer was built to catch. The protocol state
|
|
276
304
|
// costs extra API calls per issue; the scan's 10-issue cap and the
|
|
277
305
|
// budget notes in help keep that visible.
|
|
306
|
+
let partial = false;
|
|
307
|
+
let partialDuringDeep = false;
|
|
278
308
|
for (const issue of issues) {
|
|
309
|
+
if (scanBudgetExceeded(startedAt, maxTimeSec, Date.now())) {
|
|
310
|
+
partial = true;
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
279
313
|
try {
|
|
280
314
|
const { merged } = await checkIssueWithProtocol(owner, repo, issue.number, { externalGates, behavioralGate });
|
|
281
315
|
results.push({
|
|
@@ -310,6 +344,11 @@ async function cmdScan(repoRef, asJson, args) {
|
|
|
310
344
|
let deepVerified = 0;
|
|
311
345
|
if (args.includes("--deep") && results.length > 0) {
|
|
312
346
|
for (const r of results) {
|
|
347
|
+
if (scanBudgetExceeded(startedAt, maxTimeSec, Date.now())) {
|
|
348
|
+
partial = true;
|
|
349
|
+
partialDuringDeep = true;
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
313
352
|
try {
|
|
314
353
|
const xr = await xrayIssue(owner, repo, r.number);
|
|
315
354
|
deepVerified++;
|
|
@@ -321,8 +360,28 @@ async function cmdScan(repoRef, asJson, args) {
|
|
|
321
360
|
}
|
|
322
361
|
results.sort((a, b) => (rank[a.verdict] ?? 9) - (rank[b.verdict] ?? 9));
|
|
323
362
|
}
|
|
363
|
+
// v0.19.0 (item 17): a partial scan is valid, labeled output: same
|
|
364
|
+
// honest-partial contract as the wave command, still exit 0. When the
|
|
365
|
+
// budget is never hit these fields stay absent, byte-identical to
|
|
366
|
+
// before.
|
|
367
|
+
const partialReason = partial
|
|
368
|
+
? `scan time budget ${maxTimeSec}s reached; evaluated ${results.length} of ${issues.length} candidates` +
|
|
369
|
+
(partialDuringDeep ? `; deep-verified ${deepVerified} of ${results.length} before stopping` : "")
|
|
370
|
+
: null;
|
|
324
371
|
if (asJson) {
|
|
325
|
-
|
|
372
|
+
const out = {
|
|
373
|
+
repo: `${owner}/${repo}`,
|
|
374
|
+
renamedFrom: canon.renamed ? `${parsed.owner}/${parsed.repo}` : null,
|
|
375
|
+
fallback: fallbackUsed,
|
|
376
|
+
results,
|
|
377
|
+
};
|
|
378
|
+
if (partialReason) {
|
|
379
|
+
out.partial = true;
|
|
380
|
+
out.partial_reason = partialReason;
|
|
381
|
+
out.evaluated = results.length;
|
|
382
|
+
out.candidates = issues.length;
|
|
383
|
+
}
|
|
384
|
+
console.log(JSON.stringify(out, null, 2));
|
|
326
385
|
return;
|
|
327
386
|
}
|
|
328
387
|
console.log(`Scan of ${owner}/${repo}: ${results.length} candidate issues, GO first`);
|
|
@@ -344,6 +403,9 @@ async function cmdScan(repoRef, asJson, args) {
|
|
|
344
403
|
if (r.flags.length > 0)
|
|
345
404
|
console.log(` flags: ${r.flags.join(", ")}`);
|
|
346
405
|
}
|
|
406
|
+
if (partialReason) {
|
|
407
|
+
console.log(`\nPartial scan: ${partialReason}; results above cover the evaluated candidates only`);
|
|
408
|
+
}
|
|
347
409
|
}
|
|
348
410
|
async function cmdXray(repoRef, args) {
|
|
349
411
|
const { owner, repo } = parseRepoRef(repoRef);
|
|
@@ -399,6 +461,20 @@ async function cmdXray(repoRef, args) {
|
|
|
399
461
|
console.log(` flags: ${r.flags.join(", ")}`);
|
|
400
462
|
}
|
|
401
463
|
}
|
|
464
|
+
/**
|
|
465
|
+
* v0.19.0: one-line read provenance for a friction report, e.g.
|
|
466
|
+
* "policy sources: 2 read (CONTRIBUTING.md, AGENTS.md), 3 absent, 0 fetch errors".
|
|
467
|
+
* Fetch errors are counted separately so "no policy here" is never
|
|
468
|
+
* confused with "the policy fetch failed".
|
|
469
|
+
*/
|
|
470
|
+
function formatPolicyProvenance(provenance) {
|
|
471
|
+
const prov = provenance ?? [];
|
|
472
|
+
const read = prov.filter((p) => p.status === "read");
|
|
473
|
+
const absent = prov.filter((p) => p.status === "absent").length;
|
|
474
|
+
const errors = prov.filter((p) => p.status === "error").length;
|
|
475
|
+
const readPart = read.length > 0 ? `${read.length} read (${read.map((p) => p.file).join(", ")})` : "0 read";
|
|
476
|
+
return `policy sources: ${readPart}, ${absent} absent, ${errors} fetch errors`;
|
|
477
|
+
}
|
|
402
478
|
async function cmdFriction(repoRef, asJson, args = []) {
|
|
403
479
|
const { owner, repo } = parseRepoRef(repoRef);
|
|
404
480
|
// v0.9.3: one shared pipeline (fetchFrictionSummary) for the CLI, the
|
|
@@ -406,14 +482,22 @@ async function cmdFriction(repoRef, asJson, args = []) {
|
|
|
406
482
|
// policy source set everywhere instead of a single CONTRIBUTING read.
|
|
407
483
|
// v0.16.0: the behavioral gate pass (maintainer actions, not policy
|
|
408
484
|
// text) is part of the report; --no-behavioral skips its API cost.
|
|
485
|
+
// v0.19.0: --lite computes policy-file gates only (the canonical
|
|
486
|
+
// lightweight friction); --json carries lite: true in that mode.
|
|
409
487
|
const report = await fetchFrictionSummary(owner, repo, {
|
|
410
488
|
behavioral: !args.includes("--no-behavioral"),
|
|
489
|
+
lite: args.includes("--lite"),
|
|
411
490
|
});
|
|
412
491
|
if (asJson) {
|
|
413
492
|
console.log(JSON.stringify(report, null, 2));
|
|
414
493
|
return;
|
|
415
494
|
}
|
|
416
|
-
console.log(`Friction for ${owner}/${repo}: ${report.score}/100 (100 = easiest)`);
|
|
495
|
+
console.log(`Friction for ${report.owner}/${report.repo}: ${report.score}/100 (100 = easiest)`);
|
|
496
|
+
if (report.lite)
|
|
497
|
+
console.log("mode: lite (policy gates only)");
|
|
498
|
+
console.log(` ${formatPolicyProvenance(report.policy_provenance)}`);
|
|
499
|
+
if (report.resolvedFrom)
|
|
500
|
+
console.log(` repo renamed: ${report.resolvedFrom} -> ${report.owner}/${report.repo}`);
|
|
417
501
|
for (const reason of report.reasons)
|
|
418
502
|
console.log(` - ${reason}`);
|
|
419
503
|
if (report.reasons.length === 0)
|
|
@@ -429,7 +513,12 @@ async function cmdFriction(repoRef, asJson, args = []) {
|
|
|
429
513
|
console.log(` behavioral note: ${report.behavioral_gate.note}`);
|
|
430
514
|
}
|
|
431
515
|
const starterCount = report.signals.openGoodFirstIssueCount ?? null;
|
|
432
|
-
|
|
516
|
+
const candidateEstimate = report.signals.starterCandidateEstimate ?? null;
|
|
517
|
+
if (starterCount !== null && candidateEstimate !== null) {
|
|
518
|
+
// v0.19.0 (item 16): both numbers shown, never merged.
|
|
519
|
+
console.log(` open starter issues: ${starterCount} by label; candidate estimate (expanded labels, unassigned): ~${candidateEstimate}`);
|
|
520
|
+
}
|
|
521
|
+
else if (starterCount !== null)
|
|
433
522
|
console.log(` open starter issues: ${starterCount}`);
|
|
434
523
|
if (report.mergeVelocity)
|
|
435
524
|
console.log(` merge velocity: ${describeMergeVelocity(report.mergeVelocity)}`);
|
|
@@ -1281,6 +1370,38 @@ async function cmdPassport(login, args) {
|
|
|
1281
1370
|
console.log(passport.markdown);
|
|
1282
1371
|
}
|
|
1283
1372
|
}
|
|
1373
|
+
/**
|
|
1374
|
+
* `greenlight bounty <issue-url>` (v0.18.0, item 23): is the bounty this
|
|
1375
|
+
* issue advertises actually alive? The classifier never raises a
|
|
1376
|
+
* verdict: bounties are context, not landability. What it kills is
|
|
1377
|
+
* chasing a dead platform (algora has been dead since 2025; thousands
|
|
1378
|
+
* of issues still wear its labels).
|
|
1379
|
+
*/
|
|
1380
|
+
async function cmdBounty(issueUrl, args) {
|
|
1381
|
+
const { owner, repo, number } = parseIssueUrl(issueUrl);
|
|
1382
|
+
const result = await detectBountyForIssue(owner, repo, number);
|
|
1383
|
+
if (args.includes("--json")) {
|
|
1384
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
if (!result.signal) {
|
|
1388
|
+
console.log(`No bounty advertised on ${result.repo}#${number} ("${result.title}").`);
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
const s = result.signal;
|
|
1392
|
+
const head = s.amount ? `Bounty: ${s.amount}` : "Bounty advertised";
|
|
1393
|
+
const via = s.platform ? ` via ${s.platform}` : " (no platform named)";
|
|
1394
|
+
console.log(`${head}${via} on ${result.repo}#${number}`);
|
|
1395
|
+
if (s.flag === "bounty-platform-dead") {
|
|
1396
|
+
console.log(` DEAD PLATFORM: ${s.evidence}`);
|
|
1397
|
+
}
|
|
1398
|
+
else if (s.flag === "bounty-unverified") {
|
|
1399
|
+
console.log(` UNVERIFIED: ${s.evidence}`);
|
|
1400
|
+
}
|
|
1401
|
+
else {
|
|
1402
|
+
console.log(` platform is named and not known-dead; verify terms on the platform before counting on payment`);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1284
1405
|
async function cmdDigest(repoRef, args) {
|
|
1285
1406
|
const asJson = args.includes("--json");
|
|
1286
1407
|
const limitRaw = flagValue(args, "--limit");
|
|
@@ -1587,6 +1708,8 @@ async function main() {
|
|
|
1587
1708
|
return await cmdEvidence(target, args);
|
|
1588
1709
|
if (command === "passport" && target)
|
|
1589
1710
|
return await cmdPassport(target, args);
|
|
1711
|
+
if (command === "bounty" && target)
|
|
1712
|
+
return await cmdBounty(target, args);
|
|
1590
1713
|
if (command && NEEDS_TARGET[command]) {
|
|
1591
1714
|
// Known command with a missing argument: name the need and show its
|
|
1592
1715
|
// help, instead of the generic overview.
|