@esneiderbravo/speclaw 0.3.3 → 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.
Files changed (66) hide show
  1. package/README.md +54 -1
  2. package/dist/cli/commands/budget.js +36 -0
  3. package/dist/cli/commands/doctor.js +11 -1
  4. package/dist/cli/commands/init.js +3 -1
  5. package/dist/cli/commands/update.js +30 -3
  6. package/dist/cli/commands/verify.js +118 -0
  7. package/dist/cli/index.js +14 -3
  8. package/dist/modules/compass/indexer.js +9 -0
  9. package/dist/modules/compass/map.js +114 -0
  10. package/dist/modules/compass/register.js +30 -64
  11. package/dist/modules/foundation/assets/docs/compass.template.md +3 -0
  12. package/dist/modules/foundation/assets/laws/laws-manifest.json +49 -0
  13. package/dist/modules/foundation/assets/workflows/speclaw.yml +26 -0
  14. package/dist/modules/foundation/ci.js +41 -0
  15. package/dist/modules/foundation/context-budget.js +45 -0
  16. package/dist/modules/foundation/deps.js +1 -1
  17. package/dist/modules/foundation/doctor.js +9 -0
  18. package/dist/modules/foundation/graph.js +1 -1
  19. package/dist/modules/foundation/laws.js +28 -0
  20. package/dist/modules/foundation/register.js +71 -120
  21. package/dist/modules/foundation/report-md.js +33 -0
  22. package/dist/modules/foundation/sarif.js +96 -0
  23. package/dist/modules/foundation/scaffold.js +32 -12
  24. package/dist/modules/foundation/verify-model.js +14 -0
  25. package/dist/modules/foundation/verify.js +16 -17
  26. package/dist/modules/lawbook/assets/skills/archive/SKILL.md +1 -31
  27. package/dist/modules/lawbook/assets/skills/archive/steps/01-confirm-done.md +7 -0
  28. package/dist/modules/lawbook/assets/skills/archive/steps/02-reconcile.md +15 -0
  29. package/dist/modules/lawbook/assets/skills/archive/steps/03-validate-and-sync.md +7 -0
  30. package/dist/modules/lawbook/assets/skills/archive/steps/04-archive.md +9 -0
  31. package/dist/modules/lawbook/assets/skills/archive/steps/05-report.md +7 -0
  32. package/dist/modules/lawbook/assets/skills/build/SKILL.md +2 -100
  33. package/dist/modules/lawbook/assets/skills/build/steps/01-load-change.md +7 -0
  34. package/dist/modules/lawbook/assets/skills/build/steps/02-branch.md +6 -0
  35. package/dist/modules/lawbook/assets/skills/build/steps/03-implement.md +11 -0
  36. package/dist/modules/lawbook/assets/skills/build/steps/04-quality-gates.md +10 -0
  37. package/dist/modules/lawbook/assets/skills/build/steps/05-manual-verification.md +22 -0
  38. package/dist/modules/lawbook/assets/skills/build/steps/06-discipline-reports.md +44 -0
  39. package/dist/modules/lawbook/assets/skills/build/steps/07-hand-off.md +9 -0
  40. package/dist/modules/lawbook/assets/skills/draft/SKILL.md +3 -80
  41. package/dist/modules/lawbook/assets/skills/draft/steps/01-ensure-workspace.md +5 -0
  42. package/dist/modules/lawbook/assets/skills/draft/steps/02-understand.md +12 -0
  43. package/dist/modules/lawbook/assets/skills/draft/steps/03-name-capabilities.md +14 -0
  44. package/dist/modules/lawbook/assets/skills/draft/steps/04-write-artifacts.md +41 -0
  45. package/dist/modules/lawbook/assets/skills/draft/steps/05-validate.md +11 -0
  46. package/dist/modules/lawbook/assets/skills/draft/steps/06-hand-off.md +5 -0
  47. package/dist/modules/lawbook/assets/skills/explore/SKILL.md +6 -22
  48. package/dist/modules/lawbook/assets/skills/explore/steps/01-investigate.md +16 -0
  49. package/dist/modules/lawbook/assets/skills/explore/steps/02-summarize.md +7 -0
  50. package/dist/modules/lawbook/assets/skills/sync/SKILL.md +1 -30
  51. package/dist/modules/lawbook/assets/skills/sync/steps/01-confirm.md +5 -0
  52. package/dist/modules/lawbook/assets/skills/sync/steps/02-reconcile.md +16 -0
  53. package/dist/modules/lawbook/assets/skills/sync/steps/03-validate.md +6 -0
  54. package/dist/modules/lawbook/assets/skills/sync/steps/04-promote.md +10 -0
  55. package/dist/modules/lawbook/assets/skills/sync/steps/05-report.md +7 -0
  56. package/dist/modules/lawbook/register.js +17 -36
  57. package/dist/modules/tools/register.js +15 -15
  58. package/dist/server.js +13 -7
  59. package/dist/shared/budget.js +159 -0
  60. package/dist/shared/exposure.js +110 -0
  61. package/dist/shared/git.js +49 -0
  62. package/dist/shared/manifest.js +11 -2
  63. package/dist/shared/mcp.js +33 -0
  64. package/dist/shared/schema-tokens.js +86 -0
  65. package/dist/shared/tokens.js +41 -0
  66. package/package.json +2 -1
package/README.md CHANGED
@@ -82,7 +82,7 @@ too (also `pnpm dlx` / `yarn dlx`) — but installing globally means you can run
82
82
 
83
83
  | Module | What it does |
84
84
  | :-- | :-- |
85
- | **Foundation** | The project's constitution: `LAWS.md` binding a set of granular standards under `docs/standards/` (base, architecture, backend, frontend, testing, documentation, conventions, lawbook), plus strict `CLAUDE.md` / `AGENTS.md` agent contracts — filled from your real codebase. It also **enforces** them: blocking laws compile into agent hooks that deny a forbidden edit at the keystroke (`speclaw check` / `speclaw_check`), and architectural laws are verified deterministically against the Compass graph — dependency rules (`deps`) and cycles (`graph`) — via `speclaw laws verify` / `law_verify`, which reports each law as passed, failed, skipped, or unknown (an unresolved reference is *unknown*, never a silent pass). |
85
+ | **Foundation** | The project's constitution: `LAWS.md` binding a set of granular standards under `docs/standards/` (base, architecture, backend, frontend, testing, documentation, conventions, lawbook), plus strict `CLAUDE.md` / `AGENTS.md` agent contracts — filled from your real codebase. It also **enforces** them: blocking laws compile into agent hooks that deny a forbidden edit at the keystroke (`speclaw check` / `speclaw_check`), and architectural laws are verified deterministically against the Compass graph — dependency rules (`deps`) and cycles (`graph`) — via `speclaw verify` (CI orchestrator: exit codes, SARIF, markdown) and `speclaw laws verify` / `law_verify`. Each law is reported as passed, failed, skipped, or unknown (an unresolved reference is *unknown*, never a silent pass). |
86
86
  | **Compass** | speclaw's own local code graph. Parses your code (tree-sitter) into nodes + edges plus a local vector store, so an agent finds and understands code with a fraction of the tokens a grep/read loop would cost. No LLM, 100% local, lives in `.speclaw/` (gitignored). |
87
87
  | **Lawbook** | speclaw's own spec-driven workflow: `draft → build → sync → archive` (and `explore`), backed by `lawbook_*` engine tools. No external CLI. |
88
88
  | **Tools** | Opt-in packs of skills and subagents (currently the dev-agents) that agents use for specific tasks. |
@@ -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">&nbsp; 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">&nbsp; The spec-driven workflow (Lawbook)
95
119
 
96
120
  Lawbook is speclaw's answer to the biggest risk with AI agents: **code that
@@ -210,6 +234,35 @@ context. Agents without hooks (Cursor, Codex) enforce the same laws in CI via
210
234
 
211
235
  <br/>
212
236
 
237
+ ## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="◆" align="absmiddle">&nbsp; Verify in CI
238
+
239
+ `speclaw verify` evaluates your `deps` and `graph` laws against the local Compass
240
+ index. It is deterministic: **no model, no API key, no network.**
241
+
242
+ ```bash
243
+ speclaw verify --ci --sarif speclaw.sarif --json speclaw.json
244
+ ```
245
+
246
+ | Exit | Meaning |
247
+ | :-- | :-- |
248
+ | **0** | No findings at or above `--fail-on` (default `error`) |
249
+ | **1** | At least one finding at or above `--fail-on` |
250
+ | **2** | Usage error (unknown `--fail-on` / `--format`) |
251
+ | **3** | Environment (shallow clone under `--ci`, or an unwritable `--sarif`/`--json` path) |
252
+ | **4** | At least one law was skipped, and `--strict-engines` was set |
253
+
254
+ On GitHub:
255
+
256
+ ```yaml
257
+ - uses: esneiderbravo/speclaw@v1
258
+ ```
259
+
260
+ `init` / `update` write `.github/workflows/speclaw.yml` only when that path is
261
+ missing — they never overwrite your CI. Make the check required in branch
262
+ protection yourself; speclaw does not.
263
+
264
+ <br/>
265
+
213
266
  ## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="◆" align="absmiddle">&nbsp; Staying up to date
214
267
 
215
268
  speclaw checks for new releases in the background (at most once a day) and nudges
@@ -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 checks = doctor(process.cwd());
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)
@@ -60,6 +60,25 @@ const MIGRATIONS = [
60
60
  "in docs/standards/testing-standards.md (the 'Manual & end-to-end verification' section).\n" +
61
61
  "- Preserve all project-specific wording; only apply these speclaw-authored changes.",
62
62
  },
63
+ {
64
+ version: "0.3.4",
65
+ describe: "CI verify workflow (if missing) and seed-law merge",
66
+ agentPrompt: "- If you want pull requests gated on speclaw, add the `speclaw` GitHub check as a " +
67
+ "required status check in branch protection. speclaw never enables that itself.\n" +
68
+ "- Preserve all project-specific wording; only apply these speclaw-authored changes.",
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
+ },
63
82
  ];
64
83
  /**
65
84
  * Update speclaw and bring the current project up to date without a full re-init:
@@ -101,7 +120,12 @@ export async function runUpdate(flags) {
101
120
  // Re-exec the NEWLY installed binary so migrations run with the new assets
102
121
  // and any new feature steps — not this (now-stale) process. Carry --backup
103
122
  // through so the refresh honors it after the upgrade.
104
- const reArgs = ["update", "--migrate-only", ...(backup ? ["--backup"] : [])];
123
+ const reArgs = [
124
+ "update",
125
+ "--migrate-only",
126
+ ...(backup ? ["--backup"] : []),
127
+ ...(flags.minimal ? ["--minimal"] : []),
128
+ ];
105
129
  const re = spawnSync("speclaw", reArgs, {
106
130
  stdio: "inherit",
107
131
  shell: winShell,
@@ -118,7 +142,7 @@ export async function runUpdate(flags) {
118
142
  return;
119
143
  }
120
144
  }
121
- applyProjectMigrations(cwd, backup);
145
+ applyProjectMigrations(cwd, backup, flags.minimal ? true : undefined);
122
146
  }
123
147
  /**
124
148
  * Additively apply the current version's content and feature steps to a project.
@@ -128,8 +152,10 @@ export async function runUpdate(flags) {
128
152
  * @param backup - When true, a locally edited managed file is copied to
129
153
  * `<file>.bak` before it is refreshed; the default overwrites it in place
130
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.
131
157
  */
132
- function applyProjectMigrations(cwd, backup) {
158
+ function applyProjectMigrations(cwd, backup, minimal) {
133
159
  const initialized = fs.existsSync(path.join(cwd, "ai-specs")) || fs.existsSync(path.join(cwd, "LAWS.md"));
134
160
  if (!initialized) {
135
161
  ui.step("Project");
@@ -147,6 +173,7 @@ function applyProjectMigrations(cwd, backup) {
147
173
  const report = scaffold(cwd, { project_name: detectProjectName(cwd) }, packs, agents, {
148
174
  refreshManaged: true,
149
175
  backup,
176
+ ...(minimal !== undefined ? { minimal } : {}),
150
177
  });
151
178
  const changed = report.written.filter((w) => !w.includes(".gitignore"));
152
179
  if (changed.length) {
@@ -0,0 +1,118 @@
1
+ import fs from "node:fs";
2
+ import { list } from "../lib/args.js";
3
+ import { ui, c } from "../lib/ui.js";
4
+ import { isShallowRepo } from "../../shared/git-history.js";
5
+ import { pkgVersion } from "../../shared/version.js";
6
+ import { parseFailOn, verifyExitCode } from "../../modules/foundation/ci.js";
7
+ import { toMarkdown } from "../../modules/foundation/report-md.js";
8
+ import { toSarif } from "../../modules/foundation/sarif.js";
9
+ import { loadManifestForVerify } from "../../modules/foundation/laws.js";
10
+ import { verifyLaws } from "../../modules/foundation/verify.js";
11
+ const FORMATS = new Set(["text", "json", "sarif", "markdown"]);
12
+ /**
13
+ * `speclaw verify` — the CI orchestrator over {@link verifyLaws}. Formats and
14
+ * exit codes live here; graph evaluation stays in `verify.ts`. `speclaw check`
15
+ * (hooks) and `speclaw laws verify` (the thin batch twin) are unchanged.
16
+ *
17
+ * @param flags - Parsed CLI flags.
18
+ */
19
+ export async function runVerify(flags) {
20
+ const cwd = process.cwd();
21
+ const ci = Boolean(flags.ci);
22
+ if (ci)
23
+ process.env.NO_COLOR = "1";
24
+ const failOn = parseFailOn(flags["fail-on"]);
25
+ if (failOn === null) {
26
+ ui.err(`--fail-on must be error, warn, or info.`);
27
+ process.exit(2);
28
+ }
29
+ const format = flags.format === undefined || flags.format === true ? "text" : String(flags.format);
30
+ if (!FORMATS.has(format)) {
31
+ ui.err(`--format must be text, json, sarif, or markdown.`);
32
+ process.exit(2);
33
+ }
34
+ const strict = Boolean(flags["strict-engines"]);
35
+ const engines = list(flags.engine).filter((e) => e === "deps" || e === "graph");
36
+ const paths = list(flags.path);
37
+ if (ci && isShallowRepo(cwd)) {
38
+ ui.err("Shallow clone — speclaw cannot see the merge base. Check out with fetch-depth: 0.");
39
+ process.exit(3);
40
+ }
41
+ const manifest = loadManifestForVerify(cwd);
42
+ const report = verifyLaws({
43
+ projectPath: cwd,
44
+ paths: paths.length ? paths : undefined,
45
+ engines: engines.length ? engines : undefined,
46
+ lawIds: list(flags.law).length ? list(flags.law) : undefined,
47
+ });
48
+ const sarifPath = typeof flags.sarif === "string" ? flags.sarif : undefined;
49
+ const jsonPath = typeof flags.json === "string" ? flags.json : undefined;
50
+ if (sarifPath) {
51
+ if (!writeOut(sarifPath, JSON.stringify(toSarif(report, sarifCtx(manifest.laws)), null, 2))) {
52
+ process.exit(3);
53
+ }
54
+ }
55
+ if (jsonPath) {
56
+ if (!writeOut(jsonPath, JSON.stringify(report, null, 2)))
57
+ process.exit(3);
58
+ }
59
+ const summaryFile = process.env.GITHUB_STEP_SUMMARY;
60
+ if (summaryFile) {
61
+ try {
62
+ fs.appendFileSync(summaryFile, toMarkdown(report));
63
+ }
64
+ catch (err) {
65
+ ui.warn(`Could not write $GITHUB_STEP_SUMMARY: ${err.message}`);
66
+ }
67
+ }
68
+ printReport(report, format, flags.json === true && !jsonPath);
69
+ process.exit(verifyExitCode(report, { failOn, strictEngines: strict }));
70
+ }
71
+ function sarifCtx(laws) {
72
+ return { speclawVersion: pkgVersion(), laws };
73
+ }
74
+ function writeOut(file, body) {
75
+ try {
76
+ fs.writeFileSync(file, body.endsWith("\n") ? body : body + "\n");
77
+ return true;
78
+ }
79
+ catch (err) {
80
+ ui.err(`Cannot write ${file}: ${err.message}`);
81
+ return false;
82
+ }
83
+ }
84
+ function printReport(report, format, jsonStdout) {
85
+ if (jsonStdout || format === "json") {
86
+ console.log(JSON.stringify(report, null, 2));
87
+ return;
88
+ }
89
+ if (format === "sarif") {
90
+ // Rules need the loaded laws; re-read via the same fallback the run used.
91
+ const laws = loadManifestForVerify(process.cwd()).laws;
92
+ console.log(JSON.stringify(toSarif(report, sarifCtx(laws)), null, 2));
93
+ return;
94
+ }
95
+ if (format === "markdown") {
96
+ process.stdout.write(toMarkdown(report));
97
+ return;
98
+ }
99
+ const { summary } = report;
100
+ ui.heading("speclaw verify");
101
+ ui.info(`${summary.passed} passed · ${c.red(String(summary.failed))} failed · ` +
102
+ `${summary.skipped} skipped · ${summary.unknown} unknown ` +
103
+ `(${report.elapsedMs.toFixed(1)} ms)`);
104
+ if (summary.evaluated === 0 && summary.skipped === 0) {
105
+ ui.warn("0 batch laws evaluated — verify is not checking anything.");
106
+ }
107
+ for (const f of report.findings) {
108
+ const at = f.line ? `${f.file}:${f.line}` : f.file;
109
+ ui.warn(`${c.cream(f.lawId)} — ${at}${f.detail ? ` ${f.detail}` : ""}`);
110
+ }
111
+ for (const u of report.unknown)
112
+ ui.plain(` ? ${c.cream(u.lawId)} — ${u.detail}`);
113
+ for (const s of report.skipped) {
114
+ ui.plain(` – ${c.cream(s.lawId)} — skipped: ${s.reason}${s.detail ? ` (${s.detail})` : ""}`);
115
+ }
116
+ if (report.findings.length === 0 && summary.evaluated > 0)
117
+ ui.ok("No violations.");
118
+ }
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,8 +36,10 @@ 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
42
+ verify Verify laws for CI: exit codes, --sarif, --json, --strict-engines
39
43
  mcp Start the MCP server (used by your agent's config)
40
44
  help Show this help
41
45
  --version Print the installed speclaw version
@@ -54,6 +58,7 @@ const HEADER_COMMANDS = new Set([
54
58
  "update",
55
59
  "agent",
56
60
  "doctor",
61
+ "budget",
57
62
  "index",
58
63
  "watch",
59
64
  "lawbook",
@@ -63,13 +68,15 @@ const HEADER_COMMANDS = new Set([
63
68
  * header-eligible command AND stdout is an interactive terminal (so pipes,
64
69
  * redirection, and CI stay clean — mirroring the color gate in `ui.ts`). A
65
70
  * forced-color signal counts as interactive so the header is exercisable in a
66
- * child process.
71
+ * child process. `budget --json` is machine-consumed and suppresses the header.
67
72
  */
68
- function maybeHeader(cmd) {
73
+ function maybeHeader(cmd, flags) {
69
74
  if (!process.stdout.isTTY && process.env.FORCE_COLOR !== "1")
70
75
  return;
71
76
  if (!HEADER_COMMANDS.has(cmd))
72
77
  return;
78
+ if (cmd === "budget" && flags.json)
79
+ return;
73
80
  header();
74
81
  }
75
82
  /** Run the handler for a single command. Returns when the command completes. */
@@ -112,10 +119,14 @@ async function dispatch(cmd, flags) {
112
119
  return (await import("./commands/lawbook.js")).runSpec(flags);
113
120
  case "doctor":
114
121
  return (await import("./commands/doctor.js")).runDoctor(flags);
122
+ case "budget":
123
+ return (await import("./commands/budget.js")).runBudget(flags);
115
124
  case "check":
116
125
  return (await import("./commands/check.js")).runCheck(flags);
117
126
  case "laws":
118
127
  return (await import("./commands/laws.js")).runLaws(flags);
128
+ case "verify":
129
+ return (await import("./commands/verify.js")).runVerify(flags);
119
130
  default:
120
131
  ui.err(`Unknown command: ${cmd}`);
121
132
  console.log(HELP);
@@ -126,7 +137,7 @@ async function dispatch(cmd, flags) {
126
137
  async function main() {
127
138
  const [cmd, ...rest] = process.argv.slice(2);
128
139
  const flags = parseFlags(rest);
129
- maybeHeader(cmd);
140
+ maybeHeader(cmd, flags);
130
141
  await dispatch(cmd, flags);
131
142
  await maybeNotifyUpdate(cmd);
132
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
+ }