@exaudeus/workrail 3.73.2 → 3.74.1

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.
@@ -238,8 +238,8 @@
238
238
  "bytes": 31
239
239
  },
240
240
  "cli-worktrain.js": {
241
- "sha256": "ab9815d8a2a571ac8fc4923eb3c1f51fc6e096403d3b75a80cd12d0bfe1858f2",
242
- "bytes": 60401
241
+ "sha256": "fe38aca6a553491f3c9e0bd916cedbcd6a58d5cce230b1f47a52066f0b622851",
242
+ "bytes": 67617
243
243
  },
244
244
  "cli.d.ts": {
245
245
  "sha256": "43e818adf60173644896298637f47b01d5819b17eda46eaa32d0c7d64724d012",
@@ -473,8 +473,8 @@
473
473
  "sha256": "5fe866e54f796975dec5d8ba9983aefd86074db212d3fccd64eed04bc9f0b3da",
474
474
  "bytes": 8011
475
475
  },
476
- "console-ui/assets/index-CfI4I3OX.js": {
477
- "sha256": "899505f097e5252239bf07f156eb1affe5c94ecdf8c97f23388fd713565bdaa8",
476
+ "console-ui/assets/index-BmDxs-a5.js": {
477
+ "sha256": "8090617babfe49eb6f4b313ee45d13d862eba3ba8c9714ef805cda33918e4f08",
478
478
  "bytes": 768234
479
479
  },
480
480
  "console-ui/assets/index-DHrKiMCf.css": {
@@ -482,7 +482,7 @@
482
482
  "bytes": 60673
483
483
  },
484
484
  "console-ui/index.html": {
485
- "sha256": "958b1601100f528545fcd2b4fe9cf4580bfd0a7977f826fce6593af47f90c15d",
485
+ "sha256": "d7ca1e4d2950572216e245c8a6a5130f8393ed3c961f067d2320202295b18183",
486
486
  "bytes": 417
487
487
  },
488
488
  "console/standalone-console.d.ts": {
@@ -574,8 +574,8 @@
574
574
  "bytes": 1198
575
575
  },
576
576
  "coordinators/pr-review.d.ts": {
577
- "sha256": "d46e4923995a0b43aefee25da298b86235fae0ad105e548b3174c0eea9c1f8d0",
578
- "bytes": 3947
577
+ "sha256": "0dba830dd29cd82c58300ca9fdfb4c29d0acd0b257740ce3e65f2360239a106b",
578
+ "bytes": 4501
579
579
  },
580
580
  "coordinators/pr-review.js": {
581
581
  "sha256": "385baa9e6252dbd84060bb423ce219884d519752f4a6e9f8f04e5f503fa38b67",
@@ -589,6 +589,14 @@
589
589
  "sha256": "195953d6c0e28d749407e5e3fb29b963cf14629c03508792a44bf0866f7c9d33",
590
590
  "bytes": 1815
591
591
  },
592
+ "coordinators/types.d.ts": {
593
+ "sha256": "3043520258033bcdf5801ce8af81c8fa01c04573fc158a847addd8ec22f992e7",
594
+ "bytes": 393
595
+ },
596
+ "coordinators/types.js": {
597
+ "sha256": "d43aa81f5bc89faa359e0f97c814ba25155591ff078fbb9bfd40f8c7c9683230",
598
+ "bytes": 77
599
+ },
592
600
  "core/error-handler.d.ts": {
593
601
  "sha256": "80451f12ac8e185133ec3dc4c57285491a785f27525ed21e729db1da3f61010d",
594
602
  "bytes": 1368
@@ -1786,8 +1794,8 @@
1786
1794
  "bytes": 854
1787
1795
  },
1788
1796
  "trigger/coordinator-deps.js": {
1789
- "sha256": "ade7810cf782d88e58074e75a937de08b350ff2ffae533b563fb37753c7ef49c",
1790
- "bytes": 15048
1797
+ "sha256": "1b400abbd6158e900d4559c74b9f3069a46b60fd02d046f36e30cc40a58acb51",
1798
+ "bytes": 22626
1791
1799
  },
1792
1800
  "trigger/delivery-action.d.ts": {
1793
1801
  "sha256": "bba98a08e35653304b604cd3ec126374cb731620db27ee2c8d6782d5b5b31207",
@@ -45,6 +45,102 @@ const infra_js_1 = require("../context-assembly/infra.js");
45
45
  function createCoordinatorDeps(deps) {
46
46
  const { ctx, execFileAsync, consoleService } = deps;
47
47
  let dispatch = null;
48
+ async function fetchAgentResult(sessionHandle) {
49
+ const emptyResult = { recapMarkdown: null, artifacts: [] };
50
+ if (consoleService === null) {
51
+ return emptyResult;
52
+ }
53
+ try {
54
+ const detailResult = await consoleService.getSessionDetail(sessionHandle);
55
+ if (detailResult.isErr())
56
+ return emptyResult;
57
+ const run = detailResult.value.runs[0];
58
+ if (!run)
59
+ return emptyResult;
60
+ const tipNodeId = run.preferredTipNodeId;
61
+ if (!tipNodeId)
62
+ return emptyResult;
63
+ const allNodeIds = run.nodes
64
+ .map((n) => n.nodeId)
65
+ .filter((id) => typeof id === 'string' && id !== '');
66
+ const nodeIdsToFetch = allNodeIds.length > 0 ? allNodeIds : [tipNodeId];
67
+ let recap = null;
68
+ const collectedArtifacts = [];
69
+ for (const nodeId of nodeIdsToFetch) {
70
+ try {
71
+ const nodeResult = await consoleService.getNodeDetail(sessionHandle, nodeId);
72
+ if (nodeResult.isErr())
73
+ continue;
74
+ if (nodeId === tipNodeId)
75
+ recap = nodeResult.value.recapMarkdown;
76
+ if (nodeResult.value.artifacts.length > 0)
77
+ collectedArtifacts.push(...nodeResult.value.artifacts);
78
+ }
79
+ catch {
80
+ continue;
81
+ }
82
+ }
83
+ return { recapMarkdown: recap, artifacts: collectedArtifacts };
84
+ }
85
+ catch (e) {
86
+ const msg = e instanceof Error ? e.message : String(e);
87
+ process.stderr.write(`[WARN coord:reason=exception handle=${sessionHandle.slice(0, 16)}] fetchAgentResult: ${msg}\n`);
88
+ return emptyResult;
89
+ }
90
+ }
91
+ async function fetchChildSessionResult(handle, coordinatorSessionId) {
92
+ if (consoleService === null) {
93
+ process.stderr.write(`[WARN coord:reason=await_degraded handle=${handle.slice(0, 16)}${coordinatorSessionId ? ' parent=' + coordinatorSessionId.slice(0, 16) : ''}] fetchChildSessionResult: ConsoleService unavailable\n`);
94
+ return {
95
+ kind: 'await_degraded',
96
+ message: 'ConsoleService unavailable -- cannot read child session outcome',
97
+ };
98
+ }
99
+ let runStatus = null;
100
+ try {
101
+ const detailResult = await consoleService.getSessionDetail(handle);
102
+ if (detailResult.isErr()) {
103
+ process.stderr.write(`[WARN coord:reason=getSessionDetail_failed handle=${handle.slice(0, 16)}] fetchChildSessionResult: ${String(detailResult.error)}\n`);
104
+ return {
105
+ kind: 'failed',
106
+ reason: 'error',
107
+ message: `Could not read session detail: ${String(detailResult.error)}`,
108
+ };
109
+ }
110
+ const run = detailResult.value.runs[0];
111
+ runStatus = run?.status ?? null;
112
+ }
113
+ catch (e) {
114
+ const msg = e instanceof Error ? e.message : String(e);
115
+ process.stderr.write(`[WARN coord:reason=exception handle=${handle.slice(0, 16)}] fetchChildSessionResult getSessionDetail: ${msg}\n`);
116
+ return { kind: 'failed', reason: 'error', message: `Exception reading session detail: ${msg}` };
117
+ }
118
+ if (runStatus === 'complete' || runStatus === 'complete_with_gaps') {
119
+ const agentResult = await fetchAgentResult(handle);
120
+ return {
121
+ kind: 'success',
122
+ notes: agentResult.recapMarkdown,
123
+ artifacts: agentResult.artifacts,
124
+ };
125
+ }
126
+ if (runStatus === 'blocked') {
127
+ return {
128
+ kind: 'failed',
129
+ reason: 'stuck',
130
+ message: `Child session ${handle.slice(0, 16)} reached blocked state`,
131
+ };
132
+ }
133
+ if (runStatus === null) {
134
+ return {
135
+ kind: 'timed_out',
136
+ message: `Child session ${handle.slice(0, 16)} has no terminal run status (likely timed out)`,
137
+ };
138
+ }
139
+ return {
140
+ kind: 'timed_out',
141
+ message: `Child session ${handle.slice(0, 16)} is still in state '${runStatus}' -- awaitSessions may not have been called`,
142
+ };
143
+ }
48
144
  return {
49
145
  setDispatch(fn) {
50
146
  if (dispatch !== null) {
@@ -53,11 +149,16 @@ function createCoordinatorDeps(deps) {
53
149
  }
54
150
  dispatch = fn;
55
151
  },
56
- spawnSession: async (workflowId, goal, workspace, context, agentConfig) => {
152
+ spawnSession: async (workflowId, goal, workspace, context, agentConfig, parentSessionId) => {
57
153
  if (dispatch === null) {
58
154
  return { kind: 'err', error: 'in-process router not initialized -- coordinator deps not ready' };
59
155
  }
60
- const startResult = await (0, start_js_1.executeStartWorkflow)({ workflowId, workspacePath: workspace, goal }, ctx, { is_autonomous: 'true', workspacePath: workspace, triggerSource: 'daemon' });
156
+ const startResult = await (0, start_js_1.executeStartWorkflow)({ workflowId, workspacePath: workspace, goal }, ctx, {
157
+ is_autonomous: 'true',
158
+ workspacePath: workspace,
159
+ triggerSource: 'daemon',
160
+ ...(parentSessionId !== undefined ? { parentSessionId } : {}),
161
+ });
61
162
  if (startResult.isErr()) {
62
163
  const detail = `${startResult.error.kind}${'message' in startResult.error ? ': ' + startResult.error.message : ''}`;
63
164
  return { kind: 'err', error: `Session creation failed: ${detail}` };
@@ -173,47 +274,113 @@ function createCoordinatorDeps(deps) {
173
274
  };
174
275
  },
175
276
  getAgentResult: async (sessionHandle) => {
176
- const emptyResult = { recapMarkdown: null, artifacts: [] };
177
- if (consoleService === null) {
178
- return emptyResult;
277
+ return fetchAgentResult(sessionHandle);
278
+ },
279
+ getChildSessionResult: async (handle, coordinatorSessionId) => {
280
+ return fetchChildSessionResult(handle, coordinatorSessionId);
281
+ },
282
+ spawnAndAwait: async (workflowId, goal, workspace, opts) => {
283
+ const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
284
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
285
+ const coordinatorSessionId = opts?.coordinatorSessionId;
286
+ const agentConfig = opts?.agentConfig;
287
+ if (dispatch === null) {
288
+ return {
289
+ kind: 'failed',
290
+ reason: 'error',
291
+ message: 'spawnAndAwait: in-process router not initialized (setDispatch not called)',
292
+ };
179
293
  }
180
- try {
181
- const detailResult = await consoleService.getSessionDetail(sessionHandle);
182
- if (detailResult.isErr())
183
- return emptyResult;
184
- const run = detailResult.value.runs[0];
185
- if (!run)
186
- return emptyResult;
187
- const tipNodeId = run.preferredTipNodeId;
188
- if (!tipNodeId)
189
- return emptyResult;
190
- const allNodeIds = run.nodes.map((n) => n.nodeId).filter((id) => typeof id === 'string' && id !== '');
191
- const nodeIdsToFetch = allNodeIds.length > 0 ? allNodeIds : [tipNodeId];
192
- let recap = null;
193
- const collectedArtifacts = [];
194
- for (const nodeId of nodeIdsToFetch) {
195
- try {
196
- const nodeResult = await consoleService.getNodeDetail(sessionHandle, nodeId);
197
- if (nodeResult.isErr())
198
- continue;
199
- if (nodeId === tipNodeId) {
200
- recap = nodeResult.value.recapMarkdown;
294
+ const startResult = await (0, start_js_1.executeStartWorkflow)({ workflowId, workspacePath: workspace, goal }, ctx, {
295
+ is_autonomous: 'true',
296
+ workspacePath: workspace,
297
+ triggerSource: 'daemon',
298
+ ...(coordinatorSessionId !== undefined ? { parentSessionId: coordinatorSessionId } : {}),
299
+ });
300
+ if (startResult.isErr()) {
301
+ const detail = `${startResult.error.kind}${'message' in startResult.error ? ': ' + startResult.error.message : ''}`;
302
+ return { kind: 'failed', reason: 'error', message: `Session creation failed: ${detail}` };
303
+ }
304
+ const startContinueToken = startResult.value.response.continueToken;
305
+ let handle;
306
+ if (!startContinueToken) {
307
+ handle = workflowId;
308
+ }
309
+ else {
310
+ const tokenResult = await (0, v2_token_ops_js_1.parseContinueTokenOrFail)(startContinueToken, ctx.v2.tokenCodecPorts, ctx.v2.tokenAliasStore);
311
+ if (tokenResult.isErr()) {
312
+ return {
313
+ kind: 'failed',
314
+ reason: 'error',
315
+ message: `Internal error: could not extract session handle from new session: ${tokenResult.error.message}`,
316
+ };
317
+ }
318
+ handle = tokenResult.value.sessionId;
319
+ const trigger = {
320
+ workflowId,
321
+ goal,
322
+ workspacePath: workspace,
323
+ ...(agentConfig !== undefined ? { agentConfig } : {}),
324
+ };
325
+ const r = startResult.value.response;
326
+ const allocatedSession = {
327
+ continueToken: r.continueToken ?? '',
328
+ checkpointToken: r.checkpointToken,
329
+ firstStepPrompt: r.pending?.prompt ?? '',
330
+ isComplete: r.isComplete,
331
+ triggerSource: 'daemon',
332
+ };
333
+ const source = {
334
+ kind: 'pre_allocated',
335
+ trigger,
336
+ session: allocatedSession,
337
+ };
338
+ dispatch(trigger, source);
339
+ }
340
+ const awaitResult = await (async () => {
341
+ const POLL_INTERVAL_MS = 3000;
342
+ if (consoleService === null) {
343
+ return null;
344
+ }
345
+ const startMs = Date.now();
346
+ const pending = new Set([handle]);
347
+ while (pending.size > 0) {
348
+ const elapsed = Date.now() - startMs;
349
+ if (elapsed >= timeoutMs)
350
+ break;
351
+ for (const h of [...pending]) {
352
+ try {
353
+ const detail = await consoleService.getSessionDetail(h);
354
+ if (detail.isErr())
355
+ continue;
356
+ const run = detail.value.runs[0];
357
+ if (!run)
358
+ continue;
359
+ const status = run.status;
360
+ if (status === 'complete' || status === 'complete_with_gaps') {
361
+ pending.delete(h);
362
+ }
363
+ else if (status === 'blocked') {
364
+ pending.delete(h);
365
+ }
201
366
  }
202
- if (nodeResult.value.artifacts.length > 0) {
203
- collectedArtifacts.push(...nodeResult.value.artifacts);
367
+ catch {
368
+ pending.delete(h);
204
369
  }
205
370
  }
206
- catch {
207
- continue;
371
+ if (pending.size > 0) {
372
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
208
373
  }
209
374
  }
210
- return { recapMarkdown: recap, artifacts: collectedArtifacts };
211
- }
212
- catch (e) {
213
- const msg = e instanceof Error ? e.message : String(e);
214
- process.stderr.write(`[WARN coord:reason=exception handle=${sessionHandle.slice(0, 16)}] getAgentResult: ${msg}\n`);
215
- return emptyResult;
375
+ return handle;
376
+ })();
377
+ if (awaitResult === null) {
378
+ return {
379
+ kind: 'await_degraded',
380
+ message: 'ConsoleService unavailable -- cannot await child session outcome',
381
+ };
216
382
  }
383
+ return fetchChildSessionResult(handle, coordinatorSessionId);
217
384
  },
218
385
  listOpenPRs: async (workspace) => {
219
386
  try {
package/docs/authoring.md CHANGED
@@ -761,6 +761,28 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
761
761
 
762
762
 
763
763
  ## Artifacts and planning surfaces
764
+ ### coordinator-result-artifact-schema
765
+ - **Level**: required
766
+ - **Status**: active
767
+ - **Scope**: artifact.coordinator-result
768
+ - **Rule**: When a workflow step signals coordinator phase completion, emit a `wr.coordinator_result` artifact with exactly 4 fields: `outcome` (enum: success|failed|timed_out|await_degraded), `summary` (string), `sessionId` (string), `error` (string|null). No additional fields allowed.
769
+ - **Why**: Coordinators read this artifact to determine whether to proceed, retry, or escalate. Extra fields pollute the schema boundary and break forward compatibility. The 4-field constraint is a hard limit, not a guideline.
770
+ - **Enforced by**: advisory
771
+
772
+ **Checks**
773
+ - Exactly 4 fields present: outcome, summary, sessionId, error.
774
+ - outcome is one of: success, failed, timed_out, await_degraded.
775
+ - error is string|null -- null when outcome is success, non-null string when outcome is failed.
776
+ - No workflow-specific fields (prUrl, branchName, commitSha, etc.) in wr.coordinator_result. Those belong in workflow-specific artifacts.
777
+
778
+ **Anti-patterns**
779
+ - Adding prUrl, branchName, or commitSha to wr.coordinator_result
780
+ - Using a free-form notes string instead of the typed outcome enum
781
+ - Omitting sessionId (required for coordinator tracing and console parent-child display)
782
+
783
+ **Source refs**
784
+ - `src/coordinators/types.ts` (runtime) — ChildSessionResult discriminated union -- the runtime type that wr.coordinator_result maps to.
785
+
764
786
  ### artifact-canonicality
765
787
  - **Level**: recommended
766
788
  - **Status**: active
@@ -913,6 +935,7 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
913
935
  - `artifact.plan`: Implementation-planning artifacts
914
936
  - `artifact.spec`: Behavior/specification artifacts
915
937
  - `artifact.verification`: Verification or handoff artifacts
938
+ - `artifact.coordinator-result`: wr.coordinator_result artifact emitted by coordinator-phase workflows to signal phase completion to the coordinator
916
939
  - `delegation.context-packet`: Structured context passed to subagents
917
940
  - `delegation.result-envelope`: Structured result shape returned by subagents
918
941
  - `legacy.patterns`: Older authoring patterns that should now be discouraged or avoided