@chorus-aidlc/chorus-pi 0.0.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.
- package/README.md +90 -0
- package/agents/chorus-code-reviewer.md +168 -0
- package/agents/chorus-proposal-reviewer.md +137 -0
- package/agents/chorus-task-reviewer.md +160 -0
- package/bin/chorus-mcp-call.sh +182 -0
- package/extensions/chorus.ts +452 -0
- package/extensions/subagent/agents.ts +180 -0
- package/extensions/subagent/index.ts +1038 -0
- package/lib/lib.ts +409 -0
- package/package.json +62 -0
- package/skills/brainstorm/SKILL.md +166 -0
- package/skills/chorus/SKILL.md +432 -0
- package/skills/chorus-cli/SKILL.md +57 -0
- package/skills/develop/SKILL.md +444 -0
- package/skills/docs/SKILL.md +68 -0
- package/skills/idea/SKILL.md +349 -0
- package/skills/openspec-aware/SKILL.md +493 -0
- package/skills/orchestrate/SKILL.md +127 -0
- package/skills/proposal/SKILL.md +386 -0
- package/skills/quick-dev/SKILL.md +197 -0
- package/skills/review/SKILL.md +363 -0
- package/skills/yolo/SKILL.md +549 -0
package/lib/lib.ts
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers extracted from the chorus-pi extension for unit testing.
|
|
3
|
+
*
|
|
4
|
+
* These functions hold no mutable state and (except for detectOpenSpec, which
|
|
5
|
+
* takes injectable fs/execSync) have no I/O — so they can be tested without a
|
|
6
|
+
* running Pi session or a live Chorus instance. The extension imports them
|
|
7
|
+
* from here; tests import the same functions.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Minimal fs surface needed by the config readers below.
|
|
14
|
+
* (detectOpenSpec already uses FsLike; keep this as the shared type.)
|
|
15
|
+
*/
|
|
16
|
+
export interface FsLike {
|
|
17
|
+
existsSync(p: string): boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Read raw file contents (utf-8). Injected so tests can stub the disk.
|
|
22
|
+
*/
|
|
23
|
+
export type ReadFileLike = (p: string) => string;
|
|
24
|
+
|
|
25
|
+
export interface ChorusConnection {
|
|
26
|
+
url: string;
|
|
27
|
+
apiKey: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse the chorus server entry out of a standard .mcp.json shape:
|
|
32
|
+
* { "mcpServers": { "chorus": { "url": "…/api/mcp",
|
|
33
|
+
* "headers": { "Authorization": "Bearer cho_…" } } } }
|
|
34
|
+
*
|
|
35
|
+
* Returns { url, apiKey } with apiKey extracted from the Authorization header
|
|
36
|
+
* (accepts both "Bearer cho_…" and a bare "cho_…"). Empty strings if absent.
|
|
37
|
+
* Pure given the raw file text — no fs dependency — so it is unit-testable.
|
|
38
|
+
*/
|
|
39
|
+
export function parseChorusServerFromMcpJson(rawJson: string): ChorusConnection {
|
|
40
|
+
if (!rawJson) return { url: "", apiKey: "" };
|
|
41
|
+
let obj: any;
|
|
42
|
+
try {
|
|
43
|
+
obj = JSON.parse(rawJson);
|
|
44
|
+
} catch {
|
|
45
|
+
return { url: "", apiKey: "" };
|
|
46
|
+
}
|
|
47
|
+
const srv = obj?.mcpServers?.chorus;
|
|
48
|
+
if (!srv || typeof srv !== "object") return { url: "", apiKey: "" };
|
|
49
|
+
const url = typeof srv.url === "string" ? srv.url : "";
|
|
50
|
+
let apiKey = "";
|
|
51
|
+
const auth = srv?.headers?.Authorization;
|
|
52
|
+
if (typeof auth === "string") {
|
|
53
|
+
if (auth.startsWith("Bearer ")) apiKey = auth.slice("Bearer ".length);
|
|
54
|
+
else if (auth.startsWith("cho_")) apiKey = auth;
|
|
55
|
+
}
|
|
56
|
+
return { url, apiKey };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the Chorus connection (url + apiKey) from the standard .mcp.json
|
|
61
|
+
* auto-discovered by pi-mcp-adapter. Searches candidate paths in order
|
|
62
|
+
* (project-root .mcp.json, then ~/.pi/agent/mcp.json), and returns the first
|
|
63
|
+
* COMPLETE chorus server entry (both url AND apiKey present). A partial entry
|
|
64
|
+
* (e.g. only url, no Authorization) is skipped so a complete global candidate
|
|
65
|
+
* is still reached — a partial project config must NOT shadow a complete
|
|
66
|
+
* ~/.pi/agent/mcp.json. Returns { "", "" } if no candidate is complete.
|
|
67
|
+
*
|
|
68
|
+
* Used as a fallback when CHORUS_URL / CHORUS_API_KEY env vars are unset,
|
|
69
|
+
* so a single .mcp.json config source covers both the MCP gateway (literal
|
|
70
|
+
* URL+Bearer) and the extension's own checkin + the OpenSpec wrapper.
|
|
71
|
+
*/
|
|
72
|
+
export function resolveChorusConfigFromMcpJson(
|
|
73
|
+
candidatePaths: string[],
|
|
74
|
+
fs: FsLike,
|
|
75
|
+
readFile: ReadFileLike,
|
|
76
|
+
): ChorusConnection {
|
|
77
|
+
for (const p of candidatePaths) {
|
|
78
|
+
if (!fs.existsSync(p)) continue;
|
|
79
|
+
const { url, apiKey } = parseChorusServerFromMcpJson(readFile(p));
|
|
80
|
+
if (url && apiKey) return { url, apiKey };
|
|
81
|
+
}
|
|
82
|
+
return { url: "", apiKey: "" };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type ExecSync = (cmd: string, opts: { stdio: "ignore" }) => void;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Matches the three Chorus reviewer agent names. These do NOT get a Chorus
|
|
89
|
+
* session — they are read-only and post a single VERDICT comment.
|
|
90
|
+
*/
|
|
91
|
+
export function isReviewerAgent(name: string): boolean {
|
|
92
|
+
return /^(chorus-proposal|chorus-task|chorus-code)-reviewer$/.test(name);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Positive classification of an agent that should get an auto-managed Chorus
|
|
97
|
+
* session + task-lifecycle (checkin/update/report/checkout) injection.
|
|
98
|
+
*
|
|
99
|
+
* Only canonical worker agent names get a session. The three Chorus reviewers
|
|
100
|
+
* are read-only (handled by isReviewerAgent) and the official subagent example's
|
|
101
|
+
* read-only agents `scout` / `planner` / `reviewer` are NOT workers — injecting
|
|
102
|
+
* the session workflow into them adds irrelevant task-lifecycle instructions and
|
|
103
|
+
* unnecessary chorus_create_session API traffic for agents that never touch a task.
|
|
104
|
+
*
|
|
105
|
+
* This is a positive allowlist (not a reviewer exclusion) so arbitrary custom
|
|
106
|
+
* read-only agents also do NOT get a session. Add more worker names here if the
|
|
107
|
+
* project introduces them.
|
|
108
|
+
*/
|
|
109
|
+
export const WORKER_AGENT_NAMES = ["worker"] as const;
|
|
110
|
+
export function isWorkerAgent(name: string): boolean {
|
|
111
|
+
return (WORKER_AGENT_NAMES as readonly string[]).includes(name);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Enumerate the (agent, task) items in an official `subagent` tool call's input,
|
|
115
|
+
* across its three modes:
|
|
116
|
+
* - single: { agent, task }
|
|
117
|
+
* - parallel: { tasks: [{ agent, task }, ...] }
|
|
118
|
+
* - chain: { chain: [{ agent, task }, ...] }
|
|
119
|
+
*
|
|
120
|
+
* Each returned holder carries the agent name, the current task text, and a
|
|
121
|
+
* `setTask` that writes back into the SAME input object in place — so the
|
|
122
|
+
* extension can inject the Chorus session workflow into a worker's task before
|
|
123
|
+
* the ephemeral child `pi` process is spawned (pi's `tool_call` event input is
|
|
124
|
+
* mutable). Holders with a non-string / empty agent or task are skipped.
|
|
125
|
+
*
|
|
126
|
+
* Replaces the old persistent-model agentId extraction: the official subagent
|
|
127
|
+
* children are ephemeral (spawn → run → exit within one tool call) and expose
|
|
128
|
+
* no `sa_<uuid>` agentId to map, so there is nothing to parse out of a result.
|
|
129
|
+
*/
|
|
130
|
+
export interface SubagentTaskItem {
|
|
131
|
+
agent: string;
|
|
132
|
+
task: string;
|
|
133
|
+
setTask: (task: string) => void;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function subagentTaskItems(input: unknown): SubagentTaskItem[] {
|
|
137
|
+
if (!input || typeof input !== "object") return [];
|
|
138
|
+
const obj = input as Record<string, unknown>;
|
|
139
|
+
const items: SubagentTaskItem[] = [];
|
|
140
|
+
const collect = (holder: Record<string, unknown>): void => {
|
|
141
|
+
const agent = typeof holder.agent === "string" ? holder.agent : "";
|
|
142
|
+
const task = typeof holder.task === "string" ? holder.task : "";
|
|
143
|
+
if (!agent || !task) return;
|
|
144
|
+
items.push({
|
|
145
|
+
agent,
|
|
146
|
+
task,
|
|
147
|
+
setTask: (t) => {
|
|
148
|
+
holder.task = t;
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
if (Array.isArray(obj.tasks)) {
|
|
153
|
+
for (const t of obj.tasks) if (t && typeof t === "object") collect(t as Record<string, unknown>);
|
|
154
|
+
} else if (Array.isArray(obj.chain)) {
|
|
155
|
+
for (const c of obj.chain) if (c && typeof c === "object") collect(c as Record<string, unknown>);
|
|
156
|
+
} else {
|
|
157
|
+
collect(obj);
|
|
158
|
+
}
|
|
159
|
+
return items;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Build the session-workflow suffix injected into a spawned worker's task
|
|
164
|
+
* (via the tool_call event's mutable input). The subprocess receives this
|
|
165
|
+
* appended to its task prompt and reads the `Session UUID` from it.
|
|
166
|
+
*/
|
|
167
|
+
export function sessionWorkflow(sessionUuid: string): string {
|
|
168
|
+
const s = sessionUuid;
|
|
169
|
+
return [
|
|
170
|
+
"",
|
|
171
|
+
"--- Chorus session (auto-injected by the chorus-pi extension) ---",
|
|
172
|
+
`Session UUID: ${s}`,
|
|
173
|
+
"For each Chorus task you work on:",
|
|
174
|
+
` 1. chorus_session_checkin_task({ sessionUuid: "${s}", taskUuid: <task-uuid> })`,
|
|
175
|
+
` 2. chorus_update_task({ taskUuid: <task-uuid>, status: "in_progress", sessionUuid: "${s}" })`,
|
|
176
|
+
" 3. ...do the work, commit...",
|
|
177
|
+
` 4. chorus_report_work({ taskUuid: <task-uuid>, report: \"...\", sessionUuid: "${s}" })`,
|
|
178
|
+
` 5. chorus_session_checkout_task({ sessionUuid: "${s}", taskUuid: <task-uuid> })`,
|
|
179
|
+
"Do NOT call chorus_create_session or chorus_close_session — the extension owns the lifecycle.",
|
|
180
|
+
].join("\n");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Resolved OpenSpec mode for a repo. `active` is the effective on/off; `reason`
|
|
185
|
+
* is a human-readable explanation; `optout` marks an explicit opt-out (so the
|
|
186
|
+
* banner does not nag); `hint` is an optional install hint when the directory
|
|
187
|
+
* exists but the CLI is missing.
|
|
188
|
+
*/
|
|
189
|
+
export interface OpenSpecState {
|
|
190
|
+
active: boolean;
|
|
191
|
+
reason: string;
|
|
192
|
+
optout: boolean;
|
|
193
|
+
hint: string;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Detect OpenSpec mode for a repo. Active only when all three hold:
|
|
198
|
+
* (1) not explicitly opted out (CHORUS_OPENSPEC_MODE != "off")
|
|
199
|
+
* (2) an openspec/ directory exists at the project root
|
|
200
|
+
* (3) the `openspec` CLI is on PATH
|
|
201
|
+
*
|
|
202
|
+
* fs and execSync are injected so tests can stub the filesystem and the CLI
|
|
203
|
+
* presence check without touching the real environment.
|
|
204
|
+
*/
|
|
205
|
+
export function detectOpenSpec(
|
|
206
|
+
cwd: string,
|
|
207
|
+
optout: boolean,
|
|
208
|
+
fs: FsLike,
|
|
209
|
+
execSync: ExecSync,
|
|
210
|
+
): OpenSpecState {
|
|
211
|
+
if (optout) {
|
|
212
|
+
return { active: false, reason: "CHORUS_OPENSPEC_MODE=off (explicit opt-out)", optout: true, hint: "" };
|
|
213
|
+
}
|
|
214
|
+
const openspecDir = `${cwd}/openspec`;
|
|
215
|
+
if (!fs.existsSync(openspecDir)) {
|
|
216
|
+
return { active: false, reason: `no openspec/ directory at ${openspecDir}`, optout: false, hint: "" };
|
|
217
|
+
}
|
|
218
|
+
let cliPresent = false;
|
|
219
|
+
try {
|
|
220
|
+
execSync("command -v openspec", { stdio: "ignore" });
|
|
221
|
+
cliPresent = true;
|
|
222
|
+
} catch {
|
|
223
|
+
cliPresent = false;
|
|
224
|
+
}
|
|
225
|
+
if (!cliPresent) {
|
|
226
|
+
return {
|
|
227
|
+
active: false,
|
|
228
|
+
reason: "openspec/ directory present but `openspec` CLI not on PATH",
|
|
229
|
+
optout: false,
|
|
230
|
+
hint: "install with: npm i -g @fission-ai/openspec",
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return { active: true, reason: "openspec/ directory + openspec CLI both present", optout: false, hint: "" };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Build the user-visible one-line startup banner (the Pi equivalent of the
|
|
238
|
+
* Claude plugin's SessionStart `systemMessage` / Codex `$chorus` toast).
|
|
239
|
+
*
|
|
240
|
+
* Mirrors the three OpenSpec states from upstream (#442):
|
|
241
|
+
* - active -> "(OpenSpec Enabled)"
|
|
242
|
+
* - explicit opt-out -> "(OpenSpec off)" [neutral, no nag]
|
|
243
|
+
* - not set up -> "(OpenSpec off — run /skill:chorus enable openspec to set it up)"
|
|
244
|
+
*
|
|
245
|
+
* Plus two non-OpenSpec states:
|
|
246
|
+
* - not configured -> warning that CHORUS_URL / CHORUS_API_KEY are missing
|
|
247
|
+
* - connection failed -> error that the checkin couldn't reach Chorus
|
|
248
|
+
*
|
|
249
|
+
* Pure (no I/O) so it can be unit-tested without a running Pi session.
|
|
250
|
+
*/
|
|
251
|
+
export interface SessionBanner {
|
|
252
|
+
message: string;
|
|
253
|
+
level: "info" | "warning" | "error";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function buildSessionBanner(args: {
|
|
257
|
+
configured: boolean;
|
|
258
|
+
connected: boolean;
|
|
259
|
+
chorusUrl: string;
|
|
260
|
+
openspec: OpenSpecState;
|
|
261
|
+
}): SessionBanner {
|
|
262
|
+
// Not configured at all — env vars missing. Warn once so the user knows
|
|
263
|
+
// the plugin loaded but is inert (Claude's hook emits the same warning).
|
|
264
|
+
if (!args.configured) {
|
|
265
|
+
return {
|
|
266
|
+
message: "Chorus plugin: not configured (set CHORUS_URL and CHORUS_API_KEY)",
|
|
267
|
+
level: "warning",
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Configured but checkin failed — the session runs but hooks are dead.
|
|
272
|
+
if (!args.connected) {
|
|
273
|
+
return {
|
|
274
|
+
message: `Chorus: connection failed (${args.chorusUrl})`,
|
|
275
|
+
level: "error",
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Connected. Append the OpenSpec status suffix.
|
|
280
|
+
let suffix: string;
|
|
281
|
+
if (args.openspec.active) {
|
|
282
|
+
suffix = "(OpenSpec Enabled)";
|
|
283
|
+
} else if (args.openspec.optout) {
|
|
284
|
+
suffix = "(OpenSpec off)";
|
|
285
|
+
} else {
|
|
286
|
+
suffix = "(OpenSpec off — run /skill:chorus enable openspec to set it up)";
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
message: `Chorus connected at ${args.chorusUrl} ${suffix}`,
|
|
290
|
+
level: "info",
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Parse CHORUS_MAX_CODE_REVIEW_ROUNDS into a non-negative integer.
|
|
296
|
+
* Mirrors the Claude plugin's `maxCodeReviewRounds` userConfig (default 3,
|
|
297
|
+
* 0 = unlimited). Empty/absent → default. Invalid (NaN, negative, non-integer)
|
|
298
|
+
* → default. Pure so it is unit-testable.
|
|
299
|
+
*/
|
|
300
|
+
export const DEFAULT_MAX_CODE_REVIEW_ROUNDS = 3;
|
|
301
|
+
export function parseMaxCodeReviewRounds(raw: string | undefined): number {
|
|
302
|
+
if (raw == null) return DEFAULT_MAX_CODE_REVIEW_ROUNDS;
|
|
303
|
+
const trimmed = raw.trim();
|
|
304
|
+
if (trimmed === "") return DEFAULT_MAX_CODE_REVIEW_ROUNDS;
|
|
305
|
+
// Use Number (not parseInt) so "3.5" or "3abc" both fall through to default
|
|
306
|
+
// instead of silently parsing the leading digits.
|
|
307
|
+
const n = Number(trimmed);
|
|
308
|
+
if (!Number.isInteger(n) || n < 0) return DEFAULT_MAX_CODE_REVIEW_ROUNDS;
|
|
309
|
+
return n;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Resolve the bundled `bin/chorus-mcp-call.sh` wrapper path.
|
|
314
|
+
*
|
|
315
|
+
* The wrapper ships inside the chorus-pi package (`bin/chorus-mcp-call.sh`,
|
|
316
|
+
* declared as a `bin` in package.json). When installed via `pi install ./packages/chorus-pi`
|
|
317
|
+
* (a local path), the script is neither linked onto PATH nor placed under
|
|
318
|
+
* `~/.pi/agent/npm/...` (those only happen for npm/git installs), so the skill's
|
|
319
|
+
* `find ~/.pi/agent/npm` fallback misses it. Instead, the extension knows its own
|
|
320
|
+
* install location and can resolve the wrapper relative to the package root.
|
|
321
|
+
*
|
|
322
|
+
* Strategy:
|
|
323
|
+
* 1. start from the extension module's own URL (import.meta.url) → its dir
|
|
324
|
+
* 2. walk up at most a few levels looking for `bin/chorus-mcp-call.sh`
|
|
325
|
+
* (handles `extensions/chorus.ts` → `..`/bin, and `dist/extensions/...` →
|
|
326
|
+
* `../../bin` if the package is ever bundled)
|
|
327
|
+
* 3. return the first existing match, or "" if none found (the skill then falls
|
|
328
|
+
* back to PATH / find)
|
|
329
|
+
*
|
|
330
|
+
* Pure given an injectable fs so it is unit-testable without a real layout.
|
|
331
|
+
*/
|
|
332
|
+
export function resolveChorusBin(extensionFileUrl: string, fs: FsLike): string {
|
|
333
|
+
if (!extensionFileUrl) return "";
|
|
334
|
+
// file: URL → filesystem path. Works for import.meta.url of a real .ts/.js file.
|
|
335
|
+
let extPath: string;
|
|
336
|
+
try {
|
|
337
|
+
extPath = extensionFileUrl.startsWith("file:")
|
|
338
|
+
? new URL(extensionFileUrl).pathname
|
|
339
|
+
: extensionFileUrl;
|
|
340
|
+
} catch {
|
|
341
|
+
return "";
|
|
342
|
+
}
|
|
343
|
+
if (!extPath) return "";
|
|
344
|
+
let dir = dirname(extPath);
|
|
345
|
+
// Walk up at most 6 levels to find <pkgRoot>/bin/chorus-mcp-call.sh.
|
|
346
|
+
for (let i = 0; i < 6; i++) {
|
|
347
|
+
const candidate = join(dir, "bin", "chorus-mcp-call.sh");
|
|
348
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
349
|
+
const parent = dirname(dir);
|
|
350
|
+
if (parent === dir) break; // reached filesystem root
|
|
351
|
+
dir = parent;
|
|
352
|
+
}
|
|
353
|
+
return "";
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* The 3 Chorus tool names that should trigger a reviewer nudge after they run.
|
|
358
|
+
* These are the BACKEND native names (no server prefix).
|
|
359
|
+
*/
|
|
360
|
+
export const NUDGE_TOOL_NAMES = [
|
|
361
|
+
"chorus_pm_submit_proposal",
|
|
362
|
+
"chorus_submit_for_verify",
|
|
363
|
+
"chorus_admin_verify_task",
|
|
364
|
+
] as const;
|
|
365
|
+
export type NudgeToolName = (typeof NUDGE_TOOL_NAMES)[number];
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Normalize a tool name seen in a pi event to the Chorus backend native name,
|
|
369
|
+
* so it can be matched against NUDGE_TOOL_NAMES regardless of how pi-mcp-adapter
|
|
370
|
+
* exposed it.
|
|
371
|
+
*
|
|
372
|
+
* Handles all three exposure modes:
|
|
373
|
+
* - gateway mode: event.toolName === "mcp", real name in event.input.tool
|
|
374
|
+
* (e.g. "chorus_chorus_submit_for_verify" — server-prefixed)
|
|
375
|
+
* - direct, toolPrefix "server": "chorus_chorus_submit_for_verify"
|
|
376
|
+
* - direct, toolPrefix "none": "chorus_submit_for_verify" (native)
|
|
377
|
+
*
|
|
378
|
+
* Strips at most one leading "chorus_" server prefix. Returns null if the input
|
|
379
|
+
* is empty or not a chorus tool.
|
|
380
|
+
*/
|
|
381
|
+
export function normalizeChorusToolName(name: string | undefined | null): string | null {
|
|
382
|
+
if (!name) return null;
|
|
383
|
+
let n = name;
|
|
384
|
+
// The chorus server name is "chorus"; the adapter prefixes it once. Strip one.
|
|
385
|
+
if (n.startsWith("chorus_chorus_")) n = n.slice("chorus_".length);
|
|
386
|
+
// Must still be a chorus tool after stripping.
|
|
387
|
+
if (!n.startsWith("chorus_")) return null;
|
|
388
|
+
return n;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Resolve the Chorus native tool name from a tool_result / tool_execution_end event,
|
|
393
|
+
* accounting for MCP gateway mode (where the real name lives in event.input.tool).
|
|
394
|
+
*
|
|
395
|
+
* Returns the native name (e.g. "chorus_submit_for_verify") or null.
|
|
396
|
+
*/
|
|
397
|
+
export function resolveChorusToolName(event: {
|
|
398
|
+
toolName: string;
|
|
399
|
+
input?: { tool?: string } | Record<string, unknown>;
|
|
400
|
+
}): string | null {
|
|
401
|
+
// Gateway mode: the agent called the `mcp` proxy tool; the real chorus tool
|
|
402
|
+
// name is in event.input.tool.
|
|
403
|
+
if (event.toolName === "mcp") {
|
|
404
|
+
const input = event.input as { tool?: string } | undefined;
|
|
405
|
+
return normalizeChorusToolName(input?.tool);
|
|
406
|
+
}
|
|
407
|
+
// Direct mode: the tool name itself is the (possibly server-prefixed) chorus name.
|
|
408
|
+
return normalizeChorusToolName(event.toolName);
|
|
409
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chorus-aidlc/chorus-pi",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Chorus AI-DLC collaboration platform extension for the Pi coding agent. Provides skills for every stage of the AI-DLC lifecycle, read-only reviewer subagents, and session-aware extension hooks. The Chorus MCP server is auto-discovered from the repo's .mcp.json by pi-mcp-adapter — no installer required.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Chorus-AIDLC"
|
|
7
|
+
},
|
|
8
|
+
"license": "AGPL-3.0",
|
|
9
|
+
"homepage": "https://github.com/Chorus-AIDLC/Chorus",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/Chorus-AIDLC/Chorus.git",
|
|
13
|
+
"directory": "packages/chorus-pi"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"chorus",
|
|
17
|
+
"ai-dlc",
|
|
18
|
+
"project-management",
|
|
19
|
+
"multi-agent",
|
|
20
|
+
"collaboration",
|
|
21
|
+
"pi-extension",
|
|
22
|
+
"pi-package"
|
|
23
|
+
],
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"main": "extensions/chorus.ts",
|
|
28
|
+
"type": "module",
|
|
29
|
+
"bin": {
|
|
30
|
+
"chorus-mcp-call": "bin/chorus-mcp-call.sh"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"extensions",
|
|
34
|
+
"lib",
|
|
35
|
+
"skills",
|
|
36
|
+
"agents",
|
|
37
|
+
"bin",
|
|
38
|
+
"README.md"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"prepublishOnly": "pnpm run check:package",
|
|
42
|
+
"check:package": "node scripts/validate-package.mjs",
|
|
43
|
+
"check:pack": "bash scripts/check-pack.sh"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"@earendil-works/pi-coding-agent": {
|
|
50
|
+
"optional": true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"pi": {
|
|
54
|
+
"extensions": [
|
|
55
|
+
"./extensions"
|
|
56
|
+
],
|
|
57
|
+
"skills": [
|
|
58
|
+
"./skills"
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
"//note": "Reviewer subagents use pi's official subagent pattern, bundled at extensions/subagent/ (index.ts + agents.ts, copied from earendil-works/pi's examples). The copied agents.ts discovers this package's own agents/*.md via a package-relative BUNDLED_DIR, so the 3 reviewer agents load with ZERO manual copy into ~/.pi/agent/agents/. pi auto-loads extensions/subagent/index.ts as a subdirectory-with-index extension (no manifest entry needed)."
|
|
62
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: brainstorm
|
|
3
|
+
description: Optional divergent-then-convergent dialogue for fuzzy ideas. Invoked from the idea skill as a prelude to structured elaboration; produces one ElaborationRound of decision-point Q&A and returns control. Never writes files, never posts comments, never resolves elaboration.
|
|
4
|
+
license: AGPL-3.0
|
|
5
|
+
metadata:
|
|
6
|
+
author: chorus
|
|
7
|
+
version: "0.17.0"
|
|
8
|
+
category: project-management
|
|
9
|
+
mcp_server: chorus
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Brainstorm Skill
|
|
13
|
+
|
|
14
|
+
A divergent-then-convergent dialogue cadence for ideas whose direction is still being formed. Compresses the conversation into one `ElaborationRound` of decision-point Q&A — same shape as a structured elaboration round, but the questions, options and answers are synthesized at the end of the conversation rather than asked up front.
|
|
15
|
+
|
|
16
|
+
This skill is a **producer** of one elaboration round; the **scheduler** decision (resolve vs. follow-up) belongs to the calling idea skill.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## When invoked
|
|
21
|
+
|
|
22
|
+
Only as a sub-step of the idea skill, only after the user has explicitly opted in via `AskUserQuestion`. Never run standalone, never run without user opt-in. The expected entry point is the idea skill's "Step 4.5: Brainstorm Mode (Optional Prelude)" — see the idea skill for the surrounding flow.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Hard rules
|
|
27
|
+
|
|
28
|
+
1. **One question at a time.** Each `AskUserQuestion` call MUST contain exactly one question entry. Wait for the answer before asking the next.
|
|
29
|
+
2. **Multi-choice preferred.** Frame each question as 2-4 options where possible. Open-ended is acceptable when options would be premature, but lean toward concrete choices.
|
|
30
|
+
3. **Propose 2-3 directions before stopping divergence.** Once the requirement direction is clear enough to enumerate, present 2-3 distinct approaches in a single `AskUserQuestion`. Mark exactly one as the recommended option per the host tool's `AskUserQuestion` recommendation convention (the spec does not dictate a specific marking format — follow the tool's documentation).
|
|
31
|
+
4. **Explicit user approval required to exit divergence.** Do NOT proceed to synthesis until the user has selected one of the proposed directions.
|
|
32
|
+
5. **No files written.** Do NOT write any markdown, design doc, scratch file, or any other file to disk. The conversation produces an `ElaborationRound` and nothing else on disk.
|
|
33
|
+
6. **No comments posted.** Do NOT call `chorus_add_comment` from this skill. Comments belong to the idea skill or the user, not to the brainstorm step.
|
|
34
|
+
7. **No design-doc handoff.** Do NOT invoke any skill whose purpose is to produce a design document or implementation plan. The brainstorm output is the synthesized round — there is no separate doc.
|
|
35
|
+
8. **No `validate_elaboration` call.** Do NOT call `chorus_pm_validate_elaboration` from this skill. Whether to resolve the elaboration or open a follow-up round (`chorus_pm_start_elaboration` again) is the calling idea skill's decision, not this skill's.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Step-by-step
|
|
40
|
+
|
|
41
|
+
### 1. Gather context
|
|
42
|
+
|
|
43
|
+
Before asking the first divergent question, read the idea and surrounding project state. Mirror the idea skill's gather-context list:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
chorus_get_idea({ ideaUuid })
|
|
47
|
+
chorus_get_documents({ projectUuid })
|
|
48
|
+
chorus_get_document({ documentUuid }) # for any document worth reading in full
|
|
49
|
+
chorus_get_proposals({ projectUuid, status: "approved" }) # to understand patterns
|
|
50
|
+
chorus_list_tasks({ projectUuid }) # to avoid duplicating existing work
|
|
51
|
+
chorus_get_comments({ targetType: "idea", targetUuid: ideaUuid })
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Skim each result for: stated background, stated requirements, stated constraints, and what is conspicuously NOT stated. The gaps are the questions worth asking.
|
|
55
|
+
|
|
56
|
+
### 2. Divergent Q&A
|
|
57
|
+
|
|
58
|
+
Ask one question at a time via `AskUserQuestion`. Aim to surface:
|
|
59
|
+
|
|
60
|
+
- The **goal** the idea is trying to serve (often more abstract than the idea statement).
|
|
61
|
+
- The **constraints** that exclude entire branches of solution space (deadlines, compatibility, scope).
|
|
62
|
+
- The **success criteria** — how will the user know this is done.
|
|
63
|
+
|
|
64
|
+
Keep each question single-purpose. If you need to ask three things, that is three rounds, not one combined `AskUserQuestion`.
|
|
65
|
+
|
|
66
|
+
### 3. Propose 2-3 directions
|
|
67
|
+
|
|
68
|
+
When the goal, constraints, and success criteria are clear enough that you can name distinct approaches, present them in a single `AskUserQuestion`:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
AskUserQuestion({
|
|
72
|
+
questions: [
|
|
73
|
+
{
|
|
74
|
+
question: "<the convergence question>",
|
|
75
|
+
header: "<short header>",
|
|
76
|
+
options: [
|
|
77
|
+
{ label: "Option A (Recommended)", description: "<what + tradeoff>" },
|
|
78
|
+
{ label: "Option B", description: "<what + tradeoff>" },
|
|
79
|
+
{ label: "Option C", description: "<what + tradeoff>" }
|
|
80
|
+
],
|
|
81
|
+
multiSelect: false
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
})
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The recommendation must be visibly marked to the user using the host tool's `AskUserQuestion` convention. State **why** you recommend it — usually a sentence about the dominant tradeoff.
|
|
88
|
+
|
|
89
|
+
### 4. Wait for explicit approval
|
|
90
|
+
|
|
91
|
+
Do not proceed to synthesis if the user has not selected one of the options. If the user picks "Other" with free text, treat that as a new constraint — go back to step 2 or step 3 with the refined direction.
|
|
92
|
+
|
|
93
|
+
### 5. Synthesize decision-point Q&A
|
|
94
|
+
|
|
95
|
+
For each material decision the user made during the conversation, build one `ElaborationQuestion`. A "material decision" is a moment where the user chose between alternatives or set scope explicitly. Map each decision per the synthesis spec below.
|
|
96
|
+
|
|
97
|
+
### 6. Persist the round
|
|
98
|
+
|
|
99
|
+
Call `chorus_pm_start_elaboration` with the synthesized questions:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
chorus_pm_start_elaboration({
|
|
103
|
+
ideaUuid,
|
|
104
|
+
depth: "standard",
|
|
105
|
+
questions: [
|
|
106
|
+
{ id: "q1", text: "...", category: "...", options: [...] },
|
|
107
|
+
...
|
|
108
|
+
]
|
|
109
|
+
})
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Then submit the answers in one call:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
chorus_answer_elaboration({
|
|
116
|
+
ideaUuid,
|
|
117
|
+
roundUuid,
|
|
118
|
+
answers: [
|
|
119
|
+
{ questionId: "q1", selectedOptionId: "...", customText: "<rationale>" },
|
|
120
|
+
...
|
|
121
|
+
]
|
|
122
|
+
})
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 7. Return control
|
|
126
|
+
|
|
127
|
+
Stop here. Do **NOT** call `chorus_pm_validate_elaboration`. The idea skill's caller now decides:
|
|
128
|
+
|
|
129
|
+
- If the synthesized round answers cover everything → caller obtains human confirmation, then resolves with `chorus_pm_validate_elaboration`.
|
|
130
|
+
- If gaps remain → caller opens a structured Round 2 by calling `chorus_pm_start_elaboration` again.
|
|
131
|
+
|
|
132
|
+
The depth of any follow-up round is the caller's call, not yours.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Synthesis spec
|
|
137
|
+
|
|
138
|
+
Each material decision becomes exactly one `ElaborationQuestion` with these fields:
|
|
139
|
+
|
|
140
|
+
| Field | Source |
|
|
141
|
+
|---|---|
|
|
142
|
+
| `text` | The decision question, phrased neutrally. Example: "Which depth-model placement?" |
|
|
143
|
+
| `category` | `functional`, `non_functional`, `business_context`, `technical_context`, `user_scenario`, or `scope` — derived from the topic. |
|
|
144
|
+
| `options` | All directions that were considered, length 2-5. Collapse near-duplicates into one option. |
|
|
145
|
+
| `selectedOptionId` | The id of the option the user approved. |
|
|
146
|
+
| `customText` | A 1-3 sentence rationale capturing the constraint or tradeoff that drove the choice. Not a transcript dump. |
|
|
147
|
+
|
|
148
|
+
Rules:
|
|
149
|
+
|
|
150
|
+
- A `customText` longer than ~3 sentences is a sign you are summarizing transcript instead of capturing rationale. Cut.
|
|
151
|
+
- An `options` array of length 2 with binary "yes / no" framing is a sign you pre-narrowed alternatives. Re-examine — there are usually at least three meaningfully different paths, even if two of them get rejected quickly.
|
|
152
|
+
- Skip "decisions" that were never genuinely contested. If the user agreed instantly to the only proposal, that is information for the idea content, not a decision-point Q&A.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Anti-patterns
|
|
157
|
+
|
|
158
|
+
Do not do any of the following. Each has a specific failure mode that this skill must prevent:
|
|
159
|
+
|
|
160
|
+
- **Single-summary `customText` blob.** Compressing the entire conversation into one ElaborationQuestion with a long markdown summary in `customText`. The schema is multi-question for a reason — preserve the decision granularity.
|
|
161
|
+
- **Transcript-as-comment.** Posting the raw conversation log as a comment on the idea (or anywhere). The synthesized round IS the artifact. Raw transcripts pollute the audit trail with noise.
|
|
162
|
+
- **File writes.** Writing any markdown, design doc, plan, or scratch file to disk. There is no design doc in this flow. The brainstorm output is the synthesized round, not an external document.
|
|
163
|
+
- **`validate_elaboration` calls.** Closing the elaboration phase from this skill. The lifecycle decision belongs to the idea skill. Calling it here strips the caller of its scheduler role.
|
|
164
|
+
- **Design-doc handoff.** Invoking any skill that produces an implementation plan or design document. The Chorus pipeline already has Proposal → Document Drafts → Task Drafts for that — the brainstorm output feeds them through ElaborationRound, not through external doc skills.
|
|
165
|
+
- **Length-2 binary "yes / no" framings.** Reducing every decision to "do this thing — yes / no". Almost always the genuine alternatives are 3+ approaches with meaningfully different tradeoffs. Length-2 framings often mean the divergent phase ended too early.
|
|
166
|
+
- **Asking multiple questions in one `AskUserQuestion`.** The cadence is one question per turn during divergence, then one final convergence question with 2-3 options. Combining unrelated questions is a sign you are rushing.
|