@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
@@ -4,6 +4,8 @@
4
4
  // holds no state. All markup is built with DOM APIs and textContent — no
5
5
  // innerHTML for content anywhere in this file (the markdown renderer owns the
6
6
  // only sanitized-HTML path).
7
+ import { openScheduleSheet, browserTimeZone } from './schedule-sheet.mjs';
8
+ import { formatInstant, describeRule } from '../../src/shared/schedule/recurrence.mjs';
7
9
  import { createThreadModel } from './ask-model.mjs';
8
10
  import { createMarkdownRenderer } from './ask-markdown.mjs';
9
11
  import { createThinkingOrb } from './thinking-orb.mjs';
@@ -21,7 +23,7 @@ import { portsFnFor } from '../../src/shared/graph/ports.mjs';
21
23
  * default is ASK_LIMITS.defaultModel/defaultEffort, shipped as `catalog.default`
22
24
  * and already validated against the live catalog by src/core/ask/models.mjs.
23
25
  */
24
- const FALLBACK_PICK = Object.freeze({ model: 'claude-opus-5', effort: 'high' });
26
+ const FALLBACK_PICK = Object.freeze({ model: 'claude-opus-5-5', effort: 'high' });
25
27
 
26
28
  const ICONS = {
27
29
  threads: 'M4 6h16M4 12h16M4 18h9',
@@ -82,6 +84,31 @@ function clipInput(input) {
82
84
  return s.length > 60 ? `${s.slice(0, 60)}…` : s;
83
85
  }
84
86
 
87
+ const SCRIPT_TOOL_NAMES = new Set(['list_scripts', 'get_script', 'save_script', 'test_script']);
88
+
89
+ /**
90
+ * The script tools' thread line (scripts-workbench-design.md §9.3): the key instead of a JSON
91
+ * blob, plus what came back once the call finished. `block.script` is stamped by the reducer
92
+ * (events.mjs#scriptToolKey at the call, #scriptResultNote at the result) and persisted with the
93
+ * block, so the line survives a reload and a clipped input. null for every other tool — those
94
+ * keep the op / target / preview shape.
95
+ * @returns {{target: string}|null} e.g. { target: 'script runTests → blocking, exit 1' }
96
+ */
97
+ export function scriptToolLine(short, block = {}) {
98
+ if (!SCRIPT_TOOL_NAMES.has(short)) return null;
99
+ const s = block.script && typeof block.script === 'object' ? block.script : null;
100
+ const key = typeof s?.key === 'string' && s.key ? s.key
101
+ : (block.input && typeof block.input.key === 'string' ? block.input.key : '');
102
+ const bits = [];
103
+ if (s) {
104
+ if (typeof s.saved === 'string' && s.saved) bits.push(s.saved);
105
+ if (typeof s.status === 'string' && s.status) bits.push(s.status);
106
+ if (Number.isInteger(s.exitCode)) bits.push(`exit ${s.exitCode}`);
107
+ }
108
+ const noun = short.split('_').slice(1).join(' ');
109
+ return { target: [noun, key, bits.length ? `→ ${bits.join(', ')}` : ''].filter(Boolean).join(' ') };
110
+ }
111
+
85
112
  /** The launcher's shortcut hint: the keydown handler accepts BOTH Meta+K and
86
113
  * Ctrl+K, but the glyph shown must match the viewer's OS — '⌘K' is meaningless
87
114
  * on Windows/Linux, where the working chord is Ctrl+K. */
@@ -557,7 +584,8 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
557
584
  text,
558
585
  model: st.picker.model,
559
586
  effort: st.picker.effort,
560
- context: scopedContext(getPageContext() || {}),
587
+ // The browser's zone rides along: "tomorrow 02:00" is read in it (docs/scheduled-runs.md "Ask Worca").
588
+ context: { ...scopedContext(getPageContext() || {}), timeZone: browserTimeZone() },
561
589
  ...(st.pendingFiles.length ? { attachments: st.pendingFiles.map((f) => ({ name: f.name, dataBase64: f.dataBase64 })) } : {}),
562
590
  };
563
591
  const model = st.model;
@@ -652,6 +680,7 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
652
680
  scopeBtn.appendChild(svgIcon(ICONS.chevronDown, 11, 2));
653
681
  scopeBtn.addEventListener('click', () => openScopePopover(scopeBtn));
654
682
  el.scopeBtn = scopeBtn;
683
+ scopeBtn.dataset.minLevel = 'advanced'; // interface mode (docs/ui-levels.md): Auto scope is the simple path
655
684
  row.appendChild(scopeBtn);
656
685
 
657
686
  row.appendChild(make('span', 'ask-composer-spacer'));
@@ -664,12 +693,14 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
664
693
  el.meterCost = make('span', 'ask-meter-cost', '');
665
694
  meter.appendChild(el.meterCost);
666
695
  { const sep = make('span', 'ask-meter-sep', '|'); sep.setAttribute('aria-hidden', 'true'); meter.appendChild(sep); }
696
+ meter.dataset.minLevel = 'advanced';
667
697
  row.appendChild(meter);
668
698
 
669
699
  const wtBtn = make('button', 'ask-agents-btn ask-wt-btn');
670
700
  wtBtn.type = 'button';
671
701
  wtBtn.setAttribute('data-ask-wt-btn', '');
672
702
  wtBtn.hidden = true;
703
+ wtBtn.dataset.minLevel = 'expert';
673
704
  el.wtBtn = wtBtn;
674
705
  el.wtBtnLabel = make('span', null, '0 worktrees');
675
706
  wtBtn.appendChild(el.wtBtnLabel);
@@ -680,6 +711,7 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
680
711
  const agentsBtn = make('button', 'ask-agents-btn');
681
712
  agentsBtn.type = 'button';
682
713
  agentsBtn.setAttribute('data-ask-agents-btn', '');
714
+ agentsBtn.dataset.minLevel = 'expert';
683
715
  el.agentsBtnLabel = make('span', null, '0 agents');
684
716
  agentsBtn.appendChild(el.agentsBtnLabel);
685
717
  agentsBtn.appendChild(svgIcon('M6 15l6-6 6 6', 11, 2));
@@ -689,6 +721,7 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
689
721
  const modelBtn = make('button', 'ask-model-btn');
690
722
  modelBtn.type = 'button';
691
723
  modelBtn.setAttribute('data-ask-model-btn', '');
724
+ modelBtn.dataset.minLevel = 'advanced';
692
725
  el.modelBtnLabel = make('span', 'ask-model-btn-label', st.picker.model);
693
726
  el.modelBtnEffort = make('span', 'ask-model-btn-effort', st.picker.effort);
694
727
  modelBtn.appendChild(el.modelBtnLabel);
@@ -1210,7 +1243,7 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
1210
1243
  const wantedEntry = catalogEntry(wanted.model);
1211
1244
  // Unknown stored/default id -> the backend default -> the first model we do
1212
1245
  // have that is not a hidden built-in (#422; a hidden id is still a valid pick).
1213
- const entry = wantedEntry || catalogEntry(fallback.model) || list.find((m) => m && !m.hidden) || list[0] || null;
1246
+ const entry = wantedEntry || catalogEntry(fallback.model) || list.find((m) => m && !m.hidden && !m.needsSignIn) || list[0] || null;
1214
1247
  if (!entry) { updatePickerButton(); return; } // empty catalog: keep what we have
1215
1248
  const effort = wantedEntry ? wanted.effort : fallback.effort;
1216
1249
  const next = { model: entry.id, effort: coerceEffort(entry, effort) };
@@ -1266,6 +1299,7 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
1266
1299
  for (const m of st.catalog ? st.catalog.models : []) {
1267
1300
  if (!m || typeof m.id !== 'string') continue;
1268
1301
  if (m.hidden && m.id !== st.picker.model) continue; // hidden built-in (#422); the current pick stays
1302
+ if (m.needsSignIn && m.id !== st.picker.model) continue; // bridged, provider not usable (model-bridge §8.5)
1269
1303
  if (m.custom === 'global') { primary.push(m); continue; } // user models are never demoted
1270
1304
  const fam = familyKey(m);
1271
1305
  // The picked model always shows up front so its ✓ is visible and it is one click away.
@@ -1318,6 +1352,9 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
1318
1352
  item.appendChild(tag('secret not set', 'is-err',
1319
1353
  `${m.secretsMissing.join(', ')} is not set — configure it in the ${m.plugin ? `“${m.plugin}” ` : ''}plugin's Model secrets, or this model will fail.`));
1320
1354
  }
1355
+ if (m.needsSignIn) {
1356
+ item.appendChild(tag('needs sign-in', 'is-err', m.signInMessage || 'The provider behind this model is not usable yet — Settings › Models › Providers.'));
1357
+ }
1321
1358
  if (m.id === st.picker.model) item.appendChild(make('span', 'ask-model-check', '✓'));
1322
1359
  return item;
1323
1360
  };
@@ -1787,9 +1824,11 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
1787
1824
  if (!wf || !(Array.isArray(wf.nodes) || Array.isArray(wf.steps)) || !Object.keys(registry).length || !cfg) return null;
1788
1825
  const config = (cfg.config && typeof cfg.config === 'object') ? cfg.config : { steps: {}, customModels: [] };
1789
1826
  const runConfig = (config.workflows && config.workflows[workflowId]) || { nodes: {}, feedbacks: {} };
1790
- const rows = buildNodeConfigRows(wf, registry, runConfig, workflowId === 'wf_default' ? { legacySteps: config.steps || {} } : {});
1827
+ const models = Array.isArray(cfg.models) ? cfg.models : [];
1828
+ // `models`: a pinned row's hidden pick is healed against this catalog (node-tunables.mjs).
1829
+ const rows = buildNodeConfigRows(wf, registry, runConfig, { ...(workflowId === 'wf_default' ? { legacySteps: config.steps || {} } : {}), models });
1791
1830
  return { wf, registry, runConfig, rows, edits: {}, editable: !!projectDir,
1792
- models: Array.isArray(cfg.models) ? cfg.models : [], efforts: Array.isArray(cfg.efforts) ? cfg.efforts : [],
1831
+ models, efforts: Array.isArray(cfg.efforts) ? cfg.efforts : [],
1793
1832
  subagentModels: Array.isArray(cfg.subagentModels) ? cfg.subagentModels : [] };
1794
1833
  }
1795
1834
  const laneEffective = (lane, row) => ({ ...row, ...(lane.edits[row.nodeId] || {}) });
@@ -1910,15 +1949,17 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
1910
1949
  const small = make('small');
1911
1950
  // "step N" = position in the lane (launch order); row.stepIndex ranks the task card as 0.
1912
1951
  if (changed) { small.appendChild(make('span', 'ask-rp-m', 'edited')); small.appendChild(doc.createTextNode(` · step ${i + 1}`)); }
1913
- else small.textContent = `step ${i + 1} · ${row.modified ? 'project override' : 'workflow default'}`;
1952
+ else small.textContent = `step ${i + 1} · ${row.pinned ? 'model from Settings › Memory' : row.modified ? 'project override' : 'workflow default'}`;
1914
1953
  name.appendChild(small);
1915
1954
  l1.appendChild(name);
1916
1955
  // model
1917
1956
  const sel = rpSelect('ask-rp-model', `Model for ${row.label}`);
1918
1957
  sel.appendChild(opt('', 'inherit (workflow default)'));
1919
- for (const m of lane.models) if (!m.hidden || m.id === c.model) sel.appendChild(opt(m.id, m.label || m.id));
1958
+ for (const m of lane.models) if ((!m.hidden && !m.needsSignIn) || m.id === c.model) sel.appendChild(opt(m.id, (m.label || m.id) + (m.needsSignIn ? ' (needs sign-in)' : '')));
1920
1959
  sel.value = c.model || '';
1921
- sel.disabled = !lane.editable;
1960
+ // Settings › Memory pins a defragment run's pair (node-tunables.mjs `pinned`): shown, locked.
1961
+ sel.disabled = !lane.editable || !!row.pinned;
1962
+ if (row.pinned) sel.title = 'Set in Settings › Memory';
1922
1963
  sel.addEventListener('change', () => {
1923
1964
  const mid = sel.value;
1924
1965
  const list = (lane.models.find((m) => m.id === mid) || {}).efforts || [];
@@ -1937,7 +1978,7 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
1937
1978
  b.type = 'button';
1938
1979
  b.setAttribute('role', 'radio');
1939
1980
  b.setAttribute('aria-checked', String(e === c.effort));
1940
- b.disabled = !lane.editable || !offered.includes(e);
1981
+ b.disabled = !lane.editable || !!row.pinned || !offered.includes(e);
1941
1982
  if (!offered.includes(e) && c.model) b.title = `Not offered by ${modelLabel(lane, c.model)}`;
1942
1983
  b.addEventListener('click', () => { laneSet(lane, row, { effort: e }); renderLane(laneSec, lane, lc); });
1943
1984
  eff.appendChild(b);
@@ -1980,9 +2021,76 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
1980
2021
  if (block.state === 'failed') {
1981
2022
  return make('div', 'ask-card-stub ask-card-failed', `Run failed${block.error ? `: ${block.error}` : ''} — ${card.title || card.brief || ''}`);
1982
2023
  }
2024
+ if (block.state === 'scheduled') return buildCardScheduled(block);
1983
2025
  return make('div', 'ask-card-stub', `Not now — ${card.title || card.brief || 'run proposal'}`);
1984
2026
  }
1985
2027
 
2028
+ // ---- scheduled proposals (docs/scheduled-runs.md "Ask Worca") ----
2029
+ const CLOCK_ICO = 'M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18ZM12 7.5V12l3 2';
2030
+ const localWhen = (iso) => { const ms = Date.parse(iso || ''); return Number.isFinite(ms) ? formatInstant(ms, browserTimeZone()) : 'later'; };
2031
+ /** The POST /api/run fields of a proposal's schedule (schedule-spec.mjs scheduleRequestFields). */
2032
+ function scheduleFieldsOf(s) {
2033
+ if (s && s.kind === 'once') return { scheduledFor: s.runAt };
2034
+ if (s && s.kind === 'repeat') return { repeat: { rule: s.rule, overlap: s.overlap, maxFailures: s.maxFailures } };
2035
+ if (s && s.kind === 'after') return { after: { kind: s.after.kind, id: s.after.id, title: s.after.title }, afterPolicy: s.policy || 'done', ...(s.sourceFromPrevious ? { sourceFromPrevious: true } : {}) };
2036
+ return null;
2037
+ }
2038
+ /** The schedule sheet's `initial` for a pick (so Change… opens on what the card shows). */
2039
+ function sheetInitialOf(pick) {
2040
+ if (!pick) return {};
2041
+ if (pick.after) return { after: pick.after, afterPolicy: pick.afterPolicy };
2042
+ if (pick.repeat) return { rule: pick.repeat.rule, overlap: pick.repeat.overlap, maxFailures: pick.repeat.maxFailures, ifMissed: pick.ifMissed, graceMin: pick.graceMin };
2043
+ return { scheduledFor: pick.scheduledFor, ifMissed: pick.ifMissed, graceMin: pick.graceMin };
2044
+ }
2045
+ function scheduleLineText(s) {
2046
+ if (s.kind === 'after') return `${s.text}${s.sourceFromPrevious ? ' · from its branch' : ''}`;
2047
+ if (s.kind === 'repeat') return `${s.sentence}${s.next && s.next[0] ? ` · first run ${localWhen(s.next[0].at)}` : ''}`;
2048
+ return `Starts ${localWhen(s.runAt)}`;
2049
+ }
2050
+ function pickedLineText(p) {
2051
+ if (p.after) return `After ‘${p.after.title || p.after.id}’ finishes${p.sourceFromPrevious ? ' · from its branch' : ''}`;
2052
+ if (p.repeat) return describeRule(p.repeat.rule);
2053
+ return `Starts ${localWhen(p.scheduledFor)}`;
2054
+ }
2055
+
2056
+ /** A proposal the user scheduled: it waits as a ticket (block.runId) — or, when it became a repeating
2057
+ * schedule, follows the series (block.scheduleId) — until the server starts it. */
2058
+ function buildCardScheduled(block) {
2059
+ const card = block.card || {};
2060
+ const rootEl = make('div', 'ask-card-stub ask-card-sched');
2061
+ rootEl.setAttribute('data-ask-card-scheduled', '');
2062
+ const series = !!block.scheduleId;
2063
+ const title = card.title || card.brief || 'Run';
2064
+ rootEl.append(make('span', 'badge grey', series ? 'Repeats' : block.after ? 'After run' : 'Scheduled'), make('span', 'ask-card-sched-text',
2065
+ series ? `${title} — ${block.sentence || 'repeating schedule'}${block.scheduledFor ? ` · next ${localWhen(block.scheduledFor)}` : ''}`
2066
+ : block.after ? `${title} — after ‘${block.after.title || block.after.id}’ finishes` : `${title} — starts ${localWhen(block.scheduledFor)}`));
2067
+ const err = make('span', 'ask-card-err');
2068
+ const call = async (method, path, btn) => {
2069
+ err.textContent = ''; btn.disabled = true;
2070
+ try {
2071
+ const res = await fetch(path, { method, headers: { 'Content-Type': 'application/json' }, body: method === 'POST' ? '{}' : undefined });
2072
+ const data = await res.json().catch(() => ({}));
2073
+ if (!res.ok) err.textContent = data.error || `request failed (${res.status})`;
2074
+ else if (data.status === 'failed') err.textContent = data.failReason || 'The run could not be started.';
2075
+ } catch { err.textContent = 'network error'; }
2076
+ btn.disabled = false; // the flip frame re-renders the card on success
2077
+ };
2078
+ const target = series ? block.scheduleId : block.runId;
2079
+ const runNow = make('button', 'ask-card-not-now', 'Run now');
2080
+ runNow.type = 'button';
2081
+ runNow.addEventListener('click', () => call('POST', `/api/schedules/${target}/run-now`, runNow));
2082
+ const cancel = make('button', 'ask-card-not-now', series ? 'Delete schedule' : 'Cancel schedule');
2083
+ cancel.type = 'button';
2084
+ cancel.addEventListener('click', async () => {
2085
+ if (series && typeof confirm === 'function' && !(await confirm(`Delete the schedule "${title}"? Runs it already started are kept.`))) return;
2086
+ call('DELETE', `/api/schedules/${target}`, cancel);
2087
+ });
2088
+ const open = make('a', 'ask-card-sched-link', 'Schedules');
2089
+ open.href = '#schedules';
2090
+ rootEl.append(runNow, cancel, open, err);
2091
+ return rootEl;
2092
+ }
2093
+
1986
2094
  // ---- Workflow card (spec §8.3, mockup 2026-09-05 §A-§C, plan PD4/PD7/PD12-15) ---------------------------------
1987
2095
  const WF_ICO = { check: 'M5 13l4 4L19 7', save: 'M5 12l5 5L20 7' };
1988
2096
  const TUNABLE_KEYS = ['model', 'effort', 'fanOut', 'askQuestions'];
@@ -2078,15 +2186,23 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
2078
2186
  // ---- Metrics card (docs/team-metrics.md "Ask Worca"): a proposed team-metrics configuration change --------------
2079
2187
  const MC_KIND_LABEL = { enable: 'Enable team metrics', record: 'Include my runs', workspace_home: 'Metrics home', route_members: 'Route members' };
2080
2188
  const MC_APPLY_LABEL = { enable: 'Enable', record: 'Apply', workspace_home: 'Set home', route_members: 'Route members' };
2189
+ // A team-policy card (docs/team-policy.md "Ask Worca") is the same component: other words, plus the
2190
+ // before → after list of an edit.
2191
+ const PC_KIND_LABEL = { enable: 'Set up team policy', edit: 'Edit team policy', workspace_home: 'Policy home', route_members: 'Route members' };
2192
+ const pcApplyLabel = (card) => (card.kind === 'enable' ? (card.mode === 'follow' ? 'Follow' : 'Set up') : card.kind === 'edit' ? 'Publish'
2193
+ : card.kind === 'workspace_home' ? 'Set home' : card.kind === 'route_members' ? 'Route members' : 'Apply');
2081
2194
  function buildMetricsCard(block) {
2082
2195
  const card = block.card || {};
2083
- const summary = card.summary || 'metrics change';
2196
+ const isPolicy = card.type === 'policy';
2197
+ const noun = isPolicy ? 'policy change' : 'metrics change';
2198
+ const Noun = isPolicy ? 'Policy change' : 'Metrics change';
2199
+ const summary = card.summary || noun;
2084
2200
  if (block.state === 'declined') return { el: make('div', 'ask-card-stub', `Declined — ${summary}`) };
2085
- const rootEl = make('div', `ask-card ask-mcard is-${block.state}`);
2086
- rootEl.setAttribute('data-ask-mcard', block.state);
2201
+ const rootEl = make('div', `ask-card ask-mcard${isPolicy ? ' ask-pcard' : ''} is-${block.state}`);
2202
+ rootEl.setAttribute(isPolicy ? 'data-ask-pcard' : 'data-ask-mcard', block.state);
2087
2203
  const head = make('div', 'ask-mcard-head');
2088
- head.appendChild(make('span', 'ask-mcard-title', block.state === 'applied' ? 'Applied metrics change' : block.state === 'failed' ? 'Metrics change failed' : 'Proposed metrics change'));
2089
- head.appendChild(make('span', 'ask-mcard-kind', MC_KIND_LABEL[card.kind] || card.kind || ''));
2204
+ head.appendChild(make('span', 'ask-mcard-title', block.state === 'applied' ? `Applied ${noun}` : block.state === 'failed' ? `${Noun} failed` : `Proposed ${noun}`));
2205
+ head.appendChild(make('span', 'ask-mcard-kind', (isPolicy ? PC_KIND_LABEL : MC_KIND_LABEL)[card.kind] || card.kind || ''));
2090
2206
  rootEl.appendChild(head);
2091
2207
  const body = make('div', 'ask-mcard-body');
2092
2208
  const target = card.workspaceName ? `workspace ${card.workspaceName}` : card.projectName ? `project ${card.projectName}` : '';
@@ -2096,6 +2212,20 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
2096
2212
  body.appendChild(sum);
2097
2213
  if (target) body.appendChild(make('div', 'ask-mcard-target', target));
2098
2214
  if (card.note) body.appendChild(make('div', 'ask-mcard-note', card.note));
2215
+ if (isPolicy && Array.isArray(card.changes) && card.changes.length) {
2216
+ const ul = make('ul', 'ask-mcard-changes');
2217
+ for (const c of card.changes) {
2218
+ const li = make('li');
2219
+ li.appendChild(make('span', 'ask-mcard-change-label', c.label || c.key || ''));
2220
+ const val = make('span', 'ask-mcard-change-val');
2221
+ val.appendChild(make('span', c.before ? 'ask-mcard-before' : 'ask-mcard-before is-unset', c.before || 'unset'));
2222
+ val.appendChild(make('span', 'ask-mcard-arrow', '→'));
2223
+ val.appendChild(make('span', c.after ? 'ask-mcard-after' : 'ask-mcard-after is-unset', c.after || 'unset'));
2224
+ li.appendChild(val);
2225
+ ul.appendChild(li);
2226
+ }
2227
+ body.appendChild(ul);
2228
+ }
2099
2229
  if (Array.isArray(card.effects) && card.effects.length && block.state === 'proposed') {
2100
2230
  const ul = make('ul', 'ask-mcard-effects');
2101
2231
  for (const e of card.effects) ul.appendChild(make('li', null, e));
@@ -2130,7 +2260,143 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
2130
2260
  };
2131
2261
  const decline = btn('ask-card-not-now', 'Decline', 'data-ask-mc-decline');
2132
2262
  decline.addEventListener('click', () => postCard(block, rootEl, { state: 'declined' }, decline));
2133
- const apply = btn('ask-card-start', MC_APPLY_LABEL[card.kind] || 'Apply', 'data-ask-mc-apply', WF_ICO.save);
2263
+ const apply = btn('ask-card-start', isPolicy ? pcApplyLabel(card) : (MC_APPLY_LABEL[card.kind] || 'Apply'), 'data-ask-mc-apply', WF_ICO.save);
2264
+ apply.addEventListener('click', () => postCard(block, rootEl, { state: 'applied' }, apply));
2265
+ actions.append(make('span', 'ask-card-actions-spacer'), decline, apply);
2266
+ rootEl.appendChild(actions);
2267
+ }
2268
+ return { el: rootEl };
2269
+ }
2270
+
2271
+ // ---- Schedule card (docs/scheduled-runs.md "Ask Worca"): a proposed change to an existing schedule ------------
2272
+ const SC_ACTION_LABEL = { run_now: 'Run now', move: 'Change time', edit: 'Edit schedule', cancel: 'Cancel run', delete: 'Delete schedule' };
2273
+ const SC_APPLY_LABEL = { run_now: 'Run now', move: 'Move', edit: 'Apply', cancel: 'Cancel run', delete: 'Delete' };
2274
+ function buildScheduleCard(block) {
2275
+ const card = block.card || {};
2276
+ const summary = card.summary || 'schedule change';
2277
+ if (block.state === 'declined') return { el: make('div', 'ask-card-stub', `Declined — ${summary}`) };
2278
+ const rootEl = make('div', `ask-card ask-mcard ask-scard is-${block.state}`);
2279
+ rootEl.setAttribute('data-ask-scard', block.state);
2280
+ const head = make('div', 'ask-mcard-head');
2281
+ head.appendChild(make('span', 'ask-mcard-title', block.state === 'applied' ? 'Applied schedule change' : block.state === 'failed' ? 'Schedule change failed' : 'Proposed schedule change'));
2282
+ head.appendChild(make('span', 'ask-mcard-kind', SC_ACTION_LABEL[card.action] || card.action || ''));
2283
+ rootEl.appendChild(head);
2284
+ const body = make('div', 'ask-mcard-body');
2285
+ const sum = make('div', 'ask-mcard-summary');
2286
+ if (block.state === 'applied') sum.appendChild(svgIcon(WF_ICO.check, 15, 2.4));
2287
+ sum.appendChild(make('span', null, summary));
2288
+ body.appendChild(sum);
2289
+ if (card.targetName) body.appendChild(make('div', 'ask-mcard-target', card.targetName));
2290
+ if (card.note) body.appendChild(make('div', 'ask-mcard-note', card.note));
2291
+ if (block.state === 'proposed' && (card.before || card.after)) {
2292
+ const kv = make('div', 'ask-scard-kv');
2293
+ const row = (k, v) => { if (!v) return; kv.append(make('span', 'ask-scard-k', k), make('span', 'ask-scard-v', v)); };
2294
+ const b = card.before || {};
2295
+ const a = card.after || {};
2296
+ if (card.action === 'edit') {
2297
+ row('Now', b.sentence);
2298
+ if (a.sentence && a.sentence !== b.sentence) row('Becomes', a.sentence);
2299
+ if (Array.isArray(a.next) && a.next.length) row('Next runs', a.next.map((n) => localWhen(n.at)).join(' · '));
2300
+ } else if (card.action === 'move') {
2301
+ // Run chains: a move to AFTER another run has no instant on either side — say what it is.
2302
+ row('From', b.at ? localWhen(b.at) : b.when || '');
2303
+ row('To', a.at ? localWhen(a.at) : a.text || '');
2304
+ } else if (b.at) row(card.itemKind === 'recurring' ? 'Next run' : 'Scheduled for', localWhen(b.at));
2305
+ if (kv.childNodes.length) body.appendChild(kv);
2306
+ }
2307
+ const result = card.result || null;
2308
+ if (block.state === 'failed') body.appendChild(make('div', 'ask-mcard-failed', `Could not apply: ${block.error || (result && result.error) || 'unknown error'}`));
2309
+ else if (block.state === 'applied' && result && result.detail) body.appendChild(make('div', 'ask-mcard-detail', result.detail));
2310
+ if (block.state !== 'proposed') {
2311
+ const open = make('a', 'ask-card-sched-link', 'Schedules');
2312
+ open.href = '#schedules';
2313
+ body.appendChild(open);
2314
+ }
2315
+ rootEl.appendChild(body);
2316
+ rootEl.appendChild(make('div', 'ask-card-err'));
2317
+ if (block.state === 'proposed') {
2318
+ const actions = make('div', 'ask-mcard-actions');
2319
+ const btn = (cls, text, attr, icon) => {
2320
+ const b = make('button', cls, text); b.type = 'button'; b.setAttribute(attr, '');
2321
+ if (icon) b.prepend(svgIcon(icon, 12, 2.2));
2322
+ return b;
2323
+ };
2324
+ const decline = btn('ask-card-not-now', 'Decline', 'data-ask-sc-decline');
2325
+ decline.addEventListener('click', () => postCard(block, rootEl, { state: 'declined' }, decline));
2326
+ const destructive = card.action === 'cancel' || card.action === 'delete';
2327
+ const apply = btn(destructive ? 'ask-card-start is-danger' : 'ask-card-start', SC_APPLY_LABEL[card.action] || 'Apply', 'data-ask-sc-apply',
2328
+ card.action === 'run_now' ? null : WF_ICO.save);
2329
+ apply.addEventListener('click', () => postCard(block, rootEl, { state: 'applied' }, apply));
2330
+ actions.append(make('span', 'ask-card-actions-spacer'), decline, apply);
2331
+ rootEl.appendChild(actions);
2332
+ }
2333
+ return { el: rootEl };
2334
+ }
2335
+
2336
+ // ---- Model card (docs/models.md "Ask Worca"): a proposed catalog or provider change ------------------------------
2337
+ const MOD_KIND_LABEL = { add_model: 'Add model', edit_model: 'Edit model', remove_model: 'Remove model', provider: 'Provider', import_copilot: 'From Copilot' };
2338
+ const MOD_APPLY_LABEL = { add_model: 'Add', edit_model: 'Apply', remove_model: 'Remove', provider: 'Apply', import_copilot: 'Import' };
2339
+ function buildModelCard(block) {
2340
+ const card = block.card || {};
2341
+ const summary = card.summary || 'model change';
2342
+ if (block.state === 'declined') return { el: make('div', 'ask-card-stub', `Declined — ${summary}`) };
2343
+ const rootEl = make('div', `ask-card ask-mcard ask-modcard is-${block.state}`);
2344
+ rootEl.setAttribute('data-ask-modcard', block.state);
2345
+ const noun = card.kind === 'provider' ? 'provider change' : 'model change';
2346
+ const head = make('div', 'ask-mcard-head');
2347
+ head.appendChild(make('span', 'ask-mcard-title', block.state === 'applied' ? `Applied ${noun}` : block.state === 'failed' ? `${noun[0].toUpperCase()}${noun.slice(1)} failed` : `Proposed ${noun}`));
2348
+ head.appendChild(make('span', 'ask-mcard-kind', MOD_KIND_LABEL[card.kind] || card.kind || ''));
2349
+ rootEl.appendChild(head);
2350
+ const body = make('div', 'ask-mcard-body');
2351
+ const sum = make('div', 'ask-mcard-summary');
2352
+ if (block.state === 'applied') sum.appendChild(svgIcon(WF_ICO.check, 15, 2.4));
2353
+ sum.appendChild(make('span', null, summary));
2354
+ body.appendChild(sum);
2355
+ if (card.note) body.appendChild(make('div', 'ask-mcard-note', card.note));
2356
+ if (block.state === 'proposed' && Array.isArray(card.rows) && card.rows.length) {
2357
+ const ul = make('ul', 'ask-mcard-changes');
2358
+ const oneSided = card.kind === 'add_model' || card.kind === 'remove_model';
2359
+ for (const r of card.rows) {
2360
+ const li = make('li');
2361
+ li.appendChild(make('span', 'ask-mcard-change-label', r.field || ''));
2362
+ const val = make('span', 'ask-mcard-change-val');
2363
+ if (oneSided) val.appendChild(make('span', card.kind === 'remove_model' ? 'ask-mcard-before' : 'ask-mcard-after', (card.kind === 'remove_model' ? r.before : r.after) || ''));
2364
+ else {
2365
+ val.appendChild(make('span', r.before ? 'ask-mcard-before' : 'ask-mcard-before is-unset', r.before || 'unset'));
2366
+ val.appendChild(make('span', 'ask-mcard-arrow', '→'));
2367
+ val.appendChild(make('span', r.after ? 'ask-mcard-after' : 'ask-mcard-after is-unset', r.after || 'unset'));
2368
+ }
2369
+ li.appendChild(val);
2370
+ ul.appendChild(li);
2371
+ }
2372
+ body.appendChild(ul);
2373
+ }
2374
+ if (block.state === 'proposed' && Array.isArray(card.warnings) && card.warnings.length) {
2375
+ const ul = make('ul', 'ask-mcard-effects ask-modcard-warn');
2376
+ for (const w of card.warnings) ul.appendChild(make('li', null, w));
2377
+ body.appendChild(ul);
2378
+ }
2379
+ const result = card.result || null;
2380
+ if (block.state === 'failed') body.appendChild(make('div', 'ask-mcard-failed', `Could not apply: ${block.error || (result && result.error) || 'unknown error'}`));
2381
+ else if (block.state === 'applied' && result && result.detail) body.appendChild(make('div', 'ask-mcard-detail', result.detail));
2382
+ if (block.state !== 'proposed') {
2383
+ const open = make('a', 'ask-card-sched-link', 'Settings › Models');
2384
+ open.href = '#settings/models';
2385
+ body.appendChild(open);
2386
+ }
2387
+ rootEl.appendChild(body);
2388
+ rootEl.appendChild(make('div', 'ask-card-err'));
2389
+ if (block.state === 'proposed') {
2390
+ const actions = make('div', 'ask-mcard-actions');
2391
+ const btn = (cls, text, attr, icon) => {
2392
+ const b = make('button', cls, text); b.type = 'button'; b.setAttribute(attr, '');
2393
+ if (icon) b.prepend(svgIcon(icon, 12, 2.2));
2394
+ return b;
2395
+ };
2396
+ const decline = btn('ask-card-not-now', 'Decline', 'data-ask-mod-decline');
2397
+ decline.addEventListener('click', () => postCard(block, rootEl, { state: 'declined' }, decline));
2398
+ const destructive = card.kind === 'remove_model';
2399
+ const apply = btn(destructive ? 'ask-card-start is-danger' : 'ask-card-start', MOD_APPLY_LABEL[card.kind] || 'Apply', 'data-ask-mod-apply', destructive ? null : WF_ICO.save);
2134
2400
  apply.addEventListener('click', () => postCard(block, rootEl, { state: 'applied' }, apply));
2135
2401
  actions.append(make('span', 'ask-card-actions-spacer'), decline, apply);
2136
2402
  rootEl.appendChild(actions);
@@ -2307,6 +2573,11 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
2307
2573
  guardDesc.setAttribute('data-for', 'guardrails');
2308
2574
  const guardField = rpField('Guardrails', guardSel);
2309
2575
  guardField.appendChild(guardDesc);
2576
+ // Interface mode (docs/ui-levels.md). The proposal's own non-default values stay on screen at
2577
+ // any mode: a run must never start under a policy or on a branch the card did not show.
2578
+ const lvTag = (node, min, keep) => { node.dataset.minLevel = min; if (keep) node.dataset.levelKeep = '1'; return node; };
2579
+ lvTag(guardField, 'advanced', !!card.guardrailsId && card.guardrailsId !== 'permissive');
2580
+ lvTag(seg, 'advanced', card.target === 'workspace');
2310
2581
  wfRow.append(wfField, guardField);
2311
2582
  targetSec.appendChild(wfRow);
2312
2583
  rootEl.appendChild(targetSec);
@@ -2317,6 +2588,7 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
2317
2588
 
2318
2589
  // agents lane (reloadLane → renderLane fills laneSec)
2319
2590
  const laneSec = make('div', 'ask-rp-sec ask-rp-lane');
2591
+ laneSec.dataset.minLevel = 'expert';
2320
2592
  rootEl.appendChild(laneSec);
2321
2593
 
2322
2594
  const briefSec = make('div', 'ask-rp-sec ask-rp-brief-host');
@@ -2337,6 +2609,23 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
2337
2609
  const count = make('span', 'ask-rp-count');
2338
2610
  briefFoot.append(hint, count);
2339
2611
  briefSec.appendChild(briefFoot);
2612
+ if (card.source) {
2613
+ // The task IS a tracker task (propose_run source): the run reads it when it starts, so the
2614
+ // card shows the reference — never an editable copy that would silently go stale.
2615
+ briefHead.firstChild.textContent = 'Task';
2616
+ briefHead.children[1].textContent = `from ${card.source.displayName || card.source.plugin} · read when the run starts`;
2617
+ brief.hidden = true;
2618
+ briefFoot.hidden = true;
2619
+ const task = make('div', 'ask-card-task');
2620
+ task.setAttribute('data-ask-card-task', '');
2621
+ task.appendChild(make('span', 'badge grey mono', card.source.taskId));
2622
+ const name = card.source.url ? make('a', 'ask-card-task-title', card.source.title || card.source.taskId) : make('span', 'ask-card-task-title', card.source.title || '');
2623
+ if (card.source.url) { name.href = card.source.url; name.target = '_blank'; name.rel = 'noopener noreferrer'; }
2624
+ task.appendChild(name);
2625
+ if (card.source.profile) task.appendChild(make('span', 'ask-card-task-meta', `profile ${card.source.profile}`));
2626
+ briefSec.insertBefore(task, brief);
2627
+ if (card.sourceWarning) briefSec.insertBefore(make('div', 'ask-card-task-warn', card.sourceWarning), brief);
2628
+ }
2340
2629
  rootEl.appendChild(briefSec);
2341
2630
  local.pills = Array.isArray(card.attachments) ? card.attachments.filter((a) => a && a.id).map((a) => ({ ...a })) : [];
2342
2631
  function renderPills() {
@@ -2390,6 +2679,9 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
2390
2679
  const openNp = make('button', 'ask-card-open-np', '↗ Open in New Pipeline');
2391
2680
  openNp.type = 'button';
2392
2681
  openNp.setAttribute('data-ask-card-open-np', '');
2682
+ openNp.dataset.minLevel = 'advanced';
2683
+ // New pipeline's task-source pane cannot be pre-filled from here yet; a tracker task runs from the card.
2684
+ if (card.source) openNp.hidden = true;
2393
2685
  openNp.addEventListener('click', () => prefillFromCard(block, rootEl, local));
2394
2686
  const dismissBtn = make('button', 'ask-card-not-now', 'Not now');
2395
2687
  dismissBtn.type = 'button';
@@ -2403,8 +2695,58 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
2403
2695
  const playPath = doc.createElementNS('http://www.w3.org/2000/svg', 'path'); playPath.setAttribute('d', 'M6 4l14 8-14 8V4Z');
2404
2696
  play.appendChild(playPath); startBtn.appendChild(play);
2405
2697
  startBtn.appendChild(doc.createTextNode('Start run'));
2406
- startBtn.addEventListener('click', () => startCard(block, rootEl, local));
2407
- foot.append(openNp, summary, dismissBtn, startBtn);
2698
+ // A proposal Ask Worca scheduled starts at its (possibly changed) time; any other starts now.
2699
+ startBtn.addEventListener('click', () => startCard(block, rootEl, local, local.schedulePick || null));
2700
+ // Schedule…: the same request, started later — once, or on a repeat (the card then follows the schedule).
2701
+ const sheetOpts = (initial = {}) => ({
2702
+ mode: 'create', allowRepeat: true, initial, runTitle: (block.card && (block.card.title || block.card.brief)) || '',
2703
+ warning: 'A scheduled run is unattended. If this workflow asks questions, the run waits for your answer — chat notifications can reach you.',
2704
+ // Keyed off the card's LIVE target segment (local.target), not the proposal's frozen card.workspaceId —
2705
+ // the user can switch the card between project and workspace before opening the sheet.
2706
+ candidates: () => fetch(`/api/schedules/after-candidates?${local.target === 'workspace'
2707
+ ? `workspaceId=${encodeURIComponent((rootEl.querySelector('.ask-card-workspace-select') || {}).value || card.workspaceId || '')}`
2708
+ : `projectDir=${encodeURIComponent(local.projectDir())}`}`).then((r) => r.json()),
2709
+ });
2710
+ const laterBtn = make('button', 'ask-card-not-now ask-card-later', 'Schedule…');
2711
+ laterBtn.type = 'button';
2712
+ if (card.schedule) {
2713
+ // Ask Worca proposed WHEN (propose_run when / every): scheduling is the answer, so it is the primary
2714
+ // action at every interface level (docs/ui-levels.md rule 4), and Start now is the alternative.
2715
+ local.schedulePick = scheduleFieldsOf(card.schedule);
2716
+ const line = make('div', 'ask-card-sched ask-card-sched-proposed');
2717
+ line.setAttribute('data-ask-card-sched-proposed', '');
2718
+ const badge = make('span', 'badge grey', card.schedule.kind === 'repeat' ? 'Repeats' : card.schedule.kind === 'after' ? 'After run' : 'Scheduled');
2719
+ const text = make('span', 'ask-card-sched-text', scheduleLineText(card.schedule));
2720
+ const change = make('button', 'link-btn ask-card-sched-change', 'Change…');
2721
+ change.type = 'button';
2722
+ change.setAttribute('data-ask-card-sched-change', '');
2723
+ change.addEventListener('click', async () => {
2724
+ const picked = await openScheduleSheet(sheetOpts(sheetInitialOf(local.schedulePick)));
2725
+ if (!picked) return;
2726
+ // A sheet pick that stays "after another run" keeps a sourceFromPrevious the proposal carried.
2727
+ local.schedulePick = { ...picked, ...(local.schedulePick && local.schedulePick.sourceFromPrevious && picked.after ? { sourceFromPrevious: true } : {}) };
2728
+ badge.textContent = picked.repeat ? 'Repeats' : picked.after ? 'After run' : 'Scheduled';
2729
+ text.textContent = pickedLineText(local.schedulePick);
2730
+ });
2731
+ line.append(badge, text, change);
2732
+ rootEl.insertBefore(line, err);
2733
+ play.replaceWith(svgIcon(CLOCK_ICO, 13, 2.2));
2734
+ startBtn.lastChild.textContent = 'Schedule';
2735
+ startBtn.setAttribute('data-ask-card-schedule-go', '');
2736
+ laterBtn.textContent = 'Start now';
2737
+ laterBtn.title = 'Start this run now instead';
2738
+ laterBtn.setAttribute('data-ask-card-start-now', '');
2739
+ laterBtn.addEventListener('click', () => startCard(block, rootEl, local));
2740
+ } else {
2741
+ laterBtn.setAttribute('data-ask-card-schedule', '');
2742
+ laterBtn.title = 'Start this run later';
2743
+ laterBtn.dataset.minLevel = 'advanced';
2744
+ laterBtn.addEventListener('click', async () => {
2745
+ const picked = await openScheduleSheet(sheetOpts());
2746
+ if (picked) startCard(block, rootEl, local, picked);
2747
+ });
2748
+ }
2749
+ foot.append(openNp, summary, dismissBtn, laterBtn, startBtn);
2408
2750
  rootEl.appendChild(foot);
2409
2751
 
2410
2752
  local.projectDir = () => (local.target === 'project' ? ((rootEl.querySelector('.ask-card-project-select') || {}).value || '') : '');
@@ -2441,7 +2783,8 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
2441
2783
  }
2442
2784
  projSel.addEventListener('change', () => { loadBranchesInto(srcSel, projSel.value, '').then(updateTargetSub); updateTargetSub(); reloadLane(); });
2443
2785
  srcSel.addEventListener('change', updateTargetSub);
2444
- grid.append(rpField('Project', projSel), rpField('Source branch', srcSel), rpField('Feature branch', feature, 'created for the run'));
2786
+ grid.append(rpField('Project', projSel), lvTag(rpField('Source branch', srcSel), 'advanced', !!card.sourceBranch),
2787
+ lvTag(rpField('Feature branch', feature, 'created for the run'), 'advanced', !!card.featureBranch));
2445
2788
  targetHost.appendChild(grid);
2446
2789
  updateTargetSub();
2447
2790
  return;
@@ -2481,7 +2824,8 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
2481
2824
  wsSel.addEventListener('change', renderMembers);
2482
2825
  const wsField = rpField('Workspace', wsSel);
2483
2826
  wsField.appendChild(members);
2484
- grid.append(wsField, rpField('Source branch', srcInput, 'default for members'), rpField('Feature branch', feature));
2827
+ grid.append(wsField, lvTag(rpField('Source branch', srcInput, 'default for members'), 'advanced', !!card.sourceBranch),
2828
+ lvTag(rpField('Feature branch', feature), 'advanced', !!card.featureBranch));
2485
2829
  targetHost.appendChild(grid); // attach BEFORE filling: renderMembers → updateTargetSub finds the select through rootEl
2486
2830
  targetHost.appendChild(details);
2487
2831
  if (opts) {
@@ -2500,6 +2844,12 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
2500
2844
  const workflowId = local.workflowId();
2501
2845
  const projectDir = local.projectDir();
2502
2846
  local.lane = null;
2847
+ if (workflowId === 'wf_auto') {
2848
+ // No graph yet: the run classifies its task and picks the agents when it starts.
2849
+ renderLane(laneSec, null, laneCtx, 'Auto picks the agents when the run starts.');
2850
+ wfDesc.textContent = '';
2851
+ return;
2852
+ }
2503
2853
  renderLane(laneSec, null, laneCtx, 'Loading agent settings…');
2504
2854
  loadLane(workflowId, projectDir).then((lane) => {
2505
2855
  if (st.destroyed || seq !== laneSeq) return; // a later reload won
@@ -2527,7 +2877,9 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
2527
2877
  loadCardOptions({ fresh: true }).then((opts) => {
2528
2878
  if (st.destroyed) return;
2529
2879
  local.options = opts;
2530
- fillSelect(workflowSel, opts.workflows.map((w) => ({ value: w.id, label: workflowPickerLabel(w, null) || w.name || w.id })), card.workflowId || 'wf_default');
2880
+ // Auto is not a saved workflow: listed only when Ask Worca proposed it (a plain card keeps its list).
2881
+ const autoOpt = card.workflowId === 'wf_auto' ? [{ value: 'wf_auto', label: 'Auto — picks the workflow when the run starts' }] : [];
2882
+ fillSelect(workflowSel, [...autoOpt, ...opts.workflows.map((w) => ({ value: w.id, label: workflowPickerLabel(w, null) || w.name || w.id }))], card.workflowId || 'wf_default');
2531
2883
  if (card.workflowId && workflowSel.value !== card.workflowId) markWorkflowUnavailable();
2532
2884
  fillSelect(guardSel, opts.guardrails.map((g) => ({ value: g.id, label: g.id === 'permissive' ? 'Permissive' : (g.name || g.id) })), card.guardrailsId || 'normal');
2533
2885
  renderTarget();
@@ -2548,6 +2900,14 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
2548
2900
  // Agent memory (§7.3 / B17): the card's scope rides along only while its workflow is still the
2549
2901
  // defragment one — a user who switched the picker to another workflow gets a legacy body.
2550
2902
  if (card.memoryScope && body.workflowId === 'wf_memory_defrag') body.memoryScope = card.memoryScope;
2903
+ // A tracker task: the reference, never a prompt (POST /api/run takes source OR prompt).
2904
+ if (card.source) {
2905
+ delete body.prompt;
2906
+ body.source = {
2907
+ type: 'plugin', plugin: card.source.plugin, sourceId: card.source.sourceId, taskId: card.source.taskId,
2908
+ ...(card.source.profile ? { profile: card.source.profile } : {}), ...(card.source.inputs ? { inputs: card.source.inputs } : {}),
2909
+ };
2910
+ }
2551
2911
  const feature = rootEl.querySelector('.ask-card-feature').value.trim();
2552
2912
  if (feature) body.featureBranch = feature;
2553
2913
  if (local.target === 'workspace') {
@@ -2578,7 +2938,7 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
2578
2938
  }
2579
2939
  }
2580
2940
 
2581
- async function startCard(block, rootEl, local) {
2941
+ async function startCard(block, rootEl, local, schedule = null) {
2582
2942
  const err = rootEl.querySelector('.ask-card-err');
2583
2943
  const startBtn = rootEl.querySelector('[data-ask-card-start]');
2584
2944
  err.textContent = '';
@@ -2590,7 +2950,10 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
2590
2950
  if (ex.error) { err.textContent = ex.error; return; }
2591
2951
  const saveErr = await saveLaneEdits(local); // the previous phase's guard, now second
2592
2952
  if (saveErr) { err.textContent = saveErr; return; }
2593
- const body = { ...collectCardBody(rootEl, local, block.card || {}), askThreadId: st.threadId, askCardId: block.id };
2953
+ const body = { ...collectCardBody(rootEl, local, block.card || {}), askThreadId: st.threadId, askCardId: block.id, ...(schedule || {}) };
2954
+ // Run chains: "the branch of the run before it" is a flag, and the wire refuses it next to a branch name.
2955
+ // Keyed off the schedule being POSTED — a Start now on an after-proposal carries none and keeps the picked branch.
2956
+ if (body.sourceFromPrevious) { delete body.sourceBranch; delete body.sourceBranchByKey; }
2594
2957
  if (ex.extras.length) body.extras = ex.extras;
2595
2958
  let res = null;
2596
2959
  try {
@@ -2704,19 +3067,24 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
2704
3067
  function isProgressBlock(block) {
2705
3068
  const card = block.card || {};
2706
3069
  if (card.type === PROGRESS_CARD_TYPE) return true;
2707
- if (card.type === 'workflow' || card.type === 'metrics') return false;
3070
+ if (card.type === 'workflow' || card.type === 'metrics' || card.type === 'policy' || card.type === 'schedule' || card.type === 'model') return false;
2708
3071
  return block.state === 'started' || (block.state === 'failed' && !!block.runId);
2709
3072
  }
2710
3073
  function buildCard(block) {
2711
3074
  if (!st.cardEls) st.cardEls = new Map();
2712
3075
  const cached = st.cardEls.get(block.id);
2713
3076
  const isWorkflow = !!(block.card && block.card.type === 'workflow');
2714
- const isMetrics = !!(block.card && block.card.type === 'metrics');
3077
+ // A policy card is a metrics card with different words (buildMetricsCard branches on the type).
3078
+ const isMetrics = !!(block.card && (block.card.type === 'metrics' || block.card.type === 'policy'));
3079
+ const isSchedule = !!(block.card && block.card.type === 'schedule');
3080
+ const isModel = !!(block.card && block.card.type === 'model');
2715
3081
  const isProgress = isProgressBlock(block);
2716
- if (cached && cached.state === block.state && (isWorkflow || isMetrics || isProgress || block.state === 'proposed')) return cached.el;
3082
+ if (cached && cached.state === block.state && (isWorkflow || isMetrics || isSchedule || isModel || isProgress || block.state === 'proposed')) return cached.el;
2717
3083
  if (cached) disposeCardEntry(cached);
2718
3084
  const built = isWorkflow ? buildWorkflowCard(block, cached)
2719
3085
  : isMetrics ? buildMetricsCard(block)
3086
+ : isSchedule ? buildScheduleCard(block)
3087
+ : isModel ? buildModelCard(block)
2720
3088
  : isProgress ? buildProgressCard(block)
2721
3089
  : { el: block.state === 'proposed' ? buildCardForm(block) : buildCardTerminal(block) };
2722
3090
  st.cardEls.set(block.id, { el: built.el, state: block.state, handle: built.handle || null, dispose: built.dispose || null, animate: !!built.animate, cancelAnim: null, lastW: -1 });
@@ -2867,11 +3235,16 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
2867
3235
 
2868
3236
  function toolRow(block) {
2869
3237
  const rowEl = make('div', 'ask-tool-row');
3238
+ rowEl.dataset.minLevel = 'advanced'; // what the assistant ran, step by step
2870
3239
  const short = String(block.name || '').replace(/^mcp__worca__/, '');
2871
3240
  const parts = short.split('_');
2872
3241
  rowEl.appendChild(make('span', 'ask-tool-op', parts[0] || short));
2873
- const target = parts.slice(1).join(' ');
2874
- const preview = clipInput(block.input);
3242
+ // A script tool reads as `test script runTests → blocking, exit 1` (§9.3): the op column
3243
+ // (a fixed 38 px cell) keeps the verb, the target column carries the key and the outcome —
3244
+ // a script's input is a whole program, so the JSON preview is worth nothing there.
3245
+ const script = scriptToolLine(short, block);
3246
+ const target = script ? script.target : parts.slice(1).join(' ');
3247
+ const preview = script ? '' : clipInput(block.input);
2875
3248
  rowEl.appendChild(make('span', 'ask-tool-target', preview ? (target ? `${target} · ${preview}` : preview) : target));
2876
3249
  const note = block.status === 'error' ? 'error' : block.status === 'running' ? '…' : fmtElapsed(block.durationMs);
2877
3250
  rowEl.appendChild(make('span', 'ask-tool-note', note || ''));
@@ -2987,6 +3360,7 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
2987
3360
  const agentBlocks = blocks.filter((b) => b && b.kind === 'agent');
2988
3361
  if (agentBlocks.length && !agents) {
2989
3362
  agents = make('div', 'ask-agents');
3363
+ agents.dataset.minLevel = 'expert'; // per-agent logs (docs/ui-levels.md)
2990
3364
  const cap = make('div', 'ask-agents-cap');
2991
3365
  cap.appendChild(make('span', null, 'Sub-agents'));
2992
3366
  agentsCount = make('span', 'ask-agents-count', '');