@yeaft/webchat-agent 0.1.616 → 0.1.618

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,22 +1,22 @@
1
1
  /**
2
- * summary.js — task-334n: Task multi-VP collaboration summary protocol.
2
+ * summary.js — task-334n: Feature multi-VP collaboration summary protocol.
3
3
  *
4
4
  * Owns:
5
- * - postSummary() — write a `type=summary` message to the group jsonl
6
- * and run the extractor (B + C)
7
- * - extractTaskMemory() — turn a summary body into 2-5 task-memory entries
8
- * via 334f task-memory shard lib (C)
5
+ * - postSummary() — write a `type=summary` message to the group jsonl
6
+ * and run the extractor (B + C)
7
+ * - extractFeatureMemory() — turn a summary body into 2-5 feature-memory entries
8
+ * via 334f feature-memory shard lib (C)
9
9
  * - buildSummaryReminder() — compute the §Δ31.4 3-AND soft reminder shape
10
- * consumed by 334e's `taskCtx.summaryReminder` (D)
11
- * - buildTaskCtxMemories() — assemble task-memory top-5 (pinned + recent +
12
- * tag relevance) for task_ctx (E)
10
+ * consumed by 334e's `featureCtx.summaryReminder` (D)
11
+ * - buildFeatureCtxMemories() — assemble feature-memory top-5 (pinned + recent +
12
+ * tag relevance) for feature_ctx (E)
13
13
  *
14
14
  * Hard boundaries:
15
15
  * - does NOT touch 334o jsonl rotation internals (calls group.appendMessage)
16
16
  * - does NOT touch 334f shard-store impl (calls openMemoryShardStore API)
17
17
  * - does NOT touch 334e prompts main frame (returns plain shapes that feed
18
- * the existing renderTaskCtx contract)
19
- * - does NOT self-loop-write VP-memory (extractor writes task-memory only;
18
+ * the existing renderFeatureCtx contract)
19
+ * - does NOT self-loop-write VP-memory (extractor writes feature-memory only;
20
20
  * VP-level synthesis is deferred to 334g dream)
21
21
  * - softCap overflow does NOT create new shards (334f already routes into
22
22
  * dream queue via projectDeriveHint; we just surface `needsRecompression`)
@@ -44,7 +44,7 @@ export const PROGRESS_ANCHORS = Object.freeze([
44
44
  /** Whitelist of R6 kinds emitted by the summary-extractor. */
45
45
  const EXTRACT_KINDS = Object.freeze(['progress', 'decision']);
46
46
 
47
- /** Shard routing for each extracted kind (§Δ25.2 task-memory fixed set). */
47
+ /** Shard routing for each extracted kind (§Δ25.2 feature-memory fixed set). */
48
48
  const KIND_TO_SHARD = Object.freeze({
49
49
  progress: 'progress',
50
50
  decision: 'decision',
@@ -54,26 +54,25 @@ const KIND_TO_SHARD = Object.freeze({
54
54
 
55
55
  /**
56
56
  * Write a `type=summary` message to the group log, then auto-run the
57
- * extractor to derive task-memory entries.
57
+ * extractor to derive feature-memory entries.
58
58
  *
59
59
  * @param {{
60
60
  * group: import('../groups/group-store.js').GroupHandle,
61
- * taskId: string,
61
+ * featureId: string,
62
62
  * fromVpId: string,
63
63
  * body: string,
64
- * progress?: number|string, // 0..100 or PROGRESS_ANCHORS string
65
- * supersedes?: string[], // prior summary msgIds being superseded
66
- * memoryDir: string, // groups/<g>/tasks/<t>/memory/
67
- * now?: () => number, // test clock
64
+ * progress?: number|string,
65
+ * supersedes?: string[],
66
+ * memoryDir: string, // groups/<g>/features/<f>/memory/
67
+ * now?: () => number,
68
68
  * extractor?: (body:string) => Array<{kind:string,body:string,tags?:string[]}>
69
- * // optional hook; default uses `defaultExtractor` (heuristic, no LLM)
70
69
  * }} opts
71
70
  * @returns {{ message: any, memoryIds: string[], supersededSummaryIds: string[] }}
72
71
  */
73
72
  export function postSummary(opts) {
74
73
  const {
75
74
  group,
76
- taskId,
75
+ featureId,
77
76
  fromVpId,
78
77
  body,
79
78
  progress,
@@ -86,7 +85,7 @@ export function postSummary(opts) {
86
85
  if (!group || typeof group.appendMessage !== 'function') {
87
86
  throw new Error('postSummary: group handle required');
88
87
  }
89
- if (!taskId) throw new Error('postSummary: taskId required');
88
+ if (!featureId) throw new Error('postSummary: featureId required');
90
89
  if (!fromVpId) throw new Error('postSummary: fromVpId required');
91
90
  if (typeof body !== 'string' || !body.trim()) {
92
91
  throw new Error('postSummary: body required (non-empty string)');
@@ -112,7 +111,7 @@ export function postSummary(opts) {
112
111
  from: fromVpId,
113
112
  role: 'assistant',
114
113
  text: body,
115
- taskId,
114
+ featureId,
116
115
  meta: {
117
116
  type: 'summary',
118
117
  progress: progress == null ? null : (typeof progress === 'string' ? progress : Number(progress)),
@@ -120,10 +119,10 @@ export function postSummary(opts) {
120
119
  },
121
120
  });
122
121
 
123
- // 2) Run the extractor → write task-memory entries (C).
122
+ // 2) Run the extractor → write feature-memory entries (C).
124
123
  const memoryIds = [];
125
124
  try {
126
- const store = openMemoryShardStore(memoryDir, 'task');
125
+ const store = openMemoryShardStore(memoryDir, 'feature');
127
126
  const raw = extractor(body) || [];
128
127
  const bounded = clampExtracted(raw);
129
128
  for (const [i, item] of bounded.entries()) {
@@ -134,11 +133,11 @@ export function postSummary(opts) {
134
133
  id,
135
134
  shard,
136
135
  kind,
137
- taskId,
136
+ featureId,
138
137
  body: typeof item.body === 'string' ? item.body.trim() : '',
139
138
  tags: Array.isArray(item.tags) ? item.tags.slice(0, 5) : [],
140
139
  authoredBy: AUTHORED_BY.SUMMARY,
141
- sourceRef: { taskId, msgIds: [stored.id] },
140
+ sourceRef: { featureId, msgIds: [stored.id] },
142
141
  createdAt: new Date(now()).toISOString(),
143
142
  });
144
143
  memoryIds.push(id);
@@ -167,17 +166,6 @@ function clampExtracted(arr) {
167
166
 
168
167
  // ─── (C) default extractor ───────────────────────────────────────
169
168
 
170
- /**
171
- * Heuristic extractor — no LLM, deterministic, safe for tests.
172
- *
173
- * Strategy:
174
- * - Split body into non-empty lines (trim bullets).
175
- * - Lines starting with keywords "decide/decision/chose/chosen" → kind=decision.
176
- * - Lines starting with "progress/ship/shipped/done/completed/blocker/todo"
177
- * → kind=progress.
178
- * - Everything else → kind=progress (default).
179
- * - Emit up to EXTRACT_MAX_ENTRIES.
180
- */
181
169
  export function defaultExtractor(body) {
182
170
  if (typeof body !== 'string') return [];
183
171
  const lines = body
@@ -194,12 +182,7 @@ export function defaultExtractor(body) {
194
182
  out.push({ kind, body: line });
195
183
  if (out.length >= EXTRACT_MAX_ENTRIES) break;
196
184
  }
197
- // If we ended up with fewer than MIN and there was a body, collapse to
198
- // one "progress" entry carrying the trimmed full body so we never emit 0
199
- // when the caller gave us real content and asked for 2-5.
200
185
  if (out.length < EXTRACT_MIN_ENTRIES && body.trim()) {
201
- // Pad up to EXTRACT_MIN_ENTRIES with the full trimmed body when
202
- // the line-by-line pass yielded fewer entries than the minimum.
203
186
  while (out.length < EXTRACT_MIN_ENTRIES) {
204
187
  out.push({ kind: 'progress', body: body.trim() });
205
188
  }
@@ -210,39 +193,34 @@ export function defaultExtractor(body) {
210
193
  // ─── (D) soft reminder builder ───────────────────────────────────
211
194
 
212
195
  /**
213
- * Build the `taskCtx.summaryReminder` shape consumed by 334e's prompt.
214
- * Returns null when the 3-AND conditions do not all hold. The prompt layer
215
- * adds a 4th check (currentVpId === initiatorVpId) so we gate here too so
216
- * callers can debug-log why it was suppressed.
196
+ * Build the `featureCtx.summaryReminder` shape consumed by 334e's prompt.
217
197
  *
218
198
  * §Δ31.4 conditions:
219
- * (1) task.members.length > 1
220
- * (2) caller role === 'initiator' (i.e. currentVpId === task.initiator)
199
+ * (1) feature.members.length > 1
200
+ * (2) caller role === 'initiator' (i.e. currentVpId === feature.initiator)
221
201
  * (3) (now - lastSummaryAt) ≥ 20 min OR nonSummaryTurns ≥ 10
222
202
  *
223
203
  * @param {{
224
- * task: { initiator?: string, members?: string[] },
204
+ * feature: { initiator?: string, members?: string[] },
225
205
  * currentVpId: string,
226
- * lastSummaryAt: number, // epoch ms, 0 = never
206
+ * lastSummaryAt: number,
227
207
  * nonSummaryTurns: number,
228
208
  * now?: number,
229
209
  * }} input
230
- * @returns {{ triggered: boolean, nonSummaryCount: number, lastSummaryAt: number,
231
- * now: number, reasons: string[] }}
232
210
  */
233
211
  export function buildSummaryReminder(input) {
234
- const { task, currentVpId, lastSummaryAt = 0, nonSummaryTurns = 0 } = input || {};
212
+ const { feature, currentVpId, lastSummaryAt = 0, nonSummaryTurns = 0 } = input || {};
235
213
  const now = typeof input?.now === 'number' ? input.now : Date.now();
236
214
  const reasons = [];
237
215
 
238
- if (!task || typeof task !== 'object') {
239
- return { triggered: false, reasons: ['no-task'], nonSummaryCount: nonSummaryTurns, lastSummaryAt, now };
216
+ if (!feature || typeof feature !== 'object') {
217
+ return { triggered: false, reasons: ['no-feature'], nonSummaryCount: nonSummaryTurns, lastSummaryAt, now };
240
218
  }
241
- const members = Array.isArray(task.members) ? task.members : [];
242
- const isInitiator = !!currentVpId && task.initiator === currentVpId;
219
+ const members = Array.isArray(feature.members) ? feature.members : [];
220
+ const isInitiator = !!currentVpId && feature.initiator === currentVpId;
243
221
 
244
222
  if (!isInitiator) reasons.push('not-initiator');
245
- if (members.length <= SUMMARY_REMINDER_MIN_MEMBERS - 1) reasons.push('solo-task');
223
+ if (members.length <= SUMMARY_REMINDER_MIN_MEMBERS - 1) reasons.push('solo-feature');
246
224
 
247
225
  const ageMs = lastSummaryAt > 0 ? now - lastSummaryAt : Number.POSITIVE_INFINITY;
248
226
  const ageOk = ageMs >= SUMMARY_REMINDER_MIN_AGE_MS;
@@ -259,29 +237,25 @@ export function buildSummaryReminder(input) {
259
237
  };
260
238
  }
261
239
 
262
- // ─── (E) task_ctx top-5 task-memory builder ──────────────────────
240
+ // ─── (E) feature_ctx top-5 feature-memory builder ────────────────
263
241
 
264
242
  /**
265
- * Assemble task-memory top-5 for 334e's `taskCtx.memories` field.
243
+ * Assemble feature-memory top-5 for 334e's `featureCtx.memories` field.
266
244
  * Ordering (§Δ16.5): pinned first → recent → tag-relevant. Supersedes are
267
245
  * hidden (entries with supersededBy != null are filtered out).
268
246
  *
269
- * @param {string} memoryDir groups/<g>/tasks/<t>/memory/
247
+ * @param {string} memoryDir groups/<g>/features/<f>/memory/
270
248
  * @param {{ tags?: string[], top?: number }} [opts]
271
- * tags : optional tag hints to boost relevance
272
- * top : default 5
273
249
  * @returns {Array<{body:string, shard:string, authoredBy?:string}>}
274
250
  */
275
- export function buildTaskCtxMemories(memoryDir, opts = {}) {
251
+ export function buildFeatureCtxMemories(memoryDir, opts = {}) {
276
252
  const top = Number.isFinite(opts.top) ? Number(opts.top) : 5;
277
253
  const tagHints = Array.isArray(opts.tags) ? opts.tags : [];
278
254
  const nowMs = typeof opts.now === 'number' ? opts.now : Date.now();
279
255
  let results = [];
280
256
  try {
281
- const store = openMemoryShardStore(memoryDir, 'task');
257
+ const store = openMemoryShardStore(memoryDir, 'feature');
282
258
  const q = store.query({});
283
- // query() returns thin entries (id/shard/kind/tags/pinned/groupId/taskId/supersededBy);
284
- // we need the body too.
285
259
  const hits = (q.results || [])
286
260
  .filter((r) => !r.supersededBy)
287
261
  .map((r) => {
@@ -301,21 +275,17 @@ export function buildTaskCtxMemories(memoryDir, opts = {}) {
301
275
  const score = (r) => {
302
276
  let s = 0;
303
277
  if (r.pinned) s += 1000;
304
- // Recency decay: half-life of 24h. Recent entries score up to +100,
305
- // decaying toward 0 as they age.
306
278
  if (r.createdAt) {
307
279
  const ageMs = nowMs - new Date(r.createdAt).getTime();
308
- const halfLifeMs = 24 * 60 * 60 * 1000; // 24 hours
280
+ const halfLifeMs = 24 * 60 * 60 * 1000;
309
281
  s += Math.max(0, 100 * Math.pow(0.5, Math.max(0, ageMs) / halfLifeMs));
310
282
  }
311
- // tag relevance
312
283
  for (const t of tagHints) if (r.tags.includes(t)) s += 5;
313
284
  return s;
314
285
  };
315
286
  hits.sort((a, b) => {
316
287
  const ds = score(b) - score(a);
317
288
  if (ds !== 0) return ds;
318
- // stable recency tie-break
319
289
  return String(b.createdAt || '').localeCompare(String(a.createdAt || ''));
320
290
  });
321
291
  results = hits.slice(0, top).map((r) => ({
@@ -329,31 +299,30 @@ export function buildTaskCtxMemories(memoryDir, opts = {}) {
329
299
  return results;
330
300
  }
331
301
 
332
- // ─── (F) related-task ACL fail-closed gate ───────────────────────
302
+ // ─── (F) related-feature ACL fail-closed gate ─────────────────────
333
303
 
334
304
  /**
335
- * Return memory/summary hints for a related task only when ACL grants.
336
- * Caller passes the TaskStore so we can ask `canAccessRelated()`.
305
+ * Return memory/summary hints for a related feature only when ACL grants.
306
+ * Caller passes the FeatureStore so we can ask `canAccessRelated()`.
337
307
  *
338
308
  * @param {{
339
- * taskStore: import('./store.js').TaskStore,
340
- * currentTaskId: string,
341
- * otherTaskId: string,
309
+ * featureStore: import('./store.js').FeatureStore,
310
+ * currentFeatureId: string,
311
+ * otherFeatureId: string,
342
312
  * vpId: string,
343
313
  * groupsRoot: string,
344
314
  * top?: number,
345
315
  * }} input
346
316
  * @returns {null | { id:string, title:string, members:string[], updatedAt?:number, memories:Array<{body:string,shard:string}> }}
347
- * null iff ACL denies — NEVER leak taskId in that case.
348
317
  */
349
- export function getRelatedTaskCtx(input) {
350
- const { taskStore, currentTaskId, otherTaskId, vpId, groupsRoot, top = 2 } = input || {};
351
- if (!taskStore || !currentTaskId || !otherTaskId || !vpId || !groupsRoot) return null;
352
- if (!taskStore.canAccessRelated(currentTaskId, otherTaskId, vpId)) return null;
353
- const other = taskStore.get(otherTaskId);
318
+ export function getRelatedFeatureCtx(input) {
319
+ const { featureStore, currentFeatureId, otherFeatureId, vpId, groupsRoot, top = 2 } = input || {};
320
+ if (!featureStore || !currentFeatureId || !otherFeatureId || !vpId || !groupsRoot) return null;
321
+ if (!featureStore.canAccessRelated(currentFeatureId, otherFeatureId, vpId)) return null;
322
+ const other = featureStore.get(otherFeatureId);
354
323
  if (!other || !other.groupId) return null;
355
- const memoryDir = join(groupsRoot, other.groupId, 'tasks', other.id, 'memory');
356
- const mems = buildTaskCtxMemories(memoryDir, { top });
324
+ const memoryDir = join(groupsRoot, other.groupId, 'features', other.id, 'memory');
325
+ const mems = buildFeatureCtxMemories(memoryDir, { top });
357
326
  return {
358
327
  id: other.id,
359
328
  title: other.title || other.id,
@@ -4,16 +4,16 @@
4
4
  * Replaces the old entries-based dream scanner with shard-aware streaming:
5
5
  * 1. Shard scanner: iterate shards → stream entries → build orient/merge/prune inputs
6
6
  * 2. Compact job: rewrite shards with utilization < 50% to reclaim tombstones
7
- * 3. Task-memory guard: dream NEVER writes to task-memory shards (avoids double-write)
7
+ * 3. Feature-memory guard: dream NEVER writes to feature-memory shards (avoids double-write)
8
8
  *
9
9
  * References:
10
10
  * - R5 delta §Δ17.5: compact job spec
11
- * - R5 delta §Δ16.4.3: "auto-dream 不写 task-memory"
11
+ * - R5 delta §Δ16.4.3: "auto-dream 不写 feature-memory"
12
12
  * - 334f shard-store API: stageRecompression / commitRecompression / abortRecompression
13
- * - schema.js: TASK_SHARDS, softCapFor
13
+ * - schema.js: FEATURE_SHARDS, softCapFor
14
14
  */
15
15
 
16
- import { TASK_SHARDS, softCapFor } from './schema.js';
16
+ import { FEATURE_SHARDS, softCapFor } from './schema.js';
17
17
  import { AUTHORED_BY } from './shard-store.js';
18
18
  import { pickEffort } from '../effort.js';
19
19
 
@@ -28,30 +28,30 @@ const MAX_COMPACTS_PER_DREAM = 4;
28
28
  /** Maximum LLM calls for shard-based dream phases. */
29
29
  const MAX_SHARD_DREAM_LLM_CALLS = 5;
30
30
 
31
- /** Task-memory shard names — dream must never write to these. */
32
- const TASK_SHARD_SET = new Set(TASK_SHARDS);
31
+ /** Feature-memory shard names — dream must never write to these. */
32
+ const FEATURE_SHARD_SET = new Set(FEATURE_SHARDS);
33
33
 
34
- // ─── Task-Memory Guard ─────────────────────────────────────
34
+ // ─── Feature-Memory Guard ─────────────────────────────────────
35
35
 
36
36
  /**
37
- * Returns true if the shard name belongs to task-memory.
37
+ * Returns true if the shard name belongs to feature-memory.
38
38
  * Dream must NOT write entries to these shards.
39
39
  *
40
40
  * @param {string} shardName
41
41
  * @returns {boolean}
42
42
  */
43
- export function isTaskMemoryShard(shardName) {
44
- return TASK_SHARD_SET.has(shardName);
43
+ export function isFeatureMemoryShard(shardName) {
44
+ return FEATURE_SHARD_SET.has(shardName);
45
45
  }
46
46
 
47
47
  /**
48
- * Filter out task-memory shards from a list of shard names.
48
+ * Filter out feature-memory shards from a list of shard names.
49
49
  *
50
50
  * @param {string[]} shardNames
51
51
  * @returns {string[]}
52
52
  */
53
53
  export function filterDreamableShards(shardNames) {
54
- return shardNames.filter(s => !isTaskMemoryShard(s));
54
+ return shardNames.filter(s => !isFeatureMemoryShard(s));
55
55
  }
56
56
 
57
57
  // ─── Shard Scanner ─────────────────────────────────────────
@@ -319,7 +319,7 @@ function compactShard(shardStore, shardName) {
319
319
  * 3. Merge — LLM-driven merge of duplicate/superseded entries
320
320
  * 4. Prune — LLM-driven removal of stale entries
321
321
  *
322
- * Task-memory guard: all phases skip TASK_SHARDS entirely.
322
+ * Feature-memory guard: all phases skip FEATURE_SHARDS entirely.
323
323
  *
324
324
  * @param {{
325
325
  * shardStore: object,
@@ -509,7 +509,7 @@ async function runPrunePhase({ shardStore, scan, adapter, config }) {
509
509
  const st = shardStore.stats();
510
510
  const overCapShards = [];
511
511
  for (const [name, bucket] of Object.entries(st.shards)) {
512
- if (isTaskMemoryShard(name)) continue;
512
+ if (isFeatureMemoryShard(name)) continue;
513
513
  const cap = softCapFor(name);
514
514
  if (bucket.entries > cap.entries || bucket.bytes > cap.bytes) {
515
515
  overCapShards.push(name);
@@ -551,7 +551,7 @@ async function runPrunePhase({ shardStore, scan, adapter, config }) {
551
551
  if (typeof id !== 'string') continue;
552
552
  // Double-check it's not in a task shard
553
553
  const entry = scan.entries.find(e => e.id === id);
554
- if (entry && isTaskMemoryShard(entry.shard)) continue;
554
+ if (entry && isFeatureMemoryShard(entry.shard)) continue;
555
555
  try {
556
556
  shardStore.remove(id);
557
557
  result.prunedCount++;
@@ -15,7 +15,7 @@ import { createHash } from 'crypto';
15
15
  import { pickEffort } from '../effort.js';
16
16
  import {
17
17
  VP_DEFAULT_SHARDS,
18
- TASK_SHARDS,
18
+ FEATURE_SHARDS,
19
19
  USER_SHARDS,
20
20
  } from './schema.js';
21
21
 
@@ -286,6 +286,6 @@ export function clearR6RecallCache() {
286
286
 
287
287
  export const R6_DEFAULTS = {
288
288
  VP_DEFAULT_SHARDS,
289
- TASK_SHARDS,
289
+ FEATURE_SHARDS,
290
290
  USER_SHARDS,
291
291
  };
@@ -19,8 +19,8 @@ export const VP_DEFAULT_SHARDS = Object.freeze([
19
19
  'preferences',
20
20
  ]);
21
21
 
22
- // ─── §Δ25.2 Task-memory fixed 5 shards ──────────────────────────
23
- export const TASK_SHARDS = Object.freeze([
22
+ // ─── §Δ25.2 Feature-memory fixed 5 shards ───────────────────────
23
+ export const FEATURE_SHARDS = Object.freeze([
24
24
  'decision',
25
25
  'progress',
26
26
  'context',
@@ -49,7 +49,7 @@ export const SOFT_CAPS = Object.freeze({
49
49
  lessons: { entries: 80, bytes: 64 * 1024 },
50
50
  preferences: { entries: 80, bytes: 64 * 1024 },
51
51
  relations: { entries: 50, bytes: 32 * 1024 },
52
- // Task (Δ26.3 task-memory row)
52
+ // Feature (Δ26.3 feature-memory row)
53
53
  decision: { entries: 40, bytes: 24 * 1024 },
54
54
  progress: { entries: 40, bytes: 24 * 1024 },
55
55
  context: { entries: 40, bytes: 24 * 1024 },
@@ -99,16 +99,16 @@ export function softCapFor(shardName) {
99
99
  /**
100
100
  * Build a schema object suitable for `openShardStore(dir, schema)` (334o).
101
101
  *
102
- * @param {'vp'|'task'|'user'} kind
102
+ * @param {'vp'|'feature'|'user'} kind
103
103
  * @param {{ extraShards?: string[] }} [opts] e.g. existing project shards
104
104
  * @returns {{ shards: string[], softCap: Record<string,{entries:number,bytes:number}>, defaultSoftCap: object }}
105
105
  */
106
106
  export function buildShardSchema(kind, opts = {}) {
107
107
  let shards;
108
108
  switch (kind) {
109
- case 'vp': shards = [...VP_DEFAULT_SHARDS]; break;
110
- case 'task': shards = [...TASK_SHARDS]; break;
111
- case 'user': shards = [...USER_SHARDS]; break;
109
+ case 'vp': shards = [...VP_DEFAULT_SHARDS]; break;
110
+ case 'feature': shards = [...FEATURE_SHARDS]; break;
111
+ case 'user': shards = [...USER_SHARDS]; break;
112
112
  default: throw new Error(`buildShardSchema: unknown kind "${kind}"`);
113
113
  }
114
114
  if (Array.isArray(opts.extraShards)) {
@@ -10,7 +10,7 @@
10
10
  * entries/<yyyy-mm-dd>-<slug>.md
11
11
  * groups/<groupId>/ — same shape
12
12
  * vp/<vpId>/ — same shape
13
- * tasks/<taskId>/ — same shape, plus archive/
13
+ * features/<featureId>/ — same shape, plus archive/
14
14
  *
15
15
  * This module is concerned ONLY with on-disk shape + atomic writes. It does
16
16
  * NOT do any LLM work (extraction, summarisation, dream maintenance) — those
@@ -50,12 +50,12 @@ import { homedir } from 'os';
50
50
  /** Default memory root. Tests override via `opts.root`. */
51
51
  export const DEFAULT_MEMORY_ROOT = join(homedir(), '.yeaft', 'memory');
52
52
 
53
- /** @typedef {'user'|'group'|'vp'|'task'} ScopeKind */
53
+ /** @typedef {'user'|'group'|'vp'|'feature'} ScopeKind */
54
54
  /** @typedef {{kind: ScopeKind, id?: string}} Scope */
55
55
 
56
56
  /**
57
57
  * Compute the scope's path segment relative to the memory root.
58
- * `user/`, `groups/<id>/`, `vp/<id>/`, `tasks/<id>/`.
58
+ * `user/`, `groups/<id>/`, `vp/<id>/`, `features/<id>/`.
59
59
  *
60
60
  * @param {Scope} scope
61
61
  * @returns {string}
@@ -73,9 +73,9 @@ export function scopeDir(scope) {
73
73
  case 'vp':
74
74
  if (!scope.id) throw new Error('scopeDir: vp scope requires id');
75
75
  return `vp/${scope.id}`;
76
- case 'task':
77
- if (!scope.id) throw new Error('scopeDir: task scope requires id');
78
- return `tasks/${scope.id}`;
76
+ case 'feature':
77
+ if (!scope.id) throw new Error('scopeDir: feature scope requires id');
78
+ return `features/${scope.id}`;
79
79
  default:
80
80
  throw new Error(`scopeDir: unknown kind ${JSON.stringify(scope.kind)}`);
81
81
  }
@@ -74,7 +74,7 @@
74
74
  */
75
75
 
76
76
  import { MAIN_THREAD_ID } from '../threads/store.js';
77
- import { getTaskStore } from '../tools/task-tools.js';
77
+ import { getFeatureStore } from '../tools/feature-tools.js';
78
78
 
79
79
  /**
80
80
  * Per-entry transient metadata (messageId, override) lives here — a
@@ -220,7 +220,7 @@ export class Dispatcher {
220
220
  const allThreads = threadStore.list().map(t => ({
221
221
  id: t.id, name: t.name, goal: t.goal, status: t.status,
222
222
  }));
223
- const pendingTasks = this.#listPendingTasks();
223
+ const pendingFeatures = this.#listPendingFeatures();
224
224
 
225
225
  // ── Step 3: classify (explicit override > classifier) ──
226
226
  /** @type {import('../router/intent-classifier.js').RouterDecision} */
@@ -245,7 +245,7 @@ export class Dispatcher {
245
245
  userMessage: claimed.text,
246
246
  currentThreadId,
247
247
  allThreads,
248
- pendingTasks,
248
+ pendingFeatures,
249
249
  messageId: meta.messageId || undefined,
250
250
  });
251
251
  } catch (err) {
@@ -347,12 +347,12 @@ export class Dispatcher {
347
347
  }
348
348
  }
349
349
 
350
- #listPendingTasks() {
351
- // Best-effort: the TaskStore is a singleton initialised in loadSession().
350
+ #listPendingFeatures() {
351
+ // Best-effort: the FeatureStore is a singleton initialised in loadSession().
352
352
  // If the store isn't available (e.g. unit tests without a session) we
353
- // just return []. Never let a TaskStore exception break routing.
353
+ // just return []. Never let a FeatureStore exception break routing.
354
354
  try {
355
- const store = getTaskStore();
355
+ const store = getFeatureStore();
356
356
  if (!store || typeof store.list !== 'function') return [];
357
357
  const pending = store.list({ status: 'pending' }) || [];
358
358
  return pending.map(t => ({