agency-rules 0.1.0 → 0.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/CHANGELOG.md CHANGED
@@ -5,6 +5,56 @@ 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.4.0](https://github.com/deuriib/agency-rules/compare/v0.3.2...v0.4.0) (2026-10-31)
9
+
10
+
11
+ ### Features
12
+
13
+ * **cli:** expose agency-rules binary and router ([dfb6b95](https://github.com/deuriib/agency-rules/commit/dfb6b95b8b6d9ac6f4622479527f182d9c1e637c))
14
+
15
+ ## [0.3.2](https://github.com/deuriib/agency-rules/compare/v0.3.1...v0.3.2) (2026-10-31)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * **ci:** completely remove NODE_AUTH_TOKEN to enforce OIDC ([59b966d](https://github.com/deuriib/agency-rules/commit/59b966dbdf29502b127f5b271de42f9ca56687f9))
21
+ * **ci:** completely remove NODE_AUTH_TOKEN to enforce OIDC ([a8229a7](https://github.com/deuriib/agency-rules/commit/a8229a7fc912f26b34dec9a2735a659ce63d12a9))
22
+
23
+ ## [0.3.1](https://github.com/deuriib/agency-rules/compare/v0.3.0...v0.3.1) (2026-10-31)
24
+
25
+
26
+ ### Bug Fixes
27
+
28
+ * **ci:** resolve npm E404 by enforcing OIDC trusted publishing ([4266f4a](https://github.com/deuriib/agency-rules/commit/4266f4afd6a933c1835973e52ce09b4e620221a4))
29
+
30
+ ## [0.3.0](https://github.com/deuriib/agency-rules/compare/v0.2.0...v0.3.0) (2026-10-31)
31
+
32
+
33
+ ### Features
34
+
35
+ * **skills:** add writing-rules skill for rule authoring ([41b99d7](https://github.com/deuriib/agency-rules/commit/41b99d7416fa52463303fc150e237fbc26ba90e1))
36
+
37
+
38
+ ### Bug Fixes
39
+
40
+ * **ci:** correct labels config file extension ([51617c6](https://github.com/deuriib/agency-rules/commit/51617c68abcf95b13040b1cfeadcbf52013ff9d4))
41
+ * **ci:** use node 24 for npm trusted publishing ([cf49055](https://github.com/deuriib/agency-rules/commit/cf4905539693f5a1d38ed902abc5a5c7c32d1635))
42
+
43
+ ## [0.2.0](https://github.com/deuriib/agency-rules/compare/v0.1.0...v0.2.0) (2026-10-31)
44
+
45
+
46
+ ### Features
47
+
48
+ * bootstrap agency-rules with mise and four harness renderers ([c1bf3c9](https://github.com/deuriib/agency-rules/commit/c1bf3c9f078d84f578f2e37633dcc87fc98f3831))
49
+ * **install:** add interactive mode and harness/scope parameters ([e1996be](https://github.com/deuriib/agency-rules/commit/e1996bec3da5a34db23c06181dcb0071b35f5eff))
50
+ * **rules:** enforce conventional commits standard ([53f78b9](https://github.com/deuriib/agency-rules/commit/53f78b97a8669db60b7ac2110f10d9ad5658df39))
51
+
52
+
53
+ ### Bug Fixes
54
+
55
+ * **ci:** add missing mise sidecar for biome ([ded656e](https://github.com/deuriib/agency-rules/commit/ded656e16acc862ef975069459bc8dd324adc051))
56
+ * **install:** resolve biome linting errors and formatting ([04f4fee](https://github.com/deuriib/agency-rules/commit/04f4feeabcd2667d66b95865d0b7dd687cdd4798))
57
+
8
58
  ## [Unreleased]
9
59
 
10
60
  ### Added
package/README.md CHANGED
@@ -29,6 +29,23 @@ with a single command, driven by a declarative contract (`tools.json`).
29
29
 
30
30
  ---
31
31
 
32
+ ## Global CLI Usage
33
+
34
+ You can also use `agency-rules` directly via `npx` without cloning the repository:
35
+
36
+ ```bash
37
+ # Validate rules
38
+ npx agency-rules validate
39
+
40
+ # Compile rules
41
+ npx agency-rules convert
42
+
43
+ # Install rules interactively
44
+ npx agency-rules install
45
+ ```
46
+
47
+ ---
48
+
32
49
  ## Quick Start
33
50
 
34
51
  ```bash
@@ -140,6 +157,8 @@ See [CONTRIBUTING.md](CONTRIBUTING.md). Pull requests welcome.
140
157
 
141
158
  See [SECURITY.md](SECURITY.md) for responsible disclosure.
142
159
 
160
+ ![Repobeats analytics](https://repobeats.com/deuriib/agency-rules?theme=light&size=medium&period=30)
161
+
143
162
  ## License
144
163
 
145
164
  MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile } from 'node:fs/promises';
4
+ import { resolve } from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+
7
+ const command = process.argv[2];
8
+
9
+ // ── Read version dynamically ──────────────────────────────────────────
10
+ const getVersion = async () => {
11
+ try {
12
+ const pkgPath = resolve(import.meta.dirname, '..', 'package.json');
13
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
14
+ return pkg.version || '0.0.0';
15
+ } catch {
16
+ return '0.0.0';
17
+ }
18
+ };
19
+
20
+ const version = await getVersion();
21
+
22
+ // ── Help Menus ────────────────────────────────────────────────────────
23
+ const showHelp = () => {
24
+ console.log(`
25
+ \x1b[36magency-rules\x1b[0m v${version}
26
+ Write rules once. Run them in every harness.
27
+
28
+ \x1b[33mUsage:\x1b[0m
29
+ $ agency-rules <command> [options]
30
+
31
+ \x1b[33mCommands:\x1b[0m
32
+ \x1b[32mconvert\x1b[0m Compile rules/ into each harness's native format
33
+ \x1b[32minstall\x1b[0m Install compiled artifacts into detected harnesses
34
+ \x1b[32mvalidate\x1b[0m Validate each rule's frontmatter against the schema
35
+
36
+ \x1b[33mGlobal Options:\x1b[0m
37
+ -h, --help Show this help message
38
+ -v, --version Show version number
39
+
40
+ \x1b[33mExamples:\x1b[0m
41
+ $ agency-rules validate
42
+ $ agency-rules convert
43
+ $ agency-rules convert --dry-run
44
+ $ agency-rules install --scope user --harness opencode
45
+ `);
46
+ };
47
+
48
+ const showCommandHelp = (cmd) => {
49
+ const helps = {
50
+ convert: `
51
+ \x1b[36magency-rules convert\x1b[0m
52
+ Compile rules/ into each harness's native format.
53
+
54
+ \x1b[33mOptions:\x1b[0m
55
+ --dry-run Preview which files would be written (no writes)
56
+ --harness <id> Limit compilation to a specific harness (e.g., antigravity)
57
+
58
+ \x1b[33mExamples:\x1b[0m
59
+ $ agency-rules convert
60
+ $ agency-rules convert --dry-run
61
+ $ agency-rules convert --harness pi-agent
62
+ `,
63
+ install: `
64
+ \x1b[36magency-rules install\x1b[0m
65
+ Install compiled artifacts into detected harnesses.
66
+
67
+ \x1b[33mOptions:\x1b[0m
68
+ --dry-run Preview which files would be copied (no writes)
69
+ --scope <project|user> Install local to project or global to user (default: project)
70
+ --harness <id> Install into a specific harness
71
+ --all Force installation for all harnesses, ignoring auto-detection
72
+
73
+ \x1b[33mExamples:\x1b[0m
74
+ $ agency-rules install
75
+ $ agency-rules install --dry-run
76
+ $ agency-rules install --scope user
77
+ $ agency-rules install --harness opencode --scope project
78
+ `,
79
+ validate: `
80
+ \x1b[36magency-rules validate\x1b[0m
81
+ Validate each rule's frontmatter against the schema.
82
+
83
+ \x1b[33mOptions:\x1b[0m
84
+ (No specific options)
85
+
86
+ \x1b[33mExamples:\x1b[0m
87
+ $ agency-rules validate
88
+ `,
89
+ };
90
+ console.log(helps[cmd] || '');
91
+ };
92
+
93
+ // ── Global Flags ──────────────────────────────────────────────────────
94
+ if (process.argv.includes('--version') || process.argv.includes('-v')) {
95
+ console.log(version);
96
+ process.exit(0);
97
+ }
98
+
99
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
100
+ showHelp();
101
+ process.exit(0);
102
+ }
103
+
104
+ if (!['convert', 'install', 'validate'].includes(command)) {
105
+ console.error(`\x1b[31mError:\x1b[0m Unknown command '${command}'`);
106
+ showHelp();
107
+ process.exit(1);
108
+ }
109
+
110
+ // ── Command-specific Help ─────────────────────────────────────────────
111
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
112
+ showCommandHelp(command);
113
+ process.exit(0);
114
+ }
115
+
116
+ // ── Execute ───────────────────────────────────────────────────────────
117
+ // Remove the command from argv so the downstream script's parseArgs works cleanly
118
+ process.argv.splice(2, 1);
119
+
120
+ const scriptPath = resolve(import.meta.dirname, '..', 'scripts', `${command}.mjs`);
121
+
122
+ try {
123
+ await import(pathToFileURL(scriptPath).href);
124
+ } catch (err) {
125
+ console.error(`\x1b[31m❌ Failed to execute '${command}':\x1b[0m`, err.message);
126
+ process.exit(1);
127
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agency-rules",
3
- "version": "0.1.0",
3
+ "version": "0.4.0",
4
4
  "description": "Write rules once. Run them in every harness.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -13,8 +13,21 @@
13
13
  "url": "https://github.com/deuriib/agency-rules/issues"
14
14
  },
15
15
  "homepage": "https://github.com/deuriib/agency-rules#readme",
16
- "keywords": ["ai", "agents", "rules", "antigravity", "pi-agent", "opencode", "oh-my-pi"],
17
- "engines": { "node": ">=20.12" },
16
+ "keywords": [
17
+ "ai",
18
+ "agents",
19
+ "rules",
20
+ "antigravity",
21
+ "pi-agent",
22
+ "opencode",
23
+ "oh-my-pi"
24
+ ],
25
+ "bin": {
26
+ "agency-rules": "./bin/agency-rules.mjs"
27
+ },
28
+ "engines": {
29
+ "node": ">=20.12"
30
+ },
18
31
  "files": [
19
32
  "scripts/",
20
33
  "tools.json",
@@ -1,4 +1,4 @@
1
- import { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
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';
@@ -1,22 +1,19 @@
1
- import { access, copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
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', default: 'project' },
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 (args.harness) {
29
- if (!tools.harnesses[args.harness]) {
30
- console.error(`❌ Unknown harness: ${args.harness}`);
73
+ if (finalHarness) {
74
+ if (!tools.harnesses[finalHarness]) {
75
+ console.error(`❌ Unknown harness: ${finalHarness}`);
31
76
  process.exit(1);
32
77
  }
33
- targets = [args.harness];
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: ${args.scope})${dryRun ? ' — dry-run, no changes will be made' : ''}\n`,
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[args.scope];
112
+ const destTemplate = harness.dest[finalScope];
68
113
  if (!destTemplate) {
69
- console.warn(`⚠️ ${id}: no dest for scope "${args.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, args.scope, dryRun, log);
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
- ` Nothing was written.`
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) + '\n');
183
+ await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`);
139
184
  }
140
185
 
141
186
  return true;
@@ -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
- if (p === '~') return homedir();
7
- if (p.startsWith('~/')) return join(homedir(), p.slice(2));
8
- return p;
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
- try {
13
- await access(expandHome(p));
14
- return true;
15
- } catch {
16
- return false;
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
- 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)) { found = true; break; }
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
- if (found) detected.push(id);
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, basename } from 'node:path';
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
- const entries = await readdir(dir, { recursive: true, withFileTypes: true });
14
- const rules = [];
13
+ const entries = await readdir(dir, { recursive: true, withFileTypes: true });
14
+ const rules = [];
15
15
 
16
- for (const entry of entries) {
17
- if (!entry.isFile()) continue;
18
- if (!entry.name.endsWith('.md')) continue;
19
- if (entry.name.startsWith('_')) continue;
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
- 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;
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
- rules.push({
29
- slug,
30
- source: relative(dir, filePath),
31
- data: parsed.data ?? {},
32
- body: (parsed.content ?? '').trim(),
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
- return rules.sort((a, b) => a.slug.localeCompare(b.slug));
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
- const fm = yamlStringify(data).trimEnd();
6
- const content = (body ?? '').trim();
7
- return `---\n${fm}\n---\n\n${content}\n`;
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
- const d = rule.data;
14
- let trigger = d.trigger;
13
+ const d = rule.data;
14
+ let trigger = d.trigger;
15
15
 
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
- }
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
- 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;
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
- return wrap(fm, rule.body);
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
- 'md-yaml-trigger': renderAntigravity,
9
- 'md-rules-dir': renderPiAgent,
10
- 'md-or-mdc': renderOhMyPi,
11
- 'md-plugin': renderOpencode,
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
- 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);
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
- 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);
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
- 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);
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
  }
@@ -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
- console.error(`❌ No rules found in ${RULES_DIR}`);
16
- process.exit(1);
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
- const errors = [];
23
+ const errors = [];
24
24
 
25
- if (seen.has(rule.slug)) {
26
- errors.push({ instancePath: '/name', message: `duplicate rule name "${rule.slug}"` });
27
- }
28
- seen.add(rule.slug);
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
- if (!validate(rule.data)) {
31
- for (const e of validate.errors ?? []) {
32
- errors.push(e);
30
+ if (!validate(rule.data)) {
31
+ for (const e of validate.errors ?? []) {
32
+ errors.push(e);
33
+ }
33
34
  }
34
- }
35
35
 
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}`);
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
- console.error(`\n💥 ${failures}/${rules.length} rule(s) failed validation.`);
49
- process.exit(1);
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.`);