@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.
- package/ATTRIBUTION.md +34 -0
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/dist/cli/commands/agent.js +42 -0
- package/dist/cli/commands/doctor.js +21 -0
- package/dist/cli/commands/index-build.js +24 -0
- package/dist/cli/commands/init.js +121 -0
- package/dist/cli/commands/query.js +69 -0
- package/dist/cli/commands/spec.js +71 -0
- package/dist/cli/index.js +77 -0
- package/dist/cli/lib/args.js +44 -0
- package/dist/cli/lib/ui.js +89 -0
- package/dist/modules/compass/db.js +84 -0
- package/dist/modules/compass/embedder.js +87 -0
- package/dist/modules/compass/extract.js +91 -0
- package/dist/modules/compass/indexer.js +158 -0
- package/dist/modules/compass/languages.js +67 -0
- package/dist/modules/compass/parser.js +37 -0
- package/dist/modules/compass/query.js +260 -0
- package/dist/modules/compass/register.js +72 -0
- package/dist/modules/compass/watcher.js +95 -0
- package/dist/modules/foundation/assets/AGENTS.template.md +61 -0
- package/dist/modules/foundation/assets/CLAUDE.template.md +72 -0
- package/dist/modules/foundation/assets/LAWS.template.md +39 -0
- package/dist/modules/foundation/assets/docs/compass.template.md +43 -0
- package/dist/modules/foundation/assets/docs/standards/architecture.template.md +36 -0
- package/dist/modules/foundation/assets/docs/standards/backend-standards.template.md +50 -0
- package/dist/modules/foundation/assets/docs/standards/base-standards.template.md +47 -0
- package/dist/modules/foundation/assets/docs/standards/conventions.template.md +31 -0
- package/dist/modules/foundation/assets/docs/standards/documentation.template.md +50 -0
- package/dist/modules/foundation/assets/docs/standards/frontend-standards.template.md +46 -0
- package/dist/modules/foundation/assets/docs/standards/spec-workflow.template.md +46 -0
- package/dist/modules/foundation/assets/docs/standards/testing-standards.template.md +34 -0
- package/dist/modules/foundation/doctor.js +107 -0
- package/dist/modules/foundation/register.js +86 -0
- package/dist/modules/foundation/scaffold.js +103 -0
- package/dist/modules/spec/assets/commands/archive.md +10 -0
- package/dist/modules/spec/assets/commands/build.md +11 -0
- package/dist/modules/spec/assets/commands/draft.md +12 -0
- package/dist/modules/spec/assets/commands/explore.md +10 -0
- package/dist/modules/spec/assets/commands/sync.md +9 -0
- package/dist/modules/spec/assets/rules/spec-tasks-mandatory-steps.md +37 -0
- package/dist/modules/spec/assets/skills/archive/SKILL.md +22 -0
- package/dist/modules/spec/assets/skills/build/SKILL.md +49 -0
- package/dist/modules/spec/assets/skills/draft/SKILL.md +64 -0
- package/dist/modules/spec/assets/skills/explore/SKILL.md +28 -0
- package/dist/modules/spec/assets/skills/sync/SKILL.md +21 -0
- package/dist/modules/spec/engine.js +227 -0
- package/dist/modules/spec/register.js +53 -0
- package/dist/modules/tools/assets/packs/agents/backend-developer.md +61 -0
- package/dist/modules/tools/assets/packs/agents/frontend-developer.md +62 -0
- package/dist/modules/tools/assets/packs/agents/product-strategy-analyst.md +56 -0
- package/dist/modules/tools/assets/packs.json +6 -0
- package/dist/modules/tools/packs.js +44 -0
- package/dist/modules/tools/register.js +27 -0
- package/dist/server.js +21 -0
- package/dist/shared/agents.js +94 -0
- package/dist/shared/install.js +66 -0
- package/dist/shared/mcp.js +16 -0
- package/dist/shared/paths.js +10 -0
- package/dist/shared/render.js +21 -0
- 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}}
|