diff --git a/CLAUDE.md b/CLAUDE.md index 607f2ae..24b1652 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,7 +12,7 @@ This file covers working *on* scc. The product's own rules and methodology are n Note: this repo is not itself an scc workspace (no `.claude/`, `specs/`, or `docs/` are committed) — those trees only exist in workspaces the binary scaffolds, and in test temp dirs. -**Status: v0.3.0-shaped.** Everything through `design/plan.md` phase 10 is built and green: scaffolding (`init`), artifact creation (`spec`, `plan`), and all eight validators behind `scc validate`. `scc update` (phase 11) is not started, deliberately — a three-way merge is untestable until a second template version exists, so until then the upgrade story is "re-run `init`; it never overwrites what you edited". +**Status: v0.3.0-shaped.** Everything through `design/plan.md` phase 10 is built and green: scaffolding (`init`), artifact creation (`spec`, `plan`), and all eight validators behind `scc validate`. `init` also scaffolds the six knowledge-base skills named in `design/orchestration.md` §6 — one per `docs/` artifact a validator checks, plus `prd` — each with a `scc-`-prefixed slash command derived from the same list (`assets.KnowledgeSkills`). `scc update` (phase 11) is not started, deliberately — a three-way merge is untestable until a second template version exists, so until then the upgrade story is "re-run `init`; it never overwrites what you edited". `scc` is a redesign of `csdd` (`github.com/protonspy/csdd`), narrowed to spec-driven development and deliberately leaner. When reaching for something from there, port the *decision*, not the file. Already decided against: a TUI, an embedded web dashboard, an MCP server, a devcontainer. @@ -68,7 +68,7 @@ cmd/scc/main.go os.Exit(cli.Run(os.Args[1:])) | `internal/textutil` | Line-ending and BOM normalization, in exactly one place. | | `internal/finding` | One finding type and one frozen JSON shape (`{findings, count}`) for every validator, plus the grouped human report. | | `internal/manifest` | `.claude/scc-manifest.json`: `{path, hash, version}` per managed file, deterministic serialization, `Status → pristine\|edited\|missing`. Unknown fields are preserved. | -| `internal/assets` | The embedded template set. **Workspace templates are data-free** (that is what makes a future three-way merge possible); **artifact templates take data** (`spec new` renders them and the user owns the result). | +| `internal/assets` | The embedded template set — rules, review agents, skills, slash commands, artifact templates. **Workspace templates are data-free** (that is what makes a future three-way merge possible); **artifact templates take data** (`spec new` renders them and the user owns the result). `Version` is the template-set version and must be bumped whenever a workspace template changes. | | `internal/scaffold` | Applies the template set to a root. Idempotent, never overwrites, manifest written last. | | `internal/mdscan` | The only Markdown parser: fence- and HTML-comment-aware headings, checkboxes, links, wikilinks, slugs, plus a small frontmatter reader. `Body` is the comment/fence-stripped text every validator applies its grammar to. | | `internal/ears` | EARS requirement parsing, all five patterns plus complex. | diff --git a/design/orchestration.md b/design/orchestration.md index 742febb..0b42640 100644 --- a/design/orchestration.md +++ b/design/orchestration.md @@ -381,6 +381,40 @@ Don't cite it for a length rule.) import from `CLAUDE.md` too — a second file and an indirection to buy portability to tools this product isn't for. Anyone who wants cross-tool context can write it themselves. +### Which skills ship — the authors of `docs/` + +Six, and the rule that picks them is mechanical: **a skill ships for each `docs/` +artifact a validator checks, and for nothing else.** + +| Skill | Authors | Checked by | +|---|---|---| +| `wiki` | `docs/wiki/` — ingest from `docs/raw/`, query, repair | `wiki` | +| `codewiki` | `docs/codewiki/` — narrated code, cited by line range | `codewiki` | +| `glossary` | `docs/glossary.md` — canonical terms and banned synonyms | `glossary` | +| `stack` | `docs/stack.md` — adopted technology, one line of why | `stack` | +| `adr` | `docs/adr/` — hard-to-reverse decisions, superseded not edited | `adr` | +| `prd` | `plans/.md` — an initiative decomposed into specs | `plan` | + +Five of the six pair with a validator of the same name. `prd` is the exception and +earns its place for the opposite reason: it is the **entry point** for work too large +for one spec, the one place §1's routing question gets asked out loud, and without it +the only on-ramp scc offers is `scc plan new` handing the user an empty template. + +**The methodology is deliberately not a skill.** The cycles, verification, and +delivery are rules under `.claude/rules/` (§3, §7, §9) — read when the concern is +live. csdd shipped `tdd-cycle`, `unit-cycle`, and `verify-change` as skills; here +that would be a second copy of a rule, and the copy is the one that goes stale. + +**One slash command per skill, namespaced `scc-`.** A skill is model-invoked through +its description; a command is the human saying *now*. Both are cheap, they are derived +from one list so they cannot drift, and the prefix is not decoration — slash commands +share a flat namespace across every source Claude Code loads them from, and a bare +`/adr` collides on contact. + +The skills carry the *procedure and the judgment*; the format they produce stays in +`.claude/rules/knowledge-base.md`, stated once. A skill that restated the format would +be the same duplication the methodology rules are being kept out of skills to avoid. + ## 7 · Agents — review only Two subagents ship with the workspace, and both of them read rather than write: diff --git a/design/plan.md b/design/plan.md index 40b1d52..8e4d340 100644 --- a/design/plan.md +++ b/design/plan.md @@ -459,7 +459,7 @@ Phase 2 — init Phase 3 — the template set - [x] 3.1 (Unit) .claude/rules/: the nine rule files - [x] 3.2 (Unit) CLAUDE.md and the two review agents -- [ ] 3.3 (Unit) shipped skills — none ship; the design never says which would (§9) +- [x] 3.3 (Unit) shipped skills — the six §6 names, each with its scc- slash command - [x] 3.4 (Unit) spec templates (optional sections marked optional) and the plan template Phase 4 — creating work @@ -475,7 +475,7 @@ Phase 5 — markdown Phase 6 — skill - [x] 6.1 (TDD) validate: Agent Skills conformance against the published spec - [x] 6.2 (Unit) cli: scc skill validate, exit 2 on findings -- [x] 6.3 (Unit) the shipped skills pass their own validator (vacuously true; see 3.3) +- [x] 6.3 (Unit) the shipped skills pass their own validator — count asserted, not vacuous Phase 7 — spec - [x] 7.1 (TDD) internal/ears: all five patterns plus complex, clause order enforced @@ -544,11 +544,18 @@ data to them is meaningless, while artifact templates *do* take data because the authored from birth and never tracked in the manifest. Two functions make the difference impossible to get wrong at a call site. -**Deviation: no skills ship, so 3.3 and 6.3 are hollow.** The design lists skills among -what the binary scaffolds but never says which. Nothing was invented to fill the gap. -`skill validate` is complete and tested against fixtures, and the test asserting that -shipped skills pass it will start meaning something the moment one ships. **One line in -the design saying which skills ship, if any, closes this.** +**Resolved: six skills ship, so 3.3 and 6.3 now assert something.** The design listed +skills among what the binary scaffolds and never said which, so for v0.3.0 none did and +both checkboxes were vacuous. §6 now names them, picked mechanically — one skill per +`docs/` artifact a validator checks (`wiki`, `codewiki`, `glossary`, `stack`, `adr`), +plus `prd` as the entry point for work too large for one spec. Each ships with a +namespaced slash command derived from the same list. The methodology stays in +`.claude/rules/`: a skill restating a rule is a second copy of one fact. + +3.3 and 6.3 are no longer satisfiable by an empty workspace — +`TestScaffoldedSkillsPassTheSkillValidator` asserts the scaffolded count before it +asserts the clean run, which is the part that was missing while the test was hollow. +Template version bumped to `2`. **Also settled along the way**, all of it now in the rules the binary scaffolds, because a validator needs a written convention to check against: ADR frontmatter (`status`, diff --git a/internal/assets/assets.go b/internal/assets/assets.go index 4108038..5da8603 100644 --- a/internal/assets/assets.go +++ b/internal/assets/assets.go @@ -1,6 +1,6 @@ // Package assets is the template set compiled into the binary: the rules, the -// review agents, and the artifact templates, all of it plain Markdown embedded -// with //go:embed. +// review agents, the knowledge-base skills and their commands, and the artifact +// templates, all of it plain Markdown embedded with //go:embed. // // There are two kinds of template here and the difference matters: // @@ -40,7 +40,9 @@ import ( // It is not the binary's version. Two scc releases that ship identical templates // share a template version, which keeps `scc update` a no-op across them instead // of touching every managed file to record a number nobody read. -const Version = "1" +// +// 2: the knowledge-base skills and their commands. +const Version = "2" // The embedded tree. "all:" so nothing is silently dropped for having a name the // default embed pattern skips. @@ -104,10 +106,41 @@ func Workspace() []File { Rel: claude(paths.AgentsSeg, agent), }) } + // The knowledge base's authors. Every one of these produces an artifact + // `scc validate` already checks — a workspace that ships the eight validators + // and no skill teaching the formats would demand conformance to documents + // nobody was told how to write. + for _, skill := range KnowledgeSkills { + set = append(set, File{ + Name: "claude/skills/" + skill + "/SKILL.md", + Rel: claude(paths.SkillsSeg, skill, "SKILL.md"), + }) + // One command per skill, so the human has an explicit entry point where the + // model has a description. Namespaced, because slash commands share a flat + // namespace with every other source Claude Code loads them from and `/adr` + // would collide on contact. + cmd := commandPrefix + skill + ".md" + set = append(set, File{ + Name: "claude/commands/" + cmd, + Rel: claude(paths.CommandsSeg, cmd), + }) + } sort.Slice(set, func(i, j int) bool { return set[i].Rel < set[j].Rel }) return set } +// KnowledgeSkills names the skills scc ships, each one the author of a `docs/` +// artifact a validator checks. Both the skill directory and its slash command are +// derived from this list, so the two cannot drift apart. +// +// The methodology is deliberately absent from it: the cycles, verification, and +// delivery are rules under `.claude/rules/`, read when the concern is live. A skill +// restating a rule is a second copy of one fact, and the copy goes stale. +var KnowledgeSkills = []string{"adr", "codewiki", "glossary", "prd", "stack", "wiki"} + +// commandPrefix namespaces the scaffolded slash commands. +const commandPrefix = "scc-" + // Dirs returns the directories `scc init` creates even when it has no file to put // in them. An agent that can see specs/, plans/, and docs/wiki/ knows where its // output goes; one that has to infer the layout from a rule file guesses. @@ -115,6 +148,8 @@ func Dirs() []string { return []string{ path.Join(paths.ClaudeDir, paths.RulesSeg), path.Join(paths.ClaudeDir, paths.AgentsSeg), + path.Join(paths.ClaudeDir, paths.SkillsSeg), + path.Join(paths.ClaudeDir, paths.CommandsSeg), paths.SpecsSeg, paths.PlansSeg, path.Join(paths.DocsSeg, paths.WikiSeg), diff --git a/internal/assets/assets_test.go b/internal/assets/assets_test.go index 04e0c18..ebfb1d0 100644 --- a/internal/assets/assets_test.go +++ b/internal/assets/assets_test.go @@ -259,6 +259,79 @@ func TestAgentsCarryTheirFrontmatter(t *testing.T) { } } +// Every skill in KnowledgeSkills ships both a SKILL.md and a slash command, and +// nothing else lands in either directory. The two are derived from one list so they +// cannot drift, and this is the assertion that keeps the derivation honest. +func TestEverySkillShipsWithItsCommand(t *testing.T) { + skills, commands := map[string]bool{}, map[string]bool{} + for _, f := range Workspace() { + switch { + case strings.HasPrefix(f.Rel, ".claude/skills/"): + name := strings.TrimSuffix(strings.TrimPrefix(f.Rel, ".claude/skills/"), "/SKILL.md") + skills[name] = true + case strings.HasPrefix(f.Rel, ".claude/commands/"): + name := strings.TrimSuffix(strings.TrimPrefix(f.Rel, ".claude/commands/"), ".md") + commands[name] = true + } + } + if len(skills) != len(KnowledgeSkills) || len(commands) != len(KnowledgeSkills) { + t.Fatalf("%d skills and %d commands, want %d of each", len(skills), len(commands), len(KnowledgeSkills)) + } + for _, name := range KnowledgeSkills { + if !skills[name] { + t.Errorf("%s is in KnowledgeSkills and ships no SKILL.md", name) + } + if !commands[commandPrefix+name] { + t.Errorf("%s is in KnowledgeSkills and ships no %s%s command", name, commandPrefix, name) + } + } +} + +// The Agent Skills contract is what `scc skill validate` enforces on everyone else, +// and the two fields it can break loading on are checked here at the source. The +// full conformance run happens against a scaffolded workspace in internal/cli — a +// tool that ships non-conforming skills has no standing to check anyone else's. +func TestSkillsCarryTheirFrontmatter(t *testing.T) { + for _, f := range Workspace() { + if !strings.HasPrefix(f.Rel, ".claude/skills/") { + continue + } + raw, err := Content(f.Name) + if err != nil { + t.Fatalf("%s: %v", f.Name, err) + } + if !strings.HasPrefix(raw, "---\n") { + t.Errorf("%s does not open with frontmatter", f.Rel) + continue + } + // The name must equal the parent directory or the skill does not load at + // all, in any tool that reads this format. + dir := strings.TrimSuffix(strings.TrimPrefix(f.Rel, ".claude/skills/"), "/SKILL.md") + if !strings.Contains(raw, "\nname: "+dir+"\n") { + t.Errorf("%s: frontmatter name does not match the directory %q", f.Rel, dir) + } + if !strings.Contains(raw, "\ndescription: ") { + t.Errorf("%s: no description in frontmatter", f.Rel) + } + } +} + +// A slash command with no description is one that cannot be found in the picker. +func TestCommandsCarryTheirDescription(t *testing.T) { + for _, f := range Workspace() { + if !strings.HasPrefix(f.Rel, ".claude/commands/") { + continue + } + raw, err := Content(f.Name) + if err != nil { + t.Fatalf("%s: %v", f.Name, err) + } + if !strings.HasPrefix(raw, "---\n") || !strings.Contains(raw, "\ndescription: ") { + t.Errorf("%s: a command needs frontmatter carrying a description", f.Rel) + } + } +} + // Directories init creates on its own must be inside the workspace and // slash-separated, for the same reason destinations must be. func TestDirsAreRelativeAndSlashed(t *testing.T) { diff --git a/internal/assets/templates/CLAUDE.md b/internal/assets/templates/CLAUDE.md index 340efeb..686126a 100644 --- a/internal/assets/templates/CLAUDE.md +++ b/internal/assets/templates/CLAUDE.md @@ -29,6 +29,8 @@ specs// requirements.md · design.md · tasks.md plans/.md one file: structure, plus a checklist and/or spec references docs/ the knowledge base — wiki, adr, codewiki, glossary, stack .claude/rules/ the methodology above +.claude/skills/ how to author each part of docs/ — invoked when it applies +.claude/commands/ the same skills on demand, as /scc-wiki, /scc-adr, /scc-prd, … ``` ## Checking your work diff --git a/internal/assets/templates/claude/commands/scc-adr.md b/internal/assets/templates/claude/commands/scc-adr.md new file mode 100644 index 0000000..35c68da --- /dev/null +++ b/internal/assets/templates/claude/commands/scc-adr.md @@ -0,0 +1,15 @@ +--- +description: Record a hard-to-reverse decision as a numbered ADR under docs/adr/, or supersede one that stopped being true +argument-hint: [the decision, or the ADR being superseded] +--- + +Use the `adr` skill. + +Decision: $ARGUMENTS + +First ask whether this is an ADR at all: how expensive would it be to undo? A +decision that is cheap to change belongs in the spec's `design.md`, and an `adr/` +full of reversible choices buries the records that actually explain the system. + +If an existing record is being replaced, write the new one and mark the old one +superseded — never edit its prose. diff --git a/internal/assets/templates/claude/commands/scc-codewiki.md b/internal/assets/templates/claude/commands/scc-codewiki.md new file mode 100644 index 0000000..34c58e0 --- /dev/null +++ b/internal/assets/templates/claude/commands/scc-codewiki.md @@ -0,0 +1,13 @@ +--- +description: Narrate an area of the codebase into docs/codewiki/, every section citing the exact lines it explains +argument-hint: [area or path to narrate | repair] +--- + +Use the `codewiki` skill. + +Area: $ARGUMENTS + +If no area was named, run `scc validate` and repair the `codewiki.*` findings it +reports — a broken citation means the code moved and the prose describing it is now +suspect, so re-read before re-numbering. If there are no findings and no area was +named, ask which area is hard to enter cold rather than picking one at random. diff --git a/internal/assets/templates/claude/commands/scc-glossary.md b/internal/assets/templates/claude/commands/scc-glossary.md new file mode 100644 index 0000000..85c7d6f --- /dev/null +++ b/internal/assets/templates/claude/commands/scc-glossary.md @@ -0,0 +1,12 @@ +--- +description: Add, settle, or rename a canonical term in docs/glossary.md, and list the synonyms to avoid +argument-hint: [term, or the ambiguity to settle] +--- + +Use the `glossary` skill. + +Term: $ARGUMENTS + +Read `docs/glossary.md` before adding to it — an entry that duplicates an existing +concept under a different name makes the canonical source itself ambiguous. If +nothing was named, run `scc validate` and resolve the `glossary.*` findings. diff --git a/internal/assets/templates/claude/commands/scc-prd.md b/internal/assets/templates/claude/commands/scc-prd.md new file mode 100644 index 0000000..3325d18 --- /dev/null +++ b/internal/assets/templates/claude/commands/scc-prd.md @@ -0,0 +1,16 @@ +--- +description: Turn an initiative too large for one spec into a plan under plans/ — decomposed into specs and tasks +argument-hint: [the initiative, epic, or PRD] +--- + +Use the `prd` skill. + +Initiative: $ARGUMENTS + +Check the routing question before you start: work that is one feature with unsettled +requirements is a spec, not a plan — run `scc spec new ` instead of wrapping +one feature in a plan for ceremony. + +If the initiative is too vague to decompose, ask a small batch of concrete +multiple-choice questions once, then decompose. Do not guess at the scope, and do not +interview at length — stop asking the moment you can name the leaves. diff --git a/internal/assets/templates/claude/commands/scc-stack.md b/internal/assets/templates/claude/commands/scc-stack.md new file mode 100644 index 0000000..25b6297 --- /dev/null +++ b/internal/assets/templates/claude/commands/scc-stack.md @@ -0,0 +1,16 @@ +--- +description: Record an adopted technology in docs/stack.md, or account for a dependency that nobody decided on +argument-hint: [technology being adopted or dropped] +--- + +Use the `stack` skill. + +Technology: $ARGUMENTS + +If nothing was named, run `scc validate` and work through the +`stack.undocumented-dependency` findings: for each one, either record the decision or +establish that nobody can justify the dependency and remove it. Both are correct +outcomes; listing a name with no reason is not. + +Adopting technology is a decision with a long tail. If it is not obviously the right +call, stop and ask rather than committing on someone else's behalf. diff --git a/internal/assets/templates/claude/commands/scc-wiki.md b/internal/assets/templates/claude/commands/scc-wiki.md new file mode 100644 index 0000000..38ba7a2 --- /dev/null +++ b/internal/assets/templates/claude/commands/scc-wiki.md @@ -0,0 +1,13 @@ +--- +description: Build or maintain docs/wiki/ — ingest a source from docs/raw/, answer from what is known, or clear wiki.* findings +argument-hint: [ingest | query | maintain] +--- + +Use the `wiki` skill. + +Request: $ARGUMENTS + +If nothing was asked for specifically, look at `docs/raw/` first — anything sitting +there is unprocessed work and is the default job. If `raw/` is empty, run +`scc validate` and clear whatever `wiki.*` findings it reports. If there are none, +say so rather than inventing pages. diff --git a/internal/assets/templates/claude/skills/adr/SKILL.md b/internal/assets/templates/claude/skills/adr/SKILL.md new file mode 100644 index 0000000..ae737f9 --- /dev/null +++ b/internal/assets/templates/claude/skills/adr/SKILL.md @@ -0,0 +1,88 @@ +--- +name: adr +description: Record a decision that is hard to reverse as a numbered ADR under docs/adr/, and supersede an old record rather than editing it. Use it when a choice is made that would be expensive to undo — storage, wire protocol, a module boundary, a vendor holding data, a framework the code will shape itself around — when someone asks why the project is built this way, and when `scc validate` reports adr.* findings such as a numbering gap, a superseded record with no successor, or a citation resolving to no record. +--- + +You write the record of a decision, so that the reasoning survives the people who +were in the room. The filename shape, the frontmatter, and the citation form are in +[knowledge-base.md](../../rules/knowledge-base.md). This skill is when to write one +and how to write one worth reading. + +## Is this an ADR? + +One question: **how expensive would it be to undo?** + +Write one when the answer is "expensive" — a datastore, a wire format, an +authentication model, a boundary between services, a vendor that will end up holding +data, a framework the rest of the code will be shaped around. Also write one when a +previous decision is being **reversed**, which is the case people skip and the case a +reader most needs. + +Do not write one for a decision that is cheap to change. That belongs in the spec's +`design.md`, where it is read by the people building the feature and forgotten +harmlessly once it stops being true. An `adr/` directory full of reversible choices +buries the four records that actually explain the system. + +Rough test: if changing your mind next month means editing one package, it is not an +ADR. If it means a migration, it is. + +## Writing one + +```markdown +--- +status: accepted # proposed | accepted | rejected | superseded +--- + +# 0007 · Use SQLite for the cache + +## Context +## Decision +## Consequences +``` + +Numbered contiguously from `0001`; the filename is `NNNN-kebab-slug.md`, and the +number is never reused — gaps are findings because a gap means a record went +missing. Take the next number after the highest one present. + +**Context** is the section that does the work. Write what was true when the decision +was made — the constraint, the load, the deadline, the thing that was already built. +A reader a year from now is trying to learn whether the reasons still hold, and they +can only do that if the reasons are written down. "We chose SQLite because it is +simple" tells them nothing; "single writer, under 10GB, and operating a second +service was the cost we were avoiding" tells them exactly when to revisit. + +**Decision** is one paragraph in the active voice. What was chosen, and what was +chosen against — name the alternatives that were real, and say what ruled each out. + +**Consequences** includes the ones you do not like. A record listing only benefits +is marketing, and the next reader will discover the downside on their own and trust +nothing else in the file. + +## Superseding + +**A superseded record is marked, never edited.** The point of an ADR is that it +records what was believed at the time; rewriting it destroys the thing it exists to +preserve — and the new record loses the context that explains why the change was +needed. + +Write the new record, then in the old one set only the frontmatter: + +```yaml +status: superseded +superseded-by: 0012-move-the-cache-to-redis +``` + +Leave the prose exactly as it was. The new record's Context says what changed. + +## Findings + +| Finding | What it means | The fix | +|---|---|---| +| `adr.malformed-filename` | Not `NNNN-kebab-slug.md`. | Rename it. The number is how it is cited. | +| `adr.duplicate-number` | Two records share a number. | Renumber the later one; a citation cannot resolve to two records. | +| `adr.numbering-gap` | A number is missing from the sequence. | A record was deleted or never committed. Restore it, or renumber — do not leave the hole, because a gap reads as a lost decision. | +| `adr.missing-status` / `adr.status-invalid` | The frontmatter `status` is absent or not one of the four. | Set it. A record with no status does not say whether it is in force. | +| `adr.superseded-without-successor` | Marked superseded with no `superseded-by`. | Name the record that replaced it, or the reader is told this is obsolete and given nowhere to go. | +| `adr.unknown-successor` / `adr.unknown-citation` | A reference resolves to no record. | Fix the slug. | + +Cite a record from anywhere in `docs/` as `adr:0007-use-sqlite-for-the-cache`. diff --git a/internal/assets/templates/claude/skills/codewiki/SKILL.md b/internal/assets/templates/claude/skills/codewiki/SKILL.md new file mode 100644 index 0000000..08bb802 --- /dev/null +++ b/internal/assets/templates/claude/skills/codewiki/SKILL.md @@ -0,0 +1,78 @@ +--- +name: codewiki +description: Narrate an area of this codebase into docs/codewiki/, where every section cites the exact lines it explains. Use it when a subsystem is hard to enter cold and the code does not say why it is shaped that way, when someone asks for onboarding notes or an architecture walkthrough, and when `scc validate` reports codewiki.* findings — a citation that no longer resolves, one that runs past the end of its file, or a section that cites nothing at all. +--- + +You write prose that explains code, and every section of it points at the lines it +is about. The citation is what makes this different from a wiki page: it goes stale +loudly. When the code moves, `scc validate` fails instead of the page quietly +becoming a lie. + +The format is in [knowledge-base.md](../../rules/knowledge-base.md). This skill is +the procedure and the judgment. + +## The citation form + +A link whose text is `path:start-end` and whose target is empty: + +```markdown +## How the dispatcher routes + +[internal/cli/cli.go:48-64]() + +One switch, no registration. Adding a subcommand means... +``` + +- The path is relative to the workspace root, always — never to the page. +- A single line is `path:42`. A range is `path:42-58`. +- The target stays empty. `scc validate` resolves the path itself, confirms the file + exists, and confirms the range is inside it. + +## Writing a page + +1. **Read the area first — all of it.** You are claiming to explain this code. Every + sentence you write is checkable against the lines you cited, and a reader who + catches one wrong claim stops believing the page. +2. **Pick the sections from the reader's questions**, not from the file layout. "How + the dispatcher routes" is a section. "cli.go" is a table of contents. +3. **Cite before you narrate.** Put the citation directly under the heading, then + explain. It anchors the section and it is how you notice you are about to write a + paragraph that is not about any particular code. +4. **Every `##` section cites something.** This is checked. A section with no + citation is prose that has drifted free of what it describes — either find the + lines it is about, or the section belongs in `docs/wiki/` instead. +5. **Headings must be unique on the page.** Two identical headings share one anchor, + and a link to it lands on whichever came first. +6. `scc validate` before you call it done. + +## What to narrate, and what to leave alone + +Write about the parts where **reading the code does not tell you why it is like +that**: + +- A constraint the shape exists to satisfy, which is invisible from any one file. +- An invariant several files cooperate to hold. +- A deliberate choice that looks wrong until you know what it prevents. +- The entry point — where execution actually begins, which is often the single + hardest thing to find in an unfamiliar codebase. + +Do not narrate what a reader can see. "This function takes a name and returns a +path" is the signature restated at greater length, and a page full of that trains +people to skip the page. + +Prefer the range that makes the point. Citing a whole 400-line file says "the answer +is in here somewhere", which the reader already knew. + +## Repairing drift + +| Finding | What it means | The fix | +|---|---|---| +| `codewiki.citation-unresolved` | The file is gone or was renamed. | Find where the code went. If it was deleted, delete the section — a section about code that no longer exists is not repairable. | +| `codewiki.citation-out-of-range` | The file is shorter than the citation claims. | The code moved. **Re-read it before re-numbering**: the lines may now be something else entirely, and shifting the range to make the finding go away without reading is how a page becomes confidently wrong. | +| `codewiki.citation-invalid` | Malformed, or the path escapes the workspace. | A citation names a path inside the checkout. Fix the path. | +| `codewiki.section-cites-nothing` | A section explains no specific code. | Cite the lines, or move the section to `docs/wiki/`. | +| `codewiki.duplicate-heading` | Two sections share one anchor. | Rename one. | + +**A finding here is usually a real problem, not a bookkeeping chore.** The citation +broke because the code changed, which means the prose describing it is now suspect +in a way no validator can measure. Re-read, then rewrite what is no longer true. diff --git a/internal/assets/templates/claude/skills/glossary/SKILL.md b/internal/assets/templates/claude/skills/glossary/SKILL.md new file mode 100644 index 0000000..830df93 --- /dev/null +++ b/internal/assets/templates/claude/skills/glossary/SKILL.md @@ -0,0 +1,67 @@ +--- +name: glossary +description: Own docs/glossary.md — one canonical term per concept, and the synonyms nobody should use for it. Use it when a domain term is coined, contested, fuzzy, or renamed; before naming a spec, a plan, a wiki page, or an identifier that will outlive the session; and when `scc validate` reports glossary.* findings — a duplicate entry, a term listed as both canonical and avoided, or an avoided synonym used somewhere in docs/. +--- + +You own the project's vocabulary. Domain language drifts by default: two people +working in parallel produce three names for one thing inside a week, and every one +of them ends up in code, requirements, and the wiki, where nobody can tell whether +two documents are describing the same concept or two different ones. + +The entry format is in [knowledge-base.md](../../rules/knowledge-base.md). This +skill is when to reach for it and how to decide. + +## The entry + +One entry per line, in `docs/glossary.md`: + +```markdown +- **order total** — the amount charged, in minor units. Avoid: grand total, sum +- **workspace** — a directory holding .claude/scc-manifest.json. Avoid: project root +``` + +- The term is bold. The definition follows an em dash. +- `Avoid:` lists the synonyms, comma-separated. It is optional — a term with no + contested spellings does not need one. +- Every listed synonym becomes a finding wherever it appears as a whole word in + `docs/`. That is the enforcement, so list a synonym only when you mean it. + +## Adding a term + +1. **Check it is not already there** under another name. Read the file before adding + to it — a glossary with two entries for one concept is worse than none, because + now the canonical source is itself ambiguous. +2. **Define what it is, not what it is called.** "The amount charged, in minor + units" settles arguments. "The total of the order" settles nothing. +3. **List the synonyms you are banning**, and only those. Do not ban a word the + project uses correctly in an unrelated sense — the check is whole-word across all + of `docs/`, so banning a common word turns the glossary into noise the first time + it fires on an innocent sentence. +4. **Then use it.** Rename the occurrences in `docs/` that the validator will now + flag, and prefer the canonical term in new requirements, wiki pages, and + identifiers. +5. `scc validate` before you call it done. + +## When to reach for this + +- **A term is coined.** Something in the domain just got a name for the first time. +- **A term is contested.** Two documents or two people use different words and you + cannot tell whether they mean the same thing. They probably do; that is the point. +- **A term is renamed.** Move the old name into the new entry's `Avoid:` list — this + is the case the enforcement is most useful for, because it finds every place the + rename was not applied. +- **Before naming anything durable** — a spec directory, a plan, a wiki slug, a + type. The name will be read far more often than it was chosen. + +## Findings + +| Finding | What it means | The fix | +|---|---|---| +| `glossary.duplicate-term` | Two entries define the same term. | Merge them. One canonical term means one entry. | +| `glossary.synonym-is-canonical` | A term is defined as canonical and also listed as a synonym to avoid. | The glossary is contradicting itself. Decide which one the project uses. | +| `glossary.avoided-synonym` | A banned synonym is used in `docs/`. | Rewrite it to the canonical term — that is the entire point of listing it. If the sentence is genuinely about something else, the ban was too broad: narrow the `Avoid:` list. | + +**Do not define what is obvious to anyone in the domain.** A glossary that opens +with "user — a person who uses the system" is one nobody reads to the entry that +mattered. Define what is specific to this project, ambiguous in general use, or +already being said three ways. diff --git a/internal/assets/templates/claude/skills/prd/SKILL.md b/internal/assets/templates/claude/skills/prd/SKILL.md new file mode 100644 index 0000000..c4e5d84 --- /dev/null +++ b/internal/assets/templates/claude/skills/prd/SKILL.md @@ -0,0 +1,85 @@ +--- +name: prd +description: Turn an initiative that spans more than one feature into a plan under plans/ — a decomposition where each leaf is either a task you will do here or a reference to a spec that will be built separately. Use it when someone arrives with a PRD, a roadmap item, an epic, or a rough idea too large for one spec, and the first job is to find out what it actually decomposes into. For a single feature whose shape is already clear, skip this and run `scc spec new`. +--- + +You take an initiative that is too big for one spec and turn it into a plan: one +file under `plans/`, holding the decomposition and nothing else. + +**A note on the word.** "PRD" is what you arrive with — a product document, a +roadmap line, a paragraph in a chat. It is not an artifact this project keeps. The +artifact is a **plan**, and the vocabulary in +[routing.md](../../rules/routing.md) is what the rest of the workspace uses. Do not +create a `docs/prd/` directory. + +## First, check this is a plan at all + +[routing.md](../../rules/routing.md) asks one question, and it is worth asking +before you start: does this need requirements and a design settled before any code? + +- **Too large for one spec** — decomposes into several. That is a plan. Continue. +- **One feature, requirements not yet settled** — that is a spec. Run + `scc spec new ` and write `requirements.md`. Do not wrap one feature in a + plan for ceremony. +- **Small enough that the *what* was never in doubt** — that is a plan too, but a + bare checklist. Skip the interrogation below and write the list. + +## Then find out what it really is + +An initiative arrives underspecified. Your job before writing anything is to make it +specific enough to decompose, and the fastest way there is **a small batch of +concrete multiple-choice questions**, asked once — not a long interview, and not +guessing: + +1. **Who is this for, and what can they not do today?** An initiative that cannot + name the user is not ready to be decomposed. +2. **What is in and what is explicitly out?** The out-list is the more valuable + half; it is the one that prevents the third spec from growing a fourth. +3. **What does "done" mean for the whole of it?** Not per feature — the condition + under which the initiative is finished and the plan can be closed. +4. **What is already decided and not up for discussion?** A deadline, a vendor, an + existing system that has to keep working. + +Ask them together, offer concrete options rather than open prompts, and stop asking +once you can name the leaves. **If a question's answer would not change the +decomposition, do not ask it.** + +## Decompose + +``` +scc plan new +``` + +Then fill it in. The rules the file must hold to: + +- **A leaf is either a spec reference or a task with a checkbox — never both.** Where + an item is a task, the box is its state. Where it references a spec, the state + lives in that spec and is read from there. Two records of one fact disagree, and + the copy is the one that goes stale. `scc validate` reports this as + `plan.item-has-two-records`. +- **A leaf that is a spec is an ordinary spec.** `specs//` is not nested + under the plan and is built by exactly the same rules as one somebody asked for + directly. +- **Each spec-sized leaf is one coherent feature** — something a person could + describe in a sentence and verify on its own. If a leaf needs three sentences and + an "and", it is two leaves. +- **Say what order they go in, and what depends on what**, under `## Notes`. This is + the part only you know right now, and the part the next session most needs. +- **A plan is work, not knowledge.** It lives in `plans/`, never in `docs/`. + +Referenced specs must exist, or `scc validate` reports `plan.unknown-spec`. Create +them with `scc spec new` as you name them — an empty spec directory is a real +placeholder; a reference to nothing is a broken plan. + +## Before you hand it over + +- `scc validate` — exit 0, or fix what it names. +- **Read the leaf list back as a whole and ask what is missing.** Migration, + backfill, the switch-over, the thing that has to keep working while this ships, + and how it gets turned off if it goes wrong. Decompositions fail at the seams, not + in the middle of a feature. +- **Check the vocabulary.** Every term the plan coins is a term three specs will + inherit. If any of them is contested or new, use the `glossary` skill now, while + it costs one edit. +- **A choice made here that is hard to reverse is an ADR**, not a line in the plan's + notes. Use the `adr` skill. diff --git a/internal/assets/templates/claude/skills/stack/SKILL.md b/internal/assets/templates/claude/skills/stack/SKILL.md new file mode 100644 index 0000000..720902a --- /dev/null +++ b/internal/assets/templates/claude/skills/stack/SKILL.md @@ -0,0 +1,74 @@ +--- +name: stack +description: Own docs/stack.md — every adopted technology with one line on why it earned its place. Use it before adding a dependency to go.mod, package.json, or any other manifest; when a technology is being swapped, dropped, or evaluated; and when `scc validate` reports stack.missing or stack.undocumented-dependency, which means something is installed that nobody recorded a decision about. +--- + +You own `docs/stack.md`. The rule it enforces is in +[knowledge-base.md](../../rules/knowledge-base.md), and it is short: **technology +not listed here is an open decision, never something adopted silently.** + +This is checkable because dependency manifests are structured data. `scc validate` +reads the direct dependencies out of `go.mod` and `package.json` and reports any +that `stack.md` does not mention. Indirect dependencies are skipped — nobody decided +those. + +## Adding a dependency is two steps + +Adding it to the manifest is the first. The second is here, and it happens in the +same change, not later: + +1. **Say what problem it solves** — the one you actually have, not the category. +2. **Say what it was chosen over**, if anything real was considered. This is the + line that pays off in a year, when someone asks why not the obvious alternative. +3. **Say what it costs.** Every dependency is a supply-chain surface, a version to + keep current, and an API someone will have to learn. If you cannot name the cost + you have not finished evaluating it. + +```markdown +## Go + +- **stdlib only** — the binary ships to six platforms and every dependency is a + supply-chain surface. A dependency has to be worth that. +``` + +The validator matches on the module path or its last segment, so writing `chi` for +`github.com/go-chi/chi` is enough. It is looking for a decision, not a spelling. + +## Before you add anything + +Ask the three questions, in this order: + +1. **Can the standard library do it?** For most of what a dependency is reached for + first, it can, at the cost of code you then own. Sometimes that trade is right. +2. **Is this dependency load-bearing or convenient?** A convenience with a + maintainer who stops answering is a migration you did not plan. +3. **Does the project already have something that does this?** Two libraries solving + one problem is a cost paid by everyone who reads the code afterwards, forever. + +**If the answer is not obviously yes, stop and ask.** Adopting technology is a +decision with a long tail, and it is one of the few things worth interrupting an +automatic run for. Bringing a dependency in unasked is not autonomy, it is a +commitment made on someone else's behalf. + +## Removing one + +Delete it from the manifest, delete the entry, and say in the commit what replaced +it. A stack entry for something no longer installed is worse than a missing one: it +describes a project that does not exist. + +## When it is also an ADR + +`stack.md` is the inventory — what is adopted, and why. An **ADR** is the record of +a decision that is hard to reverse. Reach for the `adr` skill as well when the +choice locks something in: a database, a wire protocol, a framework the code will +shape itself around, a vendor with data in it. + +The two do not duplicate each other. The stack entry stays one line and names the +ADR; the ADR carries the context, the alternatives, and the consequences. + +## Findings + +| Finding | What it means | The fix | +|---|---|---| +| `stack.missing` | The project declares dependencies and has no `stack.md`. | Write it, one entry per direct dependency already installed. Expect to discover at least one nobody can justify. | +| `stack.undocumented-dependency` | Something is installed that `stack.md` does not mention. | Record the decision — or, if no one can state what it is for, remove the dependency. Both are correct outcomes. Silencing the finding by listing the name with no reason is not. | diff --git a/internal/assets/templates/claude/skills/wiki/SKILL.md b/internal/assets/templates/claude/skills/wiki/SKILL.md new file mode 100644 index 0000000..e9be4b4 --- /dev/null +++ b/internal/assets/templates/claude/skills/wiki/SKILL.md @@ -0,0 +1,72 @@ +--- +name: wiki +description: Build and maintain docs/wiki/ — one page per concept, linked with wikilinks and reachable from index.md. Use it when a source lands in docs/raw/ and has to be distilled into a page, when a question is better answered from accumulated knowledge than from re-reading the code, and when `scc validate` reports wiki.* findings — a broken wikilink, an orphan page, a changelog naming a page that no longer exists, or a source still sitting unprocessed in docs/raw/. +--- + +You own `docs/wiki/`: the durable half of what this project knows. A spec says what +one feature does now; the wiki says what is true across features and outlasts them. + +The format is not yours to invent — it is in +[knowledge-base.md](../../rules/knowledge-base.md), and `scc validate` enforces it. +This skill is the procedure. + +## Ingest — something landed in `docs/raw/` + +`raw/` is a drop box, not storage. A file still there is unfinished work, and +`scc validate` says so by name. Empty it: + +1. **Read the source completely** before writing anything. A page distilled from a + skim is worse than no page — it looks authoritative and is not. +2. **Decide the concept, not the document.** One page per concept. A source that + covers three concepts becomes three pages, or one page and two edits to pages + that already exist. Do not mirror the source's own structure. +3. **Check `docs/glossary.md` first.** If the source names something the project + already has a canonical term for, use the canonical term. If it coins a term + worth keeping, that is the `glossary` skill's job — do it, then come back. +4. **Write the page** at `docs/wiki/.md`. The filename is the slug, so + `order-total.md` is what `[[order-total]]` resolves to. +5. **Link it in.** Add a `[[wikilink]]` from `index.md`, or from a page already + reachable from it. A page nobody links is a page nobody finds again. +6. **Record it in `changelog.md`** — what changed, naming the pages with wikilinks. +7. **Delete the source from `docs/raw/`.** This is the step that gets skipped. The + finding exists because of it. +8. `scc validate` before you call it done. + +A source that turns out not to be worth a page still gets deleted. "Read and +rejected" is a complete outcome; leaving the file behind pretends the work is +pending. + +## Query — answer from what is already known + +Read `index.md` first, follow wikilinks from there, and answer from the pages. Cite +the page you answered from so the reader can check you. + +If the wiki does not cover it, say so plainly and go read the code. Then consider +whether what you just had to reconstruct should have been a page — the second time +someone reconstructs the same thing is the signal, not the first. + +## Maintain — clear what `scc validate` reports + +| Finding | What it means | The fix | +|---|---|---| +| `wiki.missing-index` | Pages exist with no entry point, so every page is an orphan. | Write `index.md` and link the pages. | +| `wiki.broken-link` | A `[[wikilink]]` resolves to no page. | Fix the slug, or write the page it was reaching for. | +| `wiki.orphan-page` | The page exists and nothing reaches it. | Link it from `index.md` or from a reachable page. Or delete it — an unreachable page nobody has missed is a candidate. | +| `wiki.missing-changelog` | The wiki has pages and no log of how it got that way. | Write `changelog.md`. | +| `wiki.changelog-desync` | The log names a page that no longer exists. | The page was renamed or removed; the log is a record, so correct the entry rather than rewriting history around it. | +| `wiki.unprocessed-source` | A file is still in `docs/raw/`. | Ingest it, above. | + +**`index.md` is a map, not a dump.** When it grows into a flat list of every page, +group it — the entry point earns its keep by making the shape of the knowledge +visible, and a hundred unsorted links do the opposite. + +## What does not belong here + +- **A decision that is hard to reverse.** That is an ADR — use the `adr` skill. The + wiki can link to it; it does not restate it. +- **An explanation of specific code.** That is `docs/codewiki/`, where every claim + cites the lines it is about — use the `codewiki` skill. +- **What one feature does now.** That is `specs//requirements.md`. +- **Anything you have not verified.** The wiki's whole value is that it can be + trusted without checking. One confidently wrong page costs more than the ten + correct ones beside it earn. diff --git a/internal/cli/cli.go b/internal/cli/cli.go index ea61759..33059aa 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -100,7 +100,7 @@ Usage: %s [flags] Commands: - init Scaffold a workspace: rules, review agents, layout, manifest + init Scaffold a workspace: rules, agents, skills, commands, layout, manifest spec Create and inspect specs — new | list | show | delete | validate plan Create and inspect plans — new | list | delete | validate skill Agent Skills conformance — validate diff --git a/internal/cli/init.go b/internal/cli/init.go index cfd1f37..51ec260 100644 --- a/internal/cli/init.go +++ b/internal/cli/init.go @@ -10,8 +10,9 @@ import ( "github.com/protonspy/spec-claude-code/internal/workspace" ) -// runInit scaffolds a workspace: the rules, the review agents, the directory -// layout, and the manifest that makes the directory findable as a workspace. +// runInit scaffolds a workspace: the rules, the review agents, the knowledge-base +// skills and their commands, the directory layout, and the manifest that makes the +// directory findable as a workspace. // // It is safe to re-run. Without --force it never overwrites anything, so "run init // again" is the honest upgrade story until `scc update` exists — additive by diff --git a/internal/cli/validate_test.go b/internal/cli/validate_test.go index 87aefc1..61683b9 100644 --- a/internal/cli/validate_test.go +++ b/internal/cli/validate_test.go @@ -7,6 +7,7 @@ import ( "strings" "testing" + "github.com/protonspy/spec-claude-code/internal/assets" "github.com/protonspy/spec-claude-code/internal/paths" "github.com/protonspy/spec-claude-code/internal/validate" ) @@ -49,6 +50,34 @@ func TestFreshArtifactsPassTheirOwnValidators(t *testing.T) { } } +// The shipped skills pass scc's own Agent Skills validator — and there are shipped +// skills for it to pass. The count check is the point: without it this test stays +// green on a workspace containing no skills at all, which is what it asserted for as +// long as none shipped. +func TestScaffoldedSkillsPassTheSkillValidator(t *testing.T) { + root := initWorkspace(t) + + entries, err := os.ReadDir(paths.Skills(root)) + if err != nil { + t.Fatalf("init scaffolded no skills directory: %v", err) + } + var got []string + for _, e := range entries { + if e.IsDir() { + got = append(got, e.Name()) + } + } + if len(got) != len(assets.KnowledgeSkills) { + t.Errorf("scaffolded skills = %v, want %v", got, assets.KnowledgeSkills) + } + + stdout, stderr, code := run(t, "skill", "validate", "--root", root, "--json") + if code != ExitOK { + t.Errorf("skill validate on the skills scc itself ships exited %d\nstdout: %s\nstderr: %s", + code, stdout, stderr) + } +} + // Exit 2 is the contract: a finding is a legitimate answer to a lint question, not a // failure of the tool, and CI branches on the difference. func TestValidateExitsTwoOnFindings(t *testing.T) {