@kody-ade/kody-engine-lite 0.1.67 → 0.1.69
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/agent-runner.d.ts +4 -0
- package/dist/agent-runner.js +122 -0
- package/dist/bin/cli.js +203 -13
- package/dist/ci/parse-inputs.d.ts +6 -0
- package/dist/ci/parse-inputs.js +76 -0
- package/dist/ci/parse-safety.d.ts +6 -0
- package/dist/ci/parse-safety.js +22 -0
- package/dist/cli/args.d.ts +13 -0
- package/dist/cli/args.js +42 -0
- package/dist/cli/litellm.d.ts +2 -0
- package/dist/cli/litellm.js +85 -0
- package/dist/cli/task-resolution.d.ts +2 -0
- package/dist/cli/task-resolution.js +41 -0
- package/dist/config.d.ts +49 -0
- package/dist/config.js +72 -0
- package/dist/context.d.ts +4 -0
- package/dist/context.js +83 -0
- package/dist/definitions.d.ts +3 -0
- package/dist/definitions.js +59 -0
- package/dist/entry.d.ts +1 -0
- package/dist/entry.js +236 -0
- package/dist/git-utils.d.ts +13 -0
- package/dist/git-utils.js +174 -0
- package/dist/github-api.d.ts +14 -0
- package/dist/github-api.js +114 -0
- package/dist/kody-utils.d.ts +1 -0
- package/dist/kody-utils.js +9 -0
- package/dist/learning/auto-learn.d.ts +2 -0
- package/dist/learning/auto-learn.js +169 -0
- package/dist/logger.d.ts +14 -0
- package/dist/logger.js +51 -0
- package/dist/memory.d.ts +1 -0
- package/dist/memory.js +20 -0
- package/dist/observer.d.ts +9 -0
- package/dist/observer.js +80 -0
- package/dist/pipeline/complexity.d.ts +3 -0
- package/dist/pipeline/complexity.js +12 -0
- package/dist/pipeline/executor-registry.d.ts +3 -0
- package/dist/pipeline/executor-registry.js +20 -0
- package/dist/pipeline/hooks.d.ts +17 -0
- package/dist/pipeline/hooks.js +110 -0
- package/dist/pipeline/questions.d.ts +2 -0
- package/dist/pipeline/questions.js +44 -0
- package/dist/pipeline/runner-selection.d.ts +2 -0
- package/dist/pipeline/runner-selection.js +13 -0
- package/dist/pipeline/state.d.ts +4 -0
- package/dist/pipeline/state.js +37 -0
- package/dist/pipeline.d.ts +3 -0
- package/dist/pipeline.js +213 -0
- package/dist/preflight.d.ts +1 -0
- package/dist/preflight.js +69 -0
- package/dist/retrospective.d.ts +26 -0
- package/dist/retrospective.js +211 -0
- package/dist/stages/agent.d.ts +2 -0
- package/dist/stages/agent.js +94 -0
- package/dist/stages/gate.d.ts +2 -0
- package/dist/stages/gate.js +32 -0
- package/dist/stages/review.d.ts +2 -0
- package/dist/stages/review.js +32 -0
- package/dist/stages/ship.d.ts +3 -0
- package/dist/stages/ship.js +154 -0
- package/dist/stages/verify.d.ts +2 -0
- package/dist/stages/verify.js +94 -0
- package/dist/types.d.ts +61 -0
- package/dist/types.js +1 -0
- package/dist/validators.d.ts +8 -0
- package/dist/validators.js +42 -0
- package/dist/verify-runner.d.ts +11 -0
- package/dist/verify-runner.js +110 -0
- package/package.json +9 -8
- package/prompts/plan.md +18 -1
- package/templates/kody.yml +82 -3
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { spawn, execFileSync } from "child_process";
|
|
2
|
+
const SIGKILL_GRACE_MS = 5000;
|
|
3
|
+
const STDERR_TAIL_CHARS = 500;
|
|
4
|
+
function writeStdin(child, prompt) {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
if (!child.stdin) {
|
|
7
|
+
resolve();
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
child.stdin.write(prompt, (err) => {
|
|
11
|
+
if (err)
|
|
12
|
+
reject(err);
|
|
13
|
+
else {
|
|
14
|
+
child.stdin.end();
|
|
15
|
+
resolve();
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function waitForProcess(child, timeout) {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const stdoutChunks = [];
|
|
23
|
+
const stderrChunks = [];
|
|
24
|
+
child.stdout?.on("data", (chunk) => stdoutChunks.push(chunk));
|
|
25
|
+
child.stderr?.on("data", (chunk) => stderrChunks.push(chunk));
|
|
26
|
+
const timer = setTimeout(() => {
|
|
27
|
+
child.kill("SIGTERM");
|
|
28
|
+
setTimeout(() => {
|
|
29
|
+
if (!child.killed)
|
|
30
|
+
child.kill("SIGKILL");
|
|
31
|
+
}, SIGKILL_GRACE_MS);
|
|
32
|
+
}, timeout);
|
|
33
|
+
child.on("exit", (code) => {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
resolve({
|
|
36
|
+
code,
|
|
37
|
+
stdout: Buffer.concat(stdoutChunks).toString(),
|
|
38
|
+
stderr: Buffer.concat(stderrChunks).toString(),
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
child.on("error", (err) => {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
resolve({ code: -1, stdout: "", stderr: err.message });
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async function runSubprocess(command, args, prompt, timeout, options) {
|
|
48
|
+
const child = spawn(command, args, {
|
|
49
|
+
cwd: options?.cwd ?? process.cwd(),
|
|
50
|
+
env: {
|
|
51
|
+
...process.env,
|
|
52
|
+
SKIP_BUILD: "1",
|
|
53
|
+
SKIP_HOOKS: "1",
|
|
54
|
+
...options?.env,
|
|
55
|
+
},
|
|
56
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
57
|
+
});
|
|
58
|
+
try {
|
|
59
|
+
await writeStdin(child, prompt);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
return {
|
|
63
|
+
outcome: "failed",
|
|
64
|
+
error: `Failed to send prompt: ${err instanceof Error ? err.message : String(err)}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const { code, stdout, stderr } = await waitForProcess(child, timeout);
|
|
68
|
+
if (code === 0) {
|
|
69
|
+
return { outcome: "completed", output: stdout };
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
outcome: code === null ? "timed_out" : "failed",
|
|
73
|
+
error: `Exit code ${code}\n${stderr.slice(-STDERR_TAIL_CHARS)}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function checkCommand(command, args) {
|
|
77
|
+
try {
|
|
78
|
+
execFileSync(command, args, { timeout: 10_000, stdio: "pipe" });
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// ─── Claude Code Runner ──────────────────────────────────────────────────────
|
|
86
|
+
export function createClaudeCodeRunner() {
|
|
87
|
+
return {
|
|
88
|
+
async run(_stageName, prompt, model, timeout, _taskDir, options) {
|
|
89
|
+
return runSubprocess("claude", [
|
|
90
|
+
"--print",
|
|
91
|
+
"--model", model,
|
|
92
|
+
"--dangerously-skip-permissions",
|
|
93
|
+
"--allowedTools", "Bash,Edit,Read,Write,Glob,Grep",
|
|
94
|
+
], prompt, timeout, options);
|
|
95
|
+
},
|
|
96
|
+
async healthCheck() {
|
|
97
|
+
return checkCommand("claude", ["--version"]);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// ─── Runner Factory ──────────────────────────────────────────────────────────
|
|
102
|
+
const RUNNER_FACTORIES = {
|
|
103
|
+
"claude-code": createClaudeCodeRunner,
|
|
104
|
+
};
|
|
105
|
+
export function createRunners(config) {
|
|
106
|
+
// New multi-runner config
|
|
107
|
+
if (config.agent.runners && Object.keys(config.agent.runners).length > 0) {
|
|
108
|
+
const runners = {};
|
|
109
|
+
for (const [name, runnerConfig] of Object.entries(config.agent.runners)) {
|
|
110
|
+
const factory = RUNNER_FACTORIES[runnerConfig.type];
|
|
111
|
+
if (factory) {
|
|
112
|
+
runners[name] = factory();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return runners;
|
|
116
|
+
}
|
|
117
|
+
// Legacy single-runner fallback
|
|
118
|
+
const runnerType = config.agent.runner ?? "claude-code";
|
|
119
|
+
const factory = RUNNER_FACTORIES[runnerType];
|
|
120
|
+
const defaultName = config.agent.defaultRunner ?? "claude";
|
|
121
|
+
return { [defaultName]: factory ? factory() : createClaudeCodeRunner() };
|
|
122
|
+
}
|
package/dist/bin/cli.js
CHANGED
|
@@ -679,6 +679,45 @@ function submitPRReview(prNumber, body, event) {
|
|
|
679
679
|
logger.warn(` Failed to submit PR review: ${err}`);
|
|
680
680
|
}
|
|
681
681
|
}
|
|
682
|
+
function getCIFailureLogs(runId, maxLength = 8e3) {
|
|
683
|
+
try {
|
|
684
|
+
const logsOutput = gh([
|
|
685
|
+
"run",
|
|
686
|
+
"view",
|
|
687
|
+
String(runId),
|
|
688
|
+
"--log-failed"
|
|
689
|
+
]);
|
|
690
|
+
if (!logsOutput) return null;
|
|
691
|
+
const truncated = logsOutput.slice(-maxLength);
|
|
692
|
+
const prefix = logsOutput.length > maxLength ? "...(earlier output truncated)\n" : "";
|
|
693
|
+
return `${prefix}${truncated}`;
|
|
694
|
+
} catch (err) {
|
|
695
|
+
logger.warn(` Failed to get CI failure logs for run ${runId}: ${err}`);
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
function getLatestFailedRunForBranch(branch) {
|
|
700
|
+
try {
|
|
701
|
+
const output = gh([
|
|
702
|
+
"run",
|
|
703
|
+
"list",
|
|
704
|
+
"--branch",
|
|
705
|
+
branch,
|
|
706
|
+
"--status",
|
|
707
|
+
"failure",
|
|
708
|
+
"--limit",
|
|
709
|
+
"1",
|
|
710
|
+
"--json",
|
|
711
|
+
"databaseId",
|
|
712
|
+
"--jq",
|
|
713
|
+
".[0].databaseId"
|
|
714
|
+
]);
|
|
715
|
+
return output.trim() || null;
|
|
716
|
+
} catch (err) {
|
|
717
|
+
logger.warn(` Failed to get latest failed run for branch ${branch}: ${err}`);
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
682
721
|
function getLatestKodyReviewComment(prNumber) {
|
|
683
722
|
try {
|
|
684
723
|
const output = gh([
|
|
@@ -693,13 +732,80 @@ function getLatestKodyReviewComment(prNumber) {
|
|
|
693
732
|
return null;
|
|
694
733
|
}
|
|
695
734
|
}
|
|
696
|
-
|
|
735
|
+
function getPRFeedbackSinceLastKodyAction(prNumber) {
|
|
736
|
+
try {
|
|
737
|
+
const issueCommentsRaw = gh([
|
|
738
|
+
"api",
|
|
739
|
+
`repos/{owner}/{repo}/issues/${prNumber}/comments`,
|
|
740
|
+
"--jq",
|
|
741
|
+
"[.[] | {body, created_at, user_login: .user.login, user_type: .user.type}]"
|
|
742
|
+
]);
|
|
743
|
+
const issueComments = issueCommentsRaw ? JSON.parse(issueCommentsRaw) : [];
|
|
744
|
+
const reviewCommentsRaw = gh([
|
|
745
|
+
"api",
|
|
746
|
+
`repos/{owner}/{repo}/pulls/${prNumber}/comments`,
|
|
747
|
+
"--jq",
|
|
748
|
+
"[.[] | {body, created_at, user_login: .user.login, user_type: .user.type, path, line}]"
|
|
749
|
+
]);
|
|
750
|
+
const reviewComments = reviewCommentsRaw ? JSON.parse(reviewCommentsRaw) : [];
|
|
751
|
+
const kodyTimestamp = findLastKodyActionTimestamp(issueComments);
|
|
752
|
+
const humanIssueComments = issueComments.filter(
|
|
753
|
+
(c) => !isKodyComment(c) && (!kodyTimestamp || c.created_at > kodyTimestamp)
|
|
754
|
+
);
|
|
755
|
+
const humanReviewComments = reviewComments.filter(
|
|
756
|
+
(c) => !isKodyComment(c) && (!kodyTimestamp || c.created_at > kodyTimestamp)
|
|
757
|
+
);
|
|
758
|
+
if (humanIssueComments.length === 0 && humanReviewComments.length === 0) {
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
const parts = [];
|
|
762
|
+
if (humanIssueComments.length > 0) {
|
|
763
|
+
parts.push("### PR Comments");
|
|
764
|
+
for (const c of humanIssueComments) {
|
|
765
|
+
parts.push(`**@${c.user_login}:**
|
|
766
|
+
${c.body}`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
if (humanReviewComments.length > 0) {
|
|
770
|
+
parts.push("### Code Review Comments");
|
|
771
|
+
for (const c of humanReviewComments) {
|
|
772
|
+
const location = c.path ? `\`${c.path}${c.line ? `:${c.line}` : ""}\`` : "";
|
|
773
|
+
parts.push(`**@${c.user_login}** ${location}:
|
|
774
|
+
${c.body}`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return parts.join("\n\n");
|
|
778
|
+
} catch (err) {
|
|
779
|
+
logger.warn(` Failed to get PR feedback for #${prNumber}: ${err}`);
|
|
780
|
+
return null;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function isKodyComment(comment) {
|
|
784
|
+
if (comment.user_type === "Bot") return true;
|
|
785
|
+
return KODY_MARKERS.some((marker) => comment.body.includes(marker));
|
|
786
|
+
}
|
|
787
|
+
function findLastKodyActionTimestamp(comments) {
|
|
788
|
+
const kodyComments = comments.filter(isKodyComment);
|
|
789
|
+
if (kodyComments.length === 0) return null;
|
|
790
|
+
return kodyComments[kodyComments.length - 1].created_at;
|
|
791
|
+
}
|
|
792
|
+
var API_TIMEOUT_MS, LIFECYCLE_LABELS, _ghCwd, KODY_MARKERS;
|
|
697
793
|
var init_github_api = __esm({
|
|
698
794
|
"src/github-api.ts"() {
|
|
699
795
|
"use strict";
|
|
700
796
|
init_logger();
|
|
701
797
|
API_TIMEOUT_MS = 3e4;
|
|
702
798
|
LIFECYCLE_LABELS = ["planning", "building", "review", "done", "failed", "waiting", "low", "medium", "high"];
|
|
799
|
+
KODY_MARKERS = [
|
|
800
|
+
"Kody Review",
|
|
801
|
+
"\u{1F916} Generated by Kody",
|
|
802
|
+
"Kody pipeline started",
|
|
803
|
+
"Fix pushed to PR",
|
|
804
|
+
"PR created:",
|
|
805
|
+
"Pipeline failed at",
|
|
806
|
+
"Pipeline already running",
|
|
807
|
+
"already completed"
|
|
808
|
+
];
|
|
703
809
|
}
|
|
704
810
|
});
|
|
705
811
|
|
|
@@ -2496,6 +2602,7 @@ ${learnings.join("\n")}
|
|
|
2496
2602
|
invalidateCache(conventionsPath, path13.join(memoryDir, ".tiers"));
|
|
2497
2603
|
logger.info(`Auto-learned ${learnings.length} convention(s)`);
|
|
2498
2604
|
}
|
|
2605
|
+
autoLearnDecisions(ctx.taskDir, memoryDir, ctx.taskId, timestamp2);
|
|
2499
2606
|
autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
|
|
2500
2607
|
} catch {
|
|
2501
2608
|
}
|
|
@@ -2560,6 +2667,45 @@ ${detected.join("\n")}
|
|
|
2560
2667
|
logger.info(`Auto-detected architecture (${detected.length} items)`);
|
|
2561
2668
|
}
|
|
2562
2669
|
}
|
|
2670
|
+
function autoLearnDecisions(taskDir, memoryDir, taskId, timestamp2) {
|
|
2671
|
+
const reviewPath = path13.join(taskDir, "review.md");
|
|
2672
|
+
if (!fs13.existsSync(reviewPath)) return;
|
|
2673
|
+
const review = fs13.readFileSync(reviewPath, "utf-8");
|
|
2674
|
+
const decisions = [];
|
|
2675
|
+
const existingPatternRe = /(?:use|follow|reuse|match|adopt)\s+(?:the\s+)?existing\s+(.+?)(?:\.|$)/gim;
|
|
2676
|
+
for (const match of review.matchAll(existingPatternRe)) {
|
|
2677
|
+
decisions.push(`- Use existing ${match[1].trim()}`);
|
|
2678
|
+
}
|
|
2679
|
+
const insteadOfRe = /instead\s+of\s+(.+?),?\s+(?:use|prefer|adopt)\s+(.+?)(?:\.|$)/gim;
|
|
2680
|
+
for (const match of review.matchAll(insteadOfRe)) {
|
|
2681
|
+
decisions.push(`- Prefer ${match[2].trim()} over ${match[1].trim()}`);
|
|
2682
|
+
}
|
|
2683
|
+
const consistentRe = /(?:consistent\s+with|same\s+pattern\s+as|follow\s+the\s+pattern\s+(?:in|from))\s+(.+?)(?:\.|$)/gim;
|
|
2684
|
+
for (const match of review.matchAll(consistentRe)) {
|
|
2685
|
+
decisions.push(`- Follow pattern from ${match[1].trim()}`);
|
|
2686
|
+
}
|
|
2687
|
+
const avoidRe = /(?:don't|do\s+not|never|avoid)\s+(?:use\s+)?(.+?)\s+(?:for|when|in)\s+(.+?)(?:\.|$)/gim;
|
|
2688
|
+
for (const match of review.matchAll(avoidRe)) {
|
|
2689
|
+
decisions.push(`- Avoid ${match[1].trim()} for ${match[2].trim()}`);
|
|
2690
|
+
}
|
|
2691
|
+
if (decisions.length === 0) return;
|
|
2692
|
+
const decisionsPath = path13.join(memoryDir, "decisions.md");
|
|
2693
|
+
let existing = "";
|
|
2694
|
+
if (fs13.existsSync(decisionsPath)) {
|
|
2695
|
+
existing = fs13.readFileSync(decisionsPath, "utf-8");
|
|
2696
|
+
} else {
|
|
2697
|
+
existing = "# Architectural Decisions\n\nDecisions extracted from code reviews. The planning agent MUST follow these.\n";
|
|
2698
|
+
}
|
|
2699
|
+
const newDecisions = decisions.filter((d) => !existing.includes(d));
|
|
2700
|
+
if (newDecisions.length === 0) return;
|
|
2701
|
+
const entry = `
|
|
2702
|
+
## From task ${taskId} (${timestamp2})
|
|
2703
|
+
${newDecisions.join("\n")}
|
|
2704
|
+
`;
|
|
2705
|
+
fs13.appendFileSync(decisionsPath, existing ? entry : existing + entry);
|
|
2706
|
+
invalidateCache(decisionsPath, path13.join(memoryDir, ".tiers"));
|
|
2707
|
+
logger.info(`Auto-learned ${newDecisions.length} architectural decision(s)`);
|
|
2708
|
+
}
|
|
2563
2709
|
var init_auto_learn = __esm({
|
|
2564
2710
|
"src/learning/auto-learn.ts"() {
|
|
2565
2711
|
"use strict";
|
|
@@ -3203,13 +3349,14 @@ function parseArgs() {
|
|
|
3203
3349
|
kody run --task-id <id> [--task "<desc>"] [--cwd <path>] [--issue-number <n>] [--complexity low|medium|high] [--feedback "<text>"] [--local] [--dry-run]
|
|
3204
3350
|
kody rerun --task-id <id> --from <stage> [--cwd <path>] [--issue-number <n>]
|
|
3205
3351
|
kody fix --task-id <id> [--cwd <path>] [--issue-number <n>] [--feedback "<text>"]
|
|
3352
|
+
kody fix-ci [--pr-number <n>] [--ci-run-id <id>] [--cwd <path>] [--issue-number <n>] [--feedback "<text>"]
|
|
3206
3353
|
kody review [--pr-number <n>] [--issue-number <n>] [--cwd <path>] [--local]
|
|
3207
3354
|
kody status --task-id <id> [--cwd <path>]
|
|
3208
3355
|
kody --help`);
|
|
3209
3356
|
process.exit(0);
|
|
3210
3357
|
}
|
|
3211
3358
|
const command2 = args2[0];
|
|
3212
|
-
if (!["run", "rerun", "fix", "status", "review"].includes(command2)) {
|
|
3359
|
+
if (!["run", "rerun", "fix", "fix-ci", "status", "review"].includes(command2)) {
|
|
3213
3360
|
console.error(`Unknown command: ${command2}`);
|
|
3214
3361
|
process.exit(1);
|
|
3215
3362
|
}
|
|
@@ -3227,7 +3374,8 @@ function parseArgs() {
|
|
|
3227
3374
|
prNumber: prStr ? parseInt(prStr, 10) : void 0,
|
|
3228
3375
|
feedback: getArg(args2, "--feedback") ?? process.env.FEEDBACK,
|
|
3229
3376
|
local: localFlag || !isCI2 && !hasFlag(args2, "--no-local"),
|
|
3230
|
-
complexity: getArg(args2, "--complexity") ?? process.env.COMPLEXITY
|
|
3377
|
+
complexity: getArg(args2, "--complexity") ?? process.env.COMPLEXITY,
|
|
3378
|
+
ciRunId: getArg(args2, "--ci-run-id") ?? process.env.CI_RUN_ID
|
|
3231
3379
|
};
|
|
3232
3380
|
}
|
|
3233
3381
|
var isCI2;
|
|
@@ -3463,7 +3611,7 @@ async function main() {
|
|
|
3463
3611
|
setGhCwd(projectDir);
|
|
3464
3612
|
logger.info(`Working directory: ${projectDir}`);
|
|
3465
3613
|
}
|
|
3466
|
-
const isPRFix = input.command === "fix" && !!input.prNumber;
|
|
3614
|
+
const isPRFix = (input.command === "fix" || input.command === "fix-ci") && !!input.prNumber;
|
|
3467
3615
|
if (input.issueNumber && input.command !== "review" && !isPRFix) {
|
|
3468
3616
|
const taskAction = resolveForIssue(input.issueNumber, projectDir);
|
|
3469
3617
|
logger.info(`Task action: ${taskAction.action}`);
|
|
@@ -3497,7 +3645,7 @@ async function main() {
|
|
|
3497
3645
|
let taskId = input.taskId;
|
|
3498
3646
|
if (!taskId) {
|
|
3499
3647
|
if (isPRFix) {
|
|
3500
|
-
taskId =
|
|
3648
|
+
taskId = `${input.command === "fix-ci" ? "fixci" : "fix"}-pr-${input.prNumber}-${generateTaskId()}`;
|
|
3501
3649
|
} else if (input.issueNumber) {
|
|
3502
3650
|
taskId = `${input.issueNumber}-${generateTaskId()}`;
|
|
3503
3651
|
} else if (input.command === "run" && input.task) {
|
|
@@ -3615,21 +3763,63 @@ ${issue.body ?? ""}`;
|
|
|
3615
3763
|
console.error("No task.md found. Provide --task, --issue-number, or ensure .kody/tasks/<id>/task.md exists.");
|
|
3616
3764
|
process.exit(1);
|
|
3617
3765
|
}
|
|
3618
|
-
if (input.command === "fix" && !input.fromStage) {
|
|
3766
|
+
if ((input.command === "fix" || input.command === "fix-ci") && !input.fromStage) {
|
|
3619
3767
|
input.fromStage = "build";
|
|
3620
3768
|
}
|
|
3769
|
+
if (input.command === "fix-ci" && input.prNumber) {
|
|
3770
|
+
let ciRunId = input.ciRunId;
|
|
3771
|
+
if (!ciRunId && input.feedback) {
|
|
3772
|
+
const match = input.feedback.match(/Run ID:\s*(\d+)/);
|
|
3773
|
+
ciRunId = match?.[1];
|
|
3774
|
+
}
|
|
3775
|
+
if (!ciRunId) {
|
|
3776
|
+
const prDetails = getPRDetails(input.prNumber);
|
|
3777
|
+
if (prDetails) {
|
|
3778
|
+
ciRunId = getLatestFailedRunForBranch(prDetails.headBranch) ?? void 0;
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3781
|
+
if (ciRunId) {
|
|
3782
|
+
const ciLogs = getCIFailureLogs(ciRunId);
|
|
3783
|
+
if (ciLogs) {
|
|
3784
|
+
logger.info(` Found CI failure logs for run ${ciRunId}, injecting as feedback`);
|
|
3785
|
+
const ciContext = `## CI Failure Logs (run ${ciRunId})
|
|
3786
|
+
|
|
3787
|
+
The CI pipeline failed. Fix the code to make CI pass.
|
|
3788
|
+
|
|
3789
|
+
\`\`\`
|
|
3790
|
+
${ciLogs}
|
|
3791
|
+
\`\`\``;
|
|
3792
|
+
input.feedback = input.feedback ? `${ciContext}
|
|
3793
|
+
|
|
3794
|
+
## Additional context
|
|
3795
|
+
|
|
3796
|
+
${input.feedback}` : ciContext;
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
}
|
|
3621
3800
|
if (input.command === "fix" && input.prNumber) {
|
|
3801
|
+
const feedbackParts = [];
|
|
3622
3802
|
const reviewComment = getLatestKodyReviewComment(input.prNumber);
|
|
3623
3803
|
if (reviewComment) {
|
|
3624
|
-
logger.info(` Found Kody review comment on PR #${input.prNumber}
|
|
3625
|
-
|
|
3804
|
+
logger.info(` Found Kody review comment on PR #${input.prNumber}`);
|
|
3805
|
+
feedbackParts.push(`## Review findings from PR #${input.prNumber}
|
|
3626
3806
|
|
|
3627
|
-
${reviewComment}
|
|
3628
|
-
|
|
3807
|
+
${reviewComment}`);
|
|
3808
|
+
}
|
|
3809
|
+
const humanFeedback = getPRFeedbackSinceLastKodyAction(input.prNumber);
|
|
3810
|
+
if (humanFeedback) {
|
|
3811
|
+
logger.info(` Found human feedback on PR #${input.prNumber}`);
|
|
3812
|
+
feedbackParts.push(`## Human review feedback from PR #${input.prNumber}
|
|
3629
3813
|
|
|
3630
|
-
|
|
3814
|
+
${humanFeedback}`);
|
|
3815
|
+
}
|
|
3816
|
+
if (input.feedback) {
|
|
3817
|
+
feedbackParts.push(`## Additional feedback
|
|
3631
3818
|
|
|
3632
|
-
${input.feedback}`
|
|
3819
|
+
${input.feedback}`);
|
|
3820
|
+
}
|
|
3821
|
+
if (feedbackParts.length > 0) {
|
|
3822
|
+
input.feedback = feedbackParts.join("\n\n");
|
|
3633
3823
|
}
|
|
3634
3824
|
}
|
|
3635
3825
|
const config = getProjectConfig();
|
|
@@ -3667,7 +3857,7 @@ ${input.feedback}` : reviewContext;
|
|
|
3667
3857
|
projectDir,
|
|
3668
3858
|
runners,
|
|
3669
3859
|
input: {
|
|
3670
|
-
mode: input.command === "rerun" || input.command === "fix" ? "rerun" : "full",
|
|
3860
|
+
mode: input.command === "rerun" || input.command === "fix" || input.command === "fix-ci" ? "rerun" : "full",
|
|
3671
3861
|
fromStage: input.fromStage,
|
|
3672
3862
|
dryRun: input.dryRun,
|
|
3673
3863
|
issueNumber: input.issueNumber,
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses @kody / /kody comment body into structured inputs.
|
|
3
|
+
* Run by the parse job in GitHub Actions.
|
|
4
|
+
* Reads from env, writes to $GITHUB_OUTPUT.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
const outputFile = process.env.GITHUB_OUTPUT;
|
|
8
|
+
const triggerType = process.env.TRIGGER_TYPE ?? "dispatch";
|
|
9
|
+
function output(key, value) {
|
|
10
|
+
if (outputFile) {
|
|
11
|
+
fs.appendFileSync(outputFile, `${key}=${value}\n`);
|
|
12
|
+
}
|
|
13
|
+
console.log(`${key}=${value}`);
|
|
14
|
+
}
|
|
15
|
+
// For workflow_dispatch, pass through inputs
|
|
16
|
+
if (triggerType === "dispatch") {
|
|
17
|
+
output("task_id", process.env.INPUT_TASK_ID ?? "");
|
|
18
|
+
output("mode", process.env.INPUT_MODE ?? "full");
|
|
19
|
+
output("from_stage", process.env.INPUT_FROM_STAGE ?? "");
|
|
20
|
+
output("issue_number", process.env.INPUT_ISSUE_NUMBER ?? "");
|
|
21
|
+
output("feedback", process.env.INPUT_FEEDBACK ?? "");
|
|
22
|
+
output("valid", process.env.INPUT_TASK_ID ? "true" : "false");
|
|
23
|
+
output("trigger_type", "dispatch");
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
// For issue_comment, parse the comment body
|
|
27
|
+
const commentBody = process.env.COMMENT_BODY ?? "";
|
|
28
|
+
const issueNumber = process.env.ISSUE_NUMBER ?? "";
|
|
29
|
+
// Match: @kody [mode] [task-id] [--from stage] [--feedback "text"]
|
|
30
|
+
const kodyMatch = commentBody.match(/(?:@kody|\/kody)\s*(.*)/i);
|
|
31
|
+
if (!kodyMatch) {
|
|
32
|
+
output("valid", "false");
|
|
33
|
+
output("trigger_type", "comment");
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
const parts = kodyMatch[1].trim().split(/\s+/);
|
|
37
|
+
const validModes = ["full", "rerun", "status"];
|
|
38
|
+
let mode = "full";
|
|
39
|
+
let taskId = "";
|
|
40
|
+
let fromStage = "";
|
|
41
|
+
let feedback = "";
|
|
42
|
+
let i = 0;
|
|
43
|
+
// First arg: mode or task-id
|
|
44
|
+
if (parts[i] && validModes.includes(parts[i])) {
|
|
45
|
+
mode = parts[i];
|
|
46
|
+
i++;
|
|
47
|
+
}
|
|
48
|
+
// Second arg: task-id
|
|
49
|
+
if (parts[i] && !parts[i].startsWith("--")) {
|
|
50
|
+
taskId = parts[i];
|
|
51
|
+
i++;
|
|
52
|
+
}
|
|
53
|
+
// Named args
|
|
54
|
+
while (i < parts.length) {
|
|
55
|
+
if (parts[i] === "--from" && parts[i + 1]) {
|
|
56
|
+
fromStage = parts[i + 1];
|
|
57
|
+
i += 2;
|
|
58
|
+
}
|
|
59
|
+
else if (parts[i] === "--feedback" && parts[i + 1]) {
|
|
60
|
+
// Collect quoted feedback
|
|
61
|
+
const rest = parts.slice(i + 1).join(" ");
|
|
62
|
+
const quoted = rest.match(/^"([^"]*)"/);
|
|
63
|
+
feedback = quoted ? quoted[1] : parts[i + 1];
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
i++;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
output("task_id", taskId);
|
|
71
|
+
output("mode", mode);
|
|
72
|
+
output("from_stage", fromStage);
|
|
73
|
+
output("issue_number", issueNumber);
|
|
74
|
+
output("feedback", feedback);
|
|
75
|
+
output("valid", taskId ? "true" : "false");
|
|
76
|
+
output("trigger_type", "comment");
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates that a comment trigger is safe to execute.
|
|
3
|
+
* Run by the parse job in GitHub Actions.
|
|
4
|
+
* Reads from env, writes to $GITHUB_OUTPUT.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
const ALLOWED_ASSOCIATIONS = ["COLLABORATOR", "MEMBER", "OWNER"];
|
|
8
|
+
const association = process.env.COMMENT_AUTHOR_ASSOCIATION ?? "";
|
|
9
|
+
const outputFile = process.env.GITHUB_OUTPUT;
|
|
10
|
+
function output(key, value) {
|
|
11
|
+
if (outputFile) {
|
|
12
|
+
fs.appendFileSync(outputFile, `${key}=${value}\n`);
|
|
13
|
+
}
|
|
14
|
+
console.log(`${key}=${value}`);
|
|
15
|
+
}
|
|
16
|
+
if (!ALLOWED_ASSOCIATIONS.includes(association)) {
|
|
17
|
+
output("valid", "false");
|
|
18
|
+
output("reason", `Author association '${association}' not in allowlist: ${ALLOWED_ASSOCIATIONS.join(", ")}`);
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
output("valid", "true");
|
|
22
|
+
output("reason", "");
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface CliInput {
|
|
2
|
+
command: "run" | "rerun" | "fix" | "status";
|
|
3
|
+
taskId?: string;
|
|
4
|
+
task?: string;
|
|
5
|
+
fromStage?: string;
|
|
6
|
+
dryRun?: boolean;
|
|
7
|
+
cwd?: string;
|
|
8
|
+
issueNumber?: number;
|
|
9
|
+
feedback?: string;
|
|
10
|
+
local?: boolean;
|
|
11
|
+
complexity?: "low" | "medium" | "high";
|
|
12
|
+
}
|
|
13
|
+
export declare function parseArgs(): CliInput;
|
package/dist/cli/args.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const isCI = !!process.env.GITHUB_ACTIONS;
|
|
2
|
+
function getArg(args, flag) {
|
|
3
|
+
const idx = args.indexOf(flag);
|
|
4
|
+
if (idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith("--")) {
|
|
5
|
+
return args[idx + 1];
|
|
6
|
+
}
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
function hasFlag(args, flag) {
|
|
10
|
+
return args.includes(flag);
|
|
11
|
+
}
|
|
12
|
+
export function parseArgs() {
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
if (hasFlag(args, "--help") || hasFlag(args, "-h") || args.length === 0) {
|
|
15
|
+
console.log(`Usage:
|
|
16
|
+
kody run --task-id <id> [--task "<desc>"] [--cwd <path>] [--issue-number <n>] [--complexity low|medium|high] [--feedback "<text>"] [--local] [--dry-run]
|
|
17
|
+
kody rerun --task-id <id> --from <stage> [--cwd <path>] [--issue-number <n>]
|
|
18
|
+
kody fix --task-id <id> [--cwd <path>] [--issue-number <n>] [--feedback "<text>"]
|
|
19
|
+
kody status --task-id <id> [--cwd <path>]
|
|
20
|
+
kody --help`);
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
const command = args[0];
|
|
24
|
+
if (!["run", "rerun", "fix", "status"].includes(command)) {
|
|
25
|
+
console.error(`Unknown command: ${command}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
const issueStr = getArg(args, "--issue-number") ?? process.env.ISSUE_NUMBER;
|
|
29
|
+
const localFlag = hasFlag(args, "--local");
|
|
30
|
+
return {
|
|
31
|
+
command,
|
|
32
|
+
taskId: getArg(args, "--task-id") ?? process.env.TASK_ID,
|
|
33
|
+
task: getArg(args, "--task"),
|
|
34
|
+
fromStage: getArg(args, "--from") ?? process.env.FROM_STAGE,
|
|
35
|
+
dryRun: hasFlag(args, "--dry-run") || process.env.DRY_RUN === "true",
|
|
36
|
+
cwd: getArg(args, "--cwd"),
|
|
37
|
+
issueNumber: issueStr ? parseInt(issueStr, 10) : undefined,
|
|
38
|
+
feedback: getArg(args, "--feedback") ?? process.env.FEEDBACK,
|
|
39
|
+
local: localFlag || (!isCI && !hasFlag(args, "--no-local")),
|
|
40
|
+
complexity: (getArg(args, "--complexity") ?? process.env.COMPLEXITY),
|
|
41
|
+
};
|
|
42
|
+
}
|