proposals¶
raw source¶
An agent finishing a task notices things that feel off — a smell, an awkward boundary, a wish — often unrelated to its mainline. That judgment is taste, and it must not evaporate when the session ends. So a finished session records such concerns into one shared, durable forum; other sessions sign and discuss them like an async chatroom; a supervisor later drains the forum into real work. This keeps global taste flowing into the codebase's shape, instead of every agent owning only its own slice.
The forum is narrow on purpose — it is not an issue tracker. A local issue earns its place only when it
will outlive the task at hand, in one of exactly two shapes: (a) an off-mainline proposal / taste —
a concern you are not going to act on now; or (b) a not-worth-a-spec to-do — something
trivial-but-must-not-be-forgotten that doesn't merit a spec node. That's it. It is deliberately not a
general bug tracker: tracking work is what the spec graph (the definition) and the forge (the
execution) already do, and a forum that competes with them stops being a durable taste-layer and becomes a
second, drifting source of truth — a liability. Two anti-patterns follow directly: don't open an issue for
a task you're actively driving — you'll do it and then forget the thread; the design of dispatched work
rides its dispatch, not a lingering issue. And close what you finish — an issue whose work has landed is
resolved (landed/accepted), never left open; the open set is the outstanding work, so a stale open is a
lie the drain has to re-triage. The post-merge nudge fires at exactly the moment both disciplines apply (you
just landed work), and states them.
expanded spec¶
The forum is the local store of issues — the venue where a locally-stored Issue lives. A thread
is an Issue whose store is local; that store membership is implied by where the file lives,
never written into it. This node owns the store's whole mechanism: the venue, the file format, the write
verbs, the concurrency discipline, and the post-merge nudge. Reading is not this node's surface — the one
read over every store (CLI spex issues, GET /api/issues, the board fold) is issues's port.
The forum is git-tracked data, not a spec node. A thread reuses almost nothing of the spec-node
contract — no title/hue/desc/code frontmatter, no parent-ancestor nesting, no lint, no drift, no
version-from-spec.md-log, no graph render — so forcing it into a spec.md would only earn it a pair of
graph-exemptions to blind it again. Instead each thread is a plain markdown file at
<root>/.spec/.forum/<id>.md. Because that file is not named spec.md, the spec walk descends past
it without making a node and isSpecMd ignores it: the forum is invisible to lint / drift / deriveStatus /
overlay structurally, with no special-case exemption. It lives inside .spec (not a second
top-level folder) so adopting SpexCode still adds one directory — matching how the reflexive .config
system already nests there.
- One kind of thread — the prose says what it is. A change proposal, a durable annotation, a heads-up,
a Q&A: all the same mechanism, distinguished by nothing but their own words. There is deliberately no
kindtaxonomy field (issues): it would do no mechanical work — the verbs are id-based, dispatch doesn't branch on it, the drain is judgment — so it would be a label bought with a second creation verb and a filter. The forum is the git-native discussion/annotation layer over the graph. - One file per thread. The file is a one-line
concernplus a prose body plus appended signed replies — each reply preceded by a<!-- reply: <by> @ <at> -->sentinel line. Its frontmatter carriesby(author session),status, optionalnodes:(the product nodes it concerns, linked**…**), optionalevidence:(yatsu content-addressed blob hashes — the typed reference a cross-node finding carries, per issues / video-evidence), andsigners. The sentinel is unforgeable: user body text is neutralized on write, so a body that itself contains that marker can't spawn a phantom reply or truncate the thread. - Own lifecycle status, forum-authored never git-derived:
open→accepted | rejected | landed. - The forum lives on the trunk, not per-branch. A write reads and commits straight to the main
checkout's
.spec/.forum/— a forum file is data, not contract, and the write below commits it with--no-verify(provably a single.spec/.forum/path), so it lands on the trunk without needing any main-guard exception. So there is no per-branch copy and no cross-worktree union to reconcile: every thread is always present to read, sign, and reply to. This is also what lets a post-merge proposal land durably — the author's own branch has already merged, so a proposal written then could never ride it; committed to the trunk directly, it persists. - Writes are serialized + fast, so a burst can't corrupt the forum. The whole read-mutate-write-commit of
one thread runs under a single cross-process forum lock (an atomic
.gitdir-lock, stale-stolen), so concurrent writers can neither collide on the repo index nor lose a racing reply (last-writer-wins is impossible — each read is under the lock). The commit itself is--no-verify: the file is data, structurally invisible to lint, and the commit is provably a single.spec/.forum/path, so running the seconds-long pre-commit gate would only pass anyway — pure overhead that would hold the lock. The id is minted under the lock too, so two racing posts can't claim it. - Nudged AFTER the work lands, not during it. The agent's own task is what matters most, so the forum is
never raised while it is still finishing — it is raised the moment the work merges. A
post-mergegit hook (harness-side gates live in state; this one is git-side) fires in the doer's dispatched merge turn — merge is dispatched to the session's own agent (see dispatch) — guarded to themerge node/<id>:commit so an ordinary pull never nags; its nudge lands in the agent's own command output: read the issues, sign/reply if the concern is already raised, else open a new one. Git-native, so it reaches a self-launched agent too and costs no harness block-cap. - Surface — the write verbs.
spex propose "<concern>" [--node <id>…] [--evidence <hash>…] [--body -|<text>]opens a thread;propose reply <id> --body -|<text> [--evidence <hash>…](the evidence a reply carries accrues onto the thread'sevidence[], deduped — an anchored annotation's frame blob),sign, andresolveact on any local thread. There is deliberately no store-local read command — reading isspex issues(issues), the same list every store feeds. - A human writes too — the forum is the programmatic surface. The same write verbs carry an optional
author(default the effective session id, else a caller-passed'human'), so a person can post from outside the CLI.forumReply(id, body, author)andforumPost(concern, {nodes, body, author})are the programmatic entrypoints: each does the git-committed write AND then dispatches any@-mention in the text (mentions).forumReplyadditionally loops in the thread's originator — its author, or, for an eval-comment thread (concerneval: <node> · <scenario>), the reading's filer — as a courtesy if online (the implicit loop-in, mentions);forumPostopens a new thread whose originator is the poster, so it loops in no one. Each returns{ thread, outcomes, loopIn }. Because the forum is the programmatic surface, a human's@-mention in a reply does summon the agent — that is the point. The dashboard's write path (issues-view) is a thin caller:POST /api/issues/:id/replyandPOST /api/issues(author'human'), both gated by the same on/off switch (403 when OFF). - Opt-outable, default ON. The forum workflow is a feature you can switch off:
spex propose on|offflipsspexcode.json'sproposals.enabled(the shared settings file every other toggle lives in), effective immediately with no commit (config is read from the working tree). OFF silences the post-merge nudge and hides the dashboard issues view; the raw write verbs stay usable, since running one is explicit consent. The nudge text and the toggle both live in the CLI (spex propose nudge <node>prints nothing when OFF), so the post-merge hook is a thin caller and the dashboard's Settings toggle is a thin wrapper over this same switch — one source of truth, three consumers (CLI, hook, dashboard). - Dedup is the drain's job, not the write's. Duplicate proposals are a signal (recurrence), folded into one thread by a supervisor's judgment (supervisor) — never a write-time similarity match. And recurrence is weighed as salience, not importance: a sharp singleton outranks a popular gripe, so the count never becomes the priority ranking.
The dashboard renders and writes to this same forum through issues-view — a thin caller over the programmatic write surface above, never a second store.