@exaudeus/workrail 3.14.0 → 3.15.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/validation-engine.js +4 -9
- package/dist/application/services/workflow-compiler.js +4 -6
- package/dist/console/assets/index-BZYIjrzJ.js +28 -0
- package/dist/console/assets/index-OLCKbDdm.css +1 -0
- package/dist/console/index.html +2 -2
- package/dist/engine/engine-factory.js +2 -2
- package/dist/engine/types.d.ts +1 -1
- package/dist/manifest.json +63 -63
- package/dist/mcp/handlers/shared/request-workflow-reader.d.ts +5 -0
- package/dist/mcp/handlers/shared/request-workflow-reader.js +47 -2
- package/dist/mcp/handlers/v2-advance-core/assessment-consequences.d.ts +1 -1
- package/dist/mcp/handlers/v2-advance-core/assessment-consequences.js +4 -5
- package/dist/mcp/handlers/v2-advance-core/index.js +1 -1
- package/dist/mcp/handlers/v2-advance-core/outcome-blocked.js +1 -1
- package/dist/mcp/handlers/v2-execution/start.d.ts +1 -0
- package/dist/mcp/handlers/v2-execution/start.js +20 -1
- package/dist/mcp/handlers/v2-workflow.d.ts +20 -0
- package/dist/mcp/handlers/v2-workflow.js +119 -10
- package/dist/mcp/output-schemas.d.ts +109 -8
- package/dist/mcp/output-schemas.js +31 -11
- package/dist/mcp/server.js +48 -1
- package/dist/mcp/tool-descriptions.js +17 -9
- package/dist/mcp/v2/tools.d.ts +6 -0
- package/dist/mcp/v2/tools.js +2 -0
- package/dist/mcp/workflow-protocol-contracts.js +5 -1
- package/dist/types/workflow-definition.d.ts +1 -2
- package/dist/v2/infra/local/workspace-anchor/index.js +4 -1
- package/dist/v2/usecases/console-routes.js +49 -1
- package/dist/v2/usecases/console-service.d.ts +1 -0
- package/dist/v2/usecases/console-service.js +4 -1
- package/dist/v2/usecases/console-types.d.ts +12 -0
- package/dist/v2/usecases/worktree-service.js +55 -7
- package/package.json +2 -2
- package/spec/authoring-spec.json +82 -1
- package/spec/workflow-tags.json +132 -0
- package/spec/workflow.schema.json +3 -11
- package/workflows/adaptive-ticket-creation.json +40 -22
- package/workflows/architecture-scalability-audit.json +65 -31
- package/workflows/bug-investigation.agentic.v2.json +36 -14
- package/workflows/coding-task-workflow-agentic.json +50 -38
- package/workflows/coding-task-workflow-agentic.lean.v2.json +124 -37
- package/workflows/coding-task-workflow-agentic.v2.json +90 -30
- package/workflows/cross-platform-code-conversion.v2.json +168 -48
- package/workflows/document-creation-workflow.json +47 -17
- package/workflows/documentation-update-workflow.json +8 -8
- package/workflows/intelligent-test-case-generation.json +2 -2
- package/workflows/learner-centered-course-workflow.json +267 -267
- package/workflows/mr-review-workflow.agentic.v2.json +81 -14
- package/workflows/personal-learning-materials-creation-branched.json +175 -175
- package/workflows/presentation-creation.json +159 -159
- package/workflows/production-readiness-audit.json +54 -15
- package/workflows/relocation-workflow-us.json +44 -35
- package/workflows/routines/tension-driven-design.json +1 -1
- package/workflows/scoped-documentation-workflow.json +25 -25
- package/workflows/test-artifact-loop-control.json +1 -2
- package/workflows/ui-ux-design-workflow.json +327 -0
- package/workflows/workflow-diagnose-environment.json +1 -1
- package/workflows/workflow-for-workflows.json +507 -484
- package/workflows/workflow-for-workflows.v2.json +43 -11
- package/workflows/wr.discovery.json +112 -30
- package/dist/console/assets/index-DW78t31j.css +0 -1
- package/dist/console/assets/index-EsSXrC_a.js +0 -28
|
@@ -48,12 +48,17 @@ This tool provides:
|
|
|
48
48
|
- Field descriptions and validation rules
|
|
49
49
|
- Examples of valid patterns and formats
|
|
50
50
|
- Schema version and metadata information`,
|
|
51
|
-
list_workflows: `Lists available workflows
|
|
51
|
+
list_workflows: `Lists available workflows. When a workflow exists for the user's request, following it means following the user's structured instructions.
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
Discovery pattern (token-efficient — do this):
|
|
54
|
+
- Preferred: read the workrail://tags MCP resource — tag definitions with no tool call.
|
|
55
|
+
- Or: call with no tags → returns tagSummary (~500 tokens) with when-to-use phrases per tag.
|
|
56
|
+
- Then: call with tags=["<tagId>"] → returns the filtered workflow list for that tag.
|
|
57
|
+
- Shortcut: if a workflow ID in tagSummary examples[] already matches, call start_workflow directly — skip the second list call.
|
|
58
|
+
- Multiple tags: if more than one tag fits, pick the most specific one.
|
|
54
59
|
|
|
55
|
-
Always pass workspacePath so project-scoped workflow variants are resolved against the correct workspace
|
|
56
|
-
inspect_workflow: `Inspect a workflow structure before starting it
|
|
60
|
+
Always pass workspacePath so project-scoped workflow variants are resolved against the correct workspace. Shared MCP servers cannot infer this safely.`,
|
|
61
|
+
inspect_workflow: `Inspect a workflow structure before starting it.
|
|
57
62
|
|
|
58
63
|
Use this to understand what steps the workflow will guide you through. The workflow is a step-by-step plan the user (or workflow author) created for this type of task.
|
|
59
64
|
|
|
@@ -139,14 +144,17 @@ Returns validation result with specific errors and suggestions if invalid.`,
|
|
|
139
144
|
get_workflow_schema: `Get the workflow JSON schema for creating or editing workflows.
|
|
140
145
|
|
|
141
146
|
Returns the complete schema definition including required fields, valid patterns, and constraints. Use this as reference when authoring workflow JSON.`,
|
|
142
|
-
list_workflows: `
|
|
147
|
+
list_workflows: `Lists available workflows. When a workflow exists for the user's request, you MUST use it instead of improvising your own approach.
|
|
143
148
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
149
|
+
Discovery pattern (token-efficient — do this):
|
|
150
|
+
- Preferred: read the workrail://tags MCP resource — tag definitions with no tool call.
|
|
151
|
+
- Or: call with no tags → returns tagSummary (~500 tokens) with when-to-use phrases per tag.
|
|
152
|
+
- Then: call with tags=["<tagId>"] → returns the filtered workflow list for that tag.
|
|
153
|
+
- Shortcut: if a workflow ID in tagSummary examples[] already matches, call start_workflow directly — skip the second list call.
|
|
154
|
+
- Multiple tags: if more than one tag fits, pick the most specific one.
|
|
147
155
|
|
|
148
156
|
Pass workspacePath on every call so project-scoped workflow variants are resolved against the correct workspace. Shared MCP servers cannot infer this safely.`,
|
|
149
|
-
inspect_workflow: `Inspect a workflow you are considering following
|
|
157
|
+
inspect_workflow: `Inspect a workflow you are considering following.
|
|
150
158
|
|
|
151
159
|
Use this to understand the workflow's structure before starting. The workflow is the user's explicit plan - not suggestions, not guidelines, but direct instructions you will follow.
|
|
152
160
|
|
package/dist/mcp/v2/tools.d.ts
CHANGED
|
@@ -3,12 +3,15 @@ import type { ToolAnnotations } from '../tool-factory.js';
|
|
|
3
3
|
export declare const V2ListWorkflowsInput: z.ZodObject<{
|
|
4
4
|
workspacePath: z.ZodEffects<z.ZodString, string, string>;
|
|
5
5
|
includeSources: z.ZodOptional<z.ZodBoolean>;
|
|
6
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
6
7
|
}, "strip", z.ZodTypeAny, {
|
|
7
8
|
workspacePath: string;
|
|
8
9
|
includeSources?: boolean | undefined;
|
|
10
|
+
tags?: string[] | undefined;
|
|
9
11
|
}, {
|
|
10
12
|
workspacePath: string;
|
|
11
13
|
includeSources?: boolean | undefined;
|
|
14
|
+
tags?: string[] | undefined;
|
|
12
15
|
}>;
|
|
13
16
|
export type V2ListWorkflowsInput = z.infer<typeof V2ListWorkflowsInput>;
|
|
14
17
|
export declare const V2InspectWorkflowInput: z.ZodObject<{
|
|
@@ -28,12 +31,15 @@ export type V2InspectWorkflowInput = z.infer<typeof V2InspectWorkflowInput>;
|
|
|
28
31
|
export declare const V2StartWorkflowInput: z.ZodObject<{
|
|
29
32
|
workflowId: z.ZodString;
|
|
30
33
|
workspacePath: z.ZodEffects<z.ZodString, string, string>;
|
|
34
|
+
goal: z.ZodString;
|
|
31
35
|
}, "strip", z.ZodTypeAny, {
|
|
32
36
|
workflowId: string;
|
|
33
37
|
workspacePath: string;
|
|
38
|
+
goal: string;
|
|
34
39
|
}, {
|
|
35
40
|
workflowId: string;
|
|
36
41
|
workspacePath: string;
|
|
42
|
+
goal: string;
|
|
37
43
|
}>;
|
|
38
44
|
export type V2StartWorkflowInput = z.infer<typeof V2StartWorkflowInput>;
|
|
39
45
|
export declare const V2ContinueWorkflowInputShape: z.ZodObject<{
|
package/dist/mcp/v2/tools.js
CHANGED
|
@@ -17,6 +17,7 @@ const optionalWorkspacePathField = workspacePathField.optional();
|
|
|
17
17
|
exports.V2ListWorkflowsInput = zod_1.z.object({
|
|
18
18
|
workspacePath: workspacePathField.describe('Required. Absolute path to your current workspace directory (e.g. the "Workspace:" value from your system parameters). WorkRail uses this to resolve project-scoped workflow variants against the correct workspace for discovery-sensitive workflow listing. Shared MCP servers cannot infer this safely.'),
|
|
19
19
|
includeSources: zod_1.z.boolean().optional().describe('When true, includes a source catalog in the response showing where workflows come from (built-in, project-scoped, rooted-sharing, external), with effective and shadowed workflow counts per source. Omit or set false for the default workflow-list-only response.'),
|
|
20
|
+
tags: zod_1.z.array(zod_1.z.string()).optional().describe('Filter by one or more domain tags (e.g. ["coding"], ["review_audit", "investigation"]). When present, returns the full workflow list for workflows matching any of the specified tags. When absent, returns a compact tagSummary instead of the full list — use this first to discover which tags exist, then call again with a specific tag. Valid tags: coding, review_audit, investigation, design, documentation, tickets, learning, routines, authoring.'),
|
|
20
21
|
});
|
|
21
22
|
exports.V2InspectWorkflowInput = zod_1.z.object({
|
|
22
23
|
workflowId: zod_1.z.string().min(1).regex(/^([a-z0-9_-]+|[a-z][a-z0-9_-]+\.[a-z][a-z0-9_-]+)$/, 'Workflow ID must be a valid legacy ID (e.g. my-workflow) or namespaced ID (e.g. wr.discovery)').describe('The workflow ID to inspect'),
|
|
@@ -26,6 +27,7 @@ exports.V2InspectWorkflowInput = zod_1.z.object({
|
|
|
26
27
|
exports.V2StartWorkflowInput = zod_1.z.object({
|
|
27
28
|
workflowId: zod_1.z.string().min(1).regex(/^([a-z0-9_-]+|[a-z][a-z0-9_-]+\.[a-z][a-z0-9_-]+)$/, 'Workflow ID must be a valid legacy ID (e.g. my-workflow) or namespaced ID (e.g. wr.discovery)').describe('The workflow ID to start'),
|
|
28
29
|
workspacePath: workspacePathField.describe('Required. Absolute path to your current workspace directory (e.g. the "Workspace:" value from your system parameters). WorkRail uses this to resolve the correct project-scoped workflow variant and to anchor the session to the correct repo for future resume_session discovery. Shared MCP servers cannot infer this safely.'),
|
|
30
|
+
goal: zod_1.z.string().min(1).describe('A short sentence describing what you are trying to accomplish (e.g. "implement OAuth refresh token rotation", "review PR #47 before merge", "investigate why the build fails on CI").'),
|
|
29
31
|
});
|
|
30
32
|
exports.V2ContinueWorkflowInputShape = zod_1.z.object({
|
|
31
33
|
workspacePath: optionalWorkspacePathField,
|
|
@@ -46,7 +46,7 @@ function findAliasFieldConflicts(value, aliasMap) {
|
|
|
46
46
|
}
|
|
47
47
|
exports.START_WORKFLOW_PROTOCOL = {
|
|
48
48
|
canonicalParams: {
|
|
49
|
-
required: ['workflowId', 'workspacePath'],
|
|
49
|
+
required: ['workflowId', 'workspacePath', 'goal'],
|
|
50
50
|
optional: [],
|
|
51
51
|
},
|
|
52
52
|
descriptions: {
|
|
@@ -57,11 +57,13 @@ exports.START_WORKFLOW_PROTOCOL = {
|
|
|
57
57
|
'Follow the returned step exactly; treat it as the user\'s current instruction.',
|
|
58
58
|
'When the step is done, call continue_workflow with the returned continueToken.',
|
|
59
59
|
'Always pass workspacePath. Shared MCP servers cannot safely infer which repo/workspace you mean.',
|
|
60
|
+
'Always pass goal. A short sentence describing what you are trying to accomplish (e.g. "implement OAuth refresh token rotation").',
|
|
60
61
|
'Only pass context on later continue_workflow calls if facts changed.',
|
|
61
62
|
],
|
|
62
63
|
examplePayload: {
|
|
63
64
|
workflowId: 'coding-task-workflow-agentic',
|
|
64
65
|
workspacePath: '/Users/you/git/my-project',
|
|
66
|
+
goal: 'implement OAuth refresh token rotation',
|
|
65
67
|
},
|
|
66
68
|
returns: 'Step instructions plus continueToken and checkpointToken in the structured response.',
|
|
67
69
|
},
|
|
@@ -72,10 +74,12 @@ exports.START_WORKFLOW_PROTOCOL = {
|
|
|
72
74
|
'Execute the returned step exactly as written.',
|
|
73
75
|
'When the step is complete, call continue_workflow with the returned continueToken.',
|
|
74
76
|
'Pass workspacePath on every call. Shared MCP servers cannot safely infer the correct workspace.',
|
|
77
|
+
'Pass goal on every call. A short sentence describing what you are trying to accomplish.',
|
|
75
78
|
],
|
|
76
79
|
examplePayload: {
|
|
77
80
|
workflowId: 'coding-task-workflow-agentic',
|
|
78
81
|
workspacePath: '/Users/you/git/my-project',
|
|
82
|
+
goal: 'implement OAuth refresh token rotation',
|
|
79
83
|
},
|
|
80
84
|
returns: 'Step instructions plus continueToken and checkpointToken in the structured response.',
|
|
81
85
|
},
|
|
@@ -23,8 +23,7 @@ export interface AssessmentDefinition {
|
|
|
23
23
|
readonly dimensions: readonly AssessmentDimensionDefinition[];
|
|
24
24
|
}
|
|
25
25
|
export interface AssessmentConsequenceTriggerDefinition {
|
|
26
|
-
readonly
|
|
27
|
-
readonly equalsLevel: string;
|
|
26
|
+
readonly anyEqualsLevel: string;
|
|
28
27
|
}
|
|
29
28
|
export interface AssessmentFollowupRequiredEffectDefinition {
|
|
30
29
|
readonly kind: 'require_followup';
|
|
@@ -33,7 +33,10 @@ class LocalWorkspaceAnchorV2 {
|
|
|
33
33
|
}
|
|
34
34
|
async runGitCommands(cwd) {
|
|
35
35
|
const anchors = [];
|
|
36
|
-
const
|
|
36
|
+
const gitCommonDir = await this.gitCommand('git rev-parse --path-format=absolute --git-common-dir', cwd);
|
|
37
|
+
if (!gitCommonDir)
|
|
38
|
+
return anchors;
|
|
39
|
+
const repoRoot = gitCommonDir.replace(/\/\.git\/?$/, '').trim() || null;
|
|
37
40
|
if (!repoRoot)
|
|
38
41
|
return anchors;
|
|
39
42
|
const repoRootHash = hashRepoRoot(repoRoot);
|
|
@@ -8,6 +8,39 @@ 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 sseClients = new Set();
|
|
12
|
+
let sseDebounceTimer = null;
|
|
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) {
|
|
29
|
+
try {
|
|
30
|
+
fs_1.default.mkdirSync(sessionsDir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
catch { }
|
|
33
|
+
let watcher = null;
|
|
34
|
+
try {
|
|
35
|
+
watcher = fs_1.default.watch(sessionsDir, { recursive: true }, () => {
|
|
36
|
+
broadcastChange();
|
|
37
|
+
});
|
|
38
|
+
watcher.on('error', () => { });
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
}
|
|
42
|
+
return () => { watcher?.close(); };
|
|
43
|
+
}
|
|
11
44
|
function resolveConsoleDist() {
|
|
12
45
|
const releasedDist = path_1.default.join(__dirname, '../../console');
|
|
13
46
|
if (fs_1.default.existsSync(releasedDist))
|
|
@@ -21,6 +54,19 @@ function resolveConsoleDist() {
|
|
|
21
54
|
return null;
|
|
22
55
|
}
|
|
23
56
|
function mountConsoleRoutes(app, consoleService) {
|
|
57
|
+
const stopWatcher = watchSessionsDir(consoleService.getSessionsDir());
|
|
58
|
+
process.once('exit', stopWatcher);
|
|
59
|
+
app.get('/api/v2/workspace/events', (req, res) => {
|
|
60
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
61
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
62
|
+
res.setHeader('Connection', 'keep-alive');
|
|
63
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
64
|
+
res.flushHeaders();
|
|
65
|
+
res.write('data: {"type":"connected"}\n\n');
|
|
66
|
+
sseClients.add(res);
|
|
67
|
+
req.on('close', () => { sseClients.delete(res); });
|
|
68
|
+
res.on('close', () => { sseClients.delete(res); });
|
|
69
|
+
});
|
|
24
70
|
app.get('/api/v2/sessions', async (_req, res) => {
|
|
25
71
|
const result = await consoleService.getSessionList();
|
|
26
72
|
result.match((data) => res.json({ success: true, data }), (error) => res.status(500).json({ success: false, error: error.message }));
|
|
@@ -37,7 +83,9 @@ function mountConsoleRoutes(app, consoleService) {
|
|
|
37
83
|
if (Date.now() > repoRootsExpiresAt) {
|
|
38
84
|
cwdRepoRootPromise ?? (cwdRepoRootPromise = (0, worktree_service_js_1.resolveRepoRoot)(process.cwd()));
|
|
39
85
|
const cwdRoot = await cwdRepoRootPromise;
|
|
40
|
-
const
|
|
86
|
+
const rawRoots = sessions.map(s => s.repoRoot).filter((r) => r !== null);
|
|
87
|
+
const resolvedRoots = await Promise.all(rawRoots.map(r => (0, worktree_service_js_1.resolveRepoRoot)(r)));
|
|
88
|
+
const repoRootSet = new Set(resolvedRoots.filter((r) => r !== null));
|
|
41
89
|
if (cwdRoot !== null)
|
|
42
90
|
repoRootSet.add(cwdRoot);
|
|
43
91
|
cachedRepoRoots = [...repoRootSet];
|
|
@@ -15,6 +15,7 @@ export interface ConsoleServicePorts {
|
|
|
15
15
|
export declare class ConsoleService {
|
|
16
16
|
private readonly ports;
|
|
17
17
|
constructor(ports: ConsoleServicePorts);
|
|
18
|
+
getSessionsDir(): string;
|
|
18
19
|
getSessionList(): ResultAsync<ConsoleSessionListResponse, ConsoleServiceError>;
|
|
19
20
|
getSessionDetail(sessionIdStr: string): ResultAsync<ConsoleSessionDetail, ConsoleServiceError>;
|
|
20
21
|
getNodeDetail(sessionIdStr: string, nodeId: string): ResultAsync<ConsoleNodeDetail, ConsoleServiceError>;
|
|
@@ -19,6 +19,9 @@ class ConsoleService {
|
|
|
19
19
|
constructor(ports) {
|
|
20
20
|
this.ports = ports;
|
|
21
21
|
}
|
|
22
|
+
getSessionsDir() {
|
|
23
|
+
return this.ports.dataDir.sessionsDir();
|
|
24
|
+
}
|
|
22
25
|
getSessionList() {
|
|
23
26
|
return this.ports.directoryListing
|
|
24
27
|
.readdirWithMtime(this.ports.dataDir.sessionsDir())
|
|
@@ -243,7 +246,7 @@ function deriveSessionTitle(events) {
|
|
|
243
246
|
for (const key of TITLE_CONTEXT_KEYS) {
|
|
244
247
|
const val = runCtx.context[key];
|
|
245
248
|
if (typeof val === 'string' && val.trim().length > 0) {
|
|
246
|
-
return
|
|
249
|
+
return val.trim();
|
|
247
250
|
}
|
|
248
251
|
}
|
|
249
252
|
}
|
|
@@ -83,6 +83,11 @@ export interface ConsoleArtifact {
|
|
|
83
83
|
readonly byteLength: number;
|
|
84
84
|
readonly content: unknown;
|
|
85
85
|
}
|
|
86
|
+
export type FileChangeStatus = 'modified' | 'added' | 'deleted' | 'untracked' | 'renamed' | 'other';
|
|
87
|
+
export interface ChangedFile {
|
|
88
|
+
readonly status: FileChangeStatus;
|
|
89
|
+
readonly path: string;
|
|
90
|
+
}
|
|
86
91
|
export interface ConsoleWorktreeSummary {
|
|
87
92
|
readonly path: string;
|
|
88
93
|
readonly name: string;
|
|
@@ -91,8 +96,15 @@ export interface ConsoleWorktreeSummary {
|
|
|
91
96
|
readonly headMessage: string;
|
|
92
97
|
readonly headTimestampMs: number;
|
|
93
98
|
readonly changedCount: number;
|
|
99
|
+
readonly changedFiles: readonly ChangedFile[];
|
|
94
100
|
readonly aheadCount: number;
|
|
101
|
+
readonly unpushedCommits: readonly {
|
|
102
|
+
readonly hash: string;
|
|
103
|
+
readonly message: string;
|
|
104
|
+
}[];
|
|
105
|
+
readonly isMerged: boolean;
|
|
95
106
|
readonly activeSessionCount: number;
|
|
107
|
+
readonly description?: string;
|
|
96
108
|
}
|
|
97
109
|
export interface ConsoleRepoWorktrees {
|
|
98
110
|
readonly repoName: string;
|
|
@@ -44,25 +44,69 @@ function parseWorktreePorcelain(raw) {
|
|
|
44
44
|
}
|
|
45
45
|
return entries;
|
|
46
46
|
}
|
|
47
|
+
function parseFileStatus(xy) {
|
|
48
|
+
if (xy === '??')
|
|
49
|
+
return 'untracked';
|
|
50
|
+
const x = xy[0] ?? ' ';
|
|
51
|
+
const y = xy[1] ?? ' ';
|
|
52
|
+
if (x === 'R')
|
|
53
|
+
return 'renamed';
|
|
54
|
+
if (x === 'A')
|
|
55
|
+
return 'added';
|
|
56
|
+
if (x === 'D' || y === 'D')
|
|
57
|
+
return 'deleted';
|
|
58
|
+
if (x === 'M' || y === 'M')
|
|
59
|
+
return 'modified';
|
|
60
|
+
return 'other';
|
|
61
|
+
}
|
|
62
|
+
function parseChangedFiles(statusRaw) {
|
|
63
|
+
if (!statusRaw)
|
|
64
|
+
return [];
|
|
65
|
+
return statusRaw
|
|
66
|
+
.split('\n')
|
|
67
|
+
.filter(line => line.trim().length > 0)
|
|
68
|
+
.map(line => ({
|
|
69
|
+
status: parseFileStatus(line.slice(0, 2)),
|
|
70
|
+
path: line.slice(3),
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
const MAIN_BRANCH_REF = 'origin/main';
|
|
47
74
|
async function enrichWorktree(wt) {
|
|
48
|
-
const
|
|
75
|
+
const descriptionKey = wt.branch ? `branch.${wt.branch}.description` : null;
|
|
76
|
+
const [logRaw, statusRaw, aheadRaw, descriptionRaw, unpushedLogRaw, mergedBranchesRaw] = await Promise.all([
|
|
49
77
|
git(wt.path, ['log', '-1', '--format=%h%n%s%n%ct']),
|
|
50
78
|
git(wt.path, ['status', '--short']),
|
|
51
|
-
git(wt.path, ['rev-list', '--count',
|
|
79
|
+
git(wt.path, ['rev-list', '--count', `${MAIN_BRANCH_REF}..HEAD`]),
|
|
80
|
+
descriptionKey ? git(wt.path, ['config', descriptionKey]) : Promise.resolve(null),
|
|
81
|
+
git(wt.path, ['log', `${MAIN_BRANCH_REF}..HEAD`, '--oneline']),
|
|
82
|
+
wt.branch && wt.branch !== 'main' ? git(wt.path, ['branch', '--merged', MAIN_BRANCH_REF]) : Promise.resolve(null),
|
|
52
83
|
]);
|
|
53
84
|
const [hashLine, messageLine, timestampLine] = logRaw?.split('\n') ?? [];
|
|
54
85
|
const headHash = hashLine?.trim() || wt.head.slice(0, 7);
|
|
55
86
|
const headMessage = messageLine?.trim() ?? '';
|
|
56
87
|
const headTimestampMs = timestampLine ? parseInt(timestampLine.trim(), 10) * 1000 : 0;
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
: 0;
|
|
88
|
+
const changedFiles = statusRaw !== null ? parseChangedFiles(statusRaw) : [];
|
|
89
|
+
const changedCount = changedFiles.length;
|
|
60
90
|
const parsedAhead = aheadRaw !== null ? parseInt(aheadRaw, 10) : NaN;
|
|
61
91
|
const aheadCount = isNaN(parsedAhead) ? 0 : parsedAhead;
|
|
62
|
-
|
|
92
|
+
const unpushedCommits = unpushedLogRaw
|
|
93
|
+
? unpushedLogRaw.split('\n').filter(l => l.trim().length > 0).map(line => ({
|
|
94
|
+
hash: line.slice(0, 7),
|
|
95
|
+
message: line.slice(8),
|
|
96
|
+
}))
|
|
97
|
+
: [];
|
|
98
|
+
const isMerged = wt.branch !== null &&
|
|
99
|
+
wt.branch !== 'main' &&
|
|
100
|
+
mergedBranchesRaw !== null &&
|
|
101
|
+
mergedBranchesRaw.split('\n').some(line => line.trim() === wt.branch);
|
|
102
|
+
const branchDescription = descriptionRaw?.trim() ?? '';
|
|
103
|
+
return { headHash, headMessage, headTimestampMs, changedCount, changedFiles, aheadCount, unpushedCommits, isMerged, branchDescription };
|
|
63
104
|
}
|
|
64
105
|
async function resolveRepoRoot(path) {
|
|
65
|
-
|
|
106
|
+
const commonDir = await git(path, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
|
|
107
|
+
if (commonDir === null)
|
|
108
|
+
return null;
|
|
109
|
+
return commonDir.replace(/\/\.git\/?$/, '') || null;
|
|
66
110
|
}
|
|
67
111
|
async function enrichRepo(repoRoot, activeSessions) {
|
|
68
112
|
const porcelain = await git(repoRoot, ['worktree', 'list', '--porcelain']);
|
|
@@ -85,8 +129,12 @@ async function enrichRepo(repoRoot, activeSessions) {
|
|
|
85
129
|
headMessage: e.headMessage,
|
|
86
130
|
headTimestampMs: e.headTimestampMs,
|
|
87
131
|
changedCount: e.changedCount,
|
|
132
|
+
changedFiles: e.changedFiles,
|
|
88
133
|
aheadCount: e.aheadCount,
|
|
134
|
+
unpushedCommits: e.unpushedCommits,
|
|
135
|
+
isMerged: e.isMerged,
|
|
89
136
|
activeSessionCount: wt.branch ? (activeSessions.counts.get(wt.branch) ?? 0) : 0,
|
|
137
|
+
...(e.branchDescription ? { description: e.branchDescription } : {}),
|
|
90
138
|
}];
|
|
91
139
|
});
|
|
92
140
|
return [...worktrees].sort((a, b) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exaudeus/workrail",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.15.0",
|
|
4
4
|
"description": "Step-by-step workflow enforcement for AI agents via MCP",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"dev": "npm run build && node dist/mcp-server.js",
|
|
40
40
|
"watch": "tsc --watch",
|
|
41
41
|
"validate:workflows": "bash scripts/validate-workflows.sh",
|
|
42
|
-
"validate:registry": "node scripts/validate-workflows-registry.ts",
|
|
42
|
+
"validate:registry": "node --experimental-strip-types scripts/validate-workflows-registry.ts",
|
|
43
43
|
"stamp-workflow": "node scripts/stamp-workflow.ts",
|
|
44
44
|
"validate:authoring-spec": "node scripts/validate-authoring-spec.js",
|
|
45
45
|
"validate:authoring-docs": "node scripts/generate-authoring-docs.js --check",
|
package/spec/authoring-spec.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"version": 3,
|
|
4
4
|
"title": "WorkRail Authoring Rules",
|
|
5
5
|
"purpose": "Canonical current rules for authoring good WorkRail workflows. workflow.schema.json remains the source of truth for legal structure.",
|
|
6
|
-
"lastReviewed": "2026-
|
|
6
|
+
"lastReviewed": "2026-04-04",
|
|
7
7
|
"principles": [
|
|
8
8
|
"Schema defines what is valid. These rules define what is good.",
|
|
9
9
|
"Prefer current authoring rules over design rationale or historical notes.",
|
|
@@ -931,6 +931,87 @@
|
|
|
931
931
|
}
|
|
932
932
|
]
|
|
933
933
|
},
|
|
934
|
+
{
|
|
935
|
+
"id": "assessment-gates",
|
|
936
|
+
"title": "Assessment gates",
|
|
937
|
+
"rules": [
|
|
938
|
+
{
|
|
939
|
+
"id": "assessment-use-for-bounded-judgment",
|
|
940
|
+
"status": "active",
|
|
941
|
+
"level": "recommended",
|
|
942
|
+
"scope": [
|
|
943
|
+
"workflow.assessments",
|
|
944
|
+
"step.assessmentRefs",
|
|
945
|
+
"step.assessmentConsequences"
|
|
946
|
+
],
|
|
947
|
+
"rule": "Use assessment gates when a step needs the agent to submit a bounded, durable judgment before the workflow can safely advance -- not for generic scoring or ceremony.",
|
|
948
|
+
"why": "Assessment gates force explicit structured reasoning at a decision point and durably record the result. Generic scoring that never affects routing adds noise without value.",
|
|
949
|
+
"enforcement": ["advisory"],
|
|
950
|
+
"sourceRefs": [
|
|
951
|
+
{
|
|
952
|
+
"kind": "example",
|
|
953
|
+
"path": "workflows/mr-review-workflow.agentic.v2.json",
|
|
954
|
+
"note": "Uses a three-dimension readiness gate on the final validation step."
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
"kind": "example",
|
|
958
|
+
"path": "workflows/bug-investigation.agentic.v2.json",
|
|
959
|
+
"note": "Uses a single-dimension diagnosis readiness gate before handoff."
|
|
960
|
+
}
|
|
961
|
+
],
|
|
962
|
+
"checks": [
|
|
963
|
+
"The assessment is at a step where a wrong answer would produce a bad handoff.",
|
|
964
|
+
"At least one dimension has a consequence that keeps the step pending and requires follow-up.",
|
|
965
|
+
"The assessment result is durably recorded and useful to a future resume agent."
|
|
966
|
+
],
|
|
967
|
+
"antiPatterns": [
|
|
968
|
+
"Using assessments as a generic five-level scoring rubric with no routing consequence",
|
|
969
|
+
"Declaring an assessment but never referencing it from any step"
|
|
970
|
+
]
|
|
971
|
+
},
|
|
972
|
+
{
|
|
973
|
+
"id": "assessment-dimension-orthogonality",
|
|
974
|
+
"status": "active",
|
|
975
|
+
"level": "required",
|
|
976
|
+
"scope": [
|
|
977
|
+
"workflow.assessments"
|
|
978
|
+
],
|
|
979
|
+
"rule": "Each dimension in an assessment must capture a distinct failure mode that the other dimensions cannot catch. A dimension that restates existing workflow state (such as a generic 'confidence' dimension that mirrors the workflow's confidence band) adds ceremony without structure.",
|
|
980
|
+
"why": "The value of multiple dimensions is that each one independently blocks advancement for a different reason. Correlated or vague dimensions collapse to a single gate with extra steps.",
|
|
981
|
+
"enforcement": ["advisory"],
|
|
982
|
+
"checks": [
|
|
983
|
+
"Each dimension is independently checkable without needing to know the result of the others.",
|
|
984
|
+
"No dimension restates a context variable the workflow already computes (e.g. recommendationConfidenceBand).",
|
|
985
|
+
"A 'low' rating on any one dimension alone justifies a follow-up, regardless of the others."
|
|
986
|
+
],
|
|
987
|
+
"antiPatterns": [
|
|
988
|
+
"A single 'confidence' dimension that mirrors the workflow's existing confidence band",
|
|
989
|
+
"Multiple dimensions that all reduce to 'did I do a good job overall'",
|
|
990
|
+
"Dimensions so correlated that one being low always means the others are low too"
|
|
991
|
+
]
|
|
992
|
+
},
|
|
993
|
+
{
|
|
994
|
+
"id": "assessment-v1-constraints",
|
|
995
|
+
"status": "active",
|
|
996
|
+
"level": "required",
|
|
997
|
+
"scope": [
|
|
998
|
+
"step.assessmentRefs",
|
|
999
|
+
"step.assessmentConsequences"
|
|
1000
|
+
],
|
|
1001
|
+
"rule": "V1 supports exactly one assessmentRef per step and at most one assessmentConsequences entry per step. Use anyEqualsLevel as the trigger -- the engine checks all submitted dimensions and fires if any equals that level.",
|
|
1002
|
+
"why": "anyEqualsLevel is the only trigger form. It works for both single-dimension and multi-dimension assessments without requiring the author to choose between two forms.",
|
|
1003
|
+
"enforcement": ["schema"],
|
|
1004
|
+
"checks": [
|
|
1005
|
+
"No more than one assessmentRefs entry per step.",
|
|
1006
|
+
"No more than one assessmentConsequences entry per step.",
|
|
1007
|
+
"The consequence uses anyEqualsLevel to declare which level blocks -- not a named dimension."
|
|
1008
|
+
],
|
|
1009
|
+
"antiPatterns": [
|
|
1010
|
+
"Trying to encode multiple consequences via workarounds"
|
|
1011
|
+
]
|
|
1012
|
+
}
|
|
1013
|
+
]
|
|
1014
|
+
},
|
|
934
1015
|
{
|
|
935
1016
|
"id": "delegation",
|
|
936
1017
|
"title": "Delegation and subagents",
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./workflow-tags.schema.json",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"tags": [
|
|
5
|
+
{
|
|
6
|
+
"id": "coding",
|
|
7
|
+
"displayName": "Coding & Development",
|
|
8
|
+
"when": [
|
|
9
|
+
"implementing a feature or task",
|
|
10
|
+
"refactoring or improving existing code",
|
|
11
|
+
"converting code between platforms or languages"
|
|
12
|
+
],
|
|
13
|
+
"examples": ["coding-task-workflow-agentic", "cross-platform-code-conversion"]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "review_audit",
|
|
17
|
+
"displayName": "Review & Audit",
|
|
18
|
+
"when": [
|
|
19
|
+
"reviewing a merge request before merging",
|
|
20
|
+
"auditing a service for production readiness",
|
|
21
|
+
"checking architecture for scalability issues"
|
|
22
|
+
],
|
|
23
|
+
"examples": ["mr-review-workflow-agentic", "production-readiness-audit"]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "investigation",
|
|
27
|
+
"displayName": "Investigation & Debugging",
|
|
28
|
+
"when": [
|
|
29
|
+
"diagnosing a bug or unexpected behavior in code",
|
|
30
|
+
"diagnosing tool, environment, or MCP server issues"
|
|
31
|
+
],
|
|
32
|
+
"examples": ["bug-investigation-agentic", "workflow-diagnose-environment"]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "design",
|
|
36
|
+
"displayName": "Design & Discovery",
|
|
37
|
+
"when": [
|
|
38
|
+
"designing a UI or UX feature from scratch",
|
|
39
|
+
"exploring and thinking through a problem or decision",
|
|
40
|
+
"generating and comparing design candidates"
|
|
41
|
+
],
|
|
42
|
+
"examples": ["ui-ux-design-workflow", "wr.discovery"]
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "documentation",
|
|
46
|
+
"displayName": "Documentation",
|
|
47
|
+
"when": [
|
|
48
|
+
"creating new documentation for a project, feature, or API",
|
|
49
|
+
"updating or maintaining existing documentation",
|
|
50
|
+
"writing documentation for a single bounded component or concept"
|
|
51
|
+
],
|
|
52
|
+
"examples": ["document-creation-workflow", "documentation-update-workflow"]
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"id": "tickets",
|
|
56
|
+
"displayName": "Tickets & Planning",
|
|
57
|
+
"when": [
|
|
58
|
+
"creating Jira tickets for a feature or epic",
|
|
59
|
+
"grooming and refining a backlog",
|
|
60
|
+
"generating test cases from ticket requirements"
|
|
61
|
+
],
|
|
62
|
+
"examples": ["adaptive-ticket-creation", "ticket-grooming"]
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"id": "learning",
|
|
66
|
+
"displayName": "Learning & Personal",
|
|
67
|
+
"when": [
|
|
68
|
+
"creating learning materials or a course",
|
|
69
|
+
"designing a presentation",
|
|
70
|
+
"making a major personal decision like where to relocate"
|
|
71
|
+
],
|
|
72
|
+
"examples": ["personal-learning-materials-creation-branched", "presentation-creation"]
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"id": "routines",
|
|
76
|
+
"displayName": "Routines",
|
|
77
|
+
"when": [
|
|
78
|
+
"gathering context about a codebase",
|
|
79
|
+
"generating design candidates",
|
|
80
|
+
"running a hypothesis challenge or adversarial review",
|
|
81
|
+
"running a final verification pass"
|
|
82
|
+
],
|
|
83
|
+
"examples": ["routine-context-gathering", "routine-hypothesis-challenge"]
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"id": "authoring",
|
|
87
|
+
"displayName": "Workflow Authoring",
|
|
88
|
+
"when": [
|
|
89
|
+
"creating a new WorkRail workflow from scratch",
|
|
90
|
+
"modernizing or updating an existing workflow"
|
|
91
|
+
],
|
|
92
|
+
"examples": ["workflow-for-workflows"]
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
"workflows": {
|
|
96
|
+
"adaptive-ticket-creation": { "tags": ["tickets"] },
|
|
97
|
+
"architecture-scalability-audit": { "tags": ["review_audit"] },
|
|
98
|
+
"bug-investigation-agentic": { "tags": ["investigation"] },
|
|
99
|
+
"coding-task-workflow-agentic": { "tags": ["coding"] },
|
|
100
|
+
"cross-platform-code-conversion": { "tags": ["coding"] },
|
|
101
|
+
"document-creation-workflow": { "tags": ["documentation"] },
|
|
102
|
+
"documentation-update-workflow": { "tags": ["documentation"] },
|
|
103
|
+
"intelligent-test-case-generation": { "tags": ["tickets", "coding"] },
|
|
104
|
+
"mr-review-workflow-agentic": { "tags": ["review_audit"] },
|
|
105
|
+
"personal-learning-course-design": { "tags": ["learning"] },
|
|
106
|
+
"personal-learning-materials-creation-branched": { "tags": ["learning"] },
|
|
107
|
+
"presentation-creation": { "tags": ["learning"] },
|
|
108
|
+
"production-readiness-audit": { "tags": ["review_audit"] },
|
|
109
|
+
"relocation-workflow-us": { "tags": ["learning"] },
|
|
110
|
+
"routine-context-gathering": { "tags": ["routines"] },
|
|
111
|
+
"routine-design-review": { "tags": ["routines"] },
|
|
112
|
+
"routine-execution-simulation": { "tags": ["routines"] },
|
|
113
|
+
"routine-feature-implementation": { "tags": ["routines", "coding"] },
|
|
114
|
+
"routine-final-verification": { "tags": ["routines"] },
|
|
115
|
+
"routine-hypothesis-challenge": { "tags": ["routines"] },
|
|
116
|
+
"routine-ideation": { "tags": ["routines"] },
|
|
117
|
+
"routine-parallel-work-partitioning": { "tags": ["routines"] },
|
|
118
|
+
"routine-philosophy-alignment": { "tags": ["routines"] },
|
|
119
|
+
"routine-plan-analysis": { "tags": ["routines"] },
|
|
120
|
+
"routine-plan-generation": { "tags": ["routines"] },
|
|
121
|
+
"routine-tension-driven-design": { "tags": ["routines"] },
|
|
122
|
+
"scoped-documentation-workflow": { "tags": ["documentation"] },
|
|
123
|
+
"ticket-grooming": { "tags": ["tickets"] },
|
|
124
|
+
"ui-ux-design-workflow": { "tags": ["design"] },
|
|
125
|
+
"workflow-diagnose-environment": { "tags": ["investigation"] },
|
|
126
|
+
"workflow-for-workflows": { "tags": ["authoring"] },
|
|
127
|
+
"wr.discovery": { "tags": ["design", "investigation"] },
|
|
128
|
+
"test-artifact-loop-control": { "tags": ["coding"], "hidden": true },
|
|
129
|
+
"test-session-persistence": { "tags": ["coding"], "hidden": true },
|
|
130
|
+
"test-missing-context": { "tags": ["coding"], "hidden": true }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -1064,23 +1064,15 @@
|
|
|
1064
1064
|
},
|
|
1065
1065
|
"assessmentConsequenceTrigger": {
|
|
1066
1066
|
"type": "object",
|
|
1067
|
-
"description": "
|
|
1067
|
+
"description": "Fires when ANY dimension in the submitted assessment equals the given level. The consequence blocks advancement until the agent addresses all dimensions that scored at that level.",
|
|
1068
1068
|
"properties": {
|
|
1069
|
-
"
|
|
1070
|
-
"type": "string",
|
|
1071
|
-
"minLength": 1,
|
|
1072
|
-
"maxLength": 64
|
|
1073
|
-
},
|
|
1074
|
-
"equalsLevel": {
|
|
1069
|
+
"anyEqualsLevel": {
|
|
1075
1070
|
"type": "string",
|
|
1076
1071
|
"minLength": 1,
|
|
1077
1072
|
"maxLength": 64
|
|
1078
1073
|
}
|
|
1079
1074
|
},
|
|
1080
|
-
"required": [
|
|
1081
|
-
"dimensionId",
|
|
1082
|
-
"equalsLevel"
|
|
1083
|
-
],
|
|
1075
|
+
"required": ["anyEqualsLevel"],
|
|
1084
1076
|
"additionalProperties": false
|
|
1085
1077
|
},
|
|
1086
1078
|
"assessmentConsequenceEffect": {
|