pi-rolecast 0.2.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/LICENSE +674 -0
- package/README.md +260 -0
- package/SKILL.md +89 -0
- package/dist/extension.d.ts +25 -0
- package/dist/extension.js +287 -0
- package/examples/rust/README.md +22 -0
- package/examples/rust/profile.yaml +51 -0
- package/package.json +71 -0
- package/references/dispatch-model-semantics.md +110 -0
- package/references/gate-runner-usage.md +19 -0
- package/references/migration-from-rust-agent-workflow.md +107 -0
- package/references/profile-schema.md +66 -0
- package/references/registry-resolution.md +14 -0
- package/references/scaffolder-usage.md +27 -0
- package/references/sync-settings-usage.md +67 -0
- package/registry/aliases.yaml +38 -0
- package/registry/built_in.yaml +42 -0
- package/requirements.txt +2 -0
- package/role-packs/coding/coding-architect.md +33 -0
- package/role-packs/coding/coding-auditor.md +31 -0
- package/role-packs/coding/coding-canary.md +31 -0
- package/role-packs/coding/coding-docs.md +32 -0
- package/role-packs/coding/coding-implementer.md +33 -0
- package/role-packs/coding/coding-mapper.md +31 -0
- package/role-packs/coding/coding-orchestrator.md +28 -0
- package/role-packs/coding/coding-planner.md +31 -0
- package/role-packs/coding/coding-profiler.md +31 -0
- package/role-packs/coding/coding-reviewer.md +32 -0
- package/role-packs/coding/coding-tester.md +32 -0
- package/scripts/gate_runner.py +155 -0
- package/scripts/install.sh +120 -0
- package/scripts/profile_loader.py +633 -0
- package/scripts/scaffolder.py +289 -0
- package/scripts/sync_settings.py +362 -0
- package/templates/blank.yaml +13 -0
- package/templates/go.yaml +44 -0
- package/templates/python.yaml +46 -0
- package/templates/rust.yaml +49 -0
- package/templates/typescript.yaml +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# pi-rolecast
|
|
2
|
+
|
|
3
|
+
Multi-agent role framework for [Pi](https://github.com/earendil-works/pi-coding-agent).
|
|
4
|
+
**Groups of specialist agents** (coding, video, ...) bound to per-role models
|
|
5
|
+
and dispatched via `@<group>-<role>` mention syntax or the `Agent` tool.
|
|
6
|
+
Profile-driven, gate-verified, language-agnostic.
|
|
7
|
+
|
|
8
|
+
A profile is one YAML file under your project (`<project>/.pi/rolecast.yaml`)
|
|
9
|
+
that decides which alias + channel serves each role and which commands the
|
|
10
|
+
gate-runner must execute. The framework owns the **configuration contract**,
|
|
11
|
+
not execution.
|
|
12
|
+
|
|
13
|
+
## v0.2.0 breaking changes
|
|
14
|
+
|
|
15
|
+
If you're coming from `pi-agent-workflow` v0.1.x, see [references/migration-from-rust-agent-workflow.md](references/migration-from-rust-agent-workflow.md). Summary:
|
|
16
|
+
|
|
17
|
+
- Package renamed: `@rootazero/pi-agent-workflow` → `pi-rolecast` (unscoped).
|
|
18
|
+
- Role names prefixed: `architect` → `coding-architect`, etc.
|
|
19
|
+
- Profile filename: `.pi/agent-workflow.yaml` → `.pi/rolecast.yaml`.
|
|
20
|
+
- Profile gains `workflow.role_groups: [coding]` field.
|
|
21
|
+
- Role source moved: `agents/` → `role-packs/<group>/`.
|
|
22
|
+
|
|
23
|
+
## Prerequisites
|
|
24
|
+
|
|
25
|
+
- macOS or Linux
|
|
26
|
+
- Python 3.10+ (3.12 is fine)
|
|
27
|
+
- Node.js 20+ (only needed to build the Pi extension from source; pre-built `dist/` ships in the npm tarball)
|
|
28
|
+
- For the install gate, a marker file for your language: `Cargo.toml`, `pyproject.toml`, `package.json + tsconfig.json`, or `go.mod`
|
|
29
|
+
- A pi-compatible chat client (or `pi` CLI) to dispatch roles
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
### Via `pi install` (recommended)
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# From a local checkout:
|
|
37
|
+
pi install /Volumes/TBU/Workspace/pi-rolecast
|
|
38
|
+
|
|
39
|
+
# From npm:
|
|
40
|
+
pi install npm:pi-rolecast
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This registers the framework as a Pi extension. After installing, run `/reload` in Pi. The extension exposes:
|
|
44
|
+
|
|
45
|
+
- **Slash commands**: `/workflow-init`, `/workflow-validate`, `/workflow-diff`, `/workflow-run`
|
|
46
|
+
- **Model-callable tools**: `scaffolder_init`, `scaffolder_validate`, `scaffolder_diff`, `gate_run`
|
|
47
|
+
- **A `session_start` hook** that notifies when no `.pi/rolecast.yaml` is present
|
|
48
|
+
|
|
49
|
+
The extension is a thin TypeScript bridge (`src/extension.ts` → `dist/extension.js`) that shells out to the Python CLI in `scripts/`. Python is the source of truth; the extension adds Pi integration on top.
|
|
50
|
+
|
|
51
|
+
### Manual install (non-Pi consumers, or fall-back)
|
|
52
|
+
|
|
53
|
+
From the framework root:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
bash scripts/install.sh
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Default prefix is `$HOME/.pi/agent/`. The installer:
|
|
60
|
+
|
|
61
|
+
1. Creates `~/.pi/agent/pi-rolecast/` → symlink to this repo.
|
|
62
|
+
2. Creates `~/.pi/agent/agents/<group>-<role>.md` → symlinks to `pi-rolecast/role-packs/<group>/<role>.md` for every role in every group. This is the location read by the **pi-subagents** extension.
|
|
63
|
+
3. Removes the legacy `~/.pi/agent/pi-agent-workflow` symlink if found (v0.1.x).
|
|
64
|
+
4. Removes any deprecated `~/.pi/agent/agent-<role>/SKILL.md` directories left over from earlier installs.
|
|
65
|
+
5. If a profile is found in the current working directory (`.pi/rolecast.yaml` or legacy `.pi/agent-workflow.yaml`), runs `sync_settings.py` which both updates `~/.pi/agent/settings.json` and writes project-local `.pi/agents/<group>-<role>.md` files with `model:` + `thinking:` frontmatter populated from your bindings — these project-local copies override the global symlinks for that project (per pi-subagents precedence).
|
|
66
|
+
6. Runs `python3 -m pip install --user -r requirements.txt` (PyYAML + pytest) unless PyYAML is already importable.
|
|
67
|
+
|
|
68
|
+
Flags:
|
|
69
|
+
|
|
70
|
+
- `--prefix DIR` — install under `DIR` instead of `~/.pi/agent/`
|
|
71
|
+
- `--framework-root DIR` — treat `DIR` as the framework root (default: parent of `scripts/`)
|
|
72
|
+
- `--no-pip` — skip the `pip install` step
|
|
73
|
+
- `--dry-run` — print what would be created without writing anything
|
|
74
|
+
- `--keep-old-layout` — skip removal of legacy `agent-<role>/SKILL.md` directories
|
|
75
|
+
|
|
76
|
+
## Configure (bootstrap a project)
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
cd <your-project>
|
|
80
|
+
python3 ~/.pi/agent/pi-rolecast/scripts/scaffolder.py init
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The scaffolder inspects your tree, picks the matching template, and writes `<project>/.pi/rolecast.yaml` with sensible defaults (workflow.role_groups, gates, bindings, escalation). For multi-language projects it lists candidates and asks you to pick one with `--template`.
|
|
84
|
+
|
|
85
|
+
Templates ship in `templates/{rust,typescript,python,go,blank}.yaml`. Each declares 11 role bindings (the `coding` group) and 2–3 gate phases (compile / lint / test).
|
|
86
|
+
|
|
87
|
+
Validate and inspect:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
python3 ~/.pi/agent/pi-rolecast/scripts/scaffolder.py validate \
|
|
91
|
+
--profile .pi/rolecast.yaml
|
|
92
|
+
|
|
93
|
+
python3 ~/.pi/agent/pi-rolecast/scripts/scaffolder.py diff \
|
|
94
|
+
--profile .pi/rolecast.yaml
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Use
|
|
98
|
+
|
|
99
|
+
### Via the Pi extension (recommended)
|
|
100
|
+
|
|
101
|
+
After `pi install`, the extension exposes:
|
|
102
|
+
|
|
103
|
+
**Slash commands:**
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
/workflow-init # scaffold .pi/rolecast.yaml
|
|
107
|
+
/workflow-validate # validate the project profile
|
|
108
|
+
/workflow-diff # check for framework schema drift
|
|
109
|
+
/workflow-run [phase] # run gate-runner; phase defaults to all
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Model-callable tools** (the LLM can call these directly):
|
|
113
|
+
|
|
114
|
+
- `scaffolder_init` — wraps `python3 scripts/scaffolder.py init`
|
|
115
|
+
- `scaffolder_validate` — wraps `python3 scripts/scaffolder.py validate`
|
|
116
|
+
- `scaffolder_diff` — wraps `python3 scripts/scaffolder.py diff`
|
|
117
|
+
- `gate_run` — wraps `python3 scripts/gate_runner.py`
|
|
118
|
+
|
|
119
|
+
**`session_start` hook** — if no `.pi/rolecast.yaml` is found in the project root, you'll see a one-time hint pointing to `/workflow-init`.
|
|
120
|
+
|
|
121
|
+
### Via the manual install (shell only)
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
python3 ~/.pi/agent/pi-rolecast/scripts/gate_runner.py \
|
|
125
|
+
--profile .pi/rolecast.yaml
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Dispatching a role
|
|
129
|
+
|
|
130
|
+
Roles use the **full prefixed name** in dispatch:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
@coding-architect design a module boundary for the auth layer
|
|
134
|
+
@coding-implementer add the new endpoint
|
|
135
|
+
@coding-reviewer review this diff
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The framework does not register `/role` slash commands. pi-subagents handles dispatch via the `@handle` mention syntax and the `Agent` tool. See [`references/dispatch-model-semantics.md`](references/dispatch-model-semantics.md) for the full mechanism.
|
|
139
|
+
|
|
140
|
+
## Customise models
|
|
141
|
+
|
|
142
|
+
Profile bindings reference **aliases** (e.g. `opus-thinking-medium`, `gpt-judgment-high`). Aliases resolve to specific models via `registry/aliases.yaml`.
|
|
143
|
+
|
|
144
|
+
To override the registry **without editing the framework**, drop a YAML file at one of two layers (deep-merge precedence, lowest first):
|
|
145
|
+
|
|
146
|
+
1. `<project>/.pi/rolecast-registry.yaml` — project-local (highest priority)
|
|
147
|
+
2. `~/.pi/rolecast/registry-overrides.yaml` — user-global
|
|
148
|
+
|
|
149
|
+
You can also override aliases at `<project>/.pi/rolecast-aliases-overrides.yaml` and `~/.pi/rolecast/aliases-overrides.yaml`.
|
|
150
|
+
|
|
151
|
+
See [references/registry-resolution.md](references/registry-resolution.md) for the full algorithm.
|
|
152
|
+
|
|
153
|
+
### Bridging profile bindings to pi dispatch
|
|
154
|
+
|
|
155
|
+
Profile bindings (alias -> model + channel) live in `.pi/rolecast.yaml`. Pi subagent dispatch reads `~/.pi/agent/settings.json` -> `subagents.agentOverrides.<group>-<role>.model`. The bridge is `scripts/sync_settings.py`:
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
python3 ~/.pi/agent/pi-rolecast/scripts/sync_settings.py --dry-run
|
|
159
|
+
python3 ~/.pi/agent/pi-rolecast/scripts/sync_settings.py --clear
|
|
160
|
+
python3 ~/.pi/agent/pi-rolecast/scripts/sync_settings.py --status # show current state vs profile bindings (no changes)
|
|
161
|
+
python3 ~/.pi/agent/pi-rolecast/scripts/sync_settings.py --list-groups # show available role groups from role-packs/
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`bash scripts/install.sh` runs sync automatically when a profile is found in cwd.
|
|
165
|
+
|
|
166
|
+
## Directory layout
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
pi-rolecast/
|
|
170
|
+
├── SKILL.md # skill doc for pi
|
|
171
|
+
├── README.md # this file
|
|
172
|
+
├── package.json # npm + Pi `pi.extensions` declaration
|
|
173
|
+
├── tsconfig.json # TypeScript build config
|
|
174
|
+
├── src/
|
|
175
|
+
│ └── extension.ts # Pi extension factory
|
|
176
|
+
├── dist/ # built TS (gitignored; shipped in npm tarball)
|
|
177
|
+
├── scripts/
|
|
178
|
+
│ ├── install.sh # framework installer
|
|
179
|
+
│ ├── profile_loader.py # load + validate a profile (v0.2.0 grouped roles)
|
|
180
|
+
│ ├── gate_runner.py # execute phases, write logs
|
|
181
|
+
│ ├── scaffolder.py # init / diff / validate
|
|
182
|
+
│ └── sync_settings.py # profile -> pi settings.json bridge
|
|
183
|
+
├── role-packs/
|
|
184
|
+
│ └── coding/ # 11 coding-specialist roles
|
|
185
|
+
│ ├── coding-architect.md
|
|
186
|
+
│ ├── coding-orchestrator.md
|
|
187
|
+
│ └── …
|
|
188
|
+
├── registry/
|
|
189
|
+
│ ├── built_in.yaml # model registry (status, channels)
|
|
190
|
+
│ └── aliases.yaml # alias → model
|
|
191
|
+
├── templates/ # scaffolder pre-fills
|
|
192
|
+
│ ├── rust.yaml
|
|
193
|
+
│ ├── typescript.yaml
|
|
194
|
+
│ ├── python.yaml
|
|
195
|
+
│ ├── go.yaml
|
|
196
|
+
│ └── blank.yaml
|
|
197
|
+
├── examples/
|
|
198
|
+
│ └── rust/ # reference profile
|
|
199
|
+
├── references/ # deep docs (one per concept)
|
|
200
|
+
│ ├── profile-schema.md
|
|
201
|
+
│ ├── registry-resolution.md
|
|
202
|
+
│ ├── gate-runner-usage.md
|
|
203
|
+
│ ├── scaffolder-usage.md
|
|
204
|
+
│ ├── sync-settings-usage.md
|
|
205
|
+
│ ├── dispatch-model-semantics.md
|
|
206
|
+
│ └── migration-from-rust-agent-workflow.md
|
|
207
|
+
└── tests/
|
|
208
|
+
├── unit/ # Python unit tests + TS extension smoke test
|
|
209
|
+
└── integration/ # install + sample-rust fixtures + dispatch PoC
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## How dispatch works
|
|
213
|
+
|
|
214
|
+
The framework does not run a custom dispatch extension itself. Instead, the **pi-subagents** extension (third-party, by `@tintinweb`, install separately: `pi install npm:@tintinweb/pi-subagents`) reads each role's `model:` + `thinking:` frontmatter and dispatches accordingly.
|
|
215
|
+
|
|
216
|
+
When `install.sh` or `sync_settings.py` runs against a project with a profile:
|
|
217
|
+
|
|
218
|
+
- Global symlinks at `~/.pi/agent/agents/<group>-<role>.md` point at the framework defaults (`deepseek-flash` everywhere).
|
|
219
|
+
- `sync_settings.py` then writes **project-local** copies at `<project>/.pi/agents/<group>-<role>.md` with `model:` + `thinking:` set from your profile bindings.
|
|
220
|
+
|
|
221
|
+
**Project-local copies win** (pi-subagents' load order: project before global), so `@coding-architect` in a project will use whatever you bound `coding-architect` to.
|
|
222
|
+
|
|
223
|
+
## Adding a new role group
|
|
224
|
+
|
|
225
|
+
1. Create `role-packs/<group>/<role>.md` for each role in the new group. Frontmatter must include `name: <group>-<role>` (hyphen-namespaced).
|
|
226
|
+
2. Add the group to `workflow.role_groups` in your profile.
|
|
227
|
+
3. Add bindings for the new full role names in `bindings:`.
|
|
228
|
+
4. Run `python3 scripts/sync_settings.py`.
|
|
229
|
+
|
|
230
|
+
See [references/migration-from-rust-agent-workflow.md](references/migration-from-rust-agent-workflow.md) for the planned future groups (video, research, design, music).
|
|
231
|
+
|
|
232
|
+
## Building from source
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
cd pi-rolecast
|
|
236
|
+
npm install
|
|
237
|
+
npm run build # tsc → dist/
|
|
238
|
+
npm test # extension smoke test
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
The `dist/` directory is gitignored; the npm tarball includes the prebuilt output.
|
|
242
|
+
|
|
243
|
+
## Reference docs
|
|
244
|
+
|
|
245
|
+
- [Profile schema](references/profile-schema.md) — full YAML spec, every field, every validation rule.
|
|
246
|
+
- [Registry resolution](references/registry-resolution.md) — alias → model + channel algorithm, override layers, status semantics.
|
|
247
|
+
- [Dispatch model semantics](references/dispatch-model-semantics.md) — `@handle` mention vs `Agent` tool vs plain text; why `provider/modelId` is required.
|
|
248
|
+
- [Gate runner usage](references/gate-runner-usage.md) — CLI, exit codes, escalation, logs.
|
|
249
|
+
- [Scaffolder usage](references/scaffolder-usage.md) — `init` / `diff` / `validate`, auto-detect, templates.
|
|
250
|
+
- [Sync settings usage](references/sync-settings-usage.md) — how profile bindings reach pi-subagents dispatch.
|
|
251
|
+
- [Migration from rust-agent-workflow](references/migration-from-rust-agent-workflow.md) — v0.1.x → v0.2.0 upgrade path.
|
|
252
|
+
|
|
253
|
+
## Related projects
|
|
254
|
+
|
|
255
|
+
- **[@tintinweb/pi-subagents](https://github.com/tintinweb/pi-subagents)** — concurrent sub-agent execution with live monitoring. pi-rolecast owns the *profile + gate + scaffolder* layer; pi-subagents owns *concurrent AgentSession dispatch*. They compose.
|
|
256
|
+
- **[Michaelliv/pi-dynamic-workflows](https://github.com/Michaelliv/pi-dynamic-workflows)** — dynamic workflow composition (different concern: workflows-as-data rather than profile-driven role bindings).
|
|
257
|
+
|
|
258
|
+
## License
|
|
259
|
+
|
|
260
|
+
MIT.
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pi-rolecast
|
|
3
|
+
description: Multi-agent role framework for Pi. Groups of specialist agents (coding, video, etc.) bound to per-role models, profile-driven, gate-verified. Use when you want a coordinated set of specialised agents with explicit model bindings per role and machine-checkable vs judgement routing.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# pi-rolecast
|
|
7
|
+
|
|
8
|
+
A language-agnostic multi-agent framework. **Profiles** bind roles to models; **gates** verify the project compiles and tests pass; a **scaffolder** bootstraps a profile in any project. v0.2.0 introduces **role groups** (currently `coding`; future: `video`, `research`, `design`, `music`) so the framework can host non-coding roles without name clashes.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
bash $SKILL_ROOT/scripts/install.sh
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Default prefix: `~/.pi/agent/`. The installer creates a symlink at `~/.pi/agent/pi-rolecast/` and one per-role symlink at `~/.pi/agent/agents/<group>-<role>.md` for every role under `role-packs/`, so pi-subagents' discovery picks them up.
|
|
17
|
+
|
|
18
|
+
## Use in a project
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
cd <your-project>
|
|
22
|
+
python3 $SKILL_ROOT/scripts/scaffolder.py init
|
|
23
|
+
# auto-detects language, pre-fills from template, writes .pi/rolecast.yaml
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Then run gates:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
python3 $SKILL_ROOT/scripts/gate_runner.py --profile .pi/rolecast.yaml
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## What this framework gives you
|
|
33
|
+
|
|
34
|
+
- **Role groups** shipped under `role-packs/<group>/`. v0.2.0 ships the `coding` group with 11 specialist roles.
|
|
35
|
+
- **Profile-driven model binding**: `bindings.<group>-<role>: { alias, channels }`. Aliases resolve via the built-in registry.
|
|
36
|
+
- **Registry overrides** at user-global (`~/.pi/rolecast/registry-overrides.yaml`) and project-local (`<project>/.pi/rolecast-registry.yaml`).
|
|
37
|
+
- **Custom roles** declared in profile; agent files live in `<project>/.pi/rolecast-agents/`.
|
|
38
|
+
- **Gate runner** with per-phase escalation (`max_attempts`, `on_permanent_failure: stop | continue`).
|
|
39
|
+
- **Scaffolder**: `init`, `diff`, `validate`.
|
|
40
|
+
- **Rust example profile** under `examples/rust/profile.yaml`.
|
|
41
|
+
|
|
42
|
+
## Dispatching agents
|
|
43
|
+
|
|
44
|
+
`sync_settings.py` writes project-local agent files at `<cwd>/.pi/agents/<group>-<role>.md`
|
|
45
|
+
with `model: provider/modelId` frontmatter. pi-subagents picks them up via its
|
|
46
|
+
standard discovery. Two paths actually dispatch a subagent (both honour the
|
|
47
|
+
binding):
|
|
48
|
+
|
|
49
|
+
- **Mention syntax** in interactive `pi`: `@coding-architect design the API` — pi-subagents
|
|
50
|
+
intercepts in the `input` event and dispatches synchronously.
|
|
51
|
+
- **Agent tool** from automation / workflows:
|
|
52
|
+
`Agent({ subagent_type: "coding-architect" })` or `SubagentWorkflow({ agentType: "coding-architect" })`.
|
|
53
|
+
|
|
54
|
+
Plain text (e.g. `/architect ...`) does NOT dispatch — it is sent to the main LLM.
|
|
55
|
+
See [Dispatch model semantics](references/dispatch-model-semantics.md) for the
|
|
56
|
+
exact mechanism, why `provider/modelId` is required, and the upstream-bug
|
|
57
|
+
caveat.
|
|
58
|
+
|
|
59
|
+
## Concepts in one paragraph
|
|
60
|
+
|
|
61
|
+
A profile is project-local. It declares gates (compile / lint / test commands), `workflow.role_groups` (which groups are enabled), bindings (which alias + channel serve each `<group>-<role>` or custom role), non-negotiables (forbidden patterns reviewed by the reviewer role), escalation policy, and optional custom roles. Aliases are framework-level names (`opus-thinking-medium`, `deepseek-verifiable`, `gpt-judgment-high`, etc.) that resolve to a specific model via `registry/aliases.yaml`, with a model having a `status` (`stable | deprecated | experimental | withdrawn`) and a list of channels it can be reached through. The framework does not own execution; it owns the configuration contract.
|
|
62
|
+
|
|
63
|
+
## Reference docs
|
|
64
|
+
|
|
65
|
+
- [Profile schema](references/profile-schema.md) — full YAML spec, every field, every validation rule, v0.1.x → v0.2.0 migration table.
|
|
66
|
+
- [Registry resolution](references/registry-resolution.md) — alias → model + channel algorithm, override layers, status semantics.
|
|
67
|
+
- [Dispatch model semantics](references/dispatch-model-semantics.md) — how `provider/modelId` reaches pi-subagents; `@handle` mention vs `Agent` tool vs plain text.
|
|
68
|
+
- [Sync settings](references/sync-settings-usage.md) — how profile bindings reach pi-subagents dispatch (project-local agent files + settings.json bridge).
|
|
69
|
+
- [Gate runner usage](references/gate-runner-usage.md) — CLI, exit codes, escalation, logs.
|
|
70
|
+
- [Scaffolder usage](references/scaffolder-usage.md) — `init` / `diff` / `validate`, auto-detect, templates.
|
|
71
|
+
- [Migration from rust-agent-workflow](references/migration-from-rust-agent-workflow.md) — v0.1.x → v0.2.0 upgrade path (package rename, role prefix, schema change).
|
|
72
|
+
|
|
73
|
+
## Role catalogue (coding group)
|
|
74
|
+
|
|
75
|
+
| Role | Output | Trigger |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| coding-orchestrator | judgement | (always-on) |
|
|
78
|
+
| coding-architect | judgement | "design", "architect" |
|
|
79
|
+
| coding-planner | verifiable | "plan" |
|
|
80
|
+
| coding-implementer | verifiable | "implement", "code" |
|
|
81
|
+
| coding-tester | verifiable | "write tests" |
|
|
82
|
+
| coding-reviewer | judgement | "review this diff" |
|
|
83
|
+
| coding-mapper | verifiable | "map", "repo map" |
|
|
84
|
+
| coding-profiler | verifiable | "profile this" |
|
|
85
|
+
| coding-auditor | judgement | "audit security" |
|
|
86
|
+
| coding-canary | meta | "is the relay real" |
|
|
87
|
+
| coding-docs | generation | "write README" |
|
|
88
|
+
|
|
89
|
+
See `role-packs/coding/<role>.md` for each role's instructions.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-rolecast — Pi extension entrypoint.
|
|
3
|
+
*
|
|
4
|
+
* Bridges the framework's Python CLI (scripts/) to Pi as model-callable
|
|
5
|
+
* tools and slash commands. The Python scripts remain the source of truth;
|
|
6
|
+
* this module only shells out.
|
|
7
|
+
*
|
|
8
|
+
* Provides:
|
|
9
|
+
* Tools (model-callable):
|
|
10
|
+
* - scaffolder_init wraps `python3 scripts/scaffolder.py init`
|
|
11
|
+
* - scaffolder_validate wraps `python3 scripts/scaffolder.py validate`
|
|
12
|
+
* - scaffolder_diff wraps `python3 scripts/scaffolder.py diff`
|
|
13
|
+
* - gate_run wraps `python3 scripts/gate_runner.py`
|
|
14
|
+
*
|
|
15
|
+
* Slash commands:
|
|
16
|
+
* - /workflow-init scaffold a project profile
|
|
17
|
+
* - /workflow-validate validate the project profile
|
|
18
|
+
* - /workflow-diff check for framework schema drift
|
|
19
|
+
* - /workflow-run run a gate phase
|
|
20
|
+
*
|
|
21
|
+
* Event hooks:
|
|
22
|
+
* - session_start: detect missing profile and notify
|
|
23
|
+
*/
|
|
24
|
+
import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
25
|
+
export default function piRolecastExtension(pi: ExtensionAPI): void;
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-rolecast — Pi extension entrypoint.
|
|
3
|
+
*
|
|
4
|
+
* Bridges the framework's Python CLI (scripts/) to Pi as model-callable
|
|
5
|
+
* tools and slash commands. The Python scripts remain the source of truth;
|
|
6
|
+
* this module only shells out.
|
|
7
|
+
*
|
|
8
|
+
* Provides:
|
|
9
|
+
* Tools (model-callable):
|
|
10
|
+
* - scaffolder_init wraps `python3 scripts/scaffolder.py init`
|
|
11
|
+
* - scaffolder_validate wraps `python3 scripts/scaffolder.py validate`
|
|
12
|
+
* - scaffolder_diff wraps `python3 scripts/scaffolder.py diff`
|
|
13
|
+
* - gate_run wraps `python3 scripts/gate_runner.py`
|
|
14
|
+
*
|
|
15
|
+
* Slash commands:
|
|
16
|
+
* - /workflow-init scaffold a project profile
|
|
17
|
+
* - /workflow-validate validate the project profile
|
|
18
|
+
* - /workflow-diff check for framework schema drift
|
|
19
|
+
* - /workflow-run run a gate phase
|
|
20
|
+
*
|
|
21
|
+
* Event hooks:
|
|
22
|
+
* - session_start: detect missing profile and notify
|
|
23
|
+
*/
|
|
24
|
+
import { execFile } from "node:child_process";
|
|
25
|
+
import { existsSync } from "node:fs";
|
|
26
|
+
import { dirname, join, resolve } from "node:path";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
28
|
+
import { promisify } from "node:util";
|
|
29
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
30
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
31
|
+
const execFileP = promisify(execFile);
|
|
32
|
+
// ---- Resolution ----
|
|
33
|
+
// dist/extension.js → ../ is the framework root.
|
|
34
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
const FRAMEWORK_ROOT = resolve(__dirname, "..");
|
|
36
|
+
const SCRIPTS = join(FRAMEWORK_ROOT, "scripts");
|
|
37
|
+
const PYTHON = process.env.PYTHON ?? "python3";
|
|
38
|
+
// v0.2.0: project profile is .pi/rolecast.yaml. Legacy .pi/agent-workflow.yaml
|
|
39
|
+
// is still recognised for one release as a deprecation aid.
|
|
40
|
+
const PROFILE_FILENAMES = ["rolecast.yaml", "agent-workflow.yaml"];
|
|
41
|
+
const PROFILE_DIR = ".pi";
|
|
42
|
+
function projectProfile(cwd) {
|
|
43
|
+
return join(cwd, PROFILE_DIR, PROFILE_FILENAMES[0]);
|
|
44
|
+
}
|
|
45
|
+
function findProjectProfile(cwd) {
|
|
46
|
+
const dir = join(cwd, PROFILE_DIR);
|
|
47
|
+
for (const name of PROFILE_FILENAMES) {
|
|
48
|
+
const p = join(dir, name);
|
|
49
|
+
if (existsSync(p))
|
|
50
|
+
return p;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
async function runPythonScript(script, args, options = {}) {
|
|
55
|
+
const cmd = PYTHON;
|
|
56
|
+
const cmdArgs = [join(SCRIPTS, script), ...args];
|
|
57
|
+
const cwd = options.cwd ?? process.cwd();
|
|
58
|
+
const timeoutMs = options.timeoutMs ?? 120_000;
|
|
59
|
+
try {
|
|
60
|
+
const { stdout, stderr } = await execFileP(cmd, cmdArgs, {
|
|
61
|
+
cwd,
|
|
62
|
+
timeout: timeoutMs,
|
|
63
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
64
|
+
});
|
|
65
|
+
return { exitCode: 0, stdout: String(stdout), stderr: String(stderr) };
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
const e = err;
|
|
69
|
+
return {
|
|
70
|
+
exitCode: typeof e.code === "number" ? e.code : 1,
|
|
71
|
+
stdout: e.stdout ?? "",
|
|
72
|
+
stderr: e.stderr ?? e.message ?? String(err),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function summarize(result, maxChars = 6000) {
|
|
77
|
+
const parts = [];
|
|
78
|
+
if (result.stdout)
|
|
79
|
+
parts.push(result.stdout.trim());
|
|
80
|
+
if (result.stderr)
|
|
81
|
+
parts.push(`[stderr]\n${result.stderr.trim()}`);
|
|
82
|
+
const combined = parts.join("\n");
|
|
83
|
+
if (combined.length <= maxChars)
|
|
84
|
+
return combined;
|
|
85
|
+
return `${combined.slice(0, maxChars)}\n... (truncated, full output in script logs)`;
|
|
86
|
+
}
|
|
87
|
+
// ---- Tool: scaffolder_init ----
|
|
88
|
+
const scaffolderInitTool = defineTool({
|
|
89
|
+
name: "scaffolder_init",
|
|
90
|
+
label: "Scaffolder Init",
|
|
91
|
+
description: [
|
|
92
|
+
"Bootstrap a pi-rolecast profile in the current project.",
|
|
93
|
+
"Auto-detects the project's language (Cargo.toml → rust, pyproject.toml → python,",
|
|
94
|
+
"package.json + tsconfig.json → typescript, go.mod → go) and writes",
|
|
95
|
+
".pi/rolecast.yaml from the matching template. Idempotent: refuses to overwrite",
|
|
96
|
+
"an existing profile unless --force is passed.",
|
|
97
|
+
].join(" "),
|
|
98
|
+
promptSnippet: "Bootstrap .pi/rolecast.yaml in the current project by auto-detecting the language and pre-filling from a template.",
|
|
99
|
+
promptGuidelines: [
|
|
100
|
+
"Use scaffolder_init when the user asks to set up a workflow profile, scaffold a project, or wants the framework to write its config file.",
|
|
101
|
+
"Do NOT use this to validate or diff an existing profile — use scaffolder_validate / scaffolder_diff for that.",
|
|
102
|
+
"If the project has multiple language markers, pass --template to pick one; otherwise the tool errors with the list of candidates.",
|
|
103
|
+
],
|
|
104
|
+
parameters: Type.Object({
|
|
105
|
+
template: Type.Optional(Type.Union([
|
|
106
|
+
Type.Literal("rust"),
|
|
107
|
+
Type.Literal("typescript"),
|
|
108
|
+
Type.Literal("python"),
|
|
109
|
+
Type.Literal("go"),
|
|
110
|
+
Type.Literal("blank"),
|
|
111
|
+
], {
|
|
112
|
+
description: "Template name. Pass this when the project has multiple language markers or to override auto-detect.",
|
|
113
|
+
})),
|
|
114
|
+
force: Type.Optional(Type.Boolean({
|
|
115
|
+
description: "Overwrite an existing .pi/rolecast.yaml if present. Default: refuse.",
|
|
116
|
+
})),
|
|
117
|
+
dry_run: Type.Optional(Type.Boolean({ description: "Print what would be created without writing." })),
|
|
118
|
+
}),
|
|
119
|
+
async execute(_id, params, _signal, _onUpdate, _ctx) {
|
|
120
|
+
const args = ["init"];
|
|
121
|
+
if (params.template)
|
|
122
|
+
args.push("--template", params.template);
|
|
123
|
+
if (params.force)
|
|
124
|
+
args.push("--force");
|
|
125
|
+
if (params.dry_run)
|
|
126
|
+
args.push("--dry-run");
|
|
127
|
+
const result = await runPythonScript("scaffolder.py", args);
|
|
128
|
+
return {
|
|
129
|
+
content: [{ type: "text", text: summarize(result) }],
|
|
130
|
+
details: { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr },
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
// ---- Tool: scaffolder_validate ----
|
|
135
|
+
const scaffolderValidateTool = defineTool({
|
|
136
|
+
name: "scaffolder_validate",
|
|
137
|
+
label: "Scaffolder Validate",
|
|
138
|
+
description: [
|
|
139
|
+
"Validate the project's .pi/rolecast.yaml profile against the framework schema.",
|
|
140
|
+
"Checks every binding resolves to a known alias + model + channel; rejects unknown roles,",
|
|
141
|
+
"trigger collisions, missing framework_version, etc. Delegates to profile_loader.load_profile.",
|
|
142
|
+
].join(" "),
|
|
143
|
+
promptSnippet: "Validate the project profile against the framework schema.",
|
|
144
|
+
promptGuidelines: [
|
|
145
|
+
"Use scaffolder_validate after editing a profile or before running gates to catch typos early.",
|
|
146
|
+
"If it reports INVALID, do NOT run gates — fix the violations first.",
|
|
147
|
+
],
|
|
148
|
+
parameters: Type.Object({
|
|
149
|
+
profile_path: Type.Optional(Type.String({
|
|
150
|
+
description: "Path to the profile YAML. Default: .pi/rolecast.yaml relative to the current working directory.",
|
|
151
|
+
})),
|
|
152
|
+
}),
|
|
153
|
+
async execute(_id, params, _signal, _onUpdate, _ctx) {
|
|
154
|
+
const args = ["validate"];
|
|
155
|
+
if (params.profile_path)
|
|
156
|
+
args.push("--profile", params.profile_path);
|
|
157
|
+
const result = await runPythonScript("scaffolder.py", args);
|
|
158
|
+
return {
|
|
159
|
+
content: [{ type: "text", text: summarize(result) }],
|
|
160
|
+
details: { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr },
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
// ---- Tool: scaffolder_diff ----
|
|
165
|
+
const scaffolderDiffTool = defineTool({
|
|
166
|
+
name: "scaffolder_diff",
|
|
167
|
+
label: "Scaffolder Diff",
|
|
168
|
+
description: [
|
|
169
|
+
"Compare the project profile's framework_version against the current framework version",
|
|
170
|
+
"and report any fields added or removed in newer framework schemas. No auto-merge —",
|
|
171
|
+
"you decide what to update.",
|
|
172
|
+
].join(" "),
|
|
173
|
+
promptSnippet: "Check whether the project profile is out of date with the current framework schema.",
|
|
174
|
+
promptGuidelines: [
|
|
175
|
+
"Use scaffolder_diff after upgrading pi-rolecast to see what changed in the schema.",
|
|
176
|
+
"The tool does NOT modify the profile — apply any updates manually.",
|
|
177
|
+
],
|
|
178
|
+
parameters: Type.Object({
|
|
179
|
+
profile_path: Type.Optional(Type.String({
|
|
180
|
+
description: "Path to the profile YAML. Default: .pi/rolecast.yaml relative to the current working directory.",
|
|
181
|
+
})),
|
|
182
|
+
}),
|
|
183
|
+
async execute(_id, params, _signal, _onUpdate, _ctx) {
|
|
184
|
+
const args = ["diff"];
|
|
185
|
+
if (params.profile_path)
|
|
186
|
+
args.push("--profile", params.profile_path);
|
|
187
|
+
const result = await runPythonScript("scaffolder.py", args);
|
|
188
|
+
return {
|
|
189
|
+
content: [{ type: "text", text: summarize(result) }],
|
|
190
|
+
details: { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr },
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
// ---- Tool: gate_run ----
|
|
195
|
+
const gateRunTool = defineTool({
|
|
196
|
+
name: "gate_run",
|
|
197
|
+
label: "Gate Run",
|
|
198
|
+
description: [
|
|
199
|
+
"Execute the project's gate-runner. Runs declared gate phases (compile / lint / test etc.)",
|
|
200
|
+
"in order, with per-phase retry + escalation per the profile.",
|
|
201
|
+
"Exit code 0 = all phases pass; 1 = a phase failed after retries; 2 = config error.",
|
|
202
|
+
"Logs land under <project>/.pi/rolecast-logs/<timestamp>/.",
|
|
203
|
+
].join(" "),
|
|
204
|
+
promptSnippet: "Run the project's gate-runner to verify compilation / lint / tests after code changes.",
|
|
205
|
+
promptGuidelines: [
|
|
206
|
+
"Use gate_run after implementation changes that should compile or pass tests.",
|
|
207
|
+
"Pass phase to run a single phase when iterating; omit to run all in order.",
|
|
208
|
+
"gate_run does NOT enforce non-negotiables — those are checked by the reviewer role at diff-review time.",
|
|
209
|
+
],
|
|
210
|
+
parameters: Type.Object({
|
|
211
|
+
profile_path: Type.Optional(Type.String({
|
|
212
|
+
description: "Path to the profile YAML. Default: .pi/rolecast.yaml relative to the current working directory.",
|
|
213
|
+
})),
|
|
214
|
+
phase: Type.Optional(Type.String({
|
|
215
|
+
description: "Phase name to run (e.g. 'compile', 'lint', 'test'). Omit to run all phases in declared order.",
|
|
216
|
+
})),
|
|
217
|
+
log_dir: Type.Optional(Type.String({
|
|
218
|
+
description: "Override the default log directory (.pi/rolecast-logs/).",
|
|
219
|
+
})),
|
|
220
|
+
}),
|
|
221
|
+
async execute(_id, params, _signal, _onUpdate, _ctx) {
|
|
222
|
+
const args = [];
|
|
223
|
+
if (params.profile_path)
|
|
224
|
+
args.push("--profile", params.profile_path);
|
|
225
|
+
if (params.phase)
|
|
226
|
+
args.push("--phase", params.phase);
|
|
227
|
+
if (params.log_dir)
|
|
228
|
+
args.push("--log-dir", params.log_dir);
|
|
229
|
+
// 10 minute ceiling so retries don't run forever in interactive sessions.
|
|
230
|
+
const result = await runPythonScript("gate_runner.py", args, { timeoutMs: 10 * 60_000 });
|
|
231
|
+
return {
|
|
232
|
+
content: [{ type: "text", text: summarize(result) }],
|
|
233
|
+
details: { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr },
|
|
234
|
+
};
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
function profileStatus(cwd) {
|
|
238
|
+
return { exists: existsSync(projectProfile(cwd)), path: projectProfile(cwd) };
|
|
239
|
+
}
|
|
240
|
+
// ---- Extension factory ----
|
|
241
|
+
export default function piRolecastExtension(pi) {
|
|
242
|
+
// Register all four tools so the model can call them.
|
|
243
|
+
pi.registerTool(scaffolderInitTool);
|
|
244
|
+
pi.registerTool(scaffolderValidateTool);
|
|
245
|
+
pi.registerTool(scaffolderDiffTool);
|
|
246
|
+
pi.registerTool(gateRunTool);
|
|
247
|
+
// Slash commands mirror the tools for direct user invocation.
|
|
248
|
+
pi.registerCommand("workflow-init", {
|
|
249
|
+
description: "Bootstrap .pi/rolecast.yaml in the current project.",
|
|
250
|
+
handler: async (args, ctx) => {
|
|
251
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
252
|
+
const pyArgs = ["init", ...parts];
|
|
253
|
+
const result = await runPythonScript("scaffolder.py", pyArgs);
|
|
254
|
+
ctx.ui.notify(summarize(result, 2000), result.exitCode === 0 ? "info" : "error");
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
pi.registerCommand("workflow-validate", {
|
|
258
|
+
description: "Validate the current project's profile.",
|
|
259
|
+
handler: async (_args, ctx) => {
|
|
260
|
+
const result = await runPythonScript("scaffolder.py", ["validate"]);
|
|
261
|
+
ctx.ui.notify(summarize(result, 2000), result.exitCode === 0 ? "info" : "error");
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
pi.registerCommand("workflow-diff", {
|
|
265
|
+
description: "Check the project profile for framework schema drift.",
|
|
266
|
+
handler: async (_args, ctx) => {
|
|
267
|
+
const result = await runPythonScript("scaffolder.py", ["diff"]);
|
|
268
|
+
ctx.ui.notify(summarize(result, 2000), result.exitCode === 0 ? "info" : "error");
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
pi.registerCommand("workflow-run", {
|
|
272
|
+
description: "Run the project's gate-runner. Usage: /workflow-run [phase]",
|
|
273
|
+
handler: async (args, ctx) => {
|
|
274
|
+
const phase = args.trim();
|
|
275
|
+
const pyArgs = phase ? ["--phase", phase] : [];
|
|
276
|
+
const result = await runPythonScript("gate_runner.py", pyArgs, { timeoutMs: 10 * 60_000 });
|
|
277
|
+
ctx.ui.notify(summarize(result, 2000), result.exitCode === 0 ? "info" : "error");
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
// Detect missing profile on session start and surface it as a one-time hint.
|
|
281
|
+
pi.on("session_start", (_event, ctx) => {
|
|
282
|
+
const status = profileStatus(ctx.cwd);
|
|
283
|
+
if (!status.exists) {
|
|
284
|
+
ctx.ui.notify(`pi-rolecast: no profile found at ${status.path}. Run /workflow-init to bootstrap one.`, "info");
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}
|