@esneiderbravo/speclaw 0.3.4 → 0.3.5
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 +24 -0
- package/dist/cli/commands/budget.js +36 -0
- package/dist/cli/commands/doctor.js +11 -1
- package/dist/cli/commands/init.js +3 -1
- package/dist/cli/commands/update.js +23 -3
- package/dist/cli/index.js +11 -3
- package/dist/modules/compass/indexer.js +9 -0
- package/dist/modules/compass/map.js +114 -0
- package/dist/modules/compass/register.js +30 -64
- package/dist/modules/foundation/assets/docs/compass.template.md +3 -0
- package/dist/modules/foundation/context-budget.js +45 -0
- package/dist/modules/foundation/doctor.js +9 -0
- package/dist/modules/foundation/register.js +71 -120
- package/dist/modules/foundation/scaffold.js +1 -1
- package/dist/modules/lawbook/assets/skills/archive/SKILL.md +1 -31
- package/dist/modules/lawbook/assets/skills/archive/steps/01-confirm-done.md +7 -0
- package/dist/modules/lawbook/assets/skills/archive/steps/02-reconcile.md +15 -0
- package/dist/modules/lawbook/assets/skills/archive/steps/03-validate-and-sync.md +7 -0
- package/dist/modules/lawbook/assets/skills/archive/steps/04-archive.md +9 -0
- package/dist/modules/lawbook/assets/skills/archive/steps/05-report.md +7 -0
- package/dist/modules/lawbook/assets/skills/build/SKILL.md +2 -100
- package/dist/modules/lawbook/assets/skills/build/steps/01-load-change.md +7 -0
- package/dist/modules/lawbook/assets/skills/build/steps/02-branch.md +6 -0
- package/dist/modules/lawbook/assets/skills/build/steps/03-implement.md +11 -0
- package/dist/modules/lawbook/assets/skills/build/steps/04-quality-gates.md +10 -0
- package/dist/modules/lawbook/assets/skills/build/steps/05-manual-verification.md +22 -0
- package/dist/modules/lawbook/assets/skills/build/steps/06-discipline-reports.md +44 -0
- package/dist/modules/lawbook/assets/skills/build/steps/07-hand-off.md +9 -0
- package/dist/modules/lawbook/assets/skills/draft/SKILL.md +3 -80
- package/dist/modules/lawbook/assets/skills/draft/steps/01-ensure-workspace.md +5 -0
- package/dist/modules/lawbook/assets/skills/draft/steps/02-understand.md +12 -0
- package/dist/modules/lawbook/assets/skills/draft/steps/03-name-capabilities.md +14 -0
- package/dist/modules/lawbook/assets/skills/draft/steps/04-write-artifacts.md +41 -0
- package/dist/modules/lawbook/assets/skills/draft/steps/05-validate.md +11 -0
- package/dist/modules/lawbook/assets/skills/draft/steps/06-hand-off.md +5 -0
- package/dist/modules/lawbook/assets/skills/explore/SKILL.md +6 -22
- package/dist/modules/lawbook/assets/skills/explore/steps/01-investigate.md +16 -0
- package/dist/modules/lawbook/assets/skills/explore/steps/02-summarize.md +7 -0
- package/dist/modules/lawbook/assets/skills/sync/SKILL.md +1 -30
- package/dist/modules/lawbook/assets/skills/sync/steps/01-confirm.md +5 -0
- package/dist/modules/lawbook/assets/skills/sync/steps/02-reconcile.md +16 -0
- package/dist/modules/lawbook/assets/skills/sync/steps/03-validate.md +6 -0
- package/dist/modules/lawbook/assets/skills/sync/steps/04-promote.md +10 -0
- package/dist/modules/lawbook/assets/skills/sync/steps/05-report.md +7 -0
- package/dist/modules/lawbook/register.js +17 -36
- package/dist/modules/tools/register.js +15 -15
- package/dist/server.js +13 -7
- package/dist/shared/budget.js +159 -0
- package/dist/shared/exposure.js +110 -0
- package/dist/shared/manifest.js +11 -2
- package/dist/shared/mcp.js +33 -0
- package/dist/shared/schema-tokens.js +86 -0
- package/dist/shared/tokens.js +41 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -91,6 +91,30 @@ Compass is inspired by [CodeGraph](https://github.com/colbymchenry/codegraph) an
|
|
|
91
91
|
|
|
92
92
|
<br/>
|
|
93
93
|
|
|
94
|
+
## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="◆" align="absmiddle"> Context cost
|
|
95
|
+
|
|
96
|
+
speclaw publishes and **gates** its own always-on context cost. Measured with a
|
|
97
|
+
deterministic offline estimator (`speclaw/estimate-v1`, about ±8% vs Anthropic's
|
|
98
|
+
tokenizer on this corpus — not a BPE dependency):
|
|
99
|
+
|
|
100
|
+
| | Tokens |
|
|
101
|
+
| :-- | --: |
|
|
102
|
+
| **speclaw budget (always-on)** | **~11.7k** (budget ceiling **13.0k**) |
|
|
103
|
+
| Spec Kit commands alone | ~18.6k ([spec-kit#1401](https://github.com/github/spec-kit/issues/1401)) |
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
speclaw budget # human table
|
|
107
|
+
speclaw budget --json # machine-readable; used by the suite gate
|
|
108
|
+
speclaw init --minimal # omit setup/lifecycle MCP tools from registration
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Raising a number in committed `token-budget.json` is a reviewable PR. Optional
|
|
112
|
+
calibration (never CI): `npm run budget:calibrate` with `ANTHROPIC_API_KEY`.
|
|
113
|
+
MCP servers cannot mark tools `defer_loading` — savings come from shorter
|
|
114
|
+
definitions, omitted registration (`--minimal`), and JIT skill steps.
|
|
115
|
+
|
|
116
|
+
<br/>
|
|
117
|
+
|
|
94
118
|
## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="◆" align="absmiddle"> The spec-driven workflow (Lawbook)
|
|
95
119
|
|
|
96
120
|
Lawbook is speclaw's answer to the biggest risk with AI agents: **code that
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ui } from "../lib/ui.js";
|
|
2
|
+
import { formatBudgetTable } from "../../shared/budget.js";
|
|
3
|
+
import { loadDeclaredBudget } from "../../shared/exposure.js";
|
|
4
|
+
import { measureInstallBudget } from "../../modules/foundation/context-budget.js";
|
|
5
|
+
/** Print the context-budget table or JSON. */
|
|
6
|
+
export async function runBudget(flags) {
|
|
7
|
+
const cwd = process.cwd();
|
|
8
|
+
const minimal = flags.minimal === true ? true : flags.minimal === false ? false : undefined;
|
|
9
|
+
const measurement = measureInstallBudget(cwd, minimal);
|
|
10
|
+
const declared = loadDeclaredBudget();
|
|
11
|
+
if (flags.json) {
|
|
12
|
+
console.log(JSON.stringify({
|
|
13
|
+
schemaVersion: 1,
|
|
14
|
+
profile: measurement.profile,
|
|
15
|
+
tools: measurement.tools,
|
|
16
|
+
skillsAndCommands: measurement.skillsAndCommands,
|
|
17
|
+
alwaysOnInstructions: measurement.alwaysOnInstructions,
|
|
18
|
+
pathScoped: measurement.pathScoped,
|
|
19
|
+
total: measurement.total,
|
|
20
|
+
toolCount: measurement.toolCount,
|
|
21
|
+
declared: {
|
|
22
|
+
tools: declared.surfaces.tools,
|
|
23
|
+
skillsAndCommands: declared.surfaces.skillsAndCommands,
|
|
24
|
+
alwaysOnInstructions: declared.surfaces.alwaysOnInstructions,
|
|
25
|
+
total: measurement.profile === "minimal" ? declared.minimal.total : declared.total,
|
|
26
|
+
},
|
|
27
|
+
details: measurement.details,
|
|
28
|
+
}, null, 2));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
ui.heading("speclaw budget");
|
|
32
|
+
console.log(formatBudgetTable(measurement, declared));
|
|
33
|
+
ui.plain();
|
|
34
|
+
ui.info(`profile: ${measurement.profile} · tools registered: ${measurement.toolCount}`);
|
|
35
|
+
ui.info("Spec Kit (commands only), for comparison: ~18,600 — github/spec-kit#1401");
|
|
36
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { doctor } from "../../modules/foundation/doctor.js";
|
|
2
|
+
import { measureInstallBudget } from "../../modules/foundation/context-budget.js";
|
|
2
3
|
import { ui } from "../lib/ui.js";
|
|
3
4
|
/** Verify the installation, printing each check and exiting non-zero on failure. */
|
|
4
5
|
export async function runDoctor(_flags) {
|
|
5
|
-
const
|
|
6
|
+
const cwd = process.cwd();
|
|
7
|
+
const checks = doctor(cwd);
|
|
6
8
|
ui.heading("speclaw doctor");
|
|
7
9
|
for (const c of checks) {
|
|
8
10
|
if (c.ok)
|
|
@@ -10,6 +12,14 @@ export async function runDoctor(_flags) {
|
|
|
10
12
|
else
|
|
11
13
|
ui.err(`${c.name} — ${c.detail}`);
|
|
12
14
|
}
|
|
15
|
+
try {
|
|
16
|
+
const m = measureInstallBudget(cwd);
|
|
17
|
+
ui.ok(`context budget — profile ${m.profile} · ${m.toolCount} tools · ~${m.total} always-on tokens ` +
|
|
18
|
+
`(A ${m.tools} · B ${m.skillsAndCommands} · C ${m.alwaysOnInstructions}; D path-scoped ${m.pathScoped} not in total)`);
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
ui.warn(`context budget — could not measure: ${err.message}`);
|
|
22
|
+
}
|
|
13
23
|
const failed = checks.filter((c) => !c.ok).length;
|
|
14
24
|
ui.plain();
|
|
15
25
|
if (failed === 0)
|
|
@@ -87,7 +87,9 @@ export async function runInit(flags) {
|
|
|
87
87
|
if (reinit) {
|
|
88
88
|
ui.info("speclaw is already set up here — your existing files are kept; only missing pieces are added.");
|
|
89
89
|
}
|
|
90
|
-
const report = scaffold(cwd, profile, packs, agents
|
|
90
|
+
const report = scaffold(cwd, profile, packs, agents, {
|
|
91
|
+
minimal: Boolean(flags.minimal),
|
|
92
|
+
});
|
|
91
93
|
ui.ok(`Foundation ${c.muted("— LAWS.md + 8 standards + CLAUDE.md/AGENTS.md")}`);
|
|
92
94
|
ui.ok(`Lawbook workflow ${c.muted("— draft · build · sync · archive · explore")}`);
|
|
93
95
|
for (const p of packs)
|
|
@@ -67,6 +67,18 @@ const MIGRATIONS = [
|
|
|
67
67
|
"required status check in branch protection. speclaw never enables that itself.\n" +
|
|
68
68
|
"- Preserve all project-specific wording; only apply these speclaw-authored changes.",
|
|
69
69
|
},
|
|
70
|
+
{
|
|
71
|
+
version: "0.3.5",
|
|
72
|
+
describe: "Context budget: compact map markers and budget-aware agent contracts",
|
|
73
|
+
agentPrompt: "- In docs/compass.md, ensure the markers `<!-- speclaw:map:start -->` and " +
|
|
74
|
+
"`<!-- speclaw:map:end -->` exist (usually just above 'Project-specific starting points') " +
|
|
75
|
+
"so `compass_index` can regenerate the compact project map between them. Do not hand-edit " +
|
|
76
|
+
"the content between the markers.\n" +
|
|
77
|
+
"- Optionally note in CLAUDE.md / AGENTS.md that `speclaw budget` reports always-on context " +
|
|
78
|
+
"cost and that `speclaw init --minimal` / `SPECLAW_MINIMAL=1` omit setup MCP tools " +
|
|
79
|
+
"(no server-side defer_loading).\n" +
|
|
80
|
+
"- Preserve all project-specific wording; only apply these speclaw-authored changes.",
|
|
81
|
+
},
|
|
70
82
|
];
|
|
71
83
|
/**
|
|
72
84
|
* Update speclaw and bring the current project up to date without a full re-init:
|
|
@@ -108,7 +120,12 @@ export async function runUpdate(flags) {
|
|
|
108
120
|
// Re-exec the NEWLY installed binary so migrations run with the new assets
|
|
109
121
|
// and any new feature steps — not this (now-stale) process. Carry --backup
|
|
110
122
|
// through so the refresh honors it after the upgrade.
|
|
111
|
-
const reArgs = [
|
|
123
|
+
const reArgs = [
|
|
124
|
+
"update",
|
|
125
|
+
"--migrate-only",
|
|
126
|
+
...(backup ? ["--backup"] : []),
|
|
127
|
+
...(flags.minimal ? ["--minimal"] : []),
|
|
128
|
+
];
|
|
112
129
|
const re = spawnSync("speclaw", reArgs, {
|
|
113
130
|
stdio: "inherit",
|
|
114
131
|
shell: winShell,
|
|
@@ -125,7 +142,7 @@ export async function runUpdate(flags) {
|
|
|
125
142
|
return;
|
|
126
143
|
}
|
|
127
144
|
}
|
|
128
|
-
applyProjectMigrations(cwd, backup);
|
|
145
|
+
applyProjectMigrations(cwd, backup, flags.minimal ? true : undefined);
|
|
129
146
|
}
|
|
130
147
|
/**
|
|
131
148
|
* Additively apply the current version's content and feature steps to a project.
|
|
@@ -135,8 +152,10 @@ export async function runUpdate(flags) {
|
|
|
135
152
|
* @param backup - When true, a locally edited managed file is copied to
|
|
136
153
|
* `<file>.bak` before it is refreshed; the default overwrites it in place
|
|
137
154
|
* (recoverable from git) and only reports the overwrite.
|
|
155
|
+
* @param minimal - When true, persist minimal exposure; when undefined, keep
|
|
156
|
+
* the manifest's existing value.
|
|
138
157
|
*/
|
|
139
|
-
function applyProjectMigrations(cwd, backup) {
|
|
158
|
+
function applyProjectMigrations(cwd, backup, minimal) {
|
|
140
159
|
const initialized = fs.existsSync(path.join(cwd, "ai-specs")) || fs.existsSync(path.join(cwd, "LAWS.md"));
|
|
141
160
|
if (!initialized) {
|
|
142
161
|
ui.step("Project");
|
|
@@ -154,6 +173,7 @@ function applyProjectMigrations(cwd, backup) {
|
|
|
154
173
|
const report = scaffold(cwd, { project_name: detectProjectName(cwd) }, packs, agents, {
|
|
155
174
|
refreshManaged: true,
|
|
156
175
|
backup,
|
|
176
|
+
...(minimal !== undefined ? { minimal } : {}),
|
|
157
177
|
});
|
|
158
178
|
const changed = report.written.filter((w) => !w.includes(".gitignore"));
|
|
159
179
|
if (changed.length) {
|
package/dist/cli/index.js
CHANGED
|
@@ -11,7 +11,9 @@ Install globally so the command is always available:
|
|
|
11
11
|
|
|
12
12
|
Setup
|
|
13
13
|
init Interactive setup: pick agents, scaffold, index, get the prompt
|
|
14
|
+
(--minimal omits setup/lifecycle MCP tools)
|
|
14
15
|
update Upgrade speclaw and apply only what's new (no re-init)
|
|
16
|
+
(--minimal persists minimal exposure; omit to keep prior)
|
|
15
17
|
agent list Show which agents are configured
|
|
16
18
|
agent add <id> Configure another agent later (symlinks + MCP)
|
|
17
19
|
|
|
@@ -34,6 +36,7 @@ Lawbook (spec-driven workflow)
|
|
|
34
36
|
|
|
35
37
|
Other
|
|
36
38
|
doctor Verify the installation
|
|
39
|
+
budget Measure always-on context cost (tools, skills, instructions)
|
|
37
40
|
check Evaluate an action against the laws (hooks call this; --dry-run to preview)
|
|
38
41
|
laws verify Verify the deterministic dependency/graph laws against the index
|
|
39
42
|
verify Verify laws for CI: exit codes, --sarif, --json, --strict-engines
|
|
@@ -55,6 +58,7 @@ const HEADER_COMMANDS = new Set([
|
|
|
55
58
|
"update",
|
|
56
59
|
"agent",
|
|
57
60
|
"doctor",
|
|
61
|
+
"budget",
|
|
58
62
|
"index",
|
|
59
63
|
"watch",
|
|
60
64
|
"lawbook",
|
|
@@ -64,13 +68,15 @@ const HEADER_COMMANDS = new Set([
|
|
|
64
68
|
* header-eligible command AND stdout is an interactive terminal (so pipes,
|
|
65
69
|
* redirection, and CI stay clean — mirroring the color gate in `ui.ts`). A
|
|
66
70
|
* forced-color signal counts as interactive so the header is exercisable in a
|
|
67
|
-
* child process.
|
|
71
|
+
* child process. `budget --json` is machine-consumed and suppresses the header.
|
|
68
72
|
*/
|
|
69
|
-
function maybeHeader(cmd) {
|
|
73
|
+
function maybeHeader(cmd, flags) {
|
|
70
74
|
if (!process.stdout.isTTY && process.env.FORCE_COLOR !== "1")
|
|
71
75
|
return;
|
|
72
76
|
if (!HEADER_COMMANDS.has(cmd))
|
|
73
77
|
return;
|
|
78
|
+
if (cmd === "budget" && flags.json)
|
|
79
|
+
return;
|
|
74
80
|
header();
|
|
75
81
|
}
|
|
76
82
|
/** Run the handler for a single command. Returns when the command completes. */
|
|
@@ -113,6 +119,8 @@ async function dispatch(cmd, flags) {
|
|
|
113
119
|
return (await import("./commands/lawbook.js")).runSpec(flags);
|
|
114
120
|
case "doctor":
|
|
115
121
|
return (await import("./commands/doctor.js")).runDoctor(flags);
|
|
122
|
+
case "budget":
|
|
123
|
+
return (await import("./commands/budget.js")).runBudget(flags);
|
|
116
124
|
case "check":
|
|
117
125
|
return (await import("./commands/check.js")).runCheck(flags);
|
|
118
126
|
case "laws":
|
|
@@ -129,7 +137,7 @@ async function dispatch(cmd, flags) {
|
|
|
129
137
|
async function main() {
|
|
130
138
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
131
139
|
const flags = parseFlags(rest);
|
|
132
|
-
maybeHeader(cmd);
|
|
140
|
+
maybeHeader(cmd, flags);
|
|
133
141
|
await dispatch(cmd, flags);
|
|
134
142
|
await maybeNotifyUpdate(cmd);
|
|
135
143
|
}
|
|
@@ -9,6 +9,7 @@ const SKIP_DIRS = new Set([
|
|
|
9
9
|
".git",
|
|
10
10
|
"node_modules",
|
|
11
11
|
"dist",
|
|
12
|
+
"dist-test",
|
|
12
13
|
"build",
|
|
13
14
|
".next",
|
|
14
15
|
"out",
|
|
@@ -173,5 +174,13 @@ export async function buildIndex(projectPath, onProgress) {
|
|
|
173
174
|
finally {
|
|
174
175
|
db.close();
|
|
175
176
|
}
|
|
177
|
+
// Compact map in committed docs/compass.md (between markers) — zero tool-call cost.
|
|
178
|
+
try {
|
|
179
|
+
const { writeCompactMap } = await import("./map.js");
|
|
180
|
+
writeCompactMap(projectPath);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// Map generation must never fail an index run.
|
|
184
|
+
}
|
|
176
185
|
return stats;
|
|
177
186
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { openDb } from "./db.js";
|
|
4
|
+
import { estimateTokens } from "../../shared/tokens.js";
|
|
5
|
+
import { loadDeclaredBudget } from "../../shared/exposure.js";
|
|
6
|
+
export const MAP_START = "<!-- speclaw:map:start -->";
|
|
7
|
+
export const MAP_END = "<!-- speclaw:map:end -->";
|
|
8
|
+
/**
|
|
9
|
+
* Build a compact project map (hubs + top-level layout) from the Compass DB.
|
|
10
|
+
* Truncates to the declared map token budget.
|
|
11
|
+
*
|
|
12
|
+
* @param projectPath - Project root with `.speclaw/index.db`.
|
|
13
|
+
* @returns Map body text (no markers), or null if the index is missing/empty.
|
|
14
|
+
*/
|
|
15
|
+
export function generateCompactMap(projectPath) {
|
|
16
|
+
const dbPath = path.join(projectPath, ".speclaw", "index.db");
|
|
17
|
+
if (!fs.existsSync(dbPath))
|
|
18
|
+
return null;
|
|
19
|
+
const db = openDb(projectPath);
|
|
20
|
+
try {
|
|
21
|
+
const fileCount = db.prepare("SELECT COUNT(*) AS c FROM files").get().c;
|
|
22
|
+
const nodeCount = db.prepare("SELECT COUNT(*) AS c FROM nodes").get().c;
|
|
23
|
+
if (fileCount === 0)
|
|
24
|
+
return null;
|
|
25
|
+
const hubs = db
|
|
26
|
+
.prepare(`SELECT n.name AS name, COUNT(*) AS fan_in
|
|
27
|
+
FROM edges e
|
|
28
|
+
JOIN nodes n ON n.id = e.dst_node_id
|
|
29
|
+
WHERE e.kind = 'call' AND e.dst_node_id IS NOT NULL
|
|
30
|
+
GROUP BY n.id
|
|
31
|
+
ORDER BY fan_in DESC
|
|
32
|
+
LIMIT 12`)
|
|
33
|
+
.all();
|
|
34
|
+
const topDirs = db
|
|
35
|
+
.prepare(`SELECT CASE
|
|
36
|
+
WHEN instr(path, '/') > 0 THEN substr(path, 1, instr(path, '/') - 1)
|
|
37
|
+
ELSE path
|
|
38
|
+
END AS top, COUNT(*) AS c
|
|
39
|
+
FROM files
|
|
40
|
+
GROUP BY top
|
|
41
|
+
ORDER BY c DESC
|
|
42
|
+
LIMIT 8`)
|
|
43
|
+
.all();
|
|
44
|
+
const hubLine = hubs.length === 0
|
|
45
|
+
? "hubs: (none yet)"
|
|
46
|
+
: `hubs: ${hubs.map((h) => `${h.name} ${h.fan_in}`).join(" · ")}`;
|
|
47
|
+
const dirLine = topDirs.map((d) => `${d.top}/ (${d.c})`).join(" ");
|
|
48
|
+
let body = [
|
|
49
|
+
`speclaw · ${fileCount} files · ${nodeCount} nodes`,
|
|
50
|
+
dirLine,
|
|
51
|
+
hubLine,
|
|
52
|
+
"entry: src/server.ts (mcp) · src/cli/index.ts (bin)",
|
|
53
|
+
].join("\n");
|
|
54
|
+
const cap = loadDeclaredBudget().map;
|
|
55
|
+
let omitted = false;
|
|
56
|
+
while (estimateTokens(body) > cap && hubs.length > 3) {
|
|
57
|
+
hubs.pop();
|
|
58
|
+
omitted = true;
|
|
59
|
+
const shorter = hubs.length === 0
|
|
60
|
+
? "hubs: (truncated)"
|
|
61
|
+
: `hubs: ${hubs.map((h) => `${h.name} ${h.fan_in}`).join(" · ")}`;
|
|
62
|
+
body = [
|
|
63
|
+
`speclaw · ${fileCount} files · ${nodeCount} nodes`,
|
|
64
|
+
dirLine,
|
|
65
|
+
shorter,
|
|
66
|
+
"entry: src/server.ts (mcp) · src/cli/index.ts (bin)",
|
|
67
|
+
omitted ? "(entries omitted to fit map budget)" : "",
|
|
68
|
+
]
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
.join("\n");
|
|
71
|
+
}
|
|
72
|
+
if (estimateTokens(body) > cap) {
|
|
73
|
+
body = [
|
|
74
|
+
`speclaw · ${fileCount} files · ${nodeCount} nodes`,
|
|
75
|
+
"entry: src/server.ts (mcp) · src/cli/index.ts (bin)",
|
|
76
|
+
"(entries omitted to fit map budget)",
|
|
77
|
+
].join("\n");
|
|
78
|
+
}
|
|
79
|
+
return body;
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
db.close();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Replace content between map markers in `docs/compass.md`. Preserves content
|
|
87
|
+
* outside markers. Does nothing (with reason) when markers are missing or the
|
|
88
|
+
* map cannot be generated.
|
|
89
|
+
*
|
|
90
|
+
* @param projectPath - Project root.
|
|
91
|
+
*/
|
|
92
|
+
export function writeCompactMap(projectPath) {
|
|
93
|
+
const compassPath = path.join(projectPath, "docs", "compass.md");
|
|
94
|
+
if (!fs.existsSync(compassPath)) {
|
|
95
|
+
return { written: false, reason: "docs/compass.md missing" };
|
|
96
|
+
}
|
|
97
|
+
const original = fs.readFileSync(compassPath, "utf8");
|
|
98
|
+
const start = original.indexOf(MAP_START);
|
|
99
|
+
const end = original.indexOf(MAP_END);
|
|
100
|
+
if (start < 0 || end < 0 || end < start) {
|
|
101
|
+
return { written: false, reason: "map markers missing — not regenerating" };
|
|
102
|
+
}
|
|
103
|
+
const map = generateCompactMap(projectPath);
|
|
104
|
+
if (!map) {
|
|
105
|
+
// Leave markers but clear body so we don't leave a stale map.
|
|
106
|
+
const next = original.slice(0, start + MAP_START.length) + "\n" + original.slice(end);
|
|
107
|
+
if (next !== original)
|
|
108
|
+
fs.writeFileSync(compassPath, next);
|
|
109
|
+
return { written: false, reason: "no index or empty graph — map omitted" };
|
|
110
|
+
}
|
|
111
|
+
const next = original.slice(0, start + MAP_START.length) + "\n" + map + "\n" + original.slice(end);
|
|
112
|
+
fs.writeFileSync(compassPath, next);
|
|
113
|
+
return { written: true };
|
|
114
|
+
}
|
|
@@ -1,78 +1,44 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { text } from "../../shared/mcp.js";
|
|
2
|
+
import { defineTool, text } from "../../shared/mcp.js";
|
|
3
|
+
import { shouldExpose } from "../../shared/exposure.js";
|
|
3
4
|
import { buildIndex } from "./indexer.js";
|
|
4
5
|
import { explore, search, recall, impact, trace } from "./query.js";
|
|
5
6
|
import { startWatch, stopWatch, watchStatus } from "./watcher.js";
|
|
6
7
|
import { visualize } from "./visualize.js";
|
|
7
8
|
// ─── Compass: speclaw's own code-intelligence engine (no external deps) ───
|
|
8
|
-
// A local graph of the codebase (nodes = definitions, edges = calls/imports)
|
|
9
|
-
// plus a local vector store for semantic recall. Lives in .speclaw/ (gitignored).
|
|
10
9
|
/**
|
|
11
|
-
* Register
|
|
12
|
-
* trace, watch) on the given server.
|
|
10
|
+
* Register Compass MCP tools on the given server.
|
|
13
11
|
*
|
|
14
|
-
* @param server - The MCP server to register
|
|
12
|
+
* @param server - The MCP server to register on.
|
|
13
|
+
* @param opts - Exposure options (`minimal` omits setup/specialized tools).
|
|
15
14
|
*/
|
|
16
|
-
export function registerCompass(server) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}, async ({ projectPath, node }) => text(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
limit: z.number().optional().describe("Max results (default 25)"),
|
|
34
|
-
},
|
|
35
|
-
}, async ({ projectPath, query, limit }) => text(search(projectPath, query, limit ?? 25)));
|
|
36
|
-
server.registerTool("compass_recall", {
|
|
37
|
-
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.",
|
|
38
|
-
inputSchema: {
|
|
39
|
-
projectPath: z.string().describe("Absolute path to the project"),
|
|
40
|
-
query: z.string().describe("Natural-language description of the code you want"),
|
|
41
|
-
limit: z.number().optional().describe("Max results (default 15)"),
|
|
42
|
-
},
|
|
43
|
-
}, async ({ projectPath, query, limit }) => text(await recall(projectPath, query, limit ?? 15)));
|
|
44
|
-
server.registerTool("compass_impact", {
|
|
45
|
-
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.",
|
|
46
|
-
inputSchema: {
|
|
47
|
-
projectPath: z.string().describe("Absolute path to the project"),
|
|
48
|
-
node: z.string().describe("Node name whose dependents you want"),
|
|
49
|
-
maxDepth: z.number().optional().describe("Max hops to traverse (default 4)"),
|
|
50
|
-
},
|
|
51
|
-
}, async ({ projectPath, node, maxDepth }) => text(impact(projectPath, node, maxDepth ?? 4)));
|
|
52
|
-
server.registerTool("compass_trace", {
|
|
53
|
-
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.",
|
|
54
|
-
inputSchema: {
|
|
55
|
-
projectPath: z.string().describe("Absolute path to the project"),
|
|
56
|
-
from: z.string().describe("Starting node name"),
|
|
57
|
-
to: z.string().describe("Target node name"),
|
|
58
|
-
maxDepth: z.number().optional().describe("Max hops to search (default 8)"),
|
|
59
|
-
},
|
|
15
|
+
export function registerCompass(server, opts = {}) {
|
|
16
|
+
const minimal = Boolean(opts.minimal);
|
|
17
|
+
const add = (name, description, inputSchema, handler) => {
|
|
18
|
+
if (!shouldExpose(name, minimal))
|
|
19
|
+
return;
|
|
20
|
+
defineTool(server, { name, description, inputSchema, handler });
|
|
21
|
+
};
|
|
22
|
+
add("compass_index", "Build or refresh the local code graph index. Run once per project, then on demand.", { projectPath: z.string() }, async ({ projectPath }) => text(await buildIndex(projectPath)));
|
|
23
|
+
add("compass_explore", "Read a symbol's source plus callers and callees. Prefer this before grep or Read.", { projectPath: z.string(), node: z.string() }, async ({ projectPath, node }) => text(explore(projectPath, node)));
|
|
24
|
+
add("compass_search", "Find symbols by name or keyword (substring). Cheaper structural search than grep.", { projectPath: z.string(), query: z.string(), limit: z.number().optional() }, async ({ projectPath, query, limit }) => text(search(projectPath, query, limit ?? 25)));
|
|
25
|
+
add("compass_recall", "Find symbols by meaning via local embeddings. Use when names are unknown.", { projectPath: z.string(), query: z.string(), limit: z.number().optional() }, async ({ projectPath, query, limit }) => text(await recall(projectPath, query, limit ?? 15)));
|
|
26
|
+
add("compass_impact", "List transitive callers of a symbol (blast radius) before editing.", { projectPath: z.string(), node: z.string(), maxDepth: z.number().optional() }, async ({ projectPath, node, maxDepth }) => text(impact(projectPath, node, maxDepth ?? 4)));
|
|
27
|
+
add("compass_trace", "Find a call path between two symbols within a depth limit.", {
|
|
28
|
+
projectPath: z.string(),
|
|
29
|
+
from: z.string(),
|
|
30
|
+
to: z.string(),
|
|
31
|
+
maxDepth: z.number().optional(),
|
|
60
32
|
}, async ({ projectPath, from, to, maxDepth }) => text(trace(projectPath, from, to, maxDepth ?? 8)));
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
depth: z.number().optional().describe("BFS depth around the focus node (default 2)"),
|
|
67
|
-
limit: z.number().optional().describe("Max nodes for the whole-graph view (default 300)"),
|
|
68
|
-
},
|
|
33
|
+
add("compass_visualize", "Write an offline HTML graph to .speclaw/graph.html for interactive exploration.", {
|
|
34
|
+
projectPath: z.string(),
|
|
35
|
+
node: z.string().optional(),
|
|
36
|
+
depth: z.number().optional(),
|
|
37
|
+
limit: z.number().optional(),
|
|
69
38
|
}, async ({ projectPath, node, depth, limit }) => text(visualize(projectPath, { focus: node, depth, limit })));
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
projectPath: z.string().describe("Absolute path to the project"),
|
|
74
|
-
action: z.enum(["start", "stop", "status"]).describe("start, stop, or status"),
|
|
75
|
-
},
|
|
39
|
+
add("compass_watch", "Start, stop, or status a debounced file watcher that re-indexes on change.", {
|
|
40
|
+
projectPath: z.string(),
|
|
41
|
+
action: z.enum(["start", "stop", "status"]),
|
|
76
42
|
}, async ({ projectPath, action }) => {
|
|
77
43
|
const result = action === "start"
|
|
78
44
|
? startWatch(projectPath)
|
|
@@ -42,6 +42,9 @@ a missing graph is not license to skip Compass. The only legitimate fallbacks
|
|
|
42
42
|
to Grep/Read: a Compass call returned nothing useful for your query, or the
|
|
43
43
|
target isn't indexed code (stylesheets, JSON/config, markdown, logs).
|
|
44
44
|
|
|
45
|
+
<!-- speclaw:map:start -->
|
|
46
|
+
<!-- speclaw:map:end -->
|
|
47
|
+
|
|
45
48
|
## Project-specific starting points
|
|
46
49
|
|
|
47
50
|
<!-- Filled in during speclaw init: the project's real entrypoints, core
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { registerCompass } from "../compass/register.js";
|
|
2
|
+
import { registerSpec } from "../lawbook/register.js";
|
|
3
|
+
import { registerTools } from "../tools/register.js";
|
|
4
|
+
import { registerFoundation } from "./register.js";
|
|
5
|
+
import { measureBudget } from "../../shared/budget.js";
|
|
6
|
+
import { isMinimalMode, packageRoot } from "../../shared/exposure.js";
|
|
7
|
+
/**
|
|
8
|
+
* Collect tool definitions as the MCP server would register them for a profile.
|
|
9
|
+
*
|
|
10
|
+
* @param minimal - Exposure profile.
|
|
11
|
+
*/
|
|
12
|
+
export function collectRegisteredTools(minimal) {
|
|
13
|
+
const tools = [];
|
|
14
|
+
const server = {
|
|
15
|
+
registerTool(name, config) {
|
|
16
|
+
tools.push({
|
|
17
|
+
name,
|
|
18
|
+
description: config.description ?? "",
|
|
19
|
+
inputSchema: config.inputSchema,
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
const opts = { minimal };
|
|
24
|
+
const stub = server;
|
|
25
|
+
registerFoundation(stub, opts);
|
|
26
|
+
registerSpec(stub, opts);
|
|
27
|
+
registerCompass(stub, opts);
|
|
28
|
+
registerTools(stub, opts);
|
|
29
|
+
return tools;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Measure context budget for an install (registered tools + project files).
|
|
33
|
+
*
|
|
34
|
+
* @param projectPath - Project root.
|
|
35
|
+
* @param minimal - Optional forced profile.
|
|
36
|
+
*/
|
|
37
|
+
export function measureInstallBudget(projectPath, minimal) {
|
|
38
|
+
const profile = minimal ?? isMinimalMode(projectPath);
|
|
39
|
+
return measureBudget({
|
|
40
|
+
projectPath,
|
|
41
|
+
packagePath: packageRoot(),
|
|
42
|
+
tools: collectRegisteredTools(profile),
|
|
43
|
+
minimal: profile,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { AGENTS, agentById, detectConfiguredAgents } from "../../shared/agents.js";
|
|
4
|
+
import { isMinimalMode } from "../../shared/exposure.js";
|
|
4
5
|
import { globError, hasBackend, hasBatchBackend, readLawManifest } from "./laws.js";
|
|
5
6
|
/**
|
|
6
7
|
* Run the speclaw installation health checks against a project: ai-specs and
|
|
@@ -101,6 +102,14 @@ export function doctor(projectPath) {
|
|
|
101
102
|
detail: has(".mcp.json") ? "present" : "missing — scaffold writes it",
|
|
102
103
|
});
|
|
103
104
|
lawEnforcementChecks(projectPath, checks);
|
|
105
|
+
const minimal = isMinimalMode(projectPath);
|
|
106
|
+
checks.push({
|
|
107
|
+
name: "exposure profile",
|
|
108
|
+
ok: true,
|
|
109
|
+
detail: minimal
|
|
110
|
+
? "minimal — setup/lifecycle MCP tools are omitted from registration"
|
|
111
|
+
: "full — all MCP tools registered (no server-side defer_loading)",
|
|
112
|
+
});
|
|
104
113
|
return checks;
|
|
105
114
|
}
|
|
106
115
|
/** The law ids recorded as loaded into agent context, from the append-only log. */
|