@femtomc/mu-agent 26.2.33

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,424 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { createInterface } from "node:readline";
4
+ import { z } from "zod";
5
+ import { CommandContextResolver } from "./command_context.js";
6
+ const SAFE_RESPONSE_RE = /^[\s\S]{1,2000}$/;
7
+ export const MetaApprovedCommandSchema = z.discriminatedUnion("kind", [
8
+ z.object({ kind: z.literal("status") }),
9
+ z.object({ kind: z.literal("ready") }),
10
+ z.object({ kind: z.literal("issue_list") }),
11
+ z.object({ kind: z.literal("issue_get"), issue_id: z.string().trim().min(1).optional() }),
12
+ z.object({
13
+ kind: z.literal("forum_read"),
14
+ topic: z.string().trim().min(1).optional(),
15
+ limit: z.number().int().min(1).max(500).optional(),
16
+ }),
17
+ z.object({
18
+ kind: z.literal("run_resume"),
19
+ root_issue_id: z.string().trim().min(1).optional(),
20
+ max_steps: z.number().int().min(1).max(500).optional(),
21
+ }),
22
+ z.object({
23
+ kind: z.literal("run_start"),
24
+ prompt: z.string().trim().min(1),
25
+ max_steps: z.number().int().min(1).max(500).optional(),
26
+ }),
27
+ ]);
28
+ export const MetaAgentBackendTurnResultSchema = z.discriminatedUnion("kind", [
29
+ z.object({ kind: z.literal("respond"), message: z.string().trim().min(1).max(2000) }),
30
+ z.object({ kind: z.literal("command"), command: MetaApprovedCommandSchema }),
31
+ ]);
32
+ function splitPromptIntoTokens(prompt) {
33
+ return prompt
34
+ .split(/\s+/)
35
+ .map((token) => token.trim())
36
+ .filter((token) => token.length > 0);
37
+ }
38
+ function normalizeArg(arg) {
39
+ return arg.trim();
40
+ }
41
+ export class ApprovedCommandBroker {
42
+ #contextResolver;
43
+ #runTriggersEnabled;
44
+ constructor(opts = {}) {
45
+ this.#contextResolver = opts.contextResolver ?? new CommandContextResolver();
46
+ this.#runTriggersEnabled = opts.runTriggersEnabled ?? true;
47
+ }
48
+ approve(opts) {
49
+ let commandKey;
50
+ let args;
51
+ switch (opts.proposal.kind) {
52
+ case "status":
53
+ commandKey = "status";
54
+ args = [];
55
+ break;
56
+ case "ready":
57
+ commandKey = "ready";
58
+ args = [];
59
+ break;
60
+ case "issue_list":
61
+ commandKey = "issue list";
62
+ args = [];
63
+ break;
64
+ case "issue_get":
65
+ commandKey = "issue get";
66
+ args = opts.proposal.issue_id ? [normalizeArg(opts.proposal.issue_id)] : [];
67
+ break;
68
+ case "forum_read": {
69
+ commandKey = "forum read";
70
+ args = [];
71
+ if (opts.proposal.topic) {
72
+ args.push(normalizeArg(opts.proposal.topic));
73
+ }
74
+ if (opts.proposal.limit != null) {
75
+ args.push(String(Math.trunc(opts.proposal.limit)));
76
+ }
77
+ break;
78
+ }
79
+ case "run_resume": {
80
+ if (!this.#runTriggersEnabled) {
81
+ return { kind: "reject", reason: "meta_agent_action_disallowed", details: "run triggers disabled" };
82
+ }
83
+ commandKey = "run resume";
84
+ args = [];
85
+ if (opts.proposal.root_issue_id) {
86
+ args.push(normalizeArg(opts.proposal.root_issue_id));
87
+ }
88
+ if (opts.proposal.max_steps != null) {
89
+ args.push(String(Math.trunc(opts.proposal.max_steps)));
90
+ }
91
+ break;
92
+ }
93
+ case "run_start": {
94
+ if (!this.#runTriggersEnabled) {
95
+ return { kind: "reject", reason: "meta_agent_action_disallowed", details: "run triggers disabled" };
96
+ }
97
+ commandKey = "run start";
98
+ args = splitPromptIntoTokens(opts.proposal.prompt);
99
+ break;
100
+ }
101
+ default:
102
+ return { kind: "reject", reason: "meta_agent_action_disallowed" };
103
+ }
104
+ const resolved = this.#contextResolver.resolve({
105
+ repoRoot: opts.inbound.repo_root,
106
+ commandKey,
107
+ args,
108
+ inboundTargetType: opts.inbound.target_type,
109
+ inboundTargetId: opts.inbound.target_id,
110
+ metadata: opts.inbound.metadata,
111
+ });
112
+ if (resolved.kind === "reject") {
113
+ return {
114
+ kind: "reject",
115
+ reason: resolved.reason,
116
+ details: resolved.details,
117
+ };
118
+ }
119
+ return {
120
+ kind: "approved",
121
+ commandText: `/mu ${resolved.normalizedText}`,
122
+ };
123
+ }
124
+ }
125
+ function defaultSessionId() {
126
+ return `meta-${randomUUID()}`;
127
+ }
128
+ function defaultTurnId() {
129
+ return `turn-${randomUUID()}`;
130
+ }
131
+ function conversationKey(inbound, binding) {
132
+ return `${inbound.channel}:${inbound.channel_tenant_id}:${inbound.channel_conversation_id}:${binding.binding_id}`;
133
+ }
134
+ export class MessagingMetaAgentRuntime {
135
+ #backend;
136
+ #broker;
137
+ #enabled;
138
+ #enabledChannels;
139
+ #sessionIdFactory;
140
+ #turnIdFactory;
141
+ #sessionByConversation = new Map();
142
+ constructor(opts) {
143
+ this.#backend = opts.backend;
144
+ this.#broker = opts.broker ?? new ApprovedCommandBroker();
145
+ this.#enabled = opts.enabled ?? true;
146
+ this.#enabledChannels = opts.enabledChannels ? new Set(opts.enabledChannels.map((v) => v.toLowerCase())) : null;
147
+ this.#sessionIdFactory = opts.sessionIdFactory ?? defaultSessionId;
148
+ this.#turnIdFactory = opts.turnIdFactory ?? defaultTurnId;
149
+ }
150
+ #resolveSessionId(inbound, binding) {
151
+ const key = conversationKey(inbound, binding);
152
+ const existing = this.#sessionByConversation.get(key);
153
+ if (existing) {
154
+ return existing;
155
+ }
156
+ const created = this.#sessionIdFactory();
157
+ this.#sessionByConversation.set(key, created);
158
+ return created;
159
+ }
160
+ async handleInbound(opts) {
161
+ const sessionId = this.#resolveSessionId(opts.inbound, opts.binding);
162
+ const turnId = this.#turnIdFactory();
163
+ if (!this.#enabled) {
164
+ return {
165
+ kind: "reject",
166
+ reason: "meta_agent_disabled",
167
+ metaSessionId: sessionId,
168
+ metaTurnId: turnId,
169
+ };
170
+ }
171
+ if (this.#enabledChannels && !this.#enabledChannels.has(opts.inbound.channel.toLowerCase())) {
172
+ return {
173
+ kind: "reject",
174
+ reason: "meta_agent_disabled",
175
+ metaSessionId: sessionId,
176
+ metaTurnId: turnId,
177
+ };
178
+ }
179
+ let backendResult;
180
+ try {
181
+ backendResult = MetaAgentBackendTurnResultSchema.parse(await this.#backend.runTurn({
182
+ sessionId,
183
+ turnId,
184
+ inbound: opts.inbound,
185
+ binding: opts.binding,
186
+ }));
187
+ }
188
+ catch (err) {
189
+ return {
190
+ kind: "reject",
191
+ reason: "meta_agent_invalid_output",
192
+ details: err instanceof Error ? err.message : "meta_agent_backend_error",
193
+ metaSessionId: sessionId,
194
+ metaTurnId: turnId,
195
+ };
196
+ }
197
+ if (backendResult.kind === "respond") {
198
+ const message = backendResult.message.trim();
199
+ if (!SAFE_RESPONSE_RE.test(message)) {
200
+ return {
201
+ kind: "reject",
202
+ reason: "meta_agent_invalid_output",
203
+ details: "invalid response payload",
204
+ metaSessionId: sessionId,
205
+ metaTurnId: turnId,
206
+ };
207
+ }
208
+ return {
209
+ kind: "response",
210
+ message,
211
+ metaSessionId: sessionId,
212
+ metaTurnId: turnId,
213
+ };
214
+ }
215
+ const approved = this.#broker.approve({
216
+ proposal: backendResult.command,
217
+ inbound: opts.inbound,
218
+ });
219
+ if (approved.kind === "reject") {
220
+ return {
221
+ kind: "reject",
222
+ reason: approved.reason,
223
+ details: approved.details,
224
+ metaSessionId: sessionId,
225
+ metaTurnId: turnId,
226
+ };
227
+ }
228
+ return {
229
+ kind: "command",
230
+ commandText: approved.commandText,
231
+ metaSessionId: sessionId,
232
+ metaTurnId: turnId,
233
+ };
234
+ }
235
+ }
236
+ const DEFAULT_META_SYSTEM_PROMPT = [
237
+ "You are mu's dedicated messaging meta-agent.",
238
+ "Your priorities:",
239
+ "- Answer core mu operational questions (status, ready work, issue/forum navigation, run lifecycle).",
240
+ "- Guide users through control-plane setup (Slack/Discord/Telegram env vars, identity linking, serve workflow).",
241
+ "- Keep answers practical and actionable.",
242
+ "",
243
+ "You must return exactly one JSON object and nothing else.",
244
+ "Valid shape:",
245
+ ' {"kind":"respond","message":"..."}',
246
+ "or",
247
+ ' {"kind":"command","command":{...}}',
248
+ "Allowed command kinds: status, ready, issue_list, issue_get, forum_read, run_resume, run_start.",
249
+ "Use kind=respond for explanations, setup guidance, or when context is missing.",
250
+ "Use kind=command only when one allowed command will materially help answer the user.",
251
+ "Never output shell commands.",
252
+ ].join("\n");
253
+ function extractAssistantTextFromJsonEvent(event) {
254
+ if (!event || typeof event !== "object") {
255
+ return null;
256
+ }
257
+ if (event.type !== "message_end") {
258
+ return null;
259
+ }
260
+ if (!event.message || typeof event.message !== "object") {
261
+ return null;
262
+ }
263
+ if (event.message.role !== "assistant") {
264
+ return null;
265
+ }
266
+ const message = event.message;
267
+ if (typeof message.text === "string" && message.text.trim().length > 0) {
268
+ return message.text;
269
+ }
270
+ if (typeof message.content === "string" && message.content.trim().length > 0) {
271
+ return message.content;
272
+ }
273
+ if (Array.isArray(message.content)) {
274
+ const parts = [];
275
+ for (const item of message.content) {
276
+ if (!item) {
277
+ continue;
278
+ }
279
+ if (typeof item === "string") {
280
+ if (item.trim().length > 0) {
281
+ parts.push(item);
282
+ }
283
+ continue;
284
+ }
285
+ if (typeof item === "object") {
286
+ const text = item.text;
287
+ if (typeof text === "string" && text.trim().length > 0) {
288
+ parts.push(text);
289
+ }
290
+ }
291
+ }
292
+ if (parts.length > 0) {
293
+ return parts.join("\n");
294
+ }
295
+ }
296
+ return null;
297
+ }
298
+ function extractFirstJsonObject(text) {
299
+ const trimmed = text.trim();
300
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
301
+ return trimmed;
302
+ }
303
+ const match = /\{[\s\S]*\}/.exec(trimmed);
304
+ if (!match) {
305
+ return null;
306
+ }
307
+ return match[0] ?? null;
308
+ }
309
+ function buildPiPrompt(input) {
310
+ return [
311
+ `session_id: ${input.sessionId}`,
312
+ `turn_id: ${input.turnId}`,
313
+ `channel: ${input.inbound.channel}`,
314
+ `channel_tenant_id: ${input.inbound.channel_tenant_id}`,
315
+ `channel_conversation_id: ${input.inbound.channel_conversation_id}`,
316
+ `actor_binding_id: ${input.binding.binding_id}`,
317
+ `assurance_tier: ${input.binding.assurance_tier}`,
318
+ `request_id: ${input.inbound.request_id}`,
319
+ `repo_root: ${input.inbound.repo_root}`,
320
+ `incoming_text: ${input.inbound.command_text}`,
321
+ `target_type: ${input.inbound.target_type}`,
322
+ `target_id: ${input.inbound.target_id}`,
323
+ `metadata: ${JSON.stringify(input.inbound.metadata)}`,
324
+ "Decide whether to respond or propose one approved command.",
325
+ ].join("\n");
326
+ }
327
+ export class PiMessagingMetaAgentBackend {
328
+ #provider;
329
+ #model;
330
+ #thinking;
331
+ #systemPrompt;
332
+ #timeoutMs;
333
+ #piBinary;
334
+ constructor(opts = {}) {
335
+ this.#provider = opts.provider;
336
+ this.#model = opts.model;
337
+ this.#thinking = opts.thinking ?? "minimal";
338
+ this.#systemPrompt = opts.systemPrompt ?? DEFAULT_META_SYSTEM_PROMPT;
339
+ this.#timeoutMs = Math.max(1_000, Math.trunc(opts.timeoutMs ?? 90_000));
340
+ this.#piBinary = opts.piBinary ?? "pi";
341
+ }
342
+ async runTurn(input) {
343
+ const argv = [
344
+ this.#piBinary,
345
+ "--mode",
346
+ "json",
347
+ "--no-session",
348
+ "--thinking",
349
+ this.#thinking,
350
+ "--system-prompt",
351
+ this.#systemPrompt,
352
+ ];
353
+ if (this.#provider) {
354
+ argv.push("--provider", this.#provider);
355
+ }
356
+ if (this.#model) {
357
+ argv.push("--model", this.#model);
358
+ }
359
+ argv.push(buildPiPrompt(input));
360
+ const assistantText = await new Promise((resolve, reject) => {
361
+ const proc = spawn(argv[0], argv.slice(1), {
362
+ cwd: input.inbound.repo_root,
363
+ stdio: ["ignore", "pipe", "pipe"],
364
+ env: process.env,
365
+ });
366
+ const rl = createInterface({ input: proc.stdout, crlfDelay: Number.POSITIVE_INFINITY });
367
+ const stderrChunks = [];
368
+ let resolvedText = "";
369
+ let finished = false;
370
+ let timeout = null;
371
+ const finish = (fn) => {
372
+ if (finished) {
373
+ return;
374
+ }
375
+ finished = true;
376
+ if (timeout) {
377
+ clearTimeout(timeout);
378
+ }
379
+ void rl.close();
380
+ fn();
381
+ };
382
+ proc.stderr?.on("data", (chunk) => {
383
+ stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
384
+ });
385
+ void (async () => {
386
+ for await (const line of rl) {
387
+ let parsed;
388
+ try {
389
+ parsed = JSON.parse(String(line));
390
+ }
391
+ catch {
392
+ continue;
393
+ }
394
+ const maybeText = extractAssistantTextFromJsonEvent(parsed);
395
+ if (maybeText) {
396
+ resolvedText = maybeText;
397
+ }
398
+ }
399
+ })().catch((err) => {
400
+ finish(() => reject(err));
401
+ });
402
+ proc.once("error", (err) => {
403
+ finish(() => reject(err));
404
+ });
405
+ proc.once("close", (code) => {
406
+ if ((code ?? 0) !== 0) {
407
+ finish(() => reject(new Error(`pi exited with code ${code ?? 0}: ${Buffer.concat(stderrChunks).toString("utf8")}`)));
408
+ return;
409
+ }
410
+ finish(() => resolve(resolvedText));
411
+ });
412
+ timeout = setTimeout(() => {
413
+ proc.kill("SIGKILL");
414
+ finish(() => reject(new Error("pi meta-agent timeout")));
415
+ }, this.#timeoutMs);
416
+ });
417
+ const jsonPayload = extractFirstJsonObject(assistantText);
418
+ if (!jsonPayload) {
419
+ throw new Error("meta_agent_invalid_output");
420
+ }
421
+ const parsed = JSON.parse(jsonPayload);
422
+ return MetaAgentBackendTurnResultSchema.parse(parsed);
423
+ }
424
+ }
@@ -0,0 +1,15 @@
1
+ export type MuRole = "orchestrator" | "worker";
2
+ /** Determine role from tags. Defaults to orchestrator if no role tag present. */
3
+ export declare function roleFromTags(tags: readonly string[]): MuRole;
4
+ export declare const DEFAULT_ORCHESTRATOR_PROMPT: string;
5
+ export declare const DEFAULT_WORKER_PROMPT: string;
6
+ /**
7
+ * Load the system prompt for a role.
8
+ *
9
+ * When `repoRoot` is provided, tries `.mu/roles/${role}.md` first.
10
+ * The file body IS the entire system prompt — no auto-appending.
11
+ * Frontmatter is stripped via `splitFrontmatter`.
12
+ * Falls back to the hardcoded default on any error.
13
+ */
14
+ export declare function systemPromptForRole(role: MuRole, repoRoot?: string): Promise<string>;
15
+ //# sourceMappingURL=mu_roles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mu_roles.d.ts","sourceRoot":"","sources":["../src/mu_roles.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,MAAM,GAAG,cAAc,GAAG,QAAQ,CAAC;AAE/C,iFAAiF;AACjF,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAM5D;AAgED,eAAO,MAAM,2BAA2B,QAwC5B,CAAC;AAEb,eAAO,MAAM,qBAAqB,QA0BtB,CAAC;AAMb;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAY1F"}
@@ -0,0 +1,165 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { splitFrontmatter } from "./prompt.js";
4
+ /** Determine role from tags. Defaults to orchestrator if no role tag present. */
5
+ export function roleFromTags(tags) {
6
+ for (const tag of tags) {
7
+ if (tag === "role:worker")
8
+ return "worker";
9
+ if (tag === "role:orchestrator")
10
+ return "orchestrator";
11
+ }
12
+ return "orchestrator";
13
+ }
14
+ /* ------------------------------------------------------------------ */
15
+ /* mu CLI reference */
16
+ /* ------------------------------------------------------------------ */
17
+ const MU_CLI_REFERENCE = `
18
+ ## mu CLI
19
+
20
+ You are running inside **mu**, an issue-driven orchestration system.
21
+ You have four tools: bash, read, write, edit.
22
+
23
+ - Orchestrator: use bash to run \`mu\` commands; do NOT use write/edit (and avoid read).
24
+ - Worker: use tools as needed to implement your assigned issue.
25
+
26
+ Tip: run \`mu <command> --help\` for details.
27
+
28
+ ### Issues
29
+
30
+ \`\`\`bash
31
+ # Create a child issue (always set --parent and --role)
32
+ mu issues create "<title>" --parent <parent-id> --role worker [--body "<text>"] [--priority N] [--tag TAG]
33
+
34
+ # Inspect
35
+ mu issues get <id> # full issue detail
36
+ mu issues list --root <root-id> [--status open|in_progress|closed]
37
+ mu issues children <id> # direct children
38
+ mu issues ready --root <root-id> # executable leaves
39
+
40
+ # Status transitions
41
+ mu issues claim <id> # open → in_progress
42
+ mu issues close <id> --outcome <outcome> # close with outcome
43
+
44
+ # Dependencies
45
+ mu issues dep <src> blocks <dst> # src must close before dst starts
46
+ mu issues dep <child> parent <parent> # set parent-child edge
47
+ mu issues undep <src> blocks <dst> # remove blocking edge
48
+
49
+ # Update fields
50
+ mu issues update <id> [--title "..."] [--body "..."] [--role worker|orchestrator] [--priority N] [--add-tag TAG]
51
+ \`\`\`
52
+
53
+ ### Outcomes
54
+
55
+ | Outcome | Meaning |
56
+ |--------------|-----------------------------------------------------|
57
+ | \`success\` | Work completed successfully (terminal) |
58
+ | \`failure\` | Work failed — triggers re-orchestration |
59
+ | \`needs_work\` | Partial — triggers re-orchestration |
60
+ | \`expanded\` | Decomposed into children (orchestrator closes self) |
61
+ | \`skipped\` | Not applicable (terminal) |
62
+
63
+ ### Forum (logging & coordination)
64
+
65
+ \`\`\`bash
66
+ mu forum post issue:<id> -m "<message>" --author <role>
67
+ mu forum read issue:<id> [--limit N]
68
+ \`\`\`
69
+ `.trim();
70
+ /* ------------------------------------------------------------------ */
71
+ /* Default role prompts (exported for mu init + tests) */
72
+ /* ------------------------------------------------------------------ */
73
+ export const DEFAULT_ORCHESTRATOR_PROMPT = [
74
+ "# Mu Orchestrator",
75
+ "",
76
+ "You are mu's orchestrator: the hierarchical planner for the issue DAG.",
77
+ "",
78
+ "## Non-Negotiable Constraints",
79
+ "",
80
+ "1. You MUST NOT execute work directly. No code changes, no file edits, no git commits.",
81
+ "2. You MUST decompose the assigned issue into worker child issues, then close the assigned issue with `--outcome expanded`.",
82
+ "3. Decomposition MUST be deterministic and minimal. Use `blocks` edges for sequencing.",
83
+ "",
84
+ "Even if the task looks atomic: create exactly one worker child issue rather than doing the work yourself.",
85
+ "If you catch yourself about to implement: STOP and create/refine worker issues instead.",
86
+ "",
87
+ "Your only job is to create child issues, add any required `blocks` dependencies, and then close yourself with outcome=expanded.",
88
+ "",
89
+ "## Workflow",
90
+ "",
91
+ "1. Investigate: `mu issues get <id>`, `mu forum read issue:<id> --limit 20`, `mu issues children <id>`.",
92
+ "2. Decompose: create child issues with `mu issues create` (always set `--parent` and `--role worker`).",
93
+ "3. Order: add `blocks` edges between children where sequencing matters.",
94
+ "4. Close: `mu issues close <id> --outcome expanded`.",
95
+ "",
96
+ "The ONLY valid outcome for you is `expanded`.",
97
+ "Never close with `success`, `failure`, `needs_work`, or `skipped` — those are for workers.",
98
+ "",
99
+ "## Rules",
100
+ "",
101
+ "- Use only roles: orchestrator, worker.",
102
+ "- Every executable leaf MUST be `--role worker`.",
103
+ "- Never create a child without an explicit role.",
104
+ "",
105
+ "## Strategies For Good Plans",
106
+ "",
107
+ "- Include feedback loops in worker issues: tests, typecheck, build, lint, repro steps.",
108
+ "- Prefer small issues with crisp acceptance criteria over large ambiguous ones.",
109
+ "- If the work needs verification, add a worker review issue blocked by implementation.",
110
+ " If review fails, that worker should close with outcome=needs_work and describe what failed.",
111
+ "",
112
+ MU_CLI_REFERENCE,
113
+ ].join("\n");
114
+ export const DEFAULT_WORKER_PROMPT = [
115
+ "# Mu Worker",
116
+ "",
117
+ "You are mu's worker. You execute exactly one atomic issue end-to-end.",
118
+ "",
119
+ "## Responsibilities",
120
+ "",
121
+ "- Implement the work described in your assigned issue.",
122
+ "- Keep scope tight to the issue specification.",
123
+ "- Verify results (tests, typecheck, build, lint, etc.) and report what changed.",
124
+ "- Close your issue with a terminal outcome when done.",
125
+ "",
126
+ "## Workflow",
127
+ "",
128
+ "1. Inspect: `mu issues get <id>` and `mu forum read issue:<id> --limit 20`.",
129
+ "2. Implement: edit files, run commands, and keep changes scoped to the issue.",
130
+ "3. Verify: run tests/build/typecheck/lint as appropriate. Prefer hard feedback loops.",
131
+ "4. Close: `mu issues close <id> --outcome success` (or `failure`/`skipped`).",
132
+ "5. Log key notes: `mu forum post issue:<id> -m '...' --author worker`.",
133
+ "",
134
+ "## Rules",
135
+ "",
136
+ "- Do NOT create child issues — that is the orchestrator's job.",
137
+ "- If the issue is too large/unclear, close with `--outcome needs_work` and explain what is missing.",
138
+ "",
139
+ MU_CLI_REFERENCE,
140
+ ].join("\n");
141
+ /* ------------------------------------------------------------------ */
142
+ /* Role-specific system prompts */
143
+ /* ------------------------------------------------------------------ */
144
+ /**
145
+ * Load the system prompt for a role.
146
+ *
147
+ * When `repoRoot` is provided, tries `.mu/roles/${role}.md` first.
148
+ * The file body IS the entire system prompt — no auto-appending.
149
+ * Frontmatter is stripped via `splitFrontmatter`.
150
+ * Falls back to the hardcoded default on any error.
151
+ */
152
+ export async function systemPromptForRole(role, repoRoot) {
153
+ if (repoRoot) {
154
+ try {
155
+ const filePath = join(repoRoot, ".mu", "roles", `${role}.md`);
156
+ const raw = await readFile(filePath, "utf8");
157
+ const { body } = splitFrontmatter(raw);
158
+ return body;
159
+ }
160
+ catch {
161
+ // File missing or unreadable — fall through to default.
162
+ }
163
+ }
164
+ return role === "orchestrator" ? DEFAULT_ORCHESTRATOR_PROMPT : DEFAULT_WORKER_PROMPT;
165
+ }
@@ -0,0 +1,20 @@
1
+ import type { MuRole } from "./mu_roles.js";
2
+ export type BackendRunOpts = {
3
+ issueId: string;
4
+ role: MuRole;
5
+ systemPrompt: string;
6
+ prompt: string;
7
+ provider: string;
8
+ model: string;
9
+ thinking: string;
10
+ cwd: string;
11
+ cli: string;
12
+ logSuffix: string;
13
+ onLine?: (line: string) => void;
14
+ teePath?: string;
15
+ };
16
+ export interface BackendRunner {
17
+ run(opts: BackendRunOpts): Promise<number>;
18
+ }
19
+ export declare function piStreamHasError(line: string): boolean;
20
+ //# sourceMappingURL=pi_backend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pi_backend.d.ts","sourceRoot":"","sources":["../src/pi_backend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAE5C,MAAM,MAAM,cAAc,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,WAAW,aAAa;IAC7B,GAAG,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC3C;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CA4BtD"}
@@ -0,0 +1,27 @@
1
+ export function piStreamHasError(line) {
2
+ let event;
3
+ try {
4
+ event = JSON.parse(line);
5
+ }
6
+ catch {
7
+ return false;
8
+ }
9
+ const etype = event?.type;
10
+ if (etype === "message_update") {
11
+ const assistantEvent = event?.assistantMessageEvent;
12
+ if (assistantEvent && typeof assistantEvent === "object" && assistantEvent.type === "error") {
13
+ return true;
14
+ }
15
+ }
16
+ if (etype === "message_end") {
17
+ const message = event?.message;
18
+ if (!message || typeof message !== "object") {
19
+ return false;
20
+ }
21
+ if (message.role !== "assistant") {
22
+ return false;
23
+ }
24
+ return message.stopReason === "error" || message.stopReason === "aborted";
25
+ }
26
+ return false;
27
+ }
@@ -0,0 +1,20 @@
1
+ import { DefaultResourceLoader, SettingsManager } from "@mariozechner/pi-coding-agent";
2
+ import type { BackendRunner, BackendRunOpts } from "./pi_backend.js";
3
+ /**
4
+ * In-process backend using the pi SDK.
5
+ *
6
+ * Replaces subprocess spawning of the `pi` CLI with direct use of
7
+ * `createAgentSession` from `@mariozechner/pi-coding-agent`.
8
+ */
9
+ export declare class PiSdkBackend implements BackendRunner {
10
+ run(opts: BackendRunOpts): Promise<number>;
11
+ }
12
+ export type CreateMuResourceLoaderOpts = {
13
+ cwd: string;
14
+ systemPrompt: string;
15
+ agentDir?: string;
16
+ settingsManager?: SettingsManager;
17
+ additionalSkillPaths?: string[];
18
+ };
19
+ export declare function createMuResourceLoader(opts: CreateMuResourceLoaderOpts): DefaultResourceLoader;
20
+ //# sourceMappingURL=pi_sdk_backend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pi_sdk_backend.d.ts","sourceRoot":"","sources":["../src/pi_sdk_backend.ts"],"names":[],"mappings":"AAMA,OAAO,EAQN,qBAAqB,EAErB,eAAe,EACf,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAwCrE;;;;;GAKG;AACH,qBAAa,YAAa,YAAW,aAAa;IAC3C,GAAG,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC;CAiGhD;AAED,MAAM,MAAM,0BAA0B,GAAG;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;CAChC,CAAC;AAEF,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,0BAA0B,GAAG,qBAAqB,CAsB9F"}