@exaudeus/workrail 3.17.0 → 3.18.1
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/README.md +13 -0
- package/dist/application/services/validation-engine.js +7 -11
- package/dist/application/services/workflow-compiler.js +9 -11
- package/dist/console/assets/index-DMaX2-CW.js +28 -0
- package/dist/console/assets/index-ibLhWBmX.css +1 -0
- package/dist/console/index.html +2 -2
- package/dist/infrastructure/storage/workflow-resolution.js +6 -6
- package/dist/manifest.json +55 -55
- package/dist/mcp/handlers/v2-advance-core/assessment-consequences.d.ts +1 -1
- package/dist/mcp/handlers/v2-advance-core/assessment-consequences.js +14 -11
- package/dist/mcp/handlers/v2-advance-core/assessment-validation.d.ts +5 -3
- package/dist/mcp/handlers/v2-advance-core/assessment-validation.js +109 -87
- package/dist/mcp/handlers/v2-advance-core/input-validation.d.ts +0 -4
- package/dist/mcp/handlers/v2-advance-core/input-validation.js +1 -3
- package/dist/mcp/handlers/v2-advance-core/outcome-blocked.js +8 -3
- package/dist/mcp/handlers/v2-advance-core/outcome-success.js +8 -3
- package/dist/mcp/handlers/v2-execution/replay.js +4 -4
- package/dist/mcp/output-schemas.d.ts +12 -12
- package/dist/mcp/output-schemas.js +10 -11
- package/dist/mcp-server.js +0 -0
- package/dist/types/workflow-source.js +1 -1
- package/dist/v2/durable-core/domain/observation-builder.d.ts +0 -3
- package/dist/v2/durable-core/domain/observation-builder.js +1 -3
- package/dist/v2/durable-core/domain/prompt-renderer.js +9 -1
- package/dist/v2/infra/local/session-summary-provider/index.js +1 -2
- package/dist/v2/projections/resume-ranking.d.ts +0 -1
- package/dist/v2/usecases/console-routes.js +65 -17
- package/dist/v2/usecases/console-service.js +4 -14
- package/dist/v2/usecases/console-types.d.ts +15 -1
- package/dist/v2/usecases/worktree-service.d.ts +1 -0
- package/dist/v2/usecases/worktree-service.js +143 -15
- package/package.json +3 -2
- package/spec/authoring-spec.json +3 -3
- package/spec/workflow.schema.json +1 -2
- package/workflows/coding-task-workflow-agentic.lean.v2.json +132 -1
- package/workflows/mr-review-workflow.agentic.v2.json +24 -10
- package/workflows/workflow-for-workflows.json +558 -448
- package/dist/console/assets/index-BZNM03t1.css +0 -1
- package/dist/console/assets/index-BwJelCXK.js +0 -28
|
@@ -8,6 +8,7 @@ const express_1 = __importDefault(require("express"));
|
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
10
|
const worktree_service_js_1 = require("./worktree-service.js");
|
|
11
|
+
const workflow_js_1 = require("../../types/workflow.js");
|
|
11
12
|
const dev_mode_js_1 = require("../../mcp/dev-mode.js");
|
|
12
13
|
function watchSessionsDir(sessionsDir, onChanged) {
|
|
13
14
|
try {
|
|
@@ -71,6 +72,22 @@ function mountConsoleRoutes(app, consoleService, workflowService, timingRingBuff
|
|
|
71
72
|
}, 200);
|
|
72
73
|
}
|
|
73
74
|
const stopWatcher = watchSessionsDir(consoleService.getSessionsDir(), broadcastChange);
|
|
75
|
+
let enrichmentBroadcastTimer = null;
|
|
76
|
+
(0, worktree_service_js_1.setEnrichmentCompleteCallback)(() => {
|
|
77
|
+
if (enrichmentBroadcastTimer !== null)
|
|
78
|
+
clearTimeout(enrichmentBroadcastTimer);
|
|
79
|
+
enrichmentBroadcastTimer = setTimeout(() => {
|
|
80
|
+
enrichmentBroadcastTimer = null;
|
|
81
|
+
for (const client of sseClients) {
|
|
82
|
+
try {
|
|
83
|
+
client.write('data: {"type":"worktrees-updated"}\n\n');
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
sseClients.delete(client);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}, 2000);
|
|
90
|
+
});
|
|
74
91
|
app.get('/api/v2/workspace/events', (req, res) => {
|
|
75
92
|
res.setHeader('Content-Type', 'text/event-stream');
|
|
76
93
|
res.setHeader('Cache-Control', 'no-cache');
|
|
@@ -97,35 +114,66 @@ function mountConsoleRoutes(app, consoleService, workflowService, timingRingBuff
|
|
|
97
114
|
result.match((data) => res.json({ success: true, data }), (error) => res.status(500).json({ success: false, error: error.message }));
|
|
98
115
|
});
|
|
99
116
|
let cwdRepoRootPromise = null;
|
|
100
|
-
const REPO_ROOTS_TTL_MS = 60000;
|
|
101
|
-
const REPO_ROOT_SESSION_STALENESS_MS = 30 * 24 * 60 * 60 * 1000;
|
|
102
117
|
let cachedRepoRoots = [];
|
|
103
118
|
let repoRootsExpiresAt = 0;
|
|
119
|
+
const REPO_ROOTS_TTL_MS = 60000;
|
|
120
|
+
async function discoverMainRepoRoots() {
|
|
121
|
+
const dataDir = process.env['WORKRAIL_DATA_DIR']
|
|
122
|
+
?? path_1.default.join(process.env.HOME ?? '/tmp', '.workrail', 'data');
|
|
123
|
+
const rootsFile = path_1.default.join(dataDir, 'workflow-sources', 'remembered-roots.json');
|
|
124
|
+
let workspacePaths = [];
|
|
125
|
+
try {
|
|
126
|
+
const raw = await fs_1.default.promises.readFile(rootsFile, 'utf8');
|
|
127
|
+
const parsed = JSON.parse(raw);
|
|
128
|
+
workspacePaths = (parsed.roots ?? []).map((r) => r.path).filter(Boolean);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
const resolved = await Promise.all(workspacePaths.map((p) => (0, worktree_service_js_1.resolveRepoRoot)(p)));
|
|
134
|
+
const roots = new Set(resolved.filter((r) => r !== null));
|
|
135
|
+
return [...roots];
|
|
136
|
+
}
|
|
137
|
+
const WORKTREES_REQUEST_TIMEOUT_MS = 12000;
|
|
104
138
|
app.get('/api/v2/worktrees', async (_req, res) => {
|
|
139
|
+
let timeoutId = null;
|
|
140
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
141
|
+
timeoutId = setTimeout(() => reject(new Error('worktrees scan timeout')), WORKTREES_REQUEST_TIMEOUT_MS);
|
|
142
|
+
});
|
|
105
143
|
try {
|
|
106
144
|
const sessionResult = await consoleService.getSessionList();
|
|
107
145
|
const sessions = sessionResult.isOk() ? sessionResult.value.sessions : [];
|
|
108
146
|
const activeSessions = (0, worktree_service_js_1.buildActiveSessionCounts)(sessions);
|
|
147
|
+
cwdRepoRootPromise ?? (cwdRepoRootPromise = (0, worktree_service_js_1.resolveRepoRoot)(process.cwd()));
|
|
109
148
|
if (Date.now() > repoRootsExpiresAt) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
.map(s => s.repoRoot)
|
|
116
|
-
.filter((r) => r !== null);
|
|
117
|
-
const resolvedRoots = await Promise.all(rawRoots.map(r => (0, worktree_service_js_1.resolveRepoRoot)(r)));
|
|
118
|
-
const repoRootSet = new Set(resolvedRoots.filter((r) => r !== null));
|
|
149
|
+
const [cwdRoot, discovered] = await Promise.all([
|
|
150
|
+
cwdRepoRootPromise,
|
|
151
|
+
discoverMainRepoRoots(),
|
|
152
|
+
]);
|
|
153
|
+
const repoRootsSet = new Set(discovered);
|
|
119
154
|
if (cwdRoot !== null)
|
|
120
|
-
|
|
121
|
-
cachedRepoRoots = [...
|
|
155
|
+
repoRootsSet.add(cwdRoot);
|
|
156
|
+
cachedRepoRoots = [...repoRootsSet];
|
|
122
157
|
repoRootsExpiresAt = Date.now() + REPO_ROOTS_TTL_MS;
|
|
123
158
|
}
|
|
124
|
-
const
|
|
159
|
+
const repoRoots = cachedRepoRoots;
|
|
160
|
+
const data = await Promise.race([
|
|
161
|
+
(0, worktree_service_js_1.getWorktreeList)(repoRoots, activeSessions),
|
|
162
|
+
timeoutPromise,
|
|
163
|
+
]);
|
|
164
|
+
if (timeoutId !== null)
|
|
165
|
+
clearTimeout(timeoutId);
|
|
125
166
|
res.json({ success: true, data });
|
|
126
167
|
}
|
|
127
168
|
catch (e) {
|
|
128
|
-
|
|
169
|
+
if (timeoutId !== null)
|
|
170
|
+
clearTimeout(timeoutId);
|
|
171
|
+
if (e instanceof Error && e.message === 'worktrees scan timeout') {
|
|
172
|
+
res.json({ success: true, data: { repos: [] } });
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
res.status(500).json({ success: false, error: e instanceof Error ? e.message : String(e) });
|
|
176
|
+
}
|
|
129
177
|
}
|
|
130
178
|
});
|
|
131
179
|
app.get('/api/v2/sessions/:sessionId', async (req, res) => {
|
|
@@ -162,7 +210,7 @@ function mountConsoleRoutes(app, consoleService, workflowService, timingRingBuff
|
|
|
162
210
|
description: definition.description,
|
|
163
211
|
version: definition.version,
|
|
164
212
|
tags: tagEntry?.tags ?? [],
|
|
165
|
-
source,
|
|
213
|
+
source: (0, workflow_js_1.toWorkflowSourceInfo)(source),
|
|
166
214
|
...(definition.about !== undefined ? { about: definition.about } : {}),
|
|
167
215
|
...(definition.examples?.length ? { examples: [...definition.examples] } : {}),
|
|
168
216
|
};
|
|
@@ -194,7 +242,7 @@ function mountConsoleRoutes(app, consoleService, workflowService, timingRingBuff
|
|
|
194
242
|
description: definition.description,
|
|
195
243
|
version: definition.version,
|
|
196
244
|
tags: tagEntry?.tags ?? [],
|
|
197
|
-
source,
|
|
245
|
+
source: (0, workflow_js_1.toWorkflowSourceInfo)(source),
|
|
198
246
|
stepCount: definition.steps.length,
|
|
199
247
|
...(definition.about !== undefined ? { about: definition.about } : {}),
|
|
200
248
|
...(definition.examples?.length ? { examples: [...definition.examples] } : {}),
|
|
@@ -16,7 +16,10 @@ const run_execution_trace_js_1 = require("../projections/run-execution-trace.js"
|
|
|
16
16
|
const constants_js_1 = require("../durable-core/constants.js");
|
|
17
17
|
const index_js_1 = require("../durable-core/ids/index.js");
|
|
18
18
|
const MAX_SESSIONS_TO_LOAD = 500;
|
|
19
|
-
const DORMANCY_THRESHOLD_MS =
|
|
19
|
+
const DORMANCY_THRESHOLD_MS = (() => {
|
|
20
|
+
const override = parseInt(process.env['WORKRAIL_DORMANCY_THRESHOLD_MS'] ?? '', 10);
|
|
21
|
+
return Number.isFinite(override) && override > 0 ? override : 60 * 60 * 1000;
|
|
22
|
+
})();
|
|
20
23
|
class ConsoleService {
|
|
21
24
|
constructor(ports) {
|
|
22
25
|
this.ports = ports;
|
|
@@ -337,16 +340,6 @@ function extractGitBranch(events) {
|
|
|
337
340
|
}
|
|
338
341
|
return null;
|
|
339
342
|
}
|
|
340
|
-
function extractRepoRoot(events) {
|
|
341
|
-
for (const e of events) {
|
|
342
|
-
if (e.kind !== constants_js_1.EVENT_KIND.OBSERVATION_RECORDED)
|
|
343
|
-
continue;
|
|
344
|
-
if (e.data.key === 'repo_root') {
|
|
345
|
-
return e.data.value.value;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
return null;
|
|
349
|
-
}
|
|
350
343
|
function truncateTitle(text, maxLen = 120) {
|
|
351
344
|
if (text.length <= maxLen)
|
|
352
345
|
return text;
|
|
@@ -373,7 +366,6 @@ function projectSessionSummary(sessionId, truth, completionByRunId, workflowName
|
|
|
373
366
|
const gapsRes = sortedEventsRes.isOk() ? (0, gaps_js_1.projectGapsV2)(sortedEventsRes.value) : (0, neverthrow_2.err)(sortedEventsRes.error);
|
|
374
367
|
const sessionTitle = sortedEventsRes.isOk() ? deriveSessionTitle(sortedEventsRes.value) : null;
|
|
375
368
|
const gitBranch = extractGitBranch(events);
|
|
376
|
-
const repoRoot = extractRepoRoot(events);
|
|
377
369
|
const runs = Object.values(dag.runsById);
|
|
378
370
|
const run = runs[0];
|
|
379
371
|
if (!run) {
|
|
@@ -393,7 +385,6 @@ function projectSessionSummary(sessionId, truth, completionByRunId, workflowName
|
|
|
393
385
|
hasUnresolvedGaps: false,
|
|
394
386
|
recapSnippet: null,
|
|
395
387
|
gitBranch,
|
|
396
|
-
repoRoot,
|
|
397
388
|
lastModifiedMs,
|
|
398
389
|
};
|
|
399
390
|
}
|
|
@@ -436,7 +427,6 @@ function projectSessionSummary(sessionId, truth, completionByRunId, workflowName
|
|
|
436
427
|
hasUnresolvedGaps,
|
|
437
428
|
recapSnippet,
|
|
438
429
|
gitBranch,
|
|
439
|
-
repoRoot,
|
|
440
430
|
lastModifiedMs,
|
|
441
431
|
};
|
|
442
432
|
}
|
|
@@ -16,7 +16,6 @@ export interface ConsoleSessionSummary {
|
|
|
16
16
|
readonly hasUnresolvedGaps: boolean;
|
|
17
17
|
readonly recapSnippet: string | null;
|
|
18
18
|
readonly gitBranch: string | null;
|
|
19
|
-
readonly repoRoot: string | null;
|
|
20
19
|
readonly lastModifiedMs: number;
|
|
21
20
|
}
|
|
22
21
|
export interface ConsoleSessionListResponse {
|
|
@@ -112,6 +111,20 @@ export interface ChangedFile {
|
|
|
112
111
|
readonly status: FileChangeStatus;
|
|
113
112
|
readonly path: string;
|
|
114
113
|
}
|
|
114
|
+
export interface WorktreeEnrichment {
|
|
115
|
+
readonly headHash: string;
|
|
116
|
+
readonly headMessage: string;
|
|
117
|
+
readonly headTimestampMs: number;
|
|
118
|
+
readonly changedCount: number;
|
|
119
|
+
readonly changedFiles: readonly ChangedFile[];
|
|
120
|
+
readonly aheadCount: number;
|
|
121
|
+
readonly unpushedCommits: readonly {
|
|
122
|
+
readonly hash: string;
|
|
123
|
+
readonly message: string;
|
|
124
|
+
}[];
|
|
125
|
+
readonly isMerged: boolean;
|
|
126
|
+
readonly description: string;
|
|
127
|
+
}
|
|
115
128
|
export interface ConsoleWorktreeSummary {
|
|
116
129
|
readonly path: string;
|
|
117
130
|
readonly name: string;
|
|
@@ -129,6 +142,7 @@ export interface ConsoleWorktreeSummary {
|
|
|
129
142
|
readonly isMerged: boolean;
|
|
130
143
|
readonly activeSessionCount: number;
|
|
131
144
|
readonly description?: string;
|
|
145
|
+
readonly enrichment: WorktreeEnrichment | null;
|
|
132
146
|
}
|
|
133
147
|
export interface ConsoleRepoWorktrees {
|
|
134
148
|
readonly repoName: string;
|
|
@@ -7,4 +7,5 @@ export declare function buildActiveSessionCounts(sessions: ReadonlyArray<{
|
|
|
7
7
|
gitBranch: string | null;
|
|
8
8
|
status: ConsoleSessionStatus;
|
|
9
9
|
}>): ActiveSessionsByBranch;
|
|
10
|
+
export declare function setEnrichmentCompleteCallback(cb: () => void): void;
|
|
10
11
|
export declare function getWorktreeList(repoRoots: readonly string[], activeSessions: ActiveSessionsByBranch): Promise<ConsoleWorktreeListResponse>;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.resolveRepoRoot = resolveRepoRoot;
|
|
4
4
|
exports.buildActiveSessionCounts = buildActiveSessionCounts;
|
|
5
|
+
exports.setEnrichmentCompleteCallback = setEnrichmentCompleteCallback;
|
|
5
6
|
exports.getWorktreeList = getWorktreeList;
|
|
6
7
|
const child_process_1 = require("child_process");
|
|
7
8
|
const util_1 = require("util");
|
|
@@ -9,7 +10,12 @@ const path_1 = require("path");
|
|
|
9
10
|
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
10
11
|
const GIT_TIMEOUT_MS = 5000;
|
|
11
12
|
function isExecError(e) {
|
|
12
|
-
|
|
13
|
+
if (!(e instanceof Error))
|
|
14
|
+
return false;
|
|
15
|
+
if ('killed' in e)
|
|
16
|
+
return true;
|
|
17
|
+
const sys = e.syscall ?? '';
|
|
18
|
+
return sys.startsWith('spawn');
|
|
13
19
|
}
|
|
14
20
|
async function git(cwd, args) {
|
|
15
21
|
try {
|
|
@@ -54,7 +60,7 @@ function acquireEnrichmentSlot() {
|
|
|
54
60
|
resolve();
|
|
55
61
|
}
|
|
56
62
|
else {
|
|
57
|
-
enrichmentQueue.push(
|
|
63
|
+
enrichmentQueue.push(resolve);
|
|
58
64
|
}
|
|
59
65
|
});
|
|
60
66
|
}
|
|
@@ -67,6 +73,29 @@ function releaseEnrichmentSlot() {
|
|
|
67
73
|
activeEnrichments--;
|
|
68
74
|
}
|
|
69
75
|
}
|
|
76
|
+
const MAX_BACKGROUND_ENRICHMENTS = 16;
|
|
77
|
+
let activeBackgroundEnrichments = 0;
|
|
78
|
+
const backgroundEnrichmentQueue = [];
|
|
79
|
+
function acquireBackgroundSlot() {
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
if (activeBackgroundEnrichments < MAX_BACKGROUND_ENRICHMENTS) {
|
|
82
|
+
activeBackgroundEnrichments++;
|
|
83
|
+
resolve();
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
backgroundEnrichmentQueue.push(resolve);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function releaseBackgroundSlot() {
|
|
91
|
+
const next = backgroundEnrichmentQueue.shift();
|
|
92
|
+
if (next) {
|
|
93
|
+
next();
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
activeBackgroundEnrichments--;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
70
99
|
function parseFileStatus(xy) {
|
|
71
100
|
if (xy === '??')
|
|
72
101
|
return 'untracked';
|
|
@@ -122,8 +151,8 @@ async function enrichWorktree(wt) {
|
|
|
122
151
|
wt.branch !== 'main' &&
|
|
123
152
|
mergedBranchesRaw !== null &&
|
|
124
153
|
mergedBranchesRaw.split('\n').some(line => line.trim() === wt.branch);
|
|
125
|
-
const
|
|
126
|
-
return { headHash, headMessage, headTimestampMs, changedCount, changedFiles, aheadCount, unpushedCommits, isMerged,
|
|
154
|
+
const description = descriptionRaw?.trim() ?? '';
|
|
155
|
+
return { headHash, headMessage, headTimestampMs, changedCount, changedFiles, aheadCount, unpushedCommits, isMerged, description };
|
|
127
156
|
}
|
|
128
157
|
async function resolveRepoRoot(path) {
|
|
129
158
|
const commonDir = await git(path, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
|
|
@@ -131,18 +160,39 @@ async function resolveRepoRoot(path) {
|
|
|
131
160
|
return null;
|
|
132
161
|
return commonDir.replace(/\/\.git\/?$/, '') || null;
|
|
133
162
|
}
|
|
134
|
-
async function
|
|
163
|
+
async function buildFastWorktrees(repoRoot) {
|
|
164
|
+
const porcelain = await git(repoRoot, ['worktree', 'list', '--porcelain']);
|
|
165
|
+
if (porcelain === null)
|
|
166
|
+
return null;
|
|
167
|
+
const rawWorktrees = parseWorktreePorcelain(porcelain);
|
|
168
|
+
return rawWorktrees.map((wt) => ({
|
|
169
|
+
path: wt.path,
|
|
170
|
+
name: (0, path_1.basename)(wt.path),
|
|
171
|
+
branch: wt.branch,
|
|
172
|
+
headHash: wt.head.slice(0, 7),
|
|
173
|
+
headMessage: '',
|
|
174
|
+
headTimestampMs: 0,
|
|
175
|
+
changedCount: 0,
|
|
176
|
+
changedFiles: [],
|
|
177
|
+
aheadCount: 0,
|
|
178
|
+
unpushedCommits: [],
|
|
179
|
+
isMerged: false,
|
|
180
|
+
activeSessionCount: 0,
|
|
181
|
+
enrichment: null,
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
async function enrichRepo(repoRoot) {
|
|
135
185
|
const porcelain = await git(repoRoot, ['worktree', 'list', '--porcelain']);
|
|
136
186
|
if (porcelain === null)
|
|
137
187
|
return null;
|
|
138
188
|
const rawWorktrees = parseWorktreePorcelain(porcelain);
|
|
139
189
|
const results = await Promise.allSettled(rawWorktrees.map(async (wt) => {
|
|
140
|
-
await
|
|
190
|
+
await acquireBackgroundSlot();
|
|
141
191
|
try {
|
|
142
192
|
return await enrichWorktree(wt);
|
|
143
193
|
}
|
|
144
194
|
finally {
|
|
145
|
-
|
|
195
|
+
releaseBackgroundSlot();
|
|
146
196
|
}
|
|
147
197
|
}));
|
|
148
198
|
const worktrees = rawWorktrees.flatMap((wt, i) => {
|
|
@@ -152,6 +202,17 @@ async function enrichRepo(repoRoot, activeSessions) {
|
|
|
152
202
|
return [];
|
|
153
203
|
}
|
|
154
204
|
const e = result.value;
|
|
205
|
+
const enrichment = {
|
|
206
|
+
headHash: e.headHash,
|
|
207
|
+
headMessage: e.headMessage,
|
|
208
|
+
headTimestampMs: e.headTimestampMs,
|
|
209
|
+
changedCount: e.changedCount,
|
|
210
|
+
changedFiles: e.changedFiles,
|
|
211
|
+
aheadCount: e.aheadCount,
|
|
212
|
+
unpushedCommits: e.unpushedCommits,
|
|
213
|
+
isMerged: e.isMerged,
|
|
214
|
+
description: e.description,
|
|
215
|
+
};
|
|
155
216
|
return [{
|
|
156
217
|
path: wt.path,
|
|
157
218
|
name: (0, path_1.basename)(wt.path),
|
|
@@ -164,13 +225,12 @@ async function enrichRepo(repoRoot, activeSessions) {
|
|
|
164
225
|
aheadCount: e.aheadCount,
|
|
165
226
|
unpushedCommits: e.unpushedCommits,
|
|
166
227
|
isMerged: e.isMerged,
|
|
167
|
-
activeSessionCount:
|
|
168
|
-
...(e.
|
|
228
|
+
activeSessionCount: 0,
|
|
229
|
+
...(e.description ? { description: e.description } : {}),
|
|
230
|
+
enrichment,
|
|
169
231
|
}];
|
|
170
232
|
});
|
|
171
233
|
return [...worktrees].sort((a, b) => {
|
|
172
|
-
if (b.activeSessionCount !== a.activeSessionCount)
|
|
173
|
-
return b.activeSessionCount - a.activeSessionCount;
|
|
174
234
|
if (b.changedCount !== a.changedCount)
|
|
175
235
|
return b.changedCount - a.changedCount;
|
|
176
236
|
return b.headTimestampMs - a.headTimestampMs;
|
|
@@ -185,12 +245,20 @@ function buildActiveSessionCounts(sessions) {
|
|
|
185
245
|
}
|
|
186
246
|
return { counts };
|
|
187
247
|
}
|
|
188
|
-
|
|
248
|
+
const WORKTREE_CACHE_TTL_MS = 45000;
|
|
249
|
+
let worktreeCache = null;
|
|
250
|
+
let backgroundEnrichmentInFlight = false;
|
|
251
|
+
const BACKGROUND_ENRICHMENT_TIMEOUT_MS = 120000;
|
|
252
|
+
let onEnrichmentComplete = null;
|
|
253
|
+
function setEnrichmentCompleteCallback(cb) {
|
|
254
|
+
onEnrichmentComplete = cb;
|
|
255
|
+
}
|
|
256
|
+
async function scanRepos(repoRoots) {
|
|
189
257
|
const repoResults = await Promise.allSettled(repoRoots.map(async (repoRoot) => {
|
|
190
|
-
const worktrees = await enrichRepo(repoRoot
|
|
258
|
+
const worktrees = await enrichRepo(repoRoot);
|
|
191
259
|
return { repoRoot, worktrees };
|
|
192
260
|
}));
|
|
193
|
-
|
|
261
|
+
return repoResults.flatMap((result) => {
|
|
194
262
|
if (result.status === 'rejected') {
|
|
195
263
|
console.warn(`[WorktreeService] Failed to enrich repo:`, result.reason);
|
|
196
264
|
return [];
|
|
@@ -204,7 +272,33 @@ async function getWorktreeList(repoRoots, activeSessions) {
|
|
|
204
272
|
worktrees,
|
|
205
273
|
}];
|
|
206
274
|
});
|
|
207
|
-
|
|
275
|
+
}
|
|
276
|
+
async function runBackgroundEnrichment(repoRoots, repoRootsKey) {
|
|
277
|
+
try {
|
|
278
|
+
const enriched = await Promise.race([
|
|
279
|
+
scanRepos(repoRoots),
|
|
280
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('background enrichment timeout')), BACKGROUND_ENRICHMENT_TIMEOUT_MS)),
|
|
281
|
+
]);
|
|
282
|
+
if (worktreeCache?.repoRootsKey === repoRootsKey) {
|
|
283
|
+
worktreeCache = { ...worktreeCache, enrichedRepos: enriched };
|
|
284
|
+
onEnrichmentComplete?.();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
backgroundEnrichmentInFlight = false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function applyActiveSessionsAndSort(repos, activeSessions) {
|
|
294
|
+
const reposWithActiveSessions = repos.map((repo) => ({
|
|
295
|
+
...repo,
|
|
296
|
+
worktrees: repo.worktrees.map((wt) => ({
|
|
297
|
+
...wt,
|
|
298
|
+
activeSessionCount: wt.branch ? (activeSessions.counts.get(wt.branch) ?? 0) : 0,
|
|
299
|
+
})),
|
|
300
|
+
}));
|
|
301
|
+
const sortedRepos = [...reposWithActiveSessions].sort((a, b) => {
|
|
208
302
|
const aActive = a.worktrees.some(w => w.activeSessionCount > 0) ? 0 : 1;
|
|
209
303
|
const bActive = b.worktrees.some(w => w.activeSessionCount > 0) ? 0 : 1;
|
|
210
304
|
if (aActive !== bActive)
|
|
@@ -213,3 +307,37 @@ async function getWorktreeList(repoRoots, activeSessions) {
|
|
|
213
307
|
});
|
|
214
308
|
return { repos: sortedRepos };
|
|
215
309
|
}
|
|
310
|
+
async function getWorktreeList(repoRoots, activeSessions) {
|
|
311
|
+
const repoRootsKey = [...repoRoots].sort().join(',');
|
|
312
|
+
const nowMs = Date.now();
|
|
313
|
+
const isCacheValid = worktreeCache !== null &&
|
|
314
|
+
worktreeCache.repoRootsKey === repoRootsKey &&
|
|
315
|
+
nowMs - worktreeCache.cachedAtMs < WORKTREE_CACHE_TTL_MS;
|
|
316
|
+
if (!isCacheValid) {
|
|
317
|
+
const fastRepoResults = await Promise.allSettled(repoRoots.map(async (repoRoot) => {
|
|
318
|
+
const worktrees = await buildFastWorktrees(repoRoot);
|
|
319
|
+
return { repoRoot, worktrees };
|
|
320
|
+
}));
|
|
321
|
+
const fastRepos = fastRepoResults.flatMap((result) => {
|
|
322
|
+
if (result.status === 'rejected')
|
|
323
|
+
return [];
|
|
324
|
+
const { repoRoot, worktrees } = result.value;
|
|
325
|
+
if (!worktrees || worktrees.length === 0)
|
|
326
|
+
return [];
|
|
327
|
+
return [{ repoName: (0, path_1.basename)(repoRoot), repoRoot, worktrees }];
|
|
328
|
+
});
|
|
329
|
+
worktreeCache = {
|
|
330
|
+
unenrichedRepos: fastRepos,
|
|
331
|
+
enrichedRepos: null,
|
|
332
|
+
cachedAtMs: nowMs,
|
|
333
|
+
repoRootsKey,
|
|
334
|
+
};
|
|
335
|
+
if (!backgroundEnrichmentInFlight) {
|
|
336
|
+
backgroundEnrichmentInFlight = true;
|
|
337
|
+
void runBackgroundEnrichment(repoRoots, repoRootsKey);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const cache = worktreeCache;
|
|
341
|
+
const repos = cache.enrichedRepos ?? cache.unenrichedRepos;
|
|
342
|
+
return applyActiveSessionsAndSort(repos, activeSessions);
|
|
343
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exaudeus/workrail",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.18.1",
|
|
4
4
|
"description": "Step-by-step workflow enforcement for AI agents via MCP",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"web"
|
|
29
29
|
],
|
|
30
30
|
"scripts": {
|
|
31
|
-
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true});\" && tsc -p tsconfig.build.json && npm run console:build",
|
|
31
|
+
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true});\" && tsc -p tsconfig.build.json && npm run console:build && node -e \"require('fs').chmodSync('dist/mcp-server.js',0o755)\"",
|
|
32
|
+
"prepack": "node -e \"require('fs').chmodSync('dist/mcp-server.js',0o755)\"",
|
|
32
33
|
"console:build": "cd console && npm install && npm run build",
|
|
33
34
|
"console:dev": "cd console && npm run dev",
|
|
34
35
|
"build:all": "npm run build",
|
package/spec/authoring-spec.json
CHANGED
|
@@ -998,11 +998,11 @@
|
|
|
998
998
|
"step.assessmentRefs",
|
|
999
999
|
"step.assessmentConsequences"
|
|
1000
1000
|
],
|
|
1001
|
-
"rule": "
|
|
1002
|
-
"why": "
|
|
1001
|
+
"rule": "A step may declare one or more assessmentRefs and at most one assessmentConsequences entry. When assessmentConsequences is present, at least one ref is required. Use anyEqualsLevel as the trigger -- the engine checks all submitted dimensions across all referenced assessments and fires if any equals that level.",
|
|
1002
|
+
"why": "Multiple refs allow composing separate orthogonal assessment definitions (e.g. quality-gate + coverage-gate) behind a single blocking consequence, without forcing unrelated dimensions into one monolithic definition.",
|
|
1003
1003
|
"enforcement": ["schema"],
|
|
1004
1004
|
"checks": [
|
|
1005
|
-
"
|
|
1005
|
+
"At least one assessmentRefs entry when assessmentConsequences is present.",
|
|
1006
1006
|
"No more than one assessmentConsequences entry per step.",
|
|
1007
1007
|
"The consequence uses anyEqualsLevel to declare which level blocks -- not a named dimension."
|
|
1008
1008
|
],
|
|
@@ -456,14 +456,13 @@
|
|
|
456
456
|
},
|
|
457
457
|
"assessmentRefs": {
|
|
458
458
|
"type": "array",
|
|
459
|
-
"description": "References to workflow-level assessment definitions expected for this step.
|
|
459
|
+
"description": "References to workflow-level assessment definitions expected for this step. When assessmentConsequences is present, at least one ref is required; multiple refs are supported and the consequence fires if any dimension across any referenced assessment equals the trigger level.",
|
|
460
460
|
"items": {
|
|
461
461
|
"type": "string",
|
|
462
462
|
"minLength": 1,
|
|
463
463
|
"maxLength": 64
|
|
464
464
|
},
|
|
465
465
|
"minItems": 1,
|
|
466
|
-
"maxItems": 1,
|
|
467
466
|
"uniqueItems": true
|
|
468
467
|
},
|
|
469
468
|
"assessmentConsequences": {
|