# NotMeantTo for AI agents

Reader: an AI model working in a repository whose issues live in NotMeantTo. This page is everything
needed to read, file and move issues correctly with the fewest calls and tokens. The site serves this
same file at `/agents.md` and `/llms.txt`.

- Production: `https://NotMeantTo.com`
- Local development copy: `https://notmeantto.localhost` (or `http://localhost:3300`)

## 1. The model in one screen

- A **workspace** owns everything. A token belongs to one workspace and never sees another.
- A **project** is one repository, named by its folder (`NotHangman.com`, `NotUserError`). A project has
  one or more **prefixes**; its default prefix is used for new issues.
- An **issue ID** is prefix + number, zero-padded to five digits: `NH10023`, `NH00663`, `NMT10000`.
  Numbers are unique **within a project across all its prefixes**, not globally. New projects number from
  10000; projects imported from `TODO.csv` continue their old numbering (some are below 10000).
- One prefix can belong to two projects in imported history (`GN` is in both `GateIron.com` and
  `NotHangman.com`). An ambiguous ID answers **409** naming both projects; pass the project to choose. The
  CLI always passes its folder's project, so this rarely surfaces there.
- Every write is **versioned** (`version` goes up by one) and recorded in the issue's **history** with
  the writer's name (a token writes under its token name).

## 2. The fields and their rules

These are the `~/.claude/rules/todo-format.md` rules, enforced by the server on every write. A write that
breaks one is refused with **422** and a list of `{ field, message }`.

| Field | Values | Rules |
|---|---|---|
| `status` | `1-Blocked` `2-Open` `3-InProgress` `5-Backlog` `6-OnDev` `8-Delivered` `9-Cancelled` | exact strings. `8` and `9` are **closed**; everything else is open |
| `priority` | `0`–`5` | required while open. 0 now, 1 next, 2 this cycle, 3 not now, 4 needs a decision, 5 may not be worth doing |
| `urgency` | whole number ≥ 0, or blank | sorts ascending: 0 most urgent, **blank counts as 5** (the default), 10+ parked |
| `size` | `S` `M` `L` or blank | S = the fix is known and could be done now; M = about a session; L = more, or has a decision in it |
| `title` | one line, **≤ 80 characters** while open | the release note: what changed, as a user would read it, present tense |
| `description` | one line, **≤ 600 characters** while open | the current ask and why, as plain prose. No `What:`/`Why:` labels. **Rewrite it; never append progress** |
| `release` | ≤ 64 characters | filled only at `6-OnDev` or `8-Delivered`; `6-OnDev` requires it |
| `delivered` | `YYYY-MM-DD` | exactly when `8-Delivered`; never otherwise |
| `prefix` | one of the project's prefixes | changing it never renumbers |

Other rules:

- **A closed issue is never reopened.** Moving `8-*` or `9-*` back to an open status is refused. If
  delivered work turns out incomplete, file a new issue that says so.
- An open issue's title may not repeat another open issue's title in the same project (case-insensitive).
- New issues default to priority 3, status `5-Backlog`, urgency 5.
- `ship`, `deliver` and `cancel` are the ways to reach `6`, `8` and `9`; setting those statuses by a
  plain field change fails validation unless release/delivered are also right. Bulk changes refuse them.
- **Work order** (what to do next): urgency ascending (blank = 5), then priority ascending, then ID.

## 3. Access

- Every call sends `Authorization: Bearer nmt_…`. A workspace admin creates tokens on the site's Admin
  page. Never print a token into a transcript, commit, or file inside a repository.
- Token roles: `viewer` reads; `editor` reads and writes issues; `admin` also manages projects, people
  and tokens.
- A token **scoped to a project** reads every project in its workspace but writes only its own, and is
  never more than an editor. This is how a repository's session is kept to its own issues: use the
  scoped token for your folder's project.
- A request limit per token per minute (600 in production; lower on a test server), shared across
  all server processes. Over it the server answers **429** with `Retry-After`; `nmt` waits that many
  seconds by the clock and retries by itself.

## 4. Pick the interface

| You have | Use | Why |
|---|---|---|
| A shell | **`nmt`** CLI | one command per action, dense text output, project picked from the folder |
| No shell, MCP tools | **MCP** at `/mcp` | the same actions as tools |
| A script | **REST** at `/api/v1` | JSON, full control, bulk and change feed |

To spend the fewest tokens: read the **work order** (`nmt`, top 20) rather than whole lists; ask for
text (`nmt` does; REST with `?format=text`); filter to one project; avoid `--closed`/`scope=closed`
unless you need history (closed lists are long); use `show` for one issue rather than listing.

## 5. The CLI: `nmt`

One file, Node 18+, no dependencies.

```bash
curl -fsSo ~/.local/bin/nmt https://NotMeantTo.com/cli/nmt.js && chmod +x ~/.local/bin/nmt
```

Settings, first found wins:

- URL: `--url`, `NMT_URL`, `url` in `~/.config/notmeantto/config.json`, else `https://notmeantto.com`
- Token: `NMT_TOKEN`, `tokens["<project>"]` in that config file, `token` in that config file
- Project: `--project`, `NMT_PROJECT`, the git repository's folder name, the current folder's name

A config file holding one scoped token per project:

```json
{ "url": "https://NotMeantTo.com", "tokens": { "NotHangman.com": "nmt_…", "NotUserError": "nmt_…" } }
```

Commands:

```text
nmt                                   work order for this project, top 20 (open incl. backlog and on-dev)
nmt list [--all|--closed] [--limit N] [--everywhere]
                                      default: blocked, open, in progress. --all adds backlog and on-dev.
                                      --everywhere: every project in the workspace
nmt show ID                           one issue in full (includes version)
nmt history ID                        every change: when, who, what
nmt add P "title" "description" [--prefix XX] [--status S] [--urgency N] [--size S|M|L]
nmt set ID [ID ...] field=value ...   fields: priority status title description urgency size release prefix
                                      several IDs = one bulk change, each row checked on its own
nmt prio ID 0-5 | nmt urge ID N|- | nmt size ID S|M|L|- | nmt status ID STATUS
nmt ship ID RELEASE                   6-OnDev with a release
nmt deliver ID [RELEASE] [--date YYYY-MM-DD]
                                      8-Delivered, dated today; needs a release (set by ship or given here)
nmt cancel ID "why"                   9-Cancelled; the reason REPLACES the description (history keeps the old)
nmt notes RELEASE                     release notes: titles delivered in that release
nmt check                             rule problems and notes for this project (like todo.py check)
nmt export [DIR]                      write TODO.csv and TODONE.csv in the spec's exact format
nmt import [DIR]                      load TODO.csv/TODONE.csv into this project (unscoped admin token)
nmt projects | whoami | help
--json                                raw JSON instead of text
```

Output of `nmt` / `nmt list`, one issue per line:

```text
URG PRI SZ ID      STATUS       TITLE
  0   1 S  NH00661 1-Blocked    Joining a class with a Google account ends on a blank page at the provider
  -   3 M  NH00025 2-Open       A Dunk Tank look
```

`-` in URG means blank (sorts as 5). Exit codes: 0 success, 3 refused by the rules (422, the field
messages are printed), 1 anything else. Errors go to stderr as `error <status>: <message>`.

## 6. MCP

Streamable HTTP, stateless, bearer token. Works with both current (2026-07-28) and `initialize`-era
clients. For Claude Code:

```bash
claude mcp add --transport http notmeantto https://NotMeantTo.com/mcp --header "Authorization: Bearer nmt_…"
```

Tools (a viewer token is offered only the reading ones):

| Tool | Arguments (required in **bold**) | Does |
|---|---|---|
| `list_projects` | — | projects, prefixes, counts, next ID |
| `list_issues` | project, scope (`open` default, `active`, `closed`, `all`), limit, search | work-order list, text |
| `get_issue` | **id**, project | one issue in full, with version |
| `issue_history` | **id**, project | every change |
| `create_issue` | **project**, **title**, **description**, priority, status, urgency, size, prefix | files a new issue |
| `update_issue` | **id**, project, version, priority, status, urgency, size, title, description, release, prefix | changes only the fields given |
| `bulk_update` | **ids**, project, priority, status, urgency, size | same change to many; refused rows listed |
| `ship_issue` | **id**, **release**, project | 6-OnDev |
| `deliver_issue` | **id**, project, release, date | 8-Delivered |
| `cancel_issue` | **id**, **reason**, project | 9-Cancelled, reason replaces description |
| `release_notes` | **project**, **release** | delivered titles |

A rule violation comes back as a tool result with `isError: true` and the field messages; correct and
call again. An unknown tool is a protocol error.

## 7. REST: `/api/v1`

JSON in and out. Every read also answers plain text with `?format=text` (or `Accept: text/plain`). An
issue in JSON:

```json
{ "key": "NH00661", "project": "NotHangman.com", "number": 661, "prefix": "NH", "priority": 1,
  "status": "1-Blocked", "title": "…", "description": "…", "release": "", "delivered": "",
  "urgency": 1, "size": "S", "version": 4, "createdAt": "…", "createdBy": "…", "updatedAt": "…", "updatedBy": "…" }
```

Blank optional fields are `""` (release, delivered, size) or `null` (urgency, priority on a closed row).
Add `?project=<slug>` to any `/issues/:id…` route when the ID's prefix may be shared.

| Method and path | Body | Answers |
|---|---|---|
| `GET /me` | | who you are, role, workspace |
| `GET /projects` | | projects with `prefixes`, `openCount`, `closedCount`, `nextKey` |
| `GET /issues?project=&scope=open\|active\|closed\|all&limit=` | | `{ issues, cursor }` in work order |
| `GET /issues/:id` | | `{ issue }` |
| `GET /issues/:id/history` | | `{ events: [{ at, actor, action, changes: { field: [old, new] } }] }` |
| `POST /projects/:project/issues` | `{ title, description, priority?, status?, urgency?, size?, prefix? }` | 201 `{ issue }` |
| `PATCH /issues/:id` | `{ field: value, …, version? }` | `{ issue }` |
| `POST /issues/bulk` | `{ items: [{ key, project?, version? }], patch: { field: value } }` | `{ updated, refused, results: [{ key, ok, status?, error?, issue }] }` |
| `POST /issues/:id/ship` | `{ release }` | `{ issue }` |
| `POST /issues/:id/deliver` | `{ release?, date? }` | `{ issue }` |
| `POST /issues/:id/cancel` | `{ reason }` | `{ issue }` |
| `GET /changes?since=<cursor>&project=` | | `{ cursor, issues }` changed since the cursor |
| `GET /projects/:project/check` | | `{ open, problems, notes }` |
| `GET /projects/:project/releases` | | `{ releases: [{ name, delivered, issues }] }` newest first |
| `GET /projects/:project/notes/:release` | | text, one `- title` per line |
| `GET /projects/:project/export/todo` (or `/todone`) | | the CSV file, exactly as the spec writes it |
| `POST /projects/:project/import` | `{ todo, todone }` as CSV text | `{ added, skipped, problems }` (admin) |

Admin only, for completeness: `POST/PATCH /projects…`, `POST /projects/:project/prefixes`,
`GET/POST/PATCH /members`, `DELETE /invites/:email`, `GET/POST/DELETE /tokens`.

**Values:** send numbers or strings for priority and urgency; `""` clears urgency, size and release
(`-` also clears urgency and size). Text is trimmed. Line breaks are refused.

**Concurrency:** send the `version` you read with a `PATCH` to refuse a stale edit: a newer version
answers **409** with the current `issue` in the body. Read it, reapply your change if it still makes
sense, and send again. Without `version`, only the fields you send change, atomically; that is the right
choice for a single-field change like urgency.

**Change feed:** keep the `cursor` from `GET /issues`, then poll `GET /changes?since=<cursor>` and keep
the new cursor. Each answer also re-sends the last 15 seconds of writes, so drop any issue whose
`version` you already have.

**Errors:** `{ "error": "message", "errors": [{ "field", "message" }] }` (422), `{ "error", "issue" }`
(409). Status codes: 401 no or bad token, 403 role or project scope, 404 not found, 409 stale version or
ambiguous ID, 422 rules, 429 rate limit.

## 8. Requests between projects, and threads

A **request** is work one project files into another project's list. It is an ordinary issue there,
marked `from <requester>` and tied, if given, to the requester's own issue (its **parent**). The
requester may **ask, not do**: it cannot edit, accept or close the request. Only the recipient can.

Every issue has a **thread**. Entry kinds, so you can act on the ones that need you:

| Kind | Means | Rule |
|---|---|---|
| `question` | needs an answer from someone named: the recipient, the requester, or a person | from an agent it **must** carry a fallback: what you will do if nobody answers. Sessions may not run for days; a question without a fallback stalls both sides |
| `answer` | answers the oldest question waiting on your side (or `--to #id`) | refused if no question waits on you |
| `note` | information; no reply expected | |
| `comment` | anything else | |
| `closing` | written for you when a request is delivered (the delivery note) or declined (the reason) | |

While a question is open the issue shows **waiting on** its addressee (`requester`, `recipient` or
`person`). That is separate from status.

The request lifecycle:

1. **File:** `nmt request TO[,TO...] "title" "description … Done when …" [--parent ID]`. The description
   must contain a sentence beginning **Done when**. Filing the same title again (or the same `--key`)
   returns the request already filed, so a retry never duplicates. Several TO projects file one each.
2. **Receive:** it appears in the recipient's `nmt inbox` under new requests. The recipient does one of:
   `nmt accept ID` (it becomes ordinary work at the recipient's own priority; starting work also accepts
   it), `nmt ask ID "…" --else "…"`, or `nmt decline ID "why"` (closes 9-Cancelled; the ask stays as
   written; the reason goes to the requester).
3. **Finish:** `nmt deliver ID <release> --note "what was done, anything they should know"`. A request
   cannot be delivered without a note.
4. **Hear back:** the requester's `nmt inbox` lists it under "your requests that changed", with the
   last thread entry (the delivery note or the reason).

`nmt inbox` is the **one call at the start and at the end of every session**: requests to you not yet
accepted, questions waiting on you, and what changed in your requests to others since you last looked.
Reading it marks those updates seen; `--peek` reads without marking. Never poll it in a loop waiting for
another project: sessions run at different times. Do everything else, check once at the end, and let
the next session pick it up.

Credentials (tokens, keys) in a request or a thread are refused: they are read by people and by other
projects.

| CLI | MCP tool | REST |
|---|---|---|
| `nmt inbox [--peek]` | `inbox` | `GET /inbox?project=&peek=1` |
| `nmt request TO "t" "d" [--parent ID] [--key K] [--from P]` | `file_request` | `POST /projects/:to/requests {from, title, description, parent?, key?, urgency?, size?}` |
| `nmt thread ID` | `get_thread` | `GET /issues/:id/thread` (issue, comments, requests filed from it) |
| `nmt ask ID "q" --else "fallback" [--to …]` | `post_to_thread` kind question | `POST /issues/:id/comments {kind, body, fallback, to}` |
| `nmt answer ID "a" [--to #id]` / `note` / `comment` | `post_to_thread` | same, kind answer/note/comment (`replyTo`) |
| `nmt accept ID` / `nmt decline ID "why"` | `accept_request` / `decline_request` | `POST /issues/:id/accept`, `/decline {reason}` |
| `nmt deliver ID REL --note "…"` | `deliver_issue` with note | `POST /issues/:id/deliver {release, note}` |
| `nmt requests` | | `GET /requests` (every open request in the workspace) |

A token may be scoped to several projects (one session owning two repositories). Such a token writes
its own projects and files requests from any of them (`--from`).

## 9. Recipes

**Start of a session.** `nmt inbox`, then `nmt` (the work order). If the task names an issue, `nmt show ID`.

**Pick up work.** `nmt status ID 3-InProgress` when you start it; `nmt status ID 1-Blocked` and a
rewritten description saying what it waits on, when you are stopped.

**File an issue.** `nmt add 3 "Title as the release note" "One paragraph: what and why, and the
sentence that says how you will know it is done." --size S`. Check that the title reads as a user-facing
change and is ≤ 80 characters before sending; the server will refuse it otherwise.

**Finish work.** Done in code and proven locally: `nmt ship ID <release>` then `nmt deliver ID`. The
release is the project's build or version tag. Deploying and deployment testing are separate work.

**Work turns out unneeded.** `nmt cancel ID "Why, in one sentence."` The reason replaces the description.

**Re-rate a backlog.** `nmt set NH00025 NH00037 NH00042 priority=4 urgency=10` (one bulk call).

**Edit the ask.** `nmt set ID description="…"`: rewrite the whole paragraph as the current ask.

**Release notes.** `nmt notes <release>`.

**Keep a TODO.csv snapshot in the repository.** `nmt export` writes both files in the spec's format.

**Ask another project for work.** `nmt request NotUserError "Title as their release note" "What and
why. Done when …" --parent <your issue>`, then carry on; `nmt inbox` tells you when it moves.

**Answer a request.** `nmt thread ID` to read it; `nmt accept ID`, or `nmt ask ID "…" --else "…"`, or
`nmt decline ID "why"`; later `nmt deliver ID <release> --note "…"`.

## 10. Conduct

- Write only to your own folder's project; use its scoped token. Read other projects freely. When
  another project must do something, file a request; never edit its issues.
- Change only the issues you were asked to change. If a nearby issue looks wrong, say so; leave it.
- Titles are release notes. Descriptions are the current ask, rewritten, never a log.
- Never reopen a closed issue; file a new one.
- When you finish a piece of work, update the issue and show the open work order to the person you
  are working for.
