@exaudeus/workrail 3.75.0 → 3.77.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 (95) hide show
  1. package/dist/console-ui/assets/index-D9pYbwS0.js +28 -0
  2. package/dist/console-ui/index.html +1 -1
  3. package/dist/coordinators/adaptive-pipeline.d.ts +8 -0
  4. package/dist/coordinators/context-assembly.d.ts +4 -0
  5. package/dist/coordinators/context-assembly.js +156 -0
  6. package/dist/coordinators/modes/full-pipeline.d.ts +1 -1
  7. package/dist/coordinators/modes/full-pipeline.js +140 -27
  8. package/dist/coordinators/modes/implement-shared.d.ts +3 -2
  9. package/dist/coordinators/modes/implement-shared.js +16 -6
  10. package/dist/coordinators/modes/implement.js +49 -3
  11. package/dist/coordinators/pipeline-run-context.d.ts +1811 -0
  12. package/dist/coordinators/pipeline-run-context.js +114 -0
  13. package/dist/daemon/context-loader.d.ts +1 -1
  14. package/dist/daemon/core/agent-client.d.ts +7 -0
  15. package/dist/daemon/core/agent-client.js +31 -0
  16. package/dist/daemon/core/index.d.ts +6 -0
  17. package/dist/daemon/core/index.js +19 -0
  18. package/dist/daemon/core/session-context.d.ts +14 -0
  19. package/dist/daemon/core/session-context.js +24 -0
  20. package/dist/daemon/core/session-result.d.ts +10 -0
  21. package/dist/daemon/core/session-result.js +92 -0
  22. package/dist/daemon/core/system-prompt.d.ts +6 -0
  23. package/dist/daemon/core/system-prompt.js +151 -0
  24. package/dist/daemon/io/conversation-log.d.ts +2 -0
  25. package/dist/daemon/io/conversation-log.js +45 -0
  26. package/dist/daemon/io/execution-stats.d.ts +7 -0
  27. package/dist/daemon/io/execution-stats.js +86 -0
  28. package/dist/daemon/io/index.d.ts +5 -0
  29. package/dist/daemon/io/index.js +24 -0
  30. package/dist/daemon/io/session-notes-loader.d.ts +4 -0
  31. package/dist/daemon/io/session-notes-loader.js +45 -0
  32. package/dist/daemon/io/soul-loader.d.ts +3 -0
  33. package/dist/daemon/io/soul-loader.js +68 -0
  34. package/dist/daemon/io/workspace-context-loader.d.ts +17 -0
  35. package/dist/daemon/io/workspace-context-loader.js +137 -0
  36. package/dist/daemon/runner/agent-loop-runner.d.ts +28 -0
  37. package/dist/daemon/runner/agent-loop-runner.js +250 -0
  38. package/dist/daemon/runner/construct-tools.d.ts +5 -0
  39. package/dist/daemon/runner/construct-tools.js +30 -0
  40. package/dist/daemon/runner/finalize-session.d.ts +3 -0
  41. package/dist/daemon/runner/finalize-session.js +75 -0
  42. package/dist/daemon/runner/index.d.ts +8 -0
  43. package/dist/daemon/runner/index.js +18 -0
  44. package/dist/daemon/runner/pre-agent-session.d.ts +7 -0
  45. package/dist/daemon/runner/pre-agent-session.js +227 -0
  46. package/dist/daemon/runner/runner-types.d.ts +73 -0
  47. package/dist/daemon/runner/runner-types.js +39 -0
  48. package/dist/daemon/runner/tool-schemas.d.ts +1 -0
  49. package/dist/daemon/runner/tool-schemas.js +151 -0
  50. package/dist/daemon/session-scope.d.ts +1 -1
  51. package/dist/daemon/startup-recovery.d.ts +20 -0
  52. package/dist/daemon/startup-recovery.js +323 -0
  53. package/dist/daemon/state/index.d.ts +6 -0
  54. package/dist/daemon/state/index.js +14 -0
  55. package/dist/daemon/state/session-state.d.ts +23 -0
  56. package/dist/daemon/state/session-state.js +44 -0
  57. package/dist/daemon/state/stuck-detection.d.ts +22 -0
  58. package/dist/daemon/state/stuck-detection.js +25 -0
  59. package/dist/daemon/state/terminal-signal.d.ts +9 -0
  60. package/dist/daemon/state/terminal-signal.js +10 -0
  61. package/dist/daemon/tools/file-tools.d.ts +1 -1
  62. package/dist/daemon/turn-end/detect-stuck.d.ts +2 -2
  63. package/dist/daemon/turn-end/detect-stuck.js +2 -2
  64. package/dist/daemon/turn-end/step-injector.d.ts +1 -1
  65. package/dist/daemon/types.d.ts +105 -0
  66. package/dist/daemon/types.js +11 -0
  67. package/dist/daemon/workflow-enricher.d.ts +16 -0
  68. package/dist/daemon/workflow-enricher.js +58 -0
  69. package/dist/daemon/workflow-runner.d.ts +13 -277
  70. package/dist/daemon/workflow-runner.js +63 -1421
  71. package/dist/manifest.json +280 -56
  72. package/dist/trigger/coordinator-deps.d.ts +1 -1
  73. package/dist/trigger/coordinator-deps.js +131 -0
  74. package/dist/trigger/delivery-client.d.ts +1 -1
  75. package/dist/trigger/delivery-pipeline.d.ts +1 -1
  76. package/dist/trigger/notification-service.d.ts +1 -1
  77. package/dist/trigger/trigger-listener.js +6 -2
  78. package/dist/trigger/trigger-router.d.ts +2 -2
  79. package/dist/v2/durable-core/domain/artifact-contract-validator.js +99 -0
  80. package/dist/v2/durable-core/schemas/artifacts/discovery-handoff.d.ts +39 -0
  81. package/dist/v2/durable-core/schemas/artifacts/discovery-handoff.js +10 -1
  82. package/dist/v2/durable-core/schemas/artifacts/index.d.ts +2 -1
  83. package/dist/v2/durable-core/schemas/artifacts/index.js +12 -1
  84. package/dist/v2/durable-core/schemas/artifacts/phase-handoff.d.ts +89 -0
  85. package/dist/v2/durable-core/schemas/artifacts/phase-handoff.js +56 -0
  86. package/docs/authoring-v2.md +12 -0
  87. package/docs/ideas/backlog.md +639 -25
  88. package/docs/reference/worktrain-daemon-invariants.md +33 -49
  89. package/docs/vision.md +5 -15
  90. package/package.json +2 -2
  91. package/workflows/coding-task-workflow-agentic.json +9 -6
  92. package/workflows/mr-review-workflow.agentic.v2.json +2 -2
  93. package/workflows/wr.discovery.json +2 -1
  94. package/workflows/wr.shaping.json +7 -4
  95. package/dist/console-ui/assets/index-BvBihscd.js +0 -28
@@ -32,57 +32,39 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
- var __importDefault = (this && this.__importDefault) || function (mod) {
36
- return (mod && mod.__esModule) ? mod : { "default": mod };
37
- };
38
35
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.DAEMON_SOUL_TEMPLATE = exports.DAEMON_SOUL_DEFAULT = exports.WORKTREES_DIR = exports.DEFAULT_STALL_TIMEOUT_SECONDS = exports.DEFAULT_MAX_TURNS = exports.DEFAULT_SESSION_TIMEOUT_MINUTES = exports.makeSignalCoordinatorTool = exports.makeReportIssueTool = exports.makeSpawnAgentTool = exports.makeGrepTool = exports.makeGlobTool = exports.makeEditTool = exports.makeWriteTool = exports.makeReadTool = exports.makeBashTool = exports.makeCompleteStepTool = exports.makeContinueWorkflowTool = exports.DAEMON_SIGNALS_DIR = exports.DAEMON_SESSIONS_DIR = void 0;
40
- exports.readDaemonSessionState = readDaemonSessionState;
41
- exports.readAllDaemonSessions = readAllDaemonSessions;
42
- exports.runStartupRecovery = runStartupRecovery;
43
- exports.countOrphanStepAdvances = countOrphanStepAdvances;
44
- exports.clearQueueIssueSidecars = clearQueueIssueSidecars;
45
- exports.stripFrontmatter = stripFrontmatter;
46
- exports.loadWorkspaceContext = loadWorkspaceContext;
47
- exports.loadSessionNotes = loadSessionNotes;
48
- exports.buildSessionRecap = buildSessionRecap;
49
- exports.buildSystemPrompt = buildSystemPrompt;
50
- exports.tagToStatsOutcome = tagToStatsOutcome;
51
- exports.sidecardLifecycleFor = sidecardLifecycleFor;
52
- exports.buildAgentClient = buildAgentClient;
53
- exports.setTerminalSignal = setTerminalSignal;
54
- exports.createSessionState = createSessionState;
55
- exports.evaluateStuckSignals = evaluateStuckSignals;
56
- exports.finalizeSession = finalizeSession;
57
- exports.buildSessionContext = buildSessionContext;
58
- exports.buildPreAgentSession = buildPreAgentSession;
59
- exports.buildTurnEndSubscriber = buildTurnEndSubscriber;
60
- exports.buildAgentCallbacks = buildAgentCallbacks;
61
- exports.buildSessionResult = buildSessionResult;
36
+ exports.buildAgentClient = exports.buildSessionResult = exports.sidecardLifecycleFor = exports.tagToStatsOutcome = exports.buildSessionContext = exports.DEFAULT_STALL_TIMEOUT_SECONDS = exports.DEFAULT_MAX_TURNS = exports.DEFAULT_SESSION_TIMEOUT_MINUTES = exports.buildSystemPrompt = exports.buildSessionRecap = exports.BASE_SYSTEM_PROMPT = exports.recordToolCall = exports.setSessionId = exports.updateToken = exports.recordCompletion = exports.advanceStep = exports.evaluateStuckSignals = exports.setTerminalSignal = exports.createSessionState = exports.makeSignalCoordinatorTool = exports.makeReportIssueTool = exports.makeSpawnAgentTool = exports.makeGrepTool = exports.makeGlobTool = exports.makeEditTool = exports.makeWriteTool = exports.makeReadTool = exports.makeBashTool = exports.makeCompleteStepTool = exports.makeContinueWorkflowTool = exports.DAEMON_SIGNALS_DIR = exports.DAEMON_SESSIONS_DIR = exports.clearQueueIssueSidecars = exports.countOrphanStepAdvances = exports.readAllDaemonSessions = exports.readDaemonSessionState = exports.runStartupRecovery = exports.finalizeSession = exports.buildAgentCallbacks = exports.buildTurnEndSubscriber = exports.buildPreAgentSession = exports.WORKTREES_DIR = exports.MAX_SESSION_NOTE_CHARS = exports.MAX_SESSION_RECAP_NOTES = exports.stripFrontmatter = exports.loadSessionNotes = exports.loadWorkspaceContext = exports.loadDaemonSoul = void 0;
62
37
  exports.runWorkflow = runWorkflow;
63
38
  require("reflect-metadata");
64
- const fs = __importStar(require("node:fs/promises"));
65
39
  const path = __importStar(require("node:path"));
66
- const os = __importStar(require("node:os"));
67
40
  const node_child_process_1 = require("node:child_process");
68
41
  const node_util_1 = require("node:util");
69
- const tinyglobby_1 = require("tinyglobby");
70
42
  const node_crypto_1 = require("node:crypto");
71
- const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
72
- const bedrock_sdk_1 = require("@anthropic-ai/bedrock-sdk");
73
- const agent_loop_js_1 = require("./agent-loop.js");
74
- const start_js_1 = require("../mcp/handlers/v2-execution/start.js");
75
- const index_js_1 = require("../mcp/handlers/v2-execution/index.js");
76
- const v2_token_ops_js_1 = require("../mcp/handlers/v2-token-ops.js");
77
- const index_js_2 = require("../v2/durable-core/ids/index.js");
78
- const node_outputs_js_1 = require("../v2/projections/node-outputs.js");
79
- const assert_never_js_1 = require("../runtime/assert-never.js");
80
- const session_recovery_policy_js_1 = require("./session-recovery-policy.js");
81
- const stats_summary_js_1 = require("./stats-summary.js");
82
- const step_injector_js_1 = require("./turn-end/step-injector.js");
83
- const conversation_flusher_js_1 = require("./turn-end/conversation-flusher.js");
84
- const session_scope_js_1 = require("./session-scope.js");
85
- const context_loader_js_1 = require("./context-loader.js");
43
+ const types_js_1 = require("./types.js");
44
+ const workflow_enricher_js_1 = require("./workflow-enricher.js");
45
+ const index_js_1 = require("./core/index.js");
46
+ const index_js_2 = require("./io/index.js");
47
+ var index_js_3 = require("./io/index.js");
48
+ Object.defineProperty(exports, "loadDaemonSoul", { enumerable: true, get: function () { return index_js_3.loadDaemonSoul; } });
49
+ Object.defineProperty(exports, "loadWorkspaceContext", { enumerable: true, get: function () { return index_js_3.loadWorkspaceContext; } });
50
+ Object.defineProperty(exports, "loadSessionNotes", { enumerable: true, get: function () { return index_js_3.loadSessionNotes; } });
51
+ Object.defineProperty(exports, "stripFrontmatter", { enumerable: true, get: function () { return index_js_3.stripFrontmatter; } });
52
+ Object.defineProperty(exports, "MAX_SESSION_RECAP_NOTES", { enumerable: true, get: function () { return index_js_3.MAX_SESSION_RECAP_NOTES; } });
53
+ Object.defineProperty(exports, "MAX_SESSION_NOTE_CHARS", { enumerable: true, get: function () { return index_js_3.MAX_SESSION_NOTE_CHARS; } });
54
+ const index_js_4 = require("./runner/index.js");
55
+ var runner_types_js_1 = require("./runner/runner-types.js");
56
+ Object.defineProperty(exports, "WORKTREES_DIR", { enumerable: true, get: function () { return runner_types_js_1.WORKTREES_DIR; } });
57
+ var index_js_5 = require("./runner/index.js");
58
+ Object.defineProperty(exports, "buildPreAgentSession", { enumerable: true, get: function () { return index_js_5.buildPreAgentSession; } });
59
+ Object.defineProperty(exports, "buildTurnEndSubscriber", { enumerable: true, get: function () { return index_js_5.buildTurnEndSubscriber; } });
60
+ Object.defineProperty(exports, "buildAgentCallbacks", { enumerable: true, get: function () { return index_js_5.buildAgentCallbacks; } });
61
+ Object.defineProperty(exports, "finalizeSession", { enumerable: true, get: function () { return index_js_5.finalizeSession; } });
62
+ var startup_recovery_js_1 = require("./startup-recovery.js");
63
+ Object.defineProperty(exports, "runStartupRecovery", { enumerable: true, get: function () { return startup_recovery_js_1.runStartupRecovery; } });
64
+ Object.defineProperty(exports, "readDaemonSessionState", { enumerable: true, get: function () { return startup_recovery_js_1.readDaemonSessionState; } });
65
+ Object.defineProperty(exports, "readAllDaemonSessions", { enumerable: true, get: function () { return startup_recovery_js_1.readAllDaemonSessions; } });
66
+ Object.defineProperty(exports, "countOrphanStepAdvances", { enumerable: true, get: function () { return startup_recovery_js_1.countOrphanStepAdvances; } });
67
+ Object.defineProperty(exports, "clearQueueIssueSidecars", { enumerable: true, get: function () { return startup_recovery_js_1.clearQueueIssueSidecars; } });
86
68
  const _shared_js_1 = require("./tools/_shared.js");
87
69
  const continue_workflow_js_1 = require("./tools/continue-workflow.js");
88
70
  Object.defineProperty(exports, "makeContinueWorkflowTool", { enumerable: true, get: function () { return continue_workflow_js_1.makeContinueWorkflowTool; } });
@@ -106,725 +88,28 @@ var _shared_js_2 = require("./tools/_shared.js");
106
88
  Object.defineProperty(exports, "DAEMON_SESSIONS_DIR", { enumerable: true, get: function () { return _shared_js_2.DAEMON_SESSIONS_DIR; } });
107
89
  var signal_coordinator_js_2 = require("./tools/signal-coordinator.js");
108
90
  Object.defineProperty(exports, "DAEMON_SIGNALS_DIR", { enumerable: true, get: function () { return signal_coordinator_js_2.DAEMON_SIGNALS_DIR; } });
91
+ var index_js_6 = require("./state/index.js");
92
+ Object.defineProperty(exports, "createSessionState", { enumerable: true, get: function () { return index_js_6.createSessionState; } });
93
+ Object.defineProperty(exports, "setTerminalSignal", { enumerable: true, get: function () { return index_js_6.setTerminalSignal; } });
94
+ Object.defineProperty(exports, "evaluateStuckSignals", { enumerable: true, get: function () { return index_js_6.evaluateStuckSignals; } });
95
+ Object.defineProperty(exports, "advanceStep", { enumerable: true, get: function () { return index_js_6.advanceStep; } });
96
+ Object.defineProperty(exports, "recordCompletion", { enumerable: true, get: function () { return index_js_6.recordCompletion; } });
97
+ Object.defineProperty(exports, "updateToken", { enumerable: true, get: function () { return index_js_6.updateToken; } });
98
+ Object.defineProperty(exports, "setSessionId", { enumerable: true, get: function () { return index_js_6.setSessionId; } });
99
+ Object.defineProperty(exports, "recordToolCall", { enumerable: true, get: function () { return index_js_6.recordToolCall; } });
100
+ var index_js_7 = require("./core/index.js");
101
+ Object.defineProperty(exports, "BASE_SYSTEM_PROMPT", { enumerable: true, get: function () { return index_js_7.BASE_SYSTEM_PROMPT; } });
102
+ Object.defineProperty(exports, "buildSessionRecap", { enumerable: true, get: function () { return index_js_7.buildSessionRecap; } });
103
+ Object.defineProperty(exports, "buildSystemPrompt", { enumerable: true, get: function () { return index_js_7.buildSystemPrompt; } });
104
+ Object.defineProperty(exports, "DEFAULT_SESSION_TIMEOUT_MINUTES", { enumerable: true, get: function () { return index_js_7.DEFAULT_SESSION_TIMEOUT_MINUTES; } });
105
+ Object.defineProperty(exports, "DEFAULT_MAX_TURNS", { enumerable: true, get: function () { return index_js_7.DEFAULT_MAX_TURNS; } });
106
+ Object.defineProperty(exports, "DEFAULT_STALL_TIMEOUT_SECONDS", { enumerable: true, get: function () { return index_js_7.DEFAULT_STALL_TIMEOUT_SECONDS; } });
107
+ Object.defineProperty(exports, "buildSessionContext", { enumerable: true, get: function () { return index_js_7.buildSessionContext; } });
108
+ Object.defineProperty(exports, "tagToStatsOutcome", { enumerable: true, get: function () { return index_js_7.tagToStatsOutcome; } });
109
+ Object.defineProperty(exports, "sidecardLifecycleFor", { enumerable: true, get: function () { return index_js_7.sidecardLifecycleFor; } });
110
+ Object.defineProperty(exports, "buildSessionResult", { enumerable: true, get: function () { return index_js_7.buildSessionResult; } });
111
+ Object.defineProperty(exports, "buildAgentClient", { enumerable: true, get: function () { return index_js_7.buildAgentClient; } });
109
112
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
110
- const MAX_SESSION_RECAP_NOTES = 3;
111
- const MAX_SESSION_NOTE_CHARS = 800;
112
- exports.DEFAULT_SESSION_TIMEOUT_MINUTES = 30;
113
- exports.DEFAULT_MAX_TURNS = 200;
114
- exports.DEFAULT_STALL_TIMEOUT_SECONDS = 120;
115
- const MAX_ORPHAN_AGE_MS = 2 * 60 * 60 * 1000;
116
- const MAX_WORKTREE_ORPHAN_AGE_MS = 24 * 60 * 60 * 1000;
117
- const WORKRAIL_DIR = path.join(os.homedir(), '.workrail');
118
- exports.WORKTREES_DIR = path.join(os.homedir(), '.workrail', 'worktrees');
119
- const DAEMON_STATS_DIR = path.join(os.homedir(), '.workrail', 'data');
120
- const WORKSPACE_CONTEXT_MAX_BYTES = 32 * 1024;
121
- const MAX_ASSEMBLED_CONTEXT_BYTES = 8192;
122
- const MAX_GLOB_FILES_PER_PATTERN = 20;
123
- const WORKSPACE_CONTEXT_CANDIDATE_PATHS = [
124
- { kind: 'literal', relativePath: '.claude/CLAUDE.md' },
125
- { kind: 'literal', relativePath: 'CLAUDE.md' },
126
- { kind: 'literal', relativePath: 'CLAUDE.local.md' },
127
- { kind: 'literal', relativePath: 'AGENTS.md' },
128
- { kind: 'literal', relativePath: '.github/AGENTS.md' },
129
- { kind: 'glob', pattern: '.cursor/rules/*.mdc', stripFrontmatter: true, sort: 'alpha' },
130
- { kind: 'literal', relativePath: '.cursorrules' },
131
- { kind: 'glob', pattern: '.windsurf/rules/*.md', stripFrontmatter: true, sort: 'alpha' },
132
- { kind: 'glob', pattern: '.firebender/rules/*.mdc', stripFrontmatter: true, sort: 'alpha' },
133
- { kind: 'literal', relativePath: '.firebender/AGENTS.md' },
134
- { kind: 'literal', relativePath: '.github/copilot-instructions.md' },
135
- { kind: 'glob', pattern: '.continue/rules/*.md', stripFrontmatter: false, sort: 'alpha' },
136
- ];
137
- const soul_template_js_1 = require("./soul-template.js");
138
- var soul_template_js_2 = require("./soul-template.js");
139
- Object.defineProperty(exports, "DAEMON_SOUL_DEFAULT", { enumerable: true, get: function () { return soul_template_js_2.DAEMON_SOUL_DEFAULT; } });
140
- Object.defineProperty(exports, "DAEMON_SOUL_TEMPLATE", { enumerable: true, get: function () { return soul_template_js_2.DAEMON_SOUL_TEMPLATE; } });
141
- async function appendConversationMessages(filePath, messages) {
142
- if (messages.length === 0)
143
- return;
144
- const lines = messages.map((m) => JSON.stringify(m)).join('\n') + '\n';
145
- await fs.mkdir(_shared_js_1.DAEMON_SESSIONS_DIR, { recursive: true });
146
- await fs.appendFile(filePath, lines, 'utf8');
147
- }
148
- async function readDaemonSessionState(sessionId) {
149
- const sessionPath = path.join(_shared_js_1.DAEMON_SESSIONS_DIR, `${sessionId}.json`);
150
- try {
151
- const raw = await fs.readFile(sessionPath, 'utf8');
152
- const parsed = JSON.parse(raw);
153
- return { continueToken: parsed.continueToken, checkpointToken: parsed.checkpointToken };
154
- }
155
- catch {
156
- return null;
157
- }
158
- }
159
- async function readAllDaemonSessions(sessionsDir = _shared_js_1.DAEMON_SESSIONS_DIR) {
160
- let entries;
161
- try {
162
- entries = await fs.readdir(sessionsDir);
163
- }
164
- catch (err) {
165
- const isEnoent = err instanceof Error && 'code' in err && err.code === 'ENOENT';
166
- if (!isEnoent) {
167
- console.warn(`[WorkflowRunner] Could not read sessions directory ${sessionsDir}: ${err instanceof Error ? err.message : String(err)}`);
168
- }
169
- return [];
170
- }
171
- const sessions = [];
172
- for (const entry of entries) {
173
- if (!entry.endsWith('.json') || entry.startsWith('queue-issue-'))
174
- continue;
175
- const sessionId = entry.slice(0, -5);
176
- const filePath = path.join(sessionsDir, entry);
177
- try {
178
- const raw = await fs.readFile(filePath, 'utf8');
179
- const parsed = JSON.parse(raw);
180
- if (typeof parsed.continueToken !== 'string' || typeof parsed.ts !== 'number') {
181
- console.warn(`[WorkflowRunner] Skipping malformed session file: ${filePath}`);
182
- continue;
183
- }
184
- sessions.push({
185
- sessionId,
186
- continueToken: parsed.continueToken,
187
- checkpointToken: typeof parsed.checkpointToken === 'string' ? parsed.checkpointToken : null,
188
- ts: parsed.ts,
189
- ...(typeof parsed.worktreePath === 'string' ? { worktreePath: parsed.worktreePath } : {}),
190
- ...(typeof parsed.workflowId === 'string' ? { workflowId: parsed.workflowId } : {}),
191
- ...(typeof parsed.goal === 'string' ? { goal: parsed.goal } : {}),
192
- ...(typeof parsed.workspacePath === 'string' ? { workspacePath: parsed.workspacePath } : {}),
193
- });
194
- }
195
- catch (err) {
196
- console.warn(`[WorkflowRunner] Skipping unreadable session file ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
197
- }
198
- }
199
- return sessions;
200
- }
201
- async function runStartupRecovery(sessionsDir = _shared_js_1.DAEMON_SESSIONS_DIR, execFn = execFileAsync, ctx, _countStepAdvancesFn = countOrphanStepAdvances, _executeContinueWorkflowFn = index_js_1.executeContinueWorkflow, _runWorkflowFn = runWorkflow, apiKey = '') {
202
- await clearQueueIssueSidecars(sessionsDir);
203
- const sessions = await readAllDaemonSessions(sessionsDir);
204
- if (sessions.length === 0) {
205
- await clearStrayTmpFiles(sessionsDir);
206
- return;
207
- }
208
- console.log(`[WorkflowRunner] Startup recovery: found ${sessions.length} orphaned session(s).`);
209
- const now = Date.now();
210
- let cleared = 0;
211
- let preserved = 0;
212
- for (const session of sessions) {
213
- const ageMs = now - session.ts;
214
- const isStale = ageMs > MAX_ORPHAN_AGE_MS;
215
- const ageSec = Math.round(ageMs / 1000);
216
- if (session.worktreePath && ageMs > MAX_WORKTREE_ORPHAN_AGE_MS) {
217
- console.log(`[WorkflowRunner] Removing orphan worktree: sessionId=${session.sessionId} worktreePath=${session.worktreePath}`);
218
- try {
219
- await execFn('git', ['worktree', 'remove', '--force', session.worktreePath]);
220
- console.log(`[WorkflowRunner] Removed orphan worktree: ${session.worktreePath}`);
221
- }
222
- catch (err) {
223
- console.warn(`[WorkflowRunner] Could not remove orphan worktree ${session.worktreePath}: ` +
224
- `${err instanceof Error ? err.message : String(err)}`);
225
- }
226
- }
227
- else if (session.worktreePath && ageMs <= MAX_WORKTREE_ORPHAN_AGE_MS) {
228
- const ageHours = (ageMs / (60 * 60 * 1000)).toFixed(1);
229
- console.log(`[WorkflowRunner] Keeping recent orphan worktree: sessionId=${session.sessionId} ` +
230
- `age=${ageHours}h (threshold=24h) worktreePath=${session.worktreePath}`);
231
- }
232
- if (ctx !== undefined) {
233
- let stepAdvances = 0;
234
- try {
235
- stepAdvances = await _countStepAdvancesFn(session.continueToken, ctx);
236
- }
237
- catch (err) {
238
- console.warn(`[WorkflowRunner] Could not count step advances for orphaned session ${session.sessionId}: ` +
239
- `${err instanceof Error ? err.message : String(err)} -- falling back to discard`);
240
- }
241
- const action = (0, session_recovery_policy_js_1.evaluateRecovery)({ stepAdvances, ageMs });
242
- switch (action) {
243
- case 'resume': {
244
- const hasContext = typeof session.workflowId === 'string' &&
245
- typeof session.workspacePath === 'string';
246
- if (!hasContext) {
247
- console.log(`[WorkflowRunner] Startup recovery: cannot resume session ${session.sessionId} -- ` +
248
- `missing workflowId/workspacePath in sidecar (old format). Discarding.`);
249
- break;
250
- }
251
- if (isStale) {
252
- console.log(`[WorkflowRunner] Startup recovery: discarding stale resumable session ${session.sessionId} ` +
253
- `(age=${ageSec}s > ${MAX_ORPHAN_AGE_MS / 1000}s threshold).`);
254
- break;
255
- }
256
- if (session.worktreePath !== undefined) {
257
- let worktreeExists = true;
258
- try {
259
- await fs.access(session.worktreePath);
260
- }
261
- catch {
262
- worktreeExists = false;
263
- }
264
- if (!worktreeExists) {
265
- console.log(`[WorkflowRunner] Startup recovery: discarding session ${session.sessionId} -- ` +
266
- `worktree no longer exists at ${session.worktreePath}.`);
267
- break;
268
- }
269
- }
270
- let rehydrateResult;
271
- try {
272
- rehydrateResult = await _executeContinueWorkflowFn({ continueToken: session.continueToken, intent: 'rehydrate' }, ctx);
273
- }
274
- catch (err) {
275
- console.warn(`[WorkflowRunner] Startup recovery: rehydrate failed for session ${session.sessionId}: ` +
276
- `${err instanceof Error ? err.message : String(err)}. Discarding.`);
277
- break;
278
- }
279
- if (rehydrateResult.isErr()) {
280
- console.warn(`[WorkflowRunner] Startup recovery: rehydrate error for session ${session.sessionId}: ` +
281
- `${rehydrateResult.error.kind}. Discarding.`);
282
- break;
283
- }
284
- const rehydrated = rehydrateResult.value.response;
285
- if (rehydrated.isComplete || !rehydrated.pending) {
286
- console.log(`[WorkflowRunner] Startup recovery: session ${session.sessionId} is already complete ` +
287
- `or has no pending step. Discarding.`);
288
- break;
289
- }
290
- const recoveryAllocatedSession = {
291
- continueToken: rehydrated.continueToken ?? '',
292
- checkpointToken: rehydrated.checkpointToken,
293
- firstStepPrompt: rehydrated.pending.prompt ?? '',
294
- isComplete: rehydrated.isComplete,
295
- triggerSource: 'daemon',
296
- ...(session.worktreePath !== undefined
297
- ? { sessionWorkspacePath: session.worktreePath }
298
- : {}),
299
- };
300
- const branchStrategy = 'none';
301
- const recoveredTrigger = {
302
- workflowId: session.workflowId,
303
- goal: session.goal ?? 'Resumed session (crash recovery)',
304
- workspacePath: session.workspacePath,
305
- branchStrategy,
306
- };
307
- const recoverySource = {
308
- kind: 'pre_allocated',
309
- trigger: recoveredTrigger,
310
- session: recoveryAllocatedSession,
311
- };
312
- console.log(`[WorkflowRunner] Startup recovery: resuming session ${session.sessionId} ` +
313
- `workflowId=${session.workflowId} stepAdvances=${stepAdvances}`);
314
- void _runWorkflowFn(recoveredTrigger, ctx, apiKey, undefined, undefined, undefined, undefined, undefined, recoverySource).then((result) => {
315
- console.log(`[WorkflowRunner] Startup recovery: resumed session ${session.sessionId} completed: ${result._tag}`);
316
- }).catch((err) => {
317
- console.warn(`[WorkflowRunner] Startup recovery: resumed session ${session.sessionId} failed: ` +
318
- `${err instanceof Error ? err.message : String(err)}`);
319
- });
320
- preserved++;
321
- continue;
322
- }
323
- case 'discard': {
324
- const label = isStale ? 'stale orphaned session' : 'orphaned session';
325
- console.log(`[WorkflowRunner] Discarding ${label}: sessionId=${session.sessionId} ` +
326
- `stepAdvances=${stepAdvances} age=${ageSec}s`);
327
- break;
328
- }
329
- default:
330
- (0, assert_never_js_1.assertNever)(action);
331
- }
332
- }
333
- else {
334
- const label = isStale ? 'stale orphaned session' : 'orphaned session';
335
- console.log(`[WorkflowRunner] Clearing ${label}: sessionId=${session.sessionId} age=${ageSec}s`);
336
- }
337
- try {
338
- await fs.unlink(path.join(sessionsDir, `${session.sessionId}.json`));
339
- cleared++;
340
- }
341
- catch (err) {
342
- const isEnoent = err instanceof Error && 'code' in err && err.code === 'ENOENT';
343
- if (!isEnoent) {
344
- console.warn(`[WorkflowRunner] Could not clear session file ${session.sessionId}: ${err instanceof Error ? err.message : String(err)}`);
345
- }
346
- }
347
- }
348
- await clearStrayTmpFiles(sessionsDir);
349
- if (ctx !== undefined) {
350
- console.log(`[WorkflowRunner] Startup recovery complete: preserved=${preserved} discarded=${cleared}/${sessions.length} orphaned session(s).`);
351
- }
352
- else {
353
- console.log(`[WorkflowRunner] Startup recovery complete: cleared ${cleared}/${sessions.length} orphaned session(s).`);
354
- }
355
- }
356
- async function countOrphanStepAdvances(continueToken, ctx, _parseFn = undefined, _loadFn = undefined) {
357
- const parseFn = _parseFn ?? ((raw) => (0, v2_token_ops_js_1.parseContinueTokenOrFail)(raw, ctx.v2.tokenCodecPorts, ctx.v2.tokenAliasStore));
358
- const loadFn = _loadFn ?? ctx.v2.sessionStore.loadValidatedPrefix.bind(ctx.v2.sessionStore);
359
- const resolvedResult = await parseFn(continueToken);
360
- if (resolvedResult.isErr()) {
361
- console.warn(`[WorkflowRunner] Could not decode continueToken for orphaned session: ${resolvedResult.error.message}`);
362
- return 0;
363
- }
364
- const sessionId = (0, index_js_2.asSessionId)(resolvedResult.value.sessionId);
365
- const loadResult = await loadFn(sessionId);
366
- if (loadResult.isErr()) {
367
- console.warn(`[WorkflowRunner] Could not load session event log for orphaned session: ${loadResult.error.code} -- ${loadResult.error.message}`);
368
- return 0;
369
- }
370
- const events = loadResult.value.truth.events;
371
- return events.filter((e) => e.kind === 'advance_recorded').length;
372
- }
373
- async function clearQueueIssueSidecars(sessionsDir) {
374
- let entries;
375
- try {
376
- entries = await fs.readdir(sessionsDir);
377
- }
378
- catch {
379
- return;
380
- }
381
- for (const entry of entries) {
382
- if (!entry.startsWith('queue-issue-') || !entry.endsWith('.json'))
383
- continue;
384
- try {
385
- await fs.unlink(path.join(sessionsDir, entry));
386
- const issueNum = entry.slice('queue-issue-'.length, -'.json'.length);
387
- console.log(`[WorkflowRunner] Cleared queue-issue sidecar: issue=${issueNum}`);
388
- }
389
- catch (err) {
390
- const isEnoent = err instanceof Error && 'code' in err && err.code === 'ENOENT';
391
- if (!isEnoent) {
392
- console.warn(`[WorkflowRunner] Could not clear queue-issue sidecar ${entry}: ${err instanceof Error ? err.message : String(err)}`);
393
- }
394
- }
395
- }
396
- }
397
- async function clearStrayTmpFiles(sessionsDir) {
398
- let entries;
399
- try {
400
- entries = await fs.readdir(sessionsDir);
401
- }
402
- catch {
403
- return;
404
- }
405
- for (const entry of entries) {
406
- if (!entry.endsWith('.tmp'))
407
- continue;
408
- try {
409
- await fs.unlink(path.join(sessionsDir, entry));
410
- console.log(`[WorkflowRunner] Cleared stray temp file: ${entry}`);
411
- }
412
- catch {
413
- }
414
- }
415
- }
416
- async function loadDaemonSoul(resolvedPath) {
417
- const soulPath = resolvedPath ?? path.join(WORKRAIL_DIR, 'daemon-soul.md');
418
- try {
419
- return await fs.readFile(soulPath, 'utf8');
420
- }
421
- catch (err) {
422
- const isEnoent = err instanceof Error && 'code' in err && err.code === 'ENOENT';
423
- if (isEnoent) {
424
- try {
425
- await fs.mkdir(path.dirname(soulPath), { recursive: true });
426
- await fs.writeFile(soulPath, soul_template_js_1.DAEMON_SOUL_TEMPLATE, 'utf8');
427
- console.log(`[WorkflowRunner] Created daemon-soul.md template at ${soulPath}`);
428
- }
429
- catch (writeErr) {
430
- console.warn(`[WorkflowRunner] Warning: could not write daemon-soul.md template: ${writeErr instanceof Error ? writeErr.message : String(writeErr)}`);
431
- }
432
- }
433
- else {
434
- console.warn(`[WorkflowRunner] Warning: could not read daemon-soul.md: ${err instanceof Error ? err.message : String(err)}`);
435
- }
436
- return soul_template_js_1.DAEMON_SOUL_DEFAULT;
437
- }
438
- }
439
- function stripFrontmatter(content) {
440
- if (!content.startsWith('---\n') && !content.startsWith('---\r\n'))
441
- return content;
442
- const endIdx = content.indexOf('\n---', 4);
443
- if (endIdx === -1)
444
- return content;
445
- return content.slice(endIdx + 4).trimStart();
446
- }
447
- async function loadWorkspaceContext(workspacePath) {
448
- const parts = [];
449
- const injectedPaths = [];
450
- let combinedBytes = 0;
451
- let truncated = false;
452
- function accumulateFile(relativePath, content) {
453
- const contentBytes = Buffer.byteLength(content, 'utf8');
454
- if (combinedBytes + contentBytes > WORKSPACE_CONTEXT_MAX_BYTES) {
455
- const remaining = WORKSPACE_CONTEXT_MAX_BYTES - combinedBytes;
456
- const truncatedContent = content.slice(0, remaining);
457
- parts.push(`### ${relativePath}\n${truncatedContent}`);
458
- injectedPaths.push(relativePath);
459
- truncated = true;
460
- }
461
- else {
462
- parts.push(`### ${relativePath}\n${content}`);
463
- injectedPaths.push(relativePath);
464
- combinedBytes += contentBytes;
465
- }
466
- }
467
- for (const entry of WORKSPACE_CONTEXT_CANDIDATE_PATHS) {
468
- if (truncated)
469
- break;
470
- if (entry.kind === 'literal') {
471
- const fullPath = path.join(workspacePath, entry.relativePath);
472
- let content;
473
- try {
474
- content = await fs.readFile(fullPath, 'utf8');
475
- }
476
- catch (err) {
477
- const isEnoent = err instanceof Error && 'code' in err && err.code === 'ENOENT';
478
- if (!isEnoent) {
479
- console.warn(`[WorkflowRunner] Skipping ${fullPath}: ${err instanceof Error ? err.message : String(err)}`);
480
- }
481
- continue;
482
- }
483
- accumulateFile(entry.relativePath, content);
484
- }
485
- else {
486
- const matches = await (0, tinyglobby_1.glob)(entry.pattern, { cwd: workspacePath, absolute: false });
487
- const sorted = [...matches].sort();
488
- if (sorted.length > MAX_GLOB_FILES_PER_PATTERN) {
489
- console.warn(`[WorkflowRunner] ${entry.pattern}: ${sorted.length} files found, capped at ${MAX_GLOB_FILES_PER_PATTERN}`);
490
- }
491
- for (const relativePath of sorted.slice(0, MAX_GLOB_FILES_PER_PATTERN)) {
492
- if (truncated)
493
- break;
494
- const fullPath = path.join(workspacePath, relativePath);
495
- let content;
496
- try {
497
- content = await fs.readFile(fullPath, 'utf8');
498
- }
499
- catch (err) {
500
- const isEnoent = err instanceof Error && 'code' in err && err.code === 'ENOENT';
501
- if (!isEnoent) {
502
- console.warn(`[WorkflowRunner] Skipping ${fullPath}: ${err instanceof Error ? err.message : String(err)}`);
503
- }
504
- continue;
505
- }
506
- accumulateFile(relativePath, entry.stripFrontmatter ? stripFrontmatter(content) : content);
507
- }
508
- }
509
- }
510
- if (parts.length === 0)
511
- return null;
512
- let combined = parts.join('\n\n');
513
- if (truncated) {
514
- combined += '\n\n[Workspace context truncated: combined size exceeded 32 KB limit. Some files may be missing.]';
515
- }
516
- console.log(`[WorkflowRunner] Injecting workspace context from: ${injectedPaths.join(', ')}`);
517
- return combined;
518
- }
519
- async function loadSessionNotes(continueToken, ctx) {
520
- try {
521
- const resolvedResult = await (0, v2_token_ops_js_1.parseContinueTokenOrFail)(continueToken, ctx.v2.tokenCodecPorts, ctx.v2.tokenAliasStore);
522
- if (resolvedResult.isErr()) {
523
- console.warn(`[WorkflowRunner] Warning: could not decode continueToken for session recap: ${resolvedResult.error.message}`);
524
- return [];
525
- }
526
- const sessionId = (0, index_js_2.asSessionId)(resolvedResult.value.sessionId);
527
- const loadResult = await ctx.v2.sessionStore.load(sessionId);
528
- if (loadResult.isErr()) {
529
- console.warn(`[WorkflowRunner] Warning: could not load session store for recap: ${loadResult.error.code} -- ${loadResult.error.message}`);
530
- return [];
531
- }
532
- const projectionResult = (0, node_outputs_js_1.projectNodeOutputsV2)(loadResult.value.events);
533
- if (projectionResult.isErr()) {
534
- console.warn(`[WorkflowRunner] Warning: could not project session outputs for recap: ${projectionResult.error.code} -- ${projectionResult.error.message}`);
535
- return [];
536
- }
537
- const allNotes = [];
538
- for (const nodeView of Object.values(projectionResult.value.nodesById)) {
539
- for (const output of nodeView.currentByChannel.recap) {
540
- if (output.payload.payloadKind === 'notes') {
541
- const note = output.payload.notesMarkdown.length > MAX_SESSION_NOTE_CHARS
542
- ? output.payload.notesMarkdown.slice(0, MAX_SESSION_NOTE_CHARS) + '\n[truncated]'
543
- : output.payload.notesMarkdown;
544
- allNotes.push(note);
545
- }
546
- }
547
- }
548
- return allNotes.slice(-MAX_SESSION_RECAP_NOTES);
549
- }
550
- catch (err) {
551
- console.warn(`[WorkflowRunner] Warning: unexpected error loading session notes for recap: ${err instanceof Error ? err.message : String(err)}`);
552
- return [];
553
- }
554
- }
555
- let _schemas = null;
556
- function getSchemas() {
557
- if (_schemas)
558
- return _schemas;
559
- _schemas = {
560
- ContinueWorkflowParams: {
561
- type: 'object',
562
- properties: {
563
- continueToken: {
564
- type: 'string',
565
- description: 'The continueToken from the previous start_workflow or continue_workflow call. Round-trip exactly as received.',
566
- },
567
- intent: {
568
- type: 'string',
569
- enum: ['advance', 'rehydrate'],
570
- description: 'advance: I completed this step. rehydrate: remind me what the current step is.',
571
- },
572
- notesMarkdown: {
573
- type: 'string',
574
- description: 'Notes on what you did in this step (10-30 lines, markdown).',
575
- },
576
- artifacts: {
577
- type: 'array',
578
- items: {},
579
- description: 'Optional structured artifacts to attach to this step. ' +
580
- 'Include wr.assessment objects here when the step requires an assessment gate. ' +
581
- 'Example: [{ "kind": "wr.assessment", "assessmentId": "<id>", "dimensions": { "<dimensionId>": "high" } }]',
582
- },
583
- context: {
584
- type: 'object',
585
- additionalProperties: true,
586
- description: 'Updated context variables (only changed values). Exception: metrics_commit_shas must always contain the FULL accumulated list of all commit SHAs from this session -- never send only new SHAs.',
587
- },
588
- },
589
- required: ['continueToken'],
590
- },
591
- CompleteStepParams: {
592
- type: 'object',
593
- properties: {
594
- notes: {
595
- type: 'string',
596
- minLength: 50,
597
- description: 'What you did in this step (required, at least 50 characters). Write for a human reader. ' +
598
- 'Include: what you did and key decisions, what you produced (files, tests, numbers), ' +
599
- 'anything notable (risks, open questions, things you chose NOT to do and why). ' +
600
- 'Use markdown: headings, bullets, bold. 10-30 lines is ideal.',
601
- },
602
- artifacts: {
603
- type: 'array',
604
- items: {},
605
- description: 'Optional structured artifacts to attach to this step. ' +
606
- 'Include wr.assessment objects here when the step requires an assessment gate. ' +
607
- 'Example: [{ "kind": "wr.assessment", "assessmentId": "<id>", "dimensions": { "<dimensionId>": "high" } }]',
608
- },
609
- context: {
610
- type: 'object',
611
- additionalProperties: true,
612
- description: 'Updated context variables (only changed values). Omit entirely if no facts changed. Exception: metrics_commit_shas must always contain the FULL accumulated list of all commit SHAs from this session -- never send only new SHAs.',
613
- },
614
- },
615
- required: ['notes'],
616
- additionalProperties: false,
617
- },
618
- BashParams: {
619
- type: 'object',
620
- properties: {
621
- command: { type: 'string', description: 'Shell command to execute' },
622
- cwd: { type: 'string', description: 'Working directory for the command' },
623
- },
624
- required: ['command'],
625
- },
626
- ReadParams: {
627
- type: 'object',
628
- properties: {
629
- filePath: { type: 'string', description: 'Absolute path to the file to read. Content is returned in cat -n format: each line prefixed with its 1-indexed line number and a tab character.' },
630
- offset: { type: 'number', description: '0-indexed line number to start reading from (inclusive). Omit to read from the beginning.' },
631
- limit: { type: 'number', description: 'Maximum number of lines to return. Omit to read to end of file.' },
632
- },
633
- required: ['filePath'],
634
- },
635
- WriteParams: {
636
- type: 'object',
637
- properties: {
638
- filePath: { type: 'string', description: 'Absolute path to the file to write' },
639
- content: { type: 'string', description: 'Content to write to the file' },
640
- },
641
- required: ['filePath', 'content'],
642
- },
643
- GlobParams: {
644
- type: 'object',
645
- properties: {
646
- pattern: { type: 'string', description: 'Glob pattern to match (e.g. "**/*.ts"). Supports standard glob syntax.' },
647
- path: { type: 'string', description: 'Absolute path to search root. Defaults to the workspace root.' },
648
- },
649
- required: ['pattern'],
650
- },
651
- GrepParams: {
652
- type: 'object',
653
- properties: {
654
- pattern: { type: 'string', description: 'Regular expression pattern to search for in file contents.' },
655
- path: { type: 'string', description: 'Absolute path to search in. Defaults to the workspace root.' },
656
- glob: { type: 'string', description: 'Glob pattern to restrict which files are searched (e.g. "*.ts").' },
657
- type: { type: 'string', description: 'File type filter for ripgrep (e.g. "ts", "js", "py").' },
658
- output_mode: { type: 'string', enum: ['content', 'files_with_matches', 'count'], description: 'Output mode. "files_with_matches": only file paths (default). "content": matching lines with context. "count": match counts per file.' },
659
- head_limit: { type: 'number', description: 'Maximum number of output lines to return. Default: 250.' },
660
- context: { type: 'number', description: 'Number of lines of context to show before and after each match (output_mode=content only).' },
661
- '-i': { type: 'boolean', description: 'Case-insensitive search.' },
662
- },
663
- required: ['pattern'],
664
- },
665
- EditParams: {
666
- type: 'object',
667
- properties: {
668
- file_path: { type: 'string', description: 'Absolute path to the file to edit. The file must have been read in this session via the Read tool.' },
669
- old_string: { type: 'string', description: 'Exact string to find and replace. Must appear exactly once in the file (or use replace_all=true for multiple occurrences). Do NOT include line-number prefixes from Read output.' },
670
- new_string: { type: 'string', description: 'Replacement string. Must differ from old_string.' },
671
- replace_all: { type: 'boolean', description: 'Replace all occurrences of old_string. Default: false (fails if more than one match).' },
672
- },
673
- required: ['file_path', 'old_string', 'new_string'],
674
- additionalProperties: false,
675
- },
676
- SpawnAgentParams: {
677
- type: 'object',
678
- properties: {
679
- workflowId: {
680
- type: 'string',
681
- description: 'ID of the workflow to run in the child session (e.g. "wr.discovery").',
682
- },
683
- goal: {
684
- type: 'string',
685
- description: 'One-sentence description of what the child session should accomplish.',
686
- },
687
- workspacePath: {
688
- type: 'string',
689
- description: 'Absolute path to the workspace directory for the child session.',
690
- },
691
- context: {
692
- type: 'object',
693
- additionalProperties: true,
694
- description: 'Optional initial context variables to pass to the child workflow.',
695
- },
696
- },
697
- required: ['workflowId', 'goal', 'workspacePath'],
698
- additionalProperties: false,
699
- },
700
- };
701
- return _schemas;
702
- }
703
- async function writeStuckOutboxEntry(opts) {
704
- try {
705
- const outboxPath = path.join(os.homedir(), '.workrail', 'outbox.jsonl');
706
- await fs.mkdir(path.dirname(outboxPath), { recursive: true });
707
- const entry = JSON.stringify({
708
- id: (0, node_crypto_1.randomUUID)(),
709
- kind: 'stuck',
710
- message: `Session stuck (${opts.reason}): workflowId=${opts.workflowId}` +
711
- (opts.issueSummaries && opts.issueSummaries.length > 0
712
- ? ` -- issues: ${opts.issueSummaries.join('; ')}`
713
- : ''),
714
- timestamp: new Date().toISOString(),
715
- workflowId: opts.workflowId,
716
- reason: opts.reason,
717
- ...(opts.issueSummaries && opts.issueSummaries.length > 0
718
- ? { issueSummaries: opts.issueSummaries }
719
- : {}),
720
- });
721
- await fs.appendFile(outboxPath, entry + '\n');
722
- }
723
- catch (err) {
724
- console.warn(`[WorkflowRunner] Could not write stuck outbox entry: ` +
725
- `${err instanceof Error ? err.message : String(err)}`);
726
- }
727
- }
728
- const BASE_SYSTEM_PROMPT = `\
729
- 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.
730
-
731
- ## What you are
732
- 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.
733
-
734
- ## Your oracle (consult in this order when uncertain)
735
- 1. The daemon soul rules (## Agent Rules and Philosophy below)
736
- 2. AGENTS.md / CLAUDE.md in the workspace (injected below under Workspace Context)
737
- 3. The current workflow step's prompt and guidance
738
- 4. Local code patterns in the relevant module (grep the directory, not the whole repo)
739
- 5. Industry best practices -- only when nothing above applies
740
-
741
- ## Self-directed reasoning
742
- 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.
743
-
744
- Bad pattern: "I'll analyze both layers." (no justification)
745
- 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."
746
-
747
- ## Your tools
748
- - \`complete_step\`: Mark the current step complete and advance to the next one. Call this after completing ALL work required by the step. Include your notes (min 50 characters) in the notes field. The daemon manages the session token internally -- you do NOT need a continueToken. This is the preferred advancement tool for daemon sessions.
749
- - \`continue_workflow\`: [DEPRECATED -- use complete_step instead. Do NOT pass a continueToken.] Only use this if complete_step is unavailable.
750
- - \`Bash\`: Run shell commands. Use for building, testing, running scripts.
751
- - \`Read\`: Read files.
752
- - \`Write\`: Write files.
753
- - \`report_issue\`: Record a structured issue, error, or unexpected behavior. Call this AND complete_step (unless fatal). Does not stop the session -- it creates a record for the auto-fix coordinator.
754
- - \`spawn_agent\`: Delegate a sub-task to a child WorkRail session. BLOCKS until the child completes. Returns \`{ childSessionId, outcome: "success"|"error"|"timeout", notes: string }\`. Always check \`outcome\` before using \`notes\`. IMPORTANT: your session's time limit (maxSessionMinutes) keeps running while the child executes -- ensure your parent session has enough time for both your work AND the child's work. Maximum spawn depth is 3 by default (configurable). Use only when a step explicitly asks for delegation or when a clearly separable sub-task would benefit from its own WorkRail audit trail.
755
- - \`signal_coordinator\`: Emit a structured mid-session signal to the coordinator WITHOUT advancing the workflow step. Use when the step asks you to surface a finding, request data, request approval, or report a blocking condition. Always returns immediately -- fire-and-observe. Signal kinds: "progress", "finding", "data_needed", "approval_needed", "blocked".
756
-
757
- ## Execution contract
758
- 1. Read the step carefully. Do ALL the work the step asks for.
759
- 2. Call \`complete_step\` with your notes. No continueToken needed -- the daemon manages it.
760
- 3. Repeat until the workflow reports it is complete.
761
- 4. Do NOT skip steps. Do NOT call \`complete_step\` without completing the step's work.
762
-
763
- ## The workflow is the contract
764
- Every step must be fully completed before you call complete_step. The workflow step prompt is the specification of what 'done' means -- not a suggestion. Don't advance until the work is actually done.
765
-
766
- 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.
767
-
768
- ## Silent failure is the worst outcome
769
- 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.
770
-
771
- ## Tools are your hands, not your voice
772
- 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.
773
-
774
- ## You don't have a user. You have a workflow and a soul.
775
- 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.
776
-
777
- ## IMPORTANT: Never use continue_workflow in daemon sessions
778
- complete_step is your advancement tool. It does not require a continueToken. Do NOT call continue_workflow with a token you found in a previous message -- use complete_step instead.\
779
- `;
780
- function buildSessionRecap(notes) {
781
- if (notes.length === 0)
782
- return '';
783
- const formattedNotes = notes
784
- .map((note, i) => `### Prior step ${i + 1}\n${note}`)
785
- .join('\n\n');
786
- return `<workrail_session_state>\nThe following notes summarize prior steps from this session:\n\n${formattedNotes}\n</workrail_session_state>`;
787
- }
788
- function buildSystemPrompt(trigger, sessionState, soulContent, workspaceContext, effectiveWorkspacePath) {
789
- const isWorktreeSession = effectiveWorkspacePath !== trigger.workspacePath;
790
- const lines = [
791
- BASE_SYSTEM_PROMPT,
792
- '',
793
- `<workrail_session_state>${sessionState}</workrail_session_state>`,
794
- '',
795
- '## Agent Rules and Philosophy',
796
- soulContent,
797
- '',
798
- `## Workspace: ${effectiveWorkspacePath}`,
799
- ];
800
- if (isWorktreeSession) {
801
- lines.push('');
802
- lines.push(`**Worktree session scope:** Your workspace is the isolated git worktree at \`${effectiveWorkspacePath}\`. Do not access, read, or modify the main checkout at \`${trigger.workspacePath}\`. Do not read planning docs, roadmap files, or backlog files. All Bash commands, file reads, and file writes must stay within your worktree path.`);
803
- }
804
- if (workspaceContext !== null) {
805
- lines.push('');
806
- lines.push('## Workspace Context (from AGENTS.md / CLAUDE.md)');
807
- lines.push(workspaceContext);
808
- }
809
- const assembledContextSummary = trigger.context?.['assembledContextSummary'];
810
- if (typeof assembledContextSummary === 'string' && assembledContextSummary.trim().length > 0) {
811
- let ctxStr = assembledContextSummary;
812
- if (Buffer.byteLength(ctxStr, 'utf8') > MAX_ASSEMBLED_CONTEXT_BYTES) {
813
- ctxStr = ctxStr.slice(0, MAX_ASSEMBLED_CONTEXT_BYTES) + '\n[Prior context truncated at 8KB]';
814
- }
815
- lines.push('');
816
- lines.push('## Prior Context');
817
- lines.push(ctxStr.trim());
818
- }
819
- if (trigger.referenceUrls && trigger.referenceUrls.length > 0) {
820
- lines.push('');
821
- lines.push('## Reference documents');
822
- lines.push('Before starting, fetch and read these reference documents: ' +
823
- trigger.referenceUrls.join(' '));
824
- lines.push('If you cannot fetch any of these documents, note their unavailability and proceed.');
825
- }
826
- return lines.join('\n');
827
- }
828
113
  function buildUserMessage(text) {
829
114
  return {
830
115
  role: 'user',
@@ -832,659 +117,8 @@ function buildUserMessage(text) {
832
117
  timestamp: Date.now(),
833
118
  };
834
119
  }
835
- function tagToStatsOutcome(tag) {
836
- switch (tag) {
837
- case 'success': return 'success';
838
- case 'error': return 'error';
839
- case 'timeout': return 'timeout';
840
- case 'stuck': return 'stuck';
841
- case 'delivery_failed': return 'success';
842
- default: return (0, assert_never_js_1.assertNever)(tag);
843
- }
844
- }
845
- function sidecardLifecycleFor(tag, branchStrategy) {
846
- switch (tag) {
847
- case 'success':
848
- return branchStrategy === 'worktree'
849
- ? { kind: 'retain_for_delivery' }
850
- : { kind: 'delete_now' };
851
- case 'error':
852
- case 'timeout':
853
- case 'stuck':
854
- return { kind: 'delete_now' };
855
- case 'delivery_failed':
856
- throw new Error(`sidecardLifecycleFor: delivery_failed is not a valid input (invariant 1.2)`);
857
- default:
858
- return (0, assert_never_js_1.assertNever)(tag);
859
- }
860
- }
861
- function buildAgentClient(trigger, apiKey, env) {
862
- if (trigger.agentConfig?.model) {
863
- const slashIdx = trigger.agentConfig.model.indexOf('/');
864
- if (slashIdx === -1) {
865
- throw new Error(`agentConfig.model must be in "provider/model-id" format, got: "${trigger.agentConfig.model}"`);
866
- }
867
- const provider = trigger.agentConfig.model.slice(0, slashIdx);
868
- const modelId = trigger.agentConfig.model.slice(slashIdx + 1);
869
- const agentClient = provider === 'amazon-bedrock' ? new bedrock_sdk_1.AnthropicBedrock() : new sdk_1.default({ apiKey });
870
- return { agentClient, modelId };
871
- }
872
- const usesBedrock = !!env['AWS_PROFILE'] || !!env['AWS_ACCESS_KEY_ID'];
873
- if (usesBedrock) {
874
- return {
875
- agentClient: new bedrock_sdk_1.AnthropicBedrock(),
876
- modelId: 'us.anthropic.claude-sonnet-4-6',
877
- };
878
- }
879
- return {
880
- agentClient: new sdk_1.default({ apiKey }),
881
- modelId: 'claude-sonnet-4-6',
882
- };
883
- }
884
- function setTerminalSignal(state, signal) {
885
- if (state.terminalSignal === null) {
886
- state.terminalSignal = signal;
887
- return true;
888
- }
889
- return false;
890
- }
891
- function createSessionState(initialToken) {
892
- return {
893
- isComplete: false,
894
- lastStepNotes: undefined,
895
- lastStepArtifacts: undefined,
896
- currentContinueToken: initialToken,
897
- workrailSessionId: null,
898
- stepAdvanceCount: 0,
899
- lastNToolCalls: [],
900
- issueSummaries: [],
901
- pendingSteerParts: [],
902
- terminalSignal: null,
903
- turnCount: 0,
904
- };
905
- }
906
- function evaluateStuckSignals(state, config) {
907
- if (config.maxTurns > 0 && state.turnCount >= config.maxTurns && state.terminalSignal === null) {
908
- return { kind: 'max_turns_exceeded' };
909
- }
910
- if (state.lastNToolCalls.length === config.stuckRepeatThreshold &&
911
- state.lastNToolCalls.every((c) => c.toolName === state.lastNToolCalls[0]?.toolName && c.argsSummary === state.lastNToolCalls[0]?.argsSummary)) {
912
- return {
913
- kind: 'repeated_tool_call',
914
- toolName: state.lastNToolCalls[0]?.toolName ?? 'unknown',
915
- argsSummary: state.lastNToolCalls[0]?.argsSummary ?? '',
916
- };
917
- }
918
- if (config.maxTurns > 0 &&
919
- state.turnCount >= Math.floor(config.maxTurns * 0.8) &&
920
- state.stepAdvanceCount === 0) {
921
- return { kind: 'no_progress', turnCount: state.turnCount, maxTurns: config.maxTurns };
922
- }
923
- if (state.terminalSignal?.kind === 'timeout') {
924
- return { kind: 'timeout_imminent', timeoutReason: state.terminalSignal.reason };
925
- }
926
- return null;
927
- }
928
- function writeExecutionStats(statsDir, sessionId, workflowId, startMs, outcome, stepCount) {
929
- const endMs = Date.now();
930
- const statsPath = path.join(statsDir, 'execution-stats.jsonl');
931
- fs.mkdir(statsDir, { recursive: true })
932
- .then(() => fs.appendFile(statsPath, JSON.stringify({
933
- sessionId,
934
- workflowId,
935
- startMs,
936
- endMs,
937
- durationMs: endMs - startMs,
938
- outcome,
939
- stepCount,
940
- ts: new Date().toISOString(),
941
- }) + '\n', 'utf8'))
942
- .then(() => { (0, stats_summary_js_1.writeStatsSummary)(statsDir).catch(() => { }); })
943
- .catch(() => { });
944
- }
945
- async function finalizeSession(result, ctx) {
946
- const outcome = tagToStatsOutcome(result._tag);
947
- const detail = result._tag === 'stuck' ? result.reason
948
- : result._tag === 'timeout' ? result.reason
949
- : result._tag === 'error' ? result.message.slice(0, 200)
950
- : result._tag === 'delivery_failed' ? result.deliveryError.slice(0, 200)
951
- : result.stopReason;
952
- ctx.emitter?.emit({
953
- kind: 'session_completed',
954
- sessionId: ctx.sessionId,
955
- workflowId: ctx.workflowId,
956
- outcome,
957
- detail,
958
- ...(0, _shared_js_1.withWorkrailSession)(ctx.workrailSessionId),
959
- });
960
- if (ctx.workrailSessionId !== null) {
961
- ctx.daemonRegistry?.unregister(ctx.workrailSessionId, result._tag === 'success' || result._tag === 'delivery_failed' ? 'completed' : 'failed');
962
- }
963
- writeExecutionStats(ctx.statsDir, ctx.sessionId, ctx.workflowId, ctx.startMs, outcome, ctx.stepAdvanceCount);
964
- const lifecycle = sidecardLifecycleFor(result._tag, ctx.branchStrategy);
965
- switch (lifecycle.kind) {
966
- case 'delete_now':
967
- await fs.unlink(path.join(ctx.sessionsDir, `${ctx.sessionId}.json`)).catch(() => { });
968
- break;
969
- case 'retain_for_delivery':
970
- break;
971
- default:
972
- (0, assert_never_js_1.assertNever)(lifecycle);
973
- }
974
- if (result._tag === 'success' && ctx.branchStrategy !== 'worktree') {
975
- await fs.unlink(ctx.conversationPath).catch(() => { });
976
- }
977
- }
978
- function buildSessionContext(trigger, context, firstStepPrompt, effectiveWorkspacePath) {
979
- const workspaceContext = context.workspaceRules[0]?.content ?? null;
980
- const sessionNotes = context.sessionHistory.map((n) => n.content);
981
- const sessionState = buildSessionRecap(sessionNotes);
982
- const systemPrompt = buildSystemPrompt(trigger, sessionState, context.soulContent, workspaceContext, effectiveWorkspacePath);
983
- const contextJson = trigger.context
984
- ? `\n\nTrigger context:\n\`\`\`json\n${JSON.stringify(trigger.context, null, 2)}\n\`\`\``
985
- : '';
986
- const initialPrompt = firstStepPrompt +
987
- contextJson +
988
- '\n\nComplete all step work, then call complete_step with your notes to advance.';
989
- const sessionTimeoutMs = (trigger.agentConfig?.maxSessionMinutes ?? exports.DEFAULT_SESSION_TIMEOUT_MINUTES) * 60 * 1000;
990
- const maxTurns = trigger.agentConfig?.maxTurns ?? exports.DEFAULT_MAX_TURNS;
991
- const stallTimeoutMs = (trigger.agentConfig?.stallTimeoutSeconds ?? exports.DEFAULT_STALL_TIMEOUT_SECONDS) * 1000;
992
- return { systemPrompt, initialPrompt, sessionTimeoutMs, maxTurns, stallTimeoutMs };
993
- }
994
- async function buildPreAgentSession(trigger, ctx, apiKey, sessionId, startMs, statsDir, sessionsDir, emitter, daemonRegistry, activeSessionSet, source) {
995
- let agentClient;
996
- let modelId;
997
- try {
998
- ({ agentClient, modelId } = buildAgentClient(trigger, apiKey, process.env));
999
- if (trigger.agentConfig?.model) {
1000
- console.log(`[WorkflowRunner] Model: ${modelId} (override from agentConfig.model)`);
1001
- }
1002
- else {
1003
- const usesBedrock = !!process.env['AWS_PROFILE'] || !!process.env['AWS_ACCESS_KEY_ID'];
1004
- if (usesBedrock) {
1005
- console.log(`[WorkflowRunner] Model: ${modelId} (amazon-bedrock, detected from AWS env)`);
1006
- }
1007
- else {
1008
- console.log(`[WorkflowRunner] Model: ${modelId} (anthropic direct). Set agentConfig.model or AWS env vars to use Bedrock.`);
1009
- }
1010
- }
1011
- }
1012
- catch (e) {
1013
- const message = e instanceof Error ? e.message : String(e);
1014
- return { kind: 'complete', result: { _tag: 'error', workflowId: trigger.workflowId, message, stopReason: 'error' }, workrailSessionId: null, handle: undefined };
1015
- }
1016
- const state = createSessionState('');
1017
- let continueToken;
1018
- let checkpointToken;
1019
- let firstStepPrompt;
1020
- let isComplete;
1021
- const effectiveSource = source ?? { kind: 'allocate', trigger };
1022
- if (effectiveSource.kind === 'pre_allocated') {
1023
- const s = effectiveSource.session;
1024
- continueToken = s.continueToken;
1025
- checkpointToken = s.checkpointToken ?? null;
1026
- firstStepPrompt = s.firstStepPrompt;
1027
- isComplete = s.isComplete;
1028
- }
1029
- else {
1030
- const startResult = await (0, start_js_1.executeStartWorkflow)({ workflowId: trigger.workflowId, workspacePath: trigger.workspacePath, goal: trigger.goal }, ctx, { is_autonomous: 'true', workspacePath: trigger.workspacePath, triggerSource: 'daemon' });
1031
- if (startResult.isErr()) {
1032
- return {
1033
- kind: 'complete',
1034
- result: {
1035
- _tag: 'error',
1036
- workflowId: trigger.workflowId,
1037
- message: `start_workflow failed: ${startResult.error.kind} -- ${JSON.stringify(startResult.error)}`,
1038
- stopReason: 'error',
1039
- },
1040
- workrailSessionId: null,
1041
- handle: undefined,
1042
- };
1043
- }
1044
- const r = startResult.value.response;
1045
- continueToken = r.continueToken ?? '';
1046
- checkpointToken = r.checkpointToken ?? null;
1047
- firstStepPrompt = r.pending?.prompt ?? '';
1048
- isComplete = r.isComplete;
1049
- }
1050
- state.currentContinueToken = continueToken;
1051
- if (continueToken) {
1052
- const decoded = await (0, v2_token_ops_js_1.parseContinueTokenOrFail)(continueToken, ctx.v2.tokenCodecPorts, ctx.v2.tokenAliasStore);
1053
- if (decoded.isOk()) {
1054
- state.workrailSessionId = decoded.value.sessionId;
1055
- }
1056
- else {
1057
- console.error(`[WorkflowRunner] Error: could not decode WorkRail session ID from continueToken -- isLive and liveActivity will not work. Reason: ${decoded.error.message}`);
1058
- }
1059
- }
1060
- if (continueToken) {
1061
- const persistResult = await (0, _shared_js_1.persistTokens)(sessionId, continueToken, checkpointToken, undefined, {
1062
- workflowId: trigger.workflowId,
1063
- goal: trigger.goal,
1064
- workspacePath: trigger.workspacePath,
1065
- });
1066
- if (persistResult.kind === 'err') {
1067
- return {
1068
- kind: 'complete',
1069
- result: {
1070
- _tag: 'error',
1071
- workflowId: trigger.workflowId,
1072
- message: `Initial token persist failed: ${persistResult.error.code} -- ${persistResult.error.message}`,
1073
- stopReason: 'error',
1074
- },
1075
- workrailSessionId: state.workrailSessionId,
1076
- handle: undefined,
1077
- };
1078
- }
1079
- }
1080
- let sessionWorkspacePath = trigger.workspacePath;
1081
- let sessionWorktreePath;
1082
- if (effectiveSource.kind === 'pre_allocated' && effectiveSource.session.sessionWorkspacePath !== undefined) {
1083
- sessionWorkspacePath = effectiveSource.session.sessionWorkspacePath;
1084
- sessionWorktreePath = effectiveSource.session.sessionWorkspacePath;
1085
- }
1086
- if (trigger.branchStrategy === 'worktree') {
1087
- const branchPrefix = trigger.branchPrefix ?? 'worktrain/';
1088
- const baseBranch = trigger.baseBranch ?? 'main';
1089
- sessionWorkspacePath = path.join(exports.WORKTREES_DIR, sessionId);
1090
- sessionWorktreePath = sessionWorkspacePath;
1091
- try {
1092
- await fs.mkdir(exports.WORKTREES_DIR, { recursive: true });
1093
- await execFileAsync('git', ['-C', trigger.workspacePath, 'fetch', 'origin', baseBranch]);
1094
- await execFileAsync('git', [
1095
- '-C', trigger.workspacePath,
1096
- 'worktree', 'add',
1097
- sessionWorkspacePath,
1098
- '-b', `${branchPrefix}${sessionId}`,
1099
- `origin/${baseBranch}`,
1100
- ]);
1101
- const worktreePersistResult = await (0, _shared_js_1.persistTokens)(sessionId, continueToken ?? state.currentContinueToken, checkpointToken, sessionWorktreePath, { workflowId: trigger.workflowId, goal: trigger.goal, workspacePath: trigger.workspacePath });
1102
- if (worktreePersistResult.kind === 'err') {
1103
- console.error(`[WorkflowRunner] Worktree sidecar persist failed: ${worktreePersistResult.error.code} -- ${worktreePersistResult.error.message}`);
1104
- try {
1105
- await execFileAsync('git', ['-C', trigger.workspacePath, 'worktree', 'remove', '--force', sessionWorkspacePath]);
1106
- }
1107
- catch { }
1108
- return {
1109
- kind: 'complete',
1110
- result: {
1111
- _tag: 'error',
1112
- workflowId: trigger.workflowId,
1113
- message: `Worktree sidecar persist failed: ${worktreePersistResult.error.code} -- ${worktreePersistResult.error.message}`,
1114
- stopReason: 'error',
1115
- },
1116
- workrailSessionId: state.workrailSessionId,
1117
- handle: undefined,
1118
- };
1119
- }
1120
- console.log(`[WorkflowRunner] Worktree created: sessionId=${sessionId} branch=${branchPrefix}${sessionId} path=${sessionWorkspacePath}`);
1121
- }
1122
- catch (e) {
1123
- const errMsg = e instanceof Error ? e.message : String(e);
1124
- console.error(`[WorkflowRunner] Worktree creation failed: sessionId=${sessionId} error=${errMsg}`);
1125
- return {
1126
- kind: 'complete',
1127
- result: { _tag: 'error', workflowId: trigger.workflowId, message: `Worktree creation failed: ${errMsg}`, stopReason: 'error' },
1128
- workrailSessionId: state.workrailSessionId,
1129
- handle: undefined,
1130
- };
1131
- }
1132
- }
1133
- let handle;
1134
- if (state.workrailSessionId !== null) {
1135
- daemonRegistry?.register(state.workrailSessionId, trigger.workflowId);
1136
- handle = activeSessionSet?.register(state.workrailSessionId, (text) => { state.pendingSteerParts.push(text); });
1137
- }
1138
- if (isComplete) {
1139
- return {
1140
- kind: 'complete',
1141
- result: {
1142
- _tag: 'success',
1143
- workflowId: trigger.workflowId,
1144
- stopReason: 'stop',
1145
- ...(sessionWorktreePath !== undefined ? { sessionWorkspacePath: sessionWorktreePath } : {}),
1146
- ...(sessionWorktreePath !== undefined ? { sessionId } : {}),
1147
- ...(trigger.botIdentity !== undefined ? { botIdentity: trigger.botIdentity } : {}),
1148
- },
1149
- workrailSessionId: state.workrailSessionId,
1150
- handle,
1151
- };
1152
- }
1153
- return {
1154
- kind: 'ready',
1155
- session: {
1156
- sessionId,
1157
- workrailSessionId: state.workrailSessionId,
1158
- continueToken,
1159
- checkpointToken,
1160
- sessionWorkspacePath,
1161
- sessionWorktreePath,
1162
- firstStepPrompt,
1163
- state,
1164
- spawnCurrentDepth: trigger.spawnDepth ?? 0,
1165
- spawnMaxDepth: trigger.agentConfig?.maxSubagentDepth ?? 3,
1166
- readFileState: new Map(),
1167
- agentClient,
1168
- modelId,
1169
- startMs,
1170
- ...(handle !== undefined ? { handle } : {}),
1171
- },
1172
- };
1173
- }
1174
- function constructTools(ctx, apiKey, schemas, scope) {
1175
- const { fileTracker, onAdvance, onComplete, onTokenUpdate, onIssueReported, getCurrentToken, sessionWorkspacePath, spawnCurrentDepth, spawnMaxDepth, emitter, activeSessionSet, } = scope;
1176
- const sid = scope.sessionId;
1177
- const workrailSid = scope.workrailSessionId;
1178
- const readFileStateMap = fileTracker.toMap();
1179
- return [
1180
- (0, continue_workflow_js_1.makeCompleteStepTool)(sid, ctx, getCurrentToken, onAdvance, onComplete, onTokenUpdate, schemas, index_js_1.executeContinueWorkflow, emitter, workrailSid),
1181
- (0, continue_workflow_js_1.makeContinueWorkflowTool)(sid, ctx, onAdvance, onComplete, schemas, index_js_1.executeContinueWorkflow, emitter, workrailSid),
1182
- (0, bash_js_1.makeBashTool)(sessionWorkspacePath, schemas, sid, emitter, workrailSid),
1183
- (0, file_tools_js_1.makeReadTool)(sessionWorkspacePath, readFileStateMap, schemas, sid, emitter, workrailSid),
1184
- (0, file_tools_js_1.makeWriteTool)(sessionWorkspacePath, readFileStateMap, schemas, sid, emitter, workrailSid),
1185
- (0, glob_grep_js_1.makeGlobTool)(sessionWorkspacePath, schemas, sid, emitter, workrailSid),
1186
- (0, glob_grep_js_1.makeGrepTool)(sessionWorkspacePath, schemas, sid, emitter, workrailSid),
1187
- (0, file_tools_js_1.makeEditTool)(sessionWorkspacePath, readFileStateMap, schemas, sid, emitter, workrailSid),
1188
- (0, report_issue_js_1.makeReportIssueTool)(sid, emitter, workrailSid, undefined, onIssueReported),
1189
- (0, spawn_agent_js_1.makeSpawnAgentTool)(sid, ctx, apiKey, workrailSid ?? '', spawnCurrentDepth, spawnMaxDepth, runWorkflow, schemas, emitter, activeSessionSet),
1190
- (0, signal_coordinator_js_1.makeSignalCoordinatorTool)(sid, emitter, workrailSid),
1191
- ];
1192
- }
1193
- function buildTurnEndSubscriber(ctx) {
1194
- return async (event) => {
1195
- if (event.type !== 'turn_end')
1196
- return;
1197
- for (const toolResult of event.toolResults) {
1198
- if (toolResult.isError) {
1199
- const errorText = toolResult.result?.content[0]?.text ?? 'tool error';
1200
- ctx.emitter?.emit({ kind: 'tool_error', sessionId: ctx.sessionId, toolName: toolResult.toolName, error: errorText.slice(0, 200), ...(0, _shared_js_1.withWorkrailSession)(ctx.state.workrailSessionId) });
1201
- }
1202
- }
1203
- ctx.state.turnCount++;
1204
- const signal = evaluateStuckSignals(ctx.state, ctx.stuckConfig);
1205
- if (signal !== null) {
1206
- if (signal.kind === 'max_turns_exceeded') {
1207
- setTerminalSignal(ctx.state, { kind: 'timeout', reason: 'max_turns' });
1208
- ctx.emitter?.emit({ kind: 'agent_stuck', sessionId: ctx.sessionId, reason: 'timeout_imminent', detail: 'Max-turn limit reached', ...(0, _shared_js_1.withWorkrailSession)(ctx.state.workrailSessionId) });
1209
- ctx.agent.abort();
1210
- return;
1211
- }
1212
- else if (signal.kind === 'repeated_tool_call') {
1213
- ctx.emitter?.emit({ kind: 'agent_stuck', sessionId: ctx.sessionId, reason: 'repeated_tool_call', detail: `Same tool+args called ${ctx.stuckRepeatThreshold} times: ${signal.toolName}`, toolName: signal.toolName, argsSummary: signal.argsSummary, ...(0, _shared_js_1.withWorkrailSession)(ctx.state.workrailSessionId) });
1214
- void writeStuckOutboxEntry({ workflowId: ctx.workflowId, reason: 'repeated_tool_call', ...(ctx.state.issueSummaries.length > 0 ? { issueSummaries: [...ctx.state.issueSummaries] } : {}) });
1215
- if (ctx.stuckConfig.stuckAbortPolicy !== 'notify_only') {
1216
- if (setTerminalSignal(ctx.state, { kind: 'stuck', reason: 'repeated_tool_call' })) {
1217
- ctx.agent.abort();
1218
- return;
1219
- }
1220
- }
1221
- }
1222
- else if (signal.kind === 'no_progress') {
1223
- ctx.emitter?.emit({ kind: 'agent_stuck', sessionId: ctx.sessionId, reason: 'no_progress', detail: `${signal.turnCount} turns used, 0 step advances (${signal.maxTurns} turn limit)`, ...(0, _shared_js_1.withWorkrailSession)(ctx.state.workrailSessionId) });
1224
- if (ctx.stuckConfig.noProgressAbortEnabled) {
1225
- void writeStuckOutboxEntry({ workflowId: ctx.workflowId, reason: 'no_progress', ...(ctx.state.issueSummaries.length > 0 ? { issueSummaries: [...ctx.state.issueSummaries] } : {}) });
1226
- if (ctx.stuckConfig.stuckAbortPolicy !== 'notify_only') {
1227
- if (setTerminalSignal(ctx.state, { kind: 'stuck', reason: 'no_progress' })) {
1228
- ctx.agent.abort();
1229
- return;
1230
- }
1231
- }
1232
- }
1233
- }
1234
- else if (signal.kind === 'timeout_imminent') {
1235
- ctx.emitter?.emit({ kind: 'agent_stuck', sessionId: ctx.sessionId, reason: 'timeout_imminent', detail: `${signal.timeoutReason === 'wall_clock' ? 'Wall-clock timeout' : 'Max-turn limit'} reached`, ...(0, _shared_js_1.withWorkrailSession)(ctx.state.workrailSessionId) });
1236
- }
1237
- else {
1238
- (0, assert_never_js_1.assertNever)(signal);
1239
- }
1240
- }
1241
- (0, conversation_flusher_js_1.flushConversation)(ctx.agent.state.messages, ctx.lastFlushedRef, ctx.conversationPath, appendConversationMessages);
1242
- (0, step_injector_js_1.injectPendingSteps)(ctx.state, ctx.agent);
1243
- };
1244
- }
1245
- function buildAgentCallbacks(sessionId, state, modelId, emitter, stuckRepeatThreshold, workflowId) {
1246
- return {
1247
- onLlmTurnStarted: ({ messageCount }) => {
1248
- emitter?.emit({ kind: 'llm_turn_started', sessionId, messageCount, modelId, ...(0, _shared_js_1.withWorkrailSession)(state.workrailSessionId) });
1249
- },
1250
- onLlmTurnCompleted: ({ stopReason, outputTokens, inputTokens, toolNamesRequested }) => {
1251
- emitter?.emit({ kind: 'llm_turn_completed', sessionId, stopReason, outputTokens, inputTokens, toolNamesRequested, ...(0, _shared_js_1.withWorkrailSession)(state.workrailSessionId) });
1252
- },
1253
- onToolCallStarted: ({ toolName, argsSummary }) => {
1254
- emitter?.emit({ kind: 'tool_call_started', sessionId, toolName, argsSummary, ...(0, _shared_js_1.withWorkrailSession)(state.workrailSessionId) });
1255
- state.lastNToolCalls.push({ toolName, argsSummary });
1256
- if (state.lastNToolCalls.length > stuckRepeatThreshold)
1257
- state.lastNToolCalls.shift();
1258
- },
1259
- onToolCallCompleted: ({ toolName, durationMs, resultSummary }) => {
1260
- emitter?.emit({ kind: 'tool_call_completed', sessionId, toolName, durationMs, resultSummary, ...(0, _shared_js_1.withWorkrailSession)(state.workrailSessionId) });
1261
- },
1262
- onToolCallFailed: ({ toolName, durationMs, errorMessage }) => {
1263
- emitter?.emit({ kind: 'tool_call_failed', sessionId, toolName, durationMs, errorMessage, ...(0, _shared_js_1.withWorkrailSession)(state.workrailSessionId) });
1264
- },
1265
- onStallDetected: () => {
1266
- setTerminalSignal(state, { kind: 'stuck', reason: 'stall' });
1267
- emitter?.emit({
1268
- kind: 'agent_stuck',
1269
- sessionId,
1270
- reason: 'stall',
1271
- detail: `No LLM API call started within the stall timeout window. Last tool calls: ${state.lastNToolCalls.map((c) => c.toolName).join(', ') || 'none'}`,
1272
- ...(0, _shared_js_1.withWorkrailSession)(state.workrailSessionId),
1273
- });
1274
- void writeStuckOutboxEntry({
1275
- workflowId: workflowId ?? sessionId,
1276
- reason: 'stall',
1277
- ...(state.issueSummaries.length > 0 ? { issueSummaries: [...state.issueSummaries] } : {}),
1278
- });
1279
- },
1280
- };
1281
- }
1282
- function buildSessionResult(state, stopReason, errorMessage, trigger, sessionId, sessionWorktreePath) {
1283
- if (state.terminalSignal !== null) {
1284
- const signal = state.terminalSignal;
1285
- if (signal.kind === 'stuck') {
1286
- return {
1287
- _tag: 'stuck',
1288
- workflowId: trigger.workflowId,
1289
- reason: signal.reason,
1290
- message: `Session aborted: stuck heuristic fired (${signal.reason})`,
1291
- stopReason: 'aborted',
1292
- ...(state.issueSummaries.length > 0 ? { issueSummaries: [...state.issueSummaries] } : {}),
1293
- };
1294
- }
1295
- if (signal.kind === 'timeout') {
1296
- const limitDescription = signal.reason === 'wall_clock'
1297
- ? `${trigger.agentConfig?.maxSessionMinutes ?? exports.DEFAULT_SESSION_TIMEOUT_MINUTES} minutes`
1298
- : `${trigger.agentConfig?.maxTurns ?? exports.DEFAULT_MAX_TURNS} turns`;
1299
- return {
1300
- _tag: 'timeout',
1301
- workflowId: trigger.workflowId,
1302
- reason: signal.reason,
1303
- message: `Workflow ${signal.reason === 'wall_clock' ? 'timed out' : 'exceeded turn limit'} after ${limitDescription}`,
1304
- stopReason: 'aborted',
1305
- };
1306
- }
1307
- return (0, assert_never_js_1.assertNever)(signal);
1308
- }
1309
- if (stopReason === 'error' || errorMessage) {
1310
- const errMsg = errorMessage ?? 'Agent stopped with error reason';
1311
- const lastToolCalled = state.lastNToolCalls.length > 0 ? state.lastNToolCalls[state.lastNToolCalls.length - 1] : null;
1312
- const stuckMarker = `\n\nWORKTRAIN_STUCK: ${JSON.stringify({
1313
- reason: 'session_error',
1314
- error: errMsg.slice(0, 500),
1315
- workflowId: trigger.workflowId,
1316
- sessionId,
1317
- turnCount: state.turnCount,
1318
- stepAdvanceCount: state.stepAdvanceCount,
1319
- ...(lastToolCalled !== null && { lastToolCalled }),
1320
- ...(state.issueSummaries.length > 0 && { issueSummaries: state.issueSummaries }),
1321
- })}`;
1322
- return {
1323
- _tag: 'error',
1324
- workflowId: trigger.workflowId,
1325
- message: errMsg,
1326
- stopReason,
1327
- lastStepNotes: stuckMarker,
1328
- };
1329
- }
1330
- return {
1331
- _tag: 'success',
1332
- workflowId: trigger.workflowId,
1333
- stopReason,
1334
- ...(state.lastStepNotes !== undefined ? { lastStepNotes: state.lastStepNotes } : {}),
1335
- ...(state.lastStepArtifacts !== undefined ? { lastStepArtifacts: state.lastStepArtifacts } : {}),
1336
- ...(sessionWorktreePath !== undefined ? { sessionWorkspacePath: sessionWorktreePath } : {}),
1337
- ...(sessionWorktreePath !== undefined ? { sessionId } : {}),
1338
- ...(trigger.botIdentity !== undefined ? { botIdentity: trigger.botIdentity } : {}),
1339
- };
1340
- }
1341
- async function buildAgentReadySession(preAgentSession, trigger, ctx, apiKey, sessionId, emitter, daemonRegistry, activeSessionSet) {
1342
- const { state, firstStepPrompt, sessionWorkspacePath, sessionWorktreePath, agentClient, modelId } = preAgentSession;
1343
- const startContinueToken = preAgentSession.continueToken;
1344
- const handle = preAgentSession.handle;
1345
- const MAX_ISSUE_SUMMARIES = 10;
1346
- const STUCK_REPEAT_THRESHOLD = 3;
1347
- const onAdvance = (stepText, continueToken) => {
1348
- state.pendingSteerParts.push(stepText);
1349
- state.stepAdvanceCount++;
1350
- state.currentContinueToken = continueToken;
1351
- if (state.workrailSessionId !== null)
1352
- daemonRegistry?.heartbeat(state.workrailSessionId);
1353
- emitter?.emit({ kind: 'step_advanced', sessionId, ...(0, _shared_js_1.withWorkrailSession)(state.workrailSessionId) });
1354
- };
1355
- const onComplete = (notes, artifacts) => {
1356
- state.isComplete = true;
1357
- state.lastStepNotes = notes;
1358
- state.lastStepArtifacts = artifacts;
1359
- };
1360
- const schemas = getSchemas();
1361
- const scope = {
1362
- fileTracker: new session_scope_js_1.DefaultFileStateTracker(preAgentSession.readFileState),
1363
- onAdvance,
1364
- onComplete,
1365
- onTokenUpdate: (t) => { state.currentContinueToken = t; },
1366
- onIssueReported: (summary) => {
1367
- if (state.issueSummaries.length < MAX_ISSUE_SUMMARIES) {
1368
- state.issueSummaries.push(summary);
1369
- }
1370
- },
1371
- onSteer: (text) => { state.pendingSteerParts.push(text); },
1372
- getCurrentToken: () => state.currentContinueToken,
1373
- sessionWorkspacePath,
1374
- spawnCurrentDepth: preAgentSession.spawnCurrentDepth,
1375
- spawnMaxDepth: preAgentSession.spawnMaxDepth,
1376
- workrailSessionId: state.workrailSessionId,
1377
- emitter,
1378
- sessionId,
1379
- workflowId: trigger.workflowId,
1380
- activeSessionSet,
1381
- };
1382
- const tools = constructTools(ctx, apiKey, schemas, scope);
1383
- const contextLoader = new context_loader_js_1.DefaultContextLoader(loadDaemonSoul, loadWorkspaceContext, loadSessionNotes, ctx);
1384
- const baseCtx = await contextLoader.loadBase(trigger);
1385
- const contextBundle = await contextLoader.loadSession(startContinueToken, baseCtx);
1386
- const effectiveWorkspacePath = sessionWorkspacePath ?? trigger.workspacePath;
1387
- const sessionCtx = buildSessionContext(trigger, contextBundle, firstStepPrompt || 'No step content available', effectiveWorkspacePath);
1388
- const agentCallbacks = buildAgentCallbacks(sessionId, state, modelId, emitter, STUCK_REPEAT_THRESHOLD, trigger.workflowId);
1389
- const agent = new agent_loop_js_1.AgentLoop({
1390
- systemPrompt: sessionCtx.systemPrompt,
1391
- modelId,
1392
- tools,
1393
- client: agentClient,
1394
- toolExecution: 'sequential',
1395
- callbacks: agentCallbacks,
1396
- ...(trigger.agentConfig?.maxOutputTokens !== undefined
1397
- ? { maxTokens: trigger.agentConfig.maxOutputTokens }
1398
- : {}),
1399
- stallTimeoutMs: sessionCtx.stallTimeoutMs,
1400
- });
1401
- handle?.setAgent(agent);
1402
- return {
1403
- preAgentSession,
1404
- contextBundle,
1405
- scope,
1406
- tools,
1407
- sessionCtx,
1408
- handle,
1409
- sessionId,
1410
- workflowId: trigger.workflowId,
1411
- worktreePath: sessionWorktreePath,
1412
- agent,
1413
- stuckRepeatThreshold: STUCK_REPEAT_THRESHOLD,
1414
- };
1415
- }
1416
- async function runAgentLoop(session, trigger, conversationPath) {
1417
- const { agent, preAgentSession, sessionCtx, sessionId, handle } = session;
1418
- const { state } = preAgentSession;
1419
- const { emitter } = session.scope;
1420
- const { stuckRepeatThreshold } = session;
1421
- const { sessionTimeoutMs, maxTurns } = sessionCtx;
1422
- const stuckConfig = {
1423
- maxTurns,
1424
- stuckAbortPolicy: trigger.agentConfig?.stuckAbortPolicy ?? 'abort',
1425
- noProgressAbortEnabled: trigger.agentConfig?.noProgressAbortEnabled ?? false,
1426
- stuckRepeatThreshold,
1427
- };
1428
- const lastFlushedRef = { count: 0 };
1429
- const unsubscribe = agent.subscribe(buildTurnEndSubscriber({
1430
- agent,
1431
- state,
1432
- stuckConfig,
1433
- sessionId,
1434
- workflowId: trigger.workflowId,
1435
- emitter,
1436
- conversationPath,
1437
- lastFlushedRef,
1438
- stuckRepeatThreshold,
1439
- }));
1440
- let stopReason = 'stop';
1441
- let errorMessage;
1442
- let timeoutHandle;
1443
- try {
1444
- const timeoutPromise = new Promise((_, reject) => {
1445
- timeoutHandle = setTimeout(() => {
1446
- setTerminalSignal(state, { kind: 'timeout', reason: 'wall_clock' });
1447
- reject(new Error('Workflow timed out'));
1448
- }, sessionTimeoutMs);
1449
- });
1450
- console.log(`[WorkflowRunner] Agent loop started: sessionId=${sessionId} workflowId=${trigger.workflowId} modelId=${preAgentSession.modelId}`);
1451
- await Promise.race([agent.prompt(buildUserMessage(sessionCtx.initialPrompt)), timeoutPromise])
1452
- .catch((err) => {
1453
- agent.abort();
1454
- throw err;
1455
- });
1456
- const messages = agent.state.messages;
1457
- let lastAssistant;
1458
- for (let i = messages.length - 1; i >= 0; i--) {
1459
- const m = messages[i];
1460
- if ('role' in m && m.role === 'assistant') {
1461
- lastAssistant = m;
1462
- break;
1463
- }
1464
- }
1465
- stopReason = lastAssistant?.stopReason ?? 'stop';
1466
- errorMessage = lastAssistant?.errorMessage;
1467
- }
1468
- catch (err) {
1469
- errorMessage = err instanceof Error ? err.message : String(err);
1470
- stopReason = 'error';
1471
- }
1472
- finally {
1473
- unsubscribe();
1474
- const remainingMessages = agent.state.messages.slice(lastFlushedRef.count);
1475
- void appendConversationMessages(conversationPath, remainingMessages).catch(() => { });
1476
- if (timeoutHandle !== undefined)
1477
- clearTimeout(timeoutHandle);
1478
- handle?.dispose();
1479
- console.log(`[WorkflowRunner] Agent loop ended: sessionId=${sessionId} stopReason=${stopReason}${errorMessage ? ` error=${errorMessage.slice(0, 120)}` : ''}`);
1480
- }
1481
- if (stopReason === 'error') {
1482
- return { kind: 'aborted', errorMessage };
1483
- }
1484
- return { kind: 'completed', stopReason, errorMessage };
1485
- }
1486
- async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, activeSessionSet, _statsDir, _sessionsDir, source) {
1487
- const statsDir = _statsDir ?? DAEMON_STATS_DIR;
120
+ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, activeSessionSet, _statsDir, _sessionsDir, source, enricherDeps) {
121
+ const statsDir = _statsDir ?? index_js_2.DAEMON_STATS_DIR;
1488
122
  const sessionsDir = _sessionsDir ?? _shared_js_1.DAEMON_SESSIONS_DIR;
1489
123
  const startMs = Date.now();
1490
124
  const sessionId = (0, node_crypto_1.randomUUID)();
@@ -1495,7 +129,15 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, active
1495
129
  workflowId: trigger.workflowId,
1496
130
  workspacePath: trigger.workspacePath,
1497
131
  });
1498
- const preResult = await buildPreAgentSession(trigger, ctx, apiKey, sessionId, startMs, statsDir, sessionsDir, emitter, daemonRegistry, activeSessionSet, source);
132
+ let enricherResult = workflow_enricher_js_1.EMPTY_RESULT;
133
+ if (enricherDeps !== undefined && (0, workflow_enricher_js_1.shouldEnrich)(trigger)) {
134
+ const { assembledContextSummary } = (0, types_js_1.extractContextSlots)(trigger.context);
135
+ const policy = assembledContextSummary !== undefined && assembledContextSummary.trim().length > 0
136
+ ? 'skip_coordinator_provided'
137
+ : 'inject';
138
+ enricherResult = await (0, workflow_enricher_js_1.enrichTriggerContext)(trigger, enricherDeps, policy);
139
+ }
140
+ const preResult = await (0, index_js_4.buildPreAgentSession)(trigger, ctx, apiKey, sessionId, startMs, statsDir, sessionsDir, emitter, daemonRegistry, activeSessionSet, source);
1499
141
  if (preResult.kind === 'complete') {
1500
142
  const earlyCtx = {
1501
143
  sessionId,
@@ -1511,12 +153,12 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, active
1511
153
  workflowId: trigger.workflowId,
1512
154
  };
1513
155
  preResult.handle?.dispose();
1514
- await finalizeSession(preResult.result, earlyCtx);
156
+ await (0, index_js_4.finalizeSession)(preResult.result, earlyCtx);
1515
157
  return preResult.result;
1516
158
  }
1517
- const readySession = await buildAgentReadySession(preResult.session, trigger, ctx, apiKey, sessionId, emitter, daemonRegistry, activeSessionSet);
159
+ const readySession = await (0, index_js_4.buildAgentReadySession)(preResult.session, trigger, ctx, apiKey, sessionId, emitter, daemonRegistry, activeSessionSet, runWorkflow, enricherResult);
1518
160
  const conversationPath = path.join(sessionsDir, `${sessionId}-conversation.jsonl`);
1519
- const outcome = await runAgentLoop(readySession, trigger, conversationPath);
161
+ const outcome = await (0, index_js_4.runAgentLoop)(readySession, trigger, conversationPath);
1520
162
  const stopReason = outcome.kind === 'aborted' ? 'error' : outcome.stopReason;
1521
163
  const errorMessage = outcome.errorMessage;
1522
164
  const { state, sessionWorktreePath } = readySession.preAgentSession;
@@ -1533,7 +175,7 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, active
1533
175
  daemonRegistry,
1534
176
  workflowId: trigger.workflowId,
1535
177
  };
1536
- const result = buildSessionResult(state, stopReason, errorMessage, trigger, sessionId, sessionWorktreePath);
1537
- await finalizeSession(result, finalizationCtx);
178
+ const result = (0, index_js_1.buildSessionResult)(state, stopReason, errorMessage, trigger, sessionId, sessionWorktreePath);
179
+ await (0, index_js_4.finalizeSession)(result, finalizationCtx);
1538
180
  return result;
1539
181
  }