@luizsantiago/spec-guardrails 4.2.1 → 4.5.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 +31 -82
- package/index.js +31 -6
- package/lib/adapter-registry.js +19 -7
- package/lib/adapters.js +6 -2
- package/lib/constants.js +3 -0
- package/lib/cursor-hooks-cleanup.js +172 -0
- package/lib/doctor.js +40 -21
- package/lib/gates.js +2 -0
- package/lib/install.js +55 -22
- package/lib/next-steps.js +58 -4
- package/lib/platform-detect.js +226 -0
- package/package.json +2 -2
- package/scripts/_project_config.py +126 -0
- package/scripts/check_commit.py +103 -19
- package/scripts/check_suppressions.py +114 -0
- package/scripts/run_quality_checks.py +102 -0
- package/skills/agent-architecture.md +2 -13
- package/skills/references/elicitation.md +0 -12
- package/skills/references/implement.md +2 -0
- package/skills/references/validate.md +2 -1
- package/templates/GETTING_STARTED.md +0 -1
- package/templates/config.yaml.example +24 -9
- package/lib/cursor-hooks.js +0 -224
- package/lib/memory-index.js +0 -95
- package/templates/cursor/hooks/context-guard-edit.mjs +0 -170
- package/templates/cursor/hooks/sandbox-shell.mjs +0 -102
- package/templates/cursor/hooks.json +0 -18
package/README.md
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
# Spec Guardrails
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@luizsantiago/spec-guardrails)
|
|
4
|
+
[](https://www.npmjs.com/package/@luizsantiago/spec-guardrails)
|
|
5
|
+
[](https://github.com/luizssantiago92/spec-guardrails/actions/workflows/ci.yml)
|
|
4
6
|
[](LICENSE)
|
|
5
7
|
|
|
8
|
+

|
|
9
|
+
|
|
6
10
|
**Governed spec-driven development for AI coding agents.**
|
|
7
11
|
|
|
8
12
|
Spec Guardrails installs a working method into your repository: the agent writes down what it is going to build, gets your approval, implements in small waves, and proves the result before calling it done. Nothing about your stack changes — you get written requirements, a task plan, and verification evidence stored as files in the project.
|
|
9
13
|
|
|
10
|
-
npm: [`@luizsantiago/spec-guardrails`](https://www.npmjs.com/package/@luizsantiago/spec-guardrails) **4.
|
|
14
|
+
npm: [`@luizsantiago/spec-guardrails`](https://www.npmjs.com/package/@luizsantiago/spec-guardrails) **4.5.x**
|
|
11
15
|
|
|
12
16
|
---
|
|
13
17
|
|
|
@@ -63,64 +67,11 @@ Read more: [Quick start](docs/guide/Quick-start.md) · [Platform parity](docs/gu
|
|
|
63
67
|
|
|
64
68
|
You describe your project or the feature you want — in chat, or by pointing the agent at a file such as `prd.md` or `docs/brief.md`. With Spec Guardrails already installed, the agent reads that material plus what is already in the repo and picks up from there if work is in progress (`STATE.md` tells it where you left off).
|
|
65
69
|
|
|
66
|
-
|
|
67
|
-
YOU describe the project or feature
|
|
68
|
-
(chat, prd.md, docs/brief.md, kickoff paste)
|
|
69
|
-
│
|
|
70
|
-
▼
|
|
71
|
-
┌──────────────────────┐
|
|
72
|
-
│ READ & CLASSIFY │ Agent reads your inputs and the repo,
|
|
73
|
-
│ │ then sizes the change (see table below).
|
|
74
|
-
└──────────┬───────────┘
|
|
75
|
-
│
|
|
76
|
-
Still vague? ──► Requirements analysis (optional)
|
|
77
|
-
│ Up to 5 questions per round, one topic at a time,
|
|
78
|
-
│ with suggested options — never repeats what your
|
|
79
|
-
│ document already answered.
|
|
80
|
-
▼
|
|
81
|
-
┌──────────────────────┐
|
|
82
|
-
│ REQUIREMENTS BRIEF │ Captured gaps and decisions in writing.
|
|
83
|
-
└──────────┬───────────┘
|
|
84
|
-
│
|
|
85
|
-
◆ YOU APPROVE ◆ (1 of 3 — when elicitation ran)
|
|
86
|
-
│
|
|
87
|
-
▼
|
|
88
|
-
┌──────────────────────┐
|
|
89
|
-
│ SPECIFY │ What must happen, what "done" means,
|
|
90
|
-
│ │ what is out of scope → spec.md
|
|
91
|
-
└──────────┬───────────┘
|
|
92
|
-
│
|
|
93
|
-
◆ YOU APPROVE ◆ (2 of 3)
|
|
94
|
-
│
|
|
95
|
-
▼
|
|
96
|
-
┌──────────────────────┐
|
|
97
|
-
│ TASKS │ Small checkable jobs → tasks.md
|
|
98
|
-
│ │ (+ task-graph.md when work can split)
|
|
99
|
-
└──────────┬───────────┘
|
|
100
|
-
│
|
|
101
|
-
◆ YOU APPROVE ◆ (3 of 3)
|
|
102
|
-
│
|
|
103
|
-
▼
|
|
104
|
-
┌──────────────────────┐
|
|
105
|
-
│ BUILD (loop) │ One wave at a time: test, implement,
|
|
106
|
-
│ ↺ │ check, commit — repeat until done.
|
|
107
|
-
└──────────┬───────────┘
|
|
108
|
-
▼
|
|
109
|
-
┌──────────────────────┐
|
|
110
|
-
│ VERIFY │ Independent review with proof → validation.md
|
|
111
|
-
└──────────┬───────────┘
|
|
112
|
-
▼
|
|
113
|
-
┌──────────────────────┐
|
|
114
|
-
│ ARCHIVE │ Fold outcome into project memory.
|
|
115
|
-
└──────────────────────┘
|
|
116
|
-
|
|
117
|
-
Tiny fix (≤3 files, no new dependency)?
|
|
118
|
-
Express lane: build → verify → commit (no spec/tasks ceremony).
|
|
119
|
-
```
|
|
70
|
+

|
|
120
71
|
|
|
121
72
|
The agent never skips your approvals on the full path. Requirements analysis is **suggested**, not forced — if the request is already clear, it goes straight to Specify.
|
|
122
73
|
|
|
123
|
-
Read more: [How it works](docs/guide/How-it-works.md) · [Agent commands](docs/guide/agent-commands.md)
|
|
74
|
+
Read more: [How it works](docs/guide/How-it-works.md) · [Agent commands](docs/guide/agent-commands.md) · [Glossary](docs/guide/Glossary.md)
|
|
124
75
|
|
|
125
76
|
---
|
|
126
77
|
|
|
@@ -128,6 +79,8 @@ Read more: [How it works](docs/guide/How-it-works.md) · [Agent commands](docs/g
|
|
|
128
79
|
|
|
129
80
|
Before starting, the agent classifies the change and loads only what that change needs. A typo does not get a task graph; a payments integration does not skip review.
|
|
130
81
|
|
|
82
|
+

|
|
83
|
+
|
|
131
84
|
| Complexity | Typical scope | What gets created | Your approvals |
|
|
132
85
|
| --- | --- | --- | --- |
|
|
133
86
|
| **Quick** | ≤3 files, no new dependency, no auth/payments | Code + quick evidence | None (express lane) |
|
|
@@ -173,7 +126,7 @@ Instructions the agent loads **one at a time** — hub `agent-architecture.md` (
|
|
|
173
126
|
|
|
174
127
|
→ [Skills and hub](docs/guide/skills-and-hub.md)
|
|
175
128
|
|
|
176
|
-
### Gates (
|
|
129
|
+
### Gates (11)
|
|
177
130
|
|
|
178
131
|
Automatic checks at step boundaries — each one blocks a specific kind of shortcut:
|
|
179
132
|
|
|
@@ -187,6 +140,8 @@ Automatic checks at step boundaries — each one blocks a specific kind of short
|
|
|
187
140
|
| `validate-state` | Feature is declared done without evidence |
|
|
188
141
|
| `validate-quick` | Quick-mode fix broke its size or shape rules |
|
|
189
142
|
| `check-commit` | Commit message does not follow the agreed format |
|
|
143
|
+
| `check-suppressions` | Staged diff adds `# noqa`, `eslint-disable`, `@ts-ignore`, skipped tests, or `--no-verify` |
|
|
144
|
+
| `quality-checks` | Configured project commands (`npm test`, …) fail during `/verify` |
|
|
190
145
|
| `lessons` | A failed verify tries to skip the lesson step |
|
|
191
146
|
|
|
192
147
|
→ [Gates](docs/guide/gates.md) · [Garantees matrix](docs/guide/Guarantees-matrix.md)
|
|
@@ -213,10 +168,9 @@ Implementation happens in **small waves**: pick the next runnable jobs, test, im
|
|
|
213
168
|
|
|
214
169
|
| Capability | What it adds |
|
|
215
170
|
| --- | --- |
|
|
216
|
-
| **Cursor IDE hooks** | Scope check before file edits and shell-command policy on Cursor — disabled by default; requirements analysis can ask if you want them; you can also enable or disable anytime in chat |
|
|
217
171
|
| **Semantic memory search** | Find past specs and decisions by meaning, not just keywords |
|
|
218
172
|
|
|
219
|
-
→ [
|
|
173
|
+
→ [Memory](docs/guide/Memory.md)
|
|
220
174
|
|
|
221
175
|
---
|
|
222
176
|
|
|
@@ -224,7 +178,7 @@ Implementation happens in **small waves**: pick the next runnable jobs, test, im
|
|
|
224
178
|
|
|
225
179
|
| Path | Role |
|
|
226
180
|
| --- | --- |
|
|
227
|
-
| `.cursor/skills/` (
|
|
181
|
+
| `.cursor/skills/` (or detected platform tree) | Phase instructions for your agent |
|
|
228
182
|
| `.specs/STATE.md` | Active feature and next step |
|
|
229
183
|
| `.specs/features/NNN-slug/` | Spec, tasks, and validation per feature |
|
|
230
184
|
| `.specs/guardrails/scripts/` | Python checks (when Brakes mode is on) |
|
|
@@ -232,6 +186,21 @@ Implementation happens in **small waves**: pick the next runnable jobs, test, im
|
|
|
232
186
|
|
|
233
187
|
---
|
|
234
188
|
|
|
189
|
+
## Honest limits
|
|
190
|
+
|
|
191
|
+
Gates enforce **structure and evidence in `.specs/`** — not product taste, not whether your tests are clever, and not a full AST review of implementation code. A green gate means the artifact shape and cited proof look complete; you still approve specs and tasks.
|
|
192
|
+
|
|
193
|
+
| Gates check | Gates do **not** check |
|
|
194
|
+
| --- | --- |
|
|
195
|
+
| Spec sections and `SHALL`/`MUST` criteria | Semantic alignment between tests and requirements |
|
|
196
|
+
| REQ → task → validation traceability | Stub or broken source outside cited paths |
|
|
197
|
+
| Commit message shape and suppression patterns | Whether coverage % equals test quality |
|
|
198
|
+
| Commands you list under `quality.checks` | Commands you never configured |
|
|
199
|
+
|
|
200
|
+
See [Guarantees matrix](docs/guide/Guarantees-matrix.md) for the full product view.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
235
204
|
## Documentation
|
|
236
205
|
|
|
237
206
|
| Topic | Start here | Go deeper |
|
|
@@ -242,8 +211,7 @@ Implementation happens in **small waves**: pick the next runnable jobs, test, im
|
|
|
242
211
|
| Enforcement | [Gates](docs/guide/gates.md) | [Gates and guarantees](docs/guide/Gates-and-guarantees.md) |
|
|
243
212
|
| Requirements | [Requirements analysis](docs/guide/requirements-analysis.md) | [Agent commands → /elicit](docs/guide/agent-commands.md) |
|
|
244
213
|
| Long-running projects | [Memory](docs/guide/Memory.md) | [Brownfield context](docs/guide/brownfield-context.md) |
|
|
245
|
-
|
|
|
246
|
-
| Questions | [FAQ](docs/guide/FAQ.md) | [Stability policy](docs/guide/Stability-policy.md) |
|
|
214
|
+
| Questions | [FAQ](docs/guide/FAQ.md) | [Glossary](docs/guide/Glossary.md) · [Stability policy](docs/guide/Stability-policy.md) |
|
|
247
215
|
|
|
248
216
|
Full index: [docs/guide/README.md](docs/guide/README.md)
|
|
249
217
|
|
|
@@ -251,26 +219,7 @@ Full index: [docs/guide/README.md](docs/guide/README.md)
|
|
|
251
219
|
|
|
252
220
|
## Contributing
|
|
253
221
|
|
|
254
|
-
Focused improvements
|
|
255
|
-
|
|
256
|
-
**Build your contribution with Spec Guardrails.** In your own project or fork, install the latest stable release and let the phases guide the change:
|
|
257
|
-
|
|
258
|
-
```bash
|
|
259
|
-
npx @luizsantiago/spec-guardrails@latest install
|
|
260
|
-
npx @luizsantiago/spec-guardrails doctor
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
Describe the change in chat, approve the spec and the task plan, implement, and verify before opening the pull request — the resulting `.specs/` folder is the evidence that supports your PR.
|
|
264
|
-
|
|
265
|
-
**In this source repository**, work against your branch instead of the published package:
|
|
266
|
-
|
|
267
|
-
```bash
|
|
268
|
-
git clone https://github.com/luizssantiago92/spec-guardrails.git
|
|
269
|
-
cd spec-guardrails
|
|
270
|
-
npm install
|
|
271
|
-
npm run guardrails -- install
|
|
272
|
-
npm run guardrails -- doctor
|
|
273
|
-
```
|
|
222
|
+
Focused improvements are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) for layout, gate stability rules, and local checks.
|
|
274
223
|
|
|
275
224
|
Edit sources under `skills/`, `lib/`, `scripts/`, and `rules/`; re-run `npm run guardrails -- install` after changing shipped assets, and run `npm test` before every PR.
|
|
276
225
|
|
package/index.js
CHANGED
|
@@ -28,6 +28,7 @@ import { featureInit } from "./lib/feature.js";
|
|
|
28
28
|
import { featureStatus, formatFeatureStatus } from "./lib/feature-status.js";
|
|
29
29
|
import { GATE_COMMANDS, AUX_COMMANDS, runGate, runGuardrailsScript } from "./lib/gates.js";
|
|
30
30
|
import { install } from "./lib/install.js";
|
|
31
|
+
import { parsePlatformArg } from "./lib/platform-detect.js";
|
|
31
32
|
import {
|
|
32
33
|
cleanupWorkspaces,
|
|
33
34
|
formatWorkspaceList,
|
|
@@ -67,8 +68,8 @@ Commands:
|
|
|
67
68
|
install Install skills, references, gates and .specs/ memory
|
|
68
69
|
[--preset <name>] Seed .specs/config.yaml from a built-in preset
|
|
69
70
|
[--force-config] Replace existing config.yaml when using --preset
|
|
70
|
-
[--
|
|
71
|
-
[--
|
|
71
|
+
[--all-platforms] Install every skill tree (Cursor, Claude, Copilot, Codex)
|
|
72
|
+
[--platform cursor|claude|copilot|codex] Force a platform instead of auto-detect
|
|
72
73
|
init-config [--preset <name>] Create .specs/config.yaml (default preset: default)
|
|
73
74
|
[--force] Replace existing config.yaml
|
|
74
75
|
preset list List built-in config presets
|
|
@@ -127,6 +128,7 @@ Commands:
|
|
|
127
128
|
execution-policy record-run Increment agent-run counter (blocks at budget)
|
|
128
129
|
memory-index rebuild Rebuild SQLite memory index from .specs/ artifacts
|
|
129
130
|
memory-index embed [--force] Optional semantic embeddings (requires config + provider)
|
|
131
|
+
memory-index status [--json] Index chunk, embedding, and staleness stats
|
|
130
132
|
memory-query --from <id> Bounded context package from the knowledge graph
|
|
131
133
|
[--depth N] Traversal depth (default 2)
|
|
132
134
|
[--json] Machine-readable output
|
|
@@ -178,6 +180,9 @@ Commands:
|
|
|
178
180
|
validate-req-analysis [brief.md] Requirements brief gate before /specify (/elicit)
|
|
179
181
|
validate-state [feature] Completion gate before declaring a feature done
|
|
180
182
|
check-commit --message "<msg>" Conventional Commits gate
|
|
183
|
+
[--staged] Also reject empty commits and oversized staged diffs
|
|
184
|
+
check-suppressions Block noqa/eslint-disable/ts-ignore/skip in staged diff
|
|
185
|
+
quality-checks [--json] Run quality.checks from .specs/config.yaml (/verify)
|
|
181
186
|
lessons <add|list|penalize|prune|promote|graduate|status> Lessons engine
|
|
182
187
|
--help Show this message
|
|
183
188
|
--version Print the package version
|
|
@@ -195,6 +200,7 @@ if (command === "--version" || command === "-v" || command === "version") {
|
|
|
195
200
|
} else if (command === "install") {
|
|
196
201
|
try {
|
|
197
202
|
const installOptions = {};
|
|
203
|
+
let deprecatedCursorHooksFlag = false;
|
|
198
204
|
for (let i = 0; i < args.length; i++) {
|
|
199
205
|
const arg = args[i];
|
|
200
206
|
if (arg === "--preset") {
|
|
@@ -204,15 +210,34 @@ if (command === "--version" || command === "-v" || command === "version") {
|
|
|
204
210
|
}
|
|
205
211
|
} else if (arg === "--force-config") {
|
|
206
212
|
installOptions.forceConfig = true;
|
|
207
|
-
} else if (arg === "--
|
|
208
|
-
installOptions.
|
|
209
|
-
} else if (arg === "--
|
|
210
|
-
|
|
213
|
+
} else if (arg === "--all-platforms") {
|
|
214
|
+
installOptions.allPlatforms = true;
|
|
215
|
+
} else if (arg === "--platform") {
|
|
216
|
+
const platform = parsePlatformArg(args[++i] ?? "");
|
|
217
|
+
if (!platform) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
"--platform requires cursor, claude, copilot, or codex.",
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
installOptions.platform = platform;
|
|
223
|
+
} else if (arg === "--with-cursor-hooks" || arg === "--without-cursor-hooks") {
|
|
224
|
+
deprecatedCursorHooksFlag = true;
|
|
211
225
|
} else {
|
|
212
226
|
throw new Error(`Unknown install flag: ${arg}`);
|
|
213
227
|
}
|
|
214
228
|
}
|
|
215
229
|
|
|
230
|
+
if (deprecatedCursorHooksFlag) {
|
|
231
|
+
console.warn(
|
|
232
|
+
"⚠️ --with-cursor-hooks / --without-cursor-hooks are deprecated (removed in 4.3.0). " +
|
|
233
|
+
"Running install normally; legacy hook artifacts are cleaned automatically.",
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (installOptions.allPlatforms && installOptions.platform) {
|
|
238
|
+
throw new Error("Use either --all-platforms or --platform, not both.");
|
|
239
|
+
}
|
|
240
|
+
|
|
216
241
|
await install(installOptions);
|
|
217
242
|
} catch (err) {
|
|
218
243
|
console.error(`❌ ${err.message}`);
|
package/lib/adapter-registry.js
CHANGED
|
@@ -11,7 +11,6 @@ import { injectCursorRules } from "./cursorrules.js";
|
|
|
11
11
|
* skillsDir: string | null,
|
|
12
12
|
* entryFiles: string[],
|
|
13
13
|
* capabilities: {
|
|
14
|
-
* supports_hooks: boolean,
|
|
15
14
|
* supports_commands: boolean,
|
|
16
15
|
* supports_rules: boolean,
|
|
17
16
|
* supports_skills: boolean,
|
|
@@ -28,7 +27,6 @@ export const ADAPTER_REGISTRY = [
|
|
|
28
27
|
skillsDir: ".cursor/skills",
|
|
29
28
|
entryFiles: [".cursorrules", ".cursor/rules/engineering-baseline.mdc"],
|
|
30
29
|
capabilities: {
|
|
31
|
-
supports_hooks: true,
|
|
32
30
|
supports_commands: true,
|
|
33
31
|
supports_rules: true,
|
|
34
32
|
supports_skills: true,
|
|
@@ -41,7 +39,6 @@ export const ADAPTER_REGISTRY = [
|
|
|
41
39
|
skillsDir: ".claude/skills",
|
|
42
40
|
entryFiles: [".claude/CLAUDE.md"],
|
|
43
41
|
capabilities: {
|
|
44
|
-
supports_hooks: false,
|
|
45
42
|
supports_commands: true,
|
|
46
43
|
supports_rules: false,
|
|
47
44
|
supports_skills: true,
|
|
@@ -54,7 +51,6 @@ export const ADAPTER_REGISTRY = [
|
|
|
54
51
|
skillsDir: ".github/skills",
|
|
55
52
|
entryFiles: [".github/copilot-instructions.md"],
|
|
56
53
|
capabilities: {
|
|
57
|
-
supports_hooks: false,
|
|
58
54
|
supports_commands: false,
|
|
59
55
|
supports_rules: false,
|
|
60
56
|
supports_skills: true,
|
|
@@ -67,7 +63,6 @@ export const ADAPTER_REGISTRY = [
|
|
|
67
63
|
skillsDir: ".codex/skills",
|
|
68
64
|
entryFiles: [".codex/AGENTS.md"],
|
|
69
65
|
capabilities: {
|
|
70
|
-
supports_hooks: false,
|
|
71
66
|
supports_commands: false,
|
|
72
67
|
supports_rules: false,
|
|
73
68
|
supports_skills: true,
|
|
@@ -80,7 +75,6 @@ export const ADAPTER_REGISTRY = [
|
|
|
80
75
|
skillsDir: null,
|
|
81
76
|
entryFiles: ["AGENTS.md"],
|
|
82
77
|
capabilities: {
|
|
83
|
-
supports_hooks: false,
|
|
84
78
|
supports_commands: false,
|
|
85
79
|
supports_rules: false,
|
|
86
80
|
supports_skills: false,
|
|
@@ -124,5 +118,23 @@ export async function installAdapter(cwd, adapter) {
|
|
|
124
118
|
* @param {string} cwd
|
|
125
119
|
*/
|
|
126
120
|
export async function installAllAdapters(cwd) {
|
|
127
|
-
await
|
|
121
|
+
await installAdapters(
|
|
122
|
+
cwd,
|
|
123
|
+
ADAPTER_REGISTRY.map((adapter) => adapter.id),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Install selected platform adapters by id.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} cwd
|
|
131
|
+
* @param {string[]} adapterIds
|
|
132
|
+
*/
|
|
133
|
+
export async function installAdapters(cwd, adapterIds) {
|
|
134
|
+
const wanted = new Set(adapterIds);
|
|
135
|
+
await Promise.all(
|
|
136
|
+
ADAPTER_REGISTRY.filter((adapter) => wanted.has(adapter.id)).map((adapter) =>
|
|
137
|
+
installAdapter(cwd, adapter),
|
|
138
|
+
),
|
|
139
|
+
);
|
|
128
140
|
}
|
package/lib/adapters.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import { installAllAdapters } from "./adapter-registry.js";
|
|
1
|
+
import { installAdapters, installAllAdapters } from "./adapter-registry.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Install shipped platform adapter entry files (Copilot, Codex, AGENTS.md).
|
|
5
5
|
* Cursor and Claude adapters are injected via the same registry during install.
|
|
6
6
|
*
|
|
7
7
|
* @param {string} cwd
|
|
8
|
+
* @param {{ adapterIds?: string[] }} [options]
|
|
8
9
|
*/
|
|
9
|
-
export async function installPlatformAdapters(cwd) {
|
|
10
|
+
export async function installPlatformAdapters(cwd, options = {}) {
|
|
11
|
+
if (options.adapterIds) {
|
|
12
|
+
return installAdapters(cwd, options.adapterIds);
|
|
13
|
+
}
|
|
10
14
|
return installAllAdapters(cwd);
|
|
11
15
|
}
|
package/lib/constants.js
CHANGED
|
@@ -120,6 +120,9 @@ export const SCRIPT_ASSETS = [
|
|
|
120
120
|
{ file: "code_index.py", remotePath: "scripts/code_index.py" },
|
|
121
121
|
{ file: "validate_req_analysis.py", remotePath: "scripts/validate_req_analysis.py" },
|
|
122
122
|
{ file: "req_context.py", remotePath: "scripts/req_context.py" },
|
|
123
|
+
{ file: "check_suppressions.py", remotePath: "scripts/check_suppressions.py" },
|
|
124
|
+
{ file: "run_quality_checks.py", remotePath: "scripts/run_quality_checks.py" },
|
|
125
|
+
{ file: "_project_config.py", remotePath: "scripts/_project_config.py" },
|
|
123
126
|
];
|
|
124
127
|
|
|
125
128
|
/** @type {{ file: string, remotePath: string }[]} */
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { readFileSafe } from "./fs-utils.js";
|
|
5
|
+
|
|
6
|
+
export const CURSOR_HOOK_EDIT = ".cursor/hooks/context-guard-edit.mjs";
|
|
7
|
+
export const CURSOR_HOOK_SANDBOX = ".cursor/hooks/sandbox-shell.mjs";
|
|
8
|
+
export const CURSOR_HOOKS_JSON = ".cursor/hooks.json";
|
|
9
|
+
|
|
10
|
+
/** @type {readonly string[]} */
|
|
11
|
+
const SHIPPED_HOOK_SCRIPTS = [CURSOR_HOOK_EDIT, CURSOR_HOOK_SANDBOX];
|
|
12
|
+
|
|
13
|
+
/** @type {Set<string>} */
|
|
14
|
+
const SHIPPED_HOOK_COMMANDS = new Set(SHIPPED_HOOK_SCRIPTS);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {unknown} hooks
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
function hooksObjectHasShippedEntries(hooks) {
|
|
21
|
+
if (!hooks || typeof hooks !== "object") {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (const entries of Object.values(hooks)) {
|
|
26
|
+
if (!Array.isArray(entries)) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (entries.some((entry) => SHIPPED_HOOK_COMMANDS.has(/** @type {{ command?: string }} */ (entry).command))) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {unknown} hooks
|
|
39
|
+
* @returns {boolean}
|
|
40
|
+
*/
|
|
41
|
+
function hooksObjectHasUserEntries(hooks) {
|
|
42
|
+
if (!hooks || typeof hooks !== "object") {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const entries of Object.values(hooks)) {
|
|
47
|
+
if (!Array.isArray(entries)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (
|
|
51
|
+
entries.some(
|
|
52
|
+
(entry) =>
|
|
53
|
+
/** @type {{ command?: string }} */ (entry).command &&
|
|
54
|
+
!SHIPPED_HOOK_COMMANDS.has(/** @type {{ command?: string }} */ (entry).command),
|
|
55
|
+
)
|
|
56
|
+
) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {string} filePath
|
|
66
|
+
* @returns {Promise<boolean>}
|
|
67
|
+
*/
|
|
68
|
+
async function fileExists(filePath) {
|
|
69
|
+
try {
|
|
70
|
+
await fs.access(filePath);
|
|
71
|
+
return true;
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Remove the `cursor:` block from `.specs/config.yaml` when present.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} cwd
|
|
81
|
+
* @returns {Promise<boolean>}
|
|
82
|
+
*/
|
|
83
|
+
async function removeCursorBlockFromConfig(cwd) {
|
|
84
|
+
const configPath = path.join(cwd, ".specs/config.yaml");
|
|
85
|
+
let text;
|
|
86
|
+
try {
|
|
87
|
+
text = await readFileSafe(configPath);
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!/^cursor:/m.test(text)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
text = text.replace(/^cursor:\s*\n(?:[ \t#].*\n)*/m, "");
|
|
97
|
+
text = `${text.replace(/\n{3,}/g, "\n\n").trimEnd()}\n`;
|
|
98
|
+
await fs.writeFile(configPath, text, "utf8");
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Remove legacy Cursor IDE hook artifacts shipped before 4.3.0.
|
|
104
|
+
* Idempotent and silent when nothing remains to clean.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} cwd
|
|
107
|
+
* @param {{ log?: (message: string) => void }} [options]
|
|
108
|
+
* @returns {Promise<{ changed: boolean }>}
|
|
109
|
+
*/
|
|
110
|
+
export async function cleanupLegacyCursorHooks(cwd, options = {}) {
|
|
111
|
+
const log = options.log ?? (() => {});
|
|
112
|
+
let changed = false;
|
|
113
|
+
|
|
114
|
+
for (const script of SHIPPED_HOOK_SCRIPTS) {
|
|
115
|
+
const scriptPath = path.join(cwd, script);
|
|
116
|
+
if (await fileExists(scriptPath)) {
|
|
117
|
+
await fs.unlink(scriptPath);
|
|
118
|
+
changed = true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const hooksJsonPath = path.join(cwd, CURSOR_HOOKS_JSON);
|
|
123
|
+
if (await fileExists(hooksJsonPath)) {
|
|
124
|
+
try {
|
|
125
|
+
const existing = JSON.parse(await readFileSafe(hooksJsonPath));
|
|
126
|
+
const hadShipped = hooksObjectHasShippedEntries(existing.hooks);
|
|
127
|
+
|
|
128
|
+
if (existing.hooks && typeof existing.hooks === "object") {
|
|
129
|
+
for (const [event, entries] of Object.entries(existing.hooks)) {
|
|
130
|
+
if (!Array.isArray(entries)) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
existing.hooks[event] = entries.filter(
|
|
134
|
+
(entry) => !SHIPPED_HOOK_COMMANDS.has(/** @type {{ command?: string }} */ (entry).command),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const userHooksRemain = hooksObjectHasUserEntries(existing.hooks);
|
|
140
|
+
const isEffectivelyEmpty =
|
|
141
|
+
!existing.hooks ||
|
|
142
|
+
Object.values(existing.hooks).every(
|
|
143
|
+
(entries) => !Array.isArray(entries) || entries.length === 0,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
if (hadShipped || isEffectivelyEmpty) {
|
|
147
|
+
changed = true;
|
|
148
|
+
if (userHooksRemain) {
|
|
149
|
+
await fs.writeFile(hooksJsonPath, `${JSON.stringify(existing, null, 2)}\n`, "utf8");
|
|
150
|
+
} else {
|
|
151
|
+
await fs.unlink(hooksJsonPath);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
await fs.unlink(hooksJsonPath);
|
|
156
|
+
changed = true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (await removeCursorBlockFromConfig(cwd)) {
|
|
161
|
+
changed = true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (changed) {
|
|
165
|
+
log(
|
|
166
|
+
"ℹ️ Cursor IDE hooks removed (deprecated in 4.3.0). " +
|
|
167
|
+
"Scope and sandbox checks remain available via context-guard and sandbox CLI commands.",
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { changed };
|
|
172
|
+
}
|
package/lib/doctor.js
CHANGED
|
@@ -6,10 +6,14 @@ import { promisify } from "node:util";
|
|
|
6
6
|
import {
|
|
7
7
|
CURSORRULES_MARKER_BEGIN,
|
|
8
8
|
NPX,
|
|
9
|
-
SKILL_DIRS,
|
|
10
9
|
} from "./constants.js";
|
|
10
|
+
import { getAdapter } from "./adapter-registry.js";
|
|
11
11
|
import { resolvePython, resolveScriptsDir } from "./gates.js";
|
|
12
12
|
import { readFileSafe } from "./fs-utils.js";
|
|
13
|
+
import {
|
|
14
|
+
resolveAdapterInstallTargets,
|
|
15
|
+
resolveInstalledSkillDirs,
|
|
16
|
+
} from "./platform-detect.js";
|
|
13
17
|
import { listFeatureIds, readActiveFeatureFromState } from "./specs-utils.js";
|
|
14
18
|
import { resolveMemoryHint } from "./memory-doctor.js";
|
|
15
19
|
|
|
@@ -30,27 +34,30 @@ export const DOCTOR_BRAKES_CHECK_IDS = [
|
|
|
30
34
|
"gate-smoke",
|
|
31
35
|
];
|
|
32
36
|
|
|
33
|
-
const ADAPTER_CONTRACT_PATHS = [
|
|
34
|
-
".cursorrules",
|
|
35
|
-
".claude/CLAUDE.md",
|
|
36
|
-
".github/copilot-instructions.md",
|
|
37
|
-
"AGENTS.md",
|
|
38
|
-
".codex/AGENTS.md",
|
|
39
|
-
];
|
|
40
|
-
|
|
41
37
|
/**
|
|
42
38
|
* @param {string} cwd
|
|
39
|
+
* @param {string[]} adapterIds
|
|
43
40
|
* @returns {Promise<boolean>}
|
|
44
41
|
*/
|
|
45
|
-
async function hasPlatformAdapterContract(cwd) {
|
|
46
|
-
for (const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
async function hasPlatformAdapterContract(cwd, adapterIds) {
|
|
43
|
+
for (const adapterId of adapterIds) {
|
|
44
|
+
const adapter = getAdapter(adapterId);
|
|
45
|
+
if (!adapter) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const relativePath of adapter.entryFiles) {
|
|
50
|
+
if (relativePath.endsWith(".mdc")) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const content = await readFileSafe(path.join(cwd, relativePath));
|
|
55
|
+
if (!content.includes(CURSORRULES_MARKER_BEGIN)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
50
59
|
return false;
|
|
51
60
|
}
|
|
52
|
-
} catch {
|
|
53
|
-
return false;
|
|
54
61
|
}
|
|
55
62
|
}
|
|
56
63
|
return true;
|
|
@@ -107,15 +114,20 @@ export async function runDoctorChecks(cwd) {
|
|
|
107
114
|
/** @type {DoctorCheck[]} */
|
|
108
115
|
const checks = [];
|
|
109
116
|
|
|
117
|
+
const installedSkillDirs = await resolveInstalledSkillDirs(cwd);
|
|
110
118
|
const hubResults = await Promise.all(
|
|
111
|
-
|
|
119
|
+
installedSkillDirs.map((dir) =>
|
|
120
|
+
pathExists(cwd, path.join(dir, "agent-architecture.md")),
|
|
121
|
+
),
|
|
112
122
|
);
|
|
113
123
|
const hubInstalled = hubResults.every(Boolean);
|
|
114
|
-
const missingHubDirs =
|
|
124
|
+
const missingHubDirs = installedSkillDirs.filter(
|
|
125
|
+
(_dir, index) => !hubResults[index],
|
|
126
|
+
);
|
|
115
127
|
|
|
116
128
|
checks.push({
|
|
117
129
|
id: "skills-hub",
|
|
118
|
-
label: "Agent hub skill in
|
|
130
|
+
label: "Agent hub skill in installed adapter trees (agent-architecture.md)",
|
|
119
131
|
weight: 12,
|
|
120
132
|
pass: hubInstalled,
|
|
121
133
|
suggest: hubInstalled
|
|
@@ -181,11 +193,18 @@ export async function runDoctorChecks(cwd) {
|
|
|
181
193
|
optional: true,
|
|
182
194
|
});
|
|
183
195
|
|
|
196
|
+
const adapterIds = await resolveAdapterInstallTargets(cwd, {
|
|
197
|
+
skillDirs: installedSkillDirs,
|
|
198
|
+
});
|
|
199
|
+
const adapterLabels = adapterIds
|
|
200
|
+
.map((id) => getAdapter(id)?.label ?? id)
|
|
201
|
+
.join(", ");
|
|
202
|
+
|
|
184
203
|
checks.push({
|
|
185
204
|
id: "platform-adapters",
|
|
186
|
-
label:
|
|
205
|
+
label: `Platform adapter contracts (${adapterLabels})`,
|
|
187
206
|
weight: 5,
|
|
188
|
-
pass: await hasPlatformAdapterContract(cwd),
|
|
207
|
+
pass: await hasPlatformAdapterContract(cwd, adapterIds),
|
|
189
208
|
suggest: NPX("install"),
|
|
190
209
|
});
|
|
191
210
|
|
package/lib/gates.js
CHANGED
|
@@ -37,6 +37,8 @@ const GATE_SCRIPTS = {
|
|
|
37
37
|
"validate-req-analysis": "validate_req_analysis.py",
|
|
38
38
|
"analyze-artifacts": "analyze_artifacts.py",
|
|
39
39
|
"check-commit": "check_commit.py",
|
|
40
|
+
"check-suppressions": "check_suppressions.py",
|
|
41
|
+
"quality-checks": "run_quality_checks.py",
|
|
40
42
|
lessons: "lessons.py",
|
|
41
43
|
};
|
|
42
44
|
|