From d89eb51a61424155d9df63895a75b0f2e97fa14f Mon Sep 17 00:00:00 2001 From: Joe Beda Date: Sun, 26 Jul 2026 11:11:13 -0700 Subject: [PATCH 1/2] chore(skills): add grilling and grill-with-docs for design interviews Two skills for working on modelith, under .claude/skills/ (distinct from plugin/skills/, which ships to users). grilling is the interview engine, taken verbatim from mattpocock/skills: one question at a time, look up facts but put decisions to the user, and do not act until shared understanding is confirmed. grill-with-docs is rewritten for this repo. Upstream's version maintains a CONTEXT.md glossary and carries its own ADR format doc; both are dropped here. modelith's vocabulary already lives in the schema, the structs, and the schema reference, two of which TestSchemaStructSync holds in sync, and .claude/rules/adr.md already sets the ADR bar. What the skill keeps is the capture map: write the ADR inline when a decision lands, flag format changes rather than editing them mid-session (they are test-coupled across three files), and leave running state to HANDOFF.md. Record upstream provenance and the MIT attribution in .claude/skills/SOURCES.md, kept outside any skill directory so it does not load into agent context, and note the convention in CLAUDE.md. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Joe Beda --- .claude/skills/SOURCES.md | 38 ++++++++++++++++ .claude/skills/grill-with-docs/SKILL.md | 60 +++++++++++++++++++++++++ .claude/skills/grilling/SKILL.md | 12 +++++ CLAUDE.md | 6 +++ 4 files changed, 116 insertions(+) create mode 100644 .claude/skills/SOURCES.md create mode 100644 .claude/skills/grill-with-docs/SKILL.md create mode 100644 .claude/skills/grilling/SKILL.md diff --git a/.claude/skills/SOURCES.md b/.claude/skills/SOURCES.md new file mode 100644 index 0000000..1bdf612 --- /dev/null +++ b/.claude/skills/SOURCES.md @@ -0,0 +1,38 @@ +# Where the skills in this directory came from + +Provenance and licence notes for skills imported from outside this repo. This +file is not a skill and is not loaded into an agent's context; it exists so the +origin of borrowed text is auditable. + +**Convention:** when importing a skill from elsewhere, add an entry here +recording the upstream source, the commit it was taken at, its licence, and +what was changed. Skills written from scratch for modelith don't need an entry. + +## grilling + +- **Upstream:** [`mattpocock/skills`](https://github.com/mattpocock/skills), + `skills/productivity/grilling/SKILL.md` +- **Taken at:** commit `ed37663cc5fbef691ddfecd080dff42f7e7e350d` (2026-07-21) +- **Licence:** MIT — Copyright (c) 2026 Matt Pocock. Full text: + +- **Changes:** none; the body is verbatim upstream. + +## grill-with-docs + +- **Upstream:** same repo, `skills/engineering/grill-with-docs/SKILL.md` and + the `skills/engineering/domain-modeling/` skill it delegates to, at the same + commit. +- **Licence:** MIT, as above. +- **Changes:** substantially rewritten for this repo. Upstream maintains a + `CONTEXT.md` glossary and its own ADR format doc; both were dropped, because + modelith's schema, structs, and schema reference already hold that vocabulary + under test, and `.claude/rules/adr.md` already sets the ADR bar. The retained + upstream material is the session-conduct advice (sharpen fuzzy language, + stress-test with scenarios, cross-reference the code) plus the + emission-frequency probe. + +Upstream also ships an installer (`npx skills@latest add mattpocock/skills`, +from [`vercel-labs/skills`](https://github.com/vercel-labs/skills)) and a +Claude Code plugin marketplace entry. Neither is used here: the installer +writes no lockfile, so it records no less than this file does, and both of the +skills above are edited rather than tracked upstream. diff --git a/.claude/skills/grill-with-docs/SKILL.md b/.claude/skills/grill-with-docs/SKILL.md new file mode 100644 index 0000000..0e4bb8d --- /dev/null +++ b/.claude/skills/grill-with-docs/SKILL.md @@ -0,0 +1,60 @@ +--- +name: grill-with-docs +description: A relentless interview to sharpen a plan or design, capturing decisions in this repo's canonical homes as they land. Use when the user wants to stress-test a design and record what comes out of it. +disable-model-invocation: true +--- + +Run a `/grilling` session over the plan or decision at hand, then capture what +crystallises where it belongs. Capture inline, as each decision lands — not +batched at the end. + +## Where things land in this repo + +**Hard-to-reverse decisions with a real trade-off → write the ADR now.** +`project-docs/adr/`, shape and bar in [`.claude/rules/adr.md`](../../rules/adr.md). +Read that rule before writing one; it is stricter than the generic advice +(sequential numbering, supersede rather than edit, and a `TestADR_NNNN_*` test +when the decision is one a test can pin). + +**Format or vocabulary changes → flag them, do not write them.** A new schema +key, a renamed concept, a changed definition: note it for the implementing +change and move on. Format vocabulary lives in three places that are checked +against each other — `internal/schema/v1/modelith.schema.json`, +`internal/model/model.go` (guarded by `TestSchemaStructSync`), and +`docs/06-schema-reference.md` — plus the golden `examples/`. Editing one of +them mid-grilling leaves the repo failing `task check` with the design still +half-argued. + +**Running state → `HANDOFF.md`** at the end of the session (gitignored, +local-only; see `CLAUDE.md`). + +## This repo has no CONTEXT.md, deliberately + +The upstream version of this skill maintains a `CONTEXT.md` glossary. Don't +create one here. modelith's domain *is* the domain-model format, and that +vocabulary is already defined in the schema, the Go structs, and the schema +reference — two of which a test holds in sync. A `CONTEXT.md` would be a fourth +copy that nothing checks. + +For the same reason, don't propose modelling modelith in `*.modelith.yaml`. +It would drift from the enforced definitions and compete with `examples/` for +the worked-example job. + +## During the session + +Beyond the interview itself, the angles worth pressing: + +- **Sharpen fuzzy language.** When a term is vague or overloaded, propose a + precise canonical one. Two names for one concept, or one name for two, is the + bug that this whole tool exists to catch — don't let it pass in our own + design conversation. +- **Stress-test with concrete scenarios.** Invent cases that probe the edges + and force precision about where one concept stops and the next starts. +- **Cross-reference against the code.** When a claim is made about how modelith + behaves, check it. The linter, the renderers, and the schema are right here; + a contradiction between what we believe and what the binary does is a finding. +- **Probe emission frequency.** When the design emits something observable — a + lint diagnostic, a log line, a rendered element — pin down how often it fires: + once per model, per entity, or per attribute? A diagnostic that turns out to + fire per attribute is a wall of output, and usually belongs on a different + surface. diff --git a/.claude/skills/grilling/SKILL.md b/.claude/skills/grilling/SKILL.md new file mode 100644 index 0000000..52d8eb3 --- /dev/null +++ b/.claude/skills/grilling/SKILL.md @@ -0,0 +1,12 @@ +--- +name: grilling +description: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases. +--- + +Interview me relentlessly about every aspect of this until we reach a shared understanding. Walk down each branch of the decision tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. + +Ask the questions one at a time, waiting for feedback on each question before continuing. Asking multiple questions at once is bewildering. + +If a *fact* can be found by exploring the environment (filesystem, tools, etc.), look it up rather than asking me. The *decisions*, though, are mine — put each one to me and wait for my answer. + +Do not act on it until I confirm we have reached a shared understanding. diff --git a/CLAUDE.md b/CLAUDE.md index 14c0531..a914f63 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -160,6 +160,12 @@ tracked as [issue #1](https://github.com/stacklok/modelith/issues/1). [`.claude/rules/agent-workflow.md`](./.claude/rules/agent-workflow.md). It is available discipline for changes that put a correctness-critical surface at risk, not a mandate on every commit. +- **Imported skills record where they came from.** + [`.claude/skills/`](./.claude/skills/) holds skills for working *on* modelith + (distinct from [`plugin/skills/`](./plugin/skills/), which ships to users). + When a skill is copied in from elsewhere, add its upstream source, commit, + licence, and local changes to + [`.claude/skills/SOURCES.md`](./.claude/skills/SOURCES.md). - **Scratch work** (spikes, smoke tests, throwaway fixtures, review round-records) goes in the repo-local, gitignored [`.scratch/`](./.scratch/) — not `/tmp` or a session temp dir. It stays From 659a70afcddcdb0f0dba3ab9f2d2b17b2d817ce5 Mon Sep 17 00:00:00 2001 From: Joe Beda Date: Sun, 26 Jul 2026 11:11:13 -0700 Subject: [PATCH 2/2] docs(adr): record that modelith is not modelled in modelith (ADR-0009) The self-model is the obvious dogfooding move and the question keeps coming up. Decide against it: the format's vocabulary already lives in the schema, the structs, and the schema reference, two of which TestSchemaStructSync holds in sync. A self-model would be a fourth copy that nothing checks, and drift between the tool's own model and its own schema is the worst possible advertisement for it. It would also compete with examples/ for the worked-example job. Pinned by TestADR_0009_NoSelfModel: the set of committed *.modelith.yaml files must be exactly the two known examples. Verified to fail on an added root-level model, not just to pass on the status quo. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Joe Beda --- internal/model/selfmodel_test.go | 85 +++++++++++++++++++ ...09-modelith-is-not-modelled-in-modelith.md | 59 +++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 internal/model/selfmodel_test.go create mode 100644 project-docs/adr/0009-modelith-is-not-modelled-in-modelith.md diff --git a/internal/model/selfmodel_test.go b/internal/model/selfmodel_test.go new file mode 100644 index 0000000..d503570 --- /dev/null +++ b/internal/model/selfmodel_test.go @@ -0,0 +1,85 @@ +package model + +import ( + "io/fs" + "os" + "path/filepath" + "slices" + "strings" + "testing" +) + +// TestADR_0009_NoSelfModel guards ADR-0009: modelith's own domain is not +// modelled in a *.modelith.yaml. The vocabulary lives in the schema, these +// structs, and docs/06-schema-reference.md, two of which TestSchemaStructSync +// holds in sync; a self-model would be a fourth, unchecked copy. +// +// The allowlist is the mechanism: a new model file in the repo fails this test +// until someone adds it here, which puts the ADR in front of them. +func TestADR_0009_NoSelfModel(t *testing.T) { + t.Parallel() + + allowed := map[string]string{ + "examples/example.modelith.yaml": "the worked example (golden fixture)", + "docs/05-parking-garage/garage.modelith.yaml": "the docs example (golden fixture)", + } + + root := repoRoot(t) + var found []string + err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + // Skip dot-directories: .git, and the gitignored .scratch/ and + // .claude/worktrees/, which are full of throwaway fixtures. + if d.IsDir() && strings.HasPrefix(d.Name(), ".") && path != root { + return fs.SkipDir + } + if d.IsDir() || !strings.HasSuffix(d.Name(), ".modelith.yaml") { + return nil + } + rel, err := filepath.Rel(root, path) + if err != nil { + return err + } + found = append(found, filepath.ToSlash(rel)) + return nil + }) + if err != nil { + t.Fatalf("walking repo: %v", err) + } + slices.Sort(found) + + for _, rel := range found { + if _, ok := allowed[rel]; !ok { + t.Errorf("unexpected model file %s\n"+ + "modelith does not model itself: see project-docs/adr/0009-modelith-is-not-modelled-in-modelith.md.\n"+ + "If this is a legitimate new example, add it to the allowlist in this test.", rel) + } + } + for rel, why := range allowed { + if !slices.Contains(found, rel) { + t.Errorf("allowlisted model file %s is missing (%s); update the allowlist if it moved", rel, why) + } + } +} + +// repoRoot walks up from the test's working directory to the directory holding +// go.mod. +func repoRoot(t *testing.T) string { + t.Helper() + dir, err := os.Getwd() + if err != nil { + t.Fatalf("getwd: %v", err) + } + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir + } + parent := filepath.Dir(dir) + if parent == dir { + t.Fatal("could not find repo root (no go.mod in any parent)") + } + dir = parent + } +} diff --git a/project-docs/adr/0009-modelith-is-not-modelled-in-modelith.md b/project-docs/adr/0009-modelith-is-not-modelled-in-modelith.md new file mode 100644 index 0000000..e26857d --- /dev/null +++ b/project-docs/adr/0009-modelith-is-not-modelled-in-modelith.md @@ -0,0 +1,59 @@ +# modelith is not modelled in modelith + +Do not write a `*.modelith.yaml` describing modelith's own domain — the domain +model format itself. The vocabulary it would capture is already defined in +three places that tests hold in sync; a self-model would be a fourth copy that +nothing checks, and it would drift. + +## Context + +modelith's domain is the domain-model format: entities, attributes, +relationships, invariants, actions, scenarios, enums, glossary terms. Modelling +that in modelith is the obvious dogfooding move, and the question comes up +naturally — most recently while adapting a design-interview skill whose +upstream version maintains a `CONTEXT.md` glossary. + +## Decision + +There is no self-model, and no `CONTEXT.md` either. The format's vocabulary +lives where it already lives: + +- `internal/schema/v1/modelith.schema.json` — the normative definition. +- `internal/model/model.go` — the Go structs, held property-for-property + against the schema by `TestSchemaStructSync`. +- `docs/06-schema-reference.md` — the prose definition users read. +- `examples/example.modelith.yaml` and `docs/05-parking-garage/` — the worked + examples, rendered under `task render-check` and pinned by a golden test. + +## Why + +- **A fourth copy that nothing checks.** Two of the three definitions are + machine-checked against each other. A self-model would join them as prose + nobody validates, and drift between a tool's own model and its own schema is + the worst possible advertisement for the tool. +- **It would compete with the worked examples.** `examples/` exists to show what + a good model looks like, and it is a golden fixture. A second in-repo model + with a stronger claim to being "the real one" muddies which file a newcomer + should read, and invites edits made for demo reasons. +- **The self-referential framing costs more than it teaches.** A model whose + `Entity` entity has an `attributes` relationship to an `Attribute` entity + reads as a puzzle. Readers learning the format are better served by the + parking garage. +- **Dogfooding still happens, elsewhere.** The format is exercised by real + models in other repos, which is where the pressure that produced issues #8–#13 + and #24–#25 came from. That is the useful kind of dogfooding: a model of a + different domain, hitting real gaps. + +## Consequences + +- `.claude/skills/grill-with-docs/` states this so a design-interview session + doesn't helpfully start one; it also drops the upstream skill's `CONTEXT.md` + behavior for the same reason. +- Format vocabulary changes land as one change across the schema, the structs, + and the schema reference — never as an edit to a self-model. +- Pinned by `TestADR_0009_NoSelfModel`, which asserts the set of committed + `*.modelith.yaml` files is exactly the known examples. Adding a legitimate + new example means extending that list, which puts this ADR in front of + whoever does it. +- Reopen if the format grows a construct that only a self-model can exercise — + a concrete gap, not the appeal of dogfooding.