@whitehatd/crag 0.0.1 → 0.2.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 +864 -15
- package/bin/crag.js +7 -0
- package/package.json +18 -4
- package/src/cli.js +102 -0
- package/src/commands/analyze.js +513 -0
- package/src/commands/check.js +55 -0
- package/src/commands/compile.js +104 -0
- package/src/commands/diff.js +289 -0
- package/src/commands/init.js +112 -0
- package/src/commands/upgrade.js +64 -0
- package/src/commands/workspace.js +94 -0
- package/src/compile/agents-md.js +58 -0
- package/src/compile/atomic-write.js +32 -0
- package/src/compile/cline.js +83 -0
- package/src/compile/cody.js +82 -0
- package/src/compile/continue.js +78 -0
- package/src/compile/copilot.js +70 -0
- package/src/compile/cursor-rules.js +66 -0
- package/src/compile/gemini-md.js +58 -0
- package/src/compile/github-actions.js +165 -0
- package/src/compile/husky.js +66 -0
- package/src/compile/pre-commit.js +50 -0
- package/src/compile/windsurf.js +76 -0
- package/src/compile/zed.js +86 -0
- package/src/crag-agent.md +254 -0
- package/src/governance/gate-to-shell.js +28 -0
- package/src/governance/parse.js +182 -0
- package/src/skills/post-start-validation.md +297 -0
- package/src/skills/pre-start-context.md +506 -0
- package/src/update/integrity.js +131 -0
- package/src/update/skill-sync.js +116 -0
- package/src/update/version-check.js +156 -0
- package/src/workspace/detect.js +190 -0
- package/src/workspace/enumerate.js +270 -0
- package/src/workspace/governance.js +119 -0
- package/cli.js +0 -15
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { gateToShell } = require('../governance/gate-to-shell');
|
|
6
|
+
const { flattenGatesRich } = require('../governance/parse');
|
|
7
|
+
const { atomicWrite } = require('./atomic-write');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Extract the major Node version from package.json engines.node field.
|
|
11
|
+
* Handles formats: ">=18.0.0", "^18", "18.x", "18.0.0", ">=18 <21".
|
|
12
|
+
* Returns a string like "18" or null if not found.
|
|
13
|
+
*/
|
|
14
|
+
function detectNodeVersion(cwd) {
|
|
15
|
+
try {
|
|
16
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
17
|
+
if (!fs.existsSync(pkgPath)) return null;
|
|
18
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
19
|
+
const engines = pkg.engines?.node;
|
|
20
|
+
if (!engines || typeof engines !== 'string') return null;
|
|
21
|
+
const m = engines.match(/(\d+)/);
|
|
22
|
+
return m ? m[1] : null;
|
|
23
|
+
} catch { return null; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Detect Python version from pyproject.toml requires-python field.
|
|
28
|
+
*/
|
|
29
|
+
function detectPythonVersion(cwd) {
|
|
30
|
+
try {
|
|
31
|
+
const pyprojectPath = path.join(cwd, 'pyproject.toml');
|
|
32
|
+
if (!fs.existsSync(pyprojectPath)) return null;
|
|
33
|
+
const content = fs.readFileSync(pyprojectPath, 'utf-8');
|
|
34
|
+
const m = content.match(/requires-python\s*=\s*["'][^0-9]*(\d+\.\d+)/);
|
|
35
|
+
return m ? m[1] : null;
|
|
36
|
+
} catch { return null; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Detect Java version from build.gradle.kts or pom.xml.
|
|
41
|
+
*/
|
|
42
|
+
function detectJavaVersion(cwd) {
|
|
43
|
+
try {
|
|
44
|
+
const gradle = path.join(cwd, 'build.gradle.kts');
|
|
45
|
+
if (fs.existsSync(gradle)) {
|
|
46
|
+
const content = fs.readFileSync(gradle, 'utf-8');
|
|
47
|
+
const m = content.match(/JavaVersion\.VERSION_(\d+)|languageVersion\s*=\s*JavaLanguageVersion\.of\((\d+)\)|jvmToolchain\((\d+)\)/);
|
|
48
|
+
if (m) return m[1] || m[2] || m[3];
|
|
49
|
+
}
|
|
50
|
+
const pom = path.join(cwd, 'pom.xml');
|
|
51
|
+
if (fs.existsSync(pom)) {
|
|
52
|
+
const content = fs.readFileSync(pom, 'utf-8');
|
|
53
|
+
const m = content.match(/<maven\.compiler\.source>(\d+)|<java\.version>(\d+)/);
|
|
54
|
+
if (m) return m[1] || m[2];
|
|
55
|
+
}
|
|
56
|
+
} catch { /* skip */ }
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Detect Go version from go.mod.
|
|
62
|
+
*/
|
|
63
|
+
function detectGoVersion(cwd) {
|
|
64
|
+
try {
|
|
65
|
+
const goMod = path.join(cwd, 'go.mod');
|
|
66
|
+
if (!fs.existsSync(goMod)) return null;
|
|
67
|
+
const content = fs.readFileSync(goMod, 'utf-8');
|
|
68
|
+
const m = content.match(/^go\s+(\d+\.\d+)/m);
|
|
69
|
+
return m ? m[1] : null;
|
|
70
|
+
} catch { return null; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function generateGitHubActions(cwd, parsed) {
|
|
74
|
+
const dir = path.join(cwd, '.github', 'workflows');
|
|
75
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
76
|
+
|
|
77
|
+
// Detect versions from project files, fall back to current LTS defaults
|
|
78
|
+
const nodeVersion = detectNodeVersion(cwd) || '22';
|
|
79
|
+
const pythonVersion = detectPythonVersion(cwd) || '3.12';
|
|
80
|
+
const javaVersion = detectJavaVersion(cwd) || '21';
|
|
81
|
+
const goVersion = detectGoVersion(cwd) || '1.22';
|
|
82
|
+
|
|
83
|
+
let setupSteps = '';
|
|
84
|
+
if (parsed.runtimes.includes('node')) {
|
|
85
|
+
setupSteps += ' - name: Setup Node.js\n';
|
|
86
|
+
setupSteps += ' uses: actions/setup-node@v4\n';
|
|
87
|
+
setupSteps += ` with:\n node-version: '${nodeVersion}'\n`;
|
|
88
|
+
setupSteps += ' - run: npm ci\n';
|
|
89
|
+
}
|
|
90
|
+
if (parsed.runtimes.includes('rust')) {
|
|
91
|
+
setupSteps += ' - name: Setup Rust\n';
|
|
92
|
+
setupSteps += ' uses: dtolnay/rust-toolchain@stable\n';
|
|
93
|
+
}
|
|
94
|
+
if (parsed.runtimes.includes('python')) {
|
|
95
|
+
setupSteps += ' - name: Setup Python\n';
|
|
96
|
+
setupSteps += ' uses: actions/setup-python@v5\n';
|
|
97
|
+
setupSteps += ` with:\n python-version: '${pythonVersion}'\n`;
|
|
98
|
+
setupSteps += ' - run: pip install -r requirements.txt 2>/dev/null || true\n';
|
|
99
|
+
}
|
|
100
|
+
if (parsed.runtimes.includes('java')) {
|
|
101
|
+
setupSteps += ' - name: Setup Java\n';
|
|
102
|
+
setupSteps += ' uses: actions/setup-java@v4\n';
|
|
103
|
+
setupSteps += ` with:\n distribution: temurin\n java-version: '${javaVersion}'\n`;
|
|
104
|
+
}
|
|
105
|
+
if (parsed.runtimes.includes('go')) {
|
|
106
|
+
setupSteps += ' - name: Setup Go\n';
|
|
107
|
+
setupSteps += ' uses: actions/setup-go@v5\n';
|
|
108
|
+
setupSteps += ` with:\n go-version: '${goVersion}'\n`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Escape for YAML double-quoted scalar: \, ", and control chars.
|
|
112
|
+
const yamlDqEscape = (s) => String(s)
|
|
113
|
+
.replace(/\\/g, '\\\\')
|
|
114
|
+
.replace(/"/g, '\\"')
|
|
115
|
+
.replace(/\r/g, '\\r')
|
|
116
|
+
.replace(/\n/g, '\\n')
|
|
117
|
+
.replace(/\t/g, '\\t');
|
|
118
|
+
|
|
119
|
+
let gateSteps = '';
|
|
120
|
+
for (const gate of flattenGatesRich(parsed.gates)) {
|
|
121
|
+
const shell = gateToShell(gate.cmd);
|
|
122
|
+
const label = gate.cmd.length > 60 ? gate.cmd.substring(0, 57) + '...' : gate.cmd;
|
|
123
|
+
const prefix = gate.classification !== 'MANDATORY' ? `[${gate.classification}] ` : '';
|
|
124
|
+
const condExpr = gate.condition ? ` (if: ${gate.condition})` : '';
|
|
125
|
+
const workDir = gate.path ? `\n working-directory: ${gate.path}` : '';
|
|
126
|
+
const contOnErr = (gate.classification === 'OPTIONAL' || gate.classification === 'ADVISORY')
|
|
127
|
+
? '\n continue-on-error: true' : '';
|
|
128
|
+
const ifGuard = gate.condition
|
|
129
|
+
? `\n if: hashFiles('${gate.condition}') != ''` : '';
|
|
130
|
+
gateSteps += ` - name: "${prefix}${yamlDqEscape(gate.section)}: ${yamlDqEscape(label)}${yamlDqEscape(condExpr)}"${ifGuard}${workDir}${contOnErr}\n`;
|
|
131
|
+
gateSteps += ` run: |\n ${shell.replace(/\n/g, '\n ')}\n`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const yaml = [
|
|
135
|
+
'# Generated from governance.md by crag',
|
|
136
|
+
'# Regenerate: crag compile --target github',
|
|
137
|
+
'name: Governance Gates',
|
|
138
|
+
'',
|
|
139
|
+
'on:',
|
|
140
|
+
' push:',
|
|
141
|
+
' branches: [main, master]',
|
|
142
|
+
' pull_request:',
|
|
143
|
+
' branches: [main, master]',
|
|
144
|
+
'',
|
|
145
|
+
'jobs:',
|
|
146
|
+
' gates:',
|
|
147
|
+
' name: Governance Gates',
|
|
148
|
+
' runs-on: ubuntu-latest',
|
|
149
|
+
' steps:',
|
|
150
|
+
' - uses: actions/checkout@v4',
|
|
151
|
+
setupSteps + gateSteps,
|
|
152
|
+
].join('\n');
|
|
153
|
+
|
|
154
|
+
const outPath = path.join(dir, 'gates.yml');
|
|
155
|
+
atomicWrite(outPath, yaml);
|
|
156
|
+
console.log(` \x1b[32m✓\x1b[0m ${path.relative(cwd, outPath)}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = {
|
|
160
|
+
generateGitHubActions,
|
|
161
|
+
detectNodeVersion,
|
|
162
|
+
detectPythonVersion,
|
|
163
|
+
detectJavaVersion,
|
|
164
|
+
detectGoVersion,
|
|
165
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { gateToShell } = require('../governance/gate-to-shell');
|
|
6
|
+
const { flattenGatesRich } = require('../governance/parse');
|
|
7
|
+
const { atomicWrite } = require('./atomic-write');
|
|
8
|
+
|
|
9
|
+
function generateHusky(cwd, parsed) {
|
|
10
|
+
const dir = path.join(cwd, '.husky');
|
|
11
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
12
|
+
|
|
13
|
+
// Group gates by section for readable output
|
|
14
|
+
const sections = new Map();
|
|
15
|
+
for (const gate of flattenGatesRich(parsed.gates)) {
|
|
16
|
+
if (!sections.has(gate.section)) sections.set(gate.section, []);
|
|
17
|
+
sections.get(gate.section).push(gate);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let body = '';
|
|
21
|
+
for (const [section, gates] of sections) {
|
|
22
|
+
body += `# ${section}\n`;
|
|
23
|
+
for (const gate of gates) {
|
|
24
|
+
const shell = gateToShell(gate.cmd);
|
|
25
|
+
// Quote path/condition for shell safety
|
|
26
|
+
const quotedPath = gate.path ? gate.path.replace(/"/g, '\\"') : null;
|
|
27
|
+
const quotedCond = gate.condition ? gate.condition.replace(/"/g, '\\"') : null;
|
|
28
|
+
|
|
29
|
+
// Build the core command (with cd if path-scoped)
|
|
30
|
+
const coreCmd = quotedPath ? `(cd "${quotedPath}" && ${shell})` : shell;
|
|
31
|
+
|
|
32
|
+
// Build failure handler based on classification
|
|
33
|
+
let onFail;
|
|
34
|
+
if (gate.classification === 'OPTIONAL' || gate.classification === 'ADVISORY') {
|
|
35
|
+
const escLabel = shell.replace(/"/g, '\\"');
|
|
36
|
+
onFail = `echo " [${gate.classification}] Gate failed: ${escLabel}"`;
|
|
37
|
+
} else {
|
|
38
|
+
onFail = 'exit 1';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Wrap in conditional if section has `if:` annotation — skip cleanly if file missing
|
|
42
|
+
if (quotedCond) {
|
|
43
|
+
body += `if [ -e "${quotedCond}" ]; then ${coreCmd} || ${onFail}; fi\n`;
|
|
44
|
+
} else {
|
|
45
|
+
body += `${coreCmd} || ${onFail}\n`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
body += '\n';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const script = [
|
|
52
|
+
'#!/bin/sh',
|
|
53
|
+
'# Generated from governance.md by crag',
|
|
54
|
+
'# Regenerate: crag compile --target husky',
|
|
55
|
+
'set -e',
|
|
56
|
+
'',
|
|
57
|
+
body.trim(),
|
|
58
|
+
'',
|
|
59
|
+
].join('\n');
|
|
60
|
+
|
|
61
|
+
const outPath = path.join(dir, 'pre-commit');
|
|
62
|
+
atomicWrite(outPath, script);
|
|
63
|
+
console.log(` \x1b[32m✓\x1b[0m ${path.relative(cwd, outPath)}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { generateHusky };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { gateToShell } = require('../governance/gate-to-shell');
|
|
6
|
+
const { flattenGatesRich } = require('../governance/parse');
|
|
7
|
+
const { atomicWrite } = require('./atomic-write');
|
|
8
|
+
|
|
9
|
+
function generatePreCommitConfig(cwd, parsed) {
|
|
10
|
+
const gates = flattenGatesRich(parsed.gates);
|
|
11
|
+
|
|
12
|
+
let hooks = '';
|
|
13
|
+
gates.forEach((gate, i) => {
|
|
14
|
+
const id = `gate-${i + 1}`;
|
|
15
|
+
const prefix = gate.classification !== 'MANDATORY' ? `[${gate.classification}] ` : '';
|
|
16
|
+
const name = `${prefix}${gate.section}: ${gate.cmd}`;
|
|
17
|
+
const truncated = name.length > 60 ? name.substring(0, 57) + '...' : name;
|
|
18
|
+
|
|
19
|
+
let shell = gateToShell(gate.cmd);
|
|
20
|
+
if (gate.path) shell = `cd "${gate.path}" && ${shell}`;
|
|
21
|
+
if (gate.condition) shell = `[ -e "${gate.condition}" ] && (${shell}) || true`;
|
|
22
|
+
// For OPTIONAL/ADVISORY: never fail the hook
|
|
23
|
+
if (gate.classification === 'OPTIONAL' || gate.classification === 'ADVISORY') {
|
|
24
|
+
shell = `(${shell}) || echo "[${gate.classification}] failed — continuing"`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
hooks += ` - id: ${id}\n`;
|
|
28
|
+
hooks += ` name: "${truncated.replace(/"/g, '\\"')}"\n`;
|
|
29
|
+
hooks += ` entry: bash -c '${shell.replace(/'/g, "'\\''")}'\n`;
|
|
30
|
+
hooks += ' language: system\n';
|
|
31
|
+
hooks += ' pass_filenames: false\n';
|
|
32
|
+
hooks += ' always_run: true\n';
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const yaml = [
|
|
36
|
+
'# Generated from governance.md by crag',
|
|
37
|
+
'# Regenerate: crag compile --target pre-commit',
|
|
38
|
+
'repos:',
|
|
39
|
+
' - repo: local',
|
|
40
|
+
' hooks:',
|
|
41
|
+
hooks.trimEnd(),
|
|
42
|
+
'',
|
|
43
|
+
].join('\n');
|
|
44
|
+
|
|
45
|
+
const outPath = path.join(cwd, '.pre-commit-config.yaml');
|
|
46
|
+
atomicWrite(outPath, yaml);
|
|
47
|
+
console.log(` \x1b[32m✓\x1b[0m ${path.relative(cwd, outPath)}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { generatePreCommitConfig };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { flattenGatesRich } = require('../governance/parse');
|
|
5
|
+
const { atomicWrite } = require('./atomic-write');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Compile governance.md to Windsurf rules.
|
|
9
|
+
* Output: .windsurfrules
|
|
10
|
+
*
|
|
11
|
+
* Windsurf (by Codeium) is an AI-native IDE with a Cascade agent mode.
|
|
12
|
+
* It reads `.windsurfrules` at the workspace root for project-level guidance.
|
|
13
|
+
*
|
|
14
|
+
* Reference:
|
|
15
|
+
* https://docs.windsurf.com/windsurf/cascade/memories#rules
|
|
16
|
+
*/
|
|
17
|
+
function generateWindsurf(cwd, parsed) {
|
|
18
|
+
const gates = flattenGatesRich(parsed.gates);
|
|
19
|
+
|
|
20
|
+
const gatesList = gates.length === 0
|
|
21
|
+
? '(none defined)'
|
|
22
|
+
: gates
|
|
23
|
+
.map((g, i) => {
|
|
24
|
+
const prefix = g.classification !== 'MANDATORY' ? ` [${g.classification}]` : '';
|
|
25
|
+
const scope = g.path ? ` (in ${g.path})` : '';
|
|
26
|
+
return `${i + 1}. \`${g.cmd}\`${scope}${prefix}`;
|
|
27
|
+
})
|
|
28
|
+
.join('\n');
|
|
29
|
+
|
|
30
|
+
const content = `# Windsurf Rules — ${parsed.name || 'project'}
|
|
31
|
+
|
|
32
|
+
Generated from governance.md by crag. Regenerate: \`crag compile --target windsurf\`
|
|
33
|
+
|
|
34
|
+
## Project
|
|
35
|
+
|
|
36
|
+
${parsed.description || '(No description)'}
|
|
37
|
+
|
|
38
|
+
## Runtimes
|
|
39
|
+
|
|
40
|
+
${parsed.runtimes.join(', ') || 'polyglot — detected at runtime'}
|
|
41
|
+
|
|
42
|
+
## Cascade Behavior
|
|
43
|
+
|
|
44
|
+
When Windsurf's Cascade agent operates on this project:
|
|
45
|
+
|
|
46
|
+
- **Always read governance.md first.** It is the single source of truth for quality gates and policies.
|
|
47
|
+
- **Run all mandatory gates before proposing changes.** Stop on first failure.
|
|
48
|
+
- **Respect classifications.** OPTIONAL gates warn but don't block. ADVISORY gates are informational.
|
|
49
|
+
- **Respect path scopes.** Gates with a \`path:\` annotation must run from that directory.
|
|
50
|
+
- **No destructive commands.** Never run rm -rf, dd, DROP TABLE, force-push to main, curl|bash, docker system prune.
|
|
51
|
+
- **No secrets.** Reject any code matching \`sk_live\`, \`sk_test\`, \`AKIA\`, or plaintext credentials.
|
|
52
|
+
- **Conventional commits.** Every commit must follow \`<type>(<scope>): <description>\`.
|
|
53
|
+
|
|
54
|
+
## Quality Gates (run in order)
|
|
55
|
+
|
|
56
|
+
${gatesList}
|
|
57
|
+
|
|
58
|
+
## Rules of Engagement
|
|
59
|
+
|
|
60
|
+
1. **Minimal changes.** Don't rewrite files that weren't asked to change.
|
|
61
|
+
2. **No new dependencies** without explicit approval.
|
|
62
|
+
3. **Prefer editing** existing files over creating new ones.
|
|
63
|
+
4. **Always explain** non-obvious changes in commit messages.
|
|
64
|
+
5. **Ask before** destructive operations (delete, rename, migrate schema).
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
**Tool:** crag — https://www.npmjs.com/package/@whitehatd/crag
|
|
69
|
+
`;
|
|
70
|
+
|
|
71
|
+
const outPath = path.join(cwd, '.windsurfrules');
|
|
72
|
+
atomicWrite(outPath, content);
|
|
73
|
+
console.log(` \x1b[32m✓\x1b[0m ${path.relative(cwd, outPath)}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { generateWindsurf };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { flattenGatesRich } = require('../governance/parse');
|
|
5
|
+
const { atomicWrite } = require('./atomic-write');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Compile governance.md to Zed Editor assistant prompts.
|
|
9
|
+
* Output: .zed/rules.md (read by Zed's AI assistant as project context)
|
|
10
|
+
*
|
|
11
|
+
* Zed is a high-performance multiplayer code editor with an integrated
|
|
12
|
+
* AI assistant. Project-level rules live in `.zed/` configuration.
|
|
13
|
+
*
|
|
14
|
+
* Reference:
|
|
15
|
+
* https://zed.dev/docs/assistant/assistant
|
|
16
|
+
*/
|
|
17
|
+
function generateZed(cwd, parsed) {
|
|
18
|
+
const gates = flattenGatesRich(parsed.gates);
|
|
19
|
+
|
|
20
|
+
const gatesList = gates.length === 0
|
|
21
|
+
? '_(none defined)_'
|
|
22
|
+
: gates
|
|
23
|
+
.map((g) => {
|
|
24
|
+
const tags = [];
|
|
25
|
+
if (g.classification !== 'MANDATORY') tags.push(g.classification);
|
|
26
|
+
if (g.path) tags.push(`path:${g.path}`);
|
|
27
|
+
if (g.condition) tags.push(`if:${g.condition}`);
|
|
28
|
+
return `- \`${g.cmd}\`${tags.length > 0 ? ` _(${tags.join(', ')})_` : ''}`;
|
|
29
|
+
})
|
|
30
|
+
.join('\n');
|
|
31
|
+
|
|
32
|
+
const content = `# Zed Assistant Rules — ${parsed.name || 'project'}
|
|
33
|
+
|
|
34
|
+
> Generated from governance.md by crag. Regenerate: \`crag compile --target zed\`
|
|
35
|
+
|
|
36
|
+
## Project Summary
|
|
37
|
+
|
|
38
|
+
${parsed.description || '(No description)'}
|
|
39
|
+
|
|
40
|
+
**Runtimes:** ${parsed.runtimes.join(', ') || 'polyglot'}
|
|
41
|
+
|
|
42
|
+
## Rules for Zed AI Assistant
|
|
43
|
+
|
|
44
|
+
When suggesting edits or running the inline assistant:
|
|
45
|
+
|
|
46
|
+
### 1. Quality Gates
|
|
47
|
+
|
|
48
|
+
These must pass before any commit. Run them via Zed's terminal integration:
|
|
49
|
+
|
|
50
|
+
${gatesList}
|
|
51
|
+
|
|
52
|
+
### 2. Classification Semantics
|
|
53
|
+
|
|
54
|
+
- **MANDATORY** — stop if this fails
|
|
55
|
+
- **OPTIONAL** — warn and continue
|
|
56
|
+
- **ADVISORY** — log and continue (informational)
|
|
57
|
+
|
|
58
|
+
### 3. Scope Rules
|
|
59
|
+
|
|
60
|
+
- \`path:dir/\` — run the gate from that directory
|
|
61
|
+
- \`if:file\` — skip the gate's section when the file does not exist
|
|
62
|
+
|
|
63
|
+
### 4. Behavior Boundaries
|
|
64
|
+
|
|
65
|
+
- All file operations must stay within this repository.
|
|
66
|
+
- Never run destructive system commands (\`rm -rf /\`, \`DROP TABLE\`, \`curl|bash\`, force-push to main).
|
|
67
|
+
- Never commit hardcoded secrets. Grep for \`sk_live\`, \`sk_test\`, \`AKIA\`, \`password=\` before commit.
|
|
68
|
+
- Use conventional commits (\`feat:\`, \`fix:\`, \`docs:\`, etc.).
|
|
69
|
+
|
|
70
|
+
### 5. Authoritative Source
|
|
71
|
+
|
|
72
|
+
When these rules conflict with ad-hoc instructions, **governance.md wins**. It is the single source of truth for this project's policies.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
**Generated by crag** — https://www.npmjs.com/package/@whitehatd/crag
|
|
77
|
+
|
|
78
|
+
To update these rules, edit \`.claude/governance.md\` and re-run \`crag compile --target zed\`.
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
const outPath = path.join(cwd, '.zed', 'rules.md');
|
|
82
|
+
atomicWrite(outPath, content);
|
|
83
|
+
console.log(` \x1b[32m✓\x1b[0m ${path.relative(cwd, outPath)}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { generateZed };
|