@worca/app 1.2.0 → 1.3.0-rc.2

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 (104) hide show
  1. package/README.md +42 -0
  2. package/agents/memoryDefragmenter.meta.json +24 -0
  3. package/agents/worca-cc-code-reviewer.md +6 -1
  4. package/agents/worca-cc-implementer.md +6 -1
  5. package/agents/worca-cc-memory-defragmenter.md +32 -0
  6. package/agents/worca-cc-planner.md +5 -1
  7. package/package.json +5 -2
  8. package/src/cli/render.mjs +36 -0
  9. package/src/cli/worca-cc.mjs +137 -8
  10. package/src/core/agent-registry.mjs +12 -34
  11. package/src/core/artifacts.mjs +132 -8
  12. package/src/core/ask/catalog.mjs +32 -7
  13. package/src/core/ask/comment-deps.mjs +5 -2
  14. package/src/core/ask/events.mjs +65 -2
  15. package/src/core/ask/limits.mjs +9 -0
  16. package/src/core/ask/mcp-stdio.mjs +10 -0
  17. package/src/core/ask/memory-deps.mjs +107 -0
  18. package/src/core/ask/metrics-deps.mjs +124 -0
  19. package/src/core/ask/metrics-proposal.mjs +175 -0
  20. package/src/core/ask/prompt.mjs +53 -10
  21. package/src/core/ask/proposal.mjs +49 -2
  22. package/src/core/ask/spawn.mjs +21 -4
  23. package/src/core/ask/store.mjs +14 -5
  24. package/src/core/ask/tool-deps.mjs +26 -2
  25. package/src/core/ask/tools.mjs +439 -6
  26. package/src/core/ask/turn.mjs +163 -4
  27. package/src/core/ask/workflow-deps.mjs +226 -0
  28. package/src/core/auto/classify.mjs +352 -0
  29. package/src/core/auto/fingerprint.mjs +141 -0
  30. package/src/core/auto/match.mjs +30 -0
  31. package/src/core/auto/model.mjs +23 -0
  32. package/src/core/auto/proposal.mjs +132 -0
  33. package/src/core/auto/recipes.mjs +75 -0
  34. package/src/core/auto/repo-look.mjs +46 -0
  35. package/src/core/claude-runner.mjs +132 -11
  36. package/src/core/config.mjs +120 -3
  37. package/src/core/db.mjs +44 -1
  38. package/src/core/diff-comments.mjs +55 -9
  39. package/src/core/frontmatter.mjs +75 -0
  40. package/src/core/git-info.mjs +233 -26
  41. package/src/core/graph/builtin-workflows.mjs +50 -0
  42. package/src/core/graph/executor.mjs +11 -3
  43. package/src/core/index-html.mjs +17 -0
  44. package/src/core/memory-store.mjs +441 -0
  45. package/src/core/memory-sync.mjs +300 -0
  46. package/src/core/metrics/ledger.mjs +47 -0
  47. package/src/core/metrics/lock.mjs +117 -0
  48. package/src/core/metrics/read.mjs +303 -0
  49. package/src/core/metrics/record.mjs +389 -0
  50. package/src/core/metrics/sync.mjs +1100 -0
  51. package/src/core/onboarding.mjs +99 -0
  52. package/src/core/orchestrator.mjs +394 -7
  53. package/src/core/phases.mjs +16 -3
  54. package/src/core/pipeline-delete.mjs +1 -1
  55. package/src/core/plugin-store.mjs +2 -10
  56. package/src/core/preflight.mjs +2 -3
  57. package/src/core/projects.mjs +16 -1
  58. package/src/core/run-harness.mjs +458 -32
  59. package/src/core/run-report.mjs +896 -0
  60. package/src/core/settings.mjs +162 -0
  61. package/src/core/sources.mjs +4 -1
  62. package/src/core/store.mjs +5 -0
  63. package/src/core/workflow-export.mjs +2 -0
  64. package/src/core/workflow-share.mjs +1 -0
  65. package/src/core/workflows.mjs +43 -23
  66. package/src/core/workspaces.mjs +37 -8
  67. package/src/shared/graph/agent-meta.mjs +5 -2
  68. package/src/shared/graph/assemble.mjs +455 -0
  69. package/src/shared/graph/flow-layout.mjs +249 -0
  70. package/src/shared/graph/geometry.mjs +48 -28
  71. package/src/shared/graph/isomorphic.mjs +101 -0
  72. package/src/shared/report-reasons.mjs +58 -0
  73. package/src/shared/team-metrics/aggregate.mjs +341 -0
  74. package/src/shared/team-metrics/workspace-match.mjs +13 -0
  75. package/ui/public/about-links.mjs +21 -0
  76. package/ui/public/app.js +3715 -479
  77. package/ui/public/artifact-view.mjs +135 -0
  78. package/ui/public/ask-model.mjs +18 -1
  79. package/ui/public/ask-panel.mjs +1359 -214
  80. package/ui/public/ask-run-card.mjs +209 -0
  81. package/ui/public/assets/worca-logo-mask.png +0 -0
  82. package/ui/public/assets/worca-mark-mask.png +0 -0
  83. package/ui/public/auto-build.mjs +95 -0
  84. package/ui/public/auto-proposal.mjs +174 -0
  85. package/ui/public/comment-thread.mjs +55 -0
  86. package/ui/public/getting-started.mjs +261 -0
  87. package/ui/public/graph/composer.mjs +41 -5
  88. package/ui/public/graph/inspector.mjs +3 -1
  89. package/ui/public/graph/model.mjs +1 -0
  90. package/ui/public/graph/run-hosts.mjs +73 -12
  91. package/ui/public/graph/view.mjs +218 -50
  92. package/ui/public/guide-spot.mjs +215 -0
  93. package/ui/public/index.html +423 -25
  94. package/ui/public/memory-view.mjs +192 -0
  95. package/ui/public/node-tunables.mjs +201 -0
  96. package/ui/public/report-run.mjs +75 -0
  97. package/ui/public/results-view.mjs +25 -0
  98. package/ui/public/source-pane.mjs +16 -2
  99. package/ui/public/stats-view.mjs +2 -2
  100. package/ui/public/style.css +1450 -303
  101. package/ui/public/team-metrics-surfaces.mjs +452 -0
  102. package/ui/public/team-metrics-view.mjs +533 -0
  103. package/ui/public/thinking-orb.mjs +46 -8
  104. package/ui/server.mjs +1282 -193
@@ -0,0 +1,135 @@
1
+ // ui/public/artifact-view.mjs — typed viewers for per-step run artifacts.
2
+ //
3
+ // Markdown reuses ask-markdown.mjs's createMarkdownRenderer verbatim — the SAME
4
+ // sanitizer the Ask panel uses (marked + DOMPurify, one shared allowlist +
5
+ // post-pass), so there is exactly ONE security-sensitive markdown path to audit.
6
+ // `escapeHtml`, `viewerKindFor` and `artifactsByNodeCycle` are pure and
7
+ // node-testable; the renderers touch the DOM and (for markdown) lazily load the
8
+ // vendor bundle through an injected `deps.loadMarkdown`, so a test harness can
9
+ // stub it the way window.__worcaTestHooks?.askMarkdown does.
10
+ import { createMarkdownRenderer } from './ask-markdown.mjs';
11
+
12
+ /** Escape the five HTML metacharacters. Pure. */
13
+ export function escapeHtml(s) {
14
+ return String(s ?? '')
15
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
16
+ .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
17
+ }
18
+
19
+ /**
20
+ * Pick a viewer for an artifact by kind + relPath. Pure.
21
+ * @param {string} kind
22
+ * @param {string} [relPath]
23
+ * @returns {'markdown'|'diff'|'json'|'text'}
24
+ */
25
+ export function viewerKindFor(kind, relPath = '') {
26
+ const ext = (String(relPath).split('.').pop() || '').toLowerCase();
27
+ // An explicit file extension is authoritative; the generic kind (plan/review/
28
+ // result) is only a fallback for extensionless paths, so a 'result' that is a
29
+ // .json renders as JSON rather than being forced through the diff viewer.
30
+ if (ext === 'md') return 'markdown';
31
+ if (ext === 'diff' || ext === 'patch') return 'diff';
32
+ if (ext === 'json') return 'json';
33
+ if (kind === 'plan' || kind === 'review') return 'markdown';
34
+ if (kind === 'result') return 'diff';
35
+ return 'text';
36
+ }
37
+
38
+ /**
39
+ * Group a run's artifacts by nodeId then cycle for the per-node UI. Legacy
40
+ * artifacts (nodeId == null) fall into the '__run__' bucket. Pure.
41
+ * @param {Array<{nodeId?:string|null, cycle?:number|null}>} [artifacts]
42
+ * @returns {Map<string, Map<number, Array>>} nodeId -> cycle -> artifacts[]
43
+ */
44
+ export function artifactsByNodeCycle(artifacts = []) {
45
+ const groups = new Map(); // nodeId -> Map(cycle -> [])
46
+ for (const a of artifacts) {
47
+ const node = a.nodeId ?? '__run__';
48
+ const cyc = a.cycle ?? 0;
49
+ if (!groups.has(node)) groups.set(node, new Map());
50
+ const byCyc = groups.get(node);
51
+ if (!byCyc.has(cyc)) byCyc.set(cyc, []);
52
+ byCyc.get(cyc).push(a);
53
+ }
54
+ return groups;
55
+ }
56
+
57
+ /** Render escaped plaintext into a <pre>. */
58
+ export function renderText(text, mount) {
59
+ const pre = mount.ownerDocument.createElement('pre');
60
+ pre.className = 'artifact-text';
61
+ pre.textContent = String(text ?? '');
62
+ mount.replaceChildren(pre);
63
+ return pre;
64
+ }
65
+
66
+ /** Pretty-print JSON (falls back to plaintext when it does not parse). */
67
+ export function renderJson(text, mount) {
68
+ let pretty = String(text ?? '');
69
+ try { pretty = JSON.stringify(JSON.parse(pretty), null, 2); } catch { /* keep raw text */ }
70
+ const pre = mount.ownerDocument.createElement('pre');
71
+ pre.className = 'artifact-json';
72
+ pre.textContent = pretty;
73
+ mount.replaceChildren(pre);
74
+ return pre;
75
+ }
76
+
77
+ /** Render a unified diff, colouring +/-/@@ lines. */
78
+ export function renderDiff(text, mount) {
79
+ const doc = mount.ownerDocument;
80
+ const pre = doc.createElement('pre');
81
+ pre.className = 'artifact-diff';
82
+ for (const line of String(text ?? '').split('\n')) {
83
+ const row = doc.createElement('span');
84
+ // File headers are the space-terminated '+++ '/'--- ' forms (plus diff/index);
85
+ // a bare '+++'/'---' with no space is real added/removed CONTENT (e.g. deleting
86
+ // '--flag' yields the line '---flag'), so classify meta FIRST, then +/-.
87
+ row.className = 'artifact-diff-line'
88
+ + (line.startsWith('+++ ') || line.startsWith('--- ')
89
+ || line.startsWith('diff ') || line.startsWith('index ') ? ' meta'
90
+ : line.startsWith('+') ? ' add'
91
+ : line.startsWith('-') ? ' del'
92
+ : line.startsWith('@@') ? ' hunk' : '');
93
+ row.textContent = `${line}\n`;
94
+ pre.appendChild(row);
95
+ }
96
+ mount.replaceChildren(pre);
97
+ return pre;
98
+ }
99
+
100
+ /**
101
+ * Render markdown through ask-markdown.mjs's createMarkdownRenderer — the SAME
102
+ * marked + DOMPurify sanitizer, allowlist and post-pass the Ask panel uses, so
103
+ * untrusted artifact content has no separate security path. `deps.loadMarkdown`
104
+ * returns { marked, createDOMPurify } (the seam app.js wires to
105
+ * window.__worcaTestHooks?.askMarkdown). Falls back to escaped plaintext when the
106
+ * bundle is unavailable or parsing/sanitizing fails.
107
+ */
108
+ export async function renderMarkdown(text, mount, deps = {}) {
109
+ const doc = mount.ownerDocument;
110
+ const load = deps.loadMarkdown;
111
+ if (typeof load !== 'function') return renderText(text, mount);
112
+ const renderer = createMarkdownRenderer({ doc, load });
113
+ if (!(await renderer.ensure())) return renderText(text, mount);
114
+ const out = renderer.render(text);
115
+ if (out.kind !== 'md') return renderText(text, mount);
116
+ const box = doc.createElement('div');
117
+ box.className = 'artifact-markdown';
118
+ box.appendChild(out.frag);
119
+ mount.replaceChildren(box);
120
+ return box;
121
+ }
122
+
123
+ /**
124
+ * Render one artifact into `mount`, dispatching on viewerKindFor.
125
+ * @param {{kind:string, relPath:string, text:string}} artifact
126
+ * @param {Element} mount
127
+ * @param {{loadMarkdown?:Function}} [deps]
128
+ */
129
+ export async function renderArtifact({ kind, relPath, text }, mount, deps = {}) {
130
+ const view = viewerKindFor(kind, relPath);
131
+ if (view === 'markdown') return renderMarkdown(text, mount, deps);
132
+ if (view === 'diff') return renderDiff(text, mount);
133
+ if (view === 'json') return renderJson(text, mount);
134
+ return renderText(text, mount);
135
+ }
@@ -23,7 +23,7 @@ export function createThreadModel({ threadId }) {
23
23
  let worktrees = []; // P4 §10: the chat's open worktrees (snapshot + ask-worktrees frames); the panel mirrors it
24
24
 
25
25
  function newDirty() {
26
- // runLinks dirt is produced but not yet consumed — no v1 UI renders run links directly; the follower notices carry the visible state.
26
+ // runLinks dirt: the panel's progress cards re-resolve their run identity on it (ask-panel.mjs flushExtra).
27
27
  return { structure: false, messages: new Set(), blocks: new Map(), answer: new Set(), label: false, meters: false, title: false, runLinks: false, worktrees: false };
28
28
  }
29
29
 
@@ -285,12 +285,29 @@ export function createThreadModel({ threadId }) {
285
285
  inFlight() { return inFlight; },
286
286
  live() { return live; },
287
287
  runLinks() { return links; },
288
+ /** The link for a History pipeline id, with its runId (the Map key) folded in. The server keeps ONE row per
289
+ * (thread, pipeline): POST /api/run inserts once, resumeRun MOVES the row to the new runId, track_run dedupes —
290
+ * so "last hit wins" only ever matters for the live frame path, where the newer frame lands later. */
291
+ runLinkByPipeline(pipelineId) {
292
+ if (!pipelineId) return null;
293
+ let hit = null;
294
+ for (const [runId, l] of links) if (l.pipelineId === pipelineId) hit = { runId, ...l };
295
+ return hit;
296
+ },
297
+ /** The link a run-proposal card launched (ask_run_links.card_id) — stable across a resume. */
298
+ runLinkForCard(cardId) {
299
+ if (!cardId) return null;
300
+ let hit = null;
301
+ for (const [runId, l] of links) if (l.cardId === cardId) hit = { runId, ...l };
302
+ return hit;
303
+ },
288
304
  worktrees() { return worktrees; },
289
305
  setWorktrees(list) { // the panel's heal (refreshWorktrees) feeds the same store the frames do
290
306
  worktrees = Array.isArray(list) ? list.slice() : [];
291
307
  dirty.worktrees = true;
292
308
  },
293
309
  attachmentsBytes() { return attachments.reduce((n, a) => n + (a && Number.isFinite(a.bytes) ? a.bytes : 0), 0); },
310
+ attachments() { return attachments.map((a) => ({ ...a })); }, // the run card's @-popover + pills
294
311
  findCard(cardId) {
295
312
  for (const r of rows) {
296
313
  const b = (r && Array.isArray(r.blocks) ? r.blocks : []).find((x) => x && x.kind === 'card' && x.id === cardId);