@worca/app 1.3.0 → 1.4.0-rc.1

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
@@ -25,6 +25,11 @@
25
25
  // question there and STOPS (no role side effects); the
26
26
  // resumed prompt carries no MOCK_ASK, so the role arm
27
27
  // runs then.
28
+ // MOCK_ASK_FORM: <json> a ONE-LINE {"form","data"} payload. When present it is
29
+ // written verbatim instead of the canned {questions}
30
+ // body — to MOCK_ASK for a producer, to MOCK_OUT for the
31
+ // clarify role. Lets an offline mock agent exercise the
32
+ // ask-form protocol end to end.
28
33
  //
29
34
  // Markers are matched leniently: "KEY: value" anywhere at the start of a line,
30
35
  // case-sensitive keys, value trimmed. Missing markers degrade gracefully.
@@ -346,9 +351,11 @@ export function mockEnabled(opts) {
346
351
  * @param {number} [o.maxTurns] --max-turns <n> (positive safe integer; else omitted)
347
352
  * @param {number|null} [o.maxBudgetUsd] --max-budget-usd <n> (finite > 0; null/else omitted)
348
353
  * @param {string} [o.appendSubagentSystemPrompt] --append-subagent-system-prompt <text> (Task children only)
349
- * @param {string[]} [o.addDirs] --add-dir <dir> per entry (Ask Worca's memory mount; the CLI loads <dir>/.claude/rules only with CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1 in the env — memory-deps.mjs / spawn.mjs set it)
354
+ * @param {string[]} [o.addDirs] --add-dir <dir> per entry (Ask Worca's memory mount and every pipeline spawn's writable memory copy; the CLI loads <dir>/.claude/rules only with CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1 in the env — memory-deps.mjs / spawn.mjs set it)
350
355
  * All nine are Ask Worca sandbox options (ask-worca-design.md §6.3) and default-off.
351
356
  * @param {number} [o.argvInlineLimit] override ARGV_INLINE_LIMIT (GH #380; tests force the staged path)
357
+ * @param {string[]} [o.disallowedTools] --disallowedTools <list>: built-ins withheld from this spawn
358
+ * (model bridge §5.3: WebSearch/WebFetch for a translated model). Absent/empty ⇒ flag omitted.
352
359
  * @returns {Promise<{text:string, exitCode:number}>}
353
360
  */
354
361
  export async function runClaude(o = {}) {
@@ -373,6 +380,7 @@ export async function runClaude(o = {}) {
373
380
  envScrub,
374
381
  envAllowlist,
375
382
  modelEnv,
383
+ disallowedTools,
376
384
  workspaceWriteTargets,
377
385
  resumeSessionId,
378
386
  // Ask Worca sandbox hardening (ask-worca-design.md §6.3/§6.8). All default-off:
@@ -422,6 +430,7 @@ export async function runClaude(o = {}) {
422
430
  envScrub,
423
431
  envAllowlist,
424
432
  modelEnv,
433
+ disallowedTools,
425
434
  tools,
426
435
  strictMcpConfig,
427
436
  settingSources,
@@ -453,9 +462,10 @@ export async function runClaude(o = {}) {
453
462
  * (docs/run-root-verification.md, branch (a); argv-attested
454
463
  * transcript phase0/out/v1a-rerun.jsonl, with a no-grant
455
464
  * negative control proving the grant is load-bearing).
456
- * `--add-dir` carries Ask Worca's memory mount ONLY (`addDirs`): it needs the
457
- * CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1 env override to load memory at all
458
- * (E2, re-probed 2026-09-13 on 2.1.270), and no pipeline path passes it (§5.3 / §8.18 unchanged). */
465
+ * `--add-dir` carries Ask Worca's memory mount (with the
466
+ * CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1 override, which is what makes the CLI LOAD rules
467
+ * from it) and, since the memory write split, every pipeline spawn's writable memory copy (no
468
+ * override: nothing is loaded from it, it is only made editable under acceptEdits). */
459
469
  export function buildClaudeArgs({
460
470
  prompt, systemPrompt, permissionMode, model, effort, allowedTools, resumeSessionId,
461
471
  mcpConfigPath, mcpServerGrants, permissionRules,
@@ -463,7 +473,7 @@ export function buildClaudeArgs({
463
473
  // way in because the legacy body below already owns a local `tools` (the
464
474
  // --allowedTools union).
465
475
  tools: builtinTools, strictMcpConfig, settingSources, disableSlashCommands, includePartialMessages,
466
- maxTurns, maxBudgetUsd, appendSubagentSystemPrompt, hostGuard, addDirs,
476
+ maxTurns, maxBudgetUsd, appendSubagentSystemPrompt, hostGuard, addDirs, disallowedTools,
467
477
  }, delivery = {}) {
468
478
  // delivery (GH #380, set only by planClaudeInvocation's staged branch):
469
479
  // promptViaStdin -> bare `-p`; the prompt is written to the child's stdin
@@ -495,6 +505,12 @@ export function buildClaudeArgs({
495
505
  if (tools.length) {
496
506
  args.push('--allowedTools', tools.join(','));
497
507
  }
508
+ // Model bridge (model-bridge-design.md §5.3): a translated (openai-chat)
509
+ // model has no server-side web tools, so the runner withholds them outright —
510
+ // a deny outranks any allow, frontmatter grant included. Absent/empty ⇒
511
+ // nothing emitted, so every other argv stays byte-identical.
512
+ const denied = Array.isArray(disallowedTools) ? disallowedTools.filter((s) => typeof s === 'string' && s) : [];
513
+ if (denied.length) args.push('--disallowedTools', denied.join(','));
498
514
  // ── Ask Worca hardening flags (ask-worca-design.md §6.3 / §6.8) ──────────────
499
515
  // Every one is default-off: absent / false / invalid ⇒ NOTHING is emitted, so
500
516
  // every legacy argv stays byte-identical (test/spawn-args.test.mjs). Appended
@@ -579,7 +595,7 @@ export function stageClaudeInvocation(opts, { bin = DEFAULT_BIN, limit = ARGV_IN
579
595
  return { ...plan, dir };
580
596
  }
581
597
 
582
- function runReal({ cwd, systemPrompt, prompt, allowedTools, permissionMode, model, effort, onEvent, signal, bin, resumeSessionId, mcpConfigPath, mcpServerGrants, permissionRules, envScrub, envAllowlist, modelEnv, tools, strictMcpConfig, settingSources, disableSlashCommands, includePartialMessages, maxTurns, maxBudgetUsd, appendSubagentSystemPrompt, addDirs, argvInlineLimit }) {
598
+ function runReal({ cwd, systemPrompt, prompt, allowedTools, permissionMode, model, effort, onEvent, signal, bin, resumeSessionId, mcpConfigPath, mcpServerGrants, permissionRules, envScrub, envAllowlist, modelEnv, disallowedTools, tools, strictMcpConfig, settingSources, disableSlashCommands, includePartialMessages, maxTurns, maxBudgetUsd, appendSubagentSystemPrompt, addDirs, argvInlineLimit }) {
583
599
  return new Promise((resolveP, rejectP) => {
584
600
  // Per-model routing env (design §4.4), prepared BEFORE argv: reserved keys
585
601
  // are re-dropped here defensively — the write path already rejects them, so
@@ -658,7 +674,7 @@ function runReal({ cwd, systemPrompt, prompt, allowedTools, permissionMode, mode
658
674
  permissionMode, model: wireModel, effort, allowedTools, resumeSessionId,
659
675
  mcpConfigPath, mcpServerGrants, permissionRules,
660
676
  tools, strictMcpConfig, settingSources, disableSlashCommands, includePartialMessages,
661
- maxTurns, maxBudgetUsd, appendSubagentSystemPrompt, addDirs,
677
+ maxTurns, maxBudgetUsd, appendSubagentSystemPrompt, addDirs, disallowedTools,
662
678
  }, { bin: resolved.bin, limit });
663
679
  } catch (err) {
664
680
  rejectP(new Error(`Failed to stage the claude prompt files: ${err.message}`));
@@ -946,6 +962,17 @@ function parseMarkers(prompt, systemPrompt) {
946
962
  return markers;
947
963
  }
948
964
 
965
+ /** Parse a MOCK_*_FORM marker's one-line JSON, or null. A malformed marker must
966
+ * degrade to the canned body, never throw inside the mock. */
967
+ function tryParseMockJson(raw) {
968
+ try {
969
+ const v = JSON.parse(String(raw));
970
+ return v && typeof v === 'object' ? v : null;
971
+ } catch {
972
+ return null;
973
+ }
974
+ }
975
+
949
976
  async function ensureDir(filePath) {
950
977
  await mkdir(dirname(filePath), { recursive: true });
951
978
  }
@@ -1085,21 +1112,51 @@ async function mockAsk({ markers, prompt, cwd, onEvent, signal, resumeSessionId
1085
1112
  const wfEvent = /^\s*\[worca event\] workflow card (card_[0-9a-f]{8}) (?:(declined)|saved as (\S+) "([^"]*)"; thenRun=(true|false))/.exec(userText);
1086
1113
  // A metrics-card EVENT, then the metrics trigger — both before the run arm, whose \brun\b would otherwise fire on
1087
1114
  // "include my runs"-style prose (it does not, \b stops at the s, but "propose" would).
1088
- const tmEvent = /^\s*\[worca event\] metrics card (card_[0-9a-f]{8}) (applied|declined|failed)/.exec(userText);
1115
+ const tmEvent = /^\s*\[worca event\] (?:metrics|policy|model) card (card_[0-9a-f]{8}) (applied|declined|failed)/.exec(userText);
1089
1116
  // The metrics arm wants a CHANGE, not a question: "metrics" plus a verb of intent ("stop recording my metrics",
1090
1117
  // "route ... to the metrics home"). A bare "which workspaces use team metrics?" gets the generic echo answer.
1091
1118
  const metrics = !wfEvent && !tmEvent && /\bmetrics\b/i.test(userText)
1092
1119
  && /\b(?:stop|start|turn|toggle|switch|record\w*|route|change|enable|disable|set)\b/i.test(userText);
1093
- const workflow = !wfEvent && !tmEvent && !metrics && /\bworkflow\b/i.test(userText);
1120
+ // Scheduled runs (docs/scheduled-runs.md "Ask Worca"): a schedule-card EVENT; a CHANGE to an existing schedule
1121
+ // (its id in the text); or a new run to schedule ("schedule …"). All before the run arm, whose \brun\b would fire.
1122
+ const scEvent = /^\s*\[worca event\] schedule card (card_[0-9a-f]{8}) (applied|declined|failed)/.exec(userText);
1123
+ const scId = /\b(sch_[0-9a-f]{8}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\b/i.exec(userText);
1124
+ const scChange = !wfEvent && !tmEvent && !scEvent && !!scId && /\b(?:run now|move|delete|cancel|edit|change)\b/i.test(userText);
1125
+ const scNew = !wfEvent && !tmEvent && !scEvent && !scChange && /\bschedul/i.test(userText);
1126
+ // The team-policy arm, the metrics rule: "policy" plus a verb of intent ("raise the policy cap to $30").
1127
+ // It proposes an edit of the context project's per-pipeline cap: `$<n>` in the text, else $30.
1128
+ const policy = !wfEvent && !tmEvent && !metrics && !scEvent && !scNew && !scChange && /\bpolicy\b/i.test(userText)
1129
+ && /\b(?:raise|lower|set|change|edit|make|cap)\b/i.test(userText);
1130
+ // A tracker task named by key ("fix jira bug PROJ-123"): the run's task is the issue (mock-source's
1131
+ // fixture plugin), and "auto" asks for the Auto workflow. Folds into the schedule and run arms.
1132
+ const taskKey = !wfEvent && !tmEvent && !scEvent && /\b(?:issue|ticket|bug|task)\b/i.test(userText) ? /\b([A-Z][A-Z0-9]+-\d+)\b/.exec(userText) : null;
1133
+ const wantsAuto = /\bauto\b/i.test(userText);
1134
+ const workflow = !wfEvent && !tmEvent && !metrics && !policy && !scNew && !scChange && !taskKey && /\bworkflow\b/i.test(userText);
1094
1135
  const agents = !wfEvent && !tmEvent && /\bagents?\b/i.test(userText);
1095
- const propose = !wfEvent && !tmEvent && !workflow && !metrics && /\b(propose|start|run)\b/i.test(userText);
1136
+ // Models (docs/models.md "Ask Worca"): "add a local llama model" proposes a keyless llama.cpp entry; "remove model
1137
+ // <id>" its removal. The parent re-validates the INPUT against the real catalog and mints the card.
1138
+ const modelAdd = !wfEvent && !tmEvent && !scEvent && /\bllama\b/i.test(userText) && /\b(?:add|register)\b/i.test(userText);
1139
+ const modelRemove = !wfEvent && !tmEvent && !scEvent && !modelAdd ? /\bremove model ([A-Za-z0-9._-]+)/i.exec(userText) : null;
1140
+ const propose = !wfEvent && !tmEvent && !scEvent && !workflow && !metrics && !policy && !scNew && !scChange && !modelAdd && !modelRemove && (!!taskKey || /\b(propose|start|run)\b/i.test(userText));
1141
+ // The proposal both arms send: a brief, or the task reference instead of one.
1142
+ const proposal = () => {
1143
+ if (!taskKey) return { ...card, ...(wantsAuto ? { workflowId: 'wf_auto' } : {}) };
1144
+ const { brief: _brief, ...rest } = card;
1145
+ return { ...rest, ...(wantsAuto ? { workflowId: 'wf_auto' } : {}), source: { plugin: 'mock-source', sourceId: 'mock', taskId: taskKey[1] }, note: 'mock: the run reads the issue when it starts' };
1146
+ };
1147
+ const taskFrames = (MSG) => (taskKey ? [
1148
+ atool(MSG, 'toolu_mock_sources', 'mcp__worca__list_task_sources', {}),
1149
+ uresult('toolu_mock_sources', JSON.stringify({ sources: [{ plugin: 'mock-source', sourceId: 'mock', displayName: 'Mock Tasks' }] })),
1150
+ atool(MSG, 'toolu_mock_find', 'mcp__worca__find_tasks', { plugin: 'mock-source', sourceId: 'mock', search: taskKey[1] }),
1151
+ uresult('toolu_mock_find', JSON.stringify({ tasks: [{ id: taskKey[1], title: 'Mock task' }] })),
1152
+ ] : []);
1096
1153
 
1097
1154
  const SID = resumeSessionId || 'mock-session-ask-1';
1098
1155
  const USAGE = { input_tokens: 10, output_tokens: 20, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 };
1099
1156
  const firstLine = userText.split(/\r?\n/).map((l) => l.trim()).find(Boolean) || '';
1100
1157
  const ANSWER = `[mock] ${firstLine.slice(0, 200)}`;
1101
1158
  const init = { type: 'system', subtype: 'init', session_id: SID, cwd, model: 'mock', permissionMode: 'dontAsk',
1102
- tools: ['Task', 'mcp__worca__list_runs', 'mcp__worca__get_run', 'mcp__worca__propose_run', 'mcp__worca__propose_workflow', 'mcp__worca__propose_metrics_change'],
1159
+ tools: ['Task', 'mcp__worca__list_runs', 'mcp__worca__get_run', 'mcp__worca__propose_run', 'mcp__worca__propose_workflow', 'mcp__worca__propose_metrics_change', 'mcp__worca__propose_policy_change'],
1103
1160
  mcp_servers: [{ name: 'worca', status: 'connected' }], plugins: [], skills: [], slash_commands: [], agents: [], uuid: 'mock-uuid-init' };
1104
1161
  const mstart = (id) => ({ type: 'stream_event', event: { type: 'message_start', message: { id, model: 'mock', role: 'assistant', content: [], usage: USAGE } }, parent_tool_use_id: null, session_id: SID });
1105
1162
  const delta = (t) => ({ type: 'stream_event', event: { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: t } }, parent_tool_use_id: null, session_id: SID });
@@ -1155,6 +1212,39 @@ async function mockAsk({ markers, prompt, cwd, onEvent, signal, resumeSessionId
1155
1212
  uresult('toolu_mock_metrics', JSON.stringify({ ok: true, card: { type: 'metrics', ...tmInput } })));
1156
1213
  answerMsg = MSG2;
1157
1214
  }
1215
+ if (policy) {
1216
+ const usd = Number((/\$\s*(\d+(?:\.\d+)?)/.exec(userText) || [])[1] || 30);
1217
+ const tpInput = { kind: 'edit', projectKey: card.projectKey || null, set: [{ key: 'cost.pipelineLimitUsd', value: usd, kind: 'soft' }], note: 'mock: change the team per-pipeline cap' };
1218
+ frames.push(delta('[mock] '), delta('proposing '), delta('a policy change'), atext(MSG1, 'Proposing a policy change card.'),
1219
+ atool(MSG1, 'toolu_mock_policy', 'mcp__worca__propose_policy_change', tpInput),
1220
+ uresult('toolu_mock_policy', JSON.stringify({ ok: true, card: { type: 'policy', ...tpInput } })));
1221
+ answerMsg = MSG2;
1222
+ }
1223
+ if (scNew) {
1224
+ // The parent re-validates the INPUT with the real validator: "every" in the text makes a weekday series,
1225
+ // anything else a one-off two minutes out (short enough to watch the server start it).
1226
+ const every = /\bevery\b/i.test(userText);
1227
+ const input = { ...proposal(), ...(every ? { every: 'weekdays 02:00' } : { when: '+2m' }) };
1228
+ frames.push(...taskFrames(MSG1));
1229
+ frames.push(delta('[mock] '), delta('scheduling '), delta('a run'), atext(MSG1, every ? 'Scheduling it every weekday at 02:00.' : 'Scheduling it two minutes from now.'),
1230
+ atool(MSG1, 'toolu_mock_preview', 'mcp__worca__preview_schedule', every ? { every: 'weekdays 02:00' } : { when: '+2m' }),
1231
+ uresult('toolu_mock_preview', JSON.stringify({ ok: true, kind: every ? 'repeat' : 'once' })),
1232
+ atool(MSG1, 'toolu_mock_propose', 'mcp__worca__propose_run', input), uresult('toolu_mock_propose', JSON.stringify({ ok: true })));
1233
+ answerMsg = MSG2;
1234
+ }
1235
+ if (scChange) {
1236
+ const t = userText.toLowerCase();
1237
+ const action = /run now/.test(t) ? 'run_now' : /\bmove\b/.test(t) ? 'move' : /\bdelete\b/.test(t) ? 'delete' : /\bcancel\b/.test(t) ? 'cancel' : 'edit';
1238
+ const input = { id: scId[1], action, ...(action === 'move' ? { when: '+5m' } : action === 'edit' ? { every: 'weekdays 03:00' } : {}), note: 'mock: as asked' };
1239
+ frames.push(delta('[mock] '), delta('proposing '), delta('a schedule change'), atext(MSG1, 'Proposing a schedule change card.'),
1240
+ atool(MSG1, 'toolu_mock_sched', 'mcp__worca__propose_schedule_change', input), uresult('toolu_mock_sched', JSON.stringify({ ok: true })));
1241
+ answerMsg = MSG2;
1242
+ }
1243
+ if (scEvent) {
1244
+ const line = scEvent[2] === 'declined' ? 'Declined — nothing changed.' : scEvent[2] === 'failed' ? 'The change failed; check the error and try again.' : 'Done.';
1245
+ frames.push(delta('[mock] '), delta(scEvent[2]), atext(MSG1, line));
1246
+ answerMsg = MSG2;
1247
+ }
1158
1248
  if (tmEvent) {
1159
1249
  const line = tmEvent[2] === 'declined' ? 'Declined — nothing changed.' : tmEvent[2] === 'failed' ? 'The change failed; check the error and try again.' : 'Applied.';
1160
1250
  frames.push(delta('[mock] '), delta(tmEvent[2]), atext(MSG1, line));
@@ -1173,9 +1263,17 @@ async function mockAsk({ markers, prompt, cwd, onEvent, signal, resumeSessionId
1173
1263
  }
1174
1264
  answerMsg = MSG2;
1175
1265
  }
1266
+ if (modelAdd || modelRemove) {
1267
+ const mInput = modelAdd
1268
+ ? { kind: 'add_model', model: { id: 'local-llama', label: 'Local llama', upstream: { provider: 'openai', api: 'openai-chat', model: 'qwen', baseUrl: 'http://127.0.0.1:8080/v1', capabilities: { maxPromptTokens: 65536, maxOutputTokens: 8192 } } }, note: 'mock: a local llama.cpp server' }
1269
+ : { kind: 'remove_model', id: modelRemove[1], note: 'mock: as asked' };
1270
+ frames.push(delta('[mock] '), delta('proposing '), delta('a model change'), atext(MSG1, 'Proposing a model change card.'),
1271
+ atool(MSG1, 'toolu_mock_model', 'mcp__worca__propose_model_change', mInput), uresult('toolu_mock_model', JSON.stringify({ ok: true })));
1272
+ answerMsg = MSG2;
1273
+ }
1176
1274
  if (propose) {
1177
- frames.push(delta('[mock] '), delta('preparing '), delta('a run'), atext(MSG1, 'Preparing a run card.'),
1178
- atool(MSG1, 'toolu_mock_propose', 'mcp__worca__propose_run', card), uresult('toolu_mock_propose', JSON.stringify({ ok: true })));
1275
+ frames.push(delta('[mock] '), delta('preparing '), delta('a run'), atext(MSG1, 'Preparing a run card.'), ...taskFrames(MSG1),
1276
+ atool(MSG1, 'toolu_mock_propose', 'mcp__worca__propose_run', proposal()), uresult('toolu_mock_propose', JSON.stringify({ ok: true })));
1179
1277
  answerMsg = MSG2;
1180
1278
  }
1181
1279
  if (answerMsg !== MSG1) frames.push(mstart(answerMsg));
@@ -1246,12 +1344,17 @@ async function runMock({ cwd, systemPrompt, prompt, onEvent, signal, resumeSessi
1246
1344
  // session event above already fired, so the resume has a session id.
1247
1345
  if (m.MOCK_ASK && permissionMode !== 'dontAsk') { // belt and braces: dontAsk already took the ask arm above
1248
1346
  await ensureDir(m.MOCK_ASK);
1249
- await writeFile(m.MOCK_ASK, JSON.stringify({
1347
+ // MOCK_ASK_FORM (ask-forms §4): a one-line {"form","data"} payload, written
1348
+ // verbatim. Unparseable => the canned questions body, so a typo degrades to
1349
+ // today's behaviour instead of writing garbage the gate then refuses.
1350
+ const formBody = m.MOCK_ASK_FORM ? tryParseMockJson(m.MOCK_ASK_FORM) : null;
1351
+ const body = formBody || {
1250
1352
  questions: [{ id: 'q1', question: `Mock question from ${role}?`, options: ['Option A', 'Option B'], allowFreeText: true }],
1251
- }, null, 2) + '\n', 'utf8');
1353
+ };
1354
+ await writeFile(m.MOCK_ASK, JSON.stringify(body, null, 2) + '\n', 'utf8');
1252
1355
  safeEmit(onEvent, { type: 'tool_use', text: `wrote ${m.MOCK_ASK}`, raw: { mock: true, file: m.MOCK_ASK } });
1253
1356
  safeEmit(onEvent, { type: 'result', costUsd: 0, raw: { mock: true, type: 'result', total_cost_usd: 0 } });
1254
- await emitLog(onEvent, `[mock] questions written; stopping for answers (role=${role})`);
1357
+ await emitLog(onEvent, `[mock] ${formBody ? 'form ask' : 'questions'} written; stopping for answers (role=${role})`);
1255
1358
  return { text: '[mock] asked questions', exitCode: 0 };
1256
1359
  }
1257
1360
 
@@ -1344,7 +1447,11 @@ async function mockClarify(m, cycle, onEvent) {
1344
1447
  // orchestrator's clarify loop terminates naturally. This mirrors the real fix:
1345
1448
  // the loop converges because answers are returned to the planner.
1346
1449
  const hasPrior = Number(m.MOCK_PRIOR || '0') > 0;
1347
- const payload = hasPrior
1450
+ // MOCK_ASK_FORM (ask-forms §4): a one-line {"form","data"} payload, written VERBATIM
1451
+ // to the answers port instead of the canned questions — the clarifier's half of the
1452
+ // marker (the producer's half is the MOCK_ASK arm in runMock). Unparseable => canned.
1453
+ const formBody = m.MOCK_ASK_FORM ? tryParseMockJson(m.MOCK_ASK_FORM) : null;
1454
+ const payload = formBody || (hasPrior
1348
1455
  ? { questions: [] }
1349
1456
  : {
1350
1457
  questions: [
@@ -1367,10 +1474,10 @@ async function mockClarify(m, cycle, onEvent) {
1367
1474
  allowFreeText: true,
1368
1475
  },
1369
1476
  ],
1370
- };
1477
+ });
1371
1478
  await emitLog(
1372
1479
  onEvent,
1373
- hasPrior
1480
+ formBody ? '[mock] clarifier asking with a form' : hasPrior
1374
1481
  ? '[mock] planner has no further questions'
1375
1482
  : '[mock] planner asking one clarifying question',
1376
1483
  );