pi-subagents 0.40.0 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +246 -525
  3. package/agents/oracle.md +1 -0
  4. package/package.json +12 -4
  5. package/prompts/parallel-context-build.md +1 -1
  6. package/prompts/parallel-handoff-plan.md +1 -1
  7. package/prompts/review-loop.md +1 -1
  8. package/skills/pi-subagents/SKILL.md +6 -6
  9. package/skills/pi-subagents/references/constraints-and-recipes.md +19 -26
  10. package/skills/pi-subagents/references/execution-controls.md +98 -97
  11. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  12. package/skills/pi-subagents/references/prompting-and-roles.md +18 -27
  13. package/src/agents/agent-management.ts +155 -65
  14. package/src/agents/agent-serializer.ts +19 -0
  15. package/src/agents/agents.ts +154 -71
  16. package/src/agents/chain-serializer.ts +10 -7
  17. package/src/agents/frontmatter.ts +5 -3
  18. package/src/agents/identity.ts +1 -1
  19. package/src/agents/proactive-skills.ts +13 -10
  20. package/src/agents/skills.ts +23 -6
  21. package/src/api/control-channel.ts +4 -0
  22. package/src/api/delegation.ts +26 -194
  23. package/src/api/external-runs.ts +129 -0
  24. package/src/api/intercom-bridge.ts +3 -0
  25. package/src/api/pi-args.ts +5 -0
  26. package/src/api/preflight.ts +3 -3
  27. package/src/api/shared-types.ts +19 -0
  28. package/src/extension/config.ts +10 -0
  29. package/src/extension/control-notices.ts +5 -39
  30. package/src/extension/doctor.ts +10 -9
  31. package/src/extension/fanout-child.ts +7 -4
  32. package/src/extension/index.ts +232 -68
  33. package/src/extension/rpc.ts +18 -12
  34. package/src/extension/schemas.ts +48 -37
  35. package/src/extension/tool-description.ts +36 -86
  36. package/src/inspectors/herdr/actions.ts +229 -0
  37. package/src/inspectors/herdr/client.ts +130 -0
  38. package/src/inspectors/herdr/inspector-runner.ts +141 -0
  39. package/src/inspectors/herdr/project-panes.ts +154 -0
  40. package/src/integrations/herdr-status.ts +330 -0
  41. package/src/intercom/intercom-bridge.ts +3 -2
  42. package/src/intercom/result-intercom.ts +5 -1
  43. package/src/missions/actions.ts +372 -0
  44. package/src/missions/lifecycle.ts +314 -0
  45. package/src/missions/store.ts +442 -0
  46. package/src/missions/types.ts +135 -0
  47. package/src/policy/authority.ts +46 -0
  48. package/src/profiles/profiles.ts +29 -3
  49. package/src/runs/background/async-execution.ts +98 -49
  50. package/src/runs/background/async-job-tracker.ts +10 -2
  51. package/src/runs/background/async-resume.ts +6 -6
  52. package/src/runs/background/async-status.ts +29 -1
  53. package/src/runs/background/auto-drain.ts +3 -3
  54. package/src/runs/background/chain-append.ts +3 -2
  55. package/src/runs/background/control-channel.ts +9 -7
  56. package/src/runs/background/fleet-view.ts +3 -4
  57. package/src/runs/background/notify.ts +2 -1
  58. package/src/runs/background/process-terminal.ts +5 -5
  59. package/src/runs/background/result-watcher.ts +13 -5
  60. package/src/runs/background/run-id-resolver.ts +3 -3
  61. package/src/runs/background/run-status.ts +35 -8
  62. package/src/runs/background/scheduled-runs.ts +602 -375
  63. package/src/runs/background/stale-run-reconciler.ts +3 -3
  64. package/src/runs/background/subagent-runner.ts +608 -445
  65. package/src/runs/background/subagent-wait.ts +50 -9
  66. package/src/runs/background/wait-subscriptions.ts +253 -0
  67. package/src/runs/background/wait-tool.ts +12 -4
  68. package/src/runs/foreground/async-steering-action.ts +3 -3
  69. package/src/runs/foreground/chain-clarify.ts +8 -4
  70. package/src/runs/foreground/chain-execution.ts +56 -30
  71. package/src/runs/foreground/execution.ts +15 -2
  72. package/src/runs/foreground/subagent-executor.ts +1023 -273
  73. package/src/runs/shared/acceptance.ts +28 -6
  74. package/src/runs/shared/child-protocol.ts +302 -22
  75. package/src/runs/shared/dynamic-fanout.ts +1 -1
  76. package/src/runs/shared/external-cli-runner.ts +130 -0
  77. package/src/runs/shared/long-running-guard.ts +42 -1
  78. package/src/runs/shared/nested-events.ts +59 -5
  79. package/src/runs/shared/nested-render.ts +9 -4
  80. package/src/runs/shared/parallel-handoff.ts +86 -2
  81. package/src/runs/shared/parallel-utils.ts +11 -2
  82. package/src/runs/shared/permissions.ts +95 -0
  83. package/src/runs/shared/pi-args.ts +11 -1
  84. package/src/runs/shared/pi-spawn.ts +11 -1
  85. package/src/runs/shared/run-history.ts +1 -1
  86. package/src/runs/shared/subagent-prompt-runtime.ts +36 -5
  87. package/src/runs/shared/subagent-startup-retry.ts +5 -2
  88. package/src/runs/shared/turn-budget.ts +6 -6
  89. package/src/runs/shared/worktree.ts +122 -12
  90. package/src/shared/accessible-dir.ts +29 -7
  91. package/src/shared/artifacts.ts +18 -1
  92. package/src/shared/fork-context.ts +3 -2
  93. package/src/shared/launch-contract.ts +1 -0
  94. package/src/shared/settings.ts +10 -0
  95. package/src/shared/types.ts +156 -14
  96. package/src/shared/utils.ts +8 -6
  97. package/src/slash/delegation-adapters.ts +32 -194
  98. package/src/slash/delegation-request.ts +43 -126
  99. package/src/slash/prompt-template-bridge.ts +158 -205
  100. package/src/slash/prompt-workflows.ts +21 -57
  101. package/src/slash/slash-bridge.ts +14 -0
  102. package/src/slash/slash-commands.ts +31 -632
  103. package/src/slash/subagents-admin.ts +18 -14
  104. package/src/tui/fleet-status.ts +156 -21
  105. package/src/tui/fleet-transcript.ts +110 -5
  106. package/src/tui/fleet.ts +56 -24
  107. package/src/tui/render.ts +291 -109
  108. package/src/types/pi-runtime-compat.d.ts +14 -0
  109. package/src/watchdog/lsp-diagnostics.ts +12 -7
  110. package/src/watchdog/model-selection.ts +2 -2
  111. package/src/watchdog/permission-arbiter.ts +145 -0
  112. package/src/watchdog/register-child.ts +1 -1
  113. package/src/watchdog/register-main.ts +1 -1
  114. package/src/watchdog/review.ts +4 -1
  115. package/src/watchdog/runtime.ts +3 -2
  116. package/src/workflows/chat-progress.ts +140 -0
  117. package/src/workflows/scripted-workflow.ts +415 -0
  118. package/agents/advisor.md +0 -73
  119. package/src/extension/chain-validation.ts +0 -181
@@ -0,0 +1,372 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
4
+ import type { Details } from "../shared/types.ts";
5
+ import {
6
+ MISSION_STATUSES,
7
+ type MissionArtifact,
8
+ type MissionArtifactKind,
9
+ type MissionReceipt,
10
+ type MissionReceiptKind,
11
+ type MissionReceiptStatus,
12
+ type MissionRecord,
13
+ type MissionRunMode,
14
+ type MissionStatus,
15
+ type MissionStoreConfig,
16
+ type MissionStoreLocation,
17
+ type MissionUpdateInput,
18
+ } from "./types.ts";
19
+ import {
20
+ createMission,
21
+ listGlobalMissions,
22
+ listMissions,
23
+ missionRecordPath,
24
+ readMission,
25
+ resolveMissionStoreLocation,
26
+ updateMission,
27
+ validateMissionId,
28
+ } from "./store.ts";
29
+
30
+ export const MISSION_ACTIONS = [
31
+ "mission.create",
32
+ "mission.list",
33
+ "mission.show",
34
+ "mission.update",
35
+ "mission.attach-run",
36
+ "mission.close",
37
+ ] as const;
38
+
39
+ export type MissionAction = typeof MISSION_ACTIONS[number];
40
+
41
+ export interface MissionLaunchInput {
42
+ title: string;
43
+ goal?: string;
44
+ labels?: string[];
45
+ }
46
+
47
+ export interface MissionUpdateToolInput {
48
+ title?: string;
49
+ goal?: string;
50
+ status?: MissionStatus;
51
+ summary?: string;
52
+ labels?: string[];
53
+ artifacts?: MissionArtifact[];
54
+ receipts?: Array<Omit<MissionReceipt, "createdAt">>;
55
+ decisions?: Array<{
56
+ title: string;
57
+ prompt?: string;
58
+ options?: string[];
59
+ recommendation?: string;
60
+ }>;
61
+ }
62
+
63
+ export interface MissionActionParams {
64
+ missionId?: string;
65
+ mission?: unknown;
66
+ missionUpdate?: unknown;
67
+ missionStatus?: string;
68
+ missionScope?: string;
69
+ id?: string;
70
+ runId?: string;
71
+ dir?: string;
72
+ runMode?: string;
73
+ runStatus?: string;
74
+ agent?: string;
75
+ summary?: string;
76
+ }
77
+
78
+ interface MissionActionContext {
79
+ cwd: string;
80
+ currentSessionId?: string;
81
+ config?: MissionStoreConfig;
82
+ agentDir?: string;
83
+ }
84
+
85
+ function textResult(text: string, details: Details): AgentToolResult<Details> {
86
+ return { content: [{ type: "text", text }], details };
87
+ }
88
+
89
+ function requireMissionId(params: MissionActionParams): string {
90
+ return validateMissionId(params.missionId, "missionId");
91
+ }
92
+
93
+ function validateStatus(value: unknown, label: string): MissionStatus {
94
+ if (typeof value !== "string" || !(MISSION_STATUSES as readonly string[]).includes(value)) {
95
+ throw new Error(`${label} must be one of ${MISSION_STATUSES.join(", ")}`);
96
+ }
97
+ return value as MissionStatus;
98
+ }
99
+
100
+ export function validateMissionLaunch(value: unknown): MissionLaunchInput {
101
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("mission must be an object");
102
+ const input = value as Record<string, unknown>;
103
+ for (const key of Object.keys(input)) {
104
+ if (key !== "title" && key !== "goal" && key !== "labels") throw new Error(`mission.${key} is unknown`);
105
+ }
106
+ if (typeof input.title !== "string" || !input.title.trim()) throw new Error("mission.title must be a non-empty string");
107
+ if (input.goal !== undefined && (typeof input.goal !== "string" || !input.goal.trim())) throw new Error("mission.goal must be a non-empty string");
108
+ if (input.labels !== undefined && (!Array.isArray(input.labels) || input.labels.some((label) => typeof label !== "string" || !label.trim()))) {
109
+ throw new Error("mission.labels must contain only non-empty strings");
110
+ }
111
+ return {
112
+ title: input.title.trim(),
113
+ ...(typeof input.goal === "string" ? { goal: input.goal.trim() } : {}),
114
+ ...(Array.isArray(input.labels) ? { labels: input.labels as string[] } : {}),
115
+ };
116
+ }
117
+
118
+ function validateArtifact(value: unknown, index: number): MissionArtifact {
119
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`missionUpdate.artifacts[${index}] must be an object`);
120
+ const input = value as Record<string, unknown>;
121
+ const kinds: MissionArtifactKind[] = ["status", "output", "patch", "manifest", "review", "note", "other"];
122
+ if (typeof input.kind !== "string" || !kinds.includes(input.kind as MissionArtifactKind)) throw new Error(`missionUpdate.artifacts[${index}].kind is invalid`);
123
+ if (typeof input.path !== "string" || !input.path.trim()) throw new Error(`missionUpdate.artifacts[${index}].path must be a non-empty string`);
124
+ if (input.description !== undefined && (typeof input.description !== "string" || !input.description.trim())) throw new Error(`missionUpdate.artifacts[${index}].description must be a non-empty string`);
125
+ return {
126
+ kind: input.kind as MissionArtifactKind,
127
+ path: input.path,
128
+ ...(typeof input.description === "string" ? { description: input.description } : {}),
129
+ };
130
+ }
131
+
132
+ function validateReceipt(value: unknown, index: number): Omit<MissionReceipt, "createdAt"> {
133
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`missionUpdate.receipts[${index}] must be an object`);
134
+ const input = value as Record<string, unknown>;
135
+ for (const key of Object.keys(input)) {
136
+ if (!['kind', 'status', 'title', 'url', 'description'].includes(key)) throw new Error(`missionUpdate.receipts[${index}].${key} is unknown`);
137
+ }
138
+ const kinds: MissionReceiptKind[] = ["pull_request", "ci", "deployment", "release"];
139
+ const statuses: MissionReceiptStatus[] = ["pending", "ready", "succeeded", "failed"];
140
+ if (typeof input.kind !== "string" || !kinds.includes(input.kind as MissionReceiptKind)) throw new Error(`missionUpdate.receipts[${index}].kind is invalid`);
141
+ if (typeof input.status !== "string" || !statuses.includes(input.status as MissionReceiptStatus)) throw new Error(`missionUpdate.receipts[${index}].status is invalid`);
142
+ for (const field of ["title", "url"] as const) {
143
+ if (typeof input[field] !== "string" || !input[field].trim()) throw new Error(`missionUpdate.receipts[${index}].${field} must be a non-empty string`);
144
+ }
145
+ try {
146
+ new URL(input.url as string);
147
+ } catch {
148
+ throw new Error(`missionUpdate.receipts[${index}].url must be an absolute URL`);
149
+ }
150
+ if (input.description !== undefined && (typeof input.description !== "string" || !input.description.trim())) throw new Error(`missionUpdate.receipts[${index}].description must be a non-empty string`);
151
+ return {
152
+ kind: input.kind as MissionReceiptKind,
153
+ status: input.status as MissionReceiptStatus,
154
+ title: (input.title as string).trim(),
155
+ url: (input.url as string).trim(),
156
+ ...(typeof input.description === "string" ? { description: input.description.trim() } : {}),
157
+ };
158
+ }
159
+
160
+ function validateMissionUpdate(value: unknown): MissionUpdateInput {
161
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("missionUpdate must be an object");
162
+ const input = value as Record<string, unknown>;
163
+ for (const key of Object.keys(input)) {
164
+ if (!["title", "goal", "status", "summary", "labels", "artifacts", "receipts", "decisions"].includes(key)) throw new Error(`missionUpdate.${key} is unknown`);
165
+ }
166
+ const update: MissionUpdateInput = {};
167
+ for (const field of ["title", "goal", "summary"] as const) {
168
+ const candidate = input[field];
169
+ if (candidate === undefined) continue;
170
+ if (typeof candidate !== "string" || !candidate.trim()) throw new Error(`missionUpdate.${field} must be a non-empty string`);
171
+ update[field] = candidate.trim();
172
+ }
173
+ if (input.status !== undefined) update.status = validateStatus(input.status, "missionUpdate.status");
174
+ if (input.labels !== undefined) {
175
+ if (!Array.isArray(input.labels) || input.labels.some((label) => typeof label !== "string" || !label.trim())) throw new Error("missionUpdate.labels must contain only non-empty strings");
176
+ update.labels = input.labels as string[];
177
+ }
178
+ if (input.artifacts !== undefined) {
179
+ if (!Array.isArray(input.artifacts)) throw new Error("missionUpdate.artifacts must be an array");
180
+ update.addArtifacts = input.artifacts.map(validateArtifact);
181
+ }
182
+ if (input.receipts !== undefined) {
183
+ if (!Array.isArray(input.receipts)) throw new Error("missionUpdate.receipts must be an array");
184
+ update.addReceipts = input.receipts.map(validateReceipt);
185
+ }
186
+ if (input.decisions !== undefined) {
187
+ if (!Array.isArray(input.decisions)) throw new Error("missionUpdate.decisions must be an array");
188
+ update.addDecisions = input.decisions.map((value, index) => {
189
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`missionUpdate.decisions[${index}] must be an object`);
190
+ const decision = value as Record<string, unknown>;
191
+ if (typeof decision.title !== "string" || !decision.title.trim()) throw new Error(`missionUpdate.decisions[${index}].title must be a non-empty string`);
192
+ if (decision.options !== undefined && (!Array.isArray(decision.options) || decision.options.some((option) => typeof option !== "string" || !option.trim()))) throw new Error(`missionUpdate.decisions[${index}].options must contain only non-empty strings`);
193
+ return {
194
+ title: decision.title.trim(),
195
+ ...(typeof decision.prompt === "string" && decision.prompt.trim() ? { prompt: decision.prompt } : {}),
196
+ ...(Array.isArray(decision.options) ? { options: decision.options as string[] } : {}),
197
+ ...(typeof decision.recommendation === "string" && decision.recommendation.trim() ? { recommendation: decision.recommendation } : {}),
198
+ };
199
+ });
200
+ }
201
+ if (Object.keys(update).length === 0) throw new Error("missionUpdate must include at least one supported field");
202
+ return update;
203
+ }
204
+
205
+ function refreshLinkedRunStatus(location: MissionStoreLocation, record: MissionRecord): { record: MissionRecord; warnings: string[] } {
206
+ const warnings: string[] = [];
207
+ const updates = record.runs.flatMap((run) => {
208
+ if (!run.asyncDir) return [];
209
+ const statusPath = path.join(run.asyncDir, "status.json");
210
+ if (!fs.existsSync(statusPath)) return [];
211
+ let status: unknown;
212
+ try {
213
+ status = JSON.parse(fs.readFileSync(statusPath, "utf-8"));
214
+ } catch (error) {
215
+ warnings.push(`Failed to read linked run status '${statusPath}': ${error instanceof Error ? error.message : String(error)}`);
216
+ return [];
217
+ }
218
+ if (!status || typeof status !== "object" || Array.isArray(status)) {
219
+ warnings.push(`Linked run status '${statusPath}' must be an object`);
220
+ return [];
221
+ }
222
+ const state = (status as { state?: unknown }).state;
223
+ if (typeof state !== "string" || !state.trim()) {
224
+ warnings.push(`Linked run status '${statusPath}' is missing state`);
225
+ return [];
226
+ }
227
+ if (state === run.status) return [];
228
+ return [{
229
+ ...run,
230
+ status: state,
231
+ ...(!["queued", "running"].includes(state) && !run.completedAt ? { completedAt: new Date().toISOString() } : {}),
232
+ }];
233
+ });
234
+ if (updates.length === 0) return { record, warnings };
235
+ const merged = record.runs.map((run) => updates.find((update) => update.runId === run.runId && update.childIndex === run.childIndex) ?? run);
236
+ const states = merged.map((run) => run.status);
237
+ const status: MissionStatus = states.some((state) => state === "queued" || state === "running" || state === "active")
238
+ ? "active"
239
+ : states.some((state) => state === "paused")
240
+ ? "waiting"
241
+ : states.some((state) => state === "failed")
242
+ ? "failed"
243
+ : states.some((state) => state === "stopped" || state === "rejected" || state === "cancelled")
244
+ ? "cancelled"
245
+ : states.length > 0 && states.every((state) => state === "complete" || state === "completed")
246
+ ? "completed"
247
+ : record.status;
248
+ return { record: updateMission(location, record.id, { status, addRuns: updates }), warnings };
249
+ }
250
+
251
+ function formatMission(record: MissionRecord): string {
252
+ const lines = [
253
+ `Mission: ${record.id}`,
254
+ `Title: ${record.title}`,
255
+ `Status: ${record.status}`,
256
+ `Goal: ${record.goal}`,
257
+ `Updated: ${record.updatedAt}`,
258
+ ];
259
+ if (record.summary) lines.push(`Summary: ${record.summary}`);
260
+ if (record.labels?.length) lines.push(`Labels: ${record.labels.join(", ")}`);
261
+ if (record.runs.length) {
262
+ lines.push("Runs:");
263
+ for (const run of record.runs) lines.push(` ${run.runId} (${run.mode}${run.status ? `, ${run.status}` : ""})${run.asyncDir ? ` — ${run.asyncDir}` : ""}`);
264
+ }
265
+ if (record.decisions.length) {
266
+ lines.push("Decisions:");
267
+ for (const decision of record.decisions) lines.push(` ${decision.id}: ${decision.status} — ${decision.title}`);
268
+ }
269
+ if (record.artifacts.length) {
270
+ lines.push("Artifacts:");
271
+ for (const artifact of record.artifacts) lines.push(` ${artifact.kind}: ${artifact.path}`);
272
+ }
273
+ if (record.receipts.length) {
274
+ lines.push("Delivery receipts:");
275
+ for (const receipt of record.receipts) lines.push(` ${receipt.kind} (${receipt.status}): ${receipt.title} — ${receipt.url}`);
276
+ }
277
+ return lines.join("\n");
278
+ }
279
+
280
+ export function handleMissionAction(
281
+ action: MissionAction,
282
+ params: MissionActionParams,
283
+ ctx: MissionActionContext,
284
+ ): AgentToolResult<Details> {
285
+ const location = resolveMissionStoreLocation({
286
+ projectRoot: ctx.cwd,
287
+ ...(ctx.config ? { config: ctx.config } : {}),
288
+ ...(ctx.agentDir ? { agentDir: ctx.agentDir } : {}),
289
+ });
290
+ if (action === "mission.create") {
291
+ const mission = validateMissionLaunch(params.mission);
292
+ const record = createMission(location, {
293
+ title: mission.title,
294
+ goal: mission.goal ?? mission.title,
295
+ status: params.missionStatus ? validateStatus(params.missionStatus, "missionStatus") : "planned",
296
+ ...(mission.labels ? { labels: mission.labels } : {}),
297
+ ...(ctx.currentSessionId ? { ownerSessionId: ctx.currentSessionId } : {}),
298
+ }, new Date(), ctx.config?.retainTerminal);
299
+ return textResult(`Created mission ${record.id}: ${record.title}`, { mode: "management", results: [], missionId: record.id, missionPath: pathFor(record.id), mission: record });
300
+ }
301
+ if (action === "mission.list") {
302
+ if (params.missionScope !== undefined && params.missionScope !== "project" && params.missionScope !== "global") {
303
+ throw new Error("missionScope must be \"project\" or \"global\"");
304
+ }
305
+ if (params.missionScope === "global") {
306
+ const listed = listGlobalMissions(location.globalIndexDir);
307
+ const lines = listed.entries.length === 0
308
+ ? ["No indexed missions."]
309
+ : listed.entries.map((entry) => `${entry.missionId} ${entry.status}${entry.stale ? " [stale]" : ""} ${entry.title} ${entry.projectRoot}`);
310
+ if (listed.warnings.length) lines.push("", ...listed.warnings.map((warning) => `Warning: ${warning}`));
311
+ return textResult(lines.join("\n"), { mode: "management", results: [], missions: { globalEntries: listed.entries, warnings: listed.warnings } });
312
+ }
313
+ const listed = listMissions(location);
314
+ const lines = listed.records.length === 0
315
+ ? ["No project missions."]
316
+ : listed.records.map((record) => `${record.id} ${record.status} ${record.title} ${record.updatedAt}`);
317
+ if (listed.warnings.length) lines.push("", ...listed.warnings.map((warning) => `Warning: ${warning}`));
318
+ return textResult(lines.join("\n"), { mode: "management", results: [], missions: { records: listed.records, warnings: listed.warnings } });
319
+ }
320
+ if (action === "mission.show") {
321
+ const refreshed = refreshLinkedRunStatus(location, readMission(location, requireMissionId(params)));
322
+ const lines = [formatMission(refreshed.record)];
323
+ if (refreshed.warnings.length) lines.push("", ...refreshed.warnings.map((warning) => `Warning: ${warning}`));
324
+ return textResult(lines.join("\n"), {
325
+ mode: "management",
326
+ results: [],
327
+ missionId: refreshed.record.id,
328
+ missionPath: pathFor(refreshed.record.id),
329
+ mission: refreshed.record,
330
+ missions: { warnings: refreshed.warnings },
331
+ });
332
+ }
333
+ if (action === "mission.update") {
334
+ const record = updateMission(location, requireMissionId(params), validateMissionUpdate(params.missionUpdate));
335
+ return textResult(`Updated mission ${record.id}.\n\n${formatMission(record)}`, { mode: "management", results: [], missionId: record.id, missionPath: pathFor(record.id), mission: record });
336
+ }
337
+ if (action === "mission.attach-run") {
338
+ const missionId = requireMissionId(params);
339
+ const runId = params.runId ?? params.id;
340
+ if (typeof runId !== "string" || !runId.trim()) throw new Error("mission.attach-run requires runId or id");
341
+ const rawMode = params.runMode ?? "external";
342
+ if (!["single", "parallel", "chain", "workflow", "scheduled", "external"].includes(rawMode)) throw new Error("runMode is invalid");
343
+ const mode = rawMode as MissionRunMode;
344
+ if (params.dir !== undefined && !params.dir.trim()) throw new Error("dir must be a non-empty string");
345
+ if (params.runStatus !== undefined && !params.runStatus.trim()) throw new Error("runStatus must be a non-empty string");
346
+ const record = updateMission(location, missionId, {
347
+ status: "active",
348
+ addRuns: [{
349
+ runId: runId.trim(),
350
+ mode,
351
+ ...(params.dir ? { asyncDir: params.dir } : {}),
352
+ ...(params.agent ? { agent: params.agent } : {}),
353
+ ...(params.runStatus ? { status: params.runStatus } : {}),
354
+ startedAt: new Date().toISOString(),
355
+ }],
356
+ });
357
+ return textResult(`Attached run ${runId} to mission ${record.id}.`, { mode: "management", results: [], missionId: record.id, missionPath: pathFor(record.id), mission: record });
358
+ }
359
+ const missionId = requireMissionId(params);
360
+ const status = params.missionStatus ?? "completed";
361
+ if (status !== "completed" && status !== "failed" && status !== "cancelled") throw new Error("mission.close missionStatus must be completed, failed, or cancelled");
362
+ if (params.summary !== undefined && !params.summary.trim()) throw new Error("summary must be a non-empty string");
363
+ const record = updateMission(location, missionId, {
364
+ status,
365
+ ...(params.summary !== undefined ? { summary: params.summary.trim() } : {}),
366
+ });
367
+ return textResult(`Closed mission ${record.id} as ${record.status}.`, { mode: "management", results: [], missionId: record.id, missionPath: pathFor(record.id), mission: record });
368
+
369
+ function pathFor(missionId: string): string {
370
+ return missionRecordPath(location, missionId);
371
+ }
372
+ }