@reneturcios/greenlight 0.11.0 → 0.13.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
@@ -18,7 +18,7 @@ One command sets it up:
18
18
  npx @reneturcios/greenlight init --agent claude-code
19
19
  ```
20
20
 
21
- (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 17 tools (check_issue, scan_repo, repo_friction, claim_issue, prep_issue, open_pr, ci_triage, review_triage, guard_pr, repo_digest, claim_status, preflight, verify_comment, babysit_prs, evidence_pack, scout_campaign, my_claims).
21
+ (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 24 tools (check_issue, scan_repo, repo_friction, claim_issue, prep_issue, open_pr, ci_triage, review_triage, guard_pr, repo_digest, claim_status, preflight, verify_comment, babysit_prs, evidence_pack, scout_campaign, my_claims, sync_campaign, campaign_portfolio, autopilot_report, api_status, claim_via_api, heartbeat_via_api, xray_issues).
22
22
 
23
23
  ## The problem
24
24
 
@@ -134,6 +134,10 @@ npx @reneturcios/greenlight check https://github.com/batrapulkit/squidbrake/issu
134
134
  # unassigned issues when the repo does not use those labels; --no-fallback to opt out)
135
135
  npx @reneturcios/greenlight scan PrefectHQ/fastmcp
136
136
 
137
+ # Deep-verify candidates against their timelines first (catches hidden in-flight PRs)
138
+ npx @reneturcios/greenlight xray usebruno/bruno --issues 8149,9319
139
+ npx @reneturcios/greenlight scan usebruno/bruno --deep
140
+
137
141
  # Friction score for a repo
138
142
  npx @reneturcios/greenlight friction triggerdotdev/trigger.dev
139
143
 
@@ -276,6 +280,14 @@ The Agent Policy Index field work kept finding agent bans Greenlight itself miss
276
280
  - **Scoped bans dual-class instead of collapsing.** A ban aimed at one kind of agent operation (TypeScript's "bulk, agent-driven contributions", ppt-master's "purely AI-generated PRs submitted without human review") now surfaces as both `agents_banned` for autonomous operation and `agents_disclosure` for the human-led path. An explicit prohibition (nuxt, ripgrep, zed) still stands on its own, and explicit welcome or waiver language (hyperframes welcomes AI help and waives disclosure) never reads as a ban.
277
281
  - **Friction JSON states its units.** `friction --json` now carries `outsider_merge_share` (0 to 1 fraction) and `outsider_merge_share_pct` (0 to 100), plus `signals.externalPRMergeRate_pct` next to the existing fraction field. The human-readable output is unchanged.
278
282
 
283
+ ### v0.13.0: Timeline X-Ray
284
+
285
+ `greenlight xray` deep-verifies candidate issues against their GitHub timelines: the linked-PR walk (timeline + search) with linkStrength scoring, reporter-owned-PR detection, and a lazy comment-claim scan, at ~4 API calls per issue. `scan --deep` demotes anything the x-ray finds taken before presenting results. New MCP tool `xray_issues`; `scan_repo` gains `deep`. Born from a Bruno focus session where 6 of 7 hand-vetted issues turned out to be secretly taken by timeline-only PRs.
286
+
287
+ ### v0.12.0: the Claim Protocol GitHub App client
288
+
289
+ One command claims now: `greenlight claim <issue-url> --via-api` posts through the hosted claim API instead of handing you a comment to paste. `greenlight auth login` runs the GitHub device flow once and stores the token in your OS keychain (or `~/.greenlight/api-token` at 0600 when no keychain is available); `--token` / `GREENLIGHT_API_TOKEN` covers scripts. Before any `--via-api` write, the client checks whether the Greenlight Claims app is installed on the repo: installed means the claim goes through the app backend (durable state recorded via webhooks); not installed means the current direct-API comment path with a printed one-line note saying so. The downgrade is never silent. The flow is honest end to end: a hosted status read runs the TAKEN pre-flight (refuses on another holder unless `--queue-ok`), the posted comment is verified by re-reading the status, and a 401 re-runs the device flow once before printing the manual steps. `greenlight release --via-api` and `greenlight heartbeat --via-api` cover the rest of the lifecycle. The MCP server grows to 23 tools with `api_status`, `claim_via_api`, and `heartbeat_via_api` (all report-mode-safe).
290
+
279
291
  ### v0.11.0: Autopilot
280
292
 
281
293
  Open PRs were dying between babysit sweeps: a bot review sitting unanswered, a CI failure on the PR's own code, a drifted branch, a missing DCO trailer. Autopilot closes that loop. `greenlight autopilot` (report-only by default, `--apply` to fix) collects reviews, comments, and check results per open PR, verifies bot findings against the code at head (REAL, FALSE_POSITIVE with quoted counter-evidence, or NEEDS_HUMAN), and triages CI failures as introduced vs pre-existing against the base branch. The mechanical items fix themselves: DCO gaps get message-only surgery with byte-identical trees, merge conflicts resolve only when provably mechanical (lockfiles regenerated, changelogs unioned, version or counter bumps), and formatting pushes only when the diff is whitespace-only. Everything else becomes a plan, never a speculative edit. Fixes land on your fork branch with Rene's sign-off, respect the capability ledger's blocked repos, and replies queue as drafts behind the comment block instead of posting. Also exposed as the read-only `autopilot_report` MCP tool.
@@ -321,7 +333,7 @@ The same engine is exposed over stdio for coding agents (Claude, Cursor, Cline).
321
333
  Tools:
322
334
 
323
335
  * `check_issue` - verdict for one issue URL
324
- * `scan_repo` - rank a repo's open issues, GO first (label-tagged starter issues, falling back to recent open unassigned issues)
336
+ * `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)
325
337
  * `repo_friction` - friction score for a repo
326
338
  * `claim_issue` - verdict-gated claim comment (dry run unless `execute: true`)
327
339
  * `prep_issue` - fork and branch check plus a local PREP.md checklist with the repo's learned conventions (dry run unless `execute: true`)
@@ -335,6 +347,7 @@ Tools:
335
347
  * `verify_comment` - draft the verified-finding comment for maintainer-only repos (draft only, never posts)
336
348
  * `babysit_prs` - watch open PRs and get the one prioritized action list (needs you now, waiting on them, quiet), diffed against stored watermarks; also watches the my-claims registry (linked, healthy-active, heartbeat-due, expiring, unlinked-stale, queue-lost)
337
349
  * `evidence_pack` - the Verified PR evidence pack for one PR: guard verdict, CI triage, review triage, protocol status of linked issues, every claim citing its source
350
+ * `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
338
351
 
339
352
  Run it locally: `npm run dev` (MCP entry is `src/index.ts`; CLI is `src/cli.ts`).
340
353
 
@@ -0,0 +1,311 @@
1
+ export declare const DEFAULT_API_BASE = "https://claim-api-wine.vercel.app";
2
+ /**
3
+ * The claim-api OAuth app client id. Read from the live instance's
4
+ * /auth/github/login redirect (2026-10-18); override with
5
+ * GREENLIGHT_OAUTH_CLIENT_ID or --client-id when the app rotates.
6
+ */
7
+ export declare const DEFAULT_OAUTH_CLIENT_ID = "Ov23liv9yM4RTci0UysE";
8
+ export declare const GITHUB_DEVICE_BASE = "https://github.com";
9
+ export declare const GITHUB_API_BASE = "https://api.github.com";
10
+ /** Claim Protocol marker; the fallback comment is byte-compatible with it. */
11
+ export declare const CLAIM_MARKER_V1 = "<!-- greenlight-claim v1 -->";
12
+ type FetchImpl = typeof fetch;
13
+ export interface ApiConfig {
14
+ apiBase: string;
15
+ }
16
+ export declare function configFilePath(home?: string): string;
17
+ export declare function readApiConfig(home?: string): ApiConfig | null;
18
+ export declare function writeApiConfig(apiBase: string, home?: string): void;
19
+ /**
20
+ * Resolution order: --api-base flag, GREENLIGHT_API_BASE env, the cached
21
+ * config file, then the hosted default. A successful --api-base or env
22
+ * value is cached so later calls need no flag.
23
+ */
24
+ export declare function resolveApiBase(opts: {
25
+ flag?: string;
26
+ env?: NodeJS.ProcessEnv;
27
+ home?: string;
28
+ }): string;
29
+ export type StoredTokenKind = "github" | "session";
30
+ export interface StoredToken {
31
+ token: string;
32
+ kind: StoredTokenKind;
33
+ login: string | null;
34
+ storedAt: string;
35
+ }
36
+ export interface TokenStore {
37
+ get(): Promise<StoredToken | null>;
38
+ set(token: StoredToken): Promise<void>;
39
+ clear(): Promise<void>;
40
+ /** Human line for `auth status`: names the backend in use. */
41
+ describe(): string;
42
+ }
43
+ /** File fallback: ~/.greenlight/api-token, always 0600. */
44
+ export declare class FileTokenStore implements TokenStore {
45
+ private readonly home;
46
+ constructor(home?: string);
47
+ private path;
48
+ get(): Promise<StoredToken | null>;
49
+ set(t: StoredToken): Promise<void>;
50
+ clear(): Promise<void>;
51
+ describe(): string;
52
+ }
53
+ /**
54
+ * The default store: OS keychain when a keychain tool is available,
55
+ * otherwise the 0600 file fallback. Callers (and tests) may inject their
56
+ * own TokenStore instead.
57
+ */
58
+ export declare function defaultTokenStore(home?: string): Promise<TokenStore>;
59
+ export interface ResolvedToken {
60
+ token: string;
61
+ kind: StoredTokenKind;
62
+ login: string | null;
63
+ /** Where it came from: --token, env, or the store. */
64
+ source: "flag" | "env" | "store";
65
+ }
66
+ export declare function resolveApiToken(opts: {
67
+ flag?: string;
68
+ env?: NodeJS.ProcessEnv;
69
+ store?: TokenStore;
70
+ home?: string;
71
+ }): Promise<ResolvedToken | null>;
72
+ export declare function noTokenMessage(): string;
73
+ export interface DeviceCodeResponse {
74
+ device_code: string;
75
+ user_code: string;
76
+ verification_uri: string;
77
+ expires_in: number;
78
+ interval: number;
79
+ }
80
+ export interface DeviceLoginOpts {
81
+ clientId?: string;
82
+ fetchImpl?: FetchImpl;
83
+ store?: TokenStore;
84
+ deviceBase?: string;
85
+ apiBase?: string;
86
+ /** Override the server's poll interval (tests). */
87
+ pollIntervalMs?: number;
88
+ /** Overall budget; default 10 minutes. */
89
+ timeoutMs?: number;
90
+ onCode?: (userCode: string, verificationUri: string) => void;
91
+ }
92
+ /**
93
+ * Run the GitHub OAuth device flow, store the resulting user token, and
94
+ * return it. Polling honors the server's interval; mocked-fetch tests
95
+ * inject a tiny interval.
96
+ */
97
+ export declare function deviceFlowLogin(opts?: DeviceLoginOpts): Promise<ResolvedToken>;
98
+ export interface BrowserLoginOpts {
99
+ apiBase?: string;
100
+ env?: NodeJS.ProcessEnv;
101
+ home?: string;
102
+ store?: TokenStore;
103
+ fetchImpl?: FetchImpl;
104
+ /** Opens the URL in the user's browser; default tries the platform opener. */
105
+ openUrl?: (url: string) => Promise<void>;
106
+ /** Reads one line from stdin; injected in tests. */
107
+ promptToken?: () => Promise<string | null>;
108
+ }
109
+ /**
110
+ * Browser OAuth grant: open the claim-api login page, the user completes
111
+ * the grant in the browser, and pastes the session token from the
112
+ * ?format=json callback page. Stored as kind "session".
113
+ */
114
+ export declare function browserLoginFlow(opts?: BrowserLoginOpts): Promise<ResolvedToken>;
115
+ export declare class ApiError extends Error {
116
+ readonly status: number;
117
+ readonly code: string | undefined;
118
+ readonly payload?: unknown;
119
+ constructor(status: number, code: string | undefined, message: string, payload?: unknown);
120
+ }
121
+ export interface ClaimApiClientOpts {
122
+ base: string;
123
+ token?: string;
124
+ fetchImpl?: FetchImpl;
125
+ }
126
+ export declare class ClaimApiClient {
127
+ private readonly base;
128
+ private readonly token?;
129
+ private readonly fetchImpl;
130
+ constructor(opts: ClaimApiClientOpts);
131
+ private request;
132
+ /** Public status payload (SPEC 5.1 shape plus competing PRs/gates). */
133
+ issueStatus(owner: string, repo: string, number: number): Promise<any>;
134
+ /**
135
+ * App-aware check: is the Greenlight Claims app installed on this repo?
136
+ * Returns null when the endpoint does not exist (backend older than
137
+ * v1.0.0 returns 404) or the route cannot be reached; the caller treats
138
+ * null as "not installed" and falls back to the direct comment path.
139
+ */
140
+ appInstallation(owner: string, repo: string): Promise<{
141
+ installed: boolean;
142
+ app_slug?: string;
143
+ } | null>;
144
+ postClaim(owner: string, repo: string, number: number, opts: {
145
+ plan?: string;
146
+ eta_days?: number;
147
+ queue_ok?: boolean;
148
+ }): Promise<any>;
149
+ postRelease(owner: string, repo: string, number: number): Promise<any>;
150
+ postHeartbeat(owner: string, repo: string, number: number, mode?: "comment" | "reaction"): Promise<any>;
151
+ }
152
+ export type ViaApiPath = "app" | "direct";
153
+ export interface PathChoice {
154
+ path: ViaApiPath;
155
+ /** The one-line note, printed when falling back to the direct path. */
156
+ note: string | null;
157
+ }
158
+ /**
159
+ * Before any --via-api write: ask the backend whether the Greenlight
160
+ * Claims app is installed on the repo. Installed -> proceed through the
161
+ * app backend. Not installed, or the endpoint missing/unreachable ->
162
+ * fall back to the current direct-API comment path with the printed
163
+ * note. Never silently downgrade.
164
+ */
165
+ export declare function chooseViaApiPath(client: ClaimApiClient, owner: string, repo: string): Promise<PathChoice>;
166
+ /**
167
+ * After a via-api write, re-read the status payload and check the
168
+ * caller's login appears in the active claim or the queue. Returns false
169
+ * when it cannot be confirmed (the CLI then prints the comment URL and
170
+ * tells the user to check, rather than claiming success).
171
+ */
172
+ export declare function statusConfirmsLogin(status: any, login: string | null): boolean;
173
+ export interface ViaApiCommonDeps {
174
+ fetchImpl?: FetchImpl;
175
+ env?: NodeJS.ProcessEnv;
176
+ home?: string;
177
+ store?: TokenStore;
178
+ print?: (line: string) => void;
179
+ /** Re-runs the device flow (interactive); injected in tests. */
180
+ relogin?: () => Promise<ResolvedToken>;
181
+ /**
182
+ * The direct-comment fallback executor: posts the protocol comment
183
+ * through the GitHub API exactly as `greenlight claim --yes` does.
184
+ * Injected in tests; defaults to the real claim machinery.
185
+ */
186
+ directClaim?: (args: {
187
+ owner: string;
188
+ repo: string;
189
+ number: number;
190
+ plan?: string;
191
+ etaDays?: number;
192
+ queueOk?: boolean;
193
+ githubToken: string;
194
+ }) => Promise<{
195
+ posted: boolean;
196
+ commentUrl?: string;
197
+ commentBody: string;
198
+ }>;
199
+ /**
200
+ * The direct-comment fallback for release/heartbeat markers. Injected
201
+ * in tests; defaults to posting the marker comment via the GitHub API.
202
+ */
203
+ directMarker?: (args: {
204
+ owner: string;
205
+ repo: string;
206
+ number: number;
207
+ body: string;
208
+ githubToken: string;
209
+ }) => Promise<{
210
+ posted: boolean;
211
+ commentUrl?: string;
212
+ }>;
213
+ }
214
+ export interface ViaApiClaimOptions {
215
+ plan?: string;
216
+ etaDays?: number;
217
+ queueOk?: boolean;
218
+ tokenFlag?: string;
219
+ apiBaseFlag?: string;
220
+ /** false = dry run: no write, return the plan. */
221
+ yes?: boolean;
222
+ }
223
+ export interface ViaApiClaimResult {
224
+ outcome: "claimed" | "queued" | "already-claimed" | "refused-taken" | "dry-run" | "no-token" | "api-unreachable" | "auth-failed" | "direct-fallback";
225
+ path: ViaApiPath | null;
226
+ note: string | null;
227
+ commentUrl?: string;
228
+ commentBody?: string;
229
+ queuePosition?: number;
230
+ holder?: {
231
+ claimant: string;
232
+ expiresAt: string | null;
233
+ };
234
+ login?: string | null;
235
+ message: string;
236
+ }
237
+ export declare function viaApiClaim(issue: {
238
+ owner: string;
239
+ repo: string;
240
+ number: number;
241
+ url: string;
242
+ }, opts: ViaApiClaimOptions, deps?: ViaApiCommonDeps): Promise<ViaApiClaimResult>;
243
+ export interface ViaApiWriteResult {
244
+ outcome: "done" | "refused" | "no-token" | "api-unreachable" | "auth-failed";
245
+ path: ViaApiPath | null;
246
+ note: string | null;
247
+ commentUrl?: string;
248
+ login?: string | null;
249
+ message: string;
250
+ }
251
+ export declare function viaApiRelease(issue: {
252
+ owner: string;
253
+ repo: string;
254
+ number: number;
255
+ url: string;
256
+ }, opts: {
257
+ tokenFlag?: string;
258
+ apiBaseFlag?: string;
259
+ yes?: boolean;
260
+ }, deps?: ViaApiCommonDeps): Promise<ViaApiWriteResult>;
261
+ export declare function viaApiHeartbeat(issue: {
262
+ owner: string;
263
+ repo: string;
264
+ number: number;
265
+ url: string;
266
+ }, opts: {
267
+ tokenFlag?: string;
268
+ apiBaseFlag?: string;
269
+ mode?: "comment" | "reaction";
270
+ yes?: boolean;
271
+ }, deps?: ViaApiCommonDeps): Promise<ViaApiWriteResult>;
272
+ export interface McpToolDeps extends ViaApiCommonDeps {
273
+ parseIssueUrl?: (url: string) => {
274
+ owner: string;
275
+ repo: string;
276
+ number: number;
277
+ };
278
+ }
279
+ /**
280
+ * api_status: the hosted status payload for one issue, for agents that
281
+ * want the 60-second-fresh answer instead of the 15-minute static one.
282
+ * Read only; no token needed.
283
+ */
284
+ export declare function mcpApiStatus(args: {
285
+ issue_url: string;
286
+ api_base?: string;
287
+ }, deps?: McpToolDeps): Promise<unknown>;
288
+ /**
289
+ * claim_via_api: claim one issue through the hosted claim API.
290
+ * Report-mode-safe: execute defaults to false (dry run returns the plan);
291
+ * executes only with execute true and a stored token (else it refuses
292
+ * with the auth login instructions).
293
+ */
294
+ export declare function mcpClaimViaApi(args: {
295
+ issue_url: string;
296
+ plan?: string;
297
+ eta_days?: number;
298
+ queue_ok?: boolean;
299
+ execute?: boolean;
300
+ api_base?: string;
301
+ token?: string;
302
+ }, deps?: McpToolDeps): Promise<unknown>;
303
+ /** heartbeat_via_api: renew one claim through the hosted claim API. Same report-mode safety. */
304
+ export declare function mcpHeartbeatViaApi(args: {
305
+ issue_url: string;
306
+ mode?: "comment" | "reaction";
307
+ execute?: boolean;
308
+ api_base?: string;
309
+ token?: string;
310
+ }, deps?: McpToolDeps): Promise<unknown>;
311
+ export {};