@esneiderbravo/speclaw 0.3.1 → 0.3.3

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.
@@ -2,6 +2,8 @@ import { z } from "zod";
2
2
  import { text } from "../../shared/mcp.js";
3
3
  import { scaffold } from "./scaffold.js";
4
4
  import { doctor } from "./doctor.js";
5
+ import { checkAction } from "./check.js";
6
+ import { verifyLaws } from "./verify.js";
5
7
  import { loadPacks } from "../tools/packs.js";
6
8
  import { AGENTS, configureAgent } from "../../shared/agents.js";
7
9
  import { emptyReport } from "../../shared/install.js";
@@ -116,6 +118,34 @@ export function registerFoundation(server) {
116
118
  configureAgent(projectPath, agent, report);
117
119
  return text(report);
118
120
  });
121
+ server.registerTool("speclaw_check", {
122
+ // ≤12 words: this is invoked by speclaw's hooks, never called directly.
123
+ description: "Invoked by speclaw's hooks to enforce laws — do not call directly.",
124
+ inputSchema: {
125
+ projectPath: z.string().describe("Absolute path to the project"),
126
+ event: z
127
+ .enum(["PreToolUse", "PostToolUse", "Stop", "InstructionsLoaded"])
128
+ .describe("The hook event that fired"),
129
+ toolName: z.string().optional().describe("The tool the agent is invoking, when relevant"),
130
+ payload: z.record(z.unknown()).describe("The raw hook event payload from the agent"),
131
+ },
132
+ }, async ({ projectPath, event, toolName, payload }) => text(checkAction({ projectPath, event: event, toolName, payload })));
133
+ server.registerTool("law_verify", {
134
+ // ≤30 words: the batch counterpart to speclaw_check, for the Stop hook and CI.
135
+ description: "Verify the project's deterministic laws (dependency and graph rules) and return violations by file. Run before claiming an architecture task done.",
136
+ inputSchema: {
137
+ projectPath: z.string().describe("Absolute path to the project"),
138
+ paths: z
139
+ .array(z.string())
140
+ .optional()
141
+ .describe("Restrict to source files under these project-relative paths"),
142
+ engines: z
143
+ .array(z.enum(["deps", "graph"]))
144
+ .optional()
145
+ .describe("Which batch engines to run; omit for all"),
146
+ lawIds: z.array(z.string()).optional().describe("Restrict to these law ids"),
147
+ },
148
+ }, async ({ projectPath, paths, engines, lawIds }) => text(verifyLaws({ projectPath, paths, engines: engines, lawIds })));
119
149
  server.registerTool("doctor", {
120
150
  description: "Verify a speclaw installation: ai-specs presence, the foundation (LAWS.md + standards + agent contracts), IDE symlinks health, the lawbook/ workflow, the Compass index, and .mcp.json wiring. Returns a checklist with remediation hints.",
121
151
  inputSchema: { projectPath: z.string().describe("Absolute path to the project") },
@@ -8,6 +8,8 @@ import { installWorkflow } from "../lawbook/register.js";
8
8
  import { installPack, loadPacks } from "../tools/packs.js";
9
9
  import { readManifest, writeManifest } from "../../shared/manifest.js";
10
10
  import { pkgVersion } from "../../shared/version.js";
11
+ import { readLawManifest, seedManifest, writeLawManifest } from "./laws.js";
12
+ import { installHooks } from "./hooks.js";
11
13
  const ASSETS = assetsDir(import.meta.url);
12
14
  // Every {{var}} the foundation templates may reference. Ones the agent didn't
13
15
  // provide default to empty so a bare `scaffold` never leaves a raw {{tag}}.
@@ -22,6 +24,21 @@ const FOUNDATION_DEFAULTS = {
22
24
  versioning_rules: "",
23
25
  documentation_extra: "",
24
26
  };
27
+ /**
28
+ * Ensure the project has a law manifest, seeding it from the package's starter
29
+ * laws when absent. The manifest is a derived artifact under the gitignored
30
+ * `.speclaw/`; seeding only when missing keeps a curated manifest (the MVP's
31
+ * authoring surface until executable-laws) from being overwritten on update.
32
+ */
33
+ function ensureLawManifest(projectPath, report) {
34
+ const existing = readLawManifest(projectPath);
35
+ if (existing)
36
+ return existing;
37
+ const seed = seedManifest();
38
+ writeLawManifest(projectPath, seed);
39
+ report.written.push(path.join(projectPath, ".speclaw", "laws-manifest.json"));
40
+ return seed;
41
+ }
25
42
  /**
26
43
  * Render the foundation: walk the module's assets/, mirror its structure into
27
44
  * the project, stripping the `.template` marker (foo.template.md -> foo.md).
@@ -110,6 +127,15 @@ export function scaffold(projectPath, profile, packNames, agents = [], opts = {}
110
127
  ensureGitignore(projectPath, "ai-specs/", "speclaw workflow content (regenerated by init/update; never commit)", report);
111
128
  for (const id of agents)
112
129
  configureAgent(projectPath, id, report); // only the chosen agents
130
+ // Compile the declared laws into agent hooks for every hook-capable agent just
131
+ // configured. The seam is the manifest: check-dispatcher enforces `path` laws;
132
+ // executable-laws will extend the same manifest with more backends.
133
+ const lawManifest = ensureLawManifest(projectPath, report);
134
+ report.hooks = installHooks(projectPath, agents, lawManifest, report, {
135
+ baselines: managedOpts.baselines,
136
+ backup: managedOpts.backup,
137
+ record,
138
+ });
113
139
  // Record what was installed so `speclaw update` can re-apply these packs and
114
140
  // gate feature migrations by version, plus the managed-file baselines that let
115
141
  // a later update tell user edits from stale files.
@@ -0,0 +1,107 @@
1
+ import { performance } from "node:perf_hooks";
2
+ import { openDb, indexExists } from "../compass/db.js";
3
+ import { hasBatchBackend, readLawManifest } from "./laws.js";
4
+ import { runDepsLaw } from "./deps.js";
5
+ import { runGraphLaw } from "./graph.js";
6
+ /** True when `file` (POSIX, project-relative) is at or under one of `paths`. */
7
+ export function underPaths(file, paths) {
8
+ if (!paths || paths.length === 0)
9
+ return true;
10
+ return paths.some((p) => {
11
+ const norm = p.replace(/\/+$/, "");
12
+ return file === norm || file.startsWith(norm + "/");
13
+ });
14
+ }
15
+ /**
16
+ * Verify the project's deterministic `deps`/`graph` laws against the Compass
17
+ * index and return a four-state report.
18
+ *
19
+ * When the project has no index, every selected batch law is reported as
20
+ * `skipped` with reason `no-index` (never silently passed). Each evaluated law
21
+ * lands in exactly one of `passed` / `failed` / `unknown`: it fails when the
22
+ * engine produced a finding, is `unknown` when it produced none but rests on
23
+ * unresolved edges (which could hide a violation), and passes otherwise.
24
+ *
25
+ * @param args - The project, and optional `paths` / `engines` / `lawIds` filters.
26
+ * @returns The {@link VerifyReport}.
27
+ */
28
+ export function verifyLaws(args) {
29
+ const start = performance.now();
30
+ const findings = [];
31
+ const skipped = [];
32
+ const unknown = [];
33
+ let passed = 0;
34
+ let failed = 0;
35
+ const done = () => ({
36
+ schemaVersion: 1,
37
+ summary: {
38
+ evaluated: passed + failed + unknown.length,
39
+ passed,
40
+ failed,
41
+ skipped: skipped.length,
42
+ unknown: unknown.length,
43
+ },
44
+ findings,
45
+ skipped,
46
+ unknown,
47
+ elapsedMs: performance.now() - start,
48
+ });
49
+ const manifest = readLawManifest(args.projectPath);
50
+ if (!manifest)
51
+ return done();
52
+ const engines = args.engines;
53
+ const selected = manifest.laws.filter((law) => {
54
+ if (!hasBatchBackend(law))
55
+ return false;
56
+ if (args.lawIds && !args.lawIds.includes(law.id))
57
+ return false;
58
+ if (engines && !engines.includes(law.verification.kind))
59
+ return false;
60
+ return true;
61
+ });
62
+ if (selected.length === 0)
63
+ return done();
64
+ if (!indexExists(args.projectPath)) {
65
+ for (const law of selected) {
66
+ skipped.push({
67
+ lawId: law.id,
68
+ reason: "no-index",
69
+ detail: "no .speclaw/index.db — build it with the compass_index tool",
70
+ });
71
+ }
72
+ return done();
73
+ }
74
+ const db = openDb(args.projectPath);
75
+ try {
76
+ for (const law of selected) {
77
+ let result;
78
+ try {
79
+ result =
80
+ law.verification.kind === "deps"
81
+ ? runDepsLaw(db, law, args.paths)
82
+ : runGraphLaw(db, law, args.paths);
83
+ }
84
+ catch (err) {
85
+ skipped.push({ lawId: law.id, reason: "engine-error", detail: err.message });
86
+ continue;
87
+ }
88
+ findings.push(...result.findings);
89
+ if (result.findings.length > 0) {
90
+ failed++;
91
+ }
92
+ else if (result.unresolved > 0) {
93
+ unknown.push({
94
+ lawId: law.id,
95
+ detail: `evaluated with ${result.unresolved} unresolved reference(s) — result unknown`,
96
+ });
97
+ }
98
+ else {
99
+ passed++;
100
+ }
101
+ }
102
+ }
103
+ finally {
104
+ db.close();
105
+ }
106
+ return done();
107
+ }
@@ -9,6 +9,7 @@ export const AGENTS = [
9
9
  ideDir: ".claude",
10
10
  linkTargets: ["skills", "commands", "agents"],
11
11
  mcpFile: ".mcp.json",
12
+ hooks: { file: ".claude/settings.json", key: "hooks" },
12
13
  },
13
14
  {
14
15
  id: "cursor",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@esneiderbravo/speclaw",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },