@exaudeus/workrail 3.42.0 → 3.44.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 (36) hide show
  1. package/dist/console-ui/assets/{index-DwfWMKvv.js → index-Bi38ITiQ.js} +1 -1
  2. package/dist/console-ui/index.html +1 -1
  3. package/dist/daemon/workflow-runner.d.ts +15 -1
  4. package/dist/daemon/workflow-runner.js +86 -9
  5. package/dist/manifest.json +39 -23
  6. package/dist/trigger/adapters/github-queue-poller.d.ts +34 -0
  7. package/dist/trigger/adapters/github-queue-poller.js +200 -0
  8. package/dist/trigger/delivery-action.d.ts +2 -0
  9. package/dist/trigger/delivery-action.js +24 -0
  10. package/dist/trigger/github-queue-config.d.ts +18 -0
  11. package/dist/trigger/github-queue-config.js +155 -0
  12. package/dist/trigger/polling-scheduler.d.ts +1 -0
  13. package/dist/trigger/polling-scheduler.js +185 -6
  14. package/dist/trigger/trigger-router.js +24 -1
  15. package/dist/trigger/trigger-store.js +77 -2
  16. package/dist/trigger/types.d.ts +19 -0
  17. package/docs/design/adaptive-coordinator-context-candidates.md +265 -0
  18. package/docs/design/adaptive-coordinator-context-review.md +101 -0
  19. package/docs/design/adaptive-coordinator-context.md +504 -0
  20. package/docs/design/adaptive-coordinator-routing-candidates.md +340 -0
  21. package/docs/design/adaptive-coordinator-routing-design-review.md +135 -0
  22. package/docs/design/adaptive-coordinator-routing-review.md +156 -0
  23. package/docs/design/adaptive-coordinator-routing.md +660 -0
  24. package/docs/design/context-assembly-layer-design-review.md +110 -0
  25. package/docs/design/context-assembly-layer.md +622 -0
  26. package/docs/design/stuck-escalation-candidates.md +176 -0
  27. package/docs/design/stuck-escalation-design-review.md +70 -0
  28. package/docs/design/stuck-escalation.md +326 -0
  29. package/docs/design/worktrain-task-queue-candidates.md +252 -0
  30. package/docs/design/worktrain-task-queue-design-review.md +109 -0
  31. package/docs/design/worktrain-task-queue.md +443 -0
  32. package/docs/design/worktree-review-findings-candidates.md +101 -0
  33. package/docs/design/worktree-review-findings-design-review.md +65 -0
  34. package/docs/design/worktree-review-findings-implementation-plan.md +153 -0
  35. package/docs/ideas/backlog.md +148 -0
  36. package/package.json +3 -3
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.DAEMON_SIGNALS_DIR = exports.DAEMON_SOUL_TEMPLATE = exports.DAEMON_SOUL_DEFAULT = exports.DAEMON_SESSIONS_DIR = void 0;
39
+ exports.DAEMON_SIGNALS_DIR = exports.DAEMON_SOUL_TEMPLATE = exports.DAEMON_SOUL_DEFAULT = exports.WORKTREES_DIR = exports.DAEMON_SESSIONS_DIR = void 0;
40
40
  exports.readDaemonSessionState = readDaemonSessionState;
41
41
  exports.readAllDaemonSessions = readAllDaemonSessions;
42
42
  exports.runStartupRecovery = runStartupRecovery;
@@ -83,7 +83,9 @@ function withWorkrailSession(sid) {
83
83
  }
84
84
  exports.DAEMON_SESSIONS_DIR = path.join(os.homedir(), '.workrail', 'daemon-sessions');
85
85
  const MAX_ORPHAN_AGE_MS = 2 * 60 * 60 * 1000;
86
+ const MAX_WORKTREE_ORPHAN_AGE_MS = 24 * 60 * 60 * 1000;
86
87
  const WORKRAIL_DIR = path.join(os.homedir(), '.workrail');
88
+ exports.WORKTREES_DIR = path.join(os.homedir(), '.workrail', 'worktrees');
87
89
  const WORKSPACE_CONTEXT_MAX_BYTES = 32 * 1024;
88
90
  const MAX_ASSEMBLED_CONTEXT_BYTES = 8192;
89
91
  const WORKSPACE_CONTEXT_CANDIDATE_PATHS = [
@@ -96,10 +98,10 @@ const soul_template_js_1 = require("./soul-template.js");
96
98
  var soul_template_js_2 = require("./soul-template.js");
97
99
  Object.defineProperty(exports, "DAEMON_SOUL_DEFAULT", { enumerable: true, get: function () { return soul_template_js_2.DAEMON_SOUL_DEFAULT; } });
98
100
  Object.defineProperty(exports, "DAEMON_SOUL_TEMPLATE", { enumerable: true, get: function () { return soul_template_js_2.DAEMON_SOUL_TEMPLATE; } });
99
- async function persistTokens(sessionId, continueToken, checkpointToken) {
101
+ async function persistTokens(sessionId, continueToken, checkpointToken, worktreePath) {
100
102
  await fs.mkdir(exports.DAEMON_SESSIONS_DIR, { recursive: true });
101
103
  const sessionPath = path.join(exports.DAEMON_SESSIONS_DIR, `${sessionId}.json`);
102
- const state = JSON.stringify({ continueToken, checkpointToken, ts: Date.now() }, null, 2);
104
+ const state = JSON.stringify({ continueToken, checkpointToken, ts: Date.now(), ...(worktreePath !== undefined ? { worktreePath } : {}) }, null, 2);
103
105
  const tmp = `${sessionPath}.tmp`;
104
106
  await fs.writeFile(tmp, state, 'utf8');
105
107
  await fs.rename(tmp, sessionPath);
@@ -145,6 +147,7 @@ async function readAllDaemonSessions(sessionsDir = exports.DAEMON_SESSIONS_DIR)
145
147
  continueToken: parsed.continueToken,
146
148
  checkpointToken: typeof parsed.checkpointToken === 'string' ? parsed.checkpointToken : null,
147
149
  ts: parsed.ts,
150
+ ...(typeof parsed.worktreePath === 'string' ? { worktreePath: parsed.worktreePath } : {}),
148
151
  });
149
152
  }
150
153
  catch (err) {
@@ -153,7 +156,7 @@ async function readAllDaemonSessions(sessionsDir = exports.DAEMON_SESSIONS_DIR)
153
156
  }
154
157
  return sessions;
155
158
  }
156
- async function runStartupRecovery(sessionsDir = exports.DAEMON_SESSIONS_DIR) {
159
+ async function runStartupRecovery(sessionsDir = exports.DAEMON_SESSIONS_DIR, execFn = execFileAsync) {
157
160
  const sessions = await readAllDaemonSessions(sessionsDir);
158
161
  if (sessions.length === 0) {
159
162
  await clearStrayTmpFiles(sessionsDir);
@@ -168,6 +171,22 @@ async function runStartupRecovery(sessionsDir = exports.DAEMON_SESSIONS_DIR) {
168
171
  const ageSec = Math.round(ageMs / 1000);
169
172
  const label = isStale ? 'stale orphaned session' : 'orphaned session';
170
173
  console.log(`[WorkflowRunner] Clearing ${label}: sessionId=${session.sessionId} age=${ageSec}s`);
174
+ if (session.worktreePath && ageMs > MAX_WORKTREE_ORPHAN_AGE_MS) {
175
+ console.log(`[WorkflowRunner] Removing orphan worktree: sessionId=${session.sessionId} worktreePath=${session.worktreePath}`);
176
+ try {
177
+ await execFn('git', ['worktree', 'remove', '--force', session.worktreePath]);
178
+ console.log(`[WorkflowRunner] Removed orphan worktree: ${session.worktreePath}`);
179
+ }
180
+ catch (err) {
181
+ console.warn(`[WorkflowRunner] Could not remove orphan worktree ${session.worktreePath}: ` +
182
+ `${err instanceof Error ? err.message : String(err)}`);
183
+ }
184
+ }
185
+ else if (session.worktreePath && ageMs <= MAX_WORKTREE_ORPHAN_AGE_MS) {
186
+ const ageHours = (ageMs / (60 * 60 * 1000)).toFixed(1);
187
+ console.log(`[WorkflowRunner] Keeping recent orphan worktree: sessionId=${session.sessionId} ` +
188
+ `age=${ageHours}h (threshold=24h) worktreePath=${session.worktreePath}`);
189
+ }
171
190
  try {
172
191
  await fs.unlink(path.join(sessionsDir, `${session.sessionId}.json`));
173
192
  cleared++;
@@ -1430,12 +1449,66 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
1430
1449
  if (startContinueToken) {
1431
1450
  await persistTokens(sessionId, startContinueToken, startCheckpointToken);
1432
1451
  }
1452
+ if (trigger.botIdentity) {
1453
+ try {
1454
+ await execFileAsync('git', ['-C', trigger.workspacePath, 'config', 'user.name', trigger.botIdentity.name]);
1455
+ await execFileAsync('git', ['-C', trigger.workspacePath, 'config', 'user.email', trigger.botIdentity.email]);
1456
+ console.log(`[WorkflowRunner] Bot identity set: sessionId=${sessionId} ` +
1457
+ `name=${trigger.botIdentity.name} email=${trigger.botIdentity.email}`);
1458
+ }
1459
+ catch (identityErr) {
1460
+ console.warn(`[WorkflowRunner] WARNING: Failed to set bot identity for sessionId=${sessionId}: ` +
1461
+ `${identityErr instanceof Error ? identityErr.message : String(identityErr)}. ` +
1462
+ `Commits will use default git config.`);
1463
+ }
1464
+ }
1465
+ let sessionWorkspacePath = trigger.workspacePath;
1466
+ let sessionWorktreePath;
1467
+ if (trigger.branchStrategy === 'worktree') {
1468
+ const branchPrefix = trigger.branchPrefix ?? 'worktrain/';
1469
+ const baseBranch = trigger.baseBranch ?? 'main';
1470
+ sessionWorkspacePath = path.join(exports.WORKTREES_DIR, sessionId);
1471
+ sessionWorktreePath = sessionWorkspacePath;
1472
+ try {
1473
+ await fs.mkdir(exports.WORKTREES_DIR, { recursive: true });
1474
+ await execFileAsync('git', ['-C', trigger.workspacePath, 'fetch', 'origin', baseBranch]);
1475
+ await execFileAsync('git', [
1476
+ '-C', trigger.workspacePath,
1477
+ 'worktree', 'add',
1478
+ sessionWorkspacePath,
1479
+ '-b', `${branchPrefix}${sessionId}`,
1480
+ `origin/${baseBranch}`,
1481
+ ]);
1482
+ await persistTokens(sessionId, startContinueToken ?? currentContinueToken, startCheckpointToken, sessionWorktreePath);
1483
+ console.log(`[WorkflowRunner] Worktree created: sessionId=${sessionId} ` +
1484
+ `branch=${branchPrefix}${sessionId} path=${sessionWorkspacePath}`);
1485
+ }
1486
+ catch (err) {
1487
+ const errMsg = err instanceof Error ? err.message : String(err);
1488
+ console.error(`[WorkflowRunner] Worktree creation failed: sessionId=${sessionId} error=${errMsg}`);
1489
+ emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'error', detail: errMsg.slice(0, 200), ...withWorkrailSession(workrailSessionId) });
1490
+ if (workrailSessionId !== null)
1491
+ daemonRegistry?.unregister(workrailSessionId, 'failed');
1492
+ return {
1493
+ _tag: 'error',
1494
+ workflowId: trigger.workflowId,
1495
+ message: `Worktree creation failed: ${errMsg}`,
1496
+ stopReason: 'error',
1497
+ };
1498
+ }
1499
+ }
1433
1500
  if (firstStep.isComplete) {
1434
1501
  await fs.unlink(path.join(exports.DAEMON_SESSIONS_DIR, `${sessionId}.json`)).catch(() => { });
1435
1502
  emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'success', detail: 'stop', ...withWorkrailSession(workrailSessionId) });
1436
1503
  if (workrailSessionId !== null)
1437
1504
  daemonRegistry?.unregister(workrailSessionId, 'completed');
1438
- return { _tag: 'success', workflowId: trigger.workflowId, stopReason: 'stop' };
1505
+ return {
1506
+ _tag: 'success',
1507
+ workflowId: trigger.workflowId,
1508
+ stopReason: 'stop',
1509
+ ...(sessionWorktreePath !== undefined ? { sessionWorkspacePath: sessionWorktreePath } : {}),
1510
+ ...(sessionWorktreePath !== undefined ? { sessionId } : {}),
1511
+ };
1439
1512
  }
1440
1513
  const schemas = getSchemas();
1441
1514
  const spawnCurrentDepth = trigger.spawnDepth ?? 0;
@@ -1444,12 +1517,12 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
1444
1517
  const tools = [
1445
1518
  makeCompleteStepTool(sessionId, ctx, () => currentContinueToken, onAdvance, onComplete, (t) => { currentContinueToken = t; }, schemas, index_js_1.executeContinueWorkflow, emitter, workrailSessionId),
1446
1519
  makeContinueWorkflowTool(sessionId, ctx, onAdvance, onComplete, schemas, index_js_1.executeContinueWorkflow, emitter, workrailSessionId),
1447
- makeBashTool(trigger.workspacePath, schemas, sessionId, emitter, workrailSessionId),
1520
+ makeBashTool(sessionWorkspacePath, schemas, sessionId, emitter, workrailSessionId),
1448
1521
  makeReadTool(readFileState, schemas, sessionId, emitter, workrailSessionId),
1449
1522
  makeWriteTool(readFileState, schemas, sessionId, emitter, workrailSessionId),
1450
- makeGlobTool(trigger.workspacePath, schemas, sessionId, emitter, workrailSessionId),
1451
- makeGrepTool(trigger.workspacePath, schemas, sessionId, emitter, workrailSessionId),
1452
- makeEditTool(trigger.workspacePath, readFileState, schemas, sessionId, emitter, workrailSessionId),
1523
+ makeGlobTool(sessionWorkspacePath, schemas, sessionId, emitter, workrailSessionId),
1524
+ makeGrepTool(sessionWorkspacePath, schemas, sessionId, emitter, workrailSessionId),
1525
+ makeEditTool(sessionWorkspacePath, readFileState, schemas, sessionId, emitter, workrailSessionId),
1453
1526
  makeReportIssueTool(sessionId, emitter, workrailSessionId, undefined, (summary) => {
1454
1527
  if (issueSummaries.length < MAX_ISSUE_SUMMARIES) {
1455
1528
  issueSummaries.push(summary);
@@ -1627,6 +1700,7 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
1627
1700
  const limitDescription = timeoutReason === 'wall_clock'
1628
1701
  ? `${trigger.agentConfig?.maxSessionMinutes ?? DEFAULT_SESSION_TIMEOUT_MINUTES} minutes`
1629
1702
  : `${trigger.agentConfig?.maxTurns ?? DEFAULT_MAX_TURNS} turns`;
1703
+ await fs.unlink(path.join(exports.DAEMON_SESSIONS_DIR, `${sessionId}.json`)).catch(() => { });
1630
1704
  return {
1631
1705
  _tag: 'timeout',
1632
1706
  workflowId: trigger.workflowId,
@@ -1651,6 +1725,7 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
1651
1725
  ...(lastToolCalled !== null && { lastToolCalled }),
1652
1726
  ...(issueSummaries.length > 0 && { issueSummaries }),
1653
1727
  })}`;
1728
+ await fs.unlink(path.join(exports.DAEMON_SESSIONS_DIR, `${sessionId}.json`)).catch(() => { });
1654
1729
  return {
1655
1730
  _tag: 'error',
1656
1731
  workflowId: trigger.workflowId,
@@ -1670,5 +1745,7 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
1670
1745
  stopReason,
1671
1746
  ...(lastStepNotes !== undefined ? { lastStepNotes } : {}),
1672
1747
  ...(lastStepArtifacts !== undefined ? { lastStepArtifacts } : {}),
1748
+ ...(sessionWorktreePath !== undefined ? { sessionWorkspacePath: sessionWorktreePath } : {}),
1749
+ ...(sessionWorktreePath !== undefined ? { sessionId } : {}),
1673
1750
  };
1674
1751
  }
@@ -449,16 +449,16 @@
449
449
  "sha256": "5fe866e54f796975dec5d8ba9983aefd86074db212d3fccd64eed04bc9f0b3da",
450
450
  "bytes": 8011
451
451
  },
452
+ "console-ui/assets/index-Bi38ITiQ.js": {
453
+ "sha256": "e3229116ac20f315184c69ef9eaa33267457e5e7aac1e22015ed830cb098e39a",
454
+ "bytes": 760528
455
+ },
452
456
  "console-ui/assets/index-DGj8EsFR.css": {
453
457
  "sha256": "3bdb55ec0957928e0ebbb86a7d6b36d28f7ba7d5c0f3e236fd8f2e2aacee2fa4",
454
458
  "bytes": 60631
455
459
  },
456
- "console-ui/assets/index-DwfWMKvv.js": {
457
- "sha256": "14a935b1b1ed6b4d2a178a1f21bd657c5f9bcede837ac06b35fbb713114e99d0",
458
- "bytes": 760528
459
- },
460
460
  "console-ui/index.html": {
461
- "sha256": "d0fec3b4d1f1156b9f4df60f7e5fe72af7f12fa8a29c90a331f0706dda67b908",
461
+ "sha256": "fb9efa806376b8f8f18d3c5d7853d04b9dfc9abc305b0ed22782379bc13a2c86",
462
462
  "bytes": 417
463
463
  },
464
464
  "console/standalone-console.d.ts": {
@@ -550,12 +550,12 @@
550
550
  "bytes": 1512
551
551
  },
552
552
  "daemon/workflow-runner.d.ts": {
553
- "sha256": "7025b0023c763c6899511c5d3635e9ab99bdc361616249aa25b36508753bf3a0",
554
- "bytes": 6154
553
+ "sha256": "0406654be8c6eb147706e81e0ba666ce372db140f4720246258e0f001653181e",
554
+ "bytes": 6628
555
555
  },
556
556
  "daemon/workflow-runner.js": {
557
- "sha256": "94fad3401dca1d854c3d254ac8b6e7ab5ae9a942c47e9c5900383a4e017c820e",
558
- "bytes": 84923
557
+ "sha256": "f40f265284aa1e32168d8a0cf28c08007186eafac6d55182b7ec6ddb53bed5a8",
558
+ "bytes": 89592
559
559
  },
560
560
  "di/container.d.ts": {
561
561
  "sha256": "003bb7fb7478d627524b9b1e76bd0a963a243794a687ff233b96dc0e33a06d9f",
@@ -1549,6 +1549,14 @@
1549
1549
  "sha256": "dd1a1bc20f1bf6da550960887c75fab4a794c19a1b79f3c89f4277145d009c2e",
1550
1550
  "bytes": 6723
1551
1551
  },
1552
+ "trigger/adapters/github-queue-poller.d.ts": {
1553
+ "sha256": "f36b01b118e6986e8f89701fc5d88fa7a1873fb38acaa67b1612895056bd5ef8",
1554
+ "bytes": 1363
1555
+ },
1556
+ "trigger/adapters/github-queue-poller.js": {
1557
+ "sha256": "73270636634fb8673c63b0c0347e6e5d7f676606d36b87a4c1aadc11f1939eb2",
1558
+ "bytes": 7340
1559
+ },
1552
1560
  "trigger/adapters/gitlab-poller.d.ts": {
1553
1561
  "sha256": "f685490fafad77194fdd0f0bbaf80dbc56730aeb344853da365199a120fbe399",
1554
1562
  "bytes": 911
@@ -1566,12 +1574,12 @@
1566
1574
  "bytes": 5471
1567
1575
  },
1568
1576
  "trigger/delivery-action.d.ts": {
1569
- "sha256": "58109eaa7124d2864e1d872ff0148548932d2fa4d7e672f5fd83d4bbe40afdda",
1570
- "bytes": 1188
1577
+ "sha256": "2b3f165759b0de49b7f49023a05efa50848331ab6cd9969b49c1409346959994",
1578
+ "bytes": 1257
1571
1579
  },
1572
1580
  "trigger/delivery-action.js": {
1573
- "sha256": "453931945078ee15656f7a89f9e804d1db8edcc14b756beeb9cfadfcb9e83102",
1574
- "bytes": 8026
1581
+ "sha256": "1a9c0d097dc0f14e66765366f878f5f8386a4a1b0c5eb9572fa90a2b60643bab",
1582
+ "bytes": 9016
1575
1583
  },
1576
1584
  "trigger/delivery-client.d.ts": {
1577
1585
  "sha256": "0cb2be24b854cb31e3d2fe7eeaba6032de7a9b2a5290c8bc886df94faf5306f7",
@@ -1581,6 +1589,14 @@
1581
1589
  "sha256": "da358ced4e99c327493b6d3ca975a623aca21f72e68787a092b2760601801c99",
1582
1590
  "bytes": 1269
1583
1591
  },
1592
+ "trigger/github-queue-config.d.ts": {
1593
+ "sha256": "bff922c1b435da55e02b4b11d3fde9f06e999f13b8ac0494788cac4a4fc4c432",
1594
+ "bytes": 766
1595
+ },
1596
+ "trigger/github-queue-config.js": {
1597
+ "sha256": "a0ec09a7725ac1ee1adb9d41fdcb4c9545128f0b9515c6de4df9535d4bec1acc",
1598
+ "bytes": 6857
1599
+ },
1584
1600
  "trigger/index.d.ts": {
1585
1601
  "sha256": "a9cfd053714173e2a8cc5a282fd5b09a5c3f3001304d507facd0e12de9cc0733",
1586
1602
  "bytes": 735
@@ -1606,12 +1622,12 @@
1606
1622
  "bytes": 6968
1607
1623
  },
1608
1624
  "trigger/polling-scheduler.d.ts": {
1609
- "sha256": "c5f984df836dbc78ec51bfc78fa5a26973aff56e6f88e0cf11776057f940b2b6",
1610
- "bytes": 761
1625
+ "sha256": "009b3340f5d46a3fc22b9cd087abcf484abe34c526a46dd4f958768fb8f61c9c",
1626
+ "bytes": 792
1611
1627
  },
1612
1628
  "trigger/polling-scheduler.js": {
1613
- "sha256": "e77a5eb32a54d98e9f7178fc15d7c237809519c183eea716cd01b7b9a2aae62e",
1614
- "bytes": 10255
1629
+ "sha256": "ec3c73739c9dfbbe3b2d7a55237fd35a290d6aab7bbc30ec5408bde69b2c95e0",
1630
+ "bytes": 19769
1615
1631
  },
1616
1632
  "trigger/trigger-listener.d.ts": {
1617
1633
  "sha256": "92e971ab8f47c3c867860cffc01f54c4aae54fcc4ae199b9469210f4ce639423",
@@ -1626,20 +1642,20 @@
1626
1642
  "bytes": 2123
1627
1643
  },
1628
1644
  "trigger/trigger-router.js": {
1629
- "sha256": "6584949ecc6bd8fe5d23bfe5b3b7feb3a488804987b2edd90733d2c0142aeec2",
1630
- "bytes": 15973
1645
+ "sha256": "605cdce397bd19e5b991fe7378faf17b4f25b4421749e1b5349413a208a4f3dd",
1646
+ "bytes": 17250
1631
1647
  },
1632
1648
  "trigger/trigger-store.d.ts": {
1633
1649
  "sha256": "7afb05127d55bc3757a550dd15d4b797766b3fff29d1bfe76b303764b93322e7",
1634
1650
  "bytes": 1588
1635
1651
  },
1636
1652
  "trigger/trigger-store.js": {
1637
- "sha256": "4e03bf0bea132c67087d69e07f3d9abddb504396438437705c26c9e7870b0680",
1638
- "bytes": 34667
1653
+ "sha256": "8015b54da7ab5b1cec78e1ecea099d5b457eaa5de267bb1ed52cbb75eca207c4",
1654
+ "bytes": 38148
1639
1655
  },
1640
1656
  "trigger/types.d.ts": {
1641
- "sha256": "c0f14b59c95cae52e06f8d9fe841292886587fad20497fd6686e842cc5696d3a",
1642
- "bytes": 2808
1657
+ "sha256": "4ccedde5b927f17edbb96203083e8ffd2d578e2cc007ff2427511112ae262e30",
1658
+ "bytes": 3475
1643
1659
  },
1644
1660
  "trigger/types.js": {
1645
1661
  "sha256": "45b4e4f23a6d1a2b07350196871b0c53840e5d8142b47f7acedd2f40ae7a6b73",
@@ -0,0 +1,34 @@
1
+ import type { GitHubQueuePollingSource } from '../types.js';
2
+ import type { GitHubQueueConfig } from '../github-queue-config.js';
3
+ import type { Result } from '../../runtime/result.js';
4
+ export interface GitHubQueueLabel {
5
+ readonly name: string;
6
+ }
7
+ export interface GitHubQueueIssue {
8
+ readonly id: number;
9
+ readonly number: number;
10
+ readonly title: string;
11
+ readonly body: string;
12
+ readonly url: string;
13
+ readonly labels: readonly GitHubQueueLabel[];
14
+ readonly createdAt: string;
15
+ }
16
+ export type GitHubQueuePollError = {
17
+ readonly kind: 'http_error';
18
+ readonly status: number;
19
+ readonly message: string;
20
+ } | {
21
+ readonly kind: 'network_error';
22
+ readonly message: string;
23
+ } | {
24
+ readonly kind: 'parse_error';
25
+ readonly message: string;
26
+ } | {
27
+ readonly kind: 'not_implemented';
28
+ readonly message: string;
29
+ };
30
+ export type FetchFn = (url: string, init: RequestInit) => Promise<Response>;
31
+ export declare const DEFAULT_SESSIONS_DIR: string;
32
+ export declare function pollGitHubQueueIssues(source: GitHubQueuePollingSource, config: GitHubQueueConfig, fetchFn?: FetchFn): Promise<Result<GitHubQueueIssue[], GitHubQueuePollError>>;
33
+ export declare function inferMaturity(body: string): 'idea' | 'specced' | 'ready';
34
+ export declare function checkIdempotency(issueNumber: number, sessionsDir?: string): Promise<'clear' | 'active'>;
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DEFAULT_SESSIONS_DIR = void 0;
37
+ exports.pollGitHubQueueIssues = pollGitHubQueueIssues;
38
+ exports.inferMaturity = inferMaturity;
39
+ exports.checkIdempotency = checkIdempotency;
40
+ const result_js_1 = require("../../runtime/result.js");
41
+ const fs = __importStar(require("node:fs/promises"));
42
+ const path = __importStar(require("node:path"));
43
+ const os = __importStar(require("node:os"));
44
+ exports.DEFAULT_SESSIONS_DIR = path.join(os.homedir(), '.workrail', 'daemon-sessions');
45
+ function checkRateLimit(response) {
46
+ const remainingHeader = response.headers.get('X-RateLimit-Remaining');
47
+ const resetHeader = response.headers.get('X-RateLimit-Reset');
48
+ if (remainingHeader === null)
49
+ return true;
50
+ const remaining = parseInt(remainingHeader, 10);
51
+ if (isNaN(remaining) || remaining >= 100)
52
+ return true;
53
+ const resetTs = parseInt(resetHeader ?? '0', 10);
54
+ const resetAt = resetTs > 0 ? new Date(resetTs * 1000).toISOString() : 'unknown';
55
+ console.warn(`[GitHubQueuePoller] Rate limit low: remaining=${remaining}, resets at ${resetAt}. ` +
56
+ `Skipping poll cycle to avoid exhaustion.`);
57
+ return false;
58
+ }
59
+ async function pollGitHubQueueIssues(source, config, fetchFn = globalThis.fetch) {
60
+ if (config.type !== 'assignee') {
61
+ return (0, result_js_1.err)({
62
+ kind: 'not_implemented',
63
+ message: `Queue type '${config.type}' is not implemented. Only 'assignee' is supported.`,
64
+ });
65
+ }
66
+ const [owner, repo] = source.repo.split('/');
67
+ const url = new URL(`https://api.github.com/repos/${owner}/${repo}/issues`);
68
+ url.searchParams.set('state', 'open');
69
+ url.searchParams.set('per_page', '100');
70
+ if (config.user) {
71
+ url.searchParams.set('assignee', config.user);
72
+ }
73
+ let response;
74
+ try {
75
+ response = await fetchFn(url.toString(), {
76
+ headers: {
77
+ 'Authorization': `Bearer ${source.token}`,
78
+ 'Accept': 'application/vnd.github+json',
79
+ 'X-GitHub-Api-Version': '2022-11-28',
80
+ },
81
+ });
82
+ }
83
+ catch (e) {
84
+ return (0, result_js_1.err)({
85
+ kind: 'network_error',
86
+ message: e instanceof Error ? e.message : String(e),
87
+ });
88
+ }
89
+ if (!response.ok) {
90
+ return (0, result_js_1.err)({
91
+ kind: 'http_error',
92
+ status: response.status,
93
+ message: `GitHub API returned HTTP ${response.status}: ${response.statusText}`,
94
+ });
95
+ }
96
+ if (!checkRateLimit(response)) {
97
+ return (0, result_js_1.ok)([]);
98
+ }
99
+ let raw;
100
+ try {
101
+ raw = await response.json();
102
+ }
103
+ catch (e) {
104
+ return (0, result_js_1.err)({
105
+ kind: 'parse_error',
106
+ message: `Failed to parse GitHub Issues API response: ${e instanceof Error ? e.message : String(e)}`,
107
+ });
108
+ }
109
+ if (!Array.isArray(raw)) {
110
+ return (0, result_js_1.err)({
111
+ kind: 'parse_error',
112
+ message: `Expected array from GitHub Issues API, got: ${typeof raw}`,
113
+ });
114
+ }
115
+ const issues = [];
116
+ for (const item of raw) {
117
+ const shaped = toGitHubQueueIssue(item);
118
+ if (shaped !== null) {
119
+ issues.push(shaped);
120
+ }
121
+ }
122
+ return (0, result_js_1.ok)(issues);
123
+ }
124
+ function inferMaturity(body) {
125
+ const specLineMatch = /upstream_spec:\s*(https?:\/\/\S+)/i.exec(body);
126
+ if (specLineMatch)
127
+ return 'ready';
128
+ const firstPara = body.split(/\n\s*\n/)[0] ?? '';
129
+ if (/https?:\/\/\S*\/(?:pitch|prd|spec|brd|rfc|design)\b/i.test(firstPara))
130
+ return 'ready';
131
+ if (/- \[ \]/.test(body))
132
+ return 'specced';
133
+ if (/^#{1,6}\s*(Acceptance Criteria|Implementation Plan)\s*$/im.test(body))
134
+ return 'specced';
135
+ return 'idea';
136
+ }
137
+ async function checkIdempotency(issueNumber, sessionsDir = exports.DEFAULT_SESSIONS_DIR) {
138
+ let files;
139
+ try {
140
+ files = await fs.readdir(sessionsDir);
141
+ }
142
+ catch {
143
+ return 'clear';
144
+ }
145
+ const jsonFiles = files.filter(f => f.endsWith('.json'));
146
+ for (const filename of jsonFiles) {
147
+ try {
148
+ const content = await fs.readFile(path.join(sessionsDir, filename), 'utf8');
149
+ const parsed = JSON.parse(content);
150
+ if (typeof parsed !== 'object' || parsed === null) {
151
+ return 'active';
152
+ }
153
+ const session = parsed;
154
+ const context = session['context'];
155
+ if (typeof context !== 'object' || context === null) {
156
+ continue;
157
+ }
158
+ const ctx = context;
159
+ const taskCandidate = ctx['taskCandidate'];
160
+ if (typeof taskCandidate !== 'object' || taskCandidate === null) {
161
+ continue;
162
+ }
163
+ const tc = taskCandidate;
164
+ if (tc['issueNumber'] === issueNumber) {
165
+ return 'active';
166
+ }
167
+ }
168
+ catch {
169
+ return 'active';
170
+ }
171
+ }
172
+ return 'clear';
173
+ }
174
+ function toGitHubQueueIssue(item) {
175
+ if (typeof item !== 'object' || item === null)
176
+ return null;
177
+ const obj = item;
178
+ if (typeof obj['id'] !== 'number' ||
179
+ typeof obj['number'] !== 'number' ||
180
+ typeof obj['title'] !== 'string' ||
181
+ typeof obj['html_url'] !== 'string') {
182
+ return null;
183
+ }
184
+ const body = typeof obj['body'] === 'string' ? obj['body'] : '';
185
+ const createdAt = typeof obj['created_at'] === 'string' ? obj['created_at'] : '';
186
+ const rawLabels = Array.isArray(obj['labels']) ? obj['labels'] : [];
187
+ const labels = rawLabels
188
+ .filter((l) => typeof l === 'object' && l !== null)
189
+ .filter((l) => typeof l['name'] === 'string')
190
+ .map((l) => ({ name: l['name'] }));
191
+ return {
192
+ id: obj['id'],
193
+ number: obj['number'],
194
+ title: obj['title'],
195
+ body,
196
+ url: obj['html_url'],
197
+ labels,
198
+ createdAt,
199
+ };
200
+ }
@@ -11,6 +11,8 @@ export interface HandoffArtifact {
11
11
  export interface DeliveryFlags {
12
12
  readonly autoCommit?: boolean;
13
13
  readonly autoOpenPR?: boolean;
14
+ readonly sessionId?: string;
15
+ readonly branchPrefix?: string;
14
16
  }
15
17
  export type DeliveryResult = {
16
18
  readonly _tag: 'committed';
@@ -146,6 +146,30 @@ async function runDelivery(artifact, workspacePath, flags, execFn) {
146
146
  reason: 'filesChanged is empty -- cannot stage files safely (no git add -A fallback)',
147
147
  };
148
148
  }
149
+ if (flags.sessionId) {
150
+ const expectedBranch = `${flags.branchPrefix ?? 'worktrain/'}${flags.sessionId}`;
151
+ let headBranch;
152
+ try {
153
+ const result = await execFn('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: workspacePath, timeout: DELIVERY_TIMEOUT_MS });
154
+ headBranch = result.stdout.trim();
155
+ }
156
+ catch (e) {
157
+ return {
158
+ _tag: 'error',
159
+ phase: 'commit',
160
+ details: `HEAD branch check failed (cannot stage): ${formatExecError(e)}`,
161
+ };
162
+ }
163
+ if (headBranch !== expectedBranch) {
164
+ return {
165
+ _tag: 'error',
166
+ phase: 'commit',
167
+ details: `HEAD branch mismatch: expected "${expectedBranch}" but found "${headBranch}". ` +
168
+ `Refusing to stage or push -- the agent may have switched branches. ` +
169
+ `Worktree path: ${workspacePath}`,
170
+ };
171
+ }
172
+ }
149
173
  const commitMessage = artifact.commitSubject.startsWith(`${artifact.commitType}(`)
150
174
  ? artifact.commitSubject
151
175
  : `${artifact.commitType}(${artifact.commitScope}): ${artifact.commitSubject}`;
@@ -0,0 +1,18 @@
1
+ import type { Result } from '../runtime/result.js';
2
+ export interface GitHubQueueConfig {
3
+ readonly type: 'assignee' | 'label' | 'mention' | 'query';
4
+ readonly user?: string;
5
+ readonly name?: string;
6
+ readonly handle?: string;
7
+ readonly search?: string;
8
+ readonly workOnAll?: boolean;
9
+ readonly pollIntervalSeconds: number;
10
+ readonly maxTotalConcurrentSessions: number;
11
+ readonly excludeLabels: readonly string[];
12
+ readonly repo: string;
13
+ readonly token: string;
14
+ readonly botName?: string;
15
+ readonly botEmail?: string;
16
+ }
17
+ export declare const WORKRAIL_CONFIG_PATH: string;
18
+ export declare function loadQueueConfig(configPath?: string, env?: Record<string, string | undefined>): Promise<Result<GitHubQueueConfig | null, string>>;