@exaudeus/workrail 1.17.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -31,8 +31,8 @@ to conclusions.
31
31
  WorkRail replaces the human effort of guiding an agent step-by-step.
32
32
 
33
33
  Instead of one system prompt that fades over time, WorkRail drip-feeds instructions through
34
- the [Model Context Protocol](https://modelcontextprotocol.org). The agent calls `workflow_next`,
35
- gets ONE step, completes it, calls again. Future steps stay hidden until previous ones are done.
34
+ the [Model Context Protocol](https://modelcontextprotocol.org). The agent calls `start_workflow`,
35
+ gets the first step, completes it, calls `continue_workflow`. Future steps stay hidden until previous ones are done.
36
36
 
37
37
  **The agent can't skip to implementation because it doesn't know those steps exist yet.**
38
38
 
@@ -44,7 +44,7 @@ You Agent WorkRail
44
44
  │ "Fix the auth bug" │ │
45
45
  │────────────────────────>│ │
46
46
  │ │ │
47
- │ │ workflow_next()
47
+ │ │ start_workflow()
48
48
  │ │─────────────────────────>│
49
49
  │ │ │
50
50
  │ │ Step 1: Understand │
@@ -55,7 +55,7 @@ You Agent WorkRail
55
55
  │ see exactly?" │ │
56
56
  │<────────────────────────│ │
57
57
  │ │ │
58
- │ ... │ workflow_next()
58
+ │ ... │ continue_workflow()
59
59
  │ │─────────────────────────>│
60
60
  │ │ │
61
61
  │ │ Step 2: Plan your │
@@ -81,21 +81,21 @@ Agent: "I see the issue! In auth.js line 42, there's a null check that
81
81
  You: "There's a bug in the auth flow"
82
82
 
83
83
  Agent: "I'll use the bug-investigation workflow."
84
- workflow_next()
84
+ start_workflow()
85
85
 
86
86
  Step 1: Investigation Setup
87
87
  "Before I investigate, I need to understand the problem.
88
88
  What exactly happens when it fails? Can you share the error?"
89
89
 
90
90
  [Documents bug, reproduction steps, environment]
91
- workflow_next()
91
+ continue_workflow()
92
92
 
93
93
  Step 2: Plan Investigation
94
94
  "I'll trace execution from login through the auth middleware.
95
95
  Key areas: token validation, session lookup, error handling."
96
96
 
97
97
  [Creates investigation plan before touching code]
98
- workflow_next()
98
+ continue_workflow()
99
99
 
100
100
  Step 3: Form Hypotheses
101
101
  "Based on my analysis, three possible causes:
@@ -164,13 +164,13 @@ The agent will find the workflow, start at step 1, and proceed systematically.
164
164
 
165
165
  ## Included Workflows
166
166
 
167
- 20+ workflows included for development, debugging, review, documentation, and more:
167
+ 30+ workflows included for development, debugging, review, documentation, and more:
168
168
 
169
169
  | Workflow | When to Use |
170
170
  |----------|-------------|
171
- | `coding-task-workflow-with-loops` | Feature development with analysis, planning, and review |
172
- | `bug-investigation` | Systematic debugging with hypothesis testing |
173
- | `mr-review-workflow` | Code review with architecture and security checks |
171
+ | `coding-task-workflow-agentic` | Feature development with notes-first durability and audit loops |
172
+ | `bug-investigation-agentic` | Systematic debugging with evidence-based analysis |
173
+ | `mr-review-workflow-agentic` | Code review with parallel reviewer families |
174
174
  | `exploration-workflow` | Understanding an unfamiliar codebase |
175
175
  | `document-creation-workflow` | Technical documentation with structure |
176
176
 
@@ -196,7 +196,7 @@ let WorkflowInterpreter = class WorkflowInterpreter {
196
196
  return (0, neverthrow_1.ok)(shouldEnter);
197
197
  }
198
198
  case 'while': {
199
- const res = this.evaluateWhileUntilCondition(loopCompiled, iteration, context, artifacts, frame, false);
199
+ const res = this.evaluateWhileUntilCondition(loopCompiled, iteration, context, artifacts, frame, false, trace);
200
200
  if (res.isOk()) {
201
201
  const source = loopCompiled.conditionSource?.kind === 'artifact_contract' ? 'artifact'
202
202
  : loopCompiled.conditionSource?.kind === 'context_variable' ? 'context'
@@ -208,7 +208,7 @@ let WorkflowInterpreter = class WorkflowInterpreter {
208
208
  return res;
209
209
  }
210
210
  case 'until': {
211
- const res = this.evaluateWhileUntilCondition(loopCompiled, iteration, context, artifacts, frame, true);
211
+ const res = this.evaluateWhileUntilCondition(loopCompiled, iteration, context, artifacts, frame, true, trace);
212
212
  if (res.isOk()) {
213
213
  const source = loopCompiled.conditionSource?.kind === 'artifact_contract' ? 'artifact'
214
214
  : loopCompiled.conditionSource?.kind === 'context_variable' ? 'context'
@@ -303,7 +303,7 @@ let WorkflowInterpreter = class WorkflowInterpreter {
303
303
  return (0, neverthrow_1.err)(error_1.Err.invalidState('Non-exhaustive loop decision'));
304
304
  }
305
305
  }
306
- evaluateWhileUntilCondition(loopCompiled, iteration, context, artifacts, frame, invertForUntil) {
306
+ evaluateWhileUntilCondition(loopCompiled, iteration, context, artifacts, frame, invertForUntil, traceEntries = []) {
307
307
  const source = loopCompiled.conditionSource;
308
308
  if (!source) {
309
309
  if (!loopCompiled.loop.loop.condition) {
@@ -318,11 +318,19 @@ let WorkflowInterpreter = class WorkflowInterpreter {
318
318
  }
319
319
  switch (source.kind) {
320
320
  case 'artifact_contract': {
321
- const result = (0, loop_control_evaluator_1.evaluateLoopControlFromArtifacts)(artifacts, source.loopId);
322
- if (result.kind === 'found') {
323
- return (0, neverthrow_1.ok)(result.decision === 'continue');
321
+ const result = (0, loop_control_evaluator_1.evaluateLoopControlFromArtifacts)(artifacts);
322
+ traceEntries.push((0, decision_trace_builder_1.traceArtifactMatchResult)(source.loopId, iteration, result));
323
+ switch (result.kind) {
324
+ case 'found':
325
+ return (0, neverthrow_1.ok)(result.decision === 'continue');
326
+ case 'not_found':
327
+ case 'invalid':
328
+ return (0, neverthrow_1.ok)(true);
329
+ default: {
330
+ const _exhaustive = result;
331
+ return (0, neverthrow_1.ok)(true);
332
+ }
324
333
  }
325
- return (0, neverthrow_1.ok)(true);
326
334
  }
327
335
  case 'context_variable': {
328
336
  const raw = (0, condition_evaluator_1.evaluateCondition)(source.condition, this.projectLoopContextAtIteration(loopCompiled.loop, iteration, context));
@@ -56,10 +56,10 @@ exports.FEATURE_FLAG_DEFINITIONS = [
56
56
  {
57
57
  key: 'v2Tools',
58
58
  envVar: 'WORKRAIL_ENABLE_V2_TOOLS',
59
- defaultValue: false,
60
- description: 'Enable WorkRail v2 MCP tools and .v2.json workflow overrides behind an explicit opt-in flag',
59
+ defaultValue: true,
60
+ description: 'WorkRail v2 MCP tools (list_workflows, start_workflow, continue_workflow, checkpoint_workflow, resume_session) and .v2.json workflow overrides',
61
61
  since: '0.9.0',
62
- stable: false,
62
+ stable: true,
63
63
  },
64
64
  ];
65
65
  function parseBoolean(value, defaultValue) {
@@ -110,8 +110,8 @@
110
110
  "bytes": 1507
111
111
  },
112
112
  "application/services/workflow-interpreter.js": {
113
- "sha256": "e6ea1bc6f3881bc46a901b78720dee584090f86809bfa1979f04efa4c8a35613",
114
- "bytes": 21011
113
+ "sha256": "df7a04c0b2c578cf54e3bc818a08b6e1af18ea75f6ade4901b3e9ceeabf46814",
114
+ "bytes": 21430
115
115
  },
116
116
  "application/services/workflow-service.d.ts": {
117
117
  "sha256": "b92da17c6d91c90758ec42b4ee3bc448e5d5b1dfe7351f2fe0f5e1d10a715ec6",
@@ -326,8 +326,8 @@
326
326
  "bytes": 1512
327
327
  },
328
328
  "config/feature-flags.js": {
329
- "sha256": "c7c5a4db2a0d5f86a9cf91c9a7e2c74f89ce7f1012b93c8237a3d70a783d2b45",
330
- "bytes": 6908
329
+ "sha256": "d8a709e0814b4d9bd9565b795c7b932fbab1d86762b437f7ed64be8e97a99598",
330
+ "bytes": 6957
331
331
  },
332
332
  "core/error-handler.d.ts": {
333
333
  "sha256": "80451f12ac8e185133ec3dc4c57285491a785f27525ed21e729db1da3f61010d",
@@ -770,12 +770,12 @@
770
770
  "bytes": 12661
771
771
  },
772
772
  "mcp/handlers/v2-execution/start.d.ts": {
773
- "sha256": "333f9f36756d2ce55249c6af45198a17b51aa9c6448e063f7621fcd61879f2bf",
774
- "bytes": 2684
773
+ "sha256": "ec0a9cbe95eba29cbf4f66c7fbacd10209b451e9e0a3e15bf92cbc751ef5b0c2",
774
+ "bytes": 2868
775
775
  },
776
776
  "mcp/handlers/v2-execution/start.js": {
777
- "sha256": "0c6800d22f93a36b45236d17993b445de131e00a72eac8dbd98e2f4d8d9f2b29",
778
- "bytes": 15208
777
+ "sha256": "fa724a733b2f249bcd9300d79f4f5d20fa155f005a2d9837f16f50b44bfd106c",
778
+ "bytes": 16250
779
779
  },
780
780
  "mcp/handlers/v2-resume.d.ts": {
781
781
  "sha256": "d88f6c35bcaf946666c837b72fda3702a2ebab5e478eb90f7b4b672a0e5fa24f",
@@ -846,8 +846,8 @@
846
846
  "bytes": 882
847
847
  },
848
848
  "mcp/server.js": {
849
- "sha256": "5086b482d2eb3c42caac637282a5361b4db6e5d14aaeeb7c5ffdf75d211af0d8",
850
- "bytes": 13422
849
+ "sha256": "c07f8a6e8f89f7620a063673859dcc458212b068237949acff253f15856afd5f",
850
+ "bytes": 14174
851
851
  },
852
852
  "mcp/tool-description-provider.d.ts": {
853
853
  "sha256": "1d46abc3112e11b68e57197e846f5708293ec9b2281fa71a9124ee2aad71e41b",
@@ -914,8 +914,8 @@
914
914
  "bytes": 747
915
915
  },
916
916
  "mcp/types.d.ts": {
917
- "sha256": "2ef7f9c1bffbe365b06469dfad43b45c4ae3b5292dd269b33423bf1c8c9c7c43",
918
- "bytes": 4355
917
+ "sha256": "d61a7d2b9a5f389d9fbf0092f6d38c3e6bb2925f41dce0270ed0c2e54ca856c9",
918
+ "bytes": 4533
919
919
  },
920
920
  "mcp/types.js": {
921
921
  "sha256": "d10c4070e4c3454d80f0fa9cdc0e978c69c53c13fd09baa8710fcd802fed8926",
@@ -1362,12 +1362,12 @@
1362
1362
  "bytes": 1734
1363
1363
  },
1364
1364
  "v2/durable-core/domain/decision-trace-builder.d.ts": {
1365
- "sha256": "7caf53d88745ea8f05e3904dee6a03379af5f68cabd9580e99d93e0095992fc9",
1366
- "bytes": 1520
1365
+ "sha256": "bcc7892a25099be88ca92770d98f2e7e232eef007ff8f48001aefe9c08ee876c",
1366
+ "bytes": 1742
1367
1367
  },
1368
1368
  "v2/durable-core/domain/decision-trace-builder.js": {
1369
- "sha256": "011f3155612d782c8877238893e53ac3c30962596173202dcbf1a1c4f04932a0",
1370
- "bytes": 3711
1369
+ "sha256": "411da4951de3603c811afe8f4fa93de724f162834522c84e99a219c28248bf73",
1370
+ "bytes": 4332
1371
1371
  },
1372
1372
  "v2/durable-core/domain/function-definition-expander.d.ts": {
1373
1373
  "sha256": "2720e7c4dfbd9ec0cdf75eb4384fdbea18d4430aeae7f13d9215f0e5e4980468",
@@ -1386,12 +1386,12 @@
1386
1386
  "bytes": 908
1387
1387
  },
1388
1388
  "v2/durable-core/domain/loop-control-evaluator.d.ts": {
1389
- "sha256": "8fd92ad809a19a00a856374abb5a3e3cc8cbfe7c4a26fa28bc2994294d0bac2e",
1390
- "bytes": 533
1389
+ "sha256": "fbda4d2cb49d71e066f1b9bc967d5b89f4e2d1c692b865b026cf9e0bad80ad99",
1390
+ "bytes": 517
1391
1391
  },
1392
1392
  "v2/durable-core/domain/loop-control-evaluator.js": {
1393
- "sha256": "047dcc651b9b25c63e11c8efcff7d27d9d329297db94728a35f52e88399d6e73",
1394
- "bytes": 810
1393
+ "sha256": "0db4f39141a02c3a60c50955e71e5d925a473cdd01a6d6661b2d6df5d4ca2643",
1394
+ "bytes": 1071
1395
1395
  },
1396
1396
  "v2/durable-core/domain/loop-runtime.d.ts": {
1397
1397
  "sha256": "ee4f8857d72bf0066bccc351a4e8fd8e4800f3d540e9851bb4978179b8ebfc04",
@@ -1430,8 +1430,8 @@
1430
1430
  "bytes": 935
1431
1431
  },
1432
1432
  "v2/durable-core/domain/prompt-renderer.js": {
1433
- "sha256": "5c360d989c9b8ec6c8319943067b5e5e711d37086de1b8231c7c20c5285ec075",
1434
- "bytes": 12459
1433
+ "sha256": "5021f3cc9e59db56c5aeddcc8ed6f69df7ea1b1f3011d685695021e039376e49",
1434
+ "bytes": 13910
1435
1435
  },
1436
1436
  "v2/durable-core/domain/reason-model.d.ts": {
1437
1437
  "sha256": "650fcb2d9969a4e6123cccbd4913f4d57aeab21a19bb907aa1e11f95e5a95089",
@@ -1618,12 +1618,12 @@
1618
1618
  "bytes": 1728
1619
1619
  },
1620
1620
  "v2/durable-core/schemas/artifacts/loop-control.d.ts": {
1621
- "sha256": "f1201fe5ab9680b11959985bf6004adcd8227de74b8151a6b4457553d6394203",
1622
- "bytes": 2792
1621
+ "sha256": "714e2980400da0aaf814c691d8915653393316e70bc9af9da72130bb07e959be",
1622
+ "bytes": 2768
1623
1623
  },
1624
1624
  "v2/durable-core/schemas/artifacts/loop-control.js": {
1625
- "sha256": "8f0cb5a113d7700a2a0aca158a7463bd5528b40d2e16dc14d1596e997c5b3b0d",
1626
- "bytes": 2243
1625
+ "sha256": "c4b2ed38f9fdda3fbe3e4b68b0a228890fd9a7c73117a6d202928acabe6fc0f9",
1626
+ "bytes": 2115
1627
1627
  },
1628
1628
  "v2/durable-core/schemas/compiled-workflow/index.d.ts": {
1629
1629
  "sha256": "d13d2dbbcd0971f7c170788552f533c2605f26e0f352939bf5cd2348437d7519",
@@ -6,6 +6,7 @@ import { type SessionId, type RunId, type NodeId, type WorkflowHash } from '../.
6
6
  import type { Sha256PortV2 } from '../../../v2/ports/sha256.port.js';
7
7
  import type { TokenCodecPorts } from '../../../v2/durable-core/tokens/token-codec-ports.js';
8
8
  import { ResultAsync as RA } from 'neverthrow';
9
+ import { type ValidationPipelineDepsPhase1a } from '../../../application/services/workflow-validation-pipeline.js';
9
10
  import { type ObservationEventData } from '../../../v2/durable-core/domain/observation-builder.js';
10
11
  import { type StartWorkflowError } from '../v2-execution-helpers.js';
11
12
  import * as z from 'zod';
@@ -14,6 +15,7 @@ export declare function loadAndPinWorkflow(args: {
14
15
  readonly workflowService: import('../../../application/services/workflow-service.js').WorkflowService;
15
16
  readonly crypto: Sha256PortV2;
16
17
  readonly pinnedStore: import('../../../v2/ports/pinned-workflow-store.port.js').PinnedWorkflowStorePortV2;
18
+ readonly validationPipelineDeps: ValidationPipelineDepsPhase1a;
17
19
  }): RA<{
18
20
  readonly workflow: import('../../../types/workflow.js').Workflow;
19
21
  readonly workflowHash: WorkflowHash;
@@ -10,7 +10,7 @@ const workflow_js_1 = require("../../../types/workflow.js");
10
10
  const index_js_1 = require("../../../v2/durable-core/ids/index.js");
11
11
  const workflow_hash_ref_js_1 = require("../../../v2/durable-core/ids/workflow-hash-ref.js");
12
12
  const neverthrow_1 = require("neverthrow");
13
- const v1_to_v2_shim_js_1 = require("../../../v2/read-only/v1-to-v2-shim.js");
13
+ const workflow_validation_pipeline_js_1 = require("../../../application/services/workflow-validation-pipeline.js");
14
14
  const hashing_js_1 = require("../../../v2/durable-core/canonical/hashing.js");
15
15
  const observation_builder_js_1 = require("../../../v2/durable-core/domain/observation-builder.js");
16
16
  const workflow_source_js_1 = require("../../../types/workflow-source.js");
@@ -24,7 +24,7 @@ const constants_js_1 = require("../../../v2/durable-core/constants.js");
24
24
  const index_js_2 = require("./index.js");
25
25
  const start_construction_js_1 = require("../../../v2/durable-core/domain/start-construction.js");
26
26
  function loadAndPinWorkflow(args) {
27
- const { workflowId, workflowService, crypto, pinnedStore } = args;
27
+ const { workflowId, workflowService, crypto, pinnedStore, validationPipelineDeps } = args;
28
28
  return neverthrow_1.ResultAsync.fromPromise(workflowService.getWorkflowById(workflowId), (e) => ({
29
29
  kind: 'precondition_failed',
30
30
  message: e instanceof Error ? e.message : String(e),
@@ -39,14 +39,25 @@ function loadAndPinWorkflow(args) {
39
39
  return (0, neverthrow_1.okAsync)({ workflow });
40
40
  })
41
41
  .andThen(({ workflow }) => {
42
- const normalizeResult = (0, v1_to_v2_shim_js_1.normalizeV1WorkflowToPinnedSnapshot)(workflow);
43
- if (normalizeResult.isErr()) {
42
+ const pipelineOutcome = (0, workflow_validation_pipeline_js_1.validateWorkflowPhase1a)(workflow, validationPipelineDeps);
43
+ if (pipelineOutcome.kind !== 'phase1a_valid') {
44
+ const message = pipelineOutcome.kind === 'schema_failed'
45
+ ? `Schema validation failed: ${pipelineOutcome.errors.map(e => e.message ?? e.instancePath).join('; ')}`
46
+ : pipelineOutcome.kind === 'structural_failed'
47
+ ? `Structural validation failed: ${pipelineOutcome.issues.join('; ')}`
48
+ : pipelineOutcome.kind === 'v1_compilation_failed'
49
+ ? `Compilation failed: ${pipelineOutcome.cause.message}`
50
+ : pipelineOutcome.kind === 'normalization_failed'
51
+ ? `Normalization failed: ${pipelineOutcome.cause.message}`
52
+ : pipelineOutcome.kind === 'executable_compilation_failed'
53
+ ? `Executable compilation failed: ${pipelineOutcome.cause.message}`
54
+ : 'Unknown validation failure';
44
55
  return (0, neverthrow_1.errAsync)({
45
56
  kind: 'workflow_compile_failed',
46
- message: normalizeResult.error.message,
57
+ message,
47
58
  });
48
59
  }
49
- const compiled = normalizeResult.value;
60
+ const compiled = pipelineOutcome.snapshot;
50
61
  const workflowHashRes = (0, hashing_js_1.workflowHashForCompiledSnapshot)(compiled, crypto);
51
62
  if (workflowHashRes.isErr()) {
52
63
  return (0, neverthrow_1.errAsync)({ kind: 'hash_computation_failed', message: workflowHashRes.error.message });
@@ -215,12 +226,13 @@ function mintStartTokens(args) {
215
226
  });
216
227
  }
217
228
  function executeStartWorkflow(input, ctx) {
218
- const { gate, sessionStore, snapshotStore, pinnedStore, crypto, tokenCodecPorts, idFactory } = ctx.v2;
229
+ const { gate, sessionStore, snapshotStore, pinnedStore, crypto, tokenCodecPorts, idFactory, validationPipelineDeps } = ctx.v2;
219
230
  return loadAndPinWorkflow({
220
231
  workflowId: input.workflowId,
221
232
  workflowService: ctx.workflowService,
222
233
  crypto,
223
234
  pinnedStore,
235
+ validationPipelineDeps,
224
236
  })
225
237
  .andThen(({ workflow, firstStep, workflowHash, pinnedWorkflow }) => {
226
238
  const anchorsRA = (0, v2_workspace_resolution_js_1.resolveWorkspaceAnchors)(ctx.v2, input.workspacePath)
@@ -41,6 +41,8 @@ const container_js_1 = require("../di/container.js");
41
41
  const tokens_js_1 = require("../di/tokens.js");
42
42
  const assert_never_js_1 = require("../runtime/assert-never.js");
43
43
  const token_codec_ports_js_1 = require("../v2/durable-core/tokens/token-codec-ports.js");
44
+ const validation_js_1 = require("../application/validation.js");
45
+ const v1_to_v2_shim_js_1 = require("../v2/read-only/v1-to-v2-shim.js");
44
46
  const index_js_1 = require("../v2/infra/local/workspace-anchor/index.js");
45
47
  const workspace_roots_manager_js_1 = require("./workspace-roots-manager.js");
46
48
  const index_js_2 = require("../v2/infra/local/directory-listing/index.js");
@@ -94,6 +96,14 @@ async function createToolContext() {
94
96
  const dataDir = container_js_1.container.resolve(tokens_js_1.DI.V2.DataDir);
95
97
  const fsPort = container_js_1.container.resolve(tokens_js_1.DI.V2.FileSystem);
96
98
  const directoryListing = new index_js_2.LocalDirectoryListingV2(fsPort);
99
+ const validationEngine = container_js_1.container.resolve(tokens_js_1.DI.Infra.ValidationEngine);
100
+ const compiler = container_js_1.container.resolve(tokens_js_1.DI.Services.WorkflowCompiler);
101
+ const validationPipelineDeps = {
102
+ schemaValidate: validation_js_1.validateWorkflowSchema,
103
+ structuralValidate: validationEngine.validateWorkflowStructureOnly.bind(validationEngine),
104
+ compiler,
105
+ normalizeToExecutable: v1_to_v2_shim_js_1.normalizeV1WorkflowToPinnedSnapshot,
106
+ };
97
107
  v2 = {
98
108
  gate,
99
109
  sessionStore,
@@ -103,6 +113,7 @@ async function createToolContext() {
103
113
  crypto,
104
114
  idFactory,
105
115
  tokenCodecPorts,
116
+ validationPipelineDeps,
106
117
  resolvedRootUris: [],
107
118
  workspaceResolver: new index_js_1.LocalWorkspaceAnchorV2(process.cwd()),
108
119
  dataDir,
@@ -16,6 +16,7 @@ import type { WorkspaceContextResolverPortV2 } from '../v2/ports/workspace-ancho
16
16
  import type { DataDirPortV2 } from '../v2/ports/data-dir.port.js';
17
17
  import type { DirectoryListingPortV2 } from '../v2/ports/directory-listing.port.js';
18
18
  import type { SessionSummaryProviderPortV2 } from '../v2/ports/session-summary-provider.port.js';
19
+ import type { ValidationPipelineDepsPhase1a } from '../application/services/workflow-validation-pipeline.js';
19
20
  export interface SessionHealthDetails {
20
21
  readonly health: SessionHealthV2;
21
22
  }
@@ -55,6 +56,7 @@ export interface V2Dependencies {
55
56
  readonly crypto: CryptoPortV2;
56
57
  readonly idFactory: IdFactoryV2;
57
58
  readonly tokenCodecPorts: TokenCodecPorts;
59
+ readonly validationPipelineDeps: ValidationPipelineDepsPhase1a;
58
60
  readonly resolvedRootUris?: readonly string[];
59
61
  readonly workspaceResolver?: WorkspaceContextResolverPortV2;
60
62
  readonly dataDir?: DataDirPortV2;
@@ -1,4 +1,5 @@
1
1
  import type { Result } from 'neverthrow';
2
+ import type { LoopControlEvaluationResult } from './loop-control-evaluator.js';
2
3
  export type DecisionTraceEntryKind = 'selected_next_step' | 'evaluated_condition' | 'entered_loop' | 'exited_loop' | 'detected_non_tip_advance';
3
4
  export type DecisionTraceRef = {
4
5
  readonly kind: 'step_id';
@@ -22,6 +23,7 @@ export declare function traceEnteredLoop(loopId: string, iteration: number): Dec
22
23
  export declare function traceEvaluatedCondition(loopId: string, iteration: number, result: boolean, source: 'artifact' | 'context' | 'legacy'): DecisionTraceEntry;
23
24
  export declare function traceExitedLoop(loopId: string, reason: string): DecisionTraceEntry;
24
25
  export declare function traceSelectedNextStep(stepId: string): DecisionTraceEntry;
26
+ export declare function traceArtifactMatchResult(loopId: string, iteration: number, result: LoopControlEvaluationResult): DecisionTraceEntry;
25
27
  export declare function applyTraceBudget(entries: readonly DecisionTraceEntry[]): readonly DecisionTraceEntry[];
26
28
  export declare function buildDecisionTraceEventData(traceId: string, entries: readonly DecisionTraceEntry[]): Result<{
27
29
  readonly traceId: string;
@@ -4,6 +4,7 @@ exports.traceEnteredLoop = traceEnteredLoop;
4
4
  exports.traceEvaluatedCondition = traceEvaluatedCondition;
5
5
  exports.traceExitedLoop = traceExitedLoop;
6
6
  exports.traceSelectedNextStep = traceSelectedNextStep;
7
+ exports.traceArtifactMatchResult = traceArtifactMatchResult;
7
8
  exports.applyTraceBudget = applyTraceBudget;
8
9
  exports.buildDecisionTraceEventData = buildDecisionTraceEventData;
9
10
  const neverthrow_1 = require("neverthrow");
@@ -38,6 +39,20 @@ function traceSelectedNextStep(stepId) {
38
39
  refs: [{ kind: 'step_id', stepId }],
39
40
  };
40
41
  }
42
+ function traceArtifactMatchResult(loopId, iteration, result) {
43
+ const detail = (() => {
44
+ switch (result.kind) {
45
+ case 'found': return `decision="${result.decision}"`;
46
+ case 'not_found': return result.reason;
47
+ case 'invalid': return result.reason;
48
+ }
49
+ })();
50
+ return {
51
+ kind: 'evaluated_condition',
52
+ summary: `Loop '${loopId}' iteration ${iteration}: artifact match=${result.kind} — ${detail}`,
53
+ refs: [{ kind: 'loop_id', loopId }, { kind: 'iteration', value: iteration }],
54
+ };
55
+ }
41
56
  function applyTraceBudget(entries) {
42
57
  const capped = entries.slice(0, constants_js_1.MAX_DECISION_TRACE_ENTRIES);
43
58
  let totalBytes = 0;
@@ -10,4 +10,4 @@ export type LoopControlEvaluationResult = {
10
10
  readonly kind: 'invalid';
11
11
  readonly reason: string;
12
12
  };
13
- export declare function evaluateLoopControlFromArtifacts(artifacts: readonly unknown[], loopId: string): LoopControlEvaluationResult;
13
+ export declare function evaluateLoopControlFromArtifacts(artifacts: readonly unknown[]): LoopControlEvaluationResult;
@@ -2,23 +2,30 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.evaluateLoopControlFromArtifacts = evaluateLoopControlFromArtifacts;
4
4
  const index_js_1 = require("../schemas/artifacts/index.js");
5
- function evaluateLoopControlFromArtifacts(artifacts, loopId) {
5
+ function evaluateLoopControlFromArtifacts(artifacts) {
6
6
  if (artifacts.length === 0) {
7
7
  return {
8
8
  kind: 'not_found',
9
- reason: `No artifacts provided to evaluate loop control for loopId=${loopId}`,
9
+ reason: 'No artifacts provided for loop control evaluation',
10
10
  };
11
11
  }
12
- const artifact = (0, index_js_1.findLoopControlArtifact)(artifacts, loopId);
13
- if (!artifact) {
12
+ const artifact = (0, index_js_1.findLoopControlArtifact)(artifacts);
13
+ if (artifact) {
14
14
  return {
15
- kind: 'not_found',
16
- reason: `No loop control artifact found for loopId=${loopId}`,
15
+ kind: 'found',
16
+ decision: artifact.decision,
17
+ artifact,
18
+ };
19
+ }
20
+ const hasKindMatch = artifacts.some(index_js_1.isLoopControlArtifact);
21
+ if (hasKindMatch) {
22
+ return {
23
+ kind: 'invalid',
24
+ reason: 'Artifact with kind=wr.loop_control found but failed schema validation (bad loopId format, missing decision, or extra fields)',
17
25
  };
18
26
  }
19
27
  return {
20
- kind: 'found',
21
- decision: artifact.decision,
22
- artifact,
28
+ kind: 'not_found',
29
+ reason: 'No wr.loop_control artifact found',
23
30
  };
24
31
  }
@@ -114,24 +114,55 @@ function applyPromptBudget(combinedPrompt) {
114
114
  const decoder = new TextDecoder('utf-8');
115
115
  return decoder.decode(truncatedBytes) + markerText + omissionNote;
116
116
  }
117
+ function resolveParentLoopMaxIterations(workflow, stepId) {
118
+ for (const step of workflow.definition.steps) {
119
+ if ((0, workflow_js_1.isLoopStepDefinition)(step) && Array.isArray(step.body)) {
120
+ for (const bodyStep of step.body) {
121
+ if (bodyStep.id === stepId) {
122
+ return step.loop.maxIterations;
123
+ }
124
+ }
125
+ }
126
+ }
127
+ return undefined;
128
+ }
129
+ function buildScopeInstruction(iteration, maxIterations) {
130
+ if (iteration <= 1)
131
+ return 'Focus on what the first pass missed — do not re-litigate settled findings.';
132
+ if (maxIterations !== undefined && iteration + 1 >= maxIterations)
133
+ return 'FINAL PASS — verify prior amendments landed correctly. Only flag regressions or clearly missed issues.';
134
+ return 'Diminishing returns expected. Focus on gaps and regressions, not fresh territory.';
135
+ }
117
136
  function buildLoopContextBanner(args) {
118
137
  if (args.loopPath.length === 0 || args.isExitStep)
119
138
  return '';
120
139
  const current = args.loopPath[args.loopPath.length - 1];
121
- const iterationLabel = `Iteration ${current.iteration + 1}`;
122
- return [
123
- `---`,
124
- `**LOOP: ${current.loopId} | ${iterationLabel}** This step repeats intentionally; the workflow is not stuck or broken.`,
125
- ``,
126
- `Choose the instruction that matches your current task:`,
127
- `- **Drafting / updating**: incorporate amendments discovered in previous iterations before writing.`,
128
- `- **Auditing / reviewing**: look for what previous passes *missed*, not what they already caught.`,
129
- `- **Applying changes**: follow prior findings precisely; don't re-debate settled decisions.`,
130
- ``,
131
- `Prior iteration work is visible in the **Ancestry Recap** section below (if present).`,
132
- `---`,
133
- ``,
134
- ].join('\n');
140
+ const iterationNumber = current.iteration + 1;
141
+ const maxIter = args.maxIterations;
142
+ if (current.iteration === 0) {
143
+ const bound = maxIter !== undefined ? ` (up to ${maxIter} passes)` : '';
144
+ return [
145
+ `> This step is part of an iterative loop${bound}. After your work, a decision step determines whether another pass is needed.`,
146
+ ``,
147
+ ].join('\n');
148
+ }
149
+ const lines = ['---'];
150
+ if (maxIter !== undefined) {
151
+ const filled = Math.min(iterationNumber, maxIter);
152
+ const empty = Math.max(maxIter - filled, 0);
153
+ const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(empty);
154
+ lines.push(`**Progress: [${bar}] Pass ${iterationNumber} of ${maxIter}**`);
155
+ }
156
+ else {
157
+ lines.push(`**Pass ${iterationNumber}**`);
158
+ }
159
+ lines.push('');
160
+ lines.push(`**Scope**: ${buildScopeInstruction(current.iteration, maxIter)}`);
161
+ lines.push('');
162
+ lines.push('Your previous work is in the **Ancestry Recap** below. Build on it — do not repeat work already done.');
163
+ lines.push('---');
164
+ lines.push('');
165
+ return lines.join('\n');
135
166
  }
136
167
  function formatOutputContractRequirements(outputContract) {
137
168
  const contractRef = outputContract?.contractRef;
@@ -143,7 +174,8 @@ function formatOutputContractRequirements(outputContract) {
143
174
  `Artifact contract: ${index_js_2.LOOP_CONTROL_CONTRACT_REF}`,
144
175
  `Provide an artifact with kind: "wr.loop_control"`,
145
176
  `Required field: decision ("continue" | "stop")`,
146
- `Optional field: loopId — omit unless targeting a specific named loop`,
177
+ `Do NOT include loopId — the engine matches automatically`,
178
+ `Canonical format:\n\`\`\`json\n{ "artifacts": [{ "kind": "wr.loop_control", "decision": "stop" }] }\n\`\`\``,
147
179
  ];
148
180
  default:
149
181
  return [
@@ -184,7 +216,8 @@ function renderPendingPrompt(args) {
184
216
  ? step.outputContract
185
217
  : undefined;
186
218
  const isExitStep = outputContract?.contractRef === index_js_2.LOOP_CONTROL_CONTRACT_REF;
187
- const loopBanner = buildLoopContextBanner({ loopPath: args.loopPath, isExitStep });
219
+ const maxIterations = resolveParentLoopMaxIterations(args.workflow, args.stepId);
220
+ const loopBanner = buildLoopContextBanner({ loopPath: args.loopPath, isExitStep, maxIterations });
188
221
  const validationCriteria = step.validationCriteria;
189
222
  const requirements = (0, validation_requirements_extractor_js_1.extractValidationRequirements)(validationCriteria);
190
223
  const requirementsSection = requirements.length > 0
@@ -63,4 +63,4 @@ export declare const LoopControlArtifactV1Schema: z.ZodObject<{
63
63
  export type LoopControlArtifactV1 = z.infer<typeof LoopControlArtifactV1Schema>;
64
64
  export declare function isLoopControlArtifact(artifact: unknown): artifact is LoopControlArtifactV1;
65
65
  export declare function parseLoopControlArtifact(artifact: unknown): LoopControlArtifactV1 | null;
66
- export declare function findLoopControlArtifact(artifacts: readonly unknown[], expectedLoopId: string): LoopControlArtifactV1 | null;
66
+ export declare function findLoopControlArtifact(artifacts: readonly unknown[]): LoopControlArtifactV1 | null;
@@ -35,17 +35,14 @@ function parseLoopControlArtifact(artifact) {
35
35
  const result = exports.LoopControlArtifactV1Schema.safeParse(artifact);
36
36
  return result.success ? result.data : null;
37
37
  }
38
- function findLoopControlArtifact(artifacts, expectedLoopId) {
38
+ function findLoopControlArtifact(artifacts) {
39
39
  for (let i = artifacts.length - 1; i >= 0; i--) {
40
40
  const artifact = artifacts[i];
41
41
  if (!isLoopControlArtifact(artifact))
42
42
  continue;
43
43
  const parsed = parseLoopControlArtifact(artifact);
44
- if (!parsed)
45
- continue;
46
- if (parsed.loopId === undefined || parsed.loopId === expectedLoopId) {
44
+ if (parsed)
47
45
  return parsed;
48
- }
49
46
  }
50
47
  return null;
51
48
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exaudeus/workrail",
3
- "version": "1.17.0",
3
+ "version": "2.0.0",
4
4
  "description": "Step-by-step workflow enforcement for AI agents via MCP",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -105,7 +105,7 @@
105
105
  {
106
106
  "id": "phase-4b-loop-decision",
107
107
  "title": "Evidence Loop Decision",
108
- "prompt": "Decide whether the evidence loop should continue.\n\nDecision rules:\n- if `contradictionCount > 0` → continue\n- else if `unresolvedEvidenceGapCount > 0` → continue\n- else if `hasStrongAlternative = true` and the alternative is not meaningfully weaker → continue\n- else if `diagnosisType = inconclusive_but_narrowed` and further evidence is not realistically available → stop with bounded uncertainty\n- else → stop\n\nOutput exactly:\n```json\n{\n \"artifacts\": [{\n \"kind\": \"wr.loop_control\",\n \"loopId\": \"bug_evidence_loop\",\n \"decision\": \"continue\"\n }]\n}\n```",
108
+ "prompt": "Decide whether the evidence loop should continue.\n\nDecision rules:\n- if `contradictionCount > 0` → continue\n- else if `unresolvedEvidenceGapCount > 0` → continue\n- else if `hasStrongAlternative = true` and the alternative is not meaningfully weaker → continue\n- else if `diagnosisType = inconclusive_but_narrowed` and further evidence is not realistically available → stop with bounded uncertainty\n- else → stop\n\nOutput exactly:\n```json\n{\n \"artifacts\": [{\n \"kind\": \"wr.loop_control\",\n \"decision\": \"continue\"\n }]\n}\n```",
109
109
  "requireConfirmation": true,
110
110
  "outputContract": {
111
111
  "contractRef": "wr.contracts.loop_control"
@@ -139,7 +139,7 @@
139
139
  {
140
140
  "id": "phase-4c-loop-decision",
141
141
  "title": "Loop Exit Decision",
142
- "prompt": "Provide a loop control artifact.\n\nDecision rules:\n- if `planFindings` is non-empty → continue\n- if `planFindings` is empty → stop, but enumerate what was checked to justify the clean pass\n- if max iterations reached → stop and document remaining concerns\n\nOutput exactly:\n```json\n{\n \"artifacts\": [{\n \"kind\": \"wr.loop_control\",\n \"loopId\": \"plan_audit_loop\",\n \"decision\": \"continue\"\n }]\n}\n```",
142
+ "prompt": "Provide a loop control artifact.\n\nDecision rules:\n- if `planFindings` is non-empty → continue\n- if `planFindings` is empty → stop, but enumerate what was checked to justify the clean pass\n- if max iterations reached → stop and document remaining concerns\n\nOutput exactly:\n```json\n{\n \"artifacts\": [{\n \"kind\": \"wr.loop_control\",\n \"decision\": \"continue\"\n }]\n}\n```",
143
143
  "requireConfirmation": true,
144
144
  "outputContract": {
145
145
  "contractRef": "wr.contracts.loop_control"
@@ -188,7 +188,7 @@
188
188
  {
189
189
  "id": "phase-4c-loop-decision",
190
190
  "title": "Synthesis Loop Decision",
191
- "prompt": "Decide whether the synthesis loop should continue.\n\nDecision rules:\n- if `contradictionCount > 0` → continue\n- else if `coverageUncertainCount > 0` and the uncertainty materially affects the recommendation → continue\n- else if `falsePositiveRiskCount > 0` → continue\n- else if `recommendationDriftDetected = true` → continue\n- else → stop\n\nOutput exactly:\n```json\n{\n \"artifacts\": [{\n \"kind\": \"wr.loop_control\",\n \"loopId\": \"mr_review_synthesis_loop\",\n \"decision\": \"continue\"\n }]\n}\n```",
191
+ "prompt": "Decide whether the synthesis loop should continue.\n\nDecision rules:\n- if `contradictionCount > 0` → continue\n- else if `coverageUncertainCount > 0` and the uncertainty materially affects the recommendation → continue\n- else if `falsePositiveRiskCount > 0` → continue\n- else if `recommendationDriftDetected = true` → continue\n- else → stop\n\nOutput exactly:\n```json\n{\n \"artifacts\": [{\n \"kind\": \"wr.loop_control\",\n \"decision\": \"continue\"\n }]\n}\n```",
192
192
  "requireConfirmation": true,
193
193
  "outputContract": {
194
194
  "contractRef": "wr.contracts.loop_control"
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "id": "init-loop",
15
15
  "title": "Initialize Loop",
16
- "prompt": "Initialize the bounded iteration loop.\n\n**Provide a loop control artifact:**\n```json\n{\n \"artifacts\": [{\n \"kind\": \"wr.loop_control\",\n \"loopId\": \"test-iteration\",\n \"decision\": \"continue\",\n \"metadata\": {\n \"reason\": \"Starting iteration loop\"\n }\n }]\n}\n```\n\nThe artifact must have:\n- kind: 'wr.loop_control'\n- loopId: 'test-iteration' (lowercase with hyphens)\n- decision: 'continue' or 'stop'",
16
+ "prompt": "Initialize the bounded iteration loop.\n\n**Provide a loop control artifact:**\n```json\n{\n \"artifacts\": [{\n \"kind\": \"wr.loop_control\",\n \"decision\": \"continue\",\n \"metadata\": {\n \"reason\": \"Starting iteration loop\"\n }\n }]\n}\n```\n\nThe artifact must have:\n- kind: 'wr.loop_control'\n- decision: 'continue' or 'stop'",
17
17
  "requireConfirmation": false,
18
18
  "outputContract": {
19
19
  "contractRef": "wr.contracts.loop_control"
@@ -42,7 +42,7 @@
42
42
  {
43
43
  "id": "decide-continue",
44
44
  "title": "Loop Decision",
45
- "prompt": "Decide whether to continue or stop the loop.\n\n**Provide a loop control artifact:**\n```json\n{\n \"artifacts\": [{\n \"kind\": \"wr.loop_control\",\n \"loopId\": \"test-iteration\",\n \"decision\": \"continue\",\n \"metadata\": {\n \"reason\": \"More work needed\",\n \"iterationIndex\": 1\n }\n }]\n}\n```\n\n**Decision logic:**\n- If more work is needed: decision = 'continue'\n- If work is complete: decision = 'stop'\n\nThe artifact must have:\n- kind: 'wr.loop_control'\n- loopId: 'test-iteration'\n- decision: 'continue' or 'stop'",
45
+ "prompt": "Decide whether to continue or stop the loop.\n\n**Provide a loop control artifact:**\n```json\n{\n \"artifacts\": [{\n \"kind\": \"wr.loop_control\",\n \"decision\": \"continue\",\n \"metadata\": {\n \"reason\": \"More work needed\",\n \"iterationIndex\": 1\n }\n }]\n}\n```\n\n**Decision logic:**\n- If more work is needed: decision = 'continue'\n- If work is complete: decision = 'stop'\n\nThe artifact must have:\n- kind: 'wr.loop_control'\n- decision: 'continue' or 'stop'",
46
46
  "requireConfirmation": true,
47
47
  "outputContract": {
48
48
  "contractRef": "wr.contracts.loop_control"
@@ -89,7 +89,7 @@
89
89
  {
90
90
  "id": "discovery-loop-decision",
91
91
  "title": "Discovery Loop Decision",
92
- "prompt": "**Provide a loop control decision artifact.**\n\nBased on the discovery analysis above, decide whether to continue gathering requirements or proceed to synthesis.\n\n**Decision logic:**\n- If critical information is still missing (problem unclear, user needs undefined, success criteria absent): decision = 'continue'\n- If discovery is 80%+ complete (clear problem, user needs, success criteria, basic constraints): decision = 'stop'\n\n**Output (exact format):**\nProvide a loop control artifact:\n```json\n{\n \"artifacts\": [{\n \"kind\": \"wr.loop_control\",\n \"loopId\": \"phase-0-discovery-loop\",\n \"decision\": \"continue\",\n \"metadata\": {\n \"reason\": \"One sentence explaining why continuing or stopping\"\n }\n }]\n}\n```",
92
+ "prompt": "**Provide a loop control decision artifact.**\n\nBased on the discovery analysis above, decide whether to continue gathering requirements or proceed to synthesis.\n\n**Decision logic:**\n- If critical information is still missing (problem unclear, user needs undefined, success criteria absent): decision = 'continue'\n- If discovery is 80%+ complete (clear problem, user needs, success criteria, basic constraints): decision = 'stop'\n\n**Output (exact format):**\nProvide a loop control artifact:\n```json\n{\n \"artifacts\": [{\n \"kind\": \"wr.loop_control\",\n \"decision\": \"continue\",\n \"metadata\": {\n \"reason\": \"One sentence explaining why continuing or stopping\"\n }\n }]\n}\n```",
93
93
  "agentRole": "You are a discovery analyst making an explicit go/no-go decision on whether enough requirements have been gathered.",
94
94
  "requireConfirmation": false,
95
95
  "outputContract": {
@@ -418,7 +418,7 @@
418
418
  {
419
419
  "id": "satisfaction-check",
420
420
  "title": "Iteration Satisfaction Check & Loop Decision",
421
- "prompt": "Let's assess your satisfaction with the workflow so far.\n\n**Rate your satisfaction (1-10):**\n- 10: Perfect! Ready to deploy\n- 8-9: Very good, minor tweaks only\n- 6-7: Good foundation, needs refinement\n- 4-5: Major improvements needed\n- 1-3: Significant rework required\n\n**Decision logic:**\n- Score 9+: decision = 'stop' (proceed to completion)\n- Score <9: decision = 'continue' (iterate again, up to 3 times total)\n\nAgent: Set context.satisfactionScore and increment context.iterationCount.\n\n**Provide a loop control artifact:**\n```json\n{\n \"artifacts\": [{\n \"kind\": \"wr.loop_control\",\n \"loopId\": \"phase-3-4-refinement-loop\",\n \"decision\": \"continue\",\n \"metadata\": {\n \"reason\": \"Satisfaction score X/10 - [brief reason]\"\n }\n }]\n}\n```",
421
+ "prompt": "Let's assess your satisfaction with the workflow so far.\n\n**Rate your satisfaction (1-10):**\n- 10: Perfect! Ready to deploy\n- 8-9: Very good, minor tweaks only\n- 6-7: Good foundation, needs refinement\n- 4-5: Major improvements needed\n- 1-3: Significant rework required\n\n**Decision logic:**\n- Score 9+: decision = 'stop' (proceed to completion)\n- Score <9: decision = 'continue' (iterate again, up to 3 times total)\n\nAgent: Set context.satisfactionScore and increment context.iterationCount.\n\n**Provide a loop control artifact:**\n```json\n{\n \"artifacts\": [{\n \"kind\": \"wr.loop_control\",\n \"decision\": \"continue\",\n \"metadata\": {\n \"reason\": \"Satisfaction score X/10 - [brief reason]\"\n }\n }]\n}\n```",
422
422
  "agentRole": "You are a quality assessment specialist. Guide the user through evaluating their workflow objectively. Provide a loop control artifact with the decision.",
423
423
  "requireConfirmation": false,
424
424
  "outputContract": {