@yeaft/webchat-agent 0.1.763 → 0.1.766

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.
@@ -1,713 +0,0 @@
1
- /**
2
- * Feature management tools — persistent feature tracking for Unify work.
3
- *
4
- * Features are persisted to ~/.yeaft/features/ via FeatureStore (one folder
5
- * per feature). Call initFeatureStore(yeaftDir) during session init before
6
- * tools are used.
7
- *
8
- * NOTE (PR-1a refactor): renamed from task-tools.js. Tools renamed
9
- * Task* → Feature*, exported symbols renamed (taskCreate → featureCreate,
10
- * etc.), tool param `task_id` → `feature_id`, parent param `parent_id`
11
- * is unchanged (still refers to a parent feature). The internal
12
- * feature-store object field names (parentTaskId, taskId, relatedTaskIds,
13
- * etc.) are still in use here — those rename to feature-equivalents in
14
- * PR-1b along with the memory schema migration. No backwards-compat
15
- * aliases are kept (per project policy).
16
- */
17
-
18
- import { defineTool } from './types.js';
19
- import { randomUUID } from 'crypto';
20
- import { FeatureStore } from '../features/store.js';
21
-
22
- /** @type {FeatureStore|null} */
23
- let featureStore = null;
24
-
25
- /**
26
- * Initialize the feature store with the yeaft directory.
27
- * Must be called during session startup before any feature tools are used.
28
- * @param {string} yeaftDir — Base ~/.yeaft directory
29
- * @param {{ readOnly?: boolean }} [opts]
30
- */
31
- export function initFeatureStore(yeaftDir, opts) {
32
- featureStore = new FeatureStore(yeaftDir, opts);
33
- }
34
-
35
- /** Get the feature store instance (for other tools/tests). */
36
- export function getFeatureStore() {
37
- return featureStore;
38
- }
39
-
40
- /** Get current plan text. */
41
- export function getPlan() {
42
- return featureStore ? featureStore.getPlan() : '';
43
- }
44
-
45
- /** Internal helper — ensure store is initialized. */
46
- function requireStore() {
47
- if (!featureStore) {
48
- return '{"error":"Feature store not initialized. Session may still be loading."}';
49
- }
50
- return null;
51
- }
52
-
53
- // ─── FeatureCreate ──────────────────────────────────────
54
-
55
- export const featureCreate = defineTool({
56
- name: 'FeatureCreate',
57
- description: `Create a new feature for tracking work progress.
58
-
59
- Features have a title, description, priority, and status.
60
- Each feature gets its own folder with feature.md, progress.md, and memory.md.
61
- Use this to break down complex work into trackable items.
62
-
63
- Pass \`parent_id\` to create a sub-feature under an existing feature.
64
-
65
- R6 multi-VP groups (Unify): pass \`group_id\` + \`members\` to create a
66
- collaborative feature inside a group. The caller's vpId becomes the feature
67
- \`initiator\`. \`members\` MUST be a subset of the group's roster — the
68
- tool validates this server-side and returns a \`not_in_roster\` error
69
- otherwise. The user owns invitations; the tool will not auto-invite.
70
-
71
- Use \`related_feature_ids\` to soft-link to other features (cross-group OK).`,
72
- parameters: {
73
- type: 'object',
74
- properties: {
75
- title: {
76
- type: 'string',
77
- description: 'Short feature title',
78
- },
79
- description: {
80
- type: 'string',
81
- description: 'Detailed feature description',
82
- },
83
- priority: {
84
- type: 'string',
85
- enum: ['low', 'medium', 'high', 'critical'],
86
- description: 'Feature priority (default: "medium")',
87
- },
88
- parent_id: {
89
- type: 'string',
90
- description: 'Parent feature ID for subtasks',
91
- },
92
- group_id: {
93
- type: 'string',
94
- description: 'R6: group this feature belongs to. Required for multi-VP collaboration features.',
95
- },
96
- members: {
97
- type: 'array',
98
- items: { type: 'string' },
99
- description: 'R6: VP ids participating in this feature (≥1). MUST be ⊆ group roster.',
100
- },
101
- related_feature_ids: {
102
- type: 'array',
103
- items: { type: 'string' },
104
- description: 'R6: soft-linked feature ids (cross-group OK). See arch §14.',
105
- },
106
- // PR-1a: legacy `parent_feature_id` (originally `parent_task_id`,
107
- // an alias absorbed from the former SpawnTask tool) was removed.
108
- // Use `parent_id` to create a sub-feature.
109
- },
110
- required: ['title'],
111
- },
112
- isConcurrencySafe: () => false,
113
- isReadOnly: () => false,
114
- async execute(input, ctx) {
115
- const err = requireStore();
116
- if (err) return err;
117
-
118
- const {
119
- title,
120
- description,
121
- priority = 'medium',
122
- parent_id,
123
- group_id,
124
- members,
125
- related_feature_ids,
126
- } = input;
127
- if (!title) return JSON.stringify({ error: 'title is required' });
128
-
129
- const parentId = parent_id || null;
130
- if (parentId && !featureStore.get(parentId)) {
131
- return JSON.stringify({ error: `Parent feature not found: ${parentId}` });
132
- }
133
-
134
- // R6 multi-VP fields — validated only when group_id is present, so
135
- // legacy single-tenant TaskCreate calls keep working.
136
- let groupId = null;
137
- let normalizedMembers = null;
138
- let initiator = null;
139
- if (group_id) {
140
- groupId = String(group_id);
141
-
142
- // Validate members ⊆ roster. We resolve the roster via the tool ctx
143
- // because the tool layer must not import group-store directly (loose
144
- // coupling — ctx.getGroupRoster is wired in session.js).
145
- let roster = null;
146
- if (typeof ctx?.getGroupRoster === 'function') {
147
- try { roster = ctx.getGroupRoster(groupId); } catch { roster = null; }
148
- }
149
- if (!Array.isArray(roster)) {
150
- return JSON.stringify({
151
- error: 'group_not_found',
152
- hint: `group ${groupId} has no roster (group not loaded or doesn't exist)`,
153
- });
154
- }
155
-
156
- // Default members to [initiator] if not given (R6 §1.5: members ≥ 1).
157
- const callerVpId = ctx?.currentVpId || null;
158
- const candidateMembers = Array.isArray(members) && members.length > 0
159
- ? members.map(String)
160
- : (callerVpId ? [callerVpId] : []);
161
- if (candidateMembers.length === 0) {
162
- return JSON.stringify({
163
- error: 'no_members',
164
- hint: 'Specify members[] (≥1) or call from a VP context (currentVpId resolves to self).',
165
- });
166
- }
167
- const offRoster = candidateMembers.filter((m) => !roster.includes(m));
168
- if (offRoster.length > 0) {
169
- return JSON.stringify({
170
- error: 'not_in_roster',
171
- offRoster,
172
- roster,
173
- hint: 'These VP ids are not in the group roster. Ask the user to invite them first; do not auto-invite.',
174
- });
175
- }
176
- // Always include the caller as a member (initiator must be ∈ members).
177
- if (callerVpId && !candidateMembers.includes(callerVpId)) {
178
- if (!roster.includes(callerVpId)) {
179
- return JSON.stringify({
180
- error: 'caller_not_in_roster',
181
- hint: `caller VP ${callerVpId} is not in group ${groupId} roster.`,
182
- });
183
- }
184
- candidateMembers.unshift(callerVpId);
185
- }
186
- normalizedMembers = Array.from(new Set(candidateMembers));
187
- initiator = callerVpId;
188
- }
189
-
190
- const id = `feat-${randomUUID().slice(0, 8)}`;
191
- const feature = {
192
- id,
193
- title,
194
- description: description || '',
195
- priority,
196
- status: 'pending',
197
- parentId,
198
- parentTaskId: parentId, // design §5 canonical field
199
- createdAt: Date.now(),
200
- updatedAt: Date.now(),
201
- };
202
- if (groupId) {
203
- feature.groupId = groupId;
204
- feature.members = normalizedMembers;
205
- if (initiator) feature.initiator = initiator;
206
- if (Array.isArray(related_feature_ids) && related_feature_ids.length) {
207
- feature.relatedTaskIds = related_feature_ids.map(String);
208
- }
209
- }
210
-
211
- featureStore.create(feature);
212
-
213
- return JSON.stringify({
214
- success: true,
215
- feature: {
216
- id,
217
- title,
218
- priority,
219
- status: 'pending',
220
- parentTaskId: parentId,
221
- groupId: groupId || undefined,
222
- members: normalizedMembers || undefined,
223
- initiator: initiator || undefined,
224
- },
225
- message: groupId
226
- ? `Feature created in group ${groupId}: ${title} (${id}) with members [${(normalizedMembers || []).join(', ')}]`
227
- : parentId
228
- ? `Sub-feature created: ${title} (${id}) under ${parentId}`
229
- : `Feature created: ${title} (${id})`,
230
- });
231
- },
232
- });
233
-
234
- // ─── FeatureUpdate ──────────────────────────────────────
235
-
236
- export const featureUpdate = defineTool({
237
- name: 'FeatureUpdate',
238
- description: `Update a feature's status, priority, or details.
239
-
240
- Status values: pending, in_progress, completed, blocked, cancelled`,
241
- parameters: {
242
- type: 'object',
243
- properties: {
244
- feature_id: {
245
- type: 'string',
246
- description: 'Feature ID to update',
247
- },
248
- status: {
249
- type: 'string',
250
- enum: ['pending', 'in_progress', 'completed', 'blocked', 'cancelled'],
251
- description: 'New feature status',
252
- },
253
- priority: {
254
- type: 'string',
255
- enum: ['low', 'medium', 'high', 'critical'],
256
- description: 'New priority',
257
- },
258
- title: {
259
- type: 'string',
260
- description: 'Updated title',
261
- },
262
- description: {
263
- type: 'string',
264
- description: 'Updated description',
265
- },
266
- result: {
267
- type: 'string',
268
- description: 'Feature result or completion notes',
269
- },
270
- },
271
- required: ['feature_id'],
272
- },
273
- isConcurrencySafe: () => false,
274
- isReadOnly: () => false,
275
- async execute(input, ctx) {
276
- const err = requireStore();
277
- if (err) return err;
278
-
279
- const { feature_id, status, priority, title, description, result } = input;
280
- if (!feature_id) return JSON.stringify({ error: 'feature_id is required' });
281
-
282
- const updates = {};
283
- if (status) updates.status = status;
284
- if (priority) updates.priority = priority;
285
- if (title) updates.title = title;
286
- if (description !== undefined) updates.description = description;
287
- if (result) updates.result = result;
288
-
289
- const feature = featureStore.update(feature_id, updates);
290
- if (!feature) return JSON.stringify({ error: `Feature not found: ${feature_id}` });
291
-
292
- return JSON.stringify({
293
- success: true,
294
- feature: { id: feature.id, title: feature.title, status: feature.status, priority: feature.priority },
295
- message: `Feature "${feature.title}" updated`,
296
- });
297
- },
298
- });
299
-
300
- // ─── FeatureList ────────────────────────────────────────
301
-
302
- export const featureList = defineTool({
303
- name: 'FeatureList',
304
- description: `List all tracked features with their status.
305
-
306
- Shows feature IDs, titles, status, and priority. Filter by status if needed.`,
307
- parameters: {
308
- type: 'object',
309
- properties: {
310
- status: {
311
- type: 'string',
312
- enum: ['pending', 'in_progress', 'completed', 'blocked', 'cancelled'],
313
- description: 'Filter by status (optional)',
314
- },
315
- include_completed: {
316
- type: 'boolean',
317
- description: 'Include completed features (default: true)',
318
- },
319
- },
320
- },
321
- isConcurrencySafe: () => true,
322
- isReadOnly: () => true,
323
- async execute(input, ctx) {
324
- const err = requireStore();
325
- if (err) return err;
326
-
327
- const { status, include_completed = true } = input;
328
-
329
- let results = featureStore.list(status ? { status } : undefined);
330
- if (!include_completed) {
331
- results = results.filter(t => t.status !== 'completed');
332
- }
333
-
334
- const taskItems = results.map(t => ({
335
- id: t.id,
336
- title: t.title,
337
- status: t.status,
338
- priority: t.priority,
339
- parentId: t.parentId,
340
- hasResult: !!t.result,
341
- }));
342
-
343
- // Sort: in_progress first, then pending, then others
344
- const ORDER = { in_progress: 0, pending: 1, blocked: 2, completed: 3, cancelled: 4 };
345
- taskItems.sort((a, b) => (ORDER[a.status] ?? 5) - (ORDER[b.status] ?? 5));
346
-
347
- return JSON.stringify({
348
- features: taskItems,
349
- totalCount: taskItems.length,
350
- summary: {
351
- pending: taskItems.filter(t => t.status === 'pending').length,
352
- in_progress: taskItems.filter(t => t.status === 'in_progress').length,
353
- completed: taskItems.filter(t => t.status === 'completed').length,
354
- blocked: taskItems.filter(t => t.status === 'blocked').length,
355
- },
356
- }, null, 2);
357
- },
358
- });
359
-
360
- // ─── FeatureGet ─────────────────────────────────────────
361
-
362
- export const featureGet = defineTool({
363
- name: 'FeatureGet',
364
- description: `Get detailed information about a specific feature, including its progress log and memory.`,
365
- parameters: {
366
- type: 'object',
367
- properties: {
368
- feature_id: {
369
- type: 'string',
370
- description: 'Feature ID to retrieve',
371
- },
372
- },
373
- required: ['feature_id'],
374
- },
375
- isConcurrencySafe: () => true,
376
- isReadOnly: () => true,
377
- async execute(input, ctx) {
378
- const err = requireStore();
379
- if (err) return err;
380
-
381
- const { feature_id } = input;
382
- if (!feature_id) return JSON.stringify({ error: 'feature_id is required' });
383
-
384
- const feature = featureStore.get(feature_id);
385
- if (!feature) return JSON.stringify({ error: `Feature not found: ${feature_id}` });
386
-
387
- // Find subtasks
388
- const allTasks = featureStore.list();
389
- const subtasks = allTasks
390
- .filter(t => t.parentId === feature_id)
391
- .map(t => ({ id: t.id, title: t.title, status: t.status }));
392
-
393
- return JSON.stringify({
394
- ...feature,
395
- subtasks,
396
- hasProgress: !!featureStore.getProgress(feature_id),
397
- hasMemory: !!featureStore.getMemory(feature_id),
398
- }, null, 2);
399
- },
400
- });
401
-
402
- // ─── FeatureProgress ────────────────────────────────────
403
-
404
- export const featureProgress = defineTool({
405
- name: 'FeatureProgress',
406
- description: `View or append to a feature's progress log.
407
-
408
- The progress log is an append-only timeline of what happened during feature execution.
409
- Use "view" to see the full log, or "append" to add a new entry.`,
410
- parameters: {
411
- type: 'object',
412
- properties: {
413
- feature_id: {
414
- type: 'string',
415
- description: 'Feature ID',
416
- },
417
- action: {
418
- type: 'string',
419
- enum: ['view', 'append'],
420
- description: '"view" shows progress log, "append" adds an entry',
421
- },
422
- note: {
423
- type: 'string',
424
- description: 'Progress note to append (required for "append")',
425
- },
426
- },
427
- required: ['feature_id', 'action'],
428
- },
429
- isConcurrencySafe: () => false,
430
- isReadOnly: (input) => input?.action === 'view',
431
- async execute(input, ctx) {
432
- const err = requireStore();
433
- if (err) return err;
434
-
435
- const { feature_id, action, note } = input;
436
- if (!feature_id) return JSON.stringify({ error: 'feature_id is required' });
437
-
438
- const feature = featureStore.get(feature_id);
439
- if (!feature) return JSON.stringify({ error: `Feature not found: ${feature_id}` });
440
-
441
- switch (action) {
442
- case 'view':
443
- return featureStore.getProgress(feature_id) || '(No progress entries yet)';
444
-
445
- case 'append':
446
- if (!note) return JSON.stringify({ error: 'note is required for "append"' });
447
- featureStore.appendProgress(feature_id, note, { status: feature.status });
448
- return JSON.stringify({ success: true, message: `Progress noted for "${feature.title}"` });
449
-
450
- default:
451
- return JSON.stringify({ error: `Unknown action: ${action}` });
452
- }
453
- },
454
- });
455
-
456
- // ─── FeatureMemory ──────────────────────────────────────
457
-
458
- export const featureMemory = defineTool({
459
- name: 'FeatureMemory',
460
- description: `View or update a feature's memory (context notes, key decisions, references).
461
-
462
- Feature memory stores persistent context relevant to the feature — key decisions,
463
- references to files, architectural notes, etc. Unlike progress (append-only),
464
- memory can be rewritten to keep it current.`,
465
- parameters: {
466
- type: 'object',
467
- properties: {
468
- feature_id: {
469
- type: 'string',
470
- description: 'Feature ID',
471
- },
472
- action: {
473
- type: 'string',
474
- enum: ['view', 'update'],
475
- description: '"view" shows memory, "update" replaces it',
476
- },
477
- content: {
478
- type: 'string',
479
- description: 'New memory content (required for "update")',
480
- },
481
- },
482
- required: ['feature_id', 'action'],
483
- },
484
- isConcurrencySafe: () => false,
485
- isReadOnly: (input) => input?.action === 'view',
486
- async execute(input, ctx) {
487
- const err = requireStore();
488
- if (err) return err;
489
-
490
- const { feature_id, action, content } = input;
491
- if (!feature_id) return JSON.stringify({ error: 'feature_id is required' });
492
-
493
- const feature = featureStore.get(feature_id);
494
- if (!feature) return JSON.stringify({ error: `Feature not found: ${feature_id}` });
495
-
496
- switch (action) {
497
- case 'view':
498
- return featureStore.getMemory(feature_id) || '(No memory entries yet)';
499
-
500
- case 'update':
501
- if (!content) return JSON.stringify({ error: 'content is required for "update"' });
502
- featureStore.updateMemory(feature_id, content);
503
- return JSON.stringify({ success: true, message: `Memory updated for "${feature.title}"` });
504
-
505
- default:
506
- return JSON.stringify({ error: `Unknown action: ${action}` });
507
- }
508
- },
509
- });
510
-
511
- // ─── FollowupFeature ────────────────────────────────────
512
-
513
- export const followupFeature = defineTool({
514
- name: 'FollowupFeature',
515
- description: `Create a follow-up feature linked to an existing feature.
516
-
517
- Use when a completed feature reveals additional work needed.
518
- The new feature is linked as a child of the original.`,
519
- parameters: {
520
- type: 'object',
521
- properties: {
522
- parent_feature_id: {
523
- type: 'string',
524
- description: 'ID of the original feature',
525
- },
526
- title: {
527
- type: 'string',
528
- description: 'Follow-up feature title',
529
- },
530
- description: {
531
- type: 'string',
532
- description: 'Why this follow-up is needed',
533
- },
534
- priority: {
535
- type: 'string',
536
- enum: ['low', 'medium', 'high', 'critical'],
537
- },
538
- },
539
- required: ['parent_feature_id', 'title'],
540
- },
541
- isConcurrencySafe: () => false,
542
- isReadOnly: () => false,
543
- async execute(input, ctx) {
544
- const err = requireStore();
545
- if (err) return err;
546
-
547
- const { parent_feature_id, title, description, priority = 'medium' } = input;
548
- if (!parent_feature_id) return JSON.stringify({ error: 'parent_feature_id is required' });
549
- if (!title) return JSON.stringify({ error: 'title is required' });
550
-
551
- const parent = featureStore.get(parent_feature_id);
552
- if (!parent) return JSON.stringify({ error: `Parent feature not found: ${parent_feature_id}` });
553
-
554
- const id = `feat-${randomUUID().slice(0, 8)}`;
555
- const feature = {
556
- id,
557
- title,
558
- description: description || `Follow-up to: ${parent.title}`,
559
- priority,
560
- status: 'pending',
561
- parentId: parent_feature_id,
562
- createdAt: Date.now(),
563
- updatedAt: Date.now(),
564
- };
565
-
566
- featureStore.create(feature);
567
-
568
- return JSON.stringify({
569
- success: true,
570
- feature: { id, title, priority, status: 'pending', parentId: parent_feature_id },
571
- message: `Follow-up feature created: ${title} (linked to ${parent.title})`,
572
- });
573
- },
574
- });
575
-
576
- // ─── UpdatePlan ─────────────────────────────────────────
577
-
578
- export const updatePlan = defineTool({
579
- name: 'UpdatePlan',
580
- description: `Update or view the current execution plan.
581
-
582
- The plan is a free-form markdown document that describes the overall
583
- approach, steps, and status of the current work.`,
584
- parameters: {
585
- type: 'object',
586
- properties: {
587
- action: {
588
- type: 'string',
589
- enum: ['view', 'update', 'append'],
590
- description: '"view" shows current plan, "update" replaces it, "append" adds to it',
591
- },
592
- content: {
593
- type: 'string',
594
- description: 'Plan content (for "update" and "append" actions)',
595
- },
596
- },
597
- required: ['action'],
598
- },
599
- isConcurrencySafe: () => false,
600
- isReadOnly: (input) => input?.action === 'view',
601
- async execute(input, ctx) {
602
- const err = requireStore();
603
- if (err) return err;
604
-
605
- const { action, content } = input;
606
-
607
- switch (action) {
608
- case 'view':
609
- return featureStore.getPlan() || '(No plan set yet)';
610
-
611
- case 'update':
612
- if (!content) return JSON.stringify({ error: 'content is required for "update"' });
613
- featureStore.setPlan(content);
614
- return JSON.stringify({ success: true, message: 'Plan updated', length: content.length });
615
-
616
- case 'append': {
617
- if (!content) return JSON.stringify({ error: 'content is required for "append"' });
618
- const existing = featureStore.getPlan();
619
- const newPlan = existing ? `${existing}\n\n${content}` : content;
620
- featureStore.setPlan(newPlan);
621
- return JSON.stringify({ success: true, message: 'Plan updated (appended)', length: newPlan.length });
622
- }
623
-
624
- default:
625
- return JSON.stringify({ error: `Unknown action: ${action}` });
626
- }
627
- },
628
- });
629
-
630
- // ─── FeatureSummaryPost (task-334n) ─────────────────────
631
-
632
- import { postSummary } from '../features/summary.js';
633
- import { openGroup } from '../groups/group-store.js';
634
- import { join } from 'path';
635
-
636
- /**
637
- * task-334n §B — initiator posts a progress summary to the group log.
638
- * Triggers the summary-extractor automatically (§C).
639
- */
640
- export const featureSummaryPost = defineTool({
641
- name: 'feature_summary_post',
642
- description: `Post a progress summary for a multi-VP feature (task-334n).
643
-
644
- Only the feature initiator should call this. The summary is written to the
645
- group message log as \`type=summary\` and auto-extracts 2-5 feature-memory
646
- entries (kind=progress|decision) via the feature-memory shard lib.
647
-
648
- To revise a prior summary, pass its msgId in \`supersedes\` — the old
649
- summary is marked \`supersededBy\` while staying on disk for audit.`,
650
- parameters: {
651
- type: 'object',
652
- properties: {
653
- feature_id: { type: 'string', description: 'Target feature id' },
654
- body: { type: 'string', description: 'Summary body (markdown)' },
655
- progress: { type: 'number', description: '0..100, optional' },
656
- supersedes: {
657
- type: 'array',
658
- items: { type: 'string' },
659
- description: 'Prior summary msgIds this revision supersedes',
660
- },
661
- },
662
- required: ['feature_id', 'body'],
663
- },
664
- isConcurrencySafe: () => false,
665
- isReadOnly: () => false,
666
- async execute(input, ctx) {
667
- const err = requireStore();
668
- if (err) return err;
669
- const { feature_id, body, progress, supersedes } = input || {};
670
- if (!feature_id || !body) {
671
- return JSON.stringify({ error: 'feature_id and body are required' });
672
- }
673
- const feature = featureStore.get(feature_id);
674
- if (!feature) return JSON.stringify({ error: `feature not found: ${feature_id}` });
675
- if (!feature.groupId) {
676
- return JSON.stringify({ error: 'feature has no groupId; summary requires a group' });
677
- }
678
-
679
- const currentVpId = ctx?.currentVpId;
680
- if (currentVpId && feature.initiator && currentVpId !== feature.initiator) {
681
- return JSON.stringify({ error: 'only the feature initiator may post summaries' });
682
- }
683
-
684
- const yeaftDir = ctx?.yeaftDir;
685
- if (!yeaftDir) {
686
- return JSON.stringify({ error: 'yeaftDir missing from tool context' });
687
- }
688
- const groupsRoot = join(yeaftDir, 'groups');
689
- const memoryDir = join(groupsRoot, feature.groupId, 'features', feature.id, 'memory');
690
-
691
- const group = openGroup(groupsRoot, feature.groupId);
692
- try {
693
- const res = postSummary({
694
- group,
695
- featureId: feature_id,
696
- fromVpId: currentVpId || feature.initiator || 'unknown',
697
- body,
698
- progress,
699
- supersedes,
700
- memoryDir,
701
- });
702
- return JSON.stringify({
703
- success: true,
704
- messageId: res.message.id,
705
- memoryIds: res.memoryIds,
706
- supersededSummaryIds: res.supersededSummaryIds,
707
- });
708
- } finally {
709
- group.close();
710
- }
711
- },
712
- });
713
-