Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
311b709
Record the command line rules for agent permissions (TASK-01)
dpleshakov Oct 8, 2026
cba45ca
Describe where the command line shape is enforced (TASK-02)
dpleshakov Oct 8, 2026
47fc5c3
Add the command line rules to CLAUDE.md (TASK-03)
dpleshakov Oct 8, 2026
16ed534
Refuse flags placed before the end of the command path (TASK-04)
dpleshakov Oct 8, 2026
2a93c5a
Guard against command aliases and prefix matching (TASK-05)
dpleshakov Oct 8, 2026
144b0bf
Document the command line rule and agent permission rules (TASK-06)
dpleshakov Oct 8, 2026
022c6ee
Find the command path by walking the arguments (TASK-01)
dpleshakov Oct 8, 2026
e542e3b
Drop the exceptions from the flag order rule in the docs (TASK-02)
dpleshakov Oct 8, 2026
0b28207
Cross-check the command path with cobra's Find (TASK-01)
dpleshakov Oct 9, 2026
ee2e3d4
Align the docs with the cross-checked flag order rule (TASK-02)
dpleshakov Oct 9, 2026
fba8148
Find the command path without reading flag values (TASK-01)
dpleshakov Oct 9, 2026
6c6b538
Record that a command with subcommands only groups them (TASK-02)
dpleshakov Oct 9, 2026
7460843
Let shell completion requests through the flag order check (TASK-01)
dpleshakov Oct 9, 2026
1bc2127
Check the flag order rule against cobra as an oracle (TASK-02)
dpleshakov Oct 9, 2026
9a7b9a5
Fix the flag order findings of the branch review
dpleshakov Oct 9, 2026
5d1de67
Leave a mistyped action to cobra after misplaced flags (TASK-01)
dpleshakov Oct 9, 2026
ea661ca
Show a corrected command only when both shells read it alike (TASK-02)
dpleshakov Oct 9, 2026
18b651c
Report an unknown command under completion (TASK-03)
dpleshakov Oct 9, 2026
f12db7b
Cover -p=X, keep group flags off below the root, and share the comple…
dpleshakov Oct 9, 2026
2c7d26f
Align the docs with the second branch review (TASK-05)
dpleshakov Oct 9, 2026
354e28f
Generalize the permission rules in the README
dpleshakov Oct 10, 2026
efe027c
Shorten the constraints repeated in CONTRIBUTING
dpleshakov Oct 10, 2026
5a9fd94
Trim the command line rules in the brief and the architecture
dpleshakov Oct 10, 2026
892084a
Limit tasks files to work that needs a plan
dpleshakov Oct 10, 2026
1734244
Shorten the constraints repeated in CLAUDE.md
dpleshakov Oct 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions .claude/skills/tasks/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: tasks
description: Universal task-file workflow — create a structured tasks file for work that starts now and execute it step by step, tracking what is done vs. pending in any project. No project-specific assumptions.
description: Universal task-file workflow — create a structured tasks file for work that starts now and needs a plan agreed with the user, and execute it step by step, tracking what is done vs. pending in any project. Not for fixing review findings, a single fix, or another change done directly. No project-specific assumptions.
argument-hint: "[feature description to break down | path to a tasks file]"
---

Expand All @@ -17,6 +17,21 @@ not the one you are reading, read it and follow it instead of this file.
Where the project's own documentation (for example `CONTRIBUTING.md` or `CLAUDE.md`)
sets a different convention, the project's convention wins over this skill.

## When a tasks file is warranted

A tasks file holds a plan. Work gets one when it needs a plan: it breaks down into
several tasks, the breakdown is agreed with the user before the work starts, or the
work may outlast one session.

Work that needs no plan is done directly, and its commit messages explain it. This
includes fixing the findings of a review of the current branch, a single fix, and a
small change the user asked for. Such work gets no tasks file even when the user
leaves the details to you, and even when it touches several files.

When such work turns out to change the approach — the user has to approve a decision
before the work goes on — it has become work that needs a plan: agree the decision
with the user, then create a tasks file for the work that follows from it.

## Mode detection

Decide what to do from the user's request and the current context:
Expand All @@ -25,8 +40,9 @@ Decide what to do from the user's request and the current context:
and exactly one `Active` file exists in the tasks directory (see "The tasks file") →
**Execute** mode.
- A feature or request description is given and no matching file exists → **Create**
mode (breakdown), then offer to start executing. Work that is not to start now does
not get a tasks file (see "Create mode").
mode (breakdown), then offer to start executing. Work that needs no plan (see "When
a tasks file is warranted") or is not to start now (see "Create mode") does not get
a tasks file.
- The request names no new work and no `Active` file exists → propose starting the next
item from where the project keeps accepted work that has not started, if it has such
a place, as **Create** mode; otherwise ask the user what to work on.
Expand Down Expand Up @@ -97,18 +113,20 @@ self-evident and needs no marker.
A tasks file is created when the work starts, not in advance: most of the effort before
that is discussion and decisions, and a breakdown made ahead of time goes stale.

1. Confirm that the work starts now. If it does not — the user wants it recorded,
1. Confirm that the work needs a plan (see "When a tasks file is warranted"). If it
does not, do it directly, without a tasks file.
2. Confirm that the work starts now. If it does not — the user wants it recorded,
noted, or kept for later — do not create a tasks file. Record it where the project
keeps accepted work that has not started, if it has such a place; otherwise ask the
user where it should go. If the work was recorded there earlier, use that record as
the input to the breakdown and take it out as that place's rules require.
2. Turn the feature/request into a complete, ordered list of **atomic** tasks — each
3. Turn the feature/request into a complete, ordered list of **atomic** tasks — each
one achievable in a single focused session, with a definition of done that can be
checked.
3. Propose the task structure to the user and let them adjust before writing anything.
4. Write the file to the tasks directory with header `Active` and every task `Pending`,
4. Propose the task structure to the user and let them adjust before writing anything.
5. Write the file to the tasks directory with header `Active` and every task `Pending`,
numbered `TASK-01`, `TASK-02`, … in order.
5. Offer to start executing the first task.
6. Offer to start executing the first task.

## Execute mode (tracking discipline)

Expand Down Expand Up @@ -161,6 +179,7 @@ same commit:

## Invariants

- A tasks file holds a plan; work that needs no plan gets no tasks file.
- Tasks are atomic; work on one at a time.
- Never leave a finished task unmarked — the file is the source of truth for progress.
- `Done` tasks are never rewritten; `Pending` tasks change when the plan proves wrong.
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed
- A flag placed before the end of the command path, such as `--verbose` in `tfscli --verbose wit work-items get 12345` or `--help` in `tfscli --help wit work-items get`, is now refused with a `config` error that shows how to run the command with its flags after it, so that agent permission rules can match the command by its name.

### Fixed
- An unknown command under `tfscli auth` or `tfscli completion`, such as `tfscli auth logn`, is now reported as an unknown command, where tfscli used to print the help of the group and exit successfully.
- A flag of an action placed before the action, as in `tfscli wit wiql --time-precision query-by-wiql --query "..."`, no longer makes tfscli print the help of the group and exit successfully without running the query; the command line is refused with the corrected command instead.

## [0.1.1] - 2026-10-06

### Added
Expand Down
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ Read these before the corresponding kind of change (the full list of project doc
These come from `project-brief.md` and override casual feature requests. If a change would violate one of these, surface it before implementing.

- **PAT auth only.** No SSPI or NTLM. A PAT is issued for one collection, so the server URL, the collection, and the PAT are stored together and always come from one source: `$XDG_DATA_HOME/tfscli/auth.json` (default `~/.local/share/tfscli/auth.json`, mode `0600`, written only by the interactive `tfscli auth login`) or `TFSCLI_AUTH` holding the same JSON, which takes precedence. No flag, separate env var, or config key may supply the URL, the collection, or the token. No OS keychain unless an organisation requires one.
- **Read-only in v1.** Write operations are explicitly out of scope. They additionally depend on JSON output landing first, because markdown is lossy.
- **Read-only in v1.** Write operations are explicitly out of scope. They additionally depend on JSON output landing first, because markdown is lossy, and each will require `--allow-changes` (and `--allow-irreversible` if it cannot be undone) — see "Write operations" in Non-Goals of `project-brief.md`.
- **The command line stays matchable by agent permission rules.** Flags follow the command path; each command has one form; a group of commands only groups them; local command names never coincide with REST API areas; no flag gives a reading command a side effect; no generic pass-through command. See "The command line can be governed by permission rules" in `project-brief.md`.
- **Markdown is the default output.** HTML fields (Description, ReproSteps, etc.) are converted via `github.com/JohannesKaufmann/html-to-markdown/v2`. JSON output is a planned `--json` flag, not present in v1.
- **Mirror TFS REST API structure.** Command hierarchy, parameter names, and behaviour follow the API. Any deviation needs explicit usability justification — don't invent new abstractions (no custom query syntax, no synthetic batch endpoints, no caching).
- **Stateless.** No daemon, no background process, no on-disk cache. Every call hits the server.
Expand Down
8 changes: 7 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,16 @@ if a change would break one, raise it before implementing.

- PAT authentication only; no SSPI or NTLM. The PAT is entered interactively by
`tfscli auth login` and never accepted as a command argument.
- Read-only in v1; write operations additionally depend on JSON output landing first.
- Read-only in v1; write operations additionally depend on JSON output landing first, and
each will require `--allow-changes` (and `--allow-irreversible` if it cannot be undone),
as the brief describes.
- Markdown is the default output format.
- The command hierarchy and parameter names mirror the TFS REST API; a deviation needs an
explicit usability justification, and no query syntax or batch endpoint is invented.
- The command line stays matchable by agent permission rules: flags follow the command
path, each command has one form, a group of commands only groups them, local command
names never coincide with REST API areas, no flag gives a reading command a side
effect, and there is no generic pass-through command.
- Stateless: no daemon, no background process, no on-disk state beyond the config file and the credential written by `auth login`.
- The error categories `auth`, `not_found`, `forbidden`, `server`, `config`, and `network`
are a contract. New ones may be added; existing ones are never renamed or removed.
Expand Down
86 changes: 67 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Not present in v1: write operations (out of scope) and JSON output (`--json`, pl

5. Copy the `skills/tfscli` directory from the archive into the skills directory of your AI agent.

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).
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 Claude Code and opencode, gives the copy commands, and describes an agent without skill support. Further settings, such as a default project, are described under [Configuration](#configuration).

## Installation

Expand Down Expand Up @@ -115,15 +115,9 @@ PAT is the only authentication method; SSPI and NTLM are out of scope. The token

## Use with an AI agent

tfscli is built for AI coding agents first, and every archive carries an agent
skill beside the binary: `skills/tfscli/SKILL.md`, in the Agent Skills format.
It tells an agent when the tool applies, how the output is shaped, what each
error category calls for, and where the boundaries are — none of which `--help`
conveys.
tfscli is built for AI coding agents first, and every archive carries an agent skill beside the binary: `skills/tfscli/SKILL.md`, in the Agent Skills format. It tells an agent when the tool applies, how the output is shaped, what each error category calls for, and where the boundaries are — none of which `--help` conveys.

Install it by copying the directory. Create the destination first: copying
into a path that does not exist yet leaves `SKILL.md` one level too high,
where no agent looks for it.
Install it by copying the directory. Create the destination first: copying into a path that does not exist yet leaves `SKILL.md` one level too high, where no agent looks for it.

```sh
mkdir -p ~/.claude/skills && cp -r skills/tfscli ~/.claude/skills/
Expand All @@ -134,23 +128,75 @@ New-Item -ItemType Directory -Force $HOME\.claude\skills | Out-Null
Copy-Item -Recurse skills\tfscli $HOME\.claude\skills\
```

That one location serves both agents: opencode has first-party skill support and
searches `~/.claude/skills/` among its own paths. Other locations work as well,
if the skill should be scoped to a single project or kept out of the Claude
directory:
That one location serves Claude Code and opencode: opencode has first-party skill support and searches `~/.claude/skills/` among its own paths. Other locations work as well, if the skill should be scoped to a single project or kept out of the Claude directory:

| Agent | Project | Global |
|---|---|---|
| Claude Code | `.claude/skills/tfscli/` | `~/.claude/skills/tfscli/` |
| opencode | `.opencode/skills/tfscli/`, `.claude/skills/tfscli/`, `.agents/skills/tfscli/` | `~/.config/opencode/skills/tfscli/`, `~/.claude/skills/tfscli/`, `~/.agents/skills/tfscli/` |

In opencode, loading a skill is subject to `permission.skill` in
`opencode.json` — `allow`, `ask`, or `deny` — and the mechanism can be switched
off altogether with `tools.skill: false`. If the skill is never offered, check
those settings first.
In opencode, loading a skill is subject to `permission.skill` in `opencode.json` — `allow`, `ask`, or `deny` — and the mechanism can be switched off altogether with `tools.skill: false`. If the skill is never offered, check 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.
An agent without skill support can be pointed at the same file directly: it is plain markdown under a short YAML header.

### Permission rules

An agent usually decides whether a shell command may run by matching its text against allow, ask, and deny rules. Every tfscli command starts with its full name, such as `tfscli wit work-items get` or `tfscli licenses`, followed by flags and arguments, and each command has exactly one name, so a rule written as a prefix covers the command whatever its flags. The current commands only read; `auth login` is interactive and is not for an agent to run.

Commands that change data are not part of tfscli yet. When they are added, each will require the flag `--allow-changes` and will fall under `tfscli wit *` like the reading commands.

The following rules let an agent read freely, ask before a command that changes data, and keep the agent away from the credential. `*` stands for any text, spaces included:

| Command | Decision |
|---|---|
| `tfscli wit *` | allow |
| `tfscli licenses` | allow |
| `tfscli *--allow-changes*` | ask |
| `tfscli auth *` | deny |

A command that changes data matches both `tfscli wit *` and `tfscli *--allow-changes*`, so the rules work only if the ask rule takes precedence over the allow rule. Agents resolve such a conflict differently: some rank the decisions, others apply the last matching rule.

A rule matches the command as the agent types it. A call through `tfscli.exe` or a full path matches none of these rules and gets the agent's default for unmatched commands, which may be to allow it. Likewise, a flag written with quotes in the middle, such as `--allow-chan''ges`, reaches tfscli as `--allow-changes` but may not match the rule. The rules therefore govern the usual form of a call and guard against an agent's mistake; they are not a security boundary against an agent that sets out to get around them.

The examples below write the same rules for two agents. Each agent owns its settings format, which may change; its own documentation takes precedence over these examples.

Claude Code, in `.claude/settings.json` or `~/.claude/settings.json`. Claude Code matches commands of its Bash and PowerShell tools by separate rules, so each rule appears in both forms. A `deny` rule takes precedence over an `ask` rule, and an `ask` rule over an `allow` rule, whatever their order. A command that matches no rule is asked about in the default mode:

```json
{
"permissions": {
"allow": [
"Bash(tfscli wit *)",
"Bash(tfscli licenses)",
"PowerShell(tfscli wit *)",
"PowerShell(tfscli licenses)"
],
"ask": [
"Bash(tfscli *--allow-changes*)",
"PowerShell(tfscli *--allow-changes*)"
],
"deny": [
"Bash(tfscli auth *)",
"PowerShell(tfscli auth *)"
]
}
}
```

opencode, in `opencode.json`. The last matching rule wins, so place these after any catch-all `"*"` rule, in this order. A command that matches no rule is allowed unless a catch-all `"*": "ask"` is set:

```json
{
"permission": {
"bash": {
"tfscli wit *": "allow",
"tfscli licenses": "allow",
"tfscli *--allow-changes*": "ask",
"tfscli auth *": "deny"
}
}
}
```

## Configuration

Expand Down Expand Up @@ -213,6 +259,8 @@ tfscli <area> <resource> <action> [flags] [arguments]

Commands are named after the REST API reference: the area and the resource are the segments of the operation's page path, so Get Work Item, documented under `.../wit/work-items/get-work-item`, is `wit work-items get`. `auth login` and `licenses` are local to tfscli and have no counterpart in the API.

Flags follow the command: `tfscli wit work-items get --verbose -p MyProject 12345`, never `tfscli --verbose wit work-items get ...`. A flag placed before the last word of the command is refused with a `config` error that shows the command with the flag moved, and nothing is sent to the server; a flag that the command does not define is reported as unknown instead. `--help` is no exception: `tfscli wit work-items get --help`, not `tfscli --help wit work-items get`. `--version` belongs to `tfscli` alone, and placed before a command it is refused with a message that names `tfscli --version`. The rule keeps the command at the start of the command line, where agent permission rules look for it (see [Permission rules](#permission-rules)).

`tfscli --version` prints the version and the commit it was built from; `tfscli --help`, and `--help` on any command, lists the flags. `tfscli licenses` prints the components built into the binary — tfscli itself, the Go standard library, and the third-party modules — followed by the license text of each.

### `wit work-items get`
Expand Down
Loading
Loading