@lumpcode/recipes 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -9
- package/dist/index.cjs +234 -31
- package/dist/index.d.ts +36 -8
- package/dist/index.js +232 -32
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -15,8 +15,8 @@ npm install @lumpcode/recipes
|
|
|
15
15
|
| Recipe | Export | Use when |
|
|
16
16
|
|--------|--------|----------|
|
|
17
17
|
| **backlog** | `backlog` | Generic folder backlog with a typed stage map and per-item stage resolution |
|
|
18
|
-
| **featureBacklog** | `featureBacklog` |
|
|
19
|
-
| **abstractionFinder** | `abstractionFinder` |
|
|
18
|
+
| **featureBacklog** | `featureBacklog` | Folder feature campaign: TDD stages, optional `directImpl`, tickets, and `dev` / `feature/*` discovery |
|
|
19
|
+
| **abstractionFinder** | `abstractionFinder` | One ephemeral context per tick that appends a backlog item + requirements doc while `todo/` is under `maxPendingAbstractions` |
|
|
20
20
|
| **abstractionBacklog** | `abstractionBacklog` | Folder backlog items with requirements — implement abstraction with verify-until-green, then move item to completed/ |
|
|
21
21
|
|
|
22
22
|
Recipe factories and variable-carrying kit helpers accept the same dual generics as `defineConfig` from `@lumpcode/cli-utils`: `<V extends LumpVariables, SV extends StepVariables>`, with defaults equal to the unbound bags. Pass explicit type args when refining preset contracts; omit them for classic untyped configs.
|
|
@@ -29,13 +29,14 @@ The backlog recipes (`backlog`, `featureBacklog`, `abstractionBacklog`) use a fo
|
|
|
29
29
|
.lumpcode/lumps/<lump>/backlogItems/
|
|
30
30
|
todo/<name>/desc.yml
|
|
31
31
|
todo/<name>/requirements.md # optional until makeReq / finder writes it
|
|
32
|
-
todo/<name>/testPlan.md # featureBacklog
|
|
32
|
+
todo/<name>/testPlan.md # featureBacklog TDD; optional until makeTestPlan
|
|
33
|
+
todo/<parent>/tickets/<ticket>/desc.yml # featureBacklog: parent skipped when tickets/ is non-empty
|
|
33
34
|
completed/<name>/desc.yml # includes completedAt after move-to-done
|
|
34
35
|
completed/<name>/requirements.md # moves with the folder
|
|
35
36
|
completed/<name>/testPlan.md
|
|
36
37
|
```
|
|
37
38
|
|
|
38
|
-
`desc.yml` is a single YAML object with `name`, `task`, `priority`, optional `dependsOn`, and recipe-specific fields (
|
|
39
|
+
`desc.yml` is a single YAML object with `name`, `task`, `priority`, optional `dependsOn`, and recipe-specific fields (`manualReq`, `workflow` for featureBacklog).
|
|
39
40
|
|
|
40
41
|
## Kit
|
|
41
42
|
|
|
@@ -45,7 +46,7 @@ Flat helpers under `src/kit/` (re-exported from the package root):
|
|
|
45
46
|
- `getRecursiveSteps` — agent step(s) + validation command, retry until pass (retries via `postCommandExecFn` returned steps)
|
|
46
47
|
- `retryUntilGreen` — opinionated wrapper over `getRecursiveSteps` with default fix prompt
|
|
47
48
|
- `ephemeralContextListFn` — N fresh synthetic contexts per run (`contextCount`, index-aware names)
|
|
48
|
-
- `folderBacklogContexts` — `getContextListFn` from `backlogItems/todo/` with optional per-item parsing
|
|
49
|
+
- `folderBacklogContexts` — `getContextListFn` from `backlogItems/todo/` with optional per-item parsing (`listTodoRelativeDirs` lists todo-relative item folders, including tickets)
|
|
49
50
|
- `folderSetTaskDoneStep` — move finished item folder from `todo/` to `completed/` after a context completes
|
|
50
51
|
- `ymlBacklogContexts` / `setTaskDoneStep` — **deprecated** YAML-list helpers (warn once, still work)
|
|
51
52
|
- `resolveImplValidateCommand` — string, descriptor, or fn → `ValidationCommandFn`
|
|
@@ -92,15 +93,14 @@ export default featureBacklog<
|
|
|
92
93
|
CursorPresetLumpVariables,
|
|
93
94
|
CursorPresetStepVariables
|
|
94
95
|
>({
|
|
95
|
-
baseBranch: 'dev',
|
|
96
96
|
command: 'cursor',
|
|
97
97
|
configUrl: import.meta.url,
|
|
98
98
|
registerCommands: ['cursor'],
|
|
99
|
-
maximumNumberOfConcurrentBranches:
|
|
99
|
+
maximumNumberOfConcurrentBranches: 1,
|
|
100
100
|
verbose: true,
|
|
101
101
|
keepHistory: true,
|
|
102
102
|
lumpVariables: { model: 'composer-2.5' },
|
|
103
|
-
|
|
103
|
+
discoveryBranches: ['dev', 'feature/*'],
|
|
104
104
|
implValidateCommand: [
|
|
105
105
|
'npm run build -w=@lumpcode/cli',
|
|
106
106
|
'npm run test -w=@lumpcode/cli',
|
|
@@ -108,6 +108,8 @@ export default featureBacklog<
|
|
|
108
108
|
});
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
`desc.yml` `workflow`: omit ≡ `tdd` (`makeReq` → `makeTestPlan` → `testImpl` → `implementation`); `directImpl` skips the test-plan stages; `manual` is ignored. On `dev` only top-level `directImpl` items run (tickets never run on `dev`, even if `directImpl`); on `feature/<key>` the matching item (or parent, for tickets). Ticket context names are `<parent>-<ticket>`; `manualReq: true` waits for a human requirements file. Status reads use the concrete `discoveryBranch`.
|
|
112
|
+
|
|
111
113
|
### abstractionFinder + abstractionBacklog
|
|
112
114
|
|
|
113
115
|
Two-lump pipeline: finder tops up the implementer backlog; implementer runs items that already have requirements documents.
|
|
@@ -124,15 +126,19 @@ export default abstractionFinder<
|
|
|
124
126
|
CursorPresetLumpVariables,
|
|
125
127
|
CursorPresetStepVariables
|
|
126
128
|
>({
|
|
129
|
+
configUrl: import.meta.url,
|
|
127
130
|
maxPendingAbstractions: 5,
|
|
128
|
-
scanDirectories: ['
|
|
131
|
+
scanDirectories: ['src'],
|
|
129
132
|
backlogItemsDir: '.lumpcode/lumps/abstractionImplementer/backlogItems',
|
|
130
133
|
command: 'cursor',
|
|
131
134
|
lumpVariables: { model: 'composer-2.5' },
|
|
132
135
|
discoveryBranch: 'dev',
|
|
136
|
+
scanCommand: 'npx fallow dupes --mode semantic --format json > .lumpcode/dupes.json',
|
|
133
137
|
});
|
|
134
138
|
```
|
|
135
139
|
|
|
140
|
+
`maxPendingAbstractions` caps unmerged `todo/` items (one finder context per tick while under the cap). `scanCommand` runs before the default prompt. Pass `steps` to replace the prompt (the scanner still prepends). `configUrl: import.meta.url` is required so the recipe can count pending items from the project root.
|
|
141
|
+
|
|
136
142
|
```ts
|
|
137
143
|
// .lumpcode/lumps/abstractionImplementer/config.ts
|
|
138
144
|
import {
|
package/dist/index.cjs
CHANGED
|
@@ -8,6 +8,7 @@ var path$1 = require('node:path');
|
|
|
8
8
|
var node_url = require('node:url');
|
|
9
9
|
var fs = require('fs/promises');
|
|
10
10
|
var jsYaml = require('js-yaml');
|
|
11
|
+
var fs$1 = require('node:fs/promises');
|
|
11
12
|
|
|
12
13
|
/** Run a shell script via `sh -c` (portable on Unix-like systems and Git Bash on Windows). */
|
|
13
14
|
function shellCommand(script) {
|
|
@@ -286,10 +287,26 @@ async function listTodoFolderNames(todoDir) {
|
|
|
286
287
|
throw error;
|
|
287
288
|
}
|
|
288
289
|
}
|
|
290
|
+
/** Path relative to `todo/`, using `/` so it is stable in context variables. */
|
|
291
|
+
async function listTodoRelativeDirs(todoDir) {
|
|
292
|
+
const topNames = await listTodoFolderNames(todoDir);
|
|
293
|
+
const relativeDirs = [];
|
|
294
|
+
for (const name of topNames) {
|
|
295
|
+
const ticketNames = await listTodoFolderNames(path$1.join(todoDir, name, 'tickets'));
|
|
296
|
+
if (ticketNames.length === 0) {
|
|
297
|
+
relativeDirs.push(name);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
for (const ticketName of ticketNames) {
|
|
301
|
+
relativeDirs.push(`${name}/tickets/${ticketName}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return relativeDirs;
|
|
305
|
+
}
|
|
289
306
|
function folderBacklogContexts({ backlogItemsDir, parseItem, parseContext, }) {
|
|
290
307
|
return async () => {
|
|
291
308
|
const todoDir = path$1.join(backlogItemsDir, 'todo');
|
|
292
|
-
const folderNames = await
|
|
309
|
+
const folderNames = await listTodoRelativeDirs(todoDir);
|
|
293
310
|
const discovered = await Promise.all(folderNames.map(async (folderName) => {
|
|
294
311
|
const descPath = path$1.join(todoDir, folderName, 'desc.yml');
|
|
295
312
|
let rawText;
|
|
@@ -304,13 +321,22 @@ function folderBacklogContexts({ backlogItemsDir, parseItem, parseContext, }) {
|
|
|
304
321
|
throw error;
|
|
305
322
|
}
|
|
306
323
|
const raw = jsYaml.load(rawText);
|
|
324
|
+
const itemFolderName = path$1.basename(folderName);
|
|
307
325
|
const baseItem = validateBaseBacklogItem(raw, `in folder "${folderName}"`);
|
|
308
|
-
if (baseItem.name !==
|
|
326
|
+
if (baseItem.name !== itemFolderName) {
|
|
309
327
|
throw new Error(`Backlog item folder "${folderName}" desc.yml name "${baseItem.name}" must match folder name`);
|
|
310
328
|
}
|
|
311
329
|
const item = parseItem ? parseItem(baseItem, folderName, raw) : baseItem;
|
|
312
330
|
return { item, folderName };
|
|
313
331
|
}));
|
|
332
|
+
const seenNames = new Map();
|
|
333
|
+
for (const { item, folderName } of discovered) {
|
|
334
|
+
const previous = seenNames.get(item.name);
|
|
335
|
+
if (previous !== undefined) {
|
|
336
|
+
throw new Error(`Duplicate backlog item name "${item.name}" in folders "${previous}" and "${folderName}"`);
|
|
337
|
+
}
|
|
338
|
+
seenNames.set(item.name, folderName);
|
|
339
|
+
}
|
|
314
340
|
discovered.sort((a, b) => {
|
|
315
341
|
if (a.item.priority !== b.item.priority) {
|
|
316
342
|
return a.item.priority - b.item.priority;
|
|
@@ -341,7 +367,7 @@ function folderBacklogContexts({ backlogItemsDir, parseItem, parseContext, }) {
|
|
|
341
367
|
function isPlainObject(value) {
|
|
342
368
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
343
369
|
}
|
|
344
|
-
/** Moves a finished backlog item folder from todo/ to
|
|
370
|
+
/** Moves a finished backlog item folder from todo/ to the same relative path under completed/. */
|
|
345
371
|
function folderSetTaskDoneStep(input) {
|
|
346
372
|
const nameVarName = input.nameVarName ?? 'TASK_NAME';
|
|
347
373
|
return {
|
|
@@ -349,19 +375,29 @@ function folderSetTaskDoneStep(input) {
|
|
|
349
375
|
const variables = context.variables;
|
|
350
376
|
const itemsDirRelative = variables[input.itemsDirVarName];
|
|
351
377
|
const taskName = variables[nameVarName];
|
|
352
|
-
|
|
378
|
+
const itemDirRelative = variables.BACKLOG_ITEM_DIR;
|
|
379
|
+
if (!itemsDirRelative || (!itemDirRelative && !taskName)) {
|
|
353
380
|
throw new Error('Backlog items directory and task name are required');
|
|
354
381
|
}
|
|
355
382
|
const itemsDir = path$1.join(workspacePath, itemsDirRelative);
|
|
356
|
-
const
|
|
357
|
-
const
|
|
383
|
+
const todoDir = path$1.join(itemsDir, 'todo');
|
|
384
|
+
const fromDir = itemDirRelative
|
|
385
|
+
? path$1.join(workspacePath, itemDirRelative)
|
|
386
|
+
: path$1.join(todoDir, taskName);
|
|
387
|
+
const relativeFromTodo = path$1.relative(todoDir, fromDir);
|
|
388
|
+
if (relativeFromTodo === '' ||
|
|
389
|
+
relativeFromTodo.startsWith('..') ||
|
|
390
|
+
path$1.isAbsolute(relativeFromTodo)) {
|
|
391
|
+
throw new Error(`BACKLOG_ITEM_DIR must be a folder under ${todoDir}: ${itemDirRelative ?? fromDir}`);
|
|
392
|
+
}
|
|
393
|
+
const toDir = path$1.join(itemsDir, 'completed', relativeFromTodo);
|
|
358
394
|
const descPath = path$1.join(fromDir, 'desc.yml');
|
|
359
395
|
const completedDescPath = path$1.join(toDir, 'desc.yml');
|
|
360
396
|
if (!(await core.pathExists(fromDir))) {
|
|
361
397
|
return null;
|
|
362
398
|
}
|
|
363
399
|
if (await core.pathExists(toDir)) {
|
|
364
|
-
console.warn(`[lumpcode/recipes] Cannot move backlog item "${taskName}": already exists at ${toDir}`);
|
|
400
|
+
console.warn(`[lumpcode/recipes] Cannot move backlog item "${taskName ?? relativeFromTodo}": already exists at ${toDir}`);
|
|
365
401
|
return null;
|
|
366
402
|
}
|
|
367
403
|
const rawText = await fs.readFile(descPath, 'utf-8');
|
|
@@ -373,7 +409,7 @@ function folderSetTaskDoneStep(input) {
|
|
|
373
409
|
...raw,
|
|
374
410
|
completedAt: new Date().toISOString(),
|
|
375
411
|
};
|
|
376
|
-
await fs.mkdir(path$1.
|
|
412
|
+
await fs.mkdir(path$1.dirname(toDir), { recursive: true });
|
|
377
413
|
await fs.rename(fromDir, toDir);
|
|
378
414
|
await fs.writeFile(completedDescPath, jsYaml.dump(updated));
|
|
379
415
|
return {
|
|
@@ -622,29 +658,72 @@ const abstractionBacklog = defineRecipe(function abstractionBacklog(options) {
|
|
|
622
658
|
});
|
|
623
659
|
});
|
|
624
660
|
|
|
661
|
+
function scanCommandStep(scanCommand) {
|
|
662
|
+
if (typeof scanCommand === 'function') {
|
|
663
|
+
return { commandFn: scanCommand };
|
|
664
|
+
}
|
|
665
|
+
if (typeof scanCommand === 'string') {
|
|
666
|
+
return { commandFn: () => shellCommand(scanCommand) };
|
|
667
|
+
}
|
|
668
|
+
return { commandFn: () => scanCommand };
|
|
669
|
+
}
|
|
625
670
|
const abstractionFinder = defineRecipe(function abstractionFinder(options) {
|
|
626
|
-
const { maxPendingAbstractions = 5, scanDirectories, customPrompt, backlogItemsDir,
|
|
671
|
+
const { configUrl, maxPendingAbstractions = 5, scanDirectories, utilDir, customPrompt, backlogItemsDir, scanCommand, steps: stepsOverride, ...rest } = options;
|
|
672
|
+
if (path$1.isAbsolute(backlogItemsDir)) {
|
|
673
|
+
throw new Error(`backlogItemsDir must be project-root-relative, not absolute: ${backlogItemsDir}`);
|
|
674
|
+
}
|
|
675
|
+
const projectRoot = projectRootFromConfigUrl(configUrl);
|
|
676
|
+
const todoDir = path$1.join(projectRoot, backlogItemsDir, 'todo');
|
|
677
|
+
const agentSteps = stepsOverride ?? buildFinderPrompt({
|
|
678
|
+
backlogItemsDir,
|
|
679
|
+
scanDirectories,
|
|
680
|
+
utilDir,
|
|
681
|
+
customPrompt,
|
|
682
|
+
});
|
|
683
|
+
const steps = [
|
|
684
|
+
...(scanCommand === undefined ? [] : [scanCommandStep(scanCommand)]),
|
|
685
|
+
...cliUtils.normalizeSteps({
|
|
686
|
+
prompt: undefined,
|
|
687
|
+
jsSteps: agentSteps,
|
|
688
|
+
}),
|
|
689
|
+
];
|
|
627
690
|
return cliUtils.defineConfig({
|
|
628
|
-
...
|
|
691
|
+
...rest,
|
|
629
692
|
getContextListFn: ephemeralContextListFn({
|
|
630
|
-
contextCount
|
|
693
|
+
async contextCount() {
|
|
694
|
+
const pending = (await listTodoRelativeDirs(todoDir)).length;
|
|
695
|
+
return pending >= maxPendingAbstractions ? 0 : 1;
|
|
696
|
+
},
|
|
631
697
|
variables: {
|
|
632
698
|
BACKLOG_ITEMS_DIR: backlogItemsDir,
|
|
633
699
|
},
|
|
634
700
|
}),
|
|
635
|
-
steps
|
|
701
|
+
steps,
|
|
636
702
|
});
|
|
637
703
|
});
|
|
638
|
-
function buildFinderPrompt({ backlogItemsDir, scanDirectories, customPrompt, }) {
|
|
639
|
-
|
|
640
|
-
|
|
704
|
+
function buildFinderPrompt({ backlogItemsDir, scanDirectories, utilDir, customPrompt, }) {
|
|
705
|
+
if (customPrompt) {
|
|
706
|
+
return customPrompt();
|
|
707
|
+
}
|
|
708
|
+
const scanLabel = scanDirectories && scanDirectories.length > 0
|
|
709
|
+
? scanDirectories.map((dir) => `@${dir}`).join(' and ')
|
|
710
|
+
: 'the codebase';
|
|
711
|
+
const materializeDir = utilDir ?? scanDirectories?.[0];
|
|
712
|
+
const materializeLine = materializeDir
|
|
713
|
+
? `- Would materialize as a new util under @${materializeDir}/<utilName>/, following existing conventions there.`
|
|
714
|
+
: '- Would materialize as a new util in the scanned tree, following existing conventions there.';
|
|
715
|
+
const refactorScope = scanDirectories && scanDirectories.length > 0
|
|
716
|
+
? scanDirectories.join(' and ')
|
|
717
|
+
: 'the scanned tree';
|
|
718
|
+
return `
|
|
719
|
+
Scan ${scanLabel} for duplicated logic that appears in multiple places (same pattern, not merely similar file structure).
|
|
641
720
|
|
|
642
721
|
List existing backlog item names under @${backlogItemsDir}/todo/ and @${backlogItemsDir}/completed/. Do not propose abstractions whose util name already appears in either directory.
|
|
643
722
|
|
|
644
723
|
Pick exactly one new abstraction that:
|
|
645
724
|
- Has a clear util name matching ^[a-zA-Z0-9_-]+$ that describes the pattern it captures.
|
|
646
|
-
|
|
647
|
-
- Would shrink the codebase: refactoring all call sites in
|
|
725
|
+
${materializeLine}
|
|
726
|
+
- Would shrink the codebase: refactoring all call sites in ${refactorScope} should reduce net line count (excluding new unit tests).
|
|
648
727
|
|
|
649
728
|
Create exactly one new backlog item folder at @${backlogItemsDir}/todo/<utilName>/ with:
|
|
650
729
|
- desc.yml containing:
|
|
@@ -664,6 +743,11 @@ Do not take too much time looking for every possible abstraction. Once you found
|
|
|
664
743
|
`.trim();
|
|
665
744
|
}
|
|
666
745
|
|
|
746
|
+
const FEATURE_BACKLOG_WORKFLOWS = [
|
|
747
|
+
'tdd',
|
|
748
|
+
'directImpl',
|
|
749
|
+
'manual',
|
|
750
|
+
];
|
|
667
751
|
const RESERVED_NAME_SUFFIXES = ['_req', '_testPlan', '_tests_impl'];
|
|
668
752
|
function assertValidFeatureItemName(name) {
|
|
669
753
|
for (const suffix of RESERVED_NAME_SUFFIXES) {
|
|
@@ -672,6 +756,9 @@ function assertValidFeatureItemName(name) {
|
|
|
672
756
|
}
|
|
673
757
|
}
|
|
674
758
|
}
|
|
759
|
+
function featureItemContextBaseName(item) {
|
|
760
|
+
return item.parentName ? `${item.parentName}-${item.name}` : item.name;
|
|
761
|
+
}
|
|
675
762
|
function featureContextName(itemName, stage) {
|
|
676
763
|
switch (stage) {
|
|
677
764
|
case 'makeReq':
|
|
@@ -681,6 +768,7 @@ function featureContextName(itemName, stage) {
|
|
|
681
768
|
case 'testImpl':
|
|
682
769
|
return `${itemName}_tests_impl`;
|
|
683
770
|
case 'implementation':
|
|
771
|
+
case 'directImpl':
|
|
684
772
|
return itemName;
|
|
685
773
|
default: {
|
|
686
774
|
const _exhaustive = stage;
|
|
@@ -688,9 +776,84 @@ function featureContextName(itemName, stage) {
|
|
|
688
776
|
}
|
|
689
777
|
}
|
|
690
778
|
}
|
|
691
|
-
|
|
692
|
-
const
|
|
693
|
-
|
|
779
|
+
function parentNameFromTodoRelativeDir(todoRelativeDir) {
|
|
780
|
+
const parts = todoRelativeDir.split('/');
|
|
781
|
+
if (parts.length === 3 && parts[1] === 'tickets') {
|
|
782
|
+
return parts[0];
|
|
783
|
+
}
|
|
784
|
+
return undefined;
|
|
785
|
+
}
|
|
786
|
+
function parseFeatureWorkflow(itemName, raw) {
|
|
787
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
788
|
+
return undefined;
|
|
789
|
+
}
|
|
790
|
+
const record = raw;
|
|
791
|
+
if (record.workflow === undefined) {
|
|
792
|
+
return undefined;
|
|
793
|
+
}
|
|
794
|
+
if (typeof record.workflow !== 'string' ||
|
|
795
|
+
!FEATURE_BACKLOG_WORKFLOWS.includes(record.workflow)) {
|
|
796
|
+
throw new Error(`Backlog item "${itemName}" field "workflow" must be one of: ${FEATURE_BACKLOG_WORKFLOWS.join(', ')}`);
|
|
797
|
+
}
|
|
798
|
+
return record.workflow;
|
|
799
|
+
}
|
|
800
|
+
async function resolveItemWorkflow(input) {
|
|
801
|
+
const { item, paths, projectRoot } = input;
|
|
802
|
+
if (item.workflow !== undefined) {
|
|
803
|
+
return item.workflow;
|
|
804
|
+
}
|
|
805
|
+
if (item.parentName === undefined) {
|
|
806
|
+
return 'tdd';
|
|
807
|
+
}
|
|
808
|
+
const parentDescPath = path$1.join(projectRoot, paths.backlogItemsDir, 'todo', item.parentName, 'desc.yml');
|
|
809
|
+
let rawText;
|
|
810
|
+
try {
|
|
811
|
+
rawText = await fs$1.readFile(parentDescPath, 'utf-8');
|
|
812
|
+
}
|
|
813
|
+
catch (error) {
|
|
814
|
+
const err = error;
|
|
815
|
+
if (err.code === 'ENOENT') {
|
|
816
|
+
return 'tdd';
|
|
817
|
+
}
|
|
818
|
+
throw error;
|
|
819
|
+
}
|
|
820
|
+
return parseFeatureWorkflow(item.parentName, jsYaml.load(rawText)) ?? 'tdd';
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* `dev` → only top-level `directImpl` (tickets never run on `dev`, even if `directImpl`).
|
|
824
|
+
* `feature/<key>` → exact item name, or the parent todo name for tickets.
|
|
825
|
+
* `manual` never reaches here (`resolveFeatureBacklogItem` ignores it first).
|
|
826
|
+
*/
|
|
827
|
+
function itemMatchesDiscoveryBranch(input) {
|
|
828
|
+
const { itemName, parentName, discoveryBranch, workflow } = input;
|
|
829
|
+
if (discoveryBranch === 'dev') {
|
|
830
|
+
return workflow === 'directImpl' && parentName === undefined;
|
|
831
|
+
}
|
|
832
|
+
if (!discoveryBranch.startsWith('feature/')) {
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
const key = discoveryBranch.slice('feature/'.length);
|
|
836
|
+
return (parentName ?? itemName) === key;
|
|
837
|
+
}
|
|
838
|
+
async function resolveFeatureBacklogItem(input) {
|
|
839
|
+
const { item, paths, projectRoot, discoveryBranch } = input;
|
|
840
|
+
const contextBaseName = featureItemContextBaseName(item);
|
|
841
|
+
const workflow = await resolveItemWorkflow({ item, paths, projectRoot });
|
|
842
|
+
if (workflow === 'manual') {
|
|
843
|
+
return { ignored: true };
|
|
844
|
+
}
|
|
845
|
+
if (!!item.completedAt ||
|
|
846
|
+
!itemMatchesDiscoveryBranch({
|
|
847
|
+
itemName: item.name,
|
|
848
|
+
parentName: item.parentName,
|
|
849
|
+
discoveryBranch,
|
|
850
|
+
workflow,
|
|
851
|
+
})) {
|
|
852
|
+
return { ignored: true };
|
|
853
|
+
}
|
|
854
|
+
const itemDir = path$1.join(paths.backlogItemsDir, 'todo', item.todoRelativeDir);
|
|
855
|
+
const reqFilePath = path$1.join(itemDir, 'requirements.md');
|
|
856
|
+
const testPlanFilePath = path$1.join(itemDir, 'testPlan.md');
|
|
694
857
|
const hasReq = await core.pathExists(path$1.join(projectRoot, reqFilePath));
|
|
695
858
|
if (!hasReq) {
|
|
696
859
|
if (item.manualReq === true) {
|
|
@@ -698,7 +861,14 @@ async function resolveFeatureBacklogItem(item, paths, projectRoot, lumpName, bas
|
|
|
698
861
|
}
|
|
699
862
|
return {
|
|
700
863
|
stage: 'makeReq',
|
|
701
|
-
contextName: featureContextName(
|
|
864
|
+
contextName: featureContextName(contextBaseName, 'makeReq'),
|
|
865
|
+
variables: { REQ_FILE: reqFilePath },
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
if (workflow === 'directImpl') {
|
|
869
|
+
return {
|
|
870
|
+
stage: 'directImpl',
|
|
871
|
+
contextName: featureContextName(contextBaseName, 'directImpl'),
|
|
702
872
|
variables: { REQ_FILE: reqFilePath },
|
|
703
873
|
};
|
|
704
874
|
}
|
|
@@ -706,24 +876,24 @@ async function resolveFeatureBacklogItem(item, paths, projectRoot, lumpName, bas
|
|
|
706
876
|
if (!hasTestPlan) {
|
|
707
877
|
return {
|
|
708
878
|
stage: 'makeTestPlan',
|
|
709
|
-
contextName: featureContextName(
|
|
879
|
+
contextName: featureContextName(contextBaseName, 'makeTestPlan'),
|
|
710
880
|
variables: {
|
|
711
881
|
REQ_FILE: reqFilePath,
|
|
712
882
|
TEST_PLAN_FILE: testPlanFilePath,
|
|
713
883
|
},
|
|
714
884
|
};
|
|
715
885
|
}
|
|
716
|
-
const testsImplContextName = featureContextName(
|
|
886
|
+
const testsImplContextName = featureContextName(contextBaseName, 'testImpl');
|
|
717
887
|
const testsImplStatus = await cliUtils.getContextStatus({
|
|
718
888
|
projectRoot,
|
|
719
889
|
contextName: testsImplContextName,
|
|
720
|
-
lumpName,
|
|
721
|
-
baseBranch,
|
|
890
|
+
lumpName: paths.lumpName,
|
|
891
|
+
baseBranch: discoveryBranch,
|
|
722
892
|
});
|
|
723
893
|
if (testsImplStatus === 'finished') {
|
|
724
894
|
return {
|
|
725
895
|
stage: 'implementation',
|
|
726
|
-
contextName: featureContextName(
|
|
896
|
+
contextName: featureContextName(contextBaseName, 'implementation'),
|
|
727
897
|
variables: {
|
|
728
898
|
REQ_FILE: reqFilePath,
|
|
729
899
|
TEST_PLAN_FILE: testPlanFilePath,
|
|
@@ -743,27 +913,38 @@ async function resolveFeatureBacklogItem(item, paths, projectRoot, lumpName, bas
|
|
|
743
913
|
};
|
|
744
914
|
}
|
|
745
915
|
const featureBacklog = defineRecipe(function featureBacklog(options) {
|
|
746
|
-
const { configUrl,
|
|
916
|
+
const { configUrl, implValidateCommand, backlogItemsDir, ...rest } = options;
|
|
747
917
|
const projectRoot = projectRootFromConfigUrl(configUrl);
|
|
748
918
|
const runImplValidation = resolveImplValidateCommand(implValidateCommand ??
|
|
749
919
|
'echo "No implementation validation command provided. I say, trust but verify, but well..."');
|
|
750
920
|
return backlog({
|
|
751
921
|
configUrl,
|
|
752
922
|
backlogItemsDir,
|
|
753
|
-
|
|
754
|
-
parseItem(baseItem, _folderName, raw) {
|
|
923
|
+
parseItem(baseItem, folderName, raw) {
|
|
755
924
|
assertValidFeatureItemName(baseItem.name);
|
|
756
925
|
const record = raw;
|
|
757
926
|
if (record.manualReq !== undefined && typeof record.manualReq !== 'boolean') {
|
|
758
927
|
throw new Error(`Backlog item "${baseItem.name}" field "manualReq" must be a boolean`);
|
|
759
928
|
}
|
|
929
|
+
const parentName = parentNameFromTodoRelativeDir(folderName);
|
|
760
930
|
return {
|
|
761
931
|
...baseItem,
|
|
932
|
+
todoRelativeDir: folderName,
|
|
933
|
+
parentName,
|
|
934
|
+
dependsOn: parentName
|
|
935
|
+
? baseItem.dependsOn?.map((dep) => `${parentName}-${dep}`)
|
|
936
|
+
: baseItem.dependsOn,
|
|
762
937
|
manualReq: record.manualReq === true ? true : undefined,
|
|
938
|
+
workflow: parseFeatureWorkflow(baseItem.name, raw),
|
|
763
939
|
};
|
|
764
940
|
},
|
|
765
|
-
async resolveItem({ item, paths }) {
|
|
766
|
-
return resolveFeatureBacklogItem(
|
|
941
|
+
async resolveItem({ item, paths, discoveryBranch }) {
|
|
942
|
+
return resolveFeatureBacklogItem({
|
|
943
|
+
item,
|
|
944
|
+
paths,
|
|
945
|
+
projectRoot,
|
|
946
|
+
discoveryBranch,
|
|
947
|
+
});
|
|
767
948
|
},
|
|
768
949
|
stages: {
|
|
769
950
|
makeReq: {
|
|
@@ -912,6 +1093,25 @@ The implementation should make the tests pass. Do not edit any test file except
|
|
|
912
1093
|
validationCommandFn: runImplValidation,
|
|
913
1094
|
}),
|
|
914
1095
|
},
|
|
1096
|
+
directImpl: {
|
|
1097
|
+
completion: 'moveToDone',
|
|
1098
|
+
steps: retryUntilGreen({
|
|
1099
|
+
steps: [
|
|
1100
|
+
{
|
|
1101
|
+
promptFn({ context: ctx }) {
|
|
1102
|
+
const vars = ctx.variables;
|
|
1103
|
+
const { REQ_FILE } = vars;
|
|
1104
|
+
return `
|
|
1105
|
+
Implement the feature described in @${REQ_FILE}.
|
|
1106
|
+
Add or update tests as needed so the suite covers the change, and make validation pass.
|
|
1107
|
+
Do not edit @${REQ_FILE} unless absolutely necessary.
|
|
1108
|
+
`.trim();
|
|
1109
|
+
},
|
|
1110
|
+
},
|
|
1111
|
+
],
|
|
1112
|
+
validationCommandFn: runImplValidation,
|
|
1113
|
+
}),
|
|
1114
|
+
},
|
|
915
1115
|
},
|
|
916
1116
|
...rest,
|
|
917
1117
|
});
|
|
@@ -922,6 +1122,7 @@ exports.BACKLOG_ITEM_DIR_VAR = BACKLOG_ITEM_DIR_VAR;
|
|
|
922
1122
|
exports.BACKLOG_STAGE_VAR = BACKLOG_STAGE_VAR;
|
|
923
1123
|
exports.BACKLOG_TASK_NAME_VAR = BACKLOG_TASK_NAME_VAR;
|
|
924
1124
|
exports.BACKLOG_TASK_VAR = BACKLOG_TASK_VAR;
|
|
1125
|
+
exports.FEATURE_BACKLOG_WORKFLOWS = FEATURE_BACKLOG_WORKFLOWS;
|
|
925
1126
|
exports.abstractionBacklog = abstractionBacklog;
|
|
926
1127
|
exports.abstractionFinder = abstractionFinder;
|
|
927
1128
|
exports.backlog = backlog;
|
|
@@ -932,8 +1133,10 @@ exports.featureBacklog = featureBacklog;
|
|
|
932
1133
|
exports.folderBacklogContexts = folderBacklogContexts;
|
|
933
1134
|
exports.folderSetTaskDoneStep = folderSetTaskDoneStep;
|
|
934
1135
|
exports.getRecursiveSteps = getRecursiveSteps;
|
|
1136
|
+
exports.listTodoRelativeDirs = listTodoRelativeDirs;
|
|
935
1137
|
exports.lumpPathAndName = lumpPathAndName;
|
|
936
1138
|
exports.normalizeMaybePromGetter = normalizeMaybePromGetter;
|
|
1139
|
+
exports.parseFeatureWorkflow = parseFeatureWorkflow;
|
|
937
1140
|
exports.projectRootFromConfigUrl = projectRootFromConfigUrl;
|
|
938
1141
|
exports.requireArtifactStep = requireArtifactStep;
|
|
939
1142
|
exports.resolveBacklogPaths = resolveBacklogPaths;
|
package/dist/index.d.ts
CHANGED
|
@@ -92,15 +92,21 @@ type DoneBacklogItem<T extends BaseBacklogItem = BaseBacklogItem> = T & {
|
|
|
92
92
|
|
|
93
93
|
type FolderBacklogContextsOptions<Item extends BaseBacklogItem = BaseBacklogItem> = {
|
|
94
94
|
backlogItemsDir: string;
|
|
95
|
+
/**
|
|
96
|
+
* `folderName` is the path relative to `todo/`: the item folder, or
|
|
97
|
+
* `<parent>/tickets/<ticket>` when the parent has a `tickets/` directory.
|
|
98
|
+
*/
|
|
95
99
|
parseItem?: (item: BaseBacklogItem, folderName: string, raw: unknown) => Item;
|
|
96
100
|
parseContext?: (item: Item, folderName: string) => MaybePromise$1<{
|
|
97
101
|
parsed?: Partial<Context>;
|
|
98
102
|
ignored?: boolean;
|
|
99
103
|
}>;
|
|
100
104
|
};
|
|
105
|
+
/** Path relative to `todo/`, using `/` so it is stable in context variables. */
|
|
106
|
+
declare function listTodoRelativeDirs(todoDir: string): Promise<string[]>;
|
|
101
107
|
declare function folderBacklogContexts<Item extends BaseBacklogItem = BaseBacklogItem, V extends LumpVariables = LumpVariables>({ backlogItemsDir, parseItem, parseContext, }: FolderBacklogContextsOptions<Item>): GetContextListFn<V>;
|
|
102
108
|
|
|
103
|
-
/** Moves a finished backlog item folder from todo/ to
|
|
109
|
+
/** Moves a finished backlog item folder from todo/ to the same relative path under completed/. */
|
|
104
110
|
declare function folderSetTaskDoneStep<V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables>(input: {
|
|
105
111
|
itemsDirVarName: string;
|
|
106
112
|
nameVarName?: string;
|
|
@@ -136,12 +142,20 @@ type AbstractionBacklogOptions<V extends LumpVariables = LumpVariables, SV exten
|
|
|
136
142
|
} & Omit<LumpJsConfig<V, SV>, 'contextListJson' | 'contextMatchFn' | 'getContextListFn' | 'prompt' | 'steps'>;
|
|
137
143
|
declare const abstractionBacklog: <V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables>(options: AbstractionBacklogOptions<V, SV>) => LumpJsConfig<V, SV>;
|
|
138
144
|
|
|
145
|
+
type AbstractionFinderScanCommand<V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables> = string | CommandDescriptor | CommandFn<V, SV>;
|
|
139
146
|
type AbstractionFinderOptions<V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables> = {
|
|
147
|
+
/** Lump config module URL — pass `import.meta.url` from `config.ts`. */
|
|
148
|
+
configUrl: string | URL;
|
|
140
149
|
scanDirectories?: string[];
|
|
150
|
+
/** Optional util output directory (project-root-relative). Defaults to the first scan directory. */
|
|
151
|
+
utilDir?: string;
|
|
141
152
|
customPrompt?(): string;
|
|
153
|
+
/** Max unmerged items under `backlogItemsDir/todo/`. Finder emits one context per tick while under the cap. */
|
|
142
154
|
maxPendingAbstractions?: number;
|
|
143
155
|
backlogItemsDir: string;
|
|
144
|
-
|
|
156
|
+
/** Optional scanner command prepended before the prompt (e.g. a dupes report). */
|
|
157
|
+
scanCommand?: AbstractionFinderScanCommand<V, SV>;
|
|
158
|
+
} & Omit<LumpJsConfig<V, SV>, 'contextListJson' | 'contextMatchFn' | 'getContextListFn' | 'prompt'>;
|
|
145
159
|
declare const abstractionFinder: <V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables>(options: AbstractionFinderOptions<V, SV>) => LumpJsConfig<V, SV>;
|
|
146
160
|
|
|
147
161
|
type BacklogStageDefinition<V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables> = {
|
|
@@ -178,10 +192,18 @@ declare const BACKLOG_ITEM_DIR_VAR = "BACKLOG_ITEM_DIR";
|
|
|
178
192
|
declare function backlog<Item extends BaseBacklogItem, V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables, Stages extends Record<string, BacklogStageDefinition<V, SV>> = Record<string, BacklogStageDefinition<V, SV>>>(options: BacklogOptions<Item, V, SV, Stages>): LumpJsConfig<V, SV>;
|
|
179
193
|
declare const backlogRecipe: typeof backlog;
|
|
180
194
|
|
|
195
|
+
type FeatureBacklogWorkflow = 'tdd' | 'directImpl' | 'manual';
|
|
196
|
+
type FeatureBacklogRunnableWorkflow = Exclude<FeatureBacklogWorkflow, 'manual'>;
|
|
181
197
|
type FeatureBacklogItem = BaseBacklogItem & {
|
|
182
198
|
manualReq?: boolean;
|
|
199
|
+
workflow?: FeatureBacklogWorkflow;
|
|
200
|
+
completedAt?: string;
|
|
201
|
+
/** Path relative to `backlogItems/todo/`; tickets live at `<parent>/tickets/<name>`. */
|
|
202
|
+
todoRelativeDir: string;
|
|
203
|
+
/** Parent todo folder name when this item is a ticket. */
|
|
204
|
+
parentName?: string;
|
|
183
205
|
};
|
|
184
|
-
type FeatureBacklogStage = 'makeReq' | 'makeTestPlan' | 'testImpl' | 'implementation';
|
|
206
|
+
type FeatureBacklogStage = 'makeReq' | 'makeTestPlan' | 'testImpl' | 'implementation' | 'directImpl';
|
|
185
207
|
type FeatureBacklogContextVariables = {
|
|
186
208
|
TASK_NAME: string;
|
|
187
209
|
TASK: string;
|
|
@@ -193,12 +215,18 @@ type FeatureBacklogContextVariables = {
|
|
|
193
215
|
};
|
|
194
216
|
type FeatureBacklogOptions<V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables> = {
|
|
195
217
|
configUrl: string | URL;
|
|
196
|
-
baseBranch: string;
|
|
197
218
|
implValidateCommand?: ValidationCommandFn<V, SV> | string;
|
|
198
219
|
backlogItemsDir?: string;
|
|
199
|
-
} & Omit<LumpJsConfig<V, SV>, 'contextListJson' | 'contextMatchFn' | 'getContextListFn' | 'prompt' | 'steps'
|
|
200
|
-
declare
|
|
220
|
+
} & Omit<LumpJsConfig<V, SV>, 'contextListJson' | 'contextMatchFn' | 'getContextListFn' | 'prompt' | 'steps'>;
|
|
221
|
+
declare const FEATURE_BACKLOG_WORKFLOWS: readonly ["tdd", "directImpl", "manual"];
|
|
222
|
+
declare function parseFeatureWorkflow(itemName: string, raw: unknown): FeatureBacklogWorkflow | undefined;
|
|
223
|
+
declare function resolveFeatureBacklogItem(input: {
|
|
224
|
+
item: FeatureBacklogItem;
|
|
225
|
+
paths: BacklogPaths;
|
|
226
|
+
projectRoot: string;
|
|
227
|
+
discoveryBranch: string;
|
|
228
|
+
}): Promise<BacklogItemResolution<FeatureBacklogStage>>;
|
|
201
229
|
declare const featureBacklog: <V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables>(options: FeatureBacklogOptions<V, SV>) => LumpJsConfig<V, SV>;
|
|
202
230
|
|
|
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 };
|
|
231
|
+
export { BACKLOG_ITEMS_DIR_VAR, BACKLOG_ITEM_DIR_VAR, BACKLOG_STAGE_VAR, BACKLOG_TASK_NAME_VAR, BACKLOG_TASK_VAR, FEATURE_BACKLOG_WORKFLOWS, abstractionBacklog, abstractionFinder, backlog, backlogRecipe, defineRecipe, ephemeralContextListFn, featureBacklog, folderBacklogContexts, folderSetTaskDoneStep, getRecursiveSteps, listTodoRelativeDirs, lumpPathAndName, normalizeMaybePromGetter, parseFeatureWorkflow, projectRootFromConfigUrl, requireArtifactStep, resolveBacklogPaths, resolveFeatureBacklogItem, resolveImplValidateCommand, retryUntilGreen, setTaskDoneStep, shellCommand, validateBaseBacklogItem, ymlBacklogContexts };
|
|
232
|
+
export type { AbstractionBacklogOptions, AbstractionFinderOptions, AbstractionFinderScanCommand, BacklogItemResolution, BacklogOptions, BacklogPaths, BacklogStageDefinition, BaseBacklogItem, DoneBacklogItem, EphemeralContextListFnOptions, FeatureBacklogContextVariables, FeatureBacklogItem, FeatureBacklogOptions, FeatureBacklogRunnableWorkflow, FeatureBacklogStage, FeatureBacklogWorkflow, FolderBacklogContextsOptions, GetFirstStepsInput, GetRecursiveStepsOptions, ImplValidateCommand, IsValidationCommandResultOkInput, MaybePromGetter, Recipe, RetryUntilGreenInput, StepIndex, ValidationCommandFn, ValidationCommandFnInput, YmlBacklogContextsOptions };
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import path$1 from 'node:path';
|
|
|
6
6
|
import { fileURLToPath as fileURLToPath$1 } from 'node:url';
|
|
7
7
|
import fs from 'fs/promises';
|
|
8
8
|
import { load, dump } from 'js-yaml';
|
|
9
|
+
import fs$1 from 'node:fs/promises';
|
|
9
10
|
|
|
10
11
|
/** Run a shell script via `sh -c` (portable on Unix-like systems and Git Bash on Windows). */
|
|
11
12
|
function shellCommand(script) {
|
|
@@ -284,10 +285,26 @@ async function listTodoFolderNames(todoDir) {
|
|
|
284
285
|
throw error;
|
|
285
286
|
}
|
|
286
287
|
}
|
|
288
|
+
/** Path relative to `todo/`, using `/` so it is stable in context variables. */
|
|
289
|
+
async function listTodoRelativeDirs(todoDir) {
|
|
290
|
+
const topNames = await listTodoFolderNames(todoDir);
|
|
291
|
+
const relativeDirs = [];
|
|
292
|
+
for (const name of topNames) {
|
|
293
|
+
const ticketNames = await listTodoFolderNames(path$1.join(todoDir, name, 'tickets'));
|
|
294
|
+
if (ticketNames.length === 0) {
|
|
295
|
+
relativeDirs.push(name);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
for (const ticketName of ticketNames) {
|
|
299
|
+
relativeDirs.push(`${name}/tickets/${ticketName}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return relativeDirs;
|
|
303
|
+
}
|
|
287
304
|
function folderBacklogContexts({ backlogItemsDir, parseItem, parseContext, }) {
|
|
288
305
|
return async () => {
|
|
289
306
|
const todoDir = path$1.join(backlogItemsDir, 'todo');
|
|
290
|
-
const folderNames = await
|
|
307
|
+
const folderNames = await listTodoRelativeDirs(todoDir);
|
|
291
308
|
const discovered = await Promise.all(folderNames.map(async (folderName) => {
|
|
292
309
|
const descPath = path$1.join(todoDir, folderName, 'desc.yml');
|
|
293
310
|
let rawText;
|
|
@@ -302,13 +319,22 @@ function folderBacklogContexts({ backlogItemsDir, parseItem, parseContext, }) {
|
|
|
302
319
|
throw error;
|
|
303
320
|
}
|
|
304
321
|
const raw = load(rawText);
|
|
322
|
+
const itemFolderName = path$1.basename(folderName);
|
|
305
323
|
const baseItem = validateBaseBacklogItem(raw, `in folder "${folderName}"`);
|
|
306
|
-
if (baseItem.name !==
|
|
324
|
+
if (baseItem.name !== itemFolderName) {
|
|
307
325
|
throw new Error(`Backlog item folder "${folderName}" desc.yml name "${baseItem.name}" must match folder name`);
|
|
308
326
|
}
|
|
309
327
|
const item = parseItem ? parseItem(baseItem, folderName, raw) : baseItem;
|
|
310
328
|
return { item, folderName };
|
|
311
329
|
}));
|
|
330
|
+
const seenNames = new Map();
|
|
331
|
+
for (const { item, folderName } of discovered) {
|
|
332
|
+
const previous = seenNames.get(item.name);
|
|
333
|
+
if (previous !== undefined) {
|
|
334
|
+
throw new Error(`Duplicate backlog item name "${item.name}" in folders "${previous}" and "${folderName}"`);
|
|
335
|
+
}
|
|
336
|
+
seenNames.set(item.name, folderName);
|
|
337
|
+
}
|
|
312
338
|
discovered.sort((a, b) => {
|
|
313
339
|
if (a.item.priority !== b.item.priority) {
|
|
314
340
|
return a.item.priority - b.item.priority;
|
|
@@ -339,7 +365,7 @@ function folderBacklogContexts({ backlogItemsDir, parseItem, parseContext, }) {
|
|
|
339
365
|
function isPlainObject(value) {
|
|
340
366
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
341
367
|
}
|
|
342
|
-
/** Moves a finished backlog item folder from todo/ to
|
|
368
|
+
/** Moves a finished backlog item folder from todo/ to the same relative path under completed/. */
|
|
343
369
|
function folderSetTaskDoneStep(input) {
|
|
344
370
|
const nameVarName = input.nameVarName ?? 'TASK_NAME';
|
|
345
371
|
return {
|
|
@@ -347,19 +373,29 @@ function folderSetTaskDoneStep(input) {
|
|
|
347
373
|
const variables = context.variables;
|
|
348
374
|
const itemsDirRelative = variables[input.itemsDirVarName];
|
|
349
375
|
const taskName = variables[nameVarName];
|
|
350
|
-
|
|
376
|
+
const itemDirRelative = variables.BACKLOG_ITEM_DIR;
|
|
377
|
+
if (!itemsDirRelative || (!itemDirRelative && !taskName)) {
|
|
351
378
|
throw new Error('Backlog items directory and task name are required');
|
|
352
379
|
}
|
|
353
380
|
const itemsDir = path$1.join(workspacePath, itemsDirRelative);
|
|
354
|
-
const
|
|
355
|
-
const
|
|
381
|
+
const todoDir = path$1.join(itemsDir, 'todo');
|
|
382
|
+
const fromDir = itemDirRelative
|
|
383
|
+
? path$1.join(workspacePath, itemDirRelative)
|
|
384
|
+
: path$1.join(todoDir, taskName);
|
|
385
|
+
const relativeFromTodo = path$1.relative(todoDir, fromDir);
|
|
386
|
+
if (relativeFromTodo === '' ||
|
|
387
|
+
relativeFromTodo.startsWith('..') ||
|
|
388
|
+
path$1.isAbsolute(relativeFromTodo)) {
|
|
389
|
+
throw new Error(`BACKLOG_ITEM_DIR must be a folder under ${todoDir}: ${itemDirRelative ?? fromDir}`);
|
|
390
|
+
}
|
|
391
|
+
const toDir = path$1.join(itemsDir, 'completed', relativeFromTodo);
|
|
356
392
|
const descPath = path$1.join(fromDir, 'desc.yml');
|
|
357
393
|
const completedDescPath = path$1.join(toDir, 'desc.yml');
|
|
358
394
|
if (!(await pathExists(fromDir))) {
|
|
359
395
|
return null;
|
|
360
396
|
}
|
|
361
397
|
if (await pathExists(toDir)) {
|
|
362
|
-
console.warn(`[lumpcode/recipes] Cannot move backlog item "${taskName}": already exists at ${toDir}`);
|
|
398
|
+
console.warn(`[lumpcode/recipes] Cannot move backlog item "${taskName ?? relativeFromTodo}": already exists at ${toDir}`);
|
|
363
399
|
return null;
|
|
364
400
|
}
|
|
365
401
|
const rawText = await fs.readFile(descPath, 'utf-8');
|
|
@@ -371,7 +407,7 @@ function folderSetTaskDoneStep(input) {
|
|
|
371
407
|
...raw,
|
|
372
408
|
completedAt: new Date().toISOString(),
|
|
373
409
|
};
|
|
374
|
-
await fs.mkdir(path$1.
|
|
410
|
+
await fs.mkdir(path$1.dirname(toDir), { recursive: true });
|
|
375
411
|
await fs.rename(fromDir, toDir);
|
|
376
412
|
await fs.writeFile(completedDescPath, dump(updated));
|
|
377
413
|
return {
|
|
@@ -620,29 +656,72 @@ const abstractionBacklog = defineRecipe(function abstractionBacklog(options) {
|
|
|
620
656
|
});
|
|
621
657
|
});
|
|
622
658
|
|
|
659
|
+
function scanCommandStep(scanCommand) {
|
|
660
|
+
if (typeof scanCommand === 'function') {
|
|
661
|
+
return { commandFn: scanCommand };
|
|
662
|
+
}
|
|
663
|
+
if (typeof scanCommand === 'string') {
|
|
664
|
+
return { commandFn: () => shellCommand(scanCommand) };
|
|
665
|
+
}
|
|
666
|
+
return { commandFn: () => scanCommand };
|
|
667
|
+
}
|
|
623
668
|
const abstractionFinder = defineRecipe(function abstractionFinder(options) {
|
|
624
|
-
const { maxPendingAbstractions = 5, scanDirectories, customPrompt, backlogItemsDir,
|
|
669
|
+
const { configUrl, maxPendingAbstractions = 5, scanDirectories, utilDir, customPrompt, backlogItemsDir, scanCommand, steps: stepsOverride, ...rest } = options;
|
|
670
|
+
if (path$1.isAbsolute(backlogItemsDir)) {
|
|
671
|
+
throw new Error(`backlogItemsDir must be project-root-relative, not absolute: ${backlogItemsDir}`);
|
|
672
|
+
}
|
|
673
|
+
const projectRoot = projectRootFromConfigUrl(configUrl);
|
|
674
|
+
const todoDir = path$1.join(projectRoot, backlogItemsDir, 'todo');
|
|
675
|
+
const agentSteps = stepsOverride ?? buildFinderPrompt({
|
|
676
|
+
backlogItemsDir,
|
|
677
|
+
scanDirectories,
|
|
678
|
+
utilDir,
|
|
679
|
+
customPrompt,
|
|
680
|
+
});
|
|
681
|
+
const steps = [
|
|
682
|
+
...(scanCommand === undefined ? [] : [scanCommandStep(scanCommand)]),
|
|
683
|
+
...normalizeSteps({
|
|
684
|
+
prompt: undefined,
|
|
685
|
+
jsSteps: agentSteps,
|
|
686
|
+
}),
|
|
687
|
+
];
|
|
625
688
|
return defineConfig({
|
|
626
|
-
...
|
|
689
|
+
...rest,
|
|
627
690
|
getContextListFn: ephemeralContextListFn({
|
|
628
|
-
contextCount
|
|
691
|
+
async contextCount() {
|
|
692
|
+
const pending = (await listTodoRelativeDirs(todoDir)).length;
|
|
693
|
+
return pending >= maxPendingAbstractions ? 0 : 1;
|
|
694
|
+
},
|
|
629
695
|
variables: {
|
|
630
696
|
BACKLOG_ITEMS_DIR: backlogItemsDir,
|
|
631
697
|
},
|
|
632
698
|
}),
|
|
633
|
-
steps
|
|
699
|
+
steps,
|
|
634
700
|
});
|
|
635
701
|
});
|
|
636
|
-
function buildFinderPrompt({ backlogItemsDir, scanDirectories, customPrompt, }) {
|
|
637
|
-
|
|
638
|
-
|
|
702
|
+
function buildFinderPrompt({ backlogItemsDir, scanDirectories, utilDir, customPrompt, }) {
|
|
703
|
+
if (customPrompt) {
|
|
704
|
+
return customPrompt();
|
|
705
|
+
}
|
|
706
|
+
const scanLabel = scanDirectories && scanDirectories.length > 0
|
|
707
|
+
? scanDirectories.map((dir) => `@${dir}`).join(' and ')
|
|
708
|
+
: 'the codebase';
|
|
709
|
+
const materializeDir = utilDir ?? scanDirectories?.[0];
|
|
710
|
+
const materializeLine = materializeDir
|
|
711
|
+
? `- Would materialize as a new util under @${materializeDir}/<utilName>/, following existing conventions there.`
|
|
712
|
+
: '- Would materialize as a new util in the scanned tree, following existing conventions there.';
|
|
713
|
+
const refactorScope = scanDirectories && scanDirectories.length > 0
|
|
714
|
+
? scanDirectories.join(' and ')
|
|
715
|
+
: 'the scanned tree';
|
|
716
|
+
return `
|
|
717
|
+
Scan ${scanLabel} for duplicated logic that appears in multiple places (same pattern, not merely similar file structure).
|
|
639
718
|
|
|
640
719
|
List existing backlog item names under @${backlogItemsDir}/todo/ and @${backlogItemsDir}/completed/. Do not propose abstractions whose util name already appears in either directory.
|
|
641
720
|
|
|
642
721
|
Pick exactly one new abstraction that:
|
|
643
722
|
- Has a clear util name matching ^[a-zA-Z0-9_-]+$ that describes the pattern it captures.
|
|
644
|
-
|
|
645
|
-
- Would shrink the codebase: refactoring all call sites in
|
|
723
|
+
${materializeLine}
|
|
724
|
+
- Would shrink the codebase: refactoring all call sites in ${refactorScope} should reduce net line count (excluding new unit tests).
|
|
646
725
|
|
|
647
726
|
Create exactly one new backlog item folder at @${backlogItemsDir}/todo/<utilName>/ with:
|
|
648
727
|
- desc.yml containing:
|
|
@@ -662,6 +741,11 @@ Do not take too much time looking for every possible abstraction. Once you found
|
|
|
662
741
|
`.trim();
|
|
663
742
|
}
|
|
664
743
|
|
|
744
|
+
const FEATURE_BACKLOG_WORKFLOWS = [
|
|
745
|
+
'tdd',
|
|
746
|
+
'directImpl',
|
|
747
|
+
'manual',
|
|
748
|
+
];
|
|
665
749
|
const RESERVED_NAME_SUFFIXES = ['_req', '_testPlan', '_tests_impl'];
|
|
666
750
|
function assertValidFeatureItemName(name) {
|
|
667
751
|
for (const suffix of RESERVED_NAME_SUFFIXES) {
|
|
@@ -670,6 +754,9 @@ function assertValidFeatureItemName(name) {
|
|
|
670
754
|
}
|
|
671
755
|
}
|
|
672
756
|
}
|
|
757
|
+
function featureItemContextBaseName(item) {
|
|
758
|
+
return item.parentName ? `${item.parentName}-${item.name}` : item.name;
|
|
759
|
+
}
|
|
673
760
|
function featureContextName(itemName, stage) {
|
|
674
761
|
switch (stage) {
|
|
675
762
|
case 'makeReq':
|
|
@@ -679,6 +766,7 @@ function featureContextName(itemName, stage) {
|
|
|
679
766
|
case 'testImpl':
|
|
680
767
|
return `${itemName}_tests_impl`;
|
|
681
768
|
case 'implementation':
|
|
769
|
+
case 'directImpl':
|
|
682
770
|
return itemName;
|
|
683
771
|
default: {
|
|
684
772
|
const _exhaustive = stage;
|
|
@@ -686,9 +774,84 @@ function featureContextName(itemName, stage) {
|
|
|
686
774
|
}
|
|
687
775
|
}
|
|
688
776
|
}
|
|
689
|
-
|
|
690
|
-
const
|
|
691
|
-
|
|
777
|
+
function parentNameFromTodoRelativeDir(todoRelativeDir) {
|
|
778
|
+
const parts = todoRelativeDir.split('/');
|
|
779
|
+
if (parts.length === 3 && parts[1] === 'tickets') {
|
|
780
|
+
return parts[0];
|
|
781
|
+
}
|
|
782
|
+
return undefined;
|
|
783
|
+
}
|
|
784
|
+
function parseFeatureWorkflow(itemName, raw) {
|
|
785
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
786
|
+
return undefined;
|
|
787
|
+
}
|
|
788
|
+
const record = raw;
|
|
789
|
+
if (record.workflow === undefined) {
|
|
790
|
+
return undefined;
|
|
791
|
+
}
|
|
792
|
+
if (typeof record.workflow !== 'string' ||
|
|
793
|
+
!FEATURE_BACKLOG_WORKFLOWS.includes(record.workflow)) {
|
|
794
|
+
throw new Error(`Backlog item "${itemName}" field "workflow" must be one of: ${FEATURE_BACKLOG_WORKFLOWS.join(', ')}`);
|
|
795
|
+
}
|
|
796
|
+
return record.workflow;
|
|
797
|
+
}
|
|
798
|
+
async function resolveItemWorkflow(input) {
|
|
799
|
+
const { item, paths, projectRoot } = input;
|
|
800
|
+
if (item.workflow !== undefined) {
|
|
801
|
+
return item.workflow;
|
|
802
|
+
}
|
|
803
|
+
if (item.parentName === undefined) {
|
|
804
|
+
return 'tdd';
|
|
805
|
+
}
|
|
806
|
+
const parentDescPath = path$1.join(projectRoot, paths.backlogItemsDir, 'todo', item.parentName, 'desc.yml');
|
|
807
|
+
let rawText;
|
|
808
|
+
try {
|
|
809
|
+
rawText = await fs$1.readFile(parentDescPath, 'utf-8');
|
|
810
|
+
}
|
|
811
|
+
catch (error) {
|
|
812
|
+
const err = error;
|
|
813
|
+
if (err.code === 'ENOENT') {
|
|
814
|
+
return 'tdd';
|
|
815
|
+
}
|
|
816
|
+
throw error;
|
|
817
|
+
}
|
|
818
|
+
return parseFeatureWorkflow(item.parentName, load(rawText)) ?? 'tdd';
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* `dev` → only top-level `directImpl` (tickets never run on `dev`, even if `directImpl`).
|
|
822
|
+
* `feature/<key>` → exact item name, or the parent todo name for tickets.
|
|
823
|
+
* `manual` never reaches here (`resolveFeatureBacklogItem` ignores it first).
|
|
824
|
+
*/
|
|
825
|
+
function itemMatchesDiscoveryBranch(input) {
|
|
826
|
+
const { itemName, parentName, discoveryBranch, workflow } = input;
|
|
827
|
+
if (discoveryBranch === 'dev') {
|
|
828
|
+
return workflow === 'directImpl' && parentName === undefined;
|
|
829
|
+
}
|
|
830
|
+
if (!discoveryBranch.startsWith('feature/')) {
|
|
831
|
+
return false;
|
|
832
|
+
}
|
|
833
|
+
const key = discoveryBranch.slice('feature/'.length);
|
|
834
|
+
return (parentName ?? itemName) === key;
|
|
835
|
+
}
|
|
836
|
+
async function resolveFeatureBacklogItem(input) {
|
|
837
|
+
const { item, paths, projectRoot, discoveryBranch } = input;
|
|
838
|
+
const contextBaseName = featureItemContextBaseName(item);
|
|
839
|
+
const workflow = await resolveItemWorkflow({ item, paths, projectRoot });
|
|
840
|
+
if (workflow === 'manual') {
|
|
841
|
+
return { ignored: true };
|
|
842
|
+
}
|
|
843
|
+
if (!!item.completedAt ||
|
|
844
|
+
!itemMatchesDiscoveryBranch({
|
|
845
|
+
itemName: item.name,
|
|
846
|
+
parentName: item.parentName,
|
|
847
|
+
discoveryBranch,
|
|
848
|
+
workflow,
|
|
849
|
+
})) {
|
|
850
|
+
return { ignored: true };
|
|
851
|
+
}
|
|
852
|
+
const itemDir = path$1.join(paths.backlogItemsDir, 'todo', item.todoRelativeDir);
|
|
853
|
+
const reqFilePath = path$1.join(itemDir, 'requirements.md');
|
|
854
|
+
const testPlanFilePath = path$1.join(itemDir, 'testPlan.md');
|
|
692
855
|
const hasReq = await pathExists(path$1.join(projectRoot, reqFilePath));
|
|
693
856
|
if (!hasReq) {
|
|
694
857
|
if (item.manualReq === true) {
|
|
@@ -696,7 +859,14 @@ async function resolveFeatureBacklogItem(item, paths, projectRoot, lumpName, bas
|
|
|
696
859
|
}
|
|
697
860
|
return {
|
|
698
861
|
stage: 'makeReq',
|
|
699
|
-
contextName: featureContextName(
|
|
862
|
+
contextName: featureContextName(contextBaseName, 'makeReq'),
|
|
863
|
+
variables: { REQ_FILE: reqFilePath },
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
if (workflow === 'directImpl') {
|
|
867
|
+
return {
|
|
868
|
+
stage: 'directImpl',
|
|
869
|
+
contextName: featureContextName(contextBaseName, 'directImpl'),
|
|
700
870
|
variables: { REQ_FILE: reqFilePath },
|
|
701
871
|
};
|
|
702
872
|
}
|
|
@@ -704,24 +874,24 @@ async function resolveFeatureBacklogItem(item, paths, projectRoot, lumpName, bas
|
|
|
704
874
|
if (!hasTestPlan) {
|
|
705
875
|
return {
|
|
706
876
|
stage: 'makeTestPlan',
|
|
707
|
-
contextName: featureContextName(
|
|
877
|
+
contextName: featureContextName(contextBaseName, 'makeTestPlan'),
|
|
708
878
|
variables: {
|
|
709
879
|
REQ_FILE: reqFilePath,
|
|
710
880
|
TEST_PLAN_FILE: testPlanFilePath,
|
|
711
881
|
},
|
|
712
882
|
};
|
|
713
883
|
}
|
|
714
|
-
const testsImplContextName = featureContextName(
|
|
884
|
+
const testsImplContextName = featureContextName(contextBaseName, 'testImpl');
|
|
715
885
|
const testsImplStatus = await getContextStatus({
|
|
716
886
|
projectRoot,
|
|
717
887
|
contextName: testsImplContextName,
|
|
718
|
-
lumpName,
|
|
719
|
-
baseBranch,
|
|
888
|
+
lumpName: paths.lumpName,
|
|
889
|
+
baseBranch: discoveryBranch,
|
|
720
890
|
});
|
|
721
891
|
if (testsImplStatus === 'finished') {
|
|
722
892
|
return {
|
|
723
893
|
stage: 'implementation',
|
|
724
|
-
contextName: featureContextName(
|
|
894
|
+
contextName: featureContextName(contextBaseName, 'implementation'),
|
|
725
895
|
variables: {
|
|
726
896
|
REQ_FILE: reqFilePath,
|
|
727
897
|
TEST_PLAN_FILE: testPlanFilePath,
|
|
@@ -741,27 +911,38 @@ async function resolveFeatureBacklogItem(item, paths, projectRoot, lumpName, bas
|
|
|
741
911
|
};
|
|
742
912
|
}
|
|
743
913
|
const featureBacklog = defineRecipe(function featureBacklog(options) {
|
|
744
|
-
const { configUrl,
|
|
914
|
+
const { configUrl, implValidateCommand, backlogItemsDir, ...rest } = options;
|
|
745
915
|
const projectRoot = projectRootFromConfigUrl(configUrl);
|
|
746
916
|
const runImplValidation = resolveImplValidateCommand(implValidateCommand ??
|
|
747
917
|
'echo "No implementation validation command provided. I say, trust but verify, but well..."');
|
|
748
918
|
return backlog({
|
|
749
919
|
configUrl,
|
|
750
920
|
backlogItemsDir,
|
|
751
|
-
|
|
752
|
-
parseItem(baseItem, _folderName, raw) {
|
|
921
|
+
parseItem(baseItem, folderName, raw) {
|
|
753
922
|
assertValidFeatureItemName(baseItem.name);
|
|
754
923
|
const record = raw;
|
|
755
924
|
if (record.manualReq !== undefined && typeof record.manualReq !== 'boolean') {
|
|
756
925
|
throw new Error(`Backlog item "${baseItem.name}" field "manualReq" must be a boolean`);
|
|
757
926
|
}
|
|
927
|
+
const parentName = parentNameFromTodoRelativeDir(folderName);
|
|
758
928
|
return {
|
|
759
929
|
...baseItem,
|
|
930
|
+
todoRelativeDir: folderName,
|
|
931
|
+
parentName,
|
|
932
|
+
dependsOn: parentName
|
|
933
|
+
? baseItem.dependsOn?.map((dep) => `${parentName}-${dep}`)
|
|
934
|
+
: baseItem.dependsOn,
|
|
760
935
|
manualReq: record.manualReq === true ? true : undefined,
|
|
936
|
+
workflow: parseFeatureWorkflow(baseItem.name, raw),
|
|
761
937
|
};
|
|
762
938
|
},
|
|
763
|
-
async resolveItem({ item, paths }) {
|
|
764
|
-
return resolveFeatureBacklogItem(
|
|
939
|
+
async resolveItem({ item, paths, discoveryBranch }) {
|
|
940
|
+
return resolveFeatureBacklogItem({
|
|
941
|
+
item,
|
|
942
|
+
paths,
|
|
943
|
+
projectRoot,
|
|
944
|
+
discoveryBranch,
|
|
945
|
+
});
|
|
765
946
|
},
|
|
766
947
|
stages: {
|
|
767
948
|
makeReq: {
|
|
@@ -910,9 +1091,28 @@ The implementation should make the tests pass. Do not edit any test file except
|
|
|
910
1091
|
validationCommandFn: runImplValidation,
|
|
911
1092
|
}),
|
|
912
1093
|
},
|
|
1094
|
+
directImpl: {
|
|
1095
|
+
completion: 'moveToDone',
|
|
1096
|
+
steps: retryUntilGreen({
|
|
1097
|
+
steps: [
|
|
1098
|
+
{
|
|
1099
|
+
promptFn({ context: ctx }) {
|
|
1100
|
+
const vars = ctx.variables;
|
|
1101
|
+
const { REQ_FILE } = vars;
|
|
1102
|
+
return `
|
|
1103
|
+
Implement the feature described in @${REQ_FILE}.
|
|
1104
|
+
Add or update tests as needed so the suite covers the change, and make validation pass.
|
|
1105
|
+
Do not edit @${REQ_FILE} unless absolutely necessary.
|
|
1106
|
+
`.trim();
|
|
1107
|
+
},
|
|
1108
|
+
},
|
|
1109
|
+
],
|
|
1110
|
+
validationCommandFn: runImplValidation,
|
|
1111
|
+
}),
|
|
1112
|
+
},
|
|
913
1113
|
},
|
|
914
1114
|
...rest,
|
|
915
1115
|
});
|
|
916
1116
|
});
|
|
917
1117
|
|
|
918
|
-
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 };
|
|
1118
|
+
export { BACKLOG_ITEMS_DIR_VAR, BACKLOG_ITEM_DIR_VAR, BACKLOG_STAGE_VAR, BACKLOG_TASK_NAME_VAR, BACKLOG_TASK_VAR, FEATURE_BACKLOG_WORKFLOWS, abstractionBacklog, abstractionFinder, backlog, backlogRecipe, defineRecipe, ephemeralContextListFn, featureBacklog, folderBacklogContexts, folderSetTaskDoneStep, getRecursiveSteps, listTodoRelativeDirs, lumpPathAndName, normalizeMaybePromGetter, parseFeatureWorkflow, 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.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Lumpcode lump recipes and kit helpers",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"lumpcode",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"test:watch": "vitest watch"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@lumpcode/cli-utils": "^0.
|
|
48
|
-
"@lumpcode/core": "^0.
|
|
47
|
+
"@lumpcode/cli-utils": "^0.3.0",
|
|
48
|
+
"@lumpcode/core": "^0.3.0",
|
|
49
49
|
"js-yaml": "^5.0.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|