@esneiderbravo/speclaw 2.0.5 → 2.0.7
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 +18 -15
- package/dist/cli/commands/index-build.js +18 -0
- package/dist/cli/commands/session-start.js +29 -0
- package/dist/cli/index.js +7 -1
- package/dist/cli/lib/ui.js +17 -13
- package/dist/modules/compass/db.js +2 -0
- package/dist/modules/compass/hash.js +3 -3
- package/dist/modules/compass/indexer.js +55 -31
- package/dist/modules/compass/map.js +24 -0
- package/dist/modules/compass/query.js +18 -3
- package/dist/modules/compass/visualize.js +15 -12
- package/dist/modules/foundation/assets/AGENTS.template.md +2 -1
- package/dist/modules/foundation/assets/CLAUDE.template.md +2 -1
- package/dist/modules/foundation/assets/docs/compass.template.md +6 -0
- package/dist/modules/foundation/hooks.js +61 -6
- package/dist/modules/lawbook/assets/agents/explorer.md +3 -2
- package/dist/modules/lawbook/assets/skills/build/steps/03-implement.md +2 -1
- package/dist/modules/lawbook/assets/skills/draft/steps/02-understand.md +5 -3
- package/dist/modules/lawbook/assets/skills/explore/steps/01-investigate.md +3 -1
- package/dist/modules/lawbook/assets/skills/investigate/steps/01-investigate.md +3 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
<br/>
|
|
6
6
|
|
|
7
|
-
<a href="https://www.npmjs.com/package/@esneiderbravo/speclaw"><img src="https://img.shields.io/npm/v/@esneiderbravo/speclaw?color=
|
|
8
|
-
<a href="https://github.com/esneiderbravo/speclaw/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/esneiderbravo/speclaw/ci.yml?branch=main&label=CI&labelColor=
|
|
9
|
-
<a href="https://www.npmjs.com/package/@esneiderbravo/speclaw?activeTab=versions"><img src="https://img.shields.io/badge/provenance-SLSA-
|
|
10
|
-
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-
|
|
11
|
-
<img src="https://img.shields.io/badge/node-%E2%89%A522.16-
|
|
12
|
-
<img src="https://img.shields.io/badge/v2.0-
|
|
7
|
+
<a href="https://www.npmjs.com/package/@esneiderbravo/speclaw"><img src="https://img.shields.io/npm/v/@esneiderbravo/speclaw?color=00707f&labelColor=131313&style=flat-square&label=npm" alt="npm"></a>
|
|
8
|
+
<a href="https://github.com/esneiderbravo/speclaw/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/esneiderbravo/speclaw/ci.yml?branch=main&label=CI&labelColor=131313&style=flat-square&color=00707f" alt="CI"></a>
|
|
9
|
+
<a href="https://www.npmjs.com/package/@esneiderbravo/speclaw?activeTab=versions"><img src="https://img.shields.io/badge/provenance-SLSA-00707f?labelColor=131313&style=flat-square" alt="npm provenance"></a>
|
|
10
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-00707f?labelColor=131313&style=flat-square" alt="MIT"></a>
|
|
11
|
+
<img src="https://img.shields.io/badge/node-%E2%89%A522.16-00707f?labelColor=131313&style=flat-square" alt="Node >= 22.16">
|
|
12
|
+
<img src="https://img.shields.io/badge/v2.0-00707f?labelColor=131313&style=flat-square" alt="v2.0">
|
|
13
13
|
|
|
14
14
|
<br/><br/>
|
|
15
15
|
|
|
@@ -24,12 +24,12 @@ those laws hold in the editor, in CI, and in the PR.
|
|
|
24
24
|
One command. No cloud, no LLM, no API keys — <b>everything runs on your machine.</b>
|
|
25
25
|
</p>
|
|
26
26
|
|
|
27
|
-
<img src="https://img.shields.io/badge/100%25_local-
|
|
28
|
-
<img src="https://img.shields.io/badge/no_LLM_·_no_cloud-
|
|
29
|
-
<img src="https://img.shields.io/badge/CLI_+_MCP-
|
|
30
|
-
<img src="https://img.shields.io/badge/9_canonical_tools-
|
|
31
|
-
<img src="https://img.shields.io/badge/Cortex-One_brain._Many_agents.-
|
|
32
|
-
<img src="https://img.shields.io/badge/any_agent-
|
|
27
|
+
<img src="https://img.shields.io/badge/100%25_local-00707f?labelColor=131313&style=flat-square" alt="100% local">
|
|
28
|
+
<img src="https://img.shields.io/badge/no_LLM_·_no_cloud-00707f?labelColor=131313&style=flat-square" alt="no LLM">
|
|
29
|
+
<img src="https://img.shields.io/badge/CLI_+_MCP-00707f?labelColor=131313&style=flat-square" alt="CLI + MCP">
|
|
30
|
+
<img src="https://img.shields.io/badge/9_canonical_tools-00707f?labelColor=131313&style=flat-square" alt="9 tools">
|
|
31
|
+
<img src="https://img.shields.io/badge/Cortex-One_brain._Many_agents.-00707f?labelColor=131313&style=flat-square" alt="Cortex">
|
|
32
|
+
<img src="https://img.shields.io/badge/any_agent-00707f?labelColor=131313&style=flat-square" alt="any agent">
|
|
33
33
|
|
|
34
34
|
</div>
|
|
35
35
|
|
|
@@ -107,7 +107,7 @@ gh attestation verify <tarball> --owner esneiderbravo
|
|
|
107
107
|
<img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/terminal-init.png" width="800" alt="speclaw init — terminal output">
|
|
108
108
|
</p>
|
|
109
109
|
|
|
110
|
-
<p align="center"><i>
|
|
110
|
+
<p align="center"><i>Cyan steps, green checks, a live progress bar — themed with the speclaw palette.</i></p>
|
|
111
111
|
|
|
112
112
|
<br/>
|
|
113
113
|
|
|
@@ -116,7 +116,7 @@ gh attestation verify <tarball> --owner esneiderbravo
|
|
|
116
116
|
| Module | What it does |
|
|
117
117
|
| :-- | :-- |
|
|
118
118
|
| **Foundation** | The project's constitution: `LAWS.md` binding granular standards under `docs/standards/`, plus `CLAUDE.md` / `AGENTS.md`. **Enforced** via agent hooks (`speclaw check`), deterministic graph laws (`speclaw verify` / `laws verify`), multidialect compile (`laws compile`), and committed **`speclaw.lock`** digests + injection scan (`laws lock` / `accept` / `scan`). |
|
|
119
|
-
| **Compass** | Local code graph (tree-sitter → `node:sqlite`): hybrid find (FTS5 + vectors + RRF + PageRank), impact, affected-tests, hotspots, coupling, visualize. Schema **10**. No LLM — lives in `.speclaw/` (gitignored). |
|
|
119
|
+
| **Compass** | Local code graph (tree-sitter → `node:sqlite`): hybrid find (FTS5 + vectors + RRF + PageRank), impact, affected-tests, hotspots, coupling, visualize. Schema **10**. Refreshed silently at every Claude Code session start once built. No LLM — lives in `.speclaw/` (gitignored). |
|
|
120
120
|
| **Lawbook** | Spec-driven workflow with **adaptive ceremony** (levels 0–3), EARS linting, requirement **coverage**, sealed **drift** anchors, bugfix + `lawbook_investigate`. Artifacts under `lawbook/`. |
|
|
121
121
|
| **Cortex** | **One brain. Many agents.** The multi-agent loop: durable `harness.json`, MCP tool `cortex` (`status`/`start`/`advance`/`rework`/`brief`), CLI `speclaw cortex`, skill `/lawbook/cortex`. Dispatches explorer → planner → implementer → reviewer → tester → archiver. |
|
|
122
122
|
| **Team** | Declare `team.owners` in `lawbook/config.yaml`; `speclaw owners --write` compiles a managed trailing block in `.github/CODEOWNERS` (GitHub: last match wins). Doctor checks the posture. CLI-only — no MCP tool. |
|
|
@@ -247,7 +247,10 @@ regenerate `ai-specs/` locally. Optional **`team.owners`** in
|
|
|
247
247
|
|
|
248
248
|
**Enforcement artifacts.** For agents that support hooks, speclaw merges its law
|
|
249
249
|
hooks into that agent's settings **by identity** — it never touches hooks you
|
|
250
|
-
added yourself. The
|
|
250
|
+
added yourself. The same merge adds one `SessionStart` command hook that
|
|
251
|
+
silently refreshes an existing Compass index when a session starts
|
|
252
|
+
(`speclaw session-start`; it skips when no index exists, never downloads or
|
|
253
|
+
contacts the registry, and always exits 0). The compiled law manifest lives in `.speclaw/laws-manifest.json`
|
|
251
254
|
(gitignored) and is **adapted to the target tree** on `init`/`update` — speclaw's
|
|
252
255
|
own architecture laws are seeded only when those paths exist, and the cycle law
|
|
253
256
|
follows `apps/*/src`, `packages/*/src`, or `src/` rather than copying
|
|
@@ -2,9 +2,27 @@ import { buildIndex } from "../../modules/compass/indexer.js";
|
|
|
2
2
|
import { startWatch } from "../../modules/compass/watcher.js";
|
|
3
3
|
import { recordCompassCall } from "../../shared/compass-calls.js";
|
|
4
4
|
import { ui, renderProgress, clearProgress } from "../lib/ui.js";
|
|
5
|
+
/** Usage text for `speclaw index --help`. */
|
|
6
|
+
export const INDEX_HELP = `Usage: speclaw index [options]
|
|
7
|
+
|
|
8
|
+
(Re)build the local Compass code graph for the current directory. Runs are
|
|
9
|
+
incremental: unchanged files are skipped, and an unchanged project skips the
|
|
10
|
+
global post-processing.
|
|
11
|
+
|
|
12
|
+
Options
|
|
13
|
+
--force Re-extract every file
|
|
14
|
+
--prune Also evict embedding-cache rows unused for --retention days
|
|
15
|
+
--retention <days> Retention window for --prune (default 30)
|
|
16
|
+
--max-cache-mb <mb> Embedding-cache size cap (default 256)
|
|
17
|
+
--json Print the index statistics as JSON
|
|
18
|
+
`;
|
|
5
19
|
/** (Re)build the Compass code graph for the cwd, showing progress and final stats. */
|
|
6
20
|
export async function runIndex(flags) {
|
|
7
21
|
const cwd = process.cwd();
|
|
22
|
+
if (flags.help || flags.h) {
|
|
23
|
+
process.stdout.write(INDEX_HELP);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
8
26
|
recordCompassCall(cwd, "compass_index");
|
|
9
27
|
const json = Boolean(flags.json);
|
|
10
28
|
if (!json)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* `speclaw session-start`: refresh an existing Compass index incrementally,
|
|
5
|
+
* print nothing, and never fail. It is a top-level command rather than an
|
|
6
|
+
* `index` flag so an older speclaw that resolves first rejects it as unknown
|
|
7
|
+
* before indexing anything. With no `.speclaw/index.db` it returns at once,
|
|
8
|
+
* before `node:sqlite` is even loaded — the first build stays an explicit
|
|
9
|
+
* `speclaw index` / `compass_index`. It is not logged as a Compass call: the
|
|
10
|
+
* hook would otherwise append an entry per session and rotate real evidence out.
|
|
11
|
+
*
|
|
12
|
+
* @param cwd - Project root (defaults to the process working directory).
|
|
13
|
+
*/
|
|
14
|
+
// Covers: req~session-start-index~1
|
|
15
|
+
export async function runSessionStart(cwd = process.cwd()) {
|
|
16
|
+
try {
|
|
17
|
+
if (!fs.existsSync(path.join(cwd, ".speclaw", "index.db")))
|
|
18
|
+
return;
|
|
19
|
+
// Node lines that still flag `node:sqlite` as experimental would print an
|
|
20
|
+
// ExperimentalWarning to stderr; this command must print nothing.
|
|
21
|
+
process.removeAllListeners("warning");
|
|
22
|
+
const { buildIndex } = await import("../../modules/compass/indexer.js");
|
|
23
|
+
await buildIndex(cwd, {});
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// A concurrent writer (SQLITE_BUSY after busy_timeout) or any other failure
|
|
27
|
+
// must never fail or print into a starting session.
|
|
28
|
+
}
|
|
29
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -19,6 +19,7 @@ Setup
|
|
|
19
19
|
|
|
20
20
|
Compass (code intelligence — the same surface agents use via MCP)
|
|
21
21
|
index (Re)build the local code graph (--force / --prune / --json)
|
|
22
|
+
session-start Silent, fail-safe refresh of an existing index (SessionStart hook)
|
|
22
23
|
watch Keep the index fresh on file changes
|
|
23
24
|
explore <node> A node's source + callers/callees
|
|
24
25
|
search <query> Hybrid find (BM25+vector+name); --focus --max-tokens --explain
|
|
@@ -99,7 +100,7 @@ const HEADER_COMMANDS = new Set([
|
|
|
99
100
|
* forced-color signal counts as interactive so the header is exercisable in a
|
|
100
101
|
* child process. `budget --json`, `doctor --json`, and `coverage` when emitting
|
|
101
102
|
* TAP/JSON (or when stdout is not a TTY) are machine-consumed and suppress the
|
|
102
|
-
* header.
|
|
103
|
+
* header. `session-start` is not header-eligible: it must print nothing.
|
|
103
104
|
*/
|
|
104
105
|
function maybeHeader(cmd, flags) {
|
|
105
106
|
if (!process.stdout.isTTY && process.env.FORCE_COLOR !== "1")
|
|
@@ -156,6 +157,8 @@ async function dispatch(cmd, flags) {
|
|
|
156
157
|
return (await import("./commands/index-build.js")).runIndex(flags);
|
|
157
158
|
case "watch":
|
|
158
159
|
return (await import("./commands/index-build.js")).runWatch(flags);
|
|
160
|
+
case "session-start":
|
|
161
|
+
return (await import("./commands/session-start.js")).runSessionStart();
|
|
159
162
|
case "explore":
|
|
160
163
|
case "search":
|
|
161
164
|
case "recall":
|
|
@@ -204,6 +207,9 @@ async function main() {
|
|
|
204
207
|
const flags = parseFlags(rest);
|
|
205
208
|
maybeHeader(cmd, flags);
|
|
206
209
|
await dispatch(cmd, flags);
|
|
210
|
+
// The SessionStart hook's refresh must stay silent: no update notice either.
|
|
211
|
+
if (cmd === "session-start")
|
|
212
|
+
return;
|
|
207
213
|
await maybeNotifyUpdate(cmd);
|
|
208
214
|
}
|
|
209
215
|
main().catch((err) => {
|
package/dist/cli/lib/ui.js
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
// Brand-themed terminal UI. Colors come from the speclaw
|
|
2
|
-
// = the "law"
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Brand-themed terminal UI. Colors come from the speclaw site's ink theme
|
|
2
|
+
// (`app/tokens.css` in speclaw-site): signal cyan #00E3FD = the "law" accent,
|
|
3
|
+
// ink #F4F4F3 primary text, ink-faint #999EA3 secondary text, deny #FF5C47 for
|
|
4
|
+
// errors, and green/amber re-tuned for success/warning. Every color holds at
|
|
5
|
+
// least 4.5:1 (WCAG 2.x) against the ink paper #131313. They render as 24-bit
|
|
6
|
+
// truecolor ANSI — no dependency needed — and auto-disable when the output is
|
|
7
|
+
// not a TTY or NO_COLOR is set.
|
|
5
8
|
import { pkgVersion } from "../../shared/version.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
// Covers: req~brand-terminal-palette~1
|
|
10
|
+
export const PALETTE = Object.freeze({
|
|
11
|
+
cyan: [0, 227, 253], // #00E3FD — signal: the accent / "law"
|
|
12
|
+
cyanDim: [0, 227, 253], // #00E3FD — signal (the site has no mid cyan for text)
|
|
13
|
+
cream: [244, 244, 243], // #F4F4F3 — ink: primary text
|
|
14
|
+
muted: [153, 158, 163], // #999EA3 — ink-faint: secondary text
|
|
15
|
+
green: [62, 207, 122], // #3ECF7A — success (re-tuned)
|
|
16
|
+
amber: [245, 183, 61], // #F5B73D — warning (re-tuned)
|
|
17
|
+
red: [255, 92, 71], // #FF5C47 — deny: errors
|
|
18
|
+
});
|
|
15
19
|
const colorOn = (Boolean(process.stdout.isTTY) || process.env.FORCE_COLOR === "1") && !process.env.NO_COLOR;
|
|
16
20
|
// Whether the terminal reliably renders the unicode box/block glyphs the brand
|
|
17
21
|
// output uses. Non-Windows terminals are assumed capable; a Windows console is
|
|
@@ -19,6 +19,8 @@ CREATE TABLE IF NOT EXISTS files (
|
|
|
19
19
|
);
|
|
20
20
|
CREATE INDEX IF NOT EXISTS idx_files_is_test ON files(is_test);
|
|
21
21
|
-- nodes: the definitions in the codebase (functions, classes, methods, types).
|
|
22
|
+
-- start_byte/end_byte are UTF-16 code-unit offsets into the decoded source
|
|
23
|
+
-- (tree-sitter's startIndex/endIndex), not UTF-8 byte offsets, despite the names.
|
|
22
24
|
CREATE TABLE IF NOT EXISTS nodes (
|
|
23
25
|
id INTEGER PRIMARY KEY,
|
|
24
26
|
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
|
@@ -36,9 +36,9 @@ function digest(parts) {
|
|
|
36
36
|
/**
|
|
37
37
|
* Hash of the exact source bytes for a symbol range (detects cosmetic edits).
|
|
38
38
|
*
|
|
39
|
-
* @param source - Full file source
|
|
40
|
-
* @param startByte - Inclusive start offset.
|
|
41
|
-
* @param endByte - Exclusive end offset.
|
|
39
|
+
* @param source - Full file source, decoded from UTF-8.
|
|
40
|
+
* @param startByte - Inclusive start offset: a UTF-16 code-unit index into `source`, despite the name.
|
|
41
|
+
* @param endByte - Exclusive end offset: a UTF-16 code-unit index into `source`, despite the name.
|
|
42
42
|
*/
|
|
43
43
|
export function rawHash(source, startByte, endByte) {
|
|
44
44
|
return createHash("sha256").update(source.slice(startByte, endByte)).digest("hex").slice(0, 32);
|
|
@@ -186,6 +186,13 @@ function* walkFiles(root) {
|
|
|
186
186
|
* and an embedding cache keyed by embedder-input hash so renames/moves do not
|
|
187
187
|
* recompute vectors. The whole run executes in a single transaction.
|
|
188
188
|
*
|
|
189
|
+
* A run without force, prune, or an explicit `maxCacheMB` that re-extracts and
|
|
190
|
+
* removes nothing and finds the root hash unchanged is a no-op: it skips the
|
|
191
|
+
* dir-hash rewrite, edge/import resolution, PageRank, and the embedding-cache
|
|
192
|
+
* touch and eviction, writes only `meta.indexed_at`, and rewrites
|
|
193
|
+
* `docs/compass.md` only when its map block is empty. It still returns full
|
|
194
|
+
* statistics (`totals`, `nextStep`).
|
|
195
|
+
*
|
|
189
196
|
* @param projectPath - Absolute path to the project root.
|
|
190
197
|
* @param onProgressOrOpts - Progress callback (legacy) or {@link BuildIndexOptions}.
|
|
191
198
|
*/
|
|
@@ -251,6 +258,8 @@ export async function buildIndex(projectPath, onProgressOrOpts) {
|
|
|
251
258
|
signature = excluded.signature,
|
|
252
259
|
doc = excluded.doc`);
|
|
253
260
|
const allFiles = [...walkFiles(projectPath)];
|
|
261
|
+
// Set inside the transaction; read after it to decide whether the map is written.
|
|
262
|
+
let noop;
|
|
254
263
|
db.exec("BEGIN");
|
|
255
264
|
try {
|
|
256
265
|
let done = 0;
|
|
@@ -369,37 +378,49 @@ export async function buildIndex(projectPath, onProgressOrOpts) {
|
|
|
369
378
|
const dirMap = buildDirHashMap(fileHashes);
|
|
370
379
|
const rootHash = dirMap.get("") ?? "";
|
|
371
380
|
stats.rootUnchanged = Boolean(prevRoot && prevRoot.hash === rootHash && !force && stats.files === 0);
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
db
|
|
397
|
-
|
|
398
|
-
|
|
381
|
+
// Covers: req~index-noop-fast-path~1
|
|
382
|
+
// No-op fast path: nothing re-extracted or removed and the same root means
|
|
383
|
+
// dir hashes, edges, PageRank, and the cache are already what a full pass
|
|
384
|
+
// would write — skip them so a run on every session start stays cheap.
|
|
385
|
+
// `rootUnchanged` already implies no force (explicit or needs_reindex); an
|
|
386
|
+
// explicit prune or cache cap is a maintenance request and runs in full.
|
|
387
|
+
noop = stats.rootUnchanged && !prune && opts.maxCacheMB === undefined && stats.removed === 0;
|
|
388
|
+
if (!noop) {
|
|
389
|
+
const now = Date.now();
|
|
390
|
+
db.prepare("DELETE FROM dir_hashes").run();
|
|
391
|
+
const insDir = db.prepare("INSERT INTO dir_hashes(path, hash, n_files, updated_at) VALUES (?, ?, ?, ?)");
|
|
392
|
+
for (const [dir, hash] of dirMap) {
|
|
393
|
+
const nFiles = [...fileHashes.keys()].filter((f) => dir === "" ? true : f === dir || f.startsWith(dir + "/")).length;
|
|
394
|
+
insDir.run(dir, hash, nFiles, now);
|
|
395
|
+
}
|
|
396
|
+
db.exec(`
|
|
397
|
+
UPDATE edges SET dst_node_id = (
|
|
398
|
+
SELECT n.id FROM nodes n
|
|
399
|
+
WHERE n.name = edges.dst_name
|
|
400
|
+
ORDER BY CASE WHEN n.file_id = edges.src_file_id THEN 0 ELSE 1 END, n.id
|
|
401
|
+
LIMIT 1
|
|
402
|
+
)
|
|
403
|
+
WHERE kind = 'call' AND dst_node_id IS NULL
|
|
404
|
+
`);
|
|
405
|
+
resolveImportEdges(db, projectPath);
|
|
406
|
+
recomputeGlobalPagerank(db);
|
|
407
|
+
// Touch last_seen for all live content hashes under active model
|
|
408
|
+
db.prepare(`UPDATE embedding_cache SET last_seen_at = ?
|
|
409
|
+
WHERE model = ?
|
|
410
|
+
AND content_hash IN (SELECT content_hash FROM nodes WHERE content_hash IS NOT NULL)`).run(now, embedder.id);
|
|
411
|
+
if (prune) {
|
|
412
|
+
const cutoff = now - retentionDays * 24 * 60 * 60 * 1000;
|
|
413
|
+
db.prepare(`DELETE FROM embedding_cache
|
|
414
|
+
WHERE last_seen_at < ?
|
|
415
|
+
AND content_hash NOT IN (SELECT content_hash FROM nodes WHERE content_hash IS NOT NULL)`).run(cutoff);
|
|
416
|
+
}
|
|
417
|
+
evictCacheBySize(db, maxCacheMB);
|
|
399
418
|
}
|
|
400
|
-
|
|
419
|
+
// Written on every run, no-op included, so doctor's freshness check sees a
|
|
420
|
+
// verified-current index as fresh.
|
|
401
421
|
db.prepare("INSERT INTO meta(key, value) VALUES ('indexed_at', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(new Date().toISOString());
|
|
402
|
-
|
|
422
|
+
if (!noop)
|
|
423
|
+
clearNeedsReindex(db);
|
|
403
424
|
stats.totals = countTotals(db);
|
|
404
425
|
stats.nextStep = indexNextStep(stats.totals);
|
|
405
426
|
db.exec("COMMIT");
|
|
@@ -412,8 +433,11 @@ export async function buildIndex(projectPath, onProgressOrOpts) {
|
|
|
412
433
|
db.close();
|
|
413
434
|
}
|
|
414
435
|
try {
|
|
415
|
-
const { writeCompactMap } = await import("./map.js");
|
|
416
|
-
|
|
436
|
+
const { writeCompactMap, compactMapPending } = await import("./map.js");
|
|
437
|
+
// A no-op run leaves the tracked docs/compass.md alone (no working-tree
|
|
438
|
+
// churn) unless its map block is still empty.
|
|
439
|
+
if (!noop || compactMapPending(projectPath))
|
|
440
|
+
writeCompactMap(projectPath);
|
|
417
441
|
}
|
|
418
442
|
catch {
|
|
419
443
|
// Map generation must never fail an index run.
|
|
@@ -82,6 +82,30 @@ export function generateCompactMap(projectPath) {
|
|
|
82
82
|
db.close();
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* True when `docs/compass.md` holds both map markers with nothing but
|
|
87
|
+
* whitespace between them — a freshly scaffolded or reset map block that a
|
|
88
|
+
* no-op index run must still fill. False when the file or a marker is missing
|
|
89
|
+
* (there is nothing a map write could fill) or the block already has a map.
|
|
90
|
+
*
|
|
91
|
+
* @param projectPath - Project root.
|
|
92
|
+
* @returns Whether the compact map block is present but empty.
|
|
93
|
+
*/
|
|
94
|
+
export function compactMapPending(projectPath) {
|
|
95
|
+
const compassPath = path.join(projectPath, "docs", "compass.md");
|
|
96
|
+
let text;
|
|
97
|
+
try {
|
|
98
|
+
text = fs.readFileSync(compassPath, "utf8");
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
const start = text.indexOf(MAP_START);
|
|
104
|
+
const end = text.indexOf(MAP_END);
|
|
105
|
+
if (start < 0 || end < 0 || end < start)
|
|
106
|
+
return false;
|
|
107
|
+
return text.slice(start + MAP_START.length, end).trim() === "";
|
|
108
|
+
}
|
|
85
109
|
/**
|
|
86
110
|
* Replace content between map markers in `docs/compass.md`. Preserves content
|
|
87
111
|
* outside markers. Does nothing (with reason) when markers are missing or the
|
|
@@ -43,10 +43,25 @@ export function search(projectPath, query, limit = 25) {
|
|
|
43
43
|
db.close();
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Read a symbol's source text from disk.
|
|
48
|
+
*
|
|
49
|
+
* @remarks The offsets are UTF-16 code-unit indices into the decoded file text
|
|
50
|
+
* (tree-sitter parses the `utf8`-decoded string), not byte offsets — so the file
|
|
51
|
+
* is decoded before slicing. A byte slice drifts on any preceding non-ASCII text.
|
|
52
|
+
*
|
|
53
|
+
* @param projectPath - Absolute project root.
|
|
54
|
+
* @param file - Repo-relative file path.
|
|
55
|
+
* @param startIndex - Inclusive start offset (`nodes.start_byte`).
|
|
56
|
+
* @param endIndex - Exclusive end offset (`nodes.end_byte`).
|
|
57
|
+
* @returns The symbol's source, or `""` when the file cannot be read.
|
|
58
|
+
*
|
|
59
|
+
* // Covers: req~explore-exact-source~1
|
|
60
|
+
*/
|
|
61
|
+
function readSource(projectPath, file, startIndex, endIndex) {
|
|
47
62
|
try {
|
|
48
|
-
const
|
|
49
|
-
return
|
|
63
|
+
const text = fs.readFileSync(path.join(projectPath, file), "utf8");
|
|
64
|
+
return text.slice(startIndex, endIndex);
|
|
50
65
|
}
|
|
51
66
|
catch {
|
|
52
67
|
return "";
|
|
@@ -88,11 +88,14 @@ export function visualize(projectPath, opts = {}) {
|
|
|
88
88
|
/**
|
|
89
89
|
* Render the graph as a self-contained, offline HTML page: the data is embedded
|
|
90
90
|
* as JSON and drawn by an inline canvas force-directed renderer (no CDN, no
|
|
91
|
-
* dependencies).
|
|
91
|
+
* dependencies). Colors are the site's ink tokens (`app/tokens.css` in
|
|
92
|
+
* speclaw-site): paper #131313, sheet #1f2022, rule #303236, ink #f4f4f3,
|
|
93
|
+
* ink-faint #999ea3, signal #00e3fd. No retired-palette color may return here.
|
|
92
94
|
*
|
|
93
95
|
* @param data - The nodes/links/totals to embed.
|
|
94
96
|
* @returns A complete HTML document as a string.
|
|
95
97
|
*/
|
|
98
|
+
// Covers: req~brand-viewer-palette~1
|
|
96
99
|
export function renderHtml(data) {
|
|
97
100
|
const payload = JSON.stringify(data);
|
|
98
101
|
return `<!doctype html>
|
|
@@ -100,20 +103,20 @@ export function renderHtml(data) {
|
|
|
100
103
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
101
104
|
<title>speclaw · Compass graph</title>
|
|
102
105
|
<style>
|
|
103
|
-
:root { --cy:#
|
|
106
|
+
:root { --cy:#00e3fd; --cr:#f4f4f3; --mu:#999ea3; --bg:#131313; --sh:#1f2022; --ru:#303236; }
|
|
104
107
|
* { box-sizing:border-box; }
|
|
105
108
|
html,body { margin:0; height:100%; background:var(--bg); color:var(--cr);
|
|
106
|
-
font-family:'
|
|
109
|
+
font-family:'Chivo Mono','SFMono-Regular',Menlo,monospace; overflow:hidden; }
|
|
107
110
|
canvas { display:block; cursor:grab; }
|
|
108
111
|
canvas:active { cursor:grabbing; }
|
|
109
112
|
.panel { position:fixed; top:16px; left:16px; padding:12px 14px; border-radius:10px;
|
|
110
|
-
background:rgba(
|
|
113
|
+
background:rgba(31,32,34,.82); border:1px solid var(--ru); font-size:12px; line-height:1.5; }
|
|
111
114
|
.panel b { color:var(--cr); } .panel .sub { color:var(--mu); }
|
|
112
115
|
.legend { position:fixed; bottom:16px; left:16px; font-size:11px; color:var(--mu);
|
|
113
|
-
background:rgba(
|
|
116
|
+
background:rgba(31,32,34,.82); border:1px solid var(--ru); border-radius:10px; padding:10px 12px; }
|
|
114
117
|
.legend span { display:inline-flex; align-items:center; margin-right:12px; }
|
|
115
118
|
.legend i { width:9px; height:9px; border-radius:50%; display:inline-block; margin-right:5px; }
|
|
116
|
-
.tip { position:fixed; padding:6px 9px; border-radius:7px; background
|
|
119
|
+
.tip { position:fixed; padding:6px 9px; border-radius:7px; background:var(--sh); border:1px solid var(--cy);
|
|
117
120
|
color:var(--cr); font-size:12px; pointer-events:none; opacity:0; transition:opacity .1s; white-space:nowrap; }
|
|
118
121
|
.brand { color:var(--cy); font-weight:700; }
|
|
119
122
|
</style></head><body>
|
|
@@ -127,9 +130,9 @@ export function renderHtml(data) {
|
|
|
127
130
|
<div class="tip" id="tip"></div>
|
|
128
131
|
<script>
|
|
129
132
|
const DATA = ${payload};
|
|
130
|
-
const KIND_COLORS = { function:'#
|
|
131
|
-
interface:'#
|
|
132
|
-
const colorOf = k => KIND_COLORS[k] || '#
|
|
133
|
+
const KIND_COLORS = { function:'#00e3fd', method:'#3ecf7a', class:'#f5b73d',
|
|
134
|
+
interface:'#babdc1', type:'#babdc1', enum:'#babdc1' };
|
|
135
|
+
const colorOf = k => KIND_COLORS[k] || '#7c8083';
|
|
133
136
|
|
|
134
137
|
const cv = document.getElementById('c'), ctx = cv.getContext('2d');
|
|
135
138
|
const tip = document.getElementById('tip');
|
|
@@ -179,7 +182,7 @@ function draw(){
|
|
|
179
182
|
ctx.lineWidth = 1;
|
|
180
183
|
for(const l of L){ const a=toScreen(N[l.s]), b=toScreen(N[l.t]);
|
|
181
184
|
const on = hover>=0 && (l.s===hover||l.t===hover);
|
|
182
|
-
ctx.strokeStyle = on ? 'rgba(
|
|
185
|
+
ctx.strokeStyle = on ? 'rgba(0,227,253,.7)' : 'rgba(124,128,131,.16)';
|
|
183
186
|
ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y); ctx.stroke();
|
|
184
187
|
}
|
|
185
188
|
for(let i=0;i<N.length;i++){ const n=N[i], p=toScreen(n);
|
|
@@ -187,7 +190,7 @@ function draw(){
|
|
|
187
190
|
ctx.globalAlpha = dim ? 0.28 : 1;
|
|
188
191
|
ctx.beginPath(); ctx.arc(p.x,p.y,n.r*Math.sqrt(scale),0,7); ctx.fillStyle=colorOf(n.kind); ctx.fill();
|
|
189
192
|
if((n.r>7 || i===hover || (hoverSet&&hoverSet.has(i))) && scale>0.5){
|
|
190
|
-
ctx.globalAlpha = dim?0.4:0.9; ctx.fillStyle='#
|
|
193
|
+
ctx.globalAlpha = dim?0.4:0.9; ctx.fillStyle='#f4f4f3'; ctx.font="11px 'Chivo Mono','SFMono-Regular',Menlo,monospace";
|
|
191
194
|
ctx.fillText(n.name, p.x+n.r+3, p.y+3);
|
|
192
195
|
}
|
|
193
196
|
}
|
|
@@ -208,7 +211,7 @@ addEventListener('mousemove', e=>{
|
|
|
208
211
|
lastX=e.clientX; lastY=e.clientY;
|
|
209
212
|
const i=pick(e.clientX,e.clientY); hover=i;
|
|
210
213
|
if(i>=0){ const n=N[i]; tip.style.opacity=1; tip.style.left=(e.clientX+12)+'px'; tip.style.top=(e.clientY+12)+'px';
|
|
211
|
-
tip.innerHTML='<b>'+n.name+'</b> <span style="color:#
|
|
214
|
+
tip.innerHTML='<b>'+n.name+'</b> <span style="color:#999ea3">'+n.kind+' · '+n.file+':'+n.line+'</span>'; }
|
|
212
215
|
else tip.style.opacity=0;
|
|
213
216
|
});
|
|
214
217
|
addEventListener('mouseup', ()=>{ dragging=null; panning=false; });
|
|
@@ -20,7 +20,8 @@ Claude-specific notes: [`CLAUDE.md`](CLAUDE.md). The law: [`LAWS.md`](LAWS.md).
|
|
|
20
20
|
`compass_explore` **before** any grep/sed/cat/Read,
|
|
21
21
|
including files you already know by name. Fall back to manual file tools only
|
|
22
22
|
after Compass returns nothing useful, the graph is missing (`compass_index`
|
|
23
|
-
first
|
|
23
|
+
first; Claude Code sessions refresh an existing index at start), or the
|
|
24
|
+
target isn't indexed code (stylesheets, config, logs). Cheat
|
|
24
25
|
sheet: [`docs/compass.md`](docs/compass.md).
|
|
25
26
|
3. **Follow Cortex** (*One brain. Many agents.*) for every non-trivial change;
|
|
26
27
|
archive within the same PR. The primary agent is the **coordinator**
|
|
@@ -35,7 +35,8 @@ where it lives, how a value flows — call Compass **first**: `compass_find` to
|
|
|
35
35
|
locate, `compass_explore` to read a symbol with its callers and callees (include
|
|
36
36
|
`blast_radius` for what could break, `to` for a call path), and
|
|
37
37
|
`compass_diff_context` for the context of a diff.
|
|
38
|
-
Run `compass_index` first if the graph is missing.
|
|
38
|
+
Run `compass_index` first if the graph is missing. Under Claude Code a
|
|
39
|
+
`SessionStart` hook refreshes an existing index when each session starts.
|
|
39
40
|
|
|
40
41
|
This includes files you already know the name of: to learn what `Foo` imports,
|
|
41
42
|
uses, or depends on, run `compass_explore Foo` — do **not** `cat`/`sed`/`grep`/
|
|
@@ -37,6 +37,12 @@ a request, not whole files.
|
|
|
37
37
|
`compass_index` also takes `action: start|stop|status` to keep the index fresh
|
|
38
38
|
automatically (a debounced incremental re-index on file change).
|
|
39
39
|
|
|
40
|
+
Under Claude Code, a `SessionStart` hook refreshes an existing index when each
|
|
41
|
+
session starts (`speclaw session-start`: silent, never fails, skips
|
|
42
|
+
when `.speclaw/index.db` is absent, and leaves this file alone when nothing
|
|
43
|
+
changed). The first build is always an explicit `compass_index` /
|
|
44
|
+
`speclaw index`.
|
|
45
|
+
|
|
40
46
|
If the graph is missing (no `.speclaw/index.db`), run `compass_index` first —
|
|
41
47
|
a missing graph is not license to skip Compass. The only legitimate fallbacks
|
|
42
48
|
to Grep/Read: a Compass call returned nothing useful for your query, or the
|
|
@@ -3,6 +3,39 @@ import path from "node:path";
|
|
|
3
3
|
import { agentById } from "../../shared/agents.js";
|
|
4
4
|
import { sha256 } from "../../shared/install.js";
|
|
5
5
|
import { globError, hasBackend } from "./laws.js";
|
|
6
|
+
/**
|
|
7
|
+
* Substring every speclaw session-start command contains, in each of its
|
|
8
|
+
* resolution branches. It is the merge identity of the `SessionStart` hook.
|
|
9
|
+
*/
|
|
10
|
+
export const SESSION_START_MARKER = "speclaw session-start";
|
|
11
|
+
/**
|
|
12
|
+
* The marker of the unreleased `index --session-start` hook shape that
|
|
13
|
+
* pre-release 2.0.7 builds wrote. Still recognized so a merge replaces such an
|
|
14
|
+
* entry instead of keeping it next to the current one.
|
|
15
|
+
*/
|
|
16
|
+
const LEGACY_SESSION_START_MARKER = "speclaw index --session-start";
|
|
17
|
+
/**
|
|
18
|
+
* The POSIX `sh` command the `SessionStart` hook runs. The `index.db` guard runs
|
|
19
|
+
* in the shell, so a project without an index pays no Node start-up. It invokes
|
|
20
|
+
* the top-level `session-start` command, which a speclaw older than 2.0.7
|
|
21
|
+
* rejects as unknown (exit 1) before indexing, logging, or notifying, so a stale
|
|
22
|
+
* binary first in the resolution order does nothing. speclaw resolves as local
|
|
23
|
+
* `node_modules/.bin`, then `PATH`, then `npx --no-install` with
|
|
24
|
+
* `npm_config_offline=true` and `npm_config_update_notifier=false`, so npx
|
|
25
|
+
* neither installs nor contacts the registry (offline alone still lets npm's
|
|
26
|
+
* own update notifier query it).
|
|
27
|
+
* Output is discarded because `SessionStart` stdout enters the agent's context,
|
|
28
|
+
* and `|| true` keeps the exit code 0 in every case.
|
|
29
|
+
*/
|
|
30
|
+
export const SESSION_START_COMMAND = 'cd "${CLAUDE_PROJECT_DIR:-.}" 2>/dev/null && [ -f .speclaw/index.db ] && { ' +
|
|
31
|
+
"if [ -x node_modules/.bin/speclaw ]; then node_modules/.bin/speclaw session-start; " +
|
|
32
|
+
"elif command -v speclaw >/dev/null 2>&1; then speclaw session-start; " +
|
|
33
|
+
"else npm_config_update_notifier=false npm_config_offline=true npx --no-install @esneiderbravo/speclaw session-start; fi; " +
|
|
34
|
+
"} >/dev/null 2>&1 || true";
|
|
35
|
+
/** `SessionStart` matcher covering every session source Claude Code reports. */
|
|
36
|
+
const SESSION_START_MATCHER = "startup|resume|clear|compact";
|
|
37
|
+
/** Seconds the blocking session-start hook may run (index wait plus a real refresh). */
|
|
38
|
+
const SESSION_START_TIMEOUT = 30;
|
|
6
39
|
/** Claude Code `${path}` templates — see https://code.claude.com/docs/en/hooks */
|
|
7
40
|
const SPECLAW_HOOK_INPUT = {
|
|
8
41
|
projectPath: "${cwd}",
|
|
@@ -36,17 +69,32 @@ const MUTATION_MATCHER = "Write|Edit|MultiEdit|NotebookEdit";
|
|
|
36
69
|
* and the event cannot gate the tool; a `PreToolUse` "allow" would auto-approve.
|
|
37
70
|
*/
|
|
38
71
|
export const NUDGE_MATCHER = "Read|Grep|Glob";
|
|
39
|
-
/**
|
|
40
|
-
|
|
72
|
+
/**
|
|
73
|
+
* True when a hook object is one speclaw owns (safe to replace on merge): an
|
|
74
|
+
* `mcp_tool` hook on the `speclaw` server, or a `command` hook whose command
|
|
75
|
+
* contains {@link SESSION_START_MARKER} (or the pre-release
|
|
76
|
+
* `index --session-start` marker). A user `command` hook without the
|
|
77
|
+
* marker is never speclaw's.
|
|
78
|
+
*
|
|
79
|
+
* @param h - A hook object read from an agent's settings (any shape).
|
|
80
|
+
* @returns Whether speclaw owns the hook.
|
|
81
|
+
*/
|
|
82
|
+
export function isSpeclawHook(h) {
|
|
41
83
|
const o = h;
|
|
42
|
-
|
|
84
|
+
if (o?.type === "mcp_tool" && o?.server === "speclaw")
|
|
85
|
+
return true;
|
|
86
|
+
return (o?.type === "command" &&
|
|
87
|
+
typeof o?.command === "string" &&
|
|
88
|
+
(o.command.includes(SESSION_START_MARKER) || o.command.includes(LEGACY_SESSION_START_MARKER)));
|
|
43
89
|
}
|
|
44
90
|
/**
|
|
45
91
|
* Compile a law manifest into the hook groups speclaw contributes, one per event
|
|
46
92
|
* the laws demand: `PreToolUse` when any `bloqueo` law exists, `PostToolUse` for
|
|
47
93
|
* `feedback`, `Stop` for `gate`, and `InstructionsLoaded` whenever any law exists
|
|
48
|
-
* (the context-coverage audit).
|
|
49
|
-
*
|
|
94
|
+
* (the context-coverage audit). Two groups are always emitted, even with no
|
|
95
|
+
* laws: one `PostToolUse` group matching `Read|Grep|Glob` for the Compass-first
|
|
96
|
+
* nudge, and one `SessionStart` group whose `command` hook refreshes the index
|
|
97
|
+
* silently when a session starts. A law whose scope contains a malformed glob is
|
|
50
98
|
* excluded and reported, so a bad pattern fails loudly at generation rather than
|
|
51
99
|
* silently matching nothing at runtime.
|
|
52
100
|
*
|
|
@@ -77,13 +125,20 @@ export function compileHooks(manifest) {
|
|
|
77
125
|
byEvent.Stop = [{ hooks: [{ ...SPECLAW_HOOK }] }];
|
|
78
126
|
if (valid.length > 0)
|
|
79
127
|
byEvent.InstructionsLoaded = [{ hooks: [{ ...SPECLAW_HOOK }] }];
|
|
128
|
+
// Covers: req~session-start-hook~1
|
|
129
|
+
byEvent.SessionStart = [
|
|
130
|
+
{
|
|
131
|
+
matcher: SESSION_START_MATCHER,
|
|
132
|
+
hooks: [{ type: "command", command: SESSION_START_COMMAND, timeout: SESSION_START_TIMEOUT }],
|
|
133
|
+
},
|
|
134
|
+
];
|
|
80
135
|
return { byEvent, invalid };
|
|
81
136
|
}
|
|
82
137
|
/**
|
|
83
138
|
* Merge speclaw's compiled hook groups into an existing `hooks` object by
|
|
84
139
|
* identity: for every event, drop the groups speclaw owns (a group whose hooks
|
|
85
140
|
* are all speclaw's) and re-add the freshly compiled ones, never touching a
|
|
86
|
-
*
|
|
141
|
+
* hook that is not speclaw's (see {@link isSpeclawHook}). Idempotent, and it
|
|
87
142
|
* cannot delete another tool's hooks.
|
|
88
143
|
*
|
|
89
144
|
* @param existing - The current `hooks` object from the agent's settings (any shape).
|
|
@@ -11,8 +11,9 @@ You are the **explorer** role in speclaw's **Cortex** (One brain. Many agents.).
|
|
|
11
11
|
|
|
12
12
|
Investigate the idea or bug with Compass **before** any grep/read: locate with
|
|
13
13
|
`compass_find`, read with `compass_explore`, and run `compass_index` only when
|
|
14
|
-
find returns nothing or reports a missing or stale index
|
|
15
|
-
|
|
14
|
+
find returns nothing or reports a missing or stale index (under Claude Code an
|
|
15
|
+
existing index is already refreshed when the session starts). Read/Grep code
|
|
16
|
+
only after naming which Rule 1 fallback holds. Produce a short brief for the
|
|
16
17
|
planner: relevant symbols, blast radius, risks, open questions, and
|
|
17
18
|
`Compass calls made: N`. Do **not** draft change artifacts or edit source.
|
|
18
19
|
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
- Use `compass_explore` before editing to see a symbol's callers/callees and
|
|
4
4
|
blast radius; re-run `compass_index` after significant edits to keep the
|
|
5
|
-
graph fresh
|
|
5
|
+
graph fresh (the `SessionStart` hook refreshes it only when a session
|
|
6
|
+
starts).
|
|
6
7
|
- Make the smallest correct change; match the surrounding code.
|
|
7
8
|
- The code must satisfy the delta spec exactly. If reality diverges from the
|
|
8
9
|
spec, update the spec in the change (not silently) — the two must agree.
|
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
`compass_find` or `compass_explore` only for a gap the brief names. Re-read
|
|
7
7
|
a file under `docs/standards/` only when the brief does not cite it.
|
|
8
8
|
- When the handoff has no complete explorer brief, refresh the index with
|
|
9
|
-
`compass_index
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
`compass_index` (an existing index was already refreshed at session start
|
|
10
|
+
under Claude Code; refresh again after edits or when it is missing), then use
|
|
11
|
+
`compass_explore` and `compass_find` (mode: concept) before grep/read to
|
|
12
|
+
locate the code and its blast radius. Read the governing standards in
|
|
13
|
+
`docs/standards/`.
|
|
12
14
|
- Clarify what the user wants (feature / fix / refactor) and confirm scope.
|
|
13
15
|
- When `change.json` already has a `confirmedLevel` (the coordinator recorded
|
|
14
16
|
it in the single question round), use that level and skip the separate level
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
callees. Use `compass_diff_context` when the question is about a diff.
|
|
7
7
|
- **Index only when needed.** Run `compass_index` only when `compass_find`
|
|
8
8
|
returns nothing or reports a missing or stale index, then retry the find. It
|
|
9
|
-
is incremental (unchanged files skipped by hash).
|
|
9
|
+
is incremental (unchanged files skipped by hash). Under Claude Code the
|
|
10
|
+
`SessionStart` hook already refreshes an existing index when the session
|
|
11
|
+
starts, so it is usually current.
|
|
10
12
|
- **Read/Grep code only with a named reason.** Before any Read/Grep/Glob of
|
|
11
13
|
indexed source code, name which Rule 1 fallback holds: (1) a Compass call ran
|
|
12
14
|
and returned nothing useful for the query, (2) the graph is missing and
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Investigate the bug
|
|
2
2
|
|
|
3
|
-
- Refresh the index (`compass_index`).
|
|
3
|
+
- Refresh the index (`compass_index`). Under Claude Code the `SessionStart`
|
|
4
|
+
hook already refreshed an existing index when the session started; refresh
|
|
5
|
+
again when files changed since then or the index is missing.
|
|
4
6
|
- Call **`lawbook_investigate`** with `stackTrace` and/or `symptom`.
|
|
5
7
|
- **`compass_explore`** the top suspect — read the code yourself.
|
|
6
8
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@esneiderbravo/speclaw",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.7",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -59,6 +59,8 @@
|
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@eslint/js": "^10.0.1",
|
|
62
|
+
"@fontsource/chivo": "^5.3.0",
|
|
63
|
+
"@fontsource/chivo-mono": "^5.3.0",
|
|
62
64
|
"@resvg/resvg-js": "^2.6.2",
|
|
63
65
|
"@types/node": "^22.0.0",
|
|
64
66
|
"eslint": "^10.8.0",
|
|
@@ -67,6 +69,7 @@
|
|
|
67
69
|
"globals": "^17.9.0",
|
|
68
70
|
"prettier": "^3.9.6",
|
|
69
71
|
"typescript": "^5.6.0",
|
|
70
|
-
"typescript-eslint": "^8.65.0"
|
|
72
|
+
"typescript-eslint": "^8.65.0",
|
|
73
|
+
"wawoff2": "^2.0.1"
|
|
71
74
|
}
|
|
72
75
|
}
|