gsd-pi 2.4.0 → 2.5.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.
Files changed (35) hide show
  1. package/README.md +4 -3
  2. package/dist/loader.js +21 -3
  3. package/dist/logo.d.ts +3 -3
  4. package/dist/logo.js +2 -2
  5. package/package.json +2 -2
  6. package/src/resources/GSD-WORKFLOW.md +7 -7
  7. package/src/resources/extensions/get-secrets-from-user.ts +63 -8
  8. package/src/resources/extensions/gsd/auto.ts +123 -34
  9. package/src/resources/extensions/gsd/docs/preferences-reference.md +28 -0
  10. package/src/resources/extensions/gsd/files.ts +70 -0
  11. package/src/resources/extensions/gsd/git-service.ts +151 -11
  12. package/src/resources/extensions/gsd/gitignore.ts +1 -0
  13. package/src/resources/extensions/gsd/guided-flow.ts +6 -3
  14. package/src/resources/extensions/gsd/preferences.ts +59 -0
  15. package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  16. package/src/resources/extensions/gsd/prompts/complete-slice.md +8 -6
  17. package/src/resources/extensions/gsd/prompts/discuss.md +7 -15
  18. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -7
  19. package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +4 -0
  20. package/src/resources/extensions/gsd/prompts/plan-milestone.md +33 -1
  21. package/src/resources/extensions/gsd/prompts/plan-slice.md +24 -32
  22. package/src/resources/extensions/gsd/prompts/replan-slice.md +1 -1
  23. package/src/resources/extensions/gsd/templates/plan.md +8 -10
  24. package/src/resources/extensions/gsd/templates/preferences.md +7 -0
  25. package/src/resources/extensions/gsd/templates/secrets-manifest.md +22 -0
  26. package/src/resources/extensions/gsd/templates/task-plan.md +6 -6
  27. package/src/resources/extensions/gsd/tests/git-service.test.ts +421 -0
  28. package/src/resources/extensions/gsd/tests/parsers.test.ts +211 -65
  29. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +0 -2
  30. package/src/resources/extensions/gsd/tests/secure-env-collect.test.ts +185 -0
  31. package/src/resources/extensions/gsd/types.ts +20 -0
  32. package/src/resources/extensions/gsd/worktree-command.ts +48 -6
  33. package/src/resources/extensions/gsd/worktree.ts +40 -147
  34. package/src/resources/extensions/search-the-web/index.ts +16 -25
  35. package/src/resources/extensions/search-the-web/native-search.ts +157 -0
@@ -7,10 +7,12 @@ import {
7
7
  inferCommitType,
8
8
  GitServiceImpl,
9
9
  RUNTIME_EXCLUSION_PATHS,
10
+ VALID_BRANCH_NAME,
10
11
  runGit,
11
12
  type GitPreferences,
12
13
  type CommitOptions,
13
14
  type MergeSliceResult,
15
+ type PreMergeCheckResult,
14
16
  } from "../git-service.ts";
15
17
 
16
18
  let passed = 0;
@@ -881,6 +883,425 @@ async function main(): Promise<void> {
881
883
  rmSync(repo, { recursive: true, force: true });
882
884
  }
883
885
 
886
+ // ═══════════════════════════════════════════════════════════════════════
887
+ // S05: Enhanced features — merge guards, snapshots, auto-push, rich commits
888
+ // ═══════════════════════════════════════════════════════════════════════
889
+
890
+ // ─── createSnapshot: prefs enabled ─────────────────────────────────────
891
+
892
+ console.log("\n=== createSnapshot: enabled ===");
893
+
894
+ {
895
+ const repo = initBranchTestRepo();
896
+ const svc = new GitServiceImpl(repo, { snapshots: true });
897
+
898
+ // Create a slice branch with a commit
899
+ svc.ensureSliceBranch("M001", "S01");
900
+ createFile(repo, "src/snap.ts", "snapshot me");
901
+ svc.commit({ message: "snapshot test commit" });
902
+
903
+ // Create snapshot ref for this slice branch
904
+ svc.createSnapshot("gsd/M001/S01");
905
+
906
+ // Verify ref exists under refs/gsd/snapshots/
907
+ const refs = run("git for-each-ref refs/gsd/snapshots/", repo);
908
+ assert(refs.includes("refs/gsd/snapshots/gsd/M001/S01/"), "snapshot ref created under refs/gsd/snapshots/");
909
+
910
+ rmSync(repo, { recursive: true, force: true });
911
+ }
912
+
913
+ // ─── createSnapshot: prefs disabled ────────────────────────────────────
914
+
915
+ console.log("\n=== createSnapshot: disabled ===");
916
+
917
+ {
918
+ const repo = initBranchTestRepo();
919
+ const svc = new GitServiceImpl(repo, { snapshots: false });
920
+
921
+ svc.ensureSliceBranch("M001", "S01");
922
+ createFile(repo, "src/no-snap.ts", "no snapshot");
923
+ svc.commit({ message: "no snapshot commit" });
924
+
925
+ // createSnapshot should be a no-op when disabled
926
+ svc.createSnapshot("gsd/M001/S01");
927
+
928
+ const refs = run("git for-each-ref refs/gsd/snapshots/", repo);
929
+ assertEq(refs, "", "no snapshot ref created when prefs.snapshots is false");
930
+
931
+ rmSync(repo, { recursive: true, force: true });
932
+ }
933
+
934
+ // ─── runPreMergeCheck: pass ────────────────────────────────────────────
935
+
936
+ console.log("\n=== runPreMergeCheck: pass ===");
937
+
938
+ {
939
+ const repo = initBranchTestRepo();
940
+ // Create package.json with passing test script
941
+ createFile(repo, "package.json", JSON.stringify({
942
+ name: "test-pass",
943
+ scripts: { test: "node -e 'process.exit(0)'" },
944
+ }));
945
+ run("git add -A", repo);
946
+ run("git commit -m 'add package.json'", repo);
947
+
948
+ const svc = new GitServiceImpl(repo, { pre_merge_check: true });
949
+ const result: PreMergeCheckResult = svc.runPreMergeCheck();
950
+
951
+ assertEq(result.passed, true, "runPreMergeCheck returns passed:true when tests pass");
952
+ assert(!result.skipped, "runPreMergeCheck is not skipped when enabled");
953
+
954
+ rmSync(repo, { recursive: true, force: true });
955
+ }
956
+
957
+ // ─── runPreMergeCheck: fail ────────────────────────────────────────────
958
+
959
+ console.log("\n=== runPreMergeCheck: fail ===");
960
+
961
+ {
962
+ const repo = initBranchTestRepo();
963
+ // Create package.json with failing test script
964
+ createFile(repo, "package.json", JSON.stringify({
965
+ name: "test-fail",
966
+ scripts: { test: "node -e 'process.exit(1)'" },
967
+ }));
968
+ run("git add -A", repo);
969
+ run("git commit -m 'add failing package.json'", repo);
970
+
971
+ const svc = new GitServiceImpl(repo, { pre_merge_check: true });
972
+ const result: PreMergeCheckResult = svc.runPreMergeCheck();
973
+
974
+ assertEq(result.passed, false, "runPreMergeCheck returns passed:false when tests fail");
975
+ assert(!result.skipped, "runPreMergeCheck is not skipped when enabled");
976
+
977
+ rmSync(repo, { recursive: true, force: true });
978
+ }
979
+
980
+ // ─── runPreMergeCheck: disabled ────────────────────────────────────────
981
+
982
+ console.log("\n=== runPreMergeCheck: disabled ===");
983
+
984
+ {
985
+ const repo = initBranchTestRepo();
986
+ createFile(repo, "package.json", JSON.stringify({
987
+ name: "test-disabled",
988
+ scripts: { test: "node -e 'process.exit(1)'" },
989
+ }));
990
+ run("git add -A", repo);
991
+ run("git commit -m 'add package.json'", repo);
992
+
993
+ const svc = new GitServiceImpl(repo, { pre_merge_check: false });
994
+ const result: PreMergeCheckResult = svc.runPreMergeCheck();
995
+
996
+ assertEq(result.skipped, true, "runPreMergeCheck skipped when pre_merge_check is false");
997
+ assertEq(result.passed, true, "runPreMergeCheck returns passed:true when skipped (no block)");
998
+
999
+ rmSync(repo, { recursive: true, force: true });
1000
+ }
1001
+
1002
+ // ─── runPreMergeCheck: custom command ──────────────────────────────────
1003
+
1004
+ console.log("\n=== runPreMergeCheck: custom command ===");
1005
+
1006
+ {
1007
+ const repo = initBranchTestRepo();
1008
+ // Custom command string overrides auto-detection
1009
+ const svc = new GitServiceImpl(repo, { pre_merge_check: "node -e 'process.exit(0)'" });
1010
+ const result: PreMergeCheckResult = svc.runPreMergeCheck();
1011
+
1012
+ assertEq(result.passed, true, "runPreMergeCheck passes with custom command that exits 0");
1013
+ assert(!result.skipped, "custom command is not skipped");
1014
+
1015
+ rmSync(repo, { recursive: true, force: true });
1016
+ }
1017
+
1018
+ // ─── Rich commit message ──────────────────────────────────────────────
1019
+
1020
+ console.log("\n=== mergeSliceToMain: rich commit message ===");
1021
+
1022
+ {
1023
+ const repo = initBranchTestRepo();
1024
+ const svc = new GitServiceImpl(repo, { pre_merge_check: false });
1025
+
1026
+ svc.ensureSliceBranch("M001", "S01");
1027
+
1028
+ // Make 3 distinct commits on the slice branch
1029
+ createFile(repo, "src/auth.ts", "export const auth = true;");
1030
+ svc.commit({ message: "add auth module" });
1031
+
1032
+ createFile(repo, "src/login.ts", "export const login = true;");
1033
+ svc.commit({ message: "add login page" });
1034
+
1035
+ createFile(repo, "src/session.ts", "export const session = true;");
1036
+ svc.commit({ message: "add session handling" });
1037
+
1038
+ svc.switchToMain();
1039
+ const result = svc.mergeSliceToMain("M001", "S01", "Implement user authentication");
1040
+
1041
+ // Inspect the full commit body on main
1042
+ const commitBody = run("git log -1 --format=%B", repo);
1043
+
1044
+ // Rich commit should have the subject line
1045
+ assert(commitBody.includes("feat(M001/S01): Implement user authentication"),
1046
+ "rich commit has conventional subject line");
1047
+
1048
+ // Rich commit body should include task list with commit subjects
1049
+ assert(commitBody.includes("add auth module"),
1050
+ "rich commit body includes first commit subject");
1051
+ assert(commitBody.includes("add login page"),
1052
+ "rich commit body includes second commit subject");
1053
+ assert(commitBody.includes("add session handling"),
1054
+ "rich commit body includes third commit subject");
1055
+
1056
+ // Rich commit body should include Branch: line for forensics
1057
+ assert(commitBody.includes("Branch:"),
1058
+ "rich commit body includes Branch: line");
1059
+ assert(commitBody.includes("gsd/M001/S01"),
1060
+ "rich commit body Branch: line includes slice branch name");
1061
+
1062
+ rmSync(repo, { recursive: true, force: true });
1063
+ }
1064
+
1065
+ // ─── Auto-push: enabled ───────────────────────────────────────────────
1066
+
1067
+ console.log("\n=== Auto-push: enabled ===");
1068
+
1069
+ {
1070
+ // Create a bare remote repo
1071
+ const bareDir = mkdtempSync(join(tmpdir(), "gsd-git-bare-"));
1072
+ run("git init --bare -b main", bareDir);
1073
+
1074
+ // Create local repo and add the bare as remote
1075
+ const repo = initBranchTestRepo();
1076
+ run(`git remote add origin ${bareDir}`, repo);
1077
+ run("git push -u origin main", repo);
1078
+
1079
+ const svc = new GitServiceImpl(repo, { auto_push: true, pre_merge_check: false });
1080
+
1081
+ svc.ensureSliceBranch("M001", "S01");
1082
+ createFile(repo, "src/pushed.ts", "export const pushed = true;");
1083
+ svc.commit({ message: "work to push" });
1084
+
1085
+ svc.switchToMain();
1086
+ svc.mergeSliceToMain("M001", "S01", "Add pushed feature");
1087
+
1088
+ // Verify the remote has the merge commit
1089
+ const remoteLog = run(`git --git-dir=${bareDir} log --oneline -1`, bareDir);
1090
+ assert(remoteLog.includes("Add pushed feature"),
1091
+ "auto-push: remote has the merge commit when auto_push is true");
1092
+
1093
+ rmSync(repo, { recursive: true, force: true });
1094
+ rmSync(bareDir, { recursive: true, force: true });
1095
+ }
1096
+
1097
+ // ─── Auto-push: disabled ──────────────────────────────────────────────
1098
+
1099
+ console.log("\n=== Auto-push: disabled ===");
1100
+
1101
+ {
1102
+ const bareDir = mkdtempSync(join(tmpdir(), "gsd-git-bare-"));
1103
+ run("git init --bare -b main", bareDir);
1104
+
1105
+ const repo = initBranchTestRepo();
1106
+ run(`git remote add origin ${bareDir}`, repo);
1107
+ run("git push -u origin main", repo);
1108
+
1109
+ // auto_push explicitly false (or omitted — same behavior)
1110
+ const svc = new GitServiceImpl(repo, { auto_push: false, pre_merge_check: false });
1111
+
1112
+ svc.ensureSliceBranch("M001", "S01");
1113
+ createFile(repo, "src/not-pushed.ts", "export const notPushed = true;");
1114
+ svc.commit({ message: "work not pushed" });
1115
+
1116
+ svc.switchToMain();
1117
+ svc.mergeSliceToMain("M001", "S01", "Add unpushed feature");
1118
+
1119
+ // Remote should NOT have the new merge commit — still at the initial push
1120
+ const remoteLog = run(`git --git-dir=${bareDir} log --oneline`, bareDir);
1121
+ assert(!remoteLog.includes("Add unpushed feature"),
1122
+ "auto-push: remote does NOT have merge commit when auto_push is false");
1123
+
1124
+ rmSync(repo, { recursive: true, force: true });
1125
+ rmSync(bareDir, { recursive: true, force: true });
1126
+ }
1127
+
1128
+ // ─── Remote fetch before branching: with remote ────────────────────────
1129
+
1130
+ console.log("\n=== Remote fetch: with remote ===");
1131
+
1132
+ {
1133
+ const bareDir = mkdtempSync(join(tmpdir(), "gsd-git-bare-"));
1134
+ run("git init --bare -b main", bareDir);
1135
+
1136
+ const repo = initBranchTestRepo();
1137
+ run(`git remote add origin ${bareDir}`, repo);
1138
+ run("git push -u origin main", repo);
1139
+
1140
+ // Add a commit to the remote via a temporary clone
1141
+ const cloneDir = mkdtempSync(join(tmpdir(), "gsd-git-clone-"));
1142
+ run(`git clone ${bareDir} ${cloneDir}`, cloneDir);
1143
+ run("git config user.name 'Remote Dev'", cloneDir);
1144
+ run("git config user.email 'remote@example.com'", cloneDir);
1145
+ createFile(cloneDir, "remote-file.txt", "from remote");
1146
+ run("git add -A", cloneDir);
1147
+ run("git commit -m 'remote commit'", cloneDir);
1148
+ run("git push origin main", cloneDir);
1149
+
1150
+ // ensureSliceBranch should fetch before creating the branch — no crash
1151
+ const svc = new GitServiceImpl(repo);
1152
+ let noError = true;
1153
+ try {
1154
+ svc.ensureSliceBranch("M001", "S01");
1155
+ } catch {
1156
+ noError = false;
1157
+ }
1158
+ assert(noError, "ensureSliceBranch succeeds when remote has new commits (fetch runs)");
1159
+
1160
+ rmSync(repo, { recursive: true, force: true });
1161
+ rmSync(bareDir, { recursive: true, force: true });
1162
+ rmSync(cloneDir, { recursive: true, force: true });
1163
+ }
1164
+
1165
+ // ─── Remote fetch before branching: without remote ─────────────────────
1166
+
1167
+ console.log("\n=== Remote fetch: without remote ===");
1168
+
1169
+ {
1170
+ const repo = initBranchTestRepo();
1171
+ // No remote configured — ensureSliceBranch should not crash
1172
+ const svc = new GitServiceImpl(repo);
1173
+
1174
+ let noError = true;
1175
+ try {
1176
+ svc.ensureSliceBranch("M001", "S01");
1177
+ } catch {
1178
+ noError = false;
1179
+ }
1180
+ assert(noError, "ensureSliceBranch succeeds when no remote is configured");
1181
+ assertEq(svc.getCurrentBranch(), "gsd/M001/S01", "branch created even without remote");
1182
+
1183
+ rmSync(repo, { recursive: true, force: true });
1184
+ }
1185
+
1186
+ // ─── Facade prefs: mergeSliceToMain creates snapshot when prefs set ────
1187
+
1188
+ console.log("\n=== Facade prefs: snapshot via merge with prefs ===");
1189
+
1190
+ {
1191
+ const repo = initBranchTestRepo();
1192
+ // Simulate facade behavior: GitServiceImpl with snapshots:true should
1193
+ // create a snapshot ref during mergeSliceToMain
1194
+ const svc = new GitServiceImpl(repo, { snapshots: true, pre_merge_check: false });
1195
+
1196
+ svc.ensureSliceBranch("M001", "S01");
1197
+ createFile(repo, "src/facade-test.ts", "facade");
1198
+ svc.commit({ message: "facade test commit" });
1199
+
1200
+ svc.switchToMain();
1201
+ svc.mergeSliceToMain("M001", "S01", "Facade snapshot test");
1202
+
1203
+ // After merge, a snapshot ref should exist (created before merge)
1204
+ const refs = run("git for-each-ref refs/gsd/snapshots/", repo);
1205
+ assert(refs.includes("refs/gsd/snapshots/"), "mergeSliceToMain creates snapshot when prefs.snapshots is true");
1206
+ assert(refs.includes("gsd/M001/S01"), "snapshot ref references the slice branch name");
1207
+
1208
+ rmSync(repo, { recursive: true, force: true });
1209
+ }
1210
+
1211
+ // ─── Facade prefs: no snapshot when prefs omit snapshots ───────────────
1212
+
1213
+ console.log("\n=== Facade prefs: no snapshot when prefs omit snapshots ===");
1214
+
1215
+ {
1216
+ const repo = initBranchTestRepo();
1217
+ // Default prefs — snapshots not enabled
1218
+ const svc = new GitServiceImpl(repo, { pre_merge_check: false });
1219
+
1220
+ svc.ensureSliceBranch("M001", "S01");
1221
+ createFile(repo, "src/no-facade-snap.ts", "no facade snap");
1222
+ svc.commit({ message: "no facade snapshot" });
1223
+
1224
+ svc.switchToMain();
1225
+ svc.mergeSliceToMain("M001", "S01", "No snapshot test");
1226
+
1227
+ // No snapshot ref should exist
1228
+ const refs = run("git for-each-ref refs/gsd/snapshots/", repo);
1229
+ assertEq(refs, "", "no snapshot ref when snapshots pref is not set");
1230
+
1231
+ rmSync(repo, { recursive: true, force: true });
1232
+ }
1233
+
1234
+ // ─── VALID_BRANCH_NAME regex ──────────────────────────────────────────
1235
+
1236
+ console.log("\n=== VALID_BRANCH_NAME regex ===");
1237
+
1238
+ {
1239
+ // Valid branch names
1240
+ assert(VALID_BRANCH_NAME.test("main"), "VALID_BRANCH_NAME accepts 'main'");
1241
+ assert(VALID_BRANCH_NAME.test("master"), "VALID_BRANCH_NAME accepts 'master'");
1242
+ assert(VALID_BRANCH_NAME.test("develop"), "VALID_BRANCH_NAME accepts 'develop'");
1243
+ assert(VALID_BRANCH_NAME.test("feature/foo"), "VALID_BRANCH_NAME accepts 'feature/foo'");
1244
+ assert(VALID_BRANCH_NAME.test("release-1.0"), "VALID_BRANCH_NAME accepts 'release-1.0'");
1245
+ assert(VALID_BRANCH_NAME.test("my_branch"), "VALID_BRANCH_NAME accepts 'my_branch'");
1246
+ assert(VALID_BRANCH_NAME.test("v2.0.1"), "VALID_BRANCH_NAME accepts 'v2.0.1'");
1247
+
1248
+ // Invalid / injection attempts
1249
+ assert(!VALID_BRANCH_NAME.test("main; rm -rf /"), "VALID_BRANCH_NAME rejects shell injection");
1250
+ assert(!VALID_BRANCH_NAME.test("main && echo pwned"), "VALID_BRANCH_NAME rejects && injection");
1251
+ assert(!VALID_BRANCH_NAME.test(""), "VALID_BRANCH_NAME rejects empty string");
1252
+ assert(!VALID_BRANCH_NAME.test("branch name"), "VALID_BRANCH_NAME rejects spaces");
1253
+ assert(!VALID_BRANCH_NAME.test("branch`cmd`"), "VALID_BRANCH_NAME rejects backticks");
1254
+ assert(!VALID_BRANCH_NAME.test("branch$(cmd)"), "VALID_BRANCH_NAME rejects $() subshell");
1255
+ }
1256
+
1257
+ // ─── getMainBranch: configured main_branch preference ──────────────────
1258
+
1259
+ console.log("\n=== getMainBranch: configured main_branch ===");
1260
+
1261
+ {
1262
+ const repo = initBranchTestRepo();
1263
+ const svc = new GitServiceImpl(repo, { main_branch: "trunk" });
1264
+
1265
+ assertEq(svc.getMainBranch(), "trunk", "getMainBranch returns configured main_branch preference");
1266
+
1267
+ rmSync(repo, { recursive: true, force: true });
1268
+ }
1269
+
1270
+ // ─── getMainBranch: falls back to auto-detection when not set ──────────
1271
+
1272
+ console.log("\n=== getMainBranch: fallback to auto-detection ===");
1273
+
1274
+ {
1275
+ const repo = initBranchTestRepo();
1276
+ const svc = new GitServiceImpl(repo, {});
1277
+
1278
+ assertEq(svc.getMainBranch(), "main", "getMainBranch falls back to auto-detection when main_branch not set");
1279
+
1280
+ rmSync(repo, { recursive: true, force: true });
1281
+ }
1282
+
1283
+ // ─── getMainBranch: ignores invalid branch names ───────────────────────
1284
+
1285
+ console.log("\n=== getMainBranch: ignores invalid branch name ===");
1286
+
1287
+ {
1288
+ const repo = initBranchTestRepo();
1289
+ const svc = new GitServiceImpl(repo, { main_branch: "main; rm -rf /" });
1290
+
1291
+ assertEq(svc.getMainBranch(), "main", "getMainBranch ignores invalid branch name and falls back to auto-detection");
1292
+
1293
+ rmSync(repo, { recursive: true, force: true });
1294
+ }
1295
+
1296
+ // ─── PreMergeCheckResult type export compile check ─────────────────────
1297
+
1298
+ console.log("\n=== PreMergeCheckResult type export ===");
1299
+
1300
+ {
1301
+ const _checkResult: PreMergeCheckResult = { passed: true, skipped: false };
1302
+ assert(true, "PreMergeCheckResult type exported and usable");
1303
+ }
1304
+
884
1305
  console.log(`\nResults: ${passed} passed, ${failed} failed`);
885
1306
  if (failed > 0) process.exit(1);
886
1307
  console.log("All tests passed ✓");