@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/ask/events.mjs
CHANGED
|
@@ -50,6 +50,14 @@ const GIT_NAV_SUBCOMMANDS = new Set(['checkout', 'switch', 'fetch']);
|
|
|
50
50
|
// becomes the same `memory-changed` broadcast the REST routes emit (ui/server.mjs emitMemoryChanged).
|
|
51
51
|
// The scope key rides the tool RESULT (`scopeKey`), like pokeCommentWrite reads `comment.runId`.
|
|
52
52
|
const MEMORY_WRITE_TOOLS = new Set(['mcp__worca__remember', 'mcp__worca__forget']);
|
|
53
|
+
// …and the script writer (scripts-workbench-design.md §3.3, §9.1): save_script writes a file
|
|
54
|
+
// under ~/.worca-cc/scripts in the CHILD, so the parent turns a successful call into the same
|
|
55
|
+
// `scripts-changed` broadcast the REST routes emit — a script the chat saved shows up in an
|
|
56
|
+
// open Scripts tab. There is no delete tool, and test_script writes nothing outside the bench.
|
|
57
|
+
const SCRIPT_WRITE_TOOLS = new Set(['mcp__worca__save_script']);
|
|
58
|
+
// The direct schedule writes (docs/scheduled-runs.md "Ask Worca"): reversible, never a run start.
|
|
59
|
+
const SCHEDULE_WRITE_TOOLS = new Set(['mcp__worca__pause_schedule', 'mcp__worca__resume_schedule',
|
|
60
|
+
'mcp__worca__skip_next_run', 'mcp__worca__mark_schedule_activity_read']);
|
|
53
61
|
/** True when a SUCCESSFUL call of `name` with `input` changed this thread's worktree rows. */
|
|
54
62
|
export function worktreeMutatingCall(name, input) {
|
|
55
63
|
if (!WORKTREE_TOOLS.has(name)) return false;
|
|
@@ -128,6 +136,8 @@ export function labelForTool(name, input = {}, attachmentNames = {}) {
|
|
|
128
136
|
case 'get_team_metrics': return 'Reading team metrics';
|
|
129
137
|
case 'list_team_metrics_runs': return 'Listing team runs';
|
|
130
138
|
case 'push_team_metrics': return 'Pushing team metrics';
|
|
139
|
+
case 'get_team_policy': return 'Reading team policy';
|
|
140
|
+
case 'propose_policy_change': return 'Proposing a policy change';
|
|
131
141
|
case 'track_run': return 'Tracking a run';
|
|
132
142
|
case 'read_attachment': return `Reading ${(attachmentNames && attachmentNames[id]) || 'attachment'}`;
|
|
133
143
|
case 'list_diff_comments': return id ? `Reading comments on ${id.slice(0, 12)}` : 'Reading diff comments';
|
|
@@ -139,6 +149,27 @@ export function labelForTool(name, input = {}, attachmentNames = {}) {
|
|
|
139
149
|
case 'read_memory': return input?.name ? `Reading memory: ${input.name}` : 'Reading memory';
|
|
140
150
|
case 'remember': return input?.name ? `Saving memory: ${input.name}` : 'Saving memory';
|
|
141
151
|
case 'forget': return input?.name ? `Removing memory: ${input.name}` : 'Removing memory';
|
|
152
|
+
case 'list_scripts': return 'Looking at scripts';
|
|
153
|
+
case 'get_script': return input?.key ? `Reading script: ${input.key}` : 'Reading a script';
|
|
154
|
+
case 'save_script': return input?.key ? `Saving script: ${input.key}` : 'Saving a script';
|
|
155
|
+
case 'test_script': return input?.key ? `Testing script: ${input.key}` : 'Testing a script';
|
|
156
|
+
case 'list_schedules': return 'Looking at schedules';
|
|
157
|
+
case 'get_schedule': return 'Reading a schedule';
|
|
158
|
+
case 'list_schedule_activity': return 'Reading schedule activity';
|
|
159
|
+
case 'preview_schedule': return 'Working out the dates';
|
|
160
|
+
case 'propose_schedule_change': return 'Proposing a schedule change';
|
|
161
|
+
case 'pause_schedule': return 'Pausing a schedule';
|
|
162
|
+
case 'resume_schedule': return 'Resuming a schedule';
|
|
163
|
+
case 'skip_next_run': return 'Skipping the next run';
|
|
164
|
+
case 'mark_schedule_activity_read': return 'Marking activity read';
|
|
165
|
+
case 'list_task_sources': return 'Looking at task sources';
|
|
166
|
+
case 'find_tasks': return input?.search ? `Searching tasks: ${String(input.search).slice(0, 40)}` : 'Searching tasks';
|
|
167
|
+
case 'get_task': return input?.id ? `Reading task ${String(input.id).slice(0, 40)}` : 'Reading a task';
|
|
168
|
+
case 'list_models': return 'Looking at models';
|
|
169
|
+
case 'get_providers': return 'Looking at providers';
|
|
170
|
+
case 'test_provider': return input?.provider ? `Testing ${String(input.provider).slice(0, 20)}` : 'Testing a provider';
|
|
171
|
+
case 'list_copilot_models': return 'Listing Copilot models';
|
|
172
|
+
case 'propose_model_change': return 'Proposing a model change';
|
|
142
173
|
default: return `Using ${n}`;
|
|
143
174
|
}
|
|
144
175
|
}
|
|
@@ -149,6 +180,40 @@ const resultText = (content) => {
|
|
|
149
180
|
return '';
|
|
150
181
|
};
|
|
151
182
|
|
|
183
|
+
// ── script tools on the thread row (scripts-workbench-design.md §9.3) ─────────
|
|
184
|
+
const SCRIPT_TOOL_NAMES = new Set(['list_scripts', 'get_script', 'save_script', 'test_script']);
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The key a script tool was called with, stamped on the block at the CALL: a save_script input
|
|
188
|
+
* is a whole program, so past blockIoMaxChars the persisted input is the { _truncated, preview }
|
|
189
|
+
* stub and input.key is gone. '' for a script tool with no key, null for every other tool.
|
|
190
|
+
*/
|
|
191
|
+
export function scriptToolKey(name, input = {}) {
|
|
192
|
+
if (!SCRIPT_TOOL_NAMES.has(short(name))) return null;
|
|
193
|
+
return typeof input?.key === 'string' ? input.key.trim().slice(0, 64) : '';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* What a script tool's RESULT adds to its thread row: `save script runTests → created`,
|
|
198
|
+
* `test script runTests → blocking, exit 1`. Pure, tiny and enum-shaped on purpose — it is
|
|
199
|
+
* merged into the persisted block.script, so nothing free-text (which would need redaction)
|
|
200
|
+
* rides along. null = the row keeps its ordinary shape.
|
|
201
|
+
*/
|
|
202
|
+
export function scriptResultNote(name, text, isError = false) {
|
|
203
|
+
const n = short(name);
|
|
204
|
+
if (isError || (n !== 'save_script' && n !== 'test_script')) return null;
|
|
205
|
+
let parsed = null;
|
|
206
|
+
try { parsed = JSON.parse(text); } catch { return null; }
|
|
207
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
|
|
208
|
+
if (n === 'save_script') return { saved: parsed.ok === true ? (parsed.created === true ? 'created' : 'updated') : 'not saved' };
|
|
209
|
+
const r = parsed.ok === true && parsed.result && typeof parsed.result === 'object' ? parsed.result : null;
|
|
210
|
+
if (!r) return { status: 'not run' };
|
|
211
|
+
return {
|
|
212
|
+
status: typeof r.status === 'string' ? r.status.slice(0, 16) : null,
|
|
213
|
+
exitCode: Number.isInteger(r.exitCode) ? r.exitCode : null,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
152
217
|
/**
|
|
153
218
|
* @param {object} o
|
|
154
219
|
* @param {(frame:object)=>void} o.onFrame
|
|
@@ -178,10 +243,15 @@ export function createTurnReducer({
|
|
|
178
243
|
onWorkflowStart = null,
|
|
179
244
|
onWorkflowResult = null,
|
|
180
245
|
onMetricsProposal = null, // propose_metrics_change RESULT (team metrics card; the parent re-validates the input)
|
|
246
|
+
onPolicyProposal = null, // propose_policy_change RESULT (team policy card; same split)
|
|
247
|
+
onScheduleProposal = null, // propose_schedule_change RESULT (schedule card; the parent re-validates the input)
|
|
248
|
+
onModelProposal = null, // propose_model_change RESULT (model card; same split)
|
|
249
|
+
onScheduleMutation = null, // a direct schedule write succeeded in the MCP child
|
|
181
250
|
onTrackRun = null,
|
|
182
251
|
onCommentMutation = null,
|
|
183
252
|
onWorktreeMutation = null,
|
|
184
253
|
onMemoryMutation = null,
|
|
254
|
+
onScriptMutation = null, // save_script RESULT { ok: true, key, created } → { key, action }
|
|
185
255
|
estimateLiveCost = null,
|
|
186
256
|
attachmentNames = {},
|
|
187
257
|
resolveCost = null,
|
|
@@ -368,7 +438,9 @@ export function createTurnReducer({
|
|
|
368
438
|
} else {
|
|
369
439
|
fullInputs.set(c.id, input);
|
|
370
440
|
label(labelForTool(c.name, input, attachmentNames));
|
|
371
|
-
|
|
441
|
+
const scriptKey = scriptToolKey(c.name, input);
|
|
442
|
+
upsertBlock({ kind: 'tool', id: c.id, name: c.name, input: clipJson(input, limits.blockIoMaxChars), status: 'running', durationMs: null,
|
|
443
|
+
...(scriptKey === null ? {} : { script: { key: scriptKey } }) }); // §9.3: the row's key, whatever the clip does
|
|
372
444
|
// P3: the workflow card exists from the tool_use on (state 'building' — the four-step trace), so the
|
|
373
445
|
// START is a hook too. Sync: the block must precede any frame the tool result produces.
|
|
374
446
|
if (c.name === 'mcp__worca__propose_workflow' && typeof onWorkflowStart === 'function') {
|
|
@@ -425,6 +497,25 @@ export function createTurnReducer({
|
|
|
425
497
|
} catch { /* unparseable result — no poke; the next open refetches anyway */ }
|
|
426
498
|
}
|
|
427
499
|
|
|
500
|
+
// And for scripts: save_script REFUSES by returning { ok: false, errors } with no is_error
|
|
501
|
+
// flag (that is what lets the model correct itself), so the result body — not the flag — is
|
|
502
|
+
// what decides whether anything was written.
|
|
503
|
+
function pokeScriptWrite(name, text, isError) {
|
|
504
|
+
if (isError || !SCRIPT_WRITE_TOOLS.has(name) || typeof onScriptMutation !== 'function') return;
|
|
505
|
+
try {
|
|
506
|
+
const parsed = JSON.parse(text);
|
|
507
|
+
if (!parsed || parsed.ok !== true || typeof parsed.key !== 'string' || !parsed.key) return;
|
|
508
|
+
onScriptMutation({ key: parsed.key, action: parsed.created === true ? 'created' : 'updated' });
|
|
509
|
+
} catch { /* unparseable result — no poke; the next open refetches anyway */ }
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// And for schedules: pause / resume / skip / mark-read succeeded in the CHILD, so the parent
|
|
513
|
+
// broadcasts the same schedules-changed / notifications-changed frames the REST routes emit.
|
|
514
|
+
function pokeScheduleWrite(name, isError) {
|
|
515
|
+
if (isError || !SCHEDULE_WRITE_TOOLS.has(name) || typeof onScheduleMutation !== 'function') return;
|
|
516
|
+
try { onScheduleMutation({ tool: short(name) }); } catch { /* a broken sink never breaks the stream */ }
|
|
517
|
+
}
|
|
518
|
+
|
|
428
519
|
function onUser(raw, ptu, isMain) {
|
|
429
520
|
const content = Array.isArray(raw.message?.content) ? raw.message.content : [];
|
|
430
521
|
for (const c of content) {
|
|
@@ -439,6 +530,8 @@ export function createTurnReducer({
|
|
|
439
530
|
pokeCommentWrite(ct.name, text, c.is_error);
|
|
440
531
|
pokeWorktreeMutation(ct.name, ct.input, c.is_error);
|
|
441
532
|
pokeMemoryWrite(ct.name, text, c.is_error);
|
|
533
|
+
pokeScriptWrite(ct.name, text, c.is_error);
|
|
534
|
+
pokeScheduleWrite(ct.name, c.is_error);
|
|
442
535
|
continue;
|
|
443
536
|
}
|
|
444
537
|
const b = byId.get(c.tool_use_id);
|
|
@@ -465,6 +558,8 @@ export function createTurnReducer({
|
|
|
465
558
|
b.status = c.is_error ? 'error' : 'done';
|
|
466
559
|
b.durationMs = elapsed(b.id);
|
|
467
560
|
if (c.is_error) b.error = redact(clipStr(text, limits.blockIoMaxChars));
|
|
561
|
+
const note = scriptResultNote(b.name, text, c.is_error);
|
|
562
|
+
if (note) b.script = { ...(b.script || {}), ...note }; // §9.3: the row shows what came back
|
|
468
563
|
upsertBlock(b);
|
|
469
564
|
if (b.name === 'mcp__worca__propose_run' && typeof onProposal === 'function') {
|
|
470
565
|
let childOk = null;
|
|
@@ -490,6 +585,27 @@ export function createTurnReducer({
|
|
|
490
585
|
if (ret && typeof ret.then === 'function') pendingHooks.push(ret.then(() => {}, () => { reducerErrors += 1; }));
|
|
491
586
|
} catch { reducerErrors += 1; }
|
|
492
587
|
}
|
|
588
|
+
if (b.name === 'mcp__worca__propose_policy_change' && typeof onPolicyProposal === 'function') {
|
|
589
|
+
// Same split as the metrics card: the parent re-validates from the INPUT (policy-proposal.mjs).
|
|
590
|
+
try {
|
|
591
|
+
const ret = onPolicyProposal({ toolUseId: b.id, input: fullInputs.get(b.id) ?? {}, text, isError: !!c.is_error });
|
|
592
|
+
if (ret && typeof ret.then === 'function') pendingHooks.push(ret.then(() => {}, () => { reducerErrors += 1; }));
|
|
593
|
+
} catch { reducerErrors += 1; }
|
|
594
|
+
}
|
|
595
|
+
if (b.name === 'mcp__worca__propose_schedule_change' && typeof onScheduleProposal === 'function') {
|
|
596
|
+
// Same split as the metrics card: the parent re-validates the INPUT against the live rows.
|
|
597
|
+
try {
|
|
598
|
+
const ret = onScheduleProposal({ toolUseId: b.id, input: fullInputs.get(b.id) ?? {}, text, isError: !!c.is_error });
|
|
599
|
+
if (ret && typeof ret.then === 'function') pendingHooks.push(ret.then(() => {}, () => { reducerErrors += 1; }));
|
|
600
|
+
} catch { reducerErrors += 1; }
|
|
601
|
+
}
|
|
602
|
+
if (b.name === 'mcp__worca__propose_model_change' && typeof onModelProposal === 'function') {
|
|
603
|
+
// Same split as the metrics card: the parent re-validates the INPUT over the real catalog (model-proposal.mjs).
|
|
604
|
+
try {
|
|
605
|
+
const ret = onModelProposal({ toolUseId: b.id, input: fullInputs.get(b.id) ?? {}, text, isError: !!c.is_error });
|
|
606
|
+
if (ret && typeof ret.then === 'function') pendingHooks.push(ret.then(() => {}, () => { reducerErrors += 1; }));
|
|
607
|
+
} catch { reducerErrors += 1; }
|
|
608
|
+
}
|
|
493
609
|
if (b.name === 'mcp__worca__track_run' && typeof onTrackRun === 'function') {
|
|
494
610
|
// The parent owns the runs Map, the link rows and the followers: it re-resolves the id itself (D4).
|
|
495
611
|
try {
|
|
@@ -500,6 +616,8 @@ export function createTurnReducer({
|
|
|
500
616
|
pokeCommentWrite(b.name, text, c.is_error);
|
|
501
617
|
pokeWorktreeMutation(b.name, fullInputs.get(b.id), c.is_error);
|
|
502
618
|
pokeMemoryWrite(b.name, text, c.is_error);
|
|
619
|
+
pokeScriptWrite(b.name, text, c.is_error);
|
|
620
|
+
pokeScheduleWrite(b.name, c.is_error);
|
|
503
621
|
}
|
|
504
622
|
}
|
|
505
623
|
|
package/src/core/ask/limits.mjs
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
// Fixed limits of the Ask Worca chat (ask-worca-design.md §6.9) plus the two
|
|
3
3
|
// operator-configurable per-turn guards, read fresh on every turn (D12). Pure
|
|
4
4
|
// apart from the settings readers, which are injectable for tests.
|
|
5
|
-
import { askMaxTurns as readAskMaxTurns, askMaxBudgetUsd as readAskMaxBudgetUsd } from '../settings.mjs';
|
|
5
|
+
import { askMaxTurns as readAskMaxTurns, askMaxBudgetUsd as readAskMaxBudgetUsd, readSettings } from '../settings.mjs';
|
|
6
|
+
import { cachedPolicyForKey } from '../policy/cache.mjs';
|
|
7
|
+
import { fieldsForRun } from '../policy/effective.mjs';
|
|
6
8
|
import { TEXT_EXTENSIONS, BINARY_EXTENSIONS } from './attachment-kind.mjs';
|
|
7
9
|
|
|
8
10
|
export const ASK_LIMITS = Object.freeze({
|
|
@@ -38,6 +40,18 @@ export const ASK_LIMITS = Object.freeze({
|
|
|
38
40
|
artifactsListMaxLimit: 200,
|
|
39
41
|
artifactReadDefaultBytes: 60_000,
|
|
40
42
|
artifactReadMaxBytes: 200_000,
|
|
43
|
+
// Scripts in the chat (scripts-workbench-design.md §9.1). The store's own caps (source
|
|
44
|
+
// 256 KiB, 32 cases) are NOT repeated here: script-store / script-cases own those.
|
|
45
|
+
scriptListMaxRows: 200, // list_scripts rows (= artifactsListMaxLimit)
|
|
46
|
+
scriptSourceDefaultBytes: 60_000, // get_script page (= diffDefaultBytes)
|
|
47
|
+
scriptSourceMaxBytes: 200_000, // get_script page cap (= diffMaxBytes)
|
|
48
|
+
scriptLogMaxLines: 200, // test_script: the LAST N streamed lines…
|
|
49
|
+
scriptLogMaxBytes: 16 * 1024, // …and their byte cap (the tail is kept: a failure ends the log)
|
|
50
|
+
scriptOutputMaxBytes: 16 * 1024, // test_script: per output port (the head is kept)
|
|
51
|
+
scriptTestDefaultTimeoutSec: 120, // test_script timeoutSec default
|
|
52
|
+
scriptTestMaxTimeoutSec: 600, // …and its ceiling (= the engine's 10-minute default)
|
|
53
|
+
scriptVerdictMaxIssues: 50, // test_script: verdict issues sent (the rest is counted, not sent)
|
|
54
|
+
scriptResultFieldMaxChars: 2000, // test_script: per verdict field / warning / diff / error line (chars)
|
|
41
55
|
briefMaxChars: 8000,
|
|
42
56
|
metricsRunsDefaultLimit: 20, // list_team_metrics_runs page (= listRunsDefaultLimit)
|
|
43
57
|
metricsRunsMaxLimit: 100, // list_team_metrics_runs page cap (= listRunsMaxLimit)
|
|
@@ -52,7 +66,7 @@ export const ASK_LIMITS = Object.freeze({
|
|
|
52
66
|
headerAttachments: 5,
|
|
53
67
|
deltaBatchMs: 50,
|
|
54
68
|
deltaBatchChars: 256,
|
|
55
|
-
defaultModel: 'claude-opus-5',
|
|
69
|
+
defaultModel: 'claude-opus-5-5', // D8
|
|
56
70
|
defaultEffort: 'high',
|
|
57
71
|
});
|
|
58
72
|
|
|
@@ -61,6 +75,20 @@ export const ASK_LIMITS = Object.freeze({
|
|
|
61
75
|
* applies to the next turn without a restart.
|
|
62
76
|
* @returns {{maxTurns:number, maxBudgetUsd:number|null}}
|
|
63
77
|
*/
|
|
64
|
-
export function askLimits({ readMaxTurns = readAskMaxTurns, readMaxBudgetUsd = readAskMaxBudgetUsd } = {}) {
|
|
65
|
-
|
|
78
|
+
export function askLimits({ readMaxTurns = readAskMaxTurns, readMaxBudgetUsd = readAskMaxBudgetUsd, projectKey = null, readStored = readSettings } = {}) {
|
|
79
|
+
const out = { maxTurns: readMaxTurns(), maxBudgetUsd: readMaxBudgetUsd() };
|
|
80
|
+
// Team policy defaults (team-policy design §5 `ask.*`): start a thread pinned to a governed
|
|
81
|
+
// project off the team's numbers, but only where the developer has stored nothing of their own.
|
|
82
|
+
if (projectKey) {
|
|
83
|
+
const p = cachedPolicyForKey(projectKey);
|
|
84
|
+
if (p) {
|
|
85
|
+
const f = fieldsForRun(p.doc);
|
|
86
|
+
const stored = readStored() || {};
|
|
87
|
+
const t = f['ask.maxTurns'];
|
|
88
|
+
if (t && t.kind === 'default' && stored.askMaxTurns === undefined) out.maxTurns = t.value;
|
|
89
|
+
const b = f['ask.maxBudgetUsd'];
|
|
90
|
+
if (b && b.kind === 'default' && stored.askMaxBudgetUsd === undefined) out.maxBudgetUsd = b.value;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
66
94
|
}
|
|
@@ -25,9 +25,14 @@ import { createAskTools, AskToolError } from './tools.mjs';
|
|
|
25
25
|
import { defaultToolDeps } from './tool-deps.mjs';
|
|
26
26
|
import { defaultWorktreeDeps } from './worktree-deps.mjs';
|
|
27
27
|
import { defaultMemoryDeps } from './memory-deps.mjs';
|
|
28
|
+
import { defaultScriptDeps } from './script-deps.mjs';
|
|
28
29
|
import { defaultCommentDeps } from './comment-deps.mjs';
|
|
29
30
|
import { defaultWorkflowDeps } from './workflow-deps.mjs';
|
|
30
31
|
import { defaultMetricsDeps } from './metrics-deps.mjs';
|
|
32
|
+
import { defaultPolicyDeps } from './policy-deps.mjs';
|
|
33
|
+
import { defaultScheduleDeps } from './schedule-deps.mjs';
|
|
34
|
+
import { defaultSourceDeps } from './source-deps.mjs';
|
|
35
|
+
import { defaultModelDeps } from './model-deps.mjs';
|
|
31
36
|
|
|
32
37
|
const SUPPORTED_PROTOCOLS = Object.freeze(['2024-11-05', '2025-03-26', '2025-06-18', '2025-11-25']);
|
|
33
38
|
const DEFAULT_PROTOCOL = '2025-06-18';
|
|
@@ -123,9 +128,16 @@ export async function main({ argv = process.argv.slice(2), env = process.env, st
|
|
|
123
128
|
...defaultToolDeps({ threadId }),
|
|
124
129
|
...defaultWorktreeDeps({ threadId }),
|
|
125
130
|
...defaultMemoryDeps({ threadId }),
|
|
131
|
+
// The life signal (already built for propose_workflow's nested classifier): stdin closing
|
|
132
|
+
// means the turn ended or was stopped, and a bench run still going is stopped with it.
|
|
133
|
+
...defaultScriptDeps({ threadId, signal: life.signal }),
|
|
126
134
|
...defaultCommentDeps(),
|
|
127
135
|
...defaultWorkflowDeps({ threadId, signal: life.signal }),
|
|
128
136
|
...defaultMetricsDeps({ threadId }),
|
|
137
|
+
...defaultPolicyDeps({ threadId }),
|
|
138
|
+
...defaultScheduleDeps({ threadId }),
|
|
139
|
+
...defaultSourceDeps(),
|
|
140
|
+
...defaultModelDeps({ threadId }),
|
|
129
141
|
}),
|
|
130
142
|
write: (s) => stdout.write(s),
|
|
131
143
|
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// src/core/ask/model-deps.mjs
|
|
2
|
+
// The models + providers dependency bundle of the Ask Worca tools (docs/models.md "Ask Worca"):
|
|
3
|
+
// the ONE module that touches the catalog and the bridge on the tools' behalf (tools.mjs may not
|
|
4
|
+
// import anything). Two callers:
|
|
5
|
+
// • the MCP child (mcp-stdio.mjs): deps.models.* behind list_models, get_providers,
|
|
6
|
+
// test_provider, list_copilot_models and propose_model_change (validate only);
|
|
7
|
+
// • the parent: turn.mjs re-validates a proposal with validateModelChange, and the cards route
|
|
8
|
+
// applies a confirmed card with applyModelChange.
|
|
9
|
+
// Never a credential out: env values and keys are masked unless they are ${VAR} references.
|
|
10
|
+
import { listModels, PREDEFINED_MODELS, EFFORTS, globalModelRefs, removeGlobalModelAndRefs } from '../config.mjs';
|
|
11
|
+
import { listGlobalModels, addGlobalModel, updateGlobalModel, updateProvider, providerConfig } from '../settings.mjs';
|
|
12
|
+
import { listPluginModels } from '../plugin-models.mjs';
|
|
13
|
+
import { policyCatalogModels } from '../policy/cache.mjs';
|
|
14
|
+
import { providerReadiness } from '../bridge/registry.mjs';
|
|
15
|
+
import {
|
|
16
|
+
providersState, patchProvider, copilotModelsForImport, importCopilotModels, testProviderConnection,
|
|
17
|
+
endpointModelsForImport, importEndpointModels,
|
|
18
|
+
} from '../bridge/provider-ops.mjs';
|
|
19
|
+
import { UPSTREAM_PROVIDERS } from '../model-env.mjs';
|
|
20
|
+
import { createModelChangeValidator, mergeEditPatch, maskEntry } from './model-proposal.mjs';
|
|
21
|
+
|
|
22
|
+
const envHas = (name) => typeof process.env[name] === 'string' && process.env[name].trim() !== '';
|
|
23
|
+
|
|
24
|
+
/** The authoritative validator, over the real readers (the turn's default; the child's too). */
|
|
25
|
+
export const validateModelChange = createModelChangeValidator({
|
|
26
|
+
listGlobalModels, listPluginModels, policyModels: policyCatalogModels, predefined: PREDEFINED_MODELS,
|
|
27
|
+
addModel: addGlobalModel, updateModel: updateGlobalModel, updateProvider,
|
|
28
|
+
providerConfig, providerReadiness, modelRefs: globalModelRefs, envHas,
|
|
29
|
+
copilotModels: () => copilotModelsForImport(),
|
|
30
|
+
endpointModels: (baseUrl) => endpointModelsForImport({ baseUrl }),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Every catalog model as list_models shows it: the picker row (source, efforts, bridge facts,
|
|
35
|
+
* readiness) joined with the entry's own masked config when it is a user (global) entry.
|
|
36
|
+
*/
|
|
37
|
+
export async function listModelsForAsk() {
|
|
38
|
+
const rows = await listModels('');
|
|
39
|
+
const globals = new Map(listGlobalModels().map((m) => [m.id.toLowerCase(), m]));
|
|
40
|
+
const plugins = new Map(listPluginModels().map((m) => [m.id.toLowerCase(), m]));
|
|
41
|
+
const source = { false: 'built-in', global: 'user', plugin: 'plugin', policy: 'team policy', project: 'project (legacy)' };
|
|
42
|
+
const models = rows.map((row) => {
|
|
43
|
+
const g = globals.get(row.id.toLowerCase());
|
|
44
|
+
const p = !g ? plugins.get(row.id.toLowerCase()) : null;
|
|
45
|
+
const entry = g ? maskEntry(g) : null;
|
|
46
|
+
const up = (g && g.upstream) || (p && p.upstream) || null;
|
|
47
|
+
return {
|
|
48
|
+
id: row.id, label: row.label, source: source[String(row.custom)] || String(row.custom),
|
|
49
|
+
...(row.plugin ? { plugin: row.plugin } : {}), ...(row.policy ? { policyHome: row.policy } : {}),
|
|
50
|
+
editable: !!g, efforts: row.efforts,
|
|
51
|
+
connection: up ? 'provider' : row.routed ? 'env' : 'default',
|
|
52
|
+
...(row.bridged ? { provider: row.bridged, upstreamApi: row.upstreamApi, upstreamModel: row.upstreamModel } : {}),
|
|
53
|
+
...(row.bridged ? { ready: !row.needsSignIn, ...(row.needsSignIn && up ? { notReady: providerReadiness(up).message } : {}) } : {}),
|
|
54
|
+
...(row.capabilities ? { capabilities: row.capabilities } : {}),
|
|
55
|
+
...(row.hidden ? { hidden: true } : {}), ...(row.costUnreliable ? { costUnreliable: true } : {}),
|
|
56
|
+
...(entry ? { entry: { env: entry.env || {}, ...(entry.upstream ? { upstream: entry.upstream } : {}), ...(entry.cost ? { cost: entry.cost } : {}) } } : {}),
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
return { models, efforts: [...EFFORTS], providers: [...UPSTREAM_PROVIDERS] };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Apply a CONFIRMED model card (ui/server.mjs cards route, after the user's click). Every setter
|
|
64
|
+
* re-validates; an edit re-merges its patch onto the entry as it is NOW. Returns a result the card
|
|
65
|
+
* renders and the event turn quotes; throws on failure.
|
|
66
|
+
*/
|
|
67
|
+
export async function applyModelChange(card, io = {}) {
|
|
68
|
+
const c = card.change || {};
|
|
69
|
+
switch (card.kind) {
|
|
70
|
+
case 'add_model': {
|
|
71
|
+
const m = await (io.addModel ?? addGlobalModel)(c.model);
|
|
72
|
+
return { ok: true, detail: `${m.id} is in the catalog` };
|
|
73
|
+
}
|
|
74
|
+
case 'edit_model': {
|
|
75
|
+
const current = (io.listGlobalModels ?? listGlobalModels)().find((m) => m.id.toLowerCase() === String(c.id).toLowerCase());
|
|
76
|
+
if (!current) throw new Error(`model "${c.id}" is no longer in the catalog`);
|
|
77
|
+
await (io.updateModel ?? updateGlobalModel)(current.id, mergeEditPatch(current, c.patch || {}));
|
|
78
|
+
return { ok: true, detail: `${current.id} updated` };
|
|
79
|
+
}
|
|
80
|
+
case 'remove_model': {
|
|
81
|
+
const r = await (io.removeModel ?? removeGlobalModelAndRefs)(c.id);
|
|
82
|
+
const n = (r?.clearedSteps || 0) + (r?.clearedNodes || 0);
|
|
83
|
+
// The event turn quotes this detail: it names the Settings › Memory ref it cleared too.
|
|
84
|
+
return { ok: true, detail: `${c.id} removed${n ? ` · ${n} workflow selection${n === 1 ? '' : 's'} cleared` : ''}${r?.clearedMemoryDefrag ? ' · Settings › Memory defragment model cleared' : ''}` };
|
|
85
|
+
}
|
|
86
|
+
case 'provider': {
|
|
87
|
+
await (io.patchProvider ?? patchProvider)(c.provider, c.set || {});
|
|
88
|
+
return { ok: true, detail: `${c.provider} provider saved` };
|
|
89
|
+
}
|
|
90
|
+
case 'import_endpoint': {
|
|
91
|
+
const r = await (io.importEndpoint ?? importEndpointModels)(c.ids || [], { baseUrl: c.baseUrl || '' });
|
|
92
|
+
const bits = [];
|
|
93
|
+
if (r.created?.length) bits.push(`added ${r.created.join(', ')}`);
|
|
94
|
+
if (r.updated?.length) bits.push(`refreshed ${r.updated.join(', ')}`);
|
|
95
|
+
// The endpoint's skips carry a reason (an embedding model, no tool calls); Copilot's are ids.
|
|
96
|
+
if (r.skipped?.length) bits.push(`skipped ${r.skipped.map((s) => (typeof s === 'string' ? s : `${s.id} (${s.why})`)).join(', ')}`);
|
|
97
|
+
return { ok: true, detail: `${bits.join(' · ') || 'nothing to import'} — from ${r.serverLabel || r.baseUrl || 'the endpoint'}` };
|
|
98
|
+
}
|
|
99
|
+
case 'import_copilot': {
|
|
100
|
+
const r = await (io.importCopilot ?? importCopilotModels)(c.ids || []);
|
|
101
|
+
const bits = [];
|
|
102
|
+
if (r.created?.length) bits.push(`added ${r.created.join(', ')}`);
|
|
103
|
+
if (r.updated?.length) bits.push(`refreshed ${r.updated.join(', ')}`);
|
|
104
|
+
if (r.skipped?.length) bits.push(`skipped ${r.skipped.join(', ')}`);
|
|
105
|
+
return { ok: true, detail: bits.join(' · ') || 'nothing to import' };
|
|
106
|
+
}
|
|
107
|
+
default:
|
|
108
|
+
throw new Error(`unknown model change kind "${card.kind}"`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {{threadId?:string|null}} [o] threadId unused today; keeps the *-deps.mjs signature uniform
|
|
114
|
+
*/
|
|
115
|
+
export function defaultModelDeps({ threadId = null } = {}) { // eslint-disable-line no-unused-vars
|
|
116
|
+
return {
|
|
117
|
+
models: {
|
|
118
|
+
list: listModelsForAsk,
|
|
119
|
+
providers: () => providersState(),
|
|
120
|
+
test: (name) => testProviderConnection(name),
|
|
121
|
+
copilotModels: () => copilotModelsForImport(),
|
|
122
|
+
endpointModels: (baseUrl) => endpointModelsForImport({ baseUrl }),
|
|
123
|
+
validateChange: validateModelChange,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|