@esneiderbravo/speclaw 0.3.5 → 0.3.7

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
@@ -5,6 +5,8 @@
5
5
  <br/>
6
6
 
7
7
  <a href="https://www.npmjs.com/package/@esneiderbravo/speclaw"><img src="https://img.shields.io/npm/v/@esneiderbravo/speclaw?color=0E8E8E&labelColor=0B0F10&style=flat-square&label=npm" alt="npm"></a>
8
+ &nbsp;<a href="https://github.com/esneiderbravo/speclaw/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/esneiderbravo/speclaw/ci.yml?branch=main&label=CI&labelColor=0B0F10&style=flat-square&color=0E8E8E" alt="CI"></a>
9
+ &nbsp;<a href="https://www.npmjs.com/package/@esneiderbravo/speclaw?activeTab=versions"><img src="https://img.shields.io/badge/provenance-SLSA-0E8E8E?labelColor=0B0F10&style=flat-square" alt="npm provenance"></a>
8
10
  &nbsp;<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-0E8E8E?labelColor=0B0F10&style=flat-square" alt="MIT"></a>
9
11
  &nbsp;<img src="https://img.shields.io/badge/node-%E2%89%A522-0E8E8E?labelColor=0B0F10&style=flat-square" alt="Node >= 22">
10
12
 
@@ -29,32 +31,40 @@ One command. No cloud, no LLM, no API keys — <b>everything runs on your machin
29
31
  <br/>
30
32
 
31
33
  > [!TIP]
32
- > **Install once, then one command sets everything up.** Install speclaw globally, run
33
- > `speclaw init`, pick the agents you use, and speclaw scaffolds the project, indexes
34
- > your code, and hands your agent a ready-to-paste prompt to finish the setup.
34
+ > **One command. Detects your agents and wires only those.** Paste
35
+ > `npx @esneiderbravo/speclaw@latest init` speclaw detects Claude Code, Cursor,
36
+ > Codex, Windsurf, and generic `AGENTS.md` surfaces, scaffolds the constitution +
37
+ > lawbook, indexes your code, and registers the local MCP server — only for the
38
+ > agents you pick. This one-liner is a **stable contract** (see
39
+ > [CONTRIBUTING.md](CONTRIBUTING.md)); do not invent alternate install commands in
40
+ > directories or newsletters.
35
41
 
36
42
  <br/>
37
43
 
38
44
  ## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="◆" align="absmiddle">&nbsp; Quick start
39
45
 
40
- Install speclaw globally (once), then run `init` in your project root:
41
-
42
46
  ```bash
43
- npm i -g @esneiderbravo/speclaw
44
- speclaw init
47
+ npx @esneiderbravo/speclaw@latest init
45
48
  ```
46
49
 
47
50
  <p align="center">
48
51
  <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/terminal-quickstart.png" width="800" alt="speclaw init">
49
52
  </p>
50
53
 
51
- The `speclaw` command is now available everywhere — run `speclaw index`,
52
- `speclaw visualize`, or `speclaw lawbook …` directly in any project.
54
+ Prefer a global install for repeated CLI use?
55
+
56
+ ```bash
57
+ npm i -g @esneiderbravo/speclaw
58
+ speclaw init
59
+ ```
60
+
61
+ The `speclaw` command is then available everywhere — run `speclaw index`,
62
+ `speclaw doctor`, `speclaw visualize`, or `speclaw lawbook …` directly.
53
63
 
54
64
  `init` will:
55
65
 
56
- 1. **Ask which agents you use** (Claude Code, Cursor, Codex, …) — and configure
57
- only those. Add more later; nothing is forced on you.
66
+ 1. **Ask which agents you use** (Claude Code, Cursor, Codex, Windsurf, …) — and
67
+ configure only those. Add more later; nothing is forced on you.
58
68
  2. Write the **foundation** (constitution + standards) and the **lawbook workflow**,
59
69
  and compile your blocking laws into **agent hooks** for the agents that support them.
60
70
  3. **Index your code** with a live progress bar and a summary of what it found.
@@ -62,9 +72,23 @@ The `speclaw` command is now available everywhere — run `speclaw index`,
62
72
  5. Print a prompt to paste into your agent so it fills the constitution with your
63
73
  project's real architecture and conventions.
64
74
 
65
- Prefer not to install globally? A one-off `npx @esneiderbravo/speclaw init` works
66
- too (also `pnpm dlx` / `yarn dlx`) but installing globally means you can run the
67
- `speclaw` commands directly afterwards.
75
+ When something breaks, run `speclaw doctor --json` and paste it into an issue
76
+ (required on bug reports). Output is redacted by default.
77
+
78
+ <br/>
79
+
80
+ ## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="◆" align="absmiddle">&nbsp; Verify a release (provenance)
81
+
82
+ Every npm publish is signed via **Trusted Publishing (OIDC)** and carries a
83
+ SLSA provenance attestation tied to this repository and workflow. That proves
84
+ *where* the tarball was built — not that its contents are benign. Pair it with
85
+ your own review and (later) law-integrity pinning.
86
+
87
+ ```bash
88
+ npm audit signatures
89
+ # After downloading the tarball from the registry:
90
+ gh attestation verify <tarball> --owner esneiderbravo
91
+ ```
68
92
 
69
93
  <br/>
70
94
 
@@ -202,7 +226,10 @@ you** — commit your own skills and commands there if you want to.
202
226
 
203
227
  **Enforcement artifacts.** For agents that support hooks, speclaw merges its law
204
228
  hooks into that agent's settings (e.g. `.claude/settings.json`) **by identity** —
205
- it never touches hooks you added yourself. The compiled law manifest lives in
229
+ it never touches hooks you added yourself. Each speclaw `mcp_tool` hook includes
230
+ an `input` map Claude Code substitutes from the hook event (`${cwd}`,
231
+ `${hook_event_name}`, `${tool_input.file_path}`, …) so `speclaw_check` receives
232
+ `projectPath` / `event` / `payload`. The compiled law manifest lives in
206
233
  `.speclaw/laws-manifest.json` (gitignored, regenerated on `init`/`update`), and a
207
234
  context-coverage log in `.speclaw/context-log.jsonl` feeds `speclaw doctor`.
208
235
 
@@ -1,31 +1,63 @@
1
1
  import { doctor } from "../../modules/foundation/doctor.js";
2
- import { measureInstallBudget } from "../../modules/foundation/context-budget.js";
3
2
  import { ui } from "../lib/ui.js";
4
- /** Verify the installation, printing each check and exiting non-zero on failure. */
5
- export async function runDoctor(_flags) {
6
- const cwd = process.cwd();
7
- const checks = doctor(cwd);
3
+ const STATUS_MARK = {
4
+ ok: "ok",
5
+ warn: "warn",
6
+ error: "error",
7
+ skip: "skip",
8
+ };
9
+ function printHuman(report) {
8
10
  ui.heading("speclaw doctor");
9
- for (const c of checks) {
10
- if (c.ok)
11
- ui.ok(`${c.name}${c.detail}`);
12
- else
13
- ui.err(`${c.name} ${c.detail}`);
14
- }
15
- try {
16
- const m = measureInstallBudget(cwd);
17
- ui.ok(`context budget — profile ${m.profile} · ${m.toolCount} tools · ~${m.total} always-on tokens ` +
18
- `(A ${m.tools} · B ${m.skillsAndCommands} · C ${m.alwaysOnInstructions}; D path-scoped ${m.pathScoped} not in total)`);
19
- }
20
- catch (err) {
21
- ui.warn(`context budget — could not measure: ${err.message}`);
11
+ for (const section of report.sections) {
12
+ ui.plain();
13
+ ui.ok(`${section.id} [${STATUS_MARK[section.status]}]`);
14
+ for (const c of section.checks) {
15
+ const line = `${c.title.padEnd(24)} ${c.detail ?? c.status}`;
16
+ if (c.status === "ok" || c.status === "skip")
17
+ ui.ok(` ${line}`);
18
+ else if (c.status === "warn") {
19
+ ui.warn(` ${line}`);
20
+ if (c.remedy)
21
+ ui.plain(` → ${c.remedy}`);
22
+ }
23
+ else {
24
+ ui.err(` ${line}`);
25
+ if (c.remedy)
26
+ ui.plain(` → ${c.remedy}`);
27
+ }
28
+ }
22
29
  }
23
- const failed = checks.filter((c) => !c.ok).length;
24
30
  ui.plain();
25
- if (failed === 0)
31
+ const warns = report.sections.flatMap((s) => s.checks).filter((c) => c.status === "warn").length;
32
+ const errs = report.sections.flatMap((s) => s.checks).filter((c) => c.status === "error").length;
33
+ if (errs === 0 && warns === 0)
26
34
  ui.ok("Everything is within the law.");
35
+ else if (errs === 0) {
36
+ ui.warn(`${warns} warning(s). Run \`speclaw doctor --json\` and paste it into an issue.`);
37
+ }
38
+ else {
39
+ ui.err(`${errs} error(s), ${warns} warning(s).`);
40
+ }
41
+ }
42
+ /**
43
+ * Verify the installation. Supports `--json`, `--offline`, `--strict`,
44
+ * `--redact` (default) / `--no-redact`.
45
+ */
46
+ export async function runDoctor(flags) {
47
+ const cwd = process.cwd();
48
+ const redact = flags["no-redact"] ? false : true;
49
+ const report = await doctor(cwd, {
50
+ offline: Boolean(flags.offline),
51
+ redact,
52
+ });
53
+ if (flags.json) {
54
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
55
+ }
27
56
  else {
28
- ui.warn(`${failed} check(s) failed.`);
57
+ printHuman(report);
58
+ }
59
+ const strict = Boolean(flags.strict);
60
+ if (report.status === "error" || (strict && report.status === "warn")) {
29
61
  process.exit(1);
30
62
  }
31
63
  }
@@ -0,0 +1,16 @@
1
+ import { ui } from "../lib/ui.js";
2
+ /**
3
+ * Telemetry posture command. speclaw ships **no** telemetry code — `status`
4
+ * says so; enable/disable/log are rejected.
5
+ */
6
+ export async function runTelemetry(flags) {
7
+ const sub = flags._[0] ?? "status";
8
+ if (sub === "status") {
9
+ ui.ok("speclaw includes no telemetry — nothing is collected or transmitted.");
10
+ ui.plain("Policy: 100% local. There is no enable path and no analytics endpoint.");
11
+ return;
12
+ }
13
+ ui.err(`telemetry ${sub} is unavailable: speclaw does not include telemetry. ` +
14
+ "Run `speclaw telemetry status`.");
15
+ process.exit(1);
16
+ }
@@ -79,6 +79,19 @@ const MIGRATIONS = [
79
79
  "(no server-side defer_loading).\n" +
80
80
  "- Preserve all project-specific wording; only apply these speclaw-authored changes.",
81
81
  },
82
+ {
83
+ version: "0.3.6",
84
+ describe: "Doctor JSON diagnostics and stable install one-liner",
85
+ agentPrompt: "- Mention that `speclaw doctor --json` is the support report (redacted by default) " +
86
+ "and that the stable install command is `npx @esneiderbravo/speclaw@latest init`.\n" +
87
+ "- Preserve all project-specific wording; only apply these speclaw-authored changes.",
88
+ },
89
+ {
90
+ version: "0.3.7",
91
+ describe: "Claude Code mcp_tool hooks pass speclaw_check input via ${…} templates",
92
+ // scaffold → installHooks already rewrites .claude/settings.json when the
93
+ // compiled hook shape changes; no extra run() step.
94
+ },
82
95
  ];
83
96
  /**
84
97
  * Update speclaw and bring the current project up to date without a full re-init:
package/dist/cli/index.js CHANGED
@@ -35,8 +35,9 @@ Lawbook (spec-driven workflow)
35
35
  lawbook archive <c> Finalize and archive a change
36
36
 
37
37
  Other
38
- doctor Verify the installation
38
+ doctor Verify the installation (--json, --offline, --strict)
39
39
  budget Measure always-on context cost (tools, skills, instructions)
40
+ telemetry status Confirm speclaw ships no telemetry
40
41
  check Evaluate an action against the laws (hooks call this; --dry-run to preview)
41
42
  laws verify Verify the deterministic dependency/graph laws against the index
42
43
  verify Verify laws for CI: exit codes, --sarif, --json, --strict-engines
@@ -59,6 +60,7 @@ const HEADER_COMMANDS = new Set([
59
60
  "agent",
60
61
  "doctor",
61
62
  "budget",
63
+ "telemetry",
62
64
  "index",
63
65
  "watch",
64
66
  "lawbook",
@@ -68,7 +70,8 @@ const HEADER_COMMANDS = new Set([
68
70
  * header-eligible command AND stdout is an interactive terminal (so pipes,
69
71
  * redirection, and CI stay clean — mirroring the color gate in `ui.ts`). A
70
72
  * forced-color signal counts as interactive so the header is exercisable in a
71
- * child process. `budget --json` is machine-consumed and suppresses the header.
73
+ * child process. `budget --json` and `doctor --json` are machine-consumed and
74
+ * suppress the header.
72
75
  */
73
76
  function maybeHeader(cmd, flags) {
74
77
  if (!process.stdout.isTTY && process.env.FORCE_COLOR !== "1")
@@ -77,6 +80,8 @@ function maybeHeader(cmd, flags) {
77
80
  return;
78
81
  if (cmd === "budget" && flags.json)
79
82
  return;
83
+ if (cmd === "doctor" && flags.json)
84
+ return;
80
85
  header();
81
86
  }
82
87
  /** Run the handler for a single command. Returns when the command completes. */
@@ -121,6 +126,8 @@ async function dispatch(cmd, flags) {
121
126
  return (await import("./commands/doctor.js")).runDoctor(flags);
122
127
  case "budget":
123
128
  return (await import("./commands/budget.js")).runBudget(flags);
129
+ case "telemetry":
130
+ return (await import("./commands/telemetry.js")).runTelemetry(flags);
124
131
  case "check":
125
132
  return (await import("./commands/check.js")).runCheck(flags);
126
133
  case "laws":
@@ -165,6 +165,7 @@ export async function buildIndex(projectPath, onProgress) {
165
165
  )
166
166
  WHERE kind = 'call' AND dst_node_id IS NULL
167
167
  `);
168
+ db.prepare("INSERT INTO meta(key, value) VALUES ('indexed_at', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(new Date().toISOString());
168
169
  db.exec("COMMIT");
169
170
  }
170
171
  catch (err) {
@@ -1,12 +1,22 @@
1
+ import { z } from "zod";
1
2
  import { registerCompass } from "../compass/register.js";
2
3
  import { registerSpec } from "../lawbook/register.js";
3
4
  import { registerTools } from "../tools/register.js";
4
- import { registerFoundation } from "./register.js";
5
+ import { registerFoundationCore } from "./register-core.js";
5
6
  import { measureBudget } from "../../shared/budget.js";
6
- import { isMinimalMode, packageRoot } from "../../shared/exposure.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
+ };
7
14
  /**
8
15
  * Collect tool definitions as the MCP server would register them for a profile.
9
16
  *
17
+ * Uses `registerFoundationCore` plus a static `doctor` stub so budget/doctor
18
+ * measurement never imports the live `doctor` implementation (module cycle).
19
+ *
10
20
  * @param minimal - Exposure profile.
11
21
  */
12
22
  export function collectRegisteredTools(minimal) {
@@ -22,7 +32,10 @@ export function collectRegisteredTools(minimal) {
22
32
  };
23
33
  const opts = { minimal };
24
34
  const stub = server;
25
- registerFoundation(stub, opts);
35
+ registerFoundationCore(stub, opts);
36
+ if (shouldExpose("doctor", minimal)) {
37
+ tools.push(DOCTOR_TOOL_FOR_BUDGET);
38
+ }
26
39
  registerSpec(stub, opts);
27
40
  registerCompass(stub, opts);
28
41
  registerTools(stub, opts);