omp-llm-role 1.0.0 → 1.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/README.md +179 -11
- 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 +84 -27
- package/docs/dev/ci.md +152 -0
- package/docs/dev/current-state.md +327 -24
- package/docs/dev/data-sources.md +202 -38
- package/docs/dev/explorer.md +157 -46
- package/docs/dev/quirks.md +45 -4
- package/docs/dev/releasing.md +129 -17
- package/docs/dev/scoring.md +789 -28
- package/docs/dev/testing.md +206 -0
- package/docs/dev/writes-and-state.md +176 -28
- package/docs/llm-role-rankings.md +141 -140
- package/package.json +754 -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 +104 -6
- 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 +409 -46
- package/src/error-report.ts +562 -0
- package/src/explorer/boot.ts +36 -24
- package/src/explorer/explain.ts +110 -13
- package/src/explorer/scopes.ts +110 -0
- package/src/explorer/server.ts +112 -32
- package/src/extension.ts +407 -34
- package/src/features.ts +103 -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/role-archetypes.ts +12 -2
- package/src/role-settings.ts +22 -0
- package/src/settings.ts +205 -24
- package/src/state.ts +1 -1
- package/src/updater.ts +150 -31
- package/web/app.js +411 -12
- package/web/index.html +1 -0
- package/web/style.css +44 -1
- package/docs/dev/spec.md +0 -633
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
How the plugin is verified: the unit suite (`node --test tests/`, run by the
|
|
4
|
+
pre-commit hook) and the live checks that need a real omp session and a real
|
|
5
|
+
OpenRouter key. Node type-stripping does not typecheck, so a green suite plus a
|
|
6
|
+
real run is the gate — see [`quirks.md`](quirks.md).
|
|
7
|
+
|
|
8
|
+
## Unit seams
|
|
9
|
+
|
|
10
|
+
Each seam names the test file that covers it and the scenarios it must keep
|
|
11
|
+
passing.
|
|
12
|
+
|
|
13
|
+
1. **Engine metrics** (`engine-metrics.test.ts`) — `buildModels` maps every
|
|
14
|
+
leaderboard field into `metrics`; an absent field stays null (0-filled, not
|
|
15
|
+
imputed); every metric has a `METRIC_META` entry with the right kind; the
|
|
16
|
+
affine/identity transforms; a role weighting a new metric ranks it.
|
|
17
|
+
2. **Tier gate** (`tier-gate.test.ts`) — paid+credit → billed-only; free-tier
|
|
18
|
+
key → `:free`-only; zero budget both branches → abort; non-openrouter
|
|
19
|
+
providers filtered.
|
|
20
|
+
3. **Variant resolution** (`variant-resolution.test.ts`) — exact/dated/bare/alias
|
|
21
|
+
ordering; `:batch` never emitted; `:free` only on the free branch; org
|
|
22
|
+
prefixes (incl. `~`) ignored; ties lexicographic.
|
|
23
|
+
4. **Surgical edit** (`config-edit.test.ts`) — commented config, unknown keys,
|
|
24
|
+
missing roles upsert, chain-key replace-in-place (no duplicate YAML keys),
|
|
25
|
+
unchanged file → zero-byte diff and no mtime change; structural surprises
|
|
26
|
+
rejected; the empty-collection spellings omp's own writer emits (a null token
|
|
27
|
+
on the key line, a flow `[]`/`{}` on its own indented line — and an empty
|
|
28
|
+
*parent* block, `task:`/`retry:` + ` {}`) read as empty and are normalized
|
|
29
|
+
into the block form on write, with a prune-only patch creating nothing (issue
|
|
30
|
+
#54); a sibling `retry.*` key neither aborts the chain scan nor swallows an
|
|
31
|
+
appended chain key (issue #55); the patched output parses as real YAML.
|
|
32
|
+
5. **Hysteresis** (`hysteresis.test.ts`) — no-current adopt; ineligible current
|
|
33
|
+
switch; margin-below keep; margin-above switch; `switchMargin: 0` always takes
|
|
34
|
+
best; cost override (`switched-cost`); `priceSwitchFraction: 0` keeps the
|
|
35
|
+
margin in charge; an inside-margin challenger short of the fraction stays
|
|
36
|
+
kept; the day gate.
|
|
37
|
+
6. **Chain pruning** (`chain-pruning.test.ts`) — plugin-written stale keys
|
|
38
|
+
removed; owner-written keys preserved; `writeFallbackChains: false`; a role
|
|
39
|
+
that left the managed set is pruned; the chain prefers candidates at or below
|
|
40
|
+
the chosen price.
|
|
41
|
+
7. **Chain suffixes** (`chain-suffix.test.ts`) — chain values carry the role's
|
|
42
|
+
suffix, the key stays bare; an entry without thinking support gets no suffix;
|
|
43
|
+
two roles on one model share a level-free chain.
|
|
44
|
+
8. **Probe gate** (`probe-gate.test.ts`) — 200 → ok; the no-allowed-providers 404
|
|
45
|
+
→ blocked; any other failure → usable; blocked best → next adopted; blocked
|
|
46
|
+
current → replaced; chains contain only probe-clean candidates; verdicts
|
|
47
|
+
cached across roles; all-blocked → role untouched.
|
|
48
|
+
9. **Keyed catalog** (`keyed-catalog.test.ts`) — key-blocked candidates pruned
|
|
49
|
+
from the walk; only candidates in neither catalog are probed; key-blocked
|
|
50
|
+
never in a chain; no-filter/unavailable degrade to the probe walk.
|
|
51
|
+
10. **Key availability** (`key-availability.test.ts`) — proper subset → active;
|
|
52
|
+
equal sets → no-filter; empty → unavailable; a keyed id absent from public →
|
|
53
|
+
no-filter; aliases collapse; two parallel GETs, one authenticated; never
|
|
54
|
+
throws.
|
|
55
|
+
11. **Endpoint ceilings** (`endpoint-ceilings.test.ts`) — the narrower reads
|
|
56
|
+
context/output/tools, a missing field is null; a failing route drops, a null
|
|
57
|
+
field is kept; a model with no capable route is ineligible; a filtered role
|
|
58
|
+
is priced on the surviving pool; `maxPriceUsdPerM` caps the thinking-adjusted
|
|
59
|
+
price.
|
|
60
|
+
12. **Route pricing (provider pin)** (`route-pricing.test.ts`) — a pinned role
|
|
61
|
+
prices the matching route, not the blend; a tiered pin matches verbatim;
|
|
62
|
+
`maxPriceUsdPerM` caps the route price; throughput is the route p50 with a
|
|
63
|
+
blend fallback; no matching route → ineligible; the `@<slug>` selector and
|
|
64
|
+
chain carry the pin; a pin matching no route leaves the role unchanged.
|
|
65
|
+
13. **Thinking price** (`thinking-price.test.ts`) — bare/off unadjusted; a level
|
|
66
|
+
scales only thinking-capable models; the frontier uses the effective price;
|
|
67
|
+
`resolveSettings` validates thinking and rejects the legacy `suffixes` map;
|
|
68
|
+
catalog `thinking[]` wins over the OR flag; meta levels never gate.
|
|
69
|
+
14. **OpenRouter blend** (`openrouter-blend.test.ts`) — price/throughput are the
|
|
70
|
+
`1/price²`-weighted means over the page routes; flex/priority/degraded/free/
|
|
71
|
+
`:batch` stay out; throughput renormalizes; the find-row fallback; the weight
|
|
72
|
+
follows the input price.
|
|
73
|
+
15. **Writing metric** (`writing.test.ts`) — `parseWritingEvidence` keys finite
|
|
74
|
+
scores and rejects junk; `applyWritingScores` fills uncovered models; a
|
|
75
|
+
covered model ranks above an uncovered one.
|
|
76
|
+
16. **Benchmark sources** (`benchmark-sources.test.ts`) — `resolveBenchmarkSource`
|
|
77
|
+
maps an llm-stats benchmark page, a bare id, the writing leaderboard and a
|
|
78
|
+
Design Arena link, `null` for an unknown host; `normalizeMetricKey` is
|
|
79
|
+
dot-free; `parseBenchmarkPayload` reads the llm-stats `entries[]` and writing
|
|
80
|
+
evidence shapes and rejects junk; `parseBenchmarkPayloadMeta` reads the
|
|
81
|
+
per-entry `self_reported`/`verified` flags (19/20 → `trust`, no flag → `null`,
|
|
82
|
+
no entries shape → `null`); `parseBenchmarkCatalog` carries `isCommunity`,
|
|
83
|
+
`modality` and the maintenance fields (`updatedAt`/`versionCount`/
|
|
84
|
+
`latestVersionRowCount`/`starCount`, each `null` when missing or non-finite);
|
|
85
|
+
the catalog cache rejects a pre-`isCommunity`/`modality`/maintenance cache
|
|
86
|
+
(and an empty one); the scores cache rejects a cache lacking the `meta` key
|
|
87
|
+
(and accepts `meta: null`);
|
|
88
|
+
`applyBenchmarkScores` fills uncovered
|
|
89
|
+
models; `loadBenchmarkScores` follows fresh → live → stale with a temp cache
|
|
90
|
+
dir and an injected fetch; a declaration executes (payload path, id/score
|
|
91
|
+
fields, `scoreMax`, join) and a metric colliding with a shipped key is
|
|
92
|
+
rejected; `dryRunDeclaration` reports coverage/leader and rejects a zero-join;
|
|
93
|
+
`extractBenchmarkLinks` pulls URLs from prose.
|
|
94
|
+
17. **Agent creation** (`create-agent.test.ts`) — a purpose fits the expected
|
|
95
|
+
archetype; a `--weights` set violating Σ(non-price) = 1 − w_price is refused;
|
|
96
|
+
an existing agent file without `--force` is refused and writes no role;
|
|
97
|
+
`--dry-run` writes neither file; an architect `spec` replaces the
|
|
98
|
+
description/body; `applyFocusBenchmarks` gives a named benchmark a decisive
|
|
99
|
+
share, is a no-op for the archetype's own specialist set, and keeps both
|
|
100
|
+
invariants; the nine-axis gate (the maintenance, provenance and cross-source
|
|
101
|
+
axes are annotations, never a drop).
|
|
102
|
+
18. **Agent removal** (`remove-agent.test.ts`) — the role's lock-file keys and the
|
|
103
|
+
agent file are deleted (backup written); a shipped default role, a reserved
|
|
104
|
+
name and an invalid name are refused; nothing-to-remove errors; `--dry-run`
|
|
105
|
+
writes nothing; the updater drops `modelRoles.<n>` and the plugin-managed
|
|
106
|
+
`task.disabledAgents` entry.
|
|
107
|
+
19. **Agent pins / disable** (`agent-pins.test.ts`, `agent-disable.test.ts`) —
|
|
108
|
+
`parseAgentPin` takes the first `@role`; `discoverAgentPins` scans the three
|
|
109
|
+
scopes, most specific wins; the sync is not day-gated; an unrelated entry
|
|
110
|
+
keeps its position; an unknown role is never added.
|
|
111
|
+
20. **Role enable / lock** (`role-enable.test.ts`, `role-lock.test.ts`) —
|
|
112
|
+
disabling drops the role from the resolved set and its `modelRoles` line; a
|
|
113
|
+
locked role is not switched and its chain survives the prune; unlocking
|
|
114
|
+
restores the switch.
|
|
115
|
+
21. **Settings** (`settings.test.ts`, `settings-schema.test.ts`) — opt-in roles;
|
|
116
|
+
`priceSwitchFraction` range; flat/nested merge; `required` as string or
|
|
117
|
+
array; an external metric key is weightable, a dotted one rejected;
|
|
118
|
+
`package.json` `omp.settings` deep-equals `deriveSettingsSchema()`.
|
|
119
|
+
22. **Explorer** (`explorer.test.ts`, `explorer-scopes.test.ts`) — rank deltas;
|
|
120
|
+
contributions sum to `q`; `inverseCardinal` round-trips; Export preserves
|
|
121
|
+
siblings and normalizes nested roles; the scope switcher; the availability
|
|
122
|
+
overlay.
|
|
123
|
+
23. **Project scope** (`project-updater.test.ts`, `project-setup.test.ts`,
|
|
124
|
+
`project-roles-command.test.ts`) — project mode scopes config/state/history/
|
|
125
|
+
lock to `<cwd>/.omp`; the day gate is per scope; the registry write;
|
|
126
|
+
`setupProject` add/drop/keep, `--dry-run`, `--force`; the command is
|
|
127
|
+
registered.
|
|
128
|
+
24. **Session model** (`session-model.test.ts`) — the hook fires only after a real
|
|
129
|
+
write on a real `default` change; a dry run and a kept default skip it; a
|
|
130
|
+
throwing hook is contained.
|
|
131
|
+
25. **Docs** (`docs.test.ts`) — every relative markdown link resolves; no
|
|
132
|
+
reference to a moved doc; the CHANGELOG shape; `package.json` `files` entries
|
|
133
|
+
exist; the README has no dev-only headings.
|
|
134
|
+
26. **Architect provenance** (`architect-provenance.test.ts`) — the vendored
|
|
135
|
+
prompt header matches `ARCHITECT_PROMPT_VERSION` and the version named in
|
|
136
|
+
[`agent-authoring.md`](agent-authoring.md).
|
|
137
|
+
27. **Architect retry** (`agent-architect.test.ts`) — a malformed architect
|
|
138
|
+
response is retried once and the parsed spec returned; an always-malformed
|
|
139
|
+
run throws carrying the raw output; a `run()` failure propagates with no
|
|
140
|
+
retry.
|
|
141
|
+
28. **Error reporting** (`error-report.test.ts`) — `reportUnexpectedError`
|
|
142
|
+
through its one entry point with fully injected deps: `off` → no fetch, no
|
|
143
|
+
ledger write, `action: "off"`; `auto` + token → exactly one create whose body
|
|
144
|
+
carries the fingerprint marker, plugin version, runtime, platform, label and
|
|
145
|
+
stack, with the `bug`/`needs-triage` labels; redaction (home dir → `~`,
|
|
146
|
+
`node_modules` prefix → `<pkg>/`, `sk-…`/`ghp_…`/`Bearer …` → `<redacted>`)
|
|
147
|
+
leaves nothing token-shaped in the title, body or URL; dedupe (an open match
|
|
148
|
+
is commented on, never a second create; a closed match on a later day
|
|
149
|
+
creates a fresh issue linking the old one); caps (the 4th distinct
|
|
150
|
+
fingerprint in one UTC day, and a fingerprint already created earlier the
|
|
151
|
+
same day even when its issue is closed, are both `suppressed` with no
|
|
152
|
+
request; a later UTC day re-creates the regression); permission fall-through
|
|
153
|
+
(`401`/`403`/`404` from the search or the create → `offered`, the creation
|
|
154
|
+
counters untouched); no token → no HTTP at all, an `issues/new` URL whose
|
|
155
|
+
query carries the title, body and labels opened exactly once; token
|
|
156
|
+
resolution (`GITHUB_TOKEN` → `GH_TOKEN` → the injected `gh` executor, with a
|
|
157
|
+
throwing or empty `gh` falling through to the URL path); containment (a
|
|
158
|
+
rejected fetch, a 500, a timeout and a malformed JSON body each → `failed`,
|
|
159
|
+
one notify line, no throw); fingerprint stability (a varying id inside the
|
|
160
|
+
message with the same first stack frame dedupes, a different label does not);
|
|
161
|
+
`ask` (a `confirm(true)` surfaces the payload and files exactly once, a
|
|
162
|
+
`confirm(false)` or a headless session sends nothing and offers the URL — it
|
|
163
|
+
never silently falls back to `auto`); an unrecognized `errorReporting` value
|
|
164
|
+
fails closed (no fetch, no ledger, `action: "off"`); a defect **string** (as
|
|
165
|
+
`reportDefect` passes it) is fingerprinted by its message, so two distinct
|
|
166
|
+
aborts under one label differ; the in-process guard; and a ledger that
|
|
167
|
+
tolerates junk and truncates a pathological stack — including a multi-byte
|
|
168
|
+
one — to the 64 KB **byte** budget.
|
|
169
|
+
29. **CI/CD** (`ci.test.ts`) — the workflow parses and declares both jobs with
|
|
170
|
+
`release` needing `test`; the `test` job's command is identical to the
|
|
171
|
+
pre-commit hook's; the tag trigger covers every CHANGELOG tag link and the
|
|
172
|
+
package version; `.node-version` is the file the workflow reads; the release
|
|
173
|
+
job declares `id-token: write` and references no npm token; the four
|
|
174
|
+
lockstep version fields agree; no tracked file lives under `cache/`; and
|
|
175
|
+
`scripts/release.ts` against fixture repos — `due`'s exit codes, `notes`'s
|
|
176
|
+
five refusals and its exact section body, `cut --dry-run` writing nothing,
|
|
177
|
+
and `cut` bumping the four fields, cutting the changelog, committing and
|
|
178
|
+
tagging while refusing a dirty tree, an existing tag, an empty
|
|
179
|
+
`## [Unreleased]` and a HEAD that is not `origin/main`.
|
|
180
|
+
|
|
181
|
+
## Live checks
|
|
182
|
+
|
|
183
|
+
- **E2E refresh** — `omp plugin link .` → new omp session → observe the day-gated
|
|
184
|
+
run; `/refresh-roles` → verify the `config.yml` diff + notification; a second
|
|
185
|
+
same-day session → no-op; a forced `/refresh-roles` matches the in-session
|
|
186
|
+
decisions; `/explore-roles --no-open` → `curl` the notified port for
|
|
187
|
+
`/api/bootstrap`, a repeat invocation keeps the same port, and the port is
|
|
188
|
+
released when the session ends.
|
|
189
|
+
- **Agent creation** — `/create-agent` in a session (RPC mode dispatches slash
|
|
190
|
+
commands) → the architect authors the body, the updater line
|
|
191
|
+
`@<n>: (unset) -> <selector>` + `modelRoles.<n>` land in `config.yml`, then a
|
|
192
|
+
headless spawn whose record reads
|
|
193
|
+
`{"agent":"<n>","agentSource":"user","modelRole":"<n>"}` with `resolvedModel`
|
|
194
|
+
equal to the role's selector.
|
|
195
|
+
- **Benchmark sources** — a role weighting `bench:<id>` fetches the source and
|
|
196
|
+
ranks on it; the explorer's `/api/bootstrap` lists the external metric with its
|
|
197
|
+
derived metadata.
|
|
198
|
+
- **Agent removal** — `/remove-agent --name <n>` in a session deletes both
|
|
199
|
+
artifacts and the updater pass removes the config entries.
|
|
200
|
+
- **CI/CD** — push a branch and open a PR (or push to `main`) and `gh run watch`
|
|
201
|
+
the `test` job green; push a tag that disagrees with the package version and
|
|
202
|
+
confirm the `release` job fails in the `notes` guard *before* any publish;
|
|
203
|
+
run the release job against an already-cut tag
|
|
204
|
+
(`gh workflow run ci.yml --ref v<version>`) and confirm npm's `gitHead` equals
|
|
205
|
+
the tag commit, `dist-tags.latest` moved, and the GitHub Release body is that
|
|
206
|
+
version's CHANGELOG section. See [`ci.md`](ci.md).
|
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
# Writes and state
|
|
2
2
|
|
|
3
3
|
Maintainer reference for the plugin's write path and the files it owns.
|
|
4
|
-
|
|
5
|
-
`src/
|
|
6
|
-
(orchestration), `src/agent-pins.ts` (the pin-derived sync).
|
|
4
|
+
Implementation: `src/config-edit.ts` (the edit), `src/state.ts` (the files),
|
|
5
|
+
`src/updater.ts` (orchestration), `src/agent-pins.ts` (the pin-derived sync).
|
|
7
6
|
|
|
8
7
|
## The surgical config edit
|
|
9
8
|
|
|
10
9
|
`patchConfig(configText, patch)` (`src/config-edit.ts`) rewrites
|
|
11
10
|
`~/.omp/agent/config.yml` as **text**, line-oriented — no runtime YAML
|
|
12
|
-
dependency (required for marketplace installs).
|
|
13
|
-
|
|
11
|
+
dependency (required for marketplace installs). omp hot-reloads `config.yml`
|
|
12
|
+
(task/eval preflight re-reads the settings) and writes it under its own
|
|
13
|
+
`config.yml.lock`, so the plugin's mtime guard and refresh lock are what keep a
|
|
14
|
+
concurrent omp write from being clobbered. It touches exactly three surfaces:
|
|
14
15
|
|
|
15
16
|
- `modelRoles.<role>` values (upsert missing roles at the block end, two-space
|
|
16
17
|
indent; delete the line for a role that left the managed set);
|
|
17
18
|
- the managed keys inside `retry.fallbackChains` (replace-in-place, never
|
|
18
19
|
duplicate a YAML key; prune keys the plugin wrote on a previous run and no
|
|
19
|
-
longer references)
|
|
20
|
+
longer references). A pinned role's chain key is the level-free form of its
|
|
21
|
+
selector and carries the `@<slug>` pin (`openrouter/<id>@<slug>`); the pin
|
|
22
|
+
rides before the thinking suffix on the values;
|
|
20
23
|
- the plugin-managed names in `task.disabledAgents` (add/remove, other entries
|
|
21
24
|
and their order preserved).
|
|
22
25
|
|
|
@@ -36,16 +39,40 @@ Rules the algorithm enforces:
|
|
|
36
39
|
documents; `findTopLevel` rejects inline values, indented-only blocks and
|
|
37
40
|
duplicates; `chainEntries` rejects odd indents, duplicate chain keys and
|
|
38
41
|
unexpected lines. Any surprise throws `ConfigEditError` and the caller aborts
|
|
39
|
-
without writing.
|
|
42
|
+
without writing. **Exception — an empty managed collection is not a
|
|
43
|
+
surprise.** omp re-serializes `config.yml` through its own writer (Bun's
|
|
44
|
+
`YAML.stringify` via `stringifyYamlConfig`), which spells an empty collection
|
|
45
|
+
either as a null token on the key line (`task.disabledAgents: null`) or as a
|
|
46
|
+
flow collection on its own indented line (`fallbackChains:` then ` {}`,
|
|
47
|
+
`task.disabledAgents:` then ` []`). Both read as the empty collection, and
|
|
48
|
+
the patch path rewrites that placeholder line into the block form
|
|
49
|
+
(`disabledAgents:` + `- "designer"`) instead of refusing the run — a user's
|
|
50
|
+
`disabledAgents: null` used to abort every session start with `config edit
|
|
51
|
+
refused` (issue #54). Any *other* inline value on a managed key line
|
|
52
|
+
(`disabledAgents: ["x"]`) still throws. The rule applies at both levels: a
|
|
53
|
+
managed key's own placeholder (`task.disabledAgents:` + ` []`) **and** an
|
|
54
|
+
empty **parent** block (`task:` + ` {}`, `retry:` + ` {}`), where the new
|
|
55
|
+
child key replaces the `{}` line — inserting *after* it emits YAML no parser
|
|
56
|
+
accepts, while the read-back self-check still passes, so that corruption was
|
|
57
|
+
silent. A prune-only patch creates no key at all.
|
|
58
|
+
- **Blocks are bounded by their own indent.** `blockEnd(lines, key, indent)`
|
|
59
|
+
stops at the first line at or above the key's indent, so `chainEntries` sees
|
|
60
|
+
only the `fallbackChains` block. A sibling `retry.<other>` key — omp ships ten
|
|
61
|
+
(`retry.maxRetries`, `retry.enabled`, `retry.baseDelayMs`, …) and writes them
|
|
62
|
+
into the same `retry:` block — therefore ends the block instead of throwing
|
|
63
|
+
`unexpected line in fallbackChains block`, and a new chain key is appended
|
|
64
|
+
inside the block rather than after it.
|
|
40
65
|
- **Read-back self-check.** After patching, `patchConfig` re-reads the patched
|
|
41
66
|
text through the same line-oriented reader and asserts it equals the intended
|
|
42
67
|
state (every upserted role, every removal, the exact chain-key set, every
|
|
43
68
|
disabled-agent add/remove). On any mismatch it throws — never corrupt.
|
|
44
69
|
- **Atomic write.** `writeConfigAtomic(path, patchedText, mtimeBefore)` re-stats
|
|
45
70
|
the mtime first; a changed mtime means the config moved underneath and returns
|
|
46
|
-
`"conflict"` (the caller re-reads and recomputes). Otherwise it
|
|
47
|
-
`<path>.llm-role-tmp` and `rename`s over the target.
|
|
48
|
-
time (`mtimeBefore === 0`) is writable only while still
|
|
71
|
+
`"conflict"` (the caller re-reads and recomputes). Otherwise it `mkdir -p`s the
|
|
72
|
+
target's directory, writes `<path>.llm-role-tmp` and `rename`s over the target.
|
|
73
|
+
A file absent at read time (`mtimeBefore === 0`) is writable only while still
|
|
74
|
+
absent. The `mkdir -p` is what lets the project scope write into
|
|
75
|
+
`<cwd>/.omp/plugins/`, which omp does not create until something writes there.
|
|
49
76
|
- **3-attempt mtime retry.** `writePatch` (`src/updater.ts`) loops up to
|
|
50
77
|
`CONFLICT_RETRIES = 3`: read → patch → atomic write; a `"conflict"` retries,
|
|
51
78
|
three conflicts abort with `CONFLICT_ABORT`.
|
|
@@ -53,31 +80,135 @@ Rules the algorithm enforces:
|
|
|
53
80
|
(below); a lock it cannot acquire returns `"locked"` and the caller aborts
|
|
54
81
|
(or no-ops, for the day-gated sync).
|
|
55
82
|
|
|
83
|
+
## The settings lock file and the feature flags
|
|
84
|
+
|
|
85
|
+
The plugin's settings live in `~/.omp/plugins/omp-plugins.lock.json` →
|
|
86
|
+
`settings["omp-llm-role"]` (or the project lock in project mode). The write path
|
|
87
|
+
(`src/role-settings.ts`) stores every role key as a **flat dotted key** because
|
|
88
|
+
omp's `/settings` Plugins tab shallow-merges the settings object and does not
|
|
89
|
+
flatten nested objects. The capability flags join that set:
|
|
90
|
+
|
|
91
|
+
- `features.<id>` — the global flag for capability `<id>`;
|
|
92
|
+
- `roles.<role>.features.<id>` — the per-role flag (wins over the global in
|
|
93
|
+
either direction);
|
|
94
|
+
- `roles.<role>.preferOwnProvider` — the `providerPinning` policy field.
|
|
95
|
+
|
|
96
|
+
`mergeExport` writes these as flat dotted keys alongside the existing role keys
|
|
97
|
+
(`roles.<name>.weights.<metric>`, `.description`, `.enabled`, `.locked`,
|
|
98
|
+
`.required`, `.thinking`, `.providerPin`, `.cacheHitRate`, `.lambda`,
|
|
99
|
+
`.filters.image`). `mergeExport` first deletes the role's existing
|
|
100
|
+
`roles.<name>.*` flat dotted keys, so an Export means "make this role's keys
|
|
101
|
+
match this def" — a capability flag set back to inherit (or a cleared knob) is
|
|
102
|
+
removed rather than left stale. `mergeRemove` needs no change: its existing
|
|
103
|
+
`roles.<name>.` prefix already covers the new keys, so `/remove-agent` deletes
|
|
104
|
+
them.
|
|
105
|
+
|
|
106
|
+
**Validation aborts** (notify, no write), in the existing one-error-string-per-
|
|
107
|
+
violation style (`resolveSettings` in `src/settings.ts`):
|
|
108
|
+
|
|
109
|
+
- `features.<id> is not a known capability` / `role <name>: features.<id> is not
|
|
110
|
+
a known capability` — an unknown id, global or per-role;
|
|
111
|
+
- `features.<id>: must be a boolean, got <json>` / `role <name>: features.<id>
|
|
112
|
+
must be a boolean, got <json>` — a non-boolean flag value;
|
|
113
|
+
- `features: must be an object, got <json>` / `role <name>: features must be an
|
|
114
|
+
object` — a non-record `features`;
|
|
115
|
+
- `role <name>: preferOwnProvider must be a boolean, got <json>` — a non-boolean
|
|
116
|
+
policy field.
|
|
117
|
+
|
|
118
|
+
**`mergeRawSettings` does NOT expand.** `resolveSettings` calls
|
|
119
|
+
`expandFeatures` per role, so its `roles` map is the **effective** def (the
|
|
120
|
+
authored def with the enabled flags' recommended knobs filled in, `features`
|
|
121
|
+
stripped). `mergeRawSettings` — and therefore `roleUniverse(...).def` — keeps the
|
|
122
|
+
**authored** def (shipped ⊕ raw overrides, no preset, `features` retained),
|
|
123
|
+
because that is the def the explorer edits and exports. Expanding inside
|
|
124
|
+
`mergeRawSettings` would make a role whose only authored change is a flag read as
|
|
125
|
+
dirty against its own baseline, and `mergeExport` (which writes every key of a
|
|
126
|
+
dirty def) would persist the expanded knobs as explicit values — silently
|
|
127
|
+
converting the flag into hard values and defeating both "adjust by hand" and
|
|
128
|
+
re-applying a changed recommendation. Export writes the **flag**, never the
|
|
129
|
+
expanded values.
|
|
130
|
+
|
|
131
|
+
**Near-collision warning.** omp's plugin-level `enabledFeatures` (present in the
|
|
132
|
+
lock file, `null` by default) is a **different gate** — omp's own feature
|
|
133
|
+
gating — that this plugin never reads (zero occurrences in `src/`, `web/`,
|
|
134
|
+
`tests/`). The plugin's `features.<id>` keys are plugin settings, not omp
|
|
135
|
+
feature flags; do not merge the two.
|
|
136
|
+
|
|
56
137
|
## State, history, lock
|
|
57
138
|
|
|
58
139
|
All under the agent dir (`agentDir()` in `src/state.ts`), next to the config
|
|
59
140
|
they describe. Agent-dir resolution: `OMP_LLM_ROLE_AGENT_DIR` (test hook) →
|
|
60
141
|
`PI_CODING_AGENT_DIR` → non-default `OMP_PROFILE` → `~/.omp/agent`.
|
|
61
142
|
|
|
143
|
+
**Project scope (issue #27).** `runUpdater` resolves the project dir as
|
|
144
|
+
`<cwd>/.omp` (omp's project dir, no walk-up). When
|
|
145
|
+
`<cwd>/.omp/plugins/omp-plugins.lock.json` carries a non-empty `omp-llm-role`
|
|
146
|
+
settings entry, the whole run is scoped to the project: the config
|
|
147
|
+
(`<cwd>/.omp/config.yml`), the settings read (project lock over the user-level
|
|
148
|
+
one), and the state/history/lock files all live under `<cwd>/.omp`. The global
|
|
149
|
+
`~/.omp/agent/config.yml` is **never** written in project mode. Without a project
|
|
150
|
+
settings entry the global behavior is unchanged.
|
|
151
|
+
|
|
62
152
|
| File | Shape | Purpose |
|
|
63
153
|
|---|---|---|
|
|
64
154
|
| `llm-role-state.json` | `{ lastRunDay, managedRoles, roleLastSelector, pluginWrittenChainKeys, previousModelRoles, managedDisabledAgents }` | day gate, managed-role set, last selectors, plugin-owned chain keys, pre-write `modelRoles` snapshot, **managed `task.disabledAgents` names** |
|
|
65
155
|
| `llm-role-history.jsonl` | one JSON row per completed run: `{ ts, trigger, keyMeta{isFreeTier, limitRemaining, creditsRemaining}, decisions[] }` | append-only decision log |
|
|
156
|
+
| `llm-role-projects.json` | `{ projects: [{ root, lastUsed }] }` | the plugin's memory of the projects where `/project-roles` was used (the explorer's scope list) |
|
|
66
157
|
| `.llm-role-refresh.lock` | `O_EXCL` create, holds `"<pid> <iso>"` | serializes concurrent session starts; stale (> 60 s) locks are unlinked and retried once |
|
|
158
|
+
| `llm-role-error-reports.json` | `{ reports: [{ fingerprint, label, title, firstSeen, lastSeen, createdDay?, offeredDay?, count, action, issueNumber?, issueUrl? }], dayCount: { day, count } }` | what the opt-in `errorReporting` path filed, offered or suppressed |
|
|
159
|
+
|
|
160
|
+
- `llm-role-projects.json` (issue #28) is **global knowledge** and lives in the
|
|
161
|
+
agent dir, never a project's `.omp`. `registerProject(root)` (`src/project-registry.ts`)
|
|
162
|
+
upserts the root (stamped now) and prunes entries whose project lock file is
|
|
163
|
+
gone, then replaces the file atomically (temp + rename) so two sessions in
|
|
164
|
+
different repos cannot interleave into a corrupt file. Reading is pure and
|
|
165
|
+
tolerant: a missing or unparseable file yields `[]`. The write point is the
|
|
166
|
+
project-aware `runUpdater`, **immediately after `resolveProjectDir()` and
|
|
167
|
+
before any early return**, guarded only by `dryRun` — so a day-stamped no-op
|
|
168
|
+
session start still registers the project (the whole point of "projects
|
|
169
|
+
configured before this feature appear on their next session start"), while a
|
|
170
|
+
`--dry-run`/cancelled `/project-roles` registers nothing. A write failure is
|
|
171
|
+
logged, never thrown: the registry is a convenience index, not a run
|
|
172
|
+
prerequisite.
|
|
173
|
+
|
|
174
|
+
- `llm-role-error-reports.json` is the ledger of the opt-in error-reporting path
|
|
175
|
+
(`src/error-report.ts`, `ERROR_REPORT_LEDGER_FILE`) — the plugin's memory of
|
|
176
|
+
what it filed, offered or suppressed. Like `llm-role-projects.json` it is
|
|
177
|
+
**global knowledge** and lives in the agent dir, never a project's `.omp`; the
|
|
178
|
+
write point is `writeLedger` (temp file + `rename`, so two sessions cannot
|
|
179
|
+
interleave into a corrupt file) and reading is pure and tolerant — missing,
|
|
180
|
+
unparseable or unexpected-shape content yields an empty ledger, so a corrupt
|
|
181
|
+
ledger never blocks reporting or the run. Each `reports[]` row remembers one
|
|
182
|
+
fingerprint's `action` (`created`/`commented`/`offered`/`suppressed`), its
|
|
183
|
+
`issueNumber`/`issueUrl`, the `createdDay`/`offeredDay` that gate re-creation
|
|
184
|
+
and re-offering, and a `count`; `dayCount` tracks the UTC-day creation total
|
|
185
|
+
that feeds the 3-creations-per-day cap. Nothing is written when
|
|
186
|
+
`errorReporting` is `off`.
|
|
67
187
|
|
|
68
188
|
- `previousModelRoles` is the full snapshot of the last `modelRoles` block
|
|
69
189
|
**before** the plugin changed it — a manual rollback aid; there is no rollback
|
|
70
|
-
command
|
|
190
|
+
command.
|
|
71
191
|
- `pluginWrittenChainKeys` is what makes chain pruning safe: only keys the
|
|
72
192
|
plugin wrote on a previous run are candidates for pruning; an owner-written,
|
|
73
193
|
unreferenced key is left alone.
|
|
74
194
|
- `managedRoles` drives `roleRemovals`: a role the plugin managed on a previous
|
|
75
195
|
run but no longer does (disabled via `enabled: false` / `weights: null`, or
|
|
76
196
|
removed from settings) has its `modelRoles.<role>` line deleted, so a stale
|
|
77
|
-
pin cannot keep routing `@<role>`.
|
|
197
|
+
pin cannot keep routing `@<role>`. A role whose provider pin matched no route
|
|
198
|
+
joins `managedRoles` without a rewrite, so its current chain survives the
|
|
199
|
+
prune.
|
|
78
200
|
- `managedDisabledAgents` is the set of agent names the plugin added to
|
|
79
201
|
`task.disabledAgents`; a later run removes a name whose agent file is gone
|
|
80
202
|
(e.g. `/remove-agent`) or whose role is no longer disabled.
|
|
203
|
+
- Each `decisions[]` entry carries `availabilitySource` (`"keyed-catalog"` when the
|
|
204
|
+
keyed catalog was active for that role, else `"probe"`) and `keyBlockedCount` (the
|
|
205
|
+
candidates pruned as key-blocked). The keyed-blocked set itself is deliberately
|
|
206
|
+
**not** enumerated — it can be hundreds of ids; `blocked[]` keeps its old meaning,
|
|
207
|
+
the probe-blocked ids examined in the walk. `endpointBlocked[]` records the ranking
|
|
208
|
+
ids the role's endpoint filters dropped (no capable standard-tier route), rendered
|
|
209
|
+
`; endpoint-blocked: …` on the decision line; `pinBlocked[]` records the ranking ids
|
|
210
|
+
the role's provider pin dropped (no route matching the pin), rendered
|
|
211
|
+
`; pin-blocked: …`.
|
|
81
212
|
- Role removal (`/remove-agent`) goes through `removeRoleSettings` in
|
|
82
213
|
`src/role-settings.ts` — the same validate/backup/atomic-write path as
|
|
83
214
|
`writeRoleSettings`, deleting the role's flat dotted keys and any nested entry.
|
|
@@ -92,7 +223,8 @@ no-op. `/refresh-roles` passes `force` and always runs.
|
|
|
92
223
|
A concurrent second starter loses the lock and finds the day already stamped →
|
|
93
224
|
no-op. Session-start runs are **awaited** before the first prompt is dispatched
|
|
94
225
|
(a deferred timer would be cleared when a short-lived session exits before it
|
|
95
|
-
fires).
|
|
226
|
+
fires). The gate is **per scope**: a project run loads the project's own
|
|
227
|
+
`llm-role-state.json`, so a project and the global config each refresh once a day.
|
|
96
228
|
|
|
97
229
|
## `task.disabledAgents` sync (pin-derived, not day-gated)
|
|
98
230
|
|
|
@@ -127,27 +259,43 @@ session, not the next day. Two consequences a maintainer must know:
|
|
|
127
259
|
## Error handling
|
|
128
260
|
|
|
129
261
|
`runUpdater` wraps the whole run; `ConfigEditError` and any other throw become
|
|
130
|
-
`omp-llm-role: aborted, no write: <message>`.
|
|
131
|
-
|
|
262
|
+
`omp-llm-role: aborted, no write: <message>`. The aborts split into two classes,
|
|
263
|
+
and the `defect` column below marks which ones `RunResult` flags: an **exception
|
|
264
|
+
abort** (a bug this plugin should fix) carries `defect` and, when the opt-in
|
|
265
|
+
`errorReporting` setting is on, is reported through `src/error-report.ts`; an
|
|
266
|
+
**enumerated environment abort** (an expected condition) leaves `defect` unset
|
|
267
|
+
and is notify-only. Abort paths and what each leaves untouched:
|
|
132
268
|
|
|
133
|
-
| Failure | Behavior | State / config |
|
|
134
|
-
|
|
135
|
-
| Settings validation fails
|
|
136
|
-
| Ranking data unavailable (fetch fails, no fresh cache) | abort, notify | no write, no state change |
|
|
137
|
-
| Key fetch (`omp token`/registry) or `/api/v1/key`, `/api/v1/credits` fails | abort, notify | no write, no state change |
|
|
138
|
-
| Catalog fetch fails | abort, notify | no write, no state change |
|
|
139
|
-
| Both tier branches false (no budget, no free quota) | abort, notify | no write, no state change |
|
|
140
|
-
| Lock not acquired | abort (`LOCK_ABORT`) | no write, no state change |
|
|
141
|
-
| Write conflict after 3 retries | abort (`CONFLICT_ABORT`) | no write, state file untouched |
|
|
142
|
-
| `patchConfig` structural surprise / self-check mismatch | abort (`config edit refused: …`) | no write, no state change |
|
|
143
|
-
|
|
|
144
|
-
|
|
|
269
|
+
| Failure | Behavior | State / config | `defect` |
|
|
270
|
+
|---|---|---|---|
|
|
271
|
+
| Settings validation fails | abort, notify the offending role/key | no write, no state change | — |
|
|
272
|
+
| Ranking data unavailable (fetch fails, no fresh cache) | abort, notify | no write, no state change | — |
|
|
273
|
+
| Key fetch (`omp token`/registry) or `/api/v1/key`, `/api/v1/credits` fails | abort, notify | no write, no state change | — |
|
|
274
|
+
| Catalog fetch fails | abort, notify | no write, no state change | yes |
|
|
275
|
+
| Both tier branches false (no budget, no free quota) | abort, notify | no write, no state change | — |
|
|
276
|
+
| Lock not acquired | abort (`LOCK_ABORT`) | no write, no state change | — |
|
|
277
|
+
| Write conflict after 3 retries | abort (`CONFLICT_ABORT`) | no write, state file untouched | — |
|
|
278
|
+
| `patchConfig` structural surprise / self-check mismatch | abort (`config edit refused: …`) | no write, no state change | yes |
|
|
279
|
+
| Any other throw inside the run (the catch-all) | abort, notify | no write, no state change | yes |
|
|
280
|
+
| Zero decisions changed | no write at all (config mtime untouched) | history row appended (full run) | — |
|
|
281
|
+
| Dry run | decisions reported, nothing written | no day-gate stamp, no history row | — |
|
|
282
|
+
|
|
283
|
+
- **`defect` vs the enumerated aborts.** The enumerated environment aborts —
|
|
284
|
+
settings invalid, ranking data unavailable, tier `none`, lock, write conflict
|
|
285
|
+
and the key-fetch failure — are conditions of the user's machine or account,
|
|
286
|
+
not plugin defects, so `runUpdater` returns them without `defect` and the
|
|
287
|
+
extension does not report them. Only the exception aborts carry `defect` (the
|
|
288
|
+
message), and the extension's `reportDefect` helper routes it to
|
|
289
|
+
`reportUnexpectedError`. A command handler that throws *outside* `runUpdater`
|
|
290
|
+
(the six catch sites) is reported the same way via `reportCommandError`.
|
|
145
291
|
|
|
146
292
|
- The day-gated agent sync's lock/conflict failures are handled separately: a
|
|
147
293
|
lock it cannot acquire is benign (`{ decisions: [], wrote: false }`); a
|
|
148
294
|
conflict aborts with `CONFLICT_ABORT`.
|
|
149
295
|
- A role whose probed candidates are all blocked is left untouched with a notify
|
|
150
|
-
note (not an abort).
|
|
296
|
+
note (not an abort). A role whose provider pin matches no route is likewise
|
|
297
|
+
left untouched — no selector or chain upsert — with a notify note naming the
|
|
298
|
+
role and the pin.
|
|
151
299
|
- The live-session model hook (`applySessionModel`) runs only after a real write
|
|
152
300
|
and only on a real `default` change; a hook failure is notified but never
|
|
153
301
|
fails the run (the config write already succeeded).
|