ticketing · git:20260824.895c291 · 2026-08-24 · sha256 4cf719476efc43af

ticketing git:20260824.895c291A

Immutable. This exact content never changes and is served at /api/v1/blob/4cf719476efc43af.

---
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` at create time is what saves the
separate `wb_document_set` this flow used to need.

```
wb_document_create → { workspaceId: "default", 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 — go through one
`wb_workspace_edit` rather than one call per document.

### Reading issues

```
wb_document_list  → { workspaceId: "default" }              # every document, open and closed
wb_document_get   → { workspaceId: "default", documentId }  # read it as OKF markdown
```

### Updating an issue

Re-import with updated OKF markdown (overwrites facets + body):

```
wb_document_set   → { workspaceId: "default", documentId, markdown: "<updated OKF>" }
```

### 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.** `wb_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", documentId }
# take the returned markdown, change ONLY these, keep everything else:
#   type:  issue -> note
#   title: prefixed "RESOLVED — "
#   body:  a line at the top saying what closed it
wb_document_set → { workspaceId: "default", 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_document_delete → { workspaceId: "default", 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.

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`.
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 `wb_document_create` + `wb_document_set`; `TaskCreate` only if you're acting on it now.
- **Resolve**: `wb_document_set` 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).