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.
Files changed (50) hide show
  1. package/README.md +202 -12
  2. package/agents/designer.md +4 -3
  3. package/docs/dev/README.md +44 -7
  4. package/docs/dev/agent-authoring.md +59 -1
  5. package/docs/dev/architecture.md +106 -28
  6. package/docs/dev/ci.md +163 -0
  7. package/docs/dev/current-state.md +364 -24
  8. package/docs/dev/data-sources.md +231 -38
  9. package/docs/dev/explorer.md +175 -46
  10. package/docs/dev/quirks.md +77 -4
  11. package/docs/dev/releasing.md +130 -17
  12. package/docs/dev/scoring.md +851 -27
  13. package/docs/dev/testing.md +253 -0
  14. package/docs/dev/writes-and-state.md +232 -28
  15. package/docs/llm-role-rankings.md +149 -148
  16. package/package.json +814 -80
  17. package/skills/omp-llm-role-create-agent/SKILL.md +7 -1
  18. package/src/agent-architect.ts +148 -19
  19. package/src/agent-create.ts +800 -40
  20. package/src/agent-file.ts +2 -5
  21. package/src/architect-provenance.ts +16 -0
  22. package/src/availability.ts +191 -15
  23. package/src/benchmark-author.ts +9 -4
  24. package/src/benchmark-sources.ts +283 -18
  25. package/src/cli/create-role.ts +29 -1
  26. package/src/cli/llm-role-rank.ts +40 -1
  27. package/src/config-edit.ts +207 -80
  28. package/src/engine.ts +519 -75
  29. package/src/error-report.ts +562 -0
  30. package/src/explorer/boot.ts +36 -24
  31. package/src/explorer/explain.ts +128 -13
  32. package/src/explorer/scopes.ts +110 -0
  33. package/src/explorer/server.ts +126 -33
  34. package/src/extension.ts +464 -48
  35. package/src/features.ts +105 -0
  36. package/src/project-registry.ts +65 -0
  37. package/src/project-setup.ts +382 -0
  38. package/src/prompts/agent-creation-architect.md +2 -0
  39. package/src/prompts/project-profile-architect.md +33 -0
  40. package/src/prompts/project-profile-user.md +8 -0
  41. package/src/provider-display.ts +67 -0
  42. package/src/role-archetypes.ts +12 -2
  43. package/src/role-settings.ts +22 -0
  44. package/src/settings.ts +211 -24
  45. package/src/state.ts +1 -1
  46. package/src/updater.ts +234 -37
  47. package/web/app.js +434 -12
  48. package/web/index.html +1 -0
  49. package/web/style.css +44 -1
  50. 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 | vision .30, website .20, general .20, code .10, price .12, throughput .08 (+ `filters.image`) | general, price, throughput |
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
@@ -1,21 +1,28 @@
1
1
  /**
2
- * omp's agent-creation architect, run in-process.
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. The SDK is imported from the host package root — omp's
8
- * extension loader resolves that to the running binary; subpath imports do not
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
- * This module is extension-only: it imports `@oh-my-pi/pi-coding-agent`, which
12
- * only resolves inside omp. The tests import `src/agent-create.ts` instead,
13
- * which never imports this file.
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 omp's architect and return the validated spec. The session is isolated:
94
- * no tools, no LSP/MCP/extensions/skills/context files, and it does not bind
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
- export async function generateAgentSpec(opts: ArchitectOptions): Promise<AgentSpec> {
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: [ARCHITECT_PROMPT],
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
- const request =
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 parseAgentSpec(raw);
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
  }