@yeaft/webchat-agent 1.0.166 → 1.0.167
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/package.json +1 -1
- package/yeaft/tools/create-work-item.js +3 -1
- package/yeaft/work-center/assignment.js +12 -4
- package/yeaft/work-center/bridge.js +11 -2
- package/yeaft/work-center/controller.js +46 -5
- package/yeaft/work-center/projection.js +4 -0
- package/yeaft/work-center/runner.js +173 -9
- package/yeaft/work-center/service.js +5 -0
- package/yeaft/work-center/session-context.js +75 -0
- package/yeaft/work-center/store.js +381 -67
- package/yeaft/work-center/watcher.js +164 -84
- package/yeaft/work-center/workflow.js +77 -5
- package/yeaft/work-center/workspace.js +183 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, rmSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const HIDDEN_PROCESS_OPTIONS = { windowsHide: true };
|
|
6
|
+
const INTEGRATION_FINALIZE_TIMEOUT_MS = 45_000;
|
|
7
|
+
|
|
8
|
+
function git(args, options = {}) {
|
|
9
|
+
return execFileSync('git', args, {
|
|
10
|
+
encoding: 'utf8',
|
|
11
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
12
|
+
...HIDDEN_PROCESS_OPTIONS,
|
|
13
|
+
...options,
|
|
14
|
+
}).trim();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function cleanSlug(value) {
|
|
18
|
+
return String(value || '').replace(/[^A-Za-z0-9._-]/g, '-').slice(0, 64);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function inspectGitWorkspace(workDir) {
|
|
22
|
+
try {
|
|
23
|
+
const root = git(['rev-parse', '--show-toplevel'], { cwd: workDir });
|
|
24
|
+
if (resolve(root) !== resolve(workDir)) return { git: false, reason: 'nested-repository-directory' };
|
|
25
|
+
const head = git(['rev-parse', 'HEAD'], { cwd: root });
|
|
26
|
+
const status = git(['status', '--porcelain', '--untracked-files=normal'], { cwd: root });
|
|
27
|
+
return status
|
|
28
|
+
? { git: true, clean: false, root, head, reason: 'dirty-worktree' }
|
|
29
|
+
: { git: true, clean: true, root, head };
|
|
30
|
+
} catch {
|
|
31
|
+
return { git: false, reason: 'not-a-git-repository' };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createActionWorktree({ workItem, action, runId, rootDir }) {
|
|
36
|
+
const state = inspectGitWorkspace(workItem.workspaceKey || workItem.workDir);
|
|
37
|
+
if (!state.git || !state.clean) return { isolated: false, reason: state.reason };
|
|
38
|
+
const name = `${cleanSlug(workItem.id)}-${cleanSlug(action.stageId || action.id)}-${cleanSlug(runId || 'run')}`;
|
|
39
|
+
const worktreePath = resolve(join(rootDir, name));
|
|
40
|
+
const branch = `yeaft-work/${name}`;
|
|
41
|
+
if (existsSync(worktreePath)) throw new Error(`Work Center Action worktree already exists: ${worktreePath}`);
|
|
42
|
+
mkdirSync(dirname(worktreePath), { recursive: true });
|
|
43
|
+
git(['worktree', 'add', '-b', branch, worktreePath, state.head], { cwd: state.root });
|
|
44
|
+
return {
|
|
45
|
+
isolated: true,
|
|
46
|
+
path: worktreePath,
|
|
47
|
+
branch,
|
|
48
|
+
baseCommit: state.head,
|
|
49
|
+
repositoryRoot: state.root,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function commitActionWorktree(workspace, action) {
|
|
54
|
+
if (!workspace?.isolated) return workspace;
|
|
55
|
+
const status = git(['status', '--porcelain', '--untracked-files=normal'], { cwd: workspace.path });
|
|
56
|
+
if (!status) return { ...workspace, commit: workspace.baseCommit, changed: false };
|
|
57
|
+
git(['add', '--all'], { cwd: workspace.path });
|
|
58
|
+
git([
|
|
59
|
+
'-c', 'user.name=Yeaft Work Center',
|
|
60
|
+
'-c', 'user.email=work-center@yeaft.local',
|
|
61
|
+
'commit', '-m', `work-center: ${cleanSlug(action.stageId || action.type)}`,
|
|
62
|
+
], { cwd: workspace.path });
|
|
63
|
+
return { ...workspace, commit: git(['rev-parse', 'HEAD'], { cwd: workspace.path }), changed: true };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function removeIntegrationPreparation(integration) {
|
|
67
|
+
if (!integration?.repositoryRoot) return;
|
|
68
|
+
if (integration.temporaryPath) {
|
|
69
|
+
try {
|
|
70
|
+
git(['worktree', 'remove', '--force', integration.temporaryPath], { cwd: integration.repositoryRoot });
|
|
71
|
+
} catch {
|
|
72
|
+
rmSync(integration.temporaryPath, { recursive: true, force: true });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (integration.temporaryBranch) {
|
|
76
|
+
try { git(['branch', '-D', integration.temporaryBranch], { cwd: integration.repositoryRoot }); } catch {}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function prepareActionIntegration({ workDir, dependencies }) {
|
|
81
|
+
const state = inspectGitWorkspace(workDir);
|
|
82
|
+
if (!state.git || !state.clean) throw new Error(`Work Center integration requires a clean Git root: ${state.reason}`);
|
|
83
|
+
if (dependencies.some(action => action.status !== 'completed')) {
|
|
84
|
+
throw new Error('Work Center integration requires every dependency Action to complete successfully');
|
|
85
|
+
}
|
|
86
|
+
const commits = dependencies
|
|
87
|
+
.filter(action => action.workspace?.isolated && action.workspace.changed)
|
|
88
|
+
.map(action => action.workspace.commit);
|
|
89
|
+
const dependencyBranches = dependencies
|
|
90
|
+
.filter(action => action.workspace?.changed && action.workspace.branch)
|
|
91
|
+
.map(action => action.workspace.branch);
|
|
92
|
+
const temporaryRoot = resolve(join(dirname(state.root), '.yeaft-work-center-integration'));
|
|
93
|
+
mkdirSync(temporaryRoot, { recursive: true });
|
|
94
|
+
const suffix = `${process.pid}-${Date.now()}`;
|
|
95
|
+
const integration = {
|
|
96
|
+
status: 'prepared',
|
|
97
|
+
commits,
|
|
98
|
+
dependencyBranches,
|
|
99
|
+
baseHead: state.head,
|
|
100
|
+
integratedHead: null,
|
|
101
|
+
repositoryRoot: state.root,
|
|
102
|
+
temporaryPath: resolve(join(temporaryRoot, `integration-${suffix}`)),
|
|
103
|
+
temporaryBranch: `yeaft-work/integration-${suffix}`,
|
|
104
|
+
};
|
|
105
|
+
try {
|
|
106
|
+
git([
|
|
107
|
+
'worktree', 'add', '-b', integration.temporaryBranch,
|
|
108
|
+
integration.temporaryPath, integration.baseHead,
|
|
109
|
+
], { cwd: integration.repositoryRoot });
|
|
110
|
+
for (const commit of commits) {
|
|
111
|
+
try {
|
|
112
|
+
git([
|
|
113
|
+
'-c', 'user.name=Yeaft Work Center',
|
|
114
|
+
'-c', 'user.email=work-center@yeaft.local',
|
|
115
|
+
'merge', '--no-ff', '--no-edit', commit,
|
|
116
|
+
], { cwd: integration.temporaryPath });
|
|
117
|
+
} catch (error) {
|
|
118
|
+
try { git(['merge', '--abort'], { cwd: integration.temporaryPath }); } catch {}
|
|
119
|
+
throw new Error(`Work Center could not integrate Action commit ${commit}: ${error.message}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
integration.integratedHead = git(['rev-parse', 'HEAD'], { cwd: integration.temporaryPath });
|
|
123
|
+
return integration;
|
|
124
|
+
} catch (error) {
|
|
125
|
+
removeIntegrationPreparation(integration);
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function discardActionIntegration(integration) {
|
|
131
|
+
removeIntegrationPreparation(integration);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function finalizeActionIntegration(integration) {
|
|
135
|
+
if (!integration?.repositoryRoot || !integration.baseHead || !integration.integratedHead) {
|
|
136
|
+
throw new Error('Work Center integration preparation is incomplete');
|
|
137
|
+
}
|
|
138
|
+
const currentHead = git(['rev-parse', 'HEAD'], { cwd: integration.repositoryRoot });
|
|
139
|
+
const status = git([
|
|
140
|
+
'status', '--porcelain', '--untracked-files=normal',
|
|
141
|
+
], { cwd: integration.repositoryRoot });
|
|
142
|
+
if (status || ![integration.baseHead, integration.integratedHead].includes(currentHead)) {
|
|
143
|
+
throw new Error('Work Center integration target changed before finalization');
|
|
144
|
+
}
|
|
145
|
+
const reservationExpiresAt = Number(integration.reservation?.expiresAt) || 0;
|
|
146
|
+
if (reservationExpiresAt && Date.now() + INTEGRATION_FINALIZE_TIMEOUT_MS >= reservationExpiresAt) {
|
|
147
|
+
throw new Error('Work Center integration reservation expires before finalization can complete');
|
|
148
|
+
}
|
|
149
|
+
if (currentHead === integration.baseHead) {
|
|
150
|
+
git(['merge', '--ff-only', integration.integratedHead], {
|
|
151
|
+
cwd: integration.repositoryRoot,
|
|
152
|
+
timeout: INTEGRATION_FINALIZE_TIMEOUT_MS,
|
|
153
|
+
killSignal: 'SIGKILL',
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const head = git(['rev-parse', 'HEAD'], { cwd: integration.repositoryRoot });
|
|
157
|
+
if (head !== integration.integratedHead) {
|
|
158
|
+
throw new Error('Work Center integration target did not reach the prepared commit');
|
|
159
|
+
}
|
|
160
|
+
for (const branch of integration.dependencyBranches || []) {
|
|
161
|
+
try { git(['branch', '-d', branch], { cwd: integration.repositoryRoot }); } catch {}
|
|
162
|
+
}
|
|
163
|
+
removeIntegrationPreparation(integration);
|
|
164
|
+
return {
|
|
165
|
+
status: 'finalized',
|
|
166
|
+
commits: integration.commits,
|
|
167
|
+
baseHead: integration.baseHead,
|
|
168
|
+
integratedHead: integration.integratedHead,
|
|
169
|
+
head,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function integrateActionWorktrees({ workDir, dependencies }) {
|
|
174
|
+
return finalizeActionIntegration(prepareActionIntegration({ workDir, dependencies }));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function removeActionWorktree(workspace) {
|
|
178
|
+
if (!workspace?.isolated || !workspace.path || !workspace.repositoryRoot) return;
|
|
179
|
+
try { git(['worktree', 'remove', '--force', workspace.path], { cwd: workspace.repositoryRoot }); } catch {}
|
|
180
|
+
if (!workspace.changed && workspace.branch) {
|
|
181
|
+
try { git(['branch', '-D', workspace.branch], { cwd: workspace.repositoryRoot }); } catch {}
|
|
182
|
+
}
|
|
183
|
+
}
|