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/plans.ts ADDED
@@ -0,0 +1,247 @@
1
+ import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import type { PlanEntry, PlanStatus } from "./contracts.js";
5
+ import { OMNI_DIR } from "./contracts.js";
6
+
7
+ const PLANS_DIR = "plans";
8
+ const INDEX_FILE = "INDEX.md";
9
+ const PROGRESS_FILE = "PROGRESS.md";
10
+
11
+ function plansDir(rootDir: string): string {
12
+ return path.join(rootDir, OMNI_DIR, PLANS_DIR);
13
+ }
14
+
15
+ function indexPath(rootDir: string): string {
16
+ return path.join(rootDir, OMNI_DIR, PLANS_DIR, INDEX_FILE);
17
+ }
18
+
19
+ function progressPath(rootDir: string): string {
20
+ return path.join(rootDir, OMNI_DIR, PROGRESS_FILE);
21
+ }
22
+
23
+ function planFilePath(rootDir: string, planId: string): string {
24
+ return path.join(plansDir(rootDir), `${planId}.md`);
25
+ }
26
+
27
+ function generatePlanId(): string {
28
+ const now = new Date();
29
+ const date = now.toISOString().slice(0, 10).replace(/-/gu, "");
30
+ const seq = now.toISOString().slice(11, 19).replace(/:/gu, "");
31
+ return `plan-${date}-${seq}`;
32
+ }
33
+
34
+ function renderPlanFile(
35
+ entry: PlanEntry,
36
+ description: string,
37
+ tasks: string[],
38
+ ): string {
39
+ const taskLines =
40
+ tasks.length > 0
41
+ ? tasks.map((t) => `- [ ] ${t}`).join("\n")
42
+ : "- [ ] Define tasks";
43
+ return `# ${entry.title}
44
+
45
+ Status: ${entry.status}
46
+ Created: ${entry.createdAt}
47
+
48
+ ## Description
49
+
50
+ ${description}
51
+
52
+ ## Tasks
53
+
54
+ ${taskLines}
55
+
56
+ ## Notes
57
+
58
+ -
59
+ `;
60
+ }
61
+
62
+ function parseIndexEntries(content: string): PlanEntry[] {
63
+ const entries: PlanEntry[] = [];
64
+ for (const line of content.split("\n")) {
65
+ const match = line.match(
66
+ /^\|\s*\[([^\]]+)\]\([^)]+\)\s*\|\s*([^|]+)\|\s*(\w+)\s*\|\s*([^|]+)\|\s*([^|]*)\|/u,
67
+ );
68
+ if (match) {
69
+ entries.push({
70
+ id: match[1].trim(),
71
+ title: match[2].trim(),
72
+ status: match[3].trim() as PlanStatus,
73
+ createdAt: match[4].trim(),
74
+ completedAt: match[5]?.trim() || undefined,
75
+ });
76
+ }
77
+ }
78
+ return entries;
79
+ }
80
+
81
+ function renderIndex(entries: PlanEntry[]): string {
82
+ const rows = entries.map((e) => {
83
+ const completed = e.completedAt ?? "";
84
+ return `| [${e.id}](${e.id}.md) | ${e.title} | ${e.status} | ${e.createdAt} | ${completed} |`;
85
+ });
86
+ return `# Plan Index
87
+
88
+ | ID | Title | Status | Created | Completed |
89
+ | --- | --- | --- | --- | --- |
90
+ ${rows.join("\n")}
91
+ `;
92
+ }
93
+
94
+ export async function ensurePlansDir(rootDir: string): Promise<void> {
95
+ await mkdir(plansDir(rootDir), { recursive: true });
96
+ }
97
+
98
+ export async function readPlanIndex(rootDir: string): Promise<PlanEntry[]> {
99
+ try {
100
+ const content = await readFile(indexPath(rootDir), "utf8");
101
+ return parseIndexEntries(content);
102
+ } catch {
103
+ return [];
104
+ }
105
+ }
106
+
107
+ async function writeIndex(
108
+ rootDir: string,
109
+ entries: PlanEntry[],
110
+ ): Promise<void> {
111
+ await ensurePlansDir(rootDir);
112
+ await writeFile(indexPath(rootDir), renderIndex(entries), "utf8");
113
+ }
114
+
115
+ export async function createPlan(
116
+ rootDir: string,
117
+ title: string,
118
+ description: string,
119
+ tasks: string[],
120
+ ): Promise<PlanEntry> {
121
+ const id = generatePlanId();
122
+ const createdAt = new Date().toISOString().slice(0, 10);
123
+ const entry: PlanEntry = { id, title, status: "active", createdAt };
124
+
125
+ await ensurePlansDir(rootDir);
126
+ await writeFile(
127
+ planFilePath(rootDir, id),
128
+ renderPlanFile(entry, description, tasks),
129
+ "utf8",
130
+ );
131
+
132
+ const entries = await readPlanIndex(rootDir);
133
+ entries.push(entry);
134
+ await writeIndex(rootDir, entries);
135
+
136
+ return entry;
137
+ }
138
+
139
+ export async function updatePlanStatus(
140
+ rootDir: string,
141
+ planId: string,
142
+ status: PlanStatus,
143
+ ): Promise<PlanEntry | null> {
144
+ const entries = await readPlanIndex(rootDir);
145
+ const entry = entries.find((e) => e.id === planId);
146
+ if (!entry) return null;
147
+
148
+ entry.status = status;
149
+ if (status === "completed" || status === "discarded") {
150
+ entry.completedAt = new Date().toISOString().slice(0, 10);
151
+ }
152
+
153
+ await writeIndex(rootDir, entries);
154
+
155
+ // Update status in the plan file itself
156
+ try {
157
+ const filePath = planFilePath(rootDir, planId);
158
+ const content = await readFile(filePath, "utf8");
159
+ const updated = content.replace(/^Status:\s*.+$/mu, `Status: ${status}`);
160
+ await writeFile(filePath, updated, "utf8");
161
+ } catch {
162
+ // file may have been cleaned up already
163
+ }
164
+
165
+ return entry;
166
+ }
167
+
168
+ export async function cleanupCompletedPlans(
169
+ rootDir: string,
170
+ ): Promise<string[]> {
171
+ const entries = await readPlanIndex(rootDir);
172
+ const toRemove = entries.filter(
173
+ (e) => e.status === "completed" || e.status === "discarded",
174
+ );
175
+ const removed: string[] = [];
176
+
177
+ for (const entry of toRemove) {
178
+ try {
179
+ await unlink(planFilePath(rootDir, entry.id));
180
+ removed.push(entry.id);
181
+ } catch {
182
+ // already gone
183
+ }
184
+ }
185
+
186
+ // Keep entries in index but mark them — files are gone
187
+ await writeIndex(rootDir, entries);
188
+ return removed;
189
+ }
190
+
191
+ export async function appendProgress(
192
+ rootDir: string,
193
+ message: string,
194
+ ): Promise<void> {
195
+ const filePath = progressPath(rootDir);
196
+ const timestamp = new Date().toISOString().slice(0, 16).replace("T", " ");
197
+ const bullet = `- [${timestamp}] ${message}`;
198
+
199
+ try {
200
+ const content = await readFile(filePath, "utf8");
201
+ await writeFile(filePath, `${content.trimEnd()}\n${bullet}\n`, "utf8");
202
+ } catch {
203
+ await mkdir(path.dirname(filePath), { recursive: true });
204
+ await writeFile(
205
+ filePath,
206
+ `# Progress\n\nOngoing log of project progress.\n\n${bullet}\n`,
207
+ "utf8",
208
+ );
209
+ }
210
+ }
211
+
212
+ export async function readProgress(rootDir: string): Promise<string> {
213
+ try {
214
+ return await readFile(progressPath(rootDir), "utf8");
215
+ } catch {
216
+ return "No progress recorded yet.";
217
+ }
218
+ }
219
+
220
+ export function renderPlanIndex(entries: PlanEntry[]): string {
221
+ if (entries.length === 0) return "No plans created yet.";
222
+
223
+ const active = entries.filter((e) => e.status === "active");
224
+ const completed = entries.filter((e) => e.status === "completed");
225
+ const discarded = entries.filter((e) => e.status === "discarded");
226
+
227
+ const lines: string[] = [];
228
+ if (active.length > 0) {
229
+ lines.push("Active:");
230
+ for (const e of active) {
231
+ lines.push(` ${e.id}: ${e.title} (since ${e.createdAt})`);
232
+ }
233
+ }
234
+ if (completed.length > 0) {
235
+ lines.push("Completed:");
236
+ for (const e of completed) {
237
+ lines.push(` ${e.id}: ${e.title} (done ${e.completedAt})`);
238
+ }
239
+ }
240
+ if (discarded.length > 0) {
241
+ lines.push("Discarded:");
242
+ for (const e of discarded) {
243
+ lines.push(` ${e.id}: ${e.title}`);
244
+ }
245
+ }
246
+ return lines.join("\n");
247
+ }
package/src/repo.ts ADDED
@@ -0,0 +1,210 @@
1
+ import { access, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export interface RepoSignals {
5
+ languages: string[];
6
+ frameworks: string[];
7
+ tools: string[];
8
+ files: string[];
9
+ }
10
+
11
+ async function exists(filePath: string): Promise<boolean> {
12
+ try {
13
+ await access(filePath);
14
+ return true;
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+
20
+ async function readPackageJson(
21
+ rootDir: string,
22
+ ): Promise<Record<string, unknown> | null> {
23
+ const packageJsonPath = path.join(rootDir, "package.json");
24
+ if (!(await exists(packageJsonPath))) {
25
+ return null;
26
+ }
27
+
28
+ try {
29
+ return JSON.parse(await readFile(packageJsonPath, "utf8")) as Record<
30
+ string,
31
+ unknown
32
+ >;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ function gatherPackageNames(pkg: Record<string, unknown> | null): string[] {
39
+ if (!pkg) {
40
+ return [];
41
+ }
42
+
43
+ const sections = ["dependencies", "devDependencies", "peerDependencies"];
44
+ const names = new Set<string>();
45
+
46
+ for (const section of sections) {
47
+ const value = pkg[section];
48
+ if (value && typeof value === "object") {
49
+ for (const key of Object.keys(value as Record<string, unknown>)) {
50
+ names.add(key);
51
+ }
52
+ }
53
+ }
54
+
55
+ return [...names];
56
+ }
57
+
58
+ export async function detectRepoSignals(rootDir: string): Promise<RepoSignals> {
59
+ const filesToCheck = [
60
+ "package.json",
61
+ "Cargo.toml",
62
+ "go.mod",
63
+ "requirements.txt",
64
+ "pyproject.toml",
65
+ "setup.py",
66
+ "Gemfile",
67
+ "composer.json",
68
+ "mix.exs",
69
+ "build.gradle",
70
+ "build.gradle.kts",
71
+ "pom.xml",
72
+ "Makefile",
73
+ "CMakeLists.txt",
74
+ "Package.swift",
75
+ "playwright.config.ts",
76
+ "playwright.config.js",
77
+ "cypress.config.ts",
78
+ "cypress.config.js",
79
+ "vite.config.ts",
80
+ "next.config.js",
81
+ "next.config.mjs",
82
+ "tsconfig.json",
83
+ "pytest.ini",
84
+ "setup.cfg",
85
+ "tox.ini",
86
+ ".rspec",
87
+ ];
88
+
89
+ const presentFiles = (
90
+ await Promise.all(
91
+ filesToCheck.map(async (file) =>
92
+ (await exists(path.join(rootDir, file))) ? file : null,
93
+ ),
94
+ )
95
+ ).filter((value): value is string => value !== null);
96
+
97
+ const packageJson = await readPackageJson(rootDir);
98
+ const packageNames = gatherPackageNames(packageJson);
99
+
100
+ const languages = new Set<string>();
101
+ const frameworks = new Set<string>();
102
+ const tools = new Set<string>();
103
+
104
+ if (presentFiles.includes("Cargo.toml")) {
105
+ languages.add("rust");
106
+ }
107
+
108
+ if (
109
+ presentFiles.includes("tsconfig.json") ||
110
+ packageNames.some((name) => name.includes("typescript"))
111
+ ) {
112
+ languages.add("typescript");
113
+ }
114
+
115
+ if (presentFiles.includes("go.mod")) {
116
+ languages.add("go");
117
+ }
118
+
119
+ if (
120
+ presentFiles.includes("requirements.txt") ||
121
+ presentFiles.includes("pyproject.toml") ||
122
+ presentFiles.includes("setup.py")
123
+ ) {
124
+ languages.add("python");
125
+ }
126
+
127
+ if (presentFiles.includes("Gemfile")) {
128
+ languages.add("ruby");
129
+ }
130
+
131
+ if (presentFiles.includes("composer.json")) {
132
+ languages.add("php");
133
+ }
134
+
135
+ if (presentFiles.includes("mix.exs")) {
136
+ languages.add("elixir");
137
+ }
138
+
139
+ if (
140
+ presentFiles.includes("build.gradle") ||
141
+ presentFiles.includes("build.gradle.kts") ||
142
+ presentFiles.includes("pom.xml")
143
+ ) {
144
+ languages.add("java");
145
+ }
146
+
147
+ if (presentFiles.includes("Package.swift")) {
148
+ languages.add("swift");
149
+ }
150
+
151
+ if (
152
+ presentFiles.includes("CMakeLists.txt") &&
153
+ !presentFiles.includes("Cargo.toml")
154
+ ) {
155
+ languages.add("cpp");
156
+ }
157
+
158
+ if (presentFiles.includes("pytest.ini") || presentFiles.includes("tox.ini")) {
159
+ tools.add("pytest");
160
+ }
161
+
162
+ if (presentFiles.includes(".rspec")) {
163
+ tools.add("rspec");
164
+ }
165
+
166
+ if (presentFiles.includes("Makefile")) {
167
+ tools.add("make");
168
+ }
169
+
170
+ if (packageNames.includes("react") || packageNames.includes("next")) {
171
+ frameworks.add("react");
172
+ }
173
+
174
+ if (
175
+ packageNames.includes("next") ||
176
+ presentFiles.some((file) => file.startsWith("next.config"))
177
+ ) {
178
+ frameworks.add("nextjs");
179
+ }
180
+
181
+ if (
182
+ packageNames.includes("vite") ||
183
+ presentFiles.includes("vite.config.ts")
184
+ ) {
185
+ tools.add("vite");
186
+ }
187
+
188
+ if (presentFiles.some((file) => file.startsWith("playwright.config"))) {
189
+ tools.add("playwright");
190
+ }
191
+
192
+ if (presentFiles.some((file) => file.startsWith("cypress.config"))) {
193
+ tools.add("cypress");
194
+ }
195
+
196
+ if (packageNames.includes("vitest")) {
197
+ tools.add("vitest");
198
+ }
199
+
200
+ if (packageNames.includes("jest")) {
201
+ tools.add("jest");
202
+ }
203
+
204
+ return {
205
+ languages: [...languages].sort(),
206
+ frameworks: [...frameworks].sort(),
207
+ tools: [...tools].sort(),
208
+ files: presentFiles.sort(),
209
+ };
210
+ }