@lumpcode/recipes 0.0.13 → 0.0.15

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/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
  },
@@ -446,19 +574,19 @@ const backlogRecipe = defineRecipe((options) => backlog(options));
446
574
  const abstractionBacklog = defineRecipe((options) => {
447
575
  const { implValidateCommand, configUrl, implSteps, ...rest } = options;
448
576
  const projectRoot = projectRootFromConfigUrl(configUrl);
449
- const runImplValidation = resolveImplValidateCommand(implValidateCommand);
577
+ const runImplValidation = resolveImplValidateCommand(implValidateCommand ?? 'echo "No implementation validation command provided. I say, trust but verify, but well..."');
450
578
  return backlog({
451
579
  configUrl,
452
580
  async resolveItem({ item, paths }) {
453
- const itemPrdPath = path$1.join(paths.lumpPath, 'prds', `${item.name}.prd.md`);
454
- const hasPrd = await core.pathExists(path$1.join(projectRoot, itemPrdPath));
455
- if (!hasPrd) {
581
+ const itemReqPath = path$1.join(paths.backlogItemsDir, 'todo', item.name, 'requirements.md');
582
+ const hasReq = await core.pathExists(path$1.join(projectRoot, itemReqPath));
583
+ if (!hasReq) {
456
584
  return { ignored: true };
457
585
  }
458
586
  return {
459
587
  stage: 'implementation',
460
588
  variables: {
461
- PRD_FILE: itemPrdPath,
589
+ REQ_FILE: itemReqPath,
462
590
  },
463
591
  };
464
592
  },
@@ -469,9 +597,9 @@ const abstractionBacklog = defineRecipe((options) => {
469
597
  steps: implSteps ?? [{
470
598
  promptFn({ context: ctx }) {
471
599
  const vars = ctx.variables;
472
- const { PRD_FILE, TASK_NAME, TASK } = vars;
600
+ const { REQ_FILE, TASK_NAME, TASK } = vars;
473
601
  return `
474
- Implement the abstraction described in @${PRD_FILE}.
602
+ Implement the abstraction described in @${REQ_FILE}.
475
603
 
476
604
  Backlog item: ${TASK_NAME}
477
605
  Task summary:
@@ -494,47 +622,48 @@ const abstractionBacklog = defineRecipe((options) => {
494
622
  });
495
623
 
496
624
  const abstractionFinder = defineRecipe((options) => {
497
- 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
498
626
  return cliUtils.defineConfig({
499
627
  ...options,
500
628
  getContextListFn: ephemeralContextListFn({
501
629
  contextCount: maxPendingAbstractions,
502
630
  variables: {
503
- PRD_DIR_PATH: prdDirPath ?? '',
504
- BACKLOG_FILE_PATH: backlogFilePath ?? '',
505
- DONE_FILE_PATH: doneFilePath ?? '',
631
+ BACKLOG_ITEMS_DIR: backlogItemsDir,
506
632
  },
507
633
  }),
508
- steps: buildFinderPrompt({ prdDirPath, backlogFilePath, doneFilePath, scanDirectories, customPrompt }),
634
+ steps: buildFinderPrompt({ backlogItemsDir, scanDirectories, customPrompt }),
509
635
  });
510
636
  });
511
- function buildFinderPrompt({ prdDirPath, backlogFilePath, doneFilePath, scanDirectories, customPrompt, }) {
637
+ function buildFinderPrompt({ backlogItemsDir, scanDirectories, customPrompt, }) {
512
638
  return customPrompt ? customPrompt() : `
513
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).
514
640
 
515
- 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.
516
642
 
517
643
  Pick exactly one new abstraction that:
518
644
  - Has a clear util name matching ^[a-zA-Z0-9_-]+$ that describes the pattern it captures.
519
645
  - Would materialize as a new util under packages/apps/cli/src/utils/<utilName>/.
520
646
  - Would shrink the codebase: refactoring all call sites in packages/apps/cli should reduce net line count (excluding new unit tests).
521
647
 
522
- Add exactly one entry to @${backlogFilePath} with:
523
- - task: a concise summary of the repeated pattern, proposed util name, and affected areas
524
- - priority: max existing priority in BACKLOG.yml plus 1 (or 1 if the backlog is empty)
525
- - dependsOn: optional list of util names from BACKLOG.yml or DONE.yml that must land first (only when clearly needed)
526
-
527
- Write an implementation-ready PRD to @${prdDirPath}/<utilName>.prd.md for the same util name. The PRD should be self-contained and include:
528
- - Problem statement and repeated pattern
529
- - Goals and non-goals
530
- - Proposed util API and affected files
531
- - Acceptance criteria (including net line reduction and unit tests)
532
-
533
- 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
+ - requirements.md: an implementation-ready requirements document for the same util name. It 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>/requirements.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.
534
663
  `.trim();
535
664
  }
536
665
 
537
- const RESERVED_NAME_SUFFIXES = ['_prd', '_testPlan', '_tests_impl'];
666
+ const RESERVED_NAME_SUFFIXES = ['_req', '_testPlan', '_tests_impl'];
538
667
  function assertValidFeatureItemName(name) {
539
668
  for (const suffix of RESERVED_NAME_SUFFIXES) {
540
669
  if (name.endsWith(suffix)) {
@@ -544,8 +673,8 @@ function assertValidFeatureItemName(name) {
544
673
  }
545
674
  function featureContextName(itemName, stage) {
546
675
  switch (stage) {
547
- case 'makePrd':
548
- return `${itemName}_prd`;
676
+ case 'makeReq':
677
+ return `${itemName}_req`;
549
678
  case 'makeTestPlan':
550
679
  return `${itemName}_testPlan`;
551
680
  case 'testImpl':
@@ -559,17 +688,17 @@ function featureContextName(itemName, stage) {
559
688
  }
560
689
  }
561
690
  async function resolveFeatureBacklogItem(item, paths, projectRoot, lumpName, baseBranch) {
562
- const prdFilePath = path$1.join(paths.lumpPath, 'prds', `${item.name}.prd.md`);
563
- const testPlanFilePath = path$1.join(paths.lumpPath, 'testPlans', `${item.name}.test.md`);
564
- const hasPrd = await core.pathExists(path$1.join(projectRoot, prdFilePath));
565
- if (!hasPrd) {
566
- if (item.manualPrd === true) {
691
+ const reqFilePath = path$1.join(paths.backlogItemsDir, 'todo', item.name, 'requirements.md');
692
+ const testPlanFilePath = path$1.join(paths.backlogItemsDir, 'todo', item.name, 'testPlan.md');
693
+ const hasReq = await core.pathExists(path$1.join(projectRoot, reqFilePath));
694
+ if (!hasReq) {
695
+ if (item.manualReq === true) {
567
696
  return { ignored: true };
568
697
  }
569
698
  return {
570
- stage: 'makePrd',
571
- contextName: featureContextName(item.name, 'makePrd'),
572
- variables: { PRD_FILE: prdFilePath },
699
+ stage: 'makeReq',
700
+ contextName: featureContextName(item.name, 'makeReq'),
701
+ variables: { REQ_FILE: reqFilePath },
573
702
  };
574
703
  }
575
704
  const hasTestPlan = await core.pathExists(path$1.join(projectRoot, testPlanFilePath));
@@ -578,7 +707,7 @@ async function resolveFeatureBacklogItem(item, paths, projectRoot, lumpName, bas
578
707
  stage: 'makeTestPlan',
579
708
  contextName: featureContextName(item.name, 'makeTestPlan'),
580
709
  variables: {
581
- PRD_FILE: prdFilePath,
710
+ REQ_FILE: reqFilePath,
582
711
  TEST_PLAN_FILE: testPlanFilePath,
583
712
  },
584
713
  };
@@ -595,7 +724,7 @@ async function resolveFeatureBacklogItem(item, paths, projectRoot, lumpName, bas
595
724
  stage: 'implementation',
596
725
  contextName: featureContextName(item.name, 'implementation'),
597
726
  variables: {
598
- PRD_FILE: prdFilePath,
727
+ REQ_FILE: reqFilePath,
599
728
  TEST_PLAN_FILE: testPlanFilePath,
600
729
  },
601
730
  };
@@ -607,53 +736,52 @@ async function resolveFeatureBacklogItem(item, paths, projectRoot, lumpName, bas
607
736
  stage: 'testImpl',
608
737
  contextName: testsImplContextName,
609
738
  variables: {
610
- PRD_FILE: prdFilePath,
739
+ REQ_FILE: reqFilePath,
611
740
  TEST_PLAN_FILE: testPlanFilePath,
612
741
  },
613
742
  };
614
743
  }
615
744
  const featureBacklog = defineRecipe((options) => {
616
- const { configUrl, baseBranch, implValidateCommand, backlogFilePath, doneFilePath, ...rest } = options;
745
+ const { configUrl, baseBranch, implValidateCommand, backlogItemsDir, ...rest } = options;
617
746
  const projectRoot = projectRootFromConfigUrl(configUrl);
618
- const runImplValidation = resolveImplValidateCommand(implValidateCommand);
747
+ const runImplValidation = resolveImplValidateCommand(implValidateCommand ?? 'echo "No implementation validation command provided. I say, trust but verify, but well..."');
619
748
  return backlog({
620
749
  configUrl,
621
- backlogFilePath,
622
- doneFilePath,
750
+ backlogItemsDir,
623
751
  baseBranch,
624
- parseItem(baseItem, _index, raw) {
752
+ parseItem(baseItem, _folderName, raw) {
625
753
  assertValidFeatureItemName(baseItem.name);
626
754
  const record = raw;
627
- if (record.manualPrd !== undefined && typeof record.manualPrd !== 'boolean') {
628
- throw new Error(`Backlog item "${baseItem.name}" field "manualPrd" must be a boolean`);
755
+ if (record.manualReq !== undefined && typeof record.manualReq !== 'boolean') {
756
+ throw new Error(`Backlog item "${baseItem.name}" field "manualReq" must be a boolean`);
629
757
  }
630
758
  return {
631
759
  ...baseItem,
632
- manualPrd: record.manualPrd === true ? true : undefined,
760
+ manualReq: record.manualReq === true ? true : undefined,
633
761
  };
634
762
  },
635
763
  async resolveItem({ item, paths }) {
636
764
  return resolveFeatureBacklogItem(item, paths, projectRoot, paths.lumpName, baseBranch);
637
765
  },
638
766
  stages: {
639
- makePrd: {
767
+ makeReq: {
640
768
  completion: 'keepPending',
641
769
  steps: [
642
770
  {
643
771
  promptFn({ context: ctx }) {
644
772
  const vars = ctx.variables;
645
- const { BACKLOG_FILE, TASK_NAME, TASK, PRD_FILE } = vars;
773
+ const { BACKLOG_ITEM_DIR, TASK_NAME, TASK, REQ_FILE } = vars;
646
774
  return `
647
- Write a product requirements document (PRD) for the following Lumpcode backlog item from @${BACKLOG_FILE}.
775
+ Write a requirements document for the following backlog item from @${BACKLOG_ITEM_DIR}/desc.yml.
648
776
 
649
777
  Task name: ${TASK_NAME}
650
778
 
651
779
  Task:
652
780
  ${TASK}
653
781
 
654
- Save the PRD to @${PRD_FILE}. Do not edit @${BACKLOG_FILE}.
782
+ Save the requirements document to @${REQ_FILE}. Do not edit @${BACKLOG_ITEM_DIR}/desc.yml.
655
783
 
656
- The PRD should be self-contained and implementation-ready. Include:
784
+ The requirements document should be self-contained and implementation-ready. Include:
657
785
  - Problem statement and motivation
658
786
  - Goals and non-goals
659
787
  - User stories / use cases
@@ -662,12 +790,12 @@ The PRD should be self-contained and implementation-ready. Include:
662
790
  - Technical approach and affected packages or docs
663
791
  - Acceptance criteria
664
792
 
665
- Do not implement the feature — only create the PRD markdown file.
666
- The PRD should not contain any testing strategy details.
793
+ Do not implement the feature — only create the requirements markdown file.
794
+ The requirements document should not contain any testing strategy details.
667
795
  `.trim();
668
796
  },
669
797
  },
670
- requireArtifactStep('PRD_FILE'),
798
+ requireArtifactStep('REQ_FILE'),
671
799
  ],
672
800
  },
673
801
  makeTestPlan: {
@@ -676,17 +804,17 @@ The PRD should not contain any testing strategy details.
676
804
  {
677
805
  promptFn({ context: ctx }) {
678
806
  const vars = ctx.variables;
679
- const { BACKLOG_FILE, TASK_NAME, TASK, PRD_FILE, TEST_PLAN_FILE } = vars;
807
+ const { BACKLOG_ITEM_DIR, TASK_NAME, TASK, REQ_FILE, TEST_PLAN_FILE } = vars;
680
808
  return `
681
- 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.
682
810
 
683
811
  Task name: ${TASK_NAME}
684
812
  Task:
685
813
  ${TASK}
686
814
 
687
- The PRD for this task is @${PRD_FILE}. The test plan should match the requirements of the PRD.
815
+ The requirements for this task are in @${REQ_FILE}. The test plan should match those requirements.
688
816
 
689
- 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 @${REQ_FILE}.
690
818
 
691
819
  The test plan should be self-contained and implementation-ready. Include:
692
820
  - Test cases
@@ -705,16 +833,16 @@ The test plan should be self-contained and implementation-ready. Include:
705
833
  {
706
834
  promptFn({ context: ctx }) {
707
835
  const vars = ctx.variables;
708
- const { BACKLOG_FILE, TASK_NAME, TASK, PRD_FILE, TEST_PLAN_FILE } = vars;
836
+ const { BACKLOG_ITEM_DIR, TASK_NAME, TASK, REQ_FILE, TEST_PLAN_FILE } = vars;
709
837
  return `
710
- 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.
711
839
 
712
840
  Task name: ${TASK_NAME}
713
841
  Task:
714
842
  ${TASK}
715
843
 
716
844
  Follow the test plan in @${TEST_PLAN_FILE}.
717
- The PRD for this task is @${PRD_FILE}.
845
+ The requirements for this task are in @${REQ_FILE}.
718
846
  `.trim();
719
847
  },
720
848
  },
@@ -727,9 +855,9 @@ The PRD for this task is @${PRD_FILE}.
727
855
  {
728
856
  promptFn({ context: ctx }) {
729
857
  const vars = ctx.variables;
730
- const { PRD_FILE, TEST_PLAN_FILE } = vars;
858
+ const { REQ_FILE, TEST_PLAN_FILE } = vars;
731
859
  return `
732
- Implement the feature described in @${PRD_FILE}.
860
+ Implement the feature described in @${REQ_FILE}.
733
861
  The tests have already been implemented according to the test plan in @${TEST_PLAN_FILE}.
734
862
  The implementation should make the tests pass. Do not edit any test file.
735
863
  `.trim();
@@ -744,8 +872,8 @@ The implementation should make the tests pass. Do not edit any test file.
744
872
  });
745
873
  });
746
874
 
747
- exports.BACKLOG_DONE_FILE_VAR = BACKLOG_DONE_FILE_VAR;
748
- 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;
749
877
  exports.BACKLOG_STAGE_VAR = BACKLOG_STAGE_VAR;
750
878
  exports.BACKLOG_TASK_NAME_VAR = BACKLOG_TASK_NAME_VAR;
751
879
  exports.BACKLOG_TASK_VAR = BACKLOG_TASK_VAR;
@@ -756,6 +884,8 @@ exports.backlogRecipe = backlogRecipe;
756
884
  exports.defineRecipe = defineRecipe;
757
885
  exports.ephemeralContextListFn = ephemeralContextListFn;
758
886
  exports.featureBacklog = featureBacklog;
887
+ exports.folderBacklogContexts = folderBacklogContexts;
888
+ exports.folderSetTaskDoneStep = folderSetTaskDoneStep;
759
889
  exports.getRecursiveSteps = getRecursiveSteps;
760
890
  exports.lumpPathAndName = lumpPathAndName;
761
891
  exports.normalizeMaybePromGetter = normalizeMaybePromGetter;