@kody-ade/kody-engine-lite 0.1.0
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 +1800 -0
- package/package.json +35 -0
- package/prompts/autofix.md +21 -0
- package/prompts/build.md +26 -0
- package/prompts/plan.md +30 -0
- package/prompts/review-fix.md +23 -0
- package/prompts/review.md +46 -0
- package/prompts/taskify.md +33 -0
- package/templates/kody.yml +213 -0
package/dist/bin/cli.js
ADDED
|
@@ -0,0 +1,1800 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/agent-runner.ts
|
|
8
|
+
import { spawn, execFileSync } from "child_process";
|
|
9
|
+
function writeStdin(child, prompt) {
|
|
10
|
+
return new Promise((resolve4, reject) => {
|
|
11
|
+
if (!child.stdin) {
|
|
12
|
+
resolve4();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
child.stdin.write(prompt, (err) => {
|
|
16
|
+
if (err) reject(err);
|
|
17
|
+
else {
|
|
18
|
+
child.stdin.end();
|
|
19
|
+
resolve4();
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function waitForProcess(child, timeout) {
|
|
25
|
+
return new Promise((resolve4) => {
|
|
26
|
+
const stdoutChunks = [];
|
|
27
|
+
const stderrChunks = [];
|
|
28
|
+
child.stdout?.on("data", (chunk) => stdoutChunks.push(chunk));
|
|
29
|
+
child.stderr?.on("data", (chunk) => stderrChunks.push(chunk));
|
|
30
|
+
const timer = setTimeout(() => {
|
|
31
|
+
child.kill("SIGTERM");
|
|
32
|
+
setTimeout(() => {
|
|
33
|
+
if (!child.killed) child.kill("SIGKILL");
|
|
34
|
+
}, SIGKILL_GRACE_MS);
|
|
35
|
+
}, timeout);
|
|
36
|
+
child.on("exit", (code) => {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
resolve4({
|
|
39
|
+
code,
|
|
40
|
+
stdout: Buffer.concat(stdoutChunks).toString(),
|
|
41
|
+
stderr: Buffer.concat(stderrChunks).toString()
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
child.on("error", (err) => {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
resolve4({ code: -1, stdout: "", stderr: err.message });
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function createClaudeCodeRunner() {
|
|
51
|
+
return {
|
|
52
|
+
async run(stageName, prompt, model, timeout, _taskDir, options) {
|
|
53
|
+
const child = spawn(
|
|
54
|
+
"claude",
|
|
55
|
+
[
|
|
56
|
+
"--print",
|
|
57
|
+
"--model",
|
|
58
|
+
model,
|
|
59
|
+
"--dangerously-skip-permissions",
|
|
60
|
+
"--allowedTools",
|
|
61
|
+
"Bash,Edit,Read,Write,Glob,Grep"
|
|
62
|
+
],
|
|
63
|
+
{
|
|
64
|
+
cwd: options?.cwd ?? process.cwd(),
|
|
65
|
+
env: {
|
|
66
|
+
...process.env,
|
|
67
|
+
SKIP_BUILD: "1",
|
|
68
|
+
SKIP_HOOKS: "1",
|
|
69
|
+
...options?.env
|
|
70
|
+
},
|
|
71
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
try {
|
|
75
|
+
await writeStdin(child, prompt);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
return {
|
|
78
|
+
outcome: "failed",
|
|
79
|
+
error: `Failed to send prompt: ${err instanceof Error ? err.message : String(err)}`
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const { code, stdout, stderr } = await waitForProcess(child, timeout);
|
|
83
|
+
if (code === 0) {
|
|
84
|
+
return { outcome: "completed", output: stdout };
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
outcome: code === null ? "timed_out" : "failed",
|
|
88
|
+
error: `Exit code ${code}
|
|
89
|
+
${stderr.slice(-STDERR_TAIL_CHARS)}`
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
async healthCheck() {
|
|
93
|
+
try {
|
|
94
|
+
execFileSync("claude", ["--version"], { timeout: 1e4, stdio: "pipe" });
|
|
95
|
+
return true;
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
var SIGKILL_GRACE_MS, STDERR_TAIL_CHARS;
|
|
103
|
+
var init_agent_runner = __esm({
|
|
104
|
+
"src/agent-runner.ts"() {
|
|
105
|
+
"use strict";
|
|
106
|
+
SIGKILL_GRACE_MS = 5e3;
|
|
107
|
+
STDERR_TAIL_CHARS = 500;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// src/definitions.ts
|
|
112
|
+
var STAGES;
|
|
113
|
+
var init_definitions = __esm({
|
|
114
|
+
"src/definitions.ts"() {
|
|
115
|
+
"use strict";
|
|
116
|
+
STAGES = [
|
|
117
|
+
{
|
|
118
|
+
name: "taskify",
|
|
119
|
+
type: "agent",
|
|
120
|
+
modelTier: "cheap",
|
|
121
|
+
timeout: 18e4,
|
|
122
|
+
maxRetries: 1,
|
|
123
|
+
outputFile: "task.json"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: "plan",
|
|
127
|
+
type: "agent",
|
|
128
|
+
modelTier: "mid",
|
|
129
|
+
timeout: 3e5,
|
|
130
|
+
maxRetries: 1,
|
|
131
|
+
outputFile: "plan.md"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: "build",
|
|
135
|
+
type: "agent",
|
|
136
|
+
modelTier: "strong",
|
|
137
|
+
timeout: 12e5,
|
|
138
|
+
maxRetries: 1
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: "verify",
|
|
142
|
+
type: "gate",
|
|
143
|
+
modelTier: "cheap",
|
|
144
|
+
timeout: 3e5,
|
|
145
|
+
maxRetries: 2,
|
|
146
|
+
retryWithAgent: "autofix"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: "review",
|
|
150
|
+
type: "agent",
|
|
151
|
+
modelTier: "mid",
|
|
152
|
+
timeout: 3e5,
|
|
153
|
+
maxRetries: 1,
|
|
154
|
+
outputFile: "review.md"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: "review-fix",
|
|
158
|
+
type: "agent",
|
|
159
|
+
modelTier: "strong",
|
|
160
|
+
timeout: 6e5,
|
|
161
|
+
maxRetries: 1
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: "ship",
|
|
165
|
+
type: "deterministic",
|
|
166
|
+
modelTier: "cheap",
|
|
167
|
+
timeout: 12e4,
|
|
168
|
+
maxRetries: 1,
|
|
169
|
+
outputFile: "ship.md"
|
|
170
|
+
}
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// src/memory.ts
|
|
176
|
+
import * as fs from "fs";
|
|
177
|
+
import * as path from "path";
|
|
178
|
+
function readProjectMemory(projectDir) {
|
|
179
|
+
const memoryDir = path.join(projectDir, ".kody", "memory");
|
|
180
|
+
if (!fs.existsSync(memoryDir)) return "";
|
|
181
|
+
const files = fs.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
182
|
+
if (files.length === 0) return "";
|
|
183
|
+
const sections = [];
|
|
184
|
+
for (const file of files) {
|
|
185
|
+
const content = fs.readFileSync(path.join(memoryDir, file), "utf-8").trim();
|
|
186
|
+
if (content) {
|
|
187
|
+
sections.push(`## ${file.replace(".md", "")}
|
|
188
|
+
${content}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (sections.length === 0) return "";
|
|
192
|
+
return `# Project Memory
|
|
193
|
+
|
|
194
|
+
${sections.join("\n\n")}
|
|
195
|
+
`;
|
|
196
|
+
}
|
|
197
|
+
var init_memory = __esm({
|
|
198
|
+
"src/memory.ts"() {
|
|
199
|
+
"use strict";
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// src/config.ts
|
|
204
|
+
import * as fs2 from "fs";
|
|
205
|
+
import * as path2 from "path";
|
|
206
|
+
function setConfigDir(dir) {
|
|
207
|
+
_configDir = dir;
|
|
208
|
+
_config = null;
|
|
209
|
+
}
|
|
210
|
+
function getProjectConfig() {
|
|
211
|
+
if (_config) return _config;
|
|
212
|
+
const configPath = path2.join(_configDir ?? process.cwd(), "kody.config.json");
|
|
213
|
+
if (fs2.existsSync(configPath)) {
|
|
214
|
+
try {
|
|
215
|
+
const raw = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
|
|
216
|
+
_config = {
|
|
217
|
+
quality: { ...DEFAULT_CONFIG.quality, ...raw.quality },
|
|
218
|
+
git: { ...DEFAULT_CONFIG.git, ...raw.git },
|
|
219
|
+
github: { ...DEFAULT_CONFIG.github, ...raw.github },
|
|
220
|
+
paths: { ...DEFAULT_CONFIG.paths, ...raw.paths },
|
|
221
|
+
agent: { ...DEFAULT_CONFIG.agent, ...raw.agent }
|
|
222
|
+
};
|
|
223
|
+
} catch {
|
|
224
|
+
_config = { ...DEFAULT_CONFIG };
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
_config = { ...DEFAULT_CONFIG };
|
|
228
|
+
}
|
|
229
|
+
return _config;
|
|
230
|
+
}
|
|
231
|
+
var DEFAULT_CONFIG, VERIFY_COMMAND_TIMEOUT_MS, FIX_COMMAND_TIMEOUT_MS, _config, _configDir;
|
|
232
|
+
var init_config = __esm({
|
|
233
|
+
"src/config.ts"() {
|
|
234
|
+
"use strict";
|
|
235
|
+
DEFAULT_CONFIG = {
|
|
236
|
+
quality: {
|
|
237
|
+
typecheck: "pnpm -s tsc --noEmit",
|
|
238
|
+
lint: "pnpm -s lint",
|
|
239
|
+
lintFix: "pnpm lint:fix",
|
|
240
|
+
format: "pnpm -s format:check",
|
|
241
|
+
formatFix: "pnpm format:fix",
|
|
242
|
+
testUnit: "pnpm -s test"
|
|
243
|
+
},
|
|
244
|
+
git: {
|
|
245
|
+
defaultBranch: "dev"
|
|
246
|
+
},
|
|
247
|
+
github: {
|
|
248
|
+
owner: "",
|
|
249
|
+
repo: ""
|
|
250
|
+
},
|
|
251
|
+
paths: {
|
|
252
|
+
taskDir: ".tasks"
|
|
253
|
+
},
|
|
254
|
+
agent: {
|
|
255
|
+
runner: "claude-code",
|
|
256
|
+
modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
VERIFY_COMMAND_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
260
|
+
FIX_COMMAND_TIMEOUT_MS = 2 * 60 * 1e3;
|
|
261
|
+
_config = null;
|
|
262
|
+
_configDir = null;
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// src/context.ts
|
|
267
|
+
import * as fs3 from "fs";
|
|
268
|
+
import * as path3 from "path";
|
|
269
|
+
function readPromptFile(stageName) {
|
|
270
|
+
const promptPath = path3.resolve(
|
|
271
|
+
new URL(".", import.meta.url).pathname,
|
|
272
|
+
"..",
|
|
273
|
+
"prompts",
|
|
274
|
+
`${stageName}.md`
|
|
275
|
+
);
|
|
276
|
+
if (!fs3.existsSync(promptPath)) {
|
|
277
|
+
throw new Error(`Prompt file not found: ${promptPath}`);
|
|
278
|
+
}
|
|
279
|
+
return fs3.readFileSync(promptPath, "utf-8");
|
|
280
|
+
}
|
|
281
|
+
function injectTaskContext(prompt, taskId, taskDir) {
|
|
282
|
+
let context = `## Task Context
|
|
283
|
+
`;
|
|
284
|
+
context += `Task ID: ${taskId}
|
|
285
|
+
`;
|
|
286
|
+
context += `Task Directory: ${taskDir}
|
|
287
|
+
`;
|
|
288
|
+
const taskMdPath = path3.join(taskDir, "task.md");
|
|
289
|
+
if (fs3.existsSync(taskMdPath)) {
|
|
290
|
+
const taskMd = fs3.readFileSync(taskMdPath, "utf-8");
|
|
291
|
+
context += `
|
|
292
|
+
## Task Description
|
|
293
|
+
${taskMd}
|
|
294
|
+
`;
|
|
295
|
+
}
|
|
296
|
+
const taskJsonPath = path3.join(taskDir, "task.json");
|
|
297
|
+
if (fs3.existsSync(taskJsonPath)) {
|
|
298
|
+
try {
|
|
299
|
+
const taskDef = JSON.parse(fs3.readFileSync(taskJsonPath, "utf-8"));
|
|
300
|
+
context += `
|
|
301
|
+
## Task Classification
|
|
302
|
+
`;
|
|
303
|
+
context += `Type: ${taskDef.task_type ?? "unknown"}
|
|
304
|
+
`;
|
|
305
|
+
context += `Title: ${taskDef.title ?? "unknown"}
|
|
306
|
+
`;
|
|
307
|
+
context += `Risk: ${taskDef.risk_level ?? "unknown"}
|
|
308
|
+
`;
|
|
309
|
+
} catch {
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const specPath = path3.join(taskDir, "spec.md");
|
|
313
|
+
if (fs3.existsSync(specPath)) {
|
|
314
|
+
const spec = fs3.readFileSync(specPath, "utf-8");
|
|
315
|
+
const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
|
|
316
|
+
context += `
|
|
317
|
+
## Spec Summary
|
|
318
|
+
${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
|
|
319
|
+
`;
|
|
320
|
+
}
|
|
321
|
+
const planPath = path3.join(taskDir, "plan.md");
|
|
322
|
+
if (fs3.existsSync(planPath)) {
|
|
323
|
+
const plan = fs3.readFileSync(planPath, "utf-8");
|
|
324
|
+
const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
|
|
325
|
+
context += `
|
|
326
|
+
## Plan Summary
|
|
327
|
+
${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
|
|
328
|
+
`;
|
|
329
|
+
}
|
|
330
|
+
return prompt.replace("{{TASK_CONTEXT}}", context);
|
|
331
|
+
}
|
|
332
|
+
function buildFullPrompt(stageName, taskId, taskDir, projectDir) {
|
|
333
|
+
const memory = readProjectMemory(projectDir);
|
|
334
|
+
const promptTemplate = readPromptFile(stageName);
|
|
335
|
+
const prompt = injectTaskContext(promptTemplate, taskId, taskDir);
|
|
336
|
+
return memory ? `${memory}
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
${prompt}` : prompt;
|
|
340
|
+
}
|
|
341
|
+
function resolveModel(modelTier, stageName) {
|
|
342
|
+
const config = getProjectConfig();
|
|
343
|
+
if (config.agent.usePerStageRouting && stageName) {
|
|
344
|
+
return stageName;
|
|
345
|
+
}
|
|
346
|
+
const mapped = config.agent.modelMap[modelTier];
|
|
347
|
+
if (mapped) return mapped;
|
|
348
|
+
return DEFAULT_MODEL_MAP[modelTier] ?? "sonnet";
|
|
349
|
+
}
|
|
350
|
+
var DEFAULT_MODEL_MAP, MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC;
|
|
351
|
+
var init_context = __esm({
|
|
352
|
+
"src/context.ts"() {
|
|
353
|
+
"use strict";
|
|
354
|
+
init_memory();
|
|
355
|
+
init_config();
|
|
356
|
+
DEFAULT_MODEL_MAP = {
|
|
357
|
+
cheap: "haiku",
|
|
358
|
+
mid: "sonnet",
|
|
359
|
+
strong: "opus"
|
|
360
|
+
};
|
|
361
|
+
MAX_TASK_CONTEXT_PLAN = 1500;
|
|
362
|
+
MAX_TASK_CONTEXT_SPEC = 2e3;
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// src/validators.ts
|
|
367
|
+
function validateTaskJson(content) {
|
|
368
|
+
try {
|
|
369
|
+
const parsed = JSON.parse(content);
|
|
370
|
+
for (const field of REQUIRED_TASK_FIELDS) {
|
|
371
|
+
if (!(field in parsed)) {
|
|
372
|
+
return { valid: false, error: `Missing field: ${field}` };
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return { valid: true };
|
|
376
|
+
} catch (err) {
|
|
377
|
+
return {
|
|
378
|
+
valid: false,
|
|
379
|
+
error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function validatePlanMd(content) {
|
|
384
|
+
if (content.length < 10) {
|
|
385
|
+
return { valid: false, error: "Plan is too short (< 10 chars)" };
|
|
386
|
+
}
|
|
387
|
+
if (!/^##\s+\w+/m.test(content)) {
|
|
388
|
+
return { valid: false, error: "Plan has no markdown h2 sections" };
|
|
389
|
+
}
|
|
390
|
+
return { valid: true };
|
|
391
|
+
}
|
|
392
|
+
function validateReviewMd(content) {
|
|
393
|
+
if (/pass/i.test(content) || /fail/i.test(content)) {
|
|
394
|
+
return { valid: true };
|
|
395
|
+
}
|
|
396
|
+
return { valid: false, error: "Review must contain 'pass' or 'fail'" };
|
|
397
|
+
}
|
|
398
|
+
var REQUIRED_TASK_FIELDS;
|
|
399
|
+
var init_validators = __esm({
|
|
400
|
+
"src/validators.ts"() {
|
|
401
|
+
"use strict";
|
|
402
|
+
REQUIRED_TASK_FIELDS = [
|
|
403
|
+
"task_type",
|
|
404
|
+
"title",
|
|
405
|
+
"description",
|
|
406
|
+
"scope",
|
|
407
|
+
"risk_level"
|
|
408
|
+
];
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// src/logger.ts
|
|
413
|
+
function getLevel() {
|
|
414
|
+
const env = process.env.LOG_LEVEL;
|
|
415
|
+
return LEVELS[env ?? "info"] ?? LEVELS.info;
|
|
416
|
+
}
|
|
417
|
+
function timestamp() {
|
|
418
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
|
|
419
|
+
}
|
|
420
|
+
function log(level, msg) {
|
|
421
|
+
if (LEVELS[level] < getLevel()) return;
|
|
422
|
+
const prefix = `[${timestamp()}] ${level.toUpperCase().padEnd(5)}`;
|
|
423
|
+
if (level === "error") {
|
|
424
|
+
console.error(`${prefix} ${msg}`);
|
|
425
|
+
} else if (level === "warn") {
|
|
426
|
+
console.warn(`${prefix} ${msg}`);
|
|
427
|
+
} else {
|
|
428
|
+
console.log(`${prefix} ${msg}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function ciGroup(title) {
|
|
432
|
+
if (isCI) process.stdout.write(`::group::${title}
|
|
433
|
+
`);
|
|
434
|
+
}
|
|
435
|
+
function ciGroupEnd() {
|
|
436
|
+
if (isCI) process.stdout.write(`::endgroup::
|
|
437
|
+
`);
|
|
438
|
+
}
|
|
439
|
+
var isCI, LEVELS, logger;
|
|
440
|
+
var init_logger = __esm({
|
|
441
|
+
"src/logger.ts"() {
|
|
442
|
+
"use strict";
|
|
443
|
+
isCI = !!process.env.GITHUB_ACTIONS;
|
|
444
|
+
LEVELS = {
|
|
445
|
+
debug: 0,
|
|
446
|
+
info: 1,
|
|
447
|
+
warn: 2,
|
|
448
|
+
error: 3
|
|
449
|
+
};
|
|
450
|
+
logger = {
|
|
451
|
+
debug: (msg) => log("debug", msg),
|
|
452
|
+
info: (msg) => log("info", msg),
|
|
453
|
+
warn: (msg) => log("warn", msg),
|
|
454
|
+
error: (msg) => log("error", msg)
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// src/git-utils.ts
|
|
460
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
461
|
+
function getHookSafeEnv() {
|
|
462
|
+
if (!_hookSafeEnv) {
|
|
463
|
+
_hookSafeEnv = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
|
|
464
|
+
}
|
|
465
|
+
return _hookSafeEnv;
|
|
466
|
+
}
|
|
467
|
+
function git(args2, options) {
|
|
468
|
+
return execFileSync2("git", args2, {
|
|
469
|
+
encoding: "utf-8",
|
|
470
|
+
timeout: options?.timeout ?? 3e4,
|
|
471
|
+
cwd: options?.cwd,
|
|
472
|
+
env: options?.env ?? getHookSafeEnv(),
|
|
473
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
474
|
+
}).trim();
|
|
475
|
+
}
|
|
476
|
+
function deriveBranchName(issueNumber, title) {
|
|
477
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").slice(0, 50).replace(/-$/, "");
|
|
478
|
+
return `${issueNumber}-${slug}`;
|
|
479
|
+
}
|
|
480
|
+
function getDefaultBranch(cwd) {
|
|
481
|
+
try {
|
|
482
|
+
const ref = git(["symbolic-ref", "refs/remotes/origin/HEAD"], { cwd });
|
|
483
|
+
return ref.replace("refs/remotes/origin/", "");
|
|
484
|
+
} catch {
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
const output = git(["remote", "show", "origin"], { cwd, timeout: 1e4 });
|
|
488
|
+
const match = output.match(/HEAD branch:\s*(\S+)/);
|
|
489
|
+
if (match) return match[1];
|
|
490
|
+
} catch {
|
|
491
|
+
}
|
|
492
|
+
return "dev";
|
|
493
|
+
}
|
|
494
|
+
function getCurrentBranch(cwd) {
|
|
495
|
+
return git(["branch", "--show-current"], { cwd });
|
|
496
|
+
}
|
|
497
|
+
function ensureFeatureBranch(issueNumber, title, cwd) {
|
|
498
|
+
const current = getCurrentBranch(cwd);
|
|
499
|
+
if (!BASE_BRANCHES.includes(current) && current !== "") {
|
|
500
|
+
logger.info(` Already on feature branch: ${current}`);
|
|
501
|
+
return current;
|
|
502
|
+
}
|
|
503
|
+
const branchName = deriveBranchName(issueNumber, title);
|
|
504
|
+
try {
|
|
505
|
+
git(["fetch", "origin"], { cwd, timeout: 3e4 });
|
|
506
|
+
} catch {
|
|
507
|
+
logger.warn(" Failed to fetch origin");
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
git(["rev-parse", "--verify", `origin/${branchName}`], { cwd });
|
|
511
|
+
git(["checkout", branchName], { cwd });
|
|
512
|
+
git(["pull", "origin", branchName], { cwd, timeout: 3e4 });
|
|
513
|
+
logger.info(` Checked out existing remote branch: ${branchName}`);
|
|
514
|
+
return branchName;
|
|
515
|
+
} catch {
|
|
516
|
+
}
|
|
517
|
+
try {
|
|
518
|
+
git(["rev-parse", "--verify", branchName], { cwd });
|
|
519
|
+
git(["checkout", branchName], { cwd });
|
|
520
|
+
logger.info(` Checked out existing local branch: ${branchName}`);
|
|
521
|
+
return branchName;
|
|
522
|
+
} catch {
|
|
523
|
+
}
|
|
524
|
+
const defaultBranch = getDefaultBranch(cwd);
|
|
525
|
+
try {
|
|
526
|
+
git(["checkout", "-b", branchName, `origin/${defaultBranch}`], { cwd });
|
|
527
|
+
} catch {
|
|
528
|
+
git(["checkout", "-b", branchName], { cwd });
|
|
529
|
+
}
|
|
530
|
+
logger.info(` Created new branch: ${branchName}`);
|
|
531
|
+
return branchName;
|
|
532
|
+
}
|
|
533
|
+
function commitAll(message, cwd) {
|
|
534
|
+
const status = git(["status", "--porcelain"], { cwd });
|
|
535
|
+
if (!status) {
|
|
536
|
+
return { success: false, hash: "", message: "No changes to commit" };
|
|
537
|
+
}
|
|
538
|
+
git(["add", "-A"], { cwd });
|
|
539
|
+
git(["commit", "--no-gpg-sign", "-m", message], { cwd });
|
|
540
|
+
const hash = git(["rev-parse", "HEAD"], { cwd }).slice(0, 7);
|
|
541
|
+
logger.info(` Committed: ${hash} ${message}`);
|
|
542
|
+
return { success: true, hash, message };
|
|
543
|
+
}
|
|
544
|
+
function pushBranch(cwd) {
|
|
545
|
+
git(["push", "-u", "origin", "HEAD"], { cwd, timeout: 12e4 });
|
|
546
|
+
logger.info(" Pushed to origin");
|
|
547
|
+
}
|
|
548
|
+
var BASE_BRANCHES, _hookSafeEnv;
|
|
549
|
+
var init_git_utils = __esm({
|
|
550
|
+
"src/git-utils.ts"() {
|
|
551
|
+
"use strict";
|
|
552
|
+
init_logger();
|
|
553
|
+
BASE_BRANCHES = ["dev", "main", "master"];
|
|
554
|
+
_hookSafeEnv = null;
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// src/github-api.ts
|
|
559
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
560
|
+
function setGhCwd(cwd) {
|
|
561
|
+
_ghCwd = cwd;
|
|
562
|
+
}
|
|
563
|
+
function ghToken() {
|
|
564
|
+
return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
|
|
565
|
+
}
|
|
566
|
+
function gh(args2, options) {
|
|
567
|
+
const token = ghToken();
|
|
568
|
+
const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
|
|
569
|
+
return execFileSync3("gh", args2, {
|
|
570
|
+
encoding: "utf-8",
|
|
571
|
+
timeout: API_TIMEOUT_MS,
|
|
572
|
+
cwd: _ghCwd,
|
|
573
|
+
env,
|
|
574
|
+
input: options?.input,
|
|
575
|
+
stdio: options?.input ? ["pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
|
|
576
|
+
}).trim();
|
|
577
|
+
}
|
|
578
|
+
function setLabel(issueNumber, label) {
|
|
579
|
+
try {
|
|
580
|
+
gh(["issue", "edit", String(issueNumber), "--add-label", label]);
|
|
581
|
+
logger.info(` Label added: ${label}`);
|
|
582
|
+
} catch (err) {
|
|
583
|
+
logger.warn(` Failed to set label ${label}: ${err}`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
function postComment(issueNumber, body) {
|
|
587
|
+
try {
|
|
588
|
+
gh(
|
|
589
|
+
["issue", "comment", String(issueNumber), "--body-file", "-"],
|
|
590
|
+
{ input: body }
|
|
591
|
+
);
|
|
592
|
+
logger.info(` Comment posted on #${issueNumber}`);
|
|
593
|
+
} catch (err) {
|
|
594
|
+
logger.warn(` Failed to post comment: ${err}`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
function createPR(head, base, title, body) {
|
|
598
|
+
try {
|
|
599
|
+
const output = gh(
|
|
600
|
+
[
|
|
601
|
+
"pr",
|
|
602
|
+
"create",
|
|
603
|
+
"--head",
|
|
604
|
+
head,
|
|
605
|
+
"--base",
|
|
606
|
+
base,
|
|
607
|
+
"--title",
|
|
608
|
+
title,
|
|
609
|
+
"--body-file",
|
|
610
|
+
"-"
|
|
611
|
+
],
|
|
612
|
+
{ input: body }
|
|
613
|
+
);
|
|
614
|
+
const url = output.trim();
|
|
615
|
+
const match = url.match(/\/pull\/(\d+)$/);
|
|
616
|
+
const number = match ? parseInt(match[1], 10) : 0;
|
|
617
|
+
logger.info(` PR created: ${url}`);
|
|
618
|
+
return { number, url };
|
|
619
|
+
} catch (err) {
|
|
620
|
+
logger.error(` Failed to create PR: ${err}`);
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function setLifecycleLabel(issueNumber, phase) {
|
|
625
|
+
if (!LIFECYCLE_LABELS.includes(phase)) {
|
|
626
|
+
logger.warn(` Invalid lifecycle phase: ${phase}`);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
const othersToRemove = LIFECYCLE_LABELS.filter((l) => l !== phase).map((l) => `kody:${l}`).join(",");
|
|
630
|
+
if (othersToRemove) {
|
|
631
|
+
try {
|
|
632
|
+
gh(["issue", "edit", String(issueNumber), "--remove-label", othersToRemove]);
|
|
633
|
+
} catch {
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
setLabel(issueNumber, `kody:${phase}`);
|
|
637
|
+
}
|
|
638
|
+
var API_TIMEOUT_MS, LIFECYCLE_LABELS, _ghCwd;
|
|
639
|
+
var init_github_api = __esm({
|
|
640
|
+
"src/github-api.ts"() {
|
|
641
|
+
"use strict";
|
|
642
|
+
init_logger();
|
|
643
|
+
API_TIMEOUT_MS = 3e4;
|
|
644
|
+
LIFECYCLE_LABELS = ["planning", "building", "review", "done", "failed"];
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// src/verify-runner.ts
|
|
649
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
650
|
+
function runCommand(cmd, cwd, timeout) {
|
|
651
|
+
const parts = cmd.split(/\s+/);
|
|
652
|
+
try {
|
|
653
|
+
const output = execFileSync4(parts[0], parts.slice(1), {
|
|
654
|
+
cwd,
|
|
655
|
+
timeout,
|
|
656
|
+
encoding: "utf-8",
|
|
657
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
658
|
+
env: { ...process.env, FORCE_COLOR: "0" }
|
|
659
|
+
});
|
|
660
|
+
return { success: true, output: output ?? "", timedOut: false };
|
|
661
|
+
} catch (err) {
|
|
662
|
+
const e = err;
|
|
663
|
+
const output = `${e.stdout ?? ""}${e.stderr ?? ""}`;
|
|
664
|
+
return { success: false, output, timedOut: !!e.killed };
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
function parseErrors(output) {
|
|
668
|
+
const errors = [];
|
|
669
|
+
for (const line of output.split("\n")) {
|
|
670
|
+
if (/error|Error|ERROR|failed|Failed|FAIL|warning:|Warning:|WARN/i.test(line)) {
|
|
671
|
+
errors.push(line.slice(0, 500));
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return errors;
|
|
675
|
+
}
|
|
676
|
+
function extractSummary(output, cmdName) {
|
|
677
|
+
const summaryPatterns = /Test Suites|Tests|Coverage|ERRORS|FAILURES|success|completed/i;
|
|
678
|
+
const lines = output.split("\n").filter((l) => summaryPatterns.test(l));
|
|
679
|
+
return lines.slice(-3).map((l) => `[${cmdName}] ${l.trim()}`);
|
|
680
|
+
}
|
|
681
|
+
function runQualityGates(taskDir, projectRoot) {
|
|
682
|
+
const config = getProjectConfig();
|
|
683
|
+
const cwd = projectRoot ?? process.cwd();
|
|
684
|
+
const allErrors = [];
|
|
685
|
+
const allSummary = [];
|
|
686
|
+
let allPass = true;
|
|
687
|
+
const commands = [
|
|
688
|
+
{ name: "typecheck", cmd: config.quality.typecheck },
|
|
689
|
+
{ name: "test", cmd: config.quality.testUnit }
|
|
690
|
+
];
|
|
691
|
+
if (config.quality.lint) {
|
|
692
|
+
commands.push({ name: "lint", cmd: config.quality.lint });
|
|
693
|
+
}
|
|
694
|
+
for (const { name, cmd } of commands) {
|
|
695
|
+
if (!cmd) continue;
|
|
696
|
+
logger.info(` Running ${name}: ${cmd}`);
|
|
697
|
+
const result = runCommand(cmd, cwd, VERIFY_COMMAND_TIMEOUT_MS);
|
|
698
|
+
if (result.timedOut) {
|
|
699
|
+
allErrors.push(`${name}: timed out after ${VERIFY_COMMAND_TIMEOUT_MS / 1e3}s`);
|
|
700
|
+
allPass = false;
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
if (!result.success) {
|
|
704
|
+
allPass = false;
|
|
705
|
+
const errors = parseErrors(result.output);
|
|
706
|
+
allErrors.push(...errors.map((e) => `[${name}] ${e}`));
|
|
707
|
+
}
|
|
708
|
+
allSummary.push(...extractSummary(result.output, name));
|
|
709
|
+
}
|
|
710
|
+
return { pass: allPass, errors: allErrors, summary: allSummary };
|
|
711
|
+
}
|
|
712
|
+
var init_verify_runner = __esm({
|
|
713
|
+
"src/verify-runner.ts"() {
|
|
714
|
+
"use strict";
|
|
715
|
+
init_config();
|
|
716
|
+
init_logger();
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// src/state-machine.ts
|
|
721
|
+
import * as fs4 from "fs";
|
|
722
|
+
import * as path4 from "path";
|
|
723
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
724
|
+
function loadState(taskId, taskDir) {
|
|
725
|
+
const p = path4.join(taskDir, "status.json");
|
|
726
|
+
if (!fs4.existsSync(p)) return null;
|
|
727
|
+
try {
|
|
728
|
+
const raw = JSON.parse(fs4.readFileSync(p, "utf-8"));
|
|
729
|
+
if (raw.taskId === taskId) return raw;
|
|
730
|
+
return null;
|
|
731
|
+
} catch {
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
function writeState(state, taskDir) {
|
|
736
|
+
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
737
|
+
fs4.writeFileSync(
|
|
738
|
+
path4.join(taskDir, "status.json"),
|
|
739
|
+
JSON.stringify(state, null, 2)
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
function initState(taskId) {
|
|
743
|
+
const stages = {};
|
|
744
|
+
for (const stage of STAGES) {
|
|
745
|
+
stages[stage.name] = { state: "pending", retries: 0 };
|
|
746
|
+
}
|
|
747
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
748
|
+
return { taskId, state: "running", stages, createdAt: now, updatedAt: now };
|
|
749
|
+
}
|
|
750
|
+
function validateStageOutput(stageName, content) {
|
|
751
|
+
switch (stageName) {
|
|
752
|
+
case "taskify":
|
|
753
|
+
return validateTaskJson(content);
|
|
754
|
+
case "plan":
|
|
755
|
+
return validatePlanMd(content);
|
|
756
|
+
case "review":
|
|
757
|
+
return validateReviewMd(content);
|
|
758
|
+
default:
|
|
759
|
+
return { valid: true };
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
async function executeAgentStage(ctx, def) {
|
|
763
|
+
if (ctx.input.dryRun) {
|
|
764
|
+
logger.info(` [dry-run] skipping ${def.name}`);
|
|
765
|
+
return { outcome: "completed", retries: 0 };
|
|
766
|
+
}
|
|
767
|
+
const prompt = buildFullPrompt(def.name, ctx.taskId, ctx.taskDir, ctx.projectDir);
|
|
768
|
+
const model = resolveModel(def.modelTier, def.name);
|
|
769
|
+
logger.info(` model=${model} timeout=${def.timeout / 1e3}s`);
|
|
770
|
+
const config = getProjectConfig();
|
|
771
|
+
const extraEnv = {};
|
|
772
|
+
if (config.agent.litellmUrl) {
|
|
773
|
+
extraEnv.ANTHROPIC_BASE_URL = config.agent.litellmUrl;
|
|
774
|
+
}
|
|
775
|
+
const result = await ctx.runner.run(def.name, prompt, model, def.timeout, ctx.taskDir, {
|
|
776
|
+
cwd: ctx.projectDir,
|
|
777
|
+
env: extraEnv
|
|
778
|
+
});
|
|
779
|
+
if (result.outcome !== "completed") {
|
|
780
|
+
return { outcome: result.outcome, error: result.error, retries: 0 };
|
|
781
|
+
}
|
|
782
|
+
if (def.outputFile && result.output) {
|
|
783
|
+
fs4.writeFileSync(path4.join(ctx.taskDir, def.outputFile), result.output);
|
|
784
|
+
}
|
|
785
|
+
if (def.outputFile) {
|
|
786
|
+
const outputPath = path4.join(ctx.taskDir, def.outputFile);
|
|
787
|
+
if (fs4.existsSync(outputPath)) {
|
|
788
|
+
const content = fs4.readFileSync(outputPath, "utf-8");
|
|
789
|
+
const validation = validateStageOutput(def.name, content);
|
|
790
|
+
if (!validation.valid) {
|
|
791
|
+
logger.warn(` validation warning: ${validation.error}`);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return { outcome: "completed", outputFile: def.outputFile, retries: 0 };
|
|
796
|
+
}
|
|
797
|
+
function executeGateStage(ctx, def) {
|
|
798
|
+
if (ctx.input.dryRun) {
|
|
799
|
+
logger.info(` [dry-run] skipping ${def.name}`);
|
|
800
|
+
return { outcome: "completed", retries: 0 };
|
|
801
|
+
}
|
|
802
|
+
const verifyResult = runQualityGates(ctx.taskDir, ctx.projectDir);
|
|
803
|
+
const lines = [
|
|
804
|
+
`# Verification Report
|
|
805
|
+
`,
|
|
806
|
+
`## Result: ${verifyResult.pass ? "PASS" : "FAIL"}
|
|
807
|
+
`
|
|
808
|
+
];
|
|
809
|
+
if (verifyResult.errors.length > 0) {
|
|
810
|
+
lines.push(`
|
|
811
|
+
## Errors
|
|
812
|
+
`);
|
|
813
|
+
for (const e of verifyResult.errors) {
|
|
814
|
+
lines.push(`- ${e}
|
|
815
|
+
`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
if (verifyResult.summary.length > 0) {
|
|
819
|
+
lines.push(`
|
|
820
|
+
## Summary
|
|
821
|
+
`);
|
|
822
|
+
for (const s of verifyResult.summary) {
|
|
823
|
+
lines.push(`- ${s}
|
|
824
|
+
`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
fs4.writeFileSync(path4.join(ctx.taskDir, "verify.md"), lines.join(""));
|
|
828
|
+
return {
|
|
829
|
+
outcome: verifyResult.pass ? "completed" : "failed",
|
|
830
|
+
retries: 0
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
async function executeVerifyWithAutofix(ctx, def) {
|
|
834
|
+
const maxAttempts = def.maxRetries ?? 2;
|
|
835
|
+
for (let attempt = 0; attempt <= maxAttempts; attempt++) {
|
|
836
|
+
logger.info(` verification attempt ${attempt + 1}/${maxAttempts + 1}`);
|
|
837
|
+
const gateResult = executeGateStage(ctx, def);
|
|
838
|
+
if (gateResult.outcome === "completed") {
|
|
839
|
+
return { ...gateResult, retries: attempt };
|
|
840
|
+
}
|
|
841
|
+
if (attempt < maxAttempts) {
|
|
842
|
+
logger.info(` verification failed, running fixes...`);
|
|
843
|
+
const config = getProjectConfig();
|
|
844
|
+
const runFix = (cmd) => {
|
|
845
|
+
if (!cmd) return;
|
|
846
|
+
const parts = cmd.split(/\s+/);
|
|
847
|
+
try {
|
|
848
|
+
execFileSync5(parts[0], parts.slice(1), {
|
|
849
|
+
stdio: "pipe",
|
|
850
|
+
timeout: FIX_COMMAND_TIMEOUT_MS
|
|
851
|
+
});
|
|
852
|
+
} catch {
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
runFix(config.quality.lintFix);
|
|
856
|
+
runFix(config.quality.formatFix);
|
|
857
|
+
if (def.retryWithAgent) {
|
|
858
|
+
logger.info(` running ${def.retryWithAgent} agent...`);
|
|
859
|
+
await executeAgentStage(ctx, {
|
|
860
|
+
...def,
|
|
861
|
+
name: def.retryWithAgent,
|
|
862
|
+
type: "agent",
|
|
863
|
+
modelTier: "mid",
|
|
864
|
+
timeout: 3e5,
|
|
865
|
+
outputFile: void 0
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return {
|
|
871
|
+
outcome: "failed",
|
|
872
|
+
retries: maxAttempts,
|
|
873
|
+
error: "Verification failed after autofix attempts"
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
async function executeReviewWithFix(ctx, def) {
|
|
877
|
+
const reviewDef = STAGES.find((s) => s.name === "review");
|
|
878
|
+
const reviewFixDef = STAGES.find((s) => s.name === "review-fix");
|
|
879
|
+
const reviewResult = await executeAgentStage(ctx, reviewDef);
|
|
880
|
+
if (reviewResult.outcome !== "completed") {
|
|
881
|
+
return reviewResult;
|
|
882
|
+
}
|
|
883
|
+
const reviewFile = path4.join(ctx.taskDir, "review.md");
|
|
884
|
+
if (!fs4.existsSync(reviewFile)) {
|
|
885
|
+
return { outcome: "failed", retries: 0, error: "review.md not found" };
|
|
886
|
+
}
|
|
887
|
+
const content = fs4.readFileSync(reviewFile, "utf-8");
|
|
888
|
+
const hasIssues = /\bfail\b/i.test(content) && !/pass/i.test(content);
|
|
889
|
+
if (!hasIssues) {
|
|
890
|
+
return reviewResult;
|
|
891
|
+
}
|
|
892
|
+
logger.info(` review found issues, running review-fix...`);
|
|
893
|
+
const fixResult = await executeAgentStage(ctx, reviewFixDef);
|
|
894
|
+
if (fixResult.outcome !== "completed") {
|
|
895
|
+
return fixResult;
|
|
896
|
+
}
|
|
897
|
+
logger.info(` re-running review after fix...`);
|
|
898
|
+
return executeAgentStage(ctx, reviewDef);
|
|
899
|
+
}
|
|
900
|
+
function executeShipStage(ctx, _def) {
|
|
901
|
+
const shipPath = path4.join(ctx.taskDir, "ship.md");
|
|
902
|
+
if (ctx.input.local && !ctx.input.issueNumber) {
|
|
903
|
+
fs4.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
|
|
904
|
+
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
905
|
+
}
|
|
906
|
+
try {
|
|
907
|
+
const head = getCurrentBranch(ctx.projectDir);
|
|
908
|
+
const base = getDefaultBranch(ctx.projectDir);
|
|
909
|
+
pushBranch(ctx.projectDir);
|
|
910
|
+
const config = getProjectConfig();
|
|
911
|
+
let owner = config.github?.owner;
|
|
912
|
+
let repo = config.github?.repo;
|
|
913
|
+
if (!owner || !repo) {
|
|
914
|
+
try {
|
|
915
|
+
const remoteUrl = execFileSync5("git", ["remote", "get-url", "origin"], {
|
|
916
|
+
encoding: "utf-8",
|
|
917
|
+
cwd: ctx.projectDir
|
|
918
|
+
}).trim();
|
|
919
|
+
const match = remoteUrl.match(/github\.com[/:]([^/]+)\/([^/.]+)/);
|
|
920
|
+
if (match) {
|
|
921
|
+
owner = match[1];
|
|
922
|
+
repo = match[2];
|
|
923
|
+
}
|
|
924
|
+
} catch {
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
const taskMdPath = path4.join(ctx.taskDir, "task.md");
|
|
928
|
+
let title = "Update";
|
|
929
|
+
if (fs4.existsSync(taskMdPath)) {
|
|
930
|
+
const content = fs4.readFileSync(taskMdPath, "utf-8");
|
|
931
|
+
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
|
932
|
+
title = (lines[0] ?? "Update").slice(0, 72);
|
|
933
|
+
}
|
|
934
|
+
const body = "Generated by Kody pipeline\n\n---\n\u{1F916} Generated by Kody";
|
|
935
|
+
const pr = createPR(head, base, title, body);
|
|
936
|
+
if (pr) {
|
|
937
|
+
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
938
|
+
try {
|
|
939
|
+
postComment(ctx.input.issueNumber, `\u{1F389} PR created: ${pr.url}`);
|
|
940
|
+
} catch {
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
fs4.writeFileSync(shipPath, `# Ship
|
|
944
|
+
|
|
945
|
+
PR created: ${pr.url}
|
|
946
|
+
PR #${pr.number}
|
|
947
|
+
`);
|
|
948
|
+
} else {
|
|
949
|
+
fs4.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
|
|
950
|
+
}
|
|
951
|
+
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
952
|
+
} catch (err) {
|
|
953
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
954
|
+
fs4.writeFileSync(shipPath, `# Ship
|
|
955
|
+
|
|
956
|
+
Failed: ${msg}
|
|
957
|
+
`);
|
|
958
|
+
return { outcome: "failed", retries: 0, error: msg };
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
async function runPipeline(ctx) {
|
|
962
|
+
let state = loadState(ctx.taskId, ctx.taskDir);
|
|
963
|
+
if (!state) {
|
|
964
|
+
state = initState(ctx.taskId);
|
|
965
|
+
writeState(state, ctx.taskDir);
|
|
966
|
+
}
|
|
967
|
+
if (state.state !== "running") {
|
|
968
|
+
state.state = "running";
|
|
969
|
+
for (const stage of STAGES) {
|
|
970
|
+
const s = state.stages[stage.name];
|
|
971
|
+
if (s.state === "running" || s.state === "failed" || s.state === "timeout") {
|
|
972
|
+
state.stages[stage.name] = { ...s, state: "pending" };
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
writeState(state, ctx.taskDir);
|
|
976
|
+
}
|
|
977
|
+
const fromStage = ctx.input.fromStage;
|
|
978
|
+
let startExecution = !fromStage;
|
|
979
|
+
logger.info(`Pipeline started: ${ctx.taskId}`);
|
|
980
|
+
logger.info(`Stages: ${STAGES.map((s) => s.name).join(" \u2192 ")}`);
|
|
981
|
+
if (fromStage) logger.info(`Resuming from: ${fromStage}`);
|
|
982
|
+
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
983
|
+
const initialPhase = ctx.input.mode === "rerun" ? "building" : "planning";
|
|
984
|
+
setLifecycleLabel(ctx.input.issueNumber, initialPhase);
|
|
985
|
+
}
|
|
986
|
+
if (ctx.input.issueNumber && !ctx.input.dryRun) {
|
|
987
|
+
try {
|
|
988
|
+
const taskMdPath = path4.join(ctx.taskDir, "task.md");
|
|
989
|
+
const title = fs4.existsSync(taskMdPath) ? fs4.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
|
|
990
|
+
ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
|
|
991
|
+
} catch (err) {
|
|
992
|
+
logger.warn(` Failed to create feature branch: ${err}`);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
for (const def of STAGES) {
|
|
996
|
+
if (!startExecution) {
|
|
997
|
+
if (def.name === fromStage) {
|
|
998
|
+
startExecution = true;
|
|
999
|
+
} else {
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
if (state.stages[def.name].state === "completed") {
|
|
1004
|
+
logger.info(`[${def.name}] already completed, skipping`);
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
ciGroup(`Stage: ${def.name}`);
|
|
1008
|
+
state.stages[def.name] = {
|
|
1009
|
+
state: "running",
|
|
1010
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1011
|
+
retries: 0
|
|
1012
|
+
};
|
|
1013
|
+
writeState(state, ctx.taskDir);
|
|
1014
|
+
logger.info(`[${def.name}] starting...`);
|
|
1015
|
+
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
1016
|
+
if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
|
|
1017
|
+
if (def.name === "review") setLifecycleLabel(ctx.input.issueNumber, "review");
|
|
1018
|
+
}
|
|
1019
|
+
let result;
|
|
1020
|
+
try {
|
|
1021
|
+
if (def.type === "agent") {
|
|
1022
|
+
if (def.name === "review") {
|
|
1023
|
+
result = await executeReviewWithFix(ctx, def);
|
|
1024
|
+
} else {
|
|
1025
|
+
result = await executeAgentStage(ctx, def);
|
|
1026
|
+
}
|
|
1027
|
+
} else if (def.type === "gate") {
|
|
1028
|
+
if (def.name === "verify") {
|
|
1029
|
+
result = await executeVerifyWithAutofix(ctx, def);
|
|
1030
|
+
} else {
|
|
1031
|
+
result = executeGateStage(ctx, def);
|
|
1032
|
+
}
|
|
1033
|
+
} else if (def.type === "deterministic") {
|
|
1034
|
+
result = executeShipStage(ctx, def);
|
|
1035
|
+
} else {
|
|
1036
|
+
result = { outcome: "failed", retries: 0, error: `Unknown stage type: ${def.type}` };
|
|
1037
|
+
}
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
result = {
|
|
1040
|
+
outcome: "failed",
|
|
1041
|
+
retries: 0,
|
|
1042
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
ciGroupEnd();
|
|
1046
|
+
if (result.outcome === "completed") {
|
|
1047
|
+
state.stages[def.name] = {
|
|
1048
|
+
state: "completed",
|
|
1049
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1050
|
+
retries: result.retries,
|
|
1051
|
+
outputFile: result.outputFile
|
|
1052
|
+
};
|
|
1053
|
+
logger.info(`[${def.name}] \u2713 completed`);
|
|
1054
|
+
if (!ctx.input.dryRun && ctx.input.issueNumber) {
|
|
1055
|
+
if (def.name === "build") {
|
|
1056
|
+
try {
|
|
1057
|
+
commitAll(`feat(${ctx.taskId}): implement task`, ctx.projectDir);
|
|
1058
|
+
} catch {
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
if (def.name === "review-fix") {
|
|
1062
|
+
try {
|
|
1063
|
+
commitAll(`fix(${ctx.taskId}): address review`, ctx.projectDir);
|
|
1064
|
+
} catch {
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
} else if (result.outcome === "timed_out") {
|
|
1069
|
+
state.stages[def.name] = {
|
|
1070
|
+
state: "timeout",
|
|
1071
|
+
retries: result.retries,
|
|
1072
|
+
error: "Stage timed out"
|
|
1073
|
+
};
|
|
1074
|
+
state.state = "failed";
|
|
1075
|
+
writeState(state, ctx.taskDir);
|
|
1076
|
+
logger.error(`[${def.name}] \u23F1 timed out`);
|
|
1077
|
+
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
1078
|
+
setLifecycleLabel(ctx.input.issueNumber, "failed");
|
|
1079
|
+
}
|
|
1080
|
+
break;
|
|
1081
|
+
} else {
|
|
1082
|
+
state.stages[def.name] = {
|
|
1083
|
+
state: "failed",
|
|
1084
|
+
retries: result.retries,
|
|
1085
|
+
error: result.error ?? "Stage failed"
|
|
1086
|
+
};
|
|
1087
|
+
state.state = "failed";
|
|
1088
|
+
writeState(state, ctx.taskDir);
|
|
1089
|
+
logger.error(`[${def.name}] \u2717 failed: ${result.error}`);
|
|
1090
|
+
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
1091
|
+
setLifecycleLabel(ctx.input.issueNumber, "failed");
|
|
1092
|
+
}
|
|
1093
|
+
break;
|
|
1094
|
+
}
|
|
1095
|
+
writeState(state, ctx.taskDir);
|
|
1096
|
+
}
|
|
1097
|
+
const allCompleted = STAGES.every(
|
|
1098
|
+
(s) => state.stages[s.name].state === "completed"
|
|
1099
|
+
);
|
|
1100
|
+
if (allCompleted) {
|
|
1101
|
+
state.state = "completed";
|
|
1102
|
+
writeState(state, ctx.taskDir);
|
|
1103
|
+
logger.info(`Pipeline completed: ${ctx.taskId}`);
|
|
1104
|
+
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
1105
|
+
setLifecycleLabel(ctx.input.issueNumber, "done");
|
|
1106
|
+
}
|
|
1107
|
+
autoLearn(ctx);
|
|
1108
|
+
}
|
|
1109
|
+
return state;
|
|
1110
|
+
}
|
|
1111
|
+
function stripAnsi(str) {
|
|
1112
|
+
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
1113
|
+
}
|
|
1114
|
+
function autoLearn(ctx) {
|
|
1115
|
+
try {
|
|
1116
|
+
const memoryDir = path4.join(ctx.projectDir, ".kody", "memory");
|
|
1117
|
+
if (!fs4.existsSync(memoryDir)) {
|
|
1118
|
+
fs4.mkdirSync(memoryDir, { recursive: true });
|
|
1119
|
+
}
|
|
1120
|
+
const learnings = [];
|
|
1121
|
+
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1122
|
+
const verifyPath = path4.join(ctx.taskDir, "verify.md");
|
|
1123
|
+
if (fs4.existsSync(verifyPath)) {
|
|
1124
|
+
const verify = stripAnsi(fs4.readFileSync(verifyPath, "utf-8"));
|
|
1125
|
+
if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
|
|
1126
|
+
if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
|
|
1127
|
+
if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
|
|
1128
|
+
if (/prettier/i.test(verify)) learnings.push("- Uses prettier for formatting");
|
|
1129
|
+
if (/tsc\b/i.test(verify)) learnings.push("- Uses TypeScript (tsc)");
|
|
1130
|
+
if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
|
|
1131
|
+
if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
|
|
1132
|
+
}
|
|
1133
|
+
const reviewPath = path4.join(ctx.taskDir, "review.md");
|
|
1134
|
+
if (fs4.existsSync(reviewPath)) {
|
|
1135
|
+
const review = fs4.readFileSync(reviewPath, "utf-8");
|
|
1136
|
+
if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
|
|
1137
|
+
if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
|
|
1138
|
+
if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
|
|
1139
|
+
if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
|
|
1140
|
+
}
|
|
1141
|
+
const taskJsonPath = path4.join(ctx.taskDir, "task.json");
|
|
1142
|
+
if (fs4.existsSync(taskJsonPath)) {
|
|
1143
|
+
try {
|
|
1144
|
+
const raw = stripAnsi(fs4.readFileSync(taskJsonPath, "utf-8"));
|
|
1145
|
+
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
1146
|
+
const task = JSON.parse(cleaned);
|
|
1147
|
+
if (task.scope && Array.isArray(task.scope)) {
|
|
1148
|
+
const dirs = [...new Set(task.scope.map((s) => s.split("/").slice(0, -1).join("/")).filter(Boolean))];
|
|
1149
|
+
if (dirs.length > 0) learnings.push(`- Active directories: ${dirs.join(", ")}`);
|
|
1150
|
+
}
|
|
1151
|
+
} catch {
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
if (learnings.length > 0) {
|
|
1155
|
+
const conventionsPath = path4.join(memoryDir, "conventions.md");
|
|
1156
|
+
const entry = `
|
|
1157
|
+
## Learned ${timestamp2} (task: ${ctx.taskId})
|
|
1158
|
+
${learnings.join("\n")}
|
|
1159
|
+
`;
|
|
1160
|
+
fs4.appendFileSync(conventionsPath, entry);
|
|
1161
|
+
logger.info(`Auto-learned ${learnings.length} convention(s)`);
|
|
1162
|
+
}
|
|
1163
|
+
autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
|
|
1164
|
+
} catch {
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
1168
|
+
const archPath = path4.join(memoryDir, "architecture.md");
|
|
1169
|
+
if (fs4.existsSync(archPath)) return;
|
|
1170
|
+
const detected = [];
|
|
1171
|
+
const pkgPath = path4.join(projectDir, "package.json");
|
|
1172
|
+
if (fs4.existsSync(pkgPath)) {
|
|
1173
|
+
try {
|
|
1174
|
+
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
1175
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1176
|
+
if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
|
|
1177
|
+
else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
|
|
1178
|
+
else if (allDeps.express) detected.push(`- Framework: Express ${allDeps.express}`);
|
|
1179
|
+
else if (allDeps.fastify) detected.push(`- Framework: Fastify ${allDeps.fastify}`);
|
|
1180
|
+
if (allDeps.typescript) detected.push(`- Language: TypeScript ${allDeps.typescript}`);
|
|
1181
|
+
if (allDeps.vitest) detected.push(`- Testing: vitest ${allDeps.vitest}`);
|
|
1182
|
+
else if (allDeps.jest) detected.push(`- Testing: jest ${allDeps.jest}`);
|
|
1183
|
+
if (allDeps.eslint) detected.push(`- Linting: eslint ${allDeps.eslint}`);
|
|
1184
|
+
if (allDeps.prisma || allDeps["@prisma/client"]) detected.push("- Database: Prisma ORM");
|
|
1185
|
+
if (allDeps.drizzle || allDeps["drizzle-orm"]) detected.push("- Database: Drizzle ORM");
|
|
1186
|
+
if (allDeps.pg || allDeps.postgres) detected.push("- Database: PostgreSQL");
|
|
1187
|
+
if (allDeps.payload || allDeps["@payloadcms/next"]) detected.push(`- CMS: Payload CMS`);
|
|
1188
|
+
if (pkg.type === "module") detected.push("- Module system: ESM");
|
|
1189
|
+
else detected.push("- Module system: CommonJS");
|
|
1190
|
+
if (fs4.existsSync(path4.join(projectDir, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
|
|
1191
|
+
else if (fs4.existsSync(path4.join(projectDir, "yarn.lock"))) detected.push("- Package manager: yarn");
|
|
1192
|
+
else if (fs4.existsSync(path4.join(projectDir, "package-lock.json"))) detected.push("- Package manager: npm");
|
|
1193
|
+
} catch {
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
const topDirs = [];
|
|
1197
|
+
try {
|
|
1198
|
+
const entries = fs4.readdirSync(projectDir, { withFileTypes: true });
|
|
1199
|
+
for (const entry of entries) {
|
|
1200
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
1201
|
+
topDirs.push(entry.name);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
if (topDirs.length > 0) detected.push(`- Top-level directories: ${topDirs.join(", ")}`);
|
|
1205
|
+
} catch {
|
|
1206
|
+
}
|
|
1207
|
+
const srcDir = path4.join(projectDir, "src");
|
|
1208
|
+
if (fs4.existsSync(srcDir)) {
|
|
1209
|
+
try {
|
|
1210
|
+
const srcEntries = fs4.readdirSync(srcDir, { withFileTypes: true });
|
|
1211
|
+
const srcDirs = srcEntries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1212
|
+
if (srcDirs.length > 0) detected.push(`- src/ structure: ${srcDirs.join(", ")}`);
|
|
1213
|
+
} catch {
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
if (detected.length > 0) {
|
|
1217
|
+
const content = `# Architecture (auto-detected ${timestamp2})
|
|
1218
|
+
|
|
1219
|
+
## Overview
|
|
1220
|
+
${detected.join("\n")}
|
|
1221
|
+
`;
|
|
1222
|
+
fs4.writeFileSync(archPath, content);
|
|
1223
|
+
logger.info(`Auto-detected architecture (${detected.length} items)`);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
function printStatus(taskId, taskDir) {
|
|
1227
|
+
const state = loadState(taskId, taskDir);
|
|
1228
|
+
if (!state) {
|
|
1229
|
+
console.log(`No status found for task ${taskId}`);
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
console.log(`
|
|
1233
|
+
Task: ${state.taskId}`);
|
|
1234
|
+
console.log(`State: ${state.state}`);
|
|
1235
|
+
console.log(`Created: ${state.createdAt}`);
|
|
1236
|
+
console.log(`Updated: ${state.updatedAt}
|
|
1237
|
+
`);
|
|
1238
|
+
for (const stage of STAGES) {
|
|
1239
|
+
const s = state.stages[stage.name];
|
|
1240
|
+
const icon = s.state === "completed" ? "\u2713" : s.state === "failed" ? "\u2717" : s.state === "running" ? "\u25B6" : s.state === "timeout" ? "\u23F1" : "\u25CB";
|
|
1241
|
+
const extra = s.error ? ` \u2014 ${s.error}` : "";
|
|
1242
|
+
console.log(` ${icon} ${stage.name}: ${s.state}${extra}`);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
var init_state_machine = __esm({
|
|
1246
|
+
"src/state-machine.ts"() {
|
|
1247
|
+
"use strict";
|
|
1248
|
+
init_definitions();
|
|
1249
|
+
init_context();
|
|
1250
|
+
init_validators();
|
|
1251
|
+
init_git_utils();
|
|
1252
|
+
init_github_api();
|
|
1253
|
+
init_verify_runner();
|
|
1254
|
+
init_config();
|
|
1255
|
+
init_logger();
|
|
1256
|
+
}
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
// src/preflight.ts
|
|
1260
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
1261
|
+
import * as fs5 from "fs";
|
|
1262
|
+
function check(name, fn) {
|
|
1263
|
+
try {
|
|
1264
|
+
const detail = fn() ?? void 0;
|
|
1265
|
+
return { name, ok: true, detail };
|
|
1266
|
+
} catch {
|
|
1267
|
+
return { name, ok: false };
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
function runPreflight() {
|
|
1271
|
+
const checks = [
|
|
1272
|
+
check("claude CLI", () => {
|
|
1273
|
+
const v = execFileSync6("claude", ["--version"], {
|
|
1274
|
+
encoding: "utf-8",
|
|
1275
|
+
timeout: 1e4,
|
|
1276
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1277
|
+
}).trim();
|
|
1278
|
+
return v;
|
|
1279
|
+
}),
|
|
1280
|
+
check("git repo", () => {
|
|
1281
|
+
execFileSync6("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
1282
|
+
encoding: "utf-8",
|
|
1283
|
+
timeout: 5e3,
|
|
1284
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1285
|
+
});
|
|
1286
|
+
}),
|
|
1287
|
+
check("pnpm", () => {
|
|
1288
|
+
const v = execFileSync6("pnpm", ["--version"], {
|
|
1289
|
+
encoding: "utf-8",
|
|
1290
|
+
timeout: 5e3,
|
|
1291
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1292
|
+
}).trim();
|
|
1293
|
+
return v;
|
|
1294
|
+
}),
|
|
1295
|
+
check("node >= 18", () => {
|
|
1296
|
+
const v = execFileSync6("node", ["--version"], {
|
|
1297
|
+
encoding: "utf-8",
|
|
1298
|
+
timeout: 5e3,
|
|
1299
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1300
|
+
}).trim();
|
|
1301
|
+
const major = parseInt(v.replace("v", "").split(".")[0], 10);
|
|
1302
|
+
if (major < 18) throw new Error(`Node ${v} < 18`);
|
|
1303
|
+
return v;
|
|
1304
|
+
}),
|
|
1305
|
+
check("gh CLI", () => {
|
|
1306
|
+
const v = execFileSync6("gh", ["--version"], {
|
|
1307
|
+
encoding: "utf-8",
|
|
1308
|
+
timeout: 5e3,
|
|
1309
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1310
|
+
}).trim().split("\n")[0];
|
|
1311
|
+
return v;
|
|
1312
|
+
}),
|
|
1313
|
+
check("package.json", () => {
|
|
1314
|
+
if (!fs5.existsSync("package.json")) throw new Error("not found");
|
|
1315
|
+
})
|
|
1316
|
+
];
|
|
1317
|
+
const failed = checks.filter((c) => !c.ok);
|
|
1318
|
+
for (const c of checks) {
|
|
1319
|
+
logger.info(` ${c.ok ? "\u2713" : "\u2717"} ${c.name}${c.detail ? ` (${c.detail})` : ""}`);
|
|
1320
|
+
}
|
|
1321
|
+
if (failed.length > 0) {
|
|
1322
|
+
throw new Error(
|
|
1323
|
+
`Preflight failed: ${failed.map((c) => c.name).join(", ")}`
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
var init_preflight = __esm({
|
|
1328
|
+
"src/preflight.ts"() {
|
|
1329
|
+
"use strict";
|
|
1330
|
+
init_logger();
|
|
1331
|
+
}
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
// src/entry.ts
|
|
1335
|
+
var entry_exports = {};
|
|
1336
|
+
import * as fs6 from "fs";
|
|
1337
|
+
import * as path5 from "path";
|
|
1338
|
+
function getArg(args2, flag) {
|
|
1339
|
+
const idx = args2.indexOf(flag);
|
|
1340
|
+
if (idx !== -1 && args2[idx + 1] && !args2[idx + 1].startsWith("--")) {
|
|
1341
|
+
return args2[idx + 1];
|
|
1342
|
+
}
|
|
1343
|
+
return void 0;
|
|
1344
|
+
}
|
|
1345
|
+
function hasFlag(args2, flag) {
|
|
1346
|
+
return args2.includes(flag);
|
|
1347
|
+
}
|
|
1348
|
+
function parseArgs() {
|
|
1349
|
+
const args2 = process.argv.slice(2);
|
|
1350
|
+
if (hasFlag(args2, "--help") || hasFlag(args2, "-h") || args2.length === 0) {
|
|
1351
|
+
console.log(`Usage:
|
|
1352
|
+
kody run --task-id <id> [--task "<desc>"] [--cwd <path>] [--issue-number <n>] [--feedback "<text>"] [--local] [--dry-run]
|
|
1353
|
+
kody rerun --task-id <id> --from <stage> [--cwd <path>] [--issue-number <n>]
|
|
1354
|
+
kody status --task-id <id> [--cwd <path>]
|
|
1355
|
+
kody --help`);
|
|
1356
|
+
process.exit(0);
|
|
1357
|
+
}
|
|
1358
|
+
const command2 = args2[0];
|
|
1359
|
+
if (!["run", "rerun", "status"].includes(command2)) {
|
|
1360
|
+
console.error(`Unknown command: ${command2}`);
|
|
1361
|
+
process.exit(1);
|
|
1362
|
+
}
|
|
1363
|
+
const issueStr = getArg(args2, "--issue-number") ?? process.env.ISSUE_NUMBER;
|
|
1364
|
+
const localFlag = hasFlag(args2, "--local");
|
|
1365
|
+
return {
|
|
1366
|
+
command: command2,
|
|
1367
|
+
taskId: getArg(args2, "--task-id") ?? process.env.TASK_ID,
|
|
1368
|
+
task: getArg(args2, "--task"),
|
|
1369
|
+
fromStage: getArg(args2, "--from") ?? process.env.FROM_STAGE,
|
|
1370
|
+
dryRun: hasFlag(args2, "--dry-run") || process.env.DRY_RUN === "true",
|
|
1371
|
+
cwd: getArg(args2, "--cwd"),
|
|
1372
|
+
issueNumber: issueStr ? parseInt(issueStr, 10) : void 0,
|
|
1373
|
+
feedback: getArg(args2, "--feedback") ?? process.env.FEEDBACK,
|
|
1374
|
+
local: localFlag || !isCI2 && !hasFlag(args2, "--no-local")
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
function generateTaskId() {
|
|
1378
|
+
const now = /* @__PURE__ */ new Date();
|
|
1379
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1380
|
+
return `${String(now.getFullYear()).slice(2)}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
1381
|
+
}
|
|
1382
|
+
async function main() {
|
|
1383
|
+
const input = parseArgs();
|
|
1384
|
+
let taskId = input.taskId;
|
|
1385
|
+
if (!taskId) {
|
|
1386
|
+
if (input.command === "run" && input.task) {
|
|
1387
|
+
taskId = generateTaskId();
|
|
1388
|
+
} else {
|
|
1389
|
+
console.error("--task-id is required");
|
|
1390
|
+
process.exit(1);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
const projectDir = input.cwd ? path5.resolve(input.cwd) : process.cwd();
|
|
1394
|
+
if (input.cwd) {
|
|
1395
|
+
if (!fs6.existsSync(projectDir)) {
|
|
1396
|
+
console.error(`--cwd path does not exist: ${projectDir}`);
|
|
1397
|
+
process.exit(1);
|
|
1398
|
+
}
|
|
1399
|
+
setConfigDir(projectDir);
|
|
1400
|
+
setGhCwd(projectDir);
|
|
1401
|
+
logger.info(`Working directory: ${projectDir}`);
|
|
1402
|
+
}
|
|
1403
|
+
const taskDir = path5.join(projectDir, ".tasks", taskId);
|
|
1404
|
+
fs6.mkdirSync(taskDir, { recursive: true });
|
|
1405
|
+
if (input.command === "status") {
|
|
1406
|
+
printStatus(taskId, taskDir);
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
logger.info("Preflight checks:");
|
|
1410
|
+
runPreflight();
|
|
1411
|
+
if (input.task) {
|
|
1412
|
+
fs6.writeFileSync(path5.join(taskDir, "task.md"), input.task);
|
|
1413
|
+
}
|
|
1414
|
+
if (input.command === "run") {
|
|
1415
|
+
const taskMdPath = path5.join(taskDir, "task.md");
|
|
1416
|
+
if (!fs6.existsSync(taskMdPath)) {
|
|
1417
|
+
console.error("No task.md found. Provide --task or ensure .tasks/<id>/task.md exists.");
|
|
1418
|
+
process.exit(1);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
if (input.command === "rerun" && !input.fromStage) {
|
|
1422
|
+
console.error("--from <stage> is required for rerun");
|
|
1423
|
+
process.exit(1);
|
|
1424
|
+
}
|
|
1425
|
+
const runner = createClaudeCodeRunner();
|
|
1426
|
+
const healthy = await runner.healthCheck();
|
|
1427
|
+
if (!healthy) {
|
|
1428
|
+
console.error("Claude Code CLI not available. Install: npm i -g @anthropic-ai/claude-code");
|
|
1429
|
+
process.exit(1);
|
|
1430
|
+
}
|
|
1431
|
+
const ctx = {
|
|
1432
|
+
taskId,
|
|
1433
|
+
taskDir,
|
|
1434
|
+
projectDir,
|
|
1435
|
+
runner,
|
|
1436
|
+
input: {
|
|
1437
|
+
mode: input.command === "rerun" ? "rerun" : "full",
|
|
1438
|
+
fromStage: input.fromStage,
|
|
1439
|
+
dryRun: input.dryRun,
|
|
1440
|
+
issueNumber: input.issueNumber,
|
|
1441
|
+
feedback: input.feedback,
|
|
1442
|
+
local: input.local
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
logger.info(`Mode: ${ctx.input.mode}${ctx.input.local ? " (local)" : " (CI)"}`);
|
|
1446
|
+
if (ctx.input.issueNumber) logger.info(`Issue: #${ctx.input.issueNumber}`);
|
|
1447
|
+
const state = await runPipeline(ctx);
|
|
1448
|
+
const files = fs6.readdirSync(taskDir);
|
|
1449
|
+
console.log(`
|
|
1450
|
+
Artifacts in ${taskDir}:`);
|
|
1451
|
+
for (const f of files) {
|
|
1452
|
+
console.log(` ${f}`);
|
|
1453
|
+
}
|
|
1454
|
+
if (state.state === "failed") {
|
|
1455
|
+
process.exit(1);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
var isCI2;
|
|
1459
|
+
var init_entry = __esm({
|
|
1460
|
+
"src/entry.ts"() {
|
|
1461
|
+
"use strict";
|
|
1462
|
+
init_agent_runner();
|
|
1463
|
+
init_state_machine();
|
|
1464
|
+
init_preflight();
|
|
1465
|
+
init_config();
|
|
1466
|
+
init_github_api();
|
|
1467
|
+
init_logger();
|
|
1468
|
+
isCI2 = !!process.env.GITHUB_ACTIONS;
|
|
1469
|
+
main().catch((err) => {
|
|
1470
|
+
console.error(err instanceof Error ? err.message : err);
|
|
1471
|
+
process.exit(1);
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1476
|
+
// src/bin/cli.ts
|
|
1477
|
+
import * as fs7 from "fs";
|
|
1478
|
+
import * as path6 from "path";
|
|
1479
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
1480
|
+
import { fileURLToPath } from "url";
|
|
1481
|
+
var __dirname = path6.dirname(fileURLToPath(import.meta.url));
|
|
1482
|
+
var PKG_ROOT = path6.resolve(__dirname, "..", "..");
|
|
1483
|
+
function getVersion() {
|
|
1484
|
+
const pkgPath = path6.join(PKG_ROOT, "package.json");
|
|
1485
|
+
const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
|
|
1486
|
+
return pkg.version;
|
|
1487
|
+
}
|
|
1488
|
+
function checkCommand(name, args2, fix) {
|
|
1489
|
+
try {
|
|
1490
|
+
const output = execFileSync7(name, args2, {
|
|
1491
|
+
encoding: "utf-8",
|
|
1492
|
+
timeout: 1e4,
|
|
1493
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1494
|
+
}).trim();
|
|
1495
|
+
return { name: `${name} CLI`, ok: true, detail: output.split("\n")[0] };
|
|
1496
|
+
} catch {
|
|
1497
|
+
return { name: `${name} CLI`, ok: false, fix };
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
function checkFile(filePath, description, fix) {
|
|
1501
|
+
if (fs7.existsSync(filePath)) {
|
|
1502
|
+
return { name: description, ok: true, detail: filePath };
|
|
1503
|
+
}
|
|
1504
|
+
return { name: description, ok: false, fix };
|
|
1505
|
+
}
|
|
1506
|
+
function checkGhAuth(cwd) {
|
|
1507
|
+
try {
|
|
1508
|
+
const output = execFileSync7("gh", ["auth", "status"], {
|
|
1509
|
+
encoding: "utf-8",
|
|
1510
|
+
timeout: 1e4,
|
|
1511
|
+
cwd,
|
|
1512
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1513
|
+
}).trim();
|
|
1514
|
+
const account = output.match(/Logged in to .* account (\S+)/)?.[1];
|
|
1515
|
+
return { name: "gh auth", ok: true, detail: account ?? "authenticated" };
|
|
1516
|
+
} catch (err) {
|
|
1517
|
+
const stderr = err.stderr ?? "";
|
|
1518
|
+
if (stderr.includes("not logged")) {
|
|
1519
|
+
return { name: "gh auth", ok: false, fix: "Run: gh auth login" };
|
|
1520
|
+
}
|
|
1521
|
+
return { name: "gh auth", ok: true, detail: "authenticated (partial check)" };
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
function checkGhRepoAccess(cwd) {
|
|
1525
|
+
try {
|
|
1526
|
+
const remote = execFileSync7("git", ["remote", "get-url", "origin"], {
|
|
1527
|
+
encoding: "utf-8",
|
|
1528
|
+
timeout: 5e3,
|
|
1529
|
+
cwd,
|
|
1530
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1531
|
+
}).trim();
|
|
1532
|
+
const match = remote.match(/github\.com[/:]([^/]+)\/([^/.]+)/);
|
|
1533
|
+
if (!match) {
|
|
1534
|
+
return { name: "GitHub repo", ok: false, fix: "Set git remote origin to a GitHub URL" };
|
|
1535
|
+
}
|
|
1536
|
+
const repoSlug = `${match[1]}/${match[2]}`;
|
|
1537
|
+
execFileSync7("gh", ["repo", "view", repoSlug, "--json", "name"], {
|
|
1538
|
+
encoding: "utf-8",
|
|
1539
|
+
timeout: 1e4,
|
|
1540
|
+
cwd,
|
|
1541
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1542
|
+
});
|
|
1543
|
+
return { name: "GitHub repo access", ok: true, detail: repoSlug };
|
|
1544
|
+
} catch {
|
|
1545
|
+
return { name: "GitHub repo access", ok: false, fix: "Verify gh auth and repo permissions" };
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
function checkGhSecret(repoSlug, secretName) {
|
|
1549
|
+
try {
|
|
1550
|
+
const output = execFileSync7("gh", ["secret", "list", "--repo", repoSlug], {
|
|
1551
|
+
encoding: "utf-8",
|
|
1552
|
+
timeout: 1e4,
|
|
1553
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1554
|
+
}).trim();
|
|
1555
|
+
if (output.includes(secretName)) {
|
|
1556
|
+
return { name: `Secret: ${secretName}`, ok: true, detail: "configured" };
|
|
1557
|
+
}
|
|
1558
|
+
return {
|
|
1559
|
+
name: `Secret: ${secretName}`,
|
|
1560
|
+
ok: false,
|
|
1561
|
+
fix: `Run: gh secret set ${secretName} --repo ${repoSlug}`
|
|
1562
|
+
};
|
|
1563
|
+
} catch {
|
|
1564
|
+
return {
|
|
1565
|
+
name: `Secret: ${secretName}`,
|
|
1566
|
+
ok: false,
|
|
1567
|
+
fix: `Run: gh secret set ${secretName} --repo ${repoSlug} (or check permissions)`
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
function detectArchitecture(cwd) {
|
|
1572
|
+
const detected = [];
|
|
1573
|
+
const pkgPath = path6.join(cwd, "package.json");
|
|
1574
|
+
if (fs7.existsSync(pkgPath)) {
|
|
1575
|
+
try {
|
|
1576
|
+
const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
|
|
1577
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1578
|
+
if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
|
|
1579
|
+
else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
|
|
1580
|
+
else if (allDeps.express) detected.push(`- Framework: Express ${allDeps.express}`);
|
|
1581
|
+
else if (allDeps.fastify) detected.push(`- Framework: Fastify ${allDeps.fastify}`);
|
|
1582
|
+
else if (allDeps.hono) detected.push(`- Framework: Hono ${allDeps.hono}`);
|
|
1583
|
+
if (allDeps.typescript) detected.push(`- Language: TypeScript ${allDeps.typescript}`);
|
|
1584
|
+
if (allDeps.vitest) detected.push(`- Testing: vitest ${allDeps.vitest}`);
|
|
1585
|
+
else if (allDeps.jest) detected.push(`- Testing: jest ${allDeps.jest}`);
|
|
1586
|
+
else if (allDeps.mocha) detected.push(`- Testing: mocha ${allDeps.mocha}`);
|
|
1587
|
+
if (allDeps.eslint) detected.push(`- Linting: eslint ${allDeps.eslint}`);
|
|
1588
|
+
if (allDeps.prettier) detected.push(`- Formatting: prettier ${allDeps.prettier}`);
|
|
1589
|
+
if (allDeps.biome || allDeps["@biomejs/biome"]) detected.push("- Formatting: biome");
|
|
1590
|
+
if (allDeps.prisma || allDeps["@prisma/client"]) detected.push("- ORM: Prisma");
|
|
1591
|
+
if (allDeps["drizzle-orm"]) detected.push("- ORM: Drizzle");
|
|
1592
|
+
if (allDeps.pg || allDeps.postgres) detected.push("- Database: PostgreSQL");
|
|
1593
|
+
if (allDeps.mongodb || allDeps.mongoose) detected.push("- Database: MongoDB");
|
|
1594
|
+
if (allDeps.redis || allDeps.ioredis) detected.push("- Cache: Redis");
|
|
1595
|
+
if (allDeps.payload || allDeps["@payloadcms/next"]) detected.push("- CMS: Payload CMS");
|
|
1596
|
+
if (allDeps.tailwindcss) detected.push(`- CSS: Tailwind CSS ${allDeps.tailwindcss}`);
|
|
1597
|
+
if (pkg.type === "module") detected.push("- Module system: ESM");
|
|
1598
|
+
else detected.push("- Module system: CommonJS");
|
|
1599
|
+
if (fs7.existsSync(path6.join(cwd, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
|
|
1600
|
+
else if (fs7.existsSync(path6.join(cwd, "yarn.lock"))) detected.push("- Package manager: yarn");
|
|
1601
|
+
else if (fs7.existsSync(path6.join(cwd, "bun.lockb"))) detected.push("- Package manager: bun");
|
|
1602
|
+
else if (fs7.existsSync(path6.join(cwd, "package-lock.json"))) detected.push("- Package manager: npm");
|
|
1603
|
+
} catch {
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
try {
|
|
1607
|
+
const entries = fs7.readdirSync(cwd, { withFileTypes: true });
|
|
1608
|
+
const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
|
|
1609
|
+
if (dirs.length > 0) detected.push(`- Top-level directories: ${dirs.join(", ")}`);
|
|
1610
|
+
} catch {
|
|
1611
|
+
}
|
|
1612
|
+
const srcDir = path6.join(cwd, "src");
|
|
1613
|
+
if (fs7.existsSync(srcDir)) {
|
|
1614
|
+
try {
|
|
1615
|
+
const srcEntries = fs7.readdirSync(srcDir, { withFileTypes: true });
|
|
1616
|
+
const srcDirs = srcEntries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1617
|
+
if (srcDirs.length > 0) detected.push(`- src/ structure: ${srcDirs.join(", ")}`);
|
|
1618
|
+
} catch {
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
const configs = [];
|
|
1622
|
+
if (fs7.existsSync(path6.join(cwd, "tsconfig.json"))) configs.push("tsconfig.json");
|
|
1623
|
+
if (fs7.existsSync(path6.join(cwd, "docker-compose.yml")) || fs7.existsSync(path6.join(cwd, "docker-compose.yaml"))) configs.push("docker-compose");
|
|
1624
|
+
if (fs7.existsSync(path6.join(cwd, "Dockerfile"))) configs.push("Dockerfile");
|
|
1625
|
+
if (fs7.existsSync(path6.join(cwd, ".env")) || fs7.existsSync(path6.join(cwd, ".env.local"))) configs.push(".env");
|
|
1626
|
+
if (configs.length > 0) detected.push(`- Config files: ${configs.join(", ")}`);
|
|
1627
|
+
return detected;
|
|
1628
|
+
}
|
|
1629
|
+
function initCommand(opts) {
|
|
1630
|
+
const cwd = process.cwd();
|
|
1631
|
+
console.log(`
|
|
1632
|
+
\u{1F527} Kody Engine Lite \u2014 Init
|
|
1633
|
+
`);
|
|
1634
|
+
console.log(`Project: ${cwd}
|
|
1635
|
+
`);
|
|
1636
|
+
console.log("\u2500\u2500 Files \u2500\u2500");
|
|
1637
|
+
const templatesDir = path6.join(PKG_ROOT, "templates");
|
|
1638
|
+
const workflowSrc = path6.join(templatesDir, "kody.yml");
|
|
1639
|
+
const workflowDest = path6.join(cwd, ".github", "workflows", "kody.yml");
|
|
1640
|
+
if (!fs7.existsSync(workflowSrc)) {
|
|
1641
|
+
console.error(" \u2717 Template kody.yml not found in package");
|
|
1642
|
+
process.exit(1);
|
|
1643
|
+
}
|
|
1644
|
+
if (fs7.existsSync(workflowDest) && !opts.force) {
|
|
1645
|
+
console.log(" \u25CB .github/workflows/kody.yml (exists, use --force to overwrite)");
|
|
1646
|
+
} else {
|
|
1647
|
+
fs7.mkdirSync(path6.dirname(workflowDest), { recursive: true });
|
|
1648
|
+
fs7.copyFileSync(workflowSrc, workflowDest);
|
|
1649
|
+
console.log(" \u2713 .github/workflows/kody.yml");
|
|
1650
|
+
}
|
|
1651
|
+
const configDest = path6.join(cwd, "kody.config.json");
|
|
1652
|
+
if (!fs7.existsSync(configDest)) {
|
|
1653
|
+
const defaultConfig = {
|
|
1654
|
+
quality: {
|
|
1655
|
+
typecheck: "pnpm tsc --noEmit",
|
|
1656
|
+
lint: "",
|
|
1657
|
+
lintFix: "",
|
|
1658
|
+
format: "",
|
|
1659
|
+
formatFix: "",
|
|
1660
|
+
testUnit: "pnpm test"
|
|
1661
|
+
},
|
|
1662
|
+
git: { defaultBranch: "main" },
|
|
1663
|
+
github: { owner: "", repo: "" },
|
|
1664
|
+
paths: { taskDir: ".tasks" },
|
|
1665
|
+
agent: {
|
|
1666
|
+
runner: "claude-code",
|
|
1667
|
+
modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
|
|
1668
|
+
}
|
|
1669
|
+
};
|
|
1670
|
+
fs7.writeFileSync(configDest, JSON.stringify(defaultConfig, null, 2) + "\n");
|
|
1671
|
+
console.log(" \u2713 kody.config.json (created \u2014 edit github.owner and github.repo)");
|
|
1672
|
+
} else {
|
|
1673
|
+
console.log(" \u25CB kody.config.json (exists)");
|
|
1674
|
+
}
|
|
1675
|
+
const gitignorePath = path6.join(cwd, ".gitignore");
|
|
1676
|
+
if (fs7.existsSync(gitignorePath)) {
|
|
1677
|
+
const content = fs7.readFileSync(gitignorePath, "utf-8");
|
|
1678
|
+
if (!content.includes(".tasks/")) {
|
|
1679
|
+
fs7.appendFileSync(gitignorePath, "\n.tasks/\n");
|
|
1680
|
+
console.log(" \u2713 .gitignore (added .tasks/)");
|
|
1681
|
+
} else {
|
|
1682
|
+
console.log(" \u25CB .gitignore (.tasks/ already present)");
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
console.log("\n\u2500\u2500 Prerequisites \u2500\u2500");
|
|
1686
|
+
const checks = [
|
|
1687
|
+
checkCommand("claude", ["--version"], "Install: npm i -g @anthropic-ai/claude-code"),
|
|
1688
|
+
checkCommand("gh", ["--version"], "Install: https://cli.github.com"),
|
|
1689
|
+
checkCommand("git", ["--version"], "Install git"),
|
|
1690
|
+
checkCommand("node", ["--version"], "Install Node.js >= 22"),
|
|
1691
|
+
checkCommand("pnpm", ["--version"], "Install: npm i -g pnpm"),
|
|
1692
|
+
checkFile(path6.join(cwd, "package.json"), "package.json", "Run: pnpm init")
|
|
1693
|
+
];
|
|
1694
|
+
for (const c of checks) {
|
|
1695
|
+
if (c.ok) {
|
|
1696
|
+
console.log(` \u2713 ${c.name}${c.detail ? ` (${c.detail})` : ""}`);
|
|
1697
|
+
} else {
|
|
1698
|
+
console.log(` \u2717 ${c.name} \u2014 ${c.fix}`);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
console.log("\n\u2500\u2500 GitHub \u2500\u2500");
|
|
1702
|
+
const ghAuth = checkGhAuth(cwd);
|
|
1703
|
+
console.log(ghAuth.ok ? ` \u2713 ${ghAuth.name} (${ghAuth.detail})` : ` \u2717 ${ghAuth.name} \u2014 ${ghAuth.fix}`);
|
|
1704
|
+
const ghRepo = checkGhRepoAccess(cwd);
|
|
1705
|
+
console.log(ghRepo.ok ? ` \u2713 ${ghRepo.name} (${ghRepo.detail})` : ` \u2717 ${ghRepo.name} \u2014 ${ghRepo.fix}`);
|
|
1706
|
+
if (ghRepo.ok && ghRepo.detail) {
|
|
1707
|
+
const repoSlug = ghRepo.detail;
|
|
1708
|
+
const secretChecks = [
|
|
1709
|
+
checkGhSecret(repoSlug, "ANTHROPIC_API_KEY")
|
|
1710
|
+
];
|
|
1711
|
+
for (const c of secretChecks) {
|
|
1712
|
+
if (c.ok) {
|
|
1713
|
+
console.log(` \u2713 ${c.name}`);
|
|
1714
|
+
} else {
|
|
1715
|
+
console.log(` \u2717 ${c.name} \u2014 ${c.fix}`);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
console.log("\n\u2500\u2500 Config \u2500\u2500");
|
|
1720
|
+
if (fs7.existsSync(configDest)) {
|
|
1721
|
+
try {
|
|
1722
|
+
const config = JSON.parse(fs7.readFileSync(configDest, "utf-8"));
|
|
1723
|
+
const configChecks = [];
|
|
1724
|
+
if (config.github?.owner && config.github?.repo) {
|
|
1725
|
+
configChecks.push({ name: "github.owner/repo", ok: true, detail: `${config.github.owner}/${config.github.repo}` });
|
|
1726
|
+
} else {
|
|
1727
|
+
configChecks.push({ name: "github.owner/repo", ok: false, fix: "Edit kody.config.json: set github.owner and github.repo" });
|
|
1728
|
+
}
|
|
1729
|
+
if (config.git?.defaultBranch) {
|
|
1730
|
+
configChecks.push({ name: "git.defaultBranch", ok: true, detail: config.git.defaultBranch });
|
|
1731
|
+
}
|
|
1732
|
+
if (config.quality?.testUnit) {
|
|
1733
|
+
configChecks.push({ name: "quality.testUnit", ok: true, detail: config.quality.testUnit });
|
|
1734
|
+
} else {
|
|
1735
|
+
configChecks.push({ name: "quality.testUnit", ok: false, fix: "Edit kody.config.json: set quality.testUnit command" });
|
|
1736
|
+
}
|
|
1737
|
+
for (const c of configChecks) {
|
|
1738
|
+
if (c.ok) {
|
|
1739
|
+
console.log(` \u2713 ${c.name}${c.detail ? `: ${c.detail}` : ""}`);
|
|
1740
|
+
} else {
|
|
1741
|
+
console.log(` \u2717 ${c.name} \u2014 ${c.fix}`);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
} catch {
|
|
1745
|
+
console.log(" \u2717 kody.config.json \u2014 invalid JSON");
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
console.log("\n\u2500\u2500 Architecture Detection \u2500\u2500");
|
|
1749
|
+
const memoryDir = path6.join(cwd, ".kody", "memory");
|
|
1750
|
+
const archPath = path6.join(memoryDir, "architecture.md");
|
|
1751
|
+
if (fs7.existsSync(archPath)) {
|
|
1752
|
+
console.log(" \u25CB .kody/memory/architecture.md (exists, not overwriting)");
|
|
1753
|
+
} else {
|
|
1754
|
+
const archItems = detectArchitecture(cwd);
|
|
1755
|
+
if (archItems.length > 0) {
|
|
1756
|
+
fs7.mkdirSync(memoryDir, { recursive: true });
|
|
1757
|
+
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1758
|
+
const content = `# Architecture (auto-detected ${timestamp2})
|
|
1759
|
+
|
|
1760
|
+
## Overview
|
|
1761
|
+
${archItems.join("\n")}
|
|
1762
|
+
`;
|
|
1763
|
+
fs7.writeFileSync(archPath, content);
|
|
1764
|
+
console.log(` \u2713 .kody/memory/architecture.md (${archItems.length} items detected)`);
|
|
1765
|
+
for (const item of archItems) {
|
|
1766
|
+
console.log(` ${item}`);
|
|
1767
|
+
}
|
|
1768
|
+
} else {
|
|
1769
|
+
console.log(" \u25CB No architecture detected");
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
const conventionsPath = path6.join(memoryDir, "conventions.md");
|
|
1773
|
+
if (!fs7.existsSync(conventionsPath)) {
|
|
1774
|
+
fs7.mkdirSync(memoryDir, { recursive: true });
|
|
1775
|
+
fs7.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
|
|
1776
|
+
console.log(" \u2713 .kody/memory/conventions.md (seed)");
|
|
1777
|
+
}
|
|
1778
|
+
const allChecks = [...checks, ghAuth, ghRepo];
|
|
1779
|
+
const failed = allChecks.filter((c) => !c.ok);
|
|
1780
|
+
console.log("\n\u2500\u2500 Summary \u2500\u2500");
|
|
1781
|
+
if (failed.length === 0) {
|
|
1782
|
+
console.log(" \u2713 All checks passed! Ready to use.");
|
|
1783
|
+
console.log("\n Next: Comment '@kody full <task-id>' on a GitHub issue");
|
|
1784
|
+
} else {
|
|
1785
|
+
console.log(` \u26A0 ${failed.length} issue(s) to fix:`);
|
|
1786
|
+
for (const c of failed) {
|
|
1787
|
+
console.log(` \u2022 ${c.name}: ${c.fix}`);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
console.log("");
|
|
1791
|
+
}
|
|
1792
|
+
var args = process.argv.slice(2);
|
|
1793
|
+
var command = args[0];
|
|
1794
|
+
if (command === "init") {
|
|
1795
|
+
initCommand({ force: args.includes("--force") });
|
|
1796
|
+
} else if (command === "version" || command === "--version" || command === "-v") {
|
|
1797
|
+
console.log(getVersion());
|
|
1798
|
+
} else {
|
|
1799
|
+
Promise.resolve().then(() => init_entry());
|
|
1800
|
+
}
|