@worca/app 1.0.0 → 1.1.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 (138) hide show
  1. package/README.md +22 -9
  2. package/agents/clarify.meta.json +4 -4
  3. package/agents/decomposer.meta.json +5 -5
  4. package/agents/implementer.meta.json +15 -5
  5. package/agents/manualTestsChecklist.meta.json +5 -4
  6. package/agents/manualWebUiTesting.meta.json +9 -4
  7. package/agents/planReviewer.meta.json +12 -4
  8. package/agents/planner.meta.json +12 -5
  9. package/agents/refiner.meta.json +15 -4
  10. package/agents/reviewer.meta.json +14 -4
  11. package/agents/worca-cc-clarify.md +7 -0
  12. package/agents/worca-cc-code-reviewer.md +11 -6
  13. package/agents/worca-cc-decomposer.md +7 -0
  14. package/agents/worca-cc-implementer.md +9 -0
  15. package/agents/worca-cc-manual-tests-checklist.md +8 -5
  16. package/agents/worca-cc-manual-web-ui-testing.md +10 -6
  17. package/agents/worca-cc-plan-refiner.md +11 -6
  18. package/agents/worca-cc-plan-reviewer.md +10 -7
  19. package/agents/worca-cc-planner.md +9 -0
  20. package/agents/worca-cc-workspace-reviewer.md +11 -4
  21. package/agents/worca-cc-workspace-scanner.md +8 -4
  22. package/agents/workspaceReviewer.meta.json +15 -4
  23. package/agents/workspaceScanner.meta.json +5 -4
  24. package/package.json +8 -2
  25. package/skills/worca/SKILL.md +5 -5
  26. package/src/cli/render.mjs +148 -0
  27. package/src/cli/worca-cc.mjs +319 -45
  28. package/src/core/agent-gen.mjs +69 -31
  29. package/src/core/agent-registry.mjs +124 -144
  30. package/src/core/agent-store.mjs +164 -4
  31. package/src/core/artifacts.mjs +189 -21
  32. package/src/core/ask/catalog.mjs +111 -0
  33. package/src/core/ask/comment-deps.mjs +55 -0
  34. package/src/core/ask/events.mjs +506 -0
  35. package/src/core/ask/follow.mjs +107 -0
  36. package/src/core/ask/git-allowlist.mjs +226 -0
  37. package/src/core/ask/limits.mjs +54 -0
  38. package/src/core/ask/mcp-stdio.mjs +135 -0
  39. package/src/core/ask/models.mjs +125 -0
  40. package/src/core/ask/prompt.mjs +261 -0
  41. package/src/core/ask/proposal.mjs +170 -0
  42. package/src/core/ask/redact.mjs +30 -0
  43. package/src/core/ask/spawn.mjs +153 -0
  44. package/src/core/ask/store.mjs +360 -0
  45. package/src/core/ask/tool-deps.mjs +63 -0
  46. package/src/core/ask/tools.mjs +848 -0
  47. package/src/core/ask/turn.mjs +416 -0
  48. package/src/core/ask/worktree-deps.mjs +27 -0
  49. package/src/core/ask/worktrees.mjs +285 -0
  50. package/src/core/chat/command-router.mjs +20 -3
  51. package/src/core/claude-runner.mjs +434 -57
  52. package/src/core/config.mjs +264 -41
  53. package/src/core/cost-budget.mjs +29 -2
  54. package/src/core/db.mjs +684 -47
  55. package/src/core/diff-anchor.mjs +213 -0
  56. package/src/core/diff-comments.mjs +273 -0
  57. package/src/core/engine-select.mjs +32 -0
  58. package/src/core/git-info.mjs +49 -10
  59. package/src/core/graph/builtin-workflows.mjs +51 -0
  60. package/src/core/graph/executor.mjs +894 -0
  61. package/src/core/graph/registry-ports.mjs +12 -0
  62. package/src/core/graph/scheduler.mjs +1065 -0
  63. package/src/core/graph/seed-templates.mjs +318 -0
  64. package/src/core/model-env.mjs +112 -8
  65. package/src/core/model-test.mjs +79 -0
  66. package/src/core/orchestrator.mjs +902 -4098
  67. package/src/core/overview-agent.mjs +15 -3
  68. package/src/core/phases.mjs +208 -537
  69. package/src/core/pipeline-delete.mjs +13 -2
  70. package/src/core/plugin-api.mjs +8 -3
  71. package/src/core/plugin-config.mjs +178 -28
  72. package/src/core/plugin-inventory.mjs +6 -2
  73. package/src/core/plugin-manifest.mjs +199 -11
  74. package/src/core/plugin-models.mjs +1 -0
  75. package/src/core/plugin-repo.mjs +16 -4
  76. package/src/core/plugin-shim-child.mjs +9 -3
  77. package/src/core/plugin-shim.mjs +77 -14
  78. package/src/core/plugin-store.mjs +236 -29
  79. package/src/core/plugin-workflows.mjs +90 -41
  80. package/src/core/preflight.mjs +135 -3
  81. package/src/core/projects.mjs +7 -5
  82. package/src/core/protocol.mjs +8 -35
  83. package/src/core/recoverable-error.mjs +1 -1
  84. package/src/core/run-harness.mjs +3585 -0
  85. package/src/core/run-manifest.mjs +5 -1
  86. package/src/core/settings.mjs +109 -13
  87. package/src/core/skills.mjs +10 -3
  88. package/src/core/source-bindings.mjs +175 -0
  89. package/src/core/sources.mjs +87 -25
  90. package/src/core/stats.mjs +25 -6
  91. package/src/core/title.mjs +51 -4
  92. package/src/core/workflows.mjs +358 -259
  93. package/src/core/workspace-scan.mjs +4 -0
  94. package/src/core/worktree.mjs +98 -7
  95. package/src/shared/graph/agent-meta.mjs +278 -0
  96. package/src/shared/graph/constants.mjs +105 -0
  97. package/src/shared/graph/geometry.mjs +157 -0
  98. package/src/shared/graph/layout.mjs +134 -0
  99. package/src/shared/graph/loops.mjs +130 -0
  100. package/src/shared/graph/manifest.mjs +257 -0
  101. package/src/shared/graph/ports.mjs +153 -0
  102. package/src/shared/graph/route.mjs +397 -0
  103. package/src/shared/graph/template.mjs +165 -0
  104. package/src/shared/graph/thumbnail.mjs +67 -0
  105. package/src/shared/graph/validate.mjs +491 -0
  106. package/src/shared/graph/verdict.mjs +41 -0
  107. package/ui/public/app.js +4008 -1670
  108. package/ui/public/ask-markdown.mjs +145 -0
  109. package/ui/public/ask-model.mjs +264 -0
  110. package/ui/public/ask-panel.mjs +1880 -0
  111. package/ui/public/chat-settings-view.mjs +6 -2
  112. package/ui/public/diff-view.mjs +66 -11
  113. package/ui/public/file-tree.mjs +305 -0
  114. package/ui/public/graph/composer.mjs +889 -0
  115. package/ui/public/graph/inspector.mjs +183 -0
  116. package/ui/public/graph/model.mjs +37 -0
  117. package/ui/public/graph/palette.mjs +144 -0
  118. package/ui/public/graph/run-decor.mjs +410 -0
  119. package/ui/public/graph/run-hosts.mjs +201 -0
  120. package/ui/public/graph/save-dialog.mjs +56 -0
  121. package/ui/public/graph/view.mjs +858 -0
  122. package/ui/public/guardrails-view.mjs +4 -2
  123. package/ui/public/hljs-loader.mjs +180 -0
  124. package/ui/public/index.html +269 -265
  125. package/ui/public/log-filter.mjs +22 -4
  126. package/ui/public/log-line.mjs +45 -19
  127. package/ui/public/models-view.mjs +171 -9
  128. package/ui/public/plugins-view.mjs +106 -4
  129. package/ui/public/source-pane.mjs +190 -8
  130. package/ui/public/stats-view.mjs +81 -1
  131. package/ui/public/style.css +1459 -229
  132. package/ui/public/syntax-highlight.mjs +270 -0
  133. package/ui/public/thinking-orb.mjs +110 -0
  134. package/ui/server.mjs +1667 -98
  135. package/src/core/channels.mjs +0 -302
  136. package/src/core/runners.mjs +0 -167
  137. package/src/core/workflow-validator.mjs +0 -185
  138. package/ui/public/composer-core.mjs +0 -211
@@ -3,9 +3,9 @@
3
3
  // archive exists so these numbers survive history cleanup (spec §6.9). Pure
4
4
  // synchronous DB reads; no gh, no git, offline-correct.
5
5
  //
6
- // Attribution: money = cost_ledger by event timestamp (exact); runs, time,
7
- // and PRs = cohort by started_at (a run belongs to the bucket its start falls
8
- // in, with its CURRENT status). 'today' differs: its totals count runs whose
6
+ // Attribution: money = cost_ledger + ask_cost_ledger by event timestamp
7
+ // (exact); runs, time, and PRs = cohort by started_at (a run belongs to the
8
+ // bucket its start falls in, with its CURRENT status). 'today' differs: its totals count runs whose
9
9
  // lifespan [started_at, updated_at] overlaps the day ("active today"), and its
10
10
  // hourly bars attribute terminal outcomes to the hour of the terminal write
11
11
  // (updated_at). 'all' money/time use the fallback-aware pipelines sums so
@@ -14,6 +14,7 @@
14
14
  import { prepare } from './db.mjs';
15
15
  import {
16
16
  budgetStatus, costWindowStart, costWindowEnd, allTimeTotals, roundUsd,
17
+ askWindowedSpendUsd,
17
18
  } from './cost-budget.mjs';
18
19
 
19
20
  const RANGES = ['today', 'week', 'month', 'all'];
@@ -84,6 +85,18 @@ function ledgerSpend(fromMs, toMs) {
84
85
  return roundUsd(row?.s || 0);
85
86
  }
86
87
 
88
+ /** Ask Worca window aggregate (ask-cost-statistics-design.md D5/D6): spend,
89
+ * DISTINCT sessions, turns. A thread is a session in every window it has a
90
+ * costed turn in. */
91
+ function askTotals(fromMs, toMs) {
92
+ const row = prepare(`
93
+ SELECT COALESCE(SUM(amount_usd), 0) AS s,
94
+ COUNT(DISTINCT thread_id) AS sessions,
95
+ COUNT(*) AS turns
96
+ FROM ask_cost_ledger WHERE ts >= ? AND ts < ?`).get(fromMs, toMs);
97
+ return { spendUsd: roundUsd(row?.s || 0), sessions: row?.sessions || 0, turns: row?.turns || 0 };
98
+ }
99
+
87
100
  /** Build zero-filled buckets [{startMs, endMs}] from windowStart through `now`. */
88
101
  function buildBuckets(windowStart, now, bucket) {
89
102
  const out = [];
@@ -137,8 +150,11 @@ export function getStats({ range = 'month', now = new Date() } = {}) {
137
150
  const windowTotalsFn = range === 'today' ? activeTotals : cohortTotals;
138
151
  const shapeTotals = (fromMs, toMs) => {
139
152
  const c = windowTotalsFn(fromMs, toMs);
153
+ const pipelineSpendUsd = ledgerSpend(fromMs, toMs);
154
+ const ask = askTotals(fromMs, toMs);
140
155
  return {
141
- spentUsd: ledgerSpend(fromMs, toMs),
156
+ spentUsd: roundUsd(pipelineSpendUsd + ask.spendUsd),
157
+ pipelineSpendUsd, ask,
142
158
  workedMs: c.workedMs,
143
159
  runs: c.runs, finished: c.finished, stopped: c.stopped, failed: c.failed,
144
160
  paused: c.paused, running: c.running,
@@ -150,8 +166,11 @@ export function getStats({ range = 'month', now = new Date() } = {}) {
150
166
  if (range === 'all') {
151
167
  const at = allTimeTotals();
152
168
  const c = cohortTotals(0, windowEnd.getTime());
169
+ const ask = askTotals(0, windowEnd.getTime());
153
170
  totals = {
154
- spentUsd: at.spendUsd, workedMs: at.activeMs,
171
+ spentUsd: roundUsd(at.spendUsd + ask.spendUsd),
172
+ pipelineSpendUsd: at.spendUsd, ask,
173
+ workedMs: at.activeMs,
155
174
  runs: c.runs, finished: c.finished, stopped: c.stopped, failed: c.failed,
156
175
  paused: c.paused, running: c.running,
157
176
  prsOpened: c.prsOpened, prsMerged: c.prsMerged,
@@ -169,7 +188,7 @@ export function getStats({ range = 'month', now = new Date() } = {}) {
169
188
  const c = bucketTotalsFn(startMs, endMs);
170
189
  return {
171
190
  bucketStartMs: startMs,
172
- spentUsd: ledgerSpend(startMs, endMs),
191
+ spentUsd: roundUsd(ledgerSpend(startMs, endMs) + askWindowedSpendUsd(startMs, endMs)),
173
192
  finished: c.finished, stopped: c.stopped, failed: c.failed,
174
193
  };
175
194
  });
@@ -11,6 +11,8 @@ const SYSTEM = [
11
11
  'You write a SHORT, human-readable title for a software task.',
12
12
  'Rules: 3–8 words, Title Case-ish, no trailing period, no quotes, no markdown,',
13
13
  'no preamble. Output ONLY the title on a single line.',
14
+ 'Never ask questions, never ask for more context, never apologize or explain.',
15
+ 'If the task is vague or conversational, still output your best-guess topic title.',
14
16
  ].join(' ');
15
17
 
16
18
  /** Normalize raw model output into a safe single-line title (pure, exported for tests). */
@@ -21,14 +23,45 @@ export function sanitizeTitle(raw) {
21
23
  t = t.replace(/^(?:title|task)\s*[:\-]\s*/i, ''); // strip "Title:"/"Task:" label
22
24
  t = t.replace(/^["'“”`]+|["'“”`]+$/g, '').trim(); // strip wrapping quotes/backticks
23
25
  t = t.replace(/\s+/g, ' ').replace(/\.+$/, '').trim(); // collapse ws, drop trailing dots
24
- return t.slice(0, MAX_LEN).trim();
26
+ if (t.length > MAX_LEN) {
27
+ // Cut at a word boundary instead of mid-word ("…you'd li"). The window is
28
+ // MAX_LEN+1 so a word ending exactly at the cap survives; a boundary too
29
+ // close to the start (or none at all — one unbroken run) hard-slices.
30
+ const cut = t.slice(0, MAX_LEN + 1);
31
+ const sp = cut.lastIndexOf(' ');
32
+ t = (sp >= 30 ? cut.slice(0, sp) : t.slice(0, MAX_LEN)).replace(/[\s,;:–—-]+$/, '');
33
+ }
34
+ return t.trim();
35
+ }
36
+
37
+ // A title that is really a clarifying question / refusal ("I need more context
38
+ // to write a title…"). First-person or plea openers, a trailing question mark,
39
+ // or prose far beyond the 3–8-word instruction all mean the model did not
40
+ // title the task — the caller should keep its provisional title instead.
41
+ // Narrow on purpose (review of PR #376): bare `what`/`which`/`unable`/`please`
42
+ // openers dropped legitimate titles ("What's New Page Redesign", "Which Tab Is
43
+ // Active Indicator", "Unable To Login Error Fix", "Please Wait Spinner Timing").
44
+ // A question is caught by its trailing `?` above; `unable`/`please` count only
45
+ // with the verbs a refusal actually uses.
46
+ const REFUSAL_OPENER_RE =
47
+ /^(?:i(?:['’](?:m|d|ll|ve))?\s|sorry\b|apolog|unfortunately\b|unable to (?:determine|identify|generate|create|provide|write|title|summari[sz]e|infer|tell|help|assist)\b|please (?:provide|share|clarify|give|specify|tell|include|describe|let me know)\b|could you\b|can you\b|tell me\b|what(?:['’]s)? (?:is|are|was|were|do|does|did|would|should|task|work)\b|which (?:task|one|of)\b|help me\b)/i;
48
+ const MAX_WORDS = 12;
49
+
50
+ /** True when sanitized model output looks like a refusal/question, not a title (pure, exported for tests). */
51
+ export function isRefusalTitle(t) {
52
+ if (!t || typeof t !== 'string') return false;
53
+ if (/\?$/.test(t)) return true;
54
+ if (REFUSAL_OPENER_RE.test(t)) return true;
55
+ return t.split(' ').length > MAX_WORDS;
25
56
  }
26
57
 
27
58
  /**
28
59
  * Produce a concise LLM title for a prompt. Never throws — returns '' on any
29
60
  * failure/abort/empty input so the caller keeps the provisional title.
61
+ * `permissionMode` (default 'acceptEdits') exists for Ask Worca: its title call
62
+ * passes 'dontAsk' so the mock dispatcher can never reach a file-writing role.
30
63
  * @param {string} prompt
31
- * @param {{cwd:string, signal?:AbortSignal, model?:string, bin?:string, envScrub?:boolean, envAllowlist?:string[]}} opts
64
+ * @param {{cwd:string, signal?:AbortSignal, model?:string, bin?:string, mock?:boolean, envScrub?:boolean, envAllowlist?:string[], tools?:string[], strictMcpConfig?:boolean, settingSources?:string[], disableSlashCommands?:boolean, mcpConfigPath?:string, permissionMode?:string}} opts
32
65
  * @returns {Promise<string>}
33
66
  */
34
67
  export async function generateTitle(prompt, opts = {}) {
@@ -45,18 +78,32 @@ export async function generateTitle(prompt, opts = {}) {
45
78
  // env everywhere the id is used.
46
79
  modelEnv: resolveModelEnv(opts.model || DEFAULT_TITLE_MODEL),
47
80
  effort: 'low',
48
- permissionMode: 'acceptEdits',
81
+ permissionMode: opts.permissionMode || 'acceptEdits',
49
82
  allowedTools: [], // empty → no --allowedTools flag → claude defaults; pure text gen
50
83
  signal: opts.signal,
51
84
  // Title generation runs DURING a pipeline run, so it must honor the same
52
85
  // guardrails as the run itself. All three are undefined when absent, and
53
86
  // runClaude treats undefined as "not passed" — existing callers are unchanged.
54
87
  bin: opts.bin,
88
+ // A run configured with claude:{mock:true} (tests, `--mock`) must title
89
+ // through runMock too: runClaude decides mock-vs-real from THIS field or
90
+ // WORCA_MOCK, and the 29 test files that only pass the option spawned the
91
+ // developer's real binary 157x per `npm test` (2026-08-30).
92
+ mock: opts.mock,
55
93
  envScrub: opts.envScrub,
56
94
  envAllowlist: opts.envAllowlist,
95
+ // Ask Worca (ask-worca-design.md §6.8): sandbox hardening pass-through for the
96
+ // chat's background title call. All undefined for every existing caller, and
97
+ // runClaude emits nothing for undefined — pipeline title argv is unchanged.
98
+ tools: opts.tools,
99
+ strictMcpConfig: opts.strictMcpConfig,
100
+ settingSources: opts.settingSources,
101
+ disableSlashCommands: opts.disableSlashCommands,
102
+ mcpConfigPath: opts.mcpConfigPath,
57
103
  onEvent: () => {},
58
104
  });
59
- return sanitizeTitle(out);
105
+ const title = sanitizeTitle(out);
106
+ return isRefusalTitle(title) ? '' : title;
60
107
  } catch (err) {
61
108
  if (err && err.name === 'AbortError') return ''; // run was stopped — caller keeps provisional
62
109
  return '';