@kody-ade/kody-engine-lite 0.1.108 → 0.1.110
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/bin/cli.js +1971 -1504
- package/package.json +1 -1
- package/prompts/taskify-ticket.md +112 -0
- package/templates/kody.yml +12 -0
package/dist/bin/cli.js
CHANGED
|
@@ -64,212 +64,260 @@ var init_architecture_detection = __esm({
|
|
|
64
64
|
}
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
// src/
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
runCiParse: () => runCiParse,
|
|
72
|
-
writeOutputs: () => writeOutputs
|
|
73
|
-
});
|
|
74
|
-
import * as fs8 from "fs";
|
|
75
|
-
function generateTimestamp() {
|
|
76
|
-
const now = /* @__PURE__ */ new Date();
|
|
77
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
78
|
-
const y = String(now.getFullYear()).slice(2);
|
|
79
|
-
const m = pad(now.getMonth() + 1);
|
|
80
|
-
const d = pad(now.getDate());
|
|
81
|
-
const H = pad(now.getHours());
|
|
82
|
-
const M = pad(now.getMinutes());
|
|
83
|
-
const S = pad(now.getSeconds());
|
|
84
|
-
return `${y}${m}${d}-${H}${M}${S}`;
|
|
67
|
+
// src/logger.ts
|
|
68
|
+
function getLevel() {
|
|
69
|
+
const env = process.env.LOG_LEVEL;
|
|
70
|
+
return LEVELS[env ?? "info"] ?? LEVELS.info;
|
|
85
71
|
}
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
ci_run_id: "",
|
|
99
|
-
dry_run: false,
|
|
100
|
-
valid: !!taskId2,
|
|
101
|
-
trigger_type: "dispatch"
|
|
102
|
-
};
|
|
72
|
+
function timestamp() {
|
|
73
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
|
|
74
|
+
}
|
|
75
|
+
function log(level, msg) {
|
|
76
|
+
if (LEVELS[level] < getLevel()) return;
|
|
77
|
+
const prefix = `[${timestamp()}] ${level.toUpperCase().padEnd(5)}`;
|
|
78
|
+
if (level === "error") {
|
|
79
|
+
console.error(`${prefix} ${msg}`);
|
|
80
|
+
} else if (level === "warn") {
|
|
81
|
+
console.warn(`${prefix} ${msg}`);
|
|
82
|
+
} else {
|
|
83
|
+
console.log(`${prefix} ${msg}`);
|
|
103
84
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
85
|
+
}
|
|
86
|
+
function ciGroup(title) {
|
|
87
|
+
if (isCI) process.stdout.write(`::group::${title}
|
|
88
|
+
`);
|
|
89
|
+
}
|
|
90
|
+
function ciGroupEnd() {
|
|
91
|
+
if (isCI) process.stdout.write(`::endgroup::
|
|
92
|
+
`);
|
|
93
|
+
}
|
|
94
|
+
var isCI, LEVELS, logger;
|
|
95
|
+
var init_logger = __esm({
|
|
96
|
+
"src/logger.ts"() {
|
|
97
|
+
"use strict";
|
|
98
|
+
isCI = !!process.env.GITHUB_ACTIONS;
|
|
99
|
+
LEVELS = {
|
|
100
|
+
debug: 0,
|
|
101
|
+
info: 1,
|
|
102
|
+
warn: 2,
|
|
103
|
+
error: 3
|
|
104
|
+
};
|
|
105
|
+
logger = {
|
|
106
|
+
debug: (msg) => log("debug", msg),
|
|
107
|
+
info: (msg) => log("info", msg),
|
|
108
|
+
warn: (msg) => log("warn", msg),
|
|
109
|
+
error: (msg) => log("error", msg)
|
|
121
110
|
};
|
|
122
111
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (/--dry-run/.test(argsLine)) dryRun = true;
|
|
136
|
-
const ciRunIdMatch = argsLine.match(/--ci-run-id\s+(\S+)/);
|
|
137
|
-
if (ciRunIdMatch) ciRunId = ciRunIdMatch[1];
|
|
138
|
-
const positional = argsLine.replace(/--from\s+\S+/g, "").replace(/--feedback\s+"[^"]*"/g, "").replace(/--complexity\s+\S+/g, "").replace(/--dry-run/g, "").replace(/--ci-run-id\s+\S+/g, "").replace(/\s+/g, " ").trim();
|
|
139
|
-
const parts = positional ? positional.split(/\s+/) : [];
|
|
140
|
-
let mode = "full";
|
|
141
|
-
let taskId = "";
|
|
142
|
-
let idx = 0;
|
|
143
|
-
if (parts[idx] && VALID_MODES.includes(parts[idx])) {
|
|
144
|
-
mode = parts[idx];
|
|
145
|
-
idx++;
|
|
146
|
-
}
|
|
147
|
-
if (parts[idx] && !parts[idx].startsWith("--")) {
|
|
148
|
-
taskId = parts[idx];
|
|
149
|
-
idx++;
|
|
150
|
-
} else if (parts[0] && !VALID_MODES.includes(parts[0]) && !parts[0].startsWith("--")) {
|
|
151
|
-
taskId = parts[0];
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// src/validators.ts
|
|
115
|
+
function stripFences(content) {
|
|
116
|
+
return content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
117
|
+
}
|
|
118
|
+
function parseJsonSafe(raw, requiredFields) {
|
|
119
|
+
let parsed;
|
|
120
|
+
try {
|
|
121
|
+
parsed = JSON.parse(raw);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
return { ok: false, error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}` };
|
|
152
124
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const newlineIdx = afterKodyLine.indexOf("\n");
|
|
156
|
-
const bodyAfterCommand = newlineIdx !== -1 ? afterKodyLine.slice(newlineIdx + 1) : "";
|
|
157
|
-
if (mode === "approve") {
|
|
158
|
-
mode = "rerun";
|
|
159
|
-
if (bodyAfterCommand) {
|
|
160
|
-
feedback = bodyAfterCommand;
|
|
161
|
-
}
|
|
125
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
126
|
+
return { ok: false, error: `Expected JSON object, got ${Array.isArray(parsed) ? "array" : typeof parsed}` };
|
|
162
127
|
}
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
|
|
128
|
+
if (requiredFields) {
|
|
129
|
+
for (const field of requiredFields) {
|
|
130
|
+
if (!(field in parsed)) {
|
|
131
|
+
return { ok: false, error: `Missing required field: ${field}` };
|
|
132
|
+
}
|
|
166
133
|
}
|
|
167
134
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
135
|
+
return { ok: true, data: parsed };
|
|
136
|
+
}
|
|
137
|
+
function validateTaskJson(content) {
|
|
138
|
+
try {
|
|
139
|
+
const parsed = JSON.parse(stripFences(content));
|
|
140
|
+
for (const field of REQUIRED_TASK_FIELDS) {
|
|
141
|
+
if (!(field in parsed)) {
|
|
142
|
+
return { valid: false, error: `Missing field: ${field}` };
|
|
174
143
|
}
|
|
175
144
|
}
|
|
145
|
+
return { valid: true };
|
|
146
|
+
} catch (err) {
|
|
147
|
+
return {
|
|
148
|
+
valid: false,
|
|
149
|
+
error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
150
|
+
};
|
|
176
151
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (mode === "review" && prNumber) {
|
|
182
|
-
taskId = `review-pr-${prNumber}-${generateTimestamp()}`;
|
|
152
|
+
}
|
|
153
|
+
function validatePlanMd(content) {
|
|
154
|
+
if (content.length < 10) {
|
|
155
|
+
return { valid: false, error: "Plan is too short (< 10 chars)" };
|
|
183
156
|
}
|
|
184
|
-
if (
|
|
185
|
-
|
|
157
|
+
if (!/^##\s+\w+/m.test(content)) {
|
|
158
|
+
return { valid: false, error: "Plan has no markdown h2 sections" };
|
|
186
159
|
}
|
|
187
|
-
|
|
188
|
-
const valid = !!taskId || modesWithoutTaskId.includes(mode);
|
|
189
|
-
return {
|
|
190
|
-
task_id: taskId,
|
|
191
|
-
mode,
|
|
192
|
-
from_stage: fromStage,
|
|
193
|
-
issue_number: issueNumber,
|
|
194
|
-
pr_number: prNumber,
|
|
195
|
-
feedback,
|
|
196
|
-
complexity,
|
|
197
|
-
ci_run_id: ciRunId,
|
|
198
|
-
dry_run: dryRun,
|
|
199
|
-
valid,
|
|
200
|
-
trigger_type: "comment"
|
|
201
|
-
};
|
|
160
|
+
return { valid: true };
|
|
202
161
|
}
|
|
203
|
-
function
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if (outputFile) {
|
|
207
|
-
if (value.includes("\n")) {
|
|
208
|
-
fs8.appendFileSync(outputFile, `${key}<<KODY_EOF
|
|
209
|
-
${value}
|
|
210
|
-
KODY_EOF
|
|
211
|
-
`);
|
|
212
|
-
} else {
|
|
213
|
-
fs8.appendFileSync(outputFile, `${key}=${value}
|
|
214
|
-
`);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
const display = value.includes("\n") ? value.split("\n")[0] + "..." : value;
|
|
218
|
-
console.log(`${key}=${display}`);
|
|
162
|
+
function validateReviewMd(content) {
|
|
163
|
+
if (/pass/i.test(content) || /fail/i.test(content)) {
|
|
164
|
+
return { valid: true };
|
|
219
165
|
}
|
|
220
|
-
|
|
221
|
-
output("mode", result.mode);
|
|
222
|
-
output("from_stage", result.from_stage);
|
|
223
|
-
output("issue_number", result.issue_number);
|
|
224
|
-
output("pr_number", result.pr_number);
|
|
225
|
-
output("feedback", result.feedback);
|
|
226
|
-
output("complexity", result.complexity);
|
|
227
|
-
output("ci_run_id", result.ci_run_id);
|
|
228
|
-
output("dry_run", result.dry_run ? "true" : "false");
|
|
229
|
-
output("valid", result.valid ? "true" : "false");
|
|
230
|
-
output("trigger_type", result.trigger_type);
|
|
231
|
-
}
|
|
232
|
-
function runCiParse() {
|
|
233
|
-
const result = parseCommentInputs();
|
|
234
|
-
writeOutputs(result);
|
|
166
|
+
return { valid: false, error: "Review must contain 'pass' or 'fail'" };
|
|
235
167
|
}
|
|
236
|
-
var
|
|
237
|
-
var
|
|
238
|
-
"src/
|
|
168
|
+
var REQUIRED_TASK_FIELDS;
|
|
169
|
+
var init_validators = __esm({
|
|
170
|
+
"src/validators.ts"() {
|
|
239
171
|
"use strict";
|
|
240
|
-
|
|
241
|
-
"
|
|
242
|
-
"
|
|
243
|
-
"
|
|
244
|
-
"
|
|
245
|
-
"
|
|
246
|
-
"approve",
|
|
247
|
-
"review",
|
|
248
|
-
"resolve",
|
|
249
|
-
"bootstrap"
|
|
172
|
+
REQUIRED_TASK_FIELDS = [
|
|
173
|
+
"task_type",
|
|
174
|
+
"title",
|
|
175
|
+
"description",
|
|
176
|
+
"scope",
|
|
177
|
+
"risk_level"
|
|
250
178
|
];
|
|
251
179
|
}
|
|
252
180
|
});
|
|
253
181
|
|
|
182
|
+
// src/config.ts
|
|
183
|
+
import * as fs8 from "fs";
|
|
184
|
+
import * as path7 from "path";
|
|
185
|
+
function resolveStageConfig(config, stageName, modelTier) {
|
|
186
|
+
const stageOverride = config.agent.stages?.[stageName];
|
|
187
|
+
if (stageOverride) return stageOverride;
|
|
188
|
+
if (config.agent.default) return config.agent.default;
|
|
189
|
+
const model = config.agent.modelMap[modelTier];
|
|
190
|
+
if (!model) {
|
|
191
|
+
throw new Error(`No model configured for stage '${stageName}' (tier: ${modelTier}). Set agent.stages.${stageName} or agent.default in kody.config.json`);
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
provider: config.agent.provider ?? "claude",
|
|
195
|
+
model
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function needsLitellmProxy(config) {
|
|
199
|
+
return !!(config.agent.provider && config.agent.provider !== "anthropic");
|
|
200
|
+
}
|
|
201
|
+
function stageNeedsProxy(stageConfig) {
|
|
202
|
+
return stageConfig.provider !== "claude" && stageConfig.provider !== "anthropic";
|
|
203
|
+
}
|
|
204
|
+
function anyStageNeedsProxy(config) {
|
|
205
|
+
if (config.agent.stages) {
|
|
206
|
+
for (const sc of Object.values(config.agent.stages)) {
|
|
207
|
+
if (stageNeedsProxy(sc)) return true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (config.agent.default && stageNeedsProxy(config.agent.default)) return true;
|
|
211
|
+
return needsLitellmProxy(config);
|
|
212
|
+
}
|
|
213
|
+
function getLitellmUrl() {
|
|
214
|
+
return LITELLM_DEFAULT_URL;
|
|
215
|
+
}
|
|
216
|
+
function providerApiKeyEnvVar(provider) {
|
|
217
|
+
if (provider === "anthropic") return "ANTHROPIC_API_KEY";
|
|
218
|
+
return "ANTHROPIC_COMPATIBLE_API_KEY";
|
|
219
|
+
}
|
|
220
|
+
function setConfigDir(dir) {
|
|
221
|
+
_configDir = dir;
|
|
222
|
+
_config = null;
|
|
223
|
+
}
|
|
224
|
+
function getProjectConfig() {
|
|
225
|
+
if (_config) return _config;
|
|
226
|
+
const configPath = path7.join(_configDir ?? process.cwd(), "kody.config.json");
|
|
227
|
+
if (fs8.existsSync(configPath)) {
|
|
228
|
+
try {
|
|
229
|
+
const result = parseJsonSafe(fs8.readFileSync(configPath, "utf-8"));
|
|
230
|
+
if (!result.ok) {
|
|
231
|
+
logger.warn(`kody.config.json: ${result.error} \u2014 using defaults`);
|
|
232
|
+
_config = { ...DEFAULT_CONFIG };
|
|
233
|
+
return _config;
|
|
234
|
+
}
|
|
235
|
+
const raw = result.data;
|
|
236
|
+
_config = {
|
|
237
|
+
quality: { ...DEFAULT_CONFIG.quality, ...raw.quality },
|
|
238
|
+
git: { ...DEFAULT_CONFIG.git, ...raw.git },
|
|
239
|
+
github: { ...DEFAULT_CONFIG.github, ...raw.github },
|
|
240
|
+
agent: {
|
|
241
|
+
...DEFAULT_CONFIG.agent,
|
|
242
|
+
...raw.agent,
|
|
243
|
+
modelMap: { ...DEFAULT_CONFIG.agent.modelMap, ...raw.agent?.modelMap }
|
|
244
|
+
},
|
|
245
|
+
timeouts: raw.timeouts ?? void 0,
|
|
246
|
+
contextTiers: raw.contextTiers ? { ...DEFAULT_CONFIG.contextTiers, ...raw.contextTiers } : DEFAULT_CONFIG.contextTiers,
|
|
247
|
+
mcp: raw.mcp ? {
|
|
248
|
+
servers: {},
|
|
249
|
+
stages: ["build", "verify", "review", "review-fix"],
|
|
250
|
+
...raw.mcp,
|
|
251
|
+
// Auto-enable when devServer is configured (user can still set enabled: false to override)
|
|
252
|
+
enabled: raw.mcp.enabled ?? !!raw.mcp.devServer
|
|
253
|
+
} : void 0
|
|
254
|
+
};
|
|
255
|
+
} catch {
|
|
256
|
+
logger.warn("kody.config.json is invalid JSON \u2014 using defaults");
|
|
257
|
+
_config = { ...DEFAULT_CONFIG };
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
_config = { ...DEFAULT_CONFIG };
|
|
261
|
+
}
|
|
262
|
+
return _config;
|
|
263
|
+
}
|
|
264
|
+
var DEFAULT_CONFIG, LITELLM_DEFAULT_PORT, LITELLM_DEFAULT_URL, VERIFY_COMMAND_TIMEOUT_MS, FIX_COMMAND_TIMEOUT_MS, _config, _configDir;
|
|
265
|
+
var init_config = __esm({
|
|
266
|
+
"src/config.ts"() {
|
|
267
|
+
"use strict";
|
|
268
|
+
init_logger();
|
|
269
|
+
init_validators();
|
|
270
|
+
DEFAULT_CONFIG = {
|
|
271
|
+
quality: {
|
|
272
|
+
typecheck: "pnpm -s tsc --noEmit",
|
|
273
|
+
lint: "pnpm -s lint",
|
|
274
|
+
lintFix: "pnpm lint:fix",
|
|
275
|
+
formatFix: "pnpm format:fix",
|
|
276
|
+
testUnit: "pnpm -s test"
|
|
277
|
+
},
|
|
278
|
+
git: {
|
|
279
|
+
defaultBranch: "dev"
|
|
280
|
+
},
|
|
281
|
+
github: {
|
|
282
|
+
owner: "",
|
|
283
|
+
repo: ""
|
|
284
|
+
},
|
|
285
|
+
agent: {
|
|
286
|
+
modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
|
|
287
|
+
},
|
|
288
|
+
contextTiers: {
|
|
289
|
+
enabled: true,
|
|
290
|
+
tokenBudget: 8e3
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
LITELLM_DEFAULT_PORT = 4e3;
|
|
294
|
+
LITELLM_DEFAULT_URL = `http://localhost:${LITELLM_DEFAULT_PORT}`;
|
|
295
|
+
VERIFY_COMMAND_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
296
|
+
FIX_COMMAND_TIMEOUT_MS = 2 * 60 * 1e3;
|
|
297
|
+
_config = null;
|
|
298
|
+
_configDir = null;
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
254
302
|
// src/agent-runner.ts
|
|
255
303
|
import { spawn, execFileSync as execFileSync6 } from "child_process";
|
|
256
304
|
function writeStdin(child, prompt) {
|
|
257
|
-
return new Promise((
|
|
305
|
+
return new Promise((resolve5, reject) => {
|
|
258
306
|
if (!child.stdin) {
|
|
259
|
-
|
|
307
|
+
resolve5();
|
|
260
308
|
return;
|
|
261
309
|
}
|
|
262
310
|
child.stdin.write(prompt, (err) => {
|
|
263
311
|
if (err) reject(err);
|
|
264
312
|
else {
|
|
265
313
|
child.stdin.end();
|
|
266
|
-
|
|
314
|
+
resolve5();
|
|
267
315
|
}
|
|
268
316
|
});
|
|
269
317
|
});
|
|
270
318
|
}
|
|
271
319
|
function waitForProcess(child, timeout) {
|
|
272
|
-
return new Promise((
|
|
320
|
+
return new Promise((resolve5) => {
|
|
273
321
|
const stdoutChunks = [];
|
|
274
322
|
const stderrChunks = [];
|
|
275
323
|
child.stdout?.on("data", (chunk) => stdoutChunks.push(chunk));
|
|
@@ -282,7 +330,7 @@ function waitForProcess(child, timeout) {
|
|
|
282
330
|
}, timeout);
|
|
283
331
|
child.on("exit", (code) => {
|
|
284
332
|
clearTimeout(timer);
|
|
285
|
-
|
|
333
|
+
resolve5({
|
|
286
334
|
code,
|
|
287
335
|
stdout: Buffer.concat(stdoutChunks).toString(),
|
|
288
336
|
stderr: Buffer.concat(stderrChunks).toString()
|
|
@@ -290,7 +338,7 @@ function waitForProcess(child, timeout) {
|
|
|
290
338
|
});
|
|
291
339
|
child.on("error", (err) => {
|
|
292
340
|
clearTimeout(timer);
|
|
293
|
-
|
|
341
|
+
resolve5({ code: -1, stdout: "", stderr: err.message });
|
|
294
342
|
});
|
|
295
343
|
});
|
|
296
344
|
}
|
|
@@ -339,12 +387,12 @@ function createClaudeCodeRunner() {
|
|
|
339
387
|
"--print",
|
|
340
388
|
"--model",
|
|
341
389
|
model,
|
|
342
|
-
"--dangerously-skip-permissions"
|
|
343
|
-
"--allowedTools",
|
|
344
|
-
"Bash,Edit,Read,Write,Glob,Grep"
|
|
390
|
+
"--dangerously-skip-permissions"
|
|
345
391
|
];
|
|
346
392
|
if (options?.mcpConfigJson) {
|
|
347
393
|
args2.push("--mcp-config", options.mcpConfigJson);
|
|
394
|
+
} else {
|
|
395
|
+
args2.push("--allowedTools", "Bash,Edit,Read,Write,Glob,Grep");
|
|
348
396
|
}
|
|
349
397
|
if (options?.sessionId) {
|
|
350
398
|
if (options.resumeSession) {
|
|
@@ -386,923 +434,1607 @@ var init_agent_runner = __esm({
|
|
|
386
434
|
}
|
|
387
435
|
});
|
|
388
436
|
|
|
389
|
-
// src/
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
437
|
+
// src/mcp-config.ts
|
|
438
|
+
function withPlaywrightIfNeeded(mcpConfig, hasUI) {
|
|
439
|
+
if (!mcpConfig?.enabled || !hasUI) return mcpConfig;
|
|
440
|
+
const hasPlaywright = Object.keys(mcpConfig.servers).some(
|
|
441
|
+
(name) => name.toLowerCase().includes("playwright")
|
|
442
|
+
);
|
|
443
|
+
if (hasPlaywright) return mcpConfig;
|
|
444
|
+
return {
|
|
445
|
+
...mcpConfig,
|
|
446
|
+
servers: {
|
|
447
|
+
...mcpConfig.servers,
|
|
448
|
+
playwright: PLAYWRIGHT_SERVER
|
|
449
|
+
}
|
|
450
|
+
};
|
|
398
451
|
}
|
|
399
|
-
function
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
452
|
+
function buildMcpConfigJson(mcpConfig) {
|
|
453
|
+
if (!mcpConfig?.enabled) return void 0;
|
|
454
|
+
if (Object.keys(mcpConfig.servers).length === 0) return void 0;
|
|
455
|
+
const config = { mcpServers: {} };
|
|
456
|
+
const mcpServers = config.mcpServers;
|
|
457
|
+
for (const [name, server] of Object.entries(mcpConfig.servers)) {
|
|
458
|
+
mcpServers[name] = {
|
|
459
|
+
command: server.command,
|
|
460
|
+
args: server.args ?? [],
|
|
461
|
+
...server.env ? { env: server.env } : {}
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
return JSON.stringify(config);
|
|
465
|
+
}
|
|
466
|
+
function resolveMcpEnvVars(servers) {
|
|
467
|
+
const resolved = {};
|
|
468
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
469
|
+
const env = {};
|
|
470
|
+
for (const [k, v] of Object.entries(server.env ?? {})) {
|
|
471
|
+
env[k] = v.replace(/\$\{(\w+)\}/g, (_, varName) => {
|
|
472
|
+
const val = process.env[varName];
|
|
473
|
+
if (!val) {
|
|
474
|
+
throw new Error(
|
|
475
|
+
`MCP env var \${${varName}} is not set (required by MCP server '${name}'). Add it as a GitHub secret and it will be forwarded automatically.`
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
return val;
|
|
479
|
+
});
|
|
403
480
|
}
|
|
481
|
+
resolved[name] = { ...server, ...Object.keys(env).length > 0 ? { env } : {} };
|
|
404
482
|
}
|
|
483
|
+
return resolved;
|
|
405
484
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
485
|
+
function buildTaskifyMcpConfigJson(config) {
|
|
486
|
+
const servers = config.mcp?.servers ?? {};
|
|
487
|
+
if (Object.keys(servers).length === 0) {
|
|
488
|
+
throw new Error(
|
|
489
|
+
"kody taskify requires at least one MCP server configured in kody.config.json under mcp.servers. Add your task management tool's MCP server there."
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
const resolvedServers = resolveMcpEnvVars(servers);
|
|
493
|
+
const mcpServers = {};
|
|
494
|
+
for (const [name, server] of Object.entries(resolvedServers)) {
|
|
495
|
+
mcpServers[name] = {
|
|
496
|
+
command: server.command,
|
|
497
|
+
args: server.args ?? [],
|
|
498
|
+
...server.env ? { env: server.env } : {}
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
return JSON.stringify({ mcpServers });
|
|
502
|
+
}
|
|
503
|
+
function isMcpEnabledForStage(stageName, mcpConfig) {
|
|
504
|
+
if (!mcpConfig?.enabled) return false;
|
|
505
|
+
const allowedStages = mcpConfig.stages ?? DEFAULT_MCP_STAGES;
|
|
506
|
+
return allowedStages.includes(stageName);
|
|
507
|
+
}
|
|
508
|
+
var DEFAULT_MCP_STAGES, PLAYWRIGHT_SERVER;
|
|
509
|
+
var init_mcp_config = __esm({
|
|
510
|
+
"src/mcp-config.ts"() {
|
|
409
511
|
"use strict";
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
timeout: 6e5,
|
|
416
|
-
maxRetries: 1,
|
|
417
|
-
outputFile: "task.json"
|
|
418
|
-
},
|
|
419
|
-
{
|
|
420
|
-
name: "plan",
|
|
421
|
-
type: "agent",
|
|
422
|
-
modelTier: "strong",
|
|
423
|
-
timeout: 6e5,
|
|
424
|
-
maxRetries: 1,
|
|
425
|
-
outputFile: "plan.md"
|
|
426
|
-
},
|
|
427
|
-
{
|
|
428
|
-
name: "build",
|
|
429
|
-
type: "agent",
|
|
430
|
-
modelTier: "mid",
|
|
431
|
-
timeout: 24e5,
|
|
432
|
-
maxRetries: 1
|
|
433
|
-
},
|
|
434
|
-
{
|
|
435
|
-
name: "verify",
|
|
436
|
-
type: "gate",
|
|
437
|
-
modelTier: "cheap",
|
|
438
|
-
timeout: 3e5,
|
|
439
|
-
maxRetries: 2,
|
|
440
|
-
retryWithAgent: "autofix"
|
|
441
|
-
},
|
|
442
|
-
{
|
|
443
|
-
name: "review",
|
|
444
|
-
type: "agent",
|
|
445
|
-
modelTier: "strong",
|
|
446
|
-
timeout: 6e5,
|
|
447
|
-
maxRetries: 1,
|
|
448
|
-
outputFile: "review.md"
|
|
449
|
-
},
|
|
450
|
-
{
|
|
451
|
-
name: "review-fix",
|
|
452
|
-
type: "agent",
|
|
453
|
-
modelTier: "mid",
|
|
454
|
-
timeout: 12e5,
|
|
455
|
-
maxRetries: 1
|
|
456
|
-
},
|
|
457
|
-
{
|
|
458
|
-
name: "ship",
|
|
459
|
-
type: "deterministic",
|
|
460
|
-
modelTier: "cheap",
|
|
461
|
-
timeout: 24e4,
|
|
462
|
-
maxRetries: 1,
|
|
463
|
-
outputFile: "ship.md"
|
|
464
|
-
}
|
|
465
|
-
];
|
|
512
|
+
DEFAULT_MCP_STAGES = ["build", "verify", "review", "review-fix"];
|
|
513
|
+
PLAYWRIGHT_SERVER = {
|
|
514
|
+
command: "npx",
|
|
515
|
+
args: ["-y", "@anthropic-ai/mcp-playwright"]
|
|
516
|
+
};
|
|
466
517
|
}
|
|
467
518
|
});
|
|
468
519
|
|
|
469
|
-
// src/
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
return
|
|
473
|
-
}
|
|
474
|
-
function timestamp() {
|
|
475
|
-
return (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
|
|
520
|
+
// src/github-api.ts
|
|
521
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
522
|
+
function isGhExecError(err) {
|
|
523
|
+
return typeof err === "object" && err !== null;
|
|
476
524
|
}
|
|
477
|
-
function
|
|
478
|
-
if (
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
console.error(`${prefix} ${msg}`);
|
|
482
|
-
} else if (level === "warn") {
|
|
483
|
-
console.warn(`${prefix} ${msg}`);
|
|
484
|
-
} else {
|
|
485
|
-
console.log(`${prefix} ${msg}`);
|
|
525
|
+
function ghErrorMessage(err) {
|
|
526
|
+
if (isGhExecError(err)) {
|
|
527
|
+
const stderr = err.stderr?.toString().trim();
|
|
528
|
+
if (stderr) return stderr;
|
|
486
529
|
}
|
|
530
|
+
return err instanceof Error ? err.message : String(err);
|
|
487
531
|
}
|
|
488
|
-
function
|
|
489
|
-
|
|
490
|
-
|
|
532
|
+
function isNotFoundError(err) {
|
|
533
|
+
const msg = ghErrorMessage(err).toLowerCase();
|
|
534
|
+
return msg.includes("not found") || msg.includes("no pull requests") || msg.includes("could not resolve");
|
|
491
535
|
}
|
|
492
|
-
function
|
|
493
|
-
|
|
494
|
-
`);
|
|
536
|
+
function setGhCwd(cwd) {
|
|
537
|
+
_ghCwd = cwd;
|
|
495
538
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
"src/logger.ts"() {
|
|
499
|
-
"use strict";
|
|
500
|
-
isCI = !!process.env.GITHUB_ACTIONS;
|
|
501
|
-
LEVELS = {
|
|
502
|
-
debug: 0,
|
|
503
|
-
info: 1,
|
|
504
|
-
warn: 2,
|
|
505
|
-
error: 3
|
|
506
|
-
};
|
|
507
|
-
logger = {
|
|
508
|
-
debug: (msg) => log("debug", msg),
|
|
509
|
-
info: (msg) => log("info", msg),
|
|
510
|
-
warn: (msg) => log("warn", msg),
|
|
511
|
-
error: (msg) => log("error", msg)
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
// src/validators.ts
|
|
517
|
-
function stripFences(content) {
|
|
518
|
-
return content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
539
|
+
function ghToken() {
|
|
540
|
+
return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
|
|
519
541
|
}
|
|
520
|
-
function
|
|
521
|
-
|
|
542
|
+
function gh(args2, options) {
|
|
543
|
+
const token = ghToken();
|
|
544
|
+
const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
|
|
545
|
+
return execFileSync7("gh", args2, {
|
|
546
|
+
encoding: "utf-8",
|
|
547
|
+
timeout: API_TIMEOUT_MS,
|
|
548
|
+
cwd: _ghCwd,
|
|
549
|
+
env,
|
|
550
|
+
input: options?.input,
|
|
551
|
+
stdio: options?.input ? ["pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
|
|
552
|
+
}).trim();
|
|
553
|
+
}
|
|
554
|
+
function getIssue(issueNumber) {
|
|
522
555
|
try {
|
|
523
|
-
|
|
556
|
+
const output = gh([
|
|
557
|
+
"issue",
|
|
558
|
+
"view",
|
|
559
|
+
String(issueNumber),
|
|
560
|
+
"--json",
|
|
561
|
+
"body,title"
|
|
562
|
+
]);
|
|
563
|
+
const parsed = JSON.parse(output);
|
|
564
|
+
if (!parsed || typeof parsed.title !== "string") {
|
|
565
|
+
logger.warn(` Issue #${issueNumber}: unexpected response shape`);
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
return { body: parsed.body ?? "", title: parsed.title };
|
|
524
569
|
} catch (err) {
|
|
525
|
-
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}
|
|
530
|
-
if (requiredFields) {
|
|
531
|
-
for (const field of requiredFields) {
|
|
532
|
-
if (!(field in parsed)) {
|
|
533
|
-
return { ok: false, error: `Missing required field: ${field}` };
|
|
534
|
-
}
|
|
570
|
+
if (isNotFoundError(err)) {
|
|
571
|
+
logger.info(` Issue #${issueNumber} not found`);
|
|
572
|
+
} else {
|
|
573
|
+
logger.error(` Failed to get issue #${issueNumber}: ${ghErrorMessage(err)}`);
|
|
535
574
|
}
|
|
575
|
+
return null;
|
|
536
576
|
}
|
|
537
|
-
return { ok: true, data: parsed };
|
|
538
577
|
}
|
|
539
|
-
function
|
|
578
|
+
function getIssueComments(issueNumber) {
|
|
540
579
|
try {
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
return
|
|
548
|
-
} catch
|
|
549
|
-
return
|
|
550
|
-
valid: false,
|
|
551
|
-
error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
552
|
-
};
|
|
580
|
+
const output = gh([
|
|
581
|
+
"api",
|
|
582
|
+
`repos/{owner}/{repo}/issues/${issueNumber}/comments`,
|
|
583
|
+
"--jq",
|
|
584
|
+
"[.[] | {body, created_at}]"
|
|
585
|
+
]);
|
|
586
|
+
return output ? JSON.parse(output) : [];
|
|
587
|
+
} catch {
|
|
588
|
+
return [];
|
|
553
589
|
}
|
|
554
590
|
}
|
|
555
|
-
function
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
return
|
|
591
|
+
function getIssueLabels(issueNumber) {
|
|
592
|
+
try {
|
|
593
|
+
const output = gh(["issue", "view", String(issueNumber), "--json", "labels", "--jq", ".labels[].name"]);
|
|
594
|
+
return output.split("\n").filter(Boolean);
|
|
595
|
+
} catch {
|
|
596
|
+
return [];
|
|
561
597
|
}
|
|
562
|
-
return { valid: true };
|
|
563
598
|
}
|
|
564
|
-
function
|
|
565
|
-
|
|
566
|
-
|
|
599
|
+
function setLabel(issueNumber, label) {
|
|
600
|
+
try {
|
|
601
|
+
gh(["issue", "edit", String(issueNumber), "--add-label", label]);
|
|
602
|
+
logger.info(` Label added: ${label}`);
|
|
603
|
+
} catch (err) {
|
|
604
|
+
logger.warn(` Failed to set label ${label}: ${err}`);
|
|
567
605
|
}
|
|
568
|
-
return { valid: false, error: "Review must contain 'pass' or 'fail'" };
|
|
569
606
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
"risk_level"
|
|
580
|
-
];
|
|
607
|
+
function postComment(issueNumber, body) {
|
|
608
|
+
try {
|
|
609
|
+
gh(
|
|
610
|
+
["issue", "comment", String(issueNumber), "--body-file", "-"],
|
|
611
|
+
{ input: body }
|
|
612
|
+
);
|
|
613
|
+
logger.info(` Comment posted on #${issueNumber}`);
|
|
614
|
+
} catch (err) {
|
|
615
|
+
logger.warn(` Failed to post comment: ${err}`);
|
|
581
616
|
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
617
|
+
}
|
|
618
|
+
function getPRForBranch(branch) {
|
|
619
|
+
try {
|
|
620
|
+
const output = gh([
|
|
621
|
+
"pr",
|
|
622
|
+
"view",
|
|
623
|
+
branch,
|
|
624
|
+
"--json",
|
|
625
|
+
"number,url"
|
|
626
|
+
]);
|
|
627
|
+
const data = JSON.parse(output);
|
|
628
|
+
if (typeof data.number !== "number" || typeof data.url !== "string") {
|
|
629
|
+
logger.warn(` PR for branch ${branch}: unexpected response shape`);
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
return { number: data.number, url: data.url };
|
|
633
|
+
} catch (err) {
|
|
634
|
+
if (!isNotFoundError(err)) {
|
|
635
|
+
logger.warn(` Failed to check PR for branch ${branch}: ${ghErrorMessage(err)}`);
|
|
636
|
+
}
|
|
637
|
+
return null;
|
|
594
638
|
}
|
|
595
|
-
return {
|
|
596
|
-
provider: config.agent.provider ?? "claude",
|
|
597
|
-
model
|
|
598
|
-
};
|
|
599
639
|
}
|
|
600
|
-
function
|
|
601
|
-
|
|
640
|
+
function updatePR(prNumber, body) {
|
|
641
|
+
try {
|
|
642
|
+
gh(
|
|
643
|
+
["pr", "edit", String(prNumber), "--body-file", "-"],
|
|
644
|
+
{ input: body }
|
|
645
|
+
);
|
|
646
|
+
logger.info(` PR #${prNumber} body updated`);
|
|
647
|
+
} catch (err) {
|
|
648
|
+
logger.warn(` Failed to update PR #${prNumber}: ${err}`);
|
|
649
|
+
}
|
|
602
650
|
}
|
|
603
|
-
function
|
|
604
|
-
|
|
651
|
+
function createPR(head, base, title, body) {
|
|
652
|
+
try {
|
|
653
|
+
const output = gh(
|
|
654
|
+
[
|
|
655
|
+
"pr",
|
|
656
|
+
"create",
|
|
657
|
+
"--head",
|
|
658
|
+
head,
|
|
659
|
+
"--base",
|
|
660
|
+
base,
|
|
661
|
+
"--title",
|
|
662
|
+
title,
|
|
663
|
+
"--body-file",
|
|
664
|
+
"-"
|
|
665
|
+
],
|
|
666
|
+
{ input: body }
|
|
667
|
+
);
|
|
668
|
+
const url = output.trim();
|
|
669
|
+
const match = url.match(/\/pull\/(\d+)$/);
|
|
670
|
+
const number = match ? parseInt(match[1], 10) : 0;
|
|
671
|
+
logger.info(` PR created: ${url}`);
|
|
672
|
+
return { number, url };
|
|
673
|
+
} catch (err) {
|
|
674
|
+
const reason = ghErrorMessage(err);
|
|
675
|
+
logger.error(` Failed to create PR: ${reason}`);
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
605
678
|
}
|
|
606
|
-
function
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
679
|
+
function createIssue(title, body, labels) {
|
|
680
|
+
try {
|
|
681
|
+
const args2 = ["issue", "create", "--title", title, "--body-file", "-"];
|
|
682
|
+
if (labels && labels.length > 0) {
|
|
683
|
+
args2.push("--label", labels.join(","));
|
|
610
684
|
}
|
|
685
|
+
const output = gh(args2, { input: body });
|
|
686
|
+
const url = output.trim();
|
|
687
|
+
const match = url.match(/\/issues\/(\d+)$/);
|
|
688
|
+
const number = match ? parseInt(match[1], 10) : 0;
|
|
689
|
+
logger.info(` Issue created: ${url}`);
|
|
690
|
+
return { number, url };
|
|
691
|
+
} catch (err) {
|
|
692
|
+
const reason = ghErrorMessage(err);
|
|
693
|
+
logger.error(` Failed to create issue: ${reason}`);
|
|
694
|
+
return null;
|
|
611
695
|
}
|
|
612
|
-
if (config.agent.default && stageNeedsProxy(config.agent.default)) return true;
|
|
613
|
-
return needsLitellmProxy(config);
|
|
614
|
-
}
|
|
615
|
-
function getLitellmUrl() {
|
|
616
|
-
return LITELLM_DEFAULT_URL;
|
|
617
696
|
}
|
|
618
|
-
function
|
|
619
|
-
if (
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
}
|
|
626
|
-
function getProjectConfig() {
|
|
627
|
-
if (_config) return _config;
|
|
628
|
-
const configPath = path7.join(_configDir ?? process.cwd(), "kody.config.json");
|
|
629
|
-
if (fs9.existsSync(configPath)) {
|
|
697
|
+
function setLifecycleLabel(issueNumber, phase) {
|
|
698
|
+
if (!LIFECYCLE_LABELS.includes(phase)) {
|
|
699
|
+
logger.warn(` Invalid lifecycle phase: ${phase}`);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
const othersToRemove = LIFECYCLE_LABELS.filter((l) => l !== phase).map((l) => `kody:${l}`).join(",");
|
|
703
|
+
if (othersToRemove) {
|
|
630
704
|
try {
|
|
631
|
-
|
|
632
|
-
if (!result.ok) {
|
|
633
|
-
logger.warn(`kody.config.json: ${result.error} \u2014 using defaults`);
|
|
634
|
-
_config = { ...DEFAULT_CONFIG };
|
|
635
|
-
return _config;
|
|
636
|
-
}
|
|
637
|
-
const raw = result.data;
|
|
638
|
-
_config = {
|
|
639
|
-
quality: { ...DEFAULT_CONFIG.quality, ...raw.quality },
|
|
640
|
-
git: { ...DEFAULT_CONFIG.git, ...raw.git },
|
|
641
|
-
github: { ...DEFAULT_CONFIG.github, ...raw.github },
|
|
642
|
-
agent: {
|
|
643
|
-
...DEFAULT_CONFIG.agent,
|
|
644
|
-
...raw.agent,
|
|
645
|
-
modelMap: { ...DEFAULT_CONFIG.agent.modelMap, ...raw.agent?.modelMap }
|
|
646
|
-
},
|
|
647
|
-
timeouts: raw.timeouts ?? void 0,
|
|
648
|
-
contextTiers: raw.contextTiers ? { ...DEFAULT_CONFIG.contextTiers, ...raw.contextTiers } : DEFAULT_CONFIG.contextTiers,
|
|
649
|
-
mcp: raw.mcp ? {
|
|
650
|
-
servers: {},
|
|
651
|
-
stages: ["build", "verify", "review", "review-fix"],
|
|
652
|
-
...raw.mcp,
|
|
653
|
-
// Auto-enable when devServer is configured (user can still set enabled: false to override)
|
|
654
|
-
enabled: raw.mcp.enabled ?? !!raw.mcp.devServer
|
|
655
|
-
} : void 0
|
|
656
|
-
};
|
|
705
|
+
gh(["issue", "edit", String(issueNumber), "--remove-label", othersToRemove]);
|
|
657
706
|
} catch {
|
|
658
|
-
logger.warn("kody.config.json is invalid JSON \u2014 using defaults");
|
|
659
|
-
_config = { ...DEFAULT_CONFIG };
|
|
660
707
|
}
|
|
661
|
-
} else {
|
|
662
|
-
_config = { ...DEFAULT_CONFIG };
|
|
663
708
|
}
|
|
664
|
-
|
|
709
|
+
setLabel(issueNumber, `kody:${phase}`);
|
|
665
710
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
tokenBudget: 8e3
|
|
711
|
+
function getPRsForIssue(issueNumber) {
|
|
712
|
+
try {
|
|
713
|
+
const output = gh([
|
|
714
|
+
"pr",
|
|
715
|
+
"list",
|
|
716
|
+
"--search",
|
|
717
|
+
`${issueNumber} in:body`,
|
|
718
|
+
"--json",
|
|
719
|
+
"number,title,url,headRefName",
|
|
720
|
+
"--state",
|
|
721
|
+
"open"
|
|
722
|
+
]);
|
|
723
|
+
const prs = JSON.parse(output);
|
|
724
|
+
const branchPrs = (() => {
|
|
725
|
+
try {
|
|
726
|
+
const branchOutput = gh([
|
|
727
|
+
"pr",
|
|
728
|
+
"list",
|
|
729
|
+
"--json",
|
|
730
|
+
"number,title,url,headRefName",
|
|
731
|
+
"--state",
|
|
732
|
+
"open"
|
|
733
|
+
]);
|
|
734
|
+
return JSON.parse(branchOutput).filter((pr) => pr.headRefName.startsWith(`${issueNumber}-`));
|
|
735
|
+
} catch {
|
|
736
|
+
return [];
|
|
693
737
|
}
|
|
694
|
-
};
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
if (!_hookSafeEnv) {
|
|
708
|
-
_hookSafeEnv = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
|
|
738
|
+
})();
|
|
739
|
+
const seen = /* @__PURE__ */ new Set();
|
|
740
|
+
const merged = [];
|
|
741
|
+
for (const pr of [...prs, ...branchPrs]) {
|
|
742
|
+
if (!seen.has(pr.number)) {
|
|
743
|
+
seen.add(pr.number);
|
|
744
|
+
merged.push({ number: pr.number, title: pr.title, url: pr.url, headBranch: pr.headRefName });
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return merged;
|
|
748
|
+
} catch (err) {
|
|
749
|
+
logger.error(` Failed to get PRs for issue #${issueNumber}: ${err}`);
|
|
750
|
+
return [];
|
|
709
751
|
}
|
|
710
|
-
return _hookSafeEnv;
|
|
711
|
-
}
|
|
712
|
-
function git(args2, options) {
|
|
713
|
-
return execFileSync7("git", args2, {
|
|
714
|
-
encoding: "utf-8",
|
|
715
|
-
timeout: options?.timeout ?? 3e4,
|
|
716
|
-
cwd: options?.cwd,
|
|
717
|
-
env: options?.env ?? getHookSafeEnv(),
|
|
718
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
719
|
-
}).trim();
|
|
720
|
-
}
|
|
721
|
-
function deriveBranchName(issueNumber, title) {
|
|
722
|
-
const slug = title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").slice(0, 50).replace(/-$/, "");
|
|
723
|
-
return `${issueNumber}-${slug}`;
|
|
724
752
|
}
|
|
725
|
-
function
|
|
753
|
+
function getPRDetails(prNumber) {
|
|
726
754
|
try {
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
755
|
+
const output = gh([
|
|
756
|
+
"pr",
|
|
757
|
+
"view",
|
|
758
|
+
String(prNumber),
|
|
759
|
+
"--json",
|
|
760
|
+
"title,body,headRefName,baseRefName"
|
|
761
|
+
]);
|
|
762
|
+
const data = JSON.parse(output);
|
|
763
|
+
if (typeof data.title !== "string" || typeof data.headRefName !== "string") {
|
|
764
|
+
logger.warn(` PR #${prNumber}: unexpected response shape`);
|
|
765
|
+
return null;
|
|
730
766
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
} catch {
|
|
743
|
-
}
|
|
744
|
-
return "dev";
|
|
745
|
-
}
|
|
746
|
-
function getCurrentBranch(cwd) {
|
|
747
|
-
return git(["branch", "--show-current"], { cwd });
|
|
748
|
-
}
|
|
749
|
-
function ensureFeatureBranch(issueNumber, title, cwd) {
|
|
750
|
-
const current = getCurrentBranch(cwd);
|
|
751
|
-
const branchName = deriveBranchName(issueNumber, title);
|
|
752
|
-
if (current === branchName || current.startsWith(`${issueNumber}-`)) {
|
|
753
|
-
logger.info(` Already on feature branch: ${current}`);
|
|
754
|
-
return current;
|
|
755
|
-
}
|
|
756
|
-
if (!BASE_BRANCHES.includes(current) && current !== "") {
|
|
757
|
-
const defaultBranch2 = getDefaultBranch(cwd);
|
|
758
|
-
logger.info(` Switching from ${current} to ${defaultBranch2} before creating ${branchName}`);
|
|
759
|
-
try {
|
|
760
|
-
git(["checkout", defaultBranch2], { cwd });
|
|
761
|
-
} catch {
|
|
762
|
-
logger.warn(` Failed to checkout ${defaultBranch2}, aborting branch creation`);
|
|
763
|
-
return current;
|
|
767
|
+
return {
|
|
768
|
+
title: data.title,
|
|
769
|
+
body: data.body ?? "",
|
|
770
|
+
headBranch: data.headRefName,
|
|
771
|
+
baseBranch: data.baseRefName ?? "main"
|
|
772
|
+
};
|
|
773
|
+
} catch (err) {
|
|
774
|
+
if (isNotFoundError(err)) {
|
|
775
|
+
logger.info(` PR #${prNumber} not found`);
|
|
776
|
+
} else {
|
|
777
|
+
logger.error(` Failed to get PR #${prNumber}: ${ghErrorMessage(err)}`);
|
|
764
778
|
}
|
|
779
|
+
return null;
|
|
765
780
|
}
|
|
781
|
+
}
|
|
782
|
+
function postPRComment(prNumber, body) {
|
|
766
783
|
try {
|
|
767
|
-
|
|
784
|
+
gh(
|
|
785
|
+
["pr", "comment", String(prNumber), "--body-file", "-"],
|
|
786
|
+
{ input: body }
|
|
787
|
+
);
|
|
788
|
+
logger.info(` Comment posted on PR #${prNumber}`);
|
|
768
789
|
} catch (err) {
|
|
769
|
-
|
|
770
|
-
logger.warn(` Failed to fetch origin: ${msg}`);
|
|
790
|
+
logger.warn(` Failed to post PR comment: ${err}`);
|
|
771
791
|
}
|
|
792
|
+
}
|
|
793
|
+
function submitPRReview(prNumber, body, event) {
|
|
794
|
+
const flag = event === "approve" ? "--approve" : "--request-changes";
|
|
772
795
|
try {
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
796
|
+
gh(
|
|
797
|
+
["pr", "review", String(prNumber), flag, "--body-file", "-"],
|
|
798
|
+
{ input: body }
|
|
799
|
+
);
|
|
800
|
+
logger.info(` PR review submitted on #${prNumber}: ${event}`);
|
|
801
|
+
return true;
|
|
802
|
+
} catch (err) {
|
|
803
|
+
logger.warn(` Failed to submit PR review: ${err}`);
|
|
804
|
+
return false;
|
|
779
805
|
}
|
|
806
|
+
}
|
|
807
|
+
function getCIFailureLogs(runId, maxLength = 8e3) {
|
|
780
808
|
try {
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
809
|
+
const logsOutput = gh([
|
|
810
|
+
"run",
|
|
811
|
+
"view",
|
|
812
|
+
String(runId),
|
|
813
|
+
"--log-failed"
|
|
814
|
+
]);
|
|
815
|
+
if (!logsOutput) return null;
|
|
816
|
+
const truncated = logsOutput.slice(-maxLength);
|
|
817
|
+
const prefix = logsOutput.length > maxLength ? "...(earlier output truncated)\n" : "";
|
|
818
|
+
return `${prefix}${truncated}`;
|
|
819
|
+
} catch (err) {
|
|
820
|
+
logger.warn(` Failed to get CI failure logs for run ${runId}: ${ghErrorMessage(err)}`);
|
|
821
|
+
return null;
|
|
786
822
|
}
|
|
787
|
-
|
|
823
|
+
}
|
|
824
|
+
function getLatestFailedRunForBranch(branch) {
|
|
788
825
|
try {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
826
|
+
const output = gh([
|
|
827
|
+
"run",
|
|
828
|
+
"list",
|
|
829
|
+
"--branch",
|
|
830
|
+
branch,
|
|
831
|
+
"--status",
|
|
832
|
+
"failure",
|
|
833
|
+
"--limit",
|
|
834
|
+
"1",
|
|
835
|
+
"--json",
|
|
836
|
+
"databaseId",
|
|
837
|
+
"--jq",
|
|
838
|
+
".[0].databaseId"
|
|
839
|
+
]);
|
|
840
|
+
return output.trim() || null;
|
|
802
841
|
} catch (err) {
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
return;
|
|
806
|
-
}
|
|
807
|
-
try {
|
|
808
|
-
git(["merge", `origin/${defaultBranch}`, "--no-edit"], { cwd, timeout: 3e4 });
|
|
809
|
-
logger.info(` Synced with origin/${defaultBranch}`);
|
|
810
|
-
} catch {
|
|
811
|
-
try {
|
|
812
|
-
git(["merge", "--abort"], { cwd });
|
|
813
|
-
} catch (abortErr) {
|
|
814
|
-
logger.warn(` Failed to abort merge: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}`);
|
|
815
|
-
}
|
|
816
|
-
logger.warn(` Merge conflict with origin/${defaultBranch} \u2014 skipping sync`);
|
|
842
|
+
logger.warn(` Failed to get latest failed run for branch ${branch}: ${ghErrorMessage(err)}`);
|
|
843
|
+
return null;
|
|
817
844
|
}
|
|
818
845
|
}
|
|
819
|
-
function
|
|
820
|
-
const defaultBranch = getDefaultBranch(cwd);
|
|
821
|
-
const current = getCurrentBranch(cwd);
|
|
822
|
-
if (current === defaultBranch) return "clean";
|
|
846
|
+
function getLatestKodyReviewComment(prNumber) {
|
|
823
847
|
try {
|
|
824
|
-
|
|
848
|
+
const output = gh([
|
|
849
|
+
"api",
|
|
850
|
+
`repos/{owner}/{repo}/issues/${prNumber}/comments`,
|
|
851
|
+
"--jq",
|
|
852
|
+
'[.[] | select(.body | test("Kody Review"))] | last | .body'
|
|
853
|
+
]);
|
|
854
|
+
return output.trim() || null;
|
|
825
855
|
} catch (err) {
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
return "error";
|
|
856
|
+
logger.warn(` Failed to get review comments for PR #${prNumber}: ${err}`);
|
|
857
|
+
return null;
|
|
829
858
|
}
|
|
859
|
+
}
|
|
860
|
+
function getPRFeedbackSinceLastKodyAction(prNumber) {
|
|
830
861
|
try {
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
862
|
+
const issueCommentsRaw = gh([
|
|
863
|
+
"api",
|
|
864
|
+
`repos/{owner}/{repo}/issues/${prNumber}/comments`,
|
|
865
|
+
"--jq",
|
|
866
|
+
"[.[] | {body, created_at, user_login: .user.login, user_type: .user.type}]"
|
|
867
|
+
]);
|
|
868
|
+
const issueComments = issueCommentsRaw ? JSON.parse(issueCommentsRaw) : [];
|
|
869
|
+
const reviewCommentsRaw = gh([
|
|
870
|
+
"api",
|
|
871
|
+
`repos/{owner}/{repo}/pulls/${prNumber}/comments`,
|
|
872
|
+
"--jq",
|
|
873
|
+
"[.[] | {body, created_at, user_login: .user.login, user_type: .user.type, path, line}]"
|
|
874
|
+
]);
|
|
875
|
+
const reviewComments = reviewCommentsRaw ? JSON.parse(reviewCommentsRaw) : [];
|
|
876
|
+
const kodyTimestamp = findLastKodyActionTimestamp(issueComments);
|
|
877
|
+
const humanIssueComments = issueComments.filter(
|
|
878
|
+
(c) => !isKodyComment(c) && (!kodyTimestamp || c.created_at > kodyTimestamp)
|
|
879
|
+
);
|
|
880
|
+
const humanReviewComments = reviewComments.filter(
|
|
881
|
+
(c) => !isKodyComment(c) && (!kodyTimestamp || c.created_at > kodyTimestamp)
|
|
882
|
+
);
|
|
883
|
+
if (humanIssueComments.length === 0 && humanReviewComments.length === 0) {
|
|
884
|
+
return null;
|
|
839
885
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
886
|
+
const parts = [];
|
|
887
|
+
if (humanIssueComments.length > 0) {
|
|
888
|
+
parts.push("### PR Comments");
|
|
889
|
+
for (const c of humanIssueComments) {
|
|
890
|
+
parts.push(`**@${c.user_login}:**
|
|
891
|
+
${c.body}`);
|
|
892
|
+
}
|
|
844
893
|
}
|
|
845
|
-
|
|
894
|
+
if (humanReviewComments.length > 0) {
|
|
895
|
+
parts.push("### Code Review Comments");
|
|
896
|
+
for (const c of humanReviewComments) {
|
|
897
|
+
const location = c.path ? `\`${c.path}${c.line ? `:${c.line}` : ""}\`` : "";
|
|
898
|
+
parts.push(`**@${c.user_login}** ${location}:
|
|
899
|
+
${c.body}`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return parts.join("\n\n");
|
|
903
|
+
} catch (err) {
|
|
904
|
+
logger.warn(` Failed to get PR feedback for #${prNumber}: ${err}`);
|
|
905
|
+
return null;
|
|
846
906
|
}
|
|
847
907
|
}
|
|
848
|
-
function
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
return output ? output.split("\n") : [];
|
|
852
|
-
} catch {
|
|
853
|
-
return [];
|
|
854
|
-
}
|
|
908
|
+
function isKodyComment(comment) {
|
|
909
|
+
if (comment.user_type === "Bot") return true;
|
|
910
|
+
return KODY_MARKERS.some((marker) => comment.body.includes(marker));
|
|
855
911
|
}
|
|
856
|
-
function
|
|
857
|
-
const
|
|
858
|
-
if (
|
|
859
|
-
|
|
860
|
-
}
|
|
861
|
-
git(["add", "."], { cwd });
|
|
862
|
-
git(["commit", "--no-gpg-sign", "-m", message], { cwd });
|
|
863
|
-
const hash = git(["rev-parse", "HEAD"], { cwd }).slice(0, 7);
|
|
864
|
-
logger.info(` Committed: ${hash} ${message}`);
|
|
865
|
-
return { success: true, hash, message };
|
|
912
|
+
function findLastKodyActionTimestamp(comments) {
|
|
913
|
+
const kodyComments = comments.filter(isKodyComment);
|
|
914
|
+
if (kodyComments.length === 0) return null;
|
|
915
|
+
return kodyComments[kodyComments.length - 1].created_at;
|
|
866
916
|
}
|
|
867
|
-
|
|
917
|
+
var API_TIMEOUT_MS, LIFECYCLE_LABELS, _ghCwd, KODY_MARKERS;
|
|
918
|
+
var init_github_api = __esm({
|
|
919
|
+
"src/github-api.ts"() {
|
|
920
|
+
"use strict";
|
|
921
|
+
init_logger();
|
|
922
|
+
API_TIMEOUT_MS = 3e4;
|
|
923
|
+
LIFECYCLE_LABELS = ["planning", "building", "review", "shipping", "done", "failed", "waiting", "low", "medium", "high"];
|
|
924
|
+
KODY_MARKERS = [
|
|
925
|
+
"Kody Review",
|
|
926
|
+
"\u{1F916} Generated by Kody",
|
|
927
|
+
"Kody pipeline started",
|
|
928
|
+
"Fix pushed to PR",
|
|
929
|
+
"PR created:",
|
|
930
|
+
"Pipeline failed at",
|
|
931
|
+
"Pipeline already running",
|
|
932
|
+
"already completed"
|
|
933
|
+
];
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
// src/cli/task-resolution.ts
|
|
938
|
+
import * as fs9 from "fs";
|
|
939
|
+
import * as path8 from "path";
|
|
940
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
941
|
+
function findLatestTaskForIssue(issueNumber, projectDir) {
|
|
942
|
+
const tasksDir = path8.join(projectDir, ".kody", "tasks");
|
|
943
|
+
if (!fs9.existsSync(tasksDir)) return null;
|
|
944
|
+
const allDirs = fs9.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
|
|
945
|
+
const prefix = `${issueNumber}-`;
|
|
946
|
+
const direct = allDirs.find((d) => d.startsWith(prefix));
|
|
947
|
+
if (direct) return direct;
|
|
868
948
|
try {
|
|
869
|
-
const
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
949
|
+
const branch = execFileSync8("git", ["branch", "--show-current"], {
|
|
950
|
+
encoding: "utf-8",
|
|
951
|
+
cwd: projectDir,
|
|
952
|
+
timeout: 5e3,
|
|
953
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
954
|
+
}).trim();
|
|
955
|
+
const branchIssueMatch = branch.match(/^(\d+)-/);
|
|
956
|
+
if (branchIssueMatch) {
|
|
957
|
+
const branchIssueNum = branchIssueMatch[1];
|
|
958
|
+
const branchPrefix = `${branchIssueNum}-`;
|
|
959
|
+
const fromBranch = allDirs.find((d) => d.startsWith(branchPrefix));
|
|
960
|
+
if (fromBranch) return fromBranch;
|
|
961
|
+
}
|
|
962
|
+
} catch {
|
|
876
963
|
}
|
|
964
|
+
return null;
|
|
877
965
|
}
|
|
878
|
-
function
|
|
966
|
+
function generateTaskId() {
|
|
967
|
+
const now = /* @__PURE__ */ new Date();
|
|
968
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
969
|
+
return `${String(now.getFullYear()).slice(2)}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
970
|
+
}
|
|
971
|
+
function resolveTaskIdFromComments(issueNumber) {
|
|
879
972
|
try {
|
|
880
|
-
|
|
973
|
+
const comments = getIssueComments(issueNumber);
|
|
974
|
+
const pattern = /pipeline started: `([^`]+)`/;
|
|
975
|
+
let latestTaskId = null;
|
|
976
|
+
for (const comment of comments) {
|
|
977
|
+
const match = comment.body.match(pattern);
|
|
978
|
+
if (match) {
|
|
979
|
+
latestTaskId = match[1];
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
return latestTaskId;
|
|
881
983
|
} catch {
|
|
882
|
-
|
|
883
|
-
git(["push", "--force-with-lease", "-u", "origin", "HEAD"], { cwd, timeout: 12e4 });
|
|
984
|
+
return null;
|
|
884
985
|
}
|
|
885
|
-
logger.info(" Pushed to origin");
|
|
886
986
|
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
987
|
+
function resolveTaskIdForCommand(issueNumber, projectDir) {
|
|
988
|
+
const fromTasks = findLatestTaskForIssue(issueNumber, projectDir);
|
|
989
|
+
if (fromTasks) return fromTasks;
|
|
990
|
+
const fromComments = resolveTaskIdFromComments(issueNumber);
|
|
991
|
+
if (fromComments) return fromComments;
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
var init_task_resolution = __esm({
|
|
995
|
+
"src/cli/task-resolution.ts"() {
|
|
890
996
|
"use strict";
|
|
891
|
-
|
|
892
|
-
init_config();
|
|
893
|
-
BASE_BRANCHES = ["dev", "main", "master"];
|
|
894
|
-
_hookSafeEnv = null;
|
|
997
|
+
init_github_api();
|
|
895
998
|
}
|
|
896
999
|
});
|
|
897
1000
|
|
|
898
|
-
// src/
|
|
899
|
-
import
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
}
|
|
903
|
-
function
|
|
904
|
-
|
|
905
|
-
const
|
|
906
|
-
|
|
1001
|
+
// src/cli/litellm.ts
|
|
1002
|
+
import * as fs10 from "fs";
|
|
1003
|
+
import * as os from "os";
|
|
1004
|
+
import * as path9 from "path";
|
|
1005
|
+
import { execFileSync as execFileSync9 } from "child_process";
|
|
1006
|
+
async function checkLitellmHealth(url) {
|
|
1007
|
+
try {
|
|
1008
|
+
const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
|
|
1009
|
+
return response.ok;
|
|
1010
|
+
} catch {
|
|
1011
|
+
return false;
|
|
907
1012
|
}
|
|
908
|
-
return err instanceof Error ? err.message : String(err);
|
|
909
|
-
}
|
|
910
|
-
function isNotFoundError(err) {
|
|
911
|
-
const msg = ghErrorMessage(err).toLowerCase();
|
|
912
|
-
return msg.includes("not found") || msg.includes("no pull requests") || msg.includes("could not resolve");
|
|
913
1013
|
}
|
|
914
|
-
function
|
|
915
|
-
_ghCwd = cwd;
|
|
916
|
-
}
|
|
917
|
-
function ghToken() {
|
|
918
|
-
return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
|
|
919
|
-
}
|
|
920
|
-
function gh(args2, options) {
|
|
921
|
-
const token = ghToken();
|
|
922
|
-
const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
|
|
923
|
-
return execFileSync8("gh", args2, {
|
|
924
|
-
encoding: "utf-8",
|
|
925
|
-
timeout: API_TIMEOUT_MS,
|
|
926
|
-
cwd: _ghCwd,
|
|
927
|
-
env,
|
|
928
|
-
input: options?.input,
|
|
929
|
-
stdio: options?.input ? ["pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
|
|
930
|
-
}).trim();
|
|
931
|
-
}
|
|
932
|
-
function getIssue(issueNumber) {
|
|
1014
|
+
async function checkModelHealth(baseUrl, apiKey, model) {
|
|
933
1015
|
try {
|
|
934
|
-
const
|
|
935
|
-
"
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1016
|
+
const res = await fetch(`${baseUrl}/v1/messages`, {
|
|
1017
|
+
method: "POST",
|
|
1018
|
+
headers: {
|
|
1019
|
+
"Content-Type": "application/json",
|
|
1020
|
+
"x-api-key": apiKey,
|
|
1021
|
+
"anthropic-version": "2023-06-01"
|
|
1022
|
+
},
|
|
1023
|
+
body: JSON.stringify({
|
|
1024
|
+
model,
|
|
1025
|
+
max_tokens: 4,
|
|
1026
|
+
messages: [{ role: "user", content: "Reply with: ok" }]
|
|
1027
|
+
}),
|
|
1028
|
+
signal: AbortSignal.timeout(3e4)
|
|
1029
|
+
});
|
|
1030
|
+
if (!res.ok) {
|
|
1031
|
+
const body2 = await res.text().catch(() => "");
|
|
1032
|
+
return { ok: false, error: `HTTP ${res.status}: ${body2.slice(0, 200)}` };
|
|
945
1033
|
}
|
|
946
|
-
|
|
1034
|
+
const body = await res.json();
|
|
1035
|
+
const hasAnthropicContent = Array.isArray(body.content) && body.content.some((b) => b.type === "text");
|
|
1036
|
+
const hasThinkingContent = Array.isArray(body.content) && body.content.some((b) => b.type === "thinking");
|
|
1037
|
+
const hasOpenAIContent = !!body.choices?.[0]?.message?.content;
|
|
1038
|
+
if (!hasAnthropicContent && !hasThinkingContent && !hasOpenAIContent) {
|
|
1039
|
+
return { ok: false, error: `Unexpected response format: ${JSON.stringify(body).slice(0, 200)}` };
|
|
1040
|
+
}
|
|
1041
|
+
return { ok: true };
|
|
947
1042
|
} catch (err) {
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1043
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
function generateLitellmConfig(provider, modelMap) {
|
|
1047
|
+
const apiKeyVar = providerApiKeyEnvVar(provider);
|
|
1048
|
+
const entries = ["model_list:"];
|
|
1049
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1050
|
+
for (const providerModel of Object.values(modelMap)) {
|
|
1051
|
+
if (seen.has(providerModel)) continue;
|
|
1052
|
+
seen.add(providerModel);
|
|
1053
|
+
entries.push(` - model_name: ${providerModel}`);
|
|
1054
|
+
entries.push(` litellm_params:`);
|
|
1055
|
+
entries.push(` model: ${provider}/${providerModel}`);
|
|
1056
|
+
entries.push(` api_key: os.environ/${apiKeyVar}`);
|
|
1057
|
+
}
|
|
1058
|
+
return entries.join("\n") + "\n";
|
|
1059
|
+
}
|
|
1060
|
+
function generateLitellmConfigFromStages(defaultConfig, stages) {
|
|
1061
|
+
const proxyModels = [];
|
|
1062
|
+
if (defaultConfig && defaultConfig.provider !== "claude" && defaultConfig.provider !== "anthropic") {
|
|
1063
|
+
proxyModels.push(defaultConfig);
|
|
1064
|
+
}
|
|
1065
|
+
if (stages) {
|
|
1066
|
+
for (const sc of Object.values(stages)) {
|
|
1067
|
+
if (sc.provider !== "claude" && sc.provider !== "anthropic") {
|
|
1068
|
+
proxyModels.push(sc);
|
|
1069
|
+
}
|
|
952
1070
|
}
|
|
953
|
-
return null;
|
|
954
1071
|
}
|
|
1072
|
+
if (proxyModels.length === 0) return void 0;
|
|
1073
|
+
const entries = ["model_list:"];
|
|
1074
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1075
|
+
for (const { provider, model } of proxyModels) {
|
|
1076
|
+
const key = `${provider}/${model}`;
|
|
1077
|
+
if (seen.has(key)) continue;
|
|
1078
|
+
seen.add(key);
|
|
1079
|
+
const apiKeyVar = providerApiKeyEnvVar(provider);
|
|
1080
|
+
entries.push(` - model_name: ${model}`);
|
|
1081
|
+
entries.push(` litellm_params:`);
|
|
1082
|
+
entries.push(` model: ${provider}/${model}`);
|
|
1083
|
+
entries.push(` api_key: os.environ/${apiKeyVar}`);
|
|
1084
|
+
}
|
|
1085
|
+
return entries.join("\n") + "\n";
|
|
955
1086
|
}
|
|
956
|
-
function
|
|
1087
|
+
async function tryStartLitellm(url, projectDir, generatedConfig) {
|
|
1088
|
+
if (!generatedConfig) {
|
|
1089
|
+
logger.warn("No provider configured in kody.config.json \u2014 cannot start LiteLLM proxy");
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1092
|
+
const configPath = path9.join(os.tmpdir(), "kody-litellm-config.yaml");
|
|
1093
|
+
fs10.writeFileSync(configPath, generatedConfig);
|
|
1094
|
+
const portMatch = url.match(/:(\d+)/);
|
|
1095
|
+
const port = portMatch ? portMatch[1] : "4000";
|
|
1096
|
+
let litellmFound = false;
|
|
957
1097
|
try {
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
`repos/{owner}/{repo}/issues/${issueNumber}/comments`,
|
|
961
|
-
"--jq",
|
|
962
|
-
"[.[] | {body, created_at}]"
|
|
963
|
-
]);
|
|
964
|
-
return output ? JSON.parse(output) : [];
|
|
1098
|
+
execFileSync9("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
|
|
1099
|
+
litellmFound = true;
|
|
965
1100
|
} catch {
|
|
966
|
-
|
|
1101
|
+
try {
|
|
1102
|
+
execFileSync9("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
|
|
1103
|
+
litellmFound = true;
|
|
1104
|
+
} catch {
|
|
1105
|
+
}
|
|
967
1106
|
}
|
|
968
|
-
|
|
969
|
-
|
|
1107
|
+
if (!litellmFound) {
|
|
1108
|
+
logger.warn("litellm not installed (pip install 'litellm[proxy]')");
|
|
1109
|
+
return null;
|
|
1110
|
+
}
|
|
1111
|
+
logger.info(`Starting LiteLLM proxy on port ${port}...`);
|
|
1112
|
+
let cmd;
|
|
1113
|
+
let args2;
|
|
970
1114
|
try {
|
|
971
|
-
|
|
972
|
-
|
|
1115
|
+
execFileSync9("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
|
|
1116
|
+
cmd = "litellm";
|
|
1117
|
+
args2 = ["--config", configPath, "--port", port];
|
|
973
1118
|
} catch {
|
|
974
|
-
|
|
1119
|
+
cmd = "python3";
|
|
1120
|
+
args2 = ["-m", "litellm", "--config", configPath, "--port", port];
|
|
975
1121
|
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1122
|
+
const dotenvPath = path9.join(projectDir, ".env");
|
|
1123
|
+
const dotenvVars = {};
|
|
1124
|
+
if (fs10.existsSync(dotenvPath)) {
|
|
1125
|
+
for (const rawLine of fs10.readFileSync(dotenvPath, "utf-8").split("\n")) {
|
|
1126
|
+
const line = rawLine.trim();
|
|
1127
|
+
if (!line || line.startsWith("#")) continue;
|
|
1128
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
|
|
1129
|
+
if (match) {
|
|
1130
|
+
let value = match[2].trim();
|
|
1131
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1132
|
+
value = value.slice(1, -1);
|
|
1133
|
+
}
|
|
1134
|
+
const commentIdx = value.indexOf(" #");
|
|
1135
|
+
if (commentIdx !== -1) value = value.slice(0, commentIdx).trim();
|
|
1136
|
+
if (value) dotenvVars[match[1]] = value;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
if (Object.keys(dotenvVars).length > 0) {
|
|
1140
|
+
logger.info(` Loaded API keys: ${Object.keys(dotenvVars).join(", ")}`);
|
|
1141
|
+
}
|
|
983
1142
|
}
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1143
|
+
const { spawn: spawn2 } = await import("child_process");
|
|
1144
|
+
const child = spawn2(cmd, args2, {
|
|
1145
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1146
|
+
detached: true,
|
|
1147
|
+
env: { ...process.env, ...dotenvVars }
|
|
1148
|
+
});
|
|
1149
|
+
let proxyStderr = "";
|
|
1150
|
+
child.stderr?.on("data", (chunk) => {
|
|
1151
|
+
proxyStderr += chunk.toString();
|
|
1152
|
+
});
|
|
1153
|
+
for (let i = 0; i < 30; i++) {
|
|
1154
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
1155
|
+
if (await checkLitellmHealth(url)) {
|
|
1156
|
+
logger.info(`LiteLLM proxy ready at ${url}`);
|
|
1157
|
+
return child;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
if (proxyStderr) {
|
|
1161
|
+
logger.warn(`LiteLLM stderr: ${proxyStderr.slice(-1e3)}`);
|
|
994
1162
|
}
|
|
1163
|
+
logger.warn("LiteLLM proxy failed to start within 60s");
|
|
1164
|
+
child.kill();
|
|
1165
|
+
return null;
|
|
995
1166
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1167
|
+
var init_litellm = __esm({
|
|
1168
|
+
"src/cli/litellm.ts"() {
|
|
1169
|
+
"use strict";
|
|
1170
|
+
init_logger();
|
|
1171
|
+
init_config();
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
// src/cli/taskify-command.ts
|
|
1176
|
+
var taskify_command_exports = {};
|
|
1177
|
+
__export(taskify_command_exports, {
|
|
1178
|
+
isTaskifyRun: () => isTaskifyRun,
|
|
1179
|
+
readTaskifyMarker: () => readTaskifyMarker,
|
|
1180
|
+
runTaskifyCommand: () => runTaskifyCommand,
|
|
1181
|
+
taskifyCommand: () => taskifyCommand,
|
|
1182
|
+
topoSort: () => topoSort
|
|
1183
|
+
});
|
|
1184
|
+
import * as fs11 from "fs";
|
|
1185
|
+
import * as path10 from "path";
|
|
1186
|
+
import { fileURLToPath } from "url";
|
|
1187
|
+
import { execSync } from "child_process";
|
|
1188
|
+
function topoSort(tasks) {
|
|
1189
|
+
const n = tasks.length;
|
|
1190
|
+
const inDegree = new Array(n).fill(0);
|
|
1191
|
+
const adj = Array.from({ length: n }, () => []);
|
|
1192
|
+
for (let i = 0; i < n; i++) {
|
|
1193
|
+
for (const dep of tasks[i].dependsOn ?? []) {
|
|
1194
|
+
if (dep >= 0 && dep < n && dep !== i) {
|
|
1195
|
+
adj[dep].push(i);
|
|
1196
|
+
inDegree[i]++;
|
|
1197
|
+
}
|
|
1009
1198
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1199
|
+
}
|
|
1200
|
+
const queue = [];
|
|
1201
|
+
for (let i = 0; i < n; i++) {
|
|
1202
|
+
if (inDegree[i] === 0) queue.push(i);
|
|
1203
|
+
}
|
|
1204
|
+
const sorted = [];
|
|
1205
|
+
while (queue.length > 0) {
|
|
1206
|
+
const node = queue.shift();
|
|
1207
|
+
sorted.push(tasks[node]);
|
|
1208
|
+
for (const neighbor of adj[node]) {
|
|
1209
|
+
inDegree[neighbor]--;
|
|
1210
|
+
if (inDegree[neighbor] === 0) queue.push(neighbor);
|
|
1014
1211
|
}
|
|
1015
|
-
return null;
|
|
1016
1212
|
}
|
|
1213
|
+
if (sorted.length !== n) {
|
|
1214
|
+
logger.warn("[taskify] dependency cycle detected \u2014 falling back to original order");
|
|
1215
|
+
return [...tasks];
|
|
1216
|
+
}
|
|
1217
|
+
return sorted;
|
|
1017
1218
|
}
|
|
1018
|
-
function
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1219
|
+
function getArg(args2, flag) {
|
|
1220
|
+
const idx = args2.indexOf(flag);
|
|
1221
|
+
return idx !== -1 ? args2[idx + 1] : void 0;
|
|
1222
|
+
}
|
|
1223
|
+
function hasFlag(args2, flag) {
|
|
1224
|
+
return args2.includes(flag);
|
|
1225
|
+
}
|
|
1226
|
+
async function runTaskifyCommand() {
|
|
1227
|
+
const args2 = process.argv.slice(3);
|
|
1228
|
+
const cwdArg = getArg(args2, "--cwd") ?? process.cwd();
|
|
1229
|
+
const projectDir = path10.resolve(cwdArg);
|
|
1230
|
+
const ticketId = getArg(args2, "--ticket") ?? process.env.TICKET_ID;
|
|
1231
|
+
const prdFileArg = getArg(args2, "--file") ?? process.env.PRD_FILE;
|
|
1232
|
+
const prdFile = prdFileArg ? path10.resolve(projectDir, prdFileArg) : void 0;
|
|
1233
|
+
const issueNumberStr = getArg(args2, "--issue-number") ?? process.env.ISSUE_NUMBER ?? "";
|
|
1234
|
+
const issueNumber = issueNumberStr ? parseInt(issueNumberStr, 10) : void 0;
|
|
1235
|
+
const feedback = getArg(args2, "--feedback") ?? process.env.FEEDBACK;
|
|
1236
|
+
const local = hasFlag(args2, "--local") || !process.env.CI;
|
|
1237
|
+
const taskIdArg = getArg(args2, "--task-id") ?? process.env.TASK_ID;
|
|
1238
|
+
const taskId = taskIdArg ?? (issueNumber ? `taskify-${issueNumber}-${generateTaskId()}` : `taskify-${generateTaskId()}`);
|
|
1239
|
+
if (!ticketId && !prdFile) {
|
|
1240
|
+
logger.error("Usage: kody taskify --ticket <ticket-id> OR kody taskify --file <prd.md>");
|
|
1241
|
+
process.exit(1);
|
|
1242
|
+
}
|
|
1243
|
+
if (prdFile && !fs11.existsSync(prdFile)) {
|
|
1244
|
+
logger.error(`File not found: ${prdFile}`);
|
|
1245
|
+
process.exit(1);
|
|
1246
|
+
}
|
|
1247
|
+
setConfigDir(projectDir);
|
|
1248
|
+
setGhCwd(projectDir);
|
|
1249
|
+
const config = getProjectConfig();
|
|
1250
|
+
let litellmProcess = null;
|
|
1251
|
+
let runnerEnv;
|
|
1252
|
+
if (anyStageNeedsProxy(config)) {
|
|
1253
|
+
const litellmUrl = getLitellmUrl();
|
|
1254
|
+
const proxyRunning = await checkLitellmHealth(litellmUrl);
|
|
1255
|
+
if (!proxyRunning) {
|
|
1256
|
+
let generatedConfig;
|
|
1257
|
+
if (config.agent.stages || config.agent.default) {
|
|
1258
|
+
generatedConfig = generateLitellmConfigFromStages(config.agent.default, config.agent.stages);
|
|
1259
|
+
} else if (config.agent.provider && config.agent.provider !== "anthropic") {
|
|
1260
|
+
generatedConfig = generateLitellmConfig(config.agent.provider, config.agent.modelMap);
|
|
1261
|
+
}
|
|
1262
|
+
litellmProcess = await tryStartLitellm(litellmUrl, projectDir, generatedConfig);
|
|
1263
|
+
}
|
|
1264
|
+
runnerEnv = {
|
|
1265
|
+
ANTHROPIC_BASE_URL: litellmUrl,
|
|
1266
|
+
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || "dummy"
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
await taskifyCommand({ ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId, runnerEnv });
|
|
1270
|
+
litellmProcess?.kill();
|
|
1271
|
+
}
|
|
1272
|
+
async function taskifyCommand(opts) {
|
|
1273
|
+
const { ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId } = opts;
|
|
1274
|
+
const config = getProjectConfig();
|
|
1275
|
+
const taskDir = path10.join(projectDir, ".kody", "tasks", taskId);
|
|
1276
|
+
fs11.mkdirSync(taskDir, { recursive: true });
|
|
1277
|
+
const mode = prdFile ? "file" : "ticket";
|
|
1278
|
+
logger.info(`[taskify] mode=${mode} source=${ticketId ?? prdFile} issue=${issueNumber ?? "none"} task=${taskId}`);
|
|
1279
|
+
let mcpConfigJson;
|
|
1280
|
+
if (mode === "ticket") {
|
|
1281
|
+
try {
|
|
1282
|
+
mcpConfigJson = buildTaskifyMcpConfigJson(config);
|
|
1283
|
+
} catch (err) {
|
|
1284
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1285
|
+
logger.error(`[taskify] MCP config error: ${msg}`);
|
|
1286
|
+
if (issueNumber && !local) {
|
|
1287
|
+
postComment(
|
|
1288
|
+
issueNumber,
|
|
1289
|
+
`Kody could not start the taskify command:
|
|
1290
|
+
|
|
1291
|
+
> ${msg}
|
|
1292
|
+
|
|
1293
|
+
Add the required MCP server config to \`kody.config.json\` and try again.`
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
process.exit(1);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
const sc = resolveStageConfig(config, "taskify", "strong");
|
|
1300
|
+
const model = sc.model;
|
|
1301
|
+
const fileContent = prdFile ? fs11.readFileSync(prdFile, "utf-8") : void 0;
|
|
1302
|
+
let projectContext;
|
|
1303
|
+
{
|
|
1304
|
+
const parts = [];
|
|
1305
|
+
const memoryPath = path10.join(projectDir, ".kody", "memory.md");
|
|
1306
|
+
if (fs11.existsSync(memoryPath)) {
|
|
1307
|
+
try {
|
|
1308
|
+
const content = fs11.readFileSync(memoryPath, "utf-8").slice(0, 2e3);
|
|
1309
|
+
if (content.trim()) parts.push(`### Project Memory
|
|
1310
|
+
${content}`);
|
|
1311
|
+
} catch {
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
try {
|
|
1315
|
+
const output = execSync("git ls-files", { cwd: projectDir, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
1316
|
+
const lines = output.split("\n").filter(Boolean).slice(0, 150);
|
|
1317
|
+
if (lines.length > 0) parts.push(`### File Tree
|
|
1318
|
+
\`\`\`
|
|
1319
|
+
${lines.join("\n")}
|
|
1320
|
+
\`\`\``);
|
|
1321
|
+
} catch {
|
|
1322
|
+
}
|
|
1323
|
+
if (parts.length > 0) projectContext = parts.join("\n\n");
|
|
1324
|
+
}
|
|
1325
|
+
const prompt = buildPrompt({ ticketId, fileContent, taskDir, feedback, projectContext });
|
|
1326
|
+
if (issueNumber && !local) {
|
|
1327
|
+
const src = mode === "file" ? `file \`${path10.basename(prdFile)}\`` : `ticket **${ticketId}**`;
|
|
1328
|
+
postComment(issueNumber, `Kody is decomposing ${src} into tasks...`);
|
|
1329
|
+
setLifecycleLabel(issueNumber, "planning");
|
|
1330
|
+
}
|
|
1331
|
+
fs11.writeFileSync(path10.join(taskDir, MARKER_FILE), JSON.stringify({ ticketId, prdFile, issueNumber }));
|
|
1332
|
+
const runner = opts.runner ?? createClaudeCodeRunner();
|
|
1333
|
+
logger.info(` model=${model} timeout=${TASKIFY_TIMEOUT_MS / 1e3}s`);
|
|
1334
|
+
const result = await runner.run("taskify", prompt, model, TASKIFY_TIMEOUT_MS, taskDir, {
|
|
1335
|
+
cwd: projectDir,
|
|
1336
|
+
mcpConfigJson,
|
|
1337
|
+
env: opts.runnerEnv
|
|
1338
|
+
});
|
|
1339
|
+
if (result.outcome !== "completed") {
|
|
1340
|
+
const errMsg = result.outcome === "timed_out" ? "Taskify timed out after 5 minutes." : `Taskify failed: ${result.error}`;
|
|
1341
|
+
logger.error(`[taskify] ${errMsg}`);
|
|
1342
|
+
if (issueNumber && !local) {
|
|
1343
|
+
postComment(issueNumber, `Kody taskify failed:
|
|
1344
|
+
|
|
1345
|
+
> ${errMsg}`);
|
|
1346
|
+
setLifecycleLabel(issueNumber, "failed");
|
|
1347
|
+
}
|
|
1348
|
+
process.exit(1);
|
|
1349
|
+
}
|
|
1350
|
+
const resultPath = path10.join(taskDir, RESULT_FILE);
|
|
1351
|
+
if (!fs11.existsSync(resultPath)) {
|
|
1352
|
+
const errMsg = `Claude did not write ${RESULT_FILE}. Output:
|
|
1353
|
+
|
|
1354
|
+
${result.output?.slice(0, 500) ?? "(none)"}`;
|
|
1355
|
+
logger.error(`[taskify] ${errMsg}`);
|
|
1356
|
+
if (issueNumber && !local) {
|
|
1357
|
+
postComment(issueNumber, `Kody taskify failed: result file not found.
|
|
1358
|
+
|
|
1359
|
+
${errMsg}`);
|
|
1360
|
+
setLifecycleLabel(issueNumber, "failed");
|
|
1361
|
+
}
|
|
1362
|
+
process.exit(1);
|
|
1363
|
+
}
|
|
1364
|
+
let parsed;
|
|
1365
|
+
try {
|
|
1366
|
+
parsed = JSON.parse(fs11.readFileSync(resultPath, "utf-8"));
|
|
1367
|
+
} catch {
|
|
1368
|
+
const errMsg = `Could not parse ${RESULT_FILE} as JSON.`;
|
|
1369
|
+
logger.error(`[taskify] ${errMsg}`);
|
|
1370
|
+
if (issueNumber && !local) {
|
|
1371
|
+
postComment(issueNumber, `Kody taskify failed: ${errMsg}`);
|
|
1372
|
+
setLifecycleLabel(issueNumber, "failed");
|
|
1373
|
+
}
|
|
1374
|
+
process.exit(1);
|
|
1375
|
+
}
|
|
1376
|
+
const sourceLabel = ticketId ?? (prdFile ? path10.basename(prdFile) : "spec");
|
|
1377
|
+
if (parsed.status === "questions") {
|
|
1378
|
+
handleQuestions(parsed, sourceLabel, issueNumber, local ?? false);
|
|
1379
|
+
} else if (parsed.status === "ready") {
|
|
1380
|
+
await handleTasks(parsed, sourceLabel, issueNumber, local ?? false);
|
|
1381
|
+
} else {
|
|
1382
|
+
const errMsg = `Unexpected status in ${RESULT_FILE}: ${JSON.stringify(parsed)}`;
|
|
1383
|
+
logger.error(`[taskify] ${errMsg}`);
|
|
1384
|
+
if (issueNumber && !local) {
|
|
1385
|
+
postComment(issueNumber, `Kody taskify failed: ${errMsg}`);
|
|
1386
|
+
setLifecycleLabel(issueNumber, "failed");
|
|
1387
|
+
}
|
|
1388
|
+
process.exit(1);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
function handleQuestions(parsed, ticketId, issueNumber, local) {
|
|
1392
|
+
const questions = parsed.questions ?? [];
|
|
1393
|
+
const numbered = questions.map((q, i) => `${i + 1}. ${q}`).join("\n");
|
|
1394
|
+
const comment = `Kody has questions before decomposing **${ticketId}**:
|
|
1395
|
+
|
|
1396
|
+
${numbered}
|
|
1397
|
+
|
|
1398
|
+
Reply with \`@kody approve\` and your answers to proceed.`;
|
|
1399
|
+
logger.info(`[taskify] posting ${questions.length} question(s)`);
|
|
1400
|
+
if (issueNumber && !local) {
|
|
1401
|
+
postComment(issueNumber, comment);
|
|
1402
|
+
setLifecycleLabel(issueNumber, "waiting");
|
|
1403
|
+
} else {
|
|
1404
|
+
logger.info(`[taskify] questions:
|
|
1405
|
+
${comment}`);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
async function handleTasks(parsed, ticketId, issueNumber, local) {
|
|
1409
|
+
const tasks = topoSort(parsed.tasks ?? []);
|
|
1410
|
+
if (tasks.length === 0) {
|
|
1411
|
+
logger.warn("[taskify] no tasks in result \u2014 nothing to file");
|
|
1412
|
+
if (issueNumber && !local) {
|
|
1413
|
+
postComment(issueNumber, `Kody taskify completed but found no tasks to file for **${ticketId}**.`);
|
|
1414
|
+
setLifecycleLabel(issueNumber, "done");
|
|
1415
|
+
}
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
const tooMany = tasks.length > MAX_TASKS_GUARD;
|
|
1419
|
+
if (tooMany) {
|
|
1420
|
+
logger.warn(`[taskify] ${tasks.length} tasks exceeds MAX_TASKS_GUARD (${MAX_TASKS_GUARD}) \u2014 filing issues but skipping auto-trigger`);
|
|
1421
|
+
}
|
|
1422
|
+
logger.info(`[taskify] filing ${tasks.length} issue(s)`);
|
|
1423
|
+
const filed = [];
|
|
1424
|
+
for (const task of tasks) {
|
|
1425
|
+
if (local) {
|
|
1426
|
+
logger.info(` [local] would create issue: ${task.title}`);
|
|
1427
|
+
filed.push({ number: 0, url: "#", title: task.title });
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
const allLabels = [...task.labels ?? [], ...task.priority ? [`priority:${task.priority}`] : []];
|
|
1431
|
+
const issue = createIssue(task.title, task.body, allLabels);
|
|
1432
|
+
if (issue) {
|
|
1433
|
+
filed.push({ number: issue.number, url: issue.url, title: task.title });
|
|
1434
|
+
} else {
|
|
1435
|
+
logger.warn(` failed to create issue: ${task.title}`);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
const autoTrigger = !tooMany && filed.length <= AUTO_TRIGGER_THRESHOLD;
|
|
1439
|
+
if (autoTrigger && !local) {
|
|
1440
|
+
for (const issue of filed) {
|
|
1441
|
+
if (issue.number > 0) {
|
|
1442
|
+
postComment(issue.number, "@kody");
|
|
1443
|
+
logger.info(` auto-triggered @kody on issue #${issue.number}`);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
if (issueNumber && !local) {
|
|
1448
|
+
const links = filed.map((i) => `- [#${i.number}](${i.url}) \u2014 ${i.title}`).join("\n");
|
|
1449
|
+
const triggerNote = tooMany ? `
|
|
1450
|
+
|
|
1451
|
+
> **${tasks.length} tasks filed** \u2014 auto-trigger is disabled for large epics. Comment \`@kody\` on each issue to start the pipeline.` : autoTrigger ? `
|
|
1452
|
+
|
|
1453
|
+
> Auto-triggered \`@kody\` on each issue.` : `
|
|
1454
|
+
|
|
1455
|
+
> Comment \`@kody\` on each issue to start the pipeline.`;
|
|
1456
|
+
postComment(
|
|
1457
|
+
issueNumber,
|
|
1458
|
+
`Kody decomposed **${ticketId}** into ${filed.length} task(s):
|
|
1459
|
+
|
|
1460
|
+
${links}${triggerNote}`
|
|
1461
|
+
);
|
|
1462
|
+
setLifecycleLabel(issueNumber, "done");
|
|
1463
|
+
} else if (local) {
|
|
1464
|
+
logger.info(`[taskify] local mode \u2014 would file ${filed.length} issue(s)`);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
function buildPrompt(opts) {
|
|
1468
|
+
const { ticketId, fileContent, taskDir, feedback, projectContext } = opts;
|
|
1469
|
+
const scriptDir = new URL(".", import.meta.url).pathname;
|
|
1470
|
+
const candidates = [
|
|
1471
|
+
path10.resolve(scriptDir, "..", "prompts", "taskify-ticket.md"),
|
|
1472
|
+
path10.resolve(scriptDir, "..", "..", "prompts", "taskify-ticket.md"),
|
|
1473
|
+
path10.resolve(__dirname, "..", "..", "prompts", "taskify-ticket.md"),
|
|
1474
|
+
path10.resolve(__dirname, "..", "prompts", "taskify-ticket.md")
|
|
1475
|
+
];
|
|
1476
|
+
let template = "";
|
|
1477
|
+
for (const candidate of candidates) {
|
|
1478
|
+
if (fs11.existsSync(candidate)) {
|
|
1479
|
+
template = fs11.readFileSync(candidate, "utf-8");
|
|
1480
|
+
break;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
if (!template) {
|
|
1484
|
+
throw new Error(`Could not find prompts/taskify-ticket.md. Searched: ${candidates.join(", ")}`);
|
|
1485
|
+
}
|
|
1486
|
+
const resolveBlock = (name, value) => {
|
|
1487
|
+
if (value) {
|
|
1488
|
+
template = template.replace(new RegExp(`\\{\\{#if ${name}\\}\\}\\n?([\\s\\S]*?)\\{\\{\\/if\\}\\}`, "g"), "$1");
|
|
1489
|
+
template = template.replace(new RegExp(`\\{\\{${name}\\}\\}`, "g"), value);
|
|
1490
|
+
} else {
|
|
1491
|
+
template = template.replace(new RegExp(`\\{\\{#if ${name}\\}\\}\\n?[\\s\\S]*?\\{\\{\\/if\\}\\}\\n?`, "g"), "");
|
|
1492
|
+
}
|
|
1493
|
+
};
|
|
1494
|
+
resolveBlock("PROJECT_CONTEXT", projectContext);
|
|
1495
|
+
resolveBlock("TICKET_ID", ticketId);
|
|
1496
|
+
resolveBlock("FILE_CONTENT", fileContent);
|
|
1497
|
+
resolveBlock("FEEDBACK", feedback);
|
|
1498
|
+
template = template.replace(/\{\{TASK_DIR\}\}/g, taskDir);
|
|
1499
|
+
return template;
|
|
1500
|
+
}
|
|
1501
|
+
function isTaskifyRun(taskDir) {
|
|
1502
|
+
return fs11.existsSync(path10.join(taskDir, MARKER_FILE));
|
|
1503
|
+
}
|
|
1504
|
+
function readTaskifyMarker(taskDir) {
|
|
1505
|
+
const markerPath = path10.join(taskDir, MARKER_FILE);
|
|
1506
|
+
if (!fs11.existsSync(markerPath)) return null;
|
|
1507
|
+
try {
|
|
1508
|
+
return JSON.parse(fs11.readFileSync(markerPath, "utf-8"));
|
|
1509
|
+
} catch {
|
|
1510
|
+
return null;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
var __dirname, AUTO_TRIGGER_THRESHOLD, MAX_TASKS_GUARD, TASKIFY_TIMEOUT_MS, MARKER_FILE, RESULT_FILE;
|
|
1514
|
+
var init_taskify_command = __esm({
|
|
1515
|
+
"src/cli/taskify-command.ts"() {
|
|
1516
|
+
"use strict";
|
|
1517
|
+
init_config();
|
|
1518
|
+
init_agent_runner();
|
|
1519
|
+
init_mcp_config();
|
|
1520
|
+
init_github_api();
|
|
1521
|
+
init_logger();
|
|
1522
|
+
init_task_resolution();
|
|
1523
|
+
init_litellm();
|
|
1524
|
+
__dirname = path10.dirname(fileURLToPath(import.meta.url));
|
|
1525
|
+
AUTO_TRIGGER_THRESHOLD = 5;
|
|
1526
|
+
MAX_TASKS_GUARD = 20;
|
|
1527
|
+
TASKIFY_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
1528
|
+
MARKER_FILE = "taskify.marker";
|
|
1529
|
+
RESULT_FILE = "taskify-result.json";
|
|
1530
|
+
}
|
|
1531
|
+
});
|
|
1532
|
+
|
|
1533
|
+
// src/ci/parse-inputs.ts
|
|
1534
|
+
var parse_inputs_exports = {};
|
|
1535
|
+
__export(parse_inputs_exports, {
|
|
1536
|
+
parseCommentInputs: () => parseCommentInputs,
|
|
1537
|
+
runCiParse: () => runCiParse,
|
|
1538
|
+
writeOutputs: () => writeOutputs
|
|
1539
|
+
});
|
|
1540
|
+
import * as fs12 from "fs";
|
|
1541
|
+
function generateTimestamp() {
|
|
1542
|
+
const now = /* @__PURE__ */ new Date();
|
|
1543
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1544
|
+
const y = String(now.getFullYear()).slice(2);
|
|
1545
|
+
const m = pad(now.getMonth() + 1);
|
|
1546
|
+
const d = pad(now.getDate());
|
|
1547
|
+
const H = pad(now.getHours());
|
|
1548
|
+
const M = pad(now.getMinutes());
|
|
1549
|
+
const S = pad(now.getSeconds());
|
|
1550
|
+
return `${y}${m}${d}-${H}${M}${S}`;
|
|
1551
|
+
}
|
|
1552
|
+
function parseCommentInputs() {
|
|
1553
|
+
const triggerType = process.env.TRIGGER_TYPE ?? "dispatch";
|
|
1554
|
+
if (triggerType === "dispatch") {
|
|
1555
|
+
const taskId2 = process.env.INPUT_TASK_ID ?? "";
|
|
1556
|
+
return {
|
|
1557
|
+
task_id: taskId2,
|
|
1558
|
+
mode: process.env.INPUT_MODE ?? "full",
|
|
1559
|
+
from_stage: process.env.INPUT_FROM_STAGE ?? "",
|
|
1560
|
+
issue_number: process.env.INPUT_ISSUE_NUMBER ?? "",
|
|
1561
|
+
pr_number: "",
|
|
1562
|
+
feedback: process.env.INPUT_FEEDBACK ?? "",
|
|
1563
|
+
complexity: "",
|
|
1564
|
+
ci_run_id: "",
|
|
1565
|
+
ticket_id: "",
|
|
1566
|
+
prd_file: "",
|
|
1567
|
+
dry_run: false,
|
|
1568
|
+
valid: !!taskId2,
|
|
1569
|
+
trigger_type: "dispatch"
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
const commentBody = (process.env.COMMENT_BODY ?? "").replace(/\r/g, "");
|
|
1573
|
+
const issueNumber = process.env.ISSUE_NUMBER ?? "";
|
|
1574
|
+
const isPR = !!process.env.ISSUE_IS_PR;
|
|
1575
|
+
const kodyMatch = commentBody.match(/(?:@kody|\/kody)\s*(.*)/i);
|
|
1576
|
+
if (!kodyMatch) {
|
|
1577
|
+
return {
|
|
1578
|
+
task_id: "",
|
|
1579
|
+
mode: "full",
|
|
1580
|
+
from_stage: "",
|
|
1581
|
+
issue_number: issueNumber,
|
|
1582
|
+
pr_number: "",
|
|
1583
|
+
feedback: "",
|
|
1584
|
+
complexity: "",
|
|
1585
|
+
ci_run_id: "",
|
|
1586
|
+
ticket_id: "",
|
|
1587
|
+
prd_file: "",
|
|
1588
|
+
dry_run: false,
|
|
1589
|
+
valid: false,
|
|
1590
|
+
trigger_type: "comment"
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
const argsLine = kodyMatch[1].trim();
|
|
1594
|
+
let fromStage = "";
|
|
1595
|
+
let feedback = "";
|
|
1596
|
+
let complexity = "";
|
|
1597
|
+
let dryRun = false;
|
|
1598
|
+
let ciRunId = "";
|
|
1599
|
+
let ticketId = "";
|
|
1600
|
+
let prdFile = "";
|
|
1601
|
+
const fromMatch = argsLine.match(/--from\s+(\S+)/);
|
|
1602
|
+
if (fromMatch) fromStage = fromMatch[1];
|
|
1603
|
+
const feedbackMatch = argsLine.match(/--feedback\s+"([^"]*)"/);
|
|
1604
|
+
if (feedbackMatch) feedback = feedbackMatch[1];
|
|
1605
|
+
const complexityMatch = argsLine.match(/--complexity\s+(\S+)/);
|
|
1606
|
+
if (complexityMatch) complexity = complexityMatch[1];
|
|
1607
|
+
if (/--dry-run/.test(argsLine)) dryRun = true;
|
|
1608
|
+
const ciRunIdMatch = argsLine.match(/--ci-run-id\s+(\S+)/);
|
|
1609
|
+
if (ciRunIdMatch) ciRunId = ciRunIdMatch[1];
|
|
1610
|
+
const ticketMatch = argsLine.match(/--ticket\s+(\S+)/);
|
|
1611
|
+
if (ticketMatch) ticketId = ticketMatch[1];
|
|
1612
|
+
const fileMatch = argsLine.match(/--file\s+(\S+)/);
|
|
1613
|
+
if (fileMatch) prdFile = fileMatch[1];
|
|
1614
|
+
const positional = argsLine.replace(/--from\s+\S+/g, "").replace(/--feedback\s+"[^"]*"/g, "").replace(/--complexity\s+\S+/g, "").replace(/--dry-run/g, "").replace(/--ci-run-id\s+\S+/g, "").replace(/--ticket\s+\S+/g, "").replace(/--file\s+\S+/g, "").replace(/\s+/g, " ").trim();
|
|
1615
|
+
const parts = positional ? positional.split(/\s+/) : [];
|
|
1616
|
+
let mode = "full";
|
|
1617
|
+
let taskId = "";
|
|
1618
|
+
let idx = 0;
|
|
1619
|
+
if (parts[idx] && VALID_MODES.includes(parts[idx])) {
|
|
1620
|
+
mode = parts[idx];
|
|
1621
|
+
idx++;
|
|
1622
|
+
}
|
|
1623
|
+
if (parts[idx] && !parts[idx].startsWith("--")) {
|
|
1624
|
+
taskId = parts[idx];
|
|
1625
|
+
idx++;
|
|
1626
|
+
} else if (parts[0] && !VALID_MODES.includes(parts[0]) && !parts[0].startsWith("--")) {
|
|
1627
|
+
taskId = parts[0];
|
|
1628
|
+
}
|
|
1629
|
+
const kodyLineIdx = commentBody.search(/(?:@kody|\/kody)/i);
|
|
1630
|
+
const afterKodyLine = commentBody.slice(kodyLineIdx);
|
|
1631
|
+
const newlineIdx = afterKodyLine.indexOf("\n");
|
|
1632
|
+
const bodyAfterCommand = newlineIdx !== -1 ? afterKodyLine.slice(newlineIdx + 1) : "";
|
|
1633
|
+
if (mode === "approve") {
|
|
1634
|
+
mode = "rerun";
|
|
1635
|
+
if (bodyAfterCommand) {
|
|
1636
|
+
feedback = bodyAfterCommand;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
if (mode === "fix") {
|
|
1640
|
+
if (bodyAfterCommand) {
|
|
1641
|
+
feedback = bodyAfterCommand;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
if (mode === "fix-ci") {
|
|
1645
|
+
if (bodyAfterCommand) {
|
|
1646
|
+
feedback = bodyAfterCommand;
|
|
1647
|
+
const runIdFromBody = bodyAfterCommand.match(/Run ID:\s*(\d+)/);
|
|
1648
|
+
if (runIdFromBody) {
|
|
1649
|
+
ciRunId = runIdFromBody[1];
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
if (mode === "bootstrap") {
|
|
1654
|
+
taskId = `bootstrap-${generateTimestamp()}`;
|
|
1655
|
+
}
|
|
1656
|
+
if (mode === "taskify") {
|
|
1657
|
+
taskId = `taskify-${issueNumber}-${generateTimestamp()}`;
|
|
1658
|
+
}
|
|
1659
|
+
const prNumber = isPR ? issueNumber : "";
|
|
1660
|
+
if (mode === "review" && prNumber) {
|
|
1661
|
+
taskId = `review-pr-${prNumber}-${generateTimestamp()}`;
|
|
1662
|
+
}
|
|
1663
|
+
if (!taskId && mode === "full") {
|
|
1664
|
+
taskId = `${issueNumber}-${generateTimestamp()}`;
|
|
1665
|
+
}
|
|
1666
|
+
const modesWithoutTaskId = ["fix", "fix-ci", "status", "review", "resolve", "rerun"];
|
|
1667
|
+
const valid = !!taskId || modesWithoutTaskId.includes(mode);
|
|
1668
|
+
if (mode === "taskify" && !ticketId && !prdFile) {
|
|
1669
|
+
return {
|
|
1670
|
+
task_id: taskId,
|
|
1671
|
+
mode,
|
|
1672
|
+
from_stage: fromStage,
|
|
1673
|
+
issue_number: issueNumber,
|
|
1674
|
+
pr_number: "",
|
|
1675
|
+
feedback,
|
|
1676
|
+
complexity,
|
|
1677
|
+
ci_run_id: ciRunId,
|
|
1678
|
+
ticket_id: "",
|
|
1679
|
+
prd_file: "",
|
|
1680
|
+
dry_run: dryRun,
|
|
1681
|
+
valid: false,
|
|
1682
|
+
trigger_type: "comment"
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
return {
|
|
1686
|
+
task_id: taskId,
|
|
1687
|
+
mode,
|
|
1688
|
+
from_stage: fromStage,
|
|
1689
|
+
issue_number: issueNumber,
|
|
1690
|
+
pr_number: prNumber,
|
|
1691
|
+
feedback,
|
|
1692
|
+
complexity,
|
|
1693
|
+
ci_run_id: ciRunId,
|
|
1694
|
+
ticket_id: ticketId,
|
|
1695
|
+
prd_file: prdFile,
|
|
1696
|
+
dry_run: dryRun,
|
|
1697
|
+
valid,
|
|
1698
|
+
trigger_type: "comment"
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
function writeOutputs(result) {
|
|
1702
|
+
const outputFile = process.env.GITHUB_OUTPUT;
|
|
1703
|
+
function output(key, value) {
|
|
1704
|
+
if (outputFile) {
|
|
1705
|
+
if (value.includes("\n")) {
|
|
1706
|
+
fs12.appendFileSync(outputFile, `${key}<<KODY_EOF
|
|
1707
|
+
${value}
|
|
1708
|
+
KODY_EOF
|
|
1709
|
+
`);
|
|
1710
|
+
} else {
|
|
1711
|
+
fs12.appendFileSync(outputFile, `${key}=${value}
|
|
1712
|
+
`);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
const display = value.includes("\n") ? value.split("\n")[0] + "..." : value;
|
|
1716
|
+
console.log(`${key}=${display}`);
|
|
1717
|
+
}
|
|
1718
|
+
output("task_id", result.task_id);
|
|
1719
|
+
output("mode", result.mode);
|
|
1720
|
+
output("from_stage", result.from_stage);
|
|
1721
|
+
output("issue_number", result.issue_number);
|
|
1722
|
+
output("pr_number", result.pr_number);
|
|
1723
|
+
output("feedback", result.feedback);
|
|
1724
|
+
output("complexity", result.complexity);
|
|
1725
|
+
output("ci_run_id", result.ci_run_id);
|
|
1726
|
+
output("ticket_id", result.ticket_id);
|
|
1727
|
+
output("prd_file", result.prd_file);
|
|
1728
|
+
output("dry_run", result.dry_run ? "true" : "false");
|
|
1729
|
+
output("valid", result.valid ? "true" : "false");
|
|
1730
|
+
output("trigger_type", result.trigger_type);
|
|
1731
|
+
}
|
|
1732
|
+
function runCiParse() {
|
|
1733
|
+
const result = parseCommentInputs();
|
|
1734
|
+
writeOutputs(result);
|
|
1735
|
+
}
|
|
1736
|
+
var VALID_MODES;
|
|
1737
|
+
var init_parse_inputs = __esm({
|
|
1738
|
+
"src/ci/parse-inputs.ts"() {
|
|
1739
|
+
"use strict";
|
|
1740
|
+
VALID_MODES = [
|
|
1741
|
+
"full",
|
|
1742
|
+
"rerun",
|
|
1743
|
+
"fix",
|
|
1744
|
+
"fix-ci",
|
|
1745
|
+
"status",
|
|
1746
|
+
"approve",
|
|
1747
|
+
"review",
|
|
1748
|
+
"resolve",
|
|
1749
|
+
"bootstrap",
|
|
1750
|
+
"taskify"
|
|
1751
|
+
];
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
// src/definitions.ts
|
|
1756
|
+
var definitions_exports = {};
|
|
1757
|
+
__export(definitions_exports, {
|
|
1758
|
+
STAGES: () => STAGES,
|
|
1759
|
+
applyTimeoutOverrides: () => applyTimeoutOverrides,
|
|
1760
|
+
getStage: () => getStage
|
|
1761
|
+
});
|
|
1762
|
+
function getStage(name) {
|
|
1763
|
+
return STAGES.find((s) => s.name === name);
|
|
1764
|
+
}
|
|
1765
|
+
function applyTimeoutOverrides(overrides) {
|
|
1766
|
+
for (const stage of STAGES) {
|
|
1767
|
+
if (overrides[stage.name] != null) {
|
|
1768
|
+
stage.timeout = overrides[stage.name] * 1e3;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
var STAGES;
|
|
1773
|
+
var init_definitions = __esm({
|
|
1774
|
+
"src/definitions.ts"() {
|
|
1775
|
+
"use strict";
|
|
1776
|
+
STAGES = [
|
|
1777
|
+
{
|
|
1778
|
+
name: "taskify",
|
|
1779
|
+
type: "agent",
|
|
1780
|
+
modelTier: "cheap",
|
|
1781
|
+
timeout: 6e5,
|
|
1782
|
+
maxRetries: 1,
|
|
1783
|
+
outputFile: "task.json"
|
|
1784
|
+
},
|
|
1785
|
+
{
|
|
1786
|
+
name: "plan",
|
|
1787
|
+
type: "agent",
|
|
1788
|
+
modelTier: "strong",
|
|
1789
|
+
timeout: 6e5,
|
|
1790
|
+
maxRetries: 1,
|
|
1791
|
+
outputFile: "plan.md"
|
|
1792
|
+
},
|
|
1793
|
+
{
|
|
1794
|
+
name: "build",
|
|
1795
|
+
type: "agent",
|
|
1796
|
+
modelTier: "mid",
|
|
1797
|
+
timeout: 24e5,
|
|
1798
|
+
maxRetries: 1
|
|
1799
|
+
},
|
|
1800
|
+
{
|
|
1801
|
+
name: "verify",
|
|
1802
|
+
type: "gate",
|
|
1803
|
+
modelTier: "cheap",
|
|
1804
|
+
timeout: 3e5,
|
|
1805
|
+
maxRetries: 2,
|
|
1806
|
+
retryWithAgent: "autofix"
|
|
1807
|
+
},
|
|
1808
|
+
{
|
|
1809
|
+
name: "review",
|
|
1810
|
+
type: "agent",
|
|
1811
|
+
modelTier: "strong",
|
|
1812
|
+
timeout: 6e5,
|
|
1813
|
+
maxRetries: 1,
|
|
1814
|
+
outputFile: "review.md"
|
|
1815
|
+
},
|
|
1816
|
+
{
|
|
1817
|
+
name: "review-fix",
|
|
1818
|
+
type: "agent",
|
|
1819
|
+
modelTier: "mid",
|
|
1820
|
+
timeout: 12e5,
|
|
1821
|
+
maxRetries: 1
|
|
1822
|
+
},
|
|
1823
|
+
{
|
|
1824
|
+
name: "ship",
|
|
1825
|
+
type: "deterministic",
|
|
1826
|
+
modelTier: "cheap",
|
|
1827
|
+
timeout: 24e4,
|
|
1828
|
+
maxRetries: 1,
|
|
1829
|
+
outputFile: "ship.md"
|
|
1830
|
+
}
|
|
1831
|
+
];
|
|
1832
|
+
}
|
|
1833
|
+
});
|
|
1834
|
+
|
|
1835
|
+
// src/git-utils.ts
|
|
1836
|
+
import { execFileSync as execFileSync10 } from "child_process";
|
|
1837
|
+
function getHookSafeEnv() {
|
|
1838
|
+
if (!_hookSafeEnv) {
|
|
1839
|
+
_hookSafeEnv = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
|
|
1840
|
+
}
|
|
1841
|
+
return _hookSafeEnv;
|
|
1842
|
+
}
|
|
1843
|
+
function git(args2, options) {
|
|
1844
|
+
return execFileSync10("git", args2, {
|
|
1845
|
+
encoding: "utf-8",
|
|
1846
|
+
timeout: options?.timeout ?? 3e4,
|
|
1847
|
+
cwd: options?.cwd,
|
|
1848
|
+
env: options?.env ?? getHookSafeEnv(),
|
|
1849
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1850
|
+
}).trim();
|
|
1851
|
+
}
|
|
1852
|
+
function deriveBranchName(issueNumber, title) {
|
|
1853
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").slice(0, 50).replace(/-$/, "");
|
|
1854
|
+
return `${issueNumber}-${slug}`;
|
|
1855
|
+
}
|
|
1856
|
+
function getDefaultBranch(cwd) {
|
|
1857
|
+
try {
|
|
1858
|
+
const config = getProjectConfig();
|
|
1859
|
+
if (config.git?.defaultBranch) {
|
|
1860
|
+
return config.git.defaultBranch;
|
|
1861
|
+
}
|
|
1862
|
+
} catch {
|
|
1027
1863
|
}
|
|
1028
|
-
}
|
|
1029
|
-
function createPR(head, base, title, body) {
|
|
1030
1864
|
try {
|
|
1031
|
-
const
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
title,
|
|
1041
|
-
"--body-file",
|
|
1042
|
-
"-"
|
|
1043
|
-
],
|
|
1044
|
-
{ input: body }
|
|
1045
|
-
);
|
|
1046
|
-
const url = output.trim();
|
|
1047
|
-
const match = url.match(/\/pull\/(\d+)$/);
|
|
1048
|
-
const number = match ? parseInt(match[1], 10) : 0;
|
|
1049
|
-
logger.info(` PR created: ${url}`);
|
|
1050
|
-
return { number, url };
|
|
1051
|
-
} catch (err) {
|
|
1052
|
-
const reason = ghErrorMessage(err);
|
|
1053
|
-
logger.error(` Failed to create PR: ${reason}`);
|
|
1054
|
-
return null;
|
|
1865
|
+
const ref = git(["symbolic-ref", "refs/remotes/origin/HEAD"], { cwd });
|
|
1866
|
+
return ref.replace("refs/remotes/origin/", "");
|
|
1867
|
+
} catch {
|
|
1868
|
+
}
|
|
1869
|
+
try {
|
|
1870
|
+
const output = git(["remote", "show", "origin"], { cwd, timeout: 1e4 });
|
|
1871
|
+
const match = output.match(/HEAD branch:\s*(\S+)/);
|
|
1872
|
+
if (match) return match[1];
|
|
1873
|
+
} catch {
|
|
1055
1874
|
}
|
|
1875
|
+
return "dev";
|
|
1056
1876
|
}
|
|
1057
|
-
function
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1877
|
+
function getCurrentBranch(cwd) {
|
|
1878
|
+
return git(["branch", "--show-current"], { cwd });
|
|
1879
|
+
}
|
|
1880
|
+
function ensureFeatureBranch(issueNumber, title, cwd) {
|
|
1881
|
+
const current = getCurrentBranch(cwd);
|
|
1882
|
+
const branchName = deriveBranchName(issueNumber, title);
|
|
1883
|
+
if (current === branchName || current.startsWith(`${issueNumber}-`)) {
|
|
1884
|
+
logger.info(` Already on feature branch: ${current}`);
|
|
1885
|
+
return current;
|
|
1061
1886
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1887
|
+
if (!BASE_BRANCHES.includes(current) && current !== "") {
|
|
1888
|
+
const defaultBranch2 = getDefaultBranch(cwd);
|
|
1889
|
+
logger.info(` Switching from ${current} to ${defaultBranch2} before creating ${branchName}`);
|
|
1064
1890
|
try {
|
|
1065
|
-
|
|
1891
|
+
git(["checkout", defaultBranch2], { cwd });
|
|
1066
1892
|
} catch {
|
|
1893
|
+
logger.warn(` Failed to checkout ${defaultBranch2}, aborting branch creation`);
|
|
1894
|
+
return current;
|
|
1067
1895
|
}
|
|
1068
1896
|
}
|
|
1069
|
-
setLabel(issueNumber, `kody:${phase}`);
|
|
1070
|
-
}
|
|
1071
|
-
function getPRsForIssue(issueNumber) {
|
|
1072
1897
|
try {
|
|
1073
|
-
|
|
1074
|
-
"pr",
|
|
1075
|
-
"list",
|
|
1076
|
-
"--search",
|
|
1077
|
-
`${issueNumber} in:body`,
|
|
1078
|
-
"--json",
|
|
1079
|
-
"number,title,url,headRefName",
|
|
1080
|
-
"--state",
|
|
1081
|
-
"open"
|
|
1082
|
-
]);
|
|
1083
|
-
const prs = JSON.parse(output);
|
|
1084
|
-
const branchPrs = (() => {
|
|
1085
|
-
try {
|
|
1086
|
-
const branchOutput = gh([
|
|
1087
|
-
"pr",
|
|
1088
|
-
"list",
|
|
1089
|
-
"--json",
|
|
1090
|
-
"number,title,url,headRefName",
|
|
1091
|
-
"--state",
|
|
1092
|
-
"open"
|
|
1093
|
-
]);
|
|
1094
|
-
return JSON.parse(branchOutput).filter((pr) => pr.headRefName.startsWith(`${issueNumber}-`));
|
|
1095
|
-
} catch {
|
|
1096
|
-
return [];
|
|
1097
|
-
}
|
|
1098
|
-
})();
|
|
1099
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1100
|
-
const merged = [];
|
|
1101
|
-
for (const pr of [...prs, ...branchPrs]) {
|
|
1102
|
-
if (!seen.has(pr.number)) {
|
|
1103
|
-
seen.add(pr.number);
|
|
1104
|
-
merged.push({ number: pr.number, title: pr.title, url: pr.url, headBranch: pr.headRefName });
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
return merged;
|
|
1898
|
+
git(["fetch", "origin"], { cwd, timeout: 3e4 });
|
|
1108
1899
|
} catch (err) {
|
|
1109
|
-
|
|
1110
|
-
|
|
1900
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1901
|
+
logger.warn(` Failed to fetch origin: ${msg}`);
|
|
1902
|
+
}
|
|
1903
|
+
try {
|
|
1904
|
+
git(["rev-parse", "--verify", `origin/${branchName}`], { cwd });
|
|
1905
|
+
git(["checkout", branchName], { cwd });
|
|
1906
|
+
git(["pull", "origin", branchName], { cwd, timeout: 3e4 });
|
|
1907
|
+
logger.info(` Checked out existing remote branch: ${branchName}`);
|
|
1908
|
+
return branchName;
|
|
1909
|
+
} catch {
|
|
1910
|
+
}
|
|
1911
|
+
try {
|
|
1912
|
+
git(["rev-parse", "--verify", branchName], { cwd });
|
|
1913
|
+
git(["checkout", branchName], { cwd });
|
|
1914
|
+
logger.info(` Checked out existing local branch: ${branchName}`);
|
|
1915
|
+
return branchName;
|
|
1916
|
+
} catch {
|
|
1917
|
+
}
|
|
1918
|
+
const defaultBranch = getDefaultBranch(cwd);
|
|
1919
|
+
try {
|
|
1920
|
+
git(["checkout", "-b", branchName, `origin/${defaultBranch}`], { cwd });
|
|
1921
|
+
} catch {
|
|
1922
|
+
git(["checkout", "-b", branchName], { cwd });
|
|
1111
1923
|
}
|
|
1924
|
+
logger.info(` Created new branch: ${branchName}`);
|
|
1925
|
+
return branchName;
|
|
1112
1926
|
}
|
|
1113
|
-
function
|
|
1927
|
+
function syncWithDefault(cwd, branch) {
|
|
1928
|
+
const defaultBranch = branch ?? getDefaultBranch(cwd);
|
|
1929
|
+
const current = getCurrentBranch(cwd);
|
|
1930
|
+
if (current === defaultBranch) return;
|
|
1114
1931
|
try {
|
|
1115
|
-
|
|
1116
|
-
"pr",
|
|
1117
|
-
"view",
|
|
1118
|
-
String(prNumber),
|
|
1119
|
-
"--json",
|
|
1120
|
-
"title,body,headRefName,baseRefName"
|
|
1121
|
-
]);
|
|
1122
|
-
const data = JSON.parse(output);
|
|
1123
|
-
if (typeof data.title !== "string" || typeof data.headRefName !== "string") {
|
|
1124
|
-
logger.warn(` PR #${prNumber}: unexpected response shape`);
|
|
1125
|
-
return null;
|
|
1126
|
-
}
|
|
1127
|
-
return {
|
|
1128
|
-
title: data.title,
|
|
1129
|
-
body: data.body ?? "",
|
|
1130
|
-
headBranch: data.headRefName,
|
|
1131
|
-
baseBranch: data.baseRefName ?? "main"
|
|
1132
|
-
};
|
|
1932
|
+
git(["fetch", "origin", defaultBranch], { cwd, timeout: 3e4 });
|
|
1133
1933
|
} catch (err) {
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1934
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1935
|
+
logger.warn(` Failed to fetch latest from origin: ${msg}`);
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
try {
|
|
1939
|
+
git(["merge", `origin/${defaultBranch}`, "--no-edit"], { cwd, timeout: 3e4 });
|
|
1940
|
+
logger.info(` Synced with origin/${defaultBranch}`);
|
|
1941
|
+
} catch {
|
|
1942
|
+
try {
|
|
1943
|
+
git(["merge", "--abort"], { cwd });
|
|
1944
|
+
} catch (abortErr) {
|
|
1945
|
+
logger.warn(` Failed to abort merge: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}`);
|
|
1138
1946
|
}
|
|
1139
|
-
|
|
1947
|
+
logger.warn(` Merge conflict with origin/${defaultBranch} \u2014 skipping sync`);
|
|
1140
1948
|
}
|
|
1141
1949
|
}
|
|
1142
|
-
function
|
|
1950
|
+
function mergeDefault(cwd) {
|
|
1951
|
+
const defaultBranch = getDefaultBranch(cwd);
|
|
1952
|
+
const current = getCurrentBranch(cwd);
|
|
1953
|
+
if (current === defaultBranch) return "clean";
|
|
1143
1954
|
try {
|
|
1144
|
-
|
|
1145
|
-
["pr", "comment", String(prNumber), "--body-file", "-"],
|
|
1146
|
-
{ input: body }
|
|
1147
|
-
);
|
|
1148
|
-
logger.info(` Comment posted on PR #${prNumber}`);
|
|
1955
|
+
git(["fetch", "origin", defaultBranch], { cwd, timeout: 3e4 });
|
|
1149
1956
|
} catch (err) {
|
|
1150
|
-
|
|
1957
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1958
|
+
logger.warn(` Failed to fetch latest from origin: ${msg}`);
|
|
1959
|
+
return "error";
|
|
1151
1960
|
}
|
|
1152
|
-
}
|
|
1153
|
-
function submitPRReview(prNumber, body, event) {
|
|
1154
|
-
const flag = event === "approve" ? "--approve" : "--request-changes";
|
|
1155
1961
|
try {
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1962
|
+
git(["merge", `origin/${defaultBranch}`, "--no-edit"], { cwd, timeout: 3e4 });
|
|
1963
|
+
logger.info(` Merged origin/${defaultBranch} cleanly`);
|
|
1964
|
+
return "clean";
|
|
1965
|
+
} catch {
|
|
1966
|
+
try {
|
|
1967
|
+
const unmerged = git(["diff", "--name-only", "--diff-filter=U"], { cwd });
|
|
1968
|
+
if (unmerged.trim()) return "conflict";
|
|
1969
|
+
} catch {
|
|
1970
|
+
}
|
|
1971
|
+
try {
|
|
1972
|
+
git(["merge", "--abort"], { cwd });
|
|
1973
|
+
} catch (abortErr) {
|
|
1974
|
+
logger.warn(` Failed to abort merge: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}`);
|
|
1975
|
+
}
|
|
1976
|
+
return "error";
|
|
1165
1977
|
}
|
|
1166
1978
|
}
|
|
1167
|
-
function
|
|
1979
|
+
function getConflictedFiles(cwd) {
|
|
1168
1980
|
try {
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
"--log-failed"
|
|
1174
|
-
]);
|
|
1175
|
-
if (!logsOutput) return null;
|
|
1176
|
-
const truncated = logsOutput.slice(-maxLength);
|
|
1177
|
-
const prefix = logsOutput.length > maxLength ? "...(earlier output truncated)\n" : "";
|
|
1178
|
-
return `${prefix}${truncated}`;
|
|
1179
|
-
} catch (err) {
|
|
1180
|
-
logger.warn(` Failed to get CI failure logs for run ${runId}: ${ghErrorMessage(err)}`);
|
|
1181
|
-
return null;
|
|
1981
|
+
const output = git(["diff", "--name-only", "--diff-filter=U"], { cwd });
|
|
1982
|
+
return output ? output.split("\n") : [];
|
|
1983
|
+
} catch {
|
|
1984
|
+
return [];
|
|
1182
1985
|
}
|
|
1183
1986
|
}
|
|
1184
|
-
function
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
"list",
|
|
1189
|
-
"--branch",
|
|
1190
|
-
branch,
|
|
1191
|
-
"--status",
|
|
1192
|
-
"failure",
|
|
1193
|
-
"--limit",
|
|
1194
|
-
"1",
|
|
1195
|
-
"--json",
|
|
1196
|
-
"databaseId",
|
|
1197
|
-
"--jq",
|
|
1198
|
-
".[0].databaseId"
|
|
1199
|
-
]);
|
|
1200
|
-
return output.trim() || null;
|
|
1201
|
-
} catch (err) {
|
|
1202
|
-
logger.warn(` Failed to get latest failed run for branch ${branch}: ${ghErrorMessage(err)}`);
|
|
1203
|
-
return null;
|
|
1987
|
+
function commitAll(message, cwd) {
|
|
1988
|
+
const status = git(["status", "--porcelain"], { cwd });
|
|
1989
|
+
if (!status) {
|
|
1990
|
+
return { success: false, hash: "", message: "No changes to commit" };
|
|
1204
1991
|
}
|
|
1992
|
+
git(["add", "."], { cwd });
|
|
1993
|
+
git(["commit", "--no-gpg-sign", "-m", message], { cwd });
|
|
1994
|
+
const hash = git(["rev-parse", "HEAD"], { cwd }).slice(0, 7);
|
|
1995
|
+
logger.info(` Committed: ${hash} ${message}`);
|
|
1996
|
+
return { success: true, hash, message };
|
|
1205
1997
|
}
|
|
1206
|
-
function
|
|
1998
|
+
function getDiffFiles(baseBranch, cwd) {
|
|
1207
1999
|
try {
|
|
1208
|
-
const output =
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
"--jq",
|
|
1212
|
-
'[.[] | select(.body | test("Kody Review"))] | last | .body'
|
|
1213
|
-
]);
|
|
1214
|
-
return output.trim() || null;
|
|
2000
|
+
const output = git(["diff", "--name-only", `origin/${baseBranch}...HEAD`], { cwd });
|
|
2001
|
+
if (!output) return [];
|
|
2002
|
+
return output.split("\n").filter((f) => f && !f.startsWith(".kody/"));
|
|
1215
2003
|
} catch (err) {
|
|
1216
|
-
|
|
1217
|
-
|
|
2004
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2005
|
+
logger.warn(` Failed to get diff files: ${msg}`);
|
|
2006
|
+
return [];
|
|
1218
2007
|
}
|
|
1219
2008
|
}
|
|
1220
|
-
function
|
|
2009
|
+
function pushBranch(cwd) {
|
|
1221
2010
|
try {
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
"[.[] | {body, created_at, user_login: .user.login, user_type: .user.type}]"
|
|
1227
|
-
]);
|
|
1228
|
-
const issueComments = issueCommentsRaw ? JSON.parse(issueCommentsRaw) : [];
|
|
1229
|
-
const reviewCommentsRaw = gh([
|
|
1230
|
-
"api",
|
|
1231
|
-
`repos/{owner}/{repo}/pulls/${prNumber}/comments`,
|
|
1232
|
-
"--jq",
|
|
1233
|
-
"[.[] | {body, created_at, user_login: .user.login, user_type: .user.type, path, line}]"
|
|
1234
|
-
]);
|
|
1235
|
-
const reviewComments = reviewCommentsRaw ? JSON.parse(reviewCommentsRaw) : [];
|
|
1236
|
-
const kodyTimestamp = findLastKodyActionTimestamp(issueComments);
|
|
1237
|
-
const humanIssueComments = issueComments.filter(
|
|
1238
|
-
(c) => !isKodyComment(c) && (!kodyTimestamp || c.created_at > kodyTimestamp)
|
|
1239
|
-
);
|
|
1240
|
-
const humanReviewComments = reviewComments.filter(
|
|
1241
|
-
(c) => !isKodyComment(c) && (!kodyTimestamp || c.created_at > kodyTimestamp)
|
|
1242
|
-
);
|
|
1243
|
-
if (humanIssueComments.length === 0 && humanReviewComments.length === 0) {
|
|
1244
|
-
return null;
|
|
1245
|
-
}
|
|
1246
|
-
const parts = [];
|
|
1247
|
-
if (humanIssueComments.length > 0) {
|
|
1248
|
-
parts.push("### PR Comments");
|
|
1249
|
-
for (const c of humanIssueComments) {
|
|
1250
|
-
parts.push(`**@${c.user_login}:**
|
|
1251
|
-
${c.body}`);
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
if (humanReviewComments.length > 0) {
|
|
1255
|
-
parts.push("### Code Review Comments");
|
|
1256
|
-
for (const c of humanReviewComments) {
|
|
1257
|
-
const location = c.path ? `\`${c.path}${c.line ? `:${c.line}` : ""}\`` : "";
|
|
1258
|
-
parts.push(`**@${c.user_login}** ${location}:
|
|
1259
|
-
${c.body}`);
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
return parts.join("\n\n");
|
|
1263
|
-
} catch (err) {
|
|
1264
|
-
logger.warn(` Failed to get PR feedback for #${prNumber}: ${err}`);
|
|
1265
|
-
return null;
|
|
2011
|
+
git(["push", "-u", "origin", "HEAD"], { cwd, timeout: 12e4 });
|
|
2012
|
+
} catch {
|
|
2013
|
+
logger.info(" Push rejected (non-fast-forward), retrying with --force-with-lease");
|
|
2014
|
+
git(["push", "--force-with-lease", "-u", "origin", "HEAD"], { cwd, timeout: 12e4 });
|
|
1266
2015
|
}
|
|
2016
|
+
logger.info(" Pushed to origin");
|
|
1267
2017
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
}
|
|
1272
|
-
function findLastKodyActionTimestamp(comments) {
|
|
1273
|
-
const kodyComments = comments.filter(isKodyComment);
|
|
1274
|
-
if (kodyComments.length === 0) return null;
|
|
1275
|
-
return kodyComments[kodyComments.length - 1].created_at;
|
|
1276
|
-
}
|
|
1277
|
-
var API_TIMEOUT_MS, LIFECYCLE_LABELS, _ghCwd, KODY_MARKERS;
|
|
1278
|
-
var init_github_api = __esm({
|
|
1279
|
-
"src/github-api.ts"() {
|
|
2018
|
+
var BASE_BRANCHES, _hookSafeEnv;
|
|
2019
|
+
var init_git_utils = __esm({
|
|
2020
|
+
"src/git-utils.ts"() {
|
|
1280
2021
|
"use strict";
|
|
1281
2022
|
init_logger();
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
"Kody Review",
|
|
1286
|
-
"\u{1F916} Generated by Kody",
|
|
1287
|
-
"Kody pipeline started",
|
|
1288
|
-
"Fix pushed to PR",
|
|
1289
|
-
"PR created:",
|
|
1290
|
-
"Pipeline failed at",
|
|
1291
|
-
"Pipeline already running",
|
|
1292
|
-
"already completed"
|
|
1293
|
-
];
|
|
2023
|
+
init_config();
|
|
2024
|
+
BASE_BRANCHES = ["dev", "main", "master"];
|
|
2025
|
+
_hookSafeEnv = null;
|
|
1294
2026
|
}
|
|
1295
2027
|
});
|
|
1296
2028
|
|
|
1297
2029
|
// src/pipeline/state.ts
|
|
1298
|
-
import * as
|
|
1299
|
-
import * as
|
|
2030
|
+
import * as fs13 from "fs";
|
|
2031
|
+
import * as path11 from "path";
|
|
1300
2032
|
function loadState(taskId, taskDir) {
|
|
1301
|
-
const p =
|
|
1302
|
-
if (!
|
|
2033
|
+
const p = path11.join(taskDir, "status.json");
|
|
2034
|
+
if (!fs13.existsSync(p)) return null;
|
|
1303
2035
|
try {
|
|
1304
2036
|
const result = parseJsonSafe(
|
|
1305
|
-
|
|
2037
|
+
fs13.readFileSync(p, "utf-8"),
|
|
1306
2038
|
["taskId", "state", "stages", "createdAt", "updatedAt"]
|
|
1307
2039
|
);
|
|
1308
2040
|
if (!result.ok) {
|
|
@@ -1320,10 +2052,10 @@ function writeState(state, taskDir) {
|
|
|
1320
2052
|
...state,
|
|
1321
2053
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1322
2054
|
};
|
|
1323
|
-
const target =
|
|
2055
|
+
const target = path11.join(taskDir, "status.json");
|
|
1324
2056
|
const tmp = target + ".tmp";
|
|
1325
|
-
|
|
1326
|
-
|
|
2057
|
+
fs13.writeFileSync(tmp, JSON.stringify(updated, null, 2));
|
|
2058
|
+
fs13.renameSync(tmp, target);
|
|
1327
2059
|
return updated;
|
|
1328
2060
|
}
|
|
1329
2061
|
function initState(taskId) {
|
|
@@ -1364,16 +2096,16 @@ var init_complexity = __esm({
|
|
|
1364
2096
|
});
|
|
1365
2097
|
|
|
1366
2098
|
// src/memory.ts
|
|
1367
|
-
import * as
|
|
1368
|
-
import * as
|
|
2099
|
+
import * as fs14 from "fs";
|
|
2100
|
+
import * as path12 from "path";
|
|
1369
2101
|
function readProjectMemory(projectDir) {
|
|
1370
|
-
const memoryDir =
|
|
1371
|
-
if (!
|
|
1372
|
-
const files =
|
|
2102
|
+
const memoryDir = path12.join(projectDir, ".kody", "memory");
|
|
2103
|
+
if (!fs14.existsSync(memoryDir)) return "";
|
|
2104
|
+
const files = fs14.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
1373
2105
|
if (files.length === 0) return "";
|
|
1374
2106
|
const sections = [];
|
|
1375
2107
|
for (const file of files) {
|
|
1376
|
-
const content =
|
|
2108
|
+
const content = fs14.readFileSync(path12.join(memoryDir, file), "utf-8").trim();
|
|
1377
2109
|
if (content) {
|
|
1378
2110
|
sections.push(`## ${file.replace(".md", "")}
|
|
1379
2111
|
${content}`);
|
|
@@ -1392,8 +2124,8 @@ var init_memory = __esm({
|
|
|
1392
2124
|
});
|
|
1393
2125
|
|
|
1394
2126
|
// src/context-tiers.ts
|
|
1395
|
-
import * as
|
|
1396
|
-
import * as
|
|
2127
|
+
import * as fs15 from "fs";
|
|
2128
|
+
import * as path13 from "path";
|
|
1397
2129
|
function estimateTokens(text) {
|
|
1398
2130
|
return Math.ceil(text.length / 4);
|
|
1399
2131
|
}
|
|
@@ -1484,7 +2216,7 @@ function generateL1Json(content) {
|
|
|
1484
2216
|
}
|
|
1485
2217
|
}
|
|
1486
2218
|
function getTieredContent(filePath, content) {
|
|
1487
|
-
const key =
|
|
2219
|
+
const key = path13.basename(filePath);
|
|
1488
2220
|
return {
|
|
1489
2221
|
source: filePath,
|
|
1490
2222
|
L0: generateL0(content, key),
|
|
@@ -1496,15 +2228,15 @@ function selectTier(tiered, tier) {
|
|
|
1496
2228
|
return tiered[tier];
|
|
1497
2229
|
}
|
|
1498
2230
|
function readProjectMemoryTiered(projectDir, tier) {
|
|
1499
|
-
const memoryDir =
|
|
1500
|
-
if (!
|
|
1501
|
-
const files =
|
|
2231
|
+
const memoryDir = path13.join(projectDir, ".kody", "memory");
|
|
2232
|
+
if (!fs15.existsSync(memoryDir)) return "";
|
|
2233
|
+
const files = fs15.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
1502
2234
|
if (files.length === 0) return "";
|
|
1503
2235
|
const tierLabel2 = tier === "L2" ? "full" : tier === "L1" ? "overview" : "abstract";
|
|
1504
2236
|
const sections = [];
|
|
1505
2237
|
for (const file of files) {
|
|
1506
|
-
const filePath =
|
|
1507
|
-
const content =
|
|
2238
|
+
const filePath = path13.join(memoryDir, file);
|
|
2239
|
+
const content = fs15.readFileSync(filePath, "utf-8").trim();
|
|
1508
2240
|
if (!content) continue;
|
|
1509
2241
|
const tiered = getTieredContent(filePath, content);
|
|
1510
2242
|
const selected = selectTier(tiered, tier);
|
|
@@ -1527,9 +2259,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
|
|
|
1527
2259
|
`;
|
|
1528
2260
|
context += `Task Directory: ${taskDir}
|
|
1529
2261
|
`;
|
|
1530
|
-
const taskMdPath =
|
|
1531
|
-
if (
|
|
1532
|
-
const content =
|
|
2262
|
+
const taskMdPath = path13.join(taskDir, "task.md");
|
|
2263
|
+
if (fs15.existsSync(taskMdPath)) {
|
|
2264
|
+
const content = fs15.readFileSync(taskMdPath, "utf-8");
|
|
1533
2265
|
const selected = selectContent(taskMdPath, content, policy.taskDescription);
|
|
1534
2266
|
const label = tierLabel("Task Description", policy.taskDescription);
|
|
1535
2267
|
context += `
|
|
@@ -1537,9 +2269,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
|
|
|
1537
2269
|
${selected}
|
|
1538
2270
|
`;
|
|
1539
2271
|
}
|
|
1540
|
-
const taskJsonPath =
|
|
1541
|
-
if (
|
|
1542
|
-
const content =
|
|
2272
|
+
const taskJsonPath = path13.join(taskDir, "task.json");
|
|
2273
|
+
if (fs15.existsSync(taskJsonPath)) {
|
|
2274
|
+
const content = fs15.readFileSync(taskJsonPath, "utf-8");
|
|
1543
2275
|
if (policy.taskClassification === "L2") {
|
|
1544
2276
|
try {
|
|
1545
2277
|
const taskDef = JSON.parse(content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, ""));
|
|
@@ -1565,9 +2297,9 @@ ${selected}
|
|
|
1565
2297
|
}
|
|
1566
2298
|
}
|
|
1567
2299
|
}
|
|
1568
|
-
const specPath =
|
|
1569
|
-
if (
|
|
1570
|
-
const content =
|
|
2300
|
+
const specPath = path13.join(taskDir, "spec.md");
|
|
2301
|
+
if (fs15.existsSync(specPath)) {
|
|
2302
|
+
const content = fs15.readFileSync(specPath, "utf-8");
|
|
1571
2303
|
const selected = selectContent(specPath, content, policy.spec);
|
|
1572
2304
|
const label = tierLabel("Spec", policy.spec);
|
|
1573
2305
|
context += `
|
|
@@ -1575,9 +2307,9 @@ ${selected}
|
|
|
1575
2307
|
${selected}
|
|
1576
2308
|
`;
|
|
1577
2309
|
}
|
|
1578
|
-
const planPath =
|
|
1579
|
-
if (
|
|
1580
|
-
const content =
|
|
2310
|
+
const planPath = path13.join(taskDir, "plan.md");
|
|
2311
|
+
if (fs15.existsSync(planPath)) {
|
|
2312
|
+
const content = fs15.readFileSync(planPath, "utf-8");
|
|
1581
2313
|
const selected = selectContent(planPath, content, policy.plan);
|
|
1582
2314
|
const label = tierLabel("Plan", policy.plan);
|
|
1583
2315
|
context += `
|
|
@@ -1585,9 +2317,9 @@ ${selected}
|
|
|
1585
2317
|
${selected}
|
|
1586
2318
|
`;
|
|
1587
2319
|
}
|
|
1588
|
-
const contextMdPath =
|
|
1589
|
-
if (
|
|
1590
|
-
const content =
|
|
2320
|
+
const contextMdPath = path13.join(taskDir, "context.md");
|
|
2321
|
+
if (fs15.existsSync(contextMdPath)) {
|
|
2322
|
+
const content = fs15.readFileSync(contextMdPath, "utf-8");
|
|
1591
2323
|
const selected = selectContent(contextMdPath, content, policy.accumulatedContext);
|
|
1592
2324
|
const label = tierLabel("Previous Stage Context", policy.accumulatedContext);
|
|
1593
2325
|
context += `
|
|
@@ -1672,71 +2404,25 @@ var init_context_tiers = __esm({
|
|
|
1672
2404
|
}
|
|
1673
2405
|
});
|
|
1674
2406
|
|
|
1675
|
-
// src/mcp-config.ts
|
|
1676
|
-
function withPlaywrightIfNeeded(mcpConfig, hasUI) {
|
|
1677
|
-
if (!mcpConfig?.enabled || !hasUI) return mcpConfig;
|
|
1678
|
-
const hasPlaywright = Object.keys(mcpConfig.servers).some(
|
|
1679
|
-
(name) => name.toLowerCase().includes("playwright")
|
|
1680
|
-
);
|
|
1681
|
-
if (hasPlaywright) return mcpConfig;
|
|
1682
|
-
return {
|
|
1683
|
-
...mcpConfig,
|
|
1684
|
-
servers: {
|
|
1685
|
-
...mcpConfig.servers,
|
|
1686
|
-
playwright: PLAYWRIGHT_SERVER
|
|
1687
|
-
}
|
|
1688
|
-
};
|
|
1689
|
-
}
|
|
1690
|
-
function buildMcpConfigJson(mcpConfig) {
|
|
1691
|
-
if (!mcpConfig?.enabled) return void 0;
|
|
1692
|
-
if (Object.keys(mcpConfig.servers).length === 0) return void 0;
|
|
1693
|
-
const config = { mcpServers: {} };
|
|
1694
|
-
const mcpServers = config.mcpServers;
|
|
1695
|
-
for (const [name, server] of Object.entries(mcpConfig.servers)) {
|
|
1696
|
-
mcpServers[name] = {
|
|
1697
|
-
command: server.command,
|
|
1698
|
-
args: server.args ?? [],
|
|
1699
|
-
...server.env ? { env: server.env } : {}
|
|
1700
|
-
};
|
|
1701
|
-
}
|
|
1702
|
-
return JSON.stringify(config);
|
|
1703
|
-
}
|
|
1704
|
-
function isMcpEnabledForStage(stageName, mcpConfig) {
|
|
1705
|
-
if (!mcpConfig?.enabled) return false;
|
|
1706
|
-
const allowedStages = mcpConfig.stages ?? DEFAULT_MCP_STAGES;
|
|
1707
|
-
return allowedStages.includes(stageName);
|
|
1708
|
-
}
|
|
1709
|
-
var DEFAULT_MCP_STAGES, PLAYWRIGHT_SERVER;
|
|
1710
|
-
var init_mcp_config = __esm({
|
|
1711
|
-
"src/mcp-config.ts"() {
|
|
1712
|
-
"use strict";
|
|
1713
|
-
DEFAULT_MCP_STAGES = ["build", "verify", "review", "review-fix"];
|
|
1714
|
-
PLAYWRIGHT_SERVER = {
|
|
1715
|
-
command: "npx",
|
|
1716
|
-
args: ["-y", "@anthropic-ai/mcp-playwright"]
|
|
1717
|
-
};
|
|
1718
|
-
}
|
|
1719
|
-
});
|
|
1720
|
-
|
|
1721
2407
|
// src/context.ts
|
|
1722
|
-
import * as
|
|
1723
|
-
import * as
|
|
2408
|
+
import * as fs16 from "fs";
|
|
2409
|
+
import * as path14 from "path";
|
|
1724
2410
|
function readPromptFile(stageName, projectDir) {
|
|
1725
2411
|
if (projectDir) {
|
|
1726
|
-
const stepFile =
|
|
1727
|
-
if (
|
|
1728
|
-
return
|
|
2412
|
+
const stepFile = path14.join(projectDir, ".kody", "steps", `${stageName}.md`);
|
|
2413
|
+
if (fs16.existsSync(stepFile)) {
|
|
2414
|
+
return fs16.readFileSync(stepFile, "utf-8");
|
|
1729
2415
|
}
|
|
1730
2416
|
console.warn(` \u26A0 No step file at ${stepFile}, falling back to engine defaults. Run 'kody-engine-lite init --force' to generate step files.`);
|
|
1731
2417
|
}
|
|
1732
2418
|
const scriptDir = new URL(".", import.meta.url).pathname;
|
|
1733
2419
|
const candidates = [
|
|
1734
|
-
|
|
1735
|
-
|
|
2420
|
+
path14.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
|
|
2421
|
+
path14.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
|
|
1736
2422
|
];
|
|
1737
2423
|
for (const candidate of candidates) {
|
|
1738
|
-
if (
|
|
1739
|
-
return
|
|
2424
|
+
if (fs16.existsSync(candidate)) {
|
|
2425
|
+
return fs16.readFileSync(candidate, "utf-8");
|
|
1740
2426
|
}
|
|
1741
2427
|
}
|
|
1742
2428
|
throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
|
|
@@ -1748,18 +2434,18 @@ function injectTaskContext(prompt, taskId, taskDir, feedback) {
|
|
|
1748
2434
|
`;
|
|
1749
2435
|
context += `Task Directory: ${taskDir}
|
|
1750
2436
|
`;
|
|
1751
|
-
const taskMdPath =
|
|
1752
|
-
if (
|
|
1753
|
-
const taskMd =
|
|
2437
|
+
const taskMdPath = path14.join(taskDir, "task.md");
|
|
2438
|
+
if (fs16.existsSync(taskMdPath)) {
|
|
2439
|
+
const taskMd = fs16.readFileSync(taskMdPath, "utf-8");
|
|
1754
2440
|
context += `
|
|
1755
2441
|
## Task Description
|
|
1756
2442
|
${taskMd}
|
|
1757
2443
|
`;
|
|
1758
2444
|
}
|
|
1759
|
-
const taskJsonPath =
|
|
1760
|
-
if (
|
|
2445
|
+
const taskJsonPath = path14.join(taskDir, "task.json");
|
|
2446
|
+
if (fs16.existsSync(taskJsonPath)) {
|
|
1761
2447
|
try {
|
|
1762
|
-
const taskDef = JSON.parse(
|
|
2448
|
+
const taskDef = JSON.parse(fs16.readFileSync(taskJsonPath, "utf-8"));
|
|
1763
2449
|
context += `
|
|
1764
2450
|
## Task Classification
|
|
1765
2451
|
`;
|
|
@@ -1772,27 +2458,27 @@ ${taskMd}
|
|
|
1772
2458
|
} catch {
|
|
1773
2459
|
}
|
|
1774
2460
|
}
|
|
1775
|
-
const specPath =
|
|
1776
|
-
if (
|
|
1777
|
-
const spec =
|
|
2461
|
+
const specPath = path14.join(taskDir, "spec.md");
|
|
2462
|
+
if (fs16.existsSync(specPath)) {
|
|
2463
|
+
const spec = fs16.readFileSync(specPath, "utf-8");
|
|
1778
2464
|
const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
|
|
1779
2465
|
context += `
|
|
1780
2466
|
## Spec Summary
|
|
1781
2467
|
${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
|
|
1782
2468
|
`;
|
|
1783
2469
|
}
|
|
1784
|
-
const planPath =
|
|
1785
|
-
if (
|
|
1786
|
-
const plan =
|
|
2470
|
+
const planPath = path14.join(taskDir, "plan.md");
|
|
2471
|
+
if (fs16.existsSync(planPath)) {
|
|
2472
|
+
const plan = fs16.readFileSync(planPath, "utf-8");
|
|
1787
2473
|
const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
|
|
1788
2474
|
context += `
|
|
1789
2475
|
## Plan Summary
|
|
1790
2476
|
${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
|
|
1791
2477
|
`;
|
|
1792
2478
|
}
|
|
1793
|
-
const contextMdPath =
|
|
1794
|
-
if (
|
|
1795
|
-
const accumulated =
|
|
2479
|
+
const contextMdPath = path14.join(taskDir, "context.md");
|
|
2480
|
+
if (fs16.existsSync(contextMdPath)) {
|
|
2481
|
+
const accumulated = fs16.readFileSync(contextMdPath, "utf-8");
|
|
1796
2482
|
const truncated = accumulated.slice(-MAX_ACCUMULATED_CONTEXT);
|
|
1797
2483
|
const prefix = accumulated.length > MAX_ACCUMULATED_CONTEXT ? "...(earlier context truncated)\n" : "";
|
|
1798
2484
|
context += `
|
|
@@ -1810,17 +2496,17 @@ ${feedback}
|
|
|
1810
2496
|
}
|
|
1811
2497
|
function inferHasUIFromScope(scope) {
|
|
1812
2498
|
return scope.some((filePath) => {
|
|
1813
|
-
const ext =
|
|
2499
|
+
const ext = path14.extname(filePath).toLowerCase();
|
|
1814
2500
|
if (UI_EXTENSIONS.has(ext)) return true;
|
|
1815
2501
|
const normalized = filePath.replace(/\\/g, "/");
|
|
1816
2502
|
return UI_PATH_SEGMENTS.some((seg) => normalized.includes(seg));
|
|
1817
2503
|
});
|
|
1818
2504
|
}
|
|
1819
2505
|
function taskHasUI(taskDir) {
|
|
1820
|
-
const taskJsonPath =
|
|
1821
|
-
if (!
|
|
2506
|
+
const taskJsonPath = path14.join(taskDir, "task.json");
|
|
2507
|
+
if (!fs16.existsSync(taskJsonPath)) return true;
|
|
1822
2508
|
try {
|
|
1823
|
-
const taskDef = JSON.parse(
|
|
2509
|
+
const taskDef = JSON.parse(fs16.readFileSync(taskJsonPath, "utf-8"));
|
|
1824
2510
|
const scope = Array.isArray(taskDef.scope) ? taskDef.scope : [];
|
|
1825
2511
|
if (scope.length === 0) return true;
|
|
1826
2512
|
return inferHasUIFromScope(scope);
|
|
@@ -1942,9 +2628,9 @@ ${prompt}` : prompt;
|
|
|
1942
2628
|
}
|
|
1943
2629
|
if (isMcpEnabledForStage(stageName, config.mcp) && taskHasUI(taskDir)) {
|
|
1944
2630
|
assembled = assembled + "\n\n" + getBrowserToolGuidance(stageName, taskDir);
|
|
1945
|
-
const qaGuidePath =
|
|
1946
|
-
if (
|
|
1947
|
-
const qaGuide =
|
|
2631
|
+
const qaGuidePath = path14.join(projectDir, ".kody", "qa-guide.md");
|
|
2632
|
+
if (fs16.existsSync(qaGuidePath)) {
|
|
2633
|
+
const qaGuide = fs16.readFileSync(qaGuidePath, "utf-8").trim();
|
|
1948
2634
|
assembled = assembled + "\n\n" + qaGuide;
|
|
1949
2635
|
}
|
|
1950
2636
|
}
|
|
@@ -1977,7 +2663,7 @@ function resolveModel(modelTier, stageName) {
|
|
|
1977
2663
|
if (mapped) return mapped;
|
|
1978
2664
|
return DEFAULT_MODEL_MAP[modelTier] ?? "sonnet";
|
|
1979
2665
|
}
|
|
1980
|
-
var
|
|
2666
|
+
var MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT, UI_EXTENSIONS, UI_PATH_SEGMENTS, TIER_ESCALATION, DEFAULT_MODEL_MAP;
|
|
1981
2667
|
var init_context = __esm({
|
|
1982
2668
|
"src/context.ts"() {
|
|
1983
2669
|
"use strict";
|
|
@@ -1985,11 +2671,6 @@ var init_context = __esm({
|
|
|
1985
2671
|
init_config();
|
|
1986
2672
|
init_context_tiers();
|
|
1987
2673
|
init_mcp_config();
|
|
1988
|
-
DEFAULT_MODEL_MAP = {
|
|
1989
|
-
cheap: "haiku",
|
|
1990
|
-
mid: "sonnet",
|
|
1991
|
-
strong: "opus"
|
|
1992
|
-
};
|
|
1993
2674
|
MAX_TASK_CONTEXT_PLAN = 1500;
|
|
1994
2675
|
MAX_TASK_CONTEXT_SPEC = 2e3;
|
|
1995
2676
|
MAX_ACCUMULATED_CONTEXT = 4e3;
|
|
@@ -2016,6 +2697,11 @@ var init_context = __esm({
|
|
|
2016
2697
|
mid: "strong",
|
|
2017
2698
|
strong: "strong"
|
|
2018
2699
|
};
|
|
2700
|
+
DEFAULT_MODEL_MAP = {
|
|
2701
|
+
cheap: "haiku",
|
|
2702
|
+
mid: "sonnet",
|
|
2703
|
+
strong: "opus"
|
|
2704
|
+
};
|
|
2019
2705
|
}
|
|
2020
2706
|
});
|
|
2021
2707
|
|
|
@@ -2039,8 +2725,8 @@ var init_runner_selection = __esm({
|
|
|
2039
2725
|
});
|
|
2040
2726
|
|
|
2041
2727
|
// src/stages/agent.ts
|
|
2042
|
-
import * as
|
|
2043
|
-
import * as
|
|
2728
|
+
import * as fs17 from "fs";
|
|
2729
|
+
import * as path15 from "path";
|
|
2044
2730
|
function getSessionInfo(stageName, sessions) {
|
|
2045
2731
|
const group = SESSION_GROUP[stageName];
|
|
2046
2732
|
if (!group) return void 0;
|
|
@@ -2127,27 +2813,27 @@ async function executeAgentStage(ctx, def) {
|
|
|
2127
2813
|
}
|
|
2128
2814
|
const result = lastResult;
|
|
2129
2815
|
if (def.outputFile && result.output) {
|
|
2130
|
-
|
|
2816
|
+
fs17.writeFileSync(path15.join(ctx.taskDir, def.outputFile), result.output);
|
|
2131
2817
|
}
|
|
2132
2818
|
if (def.outputFile) {
|
|
2133
|
-
const outputPath =
|
|
2134
|
-
if (!
|
|
2135
|
-
const ext =
|
|
2136
|
-
const base =
|
|
2137
|
-
const files =
|
|
2819
|
+
const outputPath = path15.join(ctx.taskDir, def.outputFile);
|
|
2820
|
+
if (!fs17.existsSync(outputPath)) {
|
|
2821
|
+
const ext = path15.extname(def.outputFile);
|
|
2822
|
+
const base = path15.basename(def.outputFile, ext);
|
|
2823
|
+
const files = fs17.readdirSync(ctx.taskDir);
|
|
2138
2824
|
const variant = files.find(
|
|
2139
2825
|
(f) => f.startsWith(base + "-") && f.endsWith(ext)
|
|
2140
2826
|
);
|
|
2141
2827
|
if (variant) {
|
|
2142
|
-
|
|
2828
|
+
fs17.renameSync(path15.join(ctx.taskDir, variant), outputPath);
|
|
2143
2829
|
logger.info(` Renamed variant ${variant} \u2192 ${def.outputFile}`);
|
|
2144
2830
|
}
|
|
2145
2831
|
}
|
|
2146
2832
|
}
|
|
2147
2833
|
if (def.outputFile) {
|
|
2148
|
-
const outputPath =
|
|
2149
|
-
if (
|
|
2150
|
-
const content =
|
|
2834
|
+
const outputPath = path15.join(ctx.taskDir, def.outputFile);
|
|
2835
|
+
if (fs17.existsSync(outputPath)) {
|
|
2836
|
+
const content = fs17.readFileSync(outputPath, "utf-8");
|
|
2151
2837
|
const validation = validateStageOutput(def.name, content);
|
|
2152
2838
|
if (!validation.valid) {
|
|
2153
2839
|
if (def.name === "taskify") {
|
|
@@ -2161,7 +2847,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
2161
2847
|
const stripped = stripFences(retryResult.output);
|
|
2162
2848
|
const retryValidation = validateTaskJson(stripped);
|
|
2163
2849
|
if (retryValidation.valid) {
|
|
2164
|
-
|
|
2850
|
+
fs17.writeFileSync(outputPath, retryResult.output);
|
|
2165
2851
|
logger.info(` taskify retry produced valid JSON`);
|
|
2166
2852
|
} else {
|
|
2167
2853
|
logger.warn(` taskify retry still invalid: ${retryValidation.error}`);
|
|
@@ -2174,7 +2860,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
2174
2860
|
risk_level: "low",
|
|
2175
2861
|
questions: []
|
|
2176
2862
|
}, null, 2);
|
|
2177
|
-
|
|
2863
|
+
fs17.writeFileSync(outputPath, fallback);
|
|
2178
2864
|
logger.info(` taskify fallback: generated minimal task.json (risk_level=low)`);
|
|
2179
2865
|
}
|
|
2180
2866
|
}
|
|
@@ -2188,7 +2874,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
2188
2874
|
return { outcome: "completed", outputFile: def.outputFile, retries };
|
|
2189
2875
|
}
|
|
2190
2876
|
function appendStageContext(taskDir, stageName, output) {
|
|
2191
|
-
const contextPath =
|
|
2877
|
+
const contextPath = path15.join(taskDir, "context.md");
|
|
2192
2878
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19);
|
|
2193
2879
|
let summary;
|
|
2194
2880
|
if (output && output.trim()) {
|
|
@@ -2201,7 +2887,7 @@ function appendStageContext(taskDir, stageName, output) {
|
|
|
2201
2887
|
### ${stageName} (${timestamp2})
|
|
2202
2888
|
${summary}
|
|
2203
2889
|
`;
|
|
2204
|
-
|
|
2890
|
+
fs17.appendFileSync(contextPath, entry);
|
|
2205
2891
|
}
|
|
2206
2892
|
var SESSION_GROUP;
|
|
2207
2893
|
var init_agent = __esm({
|
|
@@ -2224,7 +2910,7 @@ var init_agent = __esm({
|
|
|
2224
2910
|
});
|
|
2225
2911
|
|
|
2226
2912
|
// src/verify-runner.ts
|
|
2227
|
-
import { execFileSync as
|
|
2913
|
+
import { execFileSync as execFileSync11 } from "child_process";
|
|
2228
2914
|
function isExecError(err) {
|
|
2229
2915
|
return typeof err === "object" && err !== null;
|
|
2230
2916
|
}
|
|
@@ -2260,7 +2946,7 @@ function runCommand(cmd, cwd, timeout) {
|
|
|
2260
2946
|
return { success: true, output: "", timedOut: false };
|
|
2261
2947
|
}
|
|
2262
2948
|
try {
|
|
2263
|
-
const output =
|
|
2949
|
+
const output = execFileSync11(parts[0], parts.slice(1), {
|
|
2264
2950
|
cwd,
|
|
2265
2951
|
timeout,
|
|
2266
2952
|
encoding: "utf-8",
|
|
@@ -2331,7 +3017,7 @@ var init_verify_runner = __esm({
|
|
|
2331
3017
|
});
|
|
2332
3018
|
|
|
2333
3019
|
// src/observer.ts
|
|
2334
|
-
import { execFileSync as
|
|
3020
|
+
import { execFileSync as execFileSync12 } from "child_process";
|
|
2335
3021
|
async function diagnoseFailure(stageName, errorOutput, modifiedFiles, runner, model, options) {
|
|
2336
3022
|
const context = [
|
|
2337
3023
|
`Stage: ${stageName}`,
|
|
@@ -2391,13 +3077,13 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
|
|
|
2391
3077
|
}
|
|
2392
3078
|
function getModifiedFiles(projectDir) {
|
|
2393
3079
|
try {
|
|
2394
|
-
const staged =
|
|
3080
|
+
const staged = execFileSync12("git", ["diff", "--name-only", "--cached"], {
|
|
2395
3081
|
encoding: "utf-8",
|
|
2396
3082
|
cwd: projectDir,
|
|
2397
3083
|
timeout: 5e3,
|
|
2398
3084
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2399
3085
|
}).trim();
|
|
2400
|
-
const unstaged =
|
|
3086
|
+
const unstaged = execFileSync12("git", ["diff", "--name-only"], {
|
|
2401
3087
|
encoding: "utf-8",
|
|
2402
3088
|
cwd: projectDir,
|
|
2403
3089
|
timeout: 5e3,
|
|
@@ -2440,8 +3126,8 @@ Error context:
|
|
|
2440
3126
|
});
|
|
2441
3127
|
|
|
2442
3128
|
// src/stages/gate.ts
|
|
2443
|
-
import * as
|
|
2444
|
-
import * as
|
|
3129
|
+
import * as fs18 from "fs";
|
|
3130
|
+
import * as path16 from "path";
|
|
2445
3131
|
function executeGateStage(ctx, def) {
|
|
2446
3132
|
if (ctx.input.dryRun) {
|
|
2447
3133
|
logger.info(` [dry-run] skipping ${def.name}`);
|
|
@@ -2484,7 +3170,7 @@ ${output}
|
|
|
2484
3170
|
`);
|
|
2485
3171
|
}
|
|
2486
3172
|
}
|
|
2487
|
-
|
|
3173
|
+
fs18.writeFileSync(path16.join(ctx.taskDir, "verify.md"), lines.join(""));
|
|
2488
3174
|
return {
|
|
2489
3175
|
outcome: verifyResult.pass ? "completed" : "failed",
|
|
2490
3176
|
retries: 0
|
|
@@ -2499,9 +3185,9 @@ var init_gate = __esm({
|
|
|
2499
3185
|
});
|
|
2500
3186
|
|
|
2501
3187
|
// src/stages/verify.ts
|
|
2502
|
-
import * as
|
|
2503
|
-
import * as
|
|
2504
|
-
import { execFileSync as
|
|
3188
|
+
import * as fs19 from "fs";
|
|
3189
|
+
import * as path17 from "path";
|
|
3190
|
+
import { execFileSync as execFileSync13 } from "child_process";
|
|
2505
3191
|
async function executeVerifyWithAutofix(ctx, def) {
|
|
2506
3192
|
const maxAttempts = def.maxRetries ?? 2;
|
|
2507
3193
|
for (let attempt = 0; attempt <= maxAttempts; attempt++) {
|
|
@@ -2511,8 +3197,8 @@ async function executeVerifyWithAutofix(ctx, def) {
|
|
|
2511
3197
|
return { ...gateResult, retries: attempt };
|
|
2512
3198
|
}
|
|
2513
3199
|
if (attempt < maxAttempts) {
|
|
2514
|
-
const verifyPath =
|
|
2515
|
-
const errorOutput =
|
|
3200
|
+
const verifyPath = path17.join(ctx.taskDir, "verify.md");
|
|
3201
|
+
const errorOutput = fs19.existsSync(verifyPath) ? fs19.readFileSync(verifyPath, "utf-8") : "Unknown error";
|
|
2516
3202
|
const modifiedFiles = getModifiedFiles(ctx.projectDir);
|
|
2517
3203
|
const defaultRunner = getRunnerForStage(ctx, "taskify");
|
|
2518
3204
|
const diagConfig = getProjectConfig();
|
|
@@ -2555,7 +3241,7 @@ ${diagnosis.resolution}`);
|
|
|
2555
3241
|
const parts = parseCommand(cmd);
|
|
2556
3242
|
if (parts.length === 0) return;
|
|
2557
3243
|
try {
|
|
2558
|
-
|
|
3244
|
+
execFileSync13(parts[0], parts.slice(1), {
|
|
2559
3245
|
stdio: "pipe",
|
|
2560
3246
|
timeout: FIX_COMMAND_TIMEOUT_MS
|
|
2561
3247
|
});
|
|
@@ -2580,100 +3266,36 @@ ${ctx.input.feedback ?? ""}`.trim()
|
|
|
2580
3266
|
name: def.retryWithAgent,
|
|
2581
3267
|
type: "agent",
|
|
2582
3268
|
modelTier: "mid",
|
|
2583
|
-
timeout: 3e5,
|
|
2584
|
-
outputFile: void 0
|
|
2585
|
-
});
|
|
2586
|
-
}
|
|
2587
|
-
}
|
|
2588
|
-
}
|
|
2589
|
-
return {
|
|
2590
|
-
outcome: "failed",
|
|
2591
|
-
retries: maxAttempts,
|
|
2592
|
-
error: "Verification failed after autofix attempts"
|
|
2593
|
-
};
|
|
2594
|
-
}
|
|
2595
|
-
var init_verify = __esm({
|
|
2596
|
-
"src/stages/verify.ts"() {
|
|
2597
|
-
"use strict";
|
|
2598
|
-
init_context();
|
|
2599
|
-
init_config();
|
|
2600
|
-
init_verify_runner();
|
|
2601
|
-
init_runner_selection();
|
|
2602
|
-
init_github_api();
|
|
2603
|
-
init_observer();
|
|
2604
|
-
init_logger();
|
|
2605
|
-
init_agent();
|
|
2606
|
-
init_gate();
|
|
2607
|
-
}
|
|
2608
|
-
});
|
|
2609
|
-
|
|
2610
|
-
// src/cli/task-resolution.ts
|
|
2611
|
-
import * as fs17 from "fs";
|
|
2612
|
-
import * as path15 from "path";
|
|
2613
|
-
import { execFileSync as execFileSync12 } from "child_process";
|
|
2614
|
-
function findLatestTaskForIssue(issueNumber, projectDir) {
|
|
2615
|
-
const tasksDir = path15.join(projectDir, ".kody", "tasks");
|
|
2616
|
-
if (!fs17.existsSync(tasksDir)) return null;
|
|
2617
|
-
const allDirs = fs17.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
|
|
2618
|
-
const prefix = `${issueNumber}-`;
|
|
2619
|
-
const direct = allDirs.find((d) => d.startsWith(prefix));
|
|
2620
|
-
if (direct) return direct;
|
|
2621
|
-
try {
|
|
2622
|
-
const branch = execFileSync12("git", ["branch", "--show-current"], {
|
|
2623
|
-
encoding: "utf-8",
|
|
2624
|
-
cwd: projectDir,
|
|
2625
|
-
timeout: 5e3,
|
|
2626
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
2627
|
-
}).trim();
|
|
2628
|
-
const branchIssueMatch = branch.match(/^(\d+)-/);
|
|
2629
|
-
if (branchIssueMatch) {
|
|
2630
|
-
const branchIssueNum = branchIssueMatch[1];
|
|
2631
|
-
const branchPrefix = `${branchIssueNum}-`;
|
|
2632
|
-
const fromBranch = allDirs.find((d) => d.startsWith(branchPrefix));
|
|
2633
|
-
if (fromBranch) return fromBranch;
|
|
2634
|
-
}
|
|
2635
|
-
} catch {
|
|
2636
|
-
}
|
|
2637
|
-
return null;
|
|
2638
|
-
}
|
|
2639
|
-
function generateTaskId() {
|
|
2640
|
-
const now = /* @__PURE__ */ new Date();
|
|
2641
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
2642
|
-
return `${String(now.getFullYear()).slice(2)}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
2643
|
-
}
|
|
2644
|
-
function resolveTaskIdFromComments(issueNumber) {
|
|
2645
|
-
try {
|
|
2646
|
-
const comments = getIssueComments(issueNumber);
|
|
2647
|
-
const pattern = /pipeline started: `([^`]+)`/;
|
|
2648
|
-
let latestTaskId = null;
|
|
2649
|
-
for (const comment of comments) {
|
|
2650
|
-
const match = comment.body.match(pattern);
|
|
2651
|
-
if (match) {
|
|
2652
|
-
latestTaskId = match[1];
|
|
3269
|
+
timeout: 3e5,
|
|
3270
|
+
outputFile: void 0
|
|
3271
|
+
});
|
|
2653
3272
|
}
|
|
2654
3273
|
}
|
|
2655
|
-
return latestTaskId;
|
|
2656
|
-
} catch {
|
|
2657
|
-
return null;
|
|
2658
3274
|
}
|
|
3275
|
+
return {
|
|
3276
|
+
outcome: "failed",
|
|
3277
|
+
retries: maxAttempts,
|
|
3278
|
+
error: "Verification failed after autofix attempts"
|
|
3279
|
+
};
|
|
2659
3280
|
}
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
if (fromTasks) return fromTasks;
|
|
2663
|
-
const fromComments = resolveTaskIdFromComments(issueNumber);
|
|
2664
|
-
if (fromComments) return fromComments;
|
|
2665
|
-
return null;
|
|
2666
|
-
}
|
|
2667
|
-
var init_task_resolution = __esm({
|
|
2668
|
-
"src/cli/task-resolution.ts"() {
|
|
3281
|
+
var init_verify = __esm({
|
|
3282
|
+
"src/stages/verify.ts"() {
|
|
2669
3283
|
"use strict";
|
|
3284
|
+
init_context();
|
|
3285
|
+
init_config();
|
|
3286
|
+
init_verify_runner();
|
|
3287
|
+
init_runner_selection();
|
|
2670
3288
|
init_github_api();
|
|
3289
|
+
init_observer();
|
|
3290
|
+
init_logger();
|
|
3291
|
+
init_agent();
|
|
3292
|
+
init_gate();
|
|
2671
3293
|
}
|
|
2672
3294
|
});
|
|
2673
3295
|
|
|
2674
3296
|
// src/review-standalone.ts
|
|
2675
|
-
import * as
|
|
2676
|
-
import * as
|
|
3297
|
+
import * as fs20 from "fs";
|
|
3298
|
+
import * as path18 from "path";
|
|
2677
3299
|
function resolveReviewTarget(input) {
|
|
2678
3300
|
if (input.prs.length === 0) {
|
|
2679
3301
|
return {
|
|
@@ -2697,8 +3319,8 @@ Or comment on the specific PR: \`@kody review\``
|
|
|
2697
3319
|
}
|
|
2698
3320
|
async function runStandaloneReview(input) {
|
|
2699
3321
|
const taskId = input.taskId ?? `review-${generateTaskId()}`;
|
|
2700
|
-
const taskDir =
|
|
2701
|
-
|
|
3322
|
+
const taskDir = path18.join(input.projectDir, ".kody", "tasks", taskId);
|
|
3323
|
+
fs20.mkdirSync(taskDir, { recursive: true });
|
|
2702
3324
|
let diffInstruction = "";
|
|
2703
3325
|
let filesChangedSection = "";
|
|
2704
3326
|
if (input.baseBranch) {
|
|
@@ -2725,7 +3347,7 @@ ${fileList}`;
|
|
|
2725
3347
|
const taskContent = `# ${input.prTitle}
|
|
2726
3348
|
|
|
2727
3349
|
${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
|
|
2728
|
-
|
|
3350
|
+
fs20.writeFileSync(path18.join(taskDir, "task.md"), taskContent);
|
|
2729
3351
|
const reviewDef = STAGES.find((s) => s.name === "review");
|
|
2730
3352
|
const ctx = {
|
|
2731
3353
|
taskId,
|
|
@@ -2747,10 +3369,10 @@ ${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
|
|
|
2747
3369
|
error: result.error ?? "Review stage failed"
|
|
2748
3370
|
};
|
|
2749
3371
|
}
|
|
2750
|
-
const reviewPath =
|
|
3372
|
+
const reviewPath = path18.join(taskDir, "review.md");
|
|
2751
3373
|
let reviewContent;
|
|
2752
|
-
if (
|
|
2753
|
-
reviewContent =
|
|
3374
|
+
if (fs20.existsSync(reviewPath)) {
|
|
3375
|
+
reviewContent = fs20.readFileSync(reviewPath, "utf-8");
|
|
2754
3376
|
}
|
|
2755
3377
|
return {
|
|
2756
3378
|
outcome: "completed",
|
|
@@ -2790,8 +3412,8 @@ var init_review_standalone = __esm({
|
|
|
2790
3412
|
});
|
|
2791
3413
|
|
|
2792
3414
|
// src/stages/review.ts
|
|
2793
|
-
import * as
|
|
2794
|
-
import * as
|
|
3415
|
+
import * as fs21 from "fs";
|
|
3416
|
+
import * as path19 from "path";
|
|
2795
3417
|
async function executeReviewWithFix(ctx, def) {
|
|
2796
3418
|
if (ctx.input.dryRun) {
|
|
2797
3419
|
return { outcome: "completed", retries: 0 };
|
|
@@ -2805,11 +3427,11 @@ async function executeReviewWithFix(ctx, def) {
|
|
|
2805
3427
|
if (reviewResult.outcome !== "completed") {
|
|
2806
3428
|
return reviewResult;
|
|
2807
3429
|
}
|
|
2808
|
-
const reviewFile =
|
|
2809
|
-
if (!
|
|
3430
|
+
const reviewFile = path19.join(ctx.taskDir, "review.md");
|
|
3431
|
+
if (!fs21.existsSync(reviewFile)) {
|
|
2810
3432
|
return { outcome: "failed", retries: iteration, error: "review.md not found" };
|
|
2811
3433
|
}
|
|
2812
|
-
const content =
|
|
3434
|
+
const content = fs21.readFileSync(reviewFile, "utf-8");
|
|
2813
3435
|
if (detectReviewVerdict(content) !== "fail") {
|
|
2814
3436
|
return { ...reviewResult, retries: iteration };
|
|
2815
3437
|
}
|
|
@@ -2838,15 +3460,15 @@ var init_review = __esm({
|
|
|
2838
3460
|
});
|
|
2839
3461
|
|
|
2840
3462
|
// src/stages/ship.ts
|
|
2841
|
-
import * as
|
|
2842
|
-
import * as
|
|
2843
|
-
import { execFileSync as
|
|
3463
|
+
import * as fs22 from "fs";
|
|
3464
|
+
import * as path20 from "path";
|
|
3465
|
+
import { execFileSync as execFileSync14 } from "child_process";
|
|
2844
3466
|
function buildPrBody(ctx) {
|
|
2845
3467
|
const sections = [];
|
|
2846
|
-
const taskJsonPath =
|
|
2847
|
-
if (
|
|
3468
|
+
const taskJsonPath = path20.join(ctx.taskDir, "task.json");
|
|
3469
|
+
if (fs22.existsSync(taskJsonPath)) {
|
|
2848
3470
|
try {
|
|
2849
|
-
const raw =
|
|
3471
|
+
const raw = fs22.readFileSync(taskJsonPath, "utf-8");
|
|
2850
3472
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2851
3473
|
const task = JSON.parse(cleaned);
|
|
2852
3474
|
if (task.description) {
|
|
@@ -2865,9 +3487,9 @@ ${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
|
|
|
2865
3487
|
} catch {
|
|
2866
3488
|
}
|
|
2867
3489
|
}
|
|
2868
|
-
const reviewPath =
|
|
2869
|
-
if (
|
|
2870
|
-
const review =
|
|
3490
|
+
const reviewPath = path20.join(ctx.taskDir, "review.md");
|
|
3491
|
+
if (fs22.existsSync(reviewPath)) {
|
|
3492
|
+
const review = fs22.readFileSync(reviewPath, "utf-8");
|
|
2871
3493
|
const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
2872
3494
|
if (summaryMatch) {
|
|
2873
3495
|
const summary = summaryMatch[1].trim();
|
|
@@ -2884,14 +3506,14 @@ ${summary}`);
|
|
|
2884
3506
|
**Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
|
|
2885
3507
|
}
|
|
2886
3508
|
}
|
|
2887
|
-
const verifyPath =
|
|
2888
|
-
if (
|
|
2889
|
-
const verify =
|
|
3509
|
+
const verifyPath = path20.join(ctx.taskDir, "verify.md");
|
|
3510
|
+
if (fs22.existsSync(verifyPath)) {
|
|
3511
|
+
const verify = fs22.readFileSync(verifyPath, "utf-8");
|
|
2890
3512
|
if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
|
|
2891
3513
|
}
|
|
2892
|
-
const planPath =
|
|
2893
|
-
if (
|
|
2894
|
-
const plan =
|
|
3514
|
+
const planPath = path20.join(ctx.taskDir, "plan.md");
|
|
3515
|
+
if (fs22.existsSync(planPath)) {
|
|
3516
|
+
const plan = fs22.readFileSync(planPath, "utf-8").trim();
|
|
2895
3517
|
if (plan) {
|
|
2896
3518
|
const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
|
|
2897
3519
|
sections.push(`
|
|
@@ -2911,25 +3533,25 @@ Closes #${ctx.input.issueNumber}`);
|
|
|
2911
3533
|
return sections.join("\n");
|
|
2912
3534
|
}
|
|
2913
3535
|
function executeShipStage(ctx, _def) {
|
|
2914
|
-
const shipPath =
|
|
3536
|
+
const shipPath = path20.join(ctx.taskDir, "ship.md");
|
|
2915
3537
|
if (ctx.input.dryRun) {
|
|
2916
|
-
|
|
3538
|
+
fs22.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
|
|
2917
3539
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
2918
3540
|
}
|
|
2919
3541
|
if (ctx.input.local && !ctx.input.issueNumber) {
|
|
2920
|
-
|
|
3542
|
+
fs22.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
|
|
2921
3543
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
2922
3544
|
}
|
|
2923
3545
|
try {
|
|
2924
3546
|
const head = getCurrentBranch(ctx.projectDir);
|
|
2925
3547
|
const base = getDefaultBranch(ctx.projectDir);
|
|
2926
3548
|
try {
|
|
2927
|
-
|
|
3549
|
+
execFileSync14("git", ["add", ctx.taskDir], {
|
|
2928
3550
|
cwd: ctx.projectDir,
|
|
2929
3551
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
2930
3552
|
stdio: "pipe"
|
|
2931
3553
|
});
|
|
2932
|
-
|
|
3554
|
+
execFileSync14("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
|
|
2933
3555
|
cwd: ctx.projectDir,
|
|
2934
3556
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
2935
3557
|
stdio: "pipe"
|
|
@@ -2943,7 +3565,7 @@ function executeShipStage(ctx, _def) {
|
|
|
2943
3565
|
let repo = config.github?.repo;
|
|
2944
3566
|
if (!owner || !repo) {
|
|
2945
3567
|
try {
|
|
2946
|
-
const remoteUrl =
|
|
3568
|
+
const remoteUrl = execFileSync14("git", ["remote", "get-url", "origin"], {
|
|
2947
3569
|
encoding: "utf-8",
|
|
2948
3570
|
cwd: ctx.projectDir
|
|
2949
3571
|
}).trim();
|
|
@@ -2964,28 +3586,28 @@ function executeShipStage(ctx, _def) {
|
|
|
2964
3586
|
chore: "chore"
|
|
2965
3587
|
};
|
|
2966
3588
|
let prefix = "chore";
|
|
2967
|
-
const taskJsonPath =
|
|
2968
|
-
if (
|
|
3589
|
+
const taskJsonPath = path20.join(ctx.taskDir, "task.json");
|
|
3590
|
+
if (fs22.existsSync(taskJsonPath)) {
|
|
2969
3591
|
try {
|
|
2970
|
-
const raw =
|
|
3592
|
+
const raw = fs22.readFileSync(taskJsonPath, "utf-8");
|
|
2971
3593
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2972
3594
|
const task = JSON.parse(cleaned);
|
|
2973
3595
|
prefix = TYPE_PREFIX[task.task_type] ?? "chore";
|
|
2974
3596
|
} catch {
|
|
2975
3597
|
}
|
|
2976
3598
|
}
|
|
2977
|
-
const taskMdPath =
|
|
2978
|
-
if (
|
|
2979
|
-
const content =
|
|
3599
|
+
const taskMdPath = path20.join(ctx.taskDir, "task.md");
|
|
3600
|
+
if (fs22.existsSync(taskMdPath)) {
|
|
3601
|
+
const content = fs22.readFileSync(taskMdPath, "utf-8");
|
|
2980
3602
|
const heading = content.split("\n").find((l) => l.startsWith("# "));
|
|
2981
3603
|
if (heading) {
|
|
2982
3604
|
title = `${prefix}: ${heading.replace(/^#\s*/, "").trim()}`.slice(0, 72);
|
|
2983
3605
|
}
|
|
2984
3606
|
}
|
|
2985
3607
|
if (title === "Update") {
|
|
2986
|
-
if (
|
|
3608
|
+
if (fs22.existsSync(taskJsonPath)) {
|
|
2987
3609
|
try {
|
|
2988
|
-
const raw =
|
|
3610
|
+
const raw = fs22.readFileSync(taskJsonPath, "utf-8");
|
|
2989
3611
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2990
3612
|
const task = JSON.parse(cleaned);
|
|
2991
3613
|
if (task.title) title = `${prefix}: ${task.title}`.slice(0, 72);
|
|
@@ -3008,7 +3630,7 @@ function executeShipStage(ctx, _def) {
|
|
|
3008
3630
|
} catch {
|
|
3009
3631
|
}
|
|
3010
3632
|
}
|
|
3011
|
-
|
|
3633
|
+
fs22.writeFileSync(shipPath, `# Ship
|
|
3012
3634
|
|
|
3013
3635
|
Updated existing PR: ${existingPr.url}
|
|
3014
3636
|
PR #${existingPr.number}
|
|
@@ -3029,20 +3651,20 @@ PR #${existingPr.number}
|
|
|
3029
3651
|
} catch {
|
|
3030
3652
|
}
|
|
3031
3653
|
}
|
|
3032
|
-
|
|
3654
|
+
fs22.writeFileSync(shipPath, `# Ship
|
|
3033
3655
|
|
|
3034
3656
|
PR created: ${pr.url}
|
|
3035
3657
|
PR #${pr.number}
|
|
3036
3658
|
`);
|
|
3037
3659
|
} else {
|
|
3038
|
-
|
|
3660
|
+
fs22.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
|
|
3039
3661
|
}
|
|
3040
3662
|
}
|
|
3041
3663
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
3042
3664
|
} catch (err) {
|
|
3043
3665
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3044
3666
|
try {
|
|
3045
|
-
|
|
3667
|
+
fs22.writeFileSync(shipPath, `# Ship
|
|
3046
3668
|
|
|
3047
3669
|
Failed: ${msg}
|
|
3048
3670
|
`);
|
|
@@ -3091,15 +3713,15 @@ var init_executor_registry = __esm({
|
|
|
3091
3713
|
});
|
|
3092
3714
|
|
|
3093
3715
|
// src/pipeline/questions.ts
|
|
3094
|
-
import * as
|
|
3095
|
-
import * as
|
|
3716
|
+
import * as fs23 from "fs";
|
|
3717
|
+
import * as path21 from "path";
|
|
3096
3718
|
function checkForQuestions(ctx, stageName) {
|
|
3097
3719
|
if (ctx.input.local || !ctx.input.issueNumber) return false;
|
|
3098
3720
|
try {
|
|
3099
3721
|
if (stageName === "taskify") {
|
|
3100
|
-
const taskJsonPath =
|
|
3101
|
-
if (!
|
|
3102
|
-
const raw =
|
|
3722
|
+
const taskJsonPath = path21.join(ctx.taskDir, "task.json");
|
|
3723
|
+
if (!fs23.existsSync(taskJsonPath)) return false;
|
|
3724
|
+
const raw = fs23.readFileSync(taskJsonPath, "utf-8");
|
|
3103
3725
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3104
3726
|
const taskJson = JSON.parse(cleaned);
|
|
3105
3727
|
if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
|
|
@@ -3114,9 +3736,9 @@ Reply with \`@kody approve\` and your answers in the comment body.`;
|
|
|
3114
3736
|
}
|
|
3115
3737
|
}
|
|
3116
3738
|
if (stageName === "plan") {
|
|
3117
|
-
const planPath =
|
|
3118
|
-
if (!
|
|
3119
|
-
const plan =
|
|
3739
|
+
const planPath = path21.join(ctx.taskDir, "plan.md");
|
|
3740
|
+
if (!fs23.existsSync(planPath)) return false;
|
|
3741
|
+
const plan = fs23.readFileSync(planPath, "utf-8");
|
|
3120
3742
|
const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
3121
3743
|
if (questionsMatch) {
|
|
3122
3744
|
const questionsText = questionsMatch[1].trim();
|
|
@@ -3145,8 +3767,8 @@ var init_questions = __esm({
|
|
|
3145
3767
|
});
|
|
3146
3768
|
|
|
3147
3769
|
// src/pipeline/hooks.ts
|
|
3148
|
-
import * as
|
|
3149
|
-
import * as
|
|
3770
|
+
import * as fs24 from "fs";
|
|
3771
|
+
import * as path22 from "path";
|
|
3150
3772
|
function applyPreStageLabel(ctx, def) {
|
|
3151
3773
|
if (!ctx.input.issueNumber || ctx.input.local) return;
|
|
3152
3774
|
if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
|
|
@@ -3184,9 +3806,9 @@ function autoDetectComplexity(ctx, def) {
|
|
|
3184
3806
|
return { complexity, activeStages };
|
|
3185
3807
|
}
|
|
3186
3808
|
try {
|
|
3187
|
-
const taskJsonPath =
|
|
3188
|
-
if (!
|
|
3189
|
-
const raw =
|
|
3809
|
+
const taskJsonPath = path22.join(ctx.taskDir, "task.json");
|
|
3810
|
+
if (!fs24.existsSync(taskJsonPath)) return null;
|
|
3811
|
+
const raw = fs24.readFileSync(taskJsonPath, "utf-8");
|
|
3190
3812
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3191
3813
|
const taskJson = JSON.parse(cleaned);
|
|
3192
3814
|
if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
|
|
@@ -3216,8 +3838,8 @@ function checkRiskGate(ctx, def, state, complexity) {
|
|
|
3216
3838
|
if (ctx.input.dryRun || ctx.input.local) return null;
|
|
3217
3839
|
if (ctx.input.mode === "rerun") return null;
|
|
3218
3840
|
if (!ctx.input.issueNumber) return null;
|
|
3219
|
-
const planPath =
|
|
3220
|
-
const plan =
|
|
3841
|
+
const planPath = path22.join(ctx.taskDir, "plan.md");
|
|
3842
|
+
const plan = fs24.existsSync(planPath) ? fs24.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
|
|
3221
3843
|
try {
|
|
3222
3844
|
postComment(
|
|
3223
3845
|
ctx.input.issueNumber,
|
|
@@ -3284,22 +3906,22 @@ var init_hooks = __esm({
|
|
|
3284
3906
|
});
|
|
3285
3907
|
|
|
3286
3908
|
// src/learning/auto-learn.ts
|
|
3287
|
-
import * as
|
|
3288
|
-
import * as
|
|
3909
|
+
import * as fs25 from "fs";
|
|
3910
|
+
import * as path23 from "path";
|
|
3289
3911
|
function stripAnsi(str) {
|
|
3290
3912
|
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
3291
3913
|
}
|
|
3292
3914
|
function autoLearn(ctx) {
|
|
3293
3915
|
try {
|
|
3294
|
-
const memoryDir =
|
|
3295
|
-
if (!
|
|
3296
|
-
|
|
3916
|
+
const memoryDir = path23.join(ctx.projectDir, ".kody", "memory");
|
|
3917
|
+
if (!fs25.existsSync(memoryDir)) {
|
|
3918
|
+
fs25.mkdirSync(memoryDir, { recursive: true });
|
|
3297
3919
|
}
|
|
3298
3920
|
const learnings = [];
|
|
3299
3921
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3300
|
-
const verifyPath =
|
|
3301
|
-
if (
|
|
3302
|
-
const verify = stripAnsi(
|
|
3922
|
+
const verifyPath = path23.join(ctx.taskDir, "verify.md");
|
|
3923
|
+
if (fs25.existsSync(verifyPath)) {
|
|
3924
|
+
const verify = stripAnsi(fs25.readFileSync(verifyPath, "utf-8"));
|
|
3303
3925
|
if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
|
|
3304
3926
|
if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
|
|
3305
3927
|
if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
|
|
@@ -3308,18 +3930,18 @@ function autoLearn(ctx) {
|
|
|
3308
3930
|
if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
|
|
3309
3931
|
if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
|
|
3310
3932
|
}
|
|
3311
|
-
const reviewPath =
|
|
3312
|
-
if (
|
|
3313
|
-
const review =
|
|
3933
|
+
const reviewPath = path23.join(ctx.taskDir, "review.md");
|
|
3934
|
+
if (fs25.existsSync(reviewPath)) {
|
|
3935
|
+
const review = fs25.readFileSync(reviewPath, "utf-8");
|
|
3314
3936
|
if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
|
|
3315
3937
|
if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
|
|
3316
3938
|
if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
|
|
3317
3939
|
if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
|
|
3318
3940
|
}
|
|
3319
|
-
const taskJsonPath =
|
|
3320
|
-
if (
|
|
3941
|
+
const taskJsonPath = path23.join(ctx.taskDir, "task.json");
|
|
3942
|
+
if (fs25.existsSync(taskJsonPath)) {
|
|
3321
3943
|
try {
|
|
3322
|
-
const raw = stripAnsi(
|
|
3944
|
+
const raw = stripAnsi(fs25.readFileSync(taskJsonPath, "utf-8"));
|
|
3323
3945
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3324
3946
|
const task = JSON.parse(cleaned);
|
|
3325
3947
|
if (task.scope && Array.isArray(task.scope)) {
|
|
@@ -3330,12 +3952,12 @@ function autoLearn(ctx) {
|
|
|
3330
3952
|
}
|
|
3331
3953
|
}
|
|
3332
3954
|
if (learnings.length > 0) {
|
|
3333
|
-
const conventionsPath =
|
|
3955
|
+
const conventionsPath = path23.join(memoryDir, "conventions.md");
|
|
3334
3956
|
const entry = `
|
|
3335
3957
|
## Learned ${timestamp2} (task: ${ctx.taskId})
|
|
3336
3958
|
${learnings.join("\n")}
|
|
3337
3959
|
`;
|
|
3338
|
-
|
|
3960
|
+
fs25.appendFileSync(conventionsPath, entry);
|
|
3339
3961
|
logger.info(`Auto-learned ${learnings.length} convention(s)`);
|
|
3340
3962
|
}
|
|
3341
3963
|
autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
|
|
@@ -3343,8 +3965,8 @@ ${learnings.join("\n")}
|
|
|
3343
3965
|
}
|
|
3344
3966
|
}
|
|
3345
3967
|
function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
3346
|
-
const archPath =
|
|
3347
|
-
if (
|
|
3968
|
+
const archPath = path23.join(memoryDir, "architecture.md");
|
|
3969
|
+
if (fs25.existsSync(archPath)) return;
|
|
3348
3970
|
const detected = detectArchitectureBasic(projectDir);
|
|
3349
3971
|
if (detected.length > 0) {
|
|
3350
3972
|
const content = `# Architecture (auto-detected ${timestamp2})
|
|
@@ -3352,7 +3974,7 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
|
3352
3974
|
## Overview
|
|
3353
3975
|
${detected.join("\n")}
|
|
3354
3976
|
`;
|
|
3355
|
-
|
|
3977
|
+
fs25.writeFileSync(archPath, content);
|
|
3356
3978
|
logger.info(`Auto-detected architecture (${detected.length} items)`);
|
|
3357
3979
|
}
|
|
3358
3980
|
}
|
|
@@ -3365,13 +3987,13 @@ var init_auto_learn = __esm({
|
|
|
3365
3987
|
});
|
|
3366
3988
|
|
|
3367
3989
|
// src/retrospective.ts
|
|
3368
|
-
import * as
|
|
3369
|
-
import * as
|
|
3990
|
+
import * as fs26 from "fs";
|
|
3991
|
+
import * as path24 from "path";
|
|
3370
3992
|
function readArtifact(taskDir, filename, maxChars) {
|
|
3371
|
-
const p =
|
|
3372
|
-
if (!
|
|
3993
|
+
const p = path24.join(taskDir, filename);
|
|
3994
|
+
if (!fs26.existsSync(p)) return null;
|
|
3373
3995
|
try {
|
|
3374
|
-
const content =
|
|
3996
|
+
const content = fs26.readFileSync(p, "utf-8");
|
|
3375
3997
|
return content.length > maxChars ? content.slice(0, maxChars) + "\n...(truncated)" : content;
|
|
3376
3998
|
} catch {
|
|
3377
3999
|
return null;
|
|
@@ -3424,13 +4046,13 @@ function collectRunContext(ctx, state, pipelineStartTime) {
|
|
|
3424
4046
|
return lines.join("\n");
|
|
3425
4047
|
}
|
|
3426
4048
|
function getLogPath(projectDir) {
|
|
3427
|
-
return
|
|
4049
|
+
return path24.join(projectDir, ".kody", "memory", "observer-log.jsonl");
|
|
3428
4050
|
}
|
|
3429
4051
|
function readPreviousRetrospectives(projectDir, limit = 10) {
|
|
3430
4052
|
const logPath = getLogPath(projectDir);
|
|
3431
|
-
if (!
|
|
4053
|
+
if (!fs26.existsSync(logPath)) return [];
|
|
3432
4054
|
try {
|
|
3433
|
-
const content =
|
|
4055
|
+
const content = fs26.readFileSync(logPath, "utf-8");
|
|
3434
4056
|
const lines = content.split("\n").filter(Boolean);
|
|
3435
4057
|
const entries = [];
|
|
3436
4058
|
const start = Math.max(0, lines.length - limit);
|
|
@@ -3457,11 +4079,11 @@ function formatPreviousEntries(entries) {
|
|
|
3457
4079
|
}
|
|
3458
4080
|
function appendRetrospectiveEntry(projectDir, entry) {
|
|
3459
4081
|
const logPath = getLogPath(projectDir);
|
|
3460
|
-
const dir =
|
|
3461
|
-
if (!
|
|
3462
|
-
|
|
4082
|
+
const dir = path24.dirname(logPath);
|
|
4083
|
+
if (!fs26.existsSync(dir)) {
|
|
4084
|
+
fs26.mkdirSync(dir, { recursive: true });
|
|
3463
4085
|
}
|
|
3464
|
-
|
|
4086
|
+
fs26.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
3465
4087
|
}
|
|
3466
4088
|
async function runRetrospective(ctx, state, pipelineStartTime) {
|
|
3467
4089
|
if (ctx.input.dryRun) return;
|
|
@@ -3629,8 +4251,8 @@ var init_summary = __esm({
|
|
|
3629
4251
|
});
|
|
3630
4252
|
|
|
3631
4253
|
// src/pipeline.ts
|
|
3632
|
-
import * as
|
|
3633
|
-
import * as
|
|
4254
|
+
import * as fs27 from "fs";
|
|
4255
|
+
import * as path25 from "path";
|
|
3634
4256
|
function ensureFeatureBranchIfNeeded(ctx) {
|
|
3635
4257
|
if (ctx.input.dryRun) return;
|
|
3636
4258
|
if (ctx.input.prNumber) {
|
|
@@ -3643,8 +4265,8 @@ function ensureFeatureBranchIfNeeded(ctx) {
|
|
|
3643
4265
|
}
|
|
3644
4266
|
if (!ctx.input.issueNumber) return;
|
|
3645
4267
|
try {
|
|
3646
|
-
const taskMdPath =
|
|
3647
|
-
const title =
|
|
4268
|
+
const taskMdPath = path25.join(ctx.taskDir, "task.md");
|
|
4269
|
+
const title = fs27.existsSync(taskMdPath) ? fs27.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
|
|
3648
4270
|
ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
|
|
3649
4271
|
syncWithDefault(ctx.projectDir);
|
|
3650
4272
|
} catch (err) {
|
|
@@ -3658,10 +4280,10 @@ function ensureFeatureBranchIfNeeded(ctx) {
|
|
|
3658
4280
|
}
|
|
3659
4281
|
}
|
|
3660
4282
|
function acquireLock(taskDir) {
|
|
3661
|
-
const lockPath =
|
|
3662
|
-
if (
|
|
4283
|
+
const lockPath = path25.join(taskDir, ".lock");
|
|
4284
|
+
if (fs27.existsSync(lockPath)) {
|
|
3663
4285
|
try {
|
|
3664
|
-
const pid = parseInt(
|
|
4286
|
+
const pid = parseInt(fs27.readFileSync(lockPath, "utf-8").trim(), 10);
|
|
3665
4287
|
if (!isNaN(pid)) {
|
|
3666
4288
|
try {
|
|
3667
4289
|
process.kill(pid, 0);
|
|
@@ -3678,14 +4300,14 @@ function acquireLock(taskDir) {
|
|
|
3678
4300
|
logger.warn(` Corrupt lock file \u2014 overwriting`);
|
|
3679
4301
|
}
|
|
3680
4302
|
try {
|
|
3681
|
-
|
|
4303
|
+
fs27.unlinkSync(lockPath);
|
|
3682
4304
|
} catch {
|
|
3683
4305
|
}
|
|
3684
4306
|
}
|
|
3685
4307
|
try {
|
|
3686
|
-
const fd =
|
|
3687
|
-
|
|
3688
|
-
|
|
4308
|
+
const fd = fs27.openSync(lockPath, fs27.constants.O_WRONLY | fs27.constants.O_CREAT | fs27.constants.O_EXCL);
|
|
4309
|
+
fs27.writeSync(fd, String(process.pid));
|
|
4310
|
+
fs27.closeSync(fd);
|
|
3689
4311
|
} catch (err) {
|
|
3690
4312
|
if (err.code === "EEXIST") {
|
|
3691
4313
|
throw new Error("Pipeline already running (lock acquired by another process)");
|
|
@@ -3695,7 +4317,7 @@ function acquireLock(taskDir) {
|
|
|
3695
4317
|
}
|
|
3696
4318
|
function releaseLock(taskDir) {
|
|
3697
4319
|
try {
|
|
3698
|
-
|
|
4320
|
+
fs27.unlinkSync(path25.join(taskDir, ".lock"));
|
|
3699
4321
|
} catch {
|
|
3700
4322
|
}
|
|
3701
4323
|
}
|
|
@@ -3903,8 +4525,8 @@ var init_pipeline = __esm({
|
|
|
3903
4525
|
});
|
|
3904
4526
|
|
|
3905
4527
|
// src/preflight.ts
|
|
3906
|
-
import { execFileSync as
|
|
3907
|
-
import * as
|
|
4528
|
+
import { execFileSync as execFileSync15 } from "child_process";
|
|
4529
|
+
import * as fs28 from "fs";
|
|
3908
4530
|
function check(name, fn) {
|
|
3909
4531
|
try {
|
|
3910
4532
|
const detail = fn() ?? void 0;
|
|
@@ -3916,7 +4538,7 @@ function check(name, fn) {
|
|
|
3916
4538
|
function runPreflight() {
|
|
3917
4539
|
const checks = [
|
|
3918
4540
|
check("claude CLI", () => {
|
|
3919
|
-
const v =
|
|
4541
|
+
const v = execFileSync15("claude", ["--version"], {
|
|
3920
4542
|
encoding: "utf-8",
|
|
3921
4543
|
timeout: 1e4,
|
|
3922
4544
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3924,14 +4546,14 @@ function runPreflight() {
|
|
|
3924
4546
|
return v;
|
|
3925
4547
|
}),
|
|
3926
4548
|
check("git repo", () => {
|
|
3927
|
-
|
|
4549
|
+
execFileSync15("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
3928
4550
|
encoding: "utf-8",
|
|
3929
4551
|
timeout: 5e3,
|
|
3930
4552
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3931
4553
|
});
|
|
3932
4554
|
}),
|
|
3933
4555
|
check("pnpm", () => {
|
|
3934
|
-
const v =
|
|
4556
|
+
const v = execFileSync15("pnpm", ["--version"], {
|
|
3935
4557
|
encoding: "utf-8",
|
|
3936
4558
|
timeout: 5e3,
|
|
3937
4559
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3939,7 +4561,7 @@ function runPreflight() {
|
|
|
3939
4561
|
return v;
|
|
3940
4562
|
}),
|
|
3941
4563
|
check("node >= 18", () => {
|
|
3942
|
-
const v =
|
|
4564
|
+
const v = execFileSync15("node", ["--version"], {
|
|
3943
4565
|
encoding: "utf-8",
|
|
3944
4566
|
timeout: 5e3,
|
|
3945
4567
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3949,7 +4571,7 @@ function runPreflight() {
|
|
|
3949
4571
|
return v;
|
|
3950
4572
|
}),
|
|
3951
4573
|
check("gh CLI", () => {
|
|
3952
|
-
const v =
|
|
4574
|
+
const v = execFileSync15("gh", ["--version"], {
|
|
3953
4575
|
encoding: "utf-8",
|
|
3954
4576
|
timeout: 5e3,
|
|
3955
4577
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3957,7 +4579,7 @@ function runPreflight() {
|
|
|
3957
4579
|
return v;
|
|
3958
4580
|
}),
|
|
3959
4581
|
check("package.json", () => {
|
|
3960
|
-
if (!
|
|
4582
|
+
if (!fs28.existsSync("package.json")) throw new Error("not found");
|
|
3961
4583
|
})
|
|
3962
4584
|
];
|
|
3963
4585
|
const failed = checks.filter((c) => !c.ok);
|
|
@@ -3978,19 +4600,19 @@ var init_preflight = __esm({
|
|
|
3978
4600
|
});
|
|
3979
4601
|
|
|
3980
4602
|
// src/cli/args.ts
|
|
3981
|
-
function
|
|
4603
|
+
function getArg2(args2, flag) {
|
|
3982
4604
|
const idx = args2.indexOf(flag);
|
|
3983
4605
|
if (idx !== -1 && args2[idx + 1] && !args2[idx + 1].startsWith("--")) {
|
|
3984
4606
|
return args2[idx + 1];
|
|
3985
4607
|
}
|
|
3986
4608
|
return void 0;
|
|
3987
4609
|
}
|
|
3988
|
-
function
|
|
4610
|
+
function hasFlag2(args2, flag) {
|
|
3989
4611
|
return args2.includes(flag);
|
|
3990
4612
|
}
|
|
3991
4613
|
function parseArgs() {
|
|
3992
4614
|
const args2 = process.argv.slice(2);
|
|
3993
|
-
if (
|
|
4615
|
+
if (hasFlag2(args2, "--help") || hasFlag2(args2, "-h") || args2.length === 0) {
|
|
3994
4616
|
console.log(`Usage:
|
|
3995
4617
|
kody run --task-id <id> [--task "<desc>"] [--cwd <path>] [--issue-number <n>] [--complexity low|medium|high] [--feedback "<text>"] [--local] [--dry-run]
|
|
3996
4618
|
kody rerun --task-id <id> --from <stage> [--cwd <path>] [--issue-number <n>]
|
|
@@ -4007,22 +4629,22 @@ function parseArgs() {
|
|
|
4007
4629
|
console.error(`Unknown command: ${command2}`);
|
|
4008
4630
|
process.exit(1);
|
|
4009
4631
|
}
|
|
4010
|
-
const issueStr =
|
|
4011
|
-
const prStr =
|
|
4012
|
-
const localFlag =
|
|
4632
|
+
const issueStr = getArg2(args2, "--issue-number") ?? process.env.ISSUE_NUMBER;
|
|
4633
|
+
const prStr = getArg2(args2, "--pr-number") ?? process.env.PR_NUMBER;
|
|
4634
|
+
const localFlag = hasFlag2(args2, "--local");
|
|
4013
4635
|
return {
|
|
4014
4636
|
command: command2,
|
|
4015
|
-
taskId:
|
|
4016
|
-
task:
|
|
4017
|
-
fromStage:
|
|
4018
|
-
dryRun:
|
|
4019
|
-
cwd:
|
|
4637
|
+
taskId: getArg2(args2, "--task-id") ?? process.env.TASK_ID,
|
|
4638
|
+
task: getArg2(args2, "--task"),
|
|
4639
|
+
fromStage: getArg2(args2, "--from") ?? process.env.FROM_STAGE,
|
|
4640
|
+
dryRun: hasFlag2(args2, "--dry-run") || process.env.DRY_RUN === "true",
|
|
4641
|
+
cwd: getArg2(args2, "--cwd"),
|
|
4020
4642
|
issueNumber: issueStr ? parseInt(issueStr, 10) : void 0,
|
|
4021
4643
|
prNumber: prStr ? parseInt(prStr, 10) : void 0,
|
|
4022
|
-
feedback:
|
|
4023
|
-
local: localFlag || !isCI2 && !
|
|
4024
|
-
complexity:
|
|
4025
|
-
ciRunId:
|
|
4644
|
+
feedback: getArg2(args2, "--feedback") ?? process.env.FEEDBACK,
|
|
4645
|
+
local: localFlag || !isCI2 && !hasFlag2(args2, "--no-local"),
|
|
4646
|
+
complexity: getArg2(args2, "--complexity") ?? process.env.COMPLEXITY,
|
|
4647
|
+
ciRunId: getArg2(args2, "--ci-run-id") ?? process.env.CI_RUN_ID
|
|
4026
4648
|
};
|
|
4027
4649
|
}
|
|
4028
4650
|
var isCI2;
|
|
@@ -4033,183 +4655,9 @@ var init_args = __esm({
|
|
|
4033
4655
|
}
|
|
4034
4656
|
});
|
|
4035
4657
|
|
|
4036
|
-
// src/cli/litellm.ts
|
|
4037
|
-
import * as fs27 from "fs";
|
|
4038
|
-
import * as os from "os";
|
|
4039
|
-
import * as path24 from "path";
|
|
4040
|
-
import { execFileSync as execFileSync15 } from "child_process";
|
|
4041
|
-
async function checkLitellmHealth(url) {
|
|
4042
|
-
try {
|
|
4043
|
-
const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
|
|
4044
|
-
return response.ok;
|
|
4045
|
-
} catch {
|
|
4046
|
-
return false;
|
|
4047
|
-
}
|
|
4048
|
-
}
|
|
4049
|
-
async function checkModelHealth(baseUrl, apiKey, model = "claude-haiku-4-5") {
|
|
4050
|
-
try {
|
|
4051
|
-
const res = await fetch(`${baseUrl}/v1/messages`, {
|
|
4052
|
-
method: "POST",
|
|
4053
|
-
headers: {
|
|
4054
|
-
"Content-Type": "application/json",
|
|
4055
|
-
"x-api-key": apiKey,
|
|
4056
|
-
"anthropic-version": "2023-06-01"
|
|
4057
|
-
},
|
|
4058
|
-
body: JSON.stringify({
|
|
4059
|
-
model,
|
|
4060
|
-
max_tokens: 4,
|
|
4061
|
-
messages: [{ role: "user", content: "Reply with: ok" }]
|
|
4062
|
-
}),
|
|
4063
|
-
signal: AbortSignal.timeout(3e4)
|
|
4064
|
-
});
|
|
4065
|
-
if (!res.ok) {
|
|
4066
|
-
const body2 = await res.text().catch(() => "");
|
|
4067
|
-
return { ok: false, error: `HTTP ${res.status}: ${body2.slice(0, 200)}` };
|
|
4068
|
-
}
|
|
4069
|
-
const body = await res.json();
|
|
4070
|
-
const hasAnthropicContent = Array.isArray(body.content) && body.content.some((b) => b.type === "text");
|
|
4071
|
-
const hasThinkingContent = Array.isArray(body.content) && body.content.some((b) => b.type === "thinking");
|
|
4072
|
-
const hasOpenAIContent = !!body.choices?.[0]?.message?.content;
|
|
4073
|
-
if (!hasAnthropicContent && !hasThinkingContent && !hasOpenAIContent) {
|
|
4074
|
-
return { ok: false, error: `Unexpected response format: ${JSON.stringify(body).slice(0, 200)}` };
|
|
4075
|
-
}
|
|
4076
|
-
return { ok: true };
|
|
4077
|
-
} catch (err) {
|
|
4078
|
-
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
4079
|
-
}
|
|
4080
|
-
}
|
|
4081
|
-
function generateLitellmConfig(provider, modelMap) {
|
|
4082
|
-
const apiKeyVar = providerApiKeyEnvVar(provider);
|
|
4083
|
-
const entries = ["model_list:"];
|
|
4084
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4085
|
-
for (const providerModel of Object.values(modelMap)) {
|
|
4086
|
-
if (seen.has(providerModel)) continue;
|
|
4087
|
-
seen.add(providerModel);
|
|
4088
|
-
entries.push(` - model_name: ${providerModel}`);
|
|
4089
|
-
entries.push(` litellm_params:`);
|
|
4090
|
-
entries.push(` model: ${provider}/${providerModel}`);
|
|
4091
|
-
entries.push(` api_key: os.environ/${apiKeyVar}`);
|
|
4092
|
-
}
|
|
4093
|
-
return entries.join("\n") + "\n";
|
|
4094
|
-
}
|
|
4095
|
-
function generateLitellmConfigFromStages(defaultConfig, stages) {
|
|
4096
|
-
const proxyModels = [];
|
|
4097
|
-
if (defaultConfig && defaultConfig.provider !== "claude" && defaultConfig.provider !== "anthropic") {
|
|
4098
|
-
proxyModels.push(defaultConfig);
|
|
4099
|
-
}
|
|
4100
|
-
if (stages) {
|
|
4101
|
-
for (const sc of Object.values(stages)) {
|
|
4102
|
-
if (sc.provider !== "claude" && sc.provider !== "anthropic") {
|
|
4103
|
-
proxyModels.push(sc);
|
|
4104
|
-
}
|
|
4105
|
-
}
|
|
4106
|
-
}
|
|
4107
|
-
if (proxyModels.length === 0) return void 0;
|
|
4108
|
-
const entries = ["model_list:"];
|
|
4109
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4110
|
-
for (const { provider, model } of proxyModels) {
|
|
4111
|
-
const key = `${provider}/${model}`;
|
|
4112
|
-
if (seen.has(key)) continue;
|
|
4113
|
-
seen.add(key);
|
|
4114
|
-
const apiKeyVar = providerApiKeyEnvVar(provider);
|
|
4115
|
-
entries.push(` - model_name: ${model}`);
|
|
4116
|
-
entries.push(` litellm_params:`);
|
|
4117
|
-
entries.push(` model: ${provider}/${model}`);
|
|
4118
|
-
entries.push(` api_key: os.environ/${apiKeyVar}`);
|
|
4119
|
-
}
|
|
4120
|
-
return entries.join("\n") + "\n";
|
|
4121
|
-
}
|
|
4122
|
-
async function tryStartLitellm(url, projectDir, generatedConfig) {
|
|
4123
|
-
if (!generatedConfig) {
|
|
4124
|
-
logger.warn("No provider configured in kody.config.json \u2014 cannot start LiteLLM proxy");
|
|
4125
|
-
return null;
|
|
4126
|
-
}
|
|
4127
|
-
const configPath = path24.join(os.tmpdir(), "kody-litellm-config.yaml");
|
|
4128
|
-
fs27.writeFileSync(configPath, generatedConfig);
|
|
4129
|
-
const portMatch = url.match(/:(\d+)/);
|
|
4130
|
-
const port = portMatch ? portMatch[1] : "4000";
|
|
4131
|
-
let litellmFound = false;
|
|
4132
|
-
try {
|
|
4133
|
-
execFileSync15("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
|
|
4134
|
-
litellmFound = true;
|
|
4135
|
-
} catch {
|
|
4136
|
-
try {
|
|
4137
|
-
execFileSync15("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
|
|
4138
|
-
litellmFound = true;
|
|
4139
|
-
} catch {
|
|
4140
|
-
}
|
|
4141
|
-
}
|
|
4142
|
-
if (!litellmFound) {
|
|
4143
|
-
logger.warn("litellm not installed (pip install 'litellm[proxy]')");
|
|
4144
|
-
return null;
|
|
4145
|
-
}
|
|
4146
|
-
logger.info(`Starting LiteLLM proxy on port ${port}...`);
|
|
4147
|
-
let cmd;
|
|
4148
|
-
let args2;
|
|
4149
|
-
try {
|
|
4150
|
-
execFileSync15("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
|
|
4151
|
-
cmd = "litellm";
|
|
4152
|
-
args2 = ["--config", configPath, "--port", port];
|
|
4153
|
-
} catch {
|
|
4154
|
-
cmd = "python3";
|
|
4155
|
-
args2 = ["-m", "litellm", "--config", configPath, "--port", port];
|
|
4156
|
-
}
|
|
4157
|
-
const dotenvPath = path24.join(projectDir, ".env");
|
|
4158
|
-
const dotenvVars = {};
|
|
4159
|
-
if (fs27.existsSync(dotenvPath)) {
|
|
4160
|
-
for (const rawLine of fs27.readFileSync(dotenvPath, "utf-8").split("\n")) {
|
|
4161
|
-
const line = rawLine.trim();
|
|
4162
|
-
if (!line || line.startsWith("#")) continue;
|
|
4163
|
-
const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
|
|
4164
|
-
if (match) {
|
|
4165
|
-
let value = match[2].trim();
|
|
4166
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
4167
|
-
value = value.slice(1, -1);
|
|
4168
|
-
}
|
|
4169
|
-
const commentIdx = value.indexOf(" #");
|
|
4170
|
-
if (commentIdx !== -1) value = value.slice(0, commentIdx).trim();
|
|
4171
|
-
if (value) dotenvVars[match[1]] = value;
|
|
4172
|
-
}
|
|
4173
|
-
}
|
|
4174
|
-
if (Object.keys(dotenvVars).length > 0) {
|
|
4175
|
-
logger.info(` Loaded API keys: ${Object.keys(dotenvVars).join(", ")}`);
|
|
4176
|
-
}
|
|
4177
|
-
}
|
|
4178
|
-
const { spawn: spawn2 } = await import("child_process");
|
|
4179
|
-
const child = spawn2(cmd, args2, {
|
|
4180
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
4181
|
-
detached: true,
|
|
4182
|
-
env: { ...process.env, ...dotenvVars }
|
|
4183
|
-
});
|
|
4184
|
-
let proxyStderr = "";
|
|
4185
|
-
child.stderr?.on("data", (chunk) => {
|
|
4186
|
-
proxyStderr += chunk.toString();
|
|
4187
|
-
});
|
|
4188
|
-
for (let i = 0; i < 30; i++) {
|
|
4189
|
-
await new Promise((r) => setTimeout(r, 2e3));
|
|
4190
|
-
if (await checkLitellmHealth(url)) {
|
|
4191
|
-
logger.info(`LiteLLM proxy ready at ${url}`);
|
|
4192
|
-
return child;
|
|
4193
|
-
}
|
|
4194
|
-
}
|
|
4195
|
-
if (proxyStderr) {
|
|
4196
|
-
logger.warn(`LiteLLM stderr: ${proxyStderr.slice(-1e3)}`);
|
|
4197
|
-
}
|
|
4198
|
-
logger.warn("LiteLLM proxy failed to start within 60s");
|
|
4199
|
-
child.kill();
|
|
4200
|
-
return null;
|
|
4201
|
-
}
|
|
4202
|
-
var init_litellm = __esm({
|
|
4203
|
-
"src/cli/litellm.ts"() {
|
|
4204
|
-
"use strict";
|
|
4205
|
-
init_logger();
|
|
4206
|
-
init_config();
|
|
4207
|
-
}
|
|
4208
|
-
});
|
|
4209
|
-
|
|
4210
4658
|
// src/cli/task-state.ts
|
|
4211
|
-
import * as
|
|
4212
|
-
import * as
|
|
4659
|
+
import * as fs29 from "fs";
|
|
4660
|
+
import * as path26 from "path";
|
|
4213
4661
|
function resolveTaskAction(issueNumber, existingTaskId, existingState) {
|
|
4214
4662
|
if (!existingTaskId || !existingState) {
|
|
4215
4663
|
return { action: "start-fresh", taskId: `${issueNumber}-${generateTaskId()}` };
|
|
@@ -4241,11 +4689,11 @@ function resolveTaskAction(issueNumber, existingTaskId, existingState) {
|
|
|
4241
4689
|
function resolveForIssue(issueNumber, projectDir) {
|
|
4242
4690
|
const existingTaskId = findLatestTaskForIssue(issueNumber, projectDir);
|
|
4243
4691
|
if (existingTaskId) {
|
|
4244
|
-
const statusPath =
|
|
4692
|
+
const statusPath = path26.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
|
|
4245
4693
|
let existingState = null;
|
|
4246
|
-
if (
|
|
4694
|
+
if (fs29.existsSync(statusPath)) {
|
|
4247
4695
|
try {
|
|
4248
|
-
existingState = JSON.parse(
|
|
4696
|
+
existingState = JSON.parse(fs29.readFileSync(statusPath, "utf-8"));
|
|
4249
4697
|
} catch {
|
|
4250
4698
|
}
|
|
4251
4699
|
}
|
|
@@ -4402,8 +4850,8 @@ var init_resolve = __esm({
|
|
|
4402
4850
|
|
|
4403
4851
|
// src/entry.ts
|
|
4404
4852
|
var entry_exports = {};
|
|
4405
|
-
import * as
|
|
4406
|
-
import * as
|
|
4853
|
+
import * as fs30 from "fs";
|
|
4854
|
+
import * as path27 from "path";
|
|
4407
4855
|
async function ensureLitellmProxy(config, projectDir) {
|
|
4408
4856
|
if (!anyStageNeedsProxy(config)) return null;
|
|
4409
4857
|
const litellmUrl = getLitellmUrl();
|
|
@@ -4458,9 +4906,9 @@ async function runModelHealthCheck(config) {
|
|
|
4458
4906
|
}
|
|
4459
4907
|
async function main() {
|
|
4460
4908
|
const input = parseArgs();
|
|
4461
|
-
const projectDir = input.cwd ?
|
|
4909
|
+
const projectDir = input.cwd ? path27.resolve(input.cwd) : process.cwd();
|
|
4462
4910
|
if (input.cwd) {
|
|
4463
|
-
if (!
|
|
4911
|
+
if (!fs30.existsSync(projectDir)) {
|
|
4464
4912
|
console.error(`--cwd path does not exist: ${projectDir}`);
|
|
4465
4913
|
process.exit(1);
|
|
4466
4914
|
}
|
|
@@ -4526,8 +4974,24 @@ async function main() {
|
|
|
4526
4974
|
process.exit(1);
|
|
4527
4975
|
}
|
|
4528
4976
|
}
|
|
4529
|
-
const taskDir =
|
|
4530
|
-
|
|
4977
|
+
const taskDir = path27.join(projectDir, ".kody", "tasks", taskId);
|
|
4978
|
+
fs30.mkdirSync(taskDir, { recursive: true });
|
|
4979
|
+
if (input.command === "rerun" && isTaskifyRun(taskDir)) {
|
|
4980
|
+
const marker = readTaskifyMarker(taskDir);
|
|
4981
|
+
if (marker) {
|
|
4982
|
+
logger.info(`Resuming taskify run for ${marker.ticketId ?? marker.prdFile} with PM feedback`);
|
|
4983
|
+
await taskifyCommand({
|
|
4984
|
+
ticketId: marker.ticketId,
|
|
4985
|
+
prdFile: marker.prdFile,
|
|
4986
|
+
issueNumber: marker.issueNumber ?? input.issueNumber,
|
|
4987
|
+
feedback: input.feedback,
|
|
4988
|
+
local: input.local,
|
|
4989
|
+
projectDir,
|
|
4990
|
+
taskId
|
|
4991
|
+
});
|
|
4992
|
+
return;
|
|
4993
|
+
}
|
|
4994
|
+
}
|
|
4531
4995
|
if (input.command === "status") {
|
|
4532
4996
|
printStatus(taskId, taskDir);
|
|
4533
4997
|
return;
|
|
@@ -4643,31 +5107,31 @@ async function main() {
|
|
|
4643
5107
|
logger.info("Preflight checks:");
|
|
4644
5108
|
runPreflight();
|
|
4645
5109
|
if (input.task) {
|
|
4646
|
-
|
|
5110
|
+
fs30.writeFileSync(path27.join(taskDir, "task.md"), input.task);
|
|
4647
5111
|
}
|
|
4648
|
-
const taskMdPath =
|
|
4649
|
-
if (!
|
|
5112
|
+
const taskMdPath = path27.join(taskDir, "task.md");
|
|
5113
|
+
if (!fs30.existsSync(taskMdPath) && isPRFix && input.prNumber) {
|
|
4650
5114
|
logger.info(`Fetching PR #${input.prNumber} details as task context...`);
|
|
4651
5115
|
const prDetails = getPRDetails(input.prNumber);
|
|
4652
5116
|
if (prDetails) {
|
|
4653
5117
|
const taskContent = `# ${prDetails.title}
|
|
4654
5118
|
|
|
4655
5119
|
${prDetails.body ?? ""}`;
|
|
4656
|
-
|
|
5120
|
+
fs30.writeFileSync(taskMdPath, taskContent);
|
|
4657
5121
|
logger.info(` Task loaded from PR #${input.prNumber}: ${prDetails.title}`);
|
|
4658
5122
|
}
|
|
4659
|
-
} else if (!
|
|
5123
|
+
} else if (!fs30.existsSync(taskMdPath) && input.issueNumber) {
|
|
4660
5124
|
logger.info(`Fetching issue #${input.issueNumber} body as task...`);
|
|
4661
5125
|
const issue = getIssue(input.issueNumber);
|
|
4662
5126
|
if (issue) {
|
|
4663
5127
|
const taskContent = `# ${issue.title}
|
|
4664
5128
|
|
|
4665
5129
|
${issue.body ?? ""}`;
|
|
4666
|
-
|
|
5130
|
+
fs30.writeFileSync(taskMdPath, taskContent);
|
|
4667
5131
|
logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
|
|
4668
5132
|
}
|
|
4669
5133
|
}
|
|
4670
|
-
if (!
|
|
5134
|
+
if (!fs30.existsSync(taskMdPath)) {
|
|
4671
5135
|
console.error("No task.md found. Provide --task, --issue-number, or ensure .kody/tasks/<id>/task.md exists.");
|
|
4672
5136
|
process.exit(1);
|
|
4673
5137
|
}
|
|
@@ -4805,7 +5269,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
|
|
|
4805
5269
|
}
|
|
4806
5270
|
}
|
|
4807
5271
|
const state = await runPipeline(ctx);
|
|
4808
|
-
const files =
|
|
5272
|
+
const files = fs30.readdirSync(taskDir);
|
|
4809
5273
|
console.log(`
|
|
4810
5274
|
Artifacts in ${taskDir}:`);
|
|
4811
5275
|
for (const f of files) {
|
|
@@ -4851,6 +5315,7 @@ var init_entry = __esm({
|
|
|
4851
5315
|
init_litellm();
|
|
4852
5316
|
init_task_resolution();
|
|
4853
5317
|
init_task_state();
|
|
5318
|
+
init_taskify_command();
|
|
4854
5319
|
init_config();
|
|
4855
5320
|
main().catch(async (err) => {
|
|
4856
5321
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -4869,9 +5334,9 @@ var init_entry = __esm({
|
|
|
4869
5334
|
});
|
|
4870
5335
|
|
|
4871
5336
|
// src/bin/cli.ts
|
|
4872
|
-
import * as
|
|
4873
|
-
import * as
|
|
4874
|
-
import { fileURLToPath } from "url";
|
|
5337
|
+
import * as fs31 from "fs";
|
|
5338
|
+
import * as path28 from "path";
|
|
5339
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4875
5340
|
|
|
4876
5341
|
// src/bin/commands/init.ts
|
|
4877
5342
|
import * as fs3 from "fs";
|
|
@@ -5705,7 +6170,7 @@ ${repoContext}`;
|
|
|
5705
6170
|
const output = execFileSync5("claude", [
|
|
5706
6171
|
"--print",
|
|
5707
6172
|
"--model",
|
|
5708
|
-
"haiku",
|
|
6173
|
+
"claude-haiku-4-5-20251001",
|
|
5709
6174
|
"--dangerously-skip-permissions",
|
|
5710
6175
|
memoryPrompt
|
|
5711
6176
|
], {
|
|
@@ -5808,7 +6273,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
5808
6273
|
const output = execFileSync5("claude", [
|
|
5809
6274
|
"--print",
|
|
5810
6275
|
"--model",
|
|
5811
|
-
"haiku",
|
|
6276
|
+
"claude-haiku-4-5-20251001",
|
|
5812
6277
|
"--dangerously-skip-permissions",
|
|
5813
6278
|
customizationPrompt
|
|
5814
6279
|
], {
|
|
@@ -6041,11 +6506,11 @@ Create it manually.`, cwd);
|
|
|
6041
6506
|
|
|
6042
6507
|
// src/bin/cli.ts
|
|
6043
6508
|
init_architecture_detection();
|
|
6044
|
-
var
|
|
6045
|
-
var PKG_ROOT =
|
|
6509
|
+
var __dirname2 = path28.dirname(fileURLToPath2(import.meta.url));
|
|
6510
|
+
var PKG_ROOT = path28.resolve(__dirname2, "..", "..");
|
|
6046
6511
|
function getVersion() {
|
|
6047
|
-
const pkgPath =
|
|
6048
|
-
const pkg = JSON.parse(
|
|
6512
|
+
const pkgPath = path28.join(PKG_ROOT, "package.json");
|
|
6513
|
+
const pkg = JSON.parse(fs31.readFileSync(pkgPath, "utf-8"));
|
|
6049
6514
|
return pkg.version;
|
|
6050
6515
|
}
|
|
6051
6516
|
var args = process.argv.slice(2);
|
|
@@ -6054,6 +6519,8 @@ if (command === "init") {
|
|
|
6054
6519
|
initCommand({ force: args.includes("--force") }, PKG_ROOT);
|
|
6055
6520
|
} else if (command === "bootstrap") {
|
|
6056
6521
|
bootstrapCommand({ force: args.includes("--force") }, PKG_ROOT);
|
|
6522
|
+
} else if (command === "taskify") {
|
|
6523
|
+
Promise.resolve().then(() => (init_taskify_command(), taskify_command_exports)).then(({ runTaskifyCommand: runTaskifyCommand2 }) => runTaskifyCommand2());
|
|
6057
6524
|
} else if (command === "ci-parse") {
|
|
6058
6525
|
Promise.resolve().then(() => (init_parse_inputs(), parse_inputs_exports)).then(({ runCiParse: runCiParse2 }) => runCiParse2());
|
|
6059
6526
|
} else if (command === "version" || command === "--version" || command === "-v") {
|