@luizsantiago/spec-guardrails 3.6.0 → 3.8.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 +83 -201
- package/lib/cursor-hooks.js +96 -0
- package/lib/doctor.js +10 -4
- package/lib/gates.js +41 -0
- package/lib/install.js +3 -0
- package/lib/memory-doctor.js +80 -0
- package/package.json +3 -3
- package/scripts/memory_index.py +138 -1
- package/skills/agent-architecture.md +1 -1
- package/templates/GETTING_STARTED.md +2 -0
- package/templates/cursor/hooks/context-guard-edit.mjs +170 -0
- package/templates/cursor/hooks.json +12 -0
package/README.md
CHANGED
|
@@ -5,207 +5,136 @@
|
|
|
5
5
|
|
|
6
6
|
**Keep AI coding agents honest — specify the work, prove each step, verify independently.**
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
| --- | --- |
|
|
10
|
-
| **Problem** | Agents ship “looks good” with thin specs, missing evidence, and the same context that wrote the code declaring victory. |
|
|
11
|
-
| **Solution** | One kit, two deliberate modes: **Process** (Node only) for a flexible spec-driven workflow; **Brakes** (Node + Python) for the **full product** — structural gates that exit non-zero when paperwork or evidence is missing. You approve specs/tasks in both. |
|
|
12
|
-
| **Result** | Traceable `.specs/` memory, fewer fake finishes, cheaper turns (~70% less skill text on planning). Choose Process for light ceremony; add Python when you want the [Guarantees matrix](#guarantees-matrix) enforced automatically. |
|
|
13
|
-
|
|
14
|
-
npm: [`@luizsantiago/spec-guardrails`](https://www.npmjs.com/package/@luizsantiago/spec-guardrails) **3.6.x**
|
|
8
|
+
npm: [`@luizsantiago/spec-guardrails`](https://www.npmjs.com/package/@luizsantiago/spec-guardrails) **3.8.x**
|
|
15
9
|
|
|
16
10
|
---
|
|
17
11
|
|
|
18
|
-
##
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
npx @luizsantiago/spec-guardrails install
|
|
22
|
-
```
|
|
12
|
+
## What is Spec Guardrails?
|
|
23
13
|
|
|
24
|
-
|
|
14
|
+
**Spec Guardrails** is a **process kit** for AI-assisted software development. It does not replace your app, framework, or tests. It gives your agent a repeatable way to work:
|
|
25
15
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
16
|
+
1. **Write down the goal** before coding (`spec.md`)
|
|
17
|
+
2. **Break work into small jobs** (`tasks.md`)
|
|
18
|
+
3. **Implement in waves** with real checks
|
|
19
|
+
4. **Verify with fresh eyes** — proof, not “trust me”
|
|
30
20
|
|
|
31
|
-
|
|
21
|
+
Everything important lives in **`.specs/`** in your repo so the project remembers decisions across chats and teammates.
|
|
32
22
|
|
|
33
|
-
|
|
34
|
-
| --- | --- |
|
|
35
|
-
| `.cursor/skills/` + `.claude/skills/` + `.github/skills/` + `.codex/skills/` | Hub, phase references, sister skills (**shipped adapters** — same content, product-specific paths) |
|
|
36
|
-
| `.specs/guardrails/scripts/` | Python gate scripts (Brakes mode) |
|
|
37
|
-
| `.specs/STATE.md`, `.specs/features/`, … | Project memory (any agent) |
|
|
38
|
-
| `.cursor/rules/engineering-baseline.mdc` | Always-on Cursor rule |
|
|
23
|
+
**You** approve specs and tasks. **The agent** follows phase guides and runs checks. **Gates** (optional Python scripts) can **stop** the workflow when paperwork or evidence is missing.
|
|
39
24
|
|
|
40
|
-
**
|
|
41
|
-
|
|
42
|
-
Re-run `install` anytime to refresh skills; your `.specs/` decisions and `STATE.md` are kept.
|
|
43
|
-
|
|
44
|
-
### Governance focus (3.2+)
|
|
45
|
-
|
|
46
|
-
Spec Guardrails is a **single product**: governance, evidence, verification, and controlled execution for agentic software development — not a bundle of companion packages.
|
|
47
|
-
|
|
48
|
-
| Capability | What it does |
|
|
49
|
-
| --- | --- |
|
|
50
|
-
| **Artifact gates** | Structural quality for spec, tasks, and cross-artifact consistency — with **blocking / warning / info** severity |
|
|
51
|
-
| **Parallel waves** | `loop-plan` computes safe parallel groups; `workspace-prepare` isolates tasks in git worktrees |
|
|
52
|
-
| **Execution policy** | Budgets, path scope, and escalation rules in `.specs/config.yaml` — consult via `execution-policy` |
|
|
53
|
-
| **Independent verify** | Fresh-context verification with evidence-or-zero (`validate-state`) |
|
|
54
|
-
|
|
55
|
-
| Need | Command |
|
|
56
|
-
| --- | --- |
|
|
57
|
-
| First time / upgrade | `install` |
|
|
58
|
-
| Existing codebase | `project-init` (optional) |
|
|
59
|
-
| Something looks wrong | `doctor` |
|
|
60
|
-
| Full CLI list | `--help` |
|
|
25
|
+
> **Plain-language guide:** [Overview](docs/guide/Overview.md) · [How it works](docs/guide/How-it-works.md) · [Quick start](docs/guide/Quick-start.md)
|
|
61
26
|
|
|
62
27
|
---
|
|
63
28
|
|
|
64
|
-
##
|
|
29
|
+
## What it is not
|
|
65
30
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
| **Spec-driven** | Written plan before code | `spec.md` + `tasks.md`; evidence before “done” |
|
|
71
|
-
| **Brakes / Gates** | Structural stop-gates | Python scripts exit non-zero when paperwork or evidence is missing |
|
|
72
|
-
| **Loop** | Execute in waves | `loop-plan` picks the next jobs; sub-agents when files don’t overlap |
|
|
73
|
-
| **Graph** | Parallel task map | `task-graph.md` — safe parallelism without file collisions |
|
|
74
|
-
| **Memory** | Persistent project state | `.specs/` — specs, decisions, and handoff survive across chats |
|
|
75
|
-
|
|
76
|
-
**You** approve specs and tasks. **The agent** runs gates and implements. **Gates** exit non-zero when paperwork or evidence is missing.
|
|
77
|
-
|
|
78
|
-
Plain-language tour: [Home](docs/guide/Home.md) · [How it works](docs/guide/How-it-works.md) · [Quick start](docs/guide/Quick-start.md)
|
|
31
|
+
- Not a code generator or a new framework
|
|
32
|
+
- Not a vector database or “AI memory” that reads your whole codebase
|
|
33
|
+
- Not automatic push/merge/deploy — you stay in control of git tiers
|
|
34
|
+
- Not a replacement for code review or product judgment
|
|
79
35
|
|
|
80
36
|
---
|
|
81
37
|
|
|
82
|
-
##
|
|
83
|
-
|
|
84
|
-
Two modes, one package — pick how much rigor you want:
|
|
85
|
-
|
|
86
|
-
| Mode | Runtime | What you get | Best for |
|
|
87
|
-
| --- | --- | --- | --- |
|
|
88
|
-
| **Process** | Node.js 18+ | Spec-driven workflow, `.specs/` memory, progressive loading, independent `/verify` | Flexible ceremony, exploration, teams that enforce by review |
|
|
89
|
-
| **Brakes** | Node + **Python 3.10+** | Everything in Process **plus** Python gates from the [Guarantees matrix](#guarantees-matrix) — exit ≠ 0 → stop and fix | The **full Spec Guardrails** — traceability, evidence, and structural guarantees enforced automatically |
|
|
90
|
-
|
|
91
|
-
**Gates stay Python.** That is the product: Brakes mode is the complete version with automated enforcement. Process mode is the same loop without exit-code brakes — intentional, not incomplete.
|
|
92
|
-
|
|
93
|
-
Install Python when you want gates to fire; run `doctor` to confirm Brakes are available.
|
|
38
|
+
## Install (once per project)
|
|
94
39
|
|
|
95
|
-
|
|
40
|
+
```bash
|
|
41
|
+
npx @luizsantiago/spec-guardrails install
|
|
42
|
+
```
|
|
96
43
|
|
|
97
|
-
|
|
44
|
+
| Requirement | Purpose |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| **Node.js 18+** | Required — CLI and install |
|
|
47
|
+
| **Python 3.10+** | Optional — enables **Brakes mode** (automatic gates). Without Python you still get the full workflow in **Process mode** |
|
|
98
48
|
|
|
99
|
-
|
|
49
|
+
Re-run `install` after upgrading the package. Your `.specs/` notes and `STATE.md` are kept.
|
|
100
50
|
|
|
101
|
-
|
|
102
|
-
| --- | --- | --- | --- |
|
|
103
|
-
| Intent exists before code | `validate-spec` | Brakes | Hard gate |
|
|
104
|
-
| Tasks derive from requirements | `analyze-artifacts` | Brakes | Hard gate |
|
|
105
|
-
| Task shape and graph when needed | `validate-tasks` | Brakes | Hard gate |
|
|
106
|
-
| Requirements stay traceable | `validate-traceability` | Brakes | Hard gate |
|
|
107
|
-
| Quick evidence is complete | `validate-quick` | Brakes | Hard gate |
|
|
108
|
-
| Dependencies respected in Execute | `loop-plan` | Brakes | Hard gate |
|
|
109
|
-
| Parallel work is file-safe | `task-graph.md` + `validate-tasks` | Process + Brakes | Artifact + gate |
|
|
110
|
-
| Completion cites evidence | `validate-state` | Brakes | Hard gate |
|
|
111
|
-
| Commits follow policy | `check-commit` | Brakes | Hard gate |
|
|
112
|
-
| Lessons grounded after FAIL | `lessons` | Brakes | Hard gate |
|
|
113
|
-
| Verification is independent | `/verify` + `validate.md` | Process | Phase skill |
|
|
114
|
-
| Knowledge survives chats | `.specs/` + `archive-feature` | Process | Install + CLI |
|
|
51
|
+
Check readiness: `npx @luizsantiago/spec-guardrails doctor`
|
|
115
52
|
|
|
116
|
-
|
|
53
|
+
Works with **Cursor, Claude Code, GitHub Copilot, OpenAI Codex**, and other agents via root `AGENTS.md`. See [Platform parity](docs/guide/Platform-parity.md).
|
|
117
54
|
|
|
118
55
|
---
|
|
119
56
|
|
|
120
|
-
##
|
|
57
|
+
## How you use it day to day
|
|
121
58
|
|
|
122
|
-
|
|
59
|
+
You talk to the agent in **chat**, not the terminal. These are **agent commands** (phrases the agent understands):
|
|
123
60
|
|
|
124
|
-
|
|
|
125
|
-
|
|
|
126
|
-
|
|
|
127
|
-
|
|
|
128
|
-
|
|
|
129
|
-
|
|
|
130
|
-
|
|
|
61
|
+
| Command | When to use |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| `/specify` | Start a real feature — written requirements first |
|
|
64
|
+
| `/tasks` | Shopping list of jobs after you approve the spec |
|
|
65
|
+
| `/loop` | Implement — one wave at a time |
|
|
66
|
+
| `/verify` | Proof after the last job — use a **fresh** chat when possible |
|
|
67
|
+
| `/quick` | Tiny fix only (≤3 files, no big design decisions) |
|
|
131
68
|
|
|
132
|
-
|
|
69
|
+
**Typical path:** `/specify` → approve → `/tasks` → approve → `/loop` → `/verify` → `/archive`
|
|
133
70
|
|
|
134
|
-
|
|
71
|
+
You do **not** need to memorize CLI commands. The agent runs gates and helpers for you when **Brakes mode** is available.
|
|
135
72
|
|
|
136
|
-
|
|
73
|
+
**Full command reference:** [Agent commands](docs/guide/agent-commands.md)
|
|
137
74
|
|
|
138
|
-
|
|
75
|
+
---
|
|
139
76
|
|
|
140
|
-
|
|
77
|
+
## Two modes (same product)
|
|
141
78
|
|
|
142
|
-
|
|
|
79
|
+
| Mode | You need | What changes |
|
|
143
80
|
| --- | --- | --- |
|
|
144
|
-
| **
|
|
145
|
-
| **
|
|
146
|
-
| **Medium** | New feature, <10 tasks | `/specify` → `/tasks` → `/loop` → `/verify` → `/archive` |
|
|
147
|
-
| **Complex** | APIs, architecture, infra | + `/discuss`, `/plan`, optional security/QA on verify |
|
|
148
|
-
| **Parallel** | Splittable work | Above + `/task-graph` when 3+ tasks |
|
|
149
|
-
|
|
150
|
-
Rules and examples: [Concepts → Complexity tiers](docs/guide/concepts.md#complexity-tiers--how-the-agent-chooses-depth)
|
|
151
|
-
|
|
152
|
-
---
|
|
81
|
+
| **Process** | Node only | Full workflow + `.specs/` memory + phase skills |
|
|
82
|
+
| **Brakes** | Node + Python | Same + scripts that **exit non-zero** when specs, tasks, or evidence are incomplete |
|
|
153
83
|
|
|
154
|
-
|
|
84
|
+
Both are intentional. Add Python when you want automatic stop signs, not just checklists.
|
|
155
85
|
|
|
156
|
-
|
|
86
|
+
Details: [FAQ → Process vs Brakes](docs/guide/FAQ.md#process-vs-brakes)
|
|
157
87
|
|
|
158
|
-
|
|
159
|
-
| ---: | --- | --- | --- |
|
|
160
|
-
| 1 | **Hub** | Contract, complexity router, gate schedule | `agent-architecture.md` |
|
|
161
|
-
| 2 | **Reference** | One phase procedure per turn | `specify.md`, `implement.md`, `validate.md` |
|
|
162
|
-
| 3 | **Sister** (optional) | Cross-cutting depth, on demand | `engineering-standards.md`, `task-graph-engineering.md` |
|
|
163
|
-
| 4 | **Gate** | Automatic check at the boundary | `validate-spec`, `loop-plan`, `check-commit` |
|
|
88
|
+
---
|
|
164
89
|
|
|
165
|
-
|
|
90
|
+
## What lands in your repo
|
|
166
91
|
|
|
167
|
-
|
|
92
|
+
| Path | Role |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| `.cursor/skills/` (and Claude, Copilot, Codex trees) | Instructions the agent reads per phase |
|
|
95
|
+
| `.specs/STATE.md` | Where you are — active feature, next step |
|
|
96
|
+
| `.specs/features/NNN-slug/` | Spec, tasks, validation for each feature |
|
|
97
|
+
| `.specs/guardrails/scripts/` | Python gates (Brakes mode) |
|
|
98
|
+
| `.specs/config.yaml` | Optional project rules and policy |
|
|
168
99
|
|
|
169
100
|
---
|
|
170
101
|
|
|
171
|
-
##
|
|
102
|
+
## Optional capabilities (use when you need them)
|
|
172
103
|
|
|
173
|
-
|
|
104
|
+
Most projects start with Specify → Tasks → Loop → Verify only. Turn these on when the work warrants it:
|
|
174
105
|
|
|
175
|
-
|
|
|
106
|
+
| Capability | Plain purpose | Learn more |
|
|
176
107
|
| --- | --- | --- |
|
|
177
|
-
|
|
|
178
|
-
|
|
|
179
|
-
|
|
|
180
|
-
|
|
|
181
|
-
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
| Before “done” | `validate-state` | Fake PASS without test evidence |
|
|
185
|
-
| After Verify FAIL | `lessons` | Ungrounded “lessons learned” |
|
|
186
|
-
| After Verify PASS | `archive-feature` | (CLI) folds feature into domain memory |
|
|
187
|
-
|
|
188
|
-
Full reference: **[Gates](docs/guide/gates.md)** · [Guarantees matrix](docs/guide/Guarantees-matrix.md) · [Gates and guarantees](docs/guide/Gates-and-guarantees.md)
|
|
108
|
+
| **Memory search** | Find text in past specs and validations | [Memory guide](docs/guide/Memory.md) |
|
|
109
|
+
| **Context guards** | Check scope before edit or “done” | [Agent commands](docs/guide/agent-commands.md) · **Cursor:** auto via hooks (3.8+) |
|
|
110
|
+
| **Execution policy** | Limit paths, retries, dangerous ops | [Overview → Safety](docs/guide/Overview.md#safety-and-limits) |
|
|
111
|
+
| **Solution exploration** | Compare two+ implementations before committing | [Overview → Exploration](docs/guide/Overview.md#optional-exploration-mode) |
|
|
112
|
+
| **Semantic retrieval** | Search by meaning (needs OpenAI or Ollama) | [Memory → Semantic](docs/guide/Memory.md#semantic-search-optional) |
|
|
113
|
+
|
|
114
|
+
**Default:** semantic search is **off**. FTS + graph search work without any API.
|
|
189
115
|
|
|
190
116
|
---
|
|
191
117
|
|
|
192
118
|
## Documentation
|
|
193
119
|
|
|
194
|
-
|
|
|
120
|
+
| Start here | Best for |
|
|
195
121
|
| --- | --- |
|
|
196
|
-
| [
|
|
197
|
-
| [Architecture](docs/guide/Architecture.md) | Core vs platform adapters |
|
|
198
|
-
| [Agent commands](docs/guide/agent-commands.md) | Every `/specify`, `/loop`, `/verify`, … — purpose, when, examples |
|
|
122
|
+
| **[Overview](docs/guide/Overview.md)** | Complete simple picture — workflow, layers, team use |
|
|
199
123
|
| [Quick start](docs/guide/Quick-start.md) | First ten minutes |
|
|
200
|
-
| [
|
|
201
|
-
| [
|
|
202
|
-
| [Gates](docs/guide/gates.md) | How each gate works |
|
|
203
|
-
| [Platform parity](docs/guide/Platform-parity.md) | Shipped adapters (Cursor, Claude, Copilot, Codex) — core works with any agent |
|
|
204
|
-
| [Restart PRD seed](docs/guide/Restart-prd-seed.md) | Clean-project PRD template for a single-package restart |
|
|
124
|
+
| [How it works](docs/guide/How-it-works.md) | Story from idea to archive |
|
|
125
|
+
| [Memory](docs/guide/Memory.md) | `.specs/` memory and search — when and how much |
|
|
205
126
|
| [FAQ](docs/guide/FAQ.md) | Common questions |
|
|
206
|
-
| [Changelog](docs/CHANGELOG.md) | Full version history |
|
|
207
127
|
|
|
208
|
-
|
|
128
|
+
| Go deeper | Best for |
|
|
129
|
+
| --- | --- |
|
|
130
|
+
| [Agent commands](docs/guide/agent-commands.md) | Every `/specify`, `/loop`, CLI helper |
|
|
131
|
+
| [Guarantees matrix](docs/guide/Guarantees-matrix.md) | Product promises → mechanisms |
|
|
132
|
+
| [Gates](docs/guide/gates.md) | What each gate checks |
|
|
133
|
+
| [Architecture](docs/guide/Architecture.md) | Core vs platform adapters |
|
|
134
|
+
| [Concepts](docs/guide/concepts.md) | Spec-driven, loop, graph, tiers |
|
|
135
|
+
| [Changelog](docs/CHANGELOG.md) | Version history |
|
|
136
|
+
|
|
137
|
+
Doc index: [docs/guide/README.md](docs/guide/README.md)
|
|
209
138
|
|
|
210
139
|
---
|
|
211
140
|
|
|
@@ -215,59 +144,12 @@ Start after install: [Quick start](docs/guide/Quick-start.md) · [Agent commands
|
|
|
215
144
|
npx @luizsantiago/spec-guardrails install
|
|
216
145
|
```
|
|
217
146
|
|
|
218
|
-
|
|
219
|
-
| --- | --- |
|
|
220
|
-
| **3.6.x** | Hybrid retrieval (`memory-retrieve`); chunk FTS; optional semantic embed |
|
|
221
|
-
| **3.5.x** | Solution exploration (`solution-explore`) — compare candidates from approved spec |
|
|
222
|
-
| **3.4.x** | Contextual guards (`context-guard`); FTS memory search (`memory-search`) |
|
|
223
|
-
| **3.3.x** | Intent/effect policy — `check-path --op read|write|delete` and `effects` config block |
|
|
224
|
-
| **3.2.x** | Single-package focus; artifact gate severity labels; git worktree isolation CLI; execution policy (budget/scope/escalation) |
|
|
225
|
-
| **3.1.x** | Copilot/Codex/AGENTS.md adapters; doctor Process + Brakes scores; `validate-traceability` / `validate-quick`; `classify-change` / `feature-status` |
|
|
226
|
-
| **3.0.x** | Final name Spec Guardrails; `.specs/guardrails/`; no dual-path ([Migration](docs/guide/Migration.md)) |
|
|
227
|
-
| **2.2.x** | Seatbelt-era paths & markers; `doctor` Execute hints; docs split from README |
|
|
228
|
-
| **2.1.x** | `loop-plan` + parallel `/loop` waves |
|
|
229
|
-
| **2.0.x** | Package rename → `@luizsantiago/spec-seatbelt` (superseded by 3.0) |
|
|
230
|
-
| **1.1.x** | `project-init` for brownfield repos |
|
|
231
|
-
| **0.9.x** | `archive-feature` + domain memory merge |
|
|
232
|
-
|
|
233
|
-
Full history: [CHANGELOG](docs/CHANGELOG.md) · [Releases](https://github.com/luizssantiago92/spec-guardrails/releases) · [Stability policy](docs/guide/Stability-policy.md)
|
|
234
|
-
|
|
235
|
-
Lineage: `agentic-harness` → `spec-seatbelt` → **`spec-guardrails` (final)**. Run `install` once after switching. See [Migration](docs/guide/Migration.md).
|
|
147
|
+
See [CHANGELOG](docs/CHANGELOG.md) for release notes.
|
|
236
148
|
|
|
237
149
|
---
|
|
238
150
|
|
|
239
|
-
## Contributing
|
|
240
|
-
|
|
241
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md) — tests, gate freeze policy, local `npm run guardrails -- install`.
|
|
242
|
-
|
|
243
|
-
---
|
|
244
|
-
|
|
245
|
-
## Credits
|
|
246
|
-
|
|
247
|
-
Spec Guardrails adapts open ideas; we did not invent spec-driven phases, loop design, or task-graph rules.
|
|
248
|
-
|
|
249
|
-
### Core lineage
|
|
250
|
-
|
|
251
|
-
| Source | License | How we use it |
|
|
252
|
-
| --- | --- | --- |
|
|
253
|
-
| [tlc-spec-driven](https://github.com/tech-leads-club/agent-skills/tree/main/packages/skills-catalog/skills/(development)/tlc-spec-driven) | CC-BY-4.0 | Phase model, `.specs/` memory, gate lineage |
|
|
254
|
-
| [addyosmani/agent-skills](https://github.com/addyosmani/agent-skills) | MIT | Discuss patterns, definition-of-done |
|
|
255
|
-
| [graph-engineering](https://github.com/codejunkie99/graph-engineering) | MIT | Task-graph topology, stop rules, parallel merge |
|
|
256
|
-
|
|
257
|
-
### Loop & ecosystem
|
|
258
|
-
|
|
259
|
-
| Source | License | How we use it |
|
|
260
|
-
| --- | --- | --- |
|
|
261
|
-
| [loop-engineering](https://github.com/cobusgreyling/loop-engineering) | MIT | Operational loop patterns; `doctor` score metaphor |
|
|
262
|
-
| [Addy Osmani — Loop engineering](https://addyosmani.com/blog/loop-engineering/) | — | Essay lineage |
|
|
263
|
-
| [awesome-harness-engineering](https://github.com/ai-boost/awesome-harness-engineering) | CC0 | Ecosystem taxonomy |
|
|
264
|
-
|
|
265
|
-
### Adjacent (not vendored)
|
|
266
|
-
|
|
267
|
-
[DeepCode](https://github.com/HKUDS/DeepCode) · [RepoGraph](https://github.com/ozyyshr/RepoGraph)
|
|
268
|
-
|
|
269
|
-
Extended attribution: [docs/guide/credits.md](docs/guide/credits.md)
|
|
270
|
-
|
|
271
|
-
## License
|
|
151
|
+
## Contributing · Credits · License
|
|
272
152
|
|
|
273
|
-
|
|
153
|
+
- [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
154
|
+
- [Credits](docs/guide/credits.md)
|
|
155
|
+
- MIT — see [LICENSE](LICENSE)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { packagedAssetPath } from "./assets.js";
|
|
5
|
+
import { ensureDir, readFileSafe } from "./fs-utils.js";
|
|
6
|
+
|
|
7
|
+
export const CURSOR_HOOK_SCRIPT = ".cursor/hooks/context-guard-edit.mjs";
|
|
8
|
+
export const CURSOR_HOOKS_JSON = ".cursor/hooks.json";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Merge shipped hook entries without removing user hooks.
|
|
12
|
+
*
|
|
13
|
+
* @param {Record<string, unknown> | null | undefined} existing
|
|
14
|
+
* @param {Record<string, unknown>} template
|
|
15
|
+
* @returns {Record<string, unknown>}
|
|
16
|
+
*/
|
|
17
|
+
export function mergeCursorHooksConfig(existing, template) {
|
|
18
|
+
/** @type {Record<string, unknown>} */
|
|
19
|
+
const result =
|
|
20
|
+
existing && typeof existing === "object"
|
|
21
|
+
? structuredClone(existing)
|
|
22
|
+
: { version: 1, hooks: {} };
|
|
23
|
+
|
|
24
|
+
result.version = result.version ?? template.version ?? 1;
|
|
25
|
+
/** @type {Record<string, unknown[]>} */
|
|
26
|
+
const hooks =
|
|
27
|
+
result.hooks && typeof result.hooks === "object"
|
|
28
|
+
? /** @type {Record<string, unknown[]>} */ (result.hooks)
|
|
29
|
+
: {};
|
|
30
|
+
result.hooks = hooks;
|
|
31
|
+
|
|
32
|
+
/** @type {Record<string, unknown[]>} */
|
|
33
|
+
const templateHooks =
|
|
34
|
+
template.hooks && typeof template.hooks === "object"
|
|
35
|
+
? /** @type {Record<string, unknown[]>} */ (template.hooks)
|
|
36
|
+
: {};
|
|
37
|
+
|
|
38
|
+
for (const [event, entries] of Object.entries(templateHooks)) {
|
|
39
|
+
if (!Array.isArray(entries)) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const current = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
if (!entry || typeof entry !== "object") {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const command = /** @type {{ command?: string }} */ (entry).command;
|
|
49
|
+
if (!command || current.some((item) => item?.command === command)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
current.push(entry);
|
|
53
|
+
}
|
|
54
|
+
hooks[event] = current;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Install Cursor hooks that auto-run context-guard before write/edit tools.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} cwd
|
|
64
|
+
* @param {{ log?: (message: string) => void }} [options]
|
|
65
|
+
*/
|
|
66
|
+
export async function installCursorHooks(cwd, options = {}) {
|
|
67
|
+
const log = options.log ?? (() => {});
|
|
68
|
+
const hooksDir = path.join(cwd, ".cursor/hooks");
|
|
69
|
+
await ensureDir(hooksDir);
|
|
70
|
+
|
|
71
|
+
const scriptSource = packagedAssetPath("templates/cursor/hooks/context-guard-edit.mjs");
|
|
72
|
+
const scriptDest = path.join(cwd, CURSOR_HOOK_SCRIPT);
|
|
73
|
+
await fs.copyFile(scriptSource, scriptDest);
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
await fs.chmod(scriptDest, 0o755);
|
|
77
|
+
} catch {
|
|
78
|
+
// Windows may ignore chmod; node can still execute the script.
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const template = JSON.parse(
|
|
82
|
+
await readFileSafe(packagedAssetPath("templates/cursor/hooks.json")),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const hooksJsonPath = path.join(cwd, CURSOR_HOOKS_JSON);
|
|
86
|
+
let existing = null;
|
|
87
|
+
try {
|
|
88
|
+
existing = JSON.parse(await readFileSafe(hooksJsonPath));
|
|
89
|
+
} catch {
|
|
90
|
+
existing = null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const merged = mergeCursorHooksConfig(existing, template);
|
|
94
|
+
await fs.writeFile(hooksJsonPath, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
|
|
95
|
+
log(`✅ Cursor hooks → ${CURSOR_HOOKS_JSON} (context-guard on write/edit tools)`);
|
|
96
|
+
}
|
package/lib/doctor.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { resolvePython, resolveScriptsDir } from "./gates.js";
|
|
12
12
|
import { readFileSafe } from "./fs-utils.js";
|
|
13
13
|
import { listFeatureIds, readActiveFeatureFromState } from "./specs-utils.js";
|
|
14
|
+
import { resolveMemoryHint } from "./memory-doctor.js";
|
|
14
15
|
|
|
15
16
|
const execFileAsync = promisify(execFile);
|
|
16
17
|
|
|
@@ -368,7 +369,7 @@ export function topDoctorSuggestions(checks, limit = 3) {
|
|
|
368
369
|
/**
|
|
369
370
|
* @param {string} cwd
|
|
370
371
|
* @param {{ suggest?: boolean, json?: boolean }} [options]
|
|
371
|
-
* @returns {Promise<{ score: number, modes: ReturnType<typeof scoreDoctorModes>, checks: DoctorCheck[], suggestions: DoctorCheck[], executeHint: string | null, pythonMissing: boolean }>}
|
|
372
|
+
* @returns {Promise<{ score: number, modes: ReturnType<typeof scoreDoctorModes>, checks: DoctorCheck[], suggestions: DoctorCheck[], executeHint: string | null, memoryHint: string | null, pythonMissing: boolean }>}
|
|
372
373
|
*/
|
|
373
374
|
export async function doctor(cwd, options = {}) {
|
|
374
375
|
const checks = await runDoctorChecks(cwd);
|
|
@@ -377,18 +378,19 @@ export async function doctor(cwd, options = {}) {
|
|
|
377
378
|
const suggestions = topDoctorSuggestions(checks);
|
|
378
379
|
const activeFeature = await readActiveFeature(cwd);
|
|
379
380
|
const executeHint = await resolveExecuteHint(cwd, activeFeature);
|
|
381
|
+
const memoryHint = await resolveMemoryHint(cwd);
|
|
380
382
|
const pythonCheck = checks.find((check) => check.id === "python");
|
|
381
383
|
const pythonMissing = pythonCheck ? !pythonCheck.pass : false;
|
|
382
384
|
|
|
383
385
|
if (options.json) {
|
|
384
386
|
console.log(
|
|
385
387
|
JSON.stringify(
|
|
386
|
-
{ score, modes, checks, suggestions, executeHint, pythonMissing },
|
|
388
|
+
{ score, modes, checks, suggestions, executeHint, memoryHint, pythonMissing },
|
|
387
389
|
null,
|
|
388
390
|
2,
|
|
389
391
|
),
|
|
390
392
|
);
|
|
391
|
-
return { score, modes, checks, suggestions, executeHint, pythonMissing };
|
|
393
|
+
return { score, modes, checks, suggestions, executeHint, memoryHint, pythonMissing };
|
|
392
394
|
}
|
|
393
395
|
|
|
394
396
|
if (pythonMissing) {
|
|
@@ -427,5 +429,9 @@ export async function doctor(cwd, options = {}) {
|
|
|
427
429
|
console.log(`\nExecute hint:\n → ${executeHint}`);
|
|
428
430
|
}
|
|
429
431
|
|
|
430
|
-
|
|
432
|
+
if (memoryHint) {
|
|
433
|
+
console.log(`\nMemory hint:\n → ${memoryHint}`);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return { score, modes, checks, suggestions, executeHint, memoryHint, pythonMissing };
|
|
431
437
|
}
|
package/lib/gates.js
CHANGED
|
@@ -205,6 +205,47 @@ export async function runGuardrailsScript(command, args, options = {}) {
|
|
|
205
205
|
});
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Run a guardrails Python script and capture stdout/stderr.
|
|
210
|
+
*
|
|
211
|
+
* @param {string} command gate or aux command name
|
|
212
|
+
* @param {string[]} args forwarded to the Python script
|
|
213
|
+
* @param {{ cwd?: string }} [options]
|
|
214
|
+
* @returns {Promise<{ code: number, stdout: string, stderr: string }>}
|
|
215
|
+
*/
|
|
216
|
+
export async function runGuardrailsScriptCapture(command, args, options = {}) {
|
|
217
|
+
const scriptName = GUARDRAILS_SCRIPTS[command];
|
|
218
|
+
|
|
219
|
+
if (!scriptName) {
|
|
220
|
+
throw new Error(`Unknown guardrails command: ${command}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const cwd = options.cwd ?? process.cwd();
|
|
224
|
+
const scriptsDir = await resolveScriptsDir(cwd);
|
|
225
|
+
const scriptPath = path.join(cwd, scriptsDir, scriptName);
|
|
226
|
+
const python = await resolvePython();
|
|
227
|
+
|
|
228
|
+
if (!python) {
|
|
229
|
+
throw new Error("Python 3.10+ not found.");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return new Promise((resolve, reject) => {
|
|
233
|
+
let stdout = "";
|
|
234
|
+
let stderr = "";
|
|
235
|
+
const child = spawn(python.command, [...python.args, scriptPath, ...args], { cwd });
|
|
236
|
+
child.stdout.on("data", (chunk) => {
|
|
237
|
+
stdout += chunk;
|
|
238
|
+
});
|
|
239
|
+
child.stderr.on("data", (chunk) => {
|
|
240
|
+
stderr += chunk;
|
|
241
|
+
});
|
|
242
|
+
child.on("error", reject);
|
|
243
|
+
child.on("close", (code) => {
|
|
244
|
+
resolve({ code: code ?? 1, stdout, stderr });
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
208
249
|
/**
|
|
209
250
|
* Run a structural gate script installed under `.specs/guardrails/scripts/`.
|
|
210
251
|
*
|
package/lib/install.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
resolveAssetOverride,
|
|
14
14
|
} from "./constants.js";
|
|
15
15
|
import { installPlatformAdapters } from "./adapters.js";
|
|
16
|
+
import { installCursorHooks } from "./cursor-hooks.js";
|
|
16
17
|
import { ensureDir, readFileSafe, writeFileIfMissing } from "./fs-utils.js";
|
|
17
18
|
import { hasPython } from "./gates.js";
|
|
18
19
|
import { initGuardrailsMemory } from "./memory.js";
|
|
@@ -118,6 +119,8 @@ export async function install(options = {}) {
|
|
|
118
119
|
await installPlatformAdapters(cwd);
|
|
119
120
|
log("✅ Adapters → .cursorrules, CLAUDE.md, copilot-instructions.md, AGENTS.md, .codex/AGENTS.md");
|
|
120
121
|
|
|
122
|
+
await installCursorHooks(cwd, { log });
|
|
123
|
+
|
|
121
124
|
const gettingStartedCreated = await writeFileIfMissing(
|
|
122
125
|
path.join(cwd, ".specs/GETTING_STARTED.md"),
|
|
123
126
|
await readFileSafe(packagedAssetPath("templates/GETTING_STARTED.md")),
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { NPX } from "./constants.js";
|
|
2
|
+
import { hasPython, runGuardrailsScriptCapture } from "./gates.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {{
|
|
6
|
+
* database: string,
|
|
7
|
+
* exists: boolean,
|
|
8
|
+
* semantic_enabled: boolean,
|
|
9
|
+
* provider: string,
|
|
10
|
+
* entities: number,
|
|
11
|
+
* chunks: number,
|
|
12
|
+
* embeddings: number,
|
|
13
|
+
* has_artifacts: boolean,
|
|
14
|
+
* stale: boolean,
|
|
15
|
+
* }} MemoryStatus
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} cwd
|
|
20
|
+
* @returns {Promise<MemoryStatus | null>}
|
|
21
|
+
*/
|
|
22
|
+
export async function readMemoryStatus(cwd) {
|
|
23
|
+
if (!(await hasPython())) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const { code, stdout } = await runGuardrailsScriptCapture(
|
|
29
|
+
"memory-index",
|
|
30
|
+
["status", "--json"],
|
|
31
|
+
{ cwd },
|
|
32
|
+
);
|
|
33
|
+
if (code !== 0) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return JSON.parse(stdout);
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {MemoryStatus} status
|
|
44
|
+
* @returns {string | null}
|
|
45
|
+
*/
|
|
46
|
+
export function memoryHintFromStatus(status) {
|
|
47
|
+
if (!status.has_artifacts) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!status.exists) {
|
|
52
|
+
return `${NPX("memory-index rebuild")} — project artifacts exist but the search index is missing`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (status.stale) {
|
|
56
|
+
return `${NPX("memory-index rebuild")} — .specs/ changed since the last index rebuild`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
status.semantic_enabled &&
|
|
61
|
+
status.chunks > 0 &&
|
|
62
|
+
status.embeddings < Math.max(1, Math.ceil(status.chunks * 0.5))
|
|
63
|
+
) {
|
|
64
|
+
return `${NPX("memory-index embed")} — semantic search is enabled but most chunks lack embeddings`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {string} cwd
|
|
72
|
+
* @returns {Promise<string | null>}
|
|
73
|
+
*/
|
|
74
|
+
export async function resolveMemoryHint(cwd) {
|
|
75
|
+
const status = await readMemoryStatus(cwd);
|
|
76
|
+
if (!status) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return memoryHintFromStatus(status);
|
|
80
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luizsantiago/spec-guardrails",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.8.0",
|
|
4
|
+
"description": "Spec-driven process kit for AI coding agents: write goals in .specs/, break into tasks, implement in waves, verify with proof. Process mode (Node) or Brakes mode (Node + Python gates). Works with Cursor, Claude, Copilot, and Codex.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"spec-guardrails": "./index.js"
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"guardrails": "node index.js",
|
|
14
14
|
"test": "npm run test:node && npm run test:gates",
|
|
15
|
-
"test:node": "node --test test/install.test.js test/test_feature_init.test.js test/test_config.test.js test/test_archive.test.js test/test_delta_merge.test.js test/test_presets.test.js test/test_brownfield.test.js test/test_doctor.test.js test/test_token_cost.test.js test/test_next_steps.test.js test/test_classify_change.test.js test/test_feature_status.test.js test/test_agent_contract.test.js test/test_gates_python.test.js test/test_specs_utils.test.js test/test_validation_verdict.test.js test/test_execution_policy.test.js test/test_workspace_isolation.test.js test/test_adapter_registry.test.js test/test_context_guard.test.js test/test_solution_exploration.test.js",
|
|
15
|
+
"test:node": "node --test test/install.test.js test/test_feature_init.test.js test/test_config.test.js test/test_archive.test.js test/test_delta_merge.test.js test/test_presets.test.js test/test_brownfield.test.js test/test_doctor.test.js test/test_token_cost.test.js test/test_next_steps.test.js test/test_classify_change.test.js test/test_feature_status.test.js test/test_agent_contract.test.js test/test_gates_python.test.js test/test_specs_utils.test.js test/test_validation_verdict.test.js test/test_execution_policy.test.js test/test_workspace_isolation.test.js test/test_adapter_registry.test.js test/test_context_guard.test.js test/test_solution_exploration.test.js test/test_memory_doctor.test.js test/test_cursor_hooks.test.js",
|
|
16
16
|
"test:gates": "node test/run-gate-tests.mjs",
|
|
17
17
|
"prepublishOnly": "npm test"
|
|
18
18
|
},
|
package/scripts/memory_index.py
CHANGED
|
@@ -260,6 +260,82 @@ def chunk_spec_requirements(
|
|
|
260
260
|
return count
|
|
261
261
|
|
|
262
262
|
|
|
263
|
+
def chunk_task_bodies(
|
|
264
|
+
conn: sqlite3.Connection,
|
|
265
|
+
feature_id: str,
|
|
266
|
+
tasks_path: Path,
|
|
267
|
+
tasks_text: str,
|
|
268
|
+
now: str,
|
|
269
|
+
) -> int:
|
|
270
|
+
count = 0
|
|
271
|
+
matches = list(TASK_HEADING.finditer(tasks_text))
|
|
272
|
+
for index, match in enumerate(matches):
|
|
273
|
+
task_id = match.group("id").upper()
|
|
274
|
+
start = match.end()
|
|
275
|
+
end = matches[index + 1].start() if index + 1 < len(matches) else len(tasks_text)
|
|
276
|
+
body = tasks_text[start:end].strip()
|
|
277
|
+
if not body:
|
|
278
|
+
body = match.group("title").strip() or task_id
|
|
279
|
+
chunk_id = f"chunk:{feature_id}:task:{task_id}"
|
|
280
|
+
upsert_chunk(conn, chunk_id, task_id, "task", str(tasks_path), body, now)
|
|
281
|
+
count += 1
|
|
282
|
+
return count
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def prune_embeddings(conn: sqlite3.Connection) -> None:
|
|
286
|
+
conn.execute(
|
|
287
|
+
"""
|
|
288
|
+
DELETE FROM embeddings
|
|
289
|
+
WHERE chunk_id NOT IN (SELECT id FROM chunks)
|
|
290
|
+
OR chunk_id IN (
|
|
291
|
+
SELECT e.chunk_id
|
|
292
|
+
FROM embeddings e
|
|
293
|
+
JOIN chunks c ON c.id = e.chunk_id
|
|
294
|
+
WHERE e.text_hash != c.text_hash
|
|
295
|
+
)
|
|
296
|
+
"""
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def latest_artifact_mtime() -> float | None:
|
|
301
|
+
latest: float | None = None
|
|
302
|
+
candidates = [SPECS_DIR / "lessons.json", SPECS_DIR / "STATE.md"]
|
|
303
|
+
if FEATURES_DIR.is_dir():
|
|
304
|
+
for feature_dir in FEATURES_DIR.iterdir():
|
|
305
|
+
if not feature_dir.is_dir():
|
|
306
|
+
continue
|
|
307
|
+
for name in (
|
|
308
|
+
"spec.md",
|
|
309
|
+
"tasks.md",
|
|
310
|
+
"design.md",
|
|
311
|
+
"validation.md",
|
|
312
|
+
"exploration.md",
|
|
313
|
+
):
|
|
314
|
+
path = feature_dir / name
|
|
315
|
+
if path.is_file():
|
|
316
|
+
candidates.append(path)
|
|
317
|
+
|
|
318
|
+
for path in candidates:
|
|
319
|
+
if path.is_file():
|
|
320
|
+
mtime = path.stat().st_mtime
|
|
321
|
+
latest = mtime if latest is None else max(latest, mtime)
|
|
322
|
+
return latest
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def has_indexable_artifacts() -> bool:
|
|
326
|
+
if (SPECS_DIR / "lessons.json").is_file():
|
|
327
|
+
return True
|
|
328
|
+
if not FEATURES_DIR.is_dir():
|
|
329
|
+
return False
|
|
330
|
+
for feature_dir in FEATURES_DIR.iterdir():
|
|
331
|
+
if not feature_dir.is_dir():
|
|
332
|
+
continue
|
|
333
|
+
for name in ("spec.md", "tasks.md", "design.md", "validation.md", "exploration.md"):
|
|
334
|
+
if (feature_dir / name).is_file():
|
|
335
|
+
return True
|
|
336
|
+
return False
|
|
337
|
+
|
|
338
|
+
|
|
263
339
|
def chunk_markdown_sections(
|
|
264
340
|
conn: sqlite3.Connection,
|
|
265
341
|
feature_id: str,
|
|
@@ -343,7 +419,6 @@ def rebuild(json_output: bool = False) -> int:
|
|
|
343
419
|
conn.execute("DELETE FROM entity_fts")
|
|
344
420
|
conn.execute("DELETE FROM chunks")
|
|
345
421
|
conn.execute("DELETE FROM chunk_fts")
|
|
346
|
-
conn.execute("DELETE FROM embeddings")
|
|
347
422
|
|
|
348
423
|
entity_count = 0
|
|
349
424
|
relation_count = 0
|
|
@@ -397,6 +472,19 @@ def rebuild(json_output: bool = False) -> int:
|
|
|
397
472
|
now,
|
|
398
473
|
)
|
|
399
474
|
|
|
475
|
+
design_path = feature_dir / "design.md"
|
|
476
|
+
if design_path.is_file():
|
|
477
|
+
design_text = design_path.read_text(encoding="utf-8")
|
|
478
|
+
chunk_count += chunk_markdown_sections(
|
|
479
|
+
conn,
|
|
480
|
+
feature_id,
|
|
481
|
+
design_path,
|
|
482
|
+
design_text,
|
|
483
|
+
"design",
|
|
484
|
+
feature_id,
|
|
485
|
+
now,
|
|
486
|
+
)
|
|
487
|
+
|
|
400
488
|
exploration_path = feature_dir / "exploration.md"
|
|
401
489
|
if exploration_path.is_file():
|
|
402
490
|
exploration_text = exploration_path.read_text(encoding="utf-8")
|
|
@@ -448,9 +536,14 @@ def rebuild(json_output: bool = False) -> int:
|
|
|
448
536
|
upsert_relation(conn, task_id, file_entity, "touches")
|
|
449
537
|
relation_count += 1
|
|
450
538
|
|
|
539
|
+
chunk_count += chunk_task_bodies(
|
|
540
|
+
conn, feature_id, tasks_path, tasks_text, now
|
|
541
|
+
)
|
|
542
|
+
|
|
451
543
|
lesson_entities, lesson_chunks = index_lessons(conn, now)
|
|
452
544
|
entity_count += lesson_entities
|
|
453
545
|
chunk_count += lesson_chunks
|
|
546
|
+
prune_embeddings(conn)
|
|
454
547
|
conn.commit()
|
|
455
548
|
|
|
456
549
|
summary = {
|
|
@@ -545,6 +638,46 @@ def embed_chunks(force: bool = False, json_output: bool = False) -> int:
|
|
|
545
638
|
return EXIT_OK
|
|
546
639
|
|
|
547
640
|
|
|
641
|
+
def memory_status(json_output: bool = False) -> int:
|
|
642
|
+
config = load_memory_retrieval_config()
|
|
643
|
+
latest = latest_artifact_mtime()
|
|
644
|
+
summary = {
|
|
645
|
+
"database": str(DB_PATH),
|
|
646
|
+
"exists": DB_PATH.is_file(),
|
|
647
|
+
"semantic_enabled": bool(config.get("semantic")),
|
|
648
|
+
"provider": str(config.get("provider") or "none"),
|
|
649
|
+
"entities": 0,
|
|
650
|
+
"chunks": 0,
|
|
651
|
+
"embeddings": 0,
|
|
652
|
+
"has_artifacts": has_indexable_artifacts(),
|
|
653
|
+
"stale": False,
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if DB_PATH.is_file():
|
|
657
|
+
conn = sqlite3.connect(DB_PATH)
|
|
658
|
+
try:
|
|
659
|
+
init_schema(conn)
|
|
660
|
+
summary["entities"] = conn.execute("SELECT COUNT(*) FROM entities").fetchone()[0]
|
|
661
|
+
summary["chunks"] = conn.execute("SELECT COUNT(*) FROM chunks").fetchone()[0]
|
|
662
|
+
summary["embeddings"] = conn.execute("SELECT COUNT(*) FROM embeddings").fetchone()[0]
|
|
663
|
+
finally:
|
|
664
|
+
conn.close()
|
|
665
|
+
if latest is not None:
|
|
666
|
+
summary["stale"] = DB_PATH.stat().st_mtime < latest
|
|
667
|
+
else:
|
|
668
|
+
summary["stale"] = summary["has_artifacts"]
|
|
669
|
+
|
|
670
|
+
if json_output:
|
|
671
|
+
print(json.dumps(summary, indent=2))
|
|
672
|
+
else:
|
|
673
|
+
state = "ready" if summary["exists"] and not summary["stale"] else "needs attention"
|
|
674
|
+
print(
|
|
675
|
+
f"[{GATE}] memory index {state} — "
|
|
676
|
+
f"{summary['chunks']} chunk(s), {summary['embeddings']} embedding(s)"
|
|
677
|
+
)
|
|
678
|
+
return EXIT_OK
|
|
679
|
+
|
|
680
|
+
|
|
548
681
|
def build_parser() -> argparse.ArgumentParser:
|
|
549
682
|
parser = argparse.ArgumentParser(description="Rebuild SQLite memory index from .specs/")
|
|
550
683
|
sub = parser.add_subparsers(dest="command")
|
|
@@ -558,6 +691,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
558
691
|
embed_cmd.add_argument("--json", action="store_true")
|
|
559
692
|
embed_cmd.set_defaults(func=lambda args: embed_chunks(force=args.force, json_output=args.json))
|
|
560
693
|
|
|
694
|
+
status_cmd = sub.add_parser("status", help="report index stats for doctor and tooling")
|
|
695
|
+
status_cmd.add_argument("--json", action="store_true")
|
|
696
|
+
status_cmd.set_defaults(func=lambda args: memory_status(json_output=args.json))
|
|
697
|
+
|
|
561
698
|
return parser
|
|
562
699
|
|
|
563
700
|
|
|
@@ -48,7 +48,7 @@ Structural gates run **before** owner review, so they cannot drift when the mode
|
|
|
48
48
|
| Before Execute waves (3+ tasks) | `npx @luizsantiago/spec-guardrails loop-plan [feature]` |
|
|
49
49
|
| Parallel wave (2+ tasks, disjoint Files) | `npx @luizsantiago/spec-guardrails workspace-prepare [feature] --tasks T1,T2` |
|
|
50
50
|
| After parallel wave merge | `npx @luizsantiago/spec-guardrails workspace-cleanup [feature] --force` |
|
|
51
|
-
| Before editing paths outside task Files | `npx @luizsantiago/spec-guardrails context-guard check-edit <path> [--op write]` |
|
|
51
|
+
| Before editing paths outside task Files | `npx @luizsantiago/spec-guardrails context-guard check-edit <path> [--op write]` — **Cursor:** also auto-runs via `.cursor/hooks/` on write/edit tools |
|
|
52
52
|
| Before claiming feature complete | `npx @luizsantiago/spec-guardrails context-guard check-complete [feature]` |
|
|
53
53
|
| Solution exploration (explicit) | `npx @luizsantiago/spec-guardrails solution-explore init <feature> --candidates A,B` |
|
|
54
54
|
| Before exploration decision | `npx @luizsantiago/spec-guardrails solution-explore validate [feature]` |
|
|
@@ -16,6 +16,8 @@ You installed the **Spec Guardrails**. You do **not** need to memorize CLI comma
|
|
|
16
16
|
|
|
17
17
|
3. Review `.specs/features/…/spec.md` and **approve** before implementation.
|
|
18
18
|
|
|
19
|
+
**Plain-language guide:** [Overview](https://github.com/luizssantiago92/spec-guardrails/blob/main/docs/guide/Overview.md) · [Memory](https://github.com/luizssantiago92/spec-guardrails/blob/main/docs/guide/Memory.md)
|
|
20
|
+
|
|
19
21
|
---
|
|
20
22
|
|
|
21
23
|
## Agent commands (chat — not terminal)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Cursor preToolUse hook — runs context-guard before file write/edit tools.
|
|
4
|
+
* Installed to .cursor/hooks/ by spec-guardrails install.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
|
+
|
|
11
|
+
const GUARDRAILS_SCRIPTS = ".specs/guardrails/scripts";
|
|
12
|
+
const SKIP_PREFIXES = [".specs/", ".cursor/", "node_modules/"];
|
|
13
|
+
const WRITE_TOOLS = new Set([
|
|
14
|
+
"write",
|
|
15
|
+
"strreplace",
|
|
16
|
+
"search_replace",
|
|
17
|
+
"editnotebook",
|
|
18
|
+
"applypatch",
|
|
19
|
+
"edit",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {unknown} input
|
|
24
|
+
* @returns {Record<string, unknown> | null}
|
|
25
|
+
*/
|
|
26
|
+
function readHookInput(input) {
|
|
27
|
+
if (input && typeof input === "object") {
|
|
28
|
+
return /** @type {Record<string, unknown>} */ (input);
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {Record<string, unknown>} input
|
|
35
|
+
* @returns {string | null}
|
|
36
|
+
*/
|
|
37
|
+
export function extractEditPath(input) {
|
|
38
|
+
const toolName = String(input.tool_name ?? input.toolName ?? "").toLowerCase();
|
|
39
|
+
if (toolName && !WRITE_TOOLS.has(toolName)) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const rawInput = input.tool_input ?? input.toolInput ?? input.arguments ?? {};
|
|
44
|
+
/** @type {Record<string, unknown>} */
|
|
45
|
+
let toolInput = {};
|
|
46
|
+
|
|
47
|
+
if (typeof rawInput === "string") {
|
|
48
|
+
try {
|
|
49
|
+
toolInput = JSON.parse(rawInput);
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
} else if (rawInput && typeof rawInput === "object") {
|
|
54
|
+
toolInput = /** @type {Record<string, unknown>} */ (rawInput);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const pathValue =
|
|
58
|
+
toolInput.path ??
|
|
59
|
+
toolInput.file_path ??
|
|
60
|
+
toolInput.filePath ??
|
|
61
|
+
toolInput.target_notebook ??
|
|
62
|
+
toolInput.notebook_path;
|
|
63
|
+
|
|
64
|
+
return typeof pathValue === "string" && pathValue.trim() ? pathValue.trim() : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {string | null} relativePath
|
|
69
|
+
* @returns {boolean}
|
|
70
|
+
*/
|
|
71
|
+
export function shouldSkipPath(relativePath) {
|
|
72
|
+
if (!relativePath) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const normalized = relativePath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
77
|
+
return SKIP_PREFIXES.some((prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {{ permission: string, user_message?: string, agent_message?: string }} payload
|
|
82
|
+
*/
|
|
83
|
+
function respond(payload) {
|
|
84
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function allow() {
|
|
88
|
+
respond({ permission: "allow" });
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {string[]} messages
|
|
94
|
+
*/
|
|
95
|
+
function deny(messages) {
|
|
96
|
+
const detail = messages.filter(Boolean).join("; ") || "context-guard blocked this edit";
|
|
97
|
+
respond({
|
|
98
|
+
permission: "deny",
|
|
99
|
+
user_message: `Spec Guardrails blocked this edit: ${detail}`,
|
|
100
|
+
agent_message:
|
|
101
|
+
`Context guard blocked the edit. Align with .specs/STATE.md and task Files, or update tasks.md. ${detail}`,
|
|
102
|
+
});
|
|
103
|
+
process.exit(2);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {Record<string, unknown>} input
|
|
108
|
+
* @returns {{ allowed: boolean, messages: string[] }}
|
|
109
|
+
*/
|
|
110
|
+
export function evaluateHookInput(input) {
|
|
111
|
+
const editPath = extractEditPath(input);
|
|
112
|
+
if (shouldSkipPath(editPath)) {
|
|
113
|
+
return { allowed: true, messages: [] };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!existsSync(GUARDRAILS_SCRIPTS)) {
|
|
117
|
+
return { allowed: true, messages: [] };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const cli = process.env.SPEC_GUARDRAILS_CLI ?? "npx @luizsantiago/spec-guardrails";
|
|
121
|
+
const result = spawnSync(
|
|
122
|
+
cli,
|
|
123
|
+
["context-guard", "check-edit", editPath, "--op", "write", "--json"],
|
|
124
|
+
{ encoding: "utf8", shell: process.platform === "win32" },
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
if (result.status === 0) {
|
|
128
|
+
return { allowed: true, messages: [] };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** @type {string[]} */
|
|
132
|
+
let messages = ["context-guard check failed"];
|
|
133
|
+
try {
|
|
134
|
+
const parsed = JSON.parse(result.stdout || "{}");
|
|
135
|
+
if (Array.isArray(parsed.messages) && parsed.messages.length > 0) {
|
|
136
|
+
messages = parsed.messages.map(String);
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
if (result.stderr?.trim()) {
|
|
140
|
+
messages = [result.stderr.trim()];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return { allowed: false, messages };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function main() {
|
|
148
|
+
let parsed = null;
|
|
149
|
+
try {
|
|
150
|
+
parsed = readHookInput(JSON.parse(readFileSync(0, "utf8")));
|
|
151
|
+
} catch {
|
|
152
|
+
allow();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!parsed) {
|
|
156
|
+
allow();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const decision = evaluateHookInput(parsed);
|
|
160
|
+
if (decision.allowed) {
|
|
161
|
+
allow();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
deny(decision.messages);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const entry = process.argv[1] ? pathToFileURL(process.argv[1]).href : "";
|
|
168
|
+
if (entry && import.meta.url === entry) {
|
|
169
|
+
main();
|
|
170
|
+
}
|