@gkzhb/pi-roles 0.2.4-beta.1

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.
@@ -0,0 +1,291 @@
1
+ import { ExtensionAPI } from '@mariozechner/pi-coding-agent';
2
+ import { AutocompleteItem } from '@mariozechner/pi-tui';
3
+ import { Static, Type } from 'typebox';
4
+
5
+ /**
6
+ * TypeBox schemas for pi-roles.
7
+ *
8
+ * These define:
9
+ * - The role frontmatter contract (parsed from YAML in `.md` files).
10
+ * - The pi-roles settings contract (read from `settings.json`).
11
+ * - The persisted "active role" state (stored via `pi.appendEntry` so it
12
+ * survives `/reload`).
13
+ *
14
+ * Everything that touches role data downstream imports from this file. If you
15
+ * need to extend a contract, change it here first.
16
+ *
17
+ * We use `typebox` (1.x), not `@sinclair/typebox` 0.34 — pi-mono migrated and
18
+ * its docs explicitly tell new extensions to depend on the `typebox` root
19
+ * package. See pi-mono CHANGELOG for the migration notes.
20
+ */
21
+
22
+ /**
23
+ * Pi's thinking levels, mirrored from `ThinkingLevel` in @mariozechner/pi-ai.
24
+ *
25
+ * We don't import the type directly because we want to validate user-supplied
26
+ * frontmatter and produce friendly error messages instead of letting Pi
27
+ * complain later.
28
+ */
29
+ declare const ThinkingLevelSchema: Type.TUnion<[Type.TLiteral<"off">, Type.TLiteral<"minimal">, Type.TLiteral<"low">, Type.TLiteral<"medium">, Type.TLiteral<"high">, Type.TLiteral<"xhigh">]>;
30
+ type ThinkingLevelValue = Static<typeof ThinkingLevelSchema>;
31
+ /**
32
+ * Intercom integration mode for a role or for the global default.
33
+ *
34
+ * - off: no intercom tool, no prompt addendum.
35
+ * - receive: targetable by other sessions, no proactive sends.
36
+ * - send: can send to other sessions, no inbound coordination expected.
37
+ * - both: full bidirectional coordination.
38
+ */
39
+ declare const IntercomModeSchema: Type.TUnion<[Type.TLiteral<"off">, Type.TLiteral<"receive">, Type.TLiteral<"send">, Type.TLiteral<"both">]>;
40
+ type IntercomMode = Static<typeof IntercomModeSchema>;
41
+ /**
42
+ * Where a discovered role came from. Used in /role list output and for
43
+ * shadowing detection. 'built-in' refers to roles bundled with the pi-roles
44
+ * package (currently only `role-assistant`).
45
+ */
46
+ declare const RoleSourceSchema: Type.TUnion<[Type.TLiteral<"project">, Type.TLiteral<"user">, Type.TLiteral<"built-in">]>;
47
+ type RoleSource = Static<typeof RoleSourceSchema>;
48
+ /**
49
+ * Role frontmatter as it appears at the top of a role .md file.
50
+ *
51
+ * Notes on the design:
52
+ *
53
+ * - `tools` is intentionally kept as a raw string here. We need to distinguish
54
+ * three states downstream:
55
+ * - field absent in YAML -> inherit from parent or keep current
56
+ * - field present but "" -> explicitly disable all tools
57
+ * - field present with -> override exactly these tools
58
+ * a value
59
+ * YAML parsers collapse `tools:` (no value) and `tools: ~` to `null`, and
60
+ * we get `undefined` when the field is missing from the object entirely.
61
+ * The schema accepts `string | null`, and `roles.ts` handles the tri-state
62
+ * semantics. Don't try to encode "absent" inside this schema — JSON Schema
63
+ * can't distinguish `undefined` from "not validated", which is why we
64
+ * handle this in code rather than in the type.
65
+ *
66
+ * - `model` is a free string here; resolution against
67
+ * `ctx.modelRegistry.find(provider, id)` happens in `apply.ts`. Keeping it
68
+ * loose lets users use either `provider/id` or just `id` syntax, matching
69
+ * Pi's `--model` flag.
70
+ *
71
+ * - `name` is required and must equal the filename without extension. We
72
+ * enforce that in `roles.ts` after parsing, not at the schema level.
73
+ */
74
+ declare const RoleFrontmatterSchema: Type.TObject<{
75
+ /** Unique identifier; must match the filename without `.md`. */
76
+ name: Type.TString;
77
+ /** One-line description shown in /role list and pickers. */
78
+ description: Type.TString;
79
+ /**
80
+ * Model identifier in `provider/id` or `id` form. Resolved against Pi's
81
+ * model registry at apply time. If the model isn't available, we warn
82
+ * and keep the session's current model.
83
+ */
84
+ model: Type.TOptional<Type.TString>;
85
+ /** Reasoning level. Clamped to model capabilities by Pi. */
86
+ thinking: Type.TOptional<Type.TUnion<[Type.TLiteral<"off">, Type.TLiteral<"minimal">, Type.TLiteral<"low">, Type.TLiteral<"medium">, Type.TLiteral<"high">, Type.TLiteral<"xhigh">]>>;
87
+ /**
88
+ * Tool list as a raw, comma-separated string. Empty string means "no
89
+ * tools". Use the `mcp:server-name` syntax for MCP tools (requires
90
+ * pi-mcp-adapter at runtime). Parse and tri-state semantics live in
91
+ * `roles.ts`. We accept `null` because YAML's `tools:` (no value)
92
+ * deserializes to that.
93
+ */
94
+ tools: Type.TOptional<Type.TUnion<[Type.TString, Type.TNull]>>;
95
+ /** Per-role intercom mode override. Falls back to global `intercomMode`. */
96
+ intercom: Type.TOptional<Type.TUnion<[Type.TLiteral<"off">, Type.TLiteral<"receive">, Type.TLiteral<"send">, Type.TLiteral<"both">]>>;
97
+ /**
98
+ * Name of a parent role to inherit from. Resolved against the same scope
99
+ * the child was loaded from, then user, then built-in. Cycles are a hard
100
+ * error.
101
+ */
102
+ extends: Type.TOptional<Type.TString>;
103
+ }>;
104
+ type RoleFrontmatter = Static<typeof RoleFrontmatterSchema>;
105
+ /**
106
+ * The `tools` field after tri-state normalization. This is what apply.ts
107
+ * actually consumes.
108
+ *
109
+ * - `{ kind: "inherit" }` -> field absent in frontmatter; do nothing on
110
+ * apply unless an `extends` chain provides a different value.
111
+ * - `{ kind: "set", names: [] }` -> explicitly empty; pass [] to setActiveTools.
112
+ * - `{ kind: "set", names: [...] }` -> explicit list; pass through to
113
+ * setActiveTools (after stripping mcp:* entries when pi-mcp-adapter is not
114
+ * installed).
115
+ */
116
+ type ToolsDirective = {
117
+ kind: "inherit";
118
+ } | {
119
+ kind: "set";
120
+ names: string[];
121
+ };
122
+ /**
123
+ * A fully-resolved role: frontmatter + body, with `extends` chain merged in.
124
+ *
125
+ * `RawRole` is what we get from disk before merging. `ResolvedRole` is what
126
+ * apply.ts consumes. The conversion happens in `roles.ts`.
127
+ */
128
+ interface RawRole {
129
+ /** Where the role was loaded from. */
130
+ source: RoleSource;
131
+ /** Absolute path to the .md file (or a synthetic path for built-in roles). */
132
+ path: string;
133
+ /** Parsed frontmatter, validated against RoleFrontmatterSchema. */
134
+ frontmatter: RoleFrontmatter;
135
+ /** Markdown body — the system prompt. */
136
+ body: string;
137
+ }
138
+ interface ResolvedRole {
139
+ /** Final name (always equals frontmatter.name). */
140
+ name: string;
141
+ /** Final description. */
142
+ description: string;
143
+ /** Final model id, or undefined to keep current. */
144
+ model?: string;
145
+ /** Final thinking level, or undefined to keep current. */
146
+ thinking?: ThinkingLevelValue;
147
+ /** Final tools directive after merging the extends chain. */
148
+ tools: ToolsDirective;
149
+ /** Final intercom mode, or undefined to fall back to global. */
150
+ intercom?: IntercomMode;
151
+ /** Final system prompt body (parent body prepended to child body when extending). */
152
+ body: string;
153
+ /** Source of the leaf role file (always the file the user requested by name). */
154
+ source: RoleSource;
155
+ /** Path of the leaf role file. */
156
+ path: string;
157
+ /** Names of all parent roles in resolution order, leaf-first. Useful for diagnostics. */
158
+ extendsChain: string[];
159
+ }
160
+ /**
161
+ * The pi-roles section of Pi's settings.json. Project settings beat global
162
+ * per Pi's standard precedence.
163
+ *
164
+ * All fields are optional; unset fields fall back to the documented defaults
165
+ * (see README "Settings reference"). The schema is permissive so that
166
+ * settings written by future versions don't break older code.
167
+ */
168
+ declare const PiRolesSettingsSchema: Type.TObject<{
169
+ /** "user" | "project" | "both". Default: "both". */
170
+ roleScope: Type.TOptional<Type.TUnion<[Type.TLiteral<"user">, Type.TLiteral<"project">, Type.TLiteral<"both">]>>;
171
+ /**
172
+ * Default role name applied when no --role / PI_ROLE is supplied.
173
+ * Default: "role-assistant" (the built-in fallback).
174
+ * If set to a missing role, we warn and use the built-in role-assistant.
175
+ */
176
+ defaultRole: Type.TOptional<Type.TString>;
177
+ /**
178
+ * Keep the currently active role when creating a normal new conversation.
179
+ * Default: false, so new conversations resolve defaultRole as usual.
180
+ * Does not apply to --reset, reload, resume, or process restarts.
181
+ */
182
+ preserveRoleOnNewSession: Type.TOptional<Type.TBoolean>;
183
+ /** Default intercom mode for roles that don't set `intercom:`. Default: "off". */
184
+ intercomMode: Type.TOptional<Type.TUnion<[Type.TLiteral<"off">, Type.TLiteral<"receive">, Type.TLiteral<"send">, Type.TLiteral<"both">]>>;
185
+ /**
186
+ * Model used to summarize the first user message into the session-name
187
+ * intent. Default: a small/cheap model when one is available, falling
188
+ * back to the session's current model.
189
+ */
190
+ titleModel: Type.TOptional<Type.TString>;
191
+ /** Whether to surface a warning when an mcp:* tool can't be resolved. Default: true. */
192
+ warnOnMissingMcp: Type.TOptional<Type.TBoolean>;
193
+ }>;
194
+ type PiRolesSettings = Static<typeof PiRolesSettingsSchema>;
195
+
196
+ /**
197
+ * pi-roles extension entry point.
198
+ *
199
+ * Wires together discovery (roles.ts), application (apply.ts), and settings
200
+ * (settings.ts) into the three Pi integration points the role lifecycle
201
+ * actually needs:
202
+ *
203
+ * - `session_start` — restore from persisted state on reload/resume,
204
+ * otherwise resolve a role name from the precedence chain (pendingReset
205
+ * > --role > PI_ROLE > settings.defaultRole > built-in role-assistant),
206
+ * then apply it. When `preserveRoleOnNewSession` is enabled, a normal new
207
+ * session instead retains the role currently active in this extension
208
+ * instance.
209
+ * - `before_agent_start` — re-inject the active role's body as the system
210
+ * prompt every turn (Pi rebuilds the prompt per turn; this is the
211
+ * stable hook).
212
+ * - `/role` command — list, current, reload, switch (with optional
213
+ * --reset to clear history first).
214
+ *
215
+ * The module-scoped state below is the source of truth for "what role is
216
+ * live in this extension instance". Pi reloads spin up a fresh module, at
217
+ * which point we restore from the most recent `pi-roles:active-role` entry
218
+ * in the session log.
219
+ */
220
+
221
+ interface RuntimeState {
222
+ /** Live role applied to this session, or null before first apply. */
223
+ activeRole: ResolvedRole | null;
224
+ /** Set by `/role <name> --reset` so the next session_start (reason="new") applies it. */
225
+ pendingRoleAfterReset: string | null;
226
+ /** Cached discovery result; refreshed on session_start, every `/role` invocation, and `/role reload`. */
227
+ roles: RawRole[];
228
+ /** Shadowed roles found at lower-precedence scopes; shown in `/role list`. */
229
+ shadowed: {
230
+ name: string;
231
+ source: string;
232
+ path: string;
233
+ }[];
234
+ /** Cached settings for the current cwd; refreshed on session_start. */
235
+ settings: PiRolesSettings;
236
+ /** Carried across role swaps so the session-name intent survives a role change. */
237
+ intent: string | undefined;
238
+ /**
239
+ * True while a title-generation request is in flight. Prevents
240
+ * `before_agent_start` from spawning a second concurrent summarization
241
+ * if it fires again before the first resolves. Reset to false in
242
+ * `generateAndApplyTitle`'s finally block.
243
+ */
244
+ titleInFlight: boolean;
245
+ /**
246
+ * True after we've shown the one-time title-generation error hint.
247
+ * Prevents spamming the user on every prompt when the title model
248
+ * is misconfigured or lacks credentials.
249
+ */
250
+ titleErrorShown: boolean;
251
+ }
252
+ declare function export_default(pi: ExtensionAPI): void;
253
+ /**
254
+ * Build the replacement system prompt for the current active role.
255
+ *
256
+ * Returns `undefined` when there's no active role (Pi keeps its default for
257
+ * that turn). Otherwise returns `{ systemPrompt }` with the role body — and,
258
+ * when intercom is requested AND the intercom tool is registered, a small
259
+ * mode-specific addendum appended to the body.
260
+ *
261
+ * Exported for unit tests; the handler in `before_agent_start` is a one-line
262
+ * delegation.
263
+ */
264
+ declare function composeSystemPrompt(state: Pick<RuntimeState, "activeRole" | "settings">, pi: Pick<ExtensionAPI, "getAllTools" | "getSessionName">): {
265
+ systemPrompt: string;
266
+ } | undefined;
267
+ /**
268
+ * Pick the role for an ordinary new conversation. When configured, retain the
269
+ * role active in this extension instance; otherwise use normal initial-role
270
+ * resolution. An explicit --reset role is handled by session_start before
271
+ * this helper is reached.
272
+ */
273
+ declare function pickNewSessionRoleName(activeRole: Pick<ResolvedRole, "name"> | null, pi: ExtensionAPI, settings: PiRolesSettings, roles: RawRole[]): string;
274
+ /**
275
+ * Pick the role to launch with on a fresh session_start (no pendingReset, no
276
+ * persisted state to restore). Precedence per BUILD-STATUS.md:
277
+ *
278
+ * --role flag > PI_ROLE env > settings.defaultRole > built-in role-assistant
279
+ *
280
+ * If a configured `defaultRole` doesn't exist, we fall through to the
281
+ * built-in rather than failing — a missing role shouldn't lock the user out
282
+ * of the session.
283
+ */
284
+ declare function pickInitialRoleName(pi: ExtensionAPI, settings: PiRolesSettings, roles: RawRole[]): string;
285
+ /**
286
+ * Provide tab completions for `/role <here>`. Combines built-in subcommands
287
+ * with discovered role names; case-insensitive prefix match.
288
+ */
289
+ declare function roleCompletions(prefix: string, roles: RawRole[]): AutocompleteItem[] | null;
290
+
291
+ export { composeSystemPrompt, export_default as default, pickInitialRoleName, pickNewSessionRoleName, roleCompletions };