Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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. |
Expand Down
34 changes: 34 additions & 0 deletions design/orchestration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<name>.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:
Expand Down
21 changes: 14 additions & 7 deletions design/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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`,
Expand Down
41 changes: 38 additions & 3 deletions internal/assets/assets.go
Original file line number Diff line number Diff line change
@@ -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:
//
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -104,17 +106,50 @@ 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.
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),
Expand Down
73 changes: 73 additions & 0 deletions internal/assets/assets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions internal/assets/templates/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ specs/<feature>/ requirements.md · design.md · tasks.md
plans/<name>.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
Expand Down
15 changes: 15 additions & 0 deletions internal/assets/templates/claude/commands/scc-adr.md
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 13 additions & 0 deletions internal/assets/templates/claude/commands/scc-codewiki.md
Original file line number Diff line number Diff line change
@@ -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]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Handle the documented repair mode explicitly.

The argument hint advertises repair, but the instructions only run validation when $ARGUMENTS is empty. /scc-codewiki repair therefore sets Area: repair and skips the codewiki.* repair workflow. Branch on the exact repair argument, or remove it from the argument hint.

Also applies to: 10-13

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/assets/templates/claude/commands/scc-codewiki.md` at line 3, Update
the command instructions around the argument handling to recognize the exact
“repair” argument explicitly and route it through the existing codewiki.* repair
workflow, instead of treating it as an area and skipping repair. Keep normal
area/path narration behavior unchanged; alternatively remove “repair” from the
argument hint if repair mode is not intended to be supported.

---

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.
12 changes: 12 additions & 0 deletions internal/assets/templates/claude/commands/scc-glossary.md
Original file line number Diff line number Diff line change
@@ -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.
16 changes: 16 additions & 0 deletions internal/assets/templates/claude/commands/scc-prd.md
Original file line number Diff line number Diff line change
@@ -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 <feature>` 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.
16 changes: 16 additions & 0 deletions internal/assets/templates/claude/commands/scc-stack.md
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 13 additions & 0 deletions internal/assets/templates/claude/commands/scc-wiki.md
Original file line number Diff line number Diff line change
@@ -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 <file> | query <question> | 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.
Loading
Loading