@worca/app 1.0.0 → 1.2.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 (143) hide show
  1. package/README.md +30 -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 +386 -56
  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 +199 -23
  32. package/src/core/ask/attachment-kind.mjs +95 -0
  33. package/src/core/ask/catalog.mjs +111 -0
  34. package/src/core/ask/comment-deps.mjs +55 -0
  35. package/src/core/ask/events.mjs +545 -0
  36. package/src/core/ask/follow.mjs +113 -0
  37. package/src/core/ask/git-allowlist.mjs +226 -0
  38. package/src/core/ask/limits.mjs +57 -0
  39. package/src/core/ask/mcp-stdio.mjs +135 -0
  40. package/src/core/ask/models.mjs +125 -0
  41. package/src/core/ask/prompt.mjs +286 -0
  42. package/src/core/ask/proposal.mjs +170 -0
  43. package/src/core/ask/redact.mjs +30 -0
  44. package/src/core/ask/spawn.mjs +156 -0
  45. package/src/core/ask/store.mjs +438 -0
  46. package/src/core/ask/tool-deps.mjs +87 -0
  47. package/src/core/ask/tools.mjs +879 -0
  48. package/src/core/ask/turn.mjs +462 -0
  49. package/src/core/ask/worktree-deps.mjs +27 -0
  50. package/src/core/ask/worktrees.mjs +285 -0
  51. package/src/core/chat/command-router.mjs +28 -7
  52. package/src/core/chat/notifier.mjs +6 -1
  53. package/src/core/chat/renderers.mjs +15 -8
  54. package/src/core/claude-runner.mjs +541 -62
  55. package/src/core/config.mjs +310 -44
  56. package/src/core/cost-budget.mjs +29 -2
  57. package/src/core/db.mjs +773 -53
  58. package/src/core/diff-anchor.mjs +213 -0
  59. package/src/core/diff-comments.mjs +273 -0
  60. package/src/core/engine-select.mjs +32 -0
  61. package/src/core/failure-policy.mjs +201 -0
  62. package/src/core/git-info.mjs +49 -10
  63. package/src/core/graph/builtin-workflows.mjs +51 -0
  64. package/src/core/graph/executor.mjs +894 -0
  65. package/src/core/graph/registry-ports.mjs +12 -0
  66. package/src/core/graph/scheduler.mjs +1072 -0
  67. package/src/core/graph/seed-templates.mjs +318 -0
  68. package/src/core/host-guard.mjs +271 -0
  69. package/src/core/model-env.mjs +180 -8
  70. package/src/core/model-test.mjs +79 -0
  71. package/src/core/orchestrator.mjs +994 -4097
  72. package/src/core/overview-agent.mjs +15 -3
  73. package/src/core/phases.mjs +208 -537
  74. package/src/core/pipeline-delete.mjs +13 -2
  75. package/src/core/plugin-api.mjs +8 -3
  76. package/src/core/plugin-config.mjs +178 -28
  77. package/src/core/plugin-inventory.mjs +6 -2
  78. package/src/core/plugin-manifest.mjs +199 -11
  79. package/src/core/plugin-models.mjs +1 -0
  80. package/src/core/plugin-repo.mjs +16 -4
  81. package/src/core/plugin-shim-child.mjs +9 -3
  82. package/src/core/plugin-shim.mjs +80 -17
  83. package/src/core/plugin-store.mjs +236 -29
  84. package/src/core/plugin-workflows.mjs +90 -41
  85. package/src/core/preflight.mjs +135 -3
  86. package/src/core/projects.mjs +7 -5
  87. package/src/core/protocol.mjs +8 -35
  88. package/src/core/recoverable-error.mjs +1 -1
  89. package/src/core/run-harness.mjs +3934 -0
  90. package/src/core/run-manifest.mjs +5 -1
  91. package/src/core/settings.mjs +184 -13
  92. package/src/core/skills.mjs +10 -3
  93. package/src/core/source-bindings.mjs +175 -0
  94. package/src/core/sources.mjs +87 -25
  95. package/src/core/stats.mjs +25 -6
  96. package/src/core/title.mjs +51 -4
  97. package/src/core/workflows.mjs +358 -259
  98. package/src/core/workspace-scan.mjs +4 -0
  99. package/src/core/worktree.mjs +98 -7
  100. package/src/shared/graph/agent-meta.mjs +278 -0
  101. package/src/shared/graph/constants.mjs +105 -0
  102. package/src/shared/graph/geometry.mjs +157 -0
  103. package/src/shared/graph/layout.mjs +134 -0
  104. package/src/shared/graph/loops.mjs +130 -0
  105. package/src/shared/graph/manifest.mjs +257 -0
  106. package/src/shared/graph/ports.mjs +153 -0
  107. package/src/shared/graph/route.mjs +397 -0
  108. package/src/shared/graph/template.mjs +165 -0
  109. package/src/shared/graph/thumbnail.mjs +67 -0
  110. package/src/shared/graph/validate.mjs +491 -0
  111. package/src/shared/graph/verdict.mjs +41 -0
  112. package/ui/public/app.js +4240 -1682
  113. package/ui/public/ask-markdown.mjs +145 -0
  114. package/ui/public/ask-model.mjs +317 -0
  115. package/ui/public/ask-panel.mjs +2129 -0
  116. package/ui/public/chat-settings-view.mjs +6 -2
  117. package/ui/public/diff-view.mjs +66 -11
  118. package/ui/public/file-tree.mjs +305 -0
  119. package/ui/public/graph/composer.mjs +889 -0
  120. package/ui/public/graph/inspector.mjs +183 -0
  121. package/ui/public/graph/model.mjs +37 -0
  122. package/ui/public/graph/palette.mjs +144 -0
  123. package/ui/public/graph/run-decor.mjs +410 -0
  124. package/ui/public/graph/run-hosts.mjs +201 -0
  125. package/ui/public/graph/save-dialog.mjs +56 -0
  126. package/ui/public/graph/view.mjs +858 -0
  127. package/ui/public/guardrails-view.mjs +4 -2
  128. package/ui/public/hljs-loader.mjs +180 -0
  129. package/ui/public/index.html +311 -265
  130. package/ui/public/log-filter.mjs +22 -4
  131. package/ui/public/log-line.mjs +45 -19
  132. package/ui/public/models-view.mjs +171 -9
  133. package/ui/public/plugins-view.mjs +106 -4
  134. package/ui/public/source-pane.mjs +190 -8
  135. package/ui/public/stats-view.mjs +81 -1
  136. package/ui/public/style.css +1487 -229
  137. package/ui/public/syntax-highlight.mjs +270 -0
  138. package/ui/public/thinking-orb.mjs +110 -0
  139. package/ui/server.mjs +1894 -104
  140. package/src/core/channels.mjs +0 -302
  141. package/src/core/runners.mjs +0 -167
  142. package/src/core/workflow-validator.mjs +0 -185
  143. package/ui/public/composer-core.mjs +0 -211
@@ -0,0 +1,95 @@
1
+ // src/core/ask/attachment-kind.mjs
2
+ // Attachment typing for the Ask Worca chat (issue #398): which extensions are
3
+ // accepted, what kind/mime each maps to, and content sniffing for the binary
4
+ // ones. Pure and synchronous — the single source of truth for the type table;
5
+ // limits.mjs re-exports the two extension lists and ui/server.mjs validates
6
+ // uploads with classifyExtension + sniffMime.
7
+ //
8
+ // Kinds: 'text' (UTF-8, inlineable into the turn prompt, redactable),
9
+ // 'image' (fed to the model via its Read tool on the stored file) and
10
+ // 'binary' (today only PDF — same Read-tool path, never inlined).
11
+ //
12
+ // The extension names the CLAIMED type; for binary kinds the claim is verified
13
+ // against the leading bytes (magic number) so a mislabeled body is refused at
14
+ // upload rather than stored wrong. SVG is deliberately absent: it is scriptable
15
+ // markup, and the download route serves attachment bodies with their real mime.
16
+
17
+ /** Extension -> {kind, mime} for the text kinds (the pre-#398 allowlist). */
18
+ const TEXT_TYPES = Object.freeze({
19
+ '.md': 'text/markdown',
20
+ '.markdown': 'text/markdown',
21
+ '.txt': 'text/plain',
22
+ '.json': 'application/json',
23
+ '.csv': 'text/csv',
24
+ '.log': 'text/plain',
25
+ });
26
+
27
+ /** Extension -> mime for the binary kinds. Every mime here MUST be sniffable. */
28
+ const BINARY_TYPES = Object.freeze({
29
+ '.png': 'image/png',
30
+ '.jpg': 'image/jpeg',
31
+ '.jpeg': 'image/jpeg',
32
+ '.gif': 'image/gif',
33
+ '.webp': 'image/webp',
34
+ '.pdf': 'application/pdf',
35
+ });
36
+
37
+ export const TEXT_EXTENSIONS = Object.freeze(Object.keys(TEXT_TYPES));
38
+ export const BINARY_EXTENSIONS = Object.freeze(Object.keys(BINARY_TYPES));
39
+
40
+ const kindForMime = (mime) => (mime.startsWith('image/') ? 'image' : 'binary');
41
+
42
+ /** ISO 32000-1 §7.5.2 (implementation note 13): the `%PDF-` header may be
43
+ * preceded by up to 1024 bytes of junk (a UTF-8 BOM, print-driver or mail-
44
+ * gateway preamble). Acrobat and pdf.js accept such files, so the sniff does too. */
45
+ const PDF_HEADER_WINDOW = 1024;
46
+
47
+ /**
48
+ * Classify a lower-cased extension (with the leading dot) into {kind, mime},
49
+ * or null when it is not on either allowlist.
50
+ */
51
+ export function classifyExtension(ext) {
52
+ if (typeof ext !== 'string') return null;
53
+ const e = ext.toLowerCase();
54
+ if (Object.prototype.hasOwnProperty.call(TEXT_TYPES, e)) return { kind: 'text', mime: TEXT_TYPES[e] };
55
+ if (Object.prototype.hasOwnProperty.call(BINARY_TYPES, e)) return { kind: kindForMime(BINARY_TYPES[e]), mime: BINARY_TYPES[e] };
56
+ return null;
57
+ }
58
+
59
+ /**
60
+ * Sniff the real mime of a binary body from its magic number, or null when the
61
+ * bytes match none of the accepted binary types. Text kinds are validated by
62
+ * UTF-8 decoding instead (ui/server.mjs), never sniffed here.
63
+ */
64
+ export function sniffMime(buf) {
65
+ if (!Buffer.isBuffer(buf) || buf.length < 3) return null;
66
+ if (buf.length >= 8
67
+ && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47
68
+ && buf[4] === 0x0d && buf[5] === 0x0a && buf[6] === 0x1a && buf[7] === 0x0a) return 'image/png';
69
+ // SOI (FF D8) followed by the first marker's FF and its marker byte (>= 0xC0:
70
+ // APPn/DQT/SOFn/…) — a bare 3-byte FF D8 FF stub is not a JPEG anything can open.
71
+ if (buf.length >= 4 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff && buf[3] >= 0xc0) return 'image/jpeg';
72
+ if (buf.length >= 6) {
73
+ const head6 = buf.toString('latin1', 0, 6);
74
+ if (head6 === 'GIF87a' || head6 === 'GIF89a') return 'image/gif';
75
+ }
76
+ if (buf.length >= 12
77
+ && buf.toString('latin1', 0, 4) === 'RIFF'
78
+ && buf.toString('latin1', 8, 12) === 'WEBP') return 'image/webp';
79
+ if (buf.length >= 5) {
80
+ const at = buf.toString('latin1', 0, Math.min(buf.length, PDF_HEADER_WINDOW + 5)).indexOf('%PDF-');
81
+ if (at !== -1 && at <= PDF_HEADER_WINDOW) return 'application/pdf';
82
+ }
83
+ return null;
84
+ }
85
+
86
+ /** The on-disk extension for a stored body: derived from the SNIFFED mime (or
87
+ * '.txt' for text kinds), never from the user-supplied name — the path stays a
88
+ * function of row data the store minted (store.mjs traversal guard). */
89
+ export function extensionForAttachment(kind, mime) {
90
+ if (kind === 'text' || kind == null) return '.txt';
91
+ for (const [ext, m] of Object.entries(BINARY_TYPES)) {
92
+ if (m === mime) return ext; // first match: '.jpg' wins over '.jpeg' for image/jpeg
93
+ }
94
+ return '.bin';
95
+ }
@@ -0,0 +1,111 @@
1
+ // src/core/ask/catalog.mjs
2
+ // The static catalog the assistant sees (ask-worca-design.md §6.1, D9): projects,
3
+ // workspaces and workflows with their ordered step groups. ONE builder feeds both
4
+ // the system prompt (prompt.mjs renders a subset) and the list_projects /
5
+ // list_workflows tools (tools.mjs returns the objects) — never two readers.
6
+ // Readers are injected so unit tests run without a DB.
7
+ import { listProjects as realListProjects } from '../projects.mjs';
8
+ import { listWorkspaces as realListWorkspaces } from '../workspaces.mjs';
9
+ import { listWorkflows as realListWorkflows, GRAPH_DEFAULT_WORKFLOW } from '../workflows.mjs';
10
+ import { classifyLoops } from '../../shared/graph/loops.mjs';
11
+ import { rankNodes } from '../../shared/graph/layout.mjs';
12
+ import { registryPortsFn } from '../graph/registry-ports.mjs';
13
+
14
+ /** A v2 graph -> the v1-shaped step groups the assistant already understands:
15
+ * one group per rank (loop wires excluded), agent nodes only. */
16
+ function graphSteps(tpl, portsFn) {
17
+ const ranks = rankNodes(tpl, classifyLoops(tpl, portsFn));
18
+ const byRank = new Map();
19
+ for (const node of tpl.nodes) {
20
+ // buildCatalog maps EVERY template, so one malformed node used to take down
21
+ // the whole Ask system prompt and list_workflows, not just its own row.
22
+ if (!node || node.kind !== 'agent') continue;
23
+ const r = ranks[node.id] ?? 0;
24
+ if (!byRank.has(r)) byRank.set(r, []);
25
+ byRank.get(r).push(node);
26
+ }
27
+ return [...byRank.keys()].sort((a, b) => a - b).map((r) => byRank.get(r));
28
+ }
29
+ import { loadAgentRegistry as realLoadAgentRegistry } from '../agent-registry.mjs';
30
+
31
+ /**
32
+ * Pure: a stored workflow template → the catalog shape. `tpl.steps` is already
33
+ * Array<Array<{id,key}>> (outer = ordered step groups, inner = parallel nodes,
34
+ * workflows.mjs:93-111 / :208-220), so this is a straight map against the agent
35
+ * registry for display names; unknown keys fall back to the key itself.
36
+ */
37
+ export function shapeWorkflow(tpl, registry = {}) {
38
+ if (tpl && tpl.version === 2 && Array.isArray(tpl.nodes)) {
39
+ const portsFn = registryPortsFn(registry);
40
+ const groups = graphSteps(tpl, portsFn);
41
+ const { loopWireIds } = classifyLoops(tpl, portsFn);
42
+ return {
43
+ id: tpl.id,
44
+ name: tpl.name,
45
+ domain: typeof tpl.domain === 'string' && tpl.domain ? tpl.domain : 'general',
46
+ origin: tpl.origin ?? null,
47
+ steps: groups.map((group) => group.map((node) => {
48
+ const meta = registry && registry[node.key] ? registry[node.key] : null;
49
+ return {
50
+ nodeId: node.id,
51
+ key: node.key,
52
+ displayName: meta && meta.displayName ? meta.displayName : node.key,
53
+ description: meta && typeof meta.description === 'string' ? meta.description : '',
54
+ };
55
+ })),
56
+ feedbacks: (tpl.wires || []).filter((w) => w && loopWireIds.has(w.id))
57
+ .map((w) => ({ id: w.id, from: w.from.node, to: w.to.node })),
58
+ };
59
+ }
60
+ const steps = Array.isArray(tpl.steps) ? tpl.steps : [];
61
+ const feedbacks = Array.isArray(tpl.feedbacks) ? tpl.feedbacks : [];
62
+ return {
63
+ id: tpl.id,
64
+ name: tpl.name,
65
+ domain: typeof tpl.domain === 'string' && tpl.domain ? tpl.domain : 'general',
66
+ origin: tpl.origin ?? null,
67
+ steps: steps.map((group) => (Array.isArray(group) ? group : []).map((node) => {
68
+ const meta = registry && node && registry[node.key] ? registry[node.key] : null;
69
+ return {
70
+ nodeId: node.id,
71
+ key: node.key,
72
+ displayName: meta && typeof meta.displayName === 'string' && meta.displayName ? meta.displayName : node.key,
73
+ description: meta && typeof meta.description === 'string' ? meta.description : '',
74
+ };
75
+ })),
76
+ feedbacks: feedbacks.map((f) => ({ id: f.id, from: f.from, to: f.to })),
77
+ };
78
+ }
79
+
80
+ /**
81
+ * @param {{listProjects?:Function, listWorkspaces?:Function, listWorkflows?:Function, defaultWorkflow?:object, loadAgentRegistry?:Function}} [deps]
82
+ */
83
+ export function createCatalog({
84
+ listProjects = realListProjects,
85
+ listWorkspaces = realListWorkspaces,
86
+ listWorkflows = realListWorkflows,
87
+ defaultWorkflow = GRAPH_DEFAULT_WORKFLOW,
88
+ loadAgentRegistry = realLoadAgentRegistry,
89
+ } = {}) {
90
+ async function buildCatalog() {
91
+ const [projects, workspaces, workflows] = await Promise.all([listProjects(), listWorkspaces(), listWorkflows()]);
92
+ let registry = {};
93
+ try { registry = loadAgentRegistry() || {}; } catch { registry = {}; }
94
+ // Same order as GET /api/workflows: the graph default, then saved rows.
95
+ // shapeWorkflow already derives `steps` (condensation-topo ranks) +
96
+ // `feedbacks` (loop wires) for a v2 template, so the LLM-facing shape is unchanged.
97
+ const templates = [
98
+ defaultWorkflow,
99
+ ...workflows.filter((t) => t && t.id !== defaultWorkflow.id),
100
+ ];
101
+ return {
102
+ projects: projects.map((p) => ({ key: p.key, name: p.name, path: p.path })),
103
+ workspaces: workspaces.map((w) => ({ id: w.id, name: w.name, projectKeys: [...(w.projectKeys || [])] })),
104
+ workflows: templates.map((t) => shapeWorkflow(t, registry)),
105
+ };
106
+ }
107
+ return { buildCatalog };
108
+ }
109
+
110
+ /** Bound to the real readers — what the server and the MCP child use. */
111
+ export const buildCatalog = createCatalog().buildCatalog;
@@ -0,0 +1,55 @@
1
+ // src/core/ask/comment-deps.mjs
2
+ // The WRITE-CAPABLE dep bundle of the diff-comment tools. Deliberately separate
3
+ // from tool-deps.mjs, whose source is scanned as read-only
4
+ // (test/ask-tools.test.mjs): every row that is created, resolved or deleted is
5
+ // reachable ONLY through here, and test/ask-diff-comment-tools.test.mjs pins this
6
+ // module's import surface. Same shape as worktree-deps.mjs — one namespaced
7
+ // sub-object.
8
+ //
9
+ // Everything writes through src/core/diff-comments.mjs — the one mutation module —
10
+ // so the MCP path and the REST path share anchor validation, the body cap and the
11
+ // change notification. Reads add the surrounding hunk lines here (tools.mjs has no
12
+ // imports at all); tools.mjs owns the protected-path filter and the redaction, so
13
+ // the fail-closed read rules live next to get_run_diff's.
14
+ import {
15
+ addDiffComment, listDiffComments, getDiffComment, setDiffCommentResolved, deleteDiffComment,
16
+ } from '../diff-comments.mjs';
17
+ import { hunkContext } from '../diff-anchor.mjs';
18
+
19
+ /** Rows either side of an anchor served to the model. */
20
+ export const COMMENT_CONTEXT_RADIUS = 3;
21
+
22
+ // COST NOTE, deliberate: hunkContext() re-runs splitPatchSections + patchIndex +
23
+ // parseFileSection over the WHOLE patch once per comment, so listing N comments
24
+ // on a run costs N full parses. That is the price of using the ONE parser (D3)
25
+ // instead of a second, faster, divergent one. It is bounded in practice — this
26
+ // runs in the MCP child, once per tool call, on runs with a handful of comments
27
+ // — and the REST list path does not do it at all. If a real run ever makes this
28
+ // hurt, memoize the parsed index per (patchText) inside this closure; do NOT
29
+ // hand-roll a cheaper scan.
30
+
31
+ /** No thread scoping: comments belong to RUNS, not to chat threads. */
32
+ export function defaultCommentDeps() {
33
+ return {
34
+ comments: {
35
+ /** Comments of a run, each with `context` when the patch is readable.
36
+ * `keep` is the CALLER's protected-path filter (tools.mjs owns the guard
37
+ * rules); applied BEFORE the context parse so a row that will be dropped
38
+ * never costs a whole-patch parse, and so the order is fail-closed-first
39
+ * like everywhere else. */
40
+ list: (storeKey, pipelineId, { status = 'all', path = null, patchText = null, keep = null } = {}) => {
41
+ const rows = listDiffComments(storeKey, pipelineId, { status, path });
42
+ const kept = typeof keep === 'function' ? rows.filter(keep) : rows;
43
+ return kept.map((c) => (patchText == null ? c : {
44
+ ...c,
45
+ context: hunkContext(patchText, { project: c.projectKey, path: c.path, side: c.side, line: c.line },
46
+ COMMENT_CONTEXT_RADIUS),
47
+ }));
48
+ },
49
+ add: (input) => addDiffComment({ ...input, author: 'ask' }),
50
+ get: (id) => getDiffComment(id),
51
+ setResolved: (id, resolved) => setDiffCommentResolved(id, resolved),
52
+ remove: (id) => deleteDiffComment(id),
53
+ },
54
+ };
55
+ }