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