guild-agents 0.3.1 → 1.1.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 (40) hide show
  1. package/README.md +5 -1
  2. package/bin/guild.js +75 -1
  3. package/package.json +12 -5
  4. package/src/commands/doctor.js +70 -1
  5. package/src/commands/logs.js +63 -0
  6. package/src/commands/reset-learnings.js +44 -0
  7. package/src/commands/run.js +105 -0
  8. package/src/templates/agents/advisor.md +1 -0
  9. package/src/templates/agents/bugfix.md +1 -0
  10. package/src/templates/agents/code-reviewer.md +1 -0
  11. package/src/templates/agents/db-migration.md +1 -0
  12. package/src/templates/agents/developer.md +1 -0
  13. package/src/templates/agents/learnings-extractor.md +49 -0
  14. package/src/templates/agents/platform-expert.md +1 -0
  15. package/src/templates/agents/product-owner.md +1 -0
  16. package/src/templates/agents/qa.md +1 -0
  17. package/src/templates/agents/tech-lead.md +1 -0
  18. package/src/templates/skills/build-feature/SKILL.md +130 -26
  19. package/src/templates/skills/council/SKILL.md +51 -4
  20. package/src/templates/skills/create-pr/SKILL.md +32 -0
  21. package/src/templates/skills/dev-flow/SKILL.md +14 -0
  22. package/src/templates/skills/guild-specialize/SKILL.md +45 -3
  23. package/src/templates/skills/new-feature/SKILL.md +33 -0
  24. package/src/templates/skills/qa-cycle/SKILL.md +48 -5
  25. package/src/templates/skills/review/SKILL.md +22 -1
  26. package/src/templates/skills/session-end/SKILL.md +27 -0
  27. package/src/templates/skills/session-start/SKILL.md +32 -0
  28. package/src/templates/skills/status/SKILL.md +19 -0
  29. package/src/utils/dispatch-protocol.js +74 -0
  30. package/src/utils/dispatch.js +172 -0
  31. package/src/utils/executor.js +183 -0
  32. package/src/utils/learnings-io.js +76 -0
  33. package/src/utils/learnings.js +204 -0
  34. package/src/utils/orchestrator-io.js +356 -0
  35. package/src/utils/orchestrator.js +590 -0
  36. package/src/utils/providers/claude-code.js +43 -0
  37. package/src/utils/skill-loader.js +83 -0
  38. package/src/utils/trace.js +400 -0
  39. package/src/utils/version.js +90 -0
  40. package/src/utils/workflow-parser.js +225 -0
@@ -0,0 +1,204 @@
1
+ /**
2
+ * learnings.js — Pure functions for compound learning.
3
+ *
4
+ * Constants, parsing, rendering, token estimation, and context injection.
5
+ * Zero I/O, zero external dependencies — leaf module.
6
+ */
7
+
8
+ /** Fixed section names for the learnings file (spec section 3.3). */
9
+ export const LEARNINGS_SECTIONS = [
10
+ 'Project Patterns',
11
+ 'Architecture Decisions',
12
+ 'Past Issues & Resolutions',
13
+ 'User Preferences',
14
+ 'Agent Notes',
15
+ ];
16
+
17
+ /** Maximum token budget for the learnings file. */
18
+ export const TOKEN_BUDGET = 2000;
19
+
20
+ /** Default path to the learnings file, relative to project root. */
21
+ export const GUILD_LEARNINGS_PATH = '.claude/guild/learnings.md';
22
+
23
+ /**
24
+ * Estimates token count from text using a word-count heuristic.
25
+ * Approximation: tokens ≈ ceil(words × 1.35).
26
+ * @param {string | null | undefined} text
27
+ * @returns {number}
28
+ */
29
+ export function estimateTokens(text) {
30
+ if (!text || !text.trim()) return 0;
31
+ const words = text.trim().split(/\s+/).length;
32
+ return Math.ceil(words * 1.35);
33
+ }
34
+
35
+ /**
36
+ * Returns true if the estimated token count is within the budget.
37
+ * @param {string | null | undefined} text
38
+ * @param {number} [budget=TOKEN_BUDGET]
39
+ * @returns {boolean}
40
+ */
41
+ export function isWithinBudget(text, budget = TOKEN_BUDGET) {
42
+ return estimateTokens(text) <= budget;
43
+ }
44
+
45
+ /**
46
+ * Renders the initial/empty learnings file content.
47
+ * @param {string} [projectName='Project']
48
+ * @returns {string}
49
+ */
50
+ export function renderEmptyLearnings(projectName = 'Project') {
51
+ const lines = [];
52
+ lines.push(`# Guild Learnings — ${projectName}`);
53
+ lines.push('');
54
+ lines.push(`> Auto-generated by Guild. Last updated: ${new Date().toISOString()}`);
55
+ lines.push('> Total executions: 0');
56
+ lines.push('> Token budget: this file should stay under 2000 tokens');
57
+ lines.push('');
58
+
59
+ for (const section of LEARNINGS_SECTIONS) {
60
+ lines.push(`## ${section}`);
61
+ lines.push('');
62
+ lines.push('_No learnings yet._');
63
+ lines.push('');
64
+ }
65
+
66
+ return lines.join('\n');
67
+ }
68
+
69
+ const PLACEHOLDER_RE = /^_No learnings yet\._$/;
70
+ const ENTRY_RE = /^- (.+)$/;
71
+ const DISCOVERED_RE = /\(discovered: ([^)]+)\)/;
72
+
73
+ /**
74
+ * Parses a learnings.md file into a structured object.
75
+ * @param {string | null | undefined} content
76
+ * @returns {{ header: { project: string, lastUpdated: string, totalExecutions: number }, sections: Record<string, Array<{ text: string, discovered: string }>> }}
77
+ */
78
+ export function parseLearnings(content) {
79
+ const empty = {
80
+ header: { project: '', lastUpdated: '', totalExecutions: 0 },
81
+ sections: {},
82
+ };
83
+ for (const s of LEARNINGS_SECTIONS) empty.sections[s] = [];
84
+
85
+ if (!content || !content.trim()) return empty;
86
+
87
+ const lines = content.split('\n');
88
+ const result = { header: { project: '', lastUpdated: '', totalExecutions: 0 }, sections: {} };
89
+ for (const s of LEARNINGS_SECTIONS) result.sections[s] = [];
90
+
91
+ let currentSection = null;
92
+
93
+ for (const line of lines) {
94
+ // Parse H1 header
95
+ const h1Match = line.match(/^# Guild Learnings — (.+)$/);
96
+ if (h1Match) {
97
+ result.header.project = h1Match[1].trim();
98
+ continue;
99
+ }
100
+
101
+ // Parse header metadata
102
+ const updatedMatch = line.match(/^> Auto-generated by Guild\. Last updated: (.+)$/);
103
+ if (updatedMatch) {
104
+ result.header.lastUpdated = updatedMatch[1].trim();
105
+ continue;
106
+ }
107
+ const execMatch = line.match(/^> Total executions: (\d+)$/);
108
+ if (execMatch) {
109
+ result.header.totalExecutions = parseInt(execMatch[1], 10);
110
+ continue;
111
+ }
112
+
113
+ // Parse section headers
114
+ const h2Match = line.match(/^## (.+)$/);
115
+ if (h2Match) {
116
+ const sectionName = h2Match[1].trim();
117
+ if (LEARNINGS_SECTIONS.includes(sectionName)) {
118
+ currentSection = sectionName;
119
+ } else {
120
+ currentSection = null;
121
+ }
122
+ continue;
123
+ }
124
+
125
+ // Skip placeholders and blank lines
126
+ if (!currentSection) continue;
127
+ if (PLACEHOLDER_RE.test(line.trim())) continue;
128
+ if (!line.trim()) continue;
129
+
130
+ // Parse entries (lines starting with "- ")
131
+ const entryMatch = line.match(ENTRY_RE);
132
+ if (entryMatch) {
133
+ const text = entryMatch[1].trim();
134
+ const discoveredMatch = text.match(DISCOVERED_RE);
135
+ result.sections[currentSection].push({
136
+ text,
137
+ discovered: discoveredMatch ? discoveredMatch[1] : '',
138
+ });
139
+ }
140
+ }
141
+
142
+ return result;
143
+ }
144
+
145
+ /**
146
+ * Renders a parsed learnings structure back to markdown.
147
+ * @param {{ header: { project: string, lastUpdated: string, totalExecutions: number }, sections: Record<string, Array<{ text: string, discovered: string }>> }} parsed
148
+ * @returns {string}
149
+ */
150
+ export function renderLearnings(parsed) {
151
+ const lines = [];
152
+ const project = parsed.header.project || 'Project';
153
+ const lastUpdated = parsed.header.lastUpdated || new Date().toISOString();
154
+ const totalExecutions = parsed.header.totalExecutions || 0;
155
+
156
+ lines.push(`# Guild Learnings — ${project}`);
157
+ lines.push('');
158
+ lines.push(`> Auto-generated by Guild. Last updated: ${lastUpdated}`);
159
+ lines.push(`> Total executions: ${totalExecutions}`);
160
+ lines.push('> Token budget: this file should stay under 2000 tokens');
161
+ lines.push('');
162
+
163
+ for (const section of LEARNINGS_SECTIONS) {
164
+ lines.push(`## ${section}`);
165
+ lines.push('');
166
+ const entries = parsed.sections[section] || [];
167
+ if (entries.length === 0) {
168
+ lines.push('_No learnings yet._');
169
+ } else {
170
+ for (const entry of entries) {
171
+ lines.push(`- ${entry.text}`);
172
+ }
173
+ }
174
+ lines.push('');
175
+ }
176
+
177
+ return lines.join('\n');
178
+ }
179
+
180
+ /**
181
+ * Builds a context injection string from raw learnings content.
182
+ * Wraps in <guild-learnings> tags per spec section 4.2.
183
+ * Returns empty string for null/empty input.
184
+ * @param {string | null | undefined} content
185
+ * @returns {string}
186
+ */
187
+ export function buildContextInjection(content) {
188
+ if (!content || !content.trim()) return '';
189
+
190
+ const parsed = parseLearnings(content);
191
+ const hasEntries = LEARNINGS_SECTIONS.some(s => (parsed.sections[s] || []).length > 0);
192
+ if (!hasEntries) return '';
193
+
194
+ const lines = [];
195
+ lines.push('<guild-learnings>');
196
+ lines.push(content.trim());
197
+ lines.push('</guild-learnings>');
198
+ lines.push('');
199
+ lines.push('These are accumulated learnings from previous Guild executions on this project.');
200
+ lines.push('Use them to inform your work. Do not contradict established patterns unless');
201
+ lines.push('explicitly asked to change them.');
202
+
203
+ return lines.join('\n');
204
+ }
@@ -0,0 +1,356 @@
1
+ /**
2
+ * orchestrator-io.js — I/O layer for the Guild runtime orchestrator.
3
+ *
4
+ * Handles file I/O, skill loading, dispatch resolution, learnings injection,
5
+ * and trace management. Depends on the pure orchestrator.js for plan logic.
6
+ *
7
+ * Re-exports all public functions from orchestrator.js for a single import point.
8
+ */
9
+
10
+ import { join } from 'path';
11
+ import { loadSkill } from './skill-loader.js';
12
+ import { resolveAgentMetadata, resolveEffectiveTier, resolveModel } from './dispatch.js';
13
+ import { createTrace, recordStep, finalizeTrace } from './trace.js';
14
+ import { buildContextInjection } from './learnings.js';
15
+ import { readLearnings } from './learnings-io.js';
16
+ import {
17
+ createExecutionPlan,
18
+ advanceStep,
19
+ getNextSteps,
20
+ isPlanComplete,
21
+ parseCondition,
22
+ evaluateCondition,
23
+ shouldRetry,
24
+ resolveFailureTarget,
25
+ expandDelegation,
26
+ } from './orchestrator.js';
27
+
28
+ // Re-export all public functions from orchestrator.js
29
+ export {
30
+ createExecutionPlan,
31
+ advanceStep,
32
+ getNextSteps,
33
+ isPlanComplete,
34
+ parseCondition,
35
+ evaluateCondition,
36
+ shouldRetry,
37
+ resolveFailureTarget,
38
+ expandDelegation,
39
+ };
40
+
41
+ // Also re-export constants
42
+ export { DEFAULT_CIRCUIT_BREAKER_LIMIT, MAX_DELEGATION_DEPTH } from './orchestrator.js';
43
+
44
+ /**
45
+ * @typedef {Object} StepDispatchInfo
46
+ * @property {'system'|'agent'} role - Whether this is a system or agent step
47
+ * @property {string|null} tier - Resolved model tier (null for system steps)
48
+ * @property {string|null} model - Resolved concrete model ID (null for system steps)
49
+ * @property {boolean} fallback - Whether a fallback model was used
50
+ * @property {object|null} agentMetadata - Agent frontmatter metadata (null for system steps)
51
+ */
52
+
53
+ /**
54
+ * Resolves dispatch information for a workflow step.
55
+ * System role steps return minimal dispatch info (no model).
56
+ * Agent steps resolve tier and model using the dispatch utilities.
57
+ *
58
+ * @param {object} step - Workflow step definition
59
+ * @param {object} [options={}] - Options
60
+ * @param {string} [options.profile='max'] - Model profile name
61
+ * @param {string} [options.projectRoot=process.cwd()] - Project root for agent lookup
62
+ * @returns {StepDispatchInfo}
63
+ */
64
+ export function resolveStepDispatch(step, options = {}) {
65
+ const { profile = 'max', projectRoot = process.cwd() } = options;
66
+
67
+ if (step.role === 'system') {
68
+ return {
69
+ role: 'system',
70
+ tier: null,
71
+ model: null,
72
+ fallback: false,
73
+ agentMetadata: null,
74
+ };
75
+ }
76
+
77
+ const agentMetadata = resolveAgentMetadata(step.role, projectRoot);
78
+
79
+ // Build a step config object compatible with resolveEffectiveTier
80
+ const stepConfig = {
81
+ role: step.role,
82
+ 'model-tier': step.modelTier,
83
+ };
84
+
85
+ const tier = resolveEffectiveTier(stepConfig, agentMetadata);
86
+
87
+ // W4: resolveModel already handles the fallback chain internally;
88
+ // a single try-catch is sufficient here.
89
+ let model;
90
+ let fallback;
91
+
92
+ try {
93
+ model = resolveModel(tier, profile);
94
+ fallback = false;
95
+ } catch {
96
+ model = null;
97
+ fallback = false;
98
+ }
99
+
100
+ return {
101
+ role: 'agent',
102
+ tier,
103
+ model,
104
+ fallback,
105
+ agentMetadata,
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Loads and validates a skill workflow from disk.
111
+ *
112
+ * @param {string} skillName - Name of the skill directory
113
+ * @param {object} [options={}] - Options
114
+ * @param {string} [options.basePath] - Override base path for skills directory
115
+ * @returns {{ workflow: object, body: string, name: string }}
116
+ * @throws {Error} If skill not found, has no workflow, or has validation errors
117
+ */
118
+ export function loadWorkflow(skillName, options = {}) {
119
+ const skill = options.basePath
120
+ ? loadSkill(skillName, options.basePath)
121
+ : loadSkill(skillName);
122
+
123
+ if (!skill.workflow) {
124
+ throw new Error(
125
+ `Skill "${skillName}" has no workflow definition. Only skills with a workflow block can be orchestrated.`
126
+ );
127
+ }
128
+
129
+ if (skill.errors && skill.errors.length > 0) {
130
+ throw new Error(
131
+ `Skill "${skillName}" has workflow validation errors:\n${skill.errors.join('\n')}`
132
+ );
133
+ }
134
+
135
+ return {
136
+ workflow: skill.workflow,
137
+ body: skill.body,
138
+ name: skill.name || skillName,
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Builds the context string for a step's execution.
144
+ * Combines step intent, learnings injection, skill body, and required step outcomes.
145
+ *
146
+ * @param {object} step - Workflow step definition
147
+ * @param {import('./orchestrator.js').ExecutionPlan} plan - Current execution plan
148
+ * @param {object} [options={}] - Options
149
+ * @param {string} [options.learningsPath] - Override path for learnings file
150
+ * @param {string} [options.skillBody=''] - Skill body (markdown prose)
151
+ * @returns {string} Context string for the step
152
+ */
153
+ export function buildStepContext(step, plan, options = {}) {
154
+ const { skillBody = '' } = options;
155
+ const parts = [];
156
+
157
+ // Step intent
158
+ parts.push(`## Task: ${step.intent}`);
159
+ parts.push('');
160
+
161
+ // Learnings injection (graceful degradation if reading fails)
162
+ try {
163
+ const learningsContent = readLearnings(options.learningsPath || undefined);
164
+ if (learningsContent) {
165
+ const injection = buildContextInjection(learningsContent);
166
+ if (injection) {
167
+ parts.push(injection);
168
+ parts.push('');
169
+ }
170
+ }
171
+ } catch {
172
+ // Learnings reading failed — continue without them
173
+ }
174
+
175
+ // Skill body (prose instructions)
176
+ if (skillBody && skillBody.trim()) {
177
+ parts.push('## Skill Instructions');
178
+ parts.push('');
179
+ parts.push(skillBody.trim());
180
+ parts.push('');
181
+ }
182
+
183
+ // Outcomes from required steps
184
+ if (step.requires && step.requires.length > 0) {
185
+ const requiredOutcomes = [];
186
+ for (const requirement of step.requires) {
187
+ // Find which step produces this requirement
188
+ for (const group of plan.groups) {
189
+ for (const s of group.steps) {
190
+ const state = plan.stepStates[s.id];
191
+ if (state && state.outcome && s.produces && s.produces.includes(requirement)) {
192
+ if (state.outcome[requirement] !== undefined) {
193
+ requiredOutcomes.push({ field: requirement, value: state.outcome[requirement], fromStep: s.id });
194
+ }
195
+ }
196
+ }
197
+ }
198
+ }
199
+
200
+ if (requiredOutcomes.length > 0) {
201
+ parts.push('## Inputs from previous steps');
202
+ parts.push('');
203
+ for (const item of requiredOutcomes) {
204
+ parts.push(`### ${item.field} (from step: ${item.fromStep})`);
205
+ parts.push('');
206
+ parts.push(String(item.value));
207
+ parts.push('');
208
+ }
209
+ }
210
+ }
211
+
212
+ return parts.join('\n').trimEnd();
213
+ }
214
+
215
+ /**
216
+ * Records a step execution in the trace context.
217
+ *
218
+ * @param {import('./trace.js').TraceContext} traceCtx - Active trace context
219
+ * @param {object} step - Step definition
220
+ * @param {import('./orchestrator.js').StepState} stepState - Final state of the step
221
+ * @param {StepDispatchInfo} dispatchInfo - Resolved dispatch information
222
+ * @returns {import('./trace.js').TraceContext} Updated trace context (same reference)
223
+ */
224
+ export function recordStepTrace(traceCtx, step, stepState, dispatchInfo) {
225
+ const started = new Date().toISOString();
226
+
227
+ return recordStep(traceCtx, {
228
+ role: step.role,
229
+ intent: step.intent,
230
+ tier: dispatchInfo.tier || 'system',
231
+ model: dispatchInfo.model || 'none',
232
+ fallback: dispatchInfo.fallback,
233
+ started,
234
+ // TODO(v2): capture actual wall-clock duration and token usage from step executor
235
+ duration: 0,
236
+ tokens: 0,
237
+ result: stepState.status === 'passed' ? 'pass' : stepState.status === 'skipped' ? 'skip' : 'fail',
238
+ });
239
+ }
240
+
241
+ /**
242
+ * Finalizes a workflow trace and produces an execution summary.
243
+ *
244
+ * testsPass and lintPass are derived from gate steps whose intent contains
245
+ * the keywords "test" or "lint" respectively. They can be overridden via
246
+ * options when the caller has better information.
247
+ *
248
+ * @param {import('./trace.js').TraceContext} traceCtx - Completed trace context
249
+ * @param {import('./orchestrator.js').ExecutionPlan} plan - Final execution plan
250
+ * @param {object} [options={}] - Optional overrides
251
+ * @param {boolean} [options.testsPass] - Override for testsPass (derived from gate steps if omitted)
252
+ * @param {boolean} [options.lintPass] - Override for lintPass (derived from gate steps if omitted)
253
+ * @returns {{ trace: object, executionSummary: string }}
254
+ */
255
+ export function finalizeWorkflowTrace(traceCtx, plan, options = {}) {
256
+ const allStates = Object.values(plan.stepStates);
257
+ const anyFailed = allStates.some(s => s.status === 'failed');
258
+
259
+ // Derive testsPass / lintPass from gate steps when not explicitly provided
260
+ let testsPass = options.testsPass;
261
+ let lintPass = options.lintPass;
262
+
263
+ if (testsPass === undefined || lintPass === undefined) {
264
+ for (const group of plan.groups) {
265
+ for (const step of group.steps) {
266
+ if (!step.gate) continue;
267
+ const state = plan.stepStates[step.id];
268
+ const passed = state && state.status === 'passed';
269
+ const intent = (step.intent || '').toLowerCase();
270
+ if (testsPass === undefined && intent.includes('test')) {
271
+ testsPass = passed;
272
+ }
273
+ if (lintPass === undefined && intent.includes('lint')) {
274
+ lintPass = passed;
275
+ }
276
+ }
277
+ }
278
+ // Fall back to true when no matching gate step found (v1 behaviour)
279
+ if (testsPass === undefined) testsPass = true;
280
+ if (lintPass === undefined) lintPass = true;
281
+ }
282
+
283
+ const summary = {
284
+ result: anyFailed || plan.status === 'aborted' || plan.status === 'circuit-breaker' ? 'fail' : 'pass',
285
+ testsPass,
286
+ lintPass,
287
+ };
288
+
289
+ const trace = finalizeTrace(traceCtx, summary);
290
+
291
+ return {
292
+ trace,
293
+ executionSummary: trace.executionSummary,
294
+ };
295
+ }
296
+
297
+ /**
298
+ * Main entry point for the orchestrator.
299
+ * Loads the workflow, creates an execution plan, resolves dispatch for all steps,
300
+ * and creates a trace context. In v1, this produces the plan only (no step execution).
301
+ *
302
+ * @param {string} skillName - Name of the skill to orchestrate
303
+ * @param {string} [input=''] - User input / feature description
304
+ * @param {object} [options={}] - Options
305
+ * @param {string} [options.profile='max'] - Model profile
306
+ * @param {string} [options.projectRoot=process.cwd()] - Project root
307
+ * @param {string} [options.basePath] - Override skills base path
308
+ * @param {string} [options.tracesDir] - Override traces directory
309
+ * @param {string} [options.traceLevel='default'] - Trace level
310
+ * @param {number} [options.circuitBreakerLimit] - Override circuit breaker limit
311
+ * @returns {Promise<{
312
+ * plan: import('./orchestrator.js').ExecutionPlan,
313
+ * trace: import('./trace.js').TraceContext,
314
+ * dispatchInfoMap: Object.<string, StepDispatchInfo>,
315
+ * input: string
316
+ * }>}
317
+ */
318
+ export async function orchestrate(skillName, input = '', options = {}) {
319
+ const {
320
+ profile = 'max',
321
+ projectRoot = process.cwd(),
322
+ tracesDir,
323
+ traceLevel = 'default',
324
+ circuitBreakerLimit,
325
+ } = options;
326
+
327
+ // Load workflow
328
+ const loadOpts = {};
329
+ if (options.basePath) {
330
+ loadOpts.basePath = options.basePath;
331
+ }
332
+
333
+ const { workflow, name } = loadWorkflow(skillName, loadOpts);
334
+
335
+ // Create execution plan
336
+ const planOptions = { skillName: name || skillName };
337
+ if (circuitBreakerLimit !== undefined) {
338
+ planOptions.circuitBreakerLimit = circuitBreakerLimit;
339
+ }
340
+ const plan = createExecutionPlan(workflow, planOptions);
341
+
342
+ // Resolve dispatch info for all steps (for plan introspection/display)
343
+ const dispatchInfoMap = {};
344
+ for (const group of plan.groups) {
345
+ for (const step of group.steps) {
346
+ dispatchInfoMap[step.id] = resolveStepDispatch(step, { profile, projectRoot });
347
+ }
348
+ }
349
+
350
+ // W8: return dispatchInfoMap and input as separate result fields instead of
351
+ // mutating traceCtx, keeping the trace context pure.
352
+ const traceDirResolved = tracesDir || join(projectRoot, '.claude', 'guild', 'traces');
353
+ const traceCtx = createTrace(skillName, traceLevel, traceDirResolved);
354
+
355
+ return { plan, trace: traceCtx, dispatchInfoMap, input };
356
+ }