@exaudeus/workrail 3.38.0 → 3.40.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-worktrain.js +231 -0
- package/dist/console-ui/assets/{index-BtOJj6Xy.js → index-CXWCAonr.js} +1 -1
- package/dist/console-ui/index.html +1 -1
- package/dist/coordinators/pr-review.d.ts +62 -0
- package/dist/coordinators/pr-review.js +575 -0
- package/dist/daemon/workflow-runner.d.ts +3 -2
- package/dist/daemon/workflow-runner.js +6 -3
- package/dist/manifest.json +58 -34
- package/dist/mcp/output-schemas.d.ts +10 -10
- package/dist/mcp/tools.d.ts +12 -12
- package/dist/trigger/trigger-router.js +9 -2
- package/dist/types/workflow-source.d.ts +0 -1
- package/dist/types/workflow-source.js +3 -6
- package/dist/types/workflow.d.ts +1 -1
- package/dist/types/workflow.js +1 -2
- package/dist/v2/durable-core/domain/artifact-contract-validator.js +66 -0
- package/dist/v2/durable-core/schemas/artifacts/coordinator-signal.d.ts +25 -0
- package/dist/v2/durable-core/schemas/artifacts/coordinator-signal.js +31 -0
- package/dist/v2/durable-core/schemas/artifacts/index.d.ts +3 -1
- package/dist/v2/durable-core/schemas/artifacts/index.js +14 -1
- package/dist/v2/durable-core/schemas/artifacts/review-verdict.d.ts +41 -0
- package/dist/v2/durable-core/schemas/artifacts/review-verdict.js +30 -0
- package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +236 -236
- package/dist/v2/durable-core/schemas/session/events.d.ts +50 -50
- package/dist/v2/durable-core/schemas/session/gaps.d.ts +2 -2
- package/dist/v2/durable-core/schemas/session/manifest.d.ts +4 -4
- package/dist/v2/durable-core/schemas/session/outputs.d.ts +8 -8
- package/dist/v2/usecases/console-routes.js +178 -0
- package/docs/design/coordinator-artifact-protocol-design-candidates.md +155 -0
- package/docs/design/coordinator-artifact-protocol-design-review.md +103 -0
- package/docs/design/coordinator-artifact-protocol-implementation-plan.md +259 -0
- 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/docs/ideas/backlog.md +158 -100
- package/package.json +1 -1
- package/workflows/mr-review-workflow.agentic.v2.json +5 -1
package/dist/cli-worktrain.js
CHANGED
|
@@ -702,4 +702,235 @@ function runHealthSummary(sessionId, raw) {
|
|
|
702
702
|
}
|
|
703
703
|
process.stdout.write('\n');
|
|
704
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
|
+
const emptyResult = { recapMarkdown: null, artifacts: [] };
|
|
814
|
+
try {
|
|
815
|
+
const sessionUrl = `http://127.0.0.1:${port}/api/v2/sessions/${encodeURIComponent(sessionHandle)}`;
|
|
816
|
+
const sessionRes = await globalThis.fetch(sessionUrl, { signal: AbortSignal.timeout(30000) });
|
|
817
|
+
if (!sessionRes.ok) {
|
|
818
|
+
process.stderr.write(`[WARN coord:reason=http_error status=${sessionRes.status} handle=${sessionHandle.slice(0, 16)}] getAgentResult: session fetch returned HTTP ${sessionRes.status}\n`);
|
|
819
|
+
return emptyResult;
|
|
820
|
+
}
|
|
821
|
+
const sessionBody = await sessionRes.json();
|
|
822
|
+
if (sessionBody['success'] !== true) {
|
|
823
|
+
process.stderr.write(`[WARN coord:reason=api_error handle=${sessionHandle.slice(0, 16)}] getAgentResult: session API returned success=false\n`);
|
|
824
|
+
return emptyResult;
|
|
825
|
+
}
|
|
826
|
+
const data = sessionBody['data'];
|
|
827
|
+
if (!data) {
|
|
828
|
+
process.stderr.write(`[WARN coord:reason=no_data handle=${sessionHandle.slice(0, 16)}] getAgentResult: session response missing data field\n`);
|
|
829
|
+
return emptyResult;
|
|
830
|
+
}
|
|
831
|
+
const runs = data['runs'];
|
|
832
|
+
if (!Array.isArray(runs) || runs.length === 0) {
|
|
833
|
+
process.stderr.write(`[WARN coord:reason=no_runs handle=${sessionHandle.slice(0, 16)}] getAgentResult: session has no runs\n`);
|
|
834
|
+
return emptyResult;
|
|
835
|
+
}
|
|
836
|
+
const firstRun = runs[0];
|
|
837
|
+
const tipNodeId = typeof firstRun['preferredTipNodeId'] === 'string'
|
|
838
|
+
? firstRun['preferredTipNodeId']
|
|
839
|
+
: null;
|
|
840
|
+
if (!tipNodeId) {
|
|
841
|
+
process.stderr.write(`[WARN coord:reason=no_tip_node handle=${sessionHandle.slice(0, 16)}] getAgentResult: session run has no preferredTipNodeId\n`);
|
|
842
|
+
return emptyResult;
|
|
843
|
+
}
|
|
844
|
+
const allNodes = Array.isArray(firstRun['nodes'])
|
|
845
|
+
? firstRun['nodes']
|
|
846
|
+
: [];
|
|
847
|
+
const allNodeIds = allNodes
|
|
848
|
+
.map((n) => (typeof n['nodeId'] === 'string' ? n['nodeId'] : null))
|
|
849
|
+
.filter((id) => id !== null);
|
|
850
|
+
const nodeIdsToFetch = allNodeIds.length > 0
|
|
851
|
+
? allNodeIds
|
|
852
|
+
: [tipNodeId];
|
|
853
|
+
const baseNodeUrl = `http://127.0.0.1:${port}/api/v2/sessions/${encodeURIComponent(sessionHandle)}/nodes/`;
|
|
854
|
+
let recap = null;
|
|
855
|
+
const collectedArtifacts = [];
|
|
856
|
+
for (const nodeId of nodeIdsToFetch) {
|
|
857
|
+
try {
|
|
858
|
+
const nodeRes = await globalThis.fetch(baseNodeUrl + encodeURIComponent(nodeId), { signal: AbortSignal.timeout(30000) });
|
|
859
|
+
if (!nodeRes.ok) {
|
|
860
|
+
process.stderr.write(`[WARN coord:reason=node_http_error status=${nodeRes.status} handle=${sessionHandle.slice(0, 16)} node=${nodeId.slice(0, 16)}] getAgentResult: node fetch returned HTTP ${nodeRes.status}\n`);
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
const nodeBody = await nodeRes.json();
|
|
864
|
+
if (nodeBody['success'] !== true) {
|
|
865
|
+
process.stderr.write(`[WARN coord:reason=node_api_error handle=${sessionHandle.slice(0, 16)} node=${nodeId.slice(0, 16)}] getAgentResult: node API returned success=false\n`);
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
const nodeData = nodeBody['data'];
|
|
869
|
+
if (!nodeData)
|
|
870
|
+
continue;
|
|
871
|
+
if (nodeId === tipNodeId) {
|
|
872
|
+
recap = typeof nodeData['recapMarkdown'] === 'string' ? nodeData['recapMarkdown'] : null;
|
|
873
|
+
if (recap === null) {
|
|
874
|
+
process.stderr.write(`[WARN coord:reason=no_recap handle=${sessionHandle.slice(0, 16)} node=${nodeId.slice(0, 16)}] getAgentResult: tip node has no recapMarkdown\n`);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
const nodeArtifacts = nodeData['artifacts'];
|
|
878
|
+
if (Array.isArray(nodeArtifacts) && nodeArtifacts.length > 0) {
|
|
879
|
+
collectedArtifacts.push(...nodeArtifacts);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
catch (nodeErr) {
|
|
883
|
+
const msg = nodeErr instanceof Error ? nodeErr.message : String(nodeErr);
|
|
884
|
+
process.stderr.write(`[WARN coord:reason=node_exception handle=${sessionHandle.slice(0, 16)} node=${nodeId.slice(0, 16)}] getAgentResult: ${msg}\n`);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
return { recapMarkdown: recap, artifacts: collectedArtifacts };
|
|
888
|
+
}
|
|
889
|
+
catch (e) {
|
|
890
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
891
|
+
process.stderr.write(`[WARN coord:reason=exception handle=${sessionHandle.slice(0, 16)}] getAgentResult: ${msg}\n`);
|
|
892
|
+
return emptyResult;
|
|
893
|
+
}
|
|
894
|
+
},
|
|
895
|
+
listOpenPRs: async (workspace) => {
|
|
896
|
+
try {
|
|
897
|
+
const { stdout } = await execFilePromise('gh', ['pr', 'list', '--json', 'number,title,headRefName'], {
|
|
898
|
+
cwd: workspace,
|
|
899
|
+
timeout: 30000,
|
|
900
|
+
});
|
|
901
|
+
const parsed = JSON.parse(stdout);
|
|
902
|
+
return parsed.map((p) => ({ number: p.number, title: p.title, headRef: p.headRefName }));
|
|
903
|
+
}
|
|
904
|
+
catch {
|
|
905
|
+
return [];
|
|
906
|
+
}
|
|
907
|
+
},
|
|
908
|
+
mergePR: async (prNumber, workspace) => {
|
|
909
|
+
try {
|
|
910
|
+
await execFilePromise('gh', ['pr', 'merge', String(prNumber), '--squash', '--auto'], {
|
|
911
|
+
cwd: workspace,
|
|
912
|
+
timeout: 60000,
|
|
913
|
+
});
|
|
914
|
+
return { kind: 'ok', value: undefined };
|
|
915
|
+
}
|
|
916
|
+
catch (e) {
|
|
917
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
918
|
+
return { kind: 'err', error: msg };
|
|
919
|
+
}
|
|
920
|
+
},
|
|
921
|
+
writeFile: async (filePath, content) => {
|
|
922
|
+
await fs_1.default.promises.writeFile(filePath, content, 'utf-8');
|
|
923
|
+
},
|
|
924
|
+
stderr: (line) => process.stderr.write(line + '\n'),
|
|
925
|
+
now: () => Date.now(),
|
|
926
|
+
port,
|
|
927
|
+
};
|
|
928
|
+
const result = await runPrReviewCoordinator(deps, {
|
|
929
|
+
workspace: options.workspace,
|
|
930
|
+
prs: options.pr.length > 0 ? options.pr : undefined,
|
|
931
|
+
dryRun: options.dryRun ?? false,
|
|
932
|
+
port: options.port,
|
|
933
|
+
});
|
|
934
|
+
process.exit(result.hasErrors ? 1 : 0);
|
|
935
|
+
});
|
|
705
936
|
program.parse();
|