@lumpcode/recipes 0.0.15 → 0.0.17
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 +60 -46
- package/dist/index.cjs +115 -78
- package/dist/index.d.ts +52 -54
- package/dist/index.js +115 -78
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -19,6 +19,8 @@ npm install @lumpcode/recipes
|
|
|
19
19
|
| **abstractionFinder** | `abstractionFinder` | Ephemeral contexts that scan for duplicated CLI utils and append one backlog item + requirements doc per run |
|
|
20
20
|
| **abstractionBacklog** | `abstractionBacklog` | Folder backlog items with requirements — implement abstraction with verify-until-green, then move item to completed/ |
|
|
21
21
|
|
|
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.
|
|
23
|
+
|
|
22
24
|
## Backlog layout
|
|
23
25
|
|
|
24
26
|
The backlog recipes (`backlog`, `featureBacklog`, `abstractionBacklog`) use a folder backlog under `backlogItems/`:
|
|
@@ -39,8 +41,8 @@ The backlog recipes (`backlog`, `featureBacklog`, `abstractionBacklog`) use a fo
|
|
|
39
41
|
|
|
40
42
|
Flat helpers under `src/kit/` (re-exported from the package root):
|
|
41
43
|
|
|
42
|
-
- `backlog` recipe helpers — `resolveBacklogPaths`, `validateBaseBacklogItem`, `requireArtifactStep
|
|
43
|
-
- `getRecursiveSteps` — agent step(s) + validation command, retry until pass
|
|
44
|
+
- `backlog` recipe helpers — `resolveBacklogPaths`, `validateBaseBacklogItem`, `requireArtifactStep` (artifact `ValidationCommandFn`), `projectRootFromConfigUrl`
|
|
45
|
+
- `getRecursiveSteps` — agent step(s) + validation command, retry until pass (retries via `postCommandExecFn` returned steps)
|
|
44
46
|
- `retryUntilGreen` — opinionated wrapper over `getRecursiveSteps` with default fix prompt
|
|
45
47
|
- `ephemeralContextListFn` — N fresh synthetic contexts per run (`contextCount`, index-aware names)
|
|
46
48
|
- `folderBacklogContexts` — `getContextListFn` from `backlogItems/todo/` with optional per-item parsing
|
|
@@ -80,26 +82,30 @@ Context variables injected by `backlog`: `TASK_NAME`, `TASK`, `BACKLOG_ITEMS_DIR
|
|
|
80
82
|
|
|
81
83
|
```ts
|
|
82
84
|
// .lumpcode/lumps/backlog/config.ts
|
|
83
|
-
import {
|
|
85
|
+
import {
|
|
86
|
+
type CursorPresetLumpVariables,
|
|
87
|
+
type CursorPresetStepVariables,
|
|
88
|
+
} from '@lumpcode/cli-utils';
|
|
84
89
|
import { featureBacklog } from '@lumpcode/recipes';
|
|
85
90
|
|
|
86
|
-
export default
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
91
|
+
export default featureBacklog<
|
|
92
|
+
CursorPresetLumpVariables,
|
|
93
|
+
CursorPresetStepVariables
|
|
94
|
+
>({
|
|
95
|
+
baseBranch: 'dev',
|
|
96
|
+
command: 'cursor',
|
|
97
|
+
configUrl: import.meta.url,
|
|
98
|
+
registerCommands: ['cursor'],
|
|
99
|
+
maximumNumberOfConcurrentBranches: 5,
|
|
100
|
+
verbose: true,
|
|
101
|
+
keepHistory: true,
|
|
102
|
+
lumpVariables: { model: 'composer-2.5' },
|
|
103
|
+
discoveryBranch: 'dev',
|
|
104
|
+
implValidateCommand: [
|
|
105
|
+
'npm run build -w=@lumpcode/cli',
|
|
106
|
+
'npm run test -w=@lumpcode/cli',
|
|
107
|
+
].join(' && '),
|
|
108
|
+
});
|
|
103
109
|
```
|
|
104
110
|
|
|
105
111
|
### abstractionFinder + abstractionBacklog
|
|
@@ -108,45 +114,53 @@ Two-lump pipeline: finder tops up the implementer backlog; implementer runs item
|
|
|
108
114
|
|
|
109
115
|
```ts
|
|
110
116
|
// .lumpcode/lumps/abstractionFinder/config.ts
|
|
111
|
-
import {
|
|
117
|
+
import {
|
|
118
|
+
type CursorPresetLumpVariables,
|
|
119
|
+
type CursorPresetStepVariables,
|
|
120
|
+
} from '@lumpcode/cli-utils';
|
|
112
121
|
import { abstractionFinder } from '@lumpcode/recipes';
|
|
113
122
|
|
|
114
|
-
export default
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
123
|
+
export default abstractionFinder<
|
|
124
|
+
CursorPresetLumpVariables,
|
|
125
|
+
CursorPresetStepVariables
|
|
126
|
+
>({
|
|
127
|
+
maxPendingAbstractions: 5,
|
|
128
|
+
scanDirectories: ['packages/apps/cli'],
|
|
129
|
+
backlogItemsDir: '.lumpcode/lumps/abstractionImplementer/backlogItems',
|
|
130
|
+
command: 'cursor',
|
|
131
|
+
lumpVariables: { model: 'composer-2.5' },
|
|
132
|
+
discoveryBranch: 'dev',
|
|
133
|
+
});
|
|
124
134
|
```
|
|
125
135
|
|
|
126
136
|
```ts
|
|
127
137
|
// .lumpcode/lumps/abstractionImplementer/config.ts
|
|
128
|
-
import {
|
|
138
|
+
import {
|
|
139
|
+
type CursorPresetLumpVariables,
|
|
140
|
+
type CursorPresetStepVariables,
|
|
141
|
+
} from '@lumpcode/cli-utils';
|
|
129
142
|
import { abstractionBacklog } from '@lumpcode/recipes';
|
|
130
143
|
|
|
131
|
-
export default
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
+
export default abstractionBacklog<
|
|
145
|
+
CursorPresetLumpVariables,
|
|
146
|
+
CursorPresetStepVariables
|
|
147
|
+
>({
|
|
148
|
+
baseBranch: 'dev',
|
|
149
|
+
command: 'cursor',
|
|
150
|
+
configUrl: import.meta.url,
|
|
151
|
+
registerCommands: ['cursor'],
|
|
152
|
+
maximumNumberOfConcurrentBranches: 3,
|
|
153
|
+
verbose: true,
|
|
154
|
+
keepHistory: true,
|
|
155
|
+
lumpVariables: { model: 'composer-2.5' },
|
|
156
|
+
discoveryBranch: 'dev',
|
|
157
|
+
});
|
|
144
158
|
```
|
|
145
159
|
|
|
146
160
|
### Custom config with kit helpers
|
|
147
161
|
|
|
148
162
|
```ts
|
|
149
|
-
import { defineConfig } from '@lumpcode/cli-
|
|
163
|
+
import { defineConfig } from '@lumpcode/cli-utils';
|
|
150
164
|
import { retryUntilGreen, shellCommand } from '@lumpcode/recipes';
|
|
151
165
|
|
|
152
166
|
export default defineConfig({
|
package/dist/index.cjs
CHANGED
|
@@ -57,18 +57,16 @@ function ephemeralContextListFn(options = {}) {
|
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
const GET_RECURSIVE_STEPS_IS_OK_FLAG_KEY = '__getRecursiveSteps_isOk__';
|
|
61
60
|
function stepIndexDepth(stepIndex) {
|
|
62
61
|
return Array.isArray(stepIndex) ? stepIndex.length : 1;
|
|
63
62
|
}
|
|
64
63
|
/** Agent prompt(s) followed by a validation command, retried until checks pass or `maxIterations` is reached. */
|
|
65
|
-
function getRecursiveSteps({ maxIterations = 5, validationCommandFn = () => null, isValidationCommandResultOk = ({ commandSucceeded }) => commandSucceeded, getFirstSteps = () => [], currentIteration = 0, prevValidateCommandResult = null, prevValidateCommandDescriptor = null,
|
|
64
|
+
function getRecursiveSteps({ maxIterations = 5, validationCommandFn = () => null, isValidationCommandResultOk = ({ commandSucceeded }) => commandSucceeded, getFirstSteps = () => [], currentIteration = 0, prevValidateCommandResult = null, prevValidateCommandDescriptor = null, }) {
|
|
66
65
|
const firstSteps = getFirstSteps({
|
|
67
66
|
currentIteration,
|
|
68
67
|
prevValidateCommandResult,
|
|
69
68
|
prevValidateCommandDescriptor,
|
|
70
69
|
});
|
|
71
|
-
let thisIterValidateCommandResult = null;
|
|
72
70
|
let thisIterValidateCommandDescriptor = null;
|
|
73
71
|
return [
|
|
74
72
|
...cliUtils.normalizeSteps({
|
|
@@ -83,43 +81,35 @@ function getRecursiveSteps({ maxIterations = 5, validationCommandFn = () => null
|
|
|
83
81
|
args: ['Loop limit reached'],
|
|
84
82
|
};
|
|
85
83
|
}
|
|
86
|
-
|
|
87
|
-
const validateCommandDescriptor = await validationCommandFn({
|
|
88
|
-
...input,
|
|
89
|
-
currentIteration,
|
|
90
|
-
prevValidateCommandResult,
|
|
91
|
-
contextRunStateIsOkFlagKey,
|
|
92
|
-
});
|
|
93
|
-
thisIterValidateCommandDescriptor = validateCommandDescriptor || null;
|
|
94
|
-
return validateCommandDescriptor;
|
|
95
|
-
}
|
|
96
|
-
return null;
|
|
97
|
-
},
|
|
98
|
-
postCommandExecFn(input) {
|
|
99
|
-
thisIterValidateCommandResult = input.commandResult;
|
|
100
|
-
input.contextRunState[contextRunStateIsOkFlagKey] = isValidationCommandResultOk({
|
|
84
|
+
const validateCommandDescriptor = await validationCommandFn({
|
|
101
85
|
...input,
|
|
102
86
|
currentIteration,
|
|
87
|
+
prevValidateCommandResult,
|
|
103
88
|
});
|
|
89
|
+
thisIterValidateCommandDescriptor = validateCommandDescriptor || null;
|
|
90
|
+
return validateCommandDescriptor;
|
|
104
91
|
},
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
92
|
+
async postCommandExecFn(input) {
|
|
93
|
+
if (stepIndexDepth(input.stepIndex) > maxIterations) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (isValidationCommandResultOk({
|
|
97
|
+
...input,
|
|
98
|
+
currentIteration,
|
|
99
|
+
})) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
return getRecursiveSteps({
|
|
113
103
|
maxIterations,
|
|
114
104
|
validationCommandFn,
|
|
115
105
|
isValidationCommandResultOk,
|
|
116
106
|
getFirstSteps,
|
|
117
107
|
currentIteration: currentIteration + 1,
|
|
118
|
-
prevValidateCommandResult:
|
|
108
|
+
prevValidateCommandResult: input.commandResult,
|
|
119
109
|
prevValidateCommandDescriptor: thisIterValidateCommandDescriptor,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
continueOnError: currentIteration < maxIterations,
|
|
123
113
|
},
|
|
124
114
|
];
|
|
125
115
|
}
|
|
@@ -163,17 +153,16 @@ function defaultFixSteps(input) {
|
|
|
163
153
|
];
|
|
164
154
|
}
|
|
165
155
|
/** Work steps, validation command, and optional fix steps — retried until checks pass or `maxIterations`. */
|
|
166
|
-
function retryUntilGreen({ steps, fixSteps, validationCommandFn, isValidationCommandResultOk,
|
|
156
|
+
function retryUntilGreen({ steps, fixSteps, validationCommandFn, isValidationCommandResultOk, maxIterations, }) {
|
|
167
157
|
return getRecursiveSteps({
|
|
168
158
|
maxIterations,
|
|
169
159
|
validationCommandFn,
|
|
170
160
|
isValidationCommandResultOk,
|
|
171
|
-
contextRunStateIsOkFlagKey,
|
|
172
161
|
getFirstSteps({ currentIteration, prevValidateCommandResult, prevValidateCommandDescriptor }) {
|
|
173
162
|
if (currentIteration === 0) {
|
|
174
163
|
return steps;
|
|
175
164
|
}
|
|
176
|
-
return (fixSteps ?? defaultFixSteps)({
|
|
165
|
+
return (fixSteps ?? (defaultFixSteps))({
|
|
177
166
|
currentIteration,
|
|
178
167
|
prevValidateCommandResult,
|
|
179
168
|
prevValidateCommandDescriptor,
|
|
@@ -197,24 +186,26 @@ function projectRootFromConfigUrl(configUrl) {
|
|
|
197
186
|
return path$1.resolve(configDir, '../../..');
|
|
198
187
|
}
|
|
199
188
|
|
|
200
|
-
/**
|
|
189
|
+
/** Validation command that fails when the artifact referenced by a context variable was not created. */
|
|
201
190
|
function requireArtifactStep(artifactPathVarName) {
|
|
202
|
-
return {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
throw new Error(`Expected artifact at ${artifactPath} was not created`);
|
|
212
|
-
}
|
|
191
|
+
return async ({ context, workspacePath }) => {
|
|
192
|
+
const artifactPath = context.variables[artifactPathVarName];
|
|
193
|
+
if (typeof artifactPath !== 'string' || artifactPath.trim() === '') {
|
|
194
|
+
throw new Error(`Missing context variable ${artifactPathVarName}`);
|
|
195
|
+
}
|
|
196
|
+
const fullPath = path$1.join(workspacePath, artifactPath);
|
|
197
|
+
const exists = await core.pathExists(fullPath);
|
|
198
|
+
if (!exists) {
|
|
199
|
+
const message = `Expected artifact at ${artifactPath} was not created`;
|
|
213
200
|
return {
|
|
214
201
|
executable: 'node',
|
|
215
|
-
args: ['-e',
|
|
202
|
+
args: ['-e', `console.error(${JSON.stringify(message)}); process.exit(1)`],
|
|
216
203
|
};
|
|
217
|
-
}
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
executable: 'node',
|
|
207
|
+
args: ['-e', 'process.exit(0)'],
|
|
208
|
+
};
|
|
218
209
|
};
|
|
219
210
|
}
|
|
220
211
|
|
|
@@ -350,7 +341,7 @@ function isPlainObject(value) {
|
|
|
350
341
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
351
342
|
}
|
|
352
343
|
/** Moves a finished backlog item folder from todo/ to completed/ after the context completes. */
|
|
353
|
-
|
|
344
|
+
function folderSetTaskDoneStep(input) {
|
|
354
345
|
const nameVarName = input.nameVarName ?? 'TASK_NAME';
|
|
355
346
|
return {
|
|
356
347
|
async commandFn({ context, workspacePath }) {
|
|
@@ -391,7 +382,7 @@ const folderSetTaskDoneStep = (input) => {
|
|
|
391
382
|
},
|
|
392
383
|
continueOnError: true,
|
|
393
384
|
};
|
|
394
|
-
}
|
|
385
|
+
}
|
|
395
386
|
|
|
396
387
|
let setTaskDoneDeprecatedWarned = false;
|
|
397
388
|
function warnSetTaskDoneDeprecated() {
|
|
@@ -403,7 +394,7 @@ function warnSetTaskDoneDeprecated() {
|
|
|
403
394
|
'YAML backlog helpers will be removed in a future major version.');
|
|
404
395
|
}
|
|
405
396
|
/** Moves a finished backlog item from BACKLOG.yml to DONE.yml after the context completes. */
|
|
406
|
-
|
|
397
|
+
function setTaskDoneStep(input) {
|
|
407
398
|
return {
|
|
408
399
|
async commandFn({ context, workspacePath }) {
|
|
409
400
|
warnSetTaskDoneDeprecated();
|
|
@@ -432,7 +423,7 @@ const setTaskDoneStep = (input) => {
|
|
|
432
423
|
},
|
|
433
424
|
continueOnError: true,
|
|
434
425
|
};
|
|
435
|
-
}
|
|
426
|
+
}
|
|
436
427
|
|
|
437
428
|
function resolveImplValidateCommand(implValidateCommand) {
|
|
438
429
|
if (typeof implValidateCommand === 'function') {
|
|
@@ -483,6 +474,7 @@ function ymlBacklogContexts({ backlogFilePath, parseItem, parseContext, }) {
|
|
|
483
474
|
};
|
|
484
475
|
}
|
|
485
476
|
|
|
477
|
+
/** Identity helper — must preserve a generic `<V, SV>` function signature. */
|
|
486
478
|
function defineRecipe(recipe) {
|
|
487
479
|
return recipe;
|
|
488
480
|
}
|
|
@@ -569,12 +561,13 @@ function backlog(options) {
|
|
|
569
561
|
...rest,
|
|
570
562
|
});
|
|
571
563
|
}
|
|
572
|
-
const backlogRecipe = defineRecipe(
|
|
564
|
+
const backlogRecipe = defineRecipe(backlog);
|
|
573
565
|
|
|
574
|
-
const abstractionBacklog = defineRecipe((options)
|
|
566
|
+
const abstractionBacklog = defineRecipe(function abstractionBacklog(options) {
|
|
575
567
|
const { implValidateCommand, configUrl, implSteps, ...rest } = options;
|
|
576
568
|
const projectRoot = projectRootFromConfigUrl(configUrl);
|
|
577
|
-
const runImplValidation = resolveImplValidateCommand(implValidateCommand ??
|
|
569
|
+
const runImplValidation = resolveImplValidateCommand(implValidateCommand ??
|
|
570
|
+
'echo "No implementation validation command provided. I say, trust but verify, but well..."');
|
|
578
571
|
return backlog({
|
|
579
572
|
configUrl,
|
|
580
573
|
async resolveItem({ item, paths }) {
|
|
@@ -621,7 +614,7 @@ const abstractionBacklog = defineRecipe((options) => {
|
|
|
621
614
|
});
|
|
622
615
|
});
|
|
623
616
|
|
|
624
|
-
const abstractionFinder = defineRecipe((options)
|
|
617
|
+
const abstractionFinder = defineRecipe(function abstractionFinder(options) {
|
|
625
618
|
const { maxPendingAbstractions = 5, scanDirectories, customPrompt, backlogItemsDir, } = options; // TODO : check if pending abstcations in backlog are less than maxPendingAbstractions
|
|
626
619
|
return cliUtils.defineConfig({
|
|
627
620
|
...options,
|
|
@@ -741,10 +734,11 @@ async function resolveFeatureBacklogItem(item, paths, projectRoot, lumpName, bas
|
|
|
741
734
|
},
|
|
742
735
|
};
|
|
743
736
|
}
|
|
744
|
-
const featureBacklog = defineRecipe((options)
|
|
737
|
+
const featureBacklog = defineRecipe(function featureBacklog(options) {
|
|
745
738
|
const { configUrl, baseBranch, implValidateCommand, backlogItemsDir, ...rest } = options;
|
|
746
739
|
const projectRoot = projectRootFromConfigUrl(configUrl);
|
|
747
|
-
const runImplValidation = resolveImplValidateCommand(implValidateCommand ??
|
|
740
|
+
const runImplValidation = resolveImplValidateCommand(implValidateCommand ??
|
|
741
|
+
'echo "No implementation validation command provided. I say, trust but verify, but well..."');
|
|
748
742
|
return backlog({
|
|
749
743
|
configUrl,
|
|
750
744
|
backlogItemsDir,
|
|
@@ -766,12 +760,13 @@ const featureBacklog = defineRecipe((options) => {
|
|
|
766
760
|
stages: {
|
|
767
761
|
makeReq: {
|
|
768
762
|
completion: 'keepPending',
|
|
769
|
-
steps:
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
763
|
+
steps: retryUntilGreen({
|
|
764
|
+
steps: [
|
|
765
|
+
{
|
|
766
|
+
promptFn({ context: ctx }) {
|
|
767
|
+
const vars = ctx.variables;
|
|
768
|
+
const { BACKLOG_ITEM_DIR, TASK_NAME, TASK, REQ_FILE } = vars;
|
|
769
|
+
return `
|
|
775
770
|
Write a requirements document for the following backlog item from @${BACKLOG_ITEM_DIR}/desc.yml.
|
|
776
771
|
|
|
777
772
|
Task name: ${TASK_NAME}
|
|
@@ -791,21 +786,42 @@ The requirements document should be self-contained and implementation-ready. Inc
|
|
|
791
786
|
- Acceptance criteria
|
|
792
787
|
|
|
793
788
|
Do not implement the feature — only create the requirements markdown file.
|
|
789
|
+
Do not wait the user to answer any questions — make the best assumptions and just write the requirements document.
|
|
794
790
|
The requirements document should not contain any testing strategy details.
|
|
795
|
-
|
|
791
|
+
`.trim();
|
|
792
|
+
},
|
|
796
793
|
},
|
|
797
|
-
|
|
798
|
-
requireArtifactStep('REQ_FILE'),
|
|
799
|
-
|
|
794
|
+
],
|
|
795
|
+
validationCommandFn: requireArtifactStep('REQ_FILE'),
|
|
796
|
+
fixSteps: ({ prevValidateCommandResult }) => [
|
|
797
|
+
{
|
|
798
|
+
promptFn({ context: ctx }) {
|
|
799
|
+
const vars = ctx.variables;
|
|
800
|
+
const { BACKLOG_ITEM_DIR, REQ_FILE } = vars;
|
|
801
|
+
return `
|
|
802
|
+
The requirements document was not created at @${REQ_FILE}.
|
|
803
|
+
|
|
804
|
+
Create it now at that exact path. Do not edit @${BACKLOG_ITEM_DIR}/desc.yml.
|
|
805
|
+
Do not implement the feature — only write the requirements markdown file.
|
|
806
|
+
The requirements document should not contain any testing strategy details.
|
|
807
|
+
|
|
808
|
+
Verification output:
|
|
809
|
+
${prevValidateCommandResult ?? '(no output captured)'}
|
|
810
|
+
`.trim();
|
|
811
|
+
},
|
|
812
|
+
},
|
|
813
|
+
],
|
|
814
|
+
}),
|
|
800
815
|
},
|
|
801
816
|
makeTestPlan: {
|
|
802
817
|
completion: 'keepPending',
|
|
803
|
-
steps:
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
818
|
+
steps: retryUntilGreen({
|
|
819
|
+
steps: [
|
|
820
|
+
{
|
|
821
|
+
promptFn({ context: ctx }) {
|
|
822
|
+
const vars = ctx.variables;
|
|
823
|
+
const { BACKLOG_ITEM_DIR, TASK_NAME, TASK, REQ_FILE, TEST_PLAN_FILE } = vars;
|
|
824
|
+
return `
|
|
809
825
|
Write a test plan for the following backlog item from @${BACKLOG_ITEM_DIR}/desc.yml.
|
|
810
826
|
|
|
811
827
|
Task name: ${TASK_NAME}
|
|
@@ -821,11 +837,29 @@ The test plan should be self-contained and implementation-ready. Include:
|
|
|
821
837
|
- Test data
|
|
822
838
|
- Test expectations
|
|
823
839
|
- Test implementation details
|
|
824
|
-
|
|
840
|
+
`.trim();
|
|
841
|
+
},
|
|
825
842
|
},
|
|
826
|
-
|
|
827
|
-
requireArtifactStep('TEST_PLAN_FILE'),
|
|
828
|
-
|
|
843
|
+
],
|
|
844
|
+
validationCommandFn: requireArtifactStep('TEST_PLAN_FILE'),
|
|
845
|
+
fixSteps: ({ prevValidateCommandResult }) => [
|
|
846
|
+
{
|
|
847
|
+
promptFn({ context: ctx }) {
|
|
848
|
+
const vars = ctx.variables;
|
|
849
|
+
const { BACKLOG_ITEM_DIR, REQ_FILE, TEST_PLAN_FILE } = vars;
|
|
850
|
+
return `
|
|
851
|
+
The test plan was not created at @${TEST_PLAN_FILE}.
|
|
852
|
+
|
|
853
|
+
Create it now at that exact path. Match the requirements in @${REQ_FILE}.
|
|
854
|
+
Do not edit @${BACKLOG_ITEM_DIR}/desc.yml nor @${REQ_FILE}.
|
|
855
|
+
|
|
856
|
+
Verification output:
|
|
857
|
+
${prevValidateCommandResult ?? '(no output captured)'}
|
|
858
|
+
`.trim();
|
|
859
|
+
},
|
|
860
|
+
},
|
|
861
|
+
],
|
|
862
|
+
}),
|
|
829
863
|
},
|
|
830
864
|
testImpl: {
|
|
831
865
|
completion: 'keepPending',
|
|
@@ -837,6 +871,8 @@ The test plan should be self-contained and implementation-ready. Include:
|
|
|
837
871
|
return `
|
|
838
872
|
Write a test implementation for the following backlog item from @${BACKLOG_ITEM_DIR}/desc.yml.
|
|
839
873
|
|
|
874
|
+
The new tests should be skipped in order to not break the whole test suite.
|
|
875
|
+
|
|
840
876
|
Task name: ${TASK_NAME}
|
|
841
877
|
Task:
|
|
842
878
|
${TASK}
|
|
@@ -859,7 +895,8 @@ The requirements for this task are in @${REQ_FILE}.
|
|
|
859
895
|
return `
|
|
860
896
|
Implement the feature described in @${REQ_FILE}.
|
|
861
897
|
The tests have already been implemented according to the test plan in @${TEST_PLAN_FILE}.
|
|
862
|
-
|
|
898
|
+
Unskip all the tests that were skipped in the tests implementation.
|
|
899
|
+
The implementation should make the tests pass. Do not edit any test file except to unskip them or if absolutely necessary.
|
|
863
900
|
`.trim();
|
|
864
901
|
},
|
|
865
902
|
},
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CommandDescriptor, MaybePromise, PostCommandExecFn, Step } from '@lumpcode/core';
|
|
2
|
-
import { GetContextListFnInput, GetContextListFn, CommandFn, LumpJsConfig, LumpJsConfigSteps,
|
|
2
|
+
import { LumpVariables, GetContextListFnInput, GetContextListFn, StepVariables, CommandFn, LumpJsConfig, LumpJsConfigSteps, MaybePromise as MaybePromise$1, Context } from '@lumpcode/cli-utils';
|
|
3
3
|
|
|
4
4
|
/** Run a shell script via `sh -c` (portable on Unix-like systems and Git Bash on Windows). */
|
|
5
5
|
declare function shellCommand(script: string): CommandDescriptor;
|
|
@@ -9,8 +9,8 @@ declare function normalizeMaybePromGetter<T, P = void>(maybePromGetter: MaybePro
|
|
|
9
9
|
declare function normalizeMaybePromGetter<T, P = void>(maybePromGetter: MaybePromGetter<T, P> | undefined, defaultValue: T): (input?: P) => MaybePromise<T>;
|
|
10
10
|
|
|
11
11
|
type ContextVariables = Record<string, string | number | boolean>;
|
|
12
|
-
type EphemeralContextListFnOptions = {
|
|
13
|
-
contextCount?: MaybePromGetter<number, GetContextListFnInput
|
|
12
|
+
type EphemeralContextListFnOptions<V extends LumpVariables = LumpVariables> = {
|
|
13
|
+
contextCount?: MaybePromGetter<number, GetContextListFnInput<V>>;
|
|
14
14
|
contextName?: MaybePromGetter<string, {
|
|
15
15
|
index: number;
|
|
16
16
|
count: number;
|
|
@@ -21,16 +21,15 @@ type EphemeralContextListFnOptions = {
|
|
|
21
21
|
count: number;
|
|
22
22
|
}>;
|
|
23
23
|
};
|
|
24
|
-
declare function ephemeralContextListFn(options?: EphemeralContextListFnOptions): GetContextListFn
|
|
24
|
+
declare function ephemeralContextListFn<V extends LumpVariables = LumpVariables>(options?: EphemeralContextListFnOptions<V>): GetContextListFn<V>;
|
|
25
25
|
|
|
26
26
|
type StepIndex = number | number[];
|
|
27
|
-
type ValidationCommandFnInput = Parameters<CommandFn
|
|
27
|
+
type ValidationCommandFnInput<V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables> = Parameters<CommandFn<V, SV>>[0] & {
|
|
28
28
|
currentIteration: number;
|
|
29
29
|
prevValidateCommandResult: string | null;
|
|
30
|
-
contextRunStateIsOkFlagKey: string;
|
|
31
30
|
};
|
|
32
|
-
type ValidationCommandFn = (input: ValidationCommandFnInput) => MaybePromise<CommandDescriptor | null | undefined>;
|
|
33
|
-
type IsValidationCommandResultOkInput = Parameters<PostCommandExecFn
|
|
31
|
+
type ValidationCommandFn<V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables> = (input: ValidationCommandFnInput<V, SV>) => MaybePromise<CommandDescriptor | null | undefined>;
|
|
32
|
+
type IsValidationCommandResultOkInput<V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables> = Parameters<PostCommandExecFn<V, SV>>[0] & {
|
|
34
33
|
currentIteration: number;
|
|
35
34
|
};
|
|
36
35
|
type GetFirstStepsInput = {
|
|
@@ -38,38 +37,35 @@ type GetFirstStepsInput = {
|
|
|
38
37
|
prevValidateCommandResult: string | null;
|
|
39
38
|
prevValidateCommandDescriptor: CommandDescriptor | null;
|
|
40
39
|
};
|
|
41
|
-
type GetRecursiveStepsOptions = {
|
|
40
|
+
type GetRecursiveStepsOptions<V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables> = {
|
|
42
41
|
maxIterations?: number;
|
|
43
|
-
validationCommandFn?: ValidationCommandFn
|
|
44
|
-
isValidationCommandResultOk?: (input: IsValidationCommandResultOkInput) => boolean;
|
|
45
|
-
getFirstSteps?: (input: GetFirstStepsInput) => LumpJsConfig['steps'];
|
|
42
|
+
validationCommandFn?: ValidationCommandFn<V, SV>;
|
|
43
|
+
isValidationCommandResultOk?: (input: IsValidationCommandResultOkInput<V, SV>) => boolean;
|
|
44
|
+
getFirstSteps?: (input: GetFirstStepsInput) => LumpJsConfig<V, SV>['steps'];
|
|
46
45
|
currentIteration?: number;
|
|
47
46
|
prevValidateCommandResult?: string | null;
|
|
48
47
|
prevValidateCommandDescriptor?: CommandDescriptor | null;
|
|
49
|
-
contextRunStateIsOkFlagKey?: string;
|
|
50
48
|
};
|
|
51
49
|
/** Agent prompt(s) followed by a validation command, retried until checks pass or `maxIterations` is reached. */
|
|
52
|
-
declare function getRecursiveSteps({ maxIterations, validationCommandFn, isValidationCommandResultOk, getFirstSteps, currentIteration, prevValidateCommandResult, prevValidateCommandDescriptor,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
contextRunStateIsOkFlagKey?: GetRecursiveStepsOptions['contextRunStateIsOkFlagKey'];
|
|
61
|
-
maxIterations?: GetRecursiveStepsOptions['maxIterations'];
|
|
50
|
+
declare function getRecursiveSteps<V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables>({ maxIterations, validationCommandFn, isValidationCommandResultOk, getFirstSteps, currentIteration, prevValidateCommandResult, prevValidateCommandDescriptor, }: GetRecursiveStepsOptions<V, SV>): LumpJsConfigSteps<V, SV>;
|
|
51
|
+
|
|
52
|
+
interface RetryUntilGreenInput<V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables> {
|
|
53
|
+
steps: LumpJsConfig<V, SV>['steps'];
|
|
54
|
+
fixSteps?: (input: GetFirstStepsInput) => LumpJsConfig<V, SV>['steps'];
|
|
55
|
+
validationCommandFn: ValidationCommandFn<V, SV>;
|
|
56
|
+
isValidationCommandResultOk?: (input: IsValidationCommandResultOkInput<V, SV>) => boolean;
|
|
57
|
+
maxIterations?: GetRecursiveStepsOptions<V, SV>['maxIterations'];
|
|
62
58
|
}
|
|
63
59
|
/** Work steps, validation command, and optional fix steps — retried until checks pass or `maxIterations`. */
|
|
64
|
-
declare function retryUntilGreen({ steps, fixSteps, validationCommandFn, isValidationCommandResultOk,
|
|
60
|
+
declare function retryUntilGreen<V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables>({ steps, fixSteps, validationCommandFn, isValidationCommandResultOk, maxIterations, }: RetryUntilGreenInput<V, SV>): LumpJsConfigSteps<V, SV>;
|
|
65
61
|
|
|
66
62
|
declare function lumpPathAndName(configUrl: string | URL): [lumpPath: string, lumpName: string];
|
|
67
63
|
|
|
68
64
|
/** Resolves git project root from a lump `config.ts` module URL. */
|
|
69
65
|
declare function projectRootFromConfigUrl(configUrl: string | URL): string;
|
|
70
66
|
|
|
71
|
-
/**
|
|
72
|
-
declare function requireArtifactStep(artifactPathVarName: string):
|
|
67
|
+
/** Validation command that fails when the artifact referenced by a context variable was not created. */
|
|
68
|
+
declare function requireArtifactStep<V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables>(artifactPathVarName: string): ValidationCommandFn<V, SV>;
|
|
73
69
|
|
|
74
70
|
type BacklogPaths = {
|
|
75
71
|
lumpPath: string;
|
|
@@ -80,8 +76,9 @@ declare function resolveBacklogPaths(configUrl: string | URL, overrides?: {
|
|
|
80
76
|
backlogItemsDir?: string;
|
|
81
77
|
}): BacklogPaths;
|
|
82
78
|
|
|
83
|
-
type Recipe<Options, V extends LumpVariables = LumpVariables> = (options: Options) => LumpJsConfig<V>;
|
|
84
|
-
|
|
79
|
+
type Recipe<Options, V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables> = (options: Options) => LumpJsConfig<V, SV>;
|
|
80
|
+
/** Identity helper — must preserve a generic `<V, SV>` function signature. */
|
|
81
|
+
declare function defineRecipe<R extends (options: never) => LumpJsConfig>(recipe: R): R;
|
|
85
82
|
|
|
86
83
|
type BaseBacklogItem = {
|
|
87
84
|
name: string;
|
|
@@ -101,22 +98,22 @@ type FolderBacklogContextsOptions<Item extends BaseBacklogItem = BaseBacklogItem
|
|
|
101
98
|
ignored?: boolean;
|
|
102
99
|
}>;
|
|
103
100
|
};
|
|
104
|
-
declare function folderBacklogContexts<Item extends BaseBacklogItem = BaseBacklogItem>({ backlogItemsDir, parseItem, parseContext, }: FolderBacklogContextsOptions<Item>): GetContextListFn
|
|
101
|
+
declare function folderBacklogContexts<Item extends BaseBacklogItem = BaseBacklogItem, V extends LumpVariables = LumpVariables>({ backlogItemsDir, parseItem, parseContext, }: FolderBacklogContextsOptions<Item>): GetContextListFn<V>;
|
|
105
102
|
|
|
106
103
|
/** Moves a finished backlog item folder from todo/ to completed/ after the context completes. */
|
|
107
|
-
declare
|
|
104
|
+
declare function folderSetTaskDoneStep<V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables>(input: {
|
|
108
105
|
itemsDirVarName: string;
|
|
109
106
|
nameVarName?: string;
|
|
110
|
-
})
|
|
107
|
+
}): Step<V, SV>;
|
|
111
108
|
|
|
112
109
|
/** Moves a finished backlog item from BACKLOG.yml to DONE.yml after the context completes. */
|
|
113
|
-
declare
|
|
110
|
+
declare function setTaskDoneStep<V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables>(input: {
|
|
114
111
|
backlogVarName: string;
|
|
115
112
|
doneVarName: string;
|
|
116
|
-
})
|
|
113
|
+
}): Step<V, SV>;
|
|
117
114
|
|
|
118
|
-
type ImplValidateCommand = string | CommandDescriptor | ValidationCommandFn
|
|
119
|
-
declare function resolveImplValidateCommand(implValidateCommand: ImplValidateCommand): ValidationCommandFn
|
|
115
|
+
type ImplValidateCommand<V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables> = string | CommandDescriptor | ValidationCommandFn<V, SV>;
|
|
116
|
+
declare function resolveImplValidateCommand<V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables>(implValidateCommand: ImplValidateCommand<V, SV>): ValidationCommandFn<V, SV>;
|
|
120
117
|
|
|
121
118
|
type YmlBacklogContextsOptions<Item extends BaseBacklogItem = BaseBacklogItem> = {
|
|
122
119
|
backlogFilePath: string;
|
|
@@ -126,29 +123,29 @@ type YmlBacklogContextsOptions<Item extends BaseBacklogItem = BaseBacklogItem> =
|
|
|
126
123
|
ignored?: boolean;
|
|
127
124
|
}>;
|
|
128
125
|
};
|
|
129
|
-
declare function ymlBacklogContexts<Item extends BaseBacklogItem = BaseBacklogItem>({ backlogFilePath, parseItem, parseContext, }: YmlBacklogContextsOptions<Item>): GetContextListFn
|
|
126
|
+
declare function ymlBacklogContexts<Item extends BaseBacklogItem = BaseBacklogItem, V extends LumpVariables = LumpVariables>({ backlogFilePath, parseItem, parseContext, }: YmlBacklogContextsOptions<Item>): GetContextListFn<V>;
|
|
130
127
|
|
|
131
128
|
/** Validates and normalizes one backlog item; throws on invalid input. */
|
|
132
129
|
declare function validateBaseBacklogItem(raw: unknown, location: string): BaseBacklogItem;
|
|
133
130
|
|
|
134
|
-
type AbstractionBacklogOptions = {
|
|
135
|
-
implValidateCommand?: ValidationCommandFn | string;
|
|
131
|
+
type AbstractionBacklogOptions<V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables> = {
|
|
132
|
+
implValidateCommand?: ValidationCommandFn<V, SV> | string;
|
|
136
133
|
/** Lump config module URL — pass `import.meta.url` from `config.ts`. */
|
|
137
134
|
configUrl: string | URL;
|
|
138
|
-
implSteps?: LumpJsConfig['steps'];
|
|
139
|
-
} & Omit<LumpJsConfig, 'contextListJson' | 'contextMatchFn' | 'getContextListFn' | 'prompt' | 'steps'>;
|
|
140
|
-
declare const abstractionBacklog:
|
|
135
|
+
implSteps?: LumpJsConfig<V, SV>['steps'];
|
|
136
|
+
} & Omit<LumpJsConfig<V, SV>, 'contextListJson' | 'contextMatchFn' | 'getContextListFn' | 'prompt' | 'steps'>;
|
|
137
|
+
declare const abstractionBacklog: <V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables>(options: AbstractionBacklogOptions<V, SV>) => LumpJsConfig<V, SV>;
|
|
141
138
|
|
|
142
|
-
type AbstractionFinderOptions = {
|
|
139
|
+
type AbstractionFinderOptions<V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables> = {
|
|
143
140
|
scanDirectories?: string[];
|
|
144
141
|
customPrompt?(): string;
|
|
145
142
|
maxPendingAbstractions?: number;
|
|
146
143
|
backlogItemsDir: string;
|
|
147
|
-
} & LumpJsConfig
|
|
148
|
-
declare const abstractionFinder:
|
|
144
|
+
} & LumpJsConfig<V, SV>;
|
|
145
|
+
declare const abstractionFinder: <V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables>(options: AbstractionFinderOptions<V, SV>) => LumpJsConfig<V, SV>;
|
|
149
146
|
|
|
150
|
-
type BacklogStageDefinition = {
|
|
151
|
-
steps: NonNullable<LumpJsConfig['steps']>;
|
|
147
|
+
type BacklogStageDefinition<V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables> = {
|
|
148
|
+
steps: NonNullable<LumpJsConfig<V, SV>['steps']>;
|
|
152
149
|
completion: 'keepPending' | 'moveToDone';
|
|
153
150
|
};
|
|
154
151
|
type BacklogItemResolution<StageName extends string> = {
|
|
@@ -156,10 +153,11 @@ type BacklogItemResolution<StageName extends string> = {
|
|
|
156
153
|
} | {
|
|
157
154
|
stage: StageName;
|
|
158
155
|
contextName?: string;
|
|
156
|
+
/** Context variables (not lump/step variable bags). */
|
|
159
157
|
variables?: LumpVariables;
|
|
160
158
|
additionalDependsOnContexts?: string[];
|
|
161
159
|
};
|
|
162
|
-
type BacklogOptions<Item extends BaseBacklogItem, Stages extends Record<string, BacklogStageDefinition>> = {
|
|
160
|
+
type BacklogOptions<Item extends BaseBacklogItem, V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables, Stages extends Record<string, BacklogStageDefinition<V, SV>> = Record<string, BacklogStageDefinition<V, SV>>> = {
|
|
163
161
|
configUrl: string | URL;
|
|
164
162
|
backlogItemsDir?: string;
|
|
165
163
|
stages: Stages;
|
|
@@ -168,15 +166,15 @@ type BacklogOptions<Item extends BaseBacklogItem, Stages extends Record<string,
|
|
|
168
166
|
item: Item;
|
|
169
167
|
paths: BacklogPaths;
|
|
170
168
|
}): MaybePromise<BacklogItemResolution<Extract<keyof Stages, string>>>;
|
|
171
|
-
} & Omit<LumpJsConfig, 'contextListJson' | 'contextMatchFn' | 'getContextListFn' | 'prompt' | 'steps'>;
|
|
169
|
+
} & Omit<LumpJsConfig<V, SV>, 'contextListJson' | 'contextMatchFn' | 'getContextListFn' | 'prompt' | 'steps'>;
|
|
172
170
|
declare const BACKLOG_STAGE_VAR = "BACKLOG_STAGE";
|
|
173
171
|
declare const BACKLOG_TASK_NAME_VAR = "TASK_NAME";
|
|
174
172
|
declare const BACKLOG_TASK_VAR = "TASK";
|
|
175
173
|
declare const BACKLOG_ITEMS_DIR_VAR = "BACKLOG_ITEMS_DIR";
|
|
176
174
|
declare const BACKLOG_ITEM_DIR_VAR = "BACKLOG_ITEM_DIR";
|
|
177
175
|
|
|
178
|
-
declare function backlog<Item extends BaseBacklogItem, Stages extends Record<string, BacklogStageDefinition>>(options: BacklogOptions<Item, Stages>): LumpJsConfig
|
|
179
|
-
declare const backlogRecipe:
|
|
176
|
+
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>;
|
|
177
|
+
declare const backlogRecipe: typeof backlog;
|
|
180
178
|
|
|
181
179
|
type FeatureBacklogItem = BaseBacklogItem & {
|
|
182
180
|
manualReq?: boolean;
|
|
@@ -191,14 +189,14 @@ type FeatureBacklogContextVariables = {
|
|
|
191
189
|
REQ_FILE?: string;
|
|
192
190
|
TEST_PLAN_FILE?: string;
|
|
193
191
|
};
|
|
194
|
-
type FeatureBacklogOptions = {
|
|
192
|
+
type FeatureBacklogOptions<V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables> = {
|
|
195
193
|
configUrl: string | URL;
|
|
196
194
|
baseBranch: string;
|
|
197
|
-
implValidateCommand?: ValidationCommandFn | string;
|
|
195
|
+
implValidateCommand?: ValidationCommandFn<V, SV> | string;
|
|
198
196
|
backlogItemsDir?: string;
|
|
199
|
-
} & Omit<LumpJsConfig, 'contextListJson' | 'contextMatchFn' | 'getContextListFn' | 'prompt' | 'steps' | 'baseBranch'>;
|
|
197
|
+
} & Omit<LumpJsConfig<V, SV>, 'contextListJson' | 'contextMatchFn' | 'getContextListFn' | 'prompt' | 'steps' | 'baseBranch'>;
|
|
200
198
|
declare function resolveFeatureBacklogItem(item: FeatureBacklogItem, paths: BacklogPaths, projectRoot: string, lumpName: string, baseBranch: string): Promise<BacklogItemResolution<FeatureBacklogStage>>;
|
|
201
|
-
declare const featureBacklog:
|
|
199
|
+
declare const featureBacklog: <V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables>(options: FeatureBacklogOptions<V, SV>) => LumpJsConfig<V, SV>;
|
|
202
200
|
|
|
203
201
|
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
202
|
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
|
@@ -55,18 +55,16 @@ function ephemeralContextListFn(options = {}) {
|
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
const GET_RECURSIVE_STEPS_IS_OK_FLAG_KEY = '__getRecursiveSteps_isOk__';
|
|
59
58
|
function stepIndexDepth(stepIndex) {
|
|
60
59
|
return Array.isArray(stepIndex) ? stepIndex.length : 1;
|
|
61
60
|
}
|
|
62
61
|
/** Agent prompt(s) followed by a validation command, retried until checks pass or `maxIterations` is reached. */
|
|
63
|
-
function getRecursiveSteps({ maxIterations = 5, validationCommandFn = () => null, isValidationCommandResultOk = ({ commandSucceeded }) => commandSucceeded, getFirstSteps = () => [], currentIteration = 0, prevValidateCommandResult = null, prevValidateCommandDescriptor = null,
|
|
62
|
+
function getRecursiveSteps({ maxIterations = 5, validationCommandFn = () => null, isValidationCommandResultOk = ({ commandSucceeded }) => commandSucceeded, getFirstSteps = () => [], currentIteration = 0, prevValidateCommandResult = null, prevValidateCommandDescriptor = null, }) {
|
|
64
63
|
const firstSteps = getFirstSteps({
|
|
65
64
|
currentIteration,
|
|
66
65
|
prevValidateCommandResult,
|
|
67
66
|
prevValidateCommandDescriptor,
|
|
68
67
|
});
|
|
69
|
-
let thisIterValidateCommandResult = null;
|
|
70
68
|
let thisIterValidateCommandDescriptor = null;
|
|
71
69
|
return [
|
|
72
70
|
...normalizeSteps({
|
|
@@ -81,43 +79,35 @@ function getRecursiveSteps({ maxIterations = 5, validationCommandFn = () => null
|
|
|
81
79
|
args: ['Loop limit reached'],
|
|
82
80
|
};
|
|
83
81
|
}
|
|
84
|
-
|
|
85
|
-
const validateCommandDescriptor = await validationCommandFn({
|
|
86
|
-
...input,
|
|
87
|
-
currentIteration,
|
|
88
|
-
prevValidateCommandResult,
|
|
89
|
-
contextRunStateIsOkFlagKey,
|
|
90
|
-
});
|
|
91
|
-
thisIterValidateCommandDescriptor = validateCommandDescriptor || null;
|
|
92
|
-
return validateCommandDescriptor;
|
|
93
|
-
}
|
|
94
|
-
return null;
|
|
95
|
-
},
|
|
96
|
-
postCommandExecFn(input) {
|
|
97
|
-
thisIterValidateCommandResult = input.commandResult;
|
|
98
|
-
input.contextRunState[contextRunStateIsOkFlagKey] = isValidationCommandResultOk({
|
|
82
|
+
const validateCommandDescriptor = await validationCommandFn({
|
|
99
83
|
...input,
|
|
100
84
|
currentIteration,
|
|
85
|
+
prevValidateCommandResult,
|
|
101
86
|
});
|
|
87
|
+
thisIterValidateCommandDescriptor = validateCommandDescriptor || null;
|
|
88
|
+
return validateCommandDescriptor;
|
|
102
89
|
},
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
90
|
+
async postCommandExecFn(input) {
|
|
91
|
+
if (stepIndexDepth(input.stepIndex) > maxIterations) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (isValidationCommandResultOk({
|
|
95
|
+
...input,
|
|
96
|
+
currentIteration,
|
|
97
|
+
})) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
return getRecursiveSteps({
|
|
111
101
|
maxIterations,
|
|
112
102
|
validationCommandFn,
|
|
113
103
|
isValidationCommandResultOk,
|
|
114
104
|
getFirstSteps,
|
|
115
105
|
currentIteration: currentIteration + 1,
|
|
116
|
-
prevValidateCommandResult:
|
|
106
|
+
prevValidateCommandResult: input.commandResult,
|
|
117
107
|
prevValidateCommandDescriptor: thisIterValidateCommandDescriptor,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
continueOnError: currentIteration < maxIterations,
|
|
121
111
|
},
|
|
122
112
|
];
|
|
123
113
|
}
|
|
@@ -161,17 +151,16 @@ function defaultFixSteps(input) {
|
|
|
161
151
|
];
|
|
162
152
|
}
|
|
163
153
|
/** Work steps, validation command, and optional fix steps — retried until checks pass or `maxIterations`. */
|
|
164
|
-
function retryUntilGreen({ steps, fixSteps, validationCommandFn, isValidationCommandResultOk,
|
|
154
|
+
function retryUntilGreen({ steps, fixSteps, validationCommandFn, isValidationCommandResultOk, maxIterations, }) {
|
|
165
155
|
return getRecursiveSteps({
|
|
166
156
|
maxIterations,
|
|
167
157
|
validationCommandFn,
|
|
168
158
|
isValidationCommandResultOk,
|
|
169
|
-
contextRunStateIsOkFlagKey,
|
|
170
159
|
getFirstSteps({ currentIteration, prevValidateCommandResult, prevValidateCommandDescriptor }) {
|
|
171
160
|
if (currentIteration === 0) {
|
|
172
161
|
return steps;
|
|
173
162
|
}
|
|
174
|
-
return (fixSteps ?? defaultFixSteps)({
|
|
163
|
+
return (fixSteps ?? (defaultFixSteps))({
|
|
175
164
|
currentIteration,
|
|
176
165
|
prevValidateCommandResult,
|
|
177
166
|
prevValidateCommandDescriptor,
|
|
@@ -195,24 +184,26 @@ function projectRootFromConfigUrl(configUrl) {
|
|
|
195
184
|
return path$1.resolve(configDir, '../../..');
|
|
196
185
|
}
|
|
197
186
|
|
|
198
|
-
/**
|
|
187
|
+
/** Validation command that fails when the artifact referenced by a context variable was not created. */
|
|
199
188
|
function requireArtifactStep(artifactPathVarName) {
|
|
200
|
-
return {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
throw new Error(`Expected artifact at ${artifactPath} was not created`);
|
|
210
|
-
}
|
|
189
|
+
return async ({ context, workspacePath }) => {
|
|
190
|
+
const artifactPath = context.variables[artifactPathVarName];
|
|
191
|
+
if (typeof artifactPath !== 'string' || artifactPath.trim() === '') {
|
|
192
|
+
throw new Error(`Missing context variable ${artifactPathVarName}`);
|
|
193
|
+
}
|
|
194
|
+
const fullPath = path$1.join(workspacePath, artifactPath);
|
|
195
|
+
const exists = await pathExists(fullPath);
|
|
196
|
+
if (!exists) {
|
|
197
|
+
const message = `Expected artifact at ${artifactPath} was not created`;
|
|
211
198
|
return {
|
|
212
199
|
executable: 'node',
|
|
213
|
-
args: ['-e',
|
|
200
|
+
args: ['-e', `console.error(${JSON.stringify(message)}); process.exit(1)`],
|
|
214
201
|
};
|
|
215
|
-
}
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
executable: 'node',
|
|
205
|
+
args: ['-e', 'process.exit(0)'],
|
|
206
|
+
};
|
|
216
207
|
};
|
|
217
208
|
}
|
|
218
209
|
|
|
@@ -348,7 +339,7 @@ function isPlainObject(value) {
|
|
|
348
339
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
349
340
|
}
|
|
350
341
|
/** Moves a finished backlog item folder from todo/ to completed/ after the context completes. */
|
|
351
|
-
|
|
342
|
+
function folderSetTaskDoneStep(input) {
|
|
352
343
|
const nameVarName = input.nameVarName ?? 'TASK_NAME';
|
|
353
344
|
return {
|
|
354
345
|
async commandFn({ context, workspacePath }) {
|
|
@@ -389,7 +380,7 @@ const folderSetTaskDoneStep = (input) => {
|
|
|
389
380
|
},
|
|
390
381
|
continueOnError: true,
|
|
391
382
|
};
|
|
392
|
-
}
|
|
383
|
+
}
|
|
393
384
|
|
|
394
385
|
let setTaskDoneDeprecatedWarned = false;
|
|
395
386
|
function warnSetTaskDoneDeprecated() {
|
|
@@ -401,7 +392,7 @@ function warnSetTaskDoneDeprecated() {
|
|
|
401
392
|
'YAML backlog helpers will be removed in a future major version.');
|
|
402
393
|
}
|
|
403
394
|
/** Moves a finished backlog item from BACKLOG.yml to DONE.yml after the context completes. */
|
|
404
|
-
|
|
395
|
+
function setTaskDoneStep(input) {
|
|
405
396
|
return {
|
|
406
397
|
async commandFn({ context, workspacePath }) {
|
|
407
398
|
warnSetTaskDoneDeprecated();
|
|
@@ -430,7 +421,7 @@ const setTaskDoneStep = (input) => {
|
|
|
430
421
|
},
|
|
431
422
|
continueOnError: true,
|
|
432
423
|
};
|
|
433
|
-
}
|
|
424
|
+
}
|
|
434
425
|
|
|
435
426
|
function resolveImplValidateCommand(implValidateCommand) {
|
|
436
427
|
if (typeof implValidateCommand === 'function') {
|
|
@@ -481,6 +472,7 @@ function ymlBacklogContexts({ backlogFilePath, parseItem, parseContext, }) {
|
|
|
481
472
|
};
|
|
482
473
|
}
|
|
483
474
|
|
|
475
|
+
/** Identity helper — must preserve a generic `<V, SV>` function signature. */
|
|
484
476
|
function defineRecipe(recipe) {
|
|
485
477
|
return recipe;
|
|
486
478
|
}
|
|
@@ -567,12 +559,13 @@ function backlog(options) {
|
|
|
567
559
|
...rest,
|
|
568
560
|
});
|
|
569
561
|
}
|
|
570
|
-
const backlogRecipe = defineRecipe(
|
|
562
|
+
const backlogRecipe = defineRecipe(backlog);
|
|
571
563
|
|
|
572
|
-
const abstractionBacklog = defineRecipe((options)
|
|
564
|
+
const abstractionBacklog = defineRecipe(function abstractionBacklog(options) {
|
|
573
565
|
const { implValidateCommand, configUrl, implSteps, ...rest } = options;
|
|
574
566
|
const projectRoot = projectRootFromConfigUrl(configUrl);
|
|
575
|
-
const runImplValidation = resolveImplValidateCommand(implValidateCommand ??
|
|
567
|
+
const runImplValidation = resolveImplValidateCommand(implValidateCommand ??
|
|
568
|
+
'echo "No implementation validation command provided. I say, trust but verify, but well..."');
|
|
576
569
|
return backlog({
|
|
577
570
|
configUrl,
|
|
578
571
|
async resolveItem({ item, paths }) {
|
|
@@ -619,7 +612,7 @@ const abstractionBacklog = defineRecipe((options) => {
|
|
|
619
612
|
});
|
|
620
613
|
});
|
|
621
614
|
|
|
622
|
-
const abstractionFinder = defineRecipe((options)
|
|
615
|
+
const abstractionFinder = defineRecipe(function abstractionFinder(options) {
|
|
623
616
|
const { maxPendingAbstractions = 5, scanDirectories, customPrompt, backlogItemsDir, } = options; // TODO : check if pending abstcations in backlog are less than maxPendingAbstractions
|
|
624
617
|
return defineConfig({
|
|
625
618
|
...options,
|
|
@@ -739,10 +732,11 @@ async function resolveFeatureBacklogItem(item, paths, projectRoot, lumpName, bas
|
|
|
739
732
|
},
|
|
740
733
|
};
|
|
741
734
|
}
|
|
742
|
-
const featureBacklog = defineRecipe((options)
|
|
735
|
+
const featureBacklog = defineRecipe(function featureBacklog(options) {
|
|
743
736
|
const { configUrl, baseBranch, implValidateCommand, backlogItemsDir, ...rest } = options;
|
|
744
737
|
const projectRoot = projectRootFromConfigUrl(configUrl);
|
|
745
|
-
const runImplValidation = resolveImplValidateCommand(implValidateCommand ??
|
|
738
|
+
const runImplValidation = resolveImplValidateCommand(implValidateCommand ??
|
|
739
|
+
'echo "No implementation validation command provided. I say, trust but verify, but well..."');
|
|
746
740
|
return backlog({
|
|
747
741
|
configUrl,
|
|
748
742
|
backlogItemsDir,
|
|
@@ -764,12 +758,13 @@ const featureBacklog = defineRecipe((options) => {
|
|
|
764
758
|
stages: {
|
|
765
759
|
makeReq: {
|
|
766
760
|
completion: 'keepPending',
|
|
767
|
-
steps:
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
761
|
+
steps: retryUntilGreen({
|
|
762
|
+
steps: [
|
|
763
|
+
{
|
|
764
|
+
promptFn({ context: ctx }) {
|
|
765
|
+
const vars = ctx.variables;
|
|
766
|
+
const { BACKLOG_ITEM_DIR, TASK_NAME, TASK, REQ_FILE } = vars;
|
|
767
|
+
return `
|
|
773
768
|
Write a requirements document for the following backlog item from @${BACKLOG_ITEM_DIR}/desc.yml.
|
|
774
769
|
|
|
775
770
|
Task name: ${TASK_NAME}
|
|
@@ -789,21 +784,42 @@ The requirements document should be self-contained and implementation-ready. Inc
|
|
|
789
784
|
- Acceptance criteria
|
|
790
785
|
|
|
791
786
|
Do not implement the feature — only create the requirements markdown file.
|
|
787
|
+
Do not wait the user to answer any questions — make the best assumptions and just write the requirements document.
|
|
792
788
|
The requirements document should not contain any testing strategy details.
|
|
793
|
-
|
|
789
|
+
`.trim();
|
|
790
|
+
},
|
|
794
791
|
},
|
|
795
|
-
|
|
796
|
-
requireArtifactStep('REQ_FILE'),
|
|
797
|
-
|
|
792
|
+
],
|
|
793
|
+
validationCommandFn: requireArtifactStep('REQ_FILE'),
|
|
794
|
+
fixSteps: ({ prevValidateCommandResult }) => [
|
|
795
|
+
{
|
|
796
|
+
promptFn({ context: ctx }) {
|
|
797
|
+
const vars = ctx.variables;
|
|
798
|
+
const { BACKLOG_ITEM_DIR, REQ_FILE } = vars;
|
|
799
|
+
return `
|
|
800
|
+
The requirements document was not created at @${REQ_FILE}.
|
|
801
|
+
|
|
802
|
+
Create it now at that exact path. Do not edit @${BACKLOG_ITEM_DIR}/desc.yml.
|
|
803
|
+
Do not implement the feature — only write the requirements markdown file.
|
|
804
|
+
The requirements document should not contain any testing strategy details.
|
|
805
|
+
|
|
806
|
+
Verification output:
|
|
807
|
+
${prevValidateCommandResult ?? '(no output captured)'}
|
|
808
|
+
`.trim();
|
|
809
|
+
},
|
|
810
|
+
},
|
|
811
|
+
],
|
|
812
|
+
}),
|
|
798
813
|
},
|
|
799
814
|
makeTestPlan: {
|
|
800
815
|
completion: 'keepPending',
|
|
801
|
-
steps:
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
816
|
+
steps: retryUntilGreen({
|
|
817
|
+
steps: [
|
|
818
|
+
{
|
|
819
|
+
promptFn({ context: ctx }) {
|
|
820
|
+
const vars = ctx.variables;
|
|
821
|
+
const { BACKLOG_ITEM_DIR, TASK_NAME, TASK, REQ_FILE, TEST_PLAN_FILE } = vars;
|
|
822
|
+
return `
|
|
807
823
|
Write a test plan for the following backlog item from @${BACKLOG_ITEM_DIR}/desc.yml.
|
|
808
824
|
|
|
809
825
|
Task name: ${TASK_NAME}
|
|
@@ -819,11 +835,29 @@ The test plan should be self-contained and implementation-ready. Include:
|
|
|
819
835
|
- Test data
|
|
820
836
|
- Test expectations
|
|
821
837
|
- Test implementation details
|
|
822
|
-
|
|
838
|
+
`.trim();
|
|
839
|
+
},
|
|
823
840
|
},
|
|
824
|
-
|
|
825
|
-
requireArtifactStep('TEST_PLAN_FILE'),
|
|
826
|
-
|
|
841
|
+
],
|
|
842
|
+
validationCommandFn: requireArtifactStep('TEST_PLAN_FILE'),
|
|
843
|
+
fixSteps: ({ prevValidateCommandResult }) => [
|
|
844
|
+
{
|
|
845
|
+
promptFn({ context: ctx }) {
|
|
846
|
+
const vars = ctx.variables;
|
|
847
|
+
const { BACKLOG_ITEM_DIR, REQ_FILE, TEST_PLAN_FILE } = vars;
|
|
848
|
+
return `
|
|
849
|
+
The test plan was not created at @${TEST_PLAN_FILE}.
|
|
850
|
+
|
|
851
|
+
Create it now at that exact path. Match the requirements in @${REQ_FILE}.
|
|
852
|
+
Do not edit @${BACKLOG_ITEM_DIR}/desc.yml nor @${REQ_FILE}.
|
|
853
|
+
|
|
854
|
+
Verification output:
|
|
855
|
+
${prevValidateCommandResult ?? '(no output captured)'}
|
|
856
|
+
`.trim();
|
|
857
|
+
},
|
|
858
|
+
},
|
|
859
|
+
],
|
|
860
|
+
}),
|
|
827
861
|
},
|
|
828
862
|
testImpl: {
|
|
829
863
|
completion: 'keepPending',
|
|
@@ -835,6 +869,8 @@ The test plan should be self-contained and implementation-ready. Include:
|
|
|
835
869
|
return `
|
|
836
870
|
Write a test implementation for the following backlog item from @${BACKLOG_ITEM_DIR}/desc.yml.
|
|
837
871
|
|
|
872
|
+
The new tests should be skipped in order to not break the whole test suite.
|
|
873
|
+
|
|
838
874
|
Task name: ${TASK_NAME}
|
|
839
875
|
Task:
|
|
840
876
|
${TASK}
|
|
@@ -857,7 +893,8 @@ The requirements for this task are in @${REQ_FILE}.
|
|
|
857
893
|
return `
|
|
858
894
|
Implement the feature described in @${REQ_FILE}.
|
|
859
895
|
The tests have already been implemented according to the test plan in @${TEST_PLAN_FILE}.
|
|
860
|
-
|
|
896
|
+
Unskip all the tests that were skipped in the tests implementation.
|
|
897
|
+
The implementation should make the tests pass. Do not edit any test file except to unskip them or if absolutely necessary.
|
|
861
898
|
`.trim();
|
|
862
899
|
},
|
|
863
900
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumpcode/recipes",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
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.0.
|
|
48
|
-
"@lumpcode/core": "^0.0.
|
|
47
|
+
"@lumpcode/cli-utils": "^0.0.14",
|
|
48
|
+
"@lumpcode/core": "^0.0.14",
|
|
49
49
|
"js-yaml": "^5.0.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|