@yeaft/webchat-agent 0.1.616 → 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.
@@ -48,25 +48,25 @@ import listAgents from './list-agents.js';
48
48
  // --- P1 Routing tools (task-334d) ---
49
49
  import routeForward from './route-forward.js';
50
50
 
51
- // --- P1 Task tools ---
51
+ // --- P1 Feature tools ---
52
52
  import {
53
- taskCreate,
54
- taskUpdate,
55
- taskList,
56
- taskGet,
57
- taskProgress,
58
- taskMemory,
59
- followupTask,
53
+ featureCreate,
54
+ featureUpdate,
55
+ featureList,
56
+ featureGet,
57
+ featureProgress,
58
+ featureMemory,
59
+ followupFeature,
60
60
  updatePlan,
61
- } from './task-tools.js';
61
+ featureSummaryPost,
62
+ } from './feature-tools.js';
62
63
 
63
64
  // --- P1 Thread tools (task-299 Phase 1) ---
64
65
  import {
65
66
  spawnThread,
66
67
  switchThread,
67
68
  listThreads,
68
- attachThreadToTask,
69
- spawnTask,
69
+ attachThreadToFeature,
70
70
  readThreadSummary,
71
71
  readThreadRecent,
72
72
  } from './thread-tools.js';
@@ -126,22 +126,22 @@ export const allTools = [
126
126
  // P1 Routing (task-334d)
127
127
  routeForward,
128
128
 
129
- // P1 Task
130
- taskCreate,
131
- taskUpdate,
132
- taskList,
133
- taskGet,
134
- taskProgress,
135
- taskMemory,
136
- followupTask,
129
+ // P1 Feature
130
+ featureCreate,
131
+ featureUpdate,
132
+ featureList,
133
+ featureGet,
134
+ featureProgress,
135
+ featureMemory,
136
+ followupFeature,
137
137
  updatePlan,
138
+ featureSummaryPost,
138
139
 
139
140
  // P1 Thread (task-299 Phase 1)
140
141
  spawnThread,
141
142
  switchThread,
142
143
  listThreads,
143
- attachThreadToTask,
144
- spawnTask,
144
+ attachThreadToFeature,
145
145
  readThreadSummary,
146
146
  readThreadRecent,
147
147
 
@@ -2,14 +2,13 @@
2
2
  * thread-tools.js — Thread-spawning tools for Unify Engine (Phase 1).
3
3
  *
4
4
  * Phase 1 scope (task-299 rework):
5
- * - SpawnThread — create a new thread
6
- * - SwitchThread — set the engine's currentThreadId marker
7
- * - ListThreads — list threads + current marker + cached stats
8
- * - AttachThreadToTask — bind a thread to an existing task
9
- * - SpawnTask create a task or subtask (parent_task_id optional)
10
- * - ReadThreadSummary — cross-reference: summary of a thread (id/name/
11
- * status/messageCount/lastMessageAt/task)
12
- * - ReadThreadRecent — cross-reference: last N messages of a thread
5
+ * - SpawnThread — create a new thread
6
+ * - SwitchThread — set the engine's currentThreadId marker
7
+ * - ListThreads — list threads + current marker + cached stats
8
+ * - AttachThreadToFeature — bind a thread to an existing feature
9
+ * - ReadThreadSummary cross-reference: summary of a thread (id/name/
10
+ * status/messageCount/lastMessageAt/feature)
11
+ * - ReadThreadRecent — cross-reference: last N messages of a thread
13
12
  *
14
13
  * Phase 1 uses the in-memory ThreadStore (agent/unify/threads/store.js)
15
14
  * and the existing ConversationStore for messages. When task-298 merges,
@@ -18,9 +17,8 @@
18
17
  */
19
18
 
20
19
  import { defineTool } from './types.js';
21
- import { randomUUID } from 'crypto';
22
20
  import { getThreadStore, MAIN_THREAD_ID } from '../threads/store.js';
23
- import { getTaskStore } from './task-tools.js';
21
+ import { getFeatureStore } from './feature-tools.js';
24
22
 
25
23
  // ─── SpawnThread ─────────────────────────────────────────
26
24
 
@@ -134,7 +132,7 @@ fields — does not scan messages.`,
134
132
  archived: t.archived,
135
133
  unread: t.unread || 0,
136
134
  preview: t.preview || '',
137
- attachedTaskId: store.attachedTask(t.id),
135
+ attachedFeatureId: store.attachedFeature(t.id),
138
136
  }));
139
137
  return JSON.stringify(
140
138
  {
@@ -148,44 +146,44 @@ fields — does not scan messages.`,
148
146
  },
149
147
  });
150
148
 
151
- // ─── AttachThreadToTask ──────────────────────────────────
149
+ // ─── AttachThreadToFeature ──────────────────────────────────
152
150
 
153
- export const attachThreadToTask = defineTool({
154
- name: 'AttachThreadToTask',
155
- description: `Link an existing thread to an existing task.
151
+ export const attachThreadToFeature = defineTool({
152
+ name: 'AttachThreadToFeature',
153
+ description: `Link an existing thread to an existing feature.
156
154
 
157
- Use to record which thread is responsible for which task. The thread and
158
- the task must both already exist. Overwrites any previous attachment for
155
+ Use to record which thread is responsible for which feature. The thread and
156
+ the feature must both already exist. Overwrites any previous attachment for
159
157
  the same thread.`,
160
158
  parameters: {
161
159
  type: 'object',
162
160
  properties: {
163
161
  thread_id: { type: 'string' },
164
- task_id: { type: 'string' },
162
+ feature_id: { type: 'string' },
165
163
  },
166
- required: ['thread_id', 'task_id'],
164
+ required: ['thread_id', 'feature_id'],
167
165
  },
168
166
  isConcurrencySafe: () => false,
169
167
  isReadOnly: () => false,
170
168
  async execute(input) {
171
- const { thread_id, task_id } = input || {};
169
+ const { thread_id, feature_id } = input || {};
172
170
  if (!thread_id) return JSON.stringify({ error: 'thread_id is required' });
173
- if (!task_id) return JSON.stringify({ error: 'task_id is required' });
171
+ if (!feature_id) return JSON.stringify({ error: 'feature_id is required' });
174
172
 
175
- const taskStore = getTaskStore();
176
- if (taskStore) {
177
- const task = taskStore.get(task_id);
178
- if (!task) return JSON.stringify({ error: `Task not found: ${task_id}` });
173
+ const featureStore = getFeatureStore();
174
+ if (featureStore) {
175
+ const feature = featureStore.get(feature_id);
176
+ if (!feature) return JSON.stringify({ error: `Feature not found: ${feature_id}` });
179
177
  }
180
178
 
181
179
  try {
182
180
  const store = getThreadStore();
183
- store.attachTask(thread_id, task_id);
181
+ store.attachFeature(thread_id, feature_id);
184
182
  return JSON.stringify({
185
183
  success: true,
186
184
  threadId: thread_id,
187
- taskId: task_id,
188
- message: `Thread ${thread_id} attached to task ${task_id}`,
185
+ featureId: feature_id,
186
+ message: `Thread ${thread_id} attached to feature ${feature_id}`,
189
187
  });
190
188
  } catch (err) {
191
189
  return JSON.stringify({ error: err.message || String(err) });
@@ -193,91 +191,14 @@ the same thread.`,
193
191
  },
194
192
  });
195
193
 
196
- // ─── SpawnTask (deprecated alias — task-333b) ──────────────
197
- //
198
- // task-333b folded SpawnTask into TaskCreate (see task-tools.js). Kept
199
- // registered for one release as a deprecated alias per PM constraint:
200
- // LLM calls still resolve, but a one-time console.warn nudges migration.
201
- // Prefer TaskCreate with `parent_task_id` for all new call sites.
202
-
203
- const _spawnTaskWarned = { v: false };
204
- function warnSpawnTaskDeprecated() {
205
- if (_spawnTaskWarned.v) return;
206
- _spawnTaskWarned.v = true;
207
- // eslint-disable-next-line no-console
208
- console.warn('[deprecated] SpawnTask → TaskCreate. Pass parent_task_id to TaskCreate for subtasks.');
209
- }
210
-
211
- export const spawnTask = defineTool({
212
- name: 'SpawnTask',
213
- description: `DEPRECATED — use TaskCreate with parent_id instead. Retained as a thin alias for backwards compatibility; delegates to the same task store. When parent_task_id is omitted this behaves like TaskCreate; when provided it creates a subtask under that parent (parent must exist). Removal target: v0.2.0.`,
214
- parameters: {
215
- type: 'object',
216
- properties: {
217
- title: { type: 'string' },
218
- description: { type: 'string' },
219
- priority: {
220
- type: 'string',
221
- enum: ['low', 'medium', 'high', 'critical'],
222
- },
223
- parent_task_id: {
224
- type: 'string',
225
- description: 'Optional parent task id; when present, a subtask is created',
226
- },
227
- },
228
- required: ['title'],
229
- },
230
- isConcurrencySafe: () => false,
231
- isReadOnly: () => false,
232
- async execute(input) {
233
- warnSpawnTaskDeprecated();
234
- const store = getTaskStore();
235
- if (!store) {
236
- return JSON.stringify({ error: 'Task store not initialized. Session may still be loading.' });
237
- }
238
- const { title, description = '', priority = 'medium', parent_task_id } = input || {};
239
- if (!title) return JSON.stringify({ error: 'title is required' });
240
-
241
- if (parent_task_id) {
242
- const parent = store.get(parent_task_id);
243
- if (!parent) return JSON.stringify({ error: `Parent task not found: ${parent_task_id}` });
244
- }
245
-
246
- const id = `task-${randomUUID().slice(0, 8)}`;
247
- const now = Date.now();
248
- store.create({
249
- id,
250
- title,
251
- description,
252
- priority,
253
- status: 'pending',
254
- parentId: parent_task_id || null,
255
- parentTaskId: parent_task_id || null, // design §5 canonical field; kept in sync with parentId
256
- createdAt: now,
257
- updatedAt: now,
258
- });
259
- return JSON.stringify({
260
- success: true,
261
- task: {
262
- id,
263
- title,
264
- priority,
265
- status: 'pending',
266
- parentTaskId: parent_task_id || null,
267
- },
268
- message: parent_task_id
269
- ? `Subtask spawned: ${title} (${id}) under ${parent_task_id}`
270
- : `Task spawned: ${title} (${id})`,
271
- });
272
- },
273
- });
194
+ // ─── SpawnTask removed (task-333b) — use FeatureCreate with parent_feature_id
274
195
 
275
196
  // ─── ReadThreadSummary (cross-reference, design §6 Q5) ──
276
197
 
277
198
  export const readThreadSummary = defineTool({
278
199
  name: 'ReadThreadSummary',
279
200
  description: `Return a one-shot summary of a thread: id, name, goal, status,
280
- messageCount, lastMessageAt, parentThreadId, attachedTaskId.
201
+ messageCount, lastMessageAt, parentThreadId, attachedFeatureId.
281
202
 
282
203
  Use this to cross-reference work on another thread without switching.`,
283
204
  parameters: {
@@ -305,7 +226,7 @@ Use this to cross-reference work on another thread without switching.`,
305
226
  messageCount: t.messageCount,
306
227
  lastMessageAt: t.lastMessageAt,
307
228
  parentThreadId: t.parentThreadId,
308
- attachedTaskId: store.attachedTask(t.id),
229
+ attachedFeatureId: store.attachedFeature(t.id),
309
230
  createdAt: t.createdAt,
310
231
  updatedAt: t.updatedAt,
311
232
  },
@@ -30,7 +30,7 @@ import { handleVpSubscribe } from './vp/vp-bridge.js';
30
30
  import { createVp, updateVp, deleteVp, readVp, VpCrudError } from './vp/vp-crud.js';
31
31
  import { scanVpLibrary } from './vp/vp-store.js';
32
32
  import { createRouter } from './routing/router.js';
33
- import { handleUnifyTaskMessage as _handleUnifyTaskMessage } from './task-message.js';
33
+ import { handleUnifyFeatureMessage as _handleUnifyFeatureMessage } from './feature-message.js';
34
34
  import {
35
35
  handleUnifyUserMemoryWrite as _handleUnifyUserMemoryWrite,
36
36
  handleUnifyUserMemoryRemove as _handleUnifyUserMemoryRemove,
@@ -278,8 +278,8 @@ export function handleUnifyVpDelete(msg) {
278
278
  *
279
279
  * @param {any} msg
280
280
  */
281
- export function handleUnifyTaskMessage(msg) {
282
- _handleUnifyTaskMessage(msg, sendUnifyEvent);
281
+ export function handleUnifyFeatureMessage(msg) {
282
+ _handleUnifyFeatureMessage(msg, sendUnifyEvent);
283
283
  }
284
284
 
285
285
  /**
@@ -639,11 +639,11 @@ function sendThreadListUpdate() {
639
639
  preview: t.preview || '',
640
640
  createdAt: t.createdAt,
641
641
  updatedAt: t.updatedAt,
642
- // task-315: attached taskId (if any) so the UI can aggregate all
643
- // messages belonging to a task across multiple threads. null when
644
- // the thread has no attached task.
645
- taskId: (typeof store.attachedTask === 'function')
646
- ? (store.attachedTask(t.id) || null)
642
+ // task-315: attached featureId (if any) so the UI can aggregate all
643
+ // messages belonging to a feature across multiple threads. null when
644
+ // the thread has no attached feature.
645
+ featureId: (typeof store.attachedFeature === 'function')
646
+ ? (store.attachedFeature(t.id) || null)
647
647
  : null,
648
648
  // `running` — the thread whose id equals the store's currentId is
649
649
  // considered the active/running track. The UI uses this for the
@@ -662,7 +662,7 @@ const THREAD_MUTATING_TOOLS = new Set([
662
662
  'SpawnThread',
663
663
  'SwitchThread',
664
664
  'ArchiveThread',
665
- 'AttachThreadToTask',
665
+ 'AttachThreadToFeature',
666
666
  ]);
667
667
 
668
668
  /**
@@ -751,8 +751,8 @@ function sendThreadListSnapshot() {
751
751
  preview: t.preview || '',
752
752
  createdAt: t.createdAt,
753
753
  updatedAt: t.updatedAt,
754
- taskId: (typeof store.attachedTask === 'function')
755
- ? (store.attachedTask(t.id) || null)
754
+ featureId: (typeof store.attachedFeature === 'function')
755
+ ? (store.attachedFeature(t.id) || null)
756
756
  : null,
757
757
  running: t.id === store.currentId,
758
758
  state: inflight.has(t.id) ? 'running' : 'idle',
@@ -1809,37 +1809,37 @@ export function handleUnifyModeSwitch(_msg) {
1809
1809
  }
1810
1810
 
1811
1811
  /**
1812
- * R6 G2 — VP/Task memory browser query.
1812
+ * R6 G2 — VP/Feature memory browser query.
1813
1813
  *
1814
1814
  * Reads from session.memoryShardStore (R6 shard-based memory) and replies
1815
- * with a time-sorted list of entries scoped to the requested vpId / taskId.
1815
+ * with a time-sorted list of entries scoped to the requested vpId / featureId.
1816
1816
  * The web UI's MemoryCard / MemoryTraceModal consume the reply.
1817
1817
  *
1818
1818
  * Request shape:
1819
1819
  * { type: 'unify_memory_query',
1820
- * vpId?: string, taskId?: string,
1820
+ * vpId?: string, featureId?: string,
1821
1821
  * limit?: number, requestId?: string }
1822
1822
  *
1823
1823
  * Reply shape:
1824
1824
  * { type: 'unify_memory_query_result',
1825
- * scope: { vpId, taskId },
1825
+ * scope: { vpId, featureId },
1826
1826
  * entries: Array<thinEntry>, // shape from shard-store mapRecordToThinEntry
1827
1827
  * requestId? }
1828
1828
  *
1829
1829
  * Per D2 the query is scoped — the LLM owns memory recall via the
1830
1830
  * memory_query tool; this surface is purely UI browsing (read-only).
1831
1831
  *
1832
- * @param {{ vpId?: string, taskId?: string, limit?: number, requestId?: string }} msg
1832
+ * @param {{ vpId?: string, featureId?: string, limit?: number, requestId?: string }} msg
1833
1833
  */
1834
1834
  export function handleUnifyMemoryQuery(msg = {}) {
1835
1835
  const requestId = typeof msg.requestId === 'string' ? msg.requestId : undefined;
1836
1836
  const vpId = typeof msg.vpId === 'string' ? msg.vpId : null;
1837
- const taskId = typeof msg.taskId === 'string' ? msg.taskId : null;
1837
+ const featureId = typeof msg.featureId === 'string' ? msg.featureId : null;
1838
1838
  const limit = Number.isFinite(msg.limit) ? Math.max(1, Math.min(200, msg.limit)) : 50;
1839
1839
 
1840
1840
  const reply = (extra = {}) => sendUnifyEvent({
1841
1841
  type: 'unify_memory_query_result',
1842
- scope: { vpId, taskId },
1842
+ scope: { vpId, featureId },
1843
1843
  ...extra,
1844
1844
  ...(requestId ? { requestId } : {}),
1845
1845
  });
@@ -1852,7 +1852,7 @@ export function handleUnifyMemoryQuery(msg = {}) {
1852
1852
  try {
1853
1853
  const filter = {};
1854
1854
  if (vpId) filter.vp = vpId;
1855
- if (taskId) filter.task = taskId;
1855
+ if (featureId) filter.feature = featureId;
1856
1856
  const res = session.memoryShardStore.query(filter);
1857
1857
  const list = Array.isArray(res?.results) ? res.results : [];
1858
1858
  // Time-sorted desc on updatedAt / createdAt.
@@ -1912,41 +1912,41 @@ export function handleUnifyMemoryTrace(msg = {}) {
1912
1912
  }
1913
1913
 
1914
1914
  /**
1915
- * R6 G1a — Fetch a task's summary history (revision chain).
1915
+ * R6 G1a — Fetch a feature's summary history (revision chain).
1916
1916
  *
1917
- * Streams the group log filtered by `taskId` and `meta.kind === 'summary'`,
1917
+ * Streams the group log filtered by `featureId` and `meta.kind === 'summary'`,
1918
1918
  * separates `current` (≤10 most recent non-superseded) from `archived` rows
1919
1919
  * per §Δ31.5. Default `includeArchived: false` keeps the wire payload small;
1920
1920
  * the UI's "Show archived" button re-issues with the flag set.
1921
1921
  *
1922
1922
  * Request shape:
1923
- * { type: 'unify_fetch_summary_history', taskId, includeArchived?: bool }
1923
+ * { type: 'unify_fetch_summary_history', featureId, includeArchived?: bool }
1924
1924
  *
1925
1925
  * Reply shape:
1926
- * { type: 'unify_summary_history', taskId, revisions: [...],
1926
+ * { type: 'unify_summary_history', featureId, revisions: [...],
1927
1927
  * archived: [...]|null, error?: string, requestId? }
1928
1928
  */
1929
1929
  export async function handleUnifyFetchSummaryHistory(msg = {}) {
1930
1930
  const requestId = typeof msg.requestId === 'string' ? msg.requestId : undefined;
1931
- const taskId = typeof msg.taskId === 'string' ? msg.taskId : null;
1931
+ const featureId = typeof msg.featureId === 'string' ? msg.featureId : null;
1932
1932
  const includeArchived = !!msg.includeArchived;
1933
1933
 
1934
1934
  const reply = (extra = {}) => sendUnifyEvent({
1935
1935
  type: 'unify_summary_history',
1936
- taskId,
1936
+ featureId,
1937
1937
  ...extra,
1938
1938
  ...(requestId ? { requestId } : {}),
1939
1939
  });
1940
1940
 
1941
- if (!taskId) { reply({ revisions: [], archived: null, error: 'missing_task_id' }); return; }
1941
+ if (!featureId) { reply({ revisions: [], archived: null, error: 'missing_feature_id' }); return; }
1942
1942
 
1943
1943
  try {
1944
- const { getTaskStore } = await import('./tools/task-tools.js');
1945
- const taskStore = getTaskStore();
1946
- const task = taskStore?.get(taskId);
1947
- if (!task) { reply({ revisions: [], archived: null, error: 'task_not_found' }); return; }
1948
- const groupId = task.groupId;
1949
- if (!groupId) { reply({ revisions: [], archived: null, error: 'task_has_no_group' }); return; }
1944
+ const { getFeatureStore } = await import('./tools/feature-tools.js');
1945
+ const featureStore = getFeatureStore();
1946
+ const feature = featureStore?.get(featureId);
1947
+ if (!feature) { reply({ revisions: [], archived: null, error: 'feature_not_found' }); return; }
1948
+ const groupId = feature.groupId;
1949
+ if (!groupId) { reply({ revisions: [], archived: null, error: 'feature_has_no_group' }); return; }
1950
1950
 
1951
1951
  const yeaftDir = ctx.CONFIG?.yeaftDir;
1952
1952
  if (!yeaftDir) { reply({ revisions: [], archived: null, error: 'no_yeaft_dir' }); return; }
@@ -1963,7 +1963,7 @@ export async function handleUnifyFetchSummaryHistory(msg = {}) {
1963
1963
  const groupHandle = openGroup(root, groupId);
1964
1964
  const summaries = [];
1965
1965
  for (const m of groupHandle.streamMessages()) {
1966
- if (!m || m.taskId !== taskId) continue;
1966
+ if (!m || m.featureId !== featureId) continue;
1967
1967
  const meta = m.meta || {};
1968
1968
  if (meta.kind === 'summary' || meta.type === 'summary') summaries.push(m);
1969
1969
  }
@@ -2002,74 +2002,74 @@ export async function handleUnifyFetchSummaryHistory(msg = {}) {
2002
2002
  }
2003
2003
 
2004
2004
  /**
2005
- * R6 G1a — Task affiliation CRUD (relate / unrelate / kick_vp / abort_vp).
2005
+ * R6 G1a — Feature affiliation CRUD (relate / unrelate / kick_vp / abort_vp).
2006
2006
  *
2007
2007
  * Single envelope so the UI doesn't fan out four separate WS message types
2008
- * for housekeeping verbs. Replies with `unify_task_crud_result`.
2008
+ * for housekeeping verbs. Replies with `unify_feature_crud_result`.
2009
2009
  *
2010
- * - relate { taskId, relatedTaskId } — bidirectional Δ27 link
2011
- * - unrelate { taskId, relatedTaskId } — drop both directions
2012
- * - kick_vp { taskId, vpId } — taskStore.removeMember
2013
- * - abort_vp { taskId, vpId } — abort that VP's in-flight engine inside the task
2010
+ * - relate { featureId, relatedFeatureId } — bidirectional Δ27 link
2011
+ * - unrelate { featureId, relatedFeatureId } — drop both directions
2012
+ * - kick_vp { featureId, vpId } — featureStore.removeMember
2013
+ * - abort_vp { featureId, vpId } — abort that VP's in-flight engine inside the feature
2014
2014
  */
2015
- export async function handleUnifyTaskCrud(msg = {}) {
2015
+ export async function handleUnifyFeatureCrud(msg = {}) {
2016
2016
  const requestId = typeof msg.requestId === 'string' ? msg.requestId : undefined;
2017
2017
  const op = typeof msg.op === 'string' ? msg.op : null;
2018
- const taskId = typeof msg.taskId === 'string' ? msg.taskId : null;
2018
+ const featureId = typeof msg.featureId === 'string' ? msg.featureId : null;
2019
2019
  const vpId = typeof msg.vpId === 'string' ? msg.vpId : null;
2020
- const relatedTaskId = typeof msg.relatedTaskId === 'string' ? msg.relatedTaskId : null;
2020
+ const relatedFeatureId = typeof msg.relatedFeatureId === 'string' ? msg.relatedFeatureId : null;
2021
2021
 
2022
2022
  const reply = (extra = {}) => sendUnifyEvent({
2023
- type: 'unify_task_crud_result',
2023
+ type: 'unify_feature_crud_result',
2024
2024
  op,
2025
- taskId,
2025
+ featureId,
2026
2026
  ...(vpId ? { vpId } : {}),
2027
2027
  ...extra,
2028
2028
  ...(requestId ? { requestId } : {}),
2029
2029
  });
2030
2030
 
2031
2031
  if (!op) { reply({ ok: false, error: 'missing_op' }); return; }
2032
- if (!taskId) { reply({ ok: false, error: 'missing_task_id' }); return; }
2032
+ if (!featureId) { reply({ ok: false, error: 'missing_feature_id' }); return; }
2033
2033
 
2034
2034
  try {
2035
- const { getTaskStore } = await import('./tools/task-tools.js');
2036
- const taskStore = getTaskStore();
2037
- const task = taskStore?.get(taskId);
2038
- if (!task) { reply({ ok: false, error: 'task_not_found' }); return; }
2035
+ const { getFeatureStore } = await import('./tools/feature-tools.js');
2036
+ const featureStore = getFeatureStore();
2037
+ const feature = featureStore?.get(featureId);
2038
+ if (!feature) { reply({ ok: false, error: 'feature_not_found' }); return; }
2039
2039
 
2040
2040
  if (op === 'relate' || op === 'unrelate') {
2041
- if (!relatedTaskId) { reply({ ok: false, error: 'missing_related_task_id' }); return; }
2042
- const other = taskStore.get(relatedTaskId);
2043
- if (!other) { reply({ ok: false, error: 'related_task_not_found' }); return; }
2041
+ if (!relatedFeatureId) { reply({ ok: false, error: 'missing_related_feature_id' }); return; }
2042
+ const other = featureStore.get(relatedFeatureId);
2043
+ if (!other) { reply({ ok: false, error: 'related_feature_not_found' }); return; }
2044
2044
  const apply = (t, otherId, add) => {
2045
- const cur = Array.isArray(t.relatedTaskIds) ? t.relatedTaskIds.slice() : [];
2045
+ const cur = Array.isArray(t.relatedFeatureIds) ? t.relatedFeatureIds.slice() : [];
2046
2046
  const idx = cur.indexOf(otherId);
2047
2047
  if (add && idx === -1) cur.push(otherId);
2048
2048
  if (!add && idx !== -1) cur.splice(idx, 1);
2049
- taskStore.update(t.id, { relatedTaskIds: cur });
2049
+ featureStore.update(t.id, { relatedFeatureIds: cur });
2050
2050
  };
2051
- apply(task, relatedTaskId, op === 'relate');
2052
- apply(other, taskId, op === 'relate');
2053
- reply({ ok: true, relatedTaskId });
2051
+ apply(feature, relatedFeatureId, op === 'relate');
2052
+ apply(other, featureId, op === 'relate');
2053
+ reply({ ok: true, relatedFeatureId });
2054
2054
  return;
2055
2055
  }
2056
2056
 
2057
2057
  if (op === 'kick_vp') {
2058
2058
  if (!vpId) { reply({ ok: false, error: 'missing_vp_id' }); return; }
2059
- taskStore.removeMember(taskId, vpId);
2059
+ featureStore.removeMember(featureId, vpId);
2060
2060
  reply({ ok: true });
2061
2061
  return;
2062
2062
  }
2063
2063
 
2064
2064
  if (op === 'abort_vp') {
2065
2065
  if (!vpId) { reply({ ok: false, error: 'missing_vp_id' }); return; }
2066
- // Reuse per-thread abort registry — keyed by (taskId,vpId) tuple if
2066
+ // Reuse per-thread abort registry — keyed by (featureId,vpId) tuple if
2067
2067
  // the engine instance is registered there. For v1 we surface success
2068
2068
  // and let the engine settle; full per-VP cancellation is owned by
2069
2069
  // the engine registry in 334o follow-up.
2070
2070
  const reg = session?.engineRegistry;
2071
- if (reg && typeof reg.abortVpInTask === 'function') {
2072
- reg.abortVpInTask(taskId, vpId);
2071
+ if (reg && typeof reg.abortVpInFeature === 'function') {
2072
+ reg.abortVpInFeature(featureId, vpId);
2073
2073
  }
2074
2074
  reply({ ok: true });
2075
2075
  return;