@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.
- package/dist/console-ui/assets/{index-DwfWMKvv.js → index-Bi38ITiQ.js} +1 -1
- package/dist/console-ui/index.html +1 -1
- package/dist/daemon/workflow-runner.d.ts +15 -1
- package/dist/daemon/workflow-runner.js +86 -9
- package/dist/manifest.json +39 -23
- package/dist/trigger/adapters/github-queue-poller.d.ts +34 -0
- package/dist/trigger/adapters/github-queue-poller.js +200 -0
- package/dist/trigger/delivery-action.d.ts +2 -0
- package/dist/trigger/delivery-action.js +24 -0
- package/dist/trigger/github-queue-config.d.ts +18 -0
- package/dist/trigger/github-queue-config.js +155 -0
- package/dist/trigger/polling-scheduler.d.ts +1 -0
- package/dist/trigger/polling-scheduler.js +185 -6
- package/dist/trigger/trigger-router.js +24 -1
- package/dist/trigger/trigger-store.js +77 -2
- package/dist/trigger/types.d.ts +19 -0
- package/docs/design/adaptive-coordinator-context-candidates.md +265 -0
- package/docs/design/adaptive-coordinator-context-review.md +101 -0
- package/docs/design/adaptive-coordinator-context.md +504 -0
- package/docs/design/adaptive-coordinator-routing-candidates.md +340 -0
- package/docs/design/adaptive-coordinator-routing-design-review.md +135 -0
- package/docs/design/adaptive-coordinator-routing-review.md +156 -0
- package/docs/design/adaptive-coordinator-routing.md +660 -0
- package/docs/design/context-assembly-layer-design-review.md +110 -0
- package/docs/design/context-assembly-layer.md +622 -0
- package/docs/design/stuck-escalation-candidates.md +176 -0
- package/docs/design/stuck-escalation-design-review.md +70 -0
- package/docs/design/stuck-escalation.md +326 -0
- package/docs/design/worktrain-task-queue-candidates.md +252 -0
- package/docs/design/worktrain-task-queue-design-review.md +109 -0
- package/docs/design/worktrain-task-queue.md +443 -0
- package/docs/design/worktree-review-findings-candidates.md +101 -0
- package/docs/design/worktree-review-findings-design-review.md +65 -0
- package/docs/design/worktree-review-findings-implementation-plan.md +153 -0
- package/docs/ideas/backlog.md +148 -0
- 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 {
|
|
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(
|
|
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(
|
|
1451
|
-
makeGrepTool(
|
|
1452
|
-
makeEditTool(
|
|
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
|
}
|
package/dist/manifest.json
CHANGED
|
@@ -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": "
|
|
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": "
|
|
554
|
-
"bytes":
|
|
553
|
+
"sha256": "0406654be8c6eb147706e81e0ba666ce372db140f4720246258e0f001653181e",
|
|
554
|
+
"bytes": 6628
|
|
555
555
|
},
|
|
556
556
|
"daemon/workflow-runner.js": {
|
|
557
|
-
"sha256": "
|
|
558
|
-
"bytes":
|
|
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": "
|
|
1570
|
-
"bytes":
|
|
1577
|
+
"sha256": "2b3f165759b0de49b7f49023a05efa50848331ab6cd9969b49c1409346959994",
|
|
1578
|
+
"bytes": 1257
|
|
1571
1579
|
},
|
|
1572
1580
|
"trigger/delivery-action.js": {
|
|
1573
|
-
"sha256": "
|
|
1574
|
-
"bytes":
|
|
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": "
|
|
1610
|
-
"bytes":
|
|
1625
|
+
"sha256": "009b3340f5d46a3fc22b9cd087abcf484abe34c526a46dd4f958768fb8f61c9c",
|
|
1626
|
+
"bytes": 792
|
|
1611
1627
|
},
|
|
1612
1628
|
"trigger/polling-scheduler.js": {
|
|
1613
|
-
"sha256": "
|
|
1614
|
-
"bytes":
|
|
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": "
|
|
1630
|
-
"bytes":
|
|
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": "
|
|
1638
|
-
"bytes":
|
|
1653
|
+
"sha256": "8015b54da7ab5b1cec78e1ecea099d5b457eaa5de267bb1ed52cbb75eca207c4",
|
|
1654
|
+
"bytes": 38148
|
|
1639
1655
|
},
|
|
1640
1656
|
"trigger/types.d.ts": {
|
|
1641
|
-
"sha256": "
|
|
1642
|
-
"bytes":
|
|
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>>;
|