@exaudeus/workrail 3.46.0 → 3.48.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 +3 -1
- package/dist/cli/commands/worktrain-trigger-test.d.ts +21 -0
- package/dist/cli/commands/worktrain-trigger-test.js +123 -0
- package/dist/cli-worktrain.js +65 -0
- package/dist/console-ui/assets/{index-BQFhoMcY.js → index-CecBgrR7.js} +1 -1
- package/dist/console-ui/index.html +1 -1
- package/dist/coordinators/modes/implement-shared.d.ts +2 -1
- package/dist/coordinators/modes/implement-shared.js +7 -3
- package/dist/manifest.json +44 -36
- package/dist/mcp/output-schemas.d.ts +2 -2
- package/dist/trigger/adapters/github-queue-poller.js +10 -7
- package/dist/trigger/github-queue-config.d.ts +1 -0
- package/dist/trigger/github-queue-config.js +9 -0
- package/dist/trigger/polling-scheduler.js +8 -1
- package/dist/trigger/trigger-listener.js +296 -1
- package/dist/trigger/trigger-router.d.ts +4 -2
- package/dist/trigger/trigger-router.js +19 -3
- package/dist/trigger/trigger-store.js +10 -0
- package/dist/trigger/types.d.ts +2 -0
- package/dist/v2/durable-core/schemas/artifacts/review-verdict.d.ts +5 -0
- package/dist/v2/durable-core/schemas/artifacts/review-verdict.js +12 -0
- package/docs/design/connect-adaptive-dispatch-candidates.md +153 -0
- package/docs/design/connect-adaptive-dispatch-design-review.md +88 -0
- package/docs/design/connect-adaptive-dispatch-implementation-plan.md +209 -0
- package/docs/design/queue-label-support-candidates.md +83 -0
- package/docs/design/queue-label-support-design-review.md +62 -0
- package/docs/design/queue-label-support-implementation-plan.md +158 -0
- package/docs/ideas/backlog.md +147 -0
- package/package.json +1 -1
- package/workflows/mr-review-workflow.agentic.v2.json +1 -1
|
@@ -14,3 +14,4 @@ export { executeWorktrainAwaitCommand, parseDurationMs, type WorktrainAwaitComma
|
|
|
14
14
|
export { executeWorktrainDaemonCommand, type WorktrainDaemonCommandDeps, type WorktrainDaemonCommandOpts, } from './worktrain-daemon.js';
|
|
15
15
|
export { executeWorktrainOverviewCommand, buildConsoleServiceFromDataDir, type WorktrainOverviewCommandDeps, type WorktrainOverviewCommandOpts, type StatusDataPacket, type StatusSession, } from './worktrain-overview.js';
|
|
16
16
|
export { executeWorktrainPipelineCommand, type WorktrainPipelineCommandDeps, type WorktrainPipelineCommandOpts, } from './worktrain-pipeline.js';
|
|
17
|
+
export { executeWorktrainTriggerTestCommand, type WorktrainTriggerTestDeps, type WorktrainTriggerTestOpts, } from './worktrain-trigger-test.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.executeWorktrainPipelineCommand = 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;
|
|
3
|
+
exports.executeWorktrainTriggerTestCommand = exports.executeWorktrainPipelineCommand = 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; } });
|
|
@@ -40,3 +40,5 @@ Object.defineProperty(exports, "executeWorktrainOverviewCommand", { enumerable:
|
|
|
40
40
|
Object.defineProperty(exports, "buildConsoleServiceFromDataDir", { enumerable: true, get: function () { return worktrain_overview_js_1.buildConsoleServiceFromDataDir; } });
|
|
41
41
|
var worktrain_pipeline_js_1 = require("./worktrain-pipeline.js");
|
|
42
42
|
Object.defineProperty(exports, "executeWorktrainPipelineCommand", { enumerable: true, get: function () { return worktrain_pipeline_js_1.executeWorktrainPipelineCommand; } });
|
|
43
|
+
var worktrain_trigger_test_js_1 = require("./worktrain-trigger-test.js");
|
|
44
|
+
Object.defineProperty(exports, "executeWorktrainTriggerTestCommand", { enumerable: true, get: function () { return worktrain_trigger_test_js_1.executeWorktrainTriggerTestCommand; } });
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CliResult } from '../types/cli-result.js';
|
|
2
|
+
import type { TriggerDefinition } from '../../trigger/types.js';
|
|
3
|
+
import type { GitHubQueuePollingSource } from '../../trigger/types.js';
|
|
4
|
+
import type { GitHubQueueConfig } from '../../trigger/github-queue-config.js';
|
|
5
|
+
import type { GitHubQueueIssue } from '../../trigger/adapters/github-queue-poller.js';
|
|
6
|
+
import type { Result } from '../../runtime/result.js';
|
|
7
|
+
export interface WorktrainTriggerTestDeps {
|
|
8
|
+
readonly loadTriggerConfig: () => Promise<Result<Map<string, TriggerDefinition>, string>>;
|
|
9
|
+
readonly loadQueueConfig: () => Promise<Result<GitHubQueueConfig | null, string>>;
|
|
10
|
+
readonly pollGitHubQueueIssues: (source: GitHubQueuePollingSource, config: GitHubQueueConfig) => Promise<Result<GitHubQueueIssue[], string>>;
|
|
11
|
+
readonly countActiveSessions: () => Promise<number>;
|
|
12
|
+
readonly checkIdempotency: (issueNumber: number) => Promise<'active' | 'clear'>;
|
|
13
|
+
readonly inferMaturity: (issue: GitHubQueueIssue) => 'idea' | 'specced' | 'ready';
|
|
14
|
+
readonly print: (line: string) => void;
|
|
15
|
+
readonly stderr: (line: string) => void;
|
|
16
|
+
}
|
|
17
|
+
export interface WorktrainTriggerTestOpts {
|
|
18
|
+
readonly triggerId: string;
|
|
19
|
+
readonly port?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function executeWorktrainTriggerTestCommand(deps: WorktrainTriggerTestDeps, opts: WorktrainTriggerTestOpts): Promise<CliResult>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.executeWorktrainTriggerTestCommand = executeWorktrainTriggerTestCommand;
|
|
4
|
+
const cli_result_js_1 = require("../types/cli-result.js");
|
|
5
|
+
async function executeWorktrainTriggerTestCommand(deps, opts) {
|
|
6
|
+
const triggerId = opts.triggerId.trim();
|
|
7
|
+
if (!triggerId) {
|
|
8
|
+
deps.stderr('[DryRun] Error: triggerId must not be empty.');
|
|
9
|
+
return (0, cli_result_js_1.failure)('triggerId must not be empty.');
|
|
10
|
+
}
|
|
11
|
+
const triggerIndexResult = await deps.loadTriggerConfig();
|
|
12
|
+
if (triggerIndexResult.kind === 'err') {
|
|
13
|
+
deps.stderr(`[DryRun] Error: Failed to load triggers.yml: ${triggerIndexResult.error}`);
|
|
14
|
+
return (0, cli_result_js_1.failure)(`Failed to load triggers.yml: ${triggerIndexResult.error}`);
|
|
15
|
+
}
|
|
16
|
+
const triggerIndex = triggerIndexResult.value;
|
|
17
|
+
const trigger = triggerIndex.get(triggerId);
|
|
18
|
+
if (!trigger) {
|
|
19
|
+
deps.stderr(`[DryRun] Error: Trigger '${triggerId}' not found in triggers.yml.`);
|
|
20
|
+
return (0, cli_result_js_1.failure)(`Trigger '${triggerId}' not found in triggers.yml.`);
|
|
21
|
+
}
|
|
22
|
+
if (trigger.provider !== 'github_queue_poll') {
|
|
23
|
+
deps.stderr(`[DryRun] Error: Trigger '${triggerId}' is not a queue poll trigger -- ` +
|
|
24
|
+
`only github_queue_poll triggers can be tested with this command`);
|
|
25
|
+
return (0, cli_result_js_1.failure)(`Trigger '${triggerId}' is not a queue poll trigger -- ` +
|
|
26
|
+
`only github_queue_poll triggers can be tested with this command`);
|
|
27
|
+
}
|
|
28
|
+
const pollingSource = trigger.pollingSource;
|
|
29
|
+
const queueConfigResult = await deps.loadQueueConfig();
|
|
30
|
+
if (queueConfigResult.kind === 'err') {
|
|
31
|
+
deps.stderr(`[DryRun] Error: Failed to load queue config: ${queueConfigResult.error}`);
|
|
32
|
+
return (0, cli_result_js_1.failure)(`Failed to load queue config: ${queueConfigResult.error}`);
|
|
33
|
+
}
|
|
34
|
+
const queueConfig = queueConfigResult.value;
|
|
35
|
+
if (queueConfig === null) {
|
|
36
|
+
deps.stderr('[DryRun] Error: No queue config found in ~/.workrail/config.json (missing "queue" key).');
|
|
37
|
+
return (0, cli_result_js_1.failure)('No queue config found in ~/.workrail/config.json (missing "queue" key).');
|
|
38
|
+
}
|
|
39
|
+
const activeSessions = await deps.countActiveSessions();
|
|
40
|
+
deps.print(`[DryRun] Trigger: ${triggerId} (${trigger.provider})`);
|
|
41
|
+
deps.print(`[DryRun] Queue config: type=${queueConfig.type}${queueConfig.queueLabel ? ` queueLabel=${queueConfig.queueLabel}` : ''}${queueConfig.user ? ` user=${queueConfig.user}` : ''} repo=${queueConfig.repo}`);
|
|
42
|
+
deps.print(`[DryRun] Active sessions: ${activeSessions} / maxTotalConcurrentSessions: ${queueConfig.maxTotalConcurrentSessions}`);
|
|
43
|
+
deps.print('');
|
|
44
|
+
if (activeSessions >= queueConfig.maxTotalConcurrentSessions) {
|
|
45
|
+
deps.print(`[DryRun] Concurrency cap reached: active sessions (${activeSessions}) >= ` +
|
|
46
|
+
`maxTotalConcurrentSessions (${queueConfig.maxTotalConcurrentSessions})`);
|
|
47
|
+
deps.print('[DryRun] Summary: 0 would dispatch, 0 would skip (concurrency cap)');
|
|
48
|
+
return (0, cli_result_js_1.failure)('');
|
|
49
|
+
}
|
|
50
|
+
const issuesResult = await deps.pollGitHubQueueIssues(pollingSource, queueConfig);
|
|
51
|
+
if (issuesResult.kind === 'err') {
|
|
52
|
+
deps.stderr(`[DryRun] Error: Failed to fetch issues: ${issuesResult.error}`);
|
|
53
|
+
return (0, cli_result_js_1.failure)(`Failed to fetch issues: ${issuesResult.error}`);
|
|
54
|
+
}
|
|
55
|
+
const issues = issuesResult.value;
|
|
56
|
+
const decisions = [];
|
|
57
|
+
for (const issue of issues) {
|
|
58
|
+
const issueLabels = issue.labels.map((l) => l.name);
|
|
59
|
+
const excludedLabel = queueConfig.excludeLabels.find((el) => issueLabels.includes(el));
|
|
60
|
+
if (excludedLabel) {
|
|
61
|
+
decisions.push({ kind: 'skip', issue, reason: `excluded_label: ${excludedLabel}` });
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (issueLabels.includes('worktrain:in-progress')) {
|
|
65
|
+
decisions.push({ kind: 'skip', issue, reason: 'active_session_or_in_progress' });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (/sess_[a-z0-9]+/.test(issue.body)) {
|
|
69
|
+
decisions.push({ kind: 'skip', issue, reason: 'active_session_or_in_progress' });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const idempotencyStatus = await deps.checkIdempotency(issue.number);
|
|
73
|
+
if (idempotencyStatus === 'active') {
|
|
74
|
+
decisions.push({ kind: 'skip', issue, reason: 'active_session' });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const maturity = deps.inferMaturity(issue);
|
|
78
|
+
if (maturity === 'idea') {
|
|
79
|
+
decisions.push({ kind: 'skip', issue, reason: 'maturity=idea (no spec, no checklist)' });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
decisions.push({
|
|
83
|
+
kind: 'dispatch',
|
|
84
|
+
issue,
|
|
85
|
+
maturity,
|
|
86
|
+
upstreamSpecUrl: extractUpstreamSpecUrl(issue.body),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
for (const decision of decisions) {
|
|
90
|
+
if (decision.kind === 'dispatch') {
|
|
91
|
+
deps.print(`[DryRun] Issue #${decision.issue.number} "${decision.issue.title}" -- WOULD DISPATCH`);
|
|
92
|
+
deps.print(` maturity: ${decision.maturity} (${describeMaturity(decision.maturity)})`);
|
|
93
|
+
deps.print(` upstreamSpecUrl: ${decision.upstreamSpecUrl ?? '(none)'}`);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
deps.print(`[DryRun] Issue #${decision.issue.number} "${decision.issue.title}" -- WOULD SKIP`);
|
|
97
|
+
deps.print(` reason: ${decision.reason}`);
|
|
98
|
+
}
|
|
99
|
+
deps.print('');
|
|
100
|
+
}
|
|
101
|
+
const dispatchCount = decisions.filter((d) => d.kind === 'dispatch').length;
|
|
102
|
+
const skipCount = decisions.filter((d) => d.kind === 'skip').length;
|
|
103
|
+
deps.print(`[DryRun] Summary: ${dispatchCount} would dispatch, ${skipCount} would skip`);
|
|
104
|
+
if (dispatchCount === 0) {
|
|
105
|
+
return (0, cli_result_js_1.failure)('');
|
|
106
|
+
}
|
|
107
|
+
return (0, cli_result_js_1.success)();
|
|
108
|
+
}
|
|
109
|
+
function extractUpstreamSpecUrl(body) {
|
|
110
|
+
const specLineMatch = /upstream_spec:\s*(https?:\/\/\S+)/i.exec(body);
|
|
111
|
+
if (specLineMatch?.[1])
|
|
112
|
+
return specLineMatch[1];
|
|
113
|
+
const firstPara = body.split(/\n\s*\n/)[0] ?? '';
|
|
114
|
+
const urlMatch = /(https?:\/\/\S+)/.exec(firstPara);
|
|
115
|
+
return urlMatch?.[1];
|
|
116
|
+
}
|
|
117
|
+
function describeMaturity(maturity) {
|
|
118
|
+
switch (maturity) {
|
|
119
|
+
case 'ready': return 'has acceptance criteria';
|
|
120
|
+
case 'specced': return 'has checklist or acceptance criteria heading';
|
|
121
|
+
case 'idea': return 'no spec, no checklist';
|
|
122
|
+
}
|
|
123
|
+
}
|
package/dist/cli-worktrain.js
CHANGED
|
@@ -970,4 +970,69 @@ runCommand
|
|
|
970
970
|
});
|
|
971
971
|
process.exit(result.hasErrors ? 1 : 0);
|
|
972
972
|
});
|
|
973
|
+
const triggerCommand = program
|
|
974
|
+
.command('trigger')
|
|
975
|
+
.description('Trigger management commands');
|
|
976
|
+
triggerCommand
|
|
977
|
+
.command('test <triggerId>')
|
|
978
|
+
.description('Dry-run the queue picker for a trigger -- shows what would dispatch without dispatching')
|
|
979
|
+
.option('-p, --port <n>', 'Console server port for active session count', parseInt)
|
|
980
|
+
.action(async (triggerId, options) => {
|
|
981
|
+
const { loadTriggerConfigFromFile, buildTriggerIndex } = await Promise.resolve().then(() => __importStar(require('./trigger/trigger-store.js')));
|
|
982
|
+
const { loadQueueConfig } = await Promise.resolve().then(() => __importStar(require('./trigger/github-queue-config.js')));
|
|
983
|
+
const { pollGitHubQueueIssues, checkIdempotency, inferMaturity } = await Promise.resolve().then(() => __importStar(require('./trigger/adapters/github-queue-poller.js')));
|
|
984
|
+
const cwd = process.cwd();
|
|
985
|
+
const result = await (0, index_js_2.executeWorktrainTriggerTestCommand)({
|
|
986
|
+
loadTriggerConfig: async () => {
|
|
987
|
+
const configResult = await loadTriggerConfigFromFile(cwd, process.env);
|
|
988
|
+
if (configResult.kind === 'err') {
|
|
989
|
+
const e = configResult.error;
|
|
990
|
+
const msg = e.kind === 'file_not_found'
|
|
991
|
+
? `triggers.yml not found at ${e.filePath}`
|
|
992
|
+
: e.kind === 'io_error'
|
|
993
|
+
? `IO error reading triggers.yml: ${e.message}`
|
|
994
|
+
: `Failed to parse triggers.yml: ${JSON.stringify(e)}`;
|
|
995
|
+
return { kind: 'err', error: msg };
|
|
996
|
+
}
|
|
997
|
+
const indexResult = buildTriggerIndex(configResult.value);
|
|
998
|
+
if (indexResult.kind === 'err') {
|
|
999
|
+
const idxErr = indexResult.error;
|
|
1000
|
+
const triggerId2 = 'triggerId' in idxErr ? idxErr.triggerId : '(unknown)';
|
|
1001
|
+
return { kind: 'err', error: `Duplicate trigger ID: ${triggerId2}` };
|
|
1002
|
+
}
|
|
1003
|
+
return { kind: 'ok', value: indexResult.value };
|
|
1004
|
+
},
|
|
1005
|
+
loadQueueConfig: async () => {
|
|
1006
|
+
return loadQueueConfig();
|
|
1007
|
+
},
|
|
1008
|
+
pollGitHubQueueIssues: async (source, config) => {
|
|
1009
|
+
const result2 = await pollGitHubQueueIssues(source, config);
|
|
1010
|
+
if (result2.kind === 'err') {
|
|
1011
|
+
const e = result2.error;
|
|
1012
|
+
return { kind: 'err', error: `${e.kind}: ${e.message}` };
|
|
1013
|
+
}
|
|
1014
|
+
return result2;
|
|
1015
|
+
},
|
|
1016
|
+
countActiveSessions: async () => {
|
|
1017
|
+
const sessionsDir = path_1.default.join(os_1.default.homedir(), '.workrail', 'daemon-sessions');
|
|
1018
|
+
try {
|
|
1019
|
+
const files = await fs_1.default.promises.readdir(sessionsDir);
|
|
1020
|
+
return files.filter((f) => f.endsWith('.json')).length;
|
|
1021
|
+
}
|
|
1022
|
+
catch {
|
|
1023
|
+
return 0;
|
|
1024
|
+
}
|
|
1025
|
+
},
|
|
1026
|
+
checkIdempotency: async (issueNumber) => {
|
|
1027
|
+
const sessionsDir = path_1.default.join(os_1.default.homedir(), '.workrail', 'daemon-sessions');
|
|
1028
|
+
return checkIdempotency(issueNumber, sessionsDir);
|
|
1029
|
+
},
|
|
1030
|
+
inferMaturity: (issue) => inferMaturity(issue.body),
|
|
1031
|
+
print: (line) => process.stdout.write(line + '\n'),
|
|
1032
|
+
stderr: (line) => process.stderr.write(line + '\n'),
|
|
1033
|
+
}, { triggerId, port: options.port });
|
|
1034
|
+
if (result.kind === 'failure') {
|
|
1035
|
+
process.exit(1);
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
973
1038
|
program.parse();
|