@exaudeus/workrail 0.1.6 → 0.1.7

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/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > **Transform chaotic AI interactions into structured, reliable workflows**
4
4
 
5
5
  [![MCP Compatible](https://img.shields.io/badge/MCP-compatible-purple.svg)](https://modelcontextprotocol.org)
6
- [![Version](https://img.shields.io/badge/version-0.1.0-blue)]()
6
+ [![Version](https://img.shields.io/badge/version-0.2.0-blue)]()
7
7
 
8
8
  ---
9
9
 
@@ -123,6 +123,15 @@ WorkRail supports powerful iteration patterns for complex tasks:
123
123
 
124
124
  Perfect for batch operations, retries, polling, and iterative refinement.
125
125
 
126
+ ### 🚀 v0.2.0: Optimized Loop Execution
127
+
128
+ - **60-80% smaller context** after first iteration
129
+ - **Progressive disclosure** pattern for loop information
130
+ - **Native function DSL** to reduce duplication
131
+ - **Automatic empty loop detection** and skipping
132
+
133
+ See [Loop Optimization Guide](docs/features/loop-optimization.md) for details.
134
+
126
135
  ---
127
136
 
128
137
  ## 📖 Quick Example
@@ -0,0 +1,8 @@
1
+ import { EnhancedContext, OptimizedLoopContext, LoopPhaseReference, LoopStep } from '../../types/workflow-types';
2
+ import { ILoopContextOptimizer } from '../../types/loop-context-optimizer';
3
+ export declare class LoopContextOptimizer implements ILoopContextOptimizer {
4
+ optimizeLoopContext(context: EnhancedContext, iteration: number): OptimizedLoopContext;
5
+ createPhaseReference(loopStep: LoopStep): LoopPhaseReference;
6
+ hasLoopItems(context: EnhancedContext, loopStep: LoopStep): boolean;
7
+ stripLoopMetadata(context: EnhancedContext): OptimizedLoopContext;
8
+ }
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LoopContextOptimizer = void 0;
4
+ class LoopContextOptimizer {
5
+ optimizeLoopContext(context, iteration) {
6
+ if (!context._currentLoop) {
7
+ throw new Error('Cannot optimize context without active loop');
8
+ }
9
+ const { loopId, loopStep } = context._currentLoop;
10
+ const isFirstIteration = iteration === 0;
11
+ const optimizedContext = {
12
+ ...context,
13
+ _currentLoop: {
14
+ loopId,
15
+ loopType: loopStep.loop.type,
16
+ iteration,
17
+ isFirstIteration
18
+ }
19
+ };
20
+ if (!isFirstIteration && optimizedContext._currentLoop) {
21
+ optimizedContext._currentLoop.phaseReference = this.createPhaseReference(loopStep);
22
+ return this.stripLoopMetadata(optimizedContext);
23
+ }
24
+ return optimizedContext;
25
+ }
26
+ createPhaseReference(loopStep) {
27
+ const reference = {
28
+ loopId: loopStep.id,
29
+ phaseTitle: loopStep.title,
30
+ totalSteps: Array.isArray(loopStep.body) ? loopStep.body.length : 1
31
+ };
32
+ if (loopStep.functionDefinitions && loopStep.functionDefinitions.length > 0) {
33
+ reference.functionDefinitions = loopStep.functionDefinitions;
34
+ }
35
+ return reference;
36
+ }
37
+ hasLoopItems(context, loopStep) {
38
+ const { loop } = loopStep;
39
+ switch (loop.type) {
40
+ case 'forEach':
41
+ if (loop.items) {
42
+ const items = context[loop.items];
43
+ return Array.isArray(items) && items.length > 0;
44
+ }
45
+ return false;
46
+ case 'for':
47
+ const count = typeof loop.count === 'number'
48
+ ? loop.count
49
+ : (loop.count ? context[loop.count] : 0);
50
+ return count > 0;
51
+ case 'while':
52
+ case 'until':
53
+ return true;
54
+ default:
55
+ return false;
56
+ }
57
+ }
58
+ stripLoopMetadata(context) {
59
+ let optimizedContext;
60
+ if ('loopType' in (context._currentLoop || {})) {
61
+ optimizedContext = context;
62
+ }
63
+ else if (context._currentLoop) {
64
+ const { loopId, loopStep } = context._currentLoop;
65
+ const iteration = context._loopState?.[loopId]?.iteration || 0;
66
+ optimizedContext = {
67
+ ...context,
68
+ _currentLoop: {
69
+ loopId,
70
+ loopType: loopStep.loop.type,
71
+ iteration,
72
+ isFirstIteration: iteration === 0
73
+ }
74
+ };
75
+ }
76
+ else {
77
+ return context;
78
+ }
79
+ const strippedContext = { ...optimizedContext };
80
+ if (strippedContext._currentLoop?.loopType === 'forEach') {
81
+ const loopState = strippedContext._loopState;
82
+ if (loopState) {
83
+ Object.keys(loopState).forEach(loopId => {
84
+ const state = loopState[loopId];
85
+ if (state.items && Array.isArray(state.items)) {
86
+ const currentIndex = state.index || 0;
87
+ if (currentIndex < state.items.length) {
88
+ state.items = [state.items[currentIndex]];
89
+ state.index = 0;
90
+ }
91
+ }
92
+ });
93
+ }
94
+ }
95
+ const keysToCheck = Object.keys(strippedContext);
96
+ keysToCheck.forEach(key => {
97
+ if (key.startsWith('_'))
98
+ return;
99
+ const value = strippedContext[key];
100
+ if (Array.isArray(value) && value.length > 10) {
101
+ const currentLoop = strippedContext._currentLoop;
102
+ if (currentLoop?.loopType === 'forEach') {
103
+ const loopState = strippedContext._loopState?.[currentLoop.loopId];
104
+ if (loopState?.items === value) {
105
+ return;
106
+ }
107
+ }
108
+ delete strippedContext[key];
109
+ }
110
+ });
111
+ return strippedContext;
112
+ }
113
+ }
114
+ exports.LoopContextOptimizer = LoopContextOptimizer;
@@ -1,4 +1,4 @@
1
- import { LoopConfig, LoopState, EnhancedContext } from '../../types/workflow-types';
1
+ import { LoopConfig, LoopState, EnhancedContext, OptimizedLoopContext, LoopPhaseReference, LoopStep } from '../../types/workflow-types';
2
2
  import { ConditionContext } from '../../utils/condition-evaluator';
3
3
  export declare class LoopExecutionContext {
4
4
  private loopId;
@@ -10,9 +10,14 @@ export declare class LoopExecutionContext {
10
10
  getCurrentState(): LoopState[string];
11
11
  shouldContinue(context: ConditionContext): boolean;
12
12
  initializeForEach(context: ConditionContext): void;
13
- injectVariables(context: ConditionContext): EnhancedContext;
14
13
  private resolveCount;
15
14
  private addWarning;
16
15
  getLoopId(): string;
17
16
  getLoopConfig(): LoopConfig;
17
+ getMinimalContext(context: ConditionContext): OptimizedLoopContext;
18
+ getPhaseReference(loopStep: LoopStep): LoopPhaseReference;
19
+ injectVariables(context: ConditionContext, minimal?: boolean): EnhancedContext | OptimizedLoopContext;
20
+ private injectVariablesFull;
21
+ isFirstIteration(): boolean;
22
+ isEmpty(context: ConditionContext): boolean;
18
23
  }
@@ -68,7 +68,72 @@ class LoopExecutionContext {
68
68
  }
69
69
  }
70
70
  }
71
- injectVariables(context) {
71
+ resolveCount(context) {
72
+ if (this.loopConfig.type !== 'for' || !this.loopConfig.count) {
73
+ return 0;
74
+ }
75
+ if (typeof this.loopConfig.count === 'number') {
76
+ return this.loopConfig.count;
77
+ }
78
+ const count = context[this.loopConfig.count];
79
+ if (typeof count === 'number') {
80
+ return count;
81
+ }
82
+ this.addWarning(`Invalid count value for 'for' loop: ${this.loopConfig.count}`);
83
+ return 0;
84
+ }
85
+ addWarning(message) {
86
+ if (!this.state.warnings) {
87
+ this.state.warnings = [];
88
+ }
89
+ this.state.warnings.push(message);
90
+ }
91
+ getLoopId() {
92
+ return this.loopId;
93
+ }
94
+ getLoopConfig() {
95
+ return { ...this.loopConfig };
96
+ }
97
+ getMinimalContext(context) {
98
+ const optimizedContext = {
99
+ ...context,
100
+ _loopState: {
101
+ ...context._loopState,
102
+ [this.loopId]: this.getCurrentState()
103
+ },
104
+ _currentLoop: {
105
+ loopId: this.loopId,
106
+ loopType: this.loopConfig.type,
107
+ iteration: this.state.iteration,
108
+ isFirstIteration: false
109
+ }
110
+ };
111
+ if (this.loopConfig.type === 'forEach' && this.state.items) {
112
+ const index = this.state.index || 0;
113
+ const itemVar = this.loopConfig.itemVar || 'currentItem';
114
+ const indexVar = this.loopConfig.indexVar || 'currentIndex';
115
+ optimizedContext[itemVar] = this.state.items[index];
116
+ optimizedContext[indexVar] = index;
117
+ }
118
+ const iterationVar = this.loopConfig.iterationVar || 'currentIteration';
119
+ optimizedContext[iterationVar] = this.state.iteration + 1;
120
+ return optimizedContext;
121
+ }
122
+ getPhaseReference(loopStep) {
123
+ return {
124
+ loopId: this.loopId,
125
+ phaseTitle: loopStep.title,
126
+ totalSteps: Array.isArray(loopStep.body) ? loopStep.body.length : 1,
127
+ functionDefinitions: loopStep.functionDefinitions
128
+ };
129
+ }
130
+ injectVariables(context, minimal = false) {
131
+ if (minimal) {
132
+ return this.getMinimalContext(context);
133
+ }
134
+ return this.injectVariablesFull(context);
135
+ }
136
+ injectVariablesFull(context) {
72
137
  const enhancements = {
73
138
  _loopState: {
74
139
  ...context._loopState,
@@ -97,31 +162,27 @@ class LoopExecutionContext {
97
162
  }
98
163
  return { ...context, ...enhancements };
99
164
  }
100
- resolveCount(context) {
101
- if (this.loopConfig.type !== 'for' || !this.loopConfig.count) {
102
- return 0;
103
- }
104
- if (typeof this.loopConfig.count === 'number') {
105
- return this.loopConfig.count;
106
- }
107
- const count = context[this.loopConfig.count];
108
- if (typeof count === 'number') {
109
- return count;
110
- }
111
- this.addWarning(`Invalid count value for 'for' loop: ${this.loopConfig.count}`);
112
- return 0;
165
+ isFirstIteration() {
166
+ return this.state.iteration === 0;
113
167
  }
114
- addWarning(message) {
115
- if (!this.state.warnings) {
116
- this.state.warnings = [];
168
+ isEmpty(context) {
169
+ switch (this.loopConfig.type) {
170
+ case 'forEach':
171
+ return !this.state.items || this.state.items.length === 0;
172
+ case 'for':
173
+ const count = this.resolveCount(context);
174
+ return count <= 0;
175
+ case 'while':
176
+ return this.loopConfig.condition
177
+ ? !(0, condition_evaluator_1.evaluateCondition)(this.loopConfig.condition, context)
178
+ : true;
179
+ case 'until':
180
+ return this.loopConfig.condition
181
+ ? (0, condition_evaluator_1.evaluateCondition)(this.loopConfig.condition, context)
182
+ : true;
183
+ default:
184
+ return false;
117
185
  }
118
- this.state.warnings.push(message);
119
- }
120
- getLoopId() {
121
- return this.loopId;
122
- }
123
- getLoopConfig() {
124
- return { ...this.loopConfig };
125
186
  }
126
187
  }
127
188
  exports.LoopExecutionContext = LoopExecutionContext;
@@ -18,11 +18,13 @@ import { IWorkflowStorage } from '../../types/storage';
18
18
  import { ConditionContext } from '../../utils/condition-evaluator';
19
19
  import { ValidationEngine } from './validation-engine';
20
20
  import { EnhancedContext } from '../../types/workflow-types';
21
+ import { ILoopContextOptimizer } from '../../types/loop-context-optimizer';
21
22
  export declare class DefaultWorkflowService implements WorkflowService {
22
23
  private readonly storage;
23
24
  private readonly validationEngine;
25
+ private readonly loopContextOptimizer?;
24
26
  private loopStepResolver;
25
- constructor(storage?: IWorkflowStorage, validationEngine?: ValidationEngine);
27
+ constructor(storage?: IWorkflowStorage, validationEngine?: ValidationEngine, loopContextOptimizer?: ILoopContextOptimizer | undefined);
26
28
  listWorkflowSummaries(): Promise<WorkflowSummary[]>;
27
29
  getWorkflowById(id: string): Promise<Workflow | null>;
28
30
  getNextStep(workflowId: string, completedSteps: string[], context?: ConditionContext): Promise<{
@@ -11,9 +11,10 @@ const loop_step_resolver_1 = require("./loop-step-resolver");
11
11
  const context_size_1 = require("../../utils/context-size");
12
12
  const context_optimizer_1 = require("./context-optimizer");
13
13
  class DefaultWorkflowService {
14
- constructor(storage = (0, storage_1.createDefaultWorkflowStorage)(), validationEngine = new validation_engine_1.ValidationEngine()) {
14
+ constructor(storage = (0, storage_1.createDefaultWorkflowStorage)(), validationEngine = new validation_engine_1.ValidationEngine(), loopContextOptimizer) {
15
15
  this.storage = storage;
16
16
  this.validationEngine = validationEngine;
17
+ this.loopContextOptimizer = loopContextOptimizer;
17
18
  this.loopStepResolver = new loop_step_resolver_1.LoopStepResolver();
18
19
  }
19
20
  async listWorkflowSummaries() {
@@ -56,15 +57,26 @@ class DefaultWorkflowService {
56
57
  if (loopContext.shouldContinue(context)) {
57
58
  const bodyStep = this.loopStepResolver.resolveLoopBody(workflow, loopStep.body, loopStep.id);
58
59
  if (!Array.isArray(bodyStep)) {
59
- const loopEnhancedContext = loopContext.injectVariables(context);
60
- const loopSizeCheck = (0, context_size_1.checkContextSize)(loopEnhancedContext);
60
+ const isFirst = loopContext.isFirstIteration();
61
+ if (isFirst && loopContext.isEmpty(context)) {
62
+ const skipContext = context_optimizer_1.ContextOptimizer.createEnhancedContext(context, completed);
63
+ delete skipContext._currentLoop;
64
+ const nextStep = await this.getNextStep(workflow.id, completed, skipContext);
65
+ return nextStep;
66
+ }
67
+ const useMinimal = !isFirst && !!this.loopContextOptimizer;
68
+ const loopEnhancedContext = loopContext.injectVariables(context, useMinimal);
69
+ const optimizedContext = useMinimal && this.loopContextOptimizer
70
+ ? this.loopContextOptimizer.stripLoopMetadata(loopEnhancedContext)
71
+ : loopEnhancedContext;
72
+ const loopSizeCheck = (0, context_size_1.checkContextSize)(optimizedContext);
61
73
  if (loopSizeCheck.isError) {
62
74
  throw new Error(`Context size (${Math.round(loopSizeCheck.sizeBytes / 1024)}KB) exceeds maximum allowed size (256KB) during loop execution`);
63
75
  }
64
76
  return {
65
77
  step: bodyStep,
66
78
  guidance: {
67
- prompt: this.buildStepPrompt(bodyStep, loopContext)
79
+ prompt: this.buildStepPrompt(bodyStep, loopContext, useMinimal)
68
80
  },
69
81
  isComplete: false,
70
82
  context: loopSizeCheck.context
@@ -81,15 +93,26 @@ class DefaultWorkflowService {
81
93
  return true;
82
94
  });
83
95
  if (uncompletedBodyStep) {
84
- const loopEnhancedContext = loopContext.injectVariables(context);
85
- const loopSizeCheck = (0, context_size_1.checkContextSize)(loopEnhancedContext);
96
+ const isFirst = loopContext.isFirstIteration();
97
+ if (isFirst && loopContext.isEmpty(context)) {
98
+ const skipContext = context_optimizer_1.ContextOptimizer.createEnhancedContext(context, completed);
99
+ delete skipContext._currentLoop;
100
+ const nextStep = await this.getNextStep(workflow.id, completed, skipContext);
101
+ return nextStep;
102
+ }
103
+ const useMinimal = !isFirst && !!this.loopContextOptimizer;
104
+ const loopEnhancedContext = loopContext.injectVariables(context, useMinimal);
105
+ const optimizedContext = useMinimal && this.loopContextOptimizer
106
+ ? this.loopContextOptimizer.stripLoopMetadata(loopEnhancedContext)
107
+ : loopEnhancedContext;
108
+ const loopSizeCheck = (0, context_size_1.checkContextSize)(optimizedContext);
86
109
  if (loopSizeCheck.isError) {
87
110
  throw new Error(`Context size (${Math.round(loopSizeCheck.sizeBytes / 1024)}KB) exceeds maximum allowed size (256KB) during loop execution`);
88
111
  }
89
112
  return {
90
113
  step: uncompletedBodyStep,
91
114
  guidance: {
92
- prompt: this.buildStepPrompt(uncompletedBodyStep, loopContext)
115
+ prompt: this.buildStepPrompt(uncompletedBodyStep, loopContext, useMinimal)
93
116
  },
94
117
  isComplete: false,
95
118
  context: loopSizeCheck.context
@@ -180,7 +203,7 @@ class DefaultWorkflowService {
180
203
  context: enhancedContext
181
204
  };
182
205
  }
183
- buildStepPrompt(step, loopContext) {
206
+ buildStepPrompt(step, loopContext, useMinimal = false) {
184
207
  let stepGuidance = '';
185
208
  if (step.guidance && step.guidance.length > 0) {
186
209
  const guidanceHeader = '## Step Guidance';
@@ -193,10 +216,18 @@ class DefaultWorkflowService {
193
216
  }
194
217
  if (loopContext) {
195
218
  const state = loopContext.getCurrentState();
196
- finalPrompt += `\n\n## Loop Context\n- Iteration: ${state.iteration + 1}`;
197
- if (state.items) {
198
- finalPrompt += `\n- Total Items: ${state.items.length}`;
199
- finalPrompt += `\n- Current Index: ${state.index}`;
219
+ if (useMinimal && this.loopContextOptimizer) {
220
+ finalPrompt += `\n\n## Loop Context\n- Iteration: ${state.iteration + 1}`;
221
+ if (!loopContext.isFirstIteration()) {
222
+ finalPrompt += '\n\n_Note: Refer to the phase overview provided in the first iteration for overall context._';
223
+ }
224
+ }
225
+ else {
226
+ finalPrompt += `\n\n## Loop Context\n- Iteration: ${state.iteration + 1}`;
227
+ if (state.items) {
228
+ finalPrompt += `\n- Total Items: ${state.items.length}`;
229
+ finalPrompt += `\n- Current Index: ${state.index}`;
230
+ }
200
231
  }
201
232
  }
202
233
  return finalPrompt;
@@ -2,9 +2,11 @@ import { WorkflowService } from './application/services/workflow-service.js';
2
2
  import { ValidationEngine } from './application/services/validation-engine.js';
3
3
  import { IWorkflowStorage } from './types/storage.js';
4
4
  import { WorkflowLookupServer } from './types/server.js';
5
+ import { ILoopContextOptimizer } from './types/loop-context-optimizer.js';
5
6
  export interface AppContainer {
6
7
  storage: IWorkflowStorage;
7
8
  validationEngine: ValidationEngine;
9
+ loopContextOptimizer: ILoopContextOptimizer;
8
10
  workflowService: WorkflowService;
9
11
  server: WorkflowLookupServer;
10
12
  }
package/dist/container.js CHANGED
@@ -5,14 +5,17 @@ const storage_js_1 = require("./infrastructure/storage/storage.js");
5
5
  const workflow_service_js_1 = require("./application/services/workflow-service.js");
6
6
  const validation_engine_js_1 = require("./application/services/validation-engine.js");
7
7
  const server_js_1 = require("./infrastructure/rpc/server.js");
8
+ const loop_context_optimizer_js_1 = require("./application/services/loop-context-optimizer.js");
8
9
  function createAppContainer(overrides = {}) {
9
10
  const storage = overrides.storage ?? (0, storage_js_1.createDefaultWorkflowStorage)();
10
11
  const validationEngine = overrides.validationEngine ?? new validation_engine_js_1.ValidationEngine();
11
- const workflowService = overrides.workflowService ?? new workflow_service_js_1.DefaultWorkflowService(storage, validationEngine);
12
+ const loopContextOptimizer = overrides.loopContextOptimizer ?? new loop_context_optimizer_js_1.LoopContextOptimizer();
13
+ const workflowService = overrides.workflowService ?? new workflow_service_js_1.DefaultWorkflowService(storage, validationEngine, loopContextOptimizer);
12
14
  const server = overrides.server ?? (0, server_js_1.createWorkflowLookupServer)(workflowService);
13
15
  return {
14
16
  storage,
15
17
  validationEngine,
18
+ loopContextOptimizer,
16
19
  workflowService,
17
20
  server
18
21
  };
@@ -0,0 +1,7 @@
1
+ import { EnhancedContext, OptimizedLoopContext, LoopPhaseReference, LoopStep } from './workflow-types';
2
+ export interface ILoopContextOptimizer {
3
+ optimizeLoopContext(context: EnhancedContext, iteration: number): OptimizedLoopContext;
4
+ createPhaseReference(loopStep: LoopStep): LoopPhaseReference;
5
+ hasLoopItems(context: EnhancedContext, loopStep: LoopStep): boolean;
6
+ stripLoopMetadata(context: EnhancedContext): OptimizedLoopContext;
7
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -8,6 +8,7 @@ export interface Workflow {
8
8
  clarificationPrompts?: string[];
9
9
  steps: (WorkflowStep | LoopStep)[];
10
10
  metaGuidance?: string[];
11
+ functionDefinitions?: FunctionDefinition[];
11
12
  }
12
13
  export interface WorkflowStep {
13
14
  id: string;
@@ -18,6 +19,8 @@ export interface WorkflowStep {
18
19
  askForFiles?: boolean;
19
20
  requireConfirmation?: boolean;
20
21
  runCondition?: object;
22
+ functionDefinitions?: FunctionDefinition[];
23
+ functionReferences?: string[];
21
24
  }
22
25
  export interface LoopStep extends WorkflowStep {
23
26
  type: 'loop';
@@ -56,7 +59,36 @@ export interface EnhancedContext extends ConditionContext {
56
59
  loopStep: LoopStep;
57
60
  };
58
61
  }
62
+ export interface OptimizedLoopContext extends ConditionContext {
63
+ _loopState?: LoopState;
64
+ _warnings?: {
65
+ loops?: {
66
+ [loopId: string]: string[];
67
+ };
68
+ };
69
+ _contextSize?: number;
70
+ _currentLoop?: {
71
+ loopId: string;
72
+ loopType: 'for' | 'forEach' | 'while' | 'until';
73
+ iteration: number;
74
+ isFirstIteration: boolean;
75
+ phaseReference?: LoopPhaseReference;
76
+ };
77
+ _loopPhaseReference?: LoopPhaseReference;
78
+ }
79
+ export interface LoopPhaseReference {
80
+ loopId: string;
81
+ phaseTitle: string;
82
+ totalSteps: number;
83
+ functionDefinitions?: FunctionDefinition[];
84
+ }
85
+ export interface FunctionDefinition {
86
+ name: string;
87
+ definition: string;
88
+ scope?: 'workflow' | 'loop' | 'step';
89
+ }
59
90
  export declare function isLoopStep(step: WorkflowStep | LoopStep): step is LoopStep;
91
+ export declare function isFirstLoopIteration(context: EnhancedContext | OptimizedLoopContext): boolean;
60
92
  export interface WorkflowValidationResult {
61
93
  valid: boolean;
62
94
  errors: WorkflowValidationError[];
@@ -1,6 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.isLoopStep = isLoopStep;
4
+ exports.isFirstLoopIteration = isFirstLoopIteration;
4
5
  function isLoopStep(step) {
5
6
  return 'type' in step && step.type === 'loop';
6
7
  }
8
+ function isFirstLoopIteration(context) {
9
+ if ('isFirstIteration' in (context._currentLoop || {})) {
10
+ return context._currentLoop?.isFirstIteration === true;
11
+ }
12
+ const loopState = context._loopState;
13
+ if (loopState) {
14
+ const currentLoopId = context._currentLoop?.loopId;
15
+ if (currentLoopId && loopState[currentLoopId]) {
16
+ return loopState[currentLoopId].iteration === 0;
17
+ }
18
+ }
19
+ return false;
20
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exaudeus/workrail",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "MCP server for structured workflow orchestration and step-by-step task guidance",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -13,7 +13,7 @@
13
13
  ],
14
14
  "scripts": {
15
15
  "build": "tsc -p tsconfig.build.json",
16
- "release": "./scripts/release.sh",
16
+ "release": "bash -c './scripts/release.sh'",
17
17
  "dev": "npm run build && node dist/mcp-server.js",
18
18
  "prepare": "npm run build",
19
19
  "watch": "tsc --watch"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "https://workflowlookup.io/schemas/workflow/v0.1.0",
3
+ "$id": "https://workflowlookup.io/schemas/workflow/v0.2.0",
4
4
  "title": "Workflow Schema",
5
5
  "description": "Schema for defining workflows in the Workflow Orchestration System",
6
6
  "type": "object",
@@ -74,6 +74,32 @@
74
74
  "maxLength": 256
75
75
  },
76
76
  "uniqueItems": true
77
+ },
78
+ "functionDefinitions": {
79
+ "type": "array",
80
+ "description": "Function definitions for reducing duplication. Agents can reference these definitions to understand repeated patterns without full expansion.",
81
+ "items": {
82
+ "type": "object",
83
+ "properties": {
84
+ "name": {
85
+ "type": "string",
86
+ "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$",
87
+ "description": "Function name following JavaScript identifier rules"
88
+ },
89
+ "definition": {
90
+ "type": "string",
91
+ "description": "The function definition explaining what it does"
92
+ },
93
+ "scope": {
94
+ "type": "string",
95
+ "enum": ["workflow", "loop", "step"],
96
+ "description": "Scope where this function is available"
97
+ }
98
+ },
99
+ "required": ["name", "definition"],
100
+ "additionalProperties": false
101
+ },
102
+ "uniqueItems": true
77
103
  }
78
104
  },
79
105
  "required": [
@@ -91,6 +117,27 @@
91
117
  "minLength": 3,
92
118
  "maxLength": 64
93
119
  },
120
+ "functionDefinition": {
121
+ "type": "object",
122
+ "properties": {
123
+ "name": {
124
+ "type": "string",
125
+ "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$",
126
+ "description": "Function name following JavaScript identifier rules"
127
+ },
128
+ "definition": {
129
+ "type": "string",
130
+ "description": "The function definition explaining what it does"
131
+ },
132
+ "scope": {
133
+ "type": "string",
134
+ "enum": ["workflow", "loop", "step"],
135
+ "description": "Scope where this function is available"
136
+ }
137
+ },
138
+ "required": ["name", "definition"],
139
+ "additionalProperties": false
140
+ },
94
141
  "standardStep": {
95
142
  "type": "object",
96
143
  "properties": {
@@ -153,6 +200,21 @@
153
200
  "$ref": "#/$defs/validationComposition"
154
201
  }
155
202
  ]
203
+ },
204
+ "functionDefinitions": {
205
+ "type": "array",
206
+ "description": "Function definitions specific to this step. Agents can reference these for understanding step-specific patterns.",
207
+ "items": {
208
+ "$ref": "#/$defs/functionDefinition"
209
+ }
210
+ },
211
+ "functionReferences": {
212
+ "type": "array",
213
+ "description": "References to functions defined at workflow or loop level that this step uses.",
214
+ "items": {
215
+ "type": "string",
216
+ "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*\\(\\)$"
217
+ }
156
218
  }
157
219
  },
158
220
  "required": [
@@ -198,6 +260,13 @@
198
260
  }
199
261
  ]
200
262
  },
263
+ "functionDefinitions": {
264
+ "type": "array",
265
+ "description": "Function definitions specific to this loop. Available to all steps within the loop body.",
266
+ "items": {
267
+ "$ref": "#/$defs/functionDefinition"
268
+ }
269
+ },
201
270
  "requireConfirmation": {
202
271
  "$ref": "#/$defs/confirmationRule"
203
272
  },
@@ -404,15 +404,14 @@
404
404
  "requireConfirmation": false
405
405
  },
406
406
  {
407
- "id": "phase-6-prepare-steps",
408
- "title": "Phase 6 Preparation: Extract Implementation Steps",
409
- "prompt": "Before starting the implementation loop, extract the discrete implementation steps from the approved `implementation_plan.md` into a context array.\n\n**Your task:**\n1. Read the implementation_plan.md\n2. Identify each discrete, actionable step from the Implementation Strategy section\n3. Create an array called `implementationSteps` where each item has:\n - `title`: Brief title of the step\n - `description`: Detailed description of what to do\n - `outputs`: Expected outputs or changes\n\n**Example format:**\n```json\n[\n {\n \"title\": \"Create user authentication module\",\n \"description\": \"Implement the authentication service with login/logout methods\",\n \"outputs\": \"auth.service.ts file with login() and logout() methods\"\n },\n {\n \"title\": \"Add validation middleware\",\n \"description\": \"Create middleware to validate user credentials\",\n \"outputs\": \"validation.middleware.ts with credential validation logic\"\n }\n]\n```\n\n**Set this array as the `implementationSteps` context variable for the loop to consume.**",
410
- "agentRole": "You are preparing the implementation phase by extracting actionable steps from the plan into a structured format for iteration.",
407
+ "id": "phase-6-count-steps",
408
+ "title": "Phase 6 Preparation: Count Implementation Steps",
409
+ "prompt": "Before starting the implementation loop, count the discrete implementation steps in the approved `implementation_plan.md`.\n\n**Your task:**\n1. Read the implementation_plan.md\n2. Count each discrete, actionable step from the Implementation Strategy section\n3. Set `totalImplementationSteps` to the count (e.g., 8)\n\n**What counts as a step:**\n- Each bullet point or numbered item that represents a concrete implementation task\n- Each item that would result in a code change or file creation\n- Each item that could be a logical git commit\n\n**Example:**\nIf the plan has:\n- Create user model\n- Add authentication service\n- Implement validation\n- Write tests\n\nThen set `totalImplementationSteps` = 4\n\n**IMPORTANT:** Only count, don't extract or store the steps. You'll read them directly from the plan during implementation.",
410
+ "agentRole": "You are preparing the implementation phase by counting the steps to enable progress tracking.",
411
411
  "guidance": [
412
- "Each step should be a logical, committable chunk of work",
413
- "Steps should be ordered as they appear in the plan",
414
- "Include enough detail for clear implementation",
415
- "This enables the loop to iterate over a concrete list"
412
+ "Count only actionable implementation steps",
413
+ "Don't include meta-tasks like 'review' or 'document'",
414
+ "This enables progress tracking without bloating context"
416
415
  ],
417
416
  "runCondition": {"var": "taskComplexity", "not_equals": "Small"},
418
417
  "requireConfirmation": false
@@ -423,22 +422,20 @@
423
422
  "title": "Phase 6: Iterative Implementation Loop (PREP -> IMPLEMENT -> VERIFY)",
424
423
  "runCondition": {"var": "taskComplexity", "not_equals": "Small"},
425
424
  "loop": {
426
- "type": "forEach",
427
- "items": "implementationSteps",
428
- "itemVar": "currentStep",
429
- "indexVar": "stepIndex",
430
- "maxIterations": 25,
431
- "iterationVar": "stepIteration"
425
+ "type": "for",
426
+ "count": "totalImplementationSteps",
427
+ "indexVar": "currentStepNumber",
428
+ "maxIterations": 25
432
429
  },
433
430
  "body": [
434
431
  {
435
432
  "id": "phase-6-prep",
436
- "title": "PREP: Prepare for {{currentStep.title}}",
437
- "prompt": "**PREPARATION PHASE for Step {{stepIndex + 1}}/{{implementationSteps.length}}: {{currentStep.title}}**\n\nBefore implementing this step, you must first PREPARE:\n\n1. **Review the step description**: {{currentStep.description}}\n2. **Confirm prerequisites**: Verify the previous step (if any) was completed correctly\n3. **Validate current state**: Ensure the plan for this step is still valid in the current codebase\n4. **List requirements**: Identify all required inputs, files, or dependencies\n\n**BRANCH SETUP (first iteration only):** If this is the first implementation step ({{stepIndex}} === 0):\n- Check git availability with 'git status'\n- If available, create feature branch: 'git checkout -b wip-[unique-task-id]'\n- Track the featureBranch variable for later use\n- If git unavailable: Skip branching, log in CONTEXT.md\n\n**Do not proceed if anything is unclear or missing.**",
433
+ "title": "PREP: Prepare for Step {{currentStepNumber + 1}}",
434
+ "prompt": "**PREPARATION PHASE for Step {{currentStepNumber + 1}}/{{totalImplementationSteps}}**\n\nBefore implementing this step, you must first PREPARE:\n\n1. **Read implementation_plan.md** and locate step #{{currentStepNumber + 1}}\n2. **Extract step details**: Note the title, description, and expected outputs\n3. **Confirm prerequisites**: Verify the previous step (if any) was completed correctly\n4. **Validate current state**: Ensure the plan for this step is still valid in the current codebase\n5. **List requirements**: Identify all required inputs, files, or dependencies\n\n**BRANCH SETUP (first iteration only):** If this is the first implementation step ({{currentStepNumber}} === 0):\n- Check git availability with 'git status'\n- If available, create feature branch: 'git checkout -b wip-[unique-task-id]'\n- Track the featureBranch variable for later use\n- If git unavailable: Skip branching, log in CONTEXT.md\n\n**CRITICAL CONTEXT OPTIMIZATION:**\nWhen calling workflow_next after this step, send ONLY:\n- currentStepNumber and totalImplementationSteps\n- Any NEW variables you created (like featureBranch)\n- DO NOT send: arrays, plans, _loopState, _currentLoop, unchanged data\n\n**Do not proceed if anything is unclear or missing.**",
438
435
  "agentRole": "You are preparing to implement a specific step from the plan. Be meticulous in verifying all prerequisites are met before proceeding.",
439
436
  "guidance": [
440
- "This is step {{stepIndex + 1}} of {{implementationSteps.length}} total implementation steps",
441
- "Current iteration: {{stepIteration}} (for tracking purposes)",
437
+ "This is step {{currentStepNumber + 1}} of {{totalImplementationSteps}} total implementation steps",
438
+ "Read the step directly from implementation_plan.md",
442
439
  "Focus only on preparation - implementation comes next",
443
440
  "If this is the first step, handle git branch setup"
444
441
  ],
@@ -446,8 +443,8 @@
446
443
  },
447
444
  {
448
445
  "id": "phase-6-implement",
449
- "title": "IMPLEMENT: Execute {{currentStep.title}}",
450
- "prompt": "**IMPLEMENTATION PHASE for {{currentStep.title}}**\n\nNow implement this specific step:\n\n**Step Details:**\n- Title: {{currentStep.title}}\n- Description: {{currentStep.description}}\n- Expected Outputs: {{currentStep.outputs}}\n\n**Remember:** applyUserRules() and matchPatterns() throughout.\n\n**Instructions:**\n1. Focus only on this single step\n2. useTools() to make code changes\n3. Follow quality standards\n4. Adapt to unexpected discoveries\n5. createFile() for ALL code changes\n\n**Progress Tracking:**\n- This is step {{stepIndex + 1}} of {{implementationSteps.length}}\n- Total steps executed so far: {{stepIteration}}\n- If total steps > 20, pause for user intervention\n\n**CONTEXT UPDATES:** If this is every 3rd step ({{stepIteration}} % 3 === 0):\n- Update CONTEXT.md\n- addResumptionJson(phase-6-implement)\n- updateDecisionLog()\n- List files modified with line ranges",
446
+ "title": "IMPLEMENT: Execute Step {{currentStepNumber + 1}}",
447
+ "prompt": "**IMPLEMENTATION PHASE for Step {{currentStepNumber + 1}}/{{totalImplementationSteps}}**\n\nNow implement the step you just prepared for:\n\n**Instructions:**\n1. Re-read step #{{currentStepNumber + 1}} from implementation_plan.md\n2. Focus only on this single step\n3. useTools() to make code changes\n4. Follow quality standards\n5. Adapt to unexpected discoveries\n6. createFile() for ALL code changes\n\n**Remember:** applyUserRules() and matchPatterns() throughout.\n\n**Progress Tracking:**\n- This is step {{currentStepNumber + 1}} of {{totalImplementationSteps}}\n- If we've done > 20 steps total, pause for user intervention\n\n**CONTEXT UPDATES:** If this is every 3rd step ({{currentStepNumber + 1}} % 3 === 0):\n- Update CONTEXT.md\n- addResumptionJson(phase-6-implement)\n- updateDecisionLog()\n- List files modified with line ranges\n\n**CONTEXT OPTIMIZATION for workflow_next:**\nSend ONLY: currentStepNumber, totalImplementationSteps, filesCreated (new), any other NEW data\nDO NOT send: arrays, plans, _loopState, unchanged fields",
451
448
  "agentRole": "You are implementing a specific step from the approved plan. Focus on precise execution while maintaining code quality.",
452
449
  "guidance": [
453
450
  "Implement only what this step requires",
@@ -458,8 +455,8 @@
458
455
  },
459
456
  {
460
457
  "id": "phase-6-verify",
461
- "title": "VERIFY: Validate {{currentStep.title}}",
462
- "prompt": "**VERIFICATION PHASE for {{currentStep.title}}**\n\nVerify the implementation is complete and correct:\n\n**Required:** verifyImplementation()\n\n**COMMIT Decision (if all passes):**\n- checkAutomation(commit)\n- gitCommit(type, scope: description)\n- If git unavailable: Log in CONTEXT.md\n\n**FAILURE PROTOCOL:** If verification fails after 2 attempts:\n1. Do not try a third time\n2. Fall back to alternative tools\n3. updateDecisionLog() with failure details\n4. Present summary and recommendations\n5. Set 'verificationFailed' context variable to true",
458
+ "title": "VERIFY: Validate Step {{currentStepNumber + 1}}",
459
+ "prompt": "**VERIFICATION PHASE for Step {{currentStepNumber + 1}}/{{totalImplementationSteps}}**\n\nVerify the implementation is complete and correct:\n\n**Required:** verifyImplementation()\n\n**COMMIT Decision (if all passes):**\n- checkAutomation(commit)\n- gitCommit(type, scope: description)\n- If git unavailable: Log in CONTEXT.md\n\n**FAILURE PROTOCOL:** If verification fails after 2 attempts:\n1. Do not try a third time\n2. Fall back to alternative tools\n3. updateDecisionLog() with failure details\n4. Present summary and recommendations\n5. Set 'verificationFailed' context variable to true\n\n**CONTEXT OPTIMIZATION:**\nFor workflow_next, send ONLY what changed: currentStepNumber, verificationResult, verificationFailed (if true)\nNEVER send back the full context!",
463
460
  "agentRole": "You are verifying the implementation meets all quality standards. Be thorough but respect failure bounds.",
464
461
  "guidance": [
465
462
  "All three verification steps must pass",