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/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
|
+
}
|
package/src/planning.ts
ADDED
|
@@ -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
|
+
}
|