@luizsantiago/spec-guardrails 4.2.0 → 4.4.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
@@ -7,7 +7,7 @@
7
7
 
8
8
  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
9
 
10
- npm: [`@luizsantiago/spec-guardrails`](https://www.npmjs.com/package/@luizsantiago/spec-guardrails) **4.2.x**
10
+ npm: [`@luizsantiago/spec-guardrails`](https://www.npmjs.com/package/@luizsantiago/spec-guardrails) **4.4.x**
11
11
 
12
12
  ---
13
13
 
@@ -213,10 +213,9 @@ Implementation happens in **small waves**: pick the next runnable jobs, test, im
213
213
 
214
214
  | Capability | What it adds |
215
215
  | --- | --- |
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
216
  | **Semantic memory search** | Find past specs and decisions by meaning, not just keywords |
218
217
 
219
- → [Cursor hooks and sandbox](docs/guide/Cursor-hooks-and-sandbox.md) · [Memory](docs/guide/Memory.md)
218
+ → [Memory](docs/guide/Memory.md)
220
219
 
221
220
  ---
222
221
 
@@ -242,7 +241,6 @@ Implementation happens in **small waves**: pick the next runnable jobs, test, im
242
241
  | Enforcement | [Gates](docs/guide/gates.md) | [Gates and guarantees](docs/guide/Gates-and-guarantees.md) |
243
242
  | Requirements | [Requirements analysis](docs/guide/requirements-analysis.md) | [Agent commands → /elicit](docs/guide/agent-commands.md) |
244
243
  | 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
244
  | Questions | [FAQ](docs/guide/FAQ.md) | [Stability policy](docs/guide/Stability-policy.md) |
247
245
 
248
246
  Full index: [docs/guide/README.md](docs/guide/README.md)
@@ -251,26 +249,7 @@ Full index: [docs/guide/README.md](docs/guide/README.md)
251
249
 
252
250
  ## Contributing
253
251
 
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
- ```
252
+ Focused improvements are welcome see [CONTRIBUTING.md](CONTRIBUTING.md) for layout, gate stability rules, and local checks.
274
253
 
275
254
  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
255
 
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
@@ -195,6 +197,7 @@ if (command === "--version" || command === "-v" || command === "version") {
195
197
  } else if (command === "install") {
196
198
  try {
197
199
  const installOptions = {};
200
+ let deprecatedCursorHooksFlag = false;
198
201
  for (let i = 0; i < args.length; i++) {
199
202
  const arg = args[i];
200
203
  if (arg === "--preset") {
@@ -204,15 +207,34 @@ if (command === "--version" || command === "-v" || command === "version") {
204
207
  }
205
208
  } else if (arg === "--force-config") {
206
209
  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;
210
+ } else if (arg === "--all-platforms") {
211
+ installOptions.allPlatforms = true;
212
+ } else if (arg === "--platform") {
213
+ const platform = parsePlatformArg(args[++i] ?? "");
214
+ if (!platform) {
215
+ throw new Error(
216
+ "--platform requires cursor, claude, copilot, or codex.",
217
+ );
218
+ }
219
+ installOptions.platform = platform;
220
+ } else if (arg === "--with-cursor-hooks" || arg === "--without-cursor-hooks") {
221
+ deprecatedCursorHooksFlag = true;
211
222
  } else {
212
223
  throw new Error(`Unknown install flag: ${arg}`);
213
224
  }
214
225
  }
215
226
 
227
+ if (deprecatedCursorHooksFlag) {
228
+ console.warn(
229
+ "⚠️ --with-cursor-hooks / --without-cursor-hooks are deprecated (removed in 4.3.0). " +
230
+ "Running install normally; legacy hook artifacts are cleaned automatically.",
231
+ );
232
+ }
233
+
234
+ if (installOptions.allPlatforms && installOptions.platform) {
235
+ throw new Error("Use either --all-platforms or --platform, not both.");
236
+ }
237
+
216
238
  await install(installOptions);
217
239
  } catch (err) {
218
240
  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
  }
@@ -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,27 @@ 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
+ try {
51
+ const content = await readFileSafe(path.join(cwd, relativePath));
52
+ if (!content.includes(CURSORRULES_MARKER_BEGIN)) {
53
+ return false;
54
+ }
55
+ } catch {
50
56
  return false;
51
57
  }
52
- } catch {
53
- return false;
54
58
  }
55
59
  }
56
60
  return true;
@@ -107,15 +111,20 @@ export async function runDoctorChecks(cwd) {
107
111
  /** @type {DoctorCheck[]} */
108
112
  const checks = [];
109
113
 
114
+ const installedSkillDirs = await resolveInstalledSkillDirs(cwd);
110
115
  const hubResults = await Promise.all(
111
- SKILL_DIRS.map((dir) => pathExists(cwd, path.join(dir, "agent-architecture.md"))),
116
+ installedSkillDirs.map((dir) =>
117
+ pathExists(cwd, path.join(dir, "agent-architecture.md")),
118
+ ),
112
119
  );
113
120
  const hubInstalled = hubResults.every(Boolean);
114
- const missingHubDirs = SKILL_DIRS.filter((_dir, index) => !hubResults[index]);
121
+ const missingHubDirs = installedSkillDirs.filter(
122
+ (_dir, index) => !hubResults[index],
123
+ );
115
124
 
116
125
  checks.push({
117
126
  id: "skills-hub",
118
- label: "Agent hub skill in all adapter trees (agent-architecture.md)",
127
+ label: "Agent hub skill in installed adapter trees (agent-architecture.md)",
119
128
  weight: 12,
120
129
  pass: hubInstalled,
121
130
  suggest: hubInstalled
@@ -181,11 +190,18 @@ export async function runDoctorChecks(cwd) {
181
190
  optional: true,
182
191
  });
183
192
 
193
+ const adapterIds = await resolveAdapterInstallTargets(cwd, {
194
+ skillDirs: installedSkillDirs,
195
+ });
196
+ const adapterLabels = adapterIds
197
+ .map((id) => getAdapter(id)?.label ?? id)
198
+ .join(", ");
199
+
184
200
  checks.push({
185
201
  id: "platform-adapters",
186
- label: "Platform adapter contracts (Cursor, Claude, Copilot, Codex, AGENTS.md)",
202
+ label: `Platform adapter contracts (${adapterLabels})`,
187
203
  weight: 5,
188
- pass: await hasPlatformAdapterContract(cwd),
204
+ pass: await hasPlatformAdapterContract(cwd, adapterIds),
189
205
  suggest: NPX("install"),
190
206
  });
191
207
 
package/lib/install.js CHANGED
@@ -8,21 +8,32 @@ import {
8
8
  REFERENCES_SUBDIR,
9
9
  SCRIPT_ASSETS,
10
10
  SKILL_ASSETS,
11
- SKILL_DIRS,
12
11
  DISPLAY_NAME,
13
12
  resolveAssetOverride,
14
13
  } from "./constants.js";
15
14
  import { installPlatformAdapters } from "./adapters.js";
16
- import { installCursorHooks, removeCursorHooks, resolveCursorHooksInstall, writeCursorHooksInConfig } from "./cursor-hooks.js";
15
+ import { cleanupLegacyCursorHooks } from "./cursor-hooks-cleanup.js";
17
16
  import { ensureDir, readFileSafe, writeFileIfMissing } from "./fs-utils.js";
18
17
  import { hasPython } from "./gates.js";
19
18
  import { initGuardrailsMemory } from "./memory.js";
20
19
  import { printInstallNextSteps } from "./next-steps.js";
20
+ import {
21
+ resolveAdapterInstallTargets,
22
+ resolveSkillInstallTargets,
23
+ } from "./platform-detect.js";
21
24
  import { initProjectConfig } from "./presets.js";
22
25
  import { installProjectRules } from "./project-rules.js";
23
26
 
24
27
  /**
25
- * @param {{ cwd?: string, repoUrl?: string, silent?: boolean }} [options]
28
+ * @param {{
29
+ * cwd?: string,
30
+ * repoUrl?: string,
31
+ * silent?: boolean,
32
+ * preset?: string,
33
+ * forceConfig?: boolean,
34
+ * allPlatforms?: boolean,
35
+ * platform?: import("./platform-detect.js").PlatformId,
36
+ * }} [options]
26
37
  */
27
38
  export async function install(options = {}) {
28
39
  const cwd = options.cwd ?? process.cwd();
@@ -47,18 +58,35 @@ export async function install(options = {}) {
47
58
  log("📦 Copying skills, references and gates from the npm package...");
48
59
  }
49
60
 
61
+ const { skillDirs, detected, existing } = await resolveSkillInstallTargets(
62
+ cwd,
63
+ options,
64
+ );
65
+
66
+ if (!options.silent && !options.allPlatforms) {
67
+ if (detected) {
68
+ log(`🎯 Detected platform: ${detected}`);
69
+ }
70
+ if (existing.length > 0) {
71
+ log(`📁 Preserving existing skill trees: ${existing.join(", ")}`);
72
+ }
73
+ if (skillDirs.length === 1 && !detected && existing.length === 0) {
74
+ log("ℹ️ No platform detected — defaulting to .cursor/skills (use --all-platforms for every tree)");
75
+ }
76
+ }
77
+
50
78
  for (const skill of SKILL_ASSETS) {
51
- for (const dir of SKILL_DIRS) {
79
+ for (const dir of skillDirs) {
52
80
  const targetDir = path.join(cwd, dir);
53
81
  await ensureDir(targetDir);
54
82
  await fetchAsset(skill.remotePath, path.join(targetDir, skill.file));
55
83
  }
56
84
  }
57
- log(`✅ ${SKILL_ASSETS.length} sister skills → ${SKILL_DIRS.join(", ")}`);
85
+ log(`✅ ${SKILL_ASSETS.length} sister skills → ${skillDirs.join(", ")}`);
58
86
 
59
87
  log("📚 Installing phase references...");
60
88
  for (const reference of REFERENCE_ASSETS) {
61
- for (const dir of SKILL_DIRS) {
89
+ for (const dir of skillDirs) {
62
90
  const targetDir = path.join(cwd, dir, REFERENCES_SUBDIR);
63
91
  await ensureDir(targetDir);
64
92
  await fetchAsset(
@@ -88,8 +116,11 @@ export async function install(options = {}) {
88
116
  }
89
117
  log(`✅ ${SCRIPT_ASSETS.length} scripts → ${GUARDRAILS_SCRIPTS_DIR}`);
90
118
 
91
- log("📋 Installing project rules (.cursor/rules/)...");
92
- await installProjectRules(cwd, { fetchAsset });
119
+ const installCursorRules = skillDirs.includes(".cursor/skills");
120
+ if (installCursorRules) {
121
+ log("📋 Installing project rules (.cursor/rules/)...");
122
+ await installProjectRules(cwd, { fetchAsset });
123
+ }
93
124
 
94
125
  log("🧠 Setting up persistent memory in .specs/...");
95
126
  const { stateCreated, lessonsCreated } = await initGuardrailsMemory(cwd);
@@ -116,18 +147,14 @@ export async function install(options = {}) {
116
147
  }
117
148
 
118
149
  log("🔗 Installing platform adapters...");
119
- await installPlatformAdapters(cwd);
120
- log("✅ Adapters → .cursorrules, CLAUDE.md, copilot-instructions.md, AGENTS.md, .codex/AGENTS.md");
121
-
122
- const hooksEnabled = await resolveCursorHooksInstall(cwd, options);
123
- if (hooksEnabled) {
124
- await installCursorHooks(cwd, { log });
125
- await writeCursorHooksInConfig(cwd, true);
126
- } else if (options.withoutCursorHooks || options.withCursorHooks === false) {
127
- await removeCursorHooks(cwd, { log });
128
- } else {
129
- log("ℹ️ Cursor hooks skipped (off by default). Enable: install --with-cursor-hooks or cursor.hooks: true in .specs/config.yaml");
130
- }
150
+ const adapterIds = await resolveAdapterInstallTargets(cwd, {
151
+ ...options,
152
+ skillDirs,
153
+ });
154
+ await installPlatformAdapters(cwd, { adapterIds });
155
+ log(`✅ Adapters → ${adapterIds.join(", ")}`);
156
+
157
+ await cleanupLegacyCursorHooks(cwd, { log });
131
158
 
132
159
  const gettingStartedCreated = await writeFileIfMissing(
133
160
  path.join(cwd, ".specs/GETTING_STARTED.md"),