@allocator-one/harness-cli 1.12.1 → 1.14.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
@@ -1,206 +1,548 @@
1
1
  # @allocator-one/harness-cli
2
2
 
3
- CLI and MCP server for [Harness](https://harness.infra.one), allocator.one's
4
- issue tracker. A standalone Node client — no backend checkout, no Erlang
5
- toolchain, and no local issue state: issues live server-side, so there is
6
- nothing to commit or sync.
3
+ The `harness` CLI and MCP server for [Harness](https://harness.infra.one),
4
+ allocator.one's issue tracker. It is a standalone Node client that keeps no
5
+ local issue state. Issues live on the server, so there is nothing to commit or
6
+ sync.
7
7
 
8
8
  ## Install
9
9
 
10
- Zero-install, always current:
11
-
12
10
  ```sh
13
- npx -y @allocator-one/harness-cli list --team ALL
11
+ npm i -g @allocator-one/harness-cli
12
+ harness ready
14
13
  ```
15
14
 
16
- Or globally:
15
+ Or run it without installing:
17
16
 
18
17
  ```sh
19
- npm i -g @allocator-one/harness-cli
20
- harness ready
18
+ npx -y @allocator-one/harness-cli ready
21
19
  ```
22
20
 
23
21
  Requires Node >= 20.
24
22
 
25
- ## Auth
26
-
27
- No setup needed: commands target `https://harness.infra.one` and start the
28
- browser login the first time. One login covers every shell, worktree, and
29
- agent slot on a machine — credentials are stored at
30
- `~/.config/harness/credentials.json` (0600) as a single `{url, token}` pair,
31
- and a token is only ever sent to the host that minted it.
32
-
33
- - `harness login` — browser flow (PKCE loopback); `--no-browser` prints the
34
- URL and prompts for a pasted token instead.
35
- - `harness logout` — revokes the stored login server-side and deletes the file.
36
- - `harness whoami` — shows the resolved host and credential source.
37
-
38
- Headless contexts (agents, CI): mint an `aone_…` API token (admin → API
39
- tokens) and set `HARNESS_API_TOKEN`. Do not set it in interactive sessions —
40
- the browser login attributes writes to the real user.
41
-
42
- ### Provider keys
43
-
44
- Admins store the org's provider API keys (Anthropic, OpenAI, Google,
45
- OpenRouter, Brave Search, Perplexity) in Harness under `Settings → Model
46
- keys`. The CLI hands them to tooling without ever printing them:
47
-
48
- - `harness keys list` — provider slugs the org has keys for and the env var
49
- each maps to (`--json` for `{providers: [{provider, env}]}`); values are
50
- never shown.
51
- - `harness keys exec [--only <slugs>] [--override] -- <command> [args…]` —
52
- runs the command with the keys in its environment and exits with the
53
- command's exit code (128 + signal number when it was signalled). `--only
54
- brave,perplexity` passes just those providers and withholds your shell's
55
- own copies of the other provider variables, so the tool sees no more than
56
- it needs; a requested provider the org has no key for keeps your shell's
57
- variable (and says so on stderr). Slugs are trimmed and case-insensitive,
58
- and an allowlist that names nothing (`--only ,`) is a usage error, not
59
- "everything". Otherwise a variable already set in your shell wins unless
60
- `--override` is given; a blank variable counts as unset. `--json` is
61
- accepted for uniformity but changes nothing: stdout belongs to the command. A SIGTERM sent to
62
- `harness` is forwarded to the command; Ctrl+C reaches the command from the
63
- terminal directly and `harness` waits to report its exit. macOS and Linux
64
- only.
65
-
66
- Slug → variable, the same table rcl uses: anthropic → `ANTHROPIC_API_KEY`,
67
- openai → `OPENAI_API_KEY`, google → `GEMINI_API_KEY`, openrouter →
68
- `OPENROUTER_API_KEY`, brave → `BRAVE_API_KEY`, perplexity →
69
- `PERPLEXITY_API_KEY`. Reading keys needs the `model-keys:read` scope: login
70
- tokens carry it, `aone_` tokens must be minted with it. There is deliberately
71
- no MCP tool for this — a tool result is model-visible text.
72
-
73
- ## Environment variables
74
-
75
- | Variable | Effect |
76
- | --- | --- |
77
- | `HARNESS_API_URL` | Target host (default `https://harness.infra.one`). https only, plain http allowed against localhost. |
78
- | `HARNESS_API_TOKEN` | Bearer token (`aone_…`); wins over the stored login. Never inherits the stored login's host. |
79
- | `HARNESS_TEAM` | Default team key for commands that take `--team`. |
80
- | `HARNESS_ACTOR` | Write attribution, sent only when explicitly set. |
81
- | `HARNESS_CACHE_DIR` | Where `show` downloads attached images (default `$XDG_CACHE_HOME/harness/attachments` or `~/.cache/harness/attachments`). Absolute paths only. |
82
-
83
- ## For agents
84
-
85
- `harness mcp` runs a stdio MCP server exposing every operation as a typed
86
- tool — issue_list, issue_show, activity_list, issue_create, issue_update, issue_claim,
87
- issue_close, issue_comment, issue_comment_edit, issue_comment_delete,
88
- relation_list/create/update/delete, relation_add/rm (blocking shorthand),
89
- link_list/add/update/rm,
90
- project_list/show/create/update, milestone_list/create,
91
- document_list/show/create/update, label_list/create, team_list,
92
- team_create, workflow_states, user_list, stats, upload. Read-only tools carry
93
- `readOnlyHint`. Results are compact JSON of the data envelope.
94
-
95
- Register it with Claude Code:
23
+ ## Sign in
24
+
25
+ You don't need to configure anything. Commands target
26
+ `https://harness.infra.one`, and the first command you run in a terminal
27
+ opens a browser login and then carries on. One login covers every shell,
28
+ worktree, and agent slot on the machine.
29
+
30
+ - `harness login`: browser sign-in (PKCE, loopback callback on 127.0.0.1).
31
+ `--no-browser` prints the URL and asks you to paste the token instead.
32
+ `--url <host>` signs in to another host, and `--timeout <ms>` changes how
33
+ long it waits for the callback (default 3 minutes). Login tokens last
34
+ about 30 days.
35
+ - `harness logout`: revokes the stored login on the server and deletes it
36
+ locally.
37
+ - `harness whoami`: shows the resolved host and where the credential came
38
+ from.
39
+
40
+ The automatic login only runs when stdout is a terminal and `CI` is unset
41
+ or empty. Elsewhere (agents, CI, pipes), a command that has no credential
42
+ prints how to authenticate and exits with code `2`. For those headless
43
+ contexts, mint an `aone_…` API token (admin → API tokens) with the
44
+ `harness:read` scope, adding `harness:write` if the workflow changes
45
+ anything (claim, create, update, comment, close), and export it as
46
+ `HARNESS_API_TOKEN`.
47
+
48
+ > **Never set `HARNESS_API_TOKEN` or `HARNESS_ACTOR` in an interactive
49
+ > session.** The browser login attributes writes to the real user, and
50
+ > overriding it falsifies the audit trail. Both variables are only for
51
+ > headless CI and agent slots.
52
+
53
+ Every identifier includes its team key, as in `CLI-12` or `ALL-4901`. Older
54
+ issues keep legacy prefixes: `IO-` issues were created before team-prefixed
55
+ numbering, `ALL-` numbers up to the import ceiling came from Linear, and
56
+ `BD-` from beads.
57
+
58
+ ## Daily workflow
96
59
 
97
60
  ```sh
98
- claude mcp add --scope user harness -- npx -y @allocator-one/harness-cli mcp
61
+ harness ready --claim # claim the team's next ready, unassigned issue
62
+ harness show CLI-12 # read it, including attached screenshots
63
+ # … do the work …
64
+ harness close CLI-12 --reason "Implemented in 1a2b3c4"
99
65
  ```
100
66
 
101
- Example tool call: `issue_list` with `{"ready": true, "team": "ALL"}`
102
- returns the ready queue as JSON; `issue_claim` with `{"id": "IO-123"}`
103
- claims atomically (a conflict means someone else holds it).
104
-
105
- Read the same mixed timeline shown in an issue's Activity section with
106
- `harness activity IO-123`. It returns the newest 20 entries and points out
107
- when older entries exist; pass `--all` to follow the opaque cursors through
108
- the full history. `activity_list` exposes the same `id`, `limit`, and `all`
109
- inputs to MCP clients. Assignment, reassignment, and unassignment entries show
110
- the actor and previous/new assignees when supplied by the server. Unavailable
111
- identities use safe labels rather than raw user ids.
112
-
113
- Typed issue relations are available without losing the concise blocking
114
- workflow. `harness dep add|rm` and the `relation_add`/`relation_rm` MCP tools
115
- remain the blocker-first shorthand; the full surface is:
67
+ **Queue.** `harness ready` lists open, unblocked issues, highest priority
68
+ first. Blocked issues, `hold`-labeled issues, and parents with sub-issues
69
+ that are not completed are excluded. Only a completed blocker resolves: a
70
+ cancelled or archived one still blocks until it is completed or the
71
+ dependency is removed or re-pointed. An issue that is waiting on a wait
72
+ condition (see [Plans](#plans)) is not ready either. `harness blocked` lists
73
+ issues that are waiting on a blocker.
74
+ Both commands take `--team`. `ready` marks agent steps, the plan steps
75
+ Operator runs, with `[agent]` before the title and a legend under the table,
76
+ and `--json` gives every issue an `agent_step` flag. Only the plan driver
77
+ claims an agent step. The issue payload carries no executor, so the CLI
78
+ goes by the team: agent steps are filed in the organization's Operator team
79
+ (named Operator, keyed `OPERATOR`, or `OPERATOR2` and so on). That is a
80
+ heuristic. A step handed back to a person keeps its team and stays marked,
81
+ and the claim itself is the authority.
82
+
83
+ **Claim the next issue.** `harness ready --claim` is a single request: the
84
+ server picks the team's most important ready issue that has no assignee
85
+ (urgent, high, medium, low, then none; oldest first, then by id), assigns
86
+ you, and moves it to the team's first started state, all in one transaction.
87
+ Concurrent callers get different issues, and the CLI does no paging, sorting,
88
+ or retrying of its own.
89
+
90
+ - It needs a team: `--team`, then `HARNESS_TEAM`, then the repo config. With
91
+ none of them it is a usage error, and the claim is never sent.
92
+ - `--all`, `--limit`, and `--page` are refused with `--claim`, because the
93
+ server already ranks the whole ready set.
94
+ - An empty queue is a normal outcome: it prints "Nothing to claim: team CLI
95
+ has no unassigned ready issues." (`null` with `--json`) and exits `0`.
96
+ - A rate limit, server error, or unreadable response exits `1`, and an
97
+ unreachable API exits `3`. None of these is reported as an empty queue.
98
+ The CLI never retries the claim. When an unreadable response or a lost
99
+ connection leaves it unknown whether the claim landed, the error says to
100
+ check `harness list --team <key> --assignee me --status in_progress`
101
+ before claiming again.
102
+
103
+ **Read.** `harness show <id>` prints the fields, relations, children, and
104
+ comments. It also downloads the images attached to the issue's description,
105
+ design, acceptance criteria, notes, and comments (up to 20, cached locally)
106
+ and prints their paths. Screenshots are often the reproduction, so read
107
+ them. `--no-images` skips the downloads. If a download fails, a stderr
108
+ notice says so and the command still succeeds. `harness comments <id>`
109
+ prints the full, untruncated comment thread with comment ids. The
110
+ `Blocked:` line names a cancelled or archived blocker as such
111
+ (`yes, by CLI-3 (cancelled), CLI-4 (archived), CLI-5`). When the issue has
112
+ wait conditions, `Waiting:` says how many still hold it, and a
113
+ `Wait conditions:` section lists each one (`[ ] gate "Finance sign-off" · <id>`,
114
+ `[x] not before … (satisfied …)`, `system event`) with the id
115
+ `harness gate satisfy` takes.
116
+ `harness activity <id>` prints the issue's activity timeline: comments,
117
+ creation, and state and assignee changes. It shows the newest 20 entries
118
+ (`--limit` accepts 1–100), and `--all` reads further back.
119
+
120
+ **Create.** Creating an issue before you write code keeps the work tracked:
116
121
 
117
122
  ```sh
118
- harness relation list IO-123
119
- harness relation add IO-123 IO-456 --type related
120
- harness relation update IO-123 IO-456 --type blocks
121
- harness relation rm IO-123 IO-456
123
+ harness create --title "Fix login redirect" --type bug --priority high \
124
+ --description "…" --parent CLI-3 --assignee me --due-date 2026-11-01 \
125
+ --label regression --idempotency-key fix-login-redirect-1
122
126
  ```
123
127
 
124
- Relation update and removal accept either the relation id from `relation list`
125
- or a unique related issue identifier. If the same pair has more than one
126
- relation, the CLI refuses to guess and requires the relation id. Link updates
127
- use the same safe lookup rule by id or exact URL; the existing `link rm`
128
- command keeps its first-match behavior for backward compatibility:
128
+ - `--type task|bug|feature|epic` is applied as a `type/<type>` label.
129
+ - `--priority urgent|high|medium|low|none` takes names only. Bare numbers
130
+ are refused because bd and Harness number priorities differently.
131
+ - `--team <key>` falls back to `HARNESS_TEAM` and then to the repo config.
132
+ With none of these set, the command is a usage error.
133
+ - `--label` (repeatable) looks for a matching label in the target team first
134
+ and then at workspace scope. Missing labels are created at workspace scope
135
+ (which requires Harness administrator standing) before the issue is filed,
136
+ and the issue is created in a single request with all its labels.
137
+ - `--idempotency-key <key>` (1–200 printable ASCII characters, no leading or
138
+ trailing space) makes a retry safe. If a create times out, run it again
139
+ with the same key to get the original issue back instead of a duplicate.
140
+ The CLI never retries a write on its own.
141
+ - `--design`, `--notes`, and `--acceptance` set the remaining text fields.
142
+
143
+ **Update.** `harness update <id>` with:
144
+
145
+ - `--claim`: atomic. It assigns you and moves the issue to the team's first
146
+ started state. A conflict (exit `1`, HTTP 409) names its reason: someone
147
+ else holds the issue, or it is not claimable yet. Two reasons come with a
148
+ next step: `agent_step` (Operator runs it; to do it yourself, set its
149
+ executor to `none` with `harness plan edit <plan-id>` and release the plan
150
+ again) and `waiting_on_condition` (`harness show` lists what it waits on;
151
+ tick a gate with `harness gate satisfy`). It cannot be combined with
152
+ `--assignee`.
153
+ - `--state <name-or-type>`: moves the issue to the workflow state whose name
154
+ or type matches.
155
+ - `--title`, `--description`, `--design`, `--acceptance`, `--priority`,
156
+ `--parent`, `--assignee` (`""` unassigns), `--due-date` (`""` clears).
157
+ - `--notes` replaces the notes and `--append-notes` appends to them. Pass
158
+ only one of the two.
159
+ - `--add-label` / `--remove-label` (repeatable, name or id) add or remove
160
+ individual labels without replacing the rest. You can only add labels that
161
+ belong to the issue's team or the workspace, and a missing name is created
162
+ at workspace scope, as with `create`. If two attached labels match a name,
163
+ remove by id.
164
+
165
+ **Dependencies.** `harness dep add <issue> <blocker>`: **the second argument
166
+ blocks the first.** `harness dep rm <issue> <blocker>` removes the
167
+ dependency. For other relation types, see `relation` under
168
+ [Other commands](#other-commands).
169
+
170
+ **Close.** `harness close <id> [<id>…]` closes one or more issues as
171
+ completed. Use `--cancel` to close them as canceled, or `--duplicate-of <id>`
172
+ to close a single issue as a duplicate of the canonical one. The
173
+ duplicate-of form records a duplicate relation and cannot be combined with
174
+ `--cancel`. `--reason` records why the issue is done. The output lists the
175
+ issues each closure unblocked
176
+ (`Closed CLI-12 — unblocked: CLI-13, CLI-14`); act on them instead of
177
+ leaving them in the queue. Only completing releases dependents: a canceled
178
+ issue still blocks them. A duplicate close moves its blocking edges to the
179
+ canonical issue, and any edge that cannot move stays on the duplicate. When
180
+ the closed issue still blocks anything, an indented line names those
181
+ dependents (`Still blocking: CLI-13 (a cancelled issue still blocks its
182
+ dependents)`) so you can remove or re-point those dependencies. With
183
+ several ids, each one is closed on its own.
184
+ A failure prints `FAILED <id>: …`, and the command exits `1` after trying
185
+ the rest. `harness archive <id>…` archives issues, `--restore` brings them
186
+ back, and `harness list --scope archived` lists them.
187
+
188
+ **Comment.** `harness comment <id> "text" [--mention <name>]…` posts a
189
+ comment. The server resolves each mention and notifies that person.
190
+ `harness comment edit <id> <comment-id> "new text"` and
191
+ `harness comment delete <id> <comment-id>` (alias `rm`) work on your own
192
+ comments. Comment ids are UUIDs from `harness comments`.
193
+
194
+ **Find.** There are two search commands:
195
+
196
+ - `harness search <question…>` searches by meaning. It returns a ranked list
197
+ with an exact identifier first, followed by results ordered by relevance
198
+ across a lexical and a semantic signal, so it can find issues that share
199
+ none of your words. Write the question in plain language instead of
200
+ keywords. Results default to 10 and are capped at 20, with no pages
201
+ (`--page`/`--all` are accepted but ignored). Archived issues are not
202
+ searched. If the semantic side is unavailable, stderr says
203
+ `semantic search unavailable — showing lexical matches only`.
204
+ - `harness list --search <text>` is the paginated lexical filter, which
205
+ matches title or issue number.
206
+
207
+ `harness list` filters: `--status open|in_progress|closed`,
208
+ `--assignee <me|id|email|name|"">` (`""` means unassigned),
209
+ `--created-by <me|id>`, `--involved me`, `--priority <name>`,
210
+ `--label <name>` or `--type <type>` (only one of the two), `--team <key>`,
211
+ `--parent <id>` (children of an epic), and `--scope all|archived`.
212
+
213
+ **Overview.** `harness stats` prints per-team and total counts.
214
+ `harness prime` prints those counts, the organization's ten most important
215
+ ready issues across every team (urgent first and priority none last, ranked
216
+ the way `harness ready --claim` ranks, though that claims only unassigned
217
+ issues on one team), and a short workflow reminder. It is meant for the
218
+ start of an agent session. Against a server older than this read it shows
219
+ the ten newest ready issues instead.
220
+
221
+ ### Text from a file
222
+
223
+ When text comes from somewhere other than your own typing, such as issue
224
+ content, a commit message, or model output, pass a file instead of a shell
225
+ argument. Quoting can be defeated: a `"` ends the string, `$(…)`
226
+ substitutes, and a heredoc ends at any flush-left delimiter line the text
227
+ happens to contain.
129
228
 
130
229
  ```sh
131
- harness link update IO-123 https://example.com/old --url https://example.com/new --title "Runbook"
230
+ harness create --title "…" --description-file ./body.md
231
+ harness update CLI-12 --notes-file ./notes.md --acceptance-file ./ac.md
232
+ harness comment CLI-12 --body-file ./comment.md
233
+ git log -1 --format=%B | harness close CLI-12 --reason-file -
132
234
  ```
133
235
 
134
- Issue creation resolves `--label` names within the target team first, then at
135
- workspace scope. Missing names are created at workspace scope before the issue
136
- is filed, and the final label ids travel in the single issue-create request, so
137
- a preparation or permission failure cannot leave an unlabeled issue behind.
138
- Creating a missing label requires Harness administrator standing.
139
-
140
- Label repair is additive and repeatable; it never implicitly replaces the
141
- whole set:
236
+ | Command | File options |
237
+ | --- | --- |
238
+ | `create` | `--title-file`, `--description-file`, `--design-file`, `--notes-file`, `--acceptance-file` |
239
+ | `update` | the same five, plus `--append-notes-file` |
240
+ | `close` | `--reason-file` |
241
+ | `comment`, `comment edit` | `--body-file` |
242
+
243
+ Other commands (`project`, `milestone`, `doc`, `link`, and so on) take inline
244
+ values only. `plan create` and `plan edit` read a whole JSON or YAML
245
+ document with `--file` (see [Plans](#plans)), under the same stdin and
246
+ UTF-8 rules, up to 8 MB (the API's request limit). `-` reads from stdin, and only one option per invocation can do
247
+ that. stdin must not be a terminal. Passing both the inline and the file form
248
+ of a field is a usage error, and so is a file that produces an empty title,
249
+ reason, or comment. Conflicting options are caught before anything is read,
250
+ so a rejected command never consumes your pipe. A file must be valid UTF-8
251
+ and at most 1 MB, and one trailing newline is dropped, as with
252
+ `$(cat file)`.
253
+
254
+ ## Plans
255
+
256
+ A plan is a root issue and its steps, created in one request: their
257
+ parents, `blocked_by` dependencies, executors, teams and wait conditions.
258
+ Either the whole plan is created or nothing is. Plans start held, so
259
+ nothing runs until a person releases one.
142
260
 
143
261
  ```sh
144
- harness update IO-123 --add-label security --add-label needs-review
145
- harness update IO-123 --remove-label security
262
+ harness plan create --file plan.yaml --team IO --idempotency-key q3-report
263
+ harness plan show <plan-id>
264
+ harness plan release <plan-id> --collaborator grace@example.com
265
+ harness gate satisfy IO-121 "Finance sign-off"
146
266
  ```
147
267
 
148
- Both update flags accept a label name or id. Additions allow only labels from
149
- the issue's team or workspace. Removal can target an attached legacy foreign
150
- label; when attached normalized names are ambiguous, use the label id.
151
-
152
- Auth resolution is identical to the CLI. Without a credential, tools return
153
- an error telling the agent to run `harness login` in a terminal — the
154
- server never opens a browser itself.
155
-
156
- ### Text from a file, never from a shell argument
157
-
158
- Every free-text option has a `--<field>-file` companion, and `-` means stdin:
268
+ **The plan file** is the API's request body, as JSON (`.json`) or YAML
269
+ (anything else, and stdin with `--file -`). YAML is read as YAML 1.2 and
270
+ held to what JSON can carry: dates stay strings, and a duplicate key, an
271
+ explicit tag such as `!!timestamp`, or `.inf`/`.nan` is refused. The
272
+ root's fields sit at the top level, and each step has a `key` that other
273
+ steps refer to:
274
+
275
+ ```yaml
276
+ title: Ship the Q3 LP report
277
+ spend_budget_cents: 5000 # needed to release Operator steps
278
+ steps:
279
+ - key: gather
280
+ title: Gather the fund data
281
+ executor: operator # Operator runs it; the description is its instruction
282
+ description: Pull the Q3 NAVs for every fund in the portfolio
283
+ - key: review
284
+ title: Review the numbers
285
+ team: IO # a person's step; defaults to --team
286
+ blocked_by: [gather]
287
+ conditions:
288
+ - kind: gate
289
+ label: Finance sign-off
290
+ - key: send
291
+ title: Send the report
292
+ blocked_by: [review]
293
+ conditions:
294
+ - kind: not_before
295
+ not_before: "2026-10-15T09:00:00Z"
296
+ ```
159
297
 
160
- ```sh
161
- harness create --title "…" --description-file ./body.md
162
- harness update IO-123 --notes-file ./notes.md --acceptance-file ./ac.md
163
- harness comment IO-123 --body-file ./comment.md
164
- git log -1 --format=%B | harness close IO-123 --reason-file -
298
+ - A step takes the issue fields (`title`, `description`, `priority` as a
299
+ number from 0 for none to 4 for low, `assignee`, `label_ids`, …) plus
300
+ `key`, `parent` (the key of the step that contains it), `blocked_by`
301
+ (keys), `executor` (`none`, the default, or `operator`), `team`, and
302
+ `conditions`. Operator steps are filed in the Operator team. Only steps
303
+ without sub-steps carry an executor, edges, or conditions.
304
+ - Wait conditions: `not_before` (a UTC instant), `gate` (a `label` a person
305
+ ticks), and `event` (`event`, `subject_id`, and for a stage change
306
+ `target_id`). The allowed events are `deal_stage_changed`,
307
+ `opportunity_stage_changed`, `form_submitted`, and `investment_executed`.
308
+ Every condition must hold before the step is ready.
309
+ - A plan has at most 100 steps, its root included.
310
+ - `--team` (or `HARNESS_TEAM` or the repo config) is the root's team and the
311
+ default team for people's steps. `--idempotency-key` works as it does for
312
+ `create`: if a create times out, run it again with the same key to get
313
+ the same plan back.
314
+
315
+ **Validation.** The server checks the whole graph and an invalid plan
316
+ creates nothing. The command exits `1`, and stderr lists every violation by
317
+ the step key it is about:
318
+
319
+ ```text
320
+ harness: The plan is not valid; nothing was changed (HTTP 422)
321
+ step "review" (steps[1]): parent names no step of this plan, or no issue in this organization (parent: nope) [unknown_parent]
322
+ plan: A plan with Operator steps needs a positive spend budget [spend_budget_required]
165
323
  ```
166
324
 
167
- `create` and `update` cover `--title`, `--description`, `--design`, `--notes`
168
- and `--acceptance` (plus `--append-notes` on `update`); `close` takes
169
- `--reason-file`, `comment` takes `--body-file`.
325
+ **Show.** `harness plan show <plan-id>` prints the plan's status, budgets,
326
+ revision, and spend estimate, then every step in tree order (indented by
327
+ depth) with its key, executor, state, and status. The status is `ready`,
328
+ `held` (edited since the release), `not in revision` (added after it),
329
+ `waiting` (on a wait condition), `container`, `done`, `cancelled`,
330
+ `archived`, or `-`. The edges and each step's wait conditions come after
331
+ the steps.
332
+
333
+ **Edit.** `harness plan edit <plan-id> --file edits.yaml` changes step
334
+ executors and keys, naming each step by its `step` key or `issue`
335
+ identifier, and the budgets (`retry_budget`, `spend_budget_cents`):
336
+
337
+ ```yaml
338
+ steps:
339
+ - step: gather
340
+ executor: none # hand the step to a person
341
+ ```
170
342
 
171
- Use these whenever the text is not something you authored inline. Prose that
172
- reaches a shell argument has to survive quoting, and quoting can be defeated —
173
- a `"` closes the string, `$(…)` substitutes, and a heredoc ends at whatever
174
- flush-left delimiter line the text happens to contain. Handing over a path
175
- removes the question, the way `git commit -F` does. This matters most for
176
- agents relaying issue text they did not write.
343
+ Editing a released step holds it until the plan is released again. A
344
+ budget change needs the plan held first. Descriptions, parents, and edges
345
+ are edited with the issue commands (`update`, `dep`). Conditions are set
346
+ when the plan is created. Editing them later goes through the API's issue
347
+ condition endpoints, which have no command yet; `gate satisfy` ticks a
348
+ gate.
349
+
350
+ **Release and hold.** `harness plan release <plan-id>` releases the plan as
351
+ you: it freezes the next revision and lets ready steps run.
352
+ `--collaborator <id-or-email>` (repeatable) adds people to the plan room
353
+ where its Operator steps run, and `--retry-budget` and
354
+ `--spend-budget-cents` set the budgets. `harness plan hold <plan-id>`
355
+ pauses every step until the plan is released again.
356
+
357
+ **Gates.** `harness gate satisfy <issue> <gate>` ticks a gate and
358
+ `harness gate unsatisfy <issue> <gate>` unticks it. `<gate>` is the gate's
359
+ label or its condition id from `harness show`. A label matches exactly
360
+ first, then ignoring case, and a label that more than one gate carries is
361
+ refused with their ids. Ticking a gate requires staff standing in the
362
+ organization.
363
+
364
+ A server without the plan API answers the plan commands with a 404, and the
365
+ error says that the server may predate plans.
366
+
367
+ ## Uploads
368
+
369
+ `harness upload <path>` uploads a file and prints a markdown reference to
370
+ paste into a description or comment. `--json` returns the URL, filename,
371
+ content type, byte size, and markdown instead.
372
+
373
+ - Images (PNG, JPG, WebP, GIF) can be up to 25 MB.
374
+ - Videos must be `.mp4` files of up to 100 MB, with H.264 8-bit 4:2:0 video,
375
+ optional AAC audio, and optional mov_text subtitles. The server validates
376
+ the media and does not transcode other formats. Harness shows linked
377
+ videos as an inline player.
378
+
379
+ A new video is private to its uploader until that user saves its link in an
380
+ issue description or comment. After that it follows the access policy of
381
+ the issue that holds it, and reusing the link elsewhere doesn't change who
382
+ owns it. `harness show` downloads images only, never videos. The older
383
+ `attach-image <path>` command accepts images only (25 MB).
384
+
385
+ ## Other commands
386
+
387
+ | Command | Notes |
388
+ | --- | --- |
389
+ | `relation list <issue>` | Every typed relation, with relation ids. |
390
+ | `relation add <issue> <other> --type <t>` | `blocks`, `blocked_by`, `related`, `duplicate`, `duplicate_of`, `duplicated_by`. |
391
+ | `relation update <issue> <relation-id-or-issue> --type <t>` | `blocks`, `blocked_by`, `related`, `duplicate_of`. |
392
+ | `relation rm <issue> <relation-id-or-issue>` | If more than one relation matches the other issue, pass the relation id. |
393
+ | `link list\|add\|update\|rm <issue> …` | URLs attached to an issue. `add <issue> <url> [--title]`; `update <issue> <link-id-or-url> --url … \| --title …` refuses an ambiguous URL; `rm <issue> <link-id-or-url>` removes the first match. |
394
+ | `project list\|show\|create\|update` | `list [--search] [--state]` (paginated). `create --name … [--description] [--summary] [--state] [--lead <user-id>] [--start-date] [--target-date]`. |
395
+ | `milestone list\|create --project <id>` | `create … --name … [--description] [--target-date]`. |
396
+ | `doc list\|show\|create\|update` | `list [--search]` (paginated). `create --title … [--content] [--project <id>]`. |
397
+ | `label list\|create\|update\|delete` | `create <name> [--color]`, `update <id> [--name] [--color]`, `delete <id> --yes` (refused while issues carry the label). |
398
+ | `team list\|create\|rename\|set-default` | `create --key <KEY> --name …` sets up the default workflow states. The key is permanent; `rename <key> --name …` changes only the display name. `set-default <key>` makes the team the org's default filing target. |
399
+ | `state list\|add\|update\|order\|delete` | A team's workflow states (`--team`, else `HARNESS_TEAM` or the repo config). `add --name … --type backlog\|unstarted\|started\|completed\|canceled [--color]`, `order <id,id,…>` (every state id, in order), `delete <id> --yes`. `states` is an alias of `state list`. |
400
+ | `member list\|add\|set-role` | The org's tracker roster. `add --email … --role admin\|staff\|collaborator`, `set-role <user-id> --role …\|none` (`none` revokes). |
401
+ | `user list` | Users, for resolving assignees. |
402
+ | `version` | CLI and server API versions. Works before you log in, and so does `harness --version`. |
403
+
404
+ `team rename`, `team set-default`, `label update`, `label delete`, and the
405
+ write commands of `state` and `member` require Harness administrator
406
+ standing, and so does `member list`.
407
+
408
+ ## Provider keys
177
409
 
178
- Passing both an inline value and its `-file` for the same field is a usage
179
- error rather than a silent precedence, and only one field per invocation may
180
- read stdin. Both are checked before anything is read, so a rejected command
181
- never consumes your pipe. One trailing newline is dropped, like `$(cat file)`.
410
+ Admins store the org's provider API keys (Anthropic, OpenAI, Google,
411
+ OpenRouter, Brave Search, Perplexity) in Harness under `Settings → Model
412
+ keys`. `harness keys` passes them to tools without ever printing them:
182
413
 
183
- ## Image and video uploads
414
+ - `harness keys list` shows the provider slugs the org has keys for and the
415
+ environment variable each one maps to (`--json` returns
416
+ `{providers: [{provider, env}]}`). It never prints the values.
417
+ - `harness keys exec [--only <slugs>] [--override] -- <command> [args…]`
418
+ runs the command with the keys in its environment and exits with the
419
+ command's exit code (128 + the signal number if the command was killed by a
420
+ signal). `--only brave,perplexity` passes just those providers and also
421
+ removes your shell's own copies of the other provider variables, so the
422
+ command sees only what it needs. If the org has no key for a provider you
423
+ requested, your shell's variable is kept and stderr says so. Slugs are
424
+ trimmed and case-insensitive. An `--only` list that names nothing
425
+ (`--only ,`) is a usage error, not a request for everything. Without
426
+ `--override`, a variable that is already set in your shell wins (a blank
427
+ variable counts as unset). `--json` is accepted but has no effect, because
428
+ stdout belongs to the command. A SIGTERM sent to `harness` is forwarded to
429
+ the command. Ctrl+C reaches the command directly from the terminal, and
430
+ `harness` waits to report its exit. Works on macOS and Linux only.
431
+
432
+ Each slug maps to the same variable as in rcl: anthropic → `ANTHROPIC_API_KEY`,
433
+ openai → `OPENAI_API_KEY`, google → `GEMINI_API_KEY`, openrouter →
434
+ `OPENROUTER_API_KEY`, brave → `BRAVE_API_KEY`, perplexity →
435
+ `PERPLEXITY_API_KEY`. Reading keys requires the `model-keys:read` scope.
436
+ Login tokens have it; `aone_` tokens need to be minted with it. There is
437
+ deliberately no MCP tool for keys, because a tool result is text the model
438
+ can read.
439
+
440
+ ## Scripting contract
441
+
442
+ - **`--json` on every command** except `mcp`. It replaces the human output
443
+ with the response data as JSON. `keys exec` accepts it but changes
444
+ nothing: its stdout is the child command's output.
445
+ - **stdout carries data only**, so it is safe to pipe. Diagnostics go to
446
+ stderr:
447
+ - `→ https://harness.infra.one` on every command that writes, naming the
448
+ host it sent the write to.
449
+ - `harness: showing 50 of 132 (page 1 of 3) — pass --all for the rest, or --page to step through.`
450
+ when a paginated read did not return everything, and
451
+ `harness: --limit N was capped at M by the server.` when the server
452
+ reduced the page size you asked for.
453
+ - The `activity`, `search`, and `show` notices described above.
454
+ - The update notice (see [Staying current](#staying-current)).
455
+ - **Pagination.** `ready`, `blocked`, `list`, `project list`, and `doc list`
456
+ return 50 records per page by default (`ready --claim` has no pages). `--limit` changes the page size,
457
+ `--page` selects a page, and `--all` follows every page (pass `--all` or
458
+ `--page`, not both). The truncation notice goes to stderr only, so a
459
+ script that reads just the `--json` output cannot tell a full page from a
460
+ complete result. Pass `--all` when you need everything.
461
+ - **Exit codes.** `keys exec` returns the command's exit code instead.
462
+
463
+ | Code | Meaning |
464
+ | --- | --- |
465
+ | `0` | Success, including `ready --claim` finding nothing to claim. |
466
+ | `1` | The command failed. Causes include an API error (the message gives the HTTP status, followed by any per-field errors or plan violations, one per line), a response the CLI cannot interpret, a partial failure across several ids, or a local refusal such as an unreadable or oversized upload. |
467
+ | `2` | Usage or configuration error, including "not signed in" outside a terminal. Nothing was sent. |
468
+ | `3` | The API was unreachable (DNS, TLS, connection refused, or timeout). Retry with backoff, and if the API stays down, say so instead of dropping tracked work. |
469
+
470
+ ## MCP server
471
+
472
+ `harness mcp` runs an MCP server over stdio. To register it with Claude Code:
184
473
 
185
- `harness upload ./demo.mp4` prints a reusable markdown reference. Paste it
186
- into an issue description or comment to show an inline video player. Use
187
- `--json` for the server's URL, filename, content type, byte size, and markdown.
188
- The MCP `upload` tool accepts the same local path and returns the same metadata.
474
+ ```sh
475
+ claude mcp add --scope user harness -- npx -y @allocator-one/harness-cli mcp
476
+ ```
189
477
 
190
- Images (PNG, JPG, WebP, GIF) can be up to 25 MB. Videos must be `.mp4`, up to
191
- 100 MB, with H.264 8-bit 4:2:0 video, optional AAC audio, and optional mov_text
192
- subtitles. The server validates the media; it does not transcode other formats.
478
+ With a global install, `harness mcp` works as the command instead of the
479
+ `npx` form. Any other MCP client can use the same command:
193
480
 
194
- A newly uploaded video is private to its uploader until that user saves its
195
- link in an issue description or comment. It then follows the owning issue's
196
- access policy. Reusing the link elsewhere does not change that ownership.
197
- `harness show` continues downloading images only, never linked videos.
198
- The older `attach-image` command retains its 25 MB limit.
481
+ ```json
482
+ { "mcpServers": { "harness": { "command": "npx", "args": ["-y", "@allocator-one/harness-cli", "mcp"] } } }
483
+ ```
199
484
 
200
- ## Repo config
485
+ The server exposes 58 tools:
201
486
 
202
- A committed `.harness-cli/config.json`, discovered git-style by walking up
203
- from the working directory, gives every clone of a repo its defaults:
487
+ | Area | Tools |
488
+ | --- | --- |
489
+ | Issues | `issue_list`, `issue_search`, `issue_show`, `activity_list`, `issue_create`, `issue_update`, `issue_claim`, `issue_close`, `issue_archive` |
490
+ | Comments | `issue_comment`, `issue_comment_edit`, `issue_comment_delete`, `comment_list` |
491
+ | Relations | `relation_add`, `relation_rm` (blocking shorthand: `blocker` blocks `issue`), `relation_list`, `relation_create`, `relation_update`, `relation_delete` |
492
+ | Links | `link_list`, `link_add`, `link_update`, `link_rm` |
493
+ | Projects and docs | `project_list`, `project_show`, `project_create`, `project_update`, `milestone_list`, `milestone_create`, `document_list`, `document_show`, `document_create`, `document_update` |
494
+ | Labels | `label_list`, `label_create`, `label_update`, `label_delete` |
495
+ | Teams and states | `team_list`, `team_create`, `team_rename`, `team_set_default`, `workflow_states`, `state_add`, `state_update`, `state_order`, `state_delete` |
496
+ | People | `member_list`, `member_add`, `member_set_role`, `user_list` |
497
+ | Plans and gates | `plan_create`, `plan_show`, `plan_edit`, `plan_release`, `plan_hold`, `gate_set` |
498
+ | Other | `stats`, `upload` |
499
+
500
+ The tools cover the issue workflow and the admin surface. Some commands are
501
+ CLI-only: `keys` (deliberately), `login`/`logout`/`whoami`, `version`, and
502
+ `prime`. Read-only tools carry `readOnlyHint`. Each result is compact JSON
503
+ of the response data. Any stderr notice the CLI would print, such as
504
+ truncation, is added as a second text item. A partial failure across several
505
+ ids is returned as an error, with the details for each id in the JSON.
506
+ Some tools behave differently from their commands:
507
+
508
+ - `issue_list` with `ready: true` or `blocked: true` accepts every list
509
+ filter. The CLI's `ready` and `blocked` take only `--team`.
510
+ - `issue_list` only lists. Claiming the team's next issue (`ready --claim`)
511
+ is CLI-only; over MCP, claim a specific issue with `issue_claim`.
512
+ - `issue_show` downloads images only when you pass `images: true`.
513
+ - `state_delete` and `label_delete` don't need the CLI's `--yes`.
514
+ - `plan_create` takes the plan as an object (`plan`), with `team` and
515
+ `idempotency_key` beside it, instead of a file. Its schema describes the
516
+ steps and conditions, and keys inside `plan` that the schema does not
517
+ know are still sent, so the server reports them. `plan_edit` takes
518
+ `steps`, `retry_budget`, and `spend_budget_cents` directly. As with every
519
+ tool, a top-level argument the tool does not declare is dropped.
520
+ `gate_set` takes `issue`, `gate`, and `satisfied`.
521
+ - An API error's details come back in the tool error text: a validation
522
+ error's fields, and an invalid plan's violations, one per line by step
523
+ key.
524
+ - `issue_list` with `ready: true` gives every issue an `agent_step` flag,
525
+ read from its team as `ready` does.
526
+
527
+ For example, `issue_list` with `{"ready": true, "team": "CLI"}` returns the
528
+ ready queue, and `issue_claim` with `{"id": "CLI-12"}` claims an issue
529
+ atomically. `issue_close` takes `ids`, `reason`, `cancel`, and
530
+ `duplicate_of`, and `issue_create` takes an `idempotency_key` for safe
531
+ retries.
532
+
533
+ The MCP server resolves auth exactly as the CLI does, on every call, so a
534
+ login completed while the server is running takes effect without a restart.
535
+ It never opens a browser. Without a credential, a tool returns an error that
536
+ tells the agent to run `harness login` in a terminal or set
537
+ `HARNESS_API_TOKEN`.
538
+
539
+ ## Configuration
540
+
541
+ ### Repo config
542
+
543
+ A committed `.harness-cli/config.json`, found by walking up from the working
544
+ directory the way git finds `.git`, gives every clone of a repo the same
545
+ defaults:
204
546
 
205
547
  ```json
206
548
  { "team": "CLI" }
@@ -208,47 +550,90 @@ from the working directory, gives every clone of a repo its defaults:
208
550
 
209
551
  | Field | Effect |
210
552
  | --- | --- |
211
- | `team` | Default team key. Precedence: `--team` > `HARNESS_TEAM` > repo config. |
212
- | `url` | Target host. `HARNESS_API_URL` wins. Tokens stay host-bound: a stored login only serves the host that minted it, an env `HARNESS_API_TOKEN` refuses to pair with a repo-named host (set `HARNESS_API_URL` explicitly), and before the interactive auto-login opens a browser at a host named only by the repo, the CLI asks once per host (remembered in `~/.config/harness/trusted-hosts.json`). |
213
- | `org` | Reserved for Harness org-scoping; parsed today, `url` wins meanwhile. |
214
-
215
- This is config, not state — issues stay server-side.
216
-
217
- ## Development
553
+ | `team` | Default team key. Precedence: `--team`, then `HARNESS_TEAM`, then the repo config. |
554
+ | `url` | Target host. `HARNESS_API_URL` takes precedence. Before an automatic login opens a browser at a host that only the repo names, the CLI asks for confirmation, once per host. |
555
+ | `org` | Reserved. It is parsed but has no effect yet. |
218
556
 
219
- ```sh
220
- npm ci
221
- npm run typecheck
222
- npm test
223
- npm run build # bundles to dist/cli.js (tsup)
224
- node dist/cli.js version
225
- ```
557
+ A malformed file is ignored with a warning on stderr.
226
558
 
227
- The OpenAPI spec is vendored at `spec/openapi.json` and TypeScript types are
228
- generated into `src/generated/` (committed, so npx consumers never run
229
- codegen):
559
+ ### Hosts and tokens
230
560
 
231
- ```sh
232
- npm run spec:refresh # fetch the spec from a live host (uses your login)
233
- npm run codegen # regenerate src/generated/api.ts from the vendored spec
234
- ```
561
+ The host comes from `HARNESS_API_URL`, then the repo config's `url`, then
562
+ the host of the stored login, then `https://harness.infra.one`. Tokens are
563
+ bound to the host that issued them. A stored login is only sent to the host
564
+ that issued it. `HARNESS_API_TOKEN` is only sent to `HARNESS_API_URL`, or to
565
+ the default host when that variable is unset. It is never sent to the stored
566
+ login's host, and when a repo config names another host, the command
567
+ refuses with exit code 2 instead of sending the token there. Set
568
+ `HARNESS_API_URL` explicitly if you do mean that host. Without
569
+ `HARNESS_API_TOKEN`, a repo-named host needs a login of its own, and the
570
+ automatic login asks for confirmation first, as described above. Plain
571
+ `http://` is accepted only for localhost.
235
572
 
236
- CI regenerates the types and fails on drift.
573
+ ### Environment variables
237
574
 
238
- ## Release
575
+ | Variable | Effect |
576
+ | --- | --- |
577
+ | `HARNESS_API_URL` | Target host (https, or http against localhost only). |
578
+ | `HARNESS_API_TOKEN` | An `aone_…` API token. It takes precedence over the stored login. Headless use only. |
579
+ | `HARNESS_ACTOR` | Write attribution, sent only when set. Without it the server attributes writes to the signed-in user, or for `aone_` tokens to the token's name. Headless use only. |
580
+ | `HARNESS_TEAM` | Default team key for commands that take `--team`. |
581
+ | `HARNESS_CACHE_DIR` | Where `show` stores downloaded images. Must be an absolute path. |
582
+ | `HARNESS_NO_UPDATE_CHECK` | Set to a value other than `0`, `false`, or `no` to turn off the update notice and the automatic upgrade. |
583
+ | `HARNESS_AUTO_UPGRADE=0` | Keeps the notice but never installs anything (`false` and `no` also work). |
584
+ | `CI` | Any value other than empty, `0`, `false`, or `no` turns off the update notice and automatic upgrade. Separately, any non-empty value, including `0`, stops the automatic browser login. |
585
+ | `XDG_CONFIG_HOME`, `XDG_CACHE_HOME` | Used instead of `~/.config` and `~/.cache` when set to absolute paths. |
239
586
 
240
- First bump `package.json` and `package-lock.json` through a pull request. After
241
- that PR is squash-merged, fetch `main` and tag the merge commit — never the
242
- pre-merge branch tip. Pushing the tag starts the Release workflow, which builds,
243
- tests, and publishes to npm:
587
+ ### Files
244
588
 
245
- ```sh
246
- git fetch origin main
247
- git tag -a vX.Y.Z MERGE_COMMIT_SHA -m "Release vX.Y.Z"
248
- git push origin vX.Y.Z
249
- ```
250
-
251
- Publishing requires the `NPM_TOKEN` actions secret (an automation token for
252
- the `allocator-one` npm org). Provenance attestation is disabled: npm only
253
- accepts it from public source repositories, and this repo is private — turn
254
- `--provenance` back on in `release.yml` if the repo ever goes public.
589
+ | Path | Contents |
590
+ | --- | --- |
591
+ | `~/.config/harness/credentials.json` | The stored login: one `{url, token}` pair, mode 0600. |
592
+ | `~/.config/harness/trusted-hosts.json` | Repo-config hosts you agreed to log in to. |
593
+ | `~/.cache/harness/attachments/` | Images downloaded by `show` (or `HARNESS_CACHE_DIR`). |
594
+ | `~/.cache/harness/update-check.json` | The daily update-check stamp. |
595
+ | `~/.cache/harness/upgrade.lock` | Held while an automatic upgrade runs. |
596
+
597
+ None of these files is tracker state: issues stay on the server. The
598
+ attachment cache does hold copies of issue images, so clear it when you no
599
+ longer need them.
600
+
601
+ ## Staying current
602
+
603
+ **Notice.** Each run reads the cached stamp and prints at most one stderr
604
+ line when a newer release exists:
605
+ `harness: 1.13.0 is available (you have 1.12.1) — npm i -g @allocator-one/harness-cli@latest`.
606
+ When the stamp is more than a day old, a detached background process
607
+ refreshes it from the npm registry. A command never waits on that network
608
+ request. The notice applies only to global and project-local installs, not
609
+ to `npx` or a git checkout.
610
+
611
+ **Automatic upgrade on HTTP 426.** The CLI sends its version with every
612
+ request, and the server refuses versions below its minimum with a 426. In
613
+ that case the CLI runs
614
+ `npm install --global --ignore-scripts @allocator-one/harness-cli@latest`,
615
+ re-runs the original command, and exits with that command's exit code.
616
+ Re-running is safe only for this status. The server answers 426 before
617
+ authentication and before the request handler, so the refused request
618
+ changed nothing. The CLI never retries any other failure. The upgrade is
619
+ limited by these guards:
620
+
621
+ - It only upgrades a global install, located through `npm root -g`, and
622
+ only when that directory is writable.
623
+ - Only one upgrade runs at a time on a machine (a lock under
624
+ `~/.cache/harness`). A process that finds a newer version already
625
+ installed re-runs with it directly.
626
+ - `--ignore-scripts` means an unattended upgrade never runs package scripts.
627
+ - The re-run carries `HARNESS_UPGRADED=1`, so each command is upgraded at
628
+ most once.
629
+ - It is skipped for `npx`, project-local installs, git checkouts, inside
630
+ `harness mcp`, under `CI`, and when `HARNESS_NO_UPDATE_CHECK` or
631
+ `HARNESS_AUTO_UPGRADE=0` is set.
632
+
633
+ When the upgrade is skipped or fails, the command prints the server's
634
+ message, adds the reason if the upgrade was attempted, and exits `1`.
635
+
636
+ ---
637
+
638
+ Building, testing, and releasing this package are covered in `AGENTS.md`
639
+ in the (private) source repository.