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
@@ -4,7 +4,7 @@ Maintainer map of the `omp-llm-role` plugin: how the modules fit together, the
4
4
  runtime contract they must satisfy, and the invariants a change must not break.
5
5
 
6
6
  - User-facing install/usage: [`../../README.md`](../../README.md).
7
- - Normative behavior contract: [`spec.md`](spec.md).
7
+ - How it is verified: [`testing.md`](testing.md).
8
8
  - Where the numbers come from: [`data-sources.md`](data-sources.md).
9
9
 
10
10
  ## Module map
@@ -25,32 +25,35 @@ the extension's `/refresh-roles`, `/explore-roles` and `/create-agent`.
25
25
 
26
26
  | File | Role / key exports |
27
27
  |---|---|
28
- | `src/engine.ts` | Ranking engine shared by CLI and plugin. Fetch/cache chain per source, cardinal transforms, value scoring. Exports `loadRankData`, `computeRankings`, `buildModels`, `rankRole`, `paretoFrontier`, `cardinalMetric`, `roleLambda`, `thinkingPriceFactor`, `CAPABILITY_FILL`, `SUFFIX_LEVELS`, `META_LEVELS`, `THINKING_TOKEN_OVERHEAD`, `parseFindData`, `parseModelPage`, `buildOpenRouterEnrichment`, `applyOpenRouterData`, `applyDesignPercentiles`, `applyWritingScores`, `extractFlight`, `extractJsonArray`, the cache readers/writers, and the `Model`/`RoleDef`/`Ranked`/`RankData` types. Re-exports `normalizeDesignId` and `parseWritingEvidence` from the benchmark-source registry. |
29
- | `src/benchmark-sources.ts` | The benchmark-source registry: the single source of truth for which sources exist, what metric each feeds, and how to fetch/parse/join each. Exports `BENCHMARK_SOURCES` (the shipped static sources), `resolveBenchmarkSource`, `declaredSourceForLink`, `sourceForMetric`, `parseBenchmarkPayload`, `loadBenchmarkScores`, `applyBenchmarkScores`, `joinBenchmarkScores`, `normalizeMetricKey`, `externalMetricKey`, `normalizeDesignId`, `parseWritingEvidence`, `parseLlmStatsBenchmark`, `parseBenchmarkCatalog`, `loadBenchmarkCatalog`, `catalogMetric`, `SHIPPED_CATALOG_METRICS`, the declarative-source surface (`SourceDeclaration`, `validateDeclaration`, `parseSourceDeclaration`, `loadDeclaredSources`, `saveDeclaredSource`, `declaredSourcesPath`, `declarationToSource`, `executeDeclaration`, `llmStatsBenchmarkDeclaration`, `genericBenchmarkSource`, `dryRunDeclaration`), the fetch helpers (`fetchJson`, `fetchText`), and the `BenchmarkSource`/`JoinRule`/`BenchmarkScores`/`DeclarationDryRun` types. Imports `engine.ts` for types only, so there is no runtime cycle. |
30
- | `src/settings.ts` | Shipped defaults and the settings merge/validate path. Exports `DEFAULT_ROLES`, `DEFAULT_SETTINGS`, `SHIPPED_AGENTS`, `KNOWN_METRICS`, `isKnownMetric` (a shipped key or an external `<ns>:<local>`), `PLUGIN_SETTINGS_PATH`, `ACTIVATE_DEFAULT_KEY`, `deriveSettingsSchema`, `readPluginSettingsMap`, `resolveSettings`, `roleUniverse`, `deepMergeInto`, and the `ResolvedSettings`/`PluginSettingSchema`/`UniverseEntry`/`RoleKind` types. |
28
+ | `src/engine.ts` | Ranking engine shared by CLI and plugin. Fetch/cache chain per source, cardinal transforms, value scoring. Exports `loadRankData`, `computeRankings`, `buildModels`, `rankRole`, `paretoFrontier`, `cardinalMetric`, `roleLambda`, `thinkingPriceFactor`, `CAPABILITY_FILL`, `SUFFIX_LEVELS`, `META_LEVELS`, `THINKING_TOKEN_OVERHEAD`, `parseFindData`, `parseModelPage`, `narrowEndpointRecord`, `buildOpenRouterEnrichment`, `applyOpenRouterData`, `hasEndpointFilters`, `routePassesEndpointFilters`, `modelPassesEndpointFilters`, `endpointFilterDrops`, `applyDesignPercentiles`, `applyWritingScores`, `extractFlight`, `extractJsonArray`, the cache readers/writers, and the `Model`/`RoleDef`/`Ranked`/`RankData`/`OpenRouterEndpointRecord` types. Re-exports `normalizeDesignId` and `parseWritingEvidence` from the benchmark-source registry. |
29
+ | `src/benchmark-sources.ts` | The benchmark-source registry: the single source of truth for which sources exist, what metric each feeds, and how to fetch/parse/join each. Exports `BENCHMARK_SOURCES` (the shipped static sources), `resolveBenchmarkSource`, `declaredSourceForLink`, `sourceForMetric`, `parseBenchmarkPayload`, `parseBenchmarkPayloadMeta`, `loadBenchmarkScores`, `applyBenchmarkScores`, `joinBenchmarkScores`, `normalizeMetricKey`, `externalMetricKey`, `normalizeDesignId`, `parseWritingEvidence`, `parseLlmStatsBenchmark`, `parseBenchmarkCatalog`, `loadBenchmarkCatalog`, `cachedSourceInfo`, `catalogMetric`, `SHIPPED_CATALOG_METRICS`, the declarative-source surface (`SourceDeclaration`, `validateDeclaration`, `parseSourceDeclaration`, `loadDeclaredSources`, `saveDeclaredSource`, `declaredSourcesPath`, `declarationToSource`, `executeDeclaration`, `llmStatsBenchmarkDeclaration`, `genericBenchmarkSource`, `dryRunDeclaration`), the fetch helpers (`fetchJson`, `fetchText`), and the `BenchmarkSource`/`JoinRule`/`BenchmarkScores`/`BenchmarkPayloadMeta`/`FocusSourceInfo`/`BenchmarkCatalogEntry`/`DeclarationDryRun` types. Imports `engine.ts` for types only, so there is no runtime cycle. |
30
+ | `src/settings.ts` | Shipped defaults and the settings merge/validate path. Exports `DEFAULT_ROLES`, `DEFAULT_SETTINGS`, `SHIPPED_AGENTS`, `KNOWN_METRICS`, `isKnownMetric` (a shipped key or an external `<ns>:<local>`), `PLUGIN_SETTINGS_PATH`, `ACTIVATE_DEFAULT_KEY`, `deriveSettingsSchema`, `readPluginSettingsMap`, `resolveSettings`, `roleUniverse`, `deepMergeInto`, `findProjectAnchor`, `projectLockPath`, and the `ResolvedSettings`/`PluginSettingSchema`/`UniverseEntry`/`RoleKind` types. `readPluginSettingsMap` merges the project lock file (`projectLockPath(cwd)` = `<cwd>/.omp/plugins/omp-plugins.lock.json`, omp's project dir with **no walk-up**) over the user-level one; `findProjectAnchor` (the walk-up `.omp`/`.git` anchor) is kept only for the `/project-roles` divergence warning. |
31
31
  | `src/agent-pins.ts` | Agent → pinned-role derivation. Exports `parseAgentPin` (first `@<role>` in the `model:` frontmatter) and `discoverAgentPins` (scans the shipped, user and project agent dirs; project > user > plugin). Drives the `task.disabledAgents` sync. |
32
- | `src/availability.ts` | Key tier gate, catalog filter, variant resolution, provider-allowlist probe. Exports `fetchKeyMeta`, `tierGate`, `filterCatalog`, `rankingIdOf`, `currentRankingId`, `resolveVariant`, `enrichThinkingLevels`, `probeModel`, `catalogFromOmpModelsJson`, `THINKING_LEVELS`, and the `CatalogEntry`/`KeyMeta`/`Tier`/`ProbeVerdict` types. |
32
+ | `src/availability.ts` | Key tier gate, catalog filter, variant resolution, provider-allowlist probe, the keyed-catalog availability primitive, and the allowed-providers whitelist harvest. The keyed source is the Bearer-authenticated `GET /api/v1/models` (the account setting "Filter the model catalog for API keys" makes it a per-key allowlist; set membership is the only signal), shared by the explorer and the updater's companion fast path. Exports `fetchKeyMeta`, `tierGate`, `filterCatalog`, `rankingIdOf`, `currentRankingId`, `resolveVariant`, `enrichThinkingLevels`, `probeModel`, `fetchAllowedProviders`, `providerAllowed`, `computeKeyAvailability`, `fetchKeyAvailability`, `catalogFromOmpModelsJson`, `THINKING_LEVELS`, and the `CatalogEntry`/`KeyMeta`/`Tier`/`ProbeVerdict`/`KeyAvailability` types. |
33
33
  | `src/config-edit.ts` | Surgical line-oriented YAML patch for `modelRoles` + `retry.fallbackChains` + `task.disabledAgents`, plus the atomic writer. Exports `parseConfig`, `patchConfig`, `writeConfigAtomic`, `ConfigEditError`, and the `ConfigPatch` type. No runtime YAML dependency. |
34
34
  | `src/state.ts` | State/history/lock files under the agent dir, and agent-dir resolution. Exports `agentDir`, `loadState`, `saveState`, `appendHistory`, `acquireLock`, `releaseLock`, `freshState`, and the `PluginState` type (which carries `managedDisabledAgents` — the `task.disabledAgents` names the plugin added). |
35
- | `src/updater.ts` | Orchestration: rank → tier gate → probe → hysteresis → chains → agent-disable sync → config write. Exports `runUpdater`, the `Deps`/`Decision`/`RunResult`/`Trigger`/`DecisionReason` types. `agentDisablePatch` also removes a previously-managed `task.disabledAgents` name whose agent file is gone. |
36
- | `src/extension.ts` | omp extension entry (default export). Registers `session_start` (awaited day-gated run), `/refresh-roles`, `/explore-roles` (in-process explorer), `/create-agent` (architect + benchmark-link resolution + catalog discovery), `/remove-agent`, and `session_shutdown`. Owns `extDeps` (the extension's `Deps`) and the live-session model hook. |
35
+ | `src/error-report.ts` | Opt-in reporting of **unexpected** plugin errors as GitHub issues on `Gott50/omp-llm-role` (the `errorReporting` setting). The plugin's only GitHub client and the only home for a fingerprint, a redaction rule, the agent-dir ledger (`ERROR_REPORT_LEDGER_FILE`) and the issue body. **Entry point:** `reportUnexpectedError(input, deps)` — the feature's one test seam (per-call input + fully injected `deps`; tests supply a fake `fetch`, clock, env, dir and token resolver). Also exports `defaultErrorReportDeps()` (production deps: resolved settings, global `fetch`, real clock, `agentDir`, the shipped manifest, `gh auth token`, the OS opener) and `resetErrorReportGuard()` (the in-process dedupe guard's test hook). Never throws, never rejects; a no-op when `errorReporting` is `off`. |
36
+ | `src/updater.ts` | Orchestration: rank → tier gate → probe → hysteresis → chains → agent-disable sync → config write. Exports `runUpdater`, the `Deps`/`Decision`/`RunResult`/`Trigger`/`DecisionReason` types. Resolves the project scope (`<cwd>/.omp` when it carries an `omp-llm-role` settings entry) and scopes the config/state/history/lock to it. `agentDisablePatch` also removes a previously-managed `task.disabledAgents` name whose agent file is gone. `RunResult.defect` is set on the exception-abort paths only (a `ConfigEditError`/self-check mismatch and the catch-all throw); the enumerated environment aborts leave it unset. |
37
+ | `src/extension.ts` | omp extension entry (default export). Registers `session_start` (awaited day-gated run), `/refresh-roles`, `/explore-roles` (in-process explorer), `/create-agent` (architect + benchmark-link resolution + catalog discovery), `/remove-agent`, `/project-roles` (profile architect + `setupProject` + in-process updater), and `session_shutdown`. Also subscribes to `session_start`, `retry_fallback_applied`, `retry_fallback_succeeded` and `turn_start` to keep the live model's display name suffixed with its provider pin (`src/provider-display.ts`); each handler mutates the live model's `name` in place and never calls `pi.setModel` (which would wipe omp's in-flight fallback state and clobber the thinking level). Owns `extDeps` (the extension's `Deps`) and the live-session model hook. Every unexpected command-handler throw and every `runUpdater` defect is routed through `src/error-report.ts`: `reportCommandError` for the six command-handler catch sites and `reportDefect` for a `runUpdater` result whose `defect` is set — each keeps the pre-existing one-line warning and adds the reporting module's own lines. |
37
38
  | `src/guards.ts` | The package's one type guard: `isRecord`. |
39
+ | `src/provider-display.ts` | The provider display (spec #77): keep the live session model's `name` suffixed with its upstream provider pin, so omp's model segment renders e.g. `DeepSeek V4.1 Flash @coreweave/fp8`. Pure and dual-runtime (no SDK import). Exports `providerPinOf` (the single `compat.openRouterRouting.only[0]`, else `compat.vercelGatewayRouting.only[0]`; null unless exactly one non-empty string), `providerPinOfSelector` (the trailing `@<slug>` of a selector, after stripping a thinking suffix), `withProviderSuffix` (strip any previous ` @<slug>` then append, idempotent), and `applyProviderDisplay` (mutate a record's `name`; no-op otherwise). |
38
40
  | `src/role-settings.ts` | The one validated role write path (validate → merge → backup → atomic write) and its removal half. Exports `validateRole`, `mergeExport`, `writeRoleSettings`, `mergeRemove`, `removeRoleSettings`, `timestamp`. Shared by the explorer's Export, `src/cli/create-role.ts`, `/create-agent` and `/remove-agent`. |
39
41
  | `src/role-archetypes.ts` | Purpose → weight archetype table (10 sets) + keyword fitting. Exports `ARCHETYPES`, `FALLBACK_ARCHETYPE`, `fitArchetype`, `archetypeById`, and the `Archetype`/`ArchetypeMatch` types. |
40
42
  | `src/agent-file.ts` | Agent `.md` rendering/placement. Exports `renderAgentFile`, `writeAgentFile`, `removeAgentFile`, `isReadOnlyTools`, `userAgentsDir`, `projectAgentsDir`, `READ_ONLY_TOOLS`, `RESERVED_AGENT_NAMES`, `AGENT_NAME_RE`, and the `AgentFileSpec`/`AgentWriteResult`/`AgentRemoveResult` types. |
41
- | `src/agent-architect.ts` | omp's agent-creation architect, run in-process (extension-only). Exports `generateAgentSpec`, `parseAgentSpec`, `extractJsonObject`, `extractAssistantText`, and the `ArchitectOptions` type. Imports `@oh-my-pi/pi-coding-agent` at the package root. |
42
- | `src/benchmark-author.ts` | Benchmark-source authoring for an unknown link: fetch the link, run an in-process architect (`src/prompts/benchmark-source-architect.md`) to propose a declarative source spec, and validate it. Extension-only (imports `@oh-my-pi/pi-coding-agent`). Exports `authorBenchmarkSource`, `parseSourceDeclarationJson`, and the `BenchmarkAuthorOptions` type. |
43
+ | `src/agent-architect.ts` | omp's in-process architects: the agent-creation architect (`/create-agent`, `/agents` hub) and the project-profile architect (`/project-roles`). Exports `generateAgentSpec`, `generateProjectProfile`, `parseAgentSpec`, `parseProjectProfile`, `extractJsonObject`, `extractAssistantText`, and the `ArchitectOptions`/`ProjectArchitectOptions` types. Imports `@oh-my-pi/pi-coding-agent` **dynamically** (inside the runner), so the module is importable under plain Node. |
44
+ | `src/benchmark-author.ts` | Benchmark-source authoring for an unknown link: fetch the link, run an in-process architect (`src/prompts/benchmark-source-architect.md`) to propose a declarative source spec, and validate it. Imports `@oh-my-pi/pi-coding-agent` **dynamically** (inside the function). Exports `authorBenchmarkSource`, `parseSourceDeclarationJson`, and the `BenchmarkAuthorOptions` type. |
43
45
  | `src/benchmark-discovery.ts` | Judge-backed benchmark relevance for `/create-agent` discovery: one `noul` (yes/no probability) question per catalog candidate, batched into a single judgment through omp's configured `judge` role (TypeSafe jev or its fallback chain). Extension-only (imports `@oh-my-pi/pi-coding-agent` and its `./judgment` subpath — the deeper `./judgment/standalone` path does not resolve under omp's SDK injection). The SDK is imported dynamically so a resolution failure degrades to "no discovery" instead of failing the whole extension load. Exports `judgeBenchmarkRelevance`. |
44
- | `src/agent-create.ts` | `/create-agent` core: purpose → archetype → validated role → agent `.md`, plus the shared input parser and report formatter both hosts use. Exports `createAgent`, `parseCreateAgentInput` (flag form or free text), `parseCreateAgentArgs`, `tokenizeArgs`, `extractBenchmarks`, `extractBenchmarkLinks`, `applyFocusBenchmarks`, `discoverBenchmarks`, `formatBenchmarks`, `formatArchetypes`, `formatCreateAgentReport`, `CREATE_AGENT_USAGE`, and the request/result types. |
46
+ | `src/agent-create.ts` | `/create-agent` core: purpose → archetype → validated role → agent `.md`, plus the shared input parser and report formatter both hosts use. Exports `createAgent`, `resolveRole`, `parseCreateAgentInput` (flag form or free text), `parseCreateAgentArgs`, `tokenizeArgs`, `extractBenchmarks`, `extractBenchmarkLinks`, `applyFocusBenchmarks`, `assessFocusMetric`, `belowBarReason`, `focusCoverageOk`, `countMetricCoverage`, `discoverBenchmarks`, `checkWeightMath`, `formatBenchmarks`, `formatArchetypes`, `formatCreateAgentReport`, `CREATE_AGENT_USAGE`, and the request/result types (incl. `FocusMetricAssessment`/`FocusCoverageEntry`/`FocusAssessor`). |
45
47
  | `src/agent-remove.ts` | `/remove-agent` core: delete an agent `.md` and its role, plus the shared flag parser and report formatter. Exports `removeAgent`, `parseRemoveAgentArgs`, `formatRemoveAgentReport`, `REMOVE_AGENT_USAGE`, and the request/result types. Refuses shipped default roles. |
48
+ | `src/project-setup.ts` | `/project-roles` core: a discovered `ProjectProfile` → a project-scoped role set (kept shipped roles, dropped ones, new fitted roles) written to the project plugin settings lock file, plus the project agents for the new roles. All-or-nothing (every check — names, metrics, weight math, the `--force` gate, agent-file collisions — before any write), `--dry-run`. Exports `setupProject`, `parseProjectRolesArgs`, `parseRolesSpec`, `applyProfileOverrides`, `formatProjectRolesReport`, `PROJECT_ROLES_USAGE`, and the `ProjectProfile`/`ProposedRole`/`SetupProjectOpts`/`SetupProjectResult` types (the result carries an `archetypes` map). The plan report prints the capability flags each new role will author (`features: <id>=<on>`). Dual-runtime safe (no SDK import); the extension command does the LLM work and calls it. |
46
49
 
47
50
  ### `src/explorer/`
48
51
 
49
52
  | File | Role / key exports |
50
53
  |---|---|
51
54
  | `src/explorer/boot.ts` | Shared explorer launcher: bind/port fallback, lock-file roles, browser open, close. Exports `startExplorer`, `EXPLORER_DEFAULT_PORT`, and the `ExplorerHandle`/`ExplorerBootOpts` types. Used by `/explore-roles`. |
52
- | `src/explorer/server.ts` | Zero-dependency HTTP surface (static SPA + JSON API). Exports `createExplorerServer` and the `ExplorerOpts` type. Endpoints: `GET /api/bootstrap`, `POST /api/rank`, `POST /api/explain`, `POST /api/export`, `POST /api/refresh`. |
53
- | `src/explorer/explain.ts` | Pure explanation layer: rank rows with baseline deltas, per-model decomposition, inverse-cardinal targets. Exports `rankRows`, `explainModel`, `inverseCardinal`, `METRIC_META`, `metricMeta`, `metricMetaFor`, `weightableMetrics`, and the `RankRow`/`Explanation`/`Contribution`/`Closing`/`MetricMeta` types. |
55
+ | `src/explorer/server.ts` | Zero-dependency HTTP surface (static SPA + JSON API). Exports `createExplorerServer` and the `ExplorerOpts` type. Endpoints: `GET /api/bootstrap` (carries `focusAssessments`), `POST /api/rank`, `POST /api/explain`, `POST /api/export`, `POST /api/refresh`. |
56
+ | `src/explorer/explain.ts` | Pure explanation layer: rank rows with baseline deltas, per-model decomposition, inverse-cardinal targets, and the nine-axis focus assessment. Exports `rankRows`, `explainModel`, `inverseCardinal`, `focusMetricsOf`, `focusAssessments`, `METRIC_META`, `metricMeta`, `metricMetaFor`, `weightableMetrics`, and the `RankRow`/`Explanation`/`Contribution`/`Closing`/`MetricMeta` types. |
54
57
 
55
58
  ### Assets and non-code
56
59
 
@@ -58,6 +61,7 @@ the extension's `/refresh-roles`, `/explore-roles` and `/create-agent`.
58
61
  |---|---|
59
62
  | `src/prompts/agent-creation-architect.md`, `src/prompts/agent-creation-user.md` | omp's architect prompts, shipped verbatim; read by `src/agent-architect.ts` via `new URL("./prompts/…", import.meta.url)`. |
60
63
  | `src/prompts/benchmark-source-architect.md`, `src/prompts/benchmark-source-user.md` | The benchmark-source architect prompts; read by `src/benchmark-author.ts`. |
64
+ | `src/prompts/project-profile-architect.md`, `src/prompts/project-profile-user.md` | The `/project-roles` profile-discovery architect prompts; read by `src/agent-architect.ts`. |
61
65
  | `web/index.html`, `web/app.js`, `web/style.css` | Explorer SPA — no framework, no build step, no external requests. Served by `src/explorer/server.ts`. |
62
66
  | `agents/designer.md` | The shipped `designer` subagent, discovered from the plugin's extension root (`<ext>/agents/*.md`). Opt-in: kept in `task.disabledAgents` until `roles.designer.enabled=true`. |
63
67
  | `skills/omp-llm-role-create-agent/SKILL.md` | Shipped skill, discovered from the plugin's `skills/` root. Hand-driven equivalent of `/create-agent`. |
@@ -70,20 +74,35 @@ the extension's `/refresh-roles`, `/explore-roles` and `/create-agent`.
70
74
  | `skills-lock.json` | Lock for the local `.agents/skills/` collection. |
71
75
  | `.agents/skills/` | Local dev-skill collection (Matt Pocock skills); not part of the plugin's shipped surface. |
72
76
  | `docs/agents/domain.md`, `docs/agents/issue-tracker.md`, `docs/agents/triage-labels.md` | Repo process docs (domain glossary, issue tracker, triage labels). |
73
- | `docs/dev/spec.md` | Normative spec (moved from the repo root). |
77
+ | `docs/dev/testing.md` | The test-suite seam map and the live checks. |
74
78
  | `docs/dev/architecture.md`, `docs/dev/data-sources.md` | This file and the data-source map. |
75
- | `tests/` | `node --test tests/` fixtures (tier gate, variant resolution, config edit, hysteresis, chain pruning, chain suffixes, explorer, role creation, writing metric, benchmark sources, thinking-price, openrouter-blend, agent disable, agent pins, settings schema, session model, role lock, role enable, probe gate, create-agent, create-role, **remove-agent**). |
79
+ | `tests/` | `node --test tests/` fixtures — the seam map is in [`testing.md`](testing.md). |
76
80
  | `docs/llm-role-rankings.md` | Generated report (regenerate with `--out`); the README's worked example. |
77
81
  | `cache/*.json` | Daily UTC caches (gitignored; see [`data-sources.md`](data-sources.md)). |
78
82
 
83
+ ## Variant resolution (ranking id → selector)
84
+
85
+ A ranking row (an llm-stats bare id) resolves to an `openrouter/*` catalog id by
86
+ matching the id after the last `/` (org prefix — including `~`-prefixed —
87
+ ignored), or after stripping a trailing `-latest`. Among the matches,
88
+ `resolveVariant` emits in order: **exact id** (bare or dated, whatever equals the
89
+ ranking id) → **newest dated id** (max trailing `-MMDD`/date suffix) → **bare
90
+ id** → `~org/…-latest` alias (last resort — a valid selector beats no update).
91
+ Never `:batch`; `:free` only on the free-tier branch of the tier gate. Ties
92
+ break lexicographically. The emitted selector is always `openrouter/<catalogId>`.
93
+
79
94
  ## Data flow
80
95
 
81
96
  One pipeline, driven by `runUpdater(trigger, deps, opts)` in `src/updater.ts`.
82
97
  The omp extension injects the production `Deps`; tests inject fakes.
83
98
 
84
99
  1. **Trigger.** `session_start` (awaited, day-gated) or `/refresh-roles`
85
- (forced). `runUpdater` loads `loadState()` and the raw
86
- settings (`deps.getSettings?.() ?? readPluginSettingsMap()`), then
100
+ (forced). `runUpdater` resolves the **project scope** first: when
101
+ `<cwd>/.omp/plugins/omp-plugins.lock.json` carries an `omp-llm-role` settings
102
+ entry, the config, the settings read, and the state/history/lock are scoped to
103
+ `<cwd>/.omp` (the global `~/.omp/agent/config.yml` is never written in project
104
+ mode); otherwise the global agent dir is used. It then loads `loadState()` and
105
+ the raw settings (`deps.getSettings?.() ?? readPluginSettingsMap()`), then
87
106
  `resolveSettings(raw)`; any validation error aborts with no write.
88
107
  2. **Agent-disable sync.** `agentDisablePatch` computes the
89
108
  `task.disabledAgents` adds/removes from `roleUniverse` + `discoverAgentPins`,
@@ -98,10 +117,28 @@ The omp extension injects the production `Deps`; tests inject fakes.
98
117
  fetchKeyMeta(token)` → `tierGate` → `"billed" | "free" | "none"`; `"none"`
99
118
  aborts.
100
119
  5. **Catalog + probe.** `deps.getCatalog()` → `filterCatalog(catalog, tier)`;
101
- `enrichThinkingLevels(rank.models, catalog)`. Per role, candidates are
120
+ `enrichThinkingLevels(rank.models, catalog)`.
121
+ **Whitelist prune.** The updater then harvests the account's
122
+ allowed-providers privacy whitelist once per run
123
+ (`deps.getAllowedProviders ? deps.getAllowedProviders(token, canaryModelId)
124
+ : fetchAllowedProviders(token, canaryModelId)`, a canary 404 that bills no
125
+ tokens) and, when it is non-null, replaces every model's route pool with the
126
+ routes whose `providerSlug` matches a whitelist entry (`providerAllowed`,
127
+ prefix match `slug === entry || slug.startsWith(entry + "/")`,
128
+ non-mutating). The prune is
129
+ the single change point — `roleRoute`, `bestRoute`, `rankedRoutes` and
130
+ `endpointFilteredModel` all read `m.routes` — so the soft basis, the auto-pin
131
+ and the fallback chains all become whitelist-aware. A `null` whitelist (no
132
+ whitelist set, harvest failed) leaves every pool untouched. The run notes
133
+ `OpenRouter allowed-providers whitelist active — N providers; M routes pruned`
134
+ (or `… whitelist: no whitelist`).
135
+ Per role, candidates are
102
136
  `resolveVariant`-resolved and then verified by a bounded `probeModel` walk
103
137
  (current selector first, then rank order; budget `PROBE_BUDGET = 12`; verdicts
104
- cached per run). Blocked candidates are excluded and recorded on the decision.
138
+ cached per run). Only the narrow no-allowed-providers 404 disqualifies; every
139
+ other failure (5xx, timeout, unknown model) counts as usable and stays in
140
+ omp's runtime-fallback domain. Blocked candidates are excluded and recorded on
141
+ the decision.
105
142
  6. **Hysteresis.** `no-current` → adopt; ineligible current → `adopted`; best
106
143
  beats current by `switchMargin` → `switched`; inside the margin but
107
144
  `cheaperInsideMargin` → `switched-cost`; else `kept-margin` / `kept-eligible`.
@@ -154,10 +191,16 @@ Rules every module must satisfy:
154
191
  `undefined` at runtime, not a compile error. Run the script after edits and
155
192
  sanity-check stderr match/eligible counts.
156
193
 
157
- The one deliberate exception is `src/agent-architect.ts`: it imports
158
- `@oh-my-pi/pi-coding-agent` (the **package root** only — subpath imports do not
159
- resolve in the compiled binary) and is therefore **extension-only**. The tests
160
- import `src/agent-create.ts`, which never imports the architect.
194
+ The one deliberate exception is the SDK import in `src/agent-architect.ts` and
195
+ `src/benchmark-author.ts`: they import `@oh-my-pi/pi-coding-agent` (the
196
+ **package root** only — subpath imports do not resolve in the compiled binary)
197
+ **dynamically, inside the runner function**. A static import would be fatal to
198
+ the whole `extension.ts` load (it would kill every plugin command, not just the
199
+ architect); dynamic, the modules — and `src/extension.ts` — stay importable
200
+ under plain Node (the command registration is testable) and a resolution failure
201
+ degrades to the caller's error path. The architect is still **extension-only in
202
+ practice**: it is only called from the extension. The tests import
203
+ `src/agent-create.ts`, which never imports the architect.
161
204
 
162
205
  ## Injected `Deps`
163
206
 
@@ -175,6 +218,7 @@ type Deps = {
175
218
  getSettings?(): Promise<Record<string, unknown>>;
176
219
  getKeyMeta?(token: string): Promise<KeyMeta>;
177
220
  probeModel?(token: string, catalogId: string): Promise<ProbeVerdict>;
221
+ getAllowedProviders?(token: string, modelId: string): Promise<Set<string> | null>;
178
222
  // Host coupling (extension only): apply a concrete selector to the live session model.
179
223
  applySessionModel?(selector: string, previous: string | null): Promise<void>;
180
224
  };
@@ -193,7 +237,12 @@ before `catalogFromOmpModelsJson`.
193
237
  (`runUpdater("manual", …, { force: true })`), `/explore-roles` (in-process
194
238
  `startExplorer`), `/create-agent` (architect → `createAgent` → in-process
195
239
  `runUpdater`), `/remove-agent` (`removeAgent` → in-process `runUpdater`),
196
- `session_shutdown` (closes the explorer handle).
240
+ `/project-roles` (profile architect → `setupProject` → in-process
241
+ `runUpdater`), `session_shutdown` (closes the explorer handle). The provider
242
+ display adds four event subscriptions — `session_start`,
243
+ `retry_fallback_applied`, `retry_fallback_succeeded` and `turn_start` — each
244
+ applying the live model's provider pin to its `name` (see
245
+ [`quirks.md`](quirks.md) for why `turn_start` is the self-healing hook).
197
246
  - **CLI**: `src/cli/llm-role-rank.ts` (report), `src/cli/create-role.ts`
198
247
  (authoring). The updater and explorer have no CLI shim — they run through
199
248
  `/refresh-roles` and `/explore-roles`.
@@ -202,12 +251,18 @@ before `catalogFromOmpModelsJson`.
202
251
 
203
252
  `src/engine.ts` is the shared engine; `src/cli/llm-role-rank.ts` is a thin CLI over it.
204
253
 
205
- - `loadRankData(opts?: { refresh?; url?; roles? }): Promise<RankData>` — the
206
- per-source cache → fetch → stale-cache chain. `opts.roles` names the roles the
207
- caller will rank: a role-exclusive source is fetched only when some role
254
+ - `loadRankData(opts?: { refresh?; url?; roles?; extraMetrics? }): Promise<RankData>`
255
+ — the per-source cache → fetch → stale-cache chain. `opts.roles` names the roles
256
+ the caller will rank: a role-exclusive source is fetched only when some role
208
257
  weights its metric — Design Arena (`website`), the writing leaderboard
209
258
  (`writing`), and every external metric (a declared source or the generic
210
- llm-stats benchmark). `undefined` = fetch all (what the explorer passes).
259
+ llm-stats benchmark). `opts.extraMetrics` adds metrics no role weights and also
260
+ forces the `website`/`writing` fetches. `undefined` roles = fetch every declared
261
+ source. The explorer passes the resolved roles **plus** the shipped keys
262
+ (`Object.keys(KNOWN_METRICS)`), so its dataset is a superset of any def the UI
263
+ can rank — without it a role weighting an undeclared generic benchmark
264
+ (`bench:<id>`) would rank on a dataset missing that metric and disagree with the
265
+ updater.
211
266
  - `src/benchmark-sources.ts` is the registry: `resolveBenchmarkSource(link)` maps
212
267
  a link to a source, `sourceForMetric(metric)` maps a metric back, and
213
268
  `loadBenchmarkScores`/`applyBenchmarkScores` are the shared cache chain and
@@ -257,7 +312,8 @@ stderr `openrouter: matched N/<pool> models (throughput), M priced`, the
257
312
  - **One write path per artifact.** Roles go through `writeRoleSettings` (and
258
313
  `removeRoleSettings` for deletion); the config through `patchConfig` +
259
314
  `writeConfigAtomic`; agent files through `writeAgentFile` (and
260
- `removeAgentFile` for deletion). Nothing else writes those files.
315
+ `removeAgentFile` for deletion); the error-report ledger through `writeLedger`
316
+ in `src/error-report.ts` (temp + rename). Nothing else writes those files.
261
317
  - **Surgical config edits.** Only managed role lines, managed chain keys and the
262
318
  plugin-managed `task.disabledAgents` names change; comments, blank lines and
263
319
  unknown keys stay byte-identical. A no-change run writes nothing.
@@ -276,3 +332,25 @@ stderr `openrouter: matched N/<pool> models (throughput), M priced`, the
276
332
  field already sets and prices the effort.
277
333
  - **The engine is the only ranking math.** The explorer never reimplements
278
334
  `value = q − λ·$/M`; it calls `rankRole`/`explainModel`.
335
+
336
+ ## Non-goals
337
+
338
+ - **First-party provider selectors.** The plugin emits `openrouter/*` selectors
339
+ only — the ranking's price/throughput are OpenRouter-derived and the key is an
340
+ OpenRouter key.
341
+ - **Non-OpenRouter scoring sources.**
342
+ - **A `/rollback` command.** `llm-role-state.json` snapshots the previous
343
+ `modelRoles` block as a manual rollback aid; there is no command.
344
+ - **Auto-tuning `switchMargin`.**
345
+ - **A generic benchmark scraper** that auto-detects a payload with no user
346
+ review — the declarative source + authoring step is the mechanism.
347
+ - **A UI for browsing/editing benchmark sources** — the source file is edited by
348
+ hand or by the authoring step; the explorer's role editor gains the external
349
+ metrics already in use.
350
+ - **Changing the cardinal transform classes** — external metrics are normalized
351
+ to 0–1 at parse time.
352
+ - **Re-ranking or re-fetching beyond the daily UTC cache chain.**
353
+
354
+ Weight editing is the explorer's Export, `src/cli/create-role.ts` or
355
+ `/create-agent` (all validate through `resolveSettings`); role/agent removal is
356
+ `/remove-agent` (through `removeRoleSettings`).
package/docs/dev/ci.md ADDED
@@ -0,0 +1,163 @@
1
+ # CI/CD
2
+
3
+ The GitHub Actions pipeline that gates every change and delivers every release.
4
+ The release *policy* — when to bump, SemVer over the public surface, the
5
+ CHANGELOG rules, the three channels — lives in [`releasing.md`](releasing.md);
6
+ this doc owns the machinery.
7
+
8
+ ## The workflow
9
+
10
+ `.github/workflows/ci.yml` — one file, two jobs. One file because the npm
11
+ trusted-publisher registration names the workflow **filename**, so a second file
12
+ would need a second registration.
13
+
14
+ | Job | Runs on | Does |
15
+ |---|---|---|
16
+ | `test` | every `pull_request`, every push to `main`, every `v*` tag push, and `workflow_dispatch` | `npm ci`, then `node --test tests/` |
17
+ | `release` | `v*` tag pushes (and a `workflow_dispatch` against a tag ref) | verifies the tag, publishes to npm over OIDC, creates the GitHub Release |
18
+
19
+ `release` declares `needs: test`, so a red tree cannot be published: the same
20
+ ref must be green first. `test` runs **exactly** the command the local
21
+ pre-commit hook runs (`.githooks/pre-commit`), and `tests/ci.test.ts` fails when
22
+ the two drift apart — "green locally" and "green in CI" cannot diverge.
23
+
24
+ The Node version comes from `.node-version` at the repo root (the exact local
25
+ toolchain; bumping it is a normal commit). `engines` is deliberately not in
26
+ `package.json`: the plugin executes inside omp's runtime, so an `engines` field
27
+ would only produce install warnings for users.
28
+
29
+ `test` is the whole gate — no lint, no format, no typecheck, no coverage. The
30
+ repo runs Node type-stripping without a typecheck (see [`quirks.md`](quirks.md)).
31
+
32
+ ## The tag → publish contract
33
+
34
+ Pushing a `v<version>` tag is the delivery trigger. The `release` job:
35
+
36
+ 1. checks out the tag;
37
+ 2. runs `node scripts/release.ts notes "$GITHUB_REF_NAME"` as the **guard** —
38
+ tag ↔ package version ↔ lockstep ↔ CHANGELOG section — and writes the release
39
+ notes to a file. A mistyped tag, a half-applied lockstep bump or a missing
40
+ CHANGELOG section fails here, *before* anything is published;
41
+ 3. ensures npm ≥ 11.5.1 (the trusted-publishing floor);
42
+ 4. `npm publish` — authenticated by GitHub OIDC, with a provenance attestation
43
+ generated automatically. No `NPM_TOKEN`, no `.npmrc` step, no third-party
44
+ publish action;
45
+ 5. `gh release create <tag> --notes-file <notes>` — the GitHub Release body is
46
+ the CHANGELOG section, never retyped.
47
+
48
+ Because the publish runs from the tagged commit, npm's `gitHead` equals the tag.
49
+
50
+ ### One-time setup: register the trusted publisher
51
+
52
+ Required before the first CD run, and it cannot be done from the repo. On
53
+ npmjs.com → the package → Settings → Trusted publishing → GitHub Actions:
54
+
55
+ - **Organization or user**: `Gott50`
56
+ - **Repository**: `omp-llm-role`
57
+ - **Workflow filename**: `ci.yml` (the filename only, with the extension)
58
+ - **Allowed actions**: tick **direct publishing (`npm publish`)**.
59
+
60
+ That last tick is the trap. `npm stage publish` is always allowed, and a
61
+ configuration created after 2026-09-03 is automatically set to allow *only*
62
+ staged publishing — so a publisher registered today rejects the job's
63
+ `npm publish` **after** the guard has already passed, the worst possible failure
64
+ point. Staged publishing is not a substitute: it needs a separate
65
+ `npm stage approve` with 2FA, which breaks unattended CD. Do **not** tick
66
+ `npm dist-tag`; CD never moves a dist-tag.
67
+
68
+ Floors and constraints (npm's trusted-publishing docs): npm CLI ≥ 11.5.1, Node
69
+ ≥ 22.14.0 (the pinned `.node-version` clears both), and GitHub-**hosted** runners
70
+ only — hence `ubuntu-latest`.
71
+
72
+ ## The release script
73
+
74
+ `scripts/release.ts` — Node builtins only, no dependencies, outside the `files`
75
+ whitelist so it never ships in the tarball. Three subcommands, each with a
76
+ documented exit code:
77
+
78
+ | Command | Exit 0 | Exit 1 |
79
+ |---|---|---|
80
+ | `due` | `## [Unreleased]` has at least one entry under an allowed section | it is empty (message on stdout) |
81
+ | `notes <tag>` | prints **only** the CHANGELOG section body, ready for `--notes-file` | the tag is not `v<semver>`, disagrees with the package version, the lockstep fields disagree, the CHANGELOG has no `## [<version>] - <date>` heading, or that section has no entries (message on stderr) |
82
+ | `cut <patch\|minor\|major> [--dry-run]` | the cut ran | a refusal (message on stderr) |
83
+
84
+ `due` is the agent's branch point: "not owed" is not an error, so a script can
85
+ branch on the code without parsing markdown.
86
+
87
+ ### The lockstep set
88
+
89
+ Exactly four fields, bumped together:
90
+
91
+ - `package.json` `version`
92
+ - `.omp-plugin/marketplace.json` `plugins[0].version`
93
+ - `package-lock.json` `version`
94
+ - `package-lock.json` `packages[""].version`
95
+
96
+ The marketplace catalog's own `metadata.version` is **deliberately excluded** —
97
+ `releasing.md` locks the plugin entry, not the catalog. The two numbers look
98
+ like they should agree; they do not.
99
+
100
+ ### What `cut` does, in order
101
+
102
+ 1. refuse a dirty tree;
103
+ 2. refuse an existing tag;
104
+ 3. refuse an empty `## [Unreleased]`;
105
+ 4. refuse when HEAD is not `origin/main` (so the tag always points at a pushed
106
+ commit and npm's `gitHead` resolves);
107
+ 5. compute the new version from the bump argument;
108
+ 6. write the four version fields;
109
+ 7. rename `## [Unreleased]` to `## [<version>] - <YYYY-MM-DD>` (UTC) and insert
110
+ a fresh empty `## [Unreleased]` above it;
111
+ 8. rewrite `[Unreleased]: …/compare/v<version>...HEAD` and prepend
112
+ `[<version>]: …/releases/tag/v<version>` to the footer link block;
113
+ 9. regenerate `docs/llm-role-rankings.md`
114
+ (`node src/cli/llm-role-rank.ts --all --out docs/llm-role-rankings.md`);
115
+ 10. commit `Release <version>`, staging only the touched paths (never `.`);
116
+ 11. push `main`, create the annotated tag `v<version>`, push the tag.
117
+
118
+ The tag push is the CD trigger. `--dry-run` writes nothing: it prints the target
119
+ version, the file plan and the changelog diff, and skips the network-dependent
120
+ report regeneration with a note.
121
+
122
+ `cut` does not run the suite itself. The commit it makes goes through
123
+ `.githooks/pre-commit` when `core.hooksPath` is wired, and the `release` job
124
+ re-verifies with `needs: test` on the tag — a second definition of the gate
125
+ inside the script would be a second thing to keep in sync.
126
+
127
+ There is no `--force` and no interactive prompt: every refusal is a message plus
128
+ exit 1, so the command is safe to run unattended.
129
+
130
+ ## Verifying the pipeline
131
+
132
+ - **The gate:** push a branch and open a PR, or push to `main`; `gh run watch`
133
+ the `test` job green.
134
+ - **The guard (negative control):** push a tag that disagrees with the package
135
+ version (e.g. `v0.0.1` against `1.1.0`); the `release` job must fail in the
136
+ `notes` guard *before* any publish and leave the registry untouched. Delete
137
+ the tag afterwards.
138
+ - **The publish:** run the release job against an already-cut tag —
139
+ `gh workflow run ci.yml --ref v<version>` — then confirm npm's `gitHead`
140
+ equals the tag commit, `dist-tags.latest` moved, and the GitHub Release body
141
+ is that version's CHANGELOG section. This is also the recovery path for a
142
+ failed publish: the tag exists, so no new version is needed.
143
+
144
+ A successful `npm publish` is **not** immediately visible: npm prints "Your
145
+ package is being processed and may take a few minutes to become available",
146
+ and registry reads can serve the pre-publish packument for several minutes
147
+ (third-party mirrors such as jsDelivr lag longer). The step's log is the
148
+ discriminator — a direct publish prints `publishing to
149
+ https://registry.npmjs.org/ with tag latest`, `Signed provenance statement`
150
+ and `+ <pkg>@<version>`; a staged publish is a separate `npm stage publish`
151
+ and prints none of those.
152
+
153
+ The dispatch resolves the workflow **from the ref**, so the tag's commit must
154
+ contain `.github/workflows/ci.yml`. A tag cut before this workflow existed
155
+ cannot be dispatched (`HTTP 422: Workflow does not have 'workflow_dispatch'
156
+ trigger`) — the recovery path only works for tags cut after it landed. The
157
+ workaround is to repoint the tag at a commit that contains the workflow (done
158
+ once, for `v1.1.0` → `4f78ce2`).
159
+
160
+ `tests/ci.test.ts` asserts the offline half: the workflow's structure (jobs,
161
+ triggers, permissions, the command it runs), the hook/workflow command parity,
162
+ the tag-trigger ↔ CHANGELOG-link agreement, the lockstep fields, and the
163
+ script's exit codes and refusals against fixture repositories.