@exaudeus/workrail 3.37.1 → 3.39.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/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.js +4 -1
- package/dist/cli/commands/worktrain-overview.d.ts +32 -0
- package/dist/cli/commands/worktrain-overview.js +166 -0
- package/dist/cli-worktrain.js +253 -2
- package/dist/console-ui/assets/{index-t8Wi304z.js → index-3oXZ_A9m.js} +1 -1
- package/dist/console-ui/index.html +1 -1
- package/dist/coordinators/pr-review.d.ts +57 -0
- package/dist/coordinators/pr-review.js +520 -0
- package/dist/manifest.json +27 -11
- package/docs/discovery/coordinator-design-review.md +73 -0
- package/docs/discovery/coordinator-script-design.md +96 -679
- package/docs/discovery/hypothesis-challenge-report.md +44 -0
- package/docs/discovery/simulation-report.md +85 -0
- package/package.json +1 -1
|
@@ -12,3 +12,4 @@ export { executeWorktrainInboxCommand, type OutboxMessage, type InboxCursor, typ
|
|
|
12
12
|
export { executeWorktrainSpawnCommand, type WorktrainSpawnCommandDeps, type WorktrainSpawnCommandOpts, } from './worktrain-spawn.js';
|
|
13
13
|
export { executeWorktrainAwaitCommand, parseDurationMs, type WorktrainAwaitCommandDeps, type WorktrainAwaitCommandOpts, type SessionOutcome, type SessionResult, type AwaitResult, } from './worktrain-await.js';
|
|
14
14
|
export { executeWorktrainDaemonCommand, type WorktrainDaemonCommandDeps, type WorktrainDaemonCommandOpts, } from './worktrain-daemon.js';
|
|
15
|
+
export { executeWorktrainOverviewCommand, buildConsoleServiceFromDataDir, type WorktrainOverviewCommandDeps, type WorktrainOverviewCommandOpts, type StatusDataPacket, type StatusSession, } from './worktrain-overview.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.executeWorktrainDaemonCommand = exports.parseDurationMs = exports.executeWorktrainAwaitCommand = exports.executeWorktrainSpawnCommand = exports.executeWorktrainInboxCommand = exports.executeWorktrainTellCommand = exports.executeWorktrainInitCommand = exports.detectWorkflowVersion = exports.migrateWorkflowFile = exports.migrateWorkflow = exports.executeMigrateCommand = exports.executeVersionCommand = exports.executeCleanupCommand = exports.executeStartCommand = exports.executeValidateCommand = exports.executeListCommand = exports.getWorkflowSources = exports.executeSourcesCommand = exports.executeInitConfigCommand = exports.executeInitCommand = void 0;
|
|
3
|
+
exports.buildConsoleServiceFromDataDir = exports.executeWorktrainOverviewCommand = exports.executeWorktrainDaemonCommand = exports.parseDurationMs = exports.executeWorktrainAwaitCommand = exports.executeWorktrainSpawnCommand = exports.executeWorktrainInboxCommand = exports.executeWorktrainTellCommand = exports.executeWorktrainInitCommand = exports.detectWorkflowVersion = exports.migrateWorkflowFile = exports.migrateWorkflow = exports.executeMigrateCommand = exports.executeVersionCommand = exports.executeCleanupCommand = exports.executeStartCommand = exports.executeValidateCommand = exports.executeListCommand = exports.getWorkflowSources = exports.executeSourcesCommand = exports.executeInitConfigCommand = exports.executeInitCommand = void 0;
|
|
4
4
|
var init_js_1 = require("./init.js");
|
|
5
5
|
Object.defineProperty(exports, "executeInitCommand", { enumerable: true, get: function () { return init_js_1.executeInitCommand; } });
|
|
6
6
|
Object.defineProperty(exports, "executeInitConfigCommand", { enumerable: true, get: function () { return init_js_1.executeInitConfigCommand; } });
|
|
@@ -35,3 +35,6 @@ Object.defineProperty(exports, "executeWorktrainAwaitCommand", { enumerable: tru
|
|
|
35
35
|
Object.defineProperty(exports, "parseDurationMs", { enumerable: true, get: function () { return worktrain_await_js_1.parseDurationMs; } });
|
|
36
36
|
var worktrain_daemon_js_1 = require("./worktrain-daemon.js");
|
|
37
37
|
Object.defineProperty(exports, "executeWorktrainDaemonCommand", { enumerable: true, get: function () { return worktrain_daemon_js_1.executeWorktrainDaemonCommand; } });
|
|
38
|
+
var worktrain_overview_js_1 = require("./worktrain-overview.js");
|
|
39
|
+
Object.defineProperty(exports, "executeWorktrainOverviewCommand", { enumerable: true, get: function () { return worktrain_overview_js_1.executeWorktrainOverviewCommand; } });
|
|
40
|
+
Object.defineProperty(exports, "buildConsoleServiceFromDataDir", { enumerable: true, get: function () { return worktrain_overview_js_1.buildConsoleServiceFromDataDir; } });
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ConsoleService } from '../../v2/usecases/console-service.js';
|
|
2
|
+
export interface StatusSession {
|
|
3
|
+
readonly sessionId: string;
|
|
4
|
+
readonly title: string;
|
|
5
|
+
readonly status: 'active' | 'recent';
|
|
6
|
+
readonly rawStatus: string;
|
|
7
|
+
readonly stepLabel: string | null;
|
|
8
|
+
readonly lastModifiedMs: number;
|
|
9
|
+
readonly isComplete: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface StatusDataPacket {
|
|
12
|
+
readonly asOfMs: number;
|
|
13
|
+
readonly activeSessions: readonly StatusSession[];
|
|
14
|
+
readonly recentSessions: readonly StatusSession[];
|
|
15
|
+
readonly isDaemonless: true;
|
|
16
|
+
}
|
|
17
|
+
export interface WorktrainOverviewCommandDeps {
|
|
18
|
+
readonly now: () => number;
|
|
19
|
+
readonly buildConsoleService: (dataDir: string) => ConsoleService;
|
|
20
|
+
readonly homedir: () => string;
|
|
21
|
+
readonly joinPath: (...parts: string[]) => string;
|
|
22
|
+
readonly print: (line: string) => void;
|
|
23
|
+
readonly getDataDirEnv: () => string | undefined;
|
|
24
|
+
}
|
|
25
|
+
export interface WorktrainOverviewCommandOpts {
|
|
26
|
+
readonly json?: boolean;
|
|
27
|
+
readonly workspace?: string;
|
|
28
|
+
readonly activeThresholdMs?: number;
|
|
29
|
+
readonly recentWindowMs?: number;
|
|
30
|
+
}
|
|
31
|
+
export declare function executeWorktrainOverviewCommand(deps: WorktrainOverviewCommandDeps, opts?: WorktrainOverviewCommandOpts): Promise<void>;
|
|
32
|
+
export declare function buildConsoleServiceFromDataDir(dataDir: string): ConsoleService;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.executeWorktrainOverviewCommand = executeWorktrainOverviewCommand;
|
|
4
|
+
exports.buildConsoleServiceFromDataDir = buildConsoleServiceFromDataDir;
|
|
5
|
+
const index_js_1 = require("../../v2/infra/local/data-dir/index.js");
|
|
6
|
+
const index_js_2 = require("../../v2/infra/local/directory-listing/index.js");
|
|
7
|
+
const index_js_3 = require("../../v2/infra/local/fs/index.js");
|
|
8
|
+
const index_js_4 = require("../../v2/infra/local/sha256/index.js");
|
|
9
|
+
const index_js_5 = require("../../v2/infra/local/crypto/index.js");
|
|
10
|
+
const index_js_6 = require("../../v2/infra/local/snapshot-store/index.js");
|
|
11
|
+
const index_js_7 = require("../../v2/infra/local/pinned-workflow-store/index.js");
|
|
12
|
+
const index_js_8 = require("../../v2/infra/local/session-store/index.js");
|
|
13
|
+
const console_service_js_1 = require("../../v2/usecases/console-service.js");
|
|
14
|
+
const DEFAULT_ACTIVE_THRESHOLD_MS = 2 * 60 * 60 * 1000;
|
|
15
|
+
const DEFAULT_RECENT_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
16
|
+
function formatRelativeTime(deltaMs) {
|
|
17
|
+
const totalMinutes = Math.floor(deltaMs / 60000);
|
|
18
|
+
if (totalMinutes < 60)
|
|
19
|
+
return `${totalMinutes}m ago`;
|
|
20
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
21
|
+
if (hours < 24)
|
|
22
|
+
return `${hours}h ago`;
|
|
23
|
+
const days = Math.floor(hours / 24);
|
|
24
|
+
return `${days}d ago`;
|
|
25
|
+
}
|
|
26
|
+
function formatRunningTime(deltaMs) {
|
|
27
|
+
const totalMinutes = Math.floor(deltaMs / 60000);
|
|
28
|
+
if (totalMinutes < 60)
|
|
29
|
+
return `running ${totalMinutes}m`;
|
|
30
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
31
|
+
return `running ${hours}h`;
|
|
32
|
+
}
|
|
33
|
+
function buildSessionTitle(s) {
|
|
34
|
+
if (s.sessionTitle && s.sessionTitle.trim().length > 0) {
|
|
35
|
+
return s.sessionTitle.trim();
|
|
36
|
+
}
|
|
37
|
+
if (s.workflowName && s.workflowName.trim().length > 0) {
|
|
38
|
+
return s.workflowName.trim();
|
|
39
|
+
}
|
|
40
|
+
if (s.workflowId && s.workflowId.trim().length > 0) {
|
|
41
|
+
return s.workflowId.trim();
|
|
42
|
+
}
|
|
43
|
+
return String(s.sessionId).slice(0, 20) + '...';
|
|
44
|
+
}
|
|
45
|
+
function extractStepLabel(_s) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function isCompleted(s) {
|
|
49
|
+
return s.status === 'complete' || s.status === 'complete_with_gaps';
|
|
50
|
+
}
|
|
51
|
+
function isInProgress(s) {
|
|
52
|
+
return s.status === 'in_progress' || s.status === 'blocked' || s.status === 'dormant';
|
|
53
|
+
}
|
|
54
|
+
async function executeWorktrainOverviewCommand(deps, opts = {}) {
|
|
55
|
+
const nowMs = deps.now();
|
|
56
|
+
const activeThresholdMs = opts.activeThresholdMs ?? DEFAULT_ACTIVE_THRESHOLD_MS;
|
|
57
|
+
const recentWindowMs = opts.recentWindowMs ?? DEFAULT_RECENT_WINDOW_MS;
|
|
58
|
+
const dataDir = deps.getDataDirEnv()
|
|
59
|
+
?? deps.joinPath(deps.homedir(), '.workrail', 'data');
|
|
60
|
+
const consoleService = deps.buildConsoleService(dataDir);
|
|
61
|
+
const sessionListResult = await consoleService.getSessionList();
|
|
62
|
+
const sessions = sessionListResult.isOk() ? sessionListResult.value.sessions : [];
|
|
63
|
+
const activeSessions = [];
|
|
64
|
+
const recentSessions = [];
|
|
65
|
+
for (const s of sessions) {
|
|
66
|
+
const lastMod = s.lastModifiedMs;
|
|
67
|
+
const age = nowMs - lastMod;
|
|
68
|
+
if (isInProgress(s) && age <= activeThresholdMs) {
|
|
69
|
+
activeSessions.push({
|
|
70
|
+
sessionId: String(s.sessionId),
|
|
71
|
+
title: buildSessionTitle(s),
|
|
72
|
+
status: 'active',
|
|
73
|
+
rawStatus: s.status,
|
|
74
|
+
stepLabel: extractStepLabel(s),
|
|
75
|
+
lastModifiedMs: lastMod,
|
|
76
|
+
isComplete: false,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
else if (isCompleted(s) && age <= recentWindowMs) {
|
|
80
|
+
recentSessions.push({
|
|
81
|
+
sessionId: String(s.sessionId),
|
|
82
|
+
title: buildSessionTitle(s),
|
|
83
|
+
status: 'recent',
|
|
84
|
+
rawStatus: s.status,
|
|
85
|
+
stepLabel: extractStepLabel(s),
|
|
86
|
+
lastModifiedMs: lastMod,
|
|
87
|
+
isComplete: true,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
activeSessions.sort((a, b) => b.lastModifiedMs - a.lastModifiedMs);
|
|
92
|
+
recentSessions.sort((a, b) => b.lastModifiedMs - a.lastModifiedMs);
|
|
93
|
+
const packet = {
|
|
94
|
+
asOfMs: nowMs,
|
|
95
|
+
activeSessions,
|
|
96
|
+
recentSessions,
|
|
97
|
+
isDaemonless: true,
|
|
98
|
+
};
|
|
99
|
+
if (opts.json) {
|
|
100
|
+
deps.print(JSON.stringify(packet, null, 2));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const date = new Date(nowMs);
|
|
104
|
+
const dateStr = date.toLocaleDateString('en-US', {
|
|
105
|
+
weekday: 'short',
|
|
106
|
+
month: 'short',
|
|
107
|
+
day: 'numeric',
|
|
108
|
+
year: 'numeric',
|
|
109
|
+
});
|
|
110
|
+
const timeStr = date.toLocaleTimeString('en-US', {
|
|
111
|
+
hour: '2-digit',
|
|
112
|
+
minute: '2-digit',
|
|
113
|
+
hour12: false,
|
|
114
|
+
});
|
|
115
|
+
deps.print(`WorkTrain [${dateStr} ${timeStr}]`);
|
|
116
|
+
deps.print('Note: live session detection requires daemon (showing last-known state).');
|
|
117
|
+
deps.print('');
|
|
118
|
+
if (activeSessions.length === 0 && recentSessions.length === 0) {
|
|
119
|
+
deps.print('No recent sessions. Run `worktrain daemon` to start.');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (activeSessions.length > 0) {
|
|
123
|
+
deps.print(`ACTIVE (${activeSessions.length} session${activeSessions.length !== 1 ? 's' : ''})`);
|
|
124
|
+
for (const s of activeSessions) {
|
|
125
|
+
const runningStr = formatRunningTime(nowMs - s.lastModifiedMs);
|
|
126
|
+
deps.print(` ${s.rawStatus} ${s.title}`);
|
|
127
|
+
if (s.stepLabel) {
|
|
128
|
+
deps.print(` Step -- ${s.stepLabel} -- ${runningStr}`);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
deps.print(` ${runningStr}`);
|
|
132
|
+
}
|
|
133
|
+
deps.print('');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (recentSessions.length > 0) {
|
|
137
|
+
deps.print(`RECENT (last 24h, ${recentSessions.length} completed)`);
|
|
138
|
+
for (const s of recentSessions) {
|
|
139
|
+
const agoStr = formatRelativeTime(nowMs - s.lastModifiedMs);
|
|
140
|
+
deps.print(` done ${s.title} ${agoStr}`);
|
|
141
|
+
}
|
|
142
|
+
deps.print('');
|
|
143
|
+
}
|
|
144
|
+
deps.print('Run `worktrain console` for full session details.');
|
|
145
|
+
}
|
|
146
|
+
function buildConsoleServiceFromDataDir(dataDir) {
|
|
147
|
+
const envWithDataDir = {
|
|
148
|
+
...process.env,
|
|
149
|
+
WORKRAIL_DATA_DIR: dataDir,
|
|
150
|
+
};
|
|
151
|
+
const dataDirPort = new index_js_1.LocalDataDirV2(envWithDataDir);
|
|
152
|
+
const fsPort = new index_js_3.NodeFileSystemV2();
|
|
153
|
+
const sha256 = new index_js_4.NodeSha256V2();
|
|
154
|
+
const crypto = new index_js_5.NodeCryptoV2();
|
|
155
|
+
const directoryListing = new index_js_2.LocalDirectoryListingV2(fsPort);
|
|
156
|
+
const sessionStore = new index_js_8.LocalSessionEventLogStoreV2(dataDirPort, fsPort, sha256);
|
|
157
|
+
const snapshotStore = new index_js_6.LocalSnapshotStoreV2(dataDirPort, fsPort, crypto);
|
|
158
|
+
const pinnedWorkflowStore = new index_js_7.LocalPinnedWorkflowStoreV2(dataDirPort, fsPort);
|
|
159
|
+
return new console_service_js_1.ConsoleService({
|
|
160
|
+
directoryListing,
|
|
161
|
+
dataDir: dataDirPort,
|
|
162
|
+
sessionStore,
|
|
163
|
+
snapshotStore,
|
|
164
|
+
pinnedWorkflowStore,
|
|
165
|
+
});
|
|
166
|
+
}
|
package/dist/cli-worktrain.js
CHANGED
|
@@ -522,9 +522,9 @@ program
|
|
|
522
522
|
}
|
|
523
523
|
});
|
|
524
524
|
program
|
|
525
|
-
.command('
|
|
525
|
+
.command('health <sessionId>')
|
|
526
526
|
.description('Print a health summary for a daemon session. Accepts sessionId (UUID prefix) or workrailSessionId (sess_xxx).')
|
|
527
|
-
.action(
|
|
527
|
+
.action((sessionId) => {
|
|
528
528
|
if (sessionId.length < 20) {
|
|
529
529
|
process.stderr.write(`Warning: session ID "${sessionId}" is shorter than 20 characters -- ` +
|
|
530
530
|
`provide more characters to avoid matching multiple sessions.\n`);
|
|
@@ -540,6 +540,50 @@ program
|
|
|
540
540
|
process.stdout.write(`No events today. Is the daemon running? (Expected: ${filePath})\n`);
|
|
541
541
|
return;
|
|
542
542
|
}
|
|
543
|
+
runHealthSummary(sessionId, raw);
|
|
544
|
+
});
|
|
545
|
+
program
|
|
546
|
+
.command('status [sessionId]')
|
|
547
|
+
.description('Print an overview of active and recently completed sessions (no args), ' +
|
|
548
|
+
'or a session health summary when a sessionId is provided (deprecated: use `worktrain health <id>`).')
|
|
549
|
+
.option('--json', 'Output machine-readable JSON packet')
|
|
550
|
+
.option('-w, --workspace <path>', 'Filter sessions by workspace (reserved for future use)')
|
|
551
|
+
.action(async (sessionId, options) => {
|
|
552
|
+
if (sessionId !== undefined) {
|
|
553
|
+
process.stderr.write(`Deprecation notice: \`worktrain status <sessionId>\` has been renamed to \`worktrain health <sessionId>\`.\n` +
|
|
554
|
+
`Please update your scripts to use \`worktrain health ${sessionId}\`.\n\n`);
|
|
555
|
+
if (sessionId.length < 20) {
|
|
556
|
+
process.stderr.write(`Warning: session ID "${sessionId}" is shorter than 20 characters -- ` +
|
|
557
|
+
`provide more characters to avoid matching multiple sessions.\n`);
|
|
558
|
+
}
|
|
559
|
+
const eventsDir = path_1.default.join(os_1.default.homedir(), '.workrail', 'events', 'daemon');
|
|
560
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
561
|
+
const filePath = path_1.default.join(eventsDir, `${date}.jsonl`);
|
|
562
|
+
let raw;
|
|
563
|
+
try {
|
|
564
|
+
raw = fs_1.default.readFileSync(filePath, 'utf8');
|
|
565
|
+
}
|
|
566
|
+
catch {
|
|
567
|
+
process.stdout.write(`No events today. Is the daemon running? (Expected: ${filePath})\n`);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
process.stderr.write(`\nNote: This is the old \`worktrain status <id>\` output. Use \`worktrain health <id>\` instead.\n\n`);
|
|
571
|
+
runHealthSummary(sessionId, raw);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
await (0, index_js_1.executeWorktrainOverviewCommand)({
|
|
575
|
+
now: () => Date.now(),
|
|
576
|
+
buildConsoleService: index_js_1.buildConsoleServiceFromDataDir,
|
|
577
|
+
homedir: os_1.default.homedir,
|
|
578
|
+
joinPath: path_1.default.join,
|
|
579
|
+
print: (line) => process.stdout.write(line + '\n'),
|
|
580
|
+
getDataDirEnv: () => process.env['WORKRAIL_DATA_DIR'],
|
|
581
|
+
}, {
|
|
582
|
+
json: options.json,
|
|
583
|
+
workspace: options.workspace,
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
function runHealthSummary(sessionId, raw) {
|
|
543
587
|
let workflowId = null;
|
|
544
588
|
let firstTs = null;
|
|
545
589
|
let lastTs = null;
|
|
@@ -657,5 +701,212 @@ program
|
|
|
657
701
|
process.stdout.write(`*** WARNING: ${llmTurns} turns with 0 step advances (possible stuck)\n`);
|
|
658
702
|
}
|
|
659
703
|
process.stdout.write('\n');
|
|
704
|
+
}
|
|
705
|
+
const runCommand = program
|
|
706
|
+
.command('run')
|
|
707
|
+
.description('Run a coordinator script');
|
|
708
|
+
runCommand
|
|
709
|
+
.command('pr-review')
|
|
710
|
+
.description('Review open PRs autonomously: dispatch review sessions, route by findings, merge or escalate')
|
|
711
|
+
.requiredOption('-W, --workspace <path>', 'Absolute path to the git workspace')
|
|
712
|
+
.option('-r, --pr <number>', 'Review a specific PR number (repeatable)', (val, prev) => [...prev, parseInt(val, 10)], [])
|
|
713
|
+
.option('--dry-run', 'Print actions without dispatching sessions or merging')
|
|
714
|
+
.option('-p, --port <n>', 'Console HTTP server port (default: auto-discover from lock file, then 3456)', parseInt)
|
|
715
|
+
.action(async (options) => {
|
|
716
|
+
const { runPrReviewCoordinator, discoverConsolePort, } = await Promise.resolve().then(() => __importStar(require('./coordinators/pr-review.js')));
|
|
717
|
+
const { execFile: execFileRaw } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
|
718
|
+
const execFilePromise = (0, util_1.promisify)(execFileRaw);
|
|
719
|
+
if (!path_1.default.isAbsolute(options.workspace)) {
|
|
720
|
+
process.stderr.write(`Error: --workspace must be an absolute path, got: ${options.workspace}\n`);
|
|
721
|
+
process.exit(1);
|
|
722
|
+
}
|
|
723
|
+
try {
|
|
724
|
+
const stat = await fs_1.default.promises.stat(options.workspace);
|
|
725
|
+
if (!stat.isDirectory()) {
|
|
726
|
+
process.stderr.write(`Error: --workspace must be an existing directory: ${options.workspace}\n`);
|
|
727
|
+
process.exit(1);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
catch {
|
|
731
|
+
process.stderr.write(`Error: --workspace does not exist: ${options.workspace}\n`);
|
|
732
|
+
process.exit(1);
|
|
733
|
+
}
|
|
734
|
+
const port = await discoverConsolePort({
|
|
735
|
+
readFile: (p) => fs_1.default.promises.readFile(p, 'utf-8'),
|
|
736
|
+
homedir: os_1.default.homedir,
|
|
737
|
+
joinPath: path_1.default.join,
|
|
738
|
+
}, options.port);
|
|
739
|
+
const deps = {
|
|
740
|
+
spawnSession: async (workflowId, goal, workspace) => {
|
|
741
|
+
const url = `http://127.0.0.1:${port}/api/v2/auto/dispatch`;
|
|
742
|
+
try {
|
|
743
|
+
const response = await globalThis.fetch(url, {
|
|
744
|
+
method: 'POST',
|
|
745
|
+
headers: { 'Content-Type': 'application/json' },
|
|
746
|
+
body: JSON.stringify({ workflowId, goal, workspacePath: workspace }),
|
|
747
|
+
signal: AbortSignal.timeout(30000),
|
|
748
|
+
});
|
|
749
|
+
const body = await response.json();
|
|
750
|
+
if (!response.ok) {
|
|
751
|
+
const errMsg = typeof body['error'] === 'string' ? body['error'] : `HTTP ${response.status}`;
|
|
752
|
+
if (response.status === 503) {
|
|
753
|
+
return { kind: 'err', error: `WorkTrain daemon is not ready: ${errMsg}` };
|
|
754
|
+
}
|
|
755
|
+
return { kind: 'err', error: `Dispatch failed: ${errMsg}` };
|
|
756
|
+
}
|
|
757
|
+
if (body['success'] !== true || typeof body['data'] !== 'object') {
|
|
758
|
+
return { kind: 'err', error: 'Unexpected response from dispatch endpoint' };
|
|
759
|
+
}
|
|
760
|
+
const data = body['data'];
|
|
761
|
+
const handle = typeof data['sessionHandle'] === 'string' ? data['sessionHandle'] : '';
|
|
762
|
+
if (!handle) {
|
|
763
|
+
return { kind: 'err', error: 'Dispatch succeeded but no session handle returned' };
|
|
764
|
+
}
|
|
765
|
+
return { kind: 'ok', value: handle };
|
|
766
|
+
}
|
|
767
|
+
catch (e) {
|
|
768
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
769
|
+
const isConnRefused = msg.includes('ECONNREFUSED') || msg.includes('fetch failed');
|
|
770
|
+
if (isConnRefused) {
|
|
771
|
+
return { kind: 'err', error: `Could not connect to WorkTrain daemon on port ${port}. Ensure the daemon is running with: worktrain daemon` };
|
|
772
|
+
}
|
|
773
|
+
if (e instanceof Error && e.name === 'TimeoutError') {
|
|
774
|
+
return { kind: 'err', error: `Daemon request timed out after 30s` };
|
|
775
|
+
}
|
|
776
|
+
return { kind: 'err', error: `Dispatch request failed: ${msg}` };
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
awaitSessions: async (handles, timeoutMs) => {
|
|
780
|
+
const { executeWorktrainAwaitCommand } = await Promise.resolve().then(() => __importStar(require('./cli/commands/worktrain-await.js')));
|
|
781
|
+
let resolvedResult = null;
|
|
782
|
+
await executeWorktrainAwaitCommand({
|
|
783
|
+
fetch: (url) => globalThis.fetch(url),
|
|
784
|
+
readFile: (p) => fs_1.default.promises.readFile(p, 'utf-8'),
|
|
785
|
+
stdout: (line) => {
|
|
786
|
+
try {
|
|
787
|
+
resolvedResult = JSON.parse(line);
|
|
788
|
+
}
|
|
789
|
+
catch { }
|
|
790
|
+
},
|
|
791
|
+
stderr: (line) => process.stderr.write(line + '\n'),
|
|
792
|
+
homedir: os_1.default.homedir,
|
|
793
|
+
joinPath: path_1.default.join,
|
|
794
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
795
|
+
now: () => Date.now(),
|
|
796
|
+
}, {
|
|
797
|
+
sessions: [...handles].join(','),
|
|
798
|
+
mode: 'all',
|
|
799
|
+
timeout: `${Math.round(timeoutMs / 1000)}s`,
|
|
800
|
+
port,
|
|
801
|
+
});
|
|
802
|
+
if (resolvedResult === null) {
|
|
803
|
+
process.stderr.write(`[WARN coord:reason=await_failed] awaitSessions: could not get session results -- daemon may be unreachable or timed out. Returning all ${handles.length} session(s) as failed.\n`);
|
|
804
|
+
}
|
|
805
|
+
return resolvedResult ?? { results: [...handles].map((h) => ({
|
|
806
|
+
handle: h,
|
|
807
|
+
outcome: 'failed',
|
|
808
|
+
status: null,
|
|
809
|
+
durationMs: 0,
|
|
810
|
+
})), allSucceeded: false };
|
|
811
|
+
},
|
|
812
|
+
getAgentResult: async (sessionHandle) => {
|
|
813
|
+
try {
|
|
814
|
+
const sessionUrl = `http://127.0.0.1:${port}/api/v2/sessions/${encodeURIComponent(sessionHandle)}`;
|
|
815
|
+
const sessionRes = await globalThis.fetch(sessionUrl, { signal: AbortSignal.timeout(30000) });
|
|
816
|
+
if (!sessionRes.ok) {
|
|
817
|
+
process.stderr.write(`[WARN coord:reason=http_error status=${sessionRes.status} handle=${sessionHandle.slice(0, 16)}] getAgentResult: session fetch returned HTTP ${sessionRes.status}\n`);
|
|
818
|
+
return null;
|
|
819
|
+
}
|
|
820
|
+
const sessionBody = await sessionRes.json();
|
|
821
|
+
if (sessionBody['success'] !== true) {
|
|
822
|
+
process.stderr.write(`[WARN coord:reason=api_error handle=${sessionHandle.slice(0, 16)}] getAgentResult: session API returned success=false\n`);
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
const data = sessionBody['data'];
|
|
826
|
+
if (!data) {
|
|
827
|
+
process.stderr.write(`[WARN coord:reason=no_data handle=${sessionHandle.slice(0, 16)}] getAgentResult: session response missing data field\n`);
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
830
|
+
const runs = data['runs'];
|
|
831
|
+
if (!Array.isArray(runs) || runs.length === 0) {
|
|
832
|
+
process.stderr.write(`[WARN coord:reason=no_runs handle=${sessionHandle.slice(0, 16)}] getAgentResult: session has no runs\n`);
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
const firstRun = runs[0];
|
|
836
|
+
const tipNodeId = typeof firstRun['preferredTipNodeId'] === 'string'
|
|
837
|
+
? firstRun['preferredTipNodeId']
|
|
838
|
+
: null;
|
|
839
|
+
if (!tipNodeId) {
|
|
840
|
+
process.stderr.write(`[WARN coord:reason=no_tip_node handle=${sessionHandle.slice(0, 16)}] getAgentResult: session run has no preferredTipNodeId\n`);
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
const nodeUrl = `http://127.0.0.1:${port}/api/v2/sessions/${encodeURIComponent(sessionHandle)}/nodes/${encodeURIComponent(tipNodeId)}`;
|
|
844
|
+
const nodeRes = await globalThis.fetch(nodeUrl, { signal: AbortSignal.timeout(30000) });
|
|
845
|
+
if (!nodeRes.ok) {
|
|
846
|
+
process.stderr.write(`[WARN coord:reason=node_http_error status=${nodeRes.status} handle=${sessionHandle.slice(0, 16)} node=${tipNodeId.slice(0, 16)}] getAgentResult: node fetch returned HTTP ${nodeRes.status}\n`);
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
const nodeBody = await nodeRes.json();
|
|
850
|
+
if (nodeBody['success'] !== true) {
|
|
851
|
+
process.stderr.write(`[WARN coord:reason=node_api_error handle=${sessionHandle.slice(0, 16)} node=${tipNodeId.slice(0, 16)}] getAgentResult: node API returned success=false\n`);
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
const nodeData = nodeBody['data'];
|
|
855
|
+
if (!nodeData) {
|
|
856
|
+
process.stderr.write(`[WARN coord:reason=no_node_data handle=${sessionHandle.slice(0, 16)} node=${tipNodeId.slice(0, 16)}] getAgentResult: node response missing data field\n`);
|
|
857
|
+
return null;
|
|
858
|
+
}
|
|
859
|
+
const recap = typeof nodeData['recapMarkdown'] === 'string' ? nodeData['recapMarkdown'] : null;
|
|
860
|
+
if (recap === null) {
|
|
861
|
+
process.stderr.write(`[WARN coord:reason=no_recap handle=${sessionHandle.slice(0, 16)} node=${tipNodeId.slice(0, 16)}] getAgentResult: node has no recapMarkdown\n`);
|
|
862
|
+
}
|
|
863
|
+
return recap;
|
|
864
|
+
}
|
|
865
|
+
catch (e) {
|
|
866
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
867
|
+
process.stderr.write(`[WARN coord:reason=exception handle=${sessionHandle.slice(0, 16)}] getAgentResult: ${msg}\n`);
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
},
|
|
871
|
+
listOpenPRs: async (workspace) => {
|
|
872
|
+
try {
|
|
873
|
+
const { stdout } = await execFilePromise('gh', ['pr', 'list', '--json', 'number,title,headRefName'], {
|
|
874
|
+
cwd: workspace,
|
|
875
|
+
timeout: 30000,
|
|
876
|
+
});
|
|
877
|
+
const parsed = JSON.parse(stdout);
|
|
878
|
+
return parsed.map((p) => ({ number: p.number, title: p.title, headRef: p.headRefName }));
|
|
879
|
+
}
|
|
880
|
+
catch {
|
|
881
|
+
return [];
|
|
882
|
+
}
|
|
883
|
+
},
|
|
884
|
+
mergePR: async (prNumber, workspace) => {
|
|
885
|
+
try {
|
|
886
|
+
await execFilePromise('gh', ['pr', 'merge', String(prNumber), '--squash', '--auto'], {
|
|
887
|
+
cwd: workspace,
|
|
888
|
+
timeout: 60000,
|
|
889
|
+
});
|
|
890
|
+
return { kind: 'ok', value: undefined };
|
|
891
|
+
}
|
|
892
|
+
catch (e) {
|
|
893
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
894
|
+
return { kind: 'err', error: msg };
|
|
895
|
+
}
|
|
896
|
+
},
|
|
897
|
+
writeFile: async (filePath, content) => {
|
|
898
|
+
await fs_1.default.promises.writeFile(filePath, content, 'utf-8');
|
|
899
|
+
},
|
|
900
|
+
stderr: (line) => process.stderr.write(line + '\n'),
|
|
901
|
+
now: () => Date.now(),
|
|
902
|
+
port,
|
|
903
|
+
};
|
|
904
|
+
const result = await runPrReviewCoordinator(deps, {
|
|
905
|
+
workspace: options.workspace,
|
|
906
|
+
prs: options.pr.length > 0 ? options.pr : undefined,
|
|
907
|
+
dryRun: options.dryRun ?? false,
|
|
908
|
+
port: options.port,
|
|
909
|
+
});
|
|
910
|
+
process.exit(result.hasErrors ? 1 : 0);
|
|
660
911
|
});
|
|
661
912
|
program.parse();
|