@esneiderbravo/speclaw 0.3.4 → 0.3.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.
Files changed (60) hide show
  1. package/README.md +66 -15
  2. package/dist/cli/commands/budget.js +36 -0
  3. package/dist/cli/commands/doctor.js +53 -11
  4. package/dist/cli/commands/init.js +3 -1
  5. package/dist/cli/commands/telemetry.js +16 -0
  6. package/dist/cli/commands/update.js +36 -3
  7. package/dist/cli/index.js +19 -4
  8. package/dist/modules/compass/indexer.js +10 -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/context-budget.js +58 -0
  13. package/dist/modules/foundation/doctor.js +626 -161
  14. package/dist/modules/foundation/graph.js +7 -4
  15. package/dist/modules/foundation/hooks.js +12 -0
  16. package/dist/modules/foundation/laws.js +1 -0
  17. package/dist/modules/foundation/register-core.js +108 -0
  18. package/dist/modules/foundation/register.js +22 -159
  19. package/dist/modules/foundation/scaffold.js +1 -1
  20. package/dist/modules/lawbook/assets/skills/archive/SKILL.md +1 -31
  21. package/dist/modules/lawbook/assets/skills/archive/steps/01-confirm-done.md +7 -0
  22. package/dist/modules/lawbook/assets/skills/archive/steps/02-reconcile.md +15 -0
  23. package/dist/modules/lawbook/assets/skills/archive/steps/03-validate-and-sync.md +7 -0
  24. package/dist/modules/lawbook/assets/skills/archive/steps/04-archive.md +9 -0
  25. package/dist/modules/lawbook/assets/skills/archive/steps/05-report.md +7 -0
  26. package/dist/modules/lawbook/assets/skills/build/SKILL.md +2 -100
  27. package/dist/modules/lawbook/assets/skills/build/steps/01-load-change.md +7 -0
  28. package/dist/modules/lawbook/assets/skills/build/steps/02-branch.md +6 -0
  29. package/dist/modules/lawbook/assets/skills/build/steps/03-implement.md +11 -0
  30. package/dist/modules/lawbook/assets/skills/build/steps/04-quality-gates.md +10 -0
  31. package/dist/modules/lawbook/assets/skills/build/steps/05-manual-verification.md +22 -0
  32. package/dist/modules/lawbook/assets/skills/build/steps/06-discipline-reports.md +44 -0
  33. package/dist/modules/lawbook/assets/skills/build/steps/07-hand-off.md +9 -0
  34. package/dist/modules/lawbook/assets/skills/draft/SKILL.md +3 -80
  35. package/dist/modules/lawbook/assets/skills/draft/steps/01-ensure-workspace.md +5 -0
  36. package/dist/modules/lawbook/assets/skills/draft/steps/02-understand.md +12 -0
  37. package/dist/modules/lawbook/assets/skills/draft/steps/03-name-capabilities.md +14 -0
  38. package/dist/modules/lawbook/assets/skills/draft/steps/04-write-artifacts.md +41 -0
  39. package/dist/modules/lawbook/assets/skills/draft/steps/05-validate.md +11 -0
  40. package/dist/modules/lawbook/assets/skills/draft/steps/06-hand-off.md +5 -0
  41. package/dist/modules/lawbook/assets/skills/explore/SKILL.md +6 -22
  42. package/dist/modules/lawbook/assets/skills/explore/steps/01-investigate.md +16 -0
  43. package/dist/modules/lawbook/assets/skills/explore/steps/02-summarize.md +7 -0
  44. package/dist/modules/lawbook/assets/skills/sync/SKILL.md +1 -30
  45. package/dist/modules/lawbook/assets/skills/sync/steps/01-confirm.md +5 -0
  46. package/dist/modules/lawbook/assets/skills/sync/steps/02-reconcile.md +16 -0
  47. package/dist/modules/lawbook/assets/skills/sync/steps/03-validate.md +6 -0
  48. package/dist/modules/lawbook/assets/skills/sync/steps/04-promote.md +10 -0
  49. package/dist/modules/lawbook/assets/skills/sync/steps/05-report.md +7 -0
  50. package/dist/modules/lawbook/register.js +17 -36
  51. package/dist/modules/tools/register.js +15 -15
  52. package/dist/server.js +13 -7
  53. package/dist/shared/budget.js +159 -0
  54. package/dist/shared/exposure.js +110 -0
  55. package/dist/shared/manifest.js +11 -2
  56. package/dist/shared/mcp.js +33 -0
  57. package/dist/shared/redact.js +90 -0
  58. package/dist/shared/schema-tokens.js +86 -0
  59. package/dist/shared/tokens.js +41 -0
  60. package/package.json +2 -1
package/README.md CHANGED
@@ -5,6 +5,8 @@
5
5
  <br/>
6
6
 
7
7
  <a href="https://www.npmjs.com/package/@esneiderbravo/speclaw"><img src="https://img.shields.io/npm/v/@esneiderbravo/speclaw?color=0E8E8E&labelColor=0B0F10&style=flat-square&label=npm" alt="npm"></a>
8
+ &nbsp;<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=0B0F10&style=flat-square&color=0E8E8E" alt="CI"></a>
9
+ &nbsp;<a href="https://www.npmjs.com/package/@esneiderbravo/speclaw?activeTab=versions"><img src="https://img.shields.io/badge/provenance-SLSA-0E8E8E?labelColor=0B0F10&style=flat-square" alt="npm provenance"></a>
8
10
  &nbsp;<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-0E8E8E?labelColor=0B0F10&style=flat-square" alt="MIT"></a>
9
11
  &nbsp;<img src="https://img.shields.io/badge/node-%E2%89%A522-0E8E8E?labelColor=0B0F10&style=flat-square" alt="Node >= 22">
10
12
 
@@ -29,32 +31,40 @@ One command. No cloud, no LLM, no API keys — <b>everything runs on your machin
29
31
  <br/>
30
32
 
31
33
  > [!TIP]
32
- > **Install once, then one command sets everything up.** Install speclaw globally, run
33
- > `speclaw init`, pick the agents you use, and speclaw scaffolds the project, indexes
34
- > your code, and hands your agent a ready-to-paste prompt to finish the setup.
34
+ > **One command. Detects your agents and wires only those.** Paste
35
+ > `npx @esneiderbravo/speclaw@latest init` speclaw detects Claude Code, Cursor,
36
+ > Codex, Windsurf, and generic `AGENTS.md` surfaces, scaffolds the constitution +
37
+ > lawbook, indexes your code, and registers the local MCP server — only for the
38
+ > agents you pick. This one-liner is a **stable contract** (see
39
+ > [CONTRIBUTING.md](CONTRIBUTING.md)); do not invent alternate install commands in
40
+ > directories or newsletters.
35
41
 
36
42
  <br/>
37
43
 
38
44
  ## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="◆" align="absmiddle">&nbsp; Quick start
39
45
 
40
- Install speclaw globally (once), then run `init` in your project root:
41
-
42
46
  ```bash
43
- npm i -g @esneiderbravo/speclaw
44
- speclaw init
47
+ npx @esneiderbravo/speclaw@latest init
45
48
  ```
46
49
 
47
50
  <p align="center">
48
51
  <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/terminal-quickstart.png" width="800" alt="speclaw init">
49
52
  </p>
50
53
 
51
- The `speclaw` command is now available everywhere — run `speclaw index`,
52
- `speclaw visualize`, or `speclaw lawbook …` directly in any project.
54
+ Prefer a global install for repeated CLI use?
55
+
56
+ ```bash
57
+ npm i -g @esneiderbravo/speclaw
58
+ speclaw init
59
+ ```
60
+
61
+ The `speclaw` command is then available everywhere — run `speclaw index`,
62
+ `speclaw doctor`, `speclaw visualize`, or `speclaw lawbook …` directly.
53
63
 
54
64
  `init` will:
55
65
 
56
- 1. **Ask which agents you use** (Claude Code, Cursor, Codex, …) — and configure
57
- only those. Add more later; nothing is forced on you.
66
+ 1. **Ask which agents you use** (Claude Code, Cursor, Codex, Windsurf, …) — and
67
+ configure only those. Add more later; nothing is forced on you.
58
68
  2. Write the **foundation** (constitution + standards) and the **lawbook workflow**,
59
69
  and compile your blocking laws into **agent hooks** for the agents that support them.
60
70
  3. **Index your code** with a live progress bar and a summary of what it found.
@@ -62,9 +72,23 @@ The `speclaw` command is now available everywhere — run `speclaw index`,
62
72
  5. Print a prompt to paste into your agent so it fills the constitution with your
63
73
  project's real architecture and conventions.
64
74
 
65
- Prefer not to install globally? A one-off `npx @esneiderbravo/speclaw init` works
66
- too (also `pnpm dlx` / `yarn dlx`) but installing globally means you can run the
67
- `speclaw` commands directly afterwards.
75
+ When something breaks, run `speclaw doctor --json` and paste it into an issue
76
+ (required on bug reports). Output is redacted by default.
77
+
78
+ <br/>
79
+
80
+ ## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="◆" align="absmiddle">&nbsp; Verify a release (provenance)
81
+
82
+ Every npm publish is signed via **Trusted Publishing (OIDC)** and carries a
83
+ SLSA provenance attestation tied to this repository and workflow. That proves
84
+ *where* the tarball was built — not that its contents are benign. Pair it with
85
+ your own review and (later) law-integrity pinning.
86
+
87
+ ```bash
88
+ npm audit signatures
89
+ # After downloading the tarball from the registry:
90
+ gh attestation verify <tarball> --owner esneiderbravo
91
+ ```
68
92
 
69
93
  <br/>
70
94
 
@@ -91,6 +115,30 @@ Compass is inspired by [CodeGraph](https://github.com/colbymchenry/codegraph) an
91
115
 
92
116
  <br/>
93
117
 
118
+ ## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="◆" align="absmiddle">&nbsp; Context cost
119
+
120
+ speclaw publishes and **gates** its own always-on context cost. Measured with a
121
+ deterministic offline estimator (`speclaw/estimate-v1`, about ±8% vs Anthropic's
122
+ tokenizer on this corpus — not a BPE dependency):
123
+
124
+ | | Tokens |
125
+ | :-- | --: |
126
+ | **speclaw budget (always-on)** | **~11.7k** (budget ceiling **13.0k**) |
127
+ | Spec Kit commands alone | ~18.6k ([spec-kit#1401](https://github.com/github/spec-kit/issues/1401)) |
128
+
129
+ ```bash
130
+ speclaw budget # human table
131
+ speclaw budget --json # machine-readable; used by the suite gate
132
+ speclaw init --minimal # omit setup/lifecycle MCP tools from registration
133
+ ```
134
+
135
+ Raising a number in committed `token-budget.json` is a reviewable PR. Optional
136
+ calibration (never CI): `npm run budget:calibrate` with `ANTHROPIC_API_KEY`.
137
+ MCP servers cannot mark tools `defer_loading` — savings come from shorter
138
+ definitions, omitted registration (`--minimal`), and JIT skill steps.
139
+
140
+ <br/>
141
+
94
142
  ## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="◆" align="absmiddle">&nbsp; The spec-driven workflow (Lawbook)
95
143
 
96
144
  Lawbook is speclaw's answer to the biggest risk with AI agents: **code that
@@ -178,7 +226,10 @@ you** — commit your own skills and commands there if you want to.
178
226
 
179
227
  **Enforcement artifacts.** For agents that support hooks, speclaw merges its law
180
228
  hooks into that agent's settings (e.g. `.claude/settings.json`) **by identity** —
181
- it never touches hooks you added yourself. The compiled law manifest lives in
229
+ it never touches hooks you added yourself. Each speclaw `mcp_tool` hook includes
230
+ an `input` map Claude Code substitutes from the hook event (`${cwd}`,
231
+ `${hook_event_name}`, `${tool_input.file_path}`, …) so `speclaw_check` receives
232
+ `projectPath` / `event` / `payload`. The compiled law manifest lives in
182
233
  `.speclaw/laws-manifest.json` (gitignored, regenerated on `init`/`update`), and a
183
234
  context-coverage log in `.speclaw/context-log.jsonl` feeds `speclaw doctor`.
184
235
 
@@ -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,21 +1,63 @@
1
1
  import { doctor } from "../../modules/foundation/doctor.js";
2
2
  import { ui } from "../lib/ui.js";
3
- /** Verify the installation, printing each check and exiting non-zero on failure. */
4
- export async function runDoctor(_flags) {
5
- const checks = doctor(process.cwd());
3
+ const STATUS_MARK = {
4
+ ok: "ok",
5
+ warn: "warn",
6
+ error: "error",
7
+ skip: "skip",
8
+ };
9
+ function printHuman(report) {
6
10
  ui.heading("speclaw doctor");
7
- for (const c of checks) {
8
- if (c.ok)
9
- ui.ok(`${c.name}${c.detail}`);
10
- else
11
- ui.err(`${c.name} ${c.detail}`);
11
+ for (const section of report.sections) {
12
+ ui.plain();
13
+ ui.ok(`${section.id} [${STATUS_MARK[section.status]}]`);
14
+ for (const c of section.checks) {
15
+ const line = `${c.title.padEnd(24)} ${c.detail ?? c.status}`;
16
+ if (c.status === "ok" || c.status === "skip")
17
+ ui.ok(` ${line}`);
18
+ else if (c.status === "warn") {
19
+ ui.warn(` ${line}`);
20
+ if (c.remedy)
21
+ ui.plain(` → ${c.remedy}`);
22
+ }
23
+ else {
24
+ ui.err(` ${line}`);
25
+ if (c.remedy)
26
+ ui.plain(` → ${c.remedy}`);
27
+ }
28
+ }
12
29
  }
13
- const failed = checks.filter((c) => !c.ok).length;
14
30
  ui.plain();
15
- if (failed === 0)
31
+ const warns = report.sections.flatMap((s) => s.checks).filter((c) => c.status === "warn").length;
32
+ const errs = report.sections.flatMap((s) => s.checks).filter((c) => c.status === "error").length;
33
+ if (errs === 0 && warns === 0)
16
34
  ui.ok("Everything is within the law.");
35
+ else if (errs === 0) {
36
+ ui.warn(`${warns} warning(s). Run \`speclaw doctor --json\` and paste it into an issue.`);
37
+ }
38
+ else {
39
+ ui.err(`${errs} error(s), ${warns} warning(s).`);
40
+ }
41
+ }
42
+ /**
43
+ * Verify the installation. Supports `--json`, `--offline`, `--strict`,
44
+ * `--redact` (default) / `--no-redact`.
45
+ */
46
+ export async function runDoctor(flags) {
47
+ const cwd = process.cwd();
48
+ const redact = flags["no-redact"] ? false : true;
49
+ const report = await doctor(cwd, {
50
+ offline: Boolean(flags.offline),
51
+ redact,
52
+ });
53
+ if (flags.json) {
54
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
55
+ }
17
56
  else {
18
- ui.warn(`${failed} check(s) failed.`);
57
+ printHuman(report);
58
+ }
59
+ const strict = Boolean(flags.strict);
60
+ if (report.status === "error" || (strict && report.status === "warn")) {
19
61
  process.exit(1);
20
62
  }
21
63
  }
@@ -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)
@@ -0,0 +1,16 @@
1
+ import { ui } from "../lib/ui.js";
2
+ /**
3
+ * Telemetry posture command. speclaw ships **no** telemetry code — `status`
4
+ * says so; enable/disable/log are rejected.
5
+ */
6
+ export async function runTelemetry(flags) {
7
+ const sub = flags._[0] ?? "status";
8
+ if (sub === "status") {
9
+ ui.ok("speclaw includes no telemetry — nothing is collected or transmitted.");
10
+ ui.plain("Policy: 100% local. There is no enable path and no analytics endpoint.");
11
+ return;
12
+ }
13
+ ui.err(`telemetry ${sub} is unavailable: speclaw does not include telemetry. ` +
14
+ "Run `speclaw telemetry status`.");
15
+ process.exit(1);
16
+ }
@@ -67,6 +67,31 @@ 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
+ },
82
+ {
83
+ version: "0.3.6",
84
+ describe: "Doctor JSON diagnostics and stable install one-liner",
85
+ agentPrompt: "- Mention that `speclaw doctor --json` is the support report (redacted by default) " +
86
+ "and that the stable install command is `npx @esneiderbravo/speclaw@latest init`.\n" +
87
+ "- Preserve all project-specific wording; only apply these speclaw-authored changes.",
88
+ },
89
+ {
90
+ version: "0.3.7",
91
+ describe: "Claude Code mcp_tool hooks pass speclaw_check input via ${…} templates",
92
+ // scaffold → installHooks already rewrites .claude/settings.json when the
93
+ // compiled hook shape changes; no extra run() step.
94
+ },
70
95
  ];
71
96
  /**
72
97
  * Update speclaw and bring the current project up to date without a full re-init:
@@ -108,7 +133,12 @@ export async function runUpdate(flags) {
108
133
  // Re-exec the NEWLY installed binary so migrations run with the new assets
109
134
  // and any new feature steps — not this (now-stale) process. Carry --backup
110
135
  // through so the refresh honors it after the upgrade.
111
- const reArgs = ["update", "--migrate-only", ...(backup ? ["--backup"] : [])];
136
+ const reArgs = [
137
+ "update",
138
+ "--migrate-only",
139
+ ...(backup ? ["--backup"] : []),
140
+ ...(flags.minimal ? ["--minimal"] : []),
141
+ ];
112
142
  const re = spawnSync("speclaw", reArgs, {
113
143
  stdio: "inherit",
114
144
  shell: winShell,
@@ -125,7 +155,7 @@ export async function runUpdate(flags) {
125
155
  return;
126
156
  }
127
157
  }
128
- applyProjectMigrations(cwd, backup);
158
+ applyProjectMigrations(cwd, backup, flags.minimal ? true : undefined);
129
159
  }
130
160
  /**
131
161
  * Additively apply the current version's content and feature steps to a project.
@@ -135,8 +165,10 @@ export async function runUpdate(flags) {
135
165
  * @param backup - When true, a locally edited managed file is copied to
136
166
  * `<file>.bak` before it is refreshed; the default overwrites it in place
137
167
  * (recoverable from git) and only reports the overwrite.
168
+ * @param minimal - When true, persist minimal exposure; when undefined, keep
169
+ * the manifest's existing value.
138
170
  */
139
- function applyProjectMigrations(cwd, backup) {
171
+ function applyProjectMigrations(cwd, backup, minimal) {
140
172
  const initialized = fs.existsSync(path.join(cwd, "ai-specs")) || fs.existsSync(path.join(cwd, "LAWS.md"));
141
173
  if (!initialized) {
142
174
  ui.step("Project");
@@ -154,6 +186,7 @@ function applyProjectMigrations(cwd, backup) {
154
186
  const report = scaffold(cwd, { project_name: detectProjectName(cwd) }, packs, agents, {
155
187
  refreshManaged: true,
156
188
  backup,
189
+ ...(minimal !== undefined ? { minimal } : {}),
157
190
  });
158
191
  const changed = report.written.filter((w) => !w.includes(".gitignore"));
159
192
  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
 
@@ -33,7 +35,9 @@ Lawbook (spec-driven workflow)
33
35
  lawbook archive <c> Finalize and archive a change
34
36
 
35
37
  Other
36
- doctor Verify the installation
38
+ doctor Verify the installation (--json, --offline, --strict)
39
+ budget Measure always-on context cost (tools, skills, instructions)
40
+ telemetry status Confirm speclaw ships no telemetry
37
41
  check Evaluate an action against the laws (hooks call this; --dry-run to preview)
38
42
  laws verify Verify the deterministic dependency/graph laws against the index
39
43
  verify Verify laws for CI: exit codes, --sarif, --json, --strict-engines
@@ -55,6 +59,8 @@ const HEADER_COMMANDS = new Set([
55
59
  "update",
56
60
  "agent",
57
61
  "doctor",
62
+ "budget",
63
+ "telemetry",
58
64
  "index",
59
65
  "watch",
60
66
  "lawbook",
@@ -64,13 +70,18 @@ const HEADER_COMMANDS = new Set([
64
70
  * header-eligible command AND stdout is an interactive terminal (so pipes,
65
71
  * redirection, and CI stay clean — mirroring the color gate in `ui.ts`). A
66
72
  * forced-color signal counts as interactive so the header is exercisable in a
67
- * child process.
73
+ * child process. `budget --json` and `doctor --json` are machine-consumed and
74
+ * suppress the header.
68
75
  */
69
- function maybeHeader(cmd) {
76
+ function maybeHeader(cmd, flags) {
70
77
  if (!process.stdout.isTTY && process.env.FORCE_COLOR !== "1")
71
78
  return;
72
79
  if (!HEADER_COMMANDS.has(cmd))
73
80
  return;
81
+ if (cmd === "budget" && flags.json)
82
+ return;
83
+ if (cmd === "doctor" && flags.json)
84
+ return;
74
85
  header();
75
86
  }
76
87
  /** Run the handler for a single command. Returns when the command completes. */
@@ -113,6 +124,10 @@ async function dispatch(cmd, flags) {
113
124
  return (await import("./commands/lawbook.js")).runSpec(flags);
114
125
  case "doctor":
115
126
  return (await import("./commands/doctor.js")).runDoctor(flags);
127
+ case "budget":
128
+ return (await import("./commands/budget.js")).runBudget(flags);
129
+ case "telemetry":
130
+ return (await import("./commands/telemetry.js")).runTelemetry(flags);
116
131
  case "check":
117
132
  return (await import("./commands/check.js")).runCheck(flags);
118
133
  case "laws":
@@ -129,7 +144,7 @@ async function dispatch(cmd, flags) {
129
144
  async function main() {
130
145
  const [cmd, ...rest] = process.argv.slice(2);
131
146
  const flags = parseFlags(rest);
132
- maybeHeader(cmd);
147
+ maybeHeader(cmd, flags);
133
148
  await dispatch(cmd, flags);
134
149
  await maybeNotifyUpdate(cmd);
135
150
  }
@@ -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",
@@ -164,6 +165,7 @@ export async function buildIndex(projectPath, onProgress) {
164
165
  )
165
166
  WHERE kind = 'call' AND dst_node_id IS NULL
166
167
  `);
168
+ db.prepare("INSERT INTO meta(key, value) VALUES ('indexed_at', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(new Date().toISOString());
167
169
  db.exec("COMMIT");
168
170
  }
169
171
  catch (err) {
@@ -173,5 +175,13 @@ export async function buildIndex(projectPath, onProgress) {
173
175
  finally {
174
176
  db.close();
175
177
  }
178
+ // Compact map in committed docs/compass.md (between markers) — zero tool-call cost.
179
+ try {
180
+ const { writeCompactMap } = await import("./map.js");
181
+ writeCompactMap(projectPath);
182
+ }
183
+ catch {
184
+ // Map generation must never fail an index run.
185
+ }
176
186
  return stats;
177
187
  }
@@ -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
+ }