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