@exaudeus/workrail 3.13.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.
Files changed (62) hide show
  1. package/dist/application/services/validation-engine.js +4 -9
  2. package/dist/application/services/workflow-compiler.js +4 -6
  3. package/dist/console/assets/index-BZYIjrzJ.js +28 -0
  4. package/dist/console/assets/index-OLCKbDdm.css +1 -0
  5. package/dist/console/index.html +2 -2
  6. package/dist/engine/engine-factory.js +2 -2
  7. package/dist/engine/types.d.ts +1 -1
  8. package/dist/manifest.json +63 -63
  9. package/dist/mcp/handlers/shared/request-workflow-reader.d.ts +5 -0
  10. package/dist/mcp/handlers/shared/request-workflow-reader.js +47 -2
  11. package/dist/mcp/handlers/v2-advance-core/assessment-consequences.d.ts +1 -1
  12. package/dist/mcp/handlers/v2-advance-core/assessment-consequences.js +4 -5
  13. package/dist/mcp/handlers/v2-advance-core/index.js +1 -1
  14. package/dist/mcp/handlers/v2-advance-core/outcome-blocked.js +1 -1
  15. package/dist/mcp/handlers/v2-execution/start.d.ts +1 -0
  16. package/dist/mcp/handlers/v2-execution/start.js +20 -1
  17. package/dist/mcp/handlers/v2-workflow.d.ts +23 -0
  18. package/dist/mcp/handlers/v2-workflow.js +177 -10
  19. package/dist/mcp/output-schemas.d.ts +202 -8
  20. package/dist/mcp/output-schemas.js +38 -11
  21. package/dist/mcp/server.js +48 -1
  22. package/dist/mcp/tool-descriptions.js +17 -9
  23. package/dist/mcp/v2/tools.d.ts +6 -0
  24. package/dist/mcp/v2/tools.js +2 -0
  25. package/dist/mcp/workflow-protocol-contracts.js +5 -1
  26. package/dist/types/workflow-definition.d.ts +2 -2
  27. package/dist/v2/infra/local/workspace-anchor/index.js +4 -1
  28. package/dist/v2/usecases/console-routes.js +49 -1
  29. package/dist/v2/usecases/console-service.d.ts +1 -0
  30. package/dist/v2/usecases/console-service.js +4 -1
  31. package/dist/v2/usecases/console-types.d.ts +12 -0
  32. package/dist/v2/usecases/worktree-service.js +55 -7
  33. package/package.json +3 -2
  34. package/spec/authoring-spec.json +91 -3
  35. package/spec/workflow-tags.json +132 -0
  36. package/spec/workflow.schema.json +411 -97
  37. package/workflows/adaptive-ticket-creation.json +40 -22
  38. package/workflows/architecture-scalability-audit.json +65 -31
  39. package/workflows/bug-investigation.agentic.v2.json +36 -14
  40. package/workflows/coding-task-workflow-agentic.json +50 -38
  41. package/workflows/coding-task-workflow-agentic.lean.v2.json +124 -37
  42. package/workflows/coding-task-workflow-agentic.v2.json +90 -30
  43. package/workflows/cross-platform-code-conversion.v2.json +168 -48
  44. package/workflows/document-creation-workflow.json +47 -17
  45. package/workflows/documentation-update-workflow.json +8 -8
  46. package/workflows/intelligent-test-case-generation.json +2 -2
  47. package/workflows/learner-centered-course-workflow.json +267 -267
  48. package/workflows/mr-review-workflow.agentic.v2.json +81 -14
  49. package/workflows/personal-learning-materials-creation-branched.json +175 -175
  50. package/workflows/presentation-creation.json +159 -159
  51. package/workflows/production-readiness-audit.json +54 -15
  52. package/workflows/relocation-workflow-us.json +44 -35
  53. package/workflows/routines/tension-driven-design.json +1 -1
  54. package/workflows/scoped-documentation-workflow.json +25 -25
  55. package/workflows/test-artifact-loop-control.json +1 -2
  56. package/workflows/ui-ux-design-workflow.json +327 -0
  57. package/workflows/workflow-diagnose-environment.json +1 -1
  58. package/workflows/workflow-for-workflows.json +507 -484
  59. package/workflows/workflow-for-workflows.v2.json +90 -18
  60. package/workflows/wr.discovery.json +112 -30
  61. package/dist/console/assets/index-DW78t31j.css +0 -1
  62. package/dist/console/assets/index-EsSXrC_a.js +0 -28
@@ -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<{
@@ -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 dimensionId: string;
27
- readonly equalsLevel: string;
26
+ readonly anyEqualsLevel: string;
28
27
  }
29
28
  export interface AssessmentFollowupRequiredEffectDefinition {
30
29
  readonly kind: 'require_followup';
@@ -134,6 +133,7 @@ export interface WorkflowDefinition {
134
133
  readonly assessments?: readonly AssessmentDefinition[];
135
134
  readonly extensionPoints?: readonly ExtensionPoint[];
136
135
  readonly references?: readonly WorkflowReference[];
136
+ readonly validatedAgainstSpecVersion?: number;
137
137
  }
138
138
  export declare function isLoopStepDefinition(step: WorkflowStepDefinition | LoopStepDefinition): step is LoopStepDefinition;
139
139
  export declare function isWorkflowStepDefinition(step: WorkflowStepDefinition | LoopStepDefinition): step is WorkflowStepDefinition;
@@ -33,7 +33,10 @@ class LocalWorkspaceAnchorV2 {
33
33
  }
34
34
  async runGitCommands(cwd) {
35
35
  const anchors = [];
36
- const repoRoot = await this.gitCommand('git rev-parse --show-toplevel', cwd);
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 repoRootSet = new Set(sessions.map(s => s.repoRoot).filter((r) => r !== null));
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 truncateTitle(val.trim());
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 [logRaw, statusRaw, aheadRaw] = await Promise.all([
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', 'origin/main..HEAD']),
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 changedCount = statusRaw !== null
58
- ? statusRaw.split('\n').filter(l => l.trim()).length
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
- return { headHash, headMessage, headTimestampMs, changedCount, aheadCount };
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
- return git(path, ['rev-parse', '--show-toplevel']);
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.13.0",
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,8 @@
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
+ "stamp-workflow": "node scripts/stamp-workflow.ts",
43
44
  "validate:authoring-spec": "node scripts/validate-authoring-spec.js",
44
45
  "validate:authoring-docs": "node scripts/generate-authoring-docs.js --check",
45
46
  "validate:workflow-discovery": "node scripts/validate-workflow-discovery.js",
@@ -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-03-21",
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.",
@@ -20,7 +20,8 @@
20
20
  "When a workflow feature lands or changes, update the schema and runtime first.",
21
21
  "Then update this authoring spec so guidance matches the shipped behavior.",
22
22
  "Then regenerate or update human-facing docs and examples derived from this spec.",
23
- "Do not leave authoring guidance behind the runtime."
23
+ "Do not leave authoring guidance behind the runtime.",
24
+ "Increment `version` when any required-level rule is added, removed, or materially changed. Add a `changelog` entry for the new version."
24
25
  ],
25
26
  "ruleModel": {
26
27
  "levels": [
@@ -930,6 +931,87 @@
930
931
  }
931
932
  ]
932
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
+ },
933
1015
  {
934
1016
  "id": "delegation",
935
1017
  "title": "Delegation and subagents",
@@ -1402,6 +1484,12 @@
1402
1484
  ]
1403
1485
  }
1404
1486
  ],
1405
- "plannedRules": [
1487
+ "plannedRules": [],
1488
+ "changelog": [
1489
+ {
1490
+ "version": 3,
1491
+ "date": "2026-03-21",
1492
+ "summary": "Baseline version at staleness feature introduction."
1493
+ }
1406
1494
  ]
1407
1495
  }
@@ -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
+ }