shark-ai 0.3.18 → 0.4.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/dist/bin/shark.js CHANGED
@@ -417,7 +417,35 @@ var sseClient = new SSEClient();
417
417
  // src/core/agents/agent-response-parser.ts
418
418
  import { z as z2 } from "zod";
419
419
  var AgentActionSchema = z2.object({
420
- type: z2.enum(["create_file", "modify_file", "modify_ast", "search_ast", "delete_file", "talk_with_user", "list_files", "read_file", "search_file", "run_command", "use_mcp_tool"]),
420
+ type: z2.enum([
421
+ "create_file",
422
+ "modify_file",
423
+ "list_files",
424
+ "search_file",
425
+ "read_file",
426
+ "delete_file",
427
+ "list_structure",
428
+ "modify_ast",
429
+ "search_ast",
430
+ "run_command",
431
+ "talk_with_user",
432
+ "ast_list_structure",
433
+ "ast_get_method",
434
+ "ast_add_method",
435
+ "ast_modify_method",
436
+ "ast_remove_method",
437
+ "ast_add_class",
438
+ "ast_add_property",
439
+ "ast_remove_property",
440
+ "ast_add_decorator",
441
+ "ast_add_interface",
442
+ "ast_add_type_alias",
443
+ "ast_add_function",
444
+ "ast_remove_function",
445
+ "ast_add_import",
446
+ "ast_remove_import",
447
+ "ast_organize_imports"
448
+ ]),
421
449
  path: z2.string().nullable().optional(),
422
450
  // Nullable for strict mode combatibility
423
451
  content: z2.string().nullable().optional(),
@@ -433,6 +461,22 @@ var AgentActionSchema = z2.object({
433
461
  language: z2.string().nullable().optional(),
434
462
  file_path: z2.string().nullable().optional(),
435
463
  // Alias for path in ast-grep actions
464
+ // New AST Tool Specific Fields
465
+ class_name: z2.string().nullable().optional(),
466
+ method_name: z2.string().nullable().optional(),
467
+ method_code: z2.string().nullable().optional(),
468
+ property_name: z2.string().nullable().optional(),
469
+ property_code: z2.string().nullable().optional(),
470
+ extends_class: z2.string().nullable().optional(),
471
+ implements_interfaces: z2.array(z2.string()).nullable().optional(),
472
+ decorator_code: z2.string().nullable().optional(),
473
+ interface_code: z2.string().nullable().optional(),
474
+ type_code: z2.string().nullable().optional(),
475
+ function_name: z2.string().nullable().optional(),
476
+ function_code: z2.string().nullable().optional(),
477
+ import_statement: z2.string().nullable().optional(),
478
+ module_path: z2.string().nullable().optional(),
479
+ new_body: z2.string().nullable().optional(),
436
480
  // Preview confirmation
437
481
  confirmed: z2.boolean().nullable().optional()
438
482
  });
@@ -786,142 +830,413 @@ async function interactiveBusinessAnalyst() {
786
830
  }
787
831
 
788
832
  // src/core/agents/specification-agent.ts
789
- import fs4 from "fs";
790
- import path5 from "path";
833
+ import fs5 from "fs";
834
+ import path6 from "path";
791
835
 
792
836
  // src/core/agents/agent-tools.ts
793
- import fs3 from "fs";
794
- import path4 from "path";
837
+ import fs4 from "fs";
838
+ import path5 from "path";
795
839
  import fg from "fast-glob";
796
840
  import { exec } from "child_process";
797
841
  import { promisify } from "util";
798
842
  import { fileURLToPath } from "url";
799
843
 
800
- // src/core/services/code-review.service.ts
801
- import path3 from "path";
802
- var CodeReviewService = class {
844
+ // src/core/ast-editing/editors/code-editor-factory.ts
845
+ import * as path4 from "path";
846
+
847
+ // src/core/ast-editing/editors/typescript-editor.ts
848
+ import { Project, SyntaxKind } from "ts-morph";
849
+ import * as path3 from "path";
850
+ import * as fs3 from "fs";
851
+ var TypeScriptEditor = class {
852
+ project;
853
+ constructor() {
854
+ this.project = new Project({
855
+ skipAddingFilesFromTsConfig: true,
856
+ compilerOptions: {
857
+ target: 99,
858
+ // ESNext
859
+ module: 99
860
+ // ESNext
861
+ }
862
+ });
863
+ }
803
864
  /**
804
- * Reviews the code modification and returns a status/feedback string.
805
- * This string is intended to be appended to the user Preview so the Agent can see it.
865
+ * Get or add source file to project
806
866
  */
807
- static async reviewCode(filePath, newContent) {
808
- const config = ConfigManager.getInstance().getConfig();
809
- const validation = config.validation || { llmReviewExtensions: [".ts", ".tsx"], syntaxCheckExtensions: ["*"] };
810
- const ext = path3.extname(filePath).toLowerCase();
811
- const needsLlmReview = validation.llmReviewExtensions.includes(ext) || validation.llmReviewExtensions.includes("*");
812
- if (needsLlmReview) {
813
- return await this.performLlmReview(filePath, newContent);
814
- }
815
- const needsSyntaxCheck = validation.syntaxCheckExtensions.includes(ext) || validation.syntaxCheckExtensions.includes("*");
816
- if (needsSyntaxCheck) {
817
- return this.performSyntaxCheck(filePath, newContent);
818
- }
819
- return "\u2139\uFE0F No specific validation configured for this file type.";
820
- }
821
- static getCodeReviewAgentId() {
822
- const config = ConfigManager.getInstance().getConfig();
823
- if (config.agents?.codeReview) return config.agents.codeReview;
824
- if (process.env.STACKSPOT_CODE_REVIEW_AGENT_ID) return process.env.STACKSPOT_CODE_REVIEW_AGENT_ID;
825
- return "";
826
- }
827
- static async performLlmReview(filePath, content) {
828
- const agentId = this.getCodeReviewAgentId();
829
- if (!agentId) {
830
- return this.performSyntaxCheck(filePath, content);
867
+ getSourceFile(filePath) {
868
+ const absolutePath = path3.resolve(filePath);
869
+ let sourceFile = this.project.getSourceFile(absolutePath);
870
+ if (!sourceFile) {
871
+ if (!fs3.existsSync(absolutePath)) {
872
+ throw new Error(`File not found: ${absolutePath}`);
873
+ }
874
+ sourceFile = this.project.addSourceFileAtPath(absolutePath);
875
+ }
876
+ return sourceFile;
877
+ }
878
+ /**
879
+ * Get class declaration by name
880
+ */
881
+ getClass(sourceFile, className) {
882
+ const classDecl = sourceFile.getClass(className);
883
+ if (!classDecl) {
884
+ throw new Error(`Class "${className}" not found in ${sourceFile.getFilePath()}`);
885
+ }
886
+ return classDecl;
887
+ }
888
+ // ═══════════════════════════════════════════════════════
889
+ // IMPLEMENTATION: listStructure
890
+ // ═══════════════════════════════════════════════════════
891
+ async listStructure(filePath) {
892
+ const sourceFile = this.getSourceFile(filePath);
893
+ const classes = sourceFile.getClasses().map((cls) => ({
894
+ name: cls.getName() || "<anonymous>",
895
+ methods: cls.getMethods().map((m) => this.extractMethodInfo(m)),
896
+ properties: cls.getProperties().map((p) => this.extractPropertyInfo(p)),
897
+ decorators: cls.getDecorators().map((d) => d.getText()),
898
+ extendsClass: cls.getExtends()?.getText(),
899
+ implementsInterfaces: cls.getImplements().map((i) => i.getText())
900
+ }));
901
+ const interfaces = sourceFile.getInterfaces().map((iface) => ({
902
+ name: iface.getName(),
903
+ properties: iface.getProperties().map((p) => this.extractPropertyInfo(p)),
904
+ extends: iface.getExtends().map((e) => e.getText())
905
+ }));
906
+ const functions = sourceFile.getFunctions().map((fn) => ({
907
+ name: fn.getName() || "<anonymous>",
908
+ parameters: fn.getParameters().map((p) => ({
909
+ name: p.getName(),
910
+ type: p.getType().getText(),
911
+ isOptional: p.isOptional()
912
+ })),
913
+ returnType: fn.getReturnType().getText(),
914
+ isAsync: fn.isAsync(),
915
+ isExported: fn.isExported()
916
+ }));
917
+ const imports = sourceFile.getImportDeclarations().map((imp) => ({
918
+ modulePath: imp.getModuleSpecifierValue(),
919
+ isDefault: !!imp.getDefaultImport(),
920
+ namedImports: imp.getNamedImports().map((n) => n.getName()),
921
+ namespaceImport: imp.getNamespaceImport()?.getText()
922
+ }));
923
+ const exports = sourceFile.getExportedDeclarations();
924
+ const exportInfo = Array.from(exports.entries()).flatMap(
925
+ ([name, declarations]) => declarations.map((decl) => ({
926
+ name,
927
+ type: this.getDeclarationType(decl),
928
+ isDefault: sourceFile.getDefaultExportSymbol()?.getName() === name
929
+ }))
930
+ );
931
+ return {
932
+ classes,
933
+ interfaces,
934
+ functions,
935
+ imports,
936
+ exports: exportInfo
937
+ };
938
+ }
939
+ extractMethodInfo(method) {
940
+ return {
941
+ name: method.getName(),
942
+ parameters: method.getParameters().map((p) => ({
943
+ name: p.getName(),
944
+ type: p.getType().getText(),
945
+ isOptional: p.isOptional()
946
+ })),
947
+ returnType: method.getReturnType().getText(),
948
+ isAsync: method.isAsync(),
949
+ isStatic: method.isStatic(),
950
+ visibility: this.getVisibility(method),
951
+ decorators: method.getDecorators().map((d) => d.getText())
952
+ };
953
+ }
954
+ extractPropertyInfo(property) {
955
+ return {
956
+ name: property.getName(),
957
+ type: property.getType()?.getText(),
958
+ visibility: this.getVisibility(property),
959
+ isReadonly: property.isReadonly(),
960
+ initializer: property.getInitializer()?.getText()
961
+ };
962
+ }
963
+ getVisibility(node) {
964
+ if (node.hasModifier?.(SyntaxKind.PrivateKeyword)) return "private";
965
+ if (node.hasModifier?.(SyntaxKind.ProtectedKeyword)) return "protected";
966
+ return "public";
967
+ }
968
+ getDeclarationType(decl) {
969
+ if (decl.getKind() === SyntaxKind.ClassDeclaration) return "class";
970
+ if (decl.getKind() === SyntaxKind.FunctionDeclaration) return "function";
971
+ if (decl.getKind() === SyntaxKind.InterfaceDeclaration) return "interface";
972
+ if (decl.getKind() === SyntaxKind.TypeAliasDeclaration) return "type";
973
+ return "const";
974
+ }
975
+ // ═══════════════════════════════════════════════════════
976
+ // IMPLEMENTATION: Class Operations
977
+ // ═══════════════════════════════════════════════════════
978
+ async addClass(filePath, className, options) {
979
+ try {
980
+ const sourceFile = this.getSourceFile(filePath);
981
+ sourceFile.addClass({
982
+ name: className,
983
+ extends: options?.extendsClass,
984
+ implements: options?.implementsInterfaces,
985
+ isExported: true
986
+ });
987
+ await sourceFile.save();
988
+ return true;
989
+ } catch (error) {
990
+ console.error(`Failed to add class "${className}":`, error);
991
+ return false;
992
+ }
993
+ }
994
+ async addProperty(filePath, className, propertyCode) {
995
+ try {
996
+ const sourceFile = this.getSourceFile(filePath);
997
+ const classDecl = this.getClass(sourceFile, className);
998
+ const closeBrace = classDecl.getEnd() - 1;
999
+ classDecl.insertText(closeBrace, `
1000
+ ${propertyCode}`);
1001
+ classDecl.formatText();
1002
+ await sourceFile.save();
1003
+ return true;
1004
+ } catch (error) {
1005
+ console.error(`Failed to add property to "${className}":`, error);
1006
+ return false;
1007
+ }
1008
+ }
1009
+ async removeProperty(filePath, className, propertyName) {
1010
+ try {
1011
+ const sourceFile = this.getSourceFile(filePath);
1012
+ const classDecl = this.getClass(sourceFile, className);
1013
+ const property = classDecl.getProperty(propertyName);
1014
+ if (!property) {
1015
+ throw new Error(`Property "${propertyName}" not found in class "${className}"`);
1016
+ }
1017
+ property.remove();
1018
+ await sourceFile.save();
1019
+ return true;
1020
+ } catch (error) {
1021
+ console.error(`Failed to remove property "${propertyName}":`, error);
1022
+ return false;
1023
+ }
1024
+ }
1025
+ async addMethod(filePath, className, methodCode) {
1026
+ try {
1027
+ const sourceFile = this.getSourceFile(filePath);
1028
+ const classDecl = this.getClass(sourceFile, className);
1029
+ const closeBrace = classDecl.getEnd() - 1;
1030
+ classDecl.insertText(closeBrace, `
1031
+ ${methodCode}
1032
+ `);
1033
+ classDecl.formatText();
1034
+ await sourceFile.save();
1035
+ return true;
1036
+ } catch (error) {
1037
+ console.error(`Failed to add method to "${className}":`, error);
1038
+ return false;
1039
+ }
1040
+ }
1041
+ async modifyMethod(filePath, className, methodName, newBody) {
1042
+ try {
1043
+ const sourceFile = this.getSourceFile(filePath);
1044
+ const classDecl = this.getClass(sourceFile, className);
1045
+ const method = classDecl.getMethod(methodName);
1046
+ if (!method) {
1047
+ throw new Error(`Method "${methodName}" not found in class "${className}"`);
1048
+ }
1049
+ method.setBodyText(newBody);
1050
+ method.formatText();
1051
+ await sourceFile.save();
1052
+ return true;
1053
+ } catch (error) {
1054
+ console.error(`Failed to modify method "${methodName}":`, error);
1055
+ return false;
1056
+ }
1057
+ }
1058
+ async removeMethod(filePath, className, methodName) {
1059
+ try {
1060
+ const sourceFile = this.getSourceFile(filePath);
1061
+ const classDecl = this.getClass(sourceFile, className);
1062
+ const method = classDecl.getMethod(methodName);
1063
+ if (!method) {
1064
+ throw new Error(`Method "${methodName}" not found in class "${className}"`);
1065
+ }
1066
+ method.remove();
1067
+ await sourceFile.save();
1068
+ return true;
1069
+ } catch (error) {
1070
+ console.error(`Failed to remove method "${methodName}":`, error);
1071
+ return false;
831
1072
  }
1073
+ }
1074
+ async addDecorator(filePath, className, decoratorCode) {
832
1075
  try {
833
- const realm = await getActiveRealm();
834
- const token = await ensureValidToken(realm);
835
- const prompt = `You are a Code Review Specialist. Analyze the following code modification for file: ${path3.basename(filePath)}
1076
+ const sourceFile = this.getSourceFile(filePath);
1077
+ const classDecl = this.getClass(sourceFile, className);
1078
+ const start = classDecl.getStart();
1079
+ sourceFile.insertText(start, `${decoratorCode}
1080
+ `);
1081
+ sourceFile.formatText();
1082
+ await sourceFile.save();
1083
+ return true;
1084
+ } catch (error) {
1085
+ console.error(`Failed to add decorator to "${className}":`, error);
1086
+ return false;
1087
+ }
1088
+ }
1089
+ async getMethod(filePath, className, methodName) {
1090
+ try {
1091
+ const sourceFile = this.getSourceFile(filePath);
1092
+ const classDecl = this.getClass(sourceFile, className);
1093
+ const method = classDecl.getMethod(methodName);
1094
+ if (!method) {
1095
+ return null;
1096
+ }
1097
+ return method.getText();
1098
+ } catch (error) {
1099
+ console.error(`Failed to get method "${methodName}":`, error);
1100
+ return null;
1101
+ }
1102
+ }
1103
+ // ═══════════════════════════════════════════════════════
1104
+ // IMPLEMENTATION: Interface/Type Operations
1105
+ // ═══════════════════════════════════════════════════════
1106
+ async addInterface(filePath, interfaceCode) {
1107
+ try {
1108
+ const sourceFile = this.getSourceFile(filePath);
1109
+ sourceFile.insertText(sourceFile.getEnd(), `
836
1110
 
837
- NEW CODE TO BE ADDED:
838
- \`\`\`
839
- ${content}
840
- \`\`\`
1111
+ ${interfaceCode}`);
1112
+ sourceFile.formatText();
1113
+ await sourceFile.save();
1114
+ return true;
1115
+ } catch (error) {
1116
+ console.error(`Failed to add interface:`, error);
1117
+ return false;
1118
+ }
1119
+ }
1120
+ async addTypeAlias(filePath, typeCode) {
1121
+ try {
1122
+ const sourceFile = this.getSourceFile(filePath);
1123
+ sourceFile.insertText(sourceFile.getEnd(), `
841
1124
 
842
- Provide a concise review focusing on:
843
- 1. Syntax errors or obvious bugs
844
- 2. Potential runtime issues
845
- 3. Code quality concerns
846
-
847
- Respond in JSON format with status, issues array, and summary.`;
848
- const payload = {
849
- user_prompt: prompt,
850
- streaming: true,
851
- use_conversation: true,
852
- stackspot_knowledge: false
853
- };
854
- const url = `${STACKSPOT_AGENT_API_BASE}/v1/agent/${agentId}/chat`;
855
- let reviewText = "";
856
- await sseClient.streamAgentResponse(url, payload, { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }, {
857
- onChunk: (c) => {
858
- reviewText += c;
859
- },
860
- onComplete: () => {
861
- },
862
- onError: (e) => {
863
- throw e;
864
- }
865
- });
866
- const cleanedText = reviewText.trim();
867
- FileLogger.log("CODE_REVIEW", "Raw Response Received", { length: cleanedText.length, content: cleanedText });
868
- try {
869
- const reviewData = extractFirstJson(cleanedText);
870
- return this.formatReviewResponse(reviewData);
871
- } catch (parseErr) {
872
- FileLogger.log("CODE_REVIEW", "JSON Parse Failed", {
873
- error: parseErr.message,
874
- rawText: cleanedText
875
- });
876
- return `\u{1F916} **[AI Code Review Agent]**
877
- \u26A0\uFE0F Response format error. Raw output:
878
- ${cleanedText}`;
1125
+ ${typeCode}`);
1126
+ sourceFile.formatText();
1127
+ await sourceFile.save();
1128
+ return true;
1129
+ } catch (error) {
1130
+ console.error(`Failed to add type alias:`, error);
1131
+ return false;
1132
+ }
1133
+ }
1134
+ // ═══════════════════════════════════════════════════════
1135
+ // IMPLEMENTATION: Function Operations
1136
+ // ═══════════════════════════════════════════════════════
1137
+ async addFunction(filePath, functionCode) {
1138
+ try {
1139
+ const sourceFile = this.getSourceFile(filePath);
1140
+ sourceFile.insertText(sourceFile.getEnd(), `
1141
+
1142
+ ${functionCode}`);
1143
+ sourceFile.formatText();
1144
+ await sourceFile.save();
1145
+ return true;
1146
+ } catch (error) {
1147
+ console.error(`Failed to add function:`, error);
1148
+ return false;
1149
+ }
1150
+ }
1151
+ async removeFunction(filePath, functionName) {
1152
+ try {
1153
+ const sourceFile = this.getSourceFile(filePath);
1154
+ const func = sourceFile.getFunction(functionName);
1155
+ if (!func) {
1156
+ throw new Error(`Function "${functionName}" not found`);
879
1157
  }
880
- } catch (err) {
881
- FileLogger.log("CODE_REVIEW", "LLM Review Failed", { error: err.message });
882
- return `\u26A0\uFE0F AI Review unavailable (${err.message}). Using syntax check:
883
- ` + await this.performSyntaxCheck(filePath, content);
1158
+ func.remove();
1159
+ await sourceFile.save();
1160
+ return true;
1161
+ } catch (error) {
1162
+ console.error(`Failed to remove function "${functionName}":`, error);
1163
+ return false;
884
1164
  }
885
1165
  }
886
- static formatReviewResponse(data) {
887
- let report = `\u{1F916} **[AI Code Review Agent]**
888
- `;
889
- const statusIcon = data.status === "ok" ? "\u2705" : data.status === "warning" ? "\u26A0\uFE0F" : "\u274C";
890
- report += `${statusIcon} **Status:** ${data.status.toUpperCase()}
891
- `;
892
- if (data.issues && data.issues.length > 0) {
893
- report += `
894
- **Issues Found:**
895
- `;
896
- for (const issue of data.issues) {
897
- const severityIcon = issue.severity === "error" ? "\u274C" : issue.severity === "warning" ? "\u26A0\uFE0F" : "\u2139\uFE0F";
898
- const lineInfo = issue.line ? `Linha ${issue.line}: ` : "";
899
- report += `${severityIcon} ${lineInfo}${issue.message}
900
- `;
901
- if (issue.suggestion) {
902
- report += ` \u{1F4A1} ${issue.suggestion}
903
- `;
904
- }
1166
+ // ═══════════════════════════════════════════════════════
1167
+ // IMPLEMENTATION: Import/Export Operations
1168
+ // ═══════════════════════════════════════════════════════
1169
+ async addImport(filePath, importStatement) {
1170
+ try {
1171
+ const sourceFile = this.getSourceFile(filePath);
1172
+ const lastImport = sourceFile.getImportDeclarations().pop();
1173
+ const pos = lastImport ? lastImport.getEnd() : 0;
1174
+ sourceFile.insertText(pos, `
1175
+ ${importStatement}`);
1176
+ this.organizeImports(filePath);
1177
+ await sourceFile.save();
1178
+ return true;
1179
+ } catch (error) {
1180
+ console.error(`Failed to add import:`, error);
1181
+ return false;
1182
+ }
1183
+ }
1184
+ async removeImport(filePath, modulePath) {
1185
+ try {
1186
+ const sourceFile = this.getSourceFile(filePath);
1187
+ const importDecls = sourceFile.getImportDeclarations().filter((imp) => imp.getModuleSpecifierValue() === modulePath);
1188
+ if (importDecls.length === 0) {
1189
+ throw new Error(`Import from "${modulePath}" not found`);
905
1190
  }
1191
+ importDecls.forEach((d) => d.remove());
1192
+ await sourceFile.save();
1193
+ return true;
1194
+ } catch (error) {
1195
+ console.error(`Failed to remove import from "${modulePath}":`, error);
1196
+ return false;
906
1197
  }
907
- report += `
908
- **Resumo:** ${data.summary}`;
909
- return report;
910
1198
  }
911
- static performSyntaxCheck(filePath, content) {
912
- let report = `\u{1F50D} **[Syntax Validator]**
913
- `;
914
- const openBraces = (content.match(/\{/g) || []).length;
915
- const closeBraces = (content.match(/\}/g) || []).length;
916
- if (openBraces !== closeBraces) {
917
- report += `\u274C **CRITICAL:** Brace mismatch detected! Found ${openBraces} '{' and ${closeBraces} '}'.
918
- `;
919
- report += ` **Review your code carefully. Do not confirm if the block is incomplete.**`;
920
- } else {
921
- report += `\u2705 File structure looks balanced (Braces match).
922
- `;
1199
+ async organizeImports(filePath) {
1200
+ try {
1201
+ const sourceFile = this.getSourceFile(filePath);
1202
+ sourceFile.organizeImports();
1203
+ const imports = sourceFile.getImportDeclarations();
1204
+ const importStructure = imports.map((i) => i.getStructure());
1205
+ importStructure.sort((a, b) => {
1206
+ return a.moduleSpecifier.localeCompare(b.moduleSpecifier);
1207
+ });
1208
+ imports.forEach((i) => i.remove());
1209
+ sourceFile.addImportDeclarations(importStructure);
1210
+ await sourceFile.save();
1211
+ return true;
1212
+ } catch (error) {
1213
+ console.error(`Failed to organize imports:`, error);
1214
+ return false;
923
1215
  }
924
- return Promise.resolve(report);
1216
+ }
1217
+ };
1218
+
1219
+ // src/core/ast-editing/editors/code-editor-factory.ts
1220
+ var CodeEditorFactory = class {
1221
+ static tsEditor = null;
1222
+ /**
1223
+ * Returns appropriate editor for the file, or null if AST editing not supported
1224
+ */
1225
+ static getEditor(filePath) {
1226
+ const ext = path4.extname(filePath).toLowerCase();
1227
+ if ([".ts", ".tsx", ".js", ".jsx"].includes(ext)) {
1228
+ if (!this.tsEditor) {
1229
+ this.tsEditor = new TypeScriptEditor();
1230
+ }
1231
+ return this.tsEditor;
1232
+ }
1233
+ return null;
1234
+ }
1235
+ /**
1236
+ * Clear cached editors (useful for testing)
1237
+ */
1238
+ static clearCache() {
1239
+ this.tsEditor = null;
925
1240
  }
926
1241
  };
927
1242
 
@@ -934,9 +1249,9 @@ function detectLineEnding(content) {
934
1249
  }
935
1250
  function handleListFiles(dirPath) {
936
1251
  try {
937
- const fullPath = path4.resolve(process.cwd(), dirPath);
938
- if (!fs3.existsSync(fullPath)) return `Error: Directory ${dirPath} does not exist.`;
939
- const items = fs3.readdirSync(fullPath, { withFileTypes: true });
1252
+ const fullPath = path5.resolve(process.cwd(), dirPath);
1253
+ if (!fs4.existsSync(fullPath)) return `Error: Directory ${dirPath} does not exist.`;
1254
+ const items = fs4.readdirSync(fullPath, { withFileTypes: true });
940
1255
  return items.map((item) => {
941
1256
  return `${item.isDirectory() ? "[DIR]" : "[FILE]"} ${item.name}`;
942
1257
  }).join("\n");
@@ -946,11 +1261,11 @@ function handleListFiles(dirPath) {
946
1261
  }
947
1262
  function handleReadFile(filePath, showLineNumbers = true) {
948
1263
  try {
949
- const fullPath = path4.resolve(process.cwd(), filePath);
950
- if (!fs3.existsSync(fullPath)) return `Error: File ${filePath} does not exist.`;
951
- const stats = fs3.statSync(fullPath);
1264
+ const fullPath = path5.resolve(process.cwd(), filePath);
1265
+ if (!fs4.existsSync(fullPath)) return `Error: File ${filePath} does not exist.`;
1266
+ const stats = fs4.statSync(fullPath);
952
1267
  if (stats.size > 100 * 1024) return `Error: File too large to read (${stats.size} bytes). Limit is 100KB.`;
953
- const content = fs3.readFileSync(fullPath, "utf-8");
1268
+ const content = fs4.readFileSync(fullPath, "utf-8");
954
1269
  if (showLineNumbers) {
955
1270
  const lines = content.split("\n");
956
1271
  return lines.map((line, idx) => `${idx + 1}: ${line}`).join("\n");
@@ -962,11 +1277,11 @@ function handleReadFile(filePath, showLineNumbers = true) {
962
1277
  }
963
1278
  function replaceLineRange(filePath, startLine, endLine, newContent, tui2) {
964
1279
  try {
965
- if (!fs3.existsSync(filePath)) {
1280
+ if (!fs4.existsSync(filePath)) {
966
1281
  tui2.log.error(`\u274C File not found for modification: ${filePath}`);
967
1282
  return false;
968
1283
  }
969
- const currentFileContent = fs3.readFileSync(filePath, "utf-8");
1284
+ const currentFileContent = fs4.readFileSync(filePath, "utf-8");
970
1285
  const lineEnding = detectLineEnding(currentFileContent);
971
1286
  const lines = currentFileContent.split(lineEnding);
972
1287
  if (startLine < 1 || startLine > lines.length) {
@@ -984,7 +1299,7 @@ function replaceLineRange(filePath, startLine, endLine, newContent, tui2) {
984
1299
  const result = [...before, ...normalizedNewLines, ...after].join(lineEnding);
985
1300
  const BOM = "\uFEFF";
986
1301
  const finalContent = result.startsWith(BOM) ? result : BOM + result;
987
- fs3.writeFileSync(filePath, finalContent, { encoding: "utf-8" });
1302
+ fs4.writeFileSync(filePath, finalContent, { encoding: "utf-8" });
988
1303
  tui2.log.success(`\u2705 Replaced lines ${startLine}-${endLine} in ${filePath}`);
989
1304
  return true;
990
1305
  } catch (e) {
@@ -992,47 +1307,6 @@ function replaceLineRange(filePath, startLine, endLine, newContent, tui2) {
992
1307
  return false;
993
1308
  }
994
1309
  }
995
- async function generateFilePreview(filePath, startLine, endLine, newContent) {
996
- try {
997
- const fullPath = path4.resolve(process.cwd(), filePath);
998
- if (!fs3.existsSync(fullPath)) return `Error: File ${filePath} does not exist.`;
999
- const content = fs3.readFileSync(fullPath, "utf-8");
1000
- const lines = content.split(/\r\n|\r|\n/);
1001
- const startIdx = startLine - 1;
1002
- const endIdx = endLine - 1;
1003
- if (startIdx < 0 || startIdx >= lines.length) return `Error: Start line ${startLine} is out of bounds (File has ${lines.length} lines).`;
1004
- if (endIdx < startIdx || endIdx >= lines.length) return `Error: End line ${endLine} is invalid.`;
1005
- const contextBefore = lines.slice(Math.max(0, startIdx - 3), startIdx).map((l, i) => `${Math.max(1, startLine - 3) + i} | ${l}`).join("\n");
1006
- const contentReplacing = lines.slice(startIdx, endIdx + 1).map((l, i) => `${startLine + i} | - ${l}`).join("\n");
1007
- const contextAfter = lines.slice(endIdx + 1, Math.min(lines.length, endIdx + 4)).map((l, i) => `${endLine + 1 + i} | ${l}`).join("\n");
1008
- const newLines = newContent.split("\n").map((l) => `+ ${l}`).join("\n");
1009
- let reviewFeedback = "";
1010
- try {
1011
- reviewFeedback = await CodeReviewService.reviewCode(filePath, newContent);
1012
- } catch (err) {
1013
- reviewFeedback = `\u26A0\uFE0F Code Review Service Unavailable: ${err.message}`;
1014
- }
1015
- return `PREVIEW OF CHANGES to ${filePath}:
1016
- --------------------------------------------------
1017
- CONTEXT BEFORE:
1018
- ${contextBefore}
1019
-
1020
- CHANGES:
1021
- ${contentReplacing}
1022
- ${newLines}
1023
-
1024
- CONTEXT AFTER:
1025
- ${contextAfter}
1026
- --------------------------------------------------
1027
- ${reviewFeedback}
1028
-
1029
- IMPORTANT: Please verify that the lines being replaced (marked with -) are exactly what you intend to remove.
1030
- If the context looks wrong, DO NOT CONFIRM. Re-read the file to check line numbers.
1031
- `;
1032
- } catch (e) {
1033
- return `Error generating preview: ${e.message}`;
1034
- }
1035
- }
1036
1310
  function handleSearchFile(pattern) {
1037
1311
  try {
1038
1312
  const entries = fg.sync(pattern, { dot: true });
@@ -1043,11 +1317,11 @@ function handleSearchFile(pattern) {
1043
1317
  }
1044
1318
  }
1045
1319
  function startSmartReplace(filePath, newContent, targetContent, tui2) {
1046
- if (!fs3.existsSync(filePath)) {
1320
+ if (!fs4.existsSync(filePath)) {
1047
1321
  tui2.log.error(`\u274C File not found for modification: ${filePath}`);
1048
1322
  return false;
1049
1323
  }
1050
- const currentFileContent = fs3.readFileSync(filePath, "utf-8");
1324
+ const currentFileContent = fs4.readFileSync(filePath, "utf-8");
1051
1325
  const normalizedTarget = targetContent.replace(/\r\n/g, "\n");
1052
1326
  const normalizedContent = currentFileContent.replace(/\r\n/g, "\n");
1053
1327
  if (!normalizedContent.includes(normalizedTarget)) {
@@ -1064,7 +1338,7 @@ function startSmartReplace(filePath, newContent, targetContent, tui2) {
1064
1338
  const BOM = "\uFEFF";
1065
1339
  const updatedContent = currentFileContent.replace(targetContent, newContent);
1066
1340
  const finalContent = updatedContent.startsWith(BOM) ? updatedContent : BOM + updatedContent;
1067
- fs3.writeFileSync(filePath, finalContent, { encoding: "utf-8" });
1341
+ fs4.writeFileSync(filePath, finalContent, { encoding: "utf-8" });
1068
1342
  tui2.log.success(`\u2705 Smart Replace Applied: ${filePath}`);
1069
1343
  return true;
1070
1344
  }
@@ -1072,7 +1346,7 @@ async function handleRunCommand(command) {
1072
1346
  const { spawn } = await import("child_process");
1073
1347
  try {
1074
1348
  tui.log.info(`\u{1F4BB} Executing: ${colors.dim(command)}`);
1075
- return new Promise((resolve) => {
1349
+ return new Promise((resolve2) => {
1076
1350
  const child = spawn(command, {
1077
1351
  shell: true,
1078
1352
  stdio: ["ignore", "pipe", "pipe"],
@@ -1082,7 +1356,7 @@ async function handleRunCommand(command) {
1082
1356
  let stderr = "";
1083
1357
  const timer = setTimeout(() => {
1084
1358
  child.kill();
1085
- resolve(`Error: Command timed out after 5 minutes.
1359
+ resolve2(`Error: Command timed out after 5 minutes.
1086
1360
  Output so far:
1087
1361
  ${stdout}
1088
1362
  ${stderr}`);
@@ -1097,9 +1371,9 @@ ${stderr}`);
1097
1371
  child.on("close", (code) => {
1098
1372
  clearTimeout(timer);
1099
1373
  if (code === 0) {
1100
- resolve(stdout.trim() || "Command executed successfully (no output).");
1374
+ resolve2(stdout.trim() || "Command executed successfully (no output).");
1101
1375
  } else {
1102
- resolve(`Command failed with exit code ${code}.
1376
+ resolve2(`Command failed with exit code ${code}.
1103
1377
  STDERR:
1104
1378
  ${stderr}
1105
1379
  STDOUT:
@@ -1108,7 +1382,7 @@ ${stdout}`);
1108
1382
  });
1109
1383
  child.on("error", (err) => {
1110
1384
  clearTimeout(timer);
1111
- resolve(`Error executing command: ${err.message}`);
1385
+ resolve2(`Error executing command: ${err.message}`);
1112
1386
  });
1113
1387
  });
1114
1388
  } catch (e) {
@@ -1120,20 +1394,20 @@ function resolveAstGrepCommand() {
1120
1394
  const binName = isWin ? "sg.cmd" : "sg";
1121
1395
  try {
1122
1396
  const currentFile = fileURLToPath(import.meta.url);
1123
- let dir = path4.dirname(currentFile);
1397
+ let dir = path5.dirname(currentFile);
1124
1398
  for (let i = 0; i < 5; i++) {
1125
- const candidate = path4.join(dir, "node_modules", ".bin", binName);
1126
- if (fs3.existsSync(candidate)) {
1399
+ const candidate = path5.join(dir, "node_modules", ".bin", binName);
1400
+ if (fs4.existsSync(candidate)) {
1127
1401
  return `"${candidate}"`;
1128
1402
  }
1129
- const parent = path4.dirname(dir);
1403
+ const parent = path5.dirname(dir);
1130
1404
  if (parent === dir) break;
1131
1405
  dir = parent;
1132
1406
  }
1133
1407
  } catch (e) {
1134
1408
  }
1135
- const cwdBin = path4.resolve(process.cwd(), "node_modules", ".bin", binName);
1136
- if (fs3.existsSync(cwdBin)) {
1409
+ const cwdBin = path5.resolve(process.cwd(), "node_modules", ".bin", binName);
1410
+ if (fs4.existsSync(cwdBin)) {
1137
1411
  return `"${cwdBin}"`;
1138
1412
  }
1139
1413
  return "npx sg";
@@ -1141,13 +1415,13 @@ function resolveAstGrepCommand() {
1141
1415
  async function astGrepSearch(pattern, filePath, language, tui2) {
1142
1416
  const { spawn } = await import("child_process");
1143
1417
  try {
1144
- if (!fs3.existsSync(filePath)) {
1418
+ if (!fs4.existsSync(filePath)) {
1145
1419
  return `\u274C File not found: ${filePath}`;
1146
1420
  }
1147
1421
  const sgCmd = resolveAstGrepCommand();
1148
1422
  const cmd = `${sgCmd} run -p "${pattern}" -l ${language} --json ${filePath}`;
1149
1423
  tui2.log.info(`\u{1F50D} [AST-GREP] Searching: ${cmd}`);
1150
- return new Promise((resolve) => {
1424
+ return new Promise((resolve2) => {
1151
1425
  const child = spawn(cmd, {
1152
1426
  shell: true,
1153
1427
  stdio: ["ignore", "pipe", "pipe"],
@@ -1161,19 +1435,19 @@ async function astGrepSearch(pattern, filePath, language, tui2) {
1161
1435
  child.stderr.on("data", (data) => stderr += data.toString());
1162
1436
  child.on("close", (code) => {
1163
1437
  if (code === 0 && stdout) {
1164
- resolve(stdout);
1438
+ resolve2(stdout);
1165
1439
  } else if (code === 1 && !stderr) {
1166
- resolve("No structural matches found.");
1440
+ resolve2("No structural matches found.");
1167
1441
  } else {
1168
- if (!stdout && !stderr) resolve("No structural matches found.");
1442
+ if (!stdout && !stderr) resolve2("No structural matches found.");
1169
1443
  else {
1170
1444
  tui2.log.error(`\u274C ast-grep search error (code ${code}): ${stderr}`);
1171
- resolve(`Error executing ast-grep search: ${stderr || stdout}`);
1445
+ resolve2(`Error executing ast-grep search: ${stderr || stdout}`);
1172
1446
  }
1173
1447
  }
1174
1448
  });
1175
1449
  child.on("error", (err) => {
1176
- resolve(`Error executing ast-grep search: ${err.message}`);
1450
+ resolve2(`Error executing ast-grep search: ${err.message}`);
1177
1451
  });
1178
1452
  });
1179
1453
  } catch (e) {
@@ -1184,14 +1458,14 @@ async function astGrepSearch(pattern, filePath, language, tui2) {
1184
1458
  async function astGrepRewrite(pattern, fix, filePath, language, tui2) {
1185
1459
  const { spawn } = await import("child_process");
1186
1460
  try {
1187
- if (!fs3.existsSync(filePath)) {
1461
+ if (!fs4.existsSync(filePath)) {
1188
1462
  tui2.log.error(`\u274C File not found for AST modification: ${filePath}`);
1189
1463
  return false;
1190
1464
  }
1191
1465
  const sgCmd = resolveAstGrepCommand();
1192
1466
  const cmd = `${sgCmd} run -p "${pattern}" -r "${fix}" -l ${language} ${filePath} --update-all`;
1193
1467
  tui2.log.info(`\u270F\uFE0F [AST-GREP] Rewriting: pattern="${pattern}" fix="${fix.substring(0, 50)}..."`);
1194
- return new Promise((resolve) => {
1468
+ return new Promise((resolve2) => {
1195
1469
  const child = spawn(cmd, {
1196
1470
  shell: true,
1197
1471
  stdio: ["ignore", "pipe", "pipe"],
@@ -1202,15 +1476,15 @@ async function astGrepRewrite(pattern, fix, filePath, language, tui2) {
1202
1476
  child.on("close", (code) => {
1203
1477
  if (code === 0) {
1204
1478
  tui2.log.success(`\u2705 AST Rewrite applied to ${filePath}`);
1205
- resolve(true);
1479
+ resolve2(true);
1206
1480
  } else {
1207
1481
  tui2.log.error(`\u274C AST Rewrite failed (code ${code}): ${stderr}`);
1208
- resolve(false);
1482
+ resolve2(false);
1209
1483
  }
1210
1484
  });
1211
1485
  child.on("error", (err) => {
1212
1486
  tui2.log.error(`\u274C AST Rewrite spawn error: ${err.message}`);
1213
- resolve(false);
1487
+ resolve2(false);
1214
1488
  });
1215
1489
  });
1216
1490
  } catch (e) {
@@ -1218,6 +1492,173 @@ async function astGrepRewrite(pattern, fix, filePath, language, tui2) {
1218
1492
  return false;
1219
1493
  }
1220
1494
  }
1495
+ async function astListStructure(filePath) {
1496
+ try {
1497
+ const editor = CodeEditorFactory.getEditor(filePath);
1498
+ if (!editor) {
1499
+ return `[AST Error] File type not supported: ${filePath}. Use read_file instead.`;
1500
+ }
1501
+ const structure = await editor.listStructure(filePath);
1502
+ let output = `[AST Structure of ${filePath}]
1503
+
1504
+ `;
1505
+ if (structure.classes.length > 0) {
1506
+ output += `CLASSES:
1507
+ `;
1508
+ structure.classes.forEach((cls) => {
1509
+ output += ` - ${cls.name}`;
1510
+ if (cls.extendsClass) output += ` extends ${cls.extendsClass}`;
1511
+ if (cls.implementsInterfaces.length > 0) {
1512
+ output += ` implements ${cls.implementsInterfaces.join(", ")}`;
1513
+ }
1514
+ output += `
1515
+ `;
1516
+ if (cls.decorators.length > 0) {
1517
+ output += ` Decorators: ${cls.decorators.join(", ")}
1518
+ `;
1519
+ }
1520
+ if (cls.properties.length > 0) {
1521
+ output += ` Properties:
1522
+ `;
1523
+ cls.properties.forEach((prop) => {
1524
+ output += ` - ${prop.visibility} ${prop.name}: ${prop.type}
1525
+ `;
1526
+ });
1527
+ }
1528
+ if (cls.methods.length > 0) {
1529
+ output += ` Methods:
1530
+ `;
1531
+ cls.methods.forEach((method) => {
1532
+ const params = method.parameters.map((p) => `${p.name}: ${p.type}`).join(", ");
1533
+ output += ` - ${method.visibility} ${method.name}(${params}): ${method.returnType}
1534
+ `;
1535
+ });
1536
+ }
1537
+ output += `
1538
+ `;
1539
+ });
1540
+ }
1541
+ if (structure.interfaces.length > 0) {
1542
+ output += `INTERFACES:
1543
+ `;
1544
+ structure.interfaces.forEach((iface) => {
1545
+ output += ` - ${iface.name}
1546
+ `;
1547
+ iface.properties.forEach((prop) => {
1548
+ output += ` ${prop.name}: ${prop.type}
1549
+ `;
1550
+ });
1551
+ });
1552
+ output += `
1553
+ `;
1554
+ }
1555
+ if (structure.functions.length > 0) {
1556
+ output += `FUNCTIONS:
1557
+ `;
1558
+ structure.functions.forEach((fn) => {
1559
+ const params = fn.parameters.map((p) => `${p.name}: ${p.type}`).join(", ");
1560
+ output += ` - ${fn.name}(${params}): ${fn.returnType}
1561
+ `;
1562
+ });
1563
+ output += `
1564
+ `;
1565
+ }
1566
+ if (structure.imports.length > 0) {
1567
+ output += `IMPORTS:
1568
+ `;
1569
+ structure.imports.forEach((imp) => {
1570
+ output += ` - from "${imp.modulePath}": ${imp.namedImports.join(", ")}
1571
+ `;
1572
+ });
1573
+ }
1574
+ return output;
1575
+ } catch (error) {
1576
+ return `[AST Error] ${error.message}`;
1577
+ }
1578
+ }
1579
+ async function astAddMethod(filePath, className, methodCode) {
1580
+ const editor = CodeEditorFactory.getEditor(filePath);
1581
+ if (!editor) {
1582
+ throw new Error(`No AST editor available for ${filePath}`);
1583
+ }
1584
+ return await editor.addMethod(filePath, className, methodCode);
1585
+ }
1586
+ async function astGetMethod(filePath, className, methodName) {
1587
+ const editor = CodeEditorFactory.getEditor(filePath);
1588
+ if (!editor) {
1589
+ throw new Error(`No AST editor available for ${filePath}`);
1590
+ }
1591
+ const methodContent = await editor.getMethod(filePath, className, methodName);
1592
+ if (!methodContent) {
1593
+ return `Method "${methodName}" not found in class "${className}"`;
1594
+ }
1595
+ return methodContent;
1596
+ }
1597
+ async function astAddClass(filePath, className, extendsClass, implementsInterfaces) {
1598
+ const editor = CodeEditorFactory.getEditor(filePath);
1599
+ if (!editor) throw new Error(`AST editing not supported for: ${filePath}`);
1600
+ return await editor.addClass(filePath, className, { extendsClass, implementsInterfaces });
1601
+ }
1602
+ async function astAddProperty(filePath, className, propertyCode) {
1603
+ const editor = CodeEditorFactory.getEditor(filePath);
1604
+ if (!editor) throw new Error(`AST editing not supported for: ${filePath}`);
1605
+ return await editor.addProperty(filePath, className, propertyCode);
1606
+ }
1607
+ async function astRemoveProperty(filePath, className, propertyName) {
1608
+ const editor = CodeEditorFactory.getEditor(filePath);
1609
+ if (!editor) throw new Error(`AST editing not supported for: ${filePath}`);
1610
+ return await editor.removeProperty(filePath, className, propertyName);
1611
+ }
1612
+ async function astModifyMethod(filePath, className, methodName, newBody) {
1613
+ const editor = CodeEditorFactory.getEditor(filePath);
1614
+ if (!editor) throw new Error(`AST editing not supported for: ${filePath}`);
1615
+ return await editor.modifyMethod(filePath, className, methodName, newBody);
1616
+ }
1617
+ async function astRemoveMethod(filePath, className, methodName) {
1618
+ const editor = CodeEditorFactory.getEditor(filePath);
1619
+ if (!editor) throw new Error(`AST editing not supported for: ${filePath}`);
1620
+ return await editor.removeMethod(filePath, className, methodName);
1621
+ }
1622
+ async function astAddDecorator(filePath, className, decoratorCode) {
1623
+ const editor = CodeEditorFactory.getEditor(filePath);
1624
+ if (!editor) throw new Error(`AST editing not supported for: ${filePath}`);
1625
+ return await editor.addDecorator(filePath, className, decoratorCode);
1626
+ }
1627
+ async function astAddInterface(filePath, interfaceCode) {
1628
+ const editor = CodeEditorFactory.getEditor(filePath);
1629
+ if (!editor) throw new Error(`AST editing not supported for: ${filePath}`);
1630
+ return await editor.addInterface(filePath, interfaceCode);
1631
+ }
1632
+ async function astAddTypeAlias(filePath, typeCode) {
1633
+ const editor = CodeEditorFactory.getEditor(filePath);
1634
+ if (!editor) throw new Error(`AST editing not supported for: ${filePath}`);
1635
+ return await editor.addTypeAlias(filePath, typeCode);
1636
+ }
1637
+ async function astAddFunction(filePath, functionCode) {
1638
+ const editor = CodeEditorFactory.getEditor(filePath);
1639
+ if (!editor) throw new Error(`AST editing not supported for: ${filePath}`);
1640
+ return await editor.addFunction(filePath, functionCode);
1641
+ }
1642
+ async function astRemoveFunction(filePath, functionName) {
1643
+ const editor = CodeEditorFactory.getEditor(filePath);
1644
+ if (!editor) throw new Error(`AST editing not supported for: ${filePath}`);
1645
+ return await editor.removeFunction(filePath, functionName);
1646
+ }
1647
+ async function astAddImport(filePath, importStatement) {
1648
+ const editor = CodeEditorFactory.getEditor(filePath);
1649
+ if (!editor) throw new Error(`AST editing not supported for: ${filePath}`);
1650
+ return await editor.addImport(filePath, importStatement);
1651
+ }
1652
+ async function astRemoveImport(filePath, modulePath) {
1653
+ const editor = CodeEditorFactory.getEditor(filePath);
1654
+ if (!editor) throw new Error(`AST editing not supported for: ${filePath}`);
1655
+ return await editor.removeImport(filePath, modulePath);
1656
+ }
1657
+ async function astOrganizeImports(filePath) {
1658
+ const editor = CodeEditorFactory.getEditor(filePath);
1659
+ if (!editor) throw new Error(`AST editing not supported for: ${filePath}`);
1660
+ return await editor.organizeImports(filePath);
1661
+ }
1221
1662
 
1222
1663
  // src/core/agents/specification-agent.ts
1223
1664
  var AGENT_TYPE2 = "specification_agent";
@@ -1232,29 +1673,39 @@ async function interactiveSpecificationAgent(options = {}) {
1232
1673
  tui.intro("\u{1F3D7}\uFE0F Specification Agent");
1233
1674
  const projectRoot = process.cwd();
1234
1675
  let contextContent = "";
1235
- const contextPath = path5.resolve(projectRoot, "_sharkrc", "project-context.md");
1236
- if (fs4.existsSync(contextPath)) {
1676
+ const contextPath = path6.resolve(projectRoot, "_sharkrc", "project-context.md");
1677
+ if (fs5.existsSync(contextPath)) {
1237
1678
  try {
1238
- contextContent = fs4.readFileSync(contextPath, "utf-8");
1239
- tui.log.info(`\u{1F4D8} Context loaded from: ${colors.dim(path5.relative(projectRoot, contextPath))}`);
1679
+ contextContent = fs5.readFileSync(contextPath, "utf-8");
1680
+ tui.log.info(`\u{1F4D8} Context loaded from: ${colors.dim(path6.relative(projectRoot, contextPath))}`);
1240
1681
  } catch (e) {
1241
1682
  tui.log.warning(`Failed to read context file: ${e}`);
1242
1683
  }
1243
1684
  }
1244
1685
  let briefingContent = "";
1245
- if (options.briefingPath && fs4.existsSync(options.briefingPath)) {
1246
- briefingContent = fs4.readFileSync(options.briefingPath, "utf-8");
1686
+ if (options.briefingPath && fs5.existsSync(options.briefingPath)) {
1687
+ briefingContent = fs5.readFileSync(options.briefingPath, "utf-8");
1247
1688
  tui.log.info(`\u{1F4C4} Briefing loaded from: ${colors.dim(options.briefingPath)}`);
1248
1689
  } else {
1249
- const sharkRcBriefing = path5.resolve(projectRoot, "_sharkrc", "briefing.md");
1250
- if (fs4.existsSync(sharkRcBriefing)) {
1251
- briefingContent = fs4.readFileSync(sharkRcBriefing, "utf-8");
1690
+ const sharkRcBriefing = path6.resolve(projectRoot, "_sharkrc", "briefing.md");
1691
+ if (fs5.existsSync(sharkRcBriefing)) {
1692
+ briefingContent = fs5.readFileSync(sharkRcBriefing, "utf-8");
1252
1693
  tui.log.info(`\u{1F4C4} Standard Briefing loaded: ${colors.dim("_sharkrc/briefing.md")}`);
1253
1694
  } else {
1254
1695
  tui.log.info(colors.dim("\u2139\uFE0F No briefing file found in _sharkrc/briefing.md. Starting in interactive mode."));
1255
1696
  }
1256
1697
  }
1257
1698
  let initialPrompt = "";
1699
+ if (options.initialContext) {
1700
+ initialPrompt += `
1701
+ \u26A0\uFE0F **CONTEXTO DE EXECU\xC7\xC3O ANTERIOR (HANDOVER)**:
1702
+ O Developer Agent estava executando tarefas. Aqui est\xE1 o resumo do que aconteceu at\xE9 agora:
1703
+ """
1704
+ ${options.initialContext}
1705
+ """
1706
+ Analise esse hist\xF3rico acima. Se houve falha, proponha fixes na spec. Se o usu\xE1rio pediu mudan\xE7a, use isso como base.
1707
+ `;
1708
+ }
1258
1709
  if (briefingContent) {
1259
1710
  initialPrompt += `
1260
1711
  Abaixo est\xE1 o **Briefing de Neg\xF3cio** ou Descri\xE7\xE3o da Tarefa.
@@ -1363,9 +1814,9 @@ ${result}
1363
1814
  const BOM = "\uFEFF";
1364
1815
  const contentToWrite = action.content || "";
1365
1816
  const finalContent = contentToWrite.startsWith(BOM) ? contentToWrite : BOM + contentToWrite;
1366
- const dir = path5.dirname(action.path);
1367
- if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
1368
- fs4.writeFileSync(action.path, finalContent, { encoding: "utf-8" });
1817
+ const dir = path6.dirname(action.path);
1818
+ if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
1819
+ fs5.writeFileSync(action.path, finalContent, { encoding: "utf-8" });
1369
1820
  tui.log.success(`\u2705 Created: ${action.path}`);
1370
1821
  executionResults += `[Action create_file(${action.path})]: Success
1371
1822
 
@@ -1383,7 +1834,7 @@ ${result}
1383
1834
  `;
1384
1835
  }
1385
1836
  } else if (action.type === "delete_file") {
1386
- fs4.unlinkSync(action.path);
1837
+ fs5.unlinkSync(action.path);
1387
1838
  tui.log.success(`\u2705 Deleted: ${action.path}`);
1388
1839
  executionResults += `[Action delete_file(${action.path})]: Success
1389
1840
 
@@ -1512,8 +1963,8 @@ async function callSpecAgentApi(prompt, onChunk, agentId) {
1512
1963
  import { Command as Command2 } from "commander";
1513
1964
 
1514
1965
  // src/core/agents/scan-agent.ts
1515
- import fs5 from "fs";
1516
- import path6 from "path";
1966
+ import fs6 from "fs";
1967
+ import path7 from "path";
1517
1968
  var AGENT_TYPE3 = "scan_agent";
1518
1969
  function getAgentId3() {
1519
1970
  const config = ConfigManager.getInstance().getConfig();
@@ -1528,29 +1979,29 @@ async function interactiveScanAgent(options = {}) {
1528
1979
  const projectRoot = process.cwd();
1529
1980
  let outputFile;
1530
1981
  if (options.output) {
1531
- outputFile = path6.resolve(process.cwd(), options.output);
1982
+ outputFile = path7.resolve(process.cwd(), options.output);
1532
1983
  } else {
1533
- const outputDir = path6.resolve(projectRoot, "_sharkrc");
1534
- if (!fs5.existsSync(outputDir)) {
1535
- const stat = fs5.existsSync(outputDir) ? fs5.statSync(outputDir) : null;
1984
+ const outputDir = path7.resolve(projectRoot, "_sharkrc");
1985
+ if (!fs6.existsSync(outputDir)) {
1986
+ const stat = fs6.existsSync(outputDir) ? fs6.statSync(outputDir) : null;
1536
1987
  if (stat && stat.isFile()) {
1537
1988
  tui.log.warning(`Warning: '_sharkrc' exists as a file. Using '_bmad/project-context' instead to avoid overwrite.`);
1538
- const fallbackDir = path6.resolve(projectRoot, "_bmad/project-context");
1539
- if (!fs5.existsSync(fallbackDir)) fs5.mkdirSync(fallbackDir, { recursive: true });
1540
- outputFile = path6.join(fallbackDir, "project-context.md");
1989
+ const fallbackDir = path7.resolve(projectRoot, "_bmad/project-context");
1990
+ if (!fs6.existsSync(fallbackDir)) fs6.mkdirSync(fallbackDir, { recursive: true });
1991
+ outputFile = path7.join(fallbackDir, "project-context.md");
1541
1992
  } else {
1542
- fs5.mkdirSync(outputDir, { recursive: true });
1543
- outputFile = path6.join(outputDir, "project-context.md");
1993
+ fs6.mkdirSync(outputDir, { recursive: true });
1994
+ outputFile = path7.join(outputDir, "project-context.md");
1544
1995
  }
1545
1996
  } else {
1546
- fs5.mkdirSync(outputDir, { recursive: true });
1547
- outputFile = path6.join(outputDir, "project-context.md");
1997
+ fs6.mkdirSync(outputDir, { recursive: true });
1998
+ outputFile = path7.join(outputDir, "project-context.md");
1548
1999
  }
1549
2000
  }
1550
2001
  tui.log.info(`${t("commands.scan.scanningProject")} ${colors.bold(projectRoot)}`);
1551
2002
  tui.log.info(`${t("commands.scan.outputTarget")} ${colors.bold(outputFile)}`);
1552
2003
  tui.log.info(`${t("commands.scan.language")} ${colors.bold(language)}`);
1553
- const configFileRelative = path6.relative(projectRoot, outputFile);
2004
+ const configFileRelative = path7.relative(projectRoot, outputFile);
1554
2005
  const initialTemplate = `# Project Context
1555
2006
 
1556
2007
  ## Overview
@@ -1583,9 +2034,9 @@ async function interactiveScanAgent(options = {}) {
1583
2034
  ## Key Patterns & Conventions
1584
2035
  [TO BE ANALYZED]
1585
2036
  `;
1586
- if (!fs5.existsSync(outputFile)) {
2037
+ if (!fs6.existsSync(outputFile)) {
1587
2038
  const BOM = "\uFEFF";
1588
- fs5.writeFileSync(outputFile, BOM + initialTemplate, { encoding: "utf-8" });
2039
+ fs6.writeFileSync(outputFile, BOM + initialTemplate, { encoding: "utf-8" });
1589
2040
  tui.log.success(`${t("commands.scan.templateCreated")} ${outputFile}`);
1590
2041
  } else {
1591
2042
  tui.log.info(t("commands.scan.fileExists"));
@@ -1883,11 +2334,11 @@ ${result}
1883
2334
 
1884
2335
  `;
1885
2336
  } else if (action.type === "create_file" || action.type === "modify_file") {
1886
- const resolvedActionPath = path6.resolve(action.path || "");
1887
- const resolvedTargetPath = path6.resolve(targetPath);
2337
+ const resolvedActionPath = path7.resolve(action.path || "");
2338
+ const resolvedTargetPath = path7.resolve(targetPath);
1888
2339
  let isTarget = resolvedActionPath === resolvedTargetPath;
1889
- if (!isTarget && path6.basename(action.path || "") === "project-context.md") {
1890
- tui.log.warning(t("commands.scan.targetRedirect").replace("{0}", action.path || "").replace("{1}", path6.relative(process.cwd(), targetPath)));
2340
+ if (!isTarget && path7.basename(action.path || "") === "project-context.md") {
2341
+ tui.log.warning(t("commands.scan.targetRedirect").replace("{0}", action.path || "").replace("{1}", path7.relative(process.cwd(), targetPath)));
1891
2342
  isTarget = true;
1892
2343
  action.path = targetPath;
1893
2344
  }
@@ -1897,16 +2348,16 @@ ${result}
1897
2348
  if (action.type === "create_file") {
1898
2349
  const contentToWrite = action.content || "";
1899
2350
  const finalContent = contentToWrite.startsWith(BOM) ? contentToWrite : BOM + contentToWrite;
1900
- fs5.writeFileSync(finalPath, finalContent, { encoding: "utf-8" });
2351
+ fs6.writeFileSync(finalPath, finalContent, { encoding: "utf-8" });
1901
2352
  tui.log.success(t("commands.scan.generated").replace("{0}", finalPath));
1902
2353
  fileCreated = true;
1903
2354
  } else {
1904
- if (fs5.existsSync(finalPath)) {
1905
- const currentContent = fs5.readFileSync(finalPath, "utf-8");
2355
+ if (fs6.existsSync(finalPath)) {
2356
+ const currentContent = fs6.readFileSync(finalPath, "utf-8");
1906
2357
  if (action.target_content && currentContent.includes(action.target_content)) {
1907
2358
  const newContent = currentContent.replace(action.target_content, action.content || "");
1908
2359
  const finalContent = newContent.startsWith(BOM) ? newContent : BOM + newContent;
1909
- fs5.writeFileSync(finalPath, finalContent, { encoding: "utf-8" });
2360
+ fs6.writeFileSync(finalPath, finalContent, { encoding: "utf-8" });
1910
2361
  tui.log.success(t("commands.scan.updated").replace("{0}", finalPath));
1911
2362
  fileCreated = true;
1912
2363
  } else {
@@ -1935,7 +2386,7 @@ ${result}
1935
2386
  }
1936
2387
  }
1937
2388
  if (fileCreated) {
1938
- const currentContent = fs5.readFileSync(targetPath, "utf-8");
2389
+ const currentContent = fs6.readFileSync(targetPath, "utf-8");
1939
2390
  const pendingSections = [];
1940
2391
  const lines = currentContent.split("\n");
1941
2392
  let currentSection = "";
@@ -2027,8 +2478,8 @@ var scanCommand = new Command2("scan").description("Analyze the project and gene
2027
2478
  import { Command as Command3 } from "commander";
2028
2479
 
2029
2480
  // src/core/agents/developer-agent.ts
2030
- import fs6 from "fs";
2031
- import path7 from "path";
2481
+ import fs7 from "fs";
2482
+ import path8 from "path";
2032
2483
  var AGENT_TYPE4 = "developer_agent";
2033
2484
  async function validateTypeScript(filePath) {
2034
2485
  try {
@@ -2041,32 +2492,6 @@ async function validateTypeScript(filePath) {
2041
2492
  return { valid: false, error: e.message || "TypeScript validation failed" };
2042
2493
  }
2043
2494
  }
2044
- function validateHtmlTagBalance(filePath) {
2045
- const content = fs6.readFileSync(filePath, "utf-8");
2046
- const stack = [];
2047
- const tagRegex = /<\/?(\w+)(?:\s[^>]*)?\s*\/?>/g;
2048
- const selfClosingTags = ["br", "hr", "img", "input", "meta", "link", "area", "base", "col", "embed", "param", "source", "track", "wbr"];
2049
- let match;
2050
- while ((match = tagRegex.exec(content)) !== null) {
2051
- const fullTag = match[0];
2052
- const tagName = match[1].toLowerCase();
2053
- if (fullTag.startsWith("</")) {
2054
- const expected = stack.pop();
2055
- if (expected !== tagName) {
2056
- return {
2057
- valid: false,
2058
- error: `Tag mismatch: expected </${expected}> but found </${tagName}> at position ${match.index}`
2059
- };
2060
- }
2061
- } else if (!fullTag.endsWith("/>") && !selfClosingTags.includes(tagName)) {
2062
- stack.push(tagName);
2063
- }
2064
- }
2065
- if (stack.length > 0) {
2066
- return { valid: false, error: `Unclosed tags: <${stack.join(">, <")}>` };
2067
- }
2068
- return { valid: true };
2069
- }
2070
2495
  function getAgentId4(overrideId) {
2071
2496
  if (overrideId) return overrideId;
2072
2497
  const config = ConfigManager.getInstance().getConfig();
@@ -2074,19 +2499,25 @@ function getAgentId4(overrideId) {
2074
2499
  if (process.env.STACKSPOT_DEV_AGENT_ID) return process.env.STACKSPOT_DEV_AGENT_ID;
2075
2500
  return "01KEQCGJ65YENRA4QBXVN1YFFX";
2076
2501
  }
2077
- function analyzeSpecState(projectRoot) {
2078
- const specPath = path7.resolve(projectRoot, "tech-spec.md");
2079
- if (!fs6.existsSync(specPath)) {
2080
- return { status: "MISSING" };
2502
+ async function interactiveDeveloperAgent(options = {}) {
2503
+ FileLogger.init();
2504
+ const agentId = getAgentId4();
2505
+ if (agentId === "PENDING_CONFIGURATION") {
2506
+ tui.log.error("\u274C STACKSPOT_DEV_AGENT_ID not configured in .env");
2507
+ return { success: false, summary: "Missing configuration." };
2081
2508
  }
2082
- const content = fs6.readFileSync(specPath, "utf-8");
2083
- const match = content.match(/- \[ \] (.*)/);
2084
- if (match) {
2085
- return { status: "PENDING", nextTask: match[1].trim(), specContent: content };
2509
+ const projectRoot = process.cwd();
2510
+ let contextContent = "";
2511
+ const defaultContextPath = path8.resolve(projectRoot, "_sharkrc", "project-context.md");
2512
+ const specificContextPath = options.context ? path8.resolve(projectRoot, options.context) : defaultContextPath;
2513
+ if (fs7.existsSync(specificContextPath)) {
2514
+ try {
2515
+ contextContent = fs7.readFileSync(specificContextPath, "utf-8");
2516
+ } catch (e) {
2517
+ tui.log.warning(`Failed to read context file: ${e}`);
2518
+ }
2086
2519
  }
2087
- return { status: "COMPLETED", specContent: content };
2088
- }
2089
- function buildSystemPrompt(state, contextContent, additionalInstructions = "") {
2520
+ const currentTask = options.taskInstruction || "Analyze the project and fix pending issues.";
2090
2521
  let basePrompt = ``;
2091
2522
  if (contextContent) {
2092
2523
  basePrompt += `
@@ -2096,117 +2527,69 @@ ${contextContent}
2096
2527
  -----------------------
2097
2528
  `;
2098
2529
  }
2099
- if (state.status === "MISSING") {
2530
+ if (options.history) {
2100
2531
  basePrompt += `
2101
2532
 
2102
- \u{1F6A8} CRITICAL: NO 'tech-spec.md' FOUND.
2103
-
2104
- Your FIRST priority is to analyze the user request and CREATE a 'tech-spec.md' file.
2105
-
2106
- \u26A0\uFE0F WORKFLOW:
2107
- 1. **Understand**: Clarify the goal with the user if needed.
2108
- 2. **Explore**: Use 'list_files'/'read_file' to find RELEVANT files for this specific task.
2109
- 3. **Specify**: Create 'tech-spec.md' referencing REAL file paths you found.
2110
-
2111
- DO NOT create a spec based on guesses. Verify file existence before writing the plan.
2112
-
2113
- Structure for 'tech-spec.md':
2114
- \`\`\`markdown
2115
- # Technical Spec: [Title]
2116
-
2117
- ## Goal
2118
- [Brief description]
2119
-
2120
- ## Implementation Plan
2121
- - [ ] Step 1: [Description]
2122
- - [ ] Step 2: [Description]
2123
- ...
2124
- \`\`\`
2125
- User Request: "${additionalInstructions}"
2533
+ --- PREVIOUS EXECUTION SUMMARY ---
2534
+ ${options.history}
2535
+ ----------------------------------
2126
2536
  `;
2127
- } else if (state.status === "PENDING") {
2128
- basePrompt += `
2537
+ }
2538
+ basePrompt += `
2129
2539
 
2130
2540
  \u{1F7E2} EXECUTION MODE
2131
2541
 
2132
- Use 'tech-spec.md' as your source of truth.
2133
-
2134
- \u{1F449} **CURRENT TASK**: "${state.nextTask}"
2135
-
2542
+ You are a highly skilled Developer Agent.
2543
+ \u{1F449} **CURRENT TASK**: "${currentTask}"
2136
2544
 
2137
- Focus ONLY on this task. Do not jump ahead.
2545
+ Your goal is to COMPLETE this specific task and then STOP.
2138
2546
  1. Implement the necessary changes.
2139
2547
  2. Verify (compile/test).
2140
- 3. **MANDATORY**: Use 'modify_file' to mark this task as '[x]' in 'tech-spec.md' when done.
2548
+ 3. **MANDATORY**: When you are confident the task is done, output a final message starting with "TASK_COMPLETED:" followed by a brief technical summary of what you did.
2141
2549
  `;
2142
- } else {
2143
- basePrompt += `
2144
-
2145
- \u2728 ALL TASKS COMPLETED according to 'tech-spec.md'.
2146
-
2147
- Ask the user if they want to add more tasks or finish the session.
2148
- `;
2149
- }
2150
- return basePrompt;
2151
- }
2152
- async function interactiveDeveloperAgent(options = {}) {
2153
- FileLogger.init();
2154
- tui.intro("\u{1F988} Shark Dev Agent (Spec-Driven)");
2155
- const agentId = getAgentId4();
2156
- if (agentId === "PENDING_CONFIGURATION") {
2157
- tui.log.error("\u274C STACKSPOT_DEV_AGENT_ID not configured in .env");
2158
- return;
2159
- }
2160
- const projectRoot = process.cwd();
2161
- let contextContent = "";
2162
- const defaultContextPath = path7.resolve(projectRoot, "_sharkrc", "project-context.md");
2163
- const specificContextPath = options.context ? path7.resolve(projectRoot, options.context) : defaultContextPath;
2164
- if (fs6.existsSync(specificContextPath)) {
2165
- try {
2166
- contextContent = fs6.readFileSync(specificContextPath, "utf-8");
2167
- tui.log.info(`\u{1F4D8} Context loaded from: ${colors.dim(path7.relative(projectRoot, specificContextPath))}`);
2168
- } catch (e) {
2169
- tui.log.warning(`Failed to read context file: ${e}`);
2170
- }
2171
- } else {
2172
- tui.log.warning(`\u26A0\uFE0F No context file found. Agent will run without pre-loaded context.`);
2173
- }
2174
- let specState = analyzeSpecState(projectRoot);
2175
- let nextPrompt = buildSystemPrompt(specState, contextContent, options.task || "Start working.");
2550
+ let nextPrompt = basePrompt;
2176
2551
  let keepGoing = true;
2177
2552
  const spinner = tui.spinner();
2178
- let stepCount = 0;
2553
+ let finalSummary = "";
2554
+ let isTaskCompleted = false;
2555
+ const conversationKey = options.taskId ? `dev_agent_${options.taskId}` : `dev_agent_${Date.now()}`;
2179
2556
  let autoApprovals = {
2180
2557
  files: false,
2181
2558
  commands: false
2182
2559
  };
2183
2560
  while (keepGoing) {
2184
- stepCount++;
2185
2561
  try {
2186
- if (specState.status === "PENDING") {
2187
- tui.log.info(colors.bold(`\u{1F3AF} DOING: ${specState.nextTask}`));
2188
- } else if (specState.status === "MISSING") {
2189
- tui.log.info(colors.warning(`\u{1F4CB} PLANNING: Creating tech-spec.md`));
2190
- }
2191
- spinner.start("Waiting for Shark Dev...");
2192
- let lastResponse = null;
2193
- await callDevAgentApi(nextPrompt, (chunk) => {
2194
- }).then((resp) => {
2195
- lastResponse = resp;
2196
- });
2562
+ spinner.start("\u{1F988} Shark Dev working...");
2563
+ const lastResponse = await callDevAgentApi(nextPrompt, (chunk) => {
2564
+ }, conversationKey);
2197
2565
  spinner.stop("Response received");
2198
- if (lastResponse && lastResponse.actions) {
2566
+ if (lastResponse) {
2199
2567
  const response = lastResponse;
2568
+ const actions = response.actions || [];
2569
+ if (response.message && response.message.includes("TASK_COMPLETED:")) {
2570
+ isTaskCompleted = true;
2571
+ finalSummary = response.message.split("TASK_COMPLETED:")[1].trim();
2572
+ keepGoing = false;
2573
+ }
2574
+ if (actions.length === 0 && response.message && !isTaskCompleted) {
2575
+ tui.log.info(colors.primary("\u{1F916} Shark Dev:"));
2576
+ console.log(response.message);
2577
+ const userReply = await tui.text({ message: "Your answer:" });
2578
+ if (tui.isCancel(userReply)) {
2579
+ keepGoing = false;
2580
+ break;
2581
+ }
2582
+ nextPrompt = userReply;
2583
+ }
2200
2584
  let executionResults = "";
2201
2585
  let waitingForUser = false;
2202
- let specUpdated = false;
2203
- for (const action of response.actions) {
2586
+ for (const action of actions) {
2204
2587
  if (action.type === "talk_with_user") {
2205
2588
  tui.log.info(colors.primary("\u{1F916} Shark Dev:"));
2206
2589
  console.log(action.content);
2207
- waitingForUser = true;
2590
+ if (!isTaskCompleted) waitingForUser = true;
2208
2591
  } else if (action.type === "list_files") {
2209
- tui.log.info(`\u{1F4C2} Scanning dir: ${colors.dim(action.path || ".")}`);
2592
+ tui.log.info(`\u{1F4C2} Scanning: ${colors.dim(action.path || ".")}`);
2210
2593
  const result = handleListFiles(action.path || ".");
2211
2594
  executionResults += `[Action list_files(${action.path}) Result]:
2212
2595
  ${result}
@@ -2220,7 +2603,6 @@ ${result}
2220
2603
 
2221
2604
  `;
2222
2605
  } else if (action.type === "search_file") {
2223
- tui.log.info(`\u{1F50D} Searching: ${colors.dim(action.path || "")}`);
2224
2606
  const result = handleSearchFile(action.path || "");
2225
2607
  executionResults += `[Action search_file(${action.path}) Result]:
2226
2608
  ${result}
@@ -2229,26 +2611,16 @@ ${result}
2229
2611
  } else if (action.type === "run_command") {
2230
2612
  const cmd = action.command || "";
2231
2613
  tui.log.info(`\u{1F4BB} Executing: ${colors.dim(cmd)}`);
2232
- let approved = false;
2233
- if (autoApprovals.commands) {
2234
- approved = true;
2235
- tui.log.success(`\u26A1 Auto-Approved Command: ${cmd}`);
2236
- } else {
2614
+ let approved = autoApprovals.commands;
2615
+ if (!approved) {
2237
2616
  const choice = await tui.select({
2238
2617
  message: `Execute: ${cmd}?`,
2239
- options: [
2240
- { value: "yes", label: "Yes (Execute once)" },
2241
- { value: "always", label: "Yes (Output & Auto-Approve Commands for Session)" },
2242
- { value: "no", label: "No (Skip)" }
2243
- ]
2618
+ options: [{ value: "yes", label: "Yes" }, { value: "always", label: "Yes (Auto-Approve Session)" }, { value: "no", label: "No" }]
2244
2619
  });
2245
2620
  if (choice === "always") {
2246
2621
  autoApprovals.commands = true;
2247
2622
  approved = true;
2248
- tui.log.success("\u26A1 COMMANDS Auto-Approval ENABLED for this session.");
2249
- } else if (choice === "yes") {
2250
- approved = true;
2251
- }
2623
+ } else if (choice === "yes") approved = true;
2252
2624
  }
2253
2625
  if (approved) {
2254
2626
  const result = await handleRunCommand(cmd);
@@ -2262,302 +2634,159 @@ ${result}
2262
2634
  `;
2263
2635
  }
2264
2636
  } else if (["create_file", "modify_file"].includes(action.type)) {
2265
- const isCreate = action.type === "create_file";
2266
2637
  const filePath = action.path || "";
2267
- tui.log.warning(`
2268
- \u{1F916} Agent wants to ${isCreate ? "CREATE" : "MODIFY"}: ${colors.bold(filePath)}`);
2269
- if (action.content) {
2270
- const preview = action.content.length > 500 ? action.content.substring(0, 500) + "... (truncated)" : action.content;
2271
- console.log(colors.dim("--- Content ---\n") + preview + "\n" + colors.dim("---------------"));
2272
- }
2273
- if (!isCreate && action.line_range && !action.confirmed) {
2274
- tui.log.info("\u{1F6E1}\uFE0F Generation Preview for Agent Verification...");
2275
- const [start, end] = action.line_range;
2276
- const previewDiff = await generateFilePreview(filePath, start, end, action.content || "");
2277
- executionResults += `[Action modify_file]: PENDING CONFIRMATION.
2278
-
2279
- ${previewDiff}
2280
-
2281
- `;
2282
- executionResults += `USER/SYSTEM INSTRUCTION: Please review the above preview carefully.
2283
- 1. If the context and changes look correct, call 'modify_file' again with the SAME parameters AND "confirmed": true.
2284
- 2. If the context is wrong (e.g. wrong line numbers), call 'read_file' to check line numbers again, then correct your request.
2285
-
2286
- `;
2287
- tui.log.warning("\u26A0\uFE0F Sent Preview to Agent for confirmation first.");
2288
- waitingForUser = false;
2289
- continue;
2290
- }
2291
- let approved = false;
2292
- if (autoApprovals.files) {
2293
- approved = true;
2294
- tui.log.success(`\u26A1 Auto-Approved File Action: ${filePath}`);
2295
- } else {
2638
+ tui.log.warning(`\u{1F4DD} ${action.type === "create_file" ? "CREATE" : "MODIFY"}: ${colors.bold(filePath)}`);
2639
+ let approved = autoApprovals.files;
2640
+ if (!approved) {
2296
2641
  const choice = await tui.select({
2297
2642
  message: `Approve changes to ${filePath}?`,
2298
- options: [
2299
- { value: "yes", label: "Yes (Approve once)" },
2300
- { value: "always", label: "Yes (Approve & Auto-Approve FILES for Session)" },
2301
- { value: "no", label: "No (Skip)" }
2302
- ]
2643
+ options: [{ value: "yes", label: "Yes" }, { value: "always", label: "Yes (Auto-Approve Session)" }, { value: "no", label: "No" }]
2303
2644
  });
2304
2645
  if (choice === "always") {
2305
2646
  autoApprovals.files = true;
2306
2647
  approved = true;
2307
- tui.log.success("\u26A1 FILE ACTIONS Auto-Approval ENABLED for this session.");
2308
- } else if (choice === "yes") {
2309
- approved = true;
2310
- }
2648
+ } else if (choice === "yes") approved = true;
2311
2649
  }
2312
2650
  if (approved) {
2313
- if (filePath) {
2314
- const targetPath = path7.resolve(projectRoot, filePath);
2315
- const dir = path7.dirname(targetPath);
2316
- if (!fs6.existsSync(dir)) fs6.mkdirSync(dir, { recursive: true });
2317
- if (isCreate) {
2318
- const BOM = "\uFEFF";
2319
- const contentToWrite = action.content || "";
2320
- const finalContent = contentToWrite.startsWith(BOM) ? contentToWrite : BOM + contentToWrite;
2321
- fs6.writeFileSync(targetPath, finalContent, { encoding: "utf-8" });
2322
- tui.log.success(`\u2705 Created: ${filePath}`);
2323
- executionResults += `[Action create_file(${filePath})]: Success
2324
-
2325
- `;
2326
- if (filePath.endsWith("tech-spec.md")) specUpdated = true;
2327
- const config = ConfigManager.getInstance().getConfig();
2328
- const validationEnabled = config.validation?.enablePostSaveValidation ?? true;
2329
- if (validationEnabled) {
2330
- const ext = path7.extname(filePath);
2331
- if ([".ts", ".tsx"].includes(ext)) {
2332
- tui.log.info("\u{1F50D} Validating TypeScript...");
2333
- const validation = await validateTypeScript(filePath);
2334
- if (!validation.valid) {
2335
- tui.log.error("\u274C TypeScript validation failed");
2336
- executionResults += `
2337
- [TYPESCRIPT VALIDATION FAILED]:
2338
- ${validation.error}
2339
-
2340
- `;
2341
- executionResults += `CRITICAL: The file has been saved but contains errors. Read the file again to verify line numbers before attempting to fix. Do not guess line numbers.
2342
- `;
2343
- } else {
2344
- tui.log.success("\u2705 TypeScript OK");
2345
- }
2346
- }
2347
- if (ext === ".html") {
2348
- tui.log.info("\u{1F50D} Validating HTML...");
2349
- const validation = validateHtmlTagBalance(filePath);
2350
- if (!validation.valid) {
2351
- tui.log.error("\u274C HTML validation failed");
2352
- executionResults += `
2353
- [HTML VALIDATION FAILED]:
2354
- ${validation.error}
2355
-
2356
- `;
2357
- executionResults += `CRITICAL: Fix these errors before proceeding to next task.
2358
- `;
2359
- } else {
2360
- tui.log.success("\u2705 HTML OK");
2361
- }
2362
- }
2363
- }
2364
- } else {
2365
- let success = false;
2366
- tui.log.info(`\u{1F50D} Debug modify_file: line_range=${JSON.stringify(action.line_range)}, type=${typeof action.line_range}, isArray=${Array.isArray(action.line_range)}`);
2367
- if (action.line_range && Array.isArray(action.line_range) && action.line_range.length === 2) {
2368
- const [start, end] = action.line_range;
2369
- success = replaceLineRange(filePath, start, end, action.content || "", tui);
2370
- } else if (action.target_content) {
2371
- success = startSmartReplace(filePath, action.content || "", action.target_content, tui);
2372
- } else {
2373
- tui.log.error("\u274C Missing line_range (recommended) or target_content for modification.");
2374
- executionResults += `[Action modify_file]: Failed. Missing line_range or target_content.
2375
-
2376
- `;
2377
- }
2378
- if (success) {
2379
- executionResults += `[Action modify_file(${filePath})]: Success
2651
+ if (action.type === "create_file") {
2652
+ const dir = path8.dirname(path8.resolve(projectRoot, filePath));
2653
+ if (!fs7.existsSync(dir)) fs7.mkdirSync(dir, { recursive: true });
2654
+ fs7.writeFileSync(path8.resolve(projectRoot, filePath), action.content || "", "utf-8");
2655
+ executionResults += `[Action create_file]: Success
2380
2656
 
2381
2657
  `;
2382
- if (filePath.endsWith("tech-spec.md")) specUpdated = true;
2383
- const config = ConfigManager.getInstance().getConfig();
2384
- const validationEnabled = config.validation?.enablePostSaveValidation ?? true;
2385
- if (validationEnabled) {
2386
- const ext = path7.extname(filePath);
2387
- if ([".ts", ".tsx"].includes(ext)) {
2388
- tui.log.info("\u{1F50D} Validating TypeScript...");
2389
- const validation = await validateTypeScript(filePath);
2390
- if (!validation.valid) {
2391
- tui.log.error("\u274C TypeScript validation failed");
2392
- executionResults += `
2393
- [TYPESCRIPT VALIDATION FAILED]:
2394
- ${validation.error}
2658
+ } else {
2659
+ let success = false;
2660
+ if (action.line_range) success = replaceLineRange(filePath, action.line_range[0], action.line_range[1], action.content || "", tui);
2661
+ else if (action.target_content) success = startSmartReplace(filePath, action.content || "", action.target_content, tui);
2662
+ executionResults += success ? `[Action modify_file]: Success
2395
2663
 
2396
- `;
2397
- executionResults += `CRITICAL: Fix these errors before proceeding to next task.
2398
- `;
2399
- } else {
2400
- tui.log.success("\u2705 TypeScript OK");
2401
- }
2402
- }
2403
- if (ext === ".html") {
2404
- tui.log.info("\u{1F50D} Validating HTML...");
2405
- const validation = validateHtmlTagBalance(filePath);
2406
- if (!validation.valid) {
2407
- tui.log.error("\u274C HTML validation failed");
2408
- executionResults += `
2409
- [HTML VALIDATION FAILED]:
2410
- ${validation.error}
2664
+ ` : `[Action modify_file]: Failed
2411
2665
 
2412
2666
  `;
2413
- executionResults += `CRITICAL: Fix these errors before proceeding to next task.
2414
- `;
2415
- } else {
2416
- tui.log.success("\u2705 HTML OK");
2417
- }
2418
- }
2419
- }
2420
- } else if (!action.line_range && !action.target_content) {
2421
- } else {
2422
- executionResults += `[Action modify_file(${filePath})]: FAILED. Target content not found or ambiguous. Read the file again to ensure accuracy.
2667
+ }
2668
+ const val = await validateTypeScript(path8.resolve(projectRoot, filePath));
2669
+ if (!val.valid) executionResults += `[Validation Failed]: ${val.error}
2423
2670
 
2424
2671
  `;
2425
- }
2426
- }
2427
- }
2428
2672
  } else {
2429
- tui.log.error("\u274C Denied.");
2430
2673
  executionResults += `[Action ${action.type}]: User Denied.
2431
2674
 
2432
2675
  `;
2433
2676
  }
2434
- } else if (action.type === "search_ast") {
2435
- tui.log.info(`\u{1F50D} Searching AST: ${action.pattern} in ${action.file_path || action.path}`);
2436
- const result = await astGrepSearch(
2437
- action.pattern || "",
2438
- action.file_path || action.path || "",
2439
- action.language || "typescript",
2440
- // default TS
2441
- tui
2442
- );
2443
- executionResults += `[Action search_ast]:
2677
+ } else if (action.type.startsWith("ast_")) {
2678
+ try {
2679
+ let result = "";
2680
+ if (action.type === "ast_list_structure") {
2681
+ result = await astListStructure(action.path || "");
2682
+ } else if (action.type === "ast_get_method") {
2683
+ result = await astGetMethod(action.path || "", action.class_name || "", action.method_name || "");
2684
+ } else if (action.type === "ast_add_method") {
2685
+ const success = await astAddMethod(action.path || "", action.class_name || "", action.method_code || "");
2686
+ result = success ? "Method added successfully." : "Failed to add method.";
2687
+ } else if (action.type === "ast_modify_method") {
2688
+ const success = await astModifyMethod(action.path || "", action.class_name || "", action.method_name || "", action.new_body || "");
2689
+ result = success ? "Method modified successfully." : "Failed to modify method.";
2690
+ } else if (action.type === "ast_remove_method") {
2691
+ const success = await astRemoveMethod(action.path || "", action.class_name || "", action.method_name || "");
2692
+ result = success ? "Method removed successfully." : "Failed to remove method.";
2693
+ } else if (action.type === "ast_add_class") {
2694
+ const success = await astAddClass(action.path || "", action.class_name || "", action.extends_class || void 0, action.implements_interfaces || void 0);
2695
+ result = success ? "Class added successfully." : "Failed to add class.";
2696
+ } else if (action.type === "ast_add_property") {
2697
+ const success = await astAddProperty(action.path || "", action.class_name || "", action.property_code || "");
2698
+ result = success ? "Property added successfully." : "Failed to add property.";
2699
+ } else if (action.type === "ast_remove_property") {
2700
+ const success = await astRemoveProperty(action.path || "", action.class_name || "", action.property_name || "");
2701
+ result = success ? "Property removed successfully." : "Failed to remove property.";
2702
+ } else if (action.type === "ast_add_decorator") {
2703
+ const success = await astAddDecorator(action.path || "", action.class_name || "", action.decorator_code || "");
2704
+ result = success ? "Decorator added successfully." : "Failed to add decorator.";
2705
+ } else if (action.type === "ast_add_interface") {
2706
+ const success = await astAddInterface(action.path || "", action.interface_code || "");
2707
+ result = success ? "Interface added successfully." : "Failed to add interface.";
2708
+ } else if (action.type === "ast_add_type_alias") {
2709
+ const success = await astAddTypeAlias(action.path || "", action.type_code || "");
2710
+ result = success ? "Type alias added successfully." : "Failed to add type alias.";
2711
+ } else if (action.type === "ast_add_function") {
2712
+ const success = await astAddFunction(action.path || "", action.function_code || "");
2713
+ result = success ? "Function added successfully." : "Failed to add function.";
2714
+ } else if (action.type === "ast_remove_function") {
2715
+ const success = await astRemoveFunction(action.path || "", action.function_name || "");
2716
+ result = success ? "Function removed successfully." : "Failed to remove function.";
2717
+ } else if (action.type === "ast_add_import") {
2718
+ const success = await astAddImport(action.path || "", action.import_statement || "");
2719
+ result = success ? "Import added successfully." : "Failed to add import.";
2720
+ } else if (action.type === "ast_remove_import") {
2721
+ const success = await astRemoveImport(action.path || "", action.module_path || "");
2722
+ result = success ? "Import removed successfully." : "Failed to remove import.";
2723
+ } else if (action.type === "ast_organize_imports") {
2724
+ const success = await astOrganizeImports(action.path || "");
2725
+ result = success ? "Imports organized successfully." : "Failed to organize imports.";
2726
+ } else {
2727
+ result = `Unknown AST action: ${action.type}`;
2728
+ }
2729
+ executionResults += `[Action ${action.type} Result]:
2444
2730
  ${result}
2445
2731
 
2446
2732
  `;
2447
- } else if (action.type === "modify_ast") {
2448
- const targetPath = action.file_path || action.path || "";
2449
- tui.log.step(`\u{1F504} [AST] Modifying: ${targetPath}`);
2450
- tui.log.info(`Pattern: ${colors.primary(action.pattern || "")}`);
2451
- tui.log.info(`Fix: ${colors.success(action.fix || "")}`);
2452
- const approved = await tui.confirm({ message: "Execute this AST modification?" });
2453
- if (approved) {
2454
- const success = await astGrepRewrite(
2455
- action.pattern || "",
2456
- action.fix || "",
2457
- targetPath,
2458
- action.language || "typescript",
2459
- tui
2460
- );
2461
- if (success) {
2462
- executionResults += `[Action modify_ast]: Success.
2733
+ tui.log.info(`\u26A1 AST Action ${colors.dim(action.type)}: ${result}`);
2734
+ } catch (e) {
2735
+ executionResults += `[Action ${action.type} Failed]: ${e.message}
2463
2736
 
2464
2737
  `;
2465
- if (targetPath.endsWith("tech-spec.md")) specUpdated = true;
2466
- const ext = path7.extname(targetPath);
2467
- if ([".ts", ".tsx"].includes(ext)) {
2468
- const validation = await validateTypeScript(targetPath);
2469
- if (!validation.valid) {
2470
- executionResults += `
2471
- [TYPESCRIPT VALIDATION FAILED]:
2472
- ${validation.error}
2738
+ tui.log.error(`\u274C AST Action Error: ${e.message}`);
2739
+ }
2740
+ } else if (action.type === "search_ast") {
2741
+ const result = await astGrepSearch(action.pattern || "", action.path || "", action.language || "typescript", tui);
2742
+ executionResults += `[Action search_ast Result]:
2743
+ ${result}
2473
2744
 
2474
2745
  `;
2475
- }
2476
- }
2477
- } else {
2478
- executionResults += `[Action modify_ast]: Failed. Check logs.
2746
+ } else if (action.type === "modify_ast") {
2747
+ const success = await astGrepRewrite(action.pattern || "", action.fix || "", action.path || "", action.language || "typescript", tui);
2748
+ executionResults += success ? `[Action modify_ast]: Success
2479
2749
 
2480
- `;
2481
- }
2482
- } else {
2483
- tui.log.error("\u274C Denied.");
2484
- executionResults += `[Action modify_ast]: User Denied.
2750
+ ` : `[Action modify_ast]: Failed
2485
2751
 
2486
2752
  `;
2487
- }
2488
2753
  }
2489
2754
  }
2490
- const previousState = specState;
2491
- specState = analyzeSpecState(projectRoot);
2492
- let systemInjection = "";
2493
2755
  if (executionResults) {
2494
- if (previousState.status === "PENDING" && specState.status === "PENDING" && previousState.nextTask !== specState.nextTask) {
2495
- systemInjection = `
2496
- \u{1F389} Task "${previousState.nextTask}" COMPLETED! Next up: "${specState.nextTask}".
2497
- `;
2498
- } else if (previousState.status === "PENDING" && specState.status === "PENDING" && previousState.nextTask === specState.nextTask) {
2499
- if (!specUpdated && stepCount % 3 === 0) {
2500
- systemInjection = `
2501
- Reminder: You are still working on "${specState.nextTask}". Don't forget to mark it [x] in 'tech-spec.md' when done.
2502
- `;
2503
- }
2504
- } else if (previousState.status === "MISSING" && specState.status === "PENDING") {
2505
- systemInjection = `
2506
- \u2705 Spec Created! Starting execution of: "${specState.nextTask}".
2507
- `;
2508
- }
2509
2756
  if (waitingForUser) {
2510
2757
  const userReply = await tui.text({ message: "Your answer:" });
2511
2758
  if (tui.isCancel(userReply)) {
2512
2759
  keepGoing = false;
2513
2760
  break;
2514
2761
  }
2515
- nextPrompt = `${executionResults}${systemInjection}
2762
+ nextPrompt = `${executionResults}
2516
2763
  User Reply: ${userReply}`;
2517
2764
  } else {
2518
- nextPrompt = `${executionResults}${systemInjection}
2519
- [System]: Continue.`;
2765
+ nextPrompt = `${executionResults}
2766
+ [System]: Continue execution. If finished, output "TASK_COMPLETED: summary".`;
2520
2767
  tui.log.info(colors.dim("Processing results..."));
2521
2768
  }
2769
+ } else if (!keepGoing) {
2522
2770
  } else if (waitingForUser) {
2523
- const userReply = await tui.text({ message: "Your answer:" });
2524
- if (tui.isCancel(userReply)) {
2525
- keepGoing = false;
2526
- break;
2527
- }
2528
- nextPrompt = userReply;
2529
2771
  } else {
2530
- if (response.message) {
2531
- tui.log.info(colors.primary("\u{1F916} Shark Dev:"));
2532
- console.log(response.message);
2533
- const userReply = await tui.text({ message: "Your answer:" });
2534
- if (tui.isCancel(userReply)) {
2535
- keepGoing = false;
2536
- } else {
2537
- nextPrompt = userReply;
2538
- }
2539
- } else {
2540
- tui.log.warning("Agent took no actions and sent no message.");
2541
- nextPrompt = "Please proceed or ask for clarification.";
2542
- }
2772
+ if (!isTaskCompleted && actions.length > 0) nextPrompt = "Please continue.";
2543
2773
  }
2544
2774
  } else {
2545
- tui.log.warning("Invalid response from agent (no actions).");
2546
- nextPrompt = "Error: No valid actions returned. Please try again with JSON format.";
2775
+ tui.log.warning("No response received from agent.");
2547
2776
  }
2548
2777
  } catch (e) {
2549
- spinner.stop("Error");
2550
2778
  tui.log.error(e.message);
2551
- FileLogger.log("DEV_AGENT", "Main Loop Error", e);
2552
2779
  keepGoing = false;
2780
+ return { success: false, summary: `Error: ${e.message}` };
2553
2781
  }
2554
2782
  }
2555
- tui.outro("\u{1F44B} Shark Dev Session Ended");
2783
+ tui.log.success("\u2705 Task Scope Completed");
2784
+ return { success: true, summary: finalSummary || "Task completed without summary." };
2556
2785
  }
2557
- async function callDevAgentApi(prompt, onChunk) {
2786
+ async function callDevAgentApi(prompt, onChunk, conversationKey = AGENT_TYPE4) {
2558
2787
  const realm = await getActiveRealm();
2559
2788
  const token = await ensureValidToken(realm);
2560
- const conversationId = await conversationManager.getConversationId(AGENT_TYPE4);
2789
+ const conversationId = await conversationManager.getConversationId(conversationKey);
2561
2790
  const payload = {
2562
2791
  user_prompt: prompt,
2563
2792
  streaming: true,
@@ -2586,22 +2815,248 @@ async function callDevAgentApi(prompt, onChunk) {
2586
2815
  });
2587
2816
  const parsed = parseAgentResponse(raw);
2588
2817
  if (parsed.conversation_id) {
2589
- await conversationManager.saveConversationId(AGENT_TYPE4, parsed.conversation_id);
2818
+ await conversationManager.saveConversationId(conversationKey, parsed.conversation_id);
2590
2819
  }
2591
2820
  return parsed;
2592
2821
  }
2593
2822
 
2823
+ // src/core/workflow/task-manager.ts
2824
+ import fs8 from "fs";
2825
+ import path9 from "path";
2826
+ var TaskManager = class {
2827
+ projectRoot;
2828
+ specPath;
2829
+ constructor(projectRoot = process.cwd()) {
2830
+ this.projectRoot = projectRoot;
2831
+ this.specPath = path9.resolve(this.projectRoot, "tech-spec.md");
2832
+ }
2833
+ /**
2834
+ * Reads the tech-spec.md file and analyzes its current state.
2835
+ */
2836
+ analyzeSpecState() {
2837
+ if (!fs8.existsSync(this.specPath)) {
2838
+ return { status: "MISSING", allTasks: [] };
2839
+ }
2840
+ const content = fs8.readFileSync(this.specPath, "utf-8");
2841
+ const lines = content.split("\n");
2842
+ const tasks = [];
2843
+ let taskIndex = 1;
2844
+ lines.forEach((line, index) => {
2845
+ const trimmed = line.trim();
2846
+ const pendingMatch = trimmed.match(/^- \[ \] (.*)/);
2847
+ const completedMatch = trimmed.match(/^- \[x\] (.*)/i);
2848
+ const progressMatch = trimmed.match(/^- \[\/\] (.*)/);
2849
+ if (pendingMatch) {
2850
+ tasks.push({
2851
+ id: `task-${taskIndex++}`,
2852
+ description: pendingMatch[1].trim(),
2853
+ status: "PENDING",
2854
+ line_number: index
2855
+ });
2856
+ } else if (completedMatch) {
2857
+ tasks.push({
2858
+ id: `task-${taskIndex++}`,
2859
+ description: completedMatch[1].trim(),
2860
+ status: "COMPLETED",
2861
+ line_number: index
2862
+ });
2863
+ } else if (progressMatch) {
2864
+ tasks.push({
2865
+ id: `task-${taskIndex++}`,
2866
+ description: progressMatch[1].trim(),
2867
+ status: "IN_PROGRESS",
2868
+ line_number: index
2869
+ });
2870
+ }
2871
+ });
2872
+ let nextTask = tasks.find((t2) => t2.status === "IN_PROGRESS");
2873
+ if (!nextTask) {
2874
+ nextTask = tasks.find((t2) => t2.status === "PENDING");
2875
+ }
2876
+ const status = !nextTask && tasks.length > 0 && tasks.every((t2) => t2.status === "COMPLETED") ? "COMPLETED" : "PENDING";
2877
+ return {
2878
+ status: tasks.length === 0 ? "MISSING" : status,
2879
+ // Empty file is effectively "pending creation" but we treat as missing content logic elsewhere
2880
+ nextTask,
2881
+ allTasks: tasks
2882
+ };
2883
+ }
2884
+ /**
2885
+ * Marks a specific task as COMPLETED in the file.
2886
+ * Uses line-based replacement to be safe.
2887
+ */
2888
+ markTaskAsDone(taskId) {
2889
+ const state = this.analyzeSpecState();
2890
+ const task = state.allTasks.find((t2) => t2.id === taskId);
2891
+ if (!task) {
2892
+ console.error(`Task ${taskId} not found.`);
2893
+ return false;
2894
+ }
2895
+ const content = fs8.readFileSync(this.specPath, "utf-8");
2896
+ const lines = content.split("\n");
2897
+ const targetLine = lines[task.line_number];
2898
+ if (!targetLine.includes(task.description)) {
2899
+ console.error(`Concurrency Error: Task line content mistmatch. Expected "${task.description}" at line ${task.line_number}.`);
2900
+ return false;
2901
+ }
2902
+ const newLine = targetLine.replace("- [ ]", "- [x]").replace("- [/]", "- [x]");
2903
+ lines[task.line_number] = newLine;
2904
+ fs8.writeFileSync(this.specPath, lines.join("\n"), "utf-8");
2905
+ return true;
2906
+ }
2907
+ /**
2908
+ * Marks a task as IN_PROGRESS.
2909
+ */
2910
+ markTaskInProgress(taskId) {
2911
+ const state = this.analyzeSpecState();
2912
+ const task = state.allTasks.find((t2) => t2.id === taskId);
2913
+ if (!task) return false;
2914
+ const content = fs8.readFileSync(this.specPath, "utf-8");
2915
+ const lines = content.split("\n");
2916
+ let targetLine = lines[task.line_number];
2917
+ targetLine = targetLine.replace("- [ ]", "- [/]");
2918
+ lines[task.line_number] = targetLine;
2919
+ fs8.writeFileSync(this.specPath, lines.join("\n"), "utf-8");
2920
+ return true;
2921
+ }
2922
+ /**
2923
+ * Completely updates the spec file content (used by Spec Agent).
2924
+ */
2925
+ updateSpecContent(newContent) {
2926
+ fs8.writeFileSync(this.specPath, newContent, "utf-8");
2927
+ }
2928
+ getSpecPath() {
2929
+ return this.specPath;
2930
+ }
2931
+ };
2932
+
2594
2933
  // src/commands/dev.ts
2595
- var devCommand = new Command3("dev").description("Starts the Shark Developer Agent (Shark Dev)").option("-t, --task <type>", "Initial task description").option("-c, --context <path>", "Path to custom context file").action(async (options) => {
2596
- await interactiveDeveloperAgent(options);
2934
+ var devCommand = new Command3("dev").description("Starts the Shark Developer Agent (Shark Dev Orchestration V2)").option("-t, --task <type>", "Initial task description (Quick Mode)").option("-c, --context <path>", "Path to custom context file").action(async (options) => {
2935
+ const taskManager = new TaskManager(process.cwd());
2936
+ let state = taskManager.analyzeSpecState();
2937
+ if (state.status === "MISSING") {
2938
+ tui.log.warning("\u{1F4CB} No tech-spec.md found.");
2939
+ const confirm = await tui.confirm({ message: "Create a new Specification (Tech Spec)?" });
2940
+ if (confirm) {
2941
+ await interactiveSpecificationAgent({
2942
+ briefingPath: options.task ? void 0 : void 0
2943
+ // If task provided, maybe write a temp briefing? For now standard flow.
2944
+ });
2945
+ state = taskManager.analyzeSpecState();
2946
+ if (state.status === "MISSING") {
2947
+ tui.log.error("\u274C Spec creation aborted or failed.");
2948
+ return;
2949
+ }
2950
+ } else {
2951
+ return;
2952
+ }
2953
+ }
2954
+ let keepOrchestrating = true;
2955
+ let burnMode = false;
2956
+ let contextHistory = "";
2957
+ tui.intro("\u{1F988} Shark Orchestrator V2");
2958
+ while (keepOrchestrating) {
2959
+ state = taskManager.analyzeSpecState();
2960
+ if (state.status === "COMPLETED") {
2961
+ tui.log.success("\u{1F389} All tasks in tech-spec.md are COMPLETED!");
2962
+ const choice = await tui.select({
2963
+ message: "What next?",
2964
+ options: [
2965
+ { value: "exit", label: "Exit" },
2966
+ { value: "new_spec", label: "New Specification (Reset)" }
2967
+ ]
2968
+ });
2969
+ if (choice === "new_spec") {
2970
+ await interactiveSpecificationAgent();
2971
+ contextHistory = "";
2972
+ continue;
2973
+ } else {
2974
+ keepOrchestrating = false;
2975
+ break;
2976
+ }
2977
+ }
2978
+ const currentTask = state.nextTask;
2979
+ if (!currentTask) {
2980
+ tui.log.error("Something went wrong. Status is not completed but no next task found.");
2981
+ break;
2982
+ }
2983
+ tui.log.info(`
2984
+ \u{1F449} **NEXT TASK**: ${colors.bold(currentTask.description)}`);
2985
+ if (!burnMode) {
2986
+ const action = await tui.select({
2987
+ message: "Orchestration Checkpoint:",
2988
+ options: [
2989
+ { value: "execute", label: "\u{1F680} Execute Task (Start)" },
2990
+ { value: "burn", label: "\u{1F525} Burn Mode (Auto-Execute Remaining)" },
2991
+ { value: "pivot", label: "\u{1F527} Pivot/Correct (Edit Spec)" },
2992
+ { value: "skip", label: "\u23ED\uFE0F Skip Task (Mark Done without Executing)" },
2993
+ { value: "stop", label: "\u{1F6D1} Stop Session" }
2994
+ ]
2995
+ });
2996
+ if (action === "stop") {
2997
+ keepOrchestrating = false;
2998
+ break;
2999
+ } else if (action === "burn") {
3000
+ burnMode = true;
3001
+ } else if (action === "skip") {
3002
+ taskManager.markTaskAsDone(currentTask.id);
3003
+ tui.log.info("Task skipped.");
3004
+ continue;
3005
+ } else if (action === "pivot") {
3006
+ tui.log.info("Transferring control to Specification Agent...");
3007
+ await interactiveSpecificationAgent({
3008
+ initialContext: `User requested pivot after tasks:
3009
+ ${contextHistory}`
3010
+ });
3011
+ continue;
3012
+ }
3013
+ }
3014
+ taskManager.markTaskInProgress(currentTask.id);
3015
+ tui.log.info(`\u26A1 Starting Micro-Context for Task: "${currentTask.description}"`);
3016
+ const result = await interactiveDeveloperAgent({
3017
+ taskId: currentTask.id,
3018
+ taskInstruction: currentTask.description,
3019
+ history: contextHistory,
3020
+ context: options.context
3021
+ });
3022
+ if (result.success) {
3023
+ tui.log.success(`\u2705 Task Completed: ${currentTask.description}`);
3024
+ taskManager.markTaskAsDone(currentTask.id);
3025
+ contextHistory += `
3026
+ [Task "${currentTask.description}" completed]: ${result.summary}`;
3027
+ } else {
3028
+ tui.log.error(`\u274C Task Failed: ${result.summary}`);
3029
+ burnMode = false;
3030
+ const recovery = await tui.select({
3031
+ message: "Task Failed. Recovery Action:",
3032
+ options: [
3033
+ { value: "retry", label: "Retry (Run Agent Again)" },
3034
+ { value: "pivot", label: "Pivot (Adjust Spec/Instructions)" },
3035
+ { value: "ignore", label: "Ignore (Mark Done anyway)" },
3036
+ { value: "stop", label: "Stop" }
3037
+ ]
3038
+ });
3039
+ if (recovery === "stop") break;
3040
+ if (recovery === "ignore") taskManager.markTaskAsDone(currentTask.id);
3041
+ if (recovery === "pivot") {
3042
+ await interactiveSpecificationAgent({
3043
+ initialContext: `Task "${currentTask.description}" FAILED.
3044
+ Error: ${result.summary}
3045
+ History:
3046
+ ${contextHistory}`
3047
+ });
3048
+ }
3049
+ }
3050
+ }
3051
+ tui.outro("\u{1F988} Orchestration Finished.");
2597
3052
  });
2598
3053
 
2599
3054
  // src/commands/qa.ts
2600
3055
  import { Command as Command4 } from "commander";
2601
3056
 
2602
3057
  // src/core/agents/qa-agent.ts
2603
- import fs7 from "fs";
2604
- import path8 from "path";
3058
+ import fs9 from "fs";
3059
+ import path10 from "path";
2605
3060
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2606
3061
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
2607
3062
  var AGENT_TYPE5 = "qa_agent";
@@ -2670,9 +3125,9 @@ async function runQAAgent(options) {
2670
3125
  }
2671
3126
  let projectContext = "";
2672
3127
  try {
2673
- const contextPath = path8.join(process.cwd(), "_sharkrc", "project-context.md");
2674
- if (fs7.existsSync(contextPath)) {
2675
- projectContext = fs7.readFileSync(contextPath, "utf-8");
3128
+ const contextPath = path10.join(process.cwd(), "_sharkrc", "project-context.md");
3129
+ if (fs9.existsSync(contextPath)) {
3130
+ projectContext = fs9.readFileSync(contextPath, "utf-8");
2676
3131
  tui.log.info(`\u{1F4D8} Context loaded from: _sharkrc/project-context.md`);
2677
3132
  }
2678
3133
  } catch (e) {
@@ -2781,11 +3236,11 @@ ${projectContext}
2781
3236
  break;
2782
3237
  case "create_file":
2783
3238
  if (action.path && action.content) {
2784
- const fullPath = path8.resolve(process.cwd(), action.path);
3239
+ const fullPath = path10.resolve(process.cwd(), action.path);
2785
3240
  const BOM = "\uFEFF";
2786
3241
  const contentToWrite = action.content;
2787
3242
  const finalContent = contentToWrite.startsWith(BOM) ? contentToWrite : BOM + contentToWrite;
2788
- fs7.writeFileSync(fullPath, finalContent, { encoding: "utf-8" });
3243
+ fs9.writeFileSync(fullPath, finalContent, { encoding: "utf-8" });
2789
3244
  tui.log.success(`File created: ${action.path}`);
2790
3245
  result = "File created successfully.";
2791
3246
  }