@esneiderbravo/speclaw 0.3.13 → 1.0.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 (74) hide show
  1. package/README.md +88 -72
  2. package/dist/cli/commands/index-build.js +12 -3
  3. package/dist/cli/commands/lawbook.js +1 -0
  4. package/dist/cli/commands/laws.js +149 -8
  5. package/dist/cli/commands/owners.js +44 -0
  6. package/dist/cli/commands/query.js +52 -10
  7. package/dist/cli/commands/update.js +35 -5
  8. package/dist/cli/commands/verify.js +8 -0
  9. package/dist/cli/index.js +15 -4
  10. package/dist/modules/compass/budget.js +128 -0
  11. package/dist/modules/compass/db.js +290 -30
  12. package/dist/modules/compass/diff-context.js +134 -0
  13. package/dist/modules/compass/embed-input.js +28 -0
  14. package/dist/modules/compass/embedder.js +3 -1
  15. package/dist/modules/compass/explore-rich.js +134 -0
  16. package/dist/modules/compass/extract.js +86 -0
  17. package/dist/modules/compass/hybrid.js +318 -0
  18. package/dist/modules/compass/impact-summary.js +33 -0
  19. package/dist/modules/compass/indexer.js +204 -33
  20. package/dist/modules/compass/merkle.js +76 -0
  21. package/dist/modules/compass/pagerank.js +122 -0
  22. package/dist/modules/compass/rank.js +95 -0
  23. package/dist/modules/compass/register.js +169 -75
  24. package/dist/modules/foundation/check.js +4 -2
  25. package/dist/modules/foundation/compile-laws.js +212 -0
  26. package/dist/modules/foundation/context-budget.js +1 -14
  27. package/dist/modules/foundation/dialects/agentsmd.js +95 -0
  28. package/dist/modules/foundation/dialects/claude-cursor.js +45 -0
  29. package/dist/modules/foundation/dialects/coderabbit.js +27 -0
  30. package/dist/modules/foundation/dialects/copilot.js +35 -0
  31. package/dist/modules/foundation/dialects/index.js +5 -0
  32. package/dist/modules/foundation/dialects/types.js +58 -0
  33. package/dist/modules/foundation/doctor.js +266 -14
  34. package/dist/modules/foundation/import-rules.js +67 -0
  35. package/dist/modules/foundation/integrity.js +307 -0
  36. package/dist/modules/foundation/laws-parse.js +131 -0
  37. package/dist/modules/foundation/laws.js +5 -0
  38. package/dist/modules/foundation/lock.js +283 -0
  39. package/dist/modules/foundation/ownership.js +4 -0
  40. package/dist/modules/foundation/register-core.js +57 -88
  41. package/dist/modules/foundation/register.js +1 -21
  42. package/dist/modules/foundation/scaffold.js +25 -0
  43. package/dist/modules/foundation/scan.js +227 -0
  44. package/dist/modules/foundation/setup-tool.js +96 -0
  45. package/dist/modules/foundation/verify.js +9 -1
  46. package/dist/modules/lawbook/assets/commands/archive.md +1 -1
  47. package/dist/modules/lawbook/assets/commands/draft.md +1 -1
  48. package/dist/modules/lawbook/assets/commands/explore.md +1 -1
  49. package/dist/modules/lawbook/assets/commands/sync.md +2 -2
  50. package/dist/modules/lawbook/assets/skills/archive/SKILL.md +1 -1
  51. package/dist/modules/lawbook/assets/skills/archive/steps/03-validate-and-sync.md +3 -3
  52. package/dist/modules/lawbook/assets/skills/archive/steps/04-archive.md +1 -1
  53. package/dist/modules/lawbook/assets/skills/draft/steps/02-understand.md +1 -1
  54. package/dist/modules/lawbook/assets/skills/draft/steps/05-validate.md +1 -1
  55. package/dist/modules/lawbook/assets/skills/explore/steps/01-investigate.md +1 -1
  56. package/dist/modules/lawbook/assets/skills/quick/steps/02-implement.md +1 -1
  57. package/dist/modules/lawbook/assets/skills/sync/SKILL.md +1 -1
  58. package/dist/modules/lawbook/assets/skills/sync/steps/03-validate.md +1 -1
  59. package/dist/modules/lawbook/assets/skills/sync/steps/04-promote.md +1 -1
  60. package/dist/modules/lawbook/change-tool.js +90 -0
  61. package/dist/modules/lawbook/coverage.js +45 -6
  62. package/dist/modules/lawbook/ears.js +417 -0
  63. package/dist/modules/lawbook/engine.js +29 -0
  64. package/dist/modules/lawbook/register.js +96 -54
  65. package/dist/modules/lawbook/spec-items.js +4 -1
  66. package/dist/modules/team/owners.js +464 -0
  67. package/dist/modules/tools/register.js +4 -26
  68. package/dist/shared/deprecation.js +99 -0
  69. package/dist/shared/exposure.js +4 -19
  70. package/dist/shared/git.js +25 -0
  71. package/dist/shared/mcp.js +29 -3
  72. package/dist/shared/output-budget.js +68 -0
  73. package/dist/shared/tool-catalog.js +49 -0
  74. package/package.json +4 -3
@@ -0,0 +1,283 @@
1
+ /**
2
+ * speclaw.lock — committed digests for rule files (never under `.speclaw/`).
3
+ * A gitignored lock would be invisible in PR diffs and would not detect
4
+ * Rules File Backdoor edits. Like package-lock.json / go.sum.
5
+ */
6
+ // Covers: req~speclaw-lock~1
7
+ import crypto from "node:crypto";
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import { pkgName, pkgVersion } from "../../shared/version.js";
11
+ export const LOCKFILE_NAME = "speclaw.lock";
12
+ export const LOCKFILE_VERSION = 1;
13
+ /** Delimited provenance block excluded from digests (self-reference). */
14
+ export const PROVENANCE_START = "<!-- speclaw:begin-provenance";
15
+ export const PROVENANCE_END = "speclaw:end-provenance -->";
16
+ /** Project-relative path of the committed lockfile. */
17
+ export function lockfilePath(projectPath) {
18
+ return path.join(projectPath, LOCKFILE_NAME);
19
+ }
20
+ /**
21
+ * Canonical bytes for hashing: LF endings, strip provenance, trim EOL spaces,
22
+ * ensure a single trailing newline.
23
+ */
24
+ export function canonicalize(raw) {
25
+ let text = raw.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
26
+ text = stripProvenanceBlock(text);
27
+ text = text
28
+ .split("\n")
29
+ .map((line) => line.replace(/[ \t]+$/g, ""))
30
+ .join("\n");
31
+ if (!text.endsWith("\n"))
32
+ text += "\n";
33
+ else if (text.endsWith("\n\n")) {
34
+ // collapse to exactly one trailing newline
35
+ text = text.replace(/\n+$/g, "\n");
36
+ }
37
+ return text;
38
+ }
39
+ /** Remove speclaw provenance HTML comment blocks. */
40
+ export function stripProvenanceBlock(text) {
41
+ const re = new RegExp(`${escapeRegExp(PROVENANCE_START)}[\\s\\S]*?${escapeRegExp(PROVENANCE_END)}\\n?`, "g");
42
+ return text.replace(re, "");
43
+ }
44
+ function escapeRegExp(s) {
45
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
46
+ }
47
+ /** sha256 digest with `sha256:` prefix. */
48
+ export function digestOf(canonical) {
49
+ const hex = crypto.createHash("sha256").update(canonical, "utf8").digest("hex");
50
+ return `sha256:${hex}`;
51
+ }
52
+ /** Digest raw file text after canonicalization. */
53
+ export function digestText(raw) {
54
+ return digestOf(canonicalize(raw));
55
+ }
56
+ /** Root hash over sorted path → digest pairs. */
57
+ export function rootDigest(files) {
58
+ const paths = Object.keys(files).sort();
59
+ let acc = "";
60
+ for (const p of paths) {
61
+ acc += `${p}\0${files[p].digest}\n`;
62
+ }
63
+ return digestOf(acc);
64
+ }
65
+ /** Read lockfile or null if missing. Throws on unknown version / parse error. */
66
+ export function readLockfile(projectPath) {
67
+ const abs = lockfilePath(projectPath);
68
+ if (!fs.existsSync(abs))
69
+ return null;
70
+ const raw = JSON.parse(fs.readFileSync(abs, "utf8"));
71
+ if (typeof raw.lockfileVersion !== "number") {
72
+ throw new Error("speclaw.lock: missing lockfileVersion");
73
+ }
74
+ if (raw.lockfileVersion > LOCKFILE_VERSION) {
75
+ throw new Error(`speclaw.lock: unsupported lockfileVersion ${raw.lockfileVersion} (max ${LOCKFILE_VERSION})`);
76
+ }
77
+ return {
78
+ lockfileVersion: raw.lockfileVersion,
79
+ generator: String(raw.generator ?? ""),
80
+ algorithm: "sha256",
81
+ root: String(raw.root ?? ""),
82
+ files: raw.files && typeof raw.files === "object" ? raw.files : {},
83
+ symlinks: raw.symlinks && typeof raw.symlinks === "object" ? raw.symlinks : {},
84
+ accepted: Array.isArray(raw.accepted) ? raw.accepted : [],
85
+ };
86
+ }
87
+ /** Write lockfile with stable JSON formatting. */
88
+ export function writeLockfile(projectPath, lock) {
89
+ const abs = lockfilePath(projectPath);
90
+ const body = JSON.stringify(lock, null, 2) + "\n";
91
+ fs.writeFileSync(abs, body);
92
+ }
93
+ /** Build a fresh lock object from file digests + symlinks. */
94
+ export function buildLock(opts) {
95
+ const files = { ...opts.files };
96
+ return {
97
+ lockfileVersion: LOCKFILE_VERSION,
98
+ generator: `${pkgName()}@${pkgVersion()}`,
99
+ algorithm: "sha256",
100
+ root: rootDigest(files),
101
+ files,
102
+ symlinks: { ...(opts.symlinks ?? {}) },
103
+ accepted: [...(opts.accepted ?? [])],
104
+ };
105
+ }
106
+ /** Integrity severity policy for a project-relative path. */
107
+ export function integrityPolicy(relPath) {
108
+ const n = relPath.split("\\").join("/");
109
+ // `.cursor/rules/` mirrors regenerable `ai-specs/` (gitignored) — lock/CI must
110
+ // not treat them as strict committed files; scan when present, never pin.
111
+ if (n === "AGENTS.md" ||
112
+ n === "CLAUDE.md" ||
113
+ n.startsWith(".github/instructions/") ||
114
+ n === ".coderabbit.yaml" ||
115
+ n === ".claude/rules/speclaw") {
116
+ return "strict";
117
+ }
118
+ if (n === "LAWS.md" || n === "docs/compass.md" || n.startsWith("docs/standards/")) {
119
+ return "advisory";
120
+ }
121
+ return "scan-only";
122
+ }
123
+ /** True when a path is an IDE mirror of regenerable (typically gitignored) content. */
124
+ export function isRegenerableIdeMirror(relPath) {
125
+ const n = relPath.split("\\").join("/");
126
+ return (n.startsWith(".cursor/rules/") ||
127
+ n.startsWith(".cursor/skills/") ||
128
+ n.startsWith(".cursor/commands/") ||
129
+ n.startsWith(".claude/skills/") ||
130
+ n.startsWith(".claude/commands/") ||
131
+ n.startsWith("ai-specs/"));
132
+ }
133
+ /** Discover candidate paths under the project for locking / scanning. */
134
+ export function discoverIntegrityPaths(projectPath) {
135
+ const files = [];
136
+ const symlinks = [];
137
+ const addFile = (rel) => {
138
+ const abs = path.join(projectPath, rel);
139
+ if (fs.existsSync(abs) && fs.statSync(abs).isFile())
140
+ files.push(rel.split("\\").join("/"));
141
+ };
142
+ for (const f of ["AGENTS.md", "CLAUDE.md", "LAWS.md", "docs/compass.md", ".coderabbit.yaml"]) {
143
+ addFile(f);
144
+ }
145
+ walkFiles(path.join(projectPath, "docs", "standards"), projectPath, files, (p) => p.endsWith(".md"));
146
+ walkFiles(path.join(projectPath, ".cursor", "rules"), projectPath, files, () => true);
147
+ walkFiles(path.join(projectPath, ".github", "instructions"), projectPath, files, () => true);
148
+ // Outside-pipeline / skills (scan-only)
149
+ for (const f of [".clinerules", ".windsurfrules", "BUGBOT.md", ".cursorrules"])
150
+ addFile(f);
151
+ walkFiles(path.join(projectPath, "ai-specs", "skills"), projectPath, files, (p) => p.endsWith("SKILL.md") || p.endsWith(".md"));
152
+ walkFiles(path.join(projectPath, "ai-specs", "agents"), projectPath, files, (p) => p.endsWith(".md"));
153
+ walkFiles(path.join(projectPath, ".claude", "skills"), projectPath, files, (p) => p.endsWith("SKILL.md") || p.endsWith(".md"));
154
+ const linkRel = ".claude/rules/speclaw";
155
+ const linkAbs = path.join(projectPath, linkRel);
156
+ try {
157
+ const st = fs.lstatSync(linkAbs);
158
+ if (st.isSymbolicLink()) {
159
+ symlinks.push({ path: linkRel, target: fs.readlinkSync(linkAbs) });
160
+ }
161
+ }
162
+ catch {
163
+ /* missing */
164
+ }
165
+ return { files: [...new Set(files)].sort(), symlinks };
166
+ }
167
+ function walkFiles(dir, projectPath, out, pred) {
168
+ if (!fs.existsSync(dir))
169
+ return;
170
+ const stack = [dir];
171
+ while (stack.length) {
172
+ const cur = stack.pop();
173
+ let entries;
174
+ try {
175
+ entries = fs.readdirSync(cur, { withFileTypes: true });
176
+ }
177
+ catch {
178
+ continue;
179
+ }
180
+ for (const e of entries) {
181
+ const full = path.join(cur, e.name);
182
+ if (e.isDirectory())
183
+ stack.push(full);
184
+ else if (e.isFile()) {
185
+ const rel = path.relative(projectPath, full).split(path.sep).join("/");
186
+ if (pred(rel))
187
+ out.push(rel);
188
+ }
189
+ }
190
+ }
191
+ }
192
+ /** Markers for the regenerable map block inside `docs/compass.md` (see compass/map.ts). */
193
+ export const COMPASS_MAP_START = "<!-- speclaw:map:start -->";
194
+ export const COMPASS_MAP_END = "<!-- speclaw:map:end -->";
195
+ /**
196
+ * Strip the regenerable map body between markers so integrity digests stay stable
197
+ * across `speclaw index` (which rewrites the map in CI before verify).
198
+ *
199
+ * @param text - Full docs/compass.md contents.
200
+ * @returns The same text with an empty map body, or `text` if markers are missing.
201
+ */
202
+ export function stripCompassMapBlock(text) {
203
+ const start = text.indexOf(COMPASS_MAP_START);
204
+ const end = text.indexOf(COMPASS_MAP_END);
205
+ if (start < 0 || end < 0 || end < start)
206
+ return text;
207
+ return text.slice(0, start + COMPASS_MAP_START.length) + "\n" + text.slice(end);
208
+ }
209
+ /**
210
+ * Path-specific bytes that feed {@link digestText}: speclaw-owned coderabbit
211
+ * region, regenerable Compass map body stripped, otherwise the file as-is.
212
+ *
213
+ * @param relPath - Project-relative path.
214
+ * @param raw - File contents.
215
+ * @returns Text to canonicalize and hash for this path.
216
+ */
217
+ export function prepareIntegrityText(relPath, raw) {
218
+ const n = relPath.split("\\").join("/");
219
+ if (n === ".coderabbit.yaml")
220
+ return extractSpeclawYamlBlock(raw) ?? raw;
221
+ if (n === "docs/compass.md")
222
+ return stripCompassMapBlock(raw);
223
+ return raw;
224
+ }
225
+ /**
226
+ * Snapshot digests for discovered files with ownership policy.
227
+ * For `.coderabbit.yaml`, digests only the speclaw delimited block when present.
228
+ * For `docs/compass.md`, digests with the regenerable map body stripped.
229
+ */
230
+ export function snapshotLockEntries(projectPath) {
231
+ const { files: paths, symlinks } = discoverIntegrityPaths(projectPath);
232
+ const files = {};
233
+ for (const rel of paths) {
234
+ const ownership = integrityPolicy(rel);
235
+ if (ownership === "scan-only")
236
+ continue; // locked only when previously accepted / explicit
237
+ const abs = path.join(projectPath, rel);
238
+ const raw = prepareIntegrityText(rel, fs.readFileSync(abs, "utf8"));
239
+ files[rel] = { digest: digestText(raw), ownership };
240
+ }
241
+ const symlinkMap = {};
242
+ for (const s of symlinks)
243
+ symlinkMap[s.path] = { target: s.target };
244
+ return { files, symlinks: symlinkMap };
245
+ }
246
+ /** Extract a speclaw-marked region from coderabbit yaml if present. */
247
+ export function extractSpeclawYamlBlock(raw) {
248
+ const m = /# speclaw:begin[\s\S]*?# speclaw:end/.exec(raw);
249
+ if (m)
250
+ return m[0];
251
+ const m2 = /<!-- speclaw:laws:start -->[\s\S]*?<!-- speclaw:laws:end -->/.exec(raw) ??
252
+ /<!-- speclaw:begin-provenance[\s\S]*?speclaw:end-provenance -->/.exec(raw);
253
+ return m2 ? m2[0] : null;
254
+ }
255
+ /** Create or refresh speclaw.lock from the current tree. */
256
+ export function refreshLockfile(projectPath) {
257
+ const prev = (() => {
258
+ try {
259
+ return readLockfile(projectPath);
260
+ }
261
+ catch {
262
+ return null;
263
+ }
264
+ })();
265
+ const { files, symlinks } = snapshotLockEntries(projectPath);
266
+ const lock = buildLock({
267
+ files,
268
+ symlinks,
269
+ accepted: prev?.accepted ?? [],
270
+ });
271
+ writeLockfile(projectPath, lock);
272
+ return lock;
273
+ }
274
+ /** Render a data-only provenance HTML comment (no imperatives). */
275
+ export function provenanceBlock(opts) {
276
+ const laws = (opts.lawIds ?? []).map((l) => ` law: ${l}`).join("\n");
277
+ return (`${PROVENANCE_START}\n` +
278
+ (laws ? laws + "\n" : "") +
279
+ (opts.source ? ` source: ${opts.source}\n` : "") +
280
+ ` digest: ${opts.digest}\n` +
281
+ ` generator: ${pkgName()}@${pkgVersion()}\n` +
282
+ `speclaw:end-provenance -->\n`);
283
+ }
@@ -1,6 +1,10 @@
1
1
  // Which scaffolded files speclaw owns vs the user owns. `speclaw update` uses
2
2
  // this split to decide what it may overwrite automatically (managed) and what it
3
3
  // must leave to the user's agent via a prompt (personalized).
4
+ //
5
+ // Integrity severity (`strict` / `advisory` / `scan-only` in lock.ts) is a
6
+ // separate axis: CLAUDE.md / AGENTS.md stay PERSONALIZED for update, but are
7
+ // `strict` for `verifyIntegrity` digest mismatches.
4
8
  /**
5
9
  * Project-relative trees that hold speclaw's workflow machinery. The user is not
6
10
  * meant to edit these, so `speclaw update` overwrites them with the current
@@ -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
  }
@@ -10,6 +10,9 @@ import { readManifest, writeManifest } from "../../shared/manifest.js";
10
10
  import { pkgVersion } from "../../shared/version.js";
11
11
  import { mergeSeedLaws, readLawManifest, seedManifest, writeLawManifest, } from "./laws.js";
12
12
  import { installHooks } from "./hooks.js";
13
+ import { compileLaws } from "./compile-laws.js";
14
+ import { refreshLockfile } from "./lock.js";
15
+ import { refreshOwnersIfConfigured } from "../team/owners.js";
13
16
  const ASSETS = assetsDir(import.meta.url);
14
17
  // Every {{var}} the foundation templates may reference. Ones the agent didn't
15
18
  // provide default to empty so a bare `scaffold` never leaves a raw {{tag}}.
@@ -150,12 +153,34 @@ export function scaffold(projectPath, profile, packNames, agents = [], opts = {}
150
153
  // configured. The seam is the manifest: check-dispatcher enforces `path` laws;
151
154
  // executable-laws will extend the same manifest with more backends.
152
155
  const lawManifest = ensureLawManifest(projectPath, report);
156
+ try {
157
+ compileLaws({ projectPath, agents, writeManifest: false });
158
+ }
159
+ catch {
160
+ // Compilation must not fail scaffold; `speclaw laws compile` surfaces errors.
161
+ }
162
+ // Covers: req~lock-refresh-update~1
163
+ try {
164
+ refreshLockfile(projectPath);
165
+ }
166
+ catch {
167
+ // Lock refresh must not fail scaffold; `speclaw laws lock` surfaces errors.
168
+ }
153
169
  ensureVerifyWorkflow(projectPath, report);
154
170
  report.hooks = installHooks(projectPath, agents, lawManifest, report, {
155
171
  baselines: managedOpts.baselines,
156
172
  backup: managedOpts.backup,
157
173
  record,
158
174
  });
175
+ // Refresh CODEOWNERS managed block when the project declared team.owners —
176
+ // never invent owners when the key is absent.
177
+ try {
178
+ refreshOwnersIfConfigured(projectPath);
179
+ }
180
+ catch (err) {
181
+ // Surface as a soft note — invalid tokens should not abort scaffold/update.
182
+ report.skipped.push(`owners refresh skipped: ${err.message}`);
183
+ }
159
184
  // Record what was installed so `speclaw update` can re-apply these packs and
160
185
  // gate feature migrations by version, plus the managed-file baselines that let
161
186
  // a later update tell user edits from stale files.