@worca/app 1.3.0 → 1.4.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 +85 -6
- package/agents/clarify.meta.json +1 -0
- package/agents/memoryDefragmenter.meta.json +2 -1
- package/agents/reviewer.meta.json +60 -0
- package/agents/worca-cc-code-reviewer.md +33 -0
- package/agents/worca-cc-memory-defragmenter.md +5 -3
- package/agents/workspaceScanner.meta.json +1 -0
- package/package.json +14 -10
- package/scripts/git-diff.mjs +25 -0
- package/scripts/gitDiff.meta.json +18 -0
- package/scripts/js-inline.mjs +11 -0
- package/scripts/js.meta.json +22 -0
- package/scripts/py-inline.py +27 -0
- package/scripts/py.meta.json +22 -0
- package/scripts/shell.meta.json +24 -0
- package/skills/worca/SKILL.md +3 -2
- package/src/cli/models.mjs +247 -0
- package/src/cli/render.mjs +72 -4
- package/src/cli/schedule.mjs +494 -0
- package/src/cli/worca-cc.mjs +1001 -22
- package/src/core/agent-registry.mjs +75 -23
- package/src/core/agent-store.mjs +51 -2
- package/src/core/artifacts.mjs +73 -10
- package/src/core/ask/events.mjs +119 -1
- package/src/core/ask/limits.mjs +32 -4
- package/src/core/ask/mcp-stdio.mjs +12 -0
- package/src/core/ask/model-deps.mjs +126 -0
- package/src/core/ask/model-proposal.mjs +370 -0
- package/src/core/ask/models.mjs +12 -0
- package/src/core/ask/policy-deps.mjs +124 -0
- package/src/core/ask/policy-proposal.mjs +363 -0
- package/src/core/ask/prompt.mjs +74 -9
- package/src/core/ask/proposal.mjs +54 -5
- package/src/core/ask/schedule-deps.mjs +83 -0
- package/src/core/ask/schedule-spec.mjs +310 -0
- package/src/core/ask/script-deps.mjs +357 -0
- package/src/core/ask/source-deps.mjs +52 -0
- package/src/core/ask/source-spec.mjs +157 -0
- package/src/core/ask/spawn.mjs +1 -0
- package/src/core/ask/store.mjs +6 -3
- package/src/core/ask/tool-deps.mjs +4 -0
- package/src/core/ask/tools.mjs +657 -37
- package/src/core/ask/turn.mjs +109 -2
- package/src/core/ask-files.mjs +406 -0
- package/src/core/ask-forms.mjs +195 -0
- package/src/core/ask-projection.mjs +72 -0
- package/src/core/bridge/errors.mjs +84 -0
- package/src/core/bridge/provider-ops.mjs +281 -0
- package/src/core/bridge/providers/copilot.mjs +269 -0
- package/src/core/bridge/providers/endpoint.mjs +257 -0
- package/src/core/bridge/registry.mjs +88 -0
- package/src/core/bridge/semaphore.mjs +73 -0
- package/src/core/bridge/server.mjs +184 -0
- package/src/core/bridge/telemetry.mjs +53 -0
- package/src/core/bridge/translate/request.mjs +252 -0
- package/src/core/bridge/translate/response.mjs +82 -0
- package/src/core/bridge/translate/stream.mjs +242 -0
- package/src/core/bridge/upstream.mjs +209 -0
- package/src/core/chat/command-router.mjs +58 -4
- package/src/core/chat/notifier.mjs +14 -1
- package/src/core/chat/renderers.mjs +35 -0
- package/src/core/claude-runner.mjs +126 -19
- package/src/core/config.mjs +212 -31
- package/src/core/cost-budget.mjs +3 -2
- package/src/core/db.mjs +169 -15
- package/src/core/failure-policy.mjs +10 -0
- package/src/core/fs-browse.mjs +16 -4
- package/src/core/git-info.mjs +22 -0
- package/src/core/graph/builtin-workflows.mjs +3 -1
- package/src/core/graph/exec-io.mjs +71 -0
- package/src/core/graph/executor.mjs +139 -70
- package/src/core/graph/human-evidence.mjs +131 -0
- package/src/core/graph/python-probe.mjs +172 -0
- package/src/core/graph/registry-ports.mjs +10 -6
- package/src/core/graph/scheduler.mjs +39 -24
- package/src/core/graph/script-child.mjs +81 -0
- package/src/core/graph/script-runner.mjs +597 -0
- package/src/core/graph/worca_script.py +207 -0
- package/src/core/guardrail-store.mjs +16 -0
- package/src/core/human-backfill.mjs +108 -0
- package/src/core/human-rate.mjs +17 -0
- package/src/core/index-html.mjs +6 -2
- package/src/core/memory-defrag-model.mjs +112 -0
- package/src/core/memory-store.mjs +70 -18
- package/src/core/memory-sync.mjs +22 -11
- package/src/core/metrics/read.mjs +4 -1
- package/src/core/metrics/record.mjs +50 -2
- package/src/core/metrics/sync.mjs +6 -4
- package/src/core/model-env.mjs +149 -0
- package/src/core/model-test.mjs +14 -1
- package/src/core/notifications.mjs +128 -0
- package/src/core/onboarding.mjs +8 -2
- package/src/core/orchestrator.mjs +357 -26
- package/src/core/phases.mjs +95 -6
- package/src/core/plugin-api.mjs +24 -7
- package/src/core/plugin-manifest.mjs +184 -18
- package/src/core/plugin-models.mjs +1 -0
- package/src/core/plugin-script-cases.mjs +118 -0
- package/src/core/plugin-store.mjs +163 -17
- package/src/core/plugin-workflows.mjs +71 -17
- package/src/core/policy/cache.mjs +116 -0
- package/src/core/policy/effective.mjs +175 -0
- package/src/core/policy/gate.mjs +91 -0
- package/src/core/policy/local.mjs +145 -0
- package/src/core/policy/registry.mjs +330 -0
- package/src/core/policy/scope.mjs +61 -0
- package/src/core/policy/state.mjs +79 -0
- package/src/core/policy/sync.mjs +513 -0
- package/src/core/protocol.mjs +43 -0
- package/src/core/run-harness.mjs +421 -72
- package/src/core/scheduler.mjs +980 -0
- package/src/core/script-bench.mjs +628 -0
- package/src/core/script-registry.mjs +116 -0
- package/src/core/script-store.mjs +563 -0
- package/src/core/settings.mjs +489 -14
- package/src/core/stats.mjs +33 -2
- package/src/core/workflow-export.mjs +94 -3
- package/src/core/workflow-share.mjs +67 -18
- package/src/core/workflows.mjs +47 -11
- package/src/core/workspaces.mjs +18 -12
- package/src/shared/forms/answer.mjs +164 -0
- package/src/shared/forms/catalog.mjs +91 -0
- package/src/shared/forms/form-def.mjs +290 -0
- package/src/shared/forms/layout.mjs +67 -0
- package/src/shared/forms/paths.mjs +47 -0
- package/src/shared/forms/project.mjs +309 -0
- package/src/shared/forms/schema.mjs +205 -0
- package/src/shared/graph/agent-meta.mjs +55 -5
- package/src/shared/graph/constants.mjs +14 -2
- package/src/shared/graph/flow-layout.mjs +2 -1
- package/src/shared/graph/isomorphic.mjs +5 -3
- package/src/shared/graph/manifest.mjs +22 -13
- package/src/shared/graph/ports.mjs +45 -19
- package/src/shared/graph/script-cases.mjs +257 -0
- package/src/shared/graph/script-icons.mjs +46 -0
- package/src/shared/graph/script-infer.mjs +259 -0
- package/src/shared/graph/script-meta.mjs +408 -0
- package/src/shared/graph/script-templates.mjs +201 -0
- package/src/shared/graph/template.mjs +4 -4
- package/src/shared/graph/validate.mjs +89 -16
- package/src/shared/human-estimate.mjs +100 -0
- package/src/shared/schedule/recurrence.mjs +353 -0
- package/src/shared/team-metrics/aggregate.mjs +51 -11
- package/{scripts → tools}/install.mjs +3 -3
- package/ui/public/app.js +4390 -683
- package/ui/public/artifact-picker.mjs +189 -0
- package/ui/public/ask/dom.mjs +121 -0
- package/ui/public/ask/form-preview.mjs +55 -0
- package/ui/public/ask/form-renderer.mjs +250 -0
- package/ui/public/ask/registry.mjs +53 -0
- package/ui/public/ask/widgets-display.mjs +370 -0
- package/ui/public/ask/widgets-input.mjs +624 -0
- package/ui/public/ask/widgets-layout.mjs +90 -0
- package/ui/public/ask-panel.mjs +401 -27
- package/ui/public/ask-run-card.mjs +1 -1
- package/ui/public/bridge-view.mjs +694 -0
- package/ui/public/chat-settings-view.mjs +24 -0
- package/ui/public/code-editor.mjs +181 -0
- package/ui/public/getting-started.mjs +34 -7
- package/ui/public/graph/composer.mjs +138 -10
- package/ui/public/graph/inspector.mjs +61 -58
- package/ui/public/graph/palette.mjs +27 -9
- package/ui/public/graph/run-decor.mjs +34 -17
- package/ui/public/graph/run-hosts.mjs +7 -1
- package/ui/public/graph/save-dialog.mjs +3 -0
- package/ui/public/graph/view.mjs +23 -7
- package/ui/public/guardrails-view.mjs +15 -3
- package/ui/public/guide-spot.mjs +87 -9
- package/ui/public/index.html +480 -141
- package/ui/public/memory-view.mjs +22 -4
- package/ui/public/models-view.mjs +162 -17
- package/ui/public/node-tunables.mjs +33 -4
- package/ui/public/plugins-view.mjs +23 -1
- package/ui/public/results-view.mjs +4 -2
- package/ui/public/schedule-sheet.mjs +430 -0
- package/ui/public/schedules-view.mjs +432 -0
- package/ui/public/script-bench-view.mjs +1154 -0
- package/ui/public/script-forms.mjs +282 -0
- package/ui/public/script-wizard.mjs +529 -0
- package/ui/public/scripts-view.mjs +868 -0
- package/ui/public/stats-view.mjs +159 -52
- package/ui/public/style.css +1461 -44
- package/ui/public/team-metrics-surfaces.mjs +77 -16
- package/ui/public/team-metrics-view.mjs +68 -5
- package/ui/public/team-policy-view.mjs +1402 -0
- package/ui/public/ui-level.mjs +237 -0
- package/ui/server.mjs +1827 -73
package/src/core/config.mjs
CHANGED
|
@@ -16,9 +16,15 @@ import { getDb, prepare, tx } from './db.mjs';
|
|
|
16
16
|
import { projectKey } from './store.mjs';
|
|
17
17
|
import { AUTO_WORKFLOW_ID } from './graph/builtin-workflows.mjs';
|
|
18
18
|
import { loadAgentRegistry, registryToSteps } from './agent-registry.mjs';
|
|
19
|
-
import { EFFORTS, prepareModelEnv, withTierModelEnv, isSubagentModelValue, subagentModelIssue } from './model-env.mjs';
|
|
20
|
-
import {
|
|
19
|
+
import { EFFORTS, prepareModelEnv, withTierModelEnv, isSubagentModelValue, subagentModelIssue, BRIDGE_ROUTING_KEYS, bridgeExcludedTools } from './model-env.mjs';
|
|
20
|
+
import { findBridgedEntry, providerReadiness } from './bridge/registry.mjs';
|
|
21
|
+
import { bridgeBaseUrl, bridgeSecret } from './bridge/server.mjs';
|
|
22
|
+
import { listGlobalModels, addGlobalModel, removeGlobalModel, hideBuiltinModels, readSettings, memoryDefragModel, setMemoryDefragModel } from './settings.mjs';
|
|
23
|
+
/** Whether the developer stored the hide-built-ins flag (a team default applies only when not). */
|
|
24
|
+
const readSettingsHideStored = () => { const s = readSettings(); return typeof s.hideBuiltinModels === 'boolean' || s.hideBuiltinModelsChosen === true; };
|
|
21
25
|
import { listPluginModels, allPluginModels, flattenPluginModelEnv } from './plugin-models.mjs';
|
|
26
|
+
// Team policy defaults (team-policy design §6, §8): read from the discovery CACHE only (a leaf module).
|
|
27
|
+
import { policyCatalogModels, teamDefault } from './policy/cache.mjs';
|
|
22
28
|
|
|
23
29
|
/**
|
|
24
30
|
* Recompute the agent step list FRESH from the layered registry (repo agents/ +
|
|
@@ -61,12 +67,14 @@ export { EFFORTS };
|
|
|
61
67
|
* available for this subscription"). Fable 5.1 needs no `[1m]` suffix: its context
|
|
62
68
|
* window is 1M by default (verified to resolve via `claude --model`, CLI 2.1.257).
|
|
63
69
|
* It replaced Fable 5 (`claude-fable-5`) on 2026-09-01; db.mjs V26 moves every
|
|
64
|
-
* stored pin on the retired id to the successor, so nothing keeps it here. Opus 5
|
|
65
|
-
* (`claude-opus-5`) and Sonnet 5 (`claude-sonnet-5`) are likewise 1M-only and
|
|
66
|
-
* carry no `[1m]` twin.
|
|
70
|
+
* stored pin on the retired id to the successor, so nothing keeps it here. Opus 5.5
|
|
71
|
+
* (`claude-opus-5-5`) and Sonnet 5 (`claude-sonnet-5`) are likewise 1M-only and
|
|
72
|
+
* carry no `[1m]` twin. Opus 5.5 replaced Opus 5 (`claude-opus-5`) on 2026-09-22
|
|
73
|
+
* (verified to resolve via `claude --model`, CLI 2.1.280); db.mjs V35 moves the
|
|
74
|
+
* stored pins the same way.
|
|
67
75
|
*/
|
|
68
76
|
export const PREDEFINED_MODELS = [
|
|
69
|
-
{ id: 'claude-opus-5',
|
|
77
|
+
{ id: 'claude-opus-5-5', label: 'Opus 5.5', efforts: ['medium', 'high', 'xhigh', 'max'] },
|
|
70
78
|
{ id: 'claude-fable-5-1', label: 'Fable 5.1 (1M)', efforts: ['medium', 'high', 'xhigh', 'max'] },
|
|
71
79
|
{ id: 'claude-opus-4-8', label: 'Opus 4.8', efforts: ['medium', 'high', 'xhigh', 'max'] },
|
|
72
80
|
{ id: 'claude-opus-4-8[1m]', label: 'Opus 4.8 (1M)', efforts: ['medium', 'high', 'xhigh', 'max'] },
|
|
@@ -134,7 +142,7 @@ function parseJson(text, fallback) {
|
|
|
134
142
|
* @param {string} key
|
|
135
143
|
* @returns {{steps:string,custom_models:string,active_workflow_id:(string|null),extra:string}|null}
|
|
136
144
|
*/
|
|
137
|
-
function readConfigRow(key) {
|
|
145
|
+
export function readConfigRow(key) {
|
|
138
146
|
getDb();
|
|
139
147
|
return prepare(
|
|
140
148
|
'SELECT steps, custom_models, active_workflow_id, extra, human_in_loop FROM project_config WHERE project_key = ?'
|
|
@@ -175,7 +183,7 @@ export async function readConfig(projectDir) {
|
|
|
175
183
|
* env objects already in scope here, NEVER by calling modelHasBaseUrlRouting per
|
|
176
184
|
* entry (that re-reads settings + the plugins lock from disk on every row).
|
|
177
185
|
*/
|
|
178
|
-
function composeCatalog(projectCustom = []) {
|
|
186
|
+
function composeCatalog(projectCustom = [], { projectDir = null } = {}) {
|
|
179
187
|
const globals = listGlobalModels();
|
|
180
188
|
const globalByIdLc = new Map(globals.map((m) => [m.id.toLowerCase(), m]));
|
|
181
189
|
const plugins = listPluginModels();
|
|
@@ -189,17 +197,40 @@ function composeCatalog(projectCustom = []) {
|
|
|
189
197
|
// read by every picker. The entry stays in the catalog — hiding an id must
|
|
190
198
|
// never stop it resolving, or a stored run / plugin reference that names it
|
|
191
199
|
// would break — so pickers skip `hidden`, validators ignore it.
|
|
192
|
-
|
|
200
|
+
// A team-policy default applies only while the developer has not stored the flag themselves.
|
|
201
|
+
const teamHide = projectDir ? teamDefault(projectDir, 'models.hideBuiltins') : undefined;
|
|
202
|
+
const hideStored = readSettingsHideStored();
|
|
203
|
+
const hidden = (hideStored ? hideBuiltinModels() : (teamHide === true || hideBuiltinModels())) ? { hidden: true } : {};
|
|
204
|
+
// Bridged entries (model-bridge-design.md §8.5): `bridged` names the provider
|
|
205
|
+
// (false otherwise), `upstreamApi` the wire protocol, `needsSignIn` whether
|
|
206
|
+
// the provider is usable right now (pickers skip such entries unless they are
|
|
207
|
+
// the current selection), `capabilities` what the editor pinned. `routed` is
|
|
208
|
+
// true too: the CLI IS pointed at a custom endpoint (worca's own bridge).
|
|
209
|
+
// provider + the entry's own key/base URL -> readiness (both can decide it)
|
|
210
|
+
const readiness = new Map();
|
|
211
|
+
const bridgeShape = (m) => {
|
|
212
|
+
if (!m.upstream) return {};
|
|
213
|
+
const p = m.upstream.provider;
|
|
214
|
+
const key = `${p}\n${m.upstream.apiKey || ''}\n${m.upstream.baseUrl || ''}`;
|
|
215
|
+
if (!readiness.has(key)) readiness.set(key, providerReadiness(m.upstream));
|
|
216
|
+
const r = readiness.get(key);
|
|
217
|
+
return {
|
|
218
|
+
bridged: p, upstreamApi: m.upstream.api, upstreamModel: m.upstream.model,
|
|
219
|
+
needsSignIn: !r.ok, ...(r.ok ? {} : { signInReason: r.reason }),
|
|
220
|
+
...(m.upstream.capabilities ? { capabilities: { ...m.upstream.capabilities } } : {}),
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
const routedOrBridged = (m) => routedOf(m.env) || !!m.upstream;
|
|
193
224
|
const pluginShape = (id, m, lc) => ({
|
|
194
225
|
id, label: m.label, efforts: [...m.efforts], custom: 'plugin', plugin: m.plugin,
|
|
195
|
-
hasEnv: !!m.env, routed:
|
|
226
|
+
hasEnv: !!m.env, routed: routedOrBridged(m), ...unreliable(lc), ...bridgeShape(m),
|
|
196
227
|
});
|
|
197
228
|
for (const m of PREDEFINED_MODELS) {
|
|
198
229
|
const lc = m.id.toLowerCase();
|
|
199
230
|
const shadow = globalByIdLc.get(lc);
|
|
200
231
|
const pshadow = pluginByIdLc.get(lc);
|
|
201
232
|
out.push(shadow
|
|
202
|
-
? { id: m.id, label: shadow.label, efforts: [...shadow.efforts], custom: 'global', hasEnv: !!shadow.env, routed:
|
|
233
|
+
? { id: m.id, label: shadow.label, efforts: [...shadow.efforts], custom: 'global', hasEnv: !!shadow.env, routed: routedOrBridged(shadow), ...unreliable(lc), ...bridgeShape(shadow) }
|
|
203
234
|
: pshadow
|
|
204
235
|
? pluginShape(m.id, pshadow, lc)
|
|
205
236
|
: { ...m, custom: false, hasEnv: false, routed: false, ...hidden });
|
|
@@ -209,7 +240,7 @@ function composeCatalog(projectCustom = []) {
|
|
|
209
240
|
const lc = m.id.toLowerCase();
|
|
210
241
|
if (seen.has(lc)) continue; // predefined shadow, already emitted
|
|
211
242
|
seen.add(lc);
|
|
212
|
-
out.push({ id: m.id, label: m.label, efforts: [...m.efforts], custom: 'global', hasEnv: !!m.env, routed:
|
|
243
|
+
out.push({ id: m.id, label: m.label, efforts: [...m.efforts], custom: 'global', hasEnv: !!m.env, routed: routedOrBridged(m), ...unreliable(lc), ...bridgeShape(m) });
|
|
213
244
|
}
|
|
214
245
|
for (const m of plugins) {
|
|
215
246
|
const lc = m.id.toLowerCase();
|
|
@@ -217,6 +248,15 @@ function composeCatalog(projectCustom = []) {
|
|
|
217
248
|
seen.add(lc);
|
|
218
249
|
out.push(pluginShape(m.id, m, lc));
|
|
219
250
|
}
|
|
251
|
+
// Team-policy catalog entries (team-policy design §8): after global and plugin, before the
|
|
252
|
+
// legacy per-project ones. Read-only rows with a policy badge; `home` names the policy.
|
|
253
|
+
for (const m of policyCatalogModels()) {
|
|
254
|
+
const lc = m.id.toLowerCase();
|
|
255
|
+
if (seen.has(lc)) continue;
|
|
256
|
+
seen.add(lc);
|
|
257
|
+
out.push({ id: m.id, label: m.label, efforts: [...m.efforts], custom: 'policy', policy: m.home,
|
|
258
|
+
hasEnv: !!m.env, routed: routedOrBridged(m), ...unreliable(lc), ...bridgeShape(m) });
|
|
259
|
+
}
|
|
220
260
|
for (const m of projectCustom) {
|
|
221
261
|
if (seen.has(m.id.toLowerCase())) continue; // predefined/global/plugin wins
|
|
222
262
|
seen.add(m.id.toLowerCase());
|
|
@@ -241,9 +281,29 @@ export function modelHasBaseUrlRouting(modelId) {
|
|
|
241
281
|
if (!id) return false;
|
|
242
282
|
const lc = id.toLowerCase();
|
|
243
283
|
const entry = listGlobalModels().find((m) => m.id.toLowerCase() === lc);
|
|
244
|
-
if (entry) return !!(entry.env && 'ANTHROPIC_BASE_URL' in entry.env);
|
|
284
|
+
if (entry) return !!entry.upstream || !!(entry.env && 'ANTHROPIC_BASE_URL' in entry.env);
|
|
245
285
|
const pm = listPluginModels().find((m) => m.id.toLowerCase() === lc);
|
|
246
|
-
return !!
|
|
286
|
+
if (pm) return !!pm.upstream || !!(pm.env && 'ANTHROPIC_BASE_URL' in pm.env);
|
|
287
|
+
return !!findBridgedEntry(id); // a team-policy entry with an upstream
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* The bridge facts for a model id (model-bridge-design.md §4.2/§8.5), or null
|
|
292
|
+
* when it is not a bridged entry: `{id, provider, api, upstreamModel,
|
|
293
|
+
* excludeTools, ready, reason?, message?}`. `excludeTools` are the CLI built-ins
|
|
294
|
+
* the runner must withhold (web tools have no chat/completions equivalent);
|
|
295
|
+
* `ready` is the provider's sign-in state — a spawn fails fast on it instead
|
|
296
|
+
* of with an opaque 401 mid-run. Synchronous; never throws.
|
|
297
|
+
*/
|
|
298
|
+
export function bridgedModelInfo(modelId) {
|
|
299
|
+
const e = findBridgedEntry(modelId);
|
|
300
|
+
if (!e) return null;
|
|
301
|
+
const r = providerReadiness(e.upstream);
|
|
302
|
+
return {
|
|
303
|
+
id: e.id, provider: e.upstream.provider, api: e.upstream.api, upstreamModel: e.upstream.model,
|
|
304
|
+
excludeTools: bridgeExcludedTools(e.upstream),
|
|
305
|
+
ready: r.ok, ...(r.ok ? {} : { reason: r.reason, message: r.message }),
|
|
306
|
+
};
|
|
247
307
|
}
|
|
248
308
|
|
|
249
309
|
/** Lowercased ids currently flagged cost-unreliable. Never throws ({} on any
|
|
@@ -423,7 +483,8 @@ export function resolveModelCost(modelId, cliCostUsd, usage, costCfg = undefined
|
|
|
423
483
|
|
|
424
484
|
// ── display-only list prices ──────────────────────────────────────────────────
|
|
425
485
|
// USD per MILLION tokens for the built-in ids, from Anthropic's published
|
|
426
|
-
// pricing (platform.claude.com/docs/en/pricing — snapshot 2026-06-24
|
|
486
|
+
// pricing (platform.claude.com/docs/en/pricing — snapshot 2026-06-24; Opus 5.5
|
|
487
|
+
// added 2026-09-22). DISPLAY
|
|
427
488
|
// APPROXIMATION ONLY: it feeds the chat footer's live "≈" estimate while a turn
|
|
428
489
|
// streams (ask/events.mjs `estimatedCostUsd`). The CLI's result.total_cost_usd,
|
|
429
490
|
// re-priced by resolveModelCost, stays the ONLY figure any message row, thread
|
|
@@ -431,12 +492,12 @@ export function resolveModelCost(modelId, cliCostUsd, usage, costCfg = undefined
|
|
|
431
492
|
// Ids missing here get no estimate (null), which is the pre-existing behaviour;
|
|
432
493
|
// `[1m]` twins and dated ids resolve to their base row (the long-context premium
|
|
433
494
|
// is not modelled). cacheWrite = 1.25× input (5-minute TTL), cacheWrite1h = 2×
|
|
434
|
-
// input, cacheRead = 0.1× input except Fable 5.1 (0.025×). Refresh by hand when
|
|
495
|
+
// input, cacheRead = 0.1× input except Fable 5.1 (0.025×) and Opus 5.5 (0.05×). Refresh by hand when
|
|
435
496
|
// Anthropic moves a price. PREDEFINED_MODELS itself stays untouched — its entry
|
|
436
497
|
// shape is pinned (test/config-models-global.test.mjs:205).
|
|
437
498
|
export const PREDEFINED_LIST_PRICES = Object.freeze({
|
|
438
499
|
'claude-fable-5-1': { input: 10, output: 50, cacheRead: 0.25, cacheWrite: 12.5, cacheWrite1h: 20 },
|
|
439
|
-
'claude-opus-5':
|
|
500
|
+
'claude-opus-5-5': { input: 4, output: 20, cacheRead: 0.2, cacheWrite: 5, cacheWrite1h: 8 },
|
|
440
501
|
'claude-opus-4-8': { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite1h: 10 },
|
|
441
502
|
'claude-opus-4-7': { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite1h: 10 },
|
|
442
503
|
'claude-opus-4-6': { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite1h: 10 },
|
|
@@ -469,9 +530,9 @@ export function liveCostRates(modelId) {
|
|
|
469
530
|
* raw id); global entries advertise their configured subset.
|
|
470
531
|
*/
|
|
471
532
|
export async function listModels(projectDir) {
|
|
472
|
-
if (!projectDir) return composeCatalog([]); // project-less: predefined ⊕ global
|
|
533
|
+
if (!projectDir) return composeCatalog([]); // project-less: predefined ⊕ global ⊕ plugin ⊕ policy
|
|
473
534
|
const { customModels } = readRaw(projectDir);
|
|
474
|
-
return composeCatalog(customModels);
|
|
535
|
+
return composeCatalog(customModels, { projectDir });
|
|
475
536
|
}
|
|
476
537
|
|
|
477
538
|
/**
|
|
@@ -486,7 +547,7 @@ export async function listModels(projectDir) {
|
|
|
486
547
|
* @param {string} modelId
|
|
487
548
|
* @returns {Record<string,string>|undefined}
|
|
488
549
|
*/
|
|
489
|
-
export function resolveModelEnv(modelId) {
|
|
550
|
+
export function resolveModelEnv(modelId, { tag } = {}) {
|
|
490
551
|
const id = typeof modelId === 'string' ? modelId.trim() : '';
|
|
491
552
|
if (!id) return undefined;
|
|
492
553
|
const lc = id.toLowerCase();
|
|
@@ -494,6 +555,56 @@ export function resolveModelEnv(modelId) {
|
|
|
494
555
|
let who;
|
|
495
556
|
let canonicalId = id;
|
|
496
557
|
const entry = listGlobalModels().find((m) => m.id.toLowerCase() === lc);
|
|
558
|
+
// A BRIDGED entry (model-bridge-design.md §4.2): the CLI talks to worca's
|
|
559
|
+
// own loopback bridge, which forwards to the entry's upstream. The routing
|
|
560
|
+
// keys are synthesized here — never user-editable beside `upstream` — and
|
|
561
|
+
// the entry's remaining env (a CLAUDE_CODE_* knob, say) still merges. Only
|
|
562
|
+
// the user's global layer and a plugin layer may be bridged; policy entries
|
|
563
|
+
// carry `upstream` too but ride the same lookup (registry.findBridgedEntry).
|
|
564
|
+
const bridged = findBridgedEntry(id);
|
|
565
|
+
if (bridged) {
|
|
566
|
+
// Fail fast (§8.5): a provider that is not signed in / not acknowledged
|
|
567
|
+
// would otherwise surface as an opaque 401 mid-run. This is the ONE case
|
|
568
|
+
// in which this resolver throws; every dispatch site already routes a
|
|
569
|
+
// runClaude failure to its error path, and the Test button's hint keys on
|
|
570
|
+
// `bridgeReason`. The error class is `auth` so recovery policy treats it
|
|
571
|
+
// like any credential failure (never retried blindly).
|
|
572
|
+
const ready = providerReadiness(bridged.upstream);
|
|
573
|
+
if (!ready.ok) {
|
|
574
|
+
const err = new Error(ready.message);
|
|
575
|
+
err.errorClass = 'auth';
|
|
576
|
+
err.bridgeReason = ready.reason;
|
|
577
|
+
err.bridgeProvider = bridged.upstream.provider;
|
|
578
|
+
throw err;
|
|
579
|
+
}
|
|
580
|
+
const base = bridged.source === 'global' && entry ? entry : null;
|
|
581
|
+
const { env: extra, dropped } = prepareModelEnv(base && base.env ? base.env : {});
|
|
582
|
+
for (const k of dropped) {
|
|
583
|
+
console.warn(`[worca] model ${JSON.stringify(bridged.id)}: dropping env key ${JSON.stringify(k)} (reserved or unresolvable \${VAR} ref)`);
|
|
584
|
+
}
|
|
585
|
+
for (const k of BRIDGE_ROUTING_KEYS) delete extra[k];
|
|
586
|
+
const env = {
|
|
587
|
+
...extra,
|
|
588
|
+
ANTHROPIC_BASE_URL: bridgeBaseUrl(bridged.id, { tag }),
|
|
589
|
+
ANTHROPIC_AUTH_TOKEN: bridgeSecret(),
|
|
590
|
+
ANTHROPIC_MODEL: bridged.id,
|
|
591
|
+
};
|
|
592
|
+
// The CLI turns tool search off for a non-Anthropic base URL and then sends
|
|
593
|
+
// every MCP tool schema in full — hundreds of KB with a few user MCP
|
|
594
|
+
// servers, far past a translated model's prompt limit (a local 32k model
|
|
595
|
+
// fails its first call). Its ToolSearch is client-side (schemas come back
|
|
596
|
+
// as tool_result text), so it works through the translation layer; the
|
|
597
|
+
// entry's own env may still turn it off.
|
|
598
|
+
if (bridged.upstream.api === 'openai-chat' && !('ENABLE_TOOL_SEARCH' in env)) env.ENABLE_TOOL_SEARCH = 'true';
|
|
599
|
+
// A bridged id is never a model name the CLI knows, so it assumes a 200k
|
|
600
|
+
// window and compacts only once the upstream rejects a request — on a 32k
|
|
601
|
+
// local model that means turns whose reply is cut to a few hundred tokens
|
|
602
|
+
// long before any overflow. The pinned limits are the real window.
|
|
603
|
+
const caps = bridged.upstream.capabilities || {};
|
|
604
|
+
if (caps.maxPromptTokens && !('CLAUDE_CODE_MAX_CONTEXT_TOKENS' in env)) env.CLAUDE_CODE_MAX_CONTEXT_TOKENS = String(caps.maxPromptTokens);
|
|
605
|
+
if (caps.maxOutputTokens && !('CLAUDE_CODE_MAX_OUTPUT_TOKENS' in env)) env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = String(caps.maxOutputTokens);
|
|
606
|
+
return withTierModelEnv(env, bridged.id);
|
|
607
|
+
}
|
|
497
608
|
if (entry && entry.env) {
|
|
498
609
|
rawEnv = entry.env;
|
|
499
610
|
who = JSON.stringify(entry.id);
|
|
@@ -508,6 +619,15 @@ export function resolveModelEnv(modelId) {
|
|
|
508
619
|
rawEnv = env;
|
|
509
620
|
who = `${JSON.stringify(pm.id)} (plugin "${pm.plugin}")`;
|
|
510
621
|
canonicalId = pm.id;
|
|
622
|
+
} else if (!pm) {
|
|
623
|
+
// A team-policy catalog entry, reached only when neither the user nor a plugin defines the
|
|
624
|
+
// id. Its env carries literals and ${VAR} refs only: the editor and the reader refuse secrets.
|
|
625
|
+
const tm = policyCatalogModels().find((m) => m.id.toLowerCase() === lc);
|
|
626
|
+
if (tm && tm.env) {
|
|
627
|
+
rawEnv = tm.env;
|
|
628
|
+
who = `${JSON.stringify(tm.id)} (team policy ${tm.home})`;
|
|
629
|
+
canonicalId = tm.id;
|
|
630
|
+
}
|
|
511
631
|
}
|
|
512
632
|
}
|
|
513
633
|
if (!rawEnv) return undefined;
|
|
@@ -535,7 +655,8 @@ export function catalogHasModel(modelId) {
|
|
|
535
655
|
const lc = id.toLowerCase();
|
|
536
656
|
return PREDEFINED_MODELS.some((m) => m.id.toLowerCase() === lc)
|
|
537
657
|
|| listGlobalModels().some((m) => m.id.toLowerCase() === lc)
|
|
538
|
-
|| listPluginModels().some((m) => m.id.toLowerCase() === lc)
|
|
658
|
+
|| listPluginModels().some((m) => m.id.toLowerCase() === lc)
|
|
659
|
+
|| policyCatalogModels().some((m) => m.id.toLowerCase() === lc);
|
|
539
660
|
}
|
|
540
661
|
|
|
541
662
|
/**
|
|
@@ -546,9 +667,12 @@ export function catalogHasModel(modelId) {
|
|
|
546
667
|
*/
|
|
547
668
|
export async function resolveStepModels(projectDir, fallbackModel) {
|
|
548
669
|
const cfg = readRaw(projectDir);
|
|
670
|
+
// Team policy `models.steps` (a default): starts a role the project has NOT configured.
|
|
671
|
+
const team = teamDefault(projectDir, 'models.steps') || {};
|
|
549
672
|
const out = {};
|
|
550
673
|
for (const { key } of agentSteps()) {
|
|
551
|
-
const
|
|
674
|
+
const own = cfg.steps[key] || {};
|
|
675
|
+
const sel = own.model || own.effort ? own : (team[key] || {});
|
|
552
676
|
out[key] = { model: sel.model || fallbackModel || undefined, effort: sel.effort || undefined };
|
|
553
677
|
}
|
|
554
678
|
return out;
|
|
@@ -841,16 +965,22 @@ export async function readRunConfig(projectDir) {
|
|
|
841
965
|
// Forward any OTHER unknown keys verbatim too (future-proof, matches "preserve unknown").
|
|
842
966
|
// prRemotes is the ship-it dialog's own preference (readPrRemotePrefs), not run config.
|
|
843
967
|
for (const [k, v] of Object.entries(extra)) {
|
|
844
|
-
if (k !== 'webUiTesting' && k !== PR_REMOTES_KEY && k !== TEAM_METRICS_KEY && !(k in out)) out[k] = v;
|
|
968
|
+
if (k !== 'webUiTesting' && k !== PR_REMOTES_KEY && k !== TEAM_METRICS_KEY && k !== TEAM_POLICY_KEY && k !== 'humanInLoopSet' && !(k in out)) out[k] = v;
|
|
845
969
|
}
|
|
846
970
|
const active = row && typeof row.active_workflow_id === 'string' ? row.active_workflow_id.trim() : '';
|
|
847
|
-
// Spec §6.1 / D16: a project with no remembered New-pipeline choice starts on Auto
|
|
848
|
-
|
|
971
|
+
// Spec §6.1 / D16: a project with no remembered New-pipeline choice starts on Auto — unless a
|
|
972
|
+
// team policy names a default workflow (team-policy design §8), which starts it there instead.
|
|
973
|
+
const teamWf = active ? undefined : teamDefault(projectDir, 'workflows.default');
|
|
974
|
+
out.activeWorkflowId = active || (typeof teamWf === 'string' && teamWf ? teamWf : AUTO_WORKFLOW_ID);
|
|
975
|
+
if (!active && teamWf) out.activeWorkflowSource = 'team-policy';
|
|
849
976
|
// Auto workflow (spec §6.1): the human-in-the-loop switch. ON is the default
|
|
850
977
|
// and is NOT echoed — the key appears only when the project turned it off, so
|
|
851
978
|
// every consumer reads `config.humanInLoop ?? true` and the config shape of a
|
|
852
|
-
// project that never touched it stays otherwise byte-identical.
|
|
979
|
+
// project that never touched it stays otherwise byte-identical. A team default applies only
|
|
980
|
+
// while the project has never set its own switch (setHumanInLoop stamps extra.humanInLoopSet).
|
|
853
981
|
if (row && row.human_in_loop === 0) out.humanInLoop = false;
|
|
982
|
+
else if (!extra.humanInLoopSet && teamDefault(projectDir, 'run.humanInLoop') === false) out.humanInLoop = false;
|
|
983
|
+
delete out.humanInLoopSet;
|
|
854
984
|
return out;
|
|
855
985
|
}
|
|
856
986
|
|
|
@@ -1110,6 +1240,40 @@ export function writeTeamMetricsPrefs(key, patch) {
|
|
|
1110
1240
|
return next;
|
|
1111
1241
|
}
|
|
1112
1242
|
|
|
1243
|
+
// ── Team-policy preferences (project_config.extra.teamPolicy) ──────────────
|
|
1244
|
+
// The discovery cache for the worca-policy branch (team-policy design §9): the last
|
|
1245
|
+
// verdict, the document read from origin, the delegate marker, and the per-window
|
|
1246
|
+
// total-cap acknowledgements. Same KEY-taking contract as the team-metrics pair.
|
|
1247
|
+
export const TEAM_POLICY_KEY = 'teamPolicy';
|
|
1248
|
+
|
|
1249
|
+
/** @returns {object|null} the cached team-policy state for a project key */
|
|
1250
|
+
export function readTeamPolicyPrefs(key) {
|
|
1251
|
+
assertProjectKey(key);
|
|
1252
|
+
const row = prepare('SELECT extra FROM project_config WHERE project_key = ?').get(key);
|
|
1253
|
+
const extra = row ? parseJson(row.extra, {}) : {};
|
|
1254
|
+
const v = extra[TEAM_POLICY_KEY];
|
|
1255
|
+
return v && typeof v === 'object' && !Array.isArray(v) ? v : null;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/** Shallow-merge `patch` into extra.teamPolicy; returns the merged object. */
|
|
1259
|
+
export function writeTeamPolicyPrefs(key, patch) {
|
|
1260
|
+
assertProjectKey(key);
|
|
1261
|
+
let next = null;
|
|
1262
|
+
tx(() => {
|
|
1263
|
+
const row = prepare('SELECT extra FROM project_config WHERE project_key = ?').get(key);
|
|
1264
|
+
const extra = row ? parseJson(row.extra, {}) : {};
|
|
1265
|
+
const cur = extra[TEAM_POLICY_KEY] && typeof extra[TEAM_POLICY_KEY] === 'object' ? extra[TEAM_POLICY_KEY] : {};
|
|
1266
|
+
next = { ...cur, ...patch };
|
|
1267
|
+
extra[TEAM_POLICY_KEY] = next;
|
|
1268
|
+
prepare(`
|
|
1269
|
+
INSERT INTO project_config (project_key, steps, custom_models, active_workflow_id, extra)
|
|
1270
|
+
VALUES (?, '{}', '[]', NULL, ?)
|
|
1271
|
+
ON CONFLICT(project_key) DO UPDATE SET extra = excluded.extra
|
|
1272
|
+
`).run(key, JSON.stringify(extra));
|
|
1273
|
+
});
|
|
1274
|
+
return next;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1113
1277
|
/**
|
|
1114
1278
|
* Set the project's human-in-the-loop switch for Auto runs (spec D15/D20).
|
|
1115
1279
|
* @param {string} projectDir
|
|
@@ -1124,6 +1288,13 @@ export async function setHumanInLoop(projectDir, value) {
|
|
|
1124
1288
|
VALUES (?, '{}', '[]', NULL, '{}', ?)
|
|
1125
1289
|
ON CONFLICT(project_key) DO UPDATE SET human_in_loop = excluded.human_in_loop
|
|
1126
1290
|
`).run(key, v);
|
|
1291
|
+
// The project now has its own switch: a team-policy default no longer applies (design §6).
|
|
1292
|
+
const row = prepare('SELECT extra FROM project_config WHERE project_key = ?').get(key);
|
|
1293
|
+
const extra = row ? parseJson(row.extra, {}) : {};
|
|
1294
|
+
if (!extra.humanInLoopSet) {
|
|
1295
|
+
extra.humanInLoopSet = true;
|
|
1296
|
+
prepare('UPDATE project_config SET extra = ? WHERE project_key = ?').run(JSON.stringify(extra), key);
|
|
1297
|
+
}
|
|
1127
1298
|
});
|
|
1128
1299
|
}
|
|
1129
1300
|
|
|
@@ -1165,18 +1336,25 @@ function allProjectConfigRows() {
|
|
|
1165
1336
|
/**
|
|
1166
1337
|
* Preview what removing a global catalog entry would clear, for the UI's
|
|
1167
1338
|
* confirmation dialog. `predefinedShadow: true` means the removal only reverts
|
|
1168
|
-
* an override and clears nothing.
|
|
1339
|
+
* an override and clears nothing. `memoryDefrag: true` (present only then) —
|
|
1340
|
+
* Settings › Memory's defragment model is this id. Synchronous; never throws.
|
|
1169
1341
|
* @param {string} id
|
|
1170
1342
|
* @returns {{predefinedShadow: boolean,
|
|
1171
1343
|
* steps: Array<{projectKey:string, step:string}>,
|
|
1172
|
-
* nodes: Array<{projectKey:string, workflowId:string, nodeId:string}
|
|
1344
|
+
* nodes: Array<{projectKey:string, workflowId:string, nodeId:string}>,
|
|
1345
|
+
* memoryDefrag?: true}}
|
|
1173
1346
|
*/
|
|
1174
1347
|
export function globalModelRefs(id) {
|
|
1175
1348
|
const lc = (typeof id === 'string' ? id : '').trim().toLowerCase();
|
|
1176
1349
|
if (PREDEFINED_MODELS.some((m) => m.id.toLowerCase() === lc)) {
|
|
1177
1350
|
return { predefinedShadow: true, steps: [], nodes: [] };
|
|
1178
1351
|
}
|
|
1179
|
-
|
|
1352
|
+
const defrag = memoryDefragModel().model;
|
|
1353
|
+
return {
|
|
1354
|
+
predefinedShadow: false,
|
|
1355
|
+
...refsForModelId(lc, allProjectConfigRows()),
|
|
1356
|
+
...(defrag && defrag.toLowerCase() === lc ? { memoryDefrag: true } : {}),
|
|
1357
|
+
};
|
|
1180
1358
|
}
|
|
1181
1359
|
|
|
1182
1360
|
/** Cross-project step/node refs to one lowercased model id, minus projects
|
|
@@ -1235,8 +1413,10 @@ export function referencedPluginModels(pluginName) {
|
|
|
1235
1413
|
* above). Ref purge and settings removal are not one transaction — a purge
|
|
1236
1414
|
* that lands without the removal (or vice versa on a crash) is harmless, since
|
|
1237
1415
|
* refs can be re-set and purging is idempotent.
|
|
1416
|
+
* Settings › Memory's defragment model is a ref too: it is cleared with the entry
|
|
1417
|
+
* (its effort with it) and the result says so with `clearedMemoryDefrag: true`.
|
|
1238
1418
|
* @param {string} id
|
|
1239
|
-
* @returns {Promise<{clearedSteps:number, clearedNodes:number, predefinedShadow:boolean}>}
|
|
1419
|
+
* @returns {Promise<{clearedSteps:number, clearedNodes:number, predefinedShadow:boolean, clearedMemoryDefrag?:true}>}
|
|
1240
1420
|
* @throws {Error} on an unknown id (from removeGlobalModel)
|
|
1241
1421
|
*/
|
|
1242
1422
|
export async function removeGlobalModelAndRefs(id) {
|
|
@@ -1289,6 +1469,7 @@ export async function removeGlobalModelAndRefs(id) {
|
|
|
1289
1469
|
}
|
|
1290
1470
|
});
|
|
1291
1471
|
}
|
|
1472
|
+
if (refs.memoryDefrag) await setMemoryDefragModel(null); // idempotent, like the purge above
|
|
1292
1473
|
await removeGlobalModel(id); // throws on unknown id — AFTER the idempotent purge
|
|
1293
|
-
return { clearedSteps, clearedNodes, predefinedShadow: refs.predefinedShadow };
|
|
1474
|
+
return { clearedSteps, clearedNodes, predefinedShadow: refs.predefinedShadow, ...(refs.memoryDefrag ? { clearedMemoryDefrag: true } : {}) };
|
|
1294
1475
|
}
|
package/src/core/cost-budget.mjs
CHANGED
|
@@ -87,12 +87,13 @@ export function allTimeTotals() {
|
|
|
87
87
|
const row = prepare(`
|
|
88
88
|
SELECT
|
|
89
89
|
COALESCE(SUM(CASE WHEN p.total_cost_usd > 0 THEN p.total_cost_usd ELSE COALESCE(s.sc, 0) END), 0) AS spend,
|
|
90
|
-
COALESCE(SUM(CASE WHEN p.total_active_ms > 0 THEN p.total_active_ms ELSE COALESCE(s.sa, 0) END), 0) AS active
|
|
90
|
+
COALESCE(SUM(CASE WHEN p.total_active_ms > 0 THEN p.total_active_ms ELSE COALESCE(s.sa, 0) END), 0) AS active,
|
|
91
|
+
COALESCE(SUM(p.human_hours), 0) AS humanHours
|
|
91
92
|
FROM pipelines p
|
|
92
93
|
LEFT JOIN (SELECT pipeline_id, SUM(cost_usd) sc, SUM(active_ms) sa
|
|
93
94
|
FROM pipeline_steps GROUP BY pipeline_id) s ON s.pipeline_id = p.id
|
|
94
95
|
`).get();
|
|
95
|
-
return { spendUsd: roundUsd(row?.spend || 0), activeMs: Number(row?.active || 0) };
|
|
96
|
+
return { spendUsd: roundUsd(row?.spend || 0), activeMs: Number(row?.active || 0), humanHours: Math.round(Number(row?.humanHours || 0) * 100) / 100 };
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
/** True when this pipeline ignores the per-pipeline cap (persistent, F7). */
|