@femtomc/mu-agent 26.2.69 → 26.2.70

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.
Files changed (52) hide show
  1. package/README.md +20 -1
  2. package/dist/backend.d.ts +1 -0
  3. package/dist/backend.d.ts.map +1 -1
  4. package/dist/backend.js +11 -2
  5. package/dist/extensions/activities.d.ts +4 -1
  6. package/dist/extensions/activities.d.ts.map +1 -1
  7. package/dist/extensions/activities.js +127 -16
  8. package/dist/extensions/branding.d.ts +2 -3
  9. package/dist/extensions/branding.d.ts.map +1 -1
  10. package/dist/extensions/branding.js +73 -58
  11. package/dist/extensions/cron.d.ts +4 -1
  12. package/dist/extensions/cron.d.ts.map +1 -1
  13. package/dist/extensions/cron.js +74 -14
  14. package/dist/extensions/heartbeats.d.ts +4 -1
  15. package/dist/extensions/heartbeats.d.ts.map +1 -1
  16. package/dist/extensions/heartbeats.js +60 -17
  17. package/dist/extensions/index.d.ts +13 -3
  18. package/dist/extensions/index.d.ts.map +1 -1
  19. package/dist/extensions/index.js +13 -3
  20. package/dist/extensions/messaging-setup.d.ts +4 -1
  21. package/dist/extensions/messaging-setup.d.ts.map +1 -1
  22. package/dist/extensions/messaging-setup.js +47 -10
  23. package/dist/extensions/mu-full-tools.d.ts +10 -0
  24. package/dist/extensions/mu-full-tools.d.ts.map +1 -0
  25. package/dist/extensions/mu-full-tools.js +25 -0
  26. package/dist/extensions/mu-operator.d.ts.map +1 -1
  27. package/dist/extensions/mu-operator.js +2 -6
  28. package/dist/extensions/mu-query-tools.d.ts +10 -0
  29. package/dist/extensions/mu-query-tools.d.ts.map +1 -0
  30. package/dist/extensions/mu-query-tools.js +11 -0
  31. package/dist/extensions/operator-command.d.ts.map +1 -1
  32. package/dist/extensions/operator-command.js +106 -6
  33. package/dist/extensions/orchestration-runs-readonly.d.ts.map +1 -1
  34. package/dist/extensions/orchestration-runs-readonly.js +180 -10
  35. package/dist/extensions/orchestration-runs.d.ts.map +1 -1
  36. package/dist/extensions/orchestration-runs.js +206 -14
  37. package/dist/extensions/server-tools.d.ts +10 -0
  38. package/dist/extensions/server-tools.d.ts.map +1 -1
  39. package/dist/extensions/server-tools.js +688 -290
  40. package/dist/extensions/shared.d.ts +11 -0
  41. package/dist/extensions/shared.d.ts.map +1 -1
  42. package/dist/extensions/shared.js +81 -0
  43. package/dist/session_factory.d.ts.map +1 -1
  44. package/dist/session_factory.js +3 -1
  45. package/dist/ui_defaults.d.ts +4 -0
  46. package/dist/ui_defaults.d.ts.map +1 -0
  47. package/dist/ui_defaults.js +18 -0
  48. package/package.json +4 -3
  49. package/prompts/roles/operator.md +5 -0
  50. package/prompts/roles/orchestrator.md +1 -0
  51. package/prompts/roles/worker.md +10 -4
  52. package/themes/mu-gruvbox-dark.json +90 -0
@@ -6,13 +6,108 @@
6
6
  import { StringEnum } from "@mariozechner/pi-ai";
7
7
  import { Type } from "@sinclair/typebox";
8
8
  import { registerMuSubcommand } from "./mu-command-dispatcher.js";
9
- import { clampInt, fetchMuJson, fetchMuStatus, muServerUrl, textResult, toJsonText, } from "./shared.js";
9
+ import { asArray, asNumber, asRecord, asString, clampInt, fetchMuJson, fetchMuStatus, muServerUrl, parseFieldPaths, previewText, selectFields, textResult, toJsonText, } from "./shared.js";
10
10
  function trimOrNull(value) {
11
11
  if (value == null)
12
12
  return null;
13
13
  const trimmed = value.trim();
14
14
  return trimmed.length > 0 ? trimmed : null;
15
15
  }
16
+ function parseCommaList(value) {
17
+ const text = trimOrNull(value);
18
+ if (!text)
19
+ return null;
20
+ const items = text
21
+ .split(",")
22
+ .map((item) => item.trim())
23
+ .filter((item) => item.length > 0);
24
+ return items.length > 0 ? items : null;
25
+ }
26
+ function stringArray(value, max = 20) {
27
+ return asArray(value)
28
+ .map((item) => asString(item))
29
+ .filter((item) => item != null)
30
+ .slice(0, max);
31
+ }
32
+ function summarizeIssue(issue, opts = {}) {
33
+ const title = asString(issue.title) ?? "";
34
+ const body = asString(issue.body) ?? "";
35
+ const summary = {
36
+ id: asString(issue.id),
37
+ title: previewText(title, 140),
38
+ status: asString(issue.status),
39
+ priority: asNumber(issue.priority),
40
+ outcome: issue.outcome ?? null,
41
+ tags: stringArray(issue.tags, 12),
42
+ deps: asArray(issue.deps).length,
43
+ updated_at: asNumber(issue.updated_at),
44
+ };
45
+ if (opts.includeBodyPreview) {
46
+ summary.body_chars = body.length;
47
+ summary.body_preview = previewText(body, 220);
48
+ }
49
+ return summary;
50
+ }
51
+ function summarizeForumMessage(message) {
52
+ const body = asString(message.body) ?? "";
53
+ return {
54
+ author: asString(message.author) ?? "unknown",
55
+ created_at: asNumber(message.created_at),
56
+ body_chars: body.length,
57
+ body_preview: previewText(body, 240),
58
+ };
59
+ }
60
+ function summarizeEvent(event) {
61
+ const payload = asRecord(event.payload);
62
+ const payloadKeys = payload ? Object.keys(payload).slice(0, 8) : [];
63
+ return {
64
+ ts_ms: asNumber(event.ts_ms),
65
+ type: asString(event.type),
66
+ source: asString(event.source),
67
+ issue_id: asString(event.issue_id),
68
+ run_id: asString(event.run_id),
69
+ payload_keys: payloadKeys,
70
+ payload_preview: previewText(event.payload, 140),
71
+ };
72
+ }
73
+ function deriveBindingRoleFromScopes(scopes) {
74
+ const scopeSet = new Set(scopes);
75
+ if (scopeSet.has("cp.ops.admin") || scopeSet.has("cp.identity.admin")) {
76
+ return "operator";
77
+ }
78
+ if (scopeSet.has("cp.issue.write") || scopeSet.has("cp.forum.write") || scopeSet.has("cp.run.execute")) {
79
+ return "contributor";
80
+ }
81
+ if (scopeSet.has("cp.read")) {
82
+ return "viewer";
83
+ }
84
+ return null;
85
+ }
86
+ function summarizeBinding(binding) {
87
+ const scopes = stringArray(binding.scopes, 20);
88
+ const derivedRole = deriveBindingRoleFromScopes(scopes);
89
+ const status = asString(binding.status);
90
+ const active = binding.active ?? (status ? status === "active" : null);
91
+ return {
92
+ binding_id: asString(binding.binding_id),
93
+ channel: asString(binding.channel),
94
+ actor_id: asString(binding.actor_id) ?? asString(binding.channel_actor_id),
95
+ tenant_id: asString(binding.tenant_id) ?? asString(binding.channel_tenant_id),
96
+ role: asString(binding.role) ?? derivedRole,
97
+ status,
98
+ active,
99
+ scopes,
100
+ created_at_ms: asNumber(binding.created_at_ms) ?? asNumber(binding.linked_at_ms),
101
+ updated_at_ms: asNumber(binding.updated_at_ms),
102
+ };
103
+ }
104
+ function includeByContains(contains, ...fragments) {
105
+ if (!contains) {
106
+ return true;
107
+ }
108
+ const haystack = fragments.map((fragment) => previewText(fragment, 4_000).toLowerCase()).join("\n");
109
+ return haystack.includes(contains.toLowerCase());
110
+ }
16
111
  function cpRoutesFromStatus(routes, adapters) {
17
112
  if (routes && routes.length > 0) {
18
113
  return routes;
@@ -71,7 +166,7 @@ function registerServerTools(pi, opts) {
71
166
  "",
72
167
  `[MU SERVER] Connected at ${url}.`,
73
168
  opts.toolIntroLine,
74
- "Use these tools to inspect repository state and control-plane runtime before advising users.",
169
+ opts.usageLine,
75
170
  ...opts.extraSystemPromptLines,
76
171
  ].join("\n");
77
172
  return {
@@ -91,217 +186,464 @@ function registerServerTools(pi, opts) {
91
186
  ctx.ui.setStatus("mu-status", ctx.ui.theme.fg("warning", "μ status unavailable"));
92
187
  }
93
188
  });
94
- pi.registerTool({
95
- name: "mu_status",
96
- label: "mu Status",
97
- description: "Get high-level mu server status (repo root, issue counts, control-plane activity).",
98
- parameters: Type.Object({}),
99
- async execute() {
100
- const status = await fetchMuStatus();
101
- return textResult(summarizeStatus(status), {
102
- status,
103
- });
104
- },
105
- });
189
+ if (opts.includeStatusTool) {
190
+ pi.registerTool({
191
+ name: "mu_status",
192
+ label: "mu Status",
193
+ description: "Get high-level mu server status (repo root, issue counts, control-plane activity).",
194
+ parameters: Type.Object({}),
195
+ async execute() {
196
+ const status = await fetchMuStatus();
197
+ return textResult(summarizeStatus(status), {
198
+ status,
199
+ });
200
+ },
201
+ });
202
+ }
106
203
  const ControlPlaneParams = Type.Object({
107
204
  action: StringEnum(["status", "adapters", "routes"]),
108
205
  });
109
- pi.registerTool({
110
- name: "mu_control_plane",
111
- label: "Control Plane",
112
- description: "Inspect control-plane runtime state: active flag, mounted adapters, and webhook routes.",
113
- parameters: ControlPlaneParams,
114
- async execute(_toolCallId, params) {
115
- const status = await fetchMuStatus();
116
- const cp = status.control_plane;
117
- const routes = cpRoutesFromStatus(cp.routes, cp.adapters);
118
- const generation = cp.generation;
119
- const observability = cp.observability.counters;
120
- switch (params.action) {
121
- case "status":
122
- return textResult(toJsonText({
123
- active: cp.active,
124
- adapters: cp.adapters,
125
- routes,
126
- generation,
127
- observability,
128
- }), { control_plane: cp, routes, generation, observability });
129
- case "adapters":
130
- return textResult(toJsonText(cp.adapters), { adapters: cp.adapters });
131
- case "routes":
132
- return textResult(toJsonText(routes), { routes });
133
- default:
134
- return textResult(`Unknown action: ${params.action}`);
135
- }
136
- },
137
- });
206
+ if (opts.includeControlPlaneTool) {
207
+ pi.registerTool({
208
+ name: "mu_control_plane",
209
+ label: "Control Plane",
210
+ description: "Inspect control-plane runtime state: active flag, mounted adapters, and webhook routes.",
211
+ parameters: ControlPlaneParams,
212
+ async execute(_toolCallId, params) {
213
+ const status = await fetchMuStatus();
214
+ const cp = status.control_plane;
215
+ const routes = cpRoutesFromStatus(cp.routes, cp.adapters);
216
+ const generation = cp.generation;
217
+ const observability = cp.observability.counters;
218
+ switch (params.action) {
219
+ case "status":
220
+ return textResult(toJsonText({
221
+ active: cp.active,
222
+ adapters: cp.adapters,
223
+ routes,
224
+ generation,
225
+ observability,
226
+ }), { control_plane: cp, routes, generation, observability });
227
+ case "adapters":
228
+ return textResult(toJsonText(cp.adapters), { adapters: cp.adapters });
229
+ case "routes":
230
+ return textResult(toJsonText(routes), { routes });
231
+ default:
232
+ return textResult(`Unknown action: ${params.action}`);
233
+ }
234
+ },
235
+ });
236
+ }
237
+ const issueActions = opts.allowIssueMutations
238
+ ? ["list", "get", "ready", "create", "update", "claim", "close"]
239
+ : ["list", "get", "ready"];
138
240
  const IssuesParams = Type.Object({
139
- action: StringEnum(["list", "get", "ready"]),
140
- id: Type.Optional(Type.String({ description: "Issue ID (for get)" })),
141
- status: Type.Optional(Type.String({ description: "Filter by status (for list)" })),
241
+ action: StringEnum(issueActions),
242
+ id: Type.Optional(Type.String({ description: "Issue ID (for get/update/claim/close)" })),
243
+ title: Type.Optional(Type.String({ description: "Issue title (for create/update)" })),
244
+ body: Type.Optional(Type.String({ description: "Issue body (for create/update)" })),
245
+ status: Type.Optional(Type.String({ description: "Filter by status (list) or status to set (update)" })),
142
246
  tag: Type.Optional(Type.String({ description: "Filter by tag (for list)" })),
247
+ tags: Type.Optional(Type.String({ description: "Comma-separated tags (for create/update)" })),
248
+ priority: Type.Optional(Type.Number({ description: "Priority (for create/update)" })),
249
+ outcome: Type.Optional(Type.String({ description: "Outcome (for close/update)" })),
143
250
  root: Type.Optional(Type.String({ description: "Root issue ID (for ready)" })),
144
- limit: Type.Optional(Type.Number({ description: "Max returned items (default 50, max 200)" })),
251
+ contains: Type.Optional(Type.String({ description: "Case-insensitive search text over issue title/body" })),
252
+ fields: Type.Optional(Type.String({ description: "Comma-separated fields for get (e.g. title,status,tags,body)" })),
253
+ limit: Type.Optional(Type.Number({ description: "Max returned items (default 20, max 200)" })),
145
254
  });
146
- pi.registerTool({
147
- name: "mu_issues",
148
- label: "Issues",
149
- description: "Query mu issues. Actions: list, get, ready.",
150
- parameters: IssuesParams,
151
- async execute(_toolCallId, params) {
152
- switch (params.action) {
153
- case "list": {
154
- const query = new URLSearchParams();
155
- const status = trimOrNull(params.status);
156
- const tag = trimOrNull(params.tag);
157
- if (status)
255
+ if (opts.includeIssuesTool) {
256
+ pi.registerTool({
257
+ name: "mu_issues",
258
+ label: "Issues",
259
+ description: opts.allowIssueMutations
260
+ ? "Read and update mu issues. Actions: list, get, ready, create, update, claim, close. Returns concise summaries by default; use fields for precise retrieval."
261
+ : "Query mu issues. Actions: list, get, ready. Returns concise summaries by default. Use id + fields for precise retrieval.",
262
+ parameters: IssuesParams,
263
+ async execute(_toolCallId, params) {
264
+ switch (params.action) {
265
+ case "list": {
266
+ const query = new URLSearchParams();
267
+ const status = trimOrNull(params.status) ?? "open";
268
+ const tag = trimOrNull(params.tag);
269
+ const contains = trimOrNull(params.contains);
270
+ const limit = clampInt(params.limit, 20, 1, 200);
158
271
  query.set("status", status);
159
- if (tag)
160
- query.set("tag", tag);
161
- const issues = await fetchMuJson(`/api/issues${query.size > 0 ? `?${query.toString()}` : ""}`);
162
- const sliced = sliceWithLimit(issues, params.limit);
163
- return textResult(toJsonText({
164
- total: sliced.total,
165
- returned: sliced.returned,
166
- truncated: sliced.truncated,
167
- issues: sliced.items,
168
- }), { query: { status, tag }, ...sliced });
169
- }
170
- case "get": {
171
- const id = trimOrNull(params.id);
172
- if (!id)
173
- return textResult("Error: id required for get");
174
- const issue = await fetchMuJson(`/api/issues/${encodeURIComponent(id)}`);
175
- return textResult(toJsonText(issue), { id, issue });
176
- }
177
- case "ready": {
178
- const root = trimOrNull(params.root);
179
- const query = root ? `?root=${encodeURIComponent(root)}` : "";
180
- const issues = await fetchMuJson(`/api/issues/ready${query}`);
181
- const sliced = sliceWithLimit(issues, params.limit);
182
- return textResult(toJsonText({
183
- total: sliced.total,
184
- returned: sliced.returned,
185
- truncated: sliced.truncated,
186
- issues: sliced.items,
187
- }), { query: { root }, ...sliced });
272
+ query.set("limit", String(limit));
273
+ if (tag)
274
+ query.set("tag", tag);
275
+ if (contains)
276
+ query.set("contains", contains);
277
+ const issues = await fetchMuJson(`/api/issues?${query.toString()}`);
278
+ const records = issues
279
+ .map((issue) => asRecord(issue))
280
+ .filter((issue) => issue != null);
281
+ const sliced = sliceWithLimit(records, params.limit, 20);
282
+ const summaries = sliced.items.map((issue) => summarizeIssue(issue));
283
+ return textResult(toJsonText({
284
+ total: sliced.total,
285
+ returned: sliced.returned,
286
+ truncated: sliced.truncated,
287
+ issues: summaries,
288
+ next: sliced.truncated
289
+ ? "Refine filters or increase limit. Use mu_issues(action='get', id='...') for precise inspection."
290
+ : null,
291
+ }), { query: { status, tag, contains, limit }, ...sliced, issues: sliced.items });
292
+ }
293
+ case "get": {
294
+ const id = trimOrNull(params.id);
295
+ if (!id)
296
+ return textResult("Error: id required for get");
297
+ const issue = await fetchMuJson(`/api/issues/${encodeURIComponent(id)}`);
298
+ const fields = parseFieldPaths(trimOrNull(params.fields) ?? undefined);
299
+ const content = fields.length > 0
300
+ ? { id, selected: selectFields(issue, fields) }
301
+ : { issue: summarizeIssue(issue, { includeBodyPreview: true }) };
302
+ return textResult(toJsonText(content), { id, fields, issue });
303
+ }
304
+ case "ready": {
305
+ const root = trimOrNull(params.root);
306
+ const contains = trimOrNull(params.contains);
307
+ const limit = clampInt(params.limit, 20, 1, 200);
308
+ const query = new URLSearchParams({ limit: String(limit) });
309
+ if (root)
310
+ query.set("root", root);
311
+ if (contains)
312
+ query.set("contains", contains);
313
+ const issues = await fetchMuJson(`/api/issues/ready?${query.toString()}`);
314
+ const records = issues
315
+ .map((issue) => asRecord(issue))
316
+ .filter((issue) => issue != null);
317
+ const sliced = sliceWithLimit(records, params.limit, 20);
318
+ const summaries = sliced.items.map((issue) => summarizeIssue(issue));
319
+ return textResult(toJsonText({
320
+ total: sliced.total,
321
+ returned: sliced.returned,
322
+ truncated: sliced.truncated,
323
+ issues: summaries,
324
+ next: sliced.truncated ? "Narrow by root/contains or increase limit." : null,
325
+ }), { query: { root, contains, limit }, ...sliced, issues: sliced.items });
326
+ }
327
+ case "create": {
328
+ if (!opts.allowIssueMutations) {
329
+ return textResult("issue mutations are disabled in query-only mode.", {
330
+ blocked: true,
331
+ reason: "issue_query_only_mode",
332
+ });
333
+ }
334
+ const title = trimOrNull(params.title);
335
+ if (!title)
336
+ return textResult("Error: title required for create");
337
+ const bodyText = trimOrNull(params.body);
338
+ const tags = parseCommaList(params.tags);
339
+ const priority = typeof params.priority === "number" && Number.isFinite(params.priority)
340
+ ? Math.trunc(params.priority)
341
+ : undefined;
342
+ const issue = await fetchMuJson("/api/issues", {
343
+ method: "POST",
344
+ body: {
345
+ title,
346
+ body: bodyText ?? undefined,
347
+ tags: tags ?? undefined,
348
+ priority,
349
+ },
350
+ });
351
+ return textResult(toJsonText({ issue: summarizeIssue(issue, { includeBodyPreview: true }) }), {
352
+ action: "create",
353
+ issue,
354
+ });
355
+ }
356
+ case "update": {
357
+ if (!opts.allowIssueMutations) {
358
+ return textResult("issue mutations are disabled in query-only mode.", {
359
+ blocked: true,
360
+ reason: "issue_query_only_mode",
361
+ });
362
+ }
363
+ const id = trimOrNull(params.id);
364
+ if (!id)
365
+ return textResult("Error: id required for update");
366
+ const patch = {};
367
+ const title = trimOrNull(params.title);
368
+ if (title != null)
369
+ patch.title = title;
370
+ const bodyText = trimOrNull(params.body);
371
+ if (bodyText != null)
372
+ patch.body = bodyText;
373
+ const status = trimOrNull(params.status);
374
+ if (status != null)
375
+ patch.status = status;
376
+ const outcome = trimOrNull(params.outcome);
377
+ if (outcome != null)
378
+ patch.outcome = outcome;
379
+ const tags = parseCommaList(params.tags);
380
+ if (tags != null)
381
+ patch.tags = tags;
382
+ if (typeof params.priority === "number" && Number.isFinite(params.priority)) {
383
+ patch.priority = Math.trunc(params.priority);
384
+ }
385
+ if (Object.keys(patch).length === 0) {
386
+ return textResult("Error: update requires at least one field (title/body/status/outcome/tags/priority)");
387
+ }
388
+ const issue = await fetchMuJson(`/api/issues/${encodeURIComponent(id)}`, {
389
+ method: "PATCH",
390
+ body: patch,
391
+ });
392
+ return textResult(toJsonText({ issue: summarizeIssue(issue, { includeBodyPreview: true }) }), {
393
+ action: "update",
394
+ id,
395
+ patch,
396
+ issue,
397
+ });
398
+ }
399
+ case "claim": {
400
+ if (!opts.allowIssueMutations) {
401
+ return textResult("issue mutations are disabled in query-only mode.", {
402
+ blocked: true,
403
+ reason: "issue_query_only_mode",
404
+ });
405
+ }
406
+ const id = trimOrNull(params.id);
407
+ if (!id)
408
+ return textResult("Error: id required for claim");
409
+ const issue = await fetchMuJson(`/api/issues/${encodeURIComponent(id)}/claim`, {
410
+ method: "POST",
411
+ body: {},
412
+ });
413
+ return textResult(toJsonText({ issue: summarizeIssue(issue, { includeBodyPreview: true }) }), {
414
+ action: "claim",
415
+ id,
416
+ issue,
417
+ });
418
+ }
419
+ case "close": {
420
+ if (!opts.allowIssueMutations) {
421
+ return textResult("issue mutations are disabled in query-only mode.", {
422
+ blocked: true,
423
+ reason: "issue_query_only_mode",
424
+ });
425
+ }
426
+ const id = trimOrNull(params.id);
427
+ if (!id)
428
+ return textResult("Error: id required for close");
429
+ const outcome = trimOrNull(params.outcome) ?? "success";
430
+ const issue = await fetchMuJson(`/api/issues/${encodeURIComponent(id)}/close`, {
431
+ method: "POST",
432
+ body: { outcome },
433
+ });
434
+ return textResult(toJsonText({ issue: summarizeIssue(issue, { includeBodyPreview: true }) }), {
435
+ action: "close",
436
+ id,
437
+ outcome,
438
+ issue,
439
+ });
440
+ }
441
+ default:
442
+ return textResult(`Unknown action: ${params.action}`);
188
443
  }
189
- default:
190
- return textResult(`Unknown action: ${params.action}`);
191
- }
192
- },
193
- });
444
+ },
445
+ });
446
+ }
447
+ const forumActions = opts.allowForumPost ? ["read", "post", "topics"] : ["read", "topics"];
194
448
  const ForumParams = Type.Object({
195
- action: StringEnum(["read", "post", "topics"]),
449
+ action: StringEnum(forumActions),
196
450
  topic: Type.Optional(Type.String({ description: "Topic name (for read/post)" })),
197
451
  body: Type.Optional(Type.String({ description: "Message body (for post)" })),
198
452
  prefix: Type.Optional(Type.String({ description: "Topic prefix filter (for topics)" })),
199
- limit: Type.Optional(Type.Number({ description: "Max returned items (default 50, max 200)" })),
453
+ contains: Type.Optional(Type.String({ description: "Case-insensitive search within message body for read" })),
454
+ limit: Type.Optional(Type.Number({ description: "Max returned items (default 20, max 200)" })),
200
455
  });
201
- pi.registerTool({
202
- name: "mu_forum",
203
- label: "Forum",
204
- description: "Interact with mu forum. Actions: read, post, topics.",
205
- parameters: ForumParams,
206
- async execute(_toolCallId, params) {
207
- switch (params.action) {
208
- case "read": {
209
- const topic = trimOrNull(params.topic);
210
- if (!topic)
211
- return textResult("Error: topic required for read");
212
- const limit = clampInt(params.limit, 50, 1, 200);
213
- const query = new URLSearchParams({ topic, limit: String(limit) });
214
- const messages = await fetchMuJson(`/api/forum/read?${query.toString()}`);
215
- return textResult(toJsonText({ topic, count: messages.length, messages }), {
216
- topic,
217
- limit,
218
- count: messages.length,
219
- });
220
- }
221
- case "post": {
222
- if (!opts.allowForumPost) {
223
- return textResult("forum post is disabled in operator read-only mode; use approved /mu command flow for mutations.", { blocked: true, reason: "operator_read_only_tools" });
224
- }
225
- const topic = trimOrNull(params.topic);
226
- const body = trimOrNull(params.body);
227
- if (!topic)
228
- return textResult("Error: topic required for post");
229
- if (!body)
230
- return textResult("Error: body required for post");
231
- const message = await fetchMuJson("/api/forum/post", {
232
- method: "POST",
233
- body: {
456
+ if (opts.includeForumTool) {
457
+ pi.registerTool({
458
+ name: "mu_forum",
459
+ label: "Forum",
460
+ description: opts.allowForumPost
461
+ ? "Interact with mu forum. Actions: read, post, topics. Read/topics return concise summaries for context safety."
462
+ : "Read forum context. Actions: read, topics. Query-only mode excludes post.",
463
+ parameters: ForumParams,
464
+ async execute(_toolCallId, params) {
465
+ switch (params.action) {
466
+ case "read": {
467
+ const topic = trimOrNull(params.topic);
468
+ if (!topic)
469
+ return textResult("Error: topic required for read");
470
+ const contains = trimOrNull(params.contains);
471
+ const limit = clampInt(params.limit, 20, 1, 200);
472
+ const query = new URLSearchParams({ topic, limit: String(Math.max(limit, 50)) });
473
+ const messages = await fetchMuJson(`/api/forum/read?${query.toString()}`);
474
+ const records = messages
475
+ .map((message) => asRecord(message))
476
+ .filter((message) => message != null)
477
+ .filter((message) => includeByContains(contains, message.body, message.author));
478
+ const sliced = sliceWithLimit(records, params.limit, 20);
479
+ const summaries = sliced.items.map((message) => summarizeForumMessage(message));
480
+ return textResult(toJsonText({
234
481
  topic,
235
- body,
236
- author: "mu-agent",
237
- },
238
- });
239
- return textResult(toJsonText(message), { topic, posted: true });
240
- }
241
- case "topics": {
242
- const query = new URLSearchParams();
243
- const prefix = trimOrNull(params.prefix);
244
- if (prefix)
245
- query.set("prefix", prefix);
246
- const topics = await fetchMuJson(`/api/forum/topics${query.size > 0 ? `?${query.toString()}` : ""}`);
247
- const sliced = sliceWithLimit(topics, params.limit);
248
- return textResult(toJsonText({
249
- total: sliced.total,
250
- returned: sliced.returned,
251
- truncated: sliced.truncated,
252
- topics: sliced.items,
253
- }), { prefix, ...sliced });
482
+ total: sliced.total,
483
+ returned: sliced.returned,
484
+ truncated: sliced.truncated,
485
+ messages: summaries,
486
+ next: sliced.truncated ? "Use contains/topic filters or lower noise with smaller limit." : null,
487
+ }), { topic, contains, ...sliced, messages: sliced.items });
488
+ }
489
+ case "post": {
490
+ if (!opts.allowForumPost) {
491
+ return textResult("forum post is disabled in operator read-only mode; use approved /mu command flow for mutations.", { blocked: true, reason: "operator_read_only_tools" });
492
+ }
493
+ const topic = trimOrNull(params.topic);
494
+ const body = trimOrNull(params.body);
495
+ if (!topic)
496
+ return textResult("Error: topic required for post");
497
+ if (!body)
498
+ return textResult("Error: body required for post");
499
+ const message = await fetchMuJson("/api/forum/post", {
500
+ method: "POST",
501
+ body: {
502
+ topic,
503
+ body,
504
+ author: "mu-agent",
505
+ },
506
+ });
507
+ return textResult(toJsonText(message), { topic, posted: true });
508
+ }
509
+ case "topics": {
510
+ const query = new URLSearchParams();
511
+ const prefix = trimOrNull(params.prefix);
512
+ if (prefix)
513
+ query.set("prefix", prefix);
514
+ const topics = await fetchMuJson(`/api/forum/topics${query.size > 0 ? `?${query.toString()}` : ""}`);
515
+ const records = topics
516
+ .map((topic) => asRecord(topic))
517
+ .filter((topic) => topic != null)
518
+ .map((topic) => ({
519
+ topic: asString(topic.topic) ?? previewText(topic, 120),
520
+ messages: asNumber(topic.messages) ?? null,
521
+ last_at: asNumber(topic.last_at) ?? null,
522
+ }));
523
+ const fallback = topics
524
+ .filter((topic) => typeof topic === "string")
525
+ .map((topic) => ({ topic: topic, messages: null, last_at: null }));
526
+ const merged = records.length > 0 ? records : fallback;
527
+ const sliced = sliceWithLimit(merged, params.limit, 20);
528
+ return textResult(toJsonText({
529
+ total: sliced.total,
530
+ returned: sliced.returned,
531
+ truncated: sliced.truncated,
532
+ topics: sliced.items,
533
+ }), { prefix, ...sliced });
534
+ }
535
+ default:
536
+ return textResult(`Unknown action: ${params.action}`);
254
537
  }
255
- default:
256
- return textResult(`Unknown action: ${params.action}`);
257
- }
258
- },
259
- });
538
+ },
539
+ });
540
+ }
260
541
  const EventsParams = Type.Object({
261
542
  action: StringEnum(["tail", "query"]),
262
543
  type: Type.Optional(Type.String({ description: "Filter by event type" })),
263
544
  source: Type.Optional(Type.String({ description: "Filter by event source" })),
545
+ issue_id: Type.Optional(Type.String({ description: "Filter by correlated issue_id" })),
546
+ run_id: Type.Optional(Type.String({ description: "Filter by correlated run_id" })),
264
547
  since: Type.Optional(Type.Number({ description: "Only events >= ts_ms" })),
265
- limit: Type.Optional(Type.Number({ description: "Max returned items (default 50, max 200)" })),
548
+ contains: Type.Optional(Type.String({ description: "Case-insensitive search over event payload preview" })),
549
+ limit: Type.Optional(Type.Number({ description: "Max returned items (default 20, max 200)" })),
266
550
  });
267
- pi.registerTool({
268
- name: "mu_events",
269
- label: "Events",
270
- description: "Query mu event log. Actions: tail, query.",
271
- parameters: EventsParams,
272
- async execute(_toolCallId, params) {
273
- switch (params.action) {
274
- case "tail": {
275
- const limit = clampInt(params.limit, 50, 1, 200);
276
- const events = await fetchMuJson(`/api/events/tail?n=${limit}`);
277
- return textResult(toJsonText({ count: events.length, events }), { limit, count: events.length });
278
- }
279
- case "query": {
280
- const query = new URLSearchParams();
281
- const type = trimOrNull(params.type);
282
- const source = trimOrNull(params.source);
283
- const limit = clampInt(params.limit, 50, 1, 200);
284
- if (type)
285
- query.set("type", type);
286
- if (source)
287
- query.set("source", source);
288
- if (params.since != null)
289
- query.set("since", String(Math.trunc(params.since)));
290
- query.set("limit", String(limit));
291
- const events = await fetchMuJson(`/api/events?${query.toString()}`);
292
- return textResult(toJsonText({
293
- filters: { type, source, since: params.since ?? null },
294
- count: events.length,
295
- events,
296
- }), { type, source, since: params.since ?? null, limit, count: events.length });
551
+ if (opts.includeEventsTool) {
552
+ pi.registerTool({
553
+ name: "mu_events",
554
+ label: "Events",
555
+ description: "Query mu event log. Actions: tail, query. Returns compact event previews by default.",
556
+ parameters: EventsParams,
557
+ async execute(_toolCallId, params) {
558
+ switch (params.action) {
559
+ case "tail": {
560
+ const type = trimOrNull(params.type);
561
+ const source = trimOrNull(params.source);
562
+ const issueId = trimOrNull(params.issue_id);
563
+ const runId = trimOrNull(params.run_id);
564
+ const contains = trimOrNull(params.contains);
565
+ const limit = clampInt(params.limit, 20, 1, 200);
566
+ const tailFetch = Math.max(limit * 3, 50);
567
+ const events = await fetchMuJson(`/api/events/tail?n=${tailFetch}`);
568
+ const records = events
569
+ .map((event) => asRecord(event))
570
+ .filter((event) => event != null)
571
+ .filter((event) => (type ? asString(event.type) === type : true))
572
+ .filter((event) => (source ? asString(event.source) === source : true))
573
+ .filter((event) => (issueId ? asString(event.issue_id) === issueId : true))
574
+ .filter((event) => (runId ? asString(event.run_id) === runId : true))
575
+ .filter((event) => includeByContains(contains, event.type, event.source, event.issue_id, event.run_id, event.payload));
576
+ const sliced = sliceWithLimit(records, params.limit, 20);
577
+ const summaries = sliced.items.map((event) => summarizeEvent(event));
578
+ return textResult(toJsonText({
579
+ filters: { type, source, issue_id: issueId, run_id: runId, contains },
580
+ total: sliced.total,
581
+ returned: sliced.returned,
582
+ truncated: sliced.truncated,
583
+ events: summaries,
584
+ }), { action: "tail", type, source, issueId, runId, contains, ...sliced, events: sliced.items });
585
+ }
586
+ case "query": {
587
+ const query = new URLSearchParams();
588
+ const type = trimOrNull(params.type);
589
+ const source = trimOrNull(params.source);
590
+ const issueId = trimOrNull(params.issue_id);
591
+ const runId = trimOrNull(params.run_id);
592
+ const contains = trimOrNull(params.contains);
593
+ const limit = clampInt(params.limit, 20, 1, 200);
594
+ if (type)
595
+ query.set("type", type);
596
+ if (source)
597
+ query.set("source", source);
598
+ if (issueId)
599
+ query.set("issue_id", issueId);
600
+ if (runId)
601
+ query.set("run_id", runId);
602
+ if (params.since != null)
603
+ query.set("since", String(Math.trunc(params.since)));
604
+ query.set("limit", String(Math.max(limit, 50)));
605
+ const events = await fetchMuJson(`/api/events?${query.toString()}`);
606
+ const records = events
607
+ .map((event) => asRecord(event))
608
+ .filter((event) => event != null)
609
+ .filter((event) => (issueId ? asString(event.issue_id) === issueId : true))
610
+ .filter((event) => (runId ? asString(event.run_id) === runId : true))
611
+ .filter((event) => includeByContains(contains, event.type, event.source, event.issue_id, event.run_id, event.payload));
612
+ const sliced = sliceWithLimit(records, params.limit, 20);
613
+ const summaries = sliced.items.map((event) => summarizeEvent(event));
614
+ return textResult(toJsonText({
615
+ filters: {
616
+ type,
617
+ source,
618
+ issue_id: issueId,
619
+ run_id: runId,
620
+ since: params.since ?? null,
621
+ contains,
622
+ },
623
+ total: sliced.total,
624
+ returned: sliced.returned,
625
+ truncated: sliced.truncated,
626
+ events: summaries,
627
+ }), {
628
+ type,
629
+ source,
630
+ issueId,
631
+ runId,
632
+ since: params.since ?? null,
633
+ contains,
634
+ ...sliced,
635
+ events: sliced.items,
636
+ });
637
+ }
638
+ default:
639
+ return textResult(`Unknown action: ${params.action}`);
297
640
  }
298
- default:
299
- return textResult(`Unknown action: ${params.action}`);
300
- }
301
- },
302
- });
641
+ },
642
+ });
643
+ }
644
+ const identityActions = opts.allowIdentityMutations ? ["list", "link", "unlink"] : ["list"];
303
645
  const IdentityParams = Type.Object({
304
- action: StringEnum(["list", "link", "unlink"]),
646
+ action: StringEnum(identityActions),
305
647
  channel: Type.Optional(Type.String({ description: "Channel: slack, discord, telegram (for link)" })),
306
648
  actor_id: Type.Optional(Type.String({ description: "Channel actor ID (for link)" })),
307
649
  tenant_id: Type.Optional(Type.String({ description: "Channel tenant ID (for link)" })),
@@ -311,125 +653,181 @@ function registerServerTools(pi, opts) {
311
653
  reason: Type.Optional(Type.String({ description: "Unlink reason (for unlink)" })),
312
654
  include_inactive: Type.Optional(Type.Boolean({ description: "Include inactive bindings (for list)" })),
313
655
  });
314
- pi.registerTool({
315
- name: "mu_identity",
316
- label: "Identity",
317
- description: "Manage identity bindings. Actions: list (enumerate bindings), link (create binding), unlink (self-unlink).",
318
- parameters: IdentityParams,
319
- async execute(_toolCallId, params) {
320
- switch (params.action) {
321
- case "list": {
322
- const query = new URLSearchParams();
323
- if (params.include_inactive)
324
- query.set("include_inactive", "true");
325
- const data = await fetchMuJson(`/api/identities${query.size > 0 ? `?${query.toString()}` : ""}`);
326
- return textResult(toJsonText(data), { count: data.count });
656
+ if (opts.includeIdentityTool) {
657
+ pi.registerTool({
658
+ name: "mu_identity",
659
+ label: "Identity",
660
+ description: opts.allowIdentityMutations
661
+ ? "Manage identity bindings. Actions: list (enumerate bindings), link (create binding), unlink (self-unlink)."
662
+ : "Read identity bindings. Action: list.",
663
+ parameters: IdentityParams,
664
+ async execute(_toolCallId, params) {
665
+ switch (params.action) {
666
+ case "list": {
667
+ const query = new URLSearchParams();
668
+ if (params.include_inactive)
669
+ query.set("include_inactive", "true");
670
+ const data = await fetchMuJson(`/api/identities${query.size > 0 ? `?${query.toString()}` : ""}`);
671
+ const bindings = asArray(data.bindings)
672
+ .map((binding) => asRecord(binding))
673
+ .filter((binding) => binding != null)
674
+ .map((binding) => summarizeBinding(binding));
675
+ return textResult(toJsonText({ count: bindings.length, bindings }), {
676
+ count: data.count,
677
+ bindings: data.bindings,
678
+ });
679
+ }
680
+ case "link": {
681
+ if (!opts.allowIdentityMutations) {
682
+ return textResult("identity mutations are disabled in query-only mode; use list/get workflows or approved operator mutation flow.", { blocked: true, reason: "identity_query_only_mode" });
683
+ }
684
+ const channel = trimOrNull(params.channel);
685
+ const actorId = trimOrNull(params.actor_id);
686
+ const tenantId = trimOrNull(params.tenant_id);
687
+ if (!channel)
688
+ return textResult("Error: channel required for link");
689
+ if (!actorId)
690
+ return textResult("Error: actor_id required for link");
691
+ if (!tenantId)
692
+ return textResult("Error: tenant_id required for link");
693
+ const body = {
694
+ channel,
695
+ actor_id: actorId,
696
+ tenant_id: tenantId,
697
+ };
698
+ const role = trimOrNull(params.role);
699
+ if (role)
700
+ body.role = role;
701
+ const bindingId = trimOrNull(params.binding_id);
702
+ if (bindingId)
703
+ body.binding_id = bindingId;
704
+ const result = await fetchMuJson("/api/identities/link", {
705
+ method: "POST",
706
+ body,
707
+ });
708
+ return textResult(toJsonText(result), result);
709
+ }
710
+ case "unlink": {
711
+ if (!opts.allowIdentityMutations) {
712
+ return textResult("identity mutations are disabled in query-only mode; use list/get workflows or approved operator mutation flow.", { blocked: true, reason: "identity_query_only_mode" });
713
+ }
714
+ const bindingId = trimOrNull(params.binding_id);
715
+ const actorBindingId = trimOrNull(params.actor_binding_id);
716
+ if (!bindingId)
717
+ return textResult("Error: binding_id required for unlink");
718
+ if (!actorBindingId)
719
+ return textResult("Error: actor_binding_id required for unlink");
720
+ const body = {
721
+ binding_id: bindingId,
722
+ actor_binding_id: actorBindingId,
723
+ };
724
+ const reason = trimOrNull(params.reason);
725
+ if (reason)
726
+ body.reason = reason;
727
+ const result = await fetchMuJson("/api/identities/unlink", {
728
+ method: "POST",
729
+ body,
730
+ });
731
+ return textResult(toJsonText(result), result);
732
+ }
733
+ default:
734
+ return textResult(`Unknown action: ${params.action}`);
327
735
  }
328
- case "link": {
329
- const channel = trimOrNull(params.channel);
330
- const actorId = trimOrNull(params.actor_id);
331
- const tenantId = trimOrNull(params.tenant_id);
332
- if (!channel)
333
- return textResult("Error: channel required for link");
334
- if (!actorId)
335
- return textResult("Error: actor_id required for link");
336
- if (!tenantId)
337
- return textResult("Error: tenant_id required for link");
338
- const body = {
339
- channel,
340
- actor_id: actorId,
341
- tenant_id: tenantId,
342
- };
343
- const role = trimOrNull(params.role);
344
- if (role)
345
- body.role = role;
346
- const bindingId = trimOrNull(params.binding_id);
347
- if (bindingId)
348
- body.binding_id = bindingId;
349
- const result = await fetchMuJson("/api/identities/link", {
350
- method: "POST",
351
- body,
352
- });
353
- return textResult(toJsonText(result), result);
736
+ },
737
+ });
738
+ }
739
+ if (opts.includeStatusTool) {
740
+ registerMuSubcommand(pi, {
741
+ subcommand: "status",
742
+ summary: "Show concise mu server status",
743
+ usage: "/mu status",
744
+ handler: async (_args, ctx) => {
745
+ try {
746
+ const status = await fetchMuStatus();
747
+ ctx.ui.notify(summarizeStatus(status), "info");
354
748
  }
355
- case "unlink": {
356
- const bindingId = trimOrNull(params.binding_id);
357
- const actorBindingId = trimOrNull(params.actor_binding_id);
358
- if (!bindingId)
359
- return textResult("Error: binding_id required for unlink");
360
- if (!actorBindingId)
361
- return textResult("Error: actor_binding_id required for unlink");
362
- const body = {
363
- binding_id: bindingId,
364
- actor_binding_id: actorBindingId,
365
- };
366
- const reason = trimOrNull(params.reason);
367
- if (reason)
368
- body.reason = reason;
369
- const result = await fetchMuJson("/api/identities/unlink", {
370
- method: "POST",
371
- body,
372
- });
373
- return textResult(toJsonText(result), result);
749
+ catch (err) {
750
+ ctx.ui.notify(`Failed: ${err instanceof Error ? err.message : String(err)}`, "error");
374
751
  }
375
- default:
376
- return textResult(`Unknown action: ${params.action}`);
377
- }
378
- },
379
- });
380
- registerMuSubcommand(pi, {
381
- subcommand: "status",
382
- summary: "Show concise mu server status",
383
- usage: "/mu status",
384
- handler: async (_args, ctx) => {
385
- try {
386
- const status = await fetchMuStatus();
387
- ctx.ui.notify(summarizeStatus(status), "info");
388
- }
389
- catch (err) {
390
- ctx.ui.notify(`Failed: ${err instanceof Error ? err.message : String(err)}`, "error");
391
- }
392
- },
393
- });
394
- registerMuSubcommand(pi, {
395
- subcommand: "control",
396
- summary: "Show control-plane adapter/runtime status",
397
- usage: "/mu control",
398
- handler: async (_args, ctx) => {
399
- try {
400
- const status = await fetchMuStatus();
401
- const cp = status.control_plane;
402
- const routes = cpRoutesFromStatus(cp.routes, cp.adapters);
403
- const lines = [
404
- `control_plane: ${cp.active ? "active" : "inactive"}`,
405
- `adapters: ${cp.adapters.length > 0 ? cp.adapters.join(", ") : "(none)"}`,
406
- `routes: ${routes.length > 0 ? routes.map((entry) => `${entry.name}:${entry.route}`).join(", ") : "(none)"}`,
407
- generationSummary(cp.generation),
408
- observabilitySummary(cp.observability.counters),
409
- ];
410
- ctx.ui.notify(lines.join("\n"), "info");
411
- }
412
- catch (err) {
413
- ctx.ui.notify(`Failed: ${err instanceof Error ? err.message : String(err)}`, "error");
414
- }
415
- },
416
- });
752
+ },
753
+ });
754
+ }
755
+ if (opts.includeControlPlaneTool) {
756
+ registerMuSubcommand(pi, {
757
+ subcommand: "control",
758
+ summary: "Show control-plane adapter/runtime status",
759
+ usage: "/mu control",
760
+ handler: async (_args, ctx) => {
761
+ try {
762
+ const status = await fetchMuStatus();
763
+ const cp = status.control_plane;
764
+ const routes = cpRoutesFromStatus(cp.routes, cp.adapters);
765
+ const lines = [
766
+ `control_plane: ${cp.active ? "active" : "inactive"}`,
767
+ `adapters: ${cp.adapters.length > 0 ? cp.adapters.join(", ") : "(none)"}`,
768
+ `routes: ${routes.length > 0 ? routes.map((entry) => `${entry.name}:${entry.route}`).join(", ") : "(none)"}`,
769
+ generationSummary(cp.generation),
770
+ observabilitySummary(cp.observability.counters),
771
+ ];
772
+ ctx.ui.notify(lines.join("\n"), "info");
773
+ }
774
+ catch (err) {
775
+ ctx.ui.notify(`Failed: ${err instanceof Error ? err.message : String(err)}`, "error");
776
+ }
777
+ },
778
+ });
779
+ }
417
780
  }
418
781
  export function serverToolsExtension(pi, opts = {}) {
419
782
  registerServerTools(pi, {
420
783
  allowForumPost: opts.allowForumPost ?? true,
784
+ allowIssueMutations: opts.allowIssueMutations ?? true,
785
+ allowIdentityMutations: opts.allowIdentityMutations ?? true,
786
+ includeStatusTool: opts.includeStatusTool ?? true,
787
+ includeControlPlaneTool: opts.includeControlPlaneTool ?? true,
788
+ includeIssuesTool: opts.includeIssuesTool ?? true,
789
+ includeForumTool: opts.includeForumTool ?? true,
790
+ includeEventsTool: opts.includeEventsTool ?? true,
791
+ includeIdentityTool: opts.includeIdentityTool ?? true,
421
792
  toolIntroLine: opts.toolIntroLine ??
422
793
  "Tools: mu_status, mu_control_plane, mu_issues, mu_forum, mu_events, mu_runs, mu_activities, mu_heartbeats, mu_cron, mu_identity.",
794
+ usageLine: opts.usageLine ??
795
+ "Use these tools to inspect repository state and control-plane runtime before advising users.",
423
796
  extraSystemPromptLines: opts.extraSystemPromptLines ?? [],
424
797
  });
425
798
  }
426
799
  export function serverToolsReadOnlyExtension(pi) {
427
800
  registerServerTools(pi, {
428
801
  allowForumPost: false,
429
- toolIntroLine: "Tools: mu_status, mu_control_plane, mu_issues, mu_forum(read/topics), mu_events, mu_runs(read), mu_messaging_setup, mu_identity.",
802
+ allowIssueMutations: false,
803
+ allowIdentityMutations: false,
804
+ includeStatusTool: true,
805
+ includeControlPlaneTool: true,
806
+ includeIssuesTool: true,
807
+ includeForumTool: true,
808
+ includeEventsTool: true,
809
+ includeIdentityTool: true,
810
+ toolIntroLine: "Tools: mu_status, mu_control_plane, mu_issues, mu_forum(read/topics), mu_events, mu_runs(read), mu_messaging_setup(read), mu_identity(list).",
811
+ usageLine: "Use these tools to inspect repository state and control-plane runtime before advising users.",
430
812
  extraSystemPromptLines: [
431
813
  "You have Bash, Read, Write, and Edit tools. Use them to run mu CLI commands, edit config files, and complete tasks directly.",
432
814
  ],
433
815
  });
434
816
  }
817
+ export function serverToolsIssueForumExtension(pi) {
818
+ registerServerTools(pi, {
819
+ allowForumPost: true,
820
+ allowIssueMutations: true,
821
+ allowIdentityMutations: false,
822
+ includeStatusTool: false,
823
+ includeControlPlaneTool: false,
824
+ includeIssuesTool: true,
825
+ includeForumTool: true,
826
+ includeEventsTool: false,
827
+ includeIdentityTool: false,
828
+ toolIntroLine: "Tools: mu_issues, mu_forum.",
829
+ usageLine: "Use these tools to coordinate issue status and forum updates for your assigned work.",
830
+ extraSystemPromptLines: [],
831
+ });
832
+ }
435
833
  export default serverToolsExtension;