@stdd/cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/adapters/README.md +38 -0
- package/cli/lib.mjs +124 -0
- package/cli/stdd.mjs +363 -0
- package/method/README.md +141 -0
- package/package.json +50 -0
- package/playbooks/brainstorming.md +43 -0
- package/playbooks/debugging.md +36 -0
- package/playbooks/planning.md +39 -0
- package/playbooks/worktrees.md +39 -0
- package/templates/deferred-design.md +44 -0
- package/templates/pr-description.md +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Azamat Almazbek uulu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
<h1 align="center">stdd</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center"><strong>S</strong>pec + <strong>T</strong>est <strong>D</strong>riven <strong>D</strong>evelopment — a markdown-first methodology kit for teams building software with AI coding agents.</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://github.com/vsem-azamat/stdd/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/vsem-azamat/stdd/actions/workflows/ci.yml/badge.svg" /></a>
|
|
7
|
+
<a href="https://www.npmjs.com/package/@stdd/cli"><img alt="npm version" src="https://img.shields.io/npm/v/%40stdd%2Fcli?style=flat-square" /></a>
|
|
8
|
+
<img alt="node 20+" src="https://img.shields.io/badge/node-20%2B-brightgreen?style=flat-square" />
|
|
9
|
+
<img alt="zero dependencies" src="https://img.shields.io/badge/dependencies-0-blue?style=flat-square" />
|
|
10
|
+
<a href="./LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" /></a>
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
→ the docs edit IS the spec — reviewed before any code
|
|
15
|
+
→ tests gate behavior; visuals are design-first
|
|
16
|
+
→ working artifacts are never committed
|
|
17
|
+
→ one agent-neutral source, compiled per agent
|
|
18
|
+
→ mechanics over discipline — what can be checked, CI checks
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Why
|
|
22
|
+
|
|
23
|
+
AI coding agents amplify a quiet failure mode: **committed working artifacts**.
|
|
24
|
+
Plans and spec files written for one change land in the repo, go stale, and
|
|
25
|
+
keep winning code search — an agent greps the tree, finds a convincing
|
|
26
|
+
month-old spec that a newer doc contradicts, and confidently builds the wrong
|
|
27
|
+
thing. Popular SDD frameworks make this worse by design: change folders and
|
|
28
|
+
archives accumulate authoritative-looking text with no machine-readable
|
|
29
|
+
authority.
|
|
30
|
+
|
|
31
|
+
stdd inverts the model. Your permanent documentation tree is the **single**
|
|
32
|
+
source of truth. The edit to that tree is the spec — it comes first, in a
|
|
33
|
+
reviewable diff, before the failing test; the failing test comes before the
|
|
34
|
+
implementation. Everything ephemeral lives where ephemeral things belong:
|
|
35
|
+
rationale in the PR description, history in git, deferred designs as dated
|
|
36
|
+
project-log records. What can be verified mechanically, CI verifies; the
|
|
37
|
+
rest is a written contract you review against — not folklore.
|
|
38
|
+
|
|
39
|
+
## The loop
|
|
40
|
+
|
|
41
|
+
```mermaid
|
|
42
|
+
flowchart LR
|
|
43
|
+
A["Classify<br/>the change"] --> B{"Behavior<br/>change?"}
|
|
44
|
+
B -- "implementation-only" --> E["Implement"]
|
|
45
|
+
B -- "yes" --> C["Read docs →<br/><b>edit docs = the spec</b><br/>(first reviewable diff)"]
|
|
46
|
+
C --> D["Failing<br/>test"]
|
|
47
|
+
D --> E
|
|
48
|
+
E --> F["Verify"]
|
|
49
|
+
F --> G["PR evidence<br/>(CI-checked)"]
|
|
50
|
+
|
|
51
|
+
C -. "frontend visual work:<br/>design-first — build, review<br/>screenshots, then test behavior" .-> E
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Where knowledge lives
|
|
55
|
+
|
|
56
|
+
One truth inside the tree; everything ephemeral outside it — so an agent
|
|
57
|
+
grepping the repo can only find the present. The one dated exception, the
|
|
58
|
+
project log, is marked machine-readably (`authority: non-canonical`
|
|
59
|
+
frontmatter) and the generated agent instructions forbid searching it
|
|
60
|
+
unless the user explicitly asks for history or deferred work.
|
|
61
|
+
|
|
62
|
+
```mermaid
|
|
63
|
+
flowchart TD
|
|
64
|
+
CH(["a change"])
|
|
65
|
+
|
|
66
|
+
subgraph TREE["in the repository — what agents grep"]
|
|
67
|
+
DOCS["<b>docs/</b> — canonical truth<br/>present tense, single source"]
|
|
68
|
+
LOG["docs/project/ — dated records<br/>deferred designs, decisions<br/><i>non-canonical</i>"]
|
|
69
|
+
STDD[".stdd/ — method + playbooks"]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
subgraph OUT["outside the tree — explicit access only"]
|
|
73
|
+
PR["PR description<br/>rationale, alternatives, scope"]
|
|
74
|
+
GIT["git history<br/>what changed and why"]
|
|
75
|
+
PAD["session scratchpad<br/>plans, task lists"]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
CH -- "durable rules" --> DOCS
|
|
79
|
+
CH -- "deferred design" --> LOG
|
|
80
|
+
CH -- "rationale" --> PR
|
|
81
|
+
CH -- "history" --> GIT
|
|
82
|
+
CH -- "plans, sequencing" --> PAD
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## See it in action
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
You: We need admins to adjust an order's price before invoicing.
|
|
89
|
+
|
|
90
|
+
AI: [stdd-brainstorming] One question: order-level or per-item? …
|
|
91
|
+
Agreed: order-level signed adjustment, review-status only, reason
|
|
92
|
+
required, customer notified.
|
|
93
|
+
|
|
94
|
+
AI: Commit 1 — docs/domain/pricing.md: "Manual Price Adjustment" rules.
|
|
95
|
+
(the spec, reviewable on its own)
|
|
96
|
+
AI: Commit 2 — failing integration test for the new rules.
|
|
97
|
+
AI: Commit 3 — implementation; test green; verification output attached.
|
|
98
|
+
|
|
99
|
+
PR body:
|
|
100
|
+
Docs updated first: docs/domain/pricing.md
|
|
101
|
+
Decisions and alternatives: per-item adjustment rejected because …
|
|
102
|
+
|
|
103
|
+
CI: stdd check ✓ no committed working artifacts, no temporal narrative
|
|
104
|
+
stdd check-pr ✓ evidence line names the changed docs
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Quick start
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npm install -g @stdd/cli
|
|
111
|
+
cd your-project
|
|
112
|
+
stdd init --tools claude,codex
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`stdd init` installs `.stdd/` (the method contract + playbooks + config),
|
|
116
|
+
generates Claude Code skills, and prints the section to add to your
|
|
117
|
+
`AGENTS.md` for Codex and any other agent that reads it. Everything it
|
|
118
|
+
generates is recorded with content hashes in `.stdd/manifest.json`, so
|
|
119
|
+
`check` and `doctor` detect hand edits and stale copies of any generated
|
|
120
|
+
file — not just version drift.
|
|
121
|
+
|
|
122
|
+
Not sure where an existing repo stands? Get a report in seconds:
|
|
123
|
+
|
|
124
|
+
```console
|
|
125
|
+
$ npx @stdd/cli doctor
|
|
126
|
+
✗ 6 committed working artifacts may mislead coding agents
|
|
127
|
+
✗ 2 canonical docs contain temporal narrative
|
|
128
|
+
✓ generated files match stdd v0.0.1
|
|
129
|
+
✗ AGENTS.md has no STDD section — paste .stdd/AGENTS-snippet.md
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Then wire the guards into CI:
|
|
133
|
+
|
|
134
|
+
```yaml
|
|
135
|
+
- run: npx @stdd/cli check # tree invariants
|
|
136
|
+
- env:
|
|
137
|
+
PR_BODY: ${{ github.event.pull_request.body }}
|
|
138
|
+
run: printf '%s' "$PR_BODY" | npx @stdd/cli check-pr - # docs evidence line
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Commands
|
|
142
|
+
|
|
143
|
+
| Command | What it does |
|
|
144
|
+
| --- | --- |
|
|
145
|
+
| `stdd init [dir] [--tools claude,codex]` | Install `.stdd/` and compile playbooks per agent |
|
|
146
|
+
| `stdd doctor [dir]` | Adoption health report: setup, canonical docs, misleading artifacts, drift — exits 1 on findings |
|
|
147
|
+
| `stdd check [dir]` | CI guard: no committed working artifacts, no temporal narrative in canonical docs, no stale or hand-edited generated files |
|
|
148
|
+
| `stdd check-pr <file\|->` | CI guard: PR body carries exactly one non-empty docs evidence line |
|
|
149
|
+
|
|
150
|
+
All checks are configured in `.stdd/config.json` (glob patterns for
|
|
151
|
+
forbidden artifacts, canonical docs, temporal phrases).
|
|
152
|
+
|
|
153
|
+
## What's in the box
|
|
154
|
+
|
|
155
|
+
| Path | Contents |
|
|
156
|
+
| --- | --- |
|
|
157
|
+
| [`method/`](method/README.md) | The STDD contract: the loop, the rules, the exceptions |
|
|
158
|
+
| [`playbooks/`](playbooks/) | Agent-neutral playbooks: brainstorming, planning, debugging, worktrees |
|
|
159
|
+
| [`templates/`](templates/) | PR description and deferred-design templates |
|
|
160
|
+
| [`adapters/`](adapters/README.md) | How playbooks compile per agent |
|
|
161
|
+
| [`cli/`](cli/) | Zero-dependency Node CLI |
|
|
162
|
+
|
|
163
|
+
## The method in five rules
|
|
164
|
+
|
|
165
|
+
1. **Classify first.** Behavior changes (anything observable) pass the full
|
|
166
|
+
loop; implementation-only changes skip the docs step.
|
|
167
|
+
2. **The docs edit is the spec.** Missing or stale docs are updated before
|
|
168
|
+
tests and code, as the first reviewable unit of the change.
|
|
169
|
+
3. **Red before green.** A failing test gates every behavior change —
|
|
170
|
+
except frontend *visual* work, which is design-first: build, review
|
|
171
|
+
screenshots, then test only real behavior contracts.
|
|
172
|
+
4. **Working artifacts are never committed.** Rationale → PR description;
|
|
173
|
+
history → git; deferred designs → dated project-log entries.
|
|
174
|
+
5. **Evidence, not claims.** Every PR states `Docs updated first:` /
|
|
175
|
+
`Docs checked, no change needed:` / `Docs not applicable:` — naming the
|
|
176
|
+
docs or the reason. CI rejects a missing, duplicated, or bare label.
|
|
177
|
+
|
|
178
|
+
The full contract: [`method/README.md`](method/README.md).
|
|
179
|
+
|
|
180
|
+
## How it compares
|
|
181
|
+
|
|
182
|
+
**vs. [OpenSpec](https://github.com/Fission-AI/OpenSpec)** — OpenSpec models
|
|
183
|
+
changes as committed folders that archive into the repo; specs accumulate
|
|
184
|
+
alongside a separate docs reality. stdd keeps one truth (your docs tree),
|
|
185
|
+
borrows OpenSpec's best mechanics (delta discipline, drift detection,
|
|
186
|
+
init/update UX), and rejects the archive.
|
|
187
|
+
|
|
188
|
+
**vs. [Superpowers](https://github.com/obra/superpowers)** — great process
|
|
189
|
+
content, but Claude-only and delivered through plugin hooks. stdd rewrites
|
|
190
|
+
the process agent-neutrally and compiles it for every agent your team runs.
|
|
191
|
+
|
|
192
|
+
**vs. nothing** — chat context evaporates; agents build before agreeing on
|
|
193
|
+
scope; stale plans mislead the next session. stdd fixes the workflow, not
|
|
194
|
+
the model.
|
|
195
|
+
|
|
196
|
+
## Development
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
npm ci
|
|
200
|
+
npm test # node:test — unit + CLI integration
|
|
201
|
+
npm run check # Biome (Rust) — lint + format, CI mode
|
|
202
|
+
npm run format # Biome — write fixes
|
|
203
|
+
npm run selfcheck # stdd check on this repo (dogfooding)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
This repository follows its own method: PRs carry a docs evidence line
|
|
207
|
+
(enforced by CI via `stdd check-pr`), and no working artifacts are committed.
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Adapters
|
|
2
|
+
|
|
3
|
+
Playbooks are agent-neutral markdown with frontmatter (`name`, `description`,
|
|
4
|
+
`when`). Adapters compile them into what each agent consumes. `stdd init`
|
|
5
|
+
runs the adapters; re-run it after upgrading stdd to refresh the output.
|
|
6
|
+
|
|
7
|
+
## Common output
|
|
8
|
+
|
|
9
|
+
Every init installs `.stdd/` into the target repo:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
.stdd/
|
|
13
|
+
├── method.md # the STDD contract (copy of method/README.md)
|
|
14
|
+
├── playbooks/ # agent-neutral playbooks
|
|
15
|
+
└── config.json # stdd check configuration
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`.stdd/` is committed — it is methodology, not a working artifact.
|
|
19
|
+
|
|
20
|
+
## claude (Claude Code)
|
|
21
|
+
|
|
22
|
+
Writes one skill per playbook to `.claude/skills/<name>/SKILL.md`:
|
|
23
|
+
frontmatter maps `name`/`description` directly; the body is the playbook
|
|
24
|
+
body. Skills are self-contained copies — regenerate, never hand-edit.
|
|
25
|
+
|
|
26
|
+
## codex (and any agent that reads AGENTS.md)
|
|
27
|
+
|
|
28
|
+
Writes `.stdd/AGENTS-snippet.md` and prints it. Paste (or `@`-include) the
|
|
29
|
+
snippet into the repo's `AGENTS.md`. The snippet is short: it points the
|
|
30
|
+
agent at `.stdd/method.md` and lists the playbooks with their `when` lines.
|
|
31
|
+
|
|
32
|
+
## Design rules for adapters
|
|
33
|
+
|
|
34
|
+
- One source of truth: adapters copy or point, never fork playbook content.
|
|
35
|
+
- No agent-specific incantations inside `playbooks/` — if an agent needs
|
|
36
|
+
special framing, that framing lives in the adapter.
|
|
37
|
+
- Calm imperative prose. No all-caps compliance shouting: if a rule needs
|
|
38
|
+
shouting to be followed, it needs a `stdd check` rule instead.
|
package/cli/lib.mjs
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/** Content fingerprint used by the generated-files manifest. */
|
|
4
|
+
export function sha256(content) {
|
|
5
|
+
return `sha256:${createHash("sha256").update(content).digest("hex")}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_CONFIG = {
|
|
9
|
+
// Working-artifact paths that must never be committed. `stdd check` fails
|
|
10
|
+
// if any tracked file matches. Deliberately narrow — widen per repo.
|
|
11
|
+
forbiddenArtifacts: ["docs/**/plans/**", "**/*.agent-plan.md", "**/*.agent-spec.md"],
|
|
12
|
+
// Canonical docs must describe the present, in English. `stdd check`
|
|
13
|
+
// flags temporal narrative that belongs in git history or a PR
|
|
14
|
+
// description. Fenced code blocks are skipped.
|
|
15
|
+
canonicalDocs: ["docs/domain/**/*.md", "docs/product/**/*.md"],
|
|
16
|
+
temporalPhrases: ["previously", "no longer", "used to be", "before this change"],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const EVIDENCE_LABELS = [
|
|
20
|
+
"Docs updated first",
|
|
21
|
+
"Docs checked, no change needed",
|
|
22
|
+
"Docs not applicable",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const EVIDENCE_MATCHERS = EVIDENCE_LABELS.map((label) => ({
|
|
26
|
+
label,
|
|
27
|
+
re: new RegExp(`^${label}:[ \\t]*(.*)$`, "i"),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Find docs evidence lines in a PR body. Only lines that start at the
|
|
32
|
+
* beginning of a line count — quoted templates (`> Docs …`) and fenced code
|
|
33
|
+
* blocks do not. Returns `{ label, content }` per hit; a bare label yields
|
|
34
|
+
* empty content.
|
|
35
|
+
*/
|
|
36
|
+
export function findEvidenceLines(body) {
|
|
37
|
+
const hits = [];
|
|
38
|
+
let inFence = false;
|
|
39
|
+
for (const line of body.replaceAll("\r\n", "\n").split("\n")) {
|
|
40
|
+
if (/^\s*(```|~~~)/.test(line)) {
|
|
41
|
+
inFence = !inFence;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (inFence) continue;
|
|
45
|
+
for (const { label, re } of EVIDENCE_MATCHERS) {
|
|
46
|
+
const m = re.exec(line);
|
|
47
|
+
if (m) hits.push({ label, content: m[1].trim() });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return hits;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Tiny glob dialect: `*` matches within a path segment, `**` matches across
|
|
55
|
+
* segments. No `?`, braces, or character classes — by design.
|
|
56
|
+
*/
|
|
57
|
+
export function globToRegExp(glob) {
|
|
58
|
+
const segments = glob.split("/");
|
|
59
|
+
const parts = segments.map((segment, i) => {
|
|
60
|
+
const last = i === segments.length - 1;
|
|
61
|
+
if (segment === "**") return last ? ".*" : "(?:[^/]+/)*";
|
|
62
|
+
const escaped = segment.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", "[^/]*");
|
|
63
|
+
return escaped + (last ? "" : "/");
|
|
64
|
+
});
|
|
65
|
+
return new RegExp(`^${parts.join("")}$`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Parse a `---`-fenced frontmatter block. CRLF-tolerant. */
|
|
69
|
+
export function parseFrontmatter(source) {
|
|
70
|
+
const normalized = source.replaceAll("\r\n", "\n");
|
|
71
|
+
const match = /^---\n([\s\S]*?)\n---\n?/.exec(normalized);
|
|
72
|
+
if (!match) return { meta: {}, body: normalized };
|
|
73
|
+
const meta = {};
|
|
74
|
+
for (const line of match[1].split("\n")) {
|
|
75
|
+
const idx = line.indexOf(":");
|
|
76
|
+
if (idx > 0) meta[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
77
|
+
}
|
|
78
|
+
return { meta, body: normalized.slice(match[0].length) };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Merge a parsed user config over the defaults and validate shape.
|
|
83
|
+
* Throws with an actionable message on invalid input.
|
|
84
|
+
*/
|
|
85
|
+
export function mergeConfig(parsed) {
|
|
86
|
+
const config = { ...DEFAULT_CONFIG, ...parsed };
|
|
87
|
+
for (const key of ["forbiddenArtifacts", "canonicalDocs", "temporalPhrases"]) {
|
|
88
|
+
if (!Array.isArray(config[key]) || config[key].some((v) => typeof v !== "string")) {
|
|
89
|
+
throw new Error(`"${key}" must be an array of strings`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return config;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Build a temporal-phrase matcher. Word-ish boundaries on both sides so
|
|
97
|
+
* hyphenated compounds ("no longer-lived") do not match.
|
|
98
|
+
*/
|
|
99
|
+
export function temporalMatchers(phrases) {
|
|
100
|
+
return phrases.map((phrase) => ({
|
|
101
|
+
phrase,
|
|
102
|
+
re: new RegExp(`(?<![\\w-])${phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w-])`, "i"),
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Scan markdown lines for temporal narrative, skipping fenced code blocks.
|
|
108
|
+
* Returns `{ line, phrase }` hits (1-indexed lines).
|
|
109
|
+
*/
|
|
110
|
+
export function scanTemporal(lines, matchers) {
|
|
111
|
+
const hits = [];
|
|
112
|
+
let inFence = false;
|
|
113
|
+
lines.forEach((line, i) => {
|
|
114
|
+
if (/^\s*(```|~~~)/.test(line)) {
|
|
115
|
+
inFence = !inFence;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (inFence) return;
|
|
119
|
+
for (const { phrase, re } of matchers) {
|
|
120
|
+
if (re.test(line)) hits.push({ line: i + 1, phrase });
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
return hits;
|
|
124
|
+
}
|
package/cli/stdd.mjs
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_CONFIG,
|
|
8
|
+
findEvidenceLines,
|
|
9
|
+
globToRegExp,
|
|
10
|
+
mergeConfig,
|
|
11
|
+
parseFrontmatter,
|
|
12
|
+
scanTemporal,
|
|
13
|
+
sha256,
|
|
14
|
+
temporalMatchers,
|
|
15
|
+
} from "./lib.mjs";
|
|
16
|
+
|
|
17
|
+
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
18
|
+
const VERSION = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf8")).version;
|
|
19
|
+
const KNOWN_TOOLS = ["claude", "codex"];
|
|
20
|
+
const STAMP = `generated by stdd v${VERSION} — do not edit, re-run \`stdd init\``;
|
|
21
|
+
|
|
22
|
+
function fail(message) {
|
|
23
|
+
console.error(`stdd: ${message}`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function listTrackedFiles(dir) {
|
|
28
|
+
try {
|
|
29
|
+
const out = execFileSync("git", ["-C", dir, "ls-files", "-z"], {
|
|
30
|
+
encoding: "utf8",
|
|
31
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
32
|
+
});
|
|
33
|
+
const files = out.split("\0").filter(Boolean);
|
|
34
|
+
if (files.length > 0) return files;
|
|
35
|
+
} catch {
|
|
36
|
+
// not a git repo — fall through to a filesystem walk
|
|
37
|
+
}
|
|
38
|
+
const files = [];
|
|
39
|
+
const walk = (rel) => {
|
|
40
|
+
for (const entry of fs.readdirSync(path.join(dir, rel), { withFileTypes: true })) {
|
|
41
|
+
if (entry.name === ".git" || entry.name === "node_modules") continue;
|
|
42
|
+
const relPath = rel ? `${rel}/${entry.name}` : entry.name;
|
|
43
|
+
if (entry.isDirectory()) walk(relPath);
|
|
44
|
+
else files.push(relPath);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
walk("");
|
|
48
|
+
return files;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function loadPlaybooks() {
|
|
52
|
+
const dir = path.join(PKG_ROOT, "playbooks");
|
|
53
|
+
return fs
|
|
54
|
+
.readdirSync(dir)
|
|
55
|
+
.filter((f) => f.endsWith(".md"))
|
|
56
|
+
.map((f) => {
|
|
57
|
+
const source = fs.readFileSync(path.join(dir, f), "utf8");
|
|
58
|
+
const { meta, body } = parseFrontmatter(source);
|
|
59
|
+
if (!meta.name || !meta.description) {
|
|
60
|
+
fail(`playbook ${f} is missing required frontmatter (name, description)`);
|
|
61
|
+
}
|
|
62
|
+
return { file: f, source, meta, body };
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function loadConfig(targetDir) {
|
|
67
|
+
const configPath = path.join(targetDir, ".stdd", "config.json");
|
|
68
|
+
if (!fs.existsSync(configPath)) return DEFAULT_CONFIG;
|
|
69
|
+
let parsed;
|
|
70
|
+
try {
|
|
71
|
+
parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
72
|
+
} catch (err) {
|
|
73
|
+
fail(`.stdd/config.json is not valid JSON: ${err.message}`);
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
return mergeConfig(parsed);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
fail(`.stdd/config.json: ${err.message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function init(targetDir, tools) {
|
|
83
|
+
const stddDir = path.join(targetDir, ".stdd");
|
|
84
|
+
fs.mkdirSync(path.join(stddDir, "playbooks"), { recursive: true });
|
|
85
|
+
// Every generated file is recorded here (repo-relative path → content
|
|
86
|
+
// hash) and written to .stdd/manifest.json, so check/doctor can detect
|
|
87
|
+
// hand edits, deletions, and stale copies. User-owned files (config.json)
|
|
88
|
+
// are deliberately not recorded.
|
|
89
|
+
const generated = {};
|
|
90
|
+
const writeGenerated = (relPath, content) => {
|
|
91
|
+
fs.writeFileSync(path.join(targetDir, ...relPath.split("/")), content);
|
|
92
|
+
generated[relPath] = sha256(content);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
writeGenerated(".stdd/method.md", fs.readFileSync(path.join(PKG_ROOT, "method", "README.md"), "utf8"));
|
|
96
|
+
const playbooks = loadPlaybooks();
|
|
97
|
+
for (const pb of playbooks) {
|
|
98
|
+
writeGenerated(`.stdd/playbooks/${pb.file}`, pb.source);
|
|
99
|
+
}
|
|
100
|
+
const configPath = path.join(stddDir, "config.json");
|
|
101
|
+
if (!fs.existsSync(configPath)) {
|
|
102
|
+
fs.writeFileSync(configPath, `${JSON.stringify(DEFAULT_CONFIG, null, "\t")}\n`);
|
|
103
|
+
}
|
|
104
|
+
console.log(`Installed .stdd/ (method, ${playbooks.length} playbooks, config)`);
|
|
105
|
+
|
|
106
|
+
if (tools.includes("claude")) {
|
|
107
|
+
for (const pb of playbooks) {
|
|
108
|
+
const skillDir = path.join(targetDir, ".claude", "skills", pb.meta.name);
|
|
109
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
110
|
+
const skill =
|
|
111
|
+
`---\nname: ${pb.meta.name}\ndescription: ${pb.meta.description}\n---\n\n` +
|
|
112
|
+
`<!-- ${STAMP} -->\n\n${pb.body}`;
|
|
113
|
+
writeGenerated(`.claude/skills/${pb.meta.name}/SKILL.md`, skill);
|
|
114
|
+
}
|
|
115
|
+
console.log(`Installed ${playbooks.length} Claude Code skills under .claude/skills/`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (tools.includes("codex")) {
|
|
119
|
+
const lines = [
|
|
120
|
+
`<!-- ${STAMP} -->`,
|
|
121
|
+
"",
|
|
122
|
+
"## STDD",
|
|
123
|
+
"",
|
|
124
|
+
"This repository follows the STDD method: read `.stdd/method.md` before",
|
|
125
|
+
"any behavior change and follow its loop (docs edit is the spec → failing",
|
|
126
|
+
"test → implement → verify → PR evidence).",
|
|
127
|
+
"",
|
|
128
|
+
"Do not search the project log (dated entries marked",
|
|
129
|
+
"`authority: non-canonical`, e.g. `docs/project/`) unless the user",
|
|
130
|
+
"explicitly asks for historical rationale or deferred work — it records",
|
|
131
|
+
"past decisions and future intentions, never the present.",
|
|
132
|
+
"",
|
|
133
|
+
"Playbooks in `.stdd/playbooks/`:",
|
|
134
|
+
"",
|
|
135
|
+
...playbooks.map((pb) => `- \`${pb.file}\` — ${pb.meta.when ?? pb.meta.description}`),
|
|
136
|
+
];
|
|
137
|
+
const snippet = `${lines.join("\n")}\n`;
|
|
138
|
+
writeGenerated(".stdd/AGENTS-snippet.md", snippet);
|
|
139
|
+
console.log("\nAdd this to your AGENTS.md (also saved to .stdd/AGENTS-snippet.md):\n");
|
|
140
|
+
console.log(snippet);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const manifest = { generatedBy: "stdd", version: VERSION, files: generated };
|
|
144
|
+
fs.writeFileSync(path.join(stddDir, "manifest.json"), `${JSON.stringify(manifest, null, "\t")}\n`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Shared repository scan for `check` and `doctor`: committed working
|
|
149
|
+
* artifacts, canonical docs and their temporal-narrative hits, and stale
|
|
150
|
+
* generated files (a stale stamp means the CLI was upgraded without
|
|
151
|
+
* re-running `stdd init`).
|
|
152
|
+
*/
|
|
153
|
+
function scanRepo(targetDir, config) {
|
|
154
|
+
const files = listTrackedFiles(targetDir);
|
|
155
|
+
|
|
156
|
+
const forbidden = config.forbiddenArtifacts.map(globToRegExp);
|
|
157
|
+
const artifacts = files.filter((file) => forbidden.some((re) => re.test(file)));
|
|
158
|
+
|
|
159
|
+
const canonical = config.canonicalDocs.map(globToRegExp);
|
|
160
|
+
const canonicalFiles = files.filter((file) => canonical.some((re) => re.test(file)));
|
|
161
|
+
const matchers = temporalMatchers(config.temporalPhrases);
|
|
162
|
+
const temporal = [];
|
|
163
|
+
for (const file of canonicalFiles) {
|
|
164
|
+
const lines = fs.readFileSync(path.join(targetDir, file), "utf8").split("\n");
|
|
165
|
+
for (const hit of scanTemporal(lines, matchers)) temporal.push({ file, ...hit });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { artifacts, canonicalFiles, temporal, stale: scanGeneratedDrift(targetDir) };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Generated-file drift, as `{ file, message }` findings. With a manifest
|
|
173
|
+
* (written by `stdd init`), every generated file is verified by content
|
|
174
|
+
* hash. Without one (repo initialized by an older stdd), fall back to the
|
|
175
|
+
* version stamp inside Claude skills.
|
|
176
|
+
*/
|
|
177
|
+
function scanGeneratedDrift(targetDir) {
|
|
178
|
+
const stale = [];
|
|
179
|
+
const manifestPath = path.join(targetDir, ".stdd", "manifest.json");
|
|
180
|
+
if (fs.existsSync(manifestPath)) {
|
|
181
|
+
let manifest;
|
|
182
|
+
try {
|
|
183
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
184
|
+
} catch {
|
|
185
|
+
return [{ file: ".stdd/manifest.json", message: "is not valid JSON — re-run stdd init" }];
|
|
186
|
+
}
|
|
187
|
+
if (manifest.version !== VERSION) {
|
|
188
|
+
stale.push({
|
|
189
|
+
file: ".stdd/manifest.json",
|
|
190
|
+
message: `written by stdd v${manifest.version}, CLI is v${VERSION} — re-run stdd init`,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
for (const [file, hash] of Object.entries(manifest.files ?? {})) {
|
|
194
|
+
const filePath = path.join(targetDir, ...file.split("/"));
|
|
195
|
+
if (!fs.existsSync(filePath)) {
|
|
196
|
+
stale.push({ file, message: "listed in .stdd/manifest.json but missing — re-run stdd init" });
|
|
197
|
+
} else if (sha256(fs.readFileSync(filePath, "utf8")) !== hash) {
|
|
198
|
+
stale.push({ file, message: "edited by hand or stale — re-run stdd init" });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return stale;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const stampRe = /generated by stdd v([0-9][^\s]*) —/;
|
|
205
|
+
const skillsDir = path.join(targetDir, ".claude", "skills");
|
|
206
|
+
if (fs.existsSync(skillsDir)) {
|
|
207
|
+
for (const name of fs.readdirSync(skillsDir)) {
|
|
208
|
+
const skillPath = path.join(skillsDir, name, "SKILL.md");
|
|
209
|
+
if (!fs.existsSync(skillPath)) continue;
|
|
210
|
+
const m = stampRe.exec(fs.readFileSync(skillPath, "utf8"));
|
|
211
|
+
if (m && m[1] !== VERSION) {
|
|
212
|
+
stale.push({
|
|
213
|
+
file: `.claude/skills/${name}/SKILL.md`,
|
|
214
|
+
message: `generated by stdd v${m[1]}, CLI is v${VERSION} — re-run stdd init`,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return stale;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function check(targetDir) {
|
|
223
|
+
const config = loadConfig(targetDir);
|
|
224
|
+
const { artifacts, temporal, stale } = scanRepo(targetDir, config);
|
|
225
|
+
const violations = [
|
|
226
|
+
...artifacts.map((file) => `${file}: committed working artifact (forbiddenArtifacts)`),
|
|
227
|
+
...temporal.map(
|
|
228
|
+
(hit) => `${hit.file}:${hit.line}: temporal narrative in canonical doc ("${hit.phrase}")`,
|
|
229
|
+
),
|
|
230
|
+
...stale.map((s) => `${s.file}: ${s.message}`),
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
if (violations.length > 0) {
|
|
234
|
+
console.error(`stdd check: ${violations.length} violation(s)\n`);
|
|
235
|
+
for (const v of violations) console.error(` ${v}`);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
console.log("stdd check: OK");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function doctor(targetDir) {
|
|
242
|
+
const count = (n, singular, plural) => `${n} ${n === 1 ? singular : plural}`;
|
|
243
|
+
let failed = false;
|
|
244
|
+
const report = (ok, message) => {
|
|
245
|
+
console.log(`${ok ? "✓" : "✗"} ${message}`);
|
|
246
|
+
if (!ok) failed = true;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const installed = fs.existsSync(path.join(targetDir, ".stdd", "method.md"));
|
|
250
|
+
report(installed, installed ? ".stdd/ installed" : ".stdd/ is not installed — run stdd init");
|
|
251
|
+
|
|
252
|
+
const config = loadConfig(targetDir);
|
|
253
|
+
const { artifacts, canonicalFiles, temporal, stale } = scanRepo(targetDir, config);
|
|
254
|
+
|
|
255
|
+
report(
|
|
256
|
+
canonicalFiles.length > 0,
|
|
257
|
+
canonicalFiles.length > 0
|
|
258
|
+
? `canonical docs present (${count(canonicalFiles.length, "file", "files")})`
|
|
259
|
+
: "no canonical docs found — create docs/domain/ or docs/product/, or adjust canonicalDocs",
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
report(
|
|
263
|
+
artifacts.length === 0,
|
|
264
|
+
artifacts.length === 0
|
|
265
|
+
? "no committed working artifacts"
|
|
266
|
+
: `${count(artifacts.length, "committed working artifact", "committed working artifacts")} may mislead coding agents`,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const temporalFiles = new Set(temporal.map((hit) => hit.file)).size;
|
|
270
|
+
report(
|
|
271
|
+
temporalFiles === 0,
|
|
272
|
+
temporalFiles === 0
|
|
273
|
+
? "canonical docs describe the present (no temporal narrative)"
|
|
274
|
+
: `${count(temporalFiles, "canonical doc contains", "canonical docs contain")} temporal narrative`,
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
report(
|
|
278
|
+
stale.length === 0,
|
|
279
|
+
stale.length === 0
|
|
280
|
+
? `generated files match stdd v${VERSION}`
|
|
281
|
+
: `${count(stale.length, "generated file is", "generated files are")} stale — re-run stdd init`,
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
const agentsPath = path.join(targetDir, "AGENTS.md");
|
|
285
|
+
if (fs.existsSync(agentsPath)) {
|
|
286
|
+
const hasSection = /^## STDD$/m.test(fs.readFileSync(agentsPath, "utf8"));
|
|
287
|
+
report(
|
|
288
|
+
hasSection,
|
|
289
|
+
hasSection
|
|
290
|
+
? "AGENTS.md carries the STDD section"
|
|
291
|
+
: "AGENTS.md has no STDD section — paste .stdd/AGENTS-snippet.md",
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (failed) process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function checkPr(prBodyFile) {
|
|
299
|
+
const body =
|
|
300
|
+
prBodyFile === "-" || !prBodyFile ? fs.readFileSync(0, "utf8") : fs.readFileSync(prBodyFile, "utf8");
|
|
301
|
+
const matches = findEvidenceLines(body);
|
|
302
|
+
if (matches.length === 0) {
|
|
303
|
+
fail(
|
|
304
|
+
"PR body has no docs evidence line. Add exactly one of:\n" +
|
|
305
|
+
" Docs updated first: <docs>\n" +
|
|
306
|
+
" Docs checked, no change needed: <docs + reason>\n" +
|
|
307
|
+
" Docs not applicable: <why implementation-only>",
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
if (matches.length > 1) {
|
|
311
|
+
fail("PR body has more than one docs evidence line — keep exactly one.");
|
|
312
|
+
}
|
|
313
|
+
if (matches[0].content === "") {
|
|
314
|
+
fail(`"${matches[0].label}:" names no evidence — list the docs or the reason after the colon.`);
|
|
315
|
+
}
|
|
316
|
+
console.log("stdd check-pr: OK");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// --- argument parsing (strict: unknown flags are errors) ---
|
|
320
|
+
const [, , command, ...rest] = process.argv;
|
|
321
|
+
let tools = null;
|
|
322
|
+
const positional = [];
|
|
323
|
+
for (let i = 0; i < rest.length; i++) {
|
|
324
|
+
const arg = rest[i];
|
|
325
|
+
if (arg === "--tools" || arg.startsWith("--tools=")) {
|
|
326
|
+
if (command !== "init") fail(`--tools is only valid for "stdd init"`);
|
|
327
|
+
const value = arg.includes("=") ? arg.slice("--tools=".length) : (rest[++i] ?? "");
|
|
328
|
+
tools = value.split(",").filter(Boolean);
|
|
329
|
+
if (tools.length === 0) fail("--tools requires a value, e.g. --tools claude,codex");
|
|
330
|
+
const unknown = tools.filter((t) => !KNOWN_TOOLS.includes(t));
|
|
331
|
+
if (unknown.length > 0) {
|
|
332
|
+
fail(`unknown tool(s): ${unknown.join(", ")} (known: ${KNOWN_TOOLS.join(", ")})`);
|
|
333
|
+
}
|
|
334
|
+
} else if (arg.startsWith("--")) {
|
|
335
|
+
fail(`unknown flag: ${arg}`);
|
|
336
|
+
} else {
|
|
337
|
+
positional.push(arg);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (positional.length > 1) fail(`unexpected argument: ${positional[1]}`);
|
|
341
|
+
const targetDir = path.resolve(positional[0] ?? ".");
|
|
342
|
+
|
|
343
|
+
switch (command) {
|
|
344
|
+
case "init":
|
|
345
|
+
init(targetDir, tools ?? KNOWN_TOOLS);
|
|
346
|
+
break;
|
|
347
|
+
case "check":
|
|
348
|
+
check(targetDir);
|
|
349
|
+
break;
|
|
350
|
+
case "doctor":
|
|
351
|
+
doctor(targetDir);
|
|
352
|
+
break;
|
|
353
|
+
case "check-pr":
|
|
354
|
+
checkPr(positional[0]);
|
|
355
|
+
break;
|
|
356
|
+
case "--version":
|
|
357
|
+
case "version":
|
|
358
|
+
console.log(VERSION);
|
|
359
|
+
break;
|
|
360
|
+
default:
|
|
361
|
+
console.log("Usage: stdd <init|check|check-pr|doctor> [dir|pr-body-file] [--tools claude,codex]");
|
|
362
|
+
process.exit(command ? 1 : 0);
|
|
363
|
+
}
|
package/method/README.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# The STDD Method
|
|
2
|
+
|
|
3
|
+
This is the working contract. It is written for the agent or developer doing
|
|
4
|
+
the change, in the order the work happens.
|
|
5
|
+
|
|
6
|
+
## Sources of truth
|
|
7
|
+
|
|
8
|
+
Every repository adopting STDD names a **permanent docs tree** (for example
|
|
9
|
+
`docs/`) with an explicit hierarchy — typically product intent above domain
|
|
10
|
+
rules above implementation layers. When layers disagree, stop and reconcile
|
|
11
|
+
before implementing.
|
|
12
|
+
|
|
13
|
+
Three artifacts make claims about behavior, each in its own way:
|
|
14
|
+
|
|
15
|
+
- **Docs are the intended contract** — what the system is supposed to do.
|
|
16
|
+
- **Tests are the executable contract** — what the system provably does.
|
|
17
|
+
- **Code is the observed implementation** — what the system actually does.
|
|
18
|
+
|
|
19
|
+
A disagreement between them blocks implementation until they are reconciled.
|
|
20
|
+
None silently overrides the others: stale docs get corrected, wrong tests get
|
|
21
|
+
fixed, accidental behavior gets documented or removed — each resolution is an
|
|
22
|
+
explicit decision, not a default in favor of any one artifact.
|
|
23
|
+
|
|
24
|
+
## The loop
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
classify → read docs → docs edit (the spec) → failing test → implement → verify → PR evidence
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
1. **Classify the change.**
|
|
31
|
+
- *Behavior:* anything a user, operator, or downstream system can observe —
|
|
32
|
+
workflows, pricing, states, permissions, API contracts, copy with
|
|
33
|
+
business meaning.
|
|
34
|
+
- *Implementation-only:* refactors, lint fixes, build plumbing, mechanical
|
|
35
|
+
dependency updates that alter no behavior or architecture contract.
|
|
36
|
+
2. **Read the relevant docs first.** For behavior changes, read the matching
|
|
37
|
+
source-of-truth documents before proposing anything.
|
|
38
|
+
3. **Edit the docs — that edit is the spec.** If the docs are missing, stale,
|
|
39
|
+
or ambiguous, update them before tests and code. Make the docs edit the
|
|
40
|
+
first reviewable unit — the first commit where commits are used, otherwise
|
|
41
|
+
the opening docs-only diff of the PR — so the behavior contract can be
|
|
42
|
+
reviewed on its own. If the docs already cover the behavior, do not add
|
|
43
|
+
duplicate prose — record that they were checked (see PR evidence).
|
|
44
|
+
4. **Write the failing test.** Red before green. Exception below.
|
|
45
|
+
5. **Implement** until the test passes, then refactor.
|
|
46
|
+
6. **Verify with the narrowest meaningful command.** Never claim "done",
|
|
47
|
+
"fixed", or "clean" without fresh verification evidence.
|
|
48
|
+
7. **State PR evidence.** Every PR carries exactly one of:
|
|
49
|
+
- `Docs updated first:` — list the changed docs;
|
|
50
|
+
- `Docs checked, no change needed:` — list the docs and the reason;
|
|
51
|
+
- `Docs not applicable:` — why the change is implementation-only.
|
|
52
|
+
|
|
53
|
+
The line must name its evidence — docs paths or a reason. A bare label
|
|
54
|
+
with nothing after the colon fails `stdd check-pr`, and only a line
|
|
55
|
+
starting at the beginning of a line counts (quoted templates and code
|
|
56
|
+
blocks do not).
|
|
57
|
+
|
|
58
|
+
## The frontend exception: design-first
|
|
59
|
+
|
|
60
|
+
Frontend **visual** work — layout, styling, markup structure, presentation
|
|
61
|
+
copy, component composition — is design-first, not test-first. A
|
|
62
|
+
failing-test-first loop forces the visual outcome to be specified before it
|
|
63
|
+
is explored; brittle rendering assertions then punish every design iteration.
|
|
64
|
+
|
|
65
|
+
The exception covers presentation, not meaning. Copy with business meaning —
|
|
66
|
+
prices, statuses, permissions, legal text, anything a user relies on as a
|
|
67
|
+
fact — is **behavior**: it goes through the docs edit and the normal loop.
|
|
68
|
+
Only its visual arrangement is design-first.
|
|
69
|
+
|
|
70
|
+
- Build the visual part freely; verify it visually (screenshots reviewed by a
|
|
71
|
+
human).
|
|
72
|
+
- Never write tests asserting static copy, class names, or pure rendering
|
|
73
|
+
output.
|
|
74
|
+
- After the visual part settles, add tests only for real behavior contracts:
|
|
75
|
+
hooks, formatters, state transitions, eligibility and conditional logic,
|
|
76
|
+
accessibility roles.
|
|
77
|
+
- Client-side **logic** follows the normal loop.
|
|
78
|
+
|
|
79
|
+
## Working artifacts are never committed
|
|
80
|
+
|
|
81
|
+
Plans, spec files, todo lists, handoff notes, and execution logs are working
|
|
82
|
+
artifacts. They help one session and go stale immediately after. Committed,
|
|
83
|
+
they outrank fresher docs in code search and become a second source of truth.
|
|
84
|
+
|
|
85
|
+
Where their content belongs instead:
|
|
86
|
+
|
|
87
|
+
| Content | Home |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| Durable rules (behavior, architecture, conventions) | The permanent docs tree, same PR |
|
|
90
|
+
| Design rationale, scope decisions, rejected alternatives | The PR description |
|
|
91
|
+
| Designs for deferred (not yet implemented) work | Dated entries in the project log (e.g. `docs/project/`) |
|
|
92
|
+
| Task lists, sequencing | Ephemeral: session scratchpad, PR body |
|
|
93
|
+
|
|
94
|
+
The project log is **not canonical**: its entries are dated records of
|
|
95
|
+
decisions and future intentions, never a description of the present. Cite
|
|
96
|
+
canonical docs for how the system behaves; cite the project log only for why
|
|
97
|
+
something is deferred or was decided.
|
|
98
|
+
|
|
99
|
+
Because a plain `grep` cannot tell authority levels apart, the boundary is
|
|
100
|
+
made machine-readable on both sides. Every project-log entry starts with
|
|
101
|
+
frontmatter declaring itself non-canonical:
|
|
102
|
+
|
|
103
|
+
```yaml
|
|
104
|
+
---
|
|
105
|
+
authority: non-canonical
|
|
106
|
+
status: deferred
|
|
107
|
+
---
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
And the agent instructions `stdd init` generates carry a retrieval rule: do
|
|
111
|
+
not search the project log unless the user explicitly asks for historical
|
|
112
|
+
rationale or deferred work.
|
|
113
|
+
|
|
114
|
+
`stdd check` enforces the artifact ban in CI; `stdd check-pr` enforces the
|
|
115
|
+
PR evidence line; `stdd doctor` reports a repository's overall adoption
|
|
116
|
+
health (setup, canonical docs, misleading artifacts, generated-file drift). The rest of the method is review discipline — anything
|
|
117
|
+
that later proves mechanically checkable should move into `stdd check`.
|
|
118
|
+
|
|
119
|
+
## Bug fixes and refactors
|
|
120
|
+
|
|
121
|
+
- **Bug fix:** reproduce the symptom in a test before editing. Fix the root
|
|
122
|
+
cause, not the symptom.
|
|
123
|
+
- **Refactor:** prove behavior preservation with existing tests, typecheck,
|
|
124
|
+
or focused characterization tests. No docs edit needed when behavior and
|
|
125
|
+
contracts are unchanged.
|
|
126
|
+
|
|
127
|
+
## Style for docs
|
|
128
|
+
|
|
129
|
+
Concise. Short, direct sentences. Do not omit words that carry meaning. One
|
|
130
|
+
rule lives in one document — link, don't duplicate. Canonical docs are
|
|
131
|
+
written in English and describe the **present**: temporal narrative
|
|
132
|
+
("previously", "no longer") belongs in git history and PR descriptions —
|
|
133
|
+
`stdd check` flags it.
|
|
134
|
+
|
|
135
|
+
## What stdd does not cover
|
|
136
|
+
|
|
137
|
+
stdd is a process contract, not an engineering standard. Architecture rules,
|
|
138
|
+
dependency-injection styles, error-handling policy, tenant/auth/data safety,
|
|
139
|
+
and database-migration policy stay in the adopting team's own contract
|
|
140
|
+
(typically `AGENTS.md`) and docs tree. stdd tells you *where* such rules
|
|
141
|
+
live and *when* they must be written — not what they should say.
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stdd/cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Spec + Test Driven Development — a markdown-first methodology kit for teams building software with AI coding agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"stdd": "cli/stdd.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"cli/",
|
|
11
|
+
"method/",
|
|
12
|
+
"playbooks/",
|
|
13
|
+
"templates/",
|
|
14
|
+
"adapters/"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node --test",
|
|
18
|
+
"check": "biome ci .",
|
|
19
|
+
"format": "biome check --fix .",
|
|
20
|
+
"selfcheck": "node cli/stdd.mjs check ."
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"sdd",
|
|
27
|
+
"tdd",
|
|
28
|
+
"stdd",
|
|
29
|
+
"spec-driven-development",
|
|
30
|
+
"test-driven-development",
|
|
31
|
+
"ai-agents",
|
|
32
|
+
"claude-code",
|
|
33
|
+
"codex",
|
|
34
|
+
"methodology",
|
|
35
|
+
"workflow"
|
|
36
|
+
],
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/vsem-azamat/stdd.git"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/vsem-azamat/stdd/issues"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/vsem-azamat/stdd#readme",
|
|
45
|
+
"author": "Azamat Almazbek uulu",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@biomejs/biome": "^2.3.8"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: stdd-brainstorming
|
|
3
|
+
description: Shape a fuzzy idea into an agreed behavior contract before any plan or code
|
|
4
|
+
when: A non-trivial change is requested and the requirements, scope, or approach are not yet pinned down.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Brainstorming
|
|
8
|
+
|
|
9
|
+
The goal is agreement on **what** and **why** before anyone invests in **how**.
|
|
10
|
+
The output is not a document — it is a shared understanding that becomes a
|
|
11
|
+
docs edit and a PR description.
|
|
12
|
+
|
|
13
|
+
## Process
|
|
14
|
+
|
|
15
|
+
1. **Understand the current state first.** Read the relevant docs and the code
|
|
16
|
+
the change will touch. Questions asked from ignorance waste the other
|
|
17
|
+
side's time; questions asked from knowledge sharpen the idea.
|
|
18
|
+
2. **Ask one question at a time.** Prefer questions that eliminate whole
|
|
19
|
+
branches of the design space: who is it for, what triggers it, what must
|
|
20
|
+
never happen, what is explicitly out of scope.
|
|
21
|
+
3. **Challenge scope creep in both directions.** If the idea is bigger than
|
|
22
|
+
the need, say so and propose the smaller version. If the stated need hides
|
|
23
|
+
a larger real problem, surface it.
|
|
24
|
+
4. **Propose 2–3 approaches with a recommendation.** For each: one paragraph,
|
|
25
|
+
the trade-off that actually matters, and what it costs later. Recommend
|
|
26
|
+
one; do not present a menu without an opinion.
|
|
27
|
+
5. **Converge on the behavior contract.** State the agreed behavior as rules
|
|
28
|
+
precise enough to test. Confirm them explicitly.
|
|
29
|
+
|
|
30
|
+
## Output
|
|
31
|
+
|
|
32
|
+
- The agreed rules become the **docs edit** (the spec) — the first commit of
|
|
33
|
+
the branch.
|
|
34
|
+
- The rationale, rejected alternatives, and scope decisions go into the
|
|
35
|
+
**PR description** when the branch opens.
|
|
36
|
+
- Nothing from this conversation is committed as a standalone file.
|
|
37
|
+
|
|
38
|
+
## Anti-patterns
|
|
39
|
+
|
|
40
|
+
- Jumping to implementation detail while behavior is still unsettled.
|
|
41
|
+
- Asking multiple stacked questions at once.
|
|
42
|
+
- Writing a "spec document" instead of editing the real docs.
|
|
43
|
+
- Agreeing silently: if you disagree with the direction, say so with reasons.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: stdd-debugging
|
|
3
|
+
description: Find and fix the root cause of a defect, not its symptom
|
|
4
|
+
when: A bug, crash, failing test, or unexplained behavior is reported.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Debugging
|
|
8
|
+
|
|
9
|
+
The discipline: no edit before a reproduction, no fix before a diagnosis.
|
|
10
|
+
|
|
11
|
+
## Process
|
|
12
|
+
|
|
13
|
+
1. **Reproduce first.** Turn the report into a deterministic reproduction —
|
|
14
|
+
ideally a failing test. If you cannot reproduce it, you are not debugging
|
|
15
|
+
yet; you are gathering facts.
|
|
16
|
+
2. **Read the actual error.** The full message, the stack, the logs around
|
|
17
|
+
it. Do not pattern-match a familiar-looking symptom to a known failure —
|
|
18
|
+
verify the evidence supports *this* cause.
|
|
19
|
+
3. **Form one hypothesis and test it cheaply.** Predict what you will observe
|
|
20
|
+
if the hypothesis is true, then look. One hypothesis at a time; a change
|
|
21
|
+
made under two hypotheses proves neither.
|
|
22
|
+
4. **Fix the root cause minimally.** The smallest change that removes the
|
|
23
|
+
cause. Resist drive-by cleanup — it obscures the fix in review.
|
|
24
|
+
5. **Keep the reproduction as a regression test.** Red before the fix, green
|
|
25
|
+
after, committed with it.
|
|
26
|
+
6. **Verify the fix in the original context**, not only in the reduced
|
|
27
|
+
reproduction.
|
|
28
|
+
|
|
29
|
+
## Stop rules
|
|
30
|
+
|
|
31
|
+
- Two failed fix attempts mean the diagnosis is wrong. Stop editing, go back
|
|
32
|
+
to step 2, and widen what you consider suspect — including your own
|
|
33
|
+
earlier changes and the test itself.
|
|
34
|
+
- If the evidence contradicts the reported story, surface the contradiction
|
|
35
|
+
instead of forcing a fix that matches the story.
|
|
36
|
+
- A fix you cannot explain is not a fix. Do not ship it.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: stdd-planning
|
|
3
|
+
description: Turn an agreed behavior contract into an executable, verifiable sequence of work
|
|
4
|
+
when: The behavior contract is agreed (docs edit drafted or committed) and the change is large enough to need ordered steps.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Planning
|
|
8
|
+
|
|
9
|
+
A plan is a disposable working artifact: it guides one execution and is thrown
|
|
10
|
+
away. It is never committed as a file — its home is the PR description (for
|
|
11
|
+
the durable summary) and the session's scratchpad (for the working copy).
|
|
12
|
+
|
|
13
|
+
## Structure
|
|
14
|
+
|
|
15
|
+
A good plan has, in order:
|
|
16
|
+
|
|
17
|
+
1. **Intent** — one paragraph: the problem and the agreed direction.
|
|
18
|
+
2. **Docs delta** — which permanent docs change and how (added / modified /
|
|
19
|
+
removed rules, named per target file). This is the spec surface of the
|
|
20
|
+
plan; keep it exact so the docs edit is mechanical.
|
|
21
|
+
3. **Steps** — each step small enough to verify independently. Per step:
|
|
22
|
+
- what changes (files, functions);
|
|
23
|
+
- the failing test that gates it (or the visual check, for frontend
|
|
24
|
+
visual work — see the design-first exception in the method);
|
|
25
|
+
- the verification command.
|
|
26
|
+
4. **Out of scope** — what this change deliberately does not do.
|
|
27
|
+
5. **Risks** — what could invalidate the plan and how you would notice.
|
|
28
|
+
|
|
29
|
+
## Rules
|
|
30
|
+
|
|
31
|
+
- Order steps so the system stays green between them.
|
|
32
|
+
- Write verification per step, not one "run all tests" at the end.
|
|
33
|
+
- A step that cannot fail its check is not a step — merge it into another.
|
|
34
|
+
- When execution contradicts the plan, update the plan, do not force the
|
|
35
|
+
plan onto reality. If the *intent* changed, stop and re-enter
|
|
36
|
+
brainstorming.
|
|
37
|
+
- Keep the durable parts flowing to their homes as you go: rules → docs
|
|
38
|
+
edit, rationale → PR description. The plan itself must stay deletable at
|
|
39
|
+
any moment without information loss.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: stdd-worktrees
|
|
3
|
+
description: Work in an isolated workspace without fighting the platform's native isolation
|
|
4
|
+
when: Starting implementation work that should not disturb the user's current checkout.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Isolated Workspaces
|
|
8
|
+
|
|
9
|
+
## Order of preference
|
|
10
|
+
|
|
11
|
+
1. **Detect existing isolation.** If you are already in a linked worktree or
|
|
12
|
+
a platform-managed sandbox, use it. Never nest worktrees. (Check:
|
|
13
|
+
`git rev-parse --git-dir` differs from `--git-common-dir`, and you are not
|
|
14
|
+
in a submodule.)
|
|
15
|
+
2. **Use the platform's native worktree tool** if one exists. Manual
|
|
16
|
+
`git worktree add` alongside a native tool creates state the platform
|
|
17
|
+
cannot see or clean up.
|
|
18
|
+
3. **Fall back to `git worktree add`** only when neither applies:
|
|
19
|
+
- Put worktrees in a dedicated ignored directory (`.worktrees/` at the
|
|
20
|
+
repo root by default).
|
|
21
|
+
- Verify the directory is git-ignored **before** creating the worktree;
|
|
22
|
+
add it to `.gitignore` first if not.
|
|
23
|
+
- Branch from the repository's integration branch unless told otherwise.
|
|
24
|
+
|
|
25
|
+
## After creating
|
|
26
|
+
|
|
27
|
+
- Run the project's dependency setup (install, build) so the workspace is
|
|
28
|
+
self-sufficient.
|
|
29
|
+
- Run the narrowest baseline verification before changing anything. If the
|
|
30
|
+
baseline is already red, report it and ask before proceeding — otherwise
|
|
31
|
+
you cannot tell your breakage from pre-existing breakage.
|
|
32
|
+
|
|
33
|
+
## Shared state warnings
|
|
34
|
+
|
|
35
|
+
- The git stash stack is shared across all worktrees of one repository.
|
|
36
|
+
Prefer a temporary WIP commit over stashing; if you must stash, tag the
|
|
37
|
+
entry and apply (not pop) by its SHA.
|
|
38
|
+
- Never `cd` out of your assigned worktree into the main checkout to "fix
|
|
39
|
+
something quickly" — open work there belongs to someone else.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Deferred Design Template
|
|
2
|
+
|
|
3
|
+
A design for work that is agreed but not yet scheduled. Lives as a **dated
|
|
4
|
+
entry** in the project log (e.g. `docs/project/`), never as a spec file next
|
|
5
|
+
to canonical docs. Delete the entry when the work ships or is abandoned.
|
|
6
|
+
|
|
7
|
+
The frontmatter is mandatory: it is the machine-readable marker that keeps
|
|
8
|
+
agent retrieval authority-aware — canonical docs never carry
|
|
9
|
+
`authority: non-canonical`, project-log entries always do.
|
|
10
|
+
|
|
11
|
+
```markdown
|
|
12
|
+
---
|
|
13
|
+
authority: non-canonical
|
|
14
|
+
status: deferred
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# <Title>
|
|
18
|
+
|
|
19
|
+
Last updated: <YYYY-MM-DD>
|
|
20
|
+
|
|
21
|
+
- Status: Deferred | Decision needed | Ready | Blocked
|
|
22
|
+
- Priority: High | Medium | Low
|
|
23
|
+
|
|
24
|
+
## Problem
|
|
25
|
+
|
|
26
|
+
<What hurts and who it hurts. Impact if never done.>
|
|
27
|
+
|
|
28
|
+
## Agreed direction
|
|
29
|
+
|
|
30
|
+
<The design, as rules precise enough to implement from. Note explicitly that
|
|
31
|
+
this describes FUTURE behavior — canonical docs still describe the present.>
|
|
32
|
+
|
|
33
|
+
## Why deferred
|
|
34
|
+
|
|
35
|
+
<The reason it is not being done now.>
|
|
36
|
+
|
|
37
|
+
## Resume condition
|
|
38
|
+
|
|
39
|
+
<The concrete trigger that should restart this work.>
|
|
40
|
+
|
|
41
|
+
## Acceptance criteria
|
|
42
|
+
|
|
43
|
+
<How we will know it is done.>
|
|
44
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# PR Description Template
|
|
2
|
+
|
|
3
|
+
The PR description is the durable home for everything that is not a rule:
|
|
4
|
+
rationale, scope decisions, rejected alternatives. Copy, fill, delete unused
|
|
5
|
+
sections.
|
|
6
|
+
|
|
7
|
+
```markdown
|
|
8
|
+
## Why
|
|
9
|
+
|
|
10
|
+
<The problem, one paragraph. Link the discussion if there was one.>
|
|
11
|
+
|
|
12
|
+
## What changed
|
|
13
|
+
|
|
14
|
+
<Behavior first, implementation second. Bullets.>
|
|
15
|
+
|
|
16
|
+
## Decisions and alternatives
|
|
17
|
+
|
|
18
|
+
<Choices made and what was rejected, with the reason. This is the section
|
|
19
|
+
future readers will thank you for — it never goes in the docs tree.>
|
|
20
|
+
|
|
21
|
+
## Out of scope
|
|
22
|
+
|
|
23
|
+
<What this PR deliberately does not do.>
|
|
24
|
+
|
|
25
|
+
## Docs evidence
|
|
26
|
+
|
|
27
|
+
<Exactly one of:>
|
|
28
|
+
Docs updated first: <list of changed docs>
|
|
29
|
+
Docs checked, no change needed: <docs + reason>
|
|
30
|
+
Docs not applicable: <why implementation-only>
|
|
31
|
+
|
|
32
|
+
## Verification
|
|
33
|
+
|
|
34
|
+
<Commands run and their results. Screenshots for visual work.>
|
|
35
|
+
```
|