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.
Files changed (73) hide show
  1. package/README.md +92 -23
  2. package/bin/cli.js +13 -7
  3. package/bin/lynkr-init.js +34 -12
  4. package/bin/lynkr-reset.js +71 -0
  5. package/config/model-tiers.json +199 -52
  6. package/package.json +3 -3
  7. package/scripts/build-eval-set.js +256 -0
  8. package/scripts/mine-difficulty-anchors.js +288 -0
  9. package/scripts/validate-difficulty-classifier.js +144 -0
  10. package/scripts/validate-intent-anchors.js +186 -0
  11. package/src/api/providers-handler.js +0 -1
  12. package/src/api/router.js +292 -160
  13. package/src/clients/databricks.js +95 -6
  14. package/src/config/index.js +3 -43
  15. package/src/context/tool-result-compressor.js +883 -40
  16. package/src/orchestrator/index.js +117 -1235
  17. package/src/orchestrator/passthrough-stream.js +382 -0
  18. package/src/orchestrator/sse-transformer.js +408 -0
  19. package/src/routing/affinity-store.js +17 -3
  20. package/src/routing/classifier-setup.js +207 -0
  21. package/src/routing/complexity-analyzer.js +40 -4
  22. package/src/routing/difficulty-classifier.js +261 -0
  23. package/src/routing/index.js +70 -7
  24. package/src/routing/intent-score.js +132 -12
  25. package/src/routing/session-affinity.js +8 -1
  26. package/src/routing/side-channel-detector.js +103 -0
  27. package/src/server.js +20 -46
  28. package/src/agents/context-manager.js +0 -236
  29. package/src/agents/decomposition/dispatcher.js +0 -185
  30. package/src/agents/decomposition/gate.js +0 -136
  31. package/src/agents/decomposition/index.js +0 -183
  32. package/src/agents/decomposition/model-call.js +0 -75
  33. package/src/agents/decomposition/planner.js +0 -223
  34. package/src/agents/decomposition/synthesizer.js +0 -89
  35. package/src/agents/decomposition/telemetry.js +0 -55
  36. package/src/agents/definitions/loader.js +0 -653
  37. package/src/agents/executor.js +0 -457
  38. package/src/agents/index.js +0 -165
  39. package/src/agents/parallel-coordinator.js +0 -68
  40. package/src/agents/reflector.js +0 -331
  41. package/src/agents/skillbook.js +0 -331
  42. package/src/agents/store.js +0 -259
  43. package/src/edits/index.js +0 -171
  44. package/src/indexer/babel-parser.js +0 -213
  45. package/src/indexer/index.js +0 -1629
  46. package/src/indexer/navigation/index.js +0 -32
  47. package/src/indexer/navigation/providers/treeSitter.js +0 -36
  48. package/src/indexer/parser.js +0 -443
  49. package/src/tasks/store.js +0 -349
  50. package/src/tests/coverage.js +0 -173
  51. package/src/tests/index.js +0 -171
  52. package/src/tests/store.js +0 -213
  53. package/src/tools/agent-task.js +0 -145
  54. package/src/tools/code-mode.js +0 -304
  55. package/src/tools/decompose.js +0 -91
  56. package/src/tools/edits.js +0 -94
  57. package/src/tools/execution.js +0 -171
  58. package/src/tools/git.js +0 -1346
  59. package/src/tools/index.js +0 -306
  60. package/src/tools/indexer.js +0 -360
  61. package/src/tools/lazy-loader.js +0 -366
  62. package/src/tools/mcp-remote.js +0 -88
  63. package/src/tools/mcp.js +0 -116
  64. package/src/tools/process.js +0 -167
  65. package/src/tools/smart-selection.js +0 -180
  66. package/src/tools/stubs.js +0 -55
  67. package/src/tools/tasks.js +0 -260
  68. package/src/tools/tests.js +0 -132
  69. package/src/tools/tinyfish.js +0 -358
  70. package/src/tools/truncate.js +0 -106
  71. package/src/tools/web-client.js +0 -71
  72. package/src/tools/web.js +0 -415
  73. 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
- };