@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 +3 -24
- package/index.js +28 -6
- package/lib/adapter-registry.js +19 -7
- package/lib/adapters.js +6 -2
- package/lib/cursor-hooks-cleanup.js +172 -0
- package/lib/doctor.js +37 -21
- package/lib/install.js +47 -20
- package/lib/platform-detect.js +226 -0
- package/package.json +3 -3
- package/skills/agent-architecture.md +2 -13
- package/skills/references/elicitation.md +0 -12
- package/templates/GETTING_STARTED.md +0 -1
- package/templates/config.yaml.example +6 -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
|
@@ -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.
|
|
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
|
-
→ [
|
|
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
|
|
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
|
-
[--
|
|
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
|
|
@@ -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 === "--
|
|
208
|
-
installOptions.
|
|
209
|
-
} else if (arg === "--
|
|
210
|
-
|
|
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}`);
|
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
|
}
|
|
@@ -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
|
|
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
|
+
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
|
-
|
|
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 =
|
|
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
|
|
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:
|
|
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 {
|
|
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 {{
|
|
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
|
|
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 → ${
|
|
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
|
|
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
|
-
|
|
92
|
-
|
|
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
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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"),
|