@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
@@ -10,7 +10,7 @@
10
10
  import { execFile } from 'node:child_process';
11
11
  import { promisify } from 'node:util';
12
12
  import { createHash } from 'node:crypto';
13
- import { mkdirSync, existsSync, rmSync } from 'node:fs';
13
+ import { mkdirSync, existsSync, rmSync, unlinkSync } from 'node:fs';
14
14
  import { mkdtemp, rm } from 'node:fs/promises';
15
15
  import { tmpdir } from 'node:os';
16
16
  import { join, dirname } from 'node:path';
@@ -160,6 +160,9 @@ export async function addPluginRepo(repoUrl, { exec = defaultExec } = {}) {
160
160
  }
161
161
  const res = normalizeManifest(raw, { dir: subdir || '.' });
162
162
  if (!res.ok) { warnings.push(...res.errors); continue; }
163
+ // Degraded-but-usable manifests (e.g. a reserved model-env key stripped)
164
+ // stay discoverable; the notice still reaches the marketplace panel.
165
+ warnings.push(...res.warnings);
163
166
  if (seenNames.has(res.manifest.name)) {
164
167
  warnings.push(`${p}: duplicate plugin name "${res.manifest.name}" — first entry wins, skipped`);
165
168
  continue;
@@ -285,9 +288,15 @@ export async function exportVersion(name, sha, { exec = defaultExec, repoUrl, su
285
288
  const scratch = await mkdtemp(join(tmpdir(), 'worca-cc-export-'));
286
289
  const tarFile = join(scratch, 'export.tar');
287
290
  try {
288
- await gitDir(cache, ['archive', '--format=tar', '-o', tarFile, ...(sub ? [sha, '--', sub] : [sha])], exec);
291
+ await gitDir(cache, ['-c', 'core.autocrlf=false', 'archive', '--format=tar', '-o', tarFile, ...(sub ? [sha, '--', sub] : [sha])], exec);
289
292
  const strip = sub ? ['--strip-components', String(sub.split('/').length)] : [];
290
- await exec('tar', ['-xf', tarFile, '-C', versionDir, ...strip]);
293
+ // Windows GNU tar (shipped by Git for Windows) mishandles native paths two
294
+ // ways: a colon in the `-f` value is read as a `host:path` remote spec ("cannot
295
+ // connect to C:"), and backslash separators are not understood at all. So pass
296
+ // the archive as a RELATIVE name resolved against cwd, and give `-C` a
297
+ // forward-slashed path. On POSIX the replace is a no-op.
298
+ await exec('tar', ['-xf', 'export.tar', '-C', versionDir.replace(/\\/g, '/'), ...strip],
299
+ { cwd: scratch });
291
300
  } catch (err) {
292
301
  rmSync(versionDir, { recursive: true, force: true });
293
302
  throw err;
@@ -296,7 +305,10 @@ export async function exportVersion(name, sha, { exec = defaultExec, repoUrl, su
296
305
  }
297
306
  const warnings = [];
298
307
  for (const rel of findEscapingSymlinks(versionDir)) {
299
- rmSync(join(versionDir, rel), { force: true });
308
+ // unlink, not rmSync: rmSync stats THROUGH the link, so a link to a
309
+ // directory throws ERR_FS_EISDIR and a dangling one (the common escaping
310
+ // case) reads as "already gone" and survives force:true (Node 23).
311
+ unlinkSync(join(versionDir, rel));
300
312
  warnings.push(`removed symlink escaping the export dir: ${rel}`);
301
313
  }
302
314
  return { versionDir, warnings };
@@ -22,6 +22,10 @@ async function main() {
22
22
  const stateDelta = {};
23
23
  const ctx = {
24
24
  apiVersion: msg.apiVersion ?? 1,
25
+ // Which profile this op runs against. config/state are ALREADY that
26
+ // profile's — the id is here for connectors that keep their own storage and
27
+ // must key it the same way (e.g. a per-profile CLI config dir).
28
+ profile: typeof msg.profile === 'string' && msg.profile ? msg.profile : 'default',
25
29
  config: msg.config && typeof msg.config === 'object' ? msg.config : {},
26
30
  state: {
27
31
  get: async (k) => (k in stateDelta ? stateDelta[k] : (snapshot[k] ?? null)),
@@ -37,9 +41,11 @@ async function main() {
37
41
  const source = mod.default(ctx);
38
42
  const fn = source?.[msg.op];
39
43
  if (typeof fn !== 'function') {
40
- // Optional ops (e.g. capabilities) land here; the host treats this kind +
41
- // message as "op not implemented" (Task 13 defaults writeBack:true on it).
42
- throw Object.assign(new Error(`connector does not implement op "${msg.op}"`), { kind: 'plugin' });
44
+ // Optional ops (e.g. capabilities) land here. The kind is 'unimplemented',
45
+ // NOT 'plugin': an op the connector lacks and an implemented op that
46
+ // CRASHED must stay distinguishable the capabilities probe defaults
47
+ // writeBack:true only for the former (Task 13 / sources.mjs).
48
+ throw Object.assign(new Error(`connector does not implement op "${msg.op}"`), { kind: 'unimplemented' });
43
49
  }
44
50
 
45
51
  // §7.1 signatures: getTask(id) and reportResult(id, r) are positional; every
@@ -19,16 +19,19 @@ import { fileURLToPath } from 'node:url';
19
19
  import { WORCA_PLUGIN_API } from './plugin-api.mjs';
20
20
  import { normalizeManifest, negotiatedApi } from './plugin-manifest.mjs';
21
21
  import { readPluginsLock, pluginCurrentDir } from './plugins-lock.mjs';
22
- import { readPluginConfig, readPluginState, writePluginState } from './plugin-config.mjs';
22
+ import { readPluginConfig, readPluginState, writePluginState, listProfileIds, DEFAULT_PROFILE } from './plugin-config.mjs';
23
23
 
24
24
  const CHILD_PATH = fileURLToPath(new URL('./plugin-shim-child.mjs', import.meta.url));
25
25
 
26
- /** Error kinds an op can surface (spec §11); anything else normalizes to 'plugin'. */
27
- const KINDS = new Set(['auth', 'rate-limit', 'network', 'plugin', 'timeout', 'protocol']);
26
+ /** Error kinds an op can surface (spec §11) plus the host-synthesized
27
+ * 'unimplemented' (the connector lacks the op distinct from an implemented
28
+ * op that CRASHED, because callers like the capabilities probe must default
29
+ * differently for the two); anything else normalizes to 'plugin'. */
30
+ const KINDS = new Set(['auth', 'rate-limit', 'network', 'plugin', 'timeout', 'protocol', 'unimplemented']);
28
31
 
29
32
  export class PluginOpError extends Error {
30
33
  /**
31
- * @param {'auth'|'rate-limit'|'network'|'plugin'|'timeout'|'protocol'} kind
34
+ * @param {'auth'|'rate-limit'|'network'|'plugin'|'timeout'|'protocol'|'unimplemented'} kind
32
35
  * @param {string} message
33
36
  */
34
37
  constructor(kind, message) {
@@ -74,7 +77,7 @@ async function mockCall(op, args) {
74
77
  if (entry === undefined) {
75
78
  // Mirror the real child's answer for an unimplemented op — Task 13's
76
79
  // capabilities tolerant-default keys on exactly this kind.
77
- throw new PluginOpError('plugin', `mock: no canned response for op "${op}"`);
80
+ throw new PluginOpError('unimplemented', `mock: no canned response for op "${op}"`);
78
81
  }
79
82
  try {
80
83
  return await (typeof entry === 'function' ? entry(args) : entry);
@@ -112,40 +115,100 @@ function loadSource(plugin, sourceId) {
112
115
  /** Child env: PATH + HOME ONLY (spec §7.2). Notably NOT WORCA_*, tokens, npm_*.
113
116
  * Exported for the channel-worker supervisor (chat/channel-host.mjs), which
114
117
  * spawns persistent children under the same rule. */
115
- export function scrubbedEnv() {
118
+ export function scrubbedEnv(platform = process.platform) {
116
119
  const env = {};
117
120
  if (process.env.PATH) env.PATH = process.env.PATH;
118
121
  if (process.env.HOME) env.HOME = process.env.HOME;
122
+ // Windows has no HOME and a child cannot even start without SYSTEMROOT (Node
123
+ // docs); the rest are the non-secret system vars node/npm need to resolve
124
+ // temp dirs, the shell, and the .cmd/.exe lookup. Still no WORCA_*, tokens
125
+ // or npm_* — the §7.2 rule is unchanged, only the platform baseline differs.
126
+ if (platform === 'win32') {
127
+ for (const k of WIN32_ENV_BASELINE) if (process.env[k]) env[k] = process.env[k];
128
+ }
119
129
  return env;
120
130
  }
121
131
 
132
+ /** Non-secret Windows system vars a child needs to run at all (spec §7.2 note). */
133
+ export const WIN32_ENV_BASELINE = Object.freeze([
134
+ 'USERPROFILE', 'SYSTEMROOT', 'SYSTEMDRIVE', 'COMSPEC', 'PATHEXT', 'TEMP', 'TMP', 'APPDATA', 'LOCALAPPDATA',
135
+ ]);
136
+
137
+ /** The profile invariant, enforced in the shim itself so EVERY caller — the
138
+ * CLI's `worca plugin exec`, future workers — is safe by construction, not
139
+ * just the HTTP routes. Presence alone is not enough: a typo'd or deleted
140
+ * profile passes a presence check, silently runs against an EMPTY bucket
141
+ * (false "not configured" verdicts) and then persists state into a phantom
142
+ * bucket the DELETE route can never purge — so membership in the roster is
143
+ * checked here too. `allowLegacyDefault` is the ONE host-internal exception:
144
+ * write-back for a pipeline row that predates profiles targets the
145
+ * DEFAULT_PROFILE bucket, because that is where the pre-upgrade flat config
146
+ * migrated — i.e. the instance the task actually came from (sources.mjs). */
147
+ function assertProfileInvariant({ plugin, sourceId, source, profile, allowLegacyDefault }) {
148
+ if (source.multiProfile === true) {
149
+ if (!profile) {
150
+ throw new PluginOpError('plugin',
151
+ `task source "${plugin}/${sourceId}" has per-profile configuration — pass a profile (e.g. --profile <id>)`);
152
+ }
153
+ if (profile === DEFAULT_PROFILE) {
154
+ if (!allowLegacyDefault) {
155
+ throw new PluginOpError('plugin',
156
+ `"${DEFAULT_PROFILE}" is the reserved shared bucket, not a profile of "${plugin}/${sourceId}" — pass a profile from the roster`);
157
+ }
158
+ } else if (!listProfileIds(plugin).includes(profile)) {
159
+ throw new PluginOpError('plugin',
160
+ `plugin "${plugin}" has no profile "${profile}" — create it in Plugins settings (or POST /api/plugins/${plugin}/profiles) first`);
161
+ }
162
+ } else if (profile && profile !== DEFAULT_PROFILE) {
163
+ throw new PluginOpError('plugin', `task source "${plugin}/${sourceId}" does not use profiles`);
164
+ }
165
+ }
166
+
122
167
  /**
123
168
  * Run ONE connector op in an ephemeral child. Resolves with the op result;
124
169
  * rejects with PluginOpError. `logger(level, msg)` is optional — connector
125
170
  * ctx.log lines route there (default: console.error, since stdout is the UI's).
171
+ * `allowLegacyDefault` (host-internal, never surfaced to CLI/routes) lets the
172
+ * legacy write-back path name DEFAULT_PROFILE on a multiProfile source — see
173
+ * assertProfileInvariant.
126
174
  * @returns {Promise<any>}
127
175
  */
128
- export async function callSource({ plugin, sourceId, op, args = {}, timeoutMs = 30000, logger } = {}) {
176
+ export async function callSource({ plugin, sourceId, op, args = {}, profile, timeoutMs = 30000, logger, allowLegacyDefault = false } = {}) {
129
177
  const log = typeof logger === 'function'
130
178
  ? logger
131
179
  : (level, msg) => console.error(`[plugin:${plugin}] ${level}: ${msg}`);
180
+ // Which configuration of the source to run against (plugin-config.mjs
181
+ // profiles). Absent -> DEFAULT_PROFILE, which is what every single-profile
182
+ // source uses, so this parameter is invisible unless a plugin opts in.
183
+ const prof = profile || DEFAULT_PROFILE;
132
184
 
133
185
  if (mockMode()) {
134
- // Canned responses, no spawn, no plugin needed. reportResult additionally
135
- // records its args into the plugin state mirroring the real-child
136
- // stateDelta path so the offline smoke (Task 19) can assert write-back ran.
186
+ // Canned responses, no spawn, no plugin needed. The profile invariant still
187
+ // holds whenever the plugin IS installed (behavioral parity so mock runs
188
+ // catch missing/typo'd profiles too); an uninstalled plugin the offline
189
+ // smoke's whole point — skips it. reportResult additionally records its
190
+ // args into the plugin state — mirroring the real-child stateDelta path —
191
+ // so the offline smoke (Task 19) can assert write-back ran.
192
+ let mockSource = null;
193
+ try { mockSource = loadSource(plugin, sourceId).source; } catch { mockSource = null; }
194
+ if (mockSource) assertProfileInvariant({ plugin, sourceId, source: mockSource, profile, allowLegacyDefault });
137
195
  const r = await mockCall(op, args);
138
- if (op === 'reportResult') writePluginState(plugin, { lastReport: JSON.stringify(args) });
196
+ if (op === 'reportResult') writePluginState(plugin, { lastReport: JSON.stringify(args) }, prof);
139
197
  return r;
140
198
  }
141
199
 
142
200
  const { dir, source, apiVersion } = loadSource(plugin, sourceId);
201
+ assertProfileInvariant({ plugin, sourceId, source, profile, allowLegacyDefault });
143
202
  const payload = JSON.stringify({
144
203
  apiVersion,
145
204
  module: resolve(dir, source.module), // './'-relative, '..'-free (normalizeManifest)
146
205
  op,
147
- config: readPluginConfig(plugin, source.configSchema),
148
- state: readPluginState(plugin),
206
+ // The connector sees ONE profile's config/state and is told which — a
207
+ // connector that keeps its own storage (jira-source hangs a $JTR_CONFIG_DIR
208
+ // off it) needs the id, not just the values.
209
+ profile: prof,
210
+ config: readPluginConfig(plugin, source.configSchema, prof),
211
+ state: readPluginState(plugin, prof),
149
212
  args,
150
213
  });
151
214
 
@@ -191,7 +254,7 @@ export async function callSource({ plugin, sourceId, op, args = {}, timeoutMs =
191
254
  throw new PluginOpError(frame.error?.kind || 'plugin', frame.error?.message || `plugin "${plugin}" op "${op}" failed`);
192
255
  }
193
256
  if (frame.stateDelta && typeof frame.stateDelta === 'object' && Object.keys(frame.stateDelta).length) {
194
- writePluginState(plugin, frame.stateDelta); // host-side persist; child never touches the store
257
+ writePluginState(plugin, frame.stateDelta, prof); // host-side persist; child never touches the store
195
258
  }
196
259
  return frame.result;
197
260
  }