diff --git a/.github/workflows/docs-pages.yml b/.github/workflows/docs-pages.yml index 8964a18c1..b53cc3071 100644 --- a/.github/workflows/docs-pages.yml +++ b/.github/workflows/docs-pages.yml @@ -57,7 +57,9 @@ jobs: set -euo pipefail site_dir="_site" rm -rf "${site_dir}" - rsync -a --delete docs/ "${site_dir}/" + # docs/agents/ is repo-internal contributor process guidance + # (tracker, ticket, and PR norms), not site content. + rsync -a --delete --exclude 'agents/' docs/ "${site_dir}/" touch "${site_dir}/.nojekyll" echo "lash.run" > "${site_dir}/CNAME" diff --git a/docs/agents/domain.md b/docs/agents/domain.md new file mode 100644 index 000000000..8b3535c73 --- /dev/null +++ b/docs/agents/domain.md @@ -0,0 +1,40 @@ +# Domain docs + +How the engineering skills should consume this repo's domain documentation when exploring the codebase. This repo is **single-context**: one root `CONTEXT.md` glossary + one `docs/adr/`. + +## Before exploring, read these + +- **`CONTEXT.md`** at the repo root: the ubiquitous-language glossary (Host Application, Execution Mode, Runtime Scenario, Trigger Occurrence, Pending Turn Input, Queued Work, and the rest). +- **`docs/adr/`**: read the ADRs that touch the area you're about to work in. + +For a narrative map of the runtime rather than its vocabulary, the architecture chapters on the published docs site (`docs/architecture/`) orient faster than reading crates cold. + +If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill creates them lazily when terms or decisions actually get resolved. + +## File structure + +Single-context (this repo): + +``` +/ +├── CONTEXT.md +├── docs/adr/ +│ ├── 0026-model-capability-is-host-supplied-data.md +│ ├── 0045-services-are-stateless-substrates-own-continuation.md +│ └── … +└── crates/ · examples/ · runbooks/ +``` + +## Use the glossary's vocabulary + +When your output names a domain concept (an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`, and honor its `_Avoid_` lines. Don't drift to synonyms the glossary explicitly avoids (for example, don't call a Host Application a "reference host", and don't call the Deterministic Simulation Harness an "e2e fuzz test"). + +If the concept you need isn't in the glossary yet, that's a signal: either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`). + +## Flag ADR conflicts + +If your output contradicts an existing ADR, surface it explicitly rather than silently overriding: + +> _Contradicts ADR-0045 (services are stateless, substrates own continuation), but worth reopening because…_ + +Duplicate ADR numbers exist for 0034, 0036, 0037, and 0038. Cite those by full filename so the reference is unambiguous ([way-of-working.md](way-of-working.md) has the numbering rules). diff --git a/docs/agents/issue-tracker.md b/docs/agents/issue-tracker.md new file mode 100644 index 000000000..eb0447fe2 --- /dev/null +++ b/docs/agents/issue-tracker.md @@ -0,0 +1,43 @@ +# Issue tracker: Linear + +Issues and tickets for this repo live in **Linear**, in the **`lash` project** on team **`figments`**. Use the **Linear MCP tools** (`mcp__linear__*`) for all operations. + +Issues are keyed `FIG-`. Refer to issues by their **title/name**, never by a bare key; the key rides inside the name as a link. + +This repo has no offline Linear helper script. An agent running without the Linear MCP (for example a sandboxed `codex exec` run) cannot read the tracker, so the ticket context it needs must be inlined into its prompt. + +## Conventions + +- **Create an issue**: `mcp__linear__save_issue` with `team: "figments"`, `project: "lash"` (+ `title`, `description` as Markdown, `parentId`, `labels`, `blockedBy`, `priority` as needed). No `id` = create. +- **Read an issue**: `mcp__linear__get_issue` (`id: "FIG-"`, `includeRelations: true` for blockers). For comments: `mcp__linear__list_comments`. +- **List issues**: `mcp__linear__list_issues` (filter by `team`, `project`, `state`, `label`, `parentId`, `assignee`, `query`). +- **Comment**: `mcp__linear__save_comment` (`issueId`, `body`). +- **Apply / change labels**: `mcp__linear__save_issue` with `id` + `labels` (replaces the full label set; include existing labels you want to keep). Create new label strings first with `mcp__linear__create_issue_label` if they don't exist. +- **Set state / close**: `mcp__linear__save_issue` with `id` + `state` (`"Done"`, `"Canceled"`, etc.; Linear workflow states, not GitHub open/closed). +- **Assign**: `mcp__linear__save_issue` with `id` + `assignee` (user id, name, email, or `"me"`). +- **Relations**: `blockedBy` / `blocks` on `save_issue` are append-only; `removeBlockedBy` / `removeBlocks` to clear. + +Do not file housekeeping/teardown/process chores here; Linear is code-facing only. + +## Pull requests as a request surface + +**PRs as a request surface: no.** Code review happens on GitHub PRs; Linear holds the work items. Triage does not scan PRs. + +## When a skill says "publish to the issue tracker" + +Create a Linear issue in team `figments`, project `lash`. + +## When a skill says "fetch the relevant ticket" + +`mcp__linear__get_issue { id: "FIG-", includeRelations: true }`. + +## Parent and child operations + +A multi-ticket effort is one **parent** Linear issue with its slices and decisions as **sub-issues** ([way-of-working.md](way-of-working.md), [ticket-style.md](ticket-style.md)). + +- **Parent**: `save_issue { team: "figments", project: "lash", title: }`, body carrying the destination plus the one-line-per-child index. +- **Child ticket**: a Linear issue with `parentId: ""`. Once claimed, assign it to the driving dev. +- **Blocking**: Linear native relations, e.g. `save_issue { id: , blockedBy: [""] }`. A ticket is unblocked when every blocker is in a terminal state (`Done`/`Canceled`). Read blockers via `get_issue { includeRelations: true }`. +- **Frontier query**: `list_issues { parentId: "", state: }` → drop any child with an open blocker or an assignee; first in index order wins. +- **Claim**: `save_issue { id: , assignee: "me" }` (the session's first write). +- **Resolve a decision**: `save_comment { issueId: , body: "" }`, then echo the conclusion into the ticket body, then `save_issue { id: , state: "Done" }`, then append a one-line gist plus link to the parent's index. diff --git a/docs/agents/pr-style.md b/docs/agents/pr-style.md new file mode 100644 index 000000000..7a73a82dd --- /dev/null +++ b/docs/agents/pr-style.md @@ -0,0 +1,52 @@ +# Writing pull requests + +[ticket-style.md](ticket-style.md) keeps tickets high-level; the detail it pushes out lands here. A ticket says *what* and *why*. The **PR says how, and proves it works**. This is where file:line reasoning, the shape of the change, and the validation evidence belong. + +PRs are a code-review surface, not a request surface; work items live in Linear ([way-of-working.md](way-of-working.md)). A PR exists to get a change reviewed and merged, so write it for the reviewer. + +**Why the *why* lives in the ticket, not here: a deliberate departure.** Git-native convention (Google CL descriptions, Conventional Commits) anchors the *why* in the commit message. We anchor it in the ticket instead, because a ticket must be plannable, by a human or an AFK agent, before any code or diff exists. One consequence to respect: the squash commit body is durable history in this repo and feeds the release notes (below), while review scaffolding in the PR body is not. Anything that must outlive this diff belongs in the ticket, an ADR, the code, or the commit body, not only in review comments. + +## What a PR carries + +```markdown +Title: imperative, one sentence, readable without opening the ticket. + +## Why +Link the FIG ticket, and restate the goal in one or two sentences so the +reviewer needn't open it. The ticket has the full context; this orients. + +## What changed +The shape of the change and why this shape: the reasoning a reviewer needs +to judge it. Name the key files/seams, the trade-offs taken, the alternatives +rejected. This is the detail the ticket deliberately left out. If a rejected +alternative would still matter independent of this diff (a persistence, +protocol, or cross-cutting-pattern choice), promote it to an ADR and link it; +only diff-local trade-offs stay here. + +## Validation +How you know it works, matched to the Definition of Done: repeatable steps a +reviewer could rerun, i.e. which runbook passed, which `just` recipe or +confidence-gate lane you ran, what you checked live. Not "tested locally". +"Merged with green CI" is necessary, not sufficient, for behavior changes. + +## Risk / not included (optional) +Blast radius, anything deliberately out of scope, follow-ups filed as tickets. +``` + +## Rules + +- **The first line is load-bearing.** It becomes the squash commit title in `git log`, so write it imperative, one sentence, standalone: `Add cursor pagination to the process registry listing`, never `Fixes`, `WIP`, or `Phase 1`. +- **User-facing changes carry release notes.** The squash commit body must include a `Release-Notes:` section describing the change for Lash's users (breaking changes say `Breaking:` first). The release workflow collects those sections and refuses to publish a range that has none. See `docs/PUBLISHING.md`. +- **Lead with the summary.** A reviewer reads the first paragraph and knows what the PR does and why. Depth follows; it doesn't open the PR. +- **Link the ticket, don't restate it.** One or two orienting sentences, then the link. The PR carries the *how*; the ticket carries the *what/why*. Don't copy the ticket body in. +- **Prove it, per the Definition of Done.** Behavior changes are live-validated (the relevant runbook passes; turn-execution changes run both durable geometries); mechanical changes state the verification you ran ([way-of-working.md](way-of-working.md)). Say what you actually did, including what you skipped. +- **Match the diff.** The PR body describes what the diff does. No aspirational claims for code that isn't there, no stale description after a force-push. +- **Follow-ups are tickets, not TODOs.** Work discovered but out of scope gets a FIG issue and a link, not a buried comment. +- **The ticket's prose bar applies here too:** lede first, cut filler, concrete over abstract ([ticket-style.md](ticket-style.md)). A reviewer skims, and a rambling PR body costs review time. + +## Mechanics (defined elsewhere) + +- **Commits.** No AI-assistant attribution or co-author trailers; see the root agent rules. +- **Branching and releases.** Short-lived branches off fresh `origin/main`, merged by PR; releases are dispatched manually by a maintainer from a green `main` (`CONTRIBUTING.md`, `docs/PUBLISHING.md`). Never tag or publish by hand. +- **Stacked PRs.** For dependent chains, use the native stack flow (global agent rules). +- **Generated and gated artifacts.** Run `just push-gate` before pushing. Docs changes must keep `python3 scripts/lint_docs.py` green, and embedded Rust snippets are regenerated with `python3 scripts/lint_docs.py --fix-snippets` rather than hand-edited. diff --git a/docs/agents/ticket-style.md b/docs/agents/ticket-style.md new file mode 100644 index 000000000..2c414e538 --- /dev/null +++ b/docs/agents/ticket-style.md @@ -0,0 +1,123 @@ +# Writing tickets + +[way-of-working.md](way-of-working.md) says *where* each artifact goes. This says *how a ticket reads* once you're writing one. It applies to every FIG issue (bugs, features, decisions, PR slices), drafted by a human or an agent. + +The goal: a reader gets what a ticket is about from the first sentence, and never has to read an investigation to understand the work. Detail belongs in the PR, the code, an ADR, or a linked report, not above the fold in the ticket (one exception: `ready-for-agent` tickets, below). + +## The altitude contract + +**A ticket says *what*, *why*, and *how you'll know it's done*. It does not say *how to build it*.** + +File:line references, commit SHAs, builder-call code, schema conventions, ADR clause numbers, and RCA transcripts belong in the PR description, the code, an ADR, or a linked scratchpad report, not the ticket body. If a reader needs your investigation to understand the ticket, **link it; don't inline it.** + +This is the rule to cite in review. A ticket that opens with `ADR 0045 finding 1, clauses 1+4; reports: foo.md, bar.md` has failed it: the reader hits references before they know what is broken. + +**One exception, by design:** a `ready-for-agent` ticket carries the how-to detail an agent needs to start, but tucked in a collapsed Agent spec, not smeared through the body. See [Ready-for-agent tickets](#ready-for-agent-tickets-the-agent-spec). + +## One ticket, one outcome + +A ticket describes one task with one checkable outcome: a piece of code, a fix, a decision, a document. If it can't be stated as one outcome, it's not a ticket: + +- A big or foggy effort → a parent ticket with child tickets ([way-of-working.md](way-of-working.md)). +- A raw problem you haven't scoped → still a ticket, but framed as the problem, not a guessed solution. Let the assignee rewrite it as a task once scoped. +- Housekeeping, teardown, process chores → nowhere durable (Linear is code-facing). + +**Escape hatch.** The template and the orienting-parent rule below are for real work items. A trivial or mechanical ticket (a dependency bump, a one-line config change, an obvious revert) skips both: a plain title plus a one-line `## Done when` is enough. Don't make a chore fill out a form; forced templates just raise abandonment. + +## The template + +Same top on every ticket, so readers learn one scan path. A reader who stops after the TL;DR already knows what the ticket is. + +```markdown +**TL;DR:** one plain-English sentence saying what this is and why it matters. + +## Why +2–4 sentences. The problem or motivation, concrete, with real stakes. +Bugs: what breaks and for whom. Quote or link the evidence, don't paste the RCA. + +## Done when +The observable success condition. Checkable, e.g. "runbook X passes", +"the conformance suite covers the reopen path", "ADR-N is recorded and linked here". + +## Out of scope (required if non-trivial; one line each) +What this deliberately does not do; the boundaries that stop scope creep. +``` + +Anything deeper (file:line notes, design sketches, the full RCA) goes **below `## Done when`**, in a collapsible block at the very bottom or behind a link, never above it. Linear's collapsible is a `+++` fence (not HTML `
`, which Linear prints as raw tags): a line `+++ Summary` opens it, a closing `+++` on its own line ends it, and it starts collapsed. + +Notes on the fields: + +- **`## Out of scope` is required for any non-trivial ticket** (trivial/mechanical tickets skip it with the rest of the template). Naming the boundaries is how you stop scope creep before it starts; an omitted no-go is where a rabbit hole gets in. `ready-for-agent` tickets state theirs as the mandatory "Do not touch" in the Agent spec. +- **TL;DR is skippable when the title already states the outcome as a full sentence.** Don't say the same thing twice. Keep it when the title is necessarily compressed (a bug title naming the symptom, not the fix). +- **`## Done when` must be checkable, not aspirational:** an observable end state a reader could confirm. For a **behavior change**, name the runbook that proves it (new or existing); that *is* the Definition of Done ([way-of-working.md](way-of-working.md)), and "merged with green CI" is necessary, not sufficient. For a `ready-for-agent` ticket, make it *machine*-checkable: a test name, a command, an assertable behavior the agent can loop against. An agent with no runnable check knows only that it's *plausibly* done. + +Variants share the same top: + +- **Bug.** Write `## Why` as a narrated incident, not an abstract symptom: *"The worker crashed mid-turn → the session reopened on a second worker → the first attempt's tool result was replayed and charged twice."* Concrete beats "effect replay is unreliable." +- **PR slice** (one deliverable in a numbered arc). Often just TL;DR + `## Done when`. Short because the scope is one PR, not because detail was cut. Keep who-does-it and who-reviews-it out of the ticket; that's execution process, not the work. +- **Decision.** The outcome is an ADR. `## Done when` = "ADR-N merged and linked here." + +## Ready-for-agent tickets: the Agent spec + +A `ready-for-agent` ticket has a second reader: an autonomous coding agent that executes it unattended, with no human to ask mid-task. The evidence on what such an agent needs is measured, and it cuts against the human skim path. Naming the files, giving the intended approach, and setting hard scope boundaries each raise an agent's success rate; vague scope gets resolved by guessing, sometimes destructively. But padding hurts too. So we keep the two readers apart: the human gets the clean top; the agent gets a spec in a **collapsible `+++` block at the very bottom**, after everything a human skims (or a Linear document attached to the issue, which the agent's integration loads on start). Linear renders the `+++` fence as a toggle that starts collapsed, so a human sees only the summary line and expands it only if they need the detail. The agent reads the whole block. + +When you apply `ready-for-agent`, add this as the final section (a `+++` collapsible; Linear prints raw `
` tags, so never use HTML): + +```markdown ++++ ## Agent spec + +- **Files.** The modules/paths in play, and the pattern to follow (name a similar existing call site). +- **Approach.** The intended shape, in a sentence or two. Direction, not line-by-line code. +- **Do not touch.** Files/systems out of bounds. If you hit an undecided fork, stop and comment, don't guess. +- **Done check.** The runnable oracle from `## Done when` (a test/command that must pass). ++++ +``` + +Rules: + +- **Minimal is not short.** Give the agent the concrete detail it needs; the failure mode isn't length, it's padding. A precise Agent spec doesn't violate the altitude contract; it honors it by keeping the detail below the fold. +- **"Do not touch" is mandatory here, not optional.** It's the one section that stops an unattended agent from resolving ambiguity the destructive way. +- **Don't route agent-critical detail through the comment thread.** Comments aren't reliably loaded into an agent's context; put what the agent must have in the spec block or an attached document. (This deliberately overrides the `triage` skill's `AGENT-BRIEF` default, which posts the spec as a *comment* and avoids file paths. In this repo the spec is an in-body block and naming files is encouraged; pair each with the pattern to follow, so a path that goes stale is self-correcting.) +- **Size gate.** If understanding the task needs more than ~2–3 files of unfamiliar context, split it before labeling it ready-for-agent; "no open decisions" isn't enough on its own. + +## Long-form work: research, RCA, decisions + +Some tickets exist *to produce* long-form output: a research question, an investigation, a decision to grill out. The altitude contract still holds. The **body poses the ask** and states what a good answer looks like (`## Done when`); the **output lands as a comment** on the ticket. Research reports and RCA write-ups go in comments, never a repo file, never the ticket body ([way-of-working.md](way-of-working.md) routing). + +Two durability rules keep the comment thread from swallowing the decision: + +- **Echo the decision back into the body.** Once a comment-borne finding or decision is *accepted*, paste a 1–3 sentence synthesis into the ticket body (a `## Decision` line, or under `## Done when`) before the ticket closes or goes `ready-for-agent`. The full report stays in the comment; the *conclusion* lives where every reader will find it: a human skimming, or an agent calling `get_issue` without reading comments. A decision stranded in a thread is one un-read comment away from being lost. +- **Never cite a non-durable artifact as the sole record.** A session scratchpad path, an unmerged branch, or a chat message doesn't count as linked evidence; they rot or vanish. Only a resolved comment on the ticket, a PR pinned to a commit SHA (not a branch), an ADR, or a repo file is a durable link. (This overrides the `/research` skill's "write findings to a repo file" default; in this repo the durable home for a research finding is a comment on the ticket.) + +## Titles + +A plain sentence naming the outcome. `Process registry listing is unbounded and needs a cursor` reads at a glance. A 20-word RCA clause like `non-Restate AwaitEvent is process-local; SQLite hosts advertise Durable for a sticky path` is a line out of the investigation, not a title. The key rides inside the linked title; never refer to a ticket by bare `FIG-123`. + +## Every non-trivial ticket has an orienting parent + +Anything past a single-PR slice hangs off an orienting parent issue. This is the "overarching ticket" that lets a reader orient before diving in. + +Don't force it, though. Join a parent when one exists; the first ticket of a new arc *creates* the parent rather than waiting to be filed under one; and a genuinely standalone ticket (a one-off bug, a lone maintenance task) needs no parent at all, a sibling link is enough. The goal is orientation, not a hierarchy every ticket must report to. A parent that never closes because nobody shut its children is worse than no parent. + +The parent: + +- States the **destination in ≤5 plain sentences**: where we're going and why, no implementation surfaces. +- Carries a **one-line-per-child index**, each line linking its ticket. +- **Orients; it is not a changelog.** What shipped and current status live on the children and their PRs. Don't let the parent's destination turn into a run-on status report. + +## Prose bar (run before you file) + +Draft, then edit; the first draft is material, not the ticket. Run this pass before filing: + +1. **Lede.** Is the ask in sentence one? If a reader stops after it, do they know what's being asked? +2. **Altitude.** Any file:line, SHA, builder code, or ADR clause in the body? Move it out or link it. +3. **Cut filler.** Strip "it's important to note", "arguably", "several" (where a number belongs), "basically", and throat-clearing openers ("Before diving in…"). If a sentence's meaning survives deleting the word, the word was dead. +4. **Kill zombie nouns.** `-tion/-ment/-ance` nouns with `is/are/was` as the main verb hide the actor. "Dependency X blocks the migration", not "completion of the migration was blocked by dependency issues". +5. **Concreteness.** Every abstract claim ("causes reliability issues", "improves performance") has a number, file, error, or example, or it's flagged as a gap to fill before filing. +6. **Bullet audit.** No bullet is a sentence fragment standing in for a real claim, and no list is wall-to-wall `**Bold:** description` (the tell of a padded, un-edited draft). Prose or a real claim, not a template. +7. **Strip the AI tells.** They read as machine-drafted and cost the reader's trust: + - *Em-dashes.* Don't use the em dash as your default connector. Most become a period, comma, colon, or parentheses. Aim for at most one per paragraph. + - *Phrasebank words.* leverage, robust, seamless, crucial, pivotal, streamline, delve, harness, foster, underscore, "ever-evolving". Use the plain word. + - *Antithesis / false reframe.* "not just X, it's Y"; "this isn't a bug fix, it's a redesign". If Y only restates X, drop the frame. + - *Chat fossils and decoration.* "Great question", "I hope this helps", "Let me break this down", sycophantic openers, decorative emoji on headings or bullets. Delete outright. +8. **Length gate.** Shorter without losing a decision-relevant fact? Omit needless words; don't omit needed ones. diff --git a/docs/agents/triage-labels.md b/docs/agents/triage-labels.md new file mode 100644 index 000000000..44fb971c3 --- /dev/null +++ b/docs/agents/triage-labels.md @@ -0,0 +1,22 @@ +# Triage labels + +States carry the lifecycle; labels carry only what a state can't express: **who picks a ticket up**. See the "Lifecycle: states and labels" section of [way-of-working.md](way-of-working.md) for the full model. This file records the label vocabulary. + +## The two dispatch labels + +Only two triage labels exist, and both live on **Todo** tickets: + +| Label | Meaning | +| ----- | ------- | +| `ready-for-agent` | Fully specified: success condition stated, no open decisions, Agent spec filled in; an AFK agent can take it without asking anything. | +| `ready-for-human` | Needs judgment or access an agent doesn't have. | + +## Labels we deliberately don't use + +The standard triage vocabulary also names `needs-triage`, `needs-info`, and `wontfix`. We don't create these; each duplicates a workflow state: + +- `needs-triage` → the **Triage** state. +- `wontfix` → the **Canceled** state. +- `needs-info` → keep the ticket in **Triage** (or comment the question and leave it there); no label needed. + +When a skill mentions one of those roles, map it to the state above, not a new label. diff --git a/docs/agents/way-of-working.md b/docs/agents/way-of-working.md new file mode 100644 index 000000000..a3e02abc2 --- /dev/null +++ b/docs/agents/way-of-working.md @@ -0,0 +1,105 @@ +# Way of working: Linear + the repo + +The norms file for how work in this repo is planned, tracked, decided, validated, and documented. Companions in this directory: [issue-tracker.md](issue-tracker.md) (Linear mechanics), [ticket-style.md](ticket-style.md) (how a ticket reads: altitude, template, prose bar), [pr-style.md](pr-style.md) (how a PR reads: the "how" plus validation proof), [triage-labels.md](triage-labels.md) (triage-role label vocabulary), [domain.md](domain.md) (how to consume the glossary and ADRs). + +These files are repo-internal process guidance. They are not part of the published documentation site; the Pages build excludes this directory. + +## The model in one paragraph + +Planning, tracking, and everything in-flight live in **Linear** (team `figments`, project `lash`; issues keyed `FIG-`). The repo holds only **durable, code-facing artifacts**: decisions (`docs/adr/`), vocabulary (root `CONTEXT.md`), agent-driven runbooks (`runbooks/`), and the published reference site (`docs/`). The test for "belongs in the repo": *an agent needs it while touching code, at HEAD, offline.* Anything narrative, exploratory, or transient (research reports, plans, decision debates, status) belongs on a Linear issue, not in a repo file. + +## Routing: where each artifact goes + +| You are producing… | It goes… | +| --- | --- | +| A well-understood work item (bug, feature, follow-up) | A plain FIG issue in the `lash` Linear project | +| A big or foggy effort that needs decisions before building | A **parent FIG issue** holding the destination plus a one-line-per-child index, with a child ticket per decision or slice ([ticket-style.md](ticket-style.md)) | +| A research report or an RCA write-up | A **comment on the Linear ticket** that asked for it. Never a repo file | +| An implementation plan or design spec | The parent ticket and its children *are* the plan. A locked design that must outlive them → ADR. (`docs/plans/` holds two legacy files; don't add to it) | +| A decision with lasting architectural weight | An ADR in `docs/adr/` (see ADR norms) | +| A validation procedure for new or changed live behavior | `runbooks//runbook.md` (see Runbook norms) | +| A new or sharpened domain term | Root `CONTEXT.md` glossary (honor its `_Avoid_` lines). One glossary, no per-crate shadow glossaries | +| Reference documentation for people using Lash | A page on the published docs site: authored per `docs/STYLEGUIDE.md`, registered in `docs/docs.js`, gated by `python3 scripts/lint_docs.py` | +| Housekeeping / teardown / process chores | **Nowhere durable.** Linear is code-facing only; track chores in the session that owns them | + +`docs/` is a published website (lash.run), not a scratch directory. Anything you add there is public, must fit the site's structure, and must keep `scripts/lint_docs.py` green. + +## How a ticket reads + +Routing (above) decides *what goes where*. [ticket-style.md](ticket-style.md) decides *how the ticket reads once you're writing one*. The contract in one line: **a ticket says what, why, and how you'll know it's done, never how to build it.** Implementation detail (file:line, SHAs, RCA, builder code) rides in the PR, the code, an ADR, or a linked report; the ticket links it, never inlines it. Every ticket takes the fixed top (a plain-English **TL;DR** sentence, then `## Why`, then `## Done when`), and every non-trivial ticket hangs off an orienting parent that holds the destination in ≤5 sentences plus a one-line-per-child index. Draft, then run the prose bar before filing. Full template, title rules, and checklist live in [ticket-style.md](ticket-style.md). + +The altitude the ticket leaves out lands in the PR, not lost: the ticket says *what* and *why*, the **PR says how and proves it works** (the change's shape, the reasoning, and the validation evidence tied to the Definition of Done). See [pr-style.md](pr-style.md). + +## Parent-ticket norms + +- **The parent is the index, tickets hold the detail.** The full record of a decision lives in one place: the resolving ticket's comment. Its *conclusion* is echoed as a one-line gist to the parent's index, and (per [ticket-style.md](ticket-style.md)) into the resolving ticket's own body, so a reader (or an agent calling `get_issue` without reading the thread) sees the outcome without digging. Gist echoes, full record doesn't move. +- **Refer by name, never bare key.** `FIG-123` alone is illegible; the key rides inside the linked title. +- **Tickets needing a human name their human.** A decision ticket is only resolvable with the person who has the authority to decide it; say who that is in the ticket. An agent never stands in for the human's side. +- **Graduate durable decisions.** When an effort resolves something with lasting architectural weight, write the ADR as part of resolving the ticket (or as the effort's terminal step). The tracker is an append-only log; the repo is current state. Anything still only-in-a-ticket after its parent closes risks re-litigation. +- **Close the loop.** When the destination is reached: mark remaining children Done/Canceled (no orphaned open tickets), mark the parent Done, and move it out of the Triage state; completed work does not sit in Triage. +- **One live parent per effort.** Before opening a new one, search for an existing parent covering the effort and extend it instead. + +## Runbook norms + +A **runbook** is an **agent-driven test scenario**: QA performed by an agent against a live example app, with assertable gates staged as **do → expect**, following the shared rules in `runbooks/RULES.md` (poll-don't-sleep, assert the rendered output not just the status, probe values that can't pass by coincidence). + +- **Location:** `runbooks//runbook.md`, one scenario per directory. `runbooks/RULES.md` holds every shared rule; a runbook adds only its scenario-specific purpose, phases, and scorecard. +- **Two layers, kept separate.** Scripted deterministic harnesses (`runbooks/restate-postgres-workers/`, the `just *-e2e` recipes) are gate *evidence*: they boot real infrastructure and assert exact outcomes, and they stay scripts. Browser runbooks are the agent-judged semantic layer on top, gating on what the example app actually renders. A runbook never re-implements a scripted harness, and a harness never asks for judgement. +- **Safe to run wholesale.** Every runbook validates; none mutates beyond its own seeded scenario state. An agent told "run the runbooks" must never destroy anything. Destructive or maintenance procedures are not runbooks; they belong in `docs/` as operational reference. +- **Ship with the change.** A PR that creates or changes live behavior ships or updates the runbook that proves it. Merged is not done; live-validated is (see Definition of done). +- **Scope flavors:** per-behavior scenarios (regression-style, e.g. one FIG's fix) and per-subsystem validations tied to the ADR they prove out. + +## ADR norms + +- **Numbering:** next number = highest existing + 1. Check first: `ls docs/adr/ | sort -V | tail -3`. Duplicate numbers have happened (two each of 0034/0036/0037/0038; grandfathered, do not renumber, cite them by full filename); never mint a duplicate again. +- **Shape:** one decision per ADR, filename `NNNN-kebab-case-title.md`, status/context/decision/consequences. +- **Conflicts:** if your output contradicts an ADR, surface it explicitly ("Contradicts ADR-NNNN … worth reopening because …") rather than silently overriding; see [domain.md](domain.md). + +## Lifecycle: states and labels + +**States are the lifecycle position. Labels carry only what a state can't express: who picks the ticket up.** Keep the two orthogonal; don't say the same thing twice in both. + +The workflow states, in order: + +- **Triage.** Just arrived, not yet assessed. The inbox. (The state *is* the "needs triage" signal; there is no `needs-triage` label, don't add one.) +- **Backlog.** Accepted as real work, but not now. +- **Todo.** Specified and ready to pick up. This is where dispatch matters (labels below). +- **In Progress.** Claimed and being worked; claiming = assigning. +- **In Review.** A PR is open against it. +- **Done.** Live-validated per the Definition of Done (below). **Canceled** / **Duplicate**: terminal, not doing it. (`Canceled` covers "won't fix"; there is no `wontfix` label.) + +Two dispatch labels, and only these two, live on **Todo** tickets; they say who takes the ticket, which no state can: + +- **`ready-for-agent`:** *fully specified*. Success condition stated, no open decisions, an AFK agent can take it without asking anything (its Agent spec is filled in; see [ticket-style.md](ticket-style.md)). If specifying it would take a conversation, it isn't ready; send it back to Triage. +- **`ready-for-human`:** work needing judgment or access an agent doesn't have. + +Agents may self-serve from the `ready-for-agent` queue; claiming moves it to In Progress and assigns it. + +## Live debugging protocol: evidence first + +When diagnosing live behavior (a stall, a wrong decision, a failed run, a lost effect), the first move is the recorded evidence, not an experiment: + +1. **Read the trace or structured log for the operation in question** before designing any wait-and-observe or perturbation experiment (`docs/tracing.html` covers what Lash emits and how to export it). Most questions of the form "what did it read and why did it decide that" must be answerable from the recorded evidence. +2. **An unanswerable trace is itself a finding.** If the span for a decision exists but lacks the inputs that produced it, or doesn't exist at all, report that instrumentation gap as a first-class defect alongside whatever you eventually diagnose, then fall back to rows and logs. +3. **Experiments are the last resort**, for questions traces structurally cannot answer (timing races, provider-side behavior). Budget them: a multi-cycle observation loop is hours of wall clock that one well-attributed span replaces. + +**Decision seams ship decision evidence.** Any gate, claim, lease check, admission check, or adjudicator that can deny, park, cancel, or reroute must emit its full decision basis on the span or structured log: the inputs, the consulted state and its freshness, the thresholds, and the outcome. Reviewers check this like they check tests; "the code denies correctly but you can't see why from a trace" does not pass review. + +## Definition of done + +State the expected proof on the ticket. Defaults when unstated: + +- **Behavior changes:** live-validated. The relevant runbook (new or existing) passes. Merged-with-green-CI is necessary, not sufficient. Changes to turn execution in `lash-core` or its `lash-restate` adapter additionally run both durable geometries locally (`just agent-workbench-restate-e2e` and `just restate-postgres-workers-e2e`), per `CONTRIBUTING.md`. +- **Mechanical changes** (renames, link sweeps, codegen, doc moves): merged, with the stated verification in the PR. +- **Decisions:** the ADR is merged and the resolving ticket links it. + +Gate merges on the local battery (`just push-gate`, plus the confidence-gate lane the change warrants) and review; CI is the backstop, not the first signal. Deterministic failure classes (docs lint, conformance, contract drift) must be fixed, never bypassed. + +## Team and session norms + +- Read `CONTEXT.md` and area-relevant ADRs before exploring (see [domain.md](domain.md)). +- Trunk-based: `main` is the only long-lived branch, branch from fresh `origin/main`, short-lived branches, merge by PR. Never push product changes straight to `main`, and never tag or publish a release by hand; see `CONTRIBUTING.md` and `docs/PUBLISHING.md`. +- PRs are not a request surface; work items live in Linear, and GitHub is for code review only. +- **Nothing load-bearing in private agent memory.** Session memory is a personal cache. Anything a teammate (or their agent) would need must land on Linear or in the repo. +- **Outward text gets a human go-ahead.** Agents draft freely, but PR comments, review replies, and comments on tickets that teammates will read are posted only with the driving human's per-item approval. +- **Skills:** this workflow assumes the grilling / research / domain-modeling / triage skill set is installed for your agent; the repo-side bindings are the files in `docs/agents/`.