@esneiderbravo/speclaw 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,7 +35,7 @@ One command. No cloud, no LLM, no API keys — <b>everything runs on your machin
35
35
 
36
36
  <br/>
37
37
 
38
- ## ◆ Quick start
38
+ ## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="" align="absmiddle">&nbsp; Quick start
39
39
 
40
40
  Install speclaw globally (once), then run `init` in your project root:
41
41
 
@@ -67,7 +67,7 @@ too (also `pnpm dlx` / `yarn dlx`) — but installing globally means you can run
67
67
 
68
68
  <br/>
69
69
 
70
- ## ◆ It looks like this
70
+ ## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="" align="absmiddle">&nbsp; It looks like this
71
71
 
72
72
  <p align="center">
73
73
  <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/terminal-init.png" width="800" alt="speclaw init — terminal output">
@@ -77,7 +77,7 @@ too (also `pnpm dlx` / `yarn dlx`) — but installing globally means you can run
77
77
 
78
78
  <br/>
79
79
 
80
- ## ◆ The suite — four modules
80
+ ## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="" align="absmiddle">&nbsp; The suite — four modules
81
81
 
82
82
  | Module | What it does |
83
83
  | :-- | :-- |
@@ -90,7 +90,44 @@ Compass is inspired by [CodeGraph](https://github.com/colbymchenry/codegraph) an
90
90
 
91
91
  <br/>
92
92
 
93
- ## ◆ Two ways to use it
93
+ ## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="" align="absmiddle">&nbsp; The spec-driven workflow (Lawbook)
94
+
95
+ Lawbook is speclaw's answer to the biggest risk with AI agents: **code that
96
+ drifts from intent.** No non-trivial change lands without a spec change — the
97
+ intent is written first, the code is made to match it, and the spec is promoted
98
+ to the project's canonical record. It's a loop of five steps:
99
+
100
+ ```
101
+ explore → draft → build → sync → archive
102
+ ```
103
+
104
+ | Step | What happens |
105
+ | :-- | :-- |
106
+ | **explore** | Think an idea through *before* committing to it — should we do this, and how. Writes nothing. |
107
+ | **draft** | Create the change under `lawbook/changes/<name>/`: a `proposal.md` (why · what · non-goals), **delta specs** in `specs/<capability>/spec.md`, an optional `design.md`, and a `tasks.md` checklist. |
108
+ | **build** | Implement the tasks in order, keeping code and spec in agreement. |
109
+ | **sync** | Promote the change's delta specs into the canonical `lawbook/specs/` — the always-true description of how the system behaves. |
110
+ | **archive** | Validate, promote, and move the change to `lawbook/changes/archive/` — **in the same PR**, never a post-merge chore. |
111
+
112
+ **Delta specs are normative and testable.** Requirements use `SHALL`/`MUST`
113
+ under `### Requirement:` headers, each with one or more `#### Scenario:` blocks
114
+ whose acceptance criteria hold without production integrations. `lawbook_validate`
115
+ checks that the code matches what the spec promises before you sync or archive.
116
+
117
+ **Three ways to drive it — same engine, no external CLI:**
118
+
119
+ - **In your agent** — the `/lawbook:explore`, `/lawbook:draft`, `/lawbook:build`, `/lawbook:sync`, `/lawbook:archive` commands (installed as skills).
120
+ - **MCP tools** — `lawbook_init`, `lawbook_validate`, `lawbook_sync`, `lawbook_archive`, `lawbook_list`.
121
+ - **CLI** — `speclaw lawbook init | list | validate | sync | archive`.
122
+
123
+ The workspace is committed under `lawbook/`: `specs/` (canonical), `changes/`
124
+ (in-flight), `changes/archive/` (shipped), and `config.yaml` (the mandatory task
125
+ steps every change must include). The standards themselves are amended the same
126
+ way — through a spec change reviewed by a human.
127
+
128
+ <br/>
129
+
130
+ ## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="◆" align="absmiddle">&nbsp; Two ways to use it
94
131
 
95
132
  speclaw meets you where you are. Everything works through the **CLI** — so no one
96
133
  is blocked by MCP setup — and the same capabilities are exposed as **MCP tools**
@@ -105,7 +142,7 @@ still use Compass and the lawbook engine by calling the CLI from its shell.
105
142
 
106
143
  <br/>
107
144
 
108
- ## ◆ What lands in your project
145
+ ## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="" align="absmiddle">&nbsp; What lands in your project
109
146
 
110
147
  <p align="center">
111
148
  <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/terminal-tree.png" width="800" alt="what speclaw writes into your project">
@@ -113,7 +150,7 @@ still use Compass and the lawbook engine by calling the CLI from its shell.
113
150
 
114
151
  <br/>
115
152
 
116
- ## ◆ Philosophy — why "laws"?
153
+ ## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="" align="absmiddle">&nbsp; Philosophy — why "laws"?
117
154
 
118
155
  > [!NOTE]
119
156
  > A guideline is a suggestion. A **law** is enforced. The most common failure mode
@@ -124,7 +161,7 @@ still use Compass and the lawbook engine by calling the CLI from its shell.
124
161
 
125
162
  <br/>
126
163
 
127
- ## ◆ Staying up to date
164
+ ## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="" align="absmiddle">&nbsp; Staying up to date
128
165
 
129
166
  speclaw checks for new releases in the background (at most once a day) and nudges
130
167
  you when one lands. To upgrade:
@@ -143,7 +180,7 @@ packs this project already uses.
143
180
 
144
181
  <br/>
145
182
 
146
- ## ◆ Requirements
183
+ ## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="" align="absmiddle">&nbsp; Requirements
147
184
 
148
185
  - **Node.js ≥ 22** — uses the built-in `node:sqlite`.
149
186
  - **No native builds, no services, no API keys, no LLM download.** Tree-sitter
@@ -104,10 +104,14 @@ export async function runInit(flags) {
104
104
  ui.step("Indexing your code with Compass");
105
105
  const stats = await buildIndex(cwd, (e) => renderProgress(e.done, e.total, e.file));
106
106
  clearProgress();
107
- ui.ok(c.bold(c.cream(String(stats.files))) + c.muted(" files · ") +
108
- c.bold(c.cream(String(stats.nodes))) + c.muted(" nodes · ") +
109
- c.bold(c.cream(String(stats.edges))) + c.muted(" edges · ") +
110
- c.bold(c.cream(String(stats.embeddings))) + c.muted(" embeddings"));
107
+ ui.ok(c.bold(c.cream(String(stats.files))) +
108
+ c.muted(" files · ") +
109
+ c.bold(c.cream(String(stats.nodes))) +
110
+ c.muted(" nodes · ") +
111
+ c.bold(c.cream(String(stats.edges))) +
112
+ c.muted(" edges · ") +
113
+ c.bold(c.cream(String(stats.embeddings))) +
114
+ c.muted(" embeddings"));
111
115
  }
112
116
  // 3. Handoff prompt for the chosen agent — printed as a single flush-left
113
117
  // line so it copy-pastes cleanly (no borders, no wrapping artifacts).
@@ -1,4 +1,4 @@
1
- import { specInit, specValidate, specSync, specArchive, specList } from "../../modules/lawbook/engine.js";
1
+ import { specInit, specValidate, specSync, specArchive, specList, } from "../../modules/lawbook/engine.js";
2
2
  import { ui } from "../lib/ui.js";
3
3
  function today() {
4
4
  // The MCP path passes the date in; the CLL runs on a real machine, so read it here.
@@ -17,7 +17,9 @@ export async function runSpec(flags) {
17
17
  switch (sub) {
18
18
  case "init": {
19
19
  const r = specInit(cwd);
20
- ui.ok(r.alreadyExisted ? "lawbook/ already present" : `lawbook/ created (${r.created.length} entries)`);
20
+ ui.ok(r.alreadyExisted
21
+ ? "lawbook/ already present"
22
+ : `lawbook/ created (${r.created.length} entries)`);
21
23
  return;
22
24
  }
23
25
  case "list": {
@@ -39,7 +39,10 @@ export async function runUpdate(flags) {
39
39
  return;
40
40
  }
41
41
  ui.step(`Updating ${pkgName()} globally`);
42
- const install = spawnSync("npm", ["install", "-g", `${pkgName()}@latest`], { stdio: "inherit", shell: winShell });
42
+ const install = spawnSync("npm", ["install", "-g", `${pkgName()}@latest`], {
43
+ stdio: "inherit",
44
+ shell: winShell,
45
+ });
43
46
  if (install.status !== 0) {
44
47
  ui.err("Global update failed. Try again with elevated permissions (e.g. sudo), or check your npm setup.");
45
48
  process.exit(1);
@@ -47,7 +50,10 @@ export async function runUpdate(flags) {
47
50
  ui.ok(`Updated to ${latest}`);
48
51
  // Re-exec the NEWLY installed binary so migrations run with the new assets
49
52
  // and any new feature steps — not this (now-stale) process.
50
- const re = spawnSync("speclaw", ["update", "--migrate-only"], { stdio: "inherit", shell: winShell });
53
+ const re = spawnSync("speclaw", ["update", "--migrate-only"], {
54
+ stdio: "inherit",
55
+ shell: winShell,
56
+ });
51
57
  if (re.error) {
52
58
  ui.warn(`Upgraded — now run ${ui.code("speclaw update --migrate-only")} to apply project changes.`);
53
59
  return;
@@ -5,7 +5,11 @@ import { ui, c } from "../lib/ui.js";
5
5
  function openInBrowser(file) {
6
6
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
7
7
  try {
8
- spawn(cmd, [file], { stdio: "ignore", detached: true, shell: process.platform === "win32" }).unref();
8
+ spawn(cmd, [file], {
9
+ stdio: "ignore",
10
+ detached: true,
11
+ shell: process.platform === "win32",
12
+ }).unref();
9
13
  }
10
14
  catch {
11
15
  /* best-effort */
@@ -28,8 +32,10 @@ export async function runVisualize(flags) {
28
32
  limit: flags.limit ? Number(flags.limit) : undefined,
29
33
  });
30
34
  ui.step("Compass graph");
31
- ui.ok(c.bold(c.cream(String(r.shown))) + c.muted(" nodes · ") +
32
- c.bold(c.cream(String(r.links))) + c.muted(" edges") +
35
+ ui.ok(c.bold(c.cream(String(r.shown))) +
36
+ c.muted(" nodes · ") +
37
+ c.bold(c.cream(String(r.links))) +
38
+ c.muted(" edges") +
33
39
  (focus ? c.muted(` · focused on ${focus}`) : c.muted(` · top of ${r.total}`)));
34
40
  ui.info(`→ ${ui.code(".speclaw/graph.html")}`);
35
41
  if (!flags["no-open"]) {
@@ -39,6 +39,9 @@ export function list(value) {
39
39
  if (Array.isArray(value))
40
40
  return value;
41
41
  if (typeof value === "string")
42
- return value.split(",").map((s) => s.trim()).filter(Boolean);
42
+ return value
43
+ .split(",")
44
+ .map((s) => s.trim())
45
+ .filter(Boolean);
43
46
  return [];
44
47
  }
@@ -11,8 +11,7 @@ const PALETTE = {
11
11
  amber: [227, 179, 65], // #E3B341 — warning
12
12
  red: [235, 90, 90],
13
13
  };
14
- const colorOn = (Boolean(process.stdout.isTTY) || process.env.FORCE_COLOR === "1") &&
15
- !process.env.NO_COLOR;
14
+ const colorOn = (Boolean(process.stdout.isTTY) || process.env.FORCE_COLOR === "1") && !process.env.NO_COLOR;
16
15
  function paint(rgb, s) {
17
16
  if (!colorOn)
18
17
  return s;
@@ -56,7 +56,10 @@ async function fetchLatest(name) {
56
56
  * @returns True when `latest` is strictly newer than `current`.
57
57
  */
58
58
  export function isNewer(latest, current) {
59
- const parse = (v) => v.split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
59
+ const parse = (v) => v
60
+ .split("-")[0]
61
+ .split(".")
62
+ .map((n) => parseInt(n, 10) || 0);
60
63
  const a = parse(latest);
61
64
  const b = parse(current);
62
65
  for (let i = 0; i < 3; i++) {
@@ -79,7 +82,7 @@ export function isNewer(latest, current) {
79
82
  export async function checkForUpdates(opts = {}) {
80
83
  const current = pkgVersion();
81
84
  const cache = readCache();
82
- let latest = null;
85
+ let latest;
83
86
  if (!opts.force && cache && Date.now() - cache.checkedAt < TTL_MS) {
84
87
  latest = cache.latest;
85
88
  }
@@ -113,8 +116,17 @@ export async function maybeNotifyUpdate(cmd) {
113
116
  if (!updateAvailable || !latest)
114
117
  return;
115
118
  process.stderr.write("\n" +
116
- " " + c.amber("⬆ speclaw ") + c.muted(current + " → ") + c.cyan(latest) + c.muted(" available") + "\n" +
117
- " " + c.muted("run ") + c.cyan("speclaw update") + c.muted(" — upgrades and applies only what's new") + "\n\n");
119
+ " " +
120
+ c.amber(" speclaw ") +
121
+ c.muted(current + " → ") +
122
+ c.cyan(latest) +
123
+ c.muted(" available") +
124
+ "\n" +
125
+ " " +
126
+ c.muted("run ") +
127
+ c.cyan("speclaw update") +
128
+ c.muted(" — upgrades and applies only what's new") +
129
+ "\n\n");
118
130
  }
119
131
  catch {
120
132
  /* the notifier is best-effort — never let it break a command */
@@ -108,9 +108,7 @@ export function openDb(projectPath) {
108
108
  if (isStale(db))
109
109
  resetSchema(db);
110
110
  db.exec(SCHEMA);
111
- const row = db
112
- .prepare("SELECT value FROM meta WHERE key = 'schema_version'")
113
- .get();
111
+ const row = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
114
112
  if (!row) {
115
113
  db.prepare("INSERT INTO meta(key, value) VALUES ('schema_version', ?)").run(SCHEMA_VERSION);
116
114
  }
@@ -6,9 +6,23 @@ import { langForPath } from "./languages.js";
6
6
  import { extract } from "./extract.js";
7
7
  import { getEmbedder, toBlob } from "./embedder.js";
8
8
  const SKIP_DIRS = new Set([
9
- ".git", "node_modules", "dist", "build", ".next", "out", "coverage",
10
- "__pycache__", ".venv", "venv", ".speclaw", ".mypy_cache", ".pytest_cache",
11
- "vendor", "target", ".turbo", ".cache",
9
+ ".git",
10
+ "node_modules",
11
+ "dist",
12
+ "build",
13
+ ".next",
14
+ "out",
15
+ "coverage",
16
+ "__pycache__",
17
+ ".venv",
18
+ "venv",
19
+ ".speclaw",
20
+ ".mypy_cache",
21
+ ".pytest_cache",
22
+ "vendor",
23
+ "target",
24
+ ".turbo",
25
+ ".cache",
12
26
  ]);
13
27
  const MAX_FILE_BYTES = 1_500_000;
14
28
  function hashOf(content) {
@@ -57,8 +71,13 @@ export async function buildIndex(projectPath, onProgress) {
57
71
  const db = openDb(projectPath);
58
72
  const embedder = getEmbedder();
59
73
  const stats = {
60
- files: 0, nodes: 0, edges: 0, embeddings: 0,
61
- unchanged: 0, removed: 0, embedder: embedder.id,
74
+ files: 0,
75
+ nodes: 0,
76
+ edges: 0,
77
+ embeddings: 0,
78
+ unchanged: 0,
79
+ removed: 0,
80
+ embedder: embedder.id,
62
81
  };
63
82
  const existing = new Map();
64
83
  for (const row of db.prepare("SELECT id, path, hash FROM files").all()) {
@@ -112,7 +112,11 @@ export function explore(projectPath, query) {
112
112
  callers,
113
113
  otherMatches: matches.length > 1
114
114
  ? matches.slice(1).map((m) => ({
115
- name: m.name, kind: m.kind, file: m.file, line: m.start_line, signature: m.signature,
115
+ name: m.name,
116
+ kind: m.kind,
117
+ file: m.file,
118
+ line: m.start_line,
119
+ signature: m.signature,
116
120
  }))
117
121
  : undefined,
118
122
  };
@@ -74,8 +74,10 @@ export function registerCompass(server) {
74
74
  action: z.enum(["start", "stop", "status"]).describe("start, stop, or status"),
75
75
  },
76
76
  }, async ({ projectPath, action }) => {
77
- const result = action === "start" ? startWatch(projectPath)
78
- : action === "stop" ? stopWatch(projectPath)
77
+ const result = action === "start"
78
+ ? startWatch(projectPath)
79
+ : action === "stop"
80
+ ? stopWatch(projectPath)
79
81
  : watchStatus(projectPath);
80
82
  return text(result);
81
83
  });
@@ -3,8 +3,17 @@ import path from "node:path";
3
3
  import { buildIndex } from "./indexer.js";
4
4
  const active = new Map();
5
5
  const SKIP = new Set([
6
- ".git", "node_modules", "dist", "build", ".next", ".speclaw",
7
- "__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache",
6
+ ".git",
7
+ "node_modules",
8
+ "dist",
9
+ "build",
10
+ ".next",
11
+ ".speclaw",
12
+ "__pycache__",
13
+ ".venv",
14
+ "venv",
15
+ ".mypy_cache",
16
+ ".pytest_cache",
8
17
  ]);
9
18
  function scheduleReindex(projectPath, state) {
10
19
  if (state.timer)
@@ -15,9 +15,7 @@ export function doctor(projectPath) {
15
15
  checks.push({
16
16
  name: "ai-specs directory",
17
17
  ok: has("ai-specs"),
18
- detail: has("ai-specs")
19
- ? "present"
20
- : "missing — run the scaffold tool first",
18
+ detail: has("ai-specs") ? "present" : "missing — run the scaffold tool first",
21
19
  });
22
20
  checks.push({
23
21
  name: "LAWS.md constitution",
@@ -87,9 +85,7 @@ export function doctor(projectPath) {
87
85
  checks.push({
88
86
  name: "lawbook workflow",
89
87
  ok: has("lawbook"),
90
- detail: has("lawbook")
91
- ? "lawbook/ present"
92
- : "missing — run the `lawbook_init` tool",
88
+ detail: has("lawbook") ? "lawbook/ present" : "missing — run the `lawbook_init` tool",
93
89
  });
94
90
  checks.push({
95
91
  name: "Compass index",
@@ -7,30 +7,74 @@ import { AGENTS, configureAgent } from "../../shared/agents.js";
7
7
  import { emptyReport } from "../../shared/install.js";
8
8
  const profileShape = {
9
9
  project_name: z.string().describe("Short project name, e.g. the repo name"),
10
- project_description: z.string().optional().describe("One-line description of what the project does"),
10
+ project_description: z
11
+ .string()
12
+ .optional()
13
+ .describe("One-line description of what the project does"),
11
14
  organization: z.string().optional().describe("Company/team name"),
12
- stack_summary: z.string().optional().describe("e.g. 'Next.js 15 + TypeScript frontend, FastAPI + PostgreSQL backend'"),
13
- architecture: z.string().optional().describe("e.g. 'hexagonal architecture with bounded contexts'"),
14
- test_commands: z.string().optional().describe("Real commands, e.g. 'pytest backend/tests && npm run test'"),
15
- lint_commands: z.string().optional().describe("Real commands, e.g. 'ruff check . && npm run lint && tsc --noEmit'"),
15
+ stack_summary: z
16
+ .string()
17
+ .optional()
18
+ .describe("e.g. 'Next.js 15 + TypeScript frontend, FastAPI + PostgreSQL backend'"),
19
+ architecture: z
20
+ .string()
21
+ .optional()
22
+ .describe("e.g. 'hexagonal architecture with bounded contexts'"),
23
+ test_commands: z
24
+ .string()
25
+ .optional()
26
+ .describe("Real commands, e.g. 'pytest backend/tests && npm run test'"),
27
+ lint_commands: z
28
+ .string()
29
+ .optional()
30
+ .describe("Real commands, e.g. 'ruff check . && npm run lint && tsc --noEmit'"),
16
31
  branch_pattern: z.string().optional().describe("e.g. 'feature/<ticket-id>-<slug>'"),
17
32
  commit_style: z.string().optional().describe("e.g. 'conventional commits, imperative, English'"),
18
- custom_laws: z.string().optional().describe("Extra markdown appended to LAWS.md — project-specific binding rules the analysis surfaced"),
19
- compass_hints: z.string().optional().describe("Markdown bullets with the repo's real entrypoints and common traces, inserted into docs/compass.md"),
20
- base_standards_extra: z.string().optional().describe("Markdown with any project-specific cross-cutting rules, appended to docs/standards/base-standards.md"),
21
- modules_table: z.string().optional().describe("Markdown table of the repo's real modules/bounded contexts + one-line responsibility, for docs/standards/architecture.md"),
22
- layering_rules: z.string().optional().describe("Markdown describing the layers and their allowed dependencies, for docs/standards/architecture.md"),
23
- backend_layers: z.string().optional().describe("Markdown layer table (Layer | File | Responsibility) from the real backend, for docs/standards/backend-standards.md"),
24
- frontend_layers: z.string().optional().describe("Markdown layer table from the real frontend, for docs/standards/frontend-standards.md"),
25
- versioning_rules: z.string().optional().describe("The repo's versioning/release convention, for docs/standards/conventions.md"),
26
- documentation_extra: z.string().optional().describe("Repo-specific docstring notes (keep only the languages used, the enforced linter), appended to docs/standards/documentation.md"),
33
+ custom_laws: z
34
+ .string()
35
+ .optional()
36
+ .describe("Extra markdown appended to LAWS.md project-specific binding rules the analysis surfaced"),
37
+ compass_hints: z
38
+ .string()
39
+ .optional()
40
+ .describe("Markdown bullets with the repo's real entrypoints and common traces, inserted into docs/compass.md"),
41
+ base_standards_extra: z
42
+ .string()
43
+ .optional()
44
+ .describe("Markdown with any project-specific cross-cutting rules, appended to docs/standards/base-standards.md"),
45
+ modules_table: z
46
+ .string()
47
+ .optional()
48
+ .describe("Markdown table of the repo's real modules/bounded contexts + one-line responsibility, for docs/standards/architecture.md"),
49
+ layering_rules: z
50
+ .string()
51
+ .optional()
52
+ .describe("Markdown describing the layers and their allowed dependencies, for docs/standards/architecture.md"),
53
+ backend_layers: z
54
+ .string()
55
+ .optional()
56
+ .describe("Markdown layer table (Layer | File | Responsibility) from the real backend, for docs/standards/backend-standards.md"),
57
+ frontend_layers: z
58
+ .string()
59
+ .optional()
60
+ .describe("Markdown layer table from the real frontend, for docs/standards/frontend-standards.md"),
61
+ versioning_rules: z
62
+ .string()
63
+ .optional()
64
+ .describe("The repo's versioning/release convention, for docs/standards/conventions.md"),
65
+ documentation_extra: z
66
+ .string()
67
+ .optional()
68
+ .describe("Repo-specific docstring notes (keep only the languages used, the enforced linter), appended to docs/standards/documentation.md"),
27
69
  };
28
70
  // ─── The foundation module: analyze the repo, then write the constitution ───
29
71
  /** Register the foundation MCP tools (init_project, scaffold, configure_agent, doctor). */
30
72
  export function registerFoundation(server) {
31
73
  server.registerTool("init_project", {
32
74
  description: "START HERE to initialize speclaw in a project. Returns the analysis questionnaire the agent must answer by reading the target repo, plus the available skill packs. Do NOT guess answers — investigate the codebase (package.json, pyproject.toml, CI config, existing docs) and confirm the pack selection with the user before calling scaffold.",
33
- inputSchema: { projectPath: z.string().describe("Absolute path to the project to initialize") },
75
+ inputSchema: {
76
+ projectPath: z.string().describe("Absolute path to the project to initialize"),
77
+ },
34
78
  }, async () => {
35
79
  const packs = loadPacks();
36
80
  return text({
@@ -53,14 +97,19 @@ export function registerFoundation(server) {
53
97
  projectPath: z.string().describe("Absolute path to the project"),
54
98
  profile: z.object(profileShape).describe("Project profile gathered by analyzing the repo"),
55
99
  packs: z.array(z.string()).describe("Optional tool pack names (quality, workflow, agents)"),
56
- agents: z.array(z.string()).optional().describe(`Agent ids to configure (symlinks + MCP): ${AGENTS.map((a) => a.id).join(", ")}. Usually the CLI handles this; omit to write content only.`),
100
+ agents: z
101
+ .array(z.string())
102
+ .optional()
103
+ .describe(`Agent ids to configure (symlinks + MCP): ${AGENTS.map((a) => a.id).join(", ")}. Usually the CLI handles this; omit to write content only.`),
57
104
  },
58
105
  }, async ({ projectPath, profile, packs, agents }) => text(scaffold(projectPath, profile, packs, agents ?? [])));
59
106
  server.registerTool("configure_agent", {
60
107
  description: "Configure one agent's integration in an already-scaffolded project: create its IDE symlinks into ai-specs and register the speclaw MCP server in its config. Re-runnable; add agents one at a time.",
61
108
  inputSchema: {
62
109
  projectPath: z.string().describe("Absolute path to the project"),
63
- agent: z.enum(AGENTS.map((a) => a.id)).describe("Agent id to configure"),
110
+ agent: z
111
+ .enum(AGENTS.map((a) => a.id))
112
+ .describe("Agent id to configure"),
64
113
  },
65
114
  }, async ({ projectPath, agent }) => {
66
115
  const report = emptyReport();
@@ -115,7 +115,12 @@ export function specValidate(projectPath, change) {
115
115
  const changeDir = path.join(specRoot(projectPath), "changes", change);
116
116
  const issues = [];
117
117
  if (!fs.existsSync(changeDir)) {
118
- return { change, valid: false, issues: [`change "${change}" not found under lawbook/changes/`], deltaSpecs: [] };
118
+ return {
119
+ change,
120
+ valid: false,
121
+ issues: [`change "${change}" not found under lawbook/changes/`],
122
+ deltaSpecs: [],
123
+ };
119
124
  }
120
125
  if (!fs.existsSync(path.join(changeDir, "proposal.md")))
121
126
  issues.push("missing proposal.md");
@@ -138,7 +143,12 @@ export function specValidate(projectPath, change) {
138
143
  issues.push(`${rel}: no "### Requirement:" header`);
139
144
  }
140
145
  }
141
- return { change, valid: issues.length === 0, issues, deltaSpecs: deltas.map((f) => path.relative(projectPath, f)) };
146
+ return {
147
+ change,
148
+ valid: issues.length === 0,
149
+ issues,
150
+ deltaSpecs: deltas.map((f) => path.relative(projectPath, f)),
151
+ };
142
152
  }
143
153
  /**
144
154
  * Promote a change's delta specs into the canonical specs/, overwriting the
@@ -214,7 +224,8 @@ export function specList(projectPath) {
214
224
  const abs = path.join(root, rel);
215
225
  if (!fs.existsSync(abs))
216
226
  return [];
217
- return fs.readdirSync(abs, { withFileTypes: true })
227
+ return fs
228
+ .readdirSync(abs, { withFileTypes: true })
218
229
  .filter((e) => e.isDirectory() && e.name !== "archive")
219
230
  .map((e) => e.name);
220
231
  };
@@ -47,7 +47,10 @@ export function registerSpec(server) {
47
47
  inputSchema: {
48
48
  projectPath: z.string().describe("Absolute path to the project"),
49
49
  change: z.string().describe("Change name (folder under lawbook/changes/)"),
50
- date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Today's date, YYYY-MM-DD"),
50
+ date: z
51
+ .string()
52
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
53
+ .describe("Today's date, YYYY-MM-DD"),
51
54
  },
52
55
  }, async ({ projectPath, change, date }) => text(specArchive(projectPath, change, date)));
53
56
  }
@@ -1,12 +1,42 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { ensureGitignore } from "./install.js";
3
4
  /** The agents speclaw can configure. Add one here and the CLI picks it up. */
4
5
  export const AGENTS = [
5
- { id: "claude", label: "Claude Code", ideDir: ".claude", linkTargets: ["skills", "commands", "agents"], mcpFile: ".mcp.json" },
6
- { id: "cursor", label: "Cursor", ideDir: ".cursor", linkTargets: ["skills", "commands", "rules"], mcpFile: ".cursor/mcp.json" },
7
- { id: "codex", label: "Codex", ideDir: ".codex", linkTargets: ["skills", "commands"], mcpFile: ".codex/mcp.json" },
8
- { id: "windsurf", label: "Windsurf", ideDir: ".windsurf", linkTargets: ["skills", "commands"], mcpFile: ".windsurf/mcp.json" },
9
- { id: "agents", label: "Generic (AGENTS.md)", ideDir: ".agents", linkTargets: ["skills", "agents"] },
6
+ {
7
+ id: "claude",
8
+ label: "Claude Code",
9
+ ideDir: ".claude",
10
+ linkTargets: ["skills", "commands", "agents"],
11
+ mcpFile: ".mcp.json",
12
+ },
13
+ {
14
+ id: "cursor",
15
+ label: "Cursor",
16
+ ideDir: ".cursor",
17
+ linkTargets: ["skills", "commands", "rules"],
18
+ mcpFile: ".cursor/mcp.json",
19
+ },
20
+ {
21
+ id: "codex",
22
+ label: "Codex",
23
+ ideDir: ".codex",
24
+ linkTargets: ["skills", "commands"],
25
+ mcpFile: ".codex/mcp.json",
26
+ },
27
+ {
28
+ id: "windsurf",
29
+ label: "Windsurf",
30
+ ideDir: ".windsurf",
31
+ linkTargets: ["skills", "commands"],
32
+ mcpFile: ".windsurf/mcp.json",
33
+ },
34
+ {
35
+ id: "agents",
36
+ label: "Generic (AGENTS.md)",
37
+ ideDir: ".agents",
38
+ linkTargets: ["skills", "agents"],
39
+ },
10
40
  ];
11
41
  /**
12
42
  * Look up a known agent definition by its id.
@@ -30,6 +60,10 @@ function isSymlink(p) {
30
60
  }
31
61
  /** Register the speclaw stdio MCP server in an agent's config file, merging into any existing config and skipping if already present. */
32
62
  function writeMcpConfig(projectPath, mcpFile, report) {
63
+ // The agent MCP config is per-developer wiring — regenerated by `speclaw init`
64
+ // / `agent add`, points at a local runner — not shared source. Keep it out of
65
+ // version control (done regardless of whether we write or skip the file below).
66
+ ensureGitignore(projectPath, mcpFile, "speclaw agent MCP config (per-developer; regenerated by init)", report);
33
67
  const mcpPath = path.join(projectPath, mcpFile);
34
68
  let config = {};
35
69
  if (fs.existsSync(mcpPath))
@@ -12,7 +12,10 @@ function manifestPath(projectPath) {
12
12
  export function readManifest(projectPath) {
13
13
  try {
14
14
  const m = JSON.parse(fs.readFileSync(manifestPath(projectPath), "utf8"));
15
- return { version: String(m.version ?? "0.0.0"), packs: Array.isArray(m.packs) ? m.packs.map(String) : [] };
15
+ return {
16
+ version: String(m.version ?? "0.0.0"),
17
+ packs: Array.isArray(m.packs) ? m.packs.map(String) : [],
18
+ };
16
19
  }
17
20
  catch {
18
21
  return null;
@@ -10,7 +10,10 @@ function readPkg() {
10
10
  const pkgPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
11
11
  try {
12
12
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
13
- cached = { name: String(pkg.name ?? "@esneiderbravo/speclaw"), version: String(pkg.version ?? "0.0.0") };
13
+ cached = {
14
+ name: String(pkg.name ?? "@esneiderbravo/speclaw"),
15
+ version: String(pkg.version ?? "0.0.0"),
16
+ };
14
17
  }
15
18
  catch {
16
19
  cached = { name: "@esneiderbravo/speclaw", version: "0.0.0" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@esneiderbravo/speclaw",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -38,6 +38,9 @@
38
38
  "build": "tsc && node scripts/copy-assets.mjs",
39
39
  "brand": "node scripts/render-brand.mjs",
40
40
  "start": "node dist/cli/index.js",
41
+ "lint": "eslint .",
42
+ "format": "prettier --write .",
43
+ "check": "prettier --check . && eslint .",
41
44
  "prepublishOnly": "npm run build"
42
45
  },
43
46
  "engines": {
@@ -52,8 +55,14 @@
52
55
  "zod": "^3.23.0"
53
56
  },
54
57
  "devDependencies": {
58
+ "@eslint/js": "^10.0.1",
55
59
  "@resvg/resvg-js": "^2.6.2",
56
60
  "@types/node": "^22.0.0",
57
- "typescript": "^5.6.0"
61
+ "eslint": "^10.8.0",
62
+ "eslint-config-prettier": "^10.1.8",
63
+ "globals": "^17.9.0",
64
+ "prettier": "^3.9.6",
65
+ "typescript": "^5.6.0",
66
+ "typescript-eslint": "^8.65.0"
58
67
  }
59
68
  }