@vheins/local-memory-mcp 0.18.12 → 0.18.13

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.
@@ -63,7 +63,7 @@ import {
63
63
  toContextSlug,
64
64
  updateSessionFromInitialize,
65
65
  updateSessionRoots
66
- } from "../chunk-UWAZ66N5.js";
66
+ } from "../chunk-BK2QIQXS.js";
67
67
 
68
68
  // src/mcp/server.ts
69
69
  import readline from "readline";
@@ -797,25 +797,7 @@ function capitalize2(str) {
797
797
  return str.charAt(0).toUpperCase() + str.slice(1);
798
798
  }
799
799
 
800
- // src/mcp/tools/task.manage.ts
801
- import { randomUUID as randomUUID2 } from "crypto";
802
- var UUID_REGEX3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
803
- function resolveParentId(value, owner, repo, storage, localCodeMap) {
804
- if (!value) return null;
805
- if (UUID_REGEX3.test(value)) return value;
806
- if (localCodeMap?.has(value)) return localCodeMap.get(value);
807
- const parent = storage.tasks.getTaskByCode(owner, repo, value);
808
- if (!parent) throw new Error(`parent_id: task with code '${value}' not found in repo '${repo}'`);
809
- return parent.id;
810
- }
811
- function resolveDependsOn(value, owner, repo, storage, localCodeMap) {
812
- if (!value) return null;
813
- if (UUID_REGEX3.test(value)) return value;
814
- if (localCodeMap?.has(value)) return localCodeMap.get(value);
815
- const task = storage.tasks.getTaskByCode(owner, repo, value);
816
- if (!task) throw new Error(`depends_on: task with code '${value}' not found in repo '${repo}'`);
817
- return task.id;
818
- }
800
+ // src/mcp/tools/task.list.ts
819
801
  function describeTaskListFilter(status) {
820
802
  if (!status) return "active";
821
803
  if (status === "all") return "all";
@@ -864,82 +846,6 @@ function buildTaskListSummary(repo, count, status, phase, search, tasksByStatus)
864
846
  function capitalize3(str) {
865
847
  return str.charAt(0).toUpperCase() + str.slice(1);
866
848
  }
867
- function deriveTaskStatusTimestamps(status, now, existingTask) {
868
- const timestamps = {
869
- in_progress_at: null,
870
- finished_at: null,
871
- canceled_at: null
872
- };
873
- if (status === "in_progress" && existingTask?.status !== "in_progress") {
874
- timestamps.in_progress_at = now;
875
- }
876
- if (status === "completed") {
877
- timestamps.finished_at = now;
878
- }
879
- if (status === "canceled") {
880
- timestamps.canceled_at = now;
881
- }
882
- return timestamps;
883
- }
884
- async function archiveTaskToMemory(taskId, repo, storage, vectors2) {
885
- const task = storage.tasks.getTaskById(taskId);
886
- if (!task) return;
887
- const comments = storage.taskComments.getTaskCommentsByTaskId(taskId);
888
- let content = `Task: [${task.task_code}] ${task.title}
889
- `;
890
- content += `Phase: ${task.phase}
891
- `;
892
- content += `Description: ${task.description || "No description"}
893
- `;
894
- content += `Commit: ${task.commit_id || "N/A"}
895
- `;
896
- if (task.changed_files && task.changed_files.length > 0) {
897
- content += `Files changed:
898
- `;
899
- for (const f of task.changed_files) {
900
- content += ` - ${f}
901
- `;
902
- }
903
- }
904
- if (comments && comments.length > 0) {
905
- content += `
906
- Comments & History:
907
- `;
908
- const chronComments = [...comments].reverse();
909
- for (const c of chronComments) {
910
- const statusInfo = c.next_status ? ` (Status: ${c.previous_status} -> ${c.next_status})` : "";
911
- content += `- [${c.created_at}] ${c.agent}${statusInfo}: ${c.comment}
912
- `;
913
- }
914
- }
915
- const metadata = {
916
- task_id: taskId,
917
- task_code: task.task_code,
918
- original_metadata: task.metadata
919
- };
920
- const title = `Completed Task: ${task.title}`;
921
- const truncatedTitle = title.length > 100 ? title.substring(0, 97) + "..." : title;
922
- try {
923
- await handleMemoryStore(
924
- {
925
- type: "task_archive",
926
- title: truncatedTitle,
927
- content,
928
- importance: Math.min(5, task.priority + 1),
929
- agent: task.agent || "system",
930
- role: task.role || "unknown",
931
- model: "system",
932
- scope: { repo, owner: task.owner || "" },
933
- tags: ["task-archive", ...task.tags],
934
- metadata
935
- },
936
- storage,
937
- vectors2
938
- );
939
- } catch (error) {
940
- logger.error("Failed to archive task to memory", { error: String(error) });
941
- }
942
- }
943
849
  async function handleTaskList(args, storage) {
944
850
  const validated = TaskListSchema.parse(args);
945
851
  const { owner, repo, status, phase, query, limit, offset, structured: isStructuredRequest = false } = validated;
@@ -985,848 +891,398 @@ async function handleTaskList(args, storage) {
985
891
  includeSerializedStructuredContent: isStructuredRequest
986
892
  });
987
893
  }
988
- async function handleTaskCreate(args, storage) {
989
- const parsed = TaskCreateSchema.parse(args);
990
- const { owner, repo, tasks: bulkTasks, ...singleTask } = parsed;
991
- if (bulkTasks) {
992
- const createdTasks = [];
993
- const tasksToInsert = [];
994
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
995
- const codesInRequest = /* @__PURE__ */ new Set();
996
- const batchCodes = /* @__PURE__ */ new Set();
997
- for (const taskData of bulkTasks) {
998
- if (!taskData.task_code) {
999
- taskData.task_code = generateNextCode(owner ?? "", repo, "task", storage, batchCodes);
894
+
895
+ // src/mcp/tools/memory.synthesize.ts
896
+ async function handleMemorySynthesize(params, db2, vectors2, options = {}) {
897
+ const validated = MemorySynthesizeSchema.parse(params);
898
+ const session2 = options.session;
899
+ if (!options.sampleMessage || !session2?.supportsSampling) {
900
+ throw new Error("Client does not advertise MCP sampling support");
901
+ }
902
+ const repo = await resolveRepository(validated.repo, session2, options.elicit);
903
+ if (!repo) {
904
+ throw new Error("repo is required when repo cannot be inferred from active MCP roots");
905
+ }
906
+ const repoOwner = validated.owner;
907
+ const recap = await handleMemoryRecap({ owner: repoOwner, repo, limit: 8, offset: 0 }, db2);
908
+ const recapText = getPrimaryTextContent(recap);
909
+ const summary = validated.include_summary ? db2.summaries.getSummary(repoOwner, repo)?.summary : "";
910
+ const taskSnapshot = validated.include_tasks ? await handleTaskList(
911
+ { owner: repoOwner, repo, status: "backlog,pending,in_progress,blocked", limit: 15, offset: 0 },
912
+ db2
913
+ ) : null;
914
+ const taskText = taskSnapshot ? getPrimaryTextContent(taskSnapshot) : "";
915
+ const systemPrompt = [
916
+ "You are a repository memory synthesizer.",
917
+ "Answer strictly from grounded MCP context and tool results.",
918
+ "If memory is insufficient, say so explicitly instead of inventing details.",
919
+ "Prefer concise, technical answers with explicit caveats when evidence is incomplete."
920
+ ].join(" ");
921
+ const contextBlock = [
922
+ `Repository: ${repo}`,
923
+ validated.current_file_path ? `Current file: ${validated.current_file_path}` : "",
924
+ summary ? `Summary:
925
+ ${summary}` : "",
926
+ recapText ? `Recent context:
927
+ ${recapText}` : "",
928
+ taskText ? `Active tasks:
929
+ ${taskText}` : ""
930
+ ].filter(Boolean).join("\n\n");
931
+ const messages = [
932
+ {
933
+ role: "user",
934
+ content: {
935
+ type: "text",
936
+ text: `Objective: ${validated.objective}
937
+
938
+ Grounding context:
939
+ ${contextBlock || "No additional context provided."}`
1000
940
  }
1001
- batchCodes.add(taskData.task_code);
1002
- }
1003
- const allCodes = bulkTasks.map((t) => t.task_code);
1004
- const existingCodes = storage.tasks.getExistingTaskCodes(owner, repo, allCodes);
1005
- const initialStats = storage.taskStats.getTaskStats(owner, repo);
1006
- let pendingInRequestCount = 0;
1007
- const localCodeMap = /* @__PURE__ */ new Map();
1008
- for (const taskData of bulkTasks) {
1009
- localCodeMap.set(taskData.task_code, randomUUID2());
1010
941
  }
1011
- for (const taskData of bulkTasks) {
1012
- const code = taskData.task_code;
1013
- if (codesInRequest.has(code)) {
1014
- throw new Error(`Duplicate task_code in request: '${code}'`);
1015
- }
1016
- if (existingCodes.has(code)) {
1017
- throw new Error(`Duplicate task_code: '${code}' already exists in repository '${repo}'`);
1018
- }
1019
- codesInRequest.add(code);
1020
- const normalizedStatus = taskData.status || "backlog";
1021
- if (normalizedStatus !== "backlog" && normalizedStatus !== "pending") {
1022
- throw new Error(`New tasks must be 'backlog' or 'pending'. Task '${code}' has status '${normalizedStatus}'.`);
1023
- }
1024
- if (normalizedStatus === "pending") {
1025
- if (initialStats.todo + pendingInRequestCount >= 10) {
1026
- throw new Error(
1027
- `Cannot create task '${code}' as 'pending'. Maximum of 10 pending tasks reached. Please use status 'backlog' for new tasks instead.`
1028
- );
1029
- }
1030
- }
1031
- const statusTimestamps2 = deriveTaskStatusTimestamps(normalizedStatus, now2);
1032
- const tags = [...taskData.tags || []];
1033
- const phaseTag2 = `phase:${taskData.phase}`;
1034
- if (!tags.includes(phaseTag2)) {
1035
- tags.push(phaseTag2);
1036
- }
1037
- const taskId2 = localCodeMap.get(code);
1038
- const task2 = {
1039
- id: taskId2,
1040
- owner,
1041
- repo,
1042
- task_code: code,
1043
- phase: taskData.phase,
1044
- title: taskData.title,
1045
- description: taskData.description,
1046
- status: normalizedStatus,
1047
- priority: taskData.priority || 3,
1048
- agent: taskData.agent || singleTask.agent || "unknown",
1049
- role: taskData.role || singleTask.role || "unknown",
1050
- doc_path: taskData.doc_path || null,
1051
- created_at: now2,
1052
- updated_at: now2,
1053
- in_progress_at: statusTimestamps2.in_progress_at,
1054
- finished_at: statusTimestamps2.finished_at,
1055
- canceled_at: statusTimestamps2.canceled_at,
1056
- est_tokens: taskData.est_tokens ?? 0,
1057
- tags,
1058
- suggested_skills: taskData.suggested_skills || [],
1059
- commit_id: null,
1060
- changed_files: [],
1061
- metadata: taskData.metadata || {},
1062
- parent_id: resolveParentId(taskData.parent_id, owner, repo, storage, localCodeMap),
1063
- depends_on: resolveDependsOn(taskData.depends_on, owner, repo, storage, localCodeMap)
1064
- };
1065
- tasksToInsert.push(task2);
1066
- createdTasks.push(code);
1067
- if (normalizedStatus === "pending") {
1068
- pendingInRequestCount++;
942
+ ];
943
+ const toolDefinitions = buildSamplingTools(session2, validated.use_tools);
944
+ let lastResponse = null;
945
+ let totalToolCalls = 0;
946
+ let iterations = 0;
947
+ while (iterations < validated.max_iterations) {
948
+ iterations += 1;
949
+ const response = await options.sampleMessage({
950
+ messages,
951
+ systemPrompt,
952
+ maxTokens: validated.max_tokens,
953
+ tools: toolDefinitions.length > 0 ? toolDefinitions : void 0,
954
+ toolChoice: toolDefinitions.length > 0 ? { mode: iterations === validated.max_iterations ? "none" : "auto" } : void 0,
955
+ modelPreferences: {
956
+ intelligencePriority: 0.9,
957
+ speedPriority: 0.4
1069
958
  }
959
+ });
960
+ lastResponse = response;
961
+ messages.push({
962
+ role: "assistant",
963
+ content: response.content
964
+ });
965
+ const toolUses = extractToolUses(response.content);
966
+ if (toolUses.length === 0) {
967
+ break;
1070
968
  }
1071
- storage.tasks.bulkInsertTasks(tasksToInsert);
1072
- const taskCodesStr = createdTasks.length > 0 ? `: ${createdTasks.join(", ")}` : "";
1073
- return createMcpResponse(
1074
- { success: true, repo, createdCount: bulkTasks.length, taskCodes: createdTasks },
1075
- `Created ${bulkTasks.length} tasks in repo "${repo}"${taskCodesStr}.`,
1076
- { includeSerializedStructuredContent: parsed.structured || false }
1077
- );
1078
- }
1079
- const {
1080
- task_code,
1081
- phase,
1082
- title,
1083
- description,
1084
- status,
1085
- priority,
1086
- agent,
1087
- role,
1088
- doc_path,
1089
- metadata,
1090
- parent_id,
1091
- depends_on,
1092
- est_tokens
1093
- } = singleTask;
1094
- if (!phase || !title || !description) {
1095
- throw new Error("Missing required fields for single task creation (phase, title, description)");
1096
- }
1097
- const resolvedCode = task_code || generateNextCode(owner ?? "", repo, "task", storage);
1098
- if (storage.tasks.isTaskCodeDuplicate(owner, repo, resolvedCode)) {
1099
- throw new Error(`Duplicate task_code: '${resolvedCode}' already exists in repository '${repo}'`);
1100
- }
1101
- if (status !== "backlog" && status !== "pending" && status !== void 0) {
1102
- throw new Error("New tasks must be created with status 'backlog' or 'pending'.");
1103
- }
1104
- if (status === "pending") {
1105
- const stats = storage.taskStats.getTaskStats(owner, repo);
1106
- if (stats.todo >= 10) {
1107
- throw new Error(
1108
- `Cannot create task as 'pending'. Maximum of 10 pending tasks reached. Please use status 'backlog' for new tasks instead.`
1109
- );
1110
- }
969
+ totalToolCalls += toolUses.length;
970
+ const toolResults = await Promise.all(
971
+ toolUses.map(async (toolUse) => ({
972
+ type: "tool_result",
973
+ toolUseId: toolUse.id,
974
+ content: [
975
+ {
976
+ type: "text",
977
+ text: await executeSamplingTool(toolUse.name, toolUse.input, db2, vectors2, repoOwner)
978
+ }
979
+ ]
980
+ }))
981
+ );
982
+ messages.push({
983
+ role: "user",
984
+ content: toolResults
985
+ });
1111
986
  }
1112
- const taskId = randomUUID2();
1113
- const now = (/* @__PURE__ */ new Date()).toISOString();
1114
- const statusTimestamps = deriveTaskStatusTimestamps(status || "backlog", now);
1115
- const finalTags = [...singleTask.tags || []];
1116
- const phaseTag = `phase:${phase}`;
1117
- if (!finalTags.includes(phaseTag)) {
1118
- finalTags.push(phaseTag);
987
+ const answer = lastResponse ? extractTextFromContent(lastResponse.content).trim() : "";
988
+ if (!answer) {
989
+ throw new Error("Sampling did not return a final text answer");
1119
990
  }
1120
- const task = {
1121
- id: taskId,
1122
- owner,
991
+ logger.info("[Tool] memory.synthesize", {
1123
992
  repo,
1124
- task_code: resolvedCode,
1125
- phase,
1126
- title,
1127
- description,
1128
- status: status || "backlog",
1129
- priority: priority || 3,
1130
- agent: agent || "unknown",
1131
- role: role || "unknown",
1132
- doc_path: doc_path || null,
1133
- created_at: now,
1134
- updated_at: now,
1135
- in_progress_at: statusTimestamps.in_progress_at,
1136
- finished_at: statusTimestamps.finished_at,
1137
- canceled_at: statusTimestamps.canceled_at,
1138
- est_tokens: est_tokens ?? 0,
1139
- tags: finalTags,
1140
- suggested_skills: singleTask.suggested_skills || [],
1141
- commit_id: null,
1142
- changed_files: [],
1143
- metadata: metadata || {},
1144
- parent_id: resolveParentId(parent_id, owner, repo, storage),
1145
- depends_on: resolveDependsOn(depends_on, owner, repo, storage)
1146
- };
1147
- storage.tasks.insertTask(task);
993
+ objective: validated.objective,
994
+ iterations,
995
+ toolCalls: totalToolCalls
996
+ });
1148
997
  return createMcpResponse(
1149
998
  {
1150
- success: true,
1151
- id: task.id,
1152
- repo: task.repo,
1153
- task_code: task.task_code,
1154
- phase: task.phase,
1155
- title: task.title,
1156
- status: task.status,
1157
- priority: task.priority,
1158
- depends_on: task.depends_on
999
+ repo,
1000
+ objective: validated.objective,
1001
+ answer,
1002
+ model: lastResponse?.model,
1003
+ stopReason: lastResponse?.stopReason,
1004
+ iterations,
1005
+ toolCalls: totalToolCalls
1159
1006
  },
1160
- `Created task [${task.task_code}] ${task.title} in repo "${task.repo}" with status "${task.status}".`,
1161
- { includeSerializedStructuredContent: parsed.structured || false }
1007
+ `Synthesized answer for "${validated.objective}" using repository "${repo}".`,
1008
+ {
1009
+ structuredContentPathHint: "answer",
1010
+ includeSerializedStructuredContent: true
1011
+ }
1162
1012
  );
1163
1013
  }
1164
- async function handleTaskCreateInteractive(args, storage, options = {}) {
1165
- const partialTaskData = TaskCreateInteractiveSchema.parse(args ?? {});
1166
- const inferredRepo = partialTaskData.repo ?? inferRepoFromSession(options.session);
1167
- const draft = {
1168
- ...partialTaskData,
1169
- repo: inferredRepo
1170
- };
1171
- const requestedSchema = buildMissingTaskSchema(draft);
1172
- let completedDraft = draft;
1173
- if (Object.keys(requestedSchema.properties).length > 0) {
1174
- if (!options.session?.supportsElicitationForm || !options.elicit) {
1175
- throw new Error("Client does not advertise MCP elicitation form support");
1176
- }
1177
- const elicited = extractAcceptedElicitationContent(
1178
- await options.elicit({
1179
- mode: "form",
1180
- message: "Please complete the missing task details to create a new task.",
1181
- requestedSchema
1182
- })
1183
- );
1184
- completedDraft = {
1185
- ...draft,
1186
- ...elicited
1187
- };
1014
+ async function resolveRepository(repo, session2, elicit) {
1015
+ if (repo) return normalizeRepo(repo);
1016
+ const inferredRepo = inferRepoFromSession(session2);
1017
+ if (inferredRepo) return normalizeRepo(inferredRepo);
1018
+ if (!session2?.supportsElicitationForm || !elicit) {
1019
+ return void 0;
1188
1020
  }
1189
- return await handleTaskCreate(
1190
- {
1191
- ...completedDraft,
1192
- status: completedDraft.status ?? "backlog",
1193
- priority: completedDraft.priority ?? 3,
1194
- structured: true
1195
- },
1196
- storage
1021
+ const elicited = extractAcceptedElicitationContent(
1022
+ await elicit({
1023
+ mode: "form",
1024
+ message: "Repository tidak bisa diinfer dari roots aktif. Pilih repository yang ingin disintesis.",
1025
+ requestedSchema: {
1026
+ type: "object",
1027
+ properties: {
1028
+ repo: {
1029
+ type: "string",
1030
+ title: "Repository",
1031
+ description: "Nama repository yang akan dipakai untuk sintesis memori.",
1032
+ minLength: 1
1033
+ }
1034
+ },
1035
+ required: ["repo"]
1036
+ }
1037
+ })
1197
1038
  );
1039
+ return typeof elicited.repo === "string" && elicited.repo.trim() ? normalizeRepo(elicited.repo.trim()) : void 0;
1198
1040
  }
1199
- function buildMissingTaskSchema(task) {
1200
- const properties = {};
1201
- const required = [];
1202
- addRequiredStringField(properties, required, task, "repo", {
1203
- title: "Repository",
1204
- description: "Name of the repository for this task.",
1205
- minLength: 1
1206
- });
1207
- addRequiredStringField(properties, required, task, "phase", {
1208
- title: "Phase",
1209
- description: "Project phase or milestone for this task.",
1210
- minLength: 1
1211
- });
1212
- addRequiredStringField(properties, required, task, "title", {
1213
- title: "Title",
1214
- description: "Short task title.",
1215
- minLength: 3,
1216
- maxLength: 100
1217
- });
1218
- addRequiredStringField(properties, required, task, "description", {
1219
- title: "Description",
1220
- description: "Detailed description. MUST follow format: 1. Context & Analysis, 2. Step & Implementation, 3. Acceptance & Verification",
1221
- minLength: 1
1222
- });
1223
- if (!task.status) {
1224
- properties.status = {
1225
- type: "string",
1226
- title: "Status",
1227
- description: "Initial task status.",
1228
- enum: ["backlog", "pending"],
1229
- default: "backlog"
1230
- };
1231
- }
1232
- if (task.priority === void 0) {
1233
- properties.priority = {
1234
- type: "integer",
1235
- title: "Priority",
1236
- description: "Task priority from 1 to 5.",
1237
- minimum: 1,
1238
- maximum: 5,
1239
- default: 3
1240
- };
1041
+ function buildSamplingTools(session2, useTools) {
1042
+ if (!useTools || !session2?.supportsSamplingTools) {
1043
+ return [];
1241
1044
  }
1242
- return {
1243
- type: "object",
1244
- properties,
1245
- required
1246
- };
1045
+ return [
1046
+ {
1047
+ name: "memory_search",
1048
+ description: "Search local repository memories for relevant context.",
1049
+ inputSchema: {
1050
+ type: "object",
1051
+ properties: {
1052
+ repo: { type: "string" },
1053
+ query: { type: "string" },
1054
+ limit: { type: "number", minimum: 1, maximum: 10 }
1055
+ },
1056
+ required: ["repo", "query"]
1057
+ }
1058
+ },
1059
+ {
1060
+ name: "memory_recap",
1061
+ description: "Fetch a recap of the most recent memories and active tasks.",
1062
+ inputSchema: {
1063
+ type: "object",
1064
+ properties: {
1065
+ repo: { type: "string" },
1066
+ limit: { type: "number", minimum: 1, maximum: 20 }
1067
+ },
1068
+ required: ["repo"]
1069
+ }
1070
+ },
1071
+ {
1072
+ name: "task_list",
1073
+ description: "List tasks by status or search term for the repository.",
1074
+ inputSchema: {
1075
+ type: "object",
1076
+ properties: {
1077
+ repo: { type: "string" },
1078
+ status: { type: "string" },
1079
+ search: { type: "string" },
1080
+ limit: { type: "number", minimum: 1, maximum: 20 }
1081
+ },
1082
+ required: ["repo"]
1083
+ }
1084
+ }
1085
+ ];
1247
1086
  }
1248
- function addRequiredStringField(properties, required, task, field, schema) {
1249
- if (typeof task[field] === "string" && task[field].trim()) {
1250
- return;
1251
- }
1252
- properties[field] = {
1253
- type: "string",
1254
- ...schema
1255
- };
1256
- required.push(field);
1257
- }
1258
- async function handleTaskUpdate(args, storage, vectors2) {
1259
- const updateData = TaskUpdateSchema.parse(args);
1260
- const { owner, repo, id, ids, comment, force, ...updates } = updateData;
1261
- let resolvedId = id;
1262
- if (!resolvedId && !ids && updates.task_code) {
1263
- const found = storage.tasks.getTaskByCode(owner, repo, updates.task_code);
1264
- if (!found) throw new Error(`Task not found: ${updates.task_code}`);
1265
- resolvedId = found.id;
1266
- }
1267
- const targetIds = ids || (resolvedId ? [resolvedId] : []);
1268
- if (targetIds.length === 0) {
1269
- throw new Error("Either 'id' or 'ids' must be provided for update");
1270
- }
1271
- let updatedCount = 0;
1272
- const updatedTasks = [];
1273
- const completedTaskIds = [];
1274
- let releasedClaims = 0;
1275
- let expiredHandoffs = 0;
1276
- const now = (/* @__PURE__ */ new Date()).toISOString();
1277
- const isStatusChangingGlobal = updates.status !== void 0;
1278
- const existingTasks = storage.tasks.getTasksByIds(targetIds);
1279
- const taskMap = new Map(existingTasks.map((t) => [t.id, t]));
1280
- for (const targetId of targetIds) {
1281
- const existingTask = taskMap.get(targetId);
1282
- if (!existingTask) {
1283
- if (id) throw new Error(`Task not found: ${targetId}`);
1284
- continue;
1285
- }
1286
- const isStatusChanging = isStatusChangingGlobal && updates.status !== existingTask.status;
1287
- if (isStatusChanging && !force) {
1288
- if (!comment || comment.trim() === "") {
1289
- throw new Error("comment is required when changing task status");
1290
- }
1291
- const isStartable = existingTask.status === "backlog" || existingTask.status === "pending" || existingTask.status === "blocked";
1292
- if (isStartable && updates.status === "completed") {
1293
- throw new Error(
1294
- `Cannot transition task ${targetId} from '${existingTask.status}' directly to 'completed'. Must be 'in_progress' first.`
1295
- );
1296
- }
1297
- }
1298
- if (updates.status === "completed" && isStatusChanging) {
1299
- if (updates.est_tokens === void 0) {
1300
- throw new Error("est_tokens is required when changing task status to completed");
1301
- }
1302
- const children = storage.tasks.getChildrenByParentId(targetId);
1303
- const incompleteChildren = children.filter((c) => c.status !== "completed");
1304
- if (incompleteChildren.length > 0) {
1305
- const childList = incompleteChildren.map((c) => `[${c.task_code}] ${c.title} (${c.status})`).join("; ");
1306
- throw new Error(
1307
- `Cannot complete task [${existingTask.task_code}] "${existingTask.title}" \u2014 it has ${incompleteChildren.length} incomplete child task(s). Complete the following child task(s) first: ${childList}`
1308
- );
1309
- }
1310
- }
1311
- if (updates.task_code && storage.tasks.isTaskCodeDuplicate(owner, repo, updates.task_code, targetId)) {
1312
- throw new Error(`Duplicate task_code: '${updates.task_code}' already exists`);
1313
- }
1314
- const finalUpdates = { ...updates };
1315
- if (updates.parent_id !== void 0) {
1316
- finalUpdates.parent_id = resolveParentId(updates.parent_id, owner, repo, storage);
1317
- }
1318
- if (updates.depends_on !== void 0) {
1319
- finalUpdates.depends_on = resolveDependsOn(updates.depends_on, owner, repo, storage);
1320
- }
1321
- if (updates.phase !== void 0 || updates.tags !== void 0) {
1322
- let currentTags = updates.tags || existingTask.tags || [];
1323
- currentTags = currentTags.filter((t) => !t.startsWith("phase:"));
1324
- const finalPhase = updates.phase !== void 0 ? updates.phase : existingTask.phase;
1325
- if (finalPhase) {
1326
- const phaseTag = `phase:${finalPhase}`;
1327
- if (!currentTags.includes(phaseTag)) {
1328
- currentTags.push(phaseTag);
1329
- }
1330
- }
1331
- finalUpdates.tags = currentTags;
1332
- }
1333
- if (updates.status === "completed") {
1334
- finalUpdates.finished_at = now;
1335
- finalUpdates.commit_id = updates.commit_id;
1336
- finalUpdates.changed_files = updates.changed_files;
1337
- } else if (updates.status === "canceled") finalUpdates.canceled_at = now;
1338
- else if (updates.status === "in_progress" && existingTask.status !== "in_progress")
1339
- finalUpdates.in_progress_at = now;
1340
- storage.tasks.updateTask(targetId, finalUpdates);
1341
- if (comment !== void 0 || isStatusChanging) {
1342
- storage.taskComments.insertTaskComment({
1343
- id: randomUUID2(),
1344
- task_id: targetId,
1345
- owner,
1346
- repo,
1347
- comment: comment || `Status updated to ${updates.status}`,
1348
- agent: updates.agent || existingTask.agent || "unknown",
1349
- role: updates.role || existingTask.role || "unknown",
1350
- model: updates.model || "unknown",
1351
- previous_status: isStatusChanging ? existingTask.status : null,
1352
- next_status: isStatusChanging ? updates.status : null,
1353
- created_at: now
1354
- });
1087
+ async function executeSamplingTool(toolName, rawInput, db2, vectors2, owner) {
1088
+ switch (toolName) {
1089
+ case "memory_search": {
1090
+ const response = await handleMemorySearch(
1091
+ {
1092
+ owner,
1093
+ repo: rawInput.repo,
1094
+ query: rawInput.query,
1095
+ limit: rawInput.limit ?? 5
1096
+ },
1097
+ db2,
1098
+ vectors2
1099
+ );
1100
+ return getPrimaryTextContent(response);
1355
1101
  }
1356
- if (updates.status === "completed" && existingTask.status !== "completed") {
1357
- completedTaskIds.push(targetId);
1102
+ case "memory_recap": {
1103
+ const response = await handleMemoryRecap(
1104
+ {
1105
+ owner,
1106
+ repo: rawInput.repo,
1107
+ limit: rawInput.limit ?? 8,
1108
+ offset: 0
1109
+ },
1110
+ db2
1111
+ );
1112
+ return getPrimaryTextContent(response);
1358
1113
  }
1359
- if (isStatusChanging && (updates.status === "completed" || updates.status === "canceled")) {
1360
- releasedClaims += storage.handoffs.releaseClaimsForTask(targetId);
1361
- expiredHandoffs += storage.handoffs.updatePendingHandoffsForTask(targetId, "expired");
1114
+ case "task_list": {
1115
+ const response = await handleTaskList(
1116
+ {
1117
+ owner,
1118
+ repo: rawInput.repo,
1119
+ status: rawInput.status,
1120
+ search: rawInput.search,
1121
+ limit: rawInput.limit ?? 10,
1122
+ offset: 0
1123
+ },
1124
+ db2
1125
+ );
1126
+ return getPrimaryTextContent(response);
1362
1127
  }
1363
- updatedTasks.push({ id: targetId, code: updates.task_code || existingTask.task_code });
1364
- updatedCount++;
1365
- }
1366
- const taskCodesStr = updatedTasks.length > 0 ? ` Tasks: ${updatedTasks.map((t) => t.code).join(", ")}.` : "";
1367
- const fieldsStr = Object.keys(updates).length > 0 ? ` Fields: ${Object.keys(updates).join(", ")}.` : "";
1368
- const isCompleted = updates.status === "completed" && updatedCount > 0;
1369
- let summaryText = isCompleted ? `Updated ${updatedCount} task(s) in repo "${repo}". Task marked as completed with commit ${updates.commit_id} (${(updates.changed_files || []).length} files changed).` : `Updated ${updatedCount} task(s) in repo "${repo}".`;
1370
- summaryText += `${taskCodesStr}${fieldsStr}`;
1371
- if (releasedClaims || expiredHandoffs) {
1372
- summaryText += ` Auto-closed coordination: released ${releasedClaims} claim(s), expired ${expiredHandoffs} handoff(s).`;
1373
- }
1374
- const response = createMcpResponse(
1375
- {
1376
- success: true,
1377
- id: id || void 0,
1378
- ids: ids || void 0,
1379
- repo,
1380
- status: updates.status,
1381
- updatedCount,
1382
- updatedFields: Object.keys(updates),
1383
- coordinationCleanup: {
1384
- releasedClaims,
1385
- expiredHandoffs
1386
- }
1387
- },
1388
- summaryText,
1389
- { includeSerializedStructuredContent: updateData.structured }
1390
- );
1391
- if (completedTaskIds.length > 0) {
1392
- setImmediate(async () => {
1393
- for (const taskId of completedTaskIds) {
1394
- try {
1395
- await archiveTaskToMemory(taskId, repo, storage, vectors2);
1396
- } catch (err) {
1397
- logger.error("Failed to archive task to memory", { taskId, error: String(err) });
1398
- }
1399
- }
1400
- });
1128
+ default:
1129
+ throw new Error(`Unsupported sampling tool: ${toolName}`);
1401
1130
  }
1402
- return response;
1403
1131
  }
1404
- async function handleTaskDelete(args, storage) {
1405
- const validated = TaskDeleteSchema.parse(args);
1406
- const { owner, repo, id, ids, task_code } = validated;
1132
+
1133
+ // src/mcp/tools/memory.delete.ts
1134
+ async function handleMemoryDelete(params, db2, vectors2, onProgress) {
1135
+ const validated = MemoryDeleteSchema.parse(params);
1136
+ const { id, ids, code, codes, owner, repo, structured } = validated;
1407
1137
  const resolvedIds = [];
1408
1138
  if (ids) resolvedIds.push(...ids);
1409
1139
  if (id) resolvedIds.push(id);
1410
- if (task_code) {
1411
- const task = storage.tasks.getTaskByCode(owner, repo, task_code);
1412
- if (!task) throw new Error(`Task not found: ${task_code}`);
1413
- resolvedIds.push(task.id);
1140
+ if (code) {
1141
+ const entry = db2.memories.getByCode(code, owner, repo);
1142
+ if (!entry) throw new Error(`Memory not found: ${code}`);
1143
+ resolvedIds.push(entry.id);
1144
+ }
1145
+ if (codes) {
1146
+ for (const c of codes) {
1147
+ const entry = db2.memories.getByCode(c, owner, repo);
1148
+ if (!entry) throw new Error(`Memory not found: ${c}`);
1149
+ resolvedIds.push(entry.id);
1150
+ }
1414
1151
  }
1415
1152
  if (resolvedIds.length === 0) {
1416
- throw new Error("Either 'id', 'ids', or 'task_code' must be provided for deletion");
1153
+ throw new Error("Either 'id', 'ids', 'code', or 'codes' must be provided for deletion");
1417
1154
  }
1418
1155
  const targetIds = resolvedIds;
1419
- const tasksToDelete = storage.tasks.getTasksByIds(targetIds);
1420
- const deletedCodes = tasksToDelete.map((t) => t.task_code);
1156
+ let deletedCount = 0;
1157
+ const deletedCodes = [];
1158
+ let lastRepo = repo || "unknown";
1159
+ const total = targetIds.length;
1160
+ let progress = 0;
1161
+ const existingMemories = db2.memories.getByIds(targetIds);
1162
+ const existingMap = new Map(existingMemories.map((m) => [m.id, m]));
1163
+ const validIdsToDelete = [];
1421
1164
  for (const targetId of targetIds) {
1422
- storage.tasks.deleteTask(targetId);
1165
+ const existing = existingMap.get(targetId);
1166
+ if (existing) {
1167
+ lastRepo = existing.scope.repo;
1168
+ deletedCodes.push(existing.code || existing.id);
1169
+ validIdsToDelete.push(targetId);
1170
+ } else if (id) {
1171
+ throw new Error(`Memory not found: ${targetId}`);
1172
+ }
1423
1173
  }
1424
- const deletedCodesStr = deletedCodes.length > 0 ? `: ${deletedCodes.join(", ")}` : "";
1174
+ if (validIdsToDelete.length > 0) {
1175
+ db2.memoryArchives.bulkDeleteMemories(validIdsToDelete);
1176
+ for (const validId of validIdsToDelete) {
1177
+ if (onProgress) {
1178
+ onProgress(progress, total);
1179
+ }
1180
+ await vectors2.remove(validId);
1181
+ progress++;
1182
+ }
1183
+ deletedCount = validIdsToDelete.length;
1184
+ }
1185
+ if (onProgress) {
1186
+ onProgress(progress, total);
1187
+ }
1188
+ logger.info("[Tool] memory.delete", { repo: lastRepo, count: deletedCount });
1425
1189
  return createMcpResponse(
1426
1190
  {
1427
1191
  success: true,
1428
1192
  id: id || void 0,
1429
1193
  ids: ids || void 0,
1430
- repo,
1431
- deletedCount: targetIds.length,
1432
- deletedCodes
1194
+ repo: lastRepo,
1195
+ deletedCount,
1196
+ deletedCodes: deletedCount > 10 ? [...deletedCodes.slice(0, 10), "..."] : deletedCodes
1433
1197
  },
1434
- `Deleted ${targetIds.length} task(s) from repo "${repo}"${deletedCodesStr}.`,
1435
- { includeSerializedStructuredContent: validated.structured }
1198
+ `Deleted ${deletedCount} memory entry(ies) from repo "${lastRepo}".`,
1199
+ {
1200
+ structuredContentPathHint: "deletedCount",
1201
+ includeSerializedStructuredContent: structured
1202
+ }
1436
1203
  );
1437
1204
  }
1438
1205
 
1439
- // src/mcp/tools/memory.synthesize.ts
1440
- async function handleMemorySynthesize(params, db2, vectors2, options = {}) {
1441
- const validated = MemorySynthesizeSchema.parse(params);
1442
- const session2 = options.session;
1443
- if (!options.sampleMessage || !session2?.supportsSampling) {
1444
- throw new Error("Client does not advertise MCP sampling support");
1206
+ // src/mcp/tools/memory.acknowledge.ts
1207
+ async function handleMemoryAcknowledge(params, db2) {
1208
+ const validated = MemoryAcknowledgeSchema.parse(params);
1209
+ let memoryId = validated.memory_id;
1210
+ if (!memoryId && validated.code) {
1211
+ const byCode = db2.memories.getByCode(validated.code, validated.owner, validated.repo);
1212
+ if (!byCode) throw new Error(`Memory not found: ${validated.code}`);
1213
+ memoryId = byCode.id;
1214
+ } else if (!memoryId) {
1215
+ throw new Error("Either memory_id or code must be provided");
1445
1216
  }
1446
- const repo = await resolveRepository(validated.repo, session2, options.elicit);
1447
- if (!repo) {
1448
- throw new Error("repo is required when repo cannot be inferred from active MCP roots");
1217
+ const memory = db2.memories.getById(memoryId);
1218
+ if (!memory) {
1219
+ throw new Error(`Memory with ID ${memoryId} not found.`);
1449
1220
  }
1450
- const repoOwner = validated.owner;
1451
- const recap = await handleMemoryRecap({ owner: repoOwner, repo, limit: 8, offset: 0 }, db2);
1452
- const recapText = getPrimaryTextContent(recap);
1453
- const summary = validated.include_summary ? db2.summaries.getSummary(repoOwner, repo)?.summary : "";
1454
- const taskSnapshot = validated.include_tasks ? await handleTaskList(
1455
- { owner: repoOwner, repo, status: "backlog,pending,in_progress,blocked", limit: 15, offset: 0 },
1456
- db2
1457
- ) : null;
1458
- const taskText = taskSnapshot ? getPrimaryTextContent(taskSnapshot) : "";
1459
- const systemPrompt = [
1460
- "You are a repository memory synthesizer.",
1461
- "Answer strictly from grounded MCP context and tool results.",
1462
- "If memory is insufficient, say so explicitly instead of inventing details.",
1463
- "Prefer concise, technical answers with explicit caveats when evidence is incomplete."
1464
- ].join(" ");
1465
- const contextBlock = [
1466
- `Repository: ${repo}`,
1467
- validated.current_file_path ? `Current file: ${validated.current_file_path}` : "",
1468
- summary ? `Summary:
1469
- ${summary}` : "",
1470
- recapText ? `Recent context:
1471
- ${recapText}` : "",
1472
- taskText ? `Active tasks:
1473
- ${taskText}` : ""
1474
- ].filter(Boolean).join("\n\n");
1475
- const messages = [
1476
- {
1477
- role: "user",
1478
- content: {
1479
- type: "text",
1480
- text: `Objective: ${validated.objective}
1481
-
1482
- Grounding context:
1483
- ${contextBlock || "No additional context provided."}`
1484
- }
1485
- }
1486
- ];
1487
- const toolDefinitions = buildSamplingTools(session2, validated.use_tools);
1488
- let lastResponse = null;
1489
- let totalToolCalls = 0;
1490
- let iterations = 0;
1491
- while (iterations < validated.max_iterations) {
1492
- iterations += 1;
1493
- const response = await options.sampleMessage({
1494
- messages,
1495
- systemPrompt,
1496
- maxTokens: validated.max_tokens,
1497
- tools: toolDefinitions.length > 0 ? toolDefinitions : void 0,
1498
- toolChoice: toolDefinitions.length > 0 ? { mode: iterations === validated.max_iterations ? "none" : "auto" } : void 0,
1499
- modelPreferences: {
1500
- intelligencePriority: 0.9,
1501
- speedPriority: 0.4
1502
- }
1503
- });
1504
- lastResponse = response;
1505
- messages.push({
1506
- role: "assistant",
1507
- content: response.content
1508
- });
1509
- const toolUses = extractToolUses(response.content);
1510
- if (toolUses.length === 0) {
1511
- break;
1512
- }
1513
- totalToolCalls += toolUses.length;
1514
- const toolResults = await Promise.all(
1515
- toolUses.map(async (toolUse) => ({
1516
- type: "tool_result",
1517
- toolUseId: toolUse.id,
1518
- content: [
1519
- {
1520
- type: "text",
1521
- text: await executeSamplingTool(toolUse.name, toolUse.input, db2, vectors2, repoOwner)
1522
- }
1523
- ]
1524
- }))
1525
- );
1526
- messages.push({
1527
- role: "user",
1528
- content: toolResults
1221
+ if (validated.status === "used") {
1222
+ db2.memories.incrementRecallCount(memory.id);
1223
+ logger.info("[Tool] memory.acknowledge - used", { id: memory.id, context: validated.application_context });
1224
+ } else if (validated.status === "contradictory") {
1225
+ logger.warn("[Tool] memory.acknowledge - contradiction reported", {
1226
+ id: memory.id,
1227
+ context: validated.application_context
1529
1228
  });
1229
+ } else {
1230
+ logger.info("[Tool] memory.acknowledge - irrelevant", { id: memory.id, context: validated.application_context });
1530
1231
  }
1531
- const answer = lastResponse ? extractTextFromContent(lastResponse.content).trim() : "";
1532
- if (!answer) {
1533
- throw new Error("Sampling did not return a final text answer");
1534
- }
1535
- logger.info("[Tool] memory.synthesize", {
1536
- repo,
1537
- objective: validated.objective,
1538
- iterations,
1539
- toolCalls: totalToolCalls
1540
- });
1541
1232
  return createMcpResponse(
1542
1233
  {
1543
- repo,
1544
- objective: validated.objective,
1545
- answer,
1546
- model: lastResponse?.model,
1547
- stopReason: lastResponse?.stopReason,
1548
- iterations,
1549
- toolCalls: totalToolCalls
1234
+ success: true,
1235
+ id: memory.id,
1236
+ status: validated.status
1550
1237
  },
1551
- `Synthesized answer for "${validated.objective}" using repository "${repo}".`,
1238
+ `Acknowledged memory [${memory.code}] as "${validated.status}".`,
1552
1239
  {
1553
- structuredContentPathHint: "answer",
1554
- includeSerializedStructuredContent: true
1240
+ structuredContentPathHint: "status",
1241
+ includeSerializedStructuredContent: validated.structured
1555
1242
  }
1556
1243
  );
1557
1244
  }
1558
- async function resolveRepository(repo, session2, elicit) {
1559
- if (repo) return normalizeRepo(repo);
1560
- const inferredRepo = inferRepoFromSession(session2);
1561
- if (inferredRepo) return normalizeRepo(inferredRepo);
1562
- if (!session2?.supportsElicitationForm || !elicit) {
1563
- return void 0;
1245
+
1246
+ // src/mcp/tools/memory.detail.ts
1247
+ async function handleMemoryDetail(args, storage) {
1248
+ const validated = MemoryDetailSchema.parse(args);
1249
+ const { id, code, owner, repo } = validated;
1250
+ let memory;
1251
+ if (id) {
1252
+ memory = storage.memories.getById(id);
1253
+ } else if (code) {
1254
+ memory = storage.memories.getByCode(code, owner, repo);
1564
1255
  }
1565
- const elicited = extractAcceptedElicitationContent(
1566
- await elicit({
1567
- mode: "form",
1568
- message: "Repository tidak bisa diinfer dari roots aktif. Pilih repository yang ingin disintesis.",
1569
- requestedSchema: {
1570
- type: "object",
1571
- properties: {
1572
- repo: {
1573
- type: "string",
1574
- title: "Repository",
1575
- description: "Nama repository yang akan dipakai untuk sintesis memori.",
1576
- minLength: 1
1577
- }
1578
- },
1579
- required: ["repo"]
1580
- }
1581
- })
1582
- );
1583
- return typeof elicited.repo === "string" && elicited.repo.trim() ? normalizeRepo(elicited.repo.trim()) : void 0;
1584
- }
1585
- function buildSamplingTools(session2, useTools) {
1586
- if (!useTools || !session2?.supportsSamplingTools) {
1587
- return [];
1256
+ if (!memory) {
1257
+ throw new Error(`Memory not found: ${id || code}`);
1588
1258
  }
1589
- return [
1590
- {
1591
- name: "memory_search",
1592
- description: "Search local repository memories for relevant context.",
1593
- inputSchema: {
1594
- type: "object",
1595
- properties: {
1596
- repo: { type: "string" },
1597
- query: { type: "string" },
1598
- limit: { type: "number", minimum: 1, maximum: 10 }
1599
- },
1600
- required: ["repo", "query"]
1601
- }
1602
- },
1603
- {
1604
- name: "memory_recap",
1605
- description: "Fetch a recap of the most recent memories and active tasks.",
1606
- inputSchema: {
1607
- type: "object",
1608
- properties: {
1609
- repo: { type: "string" },
1610
- limit: { type: "number", minimum: 1, maximum: 20 }
1611
- },
1612
- required: ["repo"]
1613
- }
1614
- },
1615
- {
1616
- name: "task_list",
1617
- description: "List tasks by status or search term for the repository.",
1618
- inputSchema: {
1619
- type: "object",
1620
- properties: {
1621
- repo: { type: "string" },
1622
- status: { type: "string" },
1623
- search: { type: "string" },
1624
- limit: { type: "number", minimum: 1, maximum: 20 }
1625
- },
1626
- required: ["repo"]
1627
- }
1628
- }
1259
+ storage.memories.incrementHitCount(memory.id);
1260
+ const lines = [
1261
+ `Code: ${memory.code || "-"}`,
1262
+ `ID: ${memory.id}`,
1263
+ `Title: ${memory.title}`,
1264
+ `Type: ${memory.type}`,
1265
+ `Importance: ${memory.importance}`,
1266
+ `Created: ${memory.created_at}`
1629
1267
  ];
1630
- }
1631
- async function executeSamplingTool(toolName, rawInput, db2, vectors2, owner) {
1632
- switch (toolName) {
1633
- case "memory_search": {
1634
- const response = await handleMemorySearch(
1635
- {
1636
- owner,
1637
- repo: rawInput.repo,
1638
- query: rawInput.query,
1639
- limit: rawInput.limit ?? 5
1640
- },
1641
- db2,
1642
- vectors2
1643
- );
1644
- return getPrimaryTextContent(response);
1645
- }
1646
- case "memory_recap": {
1647
- const response = await handleMemoryRecap(
1648
- {
1649
- owner,
1650
- repo: rawInput.repo,
1651
- limit: rawInput.limit ?? 8,
1652
- offset: 0
1653
- },
1654
- db2
1655
- );
1656
- return getPrimaryTextContent(response);
1657
- }
1658
- case "task_list": {
1659
- const response = await handleTaskList(
1660
- {
1661
- owner,
1662
- repo: rawInput.repo,
1663
- status: rawInput.status,
1664
- search: rawInput.search,
1665
- limit: rawInput.limit ?? 10,
1666
- offset: 0
1667
- },
1668
- db2
1669
- );
1670
- return getPrimaryTextContent(response);
1671
- }
1672
- default:
1673
- throw new Error(`Unsupported sampling tool: ${toolName}`);
1674
- }
1675
- }
1676
-
1677
- // src/mcp/tools/memory.delete.ts
1678
- async function handleMemoryDelete(params, db2, vectors2, onProgress) {
1679
- const validated = MemoryDeleteSchema.parse(params);
1680
- const { id, ids, code, codes, owner, repo, structured } = validated;
1681
- const resolvedIds = [];
1682
- if (ids) resolvedIds.push(...ids);
1683
- if (id) resolvedIds.push(id);
1684
- if (code) {
1685
- const entry = db2.memories.getByCode(code, owner, repo);
1686
- if (!entry) throw new Error(`Memory not found: ${code}`);
1687
- resolvedIds.push(entry.id);
1688
- }
1689
- if (codes) {
1690
- for (const c of codes) {
1691
- const entry = db2.memories.getByCode(c, owner, repo);
1692
- if (!entry) throw new Error(`Memory not found: ${c}`);
1693
- resolvedIds.push(entry.id);
1694
- }
1695
- }
1696
- if (resolvedIds.length === 0) {
1697
- throw new Error("Either 'id', 'ids', 'code', or 'codes' must be provided for deletion");
1698
- }
1699
- const targetIds = resolvedIds;
1700
- let deletedCount = 0;
1701
- const deletedCodes = [];
1702
- let lastRepo = repo || "unknown";
1703
- const total = targetIds.length;
1704
- let progress = 0;
1705
- const existingMemories = db2.memories.getByIds(targetIds);
1706
- const existingMap = new Map(existingMemories.map((m) => [m.id, m]));
1707
- const validIdsToDelete = [];
1708
- for (const targetId of targetIds) {
1709
- const existing = existingMap.get(targetId);
1710
- if (existing) {
1711
- lastRepo = existing.scope.repo;
1712
- deletedCodes.push(existing.code || existing.id);
1713
- validIdsToDelete.push(targetId);
1714
- } else if (id) {
1715
- throw new Error(`Memory not found: ${targetId}`);
1716
- }
1717
- }
1718
- if (validIdsToDelete.length > 0) {
1719
- db2.memoryArchives.bulkDeleteMemories(validIdsToDelete);
1720
- for (const validId of validIdsToDelete) {
1721
- if (onProgress) {
1722
- onProgress(progress, total);
1723
- }
1724
- await vectors2.remove(validId);
1725
- progress++;
1726
- }
1727
- deletedCount = validIdsToDelete.length;
1728
- }
1729
- if (onProgress) {
1730
- onProgress(progress, total);
1731
- }
1732
- logger.info("[Tool] memory.delete", { repo: lastRepo, count: deletedCount });
1733
- return createMcpResponse(
1734
- {
1735
- success: true,
1736
- id: id || void 0,
1737
- ids: ids || void 0,
1738
- repo: lastRepo,
1739
- deletedCount,
1740
- deletedCodes: deletedCount > 10 ? [...deletedCodes.slice(0, 10), "..."] : deletedCodes
1741
- },
1742
- `Deleted ${deletedCount} memory entry(ies) from repo "${lastRepo}".`,
1743
- {
1744
- structuredContentPathHint: "deletedCount",
1745
- includeSerializedStructuredContent: structured
1746
- }
1747
- );
1748
- }
1749
-
1750
- // src/mcp/tools/memory.acknowledge.ts
1751
- async function handleMemoryAcknowledge(params, db2) {
1752
- const validated = MemoryAcknowledgeSchema.parse(params);
1753
- let memoryId = validated.memory_id;
1754
- if (!memoryId && validated.code) {
1755
- const byCode = db2.memories.getByCode(validated.code, validated.owner, validated.repo);
1756
- if (!byCode) throw new Error(`Memory not found: ${validated.code}`);
1757
- memoryId = byCode.id;
1758
- } else if (!memoryId) {
1759
- throw new Error("Either memory_id or code must be provided");
1760
- }
1761
- const memory = db2.memories.getById(memoryId);
1762
- if (!memory) {
1763
- throw new Error(`Memory with ID ${memoryId} not found.`);
1764
- }
1765
- if (validated.status === "used") {
1766
- db2.memories.incrementRecallCount(memory.id);
1767
- logger.info("[Tool] memory.acknowledge - used", { id: memory.id, context: validated.application_context });
1768
- } else if (validated.status === "contradictory") {
1769
- logger.warn("[Tool] memory.acknowledge - contradiction reported", {
1770
- id: memory.id,
1771
- context: validated.application_context
1772
- });
1773
- } else {
1774
- logger.info("[Tool] memory.acknowledge - irrelevant", { id: memory.id, context: validated.application_context });
1775
- }
1776
- return createMcpResponse(
1777
- {
1778
- success: true,
1779
- id: memory.id,
1780
- status: validated.status
1781
- },
1782
- `Acknowledged memory [${memory.code}] as "${validated.status}".`,
1783
- {
1784
- structuredContentPathHint: "status",
1785
- includeSerializedStructuredContent: validated.structured
1786
- }
1787
- );
1788
- }
1789
-
1790
- // src/mcp/tools/memory.detail.ts
1791
- async function handleMemoryDetail(args, storage) {
1792
- const validated = MemoryDetailSchema.parse(args);
1793
- const { id, code, owner, repo } = validated;
1794
- let memory;
1795
- if (id) {
1796
- memory = storage.memories.getById(id);
1797
- } else if (code) {
1798
- memory = storage.memories.getByCode(code, owner, repo);
1799
- }
1800
- if (!memory) {
1801
- throw new Error(`Memory not found: ${id || code}`);
1802
- }
1803
- storage.memories.incrementHitCount(memory.id);
1804
- const lines = [
1805
- `Code: ${memory.code || "-"}`,
1806
- `ID: ${memory.id}`,
1807
- `Title: ${memory.title}`,
1808
- `Type: ${memory.type}`,
1809
- `Importance: ${memory.importance}`,
1810
- `Created: ${memory.created_at}`
1811
- ];
1812
- if (memory.scope?.repo) lines.push(`Repo: ${memory.scope.repo}`);
1813
- if (memory.scope?.folder) lines.push(`Folder: ${memory.scope.folder}`);
1814
- if (memory.content) {
1815
- lines.push("", "--- Content ---", memory.content);
1816
- }
1817
- const content = lines.join("\n");
1818
- return createMcpResponse(memory, content, {
1819
- contentSummary: content,
1820
- includeSerializedStructuredContent: validated.structured
1821
- });
1268
+ if (memory.scope?.repo) lines.push(`Repo: ${memory.scope.repo}`);
1269
+ if (memory.scope?.folder) lines.push(`Folder: ${memory.scope.folder}`);
1270
+ if (memory.content) {
1271
+ lines.push("", "--- Content ---", memory.content);
1272
+ }
1273
+ const content = lines.join("\n");
1274
+ return createMcpResponse(memory, content, {
1275
+ contentSummary: content,
1276
+ includeSerializedStructuredContent: validated.structured
1277
+ });
1822
1278
  }
1823
1279
 
1824
1280
  // src/mcp/tools/standard.store.ts
1825
- import { randomUUID as randomUUID3 } from "crypto";
1826
- var UUID_REGEX4 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1281
+ import { randomUUID as randomUUID2 } from "crypto";
1282
+ var UUID_REGEX3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1827
1283
  function resolveStandardParentId(value, db2, owner, repo) {
1828
1284
  if (!value) return null;
1829
- if (UUID_REGEX4.test(value)) return value;
1285
+ if (UUID_REGEX3.test(value)) return value;
1830
1286
  const standard = db2.standards.getByCode(value, owner, repo);
1831
1287
  if (!standard) throw new Error(`parent_id: standard with code '${value}' not found`);
1832
1288
  return standard.id;
@@ -1865,7 +1321,7 @@ async function storeSingleStandard(params, db2, vectors2) {
1865
1321
  }
1866
1322
  const now = (/* @__PURE__ */ new Date()).toISOString();
1867
1323
  const entry = {
1868
- id: randomUUID3(),
1324
+ id: randomUUID2(),
1869
1325
  code: generateNextCode(params.owner, params.repo || "__global__", "standard", db2),
1870
1326
  title: params.name,
1871
1327
  content: params.content,
@@ -1948,7 +1404,7 @@ async function handleStandardStore(params, db2, vectors2) {
1948
1404
  const code = generateNextCode(validated.owner ?? "", standardRepo, "standard", db2, batchCodes);
1949
1405
  batchCodes.add(code);
1950
1406
  entries.push({
1951
- id: randomUUID3(),
1407
+ id: randomUUID2(),
1952
1408
  code,
1953
1409
  title: std.name,
1954
1410
  content: std.content,
@@ -2222,169 +1678,724 @@ async function handleStandardSearch(params, db2, vectors2) {
2222
1678
  columns: [...COLUMNS],
2223
1679
  rows
2224
1680
  }
2225
- },
2226
- `Found ${results.length} coding standards matching your query`,
2227
- {
2228
- contentSummary,
2229
- structuredContentPathHint: "results",
2230
- includeSerializedStructuredContent: true
1681
+ },
1682
+ `Found ${results.length} coding standards matching your query`,
1683
+ {
1684
+ contentSummary,
1685
+ structuredContentPathHint: "results",
1686
+ includeSerializedStructuredContent: true
1687
+ }
1688
+ );
1689
+ }
1690
+
1691
+ // src/mcp/tools/standard.update.ts
1692
+ var UUID_REGEX4 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1693
+ function resolveStandardParentId2(value, db2, owner, repo) {
1694
+ if (!value) return null;
1695
+ if (UUID_REGEX4.test(value)) return value;
1696
+ const standard = db2.standards.getByCode(value, owner, repo);
1697
+ if (!standard) throw new Error(`parent_id: standard with code '${value}' not found`);
1698
+ return standard.id;
1699
+ }
1700
+ async function handleStandardUpdate(params, db2, vectors2) {
1701
+ const validated = StandardUpdateSchema.parse(params);
1702
+ let resolvedId = validated.id;
1703
+ if (!resolvedId && validated.code) {
1704
+ const byCode = db2.standards.getByCode(validated.code, validated.owner, validated.repo);
1705
+ if (!byCode) throw new Error(`Coding standard not found: ${validated.code}`);
1706
+ resolvedId = byCode.id;
1707
+ } else if (!resolvedId) {
1708
+ throw new Error("Either id or code must be provided");
1709
+ }
1710
+ const existing = db2.standards.getById(resolvedId);
1711
+ if (!existing) {
1712
+ throw new Error(`Coding standard not found: ${resolvedId}`);
1713
+ }
1714
+ const updates = {};
1715
+ if (validated.name !== void 0) updates.title = validated.name;
1716
+ if (validated.content !== void 0) updates.content = validated.content;
1717
+ if (validated.parent_id !== void 0)
1718
+ updates.parent_id = resolveStandardParentId2(validated.parent_id, db2, existing.owner, existing.repo ?? void 0);
1719
+ if (validated.context !== void 0) updates.context = validated.context;
1720
+ if (validated.version !== void 0) updates.version = validated.version;
1721
+ if (validated.language !== void 0) updates.language = validated.language;
1722
+ if (validated.stack !== void 0) updates.stack = validated.stack;
1723
+ if (validated.repo !== void 0) updates.repo = validated.repo;
1724
+ if (validated.is_global !== void 0) updates.is_global = validated.is_global;
1725
+ if (validated.tags !== void 0) updates.tags = validated.tags;
1726
+ if (validated.metadata !== void 0) updates.metadata = validated.metadata;
1727
+ if (validated.agent !== void 0) updates.agent = validated.agent;
1728
+ if (validated.model !== void 0) updates.model = validated.model;
1729
+ db2.standards.update(resolvedId, updates);
1730
+ const merged = {
1731
+ ...existing,
1732
+ ...updates,
1733
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1734
+ };
1735
+ if (validated.name !== void 0 || validated.content !== void 0 || validated.context !== void 0 || validated.version !== void 0 || validated.language !== void 0 || validated.stack !== void 0 || validated.tags !== void 0 || validated.metadata !== void 0) {
1736
+ await vectors2.upsert(resolvedId, buildStandardVectorText(merged), "standard");
1737
+ }
1738
+ logger.info("[Tool] standard.update - updated coding standard", {
1739
+ standardId: resolvedId,
1740
+ fields: Object.keys(updates)
1741
+ });
1742
+ return createMcpResponse(
1743
+ {
1744
+ success: true,
1745
+ id: resolvedId,
1746
+ code: existing.code,
1747
+ updatedFields: Object.keys(updates)
1748
+ },
1749
+ `Updated coding standard [${existing.code}] in repo "${existing.repo || "global"}". Fields: ${Object.keys(updates).join(", ") || "none"}.`,
1750
+ {
1751
+ structuredContentPathHint: "updatedFields",
1752
+ includeSerializedStructuredContent: true
1753
+ }
1754
+ );
1755
+ }
1756
+
1757
+ // src/mcp/tools/standard.detail.ts
1758
+ async function handleStandardDetail(args, storage) {
1759
+ const validated = StandardDetailSchema.parse(args);
1760
+ const standard = validated.id ? storage.standards.getById(validated.id) : storage.standards.getByCode(validated.code, validated.owner, validated.repo);
1761
+ if (!standard) {
1762
+ const identifier = validated.id ?? validated.code;
1763
+ throw new Error(`Coding standard not found: ${identifier}`);
1764
+ }
1765
+ storage.standards.incrementHitCounts([standard.id]);
1766
+ const lines = [
1767
+ `ID: ${standard.id}`,
1768
+ ...standard.code ? [`Code: ${standard.code}`] : [],
1769
+ `Title: ${standard.title}`,
1770
+ `Parent ID: ${standard.parent_id || "-"}`,
1771
+ `Context: ${standard.context}`,
1772
+ `Version: ${standard.version}`,
1773
+ `Language: ${standard.language || "-"}`,
1774
+ `Scope: ${standard.is_global ? "global" : standard.repo || "-"}`,
1775
+ `Created: ${standard.created_at}`,
1776
+ `Updated: ${standard.updated_at}`
1777
+ ];
1778
+ if (standard.stack.length > 0) lines.push(`Stack: ${standard.stack.join(", ")}`);
1779
+ if (standard.tags.length > 0) lines.push(`Tags: ${standard.tags.join(", ")}`);
1780
+ if (Object.keys(standard.metadata).length > 0) lines.push(`Metadata: ${JSON.stringify(standard.metadata)}`);
1781
+ if (standard.content) {
1782
+ lines.push("", "--- Content ---", standard.content);
1783
+ }
1784
+ const content = lines.join("\n");
1785
+ return createMcpResponse(standard, content, {
1786
+ contentSummary: content,
1787
+ includeSerializedStructuredContent: validated.structured
1788
+ });
1789
+ }
1790
+
1791
+ // src/mcp/tools/standard.delete.ts
1792
+ async function handleStandardDelete(params, db2, vectors2) {
1793
+ const validated = StandardDeleteSchema.parse(params);
1794
+ const { id, ids, code, codes, owner, repo, structured } = validated;
1795
+ const resolvedIds = [];
1796
+ if (ids) resolvedIds.push(...ids);
1797
+ if (id) resolvedIds.push(id);
1798
+ if (code) {
1799
+ const entry = db2.standards.getByCode(code, owner, repo);
1800
+ if (!entry) throw new Error(`Coding standard not found: ${code}`);
1801
+ resolvedIds.push(entry.id);
1802
+ }
1803
+ if (codes) {
1804
+ for (const c of codes) {
1805
+ const entry = db2.standards.getByCode(c, owner, repo);
1806
+ if (!entry) throw new Error(`Coding standard not found: ${c}`);
1807
+ resolvedIds.push(entry.id);
1808
+ }
1809
+ }
1810
+ if (resolvedIds.length === 0) {
1811
+ throw new Error("Either 'id', 'ids', 'code', or 'codes' must be provided for deletion");
1812
+ }
1813
+ const targetIds = resolvedIds;
1814
+ let deletedCount = 0;
1815
+ const deletedTitles = [];
1816
+ let lastRepo = repo || "unknown";
1817
+ for (const targetId of targetIds) {
1818
+ const existing = db2.standards.getById(targetId);
1819
+ if (existing) {
1820
+ lastRepo = existing.repo || (existing.is_global ? "global" : lastRepo);
1821
+ deletedTitles.push(existing.title);
1822
+ db2.standards.delete(targetId);
1823
+ await vectors2.remove(targetId, "standard");
1824
+ deletedCount++;
1825
+ } else if (id) {
1826
+ throw new Error(`Coding standard not found: ${targetId}`);
1827
+ }
1828
+ }
1829
+ logger.info("[Tool] standard.delete", { repo: lastRepo, count: deletedCount });
1830
+ return createMcpResponse(
1831
+ {
1832
+ success: true,
1833
+ id: id || void 0,
1834
+ ids: ids || void 0,
1835
+ repo: lastRepo,
1836
+ deletedCount,
1837
+ deletedTitles: deletedTitles.length > 10 ? [...deletedTitles.slice(0, 10), "..."] : deletedTitles
1838
+ },
1839
+ `Deleted ${deletedCount} coding standard(s) from "${lastRepo}".`,
1840
+ {
1841
+ structuredContentPathHint: "deletedCount",
1842
+ includeSerializedStructuredContent: structured
1843
+ }
1844
+ );
1845
+ }
1846
+
1847
+ // src/mcp/tools/task.create.ts
1848
+ import { randomUUID as randomUUID3 } from "crypto";
1849
+
1850
+ // src/mcp/tools/task.helpers.ts
1851
+ var UUID_REGEX5 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1852
+ function resolveParentId(value, owner, repo, storage, localCodeMap) {
1853
+ if (!value) return null;
1854
+ if (UUID_REGEX5.test(value)) return value;
1855
+ if (localCodeMap?.has(value)) return localCodeMap.get(value);
1856
+ const parent = storage.tasks.getTaskByCode(owner, repo, value);
1857
+ if (!parent) throw new Error(`parent_id: task with code '${value}' not found in repo '${repo}'`);
1858
+ return parent.id;
1859
+ }
1860
+ function resolveDependsOn(value, owner, repo, storage, localCodeMap) {
1861
+ if (!value) return null;
1862
+ if (UUID_REGEX5.test(value)) return value;
1863
+ if (localCodeMap?.has(value)) return localCodeMap.get(value);
1864
+ const task = storage.tasks.getTaskByCode(owner, repo, value);
1865
+ if (!task) throw new Error(`depends_on: task with code '${value}' not found in repo '${repo}'`);
1866
+ return task.id;
1867
+ }
1868
+ function deriveTaskStatusTimestamps(status, now, existingTask) {
1869
+ const timestamps = {
1870
+ in_progress_at: null,
1871
+ finished_at: null,
1872
+ canceled_at: null
1873
+ };
1874
+ if (status === "in_progress" && existingTask?.status !== "in_progress") {
1875
+ timestamps.in_progress_at = now;
1876
+ }
1877
+ if (status === "completed") {
1878
+ timestamps.finished_at = now;
1879
+ }
1880
+ if (status === "canceled") {
1881
+ timestamps.canceled_at = now;
1882
+ }
1883
+ return timestamps;
1884
+ }
1885
+ async function archiveTaskToMemory(taskId, repo, storage, vectors2) {
1886
+ const task = storage.tasks.getTaskById(taskId);
1887
+ if (!task) return;
1888
+ const comments = storage.taskComments.getTaskCommentsByTaskId(taskId);
1889
+ let content = `Task: [${task.task_code}] ${task.title}
1890
+ `;
1891
+ content += `Phase: ${task.phase}
1892
+ `;
1893
+ content += `Description: ${task.description || "No description"}
1894
+ `;
1895
+ content += `Commit: ${task.commit_id || "N/A"}
1896
+ `;
1897
+ if (task.changed_files && task.changed_files.length > 0) {
1898
+ content += `Files changed:
1899
+ `;
1900
+ for (const f of task.changed_files) {
1901
+ content += ` - ${f}
1902
+ `;
1903
+ }
1904
+ }
1905
+ if (comments && comments.length > 0) {
1906
+ content += `
1907
+ Comments & History:
1908
+ `;
1909
+ const chronComments = [...comments].reverse();
1910
+ for (const c of chronComments) {
1911
+ const statusInfo = c.next_status ? ` (Status: ${c.previous_status} -> ${c.next_status})` : "";
1912
+ content += `- [${c.created_at}] ${c.agent}${statusInfo}: ${c.comment}
1913
+ `;
1914
+ }
1915
+ }
1916
+ const metadata = {
1917
+ task_id: taskId,
1918
+ task_code: task.task_code,
1919
+ original_metadata: task.metadata
1920
+ };
1921
+ const title = `Completed Task: ${task.title}`;
1922
+ const truncatedTitle = title.length > 100 ? title.substring(0, 97) + "..." : title;
1923
+ try {
1924
+ await handleMemoryStore(
1925
+ {
1926
+ type: "task_archive",
1927
+ title: truncatedTitle,
1928
+ content,
1929
+ importance: Math.min(5, task.priority + 1),
1930
+ agent: task.agent || "system",
1931
+ role: task.role || "unknown",
1932
+ model: "system",
1933
+ scope: { repo, owner: task.owner || "" },
1934
+ tags: ["task-archive", ...task.tags],
1935
+ metadata
1936
+ },
1937
+ storage,
1938
+ vectors2
1939
+ );
1940
+ } catch (error) {
1941
+ logger.error("Failed to archive task to memory", { error: String(error) });
1942
+ }
1943
+ }
1944
+
1945
+ // src/mcp/tools/task.create.ts
1946
+ async function handleTaskCreate(args, storage) {
1947
+ const parsed = TaskCreateSchema.parse(args);
1948
+ const { owner, repo, tasks: bulkTasks, ...singleTask } = parsed;
1949
+ if (bulkTasks) {
1950
+ const createdTasks = [];
1951
+ const tasksToInsert = [];
1952
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
1953
+ const codesInRequest = /* @__PURE__ */ new Set();
1954
+ const batchCodes = /* @__PURE__ */ new Set();
1955
+ for (const taskData of bulkTasks) {
1956
+ if (!taskData.task_code) {
1957
+ taskData.task_code = generateNextCode(owner ?? "", repo, "task", storage, batchCodes);
1958
+ }
1959
+ batchCodes.add(taskData.task_code);
1960
+ }
1961
+ const allCodes = bulkTasks.map((t) => t.task_code);
1962
+ const existingCodes = storage.tasks.getExistingTaskCodes(owner, repo, allCodes);
1963
+ const initialStats = storage.taskStats.getTaskStats(owner, repo);
1964
+ let pendingInRequestCount = 0;
1965
+ const localCodeMap = /* @__PURE__ */ new Map();
1966
+ for (const taskData of bulkTasks) {
1967
+ localCodeMap.set(taskData.task_code, randomUUID3());
1968
+ }
1969
+ for (const taskData of bulkTasks) {
1970
+ const code = taskData.task_code;
1971
+ if (codesInRequest.has(code)) {
1972
+ throw new Error(`Duplicate task_code in request: '${code}'`);
1973
+ }
1974
+ if (existingCodes.has(code)) {
1975
+ throw new Error(`Duplicate task_code: '${code}' already exists in repository '${repo}'`);
1976
+ }
1977
+ codesInRequest.add(code);
1978
+ const normalizedStatus = taskData.status || "backlog";
1979
+ if (normalizedStatus !== "backlog" && normalizedStatus !== "pending") {
1980
+ throw new Error(`New tasks must be 'backlog' or 'pending'. Task '${code}' has status '${normalizedStatus}'.`);
1981
+ }
1982
+ if (normalizedStatus === "pending") {
1983
+ if (initialStats.todo + pendingInRequestCount >= 10) {
1984
+ throw new Error(
1985
+ `Cannot create task '${code}' as 'pending'. Maximum of 10 pending tasks reached. Please use status 'backlog' for new tasks instead.`
1986
+ );
1987
+ }
1988
+ }
1989
+ const statusTimestamps2 = deriveTaskStatusTimestamps(normalizedStatus, now2);
1990
+ const tags = [...taskData.tags || []];
1991
+ const phaseTag2 = `phase:${taskData.phase}`;
1992
+ if (!tags.includes(phaseTag2)) {
1993
+ tags.push(phaseTag2);
1994
+ }
1995
+ const taskId2 = localCodeMap.get(code);
1996
+ const task2 = {
1997
+ id: taskId2,
1998
+ owner,
1999
+ repo,
2000
+ task_code: code,
2001
+ phase: taskData.phase,
2002
+ title: taskData.title,
2003
+ description: taskData.description,
2004
+ status: normalizedStatus,
2005
+ priority: taskData.priority || 3,
2006
+ agent: taskData.agent || singleTask.agent || "unknown",
2007
+ role: taskData.role || singleTask.role || "unknown",
2008
+ doc_path: taskData.doc_path || null,
2009
+ created_at: now2,
2010
+ updated_at: now2,
2011
+ in_progress_at: statusTimestamps2.in_progress_at,
2012
+ finished_at: statusTimestamps2.finished_at,
2013
+ canceled_at: statusTimestamps2.canceled_at,
2014
+ est_tokens: taskData.est_tokens ?? 0,
2015
+ tags,
2016
+ suggested_skills: taskData.suggested_skills || [],
2017
+ commit_id: null,
2018
+ changed_files: [],
2019
+ metadata: taskData.metadata || {},
2020
+ parent_id: resolveParentId(taskData.parent_id, owner, repo, storage, localCodeMap),
2021
+ depends_on: resolveDependsOn(taskData.depends_on, owner, repo, storage, localCodeMap)
2022
+ };
2023
+ tasksToInsert.push(task2);
2024
+ createdTasks.push(code);
2025
+ if (normalizedStatus === "pending") {
2026
+ pendingInRequestCount++;
2027
+ }
2028
+ }
2029
+ storage.tasks.bulkInsertTasks(tasksToInsert);
2030
+ const taskCodesStr = createdTasks.length > 0 ? `: ${createdTasks.join(", ")}` : "";
2031
+ return createMcpResponse(
2032
+ { success: true, repo, createdCount: bulkTasks.length, taskCodes: createdTasks },
2033
+ `Created ${bulkTasks.length} tasks in repo "${repo}"${taskCodesStr}.`,
2034
+ { includeSerializedStructuredContent: parsed.structured || false }
2035
+ );
2036
+ }
2037
+ const {
2038
+ task_code,
2039
+ phase,
2040
+ title,
2041
+ description,
2042
+ status,
2043
+ priority,
2044
+ agent,
2045
+ role,
2046
+ doc_path,
2047
+ metadata,
2048
+ parent_id,
2049
+ depends_on,
2050
+ est_tokens
2051
+ } = singleTask;
2052
+ if (!phase || !title || !description) {
2053
+ throw new Error("Missing required fields for single task creation (phase, title, description)");
2054
+ }
2055
+ const resolvedCode = task_code || generateNextCode(owner ?? "", repo, "task", storage);
2056
+ if (storage.tasks.isTaskCodeDuplicate(owner, repo, resolvedCode)) {
2057
+ throw new Error(`Duplicate task_code: '${resolvedCode}' already exists in repository '${repo}'`);
2058
+ }
2059
+ if (status !== "backlog" && status !== "pending" && status !== void 0) {
2060
+ throw new Error("New tasks must be created with status 'backlog' or 'pending'.");
2061
+ }
2062
+ if (status === "pending") {
2063
+ const stats = storage.taskStats.getTaskStats(owner, repo);
2064
+ if (stats.todo >= 10) {
2065
+ throw new Error(
2066
+ `Cannot create task as 'pending'. Maximum of 10 pending tasks reached. Please use status 'backlog' for new tasks instead.`
2067
+ );
2068
+ }
2069
+ }
2070
+ const taskId = randomUUID3();
2071
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2072
+ const statusTimestamps = deriveTaskStatusTimestamps(status || "backlog", now);
2073
+ const finalTags = [...singleTask.tags || []];
2074
+ const phaseTag = `phase:${phase}`;
2075
+ if (!finalTags.includes(phaseTag)) {
2076
+ finalTags.push(phaseTag);
2077
+ }
2078
+ const task = {
2079
+ id: taskId,
2080
+ owner,
2081
+ repo,
2082
+ task_code: resolvedCode,
2083
+ phase,
2084
+ title,
2085
+ description,
2086
+ status: status || "backlog",
2087
+ priority: priority || 3,
2088
+ agent: agent || "unknown",
2089
+ role: role || "unknown",
2090
+ doc_path: doc_path || null,
2091
+ created_at: now,
2092
+ updated_at: now,
2093
+ in_progress_at: statusTimestamps.in_progress_at,
2094
+ finished_at: statusTimestamps.finished_at,
2095
+ canceled_at: statusTimestamps.canceled_at,
2096
+ est_tokens: est_tokens ?? 0,
2097
+ tags: finalTags,
2098
+ suggested_skills: singleTask.suggested_skills || [],
2099
+ commit_id: null,
2100
+ changed_files: [],
2101
+ metadata: metadata || {},
2102
+ parent_id: resolveParentId(parent_id, owner, repo, storage),
2103
+ depends_on: resolveDependsOn(depends_on, owner, repo, storage)
2104
+ };
2105
+ storage.tasks.insertTask(task);
2106
+ return createMcpResponse(
2107
+ {
2108
+ success: true,
2109
+ id: task.id,
2110
+ repo: task.repo,
2111
+ task_code: task.task_code,
2112
+ phase: task.phase,
2113
+ title: task.title,
2114
+ status: task.status,
2115
+ priority: task.priority,
2116
+ depends_on: task.depends_on
2117
+ },
2118
+ `Created task [${task.task_code}] ${task.title} in repo "${task.repo}" with status "${task.status}".`,
2119
+ { includeSerializedStructuredContent: parsed.structured || false }
2120
+ );
2121
+ }
2122
+ async function handleTaskCreateInteractive(args, storage, options = {}) {
2123
+ const partialTaskData = TaskCreateInteractiveSchema.parse(args ?? {});
2124
+ const inferredRepo = partialTaskData.repo ?? inferRepoFromSession(options.session);
2125
+ const draft = {
2126
+ ...partialTaskData,
2127
+ repo: inferredRepo
2128
+ };
2129
+ const requestedSchema = buildMissingTaskSchema(draft);
2130
+ let completedDraft = draft;
2131
+ if (Object.keys(requestedSchema.properties).length > 0) {
2132
+ if (!options.session?.supportsElicitationForm || !options.elicit) {
2133
+ throw new Error("Client does not advertise MCP elicitation form support");
2134
+ }
2135
+ const elicited = extractAcceptedElicitationContent(
2136
+ await options.elicit({
2137
+ mode: "form",
2138
+ message: "Please complete the missing task details to create a new task.",
2139
+ requestedSchema
2140
+ })
2141
+ );
2142
+ completedDraft = {
2143
+ ...draft,
2144
+ ...elicited
2145
+ };
2146
+ }
2147
+ return await handleTaskCreate(
2148
+ {
2149
+ ...completedDraft,
2150
+ status: completedDraft.status ?? "backlog",
2151
+ priority: completedDraft.priority ?? 3,
2152
+ structured: true
2153
+ },
2154
+ storage
2155
+ );
2156
+ }
2157
+ function buildMissingTaskSchema(task) {
2158
+ const properties = {};
2159
+ const required = [];
2160
+ addRequiredStringField(properties, required, task, "repo", {
2161
+ title: "Repository",
2162
+ description: "Name of the repository for this task.",
2163
+ minLength: 1
2164
+ });
2165
+ addRequiredStringField(properties, required, task, "phase", {
2166
+ title: "Phase",
2167
+ description: "Project phase or milestone for this task.",
2168
+ minLength: 1
2169
+ });
2170
+ addRequiredStringField(properties, required, task, "title", {
2171
+ title: "Title",
2172
+ description: "Short task title.",
2173
+ minLength: 3,
2174
+ maxLength: 100
2175
+ });
2176
+ addRequiredStringField(properties, required, task, "description", {
2177
+ title: "Description",
2178
+ description: "Detailed description. MUST follow format: 1. Context & Analysis, 2. Step & Implementation, 3. Acceptance & Verification",
2179
+ minLength: 1
2180
+ });
2181
+ if (!task.status) {
2182
+ properties.status = {
2183
+ type: "string",
2184
+ title: "Status",
2185
+ description: "Initial task status.",
2186
+ enum: ["backlog", "pending"],
2187
+ default: "backlog"
2188
+ };
2189
+ }
2190
+ if (task.priority === void 0) {
2191
+ properties.priority = {
2192
+ type: "integer",
2193
+ title: "Priority",
2194
+ description: "Task priority from 1 to 5.",
2195
+ minimum: 1,
2196
+ maximum: 5,
2197
+ default: 3
2198
+ };
2199
+ }
2200
+ return {
2201
+ type: "object",
2202
+ properties,
2203
+ required
2204
+ };
2205
+ }
2206
+ function addRequiredStringField(properties, required, task, field, schema) {
2207
+ if (typeof task[field] === "string" && task[field].trim()) {
2208
+ return;
2209
+ }
2210
+ properties[field] = {
2211
+ type: "string",
2212
+ ...schema
2213
+ };
2214
+ required.push(field);
2215
+ }
2216
+
2217
+ // src/mcp/tools/task.update.ts
2218
+ import { randomUUID as randomUUID4 } from "crypto";
2219
+ async function handleTaskUpdate(args, storage, vectors2) {
2220
+ const updateData = TaskUpdateSchema.parse(args);
2221
+ const { owner, repo, id, ids, comment, force, ...updates } = updateData;
2222
+ let resolvedId = id;
2223
+ if (!resolvedId && !ids && updates.task_code) {
2224
+ const found = storage.tasks.getTaskByCode(owner, repo, updates.task_code);
2225
+ if (!found) throw new Error(`Task not found: ${updates.task_code}`);
2226
+ resolvedId = found.id;
2227
+ }
2228
+ const targetIds = ids || (resolvedId ? [resolvedId] : []);
2229
+ if (targetIds.length === 0) {
2230
+ throw new Error("Either 'id' or 'ids' must be provided for update");
2231
+ }
2232
+ let updatedCount = 0;
2233
+ const updatedTasks = [];
2234
+ const completedTaskIds = [];
2235
+ let releasedClaims = 0;
2236
+ let expiredHandoffs = 0;
2237
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2238
+ const isStatusChangingGlobal = updates.status !== void 0;
2239
+ const existingTasks = storage.tasks.getTasksByIds(targetIds);
2240
+ const taskMap = new Map(existingTasks.map((t) => [t.id, t]));
2241
+ for (const targetId of targetIds) {
2242
+ const existingTask = taskMap.get(targetId);
2243
+ if (!existingTask) {
2244
+ if (id) throw new Error(`Task not found: ${targetId}`);
2245
+ continue;
2246
+ }
2247
+ const isStatusChanging = isStatusChangingGlobal && updates.status !== existingTask.status;
2248
+ if (isStatusChanging && !force) {
2249
+ if (!comment || comment.trim() === "") {
2250
+ throw new Error("comment is required when changing task status");
2251
+ }
2252
+ const isStartable = existingTask.status === "backlog" || existingTask.status === "pending" || existingTask.status === "blocked";
2253
+ if (isStartable && updates.status === "completed") {
2254
+ throw new Error(
2255
+ `Cannot transition task ${targetId} from '${existingTask.status}' directly to 'completed'. Must be 'in_progress' first.`
2256
+ );
2257
+ }
2231
2258
  }
2232
- );
2233
- }
2234
-
2235
- // src/mcp/tools/standard.update.ts
2236
- var UUID_REGEX5 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2237
- function resolveStandardParentId2(value, db2, owner, repo) {
2238
- if (!value) return null;
2239
- if (UUID_REGEX5.test(value)) return value;
2240
- const standard = db2.standards.getByCode(value, owner, repo);
2241
- if (!standard) throw new Error(`parent_id: standard with code '${value}' not found`);
2242
- return standard.id;
2243
- }
2244
- async function handleStandardUpdate(params, db2, vectors2) {
2245
- const validated = StandardUpdateSchema.parse(params);
2246
- let resolvedId = validated.id;
2247
- if (!resolvedId && validated.code) {
2248
- const byCode = db2.standards.getByCode(validated.code, validated.owner, validated.repo);
2249
- if (!byCode) throw new Error(`Coding standard not found: ${validated.code}`);
2250
- resolvedId = byCode.id;
2251
- } else if (!resolvedId) {
2252
- throw new Error("Either id or code must be provided");
2253
- }
2254
- const existing = db2.standards.getById(resolvedId);
2255
- if (!existing) {
2256
- throw new Error(`Coding standard not found: ${resolvedId}`);
2259
+ if (updates.status === "completed" && isStatusChanging) {
2260
+ if (updates.est_tokens === void 0) {
2261
+ throw new Error("est_tokens is required when changing task status to completed");
2262
+ }
2263
+ const children = storage.tasks.getChildrenByParentId(targetId);
2264
+ const incompleteChildren = children.filter((c) => c.status !== "completed");
2265
+ if (incompleteChildren.length > 0) {
2266
+ const childList = incompleteChildren.map((c) => `[${c.task_code}] ${c.title} (${c.status})`).join("; ");
2267
+ throw new Error(
2268
+ `Cannot complete task [${existingTask.task_code}] "${existingTask.title}" \u2014 it has ${incompleteChildren.length} incomplete child task(s). Complete the following child task(s) first: ${childList}`
2269
+ );
2270
+ }
2271
+ }
2272
+ if (updates.task_code && storage.tasks.isTaskCodeDuplicate(owner, repo, updates.task_code, targetId)) {
2273
+ throw new Error(`Duplicate task_code: '${updates.task_code}' already exists`);
2274
+ }
2275
+ const finalUpdates = { ...updates };
2276
+ if (updates.parent_id !== void 0) {
2277
+ finalUpdates.parent_id = resolveParentId(updates.parent_id, owner, repo, storage);
2278
+ }
2279
+ if (updates.depends_on !== void 0) {
2280
+ finalUpdates.depends_on = resolveDependsOn(updates.depends_on, owner, repo, storage);
2281
+ }
2282
+ if (updates.phase !== void 0 || updates.tags !== void 0) {
2283
+ let currentTags = updates.tags || existingTask.tags || [];
2284
+ currentTags = currentTags.filter((t) => !t.startsWith("phase:"));
2285
+ const finalPhase = updates.phase !== void 0 ? updates.phase : existingTask.phase;
2286
+ if (finalPhase) {
2287
+ const phaseTag = `phase:${finalPhase}`;
2288
+ if (!currentTags.includes(phaseTag)) {
2289
+ currentTags.push(phaseTag);
2290
+ }
2291
+ }
2292
+ finalUpdates.tags = currentTags;
2293
+ }
2294
+ if (updates.status === "completed") {
2295
+ finalUpdates.finished_at = now;
2296
+ finalUpdates.commit_id = updates.commit_id;
2297
+ finalUpdates.changed_files = updates.changed_files;
2298
+ } else if (updates.status === "canceled") finalUpdates.canceled_at = now;
2299
+ else if (updates.status === "in_progress" && existingTask.status !== "in_progress")
2300
+ finalUpdates.in_progress_at = now;
2301
+ storage.tasks.updateTask(targetId, finalUpdates);
2302
+ if (comment !== void 0 || isStatusChanging) {
2303
+ storage.taskComments.insertTaskComment({
2304
+ id: randomUUID4(),
2305
+ task_id: targetId,
2306
+ owner,
2307
+ repo,
2308
+ comment: comment || `Status updated to ${updates.status}`,
2309
+ agent: updates.agent || existingTask.agent || "unknown",
2310
+ role: updates.role || existingTask.role || "unknown",
2311
+ model: updates.model || "unknown",
2312
+ previous_status: isStatusChanging ? existingTask.status : null,
2313
+ next_status: isStatusChanging ? updates.status : null,
2314
+ created_at: now
2315
+ });
2316
+ }
2317
+ if (updates.status === "completed" && existingTask.status !== "completed") {
2318
+ completedTaskIds.push(targetId);
2319
+ }
2320
+ if (isStatusChanging && (updates.status === "completed" || updates.status === "canceled")) {
2321
+ releasedClaims += storage.handoffs.releaseClaimsForTask(targetId);
2322
+ expiredHandoffs += storage.handoffs.updatePendingHandoffsForTask(targetId, "expired");
2323
+ }
2324
+ updatedTasks.push({ id: targetId, code: updates.task_code || existingTask.task_code });
2325
+ updatedCount++;
2257
2326
  }
2258
- const updates = {};
2259
- if (validated.name !== void 0) updates.title = validated.name;
2260
- if (validated.content !== void 0) updates.content = validated.content;
2261
- if (validated.parent_id !== void 0)
2262
- updates.parent_id = resolveStandardParentId2(validated.parent_id, db2, existing.owner, existing.repo ?? void 0);
2263
- if (validated.context !== void 0) updates.context = validated.context;
2264
- if (validated.version !== void 0) updates.version = validated.version;
2265
- if (validated.language !== void 0) updates.language = validated.language;
2266
- if (validated.stack !== void 0) updates.stack = validated.stack;
2267
- if (validated.repo !== void 0) updates.repo = validated.repo;
2268
- if (validated.is_global !== void 0) updates.is_global = validated.is_global;
2269
- if (validated.tags !== void 0) updates.tags = validated.tags;
2270
- if (validated.metadata !== void 0) updates.metadata = validated.metadata;
2271
- if (validated.agent !== void 0) updates.agent = validated.agent;
2272
- if (validated.model !== void 0) updates.model = validated.model;
2273
- db2.standards.update(resolvedId, updates);
2274
- const merged = {
2275
- ...existing,
2276
- ...updates,
2277
- updated_at: (/* @__PURE__ */ new Date()).toISOString()
2278
- };
2279
- if (validated.name !== void 0 || validated.content !== void 0 || validated.context !== void 0 || validated.version !== void 0 || validated.language !== void 0 || validated.stack !== void 0 || validated.tags !== void 0 || validated.metadata !== void 0) {
2280
- await vectors2.upsert(resolvedId, buildStandardVectorText(merged), "standard");
2327
+ const taskCodesStr = updatedTasks.length > 0 ? ` Tasks: ${updatedTasks.map((t) => t.code).join(", ")}.` : "";
2328
+ const fieldsStr = Object.keys(updates).length > 0 ? ` Fields: ${Object.keys(updates).join(", ")}.` : "";
2329
+ const isCompleted = updates.status === "completed" && updatedCount > 0;
2330
+ let summaryText = isCompleted ? `Updated ${updatedCount} task(s) in repo "${repo}". Task marked as completed with commit ${updates.commit_id} (${(updates.changed_files || []).length} files changed).` : `Updated ${updatedCount} task(s) in repo "${repo}".`;
2331
+ summaryText += `${taskCodesStr}${fieldsStr}`;
2332
+ if (releasedClaims || expiredHandoffs) {
2333
+ summaryText += ` Auto-closed coordination: released ${releasedClaims} claim(s), expired ${expiredHandoffs} handoff(s).`;
2281
2334
  }
2282
- logger.info("[Tool] standard.update - updated coding standard", {
2283
- standardId: resolvedId,
2284
- fields: Object.keys(updates)
2285
- });
2286
- return createMcpResponse(
2335
+ const response = createMcpResponse(
2287
2336
  {
2288
2337
  success: true,
2289
- id: resolvedId,
2290
- code: existing.code,
2291
- updatedFields: Object.keys(updates)
2338
+ id: id || void 0,
2339
+ ids: ids || void 0,
2340
+ repo,
2341
+ status: updates.status,
2342
+ updatedCount,
2343
+ updatedFields: Object.keys(updates),
2344
+ coordinationCleanup: {
2345
+ releasedClaims,
2346
+ expiredHandoffs
2347
+ }
2292
2348
  },
2293
- `Updated coding standard [${existing.code}] in repo "${existing.repo || "global"}". Fields: ${Object.keys(updates).join(", ") || "none"}.`,
2294
- {
2295
- structuredContentPathHint: "updatedFields",
2296
- includeSerializedStructuredContent: true
2297
- }
2349
+ summaryText,
2350
+ { includeSerializedStructuredContent: updateData.structured }
2298
2351
  );
2299
- }
2300
-
2301
- // src/mcp/tools/standard.detail.ts
2302
- async function handleStandardDetail(args, storage) {
2303
- const validated = StandardDetailSchema.parse(args);
2304
- const standard = validated.id ? storage.standards.getById(validated.id) : storage.standards.getByCode(validated.code, validated.owner, validated.repo);
2305
- if (!standard) {
2306
- const identifier = validated.id ?? validated.code;
2307
- throw new Error(`Coding standard not found: ${identifier}`);
2308
- }
2309
- storage.standards.incrementHitCounts([standard.id]);
2310
- const lines = [
2311
- `ID: ${standard.id}`,
2312
- ...standard.code ? [`Code: ${standard.code}`] : [],
2313
- `Title: ${standard.title}`,
2314
- `Parent ID: ${standard.parent_id || "-"}`,
2315
- `Context: ${standard.context}`,
2316
- `Version: ${standard.version}`,
2317
- `Language: ${standard.language || "-"}`,
2318
- `Scope: ${standard.is_global ? "global" : standard.repo || "-"}`,
2319
- `Created: ${standard.created_at}`,
2320
- `Updated: ${standard.updated_at}`
2321
- ];
2322
- if (standard.stack.length > 0) lines.push(`Stack: ${standard.stack.join(", ")}`);
2323
- if (standard.tags.length > 0) lines.push(`Tags: ${standard.tags.join(", ")}`);
2324
- if (Object.keys(standard.metadata).length > 0) lines.push(`Metadata: ${JSON.stringify(standard.metadata)}`);
2325
- if (standard.content) {
2326
- lines.push("", "--- Content ---", standard.content);
2352
+ if (completedTaskIds.length > 0) {
2353
+ setImmediate(async () => {
2354
+ for (const taskId of completedTaskIds) {
2355
+ try {
2356
+ await archiveTaskToMemory(taskId, repo, storage, vectors2);
2357
+ } catch (err) {
2358
+ logger.error("Failed to archive task to memory", { taskId, error: String(err) });
2359
+ }
2360
+ }
2361
+ });
2327
2362
  }
2328
- const content = lines.join("\n");
2329
- return createMcpResponse(standard, content, {
2330
- contentSummary: content,
2331
- includeSerializedStructuredContent: validated.structured
2332
- });
2363
+ return response;
2333
2364
  }
2334
2365
 
2335
- // src/mcp/tools/standard.delete.ts
2336
- async function handleStandardDelete(params, db2, vectors2) {
2337
- const validated = StandardDeleteSchema.parse(params);
2338
- const { id, ids, code, codes, owner, repo, structured } = validated;
2366
+ // src/mcp/tools/task.delete.ts
2367
+ async function handleTaskDelete(args, storage) {
2368
+ const validated = TaskDeleteSchema.parse(args);
2369
+ const { owner, repo, id, ids, task_code } = validated;
2339
2370
  const resolvedIds = [];
2340
2371
  if (ids) resolvedIds.push(...ids);
2341
2372
  if (id) resolvedIds.push(id);
2342
- if (code) {
2343
- const entry = db2.standards.getByCode(code, owner, repo);
2344
- if (!entry) throw new Error(`Coding standard not found: ${code}`);
2345
- resolvedIds.push(entry.id);
2346
- }
2347
- if (codes) {
2348
- for (const c of codes) {
2349
- const entry = db2.standards.getByCode(c, owner, repo);
2350
- if (!entry) throw new Error(`Coding standard not found: ${c}`);
2351
- resolvedIds.push(entry.id);
2352
- }
2373
+ if (task_code) {
2374
+ const task = storage.tasks.getTaskByCode(owner, repo, task_code);
2375
+ if (!task) throw new Error(`Task not found: ${task_code}`);
2376
+ resolvedIds.push(task.id);
2353
2377
  }
2354
2378
  if (resolvedIds.length === 0) {
2355
- throw new Error("Either 'id', 'ids', 'code', or 'codes' must be provided for deletion");
2379
+ throw new Error("Either 'id', 'ids', or 'task_code' must be provided for deletion");
2356
2380
  }
2357
2381
  const targetIds = resolvedIds;
2358
- let deletedCount = 0;
2359
- const deletedTitles = [];
2360
- let lastRepo = repo || "unknown";
2382
+ const tasksToDelete = storage.tasks.getTasksByIds(targetIds);
2383
+ const deletedCodes = tasksToDelete.map((t) => t.task_code);
2361
2384
  for (const targetId of targetIds) {
2362
- const existing = db2.standards.getById(targetId);
2363
- if (existing) {
2364
- lastRepo = existing.repo || (existing.is_global ? "global" : lastRepo);
2365
- deletedTitles.push(existing.title);
2366
- db2.standards.delete(targetId);
2367
- await vectors2.remove(targetId, "standard");
2368
- deletedCount++;
2369
- } else if (id) {
2370
- throw new Error(`Coding standard not found: ${targetId}`);
2371
- }
2385
+ storage.tasks.deleteTask(targetId);
2372
2386
  }
2373
- logger.info("[Tool] standard.delete", { repo: lastRepo, count: deletedCount });
2387
+ const deletedCodesStr = deletedCodes.length > 0 ? `: ${deletedCodes.join(", ")}` : "";
2374
2388
  return createMcpResponse(
2375
2389
  {
2376
2390
  success: true,
2377
2391
  id: id || void 0,
2378
2392
  ids: ids || void 0,
2379
- repo: lastRepo,
2380
- deletedCount,
2381
- deletedTitles: deletedTitles.length > 10 ? [...deletedTitles.slice(0, 10), "..."] : deletedTitles
2393
+ repo,
2394
+ deletedCount: targetIds.length,
2395
+ deletedCodes
2382
2396
  },
2383
- `Deleted ${deletedCount} coding standard(s) from "${lastRepo}".`,
2384
- {
2385
- structuredContentPathHint: "deletedCount",
2386
- includeSerializedStructuredContent: structured
2387
- }
2397
+ `Deleted ${targetIds.length} task(s) from repo "${repo}"${deletedCodesStr}.`,
2398
+ { includeSerializedStructuredContent: validated.structured }
2388
2399
  );
2389
2400
  }
2390
2401
 
@@ -2820,7 +2831,7 @@ function normalizeToolArguments(args, session2) {
2820
2831
  const parsed = parseRepoInput(repoVal2, void 0);
2821
2832
  nextArgs.owner = parsed.owner || inferOwnerFromSession(session2) || "";
2822
2833
  if (nextArgs.owner && !repoVal2.includes("/")) {
2823
- console.warn(
2834
+ logger.warn(
2824
2835
  `[router] owner inferred from session (${nextArgs.owner}) \u2014 may be incorrect. Agents should pass explicit owner/repo.`
2825
2836
  );
2826
2837
  }