@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 CHANGED
@@ -1,13 +1,17 @@
1
1
  # Spec Guardrails
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@luizsantiago/spec-guardrails.svg)](https://www.npmjs.com/package/@luizsantiago/spec-guardrails)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@luizsantiago/spec-guardrails.svg)](https://www.npmjs.com/package/@luizsantiago/spec-guardrails)
5
+ [![CI](https://github.com/luizssantiago92/spec-guardrails/actions/workflows/ci.yml/badge.svg)](https://github.com/luizssantiago92/spec-guardrails/actions/workflows/ci.yml)
4
6
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
7
 
8
+ ![Spec Guardrails — governed spec-driven development for AI coding agents](https://raw.githubusercontent.com/luizssantiago92/spec-guardrails/main/.assets/banner.svg)
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.2.x**
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
+ ![Spec Guardrails phase flow — classify, optional elicitation, three human approvals, build loop, verify, archive](https://raw.githubusercontent.com/luizssantiago92/spec-guardrails/main/.assets/flow.svg)
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
+ ![Complexity tiers — Quick, Simple, Medium, Complex, Parallel](https://raw.githubusercontent.com/luizssantiago92/spec-guardrails/main/.assets/tiers.svg)
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 (9)
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
- → [Cursor hooks and sandbox](docs/guide/Cursor-hooks-and-sandbox.md) · [Memory](docs/guide/Memory.md)
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/` (+ Claude, Copilot, Codex trees) | Phase instructions for the agent |
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
- | Cursor IDE (optional) | [Cursor hooks and sandbox](docs/guide/Cursor-hooks-and-sandbox.md) | [Guarantees matrix](docs/guide/Guarantees-matrix.md) |
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 to skills, gates, CLI, docs, and tests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for repository layout, gate stability rules, and local checks.
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
- [--with-cursor-hooks] Register Cursor IDE hooks (off by default)
71
- [--without-cursor-hooks] Remove shipped Cursor hooks and set cursor.hooks: false
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 === "--with-cursor-hooks") {
208
- installOptions.withCursorHooks = true;
209
- } else if (arg === "--without-cursor-hooks") {
210
- installOptions.withoutCursorHooks = true;
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}`);
@@ -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 Promise.all(ADAPTER_REGISTRY.map((adapter) => installAdapter(cwd, adapter)));
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 relativePath of ADAPTER_CONTRACT_PATHS) {
47
- try {
48
- const content = await readFileSafe(path.join(cwd, relativePath));
49
- if (!content.includes(CURSORRULES_MARKER_BEGIN)) {
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
- SKILL_DIRS.map((dir) => pathExists(cwd, path.join(dir, "agent-architecture.md"))),
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 = SKILL_DIRS.filter((_dir, index) => !hubResults[index]);
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 all adapter trees (agent-architecture.md)",
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: "Platform adapter contracts (Cursor, Claude, Copilot, Codex, AGENTS.md)",
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