eng-skills 0.1.0

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/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # eng-skills
2
+
3
+ **Battle-tested Claude Code engineering conventions, scaffolded into any repo in one command.**
4
+
5
+ ```bash
6
+ npx eng-skills init
7
+ ```
8
+
9
+ Most Claude Code advice lives in blog posts. This package turns it into files: an
10
+ *enforced* documentation protocol, safety hooks, and research-first workflows —
11
+ extracted from a production monorepo where this exact system sustained
12
+ **100+ feature docs, 100+ fix docs, and a full ADR log** without the docs going stale.
13
+
14
+ ## Why
15
+
16
+ Two facts about working with coding agents:
17
+
18
+ 1. **CLAUDE.md instructions are advisory; hooks are deterministic.** An instruction
19
+ like "always document your changes" gets skipped under context pressure. A Stop
20
+ hook that refuses to end the turn does not.
21
+ 2. **Sessions are ephemeral.** Whatever the agent learned — why a decision was made,
22
+ what a fix's root cause was — evaporates unless it's written where the next
23
+ session will look.
24
+
25
+ eng-skills wires those two facts together: hooks that *force* the knowledge base to
26
+ stay current, and a knowledge base structured so future sessions (and humans)
27
+ actually benefit from it.
28
+
29
+ ## What you get
30
+
31
+ ```
32
+ CLAUDE.md # protocol + TODO(onboard) markers Claude fills in itself
33
+ docs/
34
+ README.md # the knowledge-base map and "the rule"
35
+ CHANGELOG-AUTO.md # append-only machine-written edit log
36
+ _templates/ # feature / fix / decision / phase templates
37
+ features/ fixes/ # one doc per feature, one per bug fix
38
+ decisions/ # numbered ADRs (seeded with ADR-0001)
39
+ phases/ plans/ specs/ # living progress checklists + planning artifacts
40
+ .claude/
41
+ settings.json # hook wiring (merged safely into an existing file)
42
+ hooks/
43
+ pre-edit.mjs # PreToolUse: reminds about the protocol on first code edit
44
+ post-edit.mjs # PostToolUse: logs every edit to CHANGELOG-AUTO.md
45
+ stop-gate.mjs # Stop: BLOCKS ending a turn that changed code but not docs
46
+ guard.mjs # PreToolUse(Bash): denies rm -rf, force push, .env exfil, …
47
+ commands/
48
+ onboard.md # /onboard — Claude researches YOUR codebase, fills CLAUDE.md
49
+ feature.md # /feature — research → plan → implement → verify → document
50
+ fix.md # /fix — reproduce → root-cause → fix → prove → document
51
+ decide.md # /decide — record an ADR
52
+ phase.md # /phase — honest progress review, phase-gate closeout
53
+ ```
54
+
55
+ Everything is dependency-free Node (~150 lines of hooks total). No runtime, no daemon,
56
+ nothing to keep updated.
57
+
58
+ ## How the enforcement works
59
+
60
+ - **pre-edit** (PreToolUse) — on the first code edit of a turn, injects a reminder
61
+ of the docs protocol into Claude's context. Never blocks.
62
+ - **post-edit** (PostToolUse) — appends `timestamp | session | tool | file | ±lines`
63
+ to `docs/CHANGELOG-AUTO.md` for every tracked edit, and records whether the turn
64
+ touched code, docs, or both.
65
+ - **stop-gate** (Stop) — when Claude tries to end the turn: if code changed but
66
+ `docs/` didn't, it returns `{"decision": "block"}` and Claude must write the doc
67
+ before finishing. Includes the `stop_hook_active` loop guard.
68
+ - **guard** (PreToolUse on Bash) — deny-list for destructive commands (`rm -rf` on
69
+ broad paths, `git push --force`, `git reset --hard`, `DROP TABLE`, `.env`
70
+ exfiltration, credential directories). Hooks fire even under
71
+ `--dangerously-skip-permissions`, so this holds in unattended runs.
72
+
73
+ The result: **you cannot end a turn that changed source code without documenting it**,
74
+ and you get a free forensic edit log on the side.
75
+
76
+ ## Using it on an existing codebase
77
+
78
+ `init` is non-destructive and idempotent:
79
+
80
+ - Files that already exist are **skipped** (use `--force` to overwrite).
81
+ - An existing `.claude/settings.json` gets our hooks **merged in** — your
82
+ permissions and existing hooks are preserved.
83
+ - An existing `CLAUDE.md` gets the docs-protocol section **appended**, nothing else touched.
84
+
85
+ Then open Claude Code and run **`/onboard`**: Claude reads your manifests, CI config,
86
+ and source layout, fills in every `TODO(onboard)` marker in `CLAUDE.md` (stack,
87
+ verified commands, hard invariants), and retroactively records your existing
88
+ architecture as ADRs — so the knowledge base starts warm, not empty.
89
+
90
+ ## The daily workflow
91
+
92
+ | Command | What it does |
93
+ |---|---|
94
+ | `/feature <desc>` | Researches the codebase (and web when useful) *before* writing code, writes a checkbox plan to `docs/plans/`, implements with TDD, verifies end-to-end, documents in `docs/features/`. |
95
+ | `/fix <bug>` | Reproduce first, root-cause (not symptom-patch), failing test → fix, document Symptom/Root cause/Fix/Verification/Prevention in `docs/fixes/`. |
96
+ | `/decide <choice>` | Numbered ADR with context, options, decision, consequences. Supersede — never delete — old decisions. |
97
+ | `/phase status` | Cross-checks the current phase checklist against reality (do claimed tests actually pass?). |
98
+ | `/phase next <name>` | Phase-gate closeout: verify acceptance criteria, get operator sign-off, open the next phase. |
99
+
100
+ ## CLI reference
101
+
102
+ ```bash
103
+ npx eng-skills init [dir] # scaffold (default: current directory)
104
+ --force # overwrite existing files
105
+ --dry-run # print what would happen, write nothing
106
+ ```
107
+
108
+ ## Design principles (and their sources)
109
+
110
+ - **"Give Claude a check it can run."** Without a runnable check, "looks done" is the
111
+ agent's only stop signal. The stop-gate is that check for documentation; the
112
+ templates' Verification sections demand evidence (real command output), not
113
+ assertions. — [Claude Code best practices](https://code.claude.com/docs/en/best-practices)
114
+ - **Hooks for zero-exception rules, CLAUDE.md for judgment calls.** Anything that
115
+ must happen every time is a hook; CLAUDE.md carries the judgment-dependent
116
+ conventions. — [Hooks reference](https://code.claude.com/docs/en/hooks)
117
+ - **Research before code.** `/feature` front-loads codebase + web research so the
118
+ plan is grounded in what exists, not what the model remembers.
119
+ - **Keep CLAUDE.md small.** It loads into every session; every line costs context.
120
+ `/onboard` targets ~120 lines. Long-form knowledge goes in `docs/` where it's
121
+ read on demand.
122
+ - **ADRs are agent memory.** Superseded decisions are marked, never deleted — the
123
+ decision log is how session N+1 avoids re-litigating what session N settled.
124
+
125
+ ## License
126
+
127
+ MIT
package/bin/cli.mjs ADDED
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env node
2
+ // eng-skills — scaffold battle-tested Claude Code engineering conventions.
3
+ // Usage: npx eng-skills init [dir] [--force] [--dry-run]
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const TEMPLATES = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'templates');
9
+
10
+ const argv = process.argv.slice(2);
11
+ const flags = new Set(argv.filter((a) => a.startsWith('--')));
12
+ const positional = argv.filter((a) => !a.startsWith('--'));
13
+ const command = positional[0] || 'init';
14
+ const FORCE = flags.has('--force');
15
+ const DRY = flags.has('--dry-run');
16
+
17
+ if (flags.has('--help') || command === 'help') {
18
+ console.log(`
19
+ eng-skills — scaffold Claude Code engineering conventions into a repo
20
+
21
+ Usage:
22
+ npx eng-skills init [dir] scaffold into dir (default: current directory)
23
+
24
+ Options:
25
+ --force overwrite files that already exist (default: skip them)
26
+ --dry-run show what would be written without writing anything
27
+ --help this message
28
+
29
+ What it sets up:
30
+ CLAUDE.md protocol + TODO(onboard) markers Claude fills in
31
+ docs/ features/ fixes/ decisions/ phases/ plans/ specs/ + templates
32
+ .claude/hooks/ pre-edit reminder, post-edit changelog, stop-gate, bash guard
33
+ .claude/commands/ /onboard /feature /fix /decide /phase
34
+ .claude/settings.json hook wiring (merged into an existing file safely)
35
+
36
+ After installing, open Claude Code in the repo and run /onboard.
37
+ `);
38
+ process.exit(0);
39
+ }
40
+
41
+ if (command !== 'init') {
42
+ console.error(`Unknown command "${command}". Try: npx eng-skills init`);
43
+ process.exit(1);
44
+ }
45
+
46
+ const target = path.resolve(positional[1] || '.');
47
+ const today = new Date().toISOString().slice(0, 10);
48
+
49
+ function projectName() {
50
+ try {
51
+ const pkg = JSON.parse(fs.readFileSync(path.join(target, 'package.json'), 'utf8'));
52
+ if (pkg.name) return pkg.name;
53
+ } catch {}
54
+ return path.basename(target);
55
+ }
56
+
57
+ const VARS = { PROJECT_NAME: projectName(), DATE: today };
58
+ const fill = (s) => s.replace(/\{\{(\w+)\}\}/g, (m, k) => VARS[k] ?? m);
59
+
60
+ const written = [];
61
+ const skipped = [];
62
+ const merged = [];
63
+
64
+ function writeFile(rel, content) {
65
+ const dest = path.join(target, rel);
66
+ if (fs.existsSync(dest) && !FORCE) {
67
+ skipped.push(rel);
68
+ return;
69
+ }
70
+ written.push(rel);
71
+ if (DRY) return;
72
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
73
+ fs.writeFileSync(dest, content);
74
+ }
75
+
76
+ function copyTemplate(srcRel, destRel = srcRel) {
77
+ writeFile(destRel, fill(fs.readFileSync(path.join(TEMPLATES, srcRel), 'utf8')));
78
+ }
79
+
80
+ /** Merge our hook wiring into an existing .claude/settings.json without clobbering. */
81
+ function mergeSettings() {
82
+ const rel = path.join('.claude', 'settings.json');
83
+ const dest = path.join(target, rel);
84
+ const ours = JSON.parse(fs.readFileSync(path.join(TEMPLATES, 'claude', 'settings.json'), 'utf8'));
85
+
86
+ if (!fs.existsSync(dest)) {
87
+ writeFile(rel, JSON.stringify(ours, null, 2) + '\n');
88
+ return;
89
+ }
90
+
91
+ let existing;
92
+ try {
93
+ existing = JSON.parse(fs.readFileSync(dest, 'utf8'));
94
+ } catch {
95
+ console.error(` ! ${rel} exists but is not valid JSON — leaving it alone.`);
96
+ console.error(` Add the hook wiring manually (see templates in the eng-skills package).`);
97
+ skipped.push(rel);
98
+ return;
99
+ }
100
+
101
+ existing.hooks ||= {};
102
+ let changed = false;
103
+ for (const [event, entries] of Object.entries(ours.hooks)) {
104
+ existing.hooks[event] ||= [];
105
+ for (const entry of entries) {
106
+ const sig = JSON.stringify(entry.hooks.map((h) => h.command));
107
+ const already = existing.hooks[event].some(
108
+ (e) => JSON.stringify((e.hooks || []).map((h) => h.command)) === sig,
109
+ );
110
+ if (!already) {
111
+ existing.hooks[event].push(entry);
112
+ changed = true;
113
+ }
114
+ }
115
+ }
116
+ if (changed) {
117
+ merged.push(rel);
118
+ if (!DRY) fs.writeFileSync(dest, JSON.stringify(existing, null, 2) + '\n');
119
+ } else {
120
+ skipped.push(rel + ' (hooks already wired)');
121
+ }
122
+ }
123
+
124
+ /** Append the enforced docs-protocol section to an existing CLAUDE.md. */
125
+ function installClaudeMd() {
126
+ const dest = path.join(target, 'CLAUDE.md');
127
+ const template = fill(fs.readFileSync(path.join(TEMPLATES, 'CLAUDE.md'), 'utf8'));
128
+
129
+ if (!fs.existsSync(dest)) {
130
+ writeFile('CLAUDE.md', template);
131
+ return;
132
+ }
133
+ const existing = fs.readFileSync(dest, 'utf8');
134
+ if (existing.includes('Documentation protocol (ENFORCED BY HOOKS)')) {
135
+ skipped.push('CLAUDE.md (protocol section already present)');
136
+ return;
137
+ }
138
+ const marker = '## 📋 Documentation protocol (ENFORCED BY HOOKS)';
139
+ const start = template.indexOf(marker);
140
+ const end = template.indexOf('## Phase roadmap');
141
+ const section = template.slice(start, end === -1 ? undefined : end).trimEnd();
142
+ merged.push('CLAUDE.md (appended docs-protocol section)');
143
+ if (!DRY) fs.writeFileSync(dest, existing.trimEnd() + '\n\n' + section + '\n');
144
+ }
145
+
146
+ function ensureGitignore() {
147
+ const dest = path.join(target, '.gitignore');
148
+ const line = '.claude/.docs-state/';
149
+ const existing = fs.existsSync(dest) ? fs.readFileSync(dest, 'utf8') : '';
150
+ if (existing.includes(line)) return;
151
+ merged.push('.gitignore (+ .claude/.docs-state/)');
152
+ if (!DRY) fs.writeFileSync(dest, existing + (existing && !existing.endsWith('\n') ? '\n' : '') + line + '\n');
153
+ }
154
+
155
+ console.log(`\neng-skills init → ${target}${DRY ? ' (dry run)' : ''}\n`);
156
+
157
+ // Hooks + commands
158
+ for (const f of ['lib.mjs', 'pre-edit.mjs', 'post-edit.mjs', 'stop-gate.mjs', 'guard.mjs']) {
159
+ copyTemplate(path.join('claude', 'hooks', f), path.join('.claude', 'hooks', f));
160
+ }
161
+ for (const f of ['onboard.md', 'feature.md', 'fix.md', 'decide.md', 'phase.md']) {
162
+ copyTemplate(path.join('claude', 'commands', f), path.join('.claude', 'commands', f));
163
+ }
164
+ mergeSettings();
165
+
166
+ // Knowledge base
167
+ copyTemplate(path.join('docs', 'README.md'));
168
+ copyTemplate(path.join('docs', 'CHANGELOG-AUTO.md'));
169
+ for (const f of ['feature.md', 'fix.md', 'decision.md', 'phase.md']) {
170
+ copyTemplate(path.join('docs', '_templates', f));
171
+ }
172
+ copyTemplate(path.join('docs', 'decisions', '0001-record-architecture-decisions.md'));
173
+ copyTemplate(path.join('docs', 'phases', 'phase-0-foundation.md'));
174
+ for (const d of ['features', 'fixes', 'plans', 'specs']) {
175
+ writeFile(path.join('docs', d, '.gitkeep'), '');
176
+ }
177
+
178
+ installClaudeMd();
179
+ ensureGitignore();
180
+
181
+ for (const f of written) console.log(` + ${f}`);
182
+ for (const f of merged) console.log(` ~ ${f}`);
183
+ for (const f of skipped) console.log(` = ${f} (exists, skipped)`);
184
+
185
+ console.log(`
186
+ Done. Next steps:
187
+ 1. cd ${path.relative(process.cwd(), target) || '.'} && claude
188
+ 2. Run /onboard — Claude researches this codebase and fills in CLAUDE.md.
189
+ 3. Build things with /feature, fix things with /fix, record choices with /decide.
190
+
191
+ The stop-gate hook now blocks any turn that changes code without updating docs/.
192
+ Verify hooks are active with /hooks inside Claude Code.
193
+ `);
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "eng-skills",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold battle-tested Claude Code engineering conventions into any repo: enforced docs protocol (features/fixes/decisions/phases), pre/post/stop hooks, research-first slash commands, and a CLAUDE.md that Claude fills in by reading your codebase.",
5
+ "keywords": [
6
+ "claude",
7
+ "claude-code",
8
+ "ai",
9
+ "agent",
10
+ "scaffold",
11
+ "adr",
12
+ "documentation",
13
+ "hooks",
14
+ "best-practices"
15
+ ],
16
+ "license": "MIT",
17
+ "author": "Vidit Parashar <viditsharma818@gmail.com>",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/viditparashar96/eng-skills.git"
21
+ },
22
+ "bugs": "https://github.com/viditparashar96/eng-skills/issues",
23
+ "homepage": "https://github.com/viditparashar96/eng-skills#readme",
24
+ "type": "module",
25
+ "bin": {
26
+ "eng-skills": "bin/cli.mjs"
27
+ },
28
+ "files": [
29
+ "bin",
30
+ "templates",
31
+ "README.md"
32
+ ],
33
+ "engines": {
34
+ "node": ">=18"
35
+ },
36
+ "scripts": {
37
+ "test": "node test/smoke.mjs"
38
+ }
39
+ }
@@ -0,0 +1,60 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ <!-- TODO(onboard): One-paragraph mission. State the defining constraint of this
4
+ project and point at the authoritative requirement sources. Run /onboard to have
5
+ Claude research the codebase and fill every TODO(onboard) in this file. -->
6
+
7
+ > ⚠️ <!-- TODO(onboard): the single biggest risk/constraint contributors must never violate. -->
8
+
9
+ ## Stack
10
+
11
+ <!-- TODO(onboard): One bullet per package/app/module, plus an infra line
12
+ (DB, queues, docker). Keep it to what an agent needs to navigate, not a sales pitch. -->
13
+
14
+ ## Commands
15
+
16
+ ```bash
17
+ # TODO(onboard): the canonical dev commands — install, run, test, lint, migrate.
18
+ ```
19
+
20
+ ## Hard invariants (NEVER violate)
21
+
22
+ <!-- TODO(onboard): a numbered list of absolute rules, e.g. security constraints,
23
+ "never edit generated files", "all writes go through X". Keep them few and bold. -->
24
+
25
+ ## 📋 Documentation protocol (ENFORCED BY HOOKS)
26
+
27
+ This repo runs three hooks (`.claude/hooks/`, configured in `.claude/settings.json`):
28
+
29
+ - **pre-edit** (PreToolUse): reminds about this protocol on the first code edit of a turn.
30
+ - **post-edit** (PostToolUse): appends every tracked edit to `docs/CHANGELOG-AUTO.md` (never hand-edit that file).
31
+ - **stop-gate** (Stop): **blocks the turn from ending** if you changed source files without updating `docs/`.
32
+
33
+ **Therefore, every turn that touches code MUST also update docs.**
34
+
35
+ | You did… | Write/Update |
36
+ |----------|--------------|
37
+ | Implemented a feature/module | `docs/features/<phase>-<name>.md` (template: `docs/_templates/feature.md`) |
38
+ | Fixed a bug | `docs/fixes/YYYY-MM-DD-<slug>.md` (template: `fix.md`) |
39
+ | Made an architectural/trade-off decision | `docs/decisions/NNNN-<title>.md` (template: `decision.md`) |
40
+ | Any code-touching turn | tick/append progress in the **current** `docs/phases/phase-N-*.md` |
41
+
42
+ Notes:
43
+ - `docs/CHANGELOG-AUTO.md` is machine-written — never edit it by hand.
44
+ - If the Stop hook blocks you, it's working as designed: write the missing doc, then finish.
45
+ - If hooks don't fire at all, the workspace may be untrusted — verify with `/hooks`.
46
+
47
+ ## Phase roadmap
48
+
49
+ <!-- TODO(onboard): bullet list of phases, one marked `← CURRENT`, each pointing
50
+ at its docs/phases/phase-N-*.md. Pause for operator review at each phase boundary. -->
51
+
52
+ - Phase 0 — foundation ← CURRENT (`docs/phases/phase-0-foundation.md`)
53
+
54
+ ## Conventions
55
+
56
+ - Research before building: for any non-trivial feature, use `/feature` — it researches the codebase (and the web when useful) and writes a short plan before writing code.
57
+ - TDD for logic; build/boot verification for scaffolding.
58
+ - Frequent, small conventional commits (`feat:` / `fix:` / `docs:` / `chore:`), one logical change each.
59
+ - Small focused files; generated contracts are regenerated, never hand-copied.
60
+ - Record trade-off decisions as ADRs in `docs/decisions/` (use `/decide`).
@@ -0,0 +1,12 @@
1
+ ---
2
+ description: Record an Architecture Decision Record (ADR) in docs/decisions/
3
+ argument-hint: <decision to record>
4
+ ---
5
+
6
+ Record an ADR for: **$ARGUMENTS**
7
+
8
+ 1. Find the next number: list `docs/decisions/` and take the highest `NNNN` + 1 (zero-padded to 4 digits).
9
+ 2. If the decision hasn't been made yet: lay out the real options with honest pros/cons, recommend one, and ask the user to choose before writing `Status: accepted`.
10
+ 3. Write `docs/decisions/NNNN-<slug>.md` from `docs/_templates/decision.md` — Context, Options considered, Decision, Consequences.
11
+ 4. If this supersedes an earlier ADR, update that ADR's status line to `superseded by [[NNNN]]`.
12
+ 5. Link the ADR from the current `docs/phases/` doc's notes section.
@@ -0,0 +1,30 @@
1
+ ---
2
+ description: Research-first feature workflow — research, plan, implement, verify, document
3
+ argument-hint: <feature description>
4
+ ---
5
+
6
+ Build the following feature using the research-first workflow: **$ARGUMENTS**
7
+
8
+ ## 1. Research (before any code)
9
+
10
+ - Search the codebase for everything the feature touches: existing modules to reuse, conventions to follow, contracts (API schemas, types, DB models) to extend.
11
+ - Read the relevant docs: `docs/features/` for related features, `docs/decisions/` for constraints already decided, the current `docs/phases/` doc.
12
+ - If the feature involves an external library/API or a non-obvious technique, do a web search for current best practice — do not rely on memory for API details.
13
+ - If requirements are ambiguous in a way that changes the design, ask the user now, not after implementing.
14
+
15
+ ## 2. Plan
16
+
17
+ Write a short plan to `docs/plans/YYYY-MM-DD-<slug>.md`: goal, approach, files to create/change as checkboxes (`- [ ]`), test strategy. For a small feature this can be 15 lines; don't pad it. If the plan involves a real trade-off decision, record it as an ADR (`/decide`).
18
+
19
+ ## 3. Implement
20
+
21
+ Work through the plan's checkboxes, ticking them as you go. TDD for logic. Follow the codebase's existing style, not your defaults.
22
+
23
+ ## 4. Verify
24
+
25
+ Run the project's tests and build/typecheck. For user-facing behavior, exercise the feature end-to-end (run the app, hit the endpoint, drive the UI) — not just tests. Paste real output as evidence.
26
+
27
+ ## 5. Document (required — the Stop hook enforces this)
28
+
29
+ - Write `docs/features/<phase>-<slug>.md` from `docs/_templates/feature.md`.
30
+ - Tick progress in the current `docs/phases/phase-N-*.md` with a dated entry.
@@ -0,0 +1,22 @@
1
+ ---
2
+ description: Root-cause a bug, fix it, prove the fix, and document it
3
+ argument-hint: <bug description or error output>
4
+ ---
5
+
6
+ Fix the following bug: **$ARGUMENTS**
7
+
8
+ ## 1. Reproduce & root-cause
9
+
10
+ Reproduce the bug first (failing test, curl, script). Then find the *root cause* — trace the actual failure, don't patch the symptom. Check `docs/fixes/` for related past fixes before starting; the same class of bug may have history.
11
+
12
+ ## 2. Fix
13
+
14
+ Write a failing test that captures the bug, then make it pass with the smallest correct change. If the root cause reveals a design flaw, note it — fix the bug now, record the design issue as an ADR or follow-up rather than scope-creeping.
15
+
16
+ ## 3. Verify
17
+
18
+ Run the reproduction again and the full relevant test suite. Paste real output.
19
+
20
+ ## 4. Document (required — the Stop hook enforces this)
21
+
22
+ Write `docs/fixes/YYYY-MM-DD-<slug>.md` from `docs/_templates/fix.md` — Symptom, Root cause, Fix, Verification, Prevention. Tick the current `docs/phases/` doc.
@@ -0,0 +1,35 @@
1
+ ---
2
+ description: Research this codebase and fill in CLAUDE.md + seed the docs knowledge base
3
+ ---
4
+
5
+ You are onboarding this repository into the eng-skills workflow. The scaffold left
6
+ `TODO(onboard)` markers in `CLAUDE.md`. Your job is to replace every one of them
7
+ with researched, accurate content — not guesses.
8
+
9
+ ## Steps
10
+
11
+ 1. **Research the codebase thoroughly.** Read the manifest files (package.json,
12
+ pyproject.toml, go.mod, Cargo.toml, …), lockfiles' package manager, CI config,
13
+ docker/compose files, existing README/docs, and the top-level source layout.
14
+ For monorepos, map every package/app. Identify: tech stack, how to install/run/
15
+ test/lint, architecture boundaries, generated-vs-source files, and anything
16
+ dangerous (migrations, deploy scripts, secrets handling).
17
+
18
+ 2. **Fill in `CLAUDE.md`.** Replace each `TODO(onboard)` marker:
19
+ - Mission paragraph: what the project is and its defining constraint.
20
+ - Warning callout: the single biggest thing an agent must never break.
21
+ - Stack: one bullet per package/module + infra line.
22
+ - Commands: the *canonical* dev commands, verified against the manifests (don't invent scripts that don't exist).
23
+ - Hard invariants: 3–7 absolute rules derived from what you found (e.g. "never edit `src/generated/**`").
24
+ - Phase roadmap: if the project has a clear direction (roadmap file, milestones, TODOs), sketch phases; otherwise leave Phase 0 as CURRENT.
25
+ Keep CLAUDE.md under ~120 lines — it is loaded into every session; every line costs context.
26
+
27
+ 3. **Seed the knowledge base.** If this is an existing codebase (not a fresh repo):
28
+ - Write `docs/decisions/0002-<slug>.md`-style ADRs for 2–4 major *existing* architectural decisions you can infer (framework choice, data layer, auth approach) — mark them `accepted`, dated today, noting they were recorded retroactively.
29
+ - Update `docs/phases/phase-0-foundation.md`: tick the onboard task, add a dated log entry.
30
+
31
+ 4. **Verify.** Run the test/build command you documented in Commands to confirm it actually works. If it fails, document the real state honestly rather than an aspirational one.
32
+
33
+ 5. **Report.** Summarize what you filled in and anything you were unsure about so the human can correct it.
34
+
35
+ Do NOT modify source code during onboarding — this is a documentation-only pass.
@@ -0,0 +1,9 @@
1
+ ---
2
+ description: Review current phase progress, or close it out and open the next one
3
+ argument-hint: [status | next <name>]
4
+ ---
5
+
6
+ Phase management: **$ARGUMENTS**
7
+
8
+ - **`status`** (or no argument): read the current `docs/phases/phase-N-*.md`, cross-check its checkboxes against reality (do the features/tests it claims actually exist and pass?), and report honest progress: done, in flight, blocked.
9
+ - **`next <name>`**: close out the current phase — verify all acceptance criteria are ticked and true (run the tests), mark it `complete`, then create `docs/phases/phase-N+1-<name>.md` from `docs/_templates/phase.md` and update the Phase roadmap in `CLAUDE.md` to move the `← CURRENT` marker. Phase boundaries are review gates: summarize what shipped and ask the operator to confirm before starting work in the new phase.
@@ -0,0 +1,32 @@
1
+ // PreToolUse (Bash): deny obviously destructive commands and secret access.
2
+ // Hooks fire even under --dangerously-skip-permissions, so this holds in
3
+ // unattended runs. Deny-list only — everything else passes through untouched.
4
+ import { readEvent } from './lib.mjs';
5
+
6
+ const ev = readEvent();
7
+ const cmd = String(ev.tool_input?.command || '');
8
+
9
+ const RULES = [
10
+ [/\brm\s+(-[a-z]*[rf][a-z]*\s+)+.*(\/|\*|~|\$HOME)/i, 'recursive/forced rm on a broad path'],
11
+ [/\bgit\s+push\s+.*--force(?!-with-lease)/, 'force push (use --force-with-lease if truly needed)'],
12
+ [/\bgit\s+reset\s+--hard\b/, 'hard reset discards uncommitted work — stash first or ask the user'],
13
+ [/\bDROP\s+(TABLE|DATABASE|SCHEMA)\b/i, 'destructive SQL'],
14
+ [/(^|[\s/])\.env(\.[\w.]+)?\b.*(cat|cp|curl|nc |mail|post)|((cat|cp|curl)\s+[^|;]*\.env\b)/i, 'reading/exfiltrating .env secrets via shell'],
15
+ [/~\/\.(ssh|aws|gnupg)\b/, 'accessing credential directories'],
16
+ ];
17
+
18
+ for (const [re, why] of RULES) {
19
+ if (re.test(cmd)) {
20
+ process.stdout.write(
21
+ JSON.stringify({
22
+ hookSpecificOutput: {
23
+ hookEventName: 'PreToolUse',
24
+ permissionDecision: 'deny',
25
+ permissionDecisionReason: `Blocked by eng-skills guard: ${why}. If this is genuinely needed, ask the user to run it themselves or adjust .claude/hooks/guard.mjs.`,
26
+ },
27
+ }),
28
+ );
29
+ process.exit(0);
30
+ }
31
+ }
32
+ process.exit(0);
@@ -0,0 +1,91 @@
1
+ // Shared helpers for eng-skills hooks. Zero dependencies — plain Node.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { execFileSync } from 'node:child_process';
5
+
6
+ export const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
7
+ export const STATE_DIR = path.join(PROJECT_DIR, '.claude', '.docs-state');
8
+
9
+ /** Parse the hook event JSON that Claude Code pipes to stdin. */
10
+ export function readEvent() {
11
+ try {
12
+ return JSON.parse(fs.readFileSync(0, 'utf8'));
13
+ } catch {
14
+ return {};
15
+ }
16
+ }
17
+
18
+ /** Repo-relative path for display. */
19
+ export function relPath(fp) {
20
+ if (!fp) return '';
21
+ const abs = path.resolve(fp);
22
+ const root = path.resolve(PROJECT_DIR) + path.sep;
23
+ return abs.startsWith(root) ? abs.slice(root.length) : fp;
24
+ }
25
+
26
+ /**
27
+ * Classify an edited file:
28
+ * - 'ignored' — infra noise that should count as neither code nor docs
29
+ * - 'docs' — the tracked knowledge base (docs/**, CLAUDE.md)
30
+ * - 'code' — everything else
31
+ */
32
+ export function classify(fp) {
33
+ const rel = relPath(fp).replaceAll('\\', '/');
34
+ if (!rel) return 'ignored';
35
+ const base = path.basename(rel);
36
+
37
+ if (
38
+ rel.startsWith('.git/') ||
39
+ rel.startsWith('.claude/') ||
40
+ rel.includes('node_modules/') ||
41
+ rel === 'docs/CHANGELOG-AUTO.md' ||
42
+ base.startsWith('.')
43
+ ) {
44
+ return 'ignored';
45
+ }
46
+ // Agent docs/config counts as documentation.
47
+ if (rel.startsWith('docs/') || base === 'CLAUDE.md') return 'docs';
48
+ return 'code';
49
+ }
50
+
51
+ const DEFAULT_STATE = { codeChanged: false, docsChanged: false, reminded: false };
52
+
53
+ function stateFile(sessionId) {
54
+ const sid = String(sessionId || 'unknown').slice(0, 16);
55
+ return path.join(STATE_DIR, `${sid}.json`);
56
+ }
57
+
58
+ export function readState(sessionId) {
59
+ try {
60
+ return { ...DEFAULT_STATE, ...JSON.parse(fs.readFileSync(stateFile(sessionId), 'utf8')) };
61
+ } catch {
62
+ return { ...DEFAULT_STATE };
63
+ }
64
+ }
65
+
66
+ export function writeState(sessionId, state) {
67
+ try {
68
+ fs.mkdirSync(STATE_DIR, { recursive: true });
69
+ fs.writeFileSync(stateFile(sessionId), JSON.stringify(state));
70
+ } catch {
71
+ // State is best-effort; never break the agent over it.
72
+ }
73
+ }
74
+
75
+ /** Best-effort "+added/-deleted" git diff stat for one file. */
76
+ export function diffStat(fp) {
77
+ const rel = relPath(fp);
78
+ try {
79
+ const run = (args) =>
80
+ execFileSync('git', args, { cwd: PROJECT_DIR, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
81
+ const out = run(['diff', '--numstat', '--', rel]) || run(['diff', '--cached', '--numstat', '--', rel]);
82
+ const m = out.trim().split('\n')[0]?.match(/^(\d+|-)\s+(\d+|-)/);
83
+ if (m) return `+${m[1]}/-${m[2]}`;
84
+ // Untracked file — no diff yet.
85
+ const status = run(['status', '--porcelain', '--', rel]);
86
+ if (status.startsWith('??')) return 'new/?';
87
+ return '?/?';
88
+ } catch {
89
+ return '?/?';
90
+ }
91
+ }
@@ -0,0 +1,28 @@
1
+ // PostToolUse (Edit|Write|MultiEdit): append every tracked edit to
2
+ // docs/CHANGELOG-AUTO.md and record code/docs activity for the Stop gate.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { readEvent, readState, writeState, classify, relPath, diffStat, PROJECT_DIR } from './lib.mjs';
6
+
7
+ const ev = readEvent();
8
+ const fp = ev.tool_input?.file_path;
9
+ const kind = classify(fp);
10
+
11
+ if (kind !== 'ignored') {
12
+ const logFile = path.join(PROJECT_DIR, 'docs', 'CHANGELOG-AUTO.md');
13
+ try {
14
+ const row = `| ${new Date().toISOString()} | ${String(ev.session_id || '').slice(0, 8)} | ${
15
+ ev.tool_name || '?'
16
+ } | \`${relPath(fp)}\` | ${diffStat(fp)} |\n`;
17
+ fs.mkdirSync(path.dirname(logFile), { recursive: true });
18
+ fs.appendFileSync(logFile, row);
19
+ } catch {
20
+ // Logging is best-effort.
21
+ }
22
+
23
+ const st = readState(ev.session_id);
24
+ if (kind === 'code') st.codeChanged = true;
25
+ if (kind === 'docs') st.docsChanged = true;
26
+ writeState(ev.session_id, st);
27
+ }
28
+ process.exit(0);
@@ -0,0 +1,23 @@
1
+ // PreToolUse (Edit|Write|MultiEdit): on the first code edit of a turn,
2
+ // remind Claude about the docs protocol. Never blocks.
3
+ import { readEvent, readState, writeState, classify } from './lib.mjs';
4
+
5
+ const ev = readEvent();
6
+ const fp = ev.tool_input?.file_path;
7
+ if (classify(fp) === 'code') {
8
+ const st = readState(ev.session_id);
9
+ if (!st.reminded) {
10
+ st.reminded = true;
11
+ writeState(ev.session_id, st);
12
+ process.stdout.write(
13
+ JSON.stringify({
14
+ hookSpecificOutput: {
15
+ hookEventName: 'PreToolUse',
16
+ additionalContext:
17
+ 'Docs protocol: you are editing source code this turn. Before ending the turn, create/update the matching entry under docs/ (features/, fixes/, or decisions/ — templates in docs/_templates/) and tick progress in the current docs/phases/ doc. The Stop hook will block the turn otherwise.',
18
+ },
19
+ }),
20
+ );
21
+ }
22
+ }
23
+ process.exit(0);
@@ -0,0 +1,25 @@
1
+ // Stop: block the turn from ending if source files changed but docs/ did not.
2
+ // This is the enforcement half of the docs protocol in CLAUDE.md.
3
+ import { readEvent, readState, writeState } from './lib.mjs';
4
+
5
+ const ev = readEvent();
6
+
7
+ // Loop guard: if we already blocked once this turn, let it end.
8
+ if (ev.stop_hook_active) process.exit(0);
9
+
10
+ const st = readState(ev.session_id);
11
+
12
+ if (st.codeChanged && !st.docsChanged) {
13
+ process.stdout.write(
14
+ JSON.stringify({
15
+ decision: 'block',
16
+ reason:
17
+ 'You changed source files this turn but did not update docs/. Per the CLAUDE.md docs protocol: create or update the matching entry (docs/features/ for a feature, docs/fixes/ for a bug fix, docs/decisions/ for an architectural decision — templates in docs/_templates/) AND tick progress in the current docs/phases/ doc, then finish.',
18
+ }),
19
+ );
20
+ process.exit(0);
21
+ }
22
+
23
+ // Clean turn — reset state for the next one.
24
+ writeState(ev.session_id, { codeChanged: false, docsChanged: false, reminded: false });
25
+ process.exit(0);
@@ -0,0 +1,45 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "Edit|Write|MultiEdit",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/pre-edit.mjs\""
10
+ }
11
+ ]
12
+ },
13
+ {
14
+ "matcher": "Bash",
15
+ "hooks": [
16
+ {
17
+ "type": "command",
18
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard.mjs\""
19
+ }
20
+ ]
21
+ }
22
+ ],
23
+ "PostToolUse": [
24
+ {
25
+ "matcher": "Edit|Write|MultiEdit",
26
+ "hooks": [
27
+ {
28
+ "type": "command",
29
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/post-edit.mjs\""
30
+ }
31
+ ]
32
+ }
33
+ ],
34
+ "Stop": [
35
+ {
36
+ "hooks": [
37
+ {
38
+ "type": "command",
39
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/stop-gate.mjs\""
40
+ }
41
+ ]
42
+ }
43
+ ]
44
+ }
45
+ }
@@ -0,0 +1,8 @@
1
+ # CHANGELOG-AUTO
2
+
3
+ > ⚠️ **Machine-written file.** Every row below is appended automatically by the
4
+ > post-edit hook (`.claude/hooks/post-edit.mjs`) whenever Claude Code edits a
5
+ > tracked file. Never edit this file by hand.
6
+
7
+ | timestamp (UTC) | session | tool | file | ±lines |
8
+ |---|---|---|---|---|
@@ -0,0 +1,22 @@
1
+ # docs/ — the project knowledge base
2
+
3
+ This directory is the project's tracked knowledge base. It is **enforced by hooks**:
4
+ you cannot end a Claude Code turn that changed source code without updating
5
+ something here.
6
+
7
+ | Path | What it is | Who writes it |
8
+ |------|------------|---------------|
9
+ | `features/` | One doc per implemented feature (`<phase>-<name>.md`) | you (Claude) |
10
+ | `fixes/` | One doc per bug fix (`YYYY-MM-DD-<slug>.md`) | you (Claude) |
11
+ | `decisions/` | Architecture Decision Records (`NNNN-<title>.md`) | you (Claude) |
12
+ | `phases/` | Living per-phase progress checklists (`phase-N-<name>.md`) | you (Claude) |
13
+ | `plans/` | Implementation plans (`YYYY-MM-DD-<name>.md`) | planning workflow |
14
+ | `specs/` | Approved design specs (`YYYY-MM-DD-<name>-design.md`) | design workflow |
15
+ | `_templates/` | Copy-from templates for all of the above | eng-skills (don't edit casually) |
16
+ | `CHANGELOG-AUTO.md` | Append-only, machine-written edit log | the post-edit hook (**never by hand**) |
17
+
18
+ Cross-link docs with `[[wiki-style]]` links so phases ↔ features ↔ fixes ↔ decisions
19
+ stay navigable.
20
+
21
+ **The rule:** after implementing or fixing anything, write the matching doc
22
+ **before the turn ends.**
@@ -0,0 +1,17 @@
1
+ # NNNN — <title>
2
+
3
+ **Date:** YYYY-MM-DD · **Status:** proposed | accepted | superseded by [[NNNN]]
4
+
5
+ ## Context
6
+ <The forces at play: requirements, constraints, what problem this decides.>
7
+
8
+ ## Options considered
9
+ 1. **<Option A>** — pros / cons
10
+ 2. **<Option B>** — pros / cons
11
+ 3. **<Option C>** — pros / cons
12
+
13
+ ## Decision
14
+ <The choice made.>
15
+
16
+ ## Consequences
17
+ <What becomes easier/harder. Follow-ups. What this commits us to.>
@@ -0,0 +1,21 @@
1
+ # Feature — <name>
2
+
3
+ **Phase:** <N> · **Date:** YYYY-MM-DD · **Status:** implemented | partial
4
+
5
+ ## What it does
6
+ <One-paragraph summary of the capability.>
7
+
8
+ ## How to use it
9
+ <Entry points: endpoints, events, functions, CLI. Example request/payload.>
10
+
11
+ ## Contract touchpoints
12
+ <APIs, events, DB models, or types this feature touches.>
13
+
14
+ ## Dependencies
15
+ <Packages/modules/services this relies on.>
16
+
17
+ ## Tests
18
+ <Where the tests live and what they assert.>
19
+
20
+ ## Notes / follow-ups
21
+ <Known gaps, deferred work, links to related [[docs]].>
@@ -0,0 +1,18 @@
1
+ # Fix — <slug>
2
+
3
+ **Date:** YYYY-MM-DD · **Phase:** <N> · **Severity:** low | med | high
4
+
5
+ ## Symptom
6
+ <What was observed. Error messages, repro steps.>
7
+
8
+ ## Root cause
9
+ <The actual underlying cause, not just the surface symptom.>
10
+
11
+ ## Fix
12
+ <What changed and why it resolves the root cause. Files touched.>
13
+
14
+ ## Verification
15
+ <Command(s) run + observed output proving the fix works.>
16
+
17
+ ## Prevention
18
+ <Test added / guard added so this can't regress. Links to [[docs]].>
@@ -0,0 +1,17 @@
1
+ # Phase <N> — <name>
2
+
3
+ **Status:** not started | in progress | complete · **Plan:** [[docs/plans/...]]
4
+
5
+ ## Goal
6
+ <One sentence.>
7
+
8
+ ## Acceptance criteria
9
+ - [ ] <criterion 1>
10
+ - [ ] <criterion 2>
11
+
12
+ ## Task progress
13
+ - [ ] Task 1 — <name>
14
+ - [ ] Task 2 — <name>
15
+
16
+ ## Notes / decisions made during the phase
17
+ <Running log. Link [[decisions]] and [[features]].>
@@ -0,0 +1,24 @@
1
+ # 0001 — Record architecture decisions
2
+
3
+ **Date:** {{DATE}} · **Status:** accepted
4
+
5
+ ## Context
6
+ This project is built with Claude Code. Agent sessions are ephemeral: context is
7
+ lost between sessions, and the reasoning behind a choice evaporates unless it is
8
+ written down where the next session will find it. We need a durable, low-friction
9
+ record of significant architectural and trade-off decisions.
10
+
11
+ ## Options considered
12
+ 1. **No records** — zero friction, but every future session (human or agent) re-litigates old decisions and repeats mistakes.
13
+ 2. **Decisions embedded in code comments** — close to the code, but scattered, unsearchable as a set, and invisible to planning.
14
+ 3. **Architecture Decision Records in `docs/decisions/`** — one numbered file per decision with context, options, decision, consequences. Searchable, linkable, cheap.
15
+
16
+ ## Decision
17
+ We record every significant decision as an ADR in `docs/decisions/NNNN-<title>.md`,
18
+ using `docs/_templates/decision.md`. The docs-protocol hooks enforce that decisions
19
+ made while coding get written down in the same turn.
20
+
21
+ ## Consequences
22
+ Future sessions can read *why*, not just *what*. Superseded decisions are marked,
23
+ not deleted, preserving history. The set of ADRs becomes the project's long-term
24
+ memory alongside `CLAUDE.md`.
@@ -0,0 +1,18 @@
1
+ # Phase 0 — foundation
2
+
3
+ **Status:** in progress · **Plan:** —
4
+
5
+ ## Goal
6
+ Establish the eng-skills workflow in this repo: hooks live, CLAUDE.md filled in, first real work documented.
7
+
8
+ ## Acceptance criteria
9
+ - [ ] `/onboard` run — CLAUDE.md has no remaining `TODO(onboard)` markers
10
+ - [ ] Hooks verified firing (check `/hooks` inside Claude Code)
11
+ - [ ] First feature/fix documented under `docs/`
12
+
13
+ ## Task progress
14
+ - [x] Task 0 — scaffold installed via `npx eng-skills init` ({{DATE}})
15
+ - [ ] Task 1 — run `/onboard`
16
+
17
+ ## Notes / decisions made during the phase
18
+ See [[0001-record-architecture-decisions]].