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/subagents.ts
ADDED
|
@@ -0,0 +1,1031 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
|
|
6
|
+
import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
7
|
+
import { readConfig } from "./config.js";
|
|
8
|
+
import {
|
|
9
|
+
type ContextBlock,
|
|
10
|
+
gatherTaskContext,
|
|
11
|
+
renderContextBlocks,
|
|
12
|
+
} from "./context.js";
|
|
13
|
+
import type {
|
|
14
|
+
EscalationBrief,
|
|
15
|
+
OmniConfig,
|
|
16
|
+
TaskAttemptResult,
|
|
17
|
+
TaskBrief,
|
|
18
|
+
} from "./contracts.js";
|
|
19
|
+
import { detectRepoSignals } from "./repo.js";
|
|
20
|
+
import { loadSkillTriggers, matchSkillsForTask } from "./skills.js";
|
|
21
|
+
import type { WorkEngine } from "./work.js";
|
|
22
|
+
|
|
23
|
+
interface SubagentConfig {
|
|
24
|
+
name: string;
|
|
25
|
+
systemPrompt: string;
|
|
26
|
+
model?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface SubagentSingleResult {
|
|
30
|
+
agent: string;
|
|
31
|
+
exitCode: number;
|
|
32
|
+
messages: unknown[];
|
|
33
|
+
error?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface SubagentDeps {
|
|
37
|
+
discoverAgents: (
|
|
38
|
+
cwd: string,
|
|
39
|
+
scope: "user" | "project" | "both",
|
|
40
|
+
) => { agents: SubagentConfig[] };
|
|
41
|
+
runSync: (
|
|
42
|
+
runtimeCwd: string,
|
|
43
|
+
agents: SubagentConfig[],
|
|
44
|
+
agentName: string,
|
|
45
|
+
task: string,
|
|
46
|
+
options: {
|
|
47
|
+
cwd?: string;
|
|
48
|
+
runId: string;
|
|
49
|
+
sessionDir?: string;
|
|
50
|
+
maxOutput?: { bytes?: number; lines?: number };
|
|
51
|
+
onUpdate?: (result: {
|
|
52
|
+
details?: {
|
|
53
|
+
progress?: Array<{
|
|
54
|
+
agent: string;
|
|
55
|
+
currentTool?: string;
|
|
56
|
+
toolCount?: number;
|
|
57
|
+
}>;
|
|
58
|
+
};
|
|
59
|
+
}) => void;
|
|
60
|
+
},
|
|
61
|
+
) => Promise<SubagentSingleResult>;
|
|
62
|
+
getFinalOutput: (messages: unknown[]) => string;
|
|
63
|
+
recordRun?: (
|
|
64
|
+
agent: string,
|
|
65
|
+
task: string,
|
|
66
|
+
exitCode: number,
|
|
67
|
+
durationMs: number,
|
|
68
|
+
) => void;
|
|
69
|
+
loadRunsForAgent?: (agent: string) => RunHistoryEntry[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface RunHistoryEntry {
|
|
73
|
+
agent: string;
|
|
74
|
+
task: string;
|
|
75
|
+
ts: number;
|
|
76
|
+
status: "ok" | "error";
|
|
77
|
+
duration: number;
|
|
78
|
+
exit?: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface VerificationExec {
|
|
82
|
+
command: string;
|
|
83
|
+
args: string[];
|
|
84
|
+
cwd?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface VerificationPlan {
|
|
88
|
+
commands: VerificationExec[];
|
|
89
|
+
expectations: string[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface ParsedVerificationLine {
|
|
93
|
+
value: string;
|
|
94
|
+
command: VerificationExec | null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface VerificationCommandResult {
|
|
98
|
+
command: string;
|
|
99
|
+
passed: boolean;
|
|
100
|
+
stdout: string;
|
|
101
|
+
stderr: string;
|
|
102
|
+
code: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface VerificationExecutor {
|
|
106
|
+
exec: (
|
|
107
|
+
command: string,
|
|
108
|
+
args: string[],
|
|
109
|
+
options?: { cwd?: string },
|
|
110
|
+
) => Promise<{
|
|
111
|
+
stdout: string;
|
|
112
|
+
stderr: string;
|
|
113
|
+
code: number;
|
|
114
|
+
killed: boolean;
|
|
115
|
+
}>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface SubagentRunRecord {
|
|
119
|
+
agent: string;
|
|
120
|
+
taskId: string;
|
|
121
|
+
attemptLabel: string;
|
|
122
|
+
exitCode: number;
|
|
123
|
+
rawOutputPath: string;
|
|
124
|
+
passed: boolean;
|
|
125
|
+
checksRun: string[];
|
|
126
|
+
failureSummary: string[];
|
|
127
|
+
verificationCommands?: VerificationCommandResult[];
|
|
128
|
+
modifiedFiles?: string[];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function extractModifiedFiles(raw: string): string[] {
|
|
132
|
+
const files: string[] = [];
|
|
133
|
+
const patterns = [
|
|
134
|
+
/(?:created|modified|edited|wrote|updated|changed)\s+(?:file\s+)?[`"']?([^\s`"',]+\.\w+)[`"']?/giu,
|
|
135
|
+
/(?:Write|Edit|Create)\s+(?:to\s+)?[`"']?([^\s`"',]+\.\w+)[`"']?/gu,
|
|
136
|
+
];
|
|
137
|
+
for (const pattern of patterns) {
|
|
138
|
+
for (const match of raw.matchAll(pattern)) {
|
|
139
|
+
const file = match[1].replace(/^\/+/u, "");
|
|
140
|
+
if (
|
|
141
|
+
file.length > 0 &&
|
|
142
|
+
!file.startsWith("node_modules") &&
|
|
143
|
+
!files.includes(file)
|
|
144
|
+
) {
|
|
145
|
+
files.push(file);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return files;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function omniPackageDir(): string {
|
|
153
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function loadSubagentDeps(
|
|
157
|
+
packageDir = omniPackageDir(),
|
|
158
|
+
): Promise<SubagentDeps> {
|
|
159
|
+
const agentsModule = await import(
|
|
160
|
+
pathToFileURL(
|
|
161
|
+
path.join(packageDir, "node_modules", "pi-subagents", "agents.ts"),
|
|
162
|
+
).href
|
|
163
|
+
);
|
|
164
|
+
const executionModule = await import(
|
|
165
|
+
pathToFileURL(
|
|
166
|
+
path.join(packageDir, "node_modules", "pi-subagents", "execution.ts"),
|
|
167
|
+
).href
|
|
168
|
+
);
|
|
169
|
+
const utilsModule = await import(
|
|
170
|
+
pathToFileURL(
|
|
171
|
+
path.join(packageDir, "node_modules", "pi-subagents", "utils.ts"),
|
|
172
|
+
).href
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
let recordRun: SubagentDeps["recordRun"];
|
|
176
|
+
let loadRunsForAgent: SubagentDeps["loadRunsForAgent"];
|
|
177
|
+
try {
|
|
178
|
+
const historyModule = await import(
|
|
179
|
+
pathToFileURL(
|
|
180
|
+
path.join(packageDir, "node_modules", "pi-subagents", "run-history.ts"),
|
|
181
|
+
).href
|
|
182
|
+
);
|
|
183
|
+
recordRun = historyModule.recordRun;
|
|
184
|
+
loadRunsForAgent = historyModule.loadRunsForAgent;
|
|
185
|
+
} catch {
|
|
186
|
+
// run-history not available in this version of pi-subagents
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
discoverAgents: agentsModule.discoverAgents,
|
|
191
|
+
runSync: executionModule.runSync,
|
|
192
|
+
getFinalOutput: utilsModule.getFinalOutput,
|
|
193
|
+
recordRun,
|
|
194
|
+
loadRunsForAgent,
|
|
195
|
+
} as SubagentDeps;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function loadRunHistory(
|
|
199
|
+
packageDir = omniPackageDir(),
|
|
200
|
+
): Promise<{ loadRunsForAgent: (agent: string) => RunHistoryEntry[] } | null> {
|
|
201
|
+
try {
|
|
202
|
+
const historyModule = await import(
|
|
203
|
+
pathToFileURL(
|
|
204
|
+
path.join(packageDir, "node_modules", "pi-subagents", "run-history.ts"),
|
|
205
|
+
).href
|
|
206
|
+
);
|
|
207
|
+
return { loadRunsForAgent: historyModule.loadRunsForAgent };
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function buildWorkerPrompt(
|
|
214
|
+
task: TaskBrief,
|
|
215
|
+
verificationPlan: VerificationPlan,
|
|
216
|
+
skillContext?: string,
|
|
217
|
+
preReadContext?: ContextBlock[],
|
|
218
|
+
): string {
|
|
219
|
+
const verificationChecks =
|
|
220
|
+
verificationPlan.expectations.length > 0
|
|
221
|
+
? verificationPlan.expectations
|
|
222
|
+
: task.doneCriteria.length > 0
|
|
223
|
+
? task.doneCriteria
|
|
224
|
+
: ["Use the checks listed in .omni/TESTS.md"];
|
|
225
|
+
const lines = [
|
|
226
|
+
"You are Omni-Pi's worker executor.",
|
|
227
|
+
"Complete the task using the repository and relevant project files.",
|
|
228
|
+
"Run the necessary verification steps yourself when possible.",
|
|
229
|
+
"Return your final answer as JSON only with this shape:",
|
|
230
|
+
'{"summary":"...","verification":{"passed":true,"checksRun":["..."],"failureSummary":[],"retryRecommended":false}}',
|
|
231
|
+
"",
|
|
232
|
+
`Task ID: ${task.id}`,
|
|
233
|
+
`Title: ${task.title}`,
|
|
234
|
+
`Objective: ${task.objective}`,
|
|
235
|
+
`Done criteria: ${task.doneCriteria.join("; ") || "None provided"}`,
|
|
236
|
+
`Verification expectations: ${verificationChecks.join("; ")}`,
|
|
237
|
+
`Runtime verification commands: ${verificationPlan.commands.map((item) => [item.command, ...item.args].join(" ")).join("; ") || "none listed"}`,
|
|
238
|
+
`Relevant skills: ${task.skills.join(", ") || "none"}`,
|
|
239
|
+
"Required files to read before working:",
|
|
240
|
+
"- .omni/PROJECT.md",
|
|
241
|
+
"- .omni/SPEC.md",
|
|
242
|
+
"- .omni/TESTS.md",
|
|
243
|
+
`- .omni/tasks/${task.id}-BRIEF.md`,
|
|
244
|
+
...task.contextFiles.map((file) => `- ${file}`),
|
|
245
|
+
];
|
|
246
|
+
if (skillContext) {
|
|
247
|
+
lines.push("", "Matched skill guidance:", skillContext);
|
|
248
|
+
}
|
|
249
|
+
if (preReadContext && preReadContext.length > 0) {
|
|
250
|
+
lines.push(
|
|
251
|
+
"",
|
|
252
|
+
"Pre-loaded context (already read for you):",
|
|
253
|
+
renderContextBlocks(preReadContext),
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
return lines.join("\n");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function buildExpertPrompt(
|
|
260
|
+
task: TaskBrief,
|
|
261
|
+
escalation: EscalationBrief,
|
|
262
|
+
verificationPlan: VerificationPlan,
|
|
263
|
+
skillContext?: string,
|
|
264
|
+
preReadContext?: ContextBlock[],
|
|
265
|
+
): string {
|
|
266
|
+
const verificationChecks =
|
|
267
|
+
verificationPlan.expectations.length > 0
|
|
268
|
+
? verificationPlan.expectations
|
|
269
|
+
: task.doneCriteria.length > 0
|
|
270
|
+
? task.doneCriteria
|
|
271
|
+
: ["Use the checks listed in .omni/TESTS.md"];
|
|
272
|
+
const failedCommands =
|
|
273
|
+
escalation.verificationResults
|
|
274
|
+
?.filter((r) => !r.passed)
|
|
275
|
+
.map((r) => r.command)
|
|
276
|
+
.join(", ") || "none";
|
|
277
|
+
const modifiedFilesList =
|
|
278
|
+
escalation.modifiedFiles?.map((f) => `- ${f}`).join("\n") ||
|
|
279
|
+
"none recorded";
|
|
280
|
+
const lines = [
|
|
281
|
+
"You are Omni-Pi's expert executor taking over after repeated failures.",
|
|
282
|
+
"Fix the task directly and do not repeat the previous failed path blindly.",
|
|
283
|
+
"Run the necessary verification steps yourself when possible.",
|
|
284
|
+
"Return your final answer as JSON only with this shape:",
|
|
285
|
+
'{"summary":"...","verification":{"passed":true,"checksRun":["..."],"failureSummary":[],"retryRecommended":false}}',
|
|
286
|
+
"",
|
|
287
|
+
`Task ID: ${task.id}`,
|
|
288
|
+
`Title: ${task.title}`,
|
|
289
|
+
`Objective: ${task.objective}`,
|
|
290
|
+
`Verification expectations: ${verificationChecks.join("; ")}`,
|
|
291
|
+
`Runtime verification commands: ${verificationPlan.commands.map((item) => [item.command, ...item.args].join(" ")).join("; ") || "none listed"}`,
|
|
292
|
+
`Prior attempts: ${escalation.priorAttempts}`,
|
|
293
|
+
`Failed verification commands: ${failedCommands}`,
|
|
294
|
+
`Modified files in previous attempts:`,
|
|
295
|
+
modifiedFilesList,
|
|
296
|
+
`Failure logs: ${escalation.failureLogs.join(" | ") || "none recorded"}`,
|
|
297
|
+
`Expert objective: ${escalation.expertObjective}`,
|
|
298
|
+
"Required files to read before working:",
|
|
299
|
+
"- .omni/PROJECT.md",
|
|
300
|
+
"- .omni/SPEC.md",
|
|
301
|
+
"- .omni/TESTS.md",
|
|
302
|
+
`- .omni/tasks/${task.id}-BRIEF.md`,
|
|
303
|
+
`- .omni/tasks/${task.id}-ESCALATION.md`,
|
|
304
|
+
...task.contextFiles.map((file) => `- ${file}`),
|
|
305
|
+
];
|
|
306
|
+
if (skillContext) {
|
|
307
|
+
lines.push("", "Matched skill guidance:", skillContext);
|
|
308
|
+
}
|
|
309
|
+
if (preReadContext && preReadContext.length > 0) {
|
|
310
|
+
lines.push(
|
|
311
|
+
"",
|
|
312
|
+
"Pre-loaded context (already read for you):",
|
|
313
|
+
renderContextBlocks(preReadContext),
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
return lines.join("\n");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function parseAttemptResult(
|
|
320
|
+
raw: string,
|
|
321
|
+
fallbackSummary: string,
|
|
322
|
+
): TaskAttemptResult {
|
|
323
|
+
const trimmed = raw.trim();
|
|
324
|
+
const fencedMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/u);
|
|
325
|
+
const candidate = fencedMatch?.[1]?.trim() ?? trimmed;
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
const parsed = JSON.parse(candidate) as TaskAttemptResult;
|
|
329
|
+
if (parsed?.verification && typeof parsed.summary === "string") {
|
|
330
|
+
return parsed;
|
|
331
|
+
}
|
|
332
|
+
} catch {
|
|
333
|
+
// Fall through to normalized failure.
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
summary: fallbackSummary,
|
|
338
|
+
verification: {
|
|
339
|
+
taskId: "unknown",
|
|
340
|
+
passed: false,
|
|
341
|
+
checksRun: ["subagent-output-parse"],
|
|
342
|
+
failureSummary: ["Subagent did not return the expected JSON result."],
|
|
343
|
+
retryRecommended: true,
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function persistRawOutput(
|
|
349
|
+
rootDir: string,
|
|
350
|
+
taskId: string,
|
|
351
|
+
suffix: string,
|
|
352
|
+
content: string,
|
|
353
|
+
): Promise<void> {
|
|
354
|
+
const target = path.join(rootDir, ".omni", "tasks", `${taskId}-${suffix}.md`);
|
|
355
|
+
await writeFile(target, content, "utf8");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function persistRunRecord(
|
|
359
|
+
rootDir: string,
|
|
360
|
+
record: SubagentRunRecord,
|
|
361
|
+
): Promise<void> {
|
|
362
|
+
const target = path.join(
|
|
363
|
+
rootDir,
|
|
364
|
+
".omni",
|
|
365
|
+
"tasks",
|
|
366
|
+
`${record.taskId}-${record.attemptLabel}.json`,
|
|
367
|
+
);
|
|
368
|
+
await writeFile(target, JSON.stringify(record, null, 2), "utf8");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function parseCommandLine(line: string): VerificationExec | null {
|
|
372
|
+
const tokens = line.match(/(?:"[^"]*"|'[^']*'|\S+)/gu) ?? [];
|
|
373
|
+
const cleaned = tokens.map((token) => token.replace(/^['"]|['"]$/gu, ""));
|
|
374
|
+
if (cleaned.length === 0) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
command: cleaned[0],
|
|
379
|
+
args: cleaned.slice(1),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const ALLOWED_COMMANDS = new Set([
|
|
384
|
+
"npm",
|
|
385
|
+
"pnpm",
|
|
386
|
+
"yarn",
|
|
387
|
+
"bun",
|
|
388
|
+
"npx",
|
|
389
|
+
"make",
|
|
390
|
+
"cargo",
|
|
391
|
+
"go",
|
|
392
|
+
"python",
|
|
393
|
+
"python3",
|
|
394
|
+
"pytest",
|
|
395
|
+
"php",
|
|
396
|
+
"composer",
|
|
397
|
+
"bundle",
|
|
398
|
+
"rake",
|
|
399
|
+
"rspec",
|
|
400
|
+
"dotnet",
|
|
401
|
+
"swift",
|
|
402
|
+
"mix",
|
|
403
|
+
"gradle",
|
|
404
|
+
"mvn",
|
|
405
|
+
"cmake",
|
|
406
|
+
"elixir",
|
|
407
|
+
]);
|
|
408
|
+
|
|
409
|
+
function isRunnableCommand(
|
|
410
|
+
command: VerificationExec | null,
|
|
411
|
+
): command is VerificationExec {
|
|
412
|
+
return Boolean(
|
|
413
|
+
command &&
|
|
414
|
+
(ALLOWED_COMMANDS.has(command.command) ||
|
|
415
|
+
command.command.startsWith("./") ||
|
|
416
|
+
command.command.includes("/")),
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function collectSignificantTerms(task: TaskBrief): Set<string> {
|
|
421
|
+
const stopWords = new Set([
|
|
422
|
+
"the",
|
|
423
|
+
"and",
|
|
424
|
+
"for",
|
|
425
|
+
"with",
|
|
426
|
+
"task",
|
|
427
|
+
"verify",
|
|
428
|
+
"check",
|
|
429
|
+
"run",
|
|
430
|
+
"from",
|
|
431
|
+
"that",
|
|
432
|
+
"this",
|
|
433
|
+
"into",
|
|
434
|
+
"make",
|
|
435
|
+
"sure",
|
|
436
|
+
"using",
|
|
437
|
+
"use",
|
|
438
|
+
"your",
|
|
439
|
+
"goal",
|
|
440
|
+
]);
|
|
441
|
+
const values = [
|
|
442
|
+
task.id,
|
|
443
|
+
task.title,
|
|
444
|
+
task.objective,
|
|
445
|
+
...task.contextFiles,
|
|
446
|
+
...task.doneCriteria,
|
|
447
|
+
]
|
|
448
|
+
.join(" ")
|
|
449
|
+
.toLowerCase()
|
|
450
|
+
.split(/[^a-z0-9_.\-/]+/u)
|
|
451
|
+
.map((value) => value.trim())
|
|
452
|
+
.filter((value) => value.length >= 3 && !stopWords.has(value));
|
|
453
|
+
|
|
454
|
+
const expanded = new Set<string>();
|
|
455
|
+
for (const value of values) {
|
|
456
|
+
expanded.add(value);
|
|
457
|
+
for (const fragment of value.split(/[_.\-/]+/u)) {
|
|
458
|
+
if (fragment.length >= 3 && !stopWords.has(fragment)) {
|
|
459
|
+
expanded.add(fragment);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return expanded;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function matchesTask(line: string, task: TaskBrief): boolean {
|
|
467
|
+
const lower = line.toLowerCase();
|
|
468
|
+
if (lower.includes(task.id.toLowerCase())) {
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
const terms = collectSignificantTerms(task);
|
|
472
|
+
return [...terms].some((term) => lower.includes(term));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function splitVerificationSection(
|
|
476
|
+
content: string,
|
|
477
|
+
heading: string,
|
|
478
|
+
): ParsedVerificationLine[] {
|
|
479
|
+
const match = content.match(
|
|
480
|
+
new RegExp(`${heading}\\n\\n([\\s\\S]*?)(?=\\n## |$)`, "u"),
|
|
481
|
+
);
|
|
482
|
+
const lines = (match?.[1] ?? "")
|
|
483
|
+
.split("\n")
|
|
484
|
+
.map((line) => line.trim())
|
|
485
|
+
.filter((line) => line.startsWith("- "))
|
|
486
|
+
.map((line) => line.slice(2).trim())
|
|
487
|
+
.filter((line) => line.length > 0 && line !== "-");
|
|
488
|
+
|
|
489
|
+
return lines.map((value) => ({
|
|
490
|
+
value,
|
|
491
|
+
command: parseCommandLine(value),
|
|
492
|
+
}));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function inferTestCommandsFromContext(
|
|
496
|
+
task: TaskBrief,
|
|
497
|
+
languages?: string[],
|
|
498
|
+
): VerificationExec[] {
|
|
499
|
+
const inferred: VerificationExec[] = [];
|
|
500
|
+
const langs = new Set(languages ?? []);
|
|
501
|
+
|
|
502
|
+
// TypeScript / JavaScript
|
|
503
|
+
for (const file of task.contextFiles) {
|
|
504
|
+
if (/\.test\.[tj]sx?$/u.test(file) || /\.spec\.[tj]sx?$/u.test(file)) {
|
|
505
|
+
inferred.push({ command: "npx", args: ["vitest", "run", file] });
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (/\.[tj]sx?$/u.test(file)) {
|
|
509
|
+
const testFile = file
|
|
510
|
+
.replace(/\.([tj]sx?)$/u, ".test.$1")
|
|
511
|
+
.replace(/^src\//u, "tests/");
|
|
512
|
+
if (!inferred.some((cmd) => cmd.args.includes(testFile))) {
|
|
513
|
+
inferred.push({ command: "npx", args: ["vitest", "run", testFile] });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Python
|
|
519
|
+
if (langs.has("python")) {
|
|
520
|
+
for (const file of task.contextFiles) {
|
|
521
|
+
if (/test_.*\.py$/u.test(file) || /_test\.py$/u.test(file)) {
|
|
522
|
+
inferred.push({ command: "pytest", args: [file] });
|
|
523
|
+
} else if (/\.py$/u.test(file)) {
|
|
524
|
+
const testFile = file.replace(
|
|
525
|
+
/(\w+)\.py$/u,
|
|
526
|
+
(_m, name) => `test_${name}.py`,
|
|
527
|
+
);
|
|
528
|
+
if (!inferred.some((cmd) => cmd.args.includes(testFile))) {
|
|
529
|
+
inferred.push({ command: "pytest", args: [testFile] });
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Rust
|
|
536
|
+
if (langs.has("rust")) {
|
|
537
|
+
for (const file of task.contextFiles) {
|
|
538
|
+
if (/\.rs$/u.test(file)) {
|
|
539
|
+
if (!inferred.some((cmd) => cmd.command === "cargo")) {
|
|
540
|
+
inferred.push({ command: "cargo", args: ["test"] });
|
|
541
|
+
}
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Go
|
|
548
|
+
if (langs.has("go")) {
|
|
549
|
+
for (const file of task.contextFiles) {
|
|
550
|
+
if (/_test\.go$/u.test(file)) {
|
|
551
|
+
inferred.push({
|
|
552
|
+
command: "go",
|
|
553
|
+
args: ["test", `./${path.dirname(file)}/...`],
|
|
554
|
+
});
|
|
555
|
+
} else if (/\.go$/u.test(file)) {
|
|
556
|
+
inferred.push({
|
|
557
|
+
command: "go",
|
|
558
|
+
args: ["test", `./${path.dirname(file)}/...`],
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Ruby
|
|
565
|
+
if (langs.has("ruby")) {
|
|
566
|
+
for (const file of task.contextFiles) {
|
|
567
|
+
if (/_spec\.rb$/u.test(file)) {
|
|
568
|
+
inferred.push({ command: "bundle", args: ["exec", "rspec", file] });
|
|
569
|
+
} else if (/\.rb$/u.test(file)) {
|
|
570
|
+
const specFile = file
|
|
571
|
+
.replace(/\.rb$/u, "_spec.rb")
|
|
572
|
+
.replace(/^lib\//u, "spec/");
|
|
573
|
+
if (!inferred.some((cmd) => cmd.args.includes(specFile))) {
|
|
574
|
+
inferred.push({
|
|
575
|
+
command: "bundle",
|
|
576
|
+
args: ["exec", "rspec", specFile],
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// PHP
|
|
584
|
+
if (langs.has("php")) {
|
|
585
|
+
for (const file of task.contextFiles) {
|
|
586
|
+
if (/Test\.php$/u.test(file)) {
|
|
587
|
+
inferred.push({ command: "composer", args: ["test", "--", file] });
|
|
588
|
+
} else if (/\.php$/u.test(file)) {
|
|
589
|
+
const testFile = file
|
|
590
|
+
.replace(/\.php$/u, "Test.php")
|
|
591
|
+
.replace(/^src\//u, "tests/");
|
|
592
|
+
if (!inferred.some((cmd) => cmd.args.includes(testFile))) {
|
|
593
|
+
inferred.push({
|
|
594
|
+
command: "composer",
|
|
595
|
+
args: ["test", "--", testFile],
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return inferred;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export async function readVerificationPlan(
|
|
606
|
+
rootDir: string,
|
|
607
|
+
task?: TaskBrief,
|
|
608
|
+
): Promise<VerificationPlan> {
|
|
609
|
+
try {
|
|
610
|
+
const content = await readFile(
|
|
611
|
+
path.join(rootDir, ".omni", "TESTS.md"),
|
|
612
|
+
"utf8",
|
|
613
|
+
);
|
|
614
|
+
const projectLines = splitVerificationSection(
|
|
615
|
+
content,
|
|
616
|
+
"## Project-wide checks",
|
|
617
|
+
);
|
|
618
|
+
const taskLines = splitVerificationSection(
|
|
619
|
+
content,
|
|
620
|
+
"## Task-specific checks",
|
|
621
|
+
);
|
|
622
|
+
const customLines = splitVerificationSection(content, "## Custom checks");
|
|
623
|
+
const selectedTaskLines = task
|
|
624
|
+
? taskLines.filter((line) => matchesTask(line.value, task))
|
|
625
|
+
: taskLines;
|
|
626
|
+
|
|
627
|
+
const commands: VerificationExec[] = [];
|
|
628
|
+
const expectations: string[] = [];
|
|
629
|
+
for (const line of [
|
|
630
|
+
...projectLines,
|
|
631
|
+
...selectedTaskLines,
|
|
632
|
+
...customLines,
|
|
633
|
+
]) {
|
|
634
|
+
if (isRunnableCommand(line.command)) {
|
|
635
|
+
commands.push(line.command);
|
|
636
|
+
} else {
|
|
637
|
+
expectations.push(line.value);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (task) {
|
|
642
|
+
const repoSignals = await detectRepoSignals(rootDir);
|
|
643
|
+
const contextCommands = inferTestCommandsFromContext(
|
|
644
|
+
task,
|
|
645
|
+
repoSignals.languages,
|
|
646
|
+
);
|
|
647
|
+
for (const cmd of contextCommands) {
|
|
648
|
+
const key = [cmd.command, ...cmd.args].join(" ");
|
|
649
|
+
if (
|
|
650
|
+
!commands.some(
|
|
651
|
+
(existing) =>
|
|
652
|
+
[existing.command, ...existing.args].join(" ") === key,
|
|
653
|
+
)
|
|
654
|
+
) {
|
|
655
|
+
commands.push(cmd);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
for (const criterion of task.doneCriteria) {
|
|
660
|
+
if (!expectations.includes(criterion)) {
|
|
661
|
+
expectations.push(criterion);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return {
|
|
667
|
+
commands,
|
|
668
|
+
expectations: [...new Set(expectations)],
|
|
669
|
+
};
|
|
670
|
+
} catch {
|
|
671
|
+
return {
|
|
672
|
+
commands: [],
|
|
673
|
+
expectations: [],
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async function runVerificationCommands(
|
|
679
|
+
executor: VerificationExecutor | undefined,
|
|
680
|
+
plan: VerificationPlan,
|
|
681
|
+
rootDir: string,
|
|
682
|
+
): Promise<VerificationCommandResult[]> {
|
|
683
|
+
if (!executor || plan.commands.length === 0) {
|
|
684
|
+
return [];
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const results: VerificationCommandResult[] = [];
|
|
688
|
+
for (const item of plan.commands) {
|
|
689
|
+
const execResult = await executor.exec(item.command, item.args, {
|
|
690
|
+
cwd: item.cwd ?? rootDir,
|
|
691
|
+
});
|
|
692
|
+
results.push({
|
|
693
|
+
command: [item.command, ...item.args].join(" "),
|
|
694
|
+
passed: execResult.code === 0 && !execResult.killed,
|
|
695
|
+
stdout: execResult.stdout,
|
|
696
|
+
stderr: execResult.stderr,
|
|
697
|
+
code: execResult.code,
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
return results;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function findAgent(
|
|
704
|
+
agents: SubagentConfig[],
|
|
705
|
+
preferred: string,
|
|
706
|
+
fallback: string,
|
|
707
|
+
): string {
|
|
708
|
+
if (agents.some((agent) => agent.name === preferred)) {
|
|
709
|
+
return preferred;
|
|
710
|
+
}
|
|
711
|
+
return fallback;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const AGENT_ROLE_MAP: Record<string, keyof OmniConfig["models"]> = {
|
|
715
|
+
"omni-worker": "worker",
|
|
716
|
+
"omni-expert": "expert",
|
|
717
|
+
"omni-planner": "planner",
|
|
718
|
+
"omni-brain": "brain",
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
function applyModelOverrides(
|
|
722
|
+
agents: SubagentConfig[],
|
|
723
|
+
config: OmniConfig | undefined,
|
|
724
|
+
): SubagentConfig[] {
|
|
725
|
+
if (!config) {
|
|
726
|
+
return agents;
|
|
727
|
+
}
|
|
728
|
+
return agents.map((agent) => {
|
|
729
|
+
const role = AGENT_ROLE_MAP[agent.name];
|
|
730
|
+
const modelOverride = role ? config.models[role] : undefined;
|
|
731
|
+
if (modelOverride) {
|
|
732
|
+
return { ...agent, model: modelOverride };
|
|
733
|
+
}
|
|
734
|
+
return agent;
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
export async function createSubagentWorkEngine(
|
|
739
|
+
rootDir: string,
|
|
740
|
+
ctx: ExtensionCommandContext,
|
|
741
|
+
deps?: SubagentDeps,
|
|
742
|
+
verificationExecutor?: VerificationExecutor,
|
|
743
|
+
): Promise<WorkEngine> {
|
|
744
|
+
const subagentDeps = deps ?? (await loadSubagentDeps());
|
|
745
|
+
const config = await readConfig(rootDir);
|
|
746
|
+
const discovery = subagentDeps.discoverAgents(rootDir, "both");
|
|
747
|
+
const agentsWithOverrides = applyModelOverrides(discovery.agents, config);
|
|
748
|
+
const workerAgent = findAgent(agentsWithOverrides, "omni-worker", "worker");
|
|
749
|
+
const expertAgent = findAgent(agentsWithOverrides, "omni-expert", "reviewer");
|
|
750
|
+
const sessionDir = path.join(rootDir, ".omni", "subagent-sessions");
|
|
751
|
+
const packageDir = omniPackageDir();
|
|
752
|
+
const skillTriggers = await loadSkillTriggers(
|
|
753
|
+
path.join(packageDir, "skills"),
|
|
754
|
+
);
|
|
755
|
+
|
|
756
|
+
function getSkillContext(task: TaskBrief): string | undefined {
|
|
757
|
+
const matched = matchSkillsForTask(task, skillTriggers);
|
|
758
|
+
if (matched.length === 0) return undefined;
|
|
759
|
+
return matched
|
|
760
|
+
.map(
|
|
761
|
+
(s) =>
|
|
762
|
+
`[${s.name}]\n${s.content.replace(/^---[\s\S]*?---\n*/u, "").trim()}`,
|
|
763
|
+
)
|
|
764
|
+
.join("\n\n");
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
return {
|
|
768
|
+
runWorkerTask: async (task, attempt) => {
|
|
769
|
+
const verificationPlan = await readVerificationPlan(rootDir, task);
|
|
770
|
+
const preReadContext = await gatherTaskContext(rootDir, task, 4000);
|
|
771
|
+
ctx.ui.setStatus(
|
|
772
|
+
"omni",
|
|
773
|
+
`Worker ${workerAgent} is handling ${task.id} (attempt ${attempt})`,
|
|
774
|
+
);
|
|
775
|
+
const startTime = Date.now();
|
|
776
|
+
const result = await subagentDeps.runSync(
|
|
777
|
+
rootDir,
|
|
778
|
+
agentsWithOverrides,
|
|
779
|
+
workerAgent,
|
|
780
|
+
buildWorkerPrompt(
|
|
781
|
+
task,
|
|
782
|
+
verificationPlan,
|
|
783
|
+
getSkillContext(task),
|
|
784
|
+
preReadContext,
|
|
785
|
+
),
|
|
786
|
+
{
|
|
787
|
+
cwd: rootDir,
|
|
788
|
+
runId: randomUUID(),
|
|
789
|
+
sessionDir,
|
|
790
|
+
onUpdate: (update) => {
|
|
791
|
+
const progress = update.details?.progress?.[0];
|
|
792
|
+
if (progress) {
|
|
793
|
+
ctx.ui.setStatus(
|
|
794
|
+
"omni",
|
|
795
|
+
`${progress.agent}: ${progress.currentTool ?? "working"}${progress.toolCount ? ` (${progress.toolCount} tools)` : ""}`,
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
},
|
|
799
|
+
},
|
|
800
|
+
);
|
|
801
|
+
const raw = subagentDeps.getFinalOutput(result.messages);
|
|
802
|
+
const rawOutputPath = path.join(
|
|
803
|
+
rootDir,
|
|
804
|
+
".omni",
|
|
805
|
+
"tasks",
|
|
806
|
+
`${task.id}-worker-attempt-${attempt}.md`,
|
|
807
|
+
);
|
|
808
|
+
await persistRawOutput(
|
|
809
|
+
rootDir,
|
|
810
|
+
task.id,
|
|
811
|
+
`worker-attempt-${attempt}`,
|
|
812
|
+
raw,
|
|
813
|
+
);
|
|
814
|
+
const parsed = parseAttemptResult(
|
|
815
|
+
raw,
|
|
816
|
+
`Worker ${workerAgent} completed without a structured verdict.`,
|
|
817
|
+
);
|
|
818
|
+
parsed.verification.taskId = task.id;
|
|
819
|
+
if (result.exitCode !== 0 || result.error) {
|
|
820
|
+
parsed.verification.passed = false;
|
|
821
|
+
parsed.verification.failureSummary = [
|
|
822
|
+
result.error ?? `Worker exited with code ${result.exitCode}`,
|
|
823
|
+
];
|
|
824
|
+
}
|
|
825
|
+
const verificationResults = await runVerificationCommands(
|
|
826
|
+
verificationExecutor,
|
|
827
|
+
verificationPlan,
|
|
828
|
+
rootDir,
|
|
829
|
+
);
|
|
830
|
+
if (verificationResults.length > 0) {
|
|
831
|
+
parsed.verification.checksRun = verificationResults.map(
|
|
832
|
+
(item) => item.command,
|
|
833
|
+
);
|
|
834
|
+
const failed = verificationResults.filter((item) => !item.passed);
|
|
835
|
+
parsed.verification.passed = failed.length === 0;
|
|
836
|
+
parsed.verification.failureSummary = failed.map(
|
|
837
|
+
(item) => `${item.command} failed with exit code ${item.code}`,
|
|
838
|
+
);
|
|
839
|
+
parsed.verification.retryRecommended = failed.length > 0;
|
|
840
|
+
}
|
|
841
|
+
const modifiedFiles = extractModifiedFiles(raw);
|
|
842
|
+
await persistRunRecord(rootDir, {
|
|
843
|
+
agent: workerAgent,
|
|
844
|
+
taskId: task.id,
|
|
845
|
+
attemptLabel: `worker-attempt-${attempt}`,
|
|
846
|
+
exitCode: result.exitCode,
|
|
847
|
+
rawOutputPath,
|
|
848
|
+
passed: parsed.verification.passed,
|
|
849
|
+
checksRun: parsed.verification.checksRun,
|
|
850
|
+
failureSummary: parsed.verification.failureSummary,
|
|
851
|
+
verificationCommands: verificationResults,
|
|
852
|
+
modifiedFiles,
|
|
853
|
+
});
|
|
854
|
+
subagentDeps.recordRun?.(
|
|
855
|
+
workerAgent,
|
|
856
|
+
`${task.id} attempt ${attempt}`,
|
|
857
|
+
result.exitCode,
|
|
858
|
+
Date.now() - startTime,
|
|
859
|
+
);
|
|
860
|
+
return { ...parsed, modifiedFiles };
|
|
861
|
+
},
|
|
862
|
+
runExpertTask: async (task, escalation) => {
|
|
863
|
+
const verificationPlan = await readVerificationPlan(rootDir, task);
|
|
864
|
+
const failedChecksSummary =
|
|
865
|
+
escalation.verificationResults
|
|
866
|
+
?.filter((r) => !r.passed)
|
|
867
|
+
.map((r) => r.command)
|
|
868
|
+
.join(", ") || "none";
|
|
869
|
+
ctx.ui.setStatus(
|
|
870
|
+
"omni",
|
|
871
|
+
`Escalating ${task.id} to expert after ${escalation.priorAttempts} failed attempts. Failed checks: ${failedChecksSummary}`,
|
|
872
|
+
);
|
|
873
|
+
const preReadContext = await gatherTaskContext(rootDir, task, 6000);
|
|
874
|
+
const expertStartTime = Date.now();
|
|
875
|
+
const result = await subagentDeps.runSync(
|
|
876
|
+
rootDir,
|
|
877
|
+
agentsWithOverrides,
|
|
878
|
+
expertAgent,
|
|
879
|
+
buildExpertPrompt(
|
|
880
|
+
task,
|
|
881
|
+
escalation,
|
|
882
|
+
verificationPlan,
|
|
883
|
+
getSkillContext(task),
|
|
884
|
+
preReadContext,
|
|
885
|
+
),
|
|
886
|
+
{
|
|
887
|
+
cwd: rootDir,
|
|
888
|
+
runId: randomUUID(),
|
|
889
|
+
sessionDir,
|
|
890
|
+
onUpdate: (update) => {
|
|
891
|
+
const progress = update.details?.progress?.[0];
|
|
892
|
+
if (progress) {
|
|
893
|
+
ctx.ui.setStatus(
|
|
894
|
+
"omni",
|
|
895
|
+
`${progress.agent}: ${progress.currentTool ?? "resolving"}${progress.toolCount ? ` (${progress.toolCount} tools)` : ""}`,
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
},
|
|
899
|
+
},
|
|
900
|
+
);
|
|
901
|
+
const raw = subagentDeps.getFinalOutput(result.messages);
|
|
902
|
+
const rawOutputPath = path.join(
|
|
903
|
+
rootDir,
|
|
904
|
+
".omni",
|
|
905
|
+
"tasks",
|
|
906
|
+
`${task.id}-expert-output.md`,
|
|
907
|
+
);
|
|
908
|
+
await persistRawOutput(rootDir, task.id, "expert-output", raw);
|
|
909
|
+
const parsed = parseAttemptResult(
|
|
910
|
+
raw,
|
|
911
|
+
`Expert ${expertAgent} completed without a structured verdict.`,
|
|
912
|
+
);
|
|
913
|
+
parsed.verification.taskId = task.id;
|
|
914
|
+
if (result.exitCode !== 0 || result.error) {
|
|
915
|
+
parsed.verification.passed = false;
|
|
916
|
+
parsed.verification.failureSummary = [
|
|
917
|
+
result.error ?? `Expert exited with code ${result.exitCode}`,
|
|
918
|
+
];
|
|
919
|
+
}
|
|
920
|
+
const verificationResults = await runVerificationCommands(
|
|
921
|
+
verificationExecutor,
|
|
922
|
+
verificationPlan,
|
|
923
|
+
rootDir,
|
|
924
|
+
);
|
|
925
|
+
if (verificationResults.length > 0) {
|
|
926
|
+
parsed.verification.checksRun = verificationResults.map(
|
|
927
|
+
(item) => item.command,
|
|
928
|
+
);
|
|
929
|
+
const failed = verificationResults.filter((item) => !item.passed);
|
|
930
|
+
parsed.verification.passed = failed.length === 0;
|
|
931
|
+
parsed.verification.failureSummary = failed.map(
|
|
932
|
+
(item) => `${item.command} failed with exit code ${item.code}`,
|
|
933
|
+
);
|
|
934
|
+
parsed.verification.retryRecommended = failed.length > 0;
|
|
935
|
+
}
|
|
936
|
+
const modifiedFiles = extractModifiedFiles(raw);
|
|
937
|
+
await persistRunRecord(rootDir, {
|
|
938
|
+
agent: expertAgent,
|
|
939
|
+
taskId: task.id,
|
|
940
|
+
attemptLabel: "expert-output",
|
|
941
|
+
exitCode: result.exitCode,
|
|
942
|
+
rawOutputPath,
|
|
943
|
+
passed: parsed.verification.passed,
|
|
944
|
+
checksRun: parsed.verification.checksRun,
|
|
945
|
+
failureSummary: parsed.verification.failureSummary,
|
|
946
|
+
verificationCommands: verificationResults,
|
|
947
|
+
modifiedFiles,
|
|
948
|
+
});
|
|
949
|
+
subagentDeps.recordRun?.(
|
|
950
|
+
expertAgent,
|
|
951
|
+
`${task.id} expert`,
|
|
952
|
+
result.exitCode,
|
|
953
|
+
Date.now() - expertStartTime,
|
|
954
|
+
);
|
|
955
|
+
return { ...parsed, modifiedFiles };
|
|
956
|
+
},
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function buildScoutPrompt(task: TaskBrief): string {
|
|
961
|
+
return [
|
|
962
|
+
"You are Omni-Pi's scout agent.",
|
|
963
|
+
"Analyze the codebase to gather context for the upcoming implementation task.",
|
|
964
|
+
"Return a concise summary of: relevant files, existing patterns to follow, potential pitfalls, and suggested approach.",
|
|
965
|
+
"Do NOT make any code changes. Only read and analyze.",
|
|
966
|
+
"",
|
|
967
|
+
`Task ID: ${task.id}`,
|
|
968
|
+
`Title: ${task.title}`,
|
|
969
|
+
`Objective: ${task.objective}`,
|
|
970
|
+
`Context files: ${task.contextFiles.join(", ") || "none specified"}`,
|
|
971
|
+
"Required files to read:",
|
|
972
|
+
"- .omni/PROJECT.md",
|
|
973
|
+
"- .omni/SPEC.md",
|
|
974
|
+
...task.contextFiles.map((file) => `- ${file}`),
|
|
975
|
+
].join("\n");
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
export async function createChainWorkEngine(
|
|
979
|
+
rootDir: string,
|
|
980
|
+
ctx: ExtensionCommandContext,
|
|
981
|
+
deps?: SubagentDeps,
|
|
982
|
+
verificationExecutor?: VerificationExecutor,
|
|
983
|
+
): Promise<WorkEngine> {
|
|
984
|
+
const baseEngine = await createSubagentWorkEngine(
|
|
985
|
+
rootDir,
|
|
986
|
+
ctx,
|
|
987
|
+
deps,
|
|
988
|
+
verificationExecutor,
|
|
989
|
+
);
|
|
990
|
+
const subagentDeps = deps ?? (await loadSubagentDeps());
|
|
991
|
+
const config = await readConfig(rootDir);
|
|
992
|
+
const discovery = subagentDeps.discoverAgents(rootDir, "both");
|
|
993
|
+
const agentsWithOverrides = applyModelOverrides(discovery.agents, config);
|
|
994
|
+
const scoutAgent = findAgent(agentsWithOverrides, "omni-worker", "worker");
|
|
995
|
+
const sessionDir = path.join(rootDir, ".omni", "subagent-sessions");
|
|
996
|
+
|
|
997
|
+
return {
|
|
998
|
+
runWorkerTask: async (task, attempt) => {
|
|
999
|
+
ctx.ui.setStatus(
|
|
1000
|
+
"omni",
|
|
1001
|
+
`Scout analyzing ${task.id} before worker execution`,
|
|
1002
|
+
);
|
|
1003
|
+
const scoutResult = await subagentDeps.runSync(
|
|
1004
|
+
rootDir,
|
|
1005
|
+
agentsWithOverrides,
|
|
1006
|
+
scoutAgent,
|
|
1007
|
+
buildScoutPrompt(task),
|
|
1008
|
+
{
|
|
1009
|
+
cwd: rootDir,
|
|
1010
|
+
runId: randomUUID(),
|
|
1011
|
+
sessionDir,
|
|
1012
|
+
},
|
|
1013
|
+
);
|
|
1014
|
+
const scoutOutput = subagentDeps.getFinalOutput(scoutResult.messages);
|
|
1015
|
+
await persistRawOutput(
|
|
1016
|
+
rootDir,
|
|
1017
|
+
task.id,
|
|
1018
|
+
`scout-attempt-${attempt}`,
|
|
1019
|
+
scoutOutput,
|
|
1020
|
+
);
|
|
1021
|
+
|
|
1022
|
+
const enrichedTask: TaskBrief = {
|
|
1023
|
+
...task,
|
|
1024
|
+
objective: `${task.objective}\n\nScout analysis:\n${scoutOutput.slice(0, 2000)}`,
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
return baseEngine.runWorkerTask(enrichedTask, attempt);
|
|
1028
|
+
},
|
|
1029
|
+
runExpertTask: baseEngine.runExpertTask,
|
|
1030
|
+
};
|
|
1031
|
+
}
|