@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
|
@@ -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.
|
|
@@ -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
|
|
9
|
-
during) a change. Nothing is written to `lawbook/`
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
@@ -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
|
-
|
|
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,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,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.
|
|
@@ -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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}, async ({ projectPath }) => text(specList(projectPath)));
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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);
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
registerFoundation(server);
|
|
12
|
-
registerSpec(server);
|
|
13
|
-
registerCompass(server);
|
|
14
|
-
registerTools(server);
|
|
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
|
+
}
|