@xelauvas/xela-cli 0.3.1 → 0.4.0

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 (137) hide show
  1. package/bin/xela.js +0 -12
  2. package/local/README.md +112 -0
  3. package/local/start-qwen.sh +47 -0
  4. package/local/start-tunnel.sh +36 -0
  5. package/local/xela-local.sh +139 -0
  6. package/package.json +9 -5
  7. package/run.sh +33 -0
  8. package/src/Tool.ts +1 -1
  9. package/src/_shims/_generated_stubs/_universal_stub.mjs +168 -0
  10. package/src/_shims/_generated_stubs/stub_346f96741420f62caece15b196e233c6.mjs +19 -0
  11. package/src/_shims/_generated_stubs/stub_4b79af2a6ac1af5a53a55fd666b2bbb4.mjs +3 -0
  12. package/src/_shims/_generated_stubs/stub_94345ddeadacc184ff895597ec5abf71.mjs +6 -0
  13. package/src/_shims/_generated_stubs/stub_99fa0bc272643c549d8a2ec6d578436f.mjs +3 -0
  14. package/src/_shims/_generated_stubs/stub_e3451b131fcaf272a2a75270b9a427a3.mjs +9 -0
  15. package/src/_shims/_generated_stubs/text_stub.mjs +1 -0
  16. package/src/_shims/bun_bundle.js +79 -4
  17. package/src/_shims/cjs_stub.cjs +32 -10
  18. package/src/_shims/loader.js +104 -5
  19. package/src/_shims/openai_adapter.ts +446 -230
  20. package/src/_shims/register.js +16 -3
  21. package/src/assistant/gate.ts +15 -0
  22. package/src/assistant/index.ts +89 -0
  23. package/src/bootstrap/state.ts +6 -1
  24. package/src/bridge/peerSessions.ts +18 -0
  25. package/src/cli/print.ts +5 -0
  26. package/src/commands/assistant/assistant.ts +17 -0
  27. package/src/commands/assistant/index.ts +11 -0
  28. package/src/commands/brief.ts +3 -1
  29. package/src/commands/buddy/buddy.ts +88 -0
  30. package/src/commands/buddy/index.ts +12 -0
  31. package/src/commands/model/model.tsx +8 -23
  32. package/src/commands/peers/index.ts +11 -0
  33. package/src/commands/peers/peers.ts +24 -0
  34. package/src/commands/proactive/proactive.ts +64 -0
  35. package/src/commands/proactive.ts +12 -0
  36. package/src/commands/workflows/index.ts +12 -0
  37. package/src/commands/workflows/workflows.ts +36 -0
  38. package/src/commands.ts +4 -8
  39. package/src/components/Messages.tsx +1 -1
  40. package/src/components/ModelPicker.tsx +4 -34
  41. package/src/components/Spinner.tsx +54 -1
  42. package/src/components/StructuredDiff/colorDiff.ts +3 -1
  43. package/src/components/messages/SnipBoundaryMessage.tsx +15 -0
  44. package/src/components/messages/UserCrossSessionMessage.tsx +26 -0
  45. package/src/components/permissions/PermissionRequest.tsx +4 -2
  46. package/src/components/permissions/ReviewArtifactPermissionRequest/ReviewArtifactPermissionRequest.tsx +96 -0
  47. package/src/constants/prompts.ts +26 -0
  48. package/src/ink/terminal.ts +9 -1
  49. package/src/jobs/classifier.ts +103 -0
  50. package/src/memdir/paths.ts +13 -1
  51. package/src/proactive/index.ts +158 -0
  52. package/src/proactive/useProactive.ts +78 -0
  53. package/src/screens/REPL.tsx +28 -8
  54. package/src/services/api/claude.ts +6 -1
  55. package/src/services/compact/autoCompact.ts +18 -4
  56. package/src/services/compact/cachedMCConfig.ts +1 -0
  57. package/src/services/compact/cachedMicrocompact.ts +113 -0
  58. package/src/services/compact/compact.ts +22 -2
  59. package/src/services/compact/reactiveCompact.ts +251 -0
  60. package/src/services/compact/snipCompact.ts +282 -0
  61. package/src/services/compact/snipProjection.ts +50 -0
  62. package/src/services/contextCollapse/index.ts +322 -0
  63. package/src/services/contextCollapse/operations.ts +187 -0
  64. package/src/services/contextCollapse/persist.ts +40 -0
  65. package/src/services/extractMemories/extractMemories.ts +11 -2
  66. package/src/services/skillSearch/featureCheck.ts +12 -0
  67. package/src/services/skillSearch/localSearch.ts +98 -0
  68. package/src/services/skillSearch/prefetch.ts +125 -0
  69. package/src/services/skillSearch/remoteSkillLoader.ts +9 -0
  70. package/src/services/skillSearch/remoteSkillState.ts +21 -0
  71. package/src/services/skillSearch/signals.ts +7 -0
  72. package/src/services/skillSearch/telemetry.ts +9 -0
  73. package/src/setup.ts +10 -5
  74. package/src/skills/bundled/dream.ts +31 -0
  75. package/src/skills/bundled/hunter.ts +45 -0
  76. package/src/skills/bundled/index.ts +10 -23
  77. package/src/skills/bundled/runSkillGenerator.ts +61 -0
  78. package/src/skills/mcpSkills.ts +101 -0
  79. package/src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts +301 -0
  80. package/src/tasks/MonitorMcpTask/MonitorMcpTask.ts +273 -0
  81. package/src/tasks.ts +22 -8
  82. package/src/tools/AgentTool/runAgent.ts +11 -0
  83. package/src/tools/BriefTool/BriefTool.ts +2 -0
  84. package/src/tools/CtxInspectTool/CtxInspectTool.ts +102 -0
  85. package/src/tools/DiscoverSkillsTool/DiscoverSkillsTool.ts +78 -0
  86. package/src/tools/DiscoverSkillsTool/prompt.ts +3 -0
  87. package/src/tools/ListPeersTool/ListPeersTool.ts +89 -0
  88. package/src/tools/MCPTool/MCPTool.ts +6 -0
  89. package/src/tools/MonitorTool/MonitorTool.ts +273 -0
  90. package/src/tools/MonitorTool/UI.tsx +25 -0
  91. package/src/tools/MonitorTool/events.ts +84 -0
  92. package/src/tools/MonitorTool/prompt.ts +45 -0
  93. package/src/tools/OverflowTestTool/OverflowTestTool.ts +73 -0
  94. package/src/tools/PushNotificationTool/PushNotificationTool.ts +119 -0
  95. package/src/tools/ReviewArtifactTool/ReviewArtifactTool.ts +93 -0
  96. package/src/tools/ReviewArtifactTool/prompt.ts +5 -0
  97. package/src/tools/SendUserFileTool/SendUserFileTool.ts +87 -0
  98. package/src/tools/SendUserFileTool/prompt.ts +7 -0
  99. package/src/tools/SleepTool/SleepTool.ts +126 -0
  100. package/src/tools/SnipTool/SnipTool.ts +90 -0
  101. package/src/tools/SnipTool/prompt.ts +5 -0
  102. package/src/tools/SubscribePRTool/SubscribePRTool.ts +115 -0
  103. package/src/tools/TerminalCaptureTool/TerminalCaptureTool.ts +129 -0
  104. package/src/tools/TerminalCaptureTool/prompt.ts +8 -0
  105. package/src/tools/ToolSearchTool/prompt.ts +1 -1
  106. package/src/tools/VerifyPlanExecutionTool/VerifyPlanExecutionTool.ts +99 -0
  107. package/src/tools/WebBrowserTool/WebBrowserPanel.tsx +10 -0
  108. package/src/tools/WebBrowserTool/WebBrowserTool.ts +190 -0
  109. package/src/tools/WebBrowserTool/prompt.ts +16 -0
  110. package/src/tools/WorkflowTool/UI.tsx +27 -0
  111. package/src/tools/WorkflowTool/WorkflowTool.ts +401 -0
  112. package/src/tools/WorkflowTool/bundled/index.ts +88 -0
  113. package/src/tools/WorkflowTool/constants.ts +7 -0
  114. package/src/tools/WorkflowTool/createWorkflowCommand.ts +84 -0
  115. package/src/tools/WorkflowTool/prompt.ts +38 -0
  116. package/src/tools/WorkflowTool/runtime.ts +331 -0
  117. package/src/tools.ts +36 -31
  118. package/src/utils/betas.ts +5 -0
  119. package/src/utils/context.ts +9 -0
  120. package/src/utils/conversationRecovery.ts +1 -1
  121. package/src/utils/forkedAgent.ts +4 -0
  122. package/src/utils/logoV2Utils.ts +7 -3
  123. package/src/utils/messageQueueManager.ts +23 -9
  124. package/src/utils/model/model.ts +10 -9
  125. package/src/utils/model/modelOptions.ts +0 -21
  126. package/src/utils/model/validateModel.ts +0 -19
  127. package/src/utils/permissions/permissionSetup.ts +5 -0
  128. package/src/utils/permissions/permissions.ts +25 -0
  129. package/src/utils/permissions/yolo-classifier-prompts/auto_mode_system_prompt.txt +38 -0
  130. package/src/utils/permissions/yolo-classifier-prompts/permissions_external.txt +25 -0
  131. package/src/utils/permissions/yoloClassifier.ts +28 -6
  132. package/src/utils/plugins/officialMarketplaceStartupCheck.ts +9 -0
  133. package/src/utils/systemThemeWatcher.ts +62 -0
  134. package/src/utils/udsClient.ts +115 -0
  135. package/src/utils/udsMessaging.ts +157 -0
  136. package/src/utils/yaml.ts +251 -5
  137. package/src/utils/model/providerModels.ts +0 -168
@@ -0,0 +1,401 @@
1
+ import { readFileSync } from 'fs'
2
+ import { isAbsolute, resolve } from 'path'
3
+ import { z } from 'zod/v4'
4
+ import {
5
+ addWorkflowAgent,
6
+ finishWorkflow,
7
+ isLocalWorkflowTask,
8
+ type LocalWorkflowTaskState,
9
+ registerWorkflowTask,
10
+ setWorkflowPhase,
11
+ updateWorkflowAgent,
12
+ type WorkflowAgentController,
13
+ } from '../../tasks/LocalWorkflowTask/LocalWorkflowTask.js'
14
+ import { buildTool } from '../../Tool.js'
15
+ import type { AgentId } from '../../types/ids.js'
16
+ import { getCwd } from '../../utils/cwd.js'
17
+ import { lazySchema } from '../../utils/lazySchema.js'
18
+ import { extractTextContent } from '../../utils/messages.js'
19
+ import { jsonStringify } from '../../utils/slowOperations.js'
20
+ import { appendTaskOutput, getTaskOutputPath } from '../../utils/task/diskOutput.js'
21
+ import { AgentTool } from '../AgentTool/AgentTool.js'
22
+ import { getBundledWorkflow, initBundledWorkflows } from './bundled/index.js'
23
+ import {
24
+ DEFAULT_MAX_AGENTS,
25
+ HARD_MAX_AGENTS,
26
+ WORKFLOW_TOOL_NAME,
27
+ } from './constants.js'
28
+ import { DESCRIPTION } from './prompt.js'
29
+ import {
30
+ errorText,
31
+ parseWorkflowScript,
32
+ runWorkflowScript,
33
+ safeStringify,
34
+ type WorkflowAgentOptions,
35
+ WorkflowScriptError,
36
+ } from './runtime.js'
37
+ import { renderToolResultMessage, renderToolUseMessage } from './UI.js'
38
+
39
+ const inputSchema = lazySchema(() =>
40
+ z.strictObject({
41
+ script: z
42
+ .string()
43
+ .optional()
44
+ .describe('Self-contained workflow script (plain JavaScript) starting with `export const meta = {...}`.'),
45
+ scriptPath: z
46
+ .string()
47
+ .optional()
48
+ .describe('Path to a workflow script file on disk. Takes precedence over script and name.'),
49
+ name: z
50
+ .string()
51
+ .optional()
52
+ .describe('Name of a bundled or saved workflow (see /workflows).'),
53
+ args: z
54
+ .unknown()
55
+ .optional()
56
+ .describe('Input exposed to the script as the global `args`, verbatim.'),
57
+ max_agents: z
58
+ .number()
59
+ .int()
60
+ .positive()
61
+ .optional()
62
+ .describe(`Cap on agent() calls for this run (default ${DEFAULT_MAX_AGENTS}, at most ${HARD_MAX_AGENTS}).`),
63
+ run_in_background: z
64
+ .boolean()
65
+ .optional()
66
+ .describe('Default true: return a task ID now and get a notification when done. false waits and returns the result inline.'),
67
+ resumeFromRunId: z
68
+ .string()
69
+ .optional()
70
+ .describe('Not supported in this build; present for compatibility.'),
71
+ }),
72
+ )
73
+ type InputSchema = ReturnType<typeof inputSchema>
74
+ export type WorkflowInput = z.infer<InputSchema>
75
+
76
+ const outputSchema = lazySchema(() =>
77
+ z.object({
78
+ runId: z.string(),
79
+ taskId: z.string(),
80
+ name: z.string(),
81
+ description: z.string(),
82
+ status: z.enum(['running', 'completed', 'failed']),
83
+ outputFile: z.string(),
84
+ agentsLaunched: z.number(),
85
+ result: z.unknown().optional(),
86
+ error: z.string().optional(),
87
+ message: z.string(),
88
+ }),
89
+ )
90
+ type OutputSchema = ReturnType<typeof outputSchema>
91
+ export type Output = z.infer<OutputSchema>
92
+
93
+ function loadScript(input: WorkflowInput): { script: string; origin: string } {
94
+ initBundledWorkflows()
95
+ if (input.scriptPath) {
96
+ const path = isAbsolute(input.scriptPath)
97
+ ? input.scriptPath
98
+ : resolve(getCwd(), input.scriptPath)
99
+ return { script: readFileSync(path, 'utf8'), origin: path }
100
+ }
101
+ if (input.script) return { script: input.script, origin: 'inline' }
102
+ if (input.name) {
103
+ const bundled = getBundledWorkflow(input.name)
104
+ if (bundled) return { script: bundled.script, origin: `bundled:${input.name}` }
105
+ throw new Error(`Unknown workflow "${input.name}". See /workflows for the available names.`)
106
+ }
107
+ throw new Error('Provide script, scriptPath or name')
108
+ }
109
+
110
+ export const WorkflowTool = buildTool({
111
+ name: WORKFLOW_TOOL_NAME,
112
+ searchHint: 'orchestrate several subagents with a deterministic script',
113
+ maxResultSizeChars: 200_000,
114
+ userFacingName: () => 'Workflow',
115
+ get inputSchema(): InputSchema {
116
+ return inputSchema()
117
+ },
118
+ get outputSchema(): OutputSchema {
119
+ return outputSchema()
120
+ },
121
+ isConcurrencySafe() {
122
+ return false
123
+ },
124
+ toAutoClassifierInput(input) {
125
+ const src = input.script ?? input.scriptPath ?? input.name ?? ''
126
+ return src.length > 4000 ? src.slice(0, 4000) + '…' : src
127
+ },
128
+ async validateInput(input) {
129
+ if (input.resumeFromRunId) {
130
+ return {
131
+ result: false,
132
+ message: 'resumeFromRunId is not supported in this build; run the workflow again',
133
+ errorCode: 1,
134
+ }
135
+ }
136
+ try {
137
+ const { script } = loadScript(input)
138
+ parseWorkflowScript(script)
139
+ } catch (e) {
140
+ return { result: false, message: errorText(e), errorCode: 1 }
141
+ }
142
+ if (input.max_agents && input.max_agents > HARD_MAX_AGENTS) {
143
+ return {
144
+ result: false,
145
+ message: `max_agents cannot exceed ${HARD_MAX_AGENTS}`,
146
+ errorCode: 1,
147
+ }
148
+ }
149
+ return { result: true }
150
+ },
151
+ async checkPermissions(input) {
152
+ let summary = 'a workflow'
153
+ try {
154
+ const { meta } = parseWorkflowScript(loadScript(input).script)
155
+ summary = `"${meta.name}": ${meta.description}`
156
+ } catch {
157
+ // validateInput reports parse errors
158
+ }
159
+ return {
160
+ behavior: 'ask',
161
+ message: `Run workflow ${summary} (up to ${input.max_agents ?? DEFAULT_MAX_AGENTS} subagents; each subagent asks for its own tool permissions)`,
162
+ }
163
+ },
164
+ async description(input) {
165
+ try {
166
+ return parseWorkflowScript(loadScript(input).script).meta.description
167
+ } catch {
168
+ return 'Run a workflow script'
169
+ }
170
+ },
171
+ async prompt() {
172
+ return DESCRIPTION
173
+ },
174
+ mapToolResultToToolResultBlockParam(output, toolUseID) {
175
+ return {
176
+ tool_use_id: toolUseID,
177
+ type: 'tool_result',
178
+ content: jsonStringify(output),
179
+ }
180
+ },
181
+ renderToolUseMessage,
182
+ renderToolResultMessage,
183
+ async call(input, context, canUseTool, parentMessage) {
184
+ const { getAppState, setAppState } = context
185
+ const rootSetAppState =
186
+ (context as { setAppStateForTasks?: typeof setAppState }).setAppStateForTasks ??
187
+ setAppState
188
+ const toolUseId = (context as { toolUseId?: string }).toolUseId
189
+ const parentAgentId = (context as { agentId?: AgentId }).agentId
190
+ const { script } = loadScript(input)
191
+ const { meta, body } = parseWorkflowScript(script)
192
+ const maxAgents = Math.min(input.max_agents ?? DEFAULT_MAX_AGENTS, HARD_MAX_AGENTS)
193
+ const background = input.run_in_background !== false
194
+
195
+ const task = await registerWorkflowTask(
196
+ {
197
+ name: meta.name,
198
+ description: meta.description,
199
+ args: input.args,
200
+ phases: meta.phases ?? [],
201
+ toolUseId,
202
+ agentId: parentAgentId,
203
+ isBackgrounded: background,
204
+ },
205
+ rootSetAppState,
206
+ )
207
+ const taskId = task.id
208
+ const log = (line: string): void => appendTaskOutput(taskId, line + '\n')
209
+
210
+ const runAgent = async (
211
+ prompt: string,
212
+ options: WorkflowAgentOptions,
213
+ index: number,
214
+ ): Promise<string> => {
215
+ const agentRecordId = `${taskId}-a${index}`
216
+ const label = options.label ?? `agent ${index + 1}`
217
+ const controller: WorkflowAgentController = {
218
+ abortController: new AbortController(),
219
+ retry: false,
220
+ }
221
+ addWorkflowAgent(
222
+ taskId,
223
+ {
224
+ id: agentRecordId,
225
+ index,
226
+ label,
227
+ phase: options.phase,
228
+ status: 'running',
229
+ startTime: Date.now(),
230
+ },
231
+ controller,
232
+ rootSetAppState,
233
+ )
234
+ log(`[agent ${label}] start`)
235
+
236
+ // One attempt; the task panel's "retry" aborts the attempt with
237
+ // controller.retry set, which loops here for a fresh one.
238
+ for (;;) {
239
+ controller.abortController = new AbortController()
240
+ controller.retry = false
241
+ const attemptController = controller.abortController
242
+ const onParentAbort = (): void => attemptController.abort()
243
+ task.abortController.signal.addEventListener('abort', onParentAbort, { once: true })
244
+ try {
245
+ const text = await runSubagent(prompt, options, attemptController)
246
+ updateWorkflowAgent(
247
+ taskId,
248
+ agentRecordId,
249
+ { status: 'completed', endTime: Date.now(), resultPreview: text.slice(0, 200) },
250
+ rootSetAppState,
251
+ )
252
+ log(`[agent ${label}] done (${text.length} chars)`)
253
+ return text
254
+ } catch (err) {
255
+ if (attemptController.signal.aborted && controller.retry) {
256
+ log(`[agent ${label}] retrying`)
257
+ continue
258
+ }
259
+ const skipped = attemptController.signal.aborted && !task.abortController.signal.aborted
260
+ updateWorkflowAgent(
261
+ taskId,
262
+ agentRecordId,
263
+ {
264
+ status: skipped ? 'skipped' : 'failed',
265
+ endTime: Date.now(),
266
+ error: skipped ? 'skipped' : errorText(err),
267
+ },
268
+ rootSetAppState,
269
+ )
270
+ log(`[agent ${label}] ${skipped ? 'skipped' : `failed: ${errorText(err)}`}`)
271
+ throw skipped ? new WorkflowScriptError(`agent "${label}" skipped`) : err
272
+ } finally {
273
+ task.abortController.signal.removeEventListener('abort', onParentAbort)
274
+ }
275
+ }
276
+ }
277
+
278
+ const runSubagent = async (
279
+ prompt: string,
280
+ options: WorkflowAgentOptions,
281
+ abortController: AbortController,
282
+ ): Promise<string> => {
283
+ const subContext = { ...context, abortController }
284
+ const result = await AgentTool.call(
285
+ {
286
+ prompt,
287
+ description: (options.label ?? 'workflow agent').slice(0, 60),
288
+ subagent_type: options.subagent_type ?? 'general-purpose',
289
+ run_in_background: false,
290
+ model: options.model,
291
+ } as Parameters<typeof AgentTool.call>[0],
292
+ subContext as Parameters<typeof AgentTool.call>[1],
293
+ canUseTool,
294
+ parentMessage,
295
+ )
296
+ const data = result.data as {
297
+ status?: string
298
+ agentId?: string
299
+ content?: Array<{ type: string; text?: string }>
300
+ }
301
+ if (data.status === 'async_launched' && data.agentId) {
302
+ return waitForAgentTask(data.agentId, abortController.signal)
303
+ }
304
+ return extractTextContent(
305
+ (data.content ?? []) as Parameters<typeof extractTextContent>[0],
306
+ '\n',
307
+ )
308
+ }
309
+
310
+ // When the host forces subagents into the background, wait for the task
311
+ // to finish and read its final result from the task state
312
+ const waitForAgentTask = async (
313
+ agentId: string,
314
+ signal: AbortSignal,
315
+ ): Promise<string> => {
316
+ for (;;) {
317
+ if (signal.aborted) throw new WorkflowScriptError('agent aborted')
318
+ const t = getAppState().tasks?.[agentId] as
319
+ | { status?: string; result?: { content?: Array<{ type: string; text?: string }> }; error?: string }
320
+ | undefined
321
+ if (!t) throw new WorkflowScriptError(`agent task ${agentId} disappeared`)
322
+ if (t.status === 'completed') {
323
+ return extractTextContent(
324
+ (t.result?.content ?? []) as Parameters<typeof extractTextContent>[0],
325
+ '\n',
326
+ )
327
+ }
328
+ if (t.status === 'failed' || t.status === 'killed') {
329
+ throw new WorkflowScriptError(`agent ${t.status}${t.error ? `: ${t.error}` : ''}`)
330
+ }
331
+ await new Promise(r => setTimeout(r, 500))
332
+ }
333
+ }
334
+
335
+ const execution = (async (): Promise<{ result?: unknown; error?: string }> => {
336
+ try {
337
+ const result = await runWorkflowScript(body, input.args, {
338
+ runAgent,
339
+ onPhase: title => {
340
+ setWorkflowPhase(taskId, title, rootSetAppState)
341
+ log(`[phase] ${title}`)
342
+ },
343
+ onLog: line => log(`[log] ${line}`),
344
+ signal: task.abortController.signal,
345
+ maxAgents,
346
+ })
347
+ log(`[workflow] result:\n${safeStringify(result)}`)
348
+ finishWorkflow(taskId, 'completed', result, undefined, rootSetAppState)
349
+ return { result }
350
+ } catch (err) {
351
+ const message = errorText(err)
352
+ log(`[workflow] failed: ${message}`)
353
+ const current = getAppState().tasks?.[taskId]
354
+ if (isLocalWorkflowTask(current) && current.status === 'killed') {
355
+ return { error: 'stopped' }
356
+ }
357
+ finishWorkflow(taskId, 'failed', undefined, message, rootSetAppState)
358
+ return { error: message }
359
+ }
360
+ })()
361
+
362
+ const agentsLaunched = (): number => {
363
+ const t = getAppState().tasks?.[taskId]
364
+ return isLocalWorkflowTask(t) ? (t as LocalWorkflowTaskState).agents.length : 0
365
+ }
366
+
367
+ if (background) {
368
+ void execution
369
+ return {
370
+ data: {
371
+ runId: task.runId,
372
+ taskId,
373
+ name: meta.name,
374
+ description: meta.description,
375
+ status: 'running' as const,
376
+ outputFile: getTaskOutputPath(taskId),
377
+ agentsLaunched: 0,
378
+ message: `Workflow "${meta.name}" started as task ${taskId}. A task notification with the result arrives when it completes; /workflows shows progress.`,
379
+ },
380
+ }
381
+ }
382
+
383
+ const outcome = await execution
384
+ return {
385
+ data: {
386
+ runId: task.runId,
387
+ taskId,
388
+ name: meta.name,
389
+ description: meta.description,
390
+ status: outcome.error ? ('failed' as const) : ('completed' as const),
391
+ outputFile: getTaskOutputPath(taskId),
392
+ agentsLaunched: agentsLaunched(),
393
+ result: outcome.result,
394
+ error: outcome.error,
395
+ message: outcome.error
396
+ ? `Workflow "${meta.name}" failed: ${outcome.error}`
397
+ : `Workflow "${meta.name}" completed with ${agentsLaunched()} agents.`,
398
+ },
399
+ }
400
+ },
401
+ })
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Bundled (built-in) workflows, addressable by name through the Workflow
3
+ * tool's `name` input. The registry lives on globalThis so the tool and the
4
+ * slash-command loader see the same entries regardless of how each module
5
+ * was loaded.
6
+ */
7
+ export type BundledWorkflow = {
8
+ name: string
9
+ description: string
10
+ script: string
11
+ }
12
+
13
+ type Registry = Map<string, BundledWorkflow>
14
+ const registry: Registry = ((
15
+ globalThis as unknown as { __xelaBundledWorkflows?: Registry }
16
+ ).__xelaBundledWorkflows ??= new Map())
17
+
18
+ export function registerBundledWorkflow(workflow: BundledWorkflow): void {
19
+ registry.set(workflow.name, workflow)
20
+ }
21
+
22
+ export function getBundledWorkflow(name: string): BundledWorkflow | undefined {
23
+ return registry.get(name)
24
+ }
25
+
26
+ export function listBundledWorkflows(): BundledWorkflow[] {
27
+ return [...registry.values()]
28
+ }
29
+
30
+ const REVIEW_CHANGES = `export const meta = {
31
+ name: 'review-changes',
32
+ description: 'Review the current diff across dimensions in parallel, then verify each finding adversarially',
33
+ phases: [{ title: 'Review' }, { title: 'Verify' }],
34
+ }
35
+ const FINDINGS_SCHEMA = {
36
+ type: 'object',
37
+ properties: {
38
+ findings: {
39
+ type: 'array',
40
+ items: {
41
+ type: 'object',
42
+ properties: {
43
+ title: { type: 'string' },
44
+ file: { type: 'string' },
45
+ line: { type: 'number' },
46
+ severity: { type: 'string', enum: ['low', 'medium', 'high'] },
47
+ detail: { type: 'string' },
48
+ },
49
+ required: ['title', 'file', 'detail'],
50
+ },
51
+ },
52
+ },
53
+ required: ['findings'],
54
+ }
55
+ const VERDICT_SCHEMA = {
56
+ type: 'object',
57
+ properties: { isReal: { type: 'boolean' }, reason: { type: 'string' } },
58
+ required: ['isReal', 'reason'],
59
+ }
60
+ const target = args && args.target ? String(args.target) : 'the uncommitted changes (git diff) in the current repository'
61
+ const DIMENSIONS = [
62
+ { key: 'bugs', prompt: 'Review ' + target + ' for correctness bugs: wrong logic, unhandled errors, race conditions, off-by-one, null handling. Read the diff and the surrounding code. Report only real defects.' },
63
+ { key: 'security', prompt: 'Review ' + target + ' for security problems: injection, auth/permission gaps, secrets, unsafe deserialization, path traversal. Read the diff and the surrounding code. Report only real defects.' },
64
+ { key: 'tests', prompt: 'Review ' + target + ' for missing or weak test coverage of the changed behavior. Report concrete gaps with the file they belong to.' },
65
+ ]
66
+ const results = await pipeline(
67
+ DIMENSIONS,
68
+ d => agent(d.prompt, { label: 'review:' + d.key, phase: 'Review', schema: FINDINGS_SCHEMA }),
69
+ review => parallel((review.findings || []).slice(0, 4).map(f => () =>
70
+ agent('Adversarially verify this code review finding by reading the code. Finding: ' + JSON.stringify(f) + '. Decide whether it is a real problem a maintainer must fix.', { label: 'verify:' + f.file, phase: 'Verify', schema: VERDICT_SCHEMA })
71
+ .then(v => ({ ...f, verdict: v }))
72
+ ))
73
+ )
74
+ const confirmed = results.flat().filter(Boolean).filter(f => f.verdict && f.verdict.isReal)
75
+ return { confirmed, reviewed: DIMENSIONS.map(d => d.key) }
76
+ `
77
+
78
+ let initialized = false
79
+ export function initBundledWorkflows(): void {
80
+ if (initialized) return
81
+ initialized = true
82
+ registerBundledWorkflow({
83
+ name: 'review-changes',
84
+ description:
85
+ 'Review the current diff across dimensions in parallel, then verify each finding adversarially',
86
+ script: REVIEW_CHANGES,
87
+ })
88
+ }
@@ -0,0 +1,7 @@
1
+ export const WORKFLOW_TOOL_NAME = 'Workflow'
2
+
3
+ // Default cap on agent() calls per run. The tool input can raise it.
4
+ export const DEFAULT_MAX_AGENTS = 10
5
+ export const HARD_MAX_AGENTS = 50
6
+ // Concurrency for parallel()/pipeline() when the script gives none
7
+ export const DEFAULT_CONCURRENCY = 4
@@ -0,0 +1,84 @@
1
+ import { readdirSync, readFileSync } from 'fs'
2
+ import { basename, join } from 'path'
3
+ import type { Command } from '../../commands.js'
4
+ import { getClaudeConfigHomeDir } from '../../utils/envUtils.js'
5
+ import { initBundledWorkflows, listBundledWorkflows } from './bundled/index.js'
6
+ import { WORKFLOW_TOOL_NAME } from './constants.js'
7
+ import { parseWorkflowScript } from './runtime.js'
8
+
9
+ /**
10
+ * Saved workflows become slash commands. Sources, in precedence order:
11
+ * <cwd>/.claude/workflows/*.js (project)
12
+ * <config dir>/workflows/*.js (user)
13
+ * bundled workflows
14
+ * The command itself is a prompt that asks the model to run the Workflow
15
+ * tool with the script, so the user's permission flow still applies.
16
+ */
17
+ export async function getWorkflowCommands(cwd: string): Promise<Command[]> {
18
+ initBundledWorkflows()
19
+ const seen = new Set<string>()
20
+ const commands: Command[] = []
21
+
22
+ const dirs = [
23
+ join(cwd, '.claude', 'workflows'),
24
+ join(getClaudeConfigHomeDir(), 'workflows'),
25
+ ]
26
+ for (const dir of dirs) {
27
+ let entries: string[] = []
28
+ try {
29
+ entries = readdirSync(dir).filter(f => /\.(m?js)$/.test(f))
30
+ } catch {
31
+ continue
32
+ }
33
+ for (const file of entries) {
34
+ const path = join(dir, file)
35
+ let description = `Run workflow ${file}`
36
+ let name = basename(file).replace(/\.(m?js)$/, '')
37
+ try {
38
+ const { meta } = parseWorkflowScript(readFileSync(path, 'utf8'))
39
+ description = meta.description
40
+ name = meta.name || name
41
+ } catch {
42
+ // keep the filename-derived defaults; the tool reports the parse error on run
43
+ }
44
+ if (seen.has(name)) continue
45
+ seen.add(name)
46
+ commands.push(makeWorkflowCommand(name, description, `scriptPath ${JSON.stringify(path)}`))
47
+ }
48
+ }
49
+
50
+ for (const wf of listBundledWorkflows()) {
51
+ if (seen.has(wf.name)) continue
52
+ seen.add(wf.name)
53
+ commands.push(makeWorkflowCommand(wf.name, wf.description, `name ${JSON.stringify(wf.name)}`))
54
+ }
55
+ return commands
56
+ }
57
+
58
+ function makeWorkflowCommand(
59
+ name: string,
60
+ description: string,
61
+ selector: string,
62
+ ): Command {
63
+ return {
64
+ type: 'prompt',
65
+ name,
66
+ description: `${description} (workflow)`,
67
+ source: 'builtin',
68
+ progressMessage: `running workflow ${name}`,
69
+ contentLength: 0,
70
+ async getPromptForCommand(args: string) {
71
+ const argsNote = args?.trim()
72
+ ? ` Pass these arguments as the tool's \`args\` input (as a JSON string value if they are not valid JSON): ${args.trim()}`
73
+ : ''
74
+ return [
75
+ {
76
+ type: 'text' as const,
77
+ text:
78
+ `The user invoked the saved workflow "${name}". Run it now with the ${WORKFLOW_TOOL_NAME} tool using ${selector}.${argsNote} ` +
79
+ `When the completion notification arrives, summarize the workflow result for the user.`,
80
+ },
81
+ ]
82
+ },
83
+ } as Command
84
+ }
@@ -0,0 +1,38 @@
1
+ import { DEFAULT_MAX_AGENTS } from './constants.js'
2
+
3
+ export const DESCRIPTION = `Execute a workflow script that orchestrates multiple subagents deterministically. Workflows run in the background — this tool returns immediately with a task ID, and a task notification arrives when the workflow completes. Use /workflows to see runs.
4
+
5
+ Only call this tool when the user explicitly opted into multi-agent orchestration (they asked for a workflow, "fan out agents", "orchestrate with subagents", or invoked a saved/bundled workflow by name). A task that would merely benefit from parallelism does not count: use the Agent tool for individual subagents instead.
6
+
7
+ Every script must begin with \`export const meta = {...}\`: a PURE LITERAL (no variables, calls or interpolation) giving the workflow's \`name\`, a one-line \`description\` and optionally \`phases\` — one \`{ title, detail? }\` per phase() call. Pass the script inline via \`script\`; it is plain JavaScript (no imports, no require, no TypeScript). A top-level \`return\` gives the workflow result.
8
+
9
+ Script API (all available as globals):
10
+ - \`agent(prompt, { label?, phase?, schema?, model?, subagent_type?, allowedTools?, maxTurns? })\` → runs one subagent to completion and resolves to its final text. With \`schema\` (a JSON Schema object) the agent is told to answer with JSON only and the parsed object is returned.
11
+ - \`parallel(thunks, { concurrency? })\` → runs an array of \`() => Promise\` with bounded concurrency (default 4); resolves to an array of results, \`null\` for items that failed (the error is logged).
12
+ - \`pipeline(items, stage1, stage2, ...)\` → for each item runs stage1(item), then stage2(result1), ... as soon as the previous stage finishes, across items in parallel; resolves to the array of final results (\`null\` for failures).
13
+ - \`phase(title, fn?)\` → marks the current phase (shown in the task panel); if \`fn\` is given, awaits it.
14
+ - \`log(...)\` and \`console.log(...)\` → write to the run's output file.
15
+ - \`sleep(ms)\`, \`args\` (the tool's \`args\` input, verbatim), plus JSON, Math, Date, Promise and the standard globals.
16
+
17
+ The canonical multi-stage pattern — pipeline by default, each dimension verifies as soon as its review completes:
18
+
19
+ export const meta = {
20
+ name: 'review-changes',
21
+ description: 'Review changed files across dimensions, verify each finding',
22
+ phases: [{ title: 'Review' }, { title: 'Verify' }],
23
+ }
24
+ const DIMENSIONS = [{ key: 'bugs', prompt: '...' }, { key: 'perf', prompt: '...' }]
25
+ const results = await pipeline(
26
+ DIMENSIONS,
27
+ d => agent(d.prompt, { label: \`review:\${d.key}\`, phase: 'Review', schema: FINDINGS_SCHEMA }),
28
+ review => parallel(review.findings.map(f => () =>
29
+ agent(\`Adversarially verify: \${f.title}\`, { label: \`verify:\${f.file}\`, phase: 'Verify', schema: VERDICT_SCHEMA })
30
+ .then(v => ({ ...f, verdict: v }))
31
+ ))
32
+ )
33
+ const confirmed = results.flat().filter(Boolean).filter(f => f.verdict?.isReal)
34
+ return { confirmed }
35
+
36
+ Limits: at most ${DEFAULT_MAX_AGENTS} agent() calls per run unless \`max_agents\` raises it. Keep workflows small on a local model: every agent is a full subagent conversation.
37
+
38
+ Inputs: \`script\` (inline), or \`scriptPath\` (a .js file on disk), or \`name\` (a bundled or saved workflow); optional \`args\` (any JSON, exposed as \`args\`), \`max_agents\`, and \`run_in_background\` (default true; false waits and returns the result inline).`