agency-rules 0.1.0 → 0.3.2
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/CHANGELOG.md +43 -0
- package/package.json +13 -3
- package/scripts/convert.mjs +1 -1
- package/scripts/install.mjs +66 -21
- package/scripts/lib/detect.mjs +22 -19
- package/scripts/lib/frontmatter.mjs +21 -21
- package/scripts/lib/renderers/_util.mjs +3 -3
- package/scripts/lib/renderers/antigravity.mjs +14 -14
- package/scripts/lib/renderers/index.mjs +5 -5
- package/scripts/lib/renderers/oh-my-pi.mjs +8 -8
- package/scripts/lib/renderers/opencode.mjs +5 -5
- package/scripts/lib/renderers/pi-agent.mjs +9 -9
- package/scripts/validate.mjs +21 -21
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,49 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.2](https://github.com/deuriib/agency-rules/compare/v0.3.1...v0.3.2) (2026-10-31)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **ci:** completely remove NODE_AUTH_TOKEN to enforce OIDC ([59b966d](https://github.com/deuriib/agency-rules/commit/59b966dbdf29502b127f5b271de42f9ca56687f9))
|
|
14
|
+
* **ci:** completely remove NODE_AUTH_TOKEN to enforce OIDC ([a8229a7](https://github.com/deuriib/agency-rules/commit/a8229a7fc912f26b34dec9a2735a659ce63d12a9))
|
|
15
|
+
|
|
16
|
+
## [0.3.1](https://github.com/deuriib/agency-rules/compare/v0.3.0...v0.3.1) (2026-10-31)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* **ci:** resolve npm E404 by enforcing OIDC trusted publishing ([4266f4a](https://github.com/deuriib/agency-rules/commit/4266f4afd6a933c1835973e52ce09b4e620221a4))
|
|
22
|
+
|
|
23
|
+
## [0.3.0](https://github.com/deuriib/agency-rules/compare/v0.2.0...v0.3.0) (2026-10-31)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Features
|
|
27
|
+
|
|
28
|
+
* **skills:** add writing-rules skill for rule authoring ([41b99d7](https://github.com/deuriib/agency-rules/commit/41b99d7416fa52463303fc150e237fbc26ba90e1))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
### Bug Fixes
|
|
32
|
+
|
|
33
|
+
* **ci:** correct labels config file extension ([51617c6](https://github.com/deuriib/agency-rules/commit/51617c68abcf95b13040b1cfeadcbf52013ff9d4))
|
|
34
|
+
* **ci:** use node 24 for npm trusted publishing ([cf49055](https://github.com/deuriib/agency-rules/commit/cf4905539693f5a1d38ed902abc5a5c7c32d1635))
|
|
35
|
+
|
|
36
|
+
## [0.2.0](https://github.com/deuriib/agency-rules/compare/v0.1.0...v0.2.0) (2026-10-31)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
### Features
|
|
40
|
+
|
|
41
|
+
* bootstrap agency-rules with mise and four harness renderers ([c1bf3c9](https://github.com/deuriib/agency-rules/commit/c1bf3c9f078d84f578f2e37633dcc87fc98f3831))
|
|
42
|
+
* **install:** add interactive mode and harness/scope parameters ([e1996be](https://github.com/deuriib/agency-rules/commit/e1996bec3da5a34db23c06181dcb0071b35f5eff))
|
|
43
|
+
* **rules:** enforce conventional commits standard ([53f78b9](https://github.com/deuriib/agency-rules/commit/53f78b97a8669db60b7ac2110f10d9ad5658df39))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
### Bug Fixes
|
|
47
|
+
|
|
48
|
+
* **ci:** add missing mise sidecar for biome ([ded656e](https://github.com/deuriib/agency-rules/commit/ded656e16acc862ef975069459bc8dd324adc051))
|
|
49
|
+
* **install:** resolve biome linting errors and formatting ([04f4fee](https://github.com/deuriib/agency-rules/commit/04f4feeabcd2667d66b95865d0b7dd687cdd4798))
|
|
50
|
+
|
|
8
51
|
## [Unreleased]
|
|
9
52
|
|
|
10
53
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agency-rules",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Write rules once. Run them in every harness.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,8 +13,18 @@
|
|
|
13
13
|
"url": "https://github.com/deuriib/agency-rules/issues"
|
|
14
14
|
},
|
|
15
15
|
"homepage": "https://github.com/deuriib/agency-rules#readme",
|
|
16
|
-
"keywords": [
|
|
17
|
-
|
|
16
|
+
"keywords": [
|
|
17
|
+
"ai",
|
|
18
|
+
"agents",
|
|
19
|
+
"rules",
|
|
20
|
+
"antigravity",
|
|
21
|
+
"pi-agent",
|
|
22
|
+
"opencode",
|
|
23
|
+
"oh-my-pi"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20.12"
|
|
27
|
+
},
|
|
18
28
|
"files": [
|
|
19
29
|
"scripts/",
|
|
20
30
|
"tools.json",
|
package/scripts/convert.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
4
|
import { readRules } from './lib/frontmatter.mjs';
|
package/scripts/install.mjs
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
4
|
+
import { createInterface } from 'node:readline/promises';
|
|
3
5
|
import { parseArgs } from 'node:util';
|
|
4
6
|
import { detectHarnesses, expandHome, pathExists } from './lib/detect.mjs';
|
|
5
7
|
|
|
6
8
|
const { values: args } = parseArgs({
|
|
7
9
|
options: {
|
|
8
|
-
scope: { type: 'string'
|
|
10
|
+
scope: { type: 'string' },
|
|
9
11
|
harness: { type: 'string' },
|
|
10
12
|
all: { type: 'boolean', default: false },
|
|
11
13
|
'dry-run': { type: 'boolean', default: false },
|
|
12
14
|
},
|
|
13
15
|
});
|
|
14
16
|
|
|
15
|
-
if (!['project', 'user'].includes(args.scope)) {
|
|
16
|
-
console.error(`❌ Invalid --scope: ${args.scope} (use "project" or "user")`);
|
|
17
|
-
process.exit(1);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
17
|
const dryRun = args['dry-run'];
|
|
21
18
|
const tag = dryRun ? '[dry-run]' : ' ';
|
|
22
19
|
const log = (msg) => console.log(` ${tag} ${msg}`);
|
|
@@ -24,27 +21,75 @@ const log = (msg) => console.log(` ${tag} ${msg}`);
|
|
|
24
21
|
const INTEGRATIONS_DIR = process.env.INTEGRATIONS_DIR || 'integrations';
|
|
25
22
|
const tools = JSON.parse(await readFile('tools.json', 'utf8'));
|
|
26
23
|
|
|
24
|
+
let finalScope = args.scope;
|
|
25
|
+
let finalHarness = args.harness;
|
|
26
|
+
|
|
27
|
+
// ── Interactive Mode (si no se pasan parámetros y es una terminal) ─────
|
|
28
|
+
if (input.isTTY && ((!finalHarness && !args.all) || !finalScope)) {
|
|
29
|
+
const rl = createInterface({ input, output });
|
|
30
|
+
|
|
31
|
+
if (!finalHarness && !args.all) {
|
|
32
|
+
console.log('\n🎯 Select a harness to install into:');
|
|
33
|
+
const keys = Object.keys(tools.harnesses);
|
|
34
|
+
keys.forEach((k, i) => {
|
|
35
|
+
console.log(` ${i + 1}) ${k} (${tools.harnesses[k].label})`);
|
|
36
|
+
});
|
|
37
|
+
console.log(` ${keys.length + 1}) Detect automatically`);
|
|
38
|
+
console.log(` ${keys.length + 2}) All harnesses`);
|
|
39
|
+
|
|
40
|
+
const answer = await rl.question(`\nChoice [1-${keys.length + 2}] (default: detect): `);
|
|
41
|
+
const num = parseInt(answer.trim(), 10);
|
|
42
|
+
|
|
43
|
+
if (!Number.isNaN(num) && num >= 1 && num <= keys.length) {
|
|
44
|
+
finalHarness = keys[num - 1];
|
|
45
|
+
} else if (num === keys.length + 2 || answer.trim().toLowerCase() === 'all') {
|
|
46
|
+
args.all = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!finalScope) {
|
|
51
|
+
console.log('\n🌍 Select installation scope:');
|
|
52
|
+
console.log(' 1) project (local to this repo)');
|
|
53
|
+
console.log(' 2) user (global for your user)');
|
|
54
|
+
|
|
55
|
+
const answer = await rl.question('\nChoice [1-2] (default: 1): ');
|
|
56
|
+
finalScope =
|
|
57
|
+
answer.trim() === '2' || answer.trim().toLowerCase() === 'user' ? 'user' : 'project';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
rl.close();
|
|
61
|
+
console.log('');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Fallback si no se seleccionó nada
|
|
65
|
+
if (!finalScope) finalScope = 'project';
|
|
66
|
+
|
|
67
|
+
if (!['project', 'user'].includes(finalScope)) {
|
|
68
|
+
console.error(`❌ Invalid --scope: ${finalScope} (use "project" or "user")`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
27
72
|
let targets;
|
|
28
|
-
if (
|
|
29
|
-
if (!tools.harnesses[
|
|
30
|
-
console.error(`❌ Unknown harness: ${
|
|
73
|
+
if (finalHarness) {
|
|
74
|
+
if (!tools.harnesses[finalHarness]) {
|
|
75
|
+
console.error(`❌ Unknown harness: ${finalHarness}`);
|
|
31
76
|
process.exit(1);
|
|
32
77
|
}
|
|
33
|
-
targets = [
|
|
78
|
+
targets = [finalHarness];
|
|
34
79
|
} else if (args.all) {
|
|
35
80
|
targets = Object.keys(tools.harnesses);
|
|
36
81
|
} else {
|
|
37
82
|
targets = await detectHarnesses(tools);
|
|
38
83
|
if (targets.length === 0) {
|
|
39
|
-
console.warn(
|
|
40
|
-
`⚠️ No harness detected. Pass --harness <id> or --all to force.${dryRun ? '' : ''}`,
|
|
41
|
-
);
|
|
84
|
+
console.warn('⚠️ No harness detected. Pass --harness <id> or --all to force.');
|
|
42
85
|
process.exit(0);
|
|
43
86
|
}
|
|
44
87
|
}
|
|
45
88
|
|
|
46
89
|
console.log(
|
|
47
|
-
`🔍 Targets: ${targets.join(', ')} (scope: ${
|
|
90
|
+
`🔍 Targets: ${targets.join(', ')} (scope: ${finalScope})${
|
|
91
|
+
dryRun ? ' — dry-run, no changes will be made' : ''
|
|
92
|
+
}\n`,
|
|
48
93
|
);
|
|
49
94
|
|
|
50
95
|
let copied = 0;
|
|
@@ -64,9 +109,9 @@ for (const id of targets) {
|
|
|
64
109
|
}
|
|
65
110
|
|
|
66
111
|
const manifest = JSON.parse(await readFile(join(srcDir, '_manifest.json'), 'utf8'));
|
|
67
|
-
const destTemplate = harness.dest[
|
|
112
|
+
const destTemplate = harness.dest[finalScope];
|
|
68
113
|
if (!destTemplate) {
|
|
69
|
-
console.warn(`⚠️ ${id}: no dest for scope "${
|
|
114
|
+
console.warn(`⚠️ ${id}: no dest for scope "${finalScope}"`);
|
|
70
115
|
skippedHarnesses++;
|
|
71
116
|
continue;
|
|
72
117
|
}
|
|
@@ -84,7 +129,7 @@ for (const id of targets) {
|
|
|
84
129
|
}
|
|
85
130
|
|
|
86
131
|
if (harness.plugin) {
|
|
87
|
-
const changed = await handlePlugin(id, harness.plugin,
|
|
132
|
+
const changed = await handlePlugin(id, harness.plugin, finalScope, dryRun, log);
|
|
88
133
|
if (changed) pluginChanges++;
|
|
89
134
|
}
|
|
90
135
|
}
|
|
@@ -93,7 +138,7 @@ console.log(
|
|
|
93
138
|
dryRun
|
|
94
139
|
? `\n🔎 Dry-run: would copy ${copied} file(s) across ${targets.length - skippedHarnesses} harness(es).` +
|
|
95
140
|
(pluginChanges ? ` Would update ${pluginChanges} plugin config(s).` : '') +
|
|
96
|
-
|
|
141
|
+
' Nothing was written.'
|
|
97
142
|
: `\n🎉 Installed ${copied} file(s) across ${targets.length - skippedHarnesses} harness(es).` +
|
|
98
143
|
(pluginChanges ? ` Updated ${pluginChanges} plugin config(s).` : ''),
|
|
99
144
|
);
|
|
@@ -104,7 +149,7 @@ if (dryRun && skippedHarnesses > 0) {
|
|
|
104
149
|
);
|
|
105
150
|
}
|
|
106
151
|
|
|
107
|
-
// ── Plugin registration
|
|
152
|
+
// ── Plugin registration ──────────────────────────────────────────────
|
|
108
153
|
async function handlePlugin(id, plugin, scope, dry, logFn) {
|
|
109
154
|
const configPath =
|
|
110
155
|
scope === 'user'
|
|
@@ -135,7 +180,7 @@ async function handlePlugin(id, plugin, scope, dry, logFn) {
|
|
|
135
180
|
if (!dry) {
|
|
136
181
|
config.plugin.push(plugin.entry);
|
|
137
182
|
await mkdir(dirname(configPath), { recursive: true });
|
|
138
|
-
await writeFile(configPath, JSON.stringify(config, null, 2)
|
|
183
|
+
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
139
184
|
}
|
|
140
185
|
|
|
141
186
|
return true;
|
package/scripts/lib/detect.mjs
CHANGED
|
@@ -3,30 +3,33 @@ import { homedir } from 'node:os';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
|
|
5
5
|
export function expandHome(p) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
if (p === '~') return homedir();
|
|
7
|
+
if (p.startsWith('~/')) return join(homedir(), p.slice(2));
|
|
8
|
+
return p;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export async function pathExists(p) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
try {
|
|
13
|
+
await access(expandHome(p));
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export async function detectHarnesses(tools) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
const detected = [];
|
|
22
|
+
for (const [id, harness] of Object.entries(tools.harnesses)) {
|
|
23
|
+
const dirs = harness.detect?.dirs ?? [];
|
|
24
|
+
const files = harness.detect?.files ?? [];
|
|
25
|
+
let found = false;
|
|
26
|
+
for (const p of [...dirs, ...files]) {
|
|
27
|
+
if (await pathExists(p)) {
|
|
28
|
+
found = true;
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (found) detected.push(id);
|
|
28
33
|
}
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
return detected;
|
|
34
|
+
return detected;
|
|
32
35
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
-
import { join, relative
|
|
2
|
+
import { basename, join, relative } from 'node:path';
|
|
3
3
|
import matter from 'gray-matter';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -10,28 +10,28 @@ import matter from 'gray-matter';
|
|
|
10
10
|
* filename as fallback. Files starting with `_` are skipped.
|
|
11
11
|
*/
|
|
12
12
|
export async function readRules(dir) {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const entries = await readdir(dir, { recursive: true, withFileTypes: true });
|
|
14
|
+
const rules = [];
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
if (!entry.isFile()) continue;
|
|
18
|
+
if (!entry.name.endsWith('.md')) continue;
|
|
19
|
+
if (entry.name.startsWith('_')) continue;
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
const parent = entry.parentPath ?? entry.path;
|
|
22
|
+
const filePath = join(parent, entry.name);
|
|
23
|
+
const raw = await readFile(filePath, 'utf8');
|
|
24
|
+
const parsed = matter(raw);
|
|
25
|
+
const filename = basename(filePath, '.md');
|
|
26
|
+
const slug = typeof parsed.data?.name === 'string' ? parsed.data.name : filename;
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
rules.push({
|
|
29
|
+
slug,
|
|
30
|
+
source: relative(dir, filePath),
|
|
31
|
+
data: parsed.data ?? {},
|
|
32
|
+
body: (parsed.content ?? '').trim(),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
return rules.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
37
37
|
}
|
|
@@ -2,7 +2,7 @@ import { stringify as yamlStringify } from 'yaml';
|
|
|
2
2
|
|
|
3
3
|
/** Wraps YAML frontmatter + body in a Markdown document. */
|
|
4
4
|
export function wrap(data, body) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
const fm = yamlStringify(data).trimEnd();
|
|
6
|
+
const content = (body ?? '').trim();
|
|
7
|
+
return `---\n${fm}\n---\n\n${content}\n`;
|
|
8
8
|
}
|
|
@@ -10,21 +10,21 @@ import { wrap } from './_util.mjs';
|
|
|
10
10
|
* 5. nothing → manual
|
|
11
11
|
*/
|
|
12
12
|
export function renderAntigravity(rule, _harness) {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const d = rule.data;
|
|
14
|
+
let trigger = d.trigger;
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
if (!trigger) {
|
|
17
|
+
if (d.alwaysApply === true) trigger = 'always_on';
|
|
18
|
+
else if (d.globs) trigger = 'glob';
|
|
19
|
+
else if (d.description) trigger = 'model_decision';
|
|
20
|
+
else trigger = 'manual';
|
|
21
|
+
}
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
const fm = { trigger };
|
|
24
|
+
if (trigger === 'glob' && d.globs) {
|
|
25
|
+
fm.globs = Array.isArray(d.globs) ? d.globs.join(',') : d.globs;
|
|
26
|
+
}
|
|
27
|
+
if (d.description) fm.description = d.description;
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
return wrap(fm, rule.body);
|
|
30
30
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { renderAntigravity } from './antigravity.mjs';
|
|
2
|
-
import { renderPiAgent } from './pi-agent.mjs';
|
|
3
2
|
import { renderOhMyPi } from './oh-my-pi.mjs';
|
|
4
3
|
import { renderOpencode } from './opencode.mjs';
|
|
4
|
+
import { renderPiAgent } from './pi-agent.mjs';
|
|
5
5
|
|
|
6
6
|
/** Maps `format` from tools.json → renderer function. */
|
|
7
7
|
export const renderers = {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
'md-yaml-trigger': renderAntigravity,
|
|
9
|
+
'md-rules-dir': renderPiAgent,
|
|
10
|
+
'md-or-mdc': renderOhMyPi,
|
|
11
|
+
'md-plugin': renderOpencode,
|
|
12
12
|
};
|
|
@@ -5,12 +5,12 @@ import { wrap } from './_util.mjs';
|
|
|
5
5
|
* `alwaysApply` in the frontmatter. Accepts .md or .mdc; we emit .md.
|
|
6
6
|
*/
|
|
7
7
|
export function renderOhMyPi(rule, _harness) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
const d = rule.data;
|
|
9
|
+
const fm = {
|
|
10
|
+
name: d.name,
|
|
11
|
+
description: d.description,
|
|
12
|
+
};
|
|
13
|
+
if (d.globs !== undefined) fm.globs = d.globs;
|
|
14
|
+
if (d.alwaysApply !== undefined) fm.alwaysApply = d.alwaysApply;
|
|
15
|
+
return wrap(fm, rule.body);
|
|
16
16
|
}
|
|
@@ -5,9 +5,9 @@ import { wrap } from './_util.mjs';
|
|
|
5
5
|
* (used for matching) and optional `globs`/`match` controls.
|
|
6
6
|
*/
|
|
7
7
|
export function renderOpencode(rule, _harness) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
const d = rule.data;
|
|
9
|
+
const fm = { description: d.description };
|
|
10
|
+
if (d.globs !== undefined) fm.globs = d.globs;
|
|
11
|
+
if (d.match !== undefined) fm.match = d.match;
|
|
12
|
+
return wrap(fm, rule.body);
|
|
13
13
|
}
|
|
@@ -5,13 +5,13 @@ import { wrap } from './_util.mjs';
|
|
|
5
5
|
* frontmatter carrying `name`, `description`, and optionally `tools`.
|
|
6
6
|
*/
|
|
7
7
|
export function renderPiAgent(rule, _harness) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
const d = rule.data;
|
|
9
|
+
const fm = {
|
|
10
|
+
name: d.name,
|
|
11
|
+
description: d.description,
|
|
12
|
+
};
|
|
13
|
+
if (Array.isArray(d.tools) && d.tools.length > 0) {
|
|
14
|
+
fm.tools = d.tools;
|
|
15
|
+
}
|
|
16
|
+
return wrap(fm, rule.body);
|
|
17
17
|
}
|
package/scripts/validate.mjs
CHANGED
|
@@ -12,41 +12,41 @@ const validate = ajv.compile(schema);
|
|
|
12
12
|
const rules = await readRules(RULES_DIR);
|
|
13
13
|
|
|
14
14
|
if (rules.length === 0) {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
console.error(`❌ No rules found in ${RULES_DIR}`);
|
|
16
|
+
process.exit(1);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const seen = new Set();
|
|
20
20
|
let failures = 0;
|
|
21
21
|
|
|
22
22
|
for (const rule of rules) {
|
|
23
|
-
|
|
23
|
+
const errors = [];
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
if (seen.has(rule.slug)) {
|
|
26
|
+
errors.push({ instancePath: '/name', message: `duplicate rule name "${rule.slug}"` });
|
|
27
|
+
}
|
|
28
|
+
seen.add(rule.slug);
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
if (!validate(rule.data)) {
|
|
31
|
+
for (const e of validate.errors ?? []) {
|
|
32
|
+
errors.push(e);
|
|
33
|
+
}
|
|
33
34
|
}
|
|
34
|
-
}
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
if (errors.length > 0) {
|
|
37
|
+
failures++;
|
|
38
|
+
console.error(`❌ ${rule.source}`);
|
|
39
|
+
for (const e of errors) {
|
|
40
|
+
console.error(` ${e.instancePath || '/'} ${e.message}`);
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
console.log(`✅ ${rule.source}`);
|
|
41
44
|
}
|
|
42
|
-
} else {
|
|
43
|
-
console.log(`✅ ${rule.source}`);
|
|
44
|
-
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
if (failures > 0) {
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
console.error(`\n💥 ${failures}/${rules.length} rule(s) failed validation.`);
|
|
49
|
+
process.exit(1);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
console.log(`\n🎉 ${rules.length} rule(s) validated successfully.`);
|