@exaudeus/workrail 3.59.5 → 3.59.7

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.
@@ -142,7 +142,7 @@ async function readAllDaemonSessions(sessionsDir = exports.DAEMON_SESSIONS_DIR)
142
142
  }
143
143
  const sessions = [];
144
144
  for (const entry of entries) {
145
- if (!entry.endsWith('.json'))
145
+ if (!entry.endsWith('.json') || entry.startsWith('queue-issue-'))
146
146
  continue;
147
147
  const sessionId = entry.slice(0, -5);
148
148
  const filePath = path.join(sessionsDir, entry);
@@ -238,8 +238,8 @@
238
238
  "bytes": 31
239
239
  },
240
240
  "cli-worktrain.js": {
241
- "sha256": "34c2a2d4596a1f1b4c385366c601391cc329fd85abccdebce5d74a9bd1631aac",
242
- "bytes": 60787
241
+ "sha256": "cc970333db8fec7bcccec57e901bcde86f1671d1efa039d7abc9119593870bef",
242
+ "bytes": 59518
243
243
  },
244
244
  "cli.d.ts": {
245
245
  "sha256": "43e818adf60173644896298637f47b01d5819b17eda46eaa32d0c7d64724d012",
@@ -481,16 +481,16 @@
481
481
  "sha256": "5fe866e54f796975dec5d8ba9983aefd86074db212d3fccd64eed04bc9f0b3da",
482
482
  "bytes": 8011
483
483
  },
484
- "console-ui/assets/index-Ctoxo1z6.js": {
485
- "sha256": "fc11e753539cbb514c0c3c37189f2fbe2abca848fa005c5bcae09e5c88b78bd9",
486
- "bytes": 760528
487
- },
488
484
  "console-ui/assets/index-DGj8EsFR.css": {
489
485
  "sha256": "3bdb55ec0957928e0ebbb86a7d6b36d28f7ba7d5c0f3e236fd8f2e2aacee2fa4",
490
486
  "bytes": 60631
491
487
  },
488
+ "console-ui/assets/index-RXJXvJ8T.js": {
489
+ "sha256": "2d32ed2f503a4eaf9d9c4868d8bd7906df0ab8635c6d24c1f8eb333a50ac442d",
490
+ "bytes": 760528
491
+ },
492
492
  "console-ui/index.html": {
493
- "sha256": "df586c53f8a105ad224d418e70c2dd98e7be4725dfb1c194baf45eff810ff6b5",
493
+ "sha256": "546a59bc2b573a0d127fd10eb6ef2b39ed98d4bb38ea432610d831d5dd288bfb",
494
494
  "bytes": 417
495
495
  },
496
496
  "console/standalone-console.d.ts": {
@@ -546,8 +546,8 @@
546
546
  "bytes": 462
547
547
  },
548
548
  "coordinators/modes/full-pipeline.js": {
549
- "sha256": "a03cf485201d23b0ddf75ca36ea10741bb9d0373479e7df3350401653229ef8b",
550
- "bytes": 12850
549
+ "sha256": "945d726d728235f8f31f03f33fbde8f6614472b38921a44fba42da959875f37d",
550
+ "bytes": 13201
551
551
  },
552
552
  "coordinators/modes/implement-shared.d.ts": {
553
553
  "sha256": "fbad9d91d84d2112b273175618686489a7f106385e0e62d6cab80804d6d0f2d7",
@@ -582,8 +582,8 @@
582
582
  "bytes": 1198
583
583
  },
584
584
  "coordinators/pr-review.d.ts": {
585
- "sha256": "a8886a3c83a31e869522812d1342a301e9bfae92d8e5e694594c3c50912035d9",
586
- "bytes": 3833
585
+ "sha256": "d46e4923995a0b43aefee25da298b86235fae0ad105e548b3174c0eea9c1f8d0",
586
+ "bytes": 3947
587
587
  },
588
588
  "coordinators/pr-review.js": {
589
589
  "sha256": "84b51f931eb55d908de8c60f90b4d4b66540054791a28ce2f07426a841fed386",
@@ -650,8 +650,8 @@
650
650
  "bytes": 7307
651
651
  },
652
652
  "daemon/workflow-runner.js": {
653
- "sha256": "2184d5202eadecff166aeb5371d84c1660a9b6cf3fdf7598d311172fdfdb910a",
654
- "bytes": 95312
653
+ "sha256": "0d4991c3589e75679d4035d506d84ebe595df8328c6e632d352597c9e23ad741",
654
+ "bytes": 95348
655
655
  },
656
656
  "di/container.d.ts": {
657
657
  "sha256": "003bb7fb7478d627524b9b1e76bd0a963a243794a687ff233b96dc0e33a06d9f",
@@ -1650,8 +1650,8 @@
1650
1650
  "bytes": 1363
1651
1651
  },
1652
1652
  "trigger/adapters/github-queue-poller.js": {
1653
- "sha256": "c1a4866ff7ead5b33439da2ff7842747e119bf411e8057b1a08d580816689dd1",
1654
- "bytes": 7935
1653
+ "sha256": "b15f56cf0782a1eceb66ef6d58bb75b17e14f701f1e95d072fe7a71b5aa6a4f5",
1654
+ "bytes": 8824
1655
1655
  },
1656
1656
  "trigger/adapters/gitlab-poller.d.ts": {
1657
1657
  "sha256": "f685490fafad77194fdd0f0bbaf80dbc56730aeb344853da365199a120fbe399",
@@ -1661,14 +1661,6 @@
1661
1661
  "sha256": "6728a2169f4007b9ea0414fade6b21500500d9c79d0b09296d92ef8bcabb9c79",
1662
1662
  "bytes": 2763
1663
1663
  },
1664
- "trigger/daemon-console.d.ts": {
1665
- "sha256": "a3b9a9f58482c6ea379c0e02c30f55a5820c7c37fa3fae55fc336cd518f35462",
1666
- "bytes": 1162
1667
- },
1668
- "trigger/daemon-console.js": {
1669
- "sha256": "f2f09c05e48b42ebf1c7be137fc6eced46673048471b7114434710b5691fe6f2",
1670
- "bytes": 5497
1671
- },
1672
1664
  "trigger/delivery-action.d.ts": {
1673
1665
  "sha256": "559e2b2645aa60528f73de351cd35ebf45c5b82f47797aa15ddd681319315d39",
1674
1666
  "bytes": 1759
@@ -1718,20 +1710,20 @@
1718
1710
  "bytes": 6968
1719
1711
  },
1720
1712
  "trigger/polling-scheduler.d.ts": {
1721
- "sha256": "60df456a31fa87ce71de76f5e31a6c460bfab588a24c8a2f06bf926fdcea550a",
1722
- "bytes": 1096
1713
+ "sha256": "3c0865f9d21819c364575062745741405bc80006f4a0754d26ed4302253371c6",
1714
+ "bytes": 1126
1723
1715
  },
1724
1716
  "trigger/polling-scheduler.js": {
1725
- "sha256": "ef1252ee4bc4592fc416e7a00aa4e7db297035a990231941ae9316cdf5fe5b9a",
1726
- "bytes": 21667
1717
+ "sha256": "61b94e35aae2e9578a9e9cc32548791166b9ec98abb8f2cff58135fc6b3e5593",
1718
+ "bytes": 23945
1727
1719
  },
1728
1720
  "trigger/trigger-listener.d.ts": {
1729
1721
  "sha256": "1eebb3d4829030b264c3798b0b0d55d7357d313ab83e3f344ad455eaafcedb44",
1730
1722
  "bytes": 1740
1731
1723
  },
1732
1724
  "trigger/trigger-listener.js": {
1733
- "sha256": "09b8bbcda1825a9314dc29ac7435ef703fb0cdad13fa54ffe45f68767f22fbc7",
1734
- "bytes": 25095
1725
+ "sha256": "4aa62601aac5d3c7d1750ef839ee71a911dacbab346fb6dfdb3d7151e9e7d359",
1726
+ "bytes": 25179
1735
1727
  },
1736
1728
  "trigger/trigger-router.d.ts": {
1737
1729
  "sha256": "b916f33cab64d491ab04bd13dd37599d33e687f7aea1e69e50f5fcea4b3b4624",
@@ -3074,8 +3066,8 @@
3074
3066
  "bytes": 880
3075
3067
  },
3076
3068
  "v2/usecases/console-routes.js": {
3077
- "sha256": "76e34345d329bfc8a998ebc50ef8bd6ead1b7914873f6350ba479909c9c097be",
3078
- "bytes": 31648
3069
+ "sha256": "6e22d5ef4fce3d9bf2b2709d3fcfc35089b8c558d82372071ac7d22251c68ddb",
3070
+ "bytes": 31748
3079
3071
  },
3080
3072
  "v2/usecases/console-service.d.ts": {
3081
3073
  "sha256": "fc8fe65427fa9f4f3535344b385b36f66ca06b7e3bfaea708931817a3edcad2b",
@@ -148,6 +148,30 @@ function inferMaturity(body) {
148
148
  return 'idea';
149
149
  }
150
150
  async function checkIdempotency(issueNumber, sessionsDir = exports.DEFAULT_SESSIONS_DIR) {
151
+ const sidecarFilename = `queue-issue-${issueNumber}.json`;
152
+ const sidecarFilePath = path.join(sessionsDir, sidecarFilename);
153
+ try {
154
+ const sidecarContent = await fs.readFile(sidecarFilePath, 'utf8');
155
+ const sidecarParsed = JSON.parse(sidecarContent);
156
+ if (typeof sidecarParsed !== 'object' || sidecarParsed === null) {
157
+ return 'active';
158
+ }
159
+ const sidecar = sidecarParsed;
160
+ const dispatchedAt = sidecar['dispatchedAt'];
161
+ const ttlMs = sidecar['ttlMs'];
162
+ if (typeof dispatchedAt === 'number' && typeof ttlMs === 'number') {
163
+ if (dispatchedAt + ttlMs > Date.now()) {
164
+ return 'active';
165
+ }
166
+ return 'clear';
167
+ }
168
+ return 'active';
169
+ }
170
+ catch (e) {
171
+ if (e.code !== 'ENOENT') {
172
+ return 'active';
173
+ }
174
+ }
151
175
  let files;
152
176
  try {
153
177
  files = await fs.readdir(sessionsDir);
@@ -155,7 +179,7 @@ async function checkIdempotency(issueNumber, sessionsDir = exports.DEFAULT_SESSI
155
179
  catch {
156
180
  return 'clear';
157
181
  }
158
- const jsonFiles = files.filter(f => f.endsWith('.json'));
182
+ const jsonFiles = files.filter(f => f.endsWith('.json') && f !== sidecarFilename);
159
183
  for (const filename of jsonFiles) {
160
184
  try {
161
185
  const content = await fs.readFile(path.join(sessionsDir, filename), 'utf8');
@@ -29,4 +29,5 @@ export declare class PollingScheduler {
29
29
  private doPollGitHub;
30
30
  private dispatchAndRecord;
31
31
  private doPollGitHubQueue;
32
+ private applyGitHubLabel;
32
33
  }
@@ -38,6 +38,7 @@ const gitlab_poller_js_1 = require("./adapters/gitlab-poller.js");
38
38
  const github_poller_js_1 = require("./adapters/github-poller.js");
39
39
  const github_queue_poller_js_1 = require("./adapters/github-queue-poller.js");
40
40
  const github_queue_config_js_1 = require("./github-queue-config.js");
41
+ const adaptive_pipeline_js_1 = require("../coordinators/adaptive-pipeline.js");
41
42
  const fs = __importStar(require("node:fs/promises"));
42
43
  const os = __importStar(require("node:os"));
43
44
  const path = __importStar(require("node:path"));
@@ -312,15 +313,31 @@ class PollingScheduler {
312
313
  }
313
314
  this.dispatchingIssues.add(top.issue.number);
314
315
  console.log(`[QueuePoll] in-flight-add #${top.issue.number}`);
316
+ const sidecarPath = path.join(sessionsDir, `queue-issue-${top.issue.number}.json`);
317
+ const sidecarContent = JSON.stringify({
318
+ issueNumber: top.issue.number,
319
+ triggerId,
320
+ dispatchedAt: Date.now(),
321
+ ttlMs: adaptive_pipeline_js_1.DISCOVERY_TIMEOUT_MS + 60000,
322
+ }, null, 2);
323
+ void fs.writeFile(sidecarPath, sidecarContent, 'utf8').catch((e) => {
324
+ console.warn(`[QueuePoll] Failed to write sidecar for issue #${top.issue.number}: ${e instanceof Error ? e.message : String(e)}`);
325
+ });
315
326
  const dispatchP = this.router.dispatchAdaptivePipeline(workflowTrigger.goal, workflowTrigger.workspacePath, workflowTrigger.context);
327
+ const issueNumber = top.issue.number;
316
328
  void dispatchP
317
- .then(() => {
318
- this.dispatchingIssues.delete(top.issue.number);
319
- console.log(`[QueuePoll] in-flight-clear #${top.issue.number} reason=completed`);
329
+ .then((outcome) => {
330
+ this.dispatchingIssues.delete(issueNumber);
331
+ console.log(`[QueuePoll] in-flight-clear #${issueNumber} reason=completed`);
332
+ if (outcome.kind === 'escalated' || outcome.kind === 'dry_run') {
333
+ void this.applyGitHubLabel(issueNumber, 'worktrain:in-progress', queueConfig.token, source.repo);
334
+ }
335
+ void fs.unlink(sidecarPath).catch(() => { });
320
336
  })
321
337
  .catch(() => {
322
- this.dispatchingIssues.delete(top.issue.number);
323
- console.log(`[QueuePoll] in-flight-clear #${top.issue.number} reason=error`);
338
+ this.dispatchingIssues.delete(issueNumber);
339
+ console.log(`[QueuePoll] in-flight-clear #${issueNumber} reason=error`);
340
+ void fs.unlink(sidecarPath).catch(() => { });
324
341
  });
325
342
  console.log(`[QueuePoll] dispatched via adaptivePipeline goal="${workflowTrigger.goal.slice(0, 80)}"`);
326
343
  for (let i = 1; i < candidates.length; i++) {
@@ -332,6 +349,32 @@ class PollingScheduler {
332
349
  console.log(`[QueuePoll] cycle complete selected=1 skipped=${skipped.length + candidates.length - 1} elapsed=${elapsed}ms`);
333
350
  await appendQueuePollLog({ event: 'poll_cycle_complete', selected: 1, skipped: skipped.length + candidates.length - 1, elapsed, ts: new Date().toISOString() });
334
351
  }
352
+ async applyGitHubLabel(issueNumber, label, token, repo) {
353
+ const fetchFn = this.fetchFn ?? globalThis.fetch;
354
+ const url = `https://api.github.com/repos/${repo}/issues/${issueNumber}/labels`;
355
+ try {
356
+ const response = await fetchFn(url, {
357
+ method: 'POST',
358
+ headers: {
359
+ 'Authorization': `Bearer ${token}`,
360
+ 'Accept': 'application/vnd.github+json',
361
+ 'Content-Type': 'application/json',
362
+ 'X-GitHub-Api-Version': '2022-11-28',
363
+ },
364
+ body: JSON.stringify({ labels: [label] }),
365
+ });
366
+ if (!response.ok) {
367
+ const text = await response.text().catch(() => '');
368
+ console.warn(`[QueuePoll] Failed to apply label '${label}' to issue #${issueNumber}: HTTP ${response.status} ${text.slice(0, 200)}`);
369
+ }
370
+ else {
371
+ console.log(`[QueuePoll] Applied label '${label}' to issue #${issueNumber}`);
372
+ }
373
+ }
374
+ catch (e) {
375
+ console.warn(`[QueuePoll] Failed to apply label '${label}' to issue #${issueNumber}: ${e instanceof Error ? e.message : String(e)}`);
376
+ }
377
+ }
335
378
  }
336
379
  exports.PollingScheduler = PollingScheduler;
337
380
  function buildGitLabWorkflowTrigger(trigger, mr) {
@@ -432,7 +475,7 @@ function extractDotPath(obj, rawPath) {
432
475
  async function countActiveSessions(sessionsDir) {
433
476
  try {
434
477
  const files = await fs.readdir(sessionsDir);
435
- return files.filter((f) => f.endsWith('.json')).length;
478
+ return files.filter((f) => f.endsWith('.json') && !f.startsWith('queue-issue-')).length;
436
479
  }
437
480
  catch {
438
481
  return 0;
@@ -218,7 +218,7 @@ async function startTriggerListener(ctx, options) {
218
218
  }
219
219
  let routerRef;
220
220
  const coordinatorDeps = {
221
- spawnSession: async (workflowId, goal, workspace, context) => {
221
+ spawnSession: async (workflowId, goal, workspace, context, agentConfig) => {
222
222
  if (routerRef === undefined) {
223
223
  return { kind: 'err', error: 'in-process router not initialized -- coordinator deps not ready' };
224
224
  }
@@ -242,6 +242,7 @@ async function startTriggerListener(ctx, options) {
242
242
  goal,
243
243
  workspacePath: workspace,
244
244
  context,
245
+ ...(agentConfig !== undefined ? { agentConfig } : {}),
245
246
  _preAllocatedStartResponse: startResult.value.response,
246
247
  });
247
248
  return { kind: 'ok', value: sessionHandle };
@@ -429,13 +429,11 @@ function mountConsoleRoutes(app, consoleService, workflowService, timingRingBuff
429
429
  repoRootsExpiresAt = Date.now() + REPO_ROOTS_TTL_MS;
430
430
  }
431
431
  const repoRoots = cachedRepoRoots;
432
- const data = await Promise.race([
433
- (0, worktree_service_js_1.getWorktreeList)(repoRoots, activeSessions).finally(() => {
434
- if (timeoutId !== null)
435
- clearTimeout(timeoutId);
436
- }),
437
- timeoutPromise,
438
- ]);
432
+ const worktreeWork = (0, worktree_service_js_1.getWorktreeList)(repoRoots, activeSessions)
433
+ .finally(() => { if (timeoutId !== null)
434
+ clearTimeout(timeoutId); })
435
+ .catch(() => ({ repos: [] }));
436
+ const data = await Promise.race([worktreeWork, timeoutPromise]);
439
437
  if (timeoutId !== null)
440
438
  clearTimeout(timeoutId);
441
439
  res.json({ success: true, data });
@@ -532,7 +530,7 @@ function mountConsoleRoutes(app, consoleService, workflowService, timingRingBuff
532
530
  }
533
531
  app.post('/api/v2/auto/dispatch', express_1.default.json(), async (req, res) => {
534
532
  if (!v2ToolContext) {
535
- res.status(503).json({ success: false, error: 'Autonomous dispatch requires v2 tools enabled.' });
533
+ res.status(503).json({ success: false, error: 'Autonomous dispatch requires the WorkTrain daemon. Run worktrain console alongside worktrain daemon to enable browser dispatch.' });
536
534
  return;
537
535
  }
538
536
  const body = req.body;
@@ -7351,3 +7351,229 @@ An agent can die from: stream watchdog timeout (600s no progress), OOM kill, or
7351
7351
  ### Priority
7352
7352
 
7353
7353
  High. Agent crash recovery makes the overnight-autonomous bar achievable. Without it, any hung LLM call or tool timeout fails the entire pipeline silently. With it, transient failures are automatically retried and the pipeline continues.
7354
+
7355
+ ---
7356
+
7357
+ ## Workflow execution time tracking and prediction (Apr 21, 2026)
7358
+
7359
+ **The problem:** WorkTrain has no data on how long workflows actually take. Timeouts are set by intuition (55 min for discovery, 35 for shaping, 65 for coding). We just discovered that discovery on a real workrail task takes ~16 minutes. The 55-minute timeout is 3x the actual time -- but we didn't know that until we ran a benchmark manually.
7360
+
7361
+ ### What to track
7362
+
7363
+ For every completed session, record:
7364
+ - Workflow ID
7365
+ - Total wall-clock duration
7366
+ - Number of turns
7367
+ - Number of step advances
7368
+ - Outcome (success / timeout / stuck / error)
7369
+ - Task complexity signals (codebase size, number of files read, task type from context)
7370
+
7371
+ Store in `~/.workrail/data/execution-stats.jsonl` -- one line per completed session, append-only.
7372
+
7373
+ ### What to do with it
7374
+
7375
+ **Immediate use: calibrate timeouts automatically**
7376
+
7377
+ Instead of hardcoded `DISCOVERY_TIMEOUT_MS = 55 * 60 * 1000`, read the p95 completion time from execution stats and set the timeout to `p95 * 1.5`. Start with the hardcoded values as seeds; refine after 10+ samples.
7378
+
7379
+ **Medium-term use: predict duration before dispatch**
7380
+
7381
+ Given: task description + workflow ID + codebase characteristics → predicted duration range.
7382
+
7383
+ The coordinator could use this to:
7384
+ - Warn when a task is likely to exceed session limits before starting
7385
+ - Adjust timeout budgets per-dispatch based on predicted complexity
7386
+ - Surface "this type of task usually takes 45 minutes" in `worktrain trigger test` output
7387
+
7388
+ **Longer-term use: quality/efficiency metrics**
7389
+
7390
+ Track step-advance rate (steps per turn) as a proxy for workflow efficiency. A session with 50 turns and 2 step advances is spending too many turns between steps. This feeds into the workflow improvement loop.
7391
+
7392
+ ### Implementation notes
7393
+
7394
+ - Append to `execution-stats.jsonl` in `runWorkflow()`'s finally block, same pattern as the daemon event log
7395
+ - Keep it simple: flat JSONL, no database, no schema migration
7396
+ - `worktrain status` can show recent timing stats: "Last 10 wr.discovery sessions: avg 18min, p95 31min"
7397
+ - `worktrain trigger validate` can warn if configured timeouts are well below historical p95
7398
+
7399
+ ### Priority
7400
+
7401
+ Medium. The data collection is small (~5 lines in `runWorkflow()`). The prediction and calibration are more involved. Ship collection first, calibration second.
7402
+
7403
+ ---
7404
+
7405
+ ## WorkRail MCP server self-cleanup (Apr 21, 2026)
7406
+
7407
+ **The problem:** The WorkRail MCP server accumulates stale state that never cleans itself up: old workflow copies in `~/.workrail/workflows/`, dead managed sources, git repo caches that can't pull, 500+ sessions in the store, stale remembered roots. None of it has a TTL or cleanup mechanism. Every server startup loads everything and logs validation errors for stale state.
7408
+
7409
+ ### Sources of stale state
7410
+
7411
+ 1. **`~/.workrail/workflows/`** -- manually copied or `worktrain init`-placed workflows that go stale when the repo updates. MCP server loads both repo copy and user copy; older one fails validation silently or noisily.
7412
+
7413
+ 2. **Managed sources** (`~/.workrail/data/managed-sources/`) -- paths that no longer exist stay registered. Server tries to load them on every startup.
7414
+
7415
+ 3. **Git workflow cache** (`~/.workrail/cache/git-*`) -- cloned repos whose remotes have changed, been deleted, or whose auth has expired. `git pull` fails; errors logged on every startup.
7416
+
7417
+ 4. **Session store** (`~/.workrail/data/sessions/`) -- sessions accumulate forever. No TTL, no archival. Console loads all 500+ on every `/api/v2/sessions` request (partially mitigated by mtime cache).
7418
+
7419
+ 5. **Remembered roots** (`~/.workrail/data/managed-sources/remembered-roots.json`) -- workspace paths from past sessions that no longer exist.
7420
+
7421
+ ### Fix: two layers
7422
+
7423
+ **Layer 1: Defensive loading (mostly already done)**
7424
+ Every loader should already handle missing/broken sources gracefully. Audit: are all managed source failures caught and logged as warnings rather than errors? Are git cache failures non-fatal?
7425
+
7426
+ **Layer 2: `workrail cleanup` command**
7427
+ ```
7428
+ workrail cleanup [--yes] [--sessions --older-than <age>] [--sources] [--cache] [--roots]
7429
+ ```
7430
+ - `--sources`: remove managed sources where path doesn't exist on disk
7431
+ - `--cache`: remove git caches where `git pull` fails (remote gone or auth expired)
7432
+ - `--sessions --older-than 30d`: archive or delete sessions older than N days
7433
+ - `--roots`: remove remembered roots where path doesn't exist
7434
+ - Without `--yes`: show what would be removed and ask for confirmation
7435
+ - With `--yes`: remove without prompting (for CI / worktrain init)
7436
+
7437
+ **Layer 3: Automatic startup cleanup (light)**
7438
+ On MCP server startup, silently remove managed sources where the filesystem path doesn't exist (non-destructive -- the path is already gone). Log a single "removed N stale sources" line. Do not auto-remove sessions or caches -- those require explicit user intent.
7439
+
7440
+ **Layer 4: User workflow directory sync**
7441
+ `~/.workrail/workflows/` should not be a place users copy workflows to manually. It should either:
7442
+ - Be deprecated entirely (use managed sources / workspace roots instead)
7443
+ - Have a `workrail sync` command that updates it from the canonical sources
7444
+ - Auto-detect when a user workflow is an older version of a bundled workflow and skip loading it
7445
+
7446
+ ### Priority
7447
+
7448
+ Medium for the cleanup command (quality of life, stops log noise). High for startup auto-cleanup of dead managed sources (prevents the `Invalid workflow` errors that have been confusing throughout this session). Low for session TTL/archival (the mtime cache handles the performance concern).
7449
+
7450
+ ---
7451
+
7452
+ ## Worktree orphan leak on delivery failure (Apr 21, 2026, from Audit 4)
7453
+
7454
+ **The bug:** In `src/trigger/trigger-router.ts`, `maybeRunDelivery()` on the success path deletes the session sidecar file before attempting worktree removal. If worktree removal fails (network error, git command failure), the sidecar is already gone. `runStartupRecovery()` scans sidecar files to find orphan worktrees -- so the orphaned worktree is invisible and accumulates indefinitely.
7455
+
7456
+ **Fix:** In the success path cleanup, delete the sidecar AFTER worktree removal, not before. Or better: always attempt worktree removal in a `try/finally` that deletes the sidecar regardless of whether removal succeeded.
7457
+
7458
+ **File:** `src/trigger/trigger-router.ts`, `maybeRunDelivery()` success path.
7459
+
7460
+ **Priority:** Medium. Worktrees are small, but the leak is permanent across daemon restarts.
7461
+
7462
+ ---
7463
+
7464
+ ## queue-poll.jsonl never rotated (Apr 21, 2026, from Audit 5)
7465
+
7466
+ **The bug:** `~/.workrail/queue-poll.jsonl` grows indefinitely. `appendFile`-only, no rotation. At 5-minute poll intervals with 2-3 events per cycle, this is ~8-87 MB/month depending on activity. Disk exhaustion risk on long-running daemons.
7467
+
7468
+ **Fix:** Add a size check before appending in `appendQueuePollLog()`. If file exceeds 10 MB, rotate it: rename to `queue-poll.jsonl.1`, start fresh. Keep at most 2 rotated files.
7469
+
7470
+ **File:** `src/trigger/polling-scheduler.ts`, `appendQueuePollLog()` function.
7471
+
7472
+ **Priority:** Medium. Not urgent but a production correctness issue.
7473
+
7474
+ ---
7475
+
7476
+ ## ReviewSeverity missing assertNever + stderr bypassing injected dep (Apr 21, 2026, from Audit 2)
7477
+
7478
+ **Bug 1 (Major):** In `src/coordinators/modes/implement-shared.ts`, the `switch(findings.severity)` over `ReviewSeverity` has no `default: assertNever(findings.severity)`. Widening `ReviewSeverity` with a new variant compiles cleanly and falls through silently.
7479
+
7480
+ **Fix:** Add `default: assertNever(findings.severity)` to the switch.
7481
+
7482
+ **Bug 2 (Major):** In `src/coordinators/pr-review.ts`, `readVerdictArtifact()` calls `process.stderr.write(...)` directly instead of using the injected `deps.stderr`. Tests that inject a fake dep will miss this log.
7483
+
7484
+ **Fix:** Replace `process.stderr.write(...)` with `deps.stderr(...)`.
7485
+
7486
+ **Files:** `src/coordinators/modes/implement-shared.ts`, `src/coordinators/pr-review.ts`.
7487
+
7488
+ **Priority:** Medium. Correctness issues that won't crash in production but make future refactors unsafe.
7489
+
7490
+ ---
7491
+
7492
+ ## Current state update (Apr 21, 2026)
7493
+
7494
+ **npm version: v3.59.6** | Daemon PID: 54113 | Status: Running, pipeline active
7495
+
7496
+ ### What shipped in this session (Apr 19-21, 2026)
7497
+
7498
+ **All five autonomous pipeline items (previously recorded) plus:**
7499
+
7500
+ - ✅ **Discovery loop fix** (#748) -- three coupled fixes: thread `maxSessionMinutes` through `spawnSession` (sessions now get 55/35/65 min instead of 30 min default), inspect `PipelineOutcome` in polling-scheduler and apply `worktrain:in-progress` label on escalation, write issue-ownership sidecar for cross-restart idempotency
7501
+ - ✅ **In-process `awaitSessions` and `getAgentResult`** (#741) -- replaced HTTP calls to the daemon's own console with direct `ConsoleService` access
7502
+ - ✅ **Try/catch on all coordinator I/O** (#740) -- `getAgentResult`, `pollForPR`, `postToOutbox` all wrapped; coordinator no longer crashes on I/O failure
7503
+ - ✅ **Dispatch dedup prealloc bypass** (#744) -- `dispatch()` now bypasses dedup for pre-allocated sessions, fixing the zombie session bug that prevented discovery from starting
7504
+ - ✅ **Promise.race crash fix** (#733) -- worktrees scan timeout no longer crashes the daemon via unhandled rejection
7505
+ - ✅ **Trigger validator** (#690) -- `worktrain trigger validate` command, `validateTriggerStrict()` pure function
7506
+ - ✅ **`worktrain trigger poll`** (#697) -- force immediate poll cycle on any queue trigger
7507
+ - ✅ **`worktrain trigger test`** (#656) -- dry-run showing what would dispatch
7508
+ - ✅ **Auto-load ~/.workrail/.env** (#673) -- daemon reads secrets from .env automatically
7509
+ - ✅ **Daemon lifecycle events** (#674) -- `session_aborted` on SIGTERM, `daemon_heartbeat` every 30s
7510
+ - ✅ **Attribution signals** (#658) -- `[WT]` PR title prefix, `Co-authored-by: WorkTrain` commit trailers, `worktrain:generated` label
7511
+ - ✅ **Secret scan before push** (#660) -- pattern-based scan blocks commits with leaked credentials
7512
+ - ✅ **Unified logs stream** (#680) -- `worktrain logs` now merges daemon events, queue-poll.jsonl, and filtered stderr
7513
+ - ✅ **Stale lock file handling** (#705) -- validates lock file PID before trusting port discovery
7514
+ - ✅ **5 architectural audits** (docs/design/) -- coordinator access, error handling, testability, type bloat, memory management
7515
+ - ✅ **Stale user workflow cleanup** -- removed old copies from `~/.workrail/workflows/` that were causing ValidationError noise
7516
+
7517
+ ### Current pipeline state (live)
7518
+
7519
+ Discovery session `ecf359d7` running: 77 turns, 11 step advances (active, making real progress on issue #393). Session `b7df0c8b` also running (just started). First clean run after all pipeline fixes landed.
7520
+
7521
+ ### Accurate limitations (v3.59.6)
7522
+
7523
+ 1. **Ghost sessions in event log** -- sessions killed by daemon crashes don't get `session_aborted` events from old daemon instances. New daemons emit it on shutdown, but historical sessions show as RUNNING.
7524
+ 2. **Worktree orphan leak** -- if `maybeRunDelivery()` worktree removal fails after sidecar deletion, orphan is invisible to `runStartupRecovery`. See backlog.
7525
+ 3. **`queue-poll.jsonl` never rotated** -- disk exhaustion risk on long-running daemons. See backlog.
7526
+ 4. **`ReviewSeverity` missing `assertNever`** -- future variants silently fall through. See backlog.
7527
+ 5. **`process.stderr.write` in `readVerdictArtifact`** -- bypasses injected dep, invisible to test fakes. See backlog.
7528
+ 6. **WorkRail MCP stale state** -- `workrail cleanup` command doesn't exist yet. Manual cleanup needed for dead managed sources, old session accumulation.
7529
+ 7. **Trigger validation static/runtime gap** -- some runtime checks not in static validator. See trigger-validation-gap-audit.md.
7530
+ 8. **WorkflowTrigger type bloat** -- mixes trigger config, session runtime state, delivery config. See workflow-trigger-lifecycle-audit.md.
7531
+ 9. **Conversation history not persisted** -- LLM conversation history is in-memory only. On crash, context is lost. See backlog.
7532
+
7533
+ ### Next priorities (groomed Apr 21)
7534
+
7535
+ 1. **Watch the current pipeline run** -- discovery `ecf359d7` is active at 77 turns/11 steps. If it completes, shaping and coding should fire automatically. First end-to-end validation.
7536
+ 2. **Execution time tracking** -- add session timing to `execution-stats.jsonl` for timeout calibration. Small change in `runWorkflow()` finally block.
7537
+ 3. **Three audit findings from above** -- worktree orphan leak, queue-poll rotation, assertNever fixes. All small, targeted.
7538
+ 4. **`workrail cleanup` command** -- removes dead managed sources, rotates old session files, clears stale git caches. Stops ValidationError noise in MCP server logs.
7539
+ 5. **Conversation history persistence** -- `conversation.jsonl` per session, append-only. Prerequisite for true crash recovery.
7540
+ 6. **Autonomous crash recovery and interrupted-session resume** -- see full entry below (Apr 21).
7541
+
7542
+ ---
7543
+
7544
+ ## Autonomous crash recovery and interrupted-session resume (Apr 21, 2026)
7545
+
7546
+ **The problem we hit today:** A daemon crash loop (console `worktrees scan` unhandled rejection) killed all in-flight sessions. The queue correctly detected the sidecar and skipped re-dispatch for 56 min (TTL), but when the sidecar expired the session was re-dispatched from scratch with zero context from the previous attempt. The agent had already spent ~10 min in Phase 0, read codebase files, and formed a plan -- all of that work was lost.
7547
+
7548
+ **What we want:** WorkTrain should be able to detect orphaned sessions on startup and make an autonomous decision: resume if the session had meaningful progress, discard and re-dispatch from scratch if it was too early to be worth resuming.
7549
+
7550
+ **Resumability decision criteria (heuristics):**
7551
+ - Session had >= 1 `continue_workflow` call (at least one step advance): worth resuming -- the agent made real progress.
7552
+ - Session is at step 0 with 0 advances but > 5 LLM turns: borderline -- context was accumulated but no checkpoint written. Resume is risky (stale context), discard is safer. Could surface to console for human decision.
7553
+ - Session is at step 0, < 5 turns, < 2 min: discard -- nothing was lost.
7554
+ - Session's worktree is missing or corrupted: discard -- can't resume cleanly.
7555
+ - Session is on a coding workflow and has uncommitted changes in the worktree: pause for human review before discarding (could have partial work).
7556
+
7557
+ **Implementation sketch:**
7558
+
7559
+ 1. **On daemon startup**, `runStartupRecovery()` already scans `daemon-sessions/` for orphaned token files. Extend it to also inspect the session event log for each orphan:
7560
+ - Count `continue_workflow` calls and LLM turns from `~/.workrail/events/<sessionId>.jsonl`
7561
+ - Apply decision criteria above
7562
+ - For resume candidates: call `continue_workflow` with the checkpoint token and a synthesized re-entry prompt: "You are resuming a session that was interrupted by a daemon crash. Your last known step was [stepLabel]. Continue from where you left off."
7563
+ - For discard candidates: emit `session_aborted` event, delete the sidecar, re-add the issue to the queue (or just let the TTL expire and the queue re-select naturally)
7564
+
7565
+ 2. **Conversation history prerequisite**: Resume is only useful if the agent can reconstruct its context. Today, conversation history is in-memory only -- it is lost on crash. The `conversation.jsonl` per-session persistence (backlog item #5 above) is a prerequisite for high-quality resume. Without it, resume starts from the workflow system prompt plus the current step recap only. This is enough for mid-pipeline phases (shaping, coding) since they read artifacts from disk. It may be insufficient for early discovery phases.
7566
+
7567
+ 3. **`worktrain session resume <sessionId>` CLI** -- manual override for human-initiated resume. Useful when the daemon's automatic heuristic chose to discard but the user sees partial work worth keeping.
7568
+
7569
+ 4. **Queue sidecar TTL for resume vs. discard**: Today the sidecar TTL prevents re-dispatch during the entire pipeline window (56 min). With autonomous resume, the TTL for a discarded session should be much shorter (5 min) so the queue can quickly re-select. For a resumed session, keep the full TTL and extend it by the time already spent.
7570
+
7571
+ **Files to change:**
7572
+ - `src/daemon/workflow-runner.ts` -- `runStartupRecovery()`: add event log inspection and conditional resume
7573
+ - `src/trigger/polling-scheduler.ts` -- `doPollGitHubQueue()`: accept a `ttlOverride` param so discard path uses short TTL
7574
+ - `src/trigger/adapters/github-queue-poller.ts` -- `checkIdempotency()`: handle expired sidecars with `ttlOverride`
7575
+ - New: `src/daemon/session-recovery-policy.ts` -- pure function `evaluateRecovery(orphan, eventLog) -> 'resume' | 'discard' | 'human_review'`
7576
+
7577
+ **Priority:** High. Every daemon crash currently wastes all in-flight work and waits up to 56 min before retrying. With even basic resume (step > 0 → resume, step = 0 → discard + fast re-dispatch), we'd recover most of the lost work and reduce retry latency from 56 min to < 5 min.
7578
+
7579
+ **Depends on:** Conversation history persistence (for high-quality resume context).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exaudeus/workrail",
3
- "version": "3.59.5",
3
+ "version": "3.59.7",
4
4
  "description": "Step-by-step workflow enforcement for AI agents via MCP",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,28 +0,0 @@
1
- import 'reflect-metadata';
2
- import type { V2ToolContext } from '../mcp/types.js';
3
- import type { TriggerRouter } from './trigger-router.js';
4
- import type { PollingScheduler } from './polling-scheduler.js';
5
- import type { WorkflowService } from '../application/services/workflow-service.js';
6
- import type { SteerRegistry } from '../daemon/workflow-runner.js';
7
- import type { Result } from '../runtime/result.js';
8
- export interface DaemonConsoleHandle {
9
- readonly port: number;
10
- stop(): Promise<void>;
11
- }
12
- export type DaemonConsoleError = {
13
- readonly kind: 'port_conflict';
14
- readonly port: number;
15
- } | {
16
- readonly kind: 'io_error';
17
- readonly message: string;
18
- };
19
- export interface StartDaemonConsoleOptions {
20
- readonly port?: number;
21
- readonly triggerRouter?: TriggerRouter;
22
- readonly serverVersion?: string;
23
- readonly workflowService?: WorkflowService;
24
- readonly lockFilePath?: string;
25
- readonly steerRegistry?: SteerRegistry;
26
- readonly pollingScheduler?: PollingScheduler;
27
- }
28
- export declare function startDaemonConsole(ctx: V2ToolContext, options?: StartDaemonConsoleOptions): Promise<Result<DaemonConsoleHandle, DaemonConsoleError>>;