@yeaft/webchat-agent 1.0.556 → 1.0.558
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/local-runtime/server/handlers/agent-output.js +28 -8
- package/local-runtime/server/yeaft-asset-store.js +41 -2
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +320 -278
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/yeaft/tools/create-work-item.js +4 -4
- package/yeaft/web-bridge.js +3 -1
- package/yeaft/work-center/coordinator.js +18 -2
- package/yeaft/work-center/durable-model.js +1 -1
- package/yeaft/work-center/projection.js +44 -2
- package/yeaft/work-center/service.js +6 -1
- package/yeaft/work-center/store.js +38 -4
|
Binary file
|
package/package.json
CHANGED
|
@@ -21,7 +21,7 @@ Use this when work must continue beyond the current turn, needs role handoffs, r
|
|
|
21
21
|
properties: {
|
|
22
22
|
title: {
|
|
23
23
|
type: 'string',
|
|
24
|
-
description: { en: '
|
|
24
|
+
description: { en: 'Optional explicit title; otherwise the Coordinator generates one', zh: '可选的显式标题;省略时由 Coordinator 生成' },
|
|
25
25
|
},
|
|
26
26
|
goal: {
|
|
27
27
|
type: 'string',
|
|
@@ -45,7 +45,7 @@ Use this when work must continue beyond the current turn, needs role handoffs, r
|
|
|
45
45
|
description: { en: 'Start Coordinator execution immediately (default true)', zh: '是否立即启动 Coordinator 执行(默认 true)' },
|
|
46
46
|
},
|
|
47
47
|
},
|
|
48
|
-
required: ['
|
|
48
|
+
required: ['goal'],
|
|
49
49
|
},
|
|
50
50
|
isConcurrencySafe: () => false,
|
|
51
51
|
isReadOnly: () => false,
|
|
@@ -57,14 +57,14 @@ Use this when work must continue beyond the current turn, needs role handoffs, r
|
|
|
57
57
|
if (!sessionId) throw new Error('CreateWorkItem requires an active Session');
|
|
58
58
|
const title = typeof input?.title === 'string' ? input.title.trim() : '';
|
|
59
59
|
const goal = typeof input?.goal === 'string' ? input.goal.trim() : '';
|
|
60
|
-
if (!
|
|
60
|
+
if (!goal) throw new Error('goal is required');
|
|
61
61
|
|
|
62
62
|
// Dynamic import avoids tools/index -> create-work-item -> bridge -> runner
|
|
63
63
|
// -> tools/index becoming a static initialization cycle.
|
|
64
64
|
const { createWorkItemFromProducer, snapshotCurrentSessionContext } = await import('../work-center/bridge.js');
|
|
65
65
|
const sessionContext = await snapshotCurrentSessionContext(sessionId);
|
|
66
66
|
const detail = await createWorkItemFromProducer({
|
|
67
|
-
title,
|
|
67
|
+
...(title ? { title } : {}),
|
|
68
68
|
goal,
|
|
69
69
|
acceptanceCriteria: cleanCriteria(input.acceptanceCriteria),
|
|
70
70
|
workItemType: typeof input.workItemType === 'string' ? input.workItemType.trim() : 'auto',
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -4290,12 +4290,14 @@ function handleEngineEvent(event, hctx) {
|
|
|
4290
4290
|
case 'tool_end': {
|
|
4291
4291
|
const images = Array.isArray(event.displayImages) ? event.displayImages : [];
|
|
4292
4292
|
const displayOutput = typeof event.output === 'string' ? event.output : JSON.stringify(event.output ?? '');
|
|
4293
|
-
for (const image of images) {
|
|
4293
|
+
for (const [sourceImageIndex, image] of images.entries()) {
|
|
4294
4294
|
const persistedImage = imageMetadataForPersistence(image);
|
|
4295
4295
|
try {
|
|
4296
4296
|
if (!ctx.assetOutbox) throw new Error('asset outbox is unavailable');
|
|
4297
4297
|
const deliveryId = ctx.assetOutbox.enqueue({
|
|
4298
4298
|
conversationId: yeaftConversationId,
|
|
4299
|
+
sourceToolCallId: event.id,
|
|
4300
|
+
sourceImageIndex,
|
|
4299
4301
|
metadata: persistedImage,
|
|
4300
4302
|
sessionId: hctx.sessionId,
|
|
4301
4303
|
vpId: hctx.vpId,
|
|
@@ -230,6 +230,7 @@ Return exactly one JSON object and no surrounding prose:
|
|
|
230
230
|
"decision": {
|
|
231
231
|
"kind": "answer|create_actions|guide_actions|request_human|complete",
|
|
232
232
|
"reason": "short audit reason",
|
|
233
|
+
"title": null,
|
|
233
234
|
"question": null,
|
|
234
235
|
"workItemType": null,
|
|
235
236
|
"contractPatch": null,
|
|
@@ -242,8 +243,9 @@ Return exactly one JSON object and no surrounding prose:
|
|
|
242
243
|
}
|
|
243
244
|
|
|
244
245
|
Rules:
|
|
246
|
+
- When workItem.titleSource is coordinator_pending, include a concise title (prefer 6–12 words or a short Chinese phrase; at most 200 characters) in decision.title. This display label summarizes the original goal; it is not a contractPatch and must not rewrite or shorten the goal. Otherwise leave decision.title null. A missing or oversized display title is normalized by the runtime and must not change the substantive decision.
|
|
245
247
|
- answer: explain state only. Never use it for an automatic advance trigger.
|
|
246
|
-
- Never mutate
|
|
248
|
+
- Never mutate goal, acceptanceCriteria, or deliveryTarget during automatic advance/recovery. decision.title is the only automatic title-generation path and is allowed only while titleSource is coordinator_pending; contractPatch (including a user-specified title) is allowed only for explicit user-originated refinement, never to make existing evidence pass. For an older WorkItem with no acceptance criteria, request_human to establish its completion condition before commissioning new work.
|
|
247
249
|
- create_actions: create 1..8 currently runnable Actions. Every Action needs type, objective, approach, expectedOutcome, capability, candidateVpIds, assignmentReason, sourceActionIds, workspaceMode, and optional maxAttempts/separateFromActionTypes. sourceActionIds are context/audit references, never scheduling dependencies. Do not include dependsOnActionIds, dependsOnStageIds, stages, or a graph.
|
|
248
250
|
- A missing skill/capability label is not a missing execution capability. Prefer an existing VP with a task-specific brief. Missing tools, credentials, or authorization require request_human; never expand roles as a workaround. create_vp is only appropriate when creating a persistent role is itself an explicit user deliverable.
|
|
249
251
|
- closeActions may accompany create_actions. Each entry is {"actionId":"failed or waiting durable Action id","reason":"why it is no longer required"}. Close only work made obsolete by replacement evidence or a clarified contract. Closed Actions remain audit history, are never acceptance evidence, and do not block completion.
|
|
@@ -290,6 +292,13 @@ function cleanText(value, limit, name) {
|
|
|
290
292
|
return text;
|
|
291
293
|
}
|
|
292
294
|
|
|
295
|
+
function coordinatorDisplayTitle(value, detail) {
|
|
296
|
+
if (detail.titleSource !== 'coordinator_pending') return null;
|
|
297
|
+
const proposed = typeof value === 'string' ? value.trim() : '';
|
|
298
|
+
const fallback = String(detail.goal || detail.title || 'Work Item').trim().replace(/\s+/g, ' ');
|
|
299
|
+
return (proposed || fallback || 'Work Item').slice(0, 200);
|
|
300
|
+
}
|
|
301
|
+
|
|
293
302
|
function requiresDeliveryBoundaryDecision(detail, actions) {
|
|
294
303
|
const requested = Array.isArray(actions) ? actions : [];
|
|
295
304
|
return !detail?.deliveryTarget && requested.some(action => (
|
|
@@ -460,8 +469,9 @@ export function normalizeCoordinatorResponse(value, detail, options = {}) {
|
|
|
460
469
|
const kind = allowedKinds.includes(source.kind) ? source.kind : '';
|
|
461
470
|
if (!kind) throw new Error('Work Center Coordinator decision kind is invalid');
|
|
462
471
|
const reason = cleanText(source.reason, 2_000, 'decision reason');
|
|
472
|
+
const generatedTitle = coordinatorDisplayTitle(source.title, detail);
|
|
463
473
|
if (kind === 'answer') {
|
|
464
|
-
return { reply, decision: { kind, reason, contractPatch: null, guidance: [], actions: [] } };
|
|
474
|
+
return { reply, decision: { kind, reason, title: generatedTitle, contractPatch: null, guidance: [], actions: [] } };
|
|
465
475
|
}
|
|
466
476
|
if (kind === 'guide_actions') {
|
|
467
477
|
const guidance = normalizeGuidance(source.guidance, detail);
|
|
@@ -478,6 +488,7 @@ export function normalizeCoordinatorResponse(value, detail, options = {}) {
|
|
|
478
488
|
decision: {
|
|
479
489
|
kind,
|
|
480
490
|
reason,
|
|
491
|
+
title: generatedTitle,
|
|
481
492
|
contractPatch: null,
|
|
482
493
|
guidance,
|
|
483
494
|
actions: [],
|
|
@@ -491,6 +502,7 @@ export function normalizeCoordinatorResponse(value, detail, options = {}) {
|
|
|
491
502
|
decision: {
|
|
492
503
|
kind,
|
|
493
504
|
reason,
|
|
505
|
+
title: generatedTitle,
|
|
494
506
|
question: cleanText(source.question, COORDINATOR_MAX_REPLY_CHARS, 'human question'),
|
|
495
507
|
contractPatch: dynamic && options.automatic !== true ? contractPatch : null,
|
|
496
508
|
guidance: [],
|
|
@@ -504,6 +516,7 @@ export function normalizeCoordinatorResponse(value, detail, options = {}) {
|
|
|
504
516
|
decision: {
|
|
505
517
|
kind,
|
|
506
518
|
reason,
|
|
519
|
+
title: generatedTitle,
|
|
507
520
|
closeActions: normalizeDynamicActionClosures(source.closeActions, detail.actions || []),
|
|
508
521
|
completion: source.completion,
|
|
509
522
|
contractPatch: null,
|
|
@@ -520,6 +533,7 @@ export function normalizeCoordinatorResponse(value, detail, options = {}) {
|
|
|
520
533
|
const decision = {
|
|
521
534
|
kind,
|
|
522
535
|
reason,
|
|
536
|
+
title: generatedTitle,
|
|
523
537
|
workItemType: source.workItemType,
|
|
524
538
|
contractPatch,
|
|
525
539
|
closeActions: source.closeActions,
|
|
@@ -551,6 +565,7 @@ export function normalizeCoordinatorResponse(value, detail, options = {}) {
|
|
|
551
565
|
decision: {
|
|
552
566
|
kind,
|
|
553
567
|
reason,
|
|
568
|
+
title: generatedTitle,
|
|
554
569
|
contractPatch,
|
|
555
570
|
guidance: [],
|
|
556
571
|
actions: normalizeCoordinatorActionReferences(source.actions, detail),
|
|
@@ -616,6 +631,7 @@ export function coordinatorSnapshot(detail) {
|
|
|
616
631
|
ledgerRevision: detail.ledgerRevision,
|
|
617
632
|
status: truncateUtf8(detail.status, 64),
|
|
618
633
|
title: truncateUtf8(detail.title, 1 * 1024),
|
|
634
|
+
titleSource: detail.titleSource || 'explicit',
|
|
619
635
|
goal: truncateUtf8(detail.goal, 4 * 1024),
|
|
620
636
|
deliveryTarget: detail.deliveryTarget || null,
|
|
621
637
|
acceptanceCriteria,
|
|
@@ -793,18 +793,39 @@ function enforceWorkItemBrowserDtoBudget(value, options = {}) {
|
|
|
793
793
|
if (jsonByteLength(dto) <= MAX_WORK_ITEM_BROWSER_DTO_BYTES) return dto;
|
|
794
794
|
|
|
795
795
|
const originalCount = actions.length;
|
|
796
|
-
|
|
796
|
+
// Detail evidence and attempt links need the Action identity AND its readable
|
|
797
|
+
// name. Drop bulky context/dependencies before dropping their navigation
|
|
798
|
+
// targets. If even these stubs cannot fit, the minimal fallback below omits
|
|
799
|
+
// evidence with the Actions instead of leaving known sources unresolvable.
|
|
800
|
+
const retained = Array.isArray(workItem.runReferences) && Array.isArray(workItem.actions)
|
|
801
|
+
? actions.map(action => ({
|
|
802
|
+
id: action.id,
|
|
803
|
+
sequence: action.sequence,
|
|
804
|
+
generation: action.generation,
|
|
805
|
+
type: action.type,
|
|
806
|
+
stageId: action.stageId,
|
|
807
|
+
status: action.status,
|
|
808
|
+
progressRevision: action.progressRevision,
|
|
809
|
+
messageCount: action.messageCount,
|
|
810
|
+
createdAt: action.createdAt,
|
|
811
|
+
updatedAt: action.updatedAt,
|
|
812
|
+
executionDurationMs: action.executionDurationMs,
|
|
813
|
+
executionStartedAt: action.executionStartedAt,
|
|
814
|
+
brief: { objective: truncateUtf8(action.brief?.objective || action.contentSummary || '', 512) },
|
|
815
|
+
}))
|
|
816
|
+
: keep ? [stripActionBody(keep, true)] : [];
|
|
797
817
|
if (Array.isArray(workItem.actions)) workItem.actions = retained;
|
|
798
818
|
else workItem.actionStats = retained;
|
|
799
819
|
workItem.omittedActionCount = originalCount - retained.length;
|
|
800
820
|
if (jsonByteLength(dto) <= MAX_WORK_ITEM_BROWSER_DTO_BYTES) return dto;
|
|
801
821
|
|
|
802
|
-
if (retained[0]) {
|
|
822
|
+
if (retained[0] && !Array.isArray(workItem.runReferences)) {
|
|
803
823
|
delete retained[0].brief;
|
|
804
824
|
delete retained[0].failure;
|
|
805
825
|
}
|
|
806
826
|
workItem.title = truncateUtf8(workItem.title, 4 * 1024);
|
|
807
827
|
workItem.goal = truncateUtf8(workItem.goal, 4 * 1024);
|
|
828
|
+
if (workItem.requirement !== undefined) workItem.requirement = truncateUtf8(workItem.requirement, 4 * 1024);
|
|
808
829
|
workItem.waitingReason = truncateUtf8(workItem.waitingReason, 4 * 1024);
|
|
809
830
|
workItem.actionSummary = truncateUtf8(workItem.actionSummary, 4 * 1024);
|
|
810
831
|
if (Array.isArray(workItem.acceptanceCriteria)) workItem.acceptanceCriteria = [];
|
|
@@ -817,6 +838,7 @@ function enforceWorkItemBrowserDtoBudget(value, options = {}) {
|
|
|
817
838
|
revision: count(workItem.revision),
|
|
818
839
|
title: truncateUtf8(workItem.title, 4 * 1024),
|
|
819
840
|
goal: truncateUtf8(workItem.goal, 4 * 1024),
|
|
841
|
+
...(workItem.requirement !== undefined ? { requirement: truncateUtf8(workItem.requirement, 4 * 1024) } : {}),
|
|
820
842
|
status: truncateUtf8(workItem.status, 256),
|
|
821
843
|
currentActionId: truncateUtf8(workItem.currentActionId, 4 * 1024) || null,
|
|
822
844
|
executionStats: workItem.executionStats,
|
|
@@ -1054,6 +1076,7 @@ export function projectWorkItemDetail(detail, options = {}) {
|
|
|
1054
1076
|
.map(risk => truncateUtf8(risk, MAX_ACTION_MESSAGE_CHARS)).slice(0, 24) : [],
|
|
1055
1077
|
} : null,
|
|
1056
1078
|
title: detail.title,
|
|
1079
|
+
requirement: detail.requirement ?? detail.goal,
|
|
1057
1080
|
goal: detail.goal,
|
|
1058
1081
|
acceptanceCriteria: Array.isArray(detail.acceptanceCriteria) ? detail.acceptanceCriteria : [],
|
|
1059
1082
|
goalProgress: projectGoalProgress(detail.goalProgress),
|
|
@@ -1127,6 +1150,25 @@ export function projectWorkItemDetail(detail, options = {}) {
|
|
|
1127
1150
|
}))
|
|
1128
1151
|
: [],
|
|
1129
1152
|
};
|
|
1153
|
+
// Only identities needed by visible evidence links, never Run bodies or traces.
|
|
1154
|
+
// Historical evidence may belong to an older generation of the same Action.
|
|
1155
|
+
const evidenceRunIds = new Set([
|
|
1156
|
+
...(projected.goalProgress?.evidenceRunIds || []),
|
|
1157
|
+
...(projected.goalProgress?.criteria || []).flatMap(check => check.evidenceRunIds || []),
|
|
1158
|
+
...(projected.goalProgress?.delivery?.evidenceRunIds || []),
|
|
1159
|
+
...(projected.finalResult?.responses || []).map(response => response.runId),
|
|
1160
|
+
...projected.outputs.map(output => output.runId),
|
|
1161
|
+
].filter(Boolean));
|
|
1162
|
+
const actionIds = new Set(projected.actions.map(action => action.id));
|
|
1163
|
+
// The evidence collections above already have bounded browser projections.
|
|
1164
|
+
// Keep every retained reference resolvable; an independent count cap would
|
|
1165
|
+
// starve later criteria and delivery evidence. The whole DTO budget still applies.
|
|
1166
|
+
projected.runReferences = [...evidenceRunIds].flatMap(id => {
|
|
1167
|
+
const run = runById.get(id);
|
|
1168
|
+
if (!run || !actionIds.has(run.actionId)
|
|
1169
|
+
|| (run.workItemId && run.workItemId !== detail.id)) return [];
|
|
1170
|
+
return [{ id: truncateUtf8(id, 256), actionId: truncateUtf8(run.actionId, 256) }];
|
|
1171
|
+
});
|
|
1130
1172
|
return enforceWorkItemBrowserDtoBudget(projected, { keepActionId: liveActionId });
|
|
1131
1173
|
}
|
|
1132
1174
|
|
|
@@ -248,11 +248,16 @@ export class WorkCenterService {
|
|
|
248
248
|
});
|
|
249
249
|
const shouldStart = payload.start === undefined ? settings.startImmediately : payload.start !== false;
|
|
250
250
|
const goal = requiredString(payload.goal, 'goal');
|
|
251
|
+
const explicitTitle = payload.titleSource !== 'coordinator_pending' && typeof payload.title === 'string'
|
|
252
|
+
? payload.title.trim() : '';
|
|
251
253
|
const requestedCriteria = Array.isArray(payload.acceptanceCriteria)
|
|
252
254
|
? payload.acceptanceCriteria.map(value => String(value).trim()).filter(Boolean) : [];
|
|
253
255
|
this.controller.create({
|
|
254
256
|
id: workItemId,
|
|
255
|
-
|
|
257
|
+
// Goal remains the original requirement. Until coordination succeeds,
|
|
258
|
+
// title is only a backwards-compatible display fallback.
|
|
259
|
+
title: explicitTitle || goal.replace(/\s+/g, ' ').slice(0, 80),
|
|
260
|
+
titleSource: explicitTitle ? 'explicit' : 'coordinator_pending',
|
|
256
261
|
goal,
|
|
257
262
|
// With no separate criteria, the user's goal itself is the minimum
|
|
258
263
|
// contract. Do not force a follow-up or invent broader requirements.
|
|
@@ -113,6 +113,8 @@ function mapWorkItem(row) {
|
|
|
113
113
|
finalResult: parseJson(row.final_result, null),
|
|
114
114
|
deliveryTarget: row.delivery_target || null,
|
|
115
115
|
title: row.title,
|
|
116
|
+
titleSource: row.title_source || 'explicit',
|
|
117
|
+
requirement: row.requirement ?? row.goal,
|
|
116
118
|
goal: row.goal,
|
|
117
119
|
acceptanceCriteria: parseJson(row.acceptance_criteria, []),
|
|
118
120
|
workflowTemplate: row.workflow_template,
|
|
@@ -857,6 +859,8 @@ export class WorkItemStore {
|
|
|
857
859
|
final_result TEXT,
|
|
858
860
|
delivery_target TEXT,
|
|
859
861
|
title TEXT NOT NULL,
|
|
862
|
+
title_source TEXT NOT NULL DEFAULT 'explicit',
|
|
863
|
+
requirement TEXT,
|
|
860
864
|
goal TEXT NOT NULL,
|
|
861
865
|
acceptance_criteria TEXT NOT NULL,
|
|
862
866
|
workflow_template TEXT NOT NULL,
|
|
@@ -1139,6 +1143,15 @@ export class WorkItemStore {
|
|
|
1139
1143
|
if (!hasColumn(this.db, 'work_items', 'delivery_target')) {
|
|
1140
1144
|
this.db.exec('ALTER TABLE work_items ADD COLUMN delivery_target TEXT');
|
|
1141
1145
|
}
|
|
1146
|
+
if (!hasColumn(this.db, 'work_items', 'title_source')) {
|
|
1147
|
+
// Old titles were explicit under the previous creation contract.
|
|
1148
|
+
this.db.exec("ALTER TABLE work_items ADD COLUMN title_source TEXT NOT NULL DEFAULT 'explicit'");
|
|
1149
|
+
}
|
|
1150
|
+
if (!hasColumn(this.db, 'work_items', 'requirement')) {
|
|
1151
|
+
// Old items cannot reconstruct an earlier goal; retain their current contract.
|
|
1152
|
+
this.db.exec('ALTER TABLE work_items ADD COLUMN requirement TEXT');
|
|
1153
|
+
this.db.exec('UPDATE work_items SET requirement = goal');
|
|
1154
|
+
}
|
|
1142
1155
|
if (!hasColumn(this.db, 'actions', 'source_action_ids')) {
|
|
1143
1156
|
this.db.exec("ALTER TABLE actions ADD COLUMN source_action_ids TEXT NOT NULL DEFAULT '[]'");
|
|
1144
1157
|
}
|
|
@@ -2387,15 +2400,17 @@ export class WorkItemStore {
|
|
|
2387
2400
|
const workspaceKey = canonicalWorkspaceKey(input.workDir);
|
|
2388
2401
|
this.db.prepare(`INSERT INTO work_items
|
|
2389
2402
|
(id, revision, execution_schema_version, ledger_revision, coordination_mode, final_result, delivery_target,
|
|
2390
|
-
title, goal, acceptance_criteria, workflow_template, workflow_snapshot, status,
|
|
2403
|
+
title, title_source, requirement, goal, acceptance_criteria, workflow_template, workflow_snapshot, status,
|
|
2391
2404
|
current_action_id, current_run_id, work_dir, workspace_key, reuse_memory, origin, linked_session_ids,
|
|
2392
2405
|
session_context, attachments, created_at, updated_at)
|
|
2393
|
-
VALUES (?, 1, ?, 0, ?, NULL, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
|
2406
|
+
VALUES (?, 1, ?, 0, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
|
2394
2407
|
id,
|
|
2395
2408
|
Number.isInteger(input.executionSchemaVersion) ? input.executionSchemaVersion : 2,
|
|
2396
2409
|
input.coordinationMode || 'legacy',
|
|
2397
2410
|
input.deliveryTarget || null,
|
|
2398
2411
|
input.title,
|
|
2412
|
+
input.titleSource === 'coordinator_pending' ? 'coordinator_pending' : 'explicit',
|
|
2413
|
+
input.goal,
|
|
2399
2414
|
input.goal,
|
|
2400
2415
|
stringify(input.acceptanceCriteria || []),
|
|
2401
2416
|
input.workflowTemplate || 'software-change',
|
|
@@ -3759,6 +3774,19 @@ export class WorkItemStore {
|
|
|
3759
3774
|
turnId, result, expected, workItem, messages, assistantIndex, activeActions, now,
|
|
3760
3775
|
}) {
|
|
3761
3776
|
const decision = result?.decision || {};
|
|
3777
|
+
const generatedTitle = workItem.titleSource === 'coordinator_pending'
|
|
3778
|
+
? (typeof decision.title === 'string' && decision.title.trim()
|
|
3779
|
+
? decision.title.trim().slice(0, 200)
|
|
3780
|
+
: String(workItem.goal || workItem.title || 'Work Item').trim().replace(/\s+/g, ' ').slice(0, 200))
|
|
3781
|
+
: null;
|
|
3782
|
+
if (generatedTitle) {
|
|
3783
|
+
const changedTitle = this.db.prepare(`UPDATE work_items SET title = ?, title_source = 'coordinator',
|
|
3784
|
+
updated_at = ? WHERE id = ? AND title_source = 'coordinator_pending'`).run(
|
|
3785
|
+
generatedTitle, now, workItem.id,
|
|
3786
|
+
);
|
|
3787
|
+
if (Number(changedTitle.changes) !== 1) throw new Error('Coordinator title generation lost its fence');
|
|
3788
|
+
workItem = this.getWorkItem(workItem.id);
|
|
3789
|
+
}
|
|
3762
3790
|
const decisionPatch = normalizeContractPatch(decision.contractPatch);
|
|
3763
3791
|
const mutationPatch = normalizeContractPatch(result?.mutation?.contractPatch);
|
|
3764
3792
|
if (JSON.stringify(decisionPatch) !== JSON.stringify(mutationPatch) && mutationPatch) {
|
|
@@ -3783,17 +3811,23 @@ export class WorkItemStore {
|
|
|
3783
3811
|
throw new Error('WorkItem delivery target must be confirmed before creating mutating or delivery Actions');
|
|
3784
3812
|
}
|
|
3785
3813
|
const refined = { ...workItem, ...(contractPatch || {}) };
|
|
3814
|
+
const hasExplicitTitle = Object.hasOwn(contractPatch || {}, 'title');
|
|
3786
3815
|
const contractChanged = ['title', 'goal', 'deliveryTarget', 'acceptanceCriteria']
|
|
3787
3816
|
.some(key => JSON.stringify(refined[key]) !== JSON.stringify(workItem[key]));
|
|
3788
3817
|
if (contractChanged) {
|
|
3789
3818
|
this.#invalidateExecution(workItem, 'superseded', 'superseded', 'User refined the WorkItem contract', now);
|
|
3790
3819
|
this.db.prepare(`UPDATE work_items SET title = ?, goal = ?, acceptance_criteria = ?,
|
|
3791
|
-
delivery_target = ?,
|
|
3820
|
+
delivery_target = ?, title_source = CASE WHEN ? THEN 'explicit' ELSE title_source END,
|
|
3821
|
+
revision = revision + 1, updated_at = ? WHERE id = ? AND revision = ?`).run(
|
|
3792
3822
|
refined.title, refined.goal, stringify(refined.acceptanceCriteria), refined.deliveryTarget,
|
|
3793
|
-
now, workItem.id, workItem.revision,
|
|
3823
|
+
hasExplicitTitle ? 1 : 0, now, workItem.id, workItem.revision,
|
|
3794
3824
|
);
|
|
3795
3825
|
workItem = this.getWorkItem(workItem.id);
|
|
3796
3826
|
activeActions = [];
|
|
3827
|
+
} else if (hasExplicitTitle && workItem.titleSource !== 'explicit') {
|
|
3828
|
+
this.db.prepare(`UPDATE work_items SET title_source = 'explicit', updated_at = ?
|
|
3829
|
+
WHERE id = ? AND revision = ?`).run(now, workItem.id, workItem.revision);
|
|
3830
|
+
workItem = this.getWorkItem(workItem.id);
|
|
3797
3831
|
}
|
|
3798
3832
|
|
|
3799
3833
|
if (decision.kind === 'create_actions') {
|