@esneiderbravo/speclaw 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.
Files changed (62) hide show
  1. package/ATTRIBUTION.md +34 -0
  2. package/LICENSE +21 -0
  3. package/README.md +134 -0
  4. package/dist/cli/commands/agent.js +42 -0
  5. package/dist/cli/commands/doctor.js +21 -0
  6. package/dist/cli/commands/index-build.js +24 -0
  7. package/dist/cli/commands/init.js +121 -0
  8. package/dist/cli/commands/query.js +69 -0
  9. package/dist/cli/commands/spec.js +71 -0
  10. package/dist/cli/index.js +77 -0
  11. package/dist/cli/lib/args.js +44 -0
  12. package/dist/cli/lib/ui.js +89 -0
  13. package/dist/modules/compass/db.js +84 -0
  14. package/dist/modules/compass/embedder.js +87 -0
  15. package/dist/modules/compass/extract.js +91 -0
  16. package/dist/modules/compass/indexer.js +158 -0
  17. package/dist/modules/compass/languages.js +67 -0
  18. package/dist/modules/compass/parser.js +37 -0
  19. package/dist/modules/compass/query.js +260 -0
  20. package/dist/modules/compass/register.js +72 -0
  21. package/dist/modules/compass/watcher.js +95 -0
  22. package/dist/modules/foundation/assets/AGENTS.template.md +61 -0
  23. package/dist/modules/foundation/assets/CLAUDE.template.md +72 -0
  24. package/dist/modules/foundation/assets/LAWS.template.md +39 -0
  25. package/dist/modules/foundation/assets/docs/compass.template.md +43 -0
  26. package/dist/modules/foundation/assets/docs/standards/architecture.template.md +36 -0
  27. package/dist/modules/foundation/assets/docs/standards/backend-standards.template.md +50 -0
  28. package/dist/modules/foundation/assets/docs/standards/base-standards.template.md +47 -0
  29. package/dist/modules/foundation/assets/docs/standards/conventions.template.md +31 -0
  30. package/dist/modules/foundation/assets/docs/standards/documentation.template.md +50 -0
  31. package/dist/modules/foundation/assets/docs/standards/frontend-standards.template.md +46 -0
  32. package/dist/modules/foundation/assets/docs/standards/spec-workflow.template.md +46 -0
  33. package/dist/modules/foundation/assets/docs/standards/testing-standards.template.md +34 -0
  34. package/dist/modules/foundation/doctor.js +107 -0
  35. package/dist/modules/foundation/register.js +86 -0
  36. package/dist/modules/foundation/scaffold.js +103 -0
  37. package/dist/modules/spec/assets/commands/archive.md +10 -0
  38. package/dist/modules/spec/assets/commands/build.md +11 -0
  39. package/dist/modules/spec/assets/commands/draft.md +12 -0
  40. package/dist/modules/spec/assets/commands/explore.md +10 -0
  41. package/dist/modules/spec/assets/commands/sync.md +9 -0
  42. package/dist/modules/spec/assets/rules/spec-tasks-mandatory-steps.md +37 -0
  43. package/dist/modules/spec/assets/skills/archive/SKILL.md +22 -0
  44. package/dist/modules/spec/assets/skills/build/SKILL.md +49 -0
  45. package/dist/modules/spec/assets/skills/draft/SKILL.md +64 -0
  46. package/dist/modules/spec/assets/skills/explore/SKILL.md +28 -0
  47. package/dist/modules/spec/assets/skills/sync/SKILL.md +21 -0
  48. package/dist/modules/spec/engine.js +227 -0
  49. package/dist/modules/spec/register.js +53 -0
  50. package/dist/modules/tools/assets/packs/agents/backend-developer.md +61 -0
  51. package/dist/modules/tools/assets/packs/agents/frontend-developer.md +62 -0
  52. package/dist/modules/tools/assets/packs/agents/product-strategy-analyst.md +56 -0
  53. package/dist/modules/tools/assets/packs.json +6 -0
  54. package/dist/modules/tools/packs.js +44 -0
  55. package/dist/modules/tools/register.js +27 -0
  56. package/dist/server.js +21 -0
  57. package/dist/shared/agents.js +94 -0
  58. package/dist/shared/install.js +66 -0
  59. package/dist/shared/mcp.js +16 -0
  60. package/dist/shared/paths.js +10 -0
  61. package/dist/shared/render.js +21 -0
  62. package/package.json +49 -0
@@ -0,0 +1,260 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { openDb, indexExists } from "./db.js";
4
+ import { getEmbedder, fromBlob, cosine } from "./embedder.js";
5
+ function requireIndex(projectPath) {
6
+ if (!indexExists(projectPath)) {
7
+ throw new Error("No index found. Build it first with the index_build tool (creates .speclaw/index.db).");
8
+ }
9
+ }
10
+ /**
11
+ * Structural search: find nodes whose name contains `query` (substring match),
12
+ * ranking exact matches and shorter names first.
13
+ *
14
+ * @param projectPath - Absolute path to the indexed project.
15
+ * @param query - Name or keyword to match as a substring.
16
+ * @param limit - Maximum number of hits to return.
17
+ * @returns Matching nodes ordered by relevance.
18
+ * @throws If no index exists for the project.
19
+ */
20
+ export function search(projectPath, query, limit = 25) {
21
+ requireIndex(projectPath);
22
+ const db = openDb(projectPath);
23
+ try {
24
+ const rows = db
25
+ .prepare(`SELECT s.name, s.kind, f.path AS file, s.start_line AS line, s.signature
26
+ FROM nodes s JOIN files f ON f.id = s.file_id
27
+ WHERE s.name LIKE ?
28
+ ORDER BY (s.name = ?) DESC, length(s.name) ASC
29
+ LIMIT ?`)
30
+ .all(`%${query}%`, query, limit);
31
+ return rows;
32
+ }
33
+ finally {
34
+ db.close();
35
+ }
36
+ }
37
+ function readSource(projectPath, file, startByte, endByte) {
38
+ try {
39
+ const buf = fs.readFileSync(path.join(projectPath, file));
40
+ return buf.subarray(startByte, endByte).toString("utf8");
41
+ }
42
+ catch {
43
+ return "";
44
+ }
45
+ }
46
+ /**
47
+ * Explore an exact node by name: return its verbatim source, callees, and
48
+ * callers (its blast radius).
49
+ *
50
+ * When several nodes share the name, functions and classes are preferred as the
51
+ * primary result and the rest are surfaced under `otherMatches`. When no exact
52
+ * match exists, falls back to a fuzzy {@link search} so the caller still gets
53
+ * useful candidates.
54
+ *
55
+ * @param projectPath - Absolute path to the indexed project.
56
+ * @param query - Exact node name to explore.
57
+ * @returns The explore result; `found` is `false` when no exact match exists.
58
+ * @throws If no index exists for the project.
59
+ */
60
+ export function explore(projectPath, query) {
61
+ requireIndex(projectPath);
62
+ const db = openDb(projectPath);
63
+ try {
64
+ const matches = db
65
+ .prepare(`SELECT s.id, s.name, s.kind, s.start_line, s.end_line, s.start_byte, s.end_byte,
66
+ s.signature, f.path AS file
67
+ FROM nodes s JOIN files f ON f.id = s.file_id
68
+ WHERE s.name = ?
69
+ ORDER BY s.kind = 'function' DESC, s.kind = 'class' DESC
70
+ LIMIT 10`)
71
+ .all(query);
72
+ if (matches.length === 0) {
73
+ // fall back to fuzzy search so the caller gets something useful
74
+ const near = search(projectPath, query, 10);
75
+ return {
76
+ found: false,
77
+ message: `No exact symbol named "${query}". ${near.length} similar symbol(s) below.`,
78
+ otherMatches: near,
79
+ };
80
+ }
81
+ const best = matches[0];
82
+ const callees = db
83
+ .prepare(`SELECT e.dst_name AS name, e.line, f.path AS file
84
+ FROM edges e LEFT JOIN nodes s ON s.id = e.dst_node_id
85
+ LEFT JOIN files f ON f.id = s.file_id
86
+ WHERE e.src_node_id = ? AND e.kind = 'call'
87
+ ORDER BY e.line`)
88
+ .all(best.id);
89
+ // Callers: match both the resolved edge AND any call by this name, so
90
+ // dynamic dispatch (a method/function called by name across sites) is not
91
+ // missed. Over-approximates conservatively — the point of a blast radius.
92
+ const callers = db
93
+ .prepare(`SELECT DISTINCT owner.name AS name, owner.kind AS kind, f.path AS file, e.line
94
+ FROM edges e
95
+ JOIN nodes owner ON owner.id = e.src_node_id
96
+ JOIN files f ON f.id = owner.file_id
97
+ WHERE (e.dst_node_id = ? OR e.dst_name = ?) AND e.kind = 'call'
98
+ ORDER BY f.path, e.line`)
99
+ .all(best.id, best.name);
100
+ return {
101
+ found: true,
102
+ symbol: {
103
+ name: best.name,
104
+ kind: best.kind,
105
+ file: best.file,
106
+ startLine: best.start_line,
107
+ endLine: best.end_line,
108
+ signature: best.signature,
109
+ source: readSource(projectPath, best.file, best.start_byte, best.end_byte),
110
+ },
111
+ callees: callees.map((c) => ({ name: c.name, file: c.file ?? undefined, line: c.line })),
112
+ callers,
113
+ otherMatches: matches.length > 1
114
+ ? matches.slice(1).map((m) => ({
115
+ name: m.name, kind: m.kind, file: m.file, line: m.start_line, signature: m.signature,
116
+ }))
117
+ : undefined,
118
+ };
119
+ }
120
+ finally {
121
+ db.close();
122
+ }
123
+ }
124
+ /**
125
+ * Semantic search: embed the natural-language query and rank nodes by cosine
126
+ * similarity against the local vector store. Finds code by meaning, not just
127
+ * by matching identifier substrings.
128
+ *
129
+ * @param projectPath - Absolute path to the indexed project.
130
+ * @param query - Natural-language description of the code being sought.
131
+ * @param limit - Maximum number of hits to return.
132
+ * @returns Nodes sorted by descending similarity score.
133
+ * @throws If no index exists for the project.
134
+ */
135
+ export async function recall(projectPath, query, limit = 15) {
136
+ requireIndex(projectPath);
137
+ const embedder = getEmbedder();
138
+ const qvec = await embedder.embed(query);
139
+ const db = openDb(projectPath);
140
+ try {
141
+ const rows = db
142
+ .prepare(`SELECT n.name, n.kind, f.path AS file, n.start_line AS line, n.signature, e.vec
143
+ FROM node_embeddings e
144
+ JOIN nodes n ON n.id = e.node_id
145
+ JOIN files f ON f.id = n.file_id
146
+ WHERE e.dim = ?`)
147
+ .all(embedder.dim);
148
+ const scored = rows.map((r) => ({
149
+ name: r.name,
150
+ kind: r.kind,
151
+ file: r.file,
152
+ line: r.line,
153
+ signature: r.signature,
154
+ score: cosine(qvec, fromBlob(r.vec)),
155
+ }));
156
+ scored.sort((a, b) => b.score - a.score);
157
+ return scored.slice(0, limit);
158
+ }
159
+ finally {
160
+ db.close();
161
+ }
162
+ }
163
+ /**
164
+ * Transitive blast radius: every node that (transitively) calls the target,
165
+ * up to maxDepth hops. Expansion is by call-name, so dynamic dispatch is
166
+ * included — the conservative answer to "what could break if I change this?".
167
+ *
168
+ * @param projectPath - Absolute path to the indexed project.
169
+ * @param nodeName - Name of the node whose dependents are wanted.
170
+ * @param maxDepth - Maximum number of call hops to traverse outward.
171
+ * @returns The reached nodes, each tagged with its discovery depth.
172
+ * @throws If no index exists for the project.
173
+ */
174
+ export function impact(projectPath, nodeName, maxDepth = 4) {
175
+ requireIndex(projectPath);
176
+ const db = openDb(projectPath);
177
+ try {
178
+ const visited = new Set();
179
+ const results = [];
180
+ let frontier = [nodeName];
181
+ for (let depth = 1; depth <= maxDepth && frontier.length > 0; depth++) {
182
+ const placeholders = frontier.map(() => "?").join(",");
183
+ const callers = db
184
+ .prepare(`SELECT DISTINCT owner.id, owner.name, owner.kind, f.path AS file, MIN(owner.start_line) AS line
185
+ FROM edges e
186
+ JOIN nodes owner ON owner.id = e.src_node_id
187
+ JOIN files f ON f.id = owner.file_id
188
+ WHERE e.kind = 'call' AND e.dst_name IN (${placeholders})
189
+ GROUP BY owner.id`)
190
+ .all(...frontier);
191
+ const nextNames = new Set();
192
+ for (const c of callers) {
193
+ if (visited.has(c.id))
194
+ continue;
195
+ visited.add(c.id);
196
+ results.push({ name: c.name, kind: c.kind, file: c.file, line: c.line, depth });
197
+ nextNames.add(c.name);
198
+ }
199
+ frontier = [...nextNames];
200
+ }
201
+ return results;
202
+ }
203
+ finally {
204
+ db.close();
205
+ }
206
+ }
207
+ /**
208
+ * Trace a call path from one node to another: BFS forward over call edges (by
209
+ * name) from `from` until `to` is reached, returning the chain of names. null
210
+ * path means no route within maxDepth.
211
+ *
212
+ * @param projectPath - Absolute path to the indexed project.
213
+ * @param from - Name of the starting node.
214
+ * @param to - Name of the target node.
215
+ * @param maxDepth - Maximum number of call hops to search.
216
+ * @returns The trace result; `path` is `null` and `hops` is `-1` when no route
217
+ * is found within `maxDepth`.
218
+ * @throws If no index exists for the project.
219
+ */
220
+ export function trace(projectPath, from, to, maxDepth = 8) {
221
+ requireIndex(projectPath);
222
+ const db = openDb(projectPath);
223
+ try {
224
+ if (from === to)
225
+ return { from, to, path: [from], hops: 0 };
226
+ const parent = new Map();
227
+ const seen = new Set([from]);
228
+ let frontier = [from];
229
+ const calleesStmt = db.prepare(`SELECT DISTINCT e.dst_name AS callee
230
+ FROM edges e JOIN nodes src ON src.id = e.src_node_id
231
+ WHERE e.kind = 'call' AND src.name = ?`);
232
+ for (let depth = 0; depth < maxDepth && frontier.length > 0; depth++) {
233
+ const next = [];
234
+ for (const name of frontier) {
235
+ const callees = calleesStmt.all(name);
236
+ for (const { callee } of callees) {
237
+ if (seen.has(callee))
238
+ continue;
239
+ seen.add(callee);
240
+ parent.set(callee, name);
241
+ if (callee === to) {
242
+ const path = [to];
243
+ let cur = to;
244
+ while (parent.has(cur)) {
245
+ cur = parent.get(cur);
246
+ path.unshift(cur);
247
+ }
248
+ return { from, to, path, hops: path.length - 1 };
249
+ }
250
+ next.push(callee);
251
+ }
252
+ }
253
+ frontier = next;
254
+ }
255
+ return { from, to, path: null, hops: -1 };
256
+ }
257
+ finally {
258
+ db.close();
259
+ }
260
+ }
@@ -0,0 +1,72 @@
1
+ import { z } from "zod";
2
+ import { text } from "../../shared/mcp.js";
3
+ import { buildIndex } from "./indexer.js";
4
+ import { explore, search, recall, impact, trace } from "./query.js";
5
+ import { startWatch, stopWatch, watchStatus } from "./watcher.js";
6
+ // ─── Compass: speclaw's own code-intelligence engine (no external deps) ───
7
+ // A local graph of the codebase (nodes = definitions, edges = calls/imports)
8
+ // plus a local vector store for semantic recall. Lives in .speclaw/ (gitignored).
9
+ /**
10
+ * Register all Compass MCP tools (index, explore, search, recall, impact,
11
+ * trace, watch) on the given server.
12
+ *
13
+ * @param server - The MCP server to register the Compass tools on.
14
+ */
15
+ export function registerCompass(server) {
16
+ server.registerTool("compass_index", {
17
+ description: "Build or incrementally refresh the Compass — speclaw's local code graph (.speclaw/index.db). Parses TS/JS/Python with tree-sitter into nodes (definitions) and edges (calls/imports), and computes a local vector embedding per node for semantic recall. Files unchanged since the last run are skipped by content hash. Run once after init and whenever you want a fresh graph.",
18
+ inputSchema: { projectPath: z.string().describe("Absolute path to the project") },
19
+ }, async ({ projectPath }) => text(await buildIndex(projectPath)));
20
+ server.registerTool("compass_explore", {
21
+ description: "Explore a node in the Compass: returns its verbatim source, location, callees, and resolved callers (blast radius). Use this BEFORE grep/read when locating or understanding code. Requires compass_index to have run.",
22
+ inputSchema: {
23
+ projectPath: z.string().describe("Absolute path to the project"),
24
+ node: z.string().describe("Exact node name to explore (function/class/method/type)"),
25
+ },
26
+ }, async ({ projectPath, node }) => text(explore(projectPath, node)));
27
+ server.registerTool("compass_search", {
28
+ description: "Structural search of the Compass: find nodes by name or keyword (substring match). Returns name, kind, and file:line per hit. Cheaper and more structural than grep. Requires compass_index to have run.",
29
+ inputSchema: {
30
+ projectPath: z.string().describe("Absolute path to the project"),
31
+ query: z.string().describe("Name or keyword to search for"),
32
+ limit: z.number().optional().describe("Max results (default 25)"),
33
+ },
34
+ }, async ({ projectPath, query, limit }) => text(search(projectPath, query, limit ?? 25)));
35
+ server.registerTool("compass_recall", {
36
+ description: "Semantic search of the Compass: describe what you're looking for in natural language ('where auth tokens are validated') and get the nodes ranked by meaning, using the local vector store — even when the identifier names don't contain your words. Requires compass_index to have run.",
37
+ inputSchema: {
38
+ projectPath: z.string().describe("Absolute path to the project"),
39
+ query: z.string().describe("Natural-language description of the code you want"),
40
+ limit: z.number().optional().describe("Max results (default 15)"),
41
+ },
42
+ }, async ({ projectPath, query, limit }) => text(await recall(projectPath, query, limit ?? 15)));
43
+ server.registerTool("compass_impact", {
44
+ description: "Blast radius: every node that transitively calls the target, up to a depth. Answers 'what could break if I change this?' before editing. Includes dynamic-dispatch callers (matched by name). Requires compass_index.",
45
+ inputSchema: {
46
+ projectPath: z.string().describe("Absolute path to the project"),
47
+ node: z.string().describe("Node name whose dependents you want"),
48
+ maxDepth: z.number().optional().describe("Max hops to traverse (default 4)"),
49
+ },
50
+ }, async ({ projectPath, node, maxDepth }) => text(impact(projectPath, node, maxDepth ?? 4)));
51
+ server.registerTool("compass_trace", {
52
+ description: "Trace a call path from one node to another: returns the chain of calls linking them (or null if none within depth). Useful to understand how an entrypoint reaches a sink. Requires compass_index.",
53
+ inputSchema: {
54
+ projectPath: z.string().describe("Absolute path to the project"),
55
+ from: z.string().describe("Starting node name"),
56
+ to: z.string().describe("Target node name"),
57
+ maxDepth: z.number().optional().describe("Max hops to search (default 8)"),
58
+ },
59
+ }, async ({ projectPath, from, to, maxDepth }) => text(trace(projectPath, from, to, maxDepth ?? 8)));
60
+ server.registerTool("compass_watch", {
61
+ description: "Keep the Compass index fresh automatically: start/stop a file watcher that incrementally re-indexes on change (debounced). action=start|stop|status. Optional — the index is also refreshed on demand by compass_index.",
62
+ inputSchema: {
63
+ projectPath: z.string().describe("Absolute path to the project"),
64
+ action: z.enum(["start", "stop", "status"]).describe("start, stop, or status"),
65
+ },
66
+ }, async ({ projectPath, action }) => {
67
+ const result = action === "start" ? startWatch(projectPath)
68
+ : action === "stop" ? stopWatch(projectPath)
69
+ : watchStatus(projectPath);
70
+ return text(result);
71
+ });
72
+ }
@@ -0,0 +1,95 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { buildIndex } from "./indexer.js";
4
+ const active = new Map();
5
+ const SKIP = new Set([
6
+ ".git", "node_modules", "dist", "build", ".next", ".speclaw",
7
+ "__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache",
8
+ ]);
9
+ function scheduleReindex(projectPath, state) {
10
+ if (state.timer)
11
+ clearTimeout(state.timer);
12
+ state.timer = setTimeout(() => {
13
+ state.timer = null;
14
+ buildIndex(projectPath)
15
+ .then(() => {
16
+ state.reindexes++;
17
+ })
18
+ .catch(() => {
19
+ /* best-effort: a transient parse/read error must not crash the server */
20
+ });
21
+ }, 400);
22
+ }
23
+ function watchDirsRecursively(projectPath, state) {
24
+ const walk = (dir) => {
25
+ let entries;
26
+ try {
27
+ entries = fs.readdirSync(dir, { withFileTypes: true });
28
+ }
29
+ catch {
30
+ return;
31
+ }
32
+ state.watchers.push(fs.watch(dir, () => scheduleReindex(projectPath, state)));
33
+ for (const e of entries) {
34
+ if (e.isDirectory() && !SKIP.has(e.name))
35
+ walk(path.join(dir, e.name));
36
+ }
37
+ };
38
+ walk(projectPath);
39
+ }
40
+ /**
41
+ * Start watching the project; changes trigger a debounced incremental reindex.
42
+ *
43
+ * Prefers a single recursive watch (macOS/Windows) and falls back to watching
44
+ * each directory individually where recursive watching is unsupported (Linux).
45
+ * Idempotent: if already watching, returns the current status without starting
46
+ * a second watcher.
47
+ *
48
+ * @param projectPath - Absolute path to the project root.
49
+ * @returns The watcher status after starting.
50
+ */
51
+ export function startWatch(projectPath) {
52
+ if (active.has(projectPath))
53
+ return watchStatus(projectPath);
54
+ const state = { watchers: [], reindexes: 0, timer: null, recursive: false };
55
+ try {
56
+ // Recursive watch is supported on macOS and Windows.
57
+ state.watchers.push(fs.watch(projectPath, { recursive: true }, () => scheduleReindex(projectPath, state)));
58
+ state.recursive = true;
59
+ }
60
+ catch {
61
+ // Linux and others: watch each directory individually.
62
+ watchDirsRecursively(projectPath, state);
63
+ }
64
+ active.set(projectPath, state);
65
+ return watchStatus(projectPath);
66
+ }
67
+ /**
68
+ * Stop watching the project, closing all watchers and cancelling any pending
69
+ * reindex. Safe to call when not watching.
70
+ *
71
+ * @param projectPath - Absolute path to the project root.
72
+ * @returns A status reflecting the stopped watcher (preserving the reindex count).
73
+ */
74
+ export function stopWatch(projectPath) {
75
+ const state = active.get(projectPath);
76
+ if (state) {
77
+ if (state.timer)
78
+ clearTimeout(state.timer);
79
+ for (const w of state.watchers)
80
+ w.close();
81
+ active.delete(projectPath);
82
+ }
83
+ return { watching: false, reindexes: state?.reindexes ?? 0, mode: null };
84
+ }
85
+ /** Report whether the project is being watched, and in which mode. */
86
+ export function watchStatus(projectPath) {
87
+ const state = active.get(projectPath);
88
+ if (!state)
89
+ return { watching: false, reindexes: 0, mode: null };
90
+ return {
91
+ watching: true,
92
+ reindexes: state.reindexes,
93
+ mode: state.recursive ? "recursive" : "per-directory",
94
+ };
95
+ }
@@ -0,0 +1,61 @@
1
+ # AGENTS.md — {{project_name}}
2
+
3
+ Operating contract for **every** AI agent (Claude Code, Cursor, Codex, or any
4
+ other) working in this repository. These rules are STRICT and non-negotiable.
5
+ Claude-specific notes: [`CLAUDE.md`](CLAUDE.md). The law: [`LAWS.md`](LAWS.md).
6
+
7
+ ## Project
8
+
9
+ - **What it is**: {{project_description}}
10
+ - **Organization**: {{organization}}
11
+ - **Stack**: {{stack_summary}}
12
+
13
+ ## Mandatory operating rules
14
+
15
+ 1. **Read [`LAWS.md`](LAWS.md) first.** It is the constitution; it binds the
16
+ standards below. Open the standard that governs your change before making
17
+ it. Conflicts resolve in favor of the standard; amendments go through a
18
+ spec change, never silent deviation.
19
+ 2. **Use Compass before file scanning** — the `compass_explore` /
20
+ `compass_search` / `compass_recall` tools (run `compass_index` first if the
21
+ graph is missing). Cheat sheet: [`docs/compass.md`](docs/compass.md).
22
+ 3. **Follow the spec workflow** for every non-trivial change; archive
23
+ within the same PR. Rules:
24
+ [`docs/standards/spec-workflow.md`](docs/standards/spec-workflow.md).
25
+ 4. **Run the quality gates yourself** before declaring anything done — see
26
+ [`docs/standards/testing-standards.md`](docs/standards/testing-standards.md):
27
+ - Tests: `{{test_commands}}`
28
+ - Lint / type-check: `{{lint_commands}}`
29
+ 5. **Respect the conventions** — branches `{{branch_pattern}}`, commits
30
+ {{commit_style}}, code that reads like its neighbors. See
31
+ [`docs/standards/base-standards.md`](docs/standards/base-standards.md) and
32
+ [`docs/standards/conventions.md`](docs/standards/conventions.md).
33
+ 6. **Use the skills.** `ai-specs/` is the canonical home for skills, commands,
34
+ and subagents, mirrored to each IDE directory via symlinks.
35
+ 7. **Ask before irreversible or outward-facing actions.**
36
+
37
+ ## The standards (the law, in detail)
38
+
39
+ | Standard | Governs |
40
+ |----------|---------|
41
+ | [`docs/standards/base-standards.md`](docs/standards/base-standards.md) | Languages, commits, comments, dependencies |
42
+ | [`docs/standards/architecture.md`](docs/standards/architecture.md) | Modules, layering, boundaries |
43
+ | [`docs/standards/backend-standards.md`](docs/standards/backend-standards.md) | Backend layers, docstrings, typing, migrations |
44
+ | [`docs/standards/frontend-standards.md`](docs/standards/frontend-standards.md) | Frontend layers, rendering, i18n, UI |
45
+ | [`docs/standards/testing-standards.md`](docs/standards/testing-standards.md) | Quality gates, testing rules |
46
+ | [`docs/standards/documentation.md`](docs/standards/documentation.md) | Docstring/API-comment convention per language |
47
+ | [`docs/standards/conventions.md`](docs/standards/conventions.md) | Branches, PRs, tracker, versioning |
48
+ | [`docs/standards/spec-workflow.md`](docs/standards/spec-workflow.md) | Spec-driven workflow, archiving |
49
+ | [`docs/compass.md`](docs/compass.md) | Compass usage |
50
+
51
+ ## Directory map for agents
52
+
53
+ | Path | Purpose |
54
+ | --- | --- |
55
+ | `LAWS.md` | The constitution — binds the standards |
56
+ | `docs/standards/` | The individual laws (one file per standard) |
57
+ | `AGENTS.md` / `CLAUDE.md` | Agent entry points (this contract) |
58
+ | `ai-specs/` | Canonical skills, commands, rules, agents |
59
+ | `.claude/` `.cursor/` `.codex/` `.agents/` | IDE mirrors (symlinks into `ai-specs/`) |
60
+ | `spec/` | Spec-driven workflow: specs, changes, archive |
61
+ | `.mcp.json` | MCP wiring (speclaw) |
@@ -0,0 +1,72 @@
1
+ # CLAUDE.md — {{project_name}} Agent Operating Rules
2
+
3
+ Agent rules for the **{{project_name}}** repository ({{project_description}}).
4
+ These rules are STRICT. Read this file at the start of every session.
5
+ Cross-agent context: [`AGENTS.md`](AGENTS.md) · The law: [`LAWS.md`](LAWS.md)
6
+
7
+ ---
8
+
9
+ ## Rule 0 — The Law comes first
10
+
11
+ Read [`LAWS.md`](LAWS.md) before writing or changing any code. It is the
12
+ constitution: it binds the individual standards below. Open the standard that
13
+ governs the area you're touching **before** touching it:
14
+
15
+ | You're working on… | Read first |
16
+ |--------------------|-----------|
17
+ | Anything | [`docs/standards/base-standards.md`](docs/standards/base-standards.md) |
18
+ | Structure / boundaries | [`docs/standards/architecture.md`](docs/standards/architecture.md) |
19
+ | Backend code | [`docs/standards/backend-standards.md`](docs/standards/backend-standards.md) |
20
+ | Frontend code | [`docs/standards/frontend-standards.md`](docs/standards/frontend-standards.md) |
21
+ | Tests / gates | [`docs/standards/testing-standards.md`](docs/standards/testing-standards.md) |
22
+ | Docstrings / API comments | [`docs/standards/documentation.md`](docs/standards/documentation.md) |
23
+ | Branches / PRs / tickets | [`docs/standards/conventions.md`](docs/standards/conventions.md) |
24
+ | Any non-trivial change | [`docs/standards/spec-workflow.md`](docs/standards/spec-workflow.md) |
25
+
26
+ When any instruction conflicts with a standard, **the standard wins** — and if
27
+ you believe it is wrong, propose an amendment via a spec change; never silently
28
+ ignore it.
29
+
30
+ ## Rule 1 — Compass before grep
31
+
32
+ This repo is indexed by Compass, speclaw's local code graph (`.speclaw/`). Use
33
+ the `compass_explore`, `compass_search`, and `compass_recall` tools **BEFORE**
34
+ grep/find or reading files at random; run `compass_index` first if the graph
35
+ is missing. See [`docs/compass.md`](docs/compass.md). Fall back to Grep/Read
36
+ only when the graph doesn't cover what you need.
37
+
38
+ ## Rule 2 — Spec-driven, always
39
+
40
+ No non-trivial change lands without a spec change (propose → implement →
41
+ verify → archive). The rules are in
42
+ [`docs/standards/spec-workflow.md`](docs/standards/spec-workflow.md);
43
+ the workflow skills live in `ai-specs/skills/` and the `/spec` commands wrap
44
+ them. A change is not done until it is archived — archiving belongs in the PR.
45
+
46
+ ## Rule 3 — Quality gates are non-negotiable
47
+
48
+ - Tests: `{{test_commands}}`
49
+ - Lint / type-check: `{{lint_commands}}`
50
+
51
+ Run them yourself and report real output. Never claim success you did not
52
+ observe. Full rules:
53
+ [`docs/standards/testing-standards.md`](docs/standards/testing-standards.md).
54
+
55
+ ## Rule 4 — Conventions
56
+
57
+ Branches `{{branch_pattern}}`, commits {{commit_style}}, code that reads like
58
+ its neighbors, comments that carry constraints (never ticket IDs). Full rules:
59
+ [`docs/standards/base-standards.md`](docs/standards/base-standards.md) and
60
+ [`docs/standards/conventions.md`](docs/standards/conventions.md).
61
+
62
+ ## Rule 5 — Skills are law-adjacent
63
+
64
+ Skills, commands, and subagents live in `ai-specs/` (symlinked into
65
+ `.claude/`, `.cursor/`, `.codex/`, `.agents/`). When a skill matches the
66
+ task, use it — do not improvise a parallel process.
67
+
68
+ ## Rule 6 — Stop conditions
69
+
70
+ Stop and ask the user before: destructive operations (deletes, force-push,
71
+ schema drops), publishing anything outward-facing (PR reviews, tickets,
72
+ comments), or any action that contradicts a standard.
@@ -0,0 +1,39 @@
1
+ # The Laws of {{project_name}}
2
+
3
+ > This is the constitution of this project. It does not restate the standards —
4
+ > it **binds** them. Every AI agent working in this repository MUST read this
5
+ > file at the start of a session and MUST comply with every standard it links
6
+ > below. When a standard conflicts with an agent's default behavior, the
7
+ > standard wins.
8
+
9
+ - **Project**: {{project_name}} — {{project_description}}
10
+ - **Organization**: {{organization}}
11
+ - **Stack**: {{stack_summary}}
12
+
13
+ ## The standards (each is a law)
14
+
15
+ | Law | File | Governs |
16
+ |-----|------|---------|
17
+ | Base | [`docs/standards/base-standards.md`](docs/standards/base-standards.md) | Languages, commits, comments, dependencies, engineering principles |
18
+ | Architecture | [`docs/standards/architecture.md`](docs/standards/architecture.md) | Modules/bounded contexts, layering, cross-boundary rules |
19
+ | Backend | [`docs/standards/backend-standards.md`](docs/standards/backend-standards.md) | Backend layers, docstrings, typing, tests, migrations |
20
+ | Frontend | [`docs/standards/frontend-standards.md`](docs/standards/frontend-standards.md) | Frontend layers, rendering boundaries, state, i18n, UI |
21
+ | Testing | [`docs/standards/testing-standards.md`](docs/standards/testing-standards.md) | Quality gates, what must be tested, verification |
22
+ | Documentation | [`docs/standards/documentation.md`](docs/standards/documentation.md) | Docstring/API-comment convention per language |
23
+ | Conventions | [`docs/standards/conventions.md`](docs/standards/conventions.md) | Branches, PRs, tracker, versioning |
24
+ | Spec | [`docs/standards/spec-workflow.md`](docs/standards/spec-workflow.md) | Spec-driven workflow, mandatory task steps, archiving |
25
+ | Compass | [`docs/compass.md`](docs/compass.md) | Using the code knowledge graph before grep |
26
+
27
+ ## Binding rules
28
+
29
+ 1. **Read the relevant standard before touching its area.** The table above is
30
+ the map. Agents open the standard that governs the code they're changing.
31
+ 2. **The standards are enforced, not advisory.** A violation is a blocking
32
+ finding in review.
33
+ 3. **Amendments go through the spec workflow.** A standard is changed like code — via a
34
+ reviewed change (see the spec-workflow law). An agent may propose an amendment;
35
+ it may never silently ignore a standard.
36
+ 4. **Entry points reference the law.** [`CLAUDE.md`](CLAUDE.md) and
37
+ [`AGENTS.md`](AGENTS.md) point every agent here first.
38
+
39
+ {{custom_laws}}
@@ -0,0 +1,43 @@
1
+ # Compass — code intelligence for agents in {{project_name}}
2
+
3
+ **Compass** is speclaw's local code graph: a pre-indexed map of every symbol
4
+ (node) and relationship (edge) in this workspace, plus a local vector store for
5
+ semantic recall. Agents MUST use it **before** manual grep/read loops when
6
+ exploring or editing code — this is Rule 1 of the agent contract (`AGENTS.md`).
7
+
8
+ It runs entirely on your machine, needs no LLM and no external service, and
9
+ stores everything in `.speclaw/` (gitignored). It ships inside speclaw — there
10
+ is nothing extra to install.
11
+
12
+ ## Why use it
13
+
14
+ | Without Compass | With Compass |
15
+ |-----------------|--------------|
16
+ | Many `Grep` + `Read` round-trips (tokens spent scanning) | One `compass_explore` call returns just the relevant node |
17
+ | Guess which file matters | `compass_recall` finds code by meaning |
18
+ | Edit without knowing the blast radius | callers/callees returned with the node |
19
+ | Whole files dumped into context | verbatim source of the node + its neighbors only |
20
+
21
+ The point is token economy: the agent gets exactly the code it needs to answer
22
+ a request, not whole files.
23
+
24
+ ## The tools
25
+
26
+ | Tool | Use it to |
27
+ |------|-----------|
28
+ | `compass_index` | Build/refresh the graph (`.speclaw/index.db`). Incremental — unchanged files are skipped by hash. Run once after init and after significant edits. |
29
+ | `compass_explore` | Read a node's verbatim source plus its callers and callees. The default before editing. |
30
+ | `compass_search` | Structural search: find nodes by name/keyword. |
31
+ | `compass_recall` | Semantic search: describe what you want in natural language and get nodes ranked by meaning. |
32
+ | `compass_impact` | Blast radius: every node that transitively calls a target — "what could break if I change this?" before editing. |
33
+ | `compass_trace` | Trace a call path between two nodes — how an entrypoint reaches a sink. |
34
+ | `compass_watch` | Keep the index fresh automatically (start/stop a debounced incremental re-index on file change). |
35
+
36
+ If the graph is missing (no `.speclaw/index.db`), run `compass_index` first;
37
+ until then, fall back to Grep/Read.
38
+
39
+ ## Project-specific starting points
40
+
41
+ <!-- Filled in during speclaw init: the project's real entrypoints, core
42
+ modules, and the traces agents need most often. -->
43
+ {{compass_hints}}