agency-rules 0.0.0-stage → 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 +77 -0
- package/LICENSE +21 -0
- package/README.md +144 -2
- package/docs/ADDING-A-HARNESS.md +170 -0
- package/docs/ARCHITECTURE.md +130 -0
- package/docs/RULE-SPEC.md +135 -0
- package/package.json +44 -4
- package/rules/_schema.json +65 -0
- package/scripts/convert.mjs +106 -0
- package/scripts/install.mjs +187 -0
- package/scripts/lib/detect.mjs +35 -0
- package/scripts/lib/frontmatter.mjs +37 -0
- package/scripts/lib/renderers/_util.mjs +8 -0
- package/scripts/lib/renderers/antigravity.mjs +30 -0
- package/scripts/lib/renderers/index.mjs +12 -0
- package/scripts/lib/renderers/oh-my-pi.mjs +16 -0
- package/scripts/lib/renderers/opencode.mjs +13 -0
- package/scripts/lib/renderers/pi-agent.mjs +17 -0
- package/scripts/validate.mjs +52 -0
- package/tools.json +91 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
## [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
|
+
|
|
51
|
+
## [Unreleased]
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- Initial scaffold.
|
|
56
|
+
- Conversion engine driven by `tools.json`.
|
|
57
|
+
- Renderers for **Google Antigravity**, **pi-agent**, **oh-my-pi (omp)**,
|
|
58
|
+
and **OpenCode** (via `opencode-rules`).
|
|
59
|
+
- `mise.toml` with toolchain (`node@22`), tasks (`validate`, `convert`,
|
|
60
|
+
`install`, `test`, `ci`), and post-install hook.
|
|
61
|
+
- Test suites for frontmatter parsing and every renderer.
|
|
62
|
+
- Documentation: architecture, rule spec, harness extension guide.
|
|
63
|
+
|
|
64
|
+
### Changed
|
|
65
|
+
|
|
66
|
+
- N/A
|
|
67
|
+
|
|
68
|
+
### Fixed
|
|
69
|
+
|
|
70
|
+
- N/A
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## [0.1.0] — TBD
|
|
75
|
+
|
|
76
|
+
First tagged release. Reserved for the v0.1.0 cut once the harness set is
|
|
77
|
+
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
|
-
#
|
|
1
|
+
# agency-rules
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
```
|