Skip to content

issues

One Issue object over every store — a concern bound to nodes, with its own lifecycle. Local forum threads and forge issues are the same type behind a per-issue storage adapter; one merged read port serves the CLI, the API, and the board.

raw source

A proposal in the git forum and an issue on a forge are literally the same object on the proper abstract level — a recorded concern bound to spec node(s), carrying its own lifecycle, living beside the graph and never as node state. Where one is stored is a property of the individual issue, not a mode of the project: a project has both at once, mixed — an agent's taste concern living local next to a human-visible GitHub issue, on the same nodes. Don't build two parallel systems and promise a bridge; build the one object and let the stores be adapters.

expanded spec

The core type. An Issue is { id, store, concern, by, status, nodes[], signers[], created, body, replies[], evidence[], url? }. store names the adapter that holds it (local, or a forge host like github) — data, not a mode. There is deliberately no content-kind taxonomy: a field that does no mechanical work (nothing branches on it) is a label, not structure — what a thread is (a change proposal, an annotation, a question) is what its prose says. nodes[] is the binding to the graph; status is the issue's OWN lifecycle, authored in its store, never git-derived (a node defines, an issue doesspec-forge's two-plane contract holds here unchanged). evidence[] is a list of yatsu content-addressed blob hashes — the typed target video-evidence points at when a video finding routes to the responsible node's concern.

Two stores, one translation rule. The local store is the forum (proposals owns its whole mechanism — venue, file format, lock, trunk commit); a forum thread is a local Issue, its store implied by where it lives, never written into the file. The forge store rides spec-forge's tracer read: a ForgeIssue becomes an Issue at this boundary — id <host>#<number>, title → concern, state → status, its comments → replies[] (the SAME Reply shape a forum thread carries — both stores' discussions are one thread type, so every surface renders one kind of thread), and the host's node-naming conventions (Spec: body marker, transitive PR links — links) translated into nodes[] here, so product semantics see only nodes[] and never know a marker existed. Platform differences live at the adapter boundary; nothing downstream branches on store.

One read, differently freshened — and ONE time line. mergedIssues(forgeState, nodeIds) is a pure merge that interleaves every store by creation time, newest first — the stores are the same abstraction, so a github issue, a gitlab issue, and a local thread sort as one list, never store-grouped blocks (that grouping is exactly the two-surfaces smell this node exists to kill). Each caller supplies the forge slice at the freshness its surface warrants — the server (dashboard-issues's resident cache: instant view, background reconcile) for GET /api/issues and the board fold, the CLI (spex issues [--node] [--store] [--all] [--json]) via a live driver pull that degrades loudly to local-only (one stderr note) when the forge is unreachable — local reading never hostages on a network. The board fold attaches each node's merged issues (issues / open subset openIssues), so every per-node surface — tile badge, focus panel, node-info Issues tab, the issues-view page — reads the same mixed set with no second path.

Writes stay where they're owned — and the reply verb routes by store. Opening, signing, and resolving (spex propose / sign / resolve, and the dashboard's New) stay local-store writes. Replying is ONE verb over both stores (replyIssuespex propose reply <id> and POST /api/issues/:id/reply are the same routing): a local id goes through the forum's committed write, a forge id (<host>#<n>) posts a real comment through the driver's createComment — the port's second write verb, the same seam discipline as promotion (the driver stays the only network toucher; the tracer stays read-only; a failed forge write fails loud, never queues). A local reply may carry optional evidence hashes (an anchored annotation's frame blob) that accrue onto the thread's typed evidence[], deduped — a forge reply has no such field, so its frame rides the comment body's image link instead. Either way the reply text's @-mentions dispatch afterward (mentions fires on the words, store-agnostic) — mentioning @new/@session in any thread IS the "assign this issue to an agent" verb; no separate assign machinery exists. Beside that explicit dispatch, the same reply also loops in the thread's originator as a courtesy if their session is online (the implicit loop-in — mentions owns the mechanism, silent when offline, never a spawn); the originator is a local thread's author, or an eval-comment thread's reading-filer, and a forge issue's github-login author resolves to nobody, so a forge reply loops in no one. Freshness after a forge write stays caller-owned: the server forces its resident slice's read-back before answering (the comment shown is the read-back, never a local echo); the CLI's next read is a live pull anyway. The one cross-store verb is promotionspex issues promote <id>: a local concern that outgrows the repo (needs CI or external visibility) moves to the forge as one recorded action instead of a lossy hand-copy. It composes the forge issue from the thread itself — concern → title; body + the Spec: <nodes> marker + the evidence hashes + a provenance footer — and creates it through the port's driver (the driver stays the only thing that touches the network; no second gh call-site). The marker is the round-trip: the promoted issue links back to the same nodes through the EXISTING tracer read, so promotion adds no linking code. Order makes failure safe: the forge issue is created FIRST, and only then is the local thread closed out — resolved landed with a reply carrying the permalink (its file remains as the recorded trail); an unreachable forge fails loud with the local thread untouched, and only an open thread promotes. The two-plane contract is untouched throughout: a forge issue is execution, never node state.