@exaudeus/workrail 3.4.0 → 3.6.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 (49) hide show
  1. package/dist/application/services/validation-engine.js +50 -0
  2. package/dist/config/feature-flags.js +8 -0
  3. package/dist/engine/engine-factory.js +4 -2
  4. package/dist/manifest.json +100 -52
  5. package/dist/mcp/handler-factory.js +21 -4
  6. package/dist/mcp/handlers/v2-execution/continue-rehydrate.d.ts +6 -1
  7. package/dist/mcp/handlers/v2-execution/continue-rehydrate.js +22 -4
  8. package/dist/mcp/handlers/v2-execution/index.d.ts +6 -1
  9. package/dist/mcp/handlers/v2-execution/index.js +13 -3
  10. package/dist/mcp/handlers/v2-execution/start.d.ts +9 -1
  11. package/dist/mcp/handlers/v2-execution/start.js +74 -36
  12. package/dist/mcp/handlers/v2-execution-helpers.d.ts +2 -0
  13. package/dist/mcp/handlers/v2-execution-helpers.js +2 -0
  14. package/dist/mcp/handlers/v2-reference-resolver.d.ts +14 -0
  15. package/dist/mcp/handlers/v2-reference-resolver.js +112 -0
  16. package/dist/mcp/handlers/v2-resolve-refs-envelope.d.ts +5 -0
  17. package/dist/mcp/handlers/v2-resolve-refs-envelope.js +17 -0
  18. package/dist/mcp/handlers/v2-workflow.js +2 -0
  19. package/dist/mcp/output-schemas.d.ts +38 -0
  20. package/dist/mcp/output-schemas.js +8 -0
  21. package/dist/mcp/render-envelope.d.ts +21 -0
  22. package/dist/mcp/render-envelope.js +59 -0
  23. package/dist/mcp/response-supplements.d.ts +17 -0
  24. package/dist/mcp/response-supplements.js +58 -0
  25. package/dist/mcp/step-content-envelope.d.ts +32 -0
  26. package/dist/mcp/step-content-envelope.js +13 -0
  27. package/dist/mcp/v2-response-formatter.d.ts +11 -1
  28. package/dist/mcp/v2-response-formatter.js +168 -1
  29. package/dist/mcp/workflow-protocol-contracts.js +9 -7
  30. package/dist/types/workflow-definition.d.ts +16 -0
  31. package/dist/types/workflow-definition.js +1 -0
  32. package/dist/utils/condition-evaluator.d.ts +1 -0
  33. package/dist/utils/condition-evaluator.js +7 -0
  34. package/dist/v2/durable-core/domain/context-template-resolver.d.ts +2 -0
  35. package/dist/v2/durable-core/domain/context-template-resolver.js +26 -0
  36. package/dist/v2/durable-core/domain/prompt-renderer.d.ts +2 -0
  37. package/dist/v2/durable-core/domain/prompt-renderer.js +93 -15
  38. package/dist/v2/durable-core/schemas/compiled-workflow/index.d.ts +256 -0
  39. package/dist/v2/durable-core/schemas/compiled-workflow/index.js +30 -0
  40. package/package.json +4 -1
  41. package/spec/authoring-spec.json +1373 -0
  42. package/spec/authoring-spec.provenance.json +77 -0
  43. package/spec/authoring-spec.schema.json +370 -0
  44. package/spec/workflow.schema.json +88 -2
  45. package/workflows/coding-task-workflow-agentic.lean.v2.json +132 -30
  46. package/workflows/cross-platform-code-conversion.v2.json +199 -0
  47. package/workflows/routines/parallel-work-partitioning.json +43 -0
  48. package/workflows/workflow-for-workflows.json +27 -1
  49. package/workflows/workflow-for-workflows.v2.json +186 -0
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.formatV2ExecutionResponse = formatV2ExecutionResponse;
4
+ const render_envelope_js_1 = require("./render-envelope.js");
5
+ const response_supplements_js_1 = require("./response-supplements.js");
4
6
  function isV2ExecutionResponse(data) {
5
7
  if (typeof data !== 'object' || data === null)
6
8
  return false;
@@ -207,9 +209,160 @@ function formatSuccess(data) {
207
209
  }
208
210
  return lines.join('\n');
209
211
  }
212
+ function isCleanResponseFormat() {
213
+ return process.env.WORKRAIL_CLEAN_RESPONSE_FORMAT === 'true';
214
+ }
215
+ const CLEAN_ADVANCE_FOOTERS = [
216
+ 'WorkRail: when done, call continue_workflow with your notes. Token:',
217
+ 'WorkRail: advance with continue_workflow when ready. Include your notes. Token:',
218
+ 'WorkRail: call continue_workflow with your notes to move on. Token:',
219
+ 'WorkRail: finished? continue_workflow with notes. Token:',
220
+ ];
221
+ const CLEAN_REHYDRATE_FOOTERS = [
222
+ 'WorkRail: you are resuming this step. When ready, call continue_workflow with your notes. Token:',
223
+ 'WorkRail: picking up where you left off. Advance with continue_workflow and notes. Token:',
224
+ 'WorkRail: resuming. Call continue_workflow with notes when done. Token:',
225
+ ];
226
+ function pickFooter(variants, stepId) {
227
+ if (!stepId)
228
+ return variants[0];
229
+ let hash = 0;
230
+ for (let i = 0; i < stepId.length; i++) {
231
+ hash = ((hash << 5) - hash + stepId.charCodeAt(i)) | 0;
232
+ }
233
+ return variants[Math.abs(hash) % variants.length];
234
+ }
235
+ function formatCleanComplete(_data) {
236
+ return 'Workflow complete. No further steps.';
237
+ }
238
+ function formatCleanBlocked(data) {
239
+ const firstBlocker = data.blockers.blockers[0];
240
+ const heading = firstBlocker ? (BLOCKER_HEADING[firstBlocker.code] ?? firstBlocker.code) : 'Blocked';
241
+ const lines = [`Blocked: ${heading}`, ''];
242
+ for (const b of data.blockers.blockers) {
243
+ lines.push(b.message);
244
+ if (b.suggestedFix) {
245
+ lines.push('');
246
+ lines.push(`What to do: ${b.suggestedFix}`);
247
+ }
248
+ lines.push('');
249
+ }
250
+ if (data.validation) {
251
+ if (data.validation.issues.length > 0) {
252
+ lines.push('Issues:');
253
+ for (const issue of data.validation.issues)
254
+ lines.push(`- ${issue}`);
255
+ lines.push('');
256
+ }
257
+ }
258
+ const token = data.retryContinueToken ?? data.nextCall?.params.continueToken ?? data.continueToken;
259
+ if (token) {
260
+ lines.push(`WorkRail: retry with corrected output. Token: ${token}`);
261
+ }
262
+ return lines.join('\n');
263
+ }
264
+ function formatCleanRehydrate(data) {
265
+ const lines = [];
266
+ if (data.pending) {
267
+ lines.push(data.pending.prompt);
268
+ lines.push('');
269
+ }
270
+ const token = data.nextCall?.params.continueToken ?? data.continueToken;
271
+ lines.push('---');
272
+ if (token) {
273
+ const footer = pickFooter(CLEAN_REHYDRATE_FOOTERS, data.pending?.stepId);
274
+ lines.push(`${footer} ${token}`);
275
+ }
276
+ const driftBlock = formatBindingDriftWarnings(data);
277
+ if (driftBlock) {
278
+ lines.push(driftBlock);
279
+ }
280
+ return lines.join('\n');
281
+ }
282
+ function formatCleanSuccess(data) {
283
+ const lines = [];
284
+ if (data.pending) {
285
+ lines.push(data.pending.prompt);
286
+ lines.push('');
287
+ }
288
+ const token = data.nextCall?.params.continueToken ?? data.continueToken;
289
+ lines.push('---');
290
+ if (token) {
291
+ const footer = pickFooter(CLEAN_ADVANCE_FOOTERS, data.pending?.stepId);
292
+ lines.push(`${footer} ${token}`);
293
+ }
294
+ const driftBlock = formatBindingDriftWarnings(data);
295
+ if (driftBlock) {
296
+ lines.push(driftBlock);
297
+ }
298
+ return lines.join('\n');
299
+ }
300
+ function deriveRenderInput(data) {
301
+ const envelope = (0, render_envelope_js_1.getV2ExecutionRenderEnvelope)(data);
302
+ if (envelope != null) {
303
+ return isV2ExecutionResponse(envelope.response)
304
+ ? { response: envelope.response, lifecycle: envelope.lifecycle, contentEnvelope: envelope.contentEnvelope }
305
+ : null;
306
+ }
307
+ return isV2ExecutionResponse(data)
308
+ ? { response: data, lifecycle: 'advance' }
309
+ : null;
310
+ }
311
+ function renderReferencesSection(contentEnvelope, lifecycle) {
312
+ if (contentEnvelope == null)
313
+ return null;
314
+ const refs = contentEnvelope.references;
315
+ if (refs.length === 0)
316
+ return null;
317
+ switch (lifecycle) {
318
+ case 'start': {
319
+ const lines = ['Workflow References:', ''];
320
+ for (const ref of refs) {
321
+ const displayPath = ref.status === 'resolved' ? ref.resolvedPath : ref.source;
322
+ const statusTag = ref.status === 'unresolved' ? ' [unresolved]' : ref.status === 'pinned' ? ' [pinned]' : '';
323
+ const authority = ref.authoritative ? ' (authoritative)' : '';
324
+ const resolveTag = ref.resolveFrom === 'package' ? ' [package]' : '';
325
+ lines.push(`- **${ref.title}**${authority}${statusTag}${resolveTag}`);
326
+ lines.push(` Path: ${displayPath}`);
327
+ lines.push(` Purpose: ${ref.purpose}`);
328
+ lines.push('');
329
+ }
330
+ return { kind: 'references', text: lines.join('\n').trimEnd() };
331
+ }
332
+ case 'rehydrate': {
333
+ const lines = ['Workflow References (reminder):', ''];
334
+ for (const ref of refs) {
335
+ const displayPath = ref.status === 'resolved' ? ref.resolvedPath : ref.source;
336
+ const statusTag = ref.status === 'unresolved' ? ' [unresolved]' : ref.status === 'pinned' ? ' [pinned]' : '';
337
+ const resolveTag = ref.resolveFrom === 'package' ? ' [package]' : '';
338
+ lines.push(`- ${ref.title}${statusTag}${resolveTag}: ${displayPath}`);
339
+ }
340
+ return { kind: 'references', text: lines.join('\n').trimEnd() };
341
+ }
342
+ case 'advance':
343
+ return null;
344
+ }
345
+ }
210
346
  function formatV2ExecutionResponse(data) {
211
- if (!isV2ExecutionResponse(data))
347
+ const renderInput = deriveRenderInput(data);
348
+ if (!renderInput)
212
349
  return null;
350
+ const cleanFormat = isCleanResponseFormat();
351
+ const { response, lifecycle, contentEnvelope } = renderInput;
352
+ const references = renderReferencesSection(contentEnvelope, lifecycle);
353
+ if (cleanFormat) {
354
+ return {
355
+ ...formatV2Clean(response),
356
+ ...(references != null ? { references } : {}),
357
+ supplements: (0, response_supplements_js_1.buildResponseSupplements)({ lifecycle, cleanFormat }),
358
+ };
359
+ }
360
+ return {
361
+ primary: formatV2Classic(response),
362
+ ...(references != null ? { references } : {}),
363
+ };
364
+ }
365
+ function formatV2Classic(data) {
213
366
  if (data.nextIntent === 'complete' && !data.pending) {
214
367
  return formatComplete(data);
215
368
  }
@@ -221,3 +374,17 @@ function formatV2ExecutionResponse(data) {
221
374
  }
222
375
  return formatSuccess(data);
223
376
  }
377
+ function formatV2Clean(data) {
378
+ if (data.nextIntent === 'complete' && !data.pending) {
379
+ return { primary: formatCleanComplete(data) };
380
+ }
381
+ if (isBlocked(data)) {
382
+ return { primary: formatCleanBlocked(data) };
383
+ }
384
+ if (data.nextIntent === 'rehydrate_only') {
385
+ return { primary: formatCleanRehydrate(data) };
386
+ }
387
+ return {
388
+ primary: formatCleanSuccess(data),
389
+ };
390
+ }
@@ -51,10 +51,10 @@ exports.START_WORKFLOW_PROTOCOL = {
51
51
  },
52
52
  descriptions: {
53
53
  standard: {
54
- purpose: 'Start a WorkRail v2 workflow and begin following its step-by-step instructions.',
55
- whenToUse: 'Use this when you found the right workflow and are ready to execute it. The response body is the current step; the structured response includes the token(s) for your next call.',
54
+ purpose: 'Start a WorkRail v2 workflow. Treat WorkRail as a separate live system the user is actively using to direct this task. The returned step is what the user wants you to do now.',
55
+ whenToUse: 'Use this when you found the right workflow and are ready to execute it. The response is the current instruction coming from WorkRail for this task.',
56
56
  rules: [
57
- 'Follow the returned step exactly; it represents the user\'s plan for the task.',
57
+ 'Follow the returned step exactly; treat it as the user\'s current instruction.',
58
58
  'When the step is done, call continue_workflow with the returned continueToken.',
59
59
  'Only pass context on later continue_workflow calls if facts changed.',
60
60
  ],
@@ -65,8 +65,8 @@ exports.START_WORKFLOW_PROTOCOL = {
65
65
  returns: 'Step instructions plus continueToken and checkpointToken in the structured response.',
66
66
  },
67
67
  authoritative: {
68
- purpose: 'Begin executing the selected WorkRail v2 workflow.',
69
- whenToUse: 'Call this once you have chosen the workflow you will follow. The returned step is a direct instruction from the user or workflow author.',
68
+ purpose: 'Begin executing the selected WorkRail v2 workflow. Treat WorkRail as a separate live system the user is actively using to direct this task. The returned step is what the user wants you to do now.',
69
+ whenToUse: 'Call this once you have chosen the workflow. The returned step is a direct instruction from the user.',
70
70
  rules: [
71
71
  'Execute the returned step exactly as written.',
72
72
  'When the step is complete, call continue_workflow with the returned continueToken.',
@@ -90,13 +90,14 @@ exports.CONTINUE_WORKFLOW_PROTOCOL = {
90
90
  },
91
91
  descriptions: {
92
92
  standard: {
93
- purpose: 'Advance or rehydrate the current WorkRail v2 step using the single-token protocol.',
93
+ purpose: 'Advance or rehydrate the current WorkRail v2 step using the single-token protocol. Use this to send your completion back to WorkRail and receive the user\'s next instruction from that system.',
94
94
  whenToUse: 'Use this after completing a step, or to recover the current step after lost context.',
95
95
  rules: [
96
96
  'Advance by sending output (and intent: "advance" if you want to be explicit).',
97
97
  'Rehydrate by omitting output (and intent: "rehydrate" if you want to be explicit).',
98
98
  'Put changed facts under context only.',
99
99
  'Round-trip continueToken exactly as returned by WorkRail; use the single-token API only.',
100
+ 'Notes (output.notesMarkdown): write for a human reader. Include what you did and key decisions, what you produced (files, tests, numbers), and anything notable (risks, open questions, deliberate omissions). Use markdown headings, bullets, bold, code refs. Be specific. Scope: THIS step only (WorkRail concatenates automatically). 10-30 lines ideal. Omitting notes blocks the step.',
100
101
  ],
101
102
  examplePayload: {
102
103
  continueToken: 'ct_...',
@@ -107,13 +108,14 @@ exports.CONTINUE_WORKFLOW_PROTOCOL = {
107
108
  returns: 'The next step, or the same current step when rehydrating.',
108
109
  },
109
110
  authoritative: {
110
- purpose: 'Continue the active WorkRail v2 workflow with the canonical single-token API.',
111
+ purpose: 'Continue the active WorkRail v2 workflow with the canonical single-token API. Use this to send your completion back to WorkRail and receive the user\'s next instruction from that system.',
111
112
  whenToUse: 'Call this after you complete the current step, or call it in rehydrate mode to recover the current step without advancing.',
112
113
  rules: [
113
114
  'Use continueToken exactly as returned by WorkRail.',
114
115
  'Use the single-token API only.',
115
116
  'Advance by sending output; rehydrate by omitting output.',
116
117
  'Put updated facts in context only.',
118
+ 'Notes (output.notesMarkdown): write for a human reader. Include what you did and key decisions, what you produced (files, tests, numbers), and anything notable (risks, open questions, deliberate omissions). Use markdown headings, bullets, bold, code refs. Be specific. Scope: THIS step only (WorkRail concatenates automatically). 10-30 lines ideal. Omitting notes blocks the step.',
117
119
  ],
118
120
  examplePayload: {
119
121
  continueToken: 'ct_...',
@@ -1,10 +1,16 @@
1
1
  import { ValidationCriteria } from './validation';
2
2
  import type { ArtifactContractRef } from '../v2/durable-core/schemas/artifacts/index';
3
3
  import type { PromptBlocks } from '../application/services/compiler/prompt-blocks.js';
4
+ import type { Condition } from '../utils/condition-evaluator.js';
4
5
  export interface OutputContract {
5
6
  readonly contractRef: ArtifactContractRef;
6
7
  readonly required?: boolean;
7
8
  }
9
+ export interface PromptFragment {
10
+ readonly id: string;
11
+ readonly when?: Condition;
12
+ readonly text: string;
13
+ }
8
14
  export interface WorkflowStepDefinition {
9
15
  readonly id: string;
10
16
  readonly title: string;
@@ -19,6 +25,7 @@ export interface WorkflowStepDefinition {
19
25
  readonly outputContract?: OutputContract;
20
26
  readonly notesOptional?: boolean;
21
27
  readonly templateCall?: TemplateCall;
28
+ readonly promptFragments?: readonly PromptFragment[];
22
29
  readonly functionDefinitions?: readonly FunctionDefinition[];
23
30
  readonly functionCalls?: readonly FunctionCall[];
24
31
  readonly functionReferences?: readonly string[];
@@ -79,6 +86,14 @@ export interface WorkflowRecommendedPreferences {
79
86
  readonly recommendedAutonomy?: 'guided' | 'full_auto_stop_on_user_deps' | 'full_auto_never_stop';
80
87
  readonly recommendedRiskPolicy?: 'conservative' | 'balanced' | 'aggressive';
81
88
  }
89
+ export interface WorkflowReference {
90
+ readonly id: string;
91
+ readonly title: string;
92
+ readonly source: string;
93
+ readonly purpose: string;
94
+ readonly authoritative: boolean;
95
+ readonly resolveFrom?: 'workspace' | 'package';
96
+ }
82
97
  export interface WorkflowDefinition {
83
98
  readonly id: string;
84
99
  readonly name: string;
@@ -92,6 +107,7 @@ export interface WorkflowDefinition {
92
107
  readonly recommendedPreferences?: WorkflowRecommendedPreferences;
93
108
  readonly features?: readonly string[];
94
109
  readonly extensionPoints?: readonly ExtensionPoint[];
110
+ readonly references?: readonly WorkflowReference[];
95
111
  }
96
112
  export declare function isLoopStepDefinition(step: WorkflowStepDefinition | LoopStepDefinition): step is LoopStepDefinition;
97
113
  export declare function isWorkflowStepDefinition(step: WorkflowStepDefinition | LoopStepDefinition): step is WorkflowStepDefinition;
@@ -34,5 +34,6 @@ function createWorkflowDefinition(definition) {
34
34
  metaGuidance: definition.metaGuidance ? Object.freeze([...definition.metaGuidance]) : undefined,
35
35
  functionDefinitions: definition.functionDefinitions ? Object.freeze([...definition.functionDefinitions]) : undefined,
36
36
  extensionPoints: definition.extensionPoints ? Object.freeze([...definition.extensionPoints]) : undefined,
37
+ references: definition.references ? Object.freeze(definition.references.map(ref => Object.freeze({ ...ref }))) : undefined,
37
38
  });
38
39
  }
@@ -13,6 +13,7 @@ export interface Condition {
13
13
  startsWith?: string;
14
14
  endsWith?: string;
15
15
  matches?: string;
16
+ in?: unknown[];
16
17
  and?: Condition[];
17
18
  or?: Condition[];
18
19
  not?: Condition;
@@ -98,6 +98,12 @@ function evaluateConditionUnsafe(condition, context) {
98
98
  return false;
99
99
  }
100
100
  }
101
+ if (condition.in !== undefined) {
102
+ if (!Array.isArray(condition.in)) {
103
+ throw new Error('in operator requires an array');
104
+ }
105
+ return condition.in.some(item => lenientEquals(value, item));
106
+ }
101
107
  return !!value;
102
108
  }
103
109
  if (condition.and !== undefined) {
@@ -124,6 +130,7 @@ function validateCondition(condition) {
124
130
  const supportedKeys = [
125
131
  'var', 'equals', 'not_equals', 'gt', 'gte', 'lt', 'lte',
126
132
  'contains', 'startsWith', 'endsWith', 'matches',
133
+ 'in',
127
134
  'and', 'or', 'not'
128
135
  ];
129
136
  const conditionKeys = Object.keys(condition);
@@ -0,0 +1,2 @@
1
+ export declare const CONTEXT_TOKEN_PATTERN: RegExp;
2
+ export declare function resolveContextTemplates(template: string, context: Record<string, unknown>): string;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CONTEXT_TOKEN_PATTERN = void 0;
4
+ exports.resolveContextTemplates = resolveContextTemplates;
5
+ exports.CONTEXT_TOKEN_PATTERN = /\{\{(?!wr\.)([a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*)\}\}/;
6
+ const CONTEXT_TOKEN_RE_G = new RegExp(exports.CONTEXT_TOKEN_PATTERN.source, 'g');
7
+ function resolveDotPath(base, path) {
8
+ let current = base;
9
+ for (const segment of path) {
10
+ if (current === null || typeof current !== 'object')
11
+ return undefined;
12
+ current = current[segment];
13
+ }
14
+ return current;
15
+ }
16
+ function resolveContextTemplates(template, context) {
17
+ if (!template.includes('{{'))
18
+ return template;
19
+ return template.replace(CONTEXT_TOKEN_RE_G, (_match, dotPath) => {
20
+ const value = resolveDotPath(context, dotPath.split('.'));
21
+ if (value === undefined || value === null) {
22
+ return `[unset: ${dotPath}]`;
23
+ }
24
+ return String(value);
25
+ });
26
+ }
@@ -1,5 +1,6 @@
1
1
  import type { Result } from 'neverthrow';
2
2
  import type { Workflow } from '../../../types/workflow.js';
3
+ import type { PromptFragment } from '../../../types/workflow-definition.js';
3
4
  import type { LoadedSessionTruthV2 } from '../../ports/session-event-log-store.port.js';
4
5
  import type { LoopPathFrameV1 } from '../schemas/execution-snapshot/index.js';
5
6
  import type { NodeId, RunId } from '../ids/index.js';
@@ -7,6 +8,7 @@ export type PromptRenderError = {
7
8
  readonly code: 'RENDER_FAILED';
8
9
  readonly message: string;
9
10
  };
11
+ export declare function assembleFragmentedPrompt(fragments: readonly PromptFragment[], context: Record<string, unknown>): string;
10
12
  export interface StepMetadata {
11
13
  readonly stepId: string;
12
14
  readonly title: string;
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assembleFragmentedPrompt = assembleFragmentedPrompt;
3
4
  exports.renderPendingPrompt = renderPendingPrompt;
4
5
  const neverthrow_1 = require("neverthrow");
5
6
  const workflow_js_1 = require("../../../types/workflow.js");
@@ -11,6 +12,9 @@ const function_definition_expander_js_1 = require("./function-definition-expande
11
12
  const constants_js_1 = require("../constants.js");
12
13
  const validation_requirements_extractor_js_1 = require("./validation-requirements-extractor.js");
13
14
  const index_js_2 = require("../schemas/artifacts/index.js");
15
+ const run_context_js_1 = require("../../projections/run-context.js");
16
+ const condition_evaluator_js_1 = require("../../../utils/condition-evaluator.js");
17
+ const context_template_resolver_js_1 = require("./context-template-resolver.js");
14
18
  function buildNonTipSections(args) {
15
19
  const sections = [];
16
20
  const childSummary = (0, recap_recovery_js_1.buildChildSummary)({ nodeId: args.nodeId, dag: args.run });
@@ -55,6 +59,12 @@ function buildFunctionDefsSections(args) {
55
59
  }
56
60
  return [];
57
61
  }
62
+ function hasPriorNotesInRun(args) {
63
+ return args.truth.events.some((e) => e.kind === constants_js_1.EVENT_KIND.NODE_OUTPUT_APPENDED &&
64
+ e.scope.runId === args.runId &&
65
+ e.data.outputChannel === constants_js_1.OUTPUT_CHANNEL.RECAP &&
66
+ e.data.payload.payloadKind === constants_js_1.PAYLOAD_KIND.NOTES);
67
+ }
58
68
  function buildRecoverySections(args) {
59
69
  const isTip = args.run.tipNodeIds.includes(String(args.nodeId));
60
70
  return [
@@ -114,18 +124,35 @@ function applyPromptBudget(combinedPrompt) {
114
124
  const decoder = new TextDecoder('utf-8');
115
125
  return decoder.decode(truncatedBytes) + markerText + omissionNote;
116
126
  }
117
- function resolveParentLoopMaxIterations(workflow, stepId) {
127
+ function resolveParentLoopStep(workflow, stepId) {
118
128
  for (const step of workflow.definition.steps) {
119
129
  if ((0, workflow_js_1.isLoopStepDefinition)(step) && Array.isArray(step.body)) {
120
130
  for (const bodyStep of step.body) {
121
- if (bodyStep.id === stepId) {
122
- return step.loop.maxIterations;
123
- }
131
+ if (bodyStep.id === stepId)
132
+ return step;
124
133
  }
125
134
  }
126
135
  }
127
136
  return undefined;
128
137
  }
138
+ function buildLoopRenderContext(loopStep, iteration, sessionContext) {
139
+ const iterationVar = loopStep.loop.iterationVar || 'currentIteration';
140
+ const forEachVars = () => {
141
+ if (loopStep.loop.type !== 'forEach' || !loopStep.loop.items)
142
+ return {};
143
+ const items = sessionContext[loopStep.loop.items];
144
+ if (!Array.isArray(items))
145
+ return {};
146
+ return {
147
+ [loopStep.loop.itemVar || 'currentItem']: items[iteration],
148
+ [loopStep.loop.indexVar || 'currentIndex']: iteration,
149
+ };
150
+ };
151
+ return {
152
+ [iterationVar]: iteration + 1,
153
+ ...forEachVars(),
154
+ };
155
+ }
129
156
  function buildScopeInstruction(iteration, maxIterations) {
130
157
  if (iteration <= 1)
131
158
  return 'Focus on what the first pass missed — do not re-litigate settled findings.';
@@ -139,6 +166,15 @@ function buildLoopContextBanner(args) {
139
166
  const current = args.loopPath[args.loopPath.length - 1];
140
167
  const iterationNumber = current.iteration + 1;
141
168
  const maxIter = args.maxIterations;
169
+ if (args.cleanFormat) {
170
+ if (current.iteration === 0) {
171
+ const bound = maxIter !== undefined ? ` (up to ${maxIter} passes)` : '';
172
+ return `This is an iterative step${bound}. A decision step after your work determines whether another pass is needed.\n\n`;
173
+ }
174
+ const ofMax = maxIter !== undefined ? ` of ${maxIter}` : '';
175
+ const scope = buildScopeInstruction(current.iteration, maxIter);
176
+ return `Pass ${iterationNumber}${ofMax}. ${scope} Build on your previous work.\n\n`;
177
+ }
142
178
  if (current.iteration === 0) {
143
179
  const bound = maxIter !== undefined ? ` (up to ${maxIter} passes)` : '';
144
180
  return [
@@ -184,6 +220,12 @@ function formatOutputContractRequirements(outputContract) {
184
220
  ];
185
221
  }
186
222
  }
223
+ function assembleFragmentedPrompt(fragments, context) {
224
+ return fragments
225
+ .filter(f => (0, condition_evaluator_js_1.evaluateCondition)(f.when, context))
226
+ .map(f => (0, context_template_resolver_js_1.resolveContextTemplates)(f.text, context))
227
+ .join('\n\n');
228
+ }
187
229
  function loadRecoveryProjections(args) {
188
230
  const dagRes = (0, run_dag_js_1.projectRunDagV2)(args.truth.events);
189
231
  if (dagRes.isErr()) {
@@ -208,8 +250,6 @@ function renderPendingPrompt(args) {
208
250
  message: `Step '${args.stepId}' not found in workflow '${args.workflow.definition.id}'`,
209
251
  });
210
252
  }
211
- const baseTitle = step.title;
212
- const basePrompt = step.prompt;
213
253
  const agentRole = step.agentRole;
214
254
  const requireConfirmation = Boolean(step.requireConfirmation);
215
255
  const functionReferences = step.functionReferences ?? [];
@@ -217,22 +257,51 @@ function renderPendingPrompt(args) {
217
257
  ? step.outputContract
218
258
  : undefined;
219
259
  const isExitStep = outputContract?.contractRef === index_js_2.LOOP_CONTROL_CONTRACT_REF;
220
- const maxIterations = resolveParentLoopMaxIterations(args.workflow, args.stepId);
221
- const loopBanner = buildLoopContextBanner({ loopPath: args.loopPath, isExitStep, maxIterations });
260
+ const loopStep = resolveParentLoopStep(args.workflow, args.stepId);
261
+ const maxIterations = loopStep?.loop.maxIterations;
262
+ const sessionContext = (0, run_context_js_1.projectRunContextV2)(args.truth.events).match((ok) => (ok.byRunId[String(args.runId)]?.context ?? {}), (e) => {
263
+ console.warn(`[prompt-renderer] Context projection failed for step '${args.stepId}' — ` +
264
+ `{{varName}} tokens will render as [unset:...]: ${e.message}`);
265
+ return {};
266
+ });
267
+ const loopIterationFrame = args.loopPath.at(-1);
268
+ const loopRenderContext = loopStep && loopIterationFrame
269
+ ? buildLoopRenderContext(loopStep, loopIterationFrame.iteration, sessionContext)
270
+ : {};
271
+ const renderContext = { ...sessionContext, ...loopRenderContext };
272
+ const basePrompt = (0, context_template_resolver_js_1.resolveContextTemplates)(step.prompt ?? '', renderContext);
273
+ const baseTitle = (0, context_template_resolver_js_1.resolveContextTemplates)(step.title, renderContext);
274
+ const cleanResponseFormat = process.env.WORKRAIL_CLEAN_RESPONSE_FORMAT === 'true';
275
+ const loopBanner = buildLoopContextBanner({ loopPath: args.loopPath, isExitStep, maxIterations, cleanFormat: cleanResponseFormat });
222
276
  const validationCriteria = step.validationCriteria;
223
277
  const requirements = (0, validation_requirements_extractor_js_1.extractValidationRequirements)(validationCriteria);
224
278
  const requirementsSection = requirements.length > 0
225
- ? `\n\n**OUTPUT REQUIREMENTS:**\n${requirements.map(r => `- ${r}`).join('\n')}`
279
+ ? cleanResponseFormat
280
+ ? `\n\n${requirements.map(r => `- ${r}`).join('\n')}`
281
+ : `\n\n**OUTPUT REQUIREMENTS:**\n${requirements.map(r => `- ${r}`).join('\n')}`
226
282
  : '';
227
283
  const contractRequirements = formatOutputContractRequirements(outputContract);
228
284
  const contractSection = contractRequirements.length > 0
229
- ? `\n\n**OUTPUT REQUIREMENTS (System):**\n${contractRequirements.map(r => `- ${r}`).join('\n')}`
285
+ ? cleanResponseFormat
286
+ ? `\n\n${contractRequirements.map(r => `- ${r}`).join('\n')}`
287
+ : `\n\n**OUTPUT REQUIREMENTS (System):**\n${contractRequirements.map(r => `- ${r}`).join('\n')}`
230
288
  : '';
231
289
  const isNotesOptional = outputContract !== undefined ||
232
290
  ('notesOptional' in step && step.notesOptional === true);
233
- const notesSection = isNotesOptional
234
- ? ''
235
- : '\n\n**NOTES REQUIRED (System):** You must include `output.notesMarkdown` when advancing. ' +
291
+ const notesSection = (() => {
292
+ if (isNotesOptional)
293
+ return '';
294
+ if (cleanResponseFormat) {
295
+ return '';
296
+ }
297
+ const hasPriorNotes = hasPriorNotesInRun({ truth: args.truth, runId: args.runId });
298
+ if (hasPriorNotes && !args.rehydrateOnly) {
299
+ return '\n\n**NOTES REQUIRED (System):** Include `output.notesMarkdown` when advancing.\n\n' +
300
+ 'Scope: this step only — WorkRail concatenates notes automatically.\n' +
301
+ 'Include: what you did, what you produced, and anything notable.\n' +
302
+ 'Be specific. Omitting notes will block this step.';
303
+ }
304
+ return '\n\n**NOTES REQUIRED (System):** You must include `output.notesMarkdown` when advancing. ' +
236
305
  'These notes are displayed to the user in a markdown viewer and serve as the durable record of your work. Write them for a human reader.\n\n' +
237
306
  'Include:\n' +
238
307
  '- **What you did** and the key decisions or trade-offs you made\n' +
@@ -260,7 +329,15 @@ function renderPendingPrompt(args) {
260
329
  '> ### Open questions\n' +
261
330
  '> - Should refresh tokens be rotated on every use? Current impl reuses until expiry.\n\n' +
262
331
  'Omitting notes will block this step — use the `retryAckToken` to fix and retry.';
263
- const enhancedPrompt = loopBanner + basePrompt + requirementsSection + contractSection + notesSection;
332
+ })();
333
+ const promptFragments = 'promptFragments' in step
334
+ ? step.promptFragments
335
+ : undefined;
336
+ const fragmentSuffix = promptFragments && promptFragments.length > 0
337
+ ? assembleFragmentedPrompt(promptFragments, renderContext)
338
+ : '';
339
+ const enhancedPrompt = loopBanner + basePrompt + requirementsSection + contractSection + notesSection
340
+ + (fragmentSuffix ? '\n\n' + fragmentSuffix : '');
264
341
  if (!args.rehydrateOnly) {
265
342
  return (0, neverthrow_1.ok)({ stepId: args.stepId, title: baseTitle, prompt: enhancedPrompt, agentRole, requireConfirmation });
266
343
  }
@@ -288,7 +365,8 @@ function renderPendingPrompt(args) {
288
365
  if (sections.length === 0) {
289
366
  return (0, neverthrow_1.ok)({ stepId: args.stepId, title: baseTitle, prompt: enhancedPrompt, agentRole, requireConfirmation });
290
367
  }
291
- const recoveryText = `## Recovery Context\n\n${sections.join('\n\n')}`;
368
+ const recoveryHeader = cleanResponseFormat ? 'Your previous work:' : '## Recovery Context';
369
+ const recoveryText = `${recoveryHeader}\n\n${sections.join('\n\n')}`;
292
370
  const combinedPrompt = `${enhancedPrompt}\n\n${recoveryText}`;
293
371
  const finalPrompt = applyPromptBudget(combinedPrompt);
294
372
  return (0, neverthrow_1.ok)({ stepId: args.stepId, title: baseTitle, prompt: finalPrompt, agentRole, requireConfirmation });