@lumenflow/cli 3.19.0 → 3.20.0

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.
@@ -26,10 +26,11 @@ import {
26
26
  const TOOL_NAMES = {
27
27
  STORE: 'memory:store',
28
28
  RECALL: 'memory:recall',
29
+ UPDATE: 'memory:update',
29
30
  FORGET: 'memory:forget',
30
31
  } as const;
31
32
 
32
- const VALID_MEMORY_TYPES: MemoryType[] = ['fact', 'preference', 'note'];
33
+ const VALID_MEMORY_TYPES: MemoryType[] = ['fact', 'preference', 'note', 'snippet'];
33
34
 
34
35
  // ---------------------------------------------------------------------------
35
36
  // Helpers
@@ -126,6 +127,88 @@ async function memoryRecallTool(input: unknown, _context?: ToolContextLike): Pro
126
127
  });
127
128
  }
128
129
 
130
+ // ---------------------------------------------------------------------------
131
+ // memory:update
132
+ // ---------------------------------------------------------------------------
133
+
134
+ async function memoryUpdateTool(input: unknown, context?: ToolContextLike): Promise<ToolOutput> {
135
+ const parsed = toRecord(input);
136
+ const id = asNonEmptyString(parsed.id);
137
+
138
+ if (!id) {
139
+ return failure('INVALID_INPUT', 'id is required.');
140
+ }
141
+
142
+ const patch: Partial<MemoryRecord> = {};
143
+ if (parsed.type !== undefined) {
144
+ const type = asMemoryType(parsed.type);
145
+ if (!type) {
146
+ return failure('INVALID_INPUT', 'type must be one of fact, preference, note, snippet.');
147
+ }
148
+ patch.type = type;
149
+ }
150
+ if (parsed.content !== undefined) {
151
+ const content = asNonEmptyString(parsed.content);
152
+ if (!content) {
153
+ return failure('INVALID_INPUT', 'content must be a non-empty string when provided.');
154
+ }
155
+ patch.content = content;
156
+ }
157
+ if (parsed.tags !== undefined) {
158
+ patch.tags = asStringArray(parsed.tags);
159
+ }
160
+
161
+ if (Object.keys(patch).length === 0) {
162
+ return failure(
163
+ 'INVALID_INPUT',
164
+ 'memory:update requires at least one of type, content, or tags.',
165
+ );
166
+ }
167
+
168
+ const storage = getStoragePort();
169
+ const memories = await storage.readStore('memories');
170
+ const memory = memories.find((entry) => entry.id === id);
171
+
172
+ if (!memory) {
173
+ return failure('NOT_FOUND', `memory ${id} was not found.`);
174
+ }
175
+
176
+ const preview: MemoryRecord = {
177
+ ...memory,
178
+ ...patch,
179
+ updated_at: nowIso(),
180
+ };
181
+
182
+ if (isDryRun(parsed)) {
183
+ return success({
184
+ dry_run: true,
185
+ memory: preview as unknown as Record<string, unknown>,
186
+ });
187
+ }
188
+
189
+ await storage.withLock(async () => {
190
+ const latest = await storage.readStore('memories');
191
+ const target = latest.find((entry) => entry.id === id);
192
+ if (!target) {
193
+ return;
194
+ }
195
+ Object.assign(target, preview);
196
+ await storage.writeStore('memories', latest);
197
+ await storage.appendAudit(
198
+ buildAuditEvent({
199
+ tool: TOOL_NAMES.UPDATE,
200
+ op: 'update',
201
+ context,
202
+ ids: [id],
203
+ }),
204
+ );
205
+ });
206
+
207
+ const updated = await storage.readStore('memories');
208
+ const updatedMemory = updated.find((entry) => entry.id === id);
209
+ return success({ memory: updatedMemory as unknown as Record<string, unknown> });
210
+ }
211
+
129
212
  // ---------------------------------------------------------------------------
130
213
  // memory:forget
131
214
  // ---------------------------------------------------------------------------
@@ -180,6 +263,8 @@ export default async function memoryTools(
180
263
  return memoryStoreTool(input, context);
181
264
  case TOOL_NAMES.RECALL:
182
265
  return memoryRecallTool(input, context);
266
+ case TOOL_NAMES.UPDATE:
267
+ return memoryUpdateTool(input, context);
183
268
  case TOOL_NAMES.FORGET:
184
269
  return memoryForgetTool(input, context);
185
270
  default:
@@ -23,8 +23,11 @@ import {
23
23
  const TOOL_NAMES = {
24
24
  CREATE: 'routine:create',
25
25
  LIST: 'routine:list',
26
+ UPDATE: 'routine:update',
27
+ DELETE: 'routine:delete',
26
28
  RUN: 'routine:run',
27
29
  } as const;
30
+ const ROUTINE_ID_REQUIRED_MESSAGE = 'id is required.';
28
31
 
29
32
  // ---------------------------------------------------------------------------
30
33
  // Helpers
@@ -80,6 +83,13 @@ function normalizeSteps(value: unknown): NormalizeStepsResult {
80
83
  return { steps, warnings };
81
84
  }
82
85
 
86
+ function asOptionalBoolean(value: unknown): boolean | null {
87
+ if (value === true || value === false) {
88
+ return value;
89
+ }
90
+ return null;
91
+ }
92
+
83
93
  // ---------------------------------------------------------------------------
84
94
  // routine:create
85
95
  // ---------------------------------------------------------------------------
@@ -105,6 +115,8 @@ async function routineCreateTool(input: unknown, context?: ToolContextLike): Pro
105
115
  id: createId('routine'),
106
116
  name,
107
117
  steps,
118
+ cron: asNonEmptyString(parsed.cron) ?? undefined,
119
+ enabled: asOptionalBoolean(parsed.enabled) ?? true,
108
120
  created_at: now,
109
121
  updated_at: now,
110
122
  };
@@ -145,11 +157,13 @@ async function routineCreateTool(input: unknown, context?: ToolContextLike): Pro
145
157
  async function routineListTool(input: unknown, _context?: ToolContextLike): Promise<ToolOutput> {
146
158
  const parsed = toRecord(input);
147
159
  const limit = asInteger(parsed.limit);
160
+ const enabledOnly = parsed.enabled_only === true;
148
161
 
149
162
  const storage = getStoragePort();
150
163
  const routines = await storage.readStore('routines');
151
164
 
152
- const sorted = routines.toSorted((a, b) => Date.parse(b.updated_at) - Date.parse(a.updated_at));
165
+ const filtered = enabledOnly ? routines.filter((routine) => routine.enabled) : routines;
166
+ const sorted = filtered.toSorted((a, b) => Date.parse(b.updated_at) - Date.parse(a.updated_at));
153
167
 
154
168
  const items = limit && limit > 0 ? sorted.slice(0, limit) : sorted;
155
169
 
@@ -159,6 +173,142 @@ async function routineListTool(input: unknown, _context?: ToolContextLike): Prom
159
173
  });
160
174
  }
161
175
 
176
+ // ---------------------------------------------------------------------------
177
+ // routine:update
178
+ // ---------------------------------------------------------------------------
179
+
180
+ async function routineUpdateTool(input: unknown, context?: ToolContextLike): Promise<ToolOutput> {
181
+ const parsed = toRecord(input);
182
+ const id = asNonEmptyString(parsed.id);
183
+
184
+ if (!id) {
185
+ return failure('INVALID_INPUT', ROUTINE_ID_REQUIRED_MESSAGE);
186
+ }
187
+
188
+ const patch: Partial<RoutineRecord> = {};
189
+ const name = asNonEmptyString(parsed.name);
190
+ const cron =
191
+ typeof parsed.cron === 'string' ? (asNonEmptyString(parsed.cron) ?? undefined) : null;
192
+ const enabled = asOptionalBoolean(parsed.enabled);
193
+
194
+ if (name) {
195
+ patch.name = name;
196
+ }
197
+ if (cron !== null) {
198
+ patch.cron = cron;
199
+ }
200
+ if (enabled !== null) {
201
+ patch.enabled = enabled;
202
+ }
203
+ if (parsed.steps !== undefined) {
204
+ const { steps, warnings } = normalizeSteps(parsed.steps);
205
+ if (steps.length === 0) {
206
+ const detail =
207
+ warnings.length > 0
208
+ ? `steps must include at least one tool step. Issues: ${warnings.join('; ')}`
209
+ : 'steps must include at least one tool step.';
210
+ return failure('INVALID_INPUT', detail);
211
+ }
212
+ patch.steps = steps;
213
+ }
214
+
215
+ if (Object.keys(patch).length === 0) {
216
+ return failure(
217
+ 'INVALID_INPUT',
218
+ 'routine:update requires at least one of name, steps, cron, or enabled.',
219
+ );
220
+ }
221
+
222
+ const storage = getStoragePort();
223
+ const routines = await storage.readStore('routines');
224
+ const routine = routines.find((entry) => entry.id === id);
225
+
226
+ if (!routine) {
227
+ return failure('NOT_FOUND', `routine ${id} was not found.`);
228
+ }
229
+
230
+ const preview: RoutineRecord = {
231
+ ...routine,
232
+ ...patch,
233
+ updated_at: nowIso(),
234
+ };
235
+
236
+ if (isDryRun(parsed)) {
237
+ return success({
238
+ dry_run: true,
239
+ routine: preview as unknown as Record<string, unknown>,
240
+ });
241
+ }
242
+
243
+ await storage.withLock(async () => {
244
+ const latest = await storage.readStore('routines');
245
+ const target = latest.find((entry) => entry.id === id);
246
+ if (!target) {
247
+ return;
248
+ }
249
+ Object.assign(target, preview);
250
+ await storage.writeStore('routines', latest);
251
+ await storage.appendAudit(
252
+ buildAuditEvent({
253
+ tool: TOOL_NAMES.UPDATE,
254
+ op: 'update',
255
+ context,
256
+ ids: [id],
257
+ }),
258
+ );
259
+ });
260
+
261
+ const updated = await storage.readStore('routines');
262
+ const updatedRoutine = updated.find((entry) => entry.id === id);
263
+ return success({ routine: updatedRoutine as unknown as Record<string, unknown> });
264
+ }
265
+
266
+ // ---------------------------------------------------------------------------
267
+ // routine:delete
268
+ // ---------------------------------------------------------------------------
269
+
270
+ async function routineDeleteTool(input: unknown, context?: ToolContextLike): Promise<ToolOutput> {
271
+ const parsed = toRecord(input);
272
+ const id = asNonEmptyString(parsed.id);
273
+
274
+ if (!id) {
275
+ return failure('INVALID_INPUT', ROUTINE_ID_REQUIRED_MESSAGE);
276
+ }
277
+
278
+ const storage = getStoragePort();
279
+ const routines = await storage.readStore('routines');
280
+ const routine = routines.find((entry) => entry.id === id);
281
+
282
+ if (!routine) {
283
+ return failure('NOT_FOUND', `routine ${id} was not found.`);
284
+ }
285
+
286
+ if (isDryRun(parsed)) {
287
+ return success({
288
+ dry_run: true,
289
+ deleted_id: id,
290
+ });
291
+ }
292
+
293
+ await storage.withLock(async () => {
294
+ const latest = await storage.readStore('routines');
295
+ await storage.writeStore(
296
+ 'routines',
297
+ latest.filter((entry) => entry.id !== id),
298
+ );
299
+ await storage.appendAudit(
300
+ buildAuditEvent({
301
+ tool: TOOL_NAMES.DELETE,
302
+ op: 'delete',
303
+ context,
304
+ ids: [id],
305
+ }),
306
+ );
307
+ });
308
+
309
+ return success({ deleted_id: id });
310
+ }
311
+
162
312
  // ---------------------------------------------------------------------------
163
313
  // routine:run (PLAN-ONLY -- does NOT execute tool steps)
164
314
  // ---------------------------------------------------------------------------
@@ -168,7 +318,7 @@ async function routineRunTool(input: unknown, context?: ToolContextLike): Promis
168
318
  const id = asNonEmptyString(parsed.id);
169
319
 
170
320
  if (!id) {
171
- return failure('INVALID_INPUT', 'id is required.');
321
+ return failure('INVALID_INPUT', ROUTINE_ID_REQUIRED_MESSAGE);
172
322
  }
173
323
 
174
324
  const storage = getStoragePort();
@@ -218,6 +368,10 @@ export default async function routineTools(
218
368
  return routineCreateTool(input, context);
219
369
  case TOOL_NAMES.LIST:
220
370
  return routineListTool(input, context);
371
+ case TOOL_NAMES.UPDATE:
372
+ return routineUpdateTool(input, context);
373
+ case TOOL_NAMES.DELETE:
374
+ return routineDeleteTool(input, context);
221
375
  case TOOL_NAMES.RUN:
222
376
  return routineRunTool(input, context);
223
377
  default:
@@ -21,8 +21,8 @@ const RANDOM_BYTES_LENGTH = 4;
21
21
  // ---------------------------------------------------------------------------
22
22
 
23
23
  export type TaskPriority = 'P0' | 'P1' | 'P2' | 'P3';
24
- export type TaskStatus = 'pending' | 'done';
25
- export type MemoryType = 'fact' | 'preference' | 'note';
24
+ export type TaskStatus = 'pending' | 'done' | 'canceled';
25
+ export type MemoryType = 'fact' | 'preference' | 'note' | 'snippet';
26
26
 
27
27
  export interface TaskRecord {
28
28
  id: string;
@@ -32,10 +32,12 @@ export interface TaskRecord {
32
32
  status: TaskStatus;
33
33
  tags: string[];
34
34
  due_at?: string;
35
+ cron?: string;
35
36
  note?: string;
36
37
  created_at: string;
37
38
  updated_at: string;
38
39
  completed_at?: string;
40
+ canceled_at?: string;
39
41
  }
40
42
 
41
43
  export interface MemoryRecord {
@@ -71,6 +73,8 @@ export interface RoutineRecord {
71
73
  id: string;
72
74
  name: string;
73
75
  steps: RoutineStepRecord[];
76
+ cron?: string;
77
+ enabled: boolean;
74
78
  created_at: string;
75
79
  updated_at: string;
76
80
  }
@@ -140,9 +144,6 @@ const LOCK_FILE_PATH = '.lock';
140
144
  // ---------------------------------------------------------------------------
141
145
 
142
146
  function cloneStore<K extends StoreName>(store: K, value: SidekickStores[K]): SidekickStores[K] {
143
- if (Array.isArray(value)) {
144
- return value.map((entry) => ({ ...entry })) as SidekickStores[K];
145
- }
146
147
  return structuredClone(value) as SidekickStores[K];
147
148
  }
148
149
 
@@ -9,6 +9,7 @@ import {
9
9
  buildAuditEvent,
10
10
  createId,
11
11
  failure,
12
+ includesText,
12
13
  isDryRun,
13
14
  matchesTags,
14
15
  nowIso,
@@ -25,12 +26,15 @@ import {
25
26
  const TOOL_NAMES = {
26
27
  CREATE: 'task:create',
27
28
  LIST: 'task:list',
29
+ UPDATE: 'task:update',
30
+ CANCEL: 'task:cancel',
28
31
  COMPLETE: 'task:complete',
29
32
  SCHEDULE: 'task:schedule',
30
33
  } as const;
31
34
 
32
35
  const VALID_PRIORITIES: TaskPriority[] = ['P0', 'P1', 'P2', 'P3'];
33
36
  const DEFAULT_PRIORITY: TaskPriority = 'P2';
37
+ const TASK_ID_REQUIRED_MESSAGE = 'id is required.';
34
38
 
35
39
  // ---------------------------------------------------------------------------
36
40
  // Helpers
@@ -42,6 +46,57 @@ function asPriority(value: unknown): TaskPriority {
42
46
  : DEFAULT_PRIORITY;
43
47
  }
44
48
 
49
+ function resolveTaskUpdatePatch(parsed: Record<string, unknown>): Partial<TaskRecord> {
50
+ const patch: Partial<TaskRecord> = {};
51
+ const title = asNonEmptyString(parsed.title);
52
+ const description =
53
+ typeof parsed.description === 'string'
54
+ ? (asNonEmptyString(parsed.description) ?? undefined)
55
+ : null;
56
+ const dueAt =
57
+ typeof parsed.due_at === 'string' ? (asNonEmptyString(parsed.due_at) ?? undefined) : null;
58
+ const cron =
59
+ typeof parsed.cron === 'string' ? (asNonEmptyString(parsed.cron) ?? undefined) : null;
60
+
61
+ if (title) {
62
+ patch.title = title;
63
+ }
64
+ if (description !== null) {
65
+ patch.description = description;
66
+ }
67
+ if (dueAt !== null) {
68
+ patch.due_at = dueAt;
69
+ }
70
+ if (cron !== null) {
71
+ patch.cron = cron;
72
+ }
73
+ if (parsed.priority !== undefined) {
74
+ patch.priority = asPriority(parsed.priority);
75
+ }
76
+ if (parsed.tags !== undefined) {
77
+ patch.tags = asStringArray(parsed.tags);
78
+ }
79
+
80
+ return patch;
81
+ }
82
+
83
+ function applyTaskPatch(task: TaskRecord, patch: Partial<TaskRecord>): TaskRecord {
84
+ return {
85
+ ...task,
86
+ ...patch,
87
+ updated_at: nowIso(),
88
+ };
89
+ }
90
+
91
+ function buildCanceledTask(task: TaskRecord): TaskRecord {
92
+ return {
93
+ ...task,
94
+ status: 'canceled',
95
+ canceled_at: nowIso(),
96
+ updated_at: nowIso(),
97
+ };
98
+ }
99
+
45
100
  // ---------------------------------------------------------------------------
46
101
  // task:create
47
102
  // ---------------------------------------------------------------------------
@@ -97,6 +152,7 @@ async function taskListTool(input: unknown, _context?: ToolContextLike): Promise
97
152
  const statusFilter = asNonEmptyString(parsed.status);
98
153
  const priorityFilter = asNonEmptyString(parsed.priority);
99
154
  const tags = asStringArray(parsed.tags);
155
+ const search = asNonEmptyString(parsed.search);
100
156
  const dueBefore = asNonEmptyString(parsed.due_before);
101
157
  const limit = asInteger(parsed.limit);
102
158
 
@@ -113,6 +169,9 @@ async function taskListTool(input: unknown, _context?: ToolContextLike): Promise
113
169
  if (!matchesTags(tags, task.tags)) {
114
170
  return false;
115
171
  }
172
+ if (!includesText(`${task.title}\n${task.description ?? ''}`, search)) {
173
+ return false;
174
+ }
116
175
  if (dueBefore && task.due_at) {
117
176
  if (Date.parse(task.due_at) >= Date.parse(dueBefore)) {
118
177
  return false;
@@ -134,6 +193,70 @@ async function taskListTool(input: unknown, _context?: ToolContextLike): Promise
134
193
  });
135
194
  }
136
195
 
196
+ // ---------------------------------------------------------------------------
197
+ // task:update
198
+ // ---------------------------------------------------------------------------
199
+
200
+ async function taskUpdateTool(input: unknown, context?: ToolContextLike): Promise<ToolOutput> {
201
+ const parsed = toRecord(input);
202
+ const id = asNonEmptyString(parsed.id);
203
+
204
+ if (!id) {
205
+ return failure('INVALID_INPUT', TASK_ID_REQUIRED_MESSAGE);
206
+ }
207
+
208
+ const patch = resolveTaskUpdatePatch(parsed);
209
+ if (Object.keys(patch).length === 0) {
210
+ return failure(
211
+ 'INVALID_INPUT',
212
+ 'task:update requires at least one of title, description, priority, tags, due_at, or cron.',
213
+ );
214
+ }
215
+
216
+ const storage = getStoragePort();
217
+ const tasks = await storage.readStore('tasks');
218
+ const task = tasks.find((entry) => entry.id === id);
219
+
220
+ if (!task) {
221
+ return failure('NOT_FOUND', `task ${id} was not found.`);
222
+ }
223
+ if (task.status === 'canceled') {
224
+ return failure(
225
+ 'INVALID_STATE',
226
+ `task ${id} is canceled and cannot be completed. Reopen it with task:update before completing.`,
227
+ );
228
+ }
229
+
230
+ if (isDryRun(parsed)) {
231
+ return success({
232
+ dry_run: true,
233
+ task: applyTaskPatch(task, patch) as unknown as Record<string, unknown>,
234
+ });
235
+ }
236
+
237
+ await storage.withLock(async () => {
238
+ const latest = await storage.readStore('tasks');
239
+ const target = latest.find((entry) => entry.id === id);
240
+ if (!target) {
241
+ return;
242
+ }
243
+ Object.assign(target, applyTaskPatch(target, patch));
244
+ await storage.writeStore('tasks', latest);
245
+ await storage.appendAudit(
246
+ buildAuditEvent({
247
+ tool: TOOL_NAMES.UPDATE,
248
+ op: 'update',
249
+ context,
250
+ ids: [id],
251
+ }),
252
+ );
253
+ });
254
+
255
+ const updated = await storage.readStore('tasks');
256
+ const updatedTask = updated.find((entry) => entry.id === id);
257
+ return success({ task: updatedTask as unknown as Record<string, unknown> });
258
+ }
259
+
137
260
  // ---------------------------------------------------------------------------
138
261
  // task:complete
139
262
  // ---------------------------------------------------------------------------
@@ -143,7 +266,7 @@ async function taskCompleteTool(input: unknown, context?: ToolContextLike): Prom
143
266
  const id = asNonEmptyString(parsed.id);
144
267
 
145
268
  if (!id) {
146
- return failure('INVALID_INPUT', 'id is required.');
269
+ return failure('INVALID_INPUT', TASK_ID_REQUIRED_MESSAGE);
147
270
  }
148
271
 
149
272
  const storage = getStoragePort();
@@ -190,6 +313,62 @@ async function taskCompleteTool(input: unknown, context?: ToolContextLike): Prom
190
313
  return success({ task: completedTask as unknown as Record<string, unknown> });
191
314
  }
192
315
 
316
+ // ---------------------------------------------------------------------------
317
+ // task:cancel
318
+ // ---------------------------------------------------------------------------
319
+
320
+ async function taskCancelTool(input: unknown, context?: ToolContextLike): Promise<ToolOutput> {
321
+ const parsed = toRecord(input);
322
+ const id = asNonEmptyString(parsed.id);
323
+
324
+ if (!id) {
325
+ return failure('INVALID_INPUT', TASK_ID_REQUIRED_MESSAGE);
326
+ }
327
+
328
+ const storage = getStoragePort();
329
+ const tasks = await storage.readStore('tasks');
330
+ const task = tasks.find((entry) => entry.id === id);
331
+
332
+ if (!task) {
333
+ return failure('NOT_FOUND', `task ${id} was not found.`);
334
+ }
335
+ if (task.status === 'done') {
336
+ return failure(
337
+ 'INVALID_STATE',
338
+ `task ${id} is already done and cannot be canceled. Use task:update if you only need to adjust metadata.`,
339
+ );
340
+ }
341
+
342
+ if (isDryRun(parsed)) {
343
+ return success({
344
+ dry_run: true,
345
+ task: buildCanceledTask(task) as unknown as Record<string, unknown>,
346
+ });
347
+ }
348
+
349
+ await storage.withLock(async () => {
350
+ const latest = await storage.readStore('tasks');
351
+ const target = latest.find((entry) => entry.id === id);
352
+ if (!target) {
353
+ return;
354
+ }
355
+ Object.assign(target, buildCanceledTask(target));
356
+ await storage.writeStore('tasks', latest);
357
+ await storage.appendAudit(
358
+ buildAuditEvent({
359
+ tool: TOOL_NAMES.CANCEL,
360
+ op: 'update',
361
+ context,
362
+ ids: [id],
363
+ }),
364
+ );
365
+ });
366
+
367
+ const updated = await storage.readStore('tasks');
368
+ const canceledTask = updated.find((entry) => entry.id === id);
369
+ return success({ task: canceledTask as unknown as Record<string, unknown> });
370
+ }
371
+
193
372
  // ---------------------------------------------------------------------------
194
373
  // task:schedule
195
374
  // ---------------------------------------------------------------------------
@@ -199,7 +378,7 @@ async function taskScheduleTool(input: unknown, context?: ToolContextLike): Prom
199
378
  const id = asNonEmptyString(parsed.id);
200
379
 
201
380
  if (!id) {
202
- return failure('INVALID_INPUT', 'id is required.');
381
+ return failure('INVALID_INPUT', TASK_ID_REQUIRED_MESSAGE);
203
382
  }
204
383
 
205
384
  const storage = getStoragePort();
@@ -232,9 +411,8 @@ async function taskScheduleTool(input: unknown, context?: ToolContextLike): Prom
232
411
  if (dueAt) {
233
412
  target.due_at = dueAt;
234
413
  }
235
- // cron is stored but TaskRecord doesn't have it yet -- extend inline
236
414
  if (cron) {
237
- (target as unknown as Record<string, unknown>).cron = cron;
415
+ target.cron = cron;
238
416
  }
239
417
  target.updated_at = nowIso();
240
418
  await storage.writeStore('tasks', latest);
@@ -268,6 +446,10 @@ export default async function taskTools(
268
446
  return taskCreateTool(input, context);
269
447
  case TOOL_NAMES.LIST:
270
448
  return taskListTool(input, context);
449
+ case TOOL_NAMES.UPDATE:
450
+ return taskUpdateTool(input, context);
451
+ case TOOL_NAMES.CANCEL:
452
+ return taskCancelTool(input, context);
271
453
  case TOOL_NAMES.COMPLETE:
272
454
  return taskCompleteTool(input, context);
273
455
  case TOOL_NAMES.SCHEDULE:
@@ -1,4 +1,4 @@
1
1
 
2
- > @lumenflow/packs-software-delivery@3.19.0 build /home/runner/work/lumenflow-dev/lumenflow-dev/packages/@lumenflow/packs/software-delivery
2
+ > @lumenflow/packs-software-delivery@3.20.0 build /home/runner/work/lumenflow-dev/lumenflow-dev/packages/@lumenflow/packs/software-delivery
3
3
  > tsc
4
4
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumenflow/packs-software-delivery",
3
- "version": "3.19.0",
3
+ "version": "3.20.0",
4
4
  "description": "Software delivery pack for LumenFlow — work units, gates, lanes, initiatives, and agent coordination",
5
5
  "keywords": [
6
6
  "lumenflow",
@@ -102,7 +102,7 @@ Documentation WUs (`type: documentation`) have relaxed file count thresholds bec
102
102
  id: WU-XXX
103
103
  type: documentation
104
104
  code_paths:
105
- - docs/operations/_frameworks/lumenflow/*.md
105
+ - {{DOCS_OPERATIONS_PATH}}/_frameworks/lumenflow/*.md
106
106
  - docs/01-product/*.md
107
107
  ```
108
108
 
@@ -320,7 +320,7 @@ The tooling operates in two modes:
320
320
  ```
321
321
  [wu:create] WARNING (WU-100): sizing: estimated_files (30) exceeds Simple
322
322
  threshold (20). Consider adding exception_type/exception_reason or splitting
323
- the WU. See docs/operations/_frameworks/lumenflow/wu-sizing-guide.md.
323
+ the WU. See {{DOCS_OPERATIONS_PATH}}/_frameworks/lumenflow/wu-sizing-guide.md.
324
324
  ```
325
325
 
326
326
  **Strict mode (`--strict-sizing`):** `wu:brief` supports a `--strict-sizing` flag that blocks when: