@worca/app 0.0.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 (114) hide show
  1. package/README.md +403 -0
  2. package/agents/clarify.meta.json +19 -0
  3. package/agents/decomposer.meta.json +21 -0
  4. package/agents/implementer.meta.json +20 -0
  5. package/agents/manualTestsChecklist.meta.json +18 -0
  6. package/agents/manualWebUiTesting.meta.json +18 -0
  7. package/agents/planReviewer.meta.json +19 -0
  8. package/agents/planner.meta.json +20 -0
  9. package/agents/refiner.meta.json +19 -0
  10. package/agents/reviewer.meta.json +19 -0
  11. package/agents/worca-cc-clarify.md +67 -0
  12. package/agents/worca-cc-code-reviewer.md +66 -0
  13. package/agents/worca-cc-decomposer.md +84 -0
  14. package/agents/worca-cc-implementer.md +69 -0
  15. package/agents/worca-cc-manual-tests-checklist.md +63 -0
  16. package/agents/worca-cc-manual-web-ui-testing.md +64 -0
  17. package/agents/worca-cc-plan-refiner.md +69 -0
  18. package/agents/worca-cc-plan-reviewer.md +70 -0
  19. package/agents/worca-cc-planner.md +70 -0
  20. package/agents/worca-cc-workspace-reviewer.md +56 -0
  21. package/agents/worca-cc-workspace-scanner.md +55 -0
  22. package/agents/workspaceReviewer.meta.json +20 -0
  23. package/agents/workspaceScanner.meta.json +18 -0
  24. package/package.json +61 -0
  25. package/scripts/install.mjs +209 -0
  26. package/skills/worca/SKILL.md +66 -0
  27. package/src/cli/worca-cc.mjs +1520 -0
  28. package/src/core/agent-gen.mjs +206 -0
  29. package/src/core/agent-registry.mjs +417 -0
  30. package/src/core/agent-store.mjs +143 -0
  31. package/src/core/artifacts.mjs +2019 -0
  32. package/src/core/channels.mjs +302 -0
  33. package/src/core/chat/allowlist.mjs +27 -0
  34. package/src/core/chat/channel-host.mjs +562 -0
  35. package/src/core/chat/channel-protocol.mjs +117 -0
  36. package/src/core/chat/channel-worker-child.mjs +211 -0
  37. package/src/core/chat/chat-context.mjs +66 -0
  38. package/src/core/chat/command-router.mjs +343 -0
  39. package/src/core/chat/notifier.mjs +120 -0
  40. package/src/core/chat/parser.mjs +30 -0
  41. package/src/core/chat/rate-limiter.mjs +133 -0
  42. package/src/core/chat/redact.mjs +27 -0
  43. package/src/core/chat/renderers.mjs +136 -0
  44. package/src/core/claude-runner.mjs +1356 -0
  45. package/src/core/config.mjs +882 -0
  46. package/src/core/cost-budget.mjs +103 -0
  47. package/src/core/db.mjs +864 -0
  48. package/src/core/fanout.mjs +48 -0
  49. package/src/core/folder-dialog.mjs +138 -0
  50. package/src/core/fs-browse.mjs +49 -0
  51. package/src/core/git-info.mjs +200 -0
  52. package/src/core/guardrail-store.mjs +204 -0
  53. package/src/core/guardrails.mjs +302 -0
  54. package/src/core/marketplaces.mjs +267 -0
  55. package/src/core/migrate-fs-to-db.mjs +612 -0
  56. package/src/core/model-env.mjs +74 -0
  57. package/src/core/orchestrator.mjs +4279 -0
  58. package/src/core/overview-agent.mjs +124 -0
  59. package/src/core/phases.mjs +1279 -0
  60. package/src/core/pipeline-delete.mjs +428 -0
  61. package/src/core/plugin-api.mjs +13 -0
  62. package/src/core/plugin-config.mjs +100 -0
  63. package/src/core/plugin-inventory.mjs +50 -0
  64. package/src/core/plugin-manifest.mjs +447 -0
  65. package/src/core/plugin-models.mjs +130 -0
  66. package/src/core/plugin-repo.mjs +303 -0
  67. package/src/core/plugin-shim-child.mjs +76 -0
  68. package/src/core/plugin-shim.mjs +197 -0
  69. package/src/core/plugin-store.mjs +485 -0
  70. package/src/core/plugin-workflows.mjs +179 -0
  71. package/src/core/plugins-lock.mjs +49 -0
  72. package/src/core/preflight-node.mjs +122 -0
  73. package/src/core/preflight.mjs +341 -0
  74. package/src/core/projects.mjs +157 -0
  75. package/src/core/protocol.mjs +257 -0
  76. package/src/core/recoverable-error.mjs +51 -0
  77. package/src/core/results.mjs +188 -0
  78. package/src/core/run-context.mjs +1375 -0
  79. package/src/core/run-log.mjs +64 -0
  80. package/src/core/run-manifest.mjs +317 -0
  81. package/src/core/runners.mjs +167 -0
  82. package/src/core/settings.mjs +682 -0
  83. package/src/core/skills.mjs +210 -0
  84. package/src/core/sources.mjs +232 -0
  85. package/src/core/stats.mjs +182 -0
  86. package/src/core/store.mjs +67 -0
  87. package/src/core/title.mjs +64 -0
  88. package/src/core/workflow-validator.mjs +185 -0
  89. package/src/core/workflows.mjs +568 -0
  90. package/src/core/workspace-scan.mjs +420 -0
  91. package/src/core/workspaces.mjs +353 -0
  92. package/src/core/worktree.mjs +708 -0
  93. package/src/feature.mjs +9 -0
  94. package/ui/public/app.js +10647 -0
  95. package/ui/public/assets/worca-favicon.png +0 -0
  96. package/ui/public/assets/worca-logo.png +0 -0
  97. package/ui/public/chat-settings-view.mjs +89 -0
  98. package/ui/public/composer-core.mjs +211 -0
  99. package/ui/public/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  100. package/ui/public/fonts/poppins-latin-400-normal.woff2 +0 -0
  101. package/ui/public/fonts/poppins-latin-500-normal.woff2 +0 -0
  102. package/ui/public/fonts/poppins-latin-600-normal.woff2 +0 -0
  103. package/ui/public/fonts/poppins-latin-700-normal.woff2 +0 -0
  104. package/ui/public/guardrails-view.mjs +244 -0
  105. package/ui/public/index.html +1145 -0
  106. package/ui/public/log-filter.mjs +81 -0
  107. package/ui/public/log-line.mjs +86 -0
  108. package/ui/public/models-view.mjs +433 -0
  109. package/ui/public/plugins-view.mjs +430 -0
  110. package/ui/public/results-view.mjs +121 -0
  111. package/ui/public/source-pane.mjs +156 -0
  112. package/ui/public/stats-view.mjs +523 -0
  113. package/ui/public/style.css +1557 -0
  114. package/ui/server.mjs +3573 -0
@@ -0,0 +1,67 @@
1
+ // src/core/store.mjs
2
+ // Durable project identity + external history store paths.
3
+ // key = <repo-basename-slug>-<sha1(canonicalRoot)[:8]>. Canonical root is the
4
+ // parent of the shared .git (via `git rev-parse --git-common-dir`), so every
5
+ // worktree of a repo maps to the SAME key. All resolution is sync + fail-safe:
6
+ // a non-repo / missing git degrades to the realpath of the dir, never throwing.
7
+
8
+ import { execFileSync } from 'node:child_process';
9
+ import { realpathSync } from 'node:fs';
10
+ import { createHash } from 'node:crypto';
11
+ import { join, resolve, dirname, basename, isAbsolute } from 'node:path';
12
+ import { worcaHome } from './projects.mjs';
13
+
14
+ const _keyCache = new Map();
15
+
16
+ /** Absolute path to the canonical main-repo root for `projectDir`. */
17
+ export function canonicalProjectRoot(projectDir) {
18
+ const dir = resolve(projectDir);
19
+ try {
20
+ const common = execFileSync('git', ['rev-parse', '--git-common-dir'], {
21
+ cwd: dir,
22
+ stdio: ['ignore', 'pipe', 'ignore'],
23
+ }).toString().trim();
24
+ if (common) {
25
+ const commonAbs = isAbsolute(common) ? common : resolve(dir, common);
26
+ const root = dirname(commonAbs); // parent of the .git dir
27
+ try { return realpathSync(root); } catch { return resolve(root); }
28
+ }
29
+ } catch {
30
+ /* not a git repo, or git unavailable — fall through */
31
+ }
32
+ try { return realpathSync(dir); } catch { return dir; }
33
+ }
34
+
35
+ /** Stable key for a project. Memoized by resolved input path. */
36
+ export function projectKey(projectDir) {
37
+ const cacheKey = resolve(projectDir);
38
+ const hit = _keyCache.get(cacheKey);
39
+ if (hit) return hit;
40
+ const root = canonicalProjectRoot(projectDir);
41
+ const slug =
42
+ basename(root).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'project';
43
+ const hash = createHash('sha1').update(root).digest('hex').slice(0, 8);
44
+ const key = `${slug}-${hash}`;
45
+ _keyCache.set(cacheKey, key);
46
+ return key;
47
+ }
48
+
49
+ /** Root of the external history store: <worcaHome>/store. */
50
+ export function storeRoot() {
51
+ return join(worcaHome(), 'store');
52
+ }
53
+
54
+ /** Per-project store directory: <worcaHome>/store/<key>. */
55
+ export function projectStorePath(key) {
56
+ return join(storeRoot(), key);
57
+ }
58
+
59
+ /** Root of the workspace store namespace: <worcaHome>/store/workspaces. */
60
+ export function workspacesStoreRoot() {
61
+ return join(storeRoot(), 'workspaces');
62
+ }
63
+
64
+ /** Per-workspace store directory: <worcaHome>/store/workspaces/<workspaceKey>. */
65
+ export function workspaceStorePath(workspaceKey) {
66
+ return join(workspacesStoreRoot(), workspaceKey);
67
+ }
@@ -0,0 +1,64 @@
1
+ // src/core/title.mjs
2
+ import { runClaude } from './claude-runner.mjs';
3
+ import { resolveModelEnv } from './config.mjs';
4
+
5
+ // A fast, cheap model is enough for a one-line summary. Overridable for tests/cost tuning.
6
+ const DEFAULT_TITLE_MODEL =
7
+ process.env.WORCA_TITLE_MODEL || 'claude-haiku-4-5-20251001';
8
+ const MAX_LEN = 70;
9
+
10
+ const SYSTEM = [
11
+ 'You write a SHORT, human-readable title for a software task.',
12
+ 'Rules: 3–8 words, Title Case-ish, no trailing period, no quotes, no markdown,',
13
+ 'no preamble. Output ONLY the title on a single line.',
14
+ ].join(' ');
15
+
16
+ /** Normalize raw model output into a safe single-line title (pure, exported for tests). */
17
+ export function sanitizeTitle(raw) {
18
+ if (!raw || typeof raw !== 'string') return '';
19
+ let t = raw.replace(/```[\s\S]*?```/g, (m) => m.replace(/```/g, '')); // drop fence markers
20
+ t = t.split(/\r?\n/).map((l) => l.trim()).find((l) => l) || ''; // first non-empty line
21
+ t = t.replace(/^(?:title|task)\s*[:\-]\s*/i, ''); // strip "Title:"/"Task:" label
22
+ t = t.replace(/^["'“”`]+|["'“”`]+$/g, '').trim(); // strip wrapping quotes/backticks
23
+ t = t.replace(/\s+/g, ' ').replace(/\.+$/, '').trim(); // collapse ws, drop trailing dots
24
+ return t.slice(0, MAX_LEN).trim();
25
+ }
26
+
27
+ /**
28
+ * Produce a concise LLM title for a prompt. Never throws — returns '' on any
29
+ * failure/abort/empty input so the caller keeps the provisional title.
30
+ * @param {string} prompt
31
+ * @param {{cwd:string, signal?:AbortSignal, model?:string, bin?:string, envScrub?:boolean, envAllowlist?:string[]}} opts
32
+ * @returns {Promise<string>}
33
+ */
34
+ export async function generateTitle(prompt, opts = {}) {
35
+ const text = String(prompt || '').trim();
36
+ if (!text) return '';
37
+ try {
38
+ const { text: out } = await runClaude({
39
+ cwd: opts.cwd || process.cwd(),
40
+ systemPrompt: SYSTEM,
41
+ prompt: `Write the title for this task:\n\n${text.slice(0, 4000)}`,
42
+ model: opts.model || DEFAULT_TITLE_MODEL,
43
+ // Aux calls keep their model choice but still route through the catalog's
44
+ // env (design §4.8) — a global entry matching this id carries its routing
45
+ // env everywhere the id is used.
46
+ modelEnv: resolveModelEnv(opts.model || DEFAULT_TITLE_MODEL),
47
+ effort: 'low',
48
+ permissionMode: 'acceptEdits',
49
+ allowedTools: [], // empty → no --allowedTools flag → claude defaults; pure text gen
50
+ signal: opts.signal,
51
+ // Title generation runs DURING a pipeline run, so it must honor the same
52
+ // guardrails as the run itself. All three are undefined when absent, and
53
+ // runClaude treats undefined as "not passed" — existing callers are unchanged.
54
+ bin: opts.bin,
55
+ envScrub: opts.envScrub,
56
+ envAllowlist: opts.envAllowlist,
57
+ onEvent: () => {},
58
+ });
59
+ return sanitizeTitle(out);
60
+ } catch (err) {
61
+ if (err && err.name === 'AbortError') return ''; // run was stopped — caller keeps provisional
62
+ return '';
63
+ }
64
+ }
@@ -0,0 +1,185 @@
1
+ // src/core/workflow-validator.mjs
2
+ // Pure validator (imports only the channel constants from channels.mjs) for a
3
+ // WorkflowTemplate against an agent registry. Collects ALL violations (does not
4
+ // short-circuit) so the UI/API can show every problem at once. Returns
5
+ // { ok, errors:string[], warnings:string[] } (warnings are soft: reachability/
6
+ // governance/multi-producer hints that never set ok=false, so saved
7
+ // topology-only pipelines stay valid).
8
+ //
9
+ // Rules (CONTRACT §workflow-validator):
10
+ // 1. template is a non-null object with a non-empty steps array;
11
+ // 2. no empty steps (every step has >= 1 node);
12
+ // 3. every node has a non-blank string id, and ids are unique workflow-wide;
13
+ // 4. every node.key exists in the registry;
14
+ // 5. feedback from/to reference existing node ids;
15
+ // 6. a feedback's target step index < its source step index
16
+ // (a same-node self-loop, from===to, is allowed; the forward graph is
17
+ // otherwise acyclic so only back-edges are legal feedbacks);
18
+ // 7. feedback ids are unique.
19
+
20
+ import { CHANNEL_IDS, PRESEEDED_CHANNELS } from './channels.mjs';
21
+
22
+ /**
23
+ * @param {object} tpl WorkflowTemplate { steps:[[{id,key}]], feedbacks:[{id,from,to}] }
24
+ * @param {Record<string,{key:string}>} registry loadAgentRegistry() output
25
+ * @returns {{ok:boolean, errors:string[], warnings:string[]}}
26
+ */
27
+ export function validateWorkflow(tpl, registry) {
28
+ const errors = [];
29
+ const reg = registry && typeof registry === 'object' ? registry : {};
30
+
31
+ if (!tpl || typeof tpl !== 'object' || !Array.isArray(tpl.steps)) {
32
+ return { ok: false, errors: ['workflow must be an object with a steps array'], warnings: [] };
33
+ }
34
+ if (tpl.steps.length === 0) {
35
+ errors.push('workflow must have at least one step');
36
+ }
37
+
38
+ // Pass 1: nodes — shape, unique ids, known keys. Build id -> stepIndex map.
39
+ const stepOfNode = new Map(); // nodeId -> step index
40
+ const seenIds = new Set();
41
+ for (let i = 0; i < tpl.steps.length; i++) {
42
+ const group = tpl.steps[i];
43
+ if (!Array.isArray(group) || group.length === 0) {
44
+ errors.push(`step ${i} is empty (a step must contain at least one node)`);
45
+ continue;
46
+ }
47
+ for (const node of group) {
48
+ if (!node || typeof node !== 'object') {
49
+ errors.push(`step ${i} contains a non-object node`);
50
+ continue;
51
+ }
52
+ const id = typeof node.id === 'string' ? node.id.trim() : '';
53
+ if (!id) {
54
+ errors.push(`step ${i} has a node with a missing or blank id`);
55
+ continue;
56
+ }
57
+ if (seenIds.has(id)) {
58
+ errors.push(`duplicate node id "${id}"`);
59
+ } else {
60
+ seenIds.add(id);
61
+ stepOfNode.set(id, i);
62
+ }
63
+ const key = typeof node.key === 'string' ? node.key.trim() : '';
64
+ if (!key) {
65
+ errors.push(`node "${id}" has a missing or blank key`);
66
+ } else if (!Object.prototype.hasOwnProperty.call(reg, key)) {
67
+ errors.push(`node "${id}" has key "${key}" which is not in the agent registry`);
68
+ }
69
+ }
70
+ }
71
+
72
+ // Pass 2: feedbacks — unique ids, endpoints exist, target precedes source.
73
+ const feedbacks = Array.isArray(tpl.feedbacks) ? tpl.feedbacks : [];
74
+ const seenFb = new Set();
75
+ for (const fb of feedbacks) {
76
+ if (!fb || typeof fb !== 'object') {
77
+ errors.push('feedbacks contains a non-object entry');
78
+ continue;
79
+ }
80
+ const fid = typeof fb.id === 'string' ? fb.id.trim() : '';
81
+ if (!fid) {
82
+ errors.push('a feedback has a missing or blank id');
83
+ } else if (seenFb.has(fid)) {
84
+ errors.push(`duplicate feedback id "${fid}"`);
85
+ } else {
86
+ seenFb.add(fid);
87
+ }
88
+ const from = typeof fb.from === 'string' ? fb.from.trim() : '';
89
+ const to = typeof fb.to === 'string' ? fb.to.trim() : '';
90
+ const hasFrom = stepOfNode.has(from);
91
+ const hasTo = stepOfNode.has(to);
92
+ if (!hasFrom) errors.push(`feedback "${fid || '?'}" from "${from}" does not exist`);
93
+ if (!hasTo) errors.push(`feedback "${fid || '?'}" to "${to}" does not exist`);
94
+ if (hasFrom && hasTo) {
95
+ const sFrom = stepOfNode.get(from);
96
+ const sTo = stepOfNode.get(to);
97
+ // A same-node self-loop (from === to) is legal (the refine loop). Otherwise
98
+ // the target step must strictly precede the source step (a back-edge).
99
+ if (from !== to && sTo >= sFrom) {
100
+ errors.push(
101
+ `feedback "${fid || '?'}" target step (${sTo}) must precede its source step (${sFrom})`,
102
+ );
103
+ }
104
+ }
105
+ }
106
+
107
+ // Pass 3: WARNINGS (never block saves, so existing topology-only pipelines stay
108
+ // valid). Forward order = step order.
109
+ const warnings = [];
110
+ // Pre-seeded channels (derived from channels.mjs — the dispatcher's bus literal)
111
+ // are always reachable and never warn.
112
+ const PRESEEDED = new Set(PRESEEDED_CHANNELS);
113
+ const NON_MULTIPLEXABLE = new Set(['code', 'plan']); // one producer per step
114
+ const produced = new Set();
115
+ for (let i = 0; i < tpl.steps.length; i++) {
116
+ const group = Array.isArray(tpl.steps[i]) ? tpl.steps[i] : [];
117
+ // (a) reachability: a required, non-pre-seeded channel must be produced earlier
118
+ for (const node of group) {
119
+ const meta = reg[node?.key] || {};
120
+ const optional = new Set(meta.optionalConsumes || []);
121
+ for (const c of meta.consumes || []) {
122
+ if (optional.has(c) || PRESEEDED.has(c) || produced.has(c)) continue;
123
+ warnings.push(`node "${node.id}" consumes "${c}" but no upstream step produces it`);
124
+ }
125
+ }
126
+ // (b) multi-producer (D2) + stale-sibling (D3): scan within the step
127
+ const stepProducers = new Map(); // channel -> count
128
+ for (const node of group) for (const c of (reg[node?.key]?.produces || [])) {
129
+ stepProducers.set(c, (stepProducers.get(c) || 0) + 1);
130
+ }
131
+ for (const [c, n] of stepProducers) {
132
+ if (n > 1 && NON_MULTIPLEXABLE.has(c)) {
133
+ warnings.push(`step ${i} has ${n} producers of "${c}" (only one producer per step is well-defined)`);
134
+ }
135
+ }
136
+ for (const node of group) {
137
+ const meta = reg[node?.key] || {};
138
+ const optional = new Set(meta.optionalConsumes || []);
139
+ for (const c of meta.consumes || []) {
140
+ // a channel produced ONLY by a same-step sibling is read stale (pre-step snapshot)
141
+ if (!optional.has(c) && !produced.has(c) && !PRESEEDED.has(c) && stepProducers.has(c)) {
142
+ warnings.push(`node "${node.id}" consumes "${c}" produced only by a same-step sibling; it reads the pre-step value`);
143
+ }
144
+ }
145
+ }
146
+ // commit this step's production AFTER the step (matches the frozen-snapshot model)
147
+ for (const [c] of stepProducers) produced.add(c);
148
+ }
149
+ // (d) custom-channel hygiene: a PRODUCED channel that is neither built-in nor
150
+ // declared by any registry channelDef silently defaults to a generic markdown
151
+ // artifact — surface that once per channel so a wizard/meta typo is visible.
152
+ const builtinChannels = new Set(CHANNEL_IDS);
153
+ const definedCustom = new Set();
154
+ for (const m of Object.values(reg)) {
155
+ for (const d of m?.channelDefs || []) if (d?.id) definedCustom.add(d.id);
156
+ }
157
+ const flaggedDefless = new Set();
158
+ for (const group of tpl.steps) {
159
+ for (const node of Array.isArray(group) ? group : []) {
160
+ for (const c of reg[node?.key]?.produces || []) {
161
+ if (builtinChannels.has(c) || definedCustom.has(c) || flaggedDefless.has(c)) continue;
162
+ flaggedDefless.add(c);
163
+ warnings.push(`custom channel "${c}" (produced by "${node.id}") has no channelDef; it defaults to markdown "${c}.md"`);
164
+ }
165
+ }
166
+ }
167
+ // (c) governance: every adjacent forward edge + every feedback edge must be allowed
168
+ const keyOf = new Map();
169
+ tpl.steps.forEach((g) => (Array.isArray(g) ? g : []).forEach((n) => keyOf.set(n.id, n.key)));
170
+ const allows = (fromKey, toKey) => {
171
+ const ct = reg[fromKey]?.connectsTo;
172
+ return ct === '*' || ct === undefined || !Array.isArray(ct) || ct.includes(toKey);
173
+ };
174
+ for (let i = 0; i < tpl.steps.length - 1; i++) {
175
+ for (const a of tpl.steps[i] || []) for (const b of tpl.steps[i + 1] || []) {
176
+ if (!allows(a.key, b.key)) warnings.push(`"${a.key}" is not allowed to connect to "${b.key}" (connectsTo)`);
177
+ }
178
+ }
179
+ for (const fb of Array.isArray(tpl.feedbacks) ? tpl.feedbacks : []) {
180
+ const fk = keyOf.get(fb?.from), tk = keyOf.get(fb?.to);
181
+ if (fk && tk && !allows(fk, tk)) warnings.push(`feedback "${fb.id || '?'}": "${fk}" is not allowed to connect to "${tk}" (connectsTo)`);
182
+ }
183
+
184
+ return { ok: errors.length === 0, errors, warnings };
185
+ }