xforce 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +269 -147
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +125 -49
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -310,10 +310,10 @@ function resolveRepoConfig(config, owner, name) {
|
|
|
310
310
|
};
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
// src/
|
|
314
|
-
import {
|
|
315
|
-
import {
|
|
316
|
-
import
|
|
313
|
+
// src/core/credentials.ts
|
|
314
|
+
import { existsSync as existsSync2, mkdirSync, writeFileSync } from "fs";
|
|
315
|
+
import { join } from "path";
|
|
316
|
+
import { homedir } from "os";
|
|
317
317
|
|
|
318
318
|
// src/core/logger.ts
|
|
319
319
|
import pino from "pino";
|
|
@@ -335,6 +335,44 @@ function createChildLogger(name) {
|
|
|
335
335
|
return logger.child({ component: name });
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
+
// src/core/credentials.ts
|
|
339
|
+
var log = createChildLogger("credentials");
|
|
340
|
+
function getClaudeConfigDir() {
|
|
341
|
+
return process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude");
|
|
342
|
+
}
|
|
343
|
+
function ensureCredentials() {
|
|
344
|
+
const configDir = getClaudeConfigDir();
|
|
345
|
+
const credPath = join(configDir, ".credentials.json");
|
|
346
|
+
const envCreds = process.env.CLAUDE_CREDENTIALS;
|
|
347
|
+
if (existsSync2(credPath)) {
|
|
348
|
+
log.debug({ path: credPath }, "Credentials file exists");
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (!envCreds) {
|
|
352
|
+
throw new Error(
|
|
353
|
+
`Claude Code credentials not found. Either:
|
|
354
|
+
1. Log in with 'claude' CLI locally, or
|
|
355
|
+
2. Set CLAUDE_CREDENTIALS env var with the JSON credentials content.
|
|
356
|
+
Checked path: ${credPath}`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
JSON.parse(envCreds);
|
|
361
|
+
} catch {
|
|
362
|
+
throw new Error(
|
|
363
|
+
"CLAUDE_CREDENTIALS env var contains invalid JSON. It should be the contents of .credentials.json from a Claude Code login."
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
mkdirSync(configDir, { recursive: true });
|
|
367
|
+
writeFileSync(credPath, envCreds, { mode: 384 });
|
|
368
|
+
log.info({ path: credPath }, "Wrote Claude Code credentials from env var");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// src/pipeline/orchestrator.ts
|
|
372
|
+
import { nanoid } from "nanoid";
|
|
373
|
+
import { rm } from "fs/promises";
|
|
374
|
+
import ora from "ora";
|
|
375
|
+
|
|
338
376
|
// src/pipeline/state-machine.ts
|
|
339
377
|
var VALID_TRANSITIONS = {
|
|
340
378
|
parsing_issue: ["creating_branch", "failed"],
|
|
@@ -374,13 +412,13 @@ ${truncated}`;
|
|
|
374
412
|
|
|
375
413
|
// src/github/client.ts
|
|
376
414
|
import { Octokit } from "@octokit/rest";
|
|
377
|
-
var
|
|
415
|
+
var log2 = createChildLogger("github");
|
|
378
416
|
var RETRY_OPTIONS = {
|
|
379
417
|
maxRetries: 3,
|
|
380
418
|
baseDelayMs: 1e3,
|
|
381
419
|
maxDelayMs: 3e4,
|
|
382
420
|
onRetry: (error, attempt) => {
|
|
383
|
-
|
|
421
|
+
log2.warn({ error: error.message, attempt }, "GitHub API call failed, retrying");
|
|
384
422
|
}
|
|
385
423
|
};
|
|
386
424
|
async function withGitHubRetry(fn) {
|
|
@@ -415,7 +453,7 @@ function getOctokit() {
|
|
|
415
453
|
async function getIssue(owner, repo, issueNumber) {
|
|
416
454
|
return withGitHubRetry(async () => {
|
|
417
455
|
const octokit = getOctokit();
|
|
418
|
-
|
|
456
|
+
log2.debug({ owner, repo, issueNumber }, "Fetching issue");
|
|
419
457
|
const { data } = await octokit.issues.get({ owner, repo, issue_number: issueNumber });
|
|
420
458
|
return data;
|
|
421
459
|
});
|
|
@@ -434,13 +472,13 @@ async function getIssueLabels(owner, repo, issueNumber) {
|
|
|
434
472
|
async function addLabel(owner, repo, issueNumber, label) {
|
|
435
473
|
return withGitHubRetry(async () => {
|
|
436
474
|
const octokit = getOctokit();
|
|
437
|
-
|
|
475
|
+
log2.debug({ owner, repo, issueNumber, label }, "Adding label");
|
|
438
476
|
await octokit.issues.addLabels({ owner, repo, issue_number: issueNumber, labels: [label] });
|
|
439
477
|
});
|
|
440
478
|
}
|
|
441
479
|
async function removeLabel(owner, repo, issueNumber, label) {
|
|
442
480
|
const octokit = getOctokit();
|
|
443
|
-
|
|
481
|
+
log2.debug({ owner, repo, issueNumber, label }, "Removing label");
|
|
444
482
|
try {
|
|
445
483
|
await octokit.issues.removeLabel({ owner, repo, issue_number: issueNumber, name: label });
|
|
446
484
|
} catch {
|
|
@@ -449,7 +487,7 @@ async function removeLabel(owner, repo, issueNumber, label) {
|
|
|
449
487
|
async function addComment(owner, repo, issueNumber, body) {
|
|
450
488
|
return withGitHubRetry(async () => {
|
|
451
489
|
const octokit = getOctokit();
|
|
452
|
-
|
|
490
|
+
log2.debug({ owner, repo, issueNumber }, "Adding comment");
|
|
453
491
|
const { data } = await octokit.issues.createComment({
|
|
454
492
|
owner,
|
|
455
493
|
repo,
|
|
@@ -462,7 +500,7 @@ async function addComment(owner, repo, issueNumber, body) {
|
|
|
462
500
|
async function getPRDiff(owner, repo, prNumber) {
|
|
463
501
|
return withGitHubRetry(async () => {
|
|
464
502
|
const octokit = getOctokit();
|
|
465
|
-
|
|
503
|
+
log2.debug({ owner, repo, prNumber }, "Fetching PR diff");
|
|
466
504
|
const { data } = await octokit.pulls.get({
|
|
467
505
|
owner,
|
|
468
506
|
repo,
|
|
@@ -497,7 +535,7 @@ function parsePRUrl(url) {
|
|
|
497
535
|
async function getPR(owner, repo, prNumber) {
|
|
498
536
|
return withGitHubRetry(async () => {
|
|
499
537
|
const octokit = getOctokit();
|
|
500
|
-
|
|
538
|
+
log2.debug({ owner, repo, prNumber }, "Fetching PR");
|
|
501
539
|
const { data } = await octokit.pulls.get({ owner, repo, pull_number: prNumber });
|
|
502
540
|
return data;
|
|
503
541
|
});
|
|
@@ -601,7 +639,7 @@ function parseIssueBody(params) {
|
|
|
601
639
|
|
|
602
640
|
// src/github/branch.ts
|
|
603
641
|
import { tmpdir } from "os";
|
|
604
|
-
import { join } from "path";
|
|
642
|
+
import { join as join2 } from "path";
|
|
605
643
|
import { mkdtemp, access, writeFile } from "fs/promises";
|
|
606
644
|
import { simpleGit } from "simple-git";
|
|
607
645
|
var DEFAULT_GITIGNORE = `node_modules/
|
|
@@ -614,7 +652,7 @@ build/
|
|
|
614
652
|
*.tsbuildinfo
|
|
615
653
|
.DS_Store
|
|
616
654
|
`;
|
|
617
|
-
var
|
|
655
|
+
var log3 = createChildLogger("git");
|
|
618
656
|
function slugify(text) {
|
|
619
657
|
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
620
658
|
}
|
|
@@ -623,13 +661,13 @@ async function setupBranch(params) {
|
|
|
623
661
|
const slug = slugify(issueTitle);
|
|
624
662
|
const branchName = `${branchPrefix}/${issueNumber}-${slug}`;
|
|
625
663
|
if (localDir) {
|
|
626
|
-
|
|
664
|
+
log3.info({ workDir: localDir, branchName }, "Using local repository");
|
|
627
665
|
const repoGit2 = simpleGit(localDir);
|
|
628
666
|
await repoGit2.fetch("origin");
|
|
629
667
|
const status = await repoGit2.status();
|
|
630
668
|
const hadChanges = status.files.length > 0;
|
|
631
669
|
if (hadChanges) {
|
|
632
|
-
|
|
670
|
+
log3.info("Stashing local changes");
|
|
633
671
|
await repoGit2.stash();
|
|
634
672
|
}
|
|
635
673
|
try {
|
|
@@ -639,34 +677,34 @@ async function setupBranch(params) {
|
|
|
639
677
|
}
|
|
640
678
|
await repoGit2.addConfig("user.name", "X-Force Bot");
|
|
641
679
|
await repoGit2.addConfig("user.email", "xforce-bot@users.noreply.github.com");
|
|
642
|
-
|
|
680
|
+
log3.info({ branchName }, "Branch created");
|
|
643
681
|
return { workDir: localDir, branchName, git: repoGit2, isLocal: true };
|
|
644
682
|
}
|
|
645
683
|
const cloneUrl = `https://x-access-token:${process.env.GITHUB_TOKEN}@github.com/${owner}/${repo}.git`;
|
|
646
|
-
const workDir = await mkdtemp(
|
|
647
|
-
|
|
684
|
+
const workDir = await mkdtemp(join2(tmpdir(), `xforce-${repo}-`));
|
|
685
|
+
log3.info({ workDir, branchName }, "Cloning repository");
|
|
648
686
|
const git = simpleGit();
|
|
649
687
|
await git.clone(cloneUrl, workDir, ["--depth", "1", "--branch", defaultBranch]);
|
|
650
688
|
const repoGit = simpleGit(workDir);
|
|
651
689
|
await repoGit.addConfig("user.name", "X-Force Bot");
|
|
652
690
|
await repoGit.addConfig("user.email", "xforce-bot@users.noreply.github.com");
|
|
653
691
|
await repoGit.checkoutLocalBranch(branchName);
|
|
654
|
-
const gitignorePath =
|
|
692
|
+
const gitignorePath = join2(workDir, ".gitignore");
|
|
655
693
|
try {
|
|
656
694
|
await access(gitignorePath);
|
|
657
695
|
} catch {
|
|
658
|
-
|
|
696
|
+
log3.info("Creating default .gitignore");
|
|
659
697
|
await writeFile(gitignorePath, DEFAULT_GITIGNORE, "utf-8");
|
|
660
698
|
}
|
|
661
|
-
|
|
699
|
+
log3.info({ branchName }, "Branch created");
|
|
662
700
|
return { workDir, branchName, git: repoGit, isLocal: false };
|
|
663
701
|
}
|
|
664
702
|
async function restoreDefaultBranch(git, defaultBranch) {
|
|
665
703
|
try {
|
|
666
704
|
await git.checkout(defaultBranch);
|
|
667
|
-
|
|
705
|
+
log3.info({ branch: defaultBranch }, "Restored default branch");
|
|
668
706
|
} catch (error) {
|
|
669
|
-
|
|
707
|
+
log3.warn({ error: String(error) }, "Failed to restore default branch");
|
|
670
708
|
}
|
|
671
709
|
}
|
|
672
710
|
async function commitAndPush(params) {
|
|
@@ -674,23 +712,23 @@ async function commitAndPush(params) {
|
|
|
674
712
|
await git.add("-A");
|
|
675
713
|
const status = await git.status();
|
|
676
714
|
if (status.staged.length === 0 && status.files.length === 0) {
|
|
677
|
-
|
|
715
|
+
log3.warn("No changes to commit");
|
|
678
716
|
return "";
|
|
679
717
|
}
|
|
680
718
|
const commitResult = await git.commit(message);
|
|
681
|
-
|
|
719
|
+
log3.info({ sha: commitResult.commit }, "Changes committed");
|
|
682
720
|
await git.push("origin", branchName, ["--set-upstream", "--force"]);
|
|
683
|
-
|
|
721
|
+
log3.info({ branchName }, "Pushed to remote");
|
|
684
722
|
return commitResult.commit;
|
|
685
723
|
}
|
|
686
724
|
|
|
687
725
|
// src/github/pr.ts
|
|
688
|
-
var
|
|
726
|
+
var log4 = createChildLogger("pr");
|
|
689
727
|
async function createPullRequest(params) {
|
|
690
728
|
const { owner, repo, branchName, defaultBranch, taskSpec, pipeline } = params;
|
|
691
729
|
const octokit = getOctokit();
|
|
692
730
|
const body = buildPRBody(taskSpec, pipeline);
|
|
693
|
-
|
|
731
|
+
log4.info({ owner, repo, branchName }, "Creating pull request");
|
|
694
732
|
const { data } = await octokit.pulls.create({
|
|
695
733
|
owner,
|
|
696
734
|
repo,
|
|
@@ -699,7 +737,7 @@ async function createPullRequest(params) {
|
|
|
699
737
|
head: branchName,
|
|
700
738
|
base: defaultBranch
|
|
701
739
|
});
|
|
702
|
-
|
|
740
|
+
log4.info({ prNumber: data.number, prUrl: data.html_url }, "PR created");
|
|
703
741
|
return { prNumber: data.number, prUrl: data.html_url };
|
|
704
742
|
}
|
|
705
743
|
async function updatePullRequest(params) {
|
|
@@ -719,7 +757,7 @@ async function commentOnPR(owner, repo, prNumber, body) {
|
|
|
719
757
|
async function mergePR(params) {
|
|
720
758
|
const { owner, repo, prNumber, strategy, commitTitle } = params;
|
|
721
759
|
const octokit = getOctokit();
|
|
722
|
-
|
|
760
|
+
log4.info({ owner, repo, prNumber, strategy }, "Attempting to merge PR");
|
|
723
761
|
try {
|
|
724
762
|
const { data } = await octokit.pulls.merge({
|
|
725
763
|
owner,
|
|
@@ -728,14 +766,14 @@ async function mergePR(params) {
|
|
|
728
766
|
merge_method: strategy,
|
|
729
767
|
commit_title: commitTitle
|
|
730
768
|
});
|
|
731
|
-
|
|
769
|
+
log4.info({ sha: data.sha, merged: data.merged }, "PR merge result");
|
|
732
770
|
return { merged: data.merged, sha: data.sha };
|
|
733
771
|
} catch (error) {
|
|
734
772
|
const err = error;
|
|
735
773
|
const message = err.message ?? String(error);
|
|
736
774
|
const status = err.status;
|
|
737
775
|
if (status === 405 || status === 409) {
|
|
738
|
-
|
|
776
|
+
log4.warn({ status, message }, "PR cannot be auto-merged");
|
|
739
777
|
return { merged: false, error: message };
|
|
740
778
|
}
|
|
741
779
|
throw error;
|
|
@@ -916,7 +954,7 @@ Follow this plan closely. If you discover the plan is incorrect or incomplete, a
|
|
|
916
954
|
}
|
|
917
955
|
|
|
918
956
|
// src/agents/coder.ts
|
|
919
|
-
var
|
|
957
|
+
var log5 = createChildLogger("coder");
|
|
920
958
|
function truncate(s, max) {
|
|
921
959
|
return s.length > max ? s.slice(0, max) + "..." : s;
|
|
922
960
|
}
|
|
@@ -954,7 +992,7 @@ async function runCodingAgent(params) {
|
|
|
954
992
|
if (commandFailures) {
|
|
955
993
|
prompt += "\n\n" + buildCommandFailureFeedbackSection(commandFailures.kind, commandFailures.output);
|
|
956
994
|
}
|
|
957
|
-
|
|
995
|
+
log5.info(
|
|
958
996
|
{
|
|
959
997
|
task: taskSpec.title,
|
|
960
998
|
model: repoConfig.model,
|
|
@@ -965,6 +1003,7 @@ async function runCodingAgent(params) {
|
|
|
965
1003
|
},
|
|
966
1004
|
"Starting coding agent"
|
|
967
1005
|
);
|
|
1006
|
+
let stderrOutput = "";
|
|
968
1007
|
const result = query({
|
|
969
1008
|
prompt,
|
|
970
1009
|
options: {
|
|
@@ -980,30 +1019,45 @@ async function runCodingAgent(params) {
|
|
|
980
1019
|
preset: "claude_code",
|
|
981
1020
|
append: CODER_SYSTEM_PROMPT_APPEND
|
|
982
1021
|
},
|
|
1022
|
+
stderr: (data) => {
|
|
1023
|
+
stderrOutput += data;
|
|
1024
|
+
log5.debug({ stderr: data.trimEnd() }, "Agent SDK stderr");
|
|
1025
|
+
},
|
|
983
1026
|
...sessionId ? { resume: sessionId } : {}
|
|
984
1027
|
}
|
|
985
1028
|
});
|
|
986
1029
|
let resultMessage;
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1030
|
+
try {
|
|
1031
|
+
for await (const message of result) {
|
|
1032
|
+
if (message.type === "assistant" && "content" in message) {
|
|
1033
|
+
const content = message.content;
|
|
1034
|
+
for (const block of content) {
|
|
1035
|
+
if (block.type === "tool_use" && block.name) {
|
|
1036
|
+
const detail = describeToolUse(block.name, block.input);
|
|
1037
|
+
log5.info({ tool: block.name }, detail);
|
|
1038
|
+
onProgress?.(detail);
|
|
1039
|
+
}
|
|
995
1040
|
}
|
|
996
1041
|
}
|
|
1042
|
+
if (message.type === "result") {
|
|
1043
|
+
resultMessage = message;
|
|
1044
|
+
}
|
|
997
1045
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1048
|
+
const stderrDetail = stderrOutput ? `
|
|
1049
|
+
stderr:
|
|
1050
|
+
${stderrOutput.slice(0, 2e3)}` : "";
|
|
1051
|
+
throw new CodingAgentError(`Coding agent process failed: ${msg}${stderrDetail}`, [], 0);
|
|
1001
1052
|
}
|
|
1002
1053
|
if (!resultMessage) {
|
|
1003
|
-
|
|
1054
|
+
const detail = stderrOutput ? `
|
|
1055
|
+
stderr:
|
|
1056
|
+
${stderrOutput.slice(0, 2e3)}` : "";
|
|
1057
|
+
throw new CodingAgentError(`No result message received from coding agent${detail}`, [], 0);
|
|
1004
1058
|
}
|
|
1005
1059
|
if (resultMessage.subtype === "error_max_turns") {
|
|
1006
|
-
|
|
1060
|
+
log5.warn(
|
|
1007
1061
|
{
|
|
1008
1062
|
cost: resultMessage.total_cost_usd,
|
|
1009
1063
|
turns: resultMessage.num_turns
|
|
@@ -1018,13 +1072,16 @@ async function runCodingAgent(params) {
|
|
|
1018
1072
|
};
|
|
1019
1073
|
}
|
|
1020
1074
|
if (resultMessage.subtype !== "success") {
|
|
1075
|
+
const stderrDetail = stderrOutput ? `
|
|
1076
|
+
stderr:
|
|
1077
|
+
${stderrOutput.slice(0, 2e3)}` : "";
|
|
1021
1078
|
throw new CodingAgentError(
|
|
1022
|
-
`Coding agent failed: ${resultMessage.subtype}`,
|
|
1079
|
+
`Coding agent failed: ${resultMessage.subtype}${stderrDetail}`,
|
|
1023
1080
|
"errors" in resultMessage ? resultMessage.errors : [],
|
|
1024
1081
|
resultMessage.total_cost_usd
|
|
1025
1082
|
);
|
|
1026
1083
|
}
|
|
1027
|
-
|
|
1084
|
+
log5.info(
|
|
1028
1085
|
{
|
|
1029
1086
|
cost: resultMessage.total_cost_usd,
|
|
1030
1087
|
turns: resultMessage.num_turns
|
|
@@ -1042,7 +1099,7 @@ async function runCodingAgent(params) {
|
|
|
1042
1099
|
// src/agents/planner.ts
|
|
1043
1100
|
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
1044
1101
|
import { z as z2 } from "zod";
|
|
1045
|
-
var
|
|
1102
|
+
var log6 = createChildLogger("planner");
|
|
1046
1103
|
var ImplementationStepSchema = z2.object({
|
|
1047
1104
|
order: z2.number().int().positive(),
|
|
1048
1105
|
description: z2.string(),
|
|
@@ -1109,10 +1166,11 @@ function validatePlan(parsed) {
|
|
|
1109
1166
|
async function runPlanningAgent(params) {
|
|
1110
1167
|
const { taskSpec, repoConfig, workingDir, onProgress } = params;
|
|
1111
1168
|
const prompt = buildPlannerPrompt(taskSpec);
|
|
1112
|
-
|
|
1169
|
+
log6.info(
|
|
1113
1170
|
{ task: taskSpec.title, model: repoConfig.plannerModel },
|
|
1114
1171
|
"Starting planning agent"
|
|
1115
1172
|
);
|
|
1173
|
+
let stderrOutput = "";
|
|
1116
1174
|
const result = query2({
|
|
1117
1175
|
prompt,
|
|
1118
1176
|
options: {
|
|
@@ -1126,34 +1184,49 @@ async function runPlanningAgent(params) {
|
|
|
1126
1184
|
outputFormat: {
|
|
1127
1185
|
type: "json_schema",
|
|
1128
1186
|
schema: PLAN_JSON_SCHEMA
|
|
1187
|
+
},
|
|
1188
|
+
stderr: (data) => {
|
|
1189
|
+
stderrOutput += data;
|
|
1190
|
+
log6.debug({ stderr: data.trimEnd() }, "Agent SDK stderr");
|
|
1129
1191
|
}
|
|
1130
1192
|
}
|
|
1131
1193
|
});
|
|
1132
1194
|
let resultMessage;
|
|
1133
1195
|
let lastAssistantText = "";
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1196
|
+
try {
|
|
1197
|
+
for await (const message of result) {
|
|
1198
|
+
if (message.type === "assistant" && "content" in message) {
|
|
1199
|
+
const content = message.content;
|
|
1200
|
+
for (const block of content) {
|
|
1201
|
+
if (block.type === "tool_use" && block.name) {
|
|
1202
|
+
const detail = describeToolUse(block.name, block.input);
|
|
1203
|
+
log6.info({ tool: block.name }, detail);
|
|
1204
|
+
onProgress?.(detail);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
const textParts = content.filter((c) => c.type === "text" && c.text).map((c) => c.text);
|
|
1208
|
+
if (textParts.length > 0) {
|
|
1209
|
+
lastAssistantText = textParts.join("\n");
|
|
1142
1210
|
}
|
|
1143
1211
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
lastAssistantText = textParts.join("\n");
|
|
1212
|
+
if (message.type === "result") {
|
|
1213
|
+
resultMessage = message;
|
|
1147
1214
|
}
|
|
1148
1215
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1216
|
+
} catch (error) {
|
|
1217
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1218
|
+
const stderrDetail = stderrOutput ? `
|
|
1219
|
+
stderr:
|
|
1220
|
+
${stderrOutput.slice(0, 2e3)}` : "";
|
|
1221
|
+
throw new PipelineError(`Planning agent process failed: ${msg}${stderrDetail}`);
|
|
1152
1222
|
}
|
|
1153
1223
|
if (!resultMessage) {
|
|
1154
|
-
|
|
1224
|
+
const detail = stderrOutput ? `
|
|
1225
|
+
stderr:
|
|
1226
|
+
${stderrOutput.slice(0, 2e3)}` : "";
|
|
1227
|
+
throw new PipelineError(`No result message received from planning agent${detail}`);
|
|
1155
1228
|
}
|
|
1156
|
-
|
|
1229
|
+
log6.debug(
|
|
1157
1230
|
{
|
|
1158
1231
|
subtype: resultMessage.subtype,
|
|
1159
1232
|
hasStructuredOutput: "structured_output" in resultMessage && !!resultMessage.structured_output,
|
|
@@ -1167,12 +1240,15 @@ async function runPlanningAgent(params) {
|
|
|
1167
1240
|
const isMaxTurns = resultMessage.subtype === "error_max_turns";
|
|
1168
1241
|
if (!isSuccess && !isMaxTurns) {
|
|
1169
1242
|
const errorDetail = "errors" in resultMessage ? resultMessage.errors.join(", ") : "unknown";
|
|
1243
|
+
const stderrDetail = stderrOutput ? `
|
|
1244
|
+
stderr:
|
|
1245
|
+
${stderrOutput.slice(0, 2e3)}` : "";
|
|
1170
1246
|
throw new PipelineError(
|
|
1171
|
-
`Planning agent failed (${resultMessage.subtype}): ${errorDetail}`
|
|
1247
|
+
`Planning agent failed (${resultMessage.subtype}): ${errorDetail}${stderrDetail}`
|
|
1172
1248
|
);
|
|
1173
1249
|
}
|
|
1174
1250
|
if (isMaxTurns) {
|
|
1175
|
-
|
|
1251
|
+
log6.warn("Planning agent hit max turns \u2014 attempting to extract plan");
|
|
1176
1252
|
}
|
|
1177
1253
|
let parsed;
|
|
1178
1254
|
if ("structured_output" in resultMessage && resultMessage.structured_output) {
|
|
@@ -1187,7 +1263,7 @@ async function runPlanningAgent(params) {
|
|
|
1187
1263
|
);
|
|
1188
1264
|
}
|
|
1189
1265
|
} else if (lastAssistantText) {
|
|
1190
|
-
|
|
1266
|
+
log6.warn("No result output \u2014 extracting plan from last assistant message");
|
|
1191
1267
|
const jsonStr = extractJSON(lastAssistantText);
|
|
1192
1268
|
try {
|
|
1193
1269
|
parsed = JSON.parse(jsonStr);
|
|
@@ -1200,7 +1276,7 @@ async function runPlanningAgent(params) {
|
|
|
1200
1276
|
throw new PipelineError("Planning agent produced no output");
|
|
1201
1277
|
}
|
|
1202
1278
|
const plan = validatePlan(parsed);
|
|
1203
|
-
|
|
1279
|
+
log6.info(
|
|
1204
1280
|
{
|
|
1205
1281
|
complexity: plan.estimatedComplexity,
|
|
1206
1282
|
steps: plan.implementationSteps.length,
|
|
@@ -1282,7 +1358,7 @@ Provide your review as a JSON object matching the specified schema.`;
|
|
|
1282
1358
|
}
|
|
1283
1359
|
|
|
1284
1360
|
// src/agents/reviewer.ts
|
|
1285
|
-
var
|
|
1361
|
+
var log7 = createChildLogger("reviewer");
|
|
1286
1362
|
var ReviewIssueSchema = z3.object({
|
|
1287
1363
|
severity: z3.enum(["critical", "major", "minor", "suggestion"]),
|
|
1288
1364
|
file: z3.string(),
|
|
@@ -1355,11 +1431,12 @@ function validateReviewResult(parsed) {
|
|
|
1355
1431
|
}
|
|
1356
1432
|
async function runReviewerAgent(params) {
|
|
1357
1433
|
const { taskSpec, diff, repoConfig, reviewCycle } = params;
|
|
1358
|
-
|
|
1434
|
+
log7.info(
|
|
1359
1435
|
{ model: repoConfig.reviewerModel, cycle: reviewCycle },
|
|
1360
1436
|
"Starting reviewer agent"
|
|
1361
1437
|
);
|
|
1362
1438
|
const userPrompt = buildReviewerUserPrompt(taskSpec, diff, reviewCycle);
|
|
1439
|
+
let stderrOutput = "";
|
|
1363
1440
|
const agentResult = query3({
|
|
1364
1441
|
prompt: userPrompt,
|
|
1365
1442
|
options: {
|
|
@@ -1372,33 +1449,51 @@ async function runReviewerAgent(params) {
|
|
|
1372
1449
|
outputFormat: {
|
|
1373
1450
|
type: "json_schema",
|
|
1374
1451
|
schema: REVIEW_JSON_SCHEMA
|
|
1452
|
+
},
|
|
1453
|
+
stderr: (data) => {
|
|
1454
|
+
stderrOutput += data;
|
|
1455
|
+
log7.debug({ stderr: data.trimEnd() }, "Agent SDK stderr");
|
|
1375
1456
|
}
|
|
1376
1457
|
}
|
|
1377
1458
|
});
|
|
1378
1459
|
let resultMessage;
|
|
1379
1460
|
let lastAssistantText = "";
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1461
|
+
try {
|
|
1462
|
+
for await (const message of agentResult) {
|
|
1463
|
+
if (message.type === "assistant" && "content" in message) {
|
|
1464
|
+
const textParts = message.content.filter((c) => c.type === "text" && c.text).map((c) => c.text);
|
|
1465
|
+
if (textParts.length > 0) {
|
|
1466
|
+
lastAssistantText = textParts.join("\n");
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
if (message.type === "result") {
|
|
1470
|
+
resultMessage = message;
|
|
1385
1471
|
}
|
|
1386
1472
|
}
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1473
|
+
} catch (error) {
|
|
1474
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1475
|
+
const stderrDetail = stderrOutput ? `
|
|
1476
|
+
stderr:
|
|
1477
|
+
${stderrOutput.slice(0, 2e3)}` : "";
|
|
1478
|
+
throw new ReviewerError(`Reviewer agent process failed: ${msg}${stderrDetail}`);
|
|
1390
1479
|
}
|
|
1391
1480
|
if (!resultMessage) {
|
|
1392
|
-
|
|
1481
|
+
const detail = stderrOutput ? `
|
|
1482
|
+
stderr:
|
|
1483
|
+
${stderrOutput.slice(0, 2e3)}` : "";
|
|
1484
|
+
throw new ReviewerError(`No result message received from reviewer agent${detail}`);
|
|
1393
1485
|
}
|
|
1394
1486
|
const isSuccess = resultMessage.subtype === "success";
|
|
1395
1487
|
const isMaxTurns = resultMessage.subtype === "error_max_turns";
|
|
1396
1488
|
if (!isSuccess && !isMaxTurns) {
|
|
1397
1489
|
const errorDetail = "errors" in resultMessage ? resultMessage.errors.join(", ") : "unknown";
|
|
1398
|
-
|
|
1490
|
+
const stderrDetail = stderrOutput ? `
|
|
1491
|
+
stderr:
|
|
1492
|
+
${stderrOutput.slice(0, 2e3)}` : "";
|
|
1493
|
+
throw new ReviewerError(`Reviewer agent failed (${resultMessage.subtype}): ${errorDetail}${stderrDetail}`);
|
|
1399
1494
|
}
|
|
1400
1495
|
if (isMaxTurns) {
|
|
1401
|
-
|
|
1496
|
+
log7.warn("Reviewer hit max turns \u2014 attempting to extract result");
|
|
1402
1497
|
}
|
|
1403
1498
|
let parsed;
|
|
1404
1499
|
if ("structured_output" in resultMessage && resultMessage.structured_output) {
|
|
@@ -1411,7 +1506,7 @@ async function runReviewerAgent(params) {
|
|
|
1411
1506
|
throw new ReviewerError(`Reviewer returned invalid JSON: ${String(resultMessage.result).slice(0, 200)}`);
|
|
1412
1507
|
}
|
|
1413
1508
|
} else if (lastAssistantText) {
|
|
1414
|
-
|
|
1509
|
+
log7.warn("No result output \u2014 extracting review from last assistant message");
|
|
1415
1510
|
const jsonStr = extractJSON2(lastAssistantText);
|
|
1416
1511
|
try {
|
|
1417
1512
|
parsed = JSON.parse(jsonStr);
|
|
@@ -1424,7 +1519,7 @@ async function runReviewerAgent(params) {
|
|
|
1424
1519
|
throw new ReviewerError("Reviewer agent produced no output");
|
|
1425
1520
|
}
|
|
1426
1521
|
const review = validateReviewResult(parsed);
|
|
1427
|
-
|
|
1522
|
+
log7.info(
|
|
1428
1523
|
{
|
|
1429
1524
|
approved: review.approved,
|
|
1430
1525
|
issueCount: review.issues.length,
|
|
@@ -1438,7 +1533,7 @@ async function runReviewerAgent(params) {
|
|
|
1438
1533
|
// src/agents/security-scanner.ts
|
|
1439
1534
|
import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
|
|
1440
1535
|
import { z as z4 } from "zod";
|
|
1441
|
-
var
|
|
1536
|
+
var log8 = createChildLogger("security-scanner");
|
|
1442
1537
|
var SecurityFindingSchema = z4.object({
|
|
1443
1538
|
severity: z4.enum(["critical", "high", "medium", "low", "info"]),
|
|
1444
1539
|
category: z4.string(),
|
|
@@ -1517,7 +1612,7 @@ function validateReport(parsed) {
|
|
|
1517
1612
|
}
|
|
1518
1613
|
async function runSecurityScanner(params) {
|
|
1519
1614
|
const { taskSpec, diff, repoConfig } = params;
|
|
1520
|
-
|
|
1615
|
+
log8.info({ task: taskSpec.title, model: repoConfig.reviewerModel }, "Starting security scan");
|
|
1521
1616
|
const prompt = `## Security Review Request
|
|
1522
1617
|
|
|
1523
1618
|
**Task**: ${taskSpec.title}
|
|
@@ -1530,6 +1625,7 @@ ${diff}
|
|
|
1530
1625
|
\`\`\`
|
|
1531
1626
|
|
|
1532
1627
|
Analyze this diff for security vulnerabilities. Provide your report as a JSON object matching the required schema.`;
|
|
1628
|
+
let stderrOutput = "";
|
|
1533
1629
|
const result = query4({
|
|
1534
1630
|
prompt,
|
|
1535
1631
|
options: {
|
|
@@ -1542,31 +1638,49 @@ Analyze this diff for security vulnerabilities. Provide your report as a JSON ob
|
|
|
1542
1638
|
outputFormat: {
|
|
1543
1639
|
type: "json_schema",
|
|
1544
1640
|
schema: SECURITY_REPORT_JSON_SCHEMA
|
|
1641
|
+
},
|
|
1642
|
+
stderr: (data) => {
|
|
1643
|
+
stderrOutput += data;
|
|
1644
|
+
log8.debug({ stderr: data.trimEnd() }, "Agent SDK stderr");
|
|
1545
1645
|
}
|
|
1546
1646
|
}
|
|
1547
1647
|
});
|
|
1548
1648
|
let resultMessage;
|
|
1549
1649
|
let lastAssistantText = "";
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1650
|
+
try {
|
|
1651
|
+
for await (const message of result) {
|
|
1652
|
+
if (message.type === "assistant" && "content" in message) {
|
|
1653
|
+
const textParts = message.content.filter((c) => c.type === "text" && c.text).map((c) => c.text);
|
|
1654
|
+
if (textParts.length > 0) {
|
|
1655
|
+
lastAssistantText = textParts.join("\n");
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
if (message.type === "result") {
|
|
1659
|
+
resultMessage = message;
|
|
1555
1660
|
}
|
|
1556
1661
|
}
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1662
|
+
} catch (error) {
|
|
1663
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1664
|
+
const stderrDetail = stderrOutput ? `
|
|
1665
|
+
stderr:
|
|
1666
|
+
${stderrOutput.slice(0, 2e3)}` : "";
|
|
1667
|
+
throw new PipelineError(`Security scanner process failed: ${msg}${stderrDetail}`);
|
|
1560
1668
|
}
|
|
1561
1669
|
if (!resultMessage) {
|
|
1562
|
-
|
|
1670
|
+
const detail = stderrOutput ? `
|
|
1671
|
+
stderr:
|
|
1672
|
+
${stderrOutput.slice(0, 2e3)}` : "";
|
|
1673
|
+
throw new PipelineError(`No result message received from security scanner${detail}`);
|
|
1563
1674
|
}
|
|
1564
1675
|
const isSuccess = resultMessage.subtype === "success";
|
|
1565
1676
|
const isMaxTurns = resultMessage.subtype === "error_max_turns";
|
|
1566
1677
|
if (!isSuccess && !isMaxTurns) {
|
|
1567
1678
|
const errorDetail = "errors" in resultMessage ? resultMessage.errors.join(", ") : "unknown";
|
|
1679
|
+
const stderrDetail = stderrOutput ? `
|
|
1680
|
+
stderr:
|
|
1681
|
+
${stderrOutput.slice(0, 2e3)}` : "";
|
|
1568
1682
|
throw new PipelineError(
|
|
1569
|
-
`Security scanner failed (${resultMessage.subtype}): ${errorDetail}`
|
|
1683
|
+
`Security scanner failed (${resultMessage.subtype}): ${errorDetail}${stderrDetail}`
|
|
1570
1684
|
);
|
|
1571
1685
|
}
|
|
1572
1686
|
let parsed;
|
|
@@ -1582,7 +1696,7 @@ Analyze this diff for security vulnerabilities. Provide your report as a JSON ob
|
|
|
1582
1696
|
);
|
|
1583
1697
|
}
|
|
1584
1698
|
} else if (lastAssistantText) {
|
|
1585
|
-
|
|
1699
|
+
log8.warn("No result output \u2014 extracting report from last assistant message");
|
|
1586
1700
|
const jsonStr = extractJSON3(lastAssistantText);
|
|
1587
1701
|
try {
|
|
1588
1702
|
parsed = JSON.parse(jsonStr);
|
|
@@ -1595,7 +1709,7 @@ Analyze this diff for security vulnerabilities. Provide your report as a JSON ob
|
|
|
1595
1709
|
throw new PipelineError("Security scanner produced no output");
|
|
1596
1710
|
}
|
|
1597
1711
|
const report = validateReport(parsed);
|
|
1598
|
-
|
|
1712
|
+
log8.info(
|
|
1599
1713
|
{
|
|
1600
1714
|
riskLevel: report.riskLevel,
|
|
1601
1715
|
findingCount: report.findings.length
|
|
@@ -1610,11 +1724,11 @@ Analyze this diff for security vulnerabilities. Provide your report as a JSON ob
|
|
|
1610
1724
|
|
|
1611
1725
|
// src/testing/test-runner.ts
|
|
1612
1726
|
import { execa } from "execa";
|
|
1613
|
-
var
|
|
1727
|
+
var log9 = createChildLogger("test-runner");
|
|
1614
1728
|
async function runCommand(params) {
|
|
1615
1729
|
const { workingDir, command, kind, timeoutMs = 3e5 } = params;
|
|
1616
1730
|
const [cmd, ...args] = command.split(" ");
|
|
1617
|
-
|
|
1731
|
+
log9.info({ kind, command, workingDir }, `Running ${kind} command`);
|
|
1618
1732
|
const start = Date.now();
|
|
1619
1733
|
try {
|
|
1620
1734
|
const result = await execa(cmd, args, {
|
|
@@ -1625,7 +1739,7 @@ async function runCommand(params) {
|
|
|
1625
1739
|
});
|
|
1626
1740
|
const durationMs = Date.now() - start;
|
|
1627
1741
|
const passed = result.exitCode === 0;
|
|
1628
|
-
|
|
1742
|
+
log9.info({ kind, passed, durationMs, exitCode: result.exitCode }, `${kind} completed`);
|
|
1629
1743
|
return {
|
|
1630
1744
|
passed,
|
|
1631
1745
|
output: result.all ?? result.stdout + "\n" + result.stderr,
|
|
@@ -1634,7 +1748,7 @@ async function runCommand(params) {
|
|
|
1634
1748
|
} catch (error) {
|
|
1635
1749
|
const durationMs = Date.now() - start;
|
|
1636
1750
|
const message = error instanceof Error ? error.message : String(error);
|
|
1637
|
-
|
|
1751
|
+
log9.error({ kind, error: message, durationMs }, `${kind} execution failed`);
|
|
1638
1752
|
return {
|
|
1639
1753
|
passed: false,
|
|
1640
1754
|
output: message,
|
|
@@ -1652,7 +1766,7 @@ async function runTests(params) {
|
|
|
1652
1766
|
}
|
|
1653
1767
|
|
|
1654
1768
|
// src/notifications/sender.ts
|
|
1655
|
-
var
|
|
1769
|
+
var log10 = createChildLogger("notifications");
|
|
1656
1770
|
async function sendSlackWebhook(webhookUrl, message) {
|
|
1657
1771
|
try {
|
|
1658
1772
|
const response = await fetch(webhookUrl, {
|
|
@@ -1661,11 +1775,11 @@ async function sendSlackWebhook(webhookUrl, message) {
|
|
|
1661
1775
|
body: JSON.stringify(message)
|
|
1662
1776
|
});
|
|
1663
1777
|
if (!response.ok) {
|
|
1664
|
-
|
|
1778
|
+
log10.warn({ status: response.status }, "Slack webhook request failed");
|
|
1665
1779
|
}
|
|
1666
1780
|
} catch (error) {
|
|
1667
1781
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1668
|
-
|
|
1782
|
+
log10.warn({ error: msg }, "Failed to send Slack notification");
|
|
1669
1783
|
}
|
|
1670
1784
|
}
|
|
1671
1785
|
function formatSlackMessage(state, event) {
|
|
@@ -1718,7 +1832,7 @@ async function postGitHubMentions(config, owner, repo, issueNumber, event, state
|
|
|
1718
1832
|
await addComment(owner, repo, issueNumber, message);
|
|
1719
1833
|
} catch (error) {
|
|
1720
1834
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1721
|
-
|
|
1835
|
+
log10.warn({ error: msg }, "Failed to post GitHub mention");
|
|
1722
1836
|
}
|
|
1723
1837
|
}
|
|
1724
1838
|
}
|
|
@@ -1735,7 +1849,7 @@ async function notify(params) {
|
|
|
1735
1849
|
}
|
|
1736
1850
|
|
|
1737
1851
|
// src/pipeline/auto-merge.ts
|
|
1738
|
-
var
|
|
1852
|
+
var log11 = createChildLogger("auto-merge");
|
|
1739
1853
|
var SIZE_ORDER = {
|
|
1740
1854
|
xs: 1,
|
|
1741
1855
|
s: 2,
|
|
@@ -1767,20 +1881,20 @@ function isAutoMergeEligible(params) {
|
|
|
1767
1881
|
}
|
|
1768
1882
|
}
|
|
1769
1883
|
if (reasons.length > 0) {
|
|
1770
|
-
|
|
1884
|
+
log11.info({ reasons }, "PR not eligible for auto-merge");
|
|
1771
1885
|
}
|
|
1772
1886
|
return { eligible: reasons.length === 0, reasons };
|
|
1773
1887
|
}
|
|
1774
1888
|
|
|
1775
1889
|
// src/tracking/cost-tracker.ts
|
|
1776
|
-
import { homedir } from "os";
|
|
1777
|
-
import { join as
|
|
1890
|
+
import { homedir as homedir2 } from "os";
|
|
1891
|
+
import { join as join3 } from "path";
|
|
1778
1892
|
import { mkdir, appendFile, readFile } from "fs/promises";
|
|
1779
|
-
var
|
|
1780
|
-
var HISTORY_DIR =
|
|
1893
|
+
var log12 = createChildLogger("cost-tracker");
|
|
1894
|
+
var HISTORY_DIR = join3(homedir2(), ".xforce");
|
|
1781
1895
|
var HISTORY_FILE = "history.jsonl";
|
|
1782
1896
|
function getHistoryPath(basePath) {
|
|
1783
|
-
return
|
|
1897
|
+
return join3(basePath ?? HISTORY_DIR, HISTORY_FILE);
|
|
1784
1898
|
}
|
|
1785
1899
|
function buildRecordFromState(state, repoConfig) {
|
|
1786
1900
|
const startedAt = state.startedAt instanceof Date ? state.startedAt : new Date(state.startedAt);
|
|
@@ -1808,10 +1922,10 @@ function buildRecordFromState(state, repoConfig) {
|
|
|
1808
1922
|
async function appendRecord(record, basePath) {
|
|
1809
1923
|
const dir = basePath ?? HISTORY_DIR;
|
|
1810
1924
|
await mkdir(dir, { recursive: true });
|
|
1811
|
-
const filePath =
|
|
1925
|
+
const filePath = join3(dir, HISTORY_FILE);
|
|
1812
1926
|
const line = JSON.stringify(record) + "\n";
|
|
1813
1927
|
await appendFile(filePath, line, "utf-8");
|
|
1814
|
-
|
|
1928
|
+
log12.debug({ id: record.id, repo: record.repo }, "Persisted pipeline run record");
|
|
1815
1929
|
}
|
|
1816
1930
|
async function readRecords(filter, basePath) {
|
|
1817
1931
|
const filePath = getHistoryPath(basePath);
|
|
@@ -1832,7 +1946,7 @@ async function readRecords(filter, basePath) {
|
|
|
1832
1946
|
if (filter?.until && new Date(record.startedAt) > filter.until) continue;
|
|
1833
1947
|
records.push(record);
|
|
1834
1948
|
} catch {
|
|
1835
|
-
|
|
1949
|
+
log12.warn("Skipping malformed line in history file");
|
|
1836
1950
|
}
|
|
1837
1951
|
}
|
|
1838
1952
|
return records;
|
|
@@ -1869,12 +1983,12 @@ function summarize(records) {
|
|
|
1869
1983
|
}
|
|
1870
1984
|
|
|
1871
1985
|
// src/pipeline/orchestrator.ts
|
|
1872
|
-
var
|
|
1986
|
+
var log13 = createChildLogger("orchestrator");
|
|
1873
1987
|
function transition(state, to, message) {
|
|
1874
1988
|
validateTransition(state.status, to);
|
|
1875
1989
|
state.status = to;
|
|
1876
1990
|
state.logs.push({ timestamp: /* @__PURE__ */ new Date(), status: to, message });
|
|
1877
|
-
|
|
1991
|
+
log13.info({ status: to }, message);
|
|
1878
1992
|
}
|
|
1879
1993
|
async function runPipeline(params) {
|
|
1880
1994
|
const { config } = params;
|
|
@@ -1915,7 +2029,7 @@ async function runPipeline(params) {
|
|
|
1915
2029
|
await removeLabel(owner, repo, issueNumber, repoConfig.labels.failed).catch(() => {
|
|
1916
2030
|
});
|
|
1917
2031
|
await addLabel(owner, repo, issueNumber, repoConfig.labels.inProgress);
|
|
1918
|
-
|
|
2032
|
+
log13.info({ owner, repo, issueNumber }, "Fetching and parsing issue");
|
|
1919
2033
|
const issue = await getIssue(owner, repo, issueNumber);
|
|
1920
2034
|
const labels = await getIssueLabels(owner, repo, issueNumber);
|
|
1921
2035
|
const taskSpec = parseIssueBody({
|
|
@@ -1982,7 +2096,7 @@ async function runPipeline(params) {
|
|
|
1982
2096
|
await notify({ config: config.notifications, state, event: "completed", owner, repo, issueNumber });
|
|
1983
2097
|
state.completedAt = /* @__PURE__ */ new Date();
|
|
1984
2098
|
await appendRecord(buildRecordFromState(state, repoConfig)).catch((err) => {
|
|
1985
|
-
|
|
2099
|
+
log13.warn({ error: err.message }, "Failed to persist cost tracking record");
|
|
1986
2100
|
});
|
|
1987
2101
|
if (state.status === "merging") {
|
|
1988
2102
|
transition(state, "completed", "PR auto-merged");
|
|
@@ -1995,7 +2109,7 @@ async function runPipeline(params) {
|
|
|
1995
2109
|
state.status = "failed";
|
|
1996
2110
|
state.error = message;
|
|
1997
2111
|
state.completedAt = /* @__PURE__ */ new Date();
|
|
1998
|
-
|
|
2112
|
+
log13.error({ error: message }, "Pipeline failed");
|
|
1999
2113
|
await notify({ config: config.notifications, state, event: "failed", owner, repo, issueNumber }).catch(() => {
|
|
2000
2114
|
});
|
|
2001
2115
|
await appendRecord(buildRecordFromState(state, repoConfig)).catch(() => {
|
|
@@ -2013,7 +2127,7 @@ async function runPipeline(params) {
|
|
|
2013
2127
|
**Cost**: $${state.totalCostUsd.toFixed(4)}`
|
|
2014
2128
|
);
|
|
2015
2129
|
} catch {
|
|
2016
|
-
|
|
2130
|
+
log13.warn("Failed to update issue labels/comments after pipeline failure");
|
|
2017
2131
|
}
|
|
2018
2132
|
return state;
|
|
2019
2133
|
} finally {
|
|
@@ -2023,7 +2137,7 @@ async function runPipeline(params) {
|
|
|
2023
2137
|
try {
|
|
2024
2138
|
await rm(workDir, { recursive: true, force: true });
|
|
2025
2139
|
} catch {
|
|
2026
|
-
|
|
2140
|
+
log13.warn({ workDir }, "Failed to clean up working directory");
|
|
2027
2141
|
}
|
|
2028
2142
|
}
|
|
2029
2143
|
}
|
|
@@ -2326,6 +2440,7 @@ ${review.securityConcerns.map((c) => `- ${c}`).join("\n")}` : ""}
|
|
|
2326
2440
|
async function runCommand2(params) {
|
|
2327
2441
|
const spinner = ora2("Loading configuration...").start();
|
|
2328
2442
|
try {
|
|
2443
|
+
ensureCredentials();
|
|
2329
2444
|
const config = loadConfig(params.configPath);
|
|
2330
2445
|
spinner.succeed("Configuration loaded");
|
|
2331
2446
|
let owner;
|
|
@@ -2450,7 +2565,7 @@ async function reviewCommand(params) {
|
|
|
2450
2565
|
}
|
|
2451
2566
|
|
|
2452
2567
|
// src/cli/commands/init.ts
|
|
2453
|
-
import { join as
|
|
2568
|
+
import { join as join4, resolve as resolve3 } from "path";
|
|
2454
2569
|
import { mkdir as mkdir2, writeFile as writeFile2, readFile as readFile2, access as access2 } from "fs/promises";
|
|
2455
2570
|
import { fileURLToPath } from "url";
|
|
2456
2571
|
import ora4 from "ora";
|
|
@@ -2459,9 +2574,9 @@ async function findTemplateDir() {
|
|
|
2459
2574
|
const thisFile = fileURLToPath(import.meta.url);
|
|
2460
2575
|
let dir = resolve3(thisFile, "..");
|
|
2461
2576
|
for (let i = 0; i < 6; i++) {
|
|
2462
|
-
const pkgPath =
|
|
2577
|
+
const pkgPath = join4(dir, "package.json");
|
|
2463
2578
|
if (await fileExists(pkgPath)) {
|
|
2464
|
-
return
|
|
2579
|
+
return join4(dir, "templates");
|
|
2465
2580
|
}
|
|
2466
2581
|
dir = resolve3(dir, "..");
|
|
2467
2582
|
}
|
|
@@ -2532,21 +2647,21 @@ async function initCommand(params) {
|
|
|
2532
2647
|
const testCommand = params.testCommand ?? "npm test";
|
|
2533
2648
|
const created = [];
|
|
2534
2649
|
const skipped = [];
|
|
2535
|
-
const issueTemplateDir =
|
|
2650
|
+
const issueTemplateDir = join4(targetDir, ".github", "ISSUE_TEMPLATE");
|
|
2536
2651
|
await mkdir2(issueTemplateDir, { recursive: true });
|
|
2537
2652
|
const templateNames = ["feature-request.yml", "bug-fix.yml", "refactor.yml"];
|
|
2538
|
-
const srcTemplateDir =
|
|
2653
|
+
const srcTemplateDir = join4(await findTemplateDir(), "issue-templates");
|
|
2539
2654
|
for (const tmpl of templateNames) {
|
|
2540
|
-
const destPath =
|
|
2655
|
+
const destPath = join4(issueTemplateDir, tmpl);
|
|
2541
2656
|
if (await fileExists(destPath)) {
|
|
2542
2657
|
skipped.push(`.github/ISSUE_TEMPLATE/${tmpl}`);
|
|
2543
2658
|
} else {
|
|
2544
|
-
const content = await readFile2(
|
|
2659
|
+
const content = await readFile2(join4(srcTemplateDir, tmpl), "utf-8");
|
|
2545
2660
|
await writeFile2(destPath, content, "utf-8");
|
|
2546
2661
|
created.push(`.github/ISSUE_TEMPLATE/${tmpl}`);
|
|
2547
2662
|
}
|
|
2548
2663
|
}
|
|
2549
|
-
const envPath =
|
|
2664
|
+
const envPath = join4(targetDir, ".env.xforce");
|
|
2550
2665
|
if (await fileExists(envPath)) {
|
|
2551
2666
|
skipped.push(".env.xforce");
|
|
2552
2667
|
} else {
|
|
@@ -2563,7 +2678,7 @@ XFORCE_WEBHOOK_SECRET=
|
|
|
2563
2678
|
);
|
|
2564
2679
|
created.push(".env.xforce");
|
|
2565
2680
|
}
|
|
2566
|
-
const gitignorePath =
|
|
2681
|
+
const gitignorePath = join4(targetDir, ".gitignore");
|
|
2567
2682
|
let gitignoreContent = "";
|
|
2568
2683
|
try {
|
|
2569
2684
|
gitignoreContent = await readFile2(gitignorePath, "utf-8");
|
|
@@ -2577,7 +2692,7 @@ XFORCE_WEBHOOK_SECRET=
|
|
|
2577
2692
|
created.push(".gitignore");
|
|
2578
2693
|
}
|
|
2579
2694
|
}
|
|
2580
|
-
const configPath =
|
|
2695
|
+
const configPath = join4(targetDir, "xforce.config.yaml");
|
|
2581
2696
|
if (await fileExists(configPath)) {
|
|
2582
2697
|
skipped.push("xforce.config.yaml");
|
|
2583
2698
|
} else {
|
|
@@ -2702,7 +2817,7 @@ import crypto from "crypto";
|
|
|
2702
2817
|
|
|
2703
2818
|
// src/server/queue.ts
|
|
2704
2819
|
import { nanoid as nanoid2 } from "nanoid";
|
|
2705
|
-
var
|
|
2820
|
+
var log14 = createChildLogger("queue");
|
|
2706
2821
|
var DEFAULT_OPTIONS = {
|
|
2707
2822
|
maxSize: 10,
|
|
2708
2823
|
historySize: 50
|
|
@@ -2739,7 +2854,7 @@ var JobQueue = class {
|
|
|
2739
2854
|
enqueuedAt: /* @__PURE__ */ new Date()
|
|
2740
2855
|
};
|
|
2741
2856
|
this.pending.push(job);
|
|
2742
|
-
|
|
2857
|
+
log14.info(
|
|
2743
2858
|
{ jobId: job.id, owner: job.owner, repo: job.repo, issue: job.issueNumber },
|
|
2744
2859
|
"Job enqueued"
|
|
2745
2860
|
);
|
|
@@ -2776,7 +2891,7 @@ var JobQueue = class {
|
|
|
2776
2891
|
job.status = "running";
|
|
2777
2892
|
job.startedAt = /* @__PURE__ */ new Date();
|
|
2778
2893
|
this.active = job;
|
|
2779
|
-
|
|
2894
|
+
log14.info(
|
|
2780
2895
|
{ jobId: job.id, owner: job.owner, repo: job.repo, issue: job.issueNumber },
|
|
2781
2896
|
"Processing job"
|
|
2782
2897
|
);
|
|
@@ -2787,7 +2902,7 @@ var JobQueue = class {
|
|
|
2787
2902
|
} catch (error) {
|
|
2788
2903
|
job.status = "failed";
|
|
2789
2904
|
job.error = error instanceof Error ? error.message : String(error);
|
|
2790
|
-
|
|
2905
|
+
log14.error(
|
|
2791
2906
|
{ jobId: job.id, error: job.error },
|
|
2792
2907
|
"Job failed"
|
|
2793
2908
|
);
|
|
@@ -2800,7 +2915,7 @@ var JobQueue = class {
|
|
|
2800
2915
|
this.completed = this.completed.slice(0, this.options.historySize);
|
|
2801
2916
|
}
|
|
2802
2917
|
this.processing = false;
|
|
2803
|
-
|
|
2918
|
+
log14.info(
|
|
2804
2919
|
{ jobId: job.id, status: job.status, totalProcessed: this.totalProcessed },
|
|
2805
2920
|
"Job finished"
|
|
2806
2921
|
);
|
|
@@ -2810,7 +2925,7 @@ var JobQueue = class {
|
|
|
2810
2925
|
};
|
|
2811
2926
|
|
|
2812
2927
|
// src/server/webhook.ts
|
|
2813
|
-
var
|
|
2928
|
+
var log15 = createChildLogger("webhook-server");
|
|
2814
2929
|
var MAX_BODY_SIZE = 1048576;
|
|
2815
2930
|
function readBody(req) {
|
|
2816
2931
|
return new Promise((resolve4, reject) => {
|
|
@@ -2927,7 +3042,7 @@ function createWebhookServer(config, options) {
|
|
|
2927
3042
|
}
|
|
2928
3043
|
try {
|
|
2929
3044
|
const job = queue.enqueue({ owner, repo, issueNumber, issueUrl });
|
|
2930
|
-
|
|
3045
|
+
log15.info(
|
|
2931
3046
|
{ jobId: job.id, owner, repo, issueNumber },
|
|
2932
3047
|
"Webhook accepted, job enqueued"
|
|
2933
3048
|
);
|
|
@@ -2946,7 +3061,7 @@ function createWebhookServer(config, options) {
|
|
|
2946
3061
|
const server = createServer(async (req, res) => {
|
|
2947
3062
|
const method = req.method ?? "GET";
|
|
2948
3063
|
const url = req.url ?? "/";
|
|
2949
|
-
|
|
3064
|
+
log15.debug({ method, url }, "Request received");
|
|
2950
3065
|
try {
|
|
2951
3066
|
if (method === "GET" && url === "/health") {
|
|
2952
3067
|
handleHealth(req, res);
|
|
@@ -2959,7 +3074,7 @@ function createWebhookServer(config, options) {
|
|
|
2959
3074
|
}
|
|
2960
3075
|
} catch (error) {
|
|
2961
3076
|
const message = error instanceof Error ? error.message : String(error);
|
|
2962
|
-
|
|
3077
|
+
log15.error({ error: message }, "Request handler error");
|
|
2963
3078
|
if (!res.headersSent) {
|
|
2964
3079
|
sendJson(res, 500, { error: "Internal server error" });
|
|
2965
3080
|
}
|
|
@@ -2969,7 +3084,7 @@ function createWebhookServer(config, options) {
|
|
|
2969
3084
|
start() {
|
|
2970
3085
|
return new Promise((resolve4, reject) => {
|
|
2971
3086
|
server.listen(options.port, options.host, () => {
|
|
2972
|
-
|
|
3087
|
+
log15.info({ port: options.port, host: options.host }, "Webhook server started");
|
|
2973
3088
|
resolve4();
|
|
2974
3089
|
});
|
|
2975
3090
|
server.on("error", reject);
|
|
@@ -2978,7 +3093,7 @@ function createWebhookServer(config, options) {
|
|
|
2978
3093
|
stop() {
|
|
2979
3094
|
return new Promise((resolve4) => {
|
|
2980
3095
|
server.close(() => {
|
|
2981
|
-
|
|
3096
|
+
log15.info("Webhook server stopped");
|
|
2982
3097
|
resolve4();
|
|
2983
3098
|
});
|
|
2984
3099
|
});
|
|
@@ -3003,6 +3118,13 @@ async function serveCommand(params) {
|
|
|
3003
3118
|
);
|
|
3004
3119
|
process.exit(1);
|
|
3005
3120
|
}
|
|
3121
|
+
try {
|
|
3122
|
+
ensureCredentials();
|
|
3123
|
+
} catch (error) {
|
|
3124
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3125
|
+
console.error(chalk5.red(`Credentials error: ${message}`));
|
|
3126
|
+
process.exit(1);
|
|
3127
|
+
}
|
|
3006
3128
|
let config;
|
|
3007
3129
|
try {
|
|
3008
3130
|
config = loadConfig(params.configPath);
|