omni-pi 0.1.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.
- package/CREDITS.md +28 -0
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/agents/brain.md +24 -0
- package/agents/expert.md +21 -0
- package/agents/planner.md +22 -0
- package/agents/worker.md +21 -0
- package/bin/omni.js +79 -0
- package/extensions/omni-core/index.ts +22 -0
- package/extensions/omni-memory/index.ts +72 -0
- package/extensions/omni-skills/index.ts +11 -0
- package/extensions/omni-status/index.ts +11 -0
- package/package.json +75 -0
- package/prompts/brainstorm.md +15 -0
- package/prompts/spec-template.md +14 -0
- package/prompts/task-template.md +16 -0
- package/skills/omni-escalation/SKILL.md +17 -0
- package/skills/omni-execution/SKILL.md +18 -0
- package/skills/omni-init/SKILL.md +19 -0
- package/skills/omni-planning/SKILL.md +19 -0
- package/skills/omni-verification/SKILL.md +18 -0
- package/src/commands.ts +521 -0
- package/src/config.ts +154 -0
- package/src/context.ts +165 -0
- package/src/contracts.ts +183 -0
- package/src/doctor.ts +225 -0
- package/src/git.ts +135 -0
- package/src/memory.ts +25 -0
- package/src/pi.ts +240 -0
- package/src/planning.ts +303 -0
- package/src/plans.ts +247 -0
- package/src/repo.ts +210 -0
- package/src/skills.ts +308 -0
- package/src/status.ts +105 -0
- package/src/subagents.ts +1031 -0
- package/src/sync.ts +70 -0
- package/src/tasks.ts +141 -0
- package/src/templates.ts +261 -0
- package/src/work.ts +345 -0
- package/src/workflow.ts +375 -0
- package/templates/omni/DECISIONS.md +10 -0
- package/templates/omni/IDEAS.md +13 -0
- package/templates/omni/PROJECT.md +19 -0
- package/templates/omni/SESSION-SUMMARY.md +13 -0
- package/templates/omni/SKILLS.md +21 -0
- package/templates/omni/SPEC.md +11 -0
- package/templates/omni/STATE.md +7 -0
- package/templates/omni/TASKS.md +6 -0
- package/templates/omni/TESTS.md +17 -0
- package/templates/omni/research/README.md +3 -0
- package/templates/omni/specs/README.md +3 -0
- package/templates/omni/tasks/README.md +3 -0
- package/templates/pi/agents/omni-expert.md +13 -0
- package/templates/pi/agents/omni-worker.md +13 -0
package/src/commands.ts
ADDED
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
AVAILABLE_MODELS,
|
|
5
|
+
readConfig,
|
|
6
|
+
updateModelConfig,
|
|
7
|
+
writeConfig,
|
|
8
|
+
} from "./config.js";
|
|
9
|
+
import {
|
|
10
|
+
type ConversationBrief,
|
|
11
|
+
detectPreset,
|
|
12
|
+
WORKFLOW_PRESETS,
|
|
13
|
+
type WorkflowPreset,
|
|
14
|
+
} from "./contracts.js";
|
|
15
|
+
import { renderDoctorReport, runDoctor } from "./doctor.js";
|
|
16
|
+
import {
|
|
17
|
+
commitChanges,
|
|
18
|
+
createBranch,
|
|
19
|
+
prepareCommitPlan,
|
|
20
|
+
stageFiles,
|
|
21
|
+
} from "./git.js";
|
|
22
|
+
import type { AppCommandDefinition, CommandResult } from "./pi.js";
|
|
23
|
+
import type { SkillInstallResult } from "./skills.js";
|
|
24
|
+
import {
|
|
25
|
+
appendSkillUsageNote,
|
|
26
|
+
applyInstallResults,
|
|
27
|
+
readSkillRegistry,
|
|
28
|
+
renderSkillRegistry,
|
|
29
|
+
} from "./skills.js";
|
|
30
|
+
import { renderMetrics, renderPlainStatus } from "./status.js";
|
|
31
|
+
import {
|
|
32
|
+
createChainWorkEngine,
|
|
33
|
+
createSubagentWorkEngine,
|
|
34
|
+
loadRunHistory,
|
|
35
|
+
} from "./subagents.js";
|
|
36
|
+
import type { WorkEngine } from "./work.js";
|
|
37
|
+
import { prepareNextTaskDispatch } from "./work.js";
|
|
38
|
+
import {
|
|
39
|
+
initializeOmniProject,
|
|
40
|
+
planOmniProject,
|
|
41
|
+
readOmniStatus,
|
|
42
|
+
syncOmniProject,
|
|
43
|
+
workOnOmniProject,
|
|
44
|
+
} from "./workflow.js";
|
|
45
|
+
|
|
46
|
+
let runtimeWorkEngineFactory: typeof createSubagentWorkEngine =
|
|
47
|
+
createSubagentWorkEngine;
|
|
48
|
+
|
|
49
|
+
export function setRuntimeWorkEngineFactoryForTests(
|
|
50
|
+
factory: typeof createSubagentWorkEngine,
|
|
51
|
+
): void {
|
|
52
|
+
runtimeWorkEngineFactory = factory;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function resetRuntimeWorkEngineFactoryForTests(): void {
|
|
56
|
+
runtimeWorkEngineFactory = createSubagentWorkEngine;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const placeholderEngine: WorkEngine = {
|
|
60
|
+
async runWorkerTask(task, attempt) {
|
|
61
|
+
return {
|
|
62
|
+
summary: `Prepared ${task.id} for worker execution (attempt ${attempt}).`,
|
|
63
|
+
verification: {
|
|
64
|
+
taskId: task.id,
|
|
65
|
+
passed: false,
|
|
66
|
+
checksRun: ["dispatch-pending"],
|
|
67
|
+
failureSummary: [
|
|
68
|
+
"Direct worker execution is not wired into Pi runtime yet.",
|
|
69
|
+
],
|
|
70
|
+
retryRecommended: true,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
async runExpertTask(task) {
|
|
75
|
+
return {
|
|
76
|
+
summary: `Prepared ${task.id} for expert execution.`,
|
|
77
|
+
verification: {
|
|
78
|
+
taskId: task.id,
|
|
79
|
+
passed: false,
|
|
80
|
+
checksRun: ["dispatch-pending"],
|
|
81
|
+
failureSummary: ["Expert execution is not wired into Pi runtime yet."],
|
|
82
|
+
retryRecommended: false,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
function briefFromArgs(args: string[] | undefined): ConversationBrief {
|
|
89
|
+
const joined = args?.join(" ").trim() ?? "";
|
|
90
|
+
const presetMatch = joined.match(/^--preset\s+(\S+)\s*(.*)/u);
|
|
91
|
+
let preset: WorkflowPreset | undefined;
|
|
92
|
+
let summary: string;
|
|
93
|
+
|
|
94
|
+
if (presetMatch && presetMatch[1] in WORKFLOW_PRESETS) {
|
|
95
|
+
preset = presetMatch[1] as WorkflowPreset;
|
|
96
|
+
summary = presetMatch[2].trim() || `${preset} workflow`;
|
|
97
|
+
} else {
|
|
98
|
+
summary =
|
|
99
|
+
joined ||
|
|
100
|
+
"Create an initial implementation plan from the current project context.";
|
|
101
|
+
const detected = detectPreset("", summary);
|
|
102
|
+
if (detected) {
|
|
103
|
+
preset = detected;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
summary,
|
|
109
|
+
desiredOutcome: summary,
|
|
110
|
+
constraints: [],
|
|
111
|
+
userSignals: [],
|
|
112
|
+
preset,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function createOmniCommands(): AppCommandDefinition[] {
|
|
117
|
+
return [
|
|
118
|
+
{
|
|
119
|
+
name: "omni-init",
|
|
120
|
+
description: "Initialize Omni-Pi for the current project.",
|
|
121
|
+
execute: async ({ cwd, args, runtime }) => {
|
|
122
|
+
const result = await initializeOmniProject(cwd);
|
|
123
|
+
|
|
124
|
+
const skipWizard = args?.includes("--quick") ?? false;
|
|
125
|
+
if (runtime && !skipWizard) {
|
|
126
|
+
const ui = runtime.ctx.ui;
|
|
127
|
+
|
|
128
|
+
const cleanup = await ui.confirm(
|
|
129
|
+
"Plan cleanup",
|
|
130
|
+
"Auto-delete completed plan files? (You can change this later in CONFIG.md)",
|
|
131
|
+
);
|
|
132
|
+
const config = await readConfig(cwd);
|
|
133
|
+
const updatedConfig = { ...config, cleanupCompletedPlans: cleanup };
|
|
134
|
+
|
|
135
|
+
const goal = await ui.input(
|
|
136
|
+
"What are you building?",
|
|
137
|
+
"e.g., a CLI tool for managing tasks",
|
|
138
|
+
);
|
|
139
|
+
if (goal) {
|
|
140
|
+
const projectPath = path.join(cwd, ".omni", "PROJECT.md");
|
|
141
|
+
const project = await readFile(projectPath, "utf8");
|
|
142
|
+
const updated = project.replace(
|
|
143
|
+
"Describe what this project should achieve.",
|
|
144
|
+
goal,
|
|
145
|
+
);
|
|
146
|
+
await writeFile(projectPath, updated, "utf8");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const presetOptions = Object.values(WORKFLOW_PRESETS).map(
|
|
150
|
+
(p) => `${p.name} — ${p.description}`,
|
|
151
|
+
);
|
|
152
|
+
const presetChoice = await ui.select(
|
|
153
|
+
"Preferred workflow for the first plan?",
|
|
154
|
+
["(none — decide later)", ...presetOptions],
|
|
155
|
+
);
|
|
156
|
+
let suggestedPreset: string | undefined;
|
|
157
|
+
if (presetChoice && !presetChoice.startsWith("(none")) {
|
|
158
|
+
suggestedPreset = presetChoice.split(" — ")[0];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
await writeConfig(cwd, updatedConfig);
|
|
162
|
+
|
|
163
|
+
if (suggestedPreset) {
|
|
164
|
+
await ui.notify(
|
|
165
|
+
`Tip: run /omni-plan --preset ${suggestedPreset} to start planning with the ${suggestedPreset} workflow.`,
|
|
166
|
+
"info",
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const installNotes: string[] = [];
|
|
172
|
+
const installResults: SkillInstallResult[] = [];
|
|
173
|
+
|
|
174
|
+
if (runtime && result.installSteps.length > 0) {
|
|
175
|
+
for (const step of result.installSteps) {
|
|
176
|
+
const skillName =
|
|
177
|
+
result.installedSkills.find((s) => step.summary.includes(s.name))
|
|
178
|
+
?.name ?? step.summary;
|
|
179
|
+
const execResult = await runtime.pi.exec(step.command, step.args, {
|
|
180
|
+
cwd,
|
|
181
|
+
});
|
|
182
|
+
if (execResult.code === 0) {
|
|
183
|
+
installNotes.push(`${step.summary}: installed`);
|
|
184
|
+
installResults.push({ name: skillName, success: true });
|
|
185
|
+
} else {
|
|
186
|
+
const errorMsg =
|
|
187
|
+
execResult.stderr.trim() ||
|
|
188
|
+
execResult.stdout.trim() ||
|
|
189
|
+
`exit ${execResult.code}`;
|
|
190
|
+
installNotes.push(`${step.summary}: failed (${errorMsg})`);
|
|
191
|
+
installResults.push({
|
|
192
|
+
name: skillName,
|
|
193
|
+
success: false,
|
|
194
|
+
error: errorMsg,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
for (const note of installNotes) {
|
|
200
|
+
await appendSkillUsageNote(cwd, note);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (installResults.some((r) => !r.success)) {
|
|
204
|
+
const recovery = await applyInstallResults(cwd, installResults);
|
|
205
|
+
if (recovery.deferred.length > 0) {
|
|
206
|
+
installNotes.push(
|
|
207
|
+
`Deferred ${recovery.deferred.join(", ")} to retry later`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const installSummary =
|
|
214
|
+
installNotes.length > 0
|
|
215
|
+
? ` ${installNotes.join("; ")}.`
|
|
216
|
+
: result.installCommands.length > 0
|
|
217
|
+
? ` Planned install commands: ${result.installCommands.join("; ")}.`
|
|
218
|
+
: "";
|
|
219
|
+
const healthNote =
|
|
220
|
+
result.diagnostics.overall === "red"
|
|
221
|
+
? " Health: FAIL — run /omni-doctor for details."
|
|
222
|
+
: result.diagnostics.overall === "yellow"
|
|
223
|
+
? " Health: WARN — run /omni-doctor to review."
|
|
224
|
+
: " Health: OK.";
|
|
225
|
+
return `Initialized Omni-Pi in ${cwd}. Created ${result.created.length} files, reused ${result.reused.length}, and identified ${result.skillCandidates.length} skill candidates.${installSummary}${healthNote}`;
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: "omni-plan",
|
|
230
|
+
description: "Create or refresh the current spec, tasks, and tests.",
|
|
231
|
+
execute: async ({ cwd, args, runtime }) => {
|
|
232
|
+
const brief = briefFromArgs(args);
|
|
233
|
+
|
|
234
|
+
if (runtime) {
|
|
235
|
+
const ui = runtime.ctx.ui;
|
|
236
|
+
const presetConfig = brief.preset
|
|
237
|
+
? WORKFLOW_PRESETS[brief.preset]
|
|
238
|
+
: undefined;
|
|
239
|
+
|
|
240
|
+
if (!presetConfig?.skipInterview) {
|
|
241
|
+
const constraints = await ui.input(
|
|
242
|
+
"Any constraints or requirements to add?",
|
|
243
|
+
"e.g., must use existing auth system",
|
|
244
|
+
);
|
|
245
|
+
if (constraints) {
|
|
246
|
+
brief.constraints.push(constraints);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const userContext = await ui.input(
|
|
250
|
+
"Who are the primary users?",
|
|
251
|
+
"e.g., developers, end users",
|
|
252
|
+
);
|
|
253
|
+
if (userContext) {
|
|
254
|
+
brief.userSignals.push(`Primary users: ${userContext}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const result = await planOmniProject(cwd, brief);
|
|
259
|
+
|
|
260
|
+
const approved = await ui.confirm(
|
|
261
|
+
"Plan generated",
|
|
262
|
+
`Created spec, ${result.tasksPath}, and ${result.testsPath}. Review .omni/SPEC.md and .omni/TASKS.md. Accept this plan?`,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
if (!approved) {
|
|
266
|
+
return "Plan generated but not accepted. Run /omni-plan again to refine.";
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return `Accepted planning artifacts: ${result.specPath}, ${result.tasksPath}, ${result.testsPath}.`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const result = await planOmniProject(cwd, brief);
|
|
273
|
+
return `Updated planning artifacts: ${result.specPath}, ${result.tasksPath}, ${result.testsPath}.`;
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: "omni-work",
|
|
278
|
+
description:
|
|
279
|
+
"Run the next task through worker, verifier, and expert fallback.",
|
|
280
|
+
execute: async ({ cwd, runtime }) => {
|
|
281
|
+
if (runtime) {
|
|
282
|
+
try {
|
|
283
|
+
const currentSession = runtime.ctx.sessionManager.getSessionFile();
|
|
284
|
+
const sessionResult = await runtime.ctx.newSession({
|
|
285
|
+
parentSession: currentSession,
|
|
286
|
+
});
|
|
287
|
+
if (sessionResult.cancelled) {
|
|
288
|
+
return "Omni-Pi task session was cancelled.";
|
|
289
|
+
}
|
|
290
|
+
const omniConfig = await readConfig(cwd);
|
|
291
|
+
const engineFactory = omniConfig.chainEnabled
|
|
292
|
+
? createChainWorkEngine
|
|
293
|
+
: runtimeWorkEngineFactory;
|
|
294
|
+
const engine = await engineFactory(cwd, runtime.ctx, undefined, {
|
|
295
|
+
exec: runtime.pi.exec.bind(runtime.pi),
|
|
296
|
+
});
|
|
297
|
+
const result = await workOnOmniProject(cwd, engine);
|
|
298
|
+
runtime.ctx.ui.setStatus("omni", undefined);
|
|
299
|
+
const text = `${result.message} Current phase: ${result.state.currentPhase}.`;
|
|
300
|
+
if (
|
|
301
|
+
result.kind === "blocked" &&
|
|
302
|
+
result.state.currentPhase === "escalate"
|
|
303
|
+
) {
|
|
304
|
+
return {
|
|
305
|
+
text,
|
|
306
|
+
messageType: "omni-escalation",
|
|
307
|
+
details: {
|
|
308
|
+
taskId: result.taskId,
|
|
309
|
+
failedChecks: result.state.blockers,
|
|
310
|
+
recoveryOptions: result.state.recoveryOptions,
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
if (
|
|
315
|
+
result.kind === "completed" ||
|
|
316
|
+
result.kind === "expert_completed"
|
|
317
|
+
) {
|
|
318
|
+
return {
|
|
319
|
+
text,
|
|
320
|
+
messageType: "omni-verification",
|
|
321
|
+
details: {
|
|
322
|
+
passed: true,
|
|
323
|
+
checksRun: [],
|
|
324
|
+
failureSummary: [],
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
return text;
|
|
329
|
+
} catch (error) {
|
|
330
|
+
runtime.ctx.ui.notify(
|
|
331
|
+
`pi-subagents integration unavailable, falling back to guided handoff: ${error instanceof Error ? error.message : String(error)}`,
|
|
332
|
+
"warning",
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const dispatch = await prepareNextTaskDispatch(cwd);
|
|
337
|
+
if (dispatch.kind === "idle") {
|
|
338
|
+
return dispatch.message;
|
|
339
|
+
}
|
|
340
|
+
const currentSessionFile =
|
|
341
|
+
runtime.ctx.sessionManager.getSessionFile();
|
|
342
|
+
const newSessionResult = await runtime.ctx.newSession({
|
|
343
|
+
parentSession: currentSessionFile,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
if (newSessionResult.cancelled) {
|
|
347
|
+
return "Omni-Pi task dispatch was cancelled before the focused session was created.";
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
runtime.ctx.ui.setEditorText(dispatch.prompt);
|
|
351
|
+
runtime.ctx.ui.setStatus(
|
|
352
|
+
"omni",
|
|
353
|
+
`Prepared ${dispatch.taskId} in a fresh session`,
|
|
354
|
+
);
|
|
355
|
+
return `Prepared ${dispatch.taskId} in a fresh focused session. Review the drafted prompt and submit when ready.`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const result = await workOnOmniProject(cwd, placeholderEngine);
|
|
359
|
+
return `${result.message} Current phase: ${result.state.currentPhase}.`;
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
name: "omni-status",
|
|
364
|
+
description: "Show the current phase, task, blockers, and next step.",
|
|
365
|
+
execute: async ({ cwd, args, runtime }): Promise<CommandResult> => {
|
|
366
|
+
if (args?.includes("metrics")) {
|
|
367
|
+
const history = await loadRunHistory();
|
|
368
|
+
if (!history) {
|
|
369
|
+
return "Run history is not available (pi-subagents run-history module not found).";
|
|
370
|
+
}
|
|
371
|
+
const workerRuns = history.loadRunsForAgent("omni-worker");
|
|
372
|
+
const expertRuns = history.loadRunsForAgent("omni-expert");
|
|
373
|
+
return renderMetrics(workerRuns, expertRuns);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const state = await readOmniStatus(cwd);
|
|
377
|
+
if (runtime) {
|
|
378
|
+
return {
|
|
379
|
+
text: renderPlainStatus(state),
|
|
380
|
+
messageType: "omni-status",
|
|
381
|
+
details: {
|
|
382
|
+
phase: state.currentPhase,
|
|
383
|
+
activeTask: state.activeTask,
|
|
384
|
+
blockers: state.blockers,
|
|
385
|
+
nextStep: state.nextStep,
|
|
386
|
+
recoveryOptions: state.recoveryOptions,
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return renderPlainStatus(state);
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
name: "omni-sync",
|
|
395
|
+
description:
|
|
396
|
+
"Update durable Omni-Pi project memory from recent progress.",
|
|
397
|
+
execute: async ({ cwd, args }) => {
|
|
398
|
+
const summary =
|
|
399
|
+
args?.join(" ").trim() ||
|
|
400
|
+
"Captured recent progress without additional details.";
|
|
401
|
+
const result = await syncOmniProject(cwd, {
|
|
402
|
+
summary,
|
|
403
|
+
nextHandoffNotes: [summary],
|
|
404
|
+
});
|
|
405
|
+
return `Synced Omni-Pi memory. Current phase: ${result.state.currentPhase}.`;
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
name: "omni-skills",
|
|
410
|
+
description:
|
|
411
|
+
"Show installed, recommended, deferred, and rejected skills.",
|
|
412
|
+
execute: async ({ cwd }) => {
|
|
413
|
+
const registry = await readSkillRegistry(cwd);
|
|
414
|
+
const skillsPath = path.join(cwd, ".omni", "SKILLS.md");
|
|
415
|
+
await readFile(skillsPath, "utf8");
|
|
416
|
+
return renderSkillRegistry(registry);
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
name: "omni-explain",
|
|
421
|
+
description: "Explain what Omni-Pi is doing and why.",
|
|
422
|
+
execute: async () =>
|
|
423
|
+
"Omni-Pi works in guided steps: understand the goal, plan the next slice, build it, check it, and escalate only when needed.",
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
name: "omni-model",
|
|
427
|
+
description: "Interactively select the model for a specific agent role.",
|
|
428
|
+
execute: async ({ cwd, runtime }) => {
|
|
429
|
+
if (!runtime) {
|
|
430
|
+
return "omni-model requires the Pi runtime with an interactive UI.";
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const ui = runtime.ctx.ui;
|
|
434
|
+
const agentOptions = ["worker", "expert", "planner", "brain"];
|
|
435
|
+
const selectedAgent = await ui.select(
|
|
436
|
+
"Select agent role to configure:",
|
|
437
|
+
agentOptions,
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
if (!selectedAgent) {
|
|
441
|
+
return "Model selection cancelled.";
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const currentConfig = await readConfig(cwd);
|
|
445
|
+
const currentModel =
|
|
446
|
+
currentConfig.models[
|
|
447
|
+
selectedAgent as keyof typeof currentConfig.models
|
|
448
|
+
];
|
|
449
|
+
const modelOptions = AVAILABLE_MODELS.map((model) =>
|
|
450
|
+
model === currentModel ? `${model} (current)` : model,
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
const selectedModelDisplay = await ui.select(
|
|
454
|
+
`Select model for ${selectedAgent}:`,
|
|
455
|
+
modelOptions,
|
|
456
|
+
);
|
|
457
|
+
if (!selectedModelDisplay) {
|
|
458
|
+
return "Model selection cancelled.";
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const selectedModel = selectedModelDisplay.replace(" (current)", "");
|
|
462
|
+
await updateModelConfig(cwd, selectedAgent, selectedModel);
|
|
463
|
+
|
|
464
|
+
return `Updated ${selectedAgent} model to ${selectedModel}. Configuration saved to .omni/CONFIG.md`;
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
name: "omni-commit",
|
|
469
|
+
description: "Create a branch and commit for the last completed task.",
|
|
470
|
+
execute: async ({ cwd, runtime }) => {
|
|
471
|
+
const plan = await prepareCommitPlan(cwd);
|
|
472
|
+
if (!plan) {
|
|
473
|
+
return "No completed tasks found. Run /omni-work first.";
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (!runtime) {
|
|
477
|
+
return `Commit plan for ${plan.taskId}: branch=${plan.branch}, files=${plan.files.join(", ") || "none tracked"}, message=${plan.message.split("\n")[0]}`;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const exec = runtime.pi.exec.bind(runtime.pi);
|
|
481
|
+
|
|
482
|
+
if (plan.files.length === 0) {
|
|
483
|
+
return `No modified files tracked for ${plan.taskId}. Stage and commit manually, or ensure /omni-work tracked file changes.`;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const branchOk = await createBranch(exec, cwd, plan.branch);
|
|
487
|
+
if (!branchOk) {
|
|
488
|
+
return `Failed to create branch ${plan.branch}. It may already exist.`;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const stageOk = await stageFiles(exec, cwd, plan.files);
|
|
492
|
+
if (!stageOk) {
|
|
493
|
+
return `Failed to stage files for ${plan.taskId}: ${plan.files.join(", ")}`;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const commitOk = await commitChanges(exec, cwd, plan.message);
|
|
497
|
+
if (!commitOk) {
|
|
498
|
+
return `Failed to commit for ${plan.taskId}. Check git status.`;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return `Committed ${plan.taskId} on branch ${plan.branch}. ${plan.files.length} files staged.`;
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
name: "omni-doctor",
|
|
506
|
+
description:
|
|
507
|
+
"Run diagnostic health checks on the project and detect stuck tasks.",
|
|
508
|
+
execute: async ({ cwd }): Promise<CommandResult> => {
|
|
509
|
+
const report = await runDoctor(cwd);
|
|
510
|
+
return {
|
|
511
|
+
text: renderDoctorReport(report),
|
|
512
|
+
messageType: "omni-status",
|
|
513
|
+
details: {
|
|
514
|
+
title: "Omni-Pi Doctor",
|
|
515
|
+
phase: report.overall,
|
|
516
|
+
},
|
|
517
|
+
};
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
];
|
|
521
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { OmniConfig } from "./contracts.js";
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_CONFIG: OmniConfig = {
|
|
7
|
+
models: {
|
|
8
|
+
worker: "anthropic/claude-sonnet-4-6",
|
|
9
|
+
expert: "openai/gpt-5.4",
|
|
10
|
+
planner: "openai/gpt-5.4",
|
|
11
|
+
brain: "anthropic/claude-opus-4-6",
|
|
12
|
+
},
|
|
13
|
+
retryLimit: 2,
|
|
14
|
+
chainEnabled: false,
|
|
15
|
+
cleanupCompletedPlans: false,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const CONFIG_PATH = ".omni/CONFIG.md";
|
|
19
|
+
|
|
20
|
+
function parseModelTable(
|
|
21
|
+
content: string,
|
|
22
|
+
heading: string,
|
|
23
|
+
): Record<string, string> {
|
|
24
|
+
const sectionRegex = new RegExp(
|
|
25
|
+
`${heading}\\n\\n\\| Agent \\| Model \\|\\n\\|-+\\|-+\\|\\n([\\s\\S]*?)(?=\\n## |$)`,
|
|
26
|
+
"u",
|
|
27
|
+
);
|
|
28
|
+
const match = content.match(sectionRegex);
|
|
29
|
+
if (!match?.[1]) {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const models: Record<string, string> = {};
|
|
34
|
+
const lines = match[1].trim().split("\n");
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
const rowMatch = line.match(/\|\s*(\w+)\s*\|\s*([^|]+)\s*\|/u);
|
|
37
|
+
if (rowMatch) {
|
|
38
|
+
const agent = rowMatch[1].trim().toLowerCase();
|
|
39
|
+
const model = rowMatch[2].trim();
|
|
40
|
+
models[agent] = model;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return models;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseRetryLimit(content: string): number {
|
|
47
|
+
const match = content.match(
|
|
48
|
+
/Worker retries before expert takeover:\s*(\d+)/u,
|
|
49
|
+
);
|
|
50
|
+
return match ? Number.parseInt(match[1], 10) : DEFAULT_CONFIG.retryLimit;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseChainEnabled(content: string): boolean {
|
|
54
|
+
const match = content.match(/Chain execution enabled:\s*(true|false)/u);
|
|
55
|
+
return match ? match[1] === "true" : DEFAULT_CONFIG.chainEnabled;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseCleanupCompletedPlans(content: string): boolean {
|
|
59
|
+
const match = content.match(/Delete completed plan files:\s*(true|false)/u);
|
|
60
|
+
return match ? match[1] === "true" : DEFAULT_CONFIG.cleanupCompletedPlans;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function readConfig(rootDir: string): Promise<OmniConfig> {
|
|
64
|
+
const configPath = path.join(rootDir, CONFIG_PATH);
|
|
65
|
+
try {
|
|
66
|
+
const content = await readFile(configPath, "utf8");
|
|
67
|
+
const models = parseModelTable(content, "## Models");
|
|
68
|
+
const retryLimit = parseRetryLimit(content);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
models: {
|
|
72
|
+
worker: models.worker ?? DEFAULT_CONFIG.models.worker,
|
|
73
|
+
expert: models.expert ?? DEFAULT_CONFIG.models.expert,
|
|
74
|
+
planner: models.planner ?? DEFAULT_CONFIG.models.planner,
|
|
75
|
+
brain: models.brain ?? DEFAULT_CONFIG.models.brain,
|
|
76
|
+
},
|
|
77
|
+
retryLimit,
|
|
78
|
+
chainEnabled: parseChainEnabled(content),
|
|
79
|
+
cleanupCompletedPlans: parseCleanupCompletedPlans(content),
|
|
80
|
+
};
|
|
81
|
+
} catch {
|
|
82
|
+
return { ...DEFAULT_CONFIG };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function renderConfigContent(config: OmniConfig): string {
|
|
87
|
+
return `# Omni-Pi Configuration
|
|
88
|
+
|
|
89
|
+
## Models
|
|
90
|
+
|
|
91
|
+
| Agent | Model |
|
|
92
|
+
|-------|-------|
|
|
93
|
+
| worker | ${config.models.worker} |
|
|
94
|
+
| expert | ${config.models.expert} |
|
|
95
|
+
| planner | ${config.models.planner} |
|
|
96
|
+
| brain | ${config.models.brain} |
|
|
97
|
+
|
|
98
|
+
## Retry Policy
|
|
99
|
+
|
|
100
|
+
Worker retries before expert takeover: ${config.retryLimit}
|
|
101
|
+
|
|
102
|
+
## Execution
|
|
103
|
+
|
|
104
|
+
Chain execution enabled: ${config.chainEnabled}
|
|
105
|
+
|
|
106
|
+
## Memory
|
|
107
|
+
|
|
108
|
+
Delete completed plan files: ${config.cleanupCompletedPlans}
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function writeConfig(
|
|
113
|
+
rootDir: string,
|
|
114
|
+
config: OmniConfig,
|
|
115
|
+
): Promise<void> {
|
|
116
|
+
const configPath = path.join(rootDir, CONFIG_PATH);
|
|
117
|
+
await mkdir(path.dirname(configPath), { recursive: true });
|
|
118
|
+
await writeFile(configPath, renderConfigContent(config), "utf8");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function updateModelConfig(
|
|
122
|
+
rootDir: string,
|
|
123
|
+
agent: string,
|
|
124
|
+
model: string,
|
|
125
|
+
): Promise<OmniConfig> {
|
|
126
|
+
const config = await readConfig(rootDir);
|
|
127
|
+
const validAgents = ["worker", "expert", "planner", "brain"] as const;
|
|
128
|
+
const normalizedAgent = agent.toLowerCase() as (typeof validAgents)[number];
|
|
129
|
+
|
|
130
|
+
if (!validAgents.includes(normalizedAgent)) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Invalid agent: ${agent}. Valid agents: ${validAgents.join(", ")}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
config.models[normalizedAgent] = model;
|
|
137
|
+
await writeConfig(rootDir, config);
|
|
138
|
+
return config;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export const AVAILABLE_MODELS = [
|
|
142
|
+
"anthropic/claude-sonnet-4-6",
|
|
143
|
+
"anthropic/claude-opus-4-6",
|
|
144
|
+
"anthropic/claude-sonnet-4-5",
|
|
145
|
+
"anthropic/claude-opus-4-1",
|
|
146
|
+
"openai/gpt-5.4",
|
|
147
|
+
"openai/gpt-5",
|
|
148
|
+
"openai/gpt-4.1",
|
|
149
|
+
"openai/gpt-4o",
|
|
150
|
+
"openai/o3-mini",
|
|
151
|
+
"openai/o1",
|
|
152
|
+
"google/gemini-2.5-pro",
|
|
153
|
+
"google/gemini-2.5-flash",
|
|
154
|
+
];
|