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/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
|
+
}
|