@exaudeus/workrail 3.15.0 → 3.17.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/application/services/workflow-service.d.ts +2 -0
- package/dist/application/services/workflow-service.js +3 -0
- package/dist/application/use-cases/raw-workflow-file-scanner.js +10 -13
- package/dist/cli/commands/index.d.ts +1 -1
- package/dist/cli/commands/index.js +2 -1
- package/dist/cli/commands/init.d.ts +10 -0
- package/dist/cli/commands/init.js +72 -0
- package/dist/cli.js +13 -1
- package/dist/config/config-file.d.ts +8 -0
- package/dist/config/config-file.js +141 -0
- package/dist/config/feature-flags.js +8 -0
- package/dist/console/assets/index-BZNM03t1.css +1 -0
- package/dist/console/assets/index-BwJelCXK.js +28 -0
- package/dist/console/index.html +2 -2
- package/dist/di/container.d.ts +1 -0
- package/dist/di/container.js +24 -7
- package/dist/infrastructure/session/HttpServer.d.ts +3 -4
- package/dist/infrastructure/session/HttpServer.js +58 -106
- package/dist/infrastructure/storage/caching-workflow-storage.d.ts +2 -0
- package/dist/infrastructure/storage/caching-workflow-storage.js +15 -6
- package/dist/infrastructure/storage/file-workflow-storage.js +3 -4
- package/dist/infrastructure/storage/schema-validating-workflow-storage.js +9 -8
- package/dist/manifest.json +303 -247
- package/dist/mcp/assert-output.d.ts +37 -0
- package/dist/mcp/assert-output.js +53 -0
- package/dist/mcp/boundary-coercion.d.ts +1 -0
- package/dist/mcp/boundary-coercion.js +44 -0
- package/dist/mcp/dev-mode.d.ts +2 -0
- package/dist/mcp/dev-mode.js +16 -0
- package/dist/mcp/handler-factory.d.ts +1 -1
- package/dist/mcp/handler-factory.js +20 -16
- package/dist/mcp/handlers/session.js +8 -9
- package/dist/mcp/handlers/shared/request-workflow-reader.d.ts +1 -0
- package/dist/mcp/handlers/shared/request-workflow-reader.js +90 -20
- package/dist/mcp/handlers/v2-advance-core/event-builders.d.ts +2 -0
- package/dist/mcp/handlers/v2-advance-core/event-builders.js +6 -6
- package/dist/mcp/handlers/v2-advance-core/index.d.ts +2 -0
- package/dist/mcp/handlers/v2-advance-core/index.js +4 -3
- package/dist/mcp/handlers/v2-advance-core/input-validation.d.ts +2 -0
- package/dist/mcp/handlers/v2-advance-core/input-validation.js +32 -9
- package/dist/mcp/handlers/v2-advance-core/outcome-blocked.d.ts +2 -0
- package/dist/mcp/handlers/v2-advance-core/outcome-blocked.js +1 -1
- package/dist/mcp/handlers/v2-advance-core/outcome-success.d.ts +2 -0
- package/dist/mcp/handlers/v2-advance-core/outcome-success.js +1 -1
- package/dist/mcp/handlers/v2-checkpoint.d.ts +1 -1
- package/dist/mcp/handlers/v2-checkpoint.js +5 -6
- package/dist/mcp/handlers/v2-execution/advance.d.ts +4 -2
- package/dist/mcp/handlers/v2-execution/advance.js +5 -7
- package/dist/mcp/handlers/v2-execution/continue-advance.d.ts +1 -0
- package/dist/mcp/handlers/v2-execution/continue-advance.js +59 -27
- package/dist/mcp/handlers/v2-execution/continue-rehydrate.d.ts +2 -1
- package/dist/mcp/handlers/v2-execution/continue-rehydrate.js +11 -10
- package/dist/mcp/handlers/v2-execution/index.js +2 -0
- package/dist/mcp/handlers/v2-execution/replay.d.ts +8 -4
- package/dist/mcp/handlers/v2-execution/replay.js +50 -30
- package/dist/mcp/handlers/v2-execution/start.d.ts +2 -3
- package/dist/mcp/handlers/v2-execution/start.js +58 -30
- package/dist/mcp/handlers/v2-execution/workflow-object-cache.d.ts +5 -0
- package/dist/mcp/handlers/v2-execution/workflow-object-cache.js +19 -0
- package/dist/mcp/handlers/v2-execution-helpers.d.ts +1 -0
- package/dist/mcp/handlers/v2-execution-helpers.js +23 -7
- package/dist/mcp/handlers/v2-resume.d.ts +1 -1
- package/dist/mcp/handlers/v2-resume.js +3 -4
- package/dist/mcp/handlers/v2-state-conversion.js +5 -1
- package/dist/mcp/handlers/v2-workflow.d.ts +80 -0
- package/dist/mcp/handlers/v2-workflow.js +40 -23
- package/dist/mcp/handlers/workflow.d.ts +2 -5
- package/dist/mcp/handlers/workflow.js +15 -12
- package/dist/mcp/output-schemas.d.ts +25 -27
- package/dist/mcp/output-schemas.js +7 -7
- package/dist/mcp/server.js +23 -4
- package/dist/mcp/tool-call-timing.d.ts +24 -0
- package/dist/mcp/tool-call-timing.js +85 -0
- package/dist/mcp/transports/http-entry.js +3 -2
- package/dist/mcp/transports/http-listener.d.ts +1 -0
- package/dist/mcp/transports/http-listener.js +25 -0
- package/dist/mcp/transports/shutdown-hooks.d.ts +4 -1
- package/dist/mcp/transports/shutdown-hooks.js +3 -2
- package/dist/mcp/transports/stdio-entry.js +6 -28
- package/dist/mcp/v2-response-formatter.d.ts +1 -1
- package/dist/mcp/v2-response-formatter.js +2 -5
- package/dist/mcp/validation/schema-introspection.d.ts +1 -0
- package/dist/mcp/validation/schema-introspection.js +15 -5
- package/dist/mcp/validation/suggestion-generator.js +2 -2
- package/dist/runtime/adapters/node-process-signals.d.ts +1 -0
- package/dist/runtime/adapters/node-process-signals.js +5 -0
- package/dist/runtime/adapters/noop-process-signals.d.ts +1 -0
- package/dist/runtime/adapters/noop-process-signals.js +2 -0
- package/dist/runtime/ports/process-signals.d.ts +1 -0
- package/dist/types/workflow-definition.d.ts +5 -1
- package/dist/types/workflow-definition.js +2 -0
- package/dist/types/workflow.d.ts +3 -0
- package/dist/types/workflow.js +35 -26
- package/dist/v2/durable-core/domain/context-template-resolver.js +2 -2
- package/dist/v2/durable-core/domain/function-definition-expander.js +2 -17
- package/dist/v2/durable-core/domain/prompt-renderer.d.ts +2 -0
- package/dist/v2/durable-core/domain/prompt-renderer.js +22 -18
- package/dist/v2/durable-core/domain/recap-recovery.js +23 -16
- package/dist/v2/durable-core/domain/retrieval-contract.js +13 -7
- package/dist/v2/durable-core/schemas/compiled-workflow/index.js +4 -3
- package/dist/v2/durable-core/session-index.d.ts +22 -0
- package/dist/v2/durable-core/session-index.js +58 -0
- package/dist/v2/durable-core/sorted-event-log.d.ts +6 -0
- package/dist/v2/durable-core/sorted-event-log.js +15 -0
- package/dist/v2/infra/local/fs/index.js +8 -8
- package/dist/v2/infra/local/pinned-workflow-store/index.d.ts +2 -0
- package/dist/v2/infra/local/pinned-workflow-store/index.js +49 -0
- package/dist/v2/infra/local/remembered-roots-store/index.d.ts +3 -1
- package/dist/v2/infra/local/remembered-roots-store/index.js +6 -3
- package/dist/v2/infra/local/session-store/index.d.ts +1 -1
- package/dist/v2/infra/local/session-store/index.js +71 -61
- package/dist/v2/infra/local/session-summary-provider/index.js +9 -4
- package/dist/v2/infra/local/snapshot-store/index.js +2 -1
- package/dist/v2/infra/local/workspace-anchor/index.js +4 -2
- package/dist/v2/ports/pinned-workflow-store.port.d.ts +2 -0
- package/dist/v2/ports/session-event-log-store.port.d.ts +1 -1
- package/dist/v2/projections/assessment-consequences.d.ts +2 -1
- package/dist/v2/projections/assessment-consequences.js +0 -5
- package/dist/v2/projections/assessments.d.ts +2 -1
- package/dist/v2/projections/assessments.js +2 -4
- package/dist/v2/projections/gaps.d.ts +2 -1
- package/dist/v2/projections/gaps.js +0 -5
- package/dist/v2/projections/preferences.d.ts +2 -1
- package/dist/v2/projections/preferences.js +0 -5
- package/dist/v2/projections/run-context.d.ts +2 -2
- package/dist/v2/projections/run-context.js +0 -5
- package/dist/v2/projections/run-dag.js +7 -1
- package/dist/v2/projections/run-execution-trace.d.ts +8 -0
- package/dist/v2/projections/run-execution-trace.js +124 -0
- package/dist/v2/projections/run-status-signals.d.ts +2 -2
- package/dist/v2/usecases/console-routes.d.ts +3 -1
- package/dist/v2/usecases/console-routes.js +124 -25
- package/dist/v2/usecases/console-service.d.ts +1 -0
- package/dist/v2/usecases/console-service.js +83 -25
- package/dist/v2/usecases/console-types.d.ts +53 -0
- package/dist/v2/usecases/worktree-service.js +32 -1
- package/package.json +6 -5
- package/spec/workflow.schema.json +18 -0
- package/workflows/adaptive-ticket-creation.json +23 -16
- package/workflows/architecture-scalability-audit.json +29 -22
- package/workflows/bug-investigation.agentic.v2.json +7 -0
- package/workflows/coding-task-workflow-agentic.json +7 -0
- package/workflows/coding-task-workflow-agentic.lean.v2.json +16 -8
- package/workflows/coding-task-workflow-agentic.v2.json +7 -0
- package/workflows/cross-platform-code-conversion.v2.json +7 -0
- package/workflows/document-creation-workflow.json +15 -8
- package/workflows/documentation-update-workflow.json +15 -8
- package/workflows/intelligent-test-case-generation.json +7 -0
- package/workflows/learner-centered-course-workflow.json +9 -2
- package/workflows/mr-review-workflow.agentic.v2.json +7 -0
- package/workflows/personal-learning-materials-creation-branched.json +15 -8
- package/workflows/presentation-creation.json +12 -5
- package/workflows/production-readiness-audit.json +7 -0
- package/workflows/relocation-workflow-us.json +39 -32
- package/workflows/scoped-documentation-workflow.json +33 -26
- package/workflows/ui-ux-design-workflow.json +7 -0
- package/workflows/workflow-diagnose-environment.json +6 -0
- package/workflows/workflow-for-workflows.json +7 -0
- package/workflows/workflow-for-workflows.v2.json +23 -11
- package/workflows/wr.discovery.json +8 -1
- package/dist/console/assets/index-BZYIjrzJ.js +0 -28
- package/dist/console/assets/index-OLCKbDdm.css +0 -1
- package/dist/mcp/handlers/v2-resolve-refs-envelope.d.ts +0 -5
- package/dist/mcp/handlers/v2-resolve-refs-envelope.js +0 -17
|
@@ -8,32 +8,18 @@ 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
|
|
12
|
-
|
|
13
|
-
function broadcastChange() {
|
|
14
|
-
if (sseDebounceTimer !== null)
|
|
15
|
-
return;
|
|
16
|
-
sseDebounceTimer = setTimeout(() => {
|
|
17
|
-
sseDebounceTimer = null;
|
|
18
|
-
for (const client of sseClients) {
|
|
19
|
-
try {
|
|
20
|
-
client.write('data: {"type":"change"}\n\n');
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
sseClients.delete(client);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}, 200);
|
|
27
|
-
}
|
|
28
|
-
function watchSessionsDir(sessionsDir) {
|
|
11
|
+
const dev_mode_js_1 = require("../../mcp/dev-mode.js");
|
|
12
|
+
function watchSessionsDir(sessionsDir, onChanged) {
|
|
29
13
|
try {
|
|
30
14
|
fs_1.default.mkdirSync(sessionsDir, { recursive: true });
|
|
31
15
|
}
|
|
32
16
|
catch { }
|
|
33
17
|
let watcher = null;
|
|
34
18
|
try {
|
|
35
|
-
watcher = fs_1.default.watch(sessionsDir, { recursive: true }, () => {
|
|
36
|
-
|
|
19
|
+
watcher = fs_1.default.watch(sessionsDir, { recursive: true }, (_eventType, filename) => {
|
|
20
|
+
if (filename !== null && filename.endsWith('.jsonl')) {
|
|
21
|
+
onChanged();
|
|
22
|
+
}
|
|
37
23
|
});
|
|
38
24
|
watcher.on('error', () => { });
|
|
39
25
|
}
|
|
@@ -53,9 +39,38 @@ function resolveConsoleDist() {
|
|
|
53
39
|
return legacyConsoleDist;
|
|
54
40
|
return null;
|
|
55
41
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
42
|
+
let cachedWorkflowTags = null;
|
|
43
|
+
function loadWorkflowTags() {
|
|
44
|
+
if (cachedWorkflowTags !== null)
|
|
45
|
+
return cachedWorkflowTags;
|
|
46
|
+
const tagsPath = path_1.default.resolve(__dirname, '../../../spec/workflow-tags.json');
|
|
47
|
+
try {
|
|
48
|
+
cachedWorkflowTags = JSON.parse(fs_1.default.readFileSync(tagsPath, 'utf8'));
|
|
49
|
+
return cachedWorkflowTags;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return { version: 0, tags: [], workflows: {} };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function mountConsoleRoutes(app, consoleService, workflowService, timingRingBuffer) {
|
|
56
|
+
const sseClients = new Set();
|
|
57
|
+
let sseDebounceTimer = null;
|
|
58
|
+
function broadcastChange() {
|
|
59
|
+
if (sseDebounceTimer !== null)
|
|
60
|
+
return;
|
|
61
|
+
sseDebounceTimer = setTimeout(() => {
|
|
62
|
+
sseDebounceTimer = null;
|
|
63
|
+
for (const client of sseClients) {
|
|
64
|
+
try {
|
|
65
|
+
client.write('data: {"type":"change"}\n\n');
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
sseClients.delete(client);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}, 200);
|
|
72
|
+
}
|
|
73
|
+
const stopWatcher = watchSessionsDir(consoleService.getSessionsDir(), broadcastChange);
|
|
59
74
|
app.get('/api/v2/workspace/events', (req, res) => {
|
|
60
75
|
res.setHeader('Content-Type', 'text/event-stream');
|
|
61
76
|
res.setHeader('Cache-Control', 'no-cache');
|
|
@@ -67,12 +82,23 @@ function mountConsoleRoutes(app, consoleService) {
|
|
|
67
82
|
req.on('close', () => { sseClients.delete(res); });
|
|
68
83
|
res.on('close', () => { sseClients.delete(res); });
|
|
69
84
|
});
|
|
85
|
+
const devMode = (0, dev_mode_js_1.isDevMode)();
|
|
86
|
+
if (devMode) {
|
|
87
|
+
app.get('/api/v2/perf/tool-calls', (req, res) => {
|
|
88
|
+
const rawLimit = req.query['limit'];
|
|
89
|
+
const limit = typeof rawLimit === 'string' ? parseInt(rawLimit, 10) : undefined;
|
|
90
|
+
const safeLimit = (limit !== undefined && Number.isFinite(limit) && limit > 0) ? limit : undefined;
|
|
91
|
+
const observations = timingRingBuffer ? timingRingBuffer.recent(safeLimit) : [];
|
|
92
|
+
res.json({ success: true, data: { observations, total: timingRingBuffer?.size ?? 0, devMode } });
|
|
93
|
+
});
|
|
94
|
+
}
|
|
70
95
|
app.get('/api/v2/sessions', async (_req, res) => {
|
|
71
96
|
const result = await consoleService.getSessionList();
|
|
72
97
|
result.match((data) => res.json({ success: true, data }), (error) => res.status(500).json({ success: false, error: error.message }));
|
|
73
98
|
});
|
|
74
99
|
let cwdRepoRootPromise = null;
|
|
75
100
|
const REPO_ROOTS_TTL_MS = 60000;
|
|
101
|
+
const REPO_ROOT_SESSION_STALENESS_MS = 30 * 24 * 60 * 60 * 1000;
|
|
76
102
|
let cachedRepoRoots = [];
|
|
77
103
|
let repoRootsExpiresAt = 0;
|
|
78
104
|
app.get('/api/v2/worktrees', async (_req, res) => {
|
|
@@ -83,7 +109,11 @@ function mountConsoleRoutes(app, consoleService) {
|
|
|
83
109
|
if (Date.now() > repoRootsExpiresAt) {
|
|
84
110
|
cwdRepoRootPromise ?? (cwdRepoRootPromise = (0, worktree_service_js_1.resolveRepoRoot)(process.cwd()));
|
|
85
111
|
const cwdRoot = await cwdRepoRootPromise;
|
|
86
|
-
const
|
|
112
|
+
const cutoffMs = Date.now() - REPO_ROOT_SESSION_STALENESS_MS;
|
|
113
|
+
const rawRoots = sessions
|
|
114
|
+
.filter(s => s.lastModifiedMs >= cutoffMs)
|
|
115
|
+
.map(s => s.repoRoot)
|
|
116
|
+
.filter((r) => r !== null);
|
|
87
117
|
const resolvedRoots = await Promise.all(rawRoots.map(r => (0, worktree_service_js_1.resolveRepoRoot)(r)));
|
|
88
118
|
const repoRootSet = new Set(resolvedRoots.filter((r) => r !== null));
|
|
89
119
|
if (cwdRoot !== null)
|
|
@@ -116,10 +146,78 @@ function mountConsoleRoutes(app, consoleService) {
|
|
|
116
146
|
res.status(status).json({ success: false, error: error.message });
|
|
117
147
|
});
|
|
118
148
|
});
|
|
149
|
+
if (workflowService) {
|
|
150
|
+
app.get('/api/v2/workflows', async (_req, res) => {
|
|
151
|
+
try {
|
|
152
|
+
const tagsFile = loadWorkflowTags();
|
|
153
|
+
const allWorkflows = await workflowService.loadAllWorkflows();
|
|
154
|
+
const workflows = allWorkflows
|
|
155
|
+
.filter((w) => !tagsFile.workflows[w.definition.id]?.hidden)
|
|
156
|
+
.map((w) => {
|
|
157
|
+
const { definition, source } = w;
|
|
158
|
+
const tagEntry = tagsFile.workflows[definition.id];
|
|
159
|
+
return {
|
|
160
|
+
id: definition.id,
|
|
161
|
+
name: definition.name,
|
|
162
|
+
description: definition.description,
|
|
163
|
+
version: definition.version,
|
|
164
|
+
tags: tagEntry?.tags ?? [],
|
|
165
|
+
source,
|
|
166
|
+
...(definition.about !== undefined ? { about: definition.about } : {}),
|
|
167
|
+
...(definition.examples?.length ? { examples: [...definition.examples] } : {}),
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
res.json({ success: true, data: { workflows } });
|
|
171
|
+
}
|
|
172
|
+
catch (e) {
|
|
173
|
+
res.status(500).json({ success: false, error: e instanceof Error ? e.message : String(e) });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
app.get('/api/v2/workflows/:workflowId', async (req, res) => {
|
|
177
|
+
const { workflowId } = req.params;
|
|
178
|
+
try {
|
|
179
|
+
const workflow = await workflowService.getWorkflowById(workflowId);
|
|
180
|
+
if (!workflow) {
|
|
181
|
+
return res.status(404).json({ success: false, error: `Workflow not found: ${workflowId}` });
|
|
182
|
+
}
|
|
183
|
+
const tagsFile = loadWorkflowTags();
|
|
184
|
+
if (tagsFile.workflows[workflowId]?.hidden) {
|
|
185
|
+
return res.status(404).json({ success: false, error: `Workflow not found: ${workflowId}` });
|
|
186
|
+
}
|
|
187
|
+
const { definition, source } = workflow;
|
|
188
|
+
const tagEntry = tagsFile.workflows[workflowId];
|
|
189
|
+
return res.json({
|
|
190
|
+
success: true,
|
|
191
|
+
data: {
|
|
192
|
+
id: definition.id,
|
|
193
|
+
name: definition.name,
|
|
194
|
+
description: definition.description,
|
|
195
|
+
version: definition.version,
|
|
196
|
+
tags: tagEntry?.tags ?? [],
|
|
197
|
+
source,
|
|
198
|
+
stepCount: definition.steps.length,
|
|
199
|
+
...(definition.about !== undefined ? { about: definition.about } : {}),
|
|
200
|
+
...(definition.examples?.length ? { examples: [...definition.examples] } : {}),
|
|
201
|
+
...(definition.preconditions?.length ? { preconditions: [...definition.preconditions] } : {}),
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
catch (e) {
|
|
206
|
+
return res.status(500).json({ success: false, error: e instanceof Error ? e.message : String(e) });
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
119
210
|
const consoleDist = resolveConsoleDist();
|
|
120
211
|
if (consoleDist) {
|
|
121
|
-
app.use('/console', express_1.default.static(consoleDist
|
|
212
|
+
app.use('/console', express_1.default.static(consoleDist, {
|
|
213
|
+
setHeaders(res, filePath) {
|
|
214
|
+
if (path_1.default.basename(filePath) === 'index.html') {
|
|
215
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}));
|
|
122
219
|
app.get('/console/*path', (_req, res) => {
|
|
220
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
123
221
|
res.sendFile(path_1.default.join(consoleDist, 'index.html'));
|
|
124
222
|
});
|
|
125
223
|
console.error(`[Console] UI serving from ${consoleDist}`);
|
|
@@ -133,4 +231,5 @@ function mountConsoleRoutes(app, consoleService) {
|
|
|
133
231
|
});
|
|
134
232
|
console.error('[Console] UI not found (run: cd console && npm run build)');
|
|
135
233
|
}
|
|
234
|
+
return stopWatcher;
|
|
136
235
|
}
|
|
@@ -15,6 +15,7 @@ export interface ConsoleServicePorts {
|
|
|
15
15
|
export declare class ConsoleService {
|
|
16
16
|
private readonly ports;
|
|
17
17
|
constructor(ports: ConsoleServicePorts);
|
|
18
|
+
private readonly _summaryCache;
|
|
18
19
|
getSessionsDir(): string;
|
|
19
20
|
getSessionList(): ResultAsync<ConsoleSessionListResponse, ConsoleServiceError>;
|
|
20
21
|
getSessionDetail(sessionIdStr: string): ResultAsync<ConsoleSessionDetail, ConsoleServiceError>;
|
|
@@ -11,6 +11,8 @@ const node_outputs_js_1 = require("../projections/node-outputs.js");
|
|
|
11
11
|
const advance_outcomes_js_1 = require("../projections/advance-outcomes.js");
|
|
12
12
|
const artifacts_js_1 = require("../projections/artifacts.js");
|
|
13
13
|
const run_context_js_1 = require("../projections/run-context.js");
|
|
14
|
+
const sorted_event_log_js_1 = require("../durable-core/sorted-event-log.js");
|
|
15
|
+
const run_execution_trace_js_1 = require("../projections/run-execution-trace.js");
|
|
14
16
|
const constants_js_1 = require("../durable-core/constants.js");
|
|
15
17
|
const index_js_1 = require("../durable-core/ids/index.js");
|
|
16
18
|
const MAX_SESSIONS_TO_LOAD = 500;
|
|
@@ -18,6 +20,7 @@ const DORMANCY_THRESHOLD_MS = 3 * 24 * 60 * 60 * 1000;
|
|
|
18
20
|
class ConsoleService {
|
|
19
21
|
constructor(ports) {
|
|
20
22
|
this.ports = ports;
|
|
23
|
+
this._summaryCache = new Map();
|
|
21
24
|
}
|
|
22
25
|
getSessionsDir() {
|
|
23
26
|
return this.ports.dataDir.sessionsDir();
|
|
@@ -103,6 +106,10 @@ class ConsoleService {
|
|
|
103
106
|
});
|
|
104
107
|
}
|
|
105
108
|
loadSessionSummary(sessionId, lastModifiedMs, nowMs) {
|
|
109
|
+
const cached = this._summaryCache.get(sessionId);
|
|
110
|
+
if (cached !== undefined && cached.mtime === lastModifiedMs) {
|
|
111
|
+
return (0, neverthrow_2.okAsync)(cached.summary);
|
|
112
|
+
}
|
|
106
113
|
return this.ports.sessionStore
|
|
107
114
|
.load(sessionId)
|
|
108
115
|
.andThen((truth) => {
|
|
@@ -110,10 +117,22 @@ class ConsoleService {
|
|
|
110
117
|
const workflowNamesRA = dagRes.isOk()
|
|
111
118
|
? resolveWorkflowNames(dagRes.value, this.ports.pinnedWorkflowStore)
|
|
112
119
|
: (0, neverthrow_2.okAsync)({});
|
|
120
|
+
const completionRA = dagRes.isOk()
|
|
121
|
+
? resolveRunCompletionFromDag(dagRes.value, this.ports.snapshotStore)
|
|
122
|
+
: (0, neverthrow_2.okAsync)({});
|
|
113
123
|
return neverthrow_1.ResultAsync.combine([
|
|
114
|
-
|
|
124
|
+
completionRA,
|
|
115
125
|
workflowNamesRA,
|
|
116
|
-
]).map(([completionMap, workflowNames]) =>
|
|
126
|
+
]).map(([completionMap, workflowNames]) => {
|
|
127
|
+
const dag = dagRes.isOk() ? dagRes.value : undefined;
|
|
128
|
+
return projectSessionSummary(sessionId, truth, completionMap, workflowNames, lastModifiedMs, nowMs, dag);
|
|
129
|
+
});
|
|
130
|
+
})
|
|
131
|
+
.map((summary) => {
|
|
132
|
+
if (summary !== null && summary.status !== 'in_progress') {
|
|
133
|
+
this._summaryCache.set(sessionId, { mtime: lastModifiedMs, summary });
|
|
134
|
+
}
|
|
135
|
+
return summary;
|
|
117
136
|
})
|
|
118
137
|
.orElse(() => (0, neverthrow_2.okAsync)(null));
|
|
119
138
|
}
|
|
@@ -239,8 +258,8 @@ function extractStepTitlesFromCompiled(compiled) {
|
|
|
239
258
|
return titles;
|
|
240
259
|
}
|
|
241
260
|
const TITLE_CONTEXT_KEYS = ['goal', 'taskDescription', 'mrTitle', 'prTitle', 'ticketTitle', 'problem'];
|
|
242
|
-
function deriveSessionTitle(
|
|
243
|
-
const contextRes = (0, run_context_js_1.projectRunContextV2)(
|
|
261
|
+
function deriveSessionTitle(sortedEvents) {
|
|
262
|
+
const contextRes = (0, run_context_js_1.projectRunContextV2)(sortedEvents);
|
|
244
263
|
if (contextRes.isOk()) {
|
|
245
264
|
for (const runCtx of Object.values(contextRes.value.byRunId)) {
|
|
246
265
|
for (const key of TITLE_CONTEXT_KEYS) {
|
|
@@ -251,7 +270,7 @@ function deriveSessionTitle(events) {
|
|
|
251
270
|
}
|
|
252
271
|
}
|
|
253
272
|
}
|
|
254
|
-
const title = extractTitleFromFirstRecap(
|
|
273
|
+
const title = extractTitleFromFirstRecap(sortedEvents);
|
|
255
274
|
if (title)
|
|
256
275
|
return title;
|
|
257
276
|
return null;
|
|
@@ -333,19 +352,26 @@ function truncateTitle(text, maxLen = 120) {
|
|
|
333
352
|
return text;
|
|
334
353
|
return text.slice(0, maxLen - 1) + '…';
|
|
335
354
|
}
|
|
336
|
-
function projectSessionSummary(sessionId, truth, completionByRunId, workflowNames, lastModifiedMs, nowMs) {
|
|
355
|
+
function projectSessionSummary(sessionId, truth, completionByRunId, workflowNames, lastModifiedMs, nowMs, precomputedDag) {
|
|
337
356
|
const { events } = truth;
|
|
338
357
|
const health = (0, session_health_js_1.projectSessionHealthV2)(truth);
|
|
339
358
|
if (health.isErr())
|
|
340
359
|
return null;
|
|
341
360
|
const sessionHealth = health.value.kind === 'healthy' ? 'healthy' : 'corrupt';
|
|
342
|
-
|
|
343
|
-
if (
|
|
361
|
+
let dag;
|
|
362
|
+
if (precomputedDag !== undefined) {
|
|
363
|
+
dag = precomputedDag;
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
const res = (0, run_dag_js_1.projectRunDagV2)(events);
|
|
367
|
+
dag = res.isOk() ? res.value : null;
|
|
368
|
+
}
|
|
369
|
+
if (dag === null)
|
|
344
370
|
return null;
|
|
345
|
-
const
|
|
346
|
-
const statusRes = (0, run_status_signals_js_1.projectRunStatusSignalsV2)(
|
|
347
|
-
const gapsRes = (0, gaps_js_1.projectGapsV2)(
|
|
348
|
-
const sessionTitle = deriveSessionTitle(
|
|
371
|
+
const sortedEventsRes = (0, sorted_event_log_js_1.asSortedEventLog)(events);
|
|
372
|
+
const statusRes = sortedEventsRes.isOk() ? (0, run_status_signals_js_1.projectRunStatusSignalsV2)(sortedEventsRes.value) : (0, neverthrow_2.err)(sortedEventsRes.error);
|
|
373
|
+
const gapsRes = sortedEventsRes.isOk() ? (0, gaps_js_1.projectGapsV2)(sortedEventsRes.value) : (0, neverthrow_2.err)(sortedEventsRes.error);
|
|
374
|
+
const sessionTitle = sortedEventsRes.isOk() ? deriveSessionTitle(sortedEventsRes.value) : null;
|
|
349
375
|
const gitBranch = extractGitBranch(events);
|
|
350
376
|
const repoRoot = extractRepoRoot(events);
|
|
351
377
|
const runs = Object.values(dag.runsById);
|
|
@@ -418,26 +444,52 @@ function projectSessionDetail(sessionId, truth, completionByRunId, stepLabels, w
|
|
|
418
444
|
const { events } = truth;
|
|
419
445
|
const health = (0, session_health_js_1.projectSessionHealthV2)(truth);
|
|
420
446
|
const sessionHealth = health.isOk() && health.value.kind === 'healthy' ? 'healthy' : 'corrupt';
|
|
421
|
-
const
|
|
447
|
+
const sortedEventsRes = (0, sorted_event_log_js_1.asSortedEventLog)(events);
|
|
448
|
+
const sessionTitle = sortedEventsRes.isOk() ? deriveSessionTitle(sortedEventsRes.value) : null;
|
|
422
449
|
const dagRes = (0, run_dag_js_1.projectRunDagV2)(events);
|
|
423
450
|
if (dagRes.isErr()) {
|
|
424
451
|
return { sessionId, sessionTitle, health: sessionHealth, runs: [] };
|
|
425
452
|
}
|
|
426
|
-
const statusRes = (0, run_status_signals_js_1.projectRunStatusSignalsV2)(
|
|
427
|
-
const gapsRes = (0, gaps_js_1.projectGapsV2)(
|
|
453
|
+
const statusRes = sortedEventsRes.isOk() ? (0, run_status_signals_js_1.projectRunStatusSignalsV2)(sortedEventsRes.value) : (0, neverthrow_2.err)(sortedEventsRes.error);
|
|
454
|
+
const gapsRes = sortedEventsRes.isOk() ? (0, gaps_js_1.projectGapsV2)(sortedEventsRes.value) : (0, neverthrow_2.err)(sortedEventsRes.error);
|
|
455
|
+
const executionTraceRes = (0, run_execution_trace_js_1.projectRunExecutionTraceV2)(events);
|
|
456
|
+
const outputsRes = (0, node_outputs_js_1.projectNodeOutputsV2)(events);
|
|
457
|
+
const artifactsRes = (0, artifacts_js_1.projectArtifactsV2)(events);
|
|
458
|
+
const failedValidationNodeIds = new Set();
|
|
459
|
+
for (const e of events) {
|
|
460
|
+
if (e.kind !== constants_js_1.EVENT_KIND.VALIDATION_PERFORMED)
|
|
461
|
+
continue;
|
|
462
|
+
if (!e.data.result.valid) {
|
|
463
|
+
failedValidationNodeIds.add(e.scope.nodeId);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const gapNodeIds = new Set();
|
|
467
|
+
if (gapsRes.isOk()) {
|
|
468
|
+
for (const gap of Object.values(gapsRes.value.byGapId)) {
|
|
469
|
+
gapNodeIds.add(gap.nodeId);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
428
472
|
const runs = Object.values(dagRes.value.runsById).map((run) => {
|
|
429
473
|
const statusSignals = statusRes.isOk() ? statusRes.value.byRunId[run.runId] : undefined;
|
|
430
474
|
const status = deriveRunStatus(statusSignals?.isBlocked ?? false, statusSignals?.hasUnresolvedCriticalGaps ?? false, completionByRunId[run.runId] ?? false);
|
|
431
475
|
const tipSet = new Set(run.tipNodeIds);
|
|
432
|
-
const nodes = Object.values(run.nodesById).map((node) =>
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
476
|
+
const nodes = Object.values(run.nodesById).map((node) => {
|
|
477
|
+
const nodeOutputs = outputsRes.isOk() ? outputsRes.value.nodesById[node.nodeId] : undefined;
|
|
478
|
+
const nodeArtifacts = artifactsRes.isOk() ? artifactsRes.value.byNodeId[node.nodeId] : undefined;
|
|
479
|
+
return {
|
|
480
|
+
nodeId: node.nodeId,
|
|
481
|
+
nodeKind: node.nodeKind,
|
|
482
|
+
parentNodeId: node.parentNodeId,
|
|
483
|
+
createdAtEventIndex: node.createdAtEventIndex,
|
|
484
|
+
isPreferredTip: node.nodeId === run.preferredTipNodeId,
|
|
485
|
+
isTip: tipSet.has(node.nodeId),
|
|
486
|
+
stepLabel: stepLabels[node.nodeId] ?? null,
|
|
487
|
+
hasRecap: (nodeOutputs?.currentByChannel.recap.length ?? 0) > 0,
|
|
488
|
+
hasFailedValidations: failedValidationNodeIds.has(node.nodeId),
|
|
489
|
+
hasGaps: gapNodeIds.has(node.nodeId),
|
|
490
|
+
hasArtifacts: (nodeArtifacts?.artifacts.length ?? 0) > 0,
|
|
491
|
+
};
|
|
492
|
+
});
|
|
441
493
|
const edges = run.edges.map((edge) => ({
|
|
442
494
|
edgeKind: edge.edgeKind,
|
|
443
495
|
fromNodeId: edge.fromNodeId,
|
|
@@ -459,6 +511,9 @@ function projectSessionDetail(sessionId, truth, completionByRunId, stepLabels, w
|
|
|
459
511
|
hasUnresolvedCriticalGaps: gapsRes.isOk()
|
|
460
512
|
? (gapsRes.value.unresolvedCriticalByRunId[run.runId]?.length ?? 0) > 0
|
|
461
513
|
: false,
|
|
514
|
+
executionTraceSummary: executionTraceRes.isOk()
|
|
515
|
+
? (executionTraceRes.value.byRunId[run.runId] ?? null)
|
|
516
|
+
: null,
|
|
462
517
|
};
|
|
463
518
|
});
|
|
464
519
|
return { sessionId, sessionTitle, health: sessionHealth, runs };
|
|
@@ -566,7 +621,10 @@ function extractValidations(events, nodeId) {
|
|
|
566
621
|
return results;
|
|
567
622
|
}
|
|
568
623
|
function extractGaps(events, nodeId) {
|
|
569
|
-
const
|
|
624
|
+
const sortedEventsRes = (0, sorted_event_log_js_1.asSortedEventLog)(events);
|
|
625
|
+
if (sortedEventsRes.isErr())
|
|
626
|
+
return [];
|
|
627
|
+
const gapsRes = (0, gaps_js_1.projectGapsV2)(sortedEventsRes.value);
|
|
570
628
|
if (gapsRes.isErr())
|
|
571
629
|
return [];
|
|
572
630
|
const gaps = [];
|
|
@@ -31,6 +31,10 @@ export interface ConsoleDagNode {
|
|
|
31
31
|
readonly isPreferredTip: boolean;
|
|
32
32
|
readonly isTip: boolean;
|
|
33
33
|
readonly stepLabel: string | null;
|
|
34
|
+
readonly hasRecap: boolean;
|
|
35
|
+
readonly hasFailedValidations: boolean;
|
|
36
|
+
readonly hasGaps: boolean;
|
|
37
|
+
readonly hasArtifacts: boolean;
|
|
34
38
|
}
|
|
35
39
|
export interface ConsoleDagEdge {
|
|
36
40
|
readonly edgeKind: 'acked_step' | 'checkpoint';
|
|
@@ -38,6 +42,25 @@ export interface ConsoleDagEdge {
|
|
|
38
42
|
readonly toNodeId: string;
|
|
39
43
|
readonly createdAtEventIndex: number;
|
|
40
44
|
}
|
|
45
|
+
export type ConsoleExecutionTraceItemKind = 'selected_next_step' | 'evaluated_condition' | 'entered_loop' | 'exited_loop' | 'detected_non_tip_advance' | 'context_fact' | 'divergence';
|
|
46
|
+
export interface ConsoleExecutionTraceRef {
|
|
47
|
+
readonly kind: 'node_id' | 'step_id' | 'loop_id' | 'condition_id';
|
|
48
|
+
readonly value: string;
|
|
49
|
+
}
|
|
50
|
+
export interface ConsoleExecutionTraceItem {
|
|
51
|
+
readonly kind: ConsoleExecutionTraceItemKind;
|
|
52
|
+
readonly summary: string;
|
|
53
|
+
readonly recordedAtEventIndex: number;
|
|
54
|
+
readonly refs: readonly ConsoleExecutionTraceRef[];
|
|
55
|
+
}
|
|
56
|
+
export interface ConsoleExecutionTraceFact {
|
|
57
|
+
readonly key: string;
|
|
58
|
+
readonly value: string;
|
|
59
|
+
}
|
|
60
|
+
export interface ConsoleExecutionTraceSummary {
|
|
61
|
+
readonly items: readonly ConsoleExecutionTraceItem[];
|
|
62
|
+
readonly contextFacts: readonly ConsoleExecutionTraceFact[];
|
|
63
|
+
}
|
|
41
64
|
export interface ConsoleDagRun {
|
|
42
65
|
readonly runId: string;
|
|
43
66
|
readonly workflowId: string | null;
|
|
@@ -49,6 +72,7 @@ export interface ConsoleDagRun {
|
|
|
49
72
|
readonly tipNodeIds: readonly string[];
|
|
50
73
|
readonly status: ConsoleRunStatus;
|
|
51
74
|
readonly hasUnresolvedCriticalGaps: boolean;
|
|
75
|
+
readonly executionTraceSummary: ConsoleExecutionTraceSummary | null;
|
|
52
76
|
}
|
|
53
77
|
export interface ConsoleSessionDetail {
|
|
54
78
|
readonly sessionId: string;
|
|
@@ -128,3 +152,32 @@ export interface ConsoleNodeDetail {
|
|
|
128
152
|
readonly validations: readonly ConsoleValidationResult[];
|
|
129
153
|
readonly gaps: readonly ConsoleNodeGap[];
|
|
130
154
|
}
|
|
155
|
+
export interface ConsoleWorkflowSourceInfo {
|
|
156
|
+
readonly kind: 'bundled' | 'user' | 'project' | 'custom' | 'git' | 'remote' | 'plugin';
|
|
157
|
+
readonly displayName: string;
|
|
158
|
+
}
|
|
159
|
+
export interface ConsoleWorkflowSummary {
|
|
160
|
+
readonly id: string;
|
|
161
|
+
readonly name: string;
|
|
162
|
+
readonly description: string;
|
|
163
|
+
readonly version: string;
|
|
164
|
+
readonly tags: readonly string[];
|
|
165
|
+
readonly source: ConsoleWorkflowSourceInfo;
|
|
166
|
+
readonly about?: string;
|
|
167
|
+
readonly examples?: readonly string[];
|
|
168
|
+
}
|
|
169
|
+
export interface ConsoleWorkflowListResponse {
|
|
170
|
+
readonly workflows: readonly ConsoleWorkflowSummary[];
|
|
171
|
+
}
|
|
172
|
+
export interface ConsoleWorkflowDetail {
|
|
173
|
+
readonly id: string;
|
|
174
|
+
readonly name: string;
|
|
175
|
+
readonly description: string;
|
|
176
|
+
readonly version: string;
|
|
177
|
+
readonly tags: readonly string[];
|
|
178
|
+
readonly source: ConsoleWorkflowSourceInfo;
|
|
179
|
+
readonly stepCount: number;
|
|
180
|
+
readonly about?: string;
|
|
181
|
+
readonly examples?: readonly string[];
|
|
182
|
+
readonly preconditions?: readonly string[];
|
|
183
|
+
}
|
|
@@ -44,6 +44,29 @@ function parseWorktreePorcelain(raw) {
|
|
|
44
44
|
}
|
|
45
45
|
return entries;
|
|
46
46
|
}
|
|
47
|
+
const MAX_CONCURRENT_ENRICHMENTS = 8;
|
|
48
|
+
let activeEnrichments = 0;
|
|
49
|
+
const enrichmentQueue = [];
|
|
50
|
+
function acquireEnrichmentSlot() {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
if (activeEnrichments < MAX_CONCURRENT_ENRICHMENTS) {
|
|
53
|
+
activeEnrichments++;
|
|
54
|
+
resolve();
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
enrichmentQueue.push(() => { activeEnrichments++; resolve(); });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function releaseEnrichmentSlot() {
|
|
62
|
+
const next = enrichmentQueue.shift();
|
|
63
|
+
if (next) {
|
|
64
|
+
next();
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
activeEnrichments--;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
47
70
|
function parseFileStatus(xy) {
|
|
48
71
|
if (xy === '??')
|
|
49
72
|
return 'untracked';
|
|
@@ -113,7 +136,15 @@ async function enrichRepo(repoRoot, activeSessions) {
|
|
|
113
136
|
if (porcelain === null)
|
|
114
137
|
return null;
|
|
115
138
|
const rawWorktrees = parseWorktreePorcelain(porcelain);
|
|
116
|
-
const results = await Promise.allSettled(rawWorktrees.map(wt =>
|
|
139
|
+
const results = await Promise.allSettled(rawWorktrees.map(async (wt) => {
|
|
140
|
+
await acquireEnrichmentSlot();
|
|
141
|
+
try {
|
|
142
|
+
return await enrichWorktree(wt);
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
releaseEnrichmentSlot();
|
|
146
|
+
}
|
|
147
|
+
}));
|
|
117
148
|
const worktrees = rawWorktrees.flatMap((wt, i) => {
|
|
118
149
|
const result = results[i];
|
|
119
150
|
if (result.status === 'rejected') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exaudeus/workrail",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.17.0",
|
|
4
4
|
"description": "Step-by-step workflow enforcement for AI agents via MCP",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -77,7 +77,8 @@
|
|
|
77
77
|
"codemod:v2-contexts": "npx ts-node scripts/codemods/run.ts --mod v2-contexts --tsconfig tsconfig.test.json --write",
|
|
78
78
|
"codemod:v2-prune": "npx ts-node scripts/codemods/run.ts --mod v2-prune --tsconfig tsconfig.test.json --write",
|
|
79
79
|
"codemod:guard": "npx ts-node scripts/codemods/run.ts --mod guard --tsconfig tsconfig.test.json",
|
|
80
|
-
"codemod:test-platform-guard": "npx ts-node scripts/codemods/run.ts --mod test-platform-guard --tsconfig tsconfig.test.json"
|
|
80
|
+
"codemod:test-platform-guard": "npx ts-node scripts/codemods/run.ts --mod test-platform-guard --tsconfig tsconfig.test.json",
|
|
81
|
+
"prepare": "bash scripts/setup-hooks.sh"
|
|
81
82
|
},
|
|
82
83
|
"dependencies": {
|
|
83
84
|
"@modelcontextprotocol/sdk": "^1.24.0",
|
|
@@ -89,7 +90,7 @@
|
|
|
89
90
|
"dotenv": "^17.2.0",
|
|
90
91
|
"express": "^5.1.0",
|
|
91
92
|
"neverthrow": "^8.2.0",
|
|
92
|
-
"open": "^
|
|
93
|
+
"open": "^11.0.0",
|
|
93
94
|
"reflect-metadata": "^0.2.0",
|
|
94
95
|
"semver": "^7.7.2",
|
|
95
96
|
"tsconfig-paths": "^4.2.0",
|
|
@@ -121,8 +122,8 @@
|
|
|
121
122
|
"happy-dom": "^20.0.11",
|
|
122
123
|
"jsdom": "^27.0.0",
|
|
123
124
|
"lit": "^3.3.1",
|
|
124
|
-
"node-fetch": "^
|
|
125
|
-
"semantic-release": "^
|
|
125
|
+
"node-fetch": "^3.3.2",
|
|
126
|
+
"semantic-release": "^25.0.3",
|
|
126
127
|
"ts-morph": "^27.0.2",
|
|
127
128
|
"typescript": "^5.9.3",
|
|
128
129
|
"vite": "^7.1.9",
|
|
@@ -182,6 +182,24 @@
|
|
|
182
182
|
"type": "integer",
|
|
183
183
|
"minimum": 1,
|
|
184
184
|
"description": "The authoring spec version this workflow was last validated against."
|
|
185
|
+
},
|
|
186
|
+
"about": {
|
|
187
|
+
"type": "string",
|
|
188
|
+
"description": "Human-readable overview for display in the console and other UIs. Markdown is supported. Write for a user deciding whether to use this workflow: what it does, when to use it, what it produces, and how to get good results. User-facing surface -- not an agent instruction (use metaGuidance for that).",
|
|
189
|
+
"minLength": 1,
|
|
190
|
+
"maxLength": 4096
|
|
191
|
+
},
|
|
192
|
+
"examples": {
|
|
193
|
+
"type": "array",
|
|
194
|
+
"description": "Short illustrative goal strings showing what this workflow is used for. Write for humans browsing the catalog and for agents selecting the right workflow. Each item should be concrete and specific enough to be informative.",
|
|
195
|
+
"items": {
|
|
196
|
+
"type": "string",
|
|
197
|
+
"minLength": 10,
|
|
198
|
+
"maxLength": 120
|
|
199
|
+
},
|
|
200
|
+
"minItems": 1,
|
|
201
|
+
"maxItems": 6,
|
|
202
|
+
"uniqueItems": true
|
|
185
203
|
}
|
|
186
204
|
},
|
|
187
205
|
"required": [
|