feat(claude): consolidate notes and matej skills into lead

This commit is contained in:
2026-10-18 19:15:00 +00:00
parent 2c60e23ac2
commit cf2962789b
11 changed files with 1720 additions and 1154 deletions

103
claude/skills/lead/SKILL.md Normal file
View File

@@ -0,0 +1,103 @@
---
name: lead
description: How Matej likes larger work run and its notes kept - a lead session that talks to him and briefs agents, an integrator that hands back a checked branch, builders, researchers and explorers, and the project's local .notes/ (docs, plans, work handoffs, follow-ups, his parked questions). Use when he asks for orchestrated work ("let the integrator handle it", "spawn agents for this", options to compare), for work spanning several branches or sessions, or whenever writing, handing off, closing out or settling anything in .notes/, or when he asks what is pending.
---
# Running work through agents
Guidelines, not a procedure. What is written here is what isn't knowable from the repo.
## Roles
- **Lead**: the session Matej talks to. Discusses, locks decisions, keeps the notes, briefs agents, checks their replies. It doesn't edit the project, so its context stays free for judgement. One work item per lead session: at a natural break it hands off through the work folder and Matej starts a new session, which starts with `notes.py status`. Its replies state outcomes and decisions and what it took on trust, and point Matej at the work item's page, whose `## Your move` says what he does next; detail stays in the notes.
- **Integrator** (`integrator`): owns a unit of work (a phase, or the whole item when it has one part) from brief to one branch that works. It merges its builders' parts into its own branch, never its branch into the work branch or `main` (see Git).
- Parts that don't step on each other go to builders in parallel, at most 2–3 at a time, each on its own branch and worktree under the work id. What everything else builds on (a shared shell, a schema) goes first, alone. Small or tightly coupled work it does itself.
- It reads each part's diff, fixes what is real or sends it to a fresh builder, condenses the part's history, and merges it with `--no-ff`, sorting out what parallel parts collide on (migrations, generated code).
- On the integrated branch, once: code-review and simplify, every check the project has (see Tests), and a browser look when there is UI.
- It removes its builders' branches, worktrees and containers once merged, and its own when it hands back.
- **Builders** (`builder-sonnet`, `builder-opus`): one part each, from the integrator's brief.
- **Researchers** (`researcher-sonnet`, `researcher-opus`): a verified answer with sources.
- **Explorers** (`explorer`): an option Matej can inspect. Prototypes may cut corners; they never land as they are. A prototype branch ends in `-prototype` (`wNNN/<name>-prototype`), so `notes.py status` doesn't count it as work to merge.
## How many agents
One agent per question. Two independent ones only for security and quality reviews, and for a decision Matej flags as core. Any agent that runs others runs at most 2–3. A sharp brief to one researcher beats a manager with a team.
## Models
The pair types carry their model in their frontmatter; never pass a `model` when spawning them, it overrides the frontmatter. The lead picks researchers, the integrator picks builders, by the type descriptions: Sonnet for fully specified, few files, an existing pattern, fast checks (builders) or a clear answer in known sources with a short scripted check (researchers); Opus for debugging, VMs or containers, security, open design, shared code, measurements, comparisons, core decisions, or when unsure. A Sonnet agent that fails its checks twice, gets stuck or comes back thin is replaced by a new Opus agent, briefed from its branch or reply. Integrator and explorer run on the default model.
## Protocol
- Discuss, lock, relay. Only locked decisions go to agents, in the words agreed. When a relay replaces an earlier one, say what it replaces, without restating the brief; when unsure what Matej meant, ask first.
- Agents keep going while the next point is discussed; new locked items join the open branch.
- Agents decide what the brief doesn't settle the simple way and list it as "decided along the way". Only the lead asks Matej.
- Once an agent has replied, it is done. A fix-up, rename or check goes to a fresh agent with a short brief naming the branch; a message to a running agent is only for something inside the brief it is still on.
## Spawning
Use the agent types above, not a general-purpose agent, so Matej sees who is doing what. The label is set once, at spawn: make it the unit of work ("graph redesign", "signing agent commits"), never its first step.
## A brief carries
The goal and what done looks like; where the spec is, as note ids and sections: brief sections and at most one report section, never whole reports, folders or "everything it points to"; the decisions that touch it; scope and what not to touch; the checks with the test scope for the role; the reply shape when it differs from the agent's; what it may create and must clean up. Agents don't see the lead's memory: what they need from it goes in the brief. The standing rules (shell wrappers, `rm` paths, stash, tool state, refusals, quiet output, waiting on jobs, reply shape) are in the ground-rules skill, preloaded by every agent type; briefs don't restate them.
## Tests: who runs what
- **While building a part**: only the tests for what was touched (new tests, the changed modules' test files, tests that import them), plus lint and format on the changed files. Name the selection in the reply.
- **Before a part is merged**: the integrator runs the whole suite of each project member the part touched; live or slow tests (VMs, external services, browsers) only when the part touches what they cover.
- **Once, before reporting**: the integrator runs every check the project lists, on the integrated branch, as CI does. The reply's numbers come from this run.
- **The lead never re-runs suites.** It checks the cheap facts, and re-runs something only when a claim looks off.
## Before passing a reply on
Check the cheap facts: branch heads, commit authors and subject lengths, nothing staged that shouldn't be, worktrees and containers gone. Say what you checked.
## Tuning this flow
A change to this skill, the agent definitions or the notes conventions is kept with its measured difference: replay a finished task on its frozen commit with the old and the new files and compare tokens and outcome from the transcripts. Each kept change gets a line in the work item's as-built doc.
## Where things go
- Plans, decisions, replies: the work item in `.notes/` (see Notes). Its brief holds the current state: a replaced decision is edited in place, git keeps the old one. Agents can't write there; the lead files each reply with `notes.py file` when the notification arrives, and never retypes it.
- Leftovers: `followups/`, by area, at close-out. `questions/` is Matej's own.
- Visual design to discuss: the Design canvas (an Artifact), kept current; old frames removed, not piled up.
- Prototypes and finished work: branches. The repo's `.git` is on Matej's host, so he can `git worktree add --detach` any branch.
- Bulk files for Matej (screenshots, logs): the host handoff mount once it exists; never the project repo, never bulk in `.notes/`.
## Notes
Plans, decisions, work handoffs, follow-ups and Matej's questions live in the project's `.notes/`. Before writing, closing out or settling anything there, read `notes.md` next to this file: layout, ids, the `notes.py` commands (`~/.config/claude/skills/lead/notes.py`), decide blocks, close-out.
## Ownership and clean-up
- Everything a unit of work creates carries its work id: branches `<wNNN>/<name>` (see Git), worktrees under `$SANDBOX_VM_STATE/wt/<wNNN>/`, containers `<wNNN>-<name>`. Agents remove what they created and nothing else.
- The lead keeps `made.md` in the work item: each branch, worktree, container and handoff folder, with its state (open, merged, prototype); whether a branch is in main, `notes.py status` reads from git.
- Close-out removes the list, then checks nothing under the work id is left.
## Interruptions
Any agent can stop (usage limits, Matej stopping it) and can't be resumed. Keep state on disk: small commits, decisions and replies in the work item. Before re-briefing, look at the repo and hand the new agent what is actually there.
## Git
Agents never sign; Matej re-signs on his host before pushing (CLAUDE.md, Git).
```
main
└─ wNNN/<work> the work branch, off main; Matej merges it into main
├─ wNNN/phase1 an integrator's branch, off wNNN/<work>
│ ├─ wNNN/phase1-api a builder's part, off wNNN/phase1
│ └─ wNNN/phase1-ui
└─ wNNN/phase2 off wNNN/<work> once phase1 is merged into it
```
- Each level branches from the one above and goes back with a merge commit (`merge: <what>`, unsigned), so every phase and part stays one merge in the history.
- `<work>` is a short kebab name, usually the item's slug. Never plain `wNNN`, and phases and parts are siblings under `wNNN/`, never under `wNNN/<work>/`: git can't hold a branch next to branches below it. The lead creates the work branch off `main` when the first phase is briefed.
- One part, no phases: the integrator's branch is `wNNN/<work>` itself.
- Parts: the integrator merges each into its phase branch with `git merge --no-ff`.
- Phases: once the lead has checked a phase's reply, it merges the phase branch into `wNNN/<work>` with `--no-ff` and deletes it, right away; the next phase branches off that merge commit, never off the previous phase's tip. A conflict goes to an integrator; the lead doesn't resolve code.
- A phase that must start before the previous one is merged branches off its tip, then is rebased onto the merge commit (`git rebase --onto <merge> <previous tip>`) before it is merged.
- A work item built on another one not yet in `main` starts from that one's work branch tip; once Matej merges it (his signing rewrites the shas), the lead rebases with `--rebase-merges` onto that merge commit.
- Two branches that conflict (a refactor and a fix pass on the same code) aren't resolved hunk by hunk. An integrator, for each conflicting part, picks the better implementation (more correct, tested, fits the code) and says why; adopts it; rebuilds the other branch's remaining intent as new commits on a fresh branch off the winner; runs every check; and lists what was adopted, rebuilt and dropped. The lead only picks which pairs need it.
- Only Matej merges into `main`.

View File

@@ -0,0 +1,980 @@
/*
* notes.css: the one stylesheet for every page in a project's .notes/
* notes.py copies it to .notes/notes.css; edit it here, in the lead skill, never in a project
*
* gruvbox material (sainnhe/gruvbox-material: medium contrast, material foreground), light or dark
* by the system scheme; jetbrains mono for all text
*
* page skeleton for a doc (docs/dNNN-slug.html; one level deeper for a folder doc):
* <link rel="stylesheet" href="../notes.css">
* <body><main class="page">
* <nav class="back"><a href="../index.html">← notes</a></nav>
* <header>
* <p class="eyebrow">d007 · 2026-10-13</p>
* <h1>Title</h1>
* <p class="status current|stale|superseded">status: …</p>
* <p class="lede">one or two sentences</p>
* </header>
* <section><h2>…</h2> …</section>
* </main></body>
*
* components:
* .callout (+ .good .warn .bad .info) a boxed note, recommendation, open point
* .tag (+ .good .warn .bad .info .muted) a small inline label
* .decide > .option(.recommended .chosen) two options left and right, one column on a phone;
* a markdown ```decide block renders it, see the skill
* .table > table a table that scrolls sideways when wide
* figure.diagram > svg (+ figcaption) an inline svg diagram, see "diagrams" below
* dl.facts > dt + dd label and value pairs in two columns (repro, cause, fix)
* pre.diff > span(.add .del) a unified diff, one span per line
* .legend > span > svg + text a row of notation keys above a diagram
* .swatch (+ .info .good .warn .bad) a small colour square that matches a diagram box
* main.page.wide a wider page, for pages carried by wide diagrams
* .sub, .muted secondary text
* .fix a line prefixed with an arrow
*/
@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap");
:root {
color-scheme: light dark;
--radius: 0.375rem;
/* gruvbox material light */
--red: #c14a4a;
--orange: #c35e0a;
--yellow: #b47109;
--green: #6c782e;
--aqua: #4c7a5d;
--blue: #45707a;
--purple: #945e80;
--background: #fbf1c7;
--foreground: #654735;
--card: #f4e8be;
--muted: #eee0b7;
--muted-foreground: #7c6f64;
--border: #ddccab;
--input: #a89984;
--link: var(--blue);
/* text colours and their tints, both follow the scheme's palette */
--good: var(--green);
--warn: var(--yellow);
--bad: var(--red);
--info: var(--blue);
--good-bg: color-mix(in srgb, var(--green) 14%, transparent);
--warn-bg: color-mix(in srgb, var(--yellow) 16%, transparent);
--bad-bg: color-mix(in srgb, var(--red) 13%, transparent);
--info-bg: color-mix(in srgb, var(--blue) 13%, transparent);
--accent: var(--orange);
--accent-bg: color-mix(in srgb, var(--orange) 12%, transparent);
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
}
@media (prefers-color-scheme: dark) {
:root {
/* gruvbox material dark */
--red: #ea6962;
--orange: #e78a4e;
--yellow: #d8a657;
--green: #a9b665;
--aqua: #89b482;
--blue: #7daea3;
--purple: #d3869b;
--background: #282828;
--foreground: #d4be98;
--card: #32302f;
--muted: #45403d;
--muted-foreground: #a89984;
--border: #45403d;
--input: #5a524c;
--good-bg: color-mix(in srgb, var(--green) 16%, transparent);
--warn-bg: color-mix(in srgb, var(--yellow) 15%, transparent);
--bad-bg: color-mix(in srgb, var(--red) 16%, transparent);
--info-bg: color-mix(in srgb, var(--blue) 16%, transparent);
--accent-bg: color-mix(in srgb, var(--orange) 15%, transparent);
}
}
/* base */
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
scrollbar-gutter: stable;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
background: var(--background);
color: var(--foreground);
font: 400 14px/1.65 var(--font-mono);
-webkit-font-smoothing: antialiased;
}
main.page {
max-width: 860px;
margin: 0 auto;
padding: 24px 16px 96px;
}
main.page.wide {
max-width: 1240px;
}
a {
color: var(--link);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a:focus-visible {
outline: 2px solid var(--blue);
outline-offset: 2px;
border-radius: 3px;
}
h1,
h2,
h3,
h4 {
font-weight: 600;
line-height: 1.25;
text-wrap: balance;
margin: 0;
}
h1 {
font-size: 26px;
}
h2 {
font-size: 19px;
margin: 44px 0 12px;
padding-top: 24px;
border-top: 1px solid var(--border);
}
h3 {
font-size: 16px;
margin: 28px 0 8px;
}
h4 {
font-size: 15px;
margin: 20px 0 6px;
}
section > h2:first-child {
margin-top: 40px;
}
p,
ul,
ol,
blockquote,
pre,
figure,
.table,
.callout,
.decide,
details {
margin: 0 0 14px;
}
ul,
ol {
padding-left: 22px;
}
li + li {
margin-top: 4px;
}
li > ul,
li > ol {
margin: 4px 0 0;
}
strong,
b {
font-weight: 600;
}
hr {
border: 0;
border-top: 1px solid var(--border);
margin: 32px 0;
}
blockquote {
padding: 2px 0 2px 16px;
border-left: 3px solid var(--border);
color: var(--muted-foreground);
}
img {
max-width: 100%;
height: auto;
border-radius: var(--radius);
border: 1px solid var(--border);
}
code,
kbd,
pre,
.mono {
font-family: var(--font-mono);
font-size: 0.93em;
}
code {
background: var(--muted);
padding: 1px 5px;
border-radius: 4px;
overflow-wrap: break-word;
}
pre {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 12px 14px;
overflow-x: auto;
line-height: 1.5;
}
pre code {
background: none;
padding: 0;
font-size: 1em;
overflow-wrap: normal;
}
/* page chrome */
nav.back {
font-size: 13px;
margin-bottom: 28px;
}
nav.back a {
color: var(--muted-foreground);
}
nav.back a:hover {
color: var(--link);
}
header {
margin-bottom: 8px;
}
header > * + * {
margin-top: 10px;
}
.eyebrow {
font: 500 12px/1.4 var(--font-mono);
letter-spacing: 0.04em;
color: var(--muted-foreground);
margin: 0;
}
.lede {
font-size: 17px;
line-height: 1.6;
margin-top: 14px;
}
.sub,
.muted {
color: var(--muted-foreground);
}
p.status {
display: inline-block;
font-size: 13px;
line-height: 1.5;
padding: 3px 10px;
border-radius: 6px;
background: var(--muted);
color: var(--muted-foreground);
}
p.status.current {
background: var(--good-bg);
color: var(--good);
}
p.status.stale {
background: var(--warn-bg);
color: var(--warn);
}
.fix::before {
content: "→ ";
color: var(--accent);
font-weight: 600;
}
/* components */
.callout {
background: var(--card);
border: 1px solid var(--border);
border-left: 3px solid var(--input);
border-radius: var(--radius);
padding: 10px 14px;
}
.callout > :last-child {
margin-bottom: 0;
}
.callout.good {
background: var(--good-bg);
border-color: transparent;
border-left-color: var(--good);
}
.callout.warn {
background: var(--warn-bg);
border-color: transparent;
border-left-color: var(--warn);
}
.callout.bad {
background: var(--bad-bg);
border-color: transparent;
border-left-color: var(--bad);
}
.callout.info {
background: var(--info-bg);
border-color: transparent;
border-left-color: var(--info);
}
.tag {
display: inline-block;
font: 500 11px/1.5 var(--font-mono);
letter-spacing: 0.02em;
padding: 1px 8px;
border-radius: 99px;
white-space: nowrap;
vertical-align: 1px;
background: var(--muted);
color: var(--muted-foreground);
}
.tag.good {
background: var(--good-bg);
color: var(--good);
}
.tag.warn {
background: var(--warn-bg);
color: var(--warn);
}
.tag.bad {
background: var(--bad-bg);
color: var(--bad);
}
.tag.info {
background: var(--info-bg);
color: var(--info);
}
/* two options side by side, the only side by side a page has */
.decide {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
@media (max-width: 720px) {
.decide {
grid-template-columns: minmax(0, 1fr);
}
}
.decide + .decide-paste {
margin-top: -6px;
font-size: 13px;
}
.option {
display: flex;
flex-direction: column;
align-items: flex-start;
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 12px 14px;
}
.option.recommended {
border-color: var(--good);
}
.option.chosen {
border-color: var(--info);
box-shadow: inset 3px 0 0 var(--info);
}
.option > p,
.option > ul {
margin-bottom: 8px;
}
.option > :last-child {
margin-bottom: 0;
}
.option-head .tag {
margin-left: 6px;
}
.option button {
margin-top: auto;
font: 500 13px/1.4 var(--font-mono);
color: var(--foreground);
background: var(--muted);
border: 1px solid var(--input);
border-radius: var(--radius);
padding: 5px 12px;
cursor: pointer;
}
.option button:hover {
border-color: var(--accent);
}
.option button:focus-visible {
outline: 2px solid var(--blue);
outline-offset: 2px;
}
.table {
overflow-x: auto;
}
table {
border-collapse: collapse;
width: 100%;
font-size: 14px;
}
th,
td {
text-align: left;
vertical-align: top;
padding: 7px 10px;
border-bottom: 1px solid var(--border);
}
th {
font-weight: 600;
font-size: 13px;
color: var(--muted-foreground);
}
td > :last-child {
margin-bottom: 0;
}
td .tag {
white-space: normal;
}
td code {
overflow-wrap: anywhere;
}
details {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 8px 14px;
}
summary {
cursor: pointer;
font-weight: 500;
}
details[open] > summary {
margin-bottom: 8px;
}
dl.facts {
display: grid;
grid-template-columns: max-content minmax(0, 1fr);
gap: 6px 16px;
margin: 0 0 14px;
}
dl.facts dt {
color: var(--muted-foreground);
font-weight: 500;
}
dl.facts dd {
margin: 0;
}
@media (max-width: 560px) {
dl.facts {
grid-template-columns: minmax(0, 1fr);
gap: 0;
}
dl.facts dd + dt {
margin-top: 8px;
}
}
/* one column as wide as the longest line, so every line's tint spans it when scrolled */
pre.diff {
display: grid;
grid-template-columns: minmax(max-content, 1fr);
}
pre.diff span {
white-space: pre;
}
pre.diff .add {
background: var(--good-bg);
color: var(--good);
}
pre.diff .del {
background: var(--bad-bg);
color: var(--bad);
}
.legend {
display: flex;
flex-wrap: wrap;
gap: 8px 22px;
margin: 0 0 14px;
font-size: 13px;
color: var(--muted-foreground);
}
.legend > span {
display: inline-flex;
align-items: center;
gap: 8px;
}
.legend svg {
width: 34px;
height: 20px;
flex: none;
}
.swatch {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 3px;
border: 1px solid var(--muted-foreground);
background: var(--card);
vertical-align: 0;
}
.swatch.info {
background: color-mix(in srgb, var(--blue) 14%, var(--card));
border-color: var(--blue);
}
.swatch.good {
background: color-mix(in srgb, var(--green) 14%, var(--card));
border-color: var(--green);
}
.swatch.warn {
background: color-mix(in srgb, var(--yellow) 18%, var(--card));
border-color: var(--yellow);
}
.swatch.bad {
background: color-mix(in srgb, var(--red) 14%, var(--card));
border-color: var(--red);
}
/*
* diagrams: inline svg inside figure.diagram, drawn with these classes so both schemes work
* <g class="node [accent|dashed|good|bad|warn]"><rect …/><text …>Label</text><text class="sub" …>detail</text></g>
* <path class="edge [back|good|bad|warn]" … marker-end="url(#arrow)"/>
* <path class="arrow [back|good|bad|warn]" d="M0 0 L10 5 L0 10 z"/> inside a <marker>
* <text class="edge-label [back|good|bad|warn]" …>label</text>
* <rect class="group" …/> and <text class="group-label" …> a dashed box around nodes
* for drawings with their own text placement (a data flow, a sequence), classes go on the shapes:
* <rect class="box [info|good|warn|bad|accent] [strong] [dashed] [open]"/> a step, process or entity
* <rect class="zone [info|good|warn|bad|accent] [boundary]"/> a tinted dashed area; boundary strokes it red
* <rect class="panel"/> a quiet solid area behind a group of boxes
* <rect class="chip [good|accent]"/> + <text class="chip-text"> a small filled badge or numbered dot
* <path class="line"/> a plain stroke (a data store's rails) <line class="rule"/> a faint dashed guide
* <rect class="fill"/> a borderless card-coloured shape: a data store's body, a backdrop
* <text class="title|sub|mono|heading [info|good|warn|bad]"> text roles; text-anchor stays an attribute
* .edge.dashed an answer or a nudge, not a main flow
* figure.diagram.wide svg at least 1000px wide, scrolls sideways on a phone
* an svg may also use currentColor and var(--good|--bad|--warn|--accent) in fill and stroke attributes
*/
figure {
margin-left: 0;
margin-right: 0;
}
figure.diagram {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 14px;
overflow-x: auto;
color: var(--foreground);
}
figure.diagram svg {
display: block;
width: 100%;
height: auto;
min-width: 640px;
font-family: var(--font-mono);
}
figure.diagram.wide svg {
min-width: 1000px;
}
figure.diagram text {
fill: var(--foreground);
}
figcaption {
color: var(--muted-foreground);
font-size: 13px;
margin-top: 8px;
}
.node rect,
.node circle,
.node ellipse,
.node polygon {
fill: var(--card);
stroke: var(--muted-foreground);
stroke-width: 1;
}
.node.accent rect,
.node.accent circle,
.node.accent polygon {
fill: var(--accent-bg);
stroke: var(--accent);
stroke-width: 1.5;
}
.node.dashed rect,
.node.dashed circle,
.node.dashed polygon {
stroke-dasharray: 5 4;
}
.node.good rect {
fill: var(--good-bg);
stroke: var(--good);
}
.node.bad rect {
fill: var(--bad-bg);
stroke: var(--bad);
}
.node.warn rect {
fill: var(--warn-bg);
stroke: var(--warn);
}
.node text {
fill: var(--foreground);
font: 500 14px var(--font-mono);
text-anchor: middle;
}
.node text.sub {
fill: var(--muted-foreground);
font: 400 11px var(--font-mono);
}
.edge {
fill: none;
stroke: var(--muted-foreground);
stroke-width: 1.4;
}
.arrow {
fill: var(--muted-foreground);
}
.edge-label {
fill: var(--muted-foreground);
font: 400 11px var(--font-mono);
text-anchor: middle;
paint-order: stroke;
stroke: var(--card);
stroke-width: 4px;
stroke-linejoin: round;
}
.edge.back {
stroke: var(--accent);
stroke-dasharray: 5 4;
}
.arrow.back,
.edge-label.back {
fill: var(--accent);
}
.edge.good {
stroke: var(--good);
}
.arrow.good,
.edge-label.good {
fill: var(--good);
}
.edge.bad {
stroke: var(--bad);
}
.arrow.bad,
.edge-label.bad {
fill: var(--bad);
}
.edge.warn {
stroke: var(--warn);
}
.arrow.warn,
.edge-label.warn {
fill: var(--warn);
}
.group {
fill: none;
stroke: var(--muted-foreground);
stroke-opacity: 0.5;
stroke-dasharray: 5 3;
}
.group-label {
fill: var(--muted-foreground);
font: 500 11px var(--font-mono);
}
/* shapes with their own classes, for drawings that place their own text */
svg .box {
fill: var(--card);
stroke: var(--muted-foreground);
stroke-width: 1.2;
}
svg .box.info {
fill: color-mix(in srgb, var(--blue) 14%, var(--card));
stroke: var(--blue);
}
svg .box.good {
fill: color-mix(in srgb, var(--green) 14%, var(--card));
stroke: var(--green);
}
svg .box.warn {
fill: color-mix(in srgb, var(--yellow) 18%, var(--card));
stroke: var(--yellow);
}
svg .box.bad {
fill: color-mix(in srgb, var(--red) 14%, var(--card));
stroke: var(--red);
}
svg .box.accent {
fill: color-mix(in srgb, var(--orange) 14%, var(--card));
stroke: var(--orange);
}
svg .box.strong {
stroke: var(--foreground);
stroke-width: 1.6;
}
svg .box.dashed,
svg .edge.dashed {
stroke-dasharray: 5 4;
}
svg .box.open {
fill: none;
}
svg .zone {
fill: color-mix(in srgb, var(--muted-foreground) 6%, transparent);
stroke: var(--muted-foreground);
stroke-width: 1.5;
stroke-dasharray: 7 5;
}
svg .zone.info {
fill: color-mix(in srgb, var(--blue) 7%, transparent);
}
svg .zone.good {
fill: color-mix(in srgb, var(--green) 7%, transparent);
}
svg .zone.warn {
fill: color-mix(in srgb, var(--yellow) 9%, transparent);
}
svg .zone.bad {
fill: color-mix(in srgb, var(--red) 6%, transparent);
}
svg .zone.accent {
fill: color-mix(in srgb, var(--orange) 7%, transparent);
}
svg .zone.boundary {
stroke: var(--red);
}
svg .panel {
fill: color-mix(in srgb, var(--foreground) 4%, transparent);
stroke: var(--border);
}
svg .chip {
fill: var(--blue);
}
svg .chip.good {
fill: var(--green);
}
svg .chip.accent {
fill: var(--orange);
}
svg text.chip-text {
fill: var(--background);
font: 700 10px var(--font-mono);
}
svg .fill {
fill: var(--card);
stroke: none;
}
svg .line {
fill: none;
stroke: var(--foreground);
stroke-width: 1.6;
}
svg .rule {
stroke: var(--border);
stroke-width: 1.5;
stroke-dasharray: 3 5;
}
svg text.title {
fill: var(--foreground);
font: 600 13px var(--font-mono);
}
svg text.sub {
fill: var(--muted-foreground);
font: 400 11px var(--font-mono);
}
svg text.mono {
fill: var(--muted-foreground);
font: 400 10.5px var(--font-mono);
}
svg text.heading {
fill: var(--muted-foreground);
font: 600 11px var(--font-mono);
letter-spacing: 0.06em;
text-transform: uppercase;
}
svg text.info {
fill: var(--info);
}
svg text.good {
fill: var(--good);
}
svg text.warn {
fill: var(--warn);
}
svg text.bad {
fill: var(--bad);
}
/* index */
.index-head h1 {
font-size: 26px;
}
.index-head p {
margin: 4px 0 0;
color: var(--muted-foreground);
font-size: 13px;
}
.index h2 {
display: flex;
align-items: baseline;
gap: 8px;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted-foreground);
margin: 36px 0 6px;
padding: 0;
border: 0;
}
.index h2 .count {
font: 500 12px var(--font-mono);
letter-spacing: 0;
}
.rows {
list-style: none;
padding: 0;
margin: 0;
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
}
.rows li {
display: grid;
grid-template-columns: 52px minmax(0, 1fr) auto;
gap: 12px;
align-items: baseline;
padding: 9px 14px;
margin: 0;
}
.rows li + li {
border-top: 1px solid var(--border);
}
.rows .id {
font: 500 12px var(--font-mono);
color: var(--muted-foreground);
}
.rows .meta {
font-size: 12px;
color: var(--muted-foreground);
white-space: nowrap;
}
.empty {
color: var(--muted-foreground);
font-size: 13px;
margin: 0;
}
@media (max-width: 560px) {
.rows li {
grid-template-columns: 44px minmax(0, 1fr);
}
.rows .meta {
grid-column: 2;
}
}
/* a page rendered from markdown */
.md-meta {
display: flex;
flex-wrap: wrap;
gap: 4px 16px;
font-size: 13px;
color: var(--muted-foreground);
margin: 12px 0 0;
}
.md-meta dt {
display: inline;
font-weight: 500;
}
.md-meta dd {
display: inline;
margin: 0 0 0 4px;
}
.md-meta div {
min-width: 0;
}
.files {
font-size: 13px;
color: var(--muted-foreground);
margin-top: 32px;
padding-top: 16px;
border-top: 1px solid var(--border);
}
/* serve's hub: one row per project */
.rows.hub li {
grid-template-columns: minmax(0, 1fr) auto;
}
@media (max-width: 560px) {
.rows.hub li {
grid-template-columns: minmax(0, 1fr);
}
.rows.hub .meta {
grid-column: 1;
}
}
/* serve's search */
form.search,
input.filter {
margin: 24px 0 0;
}
form.search input,
input.filter {
width: 100%;
font: inherit;
color: var(--foreground);
background: var(--card);
border: 1px solid var(--input);
border-radius: var(--radius);
padding: 9px 14px;
}
form.search input:focus,
input.filter:focus {
outline: 2px solid var(--blue);
outline-offset: 1px;
}
.rows.results li {
grid-template-columns: 52px minmax(0, 1fr);
}
.rows.results .meta {
display: none;
}
.snippet {
display: block;
font-size: 13px;
color: var(--muted-foreground);
margin-top: 2px;
}
mark {
background: var(--warn-bg);
color: inherit;
border-radius: 3px;
padding: 0 2px;
}
/* a work item: its move line under the title in the index, the move box on top of its page */
.rows .title .move {
display: block;
margin-top: 2px;
}
.callout.move h2 {
font-size: 16px;
margin: 0 0 8px;
padding-top: 0;
border-top: 0;
}

107
claude/skills/lead/notes.md Normal file
View File

@@ -0,0 +1,107 @@
# Notes
Each project keeps its local working notes in `.notes/` at the project root, its own git repo with no remote, never pushed and never committed to the project. Every step that changes it ends with `commit`, which also regenerates the generated files (never committed): `index.html`, `notes.css`, `_pages/` (markdown rendered), `_out/` (`other/out/` made self-contained). Change the look only in `notes.css` next to this file.
Who reads what: Matej reads the work item, starting at its brief's `## Your move`, the pages it links, and his questions; nothing else. Docs, follow-ups and the rest of a work folder are the lead's and the agents', shaped for them. The index lists only work items and questions; everything else is still rendered, so a link from a work item opens.
```
N=~/.config/claude/skills/lead/notes.py
python3 $N init # create .notes/ if missing (idempotent)
python3 $N next d|w|f|q # next free id, counts deleted items too
python3 $N new d <slug> # docs/dNNN-slug.md on the skeleton; --html for a doc with a diagram
python3 $N new w <slug> # work/wNNN-slug/brief.md on the skeleton
python3 $N commit "<subject>" # regenerate, then commit all of .notes/
python3 $N index # regenerate only
python3 $N status # open work items: move, next, decisions waiting, branches in main or not; counts
python3 $N file wNNN <part> <output-file|agent-id> --checked "<what the lead checked>"
# the agent's final reply, verbatim, into work/wNNN-*/report-<part>.md
python3 $N serve # every project's notes on http://127.0.0.1:8765/, decide clicks recorded (the user runs this)
```
All commands take `--root <project>`; the default is the git toplevel of the current dir. Never pick an id by hand: ids are `<letter><NNN>-<slug>` (`d` docs, `w` work, `f` follow-ups, `q` questions, three digits, kebab slug), never reused, and deleted ones live only in git history. Refer to items by id everywhere ("see q014", "per d007"); in markdown an id becomes a link.
```
.notes/
docs/ dNNN-slug.md, or .html / dNNN-slug/index.html for a doc with a diagram and its assets
work/ wNNN-slug/ while in flight, removed at close-out
followups/ fNNN-slug.md, removed when done or moot
questions/ qNNN-slug.md, Matej's own, removed when he settles them
other/in/ placed by the user, read only when pointed at
other/out/ prepared for someone else
```
Markdown for everything, docs included: they are agent-facing. HTML only for a diagram Matej needs, linked from his work item. Screenshot a page (headless browser, light and dark) only when it has a diagram or a decide block.
## Docs
Point-in-time records of a type: `plan` (a proposal to approve), `research` (findings with sources), `review` (what a review found and what was done), `as built` (the lasting design of finished work, written at close-out).
- `new d <slug>`, then fill it in. The markdown skeleton's frontmatter holds `type`, `date` and `status`; the html one (`--html`) links `notes.css`, starts `main.page` with `nav.back`, and uses only the components listed at the top of `notes.css`, no `<style>` or font links. A folder doc's relative paths gain one `../`.
- status: `current`, `partly stale (<what changed>, <date>)` or `superseded by dNNN (<date>)`; in html, `<p class="status current|stale|superseded">status: …</p>` under the title, with `dNNN` a relative link. Replacing a doc means setting the older one's status.
- Once done, only a doc's status changes; a new design gets a new doc.
- At close-out a plan or review doc is deleted once its lasting decisions are in an as-built doc or in the code. "Partly stale" doesn't outlive the next session: fix the doc or delete it.
- `commit "dNNN: <slug>"`
## Work items
One folder per piece of work that spans sessions; a single-session task gets none. It is the handoff: a later session picks the work up from the folder alone.
- `new w <slug>` writes `brief.md`: goal, `## Your move`, done when, scope, prerequisites, and `## Decided` with each locked decision and its date.
- `## Your move` is Matej's entry point: one to three lines with links (a page to read, a decision to click). The page opens with it in a box, with links to the decisions still waiting and the item's branches, in main or not. The lead keeps it current and its chat reply points at the page instead of asking in chat.
- The brief holds the current state, not its history: a replaced decision, fact or next step is edited in place, and git keeps the old one. An agent's brief cites brief sections and at most one report section, never whole reports or folders.
- As the work goes: `plan.md` (the proposal), a brief per phase or part an agent builds, `report-<part>.md` per reply, `made.md` (every branch, worktree, container and folder the work created, with its state; whether a branch is in main, `status` reads from git), research, and `decisions.md` (written by `serve`; the lead copies its entries into `Decided` and leaves them there, and appends a choice Matej gives in chat as `- <date> <key>: <choice> (matej, in chat)`, so the block shows it chosen and `status` stops listing it).
- Replies are filed with `file` when the notification arrives, from its `<output-file>` or the agent id: the lead types only the check line, never the reply. Its transcript's last reply changes if the agent goes on later, so file each reply on its notification.
- `commit "wNNN: <what changed>"`. A folder untouched for weeks with no open branch: tell the user, don't close it.
### Decisions in a page
A fenced block in any markdown file of a work item:
````
```decide d1-policy
a*) fold into the skill
one file, loaded only with the skill
b) a path-scoped rule
keeps two files
```
````
The info string is `decide <key>` (`[a-z0-9-]`); an option starts with `<letter>) <title>`, `*` before the `)` marks the recommended one, and the lines up to the next option are its text. It renders as two options side by side, each with a button. Under `serve` a click appends `- <date> <key>: <choice> (matej, clicked)` to the item's `decisions.md`, commits `wNNN: <key> decided, <choice>`, and the page marks the chosen option; `git -C .notes log -1` shows it when Matej says he decided. Opened without `serve`, the block shows a line for him to paste instead (`w012 d1-policy: a|b`). `serve` accepts only loopback, same-origin posts, and only an option the item's own pages offer for that key.
## Close-out
When the work's branch is merged or the work is dropped, in that same session:
1. lasting design: a new `as built` or `review` doc, or a status line on an existing one; delete the plan and review docs it covers (see Docs)
2. leftovers: follow-ups (below), by area; never questions
3. remove everything `made.md` lists, `rm -r work/wNNN-slug`, `commit "wNNN: closed, <merged as <sha>|dropped>; <dNNN, fNNN touched>"`
## Follow-ups
What finished work found and left undone (defects, rough edges, unverified claims, ideas, to-dos after a deploy), one file per area, not per item.
1. a file for the area exists: append `## From wNNN, <date>` and add `wNNN` to its `origin`
2. otherwise `next f` and write `followups/fNNN-slug.md`: frontmatter `created`, `origin: wNNN`, `status: open`; `# <the area, as a noun phrase>`; then per item what it is, the evidence (`file:line`, a commit, a measurement) and a direction in a line, so a session with no memory of today can act on it
3. `commit "fNNN: <what was added>"`
Settling: check the repo first (items go stale), remove the done or moot items, `rm` an empty file, `commit "fNNN: <items> settled, <done in <sha> | moot because ...>"`. A follow-up area becomes work by a work item that cites it.
## Questions
Matej's own. Park one only when he asks, in his words; never for your own leftovers.
1. `next q`, write `questions/qNNN-slug.md`: frontmatter `created`, `asked-by: matej`, `origin`, `status: open`; `# <his question>`; `## Known` with what is known, sourced, and where to look (you may add sourced facts there later, never new questions)
2. `commit "qNNN: <slug>"`
Settling: check the repo first, fold the outcome into a doc or work item if worth keeping, `rm` the file, `commit "qNNN: settled, <done in <sha> | moot because ... | moved to wNNN>"`.
## Other
`other/out/` holds documents for someone else, written for that reader, and the few small files that go with them; no ids, no status line. HTML there follows the doc skeleton with `../../notes.css`; the eyebrow names the reader and date (`for the team · 2026-10-14`). What gets sent is the generated `_out/<file>`; look at it before handing it over, link it from the work item's `## Your move` (the index doesn't list `other/`), then `commit "other: <file>"`. Bulk output for a person (screenshots, builds, logs) goes to the host handoff mount, not `.notes/`.
## Never
- stage `.notes/` from the project repo (`git add .`, `git add -A`), or reference `.notes/` paths from committed project files
- keep archives or dated copies: delete, git keeps it
- push `.notes/` or add a remote
- put in notes what every session needs (AGENTS.md, CLAUDE.md, memory), what teammates need (the project repo), or secrets

1429
claude/skills/lead/notes.py Executable file

File diff suppressed because it is too large Load Diff