@yeaft/webchat-agent 1.0.129 → 1.0.131
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/config.js +6 -3
- package/yeaft/tools/create-work-item.js +3 -1
- package/yeaft/work-center/assignment.js +135 -0
- package/yeaft/work-center/bridge.js +50 -4
- package/yeaft/work-center/controller.js +72 -15
- package/yeaft/work-center/planner.js +66 -0
- package/yeaft/work-center/projection.js +97 -2
- package/yeaft/work-center/runner.js +53 -24
- package/yeaft/work-center/service.js +41 -5
- package/yeaft/work-center/settings.js +59 -0
- package/yeaft/work-center/store.js +136 -14
- package/yeaft/work-center/workflow.js +254 -33
|
@@ -1,13 +1,35 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
{ type: 'deliver', requiredRole: 'linus' },
|
|
6
|
-
]);
|
|
1
|
+
const STAGE_TYPES = new Set(['triage', 'implement', 'test', 'review', 'deliver', 'research', 'write', 'custom']);
|
|
2
|
+
const ASSIGNMENT_MODES = new Set(['auto', 'pool', 'fixed']);
|
|
3
|
+
const MODEL_MODES = new Set(['inherit', 'primary', 'fast', 'specific']);
|
|
4
|
+
const MODEL_EFFORTS = new Set(['minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
const DEFAULT_SOFTWARE_CHANGE_STAGES = Object.freeze([
|
|
7
|
+
{
|
|
8
|
+
id: 'triage', name: 'Triage', type: 'triage',
|
|
9
|
+
assignmentPolicy: { mode: 'auto', capability: 'triage', candidateVpIds: [], fixedVpId: null, separateFromStageTypes: [] },
|
|
10
|
+
modelPolicy: { mode: 'inherit', model: null, effort: null },
|
|
11
|
+
maxAttempts: 2,
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: 'implement', name: 'Implement', type: 'implement',
|
|
15
|
+
assignmentPolicy: { mode: 'auto', capability: 'implement', candidateVpIds: [], fixedVpId: null, separateFromStageTypes: [] },
|
|
16
|
+
modelPolicy: { mode: 'inherit', model: null, effort: null },
|
|
17
|
+
maxAttempts: 2,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'review', name: 'Review', type: 'review',
|
|
21
|
+
assignmentPolicy: { mode: 'auto', capability: 'review', candidateVpIds: [], fixedVpId: null, separateFromStageTypes: ['implement'] },
|
|
22
|
+
modelPolicy: { mode: 'inherit', model: null, effort: null },
|
|
23
|
+
maxAttempts: 2,
|
|
24
|
+
changesRequestedStageId: 'implement',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'deliver', name: 'Deliver', type: 'deliver',
|
|
28
|
+
assignmentPolicy: { mode: 'auto', capability: 'deliver', candidateVpIds: [], fixedVpId: null, separateFromStageTypes: [] },
|
|
29
|
+
modelPolicy: { mode: 'inherit', model: null, effort: null },
|
|
30
|
+
maxAttempts: 2,
|
|
31
|
+
},
|
|
32
|
+
]);
|
|
11
33
|
|
|
12
34
|
export const RUN_OUTCOMES = Object.freeze([
|
|
13
35
|
'completed',
|
|
@@ -16,23 +38,204 @@ export const RUN_OUTCOMES = Object.freeze([
|
|
|
16
38
|
'failed',
|
|
17
39
|
]);
|
|
18
40
|
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
41
|
+
function cleanId(value, fallback) {
|
|
42
|
+
const normalized = String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
43
|
+
return normalized || fallback;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function uniqueStrings(value) {
|
|
47
|
+
if (!Array.isArray(value)) return [];
|
|
48
|
+
return [...new Set(value.map(item => String(item || '').trim()).filter(Boolean))];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function normalizeAssignmentPolicy(value, stageType = 'custom') {
|
|
52
|
+
const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
53
|
+
const mode = ASSIGNMENT_MODES.has(source.mode) ? source.mode : 'auto';
|
|
54
|
+
const fixedVpId = typeof source.fixedVpId === 'string' && source.fixedVpId.trim()
|
|
55
|
+
? source.fixedVpId.trim()
|
|
56
|
+
: null;
|
|
57
|
+
const candidateVpIds = uniqueStrings(source.candidateVpIds);
|
|
58
|
+
if (mode === 'fixed' && !fixedVpId) throw new Error('Fixed Work Center assignment requires fixedVpId');
|
|
59
|
+
if (mode === 'pool' && candidateVpIds.length === 0) throw new Error('Work Center VP pool cannot be empty');
|
|
60
|
+
return {
|
|
61
|
+
mode,
|
|
62
|
+
capability: cleanId(source.capability, stageType),
|
|
63
|
+
candidateVpIds,
|
|
64
|
+
fixedVpId,
|
|
65
|
+
separateFromStageTypes: uniqueStrings(source.separateFromStageTypes),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function normalizeModelPolicy(value) {
|
|
70
|
+
const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
71
|
+
const mode = MODEL_MODES.has(source.mode) ? source.mode : 'inherit';
|
|
72
|
+
const model = typeof source.model === 'string' && source.model.trim() ? source.model.trim() : null;
|
|
73
|
+
const effort = MODEL_EFFORTS.has(source.effort) ? source.effort : null;
|
|
74
|
+
if (mode === 'specific' && !model) throw new Error('Specific Work Center model policy requires a model');
|
|
75
|
+
return { mode, model, effort };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function normalizeWorkflowDefinition(value, index = 0) {
|
|
79
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
80
|
+
throw new Error('Work Center workflow must be an object');
|
|
81
|
+
}
|
|
82
|
+
const id = cleanId(value.id, `workflow-${index + 1}`);
|
|
83
|
+
const name = String(value.name || id).trim() || id;
|
|
84
|
+
if (!Array.isArray(value.stages) || value.stages.length === 0) {
|
|
85
|
+
throw new Error(`Work Center workflow "${id}" requires at least one stage`);
|
|
86
|
+
}
|
|
87
|
+
const seen = new Set();
|
|
88
|
+
const stages = value.stages.map((rawStage, stageIndex) => {
|
|
89
|
+
const source = rawStage && typeof rawStage === 'object' ? rawStage : {};
|
|
90
|
+
const type = STAGE_TYPES.has(source.type) ? source.type : 'custom';
|
|
91
|
+
const stageId = cleanId(source.id, `${type}-${stageIndex + 1}`);
|
|
92
|
+
if (seen.has(stageId)) throw new Error(`Duplicate Work Center stage id: ${stageId}`);
|
|
93
|
+
seen.add(stageId);
|
|
94
|
+
const stage = {
|
|
95
|
+
id: stageId,
|
|
96
|
+
name: String(source.name || stageId).trim() || stageId,
|
|
97
|
+
type,
|
|
98
|
+
instruction: typeof source.instruction === 'string' ? source.instruction.trim() : '',
|
|
99
|
+
assignmentPolicy: normalizeAssignmentPolicy(source.assignmentPolicy, type),
|
|
100
|
+
modelPolicy: normalizeModelPolicy(source.modelPolicy),
|
|
101
|
+
maxAttempts: Math.min(Math.max(Number(source.maxAttempts) || 2, 1), 5),
|
|
102
|
+
};
|
|
103
|
+
if (type === 'review') {
|
|
104
|
+
stage.changesRequestedStageId = cleanId(source.changesRequestedStageId, 'implement');
|
|
105
|
+
}
|
|
106
|
+
return stage;
|
|
107
|
+
});
|
|
108
|
+
for (const [stageIndex, stage] of stages.entries()) {
|
|
109
|
+
if (stage.type !== 'review') continue;
|
|
110
|
+
const targetIndex = stages.findIndex(candidate => candidate.id === stage.changesRequestedStageId);
|
|
111
|
+
if (targetIndex === -1) {
|
|
112
|
+
throw new Error(`Review stage "${stage.id}" points to missing stage "${stage.changesRequestedStageId}"`);
|
|
113
|
+
}
|
|
114
|
+
const target = stages[targetIndex];
|
|
115
|
+
if (targetIndex >= stageIndex || target.type === 'review' || target.type === 'deliver') {
|
|
116
|
+
throw new Error(`Review stage "${stage.id}" must return to an earlier editable stage`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { version: 1, id, name, stages };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function defaultWorkCenterSettings() {
|
|
123
|
+
return {
|
|
124
|
+
version: 1,
|
|
125
|
+
revision: 1,
|
|
126
|
+
defaultWorkflowId: 'software-change',
|
|
127
|
+
startImmediately: true,
|
|
128
|
+
defaultWorkDir: '',
|
|
129
|
+
workflows: [normalizeWorkflowDefinition({
|
|
130
|
+
id: 'software-change',
|
|
131
|
+
name: 'Software change',
|
|
132
|
+
stages: DEFAULT_SOFTWARE_CHANGE_STAGES,
|
|
133
|
+
})],
|
|
134
|
+
};
|
|
23
135
|
}
|
|
24
136
|
|
|
25
|
-
export function
|
|
26
|
-
|
|
137
|
+
export function normalizeWorkCenterSettings(value) {
|
|
138
|
+
const defaults = defaultWorkCenterSettings();
|
|
139
|
+
const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
140
|
+
const workflows = Array.isArray(source.workflows) && source.workflows.length > 0
|
|
141
|
+
? source.workflows.map(normalizeWorkflowDefinition)
|
|
142
|
+
: defaults.workflows;
|
|
143
|
+
const workflowIds = new Set();
|
|
144
|
+
for (const workflow of workflows) {
|
|
145
|
+
if (workflowIds.has(workflow.id)) throw new Error(`Duplicate Work Center workflow id: ${workflow.id}`);
|
|
146
|
+
workflowIds.add(workflow.id);
|
|
147
|
+
}
|
|
148
|
+
const defaultWorkflowId = workflowIds.has(source.defaultWorkflowId)
|
|
149
|
+
? source.defaultWorkflowId
|
|
150
|
+
: workflows[0].id;
|
|
151
|
+
const revision = Number.isInteger(source.revision) && source.revision > 0 ? source.revision : 1;
|
|
152
|
+
return {
|
|
153
|
+
version: 1,
|
|
154
|
+
revision,
|
|
155
|
+
defaultWorkflowId,
|
|
156
|
+
startImmediately: source.startImmediately !== false,
|
|
157
|
+
defaultWorkDir: typeof source.defaultWorkDir === 'string' ? source.defaultWorkDir.trim() : '',
|
|
158
|
+
workflows,
|
|
159
|
+
};
|
|
27
160
|
}
|
|
28
161
|
|
|
29
|
-
export function
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
if (
|
|
162
|
+
export function resolveWorkflowSnapshot(settings, workflowId, stageOverrides = {}) {
|
|
163
|
+
const normalized = normalizeWorkCenterSettings(settings);
|
|
164
|
+
const id = workflowId || normalized.defaultWorkflowId;
|
|
165
|
+
const workflow = normalized.workflows.find(item => item.id === id);
|
|
166
|
+
if (!workflow) throw new Error(`Unsupported Work Center workflow: ${id}`);
|
|
167
|
+
const overrides = stageOverrides && typeof stageOverrides === 'object' && !Array.isArray(stageOverrides)
|
|
168
|
+
? stageOverrides
|
|
169
|
+
: {};
|
|
170
|
+
return normalizeWorkflowDefinition({
|
|
171
|
+
...workflow,
|
|
172
|
+
stages: workflow.stages.map(stage => {
|
|
173
|
+
const override = overrides[stage.id];
|
|
174
|
+
if (!override || typeof override !== 'object' || Array.isArray(override)) return stage;
|
|
175
|
+
return {
|
|
176
|
+
...stage,
|
|
177
|
+
assignmentPolicy: override.assignmentPolicy
|
|
178
|
+
? { ...stage.assignmentPolicy, ...override.assignmentPolicy }
|
|
179
|
+
: stage.assignmentPolicy,
|
|
180
|
+
modelPolicy: override.modelPolicy
|
|
181
|
+
? { ...stage.modelPolicy, ...override.modelPolicy }
|
|
182
|
+
: stage.modelPolicy,
|
|
183
|
+
};
|
|
184
|
+
}),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const LEGACY_SOFTWARE_CHANGE_VPS = Object.freeze({
|
|
189
|
+
triage: 'omni',
|
|
190
|
+
implement: 'linus',
|
|
191
|
+
review: 'martin',
|
|
192
|
+
deliver: 'linus',
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
function workflowFrom(source = 'software-change') {
|
|
196
|
+
if (source && typeof source === 'object' && source.workflowSnapshot?.stages) {
|
|
197
|
+
return normalizeWorkflowDefinition(source.workflowSnapshot);
|
|
198
|
+
}
|
|
199
|
+
if (source && typeof source === 'object' && source.stages) return normalizeWorkflowDefinition(source);
|
|
200
|
+
const settings = defaultWorkCenterSettings();
|
|
201
|
+
const workflow = resolveWorkflowSnapshot(settings, typeof source === 'string' ? source : 'software-change');
|
|
202
|
+
// Existing WorkItems did not persist a workflow snapshot. Preserve their
|
|
203
|
+
// historical deterministic assignments; only newly created WorkItems use
|
|
204
|
+
// pool selection. This avoids silently changing in-flight durable work.
|
|
205
|
+
return {
|
|
206
|
+
...workflow,
|
|
207
|
+
stages: workflow.stages.map(stage => ({
|
|
208
|
+
...stage,
|
|
209
|
+
assignmentPolicy: {
|
|
210
|
+
...stage.assignmentPolicy,
|
|
211
|
+
mode: 'fixed',
|
|
212
|
+
fixedVpId: LEGACY_SOFTWARE_CHANGE_VPS[stage.type],
|
|
213
|
+
},
|
|
214
|
+
})),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function getWorkflowSteps(source = 'software-change') {
|
|
219
|
+
return workflowFrom(source).stages;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function getStep(source, stageIdOrType) {
|
|
223
|
+
return getWorkflowSteps(source).find(stage => stage.id === stageIdOrType)
|
|
224
|
+
|| getWorkflowSteps(source).find(stage => stage.type === stageIdOrType)
|
|
225
|
+
|| null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function getNextStep(source, stageIdOrType, result = {}) {
|
|
229
|
+
const steps = getWorkflowSteps(source);
|
|
230
|
+
const index = steps.findIndex(stage => stage.id === stageIdOrType)
|
|
231
|
+
!== -1
|
|
232
|
+
? steps.findIndex(stage => stage.id === stageIdOrType)
|
|
233
|
+
: steps.findIndex(stage => stage.type === stageIdOrType);
|
|
234
|
+
if (index === -1) throw new Error(`Work Center stage ${stageIdOrType} is not in the workflow snapshot`);
|
|
235
|
+
const current = steps[index];
|
|
236
|
+
if (current.type === 'review') {
|
|
34
237
|
if (result.reviewDecision === 'changes_requested') {
|
|
35
|
-
return steps.find(
|
|
238
|
+
return steps.find(stage => stage.id === current.changesRequestedStageId) || null;
|
|
36
239
|
}
|
|
37
240
|
if (result.reviewDecision !== 'approved') {
|
|
38
241
|
throw new Error('Completed review requires approved or changes_requested');
|
|
@@ -54,44 +257,62 @@ function renderContext(context = []) {
|
|
|
54
257
|
const decision = entry.reviewDecision ? `\nReview decision: ${entry.reviewDecision}` : '';
|
|
55
258
|
const waitingReason = entry.waitingReason ? `\nWaiting reason: ${entry.waitingReason}` : '';
|
|
56
259
|
const answer = entry.answer ? `\nUser answer: ${entry.answer}` : '';
|
|
57
|
-
|
|
260
|
+
const source = entry.sourceTitle ? ` from ${entry.sourceTitle}` : '';
|
|
261
|
+
return `### ${entry.type}${source} (${entry.vpId || entry.role || 'unknown VP'})\n${entry.summary || '(no summary)'}${decision}${waitingReason}${answer}${evidence}`;
|
|
58
262
|
});
|
|
59
|
-
return `\n\
|
|
263
|
+
return `\n\nReusable Work Center context and prior Action results:\n${blocks.join('\n\n')}`;
|
|
60
264
|
}
|
|
61
265
|
|
|
62
|
-
export function actionInstruction(
|
|
266
|
+
export function actionInstruction(stage, workItem, context = []) {
|
|
63
267
|
const criteria = (workItem.acceptanceCriteria || []).map(item => `- ${item}`).join('\n') || '- No explicit criteria';
|
|
64
268
|
const common = `WorkItem: ${workItem.title}\nGoal: ${workItem.goal}\nAcceptance criteria:\n${criteria}${renderContext(context)}`;
|
|
65
|
-
|
|
269
|
+
if (stage.instruction) return `${common}\n\n${stage.instruction}`;
|
|
270
|
+
switch (stage.type) {
|
|
66
271
|
case 'triage':
|
|
67
272
|
return `${common}\n\nAnalyze the request, verify scope and risks, and make the contract executable. Do not implement yet. If the goal or acceptance criteria need refinement, submit a contractPatch.`;
|
|
68
273
|
case 'implement':
|
|
69
274
|
return `${common}\n\nImplement the smallest correct change in the supplied work directory. Add and run relevant tests. Use the prior triage/review findings. Do not approve your own work.`;
|
|
275
|
+
case 'test':
|
|
276
|
+
return `${common}\n\nVerify the implementation against the acceptance criteria. Run focused tests and report reproducible evidence. Do not modify unrelated code.`;
|
|
70
277
|
case 'review':
|
|
71
278
|
return `${common}\n\nReview the implementation and evidence independently. Return approved or changes_requested with concrete findings.`;
|
|
72
279
|
case 'deliver':
|
|
73
280
|
return `${common}\n\nDeliver the approved change using the repository release policy. Verify the final remote state and provide evidence.`;
|
|
281
|
+
case 'research':
|
|
282
|
+
return `${common}\n\nResearch the question using verifiable sources. Separate evidence from inference and return a concise synthesis.`;
|
|
283
|
+
case 'write':
|
|
284
|
+
return `${common}\n\nProduce the requested written deliverable, then verify it against every acceptance criterion.`;
|
|
74
285
|
default:
|
|
75
|
-
return common
|
|
286
|
+
return `${common}\n\nComplete this stage and return verifiable evidence.`;
|
|
76
287
|
}
|
|
77
288
|
}
|
|
78
289
|
|
|
79
|
-
export function
|
|
80
|
-
const step = getWorkflowSteps(workItem.workflowTemplate)[0];
|
|
290
|
+
export function actionForStage(stage, workItem, context = []) {
|
|
81
291
|
return {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
292
|
+
type: stage.type,
|
|
293
|
+
stageId: stage.id,
|
|
294
|
+
assignmentPolicy: stage.assignmentPolicy,
|
|
295
|
+
modelPolicy: stage.modelPolicy,
|
|
296
|
+
// Storage compatibility for databases created before assignment policies.
|
|
297
|
+
requiredRole: stage.assignmentPolicy.mode === 'fixed' ? stage.assignmentPolicy.fixedVpId : '',
|
|
298
|
+
context,
|
|
299
|
+
instruction: actionInstruction(stage, workItem, context),
|
|
300
|
+
maxAttempts: stage.maxAttempts,
|
|
86
301
|
};
|
|
87
302
|
}
|
|
88
303
|
|
|
304
|
+
export function initialActionFor(workItem) {
|
|
305
|
+
const stage = getWorkflowSteps(workItem)[0];
|
|
306
|
+
return actionForStage(stage, workItem, []);
|
|
307
|
+
}
|
|
308
|
+
|
|
89
309
|
export function actionContextFromRuns(runs = []) {
|
|
90
310
|
return runs
|
|
91
311
|
.filter(run => run && run.summary)
|
|
92
312
|
.map(run => ({
|
|
93
313
|
type: run.actionType || 'action',
|
|
94
|
-
|
|
314
|
+
vpId: run.vpSnapshot?.id || null,
|
|
315
|
+
role: run.roleSnapshot?.id || null,
|
|
95
316
|
summary: run.summary,
|
|
96
317
|
evidence: Array.isArray(run.evidence) ? run.evidence : [],
|
|
97
318
|
reviewDecision: run.reviewDecision || null,
|