omp-llm-role 1.0.0 → 1.2.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/README.md +202 -12
- package/agents/designer.md +4 -3
- package/docs/dev/README.md +44 -7
- package/docs/dev/agent-authoring.md +59 -1
- package/docs/dev/architecture.md +106 -28
- package/docs/dev/ci.md +163 -0
- package/docs/dev/current-state.md +364 -24
- package/docs/dev/data-sources.md +231 -38
- package/docs/dev/explorer.md +175 -46
- package/docs/dev/quirks.md +77 -4
- package/docs/dev/releasing.md +130 -17
- package/docs/dev/scoring.md +851 -27
- package/docs/dev/testing.md +253 -0
- package/docs/dev/writes-and-state.md +232 -28
- package/docs/llm-role-rankings.md +149 -148
- package/package.json +814 -80
- package/skills/omp-llm-role-create-agent/SKILL.md +7 -1
- package/src/agent-architect.ts +148 -19
- package/src/agent-create.ts +800 -40
- package/src/agent-file.ts +2 -5
- package/src/architect-provenance.ts +16 -0
- package/src/availability.ts +191 -15
- package/src/benchmark-author.ts +9 -4
- package/src/benchmark-sources.ts +283 -18
- package/src/cli/create-role.ts +29 -1
- package/src/cli/llm-role-rank.ts +40 -1
- package/src/config-edit.ts +207 -80
- package/src/engine.ts +519 -75
- package/src/error-report.ts +562 -0
- package/src/explorer/boot.ts +36 -24
- package/src/explorer/explain.ts +128 -13
- package/src/explorer/scopes.ts +110 -0
- package/src/explorer/server.ts +126 -33
- package/src/extension.ts +464 -48
- package/src/features.ts +105 -0
- package/src/project-registry.ts +65 -0
- package/src/project-setup.ts +382 -0
- package/src/prompts/agent-creation-architect.md +2 -0
- package/src/prompts/project-profile-architect.md +33 -0
- package/src/prompts/project-profile-user.md +8 -0
- package/src/provider-display.ts +67 -0
- package/src/role-archetypes.ts +12 -2
- package/src/role-settings.ts +22 -0
- package/src/settings.ts +211 -24
- package/src/state.ts +1 -1
- package/src/updater.ts +234 -37
- package/web/app.js +434 -12
- package/web/index.html +1 -0
- package/web/style.css +44 -1
- package/docs/dev/spec.md +0 -633
|
@@ -102,7 +102,7 @@ These are the same sets `/create-agent` fits automatically — the executable co
|
|
|
102
102
|
| prose / writing (WritingBench-weighted) | general .24, reasoning .16, long_context .10, writing .26, price .14, throughput .10 | general, price, throughput |
|
|
103
103
|
| data / analysis | math .28, reasoning .26, general .20, code .10, price .10, throughput .06 | general, price, throughput |
|
|
104
104
|
| research / search | search .28, general .24, reasoning .20, long_context .10, price .10, throughput .08 | general, price, throughput |
|
|
105
|
-
| design / UI |
|
|
105
|
+
| design / UI | general .26, code .06, vision .18, throughput .15, price .17, website .18 (+ `filters.image`) | general, price, throughput |
|
|
106
106
|
| refactor / migration | code .30, agents .20, general .20, long_context .10, price .12, throughput .08 | general, price, throughput |
|
|
107
107
|
| test / QA | code .28, agents .20, tool_calling .14, general .18, price .12, throughput .08 | general, price, throughput |
|
|
108
108
|
| ops / infra | agents .24, tool_calling .20, general .20, code .14, price .12, throughput .10 | general, price, throughput |
|
|
@@ -114,6 +114,12 @@ Rules of thumb:
|
|
|
114
114
|
the ranking. An external metric key is `<namespace>:<local>` and MUST be
|
|
115
115
|
dot-free (the flat dotted settings path splits on `.`); the registry resolves
|
|
116
116
|
it and the explorer/report render it without a hand-maintained table.
|
|
117
|
+
- The focus set is capped at 3 (named/linked first, then discovery order), so one
|
|
118
|
+
benchmark keeps a decisive share. Discovery refuses a fill-0 benchmark whose
|
|
119
|
+
coverage is below 35% of the ranking field (a sparse pass-rate metric would
|
|
120
|
+
turn `q` into a coverage score); a capability-filled metric (`writing`,
|
|
121
|
+
`website`, `bench:<id>`) is safe at any coverage. The report prints each focus
|
|
122
|
+
metric's coverage and warns below the bar.
|
|
117
123
|
- A weighted metric with low coverage turns q into a coverage score — prefer a `filters`
|
|
118
124
|
gate over requiring a sparse metric. `website`, `long_context` and `writing` are
|
|
119
125
|
capability-filled (0.195), so they are safe to weight. `writing` (WritingBench, the
|
package/src/agent-architect.ts
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* omp's agent-creation architect
|
|
2
|
+
* omp's in-process architects: the agent-creation architect (`/create-agent`,
|
|
3
|
+
* `/agents` hub) and the project-profile architect (`/project-roles`).
|
|
3
4
|
*
|
|
4
5
|
* `/create-agent` no longer authors the agent body from a template: it runs the
|
|
5
6
|
* same LLM architect omp's `/agents` hub uses (the prompt is shipped verbatim
|
|
6
7
|
* from omp 18.4.8), then the plugin adds the `model:`/`tools:` frontmatter omp's
|
|
7
|
-
* own writer omits.
|
|
8
|
-
*
|
|
9
|
-
* resolve, so the prompt is a plugin asset rather than an omp import.
|
|
8
|
+
* own writer omits. `/project-roles` runs a second architect over the repo's own
|
|
9
|
+
* artifacts to discover the project's usecase as a role set.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* only resolves inside omp
|
|
13
|
-
*
|
|
11
|
+
* The SDK (`@oh-my-pi/pi-coding-agent`) is imported dynamically, inside the
|
|
12
|
+
* architect runner: it only resolves inside omp, and a static import failure
|
|
13
|
+
* would be fatal to the whole `extension.ts` load. Dynamic, this module stays
|
|
14
|
+
* importable in a plain Node test (the command registration is testable) and a
|
|
15
|
+
* resolution failure degrades to the caller's error path. The prompts are plugin
|
|
16
|
+
* assets rather than omp imports (omp's extension loader does not resolve the
|
|
17
|
+
* SDK subpath they live under).
|
|
14
18
|
*/
|
|
15
19
|
|
|
16
20
|
import { readFileSync } from "node:fs";
|
|
17
|
-
import { createAgentSession } from "@oh-my-pi/pi-coding-agent";
|
|
18
21
|
import type { AgentSpec } from "./agent-create.ts";
|
|
22
|
+
import type { ProjectProfile, ProposedRole } from "./project-setup.ts";
|
|
23
|
+
import { ARCHITECT_PROMPT_VERSION } from "./architect-provenance.ts";
|
|
24
|
+
|
|
25
|
+
export { ARCHITECT_PROMPT_VERSION };
|
|
19
26
|
|
|
20
27
|
const ARCHITECT_PROMPT = readFileSync(new URL("./prompts/agent-creation-architect.md", import.meta.url), "utf8");
|
|
21
28
|
const USER_PROMPT = readFileSync(new URL("./prompts/agent-creation-user.md", import.meta.url), "utf8");
|
|
@@ -90,16 +97,30 @@ export type ArchitectOptions = {
|
|
|
90
97
|
};
|
|
91
98
|
|
|
92
99
|
/**
|
|
93
|
-
* Run
|
|
94
|
-
* no tools, no LSP/MCP/extensions/skills/context files, and
|
|
95
|
-
* process state (the host session keeps the process-wide effects).
|
|
100
|
+
* Run one isolated architect session and return its raw assistant text. The
|
|
101
|
+
* session has no tools, no LSP/MCP/extensions/skills/context files, and does not
|
|
102
|
+
* bind process state (the host session keeps the process-wide effects).
|
|
96
103
|
*/
|
|
97
|
-
|
|
104
|
+
async function runArchitect(opts: {
|
|
105
|
+
systemPrompt: string;
|
|
106
|
+
userPrompt: string;
|
|
107
|
+
cwd: string;
|
|
108
|
+
model?: unknown;
|
|
109
|
+
modelRegistry?: unknown;
|
|
110
|
+
onText?: (text: string) => void;
|
|
111
|
+
}): Promise<string> {
|
|
112
|
+
// The SDK is imported dynamically, inside the function. Static import cannot
|
|
113
|
+
// work: `@oh-my-pi/pi-coding-agent` only resolves inside omp, and a static
|
|
114
|
+
// import failure is fatal to the whole `extension.ts` load — it would kill
|
|
115
|
+
// every plugin command, not just the architect. Dynamic, the module stays
|
|
116
|
+
// importable in a plain Node test (the command registration is testable) and a
|
|
117
|
+
// resolution failure degrades to the caller's error path.
|
|
118
|
+
const { createAgentSession } = await import("@oh-my-pi/pi-coding-agent");
|
|
98
119
|
const { session } = await createAgentSession({
|
|
99
120
|
cwd: opts.cwd,
|
|
100
121
|
model: opts.model as never,
|
|
101
122
|
modelRegistry: opts.modelRegistry as never,
|
|
102
|
-
systemPrompt: [
|
|
123
|
+
systemPrompt: [opts.systemPrompt],
|
|
103
124
|
hasUI: false,
|
|
104
125
|
enableLsp: false,
|
|
105
126
|
enableMCP: false,
|
|
@@ -119,16 +140,124 @@ export async function generateAgentSpec(opts: ArchitectOptions): Promise<AgentSp
|
|
|
119
140
|
}
|
|
120
141
|
});
|
|
121
142
|
try {
|
|
122
|
-
|
|
123
|
-
opts.benchmarks !== undefined && opts.benchmarks.length > 0
|
|
124
|
-
? `${opts.description}\n\nThis agent's model role is ranked on: ${opts.benchmarks.join(", ")}. Name that benchmark in the agent's rubric so the body matches the model that was chosen.`
|
|
125
|
-
: opts.description;
|
|
126
|
-
await session.prompt(USER_PROMPT.replace("{{request}}", request), { expandPromptTemplates: false });
|
|
143
|
+
await session.prompt(opts.userPrompt, { expandPromptTemplates: false });
|
|
127
144
|
const raw = extractAssistantText(session.state.messages);
|
|
128
145
|
if (raw === null) throw new Error("architect returned no text");
|
|
129
|
-
return
|
|
146
|
+
return raw;
|
|
130
147
|
} finally {
|
|
131
148
|
unsubscribe();
|
|
132
149
|
await session.dispose();
|
|
133
150
|
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Run the architect and parse its output, retrying only a PARSE failure. A
|
|
155
|
+
* `run()` failure (the SDK import, "architect returned no text") propagates
|
|
156
|
+
* immediately: a second session would fail the same way. The final error
|
|
157
|
+
* carries the raw output so the caller's `notifyLines(ctx, ...err.message)`
|
|
158
|
+
* surfaces what the architect actually said.
|
|
159
|
+
*/
|
|
160
|
+
export async function withArchitectRetry<T>(run: () => Promise<string>, parse: (raw: string) => T, attempts = 2): Promise<T> {
|
|
161
|
+
let lastRaw = "";
|
|
162
|
+
let lastError: unknown;
|
|
163
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
164
|
+
lastRaw = await run();
|
|
165
|
+
try {
|
|
166
|
+
return parse(lastRaw);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
lastError = err;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const detail = lastError instanceof Error ? lastError.message : String(lastError);
|
|
172
|
+
const raw = lastRaw.length > 2000 ? `${lastRaw.slice(0, 2000)}…` : lastRaw;
|
|
173
|
+
throw new Error(`${detail} (after ${attempts} attempts)\n--- raw architect output ---\n${raw}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Run omp's architect and return the validated spec. The session is isolated:
|
|
178
|
+
* no tools, no LSP/MCP/extensions/skills/context files, and it does not bind
|
|
179
|
+
* process state (the host session keeps the process-wide effects).
|
|
180
|
+
*/
|
|
181
|
+
export async function generateAgentSpec(opts: ArchitectOptions): Promise<AgentSpec> {
|
|
182
|
+
const request =
|
|
183
|
+
opts.benchmarks !== undefined && opts.benchmarks.length > 0
|
|
184
|
+
? `${opts.description}\n\nThis agent's model role is ranked on: ${opts.benchmarks.join(", ")}. Name that benchmark in the agent's rubric so the body matches the model that was chosen.`
|
|
185
|
+
: opts.description;
|
|
186
|
+
return withArchitectRetry(
|
|
187
|
+
() =>
|
|
188
|
+
runArchitect({
|
|
189
|
+
systemPrompt: ARCHITECT_PROMPT,
|
|
190
|
+
userPrompt: USER_PROMPT.replace("{{request}}", request),
|
|
191
|
+
cwd: opts.cwd,
|
|
192
|
+
model: opts.model,
|
|
193
|
+
modelRegistry: opts.modelRegistry,
|
|
194
|
+
onText: opts.onText,
|
|
195
|
+
}),
|
|
196
|
+
parseAgentSpec,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const PROJECT_ARCHITECT_PROMPT = readFileSync(new URL("./prompts/project-profile-architect.md", import.meta.url), "utf8");
|
|
201
|
+
const PROJECT_USER_PROMPT = readFileSync(new URL("./prompts/project-profile-user.md", import.meta.url), "utf8");
|
|
202
|
+
|
|
203
|
+
/** Validate the profile architect's JSON into a `ProjectProfile`. */
|
|
204
|
+
export function parseProjectProfile(raw: string): ProjectProfile {
|
|
205
|
+
const parsed = JSON.parse(extractJsonObject(raw)) as Partial<ProjectProfile>;
|
|
206
|
+
if (!parsed || typeof parsed !== "object") throw new Error("profile architect output is not a JSON object");
|
|
207
|
+
const stringArray = (value: unknown, field: string): string[] => {
|
|
208
|
+
if (!Array.isArray(value) || value.some((x) => typeof x !== "string")) throw new Error(`profile.${field} must be a string array`);
|
|
209
|
+
return value as string[];
|
|
210
|
+
};
|
|
211
|
+
if (typeof parsed.summary !== "string" || parsed.summary.trim() === "") throw new Error("profile.summary is required");
|
|
212
|
+
if (typeof parsed.domain !== "string") throw new Error("profile.domain is required");
|
|
213
|
+
const primaryWork = stringArray(parsed.primaryWork, "primaryWork");
|
|
214
|
+
const stack = stringArray(parsed.stack, "stack");
|
|
215
|
+
const needs = stringArray(parsed.needs, "needs");
|
|
216
|
+
if (!Array.isArray(parsed.roles)) throw new Error("profile.roles must be an array");
|
|
217
|
+
const roles: ProposedRole[] = parsed.roles.map((role, i) => {
|
|
218
|
+
if (!role || typeof role !== "object") throw new Error(`profile.roles[${i}] is not an object`);
|
|
219
|
+
const r = role as Partial<ProposedRole>;
|
|
220
|
+
if (typeof r.name !== "string" || r.name.trim() === "") throw new Error(`profile.roles[${i}].name is required`);
|
|
221
|
+
// A shipped role to keep/drop needs no purpose; `setupProject` requires one
|
|
222
|
+
// for a new role, so the semantic check stays there.
|
|
223
|
+
const out: ProposedRole = { name: r.name.trim(), purpose: typeof r.purpose === "string" ? r.purpose.trim() : "" };
|
|
224
|
+
if (r.keep === false) out.keep = false;
|
|
225
|
+
if (r.benchmarks !== undefined) out.benchmarks = stringArray(r.benchmarks, `roles[${i}].benchmarks`);
|
|
226
|
+
return out;
|
|
227
|
+
});
|
|
228
|
+
return { summary: parsed.summary.trim(), domain: parsed.domain.trim(), primaryWork, stack, needs, roles };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export type ProjectArchitectOptions = {
|
|
232
|
+
/** The gathered repository artifacts (README, package.json, AGENTS.md, docs, git log, file tree). */
|
|
233
|
+
artifacts: string;
|
|
234
|
+
cwd: string;
|
|
235
|
+
/** Live session model (a `Model`); omitted lets the SDK resolve the default. */
|
|
236
|
+
model?: unknown;
|
|
237
|
+
modelRegistry?: unknown;
|
|
238
|
+
/** A user-supplied usecase override; the architect is told to honor it. */
|
|
239
|
+
purpose?: string;
|
|
240
|
+
/** Streamed architect text, for a live progress line. */
|
|
241
|
+
onText?: (text: string) => void;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Run the profile-discovery architect and return the validated `ProjectProfile`.
|
|
246
|
+
* Same isolated session as `generateAgentSpec`; the extension then resolves
|
|
247
|
+
* benchmarks and authors each new role's agent.
|
|
248
|
+
*/
|
|
249
|
+
export async function generateProjectProfile(opts: ProjectArchitectOptions): Promise<ProjectProfile> {
|
|
250
|
+
const purpose = opts.purpose !== undefined && opts.purpose.trim() !== "" ? `The user overrides the discovered usecase with: ${opts.purpose.trim()}` : "";
|
|
251
|
+
return withArchitectRetry(
|
|
252
|
+
() =>
|
|
253
|
+
runArchitect({
|
|
254
|
+
systemPrompt: PROJECT_ARCHITECT_PROMPT,
|
|
255
|
+
userPrompt: PROJECT_USER_PROMPT.replace("{{artifacts}}", opts.artifacts).replace("{{purpose}}", purpose),
|
|
256
|
+
cwd: opts.cwd,
|
|
257
|
+
model: opts.model,
|
|
258
|
+
modelRegistry: opts.modelRegistry,
|
|
259
|
+
onText: opts.onText,
|
|
260
|
+
}),
|
|
261
|
+
parseProjectProfile,
|
|
262
|
+
);
|
|
134
263
|
}
|