@lumpcode/recipes 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -7
- package/dist/index.cjs +498 -260
- package/dist/index.d.ts +66 -14
- package/dist/index.js +485 -260
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -390,26 +390,54 @@ async function listTodoFolderNames(todoDir) {
|
|
|
390
390
|
throw error;
|
|
391
391
|
}
|
|
392
392
|
}
|
|
393
|
-
/**
|
|
394
|
-
async function
|
|
393
|
+
/** Live and completed ticket folder names for an umbrella parent, sorted uniquely. */
|
|
394
|
+
async function listUmbrellaTicketNames(input) {
|
|
395
|
+
const { todoDir, parentName } = input;
|
|
396
|
+
const completedDir = path$1.join(path$1.dirname(todoDir), 'completed');
|
|
397
|
+
const liveTicketNames = await listTodoFolderNames(path$1.join(todoDir, parentName, 'tickets'));
|
|
398
|
+
const completedTicketNames = await listTodoFolderNames(path$1.join(completedDir, parentName, 'tickets'));
|
|
399
|
+
return [...new Set([...liveTicketNames, ...completedTicketNames])].sort();
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Path relative to `todo/`, using `/` so it is stable in context variables.
|
|
403
|
+
* A top-level folder is an umbrella (never itself an item) when it has live
|
|
404
|
+
* tickets, a leftover `tickets/` directory, or tickets already under
|
|
405
|
+
* sibling `completed/<name>/tickets/`.
|
|
406
|
+
*/
|
|
407
|
+
async function listTodoRelativeDirs(todoDir, options) {
|
|
408
|
+
const includeUmbrellaParents = options?.includeUmbrellaParents ?? false;
|
|
395
409
|
const topNames = await listTodoFolderNames(todoDir);
|
|
410
|
+
const completedDir = path$1.join(path$1.dirname(todoDir), 'completed');
|
|
396
411
|
const relativeDirs = [];
|
|
397
412
|
for (const name of topNames) {
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
413
|
+
const liveTicketsDir = path$1.join(todoDir, name, 'tickets');
|
|
414
|
+
const liveTicketNames = await listTodoFolderNames(liveTicketsDir);
|
|
415
|
+
const completedTicketNames = await listTodoFolderNames(path$1.join(completedDir, name, 'tickets'));
|
|
416
|
+
const hasAnyTicket = liveTicketNames.length > 0 || completedTicketNames.length > 0;
|
|
417
|
+
if (liveTicketNames.length > 0) {
|
|
418
|
+
for (const ticketName of liveTicketNames) {
|
|
419
|
+
relativeDirs.push(`${name}/tickets/${ticketName}`);
|
|
420
|
+
}
|
|
421
|
+
if (includeUmbrellaParents && hasAnyTicket) {
|
|
422
|
+
relativeDirs.push(name);
|
|
423
|
+
}
|
|
401
424
|
continue;
|
|
402
425
|
}
|
|
403
|
-
|
|
404
|
-
|
|
426
|
+
const isUmbrella = (await pathExists(liveTicketsDir)) || completedTicketNames.length > 0;
|
|
427
|
+
if (isUmbrella) {
|
|
428
|
+
if (includeUmbrellaParents && hasAnyTicket) {
|
|
429
|
+
relativeDirs.push(name);
|
|
430
|
+
}
|
|
431
|
+
continue;
|
|
405
432
|
}
|
|
433
|
+
relativeDirs.push(name);
|
|
406
434
|
}
|
|
407
435
|
return relativeDirs;
|
|
408
436
|
}
|
|
409
|
-
function folderBacklogContexts({ backlogItemsDir, parseItem, parseContext, }) {
|
|
437
|
+
function folderBacklogContexts({ backlogItemsDir, includeUmbrellaParents, parseItem, parseContext, }) {
|
|
410
438
|
return async () => {
|
|
411
439
|
const todoDir = path$1.join(backlogItemsDir, 'todo');
|
|
412
|
-
const folderNames = await listTodoRelativeDirs(todoDir);
|
|
440
|
+
const folderNames = await listTodoRelativeDirs(todoDir, { includeUmbrellaParents });
|
|
413
441
|
const discovered = await Promise.all(folderNames.map(async (folderName) => {
|
|
414
442
|
const descPath = path$1.join(todoDir, folderName, 'desc.yml');
|
|
415
443
|
let rawText;
|
|
@@ -470,6 +498,37 @@ function folderBacklogContexts({ backlogItemsDir, parseItem, parseContext, }) {
|
|
|
470
498
|
function isPlainObject(value) {
|
|
471
499
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
472
500
|
}
|
|
501
|
+
async function listSubdirNames(dir) {
|
|
502
|
+
try {
|
|
503
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
504
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
const err = error;
|
|
508
|
+
if (err.code === 'ENOENT') {
|
|
509
|
+
return [];
|
|
510
|
+
}
|
|
511
|
+
throw error;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
async function mergeTodoUmbrellaParentIntoCompleted(input) {
|
|
515
|
+
const { fromDir, toDir, completedDescPath, updatedDesc } = input;
|
|
516
|
+
await fs.mkdir(toDir, { recursive: true });
|
|
517
|
+
await fs.writeFile(completedDescPath, dump(updatedDesc));
|
|
518
|
+
const entries = await fs.readdir(fromDir, { withFileTypes: true });
|
|
519
|
+
for (const entry of entries) {
|
|
520
|
+
if (entry.name === 'desc.yml') {
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
const src = path$1.join(fromDir, entry.name);
|
|
524
|
+
const dest = path$1.join(toDir, entry.name);
|
|
525
|
+
if (await pathExists(dest)) {
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
await fs.rename(src, dest);
|
|
529
|
+
}
|
|
530
|
+
await fs.rm(fromDir, { recursive: true, force: true });
|
|
531
|
+
}
|
|
473
532
|
/** Moves a finished backlog item folder from todo/ to the same relative path under completed/. */
|
|
474
533
|
function folderSetTaskDoneStep(input) {
|
|
475
534
|
const nameVarName = input.nameVarName ?? 'TASK_NAME';
|
|
@@ -499,10 +558,6 @@ function folderSetTaskDoneStep(input) {
|
|
|
499
558
|
if (!(await pathExists(fromDir))) {
|
|
500
559
|
return null;
|
|
501
560
|
}
|
|
502
|
-
if (await pathExists(toDir)) {
|
|
503
|
-
console.warn(`[lumpcode/recipes] Cannot move backlog item "${taskName ?? relativeFromTodo}": already exists at ${toDir}`);
|
|
504
|
-
return null;
|
|
505
|
-
}
|
|
506
561
|
const rawText = await fs.readFile(descPath, 'utf-8');
|
|
507
562
|
const raw = load(rawText);
|
|
508
563
|
if (!isPlainObject(raw)) {
|
|
@@ -512,6 +567,26 @@ function folderSetTaskDoneStep(input) {
|
|
|
512
567
|
...raw,
|
|
513
568
|
completedAt: new Date().toISOString(),
|
|
514
569
|
};
|
|
570
|
+
const pathSegments = relativeFromTodo.split(/[/\\]/);
|
|
571
|
+
const isTopLevelParent = pathSegments.length === 1;
|
|
572
|
+
const completedTicketsDir = path$1.join(toDir, 'tickets');
|
|
573
|
+
const hasCompletedTickets = (await listSubdirNames(completedTicketsDir)).length > 0;
|
|
574
|
+
if (await pathExists(toDir)) {
|
|
575
|
+
if (isTopLevelParent && hasCompletedTickets) {
|
|
576
|
+
await mergeTodoUmbrellaParentIntoCompleted({
|
|
577
|
+
fromDir,
|
|
578
|
+
toDir,
|
|
579
|
+
completedDescPath,
|
|
580
|
+
updatedDesc: updated,
|
|
581
|
+
});
|
|
582
|
+
return {
|
|
583
|
+
executable: 'cat',
|
|
584
|
+
args: [completedDescPath],
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
console.warn(`[lumpcode/recipes] Cannot move backlog item "${taskName ?? relativeFromTodo}": already exists at ${toDir}`);
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
515
590
|
await fs.mkdir(path$1.dirname(toDir), { recursive: true });
|
|
516
591
|
await fs.rename(fromDir, toDir);
|
|
517
592
|
await fs.writeFile(completedDescPath, dump(updated));
|
|
@@ -648,7 +723,7 @@ function buildStageSteps(stages, stageName) {
|
|
|
648
723
|
return normalized;
|
|
649
724
|
}
|
|
650
725
|
function backlog(options) {
|
|
651
|
-
const { configUrl, backlogItemsDir: backlogItemsDirOverride, stages, parseItem, resolveItem, ...rest } = options;
|
|
726
|
+
const { configUrl, backlogItemsDir: backlogItemsDirOverride, includeUmbrellaParents, stages, parseItem, resolveItem, ...rest } = options;
|
|
652
727
|
const paths = resolveBacklogPaths(configUrl, {
|
|
653
728
|
backlogItemsDir: backlogItemsDirOverride,
|
|
654
729
|
});
|
|
@@ -658,6 +733,7 @@ function backlog(options) {
|
|
|
658
733
|
getContextListFn: async (input) => {
|
|
659
734
|
const listFn = folderBacklogContexts({
|
|
660
735
|
backlogItemsDir: absoluteBacklogItemsDir,
|
|
736
|
+
includeUmbrellaParents,
|
|
661
737
|
parseItem,
|
|
662
738
|
async parseContext(item, folderName) {
|
|
663
739
|
const resolution = await resolveItem({
|
|
@@ -846,32 +922,309 @@ Do not take too much time looking for every possible abstraction. Once you found
|
|
|
846
922
|
`.trim();
|
|
847
923
|
}
|
|
848
924
|
|
|
849
|
-
|
|
850
|
-
|
|
925
|
+
function featureContextVars(context) {
|
|
926
|
+
return context.variables;
|
|
927
|
+
}
|
|
928
|
+
const defaultReqPrompt = ({ context }) => {
|
|
929
|
+
const { BACKLOG_ITEM_DIR, TASK_NAME, TASK, REQ_FILE } = featureContextVars(context);
|
|
930
|
+
return `
|
|
931
|
+
Write a requirements document for the following backlog item from @${BACKLOG_ITEM_DIR}/desc.yml.
|
|
932
|
+
|
|
933
|
+
Task name: ${TASK_NAME}
|
|
934
|
+
|
|
935
|
+
Task:
|
|
936
|
+
${TASK}
|
|
937
|
+
|
|
938
|
+
Save the requirements document to @${REQ_FILE}. Do not edit @${BACKLOG_ITEM_DIR}/desc.yml.
|
|
939
|
+
|
|
940
|
+
The requirements document should be self-contained and implementation-ready. Include:
|
|
941
|
+
- Problem statement and motivation
|
|
942
|
+
- Goals and non-goals
|
|
943
|
+
- User stories / use cases
|
|
944
|
+
- Docs updates (if relevant)
|
|
945
|
+
- Proposed behavior and UX (for CLI work, include command syntax where relevant)
|
|
946
|
+
- Technical approach and affected packages or docs
|
|
947
|
+
- Acceptance criteria
|
|
948
|
+
|
|
949
|
+
Do not implement the feature — only create the requirements markdown file.
|
|
950
|
+
Do not wait the user to answer any questions — make the best assumptions and just write the requirements document.
|
|
951
|
+
The requirements document should not contain any testing strategy details.
|
|
952
|
+
`.trim();
|
|
953
|
+
};
|
|
954
|
+
function defaultReqFixPrompt(prevValidateCommandResult) {
|
|
955
|
+
return ({ context }) => {
|
|
956
|
+
const { BACKLOG_ITEM_DIR, REQ_FILE } = featureContextVars(context);
|
|
957
|
+
return `
|
|
958
|
+
The requirements document was not created at @${REQ_FILE}.
|
|
959
|
+
|
|
960
|
+
Create it now at that exact path. Do not edit @${BACKLOG_ITEM_DIR}/desc.yml.
|
|
961
|
+
Do not implement the feature — only write the requirements markdown file.
|
|
962
|
+
The requirements document should not contain any testing strategy details.
|
|
963
|
+
|
|
964
|
+
Verification output:
|
|
965
|
+
${prevValidateCommandResult ?? '(no output captured)'}
|
|
966
|
+
`.trim();
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
const defaultTestPlanPrompt = ({ context }) => {
|
|
970
|
+
const { BACKLOG_ITEM_DIR, TASK_NAME, TASK, REQ_FILE, TEST_PLAN_FILE } = featureContextVars(context);
|
|
971
|
+
return `
|
|
972
|
+
Write a test plan for the following backlog item from @${BACKLOG_ITEM_DIR}/desc.yml.
|
|
973
|
+
|
|
974
|
+
Task name: ${TASK_NAME}
|
|
975
|
+
Task:
|
|
976
|
+
${TASK}
|
|
977
|
+
|
|
978
|
+
The requirements for this task are in @${REQ_FILE}. The test plan should match those requirements.
|
|
979
|
+
|
|
980
|
+
Save the test plan to @${TEST_PLAN_FILE}. Do not edit @${BACKLOG_ITEM_DIR}/desc.yml nor @${REQ_FILE}.
|
|
981
|
+
|
|
982
|
+
The test plan should be self-contained and implementation-ready. Include:
|
|
983
|
+
- Test cases
|
|
984
|
+
- Test data
|
|
985
|
+
- Test expectations
|
|
986
|
+
- Test implementation details
|
|
987
|
+
`.trim();
|
|
988
|
+
};
|
|
989
|
+
function defaultTestPlanFixPrompt(prevValidateCommandResult) {
|
|
990
|
+
return ({ context }) => {
|
|
991
|
+
const { BACKLOG_ITEM_DIR, REQ_FILE, TEST_PLAN_FILE } = featureContextVars(context);
|
|
992
|
+
return `
|
|
993
|
+
The test plan was not created at @${TEST_PLAN_FILE}.
|
|
994
|
+
|
|
995
|
+
Create it now at that exact path. Match the requirements in @${REQ_FILE}.
|
|
996
|
+
Do not edit @${BACKLOG_ITEM_DIR}/desc.yml nor @${REQ_FILE}.
|
|
997
|
+
|
|
998
|
+
Verification output:
|
|
999
|
+
${prevValidateCommandResult ?? '(no output captured)'}
|
|
1000
|
+
`.trim();
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
const defaultTestImplPrompt = ({ context }) => {
|
|
1004
|
+
const { BACKLOG_ITEM_DIR, TASK_NAME, TASK, REQ_FILE, TEST_PLAN_FILE } = featureContextVars(context);
|
|
1005
|
+
const planLine = TEST_PLAN_FILE
|
|
1006
|
+
? `Follow the test plan in @${TEST_PLAN_FILE}.`
|
|
1007
|
+
: 'Write skipped tests from the requirements and your judgement. Do not create a testPlan.md.';
|
|
1008
|
+
return `
|
|
1009
|
+
Write a test implementation for the following backlog item from @${BACKLOG_ITEM_DIR}/desc.yml.
|
|
1010
|
+
|
|
1011
|
+
The new tests should be skipped in order to not break the whole test suite.
|
|
1012
|
+
|
|
1013
|
+
Task name: ${TASK_NAME}
|
|
1014
|
+
Task:
|
|
1015
|
+
${TASK}
|
|
1016
|
+
|
|
1017
|
+
${planLine}
|
|
1018
|
+
The requirements for this task are in @${REQ_FILE}.
|
|
1019
|
+
`.trim();
|
|
1020
|
+
};
|
|
1021
|
+
const defaultImplPrompt = ({ context }) => {
|
|
1022
|
+
const { REQ_FILE, TEST_PLAN_FILE, WORKFLOW } = featureContextVars(context);
|
|
1023
|
+
const wantsTestImpl = (WORKFLOW ?? '').split(',').includes('testImpl');
|
|
1024
|
+
if (wantsTestImpl) {
|
|
1025
|
+
return `
|
|
1026
|
+
Implement the feature described in @${REQ_FILE}.
|
|
1027
|
+
The tests have already been implemented according to the test plan${TEST_PLAN_FILE ? ` in @${TEST_PLAN_FILE}` : ''}.
|
|
1028
|
+
Unskip all the tests that were skipped in the tests implementation.
|
|
1029
|
+
The implementation should make the tests pass. Do not edit any test file except to unskip them or if absolutely necessary.
|
|
1030
|
+
`.trim();
|
|
1031
|
+
}
|
|
1032
|
+
if (TEST_PLAN_FILE) {
|
|
1033
|
+
return `
|
|
1034
|
+
Implement the feature described in @${REQ_FILE}.
|
|
1035
|
+
Write or update tests to match the test plan in @${TEST_PLAN_FILE}, and make validation pass.
|
|
1036
|
+
Do not edit @${REQ_FILE} unless absolutely necessary.
|
|
1037
|
+
`.trim();
|
|
1038
|
+
}
|
|
1039
|
+
return `
|
|
1040
|
+
Implement the feature described in @${REQ_FILE}.
|
|
1041
|
+
Add or update tests as needed so the suite covers the change, and make validation pass.
|
|
1042
|
+
Do not edit @${REQ_FILE} unless absolutely necessary.
|
|
1043
|
+
`.trim();
|
|
1044
|
+
};
|
|
1045
|
+
const defaultDirectImplPrompt = ({ context }) => {
|
|
1046
|
+
const { BACKLOG_ITEM_DIR, REQ_FILE, TEST_PLAN_FILE } = featureContextVars(context);
|
|
1047
|
+
const extras = [
|
|
1048
|
+
REQ_FILE ? `Use the requirements in @${REQ_FILE}.` : '',
|
|
1049
|
+
TEST_PLAN_FILE ? `Use the test plan in @${TEST_PLAN_FILE} for tests.` : '',
|
|
1050
|
+
]
|
|
1051
|
+
.filter(Boolean)
|
|
1052
|
+
.join('\n');
|
|
1053
|
+
return `
|
|
1054
|
+
Implement the feature described in @${BACKLOG_ITEM_DIR}/desc.yml.
|
|
1055
|
+
${extras}
|
|
1056
|
+
Add tests as needed so the suite covers the change, and make validation pass.
|
|
1057
|
+
`.trim();
|
|
1058
|
+
};
|
|
1059
|
+
|
|
1060
|
+
const FEATURE_BACKLOG_WORKFLOW_STAGES = [
|
|
1061
|
+
'req',
|
|
1062
|
+
'testPlan',
|
|
1063
|
+
'testImpl',
|
|
1064
|
+
'impl',
|
|
851
1065
|
'directImpl',
|
|
852
|
-
'manual',
|
|
853
1066
|
];
|
|
854
|
-
const
|
|
1067
|
+
const DEFAULT_FEATURE_BACKLOG_WORKFLOW = [
|
|
1068
|
+
'req',
|
|
1069
|
+
'testPlan',
|
|
1070
|
+
'testImpl',
|
|
1071
|
+
];
|
|
1072
|
+
const DEFAULT_PRIMARY_DISCOVERY_BRANCH = 'dev';
|
|
1073
|
+
const DEFAULT_ITEM_DISCOVERY_BRANCH_PREFIX = 'feature';
|
|
1074
|
+
const FEATURE_BACKLOG_RESERVED_NAME_SUFFIXES = [
|
|
1075
|
+
'_req',
|
|
1076
|
+
'_testPlan',
|
|
1077
|
+
'_testImpl',
|
|
1078
|
+
];
|
|
1079
|
+
const WORKFLOW_PREFIX_ORDER = ['req', 'testPlan', 'testImpl'];
|
|
1080
|
+
|
|
1081
|
+
function assertDiscoveryToken(value, field) {
|
|
1082
|
+
if (value.length === 0 || /\s/.test(value) || value.includes('*') || value.endsWith('/')) {
|
|
1083
|
+
throw new Error(`featureBacklog ${field} must be a non-empty exact name without whitespace, *, or a trailing /`);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
function resolveFeatureBacklogDiscoveryOptions(options) {
|
|
1087
|
+
const primaryDiscoveryBranch = options.primaryDiscoveryBranch ?? DEFAULT_PRIMARY_DISCOVERY_BRANCH;
|
|
1088
|
+
const itemDiscoveryBranchPrefix = options.itemDiscoveryBranchPrefix ?? DEFAULT_ITEM_DISCOVERY_BRANCH_PREFIX;
|
|
1089
|
+
assertDiscoveryToken(primaryDiscoveryBranch, 'primaryDiscoveryBranch');
|
|
1090
|
+
assertDiscoveryToken(itemDiscoveryBranchPrefix, 'itemDiscoveryBranchPrefix');
|
|
1091
|
+
return { primaryDiscoveryBranch, itemDiscoveryBranchPrefix };
|
|
1092
|
+
}
|
|
1093
|
+
function campaignBranchPrefix(itemDiscoveryBranchPrefix) {
|
|
1094
|
+
return `${itemDiscoveryBranchPrefix}/`;
|
|
1095
|
+
}
|
|
1096
|
+
function workflowHasCampaignStages(workflow) {
|
|
1097
|
+
return workflow.includes('testPlan') || workflow.includes('testImpl');
|
|
1098
|
+
}
|
|
1099
|
+
function classifyDiscoveryScan(discoveryBranch, primaryDiscoveryBranch, itemDiscoveryBranchPrefix) {
|
|
1100
|
+
if (discoveryBranch === primaryDiscoveryBranch) {
|
|
1101
|
+
return 'primary';
|
|
1102
|
+
}
|
|
1103
|
+
if (discoveryBranch.startsWith(campaignBranchPrefix(itemDiscoveryBranchPrefix))) {
|
|
1104
|
+
return 'itemCampaign';
|
|
1105
|
+
}
|
|
1106
|
+
return 'unmatched';
|
|
1107
|
+
}
|
|
1108
|
+
function itemNameFromCampaignBranch(discoveryBranch, itemDiscoveryBranchPrefix) {
|
|
1109
|
+
return discoveryBranch.slice(campaignBranchPrefix(itemDiscoveryBranchPrefix).length);
|
|
1110
|
+
}
|
|
1111
|
+
function itemIsEligibleForDiscoveryScan(input) {
|
|
1112
|
+
const kind = classifyDiscoveryScan(input.discoveryBranch, input.primaryDiscoveryBranch, input.itemDiscoveryBranchPrefix);
|
|
1113
|
+
switch (kind) {
|
|
1114
|
+
case 'primary':
|
|
1115
|
+
return input.parentName === undefined && !workflowHasCampaignStages(input.workflow);
|
|
1116
|
+
case 'itemCampaign': {
|
|
1117
|
+
const campaignItemName = itemNameFromCampaignBranch(input.discoveryBranch, input.itemDiscoveryBranchPrefix);
|
|
1118
|
+
return (input.parentName ?? input.itemName) === campaignItemName;
|
|
1119
|
+
}
|
|
1120
|
+
case 'unmatched':
|
|
1121
|
+
return false;
|
|
1122
|
+
default: {
|
|
1123
|
+
const _exhaustive = kind;
|
|
1124
|
+
return _exhaustive;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
let warnedManualReq = false;
|
|
1130
|
+
function warnManualReqDeprecated() {
|
|
1131
|
+
if (warnedManualReq) {
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
warnedManualReq = true;
|
|
1135
|
+
console.warn('[lumpcode/recipes] featureBacklog: desc.yml field "manualReq" is deprecated and ignored. ' +
|
|
1136
|
+
'Omit "req" from workflow to wait for a human requirements file, or set manual: true to skip the item.');
|
|
1137
|
+
}
|
|
855
1138
|
function assertValidFeatureItemName(name) {
|
|
856
|
-
for (const suffix of
|
|
1139
|
+
for (const suffix of FEATURE_BACKLOG_RESERVED_NAME_SUFFIXES) {
|
|
857
1140
|
if (name.endsWith(suffix)) {
|
|
858
1141
|
throw new Error(`Backlog item name must not end with reserved suffix ${suffix}: ${name}`);
|
|
859
1142
|
}
|
|
860
1143
|
}
|
|
861
1144
|
}
|
|
1145
|
+
function isWorkflowStage(value) {
|
|
1146
|
+
return (typeof value === 'string' &&
|
|
1147
|
+
FEATURE_BACKLOG_WORKFLOW_STAGES.includes(value));
|
|
1148
|
+
}
|
|
1149
|
+
function normalizeWorkflow(stages) {
|
|
1150
|
+
const unique = new Set(stages);
|
|
1151
|
+
const normalized = [];
|
|
1152
|
+
for (const stage of WORKFLOW_PREFIX_ORDER) {
|
|
1153
|
+
if (unique.has(stage)) {
|
|
1154
|
+
normalized.push(stage);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
if (unique.has('directImpl')) {
|
|
1158
|
+
normalized.push('directImpl');
|
|
1159
|
+
}
|
|
1160
|
+
else if (unique.has('impl')) {
|
|
1161
|
+
normalized.push('impl');
|
|
1162
|
+
}
|
|
1163
|
+
return normalized;
|
|
1164
|
+
}
|
|
1165
|
+
function parseFeatureWorkflow(itemName, raw) {
|
|
1166
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
1167
|
+
return undefined;
|
|
1168
|
+
}
|
|
1169
|
+
const record = raw;
|
|
1170
|
+
if ('manualReq' in record) {
|
|
1171
|
+
warnManualReqDeprecated();
|
|
1172
|
+
}
|
|
1173
|
+
if (record.workflow === undefined) {
|
|
1174
|
+
return undefined;
|
|
1175
|
+
}
|
|
1176
|
+
if (!Array.isArray(record.workflow)) {
|
|
1177
|
+
throw new Error(`Backlog item "${itemName}" field "workflow" must be an array of stages: ${FEATURE_BACKLOG_WORKFLOW_STAGES.join(', ')}`);
|
|
1178
|
+
}
|
|
1179
|
+
if (record.workflow.length !== new Set(record.workflow).size) {
|
|
1180
|
+
throw new Error(`Backlog item "${itemName}" field "workflow" must not contain duplicate stages`);
|
|
1181
|
+
}
|
|
1182
|
+
const stages = [];
|
|
1183
|
+
for (const entry of record.workflow) {
|
|
1184
|
+
if (!isWorkflowStage(entry)) {
|
|
1185
|
+
throw new Error(`Backlog item "${itemName}" field "workflow" contains unknown stage ${JSON.stringify(entry)}`);
|
|
1186
|
+
}
|
|
1187
|
+
stages.push(entry);
|
|
1188
|
+
}
|
|
1189
|
+
return normalizeWorkflow(stages);
|
|
1190
|
+
}
|
|
1191
|
+
function parseManual(itemName, raw) {
|
|
1192
|
+
if (raw.manual === undefined) {
|
|
1193
|
+
return undefined;
|
|
1194
|
+
}
|
|
1195
|
+
if (typeof raw.manual !== 'boolean') {
|
|
1196
|
+
throw new Error(`Backlog item "${itemName}" field "manual" must be a boolean`);
|
|
1197
|
+
}
|
|
1198
|
+
return raw.manual === true ? true : undefined;
|
|
1199
|
+
}
|
|
1200
|
+
function resolveTerminal(workflow) {
|
|
1201
|
+
if (workflow.includes('directImpl')) {
|
|
1202
|
+
return 'directImpl';
|
|
1203
|
+
}
|
|
1204
|
+
return 'impl';
|
|
1205
|
+
}
|
|
1206
|
+
function parentNameFromTodoRelativeDir(todoRelativeDir) {
|
|
1207
|
+
const parts = todoRelativeDir.split('/');
|
|
1208
|
+
if (parts.length === 3 && parts[1] === 'tickets') {
|
|
1209
|
+
return parts[0];
|
|
1210
|
+
}
|
|
1211
|
+
return undefined;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
862
1214
|
function featureItemContextBaseName(item) {
|
|
863
1215
|
return item.parentName ? `${item.parentName}-${item.name}` : item.name;
|
|
864
1216
|
}
|
|
865
1217
|
function featureContextName(itemName, stage) {
|
|
866
1218
|
switch (stage) {
|
|
867
|
-
case '
|
|
1219
|
+
case 'req':
|
|
868
1220
|
return `${itemName}_req`;
|
|
869
|
-
case '
|
|
1221
|
+
case 'testPlan':
|
|
870
1222
|
return `${itemName}_testPlan`;
|
|
871
1223
|
case 'testImpl':
|
|
872
|
-
return `${itemName}
|
|
873
|
-
case '
|
|
1224
|
+
return `${itemName}_testImpl`;
|
|
1225
|
+
case 'impl':
|
|
874
1226
|
case 'directImpl':
|
|
1227
|
+
case 'completion':
|
|
875
1228
|
return itemName;
|
|
876
1229
|
default: {
|
|
877
1230
|
const _exhaustive = stage;
|
|
@@ -879,34 +1232,13 @@ function featureContextName(itemName, stage) {
|
|
|
879
1232
|
}
|
|
880
1233
|
}
|
|
881
1234
|
}
|
|
882
|
-
function parentNameFromTodoRelativeDir(todoRelativeDir) {
|
|
883
|
-
const parts = todoRelativeDir.split('/');
|
|
884
|
-
if (parts.length === 3 && parts[1] === 'tickets') {
|
|
885
|
-
return parts[0];
|
|
886
|
-
}
|
|
887
|
-
return undefined;
|
|
888
|
-
}
|
|
889
|
-
function parseFeatureWorkflow(itemName, raw) {
|
|
890
|
-
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
891
|
-
return undefined;
|
|
892
|
-
}
|
|
893
|
-
const record = raw;
|
|
894
|
-
if (record.workflow === undefined) {
|
|
895
|
-
return undefined;
|
|
896
|
-
}
|
|
897
|
-
if (typeof record.workflow !== 'string' ||
|
|
898
|
-
!FEATURE_BACKLOG_WORKFLOWS.includes(record.workflow)) {
|
|
899
|
-
throw new Error(`Backlog item "${itemName}" field "workflow" must be one of: ${FEATURE_BACKLOG_WORKFLOWS.join(', ')}`);
|
|
900
|
-
}
|
|
901
|
-
return record.workflow;
|
|
902
|
-
}
|
|
903
1235
|
async function resolveItemWorkflow(input) {
|
|
904
1236
|
const { item, paths, projectRoot } = input;
|
|
905
1237
|
if (item.workflow !== undefined) {
|
|
906
1238
|
return item.workflow;
|
|
907
1239
|
}
|
|
908
1240
|
if (item.parentName === undefined) {
|
|
909
|
-
return
|
|
1241
|
+
return DEFAULT_FEATURE_BACKLOG_WORKFLOW;
|
|
910
1242
|
}
|
|
911
1243
|
const parentDescPath = path$1.join(projectRoot, paths.backlogItemsDir, 'todo', item.parentName, 'desc.yml');
|
|
912
1244
|
let rawText;
|
|
@@ -916,41 +1248,58 @@ async function resolveItemWorkflow(input) {
|
|
|
916
1248
|
catch (error) {
|
|
917
1249
|
const err = error;
|
|
918
1250
|
if (err.code === 'ENOENT') {
|
|
919
|
-
return
|
|
1251
|
+
return DEFAULT_FEATURE_BACKLOG_WORKFLOW;
|
|
920
1252
|
}
|
|
921
1253
|
throw error;
|
|
922
1254
|
}
|
|
923
|
-
return parseFeatureWorkflow(item.parentName, load(rawText)) ??
|
|
1255
|
+
return parseFeatureWorkflow(item.parentName, load(rawText)) ?? DEFAULT_FEATURE_BACKLOG_WORKFLOW;
|
|
924
1256
|
}
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
const { itemName, parentName, discoveryBranch, workflow } = input;
|
|
932
|
-
if (discoveryBranch === 'dev') {
|
|
933
|
-
return workflow === 'directImpl' && parentName === undefined;
|
|
1257
|
+
function artifactVariables(input) {
|
|
1258
|
+
const variables = {
|
|
1259
|
+
WORKFLOW: input.workflow.join(','),
|
|
1260
|
+
};
|
|
1261
|
+
if (input.hasReq || input.stage === 'req') {
|
|
1262
|
+
variables.REQ_FILE = input.reqFilePath;
|
|
934
1263
|
}
|
|
935
|
-
if (
|
|
936
|
-
|
|
1264
|
+
if (input.hasTestPlan || input.stage === 'testPlan') {
|
|
1265
|
+
variables.TEST_PLAN_FILE = input.testPlanFilePath;
|
|
937
1266
|
}
|
|
938
|
-
|
|
939
|
-
return (parentName ?? itemName) === key;
|
|
1267
|
+
return variables;
|
|
940
1268
|
}
|
|
941
1269
|
async function resolveFeatureBacklogItem(input) {
|
|
942
|
-
const { item, paths, projectRoot, discoveryBranch } = input;
|
|
1270
|
+
const { item, paths, projectRoot, discoveryBranch, primaryDiscoveryBranch, itemDiscoveryBranchPrefix, } = input;
|
|
1271
|
+
if (item.parentName === undefined) {
|
|
1272
|
+
const todoDir = path$1.join(projectRoot, paths.backlogItemsDir, 'todo');
|
|
1273
|
+
const ticketNames = await listUmbrellaTicketNames({
|
|
1274
|
+
todoDir,
|
|
1275
|
+
parentName: item.name,
|
|
1276
|
+
});
|
|
1277
|
+
if (ticketNames.length > 0) {
|
|
1278
|
+
if (item.completedAt) {
|
|
1279
|
+
return { ignored: true };
|
|
1280
|
+
}
|
|
1281
|
+
if (classifyDiscoveryScan(discoveryBranch, primaryDiscoveryBranch, itemDiscoveryBranchPrefix) !== 'itemCampaign' ||
|
|
1282
|
+
itemNameFromCampaignBranch(discoveryBranch, itemDiscoveryBranchPrefix) !== item.name) {
|
|
1283
|
+
return { ignored: true };
|
|
1284
|
+
}
|
|
1285
|
+
return {
|
|
1286
|
+
stage: 'completion',
|
|
1287
|
+
contextName: item.name,
|
|
1288
|
+
additionalDependsOnContexts: ticketNames.map((ticketName) => `${item.name}-${ticketName}`),
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
943
1292
|
const contextBaseName = featureItemContextBaseName(item);
|
|
944
1293
|
const workflow = await resolveItemWorkflow({ item, paths, projectRoot });
|
|
945
|
-
if (
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
if (!!item.completedAt ||
|
|
949
|
-
!itemMatchesDiscoveryBranch({
|
|
1294
|
+
if (item.manual === true ||
|
|
1295
|
+
!!item.completedAt ||
|
|
1296
|
+
!itemIsEligibleForDiscoveryScan({
|
|
950
1297
|
itemName: item.name,
|
|
951
1298
|
parentName: item.parentName,
|
|
952
1299
|
discoveryBranch,
|
|
953
1300
|
workflow,
|
|
1301
|
+
primaryDiscoveryBranch,
|
|
1302
|
+
itemDiscoveryBranchPrefix,
|
|
954
1303
|
})) {
|
|
955
1304
|
return { ignored: true };
|
|
956
1305
|
}
|
|
@@ -958,77 +1307,75 @@ async function resolveFeatureBacklogItem(input) {
|
|
|
958
1307
|
const reqFilePath = path$1.join(itemDir, 'requirements.md');
|
|
959
1308
|
const testPlanFilePath = path$1.join(itemDir, 'testPlan.md');
|
|
960
1309
|
const hasReq = await pathExists(path$1.join(projectRoot, reqFilePath));
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1310
|
+
const hasTestPlan = await pathExists(path$1.join(projectRoot, testPlanFilePath));
|
|
1311
|
+
const wants = (stage) => workflow.includes(stage);
|
|
1312
|
+
const terminal = resolveTerminal(workflow);
|
|
1313
|
+
const resolveStage = (stage) => ({
|
|
1314
|
+
stage,
|
|
1315
|
+
contextName: featureContextName(contextBaseName, stage),
|
|
1316
|
+
variables: artifactVariables({
|
|
1317
|
+
stage,
|
|
1318
|
+
workflow,
|
|
1319
|
+
reqFilePath,
|
|
1320
|
+
testPlanFilePath,
|
|
1321
|
+
hasReq,
|
|
1322
|
+
hasTestPlan,
|
|
1323
|
+
}),
|
|
1324
|
+
});
|
|
1325
|
+
if (wants('req') && !hasReq) {
|
|
1326
|
+
return resolveStage('req');
|
|
970
1327
|
}
|
|
971
|
-
if (
|
|
972
|
-
return {
|
|
973
|
-
stage: 'directImpl',
|
|
974
|
-
contextName: featureContextName(contextBaseName, 'directImpl'),
|
|
975
|
-
variables: { REQ_FILE: reqFilePath },
|
|
976
|
-
};
|
|
1328
|
+
if (!hasReq && (wants('testPlan') || wants('testImpl'))) {
|
|
1329
|
+
return { ignored: true };
|
|
977
1330
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
return {
|
|
981
|
-
stage: 'makeTestPlan',
|
|
982
|
-
contextName: featureContextName(contextBaseName, 'makeTestPlan'),
|
|
983
|
-
variables: {
|
|
984
|
-
REQ_FILE: reqFilePath,
|
|
985
|
-
TEST_PLAN_FILE: testPlanFilePath,
|
|
986
|
-
},
|
|
987
|
-
};
|
|
1331
|
+
if (wants('testPlan') && !hasTestPlan) {
|
|
1332
|
+
return resolveStage('testPlan');
|
|
988
1333
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
},
|
|
1004
|
-
};
|
|
1334
|
+
if (wants('testImpl')) {
|
|
1335
|
+
const testsImplContextName = featureContextName(contextBaseName, 'testImpl');
|
|
1336
|
+
const testsImplStatus = await getContextStatus({
|
|
1337
|
+
projectRoot,
|
|
1338
|
+
contextName: testsImplContextName,
|
|
1339
|
+
lumpName: paths.lumpName,
|
|
1340
|
+
baseBranch: discoveryBranch,
|
|
1341
|
+
});
|
|
1342
|
+
if (testsImplStatus === 'branchPushed') {
|
|
1343
|
+
return { ignored: true };
|
|
1344
|
+
}
|
|
1345
|
+
if (testsImplStatus !== 'finished') {
|
|
1346
|
+
return resolveStage('testImpl');
|
|
1347
|
+
}
|
|
1005
1348
|
}
|
|
1006
|
-
if (
|
|
1349
|
+
if (terminal === 'directImpl') {
|
|
1350
|
+
return resolveStage('directImpl');
|
|
1351
|
+
}
|
|
1352
|
+
if (!hasReq) {
|
|
1007
1353
|
return { ignored: true };
|
|
1008
1354
|
}
|
|
1009
|
-
return
|
|
1010
|
-
stage: 'testImpl',
|
|
1011
|
-
contextName: testsImplContextName,
|
|
1012
|
-
variables: {
|
|
1013
|
-
REQ_FILE: reqFilePath,
|
|
1014
|
-
TEST_PLAN_FILE: testPlanFilePath,
|
|
1015
|
-
},
|
|
1016
|
-
};
|
|
1355
|
+
return resolveStage('impl');
|
|
1017
1356
|
}
|
|
1357
|
+
|
|
1018
1358
|
const featureBacklog = defineRecipe(function featureBacklog(options) {
|
|
1019
|
-
const { configUrl, implValidateCommand, backlogItemsDir, ...rest } = options;
|
|
1359
|
+
const { configUrl, implValidateCommand, backlogItemsDir, primaryDiscoveryBranch, itemDiscoveryBranchPrefix, promptFns, ...rest } = options;
|
|
1360
|
+
const discovery = resolveFeatureBacklogDiscoveryOptions({
|
|
1361
|
+
primaryDiscoveryBranch,
|
|
1362
|
+
itemDiscoveryBranchPrefix,
|
|
1363
|
+
});
|
|
1020
1364
|
const projectRoot = projectRootFromConfigUrl(configUrl);
|
|
1021
1365
|
const runImplValidation = resolveImplValidateCommand(implValidateCommand ??
|
|
1022
1366
|
'echo "No implementation validation command provided. I say, trust but verify, but well..."');
|
|
1023
1367
|
return backlog({
|
|
1024
1368
|
configUrl,
|
|
1025
1369
|
backlogItemsDir,
|
|
1370
|
+
includeUmbrellaParents: true,
|
|
1371
|
+
...rest,
|
|
1372
|
+
discoveryBranches: [
|
|
1373
|
+
discovery.primaryDiscoveryBranch,
|
|
1374
|
+
`${discovery.itemDiscoveryBranchPrefix}/*`,
|
|
1375
|
+
],
|
|
1026
1376
|
parseItem(baseItem, folderName, raw) {
|
|
1027
1377
|
assertValidFeatureItemName(baseItem.name);
|
|
1028
1378
|
const record = raw;
|
|
1029
|
-
if (record.manualReq !== undefined && typeof record.manualReq !== 'boolean') {
|
|
1030
|
-
throw new Error(`Backlog item "${baseItem.name}" field "manualReq" must be a boolean`);
|
|
1031
|
-
}
|
|
1032
1379
|
const parentName = parentNameFromTodoRelativeDir(folderName);
|
|
1033
1380
|
return {
|
|
1034
1381
|
...baseItem,
|
|
@@ -1037,7 +1384,7 @@ const featureBacklog = defineRecipe(function featureBacklog(options) {
|
|
|
1037
1384
|
dependsOn: parentName
|
|
1038
1385
|
? baseItem.dependsOn?.map((dep) => `${parentName}-${dep}`)
|
|
1039
1386
|
: baseItem.dependsOn,
|
|
1040
|
-
|
|
1387
|
+
manual: parseManual(baseItem.name, record),
|
|
1041
1388
|
workflow: parseFeatureWorkflow(baseItem.name, raw),
|
|
1042
1389
|
};
|
|
1043
1390
|
},
|
|
@@ -1047,177 +1394,55 @@ const featureBacklog = defineRecipe(function featureBacklog(options) {
|
|
|
1047
1394
|
paths,
|
|
1048
1395
|
projectRoot,
|
|
1049
1396
|
discoveryBranch,
|
|
1397
|
+
primaryDiscoveryBranch: discovery.primaryDiscoveryBranch,
|
|
1398
|
+
itemDiscoveryBranchPrefix: discovery.itemDiscoveryBranchPrefix,
|
|
1050
1399
|
});
|
|
1051
1400
|
},
|
|
1052
1401
|
stages: {
|
|
1053
|
-
|
|
1402
|
+
req: {
|
|
1054
1403
|
completion: 'keepPending',
|
|
1055
1404
|
steps: retryUntilGreen({
|
|
1056
|
-
steps: [
|
|
1057
|
-
{
|
|
1058
|
-
promptFn({ context: ctx }) {
|
|
1059
|
-
const vars = ctx.variables;
|
|
1060
|
-
const { BACKLOG_ITEM_DIR, TASK_NAME, TASK, REQ_FILE } = vars;
|
|
1061
|
-
return `
|
|
1062
|
-
Write a requirements document for the following backlog item from @${BACKLOG_ITEM_DIR}/desc.yml.
|
|
1063
|
-
|
|
1064
|
-
Task name: ${TASK_NAME}
|
|
1065
|
-
|
|
1066
|
-
Task:
|
|
1067
|
-
${TASK}
|
|
1068
|
-
|
|
1069
|
-
Save the requirements document to @${REQ_FILE}. Do not edit @${BACKLOG_ITEM_DIR}/desc.yml.
|
|
1070
|
-
|
|
1071
|
-
The requirements document should be self-contained and implementation-ready. Include:
|
|
1072
|
-
- Problem statement and motivation
|
|
1073
|
-
- Goals and non-goals
|
|
1074
|
-
- User stories / use cases
|
|
1075
|
-
- Docs updates (if relevant)
|
|
1076
|
-
- Proposed behavior and UX (for CLI work, include command syntax where relevant)
|
|
1077
|
-
- Technical approach and affected packages or docs
|
|
1078
|
-
- Acceptance criteria
|
|
1079
|
-
|
|
1080
|
-
Do not implement the feature — only create the requirements markdown file.
|
|
1081
|
-
Do not wait the user to answer any questions — make the best assumptions and just write the requirements document.
|
|
1082
|
-
The requirements document should not contain any testing strategy details.
|
|
1083
|
-
`.trim();
|
|
1084
|
-
},
|
|
1085
|
-
},
|
|
1086
|
-
],
|
|
1405
|
+
steps: [{ promptFn: promptFns?.req ?? defaultReqPrompt }],
|
|
1087
1406
|
validationCommandFn: requireArtifactStep('REQ_FILE'),
|
|
1088
1407
|
fixSteps: ({ prevValidateCommandResult }) => [
|
|
1089
|
-
{
|
|
1090
|
-
promptFn({ context: ctx }) {
|
|
1091
|
-
const vars = ctx.variables;
|
|
1092
|
-
const { BACKLOG_ITEM_DIR, REQ_FILE } = vars;
|
|
1093
|
-
return `
|
|
1094
|
-
The requirements document was not created at @${REQ_FILE}.
|
|
1095
|
-
|
|
1096
|
-
Create it now at that exact path. Do not edit @${BACKLOG_ITEM_DIR}/desc.yml.
|
|
1097
|
-
Do not implement the feature — only write the requirements markdown file.
|
|
1098
|
-
The requirements document should not contain any testing strategy details.
|
|
1099
|
-
|
|
1100
|
-
Verification output:
|
|
1101
|
-
${prevValidateCommandResult ?? '(no output captured)'}
|
|
1102
|
-
`.trim();
|
|
1103
|
-
},
|
|
1104
|
-
},
|
|
1408
|
+
{ promptFn: defaultReqFixPrompt(prevValidateCommandResult) },
|
|
1105
1409
|
],
|
|
1106
1410
|
}),
|
|
1107
1411
|
},
|
|
1108
|
-
|
|
1412
|
+
testPlan: {
|
|
1109
1413
|
completion: 'keepPending',
|
|
1110
1414
|
steps: retryUntilGreen({
|
|
1111
|
-
steps: [
|
|
1112
|
-
{
|
|
1113
|
-
promptFn({ context: ctx }) {
|
|
1114
|
-
const vars = ctx.variables;
|
|
1115
|
-
const { BACKLOG_ITEM_DIR, TASK_NAME, TASK, REQ_FILE, TEST_PLAN_FILE } = vars;
|
|
1116
|
-
return `
|
|
1117
|
-
Write a test plan for the following backlog item from @${BACKLOG_ITEM_DIR}/desc.yml.
|
|
1118
|
-
|
|
1119
|
-
Task name: ${TASK_NAME}
|
|
1120
|
-
Task:
|
|
1121
|
-
${TASK}
|
|
1122
|
-
|
|
1123
|
-
The requirements for this task are in @${REQ_FILE}. The test plan should match those requirements.
|
|
1124
|
-
|
|
1125
|
-
Save the test plan to @${TEST_PLAN_FILE}. Do not edit @${BACKLOG_ITEM_DIR}/desc.yml nor @${REQ_FILE}.
|
|
1126
|
-
|
|
1127
|
-
The test plan should be self-contained and implementation-ready. Include:
|
|
1128
|
-
- Test cases
|
|
1129
|
-
- Test data
|
|
1130
|
-
- Test expectations
|
|
1131
|
-
- Test implementation details
|
|
1132
|
-
`.trim();
|
|
1133
|
-
},
|
|
1134
|
-
},
|
|
1135
|
-
],
|
|
1415
|
+
steps: [{ promptFn: promptFns?.testPlan ?? defaultTestPlanPrompt }],
|
|
1136
1416
|
validationCommandFn: requireArtifactStep('TEST_PLAN_FILE'),
|
|
1137
1417
|
fixSteps: ({ prevValidateCommandResult }) => [
|
|
1138
|
-
{
|
|
1139
|
-
promptFn({ context: ctx }) {
|
|
1140
|
-
const vars = ctx.variables;
|
|
1141
|
-
const { BACKLOG_ITEM_DIR, REQ_FILE, TEST_PLAN_FILE } = vars;
|
|
1142
|
-
return `
|
|
1143
|
-
The test plan was not created at @${TEST_PLAN_FILE}.
|
|
1144
|
-
|
|
1145
|
-
Create it now at that exact path. Match the requirements in @${REQ_FILE}.
|
|
1146
|
-
Do not edit @${BACKLOG_ITEM_DIR}/desc.yml nor @${REQ_FILE}.
|
|
1147
|
-
|
|
1148
|
-
Verification output:
|
|
1149
|
-
${prevValidateCommandResult ?? '(no output captured)'}
|
|
1150
|
-
`.trim();
|
|
1151
|
-
},
|
|
1152
|
-
},
|
|
1418
|
+
{ promptFn: defaultTestPlanFixPrompt(prevValidateCommandResult) },
|
|
1153
1419
|
],
|
|
1154
1420
|
}),
|
|
1155
1421
|
},
|
|
1156
1422
|
testImpl: {
|
|
1157
1423
|
completion: 'keepPending',
|
|
1158
|
-
steps: [
|
|
1159
|
-
{
|
|
1160
|
-
promptFn({ context: ctx }) {
|
|
1161
|
-
const vars = ctx.variables;
|
|
1162
|
-
const { BACKLOG_ITEM_DIR, TASK_NAME, TASK, REQ_FILE, TEST_PLAN_FILE } = vars;
|
|
1163
|
-
return `
|
|
1164
|
-
Write a test implementation for the following backlog item from @${BACKLOG_ITEM_DIR}/desc.yml.
|
|
1165
|
-
|
|
1166
|
-
The new tests should be skipped in order to not break the whole test suite.
|
|
1167
|
-
|
|
1168
|
-
Task name: ${TASK_NAME}
|
|
1169
|
-
Task:
|
|
1170
|
-
${TASK}
|
|
1171
|
-
|
|
1172
|
-
Follow the test plan in @${TEST_PLAN_FILE}.
|
|
1173
|
-
The requirements for this task are in @${REQ_FILE}.
|
|
1174
|
-
`.trim();
|
|
1175
|
-
},
|
|
1176
|
-
},
|
|
1177
|
-
],
|
|
1424
|
+
steps: [{ promptFn: promptFns?.testImpl ?? defaultTestImplPrompt }],
|
|
1178
1425
|
},
|
|
1179
|
-
|
|
1426
|
+
impl: {
|
|
1180
1427
|
completion: 'moveToDone',
|
|
1181
1428
|
steps: retryUntilGreen({
|
|
1182
|
-
steps: [
|
|
1183
|
-
{
|
|
1184
|
-
promptFn({ context: ctx }) {
|
|
1185
|
-
const vars = ctx.variables;
|
|
1186
|
-
const { REQ_FILE, TEST_PLAN_FILE } = vars;
|
|
1187
|
-
return `
|
|
1188
|
-
Implement the feature described in @${REQ_FILE}.
|
|
1189
|
-
The tests have already been implemented according to the test plan in @${TEST_PLAN_FILE}.
|
|
1190
|
-
Unskip all the tests that were skipped in the tests implementation.
|
|
1191
|
-
The implementation should make the tests pass. Do not edit any test file except to unskip them or if absolutely necessary.
|
|
1192
|
-
`.trim();
|
|
1193
|
-
},
|
|
1194
|
-
},
|
|
1195
|
-
],
|
|
1429
|
+
steps: [{ promptFn: promptFns?.impl ?? defaultImplPrompt }],
|
|
1196
1430
|
validationCommandFn: runImplValidation,
|
|
1197
1431
|
}),
|
|
1198
1432
|
},
|
|
1199
1433
|
directImpl: {
|
|
1200
1434
|
completion: 'moveToDone',
|
|
1201
1435
|
steps: retryUntilGreen({
|
|
1202
|
-
steps: [
|
|
1203
|
-
{
|
|
1204
|
-
promptFn({ context: ctx }) {
|
|
1205
|
-
const vars = ctx.variables;
|
|
1206
|
-
const { REQ_FILE } = vars;
|
|
1207
|
-
return `
|
|
1208
|
-
Implement the feature described in @${REQ_FILE}.
|
|
1209
|
-
Add or update tests as needed so the suite covers the change, and make validation pass.
|
|
1210
|
-
Do not edit @${REQ_FILE} unless absolutely necessary.
|
|
1211
|
-
`.trim();
|
|
1212
|
-
},
|
|
1213
|
-
},
|
|
1214
|
-
],
|
|
1436
|
+
steps: [{ promptFn: promptFns?.directImpl ?? defaultDirectImplPrompt }],
|
|
1215
1437
|
validationCommandFn: runImplValidation,
|
|
1216
1438
|
}),
|
|
1217
1439
|
},
|
|
1440
|
+
completion: {
|
|
1441
|
+
completion: 'moveToDone',
|
|
1442
|
+
steps: [],
|
|
1443
|
+
},
|
|
1218
1444
|
},
|
|
1219
|
-
...rest,
|
|
1220
1445
|
});
|
|
1221
1446
|
});
|
|
1222
1447
|
|
|
1223
|
-
export { BACKLOG_ITEMS_DIR_VAR, BACKLOG_ITEM_DIR_VAR, BACKLOG_STAGE_VAR, BACKLOG_TASK_NAME_VAR, BACKLOG_TASK_VAR,
|
|
1448
|
+
export { BACKLOG_ITEMS_DIR_VAR, BACKLOG_ITEM_DIR_VAR, BACKLOG_STAGE_VAR, BACKLOG_TASK_NAME_VAR, BACKLOG_TASK_VAR, DEFAULT_FEATURE_BACKLOG_WORKFLOW, DEFAULT_ITEM_DISCOVERY_BRANCH_PREFIX, DEFAULT_PRIMARY_DISCOVERY_BRANCH, FEATURE_BACKLOG_RESERVED_NAME_SUFFIXES, FEATURE_BACKLOG_WORKFLOW_STAGES, OPEN_PR_PROVIDERS, abstractionBacklog, abstractionFinder, backlog, backlogRecipe, defaultDirectImplPrompt, defaultImplPrompt, defaultReqFixPrompt, defaultReqPrompt, defaultTestImplPrompt, defaultTestPlanFixPrompt, defaultTestPlanPrompt, defineRecipe, ephemeralContextListFn, featureBacklog, folderBacklogContexts, folderSetTaskDoneStep, getRecursiveSteps, listTodoRelativeDirs, listUmbrellaTicketNames, lumpPathAndName, normalizeMaybePromGetter, openPrPostTeardown, parseFeatureWorkflow, projectRootFromConfigUrl, requireArtifactStep, resolveBacklogPaths, resolveFeatureBacklogDiscoveryOptions, resolveFeatureBacklogItem, resolveImplValidateCommand, retryUntilGreen, setTaskDoneStep, shellCommand, validateBaseBacklogItem, ymlBacklogContexts };
|