@exaudeus/workrail 3.41.0 → 3.43.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/cli-worktrain.js +40 -11
- package/dist/console-ui/assets/{index-CQt4UhPB.js → index-Sb57DW4B.js} +1 -1
- package/dist/console-ui/index.html +1 -1
- package/dist/context-assembly/deps.d.ts +8 -0
- package/dist/context-assembly/deps.js +2 -0
- package/dist/context-assembly/index.d.ts +6 -0
- package/dist/context-assembly/index.js +50 -0
- package/dist/context-assembly/infra.d.ts +3 -0
- package/dist/context-assembly/infra.js +154 -0
- package/dist/context-assembly/types.d.ts +30 -0
- package/dist/context-assembly/types.js +2 -0
- package/dist/coordinators/pr-review.d.ts +3 -1
- package/dist/coordinators/pr-review.js +25 -4
- package/dist/daemon/workflow-runner.d.ts +11 -1
- package/dist/daemon/workflow-runner.js +82 -9
- package/dist/domain/execution/state.d.ts +6 -6
- package/dist/manifest.json +76 -44
- package/dist/mcp/handlers/v2-workflow.d.ts +2 -2
- package/dist/mcp/output-schemas.d.ts +234 -234
- package/dist/mcp/tools.d.ts +2 -2
- package/dist/mcp/v2/tools.d.ts +24 -24
- package/dist/trigger/delivery-action.d.ts +2 -0
- package/dist/trigger/delivery-action.js +24 -0
- package/dist/trigger/trigger-router.js +24 -1
- package/dist/trigger/trigger-store.js +42 -0
- package/dist/trigger/types.d.ts +3 -0
- package/dist/v2/durable-core/schemas/artifacts/assessment.d.ts +2 -2
- package/dist/v2/durable-core/schemas/artifacts/coordinator-signal.d.ts +2 -2
- package/dist/v2/durable-core/schemas/artifacts/loop-control.d.ts +6 -6
- package/dist/v2/durable-core/schemas/artifacts/review-verdict.d.ts +6 -6
- package/dist/v2/durable-core/schemas/compiled-workflow/index.d.ts +56 -56
- package/dist/v2/durable-core/schemas/execution-snapshot/blocked-snapshot.d.ts +83 -83
- package/dist/v2/durable-core/schemas/execution-snapshot/execution-snapshot.v1.d.ts +1024 -1024
- package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +2336 -2336
- package/dist/v2/durable-core/schemas/session/dag-topology.d.ts +6 -6
- package/dist/v2/durable-core/schemas/session/events.d.ts +339 -339
- package/dist/v2/durable-core/schemas/session/gaps.d.ts +30 -30
- package/dist/v2/durable-core/schemas/session/manifest.d.ts +6 -6
- package/dist/v2/durable-core/schemas/session/outputs.d.ts +8 -8
- package/dist/v2/durable-core/schemas/session/validation-event.d.ts +3 -3
- package/docs/design/adaptive-coordinator-context-candidates.md +265 -0
- package/docs/design/adaptive-coordinator-context-review.md +101 -0
- package/docs/design/adaptive-coordinator-context.md +504 -0
- package/docs/design/adaptive-coordinator-routing-candidates.md +340 -0
- package/docs/design/adaptive-coordinator-routing-design-review.md +135 -0
- package/docs/design/adaptive-coordinator-routing-review.md +156 -0
- package/docs/design/adaptive-coordinator-routing.md +660 -0
- package/docs/design/context-assembly-design-candidates.md +199 -0
- package/docs/design/context-assembly-implementation-plan.md +211 -0
- package/docs/design/context-assembly-layer-design-review.md +110 -0
- package/docs/design/context-assembly-layer.md +622 -0
- package/docs/design/context-assembly-review-findings.md +112 -0
- package/docs/design/stuck-escalation-candidates.md +176 -0
- package/docs/design/stuck-escalation-design-review.md +70 -0
- package/docs/design/stuck-escalation.md +326 -0
- package/docs/design/worktrain-task-queue-candidates.md +252 -0
- package/docs/design/worktrain-task-queue-design-review.md +109 -0
- package/docs/design/worktrain-task-queue.md +443 -0
- package/docs/design/worktree-review-findings-candidates.md +101 -0
- package/docs/design/worktree-review-findings-design-review.md +65 -0
- package/docs/design/worktree-review-findings-implementation-plan.md +153 -0
- package/docs/ideas/backlog.md +212 -0
- package/package.json +3 -3
|
@@ -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
|
+
}
|
|
@@ -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;
|
|
@@ -9,6 +9,7 @@ exports.drainMessageQueue = drainMessageQueue;
|
|
|
9
9
|
exports.runPrReviewCoordinator = runPrReviewCoordinator;
|
|
10
10
|
const result_js_1 = require("../runtime/result.js");
|
|
11
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");
|
|
12
13
|
const MAX_FIX_PASSES = 3;
|
|
13
14
|
const CHILD_SESSION_TIMEOUT_MS = 15 * 60 * 1000;
|
|
14
15
|
const COORDINATOR_MAX_MS = 90 * 60 * 1000;
|
|
@@ -377,13 +378,33 @@ async function runPrReviewCoordinator(deps, opts) {
|
|
|
377
378
|
}
|
|
378
379
|
const reviewHandles = new Map();
|
|
379
380
|
const spawnErrors = new Map();
|
|
381
|
+
const spawnContexts = new Map();
|
|
380
382
|
for (const pr of prs) {
|
|
381
383
|
const goal = `Review PR #${pr.number} "${pr.title}" before merge`;
|
|
382
384
|
if (opts.dryRun) {
|
|
383
385
|
log(` PR #${pr.number} [dry-run] would spawn mr-review-workflow-agentic`);
|
|
384
386
|
continue;
|
|
385
387
|
}
|
|
386
|
-
|
|
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);
|
|
387
408
|
if (spawnResult.kind === 'err') {
|
|
388
409
|
spawnErrors.set(pr.number, spawnResult.error);
|
|
389
410
|
log(` PR #${pr.number} spawn failed: ${spawnResult.error}`);
|
|
@@ -462,7 +483,7 @@ async function runPrReviewCoordinator(deps, opts) {
|
|
|
462
483
|
sessionHandles: [handle],
|
|
463
484
|
};
|
|
464
485
|
if (severity === 'minor' && findings && sessionResult.outcome === 'success') {
|
|
465
|
-
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));
|
|
466
487
|
outcomes.set(prNum, processedOutcome);
|
|
467
488
|
}
|
|
468
489
|
else {
|
|
@@ -537,7 +558,7 @@ async function runPrReviewCoordinator(deps, opts) {
|
|
|
537
558
|
await writeReport(deps, reportPath, reportLines, result);
|
|
538
559
|
return result;
|
|
539
560
|
}
|
|
540
|
-
async function runFixAgentLoop(deps, opts, pr, initialFindings, initialOutcome, coordinatorStartMs, log) {
|
|
561
|
+
async function runFixAgentLoop(deps, opts, pr, initialFindings, initialOutcome, coordinatorStartMs, log, reviewSpawnContext) {
|
|
541
562
|
let passCount = 0;
|
|
542
563
|
let currentFindings = initialFindings;
|
|
543
564
|
let sessionHandles = [...initialOutcome.sessionHandles];
|
|
@@ -614,7 +635,7 @@ async function runFixAgentLoop(deps, opts, pr, initialFindings, initialOutcome,
|
|
|
614
635
|
};
|
|
615
636
|
}
|
|
616
637
|
const reReviewGoal = `Re-review PR #${pr.number} after fixes (pass ${passCount})`;
|
|
617
|
-
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);
|
|
618
639
|
if (reReviewSpawnResult.kind === 'err') {
|
|
619
640
|
log(` PR #${pr.number} -> re-review spawn failed: ${reReviewSpawnResult.error}`);
|
|
620
641
|
return {
|
|
@@ -6,6 +6,7 @@ import type { DaemonRegistry } from '../v2/infra/in-memory/daemon-registry/index
|
|
|
6
6
|
import type { V2StartWorkflowOutputSchema } from '../mcp/output-schemas.js';
|
|
7
7
|
import type { DaemonEventEmitter } from './daemon-events.js';
|
|
8
8
|
export declare const DAEMON_SESSIONS_DIR: string;
|
|
9
|
+
export declare const WORKTREES_DIR: string;
|
|
9
10
|
export { DAEMON_SOUL_DEFAULT, DAEMON_SOUL_TEMPLATE } from './soul-template.js';
|
|
10
11
|
export type ReadFileState = {
|
|
11
12
|
content: string;
|
|
@@ -28,6 +29,9 @@ export interface WorkflowTrigger {
|
|
|
28
29
|
readonly parentSessionId?: string;
|
|
29
30
|
readonly spawnDepth?: number;
|
|
30
31
|
readonly soulFile?: string;
|
|
32
|
+
readonly branchStrategy?: 'worktree' | 'none';
|
|
33
|
+
readonly baseBranch?: string;
|
|
34
|
+
readonly branchPrefix?: string;
|
|
31
35
|
}
|
|
32
36
|
export interface WorkflowRunSuccess {
|
|
33
37
|
readonly _tag: 'success';
|
|
@@ -35,6 +39,8 @@ export interface WorkflowRunSuccess {
|
|
|
35
39
|
readonly stopReason: string;
|
|
36
40
|
readonly lastStepNotes?: string;
|
|
37
41
|
readonly lastStepArtifacts?: readonly unknown[];
|
|
42
|
+
readonly sessionWorkspacePath?: string;
|
|
43
|
+
readonly sessionId?: string;
|
|
38
44
|
}
|
|
39
45
|
export interface WorkflowRunError {
|
|
40
46
|
readonly _tag: 'error';
|
|
@@ -64,13 +70,17 @@ export interface OrphanedSession {
|
|
|
64
70
|
readonly continueToken: string;
|
|
65
71
|
readonly checkpointToken: string | null;
|
|
66
72
|
readonly ts: number;
|
|
73
|
+
readonly worktreePath?: string;
|
|
67
74
|
}
|
|
68
75
|
export declare function readDaemonSessionState(sessionId: string): Promise<{
|
|
69
76
|
continueToken: string;
|
|
70
77
|
checkpointToken: string | null;
|
|
71
78
|
} | null>;
|
|
72
79
|
export declare function readAllDaemonSessions(sessionsDir?: string): Promise<OrphanedSession[]>;
|
|
73
|
-
export declare function runStartupRecovery(sessionsDir?: string
|
|
80
|
+
export declare function runStartupRecovery(sessionsDir?: string, execFn?: (file: string, args: string[]) => Promise<{
|
|
81
|
+
stdout: string;
|
|
82
|
+
stderr: string;
|
|
83
|
+
}>): Promise<void>;
|
|
74
84
|
export declare function makeContinueWorkflowTool(sessionId: string, ctx: V2ToolContext, onAdvance: (nextStepText: string, continueToken: string) => void, onComplete: (notes: string | undefined, artifacts?: readonly unknown[]) => void, schemas: Record<string, any>, _executeContinueWorkflowFn?: typeof executeContinueWorkflow, emitter?: DaemonEventEmitter, workrailSessionId?: string | null): AgentTool;
|
|
75
85
|
export declare function makeCompleteStepTool(sessionId: string, ctx: V2ToolContext, getCurrentToken: () => string, onAdvance: (nextStepText: string, continueToken: string) => void, onComplete: (notes: string | undefined, artifacts?: readonly unknown[]) => void, onTokenUpdate: (t: string) => void, schemas: Record<string, any>, _executeContinueWorkflowFn?: typeof executeContinueWorkflow, emitter?: DaemonEventEmitter, workrailSessionId?: string | null): AgentTool;
|
|
76
86
|
export declare function makeBashTool(workspacePath: string, schemas: Record<string, any>, sessionId?: string, emitter?: DaemonEventEmitter, workrailSessionId?: string | null): AgentTool;
|
|
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.DAEMON_SIGNALS_DIR = exports.DAEMON_SOUL_TEMPLATE = exports.DAEMON_SOUL_DEFAULT = exports.DAEMON_SESSIONS_DIR = void 0;
|
|
39
|
+
exports.DAEMON_SIGNALS_DIR = exports.DAEMON_SOUL_TEMPLATE = exports.DAEMON_SOUL_DEFAULT = exports.WORKTREES_DIR = exports.DAEMON_SESSIONS_DIR = void 0;
|
|
40
40
|
exports.readDaemonSessionState = readDaemonSessionState;
|
|
41
41
|
exports.readAllDaemonSessions = readAllDaemonSessions;
|
|
42
42
|
exports.runStartupRecovery = runStartupRecovery;
|
|
@@ -83,8 +83,11 @@ function withWorkrailSession(sid) {
|
|
|
83
83
|
}
|
|
84
84
|
exports.DAEMON_SESSIONS_DIR = path.join(os.homedir(), '.workrail', 'daemon-sessions');
|
|
85
85
|
const MAX_ORPHAN_AGE_MS = 2 * 60 * 60 * 1000;
|
|
86
|
+
const MAX_WORKTREE_ORPHAN_AGE_MS = 24 * 60 * 60 * 1000;
|
|
86
87
|
const WORKRAIL_DIR = path.join(os.homedir(), '.workrail');
|
|
88
|
+
exports.WORKTREES_DIR = path.join(os.homedir(), '.workrail', 'worktrees');
|
|
87
89
|
const WORKSPACE_CONTEXT_MAX_BYTES = 32 * 1024;
|
|
90
|
+
const MAX_ASSEMBLED_CONTEXT_BYTES = 8192;
|
|
88
91
|
const WORKSPACE_CONTEXT_CANDIDATE_PATHS = [
|
|
89
92
|
'.claude/CLAUDE.md',
|
|
90
93
|
'CLAUDE.md',
|
|
@@ -95,10 +98,10 @@ const soul_template_js_1 = require("./soul-template.js");
|
|
|
95
98
|
var soul_template_js_2 = require("./soul-template.js");
|
|
96
99
|
Object.defineProperty(exports, "DAEMON_SOUL_DEFAULT", { enumerable: true, get: function () { return soul_template_js_2.DAEMON_SOUL_DEFAULT; } });
|
|
97
100
|
Object.defineProperty(exports, "DAEMON_SOUL_TEMPLATE", { enumerable: true, get: function () { return soul_template_js_2.DAEMON_SOUL_TEMPLATE; } });
|
|
98
|
-
async function persistTokens(sessionId, continueToken, checkpointToken) {
|
|
101
|
+
async function persistTokens(sessionId, continueToken, checkpointToken, worktreePath) {
|
|
99
102
|
await fs.mkdir(exports.DAEMON_SESSIONS_DIR, { recursive: true });
|
|
100
103
|
const sessionPath = path.join(exports.DAEMON_SESSIONS_DIR, `${sessionId}.json`);
|
|
101
|
-
const state = JSON.stringify({ continueToken, checkpointToken, ts: Date.now() }, null, 2);
|
|
104
|
+
const state = JSON.stringify({ continueToken, checkpointToken, ts: Date.now(), ...(worktreePath !== undefined ? { worktreePath } : {}) }, null, 2);
|
|
102
105
|
const tmp = `${sessionPath}.tmp`;
|
|
103
106
|
await fs.writeFile(tmp, state, 'utf8');
|
|
104
107
|
await fs.rename(tmp, sessionPath);
|
|
@@ -144,6 +147,7 @@ async function readAllDaemonSessions(sessionsDir = exports.DAEMON_SESSIONS_DIR)
|
|
|
144
147
|
continueToken: parsed.continueToken,
|
|
145
148
|
checkpointToken: typeof parsed.checkpointToken === 'string' ? parsed.checkpointToken : null,
|
|
146
149
|
ts: parsed.ts,
|
|
150
|
+
...(typeof parsed.worktreePath === 'string' ? { worktreePath: parsed.worktreePath } : {}),
|
|
147
151
|
});
|
|
148
152
|
}
|
|
149
153
|
catch (err) {
|
|
@@ -152,7 +156,7 @@ async function readAllDaemonSessions(sessionsDir = exports.DAEMON_SESSIONS_DIR)
|
|
|
152
156
|
}
|
|
153
157
|
return sessions;
|
|
154
158
|
}
|
|
155
|
-
async function runStartupRecovery(sessionsDir = exports.DAEMON_SESSIONS_DIR) {
|
|
159
|
+
async function runStartupRecovery(sessionsDir = exports.DAEMON_SESSIONS_DIR, execFn = execFileAsync) {
|
|
156
160
|
const sessions = await readAllDaemonSessions(sessionsDir);
|
|
157
161
|
if (sessions.length === 0) {
|
|
158
162
|
await clearStrayTmpFiles(sessionsDir);
|
|
@@ -167,6 +171,22 @@ async function runStartupRecovery(sessionsDir = exports.DAEMON_SESSIONS_DIR) {
|
|
|
167
171
|
const ageSec = Math.round(ageMs / 1000);
|
|
168
172
|
const label = isStale ? 'stale orphaned session' : 'orphaned session';
|
|
169
173
|
console.log(`[WorkflowRunner] Clearing ${label}: sessionId=${session.sessionId} age=${ageSec}s`);
|
|
174
|
+
if (session.worktreePath && ageMs > MAX_WORKTREE_ORPHAN_AGE_MS) {
|
|
175
|
+
console.log(`[WorkflowRunner] Removing orphan worktree: sessionId=${session.sessionId} worktreePath=${session.worktreePath}`);
|
|
176
|
+
try {
|
|
177
|
+
await execFn('git', ['worktree', 'remove', '--force', session.worktreePath]);
|
|
178
|
+
console.log(`[WorkflowRunner] Removed orphan worktree: ${session.worktreePath}`);
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
console.warn(`[WorkflowRunner] Could not remove orphan worktree ${session.worktreePath}: ` +
|
|
182
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else if (session.worktreePath && ageMs <= MAX_WORKTREE_ORPHAN_AGE_MS) {
|
|
186
|
+
const ageHours = (ageMs / (60 * 60 * 1000)).toFixed(1);
|
|
187
|
+
console.log(`[WorkflowRunner] Keeping recent orphan worktree: sessionId=${session.sessionId} ` +
|
|
188
|
+
`age=${ageHours}h (threshold=24h) worktreePath=${session.worktreePath}`);
|
|
189
|
+
}
|
|
170
190
|
try {
|
|
171
191
|
await fs.unlink(path.join(sessionsDir, `${session.sessionId}.json`));
|
|
172
192
|
cleared++;
|
|
@@ -1307,6 +1327,16 @@ function buildSystemPrompt(trigger, sessionState, soulContent, workspaceContext)
|
|
|
1307
1327
|
lines.push('## Workspace Context (from AGENTS.md / CLAUDE.md)');
|
|
1308
1328
|
lines.push(workspaceContext);
|
|
1309
1329
|
}
|
|
1330
|
+
const assembledContextSummary = trigger.context?.['assembledContextSummary'];
|
|
1331
|
+
if (typeof assembledContextSummary === 'string' && assembledContextSummary.trim().length > 0) {
|
|
1332
|
+
let ctxStr = assembledContextSummary;
|
|
1333
|
+
if (Buffer.byteLength(ctxStr, 'utf8') > MAX_ASSEMBLED_CONTEXT_BYTES) {
|
|
1334
|
+
ctxStr = ctxStr.slice(0, MAX_ASSEMBLED_CONTEXT_BYTES) + '\n[Prior context truncated at 8KB]';
|
|
1335
|
+
}
|
|
1336
|
+
lines.push('');
|
|
1337
|
+
lines.push('## Prior Context');
|
|
1338
|
+
lines.push(ctxStr.trim());
|
|
1339
|
+
}
|
|
1310
1340
|
if (trigger.referenceUrls && trigger.referenceUrls.length > 0) {
|
|
1311
1341
|
lines.push('');
|
|
1312
1342
|
lines.push('## Reference documents');
|
|
@@ -1419,12 +1449,53 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
|
|
|
1419
1449
|
if (startContinueToken) {
|
|
1420
1450
|
await persistTokens(sessionId, startContinueToken, startCheckpointToken);
|
|
1421
1451
|
}
|
|
1452
|
+
let sessionWorkspacePath = trigger.workspacePath;
|
|
1453
|
+
let sessionWorktreePath;
|
|
1454
|
+
if (trigger.branchStrategy === 'worktree') {
|
|
1455
|
+
const branchPrefix = trigger.branchPrefix ?? 'worktrain/';
|
|
1456
|
+
const baseBranch = trigger.baseBranch ?? 'main';
|
|
1457
|
+
sessionWorkspacePath = path.join(exports.WORKTREES_DIR, sessionId);
|
|
1458
|
+
sessionWorktreePath = sessionWorkspacePath;
|
|
1459
|
+
try {
|
|
1460
|
+
await fs.mkdir(exports.WORKTREES_DIR, { recursive: true });
|
|
1461
|
+
await execFileAsync('git', ['-C', trigger.workspacePath, 'fetch', 'origin', baseBranch]);
|
|
1462
|
+
await execFileAsync('git', [
|
|
1463
|
+
'-C', trigger.workspacePath,
|
|
1464
|
+
'worktree', 'add',
|
|
1465
|
+
sessionWorkspacePath,
|
|
1466
|
+
'-b', `${branchPrefix}${sessionId}`,
|
|
1467
|
+
`origin/${baseBranch}`,
|
|
1468
|
+
]);
|
|
1469
|
+
await persistTokens(sessionId, startContinueToken ?? currentContinueToken, startCheckpointToken, sessionWorktreePath);
|
|
1470
|
+
console.log(`[WorkflowRunner] Worktree created: sessionId=${sessionId} ` +
|
|
1471
|
+
`branch=${branchPrefix}${sessionId} path=${sessionWorkspacePath}`);
|
|
1472
|
+
}
|
|
1473
|
+
catch (err) {
|
|
1474
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1475
|
+
console.error(`[WorkflowRunner] Worktree creation failed: sessionId=${sessionId} error=${errMsg}`);
|
|
1476
|
+
emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'error', detail: errMsg.slice(0, 200), ...withWorkrailSession(workrailSessionId) });
|
|
1477
|
+
if (workrailSessionId !== null)
|
|
1478
|
+
daemonRegistry?.unregister(workrailSessionId, 'failed');
|
|
1479
|
+
return {
|
|
1480
|
+
_tag: 'error',
|
|
1481
|
+
workflowId: trigger.workflowId,
|
|
1482
|
+
message: `Worktree creation failed: ${errMsg}`,
|
|
1483
|
+
stopReason: 'error',
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1422
1487
|
if (firstStep.isComplete) {
|
|
1423
1488
|
await fs.unlink(path.join(exports.DAEMON_SESSIONS_DIR, `${sessionId}.json`)).catch(() => { });
|
|
1424
1489
|
emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'success', detail: 'stop', ...withWorkrailSession(workrailSessionId) });
|
|
1425
1490
|
if (workrailSessionId !== null)
|
|
1426
1491
|
daemonRegistry?.unregister(workrailSessionId, 'completed');
|
|
1427
|
-
return {
|
|
1492
|
+
return {
|
|
1493
|
+
_tag: 'success',
|
|
1494
|
+
workflowId: trigger.workflowId,
|
|
1495
|
+
stopReason: 'stop',
|
|
1496
|
+
...(sessionWorktreePath !== undefined ? { sessionWorkspacePath: sessionWorktreePath } : {}),
|
|
1497
|
+
...(sessionWorktreePath !== undefined ? { sessionId } : {}),
|
|
1498
|
+
};
|
|
1428
1499
|
}
|
|
1429
1500
|
const schemas = getSchemas();
|
|
1430
1501
|
const spawnCurrentDepth = trigger.spawnDepth ?? 0;
|
|
@@ -1433,12 +1504,12 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
|
|
|
1433
1504
|
const tools = [
|
|
1434
1505
|
makeCompleteStepTool(sessionId, ctx, () => currentContinueToken, onAdvance, onComplete, (t) => { currentContinueToken = t; }, schemas, index_js_1.executeContinueWorkflow, emitter, workrailSessionId),
|
|
1435
1506
|
makeContinueWorkflowTool(sessionId, ctx, onAdvance, onComplete, schemas, index_js_1.executeContinueWorkflow, emitter, workrailSessionId),
|
|
1436
|
-
makeBashTool(
|
|
1507
|
+
makeBashTool(sessionWorkspacePath, schemas, sessionId, emitter, workrailSessionId),
|
|
1437
1508
|
makeReadTool(readFileState, schemas, sessionId, emitter, workrailSessionId),
|
|
1438
1509
|
makeWriteTool(readFileState, schemas, sessionId, emitter, workrailSessionId),
|
|
1439
|
-
makeGlobTool(
|
|
1440
|
-
makeGrepTool(
|
|
1441
|
-
makeEditTool(
|
|
1510
|
+
makeGlobTool(sessionWorkspacePath, schemas, sessionId, emitter, workrailSessionId),
|
|
1511
|
+
makeGrepTool(sessionWorkspacePath, schemas, sessionId, emitter, workrailSessionId),
|
|
1512
|
+
makeEditTool(sessionWorkspacePath, readFileState, schemas, sessionId, emitter, workrailSessionId),
|
|
1442
1513
|
makeReportIssueTool(sessionId, emitter, workrailSessionId, undefined, (summary) => {
|
|
1443
1514
|
if (issueSummaries.length < MAX_ISSUE_SUMMARIES) {
|
|
1444
1515
|
issueSummaries.push(summary);
|
|
@@ -1659,5 +1730,7 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
|
|
|
1659
1730
|
stopReason,
|
|
1660
1731
|
...(lastStepNotes !== undefined ? { lastStepNotes } : {}),
|
|
1661
1732
|
...(lastStepArtifacts !== undefined ? { lastStepArtifacts } : {}),
|
|
1733
|
+
...(sessionWorktreePath !== undefined ? { sessionWorkspacePath: sessionWorktreePath } : {}),
|
|
1734
|
+
...(sessionWorktreePath !== undefined ? { sessionId } : {}),
|
|
1662
1735
|
};
|
|
1663
1736
|
}
|
|
@@ -20,12 +20,12 @@ export declare const LoopFrameSchema: z.ZodObject<{
|
|
|
20
20
|
iteration: z.ZodNumber;
|
|
21
21
|
bodyIndex: z.ZodNumber;
|
|
22
22
|
}, "strip", z.ZodTypeAny, {
|
|
23
|
-
loopId: string;
|
|
24
23
|
iteration: number;
|
|
24
|
+
loopId: string;
|
|
25
25
|
bodyIndex: number;
|
|
26
26
|
}, {
|
|
27
|
-
loopId: string;
|
|
28
27
|
iteration: number;
|
|
28
|
+
loopId: string;
|
|
29
29
|
bodyIndex: number;
|
|
30
30
|
}>;
|
|
31
31
|
export declare const StepInstanceIdSchema: z.ZodObject<{
|
|
@@ -34,23 +34,23 @@ export declare const StepInstanceIdSchema: z.ZodObject<{
|
|
|
34
34
|
loopId: z.ZodString;
|
|
35
35
|
iteration: z.ZodNumber;
|
|
36
36
|
}, "strip", z.ZodTypeAny, {
|
|
37
|
-
loopId: string;
|
|
38
37
|
iteration: number;
|
|
39
|
-
}, {
|
|
40
38
|
loopId: string;
|
|
39
|
+
}, {
|
|
41
40
|
iteration: number;
|
|
41
|
+
loopId: string;
|
|
42
42
|
}>, "many">;
|
|
43
43
|
}, "strip", z.ZodTypeAny, {
|
|
44
44
|
stepId: string;
|
|
45
45
|
loopPath: {
|
|
46
|
-
loopId: string;
|
|
47
46
|
iteration: number;
|
|
47
|
+
loopId: string;
|
|
48
48
|
}[];
|
|
49
49
|
}, {
|
|
50
50
|
stepId: string;
|
|
51
51
|
loopPath: {
|
|
52
|
-
loopId: string;
|
|
53
52
|
iteration: number;
|
|
53
|
+
loopId: string;
|
|
54
54
|
}[];
|
|
55
55
|
}>;
|
|
56
56
|
export declare const ExecutionStateSchema: z.ZodType<ExecutionState>;
|