@allocator-one/harness-cli 1.12.1 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +411 -204
- package/dist/{chunk-VOH6BBJD.js → chunk-XFMECLOU.js} +56 -10
- package/dist/cli.js +11 -10
- package/dist/{server-FNNMISOZ.js → server-CYBKYG5E.js} +15 -6
- package/package.json +1 -1
- package/spec/openapi.json +42 -4
package/README.md
CHANGED
|
@@ -1,206 +1,370 @@
|
|
|
1
1
|
# @allocator-one/harness-cli
|
|
2
2
|
|
|
3
|
-
CLI and MCP server for [Harness](https://harness.infra.one),
|
|
4
|
-
issue tracker.
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
```sh
|
|
13
|
-
npx -y @allocator-one/harness-cli list --team ALL
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
Or globally:
|
|
17
|
-
|
|
18
10
|
```sh
|
|
19
11
|
npm i -g @allocator-one/harness-cli
|
|
20
12
|
harness ready
|
|
21
13
|
```
|
|
22
14
|
|
|
23
|
-
|
|
24
|
-
|
|
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:
|
|
15
|
+
Or run it without installing:
|
|
96
16
|
|
|
97
17
|
```sh
|
|
98
|
-
|
|
18
|
+
npx -y @allocator-one/harness-cli ready
|
|
99
19
|
```
|
|
100
20
|
|
|
101
|
-
|
|
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.
|
|
21
|
+
Requires Node >= 20.
|
|
112
22
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
116
59
|
|
|
117
60
|
```sh
|
|
118
|
-
harness
|
|
119
|
-
harness
|
|
120
|
-
|
|
121
|
-
harness
|
|
61
|
+
harness ready --claim --all # pick and claim the top uncontested issue
|
|
62
|
+
harness show CLI-12 # read it, including attached screenshots
|
|
63
|
+
# … do the work …
|
|
64
|
+
harness close CLI-12 --reason "Implemented in 1a2b3c4"
|
|
122
65
|
```
|
|
123
66
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
67
|
+
**Queue.** `harness ready` lists open, unblocked issues, highest priority
|
|
68
|
+
first. Blocked issues, `hold`-labeled issues, and parents with open children
|
|
69
|
+
are excluded. `--claim` claims the first issue in that ranking that nobody
|
|
70
|
+
else holds. It tries up to 10 issues and exits `1` with "No claimable
|
|
71
|
+
ready issues" when none can be claimed. Pass `--all` so that the ranking
|
|
72
|
+
covers the whole ready set rather than just the first page. `harness
|
|
73
|
+
blocked` lists issues that are waiting on a blocker. Both commands take
|
|
74
|
+
`--team`.
|
|
75
|
+
|
|
76
|
+
**Read.** `harness show <id>` prints the fields, relations, children, and
|
|
77
|
+
comments. It also downloads the images attached to the issue's description,
|
|
78
|
+
design, acceptance criteria, notes, and comments (up to 20, cached locally)
|
|
79
|
+
and prints their paths. Screenshots are often the reproduction, so read
|
|
80
|
+
them. `--no-images` skips the downloads. If a download fails, a stderr
|
|
81
|
+
notice says so and the command still succeeds. `harness comments <id>`
|
|
82
|
+
prints the full, untruncated comment thread with comment ids.
|
|
83
|
+
`harness activity <id>` prints the issue's activity timeline: comments,
|
|
84
|
+
creation, and state and assignee changes. It shows the newest 20 entries
|
|
85
|
+
(`--limit` accepts 1–100), and `--all` reads further back.
|
|
86
|
+
|
|
87
|
+
**Create.** Creating an issue before you write code keeps the work tracked:
|
|
129
88
|
|
|
130
89
|
```sh
|
|
131
|
-
harness
|
|
90
|
+
harness create --title "Fix login redirect" --type bug --priority high \
|
|
91
|
+
--description "…" --parent CLI-3 --assignee me --due-date 2026-11-01 \
|
|
92
|
+
--label regression --idempotency-key fix-login-redirect-1
|
|
132
93
|
```
|
|
133
94
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
95
|
+
- `--type task|bug|feature|epic` is applied as a `type/<type>` label.
|
|
96
|
+
- `--priority urgent|high|medium|low|none` takes names only. Bare numbers
|
|
97
|
+
are refused because bd and Harness number priorities differently.
|
|
98
|
+
- `--team <key>` falls back to `HARNESS_TEAM` and then to the repo config.
|
|
99
|
+
With none of these set, the command is a usage error.
|
|
100
|
+
- `--label` (repeatable) looks for a matching label in the target team first
|
|
101
|
+
and then at workspace scope. Missing labels are created at workspace scope
|
|
102
|
+
(which requires Harness administrator standing) before the issue is filed,
|
|
103
|
+
and the issue is created in a single request with all its labels.
|
|
104
|
+
- `--idempotency-key <key>` (1–200 printable ASCII characters, no leading or
|
|
105
|
+
trailing space) makes a retry safe. If a create times out, run it again
|
|
106
|
+
with the same key to get the original issue back instead of a duplicate.
|
|
107
|
+
The CLI never retries a write on its own.
|
|
108
|
+
- `--design`, `--notes`, and `--acceptance` set the remaining text fields.
|
|
109
|
+
|
|
110
|
+
**Update.** `harness update <id>` with:
|
|
111
|
+
|
|
112
|
+
- `--claim`: atomic. It assigns you and moves the issue to the team's first
|
|
113
|
+
started state. A conflict means someone else holds the issue. It cannot be
|
|
114
|
+
combined with `--assignee`.
|
|
115
|
+
- `--state <name-or-type>`: moves the issue to the workflow state whose name
|
|
116
|
+
or type matches.
|
|
117
|
+
- `--title`, `--description`, `--design`, `--acceptance`, `--priority`,
|
|
118
|
+
`--parent`, `--assignee` (`""` unassigns), `--due-date` (`""` clears).
|
|
119
|
+
- `--notes` replaces the notes and `--append-notes` appends to them. Pass
|
|
120
|
+
only one of the two.
|
|
121
|
+
- `--add-label` / `--remove-label` (repeatable, name or id) add or remove
|
|
122
|
+
individual labels without replacing the rest. You can only add labels that
|
|
123
|
+
belong to the issue's team or the workspace, and a missing name is created
|
|
124
|
+
at workspace scope, as with `create`. If two attached labels match a name,
|
|
125
|
+
remove by id.
|
|
126
|
+
|
|
127
|
+
**Dependencies.** `harness dep add <issue> <blocker>`: **the second argument
|
|
128
|
+
blocks the first.** `harness dep rm <issue> <blocker>` removes the
|
|
129
|
+
dependency. For other relation types, see `relation` under
|
|
130
|
+
[Other commands](#other-commands).
|
|
131
|
+
|
|
132
|
+
**Close.** `harness close <id> [<id>…]` closes one or more issues as
|
|
133
|
+
completed. Use `--cancel` to close them as canceled, or `--duplicate-of <id>`
|
|
134
|
+
to close a single issue as a duplicate of the canonical one. The
|
|
135
|
+
duplicate-of form records a duplicate relation and cannot be combined with
|
|
136
|
+
`--cancel`. `--reason` records why the issue is done. The output lists the
|
|
137
|
+
issues each closure unblocked
|
|
138
|
+
(`Closed CLI-12 — unblocked: CLI-13, CLI-14`); act on them instead of
|
|
139
|
+
leaving them in the queue. With several ids, each one is closed on its own.
|
|
140
|
+
A failure prints `FAILED <id>: …`, and the command exits `1` after trying
|
|
141
|
+
the rest. `harness archive <id>…` archives issues, `--restore` brings them
|
|
142
|
+
back, and `harness list --scope archived` lists them.
|
|
143
|
+
|
|
144
|
+
**Comment.** `harness comment <id> "text" [--mention <name>]…` posts a
|
|
145
|
+
comment. The server resolves each mention and notifies that person.
|
|
146
|
+
`harness comment edit <id> <comment-id> "new text"` and
|
|
147
|
+
`harness comment delete <id> <comment-id>` (alias `rm`) work on your own
|
|
148
|
+
comments. Comment ids are UUIDs from `harness comments`.
|
|
149
|
+
|
|
150
|
+
**Find.** There are two search commands:
|
|
151
|
+
|
|
152
|
+
- `harness search <question…>` searches by meaning. It returns a ranked list
|
|
153
|
+
with an exact identifier first, followed by results ordered by relevance
|
|
154
|
+
across a lexical and a semantic signal, so it can find issues that share
|
|
155
|
+
none of your words. Write the question in plain language instead of
|
|
156
|
+
keywords. Results default to 10 and are capped at 20, with no pages
|
|
157
|
+
(`--page`/`--all` are accepted but ignored). Archived issues are not
|
|
158
|
+
searched. If the semantic side is unavailable, stderr says
|
|
159
|
+
`semantic search unavailable — showing lexical matches only`.
|
|
160
|
+
- `harness list --search <text>` is the paginated lexical filter, which
|
|
161
|
+
matches title or issue number.
|
|
162
|
+
|
|
163
|
+
`harness list` filters: `--status open|in_progress|closed`,
|
|
164
|
+
`--assignee <me|id|email|name|"">` (`""` means unassigned),
|
|
165
|
+
`--created-by <me|id>`, `--involved me`, `--priority <name>`,
|
|
166
|
+
`--label <name>` or `--type <type>` (only one of the two), `--team <key>`,
|
|
167
|
+
`--parent <id>` (children of an epic), and `--scope all|archived`.
|
|
168
|
+
|
|
169
|
+
**Overview.** `harness stats` prints per-team and total counts.
|
|
170
|
+
`harness prime` prints those counts, the top of the ready queue, and a
|
|
171
|
+
short workflow reminder. It is meant for the start of an agent session.
|
|
172
|
+
|
|
173
|
+
### Text from a file
|
|
174
|
+
|
|
175
|
+
When text comes from somewhere other than your own typing, such as issue
|
|
176
|
+
content, a commit message, or model output, pass a file instead of a shell
|
|
177
|
+
argument. Quoting can be defeated: a `"` ends the string, `$(…)`
|
|
178
|
+
substitutes, and a heredoc ends at any flush-left delimiter line the text
|
|
179
|
+
happens to contain.
|
|
142
180
|
|
|
143
181
|
```sh
|
|
144
|
-
harness
|
|
145
|
-
harness update
|
|
182
|
+
harness create --title "…" --description-file ./body.md
|
|
183
|
+
harness update CLI-12 --notes-file ./notes.md --acceptance-file ./ac.md
|
|
184
|
+
harness comment CLI-12 --body-file ./comment.md
|
|
185
|
+
git log -1 --format=%B | harness close CLI-12 --reason-file -
|
|
146
186
|
```
|
|
147
187
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
188
|
+
| Command | File options |
|
|
189
|
+
| --- | --- |
|
|
190
|
+
| `create` | `--title-file`, `--description-file`, `--design-file`, `--notes-file`, `--acceptance-file` |
|
|
191
|
+
| `update` | the same five, plus `--append-notes-file` |
|
|
192
|
+
| `close` | `--reason-file` |
|
|
193
|
+
| `comment`, `comment edit` | `--body-file` |
|
|
194
|
+
|
|
195
|
+
Other commands (`project`, `milestone`, `doc`, `link`, and so on) take inline
|
|
196
|
+
values only. `-` reads from stdin, and only one option per invocation can do
|
|
197
|
+
that. stdin must not be a terminal. Passing both the inline and the file form
|
|
198
|
+
of a field is a usage error, and so is a file that produces an empty title,
|
|
199
|
+
reason, or comment. Conflicting options are caught before anything is read,
|
|
200
|
+
so a rejected command never consumes your pipe. A file must be valid UTF-8
|
|
201
|
+
and at most 1 MB, and one trailing newline is dropped, as with
|
|
202
|
+
`$(cat file)`.
|
|
203
|
+
|
|
204
|
+
## Uploads
|
|
205
|
+
|
|
206
|
+
`harness upload <path>` uploads a file and prints a markdown reference to
|
|
207
|
+
paste into a description or comment. `--json` returns the URL, filename,
|
|
208
|
+
content type, byte size, and markdown instead.
|
|
209
|
+
|
|
210
|
+
- Images (PNG, JPG, WebP, GIF) can be up to 25 MB.
|
|
211
|
+
- Videos must be `.mp4` files of up to 100 MB, with H.264 8-bit 4:2:0 video,
|
|
212
|
+
optional AAC audio, and optional mov_text subtitles. The server validates
|
|
213
|
+
the media and does not transcode other formats. Harness shows linked
|
|
214
|
+
videos as an inline player.
|
|
215
|
+
|
|
216
|
+
A new video is private to its uploader until that user saves its link in an
|
|
217
|
+
issue description or comment. After that it follows the access policy of
|
|
218
|
+
the issue that holds it, and reusing the link elsewhere doesn't change who
|
|
219
|
+
owns it. `harness show` downloads images only, never videos. The older
|
|
220
|
+
`attach-image <path>` command accepts images only (25 MB).
|
|
221
|
+
|
|
222
|
+
## Other commands
|
|
223
|
+
|
|
224
|
+
| Command | Notes |
|
|
225
|
+
| --- | --- |
|
|
226
|
+
| `relation list <issue>` | Every typed relation, with relation ids. |
|
|
227
|
+
| `relation add <issue> <other> --type <t>` | `blocks`, `blocked_by`, `related`, `duplicate`, `duplicate_of`, `duplicated_by`. |
|
|
228
|
+
| `relation update <issue> <relation-id-or-issue> --type <t>` | `blocks`, `blocked_by`, `related`, `duplicate_of`. |
|
|
229
|
+
| `relation rm <issue> <relation-id-or-issue>` | If more than one relation matches the other issue, pass the relation id. |
|
|
230
|
+
| `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. |
|
|
231
|
+
| `project list\|show\|create\|update` | `list [--search] [--state]` (paginated). `create --name … [--description] [--summary] [--state] [--lead <user-id>] [--start-date] [--target-date]`. |
|
|
232
|
+
| `milestone list\|create --project <id>` | `create … --name … [--description] [--target-date]`. |
|
|
233
|
+
| `doc list\|show\|create\|update` | `list [--search]` (paginated). `create --title … [--content] [--project <id>]`. |
|
|
234
|
+
| `label list\|create\|update\|delete` | `create <name> [--color]`, `update <id> [--name] [--color]`, `delete <id> --yes` (refused while issues carry the label). |
|
|
235
|
+
| `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. |
|
|
236
|
+
| `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`. |
|
|
237
|
+
| `member list\|add\|set-role` | The org's tracker roster. `add --email … --role admin\|staff\|collaborator`, `set-role <user-id> --role …\|none` (`none` revokes). |
|
|
238
|
+
| `user list` | Users, for resolving assignees. |
|
|
239
|
+
| `version` | CLI and server API versions. Works before you log in, and so does `harness --version`. |
|
|
240
|
+
|
|
241
|
+
`team rename`, `team set-default`, `label update`, `label delete`, and the
|
|
242
|
+
write commands of `state` and `member` require Harness administrator
|
|
243
|
+
standing, and so does `member list`.
|
|
244
|
+
|
|
245
|
+
## Provider keys
|
|
155
246
|
|
|
156
|
-
|
|
247
|
+
Admins store the org's provider API keys (Anthropic, OpenAI, Google,
|
|
248
|
+
OpenRouter, Brave Search, Perplexity) in Harness under `Settings → Model
|
|
249
|
+
keys`. `harness keys` passes them to tools without ever printing them:
|
|
157
250
|
|
|
158
|
-
|
|
251
|
+
- `harness keys list` shows the provider slugs the org has keys for and the
|
|
252
|
+
environment variable each one maps to (`--json` returns
|
|
253
|
+
`{providers: [{provider, env}]}`). It never prints the values.
|
|
254
|
+
- `harness keys exec [--only <slugs>] [--override] -- <command> [args…]`
|
|
255
|
+
runs the command with the keys in its environment and exits with the
|
|
256
|
+
command's exit code (128 + the signal number if the command was killed by a
|
|
257
|
+
signal). `--only brave,perplexity` passes just those providers and also
|
|
258
|
+
removes your shell's own copies of the other provider variables, so the
|
|
259
|
+
command sees only what it needs. If the org has no key for a provider you
|
|
260
|
+
requested, your shell's variable is kept and stderr says so. Slugs are
|
|
261
|
+
trimmed and case-insensitive. An `--only` list that names nothing
|
|
262
|
+
(`--only ,`) is a usage error, not a request for everything. Without
|
|
263
|
+
`--override`, a variable that is already set in your shell wins (a blank
|
|
264
|
+
variable counts as unset). `--json` is accepted but has no effect, because
|
|
265
|
+
stdout belongs to the command. A SIGTERM sent to `harness` is forwarded to
|
|
266
|
+
the command. Ctrl+C reaches the command directly from the terminal, and
|
|
267
|
+
`harness` waits to report its exit. Works on macOS and Linux only.
|
|
268
|
+
|
|
269
|
+
Each slug maps to the same variable as in rcl: anthropic → `ANTHROPIC_API_KEY`,
|
|
270
|
+
openai → `OPENAI_API_KEY`, google → `GEMINI_API_KEY`, openrouter →
|
|
271
|
+
`OPENROUTER_API_KEY`, brave → `BRAVE_API_KEY`, perplexity →
|
|
272
|
+
`PERPLEXITY_API_KEY`. Reading keys requires the `model-keys:read` scope.
|
|
273
|
+
Login tokens have it; `aone_` tokens need to be minted with it. There is
|
|
274
|
+
deliberately no MCP tool for keys, because a tool result is text the model
|
|
275
|
+
can read.
|
|
276
|
+
|
|
277
|
+
## Scripting contract
|
|
278
|
+
|
|
279
|
+
- **`--json` on every command** except `mcp`. It replaces the human output
|
|
280
|
+
with the response data as JSON. `keys exec` accepts it but changes
|
|
281
|
+
nothing: its stdout is the child command's output.
|
|
282
|
+
- **stdout carries data only**, so it is safe to pipe. Diagnostics go to
|
|
283
|
+
stderr:
|
|
284
|
+
- `→ https://harness.infra.one` on every command that writes, naming the
|
|
285
|
+
host it sent the write to.
|
|
286
|
+
- `harness: showing 50 of 132 (page 1 of 3) — pass --all for the rest, or --page to step through.`
|
|
287
|
+
when a paginated read did not return everything, and
|
|
288
|
+
`harness: --limit N was capped at M by the server.` when the server
|
|
289
|
+
reduced the page size you asked for.
|
|
290
|
+
- The `activity`, `search`, and `show` notices described above.
|
|
291
|
+
- The update notice (see [Staying current](#staying-current)).
|
|
292
|
+
- **Pagination.** `ready`, `blocked`, `list`, `project list`, and `doc list`
|
|
293
|
+
return 50 records per page by default. `--limit` changes the page size,
|
|
294
|
+
`--page` selects a page, and `--all` follows every page (pass `--all` or
|
|
295
|
+
`--page`, not both). The truncation notice goes to stderr only, so a
|
|
296
|
+
script that reads just the `--json` output cannot tell a full page from a
|
|
297
|
+
complete result. Pass `--all` when you need everything.
|
|
298
|
+
- **Exit codes.** `keys exec` returns the command's exit code instead.
|
|
299
|
+
|
|
300
|
+
| Code | Meaning |
|
|
301
|
+
| --- | --- |
|
|
302
|
+
| `0` | Success. |
|
|
303
|
+
| `1` | The command failed. Causes include an API error (the message gives the HTTP status), a failure such as nothing being claimable, a partial failure across several ids, or a local refusal such as an unreadable or oversized upload. |
|
|
304
|
+
| `2` | Usage or configuration error, including "not signed in" outside a terminal. Nothing was sent. |
|
|
305
|
+
| `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. |
|
|
306
|
+
|
|
307
|
+
## MCP server
|
|
308
|
+
|
|
309
|
+
`harness mcp` runs an MCP server over stdio. To register it with Claude Code:
|
|
159
310
|
|
|
160
311
|
```sh
|
|
161
|
-
|
|
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 -
|
|
312
|
+
claude mcp add --scope user harness -- npx -y @allocator-one/harness-cli mcp
|
|
165
313
|
```
|
|
166
314
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
`--reason-file`, `comment` takes `--body-file`.
|
|
170
|
-
|
|
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.
|
|
177
|
-
|
|
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)`.
|
|
315
|
+
With a global install, `harness mcp` works as the command instead of the
|
|
316
|
+
`npx` form. Any other MCP client can use the same command:
|
|
182
317
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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.
|
|
189
|
-
|
|
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.
|
|
193
|
-
|
|
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.
|
|
318
|
+
```json
|
|
319
|
+
{ "mcpServers": { "harness": { "command": "npx", "args": ["-y", "@allocator-one/harness-cli", "mcp"] } } }
|
|
320
|
+
```
|
|
199
321
|
|
|
200
|
-
|
|
322
|
+
The server exposes 52 tools:
|
|
201
323
|
|
|
202
|
-
|
|
203
|
-
|
|
324
|
+
| Area | Tools |
|
|
325
|
+
| --- | --- |
|
|
326
|
+
| Issues | `issue_list`, `issue_search`, `issue_show`, `activity_list`, `issue_create`, `issue_update`, `issue_claim`, `issue_close`, `issue_archive` |
|
|
327
|
+
| Comments | `issue_comment`, `issue_comment_edit`, `issue_comment_delete`, `comment_list` |
|
|
328
|
+
| Relations | `relation_add`, `relation_rm` (blocking shorthand: `blocker` blocks `issue`), `relation_list`, `relation_create`, `relation_update`, `relation_delete` |
|
|
329
|
+
| Links | `link_list`, `link_add`, `link_update`, `link_rm` |
|
|
330
|
+
| Projects and docs | `project_list`, `project_show`, `project_create`, `project_update`, `milestone_list`, `milestone_create`, `document_list`, `document_show`, `document_create`, `document_update` |
|
|
331
|
+
| Labels | `label_list`, `label_create`, `label_update`, `label_delete` |
|
|
332
|
+
| Teams and states | `team_list`, `team_create`, `team_rename`, `team_set_default`, `workflow_states`, `state_add`, `state_update`, `state_order`, `state_delete` |
|
|
333
|
+
| People | `member_list`, `member_add`, `member_set_role`, `user_list` |
|
|
334
|
+
| Other | `stats`, `upload` |
|
|
335
|
+
|
|
336
|
+
The tools cover the issue workflow and the admin surface. Some commands are
|
|
337
|
+
CLI-only: `keys` (deliberately), `login`/`logout`/`whoami`, `version`, and
|
|
338
|
+
`prime`. Read-only tools carry `readOnlyHint`. Each result is compact JSON
|
|
339
|
+
of the response data. Any stderr notice the CLI would print, such as
|
|
340
|
+
truncation, is added as a second text item. A partial failure across several
|
|
341
|
+
ids is returned as an error, with the details for each id in the JSON.
|
|
342
|
+
Some tools behave differently from their commands:
|
|
343
|
+
|
|
344
|
+
- `issue_list` with `ready: true` or `blocked: true` accepts every list
|
|
345
|
+
filter. The CLI's `ready` and `blocked` take only `--team`.
|
|
346
|
+
- `issue_show` downloads images only when you pass `images: true`.
|
|
347
|
+
- `state_delete` and `label_delete` don't need the CLI's `--yes`.
|
|
348
|
+
|
|
349
|
+
For example, `issue_list` with `{"ready": true, "team": "CLI"}` returns the
|
|
350
|
+
ready queue, and `issue_claim` with `{"id": "CLI-12"}` claims an issue
|
|
351
|
+
atomically. `issue_close` takes `ids`, `reason`, `cancel`, and
|
|
352
|
+
`duplicate_of`, and `issue_create` takes an `idempotency_key` for safe
|
|
353
|
+
retries.
|
|
354
|
+
|
|
355
|
+
The MCP server resolves auth exactly as the CLI does, on every call, so a
|
|
356
|
+
login completed while the server is running takes effect without a restart.
|
|
357
|
+
It never opens a browser. Without a credential, a tool returns an error that
|
|
358
|
+
tells the agent to run `harness login` in a terminal or set
|
|
359
|
+
`HARNESS_API_TOKEN`.
|
|
360
|
+
|
|
361
|
+
## Configuration
|
|
362
|
+
|
|
363
|
+
### Repo config
|
|
364
|
+
|
|
365
|
+
A committed `.harness-cli/config.json`, found by walking up from the working
|
|
366
|
+
directory the way git finds `.git`, gives every clone of a repo the same
|
|
367
|
+
defaults:
|
|
204
368
|
|
|
205
369
|
```json
|
|
206
370
|
{ "team": "CLI" }
|
|
@@ -208,47 +372,90 @@ from the working directory, gives every clone of a repo its defaults:
|
|
|
208
372
|
|
|
209
373
|
| Field | Effect |
|
|
210
374
|
| --- | --- |
|
|
211
|
-
| `team` | Default team key. Precedence: `--team
|
|
212
|
-
| `url` | Target host. `HARNESS_API_URL`
|
|
213
|
-
| `org` | Reserved
|
|
214
|
-
|
|
215
|
-
This is config, not state — issues stay server-side.
|
|
216
|
-
|
|
217
|
-
## Development
|
|
375
|
+
| `team` | Default team key. Precedence: `--team`, then `HARNESS_TEAM`, then the repo config. |
|
|
376
|
+
| `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. |
|
|
377
|
+
| `org` | Reserved. It is parsed but has no effect yet. |
|
|
218
378
|
|
|
219
|
-
|
|
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
|
-
```
|
|
379
|
+
A malformed file is ignored with a warning on stderr.
|
|
226
380
|
|
|
227
|
-
|
|
228
|
-
generated into `src/generated/` (committed, so npx consumers never run
|
|
229
|
-
codegen):
|
|
381
|
+
### Hosts and tokens
|
|
230
382
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
383
|
+
The host comes from `HARNESS_API_URL`, then the repo config's `url`, then
|
|
384
|
+
the host of the stored login, then `https://harness.infra.one`. Tokens are
|
|
385
|
+
bound to the host that issued them. A stored login is only sent to the host
|
|
386
|
+
that issued it. `HARNESS_API_TOKEN` is only sent to `HARNESS_API_URL`, or to
|
|
387
|
+
the default host when that variable is unset. It is never sent to the stored
|
|
388
|
+
login's host, and when a repo config names another host, the command
|
|
389
|
+
refuses with exit code 2 instead of sending the token there. Set
|
|
390
|
+
`HARNESS_API_URL` explicitly if you do mean that host. Without
|
|
391
|
+
`HARNESS_API_TOKEN`, a repo-named host needs a login of its own, and the
|
|
392
|
+
automatic login asks for confirmation first, as described above. Plain
|
|
393
|
+
`http://` is accepted only for localhost.
|
|
235
394
|
|
|
236
|
-
|
|
395
|
+
### Environment variables
|
|
237
396
|
|
|
238
|
-
|
|
397
|
+
| Variable | Effect |
|
|
398
|
+
| --- | --- |
|
|
399
|
+
| `HARNESS_API_URL` | Target host (https, or http against localhost only). |
|
|
400
|
+
| `HARNESS_API_TOKEN` | An `aone_…` API token. It takes precedence over the stored login. Headless use only. |
|
|
401
|
+
| `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. |
|
|
402
|
+
| `HARNESS_TEAM` | Default team key for commands that take `--team`. |
|
|
403
|
+
| `HARNESS_CACHE_DIR` | Where `show` stores downloaded images. Must be an absolute path. |
|
|
404
|
+
| `HARNESS_NO_UPDATE_CHECK` | Set to a value other than `0`, `false`, or `no` to turn off the update notice and the automatic upgrade. |
|
|
405
|
+
| `HARNESS_AUTO_UPGRADE=0` | Keeps the notice but never installs anything (`false` and `no` also work). |
|
|
406
|
+
| `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. |
|
|
407
|
+
| `XDG_CONFIG_HOME`, `XDG_CACHE_HOME` | Used instead of `~/.config` and `~/.cache` when set to absolute paths. |
|
|
239
408
|
|
|
240
|
-
|
|
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:
|
|
409
|
+
### Files
|
|
244
410
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
411
|
+
| Path | Contents |
|
|
412
|
+
| --- | --- |
|
|
413
|
+
| `~/.config/harness/credentials.json` | The stored login: one `{url, token}` pair, mode 0600. |
|
|
414
|
+
| `~/.config/harness/trusted-hosts.json` | Repo-config hosts you agreed to log in to. |
|
|
415
|
+
| `~/.cache/harness/attachments/` | Images downloaded by `show` (or `HARNESS_CACHE_DIR`). |
|
|
416
|
+
| `~/.cache/harness/update-check.json` | The daily update-check stamp. |
|
|
417
|
+
| `~/.cache/harness/upgrade.lock` | Held while an automatic upgrade runs. |
|
|
418
|
+
|
|
419
|
+
None of these files is tracker state: issues stay on the server. The
|
|
420
|
+
attachment cache does hold copies of issue images, so clear it when you no
|
|
421
|
+
longer need them.
|
|
422
|
+
|
|
423
|
+
## Staying current
|
|
424
|
+
|
|
425
|
+
**Notice.** Each run reads the cached stamp and prints at most one stderr
|
|
426
|
+
line when a newer release exists:
|
|
427
|
+
`harness: 1.13.0 is available (you have 1.12.1) — npm i -g @allocator-one/harness-cli@latest`.
|
|
428
|
+
When the stamp is more than a day old, a detached background process
|
|
429
|
+
refreshes it from the npm registry. A command never waits on that network
|
|
430
|
+
request. The notice applies only to global and project-local installs, not
|
|
431
|
+
to `npx` or a git checkout.
|
|
432
|
+
|
|
433
|
+
**Automatic upgrade on HTTP 426.** The CLI sends its version with every
|
|
434
|
+
request, and the server refuses versions below its minimum with a 426. In
|
|
435
|
+
that case the CLI runs
|
|
436
|
+
`npm install --global --ignore-scripts @allocator-one/harness-cli@latest`,
|
|
437
|
+
re-runs the original command, and exits with that command's exit code.
|
|
438
|
+
Re-running is safe only for this status. The server answers 426 before
|
|
439
|
+
authentication and before the request handler, so the refused request
|
|
440
|
+
changed nothing. The CLI never retries any other failure. The upgrade is
|
|
441
|
+
limited by these guards:
|
|
442
|
+
|
|
443
|
+
- It only upgrades a global install, located through `npm root -g`, and
|
|
444
|
+
only when that directory is writable.
|
|
445
|
+
- Only one upgrade runs at a time on a machine (a lock under
|
|
446
|
+
`~/.cache/harness`). A process that finds a newer version already
|
|
447
|
+
installed re-runs with it directly.
|
|
448
|
+
- `--ignore-scripts` means an unattended upgrade never runs package scripts.
|
|
449
|
+
- The re-run carries `HARNESS_UPGRADED=1`, so each command is upgraded at
|
|
450
|
+
most once.
|
|
451
|
+
- It is skipped for `npx`, project-local installs, git checkouts, inside
|
|
452
|
+
`harness mcp`, under `CI`, and when `HARNESS_NO_UPDATE_CHECK` or
|
|
453
|
+
`HARNESS_AUTO_UPGRADE=0` is set.
|
|
454
|
+
|
|
455
|
+
When the upgrade is skipped or fails, the command prints the server's
|
|
456
|
+
message, adds the reason if the upgrade was attempted, and exits `1`.
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
Building, testing, and releasing this package are covered in `AGENTS.md`
|
|
461
|
+
in the (private) source repository.
|
|
@@ -83,8 +83,13 @@ var Client = class {
|
|
|
83
83
|
async get(path2, params) {
|
|
84
84
|
return this.json(await this.raw("GET", path2, { params }));
|
|
85
85
|
}
|
|
86
|
-
async post(path2, body = {}) {
|
|
87
|
-
return this.json(
|
|
86
|
+
async post(path2, body = {}, options = {}) {
|
|
87
|
+
return this.json(
|
|
88
|
+
await this.raw("POST", path2, {
|
|
89
|
+
json: this.withActor(body),
|
|
90
|
+
idempotencyKey: options.idempotencyKey
|
|
91
|
+
})
|
|
92
|
+
);
|
|
88
93
|
}
|
|
89
94
|
async patch(path2, body) {
|
|
90
95
|
return this.json(await this.raw("PATCH", path2, { json: this.withActor(body) }));
|
|
@@ -138,6 +143,9 @@ var Client = class {
|
|
|
138
143
|
authorization: `Bearer ${this.config.token}`,
|
|
139
144
|
"x-harness-cli-version": CLI_VERSION
|
|
140
145
|
};
|
|
146
|
+
if (options.idempotencyKey !== void 0) {
|
|
147
|
+
headers["idempotency-key"] = options.idempotencyKey;
|
|
148
|
+
}
|
|
141
149
|
let body;
|
|
142
150
|
if (options.json !== void 0) {
|
|
143
151
|
headers["content-type"] = "application/json";
|
|
@@ -664,6 +672,8 @@ var STATUS_STATE_TYPES = {
|
|
|
664
672
|
closed: "completed,canceled"
|
|
665
673
|
};
|
|
666
674
|
var READY_CLAIM_ATTEMPTS = 10;
|
|
675
|
+
var MAX_IDEMPOTENCY_KEY_BYTES = 200;
|
|
676
|
+
var IDEMPOTENCY_KEY_FORMAT = /^[\x21-\x7e](?:[\x20-\x7e]*[\x21-\x7e])?(?![\s\S])/;
|
|
667
677
|
var IDENTIFIER_FORMAT = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
|
|
668
678
|
var UUID_FORMAT = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
669
679
|
function isUuid(value) {
|
|
@@ -810,6 +820,7 @@ async function create(ctx, opts) {
|
|
|
810
820
|
}
|
|
811
821
|
validateType(opts.type);
|
|
812
822
|
const team = requiredTeam(ctx, opts.team);
|
|
823
|
+
validateIdempotencyKey(opts.idempotencyKey);
|
|
813
824
|
const priority = optionalPriority(opts.priority);
|
|
814
825
|
const dueDate = optionalDueDate(opts.dueDate);
|
|
815
826
|
const labels = requestedLabelNames(wantedLabels(opts));
|
|
@@ -824,7 +835,9 @@ async function create(ctx, opts) {
|
|
|
824
835
|
body = putDueDate(body, dueDate);
|
|
825
836
|
body = putAssignee(body, opts.assignee);
|
|
826
837
|
if (labelIds.length > 0) body["label_ids"] = labelIds;
|
|
827
|
-
const created = await ctx.client.post(`/api/v1/teams/${team}/issues`, body
|
|
838
|
+
const created = await ctx.client.post(`/api/v1/teams/${team}/issues`, body, {
|
|
839
|
+
idempotencyKey: opts.idempotencyKey
|
|
840
|
+
});
|
|
828
841
|
const issue = created.data;
|
|
829
842
|
return {
|
|
830
843
|
json: issue,
|
|
@@ -832,6 +845,16 @@ async function create(ctx, opts) {
|
|
|
832
845
|
`
|
|
833
846
|
};
|
|
834
847
|
}
|
|
848
|
+
function validateIdempotencyKey(key) {
|
|
849
|
+
if (key === void 0) return;
|
|
850
|
+
if (key.trim() === "") throw new UsageError("idempotency key must not be blank");
|
|
851
|
+
if (Buffer.byteLength(key, "utf8") > MAX_IDEMPOTENCY_KEY_BYTES) {
|
|
852
|
+
throw new UsageError(`idempotency key must not exceed ${MAX_IDEMPOTENCY_KEY_BYTES} UTF-8 bytes`);
|
|
853
|
+
}
|
|
854
|
+
if (!IDEMPOTENCY_KEY_FORMAT.test(key)) {
|
|
855
|
+
throw new UsageError("idempotency key must use printable ASCII and cannot start or end with a space");
|
|
856
|
+
}
|
|
857
|
+
}
|
|
835
858
|
async function update(ctx, identifier, opts) {
|
|
836
859
|
requireIdentifiers([identifier]);
|
|
837
860
|
if (opts.notes !== void 0 && opts.appendNotes !== void 0) {
|
|
@@ -896,14 +919,28 @@ ${opts.appendNotes}`;
|
|
|
896
919
|
`;
|
|
897
920
|
return { json: issue, human };
|
|
898
921
|
}
|
|
899
|
-
|
|
922
|
+
function requireCloseArgs(identifiers, opts) {
|
|
900
923
|
if (identifiers.length === 0) {
|
|
901
|
-
throw new UsageError(
|
|
924
|
+
throw new UsageError(
|
|
925
|
+
"usage: harness close <id> [<id>\u2026] [--reason <text>] [--cancel | --duplicate-of <id>]"
|
|
926
|
+
);
|
|
902
927
|
}
|
|
903
928
|
requireIdentifiers(identifiers);
|
|
929
|
+
if (opts.duplicateOf === void 0) return;
|
|
930
|
+
requireIdentifiers([opts.duplicateOf]);
|
|
931
|
+
if (opts.cancel === true) {
|
|
932
|
+
throw new UsageError("--duplicate-of and --cancel are mutually exclusive");
|
|
933
|
+
}
|
|
934
|
+
if (identifiers.length !== 1) {
|
|
935
|
+
throw new UsageError("--duplicate-of accepts exactly one source issue");
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
async function close(ctx, identifiers, opts) {
|
|
939
|
+
requireCloseArgs(identifiers, opts);
|
|
904
940
|
let body = {};
|
|
905
941
|
body = putPresent(body, "reason", opts.reason);
|
|
906
942
|
body = putPresent(body, "cancel", opts.cancel);
|
|
943
|
+
body = putPresent(body, "duplicate_of", opts.duplicateOf);
|
|
907
944
|
const results = [];
|
|
908
945
|
for (const identifier of identifiers) {
|
|
909
946
|
try {
|
|
@@ -917,22 +954,28 @@ async function close(ctx, identifiers, opts) {
|
|
|
917
954
|
}
|
|
918
955
|
}
|
|
919
956
|
}
|
|
920
|
-
const human = results.map((result) => closeLine(result)).join("");
|
|
957
|
+
const human = results.map((result) => closeLine(result, opts.duplicateOf)).join("");
|
|
921
958
|
const json2 = results.map(
|
|
922
|
-
(result) => result.ok ? {
|
|
959
|
+
(result) => result.ok ? {
|
|
960
|
+
identifier: result.identifier,
|
|
961
|
+
closed: true,
|
|
962
|
+
...opts.duplicateOf === void 0 ? {} : { duplicate_of: opts.duplicateOf },
|
|
963
|
+
unblocked: result.response["unblocked"] ?? null
|
|
964
|
+
} : { identifier: result.identifier, closed: false, error: errorText(result.error) }
|
|
923
965
|
);
|
|
924
966
|
const partial = results.some((result) => !result.ok);
|
|
925
967
|
return partial ? { json: json2, human, partial: true } : { json: json2, human };
|
|
926
968
|
}
|
|
927
|
-
function closeLine(result) {
|
|
969
|
+
function closeLine(result, duplicateOf) {
|
|
928
970
|
if (!result.ok) return `FAILED ${result.identifier}: ${errorText(result.error)}
|
|
929
971
|
`;
|
|
972
|
+
const closure = duplicateOf === void 0 ? `Closed ${result.identifier}` : `Closed ${result.identifier} as duplicate of ${duplicateOf}`;
|
|
930
973
|
const unblocked = result.response["unblocked"];
|
|
931
974
|
if (Array.isArray(unblocked) && unblocked.length > 0) {
|
|
932
|
-
return
|
|
975
|
+
return `${closure} \u2014 unblocked: ${unblocked.map(String).join(", ")}
|
|
933
976
|
`;
|
|
934
977
|
}
|
|
935
|
-
return
|
|
978
|
+
return `${closure}
|
|
936
979
|
`;
|
|
937
980
|
}
|
|
938
981
|
async function archive(ctx, identifiers, opts) {
|
|
@@ -2137,6 +2180,8 @@ export {
|
|
|
2137
2180
|
Client,
|
|
2138
2181
|
clean,
|
|
2139
2182
|
json,
|
|
2183
|
+
MAX_IDEMPOTENCY_KEY_BYTES,
|
|
2184
|
+
IDEMPOTENCY_KEY_FORMAT,
|
|
2140
2185
|
requireIdentifiers,
|
|
2141
2186
|
ready,
|
|
2142
2187
|
blocked,
|
|
@@ -2150,6 +2195,7 @@ export {
|
|
|
2150
2195
|
version,
|
|
2151
2196
|
create,
|
|
2152
2197
|
update,
|
|
2198
|
+
requireCloseArgs,
|
|
2153
2199
|
close,
|
|
2154
2200
|
archive,
|
|
2155
2201
|
comments,
|
package/dist/cli.js
CHANGED
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
relationList,
|
|
60
60
|
relationRm,
|
|
61
61
|
relationUpdate,
|
|
62
|
+
requireCloseArgs,
|
|
62
63
|
requireCommentId,
|
|
63
64
|
requireIdentifiers,
|
|
64
65
|
resolveCacheHome,
|
|
@@ -82,7 +83,7 @@ import {
|
|
|
82
83
|
version,
|
|
83
84
|
workflowStates,
|
|
84
85
|
writeCredentials
|
|
85
|
-
} from "./chunk-
|
|
86
|
+
} from "./chunk-XFMECLOU.js";
|
|
86
87
|
|
|
87
88
|
// src/cli.ts
|
|
88
89
|
import { createInterface as createInterface2 } from "readline";
|
|
@@ -1676,7 +1677,7 @@ function requireNonBlankTitle(text, titleFile) {
|
|
|
1676
1677
|
program.command("create").description("create an issue atomically after resolving requested labels").option("--title <text>", "issue title (required)").option("--description <text>", "problem statement").option("--design <text>", "design notes").option("--notes <text>", "working notes").option("--acceptance <text>", "acceptance criteria").option("--title-file <path>", 'read the title from a file ("-" for stdin)').option("--description-file <path>", 'read the description from a file ("-" for stdin)').option("--design-file <path>", 'read the design notes from a file ("-" for stdin)').option("--notes-file <path>", 'read the notes from a file ("-" for stdin)').option("--acceptance-file <path>", 'read the acceptance criteria from a file ("-" for stdin)').option(
|
|
1677
1678
|
"--type <type>",
|
|
1678
1679
|
"task | bug | feature | epic (applied as a type/ label)"
|
|
1679
|
-
).option("--priority <name>", "urgent | high | medium | low | none").option("--due-date <date>", "YYYY-MM-DD").option("--parent <id>", "parent epic").option("--assignee <who>", "me, a user id, an email, or a full name").option("--team <key>", "team key (or HARNESS_TEAM)").option(
|
|
1680
|
+
).option("--priority <name>", "urgent | high | medium | low | none").option("--due-date <date>", "YYYY-MM-DD").option("--parent <id>", "parent epic").option("--assignee <who>", "me, a user id, an email, or a full name").option("--team <key>", "team key (or HARNESS_TEAM)").option("--idempotency-key <key>", "reuse the same key when deliberately retrying this create").option(
|
|
1680
1681
|
"--label <name>",
|
|
1681
1682
|
"resolve team-first, then workspace; create missing workspace labels before the issue (repeatable)",
|
|
1682
1683
|
collect,
|
|
@@ -1726,15 +1727,15 @@ program.command("update").description("update an issue; --claim takes it atomica
|
|
|
1726
1727
|
});
|
|
1727
1728
|
}
|
|
1728
1729
|
);
|
|
1729
|
-
program.command("close").description("close issues; the response lists newly unblocked issues").argument("[ids...]").option("--reason <text>", "why it is done (e.g. the commit hash)").option("--reason-file <path>", 'read the reason from a file ("-" for stdin)').option("--cancel", "close as canceled instead of completed").option("--json", "emit JSON").action(
|
|
1730
|
+
program.command("close").description("close issues; the response lists newly unblocked issues").argument("[ids...]").option("--reason <text>", "why it is done (e.g. the commit hash)").option("--reason-file <path>", 'read the reason from a file ("-" for stdin)').option("--cancel", "close as canceled instead of completed").option("--duplicate-of <id>", "close one issue as a duplicate of the canonical issue").option("--json", "emit JSON").action(
|
|
1730
1731
|
async (ids, opts) => {
|
|
1732
|
+
if (ids.length === 0) {
|
|
1733
|
+
throw new UsageError(
|
|
1734
|
+
"usage: harness close <id> [<id>\u2026] [--reason <text> | --reason-file <path>] [--cancel | --duplicate-of <id>]"
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
requireCloseArgs(ids, opts);
|
|
1731
1738
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
1732
|
-
if (ids.length === 0) {
|
|
1733
|
-
throw new UsageError(
|
|
1734
|
-
"usage: harness close <id> [<id>\u2026] [--reason <text> | --reason-file <path>] [--cancel]"
|
|
1735
|
-
);
|
|
1736
|
-
}
|
|
1737
|
-
requireIdentifiers2(ids);
|
|
1738
1739
|
const text = resolveTextSources([
|
|
1739
1740
|
{ name: "reason", inline: opts.reason, file: opts.reasonFile }
|
|
1740
1741
|
]);
|
|
@@ -2093,7 +2094,7 @@ program.command("upload").description("upload an image (25 MB) or H.264 MP4 vide
|
|
|
2093
2094
|
});
|
|
2094
2095
|
});
|
|
2095
2096
|
program.command("mcp").description("run the MCP server over stdio (for agents)").action(async () => {
|
|
2096
|
-
const { startMcpServer } = await import("./server-
|
|
2097
|
+
const { startMcpServer } = await import("./server-CYBKYG5E.js");
|
|
2097
2098
|
await startMcpServer();
|
|
2098
2099
|
});
|
|
2099
2100
|
var keys = program.command("keys").description("the org's provider API keys, handed to tooling without printing them").enablePositionalOptions();
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
CLI_VERSION,
|
|
5
5
|
Client,
|
|
6
6
|
HttpError,
|
|
7
|
+
IDEMPOTENCY_KEY_FORMAT,
|
|
8
|
+
MAX_IDEMPOTENCY_KEY_BYTES,
|
|
7
9
|
RELATION_CREATE_TYPES,
|
|
8
10
|
RELATION_UPDATE_TYPES,
|
|
9
11
|
TransportError,
|
|
@@ -61,7 +63,7 @@ import {
|
|
|
61
63
|
upload,
|
|
62
64
|
userList,
|
|
63
65
|
workflowStates
|
|
64
|
-
} from "./chunk-
|
|
66
|
+
} from "./chunk-XFMECLOU.js";
|
|
65
67
|
|
|
66
68
|
// src/mcp/server.ts
|
|
67
69
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -190,13 +192,15 @@ var TOOLS = [
|
|
|
190
192
|
due_date: dateField,
|
|
191
193
|
parent: z.string().optional(),
|
|
192
194
|
assignee: z.string().optional(),
|
|
193
|
-
labels: z.array(z.string()).optional()
|
|
195
|
+
labels: z.array(z.string()).optional(),
|
|
196
|
+
idempotency_key: z.string().min(1).max(MAX_IDEMPOTENCY_KEY_BYTES).regex(IDEMPOTENCY_KEY_FORMAT, "must use printable ASCII without leading or trailing spaces").optional().describe("reuse the same printable-ASCII key when deliberately retrying this create")
|
|
194
197
|
},
|
|
195
198
|
handler: (ctx, args) => create(ctx, {
|
|
196
199
|
...args,
|
|
197
200
|
acceptance: args["acceptance_criteria"],
|
|
198
201
|
dueDate: args["due_date"],
|
|
199
|
-
label: args["labels"]
|
|
202
|
+
label: args["labels"],
|
|
203
|
+
idempotencyKey: args["idempotency_key"]
|
|
200
204
|
})
|
|
201
205
|
},
|
|
202
206
|
{
|
|
@@ -237,14 +241,19 @@ var TOOLS = [
|
|
|
237
241
|
},
|
|
238
242
|
{
|
|
239
243
|
name: "issue_close",
|
|
240
|
-
description: "Close issues. The result lists newly unblocked issues \u2014 surface them.",
|
|
244
|
+
description: "Close issues. Set duplicate_of for one source issue to close it as a duplicate; it cannot be combined with cancel. The result lists newly unblocked issues \u2014 surface them.",
|
|
241
245
|
readOnly: false,
|
|
242
246
|
shape: {
|
|
243
247
|
ids: z.array(identifier).min(1),
|
|
244
248
|
reason: z.string().optional().describe("why it is done, e.g. the commit hash"),
|
|
245
|
-
cancel: z.boolean().optional().describe("close as canceled instead of completed")
|
|
249
|
+
cancel: z.boolean().optional().describe("close as canceled instead of completed; incompatible with duplicate_of"),
|
|
250
|
+
duplicate_of: identifier.optional().describe("canonical issue; requires exactly one source id and is incompatible with cancel")
|
|
246
251
|
},
|
|
247
|
-
handler: (ctx, args) => close(ctx, args["ids"],
|
|
252
|
+
handler: (ctx, args) => close(ctx, args["ids"], {
|
|
253
|
+
reason: args["reason"],
|
|
254
|
+
cancel: args["cancel"],
|
|
255
|
+
duplicateOf: args["duplicate_of"]
|
|
256
|
+
})
|
|
248
257
|
},
|
|
249
258
|
{
|
|
250
259
|
name: "issue_comment",
|
package/package.json
CHANGED
package/spec/openapi.json
CHANGED
|
@@ -1020,7 +1020,7 @@
|
|
|
1020
1020
|
"post": {
|
|
1021
1021
|
"callbacks": {},
|
|
1022
1022
|
"deprecated": false,
|
|
1023
|
-
"description": "Requires the `harness:write` scope.",
|
|
1023
|
+
"description": "Requires the `harness:write` scope. An optional Idempotency-Key makes deliberate retries safe for the authenticated credential. An identical replay returns the current issue with 200 and Idempotency-Replayed: true; reusing the key for a different request returns 409. Keys use 1-200 printable ASCII characters and cannot begin or end with a space.",
|
|
1024
1024
|
"parameters": [
|
|
1025
1025
|
{
|
|
1026
1026
|
"description": "Team key, e.g. ALL",
|
|
@@ -1030,6 +1030,18 @@
|
|
|
1030
1030
|
"schema": {
|
|
1031
1031
|
"type": "string"
|
|
1032
1032
|
}
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
"description": "Credential-scoped printable-ASCII key for retrying this issue creation request",
|
|
1036
|
+
"in": "header",
|
|
1037
|
+
"name": "Idempotency-Key",
|
|
1038
|
+
"required": false,
|
|
1039
|
+
"schema": {
|
|
1040
|
+
"maxLength": 200,
|
|
1041
|
+
"minLength": 1,
|
|
1042
|
+
"pattern": "^[!-~](?:[ -~]*[!-~])?(?![\\s\\S])",
|
|
1043
|
+
"type": "string"
|
|
1044
|
+
}
|
|
1033
1045
|
}
|
|
1034
1046
|
],
|
|
1035
1047
|
"requestBody": {
|
|
@@ -1099,7 +1111,23 @@
|
|
|
1099
1111
|
},
|
|
1100
1112
|
"responses": {
|
|
1101
1113
|
"200": {
|
|
1102
|
-
"description": "
|
|
1114
|
+
"description": "Idempotent replay",
|
|
1115
|
+
"headers": {
|
|
1116
|
+
"Idempotency-Replayed": {
|
|
1117
|
+
"description": "True when the response replays an earlier issue creation",
|
|
1118
|
+
"required": true,
|
|
1119
|
+
"schema": {
|
|
1120
|
+
"enum": [
|
|
1121
|
+
"true"
|
|
1122
|
+
],
|
|
1123
|
+
"type": "string"
|
|
1124
|
+
},
|
|
1125
|
+
"style": "simple"
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
},
|
|
1129
|
+
"201": {
|
|
1130
|
+
"description": "Issue created"
|
|
1103
1131
|
},
|
|
1104
1132
|
"400": {
|
|
1105
1133
|
"description": "Bad request"
|
|
@@ -1113,6 +1141,9 @@
|
|
|
1113
1141
|
"404": {
|
|
1114
1142
|
"description": "Not found"
|
|
1115
1143
|
},
|
|
1144
|
+
"409": {
|
|
1145
|
+
"description": "Idempotency key reused for a different request"
|
|
1146
|
+
},
|
|
1116
1147
|
"422": {
|
|
1117
1148
|
"description": "Validation error"
|
|
1118
1149
|
}
|
|
@@ -2304,7 +2335,7 @@
|
|
|
2304
2335
|
"post": {
|
|
2305
2336
|
"callbacks": {},
|
|
2306
2337
|
"deprecated": false,
|
|
2307
|
-
"description": "Requires the `harness:write` scope. Moves the issue to the team's first completed-type state (canceled-type with `cancel`),
|
|
2338
|
+
"description": "Requires the `harness:write` scope. Moves the issue to the team's first completed-type state (canceled-type with `cancel`). With `duplicate_of`, atomically records a duplicate relation, moves to the team's duplicate-purpose state, and posts `reason`; `cancel` and `duplicate_of` are mutually exclusive. The response's `unblocked` lists identifiers of issues that became ready because this one closed.",
|
|
2308
2339
|
"parameters": [
|
|
2309
2340
|
{
|
|
2310
2341
|
"description": "Issue identifier, e.g. ALL-4901 or CLI-12 (issues minted before team-prefixed numbering carry IO-)",
|
|
@@ -2326,11 +2357,18 @@
|
|
|
2326
2357
|
"type": "string"
|
|
2327
2358
|
},
|
|
2328
2359
|
"cancel": {
|
|
2329
|
-
"description": "Move to a canceled-type state instead of completed",
|
|
2360
|
+
"description": "Move to a canceled-type state instead of completed; cannot be used with duplicate_of",
|
|
2361
|
+
"nullable": true,
|
|
2330
2362
|
"type": "boolean"
|
|
2331
2363
|
},
|
|
2364
|
+
"duplicate_of": {
|
|
2365
|
+
"description": "Canonical issue identifier. Atomically records this issue as its duplicate and closes into the team's duplicate-purpose state; cannot be used with cancel.",
|
|
2366
|
+
"nullable": true,
|
|
2367
|
+
"type": "string"
|
|
2368
|
+
},
|
|
2332
2369
|
"reason": {
|
|
2333
2370
|
"description": "Optional close reason, posted as a comment",
|
|
2371
|
+
"nullable": true,
|
|
2334
2372
|
"type": "string"
|
|
2335
2373
|
}
|
|
2336
2374
|
},
|