@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.
Files changed (54) hide show
  1. package/README.md +24 -0
  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 +23 -3
  6. package/dist/cli/index.js +11 -3
  7. package/dist/modules/compass/indexer.js +9 -0
  8. package/dist/modules/compass/map.js +114 -0
  9. package/dist/modules/compass/register.js +30 -64
  10. package/dist/modules/foundation/assets/docs/compass.template.md +3 -0
  11. package/dist/modules/foundation/context-budget.js +45 -0
  12. package/dist/modules/foundation/doctor.js +9 -0
  13. package/dist/modules/foundation/register.js +71 -120
  14. package/dist/modules/foundation/scaffold.js +1 -1
  15. package/dist/modules/lawbook/assets/skills/archive/SKILL.md +1 -31
  16. package/dist/modules/lawbook/assets/skills/archive/steps/01-confirm-done.md +7 -0
  17. package/dist/modules/lawbook/assets/skills/archive/steps/02-reconcile.md +15 -0
  18. package/dist/modules/lawbook/assets/skills/archive/steps/03-validate-and-sync.md +7 -0
  19. package/dist/modules/lawbook/assets/skills/archive/steps/04-archive.md +9 -0
  20. package/dist/modules/lawbook/assets/skills/archive/steps/05-report.md +7 -0
  21. package/dist/modules/lawbook/assets/skills/build/SKILL.md +2 -100
  22. package/dist/modules/lawbook/assets/skills/build/steps/01-load-change.md +7 -0
  23. package/dist/modules/lawbook/assets/skills/build/steps/02-branch.md +6 -0
  24. package/dist/modules/lawbook/assets/skills/build/steps/03-implement.md +11 -0
  25. package/dist/modules/lawbook/assets/skills/build/steps/04-quality-gates.md +10 -0
  26. package/dist/modules/lawbook/assets/skills/build/steps/05-manual-verification.md +22 -0
  27. package/dist/modules/lawbook/assets/skills/build/steps/06-discipline-reports.md +44 -0
  28. package/dist/modules/lawbook/assets/skills/build/steps/07-hand-off.md +9 -0
  29. package/dist/modules/lawbook/assets/skills/draft/SKILL.md +3 -80
  30. package/dist/modules/lawbook/assets/skills/draft/steps/01-ensure-workspace.md +5 -0
  31. package/dist/modules/lawbook/assets/skills/draft/steps/02-understand.md +12 -0
  32. package/dist/modules/lawbook/assets/skills/draft/steps/03-name-capabilities.md +14 -0
  33. package/dist/modules/lawbook/assets/skills/draft/steps/04-write-artifacts.md +41 -0
  34. package/dist/modules/lawbook/assets/skills/draft/steps/05-validate.md +11 -0
  35. package/dist/modules/lawbook/assets/skills/draft/steps/06-hand-off.md +5 -0
  36. package/dist/modules/lawbook/assets/skills/explore/SKILL.md +6 -22
  37. package/dist/modules/lawbook/assets/skills/explore/steps/01-investigate.md +16 -0
  38. package/dist/modules/lawbook/assets/skills/explore/steps/02-summarize.md +7 -0
  39. package/dist/modules/lawbook/assets/skills/sync/SKILL.md +1 -30
  40. package/dist/modules/lawbook/assets/skills/sync/steps/01-confirm.md +5 -0
  41. package/dist/modules/lawbook/assets/skills/sync/steps/02-reconcile.md +16 -0
  42. package/dist/modules/lawbook/assets/skills/sync/steps/03-validate.md +6 -0
  43. package/dist/modules/lawbook/assets/skills/sync/steps/04-promote.md +10 -0
  44. package/dist/modules/lawbook/assets/skills/sync/steps/05-report.md +7 -0
  45. package/dist/modules/lawbook/register.js +17 -36
  46. package/dist/modules/tools/register.js +15 -15
  47. package/dist/server.js +13 -7
  48. package/dist/shared/budget.js +159 -0
  49. package/dist/shared/exposure.js +110 -0
  50. package/dist/shared/manifest.js +11 -2
  51. package/dist/shared/mcp.js +33 -0
  52. package/dist/shared/schema-tokens.js +86 -0
  53. package/dist/shared/tokens.js +41 -0
  54. package/package.json +2 -1
@@ -0,0 +1,5 @@
1
+ # Ensure the workspace exists
2
+
3
+ If `lawbook/` is missing, run the `lawbook_init` tool once to create it.
4
+
5
+ Next: read `steps/02-understand.md` and do only what it says.
@@ -0,0 +1,12 @@
1
+ # Understand the request and the code
2
+
3
+ - **Refresh the index first.** Run `compass_index` before reasoning about the
4
+ code — it is incremental (unchanged files are skipped by hash), so this is
5
+ cheap and guarantees your decisions rest on the current graph, not a stale one.
6
+ - Clarify what the user wants (feature / fix / refactor) and confirm scope.
7
+ - Use `compass_explore` and `compass_recall` (speclaw's code index) BEFORE
8
+ grep/read to locate the real code the change touches and its blast radius.
9
+ - Read the governing standards in `docs/standards/` (architecture, backend,
10
+ frontend, testing) so the change complies with the project's law.
11
+
12
+ Next: read `steps/03-name-capabilities.md` and do only what it says.
@@ -0,0 +1,14 @@
1
+ # Pick a change name and its capabilities
2
+
3
+ - **Change name:** kebab-case, action-oriented (e.g. `add-login`,
4
+ `fix-shift-overlap`). This is the folder under `lawbook/changes/`, and it is
5
+ per-feature — always distinct.
6
+ - **Capabilities:** run `lawbook_list` to see the canonical capabilities. A
7
+ capability is the living contract for an area of behavior — it is *not* the
8
+ change. When your change modifies behavior an existing capability already
9
+ governs, reuse that capability's **exact** name so `sync` updates its spec.
10
+ Introduce a new capability only as a deliberate choice for a genuinely distinct
11
+ area of behavior — never as a near-duplicate (`transfer` next to an existing
12
+ `transfers`) of one that already exists.
13
+
14
+ Next: read `steps/04-write-artifacts.md` and do only what it says.
@@ -0,0 +1,41 @@
1
+ # Write the artifacts
2
+
3
+ Create under `lawbook/changes/<name>/`:
4
+
5
+ - **proposal.md** — the why, the what, non-goals, and whether migrations are
6
+ needed. Reference the team's tracker ticket if there is one.
7
+ - **specs/<capability>/spec.md** — the delta spec for each affected capability.
8
+ `sync` promotes this by overwriting the whole canonical file, so the delta must
9
+ carry the capability's **full** intended spec. When you are updating an existing
10
+ capability, **start from the current `lawbook/specs/<capability>/spec.md`** and
11
+ edit on top of it, so its existing requirements are carried forward — do not
12
+ author it from scratch, or promotion will silently drop them. Use normative
13
+ language and testable scenarios:
14
+ ```markdown
15
+ # <Capability>
16
+
17
+ ### Requirement: <name>
18
+ The system SHALL <requirement>.
19
+
20
+ #### Scenario: <name>
21
+ - Given <context>
22
+ - When <action>
23
+ - Then <observable outcome>
24
+ ```
25
+ - **design.md** — always: approach, alternatives weighed, and the trade-offs
26
+ behind the decision. For a small change, keep it short — but write it.
27
+ - **tasks.md** — ordered, checkable steps. MUST include the mandatory steps
28
+ from `lawbook/config.yaml` (feature branch first; tests reviewed and run;
29
+ manual verification executed by the agent; discipline reports produced; docs
30
+ updated; archive within the PR).
31
+ - **reports/** — create the folder with a short `reports/README.md` naming the
32
+ discipline reports the change will need — one per discipline it touches, from an
33
+ open set (`backend.md`, `frontend.md`, `api.md`, `database.md`, `infra.md`,
34
+ `security.md`, … — and `api.md` is required when the change touches any API
35
+ surface) that `build` will fill, following the required report structure
36
+ (header · gates table · tests added · spec-scenario coverage · pre-existing
37
+ failures · pending manual · verdict — see the `build` skill's discipline-reports
38
+ step). Every change ships this folder; archive is blocked until it holds at
39
+ least one discipline report.
40
+
41
+ Next: read `steps/05-validate.md` and do only what it says.
@@ -0,0 +1,11 @@
1
+ # Validate
2
+
3
+ Run the `lawbook_validate` tool for the change and fix every issue it reports
4
+ (missing artifacts, non-normative specs, missing scenarios) before handing off
5
+ to implementation. Read its advisory **warnings** too: a near-duplicate
6
+ capability name usually means you should reuse the existing capability's exact
7
+ name, and a dropped-requirement warning means the delta should start from the
8
+ canonical. Warnings do not block, but resolve them unless the divergence is
9
+ intentional.
10
+
11
+ Next: read `steps/06-hand-off.md` and do only what it says.
@@ -0,0 +1,5 @@
1
+ # Hand off
2
+
3
+ Summarize the change and tell the user it's ready to `build`.
4
+
5
+ No further steps remain — draft workflow complete.
@@ -5,27 +5,11 @@ description: Enter explore mode — a thinking partner for investigating an idea
5
5
 
6
6
  # explore — Think it through
7
7
 
8
- A low-commitment mode for investigating ideas and the codebase before (or
9
- during) a change. Nothing is written to `lawbook/` here — the output is shared
10
- understanding and a recommended direction.
8
+ A low-commitment thinking partner for investigating ideas and the codebase
9
+ before (or during) a change. Nothing is written to `lawbook/` — the output is
10
+ shared understanding and a recommended direction.
11
11
 
12
- ## How to explore
12
+ Use when the user wants to think something through, understand how something
13
+ works, or clarify requirements before drafting a change.
13
14
 
14
- - **Refresh the index first.** Run `compass_index` before investigating it is
15
- incremental (unchanged files skipped by hash), so it is cheap and keeps your
16
- reasoning on the current graph rather than a stale one.
17
- - **Understand the code first.** Use `compass_recall` to find relevant code by
18
- meaning and `compass_explore` to read a symbol's source plus its callers and
19
- callees — before grep/read.
20
- - **Ask sharp questions** to surface hidden assumptions, constraints, and edge
21
- cases. Confirm scope and non-goals.
22
- - **Check the law.** Read the relevant `docs/standards/` so any direction you
23
- propose already fits the project's architecture and conventions.
24
- - **Weigh approaches.** Lay out the viable options with trade-offs and give a
25
- recommendation, not an exhaustive survey.
26
-
27
- ## Output
28
-
29
- A concise summary: the problem as understood, the constraints, the recommended
30
- approach, and the open questions. When the direction is clear, offer to `draft`
31
- the change.
15
+ Read `steps/01-investigate.md` and do only what it says.
@@ -0,0 +1,16 @@
1
+ # Investigate
2
+
3
+ - **Refresh the index first.** Run `compass_index` before investigating — it is
4
+ incremental (unchanged files skipped by hash), so it is cheap and keeps your
5
+ reasoning on the current graph rather than a stale one.
6
+ - **Understand the code first.** Use `compass_recall` to find relevant code by
7
+ meaning and `compass_explore` to read a symbol's source plus its callers and
8
+ callees — before grep/read.
9
+ - **Ask sharp questions** to surface hidden assumptions, constraints, and edge
10
+ cases. Confirm scope and non-goals.
11
+ - **Check the law.** Read the relevant `docs/standards/` so any direction you
12
+ propose already fits the project's architecture and conventions.
13
+ - **Weigh approaches.** Lay out the viable options with trade-offs and give a
14
+ recommendation, not an exhaustive survey.
15
+
16
+ Next: read `steps/02-summarize.md` and do only what it says.
@@ -0,0 +1,7 @@
1
+ # Summarize
2
+
3
+ Produce a concise summary: the problem as understood, the constraints, the
4
+ recommended approach, and the open questions. When the direction is clear,
5
+ offer to `draft` the change.
6
+
7
+ No further steps remain — explore workflow complete.
@@ -13,33 +13,4 @@ become the source of truth but the change isn't finished (e.g. multi-PR work).
13
13
  promoting, YOU reconcile the delta specs against what was actually built, so the
14
14
  specs that become canonical describe reality, not just the original draft.
15
15
 
16
- ## Steps
17
-
18
- 1. Confirm which change to sync (run `lawbook_list` if unsure).
19
-
20
- 2. **Reconcile code → delta specs (agent-executed).** Before promoting, compare
21
- what was built against the change's delta specs:
22
- - Reconstruct what shipped: `git diff <branch-point>...HEAD` for the change's
23
- branch, then `compass_explore` / `compass_impact` on the touched symbols to
24
- understand behavior, not just changed lines.
25
- - Diff intent vs reality: list behavior that is implemented but missing from,
26
- or contradicted by, `lawbook/changes/<name>/specs/**`.
27
- - Write the gaps into the delta specs — normative `SHALL`/`MUST` requirements
28
- under `### Requirement:` and `#### Scenario:` acceptance criteria — so the
29
- contract matches what was built. Capture only behavior that actually
30
- exists; never invent scope that was not implemented.
31
- - If nothing drifted, make no edits and say so.
32
-
33
- 3. Run `lawbook_validate` for the change; do not sync a change whose specs are
34
- invalid.
35
-
36
- 4. Run the `lawbook_sync` tool for the change. It copies each
37
- `lawbook/changes/<name>/specs/<capability>/spec.md` over the canonical
38
- `lawbook/specs/<capability>/spec.md` and reports what it promoted, flagging
39
- each as **created** (new capability) or **updated** (overwrote an existing
40
- one). A capability you expected to update showing up as *created* means the
41
- delta forked a near-duplicate — fix the name before promoting.
42
-
43
- 5. Report to the user what you reconciled (or that nothing drifted) and the
44
- promoted files (created vs updated). The change stays active — `archive` it
45
- when it's fully done.
16
+ Read `steps/01-confirm.md` and do only what it says.
@@ -0,0 +1,5 @@
1
+ # Confirm which change to sync
2
+
3
+ Confirm which change to sync (run `lawbook_list` if unsure).
4
+
5
+ Next: read `steps/02-reconcile.md` and do only what it says.
@@ -0,0 +1,16 @@
1
+ # Reconcile code → delta specs (agent-executed)
2
+
3
+ Before promoting, compare what was built against the change's delta specs:
4
+
5
+ - Reconstruct what shipped: `git diff <branch-point>...HEAD` for the change's
6
+ branch, then `compass_explore` / `compass_impact` on the touched symbols to
7
+ understand behavior, not just changed lines.
8
+ - Diff intent vs reality: list behavior that is implemented but missing from,
9
+ or contradicted by, `lawbook/changes/<name>/specs/**`.
10
+ - Write the gaps into the delta specs — normative `SHALL`/`MUST` requirements
11
+ under `### Requirement:` and `#### Scenario:` acceptance criteria — so the
12
+ contract matches what was built. Capture only behavior that actually
13
+ exists; never invent scope that was not implemented.
14
+ - If nothing drifted, make no edits and say so.
15
+
16
+ Next: read `steps/03-validate.md` and do only what it says.
@@ -0,0 +1,6 @@
1
+ # Validate
2
+
3
+ Run `lawbook_validate` for the change; do not sync a change whose specs are
4
+ invalid.
5
+
6
+ Next: read `steps/04-promote.md` and do only what it says.
@@ -0,0 +1,10 @@
1
+ # Promote
2
+
3
+ Run the `lawbook_sync` tool for the change. It copies each
4
+ `lawbook/changes/<name>/specs/<capability>/spec.md` over the canonical
5
+ `lawbook/specs/<capability>/spec.md` and reports what it promoted, flagging
6
+ each as **created** (new capability) or **updated** (overwrote an existing
7
+ one). A capability you expected to update showing up as *created* means the
8
+ delta forked a near-duplicate — fix the name before promoting.
9
+
10
+ Next: read `steps/05-report.md` and do only what it says.
@@ -0,0 +1,7 @@
1
+ # Report
2
+
3
+ Report to the user what you reconciled (or that nothing drifted) and the
4
+ promoted files (created vs updated). The change stays active — `archive` it
5
+ when it's fully done.
6
+
7
+ No further steps remain — sync workflow complete.
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { z } from "zod";
3
- import { text } from "../../shared/mcp.js";
3
+ import { defineTool, text } from "../../shared/mcp.js";
4
+ import { shouldExpose } from "../../shared/exposure.js";
4
5
  import { assetsDir } from "../../shared/paths.js";
5
6
  import { copyRendered } from "../../shared/install.js";
6
7
  import { specInit, specValidate, specSync, specArchive, specList } from "./engine.js";
@@ -16,41 +17,21 @@ export function installWorkflow(projectPath, vars, report, opts) {
16
17
  copyRendered(path.join(ASSETS, "commands"), path.join(aiSpecs, "commands", "lawbook"), vars, report, opts);
17
18
  copyRendered(path.join(ASSETS, "rules"), path.join(aiSpecs, "rules"), vars, report, opts);
18
19
  }
19
- // ─── The spec module: speclaw's own spec-driven workflow (no external OpenSpec) ───
20
- // Mechanical operations behind the draft/build/sync/archive/explore commands.
21
20
  /** Register the spec workflow MCP tools (init, list, validate, sync, archive). */
22
- export function registerSpec(server) {
23
- server.registerTool("lawbook_init", {
24
- description: "Initialize speclaw's spec-driven workflow in a project: creates lawbook/ (specs/, changes/, changes/archive/, config.yaml, README). Idempotent — never overwrites existing files.",
25
- inputSchema: { projectPath: z.string().describe("Absolute path to the project") },
26
- }, async ({ projectPath }) => text(specInit(projectPath)));
27
- server.registerTool("lawbook_list", {
28
- description: "List the spec workspace: active changes, archived changes, and canonical capabilities under spec/.",
29
- inputSchema: { projectPath: z.string().describe("Absolute path to the project") },
30
- }, async ({ projectPath }) => text(specList(projectPath)));
31
- server.registerTool("lawbook_validate", {
32
- description: "Validate a change's artifacts: proposal.md and tasks.md present, and delta specs use normative language (SHALL/MUST), '### Requirement:' headers, and '#### Scenario:' acceptance criteria. Returns the blocking issues to fix plus advisory (non-blocking) warnings a capability name that resembles an existing canonical one, or requirements dropped versus the canonical. Used by the draft/build commands before proceeding.",
33
- inputSchema: {
34
- projectPath: z.string().describe("Absolute path to the project"),
35
- change: z.string().describe("Change name (folder under lawbook/changes/)"),
36
- },
37
- }, async ({ projectPath, change }) => text(specValidate(projectPath, change)));
38
- server.registerTool("lawbook_sync", {
39
- description: "Promote a change's delta specs into the canonical lawbook/specs/ (per capability), without archiving. Reports each promoted spec as created (new capability) or updated (overwrote an existing one). Backs the `sync` command.",
40
- inputSchema: {
41
- projectPath: z.string().describe("Absolute path to the project"),
42
- change: z.string().describe("Change name (folder under lawbook/changes/)"),
43
- },
44
- }, async ({ projectPath, change }) => text(specSync(projectPath, change)));
45
- server.registerTool("lawbook_archive", {
46
- description: "Finalize a change: sync its delta specs into lawbook/specs/, then move it to lawbook/changes/archive/<date>-<name>/. Backs the `archive` command. Pass today's date as YYYY-MM-DD.",
47
- inputSchema: {
48
- projectPath: z.string().describe("Absolute path to the project"),
49
- change: z.string().describe("Change name (folder under lawbook/changes/)"),
50
- date: z
51
- .string()
52
- .regex(/^\d{4}-\d{2}-\d{2}$/)
53
- .describe("Today's date, YYYY-MM-DD"),
54
- },
21
+ export function registerSpec(server, opts = {}) {
22
+ const minimal = Boolean(opts.minimal);
23
+ const add = (name, description, inputSchema, handler) => {
24
+ if (!shouldExpose(name, minimal))
25
+ return;
26
+ defineTool(server, { name, description, inputSchema, handler });
27
+ };
28
+ add("lawbook_init", "Create the lawbook/ workspace (specs, changes, archive, config). Idempotent.", { projectPath: z.string() }, async ({ projectPath }) => text(specInit(projectPath)));
29
+ add("lawbook_list", "List active changes, archives, and canonical capabilities under lawbook/.", { projectPath: z.string() }, async ({ projectPath }) => text(specList(projectPath)));
30
+ add("lawbook_validate", "Validate a change's proposal, tasks, and delta specs before build or sync.", { projectPath: z.string(), change: z.string() }, async ({ projectPath, change }) => text(specValidate(projectPath, change)));
31
+ add("lawbook_sync", "Promote a change's delta specs into canonical lawbook/specs/ without archiving.", { projectPath: z.string(), change: z.string() }, async ({ projectPath, change }) => text(specSync(projectPath, change)));
32
+ add("lawbook_archive", "Sync a change into canonical specs, then move it under changes/archive/.", {
33
+ projectPath: z.string(),
34
+ change: z.string(),
35
+ date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
55
36
  }, async ({ projectPath, change, date }) => text(specArchive(projectPath, change, date)));
56
37
  }
@@ -1,27 +1,27 @@
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 { emptyReport } from "../../shared/install.js";
4
5
  import { refreshAgents } from "../../shared/agents.js";
5
6
  import { loadPacks, installPack } from "./packs.js";
6
7
  export { loadPacks, installPack };
7
- // ─── The tools module: opt-in packs of skills, agents, and commands ───
8
8
  /** Register the tools-module MCP tools (list_packs, add_pack). */
9
- export function registerTools(server) {
10
- server.registerTool("list_packs", {
11
- description: "List the available speclaw skill packs and what each contains.",
12
- inputSchema: {},
13
- }, async () => text(loadPacks()));
14
- server.registerTool("add_pack", {
15
- description: "Add a single pack to an already-initialized project, then refresh IDE symlinks. Pass template vars (organization, ...) if the pack needs them.",
16
- inputSchema: {
17
- projectPath: z.string().describe("Absolute path to the project"),
18
- pack: z.string().describe("Pack name (see list_packs)"),
19
- vars: z.record(z.string()).optional().describe("Template variables for rendering"),
20
- },
9
+ export function registerTools(server, opts = {}) {
10
+ const minimal = Boolean(opts.minimal);
11
+ const add = (name, description, inputSchema, handler) => {
12
+ if (!shouldExpose(name, minimal))
13
+ return;
14
+ defineTool(server, { name, description, inputSchema, handler });
15
+ };
16
+ add("list_packs", "List available speclaw skill packs and what each contains.", {}, async () => text(loadPacks()));
17
+ add("add_pack", "Install one pack into an initialized project and refresh IDE symlinks.", {
18
+ projectPath: z.string(),
19
+ pack: z.string(),
20
+ vars: z.record(z.string()).optional(),
21
21
  }, async ({ projectPath, pack, vars }) => {
22
22
  const report = emptyReport();
23
23
  installPack(projectPath, pack, vars ?? {}, report);
24
- refreshAgents(projectPath, report); // link the new content into already-configured agents
24
+ refreshAgents(projectPath, report);
25
25
  return text(report);
26
26
  });
27
27
  }
package/dist/server.js CHANGED
@@ -4,14 +4,20 @@ import { registerCompass } from "./modules/compass/register.js";
4
4
  import { registerSpec } from "./modules/lawbook/register.js";
5
5
  import { registerFoundation } from "./modules/foundation/register.js";
6
6
  import { registerTools } from "./modules/tools/register.js";
7
- /** Build the speclaw MCP server with every module's tools registered. */
8
- export function buildServer() {
7
+ import { isMinimalMode } from "./shared/exposure.js";
8
+ /**
9
+ * Build the speclaw MCP server with every module's tools registered.
10
+ *
11
+ * @param opts - Optional exposure overrides; defaults to {@link isMinimalMode}.
12
+ */
13
+ export function buildServer(opts = {}) {
14
+ const minimal = opts.minimal ?? isMinimalMode();
9
15
  const server = new McpServer({ name: "speclaw", version: "0.1.0" });
10
- // Each module contributes its own MCP tools. Adding a module = one line here.
11
- registerFoundation(server); // init_project, scaffold, configure_agent, doctor
12
- registerSpec(server); // lawbook_init, lawbook_validate, lawbook_sync, lawbook_archive, lawbook_list
13
- registerCompass(server); // compass_index, explore, search, recall, impact, trace, watch
14
- registerTools(server); // list_packs, add_pack
16
+ const reg = { minimal };
17
+ registerFoundation(server, reg);
18
+ registerSpec(server, reg);
19
+ registerCompass(server, reg);
20
+ registerTools(server, reg);
15
21
  return server;
16
22
  }
17
23
  /** Start the MCP server over stdio (used by `speclaw mcp`). */
@@ -0,0 +1,159 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { isMinimalMode, loadDeclaredBudget, packageRoot } from "./exposure.js";
4
+ import { toolDefinitionTokens } from "./schema-tokens.js";
5
+ import { estimateTokens } from "./tokens.js";
6
+ /**
7
+ * Measure always-on context cost across the four surfaces.
8
+ *
9
+ * @param opts - Paths and optional pre-collected tool definitions.
10
+ * @returns Structured measurement; `total` excludes path-scoped surface D.
11
+ */
12
+ export function measureBudget(opts = {}) {
13
+ const projectPath = opts.projectPath ?? process.cwd();
14
+ const packagePath = opts.packagePath ?? packageRoot();
15
+ const minimal = opts.minimal ?? isMinimalMode(projectPath);
16
+ const toolDefs = opts.tools ?? [];
17
+ const toolDetails = toolDefs.map((t) => ({
18
+ name: t.name,
19
+ tokens: toolDefinitionTokens(t),
20
+ }));
21
+ const tools = toolDetails.reduce((s, t) => s + t.tokens, 0);
22
+ const skillRoots = [
23
+ path.join(packagePath, "src/modules/lawbook/assets/skills"),
24
+ path.join(packagePath, "dist/modules/lawbook/assets/skills"),
25
+ path.join(projectPath, "ai-specs/skills"),
26
+ ];
27
+ const commandRoots = [
28
+ path.join(packagePath, "src/modules/lawbook/assets/commands"),
29
+ path.join(packagePath, "dist/modules/lawbook/assets/commands"),
30
+ path.join(projectPath, "ai-specs/commands"),
31
+ ];
32
+ const skillFiles = firstExistingFiles(skillRoots, (dir) => collectSkillBudgetFiles(dir));
33
+ const commandFiles = firstExistingFiles(commandRoots, (dir) => listFilesRecursive(dir).filter((f) => f.endsWith(".md")));
34
+ const scDetails = [...skillFiles, ...commandFiles].map((f) => ({
35
+ path: path.relative(packagePath, f) || path.relative(projectPath, f) || f,
36
+ tokens: estimateTokens(safeRead(f)),
37
+ }));
38
+ const skillsAndCommands = scDetails.reduce((s, x) => s + x.tokens, 0);
39
+ const alwaysOnPaths = ["CLAUDE.md", "AGENTS.md", "LAWS.md", "docs/compass.md"].map((rel) => path.join(projectPath, rel));
40
+ const alwaysOnDetails = alwaysOnPaths
41
+ .filter((p) => fs.existsSync(p))
42
+ .map((p) => ({
43
+ path: path.relative(projectPath, p),
44
+ tokens: estimateTokens(safeRead(p)),
45
+ }));
46
+ const alwaysOnInstructions = alwaysOnDetails.reduce((s, x) => s + x.tokens, 0);
47
+ const pathScopedDetails = collectPathScoped(projectPath);
48
+ const pathScoped = pathScopedDetails.reduce((s, x) => s + x.tokens, 0);
49
+ return {
50
+ tools,
51
+ skillsAndCommands,
52
+ alwaysOnInstructions,
53
+ pathScoped,
54
+ total: tools + skillsAndCommands + alwaysOnInstructions,
55
+ profile: minimal ? "minimal" : "full",
56
+ toolCount: toolDefs.length,
57
+ details: {
58
+ tools: toolDetails,
59
+ skillsAndCommands: scDetails,
60
+ alwaysOn: alwaysOnDetails,
61
+ pathScoped: pathScopedDetails,
62
+ },
63
+ };
64
+ }
65
+ /**
66
+ * Format a human-readable budget table.
67
+ *
68
+ * @param m - Measurement.
69
+ * @param declared - Optional declared ceilings for an ok/over column.
70
+ */
71
+ export function formatBudgetTable(m, declared) {
72
+ const d = declared ?? loadDeclaredBudget();
73
+ const row = (label, tokens, cap) => {
74
+ const capStr = cap === null ? "—".padStart(8) : String(cap).padStart(8);
75
+ const status = cap === null ? "" : tokens <= cap ? " ok" : " OVER";
76
+ return `${label.padEnd(40)} ${String(tokens).padStart(8)} ${capStr}${status}`;
77
+ };
78
+ const lines = [
79
+ "Superficie tokens presupuesto",
80
+ row(`A tools MCP (${m.toolCount}, ${m.profile})`, m.tools, d.surfaces.tools),
81
+ row("B skills + commands", m.skillsAndCommands, d.surfaces.skillsAndCommands),
82
+ row("C always-on instructions", m.alwaysOnInstructions, d.surfaces.alwaysOnInstructions),
83
+ row("D path-scoped rules", m.pathScoped, null),
84
+ `${"".padEnd(40)} ${"──────".padStart(8)} ${"──────".padStart(8)}`,
85
+ row("TOTAL always-on", m.total, m.profile === "minimal" ? d.minimal.total : d.total),
86
+ ];
87
+ return lines.join("\n");
88
+ }
89
+ function safeRead(file) {
90
+ try {
91
+ return fs.readFileSync(file, "utf8");
92
+ }
93
+ catch {
94
+ return "";
95
+ }
96
+ }
97
+ function listFilesRecursive(dir) {
98
+ if (!fs.existsSync(dir))
99
+ return [];
100
+ const out = [];
101
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
102
+ const full = path.join(dir, entry.name);
103
+ if (entry.isDirectory())
104
+ out.push(...listFilesRecursive(full));
105
+ else
106
+ out.push(full);
107
+ }
108
+ return out;
109
+ }
110
+ /** Skill budget: dispatcher SKILL.md + only that counts for B's "loaded at turn" description;
111
+ * for always-present skill index we count SKILL.md bodies (dispatchers) fully and step files
112
+ * are loaded on demand — still include step files in B per design (commands full; skills
113
+ * frontmatter+description each turn). For simplicity and honesty we count entire SKILL.md
114
+ * (dispatcher) and do NOT count steps/ toward always-on B (JIT). */
115
+ function collectSkillBudgetFiles(skillsRoot) {
116
+ if (!fs.existsSync(skillsRoot))
117
+ return [];
118
+ const files = [];
119
+ for (const entry of fs.readdirSync(skillsRoot, { withFileTypes: true })) {
120
+ if (!entry.isDirectory())
121
+ continue;
122
+ const skillMd = path.join(skillsRoot, entry.name, "SKILL.md");
123
+ if (fs.existsSync(skillMd))
124
+ files.push(skillMd);
125
+ }
126
+ return files;
127
+ }
128
+ function firstExistingFiles(roots, collect) {
129
+ for (const root of roots) {
130
+ if (!fs.existsSync(root))
131
+ continue;
132
+ const files = collect(root);
133
+ if (files.length)
134
+ return files;
135
+ }
136
+ return [];
137
+ }
138
+ function collectPathScoped(projectPath) {
139
+ const details = [];
140
+ const ruleDirs = [
141
+ path.join(projectPath, ".claude/rules"),
142
+ path.join(projectPath, "ai-specs/rules"),
143
+ ];
144
+ for (const dir of ruleDirs) {
145
+ if (!fs.existsSync(dir))
146
+ continue;
147
+ for (const f of listFilesRecursive(dir).filter((p) => p.endsWith(".md"))) {
148
+ const body = safeRead(f);
149
+ // Heuristic: frontmatter with paths: means path-scoped lazy load.
150
+ if (/^---[\s\S]*?^paths:\s*$/m.test(body) || /^---[\s\S]*?^paths:/m.test(body)) {
151
+ details.push({
152
+ path: path.relative(projectPath, f),
153
+ tokens: estimateTokens(body),
154
+ });
155
+ }
156
+ }
157
+ }
158
+ return details;
159
+ }