pi-mission-control 0.0.0-dev
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +205 -0
- package/agents/auditor.md +45 -0
- package/agents/worker.md +44 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +526 -0
- package/dist/index.js.map +1 -0
- package/dist/state.d.ts +265 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +474 -0
- package/dist/state.js.map +1 -0
- package/dist/tools/add-phase.d.ts +28 -0
- package/dist/tools/add-phase.d.ts.map +1 -0
- package/dist/tools/add-phase.js +69 -0
- package/dist/tools/add-phase.js.map +1 -0
- package/dist/tools/add-task.d.ts +30 -0
- package/dist/tools/add-task.d.ts.map +1 -0
- package/dist/tools/add-task.js +85 -0
- package/dist/tools/add-task.js.map +1 -0
- package/dist/tools/index.d.ts +13 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +16 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/init.d.ts +34 -0
- package/dist/tools/init.d.ts.map +1 -0
- package/dist/tools/init.js +75 -0
- package/dist/tools/init.js.map +1 -0
- package/dist/tools/mission-complete.d.ts +30 -0
- package/dist/tools/mission-complete.d.ts.map +1 -0
- package/dist/tools/mission-complete.js +85 -0
- package/dist/tools/mission-complete.js.map +1 -0
- package/dist/tools/mission-resume.d.ts +35 -0
- package/dist/tools/mission-resume.d.ts.map +1 -0
- package/dist/tools/mission-resume.js +87 -0
- package/dist/tools/mission-resume.js.map +1 -0
- package/dist/tools/scaffold.d.ts +24 -0
- package/dist/tools/scaffold.d.ts.map +1 -0
- package/dist/tools/scaffold.js +129 -0
- package/dist/tools/scaffold.js.map +1 -0
- package/dist/tools/update-phase.d.ts +33 -0
- package/dist/tools/update-phase.d.ts.map +1 -0
- package/dist/tools/update-phase.js +101 -0
- package/dist/tools/update-phase.js.map +1 -0
- package/dist/tools/update-task.d.ts +34 -0
- package/dist/tools/update-task.d.ts.map +1 -0
- package/dist/tools/update-task.js +104 -0
- package/dist/tools/update-task.js.map +1 -0
- package/dist/tui/dashboard.d.ts +146 -0
- package/dist/tui/dashboard.d.ts.map +1 -0
- package/dist/tui/dashboard.js +381 -0
- package/dist/tui/dashboard.js.map +1 -0
- package/dist/tui/header.d.ts +39 -0
- package/dist/tui/header.d.ts.map +1 -0
- package/dist/tui/header.js +62 -0
- package/dist/tui/header.js.map +1 -0
- package/dist/tui/idle-view.d.ts +44 -0
- package/dist/tui/idle-view.d.ts.map +1 -0
- package/dist/tui/idle-view.js +87 -0
- package/dist/tui/idle-view.js.map +1 -0
- package/dist/tui/index.d.ts +13 -0
- package/dist/tui/index.d.ts.map +1 -0
- package/dist/tui/index.js +15 -0
- package/dist/tui/index.js.map +1 -0
- package/dist/tui/past-runs.d.ts +49 -0
- package/dist/tui/past-runs.d.ts.map +1 -0
- package/dist/tui/past-runs.js +207 -0
- package/dist/tui/past-runs.js.map +1 -0
- package/dist/tui/phases-panel.d.ts +46 -0
- package/dist/tui/phases-panel.d.ts.map +1 -0
- package/dist/tui/phases-panel.js +161 -0
- package/dist/tui/phases-panel.js.map +1 -0
- package/dist/tui/progress-bar.d.ts +37 -0
- package/dist/tui/progress-bar.d.ts.map +1 -0
- package/dist/tui/progress-bar.js +123 -0
- package/dist/tui/progress-bar.js.map +1 -0
- package/dist/tui/styles.d.ts +8 -0
- package/dist/tui/styles.d.ts.map +1 -0
- package/dist/tui/styles.js +22 -0
- package/dist/tui/styles.js.map +1 -0
- package/dist/tui/tasks-panel.d.ts +48 -0
- package/dist/tui/tasks-panel.d.ts.map +1 -0
- package/dist/tui/tasks-panel.js +191 -0
- package/dist/tui/tasks-panel.js.map +1 -0
- package/package.json +42 -0
- package/skills/mission-memory/SKILL.md +88 -0
- package/skills/mission-orchestrator/SKILL.md +167 -0
- package/skills/mission-pm/SKILL.md +83 -0
- package/skills/mission-research/SKILL.md +66 -0
- package/skills/mission-tech-lead/SKILL.md +68 -0
- package/src/index.ts +659 -0
- package/src/state.ts +623 -0
- package/src/tools/add-phase.ts +98 -0
- package/src/tools/add-task.ts +121 -0
- package/src/tools/index.ts +18 -0
- package/src/tools/init.ts +109 -0
- package/src/tools/mission-complete.ts +118 -0
- package/src/tools/mission-resume.ts +119 -0
- package/src/tools/scaffold.ts +167 -0
- package/src/tools/update-phase.ts +140 -0
- package/src/tools/update-task.ts +145 -0
- package/src/tui/dashboard.ts +441 -0
- package/src/tui/header.ts +85 -0
- package/src/tui/idle-view.ts +114 -0
- package/src/tui/index.ts +20 -0
- package/src/tui/past-runs.ts +261 -0
- package/src/tui/phases-panel.ts +199 -0
- package/src/tui/progress-bar.ts +152 -0
- package/src/tui/styles.ts +27 -0
- package/src/tui/tasks-panel.ts +228 -0
- package/templates/state.json +5 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-mission-control Extension Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Visual mission orchestration with agent hierarchy and durable state.
|
|
5
|
+
* Provides /mission command and tools for mission management.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
import { existsSync } from "fs";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
import { Type } from "@sinclair/typebox";
|
|
12
|
+
import type {
|
|
13
|
+
ExtensionAPI,
|
|
14
|
+
ExtensionContext,
|
|
15
|
+
ExtensionCommandContext,
|
|
16
|
+
SessionStartEvent,
|
|
17
|
+
} from "@mariozechner/pi-coding-agent";
|
|
18
|
+
import { withFileMutationQueue } from "@mariozechner/pi-coding-agent";
|
|
19
|
+
import type { Model } from "@mariozechner/pi-ai";
|
|
20
|
+
|
|
21
|
+
// Define event types locally (not exported from main pi-coding-agent index)
|
|
22
|
+
interface ResourcesDiscoverEvent {
|
|
23
|
+
type: "resources_discover";
|
|
24
|
+
cwd: string;
|
|
25
|
+
reason: "startup" | "reload";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ResourcesDiscoverResult {
|
|
29
|
+
skillPaths?: string[];
|
|
30
|
+
promptPaths?: string[];
|
|
31
|
+
themePaths?: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// State and tools
|
|
35
|
+
import {
|
|
36
|
+
readState,
|
|
37
|
+
writeState,
|
|
38
|
+
defaultState,
|
|
39
|
+
isScaffolded,
|
|
40
|
+
getRunFilePath,
|
|
41
|
+
getStateFilePath,
|
|
42
|
+
getProjectRoot,
|
|
43
|
+
} from "./state.js";
|
|
44
|
+
|
|
45
|
+
import {
|
|
46
|
+
missionScaffold,
|
|
47
|
+
missionInit,
|
|
48
|
+
addPhase,
|
|
49
|
+
addTask,
|
|
50
|
+
updatePhase,
|
|
51
|
+
updateTask,
|
|
52
|
+
missionComplete,
|
|
53
|
+
missionResume,
|
|
54
|
+
} from "./tools/index.js";
|
|
55
|
+
import { MissionDashboard } from "./tui/dashboard.js";
|
|
56
|
+
import { readText, writeText } from "./state.js";
|
|
57
|
+
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// TypeBox Schemas for Agent Tools
|
|
60
|
+
// ============================================================================
|
|
61
|
+
|
|
62
|
+
const AddPhaseSchema = Type.Object({
|
|
63
|
+
name: Type.String({ description: "Human-readable phase name (e.g., 'Auth Backend')" }),
|
|
64
|
+
file: Type.String({ description: "Path to phase definition/reference file" }),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const AddTaskSchema = Type.Object({
|
|
68
|
+
phase_id: Type.String({ description: "ID of the phase to add task to (e.g., 'phase1')" }),
|
|
69
|
+
name: Type.String({ description: "Human-readable task name (e.g., 'Setup DB Schema')" }),
|
|
70
|
+
file: Type.String({ description: "Path to task contract file" }),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const UpdatePhaseSchema = Type.Object({
|
|
74
|
+
phase_id: Type.String({ description: "ID of the phase to update" }),
|
|
75
|
+
status: Type.Union([
|
|
76
|
+
Type.Literal("pending"),
|
|
77
|
+
Type.Literal("in_progress"),
|
|
78
|
+
Type.Literal("done"),
|
|
79
|
+
Type.Literal("removed"),
|
|
80
|
+
], { description: "New status for the phase" }),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const UpdateTaskSchema = Type.Object({
|
|
84
|
+
task_id: Type.String({ description: "ID of the task to update (e.g., 'phase1-task1')" }),
|
|
85
|
+
status: Type.Union([
|
|
86
|
+
Type.Literal("pending"),
|
|
87
|
+
Type.Literal("in_progress"),
|
|
88
|
+
Type.Literal("done"),
|
|
89
|
+
Type.Literal("failed"),
|
|
90
|
+
Type.Literal("removed"),
|
|
91
|
+
], { description: "New status for the task" }),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const MissionCompleteSchema = Type.Object({
|
|
95
|
+
run_id: Type.String({ description: "ID of the run to mark as complete" }),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const MissionResumeSchema = Type.Object({
|
|
99
|
+
run_id: Type.String({ description: "ID of the run to resume" }),
|
|
100
|
+
phase: Type.Optional(Type.String({ description: "Optional phase to set as current" })),
|
|
101
|
+
statusMessage: Type.Optional(Type.String({ description: "Optional status message" })),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ============================================================================
|
|
105
|
+
// Tool Result Formatters
|
|
106
|
+
// ============================================================================
|
|
107
|
+
|
|
108
|
+
function formatAddPhaseResult(result: Awaited<ReturnType<typeof addPhase>>): string {
|
|
109
|
+
if (!result.success) {
|
|
110
|
+
return `❌ Failed to add phase: ${result.message}\n${result.errors.map(e => ` - ${e}`).join("\n")}`;
|
|
111
|
+
}
|
|
112
|
+
return `✅ Phase added: ${result.phaseId}\n Name: ${result.message.replace("Phase added: ", "")}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function formatAddTaskResult(result: Awaited<ReturnType<typeof addTask>>): string {
|
|
116
|
+
if (!result.success) {
|
|
117
|
+
return `❌ Failed to add task: ${result.message}\n${result.errors.map(e => ` - ${e}`).join("\n")}`;
|
|
118
|
+
}
|
|
119
|
+
return `✅ Task added: ${result.taskId}\n Name: ${result.message.replace("Task added: ", "")}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function formatUpdatePhaseResult(result: Awaited<ReturnType<typeof updatePhase>>): string {
|
|
123
|
+
if (!result.success) {
|
|
124
|
+
return `❌ Failed to update phase: ${result.message}\n${result.errors.map(e => ` - ${e}`).join("\n")}`;
|
|
125
|
+
}
|
|
126
|
+
const lines = [`✅ Phase ${result.phaseId} updated: ${result.previousStatus} → ${result.newStatus}`];
|
|
127
|
+
if (result.startedAt) lines.push(` Started at: ${result.startedAt}`);
|
|
128
|
+
if (result.finishAt) lines.push(` Finished at: ${result.finishAt}`);
|
|
129
|
+
return lines.join("\n");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function formatUpdateTaskResult(result: Awaited<ReturnType<typeof updateTask>>): string {
|
|
133
|
+
if (!result.success) {
|
|
134
|
+
return `❌ Failed to update task: ${result.message}\n${result.errors.map(e => ` - ${e}`).join("\n")}`;
|
|
135
|
+
}
|
|
136
|
+
const lines = [`✅ Task ${result.taskId} in ${result.phaseId} updated: ${result.previousStatus} → ${result.newStatus}`];
|
|
137
|
+
if (result.startedAt) lines.push(` Started at: ${result.startedAt}`);
|
|
138
|
+
if (result.finishAt) lines.push(` Finished at: ${result.finishAt}`);
|
|
139
|
+
return lines.join("\n");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function formatMissionCompleteResult(result: Awaited<ReturnType<typeof missionComplete>>): string {
|
|
143
|
+
if (!result.success) {
|
|
144
|
+
return `❌ Failed to complete mission: ${result.message}\n${result.errors.map(e => ` - ${e}`).join("\n")}`;
|
|
145
|
+
}
|
|
146
|
+
return `✅ Mission ${result.runId} marked as complete\n Previous status: ${result.previousStatus}\n Finished at: ${result.finishAt}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function formatMissionResumeResult(result: Awaited<ReturnType<typeof missionResume>>): string {
|
|
150
|
+
if (!result.success) {
|
|
151
|
+
return `❌ Failed to resume mission: ${result.message}\n${result.errors.map(e => ` - ${e}`).join("\n")}`;
|
|
152
|
+
}
|
|
153
|
+
return `✅ Mission resumed: ${result.runId}\n Run status: ${result.runStatus}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function resolveBundledSkillsDir(): string | null {
|
|
157
|
+
const projectSkillFile = path.join(getProjectRoot(), ".pi", "skills", "mission-orchestrator", "SKILL.md");
|
|
158
|
+
if (existsSync(projectSkillFile)) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const extensionDir = path.dirname(fileURLToPath(import.meta.url));
|
|
163
|
+
return path.join(extensionDir, "..", "skills");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function queueOnActiveRun<T>(operation: () => T): Promise<T> {
|
|
167
|
+
const activeRunId = readState().active_run_id;
|
|
168
|
+
const runFilePath = activeRunId ? getRunFilePath(activeRunId) : getStateFilePath();
|
|
169
|
+
return withFileMutationQueue(runFilePath, async () => operation());
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function queueOnRunAndState<T>(runId: string, operation: () => T): Promise<T> {
|
|
173
|
+
const runFilePath = getRunFilePath(runId);
|
|
174
|
+
const stateFilePath = getStateFilePath();
|
|
175
|
+
return withFileMutationQueue(runFilePath, async () =>
|
|
176
|
+
withFileMutationQueue(stateFilePath, async () => operation()),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ============================================================================
|
|
181
|
+
// Agent Model Configuration
|
|
182
|
+
// ============================================================================
|
|
183
|
+
|
|
184
|
+
const AGENT_FILES = {
|
|
185
|
+
worker: ".pi/agents/worker.md",
|
|
186
|
+
auditor: ".pi/agents/auditor.md",
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Placeholder/default model values that indicate unconfigured state
|
|
190
|
+
const DEFAULT_MODEL_VALUES = ["light", "default", "placeholder", ""];
|
|
191
|
+
|
|
192
|
+
interface AgentFrontmatter {
|
|
193
|
+
model?: string;
|
|
194
|
+
[key: string]: unknown;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Parse YAML frontmatter from markdown content.
|
|
199
|
+
* Returns parsed frontmatter and the content after frontmatter.
|
|
200
|
+
*/
|
|
201
|
+
function parseFrontmatter(content: string): { frontmatter: AgentFrontmatter | null; body: string } {
|
|
202
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
203
|
+
if (!match) {
|
|
204
|
+
return { frontmatter: null, body: content };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const yamlText = match[1];
|
|
208
|
+
const body = match[2];
|
|
209
|
+
const frontmatter: AgentFrontmatter = {};
|
|
210
|
+
|
|
211
|
+
// Simple YAML parsing for key: value pairs
|
|
212
|
+
for (const line of yamlText.split("\n")) {
|
|
213
|
+
const colonIdx = line.indexOf(":");
|
|
214
|
+
if (colonIdx > 0) {
|
|
215
|
+
const key = line.slice(0, colonIdx).trim();
|
|
216
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
217
|
+
// Remove quotes if present
|
|
218
|
+
frontmatter[key] = value.replace(/^["']|["']$/g, "");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { frontmatter, body };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Serialize frontmatter and body back to markdown.
|
|
227
|
+
*/
|
|
228
|
+
function serializeFrontmatter(frontmatter: AgentFrontmatter, body: string): string {
|
|
229
|
+
const lines = ["---"];
|
|
230
|
+
for (const [key, value] of Object.entries(frontmatter)) {
|
|
231
|
+
if (value === undefined) continue;
|
|
232
|
+
// Quote string values that contain special characters
|
|
233
|
+
const strValue = String(value);
|
|
234
|
+
const needsQuotes = strValue.includes(":") || strValue.includes("#") || strValue.startsWith(" ") || strValue === "";
|
|
235
|
+
lines.push(`${key}: ${needsQuotes ? `"${strValue}"` : strValue}`);
|
|
236
|
+
}
|
|
237
|
+
lines.push("---");
|
|
238
|
+
lines.push("");
|
|
239
|
+
lines.push(body);
|
|
240
|
+
return lines.join("\n");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Check if agent file has unconfigured/placeholder model.
|
|
245
|
+
*/
|
|
246
|
+
function hasUnconfiguredModel(agentPath: string): boolean {
|
|
247
|
+
const content = readText(agentPath);
|
|
248
|
+
if (!content) return true; // File doesn't exist = unconfigured
|
|
249
|
+
|
|
250
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
251
|
+
if (!frontmatter) return true; // No frontmatter = unconfigured
|
|
252
|
+
|
|
253
|
+
const model = frontmatter.model?.toLowerCase().trim() ?? "";
|
|
254
|
+
return DEFAULT_MODEL_VALUES.includes(model);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Update agent file with selected model.
|
|
259
|
+
*/
|
|
260
|
+
function updateAgentModel(agentPath: string, model: string): boolean {
|
|
261
|
+
const content = readText(agentPath);
|
|
262
|
+
if (!content) return false;
|
|
263
|
+
|
|
264
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
265
|
+
if (!frontmatter) return false;
|
|
266
|
+
|
|
267
|
+
frontmatter.model = model;
|
|
268
|
+
writeText(agentPath, serializeFrontmatter(frontmatter, body));
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get sorted list of available models for selection.
|
|
274
|
+
* Prefers cheaper models and current model (if available).
|
|
275
|
+
* Falls back to authenticated models if scoped-model access unavailable.
|
|
276
|
+
*/
|
|
277
|
+
function getSortedAvailableModels(
|
|
278
|
+
availableModels: Model<string>[],
|
|
279
|
+
currentModel: Model<string> | undefined,
|
|
280
|
+
): Model<string>[] {
|
|
281
|
+
// Sort by cost (input + output), then by whether it's the current model
|
|
282
|
+
return [...availableModels].sort((a, b) => {
|
|
283
|
+
// Current model gets highest priority
|
|
284
|
+
if (currentModel) {
|
|
285
|
+
if (a.id === currentModel.id && a.provider === currentModel.provider) return -1;
|
|
286
|
+
if (b.id === currentModel.id && b.provider === currentModel.provider) return 1;
|
|
287
|
+
}
|
|
288
|
+
// Then sort by total cost (cheaper first)
|
|
289
|
+
const costA = a.cost.input + a.cost.output;
|
|
290
|
+
const costB = b.cost.input + b.cost.output;
|
|
291
|
+
return costA - costB;
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function formatContextWindow(tokens: number): string {
|
|
296
|
+
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M ctx`;
|
|
297
|
+
if (tokens >= 1_000) return `${Math.round(tokens / 1_000)}k ctx`;
|
|
298
|
+
return `${tokens} ctx`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function getModelCapabilityTags(model: Model<string>): string[] {
|
|
302
|
+
const tags: string[] = [];
|
|
303
|
+
const totalCost = model.cost.input + model.cost.output;
|
|
304
|
+
|
|
305
|
+
if (totalCost === 0) tags.push("free");
|
|
306
|
+
else if (totalCost <= 1) tags.push("cheap");
|
|
307
|
+
else if (totalCost >= 10) tags.push("premium");
|
|
308
|
+
|
|
309
|
+
if (model.reasoning) tags.push("reasoning");
|
|
310
|
+
if (model.input.includes("image")) tags.push("vision");
|
|
311
|
+
if (model.contextWindow >= 500_000) tags.push("long-context");
|
|
312
|
+
else if (model.contextWindow >= 100_000) tags.push("128k+");
|
|
313
|
+
|
|
314
|
+
return tags;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function formatModelOption(model: Model<string>): string {
|
|
318
|
+
const totalCost = model.cost.input + model.cost.output;
|
|
319
|
+
const cost = totalCost > 0 ? `$${totalCost.toFixed(2)}/1M` : "free";
|
|
320
|
+
const displayName = model.name && model.name !== model.id ? model.name : `${model.provider}/${model.id}`;
|
|
321
|
+
const tags = getModelCapabilityTags(model);
|
|
322
|
+
const suffix = [formatContextWindow(model.contextWindow), cost, tags.join(", ")].filter(Boolean).join(" • ");
|
|
323
|
+
return `${displayName} — ${model.provider}/${model.id} (${suffix})`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Prompt user to configure agent models after scaffold.
|
|
328
|
+
* Uses available authenticated models; notes if scoped-model access unavailable.
|
|
329
|
+
*/
|
|
330
|
+
async function promptAgentModelSetup(
|
|
331
|
+
ctx: ExtensionCommandContext,
|
|
332
|
+
extensionApi: ExtensionAPI,
|
|
333
|
+
): Promise<void> {
|
|
334
|
+
const projectRoot = getProjectRoot();
|
|
335
|
+
const workerPath = path.join(projectRoot, AGENT_FILES.worker);
|
|
336
|
+
const auditorPath = path.join(projectRoot, AGENT_FILES.auditor);
|
|
337
|
+
|
|
338
|
+
// Check if either agent needs configuration
|
|
339
|
+
const workerNeedsConfig = hasUnconfiguredModel(workerPath);
|
|
340
|
+
const auditorNeedsConfig = hasUnconfiguredModel(auditorPath);
|
|
341
|
+
|
|
342
|
+
if (!workerNeedsConfig && !auditorNeedsConfig) {
|
|
343
|
+
return; // Both already configured
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Warn if required tools are missing
|
|
347
|
+
const hasAskUser = extensionApi.getAllTools().some((tool) => tool.name === "ask_user");
|
|
348
|
+
const hasSubagent = extensionApi.getAllTools().some((tool) => tool.name === "subagent");
|
|
349
|
+
|
|
350
|
+
if (!hasAskUser) {
|
|
351
|
+
ctx.ui.notify("Mission Control: ask_user tool not available. Structured interviews and approvals will be less guided.", "warning");
|
|
352
|
+
}
|
|
353
|
+
if (!hasSubagent) {
|
|
354
|
+
ctx.ui.notify("Mission Control: subagent tool not available. Worker/Auditor delegation will not run until pi-subagents is loaded.", "warning");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Get available models from registry
|
|
358
|
+
// Note: getAvailable() returns authenticated models. If scoped-model access
|
|
359
|
+
// is unavailable, this still provides usable models but without scope filtering.
|
|
360
|
+
const availableModels = ctx.modelRegistry.getAvailable();
|
|
361
|
+
|
|
362
|
+
if (availableModels.length === 0) {
|
|
363
|
+
ctx.ui.notify("Mission Control: No authenticated models available. Please configure API keys first.", "warning");
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Sort models (prefer cheaper + current model)
|
|
368
|
+
const sortedModels = getSortedAvailableModels(availableModels, ctx.model);
|
|
369
|
+
|
|
370
|
+
const modelOptions = sortedModels.map(formatModelOption);
|
|
371
|
+
|
|
372
|
+
const selection = await ctx.ui.select(
|
|
373
|
+
"Choose a shared model for Worker + Auditor (sorted cheapest first)",
|
|
374
|
+
modelOptions,
|
|
375
|
+
{ timeout: 60000 },
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
if (!selection) {
|
|
379
|
+
ctx.ui.notify("Mission Control: Agent model setup cancelled. Using defaults.", "info");
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Extract model ID from selection
|
|
384
|
+
const selectedModel = sortedModels[modelOptions.indexOf(selection)];
|
|
385
|
+
if (!selectedModel) {
|
|
386
|
+
ctx.ui.notify("Mission Control: Invalid model selection.", "error");
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Format as provider/modelId for the frontmatter
|
|
391
|
+
const modelValue = `${selectedModel.provider}/${selectedModel.id}`;
|
|
392
|
+
|
|
393
|
+
// Update both agent files
|
|
394
|
+
let updatedCount = 0;
|
|
395
|
+
|
|
396
|
+
if (workerNeedsConfig) {
|
|
397
|
+
if (updateAgentModel(workerPath, modelValue)) {
|
|
398
|
+
updatedCount++;
|
|
399
|
+
} else {
|
|
400
|
+
ctx.ui.notify(`Mission Control: Failed to update ${AGENT_FILES.worker}`, "error");
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (auditorNeedsConfig) {
|
|
405
|
+
if (updateAgentModel(auditorPath, modelValue)) {
|
|
406
|
+
updatedCount++;
|
|
407
|
+
} else {
|
|
408
|
+
ctx.ui.notify(`Mission Control: Failed to update ${AGENT_FILES.auditor}`, "error");
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (updatedCount > 0) {
|
|
413
|
+
ctx.ui.notify(
|
|
414
|
+
`Mission Control: Set agent models to ${selectedModel.name} (${modelValue})`,
|
|
415
|
+
"info",
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ============================================================================
|
|
421
|
+
// Extension Factory
|
|
422
|
+
// ============================================================================
|
|
423
|
+
|
|
424
|
+
export default function missionControlExtension(pi: ExtensionAPI) {
|
|
425
|
+
// ========================================================================
|
|
426
|
+
// Register /mission command
|
|
427
|
+
// ========================================================================
|
|
428
|
+
|
|
429
|
+
pi.registerCommand("mission", {
|
|
430
|
+
description: "Open Mission Control dashboard for visual mission orchestration",
|
|
431
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
432
|
+
if (!isScaffolded()) {
|
|
433
|
+
const result = missionScaffold();
|
|
434
|
+
if (!result.success) {
|
|
435
|
+
ctx.ui.notify(`Scaffolding failed: ${result.message}`, "error");
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
ctx.ui.notify("Mission Control scaffolding complete", "info");
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (ctx.hasUI) {
|
|
442
|
+
await promptAgentModelSetup(ctx, pi);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const trimmedArgs = args.trim().toLowerCase();
|
|
446
|
+
let dashboardResult: string | null = null;
|
|
447
|
+
|
|
448
|
+
if (trimmedArgs === "init") {
|
|
449
|
+
dashboardResult = "init";
|
|
450
|
+
} else if (!ctx.hasUI) {
|
|
451
|
+
ctx.ui.notify("/mission init works without interactive UI", "info");
|
|
452
|
+
return;
|
|
453
|
+
} else {
|
|
454
|
+
dashboardResult = await ctx.ui.custom<string | null>(
|
|
455
|
+
(tui, _theme, _keybindings, done) => {
|
|
456
|
+
const dashboard = new MissionDashboard({
|
|
457
|
+
onClose: () => {
|
|
458
|
+
dashboard.dispose();
|
|
459
|
+
done(null);
|
|
460
|
+
},
|
|
461
|
+
onInitMission: () => {
|
|
462
|
+
dashboard.dispose();
|
|
463
|
+
done("init");
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
dashboard.startRefresh(tui);
|
|
467
|
+
return {
|
|
468
|
+
render: (width: number) => dashboard.render(width, ctx.ui.theme),
|
|
469
|
+
handleInput: (data: string) => dashboard.handleInput(data),
|
|
470
|
+
invalidate: () => dashboard.invalidate(),
|
|
471
|
+
dispose: () => dashboard.dispose(),
|
|
472
|
+
};
|
|
473
|
+
},
|
|
474
|
+
{ overlay: true },
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (dashboardResult !== "init") {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const initResult = missionInit({
|
|
483
|
+
phase: "research",
|
|
484
|
+
statusMessage: "Mission initialized - waiting for requirements",
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
if (!initResult.success || !initResult.runId) {
|
|
488
|
+
ctx.ui.notify(`Failed to initialize mission: ${initResult.message}`, "error");
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
ctx.ui.notify(`Mission initialized: ${initResult.runId}`, "info");
|
|
493
|
+
pi.sendUserMessage(`/skill:mission-orchestrator New mission initialized with run ID ${initResult.runId}. Start the requirements interview now.`);
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// ========================================================================
|
|
498
|
+
// Register Agent Tools
|
|
499
|
+
// ========================================================================
|
|
500
|
+
|
|
501
|
+
// add_phase tool
|
|
502
|
+
pi.registerTool({
|
|
503
|
+
name: "add_phase",
|
|
504
|
+
label: "Add Mission Phase",
|
|
505
|
+
description:
|
|
506
|
+
"Add a new phase to the current mission run. Auto-generates phase_id (phase1, phase2, ...). " +
|
|
507
|
+
"Sets status to 'pending'. Use this to structure the mission into logical phases.",
|
|
508
|
+
promptSnippet: "add_phase(name, file) → adds a mission phase",
|
|
509
|
+
parameters: AddPhaseSchema as never,
|
|
510
|
+
execute: async (_toolCallId, params: Parameters<typeof addPhase>[0]) => {
|
|
511
|
+
const result = await queueOnActiveRun(() => addPhase(params));
|
|
512
|
+
return {
|
|
513
|
+
isError: !result.success,
|
|
514
|
+
content: [{ type: "text" as const, text: formatAddPhaseResult(result) }],
|
|
515
|
+
details: result,
|
|
516
|
+
};
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// add_task tool
|
|
521
|
+
pi.registerTool({
|
|
522
|
+
name: "add_task",
|
|
523
|
+
label: "Add Mission Task",
|
|
524
|
+
description:
|
|
525
|
+
"Add a new task to a phase. Auto-generates task_id from phase prefix (phase1-task1, phase1-task2, ...). " +
|
|
526
|
+
"Sets status to 'pending'. Use this to break down phases into executable tasks.",
|
|
527
|
+
promptSnippet: "add_task(phase_id, name, file) → adds a task to a phase",
|
|
528
|
+
parameters: AddTaskSchema as never,
|
|
529
|
+
execute: async (_toolCallId, params: Parameters<typeof addTask>[0]) => {
|
|
530
|
+
const result = await queueOnActiveRun(() => addTask(params));
|
|
531
|
+
return {
|
|
532
|
+
isError: !result.success,
|
|
533
|
+
content: [{ type: "text" as const, text: formatAddTaskResult(result) }],
|
|
534
|
+
details: result,
|
|
535
|
+
};
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// update_phase tool
|
|
540
|
+
pi.registerTool({
|
|
541
|
+
name: "update_phase",
|
|
542
|
+
label: "Update Phase Status",
|
|
543
|
+
description:
|
|
544
|
+
"Update a phase's status. Auto-sets started_at when transitioning to 'in_progress'. " +
|
|
545
|
+
"Auto-sets finish_at when transitioning to 'done' or 'removed'. " +
|
|
546
|
+
"Valid statuses: pending, in_progress, done, removed.",
|
|
547
|
+
promptSnippet: "update_phase(phase_id, status) → updates phase status",
|
|
548
|
+
parameters: UpdatePhaseSchema as never,
|
|
549
|
+
execute: async (_toolCallId, params: Parameters<typeof updatePhase>[0]) => {
|
|
550
|
+
const result = await queueOnActiveRun(() => updatePhase(params));
|
|
551
|
+
return {
|
|
552
|
+
isError: !result.success,
|
|
553
|
+
content: [{ type: "text" as const, text: formatUpdatePhaseResult(result) }],
|
|
554
|
+
details: result,
|
|
555
|
+
};
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// update_task tool
|
|
560
|
+
pi.registerTool({
|
|
561
|
+
name: "update_task",
|
|
562
|
+
label: "Update Task Status",
|
|
563
|
+
description:
|
|
564
|
+
"Update a task's status. Auto-sets started_at when transitioning to 'in_progress'. " +
|
|
565
|
+
"Auto-sets finish_at when transitioning to 'done', 'failed', or 'removed'. " +
|
|
566
|
+
"Valid statuses: pending, in_progress, done, failed, removed.",
|
|
567
|
+
promptSnippet: "update_task(task_id, status) → updates task status",
|
|
568
|
+
parameters: UpdateTaskSchema as never,
|
|
569
|
+
execute: async (_toolCallId, params: Parameters<typeof updateTask>[0]) => {
|
|
570
|
+
const result = await queueOnActiveRun(() => updateTask(params));
|
|
571
|
+
return {
|
|
572
|
+
isError: !result.success,
|
|
573
|
+
content: [{ type: "text" as const, text: formatUpdateTaskResult(result) }],
|
|
574
|
+
details: result,
|
|
575
|
+
};
|
|
576
|
+
},
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// mission_complete tool
|
|
580
|
+
pi.registerTool({
|
|
581
|
+
name: "mission_complete",
|
|
582
|
+
label: "Complete Mission",
|
|
583
|
+
description:
|
|
584
|
+
"Mark a mission run as complete. Sets run.json status to 'done' and finish_at timestamp. " +
|
|
585
|
+
"Clears active_run_id from state.json. Use when all phases are finished successfully.",
|
|
586
|
+
promptSnippet: "mission_complete(run_id) → marks mission as complete",
|
|
587
|
+
parameters: MissionCompleteSchema as never,
|
|
588
|
+
execute: async (_toolCallId, params: Parameters<typeof missionComplete>[0]) => {
|
|
589
|
+
const result = await queueOnRunAndState(params.run_id, () => missionComplete(params));
|
|
590
|
+
return {
|
|
591
|
+
isError: !result.success,
|
|
592
|
+
content: [{ type: "text" as const, text: formatMissionCompleteResult(result) }],
|
|
593
|
+
details: result,
|
|
594
|
+
};
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// mission_resume tool
|
|
599
|
+
pi.registerTool({
|
|
600
|
+
name: "mission_resume",
|
|
601
|
+
label: "Resume Mission",
|
|
602
|
+
description:
|
|
603
|
+
"Resume a previously created mission run. Sets state.json active_run_id to the specified run_id. " +
|
|
604
|
+
"Use when the user wants to continue work on an existing mission.",
|
|
605
|
+
promptSnippet: "mission_resume(run_id, phase?, statusMessage?) → resumes a mission",
|
|
606
|
+
parameters: MissionResumeSchema as never,
|
|
607
|
+
execute: async (_toolCallId, params: Parameters<typeof missionResume>[0]) => {
|
|
608
|
+
const result = await queueOnRunAndState(params.run_id, () => missionResume(params));
|
|
609
|
+
return {
|
|
610
|
+
isError: !result.success,
|
|
611
|
+
content: [{ type: "text" as const, text: formatMissionResumeResult(result) }],
|
|
612
|
+
details: result,
|
|
613
|
+
};
|
|
614
|
+
},
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// ========================================================================
|
|
618
|
+
// Register resources_discover for bundled skills
|
|
619
|
+
// ========================================================================
|
|
620
|
+
|
|
621
|
+
pi.on("resources_discover", (_event: ResourcesDiscoverEvent): ResourcesDiscoverResult => {
|
|
622
|
+
const bundledSkillsDir = resolveBundledSkillsDir();
|
|
623
|
+
return bundledSkillsDir ? { skillPaths: [bundledSkillsDir] } : {};
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// ========================================================================
|
|
627
|
+
// Handle session_start/reset for state reset
|
|
628
|
+
// ========================================================================
|
|
629
|
+
|
|
630
|
+
pi.on("session_start", (event: SessionStartEvent, ctx: ExtensionContext) => {
|
|
631
|
+
if (event.reason !== "reload" && isScaffolded()) {
|
|
632
|
+
const currentState = readState();
|
|
633
|
+
writeState({ ...defaultState });
|
|
634
|
+
|
|
635
|
+
if (currentState.active_run_id) {
|
|
636
|
+
ctx.ui.notify(
|
|
637
|
+
`Previous mission run ${currentState.active_run_id} is inactive in this pi session. Use mission_resume to continue it.`,
|
|
638
|
+
"info",
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const hasSubagent = pi.getAllTools().some((tool) => tool.name === "subagent");
|
|
644
|
+
if (!hasSubagent) {
|
|
645
|
+
ctx.ui.notify(
|
|
646
|
+
"Mission Control: subagent tool not available. Worker/Auditor delegation will not run until pi-subagents is loaded.",
|
|
647
|
+
"warning",
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const hasAskUser = pi.getAllTools().some((tool) => tool.name === "ask_user");
|
|
652
|
+
if (!hasAskUser) {
|
|
653
|
+
ctx.ui.notify(
|
|
654
|
+
"Mission Control: ask_user tool not available. Install pi-ask-user for better interviews and approval prompts.",
|
|
655
|
+
"warning",
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
}
|