gh-glance 0.14.1 → 0.15.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.
Files changed (4) hide show
  1. package/CHANGELOG.md +96 -2
  2. package/README.md +328 -78
  3. package/index.mjs +6903 -632
  4. package/package.json +5 -3
package/CHANGELOG.md CHANGED
@@ -5,7 +5,99 @@ All notable changes to this project are documented here.
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [0.15.1] - 2026-09-17
9
+
10
+ ### Fixed
11
+
12
+ - **A failed write no longer wedges every pane on the machine.** The shared
13
+ coordination lock is created with `open("wx")` and its owner record written
14
+ afterwards. When that write failed -- a full disk is the usual cause -- the
15
+ empty lock file stayed behind with no owner to test, so every pane, on every
16
+ request, reported "Coordinating with your other panes" until the file was
17
+ deleted by hand. A failed release (the rename can fail on a full APFS volume)
18
+ left a complete record naming a live owner that then waited on itself. Lock
19
+ creation now removes exactly what it created on any failure, a release that
20
+ cannot rename falls back to unlinking its own record, an acquirer that meets
21
+ its own record releases it, and an unreadable lock or recovery marker that has
22
+ gone untouched for ten seconds is quarantined through the same double-checked
23
+ protocol dead-owner locks already use -- which also clears the files 0.15.0
24
+ panes have already left behind.
25
+ - **`--doctor --probe` names why the governor is unavailable.** The API
26
+ governor line read `unavailable` for a lock nobody released and for a state
27
+ file that failed validation alike, and those are fixed in different places.
28
+ It now reads `unavailable (busy)`, `unavailable (corrupt)`, and so on.
29
+
30
+ ## [0.15.0] - 2026-09-15
31
+
32
+ ### Added
33
+
34
+ - **Equivalent panes now share one acquisition.** A private, bounded
35
+ authorization-partitioned store elects one producer for each due canonical
36
+ query and lets local followers adopt its validated generation. Conditional
37
+ validators, last-known-good rows, pagination state, source timestamps, and
38
+ capability evidence survive compatible restarts without treating cache reads
39
+ as fresh GitHub observations.
40
+ - **An optional foreground collector can serve local or SSH-carried clients.**
41
+ `--serve --config`, `--connect local`, `--collector-stdio`, and `--connect
42
+ ssh:<alias>` reuse the same governor and acquisition engine through bounded
43
+ Unix-socket/newline-JSON transport. Clients choose only allowlisted targets,
44
+ start no local GitHub request, and never fall back to standalone polling after
45
+ disconnect.
46
+ - **Collectors can opt in to durable webhook invalidation.** A loopback-only,
47
+ default-off listener verifies the exact raw body with HMAC SHA-256, persists a
48
+ compact bounded delivery queue before acknowledging it, coalesces duplicate
49
+ work, and requests an ordinary governed refresh. Webhook content is never row
50
+ data or freshness evidence, and missed events remain bounded by quiet
51
+ reconciliation polling.
52
+ - **Collectors can opt in to GitHub App installation authentication.** A
53
+ validated provider signs short-lived JWTs locally, mints repository- and
54
+ permission-restricted installation tokens through one bounded native HTTPS
55
+ adapter, and supplies the memory-only token only to its governed `gh api`
56
+ children. Installation and human quota identities remain separate; token
57
+ renewal preserves access identity, while repository or permission changes
58
+ advance an authorization fence.
59
+ - **Acquisition diagnostics now report measured work and freshness.** Local
60
+ doctor output distinguishes HTTP attempts, REST 200/304, proven and uncertain
61
+ core/GraphQL units, observer calls, cache hits, joined followers, queue wait,
62
+ active subscriptions, source success/change, and semantic holds without
63
+ exposing internal identity or credential material.
64
+ - **A deterministic sustained-efficiency gate and measurement report cover the
65
+ complete topology.** The offline workload advances an injected clock through
66
+ one simulated hour across duplicate/distinct panes and a 3+4 client split,
67
+ then reports sharing, reserve, freshness, queue, process, CPU, and RSS
68
+ evidence. Candidate/baseline percentages are emitted only for compatible
69
+ workload, machine, platform, and runtime evidence.
70
+
71
+ ### Changed
72
+
73
+ - **Remote receipt and conditional reuse no longer invent freshness.** Source
74
+ success and content change remain separate across 304s, process restarts,
75
+ collector epochs, SSH reconnects, clock skew, and retained cache reads. A
76
+ disconnect preserves visible rows with an honest source-age lower bound.
77
+ - **The dashboard cache now retains 32 recent authorization-scoped targets, up
78
+ to 60 rows per tab.** Shared-acquisition storage is separately bounded to 32
79
+ MiB, 512 entities, 32 live targets, and 128 subscriptions, with active targets
80
+ pinned ahead of inactive least-recently-used generations.
81
+ - **GraphQL capacity is authoritative and operation-specific.** Issues, pull
82
+ requests, pages, and the shared observer use explicit bounded envelopes and
83
+ reconcile their returned costs. The free `rate_limit` endpoint remains
84
+ diagnostic and cannot admit work or refund a reservation.
85
+
86
+ ### Security
87
+
88
+ - **Collector, SSH, webhook, and App boundaries fail closed.** Socket and state
89
+ ownership are nonce/inode fenced; protocol frames and queues are bounded;
90
+ webhook secrets and App private keys must be private regular files; SSH uses a
91
+ fixed forwarding-disabled argument vector; App children scrub competing token
92
+ variables; stale generations cannot cross provider or authorization changes;
93
+ and a missing optional Security permission is visible rather than rendered as
94
+ a clean result.
95
+ - **The installed npm artifact is checked as an exact five-file CLI.** It
96
+ contains only `index.mjs`, `package.json`, `README.md`, `CHANGELOG.md`, and
97
+ `LICENSE`, keeps `exports: {}`, and exercises installed version, help,
98
+ non-TTY, unknown-argument, collector, bridge, and client contracts. Tests,
99
+ fixtures, credentials, keys, webhook secrets, and configuration/state files
100
+ remain outside the tarball.
9
101
 
10
102
  ## [0.14.1] - 2026-09-08
11
103
 
@@ -1050,7 +1142,9 @@ engineering, security, QA and UX. What follows is what changed as a result.
1050
1142
  - The `main` field from `package.json`. It advertised the file as importable,
1051
1143
  but importing it took over the terminal or exited the host process.
1052
1144
 
1053
- [Unreleased]: https://github.com/juan294/gh-glance/compare/v0.14.1...HEAD
1145
+ [Unreleased]: https://github.com/juan294/gh-glance/compare/v0.15.1...HEAD
1146
+ [0.15.1]: https://github.com/juan294/gh-glance/compare/v0.15.0...v0.15.1
1147
+ [0.15.0]: https://github.com/juan294/gh-glance/compare/v0.14.1...v0.15.0
1054
1148
  [0.14.1]: https://github.com/juan294/gh-glance/compare/v0.14.0...v0.14.1
1055
1149
  [0.14.0]: https://github.com/juan294/gh-glance/compare/v0.13.3...v0.14.0
1056
1150
  [0.13.3]: https://github.com/juan294/gh-glance/compare/v0.13.2...v0.13.3
package/README.md CHANGED
@@ -25,10 +25,10 @@ without switching to the browser.
25
25
  ╭─ Actions · owner/repo ──────────────────────────────────────────────────╮
26
26
  │ TITLE │WORKFLOW │BRANCH │TIME │UPDATED │
27
27
  │ ─────────────────────────────────────────────────────────────────────── │
28
- │ >+ ci: pin actions to com… #443 CI develop 1m20s 28d ago │
29
- │ x fix: restore the prima… #442 Code… develop 1m28s 28d ago │
30
- │ + chore: bump dependenci… #441 CI dependa…int-10 30s 28d ago │
31
- │ - docs: update the readme #440 CI develop 15s 29d ago │
28
+ │ >+ ci: pin actions to com… #443 CI develop 1m20s 44d ago │
29
+ │ x fix: restore the prima… #442 Code… develop 1m28s 44d ago │
30
+ │ + chore: bump dependenci… #441 CI dependa…int-10 30s 44d ago │
31
+ │ - docs: update the readme #440 CI develop 15s 45d ago │
32
32
  │ │
33
33
  │ │
34
34
  ╰──────────────────────────────────────────────────────────────── 4 of 4 ─╯
@@ -124,9 +124,10 @@ current.
124
124
  npm install -g gh-glance
125
125
  ```
126
126
 
127
- That is the whole install. `gh-glance` ships as a single `index.mjs` with no
128
- build step, so there is nothing to compile and nothing to configure -- the
129
- package is the source you can read in this repository.
127
+ That is the whole install for the default standalone dashboard. `gh-glance`
128
+ ships as a single `index.mjs` with no build step, so there is nothing to
129
+ compile. The optional collector, SSH, webhook, and GitHub App modes use the
130
+ explicit configuration described below.
130
131
 
131
132
  The installed package is a CLI, not a JavaScript library. The `gh-glance`
132
133
  executable is supported; package-root and deep imports are intentionally
@@ -141,9 +142,9 @@ page](https://www.npmjs.com/package/gh-glance) shows the exact source.
141
142
 
142
143
  ### First run
143
144
 
144
- `gh-glance` uses the credentials and active account already selected by your
145
- local `gh` CLI. It does not request, inspect, or store the token itself. Check
146
- that account before starting the dashboard:
145
+ The default standalone dashboard uses the credentials and active account
146
+ already selected by your local `gh` CLI. It does not persist or display the
147
+ token. Check that account before starting the dashboard:
147
148
 
148
149
  ```bash
149
150
  gh auth status
@@ -323,16 +324,233 @@ pane definition. Flags are there when you want them:
323
324
  | `--background <mode>` | `all` (default) or `off`. `off` never requests data for a tab you are not looking at. Its count keeps the last known value and ages visibly, and switching to the tab fetches it. |
324
325
  | `--verbose` | Log one line per dashboard `gh` call to stderr, with timing and outcome. Credential lookup and the account-identity proof are deliberately excluded, so that they cannot log anything derived from a token. stderr must be redirected: `gh-glance --verbose 2>gh-glance.log`. |
325
326
  | `--doctor` | Print a diagnostic report and exit. See [Diagnostics](#diagnostics). |
327
+ | `--doctor --probe` | Add bounded, admitted GitHub capability probes to the local diagnostic report. |
328
+ | `--serve --config <path>` | Run the optional local collector in the foreground from a private, versioned JSON allowlist. |
329
+ | `--connect local` | Subscribe the dashboard to the current OS user's collector. No GitHub request runs in the client process. |
330
+ | `--connect ssh:<alias>` | Subscribe through the named SSH config alias. The client starts only the fixed remote collector bridge and never falls back to local GitHub acquisition. |
331
+ | `--collector-stdio` | Bridge bounded newline JSON on stdin/stdout to an already running local collector. |
326
332
 
327
333
  An unrecognised flag exits 2 rather than being ignored, so a typo fails loudly.
328
334
 
335
+ ### Optional local collector
336
+
337
+ The collector is opt-in and stays in the foreground. It is supported on macOS
338
+ and Linux. Windows continues to support the standalone dashboard, but rejects
339
+ collector hosting and bridge modes. The endpoint is always
340
+ `collector-v1.sock` inside gh-glance's private config directory; there is no TCP
341
+ dashboard/client transport or custom socket flag. The optional loopback webhook
342
+ ingress described below is the only HTTP listener.
343
+
344
+ Create a mode-0600 configuration file:
345
+
346
+ ```json
347
+ {
348
+ "version": 1,
349
+ "providers": {
350
+ "personal": { "type": "gh", "host": "github.com" }
351
+ },
352
+ "targets": [
353
+ { "host": "github.com", "repo": "owner/repository", "provider": "personal" }
354
+ ]
355
+ }
356
+ ```
357
+
358
+ Then run the service and dashboard in separate terminals:
359
+
360
+ ```sh
361
+ gh-glance --serve --config ~/.config/gh-glance/collector.json
362
+ gh-glance --connect local --repo owner/repository
363
+ ```
364
+
365
+ Providers use the server OS user's existing host-specific `gh` login. Clients
366
+ select only an allowlisted `(host, repo)` pair and cannot submit credentials,
367
+ providers, API paths, queries, commands, or filesystem paths. Renamed aliases
368
+ that resolve to one repository cannot cross provider ownership. Disconnecting
369
+ keeps last-known-good rows and their source age, then reconnects to a new server
370
+ epoch without silently falling back to standalone polling.
371
+
372
+ #### Optional GitHub App provider
373
+
374
+ The default `gh` provider above is unchanged. A collector can instead opt in to
375
+ one GitHub App installation. gh-glance does not create or register the App,
376
+ install it, grant permissions, or generate its private key. Put the existing
377
+ PEM key in an absolute, current-user-owned mode-0600 file and name every allowed
378
+ repository ID and read permission explicitly:
379
+
380
+ ```json
381
+ {
382
+ "version": 1,
383
+ "providers": {
384
+ "automation": {
385
+ "type": "github-app",
386
+ "host": "github.com",
387
+ "clientId": "Iv1.example",
388
+ "installationId": 123456,
389
+ "privateKeyFile": "/absolute/private/path/github-app.pem",
390
+ "repositoryIds": [123456789],
391
+ "permissions": {
392
+ "metadata": "read",
393
+ "actions": "read",
394
+ "issues": "read",
395
+ "pull_requests": "read",
396
+ "vulnerability_alerts": "read",
397
+ "security_events": "read",
398
+ "secret_scanning_alerts": "read"
399
+ }
400
+ }
401
+ },
402
+ "targets": [
403
+ { "host": "github.com", "repo": "owner/repository", "provider": "automation" }
404
+ ]
405
+ }
406
+ ```
407
+
408
+ `metadata`, `actions`, `issues`, and `pull_requests` are required for the four
409
+ collector tabs. The three Security permissions are independent and optional:
410
+
411
+ | Security surface | Configuration permission |
412
+ |---|---|
413
+ | Dependabot alerts | `vulnerability_alerts: read` |
414
+ | Code scanning alerts | `security_events: read` |
415
+ | Secret scanning alerts | `secret_scanning_alerts: read` |
416
+
417
+ An omitted Security permission produces an explicit unavailable capability for
418
+ that source without disabling the other tabs or permitted Security sources.
419
+ The collector signs a short-lived JWT locally, requests only the configured
420
+ repositories and permissions, and keeps the installation token in memory. Data
421
+ still travels through `gh api`; the selected token is supplied only in that
422
+ child's environment. Token refresh, failure, or expiry never falls back to the
423
+ server user's personal `gh` credential. Configuration changes require a
424
+ collector restart. Installation deletion, suspension, and repository removal
425
+ webhooks fence old data when webhook ingress is also enabled.
426
+
427
+ #### Optional webhook invalidation
428
+
429
+ Webhook ingress is disabled unless the collector configuration contains an
430
+ enabled `webhook` object. It listens only on the configured loopback address and
431
+ fixed `/webhooks/github` route. Exposing that route requires a user-managed
432
+ HTTPS reverse proxy; gh-glance does not create a public listener, register a
433
+ GitHub webhook, or manage proxy/TLS infrastructure.
434
+
435
+ Create a separate mode-0600 secret file with no trailing newline, then add the
436
+ exact repositories and resource families covered by the GitHub webhook:
437
+
438
+ ```json
439
+ {
440
+ "version": 1,
441
+ "providers": {
442
+ "personal": { "type": "gh", "host": "github.com" }
443
+ },
444
+ "targets": [
445
+ { "host": "github.com", "repo": "owner/repository", "provider": "personal" }
446
+ ],
447
+ "webhook": {
448
+ "enabled": true,
449
+ "address": "127.0.0.1",
450
+ "port": 8787,
451
+ "secretFile": "/absolute/private/path/webhook-secret",
452
+ "targets": [
453
+ {
454
+ "host": "github.com",
455
+ "repo": "owner/repository",
456
+ "provider": "personal",
457
+ "resources": ["actions", "issues", "prs", "security"]
458
+ }
459
+ ]
460
+ }
461
+ }
462
+ ```
463
+
464
+ Configure GitHub to send JSON payloads and select only the events needed for
465
+ the covered resources: `workflow_run`/`workflow_job` for Actions, `issues` and
466
+ issue comments for Issues, pull requests/reviews/review comments/review threads
467
+ and PR issue comments for PRs, and the supported Dependabot, repository
468
+ vulnerability, code-scanning, and secret-scanning alert events for Security.
469
+ Installation and repository-access events fence permissions. Event availability
470
+ depends on the target GitHub host. The collector verifies the SHA-256 signature
471
+ over the exact request bytes before parsing, persists only delivery IDs and
472
+ compact invalidations, and returns 202 only after durable acceptance. The
473
+ listener buffers at most 25 MiB per request and 50 MiB across concurrent
474
+ requests; aggregate pressure returns 503 so GitHub or the proxy can retry. A
475
+ local signed-delivery check can use the same payload bytes and secret to POST
476
+ `Content-Type: application/json`, `X-GitHub-Event`, `X-GitHub-Delivery`, and
477
+ `X-Hub-Signature-256: sha256=<hex digest>` to
478
+ `http://127.0.0.1:8787/webhooks/github`.
479
+
480
+ For example, with the collector running and the paths adjusted locally:
481
+
482
+ ```sh
483
+ printf '%s' '{"action":"opened","repository":{"full_name":"owner/repository"},"issue":{"number":1}}' > /tmp/gh-glance-hook.json
484
+ signature=$(node --input-type=module - /absolute/private/path/webhook-secret /tmp/gh-glance-hook.json <<'NODE'
485
+ import { createHmac } from "node:crypto";
486
+ import { readFileSync } from "node:fs";
487
+ const [, , secretPath, bodyPath] = process.argv;
488
+ process.stdout.write(createHmac("sha256", readFileSync(secretPath)).update(readFileSync(bodyPath)).digest("hex"));
489
+ NODE
490
+ )
491
+ curl -i --data-binary @/tmp/gh-glance-hook.json \
492
+ -H 'Content-Type: application/json' \
493
+ -H 'X-GitHub-Event: issues' \
494
+ -H 'X-GitHub-Delivery: 11111111-1111-4111-8111-111111111111' \
495
+ -H "X-Hub-Signature-256: sha256=$signature" \
496
+ http://127.0.0.1:8787/webhooks/github
497
+ ```
498
+
499
+ The expected response is 202. Repeating the same delivery ID is acknowledged
500
+ without scheduling duplicate work.
501
+
502
+ Webhook events only request an ordinary governed GitHub refresh. They never
503
+ become rows or freshness evidence, and they do not bypass quota or secondary
504
+ cooldowns. After two validated unchanged observations, covered quiet resources
505
+ reconcile at least every
506
+ `max(--refresh, 300 seconds)`; Actions with running work keep their fast cadence,
507
+ and manual refresh remains available. Missing deliveries therefore delay an
508
+ update until reconciliation instead of disabling polling.
509
+
510
+ ### Optional SSH collector client
511
+
512
+ An SSH client connects to a collector already running under your account on a
513
+ computer you control. Configure the host, user, port, key, jump host, and host
514
+ trust in `~/.ssh/config`; the CLI accepts only a simple alias:
515
+
516
+ ```sh
517
+ # On the collector computer
518
+ gh-glance --serve --config ~/.config/gh-glance/collector.json
519
+
520
+ # On a client computer, after normal ssh host-key/key setup
521
+ gh-glance --connect ssh:my-collector --repo owner/repository
522
+ ```
523
+
524
+ The client runs exactly `ssh -T -o BatchMode=yes -o ClearAllForwardings=yes -o
525
+ ForwardAgent=no -- <alias> 'gh-glance --collector-stdio'`. It does not disable host-key checking,
526
+ install software or keys, start the collector, create forwarding, or send the
527
+ repository through a shell command. `gh-glance` must already be on the remote
528
+ login's `PATH`, and the collector must already be running there.
529
+
530
+ Local `GH_*`, `GITHUB_*`, `GH_CONFIG_DIR`, and `XDG_CONFIG_HOME` values are not
531
+ given to the SSH child. Repository rows arrive through the same bounded protocol
532
+ as a local client. A disconnect keeps them visible as `Disconnected` and retries
533
+ after a bounded 1, 2, 4, 8, 16, then 30-second ladder with jitter. A new server
534
+ epoch resubscribes once; old epoch/generation frames cannot overwrite it.
535
+
536
+ Remote snapshots carry the source observation time and collector wall time.
537
+ The client persists the resulting conservative age under a private digest of
538
+ the SSH alias plus target. Reconnect, receipt time, a backward local clock, and
539
+ a cold offline restart cannot make the same observation look younger. A newer
540
+ successful source observation can reset the age. Pressing `Enter` still opens a
541
+ validated HTTPS target URL on the client computer and makes no GitHub API call.
542
+
543
+ `gh-glance --connect ssh:my-collector --repo owner/repository --doctor` is a
544
+ local-only report of the selected source and disabled fallback. Live endpoint
545
+ probes are intentionally rejected for collector clients.
546
+
329
547
  Environment variables work too, and the flags take precedence:
330
548
 
331
549
  | Variable | Effect |
332
550
  |---|---|
333
551
  | `GH_REPO=[host/]owner/name` | Watch a specific repository instead of the current directory's. A qualified value supplies the host; an unqualified value means `github.com`. An explicit `--repo` wins. |
334
552
  | `GH_HOST=<host>` | When no explicit `--repo` overrides it, send every call and the account governor to a GitHub Enterprise or EMU host instead of `github.com`. |
335
- | `GH_TOKEN`, `GITHUB_TOKEN`, `GH_ENTERPRISE_TOKEN`, `GITHUB_ENTERPRISE_TOKEN` | Used by `gh`. gh-glance never logs or stores these values, but hashes each set value locally as part of the account-scoped cache and governor namespaces so panes with different credentials cannot share rows or admission. The raw values are not written to disk. |
553
+ | `GH_TOKEN`, `GITHUB_TOKEN`, `GH_ENTERPRISE_TOKEN`, `GITHUB_ENTERPRISE_TOKEN` | Used by the default `gh` provider with `gh`'s host-specific precedence. gh-glance never logs or stores the raw values. One-way credential digests keep private rows access-partitioned; credentials GitHub verifies as the same principal share its quota ledger. App-provider children remove these variables before setting their memory-only installation token. |
336
554
  | `GH_CONFIG_DIR` | Selects `gh`'s account configuration and contributes its normalized identity to the cache and governor namespaces. Separate values can isolate simultaneous panes on different accounts; see [No account pinning](#limitations). |
337
555
  | `GH_GLANCE_REFRESH=<seconds>` | Minimum active-tab poll interval, 2-3600. Sets the floor for every pane in a shell; `--refresh` takes precedence. See [Rate limit](#rate-limit). |
338
556
  | `GH_GLANCE_ICONS=unicode` | Unicode status glyphs and single-cell text substitutes for Nerd Font row icons |
@@ -376,13 +594,15 @@ $XDG_CONFIG_HOME/gh-glance/dashboard-cache.json
376
594
  ~/.config/gh-glance/dashboard-cache.json
377
595
  ```
378
596
 
379
- The cache is scoped to the repository, host, and effective `gh` account
380
- namespace. In inferred mode, the host and current working directory also form
381
- the identity, so rows from one checkout or credential context cannot appear in
382
- another. The namespace uses `GH_CONFIG_DIR`/`hosts.yml` identity plus one-way
383
- digests of supported token environment variables; raw credentials are never
384
- stored. It retains at most five recent targets and 60 rows per tab; a shortened
385
- cached tab keeps its `+` marker rather than presenting the saved count as exact.
597
+ The cache is scoped to the repository, host, provider, and effective access
598
+ partition. In inferred standalone mode, the host and current working directory
599
+ also form the identity, so rows from one checkout or credential context cannot
600
+ appear in another. Human-provider access includes one-way digests of the
601
+ selected credential and its authorization generation; App-provider access also
602
+ binds the configured repository and permission restrictions. Raw credentials
603
+ are never stored. It retains at most 32 recent targets and 60 rows per tab; a
604
+ shortened cached tab keeps its `+` marker rather than presenting the saved count
605
+ as exact.
386
606
 
387
607
  Only successfully parsed, non-blind observations replace saved rows. A failed
388
608
  request can add a live error or `?` marker, but it cannot turn known Security
@@ -486,30 +706,38 @@ It will not claim a feature is "not enabled" because of a lapse.
486
706
  gh-glance --doctor > report.txt
487
707
  ```
488
708
 
489
- Collects, in one plain-text block: the `gh-glance`, Node and `gh` versions;
490
- which hosts `gh` is authenticated for; how the repository target resolved and
491
- from where; your remaining REST and GraphQL budget plus the configuration's
492
- unpaced cost projection; shared governor health; the relevant environment
493
- variables; a read-only `Repository access` probe; and each bounded dashboard
494
- probe, including the Security priority lanes, with the exact argv it sent, its
495
- outcome, and how any error was classified (`unavailable`, `rate-limited`,
496
- `auth-problem` or `other`).
497
-
498
- Doctor first makes one free `rate_limit` request to the effective host. It then
499
- uses an ephemeral governor lease and admits every quota-consuming endpoint at
500
- its exact declared cost. A probe whose safe slot is later, whose resource is
501
- held, or whose budget is unavailable is reported as `SKIPPED`; diagnostics do
502
- not bypass the reserve. The `API governor` section reports `healthy`, `waiting
503
- for probe`, `stale`, `blocked`, or `unavailable`, plus the number of live leases
504
- and each resource's remaining calls, hard reserve, and reset. It does not print
505
- the scope hash, account identity, state path, lock owner, or reservation IDs.
709
+ By default, doctor reads only local evidence. It reports the executable and
710
+ configuration context, repository target, and shared-acquisition metrics and
711
+ freshness without making a GitHub API request or resolving a credential. The
712
+ acquisition section keeps the last successful observation, last payload change,
713
+ next due time, current hold, active query/subscriber count, request outcomes,
714
+ known charges, conservative outstanding charges, cache hits, joined followers,
715
+ measured queue time, observer calls, and producer epoch separate. A joined
716
+ follower is counted only when it waits on an existing producer generation; a
717
+ cached subscription is a cache hit, not a coalesced request. Active queries are
718
+ queries with a live subscriber. Retained queries remain listed as `cache-only`
719
+ without inflating that count. Holds distinguish observer failure, primary and
720
+ secondary limits, disconnected acquisition, shared producer waits, and local
721
+ coordination.
722
+
723
+ Use `gh-glance --doctor --probe` when live capability evidence is needed. Probe
724
+ mode uses an ephemeral governor lease and admits every quota-consuming endpoint
725
+ at its exact declared cost. It includes the repository and bounded dashboard
726
+ probes, including Security priority lanes, with the exact argv, outcome, and
727
+ classification (`unavailable`, `rate-limited`, `auth-problem` or `other`). A
728
+ probe whose safe slot is later, whose resource is held, or whose budget is
729
+ unavailable is reported as `SKIPPED`; diagnostics do not bypass the reserve.
730
+ The `API governor` section then reports its measured health and resource state.
731
+ Neither mode prints scope hashes, account identity keys, state paths, lock
732
+ owners, reservation IDs, response bodies, or credentials.
506
733
 
507
734
  The `Repository access` probe shows whether the target resolves for the active
508
735
  `gh` credentials. A failed GitHub resolution response cannot distinguish a
509
736
  nonexistent, renamed, or stale target from a private repository hidden from
510
737
  that identity.
511
738
 
512
- Add `--verbose` to get a log of every `gh` call it makes alongside the report.
739
+ Add `--verbose` to probe mode to get a log of every `gh` call it makes alongside
740
+ the report. Plain doctor has no GitHub API calls to log.
513
741
 
514
742
  It exits 0 and prints a report even when `gh` is missing or you are outside a
515
743
  git repository -- those are conditions worth reporting rather than failing on --
@@ -548,30 +776,24 @@ payload never counts as one: a tab that cannot be read is not a quiet tab. One
548
776
  inactive tab is considered per wake, in rotation, so three of them falling due
549
777
  together do not start together.
550
778
 
551
- The resources are independent. Actions and Security spend REST `core` calls;
552
- Issues and Pull Requests are sorted with `--search`, which routes them through
553
- GraphQL. The following table is the conservative demand before shared pacing,
554
- not a promise that the governor will start every listed request. At the default
555
- floor:
556
-
557
- | Visible tab | REST / hour | GraphQL / hour |
558
- |---|---|---|
559
- | Actions | ~192 quiet, up to ~792 | ~120 |
560
- | Issues or Pull Requests | ~102 | ~300 quiet, up to ~1,500 |
561
- | Security | ~390 quiet, up to ~4,350 | ~120 |
562
-
563
- The lower figure in each range is every tab at its quiet cadence; the upper is
564
- the active tab at its floor with Actions busy. `--doctor` prints the same range
565
- for the configuration you actually run, labelled `projected demand` to keep it
566
- distinct from the charges the governor section reports.
567
-
568
- Security is the most expensive. A repository whose newest alert pages are not
569
- full uses the three base endpoint calls and lands near 2,280 REST requests per
570
- hour. A full page activates bounded critical/high lanes for Dependabot and code
571
- scanning, raising the safe projection to about 4,440. Actions is not cheap
572
- either: one `gh run list` issues two REST requests. `gh-glance --doctor` reports
573
- this projection beside the server's actual REST and GraphQL budgets. Enterprise
574
- ceilings can differ from 5,000.
779
+ The resources are independent. Actions and Security use REST `core`; Issues
780
+ and Pull Requests use explicit GraphQL documents. The governor's declared costs
781
+ are conservative admission bounds. They are not presented as fixed hourly
782
+ charges because pagination, conditional REST 304 responses, quiet cadence,
783
+ workflow-name fallback, capability backoff, and shared followers all change
784
+ the actual result.
785
+
786
+ Plain `--doctor` reports reconciled local acquisition totals: HTTP attempts,
787
+ failed outcomes, REST 200/304 outcomes, proven actual core and GraphQL units,
788
+ conservative outstanding units, and measured follower queue delay. Probe mode
789
+ adds live governor evidence. These measured
790
+ figures stay separate from the policy-derived `projected demand` range.
791
+
792
+ Security can require the most REST work: each source starts with one newest
793
+ page, and full pages can activate bounded critical/high lanes for Dependabot
794
+ and code scanning. Actions normally requests its runs endpoint and admits a
795
+ separate conditional workflow-catalog request only when displayed runs lack a
796
+ known workflow name. Enterprise ceilings can differ from GitHub.com's.
575
797
 
576
798
  For each resource, gh-glance calculates 20% of the reported limit for other work:
577
799
 
@@ -590,11 +812,12 @@ clean probe can account for it. Missing, stale, corrupt, locked, or unwritable
590
812
  coordination denies the call instead of returning to five-second polling.
591
813
 
592
814
  Core observations come from real response headers, so the core reserve has a
593
- closed feedback loop. GraphQL is currently weaker: `gh issue` and `gh pr` do
594
- not expose response headers in their normal output, and the free `rate_limit`
595
- endpoint can remain unchanged while real GraphQL use rises. gh-glance therefore
596
- paces its declared local GraphQL work and fails closed on a missing or stale
597
- probe, but it cannot yet prove the account-wide GraphQL reserve.
815
+ closed feedback loop. GraphQL uses explicit bounded queries whose envelopes
816
+ report actual cost and counter evidence. One shared, separately accounted
817
+ GraphQL observer establishes spendable capacity; the free `rate_limit` endpoint
818
+ is diagnostic only and can never authorize or refund work. Missing or stale
819
+ authoritative evidence fails closed rather than falling back to an inferred
820
+ counter.
598
821
 
599
822
  One pane owns the free `rate_limit` probe for a control window and publishes it
600
823
  for the others. Manual and diagnostic work is considered before tab-switch,
@@ -620,17 +843,44 @@ age remains explicit through the `stale` label, and the rate-limit banner still
620
843
  describes the current failed request; cached data never turns a failure into a
621
844
  false success.
622
845
 
623
- The enforceable boundary is local admission from fresh, conservatively debited
624
- evidence. Another program can spend after the probe, and panes on another
625
- machine or in a different local account scope cannot share this file. GitHub
626
- does not offer an atomic global quota reservation. The token-wide counter still
627
- lets gh-glance measure external use and reduce future capacity, but the hard
628
- reserve is not a claim that unrelated consumers can never cross it.
846
+ The enforceable boundary is admission from fresh, conservatively debited
847
+ evidence. Another program can spend after the probe, and standalone panes in a
848
+ different local account scope cannot share the same coordination file. Optional
849
+ SSH clients can instead share the collector machine's one acquisition and quota
850
+ scope while making zero client-side GitHub requests. GitHub does not offer an
851
+ atomic global quota reservation. The account-wide counter still lets gh-glance
852
+ measure external use and reduce future capacity, but the hard reserve is not a
853
+ claim that unrelated consumers can never cross it.
629
854
 
630
855
  `GH_GLANCE_REFRESH=30` sets a wider floor for every pane in a shell; `--refresh`
631
856
  still wins per pane. A single pane on a healthy budget normally stays at its
632
857
  floor and shows Watching between checks.
633
858
 
859
+ ### Efficiency verification
860
+
861
+ The repository includes an offline sustained-workload gate for the coordination
862
+ contracts described above:
863
+
864
+ ```sh
865
+ npm run test:efficiency
866
+ npm run measure:efficiency
867
+ ```
868
+
869
+ The deterministic test advances an injected clock through one simulated hour
870
+ while using the production acquisition and governor machinery. It covers one,
871
+ two, seven, and ten panes, duplicate and distinct repositories, and a 3+4 remote
872
+ client split, with running and quiet CI, conditional responses, external spend,
873
+ primary reset, secondary hold, producer loss, and an account switch. The fake
874
+ SSH route invokes the real bridge and collector; no real GitHub account, App,
875
+ webhook, or second computer is used.
876
+
877
+ The measurement command reports operation-level HTTP outcomes, proven and
878
+ uncertain quota, observer work, coalescing, queue delay, source-to-display
879
+ percentiles, subprocesses, wall time, CPU, and RSS. A percentage is shown only
880
+ when the candidate and baseline used a compatible workload, machine, platform,
881
+ and runtime. Missing or incompatible baseline values stay labelled as such;
882
+ fixture performance is not presented as live GitHub performance.
883
+
634
884
  ## Limitations
635
885
 
636
886
  - **Security tab**: code scanning and secret scanning alerts require [GitHub
@@ -720,23 +970,23 @@ GH_GLANCE_ICONS=ascii gh-glance
720
970
  | `No GitHub remote found` | Press `Enter` to start `gh repo create`, then choose **Push an existing local repository**. Or press `q` and run `gh-glance --repo owner/name` to watch an existing repository without attaching this folder. |
721
971
  | `GitHub login or authorization required` | Run `gh auth status`. With no account, run `gh auth login`; with an expired authorization, run `gh auth refresh`; then press `R`, which also clears the endpoint backoff the failures accrued. |
722
972
  | `Repository not found or inaccessible to the active gh account` | The active identity cannot resolve the target. Check `gh auth status`, `git remote -v` or the explicit `--repo`, and use `gh auth switch` only if the wrong account is active. |
723
- | `GraphQL: Could not resolve to a Repository...` in older gh-glance versions | The response has the same ambiguity: a missing or renamed target, or a private repository not visible to the active account. Run `gh-glance --doctor`. |
724
- | Actions says `not available for this repository` while Repository access is `ok` | The repository resolved, but that endpoint is unavailable; inspect the corresponding doctor block. |
973
+ | `GraphQL: Could not resolve to a Repository...` in older gh-glance versions | The response has the same ambiguity: a missing or renamed target, or a private repository not visible to the active account. Run `gh-glance --doctor --probe`. |
974
+ | Actions says `not available for this repository` while Repository access is `ok` | The repository resolved, but that endpoint is unavailable; inspect the corresponding `--doctor --probe` block. |
725
975
  | Row icons are blank boxes | Your terminal font is not a Nerd Font. Use `GH_GLANCE_ICONS=unicode`. |
726
976
  | Security tab shows a "not enabled" note | Code scanning and secret scanning need GitHub Advanced Security. Dependabot alerts work independently. The note now appears only when the feature genuinely is unavailable: auth, SSO and network failures show the real error instead. |
727
977
  | `none of the git remotes ... point to a known GitHub host` | `gh` is not authenticated for that host. Run `gh auth login --hostname <host>`. |
728
978
  | Security tab empty on an enterprise host in an older gh-glance version | Older versions could route alert endpoints separately. Current gh-glance routes every API call and the governor to one effective host; use `--repo host/owner/name`, `GH_HOST`, or a qualified `GH_REPO`, then confirm the resolved host with `--doctor`. |
729
- | Tabs start failing after working for a while | The enterprise SAML session lapsed. Re-authorize in the browser; the dashboard recovers within about 30 seconds. Run `gh-glance --doctor` to confirm. |
979
+ | Tabs start failing after working for a while | The enterprise SAML session lapsed. Re-authorize in the browser; the dashboard recovers within about 30 seconds. Run `gh-glance --doctor --probe` to confirm. |
730
980
  | A tab's count is red | That tab's last fetch failed. The error itself is shown when you switch to it, translated into what to do about it where `gh-glance` recognises the failure. |
731
- | Security tab shows `?` instead of a number | The alert endpoints could not be read at all -- an expired SAML session, a token without `security_events`, or an org OAuth restriction. `?` means "unknown", not "zero"; run `gh-glance --doctor` to see which probe failed and how it was classified. |
981
+ | Security tab shows `?` instead of a number | The alert endpoints could not be read at all -- an expired SAML session, a token without `security_events`, or an org OAuth restriction. `?` means "unknown", not "zero"; run `gh-glance --doctor --probe` to see which probe failed and how it was classified. |
732
982
  | `Watching` with `next 2m` | The active tab has a shared budget probe or safe grant scheduled. The interval names this grant only; it is not a recurring polling interval. Pressing `r` raises safe priority but cannot bypass the lane or reserve. |
733
- | `Watching` with `sharing 4` | Four local panes share this account governor, and another pane owns the lane immediately ahead of this grant. This is pacing, not quota scarcity. |
983
+ | `Shared` | This pane is using a generation acquired for matching local subscribers. This is coalescing, not quota scarcity. |
734
984
  | `Paused` with a reset time | The active tab's REST or GraphQL resource is at its reserve, exhausted, or under a shared rate-limit block. Wait for the stated reset/probe. Other tabs can continue when they use the healthy resource. |
735
985
  | `Paused` without a reset time | Budget or coordinator evidence is unknown, corrupt, locked, or unwritable. No data call is started. Run `gh-glance --doctor`; also check the config directory permissions and whether another live process owns its private lock. |
736
986
  | A failing tab seems to have stopped retrying | Recognized endpoint failures back off rather than re-spawning `gh` at the floor. Press `r` to request a higher-priority retry, or `R` to also clear the endpoint backoff; either way the retry waits for a safe grant, so the tab remains Watching or Paused. |
737
987
  | `unknown argument: -v` | `-v` used to mean `--version` and no longer does, because this CLI also has `--verbose`. Use `--version` or `--verbose` explicitly. |
738
- | Cached rows plus `Paused` and `stale 2m` | The rows came from the separate last-known-good dashboard cache, while the live request is blocked or unsafe. Stale age is not extended by a pause. The error and footer describe current coordination; cached data never means the live check succeeded. |
739
- | Repeated GitHub rate-limit messages | A classified rate-limit response is published as one shared resource block. Local panes make no data retry before that block's probe/reset deadline. Use `--doctor` to inspect the resource and reset; repeated manual refresh cannot override it. |
988
+ | Cached rows plus `Paused`, `Stale`, or `Disconnected` | The rows are last-known-good data while the live source is blocked, overdue, or unavailable. A cache read never advances source success. The footer never calls stale or disconnected rows `Watching`. |
989
+ | Repeated GitHub rate-limit messages | A classified rate-limit response is published as one shared resource block. Local panes make no data retry before that block's probe/reset deadline. Use `--doctor --probe` to inspect the live resource and reset; repeated manual refresh cannot override it. |
740
990
  | It exits immediately when piped | Intentional. It is a full-screen dashboard, not a reporting command. |
741
991
  | `Restart required: close older gh-glance panes` | A pane from an older release still holds a live lease in the previous coordination format, which this version cannot join. Quit those panes; this one resumes on its own. Nothing is killed for you, and no state needs deleting. |
742
992
  | `Upgrade waiting for legacy quota reset` | The older panes are gone, but left spend that cannot be proven settled. It waits for the affected rate-limit window to reset rather than assume the quota is free. This one has a deadline and clears itself. |