diff --git a/CLAUDE.md b/CLAUDE.md
index 37b9dcb..658b0f6 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -8,7 +8,7 @@ The workflow is defined by four process skills kept in the repository under `.cl
## Project state
-The MVP is implemented: `tfscli wit work-items get` works end to end, `tfscli wit work-items list` and `tfscli wit work-items get-batch` read several work items by id, `tfscli wit wiql query-by-wiql` finds work item ids with a WIQL query, and the module has one package per architecture module (see the "Project Structure" section of `docs/architecture.md`). Release 0.0.5, which predates the current command names, has been run against a live TFS instance: `auth login` and reading one work item (`get`) work there; `list`, `get-batch`, and `query-by-wiql` have not been run against a live server yet; the error paths and rich-text rendering on real data have not been checked yet — see the "Status" and "Known limitations" sections of `README.md`. Accepted work that has not started is listed in `docs/backlog.md`; work in progress goes through tasks files per the `tasks` skill.
+The MVP is implemented and released: `tfscli auth login` stores the credential, `tfscli wit work-items get` reads one work item, `tfscli wit work-items list` and `tfscli wit work-items get-batch` read several work items by id, `tfscli wit wiql query-by-wiql` finds work item ids with a WIQL query, and the module has one package per architecture module (see the "Project Structure" section of `docs/architecture.md`). Known defects are listed in the "Known limitations" section of `README.md`. Accepted work that has not started is listed in `docs/backlog.md`; work in progress goes through tasks files per the `tasks` skill.
Read these before the corresponding kind of change (the full list of project documents is in "Project documents" in `CONTRIBUTING.md`):
diff --git a/README.md b/README.md
index b676e61..c265ca3 100644
--- a/README.md
+++ b/README.md
@@ -4,26 +4,31 @@ A stateless CLI for read-only access to on-premises TFS / Azure DevOps Server, d
Every invocation hits the server: there is no daemon, no background process, and no on-disk cache.
-## Status
-
-Pre-release. Four read commands exist — `wit work-items get`, `wit work-items list`, `wit work-items get-batch`, and `wit wiql query-by-wiql`. They are covered by tests that drive the full command tree against a stub HTTP server, so the request shape, the output format, and the error contract behave as documented. Against a live TFS instance, release 0.0.5 has been checked for the main path only: `auth login` with a valid PAT and reading one work item, the command now named `wit work-items get`, succeed. `wit work-items list`, `wit work-items get-batch`, and `wit wiql query-by-wiql` have not been run against a live server yet. The error paths and rich-text rendering of a variety of real work items have not been exercised yet; expect discrepancies there, particularly in rich-text rendering.
-
-Available now:
+## Features
- `wit work-items get` — one work item as markdown, whole or narrowed by `--fields`
- `wit work-items list` and `wit work-items get-batch` — several work items by id in one request
- `wit wiql query-by-wiql` — find work items with a WIQL query
- `auth login` — store the server URL, the collection, and a PAT, checked against the server
-- Configuration through a file, environment variables, and flags
-- `--verbose` request logging and the full error contract
+- A stable error contract and `--verbose` request logging
+
+Not present in v1: write operations (out of scope) and JSON output (`--json`, planned — markdown is lossy, so writes depend on it landing first). See `docs/project-brief.md` for scope and `docs/architecture.md` for design decisions. Known defects are listed under [Known limitations](#known-limitations).
+
+## Quick start
+
+1. Download the archive for your platform from the [releases page](https://github.com/dpleshakov/tfscli/releases), unpack it, and put `tfscli` (`tfscli.exe` on Windows) in a directory on your `PATH`.
+2. In the TFS web interface, issue a personal access token for your collection with these scopes only:
+ - **Work Items (Read)**
+3. Run `tfscli auth login` in a terminal and enter the server URL, the collection, and the token.
+4. Check the setup by reading a work item you know exists:
-Not present in v1: write operations (out of scope) and JSON output (`--json`, planned — markdown is lossy, so writes depend on it landing first). See `docs/project-brief.md` for scope and `docs/architecture.md` for design decisions.
+ ```
+ tfscli wit work-items get -p MyProject 12345
+ ```
-### Known limitations
+5. Copy the `skills/tfscli` directory from the archive into the skills directory of your AI agent.
-- **HTML tables collapse.** The markdown converter runs on library defaults, which have no table support: a `
` becomes its cell text run together, without separators. A work item whose Description holds a table renders unreadably.
-- **Rich-text noise is unhandled.** @-mentions, attachment links, Word- and Outlook-pasted markup, and work-item references are converted literally, with whatever wrapper markup TFS stored. Fixing this needs samples from a real instance and is recorded in `docs/backlog.md`.
-- **Servers with anonymous access are not supported.** `auth login` has not been tested against a server that admits anonymous requests, such as Azure DevOps Server with public projects enabled. On such a server an invalid PAT may pass the login check and be stored, and the other commands would then read as the anonymous user, seeing less, without an error.
+Details for each step: [Installation](#installation) covers the archive names, checksums, and building from source; [Authentication](#authentication) covers the token scope, Git Bash on Windows, and CI; [Use with an AI agent](#use-with-an-ai-agent) lists the skills directories of the supported agents, gives the copy commands, and describes an agent without skill support. Further settings, such as a default project, are described under [Configuration](#configuration).
## Installation
@@ -67,6 +72,47 @@ GOOS=windows GOARCH=amd64 go build -o tfscli.exe ./cmd/tfscli
A binary built this way reports its version as `dev (unknown)`: the real values are stamped in by the release build.
+## Authentication
+
+A personal access token is issued for one collection, so the server URL, the collection, and the token are stored together, and the token is only ever sent to the URL and the collection stored with it.
+
+Issue the token in the TFS web interface for the collection you will log in to, with these scopes only, and leave every other scope off:
+
+- **Work Items (Read)**
+
+tfscli only reads data, so no scope beyond reading is needed. A scope limits what the token can do but does not extend what its owner can do: a work item in a project or area the user has no access to is refused with a `forbidden` error whatever the scope.
+
+Store them once per machine:
+
+```
+$ tfscli auth login
+Server URL (e.g. https://tfs.company.com:8080/tfs): https://tfs.company.com:8080/tfs
+Collection (e.g. DefaultCollection): DefaultCollection
+Personal access token:
+Logged in to https://tfs.company.com:8080/tfs, collection DefaultCollection, as Jane Doe
+```
+
+The token is typed with echo turned off. The three values are checked against the server before anything is written: a rejected token, or a URL and collection that lead nowhere, is reported as an error and leaves nothing behind. The command takes no flags, refuses `--api-version`, and sends its check without `api-version` whatever the config file or `TFSCLI_API_VERSION` say.
+
+Particular setups:
+
+- **A URL copied from the browser** often ends with the collection. When its last path segment names the collection entered next, that segment is removed, and the command says which server URL it uses instead.
+- **A server behind an internal CA.** `auth login` reads `caBundle` and `insecureSkipVerify` from the config file when there is one; see [TLS](#tls).
+- **Git Bash (mintty) on Windows.** stdin is not a console there, and the command needs an interactive terminal. Run it from Windows Terminal, PowerShell, or `cmd`, or prefix it with `winpty`.
+- **CI and other environments without a terminal.** Use `TFSCLI_AUTH`, described below.
+
+The credential is written to `$XDG_DATA_HOME/tfscli/auth.json`, or `~/.local/share/tfscli/auth.json` when `XDG_DATA_HOME` is not set — the same location on every OS. The file is created with mode `0600` and its directory with `0700`; on Windows it inherits the permissions of the user profile. It holds exactly one credential: running `auth login` again replaces it. Working with another collection means logging in again with a token issued for it.
+
+Where an interactive login is impossible, put the same JSON into `TFSCLI_AUTH`. When the variable is set, `auth.json` is not read:
+
+```
+TFSCLI_AUTH='{"url": "https://tfs.company.com:8080/tfs", "collection": "DefaultCollection", "pat": "…"}'
+```
+
+All three fields are required. The URL, the collection, and the token always come from the same source, and no flag, separate environment variable, or config key can supply any of them, so a mistyped or injected server address cannot make tfscli send the token to another host.
+
+PAT is the only authentication method; SSPI and NTLM are out of scope. The token is sent as HTTP Basic authentication with an empty user name, which is what the TFS REST API expects. It is not kept in the OS keychain, which adds no protection against code running as the same user; other users are excluded by the file permissions.
+
## Use with an AI agent
tfscli is built for AI coding agents first, and every archive carries an agent
@@ -106,46 +152,28 @@ those settings first.
An agent without skill support can be pointed at the same file directly: it is
plain markdown under a short YAML header.
-## Authentication
-
-A personal access token is issued for one collection, so the server URL, the collection, and the token are stored together, and the token is only ever sent to the URL and the collection stored with it.
-
-Issue the token in the TFS web interface for the collection you will log in to. tfscli only reads work items, so the **Work Items (Read)** scope is all it needs; leave every other scope off. A scope limits what the token can do but does not extend what its owner can do: a work item in a project or area the user has no access to is refused with a `forbidden` error whatever the scope.
-
-Store them once per machine:
-
-```
-$ tfscli auth login
-Server URL (e.g. https://tfs.company.com:8080/tfs): https://tfs.company.com:8080/tfs
-Collection (e.g. DefaultCollection): DefaultCollection
-Personal access token:
-Logged in to https://tfs.company.com:8080/tfs, collection DefaultCollection, as Jane Doe
-```
-
-The token is typed with echo turned off, and the three are checked against the server (`{url}/{collection}/_apis/connectionData`) before anything is written. A rejected token is reported in the usual error format and leaves nothing behind; so is a URL or collection that leads nowhere, with the address they made up. The URL is stored with a lower-case scheme and host and without a trailing slash. A URL copied from the browser often ends with the collection: when its last path segment names the collection entered next, that segment is removed, and the command says which server URL it uses instead. The command takes no flags and needs an interactive terminal; it reads `caBundle` and `insecureSkipVerify` from the config file when there is one, so a server behind an internal CA can be reached during login too. The API version settings do not apply to it: the check is sent without `api-version`, whatever the config file or `TFSCLI_API_VERSION` say, and `--api-version` is refused. On Windows under Git Bash (mintty), stdin is not a console; run the command from Windows Terminal, PowerShell, or `cmd`, or prefix it with `winpty`.
-
-The credential is written to `$XDG_DATA_HOME/tfscli/auth.json`, or `~/.local/share/tfscli/auth.json` when `XDG_DATA_HOME` is not set — the same location on every OS. The file is created with mode `0600` and its directory with `0700`; on Windows it inherits the permissions of the user profile. It holds exactly one credential: running `auth login` again replaces it. Working with another collection means logging in again with a token issued for it.
-
-Where an interactive login is impossible, as in CI, put the same JSON into `TFSCLI_AUTH`. When the variable is set, `auth.json` is not read:
-
-```
-TFSCLI_AUTH='{"url": "https://tfs.company.com:8080/tfs", "collection": "DefaultCollection", "pat": "…"}'
-```
+## Configuration
-All three fields are required. The URL, the collection, and the token always come from the same source. There is no flag, separate environment variable, or config key for any of them, so that neither a mistyped server address nor an injected one can make tfscli send the token to another host.
+Every setting apart from the credential:
-PAT is the only authentication method. SSPI and NTLM are out of scope. The token is sent as HTTP Basic authentication with an empty user name, which is what the TFS REST API expects. It is not kept in the OS keychain: against code running as the same user a keychain adds no protection, other users are excluded by the file permissions, and a stolen disk is a matter for disk encryption.
+| Setting | Config key | Environment variable | Flag | Default | Required |
+|---|---|---|---|---|---|
+| Team project | `project` | `TFSCLI_PROJECT` | `-p`, `--project` | — | per command |
+| REST API version | `apiVersion` | `TFSCLI_API_VERSION` | `--api-version` | none | no |
+| Request logging | — | `TFSCLI_VERBOSE=1` | `--verbose` | off | no |
+| CA bundle | `caBundle` | — | — | none | no |
+| Skip certificate verification | `insecureSkipVerify` | — | — | `false` | no |
-## Configuration
+Every flag in the table is global except `-p` / `--project`, which belongs to the commands that act within a project: it is required for the `wit work-items` commands and optional for `wit wiql query-by-wiql`.
-The remaining settings come from four sources. Later sources override earlier ones:
+The settings come from four sources. Later sources override earlier ones:
1. Built-in defaults
2. The config file `$XDG_CONFIG_HOME/tfscli/config.json`, or `~/.config/tfscli/config.json` when `XDG_CONFIG_HOME` is not set. The same location is used on every OS, Windows included.
3. Environment variables
4. Command-line flags
-The config file is optional: every setting it holds, except the TLS ones below, can be given by an environment variable or a flag instead. To use one, copy `config.example.json` to that location and fill in the values:
+The config file is optional: every setting it holds, except the TLS ones, can be given by an environment variable or a flag instead. To use one, copy `config.example.json` to that location and fill in the values:
```json
{
@@ -153,24 +181,27 @@ The config file is optional: every setting it holds, except the TLS ones below,
}
```
-| Setting | Config key | Environment variable | Flag | Default | Required |
-|---|---|---|---|---|---|
-| Team project | `project` | `TFSCLI_PROJECT` | `-p`, `--project` | — | per command |
-| REST API version | `apiVersion` | `TFSCLI_API_VERSION` | `--api-version` | none | no |
-| Request logging | — | `TFSCLI_VERBOSE=1` | `--verbose` | off | no |
+### REST API version
-Every flag in the table is global except `-p` / `--project`, which belongs to the commands that act within a project: it is required for the `wit work-items` commands and optional for `wit wiql query-by-wiql`.
+By default no `api-version` is sent, and the server answers at the version it chooses. The commands that send a GET request, `wit work-items get` and `wit work-items list`, need no setting to work with any server release.
-By default no `api-version` is sent, and the server answers at the version it chooses, so the commands that send a GET request need no setting to work with any server release. `wit work-items get-batch` and `wit wiql query-by-wiql` send a POST request, for which a server may require a version: it then refuses the request with a `config` error saying that no `api-version` was supplied, and a version has to be set with any of the three settings above. Otherwise set a version only to pin the shape of the response. It must not exceed the highest version the server supports, which follows from its release: 7.1 for Azure DevOps Server 2022.1, 7.0 for 2022, 6.0 for 2020, 5.0 for 2019, and 4.1 for TFS 2018 Update 2 (see "API and TFS version mapping" in the [REST API reference](https://learn.microsoft.com/en-us/rest/api/azure/devops/)). The value is sent unchanged, so a resource that exists only in preview at that version needs the `-preview` suffix. `auth login` does not use this setting.
+`wit work-items get-batch` and `wit wiql query-by-wiql` send a POST request, which a server may refuse without a version: it then reports a `config` error saying that no `api-version` was supplied, and a version has to be set with any of the three settings above. Otherwise set a version only to pin the shape of the response. `auth login` does not use this setting.
-### TLS
-
-Two settings are accepted in the config file only, so that relaxing TLS is always a written-down decision:
+The version must not exceed the highest one the server supports:
-| Config key | Effect |
+| Server | Highest API version |
|---|---|
-| `caBundle` | Path to a PEM bundle appended to the system root pool. Use this for an internal CA. |
-| `insecureSkipVerify` | Disables certificate verification entirely. Prints a warning under `--verbose`. |
+| Azure DevOps Server 2022.1 | 7.1 |
+| Azure DevOps Server 2022 | 7.0 |
+| Azure DevOps Server 2020 | 6.0 |
+| Azure DevOps Server 2019 | 5.0 |
+| TFS 2018 Update 2 | 4.1 |
+
+The full mapping is under "API and TFS version mapping" in the [REST API reference](https://learn.microsoft.com/en-us/rest/api/azure/devops/). The value is sent unchanged, so a resource that exists only in preview at that version needs the `-preview` suffix.
+
+### TLS
+
+`caBundle` and `insecureSkipVerify` are accepted in the config file only, so that relaxing TLS is always a written-down decision. `caBundle` is the path to a PEM bundle appended to the system root pool; use it for an internal CA. `insecureSkipVerify` set to `true` disables certificate verification entirely and prints a warning under `--verbose`.
## Usage
@@ -198,8 +229,6 @@ tfscli wit work-items get -p MyProject 12345 --fields System.Title,System.State,
Without `--fields`, every field of the work item is printed in the order the server returned it.
-`-p` is required unless `project` is set in the config file or `TFSCLI_PROJECT` is exported.
-
### `wit work-items list` and `wit work-items get-batch`
Print several work items, at most 200, with one request:
@@ -339,6 +368,12 @@ $ tfscli wit work-items get -p MyProject 12345
Error [network]: cannot reach https://tfs.company.com:8080
```
+## Known limitations
+
+- **HTML tables collapse.** A `` in a rich-text field becomes its cell text run together, without separators, so a work item whose Description holds a table renders unreadably.
+- **Rich-text noise is unhandled.** @-mentions, attachment links, Word- and Outlook-pasted markup, and work-item references are converted literally, with whatever wrapper markup TFS stored. The fix for this and for tables is recorded in `docs/backlog.md`.
+- **Servers with anonymous access are not supported.** On a server that admits anonymous requests, such as Azure DevOps Server with public projects enabled, an invalid PAT may pass the login check and be stored, and the other commands would then read as the anonymous user, seeing less, without an error.
+
## Development
Everything the project verifies runs through the `Makefile`, and CI runs `make check` verbatim, so a green `make check` locally is the whole gate. External dependencies are deliberately limited to `cobra`, `html-to-markdown/v2`, and `golang.org/x/term`; HTTP, JSON, and config parsing use the standard library.
diff --git a/docs/2026-10-04-tasks-first-run-errors.md b/docs/2026-10-04-tasks-first-run-errors.md
index 50ce972..0defd80 100644
--- a/docs/2026-10-04-tasks-first-run-errors.md
+++ b/docs/2026-10-04-tasks-first-run-errors.md
@@ -109,3 +109,25 @@ optional for `wit work-items get`, taking server compatibility into account.
the Context section, and implementation tasks for the decision are added to
this file.
**Status:** Pending
+
+### TASK-08 `missing-api-version`
+**Description:** A server may refuse a POST request that carries no
+`api-version` — `wit work-items get-batch` and `wit wiql query-by-wiql`
+send one only when it is configured — with "No api-version was supplied for
+the \"POST\" request". `classify` in `internal/apiclient/apiclient.go` adds
+a next step to a version error only when a version is configured, so this
+refusal reaches the user as the server's message alone, without the settings
+that supply a version or how to choose its value. Add the next step for this
+case: name `apiVersion` in the config file with the path of the file,
+`TFSCLI_API_VERSION`, and `--api-version`, and say that the value must not
+exceed the highest version the server supports, as listed in the
+"Configuration" section of `README.md`. How the refusal is recognised is
+unverified: the `typeKey` of this error is not in `versionErrorTypeKeys`
+and has to be established from documentation or a sample response, falling
+back to the message text only if no `typeKey` is available.
+**Definition of done:** A refusal of a request that carries no `api-version`
+produces a message naming the three settings, the config file path, and how
+to choose the value; a refusal of a configured version keeps its current
+message. Tests cover both. `README.md` and the representative messages in
+`skills/tfscli/SKILL.md` match. `make check` passes.
+**Status:** Pending
diff --git a/docs/backlog.md b/docs/backlog.md
index b1c4c30..032946f 100644
--- a/docs/backlog.md
+++ b/docs/backlog.md
@@ -2,6 +2,78 @@
Accepted work that has not started yet, in priority order.
+### `api-version-negotiation`
+**Goal:** Every command works on any server without the user setting an API
+version: `wit work-items get-batch` and `wit wiql query-by-wiql` no longer fail
+with "No api-version was supplied" on servers that require a version for POST.
+**Context:** Microsoft documents `api-version` as required on every request; in
+practice GET requests work without it, while POST requests are refused by some
+servers. The official SDKs (.NET, Go, Node, Python) discover versions with
+`OPTIONS {url}/{collection}/_apis`, which lists every resource with its
+`minVersion`, `maxVersion`, `releasedVersion`, and `resourceVersion`. Decisions
+already made:
+- When no version is configured, every request except the verification in
+ `auth login` is preceded by `OPTIONS {url}/{collection}/_apis` and carries the
+ `releasedVersion` of its resource — the version the server chooses for a
+ request without one, so the output does not change. A resource without a
+ released version (`0.0`) gets `{maxVersion}-preview.{resourceVersion}`.
+- The response is not cached: an on-disk cache is excluded by the product
+ constraints, and an in-process cache gains nothing in a CLI. Each invocation
+ makes one extra request.
+- A configured version (config file, `TFSCLI_API_VERSION`, `--api-version`) is
+ sent as is, without `OPTIONS`.
+- Negotiation never fails a command on its own. A 401 or a transport error on
+ `OPTIONS` is reported at once in its usual category, since the main request
+ would fail the same way. Any other failure — 400, 403, 404, 405, 5xx, an
+ unparseable response, the resource missing from the list — falls back to
+ sending the request without a version, as before, so that no server that works
+ today stops working.
+- Under `--verbose` the `OPTIONS` request is logged like any other, and a
+ fallback adds one line saying that the version was not negotiated and why.
+ Nothing is printed without `--verbose`.
+- When a request sent without a version after a fallback is refused for lack of
+ one, the `config` error says that negotiation failed and why, names
+ `apiVersion` with the config file path, `TFSCLI_API_VERSION`, and
+ `--api-version`, and says that the value must not exceed the server's version.
+ This supersedes TASK-08 in `docs/2026-10-04-tasks-first-run-errors.md`, which
+ moves into this work when it starts.
+- No new error category.
+- The constraint "requests carry `api-version` only when one is configured" in
+ `docs/project-brief.md` and `CLAUDE.md`, and the "REST API version" section of
+ `README.md`, change together with this work.
+
+Open questions:
+- Whether `OPTIONS` answers in this form on TFS 2015 and 2017 (unverified).
+- How a request is matched to its entry in the list: by location id, as the
+ SDKs do, or by area and resource name.
+**Added:** 2026-10-06, in conversation on API version negotiation
+
+### `workitem-relations`
+**Goal:** An agent reading a work item sees the work items linked to it — parent,
+children, related, duplicates — without composing a WIQL link query.
+**Context:** "Key Success Metrics" in `docs/project-brief.md` includes referencing
+related work items. Relations are not fields: the API returns them only with
+`$expand`, and the printer renders fields only. Decisions already made:
+- `--expand` is exposed on `wit work-items get`, `list`, and `get-batch` at once,
+ together with rendering relations and not before it: a flag whose result the
+ printer drops would be accepted and silently ignored (recorded in
+ `docs/archive/2026-10-04-tasks-domains-1-workitem-batch.md`).
+- The flag follows the naming rule in "Follow TFS API structure": `$expand` is
+ `--expand`, and its value is passed to the server unchecked.
+
+Open questions:
+- How a relation is printed: the link type reference name, the target work item id
+ taken from its URL, and the link attributes such as the comment; how relations
+ that point to something other than a work item (hyperlinks, attachments, commits)
+ are printed.
+- The server is expected to refuse `$expand` combined with `fields` (unverified);
+ whether tfscli says anything beyond passing the server's error through.
+- What the other values of `$expand` (`fields`, `links`, `all`) change in the
+ output, if anything.
+- The server versions on which `$expand` is available, under "Server compatibility
+ is preserved".
+**Added:** 2026-10-06, in conversation on the MVP coverage
+
### `git-pull-requests`
**Goal:** An agent can read pull requests, their comment threads, and the work items
linked to them.
@@ -54,8 +126,6 @@ contracts. Decisions already made:
attachment file names; that is a separate feature.
- The limitation is stated in "Known limitations" in both `README.md` and
`skills/tfscli/SKILL.md`, which must stop claiming it together.
-**Blocked until:** A decision to collect rich-text samples from a live TFS instance. A
-live connection is no longer the blocker: release 0.0.5 ran `auth login` and reading one
-work item against a live server.
+**Blocked until:** A decision to collect rich-text samples from a live TFS instance.
**Added:** 2026-05-20, in the conversation that produced the Architecture section of
`docs/architecture.md`