agency-rules 0.0.0-stage → 0.1.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 ADDED
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+
12
+ - Initial scaffold.
13
+ - Conversion engine driven by `tools.json`.
14
+ - Renderers for **Google Antigravity**, **pi-agent**, **oh-my-pi (omp)**,
15
+ and **OpenCode** (via `opencode-rules`).
16
+ - `mise.toml` with toolchain (`node@22`), tasks (`validate`, `convert`,
17
+ `install`, `test`, `ci`), and post-install hook.
18
+ - Test suites for frontmatter parsing and every renderer.
19
+ - Documentation: architecture, rule spec, harness extension guide.
20
+
21
+ ### Changed
22
+
23
+ - N/A
24
+
25
+ ### Fixed
26
+
27
+ - N/A
28
+
29
+ ---
30
+
31
+ ## [0.1.0] — TBD
32
+
33
+ First tagged release. Reserved for the v0.1.0 cut once the harness set is
34
+ stable.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Deuri Vasquez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,145 @@
1
- # Temporary Holding Version
1
+ # agency-rules
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Write rules once. Run them everywhere.**
4
+
5
+ `agency-rules` compiles a single set of generic Markdown rules into the
6
+ native formats expected by **Google Antigravity**, **pi-agent**,
7
+ **oh-my-pi (omp)**, and **OpenCode** (via the `opencode-rules` plugin).
8
+
9
+ The whole pipeline — toolchain, environment, and tasks — is managed by
10
+ [mise](https://mise.jdx.dev), so every collaborator and CI runner uses the
11
+ exact same Node version and the exact same commands.
12
+
13
+ ---
14
+
15
+ ## Why
16
+
17
+ Every coding agent has its own rule format:
18
+
19
+ | Harness | Format | Location |
20
+ | --- | --- | --- |
21
+ | Google Antigravity | YAML frontmatter with `trigger` | `.agents/rules/*.md` |
22
+ | pi-agent | Per-rule directory with `RULES.md` | `.pi/rules/<slug>/RULES.md` |
23
+ | oh-my-pi (omp) | Markdown with frontmatter | `.omp/rules/*.{md,mdc}` |
24
+ | OpenCode | Markdown via `opencode-rules` plugin | `.opencode/rules/*.md` |
25
+
26
+ Writing the same rule four times is a maintenance nightmare. `agency-rules`
27
+ lets you write it **once** in a neutral format and compile it to all four
28
+ with a single command, driven by a declarative contract (`tools.json`).
29
+
30
+ ---
31
+
32
+ ## Quick Start
33
+
34
+ ```bash
35
+ # 1. Install toolchain + dependencies (one time)
36
+ git clone https://github.com/<your-org>/agency-rules.git
37
+ cd agency-rules
38
+ mise install
39
+
40
+ # 2. Compile rules to every harness
41
+ mise run convert
42
+
43
+ # 3. Install into harnesses detected on your machine
44
+ mise run install --scope project
45
+ ```
46
+
47
+ That's it. Your rules are now live in `.agents/`, `.pi/`, `.omp/`, and
48
+ `.opencode/` (and the `opencode-rules` plugin is registered in
49
+ `opencode.json` automatically).
50
+
51
+ ---
52
+
53
+ ## Repository layout
54
+
55
+ ```
56
+ agency-rules/
57
+ ├── rules/ ← source of truth (Markdown + YAML frontmatter)
58
+ ├── integrations/ ← compiled output (generated, gitignored)
59
+ ├── scripts/ ← convert, install, validate + renderers
60
+ ├── tests/ ← node:test suites for renderers and parsing
61
+ ├── docs/ ← architecture, spec, extension guide
62
+ ├── tools.json ← the conversion contract
63
+ └── mise.toml ← toolchain + tasks
64
+ ```
65
+
66
+ ---
67
+
68
+ 1. **Validate**: `mise run validate`.
69
+ 2. **Convert**: `mise run convert` and inspect `integrations/`.
70
+ 3. **Test**: `mise run test`.
71
+
72
+ ## Commands
73
+
74
+ All commands go through mise:
75
+
76
+ | Command | What it does |
77
+ | --- | --- |
78
+ | `mise run setup` | Install Node dependencies (`npm ci`) |
79
+ | `mise run validate` | Validate every rule's frontmatter against `rules/_schema.json` |
80
+ | `mise run check` | Format + lint + organize imports with Biome (write mode) |
81
+ | `mise run convert` | Compile `rules/` → `integrations/<harness>/` |
82
+ | `mise run convert:dry-run` | Preview which files `convert` would write (no writes) |
83
+ | `mise run install` | Copy compiled output into harness directories |
84
+ | `mise run install:dry-run` | Preview which files `install` would copy (no writes) |
85
+ | `mise run test` | Run the Node test suite |
86
+ | `mise run ci` | Full pipeline (`validate` → `check` → `convert` → `test`) |
87
+
88
+ See `mise tasks` for the up-to-date list.
89
+
90
+ ### Dry-run workflow
91
+
92
+ The typical inner loop uses dry-runs first to inspect what will change
93
+ before anything hits disk:
94
+
95
+ ```bash
96
+ mise run convert:dry-run # see generated file paths + sizes
97
+ mise run convert # actually write to integrations/
98
+ mise run install:dry-run -- --all # see copy destinations
99
+ mise run install -- --all # actually copy into harness dirs
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Adding a rule
105
+
106
+ 1. Create `rules/<category>/<slug>.md` following
107
+ [`docs/RULE-SPEC.md`](docs/RULE-SPEC.md).
108
+ 2. Run `mise run validate` — it must pass.
109
+ 3. Run `mise run convert` — inspect `integrations/` to sanity-check output.
110
+ 4. Open a PR.
111
+
112
+ ---
113
+
114
+ ## Adding a harness
115
+
116
+ See [`docs/ADDING-A-HARNESS.md`](docs/ADDING-A-HARNESS.md). It's roughly:
117
+
118
+ 1. Add an entry in `tools.json`.
119
+ 2. Add a renderer in `scripts/lib/renderers/`.
120
+ 3. Register the renderer in `scripts/lib/renderers/index.mjs`.
121
+ 4. Add tests in `tests/convert.test.mjs`.
122
+
123
+ The conversion engine itself never changes.
124
+
125
+ ---
126
+
127
+ ## Documentation
128
+
129
+ - [Architecture](docs/ARCHITECTURE.md) — data flow, contracts, caching.
130
+ - [Rule spec](docs/RULE-SPEC.md) — the neutral frontmatter format.
131
+ - [Adding a harness](docs/ADDING-A-HARNESS.md) — extension guide.
132
+
133
+ ---
134
+
135
+ ## Contributing
136
+
137
+ See [CONTRIBUTING.md](CONTRIBUTING.md). Pull requests welcome.
138
+
139
+ ## Security
140
+
141
+ See [SECURITY.md](SECURITY.md) for responsible disclosure.
142
+
143
+ ## License
144
+
145
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,170 @@
1
+ # Adding a Harness
2
+
3
+ This guide walks through adding support for a new coding agent. The example
4
+ uses a hypothetical harness called **`example-agent`**.
5
+
6
+ ---
7
+
8
+ ## Prerequisites
9
+
10
+ Read [`ARCHITECTURE.md`](ARCHITECTURE.md) first. You need to understand:
11
+
12
+ - Why `tools.json` is a contract (not a list).
13
+ - What renderers do (pure functions, no I/O).
14
+ - What the installer does (copies per manifest).
15
+
16
+ ---
17
+
18
+ ## Step 1 — Add an entry to `tools.json`
19
+
20
+ Add a new key under `harnesses`. Every field matters:
21
+
22
+ ```json
23
+ "example-agent": {
24
+ "id": "example-agent",
25
+ "label": "Example Agent",
26
+ "format": "md-example",
27
+ "installKind": "per-rule",
28
+ "integrationPath": "{{slug}}.md",
29
+ "detect": { "dirs": [".example", "~/.example"] },
30
+ "dest": {
31
+ "project": ".example/rules/{{slug}}.md",
32
+ "user": "~/.example/rules/{{slug}}.md"
33
+ },
34
+ "frontmatter": {
35
+ "required": ["description"],
36
+ "map": {
37
+ "description": "string",
38
+ "globs": "array<string>|string"
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ Field checklist:
45
+
46
+ - [ ] `id` — matches the key, kebab-case.
47
+ - [ ] `label` — human-readable name.
48
+ - [ ] `format` — you will use this string as a key in `renderers/index.mjs`.
49
+ - [ ] `installKind` — currently only `per-rule` is implemented.
50
+ - [ ] `integrationPath` — path under `integrations/<id>/`. Use `{{slug}}`.
51
+ - [ ] `detect.dirs` / `detect.files` — where the harness lives.
52
+ - [ ] `dest.project` / `dest.user` — install destinations.
53
+ - [ ] `frontmatter.required` — fields the harness cannot run without.
54
+ - [ ] `frontmatter.map` — documentation of supported fields.
55
+ - [ ] `plugin` — only if the harness needs a plugin registered.
56
+
57
+ ---
58
+
59
+ ## Step 2 — Create the renderer
60
+
61
+ Create `scripts/lib/renderers/example-agent.mjs`:
62
+
63
+ ```js
64
+ import { wrap } from './_util.mjs';
65
+
66
+ export function renderExampleAgent(rule, _harness) {
67
+ const d = rule.data;
68
+ const fm = { description: d.description };
69
+ if (d.globs !== undefined) fm.globs = d.globs;
70
+ return wrap(fm, rule.body);
71
+ }
72
+ ```
73
+
74
+ Rules for renderers:
75
+
76
+ - Pure. No `fs`, no `process`, no globals.
77
+ - Signature is `(rule, harness) => string`.
78
+ - Read only the fields your harness understands. Drop the rest silently —
79
+ other harnesses may need them.
80
+ - Use `wrap()` from `_util.mjs` for consistent frontmatter formatting.
81
+
82
+ ---
83
+
84
+ ## Step 3 — Register the renderer
85
+
86
+ Edit `scripts/lib/renderers/index.mjs`:
87
+
88
+ ```js
89
+ import { renderExampleAgent } from './example-agent.mjs';
90
+
91
+ export const renderers = {
92
+ // ...
93
+ 'md-example': renderExampleAgent,
94
+ };
95
+ ```
96
+
97
+ The key must be the exact string from `tools.json`'s `format` field.
98
+
99
+ ---
100
+
101
+ ## Step 4 — Add tests
102
+
103
+ Edit `tests/convert.test.mjs` and add a block:
104
+
105
+ ```js
106
+ import { renderExampleAgent } from '../scripts/lib/renderers/example-agent.mjs';
107
+
108
+ test('example-agent: emits description', () => {
109
+ const fm = matter(renderExampleAgent(baseRule)).data;
110
+ assert.equal(fm.description, 'Rules for TS.');
111
+ });
112
+
113
+ test('example-agent: preserves globs as-is', () => {
114
+ const fm = matter(renderExampleAgent(baseRule)).data;
115
+ assert.deepEqual(fm.globs, ['**/*.ts', '**/*.tsx']);
116
+ });
117
+ ```
118
+
119
+ Cover:
120
+
121
+ - A rule with only the required fields.
122
+ - A rule with every optional field your harness supports.
123
+ - The "body preserved verbatim" test in the shared block.
124
+
125
+ ---
126
+
127
+ ## Step 5 — Verify end-to-end
128
+
129
+ ```bash
130
+ mise run validate # schema must still pass
131
+ mise run convert # your harness should appear in the summary
132
+ ls integrations/example-agent/
133
+ mise run test # all renderer tests, including yours
134
+ mise run install --harness example-agent --scope project
135
+ ```
136
+
137
+ Inspect `integrations/example-agent/<slug>.md` and confirm the output matches
138
+ your harness's expected format.
139
+
140
+ ---
141
+
142
+ ## Step 6 — Update docs
143
+
144
+ - Add a row to the compatibility matrix in `docs/RULE-SPEC.md`.
145
+ - Add a row to the harness table in `README.md`.
146
+ - Update the "Adding a rule — checklist" in `CONTRIBUTING.md` if the new
147
+ harness changes requirements.
148
+
149
+ ---
150
+
151
+ ## Step 7 — Open a PR
152
+
153
+ Title: `feat(harness): support example-agent`
154
+
155
+ Include in the description:
156
+
157
+ - Link to the harness's documentation.
158
+ - A sample compiled output.
159
+ - Screenshot or log from `mise run install` (optional but helpful).
160
+
161
+ ---
162
+
163
+ ## Supported harnesses (current)
164
+
165
+ | Harness | `format` | `installKind` | Path |
166
+ | --- | --- | --- | --- |
167
+ | Google Antigravity | `md-yaml-trigger` | `per-rule` | `.agents/rules/*.md` |
168
+ | pi-agent | `md-rules-dir` | `per-rule` | `.pi/rules/<slug>/RULES.md` |
169
+ | oh-my-pi (omp) | `md-or-mdc` | `per-rule` | `.omp/rules/*.md` |
170
+ | OpenCode | `md-plugin` | `per-rule` | `.opencode/rules/*.md` |
@@ -0,0 +1,130 @@
1
+ # Architecture
2
+
3
+ This document explains how `agency-rules` turns one set of generic rules into
4
+ four native harness formats, and why the pipeline is shaped the way it is.
5
+
6
+ ---
7
+
8
+ ## Data flow
9
+
10
+ ```
11
+ rules/*.md tools.json
12
+ (source of truth) (contract)
13
+ │ │
14
+ │ │
15
+ ▼ ▼
16
+ ┌────────────────────────────────────────────┐
17
+ │ scripts/validate.mjs (schema + uniqueness)│
18
+ └────────────────────────────────────────────┘
19
+ │
20
+ ▼
21
+ ┌────────────────────────────────────────────┐
22
+ │ scripts/convert.mjs │
23
+ │ for each harness: │
24
+ │ render = renderers[harness.format] │
25
+ │ for each rule: │
26
+ │ write render(rule, harness) │
27
+ │ write _manifest.json │
28
+ └────────────────────────────────────────────┘
29
+ │
30
+ ▼
31
+ integrations/<harness>/...
32
+ (compiled, gitignored)
33
+ │
34
+ ▼
35
+ ┌────────────────────────────────────────────┐
36
+ │ scripts/install.mjs │
37
+ │ detect harnesses │
38
+ │ copy files to harness.dest[scope] │
39
+ │ register plugins (opencode) │
40
+ └────────────────────────────────────────────┘
41
+ │
42
+ ▼
43
+ user's harness dirs
44
+ ```
45
+
46
+ ---
47
+
48
+ ## Layers
49
+
50
+ | Layer | File(s) | Responsibility |
51
+ | --- | --- | --- |
52
+ | **Source** | `rules/**/*.md` | Generic Markdown + YAML frontmatter. |
53
+ | **Schema** | `rules/_schema.json` | JSON Schema enforcing the neutral format. |
54
+ | **Contract** | `tools.json` | Declares each harness: format, integration path, install destinations, frontmatter mapping, plugin metadata. |
55
+ | **Engine** | `scripts/convert.mjs` | Iterates over rules × harnesses, calls the right renderer, writes output + manifest. |
56
+ | **Renderers** | `scripts/lib/renderers/*.mjs` | Pure functions: `(rule, harness) → markdown string`. No I/O. |
57
+ | **Installer** | `scripts/install.mjs` | Detects harnesses, copies compiled files to real destinations, registers plugins. |
58
+ | **Validator** | `scripts/validate.mjs` | Runs the schema and checks slug uniqueness before anything else runs. |
59
+ | **Orchestration** | `mise.toml` | Pins Node version, defines tasks, composes `ci`, wires the `postinstall` hook. |
60
+
61
+ Each layer has exactly one job. Renderers never touch the filesystem; the
62
+ engine never hardcodes a harness; the installer never renders.
63
+
64
+ ---
65
+
66
+ ## Why `tools.json` is the single source of truth
67
+
68
+ Every harness-specific decision lives there:
69
+
70
+ - **Which renderer** to use (`format`).
71
+ - **What path** the output takes inside `integrations/<harness>/` (`integrationPath`).
72
+ - **Where** the file ultimately lands for `project` and `user` scopes (`dest`).
73
+ - **Which frontmatter fields** are required and how they map (`frontmatter`).
74
+ - **Whether** plugin registration is needed (`plugin`).
75
+
76
+ Adding a harness is additive: one `tools.json` entry + one renderer +
77
+ one test. The engine, installer, and validator never change.
78
+
79
+ ---
80
+
81
+ ## Determinism guarantees
82
+
83
+ - **Pure renderers.** Same input → same output byte for byte. No timestamps,
84
+ no random ordering. Manifests are sorted by slug.
85
+ - **Full reset on convert.** `integrations/<harness>/` is deleted before
86
+ being rewritten. No stale files survive a rename.
87
+ - **Manifest as index.** `integrations/<harness>/_manifest.json` records
88
+ every file the installer should copy. The installer trusts the manifest,
89
+ not a directory walk.
90
+ - **Locked toolchain.** `mise.lock` pins exact versions of Node and any
91
+ installed CLIs. CI uses `mise install --locked` to fail on drift.
92
+
93
+ ---
94
+
95
+ ## Caching
96
+
97
+ `mise run convert` declares:
98
+
99
+ ```toml
100
+ sources = ["rules/**/*.md", "tools.json", "scripts/lib/**/*.mjs"]
101
+ outputs = ["integrations/**/*"]
102
+ ```
103
+
104
+ If no source has changed since the last run, mise skips the task entirely.
105
+ This keeps the inner loop fast as the rules tree grows.
106
+
107
+ ---
108
+
109
+ ## OpenCode plugin registration
110
+
111
+ OpenCode needs `opencode-rules` registered in `opencode.json`. The installer:
112
+
113
+ 1. Reads (or creates) `opencode.json`.
114
+ 2. Ensures `config.plugin` is an array.
115
+ 3. Appends `"opencode-rules@latest"` if it isn't already present.
116
+ 4. Writes the config back with 2-space indentation.
117
+
118
+ If the file exists but is invalid JSON, the installer warns and skips rather
119
+ than overwriting — a corrupted config is never silently clobbered.
120
+
121
+ ---
122
+
123
+ ## Security considerations
124
+
125
+ - Rule slugs are validated against `^[a-z][a-z0-9-]*$` before they reach the
126
+ filesystem, preventing path traversal via `{{slug}}` substitution.
127
+ - `install.mjs` only reads from `integrations/` and writes to paths derived
128
+ from `tools.json` — it never reads arbitrary user input paths.
129
+ - Renderers do not execute rule content; they only stringify it. No `eval`,
130
+ no shell, no dynamic imports.
@@ -0,0 +1,135 @@
1
+ # Rule Specification
2
+
3
+ The neutral format for a rule: **Markdown with YAML frontmatter**.
4
+ Every rule compiles to every harness, so the frontmatter must carry enough
5
+ information to derive each harness's expectations.
6
+
7
+ ---
8
+
9
+ ## File layout
10
+
11
+ ```
12
+ rules/
13
+ ├── _schema.json
14
+ ├── <category>/
15
+ │ └── <slug>.md
16
+ ```
17
+
18
+ - `_schema.json` is the JSON Schema. It is not a rule and is skipped.
19
+ - Any file starting with `_` is skipped.
20
+ - `<slug>` should equal the frontmatter `name`.
21
+
22
+ ---
23
+
24
+ ## Frontmatter fields
25
+
26
+ | Field | Required | Type | Description |
27
+ | --- | --- | --- | --- |
28
+ | `name` | ✅ | string, `^[a-z][a-z0-9-]*$`, ≤64 | Unique identifier. Used as the slug. |
29
+ | `description` | ✅ | string, 1–1024 | **When** the rule applies (not what it says). Harnesses use this for matching. |
30
+ | `globs` | ❌ | string \| string[] | File patterns that trigger the rule. |
31
+ | `alwaysApply` | ❌ | boolean | If `true`, ignore `globs` and always apply. |
32
+ | `trigger` | ❌ | `always_on` \| `glob` \| `model_decision` \| `manual` | Explicit trigger for Antigravity. Overrides auto-derivation. |
33
+ | `match` | ❌ | `any` \| `all` | OpenCode: match any glob or all globs. |
34
+ | `tags` | ❌ | string[] | Freeform metadata. Ignored by all harnesses. |
35
+ | `tools` | ❌ | string[] | pi-agent: tools the rule provides. Preserved verbatim. |
36
+
37
+ ---
38
+
39
+ ## Body
40
+
41
+ The body is Markdown. It is copied verbatim into every compiled output.
42
+ Don't put harness-specific syntax here — anything that must differ goes in
43
+ the frontmatter and is handled by the renderer.
44
+
45
+ ---
46
+
47
+ ## Compatibility matrix
48
+
49
+ | Field | Antigravity | pi-agent | oh-my-pi | OpenCode |
50
+ | --- | --- | --- | --- | --- |
51
+ | `name` | (derived) | ✅ used | ✅ used | — |
52
+ | `description` | ✅ emitted | ✅ emitted | ✅ emitted | ✅ used for matching |
53
+ | `globs` | ✅ joined as string under `trigger: glob` | ✗ dropped | ✅ preserved | ✅ preserved |
54
+ | `alwaysApply` | ✅ → `trigger: always_on` | ✗ dropped | ✅ preserved | ✗ dropped |
55
+ | `trigger` | ✅ emitted | ✗ dropped | ✗ dropped | ✗ dropped |
56
+ | `match` | ✗ dropped | ✗ dropped | ✗ dropped | ✅ preserved |
57
+ | `tags` | ✗ dropped | ✗ dropped | ✗ dropped | ✗ dropped |
58
+ | `tools` | ✗ dropped | ✅ preserved | ✗ dropped | ✗ dropped |
59
+
60
+ "✗ dropped" means the field is not emitted in that harness's output. It does
61
+ not mean it's an error to set it — other harnesses may need it. The renderer
62
+ picks what its harness understands.
63
+
64
+ ---
65
+
66
+ ## Antigravity trigger precedence
67
+
68
+ Antigravity requires a `trigger` field. The renderer resolves it in this
69
+ order:
70
+
71
+ 1. Explicit `trigger` in the rule.
72
+ 2. `alwaysApply: true` → `always_on`.
73
+ 3. `globs` present → `glob`.
74
+ 4. Only `description` → `model_decision`.
75
+ 5. Nothing → `manual`.
76
+
77
+ When `trigger: glob`, the renderer joins the `globs` array with commas into a
78
+ single string, which is the format Antigravity's YAML parser expects.
79
+
80
+ ---
81
+
82
+ ## Examples
83
+
84
+ ### Always-on baseline
85
+
86
+ ```markdown
87
+ ---
88
+ name: coding-standards
89
+ description: Baseline rules applied to every edit regardless of file type.
90
+ alwaysApply: true
91
+ tags: [base]
92
+ ---
93
+
94
+ # Coding Standards
95
+ ...
96
+ ```
97
+
98
+ Compiles to Antigravity `trigger: always_on`, oh-my-pi `alwaysApply: true`,
99
+ and pi-agent / OpenCode with just `name` + `description`.
100
+
101
+ ### Scoped to file globs
102
+
103
+ ```markdown
104
+ ---
105
+ name: typescript
106
+ description: Mandatory rules for editing TypeScript and TSX files.
107
+ globs:
108
+ - "**/*.ts"
109
+ - "**/*.tsx"
110
+ tags: [language, typescript]
111
+ ---
112
+
113
+ # TypeScript Rules
114
+ ...
115
+ ```
116
+
117
+ Compiles to Antigravity `trigger: glob` + `globs: "**/*.ts,**/*.tsx"`,
118
+ oh-my-pi and OpenCode with the original array, pi-agent without `globs`.
119
+
120
+ ### Description-only (model decision)
121
+
122
+ ```markdown
123
+ ---
124
+ name: api-review
125
+ description: Use this rule when reviewing REST API endpoint definitions.
126
+ ---
127
+
128
+ # API Review
129
+ ...
130
+ ```
131
+
132
+ Compiles to Antigravity `trigger: model_decision`. Other harnesses get just
133
+ `name` + `description`.
134
+
135
+ ```
package/package.json CHANGED
@@ -1,6 +1,36 @@
1
1
  {
2
2
  "name": "agency-rules",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Write rules once. Run them in every harness.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Deuri Vasquez <deuriib@gmail.com>",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deuriib/agency-rules.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/deuriib/agency-rules/issues"
14
+ },
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" },
18
+ "files": [
19
+ "scripts/",
20
+ "tools.json",
21
+ "rules/_schema.json",
22
+ "docs/",
23
+ "README.md",
24
+ "LICENSE",
25
+ "CHANGELOG.md"
26
+ ],
27
+ "publishConfig": {
28
+ "access": "public",
29
+ "provenance": true
30
+ },
31
+ "devDependencies": {
32
+ "ajv": "^8.17.1",
33
+ "gray-matter": "^4.0.3",
34
+ "yaml": "^2.5.0"
35
+ }
36
+ }
@@ -0,0 +1,65 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "type": "object",
4
+ "required": [
5
+ "name",
6
+ "description"
7
+ ],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "name": {
11
+ "type": "string",
12
+ "pattern": "^[a-z][a-z0-9-]*$",
13
+ "maxLength": 64
14
+ },
15
+ "description": {
16
+ "type": "string",
17
+ "minLength": 1,
18
+ "maxLength": 1024
19
+ },
20
+ "globs": {
21
+ "oneOf": [
22
+ {
23
+ "type": "string",
24
+ "minLength": 1
25
+ },
26
+ {
27
+ "type": "array",
28
+ "items": {
29
+ "type": "string"
30
+ },
31
+ "minItems": 1
32
+ }
33
+ ]
34
+ },
35
+ "alwaysApply": {
36
+ "type": "boolean"
37
+ },
38
+ "trigger": {
39
+ "enum": [
40
+ "always_on",
41
+ "glob",
42
+ "model_decision",
43
+ "manual"
44
+ ]
45
+ },
46
+ "match": {
47
+ "enum": [
48
+ "any",
49
+ "all"
50
+ ]
51
+ },
52
+ "tags": {
53
+ "type": "array",
54
+ "items": {
55
+ "type": "string"
56
+ }
57
+ },
58
+ "tools": {
59
+ "type": "array",
60
+ "items": {
61
+ "type": "string"
62
+ }
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,106 @@
1
+ import { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { parseArgs } from 'node:util';
4
+ import { readRules } from './lib/frontmatter.mjs';
5
+ import { renderers } from './lib/renderers/index.mjs';
6
+
7
+ const { values: args } = parseArgs({
8
+ options: {
9
+ 'dry-run': { type: 'boolean', default: false },
10
+ harness: { type: 'string' },
11
+ },
12
+ });
13
+
14
+ const dryRun = args['dry-run'];
15
+ const prefix = dryRun ? ' [dry-run]' : ' ';
16
+
17
+ const RULES_DIR = process.env.RULES_DIR || 'rules';
18
+ const INTEGRATIONS_DIR = process.env.INTEGRATIONS_DIR || 'integrations';
19
+
20
+ const tools = JSON.parse(await readFile('tools.json', 'utf8'));
21
+ const rules = await readRules(RULES_DIR);
22
+
23
+ if (rules.length === 0) {
24
+ console.error(`❌ No rules found in ${RULES_DIR}`);
25
+ process.exit(1);
26
+ }
27
+
28
+ console.log(
29
+ `📚 Found ${rules.length} rule(s) in ${RULES_DIR}${dryRun ? ' (dry-run — nothing will be written)' : ''}\n`,
30
+ );
31
+
32
+ const harnessIds = args.harness ? [args.harness] : Object.keys(tools.harnesses);
33
+
34
+ for (const id of harnessIds) {
35
+ if (!tools.harnesses[id]) {
36
+ console.error(`❌ Unknown harness: ${id}`);
37
+ process.exit(1);
38
+ }
39
+ }
40
+
41
+ let totalFiles = 0;
42
+
43
+ for (const id of harnessIds) {
44
+ const harness = tools.harnesses[id];
45
+ const render = renderers[harness.format];
46
+ if (!render) {
47
+ console.error(`❌ No renderer for format "${harness.format}" (harness: ${id})`);
48
+ process.exit(1);
49
+ }
50
+
51
+ if (!harness.integrationPath) {
52
+ console.error(`❌ Missing "integrationPath" for harness "${id}" in tools.json`);
53
+ process.exit(1);
54
+ }
55
+
56
+ const harnessDir = join(INTEGRATIONS_DIR, id);
57
+ const manifest = [];
58
+
59
+ for (const rule of rules) {
60
+ let output;
61
+ try {
62
+ output = render(rule, harness);
63
+ } catch (err) {
64
+ console.error(`❌ ${id}: failed to render "${rule.slug}": ${err.message}`);
65
+ process.exit(1);
66
+ }
67
+
68
+ const relPath = harness.integrationPath.replace('{{slug}}', rule.slug);
69
+ const outPath = join(harnessDir, relPath);
70
+ manifest.push({ slug: rule.slug, path: relPath, size: output.length });
71
+ totalFiles++;
72
+
73
+ if (!dryRun) {
74
+ await mkdir(dirname(outPath), { recursive: true });
75
+ await writeFile(outPath, output);
76
+ }
77
+ }
78
+
79
+ if (!dryRun) {
80
+ await writeFile(
81
+ join(harnessDir, '_manifest.json'),
82
+ JSON.stringify(
83
+ {
84
+ harness: id,
85
+ format: harness.format,
86
+ files: manifest.map(({ slug, path }) => ({ slug, path })),
87
+ },
88
+ null,
89
+ 2,
90
+ ),
91
+ );
92
+ }
93
+
94
+ console.log(`✅ ${id.padEnd(12)} → ${manifest.length} file(s)`);
95
+ if (dryRun) {
96
+ for (const f of manifest) {
97
+ console.log(`${prefix} would write ${join(harnessDir, f.path)} (${f.size} bytes)`);
98
+ }
99
+ }
100
+ }
101
+
102
+ const summary = dryRun
103
+ ? `\n🔎 Dry-run: would write ${totalFiles} file(s) across ${harnessIds.length} harness(es). Nothing was written.`
104
+ : `\n🎉 Wrote ${totalFiles} file(s) across ${harnessIds.length} harness(es).`;
105
+
106
+ console.log(summary);
@@ -0,0 +1,142 @@
1
+ import { access, copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { parseArgs } from 'node:util';
4
+ import { detectHarnesses, expandHome, pathExists } from './lib/detect.mjs';
5
+
6
+ const { values: args } = parseArgs({
7
+ options: {
8
+ scope: { type: 'string', default: 'project' },
9
+ harness: { type: 'string' },
10
+ all: { type: 'boolean', default: false },
11
+ 'dry-run': { type: 'boolean', default: false },
12
+ },
13
+ });
14
+
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
+ const dryRun = args['dry-run'];
21
+ const tag = dryRun ? '[dry-run]' : ' ';
22
+ const log = (msg) => console.log(` ${tag} ${msg}`);
23
+
24
+ const INTEGRATIONS_DIR = process.env.INTEGRATIONS_DIR || 'integrations';
25
+ const tools = JSON.parse(await readFile('tools.json', 'utf8'));
26
+
27
+ let targets;
28
+ if (args.harness) {
29
+ if (!tools.harnesses[args.harness]) {
30
+ console.error(`❌ Unknown harness: ${args.harness}`);
31
+ process.exit(1);
32
+ }
33
+ targets = [args.harness];
34
+ } else if (args.all) {
35
+ targets = Object.keys(tools.harnesses);
36
+ } else {
37
+ targets = await detectHarnesses(tools);
38
+ if (targets.length === 0) {
39
+ console.warn(
40
+ `⚠️ No harness detected. Pass --harness <id> or --all to force.${dryRun ? '' : ''}`,
41
+ );
42
+ process.exit(0);
43
+ }
44
+ }
45
+
46
+ console.log(
47
+ `🔍 Targets: ${targets.join(', ')} (scope: ${args.scope})${dryRun ? ' — dry-run, no changes will be made' : ''}\n`,
48
+ );
49
+
50
+ let copied = 0;
51
+ let skippedHarnesses = 0;
52
+ let pluginChanges = 0;
53
+
54
+ for (const id of targets) {
55
+ const harness = tools.harnesses[id];
56
+ const srcDir = join(INTEGRATIONS_DIR, id);
57
+
58
+ if (!(await pathExists(srcDir))) {
59
+ console.warn(
60
+ `⚠️ ${id}: no compiled output at ${srcDir}, skipping. Run "mise run convert" first.`,
61
+ );
62
+ skippedHarnesses++;
63
+ continue;
64
+ }
65
+
66
+ const manifest = JSON.parse(await readFile(join(srcDir, '_manifest.json'), 'utf8'));
67
+ const destTemplate = harness.dest[args.scope];
68
+ if (!destTemplate) {
69
+ console.warn(`⚠️ ${id}: no dest for scope "${args.scope}"`);
70
+ skippedHarnesses++;
71
+ continue;
72
+ }
73
+
74
+ for (const { slug, path: relPath } of manifest.files) {
75
+ const destPath = expandHome(destTemplate.replace('{{slug}}', slug));
76
+ if (dryRun) {
77
+ log(`${id}: would copy ${relPath} → ${destPath}`);
78
+ } else {
79
+ await mkdir(dirname(destPath), { recursive: true });
80
+ await copyFile(join(srcDir, relPath), destPath);
81
+ log(`${id}: ${destPath}`);
82
+ }
83
+ copied++;
84
+ }
85
+
86
+ if (harness.plugin) {
87
+ const changed = await handlePlugin(id, harness.plugin, args.scope, dryRun, log);
88
+ if (changed) pluginChanges++;
89
+ }
90
+ }
91
+
92
+ console.log(
93
+ dryRun
94
+ ? `\n🔎 Dry-run: would copy ${copied} file(s) across ${targets.length - skippedHarnesses} harness(es).` +
95
+ (pluginChanges ? ` Would update ${pluginChanges} plugin config(s).` : '') +
96
+ ` Nothing was written.`
97
+ : `\n🎉 Installed ${copied} file(s) across ${targets.length - skippedHarnesses} harness(es).` +
98
+ (pluginChanges ? ` Updated ${pluginChanges} plugin config(s).` : ''),
99
+ );
100
+
101
+ if (dryRun && skippedHarnesses > 0) {
102
+ console.log(
103
+ `\n⚠️ ${skippedHarnesses} harness(es) skipped due to missing integrations. Run "mise run convert" first.`,
104
+ );
105
+ }
106
+
107
+ // ── Plugin registration ────────────────────────────────────────────────
108
+ async function handlePlugin(id, plugin, scope, dry, logFn) {
109
+ const configPath =
110
+ scope === 'user'
111
+ ? expandHome('~/.config/opencode/opencode.json')
112
+ : join(process.cwd(), 'opencode.json');
113
+
114
+ let config = {};
115
+ let existed = false;
116
+ if (await pathExists(configPath)) {
117
+ existed = true;
118
+ try {
119
+ config = JSON.parse(await readFile(configPath, 'utf8'));
120
+ } catch {
121
+ console.warn(`⚠️ ${id}: ${configPath} is not valid JSON, skipping plugin registration`);
122
+ return false;
123
+ }
124
+ }
125
+
126
+ config.plugin = config.plugin || [];
127
+ if (config.plugin.includes(plugin.entry)) {
128
+ logFn(`${id}: plugin "${plugin.entry}" already registered in ${configPath}`);
129
+ return false;
130
+ }
131
+
132
+ const action = existed ? 'would add' : 'would create and add';
133
+ logFn(`${id}: ${action} "${plugin.entry}" to ${configPath}`);
134
+
135
+ if (!dry) {
136
+ config.plugin.push(plugin.entry);
137
+ await mkdir(dirname(configPath), { recursive: true });
138
+ await writeFile(configPath, JSON.stringify(config, null, 2) + '\n');
139
+ }
140
+
141
+ return true;
142
+ }
@@ -0,0 +1,32 @@
1
+ import { access } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ export function expandHome(p) {
6
+ if (p === '~') return homedir();
7
+ if (p.startsWith('~/')) return join(homedir(), p.slice(2));
8
+ return p;
9
+ }
10
+
11
+ export async function pathExists(p) {
12
+ try {
13
+ await access(expandHome(p));
14
+ return true;
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+
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; }
28
+ }
29
+ if (found) detected.push(id);
30
+ }
31
+ return detected;
32
+ }
@@ -0,0 +1,37 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { join, relative, basename } from 'node:path';
3
+ import matter from 'gray-matter';
4
+
5
+ /**
6
+ * Reads every *.md file under `dir` (recursive), parses YAML frontmatter,
7
+ * and returns an array of { slug, source, data, body }.
8
+ *
9
+ * `slug` is derived from the frontmatter `name` (authoritative), with the
10
+ * filename as fallback. Files starting with `_` are skipped.
11
+ */
12
+ export async function readRules(dir) {
13
+ const entries = await readdir(dir, { recursive: true, withFileTypes: true });
14
+ const rules = [];
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;
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;
27
+
28
+ rules.push({
29
+ slug,
30
+ source: relative(dir, filePath),
31
+ data: parsed.data ?? {},
32
+ body: (parsed.content ?? '').trim(),
33
+ });
34
+ }
35
+
36
+ return rules.sort((a, b) => a.slug.localeCompare(b.slug));
37
+ }
@@ -0,0 +1,8 @@
1
+ import { stringify as yamlStringify } from 'yaml';
2
+
3
+ /** Wraps YAML frontmatter + body in a Markdown document. */
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`;
8
+ }
@@ -0,0 +1,30 @@
1
+ import { wrap } from './_util.mjs';
2
+
3
+ /**
4
+ * Antigravity expects a YAML frontmatter with a mandatory `trigger` field.
5
+ * Precedence:
6
+ * 1. explicit `trigger` in the rule
7
+ * 2. `alwaysApply: true` → always_on
8
+ * 3. `globs` present → glob
9
+ * 4. only `description` → model_decision
10
+ * 5. nothing → manual
11
+ */
12
+ export function renderAntigravity(rule, _harness) {
13
+ const d = rule.data;
14
+ let trigger = d.trigger;
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
+ }
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;
28
+
29
+ return wrap(fm, rule.body);
30
+ }
@@ -0,0 +1,12 @@
1
+ import { renderAntigravity } from './antigravity.mjs';
2
+ import { renderPiAgent } from './pi-agent.mjs';
3
+ import { renderOhMyPi } from './oh-my-pi.mjs';
4
+ import { renderOpencode } from './opencode.mjs';
5
+
6
+ /** Maps `format` from tools.json → renderer function. */
7
+ export const renderers = {
8
+ 'md-yaml-trigger': renderAntigravity,
9
+ 'md-rules-dir': renderPiAgent,
10
+ 'md-or-mdc': renderOhMyPi,
11
+ 'md-plugin': renderOpencode,
12
+ };
@@ -0,0 +1,16 @@
1
+ import { wrap } from './_util.mjs';
2
+
3
+ /**
4
+ * oh-my-pi preserves `name`, `description`, optional `globs` and
5
+ * `alwaysApply` in the frontmatter. Accepts .md or .mdc; we emit .md.
6
+ */
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);
16
+ }
@@ -0,0 +1,13 @@
1
+ import { wrap } from './_util.mjs';
2
+
3
+ /**
4
+ * OpenCode's `opencode-rules` plugin discovers .md files with a `description`
5
+ * (used for matching) and optional `globs`/`match` controls.
6
+ */
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);
13
+ }
@@ -0,0 +1,17 @@
1
+ import { wrap } from './_util.mjs';
2
+
3
+ /**
4
+ * pi-agent expects each rule in its own directory as RULES.md with a
5
+ * frontmatter carrying `name`, `description`, and optionally `tools`.
6
+ */
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);
17
+ }
@@ -0,0 +1,52 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import Ajv from 'ajv';
4
+ import { readRules } from './lib/frontmatter.mjs';
5
+
6
+ const RULES_DIR = process.env.RULES_DIR || 'rules';
7
+
8
+ const schema = JSON.parse(await readFile(join(RULES_DIR, '_schema.json'), 'utf8'));
9
+ const ajv = new Ajv({ allErrors: true, strict: false });
10
+ const validate = ajv.compile(schema);
11
+
12
+ const rules = await readRules(RULES_DIR);
13
+
14
+ if (rules.length === 0) {
15
+ console.error(`❌ No rules found in ${RULES_DIR}`);
16
+ process.exit(1);
17
+ }
18
+
19
+ const seen = new Set();
20
+ let failures = 0;
21
+
22
+ for (const rule of rules) {
23
+ const errors = [];
24
+
25
+ if (seen.has(rule.slug)) {
26
+ errors.push({ instancePath: '/name', message: `duplicate rule name "${rule.slug}"` });
27
+ }
28
+ seen.add(rule.slug);
29
+
30
+ if (!validate(rule.data)) {
31
+ for (const e of validate.errors ?? []) {
32
+ errors.push(e);
33
+ }
34
+ }
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}`);
41
+ }
42
+ } else {
43
+ console.log(`✅ ${rule.source}`);
44
+ }
45
+ }
46
+
47
+ if (failures > 0) {
48
+ console.error(`\n💥 ${failures}/${rules.length} rule(s) failed validation.`);
49
+ process.exit(1);
50
+ }
51
+
52
+ console.log(`\n🎉 ${rules.length} rule(s) validated successfully.`);
package/tools.json ADDED
@@ -0,0 +1,91 @@
1
+ {
2
+ "version": 1,
3
+ "harnesses": {
4
+ "antigravity": {
5
+ "id": "antigravity",
6
+ "label": "Google Antigravity",
7
+ "format": "md-yaml-trigger",
8
+ "installKind": "per-rule",
9
+ "integrationPath": "{{slug}}.md",
10
+ "detect": { "dirs": [".agents", "~/.gemini"], "files": ["GEMINI.md", "AGENTS.md"] },
11
+ "dest": {
12
+ "project": ".agents/rules/{{slug}}.md",
13
+ "user": "~/.gemini/config/rules/{{slug}}.md"
14
+ },
15
+ "frontmatter": {
16
+ "required": ["trigger"],
17
+ "map": {
18
+ "trigger": "always_on|glob|model_decision|manual",
19
+ "globs": "string",
20
+ "description": "string"
21
+ }
22
+ }
23
+ },
24
+ "pi-agent": {
25
+ "id": "pi-agent",
26
+ "label": "Pi Coding Agent",
27
+ "format": "md-rules-dir",
28
+ "installKind": "per-rule",
29
+ "integrationPath": "{{slug}}/RULES.md",
30
+ "detect": { "dirs": [".pi", "~/.pi"] },
31
+ "dest": {
32
+ "project": ".pi/rules/{{slug}}/RULES.md",
33
+ "user": "~/.pi/agent/rules/{{slug}}/RULES.md"
34
+ },
35
+ "frontmatter": {
36
+ "required": ["name", "description"],
37
+ "map": {
38
+ "name": "string",
39
+ "description": "string",
40
+ "tools": "array<string>"
41
+ }
42
+ }
43
+ },
44
+ "oh-my-pi": {
45
+ "id": "oh-my-pi",
46
+ "label": "oh-my-pi (omp)",
47
+ "format": "md-or-mdc",
48
+ "installKind": "per-rule",
49
+ "integrationPath": "{{slug}}.md",
50
+ "detect": { "dirs": [".omp", "~/.omp"] },
51
+ "dest": {
52
+ "project": ".omp/rules/{{slug}}.md",
53
+ "user": "~/.omp/agent/rules/{{slug}}.md"
54
+ },
55
+ "frontmatter": {
56
+ "required": ["name", "description"],
57
+ "map": {
58
+ "name": "string",
59
+ "description": "string",
60
+ "globs": "array<string>|string",
61
+ "alwaysApply": "boolean"
62
+ }
63
+ },
64
+ "extensions": [".md", ".mdc"]
65
+ },
66
+ "opencode": {
67
+ "id": "opencode",
68
+ "label": "OpenCode (via opencode-rules)",
69
+ "format": "md-plugin",
70
+ "installKind": "per-rule",
71
+ "integrationPath": "{{slug}}.md",
72
+ "detect": { "dirs": [".opencode", "~/.config/opencode"] },
73
+ "dest": {
74
+ "project": ".opencode/rules/{{slug}}.md",
75
+ "user": "~/.config/opencode/rules/{{slug}}.md"
76
+ },
77
+ "frontmatter": {
78
+ "required": ["description"],
79
+ "map": {
80
+ "description": "string",
81
+ "globs": "array<string>|string",
82
+ "match": "\"any\"|\"all\""
83
+ }
84
+ },
85
+ "plugin": {
86
+ "package": "opencode-rules",
87
+ "entry": "opencode-rules@latest"
88
+ }
89
+ }
90
+ }
91
+ }