@esneiderbravo/speclaw 0.3.13 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -72
- package/dist/cli/commands/index-build.js +12 -3
- package/dist/cli/commands/lawbook.js +1 -0
- package/dist/cli/commands/laws.js +149 -8
- package/dist/cli/commands/owners.js +44 -0
- package/dist/cli/commands/query.js +52 -10
- package/dist/cli/commands/update.js +35 -5
- package/dist/cli/commands/verify.js +8 -0
- package/dist/cli/index.js +15 -4
- package/dist/modules/compass/budget.js +128 -0
- package/dist/modules/compass/db.js +290 -30
- package/dist/modules/compass/diff-context.js +134 -0
- package/dist/modules/compass/embed-input.js +28 -0
- package/dist/modules/compass/embedder.js +3 -1
- package/dist/modules/compass/explore-rich.js +134 -0
- package/dist/modules/compass/extract.js +86 -0
- package/dist/modules/compass/hybrid.js +318 -0
- package/dist/modules/compass/impact-summary.js +33 -0
- package/dist/modules/compass/indexer.js +204 -33
- package/dist/modules/compass/merkle.js +76 -0
- package/dist/modules/compass/pagerank.js +122 -0
- package/dist/modules/compass/rank.js +95 -0
- package/dist/modules/compass/register.js +169 -75
- package/dist/modules/foundation/check.js +4 -2
- package/dist/modules/foundation/compile-laws.js +212 -0
- package/dist/modules/foundation/context-budget.js +1 -14
- package/dist/modules/foundation/dialects/agentsmd.js +95 -0
- package/dist/modules/foundation/dialects/claude-cursor.js +45 -0
- package/dist/modules/foundation/dialects/coderabbit.js +27 -0
- package/dist/modules/foundation/dialects/copilot.js +35 -0
- package/dist/modules/foundation/dialects/index.js +5 -0
- package/dist/modules/foundation/dialects/types.js +58 -0
- package/dist/modules/foundation/doctor.js +266 -14
- package/dist/modules/foundation/import-rules.js +67 -0
- package/dist/modules/foundation/integrity.js +307 -0
- package/dist/modules/foundation/laws-parse.js +131 -0
- package/dist/modules/foundation/laws.js +5 -0
- package/dist/modules/foundation/lock.js +283 -0
- package/dist/modules/foundation/ownership.js +4 -0
- package/dist/modules/foundation/register-core.js +57 -88
- package/dist/modules/foundation/register.js +1 -21
- package/dist/modules/foundation/scaffold.js +25 -0
- package/dist/modules/foundation/scan.js +227 -0
- package/dist/modules/foundation/setup-tool.js +96 -0
- package/dist/modules/foundation/verify.js +9 -1
- package/dist/modules/lawbook/assets/commands/archive.md +1 -1
- package/dist/modules/lawbook/assets/commands/draft.md +1 -1
- package/dist/modules/lawbook/assets/commands/explore.md +1 -1
- package/dist/modules/lawbook/assets/commands/sync.md +2 -2
- package/dist/modules/lawbook/assets/skills/archive/SKILL.md +1 -1
- package/dist/modules/lawbook/assets/skills/archive/steps/03-validate-and-sync.md +3 -3
- package/dist/modules/lawbook/assets/skills/archive/steps/04-archive.md +1 -1
- package/dist/modules/lawbook/assets/skills/draft/steps/02-understand.md +1 -1
- package/dist/modules/lawbook/assets/skills/draft/steps/05-validate.md +1 -1
- package/dist/modules/lawbook/assets/skills/explore/steps/01-investigate.md +1 -1
- package/dist/modules/lawbook/assets/skills/quick/steps/02-implement.md +1 -1
- package/dist/modules/lawbook/assets/skills/sync/SKILL.md +1 -1
- package/dist/modules/lawbook/assets/skills/sync/steps/03-validate.md +1 -1
- package/dist/modules/lawbook/assets/skills/sync/steps/04-promote.md +1 -1
- package/dist/modules/lawbook/change-tool.js +90 -0
- package/dist/modules/lawbook/coverage.js +45 -6
- package/dist/modules/lawbook/ears.js +417 -0
- package/dist/modules/lawbook/engine.js +29 -0
- package/dist/modules/lawbook/register.js +96 -54
- package/dist/modules/lawbook/spec-items.js +4 -1
- package/dist/modules/team/owners.js +464 -0
- package/dist/modules/tools/register.js +4 -26
- package/dist/shared/deprecation.js +99 -0
- package/dist/shared/exposure.js +4 -19
- package/dist/shared/git.js +25 -0
- package/dist/shared/mcp.js +29 -3
- package/dist/shared/output-budget.js +68 -0
- package/dist/shared/tool-catalog.js +49 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -8,15 +8,17 @@
|
|
|
8
8
|
<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
|
<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
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-0E8E8E?labelColor=0B0F10&style=flat-square" alt="MIT"></a>
|
|
11
|
-
<img src="https://img.shields.io/badge/node-%E2%89%A522-0E8E8E?labelColor=0B0F10&style=flat-square" alt="Node >= 22">
|
|
11
|
+
<img src="https://img.shields.io/badge/node-%E2%89%A522.16-0E8E8E?labelColor=0B0F10&style=flat-square" alt="Node >= 22.16">
|
|
12
|
+
<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,
|
|
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
|
<img src="https://img.shields.io/badge/no_LLM_·_no_cloud-0E8E8E?labelColor=0B0F10&style=flat-square" alt="no LLM">
|
|
26
28
|
<img src="https://img.shields.io/badge/CLI_+_MCP-0E8E8E?labelColor=0B0F10&style=flat-square" alt="CLI + MCP">
|
|
29
|
+
<img src="https://img.shields.io/badge/8_canonical_tools-0E8E8E?labelColor=0B0F10&style=flat-square" alt="8 tools">
|
|
27
30
|
<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
|
|
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
|
-
|
|
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
|
|
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"> The suite —
|
|
112
|
+
## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="◆" align="absmiddle"> The suite — five modules
|
|
106
113
|
|
|
107
114
|
| Module | What it does |
|
|
108
115
|
| :-- | :-- |
|
|
109
|
-
| **Foundation** | The project's constitution: `LAWS.md` binding
|
|
110
|
-
| **Compass** |
|
|
111
|
-
| **Lawbook** |
|
|
112
|
-
| **
|
|
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)** | **~
|
|
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
|
|
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** |
|
|
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
|
|
165
|
-
| **archive** | Reconcile, then validate, promote, and move the change to `lawbook/changes/archive/` — **in the same PR
|
|
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
|
-
**
|
|
174
|
+
**Ceremony levels** (confirmed in `change.json`; missing ⇒ level 3):
|
|
168
175
|
|
|
169
|
-
|
|
|
170
|
-
| :-- | :-- |
|
|
171
|
-
|
|
|
172
|
-
|
|
|
173
|
-
|
|
|
174
|
-
|
|
|
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
|
|
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
|
|
181
|
-
>
|
|
182
|
-
>
|
|
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** —
|
|
187
|
-
- **MCP tools** — `
|
|
188
|
-
- **CLI** — `speclaw lawbook
|
|
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
|
|
191
|
-
|
|
192
|
-
|
|
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 & 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> —
|
|
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`,
|
|
220
|
-
|
|
221
|
-
committed**: only `ai-specs/`
|
|
222
|
-
`.speclaw.json` manifest) is
|
|
223
|
-
|
|
224
|
-
`speclaw init` (or `speclaw update`)** to
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
|
233
|
-
|
|
234
|
-
|
|
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"> Verify in CI
|
|
269
273
|
|
|
270
274
|
`speclaw verify` evaluates your `deps` and `graph` laws against the local Compass
|
|
271
|
-
index.
|
|
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** (
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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"> Requirements
|
|
327
343
|
|
|
328
|
-
- **Node.js ≥ 22** — uses
|
|
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) ·
|
|
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(
|
|
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,
|
|
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.
|
|
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>` —
|
|
6
|
-
*
|
|
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(
|
|
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
|
+
}
|