@esneiderbravo/speclaw 0.3.12 → 0.4.0

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 (49) hide show
  1. package/README.md +2 -2
  2. package/dist/cli/commands/lawbook.js +42 -1
  3. package/dist/cli/commands/query.js +20 -0
  4. package/dist/cli/commands/update.js +10 -0
  5. package/dist/cli/index.js +8 -0
  6. package/dist/modules/compass/diff-context.js +134 -0
  7. package/dist/modules/compass/explore-rich.js +129 -0
  8. package/dist/modules/compass/impact-summary.js +33 -0
  9. package/dist/modules/compass/register.js +164 -74
  10. package/dist/modules/foundation/context-budget.js +1 -14
  11. package/dist/modules/foundation/doctor.js +77 -0
  12. package/dist/modules/foundation/register-core.js +57 -88
  13. package/dist/modules/foundation/register.js +1 -21
  14. package/dist/modules/foundation/setup-tool.js +96 -0
  15. package/dist/modules/lawbook/assets/commands/archive.md +1 -1
  16. package/dist/modules/lawbook/assets/commands/draft.md +1 -1
  17. package/dist/modules/lawbook/assets/commands/explore.md +1 -1
  18. package/dist/modules/lawbook/assets/commands/investigate.md +7 -0
  19. package/dist/modules/lawbook/assets/commands/sync.md +2 -2
  20. package/dist/modules/lawbook/assets/rules/spec-reports-disciplines.md +8 -0
  21. package/dist/modules/lawbook/assets/skills/archive/SKILL.md +1 -1
  22. package/dist/modules/lawbook/assets/skills/archive/steps/03-validate-and-sync.md +3 -3
  23. package/dist/modules/lawbook/assets/skills/archive/steps/04-archive.md +1 -1
  24. package/dist/modules/lawbook/assets/skills/draft/steps/02-understand.md +1 -1
  25. package/dist/modules/lawbook/assets/skills/draft/steps/04-write-artifacts.md +1 -0
  26. package/dist/modules/lawbook/assets/skills/draft/steps/05-validate.md +1 -1
  27. package/dist/modules/lawbook/assets/skills/explore/steps/01-investigate.md +1 -1
  28. package/dist/modules/lawbook/assets/skills/investigate/SKILL.md +10 -0
  29. package/dist/modules/lawbook/assets/skills/investigate/steps/01-investigate.md +7 -0
  30. package/dist/modules/lawbook/assets/skills/investigate/steps/02-hand-off.md +6 -0
  31. package/dist/modules/lawbook/assets/skills/quick/steps/02-implement.md +1 -1
  32. package/dist/modules/lawbook/assets/skills/sync/SKILL.md +1 -1
  33. package/dist/modules/lawbook/assets/skills/sync/steps/03-validate.md +1 -1
  34. package/dist/modules/lawbook/assets/skills/sync/steps/04-promote.md +1 -1
  35. package/dist/modules/lawbook/bugfix.js +195 -0
  36. package/dist/modules/lawbook/change-tool.js +90 -0
  37. package/dist/modules/lawbook/engine.js +70 -7
  38. package/dist/modules/lawbook/investigate.js +358 -0
  39. package/dist/modules/lawbook/levels.js +49 -2
  40. package/dist/modules/lawbook/register.js +102 -52
  41. package/dist/modules/lawbook/stack-parse.js +135 -0
  42. package/dist/modules/tools/register.js +4 -26
  43. package/dist/shared/deprecation.js +99 -0
  44. package/dist/shared/exposure.js +5 -19
  45. package/dist/shared/git.js +25 -0
  46. package/dist/shared/mcp.js +29 -3
  47. package/dist/shared/output-budget.js +68 -0
  48. package/dist/shared/tool-catalog.js +49 -0
  49. package/package.json +1 -1
@@ -1,19 +1,17 @@
1
1
  import { z } from "zod";
2
- import { defineTool, text } from "../../shared/mcp.js";
2
+ import { defineTool, defineAliasTool, text } from "../../shared/mcp.js";
3
3
  import { shouldExpose } from "../../shared/exposure.js";
4
+ import { aliasesEnabled } from "../../shared/tool-catalog.js";
5
+ import { logDeprecatedCall, prefixDeprecated } from "../../shared/deprecation.js";
4
6
  import { buildIndex } from "./indexer.js";
5
- import { explore, search, recall, impact, trace } from "./query.js";
7
+ import { impact } from "./query.js";
6
8
  import { affectedTests } from "./affected.js";
7
9
  import { hotspots, coupling } from "./hotspots.js";
8
10
  import { startWatch, stopWatch, watchStatus } from "./watcher.js";
9
- import { visualize } from "./visualize.js";
10
- // ─── Compass: speclaw's own code-intelligence engine (no external deps) ───
11
- /**
12
- * Register Compass MCP tools on the given server.
13
- *
14
- * @param server - The MCP server to register on.
15
- * @param opts - Exposure options (`minimal` omits setup/specialized tools).
16
- */
11
+ import { exploreRich, findSymbols, formatExploreRich, } from "./explore-rich.js";
12
+ import { diffContext, formatDiffContext } from "./diff-context.js";
13
+ const includeEnum = z.array(z.enum(["source", "callers", "callees", "blast_radius", "tests", "hotspot"]));
14
+ /** Register Compass MCP tools on the given server. */
17
15
  export function registerCompass(server, opts = {}) {
18
16
  const minimal = Boolean(opts.minimal);
19
17
  const add = (name, description, inputSchema, handler) => {
@@ -21,78 +19,170 @@ export function registerCompass(server, opts = {}) {
21
19
  return;
22
20
  defineTool(server, { name, description, inputSchema, handler });
23
21
  };
24
- add("compass_index", "Build or refresh the local code graph index. Run once per project, then on demand.", { projectPath: z.string() }, async ({ projectPath }) => text(await buildIndex(projectPath)));
25
- add("compass_explore", "Read a symbol's source plus callers and callees. Prefer this before grep or Read.", { projectPath: z.string(), node: z.string() }, async ({ projectPath, node }) => text(explore(projectPath, node)));
26
- add("compass_search", "Find symbols by name or keyword (substring). Cheaper structural search than grep.", { projectPath: z.string(), query: z.string(), limit: z.number().optional() }, async ({ projectPath, query, limit }) => text(search(projectPath, query, limit ?? 25)));
27
- add("compass_recall", "Find symbols by meaning via local embeddings. Use when names are unknown.", { projectPath: z.string(), query: z.string(), limit: z.number().optional() }, async ({ projectPath, query, limit }) => text(await recall(projectPath, query, limit ?? 15)));
28
- add("compass_impact", "Blast radius for a symbol or files, grouped by module (not a flat dump).", {
22
+ add("compass_explore", "Symbol context in one call: source, callers, callees, blast radius, tests, hotspot. Prefer before grep.", {
29
23
  projectPath: z.string(),
30
- /** @deprecated Prefer `symbol`. Kept for existing callers. */
31
- node: z.string().optional(),
32
- symbol: z.string().optional(),
33
- files: z.array(z.string()).optional(),
34
- nodeId: z.number().int().optional(),
35
- maxDepth: z.number().int().min(1).max(12).optional(),
36
- edgeKinds: z.array(z.enum(["call", "import"])).optional(),
37
- target: z.enum(["build", "test", "lint", "any"]).optional(),
38
- format: z.enum(["grouped", "flat"]).optional(),
39
- topModules: z.number().int().min(1).max(50).optional(),
40
- topPerModule: z.number().int().min(1).max(50).optional(),
41
- }, async (args) => text(impact(args.projectPath, {
42
- symbol: args.symbol ?? args.node,
43
- files: args.files,
44
- nodeId: args.nodeId,
45
- maxDepth: args.maxDepth ?? 4,
46
- edgeKinds: args.edgeKinds,
47
- target: args.target,
48
- format: args.format ?? "grouped",
49
- topModules: args.topModules,
50
- topPerModule: args.topPerModule,
51
- })));
52
- add("compass_affected_tests", "Select test files affected by a change; returns a ready-to-run command.", {
53
- projectPath: z.string(),
54
- files: z.array(z.string()).optional(),
55
- symbols: z.array(z.string()).optional(),
56
- fromDiff: z.string().optional(),
57
- maxDepth: z.number().int().min(1).max(12).optional(),
58
- }, async ({ projectPath, files, symbols, fromDiff, maxDepth }) => text(affectedTests(projectPath, { files, symbols, fromDiff, maxDepth })));
59
- add("compass_hotspots", "Rank files by recent churn and AST complexity; two axes, no magic score.", {
60
- projectPath: z.string(),
61
- days: z.number().int().min(1).max(3650).optional(),
62
- since: z.string().optional(),
63
- sortBy: z.enum(["churn", "complexity", "combined"]).optional(),
64
- limit: z.number().int().min(1).max(200).optional(),
65
- }, async ({ projectPath, days, since, sortBy, limit }) => text(hotspots(projectPath, { days, since, sortBy, limit })));
66
- add("compass_coupling", "Files that co-change with a target; strength, graph edge, and test-pair facts.", {
67
- projectPath: z.string(),
68
- file: z.string(),
69
- days: z.number().int().min(1).max(3650).optional(),
70
- since: z.string().optional(),
71
- minShared: z.number().int().min(1).optional(),
72
- maxFilesPerCommit: z.number().int().min(2).optional(),
73
- limit: z.number().int().min(1).max(200).optional(),
74
- }, async ({ projectPath, file, days, since, minShared, maxFilesPerCommit, limit }) => text(coupling(projectPath, file, { days, since, minShared, maxFilesPerCommit, limit })));
75
- add("compass_trace", "Find a call path between two symbols within a depth limit.", {
76
- projectPath: z.string(),
77
- from: z.string(),
78
- to: z.string(),
79
- maxDepth: z.number().optional(),
80
- }, async ({ projectPath, from, to, maxDepth }) => text(trace(projectPath, from, to, maxDepth ?? 8)));
81
- add("compass_visualize", "Write an offline HTML graph to .speclaw/graph.html for interactive exploration.", {
24
+ node: z.string(),
25
+ to: z.string().optional(),
26
+ include: includeEnum.optional(),
27
+ mode: z.enum(["brief", "full"]).optional(),
28
+ maxDepth: z.number().int().min(1).max(8).optional(),
29
+ }, async ({ projectPath, node, to, include, mode, maxDepth }) => {
30
+ const result = await exploreRich({
31
+ projectPath,
32
+ node,
33
+ to,
34
+ include: include,
35
+ mode: (mode ?? "brief"),
36
+ maxDepth,
37
+ });
38
+ return text(formatExploreRich(result, (mode ?? "brief")));
39
+ });
40
+ add("compass_find", "Find symbols by exact name or by concept. Use exact for identifiers, concept for meaning.", {
82
41
  projectPath: z.string(),
83
- node: z.string().optional(),
84
- depth: z.number().optional(),
42
+ query: z.string(),
43
+ mode: z.enum(["exact", "concept"]),
85
44
  limit: z.number().optional(),
86
- }, async ({ projectPath, node, depth, limit }) => text(visualize(projectPath, { focus: node, depth, limit })));
87
- add("compass_watch", "Start, stop, or status a debounced file watcher that re-indexes on change.", {
45
+ }, async ({ projectPath, query, mode, limit }) => text(await findSymbols(projectPath, query, mode, limit)));
46
+ add("compass_diff_context", "Graph context of changes in one call: symbols, blast radius, tests, hotspots. Default: working tree.", {
47
+ projectPath: z.string(),
48
+ rev: z.string().optional(),
49
+ paths: z.array(z.string()).optional(),
50
+ mode: z.enum(["brief", "full"]).optional(),
51
+ maxDepth: z.number().int().min(1).max(8).optional(),
52
+ }, async ({ projectPath, rev, paths, mode, maxDepth }) => {
53
+ const result = diffContext({
54
+ projectPath,
55
+ rev,
56
+ paths,
57
+ mode: (mode ?? "brief"),
58
+ maxDepth,
59
+ });
60
+ return text(formatDiffContext(result, (mode ?? "brief")));
61
+ });
62
+ add("compass_index", "Build or refresh the code graph index; optional watch action for live re-index.", {
88
63
  projectPath: z.string(),
89
- action: z.enum(["start", "stop", "status"]),
64
+ action: z.enum(["index", "start", "stop", "status"]).optional(),
90
65
  }, async ({ projectPath, action }) => {
91
- const result = action === "start"
66
+ const act = action ?? "index";
67
+ if (act === "index")
68
+ return text(await buildIndex(projectPath));
69
+ const result = act === "start"
92
70
  ? startWatch(projectPath)
93
- : action === "stop"
71
+ : act === "stop"
94
72
  ? stopWatch(projectPath)
95
73
  : watchStatus(projectPath);
96
74
  return text(result);
97
75
  });
76
+ if (minimal || !aliasesEnabled())
77
+ return;
78
+ defineAliasTool(server, {
79
+ name: "compass_search",
80
+ description: "Deprecated alias for compass_find mode exact.",
81
+ inputSchema: { projectPath: z.string(), query: z.string(), limit: z.number().optional() },
82
+ handler: async ({ projectPath, query, limit }) => {
83
+ logDeprecatedCall(projectPath, "compass_search");
84
+ const body = JSON.stringify(await findSymbols(projectPath, query, "exact", limit), null, 2);
85
+ return text(prefixDeprecated("compass_search", body));
86
+ },
87
+ });
88
+ defineAliasTool(server, {
89
+ name: "compass_recall",
90
+ description: "Deprecated alias for compass_find mode concept.",
91
+ inputSchema: { projectPath: z.string(), query: z.string(), limit: z.number().optional() },
92
+ handler: async ({ projectPath, query, limit }) => {
93
+ logDeprecatedCall(projectPath, "compass_recall");
94
+ const body = JSON.stringify(await findSymbols(projectPath, query, "concept", limit), null, 2);
95
+ return text(prefixDeprecated("compass_recall", body));
96
+ },
97
+ });
98
+ defineAliasTool(server, {
99
+ name: "compass_impact",
100
+ description: "Deprecated alias for compass_explore blast_radius include.",
101
+ inputSchema: {
102
+ projectPath: z.string(),
103
+ node: z.string().optional(),
104
+ symbol: z.string().optional(),
105
+ files: z.array(z.string()).optional(),
106
+ maxDepth: z.number().optional(),
107
+ },
108
+ handler: async (args) => {
109
+ logDeprecatedCall(args.projectPath, "compass_impact");
110
+ const sym = args.symbol ?? args.node;
111
+ const body = sym
112
+ ? JSON.stringify(await exploreRich({
113
+ projectPath: args.projectPath,
114
+ node: sym,
115
+ include: ["blast_radius"],
116
+ }), null, 2)
117
+ : JSON.stringify(impact(args.projectPath, { files: args.files, maxDepth: args.maxDepth ?? 4 }), null, 2);
118
+ return text(prefixDeprecated("compass_impact", body));
119
+ },
120
+ });
121
+ defineAliasTool(server, {
122
+ name: "compass_trace",
123
+ description: "Deprecated alias for compass_explore with to parameter.",
124
+ inputSchema: {
125
+ projectPath: z.string(),
126
+ from: z.string(),
127
+ to: z.string(),
128
+ maxDepth: z.number().optional(),
129
+ },
130
+ handler: async ({ projectPath, from, to, maxDepth }) => {
131
+ logDeprecatedCall(projectPath, "compass_trace");
132
+ const body = JSON.stringify(await exploreRich({ projectPath, node: from, to, maxDepth }), null, 2);
133
+ return text(prefixDeprecated("compass_trace", body));
134
+ },
135
+ });
136
+ defineAliasTool(server, {
137
+ name: "compass_affected_tests",
138
+ description: "Deprecated alias — use compass_diff_context or explore.",
139
+ inputSchema: {
140
+ projectPath: z.string(),
141
+ files: z.array(z.string()).optional(),
142
+ symbols: z.array(z.string()).optional(),
143
+ fromDiff: z.string().optional(),
144
+ },
145
+ handler: async (args) => {
146
+ logDeprecatedCall(args.projectPath, "compass_affected_tests");
147
+ const body = JSON.stringify(affectedTests(args.projectPath, args), null, 2);
148
+ return text(prefixDeprecated("compass_affected_tests", body));
149
+ },
150
+ });
151
+ defineAliasTool(server, {
152
+ name: "compass_hotspots",
153
+ description: "Deprecated alias — use compass_explore hotspot include.",
154
+ inputSchema: { projectPath: z.string(), limit: z.number().optional() },
155
+ handler: async ({ projectPath, limit }) => {
156
+ logDeprecatedCall(projectPath, "compass_hotspots");
157
+ const body = JSON.stringify(hotspots(projectPath, { limit }), null, 2);
158
+ return text(prefixDeprecated("compass_hotspots", body));
159
+ },
160
+ });
161
+ defineAliasTool(server, {
162
+ name: "compass_coupling",
163
+ description: "Deprecated alias — use compass_diff_context.",
164
+ inputSchema: { projectPath: z.string(), file: z.string() },
165
+ handler: async ({ projectPath, file }) => {
166
+ logDeprecatedCall(projectPath, "compass_coupling");
167
+ const body = JSON.stringify(coupling(projectPath, file, {}), null, 2);
168
+ return text(prefixDeprecated("compass_coupling", body));
169
+ },
170
+ });
171
+ defineAliasTool(server, {
172
+ name: "compass_watch",
173
+ description: "Deprecated alias for compass_index watch actions.",
174
+ inputSchema: {
175
+ projectPath: z.string(),
176
+ action: z.enum(["start", "stop", "status"]),
177
+ },
178
+ handler: async ({ projectPath, action }) => {
179
+ logDeprecatedCall(projectPath, "compass_watch");
180
+ const result = action === "start"
181
+ ? startWatch(projectPath)
182
+ : action === "stop"
183
+ ? stopWatch(projectPath)
184
+ : watchStatus(projectPath);
185
+ return text(prefixDeprecated("compass_watch", JSON.stringify(result, null, 2)));
186
+ },
187
+ });
98
188
  }
@@ -1,22 +1,12 @@
1
- import { z } from "zod";
2
1
  import { registerCompass } from "../compass/register.js";
3
2
  import { registerSpec } from "../lawbook/register.js";
4
3
  import { registerTools } from "../tools/register.js";
5
4
  import { registerFoundationCore } from "./register-core.js";
6
5
  import { measureBudget } from "../../shared/budget.js";
7
- import { isMinimalMode, packageRoot, shouldExpose } from "../../shared/exposure.js";
8
- /** Mirrors the `doctor` tool surface without importing `doctor.ts` (avoids a cycle). */
9
- const DOCTOR_TOOL_FOR_BUDGET = {
10
- name: "doctor",
11
- description: "Verify the speclaw install; returns a versioned DoctorReport (schemaVersion 1).",
12
- inputSchema: { projectPath: z.string() },
13
- };
6
+ import { isMinimalMode, packageRoot } from "../../shared/exposure.js";
14
7
  /**
15
8
  * Collect tool definitions as the MCP server would register them for a profile.
16
9
  *
17
- * Uses `registerFoundationCore` plus a static `doctor` stub so budget/doctor
18
- * measurement never imports the live `doctor` implementation (module cycle).
19
- *
20
10
  * @param minimal - Exposure profile.
21
11
  */
22
12
  export function collectRegisteredTools(minimal) {
@@ -33,9 +23,6 @@ export function collectRegisteredTools(minimal) {
33
23
  const opts = { minimal };
34
24
  const stub = server;
35
25
  registerFoundationCore(stub, opts);
36
- if (shouldExpose("doctor", minimal)) {
37
- tools.push(DOCTOR_TOOL_FOR_BUDGET);
38
- }
39
26
  registerSpec(stub, opts);
40
27
  registerCompass(stub, opts);
41
28
  registerTools(stub, opts);
@@ -11,6 +11,8 @@ import { doctorDriftCheck } from "../lawbook/drift.js";
11
11
  import { loadCeremonyConfig } from "../lawbook/levels.js";
12
12
  import { globError, hasBackend, hasBatchBackend, readLawManifest } from "./laws.js";
13
13
  import { redactValue } from "../../shared/redact.js";
14
+ import { readDeprecatedCallCounts, scanRetiredToolReferences } from "../../shared/deprecation.js";
15
+ import { CANONICAL_TOOLS, ALIAS_TARGETS, isCanonicalTool } from "../../shared/tool-catalog.js";
14
16
  const STATUS_RANK = {
15
17
  skip: 0,
16
18
  ok: 1,
@@ -397,6 +399,48 @@ async function budgetCheck(projectPath) {
397
399
  };
398
400
  }
399
401
  }
402
+ async function toolSurfaceCheck(projectPath) {
403
+ try {
404
+ const { measureInstallBudget, collectRegisteredTools } = await import("./context-budget.js");
405
+ const full = measureInstallBudget(projectPath, false);
406
+ const mini = measureInstallBudget(projectPath, true);
407
+ const canonicalCount = collectRegisteredTools(false).filter((t) => isCanonicalTool(t.name)).length;
408
+ const deprecated = readDeprecatedCallCounts(projectPath);
409
+ const aliasDetail = deprecated.size > 0
410
+ ? [...deprecated.entries()]
411
+ .map(([alias, n]) => `${alias}→${ALIAS_TARGETS[alias] ?? "?"} (${n}×)`)
412
+ .join("; ")
413
+ : "no deprecated alias calls logged";
414
+ const staleRefs = scanRetiredToolReferences(projectPath);
415
+ const staleDetail = staleRefs.length > 0
416
+ ? `retired names in: ${[...new Set(staleRefs.map((r) => `${r.file} (${r.alias}→${r.replacement})`))].join("; ")}`
417
+ : undefined;
418
+ return {
419
+ id: "cfg.tool-surface",
420
+ title: "MCP tool surface",
421
+ status: staleRefs.length > 0 ? "warn" : "ok",
422
+ value: canonicalCount,
423
+ detail: [
424
+ `${canonicalCount}/${CANONICAL_TOOLS.length} canonical tools`,
425
+ `~${full.tools} tool-definition tokens (full), ~${mini.tools} (minimal)`,
426
+ aliasDetail,
427
+ staleDetail,
428
+ ]
429
+ .filter(Boolean)
430
+ .join(" · "),
431
+ remedy: staleRefs.length > 0 ? "speclaw update" : undefined,
432
+ };
433
+ }
434
+ catch (err) {
435
+ return {
436
+ id: "cfg.tool-surface",
437
+ title: "MCP tool surface",
438
+ status: "skip",
439
+ detail: `could not measure: ${err.message}`,
440
+ remedy: "speclaw budget",
441
+ };
442
+ }
443
+ }
400
444
  function freshnessCheck(projectPath) {
401
445
  if (!indexExists(projectPath)) {
402
446
  return {
@@ -527,6 +571,37 @@ function ceremonyChecks(projectPath) {
527
571
  ? "no archived changes"
528
572
  : `archived levels: 0=${counts["0"]}, 1=${counts["1"]}, 2=${counts["2"]}, 3=${counts["3"]} (missing change.json=${counts.missing})`,
529
573
  });
574
+ const typeCounts = { feature: 0, bug: 0, unknown: 0 };
575
+ if (fs.existsSync(archiveRoot)) {
576
+ for (const name of fs.readdirSync(archiveRoot)) {
577
+ const dir = path.join(archiveRoot, name);
578
+ if (!fs.statSync(dir).isDirectory())
579
+ continue;
580
+ try {
581
+ const p = path.join(dir, "change.json");
582
+ if (!fs.existsSync(p)) {
583
+ typeCounts.unknown += 1;
584
+ typeCounts.feature += 1;
585
+ continue;
586
+ }
587
+ const raw = JSON.parse(fs.readFileSync(p, "utf8"));
588
+ if (raw.changeType === "bug")
589
+ typeCounts.bug += 1;
590
+ else
591
+ typeCounts.feature += 1;
592
+ }
593
+ catch {
594
+ typeCounts.unknown += 1;
595
+ }
596
+ }
597
+ }
598
+ out.push({
599
+ id: "cfg.ceremony.changeTypes",
600
+ title: "change type distribution",
601
+ status: "ok",
602
+ value: JSON.stringify(typeCounts),
603
+ detail: `archived types: feature=${typeCounts.feature}, bug=${typeCounts.bug}`,
604
+ });
530
605
  return out;
531
606
  }
532
607
  function configurationChecks(projectPath, initialised) {
@@ -538,6 +613,7 @@ function configurationChecks(projectPath, initialised) {
538
613
  "cfg.hooks",
539
614
  "cfg.laws",
540
615
  "cfg.budget",
616
+ "cfg.tool-surface",
541
617
  "cfg.index.freshness",
542
618
  "cfg.specs.orphans",
543
619
  ];
@@ -619,6 +695,7 @@ export async function doctor(projectPath, opts = {}) {
619
695
  const configuration = configurationChecks(projectPath, initialised);
620
696
  if (initialised) {
621
697
  configuration.push(await budgetCheck(projectPath));
698
+ configuration.push(await toolSurfaceCheck(projectPath));
622
699
  configuration.push(freshnessCheck(projectPath));
623
700
  configuration.push(specsOrphansCheck(projectPath));
624
701
  configuration.push(...ceremonyChecks(projectPath));
@@ -1,54 +1,10 @@
1
1
  import { z } from "zod";
2
- import { defineTool, text } from "../../shared/mcp.js";
2
+ import { defineTool, defineAliasTool, text } from "../../shared/mcp.js";
3
3
  import { shouldExpose } from "../../shared/exposure.js";
4
- import { scaffold } from "./scaffold.js";
4
+ import { aliasesEnabled } from "../../shared/tool-catalog.js";
5
+ import { logDeprecatedCall, prefixDeprecated } from "../../shared/deprecation.js";
5
6
  import { checkAction } from "./check.js";
6
- import { verifyLaws } from "./verify.js";
7
- import { loadPacks } from "../tools/packs.js";
8
- import { AGENTS, configureAgent } from "../../shared/agents.js";
9
- import { emptyReport } from "../../shared/install.js";
10
- /** Human help text for init_project's questionnaire (not embedded in MCP schemas). */
11
- const profileFieldHelp = {
12
- project_name: "Short project name, e.g. the repo name",
13
- project_description: "One-line description of what the project does",
14
- organization: "Company/team name",
15
- stack_summary: "e.g. 'Next.js 15 + TypeScript frontend, FastAPI + PostgreSQL backend'",
16
- architecture: "e.g. 'hexagonal architecture with bounded contexts'",
17
- test_commands: "Real commands, e.g. 'pytest backend/tests && npm run test'",
18
- lint_commands: "Real commands, e.g. 'ruff check . && npm run lint && tsc --noEmit'",
19
- branch_pattern: "e.g. 'feature/<ticket-id>-<slug>'",
20
- commit_style: "e.g. 'conventional commits, imperative, English'",
21
- custom_laws: "Extra markdown for LAWS.md — project-specific binding rules",
22
- compass_hints: "Markdown bullets with real entrypoints for docs/compass.md",
23
- base_standards_extra: "Extra cross-cutting rules for base-standards.md",
24
- modules_table: "Markdown table of modules/bounded contexts",
25
- layering_rules: "Layers and allowed dependencies for architecture.md",
26
- backend_layers: "Backend layer table for backend-standards.md",
27
- frontend_layers: "Frontend layer table for frontend-standards.md",
28
- versioning_rules: "Versioning/release convention for conventions.md",
29
- documentation_extra: "Repo-specific docstring notes for documentation.md",
30
- };
31
- /** Lean Zod shape for scaffold — no .describe() text (that cost rides in every request). */
32
- const profileShape = {
33
- project_name: z.string(),
34
- project_description: z.string().optional(),
35
- organization: z.string().optional(),
36
- stack_summary: z.string().optional(),
37
- architecture: z.string().optional(),
38
- test_commands: z.string().optional(),
39
- lint_commands: z.string().optional(),
40
- branch_pattern: z.string().optional(),
41
- commit_style: z.string().optional(),
42
- custom_laws: z.string().optional(),
43
- compass_hints: z.string().optional(),
44
- base_standards_extra: z.string().optional(),
45
- modules_table: z.string().optional(),
46
- layering_rules: z.string().optional(),
47
- backend_layers: z.string().optional(),
48
- frontend_layers: z.string().optional(),
49
- versioning_rules: z.string().optional(),
50
- documentation_extra: z.string().optional(),
51
- };
7
+ import { handleSpeclawSetup, speclawSetupSchema } from "./setup-tool.js";
52
8
  function makeAdd(server, minimal) {
53
9
  return (name, description, inputSchema, handler) => {
54
10
  if (!shouldExpose(name, minimal))
@@ -57,52 +13,65 @@ function makeAdd(server, minimal) {
57
13
  };
58
14
  }
59
15
  /**
60
- * Foundation tools except `doctor`. Lives in a separate file so budget/doctor
61
- * measurement can import it without forming a file-level SCC through
62
- * `register.ts` → `doctor.ts` → `context-budget.ts`.
16
+ * Foundation MCP tools (setup + hook check). `doctor` and `law_verify` are CLI-only.
17
+ * `scaffold` is CLI-only after tool-surface consolidation.
63
18
  */
64
19
  export function registerFoundationCore(server, opts = {}) {
65
- const add = makeAdd(server, Boolean(opts.minimal));
66
- add("init_project", "Start here to initialize speclaw: returns the analysis questionnaire and packs.", { projectPath: z.string() }, async () => {
67
- const packs = loadPacks();
68
- return text({
69
- instructions: [
70
- "1. Analyze the repository at projectPath and fill in every profile field below with REAL values from the codebase (read package.json / pyproject.toml / CI configs / README — do not invent).",
71
- "2. The foundation is a set of GRANULAR standards under docs/standards/ (base, architecture, backend, frontend, testing, conventions, lawbook), bound by LAWS.md and referenced from CLAUDE.md/AGENTS.md. Fill their structured fields from the real repo: modules_table and layering_rules (architecture), backend_layers, frontend_layers, versioning_rules, and any base_standards_extra. Omit a field only when that standard genuinely doesn't apply to this stack.",
72
- "3. Suggest packs: add stack packs whose 'detect' hints match dependencies you found; offer the rest. Ask the user which packs to install (the lawbook workflow is always installed).",
73
- "4. Infer the working language and the branch/commit/tracker conventions from the repo itself — the language already used in docstrings, commit messages, branch names, and PR/ticket bodies. Do NOT ask the user or assume English; match what the repo does, and set branch_pattern/commit_style accordingly. speclaw does not prescribe a ticket tool — leave tracker linkage to the team's own convention.",
74
- "5. Draft any custom_laws (extra binding rules for LAWS.md) from conventions you observed that the standard set doesn't cover.",
75
- "6. Call the 'scaffold' tool with { projectPath, profile, packs }.",
76
- "7. Follow the nextSteps returned by scaffold: complete the HTML-comment sections still left in docs/standards/*, then run the lawbook_init and compass_index tools (both built into speclaw — no external installs).",
77
- ],
78
- profileFields: profileFieldHelp,
79
- packs,
80
- });
81
- });
82
- add("scaffold", "Write foundation, lawbook workflow, packs, IDE symlinks, and .mcp.json. Never overwrites.", {
83
- projectPath: z.string(),
84
- profile: z.object(profileShape),
85
- packs: z.array(z.string()),
86
- agents: z.array(z.string()).optional(),
87
- }, async ({ projectPath, profile, packs, agents }) => text(scaffold(projectPath, profile, packs, agents ?? [])));
88
- add("configure_agent", "Add one agent's IDE symlinks and MCP config to an already-scaffolded project.", {
89
- projectPath: z.string(),
90
- agent: z.enum(AGENTS.map((a) => a.id)),
91
- }, async ({ projectPath, agent }) => {
92
- const report = emptyReport();
93
- configureAgent(projectPath, agent, report);
94
- return text(report);
95
- });
20
+ const minimal = Boolean(opts.minimal);
21
+ const add = makeAdd(server, minimal);
22
+ add("speclaw_setup", "Project setup: init questionnaire, configure agent, list or add packs.", speclawSetupSchema, async (args) => text(handleSpeclawSetup(args)));
96
23
  add("speclaw_check", "Invoked by speclaw's hooks to enforce laws — do not call directly.", {
97
24
  projectPath: z.string(),
98
25
  event: z.enum(["PreToolUse", "PostToolUse", "Stop", "InstructionsLoaded"]),
99
26
  toolName: z.string().optional(),
100
27
  payload: z.record(z.unknown()),
101
28
  }, async ({ projectPath, event, toolName, payload }) => text(checkAction({ projectPath, event: event, toolName, payload })));
102
- add("law_verify", "Verify deterministic deps/graph laws and return violations by file.", {
103
- projectPath: z.string(),
104
- paths: z.array(z.string()).optional(),
105
- engines: z.array(z.enum(["deps", "graph"])).optional(),
106
- lawIds: z.array(z.string()).optional(),
107
- }, async ({ projectPath, paths, engines, lawIds }) => text(verifyLaws({ projectPath, paths, engines: engines, lawIds })));
29
+ if (minimal || !aliasesEnabled())
30
+ return;
31
+ defineAliasTool(server, {
32
+ name: "init_project",
33
+ description: "Deprecated alias for speclaw_setup action init.",
34
+ inputSchema: { projectPath: z.string() },
35
+ handler: async ({ projectPath }) => {
36
+ logDeprecatedCall(projectPath, "init_project");
37
+ const body = JSON.stringify(handleSpeclawSetup({ projectPath, action: "init" }), null, 2);
38
+ return text(prefixDeprecated("init_project", body));
39
+ },
40
+ });
41
+ defineAliasTool(server, {
42
+ name: "configure_agent",
43
+ description: "Deprecated alias for speclaw_setup configure-agent.",
44
+ inputSchema: {
45
+ projectPath: z.string(),
46
+ agent: z.string(),
47
+ },
48
+ handler: async ({ projectPath, agent }) => {
49
+ logDeprecatedCall(projectPath, "configure_agent");
50
+ const body = JSON.stringify(handleSpeclawSetup({ projectPath, action: "configure-agent", agent }), null, 2);
51
+ return text(prefixDeprecated("configure_agent", body));
52
+ },
53
+ });
54
+ defineAliasTool(server, {
55
+ name: "list_packs",
56
+ description: "Deprecated alias for speclaw_setup list-packs.",
57
+ inputSchema: {},
58
+ handler: async () => {
59
+ const body = JSON.stringify(handleSpeclawSetup({ projectPath: ".", action: "list-packs" }), null, 2);
60
+ return text(prefixDeprecated("list_packs", body));
61
+ },
62
+ });
63
+ defineAliasTool(server, {
64
+ name: "add_pack",
65
+ description: "Deprecated alias for speclaw_setup add-pack.",
66
+ inputSchema: {
67
+ projectPath: z.string(),
68
+ pack: z.string(),
69
+ vars: z.record(z.string()).optional(),
70
+ },
71
+ handler: async ({ projectPath, pack, vars }) => {
72
+ logDeprecatedCall(projectPath, "add_pack");
73
+ const body = JSON.stringify(handleSpeclawSetup({ projectPath, action: "add-pack", pack, vars }), null, 2);
74
+ return text(prefixDeprecated("add_pack", body));
75
+ },
76
+ });
108
77
  }
@@ -1,26 +1,6 @@
1
- import { z } from "zod";
2
- import { defineTool, text } from "../../shared/mcp.js";
3
- import { shouldExpose } from "../../shared/exposure.js";
4
1
  import { registerFoundationCore } from "./register-core.js";
5
2
  export { registerFoundationCore } from "./register-core.js";
6
- const DOCTOR_DESCRIPTION = "Verify the speclaw install; returns a versioned DoctorReport (schemaVersion 1).";
7
- /** Register foundation MCP tools (core + doctor). */
3
+ /** Register foundation MCP tools. Doctor is CLI-only (`speclaw doctor`). */
8
4
  export function registerFoundation(server, opts = {}) {
9
5
  registerFoundationCore(server, opts);
10
- const minimal = Boolean(opts.minimal);
11
- if (!shouldExpose("doctor", minimal))
12
- return;
13
- const inputSchema = { projectPath: z.string() };
14
- const handler = async ({ projectPath }) => {
15
- // Lazy load: register.ts must stay out of the context-budget → doctor SCC.
16
- const { doctor } = await import("./doctor.js");
17
- const report = await doctor(projectPath, { redact: true });
18
- return text(report);
19
- };
20
- defineTool(server, {
21
- name: "doctor",
22
- description: DOCTOR_DESCRIPTION,
23
- inputSchema,
24
- handler,
25
- });
26
6
  }