@humanbased/crosscheck 1.2.0-beta.2 → 1.2.0-beta.33
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/crosscheck.config.example.yml +42 -0
- package/dist/__tests__/can-write-verdict.test.d.ts +2 -0
- package/dist/__tests__/can-write-verdict.test.d.ts.map +1 -0
- package/dist/__tests__/can-write-verdict.test.js +31 -0
- package/dist/__tests__/can-write-verdict.test.js.map +1 -0
- package/dist/__tests__/linear-client.test.d.ts +2 -0
- package/dist/__tests__/linear-client.test.d.ts.map +1 -0
- package/dist/__tests__/linear-client.test.js +120 -0
- package/dist/__tests__/linear-client.test.js.map +1 -0
- package/dist/__tests__/linear-comment.test.d.ts +2 -0
- package/dist/__tests__/linear-comment.test.d.ts.map +1 -0
- package/dist/__tests__/linear-comment.test.js +151 -0
- package/dist/__tests__/linear-comment.test.js.map +1 -0
- package/dist/__tests__/linear-identity.test.d.ts +2 -0
- package/dist/__tests__/linear-identity.test.d.ts.map +1 -0
- package/dist/__tests__/linear-identity.test.js +253 -0
- package/dist/__tests__/linear-identity.test.js.map +1 -0
- package/dist/__tests__/linear-notify.test.d.ts +2 -0
- package/dist/__tests__/linear-notify.test.d.ts.map +1 -0
- package/dist/__tests__/linear-notify.test.js +144 -0
- package/dist/__tests__/linear-notify.test.js.map +1 -0
- package/dist/__tests__/linear-ref.test.d.ts +2 -0
- package/dist/__tests__/linear-ref.test.d.ts.map +1 -0
- package/dist/__tests__/linear-ref.test.js +261 -0
- package/dist/__tests__/linear-ref.test.js.map +1 -0
- package/dist/__tests__/linear-verify.test.d.ts +2 -0
- package/dist/__tests__/linear-verify.test.d.ts.map +1 -0
- package/dist/__tests__/linear-verify.test.js +113 -0
- package/dist/__tests__/linear-verify.test.js.map +1 -0
- package/dist/__tests__/linear-worker.test.d.ts +2 -0
- package/dist/__tests__/linear-worker.test.d.ts.map +1 -0
- package/dist/__tests__/linear-worker.test.js +83 -0
- package/dist/__tests__/linear-worker.test.js.map +1 -0
- package/dist/__tests__/linear-write-possible.test.d.ts +2 -0
- package/dist/__tests__/linear-write-possible.test.d.ts.map +1 -0
- package/dist/__tests__/linear-write-possible.test.js +30 -0
- package/dist/__tests__/linear-write-possible.test.js.map +1 -0
- package/dist/__tests__/optimize.test.js +1 -0
- package/dist/__tests__/optimize.test.js.map +1 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +48 -1
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +23 -3
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +40 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +52 -6
- package/dist/commands/watch.js.map +1 -1
- package/dist/config/loader.d.ts +3 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +13 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/config/schema.d.ts +208 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +39 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/issues/ticket-ref.d.ts.map +1 -1
- package/dist/issues/ticket-ref.js +6 -5
- package/dist/issues/ticket-ref.js.map +1 -1
- package/dist/lib/runner.d.ts +2 -0
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/runner.js +58 -1
- package/dist/lib/runner.js.map +1 -1
- package/dist/lib/workflow.d.ts +9 -0
- package/dist/lib/workflow.d.ts.map +1 -1
- package/dist/lib/workflow.js +20 -0
- package/dist/lib/workflow.js.map +1 -1
- package/dist/linear/client.d.ts +18 -0
- package/dist/linear/client.d.ts.map +1 -0
- package/dist/linear/client.js +67 -0
- package/dist/linear/client.js.map +1 -0
- package/dist/linear/comment.d.ts +20 -0
- package/dist/linear/comment.d.ts.map +1 -0
- package/dist/linear/comment.js +53 -0
- package/dist/linear/comment.js.map +1 -0
- package/dist/linear/identity.d.ts +47 -0
- package/dist/linear/identity.d.ts.map +1 -0
- package/dist/linear/identity.js +151 -0
- package/dist/linear/identity.js.map +1 -0
- package/dist/linear/notify.d.ts +35 -0
- package/dist/linear/notify.d.ts.map +1 -0
- package/dist/linear/notify.js +65 -0
- package/dist/linear/notify.js.map +1 -0
- package/dist/linear/ref.d.ts +13 -0
- package/dist/linear/ref.d.ts.map +1 -0
- package/dist/linear/ref.js +90 -0
- package/dist/linear/ref.js.map +1 -0
- package/dist/linear/verify.d.ts +21 -0
- package/dist/linear/verify.d.ts.map +1 -0
- package/dist/linear/verify.js +65 -0
- package/dist/linear/verify.js.map +1 -0
- package/docs/linear-identity-contract.md +133 -0
- package/docs/linear-identity.md +224 -0
- package/get-started.md +39 -0
- package/package.json +1 -1
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Linear identity setup for your workspace
|
|
2
|
+
|
|
3
|
+
crosscheck can mirror a review verdict onto the Linear issue a PR belongs to. This
|
|
4
|
+
guide gets those writes attributed to *crosscheck* rather than to whoever's API key
|
|
5
|
+
the daemon happens to hold.
|
|
6
|
+
|
|
7
|
+
It assumes no prior Linear API experience. Budget about five minutes.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why this exists
|
|
12
|
+
|
|
13
|
+
By default an agent writing to Linear uses whatever API key the operator supplied —
|
|
14
|
+
usually a person's personal key. Every comment then looks like that person wrote it,
|
|
15
|
+
and there is no way to tell agent activity from human activity.
|
|
16
|
+
|
|
17
|
+
There are two tiers you can adopt, in increasing order of strength.
|
|
18
|
+
|
|
19
|
+
| Tier | Mode | Setup | How writes appear |
|
|
20
|
+
|---|---|---|---|
|
|
21
|
+
| **T0** | `api_key` | none beyond a key | Your account, with a `🤖 crosscheck · crosscheck` signature line |
|
|
22
|
+
| **T1** | `client_credentials` | one OAuth app, ~5 min | The app itself (botActor), via `createAsUser` |
|
|
23
|
+
|
|
24
|
+
T0 works everywhere and is the default. T1 is recommended — it is the only tier where
|
|
25
|
+
Linear itself, not just a text convention, distinguishes the writer.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## T0 — api_key (zero setup)
|
|
30
|
+
|
|
31
|
+
Create a personal API key under **Linear → Settings → API → Personal API keys**, then:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
export LINEAR_API_KEY=lin_api_...
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```yaml
|
|
38
|
+
linear:
|
|
39
|
+
enabled: true
|
|
40
|
+
auth:
|
|
41
|
+
mode: api_key
|
|
42
|
+
team_keys:
|
|
43
|
+
- IN # your team's key prefix
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Every write leads with `🤖 crosscheck · crosscheck`. Linear still records your account
|
|
47
|
+
as the author — the signature is a convention, not an identity.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## T1 — client_credentials (recommended)
|
|
52
|
+
|
|
53
|
+
### 1. Create an OAuth application
|
|
54
|
+
|
|
55
|
+
Go to **Linear → Settings → API → OAuth applications → Create new**.
|
|
56
|
+
|
|
57
|
+
Fill in a name (`crosscheck`, or whatever you want to see on comments) and an icon.
|
|
58
|
+
|
|
59
|
+
> **Gotcha:** the form **requires a Redirect URI** even though client credentials never
|
|
60
|
+
> uses one. Any placeholder URL on a domain you control is fine —
|
|
61
|
+
> `https://example.com/callback`. It is never called.
|
|
62
|
+
|
|
63
|
+
### 2. Enable the client credentials toggle
|
|
64
|
+
|
|
65
|
+
On the application page, enable **Client credentials**. Without this the token mint in
|
|
66
|
+
step 5 returns `invalid_client`.
|
|
67
|
+
|
|
68
|
+
Also authorize the app for your workspace with **app-actor** ("application acts as
|
|
69
|
+
itself") so writes render as the app.
|
|
70
|
+
|
|
71
|
+
### 3. Choose scopes
|
|
72
|
+
|
|
73
|
+
`read,write` covers issues and comments — that is all crosscheck needs.
|
|
74
|
+
|
|
75
|
+
> **Gotcha:** `read,write` does **not** cover initiatives. `initiative:read` and
|
|
76
|
+
> `initiative:write` are separate scopes. Add them only if something else in your
|
|
77
|
+
> workspace needs them. Skip the admin scope.
|
|
78
|
+
|
|
79
|
+
### 4. Store the credentials
|
|
80
|
+
|
|
81
|
+
Copy the client ID and secret into your environment — never into the config file:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
export LINEAR_CLIENT_ID=...
|
|
85
|
+
export LINEAR_CLIENT_SECRET=...
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 5. Verify the mint by hand
|
|
89
|
+
|
|
90
|
+
Confirm the credentials work before wiring crosscheck up. The form body goes through
|
|
91
|
+
**stdin**, so the secret never lands in your shell history or in `ps` output:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
printf 'grant_type=client_credentials&client_id=%s&client_secret=%s&scope=read,write' "$LINEAR_CLIENT_ID" "$LINEAR_CLIENT_SECRET" | curl -s -X POST https://api.linear.app/oauth/token -H 'Content-Type: application/x-www-form-urlencoded' --data @-
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
A successful response contains `access_token`. If you get `invalid_client`, revisit
|
|
98
|
+
step 2 — the toggle is the usual cause.
|
|
99
|
+
|
|
100
|
+
### 6. Configure crosscheck
|
|
101
|
+
|
|
102
|
+
```yaml
|
|
103
|
+
linear:
|
|
104
|
+
enabled: true
|
|
105
|
+
auth:
|
|
106
|
+
mode: client_credentials
|
|
107
|
+
client_id_env: LINEAR_CLIENT_ID
|
|
108
|
+
client_secret_env: LINEAR_CLIENT_SECRET
|
|
109
|
+
scopes: "read,write"
|
|
110
|
+
identity:
|
|
111
|
+
actor: crosscheck
|
|
112
|
+
comment_on:
|
|
113
|
+
- APPROVE
|
|
114
|
+
- NEEDS_WORK
|
|
115
|
+
- BLOCK
|
|
116
|
+
team_keys:
|
|
117
|
+
- IN
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Run a review against a PR whose branch or body references a Linear issue. The comment
|
|
121
|
+
should appear authored by the app, not by you.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Verifying which identity you're on
|
|
126
|
+
|
|
127
|
+
`crosscheck status` shows a **Linear** section whenever `linear.enabled` is true. It
|
|
128
|
+
resolves the configured identity for real — minting a T1 token if that's the mode —
|
|
129
|
+
and reports what a write would render as:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
Linear
|
|
133
|
+
✓ auth mode client_credentials
|
|
134
|
+
organization Inductive Network
|
|
135
|
+
✓ writes as crosscheck/<step> (app actor)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
On T0 it names the human account instead, because that's the state worth seeing:
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
Linear
|
|
142
|
+
✓ auth mode api_key
|
|
143
|
+
organization Inductive Network
|
|
144
|
+
✗ writes as yi@example.com (human — switch to client_credentials)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Run this before and after a cutover. The `✗` is the condition to eliminate.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Deploying with an existing OAuth app
|
|
152
|
+
|
|
153
|
+
If your organization already operates a gateway app, point the daemon at its
|
|
154
|
+
credentials rather than creating a second app. Only the env var *names* go in config:
|
|
155
|
+
|
|
156
|
+
```yaml
|
|
157
|
+
linear:
|
|
158
|
+
enabled: true
|
|
159
|
+
auth:
|
|
160
|
+
mode: client_credentials
|
|
161
|
+
client_id_env: LINEAR_HB_AGENT_GATEWAY_CLIENT_ID
|
|
162
|
+
client_secret_env: LINEAR_HB_AGENT_GATEWAY_CLIENT_SECRET
|
|
163
|
+
identity:
|
|
164
|
+
actor: crosscheck
|
|
165
|
+
per_step_actor: true
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Cutover sequence:
|
|
169
|
+
|
|
170
|
+
1. Make the gateway credentials available to the **daemon's** environment — not just
|
|
171
|
+
your interactive shell. A systemd unit or launchd plist needs them explicitly.
|
|
172
|
+
2. Run `crosscheck status` and confirm `writes as ... (app actor)`.
|
|
173
|
+
3. Run one review end to end and confirm the Linear comment is authored by the app.
|
|
174
|
+
4. Only then retire the old personal API key from the daemon's environment.
|
|
175
|
+
|
|
176
|
+
Step 4 last, deliberately: until steps 2 and 3 pass, the old key is your rollback.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## How crosscheck finds the issue
|
|
181
|
+
|
|
182
|
+
Checked in order — branch name, then PR title, then PR body. Within each, an explicit
|
|
183
|
+
URL wins over a bare identifier.
|
|
184
|
+
|
|
185
|
+
1. **A `linear.app` issue URL** — `https://linear.app/acme/issue/IN-2269/slug`. Works
|
|
186
|
+
with no configuration, because it is unambiguous.
|
|
187
|
+
2. **A bare identifier** — `IN-2269`, matched case-insensitively so a branch like
|
|
188
|
+
`feat/in-2269-thing` resolves. **Only for keys listed in `team_keys`.**
|
|
189
|
+
|
|
190
|
+
That second restriction is deliberate. `UTF-8`, `SHA-256`, `ISO-8601`, `GPT-5` and
|
|
191
|
+
`RFC-2119` all have the same shape as a Linear identifier. Reading the wrong issue is
|
|
192
|
+
harmless; *commenting* on the wrong issue is not. So bare matching stays off until you
|
|
193
|
+
name your team keys.
|
|
194
|
+
|
|
195
|
+
If no issue is found, crosscheck skips the Linear write and the review proceeds
|
|
196
|
+
normally.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Security properties
|
|
201
|
+
|
|
202
|
+
- **Secrets never reach argv.** The mint sends them in a POST body; the GraphQL token
|
|
203
|
+
rides in a header. Nothing shows up in `ps`.
|
|
204
|
+
- **Secrets never reach logs or error traces.** Failure messages carry HTTP status
|
|
205
|
+
codes, not credentials.
|
|
206
|
+
- **Tokens are minted per run** and held in memory only. Linear app tokens have a ~30
|
|
207
|
+
day TTL, but crosscheck treats them as ephemeral.
|
|
208
|
+
- **A failed T1 mint aborts the run.** crosscheck will not fall back to `api_key` when
|
|
209
|
+
you configured `client_credentials` — a silent downgrade would put agent writes back
|
|
210
|
+
under a human's name, which is the exact failure this feature exists to prevent. You
|
|
211
|
+
get a non-zero exit and a message naming the env var to fix.
|
|
212
|
+
- **Config holds env var *names*, never values.**
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Troubleshooting
|
|
217
|
+
|
|
218
|
+
| Symptom | Cause |
|
|
219
|
+
|---|---|
|
|
220
|
+
| `LINEAR_CLIENT_ID and LINEAR_CLIENT_SECRET is not set` | Env vars missing from the daemon's environment (not just your shell) |
|
|
221
|
+
| `token mint rejected (HTTP 401)` | Client credentials toggle off, or wrong secret |
|
|
222
|
+
| `Linear API error: Access denied` | Scope too narrow — needs `write` |
|
|
223
|
+
| Comment never appears, no error | No issue ref found. Set `team_keys`, or put a `linear.app` URL in the PR body |
|
|
224
|
+
| Comment appears as you, not the app | Still on `mode: api_key` |
|
package/get-started.md
CHANGED
|
@@ -156,6 +156,27 @@ export LINEAR_API_KEY=lin_api_...
|
|
|
156
156
|
If it's unset while enrichment is on, crosscheck just skips enrichment and
|
|
157
157
|
reviews the diff as usual — it never errors.
|
|
158
158
|
|
|
159
|
+
### Linear identity — for writing back to Linear (optional)
|
|
160
|
+
|
|
161
|
+
Separate from enrichment above, which only *reads*. When `linear.enabled: true`,
|
|
162
|
+
crosscheck mirrors each review verdict onto the PR's Linear issue.
|
|
163
|
+
|
|
164
|
+
Two tiers. `api_key` (T0) reuses `LINEAR_API_KEY` and tags writes with a
|
|
165
|
+
`🤖 crosscheck · crosscheck` signature line, but Linear still attributes them to
|
|
166
|
+
your account. `client_credentials` (T1) uses an OAuth app in your workspace so
|
|
167
|
+
writes render as the app itself:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
export LINEAR_CLIENT_ID=...
|
|
171
|
+
export LINEAR_CLIENT_SECRET=...
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
A failed T1 token mint aborts the run rather than falling back to `api_key` —
|
|
175
|
+
a silent downgrade would put agent writes back under a human's name.
|
|
176
|
+
|
|
177
|
+
Full walkthrough, including the two Linear UI gotchas that trip people up:
|
|
178
|
+
[docs/linear-identity.md](docs/linear-identity.md).
|
|
179
|
+
|
|
159
180
|
---
|
|
160
181
|
|
|
161
182
|
## Step 1 — Check your setup
|
|
@@ -1144,6 +1165,24 @@ post_review:
|
|
|
1144
1165
|
server:
|
|
1145
1166
|
port: 7891
|
|
1146
1167
|
webhook_path: /webhook
|
|
1168
|
+
|
|
1169
|
+
linear: # write review verdicts back to a Linear issue (opt-in)
|
|
1170
|
+
enabled: false
|
|
1171
|
+
auth:
|
|
1172
|
+
mode: api_key # api_key | client_credentials
|
|
1173
|
+
api_key_env: LINEAR_API_KEY
|
|
1174
|
+
client_id_env: LINEAR_CLIENT_ID
|
|
1175
|
+
client_secret_env: LINEAR_CLIENT_SECRET
|
|
1176
|
+
scopes: "read,write" # comma-separated; initiative:* are separate scopes
|
|
1177
|
+
identity:
|
|
1178
|
+
actor: crosscheck
|
|
1179
|
+
signature: "🤖 {actor} · {product}"
|
|
1180
|
+
per_step_actor: true # crosscheck/review vs crosscheck/fix in Linear
|
|
1181
|
+
comment_on: # verdicts mirrored to the issue
|
|
1182
|
+
- APPROVE
|
|
1183
|
+
- NEEDS_WORK
|
|
1184
|
+
- BLOCK
|
|
1185
|
+
team_keys: [] # e.g. [IN] — required to match bare refs like IN-42
|
|
1147
1186
|
```
|
|
1148
1187
|
|
|
1149
1188
|
### Quality tiers
|