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 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.1.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": ["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
+ "engines": {
26
+ "node": ">=20.12"
27
+ },
18
28
  "files": [
19
29
  "scripts/",
20
30
  "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.`);