ticketing · diff

git:20261002.3e13332 to git:20261004.cbf4f27

2 added, 1 removed. Audit A to A.

---
name: ticketing
description: Local-private task/ticket management for the whiteboard repo. Issues and notes live in the whiteboard itself (via MCP tools) as OKF Markdown documents. The native Task list tracks in-flight session work. Use when triaging findings, tracking work across dev-loop/review/dogfood-triage/reconcile/plan-initiative, or recording/resolving a follow-up.
---
# Ticketing (local-private)
We deliberately do NOT use GitHub Issues. Two layers + a bridge:
## Two layers
| Layer | What | Lifetime |
|---|---|---|
| **Native Task list** (`TaskCreate`/`TaskList`/`TaskUpdate`/`TaskGet`) | the LIVE board: what is running / blocked / done across workflows this session | session/team-scoped |
| **Whiteboard documents** (MCP tools) | the DURABLE private backlog: issues & notes stored as OKF Markdown documents in the `default` workspace | persists on disk via the daemon |
The **main session (integrator / team-lead) owns Task status transitions**. Subagents may update `metadata` but not flip status to completed (mirrors the team workflow).
## Whiteboard as the issue store
Issues and notes are stored as documents in the whiteboard's `default` workspace. Each is an OKF Markdown document with:
- **type**: `issue` or `note`
- **title**: human-readable title
- **facets**: extension metadata. There is no agreed schema for issue metadata
right now — see "No structured status" below before inventing one
- **body**: the issue description as markdown
### Creating an issue
One call. `kind` is required, and `markdown` on the create op is what saves the
separate `document.set` this flow used to need.
```
wb_workspace_edit → { workspaceId: "default", ops: [
{ op: "document.create", path: "issues/my-issue",
kind: "markdown", name: "My Issue",
markdown: "---\ntype: issue\ntitle: My Issue\n---\n\nDescription here." } ] }
```
Several at once — seeding a triage pass, say — are more ops in the same call,
not more calls. `wb_workspace_edit` is the ONLY tool for create / set / delete;
the standalone ones are retired.
### Reading issues
```
wb_document_list → { workspaceId: "default" } # every document, open and closed
wb_document_get → { workspaceId: "default", documentIds: [ … ] }
```
`wb_document_get` is a BATCH read: `documentIds` is an array, and it answers
`{ documents, failed }`. Each entry is `{ documentId, kind, content,
frontmatter? }` — the body is **`content`**, not `markdown`, and `frontmatter`
is present only for a markdown document (a JSON Canvas document has none, per
ADR-0009 decision 3). A document it could not read lands in `failed` with a
reason rather than failing the call, so **check `failed`**: a caller that reads
`documents[0]` alone treats an unreadable document as an empty one.
### Updating an issue
**Amending the BODY — a measurement, a refutation, a status line — is
`wb_body_edit`**, which replaces the passages you quote and leaves the rest
of the document alone:
```
wb_body_edit → { workspaceId: "default", documentId, mode: "apply",
ops: [{ id: "<yours>", op: "body.replace",
anchor: { kind: "text", start, end, quote: { exact: "<the passage>" } },
assumed: "<what that passage said when you wrote this>",
text: "<what it becomes>" }] }
```
`mode: "apply"` because the backlog is not a surface anyone is watching — the
`propose` default is for a document a person has open. `start`/`end` may be
rough: the QUOTE is what finds the passage, and `assumed` is what refuses the
edit BY NAME if that passage has changed since you read it, rather than
overwriting somebody else's sentence.
Why this rather than the re-import below: an issue accumulates, so the ones
worth amending are the long ones. Re-importing a 20k-character backlog to add
one paragraph re-sends every other paragraph too — expensive, and every
passage retyped on the way is a passage that can come back subtly wrong.
Measured 2026-09-22 on `issues/audit-2026-09-21-backlog`: two `body.replace`
ops, offsets guessed to the nearest thousand characters, both applied.
**Re-import the whole document** (`document.set`) when the change is to the
FRONTMATTER — resolution, tags, sources — since that is what it replaces:
```
wb_workspace_edit → { workspaceId: "default",
ops: [{ op: "document.set", documentId, markdown: "<updated OKF>" }] }
```
It overwrites facets AND body together, which is exactly the hazard the
Resolving section below is about: read the document first.
### Updating facets only
```
wb_facet_set → { workspaceId: "default", documentId, facets: { "<namespace>.<name>/v0": { … } } # key grammar per ADR-0013 }
```
### Resolving
**Resolution is `type: issue` -> `type: note`, not deletion.** Re-import the
document with the type changed, the name prefixed `RESOLVED — `, and a line
in the body saying what resolved it.
**Read it first.** `document.set` replaces core facets, extension facets and
body together — it does not merge — so a resolution that names only `type` and
`title` silently drops `tags`, `facets`, and any root key the document arrived
with (`status`, `sources`, `stale_after`, all preserved verbatim since
[ADR-0016](../../../docs/contributing/adr/0016-okf-trust-family.md)). Under the
old convention that cost nothing, because the document was about to be deleted.
Now it is the whole point.
```
wb_document_get → { workspaceId: "default", documentIds: [documentId] }
# take the returned `content`, change ONLY these, keep everything else:
# type: issue -> note
# title: prefixed "RESOLVED — "
# body: a line at the top saying what closed it
wb_workspace_edit → { workspaceId: "default",
ops: [{ op: "document.set", documentId, markdown: <the edited whole> }] }
```
Both halves earn their place, and neither invents a schema:
- `type` is core OKF and already means this. A resolved issue stopped being a
work item and became a record, which is what a note is. `type: issue` stays
the query for "what is open" — the same thing that identified an issue
before.
- The `RESOLVED — ` name prefix is what a reader sees. `wb_document_list`
returns `documentId`, `path` and `name` and no frontmatter, so without it
telling an open issue from a closed one costs one `wb_document_get` per
document. Someone reached for this prefix by hand before it was written
down, which is how it got here. If `wb_document_list` ever reports `type`,
the prefix becomes redundant.
### Deleting
Deletion is for a document that was never worth keeping — a scratch probe, a
duplicate, a mistake. It is **not** how work is closed.
```
wb_workspace_edit → { workspaceId: "default",
ops: [{ op: "document.delete", documentId }] }
```
Resolution used to be deletion, and that was carried over from when issues
were markdown files under `tmp/` — where deleting the file was the only move
available. It was never a decision about this store, and it cost real work:
what an issue accumulates is measurements and refuted hypotheses, and deleting
it throws exactly those away while leaving nothing to stop the same issue
being filed and investigated again. Observed: one flake was investigated,
filed, deleted as resolved, and re-filed by another session within minutes,
its analysis redone from scratch.
### No structured status
The `issue/1` facet domain was retired: it was implemented by an earlier pass
without an agreed schema, and a typed companion nobody called made it look
more settled than it was. Nothing has replaced it, and nothing needs to for
open-vs-closed — that is what `type` above carries, using a core OKF field
rather than a domain of our own.
So an issue document carries `type`, a `title`, and a body — and no
machine-readable priority or assignee. There is no evidence either is needed:
one person works this backlog, and the Task list already carries in-flight
state. Inventing one in passing makes it the
convention by accident.
Do not invent a replacement domain in passing. Extension facets round-trip
unvalidated through the generic bucket, so anything you write will persist and
quietly become the convention. Agree the schema first.
### Declaring what an issue is about
An issue MAY declare OKF `sources` — the files or documents its claims are
about. Nothing requires it, and a document without any is simply not judged.
```
sources:
- resource: apps/web/src/components/workspace-files/SearchResults.tsx
- resource: packages/server-core/src/tools/document-search.ts
```
`node .claude/scripts/stale-issues.mjs` then reports the open issues whose
sources have changed or been deleted since the document's own `generated.at`.
A bare string (`- apps/web/src/main.tsx`) is read the same way as an OKF
`- resource:` entry — half this backlog is written that way, and the two name
the same path. It reads the MAIN checkout's daemon whatever worktree it runs
- in, because that is the one every session's MCP client reaches.
+ in, because that is the one every session's MCP client reaches
+ (`wire-worktree-mcp` registers only the main checkout's proxy: the CLI keeps one slot per repository).
No new storage: `sources` rides the preserved-root-keys bucket, `generated.at`
is stamped by the write, and git already knows what moved. It also runs on
SessionStart, quietly, so a stale issue announces itself rather than waiting to
be re-read.
What it is worth, measured on the six issues one session read and found
already resolved: **four** named something that had since changed or been
deleted. The two it missed share a shape — the fix landed in a file the issue
never named — and that is the honest ceiling: `sources` says what a document is
ABOUT, not where a fix will land. It also cannot see the other failure at all,
a document that was simply wrong when written; the two most expensive
investigations in that session were of that kind.
So it reports "what this is about moved", never "this is resolved". Re-read
before closing anything.
### The other axis: is the write-up still true?
Open-vs-closed is about the WORK. Whether the document's own account is still
accurate is a separate question, and the two move independently — a closed
issue can hold a standing fact worth reading, and an open one can be built on
reasoning since disproved. Both happened in one session.
That axis is OKF v0.2's `status` (`draft` / `stable` / `deprecated`, absent
meaning `stable`), which this codebase preserves on documents that carry it.
Nothing produces or reads it yet, and it should stay that way until something
does — a signal nobody reads is inventory. Do NOT reach for `deprecated` to
mean "resolved": it says the account no longer holds, which would misreport
every closed issue whose write-up is still exactly right.
## tmp/ workspace buckets
Put temporary artifacts in the right bucket (never the root of `tmp/`):
- `tmp/issues/` — legacy issue source files (migrated to whiteboard documents)
- `tmp/notes/` — legacy design docs (migrated to whiteboard documents)
- `tmp/screenshots/` — UI captures during debug/verify
- `tmp/scripts/` — throwaway helper scripts
For **new** issues and notes, create them directly in the whiteboard via MCP tools.
Delete artifacts from `tmp/` when they're no longer useful.
## Bridge (Task list ⇄ whiteboard documents)
- **Session start**: `TaskList` to see live state; for open issues you intend to work this session, `TaskCreate` a task with `metadata.documentPath = "<path>"`.
- **In flight**: `TaskUpdate` status/owner/blockedBy as work moves.
- **New finding** (from review / dogfood-triage / reconcile): create a document via one `wb_workspace_edit` `document.create` op (the body rides on it); `TaskCreate` only if you're acting on it now.
- **Resolve**: a `document.set` op writes the document to `type: note` FIRST (read it back first — see Resolving), then `TaskUpdate status=completed` once that write has succeeded. The other order leaves the Task list saying done while the issue is still open if the write fails. Do not delete the document.
- Workflows can't call the Task tools or AskUserQuestion; they RETURN findings/openQuestions and the main session records them as tickets/tasks and asks the human.
## When to use which
- Orchestrating several workflow runs / parallel dev-loops right now → **Task list**.
- "Don't lose this for later" → **whiteboard document** (`type: issue`).
- Both, for anything you're actively working from the backlog (create the task, link the document path).