icopilot 2.2.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 (203) hide show
  1. package/CHANGELOG.md +250 -0
  2. package/LICENSE +21 -0
  3. package/README.md +214 -0
  4. package/bin/icopilot.js +6 -0
  5. package/dist/acp/router.js +123 -0
  6. package/dist/acp/schema.js +53 -0
  7. package/dist/agents/aggregator.js +187 -0
  8. package/dist/agents/custom-agents.js +97 -0
  9. package/dist/agents/goal-driven.js +411 -0
  10. package/dist/agents/multi-repo.js +350 -0
  11. package/dist/agents/parallel-runner.js +181 -0
  12. package/dist/agents/router.js +144 -0
  13. package/dist/agents/self-heal.js +481 -0
  14. package/dist/agents/tdd-agent.js +278 -0
  15. package/dist/api/github-models.js +158 -0
  16. package/dist/bridge/ide-bridge.js +479 -0
  17. package/dist/cloud/routine-executor.js +34 -0
  18. package/dist/cloud/routine-scheduler.js +67 -0
  19. package/dist/cloud/routine-storage.js +297 -0
  20. package/dist/commands/acp-cmd.js +143 -0
  21. package/dist/commands/actions-cmd.js +624 -0
  22. package/dist/commands/agent-cmd.js +144 -0
  23. package/dist/commands/alias-cmd.js +132 -0
  24. package/dist/commands/bookmark-cmd.js +77 -0
  25. package/dist/commands/changelog-cmd.js +99 -0
  26. package/dist/commands/changes-cmd.js +120 -0
  27. package/dist/commands/clipboard-cmd.js +217 -0
  28. package/dist/commands/cloud-routine-cmd.js +265 -0
  29. package/dist/commands/codegen-cmd.js +544 -0
  30. package/dist/commands/compare-cmd.js +116 -0
  31. package/dist/commands/context-cmd.js +247 -0
  32. package/dist/commands/context-viz-cmd.js +43 -0
  33. package/dist/commands/conventions-cmd.js +116 -0
  34. package/dist/commands/cost-cmd.js +51 -0
  35. package/dist/commands/deps-cmd.js +294 -0
  36. package/dist/commands/diagram-cmd.js +658 -0
  37. package/dist/commands/diff-review-cmd.js +92 -0
  38. package/dist/commands/doc-cmd.js +412 -0
  39. package/dist/commands/doctor-cmd.js +152 -0
  40. package/dist/commands/editor-cmd.js +49 -0
  41. package/dist/commands/env-cmd.js +86 -0
  42. package/dist/commands/explain-cmd.js +78 -0
  43. package/dist/commands/explain-shell-cmd.js +22 -0
  44. package/dist/commands/explore-cmd.js +231 -0
  45. package/dist/commands/feedback-cmd.js +98 -0
  46. package/dist/commands/fix-cmd.js +17 -0
  47. package/dist/commands/generate-cmd.js +38 -0
  48. package/dist/commands/git-extra.js +197 -0
  49. package/dist/commands/git-log-cmd.js +98 -0
  50. package/dist/commands/git-undo-cmd.js +137 -0
  51. package/dist/commands/git.js +155 -0
  52. package/dist/commands/history-cmd.js +122 -0
  53. package/dist/commands/index-cmd.js +65 -0
  54. package/dist/commands/init-cmd.js +73 -0
  55. package/dist/commands/lint-cmd.js +133 -0
  56. package/dist/commands/memory-cmd.js +98 -0
  57. package/dist/commands/metrics-cmd.js +97 -0
  58. package/dist/commands/mode-prefix.js +30 -0
  59. package/dist/commands/multi-cmd.js +44 -0
  60. package/dist/commands/notify-cmd.js +204 -0
  61. package/dist/commands/profile-cmd.js +101 -0
  62. package/dist/commands/prompts.js +17 -0
  63. package/dist/commands/rag-cmd.js +60 -0
  64. package/dist/commands/readme-cmd.js +564 -0
  65. package/dist/commands/reasoning-cmd.js +34 -0
  66. package/dist/commands/refactor-cmd.js +96 -0
  67. package/dist/commands/release-cmd.js +450 -0
  68. package/dist/commands/repo-cmd.js +195 -0
  69. package/dist/commands/route-cmd.js +21 -0
  70. package/dist/commands/schedule-cmd.js +109 -0
  71. package/dist/commands/search-cmd.js +47 -0
  72. package/dist/commands/security-cmd.js +156 -0
  73. package/dist/commands/settings-cmd.js +238 -0
  74. package/dist/commands/skill-cmd.js +338 -0
  75. package/dist/commands/slash.js +2721 -0
  76. package/dist/commands/snippets-cmd.js +83 -0
  77. package/dist/commands/space-cmd.js +92 -0
  78. package/dist/commands/stash-cmd.js +156 -0
  79. package/dist/commands/stats-cmd.js +36 -0
  80. package/dist/commands/style-cmd.js +85 -0
  81. package/dist/commands/suggest-cmd.js +40 -0
  82. package/dist/commands/summary-cmd.js +138 -0
  83. package/dist/commands/task-cmd.js +58 -0
  84. package/dist/commands/team-memory-cmd.js +97 -0
  85. package/dist/commands/template-cmd.js +475 -0
  86. package/dist/commands/test-cmd.js +146 -0
  87. package/dist/commands/todo-cmd.js +172 -0
  88. package/dist/commands/tokens-cmd.js +277 -0
  89. package/dist/commands/trigger-cmd.js +147 -0
  90. package/dist/commands/undo-cmd.js +18 -0
  91. package/dist/commands/voice-cmd.js +89 -0
  92. package/dist/commands/watch-cmd.js +110 -0
  93. package/dist/commands/web-cmd.js +183 -0
  94. package/dist/commands/worktree-cmd.js +119 -0
  95. package/dist/config-profile.js +66 -0
  96. package/dist/config.js +288 -0
  97. package/dist/context/compactor.js +53 -0
  98. package/dist/context/dep-context.js +329 -0
  99. package/dist/context/file-refs.js +54 -0
  100. package/dist/context/git-context.js +229 -0
  101. package/dist/context/image-input.js +66 -0
  102. package/dist/context/memory.js +55 -0
  103. package/dist/context/persistent-memory.js +104 -0
  104. package/dist/context/pinned.js +96 -0
  105. package/dist/context/priority.js +150 -0
  106. package/dist/context/read-only.js +48 -0
  107. package/dist/context/smart-files.js +286 -0
  108. package/dist/context/team-memory.js +156 -0
  109. package/dist/extensions/loader.js +149 -0
  110. package/dist/extensions/marketplace.js +49 -0
  111. package/dist/extensions/slack-provider.js +181 -0
  112. package/dist/extensions/team.js +56 -0
  113. package/dist/extensions/teams-provider.js +222 -0
  114. package/dist/extensions/voice.js +18 -0
  115. package/dist/hooks/lifecycle.js +215 -0
  116. package/dist/hooks/precommit.js +463 -0
  117. package/dist/index/embeddings.js +23 -0
  118. package/dist/index/indexer.js +86 -0
  119. package/dist/index/retrieve.js +20 -0
  120. package/dist/index/store.js +95 -0
  121. package/dist/index.js +286 -0
  122. package/dist/intelligence/dead-code.js +457 -0
  123. package/dist/intelligence/error-watch.js +263 -0
  124. package/dist/intelligence/navigation.js +141 -0
  125. package/dist/intelligence/stack-trace.js +210 -0
  126. package/dist/intelligence/symbol-index.js +410 -0
  127. package/dist/knowledge/auto-memory.js +412 -0
  128. package/dist/knowledge/conventions.js +475 -0
  129. package/dist/knowledge/corrections.js +213 -0
  130. package/dist/knowledge/rag.js +450 -0
  131. package/dist/knowledge/style-learner.js +324 -0
  132. package/dist/logger.js +35 -0
  133. package/dist/mcp/client.js +144 -0
  134. package/dist/mcp/config.js +24 -0
  135. package/dist/mcp/index.js +89 -0
  136. package/dist/modes/auto-compact.js +20 -0
  137. package/dist/modes/autopilot.js +157 -0
  138. package/dist/modes/background.js +82 -0
  139. package/dist/modes/interactive.js +187 -0
  140. package/dist/modes/oneshot.js +36 -0
  141. package/dist/modes/tui.js +265 -0
  142. package/dist/modes/turn.js +342 -0
  143. package/dist/notifications/manager.js +107 -0
  144. package/dist/plugins/marketplace.js +244 -0
  145. package/dist/providers/custom-provider.js +298 -0
  146. package/dist/providers/local-model.js +121 -0
  147. package/dist/routing/profiles.js +44 -0
  148. package/dist/routing/router.js +18 -0
  149. package/dist/sandbox/container.js +151 -0
  150. package/dist/security/audit.js +237 -0
  151. package/dist/security/content-filter.js +449 -0
  152. package/dist/security/proxy.js +301 -0
  153. package/dist/security/retention.js +281 -0
  154. package/dist/security/roles.js +252 -0
  155. package/dist/server/api-server.js +679 -0
  156. package/dist/session/bookmarks.js +72 -0
  157. package/dist/session/cloud-session.js +291 -0
  158. package/dist/session/handoff.js +405 -0
  159. package/dist/session/manager.js +35 -0
  160. package/dist/session/session.js +296 -0
  161. package/dist/session/share.js +313 -0
  162. package/dist/session/undo-journal.js +91 -0
  163. package/dist/snippets/store.js +60 -0
  164. package/dist/spaces/space-config.js +156 -0
  165. package/dist/spaces/space.js +220 -0
  166. package/dist/stats/store.js +101 -0
  167. package/dist/tools/apply-patch.js +134 -0
  168. package/dist/tools/auto-check.js +218 -0
  169. package/dist/tools/diff-edit.js +150 -0
  170. package/dist/tools/diff-prompt.js +36 -0
  171. package/dist/tools/edit-file.js +66 -0
  172. package/dist/tools/file-ops.js +205 -0
  173. package/dist/tools/glob.js +17 -0
  174. package/dist/tools/grep.js +56 -0
  175. package/dist/tools/image.js +194 -0
  176. package/dist/tools/list-directory.js +228 -0
  177. package/dist/tools/memory.js +17 -0
  178. package/dist/tools/multi-edit.js +299 -0
  179. package/dist/tools/policy.js +95 -0
  180. package/dist/tools/registry.js +484 -0
  181. package/dist/tools/retry.js +74 -0
  182. package/dist/tools/run-in-terminal.js +162 -0
  183. package/dist/tools/safety.js +64 -0
  184. package/dist/tools/sandbox.js +15 -0
  185. package/dist/tools/search-symbols.js +212 -0
  186. package/dist/tools/shell.js +118 -0
  187. package/dist/tools/web.js +167 -0
  188. package/dist/ui/prompt.js +37 -0
  189. package/dist/ui/render.js +96 -0
  190. package/dist/ui/screen.js +13 -0
  191. package/dist/ui/theme.js +56 -0
  192. package/dist/util/browser.js +34 -0
  193. package/dist/util/completion.js +350 -0
  194. package/dist/util/cost.js +28 -0
  195. package/dist/util/keybindings.js +113 -0
  196. package/dist/util/lazy.js +26 -0
  197. package/dist/util/perf.js +25 -0
  198. package/dist/util/token-worker.js +11 -0
  199. package/dist/util/tokens.js +50 -0
  200. package/dist/workflows/builtins.js +128 -0
  201. package/dist/workflows/engine.js +496 -0
  202. package/dist/workflows/file-trigger.js +197 -0
  203. package/package.json +79 -0
@@ -0,0 +1,128 @@
1
+ import { stringify } from 'yaml';
2
+ export const BUILTIN_WORKFLOWS = [
3
+ {
4
+ name: 'review-and-commit',
5
+ description: 'Inspect the working tree, summarize changes, and prepare a commit message.',
6
+ triggers: [{ type: 'manual', config: {} }],
7
+ steps: [
8
+ {
9
+ id: 'git-status',
10
+ name: 'Capture git status',
11
+ action: 'shell',
12
+ params: { command: 'git --no-pager status --short' },
13
+ onFail: 'stop',
14
+ },
15
+ {
16
+ id: 'diff-summary',
17
+ name: 'Capture diff summary',
18
+ action: 'shell',
19
+ params: { command: 'git --no-pager diff --stat' },
20
+ onFail: 'continue',
21
+ },
22
+ {
23
+ id: 'commit-prompt',
24
+ name: 'Draft commit guidance',
25
+ action: 'prompt',
26
+ params: {
27
+ prompt: 'Review this status and diff summary, then draft a conventional commit message:\n\nStatus:\n${steps.git-status.output}\n\nDiff:\n${steps.diff-summary.output}',
28
+ },
29
+ },
30
+ ],
31
+ },
32
+ {
33
+ name: 'test-fix-loop',
34
+ description: 'Run tests, summarize failures, and iterate through follow-up prompts.',
35
+ triggers: [{ type: 'manual', config: {} }],
36
+ steps: [
37
+ {
38
+ id: 'run-tests',
39
+ name: 'Run repository tests',
40
+ action: 'shell',
41
+ params: { command: 'npm test -- --runInBand', retries: 1 },
42
+ onFail: 'continue',
43
+ },
44
+ {
45
+ id: 'failure-summary',
46
+ name: 'Summarize failures',
47
+ action: 'prompt',
48
+ params: {
49
+ prompt: 'Summarize the latest test run and propose the smallest fix:\n\n${steps.run-tests.output}',
50
+ },
51
+ },
52
+ {
53
+ id: 'follow-up-loop',
54
+ name: 'Generate retry prompts',
55
+ action: 'loop',
56
+ params: {
57
+ items: ['re-run targeted tests', 'verify typecheck', 'prepare regression summary'],
58
+ steps: [
59
+ {
60
+ id: 'loop-prompt',
61
+ name: 'Draft a follow-up action',
62
+ action: 'prompt',
63
+ params: {
64
+ prompt: 'Next action ${loop.index}: ${loop.item}\nLatest output:\n${steps.run-tests.output}',
65
+ },
66
+ },
67
+ ],
68
+ },
69
+ },
70
+ ],
71
+ },
72
+ {
73
+ name: 'release-prep',
74
+ description: 'Collect the core checks needed before shipping a release.',
75
+ triggers: [
76
+ { type: 'manual', config: {} },
77
+ { type: 'schedule', config: { cron: '0 9 * * 1-5' } },
78
+ ],
79
+ steps: [
80
+ {
81
+ id: 'typecheck',
82
+ name: 'Run typecheck',
83
+ action: 'shell',
84
+ params: { command: 'npm run typecheck' },
85
+ onFail: 'continue',
86
+ },
87
+ {
88
+ id: 'tests',
89
+ name: 'Run tests',
90
+ action: 'shell',
91
+ params: { command: 'npm test' },
92
+ onFail: 'continue',
93
+ },
94
+ {
95
+ id: 'release-notes',
96
+ name: 'Draft release checklist',
97
+ action: 'prompt',
98
+ params: {
99
+ prompt: 'Prepare a release checklist using the latest typecheck and test output.\n\nTypecheck:\n${steps.typecheck.output}\n\nTests:\n${steps.tests.output}',
100
+ },
101
+ },
102
+ ],
103
+ },
104
+ ];
105
+ export function getBuiltinWorkflow(name) {
106
+ return BUILTIN_WORKFLOWS.find((workflow) => workflow.name === name);
107
+ }
108
+ export function renderWorkflowYaml(workflow) {
109
+ return `${stringify(workflow).trimEnd()}\n`;
110
+ }
111
+ export function createWorkflowTemplate(name) {
112
+ return {
113
+ name,
114
+ description: `Describe what the ${name} workflow should do.`,
115
+ triggers: [{ type: 'manual', config: {} }],
116
+ steps: [
117
+ {
118
+ id: 'first-step',
119
+ name: 'First step',
120
+ action: 'prompt',
121
+ params: {
122
+ prompt: `Workflow ${name} is ready. Replace this prompt with real steps.`,
123
+ },
124
+ onFail: 'stop',
125
+ },
126
+ ],
127
+ };
128
+ }
@@ -0,0 +1,496 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawn } from 'node:child_process';
4
+ import { config } from '../config.js';
5
+ import { parse } from 'yaml';
6
+ export class WorkflowEngine {
7
+ cwd;
8
+ execution;
9
+ constructor(opts = {}) {
10
+ this.cwd = path.resolve(opts.cwd ?? config.cwd);
11
+ this.execution = this.createExecutionState();
12
+ }
13
+ loadWorkflows(dir) {
14
+ const workflowDir = this.resolveWorkflowDir(dir);
15
+ if (!fs.existsSync(workflowDir) || !fs.statSync(workflowDir).isDirectory()) {
16
+ return [];
17
+ }
18
+ return fs
19
+ .readdirSync(workflowDir)
20
+ .filter((entry) => entry.endsWith('.yaml') || entry.endsWith('.yml'))
21
+ .sort((a, b) => a.localeCompare(b))
22
+ .map((entry) => {
23
+ const filePath = path.join(workflowDir, entry);
24
+ const raw = fs.readFileSync(filePath, 'utf8');
25
+ let parsed;
26
+ try {
27
+ parsed = parse(raw);
28
+ }
29
+ catch (error) {
30
+ throw new Error(`Failed to parse workflow ${entry}: ${error?.message || String(error)}`);
31
+ }
32
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
33
+ throw new Error(`Workflow ${entry} must contain a YAML object`);
34
+ }
35
+ return parsed;
36
+ });
37
+ }
38
+ async run(workflow, context = {}) {
39
+ const startedAt = Date.now();
40
+ const validationErrors = this.validateWorkflow(workflow);
41
+ if (validationErrors.length > 0) {
42
+ return {
43
+ success: false,
44
+ duration: Date.now() - startedAt,
45
+ steps: validationErrors.map((error, index) => ({
46
+ stepId: `validation-${index + 1}`,
47
+ success: false,
48
+ error: `${error.path}: ${error.message}`,
49
+ duration: 0,
50
+ })),
51
+ };
52
+ }
53
+ this.execution = this.createExecutionState(context, workflow);
54
+ const steps = [];
55
+ for (const step of workflow.steps) {
56
+ const result = await this.runStep(step);
57
+ steps.push(result);
58
+ this.execution.prev = result;
59
+ this.execution.steps[step.id] = result;
60
+ if (!result.success && (step.onFail ?? 'stop') === 'stop') {
61
+ break;
62
+ }
63
+ }
64
+ return {
65
+ success: steps.length === workflow.steps.length && steps.every((step) => step.success),
66
+ steps,
67
+ duration: Date.now() - startedAt,
68
+ };
69
+ }
70
+ async runStep(step) {
71
+ const startedAt = Date.now();
72
+ const retryAttempts = step.onFail === 'retry' ? this.normalizeRetryCount(step.params?.retries) : 0;
73
+ for (let attempt = 0;; attempt += 1) {
74
+ try {
75
+ const result = await this.executeStep(step);
76
+ return { ...result, duration: Date.now() - startedAt };
77
+ }
78
+ catch (error) {
79
+ const failure = {
80
+ stepId: step.id,
81
+ success: false,
82
+ error: error?.message || String(error),
83
+ duration: Date.now() - startedAt,
84
+ };
85
+ if (attempt >= retryAttempts) {
86
+ return failure;
87
+ }
88
+ }
89
+ }
90
+ }
91
+ validateWorkflow(def) {
92
+ const errors = [];
93
+ if (!def || typeof def !== 'object' || Array.isArray(def)) {
94
+ return [{ path: 'workflow', message: 'Workflow definition must be an object' }];
95
+ }
96
+ if (!this.isNonEmptyString(def.name)) {
97
+ errors.push({ path: 'name', message: 'Workflow name is required' });
98
+ }
99
+ if (!this.isNonEmptyString(def.description)) {
100
+ errors.push({ path: 'description', message: 'Workflow description is required' });
101
+ }
102
+ if (!Array.isArray(def.steps) || def.steps.length === 0) {
103
+ errors.push({ path: 'steps', message: 'Workflow must include at least one step' });
104
+ }
105
+ const seenStepIds = new Set();
106
+ for (const [index, step] of (def.steps ?? []).entries()) {
107
+ const stepPath = `steps[${index}]`;
108
+ if (!step || typeof step !== 'object' || Array.isArray(step)) {
109
+ errors.push({ path: stepPath, message: 'Step must be an object' });
110
+ continue;
111
+ }
112
+ if (!this.isNonEmptyString(step.id)) {
113
+ errors.push({ path: `${stepPath}.id`, message: 'Step id is required' });
114
+ }
115
+ else if (seenStepIds.has(step.id)) {
116
+ errors.push({ path: `${stepPath}.id`, message: `Duplicate step id "${step.id}"` });
117
+ }
118
+ else {
119
+ seenStepIds.add(step.id);
120
+ }
121
+ if (!this.isNonEmptyString(step.name)) {
122
+ errors.push({ path: `${stepPath}.name`, message: 'Step name is required' });
123
+ }
124
+ if (!['command', 'prompt', 'shell', 'condition', 'loop'].includes(step.action)) {
125
+ errors.push({
126
+ path: `${stepPath}.action`,
127
+ message: `Unsupported step action "${step.action}"`,
128
+ });
129
+ }
130
+ if (!step.params || typeof step.params !== 'object' || Array.isArray(step.params)) {
131
+ errors.push({ path: `${stepPath}.params`, message: 'Step params must be an object' });
132
+ continue;
133
+ }
134
+ switch (step.action) {
135
+ case 'command':
136
+ if (!this.isNonEmptyString(step.params.command)) {
137
+ errors.push({
138
+ path: `${stepPath}.params.command`,
139
+ message: 'command step requires params.command',
140
+ });
141
+ }
142
+ if (step.params.args !== undefined && !Array.isArray(step.params.args)) {
143
+ errors.push({
144
+ path: `${stepPath}.params.args`,
145
+ message: 'command step params.args must be an array',
146
+ });
147
+ }
148
+ break;
149
+ case 'prompt':
150
+ if (!this.isNonEmptyString(step.params.prompt) &&
151
+ !this.isNonEmptyString(step.params.template)) {
152
+ errors.push({
153
+ path: `${stepPath}.params.prompt`,
154
+ message: 'prompt step requires params.prompt or params.template',
155
+ });
156
+ }
157
+ break;
158
+ case 'shell':
159
+ if (!this.isNonEmptyString(step.params.command) &&
160
+ !this.isNonEmptyString(step.params.script)) {
161
+ errors.push({
162
+ path: `${stepPath}.params.command`,
163
+ message: 'shell step requires params.command or params.script',
164
+ });
165
+ }
166
+ break;
167
+ case 'condition':
168
+ if (step.params.if === undefined &&
169
+ step.params.expression === undefined &&
170
+ step.params.value === undefined) {
171
+ errors.push({
172
+ path: `${stepPath}.params.if`,
173
+ message: 'condition step requires params.if, params.expression, or params.value',
174
+ });
175
+ }
176
+ if (step.params.then !== undefined && !this.isStepArray(step.params.then)) {
177
+ errors.push({
178
+ path: `${stepPath}.params.then`,
179
+ message: 'condition step params.then must be a step array',
180
+ });
181
+ }
182
+ if (step.params.else !== undefined && !this.isStepArray(step.params.else)) {
183
+ errors.push({
184
+ path: `${stepPath}.params.else`,
185
+ message: 'condition step params.else must be a step array',
186
+ });
187
+ }
188
+ break;
189
+ case 'loop':
190
+ if (step.params.items === undefined) {
191
+ errors.push({
192
+ path: `${stepPath}.params.items`,
193
+ message: 'loop step requires params.items',
194
+ });
195
+ }
196
+ if (!this.isStepArray(step.params.steps)) {
197
+ errors.push({
198
+ path: `${stepPath}.params.steps`,
199
+ message: 'loop step requires params.steps as an array',
200
+ });
201
+ }
202
+ break;
203
+ }
204
+ }
205
+ for (const [index, trigger] of (def.triggers ?? []).entries()) {
206
+ const triggerPath = `triggers[${index}]`;
207
+ if (!trigger || typeof trigger !== 'object' || Array.isArray(trigger)) {
208
+ errors.push({ path: triggerPath, message: 'Trigger must be an object' });
209
+ continue;
210
+ }
211
+ if (!['manual', 'file-change', 'schedule', 'hook'].includes(trigger.type)) {
212
+ errors.push({
213
+ path: `${triggerPath}.type`,
214
+ message: `Unsupported trigger type "${trigger.type}"`,
215
+ });
216
+ }
217
+ if (!trigger.config || typeof trigger.config !== 'object' || Array.isArray(trigger.config)) {
218
+ errors.push({ path: `${triggerPath}.config`, message: 'Trigger config must be an object' });
219
+ }
220
+ }
221
+ return errors;
222
+ }
223
+ async executeStep(step) {
224
+ switch (step.action) {
225
+ case 'command':
226
+ return this.executeCommandStep(step, this.interpolate(step.params));
227
+ case 'prompt':
228
+ return {
229
+ stepId: step.id,
230
+ success: true,
231
+ output: String(this.interpolate(step.params.prompt ?? step.params.template ?? '')),
232
+ };
233
+ case 'shell':
234
+ return this.executeShellStep(step, this.interpolate(step.params));
235
+ case 'condition':
236
+ return this.executeConditionStep(step, step.params);
237
+ case 'loop':
238
+ return this.executeLoopStep(step, step.params);
239
+ default:
240
+ throw new Error(`Unsupported workflow action: ${step.action}`);
241
+ }
242
+ }
243
+ async executeCommandStep(step, params) {
244
+ const command = String(params.command);
245
+ const args = Array.isArray(params.args) ? params.args.map((arg) => String(arg)) : [];
246
+ const cwd = this.resolveCwd(params.cwd);
247
+ const result = await this.runProcess(command, args, cwd, false);
248
+ return {
249
+ stepId: step.id,
250
+ success: result.exitCode === 0,
251
+ output: result.stdout,
252
+ error: result.exitCode === 0
253
+ ? undefined
254
+ : result.stderr || `Command exited with code ${result.exitCode}`,
255
+ };
256
+ }
257
+ async executeShellStep(step, params) {
258
+ const shellCommand = String(params.command ?? params.script ?? '');
259
+ const cwd = this.resolveCwd(params.cwd);
260
+ const shell = process.platform === 'win32' ? 'powershell.exe' : 'bash';
261
+ const shellArgs = process.platform === 'win32'
262
+ ? ['-NoProfile', '-Command', shellCommand]
263
+ : ['-lc', shellCommand];
264
+ const result = await this.runProcess(shell, shellArgs, cwd, false);
265
+ return {
266
+ stepId: step.id,
267
+ success: result.exitCode === 0,
268
+ output: result.stdout,
269
+ error: result.exitCode === 0
270
+ ? undefined
271
+ : result.stderr || `Shell exited with code ${result.exitCode}`,
272
+ };
273
+ }
274
+ async executeConditionStep(step, params) {
275
+ const rawCondition = this.interpolate(params.if ?? params.expression ?? params.value);
276
+ const passed = this.toBoolean(rawCondition);
277
+ const branchSteps = this.normalizeNestedSteps(passed ? params.then : params.else);
278
+ const branchResults = await this.executeNestedSteps(branchSteps);
279
+ const branchSucceeded = branchResults.every((result) => result.success);
280
+ return {
281
+ stepId: step.id,
282
+ success: branchSucceeded,
283
+ output: {
284
+ passed,
285
+ branch: passed ? 'then' : 'else',
286
+ steps: branchResults,
287
+ },
288
+ };
289
+ }
290
+ async executeLoopStep(step, params) {
291
+ const items = this.normalizeLoopItems(this.interpolate(params.items));
292
+ const nestedSteps = this.normalizeNestedSteps(params.steps);
293
+ const previousLoop = this.execution.loop;
294
+ const loopResults = [];
295
+ let success = true;
296
+ try {
297
+ for (const [index, item] of items.entries()) {
298
+ this.execution.loop = { index, item, items };
299
+ const results = await this.executeNestedSteps(nestedSteps);
300
+ loopResults.push({ index, item, steps: results });
301
+ if (!results.every((result) => result.success)) {
302
+ success = false;
303
+ break;
304
+ }
305
+ }
306
+ }
307
+ finally {
308
+ this.execution.loop = previousLoop;
309
+ }
310
+ return {
311
+ stepId: step.id,
312
+ success,
313
+ output: loopResults,
314
+ };
315
+ }
316
+ async executeNestedSteps(steps) {
317
+ const results = [];
318
+ for (const step of steps) {
319
+ const result = await this.runStep(step);
320
+ results.push(result);
321
+ this.execution.prev = result;
322
+ this.execution.steps[step.id] = result;
323
+ if (!result.success && (step.onFail ?? 'stop') === 'stop') {
324
+ break;
325
+ }
326
+ }
327
+ return results;
328
+ }
329
+ interpolate(value) {
330
+ if (typeof value === 'string') {
331
+ return this.interpolateString(value);
332
+ }
333
+ if (Array.isArray(value)) {
334
+ return value.map((item) => this.interpolate(item));
335
+ }
336
+ if (value && typeof value === 'object') {
337
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, this.interpolate(item)]));
338
+ }
339
+ return value;
340
+ }
341
+ interpolateString(template) {
342
+ const exactMatch = template.match(/^\$\{([^}]+)\}$/);
343
+ if (exactMatch) {
344
+ return this.resolveVariable(exactMatch[1].trim());
345
+ }
346
+ return template.replace(/\$\{([^}]+)\}/g, (_match, expression) => this.renderInterpolatedValue(this.resolveVariable(String(expression).trim())));
347
+ }
348
+ resolveVariable(expression) {
349
+ const normalized = expression.replace(/\[(\d+)\]/g, '.$1');
350
+ const segments = normalized.split('.').filter(Boolean);
351
+ if (segments.length === 0)
352
+ return undefined;
353
+ const [root, ...rest] = segments;
354
+ let current;
355
+ switch (root) {
356
+ case 'prev':
357
+ current = this.execution.prev;
358
+ break;
359
+ case 'context':
360
+ current = this.execution.context;
361
+ break;
362
+ case 'steps':
363
+ current = this.execution.steps;
364
+ break;
365
+ case 'workflow':
366
+ current = this.execution.workflow;
367
+ break;
368
+ case 'loop':
369
+ current = this.execution.loop;
370
+ break;
371
+ default:
372
+ current =
373
+ this.execution.context[root] !== undefined
374
+ ? this.execution.context[root]
375
+ : this.execution.steps[root];
376
+ break;
377
+ }
378
+ for (const segment of rest) {
379
+ if (current == null)
380
+ return undefined;
381
+ current = current[segment];
382
+ }
383
+ return current;
384
+ }
385
+ renderInterpolatedValue(value) {
386
+ if (value == null)
387
+ return '';
388
+ if (typeof value === 'string')
389
+ return value;
390
+ return JSON.stringify(value);
391
+ }
392
+ toBoolean(value) {
393
+ if (typeof value === 'boolean')
394
+ return value;
395
+ if (typeof value === 'number')
396
+ return value !== 0;
397
+ if (typeof value === 'string') {
398
+ const normalized = value.trim().toLowerCase();
399
+ if (normalized === '' ||
400
+ normalized === 'false' ||
401
+ normalized === '0' ||
402
+ normalized === 'no') {
403
+ return false;
404
+ }
405
+ return true;
406
+ }
407
+ if (Array.isArray(value))
408
+ return value.length > 0;
409
+ return Boolean(value);
410
+ }
411
+ normalizeLoopItems(value) {
412
+ if (Array.isArray(value))
413
+ return value;
414
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
415
+ return Array.from({ length: Math.floor(value) }, (_item, index) => index);
416
+ }
417
+ if (value && typeof value === 'object')
418
+ return Object.values(value);
419
+ return [];
420
+ }
421
+ normalizeNestedSteps(value) {
422
+ if (!Array.isArray(value))
423
+ return [];
424
+ return value;
425
+ }
426
+ resolveWorkflowDir(inputDir) {
427
+ const absolute = path.resolve(inputDir);
428
+ const directDir = absolute;
429
+ const nestedDir = path.join(absolute, '.icopilot', 'workflows');
430
+ if (fs.existsSync(directDir) && path.basename(directDir) === 'workflows') {
431
+ return directDir;
432
+ }
433
+ if (fs.existsSync(nestedDir)) {
434
+ return nestedDir;
435
+ }
436
+ return directDir;
437
+ }
438
+ resolveCwd(candidate) {
439
+ const baseDir = this.execution.context.cwd ?? this.cwd;
440
+ if (typeof candidate !== 'string' || candidate.trim() === '') {
441
+ return path.resolve(baseDir);
442
+ }
443
+ return path.resolve(baseDir, candidate);
444
+ }
445
+ normalizeRetryCount(value) {
446
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
447
+ return Math.floor(value);
448
+ }
449
+ return 1;
450
+ }
451
+ async runProcess(command, args, cwd, useShell) {
452
+ return new Promise((resolve, reject) => {
453
+ const child = spawn(command, args, {
454
+ cwd,
455
+ shell: useShell && process.platform === 'win32',
456
+ stdio: ['ignore', 'pipe', 'pipe'],
457
+ });
458
+ let stdout = '';
459
+ let stderr = '';
460
+ child.stdout.on('data', (chunk) => {
461
+ stdout += chunk.toString();
462
+ });
463
+ child.stderr.on('data', (chunk) => {
464
+ stderr += chunk.toString();
465
+ });
466
+ child.on('error', (error) => {
467
+ reject(error);
468
+ });
469
+ child.on('close', (exitCode) => {
470
+ resolve({
471
+ exitCode,
472
+ stdout: stdout.trim(),
473
+ stderr: stderr.trim(),
474
+ });
475
+ });
476
+ });
477
+ }
478
+ createExecutionState(context = {}, workflow) {
479
+ return {
480
+ context: {
481
+ ...(context && typeof context === 'object' ? context : {}),
482
+ cwd: context && typeof context === 'object' && typeof context.cwd === 'string'
483
+ ? context.cwd
484
+ : this.cwd,
485
+ },
486
+ steps: {},
487
+ workflow,
488
+ };
489
+ }
490
+ isNonEmptyString(value) {
491
+ return typeof value === 'string' && value.trim().length > 0;
492
+ }
493
+ isStepArray(value) {
494
+ return Array.isArray(value);
495
+ }
496
+ }