lynkr 9.9.0 → 9.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -23
- package/bin/cli.js +13 -7
- package/bin/lynkr-init.js +34 -12
- package/bin/lynkr-reset.js +71 -0
- package/config/model-tiers.json +199 -52
- package/package.json +3 -3
- package/scripts/build-eval-set.js +256 -0
- package/scripts/mine-difficulty-anchors.js +288 -0
- package/scripts/validate-difficulty-classifier.js +144 -0
- package/scripts/validate-intent-anchors.js +186 -0
- package/src/api/providers-handler.js +0 -1
- package/src/api/router.js +292 -160
- package/src/clients/databricks.js +95 -6
- package/src/config/index.js +3 -43
- package/src/context/tool-result-compressor.js +883 -40
- package/src/orchestrator/index.js +117 -1235
- package/src/orchestrator/passthrough-stream.js +382 -0
- package/src/orchestrator/sse-transformer.js +408 -0
- package/src/routing/affinity-store.js +17 -3
- package/src/routing/classifier-setup.js +207 -0
- package/src/routing/complexity-analyzer.js +40 -4
- package/src/routing/difficulty-classifier.js +261 -0
- package/src/routing/index.js +70 -7
- package/src/routing/intent-score.js +132 -12
- package/src/routing/session-affinity.js +8 -1
- package/src/routing/side-channel-detector.js +103 -0
- package/src/server.js +20 -46
- package/src/agents/context-manager.js +0 -236
- package/src/agents/decomposition/dispatcher.js +0 -185
- package/src/agents/decomposition/gate.js +0 -136
- package/src/agents/decomposition/index.js +0 -183
- package/src/agents/decomposition/model-call.js +0 -75
- package/src/agents/decomposition/planner.js +0 -223
- package/src/agents/decomposition/synthesizer.js +0 -89
- package/src/agents/decomposition/telemetry.js +0 -55
- package/src/agents/definitions/loader.js +0 -653
- package/src/agents/executor.js +0 -457
- package/src/agents/index.js +0 -165
- package/src/agents/parallel-coordinator.js +0 -68
- package/src/agents/reflector.js +0 -331
- package/src/agents/skillbook.js +0 -331
- package/src/agents/store.js +0 -259
- package/src/edits/index.js +0 -171
- package/src/indexer/babel-parser.js +0 -213
- package/src/indexer/index.js +0 -1629
- package/src/indexer/navigation/index.js +0 -32
- package/src/indexer/navigation/providers/treeSitter.js +0 -36
- package/src/indexer/parser.js +0 -443
- package/src/tasks/store.js +0 -349
- package/src/tests/coverage.js +0 -173
- package/src/tests/index.js +0 -171
- package/src/tests/store.js +0 -213
- package/src/tools/agent-task.js +0 -145
- package/src/tools/code-mode.js +0 -304
- package/src/tools/decompose.js +0 -91
- package/src/tools/edits.js +0 -94
- package/src/tools/execution.js +0 -171
- package/src/tools/git.js +0 -1346
- package/src/tools/index.js +0 -306
- package/src/tools/indexer.js +0 -360
- package/src/tools/lazy-loader.js +0 -366
- package/src/tools/mcp-remote.js +0 -88
- package/src/tools/mcp.js +0 -116
- package/src/tools/process.js +0 -167
- package/src/tools/smart-selection.js +0 -180
- package/src/tools/stubs.js +0 -55
- package/src/tools/tasks.js +0 -260
- package/src/tools/tests.js +0 -132
- package/src/tools/tinyfish.js +0 -358
- package/src/tools/truncate.js +0 -106
- package/src/tools/web-client.js +0 -71
- package/src/tools/web.js +0 -415
- package/src/tools/workspace.js +0 -204
package/src/tools/git.js
DELETED
|
@@ -1,1346 +0,0 @@
|
|
|
1
|
-
const fsp = require("fs/promises");
|
|
2
|
-
const { runProcess } = require("../tools/process");
|
|
3
|
-
const { registerTool } = require(".");
|
|
4
|
-
const { workspaceRoot, resolveWorkspacePath } = require("../workspace");
|
|
5
|
-
const { invokeModel } = require("../clients/databricks");
|
|
6
|
-
const logger = require("../logger");
|
|
7
|
-
const config = require("../config");
|
|
8
|
-
const { addDiffComment, listDiffComments, deleteDiffComment } = require("../diff/comments");
|
|
9
|
-
|
|
10
|
-
async function execGit(args, { timeoutMs = 10000, allowNonZero = false } = {}) {
|
|
11
|
-
const result = await runProcess({
|
|
12
|
-
command: "git",
|
|
13
|
-
args,
|
|
14
|
-
cwd: workspaceRoot,
|
|
15
|
-
env: {},
|
|
16
|
-
timeoutMs,
|
|
17
|
-
});
|
|
18
|
-
if (!allowNonZero && result.exitCode !== 0) {
|
|
19
|
-
const error = new Error(`git ${args.join(" ")} failed with code ${result.exitCode}`);
|
|
20
|
-
error.stdout = result.stdout;
|
|
21
|
-
error.stderr = result.stderr;
|
|
22
|
-
throw error;
|
|
23
|
-
}
|
|
24
|
-
return result;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function execGitText(args, options) {
|
|
28
|
-
const result = await execGit(args, options);
|
|
29
|
-
return result.stdout ?? "";
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function parseShortStatus(output) {
|
|
33
|
-
return output
|
|
34
|
-
.split("\n")
|
|
35
|
-
.map((line) => line.trim())
|
|
36
|
-
.filter(Boolean)
|
|
37
|
-
.map((line) => {
|
|
38
|
-
const indexStatus = line[0] ?? "";
|
|
39
|
-
const worktreeStatus = line[1] ?? "";
|
|
40
|
-
const file = line.slice(3).trim();
|
|
41
|
-
return { indexStatus, worktreeStatus, file };
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function getGitStatus({ pathspec } = {}) {
|
|
46
|
-
const args = ["status", "--branch", "--short"];
|
|
47
|
-
if (pathspec) args.push(pathspec);
|
|
48
|
-
const stdout = await execGitText(args, { allowNonZero: true });
|
|
49
|
-
const lines = stdout.split("\n").filter(Boolean);
|
|
50
|
-
const branchLine = lines.shift() ?? "";
|
|
51
|
-
const match = branchLine.match(/^##\s+([^.\s]+)(?:\.\.\.(\S+))?(.*)$/);
|
|
52
|
-
const branch = match?.[1] ?? null;
|
|
53
|
-
const remote = match?.[2] ?? null;
|
|
54
|
-
const statusTail = match?.[3] ?? "";
|
|
55
|
-
const aheadMatch = statusTail.match(/ahead (\d+)/);
|
|
56
|
-
const behindMatch = statusTail.match(/behind (\d+)/);
|
|
57
|
-
const ahead = aheadMatch ? Number.parseInt(aheadMatch[1], 10) : 0;
|
|
58
|
-
const behind = behindMatch ? Number.parseInt(behindMatch[1], 10) : 0;
|
|
59
|
-
const entries = parseShortStatus(lines.join("\n"));
|
|
60
|
-
const staged = entries.filter((item) => item.indexStatus && item.indexStatus !== " " && item.indexStatus !== "?");
|
|
61
|
-
const unstaged = entries.filter((item) => item.worktreeStatus && item.worktreeStatus !== " " && item.indexStatus !== "?");
|
|
62
|
-
const untracked = entries.filter((item) => item.indexStatus === "?" && item.worktreeStatus === "?");
|
|
63
|
-
return {
|
|
64
|
-
branch,
|
|
65
|
-
remote,
|
|
66
|
-
ahead,
|
|
67
|
-
behind,
|
|
68
|
-
staged: staged.map((item) => ({ status: item.indexStatus, file: item.file })),
|
|
69
|
-
unstaged: unstaged.map((item) => ({ status: item.worktreeStatus, file: item.file })),
|
|
70
|
-
untracked: untracked.map((item) => ({ file: item.file })),
|
|
71
|
-
raw: stdout.trim(),
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function validateCommitMessage(message) {
|
|
76
|
-
const pattern = config.policy?.git?.commitMessageRegex;
|
|
77
|
-
if (!pattern) return;
|
|
78
|
-
try {
|
|
79
|
-
const regex = new RegExp(pattern);
|
|
80
|
-
if (!regex.test(message)) {
|
|
81
|
-
throw new Error(
|
|
82
|
-
`Commit message "${message}" does not satisfy POLICY_GIT_COMMIT_REGEX (${pattern}).`,
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
} catch (err) {
|
|
86
|
-
if (err instanceof SyntaxError) {
|
|
87
|
-
logger.warn({ err, pattern }, "Invalid commit message regex");
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
throw err;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function runPreCommitChecks({ skipTests, source } = {}) {
|
|
95
|
-
const testCommand = config.policy?.git?.testCommand?.trim();
|
|
96
|
-
const requireTests = config.policy?.git?.requireTests === true;
|
|
97
|
-
if (!testCommand) {
|
|
98
|
-
return {
|
|
99
|
-
ran: false,
|
|
100
|
-
skipped: false,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
if (skipTests === true && !requireTests) {
|
|
104
|
-
logger.info("Skipping pre-commit test command on user request.");
|
|
105
|
-
return {
|
|
106
|
-
ran: false,
|
|
107
|
-
skipped: true,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
const result = await runProcess({
|
|
111
|
-
command: "bash",
|
|
112
|
-
args: ["-lc", testCommand],
|
|
113
|
-
cwd: workspaceRoot,
|
|
114
|
-
timeoutMs: 300000,
|
|
115
|
-
env: {
|
|
116
|
-
PRECOMMIT_SOURCE: source ?? "workspace_git_commit",
|
|
117
|
-
},
|
|
118
|
-
});
|
|
119
|
-
if (result.exitCode !== 0) {
|
|
120
|
-
const error = new Error(
|
|
121
|
-
`Pre-commit test command failed (exit ${result.exitCode}). See stderr for details.`,
|
|
122
|
-
);
|
|
123
|
-
error.stdout = result.stdout;
|
|
124
|
-
error.stderr = result.stderr;
|
|
125
|
-
throw error;
|
|
126
|
-
}
|
|
127
|
-
return {
|
|
128
|
-
ran: true,
|
|
129
|
-
skipped: false,
|
|
130
|
-
durationMs: result.durationMs,
|
|
131
|
-
stdout: result.stdout,
|
|
132
|
-
stderr: result.stderr,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async function summarizeWithModel({ prompt, model, system, responseFormat }) {
|
|
137
|
-
try {
|
|
138
|
-
const body = {
|
|
139
|
-
model,
|
|
140
|
-
messages: [
|
|
141
|
-
{
|
|
142
|
-
role: "system",
|
|
143
|
-
content:
|
|
144
|
-
system ??
|
|
145
|
-
"You are an expert developer providing concise, actionable summaries of git changes.",
|
|
146
|
-
},
|
|
147
|
-
{ role: "user", content: prompt },
|
|
148
|
-
],
|
|
149
|
-
stream: false,
|
|
150
|
-
};
|
|
151
|
-
if (responseFormat) {
|
|
152
|
-
body.response_format = responseFormat;
|
|
153
|
-
}
|
|
154
|
-
const response = await invokeModel(body);
|
|
155
|
-
if (!response.ok || !response.json) {
|
|
156
|
-
throw new Error(
|
|
157
|
-
`Model call failed with status ${response.status}: ${response.text ?? "Unknown error"}`,
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
let content = response.json.choices?.[0]?.message?.content ?? "";
|
|
161
|
-
if (Array.isArray(content)) {
|
|
162
|
-
content = content
|
|
163
|
-
.map((part) => (typeof part === "string" ? part : part?.text ?? ""))
|
|
164
|
-
.join("");
|
|
165
|
-
}
|
|
166
|
-
return typeof content === "string" ? content.trim() : "";
|
|
167
|
-
} catch (err) {
|
|
168
|
-
logger.warn({ err }, "Model summarization failed");
|
|
169
|
-
throw err;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function registerGitTools() {
|
|
174
|
-
registerTool(
|
|
175
|
-
"workspace_diff",
|
|
176
|
-
async ({ args = {} }) => {
|
|
177
|
-
const pathspec =
|
|
178
|
-
typeof args.path === "string"
|
|
179
|
-
? args.path
|
|
180
|
-
: typeof args.file === "string"
|
|
181
|
-
? args.file
|
|
182
|
-
: undefined;
|
|
183
|
-
const staged = args.staged === true;
|
|
184
|
-
const unified =
|
|
185
|
-
typeof args.unified === "number" && args.unified >= 0 ? args.unified : 3;
|
|
186
|
-
|
|
187
|
-
const statusArgs = ["status", "--short"];
|
|
188
|
-
const diffArgs = ["diff", `--unified=${unified}`];
|
|
189
|
-
if (staged) {
|
|
190
|
-
diffArgs.splice(1, 0, "--cached");
|
|
191
|
-
}
|
|
192
|
-
if (pathspec) {
|
|
193
|
-
diffArgs.push(pathspec);
|
|
194
|
-
statusArgs.push(pathspec);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const statusOutput = await execGitText(statusArgs, { allowNonZero: true });
|
|
198
|
-
let diffOutput = "";
|
|
199
|
-
try {
|
|
200
|
-
diffOutput = await execGitText(diffArgs, { allowNonZero: true });
|
|
201
|
-
} catch (err) {
|
|
202
|
-
diffOutput = err.stdout ?? err.stderr ?? "";
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const statusEntries = parseShortStatus(statusOutput).map((item) => ({
|
|
206
|
-
status: `${item.indexStatus}${item.worktreeStatus}`.trim(),
|
|
207
|
-
file: item.file,
|
|
208
|
-
}));
|
|
209
|
-
|
|
210
|
-
const numstatArgs = ["diff", "--numstat"];
|
|
211
|
-
if (staged) numstatArgs.splice(1, 0, "--cached");
|
|
212
|
-
if (pathspec) numstatArgs.push(pathspec);
|
|
213
|
-
let numstatOutput = "";
|
|
214
|
-
try {
|
|
215
|
-
numstatOutput = await execGitText(numstatArgs, { allowNonZero: true });
|
|
216
|
-
} catch (err) {
|
|
217
|
-
numstatOutput = err.stdout ?? err.stderr ?? "";
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const files = numstatOutput
|
|
221
|
-
.split("\n")
|
|
222
|
-
.map((line) => line.trim())
|
|
223
|
-
.filter(Boolean)
|
|
224
|
-
.map((line) => {
|
|
225
|
-
const [additionsRaw, deletionsRaw, file] = line.split("\t");
|
|
226
|
-
const additions = additionsRaw === "-" ? null : Number.parseInt(additionsRaw, 10);
|
|
227
|
-
const deletions = deletionsRaw === "-" ? null : Number.parseInt(deletionsRaw, 10);
|
|
228
|
-
return {
|
|
229
|
-
file,
|
|
230
|
-
additions: Number.isNaN(additions) ? 0 : additions,
|
|
231
|
-
deletions: Number.isNaN(deletions) ? 0 : deletions,
|
|
232
|
-
};
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
const totals = files.reduce(
|
|
236
|
-
(acc, item) => {
|
|
237
|
-
acc.additions += item.additions ?? 0;
|
|
238
|
-
acc.deletions += item.deletions ?? 0;
|
|
239
|
-
return acc;
|
|
240
|
-
},
|
|
241
|
-
{ additions: 0, deletions: 0 },
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
return {
|
|
245
|
-
ok: true,
|
|
246
|
-
status: 200,
|
|
247
|
-
content: JSON.stringify(
|
|
248
|
-
{
|
|
249
|
-
staged,
|
|
250
|
-
path: pathspec ?? null,
|
|
251
|
-
status: statusEntries,
|
|
252
|
-
totals,
|
|
253
|
-
files,
|
|
254
|
-
diff: diffOutput.trim(),
|
|
255
|
-
},
|
|
256
|
-
null,
|
|
257
|
-
2,
|
|
258
|
-
),
|
|
259
|
-
metadata: {
|
|
260
|
-
staged,
|
|
261
|
-
path: pathspec ?? null,
|
|
262
|
-
filesChanged: files.length,
|
|
263
|
-
additions: totals.additions,
|
|
264
|
-
deletions: totals.deletions,
|
|
265
|
-
},
|
|
266
|
-
};
|
|
267
|
-
},
|
|
268
|
-
{ category: "git" },
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
registerTool(
|
|
272
|
-
"workspace_diff_comments",
|
|
273
|
-
async ({ args = {} }, context = {}) => {
|
|
274
|
-
const action = args.action ?? "list";
|
|
275
|
-
if (action === "list") {
|
|
276
|
-
const filePath =
|
|
277
|
-
typeof args.file === "string"
|
|
278
|
-
? args.file
|
|
279
|
-
: typeof args.path === "string"
|
|
280
|
-
? args.path
|
|
281
|
-
: undefined;
|
|
282
|
-
const threadId = typeof args.thread === "string" ? args.thread : undefined;
|
|
283
|
-
const comments = listDiffComments({ filePath, threadId });
|
|
284
|
-
return {
|
|
285
|
-
ok: true,
|
|
286
|
-
status: 200,
|
|
287
|
-
content: JSON.stringify({ comments }, null, 2),
|
|
288
|
-
metadata: {
|
|
289
|
-
count: comments.length,
|
|
290
|
-
},
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (action === "add") {
|
|
295
|
-
const filePath =
|
|
296
|
-
typeof args.file === "string"
|
|
297
|
-
? args.file
|
|
298
|
-
: typeof args.path === "string"
|
|
299
|
-
? args.path
|
|
300
|
-
: (() => {
|
|
301
|
-
throw new Error("diff comment requires a file path.");
|
|
302
|
-
})();
|
|
303
|
-
const comment = args.comment ?? args.text ?? args.body;
|
|
304
|
-
if (typeof comment !== "string" || comment.trim().length === 0) {
|
|
305
|
-
throw new Error("diff comment requires comment text.");
|
|
306
|
-
}
|
|
307
|
-
const line =
|
|
308
|
-
typeof args.line === "number"
|
|
309
|
-
? args.line
|
|
310
|
-
: typeof args.row === "number"
|
|
311
|
-
? args.row
|
|
312
|
-
: undefined;
|
|
313
|
-
const hunk = typeof args.hunk === "string" ? args.hunk : undefined;
|
|
314
|
-
const threadId = typeof args.thread === "string" ? args.thread : undefined;
|
|
315
|
-
const author =
|
|
316
|
-
typeof args.author === "string"
|
|
317
|
-
? args.author
|
|
318
|
-
: context.session?.id ?? context.sessionId ?? null;
|
|
319
|
-
|
|
320
|
-
const record = addDiffComment({
|
|
321
|
-
threadId,
|
|
322
|
-
sessionId: context.session?.id ?? context.sessionId ?? null,
|
|
323
|
-
filePath,
|
|
324
|
-
line,
|
|
325
|
-
hunk,
|
|
326
|
-
comment,
|
|
327
|
-
author,
|
|
328
|
-
});
|
|
329
|
-
return {
|
|
330
|
-
ok: true,
|
|
331
|
-
status: 201,
|
|
332
|
-
content: JSON.stringify(record, null, 2),
|
|
333
|
-
metadata: {
|
|
334
|
-
id: record.id,
|
|
335
|
-
threadId: record.threadId,
|
|
336
|
-
},
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (action === "delete") {
|
|
341
|
-
const id = args.id ?? args.comment_id ?? args.commentId;
|
|
342
|
-
if (!id) {
|
|
343
|
-
throw new Error("diff comment delete requires an id.");
|
|
344
|
-
}
|
|
345
|
-
const success = deleteDiffComment({ id });
|
|
346
|
-
return {
|
|
347
|
-
ok: success,
|
|
348
|
-
status: success ? 200 : 404,
|
|
349
|
-
content: JSON.stringify(
|
|
350
|
-
{
|
|
351
|
-
id,
|
|
352
|
-
deleted: success,
|
|
353
|
-
},
|
|
354
|
-
null,
|
|
355
|
-
2,
|
|
356
|
-
),
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
throw new Error(`Unsupported diff comment action: ${action}`);
|
|
361
|
-
},
|
|
362
|
-
{ category: "git" },
|
|
363
|
-
);
|
|
364
|
-
|
|
365
|
-
registerTool(
|
|
366
|
-
"workspace_diff_summary",
|
|
367
|
-
async ({ args = {} }) => {
|
|
368
|
-
const pathspec =
|
|
369
|
-
typeof args.path === "string"
|
|
370
|
-
? args.path
|
|
371
|
-
: typeof args.file === "string"
|
|
372
|
-
? args.file
|
|
373
|
-
: undefined;
|
|
374
|
-
const staged = args.staged === true;
|
|
375
|
-
const unified =
|
|
376
|
-
typeof args.unified === "number" && args.unified >= 0 ? args.unified : 3;
|
|
377
|
-
const model = typeof args.model === "string" ? args.model : "databricks-claude-sonnet-4-5";
|
|
378
|
-
const maxChars =
|
|
379
|
-
typeof args.max_chars === "number" && args.max_chars > 0 ? args.max_chars : 6000;
|
|
380
|
-
|
|
381
|
-
const diffResult = await execGitText(
|
|
382
|
-
staged
|
|
383
|
-
? ["diff", "--cached", `--unified=${unified}`, ...(pathspec ? [pathspec] : [])]
|
|
384
|
-
: ["diff", `--unified=${unified}`, ...(pathspec ? [pathspec] : [])],
|
|
385
|
-
{ allowNonZero: true },
|
|
386
|
-
);
|
|
387
|
-
|
|
388
|
-
const diffText = diffResult || "";
|
|
389
|
-
if (diffText.trim().length === 0) {
|
|
390
|
-
return {
|
|
391
|
-
ok: true,
|
|
392
|
-
status: 200,
|
|
393
|
-
content: JSON.stringify(
|
|
394
|
-
{
|
|
395
|
-
staged,
|
|
396
|
-
path: pathspec ?? null,
|
|
397
|
-
summary: "No changes detected.",
|
|
398
|
-
diffPreview: "",
|
|
399
|
-
},
|
|
400
|
-
null,
|
|
401
|
-
2,
|
|
402
|
-
),
|
|
403
|
-
metadata: {
|
|
404
|
-
staged,
|
|
405
|
-
path: pathspec ?? null,
|
|
406
|
-
summary: "No changes detected.",
|
|
407
|
-
},
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
const preview =
|
|
412
|
-
diffText.length > maxChars
|
|
413
|
-
? `${diffText.slice(0, maxChars)}\n... (truncated)`
|
|
414
|
-
: diffText;
|
|
415
|
-
|
|
416
|
-
const prompt = `You are an expert developer. Analyze the diff and respond with JSON:
|
|
417
|
-
{
|
|
418
|
-
"summary": "<plain text overview>",
|
|
419
|
-
"per_file": [{"file": "...", "changes": "..."}],
|
|
420
|
-
"risks": ["risk item", ...],
|
|
421
|
-
"tests": ["test to run", ...],
|
|
422
|
-
"followups": ["follow-up task", ...]
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
Git diff:
|
|
426
|
-
\`\`\`
|
|
427
|
-
${preview}
|
|
428
|
-
\`\`\``;
|
|
429
|
-
|
|
430
|
-
let summaryText = "";
|
|
431
|
-
let risks = [];
|
|
432
|
-
let tests = [];
|
|
433
|
-
let followups = [];
|
|
434
|
-
let perFile = [];
|
|
435
|
-
try {
|
|
436
|
-
summaryText = await summarizeWithModel({
|
|
437
|
-
prompt,
|
|
438
|
-
model,
|
|
439
|
-
system:
|
|
440
|
-
"You are a senior engineer providing structured review feedback. Return valid JSON exactly matching the requested schema.",
|
|
441
|
-
responseFormat: { type: "json_object" },
|
|
442
|
-
});
|
|
443
|
-
const parsed = JSON.parse(summaryText);
|
|
444
|
-
summaryText = parsed.summary ?? "";
|
|
445
|
-
risks = Array.isArray(parsed.risks) ? parsed.risks : [];
|
|
446
|
-
tests = Array.isArray(parsed.tests) ? parsed.tests : [];
|
|
447
|
-
followups = Array.isArray(parsed.followups) ? parsed.followups : [];
|
|
448
|
-
perFile = Array.isArray(parsed.per_file) ? parsed.per_file : [];
|
|
449
|
-
} catch (err) {
|
|
450
|
-
logger.warn({ err }, "Diff summary generation failed");
|
|
451
|
-
summaryText = `Automated summary unavailable: ${err.message}`;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
return {
|
|
455
|
-
ok: true,
|
|
456
|
-
status: 200,
|
|
457
|
-
content: JSON.stringify(
|
|
458
|
-
{
|
|
459
|
-
staged,
|
|
460
|
-
path: pathspec ?? null,
|
|
461
|
-
summary: summaryText,
|
|
462
|
-
perFile,
|
|
463
|
-
risks,
|
|
464
|
-
tests,
|
|
465
|
-
followups,
|
|
466
|
-
diffPreview: preview,
|
|
467
|
-
},
|
|
468
|
-
null,
|
|
469
|
-
2,
|
|
470
|
-
),
|
|
471
|
-
metadata: {
|
|
472
|
-
staged,
|
|
473
|
-
path: pathspec ?? null,
|
|
474
|
-
},
|
|
475
|
-
};
|
|
476
|
-
},
|
|
477
|
-
{ category: "git" },
|
|
478
|
-
);
|
|
479
|
-
|
|
480
|
-
registerTool(
|
|
481
|
-
"workspace_git_status",
|
|
482
|
-
async ({ args = {} }) => {
|
|
483
|
-
const pathspec =
|
|
484
|
-
typeof args.path === "string"
|
|
485
|
-
? args.path
|
|
486
|
-
: typeof args.file === "string"
|
|
487
|
-
? args.file
|
|
488
|
-
: undefined;
|
|
489
|
-
const status = await getGitStatus({ pathspec });
|
|
490
|
-
return {
|
|
491
|
-
ok: true,
|
|
492
|
-
status: 200,
|
|
493
|
-
content: JSON.stringify(status, null, 2),
|
|
494
|
-
metadata: {
|
|
495
|
-
branch: status.branch,
|
|
496
|
-
ahead: status.ahead,
|
|
497
|
-
behind: status.behind,
|
|
498
|
-
staged: status.staged.length,
|
|
499
|
-
unstaged: status.unstaged.length,
|
|
500
|
-
untracked: status.untracked.length,
|
|
501
|
-
},
|
|
502
|
-
};
|
|
503
|
-
},
|
|
504
|
-
{ category: "git" },
|
|
505
|
-
);
|
|
506
|
-
|
|
507
|
-
registerTool(
|
|
508
|
-
"workspace_git_stage",
|
|
509
|
-
async ({ args = {} }) => {
|
|
510
|
-
const paths =
|
|
511
|
-
Array.isArray(args.paths) && args.paths.length
|
|
512
|
-
? args.paths.map(String)
|
|
513
|
-
: typeof args.path === "string"
|
|
514
|
-
? [args.path]
|
|
515
|
-
: typeof args.file === "string"
|
|
516
|
-
? [args.file]
|
|
517
|
-
: [];
|
|
518
|
-
if (args.all === true || paths.length === 0) {
|
|
519
|
-
await execGit(["add", "--all"]);
|
|
520
|
-
} else {
|
|
521
|
-
await execGit(["add", ...paths]);
|
|
522
|
-
}
|
|
523
|
-
const status = await getGitStatus({});
|
|
524
|
-
return {
|
|
525
|
-
ok: true,
|
|
526
|
-
status: 200,
|
|
527
|
-
content: JSON.stringify(
|
|
528
|
-
{
|
|
529
|
-
staged: status.staged,
|
|
530
|
-
unstaged: status.unstaged,
|
|
531
|
-
untracked: status.untracked,
|
|
532
|
-
},
|
|
533
|
-
null,
|
|
534
|
-
2,
|
|
535
|
-
),
|
|
536
|
-
};
|
|
537
|
-
},
|
|
538
|
-
{ category: "git" },
|
|
539
|
-
);
|
|
540
|
-
|
|
541
|
-
registerTool(
|
|
542
|
-
"workspace_git_unstage",
|
|
543
|
-
async ({ args = {} }) => {
|
|
544
|
-
const paths =
|
|
545
|
-
Array.isArray(args.paths) && args.paths.length
|
|
546
|
-
? args.paths.map(String)
|
|
547
|
-
: typeof args.path === "string"
|
|
548
|
-
? [args.path]
|
|
549
|
-
: typeof args.file === "string"
|
|
550
|
-
? [args.file]
|
|
551
|
-
: [];
|
|
552
|
-
if (args.all === true || paths.length === 0) {
|
|
553
|
-
await execGit(["restore", "--staged", "."], { allowNonZero: true });
|
|
554
|
-
} else {
|
|
555
|
-
await execGit(["restore", "--staged", ...paths], { allowNonZero: true });
|
|
556
|
-
}
|
|
557
|
-
const status = await getGitStatus({});
|
|
558
|
-
return {
|
|
559
|
-
ok: true,
|
|
560
|
-
status: 200,
|
|
561
|
-
content: JSON.stringify(
|
|
562
|
-
{
|
|
563
|
-
staged: status.staged,
|
|
564
|
-
unstaged: status.unstaged,
|
|
565
|
-
untracked: status.untracked,
|
|
566
|
-
},
|
|
567
|
-
null,
|
|
568
|
-
2,
|
|
569
|
-
),
|
|
570
|
-
};
|
|
571
|
-
},
|
|
572
|
-
{ category: "git" },
|
|
573
|
-
);
|
|
574
|
-
|
|
575
|
-
registerTool(
|
|
576
|
-
"workspace_git_commit",
|
|
577
|
-
async ({ args = {} }) => {
|
|
578
|
-
const message = args.message ?? args.msg;
|
|
579
|
-
if (typeof message !== "string" || message.trim().length === 0) {
|
|
580
|
-
throw new Error("Commit message is required.");
|
|
581
|
-
}
|
|
582
|
-
validateCommitMessage(message);
|
|
583
|
-
|
|
584
|
-
const tests = await runPreCommitChecks({
|
|
585
|
-
skipTests: args.skip_tests === true || args.skipTests === true,
|
|
586
|
-
source: "workspace_git_commit",
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
const commitArgs = ["commit", "-m", message];
|
|
590
|
-
if (args.amend === true) {
|
|
591
|
-
commitArgs.push("--amend");
|
|
592
|
-
if (args.no_edit !== false) {
|
|
593
|
-
commitArgs.push("--no-edit");
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
const result = await execGit(commitArgs, { timeoutMs: 20000, allowNonZero: true });
|
|
597
|
-
if (result.exitCode !== 0) {
|
|
598
|
-
throw new Error(result.stderr || result.stdout || "git commit failed");
|
|
599
|
-
}
|
|
600
|
-
const status = await getGitStatus({});
|
|
601
|
-
return {
|
|
602
|
-
ok: true,
|
|
603
|
-
status: 200,
|
|
604
|
-
content: JSON.stringify(
|
|
605
|
-
{
|
|
606
|
-
message,
|
|
607
|
-
output: result.stdout.trim(),
|
|
608
|
-
branch: status.branch,
|
|
609
|
-
preCommit: tests,
|
|
610
|
-
},
|
|
611
|
-
null,
|
|
612
|
-
2,
|
|
613
|
-
),
|
|
614
|
-
metadata: {
|
|
615
|
-
branch: status.branch,
|
|
616
|
-
},
|
|
617
|
-
};
|
|
618
|
-
},
|
|
619
|
-
{ category: "git" },
|
|
620
|
-
);
|
|
621
|
-
|
|
622
|
-
registerTool(
|
|
623
|
-
"workspace_git_push",
|
|
624
|
-
async ({ args = {} }) => {
|
|
625
|
-
const remote = args.remote ?? "origin";
|
|
626
|
-
const branch = args.branch ?? args.ref ?? "HEAD";
|
|
627
|
-
const pushArgs = ["push", remote, branch];
|
|
628
|
-
if (config.policy?.git?.autoStash === true && args.autostash !== false) {
|
|
629
|
-
await execGit(["stash", "push", "--include-untracked", "-m", "auto-stash-before-push"], {
|
|
630
|
-
allowNonZero: true,
|
|
631
|
-
timeoutMs: 10000,
|
|
632
|
-
});
|
|
633
|
-
}
|
|
634
|
-
if (args.force === true) pushArgs.splice(1, 0, "--force-with-lease");
|
|
635
|
-
const result = await execGit(pushArgs, { timeoutMs: 20000, allowNonZero: true });
|
|
636
|
-
if (result.exitCode !== 0) {
|
|
637
|
-
throw new Error(result.stderr || result.stdout || "git push failed");
|
|
638
|
-
}
|
|
639
|
-
return {
|
|
640
|
-
ok: true,
|
|
641
|
-
status: 200,
|
|
642
|
-
content: JSON.stringify(
|
|
643
|
-
{
|
|
644
|
-
remote,
|
|
645
|
-
branch,
|
|
646
|
-
output: result.stdout.trim(),
|
|
647
|
-
},
|
|
648
|
-
null,
|
|
649
|
-
2,
|
|
650
|
-
),
|
|
651
|
-
};
|
|
652
|
-
},
|
|
653
|
-
{ category: "git" },
|
|
654
|
-
);
|
|
655
|
-
|
|
656
|
-
registerTool(
|
|
657
|
-
"workspace_git_pull",
|
|
658
|
-
async ({ args = {} }) => {
|
|
659
|
-
const remote = args.remote ?? "origin";
|
|
660
|
-
const branch = args.branch ?? args.ref ?? "";
|
|
661
|
-
const pullArgs = branch ? ["pull", remote, branch] : ["pull", remote];
|
|
662
|
-
const result = await execGit(pullArgs, { timeoutMs: 20000, allowNonZero: true });
|
|
663
|
-
if (result.exitCode !== 0) {
|
|
664
|
-
throw new Error(result.stderr || result.stdout || "git pull failed");
|
|
665
|
-
}
|
|
666
|
-
const status = await getGitStatus({});
|
|
667
|
-
return {
|
|
668
|
-
ok: true,
|
|
669
|
-
status: 200,
|
|
670
|
-
content: JSON.stringify(
|
|
671
|
-
{
|
|
672
|
-
remote,
|
|
673
|
-
branch: branch || null,
|
|
674
|
-
output: result.stdout.trim(),
|
|
675
|
-
branchStatus: status,
|
|
676
|
-
},
|
|
677
|
-
null,
|
|
678
|
-
2,
|
|
679
|
-
),
|
|
680
|
-
};
|
|
681
|
-
},
|
|
682
|
-
{ category: "git" },
|
|
683
|
-
);
|
|
684
|
-
|
|
685
|
-
registerTool(
|
|
686
|
-
"workspace_git_merge",
|
|
687
|
-
async ({ args = {} }) => {
|
|
688
|
-
const source =
|
|
689
|
-
typeof args.source === "string"
|
|
690
|
-
? args.source
|
|
691
|
-
: typeof args.branch === "string"
|
|
692
|
-
? args.branch
|
|
693
|
-
: (() => {
|
|
694
|
-
throw new Error("workspace_git_merge requires a source branch.");
|
|
695
|
-
})();
|
|
696
|
-
const noCommit = args.no_commit === true || args.noCommit === true;
|
|
697
|
-
const squash = args.squash === true;
|
|
698
|
-
const fastForwardOnly = args.ff_only === true || args.ffOnly === true;
|
|
699
|
-
const mergeArgs = ["merge"];
|
|
700
|
-
if (noCommit) mergeArgs.push("--no-commit");
|
|
701
|
-
if (squash) mergeArgs.push("--squash");
|
|
702
|
-
if (fastForwardOnly) mergeArgs.push("--ff-only");
|
|
703
|
-
mergeArgs.push(source);
|
|
704
|
-
const result = await execGit(mergeArgs, { timeoutMs: 20000, allowNonZero: true });
|
|
705
|
-
if (result.exitCode !== 0) {
|
|
706
|
-
throw new Error(result.stderr || result.stdout || "git merge failed");
|
|
707
|
-
}
|
|
708
|
-
const status = await getGitStatus({});
|
|
709
|
-
return {
|
|
710
|
-
ok: true,
|
|
711
|
-
status: 200,
|
|
712
|
-
content: JSON.stringify(
|
|
713
|
-
{
|
|
714
|
-
source,
|
|
715
|
-
output: result.stdout.trim(),
|
|
716
|
-
status,
|
|
717
|
-
},
|
|
718
|
-
null,
|
|
719
|
-
2,
|
|
720
|
-
),
|
|
721
|
-
};
|
|
722
|
-
},
|
|
723
|
-
{ category: "git" },
|
|
724
|
-
);
|
|
725
|
-
|
|
726
|
-
registerTool(
|
|
727
|
-
"workspace_git_rebase",
|
|
728
|
-
async ({ args = {} }) => {
|
|
729
|
-
const onto =
|
|
730
|
-
typeof args.onto === "string"
|
|
731
|
-
? args.onto
|
|
732
|
-
: typeof args.upstream === "string"
|
|
733
|
-
? args.upstream
|
|
734
|
-
: typeof args.branch === "string"
|
|
735
|
-
? args.branch
|
|
736
|
-
: (() => {
|
|
737
|
-
throw new Error("workspace_git_rebase requires an upstream branch.");
|
|
738
|
-
})();
|
|
739
|
-
const interactive = args.interactive === true || args.i === true;
|
|
740
|
-
const autostash = config.policy?.git?.autoStash === true;
|
|
741
|
-
if (autostash && args.autostash !== false) {
|
|
742
|
-
await execGit(["stash", "push", "--include-untracked", "-m", "auto-stash-before-rebase"], {
|
|
743
|
-
allowNonZero: true,
|
|
744
|
-
timeoutMs: 10000,
|
|
745
|
-
});
|
|
746
|
-
}
|
|
747
|
-
const rebaseArgs = ["rebase"];
|
|
748
|
-
if (interactive) rebaseArgs.push("-i");
|
|
749
|
-
if (args.keep_empty === true) rebaseArgs.push("--keep-empty");
|
|
750
|
-
if (args.autostash === true || (autostash && args.autostash !== false)) {
|
|
751
|
-
rebaseArgs.push("--autostash");
|
|
752
|
-
}
|
|
753
|
-
rebaseArgs.push(onto);
|
|
754
|
-
const result = await execGit(rebaseArgs, { timeoutMs: 30000, allowNonZero: true });
|
|
755
|
-
if (result.exitCode !== 0) {
|
|
756
|
-
throw new Error(result.stderr || result.stdout || "git rebase failed");
|
|
757
|
-
}
|
|
758
|
-
const status = await getGitStatus({});
|
|
759
|
-
return {
|
|
760
|
-
ok: true,
|
|
761
|
-
status: 200,
|
|
762
|
-
content: JSON.stringify(
|
|
763
|
-
{
|
|
764
|
-
onto,
|
|
765
|
-
interactive,
|
|
766
|
-
output: result.stdout.trim(),
|
|
767
|
-
status,
|
|
768
|
-
},
|
|
769
|
-
null,
|
|
770
|
-
2,
|
|
771
|
-
),
|
|
772
|
-
};
|
|
773
|
-
},
|
|
774
|
-
{ category: "git" },
|
|
775
|
-
);
|
|
776
|
-
|
|
777
|
-
registerTool(
|
|
778
|
-
"workspace_git_conflicts",
|
|
779
|
-
async () => {
|
|
780
|
-
const result = await execGit(["diff", "--name-only", "--diff-filter=U"], {
|
|
781
|
-
allowNonZero: true,
|
|
782
|
-
});
|
|
783
|
-
const files = result
|
|
784
|
-
.trim()
|
|
785
|
-
.split("\n")
|
|
786
|
-
.map((line) => line.trim())
|
|
787
|
-
.filter(Boolean);
|
|
788
|
-
const details = [];
|
|
789
|
-
for (const file of files) {
|
|
790
|
-
try {
|
|
791
|
-
const absolute = resolveWorkspacePath(file);
|
|
792
|
-
const content = await fsp.readFile(absolute, "utf8");
|
|
793
|
-
const conflictCount = (content.match(/<<<<<<< /g) || []).length;
|
|
794
|
-
details.push({
|
|
795
|
-
file,
|
|
796
|
-
conflicts: conflictCount,
|
|
797
|
-
});
|
|
798
|
-
} catch (err) {
|
|
799
|
-
details.push({
|
|
800
|
-
file,
|
|
801
|
-
conflicts: null,
|
|
802
|
-
error: err.message,
|
|
803
|
-
});
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
return {
|
|
807
|
-
ok: true,
|
|
808
|
-
status: 200,
|
|
809
|
-
content: JSON.stringify(
|
|
810
|
-
{
|
|
811
|
-
files,
|
|
812
|
-
details,
|
|
813
|
-
},
|
|
814
|
-
null,
|
|
815
|
-
2,
|
|
816
|
-
),
|
|
817
|
-
metadata: {
|
|
818
|
-
count: files.length,
|
|
819
|
-
},
|
|
820
|
-
};
|
|
821
|
-
},
|
|
822
|
-
{ category: "git" },
|
|
823
|
-
);
|
|
824
|
-
|
|
825
|
-
registerTool(
|
|
826
|
-
"workspace_git_branches",
|
|
827
|
-
async ({ args = {} }) => {
|
|
828
|
-
const includeRemote = args.remote === true;
|
|
829
|
-
const branchArgs = includeRemote ? ["branch", "--all"] : ["branch"];
|
|
830
|
-
const stdout = await execGitText(branchArgs, { allowNonZero: true });
|
|
831
|
-
const branches = stdout
|
|
832
|
-
.split("\n")
|
|
833
|
-
.map((line) => line.trim())
|
|
834
|
-
.filter(Boolean)
|
|
835
|
-
.map((line) => ({
|
|
836
|
-
current: line.startsWith("*"),
|
|
837
|
-
name: line.replace(/^\*/, "").trim(),
|
|
838
|
-
}));
|
|
839
|
-
return {
|
|
840
|
-
ok: true,
|
|
841
|
-
status: 200,
|
|
842
|
-
content: JSON.stringify({ branches }, null, 2),
|
|
843
|
-
metadata: {
|
|
844
|
-
total: branches.length,
|
|
845
|
-
},
|
|
846
|
-
};
|
|
847
|
-
},
|
|
848
|
-
{ category: "git" },
|
|
849
|
-
);
|
|
850
|
-
|
|
851
|
-
registerTool(
|
|
852
|
-
"workspace_git_checkout",
|
|
853
|
-
async ({ args = {} }) => {
|
|
854
|
-
const branch = args.branch ?? args.name;
|
|
855
|
-
if (!branch) {
|
|
856
|
-
throw new Error("Provide a branch name.");
|
|
857
|
-
}
|
|
858
|
-
const create = args.create === true;
|
|
859
|
-
const checkoutArgs = create ? ["checkout", "-b", branch] : ["checkout", branch];
|
|
860
|
-
const result = await execGit(checkoutArgs, { timeoutMs: 15000, allowNonZero: true });
|
|
861
|
-
if (result.exitCode !== 0) {
|
|
862
|
-
throw new Error(result.stderr || result.stdout || "git checkout failed");
|
|
863
|
-
}
|
|
864
|
-
const status = await getGitStatus({});
|
|
865
|
-
return {
|
|
866
|
-
ok: true,
|
|
867
|
-
status: 200,
|
|
868
|
-
content: JSON.stringify(
|
|
869
|
-
{
|
|
870
|
-
branch,
|
|
871
|
-
created: create,
|
|
872
|
-
output: result.stdout.trim(),
|
|
873
|
-
currentBranch: status.branch,
|
|
874
|
-
},
|
|
875
|
-
null,
|
|
876
|
-
2,
|
|
877
|
-
),
|
|
878
|
-
};
|
|
879
|
-
},
|
|
880
|
-
{ category: "git" },
|
|
881
|
-
);
|
|
882
|
-
|
|
883
|
-
registerTool(
|
|
884
|
-
"workspace_git_stash",
|
|
885
|
-
async ({ args = {} }) => {
|
|
886
|
-
const action = (args.action ?? "push").toLowerCase();
|
|
887
|
-
if (["push", "save"].includes(action)) {
|
|
888
|
-
const message = args.message ?? args.msg ?? "WIP";
|
|
889
|
-
const stashArgs = ["stash", "push", "-m", message];
|
|
890
|
-
if (args.include_untracked === true) stashArgs.push("--include-untracked");
|
|
891
|
-
const result = await execGit(stashArgs, { timeoutMs: 10000, allowNonZero: true });
|
|
892
|
-
if (result.exitCode !== 0) {
|
|
893
|
-
throw new Error(result.stderr || result.stdout || "git stash push failed");
|
|
894
|
-
}
|
|
895
|
-
return {
|
|
896
|
-
ok: true,
|
|
897
|
-
status: 200,
|
|
898
|
-
content: JSON.stringify(
|
|
899
|
-
{
|
|
900
|
-
action: "push",
|
|
901
|
-
message,
|
|
902
|
-
output: result.stdout.trim(),
|
|
903
|
-
},
|
|
904
|
-
null,
|
|
905
|
-
2,
|
|
906
|
-
),
|
|
907
|
-
};
|
|
908
|
-
}
|
|
909
|
-
if (action === "pop") {
|
|
910
|
-
const result = await execGit(["stash", "pop"], { timeoutMs: 10000, allowNonZero: true });
|
|
911
|
-
if (result.exitCode !== 0) {
|
|
912
|
-
throw new Error(result.stderr || result.stdout || "git stash pop failed");
|
|
913
|
-
}
|
|
914
|
-
return {
|
|
915
|
-
ok: true,
|
|
916
|
-
status: 200,
|
|
917
|
-
content: JSON.stringify(
|
|
918
|
-
{
|
|
919
|
-
action: "pop",
|
|
920
|
-
output: result.stdout.trim(),
|
|
921
|
-
},
|
|
922
|
-
null,
|
|
923
|
-
2,
|
|
924
|
-
),
|
|
925
|
-
};
|
|
926
|
-
}
|
|
927
|
-
if (action === "list") {
|
|
928
|
-
const stdout = await execGitText(["stash", "list"], { allowNonZero: true });
|
|
929
|
-
return {
|
|
930
|
-
ok: true,
|
|
931
|
-
status: 200,
|
|
932
|
-
content: JSON.stringify(
|
|
933
|
-
{
|
|
934
|
-
action: "list",
|
|
935
|
-
stashes: stdout
|
|
936
|
-
.split("\n")
|
|
937
|
-
.map((line) => line.trim())
|
|
938
|
-
.filter(Boolean),
|
|
939
|
-
},
|
|
940
|
-
null,
|
|
941
|
-
2,
|
|
942
|
-
),
|
|
943
|
-
};
|
|
944
|
-
}
|
|
945
|
-
throw new Error(`Unsupported stash action: ${action}`);
|
|
946
|
-
},
|
|
947
|
-
{ category: "git" },
|
|
948
|
-
);
|
|
949
|
-
|
|
950
|
-
registerTool(
|
|
951
|
-
"workspace_git_patch_plan",
|
|
952
|
-
async ({ args = {} }) => {
|
|
953
|
-
const staged = args.staged === true;
|
|
954
|
-
const files =
|
|
955
|
-
Array.isArray(args.files) && args.files.length
|
|
956
|
-
? args.files.map(String)
|
|
957
|
-
: typeof args.file === "string"
|
|
958
|
-
? [args.file]
|
|
959
|
-
: [];
|
|
960
|
-
|
|
961
|
-
const diffArgs = staged ? ["diff", "--cached"] : ["diff"];
|
|
962
|
-
if (files.length) diffArgs.push("--", ...files);
|
|
963
|
-
const diffOutput = await execGitText(diffArgs, { allowNonZero: true });
|
|
964
|
-
if (!diffOutput.trim()) {
|
|
965
|
-
return {
|
|
966
|
-
ok: true,
|
|
967
|
-
status: 200,
|
|
968
|
-
content: JSON.stringify(
|
|
969
|
-
{
|
|
970
|
-
staged,
|
|
971
|
-
files,
|
|
972
|
-
plan: [],
|
|
973
|
-
diff: "",
|
|
974
|
-
message: "No changes to plan.",
|
|
975
|
-
},
|
|
976
|
-
null,
|
|
977
|
-
2,
|
|
978
|
-
),
|
|
979
|
-
};
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
const prompt = `You are helping plan how to apply this diff. Produce JSON describing discrete patch steps and verification notes. Output format:
|
|
983
|
-
{
|
|
984
|
-
"steps": [
|
|
985
|
-
{"file": "...", "summary": "...", "intent": "...", "risk": "low|medium|high"}
|
|
986
|
-
],
|
|
987
|
-
"tests": ["command or check", ...],
|
|
988
|
-
"notes": ["additional considerations"]
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
Diff:
|
|
992
|
-
\`\`\`
|
|
993
|
-
${diffOutput.slice(0, 12000)}
|
|
994
|
-
\`\`\``;
|
|
995
|
-
|
|
996
|
-
let planText;
|
|
997
|
-
try {
|
|
998
|
-
planText = await summarizeWithModel({
|
|
999
|
-
prompt,
|
|
1000
|
-
model: args.model ?? "databricks-claude-sonnet-4-5",
|
|
1001
|
-
system:
|
|
1002
|
-
"You are a senior engineer decomposing diffs into actionable patch steps. Respond with valid JSON.",
|
|
1003
|
-
responseFormat: { type: "json_object" },
|
|
1004
|
-
});
|
|
1005
|
-
} catch (err) {
|
|
1006
|
-
logger.warn({ err }, "Patch planning failed");
|
|
1007
|
-
planText = JSON.stringify(
|
|
1008
|
-
{
|
|
1009
|
-
steps: [],
|
|
1010
|
-
tests: [],
|
|
1011
|
-
notes: [`Automated plan unavailable: ${err.message}`],
|
|
1012
|
-
},
|
|
1013
|
-
null,
|
|
1014
|
-
2,
|
|
1015
|
-
);
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
return {
|
|
1019
|
-
ok: true,
|
|
1020
|
-
status: 200,
|
|
1021
|
-
content: planText,
|
|
1022
|
-
};
|
|
1023
|
-
},
|
|
1024
|
-
{ category: "git" },
|
|
1025
|
-
);
|
|
1026
|
-
|
|
1027
|
-
registerTool(
|
|
1028
|
-
"workspace_diff_review",
|
|
1029
|
-
async ({ args = {} }) => {
|
|
1030
|
-
const staged = args.staged === true;
|
|
1031
|
-
const pathspec =
|
|
1032
|
-
typeof args.path === "string"
|
|
1033
|
-
? args.path
|
|
1034
|
-
: typeof args.file === "string"
|
|
1035
|
-
? args.file
|
|
1036
|
-
: undefined;
|
|
1037
|
-
const unified =
|
|
1038
|
-
typeof args.unified === "number" && args.unified >= 0 ? args.unified : 5;
|
|
1039
|
-
const reviewerModel =
|
|
1040
|
-
typeof args.model === "string" ? args.model : "databricks-claude-sonnet-4-5";
|
|
1041
|
-
const diffText =
|
|
1042
|
-
(await execGitText(
|
|
1043
|
-
staged
|
|
1044
|
-
? ["diff", "--cached", `--unified=${unified}`, ...(pathspec ? [pathspec] : [])]
|
|
1045
|
-
: ["diff", `--unified=${unified}`, ...(pathspec ? [pathspec] : [])],
|
|
1046
|
-
{ allowNonZero: true },
|
|
1047
|
-
)) || "";
|
|
1048
|
-
if (diffText.trim().length === 0) {
|
|
1049
|
-
return {
|
|
1050
|
-
ok: true,
|
|
1051
|
-
status: 200,
|
|
1052
|
-
content: JSON.stringify(
|
|
1053
|
-
{
|
|
1054
|
-
summary: "No changes to review.",
|
|
1055
|
-
checklist: [],
|
|
1056
|
-
comments: [],
|
|
1057
|
-
},
|
|
1058
|
-
null,
|
|
1059
|
-
2,
|
|
1060
|
-
),
|
|
1061
|
-
};
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
let review = {
|
|
1065
|
-
summary: "",
|
|
1066
|
-
checklist: [],
|
|
1067
|
-
comments: [],
|
|
1068
|
-
};
|
|
1069
|
-
const prompt = `You are reviewing code changes. Provide JSON with:
|
|
1070
|
-
{
|
|
1071
|
-
"summary": "<overview>",
|
|
1072
|
-
"checklist": ["item1", "item2"],
|
|
1073
|
-
"comments": [{"file": "...", "line": <number|null>, "comment": "..."}]
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
Git diff:
|
|
1077
|
-
\`\`\`
|
|
1078
|
-
${diffText}
|
|
1079
|
-
\`\`\``;
|
|
1080
|
-
|
|
1081
|
-
try {
|
|
1082
|
-
const content = await summarizeWithModel({
|
|
1083
|
-
prompt,
|
|
1084
|
-
model: reviewerModel,
|
|
1085
|
-
system:
|
|
1086
|
-
"You are a senior engineer performing a thorough code review. Respond with valid JSON matching the requested shape.",
|
|
1087
|
-
responseFormat: { type: "json_object" },
|
|
1088
|
-
});
|
|
1089
|
-
review = JSON.parse(content);
|
|
1090
|
-
} catch (err) {
|
|
1091
|
-
logger.warn({ err }, "Diff review generation failed");
|
|
1092
|
-
review.summary = `Automated review unavailable: ${err.message}`;
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
return {
|
|
1096
|
-
ok: true,
|
|
1097
|
-
status: 200,
|
|
1098
|
-
content: JSON.stringify(
|
|
1099
|
-
{
|
|
1100
|
-
staged,
|
|
1101
|
-
path: pathspec ?? null,
|
|
1102
|
-
summary: review.summary ?? "",
|
|
1103
|
-
checklist: Array.isArray(review.checklist) ? review.checklist : [],
|
|
1104
|
-
comments: Array.isArray(review.comments) ? review.comments : [],
|
|
1105
|
-
},
|
|
1106
|
-
null,
|
|
1107
|
-
2,
|
|
1108
|
-
),
|
|
1109
|
-
};
|
|
1110
|
-
},
|
|
1111
|
-
{ category: "git" },
|
|
1112
|
-
);
|
|
1113
|
-
|
|
1114
|
-
registerTool(
|
|
1115
|
-
"workspace_release_notes",
|
|
1116
|
-
async ({ args = {} }) => {
|
|
1117
|
-
const commitLimit =
|
|
1118
|
-
typeof args.limit === "number" && args.limit > 0 ? args.limit : 20;
|
|
1119
|
-
const since = args.since ?? args.from;
|
|
1120
|
-
const until = args.until ?? args.to;
|
|
1121
|
-
const model =
|
|
1122
|
-
typeof args.model === "string" ? args.model : "databricks-claude-sonnet-4-5";
|
|
1123
|
-
|
|
1124
|
-
const logArgs = ["log", `-n${commitLimit}`, "--pretty=format:%H%x09%an%x09%ad%x09%s"];
|
|
1125
|
-
if (since && until) {
|
|
1126
|
-
logArgs.splice(2, 0, `${since}..${until}`);
|
|
1127
|
-
} else if (since) {
|
|
1128
|
-
logArgs.splice(2, 0, `${since}..HEAD`);
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
const stdout = await execGitText(logArgs, { allowNonZero: true });
|
|
1132
|
-
if (!stdout.trim()) {
|
|
1133
|
-
return {
|
|
1134
|
-
ok: true,
|
|
1135
|
-
status: 200,
|
|
1136
|
-
content: JSON.stringify(
|
|
1137
|
-
{
|
|
1138
|
-
notes: "No commits found for the specified range.",
|
|
1139
|
-
commits: [],
|
|
1140
|
-
},
|
|
1141
|
-
null,
|
|
1142
|
-
2,
|
|
1143
|
-
),
|
|
1144
|
-
};
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
const commits = stdout.split("\n").map((line) => {
|
|
1148
|
-
const [hash, author, date, subject] = line.split("\t");
|
|
1149
|
-
return {
|
|
1150
|
-
hash,
|
|
1151
|
-
shortHash: hash.slice(0, 8),
|
|
1152
|
-
author,
|
|
1153
|
-
date,
|
|
1154
|
-
subject,
|
|
1155
|
-
};
|
|
1156
|
-
});
|
|
1157
|
-
|
|
1158
|
-
const prompt = `Generate release notes for the following commits. Group related changes, highlight key features, bug fixes, breaking changes, and list follow-up tasks if relevant.
|
|
1159
|
-
|
|
1160
|
-
Commits:
|
|
1161
|
-
${commits
|
|
1162
|
-
.map((c) => `- ${c.shortHash} ${c.subject} (${c.author} on ${c.date})`)
|
|
1163
|
-
.join("\n")}
|
|
1164
|
-
`;
|
|
1165
|
-
|
|
1166
|
-
let notes;
|
|
1167
|
-
try {
|
|
1168
|
-
notes = await summarizeWithModel({
|
|
1169
|
-
prompt,
|
|
1170
|
-
model,
|
|
1171
|
-
system:
|
|
1172
|
-
"You are a release manager summarizing recent changes. Produce Markdown release notes with sections such as Highlights, Fixes, Improvements, and Breaking Changes when appropriate.",
|
|
1173
|
-
});
|
|
1174
|
-
} catch (err) {
|
|
1175
|
-
notes = `Automated release notes unavailable: ${err.message}`;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
return {
|
|
1179
|
-
ok: true,
|
|
1180
|
-
status: 200,
|
|
1181
|
-
content: JSON.stringify(
|
|
1182
|
-
{
|
|
1183
|
-
notes,
|
|
1184
|
-
commits,
|
|
1185
|
-
},
|
|
1186
|
-
null,
|
|
1187
|
-
2,
|
|
1188
|
-
),
|
|
1189
|
-
};
|
|
1190
|
-
},
|
|
1191
|
-
{ category: "git" },
|
|
1192
|
-
);
|
|
1193
|
-
|
|
1194
|
-
registerTool(
|
|
1195
|
-
"workspace_diff_by_commit",
|
|
1196
|
-
async ({ args = {} }) => {
|
|
1197
|
-
const since = args.since ?? args.from;
|
|
1198
|
-
const until = args.until ?? args.to;
|
|
1199
|
-
const limit =
|
|
1200
|
-
typeof args.limit === "number" && args.limit > 0 ? args.limit : 10;
|
|
1201
|
-
|
|
1202
|
-
const range = since && until ? `${since}..${until}` : since ? `${since}..HEAD` : null;
|
|
1203
|
-
const commitsArgs = ["log", `-n${limit}`, "--pretty=format:%H"];
|
|
1204
|
-
if (range) commitsArgs.splice(2, 0, range);
|
|
1205
|
-
const stdout = await execGitText(commitsArgs, { allowNonZero: true });
|
|
1206
|
-
const commitHashes = stdout
|
|
1207
|
-
.split("\n")
|
|
1208
|
-
.map((line) => line.trim())
|
|
1209
|
-
.filter(Boolean);
|
|
1210
|
-
const results = [];
|
|
1211
|
-
for (const hash of commitHashes) {
|
|
1212
|
-
const showArgs = ["show", "--stat", "--pretty=format:%H%x09%an%x09%ad%x09%s", hash];
|
|
1213
|
-
const showOutput = await execGitText(showArgs, { allowNonZero: true });
|
|
1214
|
-
const [header, ...statLines] = showOutput.split("\n").filter(Boolean);
|
|
1215
|
-
const [commitHash, author, date, subject] = header.split("\t");
|
|
1216
|
-
const files = statLines.map((line) => line.trim());
|
|
1217
|
-
results.push({
|
|
1218
|
-
commit: commitHash,
|
|
1219
|
-
shortHash: commitHash.slice(0, 8),
|
|
1220
|
-
author,
|
|
1221
|
-
date,
|
|
1222
|
-
subject,
|
|
1223
|
-
files,
|
|
1224
|
-
});
|
|
1225
|
-
}
|
|
1226
|
-
return {
|
|
1227
|
-
ok: true,
|
|
1228
|
-
status: 200,
|
|
1229
|
-
content: JSON.stringify(
|
|
1230
|
-
{
|
|
1231
|
-
range: range ?? "latest",
|
|
1232
|
-
commits: results,
|
|
1233
|
-
},
|
|
1234
|
-
null,
|
|
1235
|
-
2,
|
|
1236
|
-
),
|
|
1237
|
-
};
|
|
1238
|
-
},
|
|
1239
|
-
{ category: "git" },
|
|
1240
|
-
);
|
|
1241
|
-
|
|
1242
|
-
registerTool(
|
|
1243
|
-
"workspace_changelog_generate",
|
|
1244
|
-
async ({ args = {} }) => {
|
|
1245
|
-
const since = args.since ?? args.from;
|
|
1246
|
-
const until = args.until ?? args.to ?? "HEAD";
|
|
1247
|
-
const limit = typeof args.limit === "number" ? Math.min(Math.max(args.limit, 1), 200) : 50;
|
|
1248
|
-
const range = since ? `${since}..${until}` : `HEAD~${limit}..${until}`;
|
|
1249
|
-
const logArgs = ["log", range, "--pretty=format:%H%x09%ad%x09%an%x09%s"];
|
|
1250
|
-
const stdout = await execGitText(logArgs, { allowNonZero: true });
|
|
1251
|
-
const rows = stdout
|
|
1252
|
-
.split("\n")
|
|
1253
|
-
.map((line) => line.trim())
|
|
1254
|
-
.filter(Boolean)
|
|
1255
|
-
.map((line) => {
|
|
1256
|
-
const [hash, date, author, subject] = line.split("\t");
|
|
1257
|
-
return { hash, shortHash: hash.slice(0, 8), date, author, subject };
|
|
1258
|
-
});
|
|
1259
|
-
|
|
1260
|
-
const prompt = `Produce a chronological changelog in Markdown for these commits.
|
|
1261
|
-
Each entry should include commit short hash, title, author, and highlight notable impact.
|
|
1262
|
-
|
|
1263
|
-
Commits:
|
|
1264
|
-
${rows
|
|
1265
|
-
.map((row) => `${row.shortHash} | ${row.subject} | ${row.author} | ${row.date}`)
|
|
1266
|
-
.join("\n")}
|
|
1267
|
-
`;
|
|
1268
|
-
let changelog;
|
|
1269
|
-
try {
|
|
1270
|
-
changelog = await summarizeWithModel({
|
|
1271
|
-
prompt,
|
|
1272
|
-
model: args.model ?? "databricks-claude-sonnet-4-5",
|
|
1273
|
-
system:
|
|
1274
|
-
"You are a release manager writing concise Markdown changelog entries grouped by theme when possible.",
|
|
1275
|
-
});
|
|
1276
|
-
} catch (err) {
|
|
1277
|
-
changelog = `Automated changelog unavailable: ${err.message}`;
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
return {
|
|
1281
|
-
ok: true,
|
|
1282
|
-
status: 200,
|
|
1283
|
-
content: JSON.stringify(
|
|
1284
|
-
{
|
|
1285
|
-
range,
|
|
1286
|
-
commits: rows,
|
|
1287
|
-
changelog,
|
|
1288
|
-
},
|
|
1289
|
-
null,
|
|
1290
|
-
2,
|
|
1291
|
-
),
|
|
1292
|
-
};
|
|
1293
|
-
},
|
|
1294
|
-
{ category: "git" },
|
|
1295
|
-
);
|
|
1296
|
-
|
|
1297
|
-
registerTool(
|
|
1298
|
-
"workspace_pr_template_generate",
|
|
1299
|
-
async ({ args = {} }) => {
|
|
1300
|
-
const stagedOnly = args.staged === true;
|
|
1301
|
-
const diffArgs = stagedOnly ? ["diff", "--cached"] : ["diff"];
|
|
1302
|
-
const diff = await execGitText(diffArgs, { allowNonZero: true });
|
|
1303
|
-
const status = await getGitStatus({});
|
|
1304
|
-
const prompt = `Generate a pull request description template based on this diff and git status.
|
|
1305
|
-
Include sections: Summary, Testing, Risk, Rollback Plan, Related Tickets.
|
|
1306
|
-
|
|
1307
|
-
Git Status:
|
|
1308
|
-
${status.raw}
|
|
1309
|
-
|
|
1310
|
-
Diff:
|
|
1311
|
-
\`\`\`
|
|
1312
|
-
${diff.slice(0, 15000)}
|
|
1313
|
-
\`\`\``;
|
|
1314
|
-
|
|
1315
|
-
let template;
|
|
1316
|
-
try {
|
|
1317
|
-
template = await summarizeWithModel({
|
|
1318
|
-
prompt,
|
|
1319
|
-
model: args.model ?? "databricks-claude-sonnet-4-5",
|
|
1320
|
-
system:
|
|
1321
|
-
"You are preparing a high-quality pull request description. Output Markdown with clear section headers and actionable content.",
|
|
1322
|
-
});
|
|
1323
|
-
} catch (err) {
|
|
1324
|
-
template = `Automated PR template unavailable: ${err.message}`;
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
return {
|
|
1328
|
-
ok: true,
|
|
1329
|
-
status: 200,
|
|
1330
|
-
content: JSON.stringify(
|
|
1331
|
-
{
|
|
1332
|
-
stagedOnly,
|
|
1333
|
-
template,
|
|
1334
|
-
},
|
|
1335
|
-
null,
|
|
1336
|
-
2,
|
|
1337
|
-
),
|
|
1338
|
-
};
|
|
1339
|
-
},
|
|
1340
|
-
{ category: "git" },
|
|
1341
|
-
);
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
module.exports = {
|
|
1345
|
-
registerGitTools,
|
|
1346
|
-
};
|