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.
Files changed (54) hide show
  1. package/CREDITS.md +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +81 -0
  4. package/agents/brain.md +24 -0
  5. package/agents/expert.md +21 -0
  6. package/agents/planner.md +22 -0
  7. package/agents/worker.md +21 -0
  8. package/bin/omni.js +79 -0
  9. package/extensions/omni-core/index.ts +22 -0
  10. package/extensions/omni-memory/index.ts +72 -0
  11. package/extensions/omni-skills/index.ts +11 -0
  12. package/extensions/omni-status/index.ts +11 -0
  13. package/package.json +75 -0
  14. package/prompts/brainstorm.md +15 -0
  15. package/prompts/spec-template.md +14 -0
  16. package/prompts/task-template.md +16 -0
  17. package/skills/omni-escalation/SKILL.md +17 -0
  18. package/skills/omni-execution/SKILL.md +18 -0
  19. package/skills/omni-init/SKILL.md +19 -0
  20. package/skills/omni-planning/SKILL.md +19 -0
  21. package/skills/omni-verification/SKILL.md +18 -0
  22. package/src/commands.ts +521 -0
  23. package/src/config.ts +154 -0
  24. package/src/context.ts +165 -0
  25. package/src/contracts.ts +183 -0
  26. package/src/doctor.ts +225 -0
  27. package/src/git.ts +135 -0
  28. package/src/memory.ts +25 -0
  29. package/src/pi.ts +240 -0
  30. package/src/planning.ts +303 -0
  31. package/src/plans.ts +247 -0
  32. package/src/repo.ts +210 -0
  33. package/src/skills.ts +308 -0
  34. package/src/status.ts +105 -0
  35. package/src/subagents.ts +1031 -0
  36. package/src/sync.ts +70 -0
  37. package/src/tasks.ts +141 -0
  38. package/src/templates.ts +261 -0
  39. package/src/work.ts +345 -0
  40. package/src/workflow.ts +375 -0
  41. package/templates/omni/DECISIONS.md +10 -0
  42. package/templates/omni/IDEAS.md +13 -0
  43. package/templates/omni/PROJECT.md +19 -0
  44. package/templates/omni/SESSION-SUMMARY.md +13 -0
  45. package/templates/omni/SKILLS.md +21 -0
  46. package/templates/omni/SPEC.md +11 -0
  47. package/templates/omni/STATE.md +7 -0
  48. package/templates/omni/TASKS.md +6 -0
  49. package/templates/omni/TESTS.md +17 -0
  50. package/templates/omni/research/README.md +3 -0
  51. package/templates/omni/specs/README.md +3 -0
  52. package/templates/omni/tasks/README.md +3 -0
  53. package/templates/pi/agents/omni-expert.md +13 -0
  54. package/templates/pi/agents/omni-worker.md +13 -0
package/src/skills.ts ADDED
@@ -0,0 +1,308 @@
1
+ import { readdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import type { SkillCandidate, SkillPolicy, TaskBrief } from "./contracts.js";
4
+
5
+ export interface SkillSignal {
6
+ label: string;
7
+ packages?: string[];
8
+ files?: string[];
9
+ reason: string;
10
+ policy?: SkillPolicy;
11
+ }
12
+
13
+ export const defaultSkillSignals: SkillSignal[] = [
14
+ {
15
+ label: "find-skills",
16
+ reason: "Discover project-relevant skills during init and planning.",
17
+ policy: "auto-install",
18
+ },
19
+ {
20
+ label: "agent-browser",
21
+ files: ["playwright.config.ts", "cypress.config.ts"],
22
+ reason: "Useful when the project needs browser automation or UI testing.",
23
+ policy: "recommend-only",
24
+ },
25
+ {
26
+ label: "rust-debugging",
27
+ files: ["Cargo.toml"],
28
+ reason:
29
+ "Useful when a Rust project needs debugging or panic investigation.",
30
+ policy: "recommend-only",
31
+ },
32
+ {
33
+ label: "rust-ui-architecture",
34
+ files: ["Cargo.toml"],
35
+ reason: "Useful when a Rust UI project needs architectural guidance.",
36
+ policy: "recommend-only",
37
+ },
38
+ ];
39
+
40
+ export function toSkillCandidate(signal: SkillSignal): SkillCandidate {
41
+ return {
42
+ name: signal.label,
43
+ reason: signal.reason,
44
+ confidence: signal.policy === "auto-install" ? "high" : "medium",
45
+ policy: signal.policy ?? "recommend-only",
46
+ };
47
+ }
48
+
49
+ export function renderSkillDecision(candidate: SkillCandidate): string {
50
+ return `- ${candidate.name} [${candidate.policy}] - ${candidate.reason}`;
51
+ }
52
+
53
+ export interface SkillRegistry {
54
+ installed: SkillCandidate[];
55
+ recommended: SkillCandidate[];
56
+ deferred: SkillCandidate[];
57
+ rejected: SkillCandidate[];
58
+ }
59
+
60
+ function parseSection(content: string, heading: string): string[] {
61
+ const escapedHeading = heading.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
62
+ const sectionRegex = new RegExp(
63
+ `${escapedHeading}\\n\\n([\\s\\S]*?)(?=\\n## |$)`,
64
+ "u",
65
+ );
66
+ const match = content.match(sectionRegex)?.[1] ?? "";
67
+ return match
68
+ .split("\n")
69
+ .map((line) => line.trim())
70
+ .filter((line) => line.startsWith("- ") && line !== "- None yet");
71
+ }
72
+
73
+ function parseSkillLine(line: string): SkillCandidate {
74
+ const value = line.slice(2);
75
+ const match = value.match(/^(.*?)\s+\[(.*?)\]\s+-\s+(.*)$/u);
76
+ if (match) {
77
+ return {
78
+ name: match[1].trim(),
79
+ policy: match[2].trim() as SkillPolicy,
80
+ reason: match[3].trim(),
81
+ confidence: match[2].trim() === "auto-install" ? "high" : "medium",
82
+ };
83
+ }
84
+
85
+ return {
86
+ name: value.trim(),
87
+ policy: "recommend-only",
88
+ reason: "No reason recorded.",
89
+ confidence: "low",
90
+ };
91
+ }
92
+
93
+ export function parseSkillRegistry(content: string): SkillRegistry {
94
+ return {
95
+ installed: parseSection(content, "## Installed").map(parseSkillLine),
96
+ recommended: parseSection(content, "## Recommended").map(parseSkillLine),
97
+ deferred: parseSection(content, "## Deferred").map(parseSkillLine),
98
+ rejected: parseSection(content, "## Rejected").map(parseSkillLine),
99
+ };
100
+ }
101
+
102
+ export function renderSkillRegistry(registry: SkillRegistry): string {
103
+ const sections: Array<[string, SkillCandidate[]]> = [
104
+ ["Installed", registry.installed],
105
+ ["Recommended", registry.recommended],
106
+ ["Deferred", registry.deferred],
107
+ ["Rejected", registry.rejected],
108
+ ];
109
+
110
+ return sections
111
+ .map(([title, skills]) => {
112
+ const items =
113
+ skills.length > 0 ? skills.map(renderSkillDecision) : ["- None yet"];
114
+ return `${title}:\n${items.join("\n")}`;
115
+ })
116
+ .join("\n\n");
117
+ }
118
+
119
+ export async function readSkillRegistry(
120
+ rootDir: string,
121
+ ): Promise<SkillRegistry> {
122
+ const skillPath = path.join(rootDir, ".omni", "SKILLS.md");
123
+ return parseSkillRegistry(await readFile(skillPath, "utf8"));
124
+ }
125
+
126
+ export interface SkillInstallPlan {
127
+ commands: string[];
128
+ installed: SkillCandidate[];
129
+ steps: Array<{
130
+ command: string;
131
+ args: string[];
132
+ summary: string;
133
+ }>;
134
+ }
135
+
136
+ export function buildSkillInstallPlan(
137
+ candidates: SkillCandidate[],
138
+ ): SkillInstallPlan {
139
+ const installed = candidates.filter(
140
+ (candidate) => candidate.policy === "auto-install",
141
+ );
142
+ const steps = installed.map((candidate) => ({
143
+ command: "npx",
144
+ args: [
145
+ "skills",
146
+ "add",
147
+ "https://github.com/vercel-labs/skills",
148
+ "--skill",
149
+ candidate.name,
150
+ ],
151
+ summary: `Install ${candidate.name}`,
152
+ }));
153
+ const commands = steps.map((step) => [step.command, ...step.args].join(" "));
154
+ return { commands, installed, steps };
155
+ }
156
+
157
+ export async function appendSkillUsageNote(
158
+ rootDir: string,
159
+ note: string,
160
+ ): Promise<void> {
161
+ const skillPath = path.join(rootDir, ".omni", "SKILLS.md");
162
+ const content = await readFile(skillPath, "utf8");
163
+ const next = content.replace(
164
+ /## Usage Notes\n\n([\s\S]*)$/u,
165
+ (_match, section) => `## Usage Notes\n\n${section.trimEnd()}\n- ${note}\n`,
166
+ );
167
+ await writeFile(skillPath, next, "utf8");
168
+ }
169
+
170
+ export interface SkillInstallResult {
171
+ name: string;
172
+ success: boolean;
173
+ error?: string;
174
+ }
175
+
176
+ function replaceSection(
177
+ content: string,
178
+ heading: string,
179
+ lines: string[],
180
+ ): string {
181
+ const escapedHeading = heading.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
182
+ const sectionRegex = new RegExp(
183
+ `(${escapedHeading}\\n\\n)([\\s\\S]*?)(?=\\n## |$)`,
184
+ "u",
185
+ );
186
+ const replacement = `$1${lines.join("\n")}\n`;
187
+ return content.match(sectionRegex)
188
+ ? content.replace(sectionRegex, replacement)
189
+ : `${content.trimEnd()}\n\n${heading}\n\n${lines.join("\n")}\n`;
190
+ }
191
+
192
+ export async function applyInstallResults(
193
+ rootDir: string,
194
+ results: SkillInstallResult[],
195
+ ): Promise<{ deferred: string[]; installed: string[] }> {
196
+ const skillPath = path.join(rootDir, ".omni", "SKILLS.md");
197
+ let content = await readFile(skillPath, "utf8");
198
+ const registry = parseSkillRegistry(content);
199
+
200
+ const installed: string[] = [];
201
+ const deferred: string[] = [];
202
+
203
+ for (const result of results) {
204
+ if (result.success) {
205
+ installed.push(result.name);
206
+ continue;
207
+ }
208
+
209
+ deferred.push(result.name);
210
+ const existing = registry.installed.find((s) => s.name === result.name);
211
+ if (existing) {
212
+ registry.installed = registry.installed.filter(
213
+ (s) => s.name !== result.name,
214
+ );
215
+ registry.deferred.push({
216
+ ...existing,
217
+ policy: "recommend-only",
218
+ reason: `${existing.reason} (install failed: ${result.error ?? "unknown error"})`,
219
+ });
220
+ } else {
221
+ registry.deferred.push({
222
+ name: result.name,
223
+ reason: `Install failed: ${result.error ?? "unknown error"}`,
224
+ confidence: "low",
225
+ policy: "recommend-only",
226
+ });
227
+ }
228
+ }
229
+
230
+ const installedLines =
231
+ registry.installed.length > 0
232
+ ? registry.installed.map(renderSkillDecision)
233
+ : ["- None yet"];
234
+ const deferredLines =
235
+ registry.deferred.length > 0
236
+ ? registry.deferred.map(renderSkillDecision)
237
+ : ["- None yet"];
238
+ content = replaceSection(content, "## Installed", installedLines);
239
+ content = replaceSection(content, "## Deferred", deferredLines);
240
+ await writeFile(skillPath, content, "utf8");
241
+
242
+ return { deferred, installed };
243
+ }
244
+
245
+ export interface SkillTrigger {
246
+ name: string;
247
+ triggers: string[];
248
+ content: string;
249
+ }
250
+
251
+ function parseTriggers(description: string): string[] {
252
+ const listMatch = description.match(/Triggers include\s+(.*)/iu);
253
+ if (!listMatch) return [];
254
+ const triggers: string[] = [];
255
+ for (const m of listMatch[1].matchAll(/"([^"]+)"/gu)) {
256
+ triggers.push(m[1]);
257
+ }
258
+ return triggers;
259
+ }
260
+
261
+ export async function loadSkillTriggers(
262
+ skillsDir: string,
263
+ ): Promise<SkillTrigger[]> {
264
+ const triggers: SkillTrigger[] = [];
265
+ try {
266
+ const entries = await readdir(skillsDir, { withFileTypes: true });
267
+ for (const entry of entries) {
268
+ if (!entry.isDirectory()) continue;
269
+ try {
270
+ const content = await readFile(
271
+ path.join(skillsDir, entry.name, "SKILL.md"),
272
+ "utf8",
273
+ );
274
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/u);
275
+ if (!frontmatterMatch) continue;
276
+ const descMatch = frontmatterMatch[1].match(/description:\s*(.*)/u);
277
+ if (!descMatch) continue;
278
+ const parsed = parseTriggers(descMatch[1]);
279
+ if (parsed.length > 0) {
280
+ triggers.push({ name: entry.name, triggers: parsed, content });
281
+ }
282
+ } catch {
283
+ /* skip unreadable skills */
284
+ }
285
+ }
286
+ } catch {
287
+ /* skills dir doesn't exist */
288
+ }
289
+ return triggers;
290
+ }
291
+
292
+ export function matchSkillsForTask(
293
+ task: TaskBrief,
294
+ skills: SkillTrigger[],
295
+ ): SkillTrigger[] {
296
+ const taskText = [
297
+ task.id,
298
+ task.title,
299
+ task.objective,
300
+ ...task.doneCriteria,
301
+ ...task.skills,
302
+ ]
303
+ .join(" ")
304
+ .toLowerCase();
305
+ return skills.filter((skill) =>
306
+ skill.triggers.some((trigger) => taskText.includes(trigger.toLowerCase())),
307
+ );
308
+ }
package/src/status.ts ADDED
@@ -0,0 +1,105 @@
1
+ import type { OmniPhase, OmniState } from "./contracts.js";
2
+ import type { HealthLevel } from "./doctor.js";
3
+ import type { RunHistoryEntry } from "./subagents.js";
4
+
5
+ const HEALTH_TAGS: Record<HealthLevel, string> = {
6
+ green: "OK",
7
+ yellow: "WARN",
8
+ red: "FAIL",
9
+ };
10
+
11
+ const phaseLabels: Record<OmniPhase, string> = {
12
+ understand: "Understand",
13
+ plan: "Plan",
14
+ build: "Build",
15
+ check: "Check",
16
+ escalate: "Escalate",
17
+ };
18
+
19
+ export function formatPhase(phase: OmniPhase): string {
20
+ return phaseLabels[phase];
21
+ }
22
+
23
+ export function renderCompactStatus(
24
+ state: OmniState,
25
+ health?: HealthLevel,
26
+ ): string[] {
27
+ const phaseBar = Object.keys(phaseLabels)
28
+ .map((key) =>
29
+ key === state.currentPhase
30
+ ? `[${phaseLabels[key as OmniPhase]}]`
31
+ : ` ${phaseLabels[key as OmniPhase]} `,
32
+ )
33
+ .join(" > ");
34
+ const healthTag = health ? ` [${HEALTH_TAGS[health]}]` : "";
35
+ const lines = [`Omni-Pi ${phaseBar}${healthTag}`];
36
+ if (state.activeTask && state.activeTask !== "None") {
37
+ lines.push(` Task: ${state.activeTask}`);
38
+ }
39
+ if (state.blockers.length > 0) {
40
+ lines.push(` Blocked: ${state.blockers.join("; ")}`);
41
+ }
42
+ lines.push(` Next: ${state.nextStep}`);
43
+ return lines;
44
+ }
45
+
46
+ export function renderPlainStatus(state: OmniState): string {
47
+ const blockers =
48
+ state.blockers.length > 0 ? state.blockers.join("; ") : "None";
49
+ const lines = [
50
+ `Phase: ${formatPhase(state.currentPhase)}`,
51
+ `Active task: ${state.activeTask}`,
52
+ `What is happening: ${state.statusSummary}`,
53
+ `Blockers: ${blockers}`,
54
+ `Next step: ${state.nextStep}`,
55
+ ];
56
+ if (state.recoveryOptions && state.recoveryOptions.length > 0) {
57
+ lines.push(
58
+ `Recovery options:\n${state.recoveryOptions.map((option) => ` - ${option}`).join("\n")}`,
59
+ );
60
+ }
61
+ return lines.join("\n");
62
+ }
63
+
64
+ export function renderMetrics(
65
+ workerRuns: RunHistoryEntry[],
66
+ expertRuns: RunHistoryEntry[],
67
+ ): string {
68
+ const allRuns = [...workerRuns, ...expertRuns];
69
+ if (allRuns.length === 0) {
70
+ return "No agent run history available yet.";
71
+ }
72
+
73
+ function stats(runs: RunHistoryEntry[]): {
74
+ total: number;
75
+ successRate: string;
76
+ avgDuration: string;
77
+ } {
78
+ if (runs.length === 0)
79
+ return { total: 0, successRate: "n/a", avgDuration: "n/a" };
80
+ const successes = runs.filter((r) => r.status === "ok").length;
81
+ const avgMs = runs.reduce((sum, r) => sum + r.duration, 0) / runs.length;
82
+ return {
83
+ total: runs.length,
84
+ successRate: `${Math.round((successes / runs.length) * 100)}%`,
85
+ avgDuration: `${(avgMs / 1000).toFixed(1)}s`,
86
+ };
87
+ }
88
+
89
+ const workerStats = stats(workerRuns);
90
+ const expertStats = stats(expertRuns);
91
+
92
+ const lines = ["Agent Metrics:"];
93
+ if (workerStats.total > 0) {
94
+ lines.push(
95
+ ` Worker: ${workerStats.total} runs, ${workerStats.successRate} success, avg ${workerStats.avgDuration}`,
96
+ );
97
+ }
98
+ if (expertStats.total > 0) {
99
+ lines.push(
100
+ ` Expert: ${expertStats.total} runs, ${expertStats.successRate} success, avg ${expertStats.avgDuration}`,
101
+ );
102
+ }
103
+ lines.push(` Total: ${allRuns.length} runs`);
104
+ return lines.join("\n");
105
+ }