Skip to content

Commit 7fe3543

Browse files
G9Pedrocursoragent
andauthored
feat: add mission primitive with orchestrator, CLI, and MCP tools (#42)
Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: G9Pedro <G9Pedro@users.noreply.github.com>
1 parent 9e7d605 commit 7fe3543

File tree

15 files changed

+2954
-1
lines changed

15 files changed

+2954
-1
lines changed

packages/cli/src/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { registerConversationCommands } from './cli/commands/conversation.js';
1010
import { registerCursorCommands } from './cli/commands/cursor.js';
1111
import { registerDispatchCommands } from './cli/commands/dispatch.js';
1212
import { registerMcpCommands } from './cli/commands/mcp.js';
13+
import { registerMissionCommands } from './cli/commands/mission.js';
1314
import { registerSafetyCommands } from './cli/commands/safety.js';
1415
import { registerPortabilityCommands } from './cli/commands/portability.js';
1516
import { registerFederationCommands } from './cli/commands/federation.js';
@@ -2325,6 +2326,7 @@ registerSafetyCommands(program, DEFAULT_ACTOR);
23252326
registerPortabilityCommands(program);
23262327
registerFederationCommands(program, threadCmd, DEFAULT_ACTOR);
23272328
registerCapabilityCommands(program, DEFAULT_ACTOR);
2329+
registerMissionCommands(program, DEFAULT_ACTOR);
23282330

23292331
// ============================================================================
23302332
// onboarding
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
import { Command } from 'commander';
4+
import * as workgraph from '@versatly/workgraph-kernel';
5+
import {
6+
addWorkspaceOption,
7+
csv,
8+
resolveWorkspacePath,
9+
runCommand,
10+
} from '../core.js';
11+
12+
export function registerMissionCommands(program: Command, defaultActor: string): void {
13+
const missionCmd = program
14+
.command('mission')
15+
.description('Mission primitive lifecycle and orchestration');
16+
17+
addWorkspaceOption(
18+
missionCmd
19+
.command('create <title>')
20+
.description('Create a mission in planning state')
21+
.requiredOption('--goal <goal>', 'Mission goal statement')
22+
.option('-a, --actor <name>', 'Actor', defaultActor)
23+
.option('--mid <mid>', 'Mission identifier slug override')
24+
.option('--description <text>', 'Mission summary/description')
25+
.option('--priority <level>', 'urgent|high|medium|low', 'medium')
26+
.option('--owner <name>', 'Mission owner')
27+
.option('--project <ref>', 'Project ref (projects/<slug>.md)')
28+
.option('--space <ref>', 'Space ref (spaces/<slug>.md)')
29+
.option('--constraints <items>', 'Comma-separated mission constraints')
30+
.option('--tags <items>', 'Comma-separated tags')
31+
.option('--json', 'Emit structured JSON output'),
32+
).action((title, opts) =>
33+
runCommand(
34+
opts,
35+
() => {
36+
const workspacePath = resolveWorkspacePath(opts);
37+
return {
38+
mission: workgraph.mission.createMission(workspacePath, title, opts.goal, opts.actor, {
39+
mid: opts.mid,
40+
description: opts.description,
41+
priority: normalizePriority(opts.priority),
42+
owner: opts.owner,
43+
project: opts.project,
44+
space: opts.space,
45+
constraints: csv(opts.constraints),
46+
tags: csv(opts.tags),
47+
}),
48+
};
49+
},
50+
(result) => [
51+
`Created mission: ${result.mission.path}`,
52+
`Status: ${String(result.mission.fields.status)}`,
53+
],
54+
),
55+
);
56+
57+
addWorkspaceOption(
58+
missionCmd
59+
.command('plan <missionRef>')
60+
.description('Plan mission milestones/features and create feature threads')
61+
.option('-a, --actor <name>', 'Actor', defaultActor)
62+
.option('--goal <goal>', 'Plan goal override')
63+
.option('--constraints <items>', 'Comma-separated constraints')
64+
.option('--estimated-runs <n>', 'Estimated number of runs')
65+
.option('--estimated-cost-usd <n>', 'Estimated USD cost')
66+
.option('--append', 'Append milestones instead of replacing')
67+
.option('--milestones <json>', 'Milestones JSON payload')
68+
.option('--milestones-file <path>', 'Milestones JSON file path')
69+
.option('--json', 'Emit structured JSON output'),
70+
).action((missionRef, opts) =>
71+
runCommand(
72+
opts,
73+
() => {
74+
const workspacePath = resolveWorkspacePath(opts);
75+
const milestones = readMissionMilestonesInput(opts.milestones, opts.milestonesFile);
76+
return {
77+
mission: workgraph.mission.planMission(
78+
workspacePath,
79+
missionRef,
80+
{
81+
goal: opts.goal,
82+
constraints: csv(opts.constraints),
83+
estimated_runs: parseOptionalInt(opts.estimatedRuns),
84+
estimated_cost_usd: parseOptionalNumber(opts.estimatedCostUsd),
85+
replaceMilestones: !opts.append,
86+
milestones,
87+
},
88+
opts.actor,
89+
),
90+
};
91+
},
92+
(result) => [
93+
`Planned mission: ${result.mission.path}`,
94+
`Milestones: ${Array.isArray(result.mission.fields.milestones) ? result.mission.fields.milestones.length : 0}`,
95+
],
96+
),
97+
);
98+
99+
addWorkspaceOption(
100+
missionCmd
101+
.command('approve <missionRef>')
102+
.description('Approve planned mission')
103+
.option('-a, --actor <name>', 'Actor', defaultActor)
104+
.option('--json', 'Emit structured JSON output'),
105+
).action((missionRef, opts) =>
106+
runCommand(
107+
opts,
108+
() => {
109+
const workspacePath = resolveWorkspacePath(opts);
110+
return {
111+
mission: workgraph.mission.approveMission(workspacePath, missionRef, opts.actor),
112+
};
113+
},
114+
(result) => [`Approved mission: ${result.mission.path}`],
115+
),
116+
);
117+
118+
addWorkspaceOption(
119+
missionCmd
120+
.command('start <missionRef>')
121+
.description('Start mission execution and optionally run one orchestrator cycle')
122+
.option('-a, --actor <name>', 'Actor', defaultActor)
123+
.option('--no-run-cycle', 'Do not run orchestrator cycle after start')
124+
.option('--json', 'Emit structured JSON output'),
125+
).action((missionRef, opts) =>
126+
runCommand(
127+
opts,
128+
() => {
129+
const workspacePath = resolveWorkspacePath(opts);
130+
const started = workgraph.mission.startMission(workspacePath, missionRef, opts.actor);
131+
const cycle = opts.runCycle === false
132+
? null
133+
: workgraph.missionOrchestrator.runMissionOrchestratorCycle(workspacePath, started.path, opts.actor);
134+
return { mission: started, cycle };
135+
},
136+
(result) => [
137+
`Started mission: ${result.mission.path}`,
138+
...(result.cycle ? [`Cycle actions: ${result.cycle.actions.length}`] : []),
139+
],
140+
),
141+
);
142+
143+
addWorkspaceOption(
144+
missionCmd
145+
.command('status <missionRef>')
146+
.description('Show mission primitive status and milestones')
147+
.option('--json', 'Emit structured JSON output'),
148+
).action((missionRef, opts) =>
149+
runCommand(
150+
opts,
151+
() => {
152+
const workspacePath = resolveWorkspacePath(opts);
153+
const missionInstance = workgraph.mission.missionStatus(workspacePath, missionRef);
154+
const progress = workgraph.mission.missionProgress(workspacePath, missionInstance.path);
155+
return { mission: missionInstance, progress };
156+
},
157+
(result) => [
158+
`Mission: ${result.mission.path}`,
159+
`Status: ${String(result.mission.fields.status)}`,
160+
`Progress: ${result.progress.percentComplete}% (${result.progress.doneFeatures}/${result.progress.totalFeatures} features)`,
161+
],
162+
),
163+
);
164+
165+
addWorkspaceOption(
166+
missionCmd
167+
.command('progress <missionRef>')
168+
.description('Show mission progress metrics only')
169+
.option('--json', 'Emit structured JSON output'),
170+
).action((missionRef, opts) =>
171+
runCommand(
172+
opts,
173+
() => {
174+
const workspacePath = resolveWorkspacePath(opts);
175+
return workgraph.mission.missionProgress(workspacePath, missionRef);
176+
},
177+
(result) => [
178+
`Mission ${result.mid}: ${result.status}`,
179+
`Milestones: ${result.passedMilestones}/${result.totalMilestones}`,
180+
`Features: ${result.doneFeatures}/${result.totalFeatures}`,
181+
],
182+
),
183+
);
184+
185+
addWorkspaceOption(
186+
missionCmd
187+
.command('intervene <missionRef>')
188+
.description('Intervene in mission execution (status/priority/skip/append milestones)')
189+
.requiredOption('--reason <reason>', 'Intervention reason')
190+
.option('-a, --actor <name>', 'Actor', defaultActor)
191+
.option('--set-priority <priority>', 'urgent|high|medium|low')
192+
.option('--set-status <status>', 'planning|approved|active|validating|completed|failed')
193+
.option('--skip-feature <milestoneId:threadPath>', 'Skip one feature in a milestone')
194+
.option('--append-milestones <json>', 'Milestones JSON to append')
195+
.option('--append-milestones-file <path>', 'Milestones JSON file to append')
196+
.option('--json', 'Emit structured JSON output'),
197+
).action((missionRef, opts) =>
198+
runCommand(
199+
opts,
200+
() => {
201+
const workspacePath = resolveWorkspacePath(opts);
202+
const skipFeature = parseSkipFeature(opts.skipFeature);
203+
const appendMilestones = readMissionMilestonesInput(opts.appendMilestones, opts.appendMilestonesFile, false);
204+
return {
205+
mission: workgraph.mission.interveneMission(workspacePath, missionRef, {
206+
reason: String(opts.reason),
207+
setPriority: opts.setPriority ? normalizePriority(opts.setPriority) : undefined,
208+
setStatus: opts.setStatus ? normalizeMissionStatus(opts.setStatus) : undefined,
209+
skipFeature: skipFeature ?? undefined,
210+
appendMilestones: appendMilestones.length > 0 ? appendMilestones : undefined,
211+
}, opts.actor),
212+
};
213+
},
214+
(result) => [`Intervened mission: ${result.mission.path}`],
215+
),
216+
);
217+
218+
addWorkspaceOption(
219+
missionCmd
220+
.command('list')
221+
.description('List missions')
222+
.option('--status <status>', 'Filter by mission status')
223+
.option('--json', 'Emit structured JSON output'),
224+
).action((opts) =>
225+
runCommand(
226+
opts,
227+
() => {
228+
const workspacePath = resolveWorkspacePath(opts);
229+
const missions = workgraph.mission.listMissions(workspacePath)
230+
.filter((entry) => !opts.status || String(entry.fields.status) === String(opts.status));
231+
return { missions };
232+
},
233+
(result) => {
234+
if (result.missions.length === 0) return ['No missions found.'];
235+
return result.missions.map((entry) =>
236+
`[${String(entry.fields.status)}] ${String(entry.fields.title)} -> ${entry.path}`,
237+
);
238+
},
239+
),
240+
);
241+
}
242+
243+
function readMissionMilestonesInput(
244+
rawJson: string | undefined,
245+
jsonFile: string | undefined,
246+
required: boolean = true,
247+
): workgraph.mission.MissionMilestonePlanInput[] {
248+
if (!rawJson && !jsonFile) {
249+
if (required) {
250+
throw new Error('Mission milestones input is required. Use --milestones or --milestones-file.');
251+
}
252+
return [];
253+
}
254+
const parsed = rawJson
255+
? JSON.parse(rawJson)
256+
: JSON.parse(fs.readFileSync(path.resolve(String(jsonFile)), 'utf-8'));
257+
if (!Array.isArray(parsed)) {
258+
throw new Error('Milestones input must be a JSON array.');
259+
}
260+
return parsed as workgraph.mission.MissionMilestonePlanInput[];
261+
}
262+
263+
function normalizePriority(value: string): 'urgent' | 'high' | 'medium' | 'low' {
264+
const normalized = String(value).trim().toLowerCase();
265+
if (normalized === 'urgent' || normalized === 'high' || normalized === 'medium' || normalized === 'low') {
266+
return normalized;
267+
}
268+
throw new Error(`Invalid mission priority "${value}". Expected urgent|high|medium|low.`);
269+
}
270+
271+
function normalizeMissionStatus(value: string): workgraph.MissionStatus {
272+
const normalized = String(value).trim().toLowerCase();
273+
if (
274+
normalized === 'planning'
275+
|| normalized === 'approved'
276+
|| normalized === 'active'
277+
|| normalized === 'validating'
278+
|| normalized === 'completed'
279+
|| normalized === 'failed'
280+
) {
281+
return normalized;
282+
}
283+
throw new Error(`Invalid mission status "${value}". Expected planning|approved|active|validating|completed|failed.`);
284+
}
285+
286+
function parseOptionalInt(value: unknown): number | undefined {
287+
if (value === undefined || value === null || String(value).trim() === '') return undefined;
288+
const parsed = Number.parseInt(String(value), 10);
289+
if (!Number.isFinite(parsed)) {
290+
throw new Error(`Invalid integer value "${String(value)}".`);
291+
}
292+
return parsed;
293+
}
294+
295+
function parseOptionalNumber(value: unknown): number | null | undefined {
296+
if (value === undefined || value === null || String(value).trim() === '') return undefined;
297+
const parsed = Number(value);
298+
if (!Number.isFinite(parsed)) {
299+
throw new Error(`Invalid number value "${String(value)}".`);
300+
}
301+
return parsed;
302+
}
303+
304+
function parseSkipFeature(
305+
value: unknown,
306+
): { milestoneId: string; threadPath: string } | null {
307+
if (value === undefined || value === null) return null;
308+
const raw = String(value).trim();
309+
if (!raw) return null;
310+
const separator = raw.indexOf(':');
311+
if (separator <= 0 || separator >= raw.length - 1) {
312+
throw new Error('Invalid --skip-feature value. Expected "<milestoneId>:<threadPath>".');
313+
}
314+
return {
315+
milestoneId: raw.slice(0, separator).trim(),
316+
threadPath: raw.slice(separator + 1).trim(),
317+
};
318+
}

0 commit comments

Comments
 (0)