@kody-ade/kody-engine-lite 0.1.107 → 0.1.109
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 +1760 -1316
- package/package.json +1 -1
- package/prompts/taskify-ticket.md +112 -0
- package/templates/kody.yml +13 -1
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,1410 @@ 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
|
-
|
|
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;
|
|
841
|
+
} catch (err) {
|
|
842
|
+
logger.warn(` Failed to get latest failed run for branch ${branch}: ${ghErrorMessage(err)}`);
|
|
843
|
+
return null;
|
|
792
844
|
}
|
|
793
|
-
logger.info(` Created new branch: ${branchName}`);
|
|
794
|
-
return branchName;
|
|
795
845
|
}
|
|
796
|
-
function
|
|
797
|
-
const defaultBranch = branch ?? getDefaultBranch(cwd);
|
|
798
|
-
const current = getCurrentBranch(cwd);
|
|
799
|
-
if (current === defaultBranch) return;
|
|
846
|
+
function getLatestKodyReviewComment(prNumber) {
|
|
800
847
|
try {
|
|
801
|
-
|
|
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;
|
|
802
855
|
} catch (err) {
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
return;
|
|
856
|
+
logger.warn(` Failed to get review comments for PR #${prNumber}: ${err}`);
|
|
857
|
+
return null;
|
|
806
858
|
}
|
|
859
|
+
}
|
|
860
|
+
function getPRFeedbackSinceLastKodyAction(prNumber) {
|
|
807
861
|
try {
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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;
|
|
815
885
|
}
|
|
816
|
-
|
|
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
|
+
}
|
|
893
|
+
}
|
|
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;
|
|
817
906
|
}
|
|
818
907
|
}
|
|
819
|
-
function
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
908
|
+
function isKodyComment(comment) {
|
|
909
|
+
if (comment.user_type === "Bot") return true;
|
|
910
|
+
return KODY_MARKERS.some((marker) => comment.body.includes(marker));
|
|
911
|
+
}
|
|
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;
|
|
916
|
+
}
|
|
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
|
+
];
|
|
829
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;
|
|
830
948
|
try {
|
|
831
|
-
git
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
logger.warn(` Failed to abort merge: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}`);
|
|
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;
|
|
844
961
|
}
|
|
845
|
-
|
|
962
|
+
} catch {
|
|
846
963
|
}
|
|
964
|
+
return null;
|
|
847
965
|
}
|
|
848
|
-
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) {
|
|
849
972
|
try {
|
|
850
|
-
const
|
|
851
|
-
|
|
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;
|
|
852
983
|
} catch {
|
|
853
|
-
return
|
|
984
|
+
return null;
|
|
854
985
|
}
|
|
855
986
|
}
|
|
856
|
-
function
|
|
857
|
-
const
|
|
858
|
-
if (
|
|
859
|
-
|
|
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"() {
|
|
996
|
+
"use strict";
|
|
997
|
+
init_github_api();
|
|
860
998
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
// src/cli/taskify-command.ts
|
|
1002
|
+
var taskify_command_exports = {};
|
|
1003
|
+
__export(taskify_command_exports, {
|
|
1004
|
+
isTaskifyRun: () => isTaskifyRun,
|
|
1005
|
+
readTaskifyMarker: () => readTaskifyMarker,
|
|
1006
|
+
runTaskifyCommand: () => runTaskifyCommand,
|
|
1007
|
+
taskifyCommand: () => taskifyCommand,
|
|
1008
|
+
topoSort: () => topoSort
|
|
1009
|
+
});
|
|
1010
|
+
import * as fs10 from "fs";
|
|
1011
|
+
import * as path9 from "path";
|
|
1012
|
+
import { fileURLToPath } from "url";
|
|
1013
|
+
import { execSync } from "child_process";
|
|
1014
|
+
function topoSort(tasks) {
|
|
1015
|
+
const n = tasks.length;
|
|
1016
|
+
const inDegree = new Array(n).fill(0);
|
|
1017
|
+
const adj = Array.from({ length: n }, () => []);
|
|
1018
|
+
for (let i = 0; i < n; i++) {
|
|
1019
|
+
for (const dep of tasks[i].dependsOn ?? []) {
|
|
1020
|
+
if (dep >= 0 && dep < n && dep !== i) {
|
|
1021
|
+
adj[dep].push(i);
|
|
1022
|
+
inDegree[i]++;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
const queue = [];
|
|
1027
|
+
for (let i = 0; i < n; i++) {
|
|
1028
|
+
if (inDegree[i] === 0) queue.push(i);
|
|
1029
|
+
}
|
|
1030
|
+
const sorted = [];
|
|
1031
|
+
while (queue.length > 0) {
|
|
1032
|
+
const node = queue.shift();
|
|
1033
|
+
sorted.push(tasks[node]);
|
|
1034
|
+
for (const neighbor of adj[node]) {
|
|
1035
|
+
inDegree[neighbor]--;
|
|
1036
|
+
if (inDegree[neighbor] === 0) queue.push(neighbor);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
if (sorted.length !== n) {
|
|
1040
|
+
logger.warn("[taskify] dependency cycle detected \u2014 falling back to original order");
|
|
1041
|
+
return [...tasks];
|
|
1042
|
+
}
|
|
1043
|
+
return sorted;
|
|
866
1044
|
}
|
|
867
|
-
function
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1045
|
+
function getArg(args2, flag) {
|
|
1046
|
+
const idx = args2.indexOf(flag);
|
|
1047
|
+
return idx !== -1 ? args2[idx + 1] : void 0;
|
|
1048
|
+
}
|
|
1049
|
+
function hasFlag(args2, flag) {
|
|
1050
|
+
return args2.includes(flag);
|
|
1051
|
+
}
|
|
1052
|
+
async function runTaskifyCommand() {
|
|
1053
|
+
const args2 = process.argv.slice(3);
|
|
1054
|
+
const cwdArg = getArg(args2, "--cwd") ?? process.cwd();
|
|
1055
|
+
const projectDir = path9.resolve(cwdArg);
|
|
1056
|
+
const ticketId = getArg(args2, "--ticket") ?? process.env.TICKET_ID;
|
|
1057
|
+
const prdFileArg = getArg(args2, "--file") ?? process.env.PRD_FILE;
|
|
1058
|
+
const prdFile = prdFileArg ? path9.resolve(projectDir, prdFileArg) : void 0;
|
|
1059
|
+
const issueNumberStr = getArg(args2, "--issue-number") ?? process.env.ISSUE_NUMBER ?? "";
|
|
1060
|
+
const issueNumber = issueNumberStr ? parseInt(issueNumberStr, 10) : void 0;
|
|
1061
|
+
const feedback = getArg(args2, "--feedback") ?? process.env.FEEDBACK;
|
|
1062
|
+
const local = hasFlag(args2, "--local") || !process.env.CI;
|
|
1063
|
+
const taskIdArg = getArg(args2, "--task-id") ?? process.env.TASK_ID;
|
|
1064
|
+
const taskId = taskIdArg ?? (issueNumber ? `taskify-${issueNumber}-${generateTaskId()}` : `taskify-${generateTaskId()}`);
|
|
1065
|
+
if (!ticketId && !prdFile) {
|
|
1066
|
+
logger.error("Usage: kody taskify --ticket <ticket-id> OR kody taskify --file <prd.md>");
|
|
1067
|
+
process.exit(1);
|
|
876
1068
|
}
|
|
1069
|
+
if (prdFile && !fs10.existsSync(prdFile)) {
|
|
1070
|
+
logger.error(`File not found: ${prdFile}`);
|
|
1071
|
+
process.exit(1);
|
|
1072
|
+
}
|
|
1073
|
+
setConfigDir(projectDir);
|
|
1074
|
+
setGhCwd(projectDir);
|
|
1075
|
+
await taskifyCommand({ ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId });
|
|
877
1076
|
}
|
|
878
|
-
function
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
1077
|
+
async function taskifyCommand(opts) {
|
|
1078
|
+
const { ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId } = opts;
|
|
1079
|
+
const config = getProjectConfig();
|
|
1080
|
+
const taskDir = path9.join(projectDir, ".kody", "tasks", taskId);
|
|
1081
|
+
fs10.mkdirSync(taskDir, { recursive: true });
|
|
1082
|
+
const mode = prdFile ? "file" : "ticket";
|
|
1083
|
+
logger.info(`[taskify] mode=${mode} source=${ticketId ?? prdFile} issue=${issueNumber ?? "none"} task=${taskId}`);
|
|
1084
|
+
let mcpConfigJson;
|
|
1085
|
+
if (mode === "ticket") {
|
|
1086
|
+
try {
|
|
1087
|
+
mcpConfigJson = buildTaskifyMcpConfigJson(config);
|
|
1088
|
+
} catch (err) {
|
|
1089
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1090
|
+
logger.error(`[taskify] MCP config error: ${msg}`);
|
|
1091
|
+
if (issueNumber && !local) {
|
|
1092
|
+
postComment(
|
|
1093
|
+
issueNumber,
|
|
1094
|
+
`Kody could not start the taskify command:
|
|
1095
|
+
|
|
1096
|
+
> ${msg}
|
|
1097
|
+
|
|
1098
|
+
Add the required MCP server config to \`kody.config.json\` and try again.`
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
process.exit(1);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
const sc = resolveStageConfig(config, "taskify", "strong");
|
|
1105
|
+
const model = sc.model;
|
|
1106
|
+
const fileContent = prdFile ? fs10.readFileSync(prdFile, "utf-8") : void 0;
|
|
1107
|
+
let projectContext;
|
|
1108
|
+
{
|
|
1109
|
+
const parts = [];
|
|
1110
|
+
const memoryPath = path9.join(projectDir, ".kody", "memory.md");
|
|
1111
|
+
if (fs10.existsSync(memoryPath)) {
|
|
1112
|
+
try {
|
|
1113
|
+
const content = fs10.readFileSync(memoryPath, "utf-8").slice(0, 2e3);
|
|
1114
|
+
if (content.trim()) parts.push(`### Project Memory
|
|
1115
|
+
${content}`);
|
|
1116
|
+
} catch {
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
try {
|
|
1120
|
+
const output = execSync("git ls-files", { cwd: projectDir, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
1121
|
+
const lines = output.split("\n").filter(Boolean).slice(0, 150);
|
|
1122
|
+
if (lines.length > 0) parts.push(`### File Tree
|
|
1123
|
+
\`\`\`
|
|
1124
|
+
${lines.join("\n")}
|
|
1125
|
+
\`\`\``);
|
|
1126
|
+
} catch {
|
|
1127
|
+
}
|
|
1128
|
+
if (parts.length > 0) projectContext = parts.join("\n\n");
|
|
1129
|
+
}
|
|
1130
|
+
const prompt = buildPrompt({ ticketId, fileContent, taskDir, feedback, projectContext });
|
|
1131
|
+
if (issueNumber && !local) {
|
|
1132
|
+
const src = mode === "file" ? `file \`${path9.basename(prdFile)}\`` : `ticket **${ticketId}**`;
|
|
1133
|
+
postComment(issueNumber, `Kody is decomposing ${src} into tasks...`);
|
|
1134
|
+
setLifecycleLabel(issueNumber, "planning");
|
|
1135
|
+
}
|
|
1136
|
+
fs10.writeFileSync(path9.join(taskDir, MARKER_FILE), JSON.stringify({ ticketId, prdFile, issueNumber }));
|
|
1137
|
+
const runner = opts.runner ?? createClaudeCodeRunner();
|
|
1138
|
+
logger.info(` model=${model} timeout=${TASKIFY_TIMEOUT_MS / 1e3}s`);
|
|
1139
|
+
const result = await runner.run("taskify", prompt, model, TASKIFY_TIMEOUT_MS, taskDir, {
|
|
1140
|
+
cwd: projectDir,
|
|
1141
|
+
mcpConfigJson
|
|
1142
|
+
});
|
|
1143
|
+
if (result.outcome !== "completed") {
|
|
1144
|
+
const errMsg = result.outcome === "timed_out" ? "Taskify timed out after 5 minutes." : `Taskify failed: ${result.error}`;
|
|
1145
|
+
logger.error(`[taskify] ${errMsg}`);
|
|
1146
|
+
if (issueNumber && !local) {
|
|
1147
|
+
postComment(issueNumber, `Kody taskify failed:
|
|
1148
|
+
|
|
1149
|
+
> ${errMsg}`);
|
|
1150
|
+
setLifecycleLabel(issueNumber, "failed");
|
|
1151
|
+
}
|
|
1152
|
+
process.exit(1);
|
|
1153
|
+
}
|
|
1154
|
+
const resultPath = path9.join(taskDir, RESULT_FILE);
|
|
1155
|
+
if (!fs10.existsSync(resultPath)) {
|
|
1156
|
+
const errMsg = `Claude did not write ${RESULT_FILE}. Output:
|
|
1157
|
+
|
|
1158
|
+
${result.output?.slice(0, 500) ?? "(none)"}`;
|
|
1159
|
+
logger.error(`[taskify] ${errMsg}`);
|
|
1160
|
+
if (issueNumber && !local) {
|
|
1161
|
+
postComment(issueNumber, `Kody taskify failed: result file not found.
|
|
1162
|
+
|
|
1163
|
+
${errMsg}`);
|
|
1164
|
+
setLifecycleLabel(issueNumber, "failed");
|
|
1165
|
+
}
|
|
1166
|
+
process.exit(1);
|
|
1167
|
+
}
|
|
1168
|
+
let parsed;
|
|
1169
|
+
try {
|
|
1170
|
+
parsed = JSON.parse(fs10.readFileSync(resultPath, "utf-8"));
|
|
1171
|
+
} catch {
|
|
1172
|
+
const errMsg = `Could not parse ${RESULT_FILE} as JSON.`;
|
|
1173
|
+
logger.error(`[taskify] ${errMsg}`);
|
|
1174
|
+
if (issueNumber && !local) {
|
|
1175
|
+
postComment(issueNumber, `Kody taskify failed: ${errMsg}`);
|
|
1176
|
+
setLifecycleLabel(issueNumber, "failed");
|
|
1177
|
+
}
|
|
1178
|
+
process.exit(1);
|
|
1179
|
+
}
|
|
1180
|
+
const sourceLabel = ticketId ?? (prdFile ? path9.basename(prdFile) : "spec");
|
|
1181
|
+
if (parsed.status === "questions") {
|
|
1182
|
+
handleQuestions(parsed, sourceLabel, issueNumber, local ?? false);
|
|
1183
|
+
} else if (parsed.status === "ready") {
|
|
1184
|
+
await handleTasks(parsed, sourceLabel, issueNumber, local ?? false);
|
|
1185
|
+
} else {
|
|
1186
|
+
const errMsg = `Unexpected status in ${RESULT_FILE}: ${JSON.stringify(parsed)}`;
|
|
1187
|
+
logger.error(`[taskify] ${errMsg}`);
|
|
1188
|
+
if (issueNumber && !local) {
|
|
1189
|
+
postComment(issueNumber, `Kody taskify failed: ${errMsg}`);
|
|
1190
|
+
setLifecycleLabel(issueNumber, "failed");
|
|
1191
|
+
}
|
|
1192
|
+
process.exit(1);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
function handleQuestions(parsed, ticketId, issueNumber, local) {
|
|
1196
|
+
const questions = parsed.questions ?? [];
|
|
1197
|
+
const numbered = questions.map((q, i) => `${i + 1}. ${q}`).join("\n");
|
|
1198
|
+
const comment = `Kody has questions before decomposing **${ticketId}**:
|
|
1199
|
+
|
|
1200
|
+
${numbered}
|
|
1201
|
+
|
|
1202
|
+
Reply with \`@kody approve\` and your answers to proceed.`;
|
|
1203
|
+
logger.info(`[taskify] posting ${questions.length} question(s)`);
|
|
1204
|
+
if (issueNumber && !local) {
|
|
1205
|
+
postComment(issueNumber, comment);
|
|
1206
|
+
setLifecycleLabel(issueNumber, "waiting");
|
|
1207
|
+
} else {
|
|
1208
|
+
logger.info(`[taskify] questions:
|
|
1209
|
+
${comment}`);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
async function handleTasks(parsed, ticketId, issueNumber, local) {
|
|
1213
|
+
const tasks = topoSort(parsed.tasks ?? []);
|
|
1214
|
+
if (tasks.length === 0) {
|
|
1215
|
+
logger.warn("[taskify] no tasks in result \u2014 nothing to file");
|
|
1216
|
+
if (issueNumber && !local) {
|
|
1217
|
+
postComment(issueNumber, `Kody taskify completed but found no tasks to file for **${ticketId}**.`);
|
|
1218
|
+
setLifecycleLabel(issueNumber, "done");
|
|
1219
|
+
}
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
const tooMany = tasks.length > MAX_TASKS_GUARD;
|
|
1223
|
+
if (tooMany) {
|
|
1224
|
+
logger.warn(`[taskify] ${tasks.length} tasks exceeds MAX_TASKS_GUARD (${MAX_TASKS_GUARD}) \u2014 filing issues but skipping auto-trigger`);
|
|
1225
|
+
}
|
|
1226
|
+
logger.info(`[taskify] filing ${tasks.length} issue(s)`);
|
|
1227
|
+
const filed = [];
|
|
1228
|
+
for (const task of tasks) {
|
|
1229
|
+
if (local) {
|
|
1230
|
+
logger.info(` [local] would create issue: ${task.title}`);
|
|
1231
|
+
filed.push({ number: 0, url: "#", title: task.title });
|
|
1232
|
+
continue;
|
|
1233
|
+
}
|
|
1234
|
+
const allLabels = [...task.labels ?? [], ...task.priority ? [`priority:${task.priority}`] : []];
|
|
1235
|
+
const issue = createIssue(task.title, task.body, allLabels);
|
|
1236
|
+
if (issue) {
|
|
1237
|
+
filed.push({ number: issue.number, url: issue.url, title: task.title });
|
|
1238
|
+
} else {
|
|
1239
|
+
logger.warn(` failed to create issue: ${task.title}`);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
const autoTrigger = !tooMany && filed.length <= AUTO_TRIGGER_THRESHOLD;
|
|
1243
|
+
if (autoTrigger && !local) {
|
|
1244
|
+
for (const issue of filed) {
|
|
1245
|
+
if (issue.number > 0) {
|
|
1246
|
+
postComment(issue.number, "@kody");
|
|
1247
|
+
logger.info(` auto-triggered @kody on issue #${issue.number}`);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (issueNumber && !local) {
|
|
1252
|
+
const links = filed.map((i) => `- [#${i.number}](${i.url}) \u2014 ${i.title}`).join("\n");
|
|
1253
|
+
const triggerNote = tooMany ? `
|
|
1254
|
+
|
|
1255
|
+
> **${tasks.length} tasks filed** \u2014 auto-trigger is disabled for large epics. Comment \`@kody\` on each issue to start the pipeline.` : autoTrigger ? `
|
|
1256
|
+
|
|
1257
|
+
> Auto-triggered \`@kody\` on each issue.` : `
|
|
1258
|
+
|
|
1259
|
+
> Comment \`@kody\` on each issue to start the pipeline.`;
|
|
1260
|
+
postComment(
|
|
1261
|
+
issueNumber,
|
|
1262
|
+
`Kody decomposed **${ticketId}** into ${filed.length} task(s):
|
|
1263
|
+
|
|
1264
|
+
${links}${triggerNote}`
|
|
1265
|
+
);
|
|
1266
|
+
setLifecycleLabel(issueNumber, "done");
|
|
1267
|
+
} else if (local) {
|
|
1268
|
+
logger.info(`[taskify] local mode \u2014 would file ${filed.length} issue(s)`);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
function buildPrompt(opts) {
|
|
1272
|
+
const { ticketId, fileContent, taskDir, feedback, projectContext } = opts;
|
|
1273
|
+
const scriptDir = new URL(".", import.meta.url).pathname;
|
|
1274
|
+
const candidates = [
|
|
1275
|
+
path9.resolve(scriptDir, "..", "prompts", "taskify-ticket.md"),
|
|
1276
|
+
path9.resolve(scriptDir, "..", "..", "prompts", "taskify-ticket.md"),
|
|
1277
|
+
path9.resolve(__dirname, "..", "..", "prompts", "taskify-ticket.md"),
|
|
1278
|
+
path9.resolve(__dirname, "..", "prompts", "taskify-ticket.md")
|
|
1279
|
+
];
|
|
1280
|
+
let template = "";
|
|
1281
|
+
for (const candidate of candidates) {
|
|
1282
|
+
if (fs10.existsSync(candidate)) {
|
|
1283
|
+
template = fs10.readFileSync(candidate, "utf-8");
|
|
1284
|
+
break;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
if (!template) {
|
|
1288
|
+
throw new Error(`Could not find prompts/taskify-ticket.md. Searched: ${candidates.join(", ")}`);
|
|
1289
|
+
}
|
|
1290
|
+
const resolveBlock = (name, value) => {
|
|
1291
|
+
if (value) {
|
|
1292
|
+
template = template.replace(new RegExp(`\\{\\{#if ${name}\\}\\}\\n?([\\s\\S]*?)\\{\\{\\/if\\}\\}`, "g"), "$1");
|
|
1293
|
+
template = template.replace(new RegExp(`\\{\\{${name}\\}\\}`, "g"), value);
|
|
1294
|
+
} else {
|
|
1295
|
+
template = template.replace(new RegExp(`\\{\\{#if ${name}\\}\\}\\n?[\\s\\S]*?\\{\\{\\/if\\}\\}\\n?`, "g"), "");
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
resolveBlock("PROJECT_CONTEXT", projectContext);
|
|
1299
|
+
resolveBlock("TICKET_ID", ticketId);
|
|
1300
|
+
resolveBlock("FILE_CONTENT", fileContent);
|
|
1301
|
+
resolveBlock("FEEDBACK", feedback);
|
|
1302
|
+
template = template.replace(/\{\{TASK_DIR\}\}/g, taskDir);
|
|
1303
|
+
return template;
|
|
1304
|
+
}
|
|
1305
|
+
function isTaskifyRun(taskDir) {
|
|
1306
|
+
return fs10.existsSync(path9.join(taskDir, MARKER_FILE));
|
|
1307
|
+
}
|
|
1308
|
+
function readTaskifyMarker(taskDir) {
|
|
1309
|
+
const markerPath = path9.join(taskDir, MARKER_FILE);
|
|
1310
|
+
if (!fs10.existsSync(markerPath)) return null;
|
|
1311
|
+
try {
|
|
1312
|
+
return JSON.parse(fs10.readFileSync(markerPath, "utf-8"));
|
|
1313
|
+
} catch {
|
|
1314
|
+
return null;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
var __dirname, AUTO_TRIGGER_THRESHOLD, MAX_TASKS_GUARD, TASKIFY_TIMEOUT_MS, MARKER_FILE, RESULT_FILE;
|
|
1318
|
+
var init_taskify_command = __esm({
|
|
1319
|
+
"src/cli/taskify-command.ts"() {
|
|
1320
|
+
"use strict";
|
|
1321
|
+
init_config();
|
|
1322
|
+
init_agent_runner();
|
|
1323
|
+
init_mcp_config();
|
|
1324
|
+
init_github_api();
|
|
1325
|
+
init_logger();
|
|
1326
|
+
init_task_resolution();
|
|
1327
|
+
__dirname = path9.dirname(fileURLToPath(import.meta.url));
|
|
1328
|
+
AUTO_TRIGGER_THRESHOLD = 5;
|
|
1329
|
+
MAX_TASKS_GUARD = 20;
|
|
1330
|
+
TASKIFY_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
1331
|
+
MARKER_FILE = "taskify.marker";
|
|
1332
|
+
RESULT_FILE = "taskify-result.json";
|
|
1333
|
+
}
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
// src/ci/parse-inputs.ts
|
|
1337
|
+
var parse_inputs_exports = {};
|
|
1338
|
+
__export(parse_inputs_exports, {
|
|
1339
|
+
parseCommentInputs: () => parseCommentInputs,
|
|
1340
|
+
runCiParse: () => runCiParse,
|
|
1341
|
+
writeOutputs: () => writeOutputs
|
|
1342
|
+
});
|
|
1343
|
+
import * as fs11 from "fs";
|
|
1344
|
+
function generateTimestamp() {
|
|
1345
|
+
const now = /* @__PURE__ */ new Date();
|
|
1346
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1347
|
+
const y = String(now.getFullYear()).slice(2);
|
|
1348
|
+
const m = pad(now.getMonth() + 1);
|
|
1349
|
+
const d = pad(now.getDate());
|
|
1350
|
+
const H = pad(now.getHours());
|
|
1351
|
+
const M = pad(now.getMinutes());
|
|
1352
|
+
const S = pad(now.getSeconds());
|
|
1353
|
+
return `${y}${m}${d}-${H}${M}${S}`;
|
|
1354
|
+
}
|
|
1355
|
+
function parseCommentInputs() {
|
|
1356
|
+
const triggerType = process.env.TRIGGER_TYPE ?? "dispatch";
|
|
1357
|
+
if (triggerType === "dispatch") {
|
|
1358
|
+
const taskId2 = process.env.INPUT_TASK_ID ?? "";
|
|
1359
|
+
return {
|
|
1360
|
+
task_id: taskId2,
|
|
1361
|
+
mode: process.env.INPUT_MODE ?? "full",
|
|
1362
|
+
from_stage: process.env.INPUT_FROM_STAGE ?? "",
|
|
1363
|
+
issue_number: process.env.INPUT_ISSUE_NUMBER ?? "",
|
|
1364
|
+
pr_number: "",
|
|
1365
|
+
feedback: process.env.INPUT_FEEDBACK ?? "",
|
|
1366
|
+
complexity: "",
|
|
1367
|
+
ci_run_id: "",
|
|
1368
|
+
ticket_id: "",
|
|
1369
|
+
prd_file: "",
|
|
1370
|
+
dry_run: false,
|
|
1371
|
+
valid: !!taskId2,
|
|
1372
|
+
trigger_type: "dispatch"
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
const commentBody = (process.env.COMMENT_BODY ?? "").replace(/\r/g, "");
|
|
1376
|
+
const issueNumber = process.env.ISSUE_NUMBER ?? "";
|
|
1377
|
+
const isPR = !!process.env.ISSUE_IS_PR;
|
|
1378
|
+
const kodyMatch = commentBody.match(/(?:@kody|\/kody)\s*(.*)/i);
|
|
1379
|
+
if (!kodyMatch) {
|
|
1380
|
+
return {
|
|
1381
|
+
task_id: "",
|
|
1382
|
+
mode: "full",
|
|
1383
|
+
from_stage: "",
|
|
1384
|
+
issue_number: issueNumber,
|
|
1385
|
+
pr_number: "",
|
|
1386
|
+
feedback: "",
|
|
1387
|
+
complexity: "",
|
|
1388
|
+
ci_run_id: "",
|
|
1389
|
+
ticket_id: "",
|
|
1390
|
+
prd_file: "",
|
|
1391
|
+
dry_run: false,
|
|
1392
|
+
valid: false,
|
|
1393
|
+
trigger_type: "comment"
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
const argsLine = kodyMatch[1].trim();
|
|
1397
|
+
let fromStage = "";
|
|
1398
|
+
let feedback = "";
|
|
1399
|
+
let complexity = "";
|
|
1400
|
+
let dryRun = false;
|
|
1401
|
+
let ciRunId = "";
|
|
1402
|
+
let ticketId = "";
|
|
1403
|
+
let prdFile = "";
|
|
1404
|
+
const fromMatch = argsLine.match(/--from\s+(\S+)/);
|
|
1405
|
+
if (fromMatch) fromStage = fromMatch[1];
|
|
1406
|
+
const feedbackMatch = argsLine.match(/--feedback\s+"([^"]*)"/);
|
|
1407
|
+
if (feedbackMatch) feedback = feedbackMatch[1];
|
|
1408
|
+
const complexityMatch = argsLine.match(/--complexity\s+(\S+)/);
|
|
1409
|
+
if (complexityMatch) complexity = complexityMatch[1];
|
|
1410
|
+
if (/--dry-run/.test(argsLine)) dryRun = true;
|
|
1411
|
+
const ciRunIdMatch = argsLine.match(/--ci-run-id\s+(\S+)/);
|
|
1412
|
+
if (ciRunIdMatch) ciRunId = ciRunIdMatch[1];
|
|
1413
|
+
const ticketMatch = argsLine.match(/--ticket\s+(\S+)/);
|
|
1414
|
+
if (ticketMatch) ticketId = ticketMatch[1];
|
|
1415
|
+
const fileMatch = argsLine.match(/--file\s+(\S+)/);
|
|
1416
|
+
if (fileMatch) prdFile = fileMatch[1];
|
|
1417
|
+
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();
|
|
1418
|
+
const parts = positional ? positional.split(/\s+/) : [];
|
|
1419
|
+
let mode = "full";
|
|
1420
|
+
let taskId = "";
|
|
1421
|
+
let idx = 0;
|
|
1422
|
+
if (parts[idx] && VALID_MODES.includes(parts[idx])) {
|
|
1423
|
+
mode = parts[idx];
|
|
1424
|
+
idx++;
|
|
1425
|
+
}
|
|
1426
|
+
if (parts[idx] && !parts[idx].startsWith("--")) {
|
|
1427
|
+
taskId = parts[idx];
|
|
1428
|
+
idx++;
|
|
1429
|
+
} else if (parts[0] && !VALID_MODES.includes(parts[0]) && !parts[0].startsWith("--")) {
|
|
1430
|
+
taskId = parts[0];
|
|
1431
|
+
}
|
|
1432
|
+
const kodyLineIdx = commentBody.search(/(?:@kody|\/kody)/i);
|
|
1433
|
+
const afterKodyLine = commentBody.slice(kodyLineIdx);
|
|
1434
|
+
const newlineIdx = afterKodyLine.indexOf("\n");
|
|
1435
|
+
const bodyAfterCommand = newlineIdx !== -1 ? afterKodyLine.slice(newlineIdx + 1) : "";
|
|
1436
|
+
if (mode === "approve") {
|
|
1437
|
+
mode = "rerun";
|
|
1438
|
+
if (bodyAfterCommand) {
|
|
1439
|
+
feedback = bodyAfterCommand;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
if (mode === "fix") {
|
|
1443
|
+
if (bodyAfterCommand) {
|
|
1444
|
+
feedback = bodyAfterCommand;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
if (mode === "fix-ci") {
|
|
1448
|
+
if (bodyAfterCommand) {
|
|
1449
|
+
feedback = bodyAfterCommand;
|
|
1450
|
+
const runIdFromBody = bodyAfterCommand.match(/Run ID:\s*(\d+)/);
|
|
1451
|
+
if (runIdFromBody) {
|
|
1452
|
+
ciRunId = runIdFromBody[1];
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
if (mode === "bootstrap") {
|
|
1457
|
+
taskId = `bootstrap-${generateTimestamp()}`;
|
|
1458
|
+
}
|
|
1459
|
+
if (mode === "taskify") {
|
|
1460
|
+
taskId = `taskify-${issueNumber}-${generateTimestamp()}`;
|
|
1461
|
+
}
|
|
1462
|
+
const prNumber = isPR ? issueNumber : "";
|
|
1463
|
+
if (mode === "review" && prNumber) {
|
|
1464
|
+
taskId = `review-pr-${prNumber}-${generateTimestamp()}`;
|
|
1465
|
+
}
|
|
1466
|
+
if (!taskId && mode === "full") {
|
|
1467
|
+
taskId = `${issueNumber}-${generateTimestamp()}`;
|
|
1468
|
+
}
|
|
1469
|
+
const modesWithoutTaskId = ["fix", "fix-ci", "status", "review", "resolve", "rerun"];
|
|
1470
|
+
const valid = !!taskId || modesWithoutTaskId.includes(mode);
|
|
1471
|
+
if (mode === "taskify" && !ticketId && !prdFile) {
|
|
1472
|
+
return {
|
|
1473
|
+
task_id: taskId,
|
|
1474
|
+
mode,
|
|
1475
|
+
from_stage: fromStage,
|
|
1476
|
+
issue_number: issueNumber,
|
|
1477
|
+
pr_number: "",
|
|
1478
|
+
feedback,
|
|
1479
|
+
complexity,
|
|
1480
|
+
ci_run_id: ciRunId,
|
|
1481
|
+
ticket_id: "",
|
|
1482
|
+
prd_file: "",
|
|
1483
|
+
dry_run: dryRun,
|
|
1484
|
+
valid: false,
|
|
1485
|
+
trigger_type: "comment"
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
return {
|
|
1489
|
+
task_id: taskId,
|
|
1490
|
+
mode,
|
|
1491
|
+
from_stage: fromStage,
|
|
1492
|
+
issue_number: issueNumber,
|
|
1493
|
+
pr_number: prNumber,
|
|
1494
|
+
feedback,
|
|
1495
|
+
complexity,
|
|
1496
|
+
ci_run_id: ciRunId,
|
|
1497
|
+
ticket_id: ticketId,
|
|
1498
|
+
prd_file: prdFile,
|
|
1499
|
+
dry_run: dryRun,
|
|
1500
|
+
valid,
|
|
1501
|
+
trigger_type: "comment"
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
function writeOutputs(result) {
|
|
1505
|
+
const outputFile = process.env.GITHUB_OUTPUT;
|
|
1506
|
+
function output(key, value) {
|
|
1507
|
+
if (outputFile) {
|
|
1508
|
+
if (value.includes("\n")) {
|
|
1509
|
+
fs11.appendFileSync(outputFile, `${key}<<KODY_EOF
|
|
1510
|
+
${value}
|
|
1511
|
+
KODY_EOF
|
|
1512
|
+
`);
|
|
1513
|
+
} else {
|
|
1514
|
+
fs11.appendFileSync(outputFile, `${key}=${value}
|
|
1515
|
+
`);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
const display = value.includes("\n") ? value.split("\n")[0] + "..." : value;
|
|
1519
|
+
console.log(`${key}=${display}`);
|
|
1520
|
+
}
|
|
1521
|
+
output("task_id", result.task_id);
|
|
1522
|
+
output("mode", result.mode);
|
|
1523
|
+
output("from_stage", result.from_stage);
|
|
1524
|
+
output("issue_number", result.issue_number);
|
|
1525
|
+
output("pr_number", result.pr_number);
|
|
1526
|
+
output("feedback", result.feedback);
|
|
1527
|
+
output("complexity", result.complexity);
|
|
1528
|
+
output("ci_run_id", result.ci_run_id);
|
|
1529
|
+
output("ticket_id", result.ticket_id);
|
|
1530
|
+
output("prd_file", result.prd_file);
|
|
1531
|
+
output("dry_run", result.dry_run ? "true" : "false");
|
|
1532
|
+
output("valid", result.valid ? "true" : "false");
|
|
1533
|
+
output("trigger_type", result.trigger_type);
|
|
1534
|
+
}
|
|
1535
|
+
function runCiParse() {
|
|
1536
|
+
const result = parseCommentInputs();
|
|
1537
|
+
writeOutputs(result);
|
|
1538
|
+
}
|
|
1539
|
+
var VALID_MODES;
|
|
1540
|
+
var init_parse_inputs = __esm({
|
|
1541
|
+
"src/ci/parse-inputs.ts"() {
|
|
1542
|
+
"use strict";
|
|
1543
|
+
VALID_MODES = [
|
|
1544
|
+
"full",
|
|
1545
|
+
"rerun",
|
|
1546
|
+
"fix",
|
|
1547
|
+
"fix-ci",
|
|
1548
|
+
"status",
|
|
1549
|
+
"approve",
|
|
1550
|
+
"review",
|
|
1551
|
+
"resolve",
|
|
1552
|
+
"bootstrap",
|
|
1553
|
+
"taskify"
|
|
1554
|
+
];
|
|
1555
|
+
}
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
// src/definitions.ts
|
|
1559
|
+
var definitions_exports = {};
|
|
1560
|
+
__export(definitions_exports, {
|
|
1561
|
+
STAGES: () => STAGES,
|
|
1562
|
+
applyTimeoutOverrides: () => applyTimeoutOverrides,
|
|
1563
|
+
getStage: () => getStage
|
|
1564
|
+
});
|
|
1565
|
+
function getStage(name) {
|
|
1566
|
+
return STAGES.find((s) => s.name === name);
|
|
1567
|
+
}
|
|
1568
|
+
function applyTimeoutOverrides(overrides) {
|
|
1569
|
+
for (const stage of STAGES) {
|
|
1570
|
+
if (overrides[stage.name] != null) {
|
|
1571
|
+
stage.timeout = overrides[stage.name] * 1e3;
|
|
1572
|
+
}
|
|
884
1573
|
}
|
|
885
|
-
logger.info(" Pushed to origin");
|
|
886
1574
|
}
|
|
887
|
-
var
|
|
888
|
-
var
|
|
889
|
-
"src/
|
|
1575
|
+
var STAGES;
|
|
1576
|
+
var init_definitions = __esm({
|
|
1577
|
+
"src/definitions.ts"() {
|
|
890
1578
|
"use strict";
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1579
|
+
STAGES = [
|
|
1580
|
+
{
|
|
1581
|
+
name: "taskify",
|
|
1582
|
+
type: "agent",
|
|
1583
|
+
modelTier: "cheap",
|
|
1584
|
+
timeout: 6e5,
|
|
1585
|
+
maxRetries: 1,
|
|
1586
|
+
outputFile: "task.json"
|
|
1587
|
+
},
|
|
1588
|
+
{
|
|
1589
|
+
name: "plan",
|
|
1590
|
+
type: "agent",
|
|
1591
|
+
modelTier: "strong",
|
|
1592
|
+
timeout: 6e5,
|
|
1593
|
+
maxRetries: 1,
|
|
1594
|
+
outputFile: "plan.md"
|
|
1595
|
+
},
|
|
1596
|
+
{
|
|
1597
|
+
name: "build",
|
|
1598
|
+
type: "agent",
|
|
1599
|
+
modelTier: "mid",
|
|
1600
|
+
timeout: 24e5,
|
|
1601
|
+
maxRetries: 1
|
|
1602
|
+
},
|
|
1603
|
+
{
|
|
1604
|
+
name: "verify",
|
|
1605
|
+
type: "gate",
|
|
1606
|
+
modelTier: "cheap",
|
|
1607
|
+
timeout: 3e5,
|
|
1608
|
+
maxRetries: 2,
|
|
1609
|
+
retryWithAgent: "autofix"
|
|
1610
|
+
},
|
|
1611
|
+
{
|
|
1612
|
+
name: "review",
|
|
1613
|
+
type: "agent",
|
|
1614
|
+
modelTier: "strong",
|
|
1615
|
+
timeout: 6e5,
|
|
1616
|
+
maxRetries: 1,
|
|
1617
|
+
outputFile: "review.md"
|
|
1618
|
+
},
|
|
1619
|
+
{
|
|
1620
|
+
name: "review-fix",
|
|
1621
|
+
type: "agent",
|
|
1622
|
+
modelTier: "mid",
|
|
1623
|
+
timeout: 12e5,
|
|
1624
|
+
maxRetries: 1
|
|
1625
|
+
},
|
|
1626
|
+
{
|
|
1627
|
+
name: "ship",
|
|
1628
|
+
type: "deterministic",
|
|
1629
|
+
modelTier: "cheap",
|
|
1630
|
+
timeout: 24e4,
|
|
1631
|
+
maxRetries: 1,
|
|
1632
|
+
outputFile: "ship.md"
|
|
1633
|
+
}
|
|
1634
|
+
];
|
|
895
1635
|
}
|
|
896
1636
|
});
|
|
897
1637
|
|
|
898
|
-
// src/
|
|
899
|
-
import { execFileSync as
|
|
900
|
-
function
|
|
901
|
-
|
|
902
|
-
}
|
|
903
|
-
function ghErrorMessage(err) {
|
|
904
|
-
if (isGhExecError(err)) {
|
|
905
|
-
const stderr = err.stderr?.toString().trim();
|
|
906
|
-
if (stderr) return stderr;
|
|
1638
|
+
// src/git-utils.ts
|
|
1639
|
+
import { execFileSync as execFileSync9 } from "child_process";
|
|
1640
|
+
function getHookSafeEnv() {
|
|
1641
|
+
if (!_hookSafeEnv) {
|
|
1642
|
+
_hookSafeEnv = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
|
|
907
1643
|
}
|
|
908
|
-
return
|
|
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
|
-
}
|
|
914
|
-
function setGhCwd(cwd) {
|
|
915
|
-
_ghCwd = cwd;
|
|
916
|
-
}
|
|
917
|
-
function ghToken() {
|
|
918
|
-
return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
|
|
1644
|
+
return _hookSafeEnv;
|
|
919
1645
|
}
|
|
920
|
-
function
|
|
921
|
-
|
|
922
|
-
const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
|
|
923
|
-
return execFileSync8("gh", args2, {
|
|
1646
|
+
function git(args2, options) {
|
|
1647
|
+
return execFileSync9("git", args2, {
|
|
924
1648
|
encoding: "utf-8",
|
|
925
|
-
timeout:
|
|
926
|
-
cwd:
|
|
927
|
-
env,
|
|
928
|
-
|
|
929
|
-
stdio: options?.input ? ["pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
|
|
1649
|
+
timeout: options?.timeout ?? 3e4,
|
|
1650
|
+
cwd: options?.cwd,
|
|
1651
|
+
env: options?.env ?? getHookSafeEnv(),
|
|
1652
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
930
1653
|
}).trim();
|
|
931
1654
|
}
|
|
932
|
-
function
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
"issue",
|
|
936
|
-
"view",
|
|
937
|
-
String(issueNumber),
|
|
938
|
-
"--json",
|
|
939
|
-
"body,title"
|
|
940
|
-
]);
|
|
941
|
-
const parsed = JSON.parse(output);
|
|
942
|
-
if (!parsed || typeof parsed.title !== "string") {
|
|
943
|
-
logger.warn(` Issue #${issueNumber}: unexpected response shape`);
|
|
944
|
-
return null;
|
|
945
|
-
}
|
|
946
|
-
return { body: parsed.body ?? "", title: parsed.title };
|
|
947
|
-
} catch (err) {
|
|
948
|
-
if (isNotFoundError(err)) {
|
|
949
|
-
logger.info(` Issue #${issueNumber} not found`);
|
|
950
|
-
} else {
|
|
951
|
-
logger.error(` Failed to get issue #${issueNumber}: ${ghErrorMessage(err)}`);
|
|
952
|
-
}
|
|
953
|
-
return null;
|
|
954
|
-
}
|
|
1655
|
+
function deriveBranchName(issueNumber, title) {
|
|
1656
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").slice(0, 50).replace(/-$/, "");
|
|
1657
|
+
return `${issueNumber}-${slug}`;
|
|
955
1658
|
}
|
|
956
|
-
function
|
|
1659
|
+
function getDefaultBranch(cwd) {
|
|
957
1660
|
try {
|
|
958
|
-
const
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
"[.[] | {body, created_at}]"
|
|
963
|
-
]);
|
|
964
|
-
return output ? JSON.parse(output) : [];
|
|
1661
|
+
const config = getProjectConfig();
|
|
1662
|
+
if (config.git?.defaultBranch) {
|
|
1663
|
+
return config.git.defaultBranch;
|
|
1664
|
+
}
|
|
965
1665
|
} catch {
|
|
966
|
-
return [];
|
|
967
1666
|
}
|
|
968
|
-
}
|
|
969
|
-
function getIssueLabels(issueNumber) {
|
|
970
1667
|
try {
|
|
971
|
-
const
|
|
972
|
-
return
|
|
1668
|
+
const ref = git(["symbolic-ref", "refs/remotes/origin/HEAD"], { cwd });
|
|
1669
|
+
return ref.replace("refs/remotes/origin/", "");
|
|
973
1670
|
} catch {
|
|
974
|
-
return [];
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
function setLabel(issueNumber, label) {
|
|
978
|
-
try {
|
|
979
|
-
gh(["issue", "edit", String(issueNumber), "--add-label", label]);
|
|
980
|
-
logger.info(` Label added: ${label}`);
|
|
981
|
-
} catch (err) {
|
|
982
|
-
logger.warn(` Failed to set label ${label}: ${err}`);
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
function postComment(issueNumber, body) {
|
|
986
|
-
try {
|
|
987
|
-
gh(
|
|
988
|
-
["issue", "comment", String(issueNumber), "--body-file", "-"],
|
|
989
|
-
{ input: body }
|
|
990
|
-
);
|
|
991
|
-
logger.info(` Comment posted on #${issueNumber}`);
|
|
992
|
-
} catch (err) {
|
|
993
|
-
logger.warn(` Failed to post comment: ${err}`);
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
function getPRForBranch(branch) {
|
|
997
|
-
try {
|
|
998
|
-
const output = gh([
|
|
999
|
-
"pr",
|
|
1000
|
-
"view",
|
|
1001
|
-
branch,
|
|
1002
|
-
"--json",
|
|
1003
|
-
"number,url"
|
|
1004
|
-
]);
|
|
1005
|
-
const data = JSON.parse(output);
|
|
1006
|
-
if (typeof data.number !== "number" || typeof data.url !== "string") {
|
|
1007
|
-
logger.warn(` PR for branch ${branch}: unexpected response shape`);
|
|
1008
|
-
return null;
|
|
1009
|
-
}
|
|
1010
|
-
return { number: data.number, url: data.url };
|
|
1011
|
-
} catch (err) {
|
|
1012
|
-
if (!isNotFoundError(err)) {
|
|
1013
|
-
logger.warn(` Failed to check PR for branch ${branch}: ${ghErrorMessage(err)}`);
|
|
1014
|
-
}
|
|
1015
|
-
return null;
|
|
1016
1671
|
}
|
|
1017
|
-
}
|
|
1018
|
-
function updatePR(prNumber, body) {
|
|
1019
1672
|
try {
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
logger.info(` PR #${prNumber} body updated`);
|
|
1025
|
-
} catch (err) {
|
|
1026
|
-
logger.warn(` Failed to update PR #${prNumber}: ${err}`);
|
|
1673
|
+
const output = git(["remote", "show", "origin"], { cwd, timeout: 1e4 });
|
|
1674
|
+
const match = output.match(/HEAD branch:\s*(\S+)/);
|
|
1675
|
+
if (match) return match[1];
|
|
1676
|
+
} catch {
|
|
1027
1677
|
}
|
|
1678
|
+
return "dev";
|
|
1028
1679
|
}
|
|
1029
|
-
function
|
|
1030
|
-
|
|
1031
|
-
const output = gh(
|
|
1032
|
-
[
|
|
1033
|
-
"pr",
|
|
1034
|
-
"create",
|
|
1035
|
-
"--head",
|
|
1036
|
-
head,
|
|
1037
|
-
"--base",
|
|
1038
|
-
base,
|
|
1039
|
-
"--title",
|
|
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;
|
|
1055
|
-
}
|
|
1680
|
+
function getCurrentBranch(cwd) {
|
|
1681
|
+
return git(["branch", "--show-current"], { cwd });
|
|
1056
1682
|
}
|
|
1057
|
-
function
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1683
|
+
function ensureFeatureBranch(issueNumber, title, cwd) {
|
|
1684
|
+
const current = getCurrentBranch(cwd);
|
|
1685
|
+
const branchName = deriveBranchName(issueNumber, title);
|
|
1686
|
+
if (current === branchName || current.startsWith(`${issueNumber}-`)) {
|
|
1687
|
+
logger.info(` Already on feature branch: ${current}`);
|
|
1688
|
+
return current;
|
|
1061
1689
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1690
|
+
if (!BASE_BRANCHES.includes(current) && current !== "") {
|
|
1691
|
+
const defaultBranch2 = getDefaultBranch(cwd);
|
|
1692
|
+
logger.info(` Switching from ${current} to ${defaultBranch2} before creating ${branchName}`);
|
|
1064
1693
|
try {
|
|
1065
|
-
|
|
1694
|
+
git(["checkout", defaultBranch2], { cwd });
|
|
1066
1695
|
} catch {
|
|
1696
|
+
logger.warn(` Failed to checkout ${defaultBranch2}, aborting branch creation`);
|
|
1697
|
+
return current;
|
|
1067
1698
|
}
|
|
1068
1699
|
}
|
|
1069
|
-
setLabel(issueNumber, `kody:${phase}`);
|
|
1070
|
-
}
|
|
1071
|
-
function getPRsForIssue(issueNumber) {
|
|
1072
1700
|
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;
|
|
1701
|
+
git(["fetch", "origin"], { cwd, timeout: 3e4 });
|
|
1108
1702
|
} catch (err) {
|
|
1109
|
-
|
|
1110
|
-
|
|
1703
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1704
|
+
logger.warn(` Failed to fetch origin: ${msg}`);
|
|
1111
1705
|
}
|
|
1112
|
-
}
|
|
1113
|
-
function getPRDetails(prNumber) {
|
|
1114
1706
|
try {
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
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
|
-
};
|
|
1133
|
-
} catch (err) {
|
|
1134
|
-
if (isNotFoundError(err)) {
|
|
1135
|
-
logger.info(` PR #${prNumber} not found`);
|
|
1136
|
-
} else {
|
|
1137
|
-
logger.error(` Failed to get PR #${prNumber}: ${ghErrorMessage(err)}`);
|
|
1138
|
-
}
|
|
1139
|
-
return null;
|
|
1707
|
+
git(["rev-parse", "--verify", `origin/${branchName}`], { cwd });
|
|
1708
|
+
git(["checkout", branchName], { cwd });
|
|
1709
|
+
git(["pull", "origin", branchName], { cwd, timeout: 3e4 });
|
|
1710
|
+
logger.info(` Checked out existing remote branch: ${branchName}`);
|
|
1711
|
+
return branchName;
|
|
1712
|
+
} catch {
|
|
1140
1713
|
}
|
|
1141
|
-
}
|
|
1142
|
-
function postPRComment(prNumber, body) {
|
|
1143
1714
|
try {
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
} catch (err) {
|
|
1150
|
-
logger.warn(` Failed to post PR comment: ${err}`);
|
|
1715
|
+
git(["rev-parse", "--verify", branchName], { cwd });
|
|
1716
|
+
git(["checkout", branchName], { cwd });
|
|
1717
|
+
logger.info(` Checked out existing local branch: ${branchName}`);
|
|
1718
|
+
return branchName;
|
|
1719
|
+
} catch {
|
|
1151
1720
|
}
|
|
1152
|
-
|
|
1153
|
-
function submitPRReview(prNumber, body, event) {
|
|
1154
|
-
const flag = event === "approve" ? "--approve" : "--request-changes";
|
|
1721
|
+
const defaultBranch = getDefaultBranch(cwd);
|
|
1155
1722
|
try {
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
);
|
|
1160
|
-
logger.info(` PR review submitted on #${prNumber}: ${event}`);
|
|
1161
|
-
return true;
|
|
1162
|
-
} catch (err) {
|
|
1163
|
-
logger.warn(` Failed to submit PR review: ${err}`);
|
|
1164
|
-
return false;
|
|
1723
|
+
git(["checkout", "-b", branchName, `origin/${defaultBranch}`], { cwd });
|
|
1724
|
+
} catch {
|
|
1725
|
+
git(["checkout", "-b", branchName], { cwd });
|
|
1165
1726
|
}
|
|
1727
|
+
logger.info(` Created new branch: ${branchName}`);
|
|
1728
|
+
return branchName;
|
|
1166
1729
|
}
|
|
1167
|
-
function
|
|
1730
|
+
function syncWithDefault(cwd, branch) {
|
|
1731
|
+
const defaultBranch = branch ?? getDefaultBranch(cwd);
|
|
1732
|
+
const current = getCurrentBranch(cwd);
|
|
1733
|
+
if (current === defaultBranch) return;
|
|
1168
1734
|
try {
|
|
1169
|
-
|
|
1170
|
-
"run",
|
|
1171
|
-
"view",
|
|
1172
|
-
String(runId),
|
|
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}`;
|
|
1735
|
+
git(["fetch", "origin", defaultBranch], { cwd, timeout: 3e4 });
|
|
1179
1736
|
} catch (err) {
|
|
1180
|
-
|
|
1181
|
-
|
|
1737
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1738
|
+
logger.warn(` Failed to fetch latest from origin: ${msg}`);
|
|
1739
|
+
return;
|
|
1182
1740
|
}
|
|
1183
|
-
}
|
|
1184
|
-
function getLatestFailedRunForBranch(branch) {
|
|
1185
1741
|
try {
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
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;
|
|
1742
|
+
git(["merge", `origin/${defaultBranch}`, "--no-edit"], { cwd, timeout: 3e4 });
|
|
1743
|
+
logger.info(` Synced with origin/${defaultBranch}`);
|
|
1744
|
+
} catch {
|
|
1745
|
+
try {
|
|
1746
|
+
git(["merge", "--abort"], { cwd });
|
|
1747
|
+
} catch (abortErr) {
|
|
1748
|
+
logger.warn(` Failed to abort merge: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}`);
|
|
1749
|
+
}
|
|
1750
|
+
logger.warn(` Merge conflict with origin/${defaultBranch} \u2014 skipping sync`);
|
|
1204
1751
|
}
|
|
1205
1752
|
}
|
|
1206
|
-
function
|
|
1753
|
+
function mergeDefault(cwd) {
|
|
1754
|
+
const defaultBranch = getDefaultBranch(cwd);
|
|
1755
|
+
const current = getCurrentBranch(cwd);
|
|
1756
|
+
if (current === defaultBranch) return "clean";
|
|
1207
1757
|
try {
|
|
1208
|
-
|
|
1209
|
-
"api",
|
|
1210
|
-
`repos/{owner}/{repo}/issues/${prNumber}/comments`,
|
|
1211
|
-
"--jq",
|
|
1212
|
-
'[.[] | select(.body | test("Kody Review"))] | last | .body'
|
|
1213
|
-
]);
|
|
1214
|
-
return output.trim() || null;
|
|
1758
|
+
git(["fetch", "origin", defaultBranch], { cwd, timeout: 3e4 });
|
|
1215
1759
|
} catch (err) {
|
|
1216
|
-
|
|
1217
|
-
|
|
1760
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1761
|
+
logger.warn(` Failed to fetch latest from origin: ${msg}`);
|
|
1762
|
+
return "error";
|
|
1218
1763
|
}
|
|
1219
|
-
}
|
|
1220
|
-
function getPRFeedbackSinceLastKodyAction(prNumber) {
|
|
1221
1764
|
try {
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
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
|
-
}
|
|
1765
|
+
git(["merge", `origin/${defaultBranch}`, "--no-edit"], { cwd, timeout: 3e4 });
|
|
1766
|
+
logger.info(` Merged origin/${defaultBranch} cleanly`);
|
|
1767
|
+
return "clean";
|
|
1768
|
+
} catch {
|
|
1769
|
+
try {
|
|
1770
|
+
const unmerged = git(["diff", "--name-only", "--diff-filter=U"], { cwd });
|
|
1771
|
+
if (unmerged.trim()) return "conflict";
|
|
1772
|
+
} catch {
|
|
1253
1773
|
}
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
parts.push(`**@${c.user_login}** ${location}:
|
|
1259
|
-
${c.body}`);
|
|
1260
|
-
}
|
|
1774
|
+
try {
|
|
1775
|
+
git(["merge", "--abort"], { cwd });
|
|
1776
|
+
} catch (abortErr) {
|
|
1777
|
+
logger.warn(` Failed to abort merge: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}`);
|
|
1261
1778
|
}
|
|
1262
|
-
return
|
|
1779
|
+
return "error";
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
function getConflictedFiles(cwd) {
|
|
1783
|
+
try {
|
|
1784
|
+
const output = git(["diff", "--name-only", "--diff-filter=U"], { cwd });
|
|
1785
|
+
return output ? output.split("\n") : [];
|
|
1786
|
+
} catch {
|
|
1787
|
+
return [];
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
function commitAll(message, cwd) {
|
|
1791
|
+
const status = git(["status", "--porcelain"], { cwd });
|
|
1792
|
+
if (!status) {
|
|
1793
|
+
return { success: false, hash: "", message: "No changes to commit" };
|
|
1794
|
+
}
|
|
1795
|
+
git(["add", "."], { cwd });
|
|
1796
|
+
git(["commit", "--no-gpg-sign", "-m", message], { cwd });
|
|
1797
|
+
const hash = git(["rev-parse", "HEAD"], { cwd }).slice(0, 7);
|
|
1798
|
+
logger.info(` Committed: ${hash} ${message}`);
|
|
1799
|
+
return { success: true, hash, message };
|
|
1800
|
+
}
|
|
1801
|
+
function getDiffFiles(baseBranch, cwd) {
|
|
1802
|
+
try {
|
|
1803
|
+
const output = git(["diff", "--name-only", `origin/${baseBranch}...HEAD`], { cwd });
|
|
1804
|
+
if (!output) return [];
|
|
1805
|
+
return output.split("\n").filter((f) => f && !f.startsWith(".kody/"));
|
|
1263
1806
|
} catch (err) {
|
|
1264
|
-
|
|
1265
|
-
|
|
1807
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1808
|
+
logger.warn(` Failed to get diff files: ${msg}`);
|
|
1809
|
+
return [];
|
|
1266
1810
|
}
|
|
1267
1811
|
}
|
|
1268
|
-
function
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1812
|
+
function pushBranch(cwd) {
|
|
1813
|
+
try {
|
|
1814
|
+
git(["push", "-u", "origin", "HEAD"], { cwd, timeout: 12e4 });
|
|
1815
|
+
} catch {
|
|
1816
|
+
logger.info(" Push rejected (non-fast-forward), retrying with --force-with-lease");
|
|
1817
|
+
git(["push", "--force-with-lease", "-u", "origin", "HEAD"], { cwd, timeout: 12e4 });
|
|
1818
|
+
}
|
|
1819
|
+
logger.info(" Pushed to origin");
|
|
1276
1820
|
}
|
|
1277
|
-
var
|
|
1278
|
-
var
|
|
1279
|
-
"src/
|
|
1821
|
+
var BASE_BRANCHES, _hookSafeEnv;
|
|
1822
|
+
var init_git_utils = __esm({
|
|
1823
|
+
"src/git-utils.ts"() {
|
|
1280
1824
|
"use strict";
|
|
1281
1825
|
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
|
-
];
|
|
1826
|
+
init_config();
|
|
1827
|
+
BASE_BRANCHES = ["dev", "main", "master"];
|
|
1828
|
+
_hookSafeEnv = null;
|
|
1294
1829
|
}
|
|
1295
1830
|
});
|
|
1296
1831
|
|
|
1297
1832
|
// src/pipeline/state.ts
|
|
1298
|
-
import * as
|
|
1299
|
-
import * as
|
|
1833
|
+
import * as fs12 from "fs";
|
|
1834
|
+
import * as path10 from "path";
|
|
1300
1835
|
function loadState(taskId, taskDir) {
|
|
1301
|
-
const p =
|
|
1302
|
-
if (!
|
|
1836
|
+
const p = path10.join(taskDir, "status.json");
|
|
1837
|
+
if (!fs12.existsSync(p)) return null;
|
|
1303
1838
|
try {
|
|
1304
1839
|
const result = parseJsonSafe(
|
|
1305
|
-
|
|
1840
|
+
fs12.readFileSync(p, "utf-8"),
|
|
1306
1841
|
["taskId", "state", "stages", "createdAt", "updatedAt"]
|
|
1307
1842
|
);
|
|
1308
1843
|
if (!result.ok) {
|
|
@@ -1320,10 +1855,10 @@ function writeState(state, taskDir) {
|
|
|
1320
1855
|
...state,
|
|
1321
1856
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1322
1857
|
};
|
|
1323
|
-
const target =
|
|
1858
|
+
const target = path10.join(taskDir, "status.json");
|
|
1324
1859
|
const tmp = target + ".tmp";
|
|
1325
|
-
|
|
1326
|
-
|
|
1860
|
+
fs12.writeFileSync(tmp, JSON.stringify(updated, null, 2));
|
|
1861
|
+
fs12.renameSync(tmp, target);
|
|
1327
1862
|
return updated;
|
|
1328
1863
|
}
|
|
1329
1864
|
function initState(taskId) {
|
|
@@ -1364,16 +1899,16 @@ var init_complexity = __esm({
|
|
|
1364
1899
|
});
|
|
1365
1900
|
|
|
1366
1901
|
// src/memory.ts
|
|
1367
|
-
import * as
|
|
1368
|
-
import * as
|
|
1902
|
+
import * as fs13 from "fs";
|
|
1903
|
+
import * as path11 from "path";
|
|
1369
1904
|
function readProjectMemory(projectDir) {
|
|
1370
|
-
const memoryDir =
|
|
1371
|
-
if (!
|
|
1372
|
-
const files =
|
|
1905
|
+
const memoryDir = path11.join(projectDir, ".kody", "memory");
|
|
1906
|
+
if (!fs13.existsSync(memoryDir)) return "";
|
|
1907
|
+
const files = fs13.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
1373
1908
|
if (files.length === 0) return "";
|
|
1374
1909
|
const sections = [];
|
|
1375
1910
|
for (const file of files) {
|
|
1376
|
-
const content =
|
|
1911
|
+
const content = fs13.readFileSync(path11.join(memoryDir, file), "utf-8").trim();
|
|
1377
1912
|
if (content) {
|
|
1378
1913
|
sections.push(`## ${file.replace(".md", "")}
|
|
1379
1914
|
${content}`);
|
|
@@ -1392,8 +1927,8 @@ var init_memory = __esm({
|
|
|
1392
1927
|
});
|
|
1393
1928
|
|
|
1394
1929
|
// src/context-tiers.ts
|
|
1395
|
-
import * as
|
|
1396
|
-
import * as
|
|
1930
|
+
import * as fs14 from "fs";
|
|
1931
|
+
import * as path12 from "path";
|
|
1397
1932
|
function estimateTokens(text) {
|
|
1398
1933
|
return Math.ceil(text.length / 4);
|
|
1399
1934
|
}
|
|
@@ -1484,7 +2019,7 @@ function generateL1Json(content) {
|
|
|
1484
2019
|
}
|
|
1485
2020
|
}
|
|
1486
2021
|
function getTieredContent(filePath, content) {
|
|
1487
|
-
const key =
|
|
2022
|
+
const key = path12.basename(filePath);
|
|
1488
2023
|
return {
|
|
1489
2024
|
source: filePath,
|
|
1490
2025
|
L0: generateL0(content, key),
|
|
@@ -1496,15 +2031,15 @@ function selectTier(tiered, tier) {
|
|
|
1496
2031
|
return tiered[tier];
|
|
1497
2032
|
}
|
|
1498
2033
|
function readProjectMemoryTiered(projectDir, tier) {
|
|
1499
|
-
const memoryDir =
|
|
1500
|
-
if (!
|
|
1501
|
-
const files =
|
|
2034
|
+
const memoryDir = path12.join(projectDir, ".kody", "memory");
|
|
2035
|
+
if (!fs14.existsSync(memoryDir)) return "";
|
|
2036
|
+
const files = fs14.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
1502
2037
|
if (files.length === 0) return "";
|
|
1503
2038
|
const tierLabel2 = tier === "L2" ? "full" : tier === "L1" ? "overview" : "abstract";
|
|
1504
2039
|
const sections = [];
|
|
1505
2040
|
for (const file of files) {
|
|
1506
|
-
const filePath =
|
|
1507
|
-
const content =
|
|
2041
|
+
const filePath = path12.join(memoryDir, file);
|
|
2042
|
+
const content = fs14.readFileSync(filePath, "utf-8").trim();
|
|
1508
2043
|
if (!content) continue;
|
|
1509
2044
|
const tiered = getTieredContent(filePath, content);
|
|
1510
2045
|
const selected = selectTier(tiered, tier);
|
|
@@ -1527,9 +2062,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
|
|
|
1527
2062
|
`;
|
|
1528
2063
|
context += `Task Directory: ${taskDir}
|
|
1529
2064
|
`;
|
|
1530
|
-
const taskMdPath =
|
|
1531
|
-
if (
|
|
1532
|
-
const content =
|
|
2065
|
+
const taskMdPath = path12.join(taskDir, "task.md");
|
|
2066
|
+
if (fs14.existsSync(taskMdPath)) {
|
|
2067
|
+
const content = fs14.readFileSync(taskMdPath, "utf-8");
|
|
1533
2068
|
const selected = selectContent(taskMdPath, content, policy.taskDescription);
|
|
1534
2069
|
const label = tierLabel("Task Description", policy.taskDescription);
|
|
1535
2070
|
context += `
|
|
@@ -1537,9 +2072,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
|
|
|
1537
2072
|
${selected}
|
|
1538
2073
|
`;
|
|
1539
2074
|
}
|
|
1540
|
-
const taskJsonPath =
|
|
1541
|
-
if (
|
|
1542
|
-
const content =
|
|
2075
|
+
const taskJsonPath = path12.join(taskDir, "task.json");
|
|
2076
|
+
if (fs14.existsSync(taskJsonPath)) {
|
|
2077
|
+
const content = fs14.readFileSync(taskJsonPath, "utf-8");
|
|
1543
2078
|
if (policy.taskClassification === "L2") {
|
|
1544
2079
|
try {
|
|
1545
2080
|
const taskDef = JSON.parse(content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, ""));
|
|
@@ -1565,9 +2100,9 @@ ${selected}
|
|
|
1565
2100
|
}
|
|
1566
2101
|
}
|
|
1567
2102
|
}
|
|
1568
|
-
const specPath =
|
|
1569
|
-
if (
|
|
1570
|
-
const content =
|
|
2103
|
+
const specPath = path12.join(taskDir, "spec.md");
|
|
2104
|
+
if (fs14.existsSync(specPath)) {
|
|
2105
|
+
const content = fs14.readFileSync(specPath, "utf-8");
|
|
1571
2106
|
const selected = selectContent(specPath, content, policy.spec);
|
|
1572
2107
|
const label = tierLabel("Spec", policy.spec);
|
|
1573
2108
|
context += `
|
|
@@ -1575,9 +2110,9 @@ ${selected}
|
|
|
1575
2110
|
${selected}
|
|
1576
2111
|
`;
|
|
1577
2112
|
}
|
|
1578
|
-
const planPath =
|
|
1579
|
-
if (
|
|
1580
|
-
const content =
|
|
2113
|
+
const planPath = path12.join(taskDir, "plan.md");
|
|
2114
|
+
if (fs14.existsSync(planPath)) {
|
|
2115
|
+
const content = fs14.readFileSync(planPath, "utf-8");
|
|
1581
2116
|
const selected = selectContent(planPath, content, policy.plan);
|
|
1582
2117
|
const label = tierLabel("Plan", policy.plan);
|
|
1583
2118
|
context += `
|
|
@@ -1585,9 +2120,9 @@ ${selected}
|
|
|
1585
2120
|
${selected}
|
|
1586
2121
|
`;
|
|
1587
2122
|
}
|
|
1588
|
-
const contextMdPath =
|
|
1589
|
-
if (
|
|
1590
|
-
const content =
|
|
2123
|
+
const contextMdPath = path12.join(taskDir, "context.md");
|
|
2124
|
+
if (fs14.existsSync(contextMdPath)) {
|
|
2125
|
+
const content = fs14.readFileSync(contextMdPath, "utf-8");
|
|
1591
2126
|
const selected = selectContent(contextMdPath, content, policy.accumulatedContext);
|
|
1592
2127
|
const label = tierLabel("Previous Stage Context", policy.accumulatedContext);
|
|
1593
2128
|
context += `
|
|
@@ -1672,71 +2207,25 @@ var init_context_tiers = __esm({
|
|
|
1672
2207
|
}
|
|
1673
2208
|
});
|
|
1674
2209
|
|
|
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
2210
|
// src/context.ts
|
|
1722
|
-
import * as
|
|
1723
|
-
import * as
|
|
2211
|
+
import * as fs15 from "fs";
|
|
2212
|
+
import * as path13 from "path";
|
|
1724
2213
|
function readPromptFile(stageName, projectDir) {
|
|
1725
2214
|
if (projectDir) {
|
|
1726
|
-
const stepFile =
|
|
1727
|
-
if (
|
|
1728
|
-
return
|
|
2215
|
+
const stepFile = path13.join(projectDir, ".kody", "steps", `${stageName}.md`);
|
|
2216
|
+
if (fs15.existsSync(stepFile)) {
|
|
2217
|
+
return fs15.readFileSync(stepFile, "utf-8");
|
|
1729
2218
|
}
|
|
1730
2219
|
console.warn(` \u26A0 No step file at ${stepFile}, falling back to engine defaults. Run 'kody-engine-lite init --force' to generate step files.`);
|
|
1731
2220
|
}
|
|
1732
2221
|
const scriptDir = new URL(".", import.meta.url).pathname;
|
|
1733
2222
|
const candidates = [
|
|
1734
|
-
|
|
1735
|
-
|
|
2223
|
+
path13.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
|
|
2224
|
+
path13.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
|
|
1736
2225
|
];
|
|
1737
2226
|
for (const candidate of candidates) {
|
|
1738
|
-
if (
|
|
1739
|
-
return
|
|
2227
|
+
if (fs15.existsSync(candidate)) {
|
|
2228
|
+
return fs15.readFileSync(candidate, "utf-8");
|
|
1740
2229
|
}
|
|
1741
2230
|
}
|
|
1742
2231
|
throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
|
|
@@ -1748,18 +2237,18 @@ function injectTaskContext(prompt, taskId, taskDir, feedback) {
|
|
|
1748
2237
|
`;
|
|
1749
2238
|
context += `Task Directory: ${taskDir}
|
|
1750
2239
|
`;
|
|
1751
|
-
const taskMdPath =
|
|
1752
|
-
if (
|
|
1753
|
-
const taskMd =
|
|
2240
|
+
const taskMdPath = path13.join(taskDir, "task.md");
|
|
2241
|
+
if (fs15.existsSync(taskMdPath)) {
|
|
2242
|
+
const taskMd = fs15.readFileSync(taskMdPath, "utf-8");
|
|
1754
2243
|
context += `
|
|
1755
2244
|
## Task Description
|
|
1756
2245
|
${taskMd}
|
|
1757
2246
|
`;
|
|
1758
2247
|
}
|
|
1759
|
-
const taskJsonPath =
|
|
1760
|
-
if (
|
|
2248
|
+
const taskJsonPath = path13.join(taskDir, "task.json");
|
|
2249
|
+
if (fs15.existsSync(taskJsonPath)) {
|
|
1761
2250
|
try {
|
|
1762
|
-
const taskDef = JSON.parse(
|
|
2251
|
+
const taskDef = JSON.parse(fs15.readFileSync(taskJsonPath, "utf-8"));
|
|
1763
2252
|
context += `
|
|
1764
2253
|
## Task Classification
|
|
1765
2254
|
`;
|
|
@@ -1772,27 +2261,27 @@ ${taskMd}
|
|
|
1772
2261
|
} catch {
|
|
1773
2262
|
}
|
|
1774
2263
|
}
|
|
1775
|
-
const specPath =
|
|
1776
|
-
if (
|
|
1777
|
-
const spec =
|
|
2264
|
+
const specPath = path13.join(taskDir, "spec.md");
|
|
2265
|
+
if (fs15.existsSync(specPath)) {
|
|
2266
|
+
const spec = fs15.readFileSync(specPath, "utf-8");
|
|
1778
2267
|
const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
|
|
1779
2268
|
context += `
|
|
1780
2269
|
## Spec Summary
|
|
1781
2270
|
${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
|
|
1782
2271
|
`;
|
|
1783
2272
|
}
|
|
1784
|
-
const planPath =
|
|
1785
|
-
if (
|
|
1786
|
-
const plan =
|
|
2273
|
+
const planPath = path13.join(taskDir, "plan.md");
|
|
2274
|
+
if (fs15.existsSync(planPath)) {
|
|
2275
|
+
const plan = fs15.readFileSync(planPath, "utf-8");
|
|
1787
2276
|
const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
|
|
1788
2277
|
context += `
|
|
1789
2278
|
## Plan Summary
|
|
1790
2279
|
${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
|
|
1791
2280
|
`;
|
|
1792
2281
|
}
|
|
1793
|
-
const contextMdPath =
|
|
1794
|
-
if (
|
|
1795
|
-
const accumulated =
|
|
2282
|
+
const contextMdPath = path13.join(taskDir, "context.md");
|
|
2283
|
+
if (fs15.existsSync(contextMdPath)) {
|
|
2284
|
+
const accumulated = fs15.readFileSync(contextMdPath, "utf-8");
|
|
1796
2285
|
const truncated = accumulated.slice(-MAX_ACCUMULATED_CONTEXT);
|
|
1797
2286
|
const prefix = accumulated.length > MAX_ACCUMULATED_CONTEXT ? "...(earlier context truncated)\n" : "";
|
|
1798
2287
|
context += `
|
|
@@ -1810,17 +2299,17 @@ ${feedback}
|
|
|
1810
2299
|
}
|
|
1811
2300
|
function inferHasUIFromScope(scope) {
|
|
1812
2301
|
return scope.some((filePath) => {
|
|
1813
|
-
const ext =
|
|
2302
|
+
const ext = path13.extname(filePath).toLowerCase();
|
|
1814
2303
|
if (UI_EXTENSIONS.has(ext)) return true;
|
|
1815
2304
|
const normalized = filePath.replace(/\\/g, "/");
|
|
1816
2305
|
return UI_PATH_SEGMENTS.some((seg) => normalized.includes(seg));
|
|
1817
2306
|
});
|
|
1818
2307
|
}
|
|
1819
2308
|
function taskHasUI(taskDir) {
|
|
1820
|
-
const taskJsonPath =
|
|
1821
|
-
if (!
|
|
2309
|
+
const taskJsonPath = path13.join(taskDir, "task.json");
|
|
2310
|
+
if (!fs15.existsSync(taskJsonPath)) return true;
|
|
1822
2311
|
try {
|
|
1823
|
-
const taskDef = JSON.parse(
|
|
2312
|
+
const taskDef = JSON.parse(fs15.readFileSync(taskJsonPath, "utf-8"));
|
|
1824
2313
|
const scope = Array.isArray(taskDef.scope) ? taskDef.scope : [];
|
|
1825
2314
|
if (scope.length === 0) return true;
|
|
1826
2315
|
return inferHasUIFromScope(scope);
|
|
@@ -1942,9 +2431,9 @@ ${prompt}` : prompt;
|
|
|
1942
2431
|
}
|
|
1943
2432
|
if (isMcpEnabledForStage(stageName, config.mcp) && taskHasUI(taskDir)) {
|
|
1944
2433
|
assembled = assembled + "\n\n" + getBrowserToolGuidance(stageName, taskDir);
|
|
1945
|
-
const qaGuidePath =
|
|
1946
|
-
if (
|
|
1947
|
-
const qaGuide =
|
|
2434
|
+
const qaGuidePath = path13.join(projectDir, ".kody", "qa-guide.md");
|
|
2435
|
+
if (fs15.existsSync(qaGuidePath)) {
|
|
2436
|
+
const qaGuide = fs15.readFileSync(qaGuidePath, "utf-8").trim();
|
|
1948
2437
|
assembled = assembled + "\n\n" + qaGuide;
|
|
1949
2438
|
}
|
|
1950
2439
|
}
|
|
@@ -1977,7 +2466,7 @@ function resolveModel(modelTier, stageName) {
|
|
|
1977
2466
|
if (mapped) return mapped;
|
|
1978
2467
|
return DEFAULT_MODEL_MAP[modelTier] ?? "sonnet";
|
|
1979
2468
|
}
|
|
1980
|
-
var
|
|
2469
|
+
var MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT, UI_EXTENSIONS, UI_PATH_SEGMENTS, TIER_ESCALATION, DEFAULT_MODEL_MAP;
|
|
1981
2470
|
var init_context = __esm({
|
|
1982
2471
|
"src/context.ts"() {
|
|
1983
2472
|
"use strict";
|
|
@@ -1985,11 +2474,6 @@ var init_context = __esm({
|
|
|
1985
2474
|
init_config();
|
|
1986
2475
|
init_context_tiers();
|
|
1987
2476
|
init_mcp_config();
|
|
1988
|
-
DEFAULT_MODEL_MAP = {
|
|
1989
|
-
cheap: "haiku",
|
|
1990
|
-
mid: "sonnet",
|
|
1991
|
-
strong: "opus"
|
|
1992
|
-
};
|
|
1993
2477
|
MAX_TASK_CONTEXT_PLAN = 1500;
|
|
1994
2478
|
MAX_TASK_CONTEXT_SPEC = 2e3;
|
|
1995
2479
|
MAX_ACCUMULATED_CONTEXT = 4e3;
|
|
@@ -2016,6 +2500,11 @@ var init_context = __esm({
|
|
|
2016
2500
|
mid: "strong",
|
|
2017
2501
|
strong: "strong"
|
|
2018
2502
|
};
|
|
2503
|
+
DEFAULT_MODEL_MAP = {
|
|
2504
|
+
cheap: "haiku",
|
|
2505
|
+
mid: "sonnet",
|
|
2506
|
+
strong: "opus"
|
|
2507
|
+
};
|
|
2019
2508
|
}
|
|
2020
2509
|
});
|
|
2021
2510
|
|
|
@@ -2039,8 +2528,8 @@ var init_runner_selection = __esm({
|
|
|
2039
2528
|
});
|
|
2040
2529
|
|
|
2041
2530
|
// src/stages/agent.ts
|
|
2042
|
-
import * as
|
|
2043
|
-
import * as
|
|
2531
|
+
import * as fs16 from "fs";
|
|
2532
|
+
import * as path14 from "path";
|
|
2044
2533
|
function getSessionInfo(stageName, sessions) {
|
|
2045
2534
|
const group = SESSION_GROUP[stageName];
|
|
2046
2535
|
if (!group) return void 0;
|
|
@@ -2127,27 +2616,27 @@ async function executeAgentStage(ctx, def) {
|
|
|
2127
2616
|
}
|
|
2128
2617
|
const result = lastResult;
|
|
2129
2618
|
if (def.outputFile && result.output) {
|
|
2130
|
-
|
|
2619
|
+
fs16.writeFileSync(path14.join(ctx.taskDir, def.outputFile), result.output);
|
|
2131
2620
|
}
|
|
2132
2621
|
if (def.outputFile) {
|
|
2133
|
-
const outputPath =
|
|
2134
|
-
if (!
|
|
2135
|
-
const ext =
|
|
2136
|
-
const base =
|
|
2137
|
-
const files =
|
|
2622
|
+
const outputPath = path14.join(ctx.taskDir, def.outputFile);
|
|
2623
|
+
if (!fs16.existsSync(outputPath)) {
|
|
2624
|
+
const ext = path14.extname(def.outputFile);
|
|
2625
|
+
const base = path14.basename(def.outputFile, ext);
|
|
2626
|
+
const files = fs16.readdirSync(ctx.taskDir);
|
|
2138
2627
|
const variant = files.find(
|
|
2139
2628
|
(f) => f.startsWith(base + "-") && f.endsWith(ext)
|
|
2140
2629
|
);
|
|
2141
2630
|
if (variant) {
|
|
2142
|
-
|
|
2631
|
+
fs16.renameSync(path14.join(ctx.taskDir, variant), outputPath);
|
|
2143
2632
|
logger.info(` Renamed variant ${variant} \u2192 ${def.outputFile}`);
|
|
2144
2633
|
}
|
|
2145
2634
|
}
|
|
2146
2635
|
}
|
|
2147
2636
|
if (def.outputFile) {
|
|
2148
|
-
const outputPath =
|
|
2149
|
-
if (
|
|
2150
|
-
const content =
|
|
2637
|
+
const outputPath = path14.join(ctx.taskDir, def.outputFile);
|
|
2638
|
+
if (fs16.existsSync(outputPath)) {
|
|
2639
|
+
const content = fs16.readFileSync(outputPath, "utf-8");
|
|
2151
2640
|
const validation = validateStageOutput(def.name, content);
|
|
2152
2641
|
if (!validation.valid) {
|
|
2153
2642
|
if (def.name === "taskify") {
|
|
@@ -2161,7 +2650,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
2161
2650
|
const stripped = stripFences(retryResult.output);
|
|
2162
2651
|
const retryValidation = validateTaskJson(stripped);
|
|
2163
2652
|
if (retryValidation.valid) {
|
|
2164
|
-
|
|
2653
|
+
fs16.writeFileSync(outputPath, retryResult.output);
|
|
2165
2654
|
logger.info(` taskify retry produced valid JSON`);
|
|
2166
2655
|
} else {
|
|
2167
2656
|
logger.warn(` taskify retry still invalid: ${retryValidation.error}`);
|
|
@@ -2174,7 +2663,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
2174
2663
|
risk_level: "low",
|
|
2175
2664
|
questions: []
|
|
2176
2665
|
}, null, 2);
|
|
2177
|
-
|
|
2666
|
+
fs16.writeFileSync(outputPath, fallback);
|
|
2178
2667
|
logger.info(` taskify fallback: generated minimal task.json (risk_level=low)`);
|
|
2179
2668
|
}
|
|
2180
2669
|
}
|
|
@@ -2188,7 +2677,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
2188
2677
|
return { outcome: "completed", outputFile: def.outputFile, retries };
|
|
2189
2678
|
}
|
|
2190
2679
|
function appendStageContext(taskDir, stageName, output) {
|
|
2191
|
-
const contextPath =
|
|
2680
|
+
const contextPath = path14.join(taskDir, "context.md");
|
|
2192
2681
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19);
|
|
2193
2682
|
let summary;
|
|
2194
2683
|
if (output && output.trim()) {
|
|
@@ -2201,7 +2690,7 @@ function appendStageContext(taskDir, stageName, output) {
|
|
|
2201
2690
|
### ${stageName} (${timestamp2})
|
|
2202
2691
|
${summary}
|
|
2203
2692
|
`;
|
|
2204
|
-
|
|
2693
|
+
fs16.appendFileSync(contextPath, entry);
|
|
2205
2694
|
}
|
|
2206
2695
|
var SESSION_GROUP;
|
|
2207
2696
|
var init_agent = __esm({
|
|
@@ -2224,7 +2713,7 @@ var init_agent = __esm({
|
|
|
2224
2713
|
});
|
|
2225
2714
|
|
|
2226
2715
|
// src/verify-runner.ts
|
|
2227
|
-
import { execFileSync as
|
|
2716
|
+
import { execFileSync as execFileSync10 } from "child_process";
|
|
2228
2717
|
function isExecError(err) {
|
|
2229
2718
|
return typeof err === "object" && err !== null;
|
|
2230
2719
|
}
|
|
@@ -2260,7 +2749,7 @@ function runCommand(cmd, cwd, timeout) {
|
|
|
2260
2749
|
return { success: true, output: "", timedOut: false };
|
|
2261
2750
|
}
|
|
2262
2751
|
try {
|
|
2263
|
-
const output =
|
|
2752
|
+
const output = execFileSync10(parts[0], parts.slice(1), {
|
|
2264
2753
|
cwd,
|
|
2265
2754
|
timeout,
|
|
2266
2755
|
encoding: "utf-8",
|
|
@@ -2331,7 +2820,7 @@ var init_verify_runner = __esm({
|
|
|
2331
2820
|
});
|
|
2332
2821
|
|
|
2333
2822
|
// src/observer.ts
|
|
2334
|
-
import { execFileSync as
|
|
2823
|
+
import { execFileSync as execFileSync11 } from "child_process";
|
|
2335
2824
|
async function diagnoseFailure(stageName, errorOutput, modifiedFiles, runner, model, options) {
|
|
2336
2825
|
const context = [
|
|
2337
2826
|
`Stage: ${stageName}`,
|
|
@@ -2391,13 +2880,13 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
|
|
|
2391
2880
|
}
|
|
2392
2881
|
function getModifiedFiles(projectDir) {
|
|
2393
2882
|
try {
|
|
2394
|
-
const staged =
|
|
2883
|
+
const staged = execFileSync11("git", ["diff", "--name-only", "--cached"], {
|
|
2395
2884
|
encoding: "utf-8",
|
|
2396
2885
|
cwd: projectDir,
|
|
2397
2886
|
timeout: 5e3,
|
|
2398
2887
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2399
2888
|
}).trim();
|
|
2400
|
-
const unstaged =
|
|
2889
|
+
const unstaged = execFileSync11("git", ["diff", "--name-only"], {
|
|
2401
2890
|
encoding: "utf-8",
|
|
2402
2891
|
cwd: projectDir,
|
|
2403
2892
|
timeout: 5e3,
|
|
@@ -2440,8 +2929,8 @@ Error context:
|
|
|
2440
2929
|
});
|
|
2441
2930
|
|
|
2442
2931
|
// src/stages/gate.ts
|
|
2443
|
-
import * as
|
|
2444
|
-
import * as
|
|
2932
|
+
import * as fs17 from "fs";
|
|
2933
|
+
import * as path15 from "path";
|
|
2445
2934
|
function executeGateStage(ctx, def) {
|
|
2446
2935
|
if (ctx.input.dryRun) {
|
|
2447
2936
|
logger.info(` [dry-run] skipping ${def.name}`);
|
|
@@ -2484,7 +2973,7 @@ ${output}
|
|
|
2484
2973
|
`);
|
|
2485
2974
|
}
|
|
2486
2975
|
}
|
|
2487
|
-
|
|
2976
|
+
fs17.writeFileSync(path15.join(ctx.taskDir, "verify.md"), lines.join(""));
|
|
2488
2977
|
return {
|
|
2489
2978
|
outcome: verifyResult.pass ? "completed" : "failed",
|
|
2490
2979
|
retries: 0
|
|
@@ -2499,9 +2988,9 @@ var init_gate = __esm({
|
|
|
2499
2988
|
});
|
|
2500
2989
|
|
|
2501
2990
|
// src/stages/verify.ts
|
|
2502
|
-
import * as
|
|
2503
|
-
import * as
|
|
2504
|
-
import { execFileSync as
|
|
2991
|
+
import * as fs18 from "fs";
|
|
2992
|
+
import * as path16 from "path";
|
|
2993
|
+
import { execFileSync as execFileSync12 } from "child_process";
|
|
2505
2994
|
async function executeVerifyWithAutofix(ctx, def) {
|
|
2506
2995
|
const maxAttempts = def.maxRetries ?? 2;
|
|
2507
2996
|
for (let attempt = 0; attempt <= maxAttempts; attempt++) {
|
|
@@ -2511,8 +3000,8 @@ async function executeVerifyWithAutofix(ctx, def) {
|
|
|
2511
3000
|
return { ...gateResult, retries: attempt };
|
|
2512
3001
|
}
|
|
2513
3002
|
if (attempt < maxAttempts) {
|
|
2514
|
-
const verifyPath =
|
|
2515
|
-
const errorOutput =
|
|
3003
|
+
const verifyPath = path16.join(ctx.taskDir, "verify.md");
|
|
3004
|
+
const errorOutput = fs18.existsSync(verifyPath) ? fs18.readFileSync(verifyPath, "utf-8") : "Unknown error";
|
|
2516
3005
|
const modifiedFiles = getModifiedFiles(ctx.projectDir);
|
|
2517
3006
|
const defaultRunner = getRunnerForStage(ctx, "taskify");
|
|
2518
3007
|
const diagConfig = getProjectConfig();
|
|
@@ -2555,7 +3044,7 @@ ${diagnosis.resolution}`);
|
|
|
2555
3044
|
const parts = parseCommand(cmd);
|
|
2556
3045
|
if (parts.length === 0) return;
|
|
2557
3046
|
try {
|
|
2558
|
-
|
|
3047
|
+
execFileSync12(parts[0], parts.slice(1), {
|
|
2559
3048
|
stdio: "pipe",
|
|
2560
3049
|
timeout: FIX_COMMAND_TIMEOUT_MS
|
|
2561
3050
|
});
|
|
@@ -2607,73 +3096,9 @@ var init_verify = __esm({
|
|
|
2607
3096
|
}
|
|
2608
3097
|
});
|
|
2609
3098
|
|
|
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];
|
|
2653
|
-
}
|
|
2654
|
-
}
|
|
2655
|
-
return latestTaskId;
|
|
2656
|
-
} catch {
|
|
2657
|
-
return null;
|
|
2658
|
-
}
|
|
2659
|
-
}
|
|
2660
|
-
function resolveTaskIdForCommand(issueNumber, projectDir) {
|
|
2661
|
-
const fromTasks = findLatestTaskForIssue(issueNumber, projectDir);
|
|
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"() {
|
|
2669
|
-
"use strict";
|
|
2670
|
-
init_github_api();
|
|
2671
|
-
}
|
|
2672
|
-
});
|
|
2673
|
-
|
|
2674
3099
|
// src/review-standalone.ts
|
|
2675
|
-
import * as
|
|
2676
|
-
import * as
|
|
3100
|
+
import * as fs19 from "fs";
|
|
3101
|
+
import * as path17 from "path";
|
|
2677
3102
|
function resolveReviewTarget(input) {
|
|
2678
3103
|
if (input.prs.length === 0) {
|
|
2679
3104
|
return {
|
|
@@ -2697,8 +3122,8 @@ Or comment on the specific PR: \`@kody review\``
|
|
|
2697
3122
|
}
|
|
2698
3123
|
async function runStandaloneReview(input) {
|
|
2699
3124
|
const taskId = input.taskId ?? `review-${generateTaskId()}`;
|
|
2700
|
-
const taskDir =
|
|
2701
|
-
|
|
3125
|
+
const taskDir = path17.join(input.projectDir, ".kody", "tasks", taskId);
|
|
3126
|
+
fs19.mkdirSync(taskDir, { recursive: true });
|
|
2702
3127
|
let diffInstruction = "";
|
|
2703
3128
|
let filesChangedSection = "";
|
|
2704
3129
|
if (input.baseBranch) {
|
|
@@ -2725,7 +3150,7 @@ ${fileList}`;
|
|
|
2725
3150
|
const taskContent = `# ${input.prTitle}
|
|
2726
3151
|
|
|
2727
3152
|
${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
|
|
2728
|
-
|
|
3153
|
+
fs19.writeFileSync(path17.join(taskDir, "task.md"), taskContent);
|
|
2729
3154
|
const reviewDef = STAGES.find((s) => s.name === "review");
|
|
2730
3155
|
const ctx = {
|
|
2731
3156
|
taskId,
|
|
@@ -2747,10 +3172,10 @@ ${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
|
|
|
2747
3172
|
error: result.error ?? "Review stage failed"
|
|
2748
3173
|
};
|
|
2749
3174
|
}
|
|
2750
|
-
const reviewPath =
|
|
3175
|
+
const reviewPath = path17.join(taskDir, "review.md");
|
|
2751
3176
|
let reviewContent;
|
|
2752
|
-
if (
|
|
2753
|
-
reviewContent =
|
|
3177
|
+
if (fs19.existsSync(reviewPath)) {
|
|
3178
|
+
reviewContent = fs19.readFileSync(reviewPath, "utf-8");
|
|
2754
3179
|
}
|
|
2755
3180
|
return {
|
|
2756
3181
|
outcome: "completed",
|
|
@@ -2790,8 +3215,8 @@ var init_review_standalone = __esm({
|
|
|
2790
3215
|
});
|
|
2791
3216
|
|
|
2792
3217
|
// src/stages/review.ts
|
|
2793
|
-
import * as
|
|
2794
|
-
import * as
|
|
3218
|
+
import * as fs20 from "fs";
|
|
3219
|
+
import * as path18 from "path";
|
|
2795
3220
|
async function executeReviewWithFix(ctx, def) {
|
|
2796
3221
|
if (ctx.input.dryRun) {
|
|
2797
3222
|
return { outcome: "completed", retries: 0 };
|
|
@@ -2805,11 +3230,11 @@ async function executeReviewWithFix(ctx, def) {
|
|
|
2805
3230
|
if (reviewResult.outcome !== "completed") {
|
|
2806
3231
|
return reviewResult;
|
|
2807
3232
|
}
|
|
2808
|
-
const reviewFile =
|
|
2809
|
-
if (!
|
|
3233
|
+
const reviewFile = path18.join(ctx.taskDir, "review.md");
|
|
3234
|
+
if (!fs20.existsSync(reviewFile)) {
|
|
2810
3235
|
return { outcome: "failed", retries: iteration, error: "review.md not found" };
|
|
2811
3236
|
}
|
|
2812
|
-
const content =
|
|
3237
|
+
const content = fs20.readFileSync(reviewFile, "utf-8");
|
|
2813
3238
|
if (detectReviewVerdict(content) !== "fail") {
|
|
2814
3239
|
return { ...reviewResult, retries: iteration };
|
|
2815
3240
|
}
|
|
@@ -2838,15 +3263,15 @@ var init_review = __esm({
|
|
|
2838
3263
|
});
|
|
2839
3264
|
|
|
2840
3265
|
// src/stages/ship.ts
|
|
2841
|
-
import * as
|
|
2842
|
-
import * as
|
|
3266
|
+
import * as fs21 from "fs";
|
|
3267
|
+
import * as path19 from "path";
|
|
2843
3268
|
import { execFileSync as execFileSync13 } from "child_process";
|
|
2844
3269
|
function buildPrBody(ctx) {
|
|
2845
3270
|
const sections = [];
|
|
2846
|
-
const taskJsonPath =
|
|
2847
|
-
if (
|
|
3271
|
+
const taskJsonPath = path19.join(ctx.taskDir, "task.json");
|
|
3272
|
+
if (fs21.existsSync(taskJsonPath)) {
|
|
2848
3273
|
try {
|
|
2849
|
-
const raw =
|
|
3274
|
+
const raw = fs21.readFileSync(taskJsonPath, "utf-8");
|
|
2850
3275
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2851
3276
|
const task = JSON.parse(cleaned);
|
|
2852
3277
|
if (task.description) {
|
|
@@ -2865,9 +3290,9 @@ ${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
|
|
|
2865
3290
|
} catch {
|
|
2866
3291
|
}
|
|
2867
3292
|
}
|
|
2868
|
-
const reviewPath =
|
|
2869
|
-
if (
|
|
2870
|
-
const review =
|
|
3293
|
+
const reviewPath = path19.join(ctx.taskDir, "review.md");
|
|
3294
|
+
if (fs21.existsSync(reviewPath)) {
|
|
3295
|
+
const review = fs21.readFileSync(reviewPath, "utf-8");
|
|
2871
3296
|
const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
2872
3297
|
if (summaryMatch) {
|
|
2873
3298
|
const summary = summaryMatch[1].trim();
|
|
@@ -2884,14 +3309,14 @@ ${summary}`);
|
|
|
2884
3309
|
**Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
|
|
2885
3310
|
}
|
|
2886
3311
|
}
|
|
2887
|
-
const verifyPath =
|
|
2888
|
-
if (
|
|
2889
|
-
const verify =
|
|
3312
|
+
const verifyPath = path19.join(ctx.taskDir, "verify.md");
|
|
3313
|
+
if (fs21.existsSync(verifyPath)) {
|
|
3314
|
+
const verify = fs21.readFileSync(verifyPath, "utf-8");
|
|
2890
3315
|
if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
|
|
2891
3316
|
}
|
|
2892
|
-
const planPath =
|
|
2893
|
-
if (
|
|
2894
|
-
const plan =
|
|
3317
|
+
const planPath = path19.join(ctx.taskDir, "plan.md");
|
|
3318
|
+
if (fs21.existsSync(planPath)) {
|
|
3319
|
+
const plan = fs21.readFileSync(planPath, "utf-8").trim();
|
|
2895
3320
|
if (plan) {
|
|
2896
3321
|
const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
|
|
2897
3322
|
sections.push(`
|
|
@@ -2911,13 +3336,13 @@ Closes #${ctx.input.issueNumber}`);
|
|
|
2911
3336
|
return sections.join("\n");
|
|
2912
3337
|
}
|
|
2913
3338
|
function executeShipStage(ctx, _def) {
|
|
2914
|
-
const shipPath =
|
|
3339
|
+
const shipPath = path19.join(ctx.taskDir, "ship.md");
|
|
2915
3340
|
if (ctx.input.dryRun) {
|
|
2916
|
-
|
|
3341
|
+
fs21.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
|
|
2917
3342
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
2918
3343
|
}
|
|
2919
3344
|
if (ctx.input.local && !ctx.input.issueNumber) {
|
|
2920
|
-
|
|
3345
|
+
fs21.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
|
|
2921
3346
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
2922
3347
|
}
|
|
2923
3348
|
try {
|
|
@@ -2964,28 +3389,28 @@ function executeShipStage(ctx, _def) {
|
|
|
2964
3389
|
chore: "chore"
|
|
2965
3390
|
};
|
|
2966
3391
|
let prefix = "chore";
|
|
2967
|
-
const taskJsonPath =
|
|
2968
|
-
if (
|
|
3392
|
+
const taskJsonPath = path19.join(ctx.taskDir, "task.json");
|
|
3393
|
+
if (fs21.existsSync(taskJsonPath)) {
|
|
2969
3394
|
try {
|
|
2970
|
-
const raw =
|
|
3395
|
+
const raw = fs21.readFileSync(taskJsonPath, "utf-8");
|
|
2971
3396
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2972
3397
|
const task = JSON.parse(cleaned);
|
|
2973
3398
|
prefix = TYPE_PREFIX[task.task_type] ?? "chore";
|
|
2974
3399
|
} catch {
|
|
2975
3400
|
}
|
|
2976
3401
|
}
|
|
2977
|
-
const taskMdPath =
|
|
2978
|
-
if (
|
|
2979
|
-
const content =
|
|
3402
|
+
const taskMdPath = path19.join(ctx.taskDir, "task.md");
|
|
3403
|
+
if (fs21.existsSync(taskMdPath)) {
|
|
3404
|
+
const content = fs21.readFileSync(taskMdPath, "utf-8");
|
|
2980
3405
|
const heading = content.split("\n").find((l) => l.startsWith("# "));
|
|
2981
3406
|
if (heading) {
|
|
2982
3407
|
title = `${prefix}: ${heading.replace(/^#\s*/, "").trim()}`.slice(0, 72);
|
|
2983
3408
|
}
|
|
2984
3409
|
}
|
|
2985
3410
|
if (title === "Update") {
|
|
2986
|
-
if (
|
|
3411
|
+
if (fs21.existsSync(taskJsonPath)) {
|
|
2987
3412
|
try {
|
|
2988
|
-
const raw =
|
|
3413
|
+
const raw = fs21.readFileSync(taskJsonPath, "utf-8");
|
|
2989
3414
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2990
3415
|
const task = JSON.parse(cleaned);
|
|
2991
3416
|
if (task.title) title = `${prefix}: ${task.title}`.slice(0, 72);
|
|
@@ -3008,7 +3433,7 @@ function executeShipStage(ctx, _def) {
|
|
|
3008
3433
|
} catch {
|
|
3009
3434
|
}
|
|
3010
3435
|
}
|
|
3011
|
-
|
|
3436
|
+
fs21.writeFileSync(shipPath, `# Ship
|
|
3012
3437
|
|
|
3013
3438
|
Updated existing PR: ${existingPr.url}
|
|
3014
3439
|
PR #${existingPr.number}
|
|
@@ -3029,20 +3454,20 @@ PR #${existingPr.number}
|
|
|
3029
3454
|
} catch {
|
|
3030
3455
|
}
|
|
3031
3456
|
}
|
|
3032
|
-
|
|
3457
|
+
fs21.writeFileSync(shipPath, `# Ship
|
|
3033
3458
|
|
|
3034
3459
|
PR created: ${pr.url}
|
|
3035
3460
|
PR #${pr.number}
|
|
3036
3461
|
`);
|
|
3037
3462
|
} else {
|
|
3038
|
-
|
|
3463
|
+
fs21.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
|
|
3039
3464
|
}
|
|
3040
3465
|
}
|
|
3041
3466
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
3042
3467
|
} catch (err) {
|
|
3043
3468
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3044
3469
|
try {
|
|
3045
|
-
|
|
3470
|
+
fs21.writeFileSync(shipPath, `# Ship
|
|
3046
3471
|
|
|
3047
3472
|
Failed: ${msg}
|
|
3048
3473
|
`);
|
|
@@ -3091,15 +3516,15 @@ var init_executor_registry = __esm({
|
|
|
3091
3516
|
});
|
|
3092
3517
|
|
|
3093
3518
|
// src/pipeline/questions.ts
|
|
3094
|
-
import * as
|
|
3095
|
-
import * as
|
|
3519
|
+
import * as fs22 from "fs";
|
|
3520
|
+
import * as path20 from "path";
|
|
3096
3521
|
function checkForQuestions(ctx, stageName) {
|
|
3097
3522
|
if (ctx.input.local || !ctx.input.issueNumber) return false;
|
|
3098
3523
|
try {
|
|
3099
3524
|
if (stageName === "taskify") {
|
|
3100
|
-
const taskJsonPath =
|
|
3101
|
-
if (!
|
|
3102
|
-
const raw =
|
|
3525
|
+
const taskJsonPath = path20.join(ctx.taskDir, "task.json");
|
|
3526
|
+
if (!fs22.existsSync(taskJsonPath)) return false;
|
|
3527
|
+
const raw = fs22.readFileSync(taskJsonPath, "utf-8");
|
|
3103
3528
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3104
3529
|
const taskJson = JSON.parse(cleaned);
|
|
3105
3530
|
if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
|
|
@@ -3114,9 +3539,9 @@ Reply with \`@kody approve\` and your answers in the comment body.`;
|
|
|
3114
3539
|
}
|
|
3115
3540
|
}
|
|
3116
3541
|
if (stageName === "plan") {
|
|
3117
|
-
const planPath =
|
|
3118
|
-
if (!
|
|
3119
|
-
const plan =
|
|
3542
|
+
const planPath = path20.join(ctx.taskDir, "plan.md");
|
|
3543
|
+
if (!fs22.existsSync(planPath)) return false;
|
|
3544
|
+
const plan = fs22.readFileSync(planPath, "utf-8");
|
|
3120
3545
|
const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
3121
3546
|
if (questionsMatch) {
|
|
3122
3547
|
const questionsText = questionsMatch[1].trim();
|
|
@@ -3145,8 +3570,8 @@ var init_questions = __esm({
|
|
|
3145
3570
|
});
|
|
3146
3571
|
|
|
3147
3572
|
// src/pipeline/hooks.ts
|
|
3148
|
-
import * as
|
|
3149
|
-
import * as
|
|
3573
|
+
import * as fs23 from "fs";
|
|
3574
|
+
import * as path21 from "path";
|
|
3150
3575
|
function applyPreStageLabel(ctx, def) {
|
|
3151
3576
|
if (!ctx.input.issueNumber || ctx.input.local) return;
|
|
3152
3577
|
if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
|
|
@@ -3184,9 +3609,9 @@ function autoDetectComplexity(ctx, def) {
|
|
|
3184
3609
|
return { complexity, activeStages };
|
|
3185
3610
|
}
|
|
3186
3611
|
try {
|
|
3187
|
-
const taskJsonPath =
|
|
3188
|
-
if (!
|
|
3189
|
-
const raw =
|
|
3612
|
+
const taskJsonPath = path21.join(ctx.taskDir, "task.json");
|
|
3613
|
+
if (!fs23.existsSync(taskJsonPath)) return null;
|
|
3614
|
+
const raw = fs23.readFileSync(taskJsonPath, "utf-8");
|
|
3190
3615
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3191
3616
|
const taskJson = JSON.parse(cleaned);
|
|
3192
3617
|
if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
|
|
@@ -3216,8 +3641,8 @@ function checkRiskGate(ctx, def, state, complexity) {
|
|
|
3216
3641
|
if (ctx.input.dryRun || ctx.input.local) return null;
|
|
3217
3642
|
if (ctx.input.mode === "rerun") return null;
|
|
3218
3643
|
if (!ctx.input.issueNumber) return null;
|
|
3219
|
-
const planPath =
|
|
3220
|
-
const plan =
|
|
3644
|
+
const planPath = path21.join(ctx.taskDir, "plan.md");
|
|
3645
|
+
const plan = fs23.existsSync(planPath) ? fs23.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
|
|
3221
3646
|
try {
|
|
3222
3647
|
postComment(
|
|
3223
3648
|
ctx.input.issueNumber,
|
|
@@ -3284,22 +3709,22 @@ var init_hooks = __esm({
|
|
|
3284
3709
|
});
|
|
3285
3710
|
|
|
3286
3711
|
// src/learning/auto-learn.ts
|
|
3287
|
-
import * as
|
|
3288
|
-
import * as
|
|
3712
|
+
import * as fs24 from "fs";
|
|
3713
|
+
import * as path22 from "path";
|
|
3289
3714
|
function stripAnsi(str) {
|
|
3290
3715
|
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
3291
3716
|
}
|
|
3292
3717
|
function autoLearn(ctx) {
|
|
3293
3718
|
try {
|
|
3294
|
-
const memoryDir =
|
|
3295
|
-
if (!
|
|
3296
|
-
|
|
3719
|
+
const memoryDir = path22.join(ctx.projectDir, ".kody", "memory");
|
|
3720
|
+
if (!fs24.existsSync(memoryDir)) {
|
|
3721
|
+
fs24.mkdirSync(memoryDir, { recursive: true });
|
|
3297
3722
|
}
|
|
3298
3723
|
const learnings = [];
|
|
3299
3724
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3300
|
-
const verifyPath =
|
|
3301
|
-
if (
|
|
3302
|
-
const verify = stripAnsi(
|
|
3725
|
+
const verifyPath = path22.join(ctx.taskDir, "verify.md");
|
|
3726
|
+
if (fs24.existsSync(verifyPath)) {
|
|
3727
|
+
const verify = stripAnsi(fs24.readFileSync(verifyPath, "utf-8"));
|
|
3303
3728
|
if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
|
|
3304
3729
|
if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
|
|
3305
3730
|
if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
|
|
@@ -3308,18 +3733,18 @@ function autoLearn(ctx) {
|
|
|
3308
3733
|
if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
|
|
3309
3734
|
if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
|
|
3310
3735
|
}
|
|
3311
|
-
const reviewPath =
|
|
3312
|
-
if (
|
|
3313
|
-
const review =
|
|
3736
|
+
const reviewPath = path22.join(ctx.taskDir, "review.md");
|
|
3737
|
+
if (fs24.existsSync(reviewPath)) {
|
|
3738
|
+
const review = fs24.readFileSync(reviewPath, "utf-8");
|
|
3314
3739
|
if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
|
|
3315
3740
|
if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
|
|
3316
3741
|
if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
|
|
3317
3742
|
if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
|
|
3318
3743
|
}
|
|
3319
|
-
const taskJsonPath =
|
|
3320
|
-
if (
|
|
3744
|
+
const taskJsonPath = path22.join(ctx.taskDir, "task.json");
|
|
3745
|
+
if (fs24.existsSync(taskJsonPath)) {
|
|
3321
3746
|
try {
|
|
3322
|
-
const raw = stripAnsi(
|
|
3747
|
+
const raw = stripAnsi(fs24.readFileSync(taskJsonPath, "utf-8"));
|
|
3323
3748
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3324
3749
|
const task = JSON.parse(cleaned);
|
|
3325
3750
|
if (task.scope && Array.isArray(task.scope)) {
|
|
@@ -3330,12 +3755,12 @@ function autoLearn(ctx) {
|
|
|
3330
3755
|
}
|
|
3331
3756
|
}
|
|
3332
3757
|
if (learnings.length > 0) {
|
|
3333
|
-
const conventionsPath =
|
|
3758
|
+
const conventionsPath = path22.join(memoryDir, "conventions.md");
|
|
3334
3759
|
const entry = `
|
|
3335
3760
|
## Learned ${timestamp2} (task: ${ctx.taskId})
|
|
3336
3761
|
${learnings.join("\n")}
|
|
3337
3762
|
`;
|
|
3338
|
-
|
|
3763
|
+
fs24.appendFileSync(conventionsPath, entry);
|
|
3339
3764
|
logger.info(`Auto-learned ${learnings.length} convention(s)`);
|
|
3340
3765
|
}
|
|
3341
3766
|
autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
|
|
@@ -3343,8 +3768,8 @@ ${learnings.join("\n")}
|
|
|
3343
3768
|
}
|
|
3344
3769
|
}
|
|
3345
3770
|
function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
3346
|
-
const archPath =
|
|
3347
|
-
if (
|
|
3771
|
+
const archPath = path22.join(memoryDir, "architecture.md");
|
|
3772
|
+
if (fs24.existsSync(archPath)) return;
|
|
3348
3773
|
const detected = detectArchitectureBasic(projectDir);
|
|
3349
3774
|
if (detected.length > 0) {
|
|
3350
3775
|
const content = `# Architecture (auto-detected ${timestamp2})
|
|
@@ -3352,7 +3777,7 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
|
3352
3777
|
## Overview
|
|
3353
3778
|
${detected.join("\n")}
|
|
3354
3779
|
`;
|
|
3355
|
-
|
|
3780
|
+
fs24.writeFileSync(archPath, content);
|
|
3356
3781
|
logger.info(`Auto-detected architecture (${detected.length} items)`);
|
|
3357
3782
|
}
|
|
3358
3783
|
}
|
|
@@ -3365,13 +3790,13 @@ var init_auto_learn = __esm({
|
|
|
3365
3790
|
});
|
|
3366
3791
|
|
|
3367
3792
|
// src/retrospective.ts
|
|
3368
|
-
import * as
|
|
3369
|
-
import * as
|
|
3793
|
+
import * as fs25 from "fs";
|
|
3794
|
+
import * as path23 from "path";
|
|
3370
3795
|
function readArtifact(taskDir, filename, maxChars) {
|
|
3371
|
-
const p =
|
|
3372
|
-
if (!
|
|
3796
|
+
const p = path23.join(taskDir, filename);
|
|
3797
|
+
if (!fs25.existsSync(p)) return null;
|
|
3373
3798
|
try {
|
|
3374
|
-
const content =
|
|
3799
|
+
const content = fs25.readFileSync(p, "utf-8");
|
|
3375
3800
|
return content.length > maxChars ? content.slice(0, maxChars) + "\n...(truncated)" : content;
|
|
3376
3801
|
} catch {
|
|
3377
3802
|
return null;
|
|
@@ -3424,13 +3849,13 @@ function collectRunContext(ctx, state, pipelineStartTime) {
|
|
|
3424
3849
|
return lines.join("\n");
|
|
3425
3850
|
}
|
|
3426
3851
|
function getLogPath(projectDir) {
|
|
3427
|
-
return
|
|
3852
|
+
return path23.join(projectDir, ".kody", "memory", "observer-log.jsonl");
|
|
3428
3853
|
}
|
|
3429
3854
|
function readPreviousRetrospectives(projectDir, limit = 10) {
|
|
3430
3855
|
const logPath = getLogPath(projectDir);
|
|
3431
|
-
if (!
|
|
3856
|
+
if (!fs25.existsSync(logPath)) return [];
|
|
3432
3857
|
try {
|
|
3433
|
-
const content =
|
|
3858
|
+
const content = fs25.readFileSync(logPath, "utf-8");
|
|
3434
3859
|
const lines = content.split("\n").filter(Boolean);
|
|
3435
3860
|
const entries = [];
|
|
3436
3861
|
const start = Math.max(0, lines.length - limit);
|
|
@@ -3457,11 +3882,11 @@ function formatPreviousEntries(entries) {
|
|
|
3457
3882
|
}
|
|
3458
3883
|
function appendRetrospectiveEntry(projectDir, entry) {
|
|
3459
3884
|
const logPath = getLogPath(projectDir);
|
|
3460
|
-
const dir =
|
|
3461
|
-
if (!
|
|
3462
|
-
|
|
3885
|
+
const dir = path23.dirname(logPath);
|
|
3886
|
+
if (!fs25.existsSync(dir)) {
|
|
3887
|
+
fs25.mkdirSync(dir, { recursive: true });
|
|
3463
3888
|
}
|
|
3464
|
-
|
|
3889
|
+
fs25.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
3465
3890
|
}
|
|
3466
3891
|
async function runRetrospective(ctx, state, pipelineStartTime) {
|
|
3467
3892
|
if (ctx.input.dryRun) return;
|
|
@@ -3629,8 +4054,8 @@ var init_summary = __esm({
|
|
|
3629
4054
|
});
|
|
3630
4055
|
|
|
3631
4056
|
// src/pipeline.ts
|
|
3632
|
-
import * as
|
|
3633
|
-
import * as
|
|
4057
|
+
import * as fs26 from "fs";
|
|
4058
|
+
import * as path24 from "path";
|
|
3634
4059
|
function ensureFeatureBranchIfNeeded(ctx) {
|
|
3635
4060
|
if (ctx.input.dryRun) return;
|
|
3636
4061
|
if (ctx.input.prNumber) {
|
|
@@ -3643,8 +4068,8 @@ function ensureFeatureBranchIfNeeded(ctx) {
|
|
|
3643
4068
|
}
|
|
3644
4069
|
if (!ctx.input.issueNumber) return;
|
|
3645
4070
|
try {
|
|
3646
|
-
const taskMdPath =
|
|
3647
|
-
const title =
|
|
4071
|
+
const taskMdPath = path24.join(ctx.taskDir, "task.md");
|
|
4072
|
+
const title = fs26.existsSync(taskMdPath) ? fs26.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
|
|
3648
4073
|
ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
|
|
3649
4074
|
syncWithDefault(ctx.projectDir);
|
|
3650
4075
|
} catch (err) {
|
|
@@ -3658,10 +4083,10 @@ function ensureFeatureBranchIfNeeded(ctx) {
|
|
|
3658
4083
|
}
|
|
3659
4084
|
}
|
|
3660
4085
|
function acquireLock(taskDir) {
|
|
3661
|
-
const lockPath =
|
|
3662
|
-
if (
|
|
4086
|
+
const lockPath = path24.join(taskDir, ".lock");
|
|
4087
|
+
if (fs26.existsSync(lockPath)) {
|
|
3663
4088
|
try {
|
|
3664
|
-
const pid = parseInt(
|
|
4089
|
+
const pid = parseInt(fs26.readFileSync(lockPath, "utf-8").trim(), 10);
|
|
3665
4090
|
if (!isNaN(pid)) {
|
|
3666
4091
|
try {
|
|
3667
4092
|
process.kill(pid, 0);
|
|
@@ -3678,14 +4103,14 @@ function acquireLock(taskDir) {
|
|
|
3678
4103
|
logger.warn(` Corrupt lock file \u2014 overwriting`);
|
|
3679
4104
|
}
|
|
3680
4105
|
try {
|
|
3681
|
-
|
|
4106
|
+
fs26.unlinkSync(lockPath);
|
|
3682
4107
|
} catch {
|
|
3683
4108
|
}
|
|
3684
4109
|
}
|
|
3685
4110
|
try {
|
|
3686
|
-
const fd =
|
|
3687
|
-
|
|
3688
|
-
|
|
4111
|
+
const fd = fs26.openSync(lockPath, fs26.constants.O_WRONLY | fs26.constants.O_CREAT | fs26.constants.O_EXCL);
|
|
4112
|
+
fs26.writeSync(fd, String(process.pid));
|
|
4113
|
+
fs26.closeSync(fd);
|
|
3689
4114
|
} catch (err) {
|
|
3690
4115
|
if (err.code === "EEXIST") {
|
|
3691
4116
|
throw new Error("Pipeline already running (lock acquired by another process)");
|
|
@@ -3695,7 +4120,7 @@ function acquireLock(taskDir) {
|
|
|
3695
4120
|
}
|
|
3696
4121
|
function releaseLock(taskDir) {
|
|
3697
4122
|
try {
|
|
3698
|
-
|
|
4123
|
+
fs26.unlinkSync(path24.join(taskDir, ".lock"));
|
|
3699
4124
|
} catch {
|
|
3700
4125
|
}
|
|
3701
4126
|
}
|
|
@@ -3904,7 +4329,7 @@ var init_pipeline = __esm({
|
|
|
3904
4329
|
|
|
3905
4330
|
// src/preflight.ts
|
|
3906
4331
|
import { execFileSync as execFileSync14 } from "child_process";
|
|
3907
|
-
import * as
|
|
4332
|
+
import * as fs27 from "fs";
|
|
3908
4333
|
function check(name, fn) {
|
|
3909
4334
|
try {
|
|
3910
4335
|
const detail = fn() ?? void 0;
|
|
@@ -3957,7 +4382,7 @@ function runPreflight() {
|
|
|
3957
4382
|
return v;
|
|
3958
4383
|
}),
|
|
3959
4384
|
check("package.json", () => {
|
|
3960
|
-
if (!
|
|
4385
|
+
if (!fs27.existsSync("package.json")) throw new Error("not found");
|
|
3961
4386
|
})
|
|
3962
4387
|
];
|
|
3963
4388
|
const failed = checks.filter((c) => !c.ok);
|
|
@@ -3978,19 +4403,19 @@ var init_preflight = __esm({
|
|
|
3978
4403
|
});
|
|
3979
4404
|
|
|
3980
4405
|
// src/cli/args.ts
|
|
3981
|
-
function
|
|
4406
|
+
function getArg2(args2, flag) {
|
|
3982
4407
|
const idx = args2.indexOf(flag);
|
|
3983
4408
|
if (idx !== -1 && args2[idx + 1] && !args2[idx + 1].startsWith("--")) {
|
|
3984
4409
|
return args2[idx + 1];
|
|
3985
4410
|
}
|
|
3986
4411
|
return void 0;
|
|
3987
4412
|
}
|
|
3988
|
-
function
|
|
4413
|
+
function hasFlag2(args2, flag) {
|
|
3989
4414
|
return args2.includes(flag);
|
|
3990
4415
|
}
|
|
3991
4416
|
function parseArgs() {
|
|
3992
4417
|
const args2 = process.argv.slice(2);
|
|
3993
|
-
if (
|
|
4418
|
+
if (hasFlag2(args2, "--help") || hasFlag2(args2, "-h") || args2.length === 0) {
|
|
3994
4419
|
console.log(`Usage:
|
|
3995
4420
|
kody run --task-id <id> [--task "<desc>"] [--cwd <path>] [--issue-number <n>] [--complexity low|medium|high] [--feedback "<text>"] [--local] [--dry-run]
|
|
3996
4421
|
kody rerun --task-id <id> --from <stage> [--cwd <path>] [--issue-number <n>]
|
|
@@ -4007,22 +4432,22 @@ function parseArgs() {
|
|
|
4007
4432
|
console.error(`Unknown command: ${command2}`);
|
|
4008
4433
|
process.exit(1);
|
|
4009
4434
|
}
|
|
4010
|
-
const issueStr =
|
|
4011
|
-
const prStr =
|
|
4012
|
-
const localFlag =
|
|
4435
|
+
const issueStr = getArg2(args2, "--issue-number") ?? process.env.ISSUE_NUMBER;
|
|
4436
|
+
const prStr = getArg2(args2, "--pr-number") ?? process.env.PR_NUMBER;
|
|
4437
|
+
const localFlag = hasFlag2(args2, "--local");
|
|
4013
4438
|
return {
|
|
4014
4439
|
command: command2,
|
|
4015
|
-
taskId:
|
|
4016
|
-
task:
|
|
4017
|
-
fromStage:
|
|
4018
|
-
dryRun:
|
|
4019
|
-
cwd:
|
|
4440
|
+
taskId: getArg2(args2, "--task-id") ?? process.env.TASK_ID,
|
|
4441
|
+
task: getArg2(args2, "--task"),
|
|
4442
|
+
fromStage: getArg2(args2, "--from") ?? process.env.FROM_STAGE,
|
|
4443
|
+
dryRun: hasFlag2(args2, "--dry-run") || process.env.DRY_RUN === "true",
|
|
4444
|
+
cwd: getArg2(args2, "--cwd"),
|
|
4020
4445
|
issueNumber: issueStr ? parseInt(issueStr, 10) : void 0,
|
|
4021
4446
|
prNumber: prStr ? parseInt(prStr, 10) : void 0,
|
|
4022
|
-
feedback:
|
|
4023
|
-
local: localFlag || !isCI2 && !
|
|
4024
|
-
complexity:
|
|
4025
|
-
ciRunId:
|
|
4447
|
+
feedback: getArg2(args2, "--feedback") ?? process.env.FEEDBACK,
|
|
4448
|
+
local: localFlag || !isCI2 && !hasFlag2(args2, "--no-local"),
|
|
4449
|
+
complexity: getArg2(args2, "--complexity") ?? process.env.COMPLEXITY,
|
|
4450
|
+
ciRunId: getArg2(args2, "--ci-run-id") ?? process.env.CI_RUN_ID
|
|
4026
4451
|
};
|
|
4027
4452
|
}
|
|
4028
4453
|
var isCI2;
|
|
@@ -4034,9 +4459,9 @@ var init_args = __esm({
|
|
|
4034
4459
|
});
|
|
4035
4460
|
|
|
4036
4461
|
// src/cli/litellm.ts
|
|
4037
|
-
import * as
|
|
4462
|
+
import * as fs28 from "fs";
|
|
4038
4463
|
import * as os from "os";
|
|
4039
|
-
import * as
|
|
4464
|
+
import * as path25 from "path";
|
|
4040
4465
|
import { execFileSync as execFileSync15 } from "child_process";
|
|
4041
4466
|
async function checkLitellmHealth(url) {
|
|
4042
4467
|
try {
|
|
@@ -4046,7 +4471,7 @@ async function checkLitellmHealth(url) {
|
|
|
4046
4471
|
return false;
|
|
4047
4472
|
}
|
|
4048
4473
|
}
|
|
4049
|
-
async function checkModelHealth(baseUrl, apiKey, model
|
|
4474
|
+
async function checkModelHealth(baseUrl, apiKey, model) {
|
|
4050
4475
|
try {
|
|
4051
4476
|
const res = await fetch(`${baseUrl}/v1/messages`, {
|
|
4052
4477
|
method: "POST",
|
|
@@ -4124,8 +4549,8 @@ async function tryStartLitellm(url, projectDir, generatedConfig) {
|
|
|
4124
4549
|
logger.warn("No provider configured in kody.config.json \u2014 cannot start LiteLLM proxy");
|
|
4125
4550
|
return null;
|
|
4126
4551
|
}
|
|
4127
|
-
const configPath =
|
|
4128
|
-
|
|
4552
|
+
const configPath = path25.join(os.tmpdir(), "kody-litellm-config.yaml");
|
|
4553
|
+
fs28.writeFileSync(configPath, generatedConfig);
|
|
4129
4554
|
const portMatch = url.match(/:(\d+)/);
|
|
4130
4555
|
const port = portMatch ? portMatch[1] : "4000";
|
|
4131
4556
|
let litellmFound = false;
|
|
@@ -4154,10 +4579,10 @@ async function tryStartLitellm(url, projectDir, generatedConfig) {
|
|
|
4154
4579
|
cmd = "python3";
|
|
4155
4580
|
args2 = ["-m", "litellm", "--config", configPath, "--port", port];
|
|
4156
4581
|
}
|
|
4157
|
-
const dotenvPath =
|
|
4582
|
+
const dotenvPath = path25.join(projectDir, ".env");
|
|
4158
4583
|
const dotenvVars = {};
|
|
4159
|
-
if (
|
|
4160
|
-
for (const rawLine of
|
|
4584
|
+
if (fs28.existsSync(dotenvPath)) {
|
|
4585
|
+
for (const rawLine of fs28.readFileSync(dotenvPath, "utf-8").split("\n")) {
|
|
4161
4586
|
const line = rawLine.trim();
|
|
4162
4587
|
if (!line || line.startsWith("#")) continue;
|
|
4163
4588
|
const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
|
|
@@ -4208,8 +4633,8 @@ var init_litellm = __esm({
|
|
|
4208
4633
|
});
|
|
4209
4634
|
|
|
4210
4635
|
// src/cli/task-state.ts
|
|
4211
|
-
import * as
|
|
4212
|
-
import * as
|
|
4636
|
+
import * as fs29 from "fs";
|
|
4637
|
+
import * as path26 from "path";
|
|
4213
4638
|
function resolveTaskAction(issueNumber, existingTaskId, existingState) {
|
|
4214
4639
|
if (!existingTaskId || !existingState) {
|
|
4215
4640
|
return { action: "start-fresh", taskId: `${issueNumber}-${generateTaskId()}` };
|
|
@@ -4241,11 +4666,11 @@ function resolveTaskAction(issueNumber, existingTaskId, existingState) {
|
|
|
4241
4666
|
function resolveForIssue(issueNumber, projectDir) {
|
|
4242
4667
|
const existingTaskId = findLatestTaskForIssue(issueNumber, projectDir);
|
|
4243
4668
|
if (existingTaskId) {
|
|
4244
|
-
const statusPath =
|
|
4669
|
+
const statusPath = path26.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
|
|
4245
4670
|
let existingState = null;
|
|
4246
|
-
if (
|
|
4671
|
+
if (fs29.existsSync(statusPath)) {
|
|
4247
4672
|
try {
|
|
4248
|
-
existingState = JSON.parse(
|
|
4673
|
+
existingState = JSON.parse(fs29.readFileSync(statusPath, "utf-8"));
|
|
4249
4674
|
} catch {
|
|
4250
4675
|
}
|
|
4251
4676
|
}
|
|
@@ -4402,8 +4827,8 @@ var init_resolve = __esm({
|
|
|
4402
4827
|
|
|
4403
4828
|
// src/entry.ts
|
|
4404
4829
|
var entry_exports = {};
|
|
4405
|
-
import * as
|
|
4406
|
-
import * as
|
|
4830
|
+
import * as fs30 from "fs";
|
|
4831
|
+
import * as path27 from "path";
|
|
4407
4832
|
async function ensureLitellmProxy(config, projectDir) {
|
|
4408
4833
|
if (!anyStageNeedsProxy(config)) return null;
|
|
4409
4834
|
const litellmUrl = getLitellmUrl();
|
|
@@ -4458,9 +4883,9 @@ async function runModelHealthCheck(config) {
|
|
|
4458
4883
|
}
|
|
4459
4884
|
async function main() {
|
|
4460
4885
|
const input = parseArgs();
|
|
4461
|
-
const projectDir = input.cwd ?
|
|
4886
|
+
const projectDir = input.cwd ? path27.resolve(input.cwd) : process.cwd();
|
|
4462
4887
|
if (input.cwd) {
|
|
4463
|
-
if (!
|
|
4888
|
+
if (!fs30.existsSync(projectDir)) {
|
|
4464
4889
|
console.error(`--cwd path does not exist: ${projectDir}`);
|
|
4465
4890
|
process.exit(1);
|
|
4466
4891
|
}
|
|
@@ -4526,8 +4951,24 @@ async function main() {
|
|
|
4526
4951
|
process.exit(1);
|
|
4527
4952
|
}
|
|
4528
4953
|
}
|
|
4529
|
-
const taskDir =
|
|
4530
|
-
|
|
4954
|
+
const taskDir = path27.join(projectDir, ".kody", "tasks", taskId);
|
|
4955
|
+
fs30.mkdirSync(taskDir, { recursive: true });
|
|
4956
|
+
if (input.command === "rerun" && isTaskifyRun(taskDir)) {
|
|
4957
|
+
const marker = readTaskifyMarker(taskDir);
|
|
4958
|
+
if (marker) {
|
|
4959
|
+
logger.info(`Resuming taskify run for ${marker.ticketId ?? marker.prdFile} with PM feedback`);
|
|
4960
|
+
await taskifyCommand({
|
|
4961
|
+
ticketId: marker.ticketId,
|
|
4962
|
+
prdFile: marker.prdFile,
|
|
4963
|
+
issueNumber: marker.issueNumber ?? input.issueNumber,
|
|
4964
|
+
feedback: input.feedback,
|
|
4965
|
+
local: input.local,
|
|
4966
|
+
projectDir,
|
|
4967
|
+
taskId
|
|
4968
|
+
});
|
|
4969
|
+
return;
|
|
4970
|
+
}
|
|
4971
|
+
}
|
|
4531
4972
|
if (input.command === "status") {
|
|
4532
4973
|
printStatus(taskId, taskDir);
|
|
4533
4974
|
return;
|
|
@@ -4643,31 +5084,31 @@ async function main() {
|
|
|
4643
5084
|
logger.info("Preflight checks:");
|
|
4644
5085
|
runPreflight();
|
|
4645
5086
|
if (input.task) {
|
|
4646
|
-
|
|
5087
|
+
fs30.writeFileSync(path27.join(taskDir, "task.md"), input.task);
|
|
4647
5088
|
}
|
|
4648
|
-
const taskMdPath =
|
|
4649
|
-
if (!
|
|
5089
|
+
const taskMdPath = path27.join(taskDir, "task.md");
|
|
5090
|
+
if (!fs30.existsSync(taskMdPath) && isPRFix && input.prNumber) {
|
|
4650
5091
|
logger.info(`Fetching PR #${input.prNumber} details as task context...`);
|
|
4651
5092
|
const prDetails = getPRDetails(input.prNumber);
|
|
4652
5093
|
if (prDetails) {
|
|
4653
5094
|
const taskContent = `# ${prDetails.title}
|
|
4654
5095
|
|
|
4655
5096
|
${prDetails.body ?? ""}`;
|
|
4656
|
-
|
|
5097
|
+
fs30.writeFileSync(taskMdPath, taskContent);
|
|
4657
5098
|
logger.info(` Task loaded from PR #${input.prNumber}: ${prDetails.title}`);
|
|
4658
5099
|
}
|
|
4659
|
-
} else if (!
|
|
5100
|
+
} else if (!fs30.existsSync(taskMdPath) && input.issueNumber) {
|
|
4660
5101
|
logger.info(`Fetching issue #${input.issueNumber} body as task...`);
|
|
4661
5102
|
const issue = getIssue(input.issueNumber);
|
|
4662
5103
|
if (issue) {
|
|
4663
5104
|
const taskContent = `# ${issue.title}
|
|
4664
5105
|
|
|
4665
5106
|
${issue.body ?? ""}`;
|
|
4666
|
-
|
|
5107
|
+
fs30.writeFileSync(taskMdPath, taskContent);
|
|
4667
5108
|
logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
|
|
4668
5109
|
}
|
|
4669
5110
|
}
|
|
4670
|
-
if (!
|
|
5111
|
+
if (!fs30.existsSync(taskMdPath)) {
|
|
4671
5112
|
console.error("No task.md found. Provide --task, --issue-number, or ensure .kody/tasks/<id>/task.md exists.");
|
|
4672
5113
|
process.exit(1);
|
|
4673
5114
|
}
|
|
@@ -4805,7 +5246,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
|
|
|
4805
5246
|
}
|
|
4806
5247
|
}
|
|
4807
5248
|
const state = await runPipeline(ctx);
|
|
4808
|
-
const files =
|
|
5249
|
+
const files = fs30.readdirSync(taskDir);
|
|
4809
5250
|
console.log(`
|
|
4810
5251
|
Artifacts in ${taskDir}:`);
|
|
4811
5252
|
for (const f of files) {
|
|
@@ -4851,6 +5292,7 @@ var init_entry = __esm({
|
|
|
4851
5292
|
init_litellm();
|
|
4852
5293
|
init_task_resolution();
|
|
4853
5294
|
init_task_state();
|
|
5295
|
+
init_taskify_command();
|
|
4854
5296
|
init_config();
|
|
4855
5297
|
main().catch(async (err) => {
|
|
4856
5298
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -4869,9 +5311,9 @@ var init_entry = __esm({
|
|
|
4869
5311
|
});
|
|
4870
5312
|
|
|
4871
5313
|
// src/bin/cli.ts
|
|
4872
|
-
import * as
|
|
4873
|
-
import * as
|
|
4874
|
-
import { fileURLToPath } from "url";
|
|
5314
|
+
import * as fs31 from "fs";
|
|
5315
|
+
import * as path28 from "path";
|
|
5316
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4875
5317
|
|
|
4876
5318
|
// src/bin/commands/init.ts
|
|
4877
5319
|
import * as fs3 from "fs";
|
|
@@ -5705,7 +6147,7 @@ ${repoContext}`;
|
|
|
5705
6147
|
const output = execFileSync5("claude", [
|
|
5706
6148
|
"--print",
|
|
5707
6149
|
"--model",
|
|
5708
|
-
"haiku",
|
|
6150
|
+
"claude-haiku-4-5-20251001",
|
|
5709
6151
|
"--dangerously-skip-permissions",
|
|
5710
6152
|
memoryPrompt
|
|
5711
6153
|
], {
|
|
@@ -5808,7 +6250,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
5808
6250
|
const output = execFileSync5("claude", [
|
|
5809
6251
|
"--print",
|
|
5810
6252
|
"--model",
|
|
5811
|
-
"haiku",
|
|
6253
|
+
"claude-haiku-4-5-20251001",
|
|
5812
6254
|
"--dangerously-skip-permissions",
|
|
5813
6255
|
customizationPrompt
|
|
5814
6256
|
], {
|
|
@@ -6041,11 +6483,11 @@ Create it manually.`, cwd);
|
|
|
6041
6483
|
|
|
6042
6484
|
// src/bin/cli.ts
|
|
6043
6485
|
init_architecture_detection();
|
|
6044
|
-
var
|
|
6045
|
-
var PKG_ROOT =
|
|
6486
|
+
var __dirname2 = path28.dirname(fileURLToPath2(import.meta.url));
|
|
6487
|
+
var PKG_ROOT = path28.resolve(__dirname2, "..", "..");
|
|
6046
6488
|
function getVersion() {
|
|
6047
|
-
const pkgPath =
|
|
6048
|
-
const pkg = JSON.parse(
|
|
6489
|
+
const pkgPath = path28.join(PKG_ROOT, "package.json");
|
|
6490
|
+
const pkg = JSON.parse(fs31.readFileSync(pkgPath, "utf-8"));
|
|
6049
6491
|
return pkg.version;
|
|
6050
6492
|
}
|
|
6051
6493
|
var args = process.argv.slice(2);
|
|
@@ -6054,6 +6496,8 @@ if (command === "init") {
|
|
|
6054
6496
|
initCommand({ force: args.includes("--force") }, PKG_ROOT);
|
|
6055
6497
|
} else if (command === "bootstrap") {
|
|
6056
6498
|
bootstrapCommand({ force: args.includes("--force") }, PKG_ROOT);
|
|
6499
|
+
} else if (command === "taskify") {
|
|
6500
|
+
Promise.resolve().then(() => (init_taskify_command(), taskify_command_exports)).then(({ runTaskifyCommand: runTaskifyCommand2 }) => runTaskifyCommand2());
|
|
6057
6501
|
} else if (command === "ci-parse") {
|
|
6058
6502
|
Promise.resolve().then(() => (init_parse_inputs(), parse_inputs_exports)).then(({ runCiParse: runCiParse2 }) => runCiParse2());
|
|
6059
6503
|
} else if (command === "version" || command === "--version" || command === "-v") {
|