@lumpcode/recipes 0.0.12 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,10 +8,26 @@ Private monorepo workspace for now (same rollout path as `@lumpcode/cli-utils`).
8
8
 
9
9
  | Recipe | Export | Use when |
10
10
  |--------|--------|----------|
11
- | **backlog** | `backlog` | Generic YAML backlog with a typed stage map and per-item stage resolution |
11
+ | **backlog** | `backlog` | Generic folder backlog with a typed stage map and per-item stage resolution |
12
12
  | **featureBacklog** | `featureBacklog` | Feature items with PRD → test plan → test implementation → implementation |
13
13
  | **abstractionFinder** | `abstractionFinder` | Ephemeral contexts that scan for duplicated CLI utils and append one backlog item + PRD per run |
14
- | **abstractionBacklog** | `abstractionBacklog` | YAML backlog items with PRDs — implement abstraction with verify-until-green, then move item to DONE |
14
+ | **abstractionBacklog** | `abstractionBacklog` | Folder backlog items with PRDs — implement abstraction with verify-until-green, then move item to completed/ |
15
+
16
+ ## Backlog layout
17
+
18
+ Each lump stores backlog items under `backlogItems/`:
19
+
20
+ ```
21
+ .lumpcode/lumps/<lump>/backlogItems/
22
+ todo/<name>/desc.yml
23
+ todo/<name>/prd.md # optional until makePrd / finder writes it
24
+ todo/<name>/testPlan.md # featureBacklog only; optional until makeTestPlan
25
+ completed/<name>/desc.yml # includes completedAt after move-to-done
26
+ completed/<name>/prd.md # moves with the folder
27
+ completed/<name>/testPlan.md
28
+ ```
29
+
30
+ `desc.yml` is a single YAML object with `name`, `task`, `priority`, optional `dependsOn`, and recipe-specific fields (e.g. `manualPrd` for featureBacklog).
15
31
 
16
32
  ## Kit
17
33
 
@@ -21,8 +37,9 @@ Flat helpers under `src/kit/` (re-exported from the package root):
21
37
  - `getRecursiveSteps` — agent step(s) + validation command, retry until pass
22
38
  - `retryUntilGreen` — opinionated wrapper over `getRecursiveSteps` with default fix prompt
23
39
  - `ephemeralContextListFn` — N fresh synthetic contexts per run (`contextCount`, index-aware names)
24
- - `ymlBacklogContexts` — `getContextListFn` from a YAML backlog file with optional per-item parsing
25
- - `setTaskDoneStep` — move finished item from BACKLOG to DONE after a context completes
40
+ - `folderBacklogContexts` — `getContextListFn` from `backlogItems/todo/` with optional per-item parsing
41
+ - `folderSetTaskDoneStep` — move finished item folder from `todo/` to `completed/` after a context completes
42
+ - `ymlBacklogContexts` / `setTaskDoneStep` — **deprecated** YAML-list helpers (warn once, still work)
26
43
  - `resolveImplValidateCommand` — string, descriptor, or fn → `ValidationCommandFn`
27
44
  - `shellCommand` — `sh -c` helper for validation commands
28
45
 
@@ -47,7 +64,9 @@ export default backlog({
47
64
  });
48
65
  ```
49
66
 
50
- `resolveItem` returns `{ stage, contextName?, variables?, additionalDependsOnContexts? }` or `{ ignored: true }`. Terminal stages with `completion: 'moveToDone'` append `setTaskDoneStep`.
67
+ `resolveItem` returns `{ stage, contextName?, variables?, additionalDependsOnContexts? }` or `{ ignored: true }`. Terminal stages with `completion: 'moveToDone'` append `folderSetTaskDoneStep`.
68
+
69
+ Context variables injected by `backlog`: `TASK_NAME`, `TASK`, `BACKLOG_ITEMS_DIR`, `BACKLOG_ITEM_DIR`, `BACKLOG_STAGE`.
51
70
 
52
71
  ## Examples
53
72
 
@@ -90,9 +109,7 @@ export default {
90
109
  ...abstractionFinder({
91
110
  maxPendingAbstractions: 5,
92
111
  scanDirectories: ['packages/apps/cli'],
93
- backlogFilePath: '.lumpcode/lumps/abstractionImplementer/BACKLOG.yml',
94
- doneFilePath: '.lumpcode/lumps/abstractionImplementer/DONE.yml',
95
- prdDirPath: '.lumpcode/lumps/abstractionImplementer/prds',
112
+ backlogItemsDir: '.lumpcode/lumps/abstractionImplementer/backlogItems',
96
113
  command: 'cursor',
97
114
  lumpVariables: { model: 'composer-2.5' },
98
115
  discoveryBranch: 'dev',
package/dist/index.cjs CHANGED
@@ -225,22 +225,188 @@ function assertProjectRelativePath(filePath, label) {
225
225
  }
226
226
  function resolveBacklogPaths(configUrl, overrides) {
227
227
  const [lumpPath, lumpName] = lumpPathAndName(configUrl);
228
- const backlogFilePath = overrides?.backlogFilePath ?? path$1.join(lumpPath, 'BACKLOG.yml');
229
- const doneFilePath = overrides?.doneFilePath ?? path$1.join(lumpPath, 'DONE.yml');
230
- assertProjectRelativePath(backlogFilePath, 'backlogFilePath');
231
- assertProjectRelativePath(doneFilePath, 'doneFilePath');
228
+ const backlogItemsDir = overrides?.backlogItemsDir ?? path$1.join(lumpPath, 'backlogItems');
229
+ assertProjectRelativePath(backlogItemsDir, 'backlogItemsDir');
232
230
  return {
233
231
  lumpPath,
234
232
  lumpName,
235
- backlogFilePath,
236
- doneFilePath,
233
+ backlogItemsDir,
237
234
  };
238
235
  }
239
236
 
237
+ const CONTEXT_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
238
+ function assertRecord(value, location) {
239
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
240
+ throw new Error(`Backlog item ${location} must be an object`);
241
+ }
242
+ }
243
+ function assertStringField(record, field, location) {
244
+ const value = record[field];
245
+ if (typeof value !== 'string' || value.trim() === '') {
246
+ throw new Error(`Backlog item ${location} requires non-empty string field "${field}"`);
247
+ }
248
+ }
249
+ function assertNumberField(record, field, location) {
250
+ const value = record[field];
251
+ if (typeof value !== 'number' || Number.isNaN(value)) {
252
+ throw new Error(`Backlog item ${location} requires numeric field "${field}"`);
253
+ }
254
+ }
255
+ /** Validates and normalizes one backlog item; throws on invalid input. */
256
+ function validateBaseBacklogItem(raw, location) {
257
+ assertRecord(raw, location);
258
+ assertStringField(raw, 'name', location);
259
+ assertStringField(raw, 'task', location);
260
+ assertNumberField(raw, 'priority', location);
261
+ if (!CONTEXT_NAME_PATTERN.test(raw.name)) {
262
+ throw new Error(`Backlog item ${location} has invalid name "${raw.name}" (expected ^[a-zA-Z0-9_-]+$)`);
263
+ }
264
+ let dependsOn;
265
+ if (raw.dependsOn !== undefined) {
266
+ if (!Array.isArray(raw.dependsOn)) {
267
+ throw new Error(`Backlog item ${location} field "dependsOn" must be an array`);
268
+ }
269
+ for (const dep of raw.dependsOn) {
270
+ if (typeof dep !== 'string' || dep.trim() === '') {
271
+ throw new Error(`Backlog item ${location} field "dependsOn" must contain non-empty strings`);
272
+ }
273
+ }
274
+ dependsOn = raw.dependsOn;
275
+ }
276
+ return {
277
+ name: raw.name,
278
+ task: raw.task,
279
+ priority: raw.priority,
280
+ dependsOn,
281
+ };
282
+ }
283
+
284
+ async function listTodoFolderNames(todoDir) {
285
+ try {
286
+ const entries = await fs.readdir(todoDir, { withFileTypes: true });
287
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
288
+ }
289
+ catch (error) {
290
+ const err = error;
291
+ if (err.code === 'ENOENT') {
292
+ return [];
293
+ }
294
+ throw error;
295
+ }
296
+ }
297
+ function folderBacklogContexts({ backlogItemsDir, parseItem, parseContext, }) {
298
+ return async () => {
299
+ const todoDir = path$1.join(backlogItemsDir, 'todo');
300
+ const folderNames = await listTodoFolderNames(todoDir);
301
+ const discovered = await Promise.all(folderNames.map(async (folderName) => {
302
+ const descPath = path$1.join(todoDir, folderName, 'desc.yml');
303
+ let rawText;
304
+ try {
305
+ rawText = await fs.readFile(descPath, 'utf-8');
306
+ }
307
+ catch (error) {
308
+ const err = error;
309
+ if (err.code === 'ENOENT') {
310
+ throw new Error(`Backlog item folder "${folderName}" is missing desc.yml at ${descPath}`);
311
+ }
312
+ throw error;
313
+ }
314
+ const raw = jsYaml.load(rawText);
315
+ const baseItem = validateBaseBacklogItem(raw, `in folder "${folderName}"`);
316
+ if (baseItem.name !== folderName) {
317
+ throw new Error(`Backlog item folder "${folderName}" desc.yml name "${baseItem.name}" must match folder name`);
318
+ }
319
+ const item = parseItem ? parseItem(baseItem, folderName, raw) : baseItem;
320
+ return { item, folderName };
321
+ }));
322
+ discovered.sort((a, b) => {
323
+ if (a.item.priority !== b.item.priority) {
324
+ return a.item.priority - b.item.priority;
325
+ }
326
+ return a.item.name.localeCompare(b.item.name);
327
+ });
328
+ const allCtxs = await Promise.all(discovered.map(async ({ item, folderName }) => {
329
+ const { parsed, ignored } = parseContext
330
+ ? await parseContext(item, folderName)
331
+ : { parsed: undefined, ignored: false };
332
+ if (ignored) {
333
+ return null;
334
+ }
335
+ return {
336
+ name: item.name,
337
+ options: {
338
+ priority: item.priority,
339
+ dependsOnContexts: item.dependsOn,
340
+ },
341
+ variables: parsed?.variables ?? {},
342
+ ...parsed,
343
+ };
344
+ }));
345
+ return allCtxs.filter((ctx) => !!ctx);
346
+ };
347
+ }
348
+
349
+ function isPlainObject(value) {
350
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
351
+ }
352
+ /** Moves a finished backlog item folder from todo/ to completed/ after the context completes. */
353
+ const folderSetTaskDoneStep = (input) => {
354
+ const nameVarName = input.nameVarName ?? 'TASK_NAME';
355
+ return {
356
+ async commandFn({ context, workspacePath }) {
357
+ const variables = context.variables;
358
+ const itemsDirRelative = variables[input.itemsDirVarName];
359
+ const taskName = variables[nameVarName];
360
+ if (!itemsDirRelative || !taskName) {
361
+ throw new Error('Backlog items directory and task name are required');
362
+ }
363
+ const itemsDir = path$1.join(workspacePath, itemsDirRelative);
364
+ const fromDir = path$1.join(itemsDir, 'todo', taskName);
365
+ const toDir = path$1.join(itemsDir, 'completed', taskName);
366
+ const descPath = path$1.join(fromDir, 'desc.yml');
367
+ const completedDescPath = path$1.join(toDir, 'desc.yml');
368
+ if (!(await core.pathExists(fromDir))) {
369
+ return null;
370
+ }
371
+ if (await core.pathExists(toDir)) {
372
+ console.warn(`[lumpcode/recipes] Cannot move backlog item "${taskName}": already exists at ${toDir}`);
373
+ return null;
374
+ }
375
+ const rawText = await fs.readFile(descPath, 'utf-8');
376
+ const raw = jsYaml.load(rawText);
377
+ if (!isPlainObject(raw)) {
378
+ throw new Error(`Backlog desc.yml at ${descPath} must contain a YAML object`);
379
+ }
380
+ const updated = {
381
+ ...raw,
382
+ completedAt: new Date().toISOString(),
383
+ };
384
+ await fs.mkdir(path$1.join(itemsDir, 'completed'), { recursive: true });
385
+ await fs.rename(fromDir, toDir);
386
+ await fs.writeFile(completedDescPath, jsYaml.dump(updated));
387
+ return {
388
+ executable: 'cat',
389
+ args: [completedDescPath],
390
+ };
391
+ },
392
+ continueOnError: true,
393
+ };
394
+ };
395
+
396
+ let setTaskDoneDeprecatedWarned = false;
397
+ function warnSetTaskDoneDeprecated() {
398
+ if (setTaskDoneDeprecatedWarned) {
399
+ return;
400
+ }
401
+ setTaskDoneDeprecatedWarned = true;
402
+ console.warn('[lumpcode/recipes] setTaskDoneStep is deprecated; use folderSetTaskDoneStep. ' +
403
+ 'YAML backlog helpers will be removed in a future major version.');
404
+ }
240
405
  /** Moves a finished backlog item from BACKLOG.yml to DONE.yml after the context completes. */
241
406
  const setTaskDoneStep = (input) => {
242
407
  return {
243
408
  async commandFn({ context, workspacePath }) {
409
+ warnSetTaskDoneDeprecated();
244
410
  const variables = context.variables;
245
411
  const { backlogVarName, doneVarName } = input;
246
412
  const baseBacklogFilePath = variables[backlogVarName];
@@ -278,62 +444,25 @@ function resolveImplValidateCommand(implValidateCommand) {
278
444
  return (_input) => implValidateCommand;
279
445
  }
280
446
 
281
- const CONTEXT_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
282
- function assertRecord(value, index) {
283
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
284
- throw new Error(`Backlog item at index ${index} must be an object`);
285
- }
286
- }
287
- function assertStringField(record, field, index) {
288
- const value = record[field];
289
- if (typeof value !== 'string' || value.trim() === '') {
290
- throw new Error(`Backlog item at index ${index} requires non-empty string field "${field}"`);
291
- }
292
- }
293
- function assertNumberField(record, field, index) {
294
- const value = record[field];
295
- if (typeof value !== 'number' || Number.isNaN(value)) {
296
- throw new Error(`Backlog item at index ${index} requires numeric field "${field}"`);
447
+ let ymlBacklogDeprecatedWarned = false;
448
+ function warnYmlBacklogDeprecated() {
449
+ if (ymlBacklogDeprecatedWarned) {
450
+ return;
297
451
  }
452
+ ymlBacklogDeprecatedWarned = true;
453
+ console.warn('[lumpcode/recipes] ymlBacklogContexts is deprecated; use folderBacklogContexts. ' +
454
+ 'YAML backlog helpers will be removed in a future major version.');
298
455
  }
299
- /** Validates and normalizes one YAML backlog row; throws on invalid input. */
300
- function validateBaseBacklogItem(raw, index) {
301
- assertRecord(raw, index);
302
- assertStringField(raw, 'name', index);
303
- assertStringField(raw, 'task', index);
304
- assertNumberField(raw, 'priority', index);
305
- if (!CONTEXT_NAME_PATTERN.test(raw.name)) {
306
- throw new Error(`Backlog item at index ${index} has invalid name "${raw.name}" (expected ^[a-zA-Z0-9_-]+$)`);
307
- }
308
- let dependsOn;
309
- if (raw.dependsOn !== undefined) {
310
- if (!Array.isArray(raw.dependsOn)) {
311
- throw new Error(`Backlog item at index ${index} field "dependsOn" must be an array`);
312
- }
313
- for (const dep of raw.dependsOn) {
314
- if (typeof dep !== 'string' || dep.trim() === '') {
315
- throw new Error(`Backlog item at index ${index} field "dependsOn" must contain non-empty strings`);
316
- }
317
- }
318
- dependsOn = raw.dependsOn;
319
- }
320
- return {
321
- name: raw.name,
322
- task: raw.task,
323
- priority: raw.priority,
324
- dependsOn,
325
- };
326
- }
327
-
328
456
  function ymlBacklogContexts({ backlogFilePath, parseItem, parseContext, }) {
329
457
  return async () => {
458
+ warnYmlBacklogDeprecated();
330
459
  const raw = await fs.readFile(backlogFilePath, 'utf-8');
331
460
  const doc = jsYaml.load(raw);
332
461
  if (!Array.isArray(doc)) {
333
462
  throw new Error(`Backlog file ${backlogFilePath} must contain a YAML list`);
334
463
  }
335
464
  const allCtxs = await Promise.all(doc.map(async (rawItem, index) => {
336
- const baseItem = validateBaseBacklogItem(rawItem, index);
465
+ const baseItem = validateBaseBacklogItem(rawItem, `at index ${index}`);
337
466
  const item = parseItem ? parseItem(baseItem, index, rawItem) : baseItem;
338
467
  const { parsed, ignored } = parseContext
339
468
  ? await parseContext(item, index)
@@ -361,8 +490,8 @@ function defineRecipe(recipe) {
361
490
  const BACKLOG_STAGE_VAR = 'BACKLOG_STAGE';
362
491
  const BACKLOG_TASK_NAME_VAR = 'TASK_NAME';
363
492
  const BACKLOG_TASK_VAR = 'TASK';
364
- const BACKLOG_FILE_VAR = 'BACKLOG_FILE';
365
- const BACKLOG_DONE_FILE_VAR = 'DONE_FILE';
493
+ const BACKLOG_ITEMS_DIR_VAR = 'BACKLOG_ITEMS_DIR';
494
+ const BACKLOG_ITEM_DIR_VAR = 'BACKLOG_ITEM_DIR';
366
495
 
367
496
  function isIgnoredResolution(resolution) {
368
497
  return 'ignored' in resolution && resolution.ignored === true;
@@ -379,27 +508,25 @@ function buildStageSteps(stages, stageName) {
379
508
  if (stageDef.completion === 'moveToDone') {
380
509
  return [
381
510
  ...normalized,
382
- setTaskDoneStep({
383
- backlogVarName: BACKLOG_FILE_VAR,
384
- doneVarName: BACKLOG_DONE_FILE_VAR,
511
+ folderSetTaskDoneStep({
512
+ itemsDirVarName: BACKLOG_ITEMS_DIR_VAR,
385
513
  }),
386
514
  ];
387
515
  }
388
516
  return normalized;
389
517
  }
390
518
  function backlog(options) {
391
- const { configUrl, backlogFilePath: backlogFilePathOverride, doneFilePath: doneFilePathOverride, stages, parseItem, resolveItem, ...rest } = options;
519
+ const { configUrl, backlogItemsDir: backlogItemsDirOverride, stages, parseItem, resolveItem, ...rest } = options;
392
520
  const paths = resolveBacklogPaths(configUrl, {
393
- backlogFilePath: backlogFilePathOverride,
394
- doneFilePath: doneFilePathOverride,
521
+ backlogItemsDir: backlogItemsDirOverride,
395
522
  });
396
523
  const projectRoot = projectRootFromConfigUrl(configUrl);
397
- const absoluteBacklogPath = path$1.join(projectRoot, paths.backlogFilePath);
524
+ const absoluteBacklogItemsDir = path$1.join(projectRoot, paths.backlogItemsDir);
398
525
  return cliUtils.defineConfig({
399
- getContextListFn: ymlBacklogContexts({
400
- backlogFilePath: absoluteBacklogPath,
526
+ getContextListFn: folderBacklogContexts({
527
+ backlogItemsDir: absoluteBacklogItemsDir,
401
528
  parseItem,
402
- async parseContext(item) {
529
+ async parseContext(item, folderName) {
403
530
  const resolution = await resolveItem({ item, paths });
404
531
  if (isIgnoredResolution(resolution)) {
405
532
  return { ignored: true };
@@ -409,14 +536,15 @@ function backlog(options) {
409
536
  ...(item.dependsOn ?? []),
410
537
  ...(additionalDependsOnContexts ?? []),
411
538
  ];
539
+ const backlogItemDir = path$1.join(paths.backlogItemsDir, 'todo', folderName);
412
540
  return {
413
541
  parsed: {
414
542
  name: contextName ?? item.name,
415
543
  variables: {
416
544
  [BACKLOG_TASK_NAME_VAR]: item.name,
417
545
  [BACKLOG_TASK_VAR]: item.task,
418
- [BACKLOG_FILE_VAR]: paths.backlogFilePath,
419
- [BACKLOG_DONE_FILE_VAR]: paths.doneFilePath,
546
+ [BACKLOG_ITEMS_DIR_VAR]: paths.backlogItemsDir,
547
+ [BACKLOG_ITEM_DIR_VAR]: backlogItemDir,
420
548
  [BACKLOG_STAGE_VAR]: stage,
421
549
  ...variables,
422
550
  },
@@ -443,18 +571,14 @@ function backlog(options) {
443
571
  }
444
572
  const backlogRecipe = defineRecipe((options) => backlog(options));
445
573
 
446
- const DEFAULT_IMPL_VALIDATE_COMMAND = [
447
- 'npm run build -w=@lumpcode/cli',
448
- 'npm run test -w=@lumpcode/cli',
449
- ].join(' && ');
450
574
  const abstractionBacklog = defineRecipe((options) => {
451
- const { implValidateCommand = DEFAULT_IMPL_VALIDATE_COMMAND, configUrl, ...rest } = options;
575
+ const { implValidateCommand, configUrl, implSteps, ...rest } = options;
452
576
  const projectRoot = projectRootFromConfigUrl(configUrl);
453
- const runImplValidation = resolveImplValidateCommand(implValidateCommand);
577
+ const runImplValidation = resolveImplValidateCommand(implValidateCommand ?? 'echo "No implementation validation command provided. I say, trust but verify, but well..."');
454
578
  return backlog({
455
579
  configUrl,
456
580
  async resolveItem({ item, paths }) {
457
- const itemPrdPath = path$1.join(paths.lumpPath, 'prds', `${item.name}.prd.md`);
581
+ const itemPrdPath = path$1.join(paths.backlogItemsDir, 'todo', item.name, 'prd.md');
458
582
  const hasPrd = await core.pathExists(path$1.join(projectRoot, itemPrdPath));
459
583
  if (!hasPrd) {
460
584
  return { ignored: true };
@@ -470,7 +594,7 @@ const abstractionBacklog = defineRecipe((options) => {
470
594
  implementation: {
471
595
  completion: 'moveToDone',
472
596
  steps: retryUntilGreen({
473
- steps: [{
597
+ steps: implSteps ?? [{
474
598
  promptFn({ context: ctx }) {
475
599
  const vars = ctx.variables;
476
600
  const { PRD_FILE, TASK_NAME, TASK } = vars;
@@ -482,10 +606,10 @@ const abstractionBacklog = defineRecipe((options) => {
482
606
  ${TASK}
483
607
 
484
608
  Requirements:
485
- - Materialize the abstraction as a new util under packages/apps/cli/src/utils/<utilName>/ following existing conventions: main.ts (implementation), index.ts (re-export), unit.test.ts (unit tests), and a barrel export from packages/apps/cli/src/utils/index.ts.
486
- - Refactor all call sites in packages/apps/cli to import the new util.
609
+ - Materialize the abstraction as a new util following existing conventions in the codebase.
610
+ - Refactor all call sites to import the new util.
487
611
  - Net line count must go down after the refactor (removed duplication minus new util code, excluding the new unit test file). Do not extract one-off logic or move code without deleting repetition.
488
- - Include unit tests in unit.test.ts (match sibling utils in packages/apps/cli/src/utils/).
612
+ - Include unit tests for the new util.
489
613
  `.trim();
490
614
  },
491
615
  }],
@@ -498,43 +622,44 @@ const abstractionBacklog = defineRecipe((options) => {
498
622
  });
499
623
 
500
624
  const abstractionFinder = defineRecipe((options) => {
501
- const { maxPendingAbstractions = 5, scanDirectories, customPrompt, backlogFilePath, prdDirPath, doneFilePath, } = options;
625
+ const { maxPendingAbstractions = 5, scanDirectories, customPrompt, backlogItemsDir, } = options; // TODO : check if pending abstcations in backlog are less than maxPendingAbstractions
502
626
  return cliUtils.defineConfig({
503
627
  ...options,
504
628
  getContextListFn: ephemeralContextListFn({
505
629
  contextCount: maxPendingAbstractions,
506
630
  variables: {
507
- PRD_DIR_PATH: prdDirPath ?? '',
508
- BACKLOG_FILE_PATH: backlogFilePath ?? '',
509
- DONE_FILE_PATH: doneFilePath ?? '',
631
+ BACKLOG_ITEMS_DIR: backlogItemsDir,
510
632
  },
511
633
  }),
512
- steps: buildFinderPrompt({ prdDirPath, backlogFilePath, doneFilePath, scanDirectories, customPrompt }),
634
+ steps: buildFinderPrompt({ backlogItemsDir, scanDirectories, customPrompt }),
513
635
  });
514
636
  });
515
- function buildFinderPrompt({ prdDirPath, backlogFilePath, doneFilePath, scanDirectories, customPrompt, }) {
637
+ function buildFinderPrompt({ backlogItemsDir, scanDirectories, customPrompt, }) {
516
638
  return customPrompt ? customPrompt() : `
517
639
  Scan ${(scanDirectories || []).map((dir) => `@${dir}`).join(' and ') || 'the codebase'} for duplicated logic that appears in multiple places (same pattern, not merely similar file structure).
518
640
 
519
- Read @${backlogFilePath} and @${doneFilePath}. Do not propose abstractions whose util name already appears in either file.
641
+ List existing backlog item names under @${backlogItemsDir}/todo/ and @${backlogItemsDir}/completed/. Do not propose abstractions whose util name already appears in either directory.
520
642
 
521
643
  Pick exactly one new abstraction that:
522
644
  - Has a clear util name matching ^[a-zA-Z0-9_-]+$ that describes the pattern it captures.
523
645
  - Would materialize as a new util under packages/apps/cli/src/utils/<utilName>/.
524
646
  - Would shrink the codebase: refactoring all call sites in packages/apps/cli should reduce net line count (excluding new unit tests).
525
647
 
526
- Add exactly one entry to @${backlogFilePath} with:
527
- - task: a concise summary of the repeated pattern, proposed util name, and affected areas
528
- - priority: max existing priority in BACKLOG.yml plus 1 (or 1 if the backlog is empty)
529
- - dependsOn: optional list of util names from BACKLOG.yml or DONE.yml that must land first (only when clearly needed)
530
-
531
- Write an implementation-ready PRD to @${prdDirPath}/<utilName>.prd.md for the same util name. The PRD should be self-contained and include:
532
- - Problem statement and repeated pattern
533
- - Goals and non-goals
534
- - Proposed util API and affected files
535
- - Acceptance criteria (including net line reduction and unit tests)
536
-
537
- Do not implement code. Only edit @${backlogFilePath} (append one item) and create the PRD file.
648
+ Create exactly one new backlog item folder at @${backlogItemsDir}/todo/<utilName>/ with:
649
+ - desc.yml containing:
650
+ - name: <utilName> (must match folder name)
651
+ - task: a concise summary of the repeated pattern, proposed util name, and affected areas
652
+ - priority: max existing priority in todo/ plus 1 (or 1 if todo/ is empty)
653
+ - dependsOn: optional list of util names from todo/ or completed/ that must land first (only when clearly needed)
654
+ - prd.md: an implementation-ready PRD for the same util name. The PRD should be self-contained and include:
655
+ - Problem statement and repeated pattern
656
+ - Goals and non-goals
657
+ - Proposed util API and affected files
658
+ - Acceptance criteria (including net line reduction and unit tests)
659
+
660
+ Do not implement code. Only create @${backlogItemsDir}/todo/<utilName>/desc.yml and @${backlogItemsDir}/todo/<utilName>/prd.md.
661
+
662
+ Do not take too much time looking for every possible abstraction. Once you found a good abstraction, stop and create the backlog item.
538
663
  `.trim();
539
664
  }
540
665
 
@@ -563,8 +688,8 @@ function featureContextName(itemName, stage) {
563
688
  }
564
689
  }
565
690
  async function resolveFeatureBacklogItem(item, paths, projectRoot, lumpName, baseBranch) {
566
- const prdFilePath = path$1.join(paths.lumpPath, 'prds', `${item.name}.prd.md`);
567
- const testPlanFilePath = path$1.join(paths.lumpPath, 'testPlans', `${item.name}.test.md`);
691
+ const prdFilePath = path$1.join(paths.backlogItemsDir, 'todo', item.name, 'prd.md');
692
+ const testPlanFilePath = path$1.join(paths.backlogItemsDir, 'todo', item.name, 'testPlan.md');
568
693
  const hasPrd = await core.pathExists(path$1.join(projectRoot, prdFilePath));
569
694
  if (!hasPrd) {
570
695
  if (item.manualPrd === true) {
@@ -617,15 +742,14 @@ async function resolveFeatureBacklogItem(item, paths, projectRoot, lumpName, bas
617
742
  };
618
743
  }
619
744
  const featureBacklog = defineRecipe((options) => {
620
- const { configUrl, baseBranch, implValidateCommand, backlogFilePath, doneFilePath, ...rest } = options;
745
+ const { configUrl, baseBranch, implValidateCommand, backlogItemsDir, ...rest } = options;
621
746
  const projectRoot = projectRootFromConfigUrl(configUrl);
622
- const runImplValidation = resolveImplValidateCommand(implValidateCommand);
747
+ const runImplValidation = resolveImplValidateCommand(implValidateCommand ?? 'echo "No implementation validation command provided. I say, trust but verify, but well..."');
623
748
  return backlog({
624
749
  configUrl,
625
- backlogFilePath,
626
- doneFilePath,
750
+ backlogItemsDir,
627
751
  baseBranch,
628
- parseItem(baseItem, _index, raw) {
752
+ parseItem(baseItem, _folderName, raw) {
629
753
  assertValidFeatureItemName(baseItem.name);
630
754
  const record = raw;
631
755
  if (record.manualPrd !== undefined && typeof record.manualPrd !== 'boolean') {
@@ -646,16 +770,16 @@ const featureBacklog = defineRecipe((options) => {
646
770
  {
647
771
  promptFn({ context: ctx }) {
648
772
  const vars = ctx.variables;
649
- const { BACKLOG_FILE, TASK_NAME, TASK, PRD_FILE } = vars;
773
+ const { BACKLOG_ITEM_DIR, TASK_NAME, TASK, PRD_FILE } = vars;
650
774
  return `
651
- Write a product requirements document (PRD) for the following Lumpcode backlog item from @${BACKLOG_FILE}.
775
+ Write a product requirements document (PRD) for the following backlog item from @${BACKLOG_ITEM_DIR}/desc.yml.
652
776
 
653
777
  Task name: ${TASK_NAME}
654
778
 
655
779
  Task:
656
780
  ${TASK}
657
781
 
658
- Save the PRD to @${PRD_FILE}. Do not edit @${BACKLOG_FILE}.
782
+ Save the PRD to @${PRD_FILE}. Do not edit @${BACKLOG_ITEM_DIR}/desc.yml.
659
783
 
660
784
  The PRD should be self-contained and implementation-ready. Include:
661
785
  - Problem statement and motivation
@@ -680,9 +804,9 @@ The PRD should not contain any testing strategy details.
680
804
  {
681
805
  promptFn({ context: ctx }) {
682
806
  const vars = ctx.variables;
683
- const { BACKLOG_FILE, TASK_NAME, TASK, PRD_FILE, TEST_PLAN_FILE } = vars;
807
+ const { BACKLOG_ITEM_DIR, TASK_NAME, TASK, PRD_FILE, TEST_PLAN_FILE } = vars;
684
808
  return `
685
- Write a test plan for the following Lumpcode backlog item from @${BACKLOG_FILE}.
809
+ Write a test plan for the following backlog item from @${BACKLOG_ITEM_DIR}/desc.yml.
686
810
 
687
811
  Task name: ${TASK_NAME}
688
812
  Task:
@@ -690,7 +814,7 @@ ${TASK}
690
814
 
691
815
  The PRD for this task is @${PRD_FILE}. The test plan should match the requirements of the PRD.
692
816
 
693
- Save the test plan to @${TEST_PLAN_FILE}. Do not edit @${BACKLOG_FILE} nor @${PRD_FILE}.
817
+ Save the test plan to @${TEST_PLAN_FILE}. Do not edit @${BACKLOG_ITEM_DIR}/desc.yml nor @${PRD_FILE}.
694
818
 
695
819
  The test plan should be self-contained and implementation-ready. Include:
696
820
  - Test cases
@@ -709,9 +833,9 @@ The test plan should be self-contained and implementation-ready. Include:
709
833
  {
710
834
  promptFn({ context: ctx }) {
711
835
  const vars = ctx.variables;
712
- const { BACKLOG_FILE, TASK_NAME, TASK, PRD_FILE, TEST_PLAN_FILE } = vars;
836
+ const { BACKLOG_ITEM_DIR, TASK_NAME, TASK, PRD_FILE, TEST_PLAN_FILE } = vars;
713
837
  return `
714
- Write a test implementation for the following Lumpcode backlog item from @${BACKLOG_FILE}.
838
+ Write a test implementation for the following backlog item from @${BACKLOG_ITEM_DIR}/desc.yml.
715
839
 
716
840
  Task name: ${TASK_NAME}
717
841
  Task:
@@ -748,8 +872,8 @@ The implementation should make the tests pass. Do not edit any test file.
748
872
  });
749
873
  });
750
874
 
751
- exports.BACKLOG_DONE_FILE_VAR = BACKLOG_DONE_FILE_VAR;
752
- exports.BACKLOG_FILE_VAR = BACKLOG_FILE_VAR;
875
+ exports.BACKLOG_ITEMS_DIR_VAR = BACKLOG_ITEMS_DIR_VAR;
876
+ exports.BACKLOG_ITEM_DIR_VAR = BACKLOG_ITEM_DIR_VAR;
753
877
  exports.BACKLOG_STAGE_VAR = BACKLOG_STAGE_VAR;
754
878
  exports.BACKLOG_TASK_NAME_VAR = BACKLOG_TASK_NAME_VAR;
755
879
  exports.BACKLOG_TASK_VAR = BACKLOG_TASK_VAR;
@@ -760,6 +884,8 @@ exports.backlogRecipe = backlogRecipe;
760
884
  exports.defineRecipe = defineRecipe;
761
885
  exports.ephemeralContextListFn = ephemeralContextListFn;
762
886
  exports.featureBacklog = featureBacklog;
887
+ exports.folderBacklogContexts = folderBacklogContexts;
888
+ exports.folderSetTaskDoneStep = folderSetTaskDoneStep;
763
889
  exports.getRecursiveSteps = getRecursiveSteps;
764
890
  exports.lumpPathAndName = lumpPathAndName;
765
891
  exports.normalizeMaybePromGetter = normalizeMaybePromGetter;
package/dist/index.d.ts CHANGED
@@ -71,25 +71,14 @@ declare function projectRootFromConfigUrl(configUrl: string | URL): string;
71
71
  /** Fails the context when the artifact referenced by a context variable was not created. */
72
72
  declare function requireArtifactStep(artifactPathVarName: string): Step;
73
73
 
74
- type BacklogPaths$1 = {
74
+ type BacklogPaths = {
75
75
  lumpPath: string;
76
76
  lumpName: string;
77
- backlogFilePath: string;
78
- doneFilePath: string;
77
+ backlogItemsDir: string;
79
78
  };
80
79
  declare function resolveBacklogPaths(configUrl: string | URL, overrides?: {
81
- backlogFilePath?: string;
82
- doneFilePath?: string;
83
- }): BacklogPaths$1;
84
-
85
- /** Moves a finished backlog item from BACKLOG.yml to DONE.yml after the context completes. */
86
- declare const setTaskDoneStep: (input: {
87
- backlogVarName: string;
88
- doneVarName: string;
89
- }) => Step;
90
-
91
- type ImplValidateCommand = string | CommandDescriptor | ValidationCommandFn;
92
- declare function resolveImplValidateCommand(implValidateCommand: ImplValidateCommand): ValidationCommandFn;
80
+ backlogItemsDir?: string;
81
+ }): BacklogPaths;
93
82
 
94
83
  type Recipe<Options, V extends LumpVariables = LumpVariables> = (options: Options) => LumpJsConfig<V>;
95
84
  declare function defineRecipe<Options, V extends LumpVariables = LumpVariables>(recipe: Recipe<Options, V>): Recipe<Options, V>;
@@ -104,6 +93,31 @@ type DoneBacklogItem<T extends BaseBacklogItem = BaseBacklogItem> = T & {
104
93
  completedAt: string;
105
94
  };
106
95
 
96
+ type FolderBacklogContextsOptions<Item extends BaseBacklogItem = BaseBacklogItem> = {
97
+ backlogItemsDir: string;
98
+ parseItem?: (item: BaseBacklogItem, folderName: string, raw: unknown) => Item;
99
+ parseContext?: (item: Item, folderName: string) => MaybePromise$1<{
100
+ parsed?: Partial<Context>;
101
+ ignored?: boolean;
102
+ }>;
103
+ };
104
+ declare function folderBacklogContexts<Item extends BaseBacklogItem = BaseBacklogItem>({ backlogItemsDir, parseItem, parseContext, }: FolderBacklogContextsOptions<Item>): GetContextListFn;
105
+
106
+ /** Moves a finished backlog item folder from todo/ to completed/ after the context completes. */
107
+ declare const folderSetTaskDoneStep: (input: {
108
+ itemsDirVarName: string;
109
+ nameVarName?: string;
110
+ }) => Step;
111
+
112
+ /** Moves a finished backlog item from BACKLOG.yml to DONE.yml after the context completes. */
113
+ declare const setTaskDoneStep: (input: {
114
+ backlogVarName: string;
115
+ doneVarName: string;
116
+ }) => Step;
117
+
118
+ type ImplValidateCommand = string | CommandDescriptor | ValidationCommandFn;
119
+ declare function resolveImplValidateCommand(implValidateCommand: ImplValidateCommand): ValidationCommandFn;
120
+
107
121
  type YmlBacklogContextsOptions<Item extends BaseBacklogItem = BaseBacklogItem> = {
108
122
  backlogFilePath: string;
109
123
  parseItem?: (item: BaseBacklogItem, index: number, raw: unknown) => Item;
@@ -114,23 +128,22 @@ type YmlBacklogContextsOptions<Item extends BaseBacklogItem = BaseBacklogItem> =
114
128
  };
115
129
  declare function ymlBacklogContexts<Item extends BaseBacklogItem = BaseBacklogItem>({ backlogFilePath, parseItem, parseContext, }: YmlBacklogContextsOptions<Item>): GetContextListFn;
116
130
 
117
- /** Validates and normalizes one YAML backlog row; throws on invalid input. */
118
- declare function validateBaseBacklogItem(raw: unknown, index: number): BaseBacklogItem;
131
+ /** Validates and normalizes one backlog item; throws on invalid input. */
132
+ declare function validateBaseBacklogItem(raw: unknown, location: string): BaseBacklogItem;
119
133
 
120
134
  type AbstractionBacklogOptions = {
121
135
  implValidateCommand?: ValidationCommandFn | string;
122
136
  /** Lump config module URL — pass `import.meta.url` from `config.ts`. */
123
137
  configUrl: string | URL;
138
+ implSteps?: LumpJsConfig['steps'];
124
139
  } & Omit<LumpJsConfig, 'contextListJson' | 'contextMatchFn' | 'getContextListFn' | 'prompt' | 'steps'>;
125
140
  declare const abstractionBacklog: Recipe<AbstractionBacklogOptions>;
126
141
 
127
142
  type AbstractionFinderOptions = {
128
- scanDirectories: string[];
143
+ scanDirectories?: string[];
129
144
  customPrompt?(): string;
130
145
  maxPendingAbstractions?: number;
131
- backlogFilePath: string;
132
- doneFilePath?: string;
133
- prdDirPath?: string;
146
+ backlogItemsDir: string;
134
147
  } & LumpJsConfig;
135
148
  declare const abstractionFinder: Recipe<AbstractionFinderOptions>;
136
149
 
@@ -138,12 +151,6 @@ type BacklogStageDefinition = {
138
151
  steps: NonNullable<LumpJsConfig['steps']>;
139
152
  completion: 'keepPending' | 'moveToDone';
140
153
  };
141
- type BacklogPaths = {
142
- lumpPath: string;
143
- lumpName: string;
144
- backlogFilePath: string;
145
- doneFilePath: string;
146
- };
147
154
  type BacklogItemResolution<StageName extends string> = {
148
155
  ignored: true;
149
156
  } | {
@@ -154,10 +161,9 @@ type BacklogItemResolution<StageName extends string> = {
154
161
  };
155
162
  type BacklogOptions<Item extends BaseBacklogItem, Stages extends Record<string, BacklogStageDefinition>> = {
156
163
  configUrl: string | URL;
157
- backlogFilePath?: string;
158
- doneFilePath?: string;
164
+ backlogItemsDir?: string;
159
165
  stages: Stages;
160
- parseItem?: (item: BaseBacklogItem, index: number, raw: unknown) => Item;
166
+ parseItem?: (item: BaseBacklogItem, folderName: string, raw: unknown) => Item;
161
167
  resolveItem(input: {
162
168
  item: Item;
163
169
  paths: BacklogPaths;
@@ -166,8 +172,8 @@ type BacklogOptions<Item extends BaseBacklogItem, Stages extends Record<string,
166
172
  declare const BACKLOG_STAGE_VAR = "BACKLOG_STAGE";
167
173
  declare const BACKLOG_TASK_NAME_VAR = "TASK_NAME";
168
174
  declare const BACKLOG_TASK_VAR = "TASK";
169
- declare const BACKLOG_FILE_VAR = "BACKLOG_FILE";
170
- declare const BACKLOG_DONE_FILE_VAR = "DONE_FILE";
175
+ declare const BACKLOG_ITEMS_DIR_VAR = "BACKLOG_ITEMS_DIR";
176
+ declare const BACKLOG_ITEM_DIR_VAR = "BACKLOG_ITEM_DIR";
171
177
 
172
178
  declare function backlog<Item extends BaseBacklogItem, Stages extends Record<string, BacklogStageDefinition>>(options: BacklogOptions<Item, Stages>): LumpJsConfig;
173
179
  declare const backlogRecipe: Recipe<BacklogOptions<BaseBacklogItem, Record<string, BacklogStageDefinition>>>;
@@ -179,8 +185,8 @@ type FeatureBacklogStage = 'makePrd' | 'makeTestPlan' | 'testImpl' | 'implementa
179
185
  type FeatureBacklogContextVariables = {
180
186
  TASK_NAME: string;
181
187
  TASK: string;
182
- BACKLOG_FILE: string;
183
- DONE_FILE: string;
188
+ BACKLOG_ITEMS_DIR: string;
189
+ BACKLOG_ITEM_DIR: string;
184
190
  BACKLOG_STAGE: FeatureBacklogStage;
185
191
  PRD_FILE?: string;
186
192
  TEST_PLAN_FILE?: string;
@@ -188,12 +194,11 @@ type FeatureBacklogContextVariables = {
188
194
  type FeatureBacklogOptions = {
189
195
  configUrl: string | URL;
190
196
  baseBranch: string;
191
- implValidateCommand: ValidationCommandFn | string;
192
- backlogFilePath?: string;
193
- doneFilePath?: string;
197
+ implValidateCommand?: ValidationCommandFn | string;
198
+ backlogItemsDir?: string;
194
199
  } & Omit<LumpJsConfig, 'contextListJson' | 'contextMatchFn' | 'getContextListFn' | 'prompt' | 'steps' | 'baseBranch'>;
195
- declare function resolveFeatureBacklogItem(item: FeatureBacklogItem, paths: BacklogPaths$1, projectRoot: string, lumpName: string, baseBranch: string): Promise<BacklogItemResolution<FeatureBacklogStage>>;
200
+ declare function resolveFeatureBacklogItem(item: FeatureBacklogItem, paths: BacklogPaths, projectRoot: string, lumpName: string, baseBranch: string): Promise<BacklogItemResolution<FeatureBacklogStage>>;
196
201
  declare const featureBacklog: Recipe<FeatureBacklogOptions>;
197
202
 
198
- export { BACKLOG_DONE_FILE_VAR, BACKLOG_FILE_VAR, BACKLOG_STAGE_VAR, BACKLOG_TASK_NAME_VAR, BACKLOG_TASK_VAR, abstractionBacklog, abstractionFinder, backlog, backlogRecipe, defineRecipe, ephemeralContextListFn, featureBacklog, getRecursiveSteps, lumpPathAndName, normalizeMaybePromGetter, projectRootFromConfigUrl, requireArtifactStep, resolveBacklogPaths, resolveFeatureBacklogItem, resolveImplValidateCommand, retryUntilGreen, setTaskDoneStep, shellCommand, validateBaseBacklogItem, ymlBacklogContexts };
199
- export type { AbstractionBacklogOptions, AbstractionFinderOptions, BacklogItemResolution, BacklogOptions, BacklogPaths$1 as BacklogPaths, BacklogStageDefinition, BaseBacklogItem, DoneBacklogItem, EphemeralContextListFnOptions, FeatureBacklogContextVariables, FeatureBacklogItem, FeatureBacklogOptions, FeatureBacklogStage, GetFirstStepsInput, GetRecursiveStepsOptions, ImplValidateCommand, IsValidationCommandResultOkInput, MaybePromGetter, Recipe, RetryUntilGreenInput, StepIndex, ValidationCommandFn, ValidationCommandFnInput, YmlBacklogContextsOptions };
203
+ export { BACKLOG_ITEMS_DIR_VAR, BACKLOG_ITEM_DIR_VAR, BACKLOG_STAGE_VAR, BACKLOG_TASK_NAME_VAR, BACKLOG_TASK_VAR, abstractionBacklog, abstractionFinder, backlog, backlogRecipe, defineRecipe, ephemeralContextListFn, featureBacklog, folderBacklogContexts, folderSetTaskDoneStep, getRecursiveSteps, lumpPathAndName, normalizeMaybePromGetter, projectRootFromConfigUrl, requireArtifactStep, resolveBacklogPaths, resolveFeatureBacklogItem, resolveImplValidateCommand, retryUntilGreen, setTaskDoneStep, shellCommand, validateBaseBacklogItem, ymlBacklogContexts };
204
+ export type { AbstractionBacklogOptions, AbstractionFinderOptions, BacklogItemResolution, BacklogOptions, BacklogPaths, BacklogStageDefinition, BaseBacklogItem, DoneBacklogItem, EphemeralContextListFnOptions, FeatureBacklogContextVariables, FeatureBacklogItem, FeatureBacklogOptions, FeatureBacklogStage, FolderBacklogContextsOptions, GetFirstStepsInput, GetRecursiveStepsOptions, ImplValidateCommand, IsValidationCommandResultOkInput, MaybePromGetter, Recipe, RetryUntilGreenInput, StepIndex, ValidationCommandFn, ValidationCommandFnInput, YmlBacklogContextsOptions };
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import path from 'path';
5
5
  import path$1 from 'node:path';
6
6
  import { fileURLToPath as fileURLToPath$1 } from 'node:url';
7
7
  import fs from 'fs/promises';
8
- import { dump, load } from 'js-yaml';
8
+ import { load, dump } from 'js-yaml';
9
9
 
10
10
  /** Run a shell script via `sh -c` (portable on Unix-like systems and Git Bash on Windows). */
11
11
  function shellCommand(script) {
@@ -223,22 +223,188 @@ function assertProjectRelativePath(filePath, label) {
223
223
  }
224
224
  function resolveBacklogPaths(configUrl, overrides) {
225
225
  const [lumpPath, lumpName] = lumpPathAndName(configUrl);
226
- const backlogFilePath = overrides?.backlogFilePath ?? path$1.join(lumpPath, 'BACKLOG.yml');
227
- const doneFilePath = overrides?.doneFilePath ?? path$1.join(lumpPath, 'DONE.yml');
228
- assertProjectRelativePath(backlogFilePath, 'backlogFilePath');
229
- assertProjectRelativePath(doneFilePath, 'doneFilePath');
226
+ const backlogItemsDir = overrides?.backlogItemsDir ?? path$1.join(lumpPath, 'backlogItems');
227
+ assertProjectRelativePath(backlogItemsDir, 'backlogItemsDir');
230
228
  return {
231
229
  lumpPath,
232
230
  lumpName,
233
- backlogFilePath,
234
- doneFilePath,
231
+ backlogItemsDir,
235
232
  };
236
233
  }
237
234
 
235
+ const CONTEXT_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
236
+ function assertRecord(value, location) {
237
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
238
+ throw new Error(`Backlog item ${location} must be an object`);
239
+ }
240
+ }
241
+ function assertStringField(record, field, location) {
242
+ const value = record[field];
243
+ if (typeof value !== 'string' || value.trim() === '') {
244
+ throw new Error(`Backlog item ${location} requires non-empty string field "${field}"`);
245
+ }
246
+ }
247
+ function assertNumberField(record, field, location) {
248
+ const value = record[field];
249
+ if (typeof value !== 'number' || Number.isNaN(value)) {
250
+ throw new Error(`Backlog item ${location} requires numeric field "${field}"`);
251
+ }
252
+ }
253
+ /** Validates and normalizes one backlog item; throws on invalid input. */
254
+ function validateBaseBacklogItem(raw, location) {
255
+ assertRecord(raw, location);
256
+ assertStringField(raw, 'name', location);
257
+ assertStringField(raw, 'task', location);
258
+ assertNumberField(raw, 'priority', location);
259
+ if (!CONTEXT_NAME_PATTERN.test(raw.name)) {
260
+ throw new Error(`Backlog item ${location} has invalid name "${raw.name}" (expected ^[a-zA-Z0-9_-]+$)`);
261
+ }
262
+ let dependsOn;
263
+ if (raw.dependsOn !== undefined) {
264
+ if (!Array.isArray(raw.dependsOn)) {
265
+ throw new Error(`Backlog item ${location} field "dependsOn" must be an array`);
266
+ }
267
+ for (const dep of raw.dependsOn) {
268
+ if (typeof dep !== 'string' || dep.trim() === '') {
269
+ throw new Error(`Backlog item ${location} field "dependsOn" must contain non-empty strings`);
270
+ }
271
+ }
272
+ dependsOn = raw.dependsOn;
273
+ }
274
+ return {
275
+ name: raw.name,
276
+ task: raw.task,
277
+ priority: raw.priority,
278
+ dependsOn,
279
+ };
280
+ }
281
+
282
+ async function listTodoFolderNames(todoDir) {
283
+ try {
284
+ const entries = await fs.readdir(todoDir, { withFileTypes: true });
285
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
286
+ }
287
+ catch (error) {
288
+ const err = error;
289
+ if (err.code === 'ENOENT') {
290
+ return [];
291
+ }
292
+ throw error;
293
+ }
294
+ }
295
+ function folderBacklogContexts({ backlogItemsDir, parseItem, parseContext, }) {
296
+ return async () => {
297
+ const todoDir = path$1.join(backlogItemsDir, 'todo');
298
+ const folderNames = await listTodoFolderNames(todoDir);
299
+ const discovered = await Promise.all(folderNames.map(async (folderName) => {
300
+ const descPath = path$1.join(todoDir, folderName, 'desc.yml');
301
+ let rawText;
302
+ try {
303
+ rawText = await fs.readFile(descPath, 'utf-8');
304
+ }
305
+ catch (error) {
306
+ const err = error;
307
+ if (err.code === 'ENOENT') {
308
+ throw new Error(`Backlog item folder "${folderName}" is missing desc.yml at ${descPath}`);
309
+ }
310
+ throw error;
311
+ }
312
+ const raw = load(rawText);
313
+ const baseItem = validateBaseBacklogItem(raw, `in folder "${folderName}"`);
314
+ if (baseItem.name !== folderName) {
315
+ throw new Error(`Backlog item folder "${folderName}" desc.yml name "${baseItem.name}" must match folder name`);
316
+ }
317
+ const item = parseItem ? parseItem(baseItem, folderName, raw) : baseItem;
318
+ return { item, folderName };
319
+ }));
320
+ discovered.sort((a, b) => {
321
+ if (a.item.priority !== b.item.priority) {
322
+ return a.item.priority - b.item.priority;
323
+ }
324
+ return a.item.name.localeCompare(b.item.name);
325
+ });
326
+ const allCtxs = await Promise.all(discovered.map(async ({ item, folderName }) => {
327
+ const { parsed, ignored } = parseContext
328
+ ? await parseContext(item, folderName)
329
+ : { parsed: undefined, ignored: false };
330
+ if (ignored) {
331
+ return null;
332
+ }
333
+ return {
334
+ name: item.name,
335
+ options: {
336
+ priority: item.priority,
337
+ dependsOnContexts: item.dependsOn,
338
+ },
339
+ variables: parsed?.variables ?? {},
340
+ ...parsed,
341
+ };
342
+ }));
343
+ return allCtxs.filter((ctx) => !!ctx);
344
+ };
345
+ }
346
+
347
+ function isPlainObject(value) {
348
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
349
+ }
350
+ /** Moves a finished backlog item folder from todo/ to completed/ after the context completes. */
351
+ const folderSetTaskDoneStep = (input) => {
352
+ const nameVarName = input.nameVarName ?? 'TASK_NAME';
353
+ return {
354
+ async commandFn({ context, workspacePath }) {
355
+ const variables = context.variables;
356
+ const itemsDirRelative = variables[input.itemsDirVarName];
357
+ const taskName = variables[nameVarName];
358
+ if (!itemsDirRelative || !taskName) {
359
+ throw new Error('Backlog items directory and task name are required');
360
+ }
361
+ const itemsDir = path$1.join(workspacePath, itemsDirRelative);
362
+ const fromDir = path$1.join(itemsDir, 'todo', taskName);
363
+ const toDir = path$1.join(itemsDir, 'completed', taskName);
364
+ const descPath = path$1.join(fromDir, 'desc.yml');
365
+ const completedDescPath = path$1.join(toDir, 'desc.yml');
366
+ if (!(await pathExists(fromDir))) {
367
+ return null;
368
+ }
369
+ if (await pathExists(toDir)) {
370
+ console.warn(`[lumpcode/recipes] Cannot move backlog item "${taskName}": already exists at ${toDir}`);
371
+ return null;
372
+ }
373
+ const rawText = await fs.readFile(descPath, 'utf-8');
374
+ const raw = load(rawText);
375
+ if (!isPlainObject(raw)) {
376
+ throw new Error(`Backlog desc.yml at ${descPath} must contain a YAML object`);
377
+ }
378
+ const updated = {
379
+ ...raw,
380
+ completedAt: new Date().toISOString(),
381
+ };
382
+ await fs.mkdir(path$1.join(itemsDir, 'completed'), { recursive: true });
383
+ await fs.rename(fromDir, toDir);
384
+ await fs.writeFile(completedDescPath, dump(updated));
385
+ return {
386
+ executable: 'cat',
387
+ args: [completedDescPath],
388
+ };
389
+ },
390
+ continueOnError: true,
391
+ };
392
+ };
393
+
394
+ let setTaskDoneDeprecatedWarned = false;
395
+ function warnSetTaskDoneDeprecated() {
396
+ if (setTaskDoneDeprecatedWarned) {
397
+ return;
398
+ }
399
+ setTaskDoneDeprecatedWarned = true;
400
+ console.warn('[lumpcode/recipes] setTaskDoneStep is deprecated; use folderSetTaskDoneStep. ' +
401
+ 'YAML backlog helpers will be removed in a future major version.');
402
+ }
238
403
  /** Moves a finished backlog item from BACKLOG.yml to DONE.yml after the context completes. */
239
404
  const setTaskDoneStep = (input) => {
240
405
  return {
241
406
  async commandFn({ context, workspacePath }) {
407
+ warnSetTaskDoneDeprecated();
242
408
  const variables = context.variables;
243
409
  const { backlogVarName, doneVarName } = input;
244
410
  const baseBacklogFilePath = variables[backlogVarName];
@@ -276,62 +442,25 @@ function resolveImplValidateCommand(implValidateCommand) {
276
442
  return (_input) => implValidateCommand;
277
443
  }
278
444
 
279
- const CONTEXT_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
280
- function assertRecord(value, index) {
281
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
282
- throw new Error(`Backlog item at index ${index} must be an object`);
283
- }
284
- }
285
- function assertStringField(record, field, index) {
286
- const value = record[field];
287
- if (typeof value !== 'string' || value.trim() === '') {
288
- throw new Error(`Backlog item at index ${index} requires non-empty string field "${field}"`);
289
- }
290
- }
291
- function assertNumberField(record, field, index) {
292
- const value = record[field];
293
- if (typeof value !== 'number' || Number.isNaN(value)) {
294
- throw new Error(`Backlog item at index ${index} requires numeric field "${field}"`);
445
+ let ymlBacklogDeprecatedWarned = false;
446
+ function warnYmlBacklogDeprecated() {
447
+ if (ymlBacklogDeprecatedWarned) {
448
+ return;
295
449
  }
450
+ ymlBacklogDeprecatedWarned = true;
451
+ console.warn('[lumpcode/recipes] ymlBacklogContexts is deprecated; use folderBacklogContexts. ' +
452
+ 'YAML backlog helpers will be removed in a future major version.');
296
453
  }
297
- /** Validates and normalizes one YAML backlog row; throws on invalid input. */
298
- function validateBaseBacklogItem(raw, index) {
299
- assertRecord(raw, index);
300
- assertStringField(raw, 'name', index);
301
- assertStringField(raw, 'task', index);
302
- assertNumberField(raw, 'priority', index);
303
- if (!CONTEXT_NAME_PATTERN.test(raw.name)) {
304
- throw new Error(`Backlog item at index ${index} has invalid name "${raw.name}" (expected ^[a-zA-Z0-9_-]+$)`);
305
- }
306
- let dependsOn;
307
- if (raw.dependsOn !== undefined) {
308
- if (!Array.isArray(raw.dependsOn)) {
309
- throw new Error(`Backlog item at index ${index} field "dependsOn" must be an array`);
310
- }
311
- for (const dep of raw.dependsOn) {
312
- if (typeof dep !== 'string' || dep.trim() === '') {
313
- throw new Error(`Backlog item at index ${index} field "dependsOn" must contain non-empty strings`);
314
- }
315
- }
316
- dependsOn = raw.dependsOn;
317
- }
318
- return {
319
- name: raw.name,
320
- task: raw.task,
321
- priority: raw.priority,
322
- dependsOn,
323
- };
324
- }
325
-
326
454
  function ymlBacklogContexts({ backlogFilePath, parseItem, parseContext, }) {
327
455
  return async () => {
456
+ warnYmlBacklogDeprecated();
328
457
  const raw = await fs.readFile(backlogFilePath, 'utf-8');
329
458
  const doc = load(raw);
330
459
  if (!Array.isArray(doc)) {
331
460
  throw new Error(`Backlog file ${backlogFilePath} must contain a YAML list`);
332
461
  }
333
462
  const allCtxs = await Promise.all(doc.map(async (rawItem, index) => {
334
- const baseItem = validateBaseBacklogItem(rawItem, index);
463
+ const baseItem = validateBaseBacklogItem(rawItem, `at index ${index}`);
335
464
  const item = parseItem ? parseItem(baseItem, index, rawItem) : baseItem;
336
465
  const { parsed, ignored } = parseContext
337
466
  ? await parseContext(item, index)
@@ -359,8 +488,8 @@ function defineRecipe(recipe) {
359
488
  const BACKLOG_STAGE_VAR = 'BACKLOG_STAGE';
360
489
  const BACKLOG_TASK_NAME_VAR = 'TASK_NAME';
361
490
  const BACKLOG_TASK_VAR = 'TASK';
362
- const BACKLOG_FILE_VAR = 'BACKLOG_FILE';
363
- const BACKLOG_DONE_FILE_VAR = 'DONE_FILE';
491
+ const BACKLOG_ITEMS_DIR_VAR = 'BACKLOG_ITEMS_DIR';
492
+ const BACKLOG_ITEM_DIR_VAR = 'BACKLOG_ITEM_DIR';
364
493
 
365
494
  function isIgnoredResolution(resolution) {
366
495
  return 'ignored' in resolution && resolution.ignored === true;
@@ -377,27 +506,25 @@ function buildStageSteps(stages, stageName) {
377
506
  if (stageDef.completion === 'moveToDone') {
378
507
  return [
379
508
  ...normalized,
380
- setTaskDoneStep({
381
- backlogVarName: BACKLOG_FILE_VAR,
382
- doneVarName: BACKLOG_DONE_FILE_VAR,
509
+ folderSetTaskDoneStep({
510
+ itemsDirVarName: BACKLOG_ITEMS_DIR_VAR,
383
511
  }),
384
512
  ];
385
513
  }
386
514
  return normalized;
387
515
  }
388
516
  function backlog(options) {
389
- const { configUrl, backlogFilePath: backlogFilePathOverride, doneFilePath: doneFilePathOverride, stages, parseItem, resolveItem, ...rest } = options;
517
+ const { configUrl, backlogItemsDir: backlogItemsDirOverride, stages, parseItem, resolveItem, ...rest } = options;
390
518
  const paths = resolveBacklogPaths(configUrl, {
391
- backlogFilePath: backlogFilePathOverride,
392
- doneFilePath: doneFilePathOverride,
519
+ backlogItemsDir: backlogItemsDirOverride,
393
520
  });
394
521
  const projectRoot = projectRootFromConfigUrl(configUrl);
395
- const absoluteBacklogPath = path$1.join(projectRoot, paths.backlogFilePath);
522
+ const absoluteBacklogItemsDir = path$1.join(projectRoot, paths.backlogItemsDir);
396
523
  return defineConfig({
397
- getContextListFn: ymlBacklogContexts({
398
- backlogFilePath: absoluteBacklogPath,
524
+ getContextListFn: folderBacklogContexts({
525
+ backlogItemsDir: absoluteBacklogItemsDir,
399
526
  parseItem,
400
- async parseContext(item) {
527
+ async parseContext(item, folderName) {
401
528
  const resolution = await resolveItem({ item, paths });
402
529
  if (isIgnoredResolution(resolution)) {
403
530
  return { ignored: true };
@@ -407,14 +534,15 @@ function backlog(options) {
407
534
  ...(item.dependsOn ?? []),
408
535
  ...(additionalDependsOnContexts ?? []),
409
536
  ];
537
+ const backlogItemDir = path$1.join(paths.backlogItemsDir, 'todo', folderName);
410
538
  return {
411
539
  parsed: {
412
540
  name: contextName ?? item.name,
413
541
  variables: {
414
542
  [BACKLOG_TASK_NAME_VAR]: item.name,
415
543
  [BACKLOG_TASK_VAR]: item.task,
416
- [BACKLOG_FILE_VAR]: paths.backlogFilePath,
417
- [BACKLOG_DONE_FILE_VAR]: paths.doneFilePath,
544
+ [BACKLOG_ITEMS_DIR_VAR]: paths.backlogItemsDir,
545
+ [BACKLOG_ITEM_DIR_VAR]: backlogItemDir,
418
546
  [BACKLOG_STAGE_VAR]: stage,
419
547
  ...variables,
420
548
  },
@@ -441,18 +569,14 @@ function backlog(options) {
441
569
  }
442
570
  const backlogRecipe = defineRecipe((options) => backlog(options));
443
571
 
444
- const DEFAULT_IMPL_VALIDATE_COMMAND = [
445
- 'npm run build -w=@lumpcode/cli',
446
- 'npm run test -w=@lumpcode/cli',
447
- ].join(' && ');
448
572
  const abstractionBacklog = defineRecipe((options) => {
449
- const { implValidateCommand = DEFAULT_IMPL_VALIDATE_COMMAND, configUrl, ...rest } = options;
573
+ const { implValidateCommand, configUrl, implSteps, ...rest } = options;
450
574
  const projectRoot = projectRootFromConfigUrl(configUrl);
451
- const runImplValidation = resolveImplValidateCommand(implValidateCommand);
575
+ const runImplValidation = resolveImplValidateCommand(implValidateCommand ?? 'echo "No implementation validation command provided. I say, trust but verify, but well..."');
452
576
  return backlog({
453
577
  configUrl,
454
578
  async resolveItem({ item, paths }) {
455
- const itemPrdPath = path$1.join(paths.lumpPath, 'prds', `${item.name}.prd.md`);
579
+ const itemPrdPath = path$1.join(paths.backlogItemsDir, 'todo', item.name, 'prd.md');
456
580
  const hasPrd = await pathExists(path$1.join(projectRoot, itemPrdPath));
457
581
  if (!hasPrd) {
458
582
  return { ignored: true };
@@ -468,7 +592,7 @@ const abstractionBacklog = defineRecipe((options) => {
468
592
  implementation: {
469
593
  completion: 'moveToDone',
470
594
  steps: retryUntilGreen({
471
- steps: [{
595
+ steps: implSteps ?? [{
472
596
  promptFn({ context: ctx }) {
473
597
  const vars = ctx.variables;
474
598
  const { PRD_FILE, TASK_NAME, TASK } = vars;
@@ -480,10 +604,10 @@ const abstractionBacklog = defineRecipe((options) => {
480
604
  ${TASK}
481
605
 
482
606
  Requirements:
483
- - Materialize the abstraction as a new util under packages/apps/cli/src/utils/<utilName>/ following existing conventions: main.ts (implementation), index.ts (re-export), unit.test.ts (unit tests), and a barrel export from packages/apps/cli/src/utils/index.ts.
484
- - Refactor all call sites in packages/apps/cli to import the new util.
607
+ - Materialize the abstraction as a new util following existing conventions in the codebase.
608
+ - Refactor all call sites to import the new util.
485
609
  - Net line count must go down after the refactor (removed duplication minus new util code, excluding the new unit test file). Do not extract one-off logic or move code without deleting repetition.
486
- - Include unit tests in unit.test.ts (match sibling utils in packages/apps/cli/src/utils/).
610
+ - Include unit tests for the new util.
487
611
  `.trim();
488
612
  },
489
613
  }],
@@ -496,43 +620,44 @@ const abstractionBacklog = defineRecipe((options) => {
496
620
  });
497
621
 
498
622
  const abstractionFinder = defineRecipe((options) => {
499
- const { maxPendingAbstractions = 5, scanDirectories, customPrompt, backlogFilePath, prdDirPath, doneFilePath, } = options;
623
+ const { maxPendingAbstractions = 5, scanDirectories, customPrompt, backlogItemsDir, } = options; // TODO : check if pending abstcations in backlog are less than maxPendingAbstractions
500
624
  return defineConfig({
501
625
  ...options,
502
626
  getContextListFn: ephemeralContextListFn({
503
627
  contextCount: maxPendingAbstractions,
504
628
  variables: {
505
- PRD_DIR_PATH: prdDirPath ?? '',
506
- BACKLOG_FILE_PATH: backlogFilePath ?? '',
507
- DONE_FILE_PATH: doneFilePath ?? '',
629
+ BACKLOG_ITEMS_DIR: backlogItemsDir,
508
630
  },
509
631
  }),
510
- steps: buildFinderPrompt({ prdDirPath, backlogFilePath, doneFilePath, scanDirectories, customPrompt }),
632
+ steps: buildFinderPrompt({ backlogItemsDir, scanDirectories, customPrompt }),
511
633
  });
512
634
  });
513
- function buildFinderPrompt({ prdDirPath, backlogFilePath, doneFilePath, scanDirectories, customPrompt, }) {
635
+ function buildFinderPrompt({ backlogItemsDir, scanDirectories, customPrompt, }) {
514
636
  return customPrompt ? customPrompt() : `
515
637
  Scan ${(scanDirectories || []).map((dir) => `@${dir}`).join(' and ') || 'the codebase'} for duplicated logic that appears in multiple places (same pattern, not merely similar file structure).
516
638
 
517
- Read @${backlogFilePath} and @${doneFilePath}. Do not propose abstractions whose util name already appears in either file.
639
+ List existing backlog item names under @${backlogItemsDir}/todo/ and @${backlogItemsDir}/completed/. Do not propose abstractions whose util name already appears in either directory.
518
640
 
519
641
  Pick exactly one new abstraction that:
520
642
  - Has a clear util name matching ^[a-zA-Z0-9_-]+$ that describes the pattern it captures.
521
643
  - Would materialize as a new util under packages/apps/cli/src/utils/<utilName>/.
522
644
  - Would shrink the codebase: refactoring all call sites in packages/apps/cli should reduce net line count (excluding new unit tests).
523
645
 
524
- Add exactly one entry to @${backlogFilePath} with:
525
- - task: a concise summary of the repeated pattern, proposed util name, and affected areas
526
- - priority: max existing priority in BACKLOG.yml plus 1 (or 1 if the backlog is empty)
527
- - dependsOn: optional list of util names from BACKLOG.yml or DONE.yml that must land first (only when clearly needed)
528
-
529
- Write an implementation-ready PRD to @${prdDirPath}/<utilName>.prd.md for the same util name. The PRD should be self-contained and include:
530
- - Problem statement and repeated pattern
531
- - Goals and non-goals
532
- - Proposed util API and affected files
533
- - Acceptance criteria (including net line reduction and unit tests)
534
-
535
- Do not implement code. Only edit @${backlogFilePath} (append one item) and create the PRD file.
646
+ Create exactly one new backlog item folder at @${backlogItemsDir}/todo/<utilName>/ with:
647
+ - desc.yml containing:
648
+ - name: <utilName> (must match folder name)
649
+ - task: a concise summary of the repeated pattern, proposed util name, and affected areas
650
+ - priority: max existing priority in todo/ plus 1 (or 1 if todo/ is empty)
651
+ - dependsOn: optional list of util names from todo/ or completed/ that must land first (only when clearly needed)
652
+ - prd.md: an implementation-ready PRD for the same util name. The PRD should be self-contained and include:
653
+ - Problem statement and repeated pattern
654
+ - Goals and non-goals
655
+ - Proposed util API and affected files
656
+ - Acceptance criteria (including net line reduction and unit tests)
657
+
658
+ Do not implement code. Only create @${backlogItemsDir}/todo/<utilName>/desc.yml and @${backlogItemsDir}/todo/<utilName>/prd.md.
659
+
660
+ Do not take too much time looking for every possible abstraction. Once you found a good abstraction, stop and create the backlog item.
536
661
  `.trim();
537
662
  }
538
663
 
@@ -561,8 +686,8 @@ function featureContextName(itemName, stage) {
561
686
  }
562
687
  }
563
688
  async function resolveFeatureBacklogItem(item, paths, projectRoot, lumpName, baseBranch) {
564
- const prdFilePath = path$1.join(paths.lumpPath, 'prds', `${item.name}.prd.md`);
565
- const testPlanFilePath = path$1.join(paths.lumpPath, 'testPlans', `${item.name}.test.md`);
689
+ const prdFilePath = path$1.join(paths.backlogItemsDir, 'todo', item.name, 'prd.md');
690
+ const testPlanFilePath = path$1.join(paths.backlogItemsDir, 'todo', item.name, 'testPlan.md');
566
691
  const hasPrd = await pathExists(path$1.join(projectRoot, prdFilePath));
567
692
  if (!hasPrd) {
568
693
  if (item.manualPrd === true) {
@@ -615,15 +740,14 @@ async function resolveFeatureBacklogItem(item, paths, projectRoot, lumpName, bas
615
740
  };
616
741
  }
617
742
  const featureBacklog = defineRecipe((options) => {
618
- const { configUrl, baseBranch, implValidateCommand, backlogFilePath, doneFilePath, ...rest } = options;
743
+ const { configUrl, baseBranch, implValidateCommand, backlogItemsDir, ...rest } = options;
619
744
  const projectRoot = projectRootFromConfigUrl(configUrl);
620
- const runImplValidation = resolveImplValidateCommand(implValidateCommand);
745
+ const runImplValidation = resolveImplValidateCommand(implValidateCommand ?? 'echo "No implementation validation command provided. I say, trust but verify, but well..."');
621
746
  return backlog({
622
747
  configUrl,
623
- backlogFilePath,
624
- doneFilePath,
748
+ backlogItemsDir,
625
749
  baseBranch,
626
- parseItem(baseItem, _index, raw) {
750
+ parseItem(baseItem, _folderName, raw) {
627
751
  assertValidFeatureItemName(baseItem.name);
628
752
  const record = raw;
629
753
  if (record.manualPrd !== undefined && typeof record.manualPrd !== 'boolean') {
@@ -644,16 +768,16 @@ const featureBacklog = defineRecipe((options) => {
644
768
  {
645
769
  promptFn({ context: ctx }) {
646
770
  const vars = ctx.variables;
647
- const { BACKLOG_FILE, TASK_NAME, TASK, PRD_FILE } = vars;
771
+ const { BACKLOG_ITEM_DIR, TASK_NAME, TASK, PRD_FILE } = vars;
648
772
  return `
649
- Write a product requirements document (PRD) for the following Lumpcode backlog item from @${BACKLOG_FILE}.
773
+ Write a product requirements document (PRD) for the following backlog item from @${BACKLOG_ITEM_DIR}/desc.yml.
650
774
 
651
775
  Task name: ${TASK_NAME}
652
776
 
653
777
  Task:
654
778
  ${TASK}
655
779
 
656
- Save the PRD to @${PRD_FILE}. Do not edit @${BACKLOG_FILE}.
780
+ Save the PRD to @${PRD_FILE}. Do not edit @${BACKLOG_ITEM_DIR}/desc.yml.
657
781
 
658
782
  The PRD should be self-contained and implementation-ready. Include:
659
783
  - Problem statement and motivation
@@ -678,9 +802,9 @@ The PRD should not contain any testing strategy details.
678
802
  {
679
803
  promptFn({ context: ctx }) {
680
804
  const vars = ctx.variables;
681
- const { BACKLOG_FILE, TASK_NAME, TASK, PRD_FILE, TEST_PLAN_FILE } = vars;
805
+ const { BACKLOG_ITEM_DIR, TASK_NAME, TASK, PRD_FILE, TEST_PLAN_FILE } = vars;
682
806
  return `
683
- Write a test plan for the following Lumpcode backlog item from @${BACKLOG_FILE}.
807
+ Write a test plan for the following backlog item from @${BACKLOG_ITEM_DIR}/desc.yml.
684
808
 
685
809
  Task name: ${TASK_NAME}
686
810
  Task:
@@ -688,7 +812,7 @@ ${TASK}
688
812
 
689
813
  The PRD for this task is @${PRD_FILE}. The test plan should match the requirements of the PRD.
690
814
 
691
- Save the test plan to @${TEST_PLAN_FILE}. Do not edit @${BACKLOG_FILE} nor @${PRD_FILE}.
815
+ Save the test plan to @${TEST_PLAN_FILE}. Do not edit @${BACKLOG_ITEM_DIR}/desc.yml nor @${PRD_FILE}.
692
816
 
693
817
  The test plan should be self-contained and implementation-ready. Include:
694
818
  - Test cases
@@ -707,9 +831,9 @@ The test plan should be self-contained and implementation-ready. Include:
707
831
  {
708
832
  promptFn({ context: ctx }) {
709
833
  const vars = ctx.variables;
710
- const { BACKLOG_FILE, TASK_NAME, TASK, PRD_FILE, TEST_PLAN_FILE } = vars;
834
+ const { BACKLOG_ITEM_DIR, TASK_NAME, TASK, PRD_FILE, TEST_PLAN_FILE } = vars;
711
835
  return `
712
- Write a test implementation for the following Lumpcode backlog item from @${BACKLOG_FILE}.
836
+ Write a test implementation for the following backlog item from @${BACKLOG_ITEM_DIR}/desc.yml.
713
837
 
714
838
  Task name: ${TASK_NAME}
715
839
  Task:
@@ -746,4 +870,4 @@ The implementation should make the tests pass. Do not edit any test file.
746
870
  });
747
871
  });
748
872
 
749
- export { BACKLOG_DONE_FILE_VAR, BACKLOG_FILE_VAR, BACKLOG_STAGE_VAR, BACKLOG_TASK_NAME_VAR, BACKLOG_TASK_VAR, abstractionBacklog, abstractionFinder, backlog, backlogRecipe, defineRecipe, ephemeralContextListFn, featureBacklog, getRecursiveSteps, lumpPathAndName, normalizeMaybePromGetter, projectRootFromConfigUrl, requireArtifactStep, resolveBacklogPaths, resolveFeatureBacklogItem, resolveImplValidateCommand, retryUntilGreen, setTaskDoneStep, shellCommand, validateBaseBacklogItem, ymlBacklogContexts };
873
+ export { BACKLOG_ITEMS_DIR_VAR, BACKLOG_ITEM_DIR_VAR, BACKLOG_STAGE_VAR, BACKLOG_TASK_NAME_VAR, BACKLOG_TASK_VAR, abstractionBacklog, abstractionFinder, backlog, backlogRecipe, defineRecipe, ephemeralContextListFn, featureBacklog, folderBacklogContexts, folderSetTaskDoneStep, getRecursiveSteps, lumpPathAndName, normalizeMaybePromGetter, projectRootFromConfigUrl, requireArtifactStep, resolveBacklogPaths, resolveFeatureBacklogItem, resolveImplValidateCommand, retryUntilGreen, setTaskDoneStep, shellCommand, validateBaseBacklogItem, ymlBacklogContexts };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumpcode/recipes",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "description": "Lumpcode lump recipes and kit helpers",
5
5
  "keywords": [
6
6
  "lumpcode",