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/memory.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { type StarterFile, starterFiles } from "./templates.js";
2
+
3
+ export function listStarterFiles(): StarterFile[] {
4
+ return starterFiles;
5
+ }
6
+
7
+ export function getStarterFile(path: string): StarterFile | undefined {
8
+ return starterFiles.find((file) => file.path === path);
9
+ }
10
+
11
+ export function buildStarterFileMap(): Record<string, string> {
12
+ return Object.fromEntries(
13
+ starterFiles.map((file) => [file.path, file.content]),
14
+ );
15
+ }
16
+
17
+ export function updateStateSummary(
18
+ content: string,
19
+ nextSummary: string,
20
+ ): string {
21
+ return content.replace(
22
+ /Status Summary:.*/u,
23
+ `Status Summary: ${nextSummary}`,
24
+ );
25
+ }
package/src/pi.ts ADDED
@@ -0,0 +1,240 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionCommandContext,
4
+ Theme,
5
+ } from "@mariozechner/pi-coding-agent";
6
+ import { Box, Text } from "@mariozechner/pi-tui";
7
+
8
+ export interface AppCommandContext {
9
+ cwd: string;
10
+ args?: string[];
11
+ runtime?: {
12
+ pi: ExtensionAPI;
13
+ ctx: ExtensionCommandContext;
14
+ };
15
+ }
16
+
17
+ export interface StructuredResult {
18
+ text: string;
19
+ messageType?: string;
20
+ details?: Record<string, unknown>;
21
+ }
22
+
23
+ export type CommandResult = string | StructuredResult;
24
+
25
+ export interface AppCommandDefinition {
26
+ name: string;
27
+ description: string;
28
+ execute: (context: AppCommandContext) => Promise<CommandResult>;
29
+ }
30
+
31
+ function splitArgs(rawArgs: string): string[] {
32
+ const trimmed = rawArgs.trim();
33
+ return trimmed.length > 0 ? trimmed.split(/\s+/u) : [];
34
+ }
35
+
36
+ async function emitResult(
37
+ result: string,
38
+ ctx: ExtensionCommandContext,
39
+ ): Promise<void> {
40
+ if (result.trim().length === 0) {
41
+ return;
42
+ }
43
+
44
+ if (ctx.hasUI) {
45
+ ctx.ui.notify(result, "info");
46
+ } else {
47
+ console.log(result);
48
+ }
49
+ }
50
+
51
+ interface VerificationDetails {
52
+ title?: string;
53
+ passed?: boolean;
54
+ checksRun?: string[];
55
+ failureSummary?: string[];
56
+ }
57
+
58
+ interface StatusDetails {
59
+ title?: string;
60
+ phase?: string;
61
+ activeTask?: string;
62
+ blockers?: string[];
63
+ nextStep?: string;
64
+ recoveryOptions?: string[];
65
+ }
66
+
67
+ interface EscalationDetails {
68
+ title?: string;
69
+ taskId?: string;
70
+ priorAttempts?: number;
71
+ failedChecks?: string[];
72
+ }
73
+
74
+ function renderVerificationMessage(
75
+ message: { content: unknown; details?: unknown },
76
+ expanded: boolean,
77
+ theme: Theme,
78
+ ): { box: InstanceType<typeof Box> } {
79
+ const body =
80
+ typeof message.content === "string"
81
+ ? message.content
82
+ : String(message.content ?? "");
83
+ const details = (message.details ?? {}) as VerificationDetails;
84
+ const icon = details.passed ? "✓" : "✗";
85
+ const color = details.passed ? "accent" : "error";
86
+ const header = theme.fg(
87
+ color,
88
+ theme.bold(`${icon} ${details.title ?? "Verification"}`),
89
+ );
90
+ const checksLine = details.checksRun?.length
91
+ ? `Checks: ${details.checksRun.join(", ")}`
92
+ : "";
93
+ const failureLine = details.failureSummary?.length
94
+ ? `Failures: ${details.failureSummary.join("; ")}`
95
+ : "";
96
+ const lines = [header, body, checksLine, failureLine].filter(Boolean);
97
+ const box = new Box(1, 1, (text: string) =>
98
+ theme.bg("customMessageBg", text),
99
+ );
100
+ box.addChild(
101
+ new Text(expanded ? lines.join("\n\n") : lines.join("\n"), 0, 0),
102
+ );
103
+ return { box };
104
+ }
105
+
106
+ function renderStatusMessage(
107
+ message: { content: unknown; details?: unknown },
108
+ expanded: boolean,
109
+ theme: Theme,
110
+ ): { box: InstanceType<typeof Box> } {
111
+ const body =
112
+ typeof message.content === "string"
113
+ ? message.content
114
+ : String(message.content ?? "");
115
+ const details = (message.details ?? {}) as StatusDetails;
116
+ const header = theme.fg(
117
+ "accent",
118
+ theme.bold(details.title ?? "Omni-Pi Status"),
119
+ );
120
+ const lines = [header];
121
+ if (details.phase) lines.push(`Phase: ${details.phase}`);
122
+ if (details.activeTask) lines.push(`Task: ${details.activeTask}`);
123
+ if (expanded) lines.push(body);
124
+ if (details.blockers?.length)
125
+ lines.push(`Blockers: ${details.blockers.join("; ")}`);
126
+ if (details.nextStep) lines.push(`Next: ${details.nextStep}`);
127
+ if (details.recoveryOptions?.length) {
128
+ lines.push("Recovery options:");
129
+ for (const option of details.recoveryOptions) lines.push(` - ${option}`);
130
+ }
131
+ const box = new Box(1, 1, (text: string) =>
132
+ theme.bg("customMessageBg", text),
133
+ );
134
+ box.addChild(new Text(lines.join("\n"), 0, 0));
135
+ return { box };
136
+ }
137
+
138
+ function renderEscalationMessage(
139
+ message: { content: unknown; details?: unknown },
140
+ expanded: boolean,
141
+ theme: Theme,
142
+ ): { box: InstanceType<typeof Box> } {
143
+ const body =
144
+ typeof message.content === "string"
145
+ ? message.content
146
+ : String(message.content ?? "");
147
+ const details = (message.details ?? {}) as EscalationDetails;
148
+ const header = theme.fg(
149
+ "warning",
150
+ theme.bold(`⚠ ${details.title ?? "Escalation"}`),
151
+ );
152
+ const lines = [header];
153
+ if (details.taskId) lines.push(`Task: ${details.taskId}`);
154
+ if (details.priorAttempts != null)
155
+ lines.push(`Prior attempts: ${details.priorAttempts}`);
156
+ if (details.failedChecks?.length)
157
+ lines.push(`Failed checks: ${details.failedChecks.join(", ")}`);
158
+ if (expanded) lines.push(body);
159
+ const box = new Box(1, 1, (text: string) =>
160
+ theme.bg("customMessageBg", text),
161
+ );
162
+ box.addChild(new Text(lines.join("\n"), 0, 0));
163
+ return { box };
164
+ }
165
+
166
+ export function registerOmniMessageRenderer(api: ExtensionAPI): void {
167
+ api.registerMessageRenderer("omni-update", (message, { expanded }, theme) => {
168
+ const body =
169
+ typeof message.content === "string"
170
+ ? message.content
171
+ : String(message.content ?? "");
172
+ const details = (message.details ?? {}) as { title?: string };
173
+ const lines = [
174
+ theme.fg("accent", theme.bold(details.title ?? "Omni-Pi")),
175
+ body,
176
+ ];
177
+ const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
178
+ box.addChild(
179
+ new Text(expanded ? lines.join("\n\n") : lines.join("\n"), 0, 0),
180
+ );
181
+ return box;
182
+ });
183
+
184
+ api.registerMessageRenderer(
185
+ "omni-verification",
186
+ (message, { expanded }, theme) => {
187
+ return renderVerificationMessage(message, expanded, theme).box;
188
+ },
189
+ );
190
+
191
+ api.registerMessageRenderer("omni-status", (message, { expanded }, theme) => {
192
+ return renderStatusMessage(message, expanded, theme).box;
193
+ });
194
+
195
+ api.registerMessageRenderer(
196
+ "omni-escalation",
197
+ (message, { expanded }, theme) => {
198
+ return renderEscalationMessage(message, expanded, theme).box;
199
+ },
200
+ );
201
+ }
202
+
203
+ function normalizeResult(raw: CommandResult): StructuredResult {
204
+ if (typeof raw === "string") {
205
+ return { text: raw };
206
+ }
207
+ return raw;
208
+ }
209
+
210
+ export function registerPiCommands(
211
+ api: ExtensionAPI,
212
+ commands: AppCommandDefinition[],
213
+ ): void {
214
+ for (const command of commands) {
215
+ api.registerCommand(command.name, {
216
+ description: command.description,
217
+ handler: async (args, ctx) => {
218
+ const raw = await command.execute({
219
+ cwd: ctx.cwd,
220
+ args: splitArgs(args),
221
+ runtime: {
222
+ pi: api,
223
+ ctx,
224
+ },
225
+ });
226
+ const result = normalizeResult(raw);
227
+ if (result.text.trim().length > 0 && ctx.hasUI) {
228
+ api.sendMessage({
229
+ customType: result.messageType ?? "omni-update",
230
+ content: result.text,
231
+ display: true,
232
+ details: { title: command.name, ...result.details },
233
+ });
234
+ } else {
235
+ await emitResult(result.text, ctx);
236
+ }
237
+ },
238
+ });
239
+ }
240
+ }
@@ -0,0 +1,303 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import type {
5
+ ConversationBrief,
6
+ ImplementationSpec,
7
+ PresetConfig,
8
+ TaskBrief,
9
+ } from "./contracts.js";
10
+ import { WORKFLOW_PRESETS } from "./contracts.js";
11
+ import type { RepoSignals } from "./repo.js";
12
+ import { escapeTaskTableCell } from "./tasks.js";
13
+
14
+ export interface PlanningContext {
15
+ existingDecisions: string[];
16
+ sessionNotes: string[];
17
+ priorScope: string[];
18
+ completedTaskIds: string[];
19
+ }
20
+
21
+ export async function gatherPlanningContext(
22
+ rootDir: string,
23
+ ): Promise<PlanningContext> {
24
+ const ctx: PlanningContext = {
25
+ existingDecisions: [],
26
+ sessionNotes: [],
27
+ priorScope: [],
28
+ completedTaskIds: [],
29
+ };
30
+
31
+ try {
32
+ const decisions = await readFile(
33
+ path.join(rootDir, ".omni", "DECISIONS.md"),
34
+ "utf8",
35
+ );
36
+ ctx.existingDecisions = decisions
37
+ .split("\n")
38
+ .filter((line) => line.trim().startsWith("- Decision:"))
39
+ .map((line) => line.replace(/^.*- Decision:\s*/u, "").trim())
40
+ .filter(Boolean);
41
+ } catch {
42
+ /* no decisions file yet */
43
+ }
44
+
45
+ try {
46
+ const session = await readFile(
47
+ path.join(rootDir, ".omni", "SESSION-SUMMARY.md"),
48
+ "utf8",
49
+ );
50
+ const progressMatch = session.match(
51
+ /## Recent progress\n\n([\s\S]*?)(?=\n## |$)/u,
52
+ );
53
+ if (progressMatch) {
54
+ ctx.sessionNotes = progressMatch[1]
55
+ .split("\n")
56
+ .map((line) => line.replace(/^- /u, "").trim())
57
+ .filter((line) => line.length > 0 && line !== "-");
58
+ }
59
+ } catch {
60
+ /* no session summary yet */
61
+ }
62
+
63
+ try {
64
+ const spec = await readFile(path.join(rootDir, ".omni", "SPEC.md"), "utf8");
65
+ const scopeMatch = spec.match(/## Scope\n\n([\s\S]*?)(?=\n## |$)/u);
66
+ if (scopeMatch) {
67
+ ctx.priorScope = scopeMatch[1]
68
+ .split("\n")
69
+ .map((line) => line.replace(/^- /u, "").trim())
70
+ .filter((line) => line.length > 0);
71
+ }
72
+ } catch {
73
+ /* no spec yet */
74
+ }
75
+
76
+ try {
77
+ const tasks = await readFile(
78
+ path.join(rootDir, ".omni", "TASKS.md"),
79
+ "utf8",
80
+ );
81
+ ctx.completedTaskIds = tasks
82
+ .split("\n")
83
+ .filter((line) => line.startsWith("| T") && line.includes("| done |"))
84
+ .map((line) => line.split("|")[1]?.trim())
85
+ .filter((id): id is string => Boolean(id));
86
+ } catch {
87
+ /* no tasks yet */
88
+ }
89
+
90
+ return ctx;
91
+ }
92
+
93
+ function buildBootstrapTasks(repoSignals: RepoSignals): TaskBrief[] {
94
+ const tasks: TaskBrief[] = [
95
+ {
96
+ id: "T01",
97
+ title: "Confirm the initial project direction",
98
+ objective:
99
+ "Refine the problem statement, constraints, and success criteria into a stable first-pass spec.",
100
+ contextFiles: [".omni/PROJECT.md", ".omni/IDEAS.md", ".omni/SPEC.md"],
101
+ skills: ["omni-planning"],
102
+ doneCriteria: [
103
+ "The problem statement is clear.",
104
+ "Initial constraints are captured.",
105
+ "Success criteria are explicit.",
106
+ ],
107
+ role: "worker",
108
+ status: "todo",
109
+ dependsOn: [],
110
+ },
111
+ {
112
+ id: "T02",
113
+ title: "Define the first implementation slice",
114
+ objective:
115
+ "Break the first meaningful delivery slice into bounded tasks with clear verification steps.",
116
+ contextFiles: [".omni/SPEC.md", ".omni/TASKS.md", ".omni/TESTS.md"],
117
+ skills: ["omni-planning"],
118
+ doneCriteria: [
119
+ "The first slice is broken into bounded tasks.",
120
+ "Each task has explicit done criteria.",
121
+ "Verification requirements are listed.",
122
+ ],
123
+ role: "worker",
124
+ status: "todo",
125
+ dependsOn: ["T01"],
126
+ },
127
+ ];
128
+
129
+ if (
130
+ repoSignals.tools.includes("playwright") ||
131
+ repoSignals.tools.includes("cypress")
132
+ ) {
133
+ tasks.push({
134
+ id: "T03",
135
+ title: "Align browser testing expectations",
136
+ objective:
137
+ "Document how browser-based checks should be used during future work.",
138
+ contextFiles: [".omni/TESTS.md", ".omni/SPEC.md"],
139
+ skills: ["agent-browser", "omni-verification"],
140
+ doneCriteria: [
141
+ "Browser testing expectations are documented.",
142
+ "The verification plan names the browser toolchain.",
143
+ ],
144
+ role: "worker",
145
+ status: "todo",
146
+ dependsOn: ["T02"],
147
+ });
148
+ }
149
+
150
+ return tasks;
151
+ }
152
+
153
+ export function createInitialSpec(
154
+ brief: ConversationBrief,
155
+ repoSignals: RepoSignals,
156
+ planningCtx?: PlanningContext,
157
+ ): ImplementationSpec {
158
+ const presetConfig: PresetConfig | undefined = brief.preset
159
+ ? WORKFLOW_PRESETS[brief.preset]
160
+ : undefined;
161
+ const scopeItems = [
162
+ brief.summary,
163
+ ...brief.constraints,
164
+ ...brief.userSignals,
165
+ ...(presetConfig
166
+ ? [`Workflow preset: ${presetConfig.name} — ${presetConfig.description}`]
167
+ : []),
168
+ ].filter(Boolean);
169
+
170
+ if (planningCtx?.priorScope.length) {
171
+ for (const item of planningCtx.priorScope) {
172
+ if (!scopeItems.includes(item)) {
173
+ scopeItems.push(item);
174
+ }
175
+ }
176
+ }
177
+
178
+ const architecture = [
179
+ "Use `.omni/` as the durable project memory layer.",
180
+ "Keep the user-facing brain simple and route deeper work through planner, worker, verifier, and expert roles.",
181
+ `Detected repo signals: languages=${repoSignals.languages.join(", ") || "unknown"}; frameworks=${repoSignals.frameworks.join(", ") || "unknown"}; tools=${repoSignals.tools.join(", ") || "unknown"}.`,
182
+ ];
183
+
184
+ if (presetConfig) {
185
+ architecture.push(`Worker hint: ${presetConfig.workerHint}`);
186
+ }
187
+
188
+ if (planningCtx?.existingDecisions.length) {
189
+ architecture.push(
190
+ `Prior decisions to honor: ${planningCtx.existingDecisions.join("; ")}`,
191
+ );
192
+ }
193
+
194
+ const acceptanceCriteria = [
195
+ "The project direction is captured in `.omni/PROJECT.md` and `.omni/SPEC.md`.",
196
+ "The next tasks are small, verifiable, and ready for guided execution.",
197
+ "The verification plan names the checks needed for the first slice.",
198
+ ];
199
+
200
+ if (planningCtx?.sessionNotes.length) {
201
+ acceptanceCriteria.push(
202
+ `Build on recent progress: ${planningCtx.sessionNotes.slice(0, 3).join("; ")}`,
203
+ );
204
+ }
205
+
206
+ let tasks = buildBootstrapTasks(repoSignals);
207
+ if (presetConfig && tasks.length > presetConfig.maxTasks) {
208
+ tasks = tasks.slice(0, presetConfig.maxTasks);
209
+ }
210
+ if (planningCtx?.completedTaskIds.length) {
211
+ for (const task of tasks) {
212
+ if (planningCtx.completedTaskIds.includes(task.id)) {
213
+ task.status = "done";
214
+ }
215
+ }
216
+ }
217
+
218
+ return {
219
+ title: brief.desiredOutcome || "Initial Omni-Pi plan",
220
+ scope: scopeItems,
221
+ architecture,
222
+ taskSlices: tasks,
223
+ acceptanceCriteria,
224
+ };
225
+ }
226
+
227
+ export function renderSpecMarkdown(spec: ImplementationSpec): string {
228
+ return `# Spec
229
+
230
+ ## Title
231
+
232
+ ${spec.title}
233
+
234
+ ## Scope
235
+
236
+ ${spec.scope.map((item) => `- ${item}`).join("\n")}
237
+
238
+ ## Architecture
239
+
240
+ ${spec.architecture.map((item) => `- ${item}`).join("\n")}
241
+
242
+ ## Acceptance Criteria
243
+
244
+ ${spec.acceptanceCriteria.map((item) => `- ${item}`).join("\n")}
245
+
246
+ ## Risks
247
+
248
+ - To be identified during planning.
249
+
250
+ ## Open Questions
251
+
252
+ - To be captured during the understand phase.
253
+ `;
254
+ }
255
+
256
+ export function renderTasksMarkdown(tasks: TaskBrief[]): string {
257
+ const rows = tasks.map((task) => {
258
+ const dependsOn =
259
+ task.dependsOn.length > 0 ? task.dependsOn.join(", ") : "-";
260
+ const doneCriteria = task.doneCriteria.join("; ");
261
+ return `| ${escapeTaskTableCell(task.id)} | ${escapeTaskTableCell(task.title)} | ${escapeTaskTableCell(task.role)} | ${escapeTaskTableCell(dependsOn)} | ${escapeTaskTableCell(task.status)} | ${escapeTaskTableCell(doneCriteria)} |`;
262
+ });
263
+
264
+ return `# Tasks
265
+
266
+ ## Task slices
267
+
268
+ | ID | Title | Role | Depends On | Status | Done Criteria |
269
+ | --- | --- | --- | --- | --- | --- |
270
+ ${rows.join("\n")}
271
+ `;
272
+ }
273
+
274
+ export function renderTestsMarkdown(repoSignals: RepoSignals): string {
275
+ const projectChecks = ["npm test"];
276
+
277
+ if (repoSignals.tools.includes("vitest")) {
278
+ projectChecks.push("npm run test");
279
+ }
280
+
281
+ if (repoSignals.tools.includes("playwright")) {
282
+ projectChecks.push("npx playwright test");
283
+ }
284
+
285
+ return `# Tests
286
+
287
+ ## Project-wide checks
288
+
289
+ ${projectChecks.map((check) => `- ${check}`).join("\n")}
290
+
291
+ ## Task-specific checks
292
+
293
+ - Add task-level checks as each slice is planned.
294
+
295
+ ## Retry policy
296
+
297
+ - Worker retries before expert takeover: 2
298
+
299
+ ## Escalation threshold
300
+
301
+ - Escalate after repeated failures or when the planner marks the task as high-risk.
302
+ `;
303
+ }