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/work.ts ADDED
@@ -0,0 +1,345 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import {
5
+ gatherTaskContext,
6
+ renderContextBlocks,
7
+ renderContextSummary,
8
+ } from "./context.js";
9
+ import type {
10
+ EscalationBrief,
11
+ TaskAttemptResult,
12
+ TaskBrief,
13
+ } from "./contracts.js";
14
+ import {
15
+ findNextExecutableTask,
16
+ readTasks,
17
+ updateTaskStatus,
18
+ writeTasks,
19
+ } from "./tasks.js";
20
+
21
+ export interface WorkEngine {
22
+ runWorkerTask: (
23
+ task: TaskBrief,
24
+ attempt: number,
25
+ ) => Promise<TaskAttemptResult>;
26
+ runExpertTask: (
27
+ task: TaskBrief,
28
+ escalation: EscalationBrief,
29
+ ) => Promise<TaskAttemptResult>;
30
+ }
31
+
32
+ export interface WorkResult {
33
+ kind: "completed" | "expert_completed" | "blocked" | "idle";
34
+ taskId: string | null;
35
+ message: string;
36
+ recoveryOptions?: string[];
37
+ }
38
+
39
+ export interface WorkDispatchResult {
40
+ kind: "ready" | "idle";
41
+ taskId: string | null;
42
+ prompt: string;
43
+ briefPath?: string;
44
+ message: string;
45
+ }
46
+
47
+ const DEFAULT_RETRY_LIMIT = 2;
48
+
49
+ async function readRetryLimit(testsPath: string): Promise<number> {
50
+ try {
51
+ const content = await readFile(testsPath, "utf8");
52
+ const match = content.match(
53
+ /Worker retries before expert takeover:\s*(\d+)/u,
54
+ );
55
+ return match ? Number.parseInt(match[1], 10) : DEFAULT_RETRY_LIMIT;
56
+ } catch {
57
+ return DEFAULT_RETRY_LIMIT;
58
+ }
59
+ }
60
+
61
+ async function ensureTaskDir(rootDir: string): Promise<string> {
62
+ const taskDir = path.join(rootDir, ".omni", "tasks");
63
+ await mkdir(taskDir, { recursive: true });
64
+ return taskDir;
65
+ }
66
+
67
+ function historyPath(taskDir: string, taskId: string): string {
68
+ return path.join(taskDir, `${taskId}.history.json`);
69
+ }
70
+
71
+ async function readTaskHistory(
72
+ taskDir: string,
73
+ taskId: string,
74
+ ): Promise<TaskAttemptResult[]> {
75
+ try {
76
+ return JSON.parse(
77
+ await readFile(historyPath(taskDir, taskId), "utf8"),
78
+ ) as TaskAttemptResult[];
79
+ } catch {
80
+ return [];
81
+ }
82
+ }
83
+
84
+ async function writeTaskHistory(
85
+ taskDir: string,
86
+ taskId: string,
87
+ history: TaskAttemptResult[],
88
+ ): Promise<void> {
89
+ await writeFile(
90
+ historyPath(taskDir, taskId),
91
+ JSON.stringify(history, null, 2),
92
+ "utf8",
93
+ );
94
+ }
95
+
96
+ async function writeTaskBrief(taskDir: string, task: TaskBrief): Promise<void> {
97
+ const content = `# ${task.id}: ${task.title}
98
+
99
+ ## Objective
100
+
101
+ ${task.objective}
102
+
103
+ ## Done Criteria
104
+
105
+ ${task.doneCriteria.map((item) => `- ${item}`).join("\n") || "- None yet"}
106
+
107
+ ## Skills
108
+
109
+ ${task.skills.map((item) => `- ${item}`).join("\n") || "- None"}
110
+
111
+ ## Context Files
112
+
113
+ ${task.contextFiles.map((item) => `- ${item}`).join("\n") || "- None"}
114
+ `;
115
+ await writeFile(path.join(taskDir, `${task.id}-BRIEF.md`), content, "utf8");
116
+ }
117
+
118
+ export async function prepareNextTaskDispatch(
119
+ rootDir: string,
120
+ ): Promise<WorkDispatchResult> {
121
+ const tasksPath = path.join(rootDir, ".omni", "TASKS.md");
122
+ const tasks = await readTasks(tasksPath);
123
+ const nextTask = findNextExecutableTask(tasks);
124
+
125
+ if (!nextTask) {
126
+ return {
127
+ kind: "idle",
128
+ taskId: null,
129
+ prompt: "",
130
+ message:
131
+ "No executable tasks are available. Refresh the plan or complete dependencies first.",
132
+ };
133
+ }
134
+
135
+ const taskDir = await ensureTaskDir(rootDir);
136
+ await writeTaskBrief(taskDir, nextTask);
137
+ await writeTasks(
138
+ tasksPath,
139
+ updateTaskStatus(tasks, nextTask.id, "in_progress"),
140
+ );
141
+
142
+ const briefPath = path.join(taskDir, `${nextTask.id}-BRIEF.md`);
143
+ const preReadContext = await gatherTaskContext(rootDir, nextTask, 4000);
144
+ const prompt = [
145
+ "You are working inside an Omni-Pi guided task session.",
146
+ "",
147
+ `Task: ${nextTask.id} - ${nextTask.title}`,
148
+ `Objective: ${nextTask.objective}`,
149
+ "",
150
+ "Read these files first:",
151
+ "- .omni/PROJECT.md",
152
+ "- .omni/SPEC.md",
153
+ "- .omni/TESTS.md",
154
+ `- ${path.relative(rootDir, briefPath)}`,
155
+ ...nextTask.contextFiles.map((file) => `- ${file}`),
156
+ "",
157
+ "Then implement the task, explain the change briefly, and run the planned verification steps before finishing.",
158
+ nextTask.skills.length > 0
159
+ ? `Relevant skills: ${nextTask.skills.join(", ")}`
160
+ : "Relevant skills: none explicitly listed",
161
+ ...(preReadContext.length > 0
162
+ ? [
163
+ "",
164
+ renderContextSummary(preReadContext),
165
+ "",
166
+ "Pre-loaded context (already read for you):",
167
+ renderContextBlocks(preReadContext),
168
+ ]
169
+ : []),
170
+ ].join("\n");
171
+
172
+ return {
173
+ kind: "ready",
174
+ taskId: nextTask.id,
175
+ prompt,
176
+ briefPath,
177
+ message: `Prepared ${nextTask.id} for a focused worker session.`,
178
+ };
179
+ }
180
+
181
+ async function writeEscalationBrief(
182
+ taskDir: string,
183
+ escalation: EscalationBrief,
184
+ ): Promise<void> {
185
+ const verificationResultsSection = escalation.verificationResults
186
+ ? escalation.verificationResults
187
+ .map((r) => `- ${r.command}: ${r.passed ? "passed" : "failed"}`)
188
+ .join("\n")
189
+ : "- None recorded";
190
+ const modifiedFilesSection =
191
+ escalation.modifiedFiles?.map((f) => `- ${f}`).join("\n") ||
192
+ "- None recorded";
193
+
194
+ const content = `# Escalation for ${escalation.taskId}
195
+
196
+ ## Prior Attempts
197
+
198
+ ${escalation.priorAttempts}
199
+
200
+ ## Failure Logs
201
+
202
+ ${escalation.failureLogs.map((item) => `- ${item}`).join("\n") || "- None"}
203
+
204
+ ## Verification Results
205
+
206
+ ${verificationResultsSection}
207
+
208
+ ## Modified Files
209
+
210
+ ${modifiedFilesSection}
211
+
212
+ ## Expert Objective
213
+
214
+ ${escalation.expertObjective}
215
+ `;
216
+ await writeFile(
217
+ path.join(taskDir, `${escalation.taskId}-ESCALATION.md`),
218
+ content,
219
+ "utf8",
220
+ );
221
+ }
222
+
223
+ function createEscalationBrief(
224
+ task: TaskBrief,
225
+ history: TaskAttemptResult[],
226
+ ): EscalationBrief {
227
+ const failureLogs = history
228
+ .filter((attempt) => !attempt.verification.passed)
229
+ .map((attempt) => attempt.verification.failureSummary.join("; "))
230
+ .filter((log) => log.length > 0);
231
+
232
+ const verificationResults = history.flatMap((attempt) =>
233
+ attempt.verification.checksRun.map((command) => ({
234
+ command,
235
+ passed: attempt.verification.passed,
236
+ stdout: "",
237
+ stderr: attempt.verification.failureSummary.join("\n"),
238
+ })),
239
+ );
240
+
241
+ const modifiedFiles = [
242
+ ...new Set(history.flatMap((attempt) => attempt.modifiedFiles ?? [])),
243
+ ];
244
+
245
+ return {
246
+ taskId: task.id,
247
+ priorAttempts: history.length,
248
+ failureLogs,
249
+ expertObjective: `Resolve the root cause preventing ${task.id} from passing verification and complete the task.`,
250
+ verificationResults,
251
+ modifiedFiles,
252
+ };
253
+ }
254
+
255
+ function formatVerificationSummary(result: TaskAttemptResult): string {
256
+ const checks =
257
+ result.verification.checksRun.length > 0
258
+ ? result.verification.checksRun.join(", ")
259
+ : "no recorded checks";
260
+ if (result.verification.passed) {
261
+ return `Verification passed: ${checks}.`;
262
+ }
263
+ const failures =
264
+ result.verification.failureSummary.length > 0
265
+ ? result.verification.failureSummary.join("; ")
266
+ : "unknown verification failure";
267
+ return `Verification failed: ${checks}. Reason: ${failures}.`;
268
+ }
269
+
270
+ export async function executeNextTask(
271
+ rootDir: string,
272
+ engine: WorkEngine,
273
+ ): Promise<WorkResult> {
274
+ const tasksPath = path.join(rootDir, ".omni", "TASKS.md");
275
+ const testsPath = path.join(rootDir, ".omni", "TESTS.md");
276
+ const tasks = await readTasks(tasksPath);
277
+ const nextTask = findNextExecutableTask(tasks);
278
+
279
+ if (!nextTask) {
280
+ return {
281
+ kind: "idle",
282
+ taskId: null,
283
+ message:
284
+ "No executable tasks are available. Complete dependencies or refresh the plan first.",
285
+ };
286
+ }
287
+
288
+ const taskDir = await ensureTaskDir(rootDir);
289
+ await writeTaskBrief(taskDir, nextTask);
290
+
291
+ const history = await readTaskHistory(taskDir, nextTask.id);
292
+ const retryLimit = await readRetryLimit(testsPath);
293
+ const attempt = history.length + 1;
294
+ const workerResult = await engine.runWorkerTask(nextTask, attempt);
295
+ const workerHistory = [...history, workerResult];
296
+ await writeTaskHistory(taskDir, nextTask.id, workerHistory);
297
+
298
+ if (workerResult.verification.passed) {
299
+ await writeTasks(tasksPath, updateTaskStatus(tasks, nextTask.id, "done"));
300
+ return {
301
+ kind: "completed",
302
+ taskId: nextTask.id,
303
+ message: `Completed ${nextTask.id} with the worker path. ${formatVerificationSummary(workerResult)}`,
304
+ };
305
+ }
306
+
307
+ if (attempt < retryLimit) {
308
+ await writeTasks(tasksPath, updateTaskStatus(tasks, nextTask.id, "todo"));
309
+ return {
310
+ kind: "blocked",
311
+ taskId: nextTask.id,
312
+ message: `Worker attempt ${attempt} for ${nextTask.id} failed verification and is queued for retry. ${formatVerificationSummary(workerResult)}`,
313
+ };
314
+ }
315
+
316
+ const escalation = createEscalationBrief(nextTask, workerHistory);
317
+ await writeEscalationBrief(taskDir, escalation);
318
+ const expertResult = await engine.runExpertTask(nextTask, escalation);
319
+ await writeTaskHistory(taskDir, nextTask.id, [
320
+ ...workerHistory,
321
+ expertResult,
322
+ ]);
323
+
324
+ if (expertResult.verification.passed) {
325
+ await writeTasks(tasksPath, updateTaskStatus(tasks, nextTask.id, "done"));
326
+ return {
327
+ kind: "expert_completed",
328
+ taskId: nextTask.id,
329
+ message: `Completed ${nextTask.id} after expert escalation. ${formatVerificationSummary(expertResult)}`,
330
+ };
331
+ }
332
+
333
+ await writeTasks(tasksPath, updateTaskStatus(tasks, nextTask.id, "blocked"));
334
+ return {
335
+ kind: "blocked",
336
+ taskId: nextTask.id,
337
+ message: `Task ${nextTask.id} remains blocked after worker retries and expert escalation. ${formatVerificationSummary(expertResult)}`,
338
+ recoveryOptions: [
339
+ "Review the escalation notes in `.omni/tasks/` and refine the task inputs.",
340
+ "Run /omni-plan to restructure the task into smaller slices.",
341
+ "Run /omni-sync to capture learnings before attempting a different approach.",
342
+ "Manually inspect and fix the failing checks listed in `.omni/TESTS.md`.",
343
+ ],
344
+ };
345
+ }
@@ -0,0 +1,375 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { readConfig } from "./config.js";
4
+ import type {
5
+ ConversationBrief,
6
+ OmniState,
7
+ SkillCandidate,
8
+ } from "./contracts.js";
9
+ import { type DoctorReport, runDoctor } from "./doctor.js";
10
+ import { buildStarterFileMap, listStarterFiles } from "./memory.js";
11
+ import {
12
+ createInitialSpec,
13
+ gatherPlanningContext,
14
+ renderSpecMarkdown,
15
+ renderTasksMarkdown,
16
+ renderTestsMarkdown,
17
+ } from "./planning.js";
18
+ import { appendProgress, cleanupCompletedPlans, createPlan } from "./plans.js";
19
+ import { detectRepoSignals } from "./repo.js";
20
+ import {
21
+ appendSkillUsageNote,
22
+ buildSkillInstallPlan,
23
+ defaultSkillSignals,
24
+ renderSkillDecision,
25
+ toSkillCandidate,
26
+ } from "./skills.js";
27
+ import { type SyncRequest, syncOmniMemory } from "./sync.js";
28
+ import { executeNextTask, type WorkEngine, type WorkResult } from "./work.js";
29
+
30
+ export interface InitResult {
31
+ created: string[];
32
+ reused: string[];
33
+ repoSignals: Awaited<ReturnType<typeof detectRepoSignals>>;
34
+ skillCandidates: SkillCandidate[];
35
+ installedSkills: SkillCandidate[];
36
+ installCommands: string[];
37
+ installSteps: Array<{
38
+ command: string;
39
+ args: string[];
40
+ summary: string;
41
+ }>;
42
+ diagnostics: DoctorReport;
43
+ }
44
+
45
+ export interface PlanResult {
46
+ specPath: string;
47
+ tasksPath: string;
48
+ testsPath: string;
49
+ }
50
+
51
+ export interface WorkExecutionResult extends WorkResult {
52
+ state: OmniState;
53
+ }
54
+
55
+ export interface SyncResult {
56
+ state: OmniState;
57
+ }
58
+
59
+ const starterFileMap = buildStarterFileMap();
60
+
61
+ async function writeIfMissing(
62
+ filePath: string,
63
+ content: string,
64
+ ): Promise<boolean> {
65
+ try {
66
+ await readFile(filePath, "utf8");
67
+ return false;
68
+ } catch {
69
+ await mkdir(path.dirname(filePath), { recursive: true });
70
+ await writeFile(filePath, content, "utf8");
71
+ return true;
72
+ }
73
+ }
74
+
75
+ async function replaceSection(
76
+ filePath: string,
77
+ heading: string,
78
+ lines: string[],
79
+ ): Promise<void> {
80
+ const current = await readFile(filePath, "utf8");
81
+ const escapedHeading = heading.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
82
+ const sectionRegex = new RegExp(
83
+ `(${escapedHeading}\\n\\n)([\\s\\S]*?)(?=\\n## |$)`,
84
+ "u",
85
+ );
86
+ const replacement = `$1${lines.join("\n")}\n`;
87
+ const next = current.match(sectionRegex)
88
+ ? current.replace(sectionRegex, replacement)
89
+ : `${current.trimEnd()}\n\n${heading}\n\n${lines.join("\n")}\n`;
90
+ await writeFile(filePath, next, "utf8");
91
+ }
92
+
93
+ async function writeState(rootDir: string, state: OmniState): Promise<void> {
94
+ const statePath = path.join(rootDir, ".omni", "STATE.md");
95
+ const recoverySection =
96
+ state.recoveryOptions && state.recoveryOptions.length > 0
97
+ ? `\nRecovery Options:\n${state.recoveryOptions.map((option) => `- ${option}`).join("\n")}\n`
98
+ : "";
99
+ const content = `# State
100
+
101
+ Current Phase: ${state.currentPhase[0].toUpperCase()}${state.currentPhase.slice(1)}
102
+ Active Task: ${state.activeTask}
103
+ Status Summary: ${state.statusSummary}
104
+ Blockers: ${state.blockers.length > 0 ? state.blockers.join("; ") : "None"}
105
+ Next Step: ${state.nextStep}
106
+ ${recoverySection}`;
107
+ await writeFile(statePath, content, "utf8");
108
+ }
109
+
110
+ function buildSkillCandidates(
111
+ repoSignals: Awaited<ReturnType<typeof detectRepoSignals>>,
112
+ ): SkillCandidate[] {
113
+ const candidates = defaultSkillSignals.map(toSkillCandidate);
114
+
115
+ if (
116
+ repoSignals.tools.includes("playwright") ||
117
+ repoSignals.tools.includes("cypress")
118
+ ) {
119
+ candidates.push({
120
+ name: "browser-test-helpers",
121
+ reason:
122
+ "The repository already has browser testing signals, so browser-oriented workflow helpers are useful.",
123
+ confidence: "medium",
124
+ policy: "recommend-only",
125
+ });
126
+ }
127
+
128
+ return candidates;
129
+ }
130
+
131
+ export async function initializeOmniProject(
132
+ rootDir: string,
133
+ ): Promise<InitResult> {
134
+ const created: string[] = [];
135
+ const reused: string[] = [];
136
+
137
+ for (const file of listStarterFiles()) {
138
+ const absolutePath = path.join(rootDir, file.path);
139
+ if (await writeIfMissing(absolutePath, file.content)) {
140
+ created.push(file.path);
141
+ } else {
142
+ reused.push(file.path);
143
+ }
144
+ }
145
+
146
+ const repoSignals = await detectRepoSignals(rootDir);
147
+ const skillCandidates = buildSkillCandidates(repoSignals);
148
+ const {
149
+ installed: installedSkills,
150
+ commands: installCommands,
151
+ steps: installSteps,
152
+ } = buildSkillInstallPlan(skillCandidates);
153
+
154
+ const skillsPath = path.join(rootDir, ".omni", "SKILLS.md");
155
+ await replaceSection(
156
+ skillsPath,
157
+ "## Installed",
158
+ installedSkills.length > 0
159
+ ? installedSkills.map(renderSkillDecision)
160
+ : ["- None yet"],
161
+ );
162
+ await replaceSection(
163
+ skillsPath,
164
+ "## Recommended",
165
+ skillCandidates
166
+ .filter((candidate) => candidate.policy !== "auto-install")
167
+ .map(renderSkillDecision)
168
+ .concat(
169
+ skillCandidates.every(
170
+ (candidate) => candidate.policy === "auto-install",
171
+ )
172
+ ? ["- None yet"]
173
+ : [],
174
+ ),
175
+ );
176
+
177
+ const projectPath = path.join(rootDir, ".omni", "PROJECT.md");
178
+ const project = await readFile(projectPath, "utf8");
179
+ const signalSummary = [
180
+ `- Detected languages: ${repoSignals.languages.join(", ") || "unknown"}`,
181
+ `- Detected frameworks: ${repoSignals.frameworks.join(", ") || "unknown"}`,
182
+ `- Detected tools: ${repoSignals.tools.join(", ") || "unknown"}`,
183
+ ].join("\n");
184
+ if (!project.includes("## Repo Signals")) {
185
+ await writeFile(
186
+ projectPath,
187
+ `${project.trimEnd()}\n\n## Repo Signals\n\n${signalSummary}\n`,
188
+ "utf8",
189
+ );
190
+ }
191
+
192
+ if (installCommands.length > 0) {
193
+ await appendSkillUsageNote(
194
+ rootDir,
195
+ `Planned install commands: ${installCommands.join(" ; ")}`,
196
+ );
197
+ }
198
+
199
+ const diagnostics = await runDoctor(rootDir);
200
+
201
+ await writeState(rootDir, {
202
+ currentPhase: "understand",
203
+ activeTask: "Initialize Omni-Pi",
204
+ statusSummary:
205
+ "Omni-Pi has created its project memory files and scanned the repository for useful signals.",
206
+ blockers: [],
207
+ nextStep:
208
+ diagnostics.overall === "red"
209
+ ? "Run /omni-doctor to review issues before proceeding."
210
+ : "Run /omni-plan to turn the current project context into a spec and first task slices.",
211
+ });
212
+
213
+ return {
214
+ created,
215
+ reused,
216
+ repoSignals,
217
+ skillCandidates,
218
+ installedSkills,
219
+ installCommands,
220
+ installSteps,
221
+ diagnostics,
222
+ };
223
+ }
224
+
225
+ export async function planOmniProject(
226
+ rootDir: string,
227
+ brief: ConversationBrief,
228
+ ): Promise<PlanResult> {
229
+ const specPath = path.join(rootDir, ".omni", "SPEC.md");
230
+ const tasksPath = path.join(rootDir, ".omni", "TASKS.md");
231
+ const testsPath = path.join(rootDir, ".omni", "TESTS.md");
232
+
233
+ for (const required of [specPath, tasksPath, testsPath]) {
234
+ const relative = path.relative(rootDir, required);
235
+ if (!starterFileMap[relative]) {
236
+ continue;
237
+ }
238
+ await writeIfMissing(required, starterFileMap[relative]);
239
+ }
240
+
241
+ const repoSignals = await detectRepoSignals(rootDir);
242
+ const planningCtx = await gatherPlanningContext(rootDir);
243
+ const spec = createInitialSpec(brief, repoSignals, planningCtx);
244
+ await writeFile(specPath, renderSpecMarkdown(spec), "utf8");
245
+ await writeFile(tasksPath, renderTasksMarkdown(spec.taskSlices), "utf8");
246
+ await writeFile(testsPath, renderTestsMarkdown(repoSignals), "utf8");
247
+
248
+ const planEntry = await createPlan(
249
+ rootDir,
250
+ spec.title,
251
+ brief.summary,
252
+ spec.taskSlices.map((t) => `${t.id}: ${t.title}`),
253
+ );
254
+ await appendProgress(rootDir, `Created plan ${planEntry.id}: ${spec.title}`);
255
+
256
+ await writeState(rootDir, {
257
+ currentPhase: "plan",
258
+ activeTask: "Prepare the first implementation slice",
259
+ statusSummary:
260
+ "Omni-Pi refreshed the spec, task slices, and verification plan.",
261
+ blockers: [],
262
+ nextStep:
263
+ "Review the proposed tasks, then run /omni-work when you are ready to execute the next slice.",
264
+ });
265
+
266
+ return { specPath, tasksPath, testsPath };
267
+ }
268
+
269
+ export async function readOmniStatus(rootDir: string): Promise<OmniState> {
270
+ const statePath = path.join(rootDir, ".omni", "STATE.md");
271
+ const content = await readFile(statePath, "utf8");
272
+
273
+ const matchValue = (label: string): string => {
274
+ const regex = new RegExp(`^${label}:\\s*(.*)$`, "mu");
275
+ return content.match(regex)?.[1]?.trim() ?? "";
276
+ };
277
+
278
+ const blockersValue = matchValue("Blockers");
279
+ const recoveryMatch = content.match(/Recovery Options:\n((?:- .*\n?)*)/u);
280
+ const recoveryOptions = recoveryMatch
281
+ ? recoveryMatch[1]
282
+ .split("\n")
283
+ .map((line) => line.replace(/^- /u, "").trim())
284
+ .filter(Boolean)
285
+ : undefined;
286
+ return {
287
+ currentPhase: matchValue(
288
+ "Current Phase",
289
+ ).toLowerCase() as OmniState["currentPhase"],
290
+ activeTask: matchValue("Active Task"),
291
+ statusSummary: matchValue("Status Summary"),
292
+ blockers:
293
+ blockersValue && blockersValue !== "None"
294
+ ? blockersValue.split(/;\s*/u)
295
+ : [],
296
+ nextStep: matchValue("Next Step"),
297
+ recoveryOptions,
298
+ };
299
+ }
300
+
301
+ export async function workOnOmniProject(
302
+ rootDir: string,
303
+ engine: WorkEngine,
304
+ ): Promise<WorkExecutionResult> {
305
+ const result = await executeNextTask(rootDir, engine);
306
+
307
+ let state: OmniState;
308
+ if (result.kind === "completed" || result.kind === "expert_completed") {
309
+ state = {
310
+ currentPhase: result.kind === "expert_completed" ? "escalate" : "build",
311
+ activeTask: result.taskId ?? "None",
312
+ statusSummary: result.message,
313
+ blockers: [],
314
+ nextStep:
315
+ "Run /omni-status to review progress or /omni-work to continue with the next task.",
316
+ };
317
+ } else if (result.kind === "blocked") {
318
+ state = {
319
+ currentPhase: result.message.includes("expert escalation")
320
+ ? "escalate"
321
+ : "check",
322
+ activeTask: result.taskId ?? "None",
323
+ statusSummary: result.message,
324
+ blockers: result.taskId
325
+ ? [`Verification failures on ${result.taskId}`]
326
+ : ["A task is blocked."],
327
+ nextStep: result.message.includes("queued for retry")
328
+ ? "Run /omni-work again to retry the task or inspect `.omni/tasks/` for the failure history."
329
+ : "Review the escalation notes in `.omni/tasks/` and refine the plan or task inputs.",
330
+ recoveryOptions: result.recoveryOptions,
331
+ };
332
+ } else {
333
+ state = {
334
+ currentPhase: "plan",
335
+ activeTask: "None",
336
+ statusSummary: result.message,
337
+ blockers: [],
338
+ nextStep:
339
+ "Run /omni-plan to refresh the task list if more work is needed.",
340
+ };
341
+ }
342
+
343
+ await writeState(rootDir, state);
344
+ if (result.kind === "completed" || result.kind === "expert_completed") {
345
+ await appendProgress(
346
+ rootDir,
347
+ `Completed ${result.taskId ?? "task"}: ${result.message}`,
348
+ );
349
+ }
350
+ return { ...result, state };
351
+ }
352
+
353
+ export async function syncOmniProject(
354
+ rootDir: string,
355
+ request: SyncRequest,
356
+ ): Promise<SyncResult> {
357
+ await syncOmniMemory(rootDir, request);
358
+ await appendProgress(rootDir, request.summary);
359
+
360
+ const config = await readConfig(rootDir);
361
+ if (config.cleanupCompletedPlans) {
362
+ await cleanupCompletedPlans(rootDir);
363
+ }
364
+
365
+ const state: OmniState = {
366
+ currentPhase: "understand",
367
+ activeTask: "Sync project memory",
368
+ statusSummary: "Omni-Pi synced recent progress into durable memory files.",
369
+ blockers: [],
370
+ nextStep:
371
+ "Run /omni-status to inspect the latest state or /omni-plan to refine the next slice.",
372
+ };
373
+ await writeState(rootDir, state);
374
+ return { state };
375
+ }