@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 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`, `projectRootFromConfigUrl`
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 { LumpJsConfig } from '@lumpcode/cli-types';
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
- ...featureBacklog({
88
- baseBranch: 'dev',
89
- command: 'cursor',
90
- configUrl: import.meta.url,
91
- registerCommands: ['cursor'],
92
- maximumNumberOfConcurrentBranches: 5,
93
- verbose: true,
94
- keepHistory: true,
95
- lumpVariables: { model: 'composer-2.5' },
96
- discoveryBranch: 'dev',
97
- implValidateCommand: [
98
- 'npm run build -w=@lumpcode/cli',
99
- 'npm run test -w=@lumpcode/cli',
100
- ].join(' && '),
101
- }),
102
- } satisfies LumpJsConfig;
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 { LumpJsConfig } from '@lumpcode/cli-types';
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
- ...abstractionFinder({
116
- maxPendingAbstractions: 5,
117
- scanDirectories: ['packages/apps/cli'],
118
- backlogItemsDir: '.lumpcode/lumps/abstractionImplementer/backlogItems',
119
- command: 'cursor',
120
- lumpVariables: { model: 'composer-2.5' },
121
- discoveryBranch: 'dev',
122
- }),
123
- } satisfies LumpJsConfig;
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 { LumpJsConfig } from '@lumpcode/cli-types';
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
- ...abstractionBacklog({
133
- baseBranch: 'dev',
134
- command: 'cursor',
135
- configUrl: import.meta.url,
136
- registerCommands: ['cursor'],
137
- maximumNumberOfConcurrentBranches: 3,
138
- verbose: true,
139
- keepHistory: true,
140
- lumpVariables: { model: 'composer-2.5' },
141
- discoveryBranch: 'dev',
142
- }),
143
- } satisfies LumpJsConfig;
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-types';
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, contextRunStateIsOkFlagKey = GET_RECURSIVE_STEPS_IS_OK_FLAG_KEY, }) {
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
- if (!input.contextRunState[contextRunStateIsOkFlagKey]) {
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
- continueOnError: currentIteration < maxIterations,
106
- },
107
- ({ contextRunState, stepIndex }) => {
108
- if (stepIndexDepth(stepIndex) > maxIterations) {
109
- return [];
110
- }
111
- return !contextRunState[contextRunStateIsOkFlagKey]
112
- ? getRecursiveSteps({
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: thisIterValidateCommandResult,
108
+ prevValidateCommandResult: input.commandResult,
119
109
  prevValidateCommandDescriptor: thisIterValidateCommandDescriptor,
120
- contextRunStateIsOkFlagKey,
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, contextRunStateIsOkFlagKey, maxIterations, }) {
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
- /** Fails the context when the artifact referenced by a context variable was not created. */
189
+ /** Validation command that fails when the artifact referenced by a context variable was not created. */
201
190
  function requireArtifactStep(artifactPathVarName) {
202
- return {
203
- async commandFn({ context, workspacePath }) {
204
- const artifactPath = context.variables[artifactPathVarName];
205
- if (typeof artifactPath !== 'string' || artifactPath.trim() === '') {
206
- throw new Error(`Missing context variable ${artifactPathVarName}`);
207
- }
208
- const fullPath = path$1.join(workspacePath, artifactPath);
209
- const exists = await core.pathExists(fullPath);
210
- if (!exists) {
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', 'process.exit(0)'],
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
- const folderSetTaskDoneStep = (input) => {
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
- const setTaskDoneStep = (input) => {
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((options) => backlog(options));
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 ?? 'echo "No implementation validation command provided. I say, trust but verify, but well..."');
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 ?? 'echo "No implementation validation command provided. I say, trust but verify, but well..."');
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
- promptFn({ context: ctx }) {
772
- const vars = ctx.variables;
773
- const { BACKLOG_ITEM_DIR, TASK_NAME, TASK, REQ_FILE } = vars;
774
- return `
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
- `.trim();
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
- promptFn({ context: ctx }) {
806
- const vars = ctx.variables;
807
- const { BACKLOG_ITEM_DIR, TASK_NAME, TASK, REQ_FILE, TEST_PLAN_FILE } = vars;
808
- return `
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
- `.trim();
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
- The implementation should make the tests pass. Do not edit any test file.
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, LumpVariables, MaybePromise as MaybePromise$1, Context } from '@lumpcode/cli-utils';
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>[0] & {
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>[0] & {
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, contextRunStateIsOkFlagKey, }: GetRecursiveStepsOptions): LumpJsConfigSteps;
53
-
54
- declare function defaultFixSteps(input: GetFirstStepsInput): LumpJsConfig['steps'];
55
- interface RetryUntilGreenInput {
56
- steps: LumpJsConfig['steps'];
57
- fixSteps?: typeof defaultFixSteps;
58
- validationCommandFn: ValidationCommandFn;
59
- isValidationCommandResultOk?: (input: IsValidationCommandResultOkInput) => boolean;
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, contextRunStateIsOkFlagKey, maxIterations, }: RetryUntilGreenInput): LumpJsConfigSteps;
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
- /** Fails the context when the artifact referenced by a context variable was not created. */
72
- declare function requireArtifactStep(artifactPathVarName: string): Step;
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
- declare function defineRecipe<Options, V extends LumpVariables = LumpVariables>(recipe: Recipe<Options, V>): Recipe<Options, V>;
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 const folderSetTaskDoneStep: (input: {
104
+ declare function folderSetTaskDoneStep<V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables>(input: {
108
105
  itemsDirVarName: string;
109
106
  nameVarName?: string;
110
- }) => Step;
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 const setTaskDoneStep: (input: {
110
+ declare function setTaskDoneStep<V extends LumpVariables = LumpVariables, SV extends StepVariables = StepVariables>(input: {
114
111
  backlogVarName: string;
115
112
  doneVarName: string;
116
- }) => Step;
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: Recipe<AbstractionBacklogOptions>;
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: Recipe<AbstractionFinderOptions>;
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: Recipe<BacklogOptions<BaseBacklogItem, Record<string, BacklogStageDefinition>>>;
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: Recipe<FeatureBacklogOptions>;
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, contextRunStateIsOkFlagKey = GET_RECURSIVE_STEPS_IS_OK_FLAG_KEY, }) {
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
- if (!input.contextRunState[contextRunStateIsOkFlagKey]) {
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
- continueOnError: currentIteration < maxIterations,
104
- },
105
- ({ contextRunState, stepIndex }) => {
106
- if (stepIndexDepth(stepIndex) > maxIterations) {
107
- return [];
108
- }
109
- return !contextRunState[contextRunStateIsOkFlagKey]
110
- ? getRecursiveSteps({
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: thisIterValidateCommandResult,
106
+ prevValidateCommandResult: input.commandResult,
117
107
  prevValidateCommandDescriptor: thisIterValidateCommandDescriptor,
118
- contextRunStateIsOkFlagKey,
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, contextRunStateIsOkFlagKey, maxIterations, }) {
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
- /** Fails the context when the artifact referenced by a context variable was not created. */
187
+ /** Validation command that fails when the artifact referenced by a context variable was not created. */
199
188
  function requireArtifactStep(artifactPathVarName) {
200
- return {
201
- async commandFn({ context, workspacePath }) {
202
- const artifactPath = context.variables[artifactPathVarName];
203
- if (typeof artifactPath !== 'string' || artifactPath.trim() === '') {
204
- throw new Error(`Missing context variable ${artifactPathVarName}`);
205
- }
206
- const fullPath = path$1.join(workspacePath, artifactPath);
207
- const exists = await pathExists(fullPath);
208
- if (!exists) {
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', 'process.exit(0)'],
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
- const folderSetTaskDoneStep = (input) => {
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
- const setTaskDoneStep = (input) => {
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((options) => backlog(options));
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 ?? 'echo "No implementation validation command provided. I say, trust but verify, but well..."');
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 ?? 'echo "No implementation validation command provided. I say, trust but verify, but well..."');
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
- promptFn({ context: ctx }) {
770
- const vars = ctx.variables;
771
- const { BACKLOG_ITEM_DIR, TASK_NAME, TASK, REQ_FILE } = vars;
772
- return `
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
- `.trim();
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
- promptFn({ context: ctx }) {
804
- const vars = ctx.variables;
805
- const { BACKLOG_ITEM_DIR, TASK_NAME, TASK, REQ_FILE, TEST_PLAN_FILE } = vars;
806
- return `
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
- `.trim();
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
- The implementation should make the tests pass. Do not edit any test file.
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.15",
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.12",
48
- "@lumpcode/core": "^0.0.12",
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": {