22 Commits

Author SHA1 Message Date
Dobromir Popov
ba7c656364 node metrics 2026-07-14 20:33:02 +02:00
Dobromir Popov
b661590ac7 log window bigger 2026-07-14 17:47:20 +02:00
Dobromir Popov
21e6c86147 fix: let admin placement recover joined nodes 2026-07-14 16:37:42 +02:00
Dobromir Popov
def47f1a42 Merge branch 'master' of https://git.d-popov.com/popov/neuron-tai 2026-07-14 16:11:26 +02:00
Dobromir Popov
8cb00e951f feat: show admin node pool capacity 2026-07-14 16:11:18 +02:00
Dobromir Popov
7b3399760e chore: wrap up completed story metadata 2026-07-14 17:09:04 +03:00
Dobromir Popov
22467f145c merge: distributed performance baseline benchmark 2026-07-14 17:01:08 +03:00
Dobromir Popov
35af1e21de fix: make model placement controls observable 2026-07-14 16:00:37 +02:00
Dobromir Popov
905ea16ce0 feat: complete route session baseline benchmark 2026-07-14 16:55:52 +03:00
Dobromir Popov
348b003d6e fix: restore responsive dashboard panel grid 2026-07-14 15:55:24 +02:00
Dobromir Popov
1e64a5b2b9 new dash update 2026-07-14 15:29:11 +02:00
Dobromir Popov
e2f3ae32b8 feat: let admins manage model placement 2026-07-14 15:16:23 +02:00
Dobromir Popov
29351d6217 chore: ignore local model cache 2026-07-14 14:05:37 +02:00
Dobromir Popov
5c9a2f6c97 dash style fix 2026-07-14 13:29:51 +02:00
Dobromir Popov
13d82f8032 dash, tests 2026-07-14 12:26:10 +02:00
Dobromir Popov
d1a1400db9 Move tracker hive to admin and expand nodes panel.
Give Nodes & coverage full width on overview with inference prices and live speed, and expose model pricing on /v1/models.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 12:19:25 +02:00
Dobromir Popov
5d87e81bc9 feat: harden node placement and partial model loading 2026-07-13 21:58:08 +02:00
Dobromir Popov
a6bcc69288 sol mainnet payouts tasks 2026-07-13 18:51:40 +02:00
Dobromir Popov
c938d38031 more docs review 2026-07-13 18:37:07 +02:00
Dobromir Popov
95245be512 documentation revision 2026-07-13 18:14:21 +02:00
Dobromir Popov
180a7674e6 configure ralph 2026-07-13 17:56:00 +02:00
Dobromir Popov
f420dc1092 matt's skills updatged with upstream 2026-07-13 14:23:13 +02:00
119 changed files with 2462 additions and 498 deletions

View File

@@ -1,6 +1,6 @@
--- ---
name: ask-matt name: ask-matt
description: Ask which skill or flow fits your situation. A router over the user-invoked skills in this repo. description: Ask which skill or flow fits your situation. A router over the skills in this repo.
disable-model-invocation: true disable-model-invocation: true
--- ---
@@ -8,26 +8,28 @@ disable-model-invocation: true
You don't remember every skill, so ask. You don't remember every skill, so ask.
A **flow** is a path through the skills. Most paths run along one **main flow**, and two **on-ramps** merge onto it. Everything else is standalone. A **flow** is a path through the skills. Most paths run along one **main flow**, and two **on-ramps** merge onto it. Everything else is standalone, or a vocabulary layer that runs underneath.
## The main flow: idea → ship ## The main flow: idea → ship
The route most work travels. You have an idea and want it built. The route most work travels. You have an idea and want it built.
1. **`/grill-with-docs`** — sharpen the idea by interview. Start here when you **have a codebase**: it's stateful, retaining what it learns in `CONTEXT.md` and ADRs. (No codebase? Use `/grill-me` — see Standalone.) 1. **`/grill-with-docs`** — sharpen the idea by interview. Start here when you **have a codebase**: it's stateful, retaining what it learns in `CONTEXT.md` and ADRs. (No codebase? Use `/grill-me` — see Standalone. Both run the same `/grilling` primitive; `grill-with-docs` is the one that leaves a paper trail.)
2. **Branch — can you settle every question in conversation?** If a question needs a runnable answer (state, business logic, a UI you have to see), detour through a prototype, bridged by **`/handoff`** in both directions (see Crossing sessions): 2. **Branch — can you settle every question in conversation?** If a question needs a runnable answer (state, business logic, a UI you have to see), detour through a prototype, bridged by **`/handoff`** in both directions (see Crossing sessions):
- **`/handoff`** out, then open a fresh session against that file, - **`/handoff`** out, then open a fresh session against that file,
- **`/prototype`** to answer the question with throwaway code, - **`/prototype`** to answer the question with throwaway code,
- **`/handoff`** back what you learned, and reference it from the original idea thread. - **`/handoff`** back what you learned, and reference it from the original idea thread.
3. **Branch — is this a multi-session build?** 3. **Branch — is this a multi-session build?**
- **Yes** → **`/to-prd`** (turn the thread into a PRD) → **`/to-issues`** (split the PRD into independently-grabbable issues). Because the issues are independent, **clear context between each one**: start a fresh session per issue and kick off **`/implement`** by passing it the PRD and the single issue to work on. - **Yes** → **`/to-spec`** (turn the thread into a spec), then **`/to-tickets`** to split it into tracer-bullet tickets, each declaring its **blocking edges**. On a local tracker that's one file per ticket under `.scratch/<feature>/issues/`, worked blockers-first by hand; on a real tracker the edges become native blocking links, so any ticket whose blockers are done can be grabbed — kick off **`/implement`** per ticket, **clearing context between each one**.
- **No** → **`/implement`** right here, in the same context window. - **No** → **`/implement`** right here, in the same context window.
Either way, **`/implement`** builds each issue by driving **`/tdd`** internally — one red-green slice at a time — then closes out by running **`/code-review`**, a two-axis review (Standards + Spec) of the diff, before committing. Reach for **`/tdd`** on its own when you just want to build a concrete behaviour test-first without a full spec, and **`/code-review`** on its own whenever you want to review a branch or PR against a fixed point.
### Context hygiene ### Context hygiene
Keep steps 13 in **one unbroken context window** — don't compact or clear until after `/to-issues` — so the grilling, PRD, and issues all build on the same thinking. Each `/implement` then starts fresh, working from the issue. Keep steps 13 in **one unbroken context window** — don't compact or clear until after `/to-tickets` — so the grilling, spec, and tickets all build on the same thinking. Each `/implement` then starts fresh, working from the ticket.
The limit on this is the **[smart zone](https://www.aihero.dev/ai-coding-dictionary/smart-zone)**: the window (~120k tokens on state-of-the-art models) within which the model still reasons sharply. If a session approaches it before `/to-issues`, don't push on degraded — `/handoff` and continue in a fresh thread. The limit on this is the **[smart zone](https://www.aihero.dev/ai-coding-dictionary/smart-zone)**: the window (~120k tokens on state-of-the-art models) within which the model still reasons sharply. If a session approaches it before `/to-tickets`, don't push on degraded — `/handoff` and continue in a fresh thread.
## On-ramps ## On-ramps
@@ -35,13 +37,26 @@ A starting situation that generates work, then merges onto the main flow.
- **Bugs and requests piling up** → **`/triage`**. It moves issues through triage roles and produces agent-ready issues, which **`/implement`** later picks up. - **Bugs and requests piling up** → **`/triage`**. It moves issues through triage roles and produces agent-ready issues, which **`/implement`** later picks up.
Triage is only for issues **you didn't create** — bug reports, incoming feature requests, anything that arrives raw. Issues that `/to-issues` produced are already agent-ready, so **don't triage them**. Triage is only for issues **you didn't create** — bug reports, incoming feature requests, anything that arrives raw. Tickets that `/to-tickets` produced are already agent-ready, so **don't triage them**.
- **Something's broken** → **`/diagnosing-bugs`**. For the hard ones: the bug that resists a first glance, the intermittent flake, the regression that crept in between two known-good states. It refuses to theorise until it has a **tight feedback loop** — one command that already goes red on *this* bug — then fixes with a regression test. Its post-mortem hands off to **`/improve-codebase-architecture`** when the real finding is that there's no good seam to lock the bug down.
- **A huge, foggy effort — a greenfield project or a huge feature build, too big for one session** → **`/wayfinder`**, the most cognitively demanding flow here. When the way from here to the destination isn't visible yet, it charts a **shared map** of **decision tickets** on the issue tracker and resolves them one at a time — producing **decisions, not deliverables** — until the fog is pushed back and the way is clear. Where **`/grill-with-docs`** sharpens an idea you can hold in one session, wayfinder is for the idea you can't — and it's slower and denser, so save it for exactly that, never a well-scoped feature.
When the map clears, **it hands off, it doesn't build**: merge onto the main flow at **`/to-spec`**, which collapses the map's linked decisions into a buildable plan, then `/to-tickets` and `/implement` as usual. Looping the map straight into `/implement` skips that collapse and throws the linked detail away — go straight to `/implement` only when the effort turned out genuinely small.
## Codebase health ## Codebase health
Not feature work — upkeep. Not feature work — upkeep.
- **`/improve-codebase-architecture`** — run whenever you have a spare moment to keep the codebase good for agents to operate in. It surfaces deepening opportunities; picking one _generates an idea_ you can take into the main flow at `/grill-with-docs`. - **`/improve-codebase-architecture`** — run whenever you have a spare moment to keep the codebase good for agents to operate in. It surfaces **deepening opportunities**; picking one _generates an idea_ you can take into the main flow at `/grill-with-docs`. It's the survey that finds the candidates; **`/codebase-design`** (below) is the bench you design the chosen one on.
## Vocabulary underneath
Two model-invoked references that run *beneath* the other skills — each the single source of truth for its vocabulary. Reach for them directly when the **words**, not the process, are the problem; or let the skills above pull them in.
- **`/domain-modeling`** — sharpen the project's *domain* language: challenge a fuzzy term, resolve an overloaded word ("account" doing three jobs), record a hard-to-reverse decision as an ADR. It's the active discipline `/grill-with-docs` drives to keep `CONTEXT.md` a clean glossary.
- **`/codebase-design`** — the deep-module vocabulary (module, interface, depth, seam, adapter, leverage, locality) for designing a module's *shape*: a lot of behaviour behind a small interface at a clean seam. `/tdd` and `/improve-codebase-architecture` both speak it.
## Crossing sessions ## Crossing sessions
@@ -53,6 +68,8 @@ Not feature work — upkeep.
Off the main flow entirely. Off the main flow entirely.
- **`/grill-me`** — the same relentless interview as `/grill-with-docs`, but for when you have **no codebase**. Stateless: it saves nothing locally, builds no `CONTEXT.md`. Reach for it to sharpen any plan or design that doesn't live in a repo. - **`/grill-me`** — the same relentless interview as `/grill-with-docs`, but for when you have **no codebase**. Stateless: it saves nothing locally, builds no `CONTEXT.md`. Reach for it to sharpen any plan or design that doesn't live in a repo.
- **`/prototype`** — a small, throwaway program that answers one design question: does this state model feel right, or what should this UI look like. Throwaway from day one — keep the answer, delete the code. It's the detour in step 2 of the main flow, but reach for it any time a design question is hard to settle on paper.
- **`/research`** — delegate reading legwork to a **background agent**: it investigates a question against **primary sources**, then leaves a cited Markdown file in the repo. Keep working while it reads. The file it produces is something to take *into* the main flow at `/grill-with-docs` — research feeds the thinking, it doesn't replace it.
- **`/teach`** — learn a concept over multiple sessions, using the current directory as a stateful workspace. - **`/teach`** — learn a concept over multiple sessions, using the current directory as a stateful workspace.
- **`/writing-great-skills`** — reference for writing and editing skills well. - **`/writing-great-skills`** — reference for writing and editing skills well.

View File

@@ -0,0 +1,5 @@
interface:
display_name: "Ask Matt"
short_description: "Find the right skill or workflow"
policy:
allow_implicit_invocation: false

View File

@@ -1,10 +1,12 @@
--- ---
name: grilling name: grilling
description: Interview the user relentlessly about a plan or design. Use when the user wants to stress-test a plan before building, or uses any 'grill' trigger phrases. description: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.
--- ---
Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. Interview me relentlessly about every aspect of this until we reach a shared understanding. Walk down each branch of the decision tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.
Ask the questions one at a time, waiting for feedback on each question before continuing. Asking multiple questions at once is bewildering. Ask the questions one at a time, waiting for feedback on each question before continuing. Asking multiple questions at once is bewildering.
If a question can be answered by exploring the codebase, explore the codebase instead. If a *fact* can be found by exploring the environment (filesystem, tools, etc.), look it up rather than asking me. The *decisions*, though, are mine — put each one to me and wait for my answer.
Do not act on it until I confirm we have reached a shared understanding.

View File

@@ -0,0 +1,3 @@
interface:
display_name: "Grilling"
short_description: "Stress-test thinking one question at a time"

View File

@@ -9,7 +9,7 @@ Write a handoff document summarising the current conversation so a fresh agent c
Include a "suggested skills" section in the document, which suggests skills that the agent should invoke. Include a "suggested skills" section in the document, which suggests skills that the agent should invoke.
Do not duplicate content already captured in other artifacts (PRDs, plans, ADRs, issues, commits, diffs). Reference them by path or URL instead. Do not duplicate content already captured in other artifacts (specs, plans, ADRs, issues, commits, diffs). Reference them by path or URL instead.
Redact any sensitive information, such as API keys, passwords, or personally identifiable information. Redact any sensitive information, such as API keys, passwords, or personally identifiable information.

View File

@@ -0,0 +1,5 @@
interface:
display_name: "Handoff"
short_description: "Compact a conversation into a handoff"
policy:
allow_implicit_invocation: false

View File

@@ -1,15 +1,15 @@
--- ---
name: implement name: implement
description: "Implement a piece of work based on a PRD or set of issues." description: "Implement a piece of work based on a spec or set of tickets."
disable-model-invocation: true disable-model-invocation: true
--- ---
Implement the work described by the user in the PRD or issues. Implement the work described by the user in the spec or tickets.
Use /tdd where possible, at pre-agreed seams. Use /tdd where possible, at pre-agreed seams.
Run typechecking regularly, single test files regularly, and the full test suite once at the end. Run typechecking regularly, single test files regularly, and the full test suite once at the end.
Once done, use /review to review the work. Once done, use /code-review to review the work.
Commit your work to the current branch. Commit your work to the current branch.

View File

@@ -0,0 +1,5 @@
interface:
display_name: "Implement"
short_description: "Build work from a spec or tickets"
policy:
allow_implicit_invocation: false

View File

@@ -17,6 +17,11 @@ This command is _informed_ by the project's domain model and built on a shared d
### 1. Explore ### 1. Explore
**Scope before you scan — YAGNI.** Deepening a module pays off by making future changes to it easier, so put extra weight on the parts of the codebase that have recently changed. Decide *where* to look before you look:
- If the user named a direction — a module, a subsystem, a pain point — take it, and skip the inference below.
- Otherwise, walk back a good stretch of the commit history (`git log --oneline`) to find the codebase's hot spots — the files and areas that keep coming up — and let those paths pull your attention first. If the changes are scattered with no clear hot spot, widen the net.
Read the project's domain glossary (`CONTEXT.md`) and any ADRs in the area you're touching first. Read the project's domain glossary (`CONTEXT.md`) and any ADRs in the area you're touching first.
Then use the Agent tool with `subagent_type=Explore` to walk the codebase. Don't follow rigid heuristics — explore organically and note where you experience friction: Then use the Agent tool with `subagent_type=Explore` to walk the codebase. Don't follow rigid heuristics — explore organically and note where you experience friction:
@@ -56,7 +61,7 @@ Do NOT propose interfaces yet. After the file is written, ask the user: "Which o
### 3. Grilling loop ### 3. Grilling loop
Once the user picks a candidate, run the `/grilling` skill to walk the design tree with them — constraints, dependencies, the shape of the deepened module, what sits behind the seam, what tests survive. Once the user picks a candidate, run the `/grilling` skill to walk the decision tree with them — constraints, dependencies, the shape of the deepened module, what sits behind the seam, what tests survive.
Side effects happen inline as decisions crystallize — run the `/domain-modeling` skill to keep the domain model current as you go: Side effects happen inline as decisions crystallize — run the `/domain-modeling` skill to keep the domain model current as you go:

View File

@@ -0,0 +1,5 @@
interface:
display_name: "Improve Codebase Architecture"
short_description: "Find and grill architecture improvements"
policy:
allow_implicit_invocation: false

View File

@@ -36,7 +36,7 @@ The right shape depends on the question:
Pick whichever shape best fits the question being asked, *not* whichever is easiest to wire to a TUI. Keep it pure: no I/O, no terminal code, no `console.log` for control flow. The TUI imports it and calls into it; nothing flows the other direction. Pick whichever shape best fits the question being asked, *not* whichever is easiest to wire to a TUI. Keep it pure: no I/O, no terminal code, no `console.log` for control flow. The TUI imports it and calls into it; nothing flows the other direction.
This is what makes the prototype useful past its own lifetime. When the question's been answered, the validated reducer / machine / function set can be lifted into the real module — the TUI shell gets deleted. This is what makes the prototype useful past its own lifetime: when the question's been answered, the validated reducer / machine / function set can be lifted into the real module on its own.
### 4. Build the smallest TUI that exposes the state ### 4. Build the smallest TUI that exposes the state
@@ -66,9 +66,9 @@ If the host project has no task runner, just put the command at the top of the p
Give the user the run command. They'll drive it themselves; the interesting moments are when they say "wait, that shouldn't be possible" or "huh, I assumed X would be different" — those are the bugs in the _idea_, which is the whole point. If they want new actions added, add them. Prototypes evolve. Give the user the run command. They'll drive it themselves; the interesting moments are when they say "wait, that shouldn't be possible" or "huh, I assumed X would be different" — those are the bugs in the _idea_, which is the whole point. If they want new actions added, add them. Prototypes evolve.
### 7. Capture the answer ### 7. Capture the answer and the prototype
When the prototype has done its job, the answer to the question is the only thing worth keeping. If the user is around, ask what it taught them. If not, leave a `NOTES.md` next to the prototype so the answer can be filled in (or filled in by you, if you've watched the session) before the prototype gets deleted. Once the prototype has answered its question, capture the answer, then capture the prototype the way the [SKILL](SKILL.md) describes. The logic-specific mapping: the validated reducer / machine / function set lifts into the real module (the decision, absorbed); the TUI shell rides along to the throwaway branch that keeps the prototype as a primary source.
## Anti-patterns ## Anti-patterns

View File

@@ -1,7 +1,6 @@
--- ---
name: prototype name: prototype
description: Build a throwaway prototype to flesh out a design — a runnable terminal app for state/business-logic questions, or several radically different UI variations toggleable from one route. description: Build a throwaway prototype to answer a design question. Use when the user wants to sanity-check whether a state model or logic feels right, or explore what a UI should look like.
disable-model-invocation: true
--- ---
# Prototype # Prototype
@@ -22,10 +21,6 @@ The two branches produce very different artifacts — getting this wrong wastes
1. **Throwaway from day one, and clearly marked as such.** Locate the prototype code close to where it will actually be used (next to the module or page it's prototyping for) so context is obvious — but name it so a casual reader can see it's a prototype, not production. For throwaway UI routes, obey whatever routing convention the project already uses; don't invent a new top-level structure. 1. **Throwaway from day one, and clearly marked as such.** Locate the prototype code close to where it will actually be used (next to the module or page it's prototyping for) so context is obvious — but name it so a casual reader can see it's a prototype, not production. For throwaway UI routes, obey whatever routing convention the project already uses; don't invent a new top-level structure.
2. **One command to run.** Whatever the project's existing task runner supports — `pnpm <name>`, `python <path>`, `bun <path>`, etc. The user must be able to start it without thinking. 2. **One command to run.** Whatever the project's existing task runner supports — `pnpm <name>`, `python <path>`, `bun <path>`, etc. The user must be able to start it without thinking.
3. **No persistence by default.** State lives in memory. Persistence is the thing the prototype is _checking_, not something it should depend on. If the question explicitly involves a database, hit a scratch DB or a local file with a clear "PROTOTYPE — wipe me" name. 3. **No persistence by default.** State lives in memory. Persistence is the thing the prototype is _checking_, not something it should depend on. If the question explicitly involves a database, hit a scratch DB or a local file with a clear "PROTOTYPE — wipe me" name.
4. **Skip the polish.** No tests, no error handling beyond what makes the prototype _runnable_, no abstractions. The point is to learn something fast and then delete it. 4. **Skip the polish.** No tests, no error handling beyond what makes the prototype _runnable_, no abstractions. The point is to learn something fast.
5. **Surface the state.** After every action (logic) or on every variant switch (UI), print or render the full relevant state so the user can see what changed. 5. **Surface the state.** After every action (logic) or on every variant switch (UI), print or render the full relevant state so the user can see what changed.
6. **Delete or absorb when done.** When the prototype has answered its question, either delete it or fold the validated decision into the real code — don't leave it rotting in the repo. 6. **Capture it when done.** Fold any validated decision into the real code, then capture the prototype itself as a **primary source**: commit it to a throwaway branch, out of main, and leave a context pointer to that branch on the implementation issue. Capture the answer too — the verdict and the question it settled — in the issue or a commit. The main branch keeps only the validated decision.
## When done
The _answer_ is the only thing worth keeping from a prototype. Capture it somewhere durable (commit message, ADR, issue, or a `NOTES.md` next to the prototype) along with the question it was answering. If the user is around, that capture is a quick conversation; if not, leave the placeholder so they (or you, on the next pass) can fill in the verdict before deleting the prototype.

View File

@@ -97,12 +97,12 @@ Surface the URL (and the `?variant=` keys). The user will flip through whenever
### 6. Capture the answer and clean up ### 6. Capture the answer and clean up
Once a variant has won, write down which one and why (commit message, ADR, issue, or a `NOTES.md` next to the prototype if running AFK and the user hasn't responded yet). Then: Once a variant has won, capture the answer — which variant and why — then capture the prototype the way the [SKILL](SKILL.md) describes. Fold the winner into the real code and move the rest onto the throwaway branch, not into main:
- **Sub-shape A** — delete the losing variants and the switcher; fold the winner into the existing page. - **Sub-shape A** — fold the winner into the existing page; drop the losing variants and the switcher from main.
- **Sub-shape B** — promote the winning variant to a real route, delete the throwaway route and the switcher. - **Sub-shape B** — promote the winning variant to a real route; drop the throwaway route and the switcher from main.
Don't leave variant components or the switcher lying around. They rot fast and confuse the next reader. The full set of variants is the primary source, so it lands on the throwaway branch, not the bin — variant components and the switcher left in the main branch rot fast and confuse the next reader.
## Anti-patterns ## Anti-patterns

View File

@@ -0,0 +1,3 @@
interface:
display_name: "Prototype"
short_description: "Prototype to answer a design question"

View File

@@ -26,16 +26,18 @@ Look at the current repo to understand its starting state. Read whatever exists;
- `docs/adr/` and any `src/*/docs/adr/` directories - `docs/adr/` and any `src/*/docs/adr/` directories
- `docs/agents/` — does this skill's prior output already exist? - `docs/agents/` — does this skill's prior output already exist?
- `.scratch/` — sign that a local-markdown issue tracker convention is already in use - `.scratch/` — sign that a local-markdown issue tracker convention is already in use
- Is the `triage` skill installed? (a `triage` skill folder alongside this one, or `triage` in your available skills.) This decides whether Section B runs at all.
- Monorepo signals — a `pnpm-workspace.yaml`, a `workspaces` field in `package.json`, or a populated `packages/*` with its own `src/`. Present only in a genuinely large multi-package repo; their absence means single-context, which is almost every repo.
### 2. Present findings and ask ### 2. Present findings and ask
Summarise what's present and what's missing. Then walk the user through the three decisions **one at a time** — present a section, get the user's answer, then move to the next. Don't dump all three at once. Summarise what's present and what's missing. Then take the sections in order — one section, one answer, then the next.
Assume the user does not know what these terms mean. Each section starts with a short explainer (what it is, why these skills need it, what changes if they pick differently). Then show the choices and the default. Lead each section with the recommended answer so the user can accept it in a word. Give a one-line explainer only when the choice genuinely branches; skip the section entirely when exploration already settled it (Section B when `triage` isn't installed, Section C when there's no monorepo).
**Section A — Issue tracker.** **Section A — Issue tracker.**
> Explainer: The "issue tracker" is where issues live for this repo. Skills like `to-issues`, `triage`, `to-prd`, and `qa` read from and write to it — they need to know whether to call `gh issue create`, write a markdown file under `.scratch/`, or follow some other workflow you describe. Pick the place you actually track work for this repo. > Explainer: The "issue tracker" is where issues live for this repo. Skills like `to-tickets`, `triage`, `to-spec`, and `qa` read from and write to it — they need to know whether to call `gh issue create`, write a markdown file under `.scratch/`, or follow some other workflow you describe. Pick the place you actually track work for this repo.
Default posture: these skills were designed for GitHub. If a `git remote` points at GitHub, propose that. If a `git remote` points at GitLab (`gitlab.com` or a self-hosted host), propose GitLab. Otherwise (or if the user prefers), offer: Default posture: these skills were designed for GitHub. If a `git remote` points at GitHub, propose that. If a `git remote` points at GitLab (`gitlab.com` or a self-hosted host), propose GitLab. Otherwise (or if the user prefers), offer:
@@ -44,41 +46,26 @@ Default posture: these skills were designed for GitHub. If a `git remote` points
- **Local markdown** — issues live as files under `.scratch/<feature>/` in this repo (good for solo projects or repos without a remote) - **Local markdown** — issues live as files under `.scratch/<feature>/` in this repo (good for solo projects or repos without a remote)
- **Other** (Jira, Linear, etc.) — ask the user to describe the workflow in one paragraph; the skill will record it as freeform prose - **Other** (Jira, Linear, etc.) — ask the user to describe the workflow in one paragraph; the skill will record it as freeform prose
If — and only if — the user picked **GitHub** or **GitLab**, ask one follow-up: Record the choice in `docs/agents/issue-tracker.md`. The GitHub and GitLab templates carry a "PRs as a request surface" flag, defaulted **off** — leave it off and don't raise it; a user who wants external PRs in the triage queue can flip the flag in the file later.
> Explainer: Open-source repos often receive feature requests as pull requests, not just issues — a PR is an issue with attached code. If you turn this on, `/triage` pulls *external* PRs into the same queue and runs them through the same labels and states as issues (collaborators' in-flight PRs are left alone). Leave it off if PRs aren't a request surface for you. **Section B — Triage label vocabulary.** Skip this section entirely if the `triage` skill isn't installed (exploration told you) — an uninstalled skill needs no labels.
- **PRs as a request surface** — yes / no (default: no). Record the answer in `docs/agents/issue-tracker.md`. For local-markdown and other trackers, skip this question — there are no PRs. If it is installed, ask exactly one question:
**Section B — Triage label vocabulary.** > Do you want to keep the default triage labels? (recommended: **yes**)
> Explainer: When the `triage` skill processes an incoming issue, it moves it through a state machine — needs evaluation, waiting on reporter, ready for an AFK agent to pick up, ready for a human, or won't fix. To do that, it needs to apply labels (or the equivalent in your issue tracker) that match strings *you've actually configured*. If your repo already uses different label names (e.g. `bug:triage` instead of `needs-triage`), map them here so the skill applies the right ones instead of creating duplicates. The defaults are the five canonical roles, each label string equal to its name: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`. On **yes**, write them as-is. Only if the user says no — usually because their tracker already uses other names (e.g. `bug:triage` for `needs-triage`) — collect the overrides so `triage` applies existing labels instead of creating duplicates.
The five canonical roles: **Section C — Domain docs.** Default to **single-context** — one `CONTEXT.md` + `docs/adr/` at the repo root. This fits almost every repo; write it without asking.
- `needs-triage` — maintainer needs to evaluate Offer **multi-context** — a root `CONTEXT-MAP.md` pointing to per-context `CONTEXT.md` files — only when exploration found monorepo signals. Then confirm which layout they want.
- `needs-info` — waiting on reporter
- `ready-for-agent` — fully specified, AFK-ready (an agent can pick it up with no human context)
- `ready-for-human` — needs human implementation
- `wontfix` — will not be actioned
Default: each role's string equals its name. Ask the user if they want to override any. If their issue tracker has no existing labels, the defaults are fine.
**Section C — Domain docs.**
> Explainer: Some skills (`improve-codebase-architecture`, `diagnosing-bugs`, `tdd`) read a `CONTEXT.md` file to learn the project's domain language, and `docs/adr/` for past architectural decisions. They need to know whether the repo has one global context or multiple (e.g. a monorepo with separate frontend/backend contexts) so they look in the right place.
Confirm the layout:
- **Single-context** — one `CONTEXT.md` + `docs/adr/` at the repo root. Most repos are this.
- **Multi-context** — `CONTEXT-MAP.md` at the root pointing to per-context `CONTEXT.md` files (typically a monorepo).
### 3. Confirm and edit ### 3. Confirm and edit
Show the user a draft of: Show the user a draft of:
- The `## Agent skills` block to add to whichever of `CLAUDE.md` / `AGENTS.md` is being edited (see step 4 for selection rules) - The `## Agent skills` block to add to whichever of `CLAUDE.md` / `AGENTS.md` is being edited (see step 4 for selection rules)
- The contents of `docs/agents/issue-tracker.md`, `docs/agents/triage-labels.md`, `docs/agents/domain.md` - The contents of `docs/agents/issue-tracker.md`, `docs/agents/domain.md`, and `docs/agents/triage-labels.md` (the last only when `triage` is installed)
Let them edit before writing. Let them edit before writing.
@@ -101,7 +88,7 @@ The block:
### Issue tracker ### Issue tracker
[one-line summary of where issues are tracked, plus whether external PRs are a triage surface]. See `docs/agents/issue-tracker.md`. [one-line summary of where issues are tracked]. See `docs/agents/issue-tracker.md`.
### Triage labels ### Triage labels
@@ -112,12 +99,14 @@ The block:
[one-line summary of layout — "single-context" or "multi-context"]. See `docs/agents/domain.md`. [one-line summary of layout — "single-context" or "multi-context"]. See `docs/agents/domain.md`.
``` ```
Then write the three docs files using the seed templates in this skill folder as a starting point: Include the `### Triage labels` sub-block, and write `docs/agents/triage-labels.md`, only when `triage` is installed and Section B ran. When it isn't, both are omitted.
Then write the docs files using the seed templates in this skill folder as a starting point:
- [issue-tracker-github.md](./issue-tracker-github.md) — GitHub issue tracker - [issue-tracker-github.md](./issue-tracker-github.md) — GitHub issue tracker
- [issue-tracker-gitlab.md](./issue-tracker-gitlab.md) — GitLab issue tracker - [issue-tracker-gitlab.md](./issue-tracker-gitlab.md) — GitLab issue tracker
- [issue-tracker-local.md](./issue-tracker-local.md) — local-markdown issue tracker - [issue-tracker-local.md](./issue-tracker-local.md) — local-markdown issue tracker
- [triage-labels.md](./triage-labels.md) — label mapping - [triage-labels.md](./triage-labels.md) — label mapping (only if `triage` is installed)
- [domain.md](./domain.md) — domain doc consumer rules + layout - [domain.md](./domain.md) — domain doc consumer rules + layout
For "other" issue trackers, write `docs/agents/issue-tracker.md` from scratch using the user's description. For "other" issue trackers, write `docs/agents/issue-tracker.md` from scratch using the user's description.

View File

@@ -0,0 +1,5 @@
interface:
display_name: "Setup Matt Pocock Skills"
short_description: "Configure a repo for the skills"
policy:
allow_implicit_invocation: false

View File

@@ -32,3 +32,14 @@ Create a GitHub issue.
## When a skill says "fetch the relevant ticket" ## When a skill says "fetch the relevant ticket"
Run `gh issue view <number> --comments`. Run `gh issue view <number> --comments`.
## Wayfinding operations
Used by `/wayfinder`. The **map** is a single issue with **child** issues as tickets.
- **Map**: a single issue labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. `gh issue create --label wayfinder:map`.
- **Child ticket**: an issue linked to the map as a GitHub sub-issue (`gh api` on the sub-issues endpoint). Where sub-issues aren't enabled, add the child to a task list in the map body and put `Part of #<map>` at the top of the child body. Labels: `wayfinder:<type>` (`research`/`prototype`/`grilling`/`task`). Once claimed, the ticket is assigned to the driving dev.
- **Blocking**: GitHub's **native issue dependencies** — the canonical, UI-visible representation. Add an edge with `gh api --method POST repos/<owner>/<repo>/issues/<child>/dependencies/blocked_by -F issue_id=<blocker-db-id>`, where `<blocker-db-id>` is the blocker's numeric **database id** (`gh api repos/<owner>/<repo>/issues/<n> --jq .id`, _not_ the `#number` or `node_id`). GitHub reports `issue_dependencies_summary.blocked_by` (open blockers only — the live gate). Where dependencies aren't available, fall back to a `Blocked by: #<n>, #<n>` line at the top of the child body. A ticket is unblocked when every blocker is closed.
- **Frontier query**: list the map's open children (`gh issue list --state open`, scoped to the map's sub-issues / task list), drop any with an open blocker (`issue_dependencies_summary.blocked_by > 0`, or an open issue in the `Blocked by` line) or an assignee; first in map order wins.
- **Claim**: `gh issue edit <n> --add-assignee @me` — the session's first write.
- **Resolve**: `gh issue comment <n> --body "<answer>"`, then `gh issue close <n>`, then append a context pointer (gist + link) to the map's Decisions-so-far.

View File

@@ -33,3 +33,14 @@ Create a GitLab issue.
## When a skill says "fetch the relevant ticket" ## When a skill says "fetch the relevant ticket"
Run `glab issue view <number> --comments`. Run `glab issue view <number> --comments`.
## Wayfinding operations
Used by `/wayfinder`. The **map** is a single issue with **child** issues as tickets.
- **Map**: a single issue labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. `glab issue create --label wayfinder:map`. (On GitLab tiers with native epics, an epic may hold the map instead; a labelled issue works everywhere.)
- **Child ticket**: an issue carrying `Part of #<map>` at the top of its description and labels `wayfinder:<type>` (`research`/`prototype`/`grilling`/`task`). Once claimed, the ticket is assigned to the driving dev.
- **Blocking**: GitLab's **native blocking link** — the canonical, UI-visible representation. Add it with the `/blocked_by #<n>` quick action, posted as a note (`glab issue note <child> --message "/blocked_by #<blocker>"`). Native blocking links are a Premium/Ultimate feature; on the free tier (or where unavailable) fall back to a `Blocked by: #<n>, #<n>` line at the top of the description. A ticket is unblocked when every blocker is closed.
- **Frontier query**: `glab issue list -F json` scoped to the map's children, drop any with an open blocker — a native `blocked_by` link to an open issue (`glab api projects/:id/issues/:iid/links`), or an open issue in the `Blocked by` line — or an assignee; first in map order wins.
- **Claim**: `glab issue update <n> --assignee @me` — the session's first write.
- **Resolve**: `glab issue note <n> --message "<answer>"`, then `glab issue close <n>`, then append a context pointer (gist + link) to the map's Decisions-so-far.

View File

@@ -1,12 +1,12 @@
# Issue tracker: Local Markdown # Issue tracker: Local Markdown
Issues and PRDs for this repo live as markdown files in `.scratch/`. Issues and specs (you may know a spec as a PRD) for this repo live as markdown files in `.scratch/`.
## Conventions ## Conventions
- One feature per directory: `.scratch/<feature-slug>/` - One feature per directory: `.scratch/<feature-slug>/`
- The PRD is `.scratch/<feature-slug>/PRD.md` - The spec is `.scratch/<feature-slug>/spec.md`
- Implementation issues are `.scratch/<feature-slug>/issues/<NN>-<slug>.md`, numbered from `01` - Implementation issues are one file per ticket at `.scratch/<feature-slug>/issues/<NN>-<slug>.md`, numbered from `01` — never a single combined tickets file
- Triage state is recorded as a `Status:` line near the top of each issue file (see `triage-labels.md` for the role strings) - Triage state is recorded as a `Status:` line near the top of each issue file (see `triage-labels.md` for the role strings)
- Comments and conversation history append to the bottom of the file under a `## Comments` heading - Comments and conversation history append to the bottom of the file under a `## Comments` heading
@@ -17,3 +17,14 @@ Create a new file under `.scratch/<feature-slug>/` (creating the directory if ne
## When a skill says "fetch the relevant ticket" ## When a skill says "fetch the relevant ticket"
Read the file at the referenced path. The user will normally pass the path or the issue number directly. Read the file at the referenced path. The user will normally pass the path or the issue number directly.
## Wayfinding operations
Used by `/wayfinder`. The **map** is a file with one **child** file per ticket.
- **Map**: `.scratch/<effort>/map.md` — the Notes / Decisions-so-far / Fog body.
- **Child ticket**: `.scratch/<effort>/issues/NN-<slug>.md`, numbered from `01`, with the question in the body. A `Type:` line records the ticket type (`research`/`prototype`/`grilling`/`task`); a `Status:` line records `claimed`/`resolved`.
- **Blocking**: a `Blocked by: NN, NN` line near the top. A ticket is unblocked when every file it lists is `resolved`.
- **Frontier**: scan `.scratch/<effort>/issues/` for files that are open, unblocked, and unclaimed; first by number wins.
- **Claim**: set `Status: claimed` and save before any work.
- **Resolve**: append the answer under an `## Answer` heading, set `Status: resolved`, then append a context pointer (gist + link) to the map's Decisions-so-far in `map.md`.

View File

@@ -0,0 +1,102 @@
---
name: setup-ts-deep-modules
description: Wire dependency-cruiser into a TypeScript repo so each package is a deep module — implementation hidden in subfolders, reachable only through its entry-point files. User-invoked.
disable-model-invocation: true
---
# Setup TS Deep Modules
Make every package in this repo a **deep module**: a lot of behaviour behind a small interface. A package's public surface is its **entry points** — the files at the package root — and everything in its subfolders is hidden. This skill installs [dependency-cruiser](https://github.com/sverweij/dependency-cruiser) and the rules that make the entry points the only way in, then proves the rules bite.
For the vocabulary (deep module, interface, seam, depth), run the `/codebase-design` skill — use its language throughout.
## The shape this enforces
```
src/packages/
<name>/
index.ts ← an entry point (public). Import this from outside.
client.ts ← another entry point. Packages may expose SEVERAL.
lib/ ← implementation: hidden from outside, free to import each other.
tests/ ← co-located tests + fixtures (a subfolder, so private).
```
The public surface is the package's **root files** — not one designated `index.ts`. By convention implementation lives in `lib/` and tests in `tests/`, giving every package the same two-folder shape. The rule itself is general, though: *anything* in *any* subfolder is private, so you never extend the config to add a folder.
Four rules, all `error`:
1. **Entry-point boundary** — code outside a package (app code or another package) may import only that package's entry points (its root files), never anything in its subfolders.
2. **Intra-package freedom** — a package's own files import each other freely.
3. **Tests through the entry points** — files under `<pkg>/tests/` may import any package's entry points and their own `tests/` fixtures, but never any package's subfolder internals (not even their own). Integration tests across packages are fine; deep imports are not.
4. **No cycles** — no dependency cycles.
**Entry points, not a barrel.** Because the public surface is *every* root file, a package can expose several small entry points (`index.ts`, `client.ts`, `server.ts`) instead of funnelling everything through one giant `index.ts`. Barrel files that re-export a whole subtree are discouraged — keep entry points small and hide implementation in subfolders.
Layering (which packages may depend on which) is a *different* concern and is left as a commented stub in the config for this repo to fill in.
## Steps
### 1. Detect the environment
- **Package manager** — `pnpm-lock.yaml` → pnpm, `yarn.lock` → yarn, `bun.lockb` → bun, else npm. Use it for every command below (`pnpm`/`yarn`/`npm run`/`bunx`).
- **Packages root** — if `src/` exists use `src/packages`, else `packages`. Confirm the choice with the user if the repo already has a different obvious convention.
- **Existing config** — check for a `.dependency-cruiser.*` file. If one exists, do **not** overwrite it: merge the four rules and the options in, and tell the user what you added.
**Done when:** package manager, packages root, and existing-config status are all known.
### 2. Install dependency-cruiser
Install `dependency-cruiser` as a devDependency with the detected package manager.
**Done when:** `dependency-cruiser` is in `devDependencies`.
### 3. Write the config
Copy [`dependency-cruiser.config.cjs`](./dependency-cruiser.config.cjs) to the repo root as `.dependency-cruiser.cjs`. Set `PACKAGES_ROOT` to the root detected in step 1. The rules are path-depth based and extension-agnostic, so nothing else needs adapting.
**Done when:** `.dependency-cruiser.cjs` exists with the correct `PACKAGES_ROOT`, and the four forbidden rules are present.
### 4. Wire it into the checks
- Add a `lint:boundaries` script: `depcruise <packages-root>` (or `depcruise src`).
- Fold it into the repo's umbrella check command — the one that already runs typecheck (e.g. a `check` / `ci` / `validate` script). Do **not** touch `tsconfig` or add path aliases.
- If there is no umbrella script, add `lint:boundaries` and tell the user to include it in CI.
**Done when:** `lint:boundaries` exists and runs as part of the same command as typecheck.
### 5. Scaffold the example package
Create a committed `<packages-root>/example/` as a copy-me template:
- `index.ts` — an entry point. Export one function that delegates to an internal file (so the package is visibly *deep*, not a pass-through).
- `lib/impl.ts` — an internal file in a **subfolder**, imported by `index.ts`, not reachable from outside.
- `tests/example.test.ts` — imports **only** `../index` (an entry point), and asserts against the public function.
Tell the user this is a starter template to copy or delete.
**Done when:** the example package exists, exposes its behaviour through a root entry point, and hides `impl` in a subfolder.
### 6. Prove the rules bite
This is the completion criterion for the whole skill — a config that doesn't fail on a violation is worthless.
1. Run `lint:boundaries`. It must **pass** on the clean example.
2. Temporarily add a deep import to `tests/example.test.ts` (e.g. `import { thing } from "../lib/impl"`). Run `lint:boundaries` again — it must **fail** with `tests-through-entrypoints`.
3. Revert the deep import. Run once more — it must **pass**.
**Done when:** you have observed a pass, then a fail on the deep import, then a pass again. If step 2 does not fail, the rules are not wired correctly — fix before finishing.
### 7. Document the convention
Write a `README.md` **in the packages folder** (`<packages-root>/README.md`) — next to the packages it governs — covering: the `src/packages/<name>/` layout (entry points at the root, `lib/` for implementation, `tests/` for tests), "import only through a package's entry points (its root files)", and how to run `lint:boundaries`. **Discourage barrel files** explicitly — expose several small entry points instead of re-exporting a whole subtree through one index. Keep it to the copy-me snippet plus the four rules in one paragraph each.
Then add a **context pointer** to it from the repo's agent-instructions file — `CLAUDE.md` if present, else `AGENTS.md` (create `AGENTS.md` if neither exists). One line is enough, e.g. `Packages are deep modules — see [src/packages/README.md](./src/packages/README.md) before adding or importing one.` This is what makes an agent discover the boundary rule instead of tripping over it.
**Done when:** `<packages-root>/README.md` exists and discourages barrels, and the repo's `CLAUDE.md`/`AGENTS.md` links to it.
## Notes
- The config's `$1` back-references (dependency-cruiser's group matching) are what let a package reach its own internals while outsiders can't — don't flatten them into separate per-package rules.
- Public vs private is decided by **depth**: a package's root files are entry points; anything in a subfolder is private. The conventional subfolders are `lib/` (implementation) and `tests/`, but the rule doesn't hardcode them — any subfolder is private, so a new folder never needs a config change. Adding an entry point is just adding a root file — no barrel.
- Packages are **flat**: one tier of immediate children under the root. A package's internals may nest as deep as you like; a package may not contain another package.
- Use `.cjs` (not `.js`) so the config's `module.exports` works even in `"type": "module"` repos.

View File

@@ -0,0 +1,5 @@
interface:
display_name: "Setup TS Deep Modules"
short_description: "Enforce deep TypeScript modules"
policy:
allow_implicit_invocation: false

View File

@@ -0,0 +1,95 @@
// @ts-check
// Deep-module enforcement for dependency-cruiser.
//
// Each package under the packages root is a DEEP MODULE: a lot of behaviour
// behind a small interface. A package's PUBLIC SURFACE is its ENTRY POINTS —
// the files at the package root. Implementation lives in SUBFOLDERS and is
// private — by convention `lib/` for implementation and `tests/` for tests,
// though any subfolder is private. A package may expose several small entry
// points (index.ts, client.ts, server.ts, …) — prefer that over one giant
// barrel index.
//
// The only thing you should ever need to edit here is PACKAGES_ROOT.
/** Where packages live. One immediate child dir per package (flat, no nesting). */
const PACKAGES_ROOT = "src/packages";
// --- derived patterns (no need to edit) -------------------------------------
const R = PACKAGES_ROOT;
/**
* A package's private internals: anything nested inside a package subfolder.
* The package's root files are its entry points and are NOT matched here —
* they stay importable from outside.
*/
const PACKAGE_INTERNALS = `^${R}/[^/]+/[^/]+/`;
/** @type {import('dependency-cruiser').IConfiguration} */
module.exports = {
forbidden: [
{
name: "entrypoint-boundary-from-app",
comment:
"App/root code may import a package's entry points (its root files), but nothing inside its subfolders.",
severity: "error",
from: { pathNot: `^${R}/` }, // importer is NOT inside any package
to: { path: PACKAGE_INTERNALS },
},
{
name: "entrypoint-boundary-across-packages",
comment:
"A package's own files import each other freely, but may reach OTHER packages only through their entry points — never their internals.",
severity: "error",
// importer is inside a package ($1), but is not a test file
from: { path: `^${R}/([^/]+)/`, pathNot: `^${R}/[^/]+/tests/` },
to: {
path: PACKAGE_INTERNALS,
pathNot: `^${R}/$1/`, // same package → intra-package freedom
},
},
{
name: "tests-through-entrypoints",
comment:
"A package's tests exercise it through its entry points like everyone else: they may import any package's entry points and their own tests/ fixtures, but never any package's internals — not even their own.",
severity: "error",
from: { path: `^${R}/([^/]+)/tests/` }, // a test file, in package $1
to: {
path: PACKAGE_INTERNALS,
pathNot: `^${R}/$1/tests/`, // own tests/ fixtures → allowed
},
},
{
name: "tests-folder-is-private",
comment:
"A package's tests/ folder is reachable only from tests — nothing else may import fixtures.",
severity: "error",
from: { pathNot: `^${R}/[^/]+/tests/` }, // importer is not itself a test
to: { path: `^${R}/[^/]+/tests/` },
},
{
name: "no-circular",
comment: "No dependency cycles. Scope to `^${R}/` if you want to allow cycles outside packages.",
severity: "error",
from: {},
to: { circular: true },
},
// --- Layering (optional, off by default) ----------------------------------
// Interface-hiding controls HOW you import (through the entry points).
// Layering controls WHICH packages may depend on which. Add your own rules
// here, e.g.:
//
// {
// name: "ui-may-not-depend-on-billing",
// severity: "error",
// from: { path: `^${R}/ui/` },
// to: { path: `^${R}/billing/` },
// },
],
options: {
doNotFollow: { path: "node_modules" },
tsConfig: { fileName: "tsconfig.json" },
enhancedResolveOptions: {
extensions: [".ts", ".tsx", ".js", ".jsx", ".json"],
},
},
};

View File

@@ -5,104 +5,32 @@ description: Test-driven development. Use when the user wants to build features
# Test-Driven Development # Test-Driven Development
## Philosophy TDD is the red → green loop. This skill is the reference that makes that loop produce tests worth keeping: what a good test is, where tests go, the anti-patterns, and the rules of the loop. Every section applies on every cycle — consult them before and during the loop, not after.
**Core principle**: Tests should verify behavior through public interfaces, not implementation details. Code can change entirely; tests shouldn't. When exploring the codebase, read `CONTEXT.md` (if it exists) so test names and interface vocabulary match the project's domain language, and respect ADRs in the area you're touching.
**Good tests** are integration-style: they exercise real code paths through public APIs. They describe _what_ the system does, not _how_ it does it. A good test reads like a specification - "user can checkout with valid cart" tells you exactly what capability exists. These tests survive refactors because they don't care about internal structure. ## What a good test is
**Bad tests** are coupled to implementation. They mock internal collaborators, test private methods, or verify through external means (like querying a database directly instead of using the interface). The warning sign: your test breaks when you refactor, but behavior hasn't changed. If you rename an internal function and tests fail, those tests were testing implementation, not behavior. Tests verify behavior through public interfaces, not implementation details. Code can change entirely; tests shouldn't. A good test reads like a specification — "user can checkout with valid cart" tells you exactly what capability exists — and survives refactors because it doesn't care about internal structure.
See [tests.md](tests.md) for examples and [mocking.md](mocking.md) for mocking guidelines. See [tests.md](tests.md) for examples and [mocking.md](mocking.md) for mocking guidelines.
## Anti-Pattern: Horizontal Slices ## Seams — where tests go
**DO NOT write all tests first, then all implementation.** This is "horizontal slicing" - treating RED as "write all tests" and GREEN as "write all code." A **seam** is the public boundary you test at: the interface where you observe behavior without reaching inside. Tests live at seams, never against internals.
This produces **crap tests**: **Test only at pre-agreed seams.** Before writing any test, write down the seams under test and confirm them with the user. No test is written at an unconfirmed seam. You can't test everything — agreeing the seams up front is how testing effort lands on the critical paths and complex logic instead of every edge case.
- Tests written in bulk test _imagined_ behavior, not _actual_ behavior Ask: "What's the public interface, and which seams should we test?"
- You end up testing the _shape_ of things (data structures, function signatures) rather than user-facing behavior
- Tests become insensitive to real changes - they pass when behavior breaks, fail when behavior is fine
- You outrun your headlights, committing to test structure before understanding the implementation
**Correct approach**: Vertical slices via tracer bullets. One test → one implementation → repeat. Each test responds to what you learned from the previous cycle. Because you just wrote the code, you know exactly what behavior matters and how to verify it. ## Anti-patterns
``` - **Implementation-coupled** — mocks internal collaborators, tests private methods, or verifies through a side channel (querying the database instead of using the interface). The tell: the test breaks when you refactor but behavior hasn't changed.
WRONG (horizontal): - **Tautological** — the assertion recomputes the expected value the way the code does (`expect(add(a, b)).toBe(a + b)`, a snapshot derived by hand the same way, a constant asserted equal to itself), so it passes by construction and can never disagree with the code. Expected values must come from an independent source of truth — a known-good literal, a worked example, the spec.
RED: test1, test2, test3, test4, test5 - **Horizontal slicing** — writing all tests first, then all implementation. Bulk tests verify _imagined_ behavior: you test the _shape_ of things rather than user-facing behavior, the tests go insensitive to real changes, and you commit to test structure before understanding the implementation. Work in **vertical slices** instead — one test → one implementation → repeat, each test a **tracer bullet** that responds to what the last cycle taught you.
GREEN: impl1, impl2, impl3, impl4, impl5
RIGHT (vertical): ## Rules of the loop
RED→GREEN: test1→impl1
RED→GREEN: test2→impl2
RED→GREEN: test3→impl3
...
```
## Workflow - **Red before green.** Write the failing test first, then only enough code to pass it. Don't anticipate future tests or add speculative features.
- **One slice at a time.** One seam, one test, one minimal implementation per cycle.
### 1. Planning - **Refactoring is not part of the loop.** It belongs to the review stage (see the `code-review` skill), not the red → green implementation cycle.
When exploring the codebase, read `CONTEXT.md` (if it exists) so that test names and interface vocabulary match the project's domain language, and respect ADRs in the area you're touching.
Before writing any code:
- [ ] Confirm with user what interface changes are needed
- [ ] Confirm with user which behaviors to test (prioritize)
- [ ] Identify opportunities for deep modules (small interface, deep implementation) — run the `/codebase-design` skill for the vocabulary and the testability checks
- [ ] List the behaviors to test (not implementation steps)
- [ ] Get user approval on the plan
Ask: "What should the public interface look like? Which behaviors are most important to test?"
**You can't test everything.** Confirm with the user exactly which behaviors matter most. Focus testing effort on critical paths and complex logic, not every possible edge case.
### 2. Tracer Bullet
Write ONE test that confirms ONE thing about the system:
```
RED: Write test for first behavior → test fails
GREEN: Write minimal code to pass → test passes
```
This is your tracer bullet - proves the path works end-to-end.
### 3. Incremental Loop
For each remaining behavior:
```
RED: Write next test → fails
GREEN: Minimal code to pass → passes
```
Rules:
- One test at a time
- Only enough code to pass current test
- Don't anticipate future tests
- Keep tests focused on observable behavior
### 4. Refactor
After all tests pass, look for [refactor candidates](refactoring.md):
- [ ] Extract duplication
- [ ] Deepen modules (move complexity behind simple interfaces)
- [ ] Apply SOLID principles where natural
- [ ] Consider what new code reveals about existing code
- [ ] Run tests after each refactor step
**Never refactor while RED.** Get to GREEN first.
## Checklist Per Cycle
```
[ ] Test describes behavior, not implementation
[ ] Test uses public interface only
[ ] Test would survive internal refactor
[ ] Code is minimal for this test
[ ] No speculative features added
```

View File

@@ -0,0 +1,3 @@
interface:
display_name: "TDD"
short_description: "Test-driven red-green-refactor"

View File

@@ -59,3 +59,19 @@ test("createUser makes user retrievable", async () => {
expect(retrieved.name).toBe("Alice"); expect(retrieved.name).toBe("Alice");
}); });
``` ```
**Tautological tests**: Expected value restates the implementation, so the test passes by construction.
```typescript
// BAD: Expected value is recomputed the way the code computes it
test("calculateTotal sums line items", () => {
const items = [{ price: 10 }, { price: 5 }];
const expected = items.reduce((sum, i) => sum + i.price, 0);
expect(calculateTotal(items)).toBe(expected);
});
// GOOD: Expected value is an independent, known literal
test("calculateTotal sums line items", () => {
expect(calculateTotal([{ price: 10 }, { price: 5 }])).toBe(15);
});
```

View File

@@ -0,0 +1,75 @@
---
name: to-spec
description: Turn the current conversation into a spec and publish it to the project issue tracker — no interview, just synthesis of what you've already discussed.
disable-model-invocation: true
---
This skill takes the current conversation context and codebase understanding and produces a spec (you may know this document as a PRD). Do NOT interview the user — just synthesize what you already know.
The issue tracker and triage label vocabulary should have been provided to you — run `/setup-matt-pocock-skills` if not.
## Process
1. Explore the repo to understand the current state of the codebase, if you haven't already. Use the project's domain glossary vocabulary throughout the spec, and respect any ADRs in the area you're touching.
2. Sketch out the seams at which you're going to test the feature. Existing seams should be preferred to new ones. Use the highest seam possible. If new seams are needed, propose them at the highest point you can. The fewer seams across the codebase, the better - the ideal number is one.
Check with the user that these seams match their expectations.
3. Write the spec using the template below, then publish it to the project issue tracker. Apply the `ready-for-agent` triage label - no need for additional triage.
<spec-template>
## Problem Statement
The problem that the user is facing, from the user's perspective.
## Solution
The solution to the problem, from the user's perspective.
## User Stories
A LONG, numbered list of user stories. Each user story should be in the format of:
1. As an <actor>, I want a <feature>, so that <benefit>
<user-story-example>
1. As a mobile bank customer, I want to see balance on my accounts, so that I can make better informed decisions about my spending
</user-story-example>
This list of user stories should be extremely extensive and cover all aspects of the feature.
## Implementation Decisions
A list of implementation decisions that were made. This can include:
- The modules that will be built/modified
- The interfaces of those modules that will be modified
- Technical clarifications from the developer
- Architectural decisions
- Schema changes
- API contracts
- Specific interactions
Do NOT include specific file paths or code snippets. They may end up being outdated very quickly.
Exception: if a prototype produced a snippet that encodes a decision more precisely than prose can (state machine, reducer, schema, type shape), inline it within the relevant decision and note briefly that it came from a prototype. Trim to the decision-rich parts — not a working demo, just the important bits.
## Testing Decisions
A list of testing decisions that were made. Include:
- A description of what makes a good test (only test external behavior, not implementation details)
- Which modules will be tested
- Prior art for the tests (i.e. similar types of tests in the codebase)
## Out of Scope
A description of the things that are out of scope for this spec.
## Further Notes
Any further notes about the feature.
</spec-template>

View File

@@ -0,0 +1,5 @@
interface:
display_name: "To Spec"
short_description: "Turn a conversation into a spec"
policy:
allow_implicit_invocation: false

View File

@@ -0,0 +1,107 @@
---
name: to-tickets
description: Break a plan, spec, or the current conversation into a set of tracer-bullet tickets, each declaring its blocking edges, published to the configured tracker — edges as text in one file per ticket locally, or native blocking links on a real tracker.
disable-model-invocation: true
---
# To Tickets
Break a plan, spec, or conversation into a set of **tickets** — tracer-bullet vertical slices, each declaring the tickets that **block** it.
The issue tracker and triage label vocabulary should have been provided to you — run `/setup-matt-pocock-skills` if not.
## Process
### 1. Gather context
Work from whatever is already in the conversation context. If the user passes a reference (a spec path, an issue number or URL) as an argument, fetch it and read its full body and comments.
### 2. Explore the codebase (optional)
If you have not already explored the codebase, do so to understand the current state of the code. Ticket titles and descriptions should use the project's domain glossary vocabulary, and respect ADRs in the area you're touching.
Look for opportunities to prefactor the code to make the implementation easier. "Make the change easy, then make the easy change."
### 3. Draft vertical slices
Break the work into **tracer bullet** tickets.
<vertical-slice-rules>
- Each slice cuts a narrow but COMPLETE path through every layer (schema, API, UI, tests) — vertical, NOT a horizontal slice of one layer
- A completed slice is demoable or verifiable on its own
- Each slice is sized to fit in a single fresh context window
- Any prefactoring should be done first
</vertical-slice-rules>
Give each ticket its **blocking edges** — the other tickets that must complete before it can start. A ticket with no blockers can start immediately.
**Wide refactors are the exception to vertical slicing.** A **wide refactor** is one mechanical change — rename a column, retype a shared symbol — whose **blast radius** fans across the whole codebase, so a single edit breaks thousands of call sites at once and no vertical slice can land green. Don't force it into a tracer bullet; sequence it as **expandcontract**. First expand: add the new form beside the old so nothing breaks. Then migrate the call sites over in batches sized by blast radius (per package, per directory), each batch its own ticket blocked by the expand, keeping CI green batch to batch because the old form still exists. Finally contract: delete the old form once no caller remains, in a ticket blocked by every migrate batch. When even the batches can't stay green alone, keep the sequence but let them share an integration branch that all block a final integrate-and-verify ticket — green is promised only there.
### 4. Quiz the user
Present the proposed breakdown as a numbered list. For each ticket, show:
- **Title**: short descriptive name
- **Blocked by**: which other tickets (if any) must complete first
- **What it delivers**: the end-to-end behaviour this ticket makes work
Ask the user:
- Does the granularity feel right? (too coarse / too fine)
- Are the blocking edges correct — does each ticket only depend on tickets that genuinely gate it?
- Should any tickets be merged or split further?
Iterate until the user approves the breakdown.
### 5. Publish the tickets to the configured tracker
Publish the approved tickets. **How** depends on the tracker `/setup-matt-pocock-skills` configured — the tickets are the same either way, only the shape of the blocking edges changes:
- **Local files** → write one file per ticket under `.scratch/<feature-slug>/issues/<NN>-<slug>.md`, numbered from `01` in dependency order (blockers first). Each file's "Blocked by" lists the numbers/titles it depends on. Use the per-ticket file template below — one ticket per file, never a single combined file.
- **A real issue tracker (GitHub, Linear, …)** → publish one issue per ticket in dependency order (blockers first) so each ticket's blocking edges can reference real identifiers. Use the platform's native blocking / sub-issue relationship where it has one; otherwise set each ticket's "Blocked by" to the blocking issues. Apply the `ready-for-agent` triage label unless instructed otherwise — the tickets are agent-grabbable by construction.
Work the **frontier**: any ticket whose blockers are all done. For a purely linear chain that means top to bottom.
Do NOT close or modify any parent issue.
<local-ticket-template>
# <NN> — <Ticket title>
**What to build:** the end-to-end behaviour this ticket makes work, from the user's perspective — not a layer-by-layer implementation list.
**Blocked by:** the numbers/titles of the tickets that gate this one, or "None — can start immediately".
**Status:** ready-for-agent
- [ ] Acceptance criterion 1
- [ ] Acceptance criterion 2
</local-ticket-template>
<issue-template>
## Parent
A reference to the parent issue on the tracker (if the source was an existing issue, otherwise omit this section).
## What to build
The end-to-end behaviour this ticket makes work, from the user's perspective — not layer-by-layer implementation.
## Acceptance criteria
- [ ] Criterion 1
- [ ] Criterion 2
## Blocked by
- A reference to each blocking ticket, or "None — can start immediately".
</issue-template>
In either form, avoid specific file paths or code snippets — they go stale fast. Exception: if a prototype produced a snippet that encodes a decision more precisely than prose can (state machine, reducer, schema, type shape), inline it and note briefly that it came from a prototype. Trim to the decision-rich parts — not a working demo, just the important bits.
Work the frontier one ticket at a time with `/implement`, clearing context between tickets.

View File

@@ -0,0 +1,5 @@
interface:
display_name: "To Tickets"
short_description: "Split a plan into tracer-bullet tickets"
policy:
allow_implicit_invocation: false

View File

@@ -1,9 +1,16 @@
--- ---
name: wayfinder name: wayfinder
description: Plan a huge chunk of work — more than one agent session can hold — as a shared map of investigation tickets on your issue tracker, and resolve them one at a time until the way to the goal is clear. description: Plan a huge chunk of work — more than one agent session can hold — as a shared map of decision tickets on your issue tracker, and resolve them one at a time until the way to the destination is clear.
disable-model-invocation: true
--- ---
A loose idea has arrived — too big for one agent session, and wrapped in fog: the route from here to a plan isn't visible yet. This skill charts it as a **shared map** on the repo's issue tracker, then works its tickets one at a time. The map is domain-agnostic — engineering work, course content, whatever fits the shape. A loose idea has arrived — too big for one agent session, and wrapped in fog: the way from here to the **destination** isn't visible yet. Wayfinding is about finding that way, not charging at the destination. This skill charts the way as a **shared map** on the repo's issue tracker, then works its **decision tickets** — questions whose resolution is a decision, not slices of a build to execute — one at a time until the route is clear.
The destination varies per effort, and naming it is the first act of charting — it shapes every ticket. It might be a spec to hand off and iterate on, a decision to lock before planning starts, or a change made in place like a data-structure migration. The map is domain-agnostic — engineering work, course content, whatever fits the shape.
## Plan, don't do
Wayfinder is **planning** by default: each ticket resolves a decision, and the map is done when the way is clear — nothing left to decide before someone goes and does the thing. The pull to just do the work is usually the signal you've reached the edge of the map and it's time to hand off. An effort can override this in its **Notes** — carrying execution into the map itself — but absent that, produce decisions, not deliverables.
## Refer by name ## Refer by name
@@ -15,13 +22,17 @@ The map is a single issue on this repo's issue tracker, labelled `wayfinder:map`
The map is an **index**, not a store. It lists the decisions made and points at the tickets that hold their detail; a decision lives in exactly one place — its ticket — so the map never restates it, only gists it and links. The map is an **index**, not a store. It lists the decisions made and points at the tickets that hold their detail; a decision lives in exactly one place — its ticket — so the map never restates it, only gists it and links.
**Where the map, its child tickets, blocking, and frontier queries physically live is tracker-specific.** Consult `docs/agents/issue-tracker.md` (the "Wayfinding operations" section) for how _this_ repo expresses them. If that doc is absent, default to the local-markdown tracker. **Where the map, its child tickets, blocking, and frontier queries physically live is tracker-specific.** The issue tracker should have been provided to you — run `/setup-matt-pocock-skills` if not. Consult the tracker doc's "Wayfinding operations" section for how _this_ repo expresses them. If no tracker has been provided, default to the local-markdown tracker.
### The map body ### The map body
The whole map at low resolution, loaded once per session. Open tickets are **not** listed — they are open child issues, found by query. The whole map at low resolution, loaded once per session. Open tickets are **not** listed — they are open child issues, found by query.
```markdown ```markdown
## Destination
<what reaching the end of this map looks like — the spec, decision, or change this effort is finding its way to. One or two lines; every session orients to it before choosing a ticket.>
## Notes ## Notes
<domain; skills every session should consult; standing preferences for this effort> <domain; skills every session should consult; standing preferences for this effort>
@@ -32,9 +43,13 @@ The whole map at low resolution, loaded once per session. Open tickets are **not
- [<closed ticket title>](link) — <one-line gist of the answer> - [<closed ticket title>](link) — <one-line gist of the answer>
## Fog ## Not yet specified
<!-- see "Fog of war" for what belongs here --> <!-- see "Fog of war": in-scope fog you can't ticket yet; graduates as the frontier advances -->
## Out of scope
<!-- see "Out of scope": work ruled beyond the destination; closed, never graduates -->
``` ```
### Tickets ### Tickets
@@ -57,36 +72,48 @@ The answer isn't part of the body — it's recorded on resolution (see [Work thr
## Ticket Types ## Ticket Types
- **Research**: Reading documentation, third-party APIs, or local resources like knowledge bases. Creates a markdown summary as a linked asset. Use when knowledge outside the current working directory is required. Every ticket is either **HITL** — human in the loop, worked *with* a human who speaks for themselves — or **AFK**, driven by the agent alone. A HITL ticket only resolves through that live exchange; the agent never stands in for the human's side of it (a grilling agent that answers its own questions has broken this).
- **Prototype**: Raise the fidelity of the discussion by making a cheap, rough, concrete artifact to react to — an outline, a rough take, a stub, or UI/logic code via the /prototype skill. Links the prototype as an asset. Use when "how should it look" or "how should it behave" is the key question.
- **Grilling**: Conversation with the agent. Uses the /grilling and /domain-modeling skills. Asks one question at a time. The default case. - **Research** (AFK): Reading documentation, third-party APIs, or local resources like knowledge bases to surface a fact a decision waits on. Resolved by a `/research` **subagent**. Use when knowledge outside the current working directory is required.
- **Task**: Literal manual work that must be done before the discussion can move forward — nothing to decide, prototype, or research. Moving data, signing up for a service, provisioning access. The agent automates it where it can; otherwise it hands the human a precise checklist. Resolved when the work is done; the answer records what was done and any resulting facts (credentials location, new URLs, row counts) later tickets depend on. - **Prototype** (HITL): Raise the fidelity of the discussion by making a cheap, rough, concrete artifact to react to — an outline, a rough take, a stub, or UI/logic code via the /prototype skill. Links the prototype as an asset. Use when "how should it look" or "how should it behave" is the key question.
- **Grilling** (HITL): Conversation via the /grilling and /domain-modeling skills, one question at a time. The default case.
- **Task** (HITL or AFK): Manual work that must happen before a *decision* can be made — nothing to decide, prototype, or research, but the discussion is blocked until it's done. Signing up for a service so its API can be judged, provisioning access, moving data so its shape can be seen. This is the one type that *does* rather than decides — and it earns its place by unblocking a decision, not by delivering the destination. The agent drives it alone where it can (AFK); otherwise it hands the human a precise checklist (HITL). Resolved when the work is done; the answer records what was done and any resulting facts (credentials location, new URLs, row counts) later tickets depend on.
## Fog of war ## Fog of war
The map is _deliberately_ incomplete: don't chart what you can't yet see. Beyond the tickets lies fog — the dim view of decisions and investigations you can tell are coming but can't yet pin down, because they hang on questions still open. Resolving a ticket clears the fog ahead of it, graduating whatever's now specifiable into fresh tickets — one at a time, until the way to the goal is clear and no tickets remain. The map is _deliberately_ incomplete: don't chart what you can't yet see. Beyond the live tickets lies the **fog of war** — the dim view of decisions and investigations you can tell are coming but can't yet pin down, because they hang on questions still open. Resolving a ticket clears the fog ahead of it, graduating whatever's now specifiable into fresh tickets — one at a time, until the way to the destination is clear and no tickets remain.
The map's **Fog** section is where that dim view is written down: the suspected question, the area to revisit later, the risk you're deferring. Write as loosely or as fully as the view allows; it doubles as a signpost for collaborators reading where the effort is headed. The map's **Not yet specified** section is where that dim view is written down: the suspected question, the area to revisit later. It's the undiscovered frontier _toward_ the destination — everything here is in scope, just not sharp enough to ticket. Write as loosely or as fully as the view allows; it doubles as a signpost for collaborators reading where the effort is headed.
**Fog or ticket?** The test is whether you can state the question precisely now — _not_ whether you can answer it now. **Fog or ticket?** The test is whether you can state the question precisely now — _not_ whether you can answer it now.
- **Ticket when** the question is already sharp — even if it's blocked and you can't act on it yet. - **Ticket when** the question is already sharp — even if it's blocked and you can't act on it yet.
- **Fog when** you can't yet phrase it that sharply. Don't pre-slice fog into ticket-sized pieces: it's coarser than a ticket, and one patch may graduate into several tickets, or none, once the frontier reaches it. - **Not yet specified when** you can't yet phrase it that sharply. Don't pre-slice the fog into ticket-sized pieces: it's coarser than a ticket, and one patch may graduate into several tickets, or none, once the frontier reaches it.
Fog excludes only what's already decided (that's Decisions so far) and what's already a ticket. **Not yet specified** excludes what's already decided (Decisions so far), what's already a live ticket, and what's out of scope (the next section).
## Out of scope
Fog only ever gathers _toward_ the destination. The destination fixes the scope, so work beyond it is **out of scope** — it isn't fog, and it doesn't belong in **Not yet specified**. It gets its own **Out of scope** section on the map: work you've consciously ruled out of _this_ effort. Scope, not sharpness, lands it here.
Out-of-scope work never graduates — the frontier stops at the destination — so it returns only if the destination is redrawn, and then as a fresh effort, not a resumption.
Ruling something out of scope is a scoping act, not a step on the route. When a ticket that already exists turns out to sit past the destination — mis-scoped in while charting, or exposed by a resolution — **close it** (a closed ticket is unambiguously off the frontier) and leave one line in the **Out of scope** section: the gist plus why it's out of scope, linking the closed ticket. It stays out of **Decisions so far**, which records the route actually walked — a scope boundary isn't a step on it.
## Invocation ## Invocation
Two modes. Either way, **never resolve more than one ticket per session.** Two modes. Either way, **never resolve more than one ticket per session** — with the exception of research tickets.
### Chart the map ### Chart the map
User invokes with a loose idea. User invokes with a loose idea.
1. Run a `/grilling` and `/domain-modeling` session to surface the open decisions. 1. **Name the destination.** Run a `/grilling` and `/domain-modeling` session to pin down what this map is finding its way to — the spec, decision, or change. The destination fixes the scope, so it's settled first.
2. **Create the map** (label `wayfinder:map`): Notes filled in, Decisions-so-far empty, Fog sketched. 2. **Map the frontier.** Grill again, **breadth-first** this time: fan out across the whole space rather than deep on any one thread, surfacing the open decisions and the first steps takeable now. **If this surfaces no fog** — the way to the destination is already clear, the whole journey small enough for one session — you don't need a map. Stop and ask the user how they'd like to proceed.
3. **Create the tickets you can specify now** as child issues of the map — then wire blocking edges in a **second pass** (issues need ids before they can reference each other). Wiring sorts them into the frontier and the blocked; everything you can't yet specify stays in the Fog. 3. **Create the map** (label `wayfinder:map`): Destination and Notes filled in, Decisions-so-far empty, the fog sketched into **Not yet specified**.
4. Stop — charting the map is one session's work; do not also resolve tickets. 4. **Create the tickets you can specify now** as child issues of the map — then wire blocking edges in a **second pass** (issues need ids before they can reference each other). Wiring sorts them into the frontier and the blocked; everything you can't yet specify stays in the fog — the **Not yet specified** section.
5. **Fire the research subagents.** For each `research` ticket you just created, spin up a `/research` subagent to resolve it in parallel, capturing its findings on a throwaway `research/<name>` branch with a context pointer from the ticket.
6. Stop — charting is one session's work; it hand-resolves nothing.
### Work through the map ### Work through the map
@@ -96,6 +123,6 @@ User invokes with a map (URL or number). A ticket is **optional** — without on
2. Choose the ticket. If the user named one, use it. Otherwise take the first frontier ticket in order. **Claim it**: assign it to yourself before any work. 2. Choose the ticket. If the user named one, use it. Otherwise take the first frontier ticket in order. **Claim it**: assign it to yourself before any work.
3. Resolve it — **zoom as needed**: fetch the full body of any related or closed ticket on demand; invoke the skills the `## Notes` block names. If in doubt, use `/grilling` and `/domain-modeling`. 3. Resolve it — **zoom as needed**: fetch the full body of any related or closed ticket on demand; invoke the skills the `## Notes` block names. If in doubt, use `/grilling` and `/domain-modeling`.
4. Record the resolution: post the answer as a **resolution comment**, **close** the issue, and **append a context pointer** to the map's Decisions-so-far. 4. Record the resolution: post the answer as a **resolution comment**, **close** the issue, and **append a context pointer** to the map's Decisions-so-far.
5. Add newly-surfaced tickets (create-then-wire); graduate any fog the answer has made specifiable, clearing each graduated patch from the Fog so it lives only as its new ticket. If the decision invalidates other parts of the map, update or delete those tickets. 5. Add newly-surfaced tickets (create-then-wire); graduate any fog the answer has made specifiable, clearing each graduated patch from **Not yet specified** so it lives only as its new ticket. If the answer reveals a ticket — this one or another — sits beyond the destination, **rule it out of scope** rather than resolving it on the route. If the decision invalidates other parts of the map, update or delete those tickets.
The user may run unblocked tickets in parallel, so expect other sessions to be editing the tracker concurrently. The user may run unblocked tickets in parallel, so expect other sessions to be editing the tracker concurrently.

View File

@@ -0,0 +1,5 @@
interface:
display_name: "Wayfinder"
short_description: "Map a large effort as decision tickets"
policy:
allow_implicit_invocation: false

View File

@@ -158,6 +158,12 @@ _Failure mode._ Ending the current step before it is genuinely done, because the
_Avoid_: premature closure, the rush, rushing, shortcutting _Avoid_: premature closure, the rush, rushing, shortcutting
### Negation
_Failure mode._ Steering by prohibition — telling the agent what _not_ to do — which drags the forbidden behaviour into context and makes it _more_ available, not less. _Don't think of an elephant_, and the elephant is all there is; _never write verbose comments_, and verbosity is the pattern the agent has just read. The negation is a weak modifier the strongly-activated concept overruns, so the ban half-reads as an instruction to do the thing. Its **leading word** is the _elephant_: whatever a prohibition names into the frame. Cure: prompt the **positive** — describe the target behaviour ("write one-line comments") so the banned one is never spoken. A prohibition earns its place only as a hard guardrail on a behaviour you cannot phrase positively; even then, pair it with the positive target so attention lands on what to do.
_Avoid_: ironic rebound, don't-prompting, the pink elephant
## Pruning ## Pruning
Keeping a skill lean — each remedy paired with the failure it cures. Keeping a skill lean — each remedy paired with the failure it cures.

View File

@@ -80,3 +80,4 @@ Use these to diagnose issues the user may be having with the skill.
- **Sediment** — stale layers that settle because adding feels safe and removing feels risky. The default fate of any skill without a pruning discipline. - **Sediment** — stale layers that settle because adding feels safe and removing feels risky. The default fate of any skill without a pruning discipline.
- **Sprawl** — a skill simply too long, even when every line is live and unique. Hurts readability and maintainability and wastes tokens. The cure is the ladder: disclose **reference** behind pointers, and split by **branch** or sequence so each path carries only what it needs. - **Sprawl** — a skill simply too long, even when every line is live and unique. Hurts readability and maintainability and wastes tokens. The cure is the ladder: disclose **reference** behind pointers, and split by **branch** or sequence so each path carries only what it needs.
- **No-op** — a line the model already obeys by default, so you pay load to say nothing. The test: does it change behaviour versus the default? A weak leading word (_be thorough_ when the agent is already thorough-ish) is a no-op; the fix is a stronger word (_relentless_), not a different technique. - **No-op** — a line the model already obeys by default, so you pay load to say nothing. The test: does it change behaviour versus the default? A weak leading word (_be thorough_ when the agent is already thorough-ish) is a no-op; the fix is a stronger word (_relentless_), not a different technique.
- **Negation** — steering by prohibition backfires: _don't think of an elephant_ names the elephant and makes it more available, not less. Prompt the **positive** — state the target behaviour so the banned one is never spoken; keep a prohibition only as a hard guardrail you can't phrase positively, and even then pair it with what to do instead.

View File

@@ -0,0 +1,5 @@
interface:
display_name: "Writing Great Skills"
short_description: "Principles for predictable skills"
policy:
allow_implicit_invocation: false

View File

@@ -2,9 +2,9 @@
- [Product selling points](product-selling-points.md) — key differentiators and landing page angles for neuron-tai - [Product selling points](product-selling-points.md) — key differentiators and landing page angles for neuron-tai
- [User profile](user-profile.md) — who Dobromir is and how to work with him - [User profile](user-profile.md) — who Dobromir is and how to work with him
- [Project status](project-status.md) — 35/35 stories done; alpha hardening next - [Project status](project-status.md) — US-001…US-035 done; US-036…US-050 in docs/prd.json; alpha hardening + scratch features next
- **Alpha hardening** — `.scratch/alpha-hardening/` (22 issues, ADRs 00160019, [README](../../.scratch/alpha-hardening/README.md), [handoff](../../.scratch/alpha-hardening/handoff.md)) - **Alpha hardening** — `.scratch/alpha-hardening/` (22 issues, ADRs 00160019, [README](../../.scratch/alpha-hardening/README.md), [handoff](../../.scratch/alpha-hardening/handoff.md))
- [Alpha hardening navigation](alpha-hardening-navigation.md) — locked fraud/auth decisions, Bucket-1 order, handoff pointers - [Alpha hardening navigation](alpha-hardening-navigation.md) — locked fraud/auth decisions, Bucket-1 order, handoff pointers
- **Node capability admission** — `.scratch/node-capability-admission/` (P0 plan: generic doctor/real-forward validation, fail-closed readiness, tracker admission gate; [PRD](../../.scratch/node-capability-admission/PRD.md), [README](../../.scratch/node-capability-admission/README.md), ADR-0023) - **Node capability admission** — `.scratch/node-capability-admission/` (P0 plan; [ADR-0023](../../docs/adr/0023-model-agnostic-node-capability-admission.md), [ADR-0026](../../docs/adr/0026-node-assignment-ownership-and-managed-placement.md))
- **Distributed relay performance** — relay `/rpc` requester sockets are persistent per Route Session and Activation Seam as of 2026-07-10; `request_id` remains unique per activation while `X-Meshnet-Session` remains stable for KV state. Next low-risk priorities: persistent direct/loopback HTTP, seam byte/latency telemetry, then trace-driven zstd tuning. - **Distributed relay performance** — relay `/rpc` requester sockets are persistent per Route Session and Activation Seam as of 2026-07-10; `request_id` remains unique per activation while `X-Meshnet-Session` remains stable for KV state. Next low-risk priorities: persistent direct/loopback HTTP, seam byte/latency telemetry, then trace-driven zstd tuning.
- **Distributed GGUF direction** — benchmark-gated native runtime: compare controlled Transformers/safetensors and whole-model llama.cpp lanes before expensive work; ship only for measured speed or model-fit advantage. Public parallelism is contiguous Shards in an Inference Route; concurrency comes from per-node continuous batching across isolated Route Sessions, while tensor/expert collectives stay inside optional trusted composite providers. Native data plane uses versioned Protobuf over long-lived gRPC/HTTP2 seam streams, with existing relay carrying the same opaque frames when needed. llama.cpp/GGML remains the substrate behind a project-owned standalone worker and small pinned fork; vLLM is an optional complete managed provider and concept donor, not a fork. Nakshatra, `prima.cpp`, `llama-gguf`, LiGGUF and historical GPUStack are source/test donors only. Active plan: [README](../../.scratch/distributed-gguf-runtime/README.md), [architecture](../../.scratch/distributed-gguf-runtime/architecture.md), [PRD](../../.scratch/distributed-gguf-runtime/PRD.md), [Ralph backlog](../../.scratch/distributed-gguf-runtime/prd.json). Research: [landscape](../../docs/research/distributed-gguf-landscape.md), [GitHub follow-up](../../docs/research/distributed-gguf-github-followup.md), [vLLM](../../docs/research/vllm-distributed-gguf-assessment.md). - **Distributed GGUF direction** — benchmark-gated native runtime: compare controlled Transformers/safetensors and whole-model llama.cpp lanes before expensive work; ship only for measured speed or model-fit advantage. Public parallelism is contiguous Shards in an Inference Route; concurrency comes from per-node continuous batching across isolated Route Sessions, while tensor/expert collectives stay inside optional trusted composite providers. Native data plane uses versioned Protobuf over long-lived gRPC/HTTP2 seam streams, with existing relay carrying the same opaque frames when needed. llama.cpp/GGML remains the substrate behind a project-owned standalone worker and small pinned fork; vLLM is an optional complete managed provider and concept donor, not a fork. Nakshatra, `prima.cpp`, `llama-gguf`, LiGGUF and historical GPUStack are source/test donors only. Active plan: [README](../../.scratch/distributed-gguf-runtime/README.md), [architecture](../../.scratch/distributed-gguf-runtime/architecture.md), [PRD](../../.scratch/distributed-gguf-runtime/PRD.md), [Ralph backlog](../../.scratch/distributed-gguf-runtime/prd.json). ADR: [0024](../../docs/adr/0024-distributed-gguf-runtime.md). Research: [landscape](../../docs/research/distributed-gguf-landscape.md), [GitHub follow-up](../../docs/research/distributed-gguf-github-followup.md), [vLLM](../../docs/research/vllm-distributed-gguf-assessment.md).

View File

@@ -20,13 +20,13 @@ Active workstream (started 2026-07-04): alpha hardening of the money/trust path.
**Launch-readiness grilling (2026-07-06):** Locked launch plan — devnet dev/test run now, then **real mainnet SOL/USDT** (not devnet, not a new public token) for the first cohort: friends (API clients) + hired VPS/VPC hosts (our own test infra, not third-party volunteers — stake-free, risk-free if something breaks, not a long-term topology). Pricing: clients are the only party spending real money; nodes only accumulate off-chain credit and get paid in batches (30min dev / 24h later) — a failed distribution leaves funds parked, not lost, so mainnet-vs-devnet mixups are lower-risk than initially assumed. TAI token: do NOT issue/list now — ADR-0002 already locks listing behind $50k volume + 25 nodes/15 wallets plus an unresolved securities-review gate; only a dormant mainnet mint (cheap, ~few $ SOL) for name/branding reservation is in scope, bundled with treasury-key work, not before it. Treasury custody: bare keypair file (current runbook 02) is not acceptable for real funds — plan is **free native SPL multisig** (`spl-token create-multisig`, no protocol fee unlike Squads' 0.5 SOL), 2-of-3 signers, at least one cold/offline, others one-per-hired-VPS-provider to avoid correlated compromise (not yet built — ops task, no issue filed). Stake/slash asymmetry (registry/slash is a local Python adapter per ADR-0007, not on-chain) accepted for now since hired hosts are our own infra and friends aren't node operators — revisit before opening to real third-party node operators. A mainnet-vs-devnet boot guardrail was proposed and explicitly declined by the owner given the safe-by-default money flow above. **Launch-readiness grilling (2026-07-06):** Locked launch plan — devnet dev/test run now, then **real mainnet SOL/USDT** (not devnet, not a new public token) for the first cohort: friends (API clients) + hired VPS/VPC hosts (our own test infra, not third-party volunteers — stake-free, risk-free if something breaks, not a long-term topology). Pricing: clients are the only party spending real money; nodes only accumulate off-chain credit and get paid in batches (30min dev / 24h later) — a failed distribution leaves funds parked, not lost, so mainnet-vs-devnet mixups are lower-risk than initially assumed. TAI token: do NOT issue/list now — ADR-0002 already locks listing behind $50k volume + 25 nodes/15 wallets plus an unresolved securities-review gate; only a dormant mainnet mint (cheap, ~few $ SOL) for name/branding reservation is in scope, bundled with treasury-key work, not before it. Treasury custody: bare keypair file (current runbook 02) is not acceptable for real funds — plan is **free native SPL multisig** (`spl-token create-multisig`, no protocol fee unlike Squads' 0.5 SOL), 2-of-3 signers, at least one cold/offline, others one-per-hired-VPS-provider to avoid correlated compromise (not yet built — ops task, no issue filed). Stake/slash asymmetry (registry/slash is a local Python adapter per ADR-0007, not on-chain) accepted for now since hired hosts are our own infra and friends aren't node operators — revisit before opening to real third-party node operators. A mainnet-vs-devnet boot guardrail was proposed and explicitly declined by the owner given the safe-by-default money flow above.
**Two new issues from this session, both `ready-for-agent`:** **Two new issues from this session:**
- **21 — Honest-noise calibration corpus** (`.scratch/alpha-hardening/issues/21-honest-noise-calibration-corpus.md`) rescoped from "prod gate" to a **hard alpha-release blocker**. Confirmed by code read: `verify_activation_proofs()` (`packages/validator/meshnet_validator/audit.py:94-127`) returns bool only, no raw divergence value; fleet-dispatch exists but wrong shape (`server.py:2998-3104`, pinned routes + latency, not full-fleet + TOPLOC divergence); storage wrong shape (`registry_events` has no divergence/hardware columns). Three-part build: (1) surface raw TOPLOC distance from audit.py, (2) extend dispatch to hit every registered node with fixed prompt/seed, (3) new SQLite table keyed by node+GPU+dtype. Small-fleet exception granted (N = actual hired-VPS fleet size). Hired VPS hosts stay stake-free until this closes. - **21 — Honest-noise calibration corpus** `Status: ready-for-human` (engineering done 2026-07-06; blocked on human fleet calibration run before mainnet launch).
- **23 — Dynamic HF-benchmarked pricing** (`.scratch/alpha-hardening/issues/23-dynamic-hf-pricing.md`), high priority but not a release blocker. Pricing today is 100% static (`DEFAULT_PRICE_PER_1K_TOKENS = 0.02`, `billing.py:21`; `model_presets.json` has no per-model price). Target: 80% of cheapest comparable provider on `https://huggingface.co/inference/models` (per-provider-per-model marketplace, `?search=` query param works, no confirmed JSON API — plain scrape attempted first, escalate to headless browser only if the table isn't in raw HTML). Human-verified `hf_aliases` + `hf_verified_match_note` (params/quantization) per model, not auto-discovered matching. Reuses the `_settlement_loop` daemon-thread pattern for a daily refresh; falls back silently to the static default on any failure. - **23 — Dynamic HF-benchmarked pricing** `Status: done` (see `23-dynamic-hf-pricing_completed.md`).
Both are already migrated into `.scratch/alpha-hardening/prd.json` (AH-021 updated, AH-023 added) and the README index — ready for Ralph to pick up unattended. Both are already migrated into `.scratch/alpha-hardening/prd.json` (AH-021 updated, AH-023 added) and the README index — ready for Ralph to pick up unattended.
**Ralph note:** `scripts/ralph_progress.py` tracks `docs/prd.json` (35/35 done) and does NOT see `.scratch/alpha-hardening/issues/`. No ralph loop is running and no `.ralph-tui/` state exists. `.scratch/alpha-hardening/prd.json` now has 23 stories (AH-001…AH-023); point Ralph at that file for the alpha-hardening branch. Do NOT use `ralph auto --parallel` on server.py-touching issues — 21 and 23 both touch `server.py`/`billing.py`/`audit.py`; if run in the same Ralph pass, run them serially, not in parallel (merge-conflict risk, same lesson as 03/04 previously). **Ralph note:** `scripts/ralph_progress.py` tracks `docs/prd.json` (US-001…US-047; base 35/35 done, friends-test arc 3647 open/in-progress). Alpha hardening uses `.scratch/alpha-hardening/prd.json` (AH-001…AH-023). Point Ralph at the prd.json for the branch you're running.
**Why:** three audits agreed the alpha blockers are unauthenticated gossip (anyone can inject billing events), the free-credit faucet, and ephemeral bans. **Why:** three audits agreed the alpha blockers are unauthenticated gossip (anyone can inject billing events), the free-credit faucet, and ephemeral bans.
**How to apply:** work test-first per issue acceptance criteria; use `.venv`; `cryptography` belongs in node deps (wallet.py imports it — causes many of the 24 "failures" in a fresh env). See [[project-status]] and [[autonomous-work-style]]. **How to apply:** work test-first per issue acceptance criteria; use `.venv`; `cryptography` belongs in node deps (wallet.py imports it — causes many of the 24 "failures" in a fresh env). See [[project-status]] and [[autonomous-work-style]].

View File

@@ -6,7 +6,18 @@ metadata:
type: project type: project
--- ---
# Project Status (2026-07-02) # Project Status (2026-07-13)
## Selected-node model placement (2026-07-14)
- Admin Model placement now opens a node selector for load and release; the control-plane accepts optional `node_id` and targets only that registry assignment. Multi-model serving remains supported through `ADD_SHARD` and `max_loaded_shards`.
- Total node pool resource values are rendered from `/v1/network/map`'s `node.capacity` contract. Route selection remains assignment/capability/throughput/queue based; capacity is used for placement and falls back to tracker defaults only if a node truly omits it.
## Distributed inference performance (2026-07-14)
`DIP-001` is done in `.scratch/distributed-inference-performance/`: the deterministic two-node Route Session stub benchmark covers direct/relay plus cached/stateless prefill and decode. Its JSON and concise summary explicitly attribute model execution, activation encode/decode, compression, connection setup, relay queueing, local HTTP forwarding, and end-to-end seam latency. `PYTHONPATH=packages/node pytest -q tests/test_route_session_benchmark.py` passed (7); the fixture assertion checks output-token identity and connection attempts.
> Doc reconciliation 2026-07-13: `docs/prd.json` tracks US-001…US-050 (048 memory budget, 049 mainnet pilot, 050 Qwen demand placement). ADRs 00250026 added (TAI phase B/C, assignment ownership).
All 35 user stories in docs/prd.json are done (35/35), including the reward-system arc US-030…US-035 completed 2026-07-02: All 35 user stories in docs/prd.json are done (35/35), including the reward-system arc US-030…US-035 completed 2026-07-02:

1
.gitignore vendored
View File

@@ -20,6 +20,7 @@ dist/
!.env.testnet !.env.testnet
.rocm-local/* .rocm-local/*
.pytest-tmp/* .pytest-tmp/*
.cache/
# Local tracker/node sqlite databases (never commit runtime state) # Local tracker/node sqlite databases (never commit runtime state)
*.sqlite *.sqlite

View File

@@ -4,7 +4,8 @@
configVersion = "2.1" configVersion = "2.1"
tracker = "json" tracker = "json"
agent = "opencode" agent = "codex"
model = "gpt-5.6-terra"
maxIterations = 0 maxIterations = 0
autoCommit = true autoCommit = true

View File

@@ -2,9 +2,9 @@
Pre-release alpha audit + grilling (2026-07-04). Bucket 1 trust-boundary blockers + fraud arc: **done** (16/22 original issues). Bucket 2 (12-15, multi-tracker) and 17 (doc dedup) remain deferred/human-gated — not launch blockers. Pre-release alpha audit + grilling (2026-07-04). Bucket 1 trust-boundary blockers + fraud arc: **done** (16/22 original issues). Bucket 2 (12-15, multi-tracker) and 17 (doc dedup) remain deferred/human-gated — not launch blockers.
**Launch-readiness grilling (2026-07-06):** locked plan is devnet dev/test run now, then real mainnet SOL/USDT for the first cohort — friends (API clients) + hired VPS/VPC hosts (own test infra, not third-party volunteers, stake-free). No new public token; TAI stays dormant per ADR-0002's existing volume/legal gates. Two new issues came out of this session: **Launch-readiness grilling (2026-07-06):** locked plan is devnet dev/test run now, then real mainnet USDT for the first cohort — friends (API clients) + hired VPS/VPC hosts (own test infra, not third-party volunteers; no upfront stake, probation only). No new public token; TAI stays dormant per ADR-0002's existing volume/legal gates. Two new issues came out of this session:
- **[21 — Honest-noise calibration corpus](./issues/21-honest-noise-calibration-corpus.md)** — rescoped from "prod gate" to a hard **alpha-release blocker**. `Status: ready-for-human` — engineering (audit.py raw divergence, tracker dispatch endpoint, SQLite corpus, p99 envelope) done 2026-07-06; blocked on a human running the calibration job against the real hired-VPS fleet before launch. - **[21 — Honest-noise calibration corpus](./issues/21-honest-noise-calibration-corpus.md)** — rescoped from "prod gate" to a hard **alpha-release blocker**. `Status: ready-for-human` — engineering (audit.py raw divergence, tracker dispatch endpoint, SQLite corpus, p99 envelope) done 2026-07-06; blocked on a human running the calibration job against the real hired-VPS fleet before launch. Runbook: [04-toploc-calibration-run](./runbooks/04-toploc-calibration-run.md).
- **[23 — Dynamic HF-benchmarked pricing](./issues/23-dynamic-hf-pricing_completed.md)** — new, high priority but not a release blocker. `Status: done` — engineering complete 2026-07-06 (hf_pricing.py, opt-in daily refresh loop, GET /v1/pricing/hf/history); real `hf_aliases` curation per model is a follow-up human sign-off, not a completion blocker. - **[23 — Dynamic HF-benchmarked pricing](./issues/23-dynamic-hf-pricing_completed.md)** — new, high priority but not a release blocker. `Status: done` — engineering complete 2026-07-06 (hf_pricing.py, opt-in daily refresh loop, GET /v1/pricing/hf/history); real `hf_aliases` curation per model is a follow-up human sign-off, not a completion blocker.
Locked scope: one settlement tracker, open node join, devnet mock-USDT, reputation carries forward → fraud must be bounded. See [ADR-0016](../../docs/adr/0016-alpha-scope-and-known-limitations.md). Locked scope: one settlement tracker, open node join, devnet mock-USDT, reputation carries forward → fraud must be bounded. See [ADR-0016](../../docs/adr/0016-alpha-scope-and-known-limitations.md).
@@ -77,6 +77,7 @@ Locked scope: one settlement tracker, open node join, devnet mock-USDT, reputati
| [17 Duplicate US-020 dedup](./issues/17-doc-duplicate-us020-dedup.md) | | [17 Duplicate US-020 dedup](./issues/17-doc-duplicate-us020-dedup.md) |
| [18 Operational runbooks](./issues/18-doc-operational-runbooks_completed.md) | | [18 Operational runbooks](./issues/18-doc-operational-runbooks_completed.md) |
| [19 Cryptography + test env](./issues/19-doc-cryptography-test-env_completed.md) | | [19 Cryptography + test env](./issues/19-doc-cryptography-test-env_completed.md) |
| [04 TOPLOC calibration run](./runbooks/04-toploc-calibration-run.md) (issue 21 ops) |
| [22 MEMORY + project-status index](./issues/22-doc-memory-project-status_completed.md) (done) | | [22 MEMORY + project-status index](./issues/22-doc-memory-project-status_completed.md) (done) |
| [21 Honest-noise calibration corpus](./issues/21-honest-noise-calibration-corpus.md) (ops; prod gate for audits) | | [21 Honest-noise calibration corpus](./issues/21-honest-noise-calibration-corpus.md) (ops; prod gate for audits) |

View File

@@ -8,7 +8,7 @@
## 1. Mission / where we are ## 1. Mission / where we are
neuron-tai is a volunteer-GPU, pipeline-parallel LLM inference network with a working routing layer and a **broken money/trust path**. Three independent audits agreed: unauthenticated gossip, free-credit faucet, double-pay risks, ephemeral bans, and node self-reported accounting undermine alpha release. The owner locked alpha scope (single settlement tracker, open node join, devnet mock-USDT, carried-forward reputation) and a fraud/verification design (TOPLOC adoption, adaptive audits, on-demand hop bisection, persisted graduated reputation, tracker-authoritative accounting). **Research and planning artifacts are complete** (ADRs 00160019, 22 issue files, README index). Next: implement Bucket 1 blockers test-first. neuron-tai is a volunteer-GPU, pipeline-parallel LLM inference network with a working routing layer. Pre-release audits found the money/trust path was not alpha-ready; **Bucket 1 alpha blockers are implemented** (see `.scratch/alpha-hardening/README.md`). Remaining launch gates: issue **21** (human calibration run), post-alpha Bucket 2 (1215), and active scratch tracks (NCA, perf, distributed GGUF).
--- ---
@@ -42,7 +42,7 @@ Point to artifacts — do not re-derive from this handoff.
| Path | What it contains | | Path | What it contains |
|---|---| |---|---|
| `.scratch/alpha-hardening/README.md` | Issue/ADR index + implementation order | | `.scratch/alpha-hardening/README.md` | Issue/ADR index + implementation order |
| `.scratch/alpha-hardening/issues/` | 22 work items (Buckets 13) | | `.scratch/alpha-hardening/issues/` | 25 work items (Buckets 13 + perf follow-ups) |
| `.scratch/alpha-hardening/research-verifiable-inference.md` | SOTA research, layered alpha scheme (§8), build-vs-adopt (§9) | | `.scratch/alpha-hardening/research-verifiable-inference.md` | SOTA research, layered alpha scheme (§8), build-vs-adopt (§9) |
| `docs/adr/00160019` | Alpha scope, auth, fraud, multi-tracker design | | `docs/adr/00160019` | Alpha scope, auth, fraud, multi-tracker design |
| `docs/agents/issue-tracker.md` | Issue file conventions | | `docs/agents/issue-tracker.md` | Issue file conventions |

View File

@@ -1,6 +1,6 @@
Status: ready-for-human Status: ready-for-human
**BLOCKS ALPHA RELEASE.** Scoped 2026-07-06 during alpha-launch-readiness grilling session — must complete before real-money (mainnet SOL/USDT) traffic goes live for the friends + hired-VPS-host launch. Loose/uncalibrated thresholds + manual admin slash-reversal are the stopgap only until this closes. **BLOCKS ALPHA RELEASE.** Scoped 2026-07-06 during alpha-launch-readiness grilling session — must complete before real-money mainnet USDT traffic goes live for the friends + hired-VPS-host launch. Loose/uncalibrated thresholds + manual admin slash-reversal are the stopgap only until this closes.
**Engineering complete 2026-07-06; blocked on a human running it against the real hired-VPS fleet before launch.** The three code gaps below are closed and unit-tested (see Deliverables), but nothing in a dev session can stand in for actually dispatching the job at real hardware — that step, plus the threshold/FPR write-up that depends on its output, needs an operator with the live fleet. See the validator README's "Honest-noise calibration corpus" section for the operational how-to. **Engineering complete 2026-07-06; blocked on a human running it against the real hired-VPS fleet before launch.** The three code gaps below are closed and unit-tested (see Deliverables), but nothing in a dev session can stand in for actually dispatching the job at real hardware — that step, plus the threshold/FPR write-up that depends on its output, needs an operator with the live fleet. See the validator README's "Honest-noise calibration corpus" section for the operational how-to.
@@ -14,9 +14,9 @@ Per [ADR-0018 consequences](../../docs/adr/0018-fraud-detection-verification-and
Research anchor: `.scratch/alpha-hardening/research-verifiable-inference.md` §8 layer 3 — "collect this first — run identical jobs across the current node fleet to measure the honest divergence envelope before setting thresholds." Research anchor: `.scratch/alpha-hardening/research-verifiable-inference.md` §8 layer 3 — "collect this first — run identical jobs across the current node fleet to measure the honest divergence envelope before setting thresholds."
**Launch context (why this is buildable now, not a research project):** first-launch nodes are hired VPS/VPC hosts under our own direct control (test infrastructure we pay for, not third-party volunteers) — not a long-term topology, but risk-free for calibration purposes since there's no external party to dispute a bad reading. Friends are client-side users of the API in this phase, not node operators. Run the calibration pass against this small, fully-controlled fleet first; hired hosts stay stake-free until it's done, then move to real staking once thresholds derive from their own hardware. **Launch context (why this is buildable now, not a research project):** first-launch nodes are hired VPS/VPC hosts under our own direct control (test infrastructure we pay for, not third-party volunteers) — not a long-term topology, but risk-free for calibration purposes since there's no external party to dispute a bad reading. Friends are client-side users of the API in this phase, not node operators. Run the calibration pass against this small, fully-controlled fleet first; hired hosts stay on probation (no upfront stake) until it's done, then move to paid USDT serving once thresholds derive from their own hardware.
**Current gap (confirmed 2026-07-06 by code read):** none of the three pieces below exist yet. **Current gap (historical — closed 2026-07-06):** the three engineering pieces below were missing when this issue was filed; all are now implemented and unit-tested. Remaining work is the human calibration run on the live hired-VPS fleet.
1. `verify_activation_proofs()` (`packages/validator/meshnet_validator/audit.py:94-127`) returns a **plain bool** — no raw TOPLOC divergence/distance value is ever computed or surfaced. Every "done" fraud-detection issue (0610) currently runs on a guessed threshold baked into that bool, not a calibrated one. 1. `verify_activation_proofs()` (`packages/validator/meshnet_validator/audit.py:94-127`) returns a **plain bool** — no raw TOPLOC divergence/distance value is ever computed or surfaced. Every "done" fraud-detection issue (0610) currently runs on a guessed threshold baked into that bool, not a calibrated one.
2. Fleet dispatch exists but is the wrong shape: `_handle_benchmark_hop_penalty` / `_handle_benchmark_results` (`packages/tracker/meshnet_tracker/server.py:2998-3104`, from the old US-030 latency work) targets pinned 13-node *routes* and measures latency, not TOPLOC divergence across *every* registered node. 2. Fleet dispatch exists but is the wrong shape: `_handle_benchmark_hop_penalty` / `_handle_benchmark_results` (`packages/tracker/meshnet_tracker/server.py:2998-3104`, from the old US-030 latency work) targets pinned 13-node *routes* and measures latency, not TOPLOC divergence across *every* registered node.
@@ -36,7 +36,7 @@ Research anchor: `.scratch/alpha-hardening/research-verifiable-inference.md` §8
- [ ] Threshold constants in validator config derived from corpus, not guessed — mechanically ready (`envelope()` returns them) but depends on the real corpus above; not yet wired into `ToplocAuditConfig` as enforced thresholds (deliberately — enforcing unvalidated thresholds would be worse than today's guessed bool). - [ ] Threshold constants in validator config derived from corpus, not guessed — mechanically ready (`envelope()` returns them) but depends on the real corpus above; not yet wired into `ToplocAuditConfig` as enforced thresholds (deliberately — enforcing unvalidated thresholds would be worse than today's guessed bool).
- [ ] False-positive rate estimate documented at chosen thresholds — `envelope()` returns `estimated_false_positive_rate` (in-sample: fraction of the recorded corpus the recommended thresholds would themselves flag); needs the real corpus to be a meaningful number, and should be written up in the runbook once collected. - [ ] False-positive rate estimate documented at chosen thresholds — `envelope()` returns `estimated_false_positive_rate` (in-sample: fraction of the recorded corpus the recommended thresholds would themselves flag); needs the real corpus to be a meaningful number, and should be written up in the runbook once collected.
- [x] README / runbook cross-link: **do not enable production audits** until this issue closes — `packages/validator/README.md` "TOPLOC audit contract" section, updated with the full operational how-to. - [x] README / runbook cross-link: **do not enable production audits** until this issue closes — `packages/validator/README.md` "TOPLOC audit contract" section, updated with the full operational how-to.
- [x] Note in the runbook that this alpha corpus must be re-run once the fleet grows beyond the hired-VPS set (different hardware mix invalidates the envelope) — same README section. - [x] Note in the runbook that this alpha corpus must be re-run once the fleet grows beyond the hired-VPS set (different hardware mix invalidates the envelope) — same README section; [runbook 04](../runbooks/04-toploc-calibration-run.md).
## ADR links ## ADR links

View File

@@ -440,12 +440,12 @@
"Run relevant pytest tests; run the full suite when practical or document why not" "Run relevant pytest tests; run the full suite when practical or document why not"
], ],
"priority": 21, "priority": 21,
"passes": true, "passes": false,
"notes": "Source issue: .scratch/alpha-hardening/issues/21-honest-noise-calibration-corpus.md. BLOCKS ALPHA RELEASE (real-money friends+hired-VPS launch) — rescoped 2026-07-06, no longer a Ralph-skip.", "notes": "Source issue: .scratch/alpha-hardening/issues/21-honest-noise-calibration-corpus.md. BLOCKS ALPHA RELEASE (real-money mainnet USDT). Operator runbook: .scratch/alpha-hardening/runbooks/04-toploc-calibration-run.md",
"dependsOn": [ "dependsOn": [
"AH-006" "AH-006"
], ],
"completionNotes": "Engineering complete and unit-tested (validator audit.py detailed-verify aggregation, tracker calibration.py corpus store, calibration dispatch endpoints). Marked ready-for-human, not done: real corpus collection against the live hired-VPS fleet, and the threshold/FPR write-up that depends on its output, need a human operator — see .ralph-tui/progress.md and packages/validator/README.md." "completionNotes": "Engineering complete and unit-tested. Remaining: human runs POST /v1/calibration/toploc/run on live hired-VPS fleet, records envelope/FPR, wires thresholds — see runbook 04 and packages/validator/README.md."
}, },
{ {
"id": "AH-022", "id": "AH-022",

View File

@@ -0,0 +1,70 @@
# Runbook 04 — Honest-noise TOPLOC calibration (issue 21)
**Status:** engineering complete; **operator action required** before production audit thresholds.
**Blocks:** enabling calibrated TOPLOC thresholds on a mainnet / friends-test fleet (issue 21, ADR-0018).
## When to run
- Before first real-money traffic with audit enforcement enabled.
- Again whenever the fleets **hardware mix** changes materially (new GPU generation, CPU-only nodes added, precision/recipe change per model).
Alpha exception: with a **small hired-VPS-only** fleet, `gate_status.ready` may mean “covers every node we operate today” (`--toploc-calibration-gate-min-hardware-profiles 1`).
## Prerequisites
- Tracker running with billing + registry + `--toploc-calibration-db PATH` (or default under tracker cwd).
- At least one **solo-capable** node per hardware profile you want in the corpus (full model coverage — partial shards are skipped).
- Admin or validator credentials (`Authorization` header or validator service token per ADR-0017).
- Reference validator can replay the fixed calibration prompt (same model/seed as dispatch uses).
## Steps
1. **Register the fleet** — all nodes you intend to pay on mainnet should be up, admitted (NCA when enabled), and solo-serving the calibration model.
2. **Dispatch the job** (admin/validator only):
```bash
curl -X POST "https://<tracker>/v1/calibration/toploc/run" \
-H "Authorization: Bearer <admin-or-validator-token>" \
-H "Content-Type: application/json" \
-d '{}'
```
Partial-shard nodes appear under `skipped_partial_shard_node_ids`. Per-node failures appear under `skipped` with reasons.
3. **Wait for completion** — watch tracker logs and node consoles until every solo-capable node has a row in the corpus.
4. **Fetch results**:
```bash
curl "https://<tracker>/v1/calibration/toploc/results" \
-H "Authorization: Bearer <admin-or-validator-token>"
```
Record:
- `envelope` — p99 metrics + 20% safety margin (recommended tolerances).
- `gate_status.ready` and `gate_status.hardware_profiles`.
- `estimated_false_positive_rate` (in-sample sanity check only).
5. **Write up thresholds** — paste envelope values into operator notes / issue 21 comment. Do **not** wire into production `ToplocAuditConfig` until you have reviewed FPR on this fleet.
6. **Mark issue 21 done** — when corpus covers the launch fleet and thresholds are documented.
## Two-wallet / minimal pilot variant
If your “fleet” is one node machine + one client:
- Run calibration against the **node** profile only (one hardware row is enough for `gate_status` with min profiles = 1).
- Client wallet is irrelevant to calibration — it never serves inference.
## Do not
- Enable stricter production audit thresholds before this completes.
- Reuse a corpus collected on devnet/mock hardware for a different mainnet GPU mix without re-running.
## References
- Issue: `.scratch/alpha-hardening/issues/21-honest-noise-calibration-corpus.md`
- Code: `packages/tracker/meshnet_tracker/calibration.py`, `POST /v1/calibration/toploc/run`
- Validator: `packages/validator/README.md` — TOPLOC audit contract

View File

@@ -12,4 +12,10 @@ Provide an opt-in, admin-only tracker Dashboard Testing tab that dynamically dis
- One active run. - One active run.
- Real inference stays separately environment-gated and excluded from default suites. - Real inference stays separately environment-gated and excluded from default suites.
## Operator workflow
See [`docs/dev/dashboard-test-runner.md`](../../docs/dev/dashboard-test-runner.md)
for launch configuration, default safe suites vs the gated real-inference suite,
and required environment variables.
See `prd.json` for executable Ralph user stories and acceptance criteria. See `prd.json` for executable Ralph user stories and acceptance criteria.

View File

@@ -51,15 +51,16 @@
"uv run pytest tests/test_dashboard.py tests/test_dynamic_routing.py -q passes." "uv run pytest tests/test_dashboard.py tests/test_dynamic_routing.py -q passes."
], ],
"priority": 3, "priority": 3,
"passes": false, "passes": true,
"notes": "Do not reintroduce --enable-test-runner without implementing its CLI argument in US-001.", "notes": "Do not reintroduce --enable-test-runner without implementing its CLI argument in US-001.",
"dependsOn": [ "dependsOn": [
"US-001", "US-001",
"US-002" "US-002"
] ],
"completionNotes": "Completed by agent"
} }
], ],
"metadata": { "metadata": {
"updatedAt": "2026-07-11T17:02:30.520Z" "updatedAt": "2026-07-12T01:58:06.286Z"
} }
} }

View File

@@ -9,7 +9,7 @@ Before changing code, every Ralph agent must:
1. Read this file completely. 1. Read this file completely.
2. Read the selected issue under `.scratch/distributed-gguf-runtime/issues/`. 2. Read the selected issue under `.scratch/distributed-gguf-runtime/issues/`.
3. Read `.scratch/distributed-gguf-runtime/ADR-0020-distributed-gguf-runtime.md` and the relevant part of `architecture.md`. 3. Read `docs/adr/0024-distributed-gguf-runtime.md` and the relevant part of `architecture.md`.
4. Read `.claude/memory/MEMORY.md` and root `CONTEXT.md` for current project vocabulary and constraints. 4. Read `.claude/memory/MEMORY.md` and root `CONTEXT.md` for current project vocabulary and constraints.
5. Inspect the current implementation and tests; do not assume historical scratch text describes live code. 5. Inspect the current implementation and tests; do not assume historical scratch text describes live code.
6. Read the evidence/handoff directories for every declared dependency. 6. Read the evidence/handoff directories for every declared dependency.
@@ -296,7 +296,7 @@ Active decisions:
- `.scratch/distributed-gguf-runtime/README.md` - `.scratch/distributed-gguf-runtime/README.md`
- `.scratch/distributed-gguf-runtime/implementation-strategy.md` - `.scratch/distributed-gguf-runtime/implementation-strategy.md`
- `.scratch/distributed-gguf-runtime/architecture.md` - `.scratch/distributed-gguf-runtime/architecture.md`
- `.scratch/distributed-gguf-runtime/ADR-0020-distributed-gguf-runtime.md` - `docs/adr/0024-distributed-gguf-runtime.md`
- `.scratch/distributed-gguf-runtime/PRD.md` - `.scratch/distributed-gguf-runtime/PRD.md`
- `.scratch/distributed-gguf-runtime/prd.json` - `.scratch/distributed-gguf-runtime/prd.json`

View File

@@ -25,7 +25,7 @@ Transformers/safetensors remains the correctness baseline. vLLM remains an optio
- [Current architecture](architecture.md) - [Current architecture](architecture.md)
- [PRD](PRD.md) - [PRD](PRD.md)
- [Ralph backlog](prd.json) - [Ralph backlog](prd.json)
- [ADR-0020](ADR-0020-distributed-gguf-runtime.md) - [ADR-0024](../../docs/adr/0024-distributed-gguf-runtime.md)
- [Milestones](milestones.md) - [Milestones](milestones.md)
- [Issues](issues/) - [Issues](issues/)
- [Distributed GGUF research](../../docs/research/distributed-gguf-landscape.md) - [Distributed GGUF research](../../docs/research/distributed-gguf-landscape.md)

View File

@@ -1,6 +1,6 @@
# Distributed GGUF Decision Framework # Distributed GGUF Decision Framework
> **Superseded for active implementation decisions.** The grill was resolved on 2026-07-13. Use [implementation-strategy.md](implementation-strategy.md), [architecture.md](architecture.md), [ADR-0020](ADR-0020-distributed-gguf-runtime.md), and [prd.json](prd.json). This file remains as historical decision rationale. > **Superseded for active implementation decisions.** The grill was resolved on 2026-07-13. Use [implementation-strategy.md](implementation-strategy.md), [architecture.md](architecture.md), [ADR-0024](../../docs/adr/0024-distributed-gguf-runtime.md), and [prd.json](prd.json). This file remains as historical decision rationale.
This framework is for grilling open decisions. It keeps decisions tied to project vocabulary and implementation gates instead of vague "distributed inference" language. This framework is for grilling open decisions. It keeps decisions tied to project vocabulary and implementation gates instead of vague "distributed inference" language.

View File

@@ -56,4 +56,4 @@ As a runtime engineer, I need a controlled baseline so that GGUF work proceeds f
- [PRD](../PRD.md) - [PRD](../PRD.md)
- [Implementation strategy](../implementation-strategy.md) - [Implementation strategy](../implementation-strategy.md)
- [Current architecture](../architecture.md) - [Current architecture](../architecture.md)
- [Architecture decision](../ADR-0020-distributed-gguf-runtime.md) - [Architecture decision](../../docs/adr/0024-distributed-gguf-runtime.md)

View File

@@ -56,4 +56,4 @@ As a node developer, I need a battle-proven streaming protocol so that Python an
- [PRD](../PRD.md) - [PRD](../PRD.md)
- [Implementation strategy](../implementation-strategy.md) - [Implementation strategy](../implementation-strategy.md)
- [Current architecture](../architecture.md) - [Current architecture](../architecture.md)
- [Architecture decision](../ADR-0020-distributed-gguf-runtime.md) - [Architecture decision](../../docs/adr/0024-distributed-gguf-runtime.md)

View File

@@ -54,4 +54,4 @@ As the Tracker, I need exact compatibility identity so that only numerically and
- [PRD](../PRD.md) - [PRD](../PRD.md)
- [Implementation strategy](../implementation-strategy.md) - [Implementation strategy](../implementation-strategy.md)
- [Current architecture](../architecture.md) - [Current architecture](../architecture.md)
- [Architecture decision](../ADR-0020-distributed-gguf-runtime.md) - [Architecture decision](../../docs/adr/0024-distributed-gguf-runtime.md)

View File

@@ -58,4 +58,4 @@ As a maintainer, I need a small auditable fork boundary so that upstream updates
- [PRD](../PRD.md) - [PRD](../PRD.md)
- [Implementation strategy](../implementation-strategy.md) - [Implementation strategy](../implementation-strategy.md)
- [Current architecture](../architecture.md) - [Current architecture](../architecture.md)
- [Architecture decision](../ADR-0020-distributed-gguf-runtime.md) - [Architecture decision](../../docs/adr/0024-distributed-gguf-runtime.md)

View File

@@ -58,4 +58,4 @@ As a node, I need to map only my assigned dense-Llama Shard so that aggregate co
- [PRD](../PRD.md) - [PRD](../PRD.md)
- [Implementation strategy](../implementation-strategy.md) - [Implementation strategy](../implementation-strategy.md)
- [Current architecture](../architecture.md) - [Current architecture](../architecture.md)
- [Architecture decision](../ADR-0020-distributed-gguf-runtime.md) - [Architecture decision](../../docs/adr/0024-distributed-gguf-runtime.md)

View File

@@ -58,4 +58,4 @@ As a Shard, I need to consume and emit the correct transformer boundary state so
- [PRD](../PRD.md) - [PRD](../PRD.md)
- [Implementation strategy](../implementation-strategy.md) - [Implementation strategy](../implementation-strategy.md)
- [Current architecture](../architecture.md) - [Current architecture](../architecture.md)
- [Architecture decision](../ADR-0020-distributed-gguf-runtime.md) - [Architecture decision](../../docs/adr/0024-distributed-gguf-runtime.md)

View File

@@ -57,4 +57,4 @@ As a client, I need concurrent Route Sessions to retain independent per-Shard ca
- [PRD](../PRD.md) - [PRD](../PRD.md)
- [Implementation strategy](../implementation-strategy.md) - [Implementation strategy](../implementation-strategy.md)
- [Current architecture](../architecture.md) - [Current architecture](../architecture.md)
- [Architecture decision](../ADR-0020-distributed-gguf-runtime.md) - [Architecture decision](../../docs/adr/0024-distributed-gguf-runtime.md)

View File

@@ -62,4 +62,4 @@ As a node runtime, I need one supervised native process so that llama.cpp intern
- [PRD](../PRD.md) - [PRD](../PRD.md)
- [Implementation strategy](../implementation-strategy.md) - [Implementation strategy](../implementation-strategy.md)
- [Current architecture](../architecture.md) - [Current architecture](../architecture.md)
- [Architecture decision](../ADR-0020-distributed-gguf-runtime.md) - [Architecture decision](../../docs/adr/0024-distributed-gguf-runtime.md)

View File

@@ -58,4 +58,4 @@ As the existing node service, I need a GGUF Shard backend adapter so that the Tr
- [PRD](../PRD.md) - [PRD](../PRD.md)
- [Implementation strategy](../implementation-strategy.md) - [Implementation strategy](../implementation-strategy.md)
- [Current architecture](../architecture.md) - [Current architecture](../architecture.md)
- [Architecture decision](../ADR-0020-distributed-gguf-runtime.md) - [Architecture decision](../../docs/adr/0024-distributed-gguf-runtime.md)

View File

@@ -59,4 +59,4 @@ As a release engineer, I need real local distributed parity before involving net
- [PRD](../PRD.md) - [PRD](../PRD.md)
- [Implementation strategy](../implementation-strategy.md) - [Implementation strategy](../implementation-strategy.md)
- [Current architecture](../architecture.md) - [Current architecture](../architecture.md)
- [Architecture decision](../ADR-0020-distributed-gguf-runtime.md) - [Architecture decision](../../docs/adr/0024-distributed-gguf-runtime.md)

View File

@@ -59,4 +59,4 @@ As a consumer-hardware operator, I need two physical machines to execute one GGU
- [PRD](../PRD.md) - [PRD](../PRD.md)
- [Implementation strategy](../implementation-strategy.md) - [Implementation strategy](../implementation-strategy.md)
- [Current architecture](../architecture.md) - [Current architecture](../architecture.md)
- [Architecture decision](../ADR-0020-distributed-gguf-runtime.md) - [Architecture decision](../../docs/adr/0024-distributed-gguf-runtime.md)

View File

@@ -60,4 +60,4 @@ As a node operator, I need active sessions batched safely so that concurrency in
- [PRD](../PRD.md) - [PRD](../PRD.md)
- [Implementation strategy](../implementation-strategy.md) - [Implementation strategy](../implementation-strategy.md)
- [Current architecture](../architecture.md) - [Current architecture](../architecture.md)
- [Architecture decision](../ADR-0020-distributed-gguf-runtime.md) - [Architecture decision](../../docs/adr/0024-distributed-gguf-runtime.md)

View File

@@ -59,4 +59,4 @@ As a client, I need failures to be bounded and explicit so that distributed spee
- [PRD](../PRD.md) - [PRD](../PRD.md)
- [Implementation strategy](../implementation-strategy.md) - [Implementation strategy](../implementation-strategy.md)
- [Current architecture](../architecture.md) - [Current architecture](../architecture.md)
- [Architecture decision](../ADR-0020-distributed-gguf-runtime.md) - [Architecture decision](../../docs/adr/0024-distributed-gguf-runtime.md)

View File

@@ -62,4 +62,4 @@ As the product owner, I need an end-to-end comparison so that the native runtime
- [PRD](../PRD.md) - [PRD](../PRD.md)
- [Implementation strategy](../implementation-strategy.md) - [Implementation strategy](../implementation-strategy.md)
- [Current architecture](../architecture.md) - [Current architecture](../architecture.md)
- [Architecture decision](../ADR-0020-distributed-gguf-runtime.md) - [Architecture decision](../../docs/adr/0024-distributed-gguf-runtime.md)

View File

@@ -58,4 +58,4 @@ As a client seeking top models, I need a separately certified MoE-capable archit
- [PRD](../PRD.md) - [PRD](../PRD.md)
- [Implementation strategy](../implementation-strategy.md) - [Implementation strategy](../implementation-strategy.md)
- [Current architecture](../architecture.md) - [Current architecture](../architecture.md)
- [Architecture decision](../ADR-0020-distributed-gguf-runtime.md) - [Architecture decision](../../docs/adr/0024-distributed-gguf-runtime.md)

View File

@@ -57,4 +57,4 @@ As a maintainer, I need narrow upstreamable proposals so that our patch burden c
- [PRD](../PRD.md) - [PRD](../PRD.md)
- [Implementation strategy](../implementation-strategy.md) - [Implementation strategy](../implementation-strategy.md)
- [Current architecture](../architecture.md) - [Current architecture](../architecture.md)
- [Architecture decision](../ADR-0020-distributed-gguf-runtime.md) - [Architecture decision](../../docs/adr/0024-distributed-gguf-runtime.md)

View File

@@ -1,4 +1,4 @@
Status: ready-for-agent Status: done (2026-07-14)
# 01 — Baseline and profiling harness # 01 — Baseline and profiling harness
@@ -12,16 +12,15 @@ sizes and connection counts without requiring a real model or external host.
## Acceptance criteria ## Acceptance criteria
- [ ] The harness runs a fixed prompt and fixed generated-token count through a - [x] The harness runs a fixed prompt and fixed generated-token count through a
two-node route in direct and relay modes. two-node route in direct and relay modes.
- [ ] It reports p50/p95 per-token latency, per-hop latency, payload bytes, - [x] It reports p50/p95 per-token latency, per-hop latency, payload bytes,
compression ratio, connection attempts, and queue wait. compression ratio, connection attempts, and queue wait.
- [ ] It distinguishes prefill from decode and cached from stateless mode. - [x] It distinguishes prefill from decode and cached from stateless mode.
- [ ] It emits machine-readable JSON suitable for CI artifacts and a concise - [x] It emits machine-readable JSON suitable for CI artifacts and a concise
human-readable summary. human-readable summary.
- [ ] A test fixture can assert connection attempts and output token identity. - [x] A test fixture can assert connection attempts and output token identity.
## Blocked by ## Blocked by
None - can start immediately. None - completed. Verified with `PYTHONPATH=packages/node pytest -q tests/test_route_session_benchmark.py` (7 passed).

View File

@@ -15,9 +15,10 @@
"Can assert connection count and output token identity" "Can assert connection count and output token identity"
], ],
"priority": 1, "priority": 1,
"passes": false, "passes": true,
"notes": "Source issue: .scratch/distributed-inference-performance/issues/01-baseline-profiling-harness.md", "notes": "Source issue: .scratch/distributed-inference-performance/issues/01-baseline-profiling-harness.md",
"dependsOn": [] "dependsOn": [],
"completionNotes": "Completed by agent"
}, },
{ {
"id": "DIP-002", "id": "DIP-002",
@@ -31,9 +32,12 @@
"Tests cover binary, JSON, timeout, disconnect, cancellation, and cleanup" "Tests cover binary, JSON, timeout, disconnect, cancellation, and cleanup"
], ],
"priority": 2, "priority": 2,
"passes": false, "passes": true,
"notes": "Source issue: .scratch/distributed-inference-performance/issues/02-relay-session-compatibility.md", "notes": "Source issue: .scratch/distributed-inference-performance/issues/02-relay-session-compatibility.md",
"dependsOn": ["DIP-001"] "dependsOn": [
"DIP-001"
],
"completionNotes": "Completed by agent"
}, },
{ {
"id": "DIP-003", "id": "DIP-003",
@@ -47,9 +51,12 @@
"Benchmark shows healthy-session connection count independent of token count" "Benchmark shows healthy-session connection count independent of token count"
], ],
"priority": 3, "priority": 3,
"passes": false, "passes": true,
"notes": "Source issue: .scratch/distributed-inference-performance/issues/03-http-keepalive.md", "notes": "Source issue: .scratch/distributed-inference-performance/issues/03-http-keepalive.md",
"dependsOn": ["DIP-001"] "dependsOn": [
"DIP-001"
],
"completionNotes": "Completed by agent"
}, },
{ {
"id": "DIP-004", "id": "DIP-004",
@@ -63,9 +70,12 @@
"Tests verify cadence and cleanup" "Tests verify cadence and cleanup"
], ],
"priority": 4, "priority": 4,
"passes": false, "passes": true,
"notes": "Source issue: .scratch/distributed-inference-performance/issues/04-seam-telemetry.md", "notes": "Source issue: .scratch/distributed-inference-performance/issues/04-seam-telemetry.md",
"dependsOn": ["DIP-001"] "dependsOn": [
"DIP-001"
],
"completionNotes": "Completed by agent"
}, },
{ {
"id": "DIP-005", "id": "DIP-005",
@@ -79,9 +89,12 @@
"Tests cover compressible, incompressible, threshold, malformed, and legacy bodies" "Tests cover compressible, incompressible, threshold, malformed, and legacy bodies"
], ],
"priority": 5, "priority": 5,
"passes": false, "passes": true,
"notes": "Source issue: .scratch/distributed-inference-performance/issues/05-adaptive-compression.md", "notes": "Source issue: .scratch/distributed-inference-performance/issues/05-adaptive-compression.md",
"dependsOn": ["DIP-001"] "dependsOn": [
"DIP-001"
],
"completionNotes": "Completed by agent"
}, },
{ {
"id": "DIP-006", "id": "DIP-006",
@@ -95,9 +108,12 @@
"Wire and token-output regression tests pass" "Wire and token-output regression tests pass"
], ],
"priority": 6, "priority": 6,
"passes": false, "passes": true,
"notes": "Source issue: .scratch/distributed-inference-performance/issues/06-activation-framing-copies.md", "notes": "Source issue: .scratch/distributed-inference-performance/issues/06-activation-framing-copies.md",
"dependsOn": ["DIP-001"] "dependsOn": [
"DIP-001"
],
"completionNotes": "Completed by agent"
}, },
{ {
"id": "DIP-007", "id": "DIP-007",
@@ -111,9 +127,13 @@
"Tests cover chunking, slow consumers, failure, and legacy peers" "Tests cover chunking, slow consumers, failure, and legacy peers"
], ],
"priority": 7, "priority": 7,
"passes": false, "passes": true,
"notes": "Source issue: .scratch/distributed-inference-performance/issues/07-prefill-backpressure.md", "notes": "Source issue: .scratch/distributed-inference-performance/issues/07-prefill-backpressure.md",
"dependsOn": ["DIP-001", "DIP-004"] "dependsOn": [
"DIP-001",
"DIP-004"
],
"completionNotes": "Completed by agent"
}, },
{ {
"id": "DIP-008", "id": "DIP-008",
@@ -127,9 +147,20 @@
"Gate verifies token identity, session stability, and resource cleanup" "Gate verifies token identity, session stability, and resource cleanup"
], ],
"priority": 8, "priority": 8,
"passes": false, "passes": true,
"notes": "Source issue: .scratch/distributed-inference-performance/issues/08-end-to-end-performance-gate.md", "notes": "Source issue: .scratch/distributed-inference-performance/issues/08-end-to-end-performance-gate.md",
"dependsOn": ["DIP-002", "DIP-003", "DIP-004", "DIP-005", "DIP-006", "DIP-007"] "dependsOn": [
"DIP-002",
"DIP-003",
"DIP-004",
"DIP-005",
"DIP-006",
"DIP-007"
],
"completionNotes": "Completed by agent"
} }
] ],
"metadata": {
"updatedAt": "2026-07-12T02:35:28.752Z"
}
} }

View File

@@ -71,6 +71,8 @@ As an operator and release engineer, I need clear doctor output and opt-in hardw
Add a small generic capability domain object in the node package. `doctor` loads the requested generic model path through the same backend startup uses, executes a bounded real forward at the assigned Shard, and emits the report. Startup gates routable registration on the successful report. Registration carries validated capabilities; the tracker persists/exposes them and filters route candidates at the model/shard/recipe seam. Add a small generic capability domain object in the node package. `doctor` loads the requested generic model path through the same backend startup uses, executes a bounded real forward at the assigned Shard, and emits the report. Startup gates routable registration on the successful report. Registration carries validated capabilities; the tracker persists/exposes them and filters route candidates at the model/shard/recipe seam.
**Assignment ownership:** NCA validates whatever the node loads; it does not assign models. Pinned vs tracker-managed assignment rules are in [ADR-0026](../../docs/adr/0026-node-assignment-ownership-and-managed-placement.md). Demand-driven managed placement (Qwen scratch PRD) may only consume spare capacity; admission applies equally to pinned and managed loads.
The future signed-update contract is represented only by a local manifest version and generic schema in P0. A future Tracker Model Artifact Manifest may be signed data, but Node executable behavior remains supplied by signed Node releases. The future signed-update contract is represented only by a local manifest version and generic schema in P0. A future Tracker Model Artifact Manifest may be signed data, but Node executable behavior remains supplied by signed Node releases.
## Success measures ## Success measures

View File

@@ -7,6 +7,7 @@ This P0 makes a Node prove it can serve its selected Model Artifact and Shard be
## Locked decisions ## Locked decisions
- A Node explicitly asked to serve a Model Preset fails closed when no validated recipe can execute it; it must not register as ready or accept paid inference. - A Node explicitly asked to serve a Model Preset fails closed when no validated recipe can execute it; it must not register as ready or accept paid inference.
- **Assignment ownership:** startup/`--model` loads are **pinned**; tracker-managed demand placement (Qwen US-050) may use **spare capacity only** — [ADR-0026](../../docs/adr/0026-node-assignment-ownership-and-managed-placement.md).
- Default validation covers the selected model/shard only. `meshnet-node doctor --all-recipes` is reserved for support and CI. - Default validation covers the selected model/shard only. `meshnet-node doctor --all-recipes` is reserved for support and CI.
- A Model Preset may have multiple named recipes. Each independently proves a real forward; the Tracker schedules only validated recipes while considering measured performance. - A Model Preset may have multiple named recipes. Each independently proves a real forward; the Tracker schedules only validated recipes while considering measured performance.
- Compatibility schemas are generic. A future Tracker may publish signed, data-only Model Artifact Manifests, but executable recipes arrive only through signed Node releases. - Compatibility schemas are generic. A future Tracker may publish signed, data-only Model Artifact Manifests, but executable recipes arrive only through signed Node releases.

View File

@@ -35,11 +35,12 @@
"Full pytest passes or an exact unrelated blocker is recorded" "Full pytest passes or an exact unrelated blocker is recorded"
], ],
"priority": 2, "priority": 2,
"passes": false, "passes": true,
"notes": "Source issue: .scratch/node-capability-admission/issues/02-doctor-real-forward.md", "notes": "Source issue: .scratch/node-capability-admission/issues/02-doctor-real-forward.md",
"dependsOn": [ "dependsOn": [
"NCA-001" "NCA-001"
] ],
"completionNotes": "Completed by agent"
}, },
{ {
"id": "NCA-003", "id": "NCA-003",
@@ -54,12 +55,13 @@
"Full pytest passes or an exact unrelated blocker is recorded" "Full pytest passes or an exact unrelated blocker is recorded"
], ],
"priority": 3, "priority": 3,
"passes": false, "passes": true,
"notes": "Source issue: .scratch/node-capability-admission/issues/03-fail-closed-startup-admission.md", "notes": "Source issue: .scratch/node-capability-admission/issues/03-fail-closed-startup-admission.md",
"dependsOn": [ "dependsOn": [
"NCA-001", "NCA-001",
"NCA-002" "NCA-002"
] ],
"completionNotes": "Completed by agent"
}, },
{ {
"id": "NCA-004", "id": "NCA-004",
@@ -76,12 +78,13 @@
"Full pytest passes or an exact unrelated blocker is recorded" "Full pytest passes or an exact unrelated blocker is recorded"
], ],
"priority": 4, "priority": 4,
"passes": false, "passes": true,
"notes": "Source issue: .scratch/node-capability-admission/issues/04-tracker-validated-capability-routing.md", "notes": "Source issue: .scratch/node-capability-admission/issues/04-tracker-validated-capability-routing.md",
"dependsOn": [ "dependsOn": [
"NCA-001", "NCA-001",
"NCA-003" "NCA-003"
] ],
"completionNotes": "Completed by agent"
}, },
{ {
"id": "NCA-005", "id": "NCA-005",
@@ -96,15 +99,16 @@
"Full pytest passes or an exact unrelated blocker is recorded" "Full pytest passes or an exact unrelated blocker is recorded"
], ],
"priority": 5, "priority": 5,
"passes": false, "passes": true,
"notes": "Source issue: .scratch/node-capability-admission/issues/05-docs-hardware-lane-contract.md", "notes": "Source issue: .scratch/node-capability-admission/issues/05-docs-hardware-lane-contract.md",
"dependsOn": [ "dependsOn": [
"NCA-002", "NCA-002",
"NCA-004" "NCA-004"
] ],
"completionNotes": "Completed by agent"
} }
], ],
"metadata": { "metadata": {
"updatedAt": "2026-07-11T19:16:52.768Z" "updatedAt": "2026-07-12T01:54:03.030Z"
} }
} }

View File

@@ -46,13 +46,12 @@ model rather than waiting for an operator to request a load.
## Node ownership ## Node ownership
- A startup-assigned `(model, shard range, quantization)` is pinned and never Reconciled with [ADR-0026](../../docs/adr/0026-node-assignment-ownership-and-managed-placement.md) and NCA (ADR-0023):
changed by the tracker.
- Spare capacity on a pinned node, and all capacity on a model-less node, is - A **startup-assigned** `(model, shard range, quantization)` from explicit `--model` or accepted bootstrap assign is **pinned** until the operator restarts.
available for tracker-managed assignments. - **Tracker-managed** assignments (this feature) use only **spare capacity** — model-less nodes or (future, US-048) unused shard slots — and are marked `managed: true`.
- Tracker-added assignments are explicitly marked managed and may be moved or - The tracker may move or remove managed assignments under the safety policy below; it must not retarget a pinned serving assignment to satisfy demand.
removed by the tracker under the safety policy. Runtime UI controls are a - Every assignment, pinned or managed, must pass NCA `doctor` before becoming routable when admission is enabled.
later feature.
## Pricing ## Pricing

10
CONTEXT-MAP.md Normal file
View File

@@ -0,0 +1,10 @@
# Context map
Multi-context layout is not yet split. Use the root domain vocabulary:
- **[CONTEXT.md](../CONTEXT.md)** — ubiquitous language for the distributed inference network
- **`docs/adr/`** — system-wide architectural decisions
- **`.scratch/<feature>/`** — active feature plans and issues
- **`.claude/memory/MEMORY.md`** — agent session index and current workstreams
Per-context `src/<context>/docs/adr/` ADRs will be added when bounded contexts graduate out of the monorepo packages layout.

View File

@@ -16,12 +16,9 @@
.\.venv\Scripts\meshnet-node.exe start http://192.168.0.179:8081 --model-id Qwen/Qwen2.5-0.5B-Instruct --advertise-host 192.168.0.20 .\.venv\Scripts\meshnet-node.exe start http://192.168.0.179:8081 --model-id Qwen/Qwen2.5-0.5B-Instruct --advertise-host 192.168.0.20
.\.venv\Scripts\meshnet-node.exe start --tracker http://ai.neuron.d-popov.com --model-id Qwen/Qwen2.5-0.5B-Instruct --advertise-host 192.168.0.20 .\.venv\Scripts\meshnet-node.exe start --tracker http://ai.neuron.d-popov.com --model Qwen/Qwen2.5-0.5B-Instruct --advertise-host 192.168.0.20
we .\.venv\Scripts\meshnet-node.exe start ` we .\.venv\Scripts\meshnet-node.exe start --tracker http://192.168.0.179:8081 --model Qwen/Qwen2.5-0.5B-Instruct
--tracker http://192.168.0.179:8081 `
--model Qwen/Qwen2.5-0.5B-Instruct `
--advertise-host 192.168.0.20
# trackers: # trackers:
https://meshnet.2.d-popov.com https://meshnet.2.d-popov.com
https://ai.neuron.d-popov.com https://ai.neuron.d-popov.com

View File

@@ -1,4 +1,4 @@
Status: ready-for-agent Status: done (US-001…US-035 complete; friends-test arc US-036…US-049 in `docs/prd.json`; US-048/050 tracked. See ADRs 00150018, 0023, 00250026.)
# Distributed Inference Network — PRD # Distributed Inference Network — PRD
@@ -8,10 +8,12 @@ Running large language models requires expensive dedicated hardware that most pe
## Solution ## Solution
A volunteer GPU network where anyone can share their GPU by running a single command and immediately start earning tokens. Nodes each load a shard of a large model; a tracker routes inference requests through the optimal chain of nodes whose shards collectively cover all layers. Developers access the network through an OpenAI-compatible API — a one-line change from any existing LLM integration. Clients pay in SOL or USDC; node operators earn our native token. Everything is auto-configured: GPU detection, shard download, wallet creation, and network registration happen automatically on first start. A volunteer GPU network where anyone can share their GPU by running a single command and immediately start earning **USDT**. Nodes each load a shard of a large model; a tracker routes inference requests through the optimal chain of nodes whose shards collectively cover all layers. Developers access the network through an OpenAI-compatible API — a one-line change from any existing LLM integration. Clients pay in **USDT** (alpha: devnet mock-USDT; production: mainnet USDT). Node operators earn USDT payouts from the custodial treasury (ADR-0015); the TAI reward token (ADR-0002) remains deferred. Everything is auto-configured: GPU detection, shard download, wallet creation, and network registration happen automatically on first start.
## User Stories ## User Stories
> **Status (2026-07-13):** Stories below are the original product intent. **Shipped behavior** is in [Implementation Decisions](#implementation-decisions) and ADRs 00150018, 0023, 00250026. Superseded lines are marked inline.
### Node Operator ### Node Operator
1. As a node operator, I want to install the node client with a single command (`pip install meshnet-node`), so that I can start contributing without reading documentation. 1. As a node operator, I want to install the node client with a single command (`pip install meshnet-node`), so that I can start contributing without reading documentation.
@@ -21,10 +23,10 @@ A volunteer GPU network where anyone can share their GPU by running a single com
5. As a node operator, I want my assigned shard to download automatically from HuggingFace on first start, so that I don't have to manually find or download model weights. 5. As a node operator, I want my assigned shard to download automatically from HuggingFace on first start, so that I don't have to manually find or download model weights.
6. As a node operator, I want to seed my shard to other nodes via P2P once I have it, so that new nodes with the same shard assignment don't need to download from HuggingFace. 6. As a node operator, I want to seed my shard to other nodes via P2P once I have it, so that new nodes with the same shard assignment don't need to download from HuggingFace.
7. As a node operator, I want the node client to register with the tracker automatically and begin serving inference requests, so that I start earning as soon as setup is complete. 7. As a node operator, I want the node client to register with the tracker automatically and begin serving inference requests, so that I start earning as soon as setup is complete.
8. As a node operator, I want to see my current node score, shard assignment, and token earnings in the terminal, so that I can verify my node is contributing correctly. 8. As a node operator, I want to see my current node score, shard assignment, and USDT earnings in the terminal, so that I can verify my node is contributing correctly.
9. As a node operator, I want to stake tokens before serving paid inference, so that I have skin in the game and the network can trust my outputs. 9. As a node operator, I want to serve paid inference without upfront stake deposits, with my accrued USDT pending balance as fraud collateral and probation as the anti-sybil cost, so that onboarding stays frictionless. *(Supersedes stake-before-serving; ADR-0015/0018.)*
10. As a node operator, I want my first N jobs to run without earning (probationary period), so that the network can establish trust before paying me. 10. As a node operator, I want my first N jobs to run without earning (probationary period), so that the network can establish trust before paying me.
11. As a node operator, I want to be notified immediately if my stake is slashed due to a fraud detection event, so that I can investigate and fix the issue. 11. As a node operator, I want to be notified when my pending balance is forfeited due to a failed audit, so that I can investigate and fix the issue. *(Supersedes stake slash; ADR-0018 forfeiture.)*
12. As a node operator, I want to receive a strike and a warning before being banned, so that accidental failures don't immediately end my participation. 12. As a node operator, I want to receive a strike and a warning before being banned, so that accidental failures don't immediately end my participation.
13. As a node operator, I want to be automatically reassigned to a different shard when the tracker determines another shard is more in demand, so that my hardware is always optimally used. 13. As a node operator, I want to be automatically reassigned to a different shard when the tracker determines another shard is more in demand, so that my hardware is always optimally used.
14. As a node operator, I want the node client to reconnect automatically if the tracker is temporarily unavailable, so that transient network issues don't stop me from earning. 14. As a node operator, I want the node client to reconnect automatically if the tracker is temporarily unavailable, so that transient network issues don't stop me from earning.
@@ -34,8 +36,8 @@ A volunteer GPU network where anyone can share their GPU by running a single com
### Client Developer ### Client Developer
17. As a client developer, I want to send `POST /v1/chat/completions` requests to the gateway in the same format as the OpenAI API, so that I can switch to the network with a one-line code change. 17. As a client developer, I want to send `POST /v1/chat/completions` requests to the gateway in the same format as the OpenAI API, so that I can switch to the network with a one-line code change.
18. As a client developer, I want to authenticate with an API key funded by SOL or USDC, so that I never need to acquire or hold our native token. 18. As a client developer, I want to authenticate with an API key funded by USDT, so that I never need to acquire or hold our native token. *(ADR-0015.)*
19. As a client developer, I want to top up my API key balance by sending SOL or USDC to a Solana address, so that payment is simple and familiar. 19. As a client developer, I want to top up my API key balance by sending USDT to the treasury Solana address, so that payment is simple and familiar. *(ADR-0015; wallet binding US-039/041.)*
20. As a client developer, I want to see a per-request cost estimate before sending a request, so that I can budget inference costs accurately. 20. As a client developer, I want to see a per-request cost estimate before sending a request, so that I can budget inference costs accurately.
21. As a client developer, I want to receive streaming responses (`text/event-stream`) in OpenAI-compatible format, so that I can build low-latency user experiences. 21. As a client developer, I want to receive streaming responses (`text/event-stream`) in OpenAI-compatible format, so that I can build low-latency user experiences.
22. As a client developer, I want `GET /v1/models` to return the list of available model presets on the network, so that I know what I can request. 22. As a client developer, I want `GET /v1/models` to return the list of available model presets on the network, so that I know what I can request.
@@ -46,15 +48,15 @@ A volunteer GPU network where anyone can share their GPU by running a single com
### End User (via a client app) ### End User (via a client app)
27. As an end user, I want to buy SOL on any exchange and use it to pay for inference, so that I don't need to understand blockchain technology to use the service. 27. As an end user, I want to buy USDT on an exchange and use it to pay for inference via Solana, so that I don't need deep crypto knowledge to use the service. *(Clients pay USDT; SOL is only for network fees if they self-custody.)*
28. As an end user, I want responses of equivalent quality to centralised providers, so that I don't have to trade quality for cost savings. 28. As an end user, I want responses of equivalent quality to centralised providers, so that I don't have to trade quality for cost savings.
29. As an end user, I want low latency on first token, so that conversational applications feel responsive. 29. As an end user, I want low latency on first token, so that conversational applications feel responsive.
### Validator ### Validator
30. As a validator, I want to automatically re-run a random sample (~5%) of completed inference requests on a reference node, so that I can detect nodes returning fraudulent outputs. 30. As a validator, I want to automatically re-run a random sample (~5%) of completed inference requests on a reference node with TOPLOC activation verification, so that I can detect nodes returning fraudulent outputs. *(ADR-0018.)*
31. As a validator, I want to submit a fraud proof on-chain when a node's output diverges beyond tolerance, so that the slash event is recorded trustlessly. 31. As a validator, I want the tracker to record forfeiture and strikes when an audit fails, so that penalties are applied consistently. *(Supersedes on-chain fraud proof in alpha; ADR-0018.)*
32. As a validator, I want to earn a reward for each successful fraud detection, so that there is an economic incentive to run validation. 32. As a validator, I want economic incentive to run validation, so that fraud detection is not purely altruistic. *(Validator reward share deferred; forfeiture to protocol cut today.)*
### Network (tracker / system) ### Network (tracker / system)
@@ -62,7 +64,7 @@ A volunteer GPU network where anyone can share their GPU by running a single com
34. As the tracker, I want to rebalance shard assignments across nodes when demand for a model preset changes, so that the network always covers the most-requested models. 34. As the tracker, I want to rebalance shard assignments across nodes when demand for a model preset changes, so that the network always covers the most-requested models.
35. As the tracker, I want to instruct a node to download a new shard when no other node covers it, so that model preset coverage is maintained automatically. 35. As the tracker, I want to instruct a node to download a new shard when no other node covers it, so that model preset coverage is maintained automatically.
36. As the tracker, I want to exclude banned wallets from route selection, so that fraudulent nodes cannot serve paid inference. 36. As the tracker, I want to exclude banned wallets from route selection, so that fraudulent nodes cannot serve paid inference.
37. As the tracker, I want to read stake, slash, strike, and ban state exclusively from Solana smart contracts, so that I cannot manipulate payouts even with full control of the routing layer. 37. As the tracker, I want strike and ban state persisted in the registry and enforced on route selection, so that fraudulent wallets cannot serve paid inference. *(Supersedes on-chain-only stake/slash registry; ADR-0018; on-chain deferred per ADR-0007/0015.)*
38. As the network, I want new model presets to be addable by submitting a HuggingFace model ID and shard count, so that the set of available models can grow without code changes. 38. As the network, I want new model presets to be addable by submitting a HuggingFace model ID and shard count, so that the set of available models can grow without code changes.
## Implementation Decisions ## Implementation Decisions
@@ -73,11 +75,11 @@ The codebase is organized as a Python monorepo with the following top-level pack
- `packages/gateway` — OpenAI-compatible HTTP gateway and route orchestration - `packages/gateway` — OpenAI-compatible HTTP gateway and route orchestration
- `packages/tracker` — centralized tracker service (node registry, scoring, route selection) - `packages/tracker` — centralized tracker service (node registry, scoring, route selection)
- `packages/sdk``meshnet` Python SDK wrapping gateway + wallet controls - `packages/sdk``meshnet` Python SDK wrapping gateway + wallet controls
- `packages/contracts` — Solana L2 smart contracts (stake, slash, strike, ban, settlement) - `packages/contracts` — Solana adapter boundary (custodial USDT treasury, local registry prototype)
- `packages/p2p` — P2P gossip layer and shard swarm seeding - `packages/p2p` — P2P gossip layer and shard swarm seeding
### Inference engine (ADR-0001) ### Inference engine (ADR-0001; native GGUF path ADR-0024)
PyTorch with a Petals-style shard pipeline. Each node independently loads its assigned shard from local disk. At inference time, only activation tensors (~8 KB per layer boundary per token) travel between nodes — no model weights cross the network during serving. PyTorch with a Petals-style shard pipeline remains the current production backend. A benchmark-gated llama.cpp/GGUF native path is planned in ADR-0024. Each node independently loads its assigned shard from local disk. At inference time, only activation tensors (~8 KB per layer boundary per token) travel between nodes — no model weights cross the network during serving.
### Inference route execution ### Inference route execution
The gateway receives a client request, asks the tracker for an inference route (ordered list of node endpoints covering all layers), opens a persistent TCP session to the first node in the route, streams activation tensors through each node in sequence, and returns the final logits as a streaming chat completion response. The gateway receives a client request, asks the tracker for an inference route (ordered list of node endpoints covering all layers), opens a persistent TCP session to the first node in the route, streams activation tensors through each node in sequence, and returns the final logits as a streaming chat completion response.
@@ -91,14 +93,14 @@ The gateway receives a client request, asks the tracker for an inference route (
6. Register with tracker (wallet, hardware profile, shard, endpoint) 6. Register with tracker (wallet, hardware profile, shard, endpoint)
7. Begin accepting inference connections 7. Begin accepting inference connections
### Payment flow ### Payment flow (ADR-0015 supersedes ADR-0002 settlement mechanics)
Clients pre-fund an API key with SOL/USDC. The gateway records per-request compute attribution. A settlement transaction runs on Solana L2 at the end of each epoch: client balance is debited, node operators receive our native token proportional to layers served, validators receive a reward share. Solana contracts are the authoritative source for all stake, slash, strike, and ban state (ADR-0002). Clients pre-fund an API key with USDT. The tracker meters each request against the off-chain ledger. Periodic settlement batches USDT payouts from the custodial treasury to node operators proportional to work units (default: every 24 h or when pending ≥ 5 USDT). Fraud penalties forfeit pending balance (ADR-0018); strike/ban state persists in the tracker registry. TAI reward accrual is deferred — see ADR-0025 for reserved-mint / off-chain phase B/C; ADR-0002 roadmap for public listing.
### Fraud detection (ADR-0003) ### Fraud detection (ADR-0018; historical ADR-0003)
Validators re-run ~5% of completed requests. If a node's output diverges beyond floating-point tolerance from the reference, the validator submits a slash transaction on-chain. Strike count increments. At the configured strike threshold, the wallet is banned on-chain. New wallets complete N unpaid jobs before earning begins. Validators re-run ~5% of completed requests with TOPLOC activation verification. Caught cheaters forfeit pending balance and receive strikes; three strikes bans the wallet. Probation (first N unpaid jobs) remains the anti-sybil re-entry cost.
### Tracker architecture (ADR-0004) ### Tracker architecture (ADR-0004)
Centralized tracker service (HTTP + WebSocket) for fast routing. Nodes gossip state via a lightweight P2P layer so the node client can discover routes during tracker outages. Solana is the authoritative source of truth for all incentive-relevant state. Centralized tracker service (HTTP + WebSocket) for fast routing. Nodes gossip state via a lightweight P2P layer so the node client can discover routes during tracker outages. **Alpha:** strike/ban/forfeiture state lives in the tracker registry (ADR-0018); USDT settlement via custodial treasury (ADR-0015). On-chain programs deferred (ADR-0007).
### Shard distribution (ADR-0005) ### Shard distribution (ADR-0005)
Shards are identified by `(model_preset, shard_index)`. On assignment, the node downloads the shard layers from HuggingFace using `huggingface_hub`. Once downloaded, the node joins the P2P shard swarm and seeds to other nodes requesting the same shard. Popular shards propagate entirely via P2P; cold shards fall back to HuggingFace. Shards are identified by `(model_preset, shard_index)`. On assignment, the node downloads the shard layers from HuggingFace using `huggingface_hub`. Once downloaded, the node joins the P2P shard swarm and seeds to other nodes requesting the same shard. Popular shards propagate entirely via P2P; cold shards fall back to HuggingFace.
@@ -115,9 +117,9 @@ The gateway exposes OpenAI-compatible endpoints (`/v1/chat/completions`, `/v1/mo
**Per-component seams:** **Per-component seams:**
- **Tracker**: given a set of registered nodes with known shard coverage and node scores, assert `select_route(model_preset)` returns an optimal ordered list of node endpoints. - **Tracker**: given a set of registered nodes with known shard coverage and node scores, assert `select_route(model_preset)` returns an optimal ordered list of node endpoints.
- **Node shard serving**: given an activation tensor for the node's layer range, assert the output tensor shape and dtype are correct. - **Node shard serving**: given an activation tensor for the node's layer range, assert the output tensor shape and dtype are correct.
- **Fraud detection**: given a validator that re-runs a known-bad node response, assert a slash transaction is submitted on-chain with correct attribution. - **Fraud detection**: given a validator that re-runs a known-bad node response, assert strike/forfeiture state updates with correct attribution (ADR-0018; on-chain slash deferred).
- **Shard swarm**: given a node that has a shard, assert a second node with the same assignment downloads it via P2P rather than HuggingFace. - **Shard swarm**: given a node that has a shard, assert a second node with the same assignment downloads it via P2P rather than HuggingFace.
- **Payment settlement**: given a completed inference session with known compute attribution, assert token balances change by the expected amounts after epoch settlement. - **Payment settlement**: given a completed inference session with known compute attribution, assert USDT ledger balances change by the expected amounts after epoch settlement (ADR-0015).
## Out of Scope ## Out of Scope
@@ -135,4 +137,4 @@ The gateway exposes OpenAI-compatible endpoints (`/v1/chat/completions`, `/v1/mo
- The `meshnet-node` CLI is the primary viral growth vector. Every friction point in the install/start sequence costs node operators. The startup sequence must complete without any manual configuration on a machine with a CUDA-capable GPU. - The `meshnet-node` CLI is the primary viral growth vector. Every friction point in the install/start sequence costs node operators. The startup sequence must complete without any manual configuration on a machine with a CUDA-capable GPU.
- The name "meshnet" is a working name. The actual package and token names are TBD. - The name "meshnet" is a working name. The actual package and token names are TBD.
- The Solana L2 chain selection (vs Base/Arbitrum) is not yet finalised — both are cheap, EVM-compatible fallbacks. The contracts package should abstract chain-specific details. - The Solana L2 chain selection (vs Base/Arbitrum) is not yet finalised — both are cheap, EVM-compatible fallbacks. The contracts package should abstract chain-specific details.
- The probationary period length (N free jobs) and slash amounts are economic parameters that will need tuning once the network has real usage data. Hardcode sensible defaults; make them on-chain governable. - The probationary period length (N free jobs) and forfeiture amounts are economic parameters that will need tuning once the network has real usage data. Hardcode sensible defaults; governance TBD (ADR-0018).

View File

@@ -1,5 +1,7 @@
# PyTorch over llama.cpp for the inference engine # PyTorch over llama.cpp for the inference engine
> **Runtime direction update (2026-07-13):** PyTorch/safetensors remains the current production backend and correctness reference. A benchmark-gated native GGUF path is defined in [ADR-0024](0024-distributed-gguf-runtime.md); it does not replace this ADR until release gates pass.
We started with llama.cpp RPC as the distributed backend (following kyuz0/amd-strix-halo-toolboxes), but switched to PyTorch with a Petals-style shard pipeline. llama.cpp RPC requires the primary node to load the full model and distribute weights over the network at every session start — for a 70B model that's ~70GB over LAN per launch, making tracker-driven node rebalancing prohibitively expensive. PyTorch/Petals lets each node load its shard independently from local disk; only activations (~8KB per layer boundary per token) cross the network at inference time. PyTorch also has same-day support for new model architectures, training support (required for the planned torrent-style fine-tuning feature), and is the engine Petals itself uses for this exact use case. We started with llama.cpp RPC as the distributed backend (following kyuz0/amd-strix-halo-toolboxes), but switched to PyTorch with a Petals-style shard pipeline. llama.cpp RPC requires the primary node to load the full model and distribute weights over the network at every session start — for a 70B model that's ~70GB over LAN per launch, making tracker-driven node rebalancing prohibitively expensive. PyTorch/Petals lets each node load its shard independently from local disk; only activations (~8KB per layer boundary per token) cross the network at inference time. PyTorch also has same-day support for new model architectures, training support (required for the planned torrent-style fine-tuning feature), and is the engine Petals itself uses for this exact use case.
## Considered Options ## Considered Options

View File

@@ -1,5 +1,7 @@
# Optimistic trust with stake slashing and strike-based bans # Optimistic trust with stake slashing and strike-based bans
> **Settlement update (2026-07-04):** Alpha uses pending-balance forfeiture instead of stake slashing ([ADR-0015](0015-usdt-custodial-settlement.md)). Fraud detection, TOPLOC audits, and persisted reputation are specified in [ADR-0018](0018-fraud-detection-verification-and-reputation.md). The text below is the historical prototype design.
All inference responses are trusted by default. Validators re-run a random sample (~5%) of requests on reference nodes and compare outputs. Nodes that fail are slashed (stake reduced). Enough strikes result in a permanent on-chain ban. All inference responses are trusted by default. Validators re-run a random sample (~5%) of requests on reference nodes and compare outputs. Nodes that fail are slashed (stake reduced). Enough strikes result in a permanent on-chain ban.
For the prototype, the gateway emits validation events after completed requests. A validation event records the session id, model preset, request messages, observed output, and the route metadata for each node that served the request. The validator samples events with a configurable rate and deterministic seed for tests. Sampled events are re-run against a trusted reference node/reference function; string outputs must match exactly for stub models, while future tensor/model outputs use a configurable floating-point tolerance. For the prototype, the gateway emits validation events after completed requests. A validation event records the session id, model preset, request messages, observed output, and the route metadata for each node that served the request. The validator samples events with a configurable rate and deterministic seed for tests. Sampled events are re-run against a trusted reference node/reference function; string outputs must match exactly for stub models, while future tensor/model outputs use a configurable floating-point tolerance.

View File

@@ -1,6 +1,6 @@
# ADR-0020: Dashboard chat streaming, live request progress, and the mixed-topology routing flaw # ADR-0020: Dashboard chat streaming, live request progress, and the mixed-topology routing flaw
## Status: Accepted (chat/streaming/styles implemented); routing flaw documented, fix pending ## Status: Accepted (chat/streaming/styles and mixed-topology routing fix implemented)
## Context ## Context
@@ -94,7 +94,7 @@ head + full-model downstream is a topology the planner never had to handle befor
prior split tests used disjoint shards (011 + 1223) where `shard_start` happened to prior split tests used disjoint shards (011 + 1223) where `shard_start` happened to
equal the correct continuation layer. equal the correct continuation layer.
### Required fix (not yet implemented) ### Required fix (implemented 2026-07-07 — commits `518c259`, `e44abc9`, `1ecc599`; see ADR-0021)
1. **Correct continuation layer:** when hop N ends at layer `e`, hop N+1 must execute 1. **Correct continuation layer:** when hop N ends at layer `e`, hop N+1 must execute
from `start_layer = e + 1` regardless of the downstream node's own `shard_start` from `start_layer = e + 1` regardless of the downstream node's own `shard_start`

View File

@@ -1,6 +1,8 @@
# ADR-0022: Sharded per-node generation cache for distributed PyTorch routes # ADR-0022: Sharded per-node generation cache for distributed PyTorch routes
## Status: Accepted ## Status: Superseded — see [0022-sharded-per-node-kv-cache.md](0022-sharded-per-node-kv-cache.md)
> Draft alternate header names (`X-Meshnet-Cache-Mode`, `X-Meshnet-Seq-Len`) were not implemented. The accepted wire protocol and implementation use `X-Meshnet-Cache` and `X-Meshnet-Past-Len` per the linked ADR.
## Context ## Context

View File

@@ -1,8 +1,12 @@
# ADR-0020: Lean Native Distributed GGUF Runtime # ADR-0024: Lean Native Distributed GGUF Runtime
Status: Accepted Status: Accepted
Date: 2026-07-13 Date: 2026-07-13
> **Numbering note:** ADR-0020 is reserved for dashboard chat streaming and mixed-topology routing (`docs/adr/0020-chat-streaming-live-progress-and-mixed-topology-routing.md`). This record was originally drafted as ADR-0020 in `.scratch/distributed-gguf-runtime/` and renumbered to avoid the collision.
>
> **Relation to ADR-0001:** PyTorch/safetensors remains the correctness reference and current production backend. This ADR defines a benchmark-gated native GGUF path; it does not revoke ADR-0001 until release gates pass.
## Context ## Context
The project currently uses Transformers/safetensors as its real model execution backend. This provides broad architecture coverage and a correctness reference, but reported and observed consumer CPU/GPU inference performance motivates evaluating llama.cpp/GGML and quantized GGUF. The project currently uses Transformers/safetensors as its real model execution backend. This provides broad architecture coverage and a correctness reference, but reported and observed consumer CPU/GPU inference performance motivates evaluating llama.cpp/GGML and quantized GGUF.
@@ -119,3 +123,11 @@ Rejected. gRPC/HTTP2 already provides mature streaming, flow control, deadlines,
4. Real two-machine execution using both Shards. 4. Real two-machine execution using both Shards.
5. End-to-end performance/fit advantage over the current distributed route. 5. End-to-end performance/fit advantage over the current distributed route.
6. Separate Qwen3-family architecture certification. 6. Separate Qwen3-family architecture certification.
## Relationship to US-042 (whole-model GGUF shortcut)
[US-042](../issues/42-gguf-llamacpp-node-backend.md) **phase C** ships first: a node with enough RAM serves a **full** GGUF via llama.cpp on a single-hop Inference Route using the existing HTTP activation seam and PyTorch-era tracker integration. That is intentionally small and does not require this ADR's gRPC worker or llama.cpp patch stack.
This ADR's track starts only after **DGR-001** (controlled safetensors-vs-GGUF benchmark) shows a meaningful speed or fit benefit. Then implement the native worker (DGR-002+) — which subsumes US-042 direction A (layer-range GGUF + boundary tensors) if the benchmark warrants it.
Do not run US-042 phase C and DGR-008+ in parallel on the same node backend without an explicit integration plan; phase C uses llama-cpp-python (or equivalent) whole-model path; ADR-0024 uses the standalone C++ worker.

View File

@@ -0,0 +1,52 @@
# ADR-0025: TAI reserved mint and off-chain accrual (phase B/C)
## Status: Accepted
## Context
ADR-0015 chose **USDT-direct custodial settlement** for alpha and near-term production. Clients pay USDT; nodes receive batched USDT SPL payouts. ADR-0002's TAI reward token, revenue-backed floor, and open-market listing gates remain the long-term design but are **not** the live payment path.
The owner wants TAI to exist without the cost and legal surface of a public launch: no AMM, no open listing, no client-facing TAI, no on-chain stake machinery.
## Decision
### Phase B — Reserved mainnet mint (cheap, optional early)
- Create a fixed-supply TAI SPL mint on **mainnet** when treasury work happens (~0.002 SOL).
- Entire initial supply sits in a **team-controlled** wallet (same custody posture as the USDT treasury today).
- **No public emission, no market, no client UX.** Mint exists for name reservation and future programmatic rewards only.
- Document mint address in operator config; do not advertise to users.
### Phase C — Off-chain TAI accrual alongside USDT (before automatic on-chain TAI payouts)
- Extend the billing ledger with **`tai_pending[wallet]`** accrued from completed inference work using a simple rule (e.g. USDT node share × configurable TAI-per-USDT rate, or fixed TAI per work unit).
- TAI accrual is **display-only + ledger-persisted** initially; nodes see pending TAI in dashboard/CLI.
- **Clients never pay or hold TAI.** USDT remains the only client-facing asset.
- Optional manual or scheduled **TAI SPL batch transfers** from the team wallet (same batching pattern as USDT `send_payouts`) — operator-triggered until automatic emission is justified by volume.
- The existing **10% protocol USDT cut** continues to accumulate as future TAI liquidity per ADR-0015/0002; do not redirect it until a deliberate liquidity event.
### Explicit non-goals (this ADR)
- Open-market listing, AMM, or DEX liquidity
- Buyback floor endpoint or backing-price oracle (ADR-0002 machinery)
- On-chain stake deposits or slash contracts
- Paying clients rebates or accepting TAI for inference
- Replacing USDT node payouts with TAI-only payouts before volume gates in ADR-0002 pass
## Relation to ADR-0002 listing gates
Public TAI listing stays gated on **$50k cumulative USDT volume** and **25+ nodes / 15+ wallets**. Phase B/C may proceed **below** those gates because they do not create a public market — only reserved supply and off-chain accounting.
Securities review remains required before any **public** distribution or listing; off-chain accrual to hired/known operators with manual SPL transfers is an operator discretion, not a product promise.
## Consequences
- USDT mainnet pilot (two-wallet setup) is unblocked without TAI complexity.
- TAI narrative is preserved at minimal cost (mint + ledger column + optional manual transfers).
- Automatic TAI emission can later reuse the US-033 settlement loop shape with a second mint and separate pending bucket.
- Dashboard and APIs must label TAI balances as **non-withdrawable** until an on-chain payout batch confirms.
## Verification
- USDT settlement tests remain authoritative for production payouts (`tests/test_settlement_loop.py`).
- When phase C lands: ledger tests for `tai_pending` accrual, idempotent gossip replication, and optional TAI batch payout adapter tests mirroring USDT.

View File

@@ -0,0 +1,51 @@
# ADR-0026: Node assignment ownership — pinned startup vs managed demand placement
## Status: Accepted
## Context
Three features define how a node gets its `(model, shard range, recipe/quantization)`:
1. **ADR-0011 / US-013** — tracker suggests a gap from coverage map on startup or auto-join.
2. **Node capability admission (ADR-0023 / NCA)** — a node must pass `doctor` + real forward before becoming routable; startup-assigned work is validated, not blindly trusted.
3. **Qwen demand placement** (`.scratch/qwen3.6-27b-demand-placement/`) — tracker deploys a model when chat demand appears and spare capacity exists.
These looked contradictory: NCA and the Qwen PRD both say startup assignments are "pinned," while demand placement wants the tracker to assign models dynamically.
## Decision
### Three assignment tiers
| Tier | How it is created | Mutable by tracker? | Admission |
|---|---|---|---|
| **Operator-initiated** | Node starts with explicit `--model` / shard flags | **No** — pinned until operator restarts or explicitly reloads | Must pass NCA `doctor` before routable |
| **Network bootstrap** | `/v1/network/assign` or `/v1/nodes/assign` on first join (ADR-0011) | **No** for the active loaded shard — treated as operator-equivalent once accepted at startup | Must pass NCA before routable |
| **Tracker-managed** | Demand-driven placement (Qwen PRD) on spare capacity | **Yes** — marked `managed: true`; subject to cooldown / safety policy | Must pass NCA for the new assignment before routable |
### Spare capacity rule (unifies NCA + Qwen)
- A nodes **active** `(model, shard, recipe)` from startup is **pinned** — the tracker does not silently retarget a serving node to a different model.
- **Spare capacity** — memory/slots not holding the pinned assignment, or a node registered without a model — may receive **tracker-managed** assignments to satisfy demand.
- Until multi-shard runtime exists (US-048), “spare capacity” effectively means **model-less nodes** or nodes explicitly registered for managed placement; do not overload a single-shard node with a second assignment.
### Demand placement interaction
- First chat request for an unrouted model queues **demand**; leader tracker may assign **managed** nodes only when eligible spare capacity exists (Qwen PRD).
- Until complete coverage + validated recipes exist, return retryable `503 model_loading` with coverage metadata.
- Managed assignments must not evict pinned assignments on other nodes without the Qwen safety policy (≥3 copies, 1.5× demand multiplier, cooldown).
### NCA is not optional for any tier
Regardless of assignment source, registration carries **validated capability** only after `doctor` succeeds. The tracker excludes nodes with absent, stale, or failed capability reports (ADR-0023).
## Consequences
- NCA and Qwen demand placement are complementary: NCA gates *quality*; demand placement gates *where new coverage comes from*.
- US-048 (multi-shard slots) extends spare capacity — until then, demand placement primarily targets nodes that join without `--model`.
- Rebalance / dropout relocation (US-013, US-048) applies to **coverage gaps**, not retroactive retargeting of pinned nodes for demand convenience.
## Verification
- NCA tests: unvalidated nodes never routed.
- Demand-placement tests (when implemented): managed flag set; pinned nodes unchanged.
- Documented in Qwen scratch PRD and NCA README cross-links.

View File

@@ -1,4 +1,4 @@
Status: ready-for-agent Status: done
# 01 — Monorepo scaffold + single-node smoke test # 01 — Monorepo scaffold + single-node smoke test

View File

@@ -1,4 +1,4 @@
Status: ready-for-agent Status: done
# 02 — Two-node shard pipeline # 02 — Two-node shard pipeline

View File

@@ -1,4 +1,4 @@
Status: ready-for-agent Status: done
# 03 — Tracker: node registration + route selection # 03 — Tracker: node registration + route selection

View File

@@ -1,4 +1,4 @@
Status: ready-for-agent Status: done
# 04 — Node client startup flow (`meshnet-node start`) # 04 — Node client startup flow (`meshnet-node start`)

View File

@@ -1,4 +1,4 @@
Status: ready-for-agent Status: done
# 05 — OpenAI-compatible gateway # 05 — OpenAI-compatible gateway

View File

@@ -1,4 +1,4 @@
Status: ready-for-agent Status: done (on-chain registry mechanics superseded — probation/ban enforcement uses tracker registry + ADR-0015/0018)
# 08 — Node probationary period + ban enforcement # 08 — Node probationary period + ban enforcement

View File

@@ -1,4 +1,4 @@
Status: ready-for-agent Status: done
# 09 — P2P shard swarm # 09 — P2P shard swarm

View File

@@ -1,4 +1,4 @@
Status: ready-for-agent Status: done
# 10 — `meshnet` Python SDK # 10 — `meshnet` Python SDK

View File

@@ -1,8 +1,6 @@
# US-019 — Binary data plane and optional peer weight transfer # US-019 — Binary data plane and optional peer weight transfer
Status: needs-triage Status: done (design parking lot; binary activation path shipped in US-011/US-019)
Priority: Low
Stage: Design parking lot
## Context ## Context

View File

@@ -1,66 +1,10 @@
Status: ready-for-agent Status: superseded
# US-020 - Memory budget, shard slots, and dropout relocation hardening # Superseded — renumbered to US-048
## Goal This issue slot was a duplicate of tracker-node-hardening (US-020). Memory budget / shard slots / dropout relocation work lives at:
Make node capacity limits explicit and enforce them consistently when the tracker assigns, rebalances, and relocates shards after a node dropout. - **Issue:** [48-memory-budget-shard-slots-and-dropout-relocation.md](./48-memory-budget-shard-slots-and-dropout-relocation.md)
- **PRD:** `docs/prd.json``US-048`
This is a follow-up to US-013, not a replacement. US-013 owns the coverage-first assignment and rebalance algorithm. This issue hardens the capacity contract around that algorithm: operator memory budget, maximum loaded shard slots, and relocation behavior when one node must absorb or split ranges after another node disappears. Do not implement from this file.
## Context
Recent work added the first part of the contract:
- `meshnet-node --memory MB` is registered with the tracker as `vram_bytes` when explicitly set.
- CPU nodes without `--memory` keep the tracker default capacity, preserving old behavior.
- `meshnet-node --max-shards N` is accepted and registered as `max_loaded_shards`.
- Tracker registration validates `max_loaded_shards >= 1`.
The current runtime still effectively has one active backend shard per node. A node may advertise `max_loaded_shards`, but the tracker does not yet use multiple shard slots in bin-packing, and the node does not yet host multiple concurrently loaded shard ranges.
## Scope
- Make tracker rebalance logic account for `max_loaded_shards` as a capacity multiplier or explicit shard-slot list.
- Ensure a node is never assigned more total layers than its memory budget can support across all loaded shard slots.
- Decide and implement the runtime behavior for multiple loaded shards:
- either support multiple concurrently loaded shard backends on one node, or
- keep one backend active and treat `max_loaded_shards` as future metadata, with tracker enforcement preventing multi-range assignment for now.
- On heartbeat timeout, relocate the dropped node's uncovered layer range to eligible managed nodes while respecting both memory and shard-slot limits.
- Surface the effective memory budget and shard slot count in tracker/network inspection output so operators can diagnose why a node did or did not receive a range.
## Non-Goals
- Do not redesign the US-013 coverage-first algorithm from scratch.
- Do not change relay, `/ws`, or `/rpc` behavior.
- Do not change the token/reward model.
- Do not require public internet verification; all behavior must be locally testable.
## Acceptance Criteria
- Tracker stores and exposes `max_loaded_shards` for registered nodes.
- Assignment/rebalance never exceeds:
- `assigned_layers_total <= floor((vram_bytes * 0.8) / bytes_per_layer_at_quant)`
- `assigned_range_count <= max_loaded_shards`
- A managed node with `max_loaded_shards=1` only receives one active shard range.
- A managed node with `max_loaded_shards=2` can absorb two non-contiguous uncovered ranges only if the node runtime supports serving both; otherwise tracker must keep assigning at most one range and document `max_loaded_shards` as reserved.
- Dropout test: register nodes covering a model, let a middle/tail node heartbeat-expire, and assert the tracker queues `LOAD_SHARD` directives that restore full coverage without violating memory or shard-slot limits.
- CLI test: `--memory` and `--max-shards` are reflected in the registration payload.
- `python -m pytest tests/test_tracker_routing.py tests/test_node_startup.py` passes in the project virtualenv, aside from any pre-existing platform-specific wallet permission assertion documented in the final notes.
## Implementation Notes
- Existing files likely involved:
- `packages/node/meshnet_node/cli.py`
- `packages/node/meshnet_node/startup.py`
- `packages/node/meshnet_node/torch_server.py`
- `packages/tracker/meshnet_tracker/server.py`
- `tests/test_tracker_routing.py`
- `tests/test_node_startup.py`
- Keep backward compatibility: nodes that omit `vram_bytes` default to tracker defaults; nodes that omit `max_loaded_shards` default to `1`.
- Prefer a small internal representation for assigned ranges if multiple ranges become real, for example `assigned_shards: list[tuple[int, int]]`, while preserving `shard_start`/`shard_end` in public responses for single-range nodes.
## Comments
- 2026-06-30: Created after implementing the initial registration plumbing in commit `f1e4ed6` (`--memory`, `--max-shards`, tracker validation). This issue captures the remaining end-to-end behavior so it does not conflict with US-013.
- 2026-06-30: Implementation decision: `max_loaded_shards` is currently a validated and exposed capacity field, but multi-range assignment remains reserved because `TorchNodeServer` serves one active backend shard. The tracker therefore emits at most one active range per node while exposing `vram_bytes`, `ram_bytes`, `max_loaded_shards`, quantization, throughput, and computed `max_assignable_layers` in inspection endpoints.

View File

@@ -1,8 +1,6 @@
# US-036 — Streamed chat completions over the relay RPC path # US-036 — Streamed chat completions over the relay RPC path
Status: planned Status: done (implemented — `_stream_relayed_frames` in `server.py`; verify on public NAT relay before friends-test)
Priority: Critical (blocks public friends-test deployment)
Stage: Designed
## Context ## Context

View File

@@ -1,9 +1,16 @@
# US-042 — GGUF/llama.cpp node backend # US-042 — GGUF/llama.cpp node backend
Status: planned Status: planned
Priority: High (unlocks big MoE models on volunteer hardware — the pool's core value) Priority: High (unlocks DeepSeek-V4-Flash on volunteer hardware — the pool's core value)
Stage: Draft design Stage: Draft design
## Goal
Run **DeepSeek-V4-Flash** as the first real large-model target on volunteer
hardware via GGUF/llama.cpp. This epic is no longer GLM-oriented: the initial
objective is to prove that DeepSeek-V4-Flash can load and serve correctly on
consumer/unified-memory nodes, then expand from there.
## Context ## Context
The node backend is transformers-only (`model_backend.py` The node backend is transformers-only (`model_backend.py`

View File

@@ -86,10 +86,10 @@ What exists already (build on it, don't duplicate):
- [ ] Two-machine test: machine A (tracker + node, holds full snapshot) serves - [ ] Two-machine test: machine A (tracker + node, holds full snapshot) serves
layers 0k; machine B joins with no model and receives **only** the files layers 0k; machine B joins with no model and receives **only** the files
for its assigned range from A — nothing fetched from HF for its assigned range from A — nothing fetched from HF
- [ ] Machine B's resident memory scales with its shard size, not model size - [x] Machine B's resident memory scales with its shard size, not model size
- [ ] Checksums verified end-to-end; corrupted transfer falls back cleanly - [x] Checksums verified end-to-end; corrupted transfer falls back cleanly
- [x] Single-node/full-model flows unchanged - [x] Single-node/full-model flows unchanged
- [ ] `python -m pytest` passes from repo root - [x] `python -m pytest` passes from repo root
## Implementation notes ## Implementation notes
@@ -98,6 +98,13 @@ What exists already (build on it, don't duplicate):
`full_url`; HuggingFace remains fallback-only, and when it is used the node `full_url`; HuggingFace remains fallback-only, and when it is used the node
computes `allow_patterns` from the repo's remote SafeTensors index so it computes `allow_patterns` from the repo's remote SafeTensors index so it
stays layer-filtered even without tracker-cached files. Remaining hard half stays layer-filtered even without tracker-cached files. Remaining hard half
is true partial model materialization: the backend can prefer a downloaded is partial model materialization: the backend can prefer a downloaded
local model directory, but Transformers still needs a `meta`-device load local model directory, but Transformers still needs a `meta`-device load
path that materializes only assigned layers. path that materializes only assigned layers.
- 2026-07-13: Partial LOAD implemented. `_load_partial_model_from_snapshot` builds
on `meta` via `init_empty_weights`, materializes only layer-scoped checkpoint
tensors, and finalizes device placement without copying unmaterialized meta
weights (`_finalize_active_shard_modules_on_device`). Tests cover memory
scaling (`test_partial_snapshot_resident_weight_numel_scales_with_shard`)
and real-torch meta-vs-materialized counts. Remaining: live two-machine LAN
verification.

View File

@@ -0,0 +1,69 @@
Status: planned
# US-048 — Memory budget, shard slots, and dropout relocation hardening
> Renumbered from duplicate slot `20` (which belongs to tracker-node-hardening / US-020 in `docs/prd.json`).
## Goal
Make node capacity limits explicit and enforce them consistently when the tracker assigns, rebalances, and relocates shards after a node dropout.
This is a follow-up to US-013, not a replacement. US-013 owns the coverage-first assignment and rebalance algorithm. This issue hardens the capacity contract around that algorithm: operator memory budget, maximum loaded shard slots, and relocation behavior when one node must absorb or split ranges after another node disappears.
## Context
Recent work added the first part of the contract:
- `meshnet-node --memory MB` is registered with the tracker as `vram_bytes` when explicitly set.
- CPU nodes without `--memory` keep the tracker default capacity, preserving old behavior.
- `meshnet-node --max-shards N` is accepted and registered as `max_loaded_shards`.
- Tracker registration validates `max_loaded_shards >= 1`.
The current runtime still effectively has one active backend shard per node. A node may advertise `max_loaded_shards`, but the tracker does not yet use multiple shard slots in bin-packing, and the node does not yet host multiple concurrently loaded shard ranges.
## Scope
- Make tracker rebalance logic account for `max_loaded_shards` as a capacity multiplier or explicit shard-slot list.
- Ensure a node is never assigned more total layers than its memory budget can support across all loaded shard slots.
- Decide and implement the runtime behavior for multiple loaded shards:
- either support multiple concurrently loaded shard backends on one node, or
- keep one backend active and treat `max_loaded_shards` as future metadata, with tracker enforcement preventing multi-range assignment for now.
- On heartbeat timeout, relocate the dropped node's uncovered layer range to eligible managed nodes while respecting both memory and shard-slot limits.
- Surface the effective memory budget and shard slot count in tracker/network inspection output so operators can diagnose why a node did or did not receive a range.
## Non-Goals
- Do not redesign the US-013 coverage-first algorithm from scratch.
- Do not change relay, `/ws`, or `/rpc` behavior.
- Do not change the token/reward model.
- Do not require public internet verification; all behavior must be locally testable.
## Acceptance Criteria
- Tracker stores and exposes `max_loaded_shards` for registered nodes.
- Assignment/rebalance never exceeds:
- `assigned_layers_total <= floor((vram_bytes * 0.8) / bytes_per_layer_at_quant)`
- `assigned_range_count <= max_loaded_shards`
- A managed node with `max_loaded_shards=1` only receives one active shard range.
- A managed node with `max_loaded_shards=2` can absorb two non-contiguous uncovered ranges only if the node runtime supports serving both; otherwise tracker must keep assigning at most one range and document `max_loaded_shards` as reserved.
- Dropout test: register nodes covering a model, let a middle/tail node heartbeat-expire, and assert the tracker queues `LOAD_SHARD` directives that restore full coverage without violating memory or shard-slot limits.
- CLI test: `--memory` and `--max-shards` are reflected in the registration payload.
- `python -m pytest tests/test_tracker_routing.py tests/test_node_startup.py` passes in the project virtualenv, aside from any pre-existing platform-specific wallet permission assertion documented in the final notes.
## Implementation Notes
- Existing files likely involved:
- `packages/node/meshnet_node/cli.py`
- `packages/node/meshnet_node/startup.py`
- `packages/node/meshnet_node/torch_server.py`
- `packages/tracker/meshnet_tracker/server.py`
- `tests/test_tracker_routing.py`
- `tests/test_node_startup.py`
- Keep backward compatibility: nodes that omit `vram_bytes` default to tracker defaults; nodes that omit `max_loaded_shards` default to `1`.
- Prefer a small internal representation for assigned ranges if multiple ranges become real, for example `assigned_shards: list[tuple[int, int]]`, while preserving `shard_start`/`shard_end` in public responses for single-range nodes.
## Comments
- 2026-06-30: Created after implementing the initial registration plumbing in commit `f1e4ed6` (`--memory`, `--max-shards`, tracker validation). This issue captures the remaining end-to-end behavior so it does not conflict with US-013.
- 2026-06-30: Implementation decision: `max_loaded_shards` is currently a validated and exposed capacity field, but multi-range assignment remains reserved because `TorchNodeServer` serves one active backend shard. The tracker therefore emits at most one active range per node while exposing `vram_bytes`, `ram_bytes`, `max_loaded_shards`, quantization, throughput, and computed `max_assignable_layers` in inspection endpoints.
- 2026-07-13: Renumbered from `docs/issues/20-memory-budget-…` to resolve duplicate issue slot 20.

View File

@@ -0,0 +1,101 @@
Status: ready-for-agent
# US-049 — Mainnet USDT cutover: two-wallet pilot checklist
Priority: High (first real-money friends test)
Stage: Operator runbook + config verification
## Goal
Move from **Solana devnet + mock-USDT** to **Solana mainnet + real USDT** for a minimal pilot: **one client wallet** (inference payer) and **one node-operator wallet** (payout recipient). Treasury holds USDT and pays SOL fees. TAI stays phase B/C per [ADR-0025](../adr/0025-tai-off-chain-accrual-and-reserved-mint.md).
## Wallet roles
| Role | Keypair | On-chain use |
|---|---|---|
| **Treasury** | Operator `treasury-keypair.json` (multisig when ready) | Holds USDT float + SOL for fees; sends batched node payouts |
| **Client** | Your inference-user wallet | SPL USDT → treasury; bound to API key for ledger credit |
| **Node** | Your node-operator wallet | Receives USDT payout batches from treasury |
The node process already creates/loads a Solana wallet at startup; the client wallet is bound via accounts/dashboard (`POST /v1/wallet/register` or US-041 flows).
## Pre-flight (devnet smoke — do not skip)
- [ ] Tracker with `--solana-rpc-url https://api.devnet.solana.com`, mock mint, treasury keypair
- [ ] `--settle-period 60 --payout-threshold 0` — confirm payout appears on dashboard **Settlement history** with explorer link
- [ ] Run `python -m pytest tests/test_settlement_loop.py -q` — includes prod 24h/5 USDT gate tests
- [ ] One inference request → node pending → settlement tx → node wallet balance increases
## Mainnet config change (config-only cutover)
Replace devnet values; **no code deploy required** beyond what is already on the branch.
```bash
# Example — use your mainnet RPC provider
meshnet-tracker start \
--solana-rpc-url https://api.mainnet-beta.solana.com \
--usdt-mint EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v \
--treasury-keypair ~/.config/solana/meshnet-treasury-mainnet.json \
--settle-period 86400 \
--payout-threshold 5.0 \
--payout-dust-floor 0.01 \
--starting-credit 0 \
--devnet-topup 0
```
| Flag | Devnet (test) | Mainnet (pilot) |
|---|---|---|
| RPC | `api.devnet.solana.com` | Mainnet RPC (Helius/QuickNode/etc.) |
| `--usdt-mint` | mock mint from `devnet_setup.py` | Real USDT mint (`EPjF…` on Solana) |
| `--settle-period` | `60` (fast verify) | `86400` (24 h) |
| `--payout-threshold` | `0` | `5.0` USDT |
| `--starting-credit` | `1.0` (optional) | `0` |
| `--devnet-topup` | `1.0` | `0` |
## Treasury funding
- [ ] Fund treasury wallet with **SOL** for fees (~0.10.5 SOL to start; ~$0.001 per daily batch + ~$0.30 once per new node ATA)
- [ ] Fund treasury with **USDT** for node payouts (your float — e.g. first week of expected node earnings)
- [ ] Client wallet holds USDT; send a test SPL transfer to treasury ATA; confirm deposit watcher credits API key within one poll interval
## Two-wallet pilot steps
1. **Start tracker** on mainnet config above (single settlement tracker per ADR-0016).
2. **Client path:** register account → create API key → bind **client wallet** → deposit USDT to treasury → verify ledger balance on dashboard.
3. **Node path:** start `meshnet-node` with **node wallet** keypair → register → serve inference.
4. **Inference:** client sends `POST /v1/chat/completions` with API key; verify 402 before deposit, success after.
5. **Accrual:** confirm node **pending USDT** on dashboard rises; client balance debits.
6. **Payout (24 h):** wait for `--settle-period` **or** temporarily lower to `300` for first pilot verification, then restore `86400`.
7. **Threshold path:** alternatively, accumulate ≥ `5` USDT pending in one session to trigger immediate batch without waiting 24 h.
8. **Verify on-chain:** settlement history shows mainnet tx signature; node wallet USDT ATA balance increased; pending zeroed.
## Safety checks
- [ ] `--devnet-topup 0` — no faucet on mainnet
- [ ] `--starting-credit 0` — no free inference credit
- [ ] Treasury keypair not committed to git; file mode 600
- [ ] Plan multisig migration before large float ([alpha runbook](../../.scratch/alpha-hardening/runbooks/02-treasury-key-rotation.md) intent)
- [ ] Issue **21** (TOPLOC calibration) before production audit thresholds on untrusted nodes — runbook: [04-toploc-calibration-run](../../.scratch/alpha-hardening/runbooks/04-toploc-calibration-run.md)
## Cost estimate (this pilot)
- **SOL fees:** pennies per day at 1 batch × 1 node
- **USDT:** whatever clients deposit and nodes earn (treasury is passthrough for payouts)
- **TAI mint (optional, ADR-0025 phase B):** ~0.002 SOL one-time — defer if not needed for pilot
## Acceptance criteria
- [ ] Devnet checklist completed once
- [ ] Mainnet tracker serves dashboard; billing enabled
- [ ] Client wallet deposit → ledger credit → inference → debit
- [ ] Node wallet receives ≥1 confirmed USDT payout batch on mainnet
- [ ] 24 h period enforced: sub-threshold pending not paid before period (covered by `tests/test_settlement_loop.py`)
- [ ] ≥5 USDT pending triggers payout without waiting full period (covered by tests)
- [ ] Rollback documented: switch RPC + mint back to devnet if needed
## Related
- ADR-0015 (USDT custodial settlement)
- ADR-0025 (TAI reserved mint / off-chain accrual — not blocking this pilot)
- US-033 / US-032 (settlement + deposits)
- `scripts/devnet_setup.py` (devnet only)

View File

@@ -0,0 +1,15 @@
Status: in-design
# US-050 — Qwen3.6-27B demand-driven managed placement
> Full spec: [.scratch/qwen3.6-27b-demand-placement/PRD.md](../../.scratch/qwen3.6-27b-demand-placement/PRD.md)
> Assignment rules: [ADR-0026](../adr/0026-node-assignment-ownership-and-managed-placement.md)
> Admission: [ADR-0023](../adr/0023-model-agnostic-node-capability-admission.md)
## Summary
Deploy `Qwen/Qwen3.6-27B` when chat demand appears and **spare** fleet capacity exists. Startup `--model` assignments stay **pinned**; tracker-managed loads fill gaps on model-less or (future US-048) unused slot capacity only.
## Acceptance criteria
See scratch PRD and `docs/prd.json` US-050.

View File

@@ -1,6 +1,6 @@
{ {
"name": "Distributed Inference Network", "name": "Distributed Inference Network",
"description": "Build a distributed inference network with node, gateway, tracker, SDK, contracts, and P2P shard distribution components from the grill session PRD.", "description": "Distributed inference network: base program US-001…US-035 complete; friends-test arc US-036…US-049; capacity/placement US-048/050. Scratch features (alpha hardening AH-001…AH-025, NCA, distributed GGUF, Qwen demand) have separate prd.json files under .scratch/.",
"branchName": "ralph/distributed-inference-network", "branchName": "ralph/distributed-inference-network",
"userStories": [ "userStories": [
{ {
@@ -814,10 +814,307 @@
"US-033" "US-033"
], ],
"completionNotes": "GET /dashboard served from embedded dashboard.html (package-data, no build step) by any tracker. Panels: hive/leader (raft status), nodes+coverage grouped by model, client balances, node pending + protocol cut, settlement history with devnet explorer links, strikes/bans/forfeitures (GET /v1/registry/wallets + snapshot forfeits), RPM stats. 4s auto-refresh via fetch polling. 3 tests in tests/test_dashboard.py." "completionNotes": "GET /dashboard served from embedded dashboard.html (package-data, no build step) by any tracker. Panels: hive/leader (raft status), nodes+coverage grouped by model, client balances, node pending + protocol cut, settlement history with devnet explorer links, strikes/bans/forfeitures (GET /v1/registry/wallets + snapshot forfeits), RPM stats. 4s auto-refresh via fetch polling. 3 tests in tests/test_dashboard.py."
},
{
"id": "US-036",
"title": "36 — Streamed chat completions over the relay RPC path",
"description": "Public NAT deployments proxy every chat request tracker → relay → head node. Implement true multi-frame SSE streaming over the relay WebSocket so clients see live tokens and relayed streams bill through the same SSE accounting loop as direct proxy streams. Inter-node /forward activation hops stay single-frame (ADR-0014).",
"acceptanceCriteria": [
"stream: true chat via relay delivers SSE chunks incrementally (≥2 distinct frame arrivals before [DONE])",
"Relayed streamed request records nonzero billed tokens and node work credit",
"Non-streamed relayed requests and /forward binary hops behave exactly as before (single frame, body_base64 intact)",
"Legacy single-frame response from an old node is accepted as terminal",
"Idle stream (no frame for 120 s) returns 504 and cleans up the relay-side queue",
"Extend tests/test_gossip_and_relay.py alongside test_relay_rpc_round_trips_http_request_to_peer",
"python -m pytest passes from repo root"
],
"priority": 36,
"status": "needs-review",
"notes": "Source issue: docs/issues/36-relay-streamed-chat.md. Implemented via _stream_relayed_frames in server.py; verify on public NAT relay before friends-test.",
"dependsOn": [
"US-029",
"US-031"
],
"completionNotes": "Multi-frame relay-http-response protocol; node relay_bridge line-by-line SSE emit; relay server per-request asyncio.Queue; tracker _stream_relayed_frames with SSE billing parity. Client mid-stream disconnect accepted limitation for alpha."
},
{
"id": "US-037",
"title": "37 — Concurrent request handling in the node relay bridge",
"description": "RelayHttpBridge currently handles relay-http-request envelopes serially, blocking up to 300 s per request. Off-LAN a node can be head of one route and downstream hop of another — overlapping routes through a shared node break. Dispatch on a bounded ThreadPoolExecutor (default 8, configurable) with per-frame WS send locking compatible with US-036 streaming.",
"acceptanceCriteria": [
"While one relayed request is in flight, a second relay-http-request to the same node completes without waiting for the first",
"Responses are correctly matched by request_id when they complete out of order",
"More than N simultaneous requests queue and all eventually complete; thread count never exceeds N workers",
"Bridge survives a relay reconnect with workers still in flight (no crash, no deadlock; orphaned responses dropped)",
"Configurable via meshnet-node start --relay-concurrency N (env MESHNET_RELAY_CONCURRENCY)",
"Extend tests/test_gossip_and_relay.py",
"python -m pytest passes from repo root"
],
"priority": 37,
"status": "done",
"notes": "Source issue: docs/issues/37-relay-bridge-concurrency.md. Critical for public friends-test; blocks concurrent head + hop on same node.",
"dependsOn": [
"US-036"
],
"completionNotes": "ThreadPoolExecutor dispatch in relay_bridge.py; per-frame WS send lock; test_relay_bridge_serves_concurrent_requests; --relay-concurrency CLI flag sets MESHNET_RELAY_CONCURRENCY."
},
{
"id": "US-038",
"title": "38 — Tracker cluster join via a single seed peer",
"description": "Tracker cluster membership is static today — a newcomer configured with only one existing peer is never learned by the rest of the hive and quorum math diverges. A joining tracker configured with any one live seed announces via hive-HMAC-signed POST /v1/cluster/join; membership changes replicate through the Raft log and persist across restarts.",
"acceptanceCriteria": [
"Start trackers A+B; start C with only A as seed → within one election timeout A, B, and C report the same 3-peer membership on GET /v1/cluster/peers, and a value proposed on C commits on A and B",
"Join without a valid hive signature is rejected with 403; join to a follower is forwarded to the leader",
"Restarting C with its seed offline rejoins from persisted membership",
"Standalone tracker (no seeds) behaves exactly as today",
"python -m pytest passes from repo root"
],
"priority": 38,
"status": "open",
"notes": "Source issue: docs/issues/38-tracker-seed-join.md. Out of scope: peer removal, joint consensus, automatic seed retry.",
"dependsOn": [
"US-013",
"US-017"
]
},
{
"id": "US-039",
"title": "39 — Caller Credit granted once per account; chat requires account keys",
"description": "DEFAULT_STARTING_CREDIT=0 and no grant path leaves every fresh public tracker request at 402. Grant Caller Credit once per account on first API key creation via deterministic event id caller-credit-{account_id}; chat on accounts-enabled trackers requires a real active sk-mesh- key (401 for invented bearers).",
"acceptanceCriteria": [
"Fresh account → first key → key has --starting-credit balance; chat succeeds",
"Second key on the same account → no additional credit",
"Revoke-and-recreate keys → still no additional credit (deterministic event id)",
"Random bearer string on an accounts-enabled tracker → 401, never 402/free work",
"Tracker without accounts store: gate behavior unchanged",
"--starting-credit 0 disables the grant entirely (mainnet posture)",
"python -m pytest passes from repo root"
],
"priority": 39,
"status": "done",
"notes": "Source issue: docs/issues/39-caller-credit-account-keys.md. Critical for friends-test inference.",
"dependsOn": [
"US-031",
"US-035"
],
"completionNotes": "Caller credit granted once per account on first API key via deterministic event id; tests/test_accounts.py covers grant, revoke, and invented-bearer rejection."
},
{
"id": "US-040",
"title": "40 — Devnet top-up button on the dashboard",
"description": "After Caller Credit (US-039) is spent, devnet friends need a dashboard faucet refill without on-chain USDT deposits. POST /v1/account/topup (session-authenticated) credits a configured fixed amount per click; flag off returns 404 and hides the button.",
"acceptanceCriteria": [
"Flag off: endpoint 404s, dashboard shows no top-up button",
"Flag on: logged-in user tops up own key, balance rises by exactly N",
"Topping up another account's key → 403",
"python -m pytest passes from repo root"
],
"priority": 40,
"status": "done",
"notes": "Source issue: docs/issues/40-devnet-dashboard-topup.md. Mainnet deployments set --devnet-topup 0.",
"dependsOn": [
"US-039"
],
"completionNotes": "POST /v1/account/topup with session auth and flag gating; tests/test_accounts.py covers flag off/on, own-account credit, and cross-account 403."
},
{
"id": "US-041",
"title": "41 — Account wallet: browser-extension signing, in-browser generation, export-only",
"description": "Accounts need a visible wallet for deposit attribution without the tracker ever holding private keys. Dashboard integrates Solana wallet-adapter connect+nonce proof, or in-browser keypair generation with one-time export; no private-key import endpoint.",
"acceptanceCriteria": [
"Connect-extension flow stores a verified pubkey (rejects unsigned/mismatched nonce proofs)",
"Generate flow: pubkey lands on the account; private key is never sent to the tracker, export works",
"No endpoint or UI accepts a private key",
"Deposits to the shown address credit the account's keys via the existing watcher",
"Address visible on the account panel after either flow",
"python -m pytest passes from repo root"
],
"priority": 41,
"status": "open",
"notes": "Source issue: docs/issues/41-account-wallet-keypair.md. Not needed for devnet friends test; needed before mainnet.",
"dependsOn": [
"US-032",
"US-039"
]
},
{
"id": "US-042",
"title": "42 — GGUF/llama.cpp node backend (phase C whole-model first)",
"description": "Node backend is transformers-only today; large MoE models on consumer hardware require GGUF via llama.cpp. Phase C: whole-model GGUF nodes (single-hop routes) first; partial-layer distributed GGUF deferred to ADR-0024. Also: GGUF catalog entries, Strix Halo/Vulkan hardware detection, download dir applies to GGUF files.",
"acceptanceCriteria": [
"A node with --gguf <repo-or-path> --quant IQ3_XXS serves /v1/chat/completions via llama.cpp with GPU offload where available",
"Tracker treats it as a full-coverage node (single-hop routes, billing works)",
"Streamed responses work through the tracker proxy and the relay (US-036)",
"python -m pytest passes from repo root (llama.cpp behind an optional extra)"
],
"priority": 42,
"status": "in-design",
"notes": "Source issue: docs/issues/42-gguf-llamacpp-node-backend.md. Phase C before ADR-0024 distributed worker; see runtime sequencing in issue file.",
"dependsOn": [
"US-036"
]
},
{
"id": "US-043",
"title": "43 — Dashboard model search and model cards",
"description": "Dashboard lacks model-centric discovery. Add server-side HF search proxy merged with tracker presets and live coverage; model cards show architecture, coverage gaps, pricing, memory per quant, and a request-this-model action. Featured section driven by CURATED_MODELS including GGUF once US-042 lands.",
"acceptanceCriteria": [
"Searching a HF repo id or free text returns results without the browser calling HF directly",
"A served model's card shows live coverage and a working chat-now state",
"An unserved model's card shows the request action and estimated memory per quant",
"python -m pytest passes from repo root"
],
"priority": 43,
"status": "open",
"notes": "Source issue: docs/issues/43-dashboard-model-search-cards.md. Post-deploy polish.",
"dependsOn": [
"US-035"
]
},
{
"id": "US-044",
"title": "44 — Tracker as model-file source; nodes download only their shard",
"description": "Second nodes joining a fleet today download entire HF snapshots even for small shard assignments. Tracker --models-dir advertises layer-scoped safetensors subsets; nodes race tracker/peer sources before HF allow_patterns fallback. Hard half remaining: meta-device partial model materialization so resident memory scales with shard size, not full model size.",
"acceptanceCriteria": [
"Tracker started with --models-dir / MESHNET_MODELS_DIR advertises local model-file sources in assignment responses",
"Tracker serves a tar stream (or per-file API) containing only safetensors files for the assigned layer range plus config/tokenizer/index metadata",
"Node downloader tries exact-shard peers, then tracker/peer file subsets, then HF snapshot_download with allow_patterns — never silently full-repo when layer index is available",
"Two-machine test: machine B receives only its assigned range from machine A — nothing fetched from HF",
"Machine B resident memory scales with its shard size, not model size",
"Checksums verified end-to-end; corrupted transfer falls back cleanly",
"Single-node/full-model flows unchanged",
"python -m pytest passes from repo root"
],
"priority": 44,
"status": "in-progress",
"notes": "Source issue: docs/issues/44-tracker-shard-source-partial-download.md. Download path and partial LOAD implemented; live two-machine LAN verification remains.",
"dependsOn": [
"US-004",
"US-012"
],
"completionNotes": "Tracker models-dir indexing, layer-scoped tar stream, HF allow_patterns client-side from remote index, per-file download API with retries, symlink dereference in tar writers. Partial LOAD via init_empty_weights + layer-scoped safetensors materialization; memory-scaling and checksum fallback tests pass. Remaining: live two-machine test (machine B receives only assigned files from A, no HF)."
},
{
"id": "US-045",
"title": "45 — Dual-rate billing: separate input and output token prices",
"description": "Ledger has one price_per_1k_tokens and stream vs non-stream paths disagree on input vs output counting. Charge both input and output tokens at separate rates per model; HF pricing refresher applies 80% of each marketplace side separately.",
"acceptanceCriteria": [
"Streamed and non-streamed requests for the same exchange bill the same split (input charged in both)",
"A model with asymmetric provider rates bills input and output differently; usage_for / billing events expose the split",
"Old persisted billing events replay byte-identically (balances unchanged)",
"HF refresh sets both rates from the marketplace row, not the average",
"Spend cap (--max-charge-per-request) uses the dual rates",
"python -m pytest passes from repo root"
],
"priority": 45,
"status": "in-progress",
"notes": "Source issue: docs/issues/45-dual-rate-billing.md. Billing correctness before friends test.",
"dependsOn": [
"US-031"
]
},
{
"id": "US-046",
"title": "46 — Tracker .env awareness + first-node auto-join bootstrap",
"description": "Fresh trackers return 503 on auto-join because deployability ignores the joining caller's hardware, and meshnet-tracker ignores .env MESHNET_DOWNLOAD_DIR. Fix empty-registry bootstrap, tracker env loading parity with node CLI, models-dir fallback chain, and tar dereference for HF symlink snapshots.",
"acceptanceCriteria": [
"Fresh tracker (empty registry) + caller with enough memory for a recommended preset → /v1/network/assign returns 200 with model_sources populated when tracker holds a local snapshot",
"Fresh tracker + caller too small for any recommended preset → still 503",
"meshnet-tracker start in a directory with .env setting MESHNET_DOWNLOAD_DIR serves /v1/model-files/download from that dir with no extra flags",
"Explicit --models-dir and MESHNET_MODELS_DIR still take precedence",
"python -m pytest passes from repo root"
],
"priority": 46,
"status": "needs-review",
"notes": "Source issue: docs/issues/46-tracker-env-and-first-node-autojoin.md. Verified live 2026-07-06.",
"dependsOn": [
"US-044"
],
"completionNotes": "Empty-registry synthesizes caller as candidate node; tracker loads .env; models_dir falls back MESHNET_MODELS_DIR → MESHNET_DOWNLOAD_DIR; tar dereference=True. Pytest passes aside from known port-7000 env conflict."
},
{
"id": "US-047",
"title": "47 — Tracker-first model downloads: visibility, sane timeouts, RAM-based sizing",
"description": "Explicit --model startup should skip pointless auto-join; tracker/peer sources preferred over HF with visible progress and 30 s socket timeouts; client abort during tar stream logs one line; CPU nodes size shards from RAM not phantom GPU VRAM; per-file downloads for robustness over fragile multi-GB tar streams.",
"acceptanceCriteria": [
"Node started with explicit --model never queries /v1/network/assign and never prints auto-join unavailable",
"Tracker/peer model source preferred; HF only when no source, all sources fail, or --tracker-source-disabled",
"Tracker-source downloads print progress every 512 MB and print exception + URL on failure",
"A ≥2 s read stall no longer aborts a tracker model-source download (30 s socket timeout)",
"Client disconnect during /v1/model-files/download logs one line on the tracker, no traceback",
"CPU node with big RAM gets a RAM-sized shard: sizing paths ignore VRAM unless device=cuda",
"Live two-machine retest: Windows node downloads from tracker at LAN speed with RAM-sized shard assignment",
"python -m pytest passes from repo root"
],
"priority": 47,
"status": "in-progress",
"notes": "Source issue: docs/issues/47-model-source-download-visibility.md. Engineering largely complete 2026-07-06; live two-machine retest pending.",
"dependsOn": [
"US-044",
"US-046"
],
"completionNotes": "Skip auto-join when model explicit; sequential source try with progress; 30 s model-source timeout; assignment_vram_mb for CPU; per-file /v1/model-files/download with manifest and retries. Remaining: live Windows two-machine retest."
},
{
"id": "US-049",
"title": "49 — Mainnet USDT cutover: two-wallet pilot checklist",
"description": "Operator runbook to move from Solana devnet + mock-USDT to mainnet + real USDT for a minimal pilot: one client wallet (deposits USDT, pays for inference) and one node wallet (receives batched payouts). Treasury holds USDT float and SOL for fees. TAI deferred per ADR-0025.",
"acceptanceCriteria": [
"Devnet smoke completed: settlement loop pays with --settle-period 60 and mock mint",
"python -m pytest tests/test_settlement_loop.py -q passes (includes 24 h / 5 USDT gate tests)",
"Mainnet tracker configured: real USDT mint, --starting-credit 0, --devnet-topup 0, --settle-period 86400",
"Client wallet deposit credits API key ledger; inference debits balance",
"Node wallet receives at least one confirmed mainnet USDT payout batch",
"Sub-threshold pending not paid before 24 h; ≥5 USDT pending triggers immediate payout"
],
"priority": 49,
"status": "open",
"notes": "Source issue: docs/issues/49-mainnet-usdt-cutover-two-wallet-pilot.md. ADR-0025 covers optional TAI mint; not blocking this pilot.",
"dependsOn": [
"US-032",
"US-033",
"US-039"
]
},
{
"id": "US-048",
"title": "48 — Memory budget, shard slots, and dropout relocation hardening",
"description": "Harden the capacity contract around US-013 coverage-first assignment: enforce memory budget and max_loaded_shards in rebalance/dropout relocation, and decide whether one node may host multiple concurrent shard backends or max_loaded_shards remains metadata until runtime support lands.",
"acceptanceCriteria": [
"Assignment/rebalance never exceeds memory budget or max_loaded_shards",
"Dropout test restores full coverage without violating capacity limits",
"CLI --memory and --max-shards reflected in registration payload",
"python -m pytest tests/test_tracker_routing.py tests/test_node_startup.py passes"
],
"priority": 48,
"status": "open",
"notes": "Source issue: docs/issues/48-memory-budget-shard-slots-and-dropout-relocation.md. Renumbered from duplicate slot 20. Enables spare shard slots for ADR-0026 managed placement.",
"dependsOn": [
"US-013"
]
},
{
"id": "US-050",
"title": "50 — Qwen3.6-27B demand-driven managed placement",
"description": "Offer pinned Qwen/Qwen3.6-27B as a recommended text-only chat model. Valid chat requests prove demand; when spare fleet capacity exists, the tracker assigns managed nodes to reach complete coverage. Pinned startup assignments remain immutable per ADR-0026; NCA admission required before routable.",
"acceptanceCriteria": [
"First valid request for an uncovered variant queues demand and returns 503 model_loading until complete validated coverage exists",
"Managed assignments use only spare capacity and carry managed: true",
"Pinned startup assignments are never silently retargeted",
"Optional quantization field (bfloat16/int8/nf4) with coverage-vote UI semantics per scratch PRD",
"python -m pytest passes from repo root"
],
"priority": 50,
"status": "in-design",
"notes": "Source: .scratch/qwen3.6-27b-demand-placement/PRD.md and docs/issues/50-qwen3.6-27b-demand-placement.md. Reconciled with ADR-0026 and ADR-0023.",
"dependsOn": [
"US-035",
"US-048"
]
} }
], ],
"metadata": { "metadata": {
"updatedAt": "2026-07-01T00:00:00.000Z", "updatedAt": "2026-07-13T17:00:00.000Z",
"statusVocabulary": { "statusVocabulary": {
"open": "Not started", "open": "Not started",
"in-design": "Decisions pending before implementation can begin", "in-design": "Decisions pending before implementation can begin",

View File

@@ -36,6 +36,12 @@ def _load_env_file(path: Path) -> None:
os.environ[key] = value os.environ[key] = value
def _apply_relay_concurrency_flag(value: int | None) -> None:
"""Expose relay bridge worker cap via CLI (env MESHNET_RELAY_CONCURRENCY)."""
if value is not None:
os.environ["MESHNET_RELAY_CONCURRENCY"] = str(max(1, value))
def _load_env_defaults() -> None: def _load_env_defaults() -> None:
"""Load machine-specific, local, and user-level node env defaults.""" """Load machine-specific, local, and user-level node env defaults."""
machine = socket.gethostname().strip() machine = socket.gethostname().strip()
@@ -189,6 +195,8 @@ def _cmd_default(args) -> int:
if getattr(args, "cpu", False): if getattr(args, "cpu", False):
overrides["force_cpu"] = True overrides["force_cpu"] = True
_apply_relay_concurrency_flag(getattr(args, "relay_concurrency", None))
if overrides: if overrides:
cfg = merge_cli_overrides(cfg, **overrides) cfg = merge_cli_overrides(cfg, **overrides)
@@ -349,6 +357,8 @@ def _cmd_start(args) -> int:
if getattr(args, "node_name", None): if getattr(args, "node_name", None):
cfg["node_name"] = args.node_name cfg["node_name"] = args.node_name
_apply_relay_concurrency_flag(getattr(args, "relay_concurrency", None))
# Legacy start: just run without the dashboard (keep original blocking loop) # Legacy start: just run without the dashboard (keep original blocking loop)
from .startup import run_startup from .startup import run_startup
@@ -433,6 +443,8 @@ def main() -> None:
help="Set PyTorch inter-op CPU worker threads") help="Set PyTorch inter-op CPU worker threads")
parser.add_argument("--cpu", action="store_true", parser.add_argument("--cpu", action="store_true",
help="Force CPU inference even when a GPU is available") help="Force CPU inference even when a GPU is available")
parser.add_argument("--relay-concurrency", type=int, metavar="N",
help="Max concurrent relay-http-request workers (env MESHNET_RELAY_CONCURRENCY)")
parser.add_argument("--debug", action="store_true", help="Enable verbose node debug logging") parser.add_argument("--debug", action="store_true", help="Enable verbose node debug logging")
parser.add_argument("--no-tui", action="store_true", help="Plain-text output (no rich dashboard)") parser.add_argument("--no-tui", action="store_true", help="Plain-text output (no rich dashboard)")
parser.add_argument("--compact", action="store_true", help="Single-line status output") parser.add_argument("--compact", action="store_true", help="Single-line status output")
@@ -510,6 +522,8 @@ def main() -> None:
help="Set PyTorch inter-op CPU worker threads") help="Set PyTorch inter-op CPU worker threads")
start_cmd.add_argument("--cpu", action="store_true", start_cmd.add_argument("--cpu", action="store_true",
help="Force CPU inference even when a GPU is available") help="Force CPU inference even when a GPU is available")
start_cmd.add_argument("--relay-concurrency", type=int, metavar="N",
help="Max concurrent relay-http-request workers (env MESHNET_RELAY_CONCURRENCY)")
start_cmd.add_argument("--debug", action="store_true", help="Enable verbose node debug logging") start_cmd.add_argument("--debug", action="store_true", help="Enable verbose node debug logging")
start_cmd.add_argument("--tracker-source-disabled", action="store_true", start_cmd.add_argument("--tracker-source-disabled", action="store_true",
help="Skip tracker/peer model-file sources and download from HuggingFace directly") help="Skip tracker/peer model-file sources and download from HuggingFace directly")

View File

@@ -2,6 +2,7 @@
import json import json
import os import os
import shutil
import subprocess import subprocess
import time import time
@@ -183,6 +184,17 @@ def with_forced_cpu(hw: dict) -> dict:
return forced return forced
def _with_model_drive(profile: dict) -> dict:
"""Attach free space for the default model cache drive to tracker diagnostics."""
try:
cache_root = os.path.expanduser("~/.cache/meshnet/shards")
profile["model_drive_free_bytes"] = shutil.disk_usage(os.path.expanduser("~")).free
profile["model_drive_path"] = cache_root
except OSError:
pass
return profile
def detect_hardware() -> dict: def detect_hardware() -> dict:
"""Detect GPU model and available VRAM. Returns hardware profile dict.""" """Detect GPU model and available VRAM. Returns hardware profile dict."""
ram_mb = _detect_ram_mb() ram_mb = _detect_ram_mb()
@@ -208,23 +220,23 @@ def detect_hardware() -> dict:
} }
if torch_gpu is not None and torch_gpu.get("gcn_arch"): if torch_gpu is not None and torch_gpu.get("gcn_arch"):
profile["gcn_arch"] = torch_gpu["gcn_arch"] profile["gcn_arch"] = torch_gpu["gcn_arch"]
return profile return _with_model_drive(profile)
except ImportError: except ImportError:
pass pass
torch_inventory = _gpu_inventory_profile(torch_gpu, ram_mb) torch_inventory = _gpu_inventory_profile(torch_gpu, ram_mb)
if torch_inventory is not None: if torch_inventory is not None:
return torch_inventory return _with_model_drive(torch_inventory)
nvidia_gpu = _gpu_inventory_profile(_detect_nvidia_smi_gpu_memory(), ram_mb) nvidia_gpu = _gpu_inventory_profile(_detect_nvidia_smi_gpu_memory(), ram_mb)
if nvidia_gpu is not None: if nvidia_gpu is not None:
return nvidia_gpu return _with_model_drive(nvidia_gpu)
windows_gpu = _gpu_inventory_profile(_detect_windows_gpu_memory(), ram_mb) windows_gpu = _gpu_inventory_profile(_detect_windows_gpu_memory(), ram_mb)
if windows_gpu is not None: if windows_gpu is not None:
return windows_gpu return _with_model_drive(windows_gpu)
return { return _with_model_drive({
"device": "cpu", "device": "cpu",
"gpu_name": None, "gpu_name": None,
"vram_mb": 0, "vram_mb": 0,
@@ -232,7 +244,7 @@ def detect_hardware() -> dict:
"shared_vram_mb": 0, "shared_vram_mb": 0,
"ram_mb": ram_mb, "ram_mb": ram_mb,
"cuda_available": False, "cuda_available": False,
} })
def benchmark_throughput_checked(device_str: str = "cpu") -> tuple[float, bool, str | None]: def benchmark_throughput_checked(device_str: str = "cpu") -> tuple[float, bool, str | None]:

Some files were not shown because too many files have changed in this diff Show More