@esneiderbravo/speclaw 0.4.0 → 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 (45) 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 +32 -10
  7. package/dist/cli/commands/update.js +28 -0
  8. package/dist/cli/commands/verify.js +8 -0
  9. package/dist/cli/index.js +13 -4
  10. package/dist/modules/compass/budget.js +128 -0
  11. package/dist/modules/compass/db.js +290 -30
  12. package/dist/modules/compass/embed-input.js +28 -0
  13. package/dist/modules/compass/embedder.js +3 -1
  14. package/dist/modules/compass/explore-rich.js +10 -5
  15. package/dist/modules/compass/extract.js +86 -0
  16. package/dist/modules/compass/hybrid.js +318 -0
  17. package/dist/modules/compass/indexer.js +204 -33
  18. package/dist/modules/compass/merkle.js +76 -0
  19. package/dist/modules/compass/pagerank.js +122 -0
  20. package/dist/modules/compass/rank.js +95 -0
  21. package/dist/modules/compass/register.js +8 -4
  22. package/dist/modules/foundation/check.js +4 -2
  23. package/dist/modules/foundation/compile-laws.js +212 -0
  24. package/dist/modules/foundation/dialects/agentsmd.js +95 -0
  25. package/dist/modules/foundation/dialects/claude-cursor.js +45 -0
  26. package/dist/modules/foundation/dialects/coderabbit.js +27 -0
  27. package/dist/modules/foundation/dialects/copilot.js +35 -0
  28. package/dist/modules/foundation/dialects/index.js +5 -0
  29. package/dist/modules/foundation/dialects/types.js +58 -0
  30. package/dist/modules/foundation/doctor.js +220 -14
  31. package/dist/modules/foundation/import-rules.js +67 -0
  32. package/dist/modules/foundation/integrity.js +307 -0
  33. package/dist/modules/foundation/laws-parse.js +131 -0
  34. package/dist/modules/foundation/laws.js +5 -0
  35. package/dist/modules/foundation/lock.js +283 -0
  36. package/dist/modules/foundation/ownership.js +4 -0
  37. package/dist/modules/foundation/scaffold.js +25 -0
  38. package/dist/modules/foundation/scan.js +227 -0
  39. package/dist/modules/foundation/verify.js +9 -1
  40. package/dist/modules/lawbook/coverage.js +45 -6
  41. package/dist/modules/lawbook/ears.js +417 -0
  42. package/dist/modules/lawbook/engine.js +29 -0
  43. package/dist/modules/lawbook/spec-items.js +4 -1
  44. package/dist/modules/team/owners.js +464 -0
  45. package/package.json +4 -3
package/README.md CHANGED
@@ -8,15 +8,17 @@
8
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
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>
10
10
  &nbsp;<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-0E8E8E?labelColor=0B0F10&style=flat-square" alt="MIT"></a>
11
- &nbsp;<img src="https://img.shields.io/badge/node-%E2%89%A522-0E8E8E?labelColor=0B0F10&style=flat-square" alt="Node >= 22">
11
+ &nbsp;<img src="https://img.shields.io/badge/node-%E2%89%A522.16-0E8E8E?labelColor=0B0F10&style=flat-square" alt="Node >= 22.16">
12
+ &nbsp;<img src="https://img.shields.io/badge/v1.0-0E8E8E?labelColor=0B0F10&style=flat-square" alt="v1.0">
12
13
 
13
14
  <br/><br/>
14
15
 
15
16
  <p align="center">
16
17
  <b>AI agents are brilliant and blind</b> — brilliant at writing code, blind to <i>your</i>
17
- project's rules. <b>speclaw</b> hands them what they're missing: the codebase's
18
+ project's rules. <b>speclaw 1.0</b> hands them what they're missing: the codebase's
18
19
  <b>written laws</b> (a constitution built from your real code), a <b>local map</b> to
19
- navigate it without burning tokens, and a <b>disciplined workflow</b> for every change.
20
+ navigate it without burning tokens, a <b>disciplined workflow</b> for every change,
21
+ and the <b>gates</b> that make those laws hold in the editor, in CI, and in the PR.
20
22
  <br/>
21
23
  One command. No cloud, no LLM, no API keys — <b>everything runs on your machine.</b>
22
24
  </p>
@@ -24,6 +26,7 @@ One command. No cloud, no LLM, no API keys — <b>everything runs on your machin
24
26
  <img src="https://img.shields.io/badge/100%25_local-0E8E8E?labelColor=0B0F10&style=flat-square" alt="100% local">
25
27
  &nbsp;<img src="https://img.shields.io/badge/no_LLM_·_no_cloud-0E8E8E?labelColor=0B0F10&style=flat-square" alt="no LLM">
26
28
  &nbsp;<img src="https://img.shields.io/badge/CLI_+_MCP-0E8E8E?labelColor=0B0F10&style=flat-square" alt="CLI + MCP">
29
+ &nbsp;<img src="https://img.shields.io/badge/8_canonical_tools-0E8E8E?labelColor=0B0F10&style=flat-square" alt="8 tools">
27
30
  &nbsp;<img src="https://img.shields.io/badge/any_agent-0E8E8E?labelColor=0B0F10&style=flat-square" alt="any agent">
28
31
 
29
32
  </div>
@@ -59,16 +62,19 @@ speclaw init
59
62
  ```
60
63
 
61
64
  The `speclaw` command is then available everywhere — run `speclaw index`,
62
- `speclaw doctor`, `speclaw visualize`, or `speclaw lawbook …` directly.
65
+ `speclaw doctor`, `speclaw verify`, `speclaw owners --write`, or
66
+ `speclaw lawbook …` directly.
63
67
 
64
68
  `init` will:
65
69
 
66
70
  1. **Ask which agents you use** (Claude Code, Cursor, Codex, Windsurf, …) — and
67
71
  configure only those. Add more later; nothing is forced on you.
68
72
  2. Write the **foundation** (constitution + standards) and the **lawbook workflow**,
69
- and compile your blocking laws into **agent hooks** for the agents that support them.
73
+ compile your blocking laws into **agent hooks**, and create a committed
74
+ **`speclaw.lock`** baseline for rule-file integrity.
70
75
  3. **Index your code** with a live progress bar and a summary of what it found.
71
- 4. Register the speclaw **MCP server** in each chosen agent's config.
76
+ 4. Register the speclaw **MCP server** in each chosen agent's config
77
+ (eight canonical tools; use `--minimal` to omit setup/lifecycle tools).
72
78
  5. Print a prompt to paste into your agent so it fills the constitution with your
73
79
  project's real architecture and conventions.
74
80
 
@@ -82,7 +88,8 @@ When something breaks, run `speclaw doctor --json` and paste it into an issue
82
88
  Every npm publish is signed via **Trusted Publishing (OIDC)** and carries a
83
89
  SLSA provenance attestation tied to this repository and workflow. That proves
84
90
  *where* the tarball was built — not that its contents are benign. Pair it with
85
- your own review and (later) law-integrity pinning.
91
+ your own review and with **`speclaw.lock`** digests on rule files (see
92
+ [Verify in CI](#-verify-in-ci)).
86
93
 
87
94
  ```bash
88
95
  npm audit signatures
@@ -102,14 +109,15 @@ gh attestation verify <tarball> --owner esneiderbravo
102
109
 
103
110
  <br/>
104
111
 
105
- ## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="◆" align="absmiddle">&nbsp; The suite — four modules
112
+ ## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="◆" align="absmiddle">&nbsp; The suite — five modules
106
113
 
107
114
  | Module | What it does |
108
115
  | :-- | :-- |
109
- | **Foundation** | The project's constitution: `LAWS.md` binding a set of granular standards under `docs/standards/` (base, architecture, backend, frontend, testing, documentation, conventions, lawbook), plus strict `CLAUDE.md` / `AGENTS.md` agent contracts — filled from your real codebase. It also **enforces** them: blocking laws compile into agent hooks that deny a forbidden edit at the keystroke (`speclaw check` / `speclaw_check`), and architectural laws are verified deterministically against the Compass graph — dependency rules (`deps`) and cycles (`graph`) via `speclaw verify` (CI orchestrator: exit codes, SARIF, markdown) and `speclaw laws verify` / `law_verify`. Each law is reported as passed, failed, skipped, or unknown (an unresolved reference is *unknown*, never a silent pass). |
110
- | **Compass** | speclaw's own local code graph. Parses your code (tree-sitter) into nodes + edges plus a local vector store, so an agent finds and understands code with a fraction of the tokens a grep/read loop would cost. No LLM, 100% local, lives in `.speclaw/` (gitignored). |
111
- | **Lawbook** | speclaw's own spec-driven workflow: `draft build sync archive` (and `explore`), backed by `lawbook_*` engine tools. No external CLI. |
112
- | **Tools** | Opt-in packs of skills and subagents (currently the dev-agents) that agents use for specific tasks. |
116
+ | **Foundation** | The project's constitution: `LAWS.md` binding granular standards under `docs/standards/`, plus `CLAUDE.md` / `AGENTS.md`. **Enforced** via agent hooks (`speclaw check`), deterministic graph laws (`speclaw verify` / `laws verify`), multidialect compile (`laws compile`), and committed **`speclaw.lock`** digests + injection scan (`laws lock` / `accept` / `scan`). |
117
+ | **Compass** | Local code graph (tree-sitter → `node:sqlite`): hybrid find (FTS5 + vectors + RRF + PageRank), impact, affected-tests, hotspots, coupling, visualize. Schema **10**. No LLM lives in `.speclaw/` (gitignored). |
118
+ | **Lawbook** | Spec-driven workflow with **adaptive ceremony** (levels 0–3), bugfix changes + `lawbook_investigate`, EARS linting, requirement **coverage**, and sealed **drift** anchors. Loop: `explore draft build sync archive`. |
119
+ | **Team** | Declare `team.owners` in `lawbook/config.yaml`; `speclaw owners --write` compiles a managed trailing block in `.github/CODEOWNERS` (GitHub: last match wins). Doctor checks the posture. CLI-only — no MCP tool. |
120
+ | **Tools** | Opt-in packs of skills and subagents (currently the dev-agents). |
113
121
 
114
122
  Compass is inspired by [CodeGraph](https://github.com/colbymchenry/codegraph) and the Lawbook module by [OpenSpec](https://github.com/Fission-AI/openspec) — both MIT. speclaw reimplements the ideas as its own code and gives full credit; see [ATTRIBUTION.md](ATTRIBUTION.md).
115
123
 
@@ -123,16 +131,15 @@ tokenizer on this corpus — not a BPE dependency):
123
131
 
124
132
  | | Tokens |
125
133
  | :-- | --: |
126
- | **speclaw budget (always-on)** | **~12.9k** (8 MCP tools · ceiling **13.0k**) |
134
+ | **speclaw budget (always-on)** | **~13.7k** (8 MCP tools · ceiling **14.0k**) |
127
135
  | Spec Kit commands alone | ~18.6k ([spec-kit#1401](https://github.com/github/spec-kit/issues/1401)) |
128
136
 
129
137
  ```bash
130
138
  speclaw budget # human table
131
139
  speclaw budget --json # machine-readable; used by the suite gate
132
140
  speclaw coverage # requirement → impl → test coverage (TAP / table)
133
- speclaw coverage --json # machine-readable coverage report
134
141
  speclaw drift # sealed spec ↔ code drift (default --fail-on semantic)
135
- speclaw drift --reseal # photograph current bodies into lawbook/anchors/
142
+ speclaw owners --write # team.owners .github/CODEOWNERS
136
143
  speclaw init --minimal # omit setup/lifecycle MCP tools from registration
137
144
  ```
138
145
 
@@ -159,38 +166,40 @@ non-trivial lands without a spec change. It's a loop of five steps:
159
166
  | Step | What happens |
160
167
  | :-- | :-- |
161
168
  | **explore** | Think an idea through *before* committing to it — should we do this, and how. Writes nothing. |
162
- | **draft** | Capture the intent as a change under `lawbook/changes/<name>/` four artifacts plus a `reports/` folder, always (see below). |
169
+ | **draft** | Propose a **ceremony level** (0–3) from graph signals, confirm it, then scaffold only what that level needs under `lawbook/changes/<name>/`. |
163
170
  | **build** | Implement the tasks in order, keeping code and spec in agreement, and record test results under `reports/`. |
164
- | **sync** | Reconcile the delta specs against what was actually built, then promote them into the canonical `lawbook/specs/` — the always-true description of how the system behaves. |
165
- | **archive** | Reconcile, then validate, promote, and move the change to `lawbook/changes/archive/` — **in the same PR**, never a post-merge chore. Gated: refused while any task is unchecked, `reports/` is empty, or the specs are unsynced. |
171
+ | **sync** | Reconcile the delta specs against what was actually built, then promote them into the canonical `lawbook/specs/`. |
172
+ | **archive** | Reconcile, then validate, promote, and move the change to `lawbook/changes/archive/` — **in the same PR**. Gated on tasks, reports, synced specs, and coverage. |
166
173
 
167
- **Every `draft` writes four artifacts under `lawbook/changes/<name>/` none optional:**
174
+ **Ceremony levels** (confirmed in `change.json`; missing level 3):
168
175
 
169
- | Artifact | What it captures |
170
- | :-- | :-- |
171
- | `proposal.md` | The **why** motivation, what changes, non-goals, and whether migrations are needed. |
172
- | `specs/<capability>/spec.md` | The **delta specs** one per affected capability, normative and testable. |
173
- | `design.md` | The **how** approach, alternatives weighed, and the trade-offs behind the decision. |
174
- | `tasks.md` | The **plan** ordered, checkable steps, including the mandatory ones from `config.yaml`. |
176
+ | Level | When | Artifacts |
177
+ | :-- | :-- | :-- |
178
+ | **0** | One-liner / typo / docs-only | `speclaw quick` `record.md` + `reports/` |
179
+ | **1** | Small fix with a delta | `record.md` + `tasks.md` + ≥1 delta + `reports/` |
180
+ | **2** | Normal feature | `proposal.md` + tasks + deltas + `reports/` (`design.md` optional) |
181
+ | **3** | Full ceremony | proposal + design + tasks + deltas + `reports/` |
182
+ | **bug** | Regression / RCA | `speclaw lawbook draft --bug` → `bugfix.md` + investigate first |
175
183
 
176
- Plus a **`reports/`** folder — scaffolded at draft, filled at build with one report per discipline (`backend.md`, `frontend.md`, …) recording the real unit/integration/e2e results. Evidence of testing travels with the change, and `lawbook_archive` refuses to archive without it.
184
+ Plus a **`reports/`** folder — scaffolded at draft, filled at build with one report
185
+ per discipline (`backend.md`, `frontend.md`, `api.md`, …). `lawbook_archive`
186
+ refuses to archive without it.
177
187
 
178
188
  > [!NOTE]
179
189
  > **Delta specs are normative and testable.** Requirements use `SHALL`/`MUST`
180
- > under `### Requirement:` headers, each with one or more `#### Scenario:` blocks
181
- > whose acceptance criteria hold without production integrations. `lawbook_validate`
182
- > checks that the code matches what the spec promises before you sync or archive.
190
+ > under `### Requirement:` headers (EARS-friendly), each with `#### Scenario:`
191
+ > blocks. `lawbook_validate` checks structure; `speclaw coverage` tracks
192
+ > `req~…~N` impl/test via `// Covers:` comments.
183
193
 
184
194
  **Three ways to drive it — same engine, no external CLI:**
185
195
 
186
- - **In your agent** — the `/lawbook:explore`, `/lawbook:draft`, `/lawbook:build`, `/lawbook:sync`, `/lawbook:archive` commands (installed as skills).
187
- - **MCP tools** — eight canonical tools: `compass_explore`, `compass_find`, `compass_diff_context`, `compass_index`, `lawbook_change`, `lawbook_investigate`, `speclaw_setup`, `speclaw_check`.
188
- - **CLI** — `speclaw lawbook init | list | validate | sync | archive`.
196
+ - **In your agent** — `/lawbook:explore`, `/lawbook:draft`, `/lawbook:build`, `/lawbook:sync`, `/lawbook:archive` (and `/lawbook:quick`, investigate).
197
+ - **MCP tools** — eight canonical: `compass_explore`, `compass_find`, `compass_diff_context`, `compass_index`, `lawbook_change`, `lawbook_investigate`, `speclaw_setup`, `speclaw_check`.
198
+ - **CLI** — `speclaw lawbook …`, `speclaw quick`, `speclaw coverage`, `speclaw drift`.
189
199
 
190
- The workspace is committed under `lawbook/`: `specs/` (canonical), `changes/`
191
- (in-flight), `changes/archive/` (shipped), and `config.yaml` (the mandatory task
192
- steps every change must include). The standards themselves are amended the same
193
- way — through a spec change reviewed by a human.
200
+ The workspace is committed under `lawbook/`: `specs/`, `changes/`,
201
+ `changes/archive/`, `anchors/` (drift photographs), and `config.yaml`
202
+ (mandatory tasks, ceremony cuts, `team.owners`, EARS knobs).
194
203
 
195
204
  <br/>
196
205
 
@@ -204,7 +213,7 @@ still use Compass and the lawbook engine by calling the CLI from its shell.
204
213
  <p align="center"><b>CLI</b> — the installer &amp; operator, runs anywhere <code>node</code> does</p>
205
214
  <p align="center"><img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/terminal-cli.png" width="800" alt="speclaw CLI commands"></p>
206
215
 
207
- <p align="center"><b>MCP</b> — the integrated agent surface, auto-registered by <code>init</code></p>
216
+ <p align="center"><b>MCP</b> — eight canonical tools, auto-registered by <code>init</code></p>
208
217
  <p align="center"><img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/terminal-mcp.png" width="800" alt="speclaw MCP tools"></p>
209
218
 
210
219
  <br/>
@@ -216,26 +225,20 @@ still use Compass and the lawbook engine by calling the CLI from its shell.
216
225
  </p>
217
226
 
218
227
  **Committed vs. local.** Your **personalized source** is committed — `LAWS.md`,
219
- `CLAUDE.md`, `AGENTS.md`, `docs/standards/*`, `docs/compass.md`, and the
220
- `lawbook/` workspace. speclaw's **regenerable workflow content is local, not
221
- committed**: only `ai-specs/` (skills, commands, rules, agent packs, and its
222
- `.speclaw.json` manifest) is gitignored, because `init`/`update` reconstruct it
223
- from the package like a dependency. So **after cloning a speclaw project, run
224
- `speclaw init` (or `speclaw update`)** to regenerate `ai-specs/` locally, which
225
- the agent IDE symlinks point into. If a project committed `ai-specs/` before
226
- this behavior existed, `init`/`update` print the exact `git rm -r --cached
227
- ai-specs` command to stop tracking it (they never touch your git index
228
- themselves). The agent directories (`.claude/`, `.cursor/`, …) are **left to
229
- you** — commit your own skills and commands there if you want to.
228
+ `CLAUDE.md`, `AGENTS.md`, `docs/standards/*`, `docs/compass.md`, the `lawbook/`
229
+ workspace, and **`speclaw.lock`** (rule digests at the repo root). speclaw's
230
+ **regenerable workflow content is local, not committed**: only `ai-specs/`
231
+ (skills, commands, rules, agent packs, and its `.speclaw.json` manifest) is
232
+ gitignored, because `init`/`update` reconstruct it from the package. So **after
233
+ cloning a speclaw project, run `speclaw init` (or `speclaw update`)** to
234
+ regenerate `ai-specs/` locally. Optional **`team.owners`** in
235
+ `lawbook/config.yaml` compiles into a managed block at the end of
236
+ `.github/CODEOWNERS` via `speclaw owners --write`.
230
237
 
231
238
  **Enforcement artifacts.** For agents that support hooks, speclaw merges its law
232
- hooks into that agent's settings (e.g. `.claude/settings.json`) **by identity** —
233
- it never touches hooks you added yourself. Each speclaw `mcp_tool` hook includes
234
- an `input` map Claude Code substitutes from the hook event (`${cwd}`,
235
- `${hook_event_name}`, `${tool_input.file_path}`, …) so `speclaw_check` receives
236
- `projectPath` / `event` / `payload`. The compiled law manifest lives in
237
- `.speclaw/laws-manifest.json` (gitignored, regenerated on `init`/`update`), and a
238
- context-coverage log in `.speclaw/context-log.jsonl` feeds `speclaw doctor`.
239
+ hooks into that agent's settings **by identity** — it never touches hooks you
240
+ added yourself. The compiled law manifest lives in `.speclaw/laws-manifest.json`
241
+ (gitignored), and a context-coverage log feeds `speclaw doctor`.
239
242
 
240
243
  <br/>
241
244
 
@@ -261,27 +264,44 @@ So `speclaw init` compiles your blocking laws into agent hooks: a law marked
261
264
  and source. `speclaw check --dry-run --path <file>` previews what would block, and
262
265
  `speclaw doctor` reports how many of your laws actually reached the agent's
263
266
  context. Agents without hooks (Cursor, Codex) enforce the same laws in CI via
264
- `speclaw verify`.
267
+ `speclaw verify`. Digests in `speclaw.lock` catch silent edits to the rule files
268
+ themselves (the *Rules File Backdoor*).
265
269
 
266
270
  <br/>
267
271
 
268
272
  ## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="◆" align="absmiddle">&nbsp; Verify in CI
269
273
 
270
274
  `speclaw verify` evaluates your `deps` and `graph` laws against the local Compass
271
- index. It is deterministic: **no model, no API key, no network.**
275
+ index, and — when `speclaw.lock` is present compares digests of managed rule
276
+ files and scans them (plus skill packs) for known injection patterns. It is
277
+ deterministic: **no model, no API key, no network.**
272
278
 
273
279
  ```bash
274
280
  speclaw verify --ci --sarif speclaw.sarif --json speclaw.json
275
281
  ```
276
282
 
283
+ Create or refresh the committed lockfile (repo root, never under `.speclaw/`):
284
+
285
+ ```bash
286
+ speclaw laws lock
287
+ speclaw laws scan
288
+ speclaw laws accept AGENTS.md # interactive TTY only — never via MCP
289
+ ```
290
+
277
291
  | Exit | Meaning |
278
292
  | :-- | :-- |
279
293
  | **0** | No findings at or above `--fail-on` (default `error`) |
280
- | **1** | At least one finding at or above `--fail-on` |
294
+ | **1** | At least one finding at or above `--fail-on` (including strict integrity / scan errors) |
281
295
  | **2** | Usage error (unknown `--fail-on` / `--format`) |
282
296
  | **3** | Environment (shallow clone under `--ci`, or an unwritable `--sarif`/`--json` path) |
283
297
  | **4** | At least one law was skipped, and `--strict-engines` was set |
284
298
 
299
+ **Limits (honest):** digests catch any edit; the scanner catches known payload
300
+ shapes after Unicode normalization — not LLM-grade semantic injection. Digest
301
+ acceptance is a human gate (`laws accept` on a TTY). There is no Sigstore signing
302
+ of the lock in this release. Regenerable IDE mirrors (e.g. `.cursor/rules` →
303
+ `ai-specs/`) are not pinned as strict committed files.
304
+
285
305
  On GitHub:
286
306
 
287
307
  ```yaml
@@ -290,7 +310,8 @@ On GitHub:
290
310
 
291
311
  `init` / `update` write `.github/workflows/speclaw.yml` only when that path is
292
312
  missing — they never overwrite your CI. Make the check required in branch
293
- protection yourself; speclaw does not.
313
+ protection yourself; speclaw does not. Pair with `speclaw owners` + *Require
314
+ review from Code Owners* when you declare `team.owners`.
294
315
 
295
316
  <br/>
296
317
 
@@ -306,17 +327,12 @@ speclaw update
306
327
  `update` upgrades the global package **and** brings the current project up to date
307
328
  without a re-init, splitting files by who owns them:
308
329
 
309
- - **Managed files** (speclaw's workflow machinery the skills, commands, rules,
310
- and agent packs under `ai-specs/`) are **refreshed** to the new version, so
311
- improvements actually reach your project. They live locally (gitignored, see
312
- *What lands in your project*) and are reconstructed from the package. If you
313
- edited one locally, `update` reports the overwrite; pass `--backup` to keep a
314
- `<file>.bak` (itself gitignored) before it is refreshed.
315
- - **Personalized files** (your constitution and standards — `CLAUDE.md`,
316
- `AGENTS.md`, `LAWS.md`, `docs/standards/*`, `docs/compass.md`,
317
- `lawbook/config.yaml`) are **never auto-edited**. When a release changes their
318
- speclaw-authored content, `update` prints a prompt for **the agent you're
319
- using** to apply the change while preserving your project's specifics.
330
+ - **Managed files** (skills/commands/rules under `ai-specs/`) are **refreshed**.
331
+ Pass `--backup` to keep a `<file>.bak` before overwrite.
332
+ - **Personalized files** (`CLAUDE.md`, `AGENTS.md`, `LAWS.md`, `docs/standards/*`,
333
+ `docs/compass.md`, `lawbook/config.yaml`) are **never auto-edited** `update`
334
+ prints a prompt for the agent you're using.
335
+ - **`speclaw.lock`** and the CODEOWNERS owners block are refreshed when configured.
320
336
 
321
337
  - `speclaw update --check` — report whether an update exists, change nothing.
322
338
  - `NO_UPDATE_NOTIFIER=1` — silence the reminder.
@@ -325,7 +341,7 @@ without a re-init, splitting files by who owns them:
325
341
 
326
342
  ## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="◆" align="absmiddle">&nbsp; Requirements
327
343
 
328
- - **Node.js ≥ 22** — uses the built-in `node:sqlite`.
344
+ - **Node.js ≥ 22.16** — uses built-in `node:sqlite` (FTS5 for hybrid find).
329
345
  - **No native builds, no services, no API keys, no LLM download.** Tree-sitter
330
346
  parsers ship as WASM; the vector store is local.
331
347
 
@@ -338,6 +354,6 @@ without a re-init, splitting files by who owns them:
338
354
  [CodeGraph](https://github.com/colbymchenry/codegraph) &nbsp;·&nbsp;
339
355
  see [ATTRIBUTION.md](ATTRIBUTION.md)
340
356
 
341
- <i>speclaw · where specs become law</i>
357
+ <i>speclaw 1.0 · where specs become law</i>
342
358
 
343
359
  </div>
@@ -2,15 +2,24 @@ import { buildIndex } from "../../modules/compass/indexer.js";
2
2
  import { startWatch } from "../../modules/compass/watcher.js";
3
3
  import { ui, renderProgress, clearProgress } from "../lib/ui.js";
4
4
  /** (Re)build the Compass code graph for the cwd, showing progress and final stats. */
5
- export async function runIndex(_flags) {
5
+ export async function runIndex(flags) {
6
6
  const cwd = process.cwd();
7
7
  ui.step("Indexing with Compass");
8
8
  const start = Date.now();
9
- const stats = await buildIndex(cwd, (e) => renderProgress(e.done, e.total, e.file));
9
+ const stats = await buildIndex(cwd, {
10
+ force: Boolean(flags.force),
11
+ prune: Boolean(flags.prune),
12
+ maxCacheMB: flags["max-cache-mb"] ? Number(flags["max-cache-mb"]) : undefined,
13
+ retentionDays: flags.retention ? Number(flags.retention) : undefined,
14
+ onProgress: (e) => renderProgress(e.done, e.total, e.file),
15
+ });
10
16
  clearProgress();
11
17
  const secs = ((Date.now() - start) / 1000).toFixed(1);
18
+ const root = stats.rootUnchanged ? " · root unchanged" : "";
12
19
  ui.ok(`${stats.files} files · ${stats.nodes} nodes · ${stats.edges} edges · ` +
13
- `${stats.embeddings} embeddings · ${stats.unchanged} unchanged (${secs}s)`);
20
+ `${stats.computed} computed · ${stats.fromCache} fromCache · ` +
21
+ `${stats.unchanged} unchanged · ${stats.skippedByStat} skippedByStat` +
22
+ `${root} (${secs}s)`);
14
23
  }
15
24
  /** Build the initial index, then watch for file changes to keep it fresh. */
16
25
  export async function runWatch(_flags) {
@@ -72,6 +72,7 @@ export async function runSpec(flags) {
72
72
  return;
73
73
  }
74
74
  case "validate": {
75
+ // Covers: req~ears-cli-surface~1
75
76
  const r = specValidate(cwd, req(change, "lawbook validate <change>"));
76
77
  if (r.valid)
77
78
  ui.ok(`${r.change} is valid (${r.deltaSpecs.length} delta spec(s))`);
@@ -1,21 +1,107 @@
1
1
  import { list } from "../lib/args.js";
2
2
  import { ui, c } from "../lib/ui.js";
3
+ import * as clack from "@clack/prompts";
3
4
  import { verifyLaws } from "../../modules/foundation/verify.js";
5
+ import { compileLaws } from "../../modules/foundation/compile-laws.js";
6
+ import { importRulesFrom } from "../../modules/foundation/import-rules.js";
7
+ import { acceptLockPath, isInteractiveTty, refreshLockfile, verifyIntegrity, } from "../../modules/foundation/integrity.js";
8
+ import { digestText, prepareIntegrityText, readLockfile } from "../../modules/foundation/lock.js";
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import os from "node:os";
12
+ const LAWS_SUBS = "verify|compile|import|lock|accept|scan";
4
13
  /**
5
- * `speclaw laws <subcommand>` — the CLI twin of the batch law tools. Today it
6
- * exposes `verify`, the twin of the `law_verify` MCP tool: it runs the project's
7
- * deterministic `deps`/`graph` laws against the Compass index and prints the
8
- * four-state result. Both transports delegate to the same {@link verifyLaws}
9
- * core, so the CLI and the tool never diverge.
10
- *
11
- * - `laws verify [--engine deps,graph] [--path a,b] [--law id1,id2] [--json]`
14
+ * `speclaw laws <subcommand>` — verify (batch), compile (dialects), import (draft),
15
+ * lock / accept / scan (rule-file integrity).
12
16
  *
13
17
  * @param flags - Parsed CLI flags; `flags._[0]` is the subcommand.
14
18
  */
19
+ // Covers: req~laws-integrity-cli~1, req~laws-accept-human~1
15
20
  export async function runLaws(flags) {
16
21
  const sub = flags._[0];
22
+ if (sub === "compile") {
23
+ const agents = list(flags.agent);
24
+ const report = compileLaws({
25
+ projectPath: process.cwd(),
26
+ agents: agents.length ? agents : undefined,
27
+ });
28
+ if (flags.json) {
29
+ console.log(JSON.stringify(report, null, 2));
30
+ return;
31
+ }
32
+ ui.heading("speclaw laws compile");
33
+ ui.ok(`${report.lawCount} active · ${report.draftCount} draft · ` +
34
+ `${report.written.length} written · ${report.unchanged.length} unchanged` +
35
+ (report.failed.length ? ` · ${report.failed.length} failed` : ""));
36
+ for (const f of report.failed)
37
+ ui.warn(`${f.path}: ${f.error}`);
38
+ if (report.failed.length)
39
+ process.exit(1);
40
+ return;
41
+ }
42
+ if (sub === "import") {
43
+ const from = typeof flags.from === "string" ? flags.from : "";
44
+ if (!from) {
45
+ ui.err(`Usage: ${ui.code("speclaw laws import --from rulesync")}`);
46
+ process.exit(1);
47
+ }
48
+ try {
49
+ const report = importRulesFrom(process.cwd(), from);
50
+ if (flags.json) {
51
+ console.log(JSON.stringify(report, null, 2));
52
+ return;
53
+ }
54
+ ui.heading("speclaw laws import");
55
+ ui.ok(`${report.imported.length} imported · ${report.skipped.length} skipped`);
56
+ for (const id of report.imported)
57
+ ui.plain(` + ${c.cream(id)}`);
58
+ }
59
+ catch (err) {
60
+ ui.err(err.message);
61
+ process.exit(1);
62
+ }
63
+ return;
64
+ }
65
+ if (sub === "lock") {
66
+ const lock = refreshLockfile(process.cwd());
67
+ if (flags.json) {
68
+ console.log(JSON.stringify(lock, null, 2));
69
+ return;
70
+ }
71
+ ui.heading("speclaw laws lock");
72
+ ui.ok(`Wrote speclaw.lock — ${Object.keys(lock.files).length} file(s), ` +
73
+ `${Object.keys(lock.symlinks).length} symlink(s), root ${lock.root.slice(0, 19)}…`);
74
+ return;
75
+ }
76
+ if (sub === "scan") {
77
+ const report = verifyIntegrity({ projectPath: process.cwd(), checks: "scan" });
78
+ if (flags.json) {
79
+ console.log(JSON.stringify(report, null, 2));
80
+ return;
81
+ }
82
+ ui.heading("speclaw laws scan");
83
+ if (report.findings.length === 0) {
84
+ ui.ok("No injection findings.");
85
+ return;
86
+ }
87
+ for (const f of report.findings) {
88
+ const line = `${f.path}:${f.line}`;
89
+ const msg = `${c.cream(f.detector)} — ${line} ${f.message}`;
90
+ if (f.severity === "error")
91
+ ui.err(msg);
92
+ else
93
+ ui.warn(msg);
94
+ }
95
+ if (report.findings.some((f) => f.severity === "error"))
96
+ process.exit(1);
97
+ return;
98
+ }
99
+ if (sub === "accept") {
100
+ await runAccept(flags);
101
+ return;
102
+ }
17
103
  if (sub !== "verify") {
18
- ui.err(`Unknown laws subcommand: ${sub ?? "(none)"} — try ${ui.code("speclaw laws verify")}.`);
104
+ ui.err(`Unknown laws subcommand: ${sub ?? "(none)"} — try ${ui.code(`speclaw laws ${LAWS_SUBS}`)}.`);
19
105
  process.exit(1);
20
106
  }
21
107
  const engines = list(flags.engine).filter((e) => e === "deps" || e === "graph");
@@ -46,3 +132,58 @@ export async function runLaws(flags) {
46
132
  if (report.findings.length === 0 && summary.evaluated > 0)
47
133
  ui.ok("No violations.");
48
134
  }
135
+ async function runAccept(flags) {
136
+ const cwd = process.cwd();
137
+ if (!isInteractiveTty()) {
138
+ ui.err("`speclaw laws accept` requires an interactive TTY — digest acceptance is human-only.");
139
+ process.exit(1);
140
+ }
141
+ const rel = typeof flags._[1] === "string" ? flags._[1] : "";
142
+ if (!rel) {
143
+ ui.err(`Usage: ${ui.code("speclaw laws accept <path>")}`);
144
+ process.exit(1);
145
+ }
146
+ const lock = readLockfile(cwd);
147
+ if (!lock) {
148
+ ui.err("No speclaw.lock — run `speclaw laws lock` first.");
149
+ process.exit(1);
150
+ }
151
+ const abs = path.join(cwd, rel);
152
+ if (!fs.existsSync(abs)) {
153
+ ui.err(`File not found: ${rel}`);
154
+ process.exit(1);
155
+ }
156
+ const raw = prepareIntegrityText(rel, fs.readFileSync(abs, "utf8"));
157
+ const actual = digestText(raw);
158
+ const expected = lock.files[rel]?.digest;
159
+ ui.heading("speclaw laws accept");
160
+ ui.info(`${rel}`);
161
+ if (expected)
162
+ ui.plain(` expected ${expected}`);
163
+ ui.plain(` actual ${actual}`);
164
+ if (expected === actual) {
165
+ ui.ok("Digest already matches the lock — nothing to accept.");
166
+ return;
167
+ }
168
+ const noteFlag = typeof flags.note === "string" ? flags.note : undefined;
169
+ const confirmed = await clack.confirm({
170
+ message: `Update speclaw.lock digest for ${rel}?`,
171
+ initialValue: false,
172
+ });
173
+ if (clack.isCancel(confirmed) || !confirmed) {
174
+ ui.warn("Accept cancelled — lockfile unchanged.");
175
+ process.exit(1);
176
+ }
177
+ let note = noteFlag;
178
+ if (!note) {
179
+ const n = await clack.text({
180
+ message: "Optional note for the accept audit trail",
181
+ placeholder: "why this digest is trusted",
182
+ });
183
+ if (!clack.isCancel(n) && n.trim())
184
+ note = n.trim();
185
+ }
186
+ const by = os.userInfo().username || process.env.USER || "unknown";
187
+ acceptLockPath(cwd, rel, { by, note });
188
+ ui.ok(`Accepted ${rel} — lock updated (by ${by}).`);
189
+ }
@@ -0,0 +1,44 @@
1
+ import { ui } from "../lib/ui.js";
2
+ import { checkOwners, writeOwners } from "../../modules/team/owners.js";
3
+ /**
4
+ * Spec-ownership CLI: compile `team.owners` into `.github/CODEOWNERS`.
5
+ * `--write` mutates; default / `--check` reports drift without writing.
6
+ */
7
+ // Covers: req~owners-cli~1
8
+ export async function runOwners(flags) {
9
+ const cwd = process.cwd();
10
+ const doWrite = Boolean(flags.write);
11
+ if (doWrite) {
12
+ try {
13
+ const result = writeOwners(cwd);
14
+ if (!result.written) {
15
+ ui.info(result.reason ?? "nothing to write");
16
+ return;
17
+ }
18
+ ui.ok(`wrote speclaw owners block (${result.capabilities} key(s)) → ${result.path.replace(cwd + "/", "")}`);
19
+ return;
20
+ }
21
+ catch (err) {
22
+ ui.err(err.message);
23
+ process.exit(1);
24
+ }
25
+ }
26
+ const check = checkOwners(cwd);
27
+ if (check.ok) {
28
+ ui.ok(check.detail);
29
+ return;
30
+ }
31
+ ui.err(check.detail);
32
+ if (check.expected !== undefined && flags.diff) {
33
+ ui.plain();
34
+ ui.step("expected");
35
+ console.log(check.expected);
36
+ ui.plain();
37
+ ui.step("actual");
38
+ console.log(check.actual ?? "(missing)");
39
+ }
40
+ else {
41
+ ui.info(`Run ${ui.code("speclaw owners --write")} to refresh, or ${ui.code("--diff")} to compare.`);
42
+ }
43
+ process.exit(1);
44
+ }
@@ -1,4 +1,5 @@
1
- import { explore, search, recall, impact, trace } from "../../modules/compass/query.js";
1
+ import { explore, impact, trace } from "../../modules/compass/query.js";
2
+ import { hybridSearch } from "../../modules/compass/hybrid.js";
2
3
  import { affectedTests } from "../../modules/compass/affected.js";
3
4
  import { hotspots, coupling } from "../../modules/compass/hotspots.js";
4
5
  import { diffContext, formatDiffContext } from "../../modules/compass/diff-context.js";
@@ -16,6 +17,9 @@ export async function runQuery(cmd, flags) {
16
17
  const cwd = process.cwd();
17
18
  const args = flags._;
18
19
  const asJson = Boolean(flags.json);
20
+ const focus = list(flags.focus);
21
+ const maxTokens = flags["max-tokens"] ? Number(flags["max-tokens"]) : undefined;
22
+ const explain = Boolean(flags.explain);
19
23
  try {
20
24
  switch (cmd) {
21
25
  case "explore": {
@@ -34,16 +38,34 @@ export async function runQuery(cmd, flags) {
34
38
  r.callers?.forEach((c) => ui.info(`${c.name} (${c.file}:${c.line})`));
35
39
  return;
36
40
  }
37
- case "search": {
38
- const hits = search(cwd, need(args[0], "search <query>"));
39
- ui.heading(`${hits.length} result(s)`);
40
- hits.forEach((h) => ui.info(`${h.name} (${h.kind}) ${h.file}:${h.line}`));
41
- return;
42
- }
41
+ case "search":
43
42
  case "recall": {
44
- const hits = await recall(cwd, need(args[0], 'recall "<query>"'));
45
- ui.heading(`${hits.length} result(s) by meaning`);
46
- hits.forEach((h) => ui.info(`${h.score.toFixed(3)} ${h.name} (${h.kind}) ${h.file}:${h.line}`));
43
+ const q = need(args[0], cmd === "recall" ? 'recall "<query>"' : "search <query>");
44
+ const mode = cmd === "recall" ? "concept" : "exact";
45
+ const result = await hybridSearch(cwd, q, {
46
+ mode,
47
+ focus: focus.length ? focus : undefined,
48
+ maxTokens,
49
+ });
50
+ if (asJson) {
51
+ console.log(JSON.stringify(result, null, 2));
52
+ return;
53
+ }
54
+ ui.heading(`${result.hits.length} hybrid hit(s) · route=${result.route} · tokens=${result.tokens}/${result.budget}`);
55
+ if (result.degraded.length)
56
+ ui.warn(`degraded: ${result.degraded.join(", ")}`);
57
+ if (result.focus.length)
58
+ ui.info(`focus: ${result.focus.join(", ")}`);
59
+ for (const h of result.hits) {
60
+ const sig = explain
61
+ ? ` [bm25=${h.signals.bm25Rank ?? "-"} knn=${h.signals.knnRank ?? "-"} name=${h.signals.nameRank ?? "-"} pr=${h.signals.pagerank.toFixed(4)} hops=${h.signals.hops} score=${h.signals.score.toFixed(4)}]`
62
+ : "";
63
+ ui.info(`${h.name} (${h.kind}) ${h.file}:${h.line}${sig}`);
64
+ }
65
+ if (explain && result.rendered) {
66
+ ui.heading("TreeContext");
67
+ console.log(result.rendered);
68
+ }
47
69
  return;
48
70
  }
49
71
  case "impact": {