@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.
Files changed (187) hide show
  1. package/README.md +85 -6
  2. package/agents/clarify.meta.json +1 -0
  3. package/agents/memoryDefragmenter.meta.json +2 -1
  4. package/agents/reviewer.meta.json +60 -0
  5. package/agents/worca-cc-code-reviewer.md +33 -0
  6. package/agents/worca-cc-memory-defragmenter.md +5 -3
  7. package/agents/workspaceScanner.meta.json +1 -0
  8. package/package.json +14 -10
  9. package/scripts/git-diff.mjs +25 -0
  10. package/scripts/gitDiff.meta.json +18 -0
  11. package/scripts/js-inline.mjs +11 -0
  12. package/scripts/js.meta.json +22 -0
  13. package/scripts/py-inline.py +27 -0
  14. package/scripts/py.meta.json +22 -0
  15. package/scripts/shell.meta.json +24 -0
  16. package/skills/worca/SKILL.md +3 -2
  17. package/src/cli/models.mjs +247 -0
  18. package/src/cli/render.mjs +72 -4
  19. package/src/cli/schedule.mjs +494 -0
  20. package/src/cli/worca-cc.mjs +1001 -22
  21. package/src/core/agent-registry.mjs +75 -23
  22. package/src/core/agent-store.mjs +51 -2
  23. package/src/core/artifacts.mjs +73 -10
  24. package/src/core/ask/events.mjs +119 -1
  25. package/src/core/ask/limits.mjs +32 -4
  26. package/src/core/ask/mcp-stdio.mjs +12 -0
  27. package/src/core/ask/model-deps.mjs +126 -0
  28. package/src/core/ask/model-proposal.mjs +370 -0
  29. package/src/core/ask/models.mjs +12 -0
  30. package/src/core/ask/policy-deps.mjs +124 -0
  31. package/src/core/ask/policy-proposal.mjs +363 -0
  32. package/src/core/ask/prompt.mjs +74 -9
  33. package/src/core/ask/proposal.mjs +54 -5
  34. package/src/core/ask/schedule-deps.mjs +83 -0
  35. package/src/core/ask/schedule-spec.mjs +310 -0
  36. package/src/core/ask/script-deps.mjs +357 -0
  37. package/src/core/ask/source-deps.mjs +52 -0
  38. package/src/core/ask/source-spec.mjs +157 -0
  39. package/src/core/ask/spawn.mjs +1 -0
  40. package/src/core/ask/store.mjs +6 -3
  41. package/src/core/ask/tool-deps.mjs +4 -0
  42. package/src/core/ask/tools.mjs +657 -37
  43. package/src/core/ask/turn.mjs +109 -2
  44. package/src/core/ask-files.mjs +406 -0
  45. package/src/core/ask-forms.mjs +195 -0
  46. package/src/core/ask-projection.mjs +72 -0
  47. package/src/core/bridge/errors.mjs +84 -0
  48. package/src/core/bridge/provider-ops.mjs +281 -0
  49. package/src/core/bridge/providers/copilot.mjs +269 -0
  50. package/src/core/bridge/providers/endpoint.mjs +257 -0
  51. package/src/core/bridge/registry.mjs +88 -0
  52. package/src/core/bridge/semaphore.mjs +73 -0
  53. package/src/core/bridge/server.mjs +184 -0
  54. package/src/core/bridge/telemetry.mjs +53 -0
  55. package/src/core/bridge/translate/request.mjs +252 -0
  56. package/src/core/bridge/translate/response.mjs +82 -0
  57. package/src/core/bridge/translate/stream.mjs +242 -0
  58. package/src/core/bridge/upstream.mjs +209 -0
  59. package/src/core/chat/command-router.mjs +58 -4
  60. package/src/core/chat/notifier.mjs +14 -1
  61. package/src/core/chat/renderers.mjs +35 -0
  62. package/src/core/claude-runner.mjs +126 -19
  63. package/src/core/config.mjs +212 -31
  64. package/src/core/cost-budget.mjs +3 -2
  65. package/src/core/db.mjs +169 -15
  66. package/src/core/failure-policy.mjs +10 -0
  67. package/src/core/fs-browse.mjs +16 -4
  68. package/src/core/git-info.mjs +22 -0
  69. package/src/core/graph/builtin-workflows.mjs +3 -1
  70. package/src/core/graph/exec-io.mjs +71 -0
  71. package/src/core/graph/executor.mjs +139 -70
  72. package/src/core/graph/human-evidence.mjs +131 -0
  73. package/src/core/graph/python-probe.mjs +172 -0
  74. package/src/core/graph/registry-ports.mjs +10 -6
  75. package/src/core/graph/scheduler.mjs +39 -24
  76. package/src/core/graph/script-child.mjs +81 -0
  77. package/src/core/graph/script-runner.mjs +597 -0
  78. package/src/core/graph/worca_script.py +207 -0
  79. package/src/core/guardrail-store.mjs +16 -0
  80. package/src/core/human-backfill.mjs +108 -0
  81. package/src/core/human-rate.mjs +17 -0
  82. package/src/core/index-html.mjs +6 -2
  83. package/src/core/memory-defrag-model.mjs +112 -0
  84. package/src/core/memory-store.mjs +70 -18
  85. package/src/core/memory-sync.mjs +22 -11
  86. package/src/core/metrics/read.mjs +4 -1
  87. package/src/core/metrics/record.mjs +50 -2
  88. package/src/core/metrics/sync.mjs +6 -4
  89. package/src/core/model-env.mjs +149 -0
  90. package/src/core/model-test.mjs +14 -1
  91. package/src/core/notifications.mjs +128 -0
  92. package/src/core/onboarding.mjs +8 -2
  93. package/src/core/orchestrator.mjs +357 -26
  94. package/src/core/phases.mjs +95 -6
  95. package/src/core/plugin-api.mjs +24 -7
  96. package/src/core/plugin-manifest.mjs +184 -18
  97. package/src/core/plugin-models.mjs +1 -0
  98. package/src/core/plugin-script-cases.mjs +118 -0
  99. package/src/core/plugin-store.mjs +163 -17
  100. package/src/core/plugin-workflows.mjs +71 -17
  101. package/src/core/policy/cache.mjs +116 -0
  102. package/src/core/policy/effective.mjs +175 -0
  103. package/src/core/policy/gate.mjs +91 -0
  104. package/src/core/policy/local.mjs +145 -0
  105. package/src/core/policy/registry.mjs +330 -0
  106. package/src/core/policy/scope.mjs +61 -0
  107. package/src/core/policy/state.mjs +79 -0
  108. package/src/core/policy/sync.mjs +513 -0
  109. package/src/core/protocol.mjs +43 -0
  110. package/src/core/run-harness.mjs +421 -72
  111. package/src/core/scheduler.mjs +980 -0
  112. package/src/core/script-bench.mjs +628 -0
  113. package/src/core/script-registry.mjs +116 -0
  114. package/src/core/script-store.mjs +563 -0
  115. package/src/core/settings.mjs +489 -14
  116. package/src/core/stats.mjs +33 -2
  117. package/src/core/workflow-export.mjs +94 -3
  118. package/src/core/workflow-share.mjs +67 -18
  119. package/src/core/workflows.mjs +47 -11
  120. package/src/core/workspaces.mjs +18 -12
  121. package/src/shared/forms/answer.mjs +164 -0
  122. package/src/shared/forms/catalog.mjs +91 -0
  123. package/src/shared/forms/form-def.mjs +290 -0
  124. package/src/shared/forms/layout.mjs +67 -0
  125. package/src/shared/forms/paths.mjs +47 -0
  126. package/src/shared/forms/project.mjs +309 -0
  127. package/src/shared/forms/schema.mjs +205 -0
  128. package/src/shared/graph/agent-meta.mjs +55 -5
  129. package/src/shared/graph/constants.mjs +14 -2
  130. package/src/shared/graph/flow-layout.mjs +2 -1
  131. package/src/shared/graph/isomorphic.mjs +5 -3
  132. package/src/shared/graph/manifest.mjs +22 -13
  133. package/src/shared/graph/ports.mjs +45 -19
  134. package/src/shared/graph/script-cases.mjs +257 -0
  135. package/src/shared/graph/script-icons.mjs +46 -0
  136. package/src/shared/graph/script-infer.mjs +259 -0
  137. package/src/shared/graph/script-meta.mjs +408 -0
  138. package/src/shared/graph/script-templates.mjs +201 -0
  139. package/src/shared/graph/template.mjs +4 -4
  140. package/src/shared/graph/validate.mjs +89 -16
  141. package/src/shared/human-estimate.mjs +100 -0
  142. package/src/shared/schedule/recurrence.mjs +353 -0
  143. package/src/shared/team-metrics/aggregate.mjs +51 -11
  144. package/{scripts → tools}/install.mjs +3 -3
  145. package/ui/public/app.js +4390 -683
  146. package/ui/public/artifact-picker.mjs +189 -0
  147. package/ui/public/ask/dom.mjs +121 -0
  148. package/ui/public/ask/form-preview.mjs +55 -0
  149. package/ui/public/ask/form-renderer.mjs +250 -0
  150. package/ui/public/ask/registry.mjs +53 -0
  151. package/ui/public/ask/widgets-display.mjs +370 -0
  152. package/ui/public/ask/widgets-input.mjs +624 -0
  153. package/ui/public/ask/widgets-layout.mjs +90 -0
  154. package/ui/public/ask-panel.mjs +401 -27
  155. package/ui/public/ask-run-card.mjs +1 -1
  156. package/ui/public/bridge-view.mjs +694 -0
  157. package/ui/public/chat-settings-view.mjs +24 -0
  158. package/ui/public/code-editor.mjs +181 -0
  159. package/ui/public/getting-started.mjs +34 -7
  160. package/ui/public/graph/composer.mjs +138 -10
  161. package/ui/public/graph/inspector.mjs +61 -58
  162. package/ui/public/graph/palette.mjs +27 -9
  163. package/ui/public/graph/run-decor.mjs +34 -17
  164. package/ui/public/graph/run-hosts.mjs +7 -1
  165. package/ui/public/graph/save-dialog.mjs +3 -0
  166. package/ui/public/graph/view.mjs +23 -7
  167. package/ui/public/guardrails-view.mjs +15 -3
  168. package/ui/public/guide-spot.mjs +87 -9
  169. package/ui/public/index.html +480 -141
  170. package/ui/public/memory-view.mjs +22 -4
  171. package/ui/public/models-view.mjs +162 -17
  172. package/ui/public/node-tunables.mjs +33 -4
  173. package/ui/public/plugins-view.mjs +23 -1
  174. package/ui/public/results-view.mjs +4 -2
  175. package/ui/public/schedule-sheet.mjs +430 -0
  176. package/ui/public/schedules-view.mjs +432 -0
  177. package/ui/public/script-bench-view.mjs +1154 -0
  178. package/ui/public/script-forms.mjs +282 -0
  179. package/ui/public/script-wizard.mjs +529 -0
  180. package/ui/public/scripts-view.mjs +868 -0
  181. package/ui/public/stats-view.mjs +159 -52
  182. package/ui/public/style.css +1461 -44
  183. package/ui/public/team-metrics-surfaces.mjs +77 -16
  184. package/ui/public/team-metrics-view.mjs +68 -5
  185. package/ui/public/team-policy-view.mjs +1402 -0
  186. package/ui/public/ui-level.mjs +237 -0
  187. package/ui/server.mjs +1827 -73
@@ -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
- upsertBlock({ kind: 'tool', id: c.id, name: c.name, input: clipJson(input, limits.blockIoMaxChars), status: 'running', durationMs: null });
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
 
@@ -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', // D8
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
- return { maxTurns: readMaxTurns(), maxBudgetUsd: readMaxBudgetUsd() };
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
+ }