@dsdenes/devboard 0.1.562 → 0.1.564

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 CHANGED
@@ -159,9 +159,11 @@ issue through `/api/feedback` at once, then replaces itself with the issue URL.
159
159
  never replace mode content or steal focus, and several arrivals are all retained. Flow blockers
160
160
  and verification results also remain in their mode context.
161
161
 
162
- The 26px status bar is two groups. The left group states the WebSocket connection and the
162
+ The 26px status bar is two groups. The left group states the WebSocket connection, the
163
163
  project's own name (the last path segment, with a folder mark and the full path in its tooltip),
164
- separated by a hairline rule. The right group states the running lane count, the issues worker
164
+ and the Providers control: the selected provider with the lowest remaining percentage, or
165
+ `No provider`, which opens the Providers dialog. The right group states the running lane count,
166
+ the issues worker
165
167
  state with a state dot, the watchdog's last action and age, and the running and installed
166
168
  versions; each metric carries a 10px uppercase label so its meaning never rests on color. When
167
169
  `versionState.stale` is true it shows the existing reload control (`reload-devboard`), whose
@@ -220,6 +222,9 @@ npx @dsdenes/devboard update # install the newest npm version; do not restar
220
222
  npx @dsdenes/devboard doctor # one PASS or FAIL line per check
221
223
  ```
222
224
 
225
+ The launcher never discovers, reads, copies, or waits on another local Devboard installation or
226
+ source checkout; it resolves the npm version and artifact for the target host alone.
227
+
223
228
  `install` writes a real npm prefix at `<host>/.devboard`, replacing `.devboard`,
224
229
  `.omp/commands`, and `.omp/extensions` symlinks with real directories that hold the installed
225
230
  package's files. It keeps `.devboard/.tmp`, copies `templates/host-AGENTS.md` to
@@ -254,18 +259,63 @@ The global `/setup-devboard` command runs the launcher against the invoking sess
254
259
  directory. It lives here as `omp/commands/setup-devboard.md` and is linked into the user-level
255
260
  command directory (`$HOME/.omp/agent/commands/setup-devboard.md`) so every session sees it.
256
261
 
257
- ### Model selection
262
+ ### Providers and model selection
258
263
 
259
- `omp/config.yml` is installed as `<host>/.omp/config.yml`, the host's omp project settings.
260
- Every harness peer starts its ACP process with `<REPO_DIR>` as the working directory, so each
261
- one, and the interactive agent started in that repository, uses the same policy:
264
+ Provider authentication is a harness capability, not host settings. Exactly two providers exist:
262
265
 
263
- |Setting|Value|Meaning|
266
+ |Provider|Environment variable|Model|
264
267
  |---|---|---|
265
- |`modelRoles`|`opencode-go/deepseek-v4.1-flash` for `default`, `plan`, `smol`, `slow`, and `task`|Model for every chat role the harness uses.|
266
-
267
- The model is served by OpenCode Go and reads `OPENCODE_API_KEY`. No fallback chain is
268
- configured. Editing `omp/config.yml` here and rerunning `install.sh` changes the installed copy.
268
+ |OpenCode Go|`OPENCODE_API_KEY`|`opencode-go/deepseek-v4.1-flash`|
269
+ |OpenRouter|`OPENROUTER_API_KEY`|`openrouter/deepseek/deepseek-v4.1-flash-1017`|
270
+
271
+ The operator registers keys in the **Providers** dialog, reachable from the status-bar control
272
+ (`data-testid="providers-indicator"`) and the `settings.providers` palette command. The dialog
273
+ supports add, edit, enable/disable, delete with confirmation, refresh, and save. It shows each
274
+ key's masked value, status (`ok`, `near limit`, `exhausted`, `invalid`, `unknown`), every quota
275
+ window's remaining fraction, and the last check time.
276
+
277
+ Credentials live in `<stateDir>/providers/credentials.json` (directory `0700`, file `0600`,
278
+ atomic replacement). The file survives a dashboard restart and is never removed by
279
+ `bun run cleanup` or the watchdog sweep, however old it is. Generated per-peer agent directories
280
+ live beside it under `<stateDir>/providers/agents/<selection-id>/`. No key is ever returned by a
281
+ command or event, written to the journal or activity log, or placed in a process's argv; the
282
+ only value the interface receives is a mask ending in at most the last four characters. The
283
+ durable state file under `<stateDir>/state/` carries no key material.
284
+
285
+ Usage is read from `https://opencode.ai/zen/go/v1/usage` (`rolling`, `weekly`, and `monthly`
286
+ percent used) and `https://openrouter.ai/api/v1/key` (`limit_remaining` and `limit`).
287
+ `DEVBOARD_OPENCODE_GO_USAGE_BASE_URL` and `DEVBOARD_OPENROUTER_USAGE_BASE_URL` override the base
288
+ URLs for tests and offline verification. A key's usage is fetched at most once per 60 seconds
289
+ unless the operator forces a refresh.
290
+
291
+ Remaining is `1 - percent/100` for each Go window and `limit_remaining / limit` for OpenRouter,
292
+ or unlimited when the account has no limit. A subscription is eligible only when every gating
293
+ window is strictly above **1% remaining**; that is the rule the dialog states. Eligible
294
+ OpenCode Go subscriptions rank by their lowest remaining window, and ties keep the operator's
295
+ list order. A key whose usage check failed or timed out stays eligible with unknown headroom and
296
+ ranks below every key with known headroom. When no Go subscription is eligible, the peer starts
297
+ on the OpenRouter fallback model; when neither provider has an eligible subscription, no process
298
+ starts and the UI and event stream report the reason. With no subscription configured at all,
299
+ every peer start is refused with an error pointing at the Providers dialog, so a peer never
300
+ silently inherits the operator's OMP login.
301
+
302
+ Every agent process — goal work, lifecycle lanes, Flow, issues, Auto Scout, Ask, Discover, goal
303
+ drafts, and skills preparation — starts with `PI_CODING_AGENT_DIR` pointing at a generated
304
+ directory under `<stateDir>/providers/agents/`. Its `config.yml` pins `default`, `plan`, `smol`,
305
+ `slow`, and `task` to the selected model. A Go selection also sets `retry.modelFallback: true`,
306
+ `retry.fallbackOn: any`, and a chain from `opencode-go/*` to the OpenRouter fallback; an
307
+ OpenRouter selection sets no chain. The peer environment removes the operator's inherited
308
+ `OPENCODE_API_KEY` and `OPENROUTER_API_KEY` first, then adds only the selected keys (a Go peer
309
+ also receives an OpenRouter key when one exists, for its fallback chain).
310
+
311
+ `omp/config.yml` is still installed as `<host>/.omp/config.yml`, but it no longer pins
312
+ `modelRoles`: a project-level pin would override the per-peer selection. The interactive agent
313
+ started in the repository keeps OMP's configured default. Editing `omp/config.yml` here and
314
+ rerunning `install.sh` changes the installed copy.
315
+
316
+ `bun run verify:providers` proves the selection matrix, the 1% boundary, unknown-usage ranking,
317
+ OpenRouter fallback, refusal, masking, environment and argv hygiene, generated roles,
318
+ persistence, and cleanup protection against a local provider stub and a deterministic ACP peer.
269
319
 
270
320
  ## Build and run
271
321
 
@@ -1097,6 +1147,8 @@ and is retried the same way.
1097
1147
  |`STATIC_DIR`|`dist`|Built client directory.|
1098
1148
  |`AGENT_COMMAND`|`omp acp`|ACP peer command, split on whitespace. `server/fixtures/stub-agent.ts` is a deterministic substitute for tests and local verification.|
1099
1149
  |`STATE_DIR`|`<harness>/.tmp`|State and journal directory; an installed dashboard passes `<host>/.devboard/.tmp`.|
1150
+ |`DEVBOARD_OPENCODE_GO_USAGE_BASE_URL`|`https://opencode.ai/zen/go`|OpenCode Go usage base; overridden by tests and offline verification.|
1151
+ |`DEVBOARD_OPENROUTER_USAGE_BASE_URL`|`https://openrouter.ai/api`|OpenRouter usage base; overridden by tests and offline verification.|
1100
1152
  |`DEVBOARD_UPDATE_INTERVAL_MS`|`60000`|Automatic update check interval.|
1101
1153
  |`DEVBOARD_UPDATE_BACKOFF_MAX_MS`|`3600000`|Ceiling for the exponential retry delay after a failed check or install.|
1102
1154
  |`DEVBOARD_RELEASE_DIR`|unset|Local release directory for offline verification; unset reads the newest Gitea release over the public HTTP API.|
@@ -1224,6 +1276,8 @@ stale files are therefore removed after a week. These entries are never deleted
1224
1276
 
1225
1277
  - `.tmp/state/` and everything below it.
1226
1278
  - `.tmp/journal/`, which the journal rule above governs.
1279
+ - `.tmp/providers/` in full: the operator's credential store and every generated per-peer agent
1280
+ directory. A credential is operator input, not scratch, and nothing recreates it.
1227
1281
  - `.tmp/watchdog/state.json`, `.tmp/watchdog/latest.json`, and `.tmp/watchdog/lock`.
1228
1282
  - `.tmp/orchestrator/state.json`, `.tmp/orchestrator/ledger.json`, and `.tmp/orchestrator/lock`,
1229
1283
  which the first Issues Start still reads to migrate handled issue numbers.
@@ -1995,6 +2049,11 @@ The client sends:
1995
2049
  { "type": "answer", "id": 1, "value": "Gamma" }
1996
2050
  { "type": "ask", "conversationId": "…", "question": "What does this label mean?", "context": { … } }
1997
2051
  { "type": "closeAsk", "conversationId": "…" }
2052
+ { "type": "getProviders" }
2053
+ { "type": "saveProvider", "id": "…", "provider": "opencode-go", "label": "Work", "key": "…" }
2054
+ { "type": "deleteProvider", "id": "…" }
2055
+ { "type": "setProviderEnabled", "id": "…", "enabled": false }
2056
+ { "type": "refreshProviders" }
1998
2057
  ```
1999
2058
 
2000
2059
  - `getGoal` reads the goal document from disk and answers with its complete contents; an
@@ -2010,6 +2069,14 @@ The client sends:
2010
2069
  version a release packs, so an unpacked checkout never claims `0.1.0`. A client that connects,
2011
2070
  and the page every few seconds, asks for it; a stale answer makes the reload control appear
2012
2071
  with both versions in its title.
2072
+ - `getProviders` answers with `providersState`: every subscription with its masked key, status,
2073
+ quota windows, remaining fraction, last check time, and error; the current `selection` (or
2074
+ `null`), the refusal reason, and the fixed rule `rotates at 1% remaining`. `saveProvider`
2075
+ validates a new or changed key first: HTTP 401/403 answers `providerError` and changes nothing,
2076
+ while an unreachable endpoint or 5xx saves the subscription with status `unknown`. Editing a
2077
+ label without a key never revalidates. `deleteProvider` and `setProviderEnabled` persist the
2078
+ store without touching a running peer. `refreshProviders` bypasses the 60-second usage cache.
2079
+ Every mutation broadcasts a fresh `providersState`; failures answer `providerError`.
2013
2080
  - `getIssues` answers with `issuesState`, the worker snapshot plus `present` and `attached`:
2014
2081
  lifecycle, enabled, paused, repository, poll times, queue entries with `labels`, `handled`,
2015
2082
  `runId`, and `agentRunId`, run summaries, the running `agents` (run id, issue number, startedAt,
@@ -2352,6 +2419,7 @@ The server answers with events:
2352
2419
  cd .devboard
2353
2420
  bun test
2354
2421
  bun run verify:interface
2422
+ bun run verify:providers
2355
2423
  bun run verify:install
2356
2424
  bun run verify:update
2357
2425
  bun run verify:ui
package/lib/gitea.ts CHANGED
@@ -668,17 +668,7 @@ export async function giteaDeleteBranch(
668
668
  }
669
669
 
670
670
  /** Workflow runs whose triggering commit is `headSha`. */
671
- export async function giteaWorkflowRuns(
672
- target: GiteaTarget,
673
- headSha: string,
674
- fetchImpl: FetchLike = fetch,
675
- ): Promise<GiteaWorkflowRun[]> {
676
- const document = await giteaJson<unknown>(
677
- target,
678
- `${repoPath(target)}/actions/runs?head_sha=${encodeURIComponent(headSha)}&limit=50`,
679
- undefined,
680
- fetchImpl,
681
- );
671
+ function workflowRunsFrom(document: unknown): GiteaWorkflowRun[] {
682
672
  let entries: unknown;
683
673
  if (Array.isArray(document)) {
684
674
  entries = document;
@@ -707,3 +697,33 @@ export async function giteaWorkflowRuns(
707
697
  }
708
698
  return runs;
709
699
  }
700
+
701
+ /**
702
+ * Workflow runs for one commit. The run-list endpoint needs a credential on
703
+ * the canonical instance; the public task feed answers the same records, so
704
+ * read-only verification works without a token.
705
+ */
706
+ export async function giteaWorkflowRuns(
707
+ target: GiteaTarget,
708
+ headSha: string,
709
+ fetchImpl: FetchLike = fetch,
710
+ ): Promise<GiteaWorkflowRun[]> {
711
+ try {
712
+ const document = await giteaJson<unknown>(
713
+ target,
714
+ `${repoPath(target)}/actions/runs?head_sha=${encodeURIComponent(headSha)}&limit=50`,
715
+ undefined,
716
+ fetchImpl,
717
+ );
718
+ return workflowRunsFrom(document);
719
+ } catch (error) {
720
+ if (!(error instanceof GiteaError) || error.status !== 401) throw error;
721
+ const document = await giteaJson<unknown>(
722
+ target,
723
+ `${repoPath(target)}/actions/tasks?limit=50`,
724
+ undefined,
725
+ fetchImpl,
726
+ );
727
+ return workflowRunsFrom(document);
728
+ }
729
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsdenes/devboard",
3
- "version": "0.1.562",
3
+ "version": "0.1.564",
4
4
  "type": "module",
5
5
  "description": "Devboard harness: ADR dashboard server, client, prompts, wrappers, and installer.",
6
6
  "scripts": {
@@ -84,7 +84,8 @@ no local checkout, running dashboard, or required release tarball is involved. G
84
84
  push mirror of this repository; it carries no release the dashboard reads.
85
85
 
86
86
  A host installs a real npm prefix at `<host>/.devboard` with
87
- `npx @dsdenes/devboard install`. `<host>/.omp/commands`, `<host>/.omp/extensions`, and
87
+ `npx @dsdenes/devboard install`. The launcher never discovers, reads, copies, or waits on another
88
+ local Devboard installation or source checkout. `<host>/.omp/commands`, `<host>/.omp/extensions`, and
88
89
  `<host>/AGENTS.md` are real copies from the installed package; no harness path is a symlink.
89
90
  The canonical `.devboard/` in this checkout is the source tree and is never an npm install.
90
91