@humanbased/crosscheck 1.1.1-beta.0 → 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.
Files changed (102) hide show
  1. package/crosscheck.config.example.yml +42 -0
  2. package/dist/__tests__/can-write-verdict.test.d.ts +2 -0
  3. package/dist/__tests__/can-write-verdict.test.d.ts.map +1 -0
  4. package/dist/__tests__/can-write-verdict.test.js +31 -0
  5. package/dist/__tests__/can-write-verdict.test.js.map +1 -0
  6. package/dist/__tests__/linear-client.test.d.ts +2 -0
  7. package/dist/__tests__/linear-client.test.d.ts.map +1 -0
  8. package/dist/__tests__/linear-client.test.js +120 -0
  9. package/dist/__tests__/linear-client.test.js.map +1 -0
  10. package/dist/__tests__/linear-comment.test.d.ts +2 -0
  11. package/dist/__tests__/linear-comment.test.d.ts.map +1 -0
  12. package/dist/__tests__/linear-comment.test.js +151 -0
  13. package/dist/__tests__/linear-comment.test.js.map +1 -0
  14. package/dist/__tests__/linear-identity.test.d.ts +2 -0
  15. package/dist/__tests__/linear-identity.test.d.ts.map +1 -0
  16. package/dist/__tests__/linear-identity.test.js +253 -0
  17. package/dist/__tests__/linear-identity.test.js.map +1 -0
  18. package/dist/__tests__/linear-notify.test.d.ts +2 -0
  19. package/dist/__tests__/linear-notify.test.d.ts.map +1 -0
  20. package/dist/__tests__/linear-notify.test.js +144 -0
  21. package/dist/__tests__/linear-notify.test.js.map +1 -0
  22. package/dist/__tests__/linear-ref.test.d.ts +2 -0
  23. package/dist/__tests__/linear-ref.test.d.ts.map +1 -0
  24. package/dist/__tests__/linear-ref.test.js +261 -0
  25. package/dist/__tests__/linear-ref.test.js.map +1 -0
  26. package/dist/__tests__/linear-verify.test.d.ts +2 -0
  27. package/dist/__tests__/linear-verify.test.d.ts.map +1 -0
  28. package/dist/__tests__/linear-verify.test.js +113 -0
  29. package/dist/__tests__/linear-verify.test.js.map +1 -0
  30. package/dist/__tests__/linear-worker.test.d.ts +2 -0
  31. package/dist/__tests__/linear-worker.test.d.ts.map +1 -0
  32. package/dist/__tests__/linear-worker.test.js +83 -0
  33. package/dist/__tests__/linear-worker.test.js.map +1 -0
  34. package/dist/__tests__/linear-write-possible.test.d.ts +2 -0
  35. package/dist/__tests__/linear-write-possible.test.d.ts.map +1 -0
  36. package/dist/__tests__/linear-write-possible.test.js +30 -0
  37. package/dist/__tests__/linear-write-possible.test.js.map +1 -0
  38. package/dist/__tests__/optimize.test.js +1 -0
  39. package/dist/__tests__/optimize.test.js.map +1 -1
  40. package/dist/commands/review.d.ts.map +1 -1
  41. package/dist/commands/review.js +49 -1
  42. package/dist/commands/review.js.map +1 -1
  43. package/dist/commands/run.d.ts.map +1 -1
  44. package/dist/commands/run.js +23 -3
  45. package/dist/commands/run.js.map +1 -1
  46. package/dist/commands/status.d.ts.map +1 -1
  47. package/dist/commands/status.js +40 -1
  48. package/dist/commands/status.js.map +1 -1
  49. package/dist/commands/watch.d.ts.map +1 -1
  50. package/dist/commands/watch.js +52 -6
  51. package/dist/commands/watch.js.map +1 -1
  52. package/dist/config/loader.d.ts +3 -1
  53. package/dist/config/loader.d.ts.map +1 -1
  54. package/dist/config/loader.js +13 -0
  55. package/dist/config/loader.js.map +1 -1
  56. package/dist/config/schema.d.ts +208 -0
  57. package/dist/config/schema.d.ts.map +1 -1
  58. package/dist/config/schema.js +39 -0
  59. package/dist/config/schema.js.map +1 -1
  60. package/dist/issues/ticket-ref.d.ts.map +1 -1
  61. package/dist/issues/ticket-ref.js +6 -5
  62. package/dist/issues/ticket-ref.js.map +1 -1
  63. package/dist/lib/clone.d.ts +1 -0
  64. package/dist/lib/clone.d.ts.map +1 -1
  65. package/dist/lib/clone.js +28 -10
  66. package/dist/lib/clone.js.map +1 -1
  67. package/dist/lib/runner.d.ts +2 -0
  68. package/dist/lib/runner.d.ts.map +1 -1
  69. package/dist/lib/runner.js +58 -1
  70. package/dist/lib/runner.js.map +1 -1
  71. package/dist/lib/workflow.d.ts +9 -0
  72. package/dist/lib/workflow.d.ts.map +1 -1
  73. package/dist/lib/workflow.js +20 -0
  74. package/dist/lib/workflow.js.map +1 -1
  75. package/dist/linear/client.d.ts +18 -0
  76. package/dist/linear/client.d.ts.map +1 -0
  77. package/dist/linear/client.js +67 -0
  78. package/dist/linear/client.js.map +1 -0
  79. package/dist/linear/comment.d.ts +20 -0
  80. package/dist/linear/comment.d.ts.map +1 -0
  81. package/dist/linear/comment.js +53 -0
  82. package/dist/linear/comment.js.map +1 -0
  83. package/dist/linear/identity.d.ts +47 -0
  84. package/dist/linear/identity.d.ts.map +1 -0
  85. package/dist/linear/identity.js +151 -0
  86. package/dist/linear/identity.js.map +1 -0
  87. package/dist/linear/notify.d.ts +35 -0
  88. package/dist/linear/notify.d.ts.map +1 -0
  89. package/dist/linear/notify.js +65 -0
  90. package/dist/linear/notify.js.map +1 -0
  91. package/dist/linear/ref.d.ts +13 -0
  92. package/dist/linear/ref.d.ts.map +1 -0
  93. package/dist/linear/ref.js +90 -0
  94. package/dist/linear/ref.js.map +1 -0
  95. package/dist/linear/verify.d.ts +21 -0
  96. package/dist/linear/verify.d.ts.map +1 -0
  97. package/dist/linear/verify.js +65 -0
  98. package/dist/linear/verify.js.map +1 -0
  99. package/docs/linear-identity-contract.md +133 -0
  100. package/docs/linear-identity.md +224 -0
  101. package/get-started.md +39 -0
  102. package/package.json +1 -1
@@ -0,0 +1,133 @@
1
+ # Linear identity: the shared contract
2
+
3
+ crosscheck and symphony are separate open-source products that both write to Linear.
4
+ This document is the contract they implement identically, so an operator configures
5
+ identity once and it means the same thing in both — and so a third product can adopt
6
+ it without inventing a fourth shape.
7
+
8
+ Humanbased's HB Agent Gateway is not special here. It is one `client_credentials`
9
+ configuration of this contract.
10
+
11
+ For step-by-step setup, see [linear-identity.md](linear-identity.md). This document is
12
+ the specification.
13
+
14
+ ---
15
+
16
+ ## Config shape
17
+
18
+ ```yaml
19
+ linear:
20
+ enabled: false # opt-in; default off
21
+ auth:
22
+ mode: api_key # api_key | client_credentials
23
+ api_key_env: LINEAR_API_KEY
24
+ client_id_env: LINEAR_CLIENT_ID
25
+ client_secret_env: LINEAR_CLIENT_SECRET
26
+ scopes: "read write"
27
+ identity:
28
+ actor: crosscheck # product name; symphony uses `symphony`
29
+ signature: "🤖 {actor} · {product}"
30
+ per_step_actor: true # suffix the actor with the unit of work
31
+ ```
32
+
33
+ Every key under `auth` ending in `_env` names an **environment variable**, never a
34
+ secret. A product implementing this contract must not accept an inline credential in
35
+ config. Non-credential fields (`mode`, `scopes`, everything under `identity`) are
36
+ ordinary config values and are written literally.
37
+
38
+ ---
39
+
40
+ ## Tiers
41
+
42
+ | Tier | Mode | Setup | Attribution |
43
+ |---|---|---|---|
44
+ | **T0** | `api_key` | none | Operator's account; signature line only |
45
+ | **T1** | `client_credentials` | one OAuth app | The app itself (botActor), via `createAsUser` |
46
+ | **T2** | public app | none (future) | Centrally operated app — see IN-2272 |
47
+
48
+ T0 is the default so an existing `api_key` config keeps working unchanged. T2 is not
49
+ implemented; it is gated on T1 adoption feedback.
50
+
51
+ ---
52
+
53
+ ## Required behaviours
54
+
55
+ A conforming implementation must:
56
+
57
+ 1. **Lead every write with the rendered signature.** `{actor}` and `{product}` are the
58
+ defined placeholders. This is T0's entire attribution mechanism and is retained in
59
+ T1 as fallback text.
60
+
61
+ 2. **Mint T1 tokens per run, not per write.** Linear app tokens carry roughly a 30-day
62
+ TTL; treat them as ephemeral anyway. Resolve identity at run start so a
63
+ misconfiguration fails before any expensive work.
64
+
65
+ 3. **Abort when a configured T1 mint fails.** Never fall back to `api_key`. A silent
66
+ downgrade re-attributes agent writes to a human, which is the failure the whole
67
+ contract exists to prevent. Surface the env var name that needs fixing.
68
+
69
+ 4. **Keep secrets off argv.** The mint sends credentials in a POST body; the token
70
+ travels in an `Authorization` header. Nothing reaches a process list. Failure
71
+ messages carry HTTP status codes, never credential material.
72
+
73
+ 5. **Preserve backward compatibility.** An existing `api_key` configuration must keep
74
+ working with T0 semantics and no new required keys.
75
+
76
+ ---
77
+
78
+ ## Per-worker actors
79
+
80
+ `per_step_actor: true` suffixes the actor with the unit of work, so a fleet does not
81
+ collapse into one indistinguishable bot:
82
+
83
+ | Product | Unit of work | Example actor |
84
+ |---|---|---|
85
+ | crosscheck | workflow step | `crosscheck/review`, `crosscheck/fix`, `crosscheck/recheck` |
86
+ | symphony | worker | `symphony/worker-3` |
87
+
88
+ In T1 the suffixed name is what `createAsUser` sends, so it is what Linear renders. In
89
+ T0 it reaches only the signature line — still worth doing, since that is all T0 has.
90
+
91
+ The suffix composes: deriving twice yields `crosscheck/review/shard-2`. Deriving must
92
+ not mutate the base identity, so one run can produce several scoped identities from a
93
+ single minted token.
94
+
95
+ ---
96
+
97
+ ## The token mint
98
+
99
+ ```
100
+ POST https://api.linear.app/oauth/token
101
+ Content-Type: application/x-www-form-urlencoded
102
+
103
+ grant_type=client_credentials&client_id=<id>&client_secret=<secret>&scope=read+write
104
+ ```
105
+
106
+ Response: `{ "access_token": "..." }`, used as `Authorization: Bearer <token>`.
107
+
108
+ T0 keys are sent **bare** — `Authorization: <key>` with no `Bearer` prefix. Getting
109
+ this backwards is the most common integration bug.
110
+
111
+ ---
112
+
113
+ ## Two Linear gotchas worth encoding
114
+
115
+ Both cost real debugging time and neither is discoverable from the API:
116
+
117
+ - The OAuth application form **requires a Redirect URI** even though
118
+ `client_credentials` never uses one. Any placeholder on a domain you control works.
119
+ - `read write` does **not** cover initiatives. `initiative:read` and `initiative:write`
120
+ are separate scopes.
121
+
122
+ ---
123
+
124
+ ## Reference implementation
125
+
126
+ crosscheck's lives in `src/linear/`:
127
+
128
+ | Concern | File |
129
+ |---|---|
130
+ | Auth resolution, token mint, `withWorker` | `identity.ts` |
131
+ | All Linear API traffic | `client.ts` |
132
+ | Which issue a write targets | `ref.ts` |
133
+ | Credential reads (the only place) | `../config/loader.ts` |
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanbased/crosscheck",
3
- "version": "1.1.1-beta.0",
3
+ "version": "1.2.0-beta.33",
4
4
  "description": "AI code review pipeline that turns agent-written PRs into merge-ready patches",
5
5
  "bin": {
6
6
  "crosscheck": "dist/cli.js",