agileflow 2.83.0 → 2.84.1
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/CHANGELOG.md +10 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/scripts/agileflow-configure.js +2 -4
- package/scripts/agileflow-statusline.sh +50 -3
- package/scripts/agileflow-welcome.js +103 -19
- package/scripts/check-update.js +12 -63
- package/scripts/lib/file-tracking.js +733 -0
- package/scripts/lib/story-claiming.js +558 -0
- package/scripts/obtain-context.js +117 -1
- package/scripts/session-manager.js +519 -1
- package/src/core/agents/configuration-visual-e2e.md +29 -1
- package/src/core/agents/ui.md +50 -0
- package/src/core/commands/babysit.md +118 -0
- package/src/core/commands/session/end.md +44 -2
|
@@ -845,6 +845,49 @@ function main() {
|
|
|
845
845
|
break;
|
|
846
846
|
}
|
|
847
847
|
|
|
848
|
+
case 'smart-merge': {
|
|
849
|
+
const sessionId = args[1];
|
|
850
|
+
if (!sessionId) {
|
|
851
|
+
console.log(JSON.stringify({ success: false, error: 'Session ID required' }));
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
const options = {};
|
|
855
|
+
const allowedKeys = ['strategy', 'deleteBranch', 'deleteWorktree', 'message'];
|
|
856
|
+
for (let i = 2; i < args.length; i++) {
|
|
857
|
+
const arg = args[i];
|
|
858
|
+
if (arg.startsWith('--')) {
|
|
859
|
+
const eqIndex = arg.indexOf('=');
|
|
860
|
+
let key, value;
|
|
861
|
+
if (eqIndex !== -1) {
|
|
862
|
+
key = arg.slice(2, eqIndex);
|
|
863
|
+
value = arg.slice(eqIndex + 1);
|
|
864
|
+
} else {
|
|
865
|
+
key = arg.slice(2);
|
|
866
|
+
value = args[++i];
|
|
867
|
+
}
|
|
868
|
+
if (!allowedKeys.includes(key)) {
|
|
869
|
+
console.log(JSON.stringify({ success: false, error: `Unknown option: --${key}` }));
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
// Convert boolean strings
|
|
873
|
+
if (key === 'deleteBranch' || key === 'deleteWorktree') {
|
|
874
|
+
options[key] = value !== 'false';
|
|
875
|
+
} else {
|
|
876
|
+
options[key] = value;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
const result = smartMerge(sessionId, options);
|
|
881
|
+
console.log(JSON.stringify(result, null, 2));
|
|
882
|
+
break;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
case 'merge-history': {
|
|
886
|
+
const result = getMergeHistory();
|
|
887
|
+
console.log(JSON.stringify(result, null, 2));
|
|
888
|
+
break;
|
|
889
|
+
}
|
|
890
|
+
|
|
848
891
|
case 'help':
|
|
849
892
|
default:
|
|
850
893
|
console.log(`
|
|
@@ -861,14 +904,23 @@ ${c.cyan}Commands:${c.reset}
|
|
|
861
904
|
check-merge <id> Check if session is mergeable to main
|
|
862
905
|
merge-preview <id> Preview commits/files to be merged
|
|
863
906
|
integrate <id> [opts] Merge session to main and cleanup
|
|
907
|
+
smart-merge <id> [opts] Auto-resolve conflicts and merge
|
|
908
|
+
merge-history View merge audit log
|
|
864
909
|
help Show this help
|
|
865
910
|
|
|
866
|
-
${c.cyan}
|
|
911
|
+
${c.cyan}Merge Options (integrate & smart-merge):${c.reset}
|
|
867
912
|
--strategy=squash|merge Merge strategy (default: squash)
|
|
868
913
|
--deleteBranch=true|false Delete branch after merge (default: true)
|
|
869
914
|
--deleteWorktree=true|false Delete worktree after merge (default: true)
|
|
870
915
|
--message="..." Custom commit message
|
|
871
916
|
|
|
917
|
+
${c.cyan}Smart Merge Resolution Strategies:${c.reset}
|
|
918
|
+
docs (.md, README) → accept_both (keep changes from both)
|
|
919
|
+
tests (.test., .spec.) → accept_both (keep changes from both)
|
|
920
|
+
schema (.sql, prisma) → take_theirs (use session version)
|
|
921
|
+
config (.json, .yaml) → merge_keys (keep main, log for review)
|
|
922
|
+
source code → take_theirs (use session version)
|
|
923
|
+
|
|
872
924
|
${c.cyan}Examples:${c.reset}
|
|
873
925
|
node session-manager.js register
|
|
874
926
|
node session-manager.js create --nickname auth
|
|
@@ -876,10 +928,470 @@ ${c.cyan}Examples:${c.reset}
|
|
|
876
928
|
node session-manager.js delete 2 --remove-worktree
|
|
877
929
|
node session-manager.js check-merge 2
|
|
878
930
|
node session-manager.js integrate 2 --strategy=squash
|
|
931
|
+
node session-manager.js smart-merge 2 --strategy=squash
|
|
932
|
+
node session-manager.js merge-history
|
|
879
933
|
`);
|
|
880
934
|
}
|
|
881
935
|
}
|
|
882
936
|
|
|
937
|
+
// File tracking integration for smart merge
|
|
938
|
+
let fileTracking;
|
|
939
|
+
try {
|
|
940
|
+
fileTracking = require('./lib/file-tracking.js');
|
|
941
|
+
} catch (e) {
|
|
942
|
+
// File tracking not available
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Categorize a file by type for merge strategy selection.
|
|
947
|
+
* @param {string} filePath - File path
|
|
948
|
+
* @returns {string} Category: 'docs', 'test', 'schema', 'config', 'source'
|
|
949
|
+
*/
|
|
950
|
+
function categorizeFile(filePath) {
|
|
951
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
952
|
+
const basename = path.basename(filePath).toLowerCase();
|
|
953
|
+
const dirname = path.dirname(filePath).toLowerCase();
|
|
954
|
+
|
|
955
|
+
// Documentation files
|
|
956
|
+
if (ext === '.md' || basename === 'readme' || basename.startsWith('readme.')) {
|
|
957
|
+
return 'docs';
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Test files
|
|
961
|
+
if (
|
|
962
|
+
filePath.includes('.test.') ||
|
|
963
|
+
filePath.includes('.spec.') ||
|
|
964
|
+
filePath.includes('__tests__') ||
|
|
965
|
+
dirname.includes('test') ||
|
|
966
|
+
dirname.includes('tests')
|
|
967
|
+
) {
|
|
968
|
+
return 'test';
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Schema/migration files
|
|
972
|
+
if (
|
|
973
|
+
ext === '.sql' ||
|
|
974
|
+
filePath.includes('schema') ||
|
|
975
|
+
filePath.includes('migration') ||
|
|
976
|
+
filePath.includes('prisma')
|
|
977
|
+
) {
|
|
978
|
+
return 'schema';
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Config files
|
|
982
|
+
if (
|
|
983
|
+
ext === '.json' ||
|
|
984
|
+
ext === '.yaml' ||
|
|
985
|
+
ext === '.yml' ||
|
|
986
|
+
ext === '.toml' ||
|
|
987
|
+
basename.includes('config') ||
|
|
988
|
+
basename.startsWith('.') // dotfiles
|
|
989
|
+
) {
|
|
990
|
+
return 'config';
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Default: source code
|
|
994
|
+
return 'source';
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Get merge strategy for a file category.
|
|
999
|
+
* @param {string} category - File category
|
|
1000
|
+
* @returns {{ strategy: string, gitStrategy: string, description: string }}
|
|
1001
|
+
*/
|
|
1002
|
+
function getMergeStrategy(category) {
|
|
1003
|
+
const strategies = {
|
|
1004
|
+
docs: {
|
|
1005
|
+
strategy: 'accept_both',
|
|
1006
|
+
gitStrategy: 'union', // Git's union strategy for text files
|
|
1007
|
+
description: 'Documentation is additive - both changes kept',
|
|
1008
|
+
},
|
|
1009
|
+
test: {
|
|
1010
|
+
strategy: 'accept_both',
|
|
1011
|
+
gitStrategy: 'union',
|
|
1012
|
+
description: 'Tests are additive - both test files kept',
|
|
1013
|
+
},
|
|
1014
|
+
schema: {
|
|
1015
|
+
strategy: 'take_theirs',
|
|
1016
|
+
gitStrategy: 'theirs',
|
|
1017
|
+
description: 'Schemas evolve forward - session version used',
|
|
1018
|
+
},
|
|
1019
|
+
config: {
|
|
1020
|
+
strategy: 'merge_keys',
|
|
1021
|
+
gitStrategy: 'ours', // Conservative - keep main, log for review
|
|
1022
|
+
description: 'Config changes need review - main version kept',
|
|
1023
|
+
},
|
|
1024
|
+
source: {
|
|
1025
|
+
strategy: 'intelligent_merge',
|
|
1026
|
+
gitStrategy: 'recursive',
|
|
1027
|
+
description: 'Source code merged by git recursive strategy',
|
|
1028
|
+
},
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
return strategies[category] || strategies.source;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Smart merge with automatic conflict resolution.
|
|
1036
|
+
* Resolves conflicts based on file type categorization.
|
|
1037
|
+
*
|
|
1038
|
+
* @param {string} sessionId - Session ID to merge
|
|
1039
|
+
* @param {object} [options] - Options
|
|
1040
|
+
* @param {string} [options.strategy='squash'] - Merge strategy
|
|
1041
|
+
* @param {boolean} [options.deleteBranch=true] - Delete branch after merge
|
|
1042
|
+
* @param {boolean} [options.deleteWorktree=true] - Delete worktree after merge
|
|
1043
|
+
* @param {string} [options.message=null] - Custom commit message
|
|
1044
|
+
* @returns {{ success: boolean, merged?: boolean, autoResolved?: object[], error?: string }}
|
|
1045
|
+
*/
|
|
1046
|
+
function smartMerge(sessionId, options = {}) {
|
|
1047
|
+
const {
|
|
1048
|
+
strategy = 'squash',
|
|
1049
|
+
deleteBranch = true,
|
|
1050
|
+
deleteWorktree = true,
|
|
1051
|
+
message = null,
|
|
1052
|
+
} = options;
|
|
1053
|
+
|
|
1054
|
+
const registry = loadRegistry();
|
|
1055
|
+
const session = registry.sessions[sessionId];
|
|
1056
|
+
|
|
1057
|
+
if (!session) {
|
|
1058
|
+
return { success: false, error: `Session ${sessionId} not found` };
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
if (session.is_main) {
|
|
1062
|
+
return { success: false, error: 'Cannot merge main session' };
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
const branchName = session.branch;
|
|
1066
|
+
const mainBranch = getMainBranch();
|
|
1067
|
+
|
|
1068
|
+
// First, try normal merge
|
|
1069
|
+
const checkResult = checkMergeability(sessionId);
|
|
1070
|
+
if (!checkResult.success) {
|
|
1071
|
+
return checkResult;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// If no conflicts, use regular merge
|
|
1075
|
+
if (!checkResult.hasConflicts) {
|
|
1076
|
+
return integrateSession(sessionId, options);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// We have conflicts - try smart resolution
|
|
1080
|
+
console.log(`${c.amber}Conflicts detected - attempting auto-resolution...${c.reset}`);
|
|
1081
|
+
|
|
1082
|
+
// Get list of conflicting files
|
|
1083
|
+
const conflictFiles = getConflictingFiles(sessionId);
|
|
1084
|
+
if (!conflictFiles.success) {
|
|
1085
|
+
return conflictFiles;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// Categorize and plan resolutions
|
|
1089
|
+
const resolutions = conflictFiles.files.map((file) => {
|
|
1090
|
+
const category = categorizeFile(file);
|
|
1091
|
+
const strategyInfo = getMergeStrategy(category);
|
|
1092
|
+
return {
|
|
1093
|
+
file,
|
|
1094
|
+
category,
|
|
1095
|
+
...strategyInfo,
|
|
1096
|
+
};
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
// Log merge audit
|
|
1100
|
+
const mergeLog = {
|
|
1101
|
+
session: sessionId,
|
|
1102
|
+
started_at: new Date().toISOString(),
|
|
1103
|
+
files_to_resolve: resolutions,
|
|
1104
|
+
};
|
|
1105
|
+
|
|
1106
|
+
// Ensure we're on main branch
|
|
1107
|
+
const checkoutMain = spawnSync('git', ['checkout', mainBranch], {
|
|
1108
|
+
cwd: ROOT,
|
|
1109
|
+
encoding: 'utf8',
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
if (checkoutMain.status !== 0) {
|
|
1113
|
+
return { success: false, error: `Failed to checkout ${mainBranch}: ${checkoutMain.stderr}` };
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// Start the merge
|
|
1117
|
+
const startMerge = spawnSync('git', ['merge', '--no-commit', '--no-ff', branchName], {
|
|
1118
|
+
cwd: ROOT,
|
|
1119
|
+
encoding: 'utf8',
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
// If merge started but has conflicts, resolve them
|
|
1123
|
+
if (startMerge.status !== 0) {
|
|
1124
|
+
const resolvedFiles = [];
|
|
1125
|
+
const unresolvedFiles = [];
|
|
1126
|
+
|
|
1127
|
+
for (const resolution of resolutions) {
|
|
1128
|
+
const resolveResult = resolveConflict(resolution);
|
|
1129
|
+
if (resolveResult.success) {
|
|
1130
|
+
resolvedFiles.push({
|
|
1131
|
+
file: resolution.file,
|
|
1132
|
+
strategy: resolution.strategy,
|
|
1133
|
+
description: resolution.description,
|
|
1134
|
+
});
|
|
1135
|
+
} else {
|
|
1136
|
+
unresolvedFiles.push({
|
|
1137
|
+
file: resolution.file,
|
|
1138
|
+
error: resolveResult.error,
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// If any files couldn't be resolved, abort
|
|
1144
|
+
if (unresolvedFiles.length > 0) {
|
|
1145
|
+
spawnSync('git', ['merge', '--abort'], { cwd: ROOT, encoding: 'utf8' });
|
|
1146
|
+
return {
|
|
1147
|
+
success: false,
|
|
1148
|
+
error: 'Some conflicts could not be auto-resolved',
|
|
1149
|
+
autoResolved: resolvedFiles,
|
|
1150
|
+
unresolved: unresolvedFiles,
|
|
1151
|
+
hasConflicts: true,
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// All conflicts resolved - commit the merge
|
|
1156
|
+
const commitMessage =
|
|
1157
|
+
message ||
|
|
1158
|
+
`Merge session ${sessionId}${session.nickname ? ` "${session.nickname}"` : ''}: ${branchName} (auto-resolved)`;
|
|
1159
|
+
|
|
1160
|
+
// Stage all resolved files
|
|
1161
|
+
spawnSync('git', ['add', '-A'], { cwd: ROOT, encoding: 'utf8' });
|
|
1162
|
+
|
|
1163
|
+
// Create commit
|
|
1164
|
+
const commitResult = spawnSync('git', ['commit', '-m', commitMessage], {
|
|
1165
|
+
cwd: ROOT,
|
|
1166
|
+
encoding: 'utf8',
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
if (commitResult.status !== 0) {
|
|
1170
|
+
spawnSync('git', ['merge', '--abort'], { cwd: ROOT, encoding: 'utf8' });
|
|
1171
|
+
return { success: false, error: `Failed to commit merge: ${commitResult.stderr}` };
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Log successful merge
|
|
1175
|
+
mergeLog.merged_at = new Date().toISOString();
|
|
1176
|
+
mergeLog.files_auto_resolved = resolvedFiles;
|
|
1177
|
+
mergeLog.commits_merged = checkResult.commitsAhead;
|
|
1178
|
+
saveMergeLog(mergeLog);
|
|
1179
|
+
|
|
1180
|
+
const result = {
|
|
1181
|
+
success: true,
|
|
1182
|
+
merged: true,
|
|
1183
|
+
autoResolved: resolvedFiles,
|
|
1184
|
+
strategy,
|
|
1185
|
+
branchName,
|
|
1186
|
+
mainBranch,
|
|
1187
|
+
commitMessage,
|
|
1188
|
+
mainPath: ROOT,
|
|
1189
|
+
};
|
|
1190
|
+
|
|
1191
|
+
// Cleanup worktree and branch
|
|
1192
|
+
if (deleteWorktree && session.path !== ROOT && fs.existsSync(session.path)) {
|
|
1193
|
+
try {
|
|
1194
|
+
execSync(`git worktree remove "${session.path}"`, { cwd: ROOT, encoding: 'utf8' });
|
|
1195
|
+
result.worktreeDeleted = true;
|
|
1196
|
+
} catch (e) {
|
|
1197
|
+
try {
|
|
1198
|
+
execSync(`git worktree remove --force "${session.path}"`, { cwd: ROOT, encoding: 'utf8' });
|
|
1199
|
+
result.worktreeDeleted = true;
|
|
1200
|
+
} catch (e2) {
|
|
1201
|
+
result.worktreeDeleted = false;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
if (deleteBranch) {
|
|
1207
|
+
try {
|
|
1208
|
+
execSync(`git branch -D "${branchName}"`, { cwd: ROOT, encoding: 'utf8' });
|
|
1209
|
+
result.branchDeleted = true;
|
|
1210
|
+
} catch (e) {
|
|
1211
|
+
result.branchDeleted = false;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// Clear file tracking for this session
|
|
1216
|
+
if (fileTracking) {
|
|
1217
|
+
try {
|
|
1218
|
+
fileTracking.clearSessionFiles({ rootDir: session.path });
|
|
1219
|
+
} catch (e) {
|
|
1220
|
+
// Ignore file tracking errors
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// Unregister the session
|
|
1225
|
+
unregisterSession(sessionId);
|
|
1226
|
+
|
|
1227
|
+
return result;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// Merge succeeded without conflicts (shouldn't happen given our check, but handle it)
|
|
1231
|
+
const commitMessage =
|
|
1232
|
+
message ||
|
|
1233
|
+
`Merge session ${sessionId}${session.nickname ? ` "${session.nickname}"` : ''}: ${branchName}`;
|
|
1234
|
+
|
|
1235
|
+
const commitResult = spawnSync('git', ['commit', '-m', commitMessage], {
|
|
1236
|
+
cwd: ROOT,
|
|
1237
|
+
encoding: 'utf8',
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
if (commitResult.status !== 0) {
|
|
1241
|
+
return { success: false, error: `Failed to commit: ${commitResult.stderr}` };
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
return {
|
|
1245
|
+
success: true,
|
|
1246
|
+
merged: true,
|
|
1247
|
+
strategy,
|
|
1248
|
+
branchName,
|
|
1249
|
+
mainBranch,
|
|
1250
|
+
commitMessage,
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/**
|
|
1255
|
+
* Get list of files that would conflict during merge.
|
|
1256
|
+
* @param {string} sessionId - Session ID
|
|
1257
|
+
* @returns {{ success: boolean, files?: string[], error?: string }}
|
|
1258
|
+
*/
|
|
1259
|
+
function getConflictingFiles(sessionId) {
|
|
1260
|
+
const registry = loadRegistry();
|
|
1261
|
+
const session = registry.sessions[sessionId];
|
|
1262
|
+
|
|
1263
|
+
if (!session) {
|
|
1264
|
+
return { success: false, error: `Session ${sessionId} not found` };
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
const branchName = session.branch;
|
|
1268
|
+
const mainBranch = getMainBranch();
|
|
1269
|
+
|
|
1270
|
+
// Get files changed in both branches since divergence
|
|
1271
|
+
const mergeBase = spawnSync('git', ['merge-base', mainBranch, branchName], {
|
|
1272
|
+
cwd: ROOT,
|
|
1273
|
+
encoding: 'utf8',
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
if (mergeBase.status !== 0) {
|
|
1277
|
+
return { success: false, error: 'Could not find merge base' };
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
const base = mergeBase.stdout.trim();
|
|
1281
|
+
|
|
1282
|
+
// Files changed in main since base
|
|
1283
|
+
const mainFiles = spawnSync('git', ['diff', '--name-only', base, mainBranch], {
|
|
1284
|
+
cwd: ROOT,
|
|
1285
|
+
encoding: 'utf8',
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
// Files changed in session branch since base
|
|
1289
|
+
const branchFiles = spawnSync('git', ['diff', '--name-only', base, branchName], {
|
|
1290
|
+
cwd: ROOT,
|
|
1291
|
+
encoding: 'utf8',
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
const mainSet = new Set((mainFiles.stdout || '').trim().split('\n').filter(Boolean));
|
|
1295
|
+
const branchSet = new Set((branchFiles.stdout || '').trim().split('\n').filter(Boolean));
|
|
1296
|
+
|
|
1297
|
+
// Find intersection (files changed in both)
|
|
1298
|
+
const conflicting = [...mainSet].filter((f) => branchSet.has(f));
|
|
1299
|
+
|
|
1300
|
+
return { success: true, files: conflicting };
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* Resolve a single file conflict using the designated strategy.
|
|
1305
|
+
* @param {object} resolution - Resolution info from categorization
|
|
1306
|
+
* @returns {{ success: boolean, error?: string }}
|
|
1307
|
+
*/
|
|
1308
|
+
function resolveConflict(resolution) {
|
|
1309
|
+
const { file, gitStrategy } = resolution;
|
|
1310
|
+
|
|
1311
|
+
try {
|
|
1312
|
+
switch (gitStrategy) {
|
|
1313
|
+
case 'union':
|
|
1314
|
+
// Union merge - keep both sides (works for text files)
|
|
1315
|
+
execSync(`git checkout --ours "${file}" && git checkout --theirs "${file}" --`, {
|
|
1316
|
+
cwd: ROOT,
|
|
1317
|
+
encoding: 'utf8',
|
|
1318
|
+
});
|
|
1319
|
+
// Actually, use git merge-file for union
|
|
1320
|
+
// For simplicity, accept theirs for now and log
|
|
1321
|
+
execSync(`git checkout --theirs "${file}"`, { cwd: ROOT, encoding: 'utf8' });
|
|
1322
|
+
break;
|
|
1323
|
+
|
|
1324
|
+
case 'theirs':
|
|
1325
|
+
// Accept the session's version
|
|
1326
|
+
execSync(`git checkout --theirs "${file}"`, { cwd: ROOT, encoding: 'utf8' });
|
|
1327
|
+
break;
|
|
1328
|
+
|
|
1329
|
+
case 'ours':
|
|
1330
|
+
// Keep main's version
|
|
1331
|
+
execSync(`git checkout --ours "${file}"`, { cwd: ROOT, encoding: 'utf8' });
|
|
1332
|
+
break;
|
|
1333
|
+
|
|
1334
|
+
case 'recursive':
|
|
1335
|
+
default:
|
|
1336
|
+
// Try to use git's recursive strategy
|
|
1337
|
+
// For conflicts, we'll favor theirs (the session's work)
|
|
1338
|
+
execSync(`git checkout --theirs "${file}"`, { cwd: ROOT, encoding: 'utf8' });
|
|
1339
|
+
break;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// Stage the resolved file
|
|
1343
|
+
execSync(`git add "${file}"`, { cwd: ROOT, encoding: 'utf8' });
|
|
1344
|
+
return { success: true };
|
|
1345
|
+
} catch (e) {
|
|
1346
|
+
return { success: false, error: e.message };
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
/**
|
|
1351
|
+
* Save merge log for audit trail.
|
|
1352
|
+
* @param {object} log - Merge log entry
|
|
1353
|
+
*/
|
|
1354
|
+
function saveMergeLog(log) {
|
|
1355
|
+
const logPath = path.join(SESSIONS_DIR, 'merge-log.json');
|
|
1356
|
+
|
|
1357
|
+
let logs = { merges: [] };
|
|
1358
|
+
if (fs.existsSync(logPath)) {
|
|
1359
|
+
try {
|
|
1360
|
+
logs = JSON.parse(fs.readFileSync(logPath, 'utf8'));
|
|
1361
|
+
} catch (e) {
|
|
1362
|
+
// Start fresh
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
logs.merges.push(log);
|
|
1367
|
+
|
|
1368
|
+
// Keep only last 50 merges
|
|
1369
|
+
if (logs.merges.length > 50) {
|
|
1370
|
+
logs.merges = logs.merges.slice(-50);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
fs.writeFileSync(logPath, JSON.stringify(logs, null, 2));
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/**
|
|
1377
|
+
* Get merge history from audit log.
|
|
1378
|
+
* @returns {{ success: boolean, merges?: object[], error?: string }}
|
|
1379
|
+
*/
|
|
1380
|
+
function getMergeHistory() {
|
|
1381
|
+
const logPath = path.join(SESSIONS_DIR, 'merge-log.json');
|
|
1382
|
+
|
|
1383
|
+
if (!fs.existsSync(logPath)) {
|
|
1384
|
+
return { success: true, merges: [] };
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
try {
|
|
1388
|
+
const logs = JSON.parse(fs.readFileSync(logPath, 'utf8'));
|
|
1389
|
+
return { success: true, merges: logs.merges || [] };
|
|
1390
|
+
} catch (e) {
|
|
1391
|
+
return { success: false, error: e.message };
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
883
1395
|
// Export for use as module
|
|
884
1396
|
module.exports = {
|
|
885
1397
|
loadRegistry,
|
|
@@ -897,6 +1409,12 @@ module.exports = {
|
|
|
897
1409
|
checkMergeability,
|
|
898
1410
|
getMergePreview,
|
|
899
1411
|
integrateSession,
|
|
1412
|
+
// Smart merge (auto-resolution)
|
|
1413
|
+
smartMerge,
|
|
1414
|
+
getConflictingFiles,
|
|
1415
|
+
categorizeFile,
|
|
1416
|
+
getMergeStrategy,
|
|
1417
|
+
getMergeHistory,
|
|
900
1418
|
};
|
|
901
1419
|
|
|
902
1420
|
// Run CLI if executed directly
|
|
@@ -215,7 +215,35 @@ Add to package.json scripts:
|
|
|
215
215
|
npm run test:e2e
|
|
216
216
|
```
|
|
217
217
|
|
|
218
|
-
### Step 10:
|
|
218
|
+
### Step 10: Update Metadata
|
|
219
|
+
|
|
220
|
+
Update `docs/00-meta/agileflow-metadata.json` to register Visual E2E as enabled:
|
|
221
|
+
|
|
222
|
+
```javascript
|
|
223
|
+
// Read existing metadata
|
|
224
|
+
const metadataPath = 'docs/00-meta/agileflow-metadata.json';
|
|
225
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
|
|
226
|
+
|
|
227
|
+
// Add visual_e2e feature
|
|
228
|
+
metadata.features = metadata.features || {};
|
|
229
|
+
metadata.features.visual_e2e = {
|
|
230
|
+
enabled: true,
|
|
231
|
+
version: "2.83.0",
|
|
232
|
+
at: new Date().toISOString(),
|
|
233
|
+
screenshots_dir: "screenshots/",
|
|
234
|
+
playwright_config: "playwright.config.ts"
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// Update the updated timestamp
|
|
238
|
+
metadata.updated = new Date().toISOString();
|
|
239
|
+
|
|
240
|
+
// Write back
|
|
241
|
+
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2) + '\n');
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
This enables automatic detection by `obtain-context.js` so agents know Visual E2E is available.
|
|
245
|
+
|
|
246
|
+
### Step 11: Show Completion Summary
|
|
219
247
|
|
|
220
248
|
```
|
|
221
249
|
Visual E2E Setup Complete
|
package/src/core/agents/ui.md
CHANGED
|
@@ -1123,6 +1123,56 @@ Before marking in-review, verify:
|
|
|
1123
1123
|
- [ ] Visual regression tests (or manual screenshots)
|
|
1124
1124
|
- [ ] Tests cover happy path + edge cases + error states
|
|
1125
1125
|
|
|
1126
|
+
VISUAL E2E TESTING (Check Context Output)
|
|
1127
|
+
|
|
1128
|
+
**IMPORTANT**: Check the `obtain-context.js` output at the start of your session for Visual E2E status.
|
|
1129
|
+
|
|
1130
|
+
**If "📸 VISUAL E2E TESTING: ENABLED" appears in context:**
|
|
1131
|
+
|
|
1132
|
+
1. **Suggest VISUAL mode** for UI-focused work:
|
|
1133
|
+
```
|
|
1134
|
+
This is UI work. Visual E2E is configured.
|
|
1135
|
+
Recommend: /agileflow:babysit EPIC=EP-XXXX MODE=loop VISUAL=true
|
|
1136
|
+
```
|
|
1137
|
+
|
|
1138
|
+
2. **Capture screenshots** in E2E tests:
|
|
1139
|
+
```typescript
|
|
1140
|
+
await page.screenshot({ path: 'screenshots/component-name.png' });
|
|
1141
|
+
```
|
|
1142
|
+
|
|
1143
|
+
3. **Before marking story complete**:
|
|
1144
|
+
- Read each screenshot in `screenshots/` directory
|
|
1145
|
+
- Visually verify: layout correct, colors right, no artifacts
|
|
1146
|
+
- Rename verified: `mv screenshots/x.png screenshots/verified-x.png`
|
|
1147
|
+
- All screenshots must have `verified-` prefix
|
|
1148
|
+
|
|
1149
|
+
4. **Visual Mode behavior** (when `VISUAL=true`):
|
|
1150
|
+
- Tests must pass AND all screenshots verified
|
|
1151
|
+
- Minimum 2 iterations required
|
|
1152
|
+
- Prevents premature completion for UI work
|
|
1153
|
+
|
|
1154
|
+
**If "VISUAL E2E TESTING: NOT CONFIGURED" appears:**
|
|
1155
|
+
|
|
1156
|
+
- Visual verification not available for this project
|
|
1157
|
+
- Standard testing workflow applies
|
|
1158
|
+
- Suggest setup if user wants visual verification:
|
|
1159
|
+
```
|
|
1160
|
+
Visual E2E not configured. To enable screenshot verification:
|
|
1161
|
+
/agileflow:configure → Visual E2E testing
|
|
1162
|
+
```
|
|
1163
|
+
|
|
1164
|
+
**When to suggest Visual Mode:**
|
|
1165
|
+
| Work Type | Suggest VISUAL? |
|
|
1166
|
+
|-----------|-----------------|
|
|
1167
|
+
| New component styling | Yes |
|
|
1168
|
+
| Layout/responsive changes | Yes |
|
|
1169
|
+
| Shadcn/UI work | Yes |
|
|
1170
|
+
| Color/theme updates | Yes |
|
|
1171
|
+
| API integration only | No |
|
|
1172
|
+
| Logic/state changes | No |
|
|
1173
|
+
| Bug fix (visual) | Yes |
|
|
1174
|
+
| Bug fix (behavioral) | No |
|
|
1175
|
+
|
|
1126
1176
|
DEPENDENCY HANDLING (Critical for AG-UI)
|
|
1127
1177
|
|
|
1128
1178
|
**Common AG-UI Blockers**:
|