@kody-ade/kody-engine-lite 0.1.63 → 0.1.65
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 +162 -8
- 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/kody.config.schema.json +66 -0
- package/package.json +8 -9
- package/prompts/taskify.md +5 -0
- package/templates/kody.yml +6 -1
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface RunnerConfig {
|
|
2
|
+
type: "claude-code";
|
|
3
|
+
}
|
|
4
|
+
export interface KodyConfig {
|
|
5
|
+
quality: {
|
|
6
|
+
typecheck: string;
|
|
7
|
+
lint: string;
|
|
8
|
+
lintFix: string;
|
|
9
|
+
format: string;
|
|
10
|
+
formatFix: string;
|
|
11
|
+
testUnit: string;
|
|
12
|
+
};
|
|
13
|
+
git: {
|
|
14
|
+
defaultBranch: string;
|
|
15
|
+
userEmail?: string;
|
|
16
|
+
userName?: string;
|
|
17
|
+
};
|
|
18
|
+
github: {
|
|
19
|
+
owner: string;
|
|
20
|
+
repo: string;
|
|
21
|
+
};
|
|
22
|
+
paths: {
|
|
23
|
+
taskDir: string;
|
|
24
|
+
};
|
|
25
|
+
agent: {
|
|
26
|
+
runner?: string;
|
|
27
|
+
modelMap: {
|
|
28
|
+
cheap: string;
|
|
29
|
+
mid: string;
|
|
30
|
+
strong: string;
|
|
31
|
+
};
|
|
32
|
+
litellmUrl?: string;
|
|
33
|
+
usePerStageRouting?: boolean;
|
|
34
|
+
defaultRunner?: string;
|
|
35
|
+
runners?: Record<string, RunnerConfig>;
|
|
36
|
+
stageRunners?: Record<string, string>;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export declare const SIGKILL_GRACE_MS = 5000;
|
|
40
|
+
export declare const MAX_PR_TITLE_LENGTH = 72;
|
|
41
|
+
export declare const STDERR_TAIL_CHARS = 500;
|
|
42
|
+
export declare const API_TIMEOUT_MS = 30000;
|
|
43
|
+
export declare const DEFAULT_MAX_FIX_ATTEMPTS = 2;
|
|
44
|
+
export declare const AGENT_RETRY_DELAY_MS = 2000;
|
|
45
|
+
export declare const VERIFY_COMMAND_TIMEOUT_MS: number;
|
|
46
|
+
export declare const FIX_COMMAND_TIMEOUT_MS: number;
|
|
47
|
+
export declare function setConfigDir(dir: string): void;
|
|
48
|
+
export declare function getProjectConfig(): KodyConfig;
|
|
49
|
+
export declare function resetProjectConfig(): void;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { logger } from "./logger.js";
|
|
4
|
+
const DEFAULT_CONFIG = {
|
|
5
|
+
quality: {
|
|
6
|
+
typecheck: "pnpm -s tsc --noEmit",
|
|
7
|
+
lint: "pnpm -s lint",
|
|
8
|
+
lintFix: "pnpm lint:fix",
|
|
9
|
+
format: "pnpm -s format:check",
|
|
10
|
+
formatFix: "pnpm format:fix",
|
|
11
|
+
testUnit: "pnpm -s test",
|
|
12
|
+
},
|
|
13
|
+
git: {
|
|
14
|
+
defaultBranch: "dev",
|
|
15
|
+
},
|
|
16
|
+
github: {
|
|
17
|
+
owner: "",
|
|
18
|
+
repo: "",
|
|
19
|
+
},
|
|
20
|
+
paths: {
|
|
21
|
+
taskDir: ".tasks",
|
|
22
|
+
},
|
|
23
|
+
agent: {
|
|
24
|
+
runner: "claude-code",
|
|
25
|
+
defaultRunner: "claude",
|
|
26
|
+
modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" },
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
// Pipeline constants
|
|
30
|
+
export const SIGKILL_GRACE_MS = 5000;
|
|
31
|
+
export const MAX_PR_TITLE_LENGTH = 72;
|
|
32
|
+
export const STDERR_TAIL_CHARS = 500;
|
|
33
|
+
export const API_TIMEOUT_MS = 30_000;
|
|
34
|
+
export const DEFAULT_MAX_FIX_ATTEMPTS = 2;
|
|
35
|
+
export const AGENT_RETRY_DELAY_MS = 2000;
|
|
36
|
+
export const VERIFY_COMMAND_TIMEOUT_MS = 5 * 60 * 1000;
|
|
37
|
+
export const FIX_COMMAND_TIMEOUT_MS = 2 * 60 * 1000;
|
|
38
|
+
let _config = null;
|
|
39
|
+
let _configDir = null;
|
|
40
|
+
export function setConfigDir(dir) {
|
|
41
|
+
_configDir = dir;
|
|
42
|
+
_config = null;
|
|
43
|
+
}
|
|
44
|
+
export function getProjectConfig() {
|
|
45
|
+
if (_config)
|
|
46
|
+
return _config;
|
|
47
|
+
const configPath = path.join(_configDir ?? process.cwd(), "kody.config.json");
|
|
48
|
+
if (fs.existsSync(configPath)) {
|
|
49
|
+
try {
|
|
50
|
+
const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
51
|
+
_config = {
|
|
52
|
+
quality: { ...DEFAULT_CONFIG.quality, ...raw.quality },
|
|
53
|
+
git: { ...DEFAULT_CONFIG.git, ...raw.git },
|
|
54
|
+
github: { ...DEFAULT_CONFIG.github, ...raw.github },
|
|
55
|
+
paths: { ...DEFAULT_CONFIG.paths, ...raw.paths },
|
|
56
|
+
agent: { ...DEFAULT_CONFIG.agent, ...raw.agent },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
logger.warn("kody.config.json is invalid JSON — using defaults");
|
|
61
|
+
_config = { ...DEFAULT_CONFIG };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
_config = { ...DEFAULT_CONFIG };
|
|
66
|
+
}
|
|
67
|
+
return _config;
|
|
68
|
+
}
|
|
69
|
+
export function resetProjectConfig() {
|
|
70
|
+
_config = null;
|
|
71
|
+
_configDir = null;
|
|
72
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function readPromptFile(stageName: string): string;
|
|
2
|
+
export declare function injectTaskContext(prompt: string, taskId: string, taskDir: string, feedback?: string): string;
|
|
3
|
+
export declare function buildFullPrompt(stageName: string, taskId: string, taskDir: string, projectDir: string, feedback?: string): string;
|
|
4
|
+
export declare function resolveModel(modelTier: string, stageName?: string): string;
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { readProjectMemory } from "./memory.js";
|
|
4
|
+
import { getProjectConfig } from "./config.js";
|
|
5
|
+
const DEFAULT_MODEL_MAP = {
|
|
6
|
+
cheap: "haiku",
|
|
7
|
+
mid: "sonnet",
|
|
8
|
+
strong: "opus",
|
|
9
|
+
};
|
|
10
|
+
const MAX_TASK_CONTEXT_PLAN = 1500;
|
|
11
|
+
const MAX_TASK_CONTEXT_SPEC = 2000;
|
|
12
|
+
export function readPromptFile(stageName) {
|
|
13
|
+
const scriptDir = new URL(".", import.meta.url).pathname;
|
|
14
|
+
// Try multiple resolution paths (dev: src/../prompts, prod: dist/bin/../../prompts)
|
|
15
|
+
const candidates = [
|
|
16
|
+
path.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
|
|
17
|
+
path.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`),
|
|
18
|
+
];
|
|
19
|
+
for (const candidate of candidates) {
|
|
20
|
+
if (fs.existsSync(candidate)) {
|
|
21
|
+
return fs.readFileSync(candidate, "utf-8");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
|
|
25
|
+
}
|
|
26
|
+
export function injectTaskContext(prompt, taskId, taskDir, feedback) {
|
|
27
|
+
let context = `## Task Context\n`;
|
|
28
|
+
context += `Task ID: ${taskId}\n`;
|
|
29
|
+
context += `Task Directory: ${taskDir}\n`;
|
|
30
|
+
const taskMdPath = path.join(taskDir, "task.md");
|
|
31
|
+
if (fs.existsSync(taskMdPath)) {
|
|
32
|
+
const taskMd = fs.readFileSync(taskMdPath, "utf-8");
|
|
33
|
+
context += `\n## Task Description\n${taskMd}\n`;
|
|
34
|
+
}
|
|
35
|
+
const taskJsonPath = path.join(taskDir, "task.json");
|
|
36
|
+
if (fs.existsSync(taskJsonPath)) {
|
|
37
|
+
try {
|
|
38
|
+
const taskDef = JSON.parse(fs.readFileSync(taskJsonPath, "utf-8"));
|
|
39
|
+
context += `\n## Task Classification\n`;
|
|
40
|
+
context += `Type: ${taskDef.task_type ?? "unknown"}\n`;
|
|
41
|
+
context += `Title: ${taskDef.title ?? "unknown"}\n`;
|
|
42
|
+
context += `Risk: ${taskDef.risk_level ?? "unknown"}\n`;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Ignore parse errors
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const specPath = path.join(taskDir, "spec.md");
|
|
49
|
+
if (fs.existsSync(specPath)) {
|
|
50
|
+
const spec = fs.readFileSync(specPath, "utf-8");
|
|
51
|
+
const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
|
|
52
|
+
context += `\n## Spec Summary\n${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}\n`;
|
|
53
|
+
}
|
|
54
|
+
const planPath = path.join(taskDir, "plan.md");
|
|
55
|
+
if (fs.existsSync(planPath)) {
|
|
56
|
+
const plan = fs.readFileSync(planPath, "utf-8");
|
|
57
|
+
const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
|
|
58
|
+
context += `\n## Plan Summary\n${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}\n`;
|
|
59
|
+
}
|
|
60
|
+
if (feedback) {
|
|
61
|
+
context += `\n## Human Feedback\n${feedback}\n`;
|
|
62
|
+
}
|
|
63
|
+
return prompt.replace("{{TASK_CONTEXT}}", context);
|
|
64
|
+
}
|
|
65
|
+
export function buildFullPrompt(stageName, taskId, taskDir, projectDir, feedback) {
|
|
66
|
+
const memory = readProjectMemory(projectDir);
|
|
67
|
+
const promptTemplate = readPromptFile(stageName);
|
|
68
|
+
const prompt = injectTaskContext(promptTemplate, taskId, taskDir, feedback);
|
|
69
|
+
return memory ? `${memory}\n---\n\n${prompt}` : prompt;
|
|
70
|
+
}
|
|
71
|
+
export function resolveModel(modelTier, stageName) {
|
|
72
|
+
const config = getProjectConfig();
|
|
73
|
+
// Per-stage routing: use stage name as LiteLLM alias
|
|
74
|
+
if (config.agent.usePerStageRouting && stageName) {
|
|
75
|
+
return stageName;
|
|
76
|
+
}
|
|
77
|
+
// Config model map (may be LiteLLM aliases or direct model names)
|
|
78
|
+
const mapped = config.agent.modelMap[modelTier];
|
|
79
|
+
if (mapped)
|
|
80
|
+
return mapped;
|
|
81
|
+
// Fallback to defaults
|
|
82
|
+
return DEFAULT_MODEL_MAP[modelTier] ?? "sonnet";
|
|
83
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export const STAGES = [
|
|
2
|
+
{
|
|
3
|
+
name: "taskify",
|
|
4
|
+
type: "agent",
|
|
5
|
+
modelTier: "cheap",
|
|
6
|
+
timeout: 300_000,
|
|
7
|
+
maxRetries: 1,
|
|
8
|
+
outputFile: "task.json",
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: "plan",
|
|
12
|
+
type: "agent",
|
|
13
|
+
modelTier: "strong",
|
|
14
|
+
timeout: 600_000,
|
|
15
|
+
maxRetries: 1,
|
|
16
|
+
outputFile: "plan.md",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: "build",
|
|
20
|
+
type: "agent",
|
|
21
|
+
modelTier: "mid",
|
|
22
|
+
timeout: 1_200_000,
|
|
23
|
+
maxRetries: 1,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "verify",
|
|
27
|
+
type: "gate",
|
|
28
|
+
modelTier: "cheap",
|
|
29
|
+
timeout: 300_000,
|
|
30
|
+
maxRetries: 2,
|
|
31
|
+
retryWithAgent: "autofix",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "review",
|
|
35
|
+
type: "agent",
|
|
36
|
+
modelTier: "strong",
|
|
37
|
+
timeout: 600_000,
|
|
38
|
+
maxRetries: 1,
|
|
39
|
+
outputFile: "review.md",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "review-fix",
|
|
43
|
+
type: "agent",
|
|
44
|
+
modelTier: "mid",
|
|
45
|
+
timeout: 600_000,
|
|
46
|
+
maxRetries: 1,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "ship",
|
|
50
|
+
type: "deterministic",
|
|
51
|
+
modelTier: "cheap",
|
|
52
|
+
timeout: 120_000,
|
|
53
|
+
maxRetries: 1,
|
|
54
|
+
outputFile: "ship.md",
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
export function getStage(name) {
|
|
58
|
+
return STAGES.find((s) => s.name === name);
|
|
59
|
+
}
|
package/dist/entry.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/entry.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { createRunners } from "./agent-runner.js";
|
|
4
|
+
import { runPipeline, printStatus } from "./pipeline.js";
|
|
5
|
+
import { runPreflight } from "./preflight.js";
|
|
6
|
+
import { setConfigDir, getProjectConfig } from "./config.js";
|
|
7
|
+
import { setGhCwd, getIssue, postComment } from "./github-api.js";
|
|
8
|
+
import { logger } from "./logger.js";
|
|
9
|
+
// Extracted modules
|
|
10
|
+
import { parseArgs } from "./cli/args.js";
|
|
11
|
+
import { checkLitellmHealth, tryStartLitellm } from "./cli/litellm.js";
|
|
12
|
+
import { findLatestTaskForIssue, generateTaskId } from "./cli/task-resolution.js";
|
|
13
|
+
async function main() {
|
|
14
|
+
const input = parseArgs();
|
|
15
|
+
// Resolve working directory first (needed for task lookup)
|
|
16
|
+
const projectDir = input.cwd ? path.resolve(input.cwd) : process.cwd();
|
|
17
|
+
if (input.cwd) {
|
|
18
|
+
if (!fs.existsSync(projectDir)) {
|
|
19
|
+
console.error(`--cwd path does not exist: ${projectDir}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
setConfigDir(projectDir);
|
|
23
|
+
setGhCwd(projectDir);
|
|
24
|
+
logger.info(`Working directory: ${projectDir}`);
|
|
25
|
+
}
|
|
26
|
+
// Resolve taskId
|
|
27
|
+
let taskId = input.taskId;
|
|
28
|
+
if (!taskId) {
|
|
29
|
+
if ((input.command === "rerun" || input.command === "fix") && input.issueNumber) {
|
|
30
|
+
const found = findLatestTaskForIssue(input.issueNumber, projectDir);
|
|
31
|
+
if (!found) {
|
|
32
|
+
console.error(`No previous task found for issue #${input.issueNumber}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
taskId = found;
|
|
36
|
+
logger.info(`Found latest task for issue #${input.issueNumber}: ${taskId}`);
|
|
37
|
+
}
|
|
38
|
+
else if (input.issueNumber) {
|
|
39
|
+
taskId = `${input.issueNumber}-${generateTaskId()}`;
|
|
40
|
+
}
|
|
41
|
+
else if (input.command === "run" && input.task) {
|
|
42
|
+
taskId = generateTaskId();
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
console.error("--task-id is required (or provide --issue-number to auto-generate)");
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const taskDir = path.join(projectDir, ".tasks", taskId);
|
|
50
|
+
fs.mkdirSync(taskDir, { recursive: true });
|
|
51
|
+
// Status command — no preflight needed
|
|
52
|
+
if (input.command === "status") {
|
|
53
|
+
printStatus(taskId, taskDir);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// Preflight
|
|
57
|
+
logger.info("Preflight checks:");
|
|
58
|
+
runPreflight();
|
|
59
|
+
// Write task.md if --task provided
|
|
60
|
+
if (input.task) {
|
|
61
|
+
fs.writeFileSync(path.join(taskDir, "task.md"), input.task);
|
|
62
|
+
}
|
|
63
|
+
// Auto-fetch issue body as task if no task.md and issue-number provided
|
|
64
|
+
const taskMdPath = path.join(taskDir, "task.md");
|
|
65
|
+
if (!fs.existsSync(taskMdPath) && input.issueNumber) {
|
|
66
|
+
logger.info(`Fetching issue #${input.issueNumber} body as task...`);
|
|
67
|
+
const issue = getIssue(input.issueNumber);
|
|
68
|
+
if (issue) {
|
|
69
|
+
const taskContent = `# ${issue.title}\n\n${issue.body ?? ""}`;
|
|
70
|
+
fs.writeFileSync(taskMdPath, taskContent);
|
|
71
|
+
logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Verify task.md exists for run
|
|
75
|
+
if (input.command === "run") {
|
|
76
|
+
if (!fs.existsSync(taskMdPath)) {
|
|
77
|
+
console.error("No task.md found. Provide --task, --issue-number, or ensure .tasks/<id>/task.md exists.");
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Fix command defaults to --from build
|
|
82
|
+
if (input.command === "fix" && !input.fromStage) {
|
|
83
|
+
input.fromStage = "build";
|
|
84
|
+
}
|
|
85
|
+
// Auto-detect --from for rerun if not provided (find paused stage)
|
|
86
|
+
if (input.command === "rerun" && !input.fromStage) {
|
|
87
|
+
const statusPath = path.join(taskDir, "status.json");
|
|
88
|
+
if (fs.existsSync(statusPath)) {
|
|
89
|
+
try {
|
|
90
|
+
const status = JSON.parse(fs.readFileSync(statusPath, "utf-8"));
|
|
91
|
+
const stageNames = ["taskify", "plan", "build", "verify", "review", "review-fix", "ship"];
|
|
92
|
+
let foundPaused = false;
|
|
93
|
+
for (const name of stageNames) {
|
|
94
|
+
const s = status.stages[name];
|
|
95
|
+
if (s?.error?.includes("paused")) {
|
|
96
|
+
const idx = stageNames.indexOf(name);
|
|
97
|
+
if (idx < stageNames.length - 1) {
|
|
98
|
+
input.fromStage = stageNames[idx + 1];
|
|
99
|
+
foundPaused = true;
|
|
100
|
+
logger.info(`Auto-detected resume from: ${input.fromStage} (after paused ${name})`);
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (s?.state === "failed" || s?.state === "pending") {
|
|
105
|
+
input.fromStage = name;
|
|
106
|
+
foundPaused = true;
|
|
107
|
+
logger.info(`Auto-detected resume from: ${input.fromStage}`);
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (!foundPaused) {
|
|
112
|
+
input.fromStage = "taskify";
|
|
113
|
+
logger.info("No paused/failed stage found, resuming from taskify");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
console.error("--from <stage> is required (could not read status.json)");
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
// No status.json — fall back to full run with feedback preserved
|
|
123
|
+
logger.info("No status.json found — running full pipeline with feedback");
|
|
124
|
+
input.command = "run";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Start LiteLLM proxy if configured and not running
|
|
128
|
+
const config = getProjectConfig();
|
|
129
|
+
let litellmProcess = null;
|
|
130
|
+
const cleanupLitellm = () => { if (litellmProcess) {
|
|
131
|
+
litellmProcess.kill();
|
|
132
|
+
litellmProcess = null;
|
|
133
|
+
} };
|
|
134
|
+
process.on("exit", cleanupLitellm);
|
|
135
|
+
process.on("SIGINT", () => { cleanupLitellm(); process.exit(130); });
|
|
136
|
+
process.on("SIGTERM", () => { cleanupLitellm(); process.exit(143); });
|
|
137
|
+
if (config.agent.litellmUrl) {
|
|
138
|
+
const proxyRunning = await checkLitellmHealth(config.agent.litellmUrl);
|
|
139
|
+
if (!proxyRunning) {
|
|
140
|
+
litellmProcess = await tryStartLitellm(config.agent.litellmUrl, projectDir);
|
|
141
|
+
if (!litellmProcess) {
|
|
142
|
+
logger.warn("LiteLLM not available — falling back to Anthropic models");
|
|
143
|
+
config.agent.litellmUrl = undefined;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
logger.info(`LiteLLM proxy already running at ${config.agent.litellmUrl}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Create runners
|
|
151
|
+
const runners = createRunners(config);
|
|
152
|
+
const defaultRunnerName = config.agent.defaultRunner ?? Object.keys(runners)[0] ?? "claude";
|
|
153
|
+
const defaultRunner = runners[defaultRunnerName];
|
|
154
|
+
if (!defaultRunner) {
|
|
155
|
+
console.error(`Default runner "${defaultRunnerName}" not configured`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
const healthy = await defaultRunner.healthCheck();
|
|
159
|
+
if (!healthy) {
|
|
160
|
+
console.error(`Runner "${defaultRunnerName}" health check failed`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
// Build context
|
|
164
|
+
const ctx = {
|
|
165
|
+
taskId,
|
|
166
|
+
taskDir,
|
|
167
|
+
projectDir,
|
|
168
|
+
runners,
|
|
169
|
+
input: {
|
|
170
|
+
mode: (input.command === "rerun" || input.command === "fix") ? "rerun" : "full",
|
|
171
|
+
fromStage: input.fromStage,
|
|
172
|
+
dryRun: input.dryRun,
|
|
173
|
+
issueNumber: input.issueNumber,
|
|
174
|
+
feedback: input.feedback,
|
|
175
|
+
local: input.local,
|
|
176
|
+
complexity: input.complexity,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
logger.info(`Task: ${taskId}`);
|
|
180
|
+
logger.info(`Mode: ${ctx.input.mode}${ctx.input.local ? " (local)" : " (CI)"}`);
|
|
181
|
+
if (ctx.input.issueNumber)
|
|
182
|
+
logger.info(`Issue: #${ctx.input.issueNumber}`);
|
|
183
|
+
// Post task-id comment so user knows the ID for rerun
|
|
184
|
+
if (ctx.input.issueNumber && !ctx.input.local && ctx.input.mode === "full") {
|
|
185
|
+
const runUrl = process.env.RUN_URL ?? "";
|
|
186
|
+
const runLink = runUrl ? ` ([logs](${runUrl}))` : "";
|
|
187
|
+
try {
|
|
188
|
+
postComment(ctx.input.issueNumber, `🚀 Kody pipeline started: \`${taskId}\`${runLink}\n\nTo rerun: \`@kody rerun ${taskId} --from <stage>\``);
|
|
189
|
+
}
|
|
190
|
+
catch { /* best effort */ }
|
|
191
|
+
}
|
|
192
|
+
// Run pipeline
|
|
193
|
+
const state = await runPipeline(ctx);
|
|
194
|
+
// Report
|
|
195
|
+
const files = fs.readdirSync(taskDir);
|
|
196
|
+
console.log(`\nArtifacts in ${taskDir}:`);
|
|
197
|
+
for (const f of files) {
|
|
198
|
+
console.log(` ${f}`);
|
|
199
|
+
}
|
|
200
|
+
if (state.state === "failed") {
|
|
201
|
+
// Check if this is a "paused" state (questions posted) — not a real failure
|
|
202
|
+
const isPaused = Object.values(state.stages).some((s) => s.error?.includes("paused") ?? false);
|
|
203
|
+
if (isPaused) {
|
|
204
|
+
process.exit(0);
|
|
205
|
+
}
|
|
206
|
+
// Post failure comment on issue
|
|
207
|
+
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
208
|
+
const failedStage = Object.entries(state.stages).find(([, s]) => s.state === "failed" || s.state === "timeout");
|
|
209
|
+
const stageName = failedStage ? failedStage[0] : "unknown";
|
|
210
|
+
const error = failedStage ? failedStage[1].error ?? "" : "";
|
|
211
|
+
try {
|
|
212
|
+
postComment(ctx.input.issueNumber, `❌ Pipeline failed at **${stageName}**${error ? `: ${error.slice(0, 200)}` : ""}`);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// Best effort
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
main().catch(async (err) => {
|
|
222
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
223
|
+
console.error(msg);
|
|
224
|
+
// Post crash comment if we have issue context
|
|
225
|
+
const issueStr = process.argv.find((_, i, a) => a[i - 1] === "--issue-number") ?? process.env.ISSUE_NUMBER;
|
|
226
|
+
const isLocal = process.argv.includes("--local") || !process.env.GITHUB_ACTIONS;
|
|
227
|
+
if (issueStr && !isLocal) {
|
|
228
|
+
try {
|
|
229
|
+
postComment(parseInt(issueStr, 10), `❌ Pipeline crashed: ${msg.slice(0, 200)}`);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// Best effort
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
process.exit(1);
|
|
236
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare function deriveBranchName(issueNumber: number, title: string): string;
|
|
2
|
+
export declare function getDefaultBranch(cwd?: string): string;
|
|
3
|
+
export declare function getCurrentBranch(cwd?: string): string;
|
|
4
|
+
export declare function ensureFeatureBranch(issueNumber: number, title: string, cwd?: string): string;
|
|
5
|
+
export declare function syncWithDefault(cwd?: string): void;
|
|
6
|
+
export declare function commitAll(message: string, cwd?: string): {
|
|
7
|
+
success: boolean;
|
|
8
|
+
hash: string;
|
|
9
|
+
message: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function pushBranch(cwd?: string): void;
|
|
12
|
+
export declare function getChangedFiles(baseBranch: string, cwd?: string): string[];
|
|
13
|
+
export declare function getDiff(baseBranch: string, cwd?: string): string;
|