@luizsantiago/spec-guardrails 3.0.1

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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +206 -0
  3. package/index.js +335 -0
  4. package/lib/archive.js +208 -0
  5. package/lib/assets.js +145 -0
  6. package/lib/brownfield.js +446 -0
  7. package/lib/config.js +293 -0
  8. package/lib/constants.js +262 -0
  9. package/lib/cursorrules.js +92 -0
  10. package/lib/delta-merge.js +248 -0
  11. package/lib/doctor.js +343 -0
  12. package/lib/download.js +133 -0
  13. package/lib/feature.js +272 -0
  14. package/lib/fs-utils.js +114 -0
  15. package/lib/gates.js +138 -0
  16. package/lib/install.js +140 -0
  17. package/lib/memory.js +34 -0
  18. package/lib/next-steps.js +50 -0
  19. package/lib/presets.js +176 -0
  20. package/lib/project-rules.js +210 -0
  21. package/lib/specs-utils.js +117 -0
  22. package/lib/token-cost.js +124 -0
  23. package/package.json +46 -0
  24. package/rules/engineering-baseline.mdc +56 -0
  25. package/scripts/_common.py +356 -0
  26. package/scripts/analyze_artifacts.py +187 -0
  27. package/scripts/check_commit.py +140 -0
  28. package/scripts/lessons.py +447 -0
  29. package/scripts/loop_plan.py +217 -0
  30. package/scripts/validate_spec.py +345 -0
  31. package/scripts/validate_state.py +385 -0
  32. package/scripts/validate_tasks.py +379 -0
  33. package/skills/agent-architecture.md +221 -0
  34. package/skills/appsec.md +83 -0
  35. package/skills/code-simplify.md +49 -0
  36. package/skills/engineering-standards.md +98 -0
  37. package/skills/git-handoff.md +213 -0
  38. package/skills/qa-strategy.md +83 -0
  39. package/skills/references/analyze.md +56 -0
  40. package/skills/references/archive.md +60 -0
  41. package/skills/references/constitution.md +66 -0
  42. package/skills/references/context-limits.md +73 -0
  43. package/skills/references/converge.md +47 -0
  44. package/skills/references/design.md +88 -0
  45. package/skills/references/discuss.md +68 -0
  46. package/skills/references/explore.md +61 -0
  47. package/skills/references/implement.md +175 -0
  48. package/skills/references/lessons.md +71 -0
  49. package/skills/references/memory.md +98 -0
  50. package/skills/references/project-init.md +62 -0
  51. package/skills/references/quick-mode.md +84 -0
  52. package/skills/references/specify.md +144 -0
  53. package/skills/references/sub-agents.md +117 -0
  54. package/skills/references/tasks.md +178 -0
  55. package/skills/references/validate.md +210 -0
  56. package/skills/security-review.md +120 -0
  57. package/skills/ship-ready.md +50 -0
  58. package/skills/task-graph-engineering.md +180 -0
  59. package/templates/GETTING_STARTED.md +61 -0
  60. package/templates/config.yaml.example +28 -0
  61. package/templates/presets/default.yaml +16 -0
  62. package/templates/presets/node-ts.yaml +22 -0
  63. package/templates/presets/python.yaml +22 -0
package/lib/feature.js ADDED
@@ -0,0 +1,272 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import { STATE_HEADER } from "./constants.js";
6
+ import { ensureDir, readFileSafe, writeFileSafe } from "./fs-utils.js";
7
+ import { loadResolvedConfig, readBranchPrefix } from "./presets.js";
8
+
9
+ const FEATURES_DIR = ".specs/features";
10
+ const FEATURE_ID_PATTERN = /^(\d{3})-([a-z0-9][a-z0-9-]*)$/;
11
+ const DEFAULT_BRANCH_PREFIX = "feat";
12
+
13
+ /**
14
+ * @param {string} description
15
+ * @returns {string}
16
+ */
17
+ export function slugifyDescription(description) {
18
+ const slug = description
19
+ .toLowerCase()
20
+ .normalize("NFKD")
21
+ .replace(/[\u0300-\u036f]/g, "")
22
+ .replace(/[^a-z0-9]+/g, "-")
23
+ .replace(/^-+|-+$/g, "")
24
+ .slice(0, 48);
25
+
26
+ if (!slug) {
27
+ throw new Error("Description must contain at least one letter or digit.");
28
+ }
29
+
30
+ return slug;
31
+ }
32
+
33
+ /**
34
+ * @param {string} cwd
35
+ * @returns {Promise<number>}
36
+ */
37
+ export async function nextFeatureNumber(cwd) {
38
+ const featuresRoot = path.join(cwd, FEATURES_DIR);
39
+
40
+ try {
41
+ const entries = await fs.readdir(featuresRoot, { withFileTypes: true });
42
+ let max = 0;
43
+
44
+ for (const entry of entries) {
45
+ if (!entry.isDirectory()) {
46
+ continue;
47
+ }
48
+
49
+ const match = FEATURE_ID_PATTERN.exec(entry.name);
50
+ if (match) {
51
+ max = Math.max(max, Number.parseInt(match[1], 10));
52
+ }
53
+ }
54
+
55
+ return max + 1;
56
+ } catch (err) {
57
+ if (err.code === "ENOENT") {
58
+ return 1;
59
+ }
60
+ throw err;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * @param {number} number
66
+ * @param {string} slug
67
+ * @returns {string}
68
+ */
69
+ export function formatFeatureId(number, slug) {
70
+ return `${String(number).padStart(3, "0")}-${slug}`;
71
+ }
72
+
73
+ /**
74
+ * @param {string} featureId
75
+ * @param {string} [prefix]
76
+ * @returns {string}
77
+ */
78
+ export function featureBranchName(featureId, prefix = DEFAULT_BRANCH_PREFIX) {
79
+ return `${prefix}/${featureId}`;
80
+ }
81
+
82
+ /**
83
+ * @param {string} command
84
+ * @param {string[]} args
85
+ * @param {string} cwd
86
+ * @returns {Promise<{ code: number, stdout: string, stderr: string }>}
87
+ */
88
+ function runCapture(command, args, cwd) {
89
+ return new Promise((resolve, reject) => {
90
+ const child = spawn(command, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
91
+ let stdout = "";
92
+ let stderr = "";
93
+
94
+ child.stdout?.on("data", (chunk) => {
95
+ stdout += chunk;
96
+ });
97
+ child.stderr?.on("data", (chunk) => {
98
+ stderr += chunk;
99
+ });
100
+ child.on("error", reject);
101
+ child.on("close", (code) => resolve({ code: code ?? 1, stdout, stderr }));
102
+ });
103
+ }
104
+
105
+ /**
106
+ * @param {string} cwd
107
+ * @returns {Promise<boolean>}
108
+ */
109
+ async function isGitRepo(cwd) {
110
+ const result = await runCapture("git", ["rev-parse", "--is-inside-work-tree"], cwd);
111
+ return result.code === 0 && result.stdout.trim() === "true";
112
+ }
113
+
114
+ /**
115
+ * @param {string} cwd
116
+ * @param {string} branchName
117
+ * @returns {Promise<{ created: boolean, message: string }>}
118
+ */
119
+ async function ensureFeatureBranch(cwd, branchName) {
120
+ if (!(await isGitRepo(cwd))) {
121
+ return {
122
+ created: false,
123
+ message: "Not a git repository — feature folder and STATE updated; branch skipped.",
124
+ };
125
+ }
126
+
127
+ const current = await runCapture("git", ["branch", "--show-current"], cwd);
128
+ if (current.code !== 0) {
129
+ return {
130
+ created: false,
131
+ message: "Could not read current git branch — branch creation skipped.",
132
+ };
133
+ }
134
+
135
+ const active = current.stdout.trim();
136
+ if (active === branchName) {
137
+ return { created: false, message: `Already on ${branchName}.` };
138
+ }
139
+
140
+ const exists = await runCapture("git", ["show-ref", "--verify", `refs/heads/${branchName}`], cwd);
141
+ if (exists.code === 0) {
142
+ const checkout = await runCapture("git", ["checkout", branchName], cwd);
143
+ if (checkout.code !== 0) {
144
+ throw new Error(
145
+ `Branch ${branchName} exists but checkout failed: ${checkout.stderr.trim() || checkout.stdout.trim()}`,
146
+ );
147
+ }
148
+ return { created: false, message: `Checked out existing branch ${branchName}.` };
149
+ }
150
+
151
+ const create = await runCapture("git", ["checkout", "-b", branchName], cwd);
152
+ if (create.code !== 0) {
153
+ throw new Error(
154
+ `Failed to create branch ${branchName}: ${create.stderr.trim() || create.stdout.trim()}`,
155
+ );
156
+ }
157
+
158
+ return { created: true, message: `Created and checked out ${branchName} (Tier 0).` };
159
+ }
160
+
161
+ /**
162
+ * @param {string} cwd
163
+ * @param {{ featureId: string, branchName: string, phase?: string }} info
164
+ */
165
+ async function updateStateForFeature(cwd, info) {
166
+ const statePath = path.join(cwd, ".specs/STATE.md");
167
+ const phase = info.phase ?? "Specify";
168
+ const nextStep = `- [ ] Draft spec.md in .specs/features/${info.featureId}/`;
169
+
170
+ let content;
171
+ try {
172
+ content = await readFileSafe(statePath);
173
+ } catch {
174
+ content = STATE_HEADER;
175
+ }
176
+
177
+ const setField = (label, value) => {
178
+ const pattern = new RegExp(`(^-\\s*${label}:\\s*).*$`, "m");
179
+ if (pattern.test(content)) {
180
+ content = content.replace(pattern, `$1${value}`);
181
+ }
182
+ };
183
+
184
+ setField("Feature", info.featureId);
185
+ setField("Phase", phase);
186
+ setField("Branch", info.branchName);
187
+
188
+ const nextStepPattern = /^## Next Step \(single item\)\n- \[ \].*$/m;
189
+ if (nextStepPattern.test(content)) {
190
+ content = content.replace(nextStepPattern, `## Next Step (single item)\n${nextStep}`);
191
+ }
192
+
193
+ await writeFileSafe(statePath, content);
194
+ }
195
+
196
+ /**
197
+ * @param {string} cwd
198
+ * @param {string} featureId
199
+ * @param {string} description
200
+ */
201
+ async function writeSpecStub(cwd, featureId, description) {
202
+ const specPath = path.join(cwd, FEATURES_DIR, featureId, "spec.md");
203
+ const stub = `# Spec: ${featureId}
204
+
205
+ ## Goal
206
+ ${description.trim()}
207
+
208
+ ## Requirements
209
+
210
+ ### REQ-001: [Short title]
211
+ - **Acceptance Criteria**: WHEN [trigger] THEN the system SHALL [outcome]
212
+
213
+ ## Assumptions
214
+ - none
215
+
216
+ ## Out of Scope
217
+ - [Explicitly excluded work]
218
+ `;
219
+
220
+ await writeFileSafe(specPath, stub);
221
+ }
222
+
223
+ /**
224
+ * Initialize a numbered feature workspace (Tier 0).
225
+ *
226
+ * @param {string} description
227
+ * @param {{ cwd?: string, branchPrefix?: string, skipBranch?: boolean, skipSpec?: boolean }} [options]
228
+ */
229
+ export async function featureInit(description, options = {}) {
230
+ const cwd = options.cwd ?? process.cwd();
231
+ const trimmed = description?.trim();
232
+
233
+ if (!trimmed) {
234
+ throw new Error("Description is required. Example: feature-init \"chat with presence\"");
235
+ }
236
+
237
+ const slug = slugifyDescription(trimmed);
238
+ const number = await nextFeatureNumber(cwd);
239
+ const featureId = formatFeatureId(number, slug);
240
+
241
+ const resolvedConfig = await loadResolvedConfig(cwd);
242
+ const branchPrefix =
243
+ options.branchPrefix ?? readBranchPrefix(resolvedConfig) ?? DEFAULT_BRANCH_PREFIX;
244
+ const branchName = featureBranchName(featureId, branchPrefix);
245
+ const featureDir = path.join(cwd, FEATURES_DIR, featureId);
246
+
247
+ await ensureDir(featureDir);
248
+
249
+ if (!options.skipSpec) {
250
+ const specPath = path.join(featureDir, "spec.md");
251
+ try {
252
+ await fs.access(specPath);
253
+ } catch {
254
+ await writeSpecStub(cwd, featureId, trimmed);
255
+ }
256
+ }
257
+
258
+ await updateStateForFeature(cwd, { featureId, branchName });
259
+
260
+ let branchResult = { created: false, message: "Branch creation skipped (--no-branch)." };
261
+ if (!options.skipBranch) {
262
+ branchResult = await ensureFeatureBranch(cwd, branchName);
263
+ }
264
+
265
+ return {
266
+ featureId,
267
+ featureDir: path.join(FEATURES_DIR, featureId),
268
+ branchName,
269
+ branchCreated: branchResult.created,
270
+ branchMessage: branchResult.message,
271
+ };
272
+ }
@@ -0,0 +1,114 @@
1
+ import fs from "node:fs/promises";
2
+
3
+ function isPermissionError(err) {
4
+ return err && (err.code === "EACCES" || err.code === "EPERM");
5
+ }
6
+
7
+ /**
8
+ * Refuse to overwrite through a symlink (install must not clobber .env etc.).
9
+ * Used for packaged copies, remote downloads, memory files, and `.cursorrules`.
10
+ *
11
+ * @param {string} destPath
12
+ */
13
+ export async function assertSafeWriteTarget(destPath) {
14
+ let st;
15
+ try {
16
+ st = await fs.lstat(destPath);
17
+ } catch (err) {
18
+ if (err.code === "ENOENT") {
19
+ return;
20
+ }
21
+ if (isPermissionError(err)) {
22
+ throw new Error(`Permission denied: cannot access ${destPath}`);
23
+ }
24
+ throw err;
25
+ }
26
+
27
+ if (st.isSymbolicLink()) {
28
+ throw new Error(
29
+ `Refusing to write through symlink: ${destPath} — ` +
30
+ "remove the link or choose another destination before installing.",
31
+ );
32
+ }
33
+ }
34
+
35
+ export async function ensureDir(dirPath) {
36
+ try {
37
+ await fs.mkdir(dirPath, { recursive: true });
38
+ } catch (err) {
39
+ if (isPermissionError(err)) {
40
+ throw new Error(`Permission denied: cannot create ${dirPath}`);
41
+ }
42
+ throw err;
43
+ }
44
+ }
45
+
46
+ export async function writeFileIfMissing(filePath, content) {
47
+ await assertSafeWriteTarget(filePath);
48
+
49
+ try {
50
+ await fs.writeFile(filePath, content, { encoding: "utf8", flag: "wx" });
51
+ return true;
52
+ } catch (err) {
53
+ if (err.code === "EEXIST") {
54
+ return false;
55
+ }
56
+ if (isPermissionError(err)) {
57
+ throw new Error(`Permission denied: cannot write ${filePath}`);
58
+ }
59
+ throw err;
60
+ }
61
+ }
62
+
63
+ export async function appendFileSafe(filePath, content) {
64
+ await assertSafeWriteTarget(filePath);
65
+
66
+ try {
67
+ await fs.appendFile(filePath, content, "utf8");
68
+ } catch (err) {
69
+ if (isPermissionError(err)) {
70
+ throw new Error(`Permission denied: cannot write ${filePath}`);
71
+ }
72
+ throw err;
73
+ }
74
+ }
75
+
76
+ export async function writeFileSafe(filePath, content) {
77
+ await assertSafeWriteTarget(filePath);
78
+
79
+ try {
80
+ await fs.writeFile(filePath, content, "utf8");
81
+ } catch (err) {
82
+ if (isPermissionError(err)) {
83
+ throw new Error(`Permission denied: cannot write ${filePath}`);
84
+ }
85
+ throw err;
86
+ }
87
+ }
88
+
89
+ export async function readFileSafe(filePath) {
90
+ try {
91
+ return await fs.readFile(filePath, "utf8");
92
+ } catch (err) {
93
+ if (isPermissionError(err)) {
94
+ throw new Error(`Permission denied: cannot read ${filePath}`);
95
+ }
96
+ throw err;
97
+ }
98
+ }
99
+
100
+ export async function removeFileSafe(filePath) {
101
+ try {
102
+ await fs.unlink(filePath);
103
+ } catch (err) {
104
+ if (err.code === "ENOENT") {
105
+ return;
106
+ }
107
+ if (isPermissionError(err)) {
108
+ throw new Error(`Permission denied: cannot remove ${filePath}`);
109
+ }
110
+ throw err;
111
+ }
112
+ }
113
+
114
+ export { isPermissionError };
package/lib/gates.js ADDED
@@ -0,0 +1,138 @@
1
+ import { spawn } from "node:child_process";
2
+ import { constants } from "node:fs";
3
+ import { access } from "node:fs/promises";
4
+ import path from "node:path";
5
+
6
+ import { NPX, GUARDRAILS_SCRIPTS_DIR } from "./constants.js";
7
+
8
+ const PYTHON_CANDIDATES = ["python3", "python"];
9
+
10
+ const GATE_SCRIPTS = {
11
+ "validate-spec": "validate_spec.py",
12
+ "validate-tasks": "validate_tasks.py",
13
+ "validate-state": "validate_state.py",
14
+ "analyze-artifacts": "analyze_artifacts.py",
15
+ "check-commit": "check_commit.py",
16
+ lessons: "lessons.py",
17
+ };
18
+
19
+ const AUX_SCRIPTS = {
20
+ "loop-plan": "loop_plan.py",
21
+ };
22
+
23
+ const GUARDRAILS_SCRIPTS = { ...GATE_SCRIPTS, ...AUX_SCRIPTS };
24
+
25
+ export const GATE_COMMANDS = Object.keys(GATE_SCRIPTS);
26
+
27
+ export const AUX_COMMANDS = Object.keys(AUX_SCRIPTS);
28
+
29
+ /**
30
+ * @param {string} command
31
+ * @param {string[]} args
32
+ * @returns {Promise<number>}
33
+ */
34
+ function run(command, args, options = {}) {
35
+ return new Promise((resolve, reject) => {
36
+ const child = spawn(command, args, {
37
+ stdio: options.stdio ?? "inherit",
38
+ ...(options.cwd ? { cwd: options.cwd } : {}),
39
+ });
40
+ child.on("error", reject);
41
+ child.on("close", (code) => resolve(code ?? 1));
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Resolve gate scripts directory (`.specs/guardrails/scripts` only — no legacy dual-path).
47
+ *
48
+ * @param {string} _cwd
49
+ * @returns {Promise<string>}
50
+ */
51
+ export async function resolveScriptsDir(_cwd) {
52
+ return GUARDRAILS_SCRIPTS_DIR;
53
+ }
54
+
55
+ /**
56
+ * Resolve the available Python interpreter, or null when none is installed.
57
+ * @returns {Promise<string | null>}
58
+ */
59
+ export async function resolvePython() {
60
+ for (const candidate of PYTHON_CANDIDATES) {
61
+ try {
62
+ const code = await run(candidate, ["--version"], { stdio: "ignore" });
63
+ if (code === 0) {
64
+ return candidate;
65
+ }
66
+ } catch {
67
+ // Interpreter not on PATH; try the next candidate.
68
+ }
69
+ }
70
+
71
+ return null;
72
+ }
73
+
74
+ /** @returns {Promise<boolean>} */
75
+ export async function hasPython() {
76
+ return (await resolvePython()) !== null;
77
+ }
78
+
79
+ /**
80
+ * Run a guardrails Python script installed under `.specs/guardrails/scripts/`.
81
+ *
82
+ * @param {string} command gate or aux command name
83
+ * @param {string[]} args forwarded to the Python script
84
+ * @param {{ cwd?: string, stdio?: import("node:child_process").StdioOptions }} [options]
85
+ * @returns {Promise<number>} process exit code
86
+ */
87
+ export async function runGuardrailsScript(command, args, options = {}) {
88
+ const scriptName = GUARDRAILS_SCRIPTS[command];
89
+
90
+ if (!scriptName) {
91
+ throw new Error(`Unknown guardrails command: ${command}`);
92
+ }
93
+
94
+ const cwd = options.cwd ?? process.cwd();
95
+ const scriptsDir = await resolveScriptsDir(cwd);
96
+ const scriptPath = path.join(cwd, scriptsDir, scriptName);
97
+
98
+ for (const required of [scriptName, "_common.py"]) {
99
+ try {
100
+ await access(path.join(cwd, scriptsDir, required), constants.R_OK);
101
+ } catch {
102
+ throw new Error(
103
+ `Guardrails script not found at ${path.join(scriptsDir, required)}. ` +
104
+ `Run \`${NPX("install")}\` in this project first.`,
105
+ );
106
+ }
107
+ }
108
+
109
+ const python = await resolvePython();
110
+
111
+ if (!python) {
112
+ throw new Error(
113
+ "Python 3 not found. Install Python 3.10+ to run guardrails scripts, " +
114
+ "or perform the equivalent checks manually (degraded mode).",
115
+ );
116
+ }
117
+
118
+ return run(python, [scriptPath, ...args], {
119
+ cwd,
120
+ stdio: options.stdio,
121
+ });
122
+ }
123
+
124
+ /**
125
+ * Run a structural gate script installed under `.specs/guardrails/scripts/`.
126
+ *
127
+ * @param {string} gate one of GATE_COMMANDS
128
+ * @param {string[]} args forwarded to the Python script
129
+ * @param {{ cwd?: string, stdio?: import("node:child_process").StdioOptions }} [options]
130
+ * @returns {Promise<number>} process exit code
131
+ */
132
+ export async function runGate(gate, args, options = {}) {
133
+ if (!GATE_SCRIPTS[gate]) {
134
+ throw new Error(`Unknown gate: ${gate}`);
135
+ }
136
+
137
+ return runGuardrailsScript(gate, args, options);
138
+ }
package/lib/install.js ADDED
@@ -0,0 +1,140 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { installAsset, resolveInstallSource, packagedAssetPath } from "./assets.js";
5
+ import {
6
+ GUARDRAILS_SCRIPTS_DIR,
7
+ REFERENCE_ASSETS,
8
+ REFERENCES_SUBDIR,
9
+ SCRIPT_ASSETS,
10
+ SKILL_ASSETS,
11
+ SKILL_DIRS,
12
+ DISPLAY_NAME,
13
+ resolveAssetOverride,
14
+ } from "./constants.js";
15
+ import { injectCursorRules } from "./cursorrules.js";
16
+ import { ensureDir, readFileSafe, writeFileIfMissing } from "./fs-utils.js";
17
+ import { hasPython } from "./gates.js";
18
+ import { initGuardrailsMemory } from "./memory.js";
19
+ import { printInstallNextSteps } from "./next-steps.js";
20
+ import { initProjectConfig } from "./presets.js";
21
+ import { installProjectRules } from "./project-rules.js";
22
+
23
+ /**
24
+ * @param {{ cwd?: string, repoUrl?: string, silent?: boolean }} [options]
25
+ */
26
+ export async function install(options = {}) {
27
+ const cwd = options.cwd ?? process.cwd();
28
+ const log = options.silent ? () => {} : console.log;
29
+ const override = options.repoUrl ?? resolveAssetOverride();
30
+ const source = resolveInstallSource(options.repoUrl);
31
+ const state = { warned: false };
32
+
33
+ if (!options.repoUrl && override) {
34
+ log(
35
+ `âš ī¸ SPEC_GUARDRAILS_REPO_URL is set — installing skills and executable gate ` +
36
+ `scripts from ${override} instead of the packaged assets.`,
37
+ );
38
+ }
39
+
40
+ const fetchAsset = (remotePath, destPath) =>
41
+ installAsset({ remotePath, destPath, source, state, log });
42
+
43
+ log(`🚀 Installing ${DISPLAY_NAME}...`);
44
+
45
+ if (source.mode === "package") {
46
+ log("đŸ“Ļ Copying skills, references and gates from the npm package...");
47
+ }
48
+
49
+ for (const skill of SKILL_ASSETS) {
50
+ for (const dir of SKILL_DIRS) {
51
+ const targetDir = path.join(cwd, dir);
52
+ await ensureDir(targetDir);
53
+ await fetchAsset(skill.remotePath, path.join(targetDir, skill.file));
54
+ }
55
+ }
56
+ log(`✅ ${SKILL_ASSETS.length} sister skills → ${SKILL_DIRS.join(", ")}`);
57
+
58
+ log("📚 Installing phase references...");
59
+ for (const reference of REFERENCE_ASSETS) {
60
+ for (const dir of SKILL_DIRS) {
61
+ const targetDir = path.join(cwd, dir, REFERENCES_SUBDIR);
62
+ await ensureDir(targetDir);
63
+ await fetchAsset(
64
+ reference.remotePath,
65
+ path.join(targetDir, reference.file),
66
+ );
67
+ }
68
+ }
69
+ log(`✅ ${REFERENCE_ASSETS.length} references → ${REFERENCES_SUBDIR}/`);
70
+
71
+ log("🔒 Installing deterministic gates (Python)...");
72
+ const scriptsDir = path.join(cwd, GUARDRAILS_SCRIPTS_DIR);
73
+ await ensureDir(scriptsDir);
74
+
75
+ for (const script of SCRIPT_ASSETS) {
76
+ const destPath = path.join(scriptsDir, script.file);
77
+ await fetchAsset(script.remotePath, destPath);
78
+
79
+ try {
80
+ await fs.chmod(destPath, 0o755);
81
+ } catch (err) {
82
+ log(
83
+ `âš ī¸ Could not mark ${script.file} as executable (${err.code ?? err.message}). ` +
84
+ "Run it with `python3 <script>` instead.",
85
+ );
86
+ }
87
+ }
88
+ log(`✅ ${SCRIPT_ASSETS.length} scripts → ${GUARDRAILS_SCRIPTS_DIR}`);
89
+
90
+ log("📋 Installing project rules (.cursor/rules/)...");
91
+ await installProjectRules(cwd, { fetchAsset });
92
+
93
+ log("🧠 Setting up persistent memory in .specs/...");
94
+ const { stateCreated, lessonsCreated } = await initGuardrailsMemory(cwd);
95
+
96
+ if (options.preset) {
97
+ const configResult = await initProjectConfig({
98
+ cwd,
99
+ preset: options.preset,
100
+ force: options.forceConfig,
101
+ });
102
+ if (configResult.created) {
103
+ log(`✅ config.yaml initialized from preset: ${options.preset}`);
104
+ } else if (configResult.skipped) {
105
+ log("â„šī¸ config.yaml already exists — kept your file (use --force-config to replace)");
106
+ }
107
+ }
108
+
109
+ if (stateCreated) {
110
+ log("✅ STATE.md initialized [feed forward]");
111
+ }
112
+
113
+ if (lessonsCreated) {
114
+ log("✅ LESSONS.md initialized [feedback loop]");
115
+ }
116
+
117
+ await injectCursorRules(cwd);
118
+
119
+ const gettingStartedCreated = await writeFileIfMissing(
120
+ path.join(cwd, ".specs/GETTING_STARTED.md"),
121
+ await readFileSafe(packagedAssetPath("templates/GETTING_STARTED.md")),
122
+ );
123
+ if (gettingStartedCreated) {
124
+ log("✅ GETTING_STARTED.md → .specs/");
125
+ }
126
+
127
+ const pythonAvailable = await hasPython();
128
+ if (!pythonAvailable) {
129
+ log(
130
+ "âš ī¸ Python 3 not found. Skills still work in degraded mode " +
131
+ "(manual checks). Install Python 3.10+ to enable the gates.",
132
+ );
133
+ }
134
+
135
+ if (!options.silent) {
136
+ printInstallNextSteps({ pythonAvailable, preset: options.preset });
137
+ }
138
+
139
+ return { pythonAvailable };
140
+ }
package/lib/memory.js ADDED
@@ -0,0 +1,34 @@
1
+ import path from "node:path";
2
+
3
+ import { packagedAssetPath } from "./assets.js";
4
+ import {
5
+ LESSONS_HEADER,
6
+ STATE_HEADER,
7
+ } from "./constants.js";
8
+ import { ensureDir, readFileSafe, writeFileIfMissing } from "./fs-utils.js";
9
+
10
+ export async function initGuardrailsMemory(cwd) {
11
+ const specsDir = path.join(cwd, ".specs");
12
+ const featuresDir = path.join(specsDir, "features");
13
+ const projectDir = path.join(specsDir, "project");
14
+ const domainsDir = path.join(specsDir, "domains");
15
+
16
+ await ensureDir(featuresDir);
17
+ await ensureDir(projectDir);
18
+ await ensureDir(domainsDir);
19
+
20
+ const stateCreated = await writeFileIfMissing(
21
+ path.join(specsDir, "STATE.md"),
22
+ STATE_HEADER,
23
+ );
24
+ const lessonsCreated = await writeFileIfMissing(
25
+ path.join(specsDir, "LESSONS.md"),
26
+ LESSONS_HEADER,
27
+ );
28
+ await writeFileIfMissing(
29
+ path.join(specsDir, "config.yaml.example"),
30
+ await readFileSafe(packagedAssetPath("templates/config.yaml.example")),
31
+ );
32
+
33
+ return { stateCreated, lessonsCreated };
34
+ }