# Operation Catalog

Full argument reference for the workflow operations. `SKILL.md` carries the
one-line index; this file carries the flags.

Hot-path lifecycle operations are MCP tools — see the invocation matrix in
`SKILL.md`. Everything else runs through the runner:

```bash
flydocs run <operation-id> [args]
```

Each table below gives the operation ID and the dispatcher subcommand it
fronts. The ID is what you type; the script column is the fallback spelling for
a harness with no `flydocs` binary on PATH, and for reading the source.

`flydocs run --list` prints every ID. There is no per-operation `--help`: the
arguments are here, and an invocation the registry cannot accept is refused
with the accepted shape printed.

**No `cd`.** The runner resolves the repo that owns `.flydocs/config.json` from
the working directory. In a multi-repo workspace, name the repo:

```bash
flydocs run issue.list --focused              # from anywhere in a repo
flydocs run issue.list --repo repo-a --focused # from the workspace root
```

The dispatcher scripts still work when invoked directly
(`python3 .claude/skills/flydocs-workflow/scripts/<script>`), and still need
the repo as their working directory when you do.

**Long bodies go in a file**, never inline in an operation argument: write to
`.flydocs/scratch/`, pass `--file`, delete it. See "Long Inputs on the Runner"
in `SKILL.md`.

## Tier routing

The unified client (`flydocs_api.py`) reads tier from `.flydocs/config.json`
and routes operations automatically:

- **Cloud tier** → RelayBackend (HTTP calls to FlyDocs relay API)
- **Local tier** → LocalBackend (filesystem operations in `_local/file_store.py`)

Scripts never check tier. The client handles routing transparently. Commands
marked "cloud only" below no-op or error on the local tier.

## Issue Operations — `issues.py`

| Operation                | Script command     | Usage                                                                                                                                                                          | Notes                                                                                  |
| ------------------------ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- |
| `issue.create`           | `create`           | `--title T --type feature\|bug\|chore\|idea [--description D] [--file F] [--priority 0-4] [--estimate N] [--assignee A] [--project P] [--milestone M] [--template] [--triage]` | `--file` aliases `--description-file`; estimate is non-negative, on the provider scale |
| `issue.get`              | `get`              | `REF [--fields basic\|full]`                                                                                                                                                   | Full includes comments                                                                 |
| `issue.list`             | `list`             | `[--status S] [--active] [--project P] [--assignee A] [--milestone M] [--mine] [--all] [--limit N] [--sprint S] [--board B] [--focused]`                                       | `--focused` = smart filter by board type                                               |
| `issue.transition`       | `transition`       | `REF STATUS COMMENT [--force PROVIDER_STATUS]`                                                                                                                                 | Canonical only; `--force` for stuck workflows                                          |
| `issue.assign`           | `assign`           | `REF ASSIGNEE` or `REF --unassign`                                                                                                                                             |                                                                                        |
| `issue.update`           | `update`           | `REF [--title] [--priority] [--estimate] [--assignee] [--state] [--labels] [--milestone] [--due-date] [--comment] [--project]`                                                 | Fields only. `--description` / `--file` are RETIRED (FLY-1469) — refused with a pointer to `issue.description` |
| `issue.description`      | `description`      | `REF [--text T] [--file F] [--expected-revision REV] [--expected-description-hash HASH]`                                                                                        | Prose edits. Whole-description replace. On cloud, `--file` REQUIRES one of the two guards (both from `issue.get`); prefer the hash |
| `issue.acceptance`       | `acceptance`       | `REF [--check N] [--uncheck N] [--defer N:REF] [--note N:TEXT]`                                                                                                                | Checkbox edits by number. Cloud only                                                   |
| `issue.comment`          | `comment`          | `REF [BODY]`                                                                                                                                                                   | Body is a positional; no `--file` (see below)                                          |
| `issue.estimate`         | `estimate`         | `REF POINTS`                                                                                                                                                                   | Non-negative integer                                                                   |
| `issue.priority`         | `priority`         | `REF LEVEL`                                                                                                                                                                    | 0 (none) to 4 (low)                                                                    |
| `issue.link`             | `link`             | `REF RELATED_REF TYPE`                                                                                                                                                         | Type: blocks, related, duplicate                                                       |
| `issue.assign-milestone` | `assign-milestone` | `REF MILESTONE_ID`                                                                                                                                                             | Cloud only                                                                             |
| `issue.assign-sprint`    | `assign-sprint`    | `REF [SPRINT_ID\|current\|next\|previous]`                                                                                                                                     | Cloud only                                                                             |
| `issue.assign-cycle`     | `assign-cycle`     | `REF [CYCLE_ID]`                                                                                                                                                               | Deprecated — use `issue.assign-sprint`                                                 |
| `issue.pr`               | `pr`               | `[--issue REF] [--title T] [--base B] [--changes C] [--test-plan T] [--notes N] [--draft] [--dry-run]`                                                                         | Detects GitHub/GitLab, auto-populates from issue                                       |
| `issue.audit`            | `audit`            | `[--status S] [--limit N] [--deep]`                                                                                                                                            | Check recent issues for compliance gaps                                                |
| `issue.fix`              | `fix`              | `REF`                                                                                                                                                                          | Fill missing fields from config defaults                                               |

Repeatable flags (`--check`, `--uncheck`, `--defer`, `--note`) are passed once
per value on the runner: `--check 1 --check 3`.

`issue.comment` and `issue.transition` take their body as a positional and have
no file flag. That is deliberate rather than an oversight: both are hot-path
MCP tools, where the body is a structured string argument and the problem does
not arise. On the runner, keep those bodies to something a command line can
carry — a comment that needs a file is a description, and belongs on
`issue.description`. The same applies to `issue.pr`'s `--changes`,
`--test-plan` and `--notes`, which are bullet lists, not documents.

### Description writes: `issue.description` (FLY-1468 / FLY-1470)

The write replaces the whole document, so it carries a guard proving what it
was written against. There are two, and `issue.get` returns both on the basic
field set:

```bash
flydocs run issue.get FLY-100          # note `revision` and `descriptionHash`
flydocs run issue.description FLY-100 \
  --file .flydocs/scratch/fly-100-description.md \
  --expected-description-hash <HASH>
```

`<HASH>` is the `descriptionHash` field that `issue.get` returned; `<REVISION>`
below is the `revision` field from the same read.

- **`--expected-description-hash <HASH>` is the one to prefer.** It is a digest of the
  description you read, so it refuses exactly when someone else edited the
  prose — `DESCRIPTION_CHANGED`, with the fresh `currentHash` to recover from.
- **`--expected-revision <REVISION>` is a last-modified timestamp.** It refuses when
  *anything* about the issue moved, including a status change that touched no
  prose. Read an issue, draft a description, transition to IMPLEMENTING, and
  the token you read is already stale.
- **Either one satisfies the cloud `--file` refusal**; neither present is
  refused before anything is read or written. `--text` and stdin fall back to a
  self-read and warn about what it does not cover.
- **A matched hash overrides a stale revision.** Send both and the write goes
  through, because the prose is provably unchanged.
- **The command never retries.** Re-sending text written against the old
  document is the clobber the guard exists to prevent. To tick a checkbox, use
  `issue.acceptance` — that route merges line-surgically and may retry.

### Acceptance criteria: `issue.acceptance` (FLY-1265)

Criteria are addressed by their number — the `id` in the `acceptance` array
that `issue.get` returns, counting every checkbox in the description in
document order.

```bash
flydocs run issue.acceptance FLY-100 --check 1 --check 3
flydocs run issue.acceptance FLY-100 --defer 5:FLY-1234 --uncheck 2
flydocs run issue.acceptance FLY-100 --note 4:"partially covered by the smoke test"
```

- **The command reads before it writes.** It fetches the issue, echoes the
  first 80 characters of each targeted criterion back as a guard, and sends the
  issue's revision token. A criterion that moved under it is a rejection, never
  a wrong box flipped.
- **One status per criterion per run.** A note is its own change, so
  `--uncheck 2 --note 2:"…"` is two commands, not one — the command says so
  rather than letting the relay answer with a 400.
- **`--defer` needs a destination** (`N:FLY-1234`, FLY-1087). The box stays
  unchecked; the marker is what the Stop gate counts as accounted for.
- **Failures name the fresh state.** `REVISION_MISMATCH` retries itself once
  when the targeted criteria are unmoved, and otherwise prints the criteria as
  they now read. `CRITERION_MISMATCH` prints which guard failed plus the same
  list. Either way nothing was written.
- **Cloud tier only** — the merge runs on the relay. On local tier, edit the
  description with `issue.description`.

### Listing: focused vs wider reads (FLY-1098)

`issue.list --focused` resolves to the narrowest signal the workspace actually has —
active sprint, else board, else your open assigned work — so the agent opens on
what is in flight rather than the whole backlog.

| Use                          | When                                                     |
| ---------------------------- | -------------------------------------------------------- |
| `issue.list --focused`       | Default. What the developer is working on now.           |
| `issue.list --mine --active` | Their wider assigned backlog. Use for counts, not dumps. |
| `issue.list --project <ref>` | A specific project. Explicit opt-out from focus.         |
| `issue.list --all`           | Everything. Rarely correct; say why you needed it.       |

Focus narrows attention, it does not restrict access — out-of-focus work is
always reachable on request. When focus cannot be resolved, `--focused` returns
your open assigned issues and says so on stderr; it never widens to the full
project.

## Project, Milestone and Sprint Operations — `projects.py` (cloud only)

| Operation            | Script command     | Usage                                                                    |
| -------------------- | ------------------ | ------------------------------------------------------------------------ |
| `project.list`       | `list-projects`    | `[--active] [--all]`                                                     |
| `project.create`     | `create-project`   | `--name N [--description D]`                                             |
| `project.update`     | `update-project`   | `REF [--name N] [--description D] [--state active\|completed\|canceled]` |
| `project.archive`    | `archive-project`  | `REF`                                                                    |
| `milestone.list`     | `list-milestones`  | `[--all]`                                                                |
| `milestone.create`   | `create-milestone` | `--name N [--project P] [--target-date D]`                               |
| `milestone.update`   | `update-milestone` | `ID [--name] [--target-date] [--description]`                            |
| `milestone.delete`   | `delete-milestone` | `ID`                                                                     |
| `sprint.list`        | `list-sprints`     | `[--active] [--future] [--closed] [--current] [--all]`                   |
| `sprint.list-cycles` | `list-cycles`      | `[--active]` — deprecated, use `sprint.list`                             |

## Workspace Operations — `workspace.py` (cloud only)

| Operation                         | Script command          | Usage                                                | Notes                                       |
| --------------------------------- | ----------------------- | ---------------------------------------------------- | ------------------------------------------- |
| `workspace.validate`              | `validate`              | (no args)                                            | Writes validation cache, sets setupComplete |
| `workspace.list-labels`           | `list-labels`           | (no args)                                            |                                             |
| `workspace.refresh-labels`        | `refresh-labels`        | `[--fix]`                                            | Compare local config with provider          |
| `workspace.list-statuses`         | `list-statuses`         | (no args)                                            |                                             |
| `workspace.list-providers`        | `list-providers`        | (no args)                                            |                                             |
| `workspace.set-provider`          | `set-provider`          | `TYPE`                                               | linear, jira                                |
| `workspace.list-teams`            | `list-teams`            | (no args)                                            | Jira projects, NOT boards                   |
| `workspace.list-boards`           | `list-boards`           | (no args)                                            | Boards in connected project (Jira)          |
| `workspace.create-team`           | `create-team`           | `--name N [--key K] [--description D] [--parent ID]` |                                             |
| `workspace.set-team`              | `set-team`              | `TEAM_ID`                                            |                                             |
| `workspace.set-project-mapping`   | `set-project-mapping`   | `[epic\|component\|none]`                            | This repo's `projectMapsTo`                 |
| `workspace.set-labels`            | `set-labels`            | `[--defaults JSON] [--type-map JSON]`                | Also accepts stdin                          |
| `workspace.set-status-mapping`    | `set-status-mapping`    | `[--auto] [--mapping JSON]`                          | Also accepts stdin                          |
| `workspace.set-identity`          | `set-identity`          | `PROVIDER PROVIDER_USER_ID`                          | Writes .flydocs/me.json                     |
| `workspace.set-preferences`       | `set-preferences`       | `[--workspace] [--assignee] [--display JSON]`        | No flags = GET                              |
| `workspace.get-estimate-scale`    | `get-estimate-scale`    | (no args)                                            |                                             |
| `workspace.get-me`                | `get-me`                | (no args)                                            | Writes .flydocs/me.json                     |
| `workspace.set-active-project`    | `set-active-project`    | `PROJECT_ID`                                         |                                             |
| `workspace.clear-active-projects` | `clear-active-projects` | (no args)                                            |                                             |
| `workspace.set-active-sprint`     | `set-active-sprint`     | `SPRINT_ID`                                          |                                             |
| `workspace.clear-active-sprint`   | `clear-active-sprint`   | (no args)                                            |                                             |

## Session Operations — `session.py`

| Operation                | Script command   | Usage                                                                                                                              | Notes                                             |
| ------------------------ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
| `session.start-context`  | `start-context`  | (no args)                                                                                                                          | Returns all start-session data as structured JSON |
| `session.list-issues`    | `list-issues`    | `[--limit N]`                                                                                                                      | Board-scoped issue fetch with automatic fallback  |
| `session.wrap`           | `wrap`           | `[--issues ID...] [--health onTrack\|atRisk\|offTrack] [--title "..."] [--notes "..."] [--pending "..."] [--blockers "..."] [--body B] [--file F] [--visibility V] [--started-at T]` | Full wrap: record + summary + cleanup + update + graph |
| `session.project-update` | `project-update` | `--health onTrack\|atRisk\|offTrack [--body B] [--file F]`                                                                         | `--file` aliases `--body-file`                    |
| `session.status-summary` | `status-summary` | (no args)                                                                                                                          | Issue counts by status                            |

On the cloud tier `session.wrap` reaches one governed relay operation,
`sessionUpdate.create` (FLY-1410): it stores the session record, validates the
wrap body's required sections server-side, and posts the project update as a
destination of that record — falling back to the project-update route itself if
the destination refuses. There is no separate operation ID for it; the wrap is
how it is called, and `session.project-update` remains the mid-session post.
The record is appended locally to
`.flydocs/session/<workspace>/stream.jsonl` on every tier — read it with
`flydocs stream`.

`--title` is the record's one-line label (max 120 characters, single line) and
`--notes` is its `summary` (max 600), the prose a teammate reads on the
timeline and in Slack. Both are optional; omitted, the reader derives a
fragment from the wrap body instead.

## Context and Service Sync

| Operation        | Script              | Usage                                                                                           | Notes                                    |
| ---------------- | ------------------- | ----------------------------------------------------------------------------------------------- | ---------------------------------------- |
| `context.pull`   | `context.py pull`   | `[--root PATH]`                                                                                 | Pull project context from the cloud      |
| `context.repair` | `context.py repair` | `[--root PATH] [--dry-run]`                                                                     | Re-seed project.md section markers       |
| `context.push`   | `context.py push`   | `[--root PATH] [--project-md] [--service-json] [--dry-run [--with-read]] [--no-preserve-rules]` | Push locally generated context (cloud)   |
| `service.push`   | `push_service.py`   | `[--root PATH]`                                                                                 | Push service descriptor to relay (cloud) |
| `service.pull`   | `pull_services.py`  | `[--root PATH]`                                                                                 | Pull workspace composite (cloud)         |

`context.push` exists for the repo the portal cannot crawl — a GitHub
Enterprise or self-hosted host where the GitHub App cannot be installed, so the
server has no context to generate and `context.pull` returns nothing. The agent
writes the two artifacts from the repo on disk (`/generate-context`) and this
sends them to `PUT /api/relay/context`. Admin API key required.

Four things it does on purpose:

- **Only this repo's own narrative is sent.** The relay serves `project.md`
  flat: `config/generate` strips every `<!-- flydocs: … -->` marker from what
  it stores and appends the workspace's rules as `## Workspace Rules`,
  `## Repo Rules` and `## Status Workflow`. The push cuts that tail off (and
  says in its warnings what it cut) — send it back and the next read has every
  rule twice.
- **It reads before it cuts.** `stripSectionMarkers` removes the marker lines
  and keeps the section contents, so a repo whose stored document has rules
  sections is served with those bodies unlabelled in the middle and the same
  rules again under the headings. The push reads `GET /api/relay/context`
  first and subtracts the stored bodies from the end of the narrative as well
  as cutting the headed tail; without that, every cycle adds a copy. A read it
  cannot make is a refusal naming the reason — `--no-preserve-rules` pushes
  anyway, with empty rules sections in the stored document.
- **It sends the shape the portal stores.** The stored `generatedProjectMd` is
  a marked-up document (`assembleProjectMd` in the app's generation
  orchestrator), which the portal's context-rules panel parses into three
  editors. The rules sections go back byte-for-byte from that read, and the
  narrative is wrapped the same way.
- **`--dry-run` makes no request**, so its preview is of the narrative alone
  and may still carry rules the real push subtracts; `--dry-run --with-read`
  performs the read and nothing else, and previews the real payload.
- **`service.json` is validated and stamped.** `version`, `name`, `repoSlug`,
  `purpose` and `stack` must be present and non-empty, and the descriptor's
  `repoSlug` must match this repo's, or the push is refused naming what is
  wrong. It is then stamped with `generatedBy: "agent"` and `provenance`
  (branch, commit, dirty, generator, time) before it goes.

Order matters: **generate, push, then `flydocs update`** — never generate then
update. An update pulls the server's copy over the local files, so an unpushed
generation is lost.

## Knowledge Graph

| Operation           | Script                        | Usage                                                                                   | Notes                                                 |
| ------------------- | ----------------------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| `graph.build`       | `graph_build.py`              | `[--root PATH] [--workspace]`                                                           | Rebuild graph from skills, ADRs, service descriptors  |
| `graph.query`       | `graph_query.py`              | `--node ID [--depth N] [--rel TYPE]... [--direction out\|in\|both] [--format json\|md]` | BFS traversal; bad `--rel` errors with the valid list |
| `graph.session`     | `graph_session.py`            | `--summary "..." [--issue REF]... [--decision NNN]... [--session-id ID]`                | Called at session wrap; the id matches the record     |
| `graph.add-node`    | `graph_update.py add-node`    | `ID --type TYPE [--label STR] [--path P] [--status S] [--date D]`                       | Manual CRUD                                           |
| `graph.remove-node` | `graph_update.py remove-node` | `ID`                                                                                    |                                                       |
| `graph.add-edge`    | `graph_update.py add-edge`    | `FROM TO REL [--weight W] [--manual]`                                                   |                                                       |
| `graph.remove-edge` | `graph_update.py remove-edge` | `FROM TO REL`                                                                           |                                                       |

`change_context` has no operation ID — it is **MCP-only**. It joins an issue to
what actually shipped around it (commits, PRs, provenance), which is a read the
dispatchers cannot perform: it needs the relay's shipped-mode index, not the
graph on disk. On a harness with no MCP server there is no fallback for it;
`graph.query` answers a different question and should not be substituted.

## Skills

| Operation                  | Script                 | Usage                       | Notes                                                |
| -------------------------- | ---------------------- | --------------------------- | ---------------------------------------------------- |
| `skills.generate-manifest` | `generate_manifest.py` | `[--root PATH] [--dry-run]` | Regenerate the skills manifest in agent instructions |

## Related

- Graph node and edge schema: `reference/graph-schema.md`
- Service descriptor fields: `reference/service-descriptor-schema.md`
- Canonical statuses and transitions: `reference/status-workflow.md`
