A spec in traditional agile shape — the full Atlassian ladder, initiative → epics → user stories, with themes as the labels that span them — that opens collapsed to headlines: the initiative's goal, its epics, their story titles; one screen, no scrolling. Click a story for its acceptance criteria; click a criterion for how it will be verified. Themes are "large focus areas that span the organization" — tags, not work items: a catalog shown on the initiative, toggled onto the stories they span (auditability, performance, compliance). The complaint this fixes: a spec today is a long document nobody reads.
wireframe + product.md + the conversation ──► tree JSON ──► drill-down page ──► edits & notes ──┐
▲ │
└────────── you apply it, reply in notes, regenerate ◄────────────────┘
│
specs/YYYY-MM-DD-<feature>.md ← generated every round
The tree is the spec. .vstack/specs/<feature>.json is the source of truth; the markdown under
specs/ is generated from it, never edited by hand. There is no approve/reject — the user
changes what's wrong, and the change is the feedback.
1 · Draft the tree
Read whatever exists — the wireframe (design/<feature>.html or wherever it lives),
specs/product.md, specs/requirements.md, and the conversation. Then write
.vstack/specs/<feature>.json:
{
"feature": "candidate-pipeline",
"title": "Candidate pipeline",
"goal": "The epic statement — one sentence on what the user gets.",
"source": ["design/candidate-pipeline.html", "specs/product.md"],
"themes": ["Auditability", "Reporting"],
"epics": [
{ "id": "e1",
"title": "As a hiring manager, I want to run a role's pipeline in one place, so that no candidate stalls unseen.",
"stories": [
{ "id": "s1",
"title": "As a hiring manager, I want to see everyone who applied to a role, so that I can act on every application.",
"themes": ["Reporting"], "notes": [],
"note": "Newest application first — the sort is the point, not a detail.",
"crit": [
{ "id": "c1", "scenario": "Opening a role",
"given": "Role 42 has applications",
"when": "I open /roles/42",
"then": "The top row is the most recent application",
"and": "", "notes": [] }
] }
] }
]
}
The model is Epic → User story → Acceptance criteria inside one initiative (title + goal,
the document itself), with Atlassian's themes spanning it as labels:
- Epics and user stories are both written as "As a [persona], I want to [goal], so that [benefit]". The story is the experience — what the person wants.
- There is no separate requirements list. A requirement that isn't checkable is an opinion, and
one that is checkable is a scenario — so it goes in
crit, wherephase-buildwill test it. Writing both meant writing everything twice and letting the two drift. noteis the margin — one optional string per story for the aside that isn't a criterion: a constraint, a decision already taken, a thing not to forget. It is never a hiding place for behaviour that belongs incrit.critare the acceptance criteria, as Gherkin scenarios —scenarionames the behaviour;given/when/then(and optionaland) make it checkable with real values, not restatements. A criterion you can't write as Given/When/Then isn't done. This is also whatphase-buildlater tests against. Cover the flows, not just the demo path: every story's scenarios should span the happy flow, the sad flows (invalid input, refusals, failures), and the edge cases (empty, duplicate, boundary). One happy scenario alone is a spec that lies by omission.doc.themesis the initiative's theme catalog — plain labels, edited here, not on the page; each story'sthemesarray tags it. Keep the catalog short — a theme only one story wears isn't spanning anything.- No priorities here. Which stories land first is a phasing decision, and phasing is
/vstack:user-story-map's job — the spec says what, the story map says when. Ids are stable — never renumber existing ones on a rewrite. - A small feature is one epic; don't invent a second epic to look thorough. Keep the first pass lean — the loop is how it gets rich; a bloated v1 wastes the user's first round on deletions.
2 · Serve it
SKILL=<this skill dir>
LIB="$SKILL/../../lib"
DOC=.vstack/specs/<feature>.json
node "$LIB/json-bridge.mjs" serve --json "$DOC" --template "$SKILL/assets/spec-tree.html" --port 7791 --tool spec
Start it with run_in_background: true. It prints the URL (with its token) — tell the user to
open it. Then arm the waiter, carrying the seq the server printed:
node "$LIB/json-bridge.mjs" watch --json .vstack/specs/<feature>.json --stream --tool spec \
--seq <the seq printed when the server started>
Start it with the Monitor tool, persistent: true. How the loop behaves — it never exits, one
event per line, the Linked/Unlinked states, the idle close — is
contracts/bridge-loop.md.
SENT means an edited tree landed in the JSON; CLOSED means the tab went away — say so and
stop serving. Pass the seq the server printed — a send that lands between rounds would be
swallowed; use the seq printed by the previous waiter's output.
3 · The round
On SENT, read the JSON back:
- Edited text is the new truth. Titles, criteria, verifies, the goal, priorities, deletions, additions — apply them silently. Don't re-litigate a deletion.
- Notes are clarifications — the page presents every note as one kind of thing: a question on
the spec, with a place to answer. A user-written clarification is the user talking to you: answer
it by fixing the spec and removing the note. When something genuinely needs the user's
decision, leave your own clarification (
who: "agent"for provenance; the page renders all clarifications identically). - Prefer multiple choice when asking. A clarification can carry
"options": ["HR owns the list", "Admins only", "Either, behind a permission"]— the page renders them as radio choices and writes the pick into"answer"; without options it offers a free-text answer line. An answered clarification is a decision: apply it and remove the note in the next round. - Rewrite the JSON (the page offers a Refresh bar — it never yanks the tree mid-edit), regenerate the markdown (§4), and say in a few lines what changed. Don't ask "shall I continue?" — the loop is the point.
4 · Generate the markdown, every round
specs/YYYY-MM-DD-<feature>.md — date fixed on first creation; later rounds update the same file.
Re-running on a feature that already has a dated spec updates that file, never a second one.
<!-- Generated from .vstack/specs/<feature>.json — edit on the spec page (/vstack:spec), not here. -->
# Initiative — <title>
<goal>
**Themes:** <theme> · <theme>
## Epic — As a …, I want to …, so that …
### As a …, I want to …, so that … `<theme>`
**Acceptance criteria**
- **Scenario:** <scenario>
**Given** <given> · **When** <when> · **Then** <then> · **And** <and>
> <note> ← only when the story has one
...
The header warning matters: this file is an export. A human editing it directly is editing a generated file, and the next round will overwrite them.
Notes
- Never edit
assets/spec-tree.htmlorlib/json-bridge.mjsto fit a project — they're the engine; only the JSON document is yours. The shell chrome is stamped in fromlib/shell/— seelib/shell/README.md. - The bridge binds
127.0.0.1and dies when the tab closes (90s grace). Port busy → another spec page is up; pass--port. - The version timeline is kept on disk, in
.vstack/local/spec/<feature>.history/— one frozen copy per open, send and rewrite. It survives a reload and carries across rounds, so the page opens on the whole trail rather than starting from blank. Nothing to run: the bridge records it. If the user wants a version back, the bodies are plain JSON files. - One review is one feature. Several features means several JSON files served one at a time.
- Works beside any other tooling. vstack writes
.vstack/and plain markdown underspecs/, and touches nothing else — no.specify/, no other tool's state, no hooks, no config. If the project also uses speckit or another spec tool, both coexist: vstack does not ownspecs/, it owns only the files it wrote.
State & handoff
No .vstack/pipeline.json? You're standalone — everything above still applies. Take the brief
from the user, write the markdown where they ask (default specs/), and skip the rest.
- Read
.vstack/pipeline.json→artifacts.wireframes[]for the feature's page,artifacts.productfor the constitution. A missing wireframe isn't a blocker — a spec can come from conversation alone; say what it's based on. - Write
artifacts.specs[](append for a new feature; replace in place for an existing one — match on feature, never on array position) andstage: "spec". If the template'sdesign/README.mdinventory exists, fill the feature's owning spec cell. - Next —
/vstack:user-story-mapslices the specs into release phases. Offer to run it; don't ask whether to continue.