@lumenflow/cli 3.19.0 → 3.21.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.
Files changed (36) hide show
  1. package/dist/gates-runners.js +5 -4
  2. package/dist/gates-runners.js.map +1 -1
  3. package/dist/gates-utils.js +71 -0
  4. package/dist/gates-utils.js.map +1 -1
  5. package/dist/init-templates.js +30 -22
  6. package/dist/init-templates.js.map +1 -1
  7. package/dist/wu-prune.js +2 -2
  8. package/dist/wu-prune.js.map +1 -1
  9. package/dist/wu-verify.js +22 -17
  10. package/dist/wu-verify.js.map +1 -1
  11. package/package.json +8 -8
  12. package/packs/agent-runtime/.turbo/turbo-build.log +1 -1
  13. package/packs/agent-runtime/package.json +1 -1
  14. package/packs/sidekick/.turbo/turbo-build.log +1 -1
  15. package/packs/sidekick/README.md +118 -113
  16. package/packs/sidekick/manifest-schema.ts +15 -228
  17. package/packs/sidekick/manifest.ts +107 -7
  18. package/packs/sidekick/manifest.yaml +199 -1
  19. package/packs/sidekick/package.json +4 -1
  20. package/packs/sidekick/policy-factory.ts +38 -0
  21. package/packs/sidekick/tool-impl/channel-tools.ts +99 -0
  22. package/packs/sidekick/tool-impl/memory-tools.ts +86 -1
  23. package/packs/sidekick/tool-impl/routine-tools.ts +156 -2
  24. package/packs/sidekick/tool-impl/storage.ts +6 -5
  25. package/packs/sidekick/tool-impl/task-tools.ts +186 -4
  26. package/packs/software-delivery/.turbo/turbo-build.log +1 -1
  27. package/packs/software-delivery/package.json +1 -1
  28. package/templates/core/.lumenflow/constraints.md.template +68 -3
  29. package/templates/core/LUMENFLOW.md.template +26 -26
  30. package/templates/core/_frameworks/lumenflow/wu-sizing-guide.md.template +3 -5
  31. package/templates/core/ai/onboarding/agent-safety-card.md.template +14 -4
  32. package/templates/core/ai/onboarding/first-wu-mistakes.md.template +96 -0
  33. package/templates/core/ai/onboarding/quick-ref-commands.md.template +50 -39
  34. package/templates/core/ai/onboarding/rapid-prototyping.md +2 -1
  35. package/templates/core/ai/onboarding/starting-prompt.md.template +5 -4
  36. package/templates/core/ai/onboarding/troubleshooting-wu-done.md.template +69 -1
@@ -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.21.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.21.0",
4
4
  "description": "Software delivery pack for LumenFlow — work units, gates, lanes, initiatives, and agent coordination",
5
5
  "keywords": [
6
6
  "lumenflow",
@@ -3,11 +3,11 @@
3
3
  **Version:** 1.3
4
4
  **Last updated:** {{DATE}}
5
5
 
6
- This document contains the 9 non-negotiable constraints that every agent must keep "in working memory" from first plan through `wu:done`.
6
+ This document contains the 11 non-negotiable constraints that every agent must keep "in working memory" from first plan through `wu:done`.
7
7
 
8
8
  ---
9
9
 
10
- ## The 9 Non-Negotiable Constraints
10
+ ## The 11 Non-Negotiable Constraints
11
11
 
12
12
  ### 1. Worktree Discipline and Git Safety
13
13
 
@@ -175,6 +175,16 @@ If you see something broken on main (failing gates, format issues, typos, lint e
175
175
 
176
176
  **Why:** Lane boundaries define ownership, WIP limits, and parallel execution safety. Misassigned WUs undermine these guarantees and create coordination confusion.
177
177
 
178
+ **Occupied-lane workaround:**
179
+
180
+ If the ideal lane is occupied by another WU (WIP limit reached), do NOT wait, force the lock, or try to modify the other WU. Instead:
181
+
182
+ 1. Use a nearby lane whose `code_paths` overlap with the files you need to change
183
+ 2. Or ask the user which lane to use
184
+ 3. Or wait for the lane to free up if the user prefers
185
+
186
+ Never touch, release, or complete another agent's WU to free a lane.
187
+
178
188
  ---
179
189
 
180
190
  ### 9. YAML Files Must Be Modified via CLI Tooling Only (WU-1907)
@@ -205,6 +215,61 @@ If you see something broken on main (failing gates, format issues, typos, lint e
205
215
 
206
216
  ---
207
217
 
218
+ ### 10. Stop and Escalate After 3 Failures
219
+
220
+ **Rule:** If the same operation fails 3 times, STOP. Do not retry. Escalate to the user with a summary of what failed and why.
221
+
222
+ **Applies to:**
223
+
224
+ - CLI commands (`wu:create`, `wu:claim`, `wu:done`, `gates`, etc.)
225
+ - Git operations (push, merge, rebase)
226
+ - Build/install operations
227
+ - Any repeated error, including validation failures
228
+
229
+ **What "same operation" means:**
230
+
231
+ Retrying `wu:create` with slightly different flags after each validation error counts as the same operation. Six attempts with six different missing-field errors is still one operation failing six times. The fix is to run `--help` and read the requirements, not to guess-and-retry.
232
+
233
+ **Escalation format:**
234
+
235
+ ```
236
+ I've attempted [operation] 3 times and it's still failing.
237
+
238
+ Attempts:
239
+ 1. [what I tried] → [error]
240
+ 2. [what I tried] → [error]
241
+ 3. [what I tried] → [error]
242
+
243
+ Root cause: [my best diagnosis]
244
+ Options: [what I think the fix is, or "I don't know"]
245
+ ```
246
+
247
+ **Why:** Retry loops waste user time and context window. Three failures usually means the agent has a wrong mental model of the problem. Escalating early lets the user correct course before the agent creates additional mess (orphaned commits, diverged branches, locked lanes) that requires manual cleanup.
248
+
249
+ ---
250
+
251
+ ### 11. Client-Facing Content Accuracy
252
+
253
+ **Rule:** When writing or updating content shown to external stakeholders (client portals, proposals, meeting materials), do not treat internal docs as ground truth for factual claims. Flag specific claims to the user for verification.
254
+
255
+ **Claims that require user verification:**
256
+
257
+ - Headcount, team size, or organisational structure
258
+ - Revenue, AUM, or financial figures
259
+ - Product status (shipped vs planned vs architectural)
260
+ - Customer-specific details (tech stack, timelines, contract terms)
261
+ - Competitive claims ("no other platform can...")
262
+
263
+ **Enforcement:**
264
+
265
+ - Before committing client-facing copy with specific factual claims, present a summary of claims to the user: "I'm about to commit these facts — please confirm they're current"
266
+ - If internal docs contradict each other, flag the contradiction rather than picking one
267
+ - Never cite a public URL or external source to "verify" an internal claim without telling the user you did so — the external source may also be stale
268
+
269
+ **Why:** Internal docs go stale. An agent confidently writing "four-person engineering team" into a client portal when the team is actually 12+ undermines trust in both the portal and the agent. The user always knows what's current; the docs often don't.
270
+
271
+ ---
272
+
208
273
  ## Mini Audit Checklist
209
274
 
210
275
  Before running `wu:done`, verify:
@@ -296,7 +361,7 @@ Context compaction causes agents to lose critical rules. See [wu-sizing-guide.md
296
361
 
297
362
  Stop and ask a human when:
298
363
 
299
- - Same error repeats 3 times
364
+ - Same error repeats 3 times (see Constraint 10 — this is non-negotiable, not advisory)
300
365
  - Auth or permissions changes required
301
366
  - PII/safety issues discovered
302
367
  - Cloud spend or secrets involved