@esneiderbravo/speclaw 0.3.13 → 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 (37) hide show
  1. package/README.md +2 -2
  2. package/dist/cli/commands/query.js +20 -0
  3. package/dist/cli/commands/update.js +7 -5
  4. package/dist/cli/index.js +2 -0
  5. package/dist/modules/compass/diff-context.js +134 -0
  6. package/dist/modules/compass/explore-rich.js +129 -0
  7. package/dist/modules/compass/impact-summary.js +33 -0
  8. package/dist/modules/compass/register.js +164 -74
  9. package/dist/modules/foundation/context-budget.js +1 -14
  10. package/dist/modules/foundation/doctor.js +46 -0
  11. package/dist/modules/foundation/register-core.js +57 -88
  12. package/dist/modules/foundation/register.js +1 -21
  13. package/dist/modules/foundation/setup-tool.js +96 -0
  14. package/dist/modules/lawbook/assets/commands/archive.md +1 -1
  15. package/dist/modules/lawbook/assets/commands/draft.md +1 -1
  16. package/dist/modules/lawbook/assets/commands/explore.md +1 -1
  17. package/dist/modules/lawbook/assets/commands/sync.md +2 -2
  18. package/dist/modules/lawbook/assets/skills/archive/SKILL.md +1 -1
  19. package/dist/modules/lawbook/assets/skills/archive/steps/03-validate-and-sync.md +3 -3
  20. package/dist/modules/lawbook/assets/skills/archive/steps/04-archive.md +1 -1
  21. package/dist/modules/lawbook/assets/skills/draft/steps/02-understand.md +1 -1
  22. package/dist/modules/lawbook/assets/skills/draft/steps/05-validate.md +1 -1
  23. package/dist/modules/lawbook/assets/skills/explore/steps/01-investigate.md +1 -1
  24. package/dist/modules/lawbook/assets/skills/quick/steps/02-implement.md +1 -1
  25. package/dist/modules/lawbook/assets/skills/sync/SKILL.md +1 -1
  26. package/dist/modules/lawbook/assets/skills/sync/steps/03-validate.md +1 -1
  27. package/dist/modules/lawbook/assets/skills/sync/steps/04-promote.md +1 -1
  28. package/dist/modules/lawbook/change-tool.js +90 -0
  29. package/dist/modules/lawbook/register.js +96 -54
  30. package/dist/modules/tools/register.js +4 -26
  31. package/dist/shared/deprecation.js +99 -0
  32. package/dist/shared/exposure.js +4 -19
  33. package/dist/shared/git.js +25 -0
  34. package/dist/shared/mcp.js +29 -3
  35. package/dist/shared/output-budget.js +68 -0
  36. package/dist/shared/tool-catalog.js +49 -0
  37. package/package.json +1 -1
@@ -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 {
@@ -569,6 +613,7 @@ function configurationChecks(projectPath, initialised) {
569
613
  "cfg.hooks",
570
614
  "cfg.laws",
571
615
  "cfg.budget",
616
+ "cfg.tool-surface",
572
617
  "cfg.index.freshness",
573
618
  "cfg.specs.orphans",
574
619
  ];
@@ -650,6 +695,7 @@ export async function doctor(projectPath, opts = {}) {
650
695
  const configuration = configurationChecks(projectPath, initialised);
651
696
  if (initialised) {
652
697
  configuration.push(await budgetCheck(projectPath));
698
+ configuration.push(await toolSurfaceCheck(projectPath));
653
699
  configuration.push(freshnessCheck(projectPath));
654
700
  configuration.push(specsOrphansCheck(projectPath));
655
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
  }
@@ -0,0 +1,96 @@
1
+ import { z } from "zod";
2
+ import { loadPacks } from "../tools/packs.js";
3
+ import { AGENTS, configureAgent } from "../../shared/agents.js";
4
+ import { emptyReport } from "../../shared/install.js";
5
+ import { refreshAgents } from "../../shared/agents.js";
6
+ import { installPack } from "../tools/packs.js";
7
+ /** Human help text for init questionnaire (not embedded in MCP schemas). */
8
+ const profileFieldHelp = {
9
+ project_name: "Short project name, e.g. the repo name",
10
+ project_description: "One-line description of what the project does",
11
+ organization: "Company/team name",
12
+ stack_summary: "e.g. 'Next.js 15 + TypeScript frontend, FastAPI + PostgreSQL backend'",
13
+ architecture: "e.g. 'hexagonal architecture with bounded contexts'",
14
+ test_commands: "Real commands, e.g. 'pytest backend/tests && npm run test'",
15
+ lint_commands: "Real commands, e.g. 'ruff check . && npm run lint && tsc --noEmit'",
16
+ branch_pattern: "e.g. 'feature/<ticket-id>-<slug>'",
17
+ commit_style: "e.g. 'conventional commits, imperative, English'",
18
+ custom_laws: "Extra markdown for LAWS.md — project-specific binding rules",
19
+ compass_hints: "Markdown bullets with real entrypoints for docs/compass.md",
20
+ base_standards_extra: "Extra cross-cutting rules for base-standards.md",
21
+ modules_table: "Markdown table of modules/bounded contexts",
22
+ layering_rules: "Layers and allowed dependencies for architecture.md",
23
+ backend_layers: "Backend layer table for backend-standards.md",
24
+ frontend_layers: "Frontend layer table for frontend-standards.md",
25
+ versioning_rules: "Versioning/release convention for conventions.md",
26
+ documentation_extra: "Repo-specific docstring notes for documentation.md",
27
+ };
28
+ const profileShape = {
29
+ project_name: z.string(),
30
+ project_description: z.string().optional(),
31
+ organization: z.string().optional(),
32
+ stack_summary: z.string().optional(),
33
+ architecture: z.string().optional(),
34
+ test_commands: z.string().optional(),
35
+ lint_commands: z.string().optional(),
36
+ branch_pattern: z.string().optional(),
37
+ commit_style: z.string().optional(),
38
+ custom_laws: z.string().optional(),
39
+ compass_hints: z.string().optional(),
40
+ base_standards_extra: z.string().optional(),
41
+ modules_table: z.string().optional(),
42
+ layering_rules: z.string().optional(),
43
+ backend_layers: z.string().optional(),
44
+ frontend_layers: z.string().optional(),
45
+ versioning_rules: z.string().optional(),
46
+ documentation_extra: z.string().optional(),
47
+ };
48
+ export const setupActions = ["init", "configure-agent", "add-pack", "list-packs"];
49
+ export const speclawSetupSchema = {
50
+ projectPath: z.string(),
51
+ action: z.enum(setupActions),
52
+ agent: z.enum(AGENTS.map((a) => a.id)).optional(),
53
+ pack: z.string().optional(),
54
+ vars: z.record(z.string()).optional(),
55
+ };
56
+ /**
57
+ * Dispatch `speclaw_setup` by action. Scaffold is CLI-only — not exposed here.
58
+ *
59
+ * @param args - Setup action and parameters.
60
+ */
61
+ export function handleSpeclawSetup(args) {
62
+ switch (args.action) {
63
+ case "init":
64
+ return {
65
+ instructions: [
66
+ "1. Analyze the repository at projectPath and fill profile fields from the real codebase.",
67
+ "2. Call speclaw_setup with action configure-agent / add-pack as needed.",
68
+ "3. Run lawbook_change action init and compass_index when scaffold completes via CLI if needed.",
69
+ ],
70
+ profileFields: profileFieldHelp,
71
+ packs: loadPacks(),
72
+ note: "Full scaffold runs via CLI: speclaw init — not MCP.",
73
+ };
74
+ case "configure-agent": {
75
+ if (!args.agent)
76
+ throw new Error(`speclaw_setup: action 'configure-agent' requires 'agent'`);
77
+ const report = emptyReport();
78
+ configureAgent(args.projectPath, args.agent, report);
79
+ return report;
80
+ }
81
+ case "list-packs":
82
+ return loadPacks();
83
+ case "add-pack": {
84
+ if (!args.pack)
85
+ throw new Error(`speclaw_setup: action 'add-pack' requires 'pack'`);
86
+ const report = emptyReport();
87
+ installPack(args.projectPath, args.pack, args.vars ?? {}, report);
88
+ refreshAgents(args.projectPath, report);
89
+ return report;
90
+ }
91
+ default:
92
+ throw new Error(`speclaw_setup: unknown action '${String(args.action)}'`);
93
+ }
94
+ }
95
+ /** Zod profile shape for CLI scaffold (not in MCP schema). */
96
+ export { profileShape as setupProfileShape };
@@ -6,5 +6,5 @@ Archive the completed change: $ARGUMENTS
6
6
 
7
7
  Follow the `archive` skill: confirm every task (or level-0 checklist) is done
8
8
  and gates are green, reconcile if the level has delta specs, run
9
- `lawbook_validate`, then `lawbook_archive` with today's date (YYYY-MM-DD). Sync
9
+ `lawbook_change` (action: validate), then `lawbook_change` (action: archive) with today's date (YYYY-MM-DD). Sync
10
10
  runs only when the ceremony level requires specs. Never move the folder by hand.
@@ -8,4 +8,4 @@ Follow the `draft` skill: ensure `lawbook/` exists (`lawbook_init`), investigate
8
8
  with Compass, propose a ceremony level (`lawbook_level` mode `propose`) and
9
9
  **confirm** it with the human (`set`), then scaffold only the artifacts that
10
10
  level requires. For true one-liners use `speclaw quick` / the `quick` skill
11
- instead. Finish by running `lawbook_validate` and fixing every issue.
11
+ instead. Finish by running `lawbook_change` (action: validate) and fixing every issue.
@@ -4,7 +4,7 @@ description: Enter explore mode — a thinking partner before or during a change
4
4
 
5
5
  Explore: $ARGUMENTS
6
6
 
7
- Follow the `explore` skill: use `compass_recall`/`compass_explore` to understand
7
+ Follow the `explore` skill: use `compass_find` (mode: concept)/`compass_explore` to understand
8
8
  the code, ask sharp questions, check the relevant `docs/standards/`, weigh
9
9
  approaches with trade-offs, and give a recommendation. Write nothing to
10
10
  `lawbook/`; when the direction is clear, offer to `draft` the change.
@@ -5,6 +5,6 @@ description: Promote a change's delta specs into the canonical specs, without ar
5
5
  Sync the change's specs into canonical: $ARGUMENTS
6
6
 
7
7
  Follow the `sync` skill: reconcile the delta specs against what was actually
8
- built (branch diff + code graph), validate the change (`lawbook_validate`), then
9
- run `lawbook_sync` to promote each delta spec into `lawbook/specs/`. Report what
8
+ built (branch diff + code graph), validate the change (`lawbook_change` (action: validate)), then
9
+ run `lawbook_change` (action: sync) to promote each delta spec into `lawbook/specs/`. Report what
10
10
  you reconciled and what was promoted; leave the change active.
@@ -9,7 +9,7 @@ Close out a completed change: its delta specs become canonical and the change
9
9
  folder moves to `lawbook/changes/archive/`. This is part of the PR that
10
10
  implements the change, not a post-merge chore.
11
11
 
12
- `lawbook_archive` is **gated** — the engine refuses to archive (and reports the
12
+ `lawbook_change` (action: archive) is **gated** — the engine refuses to archive (and reports the
13
13
  reason) while any task is unchecked, while `reports/` holds no discipline report,
14
14
  or while the delta specs are not yet synced into the canonical specs. So archive
15
15
  is the last step of a completed change: reconcile, sync, then archive.
@@ -1,8 +1,8 @@
1
1
  # Validate and sync
2
2
 
3
- Run `lawbook_validate`. If the confirmed ceremony level requires delta specs
4
- (levels 1–3), run `lawbook_sync` to promote them into `lawbook/specs/` —
5
- `lawbook_archive` refuses unless the canonical specs already match. At **level
3
+ Run `lawbook_change` (action: validate). If the confirmed ceremony level requires delta specs
4
+ (levels 1–3), run `lawbook_change` (action: sync) to promote them into `lawbook/specs/` —
5
+ `lawbook_change` (action: archive) refuses unless the canonical specs already match. At **level
6
6
  0**, skip sync (there are no deltas).
7
7
 
8
8
  Next: read `steps/04-archive.md` and do only what it says.
@@ -1,6 +1,6 @@
1
1
  # Archive
2
2
 
3
- Run the `lawbook_archive` tool with the change name and today's date
3
+ Run the `lawbook_change` (action: archive) tool with the change name and today's date
4
4
  (`YYYY-MM-DD`). It re-checks the gate deterministically and, if it passes,
5
5
  moves `lawbook/changes/<name>/` to `lawbook/changes/archive/<date>-<name>/`.
6
6
  If it refuses, resolve the reported blockers (unchecked tasks, missing
@@ -4,7 +4,7 @@
4
4
  code — it is incremental (unchanged files are skipped by hash), so this is
5
5
  cheap and guarantees your decisions rest on the current graph, not a stale one.
6
6
  - Clarify what the user wants (feature / fix / refactor) and confirm scope.
7
- - Use `compass_explore` and `compass_recall` (speclaw's code index) BEFORE
7
+ - Use `compass_explore` and `compass_find` (mode: concept) (speclaw's code index) BEFORE
8
8
  grep/read to locate the real code the change touches and its blast radius.
9
9
  - **Propose a ceremony level** with `lawbook_level` (mode `propose`) using the
10
10
  paths/symbols you found; **confirm with the human** (mode `set`) before
@@ -1,6 +1,6 @@
1
1
  # Validate
2
2
 
3
- Run the `lawbook_validate` tool for the change and fix every issue it reports
3
+ Run the `lawbook_change` (action: validate) tool for the change and fix every issue it reports
4
4
  (missing artifacts, non-normative specs, missing scenarios) before handing off
5
5
  to implementation. Read its advisory **warnings** too: a near-duplicate
6
6
  capability name usually means you should reuse the existing capability's exact
@@ -3,7 +3,7 @@
3
3
  - **Refresh the index first.** Run `compass_index` before investigating — it is
4
4
  incremental (unchanged files skipped by hash), so it is cheap and keeps your
5
5
  reasoning on the current graph rather than a stale one.
6
- - **Understand the code first.** Use `compass_recall` to find relevant code by
6
+ - **Understand the code first.** Use `compass_find` (mode: concept) to find relevant code by
7
7
  meaning and `compass_explore` to read a symbol's source plus its callers and
8
8
  callees — before grep/read.
9
9
  - **Ask sharp questions** to surface hidden assumptions, constraints, and edge
@@ -1,7 +1,7 @@
1
1
  # Implement and evidence
2
2
 
3
3
  Make the fix, tick every `- [ ]` in `record.md`, and write at least one
4
- discipline report under `reports/`. Archive with `lawbook_archive` (no sync at
4
+ discipline report under `reports/`. Archive with `lawbook_change` (action: archive) (no sync at
5
5
  level 0). Promote via `lawbook_level` if scope grew.
6
6
 
7
7
  No further steps — workflow complete.
@@ -9,7 +9,7 @@ Update the project's canonical specifications (`lawbook/specs/`) with a change's
9
9
  delta specs, without archiving the change. Use this when the specs should
10
10
  become the source of truth but the change isn't finished (e.g. multi-PR work).
11
11
 
12
- `lawbook_sync` is a deterministic copy — it is blind to the code. So before
12
+ `lawbook_change` (action: sync) is a deterministic copy — it is blind to the code. So before
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
 
@@ -1,6 +1,6 @@
1
1
  # Validate
2
2
 
3
- Run `lawbook_validate` for the change; do not sync a change whose specs are
3
+ Run `lawbook_change` (action: validate) for the change; do not sync a change whose specs are
4
4
  invalid.
5
5
 
6
6
  Next: read `steps/04-promote.md` and do only what it says.
@@ -1,6 +1,6 @@
1
1
  # Promote
2
2
 
3
- Run the `lawbook_sync` tool for the change. It copies each
3
+ Run the `lawbook_change` (action: sync) tool for the change. It copies each
4
4
  `lawbook/changes/<name>/specs/<capability>/spec.md` over the canonical
5
5
  `lawbook/specs/<capability>/spec.md` and reports what it promoted, flagging
6
6
  each as **created** (new capability) or **updated** (overwrote an existing
@@ -0,0 +1,90 @@
1
+ import { z } from "zod";
2
+ import { specInit, specValidate, specSync, specArchive, specList } from "./engine.js";
3
+ import { handleLevel } from "./quick.js";
4
+ import { buildCoverageReport, loadCoverageConfig, renderCoverageAgent } from "./coverage.js";
5
+ import { buildDriftReport, renderDriftAgent } from "./drift.js";
6
+ export const lawbookChangeActions = [
7
+ "init",
8
+ "list",
9
+ "validate",
10
+ "sync",
11
+ "archive",
12
+ "level",
13
+ "coverage",
14
+ "drift",
15
+ ];
16
+ export const lawbookChangeSchema = {
17
+ projectPath: z.string(),
18
+ action: z.enum(lawbookChangeActions),
19
+ change: z.string().optional(),
20
+ date: z
21
+ .string()
22
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
23
+ .optional(),
24
+ mode: z.enum(["propose", "set", "promote", "explain"]).optional(),
25
+ paths: z.array(z.string()).optional(),
26
+ symbols: z.array(z.string()).optional(),
27
+ level: z.union([z.literal(0), z.literal(1), z.literal(2), z.literal(3)]).optional(),
28
+ reason: z.string().optional(),
29
+ onlyDefects: z.boolean().optional(),
30
+ json: z.boolean().optional(),
31
+ capability: z.string().optional(),
32
+ includeReverse: z.boolean().optional(),
33
+ maxItems: z.number().int().min(1).max(50).optional(),
34
+ };
35
+ function requireField(args, field) {
36
+ const v = args[field];
37
+ if (typeof v === "string" && v.length > 0)
38
+ return v;
39
+ throw new Error(`lawbook_change: action '${args.action}' requires '${String(field)}'`);
40
+ }
41
+ /**
42
+ * Dispatch `lawbook_change` by action.
43
+ *
44
+ * @param args - Unified lawbook lifecycle arguments.
45
+ */
46
+ export function handleLawbookChange(args) {
47
+ switch (args.action) {
48
+ case "init":
49
+ return specInit(args.projectPath);
50
+ case "list":
51
+ return specList(args.projectPath);
52
+ case "validate":
53
+ return specValidate(args.projectPath, requireField(args, "change"));
54
+ case "sync":
55
+ return specSync(args.projectPath, requireField(args, "change"));
56
+ case "archive":
57
+ return specArchive(args.projectPath, requireField(args, "change"), requireField(args, "date"));
58
+ case "level":
59
+ if (!args.mode)
60
+ throw new Error(`lawbook_change: action 'level' requires 'mode'`);
61
+ return handleLevel({
62
+ projectPath: args.projectPath,
63
+ mode: args.mode,
64
+ change: args.change,
65
+ paths: args.paths,
66
+ symbols: args.symbols,
67
+ level: args.level,
68
+ reason: args.reason,
69
+ });
70
+ case "coverage": {
71
+ const cfg = loadCoverageConfig(args.projectPath);
72
+ const report = buildCoverageReport(args.projectPath, { change: args.change, cfg });
73
+ if (args.json)
74
+ return report;
75
+ return renderCoverageAgent(report, args.onlyDefects !== false);
76
+ }
77
+ case "drift": {
78
+ const report = buildDriftReport(args.projectPath, {
79
+ capability: args.capability,
80
+ reverse: args.includeReverse === true,
81
+ failOn: "semantic",
82
+ });
83
+ if (args.json)
84
+ return report;
85
+ return renderDriftAgent(report, args.maxItems ?? 10);
86
+ }
87
+ default:
88
+ throw new Error(`lawbook_change: unknown action '${String(args.action)}'`);
89
+ }
90
+ }