@exaudeus/workrail 3.40.0 → 3.42.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 (105) hide show
  1. package/dist/cli/commands/init.js +0 -3
  2. package/dist/cli-worktrain.js +48 -11
  3. package/dist/cli.js +0 -18
  4. package/dist/config/app-config.d.ts +0 -16
  5. package/dist/config/app-config.js +0 -14
  6. package/dist/config/config-file.js +0 -3
  7. package/dist/console-ui/assets/index-DGj8EsFR.css +1 -0
  8. package/dist/console-ui/assets/index-DwfWMKvv.js +28 -0
  9. package/dist/console-ui/index.html +2 -2
  10. package/dist/context-assembly/deps.d.ts +8 -0
  11. package/dist/context-assembly/deps.js +2 -0
  12. package/dist/context-assembly/index.d.ts +6 -0
  13. package/dist/context-assembly/index.js +50 -0
  14. package/dist/context-assembly/infra.d.ts +3 -0
  15. package/dist/context-assembly/infra.js +154 -0
  16. package/dist/context-assembly/types.d.ts +30 -0
  17. package/dist/context-assembly/types.js +2 -0
  18. package/dist/coordinators/pr-review.d.ts +20 -1
  19. package/dist/coordinators/pr-review.js +189 -4
  20. package/dist/daemon/daemon-events.d.ts +9 -1
  21. package/dist/daemon/soul-template.d.ts +2 -2
  22. package/dist/daemon/soul-template.js +11 -1
  23. package/dist/daemon/workflow-runner.d.ts +14 -1
  24. package/dist/daemon/workflow-runner.js +406 -25
  25. package/dist/di/container.js +1 -25
  26. package/dist/di/tokens.d.ts +0 -3
  27. package/dist/di/tokens.js +0 -3
  28. package/dist/domain/execution/state.d.ts +6 -6
  29. package/dist/engine/engine-factory.js +0 -1
  30. package/dist/infrastructure/console-defaults.d.ts +1 -0
  31. package/dist/infrastructure/console-defaults.js +4 -0
  32. package/dist/infrastructure/session/index.d.ts +0 -1
  33. package/dist/infrastructure/session/index.js +1 -3
  34. package/dist/manifest.json +138 -122
  35. package/dist/mcp/handlers/session.d.ts +1 -0
  36. package/dist/mcp/handlers/session.js +61 -13
  37. package/dist/mcp/handlers/v2-workflow.d.ts +2 -2
  38. package/dist/mcp/output-schemas.d.ts +234 -234
  39. package/dist/mcp/server.js +1 -18
  40. package/dist/mcp/tools.d.ts +2 -2
  41. package/dist/mcp/transports/http-entry.js +0 -2
  42. package/dist/mcp/transports/stdio-entry.js +1 -2
  43. package/dist/mcp/types.d.ts +0 -2
  44. package/dist/mcp/v2/tools.d.ts +24 -24
  45. package/dist/trigger/daemon-console.d.ts +2 -0
  46. package/dist/trigger/daemon-console.js +1 -1
  47. package/dist/trigger/trigger-listener.d.ts +2 -0
  48. package/dist/trigger/trigger-listener.js +3 -1
  49. package/dist/trigger/trigger-router.d.ts +4 -3
  50. package/dist/trigger/trigger-router.js +4 -3
  51. package/dist/trigger/trigger-store.js +17 -4
  52. package/dist/v2/durable-core/schemas/artifacts/assessment.d.ts +2 -2
  53. package/dist/v2/durable-core/schemas/artifacts/coordinator-signal.d.ts +2 -2
  54. package/dist/v2/durable-core/schemas/artifacts/loop-control.d.ts +6 -6
  55. package/dist/v2/durable-core/schemas/artifacts/review-verdict.d.ts +6 -6
  56. package/dist/v2/durable-core/schemas/compiled-workflow/index.d.ts +56 -56
  57. package/dist/v2/durable-core/schemas/execution-snapshot/blocked-snapshot.d.ts +83 -83
  58. package/dist/v2/durable-core/schemas/execution-snapshot/execution-snapshot.v1.d.ts +1024 -1024
  59. package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +2336 -2336
  60. package/dist/v2/durable-core/schemas/session/dag-topology.d.ts +6 -6
  61. package/dist/v2/durable-core/schemas/session/events.d.ts +339 -339
  62. package/dist/v2/durable-core/schemas/session/gaps.d.ts +30 -30
  63. package/dist/v2/durable-core/schemas/session/manifest.d.ts +6 -6
  64. package/dist/v2/durable-core/schemas/session/outputs.d.ts +8 -8
  65. package/dist/v2/durable-core/schemas/session/validation-event.d.ts +3 -3
  66. package/dist/v2/usecases/console-routes.d.ts +2 -1
  67. package/dist/v2/usecases/console-routes.js +29 -5
  68. package/dist/v2/usecases/console-service.js +14 -0
  69. package/dist/v2/usecases/console-types.d.ts +1 -0
  70. package/docs/authoring.md +16 -16
  71. package/docs/design/context-assembly-design-candidates.md +199 -0
  72. package/docs/design/context-assembly-implementation-plan.md +211 -0
  73. package/docs/design/context-assembly-review-findings.md +112 -0
  74. package/docs/design/coordinator-message-queue-drain-plan.md +241 -0
  75. package/docs/design/coordinator-message-queue-drain-review.md +120 -0
  76. package/docs/design/coordinator-message-queue-drain.md +289 -0
  77. package/docs/design/shaping-workflow-external-research.md +119 -0
  78. package/docs/discovery/late-bound-goals-impl-plan.md +147 -0
  79. package/docs/discovery/late-bound-goals-review.md +82 -0
  80. package/docs/discovery/late-bound-goals.md +118 -0
  81. package/docs/discovery/steer-endpoint-design-candidates.md +288 -0
  82. package/docs/discovery/steer-endpoint-design-review-findings.md +104 -0
  83. package/docs/discovery/steer-endpoint-implementation-plan.md +284 -0
  84. package/docs/ideas/backlog.md +356 -0
  85. package/docs/ideas/design-candidates-console-session-tree-impl.md +64 -0
  86. package/docs/ideas/design-candidates-session-tree-view.md +196 -0
  87. package/docs/ideas/design-review-findings-console-session-tree-impl.md +75 -0
  88. package/docs/ideas/design-review-findings-session-tree-view.md +88 -0
  89. package/docs/ideas/implementation_plan_session_tree_view.md +238 -0
  90. package/package.json +2 -1
  91. package/spec/authoring-spec.json +16 -16
  92. package/spec/shape.schema.json +178 -0
  93. package/spec/workflow-tags.json +232 -47
  94. package/workflows/coding-task-workflow-agentic.json +491 -480
  95. package/workflows/wr.shaping.json +182 -0
  96. package/dist/console-ui/assets/index-8dh0Psu-.css +0 -1
  97. package/dist/console-ui/assets/index-CXWCAonr.js +0 -28
  98. package/dist/infrastructure/session/DashboardHeartbeat.d.ts +0 -8
  99. package/dist/infrastructure/session/DashboardHeartbeat.js +0 -39
  100. package/dist/infrastructure/session/DashboardLockRelease.d.ts +0 -2
  101. package/dist/infrastructure/session/DashboardLockRelease.js +0 -29
  102. package/dist/infrastructure/session/HttpServer.d.ts +0 -60
  103. package/dist/infrastructure/session/HttpServer.js +0 -912
  104. package/workflows/coding-task-workflow-agentic.lean.v2.json +0 -648
  105. package/workflows/coding-task-workflow-agentic.v2.json +0 -324
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>WorkRail Console</title>
7
- <script type="module" crossorigin src="/console/assets/index-CXWCAonr.js"></script>
8
- <link rel="stylesheet" crossorigin href="/console/assets/index-8dh0Psu-.css">
7
+ <script type="module" crossorigin src="/console/assets/index-DwfWMKvv.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/console/assets/index-DGj8EsFR.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -0,0 +1,8 @@
1
+ import type { Result } from '../runtime/result.js';
2
+ import type { SessionNote } from './types.js';
3
+ export interface ContextAssemblerDeps {
4
+ readonly execGit: (args: readonly string[], cwd: string) => Promise<Result<string, string>>;
5
+ readonly execGh: (args: readonly string[], cwd: string) => Promise<Result<string, string>>;
6
+ readonly listRecentSessions: (workspacePath: string, limit: number) => Promise<Result<readonly SessionNote[], string>>;
7
+ readonly nowIso: () => string;
8
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,6 @@
1
+ import type { ContextAssemblerDeps } from './deps.js';
2
+ import type { ContextAssembler, ContextBundle, RenderOpts } from './types.js';
3
+ export declare function createContextAssembler(deps: ContextAssemblerDeps): ContextAssembler;
4
+ export declare function renderContextBundle(bundle: ContextBundle, _opts?: RenderOpts): string;
5
+ export type { AssemblyTask, ContextBundle, ContextAssembler, SessionNote, RenderOpts } from './types.js';
6
+ export type { ContextAssemblerDeps } from './deps.js';
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createContextAssembler = createContextAssembler;
4
+ exports.renderContextBundle = renderContextBundle;
5
+ function createContextAssembler(deps) {
6
+ return {
7
+ async assemble(task) {
8
+ const [gitDiff, priorSessionNotes] = await Promise.all([
9
+ assembleGitDiff(deps, task),
10
+ assemblePriorNotes(deps, task.workspacePath),
11
+ ]);
12
+ return {
13
+ task,
14
+ gitDiff,
15
+ priorSessionNotes,
16
+ assembledAt: deps.nowIso(),
17
+ };
18
+ },
19
+ };
20
+ }
21
+ async function assembleGitDiff(deps, task) {
22
+ if (task.kind === 'pr_review') {
23
+ const ghResult = await deps.execGh(['pr', 'diff', String(task.prNumber), '--name-only'], task.workspacePath);
24
+ if (ghResult.kind === 'ok' && ghResult.value.trim().length > 0) {
25
+ return { kind: 'ok', value: ghResult.value.trim() };
26
+ }
27
+ }
28
+ return deps.execGit(['diff', 'HEAD~1', '--stat'], task.workspacePath);
29
+ }
30
+ async function assemblePriorNotes(deps, workspacePath) {
31
+ const PRIOR_SESSION_LIMIT = 3;
32
+ return deps.listRecentSessions(workspacePath, PRIOR_SESSION_LIMIT);
33
+ }
34
+ function renderContextBundle(bundle, _opts) {
35
+ const parts = [];
36
+ if (bundle.priorSessionNotes.kind === 'ok' && bundle.priorSessionNotes.value.length > 0) {
37
+ parts.push('### Recent session notes for this workspace\n');
38
+ for (const note of bundle.priorSessionNotes.value) {
39
+ const title = note.sessionTitle ?? note.sessionId.slice(0, 12);
40
+ const branch = note.gitBranch ? ` (branch: ${note.gitBranch})` : '';
41
+ const recap = note.recapSnippet ?? '(no recap available)';
42
+ parts.push(`**${title}**${branch}\n${recap}\n`);
43
+ }
44
+ }
45
+ if (bundle.gitDiff.kind === 'ok' && bundle.gitDiff.value.trim().length > 0) {
46
+ parts.push('### Changed files\n');
47
+ parts.push('```\n' + bundle.gitDiff.value.trim() + '\n```\n');
48
+ }
49
+ return parts.join('\n');
50
+ }
@@ -0,0 +1,3 @@
1
+ import type { Result } from '../runtime/result.js';
2
+ import type { SessionNote } from './types.js';
3
+ export declare function createListRecentSessions(): (workspacePath: string, limit: number) => Promise<Result<readonly SessionNote[], string>>;
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.createListRecentSessions = createListRecentSessions;
37
+ const fs = __importStar(require("node:fs/promises"));
38
+ const nodePath = __importStar(require("node:path"));
39
+ const node_crypto_1 = require("node:crypto");
40
+ const neverthrow_1 = require("neverthrow");
41
+ const result_js_1 = require("../runtime/result.js");
42
+ const index_js_1 = require("../v2/infra/local/session-summary-provider/index.js");
43
+ const index_js_2 = require("../v2/infra/local/data-dir/index.js");
44
+ const index_js_3 = require("../v2/infra/local/directory-listing/index.js");
45
+ const index_js_4 = require("../v2/infra/local/session-store/index.js");
46
+ const index_js_5 = require("../v2/infra/local/sha256/index.js");
47
+ const WRITE_NOT_SUPPORTED = 'context-assembly: write ops not supported in read-only session store';
48
+ function makeReadOnlyFsPort() {
49
+ return {
50
+ readFileUtf8(filePath) {
51
+ return (0, neverthrow_1.fromPromise)(fs.readFile(filePath, 'utf-8'), (e) => {
52
+ const nodeErr = e;
53
+ if (nodeErr.code === 'ENOENT')
54
+ return { code: 'FS_NOT_FOUND', message: nodeErr.message ?? 'not found' };
55
+ return { code: 'FS_IO_ERROR', message: nodeErr.message ?? String(e) };
56
+ });
57
+ },
58
+ readFileBytes(filePath) {
59
+ return (0, neverthrow_1.fromPromise)(fs.readFile(filePath).then((buf) => new Uint8Array(buf)), (e) => {
60
+ const nodeErr = e;
61
+ if (nodeErr.code === 'ENOENT')
62
+ return { code: 'FS_NOT_FOUND', message: nodeErr.message ?? 'not found' };
63
+ return { code: 'FS_IO_ERROR', message: nodeErr.message ?? String(e) };
64
+ });
65
+ },
66
+ stat(_filePath) {
67
+ throw new Error(WRITE_NOT_SUPPORTED);
68
+ },
69
+ mkdirp(_dirPath) { throw new Error(WRITE_NOT_SUPPORTED); },
70
+ fsyncDir(_dirPath) { throw new Error(WRITE_NOT_SUPPORTED); },
71
+ openWriteTruncate(_filePath) { throw new Error(WRITE_NOT_SUPPORTED); },
72
+ openAppend(_filePath) { throw new Error(WRITE_NOT_SUPPORTED); },
73
+ openExclusive(_filePath, _bytes) { throw new Error(WRITE_NOT_SUPPORTED); },
74
+ writeAll(_fd, _bytes) { throw new Error(WRITE_NOT_SUPPORTED); },
75
+ fsyncFile(_fd) { throw new Error(WRITE_NOT_SUPPORTED); },
76
+ closeFile(_fd) { throw new Error(WRITE_NOT_SUPPORTED); },
77
+ rename(_from, _to) { throw new Error(WRITE_NOT_SUPPORTED); },
78
+ unlink(_filePath) { throw new Error(WRITE_NOT_SUPPORTED); },
79
+ writeFileBytes(_filePath, _bytes) { throw new Error(WRITE_NOT_SUPPORTED); },
80
+ readdir(_dirPath) { throw new Error(WRITE_NOT_SUPPORTED); },
81
+ readdirWithMtime(_dirPath) { throw new Error(WRITE_NOT_SUPPORTED); },
82
+ };
83
+ }
84
+ function makeDirectoryListingOpsPort() {
85
+ return {
86
+ readdir(dirPath) {
87
+ return (0, neverthrow_1.fromPromise)(fs.readdir(dirPath), (e) => {
88
+ const nodeErr = e;
89
+ if (nodeErr.code === 'ENOENT')
90
+ return { code: 'FS_NOT_FOUND', message: nodeErr.message ?? 'not found' };
91
+ return { code: 'FS_IO_ERROR', message: nodeErr.message ?? String(e) };
92
+ });
93
+ },
94
+ readdirWithMtime(dirPath) {
95
+ return (0, neverthrow_1.fromPromise)((async () => {
96
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
97
+ const withMtimes = await Promise.all(entries.map(async (entry) => {
98
+ try {
99
+ const stat = await fs.stat(nodePath.join(dirPath, entry.name));
100
+ return { name: entry.name, mtimeMs: stat.mtimeMs };
101
+ }
102
+ catch {
103
+ return null;
104
+ }
105
+ }));
106
+ return withMtimes.filter((e) => e !== null);
107
+ })(), (e) => {
108
+ const nodeErr = e;
109
+ if (nodeErr.code === 'ENOENT')
110
+ return { code: 'FS_NOT_FOUND', message: nodeErr.message ?? 'not found' };
111
+ return { code: 'FS_IO_ERROR', message: nodeErr.message ?? String(e) };
112
+ });
113
+ },
114
+ };
115
+ }
116
+ function createListRecentSessions() {
117
+ return async (workspacePath, limit) => {
118
+ try {
119
+ const dataDir = new index_js_2.LocalDataDirV2(process.env);
120
+ const directoryListingOps = makeDirectoryListingOpsPort();
121
+ const directoryListing = new index_js_3.LocalDirectoryListingV2(directoryListingOps);
122
+ const fsPort = makeReadOnlyFsPort();
123
+ const sha256 = new index_js_5.NodeSha256V2();
124
+ const sessionStore = new index_js_4.LocalSessionEventLogStoreV2(dataDir, fsPort, sha256);
125
+ const provider = new index_js_1.LocalSessionSummaryProviderV2({
126
+ directoryListing,
127
+ dataDir,
128
+ sessionStore,
129
+ });
130
+ const result = await provider.loadHealthySummaries();
131
+ if (result.isErr()) {
132
+ return (0, result_js_1.err)(`listRecentSessions: ${result.error.message}`);
133
+ }
134
+ const workspaceHash = `sha256:${(0, node_crypto_1.createHash)('sha256').update(workspacePath).digest('hex')}`;
135
+ const notes = result.value
136
+ .filter((s) => s.observations.repoRootHash === null ||
137
+ s.observations.repoRootHash === workspaceHash)
138
+ .slice()
139
+ .sort((a, b) => (b.lastModifiedMs ?? Date.now()) - (a.lastModifiedMs ?? Date.now()))
140
+ .slice(0, limit)
141
+ .map((s) => ({
142
+ sessionId: String(s.sessionId),
143
+ recapSnippet: s.recapSnippet != null ? String(s.recapSnippet) : null,
144
+ sessionTitle: s.sessionTitle,
145
+ gitBranch: s.observations.gitBranch,
146
+ lastModifiedMs: s.lastModifiedMs ?? Date.now(),
147
+ }));
148
+ return (0, result_js_1.ok)(notes);
149
+ }
150
+ catch (e) {
151
+ return (0, result_js_1.err)(`listRecentSessions error: ${e instanceof Error ? e.message : String(e)}`);
152
+ }
153
+ };
154
+ }
@@ -0,0 +1,30 @@
1
+ import type { Result } from '../runtime/result.js';
2
+ export type AssemblyTask = {
3
+ readonly kind: 'pr_review';
4
+ readonly prNumber: number;
5
+ readonly workspacePath: string;
6
+ readonly payloadBody?: string;
7
+ } | {
8
+ readonly kind: 'coding_task';
9
+ readonly issueNumber?: number;
10
+ readonly workspacePath: string;
11
+ readonly payloadBody?: string;
12
+ };
13
+ export interface SessionNote {
14
+ readonly sessionId: string;
15
+ readonly recapSnippet: string | null;
16
+ readonly sessionTitle: string | null;
17
+ readonly gitBranch: string | null;
18
+ readonly lastModifiedMs: number;
19
+ }
20
+ export interface ContextBundle {
21
+ readonly task: AssemblyTask;
22
+ readonly gitDiff: Result<string, string>;
23
+ readonly priorSessionNotes: Result<readonly SessionNote[], string>;
24
+ readonly assembledAt: string;
25
+ }
26
+ export interface RenderOpts {
27
+ }
28
+ export interface ContextAssembler {
29
+ assemble(task: AssemblyTask): Promise<ContextBundle>;
30
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,5 +1,6 @@
1
1
  import type { Result } from '../runtime/result.js';
2
2
  import type { AwaitResult } from '../cli/commands/worktrain-await.js';
3
+ import type { ContextAssembler } from '../context-assembly/types.js';
3
4
  export type ReviewSeverity = 'clean' | 'minor' | 'blocking' | 'unknown';
4
5
  export interface ReviewFindings {
5
6
  readonly severity: ReviewSeverity;
@@ -36,7 +37,8 @@ export interface PrReviewOpts {
36
37
  readonly port?: number;
37
38
  }
38
39
  export interface CoordinatorDeps {
39
- readonly spawnSession: (workflowId: string, goal: string, workspace: string) => Promise<Result<string, string>>;
40
+ readonly spawnSession: (workflowId: string, goal: string, workspace: string, context?: Readonly<Record<string, unknown>>) => Promise<Result<string, string>>;
41
+ readonly contextAssembler?: ContextAssembler;
40
42
  readonly awaitSessions: (handles: readonly string[], timeoutMs: number) => Promise<AwaitResult>;
41
43
  readonly getAgentResult: (sessionHandle: string) => Promise<{
42
44
  recapMarkdown: string | null;
@@ -48,6 +50,15 @@ export interface CoordinatorDeps {
48
50
  readonly stderr: (line: string) => void;
49
51
  readonly now: () => number;
50
52
  readonly port: number;
53
+ readonly readFile: (path: string) => Promise<string>;
54
+ readonly appendFile: (path: string, content: string) => Promise<void>;
55
+ readonly mkdir: (path: string, options: {
56
+ recursive: boolean;
57
+ }) => Promise<string | undefined>;
58
+ readonly homedir: () => string;
59
+ readonly joinPath: (...paths: string[]) => string;
60
+ readonly nowIso: () => string;
61
+ readonly generateId: () => string;
51
62
  }
52
63
  export declare function parseFindingsFromNotes(notes: string | null): Result<ReviewFindings, string>;
53
64
  export declare function readVerdictArtifact(artifacts: readonly unknown[], sessionHandle?: string): ReviewFindings | null;
@@ -59,4 +70,12 @@ export interface CoordinatorPortDiscoveryDeps {
59
70
  readonly homedir: () => string;
60
71
  readonly joinPath: (...paths: string[]) => string;
61
72
  }
73
+ export interface DrainResult {
74
+ readonly stop: boolean;
75
+ readonly stopReason: string | null;
76
+ readonly skipPrNumbers: readonly number[];
77
+ readonly addPrNumbers: readonly number[];
78
+ readonly messagesProcessed: number;
79
+ }
80
+ export declare function drainMessageQueue(deps: Pick<CoordinatorDeps, 'readFile' | 'appendFile' | 'writeFile' | 'mkdir' | 'homedir' | 'joinPath' | 'nowIso' | 'generateId' | 'stderr'>): Promise<DrainResult>;
62
81
  export declare function runPrReviewCoordinator(deps: CoordinatorDeps, opts: PrReviewOpts): Promise<CoordinatorResult>;
@@ -5,9 +5,11 @@ exports.readVerdictArtifact = readVerdictArtifact;
5
5
  exports.buildFixGoal = buildFixGoal;
6
6
  exports.formatElapsed = formatElapsed;
7
7
  exports.discoverConsolePort = discoverConsolePort;
8
+ exports.drainMessageQueue = drainMessageQueue;
8
9
  exports.runPrReviewCoordinator = runPrReviewCoordinator;
9
10
  const result_js_1 = require("../runtime/result.js");
10
11
  const review_verdict_js_1 = require("../v2/durable-core/schemas/artifacts/review-verdict.js");
12
+ const index_js_1 = require("../context-assembly/index.js");
11
13
  const MAX_FIX_PASSES = 3;
12
14
  const CHILD_SESSION_TIMEOUT_MS = 15 * 60 * 1000;
13
15
  const COORDINATOR_MAX_MS = 90 * 60 * 1000;
@@ -161,6 +163,126 @@ async function discoverConsolePort(deps, portOverride) {
161
163
  }
162
164
  return DEFAULT_CONSOLE_PORT;
163
165
  }
166
+ async function drainMessageQueue(deps) {
167
+ const workrailDir = deps.joinPath(deps.homedir(), '.workrail');
168
+ const queuePath = deps.joinPath(workrailDir, 'message-queue.jsonl');
169
+ const cursorPath = deps.joinPath(workrailDir, 'message-queue-cursor.json');
170
+ const outboxPath = deps.joinPath(workrailDir, 'outbox.jsonl');
171
+ let queueContent;
172
+ try {
173
+ queueContent = await deps.readFile(queuePath);
174
+ }
175
+ catch (err) {
176
+ if (isEnoentError(err)) {
177
+ return { stop: false, stopReason: null, skipPrNumbers: [], addPrNumbers: [], messagesProcessed: 0 };
178
+ }
179
+ deps.stderr(`[WARN coord:drain reason=read_failed] drainMessageQueue: could not read message queue: ${err instanceof Error ? err.message : String(err)}`);
180
+ return { stop: false, stopReason: null, skipPrNumbers: [], addPrNumbers: [], messagesProcessed: 0 };
181
+ }
182
+ const allLines = queueContent.split('\n').filter((line) => line.trim() !== '');
183
+ const parsedMessages = [];
184
+ for (const line of allLines) {
185
+ try {
186
+ const msg = JSON.parse(line);
187
+ parsedMessages.push(msg);
188
+ }
189
+ catch {
190
+ deps.stderr(`[WARN coord:drain reason=malformed_line] drainMessageQueue: skipped malformed JSONL line`);
191
+ }
192
+ }
193
+ const totalLines = parsedMessages.length;
194
+ let lastReadCount = 0;
195
+ try {
196
+ const cursorContent = await deps.readFile(cursorPath);
197
+ const cursor = JSON.parse(cursorContent);
198
+ if (typeof cursor.lastReadCount === 'number' && cursor.lastReadCount >= 0) {
199
+ lastReadCount = cursor.lastReadCount;
200
+ }
201
+ }
202
+ catch {
203
+ lastReadCount = 0;
204
+ }
205
+ if (lastReadCount > totalLines) {
206
+ lastReadCount = 0;
207
+ }
208
+ const newMessages = parsedMessages.slice(lastReadCount);
209
+ let stop = false;
210
+ let stopReason = null;
211
+ const skipSet = new Set();
212
+ const addSet = new Set();
213
+ const outboxEntries = [];
214
+ const STOP_RE = /^\s*stop\b/i;
215
+ const SKIP_PR_RE = /\bskip[- ]pr[\s#]+([0-9]+)/i;
216
+ const ADD_PR_RE = /\badd[- ]pr[\s#]+([0-9]+)/i;
217
+ for (const msg of newMessages) {
218
+ const text = msg.message;
219
+ if (STOP_RE.test(text)) {
220
+ stop = true;
221
+ stopReason = text;
222
+ deps.stderr(`[INFO coord:drain kind=stop ts=${msg.timestamp}] drainMessageQueue: stop signal received -- message: "${text}"`);
223
+ outboxEntries.push(JSON.stringify({
224
+ id: deps.generateId(),
225
+ message: `WorkTrain coordinator stopped by queued message: "${text}" (queued at ${msg.timestamp})`,
226
+ timestamp: deps.nowIso(),
227
+ }) + '\n');
228
+ continue;
229
+ }
230
+ const skipMatch = SKIP_PR_RE.exec(text);
231
+ if (skipMatch !== null) {
232
+ const prNum = parseInt(skipMatch[1], 10);
233
+ skipSet.add(prNum);
234
+ deps.stderr(`[INFO coord:drain kind=skip-pr prNumber=${prNum} ts=${msg.timestamp}] drainMessageQueue: skip-pr signal received -- message: "${text}"`);
235
+ outboxEntries.push(JSON.stringify({
236
+ id: deps.generateId(),
237
+ message: `WorkTrain coordinator skipping PR #${prNum} per queued message: "${text}" (queued at ${msg.timestamp})`,
238
+ timestamp: deps.nowIso(),
239
+ }) + '\n');
240
+ continue;
241
+ }
242
+ const addMatch = ADD_PR_RE.exec(text);
243
+ if (addMatch !== null) {
244
+ const prNum = parseInt(addMatch[1], 10);
245
+ addSet.add(prNum);
246
+ deps.stderr(`[INFO coord:drain kind=add-pr prNumber=${prNum} ts=${msg.timestamp}] drainMessageQueue: add-pr signal received -- message: "${text}"`);
247
+ outboxEntries.push(JSON.stringify({
248
+ id: deps.generateId(),
249
+ message: `WorkTrain coordinator adding PR #${prNum} per queued message: "${text}" (queued at ${msg.timestamp})`,
250
+ timestamp: deps.nowIso(),
251
+ }) + '\n');
252
+ continue;
253
+ }
254
+ }
255
+ if (outboxEntries.length > 0) {
256
+ try {
257
+ await deps.mkdir(workrailDir, { recursive: true });
258
+ await deps.appendFile(outboxPath, outboxEntries.join(''));
259
+ }
260
+ catch (err) {
261
+ deps.stderr(`[WARN coord:drain reason=outbox_write_failed] drainMessageQueue: could not write outbox notifications: ${err instanceof Error ? err.message : String(err)}`);
262
+ }
263
+ }
264
+ const newCursor = JSON.stringify({ lastReadCount: totalLines }, null, 2) + '\n';
265
+ try {
266
+ await deps.mkdir(workrailDir, { recursive: true });
267
+ await deps.writeFile(cursorPath, newCursor);
268
+ }
269
+ catch (err) {
270
+ deps.stderr(`[WARN coord:drain reason=cursor_write_failed] drainMessageQueue: could not update cursor: ${err instanceof Error ? err.message : String(err)}`);
271
+ }
272
+ return {
273
+ stop,
274
+ stopReason,
275
+ skipPrNumbers: [...skipSet],
276
+ addPrNumbers: [...addSet],
277
+ messagesProcessed: newMessages.length,
278
+ };
279
+ }
280
+ function isEnoentError(err) {
281
+ return (err !== null &&
282
+ typeof err === 'object' &&
283
+ 'code' in err &&
284
+ err.code === 'ENOENT');
285
+ }
164
286
  async function runPrReviewCoordinator(deps, opts) {
165
287
  const coordinatorStartMs = deps.now();
166
288
  const today = new Date(deps.now()).toISOString().slice(0, 10);
@@ -170,6 +292,30 @@ async function runPrReviewCoordinator(deps, opts) {
170
292
  deps.stderr(line);
171
293
  reportLines.push(line);
172
294
  }
295
+ const drainResult = await drainMessageQueue(deps);
296
+ if (drainResult.messagesProcessed > 0) {
297
+ const skipStr = drainResult.skipPrNumbers.length > 0
298
+ ? `, skip=[${drainResult.skipPrNumbers.join(',')}]`
299
+ : '';
300
+ const addStr = drainResult.addPrNumbers.length > 0
301
+ ? `, add=[${drainResult.addPrNumbers.join(',')}]`
302
+ : '';
303
+ log(`[drain] processed ${drainResult.messagesProcessed} message(s)${skipStr}${addStr}${drainResult.stop ? ', STOP SIGNAL' : ''}`);
304
+ }
305
+ if (drainResult.stop) {
306
+ const stopMsg = drainResult.stopReason ?? 'stop signal in message queue';
307
+ log(` STOP: coordinator halted by queued message: "${stopMsg}"`);
308
+ const result = {
309
+ reviewed: 0,
310
+ approved: 0,
311
+ escalated: 0,
312
+ mergedPrs: [],
313
+ reportPath,
314
+ hasErrors: false,
315
+ };
316
+ await writeReport(deps, reportPath, reportLines, result);
317
+ return result;
318
+ }
173
319
  log('[1/3] Gathering open PRs...');
174
320
  const stageStart = deps.now();
175
321
  let prs;
@@ -183,6 +329,25 @@ async function runPrReviewCoordinator(deps, opts) {
183
329
  else {
184
330
  prs = await deps.listOpenPRs(opts.workspace);
185
331
  }
332
+ if (drainResult.addPrNumbers.length > 0) {
333
+ const existingNums = new Set(prs.map((p) => p.number));
334
+ for (const addNum of drainResult.addPrNumbers) {
335
+ if (!existingNums.has(addNum)) {
336
+ prs = [...prs, { number: addNum, title: `PR #${addNum}`, headRef: '' }];
337
+ existingNums.add(addNum);
338
+ log(` [drain] added PR #${addNum} from message queue`);
339
+ }
340
+ }
341
+ }
342
+ if (drainResult.skipPrNumbers.length > 0) {
343
+ const skipSet = new Set(drainResult.skipPrNumbers);
344
+ const prsBefore = prs.length;
345
+ prs = prs.filter((p) => !skipSet.has(p.number));
346
+ const skippedCount = prsBefore - prs.length;
347
+ if (skippedCount > 0) {
348
+ log(` [drain] skipped ${skippedCount} PR(s) from message queue: [${[...skipSet].join(',')}]`);
349
+ }
350
+ }
186
351
  log(` done (${formatElapsed(deps.now() - stageStart)}) -- ${prs.length} PR(s) found`);
187
352
  if (prs.length === 0) {
188
353
  const result = {
@@ -213,13 +378,33 @@ async function runPrReviewCoordinator(deps, opts) {
213
378
  }
214
379
  const reviewHandles = new Map();
215
380
  const spawnErrors = new Map();
381
+ const spawnContexts = new Map();
216
382
  for (const pr of prs) {
217
383
  const goal = `Review PR #${pr.number} "${pr.title}" before merge`;
218
384
  if (opts.dryRun) {
219
385
  log(` PR #${pr.number} [dry-run] would spawn mr-review-workflow-agentic`);
220
386
  continue;
221
387
  }
222
- const spawnResult = await deps.spawnSession('mr-review-workflow-agentic', goal, opts.workspace);
388
+ let spawnContext;
389
+ if (deps.contextAssembler) {
390
+ const bundle = await deps.contextAssembler.assemble({
391
+ kind: 'pr_review',
392
+ prNumber: pr.number,
393
+ workspacePath: opts.workspace,
394
+ });
395
+ const rendered = (0, index_js_1.renderContextBundle)(bundle);
396
+ if (rendered.trim().length > 0) {
397
+ spawnContext = { assembledContextSummary: rendered };
398
+ spawnContexts.set(pr.number, spawnContext);
399
+ }
400
+ if (bundle.gitDiff.kind === 'err') {
401
+ deps.stderr(`[WARN coord:context prNumber=${pr.number}] gitDiff failed: ${bundle.gitDiff.error}`);
402
+ }
403
+ if (bundle.priorSessionNotes.kind === 'err') {
404
+ deps.stderr(`[WARN coord:context prNumber=${pr.number}] priorSessionNotes failed: ${bundle.priorSessionNotes.error}`);
405
+ }
406
+ }
407
+ const spawnResult = await deps.spawnSession('mr-review-workflow-agentic', goal, opts.workspace, spawnContext);
223
408
  if (spawnResult.kind === 'err') {
224
409
  spawnErrors.set(pr.number, spawnResult.error);
225
410
  log(` PR #${pr.number} spawn failed: ${spawnResult.error}`);
@@ -298,7 +483,7 @@ async function runPrReviewCoordinator(deps, opts) {
298
483
  sessionHandles: [handle],
299
484
  };
300
485
  if (severity === 'minor' && findings && sessionResult.outcome === 'success') {
301
- const processedOutcome = await runFixAgentLoop(deps, opts, pr, findings, outcome, coordinatorStartMs, log);
486
+ const processedOutcome = await runFixAgentLoop(deps, opts, pr, findings, outcome, coordinatorStartMs, log, spawnContexts.get(prNum));
302
487
  outcomes.set(prNum, processedOutcome);
303
488
  }
304
489
  else {
@@ -373,7 +558,7 @@ async function runPrReviewCoordinator(deps, opts) {
373
558
  await writeReport(deps, reportPath, reportLines, result);
374
559
  return result;
375
560
  }
376
- async function runFixAgentLoop(deps, opts, pr, initialFindings, initialOutcome, coordinatorStartMs, log) {
561
+ async function runFixAgentLoop(deps, opts, pr, initialFindings, initialOutcome, coordinatorStartMs, log, reviewSpawnContext) {
377
562
  let passCount = 0;
378
563
  let currentFindings = initialFindings;
379
564
  let sessionHandles = [...initialOutcome.sessionHandles];
@@ -450,7 +635,7 @@ async function runFixAgentLoop(deps, opts, pr, initialFindings, initialOutcome,
450
635
  };
451
636
  }
452
637
  const reReviewGoal = `Re-review PR #${pr.number} after fixes (pass ${passCount})`;
453
- const reReviewSpawnResult = await deps.spawnSession('mr-review-workflow-agentic', reReviewGoal, opts.workspace);
638
+ const reReviewSpawnResult = await deps.spawnSession('mr-review-workflow-agentic', reReviewGoal, opts.workspace, reviewSpawnContext);
454
639
  if (reReviewSpawnResult.kind === 'err') {
455
640
  log(` PR #${pr.number} -> re-review spawn failed: ${reReviewSpawnResult.error}`);
456
641
  return {
@@ -101,6 +101,14 @@ export interface ToolCallFailedEvent {
101
101
  readonly errorMessage: string;
102
102
  readonly workrailSessionId?: string;
103
103
  }
104
+ export interface SignalEmittedEvent {
105
+ readonly kind: 'signal_emitted';
106
+ readonly sessionId: string;
107
+ readonly signalKind: string;
108
+ readonly signalId: string;
109
+ readonly payload: Readonly<Record<string, unknown>>;
110
+ readonly workrailSessionId?: string;
111
+ }
104
112
  export interface AgentStuckEvent {
105
113
  readonly kind: 'agent_stuck';
106
114
  readonly sessionId: string;
@@ -110,7 +118,7 @@ export interface AgentStuckEvent {
110
118
  readonly argsSummary?: string;
111
119
  readonly workrailSessionId?: string;
112
120
  }
113
- export type DaemonEvent = DaemonStartedEvent | TriggerFiredEvent | SessionQueuedEvent | SessionStartedEvent | ToolCalledEvent | ToolErrorEvent | StepAdvancedEvent | SessionCompletedEvent | DeliveryAttemptedEvent | IssueReportedEvent | LlmTurnStartedEvent | LlmTurnCompletedEvent | ToolCallStartedEvent | ToolCallCompletedEvent | ToolCallFailedEvent | AgentStuckEvent;
121
+ export type DaemonEvent = DaemonStartedEvent | TriggerFiredEvent | SessionQueuedEvent | SessionStartedEvent | ToolCalledEvent | ToolErrorEvent | StepAdvancedEvent | SessionCompletedEvent | DeliveryAttemptedEvent | IssueReportedEvent | LlmTurnStartedEvent | LlmTurnCompletedEvent | ToolCallStartedEvent | ToolCallCompletedEvent | ToolCallFailedEvent | AgentStuckEvent | SignalEmittedEvent;
114
122
  export declare class DaemonEventEmitter {
115
123
  private readonly _dir;
116
124
  constructor(dirOverride?: string);
@@ -1,2 +1,2 @@
1
- export declare const DAEMON_SOUL_DEFAULT = "- Write code that follows the patterns already established in the codebase\n- Never skip tests. Run existing tests before and after changes\n- Prefer small, focused changes over large rewrites\n- If a step asks you to write code, write actual code -- do not write pseudocode or placeholders\n- Commit your work when you complete a logical unit";
2
- export declare const DAEMON_SOUL_TEMPLATE = "# WorkRail Daemon Soul\n#\n# This file is injected into every WorkRail Auto daemon session system prompt under\n# \"## Agent Rules and Philosophy\". Edit it to customize the agent's behavior for\n# your environment: coding conventions, commit style, tool preferences, etc.\n#\n# Changes take effect on the next daemon session -- no restart required.\n#\n# The defaults below reflect general best practices. Override them freely.\n\n- Write code that follows the patterns already established in the codebase\n- Never skip tests. Run existing tests before and after changes\n- Prefer small, focused changes over large rewrites\n- If a step asks you to write code, write actual code -- do not write pseudocode or placeholders\n- Commit your work when you complete a logical unit\n";
1
+ export declare const DAEMON_SOUL_DEFAULT = "- Write code that follows the patterns already established in the codebase\n- Never skip tests. Run existing tests before and after changes\n- Prefer small, focused changes over large rewrites\n- If a step asks you to write code, write actual code -- do not write pseudocode or placeholders\n- Commit your work when you complete a logical unit\n\n## File work\n- File search: Use Glob (NOT find or ls)\n- Content search: Use Grep (NOT grep or rg)\n- Read files: Use Read (NOT cat/head/tail)\n- Edit files: Use Edit (NOT sed/awk)\n- Write files: Use Write (NOT echo >/cat <<EOF)\n- Always Read a file before Edit. Edit requires the file to have been read in this session.\n- Use Edit for targeted in-place changes. Use Write only for new files or full rewrites.\n- Grep output_mode: use \"files_with_matches\" to find which files, then Read the relevant ones.";
2
+ export declare const DAEMON_SOUL_TEMPLATE = "# WorkRail Daemon Soul\n#\n# This file is injected into every WorkRail Auto daemon session system prompt under\n# \"## Agent Rules and Philosophy\". Edit it to customize the agent's behavior for\n# your environment: coding conventions, commit style, tool preferences, etc.\n#\n# Changes take effect on the next daemon session -- no restart required.\n#\n# The defaults below reflect general best practices. Override them freely.\n\n- Write code that follows the patterns already established in the codebase\n- Never skip tests. Run existing tests before and after changes\n- Prefer small, focused changes over large rewrites\n- If a step asks you to write code, write actual code -- do not write pseudocode or placeholders\n- Commit your work when you complete a logical unit\n\n## File work\n- File search: Use Glob (NOT find or ls)\n- Content search: Use Grep (NOT grep or rg)\n- Read files: Use Read (NOT cat/head/tail)\n- Edit files: Use Edit (NOT sed/awk)\n- Write files: Use Write (NOT echo >/cat <<EOF)\n- Always Read a file before Edit. Edit requires the file to have been read in this session.\n- Use Edit for targeted in-place changes. Use Write only for new files or full rewrites.\n- Grep output_mode: use \"files_with_matches\" to find which files, then Read the relevant ones.\n";
@@ -6,7 +6,17 @@ exports.DAEMON_SOUL_DEFAULT = `\
6
6
  - Never skip tests. Run existing tests before and after changes
7
7
  - Prefer small, focused changes over large rewrites
8
8
  - If a step asks you to write code, write actual code -- do not write pseudocode or placeholders
9
- - Commit your work when you complete a logical unit`;
9
+ - Commit your work when you complete a logical unit
10
+
11
+ ## File work
12
+ - File search: Use Glob (NOT find or ls)
13
+ - Content search: Use Grep (NOT grep or rg)
14
+ - Read files: Use Read (NOT cat/head/tail)
15
+ - Edit files: Use Edit (NOT sed/awk)
16
+ - Write files: Use Write (NOT echo >/cat <<EOF)
17
+ - Always Read a file before Edit. Edit requires the file to have been read in this session.
18
+ - Use Edit for targeted in-place changes. Use Write only for new files or full rewrites.
19
+ - Grep output_mode: use "files_with_matches" to find which files, then Read the relevant ones.`;
10
20
  exports.DAEMON_SOUL_TEMPLATE = `\
11
21
  # WorkRail Daemon Soul
12
22
  #