@exaudeus/workrail 3.32.0 → 3.33.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 (77) hide show
  1. package/dist/cli/commands/index.d.ts +1 -0
  2. package/dist/cli/commands/index.js +3 -1
  3. package/dist/cli/commands/worktrain-await.js +11 -9
  4. package/dist/cli/commands/worktrain-daemon-install.d.ts +35 -0
  5. package/dist/cli/commands/worktrain-daemon-install.js +291 -0
  6. package/dist/cli/commands/worktrain-daemon.d.ts +31 -0
  7. package/dist/cli/commands/worktrain-daemon.js +272 -0
  8. package/dist/cli/commands/worktrain-spawn.js +11 -9
  9. package/dist/cli-worktrain.js +329 -0
  10. package/dist/cli.js +1 -22
  11. package/dist/console/standalone-console.d.ts +28 -0
  12. package/dist/console/standalone-console.js +142 -0
  13. package/dist/{console/assets/index-Cb_LO718.js → console-ui/assets/index-BuJFLLfY.js} +1 -1
  14. package/dist/{console → console-ui}/index.html +1 -1
  15. package/dist/daemon/agent-loop.d.ts +26 -0
  16. package/dist/daemon/agent-loop.js +39 -1
  17. package/dist/daemon/daemon-events.d.ts +47 -1
  18. package/dist/daemon/workflow-runner.d.ts +3 -2
  19. package/dist/daemon/workflow-runner.js +205 -41
  20. package/dist/infrastructure/session/HttpServer.js +133 -34
  21. package/dist/manifest.json +118 -62
  22. package/dist/mcp/output-schemas.d.ts +30 -30
  23. package/dist/mcp/transports/bridge-events.d.ts +4 -0
  24. package/dist/mcp/transports/fatal-exit.js +4 -0
  25. package/dist/mcp/transports/http-entry.js +2 -0
  26. package/dist/mcp/transports/stdio-entry.js +26 -6
  27. package/dist/mcp/v2/tools.d.ts +4 -4
  28. package/dist/trigger/adapters/github-poller.d.ts +44 -0
  29. package/dist/trigger/adapters/github-poller.js +190 -0
  30. package/dist/trigger/adapters/gitlab-poller.d.ts +27 -0
  31. package/dist/trigger/adapters/gitlab-poller.js +81 -0
  32. package/dist/trigger/index.d.ts +4 -1
  33. package/dist/trigger/index.js +5 -1
  34. package/dist/trigger/polled-event-store.d.ts +22 -0
  35. package/dist/trigger/polled-event-store.js +173 -0
  36. package/dist/trigger/polling-scheduler.d.ts +20 -0
  37. package/dist/trigger/polling-scheduler.js +249 -0
  38. package/dist/trigger/trigger-listener.d.ts +3 -0
  39. package/dist/trigger/trigger-listener.js +47 -3
  40. package/dist/trigger/trigger-store.js +114 -33
  41. package/dist/trigger/types.d.ts +17 -1
  42. package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +224 -224
  43. package/dist/v2/durable-core/schemas/session/events.d.ts +42 -42
  44. package/dist/v2/durable-core/schemas/session/manifest.d.ts +6 -6
  45. package/dist/v2/durable-core/schemas/session/validation-event.d.ts +2 -2
  46. package/dist/v2/durable-core/tokens/payloads.d.ts +52 -52
  47. package/dist/v2/usecases/console-routes.js +3 -3
  48. package/dist/v2/usecases/console-service.js +133 -9
  49. package/dist/v2/usecases/console-types.d.ts +7 -0
  50. package/docs/design/daemon-conversation-logging-plan.md +98 -0
  51. package/docs/design/daemon-conversation-logging-review.md +55 -0
  52. package/docs/design/daemon-conversation-logging.md +129 -0
  53. package/docs/design/github-polling-adapter-design-candidates.md +226 -0
  54. package/docs/design/github-polling-adapter-design-review-findings.md +131 -0
  55. package/docs/design/github-polling-adapter-implementation-plan.md +284 -0
  56. package/docs/design/implementation_plan.md +192 -0
  57. package/docs/design/workflow-id-validation-at-startup.md +146 -0
  58. package/docs/design/workflow-id-validation-design-review.md +87 -0
  59. package/docs/design/workflow-id-validation-implementation-plan.md +185 -0
  60. package/docs/design/worktrain-system-prompt-report-issue-candidates.md +135 -0
  61. package/docs/design/worktrain-system-prompt-report-issue-design-review.md +73 -0
  62. package/docs/ideas/backlog.md +361 -0
  63. package/package.json +1 -1
  64. package/workflows/architecture-scalability-audit.json +1 -1
  65. package/workflows/bug-investigation.agentic.v2.json +3 -3
  66. package/workflows/coding-task-workflow-agentic.json +32 -32
  67. package/workflows/coding-task-workflow-agentic.lean.v2.json +1 -1
  68. package/workflows/coding-task-workflow-agentic.v2.json +7 -7
  69. package/workflows/mr-review-workflow.agentic.v2.json +21 -12
  70. package/workflows/personal-learning-materials-creation-branched.json +2 -2
  71. package/workflows/production-readiness-audit.json +1 -1
  72. package/workflows/relocation-workflow-us.json +2 -2
  73. package/workflows/ui-ux-design-workflow.json +14 -14
  74. package/workflows/workflow-for-workflows.json +3 -3
  75. package/workflows/workflow-for-workflows.v2.json +2 -2
  76. package/workflows/wr.discovery.json +1 -1
  77. /package/dist/{console → console-ui}/assets/index-8dh0Psu-.css +0 -0
@@ -43,7 +43,7 @@ class AgentLoop {
43
43
  }
44
44
  }
45
45
  async _runLoop() {
46
- const { client, modelId, systemPrompt, tools, maxTokens = 8192 } = this._options;
46
+ const { client, modelId, systemPrompt, tools, maxTokens = 8192, callbacks } = this._options;
47
47
  while (true) {
48
48
  if (this._aborted || this._abortController.signal.aborted) {
49
49
  this._appendErrorMessage('aborted');
@@ -56,6 +56,10 @@ class AgentLoop {
56
56
  description: t.description,
57
57
  input_schema: t.inputSchema,
58
58
  }));
59
+ try {
60
+ callbacks?.onLlmTurnStarted?.({ messageCount: apiMessages.length });
61
+ }
62
+ catch { }
59
63
  let response;
60
64
  try {
61
65
  response = await client.messages.create({
@@ -74,6 +78,20 @@ class AgentLoop {
74
78
  await this._emitEvent({ type: 'agent_end' });
75
79
  return;
76
80
  }
81
+ {
82
+ const toolNamesRequested = response.content
83
+ .filter((block) => block.type === 'tool_use')
84
+ .map((block) => block.name);
85
+ try {
86
+ callbacks?.onLlmTurnCompleted?.({
87
+ stopReason: response.stop_reason ?? 'unknown',
88
+ outputTokens: response.usage.output_tokens,
89
+ inputTokens: response.usage.input_tokens,
90
+ toolNamesRequested,
91
+ });
92
+ }
93
+ catch { }
94
+ }
77
95
  const stopReason = this._mapStopReason(response.stop_reason);
78
96
  const assistantMsg = {
79
97
  role: 'assistant',
@@ -116,6 +134,7 @@ class AgentLoop {
116
134
  }
117
135
  }
118
136
  async _executeTools(toolUseBlocks) {
137
+ const { callbacks } = this._options;
119
138
  const results = [];
120
139
  for (const block of toolUseBlocks) {
121
140
  if (this._abortController.signal.aborted) {
@@ -141,12 +160,23 @@ class AgentLoop {
141
160
  continue;
142
161
  }
143
162
  const params = (block.input ?? {});
163
+ const argsSummary = JSON.stringify(params).slice(0, 200);
164
+ try {
165
+ callbacks?.onToolCallStarted?.({ toolName: block.name, argsSummary });
166
+ }
167
+ catch { }
168
+ const toolStartMs = Date.now();
144
169
  let result;
145
170
  try {
146
171
  result = await tool.execute(block.id, params);
147
172
  }
148
173
  catch (err) {
174
+ const durationMs = Date.now() - toolStartMs;
149
175
  const message = err instanceof Error ? err.message : String(err);
176
+ try {
177
+ callbacks?.onToolCallFailed?.({ toolName: block.name, durationMs, errorMessage: message.slice(0, 200) });
178
+ }
179
+ catch { }
150
180
  results.push({
151
181
  toolCallId: block.id,
152
182
  toolName: block.name,
@@ -155,6 +185,14 @@ class AgentLoop {
155
185
  });
156
186
  continue;
157
187
  }
188
+ {
189
+ const durationMs = Date.now() - toolStartMs;
190
+ const resultSummary = (result.content[0]?.text ?? '(no output)').slice(0, 200);
191
+ try {
192
+ callbacks?.onToolCallCompleted?.({ toolName: block.name, durationMs, resultSummary });
193
+ }
194
+ catch { }
195
+ }
158
196
  results.push({
159
197
  toolCallId: block.id,
160
198
  toolName: block.name,
@@ -18,22 +18,26 @@ export interface SessionStartedEvent {
18
18
  readonly sessionId: string;
19
19
  readonly workflowId: string;
20
20
  readonly workspacePath: string;
21
+ readonly workrailSessionId?: string;
21
22
  }
22
23
  export interface ToolCalledEvent {
23
24
  readonly kind: 'tool_called';
24
25
  readonly sessionId: string;
25
26
  readonly toolName: string;
26
27
  readonly summary?: string;
28
+ readonly workrailSessionId?: string;
27
29
  }
28
30
  export interface ToolErrorEvent {
29
31
  readonly kind: 'tool_error';
30
32
  readonly sessionId: string;
31
33
  readonly toolName: string;
32
34
  readonly error: string;
35
+ readonly workrailSessionId?: string;
33
36
  }
34
37
  export interface StepAdvancedEvent {
35
38
  readonly kind: 'step_advanced';
36
39
  readonly sessionId: string;
40
+ readonly workrailSessionId?: string;
37
41
  }
38
42
  export interface SessionCompletedEvent {
39
43
  readonly kind: 'session_completed';
@@ -41,6 +45,7 @@ export interface SessionCompletedEvent {
41
45
  readonly workflowId: string;
42
46
  readonly outcome: 'success' | 'error' | 'timeout';
43
47
  readonly detail?: string;
48
+ readonly workrailSessionId?: string;
44
49
  }
45
50
  export interface DeliveryAttemptedEvent {
46
51
  readonly kind: 'delivery_attempted';
@@ -48,7 +53,48 @@ export interface DeliveryAttemptedEvent {
48
53
  readonly outcome: 'success' | 'http_error' | 'network_error';
49
54
  readonly statusCode?: number;
50
55
  }
51
- export type DaemonEvent = DaemonStartedEvent | TriggerFiredEvent | SessionQueuedEvent | SessionStartedEvent | ToolCalledEvent | ToolErrorEvent | StepAdvancedEvent | SessionCompletedEvent | DeliveryAttemptedEvent;
56
+ export interface IssueReportedEvent {
57
+ readonly kind: 'issue_reported';
58
+ readonly sessionId: string;
59
+ readonly issueKind: 'tool_failure' | 'blocked' | 'unexpected_behavior' | 'needs_human' | 'self_correction';
60
+ readonly severity: 'info' | 'warn' | 'error' | 'fatal';
61
+ readonly summary: string;
62
+ readonly continueToken?: string;
63
+ }
64
+ export interface LlmTurnStartedEvent {
65
+ readonly kind: 'llm_turn_started';
66
+ readonly sessionId: string;
67
+ readonly messageCount: number;
68
+ }
69
+ export interface LlmTurnCompletedEvent {
70
+ readonly kind: 'llm_turn_completed';
71
+ readonly sessionId: string;
72
+ readonly stopReason: string;
73
+ readonly outputTokens: number;
74
+ readonly inputTokens: number;
75
+ readonly toolNamesRequested: readonly string[];
76
+ }
77
+ export interface ToolCallStartedEvent {
78
+ readonly kind: 'tool_call_started';
79
+ readonly sessionId: string;
80
+ readonly toolName: string;
81
+ readonly argsSummary: string;
82
+ }
83
+ export interface ToolCallCompletedEvent {
84
+ readonly kind: 'tool_call_completed';
85
+ readonly sessionId: string;
86
+ readonly toolName: string;
87
+ readonly durationMs: number;
88
+ readonly resultSummary: string;
89
+ }
90
+ export interface ToolCallFailedEvent {
91
+ readonly kind: 'tool_call_failed';
92
+ readonly sessionId: string;
93
+ readonly toolName: string;
94
+ readonly durationMs: number;
95
+ readonly errorMessage: string;
96
+ }
97
+ export type DaemonEvent = DaemonStartedEvent | TriggerFiredEvent | SessionQueuedEvent | SessionStartedEvent | ToolCalledEvent | ToolErrorEvent | StepAdvancedEvent | SessionCompletedEvent | DeliveryAttemptedEvent | IssueReportedEvent | LlmTurnStartedEvent | LlmTurnCompletedEvent | ToolCallStartedEvent | ToolCallCompletedEvent | ToolCallFailedEvent;
52
98
  export declare class DaemonEventEmitter {
53
99
  private readonly _dir;
54
100
  constructor(dirOverride?: string);
@@ -60,8 +60,9 @@ export declare function readDaemonSessionState(sessionId: string): Promise<{
60
60
  } | null>;
61
61
  export declare function readAllDaemonSessions(sessionsDir?: string): Promise<OrphanedSession[]>;
62
62
  export declare function runStartupRecovery(sessionsDir?: string): Promise<void>;
63
- export declare function makeContinueWorkflowTool(sessionId: string, ctx: V2ToolContext, onAdvance: (nextStepText: string, continueToken: string) => void, onComplete: (notes: string | undefined) => void, schemas: Record<string, any>, _executeContinueWorkflowFn?: typeof executeContinueWorkflow, emitter?: DaemonEventEmitter): AgentTool;
64
- export declare function makeBashTool(workspacePath: string, schemas: Record<string, any>, sessionId?: string, emitter?: DaemonEventEmitter): AgentTool;
63
+ export declare function makeContinueWorkflowTool(sessionId: string, ctx: V2ToolContext, onAdvance: (nextStepText: string, continueToken: string) => void, onComplete: (notes: string | undefined) => void, schemas: Record<string, any>, _executeContinueWorkflowFn?: typeof executeContinueWorkflow, emitter?: DaemonEventEmitter, workrailSessionId?: string | null): AgentTool;
64
+ export declare function makeBashTool(workspacePath: string, schemas: Record<string, any>, sessionId?: string, emitter?: DaemonEventEmitter, workrailSessionId?: string | null): AgentTool;
65
+ export declare function makeReportIssueTool(sessionId: string, emitter?: DaemonEventEmitter, issuesDirOverride?: string): AgentTool;
65
66
  export declare function buildSessionRecap(notes: readonly string[]): string;
66
67
  export declare function buildSystemPrompt(trigger: WorkflowTrigger, sessionState: string, soulContent: string, workspaceContext: string | null): string;
67
68
  export declare function runWorkflow(trigger: WorkflowTrigger, ctx: V2ToolContext, apiKey: string, daemonRegistry?: DaemonRegistry, emitter?: DaemonEventEmitter): Promise<WorkflowRunResult>;
@@ -42,6 +42,7 @@ exports.readAllDaemonSessions = readAllDaemonSessions;
42
42
  exports.runStartupRecovery = runStartupRecovery;
43
43
  exports.makeContinueWorkflowTool = makeContinueWorkflowTool;
44
44
  exports.makeBashTool = makeBashTool;
45
+ exports.makeReportIssueTool = makeReportIssueTool;
45
46
  exports.buildSessionRecap = buildSessionRecap;
46
47
  exports.buildSystemPrompt = buildSystemPrompt;
47
48
  exports.runWorkflow = runWorkflow;
@@ -338,7 +339,7 @@ function getSchemas() {
338
339
  };
339
340
  return _schemas;
340
341
  }
341
- function makeContinueWorkflowTool(sessionId, ctx, onAdvance, onComplete, schemas, _executeContinueWorkflowFn = index_js_1.executeContinueWorkflow, emitter) {
342
+ function makeContinueWorkflowTool(sessionId, ctx, onAdvance, onComplete, schemas, _executeContinueWorkflowFn = index_js_1.executeContinueWorkflow, emitter, workrailSessionId) {
342
343
  return {
343
344
  name: 'continue_workflow',
344
345
  description: 'Advance the WorkRail workflow to the next step. Call this after completing all work ' +
@@ -347,7 +348,7 @@ function makeContinueWorkflowTool(sessionId, ctx, onAdvance, onComplete, schemas
347
348
  label: 'Continue Workflow',
348
349
  execute: async (_toolCallId, params) => {
349
350
  console.log(`[WorkflowRunner] Tool: continue_workflow sessionId=${sessionId}`);
350
- emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'continue_workflow', summary: params.intent ?? 'advance' });
351
+ emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'continue_workflow', summary: params.intent ?? 'advance', ...(workrailSessionId != null ? { workrailSessionId } : {}) });
351
352
  const result = await _executeContinueWorkflowFn({
352
353
  continueToken: params.continueToken,
353
354
  intent: (params.intent ?? 'advance'),
@@ -426,7 +427,7 @@ function makeContinueWorkflowTool(sessionId, ctx, onAdvance, onComplete, schemas
426
427
  },
427
428
  };
428
429
  }
429
- function makeBashTool(workspacePath, schemas, sessionId, emitter) {
430
+ function makeBashTool(workspacePath, schemas, sessionId, emitter, workrailSessionId) {
430
431
  return {
431
432
  name: 'Bash',
432
433
  description: 'Execute a shell command. Throws on failure (non-zero exit with stderr, or exit code 2+). ' +
@@ -438,12 +439,13 @@ function makeBashTool(workspacePath, schemas, sessionId, emitter) {
438
439
  execute: async (_toolCallId, params) => {
439
440
  console.log(`[WorkflowRunner] Tool: bash "${String(params.command).slice(0, 80)}"`);
440
441
  if (sessionId)
441
- emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Bash', summary: String(params.command).slice(0, 80) });
442
+ emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Bash', summary: String(params.command).slice(0, 80), ...(workrailSessionId != null ? { workrailSessionId } : {}) });
442
443
  const cwd = params.cwd ?? workspacePath;
443
444
  try {
444
445
  const { stdout, stderr } = await execAsync(params.command, {
445
446
  cwd,
446
447
  timeout: BASH_TIMEOUT_MS,
448
+ shell: '/bin/bash',
447
449
  });
448
450
  const output = [stdout, stderr].filter(Boolean).join('\n');
449
451
  return {
@@ -473,7 +475,7 @@ function makeBashTool(workspacePath, schemas, sessionId, emitter) {
473
475
  },
474
476
  };
475
477
  }
476
- function makeReadTool(schemas, sessionId, emitter) {
478
+ function makeReadTool(schemas, sessionId, emitter, workrailSessionId) {
477
479
  return {
478
480
  name: 'Read',
479
481
  description: 'Read the contents of a file at the given absolute path.',
@@ -481,7 +483,7 @@ function makeReadTool(schemas, sessionId, emitter) {
481
483
  label: 'Read',
482
484
  execute: async (_toolCallId, params) => {
483
485
  if (sessionId)
484
- emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Read', summary: String(params.filePath).slice(0, 80) });
486
+ emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Read', summary: String(params.filePath).slice(0, 80), ...(workrailSessionId != null ? { workrailSessionId } : {}) });
485
487
  const content = await fs.readFile(params.filePath, 'utf8');
486
488
  return {
487
489
  content: [{ type: 'text', text: content }],
@@ -490,7 +492,7 @@ function makeReadTool(schemas, sessionId, emitter) {
490
492
  },
491
493
  };
492
494
  }
493
- function makeWriteTool(schemas, sessionId, emitter) {
495
+ function makeWriteTool(schemas, sessionId, emitter, workrailSessionId) {
494
496
  return {
495
497
  name: 'Write',
496
498
  description: 'Write content to a file at the given absolute path. Creates parent directories if needed.',
@@ -498,7 +500,7 @@ function makeWriteTool(schemas, sessionId, emitter) {
498
500
  label: 'Write',
499
501
  execute: async (_toolCallId, params) => {
500
502
  if (sessionId)
501
- emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Write', summary: String(params.filePath).slice(0, 80) });
503
+ emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Write', summary: String(params.filePath).slice(0, 80), ...(workrailSessionId != null ? { workrailSessionId } : {}) });
502
504
  await fs.mkdir(path.dirname(params.filePath), { recursive: true });
503
505
  await fs.writeFile(params.filePath, params.content, 'utf8');
504
506
  return {
@@ -508,6 +510,140 @@ function makeWriteTool(schemas, sessionId, emitter) {
508
510
  },
509
511
  };
510
512
  }
513
+ async function appendIssueAsync(issuesDir, sessionId, record) {
514
+ await fs.mkdir(issuesDir, { recursive: true });
515
+ const filePath = path.join(issuesDir, `${sessionId}.jsonl`);
516
+ const line = JSON.stringify({ ...record, ts: Date.now() }) + '\n';
517
+ await fs.appendFile(filePath, line, 'utf8');
518
+ }
519
+ function makeReportIssueTool(sessionId, emitter, issuesDirOverride) {
520
+ const issuesDir = issuesDirOverride ?? path.join(os.homedir(), '.workrail', 'issues');
521
+ return {
522
+ name: 'report_issue',
523
+ description: "Record a structured issue, error, or unexpected behavior. Call this AND continue_workflow (unless fatal). " +
524
+ "Does not stop the session -- it creates a record for the auto-fix coordinator.",
525
+ inputSchema: {
526
+ type: 'object',
527
+ properties: {
528
+ kind: {
529
+ type: 'string',
530
+ enum: ['tool_failure', 'blocked', 'unexpected_behavior', 'needs_human', 'self_correction'],
531
+ description: 'Category of issue being reported.',
532
+ },
533
+ severity: {
534
+ type: 'string',
535
+ enum: ['info', 'warn', 'error', 'fatal'],
536
+ description: 'Severity level. Fatal means the session cannot continue productively.',
537
+ },
538
+ summary: {
539
+ type: 'string',
540
+ description: 'One-line summary of the issue. Max 200 chars.',
541
+ maxLength: 200,
542
+ },
543
+ context: {
544
+ type: 'string',
545
+ description: 'What you were trying to do when this issue occurred.',
546
+ },
547
+ toolName: {
548
+ type: 'string',
549
+ description: 'Name of the tool that failed or behaved unexpectedly, if applicable.',
550
+ },
551
+ command: {
552
+ type: 'string',
553
+ description: 'The shell command or expression that caused the issue, if applicable.',
554
+ },
555
+ suggestedFix: {
556
+ type: 'string',
557
+ description: 'A suggested fix or recovery action for the auto-fix coordinator.',
558
+ },
559
+ continueToken: {
560
+ type: 'string',
561
+ description: 'The current continueToken, so the coordinator can resume this session.',
562
+ },
563
+ },
564
+ required: ['kind', 'severity', 'summary'],
565
+ additionalProperties: false,
566
+ },
567
+ label: 'report_issue',
568
+ execute: async (_toolCallId, params) => {
569
+ const record = {
570
+ sessionId,
571
+ kind: params.kind,
572
+ severity: params.severity,
573
+ summary: String(params.summary ?? '').slice(0, 200),
574
+ ...(params.context !== undefined && { context: String(params.context) }),
575
+ ...(params.toolName !== undefined && { toolName: String(params.toolName) }),
576
+ ...(params.command !== undefined && { command: String(params.command) }),
577
+ ...(params.suggestedFix !== undefined && { suggestedFix: String(params.suggestedFix) }),
578
+ ...(params.continueToken !== undefined && { continueToken: String(params.continueToken) }),
579
+ };
580
+ void appendIssueAsync(issuesDir, sessionId, record).catch(() => {
581
+ });
582
+ emitter?.emit({
583
+ kind: 'issue_reported',
584
+ sessionId,
585
+ issueKind: record.kind,
586
+ severity: record.severity,
587
+ summary: record.summary,
588
+ ...(record.continueToken !== undefined && { continueToken: record.continueToken }),
589
+ });
590
+ const isFatal = record.severity === 'fatal';
591
+ const message = isFatal
592
+ ? `FATAL issue recorded. Call continue_workflow with notes explaining the blocker, then the session will end.`
593
+ : `Issue recorded (severity=${record.severity}). Continue with your work unless this is fatal.`;
594
+ return {
595
+ content: [{ type: 'text', text: message }],
596
+ details: { sessionId, kind: record.kind, severity: record.severity },
597
+ };
598
+ },
599
+ };
600
+ }
601
+ const BASE_SYSTEM_PROMPT = `\
602
+ You are WorkRail Auto, an autonomous agent that executes workflows step by step. You are running unattended -- there is no user watching. Your entire job is to faithfully complete the current workflow.
603
+
604
+ ## What you are
605
+ You are highly capable. You handle ambitious, multi-step tasks that require real codebase understanding. You don't hedge, ask for permission, or stop to check in. You work.
606
+
607
+ ## Your oracle (consult in this order when uncertain)
608
+ 1. The daemon soul rules (## Agent Rules and Philosophy below)
609
+ 2. AGENTS.md / CLAUDE.md in the workspace (injected below under Workspace Context)
610
+ 3. The current workflow step's prompt and guidance
611
+ 4. Local code patterns in the relevant module (grep the directory, not the whole repo)
612
+ 5. Industry best practices -- only when nothing above applies
613
+
614
+ ## Self-directed reasoning
615
+ Ask yourself questions to clarify your approach, then answer them yourself using tools before acting. Never wait for a human to answer -- you are the oracle.
616
+
617
+ Bad pattern: "I'll analyze both layers." (no justification)
618
+ Good pattern: "Question: Should I check the middleware? Answer: The workflow step says 'trace the full call chain', and the AGENTS.md says the entry point is in the middleware layer. Yes, start there."
619
+
620
+ ## Your tools
621
+ - \`continue_workflow\`: Advance to the next step. Call this after completing each step's work. Always include your notes in notesMarkdown and round-trip the continueToken exactly.
622
+ - \`Bash\`: Run shell commands. Use for building, testing, running scripts.
623
+ - \`Read\`: Read files.
624
+ - \`Write\`: Write files.
625
+ - \`report_issue\`: Record a structured issue, error, or unexpected behavior. Call this AND continue_workflow (unless fatal). Does not stop the session -- it creates a record for the auto-fix coordinator.
626
+
627
+ ## Execution contract
628
+ 1. Read the step carefully. Do ALL the work the step asks for.
629
+ 2. Call \`continue_workflow\` with your notes. Include the continueToken exactly.
630
+ 3. Repeat until the workflow reports it is complete.
631
+ 4. Do NOT skip steps. Do NOT call \`continue_workflow\` without completing the step's work.
632
+
633
+ ## The workflow is the contract
634
+ Every step must be fully completed before you call continue_workflow. The workflow step prompt is the specification of what 'done' means -- not a suggestion. Don't advance until the work is actually done.
635
+
636
+ Your cognitive mode changes per step: some steps make you a researcher, others a reviewer, others an implementer. Adopt the mode the step describes. Don't bring your own agenda.
637
+
638
+ ## Silent failure is the worst outcome
639
+ If something goes wrong: call report_issue, then continue unless severity is 'fatal'. Do NOT silently retry forever, work around failures without noting them, or pretend things worked. The issue record is how the system learns and self-heals.
640
+
641
+ ## Tools are your hands, not your voice
642
+ Don't narrate what you're about to do. Use the tool and report what you found. Token efficiency matters -- you have a wall-clock timeout.
643
+
644
+ ## You don't have a user. You have a workflow and a soul.
645
+ If you're unsure, consult the oracle above. If nothing answers the question, make a reasoned decision, call report_issue with kind='self_correction' to document it, and continue.\
646
+ `;
511
647
  function buildSessionRecap(notes) {
512
648
  if (notes.length === 0)
513
649
  return '';
@@ -518,20 +654,7 @@ function buildSessionRecap(notes) {
518
654
  }
519
655
  function buildSystemPrompt(trigger, sessionState, soulContent, workspaceContext) {
520
656
  const lines = [
521
- 'You are WorkRail Auto, an autonomous agent that executes workflows step by step.',
522
- '',
523
- '## Your tools',
524
- '- `continue_workflow`: Advance to the next step. Call this after completing each step\'s work.',
525
- ' Always include your notes in notesMarkdown and round-trip the continueToken exactly.',
526
- '- `Bash`: Run shell commands. Use for building, testing, running scripts.',
527
- '- `Read`: Read files.',
528
- '- `Write`: Write files.',
529
- '',
530
- '## Execution contract',
531
- '1. Read the step carefully. Do ALL the work the step asks for.',
532
- '2. Call `continue_workflow` with your notes. Include the continueToken exactly.',
533
- '3. Repeat until the workflow reports it is complete.',
534
- '4. Do NOT skip steps. Do NOT call `continue_workflow` without completing the step\'s work.',
657
+ BASE_SYSTEM_PROMPT,
535
658
  '',
536
659
  `<workrail_session_state>${sessionState}</workrail_session_state>`,
537
660
  '',
@@ -570,13 +693,12 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter) {
570
693
  workflowId: trigger.workflowId,
571
694
  workspacePath: trigger.workspacePath,
572
695
  });
573
- daemonRegistry?.register(sessionId, trigger.workflowId);
696
+ let workrailSessionId = null;
574
697
  let agentClient;
575
698
  let modelId;
576
699
  if (trigger.agentConfig?.model) {
577
700
  const slashIdx = trigger.agentConfig.model.indexOf('/');
578
701
  if (slashIdx === -1) {
579
- daemonRegistry?.unregister(sessionId, 'failed');
580
702
  return {
581
703
  _tag: 'error',
582
704
  workflowId: trigger.workflowId,
@@ -604,8 +726,9 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter) {
604
726
  let lastStepNotes;
605
727
  const onAdvance = (stepText, _continueToken) => {
606
728
  pendingSteerText = stepText;
607
- daemonRegistry?.heartbeat(sessionId);
608
- emitter?.emit({ kind: 'step_advanced', sessionId });
729
+ if (workrailSessionId !== null)
730
+ daemonRegistry?.heartbeat(workrailSessionId);
731
+ emitter?.emit({ kind: 'step_advanced', sessionId, ...(workrailSessionId != null ? { workrailSessionId } : {}) });
609
732
  };
610
733
  const onComplete = (notes) => {
611
734
  isComplete = true;
@@ -616,9 +739,8 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter) {
616
739
  firstStep = trigger._preAllocatedStartResponse;
617
740
  }
618
741
  else {
619
- const startResult = await (0, start_js_1.executeStartWorkflow)({ workflowId: trigger.workflowId, workspacePath: trigger.workspacePath, goal: trigger.goal }, ctx, { is_autonomous: 'true' });
742
+ const startResult = await (0, start_js_1.executeStartWorkflow)({ workflowId: trigger.workflowId, workspacePath: trigger.workspacePath, goal: trigger.goal }, ctx, { is_autonomous: 'true', workspacePath: trigger.workspacePath });
620
743
  if (startResult.isErr()) {
621
- daemonRegistry?.unregister(sessionId, 'failed');
622
744
  return {
623
745
  _tag: 'error',
624
746
  workflowId: trigger.workflowId,
@@ -630,21 +752,35 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter) {
630
752
  }
631
753
  const startContinueToken = firstStep.continueToken ?? '';
632
754
  const startCheckpointToken = firstStep.checkpointToken ?? null;
755
+ if (startContinueToken) {
756
+ const decoded = await (0, v2_token_ops_js_1.parseContinueTokenOrFail)(startContinueToken, ctx.v2.tokenCodecPorts, ctx.v2.tokenAliasStore);
757
+ if (decoded.isOk()) {
758
+ workrailSessionId = decoded.value.sessionId;
759
+ }
760
+ else {
761
+ console.error(`[WorkflowRunner] Error: could not decode WorkRail session ID from continueToken -- isLive and liveActivity will not work for this session. Reason: ${decoded.error.message}`);
762
+ }
763
+ }
764
+ if (workrailSessionId !== null) {
765
+ daemonRegistry?.register(workrailSessionId, trigger.workflowId);
766
+ }
633
767
  if (startContinueToken) {
634
768
  await persistTokens(sessionId, startContinueToken, startCheckpointToken);
635
769
  }
636
770
  if (firstStep.isComplete) {
637
771
  await fs.unlink(path.join(exports.DAEMON_SESSIONS_DIR, `${sessionId}.json`)).catch(() => { });
638
- emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'success', detail: 'stop' });
639
- daemonRegistry?.unregister(sessionId, 'completed');
772
+ emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'success', detail: 'stop', ...(workrailSessionId != null ? { workrailSessionId } : {}) });
773
+ if (workrailSessionId !== null)
774
+ daemonRegistry?.unregister(workrailSessionId, 'completed');
640
775
  return { _tag: 'success', workflowId: trigger.workflowId, stopReason: 'stop' };
641
776
  }
642
777
  const schemas = getSchemas();
643
778
  const tools = [
644
- makeContinueWorkflowTool(sessionId, ctx, onAdvance, onComplete, schemas, index_js_1.executeContinueWorkflow, emitter),
645
- makeBashTool(trigger.workspacePath, schemas, sessionId, emitter),
646
- makeReadTool(schemas, sessionId, emitter),
647
- makeWriteTool(schemas, sessionId, emitter),
779
+ makeContinueWorkflowTool(sessionId, ctx, onAdvance, onComplete, schemas, index_js_1.executeContinueWorkflow, emitter, workrailSessionId),
780
+ makeBashTool(trigger.workspacePath, schemas, sessionId, emitter, workrailSessionId),
781
+ makeReadTool(schemas, sessionId, emitter, workrailSessionId),
782
+ makeWriteTool(schemas, sessionId, emitter, workrailSessionId),
783
+ makeReportIssueTool(sessionId, emitter),
648
784
  ];
649
785
  const [soulContent, workspaceContext, sessionNotes] = await Promise.all([
650
786
  loadDaemonSoul(trigger.soulFile),
@@ -659,12 +795,37 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter) {
659
795
  `\n\ncontinueToken: ${startContinueToken}` +
660
796
  contextJson +
661
797
  '\n\nComplete all step work, then call continue_workflow with your notes to begin.';
798
+ const agentCallbacks = {
799
+ onLlmTurnStarted: ({ messageCount }) => {
800
+ emitter?.emit({ kind: 'llm_turn_started', sessionId, messageCount });
801
+ },
802
+ onLlmTurnCompleted: ({ stopReason, outputTokens, inputTokens, toolNamesRequested }) => {
803
+ emitter?.emit({
804
+ kind: 'llm_turn_completed',
805
+ sessionId,
806
+ stopReason,
807
+ outputTokens,
808
+ inputTokens,
809
+ toolNamesRequested,
810
+ });
811
+ },
812
+ onToolCallStarted: ({ toolName, argsSummary }) => {
813
+ emitter?.emit({ kind: 'tool_call_started', sessionId, toolName, argsSummary });
814
+ },
815
+ onToolCallCompleted: ({ toolName, durationMs, resultSummary }) => {
816
+ emitter?.emit({ kind: 'tool_call_completed', sessionId, toolName, durationMs, resultSummary });
817
+ },
818
+ onToolCallFailed: ({ toolName, durationMs, errorMessage }) => {
819
+ emitter?.emit({ kind: 'tool_call_failed', sessionId, toolName, durationMs, errorMessage });
820
+ },
821
+ };
662
822
  const agent = new agent_loop_js_1.AgentLoop({
663
823
  systemPrompt: buildSystemPrompt(trigger, sessionState, soulContent, workspaceContext),
664
824
  modelId,
665
825
  tools,
666
826
  client: agentClient,
667
827
  toolExecution: 'sequential',
828
+ callbacks: agentCallbacks,
668
829
  });
669
830
  const sessionTimeoutMs = (trigger.agentConfig?.maxSessionMinutes ?? DEFAULT_SESSION_TIMEOUT_MINUTES) * 60 * 1000;
670
831
  const maxTurns = trigger.agentConfig?.maxTurns ?? 0;
@@ -676,7 +837,7 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter) {
676
837
  for (const toolResult of event.toolResults) {
677
838
  if (toolResult.isError) {
678
839
  const errorText = toolResult.result?.content[0]?.text ?? 'tool error';
679
- emitter?.emit({ kind: 'tool_error', sessionId, toolName: toolResult.toolName, error: errorText.slice(0, 200) });
840
+ emitter?.emit({ kind: 'tool_error', sessionId, toolName: toolResult.toolName, error: errorText.slice(0, 200), ...(workrailSessionId != null ? { workrailSessionId } : {}) });
680
841
  }
681
842
  }
682
843
  turnCount++;
@@ -732,8 +893,9 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter) {
732
893
  console.log(`[WorkflowRunner] Agent loop ended: sessionId=${sessionId} stopReason=${stopReason}${errorMessage ? ` error=${errorMessage.slice(0, 120)}` : ''}`);
733
894
  }
734
895
  if (timeoutReason !== null) {
735
- emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'timeout', detail: timeoutReason });
736
- daemonRegistry?.unregister(sessionId, 'failed');
896
+ emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'timeout', detail: timeoutReason, ...(workrailSessionId != null ? { workrailSessionId } : {}) });
897
+ if (workrailSessionId !== null)
898
+ daemonRegistry?.unregister(workrailSessionId, 'failed');
737
899
  const limitDescription = timeoutReason === 'wall_clock'
738
900
  ? `${trigger.agentConfig?.maxSessionMinutes ?? DEFAULT_SESSION_TIMEOUT_MINUTES} minutes`
739
901
  : `${trigger.agentConfig?.maxTurns} turns`;
@@ -747,8 +909,9 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter) {
747
909
  }
748
910
  if (stopReason === 'error' || errorMessage) {
749
911
  const errMsg = errorMessage ?? 'Agent stopped with error reason';
750
- emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'error', detail: errMsg.slice(0, 200) });
751
- daemonRegistry?.unregister(sessionId, 'failed');
912
+ emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'error', detail: errMsg.slice(0, 200), ...(workrailSessionId != null ? { workrailSessionId } : {}) });
913
+ if (workrailSessionId !== null)
914
+ daemonRegistry?.unregister(workrailSessionId, 'failed');
752
915
  const stuckMarker = `\n\nWORKTRAIN_STUCK: ${JSON.stringify({
753
916
  reason: 'session_error',
754
917
  error: errMsg.slice(0, 500),
@@ -765,8 +928,9 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter) {
765
928
  }
766
929
  await fs.unlink(path.join(exports.DAEMON_SESSIONS_DIR, `${sessionId}.json`)).catch(() => {
767
930
  });
768
- emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'success', detail: stopReason });
769
- daemonRegistry?.unregister(sessionId, 'completed');
931
+ emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'success', detail: stopReason, ...(workrailSessionId != null ? { workrailSessionId } : {}) });
932
+ if (workrailSessionId !== null)
933
+ daemonRegistry?.unregister(workrailSessionId, 'completed');
770
934
  return {
771
935
  _tag: 'success',
772
936
  workflowId: trigger.workflowId,