@xelauvas/xela-cli 0.3.0 → 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.
- package/local/README.md +112 -0
- package/local/start-qwen.sh +47 -0
- package/local/start-tunnel.sh +36 -0
- package/local/xela-local.sh +139 -0
- package/package.json +19 -5
- package/run.sh +33 -0
- package/src/Tool.ts +1 -1
- package/src/_shims/_generated_stubs/_universal_stub.mjs +168 -0
- package/src/_shims/_generated_stubs/stub_346f96741420f62caece15b196e233c6.mjs +19 -0
- package/src/_shims/_generated_stubs/stub_4b79af2a6ac1af5a53a55fd666b2bbb4.mjs +3 -0
- package/src/_shims/_generated_stubs/stub_94345ddeadacc184ff895597ec5abf71.mjs +6 -0
- package/src/_shims/_generated_stubs/stub_99fa0bc272643c549d8a2ec6d578436f.mjs +3 -0
- package/src/_shims/_generated_stubs/stub_e3451b131fcaf272a2a75270b9a427a3.mjs +9 -0
- package/src/_shims/_generated_stubs/text_stub.mjs +1 -0
- package/src/_shims/bun_bundle.js +79 -4
- package/src/_shims/cjs_stub.cjs +32 -10
- package/src/_shims/loader.js +104 -5
- package/src/_shims/openai_adapter.ts +446 -230
- package/src/_shims/register.js +16 -3
- package/src/assistant/gate.ts +15 -0
- package/src/assistant/index.ts +89 -0
- package/src/bootstrap/state.ts +6 -1
- package/src/bridge/peerSessions.ts +18 -0
- package/src/cli/print.ts +5 -0
- package/src/commands/assistant/assistant.ts +17 -0
- package/src/commands/assistant/index.ts +11 -0
- package/src/commands/brief.ts +3 -1
- package/src/commands/buddy/buddy.ts +88 -0
- package/src/commands/buddy/index.ts +12 -0
- package/src/commands/model/model.tsx +8 -23
- package/src/commands/peers/index.ts +11 -0
- package/src/commands/peers/peers.ts +24 -0
- package/src/commands/proactive/proactive.ts +64 -0
- package/src/commands/proactive.ts +12 -0
- package/src/commands/workflows/index.ts +12 -0
- package/src/commands/workflows/workflows.ts +36 -0
- package/src/commands.ts +4 -8
- package/src/components/Messages.tsx +1 -1
- package/src/components/ModelPicker.tsx +4 -34
- package/src/components/Spinner.tsx +54 -1
- package/src/components/StructuredDiff/colorDiff.ts +3 -1
- package/src/components/messages/SnipBoundaryMessage.tsx +15 -0
- package/src/components/messages/UserCrossSessionMessage.tsx +26 -0
- package/src/components/permissions/PermissionRequest.tsx +4 -2
- package/src/components/permissions/ReviewArtifactPermissionRequest/ReviewArtifactPermissionRequest.tsx +96 -0
- package/src/constants/prompts.ts +26 -0
- package/src/ink/terminal.ts +9 -1
- package/src/jobs/classifier.ts +103 -0
- package/src/memdir/paths.ts +13 -1
- package/src/proactive/index.ts +158 -0
- package/src/proactive/useProactive.ts +78 -0
- package/src/screens/REPL.tsx +28 -8
- package/src/services/api/claude.ts +6 -1
- package/src/services/compact/autoCompact.ts +18 -4
- package/src/services/compact/cachedMCConfig.ts +1 -0
- package/src/services/compact/cachedMicrocompact.ts +113 -0
- package/src/services/compact/compact.ts +22 -2
- package/src/services/compact/reactiveCompact.ts +251 -0
- package/src/services/compact/snipCompact.ts +282 -0
- package/src/services/compact/snipProjection.ts +50 -0
- package/src/services/contextCollapse/index.ts +322 -0
- package/src/services/contextCollapse/operations.ts +187 -0
- package/src/services/contextCollapse/persist.ts +40 -0
- package/src/services/extractMemories/extractMemories.ts +11 -2
- package/src/services/skillSearch/featureCheck.ts +12 -0
- package/src/services/skillSearch/localSearch.ts +98 -0
- package/src/services/skillSearch/prefetch.ts +125 -0
- package/src/services/skillSearch/remoteSkillLoader.ts +9 -0
- package/src/services/skillSearch/remoteSkillState.ts +21 -0
- package/src/services/skillSearch/signals.ts +7 -0
- package/src/services/skillSearch/telemetry.ts +9 -0
- package/src/setup.ts +10 -5
- package/src/skills/bundled/dream.ts +31 -0
- package/src/skills/bundled/hunter.ts +45 -0
- package/src/skills/bundled/index.ts +10 -23
- package/src/skills/bundled/runSkillGenerator.ts +61 -0
- package/src/skills/mcpSkills.ts +101 -0
- package/src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts +301 -0
- package/src/tasks/MonitorMcpTask/MonitorMcpTask.ts +273 -0
- package/src/tasks.ts +22 -8
- package/src/tools/AgentTool/runAgent.ts +11 -0
- package/src/tools/BriefTool/BriefTool.ts +2 -0
- package/src/tools/CtxInspectTool/CtxInspectTool.ts +102 -0
- package/src/tools/DiscoverSkillsTool/DiscoverSkillsTool.ts +78 -0
- package/src/tools/DiscoverSkillsTool/prompt.ts +3 -0
- package/src/tools/ListPeersTool/ListPeersTool.ts +89 -0
- package/src/tools/MCPTool/MCPTool.ts +6 -0
- package/src/tools/MonitorTool/MonitorTool.ts +273 -0
- package/src/tools/MonitorTool/UI.tsx +25 -0
- package/src/tools/MonitorTool/events.ts +84 -0
- package/src/tools/MonitorTool/prompt.ts +45 -0
- package/src/tools/OverflowTestTool/OverflowTestTool.ts +73 -0
- package/src/tools/PushNotificationTool/PushNotificationTool.ts +119 -0
- package/src/tools/ReviewArtifactTool/ReviewArtifactTool.ts +93 -0
- package/src/tools/ReviewArtifactTool/prompt.ts +5 -0
- package/src/tools/SendUserFileTool/SendUserFileTool.ts +87 -0
- package/src/tools/SendUserFileTool/prompt.ts +7 -0
- package/src/tools/SleepTool/SleepTool.ts +126 -0
- package/src/tools/SnipTool/SnipTool.ts +90 -0
- package/src/tools/SnipTool/prompt.ts +5 -0
- package/src/tools/SubscribePRTool/SubscribePRTool.ts +115 -0
- package/src/tools/TerminalCaptureTool/TerminalCaptureTool.ts +129 -0
- package/src/tools/TerminalCaptureTool/prompt.ts +8 -0
- package/src/tools/ToolSearchTool/prompt.ts +1 -1
- package/src/tools/VerifyPlanExecutionTool/VerifyPlanExecutionTool.ts +99 -0
- package/src/tools/WebBrowserTool/WebBrowserPanel.tsx +10 -0
- package/src/tools/WebBrowserTool/WebBrowserTool.ts +190 -0
- package/src/tools/WebBrowserTool/prompt.ts +16 -0
- package/src/tools/WorkflowTool/UI.tsx +27 -0
- package/src/tools/WorkflowTool/WorkflowTool.ts +401 -0
- package/src/tools/WorkflowTool/bundled/index.ts +88 -0
- package/src/tools/WorkflowTool/constants.ts +7 -0
- package/src/tools/WorkflowTool/createWorkflowCommand.ts +84 -0
- package/src/tools/WorkflowTool/prompt.ts +38 -0
- package/src/tools/WorkflowTool/runtime.ts +331 -0
- package/src/tools.ts +36 -31
- package/src/utils/betas.ts +5 -0
- package/src/utils/context.ts +9 -0
- package/src/utils/conversationRecovery.ts +1 -1
- package/src/utils/forkedAgent.ts +4 -0
- package/src/utils/logoV2Utils.ts +7 -3
- package/src/utils/messageQueueManager.ts +23 -9
- package/src/utils/model/model.ts +10 -9
- package/src/utils/model/modelOptions.ts +0 -21
- package/src/utils/model/validateModel.ts +0 -19
- package/src/utils/permissions/permissionSetup.ts +5 -0
- package/src/utils/permissions/permissions.ts +25 -0
- package/src/utils/permissions/yolo-classifier-prompts/auto_mode_system_prompt.txt +38 -0
- package/src/utils/permissions/yolo-classifier-prompts/permissions_external.txt +25 -0
- package/src/utils/permissions/yoloClassifier.ts +28 -6
- package/src/utils/plugins/officialMarketplaceStartupCheck.ts +9 -0
- package/src/utils/systemThemeWatcher.ts +62 -0
- package/src/utils/udsClient.ts +115 -0
- package/src/utils/udsMessaging.ts +157 -0
- package/src/utils/yaml.ts +251 -5
- 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).`
|