@exaudeus/workrail 3.72.0 → 3.72.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-worktrain.js +4 -6
- package/dist/console-ui/assets/{index-CTza1zb5.js → index-J97yE18I.js} +1 -1
- package/dist/console-ui/index.html +1 -1
- package/dist/daemon/active-sessions.d.ts +17 -0
- package/dist/daemon/active-sessions.js +55 -0
- package/dist/daemon/context-loader.d.ts +32 -0
- package/dist/daemon/context-loader.js +34 -0
- package/dist/daemon/session-scope.d.ts +3 -2
- package/dist/daemon/tools/_shared.d.ts +38 -0
- package/dist/daemon/tools/_shared.js +101 -0
- package/dist/daemon/tools/bash.d.ts +3 -0
- package/dist/daemon/tools/bash.js +57 -0
- package/dist/daemon/tools/continue-workflow.d.ts +6 -0
- package/dist/daemon/tools/continue-workflow.js +208 -0
- package/dist/daemon/tools/file-tools.d.ts +6 -0
- package/dist/daemon/tools/file-tools.js +195 -0
- package/dist/daemon/tools/glob-grep.d.ts +4 -0
- package/dist/daemon/tools/glob-grep.js +172 -0
- package/dist/daemon/tools/report-issue.d.ts +3 -0
- package/dist/daemon/tools/report-issue.js +129 -0
- package/dist/daemon/tools/signal-coordinator.d.ts +4 -0
- package/dist/daemon/tools/signal-coordinator.js +105 -0
- package/dist/daemon/tools/spawn-agent.d.ts +6 -0
- package/dist/daemon/tools/spawn-agent.js +135 -0
- package/dist/daemon/workflow-runner.d.ts +56 -30
- package/dist/daemon/workflow-runner.js +172 -984
- package/dist/infrastructure/storage/workflow-resolution.js +5 -6
- package/dist/manifest.json +131 -27
- package/dist/mcp/handlers/shared/request-workflow-reader.js +14 -0
- package/dist/trigger/coordinator-deps.d.ts +15 -0
- package/dist/trigger/coordinator-deps.js +322 -0
- package/dist/trigger/delivery-pipeline.d.ts +18 -0
- package/dist/trigger/delivery-pipeline.js +148 -0
- package/dist/trigger/dispatch-deduplicator.d.ts +6 -0
- package/dist/trigger/dispatch-deduplicator.js +24 -0
- package/dist/trigger/trigger-listener.d.ts +2 -3
- package/dist/trigger/trigger-listener.js +9 -276
- package/dist/trigger/trigger-router.d.ts +8 -7
- package/dist/trigger/trigger-router.js +19 -97
- package/dist/v2/usecases/console-routes.js +10 -2
- package/docs/ideas/backlog.md +82 -48
- package/package.json +3 -2
|
@@ -36,16 +36,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.TriggerRouter = void 0;
|
|
37
37
|
exports.interpolateGoalTemplate = interpolateGoalTemplate;
|
|
38
38
|
const crypto = __importStar(require("node:crypto"));
|
|
39
|
-
const fs = __importStar(require("node:fs/promises"));
|
|
40
|
-
const path = __importStar(require("node:path"));
|
|
41
39
|
const node_child_process_1 = require("node:child_process");
|
|
42
40
|
const node_util_1 = require("node:util");
|
|
43
|
-
const workflow_runner_js_1 = require("../daemon/workflow-runner.js");
|
|
44
41
|
const assert_never_js_1 = require("../runtime/assert-never.js");
|
|
45
42
|
const index_js_1 = require("../v2/infra/in-memory/keyed-async-queue/index.js");
|
|
46
43
|
const delivery_client_js_1 = require("./delivery-client.js");
|
|
47
|
-
const
|
|
44
|
+
const delivery_pipeline_js_1 = require("./delivery-pipeline.js");
|
|
48
45
|
const adaptive_pipeline_js_1 = require("../coordinators/adaptive-pipeline.js");
|
|
46
|
+
const dispatch_deduplicator_js_1 = require("./dispatch-deduplicator.js");
|
|
49
47
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
50
48
|
function interpolateGoalTemplate(template, staticGoal, payload, triggerId) {
|
|
51
49
|
const TOKEN_RE = /\{\{([^}]+)\}\}/g;
|
|
@@ -123,68 +121,17 @@ function validateHmac(rawBody, secret, headerValue) {
|
|
|
123
121
|
async function maybeRunDelivery(triggerId, trigger, result, execFn) {
|
|
124
122
|
if (result._tag !== 'success')
|
|
125
123
|
return;
|
|
126
|
-
if (trigger.autoCommit !== true) {
|
|
127
|
-
console.log(`[TriggerRouter] Delivery skipped: triggerId=${triggerId} -- autoCommit not set for this trigger.`);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
124
|
if (result.lastStepNotes === undefined) {
|
|
131
|
-
|
|
132
|
-
`
|
|
133
|
-
|
|
125
|
+
if (trigger.autoCommit === true) {
|
|
126
|
+
console.warn(`[TriggerRouter] Delivery skipped: triggerId=${triggerId} -- ` +
|
|
127
|
+
`lastStepNotes is absent (agent did not provide notes on the final step). ` +
|
|
128
|
+
`Ensure the workflow produces a JSON handoff block in its final step notes.`);
|
|
129
|
+
}
|
|
134
130
|
return;
|
|
135
131
|
}
|
|
136
|
-
|
|
137
|
-
if (parseResult.kind === 'err') {
|
|
138
|
-
console.warn(`[TriggerRouter] Delivery skipped: triggerId=${triggerId} -- ` +
|
|
139
|
-
`handoff artifact not parseable: ${parseResult.error}. ` +
|
|
140
|
-
`Ensure the workflow's final step produces a JSON block with commitType, filesChanged, etc.`);
|
|
132
|
+
if (trigger.autoCommit !== true)
|
|
141
133
|
return;
|
|
142
|
-
|
|
143
|
-
const deliveryCwd = result.sessionWorkspacePath ?? trigger.workspacePath;
|
|
144
|
-
const deliveryResult = await (0, delivery_action_js_1.runDelivery)(parseResult.value, deliveryCwd, {
|
|
145
|
-
autoCommit: trigger.autoCommit,
|
|
146
|
-
autoOpenPR: trigger.autoOpenPR,
|
|
147
|
-
secretScan: trigger.secretScan ?? true,
|
|
148
|
-
triggerId,
|
|
149
|
-
workflowId: trigger.workflowId,
|
|
150
|
-
...(result.botIdentity !== undefined ? { botIdentity: result.botIdentity } : {}),
|
|
151
|
-
...(trigger.branchStrategy === 'worktree' && result.sessionWorkspacePath
|
|
152
|
-
? {
|
|
153
|
-
sessionId: result.sessionId ?? '',
|
|
154
|
-
branchPrefix: trigger.branchPrefix ?? 'worktrain/',
|
|
155
|
-
}
|
|
156
|
-
: {}),
|
|
157
|
-
}, execFn);
|
|
158
|
-
switch (deliveryResult._tag) {
|
|
159
|
-
case 'committed':
|
|
160
|
-
console.log(`[TriggerRouter] Delivery committed: triggerId=${triggerId} sha=${deliveryResult.sha}`);
|
|
161
|
-
break;
|
|
162
|
-
case 'pr_opened':
|
|
163
|
-
console.log(`[TriggerRouter] Delivery PR opened: triggerId=${triggerId} url=${deliveryResult.url}`);
|
|
164
|
-
break;
|
|
165
|
-
case 'skipped':
|
|
166
|
-
console.log(`[TriggerRouter] Delivery skipped: triggerId=${triggerId} reason=${deliveryResult.reason}`);
|
|
167
|
-
break;
|
|
168
|
-
case 'error':
|
|
169
|
-
console.warn(`[TriggerRouter] Delivery error: triggerId=${triggerId} phase=${deliveryResult.phase} ` +
|
|
170
|
-
`details=${deliveryResult.details}`);
|
|
171
|
-
break;
|
|
172
|
-
}
|
|
173
|
-
if (trigger.branchStrategy === 'worktree' && result.sessionWorkspacePath) {
|
|
174
|
-
try {
|
|
175
|
-
await execFn('git', ['-C', trigger.workspacePath, 'worktree', 'remove', '--force', result.sessionWorkspacePath], { cwd: trigger.workspacePath, timeout: 60000 });
|
|
176
|
-
console.log(`[TriggerRouter] Worktree removed: triggerId=${triggerId} path=${result.sessionWorkspacePath}`);
|
|
177
|
-
}
|
|
178
|
-
catch (err) {
|
|
179
|
-
console.warn(`[TriggerRouter] Could not remove worktree: triggerId=${triggerId} ` +
|
|
180
|
-
`path=${result.sessionWorkspacePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
181
|
-
}
|
|
182
|
-
if (result.sessionId !== undefined) {
|
|
183
|
-
await fs.unlink(path.join(workflow_runner_js_1.DAEMON_SESSIONS_DIR, `${result.sessionId}.json`)).catch(() => { });
|
|
184
|
-
await fs.unlink(path.join(workflow_runner_js_1.DAEMON_SESSIONS_DIR, `${result.sessionId}-conversation.jsonl`)).catch(() => { });
|
|
185
|
-
console.log(`[TriggerRouter] Session sidecar removed: triggerId=${triggerId} sessionId=${result.sessionId}`);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
134
|
+
await (0, delivery_pipeline_js_1.runDeliveryPipeline)(delivery_pipeline_js_1.DEFAULT_DELIVERY_PIPELINE, result, trigger, execFn, triggerId);
|
|
188
135
|
}
|
|
189
136
|
class Semaphore {
|
|
190
137
|
constructor(max) {
|
|
@@ -216,20 +163,19 @@ class Semaphore {
|
|
|
216
163
|
}
|
|
217
164
|
const DEFAULT_MAX_CONCURRENT_SESSIONS = 3;
|
|
218
165
|
class TriggerRouter {
|
|
219
|
-
constructor(index, ctx, apiKey, runWorkflowFn, execFn, maxConcurrentSessions, emitter, notificationService,
|
|
166
|
+
constructor(index, ctx, apiKey, runWorkflowFn, execFn, maxConcurrentSessions, emitter, notificationService, activeSessionSet, coordinatorDeps, modeExecutors, deduplicator) {
|
|
220
167
|
this.index = index;
|
|
221
168
|
this.ctx = ctx;
|
|
222
169
|
this.apiKey = apiKey;
|
|
223
170
|
this.runWorkflowFn = runWorkflowFn;
|
|
224
171
|
this.queue = new index_js_1.KeyedAsyncQueue();
|
|
225
|
-
this._recentAdaptiveDispatches = new Map();
|
|
226
172
|
this.execFn = execFn ?? execFileAsync;
|
|
227
173
|
this.emitter = emitter;
|
|
228
174
|
this.notificationService = notificationService;
|
|
229
|
-
this.
|
|
230
|
-
this.abortRegistry = abortRegistry;
|
|
175
|
+
this._activeSessionSet = activeSessionSet;
|
|
231
176
|
this._coordinatorDeps = coordinatorDeps;
|
|
232
177
|
this._modeExecutors = modeExecutors;
|
|
178
|
+
this._deduplicator = deduplicator ?? new dispatch_deduplicator_js_1.DispatchDeduplicator(TriggerRouter.ADAPTIVE_DEDUPE_TTL_MS);
|
|
233
179
|
const requested = maxConcurrentSessions ?? DEFAULT_MAX_CONCURRENT_SESSIONS;
|
|
234
180
|
const cap = Number.isNaN(requested) ? DEFAULT_MAX_CONCURRENT_SESSIONS : requested;
|
|
235
181
|
if (cap < 1) {
|
|
@@ -324,18 +270,10 @@ class TriggerRouter {
|
|
|
324
270
|
};
|
|
325
271
|
{
|
|
326
272
|
const dedupeKey = `${workflowTrigger.workflowId}::${workflowTrigger.goal}::${workflowTrigger.workspacePath}`;
|
|
327
|
-
|
|
328
|
-
for (const [key, ts] of this._recentAdaptiveDispatches) {
|
|
329
|
-
if (now - ts >= TriggerRouter.ADAPTIVE_DEDUPE_TTL_MS) {
|
|
330
|
-
this._recentAdaptiveDispatches.delete(key);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
const lastDispatch = this._recentAdaptiveDispatches.get(dedupeKey);
|
|
334
|
-
if (lastDispatch !== undefined && now - lastDispatch < TriggerRouter.ADAPTIVE_DEDUPE_TTL_MS) {
|
|
273
|
+
if (this._deduplicator.checkAndRecord(dedupeKey)) {
|
|
335
274
|
console.log(`[TriggerRouter] Skipping duplicate route dispatch: workflowId=${workflowTrigger.workflowId} goal="${workflowTrigger.goal.slice(0, 60)}" (already dispatched within 30s)`);
|
|
336
275
|
return { _tag: 'enqueued', triggerId: trigger.id };
|
|
337
276
|
}
|
|
338
|
-
this._recentAdaptiveDispatches.set(dedupeKey, now);
|
|
339
277
|
}
|
|
340
278
|
this.emitter?.emit({ kind: 'trigger_fired', triggerId: trigger.id, workflowId: trigger.workflowId });
|
|
341
279
|
const queueKey = trigger.concurrencyMode === 'parallel'
|
|
@@ -351,7 +289,7 @@ class TriggerRouter {
|
|
|
351
289
|
await this.semaphore.acquire();
|
|
352
290
|
let result;
|
|
353
291
|
try {
|
|
354
|
-
result = await this.runWorkflowFn(workflowTrigger, this.ctx, this.apiKey, undefined, this.emitter, this.
|
|
292
|
+
result = await this.runWorkflowFn(workflowTrigger, this.ctx, this.apiKey, undefined, this.emitter, this._activeSessionSet);
|
|
355
293
|
}
|
|
356
294
|
finally {
|
|
357
295
|
this.semaphore.release();
|
|
@@ -406,21 +344,13 @@ class TriggerRouter {
|
|
|
406
344
|
});
|
|
407
345
|
return { _tag: 'enqueued', triggerId: trigger.id };
|
|
408
346
|
}
|
|
409
|
-
dispatch(workflowTrigger) {
|
|
410
|
-
if (
|
|
347
|
+
dispatch(workflowTrigger, source) {
|
|
348
|
+
if (source?.kind !== 'pre_allocated') {
|
|
411
349
|
const dedupeKey = `${workflowTrigger.workflowId}::${workflowTrigger.goal}::${workflowTrigger.workspacePath}`;
|
|
412
|
-
|
|
413
|
-
for (const [key, ts] of this._recentAdaptiveDispatches) {
|
|
414
|
-
if (now - ts >= TriggerRouter.ADAPTIVE_DEDUPE_TTL_MS) {
|
|
415
|
-
this._recentAdaptiveDispatches.delete(key);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
const lastDispatch = this._recentAdaptiveDispatches.get(dedupeKey);
|
|
419
|
-
if (lastDispatch !== undefined && now - lastDispatch < TriggerRouter.ADAPTIVE_DEDUPE_TTL_MS) {
|
|
350
|
+
if (this._deduplicator.checkAndRecord(dedupeKey)) {
|
|
420
351
|
console.log(`[TriggerRouter] Skipping duplicate dispatch: workflowId=${workflowTrigger.workflowId} goal="${workflowTrigger.goal.slice(0, 60)}" (already dispatched within 30s)`);
|
|
421
352
|
return workflowTrigger.workflowId;
|
|
422
353
|
}
|
|
423
|
-
this._recentAdaptiveDispatches.set(dedupeKey, now);
|
|
424
354
|
}
|
|
425
355
|
else {
|
|
426
356
|
console.log(`[TriggerRouter] Pre-allocated session dispatched: workflowId=${workflowTrigger.workflowId} goal="${workflowTrigger.goal.slice(0, 60)}"`);
|
|
@@ -434,7 +364,7 @@ class TriggerRouter {
|
|
|
434
364
|
await this.semaphore.acquire();
|
|
435
365
|
let result;
|
|
436
366
|
try {
|
|
437
|
-
result = await this.runWorkflowFn(workflowTrigger, this.ctx, this.apiKey, undefined, this.emitter, this.
|
|
367
|
+
result = await this.runWorkflowFn(workflowTrigger, this.ctx, this.apiKey, undefined, this.emitter, this._activeSessionSet, undefined, undefined, source);
|
|
438
368
|
}
|
|
439
369
|
finally {
|
|
440
370
|
this.semaphore.release();
|
|
@@ -485,14 +415,7 @@ class TriggerRouter {
|
|
|
485
415
|
};
|
|
486
416
|
}
|
|
487
417
|
const dedupeKey = `${goal}::${workspace}`;
|
|
488
|
-
|
|
489
|
-
for (const [key, ts] of this._recentAdaptiveDispatches) {
|
|
490
|
-
if (now - ts >= TriggerRouter.ADAPTIVE_DEDUPE_TTL_MS) {
|
|
491
|
-
this._recentAdaptiveDispatches.delete(key);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
const lastDispatch = this._recentAdaptiveDispatches.get(dedupeKey);
|
|
495
|
-
if (lastDispatch !== undefined && now - lastDispatch < TriggerRouter.ADAPTIVE_DEDUPE_TTL_MS) {
|
|
418
|
+
if (this._deduplicator.checkAndRecord(dedupeKey)) {
|
|
496
419
|
console.log(`[TriggerRouter] Skipping duplicate adaptive dispatch: goal="${goal.slice(0, 60)}" ` +
|
|
497
420
|
`(already dispatched within 30s)`);
|
|
498
421
|
return {
|
|
@@ -503,7 +426,6 @@ class TriggerRouter {
|
|
|
503
426
|
},
|
|
504
427
|
};
|
|
505
428
|
}
|
|
506
|
-
this._recentAdaptiveDispatches.set(dedupeKey, now);
|
|
507
429
|
const opts = {
|
|
508
430
|
goal,
|
|
509
431
|
workspace,
|
|
@@ -646,8 +646,16 @@ function mountConsoleRoutes(app, consoleService, workflowService, timingRingBuff
|
|
|
646
646
|
else {
|
|
647
647
|
sessionHandle = workflowId;
|
|
648
648
|
}
|
|
649
|
-
const trigger = { workflowId, goal, workspacePath, context
|
|
650
|
-
|
|
649
|
+
const trigger = { workflowId, goal, workspacePath, context };
|
|
650
|
+
const allocatedSession = {
|
|
651
|
+
continueToken: startResponse.continueToken ?? '',
|
|
652
|
+
checkpointToken: startResponse.checkpointToken,
|
|
653
|
+
firstStepPrompt: startResponse.pending?.prompt ?? '',
|
|
654
|
+
isComplete: startResponse.isComplete,
|
|
655
|
+
triggerSource: 'mcp',
|
|
656
|
+
};
|
|
657
|
+
const source = { kind: 'pre_allocated', trigger, session: allocatedSession };
|
|
658
|
+
void (0, workflow_runner_js_1.runWorkflow)(trigger, v2ToolContext, apiKey ?? '', undefined, undefined, undefined, undefined, undefined, source).then((result) => {
|
|
651
659
|
if (result._tag === 'success') {
|
|
652
660
|
console.log(`[ConsoleRoutes] Auto dispatch completed: workflowId=${workflowId} stopReason=${result.stopReason}`);
|
|
653
661
|
}
|
package/docs/ideas/backlog.md
CHANGED
|
@@ -74,6 +74,22 @@ Agent writes a complete handoff block (commitType, prTitle, prBody, filesChanged
|
|
|
74
74
|
The autonomous workflow runner (`worktrain daemon`). Completely separate from the MCP server -- calls the engine directly in-process.
|
|
75
75
|
|
|
76
76
|
|
|
77
|
+
### Daemon architecture: remaining migrations (Apr 29, 2026)
|
|
78
|
+
|
|
79
|
+
**Status: partial** | A9 shipped Apr 29, 2026.
|
|
80
|
+
|
|
81
|
+
Track A (A1-A9) shipped and the `SessionSource` migration is complete. `WorkflowTrigger._preAllocatedStartResponse` is gone.
|
|
82
|
+
|
|
83
|
+
**Remaining items:**
|
|
84
|
+
|
|
85
|
+
- `CriticalEffect<T>` / `ObservabilityEffect` type distinction -- categorize side effects in `runAgentLoop` and finalization as either crash-relevant or observability-only
|
|
86
|
+
- `StateRef` mutation wrapper -- replace direct `state.pendingSteerParts.push()` mutations with an explicit mutation API
|
|
87
|
+
- Zod tool param validation -- replace manual `typeof` checks in tool factories with Zod schema validation (requires `zodToJsonSchema` or maintaining two sources of truth for param schemas)
|
|
88
|
+
- `createCoordinatorDeps` unit tests -- extraction in B3 improved testability; cover `spawnSession`, `awaitSessions`, `getAgentResult` at minimum
|
|
89
|
+
- Wire `AllocatedSession.triggerSource` to the `run_started` event for session attribution (one-liner once the event schema field is added -- see "Session trigger source attribution" entry below)
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
77
93
|
### `wr.refactoring` workflow (Apr 28, 2026)
|
|
78
94
|
|
|
79
95
|
**Status: idea** | Priority: medium
|
|
@@ -103,59 +119,35 @@ The `wr.coding-task` workflow has too much overhead for pure refactors (design r
|
|
|
103
119
|
|
|
104
120
|
---
|
|
105
121
|
|
|
106
|
-
### runWorkflow() functional core refactor --
|
|
122
|
+
### runWorkflow() functional core refactor -- Phases 2-4 (Apr 24-29, 2026)
|
|
107
123
|
|
|
108
|
-
**Status: done** |
|
|
124
|
+
**Status: done** | Phases 2-3 shipped Apr 29, 2026. Phase 4 (A1-A8) shipped Apr 29, 2026.
|
|
109
125
|
|
|
110
|
-
Phase 1
|
|
126
|
+
Phase 1 (PR #818): `tagToStatsOutcome`, `buildAgentClient`, `evaluateStuckSignals`, `SessionState`, `finalizeSession`.
|
|
127
|
+
Phase 2 (PR #830): `PreAgentSession`/`PreAgentSessionResult`, `buildPreAgentSession`, `constructTools`, `persistTokens` Result type, TDZ fix.
|
|
128
|
+
Phase 3 (PRs #835, #837): `buildTurnEndSubscriber`, `buildAgentCallbacks`, `buildSessionResult`. runWorkflow() body: 539 → 308 lines.
|
|
111
129
|
|
|
112
|
-
**
|
|
130
|
+
**Phase 4 (Track A, PRs #839-#861, Apr 29, 2026):**
|
|
131
|
+
- A1: `runStartupRecovery` apiKey injected as parameter (removes process.env read)
|
|
132
|
+
- A2: Turn-end collaborators extracted to `src/daemon/turn-end/` (`step-injector`, `detect-stuck`, `conversation-flusher`)
|
|
133
|
+
- A3: `SessionScope` + `FileStateTracker` -- typed tool-layer contract, raw Map encapsulated (#843)
|
|
134
|
+
- A4: All 11 tool factories extracted to `src/daemon/tools/` -- workflow-runner.ts -1,500 lines (#851)
|
|
135
|
+
- A5: `ContextLoader` + `ContextBundle` -- two-phase context assembly, parallelized with pre-agent session setup (#855)
|
|
136
|
+
- A6: `ActiveSessionSet` + `SessionHandle` -- replaces `SteerRegistry` + `AbortRegistry` dual Maps; closes TDZ hazard (#856)
|
|
137
|
+
- A7: `buildAgentReadySession` + `runAgentLoop` extracted -- runWorkflow() body: 302 → 92 lines (#859)
|
|
138
|
+
- A8: `SessionSource` discriminated union + `AllocatedSession` -- typed vocabulary for `_preAllocatedStartResponse` migration (#861)
|
|
139
|
+
- A9: Full `SessionSource` migration -- `WorkflowTrigger._preAllocatedStartResponse` removed; all 4 call sites construct `SessionSource` directly; `runWorkflow()` accepts `source?: SessionSource` (#869)
|
|
113
140
|
|
|
114
|
-
**
|
|
141
|
+
**Also shipped (Track B, PRs #846-#848):**
|
|
142
|
+
- B1: `DispatchDeduplicator` -- compile-enforced dedup contract, replaces verbal MUST comment
|
|
143
|
+
- B2: `DeliveryPipeline` + `DeliveryStage` -- staged delivery, preempts accretion in trigger-router.ts
|
|
144
|
+
- B3: `createCoordinatorDeps` + `setDispatch` -- extracted from 900-line trigger-listener.ts; circular dep fixed
|
|
115
145
|
|
|
116
|
-
|
|
117
|
-
interface SessionContext {
|
|
118
|
-
readonly systemPrompt: string;
|
|
119
|
-
readonly tools: readonly AgentTool[];
|
|
120
|
-
readonly sessionTimeoutMs: number;
|
|
121
|
-
readonly maxTurns: number;
|
|
122
|
-
readonly initialPrompt: string;
|
|
123
|
-
readonly agentCallbacks: AgentLoopCallbacks;
|
|
124
|
-
}
|
|
146
|
+
**Unit tests added (PRs #863-#865):** `DefaultFileStateTracker` (15), `DefaultContextLoader` (12), `ActiveSessionSet`/`SessionHandle` (11).
|
|
125
147
|
|
|
126
|
-
|
|
127
|
-
trigger: WorkflowTrigger,
|
|
128
|
-
agentClient: AgentClientInterface,
|
|
129
|
-
modelId: string,
|
|
130
|
-
soulContent: string, // already loaded by loadDaemonSoul()
|
|
131
|
-
workspaceContext: string | null, // already loaded by loadWorkspaceContext()
|
|
132
|
-
sessionNotes: readonly string[], // already loaded by loadSessionNotes()
|
|
133
|
-
state: SessionState,
|
|
134
|
-
// ... tool factories, schemas, etc.
|
|
135
|
-
): SessionContext
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
The shell then does:
|
|
139
|
-
1. All I/O in sequence: `loadDaemonSoul`, `loadWorkspaceContext`, `loadSessionNotes`, `git worktree add`, `executeStartWorkflow`, `parseContinueTokenOrFail`, `persistTokens`
|
|
140
|
-
**What Phase 2 delivered (PR #830):**
|
|
141
|
-
- `PreAgentSession` interface + `PreAgentSessionResult` discriminated union -- all early-exit paths type-enforced
|
|
142
|
-
- `buildPreAgentSession()` -- all pre-agent I/O extracted; steer+daemon registries registered after all failing I/O (FM1 invariant)
|
|
143
|
-
- `constructTools()` -- explicitly impure named function, `state` as explicit parameter
|
|
144
|
-
- `persistTokens()` returns `Promise<Result<void, PersistTokensError>>` using `src/runtime/result.ts`
|
|
145
|
-
- `sidecardLifecycleFor()` pure function with `assertNever` exhaustiveness
|
|
146
|
-
- TDZ hazard fixed: `abortRegistry.set()` now registered after `const agent = new AgentLoop()`
|
|
148
|
+
**Total workflow-runner.ts reduction: ~4,955 → ~2,800 lines (44%).**
|
|
147
149
|
|
|
148
|
-
**
|
|
149
|
-
- `buildTurnEndSubscriber()` extracted -- runWorkflow() body: 539 → 426 lines
|
|
150
|
-
- Tool param validation at LLM boundary (8 tool factories)
|
|
151
|
-
- `buildAgentCallbacks()` + `buildSessionResult()` pure functions -- body: 426 → 308 lines
|
|
152
|
-
- Test flakiness fix: `settleFireAndForget()` + `retry: 2` in vitest config
|
|
153
|
-
|
|
154
|
-
**Still deferred:**
|
|
155
|
-
- `CriticalEffect<T>` / `ObservabilityEffect` type distinction
|
|
156
|
-
- `StateRef` mutation wrapper
|
|
157
|
-
- Zod tool param validation (replacing manual typeof checks -- requires zodToJsonSchema or two sources of truth)
|
|
158
|
-
- `wr.refactoring` workflow (see backlog entry above)
|
|
150
|
+
**Follow-on:** `wr.refactoring` workflow (see backlog entry above). Remaining items in "Daemon architecture: remaining migrations" entry below.
|
|
159
151
|
|
|
160
152
|
---
|
|
161
153
|
|
|
@@ -623,6 +615,29 @@ The stdio/HTTP MCP server that Claude Code (and other MCP clients) connect to. M
|
|
|
623
615
|
|
|
624
616
|
## Console
|
|
625
617
|
|
|
618
|
+
### Task picker mode: browse and launch available work (Apr 29, 2026)
|
|
619
|
+
|
|
620
|
+
**Status: idea** | Priority: high
|
|
621
|
+
|
|
622
|
+
**Problem:** Once WorkTrain is configured (workspace set up, triggers.yml written, daemon running), there is still no easy way to say "run this workflow now" from the console. Dispatch requires knowing the API or writing a webhook. The console has a dispatch endpoint but no UI to drive it.
|
|
623
|
+
|
|
624
|
+
**Vision:** A console panel that lists the triggers already configured in triggers.yml and lets the user click one to fire it immediately -- without leaving the browser, without touching the API, without writing YAML.
|
|
625
|
+
|
|
626
|
+
**How it works:**
|
|
627
|
+
1. Console calls `GET /api/v2/triggers` to list all triggers loaded by the daemon.
|
|
628
|
+
2. User sees a list: trigger ID, workflow, goal, last-fired timestamp. Clicks "Run".
|
|
629
|
+
3. Console POSTs to `/api/v2/auto/dispatch` (already implemented) with the trigger's workflowId + goal + workspace.
|
|
630
|
+
4. New session appears in the session list immediately. User watches the DAG advance live.
|
|
631
|
+
5. On completion: outcome, PR link (if opened), and step notes all visible in the same panel.
|
|
632
|
+
|
|
633
|
+
**What this is not:** An onboarding wizard or zero-setup flow -- the daemon and environment must already be configured. This is a dispatch surface for *already-configured* users who want to trigger work without using the CLI or waiting for a webhook.
|
|
634
|
+
|
|
635
|
+
**Why it matters:** Makes the console a control plane, not just a read-only viewer. The daemon gains a "run this now" button. Users get to watch the agent work in real time, which builds confidence before trusting it on unattended tasks.
|
|
636
|
+
|
|
637
|
+
**Dependency:** `GET /api/v2/triggers` endpoint (returns the live trigger index -- may need to be added). `POST /api/v2/auto/dispatch` already exists. No new daemon work required.
|
|
638
|
+
|
|
639
|
+
---
|
|
640
|
+
|
|
626
641
|
### Console interactivity and liveliness
|
|
627
642
|
|
|
628
643
|
**Status: idea** | Priority: medium
|
|
@@ -733,6 +748,14 @@ A workflow that aggregates activity across git history, GitLab/GitHub MRs and re
|
|
|
733
748
|
|
|
734
749
|
## Platform Vision (longer-term)
|
|
735
750
|
|
|
751
|
+
### Inspiration: openclaw (Apr 29, 2026)
|
|
752
|
+
|
|
753
|
+
**Source:** https://github.com/openclaw/openclaw
|
|
754
|
+
|
|
755
|
+
openclaw is worth studying deeply before building out the platform layer. Draw inspiration from it when designing: multi-agent orchestration patterns, coordinator architecture, context packaging for subagents, task queue and dispatch models, and the overall shape of an autonomous engineering platform. Review it before making architectural decisions on any of the Platform Vision items below.
|
|
756
|
+
|
|
757
|
+
---
|
|
758
|
+
|
|
736
759
|
### Knowledge graph for agent context
|
|
737
760
|
|
|
738
761
|
**Status: idea** | Priority: medium
|
|
@@ -1089,8 +1112,7 @@ WorkTrain is a persistent background daemon that initiates workflows autonomousl
|
|
|
1089
1112
|
- Bot identity (`botIdentity`) and acting-as-user support
|
|
1090
1113
|
- Dynamic model selection (`agentConfig.model`)
|
|
1091
1114
|
- macOS notifications
|
|
1092
|
-
-
|
|
1093
|
-
- AbortRegistry + SIGTERM graceful shutdown
|
|
1115
|
+
- `ActiveSessionSet` + mid-session steer injection + SIGTERM graceful shutdown (replaces SteerRegistry + AbortRegistry)
|
|
1094
1116
|
- `maxOutputTokens` per trigger, `maxQueueDepth` with HTTP 429
|
|
1095
1117
|
- Crash recovery Phase B
|
|
1096
1118
|
- `daemon-soul.md` / workspace context injection
|
|
@@ -1103,6 +1125,7 @@ WorkTrain is a persistent background daemon that initiates workflows autonomousl
|
|
|
1103
1125
|
- Worktree orphan cleanup on delivery failure
|
|
1104
1126
|
- runWorkflow() Phase 2 architecture (PR #830): `PreAgentSession`/`buildPreAgentSession`, `constructTools`, `persistTokens` Result type, `sidecardLifecycleFor` pure function, TDZ hazard fix for abort registry
|
|
1105
1127
|
- runWorkflow() Phase 3 architecture (PRs #835, #837): `buildTurnEndSubscriber` (539→426 lines), tool param validation at LLM boundary (8 factories), `buildAgentCallbacks` + `buildSessionResult` pure functions (426→308 lines), test flakiness fix (settleFireAndForget + retry:2)
|
|
1128
|
+
- runWorkflow() Phase 4 / Track A+B architecture (PRs #839-#869, Apr 29, 2026): six-layer daemon decomposition -- `SessionScope`+`FileStateTracker`, tool extraction to `src/daemon/tools/`, `ContextLoader`+`ContextBundle`, `ActiveSessionSet`+`SessionHandle` (TDZ fix), `buildAgentReadySession`+`runAgentLoop`, `SessionSource`+`AllocatedSession`+full `_preAllocatedStartResponse` removal, `DispatchDeduplicator`, `DeliveryPipeline`, `createCoordinatorDeps`. workflow-runner.ts: 4,955 → 2,800 lines (44%). 38 new unit tests for new abstractions. `ActiveSessionSet` replaces `SteerRegistry`+`AbortRegistry`.
|
|
1106
1129
|
|
|
1107
1130
|
### WorkRail engine / MCP features
|
|
1108
1131
|
|
|
@@ -1165,3 +1188,14 @@ The agent is expensive, inconsistent, and slow. Scripts are free, deterministic,
|
|
|
1165
1188
|
|
|
1166
1189
|
---
|
|
1167
1190
|
|
|
1191
|
+
### Worktree and branch lifecycle management
|
|
1192
|
+
|
|
1193
|
+
WorkTrain has no tooling to surface the state of worktrees and branches relative to main. Doing this manually today requires running git commands across every registered worktree, cross-referencing merged PR lists, and inspecting each branch's unique commits to determine if the work landed. Pain points observed in practice:
|
|
1194
|
+
|
|
1195
|
+
- Worktrees persist after their branch's PR is squash-merged -- no signal that they are safe to delete
|
|
1196
|
+
- No inventory of which branches have genuinely unmerged work vs. fully superseded content
|
|
1197
|
+
- Abandoned in-progress branches have no attached context about why they were abandoned or what state they were in
|
|
1198
|
+
- Daemon-spawned worktrees under `~/.workrail/worktrees/` are opaque -- no indication of which session created them or whether cleanup is safe
|
|
1199
|
+
|
|
1200
|
+
---
|
|
1201
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exaudeus/workrail",
|
|
3
|
-
"version": "3.72.
|
|
3
|
+
"version": "3.72.2",
|
|
4
4
|
"description": "Step-by-step workflow enforcement for AI agents via MCP",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -145,6 +145,7 @@
|
|
|
145
145
|
"vitest": "^3.2.4"
|
|
146
146
|
},
|
|
147
147
|
"engines": {
|
|
148
|
-
"node": ">=20"
|
|
148
|
+
"node": ">=20",
|
|
149
|
+
"npm": ">=11.11.1"
|
|
149
150
|
}
|
|
150
151
|
}
|