@kody-ade/kody-engine-lite 0.1.20 → 0.1.22
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/README.md +167 -298
- package/dist/bin/cli.js +1280 -827
- package/kody.config.schema.json +170 -0
- package/package.json +3 -2
package/dist/bin/cli.js
CHANGED
|
@@ -107,26 +107,6 @@ function createClaudeCodeRunner() {
|
|
|
107
107
|
}
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
|
-
function createOpenCodeRunner() {
|
|
111
|
-
return {
|
|
112
|
-
async run(stageName, prompt, model, timeout, _taskDir, options) {
|
|
113
|
-
const args2 = ["run", "--agent", "build"];
|
|
114
|
-
if (model) {
|
|
115
|
-
args2.push("--model", model);
|
|
116
|
-
}
|
|
117
|
-
return runSubprocess(
|
|
118
|
-
"opencode",
|
|
119
|
-
args2,
|
|
120
|
-
prompt,
|
|
121
|
-
timeout,
|
|
122
|
-
options
|
|
123
|
-
);
|
|
124
|
-
},
|
|
125
|
-
async healthCheck() {
|
|
126
|
-
return checkCommand("opencode", ["--version"]);
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
110
|
function createRunners(config) {
|
|
131
111
|
if (config.agent.runners && Object.keys(config.agent.runners).length > 0) {
|
|
132
112
|
const runners = {};
|
|
@@ -150,8 +130,7 @@ var init_agent_runner = __esm({
|
|
|
150
130
|
SIGKILL_GRACE_MS = 5e3;
|
|
151
131
|
STDERR_TAIL_CHARS = 500;
|
|
152
132
|
RUNNER_FACTORIES = {
|
|
153
|
-
"claude-code": createClaudeCodeRunner
|
|
154
|
-
"opencode": createOpenCodeRunner
|
|
133
|
+
"claude-code": createClaudeCodeRunner
|
|
155
134
|
};
|
|
156
135
|
}
|
|
157
136
|
});
|
|
@@ -220,251 +199,6 @@ var init_definitions = __esm({
|
|
|
220
199
|
}
|
|
221
200
|
});
|
|
222
201
|
|
|
223
|
-
// src/memory.ts
|
|
224
|
-
import * as fs from "fs";
|
|
225
|
-
import * as path from "path";
|
|
226
|
-
function readProjectMemory(projectDir) {
|
|
227
|
-
const memoryDir = path.join(projectDir, ".kody", "memory");
|
|
228
|
-
if (!fs.existsSync(memoryDir)) return "";
|
|
229
|
-
const files = fs.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
230
|
-
if (files.length === 0) return "";
|
|
231
|
-
const sections = [];
|
|
232
|
-
for (const file of files) {
|
|
233
|
-
const content = fs.readFileSync(path.join(memoryDir, file), "utf-8").trim();
|
|
234
|
-
if (content) {
|
|
235
|
-
sections.push(`## ${file.replace(".md", "")}
|
|
236
|
-
${content}`);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
if (sections.length === 0) return "";
|
|
240
|
-
return `# Project Memory
|
|
241
|
-
|
|
242
|
-
${sections.join("\n\n")}
|
|
243
|
-
`;
|
|
244
|
-
}
|
|
245
|
-
var init_memory = __esm({
|
|
246
|
-
"src/memory.ts"() {
|
|
247
|
-
"use strict";
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
// src/config.ts
|
|
252
|
-
import * as fs2 from "fs";
|
|
253
|
-
import * as path2 from "path";
|
|
254
|
-
function setConfigDir(dir) {
|
|
255
|
-
_configDir = dir;
|
|
256
|
-
_config = null;
|
|
257
|
-
}
|
|
258
|
-
function getProjectConfig() {
|
|
259
|
-
if (_config) return _config;
|
|
260
|
-
const configPath = path2.join(_configDir ?? process.cwd(), "kody.config.json");
|
|
261
|
-
if (fs2.existsSync(configPath)) {
|
|
262
|
-
try {
|
|
263
|
-
const raw = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
|
|
264
|
-
_config = {
|
|
265
|
-
quality: { ...DEFAULT_CONFIG.quality, ...raw.quality },
|
|
266
|
-
git: { ...DEFAULT_CONFIG.git, ...raw.git },
|
|
267
|
-
github: { ...DEFAULT_CONFIG.github, ...raw.github },
|
|
268
|
-
paths: { ...DEFAULT_CONFIG.paths, ...raw.paths },
|
|
269
|
-
agent: { ...DEFAULT_CONFIG.agent, ...raw.agent }
|
|
270
|
-
};
|
|
271
|
-
} catch {
|
|
272
|
-
_config = { ...DEFAULT_CONFIG };
|
|
273
|
-
}
|
|
274
|
-
} else {
|
|
275
|
-
_config = { ...DEFAULT_CONFIG };
|
|
276
|
-
}
|
|
277
|
-
return _config;
|
|
278
|
-
}
|
|
279
|
-
var DEFAULT_CONFIG, VERIFY_COMMAND_TIMEOUT_MS, FIX_COMMAND_TIMEOUT_MS, _config, _configDir;
|
|
280
|
-
var init_config = __esm({
|
|
281
|
-
"src/config.ts"() {
|
|
282
|
-
"use strict";
|
|
283
|
-
DEFAULT_CONFIG = {
|
|
284
|
-
quality: {
|
|
285
|
-
typecheck: "pnpm -s tsc --noEmit",
|
|
286
|
-
lint: "pnpm -s lint",
|
|
287
|
-
lintFix: "pnpm lint:fix",
|
|
288
|
-
format: "pnpm -s format:check",
|
|
289
|
-
formatFix: "pnpm format:fix",
|
|
290
|
-
testUnit: "pnpm -s test"
|
|
291
|
-
},
|
|
292
|
-
git: {
|
|
293
|
-
defaultBranch: "dev"
|
|
294
|
-
},
|
|
295
|
-
github: {
|
|
296
|
-
owner: "",
|
|
297
|
-
repo: ""
|
|
298
|
-
},
|
|
299
|
-
paths: {
|
|
300
|
-
taskDir: ".tasks"
|
|
301
|
-
},
|
|
302
|
-
agent: {
|
|
303
|
-
runner: "claude-code",
|
|
304
|
-
defaultRunner: "claude",
|
|
305
|
-
modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
|
|
306
|
-
}
|
|
307
|
-
};
|
|
308
|
-
VERIFY_COMMAND_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
309
|
-
FIX_COMMAND_TIMEOUT_MS = 2 * 60 * 1e3;
|
|
310
|
-
_config = null;
|
|
311
|
-
_configDir = null;
|
|
312
|
-
}
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
// src/context.ts
|
|
316
|
-
import * as fs3 from "fs";
|
|
317
|
-
import * as path3 from "path";
|
|
318
|
-
function readPromptFile(stageName) {
|
|
319
|
-
const scriptDir = new URL(".", import.meta.url).pathname;
|
|
320
|
-
const candidates = [
|
|
321
|
-
path3.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
|
|
322
|
-
path3.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
|
|
323
|
-
];
|
|
324
|
-
for (const candidate of candidates) {
|
|
325
|
-
if (fs3.existsSync(candidate)) {
|
|
326
|
-
return fs3.readFileSync(candidate, "utf-8");
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
|
|
330
|
-
}
|
|
331
|
-
function injectTaskContext(prompt, taskId, taskDir, feedback) {
|
|
332
|
-
let context = `## Task Context
|
|
333
|
-
`;
|
|
334
|
-
context += `Task ID: ${taskId}
|
|
335
|
-
`;
|
|
336
|
-
context += `Task Directory: ${taskDir}
|
|
337
|
-
`;
|
|
338
|
-
const taskMdPath = path3.join(taskDir, "task.md");
|
|
339
|
-
if (fs3.existsSync(taskMdPath)) {
|
|
340
|
-
const taskMd = fs3.readFileSync(taskMdPath, "utf-8");
|
|
341
|
-
context += `
|
|
342
|
-
## Task Description
|
|
343
|
-
${taskMd}
|
|
344
|
-
`;
|
|
345
|
-
}
|
|
346
|
-
const taskJsonPath = path3.join(taskDir, "task.json");
|
|
347
|
-
if (fs3.existsSync(taskJsonPath)) {
|
|
348
|
-
try {
|
|
349
|
-
const taskDef = JSON.parse(fs3.readFileSync(taskJsonPath, "utf-8"));
|
|
350
|
-
context += `
|
|
351
|
-
## Task Classification
|
|
352
|
-
`;
|
|
353
|
-
context += `Type: ${taskDef.task_type ?? "unknown"}
|
|
354
|
-
`;
|
|
355
|
-
context += `Title: ${taskDef.title ?? "unknown"}
|
|
356
|
-
`;
|
|
357
|
-
context += `Risk: ${taskDef.risk_level ?? "unknown"}
|
|
358
|
-
`;
|
|
359
|
-
} catch {
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
const specPath = path3.join(taskDir, "spec.md");
|
|
363
|
-
if (fs3.existsSync(specPath)) {
|
|
364
|
-
const spec = fs3.readFileSync(specPath, "utf-8");
|
|
365
|
-
const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
|
|
366
|
-
context += `
|
|
367
|
-
## Spec Summary
|
|
368
|
-
${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
|
|
369
|
-
`;
|
|
370
|
-
}
|
|
371
|
-
const planPath = path3.join(taskDir, "plan.md");
|
|
372
|
-
if (fs3.existsSync(planPath)) {
|
|
373
|
-
const plan = fs3.readFileSync(planPath, "utf-8");
|
|
374
|
-
const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
|
|
375
|
-
context += `
|
|
376
|
-
## Plan Summary
|
|
377
|
-
${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
|
|
378
|
-
`;
|
|
379
|
-
}
|
|
380
|
-
if (feedback) {
|
|
381
|
-
context += `
|
|
382
|
-
## Human Feedback
|
|
383
|
-
${feedback}
|
|
384
|
-
`;
|
|
385
|
-
}
|
|
386
|
-
return prompt.replace("{{TASK_CONTEXT}}", context);
|
|
387
|
-
}
|
|
388
|
-
function buildFullPrompt(stageName, taskId, taskDir, projectDir, feedback) {
|
|
389
|
-
const memory = readProjectMemory(projectDir);
|
|
390
|
-
const promptTemplate = readPromptFile(stageName);
|
|
391
|
-
const prompt = injectTaskContext(promptTemplate, taskId, taskDir, feedback);
|
|
392
|
-
return memory ? `${memory}
|
|
393
|
-
---
|
|
394
|
-
|
|
395
|
-
${prompt}` : prompt;
|
|
396
|
-
}
|
|
397
|
-
function resolveModel(modelTier, stageName) {
|
|
398
|
-
const config = getProjectConfig();
|
|
399
|
-
if (config.agent.usePerStageRouting && stageName) {
|
|
400
|
-
return stageName;
|
|
401
|
-
}
|
|
402
|
-
const mapped = config.agent.modelMap[modelTier];
|
|
403
|
-
if (mapped) return mapped;
|
|
404
|
-
return DEFAULT_MODEL_MAP[modelTier] ?? "sonnet";
|
|
405
|
-
}
|
|
406
|
-
var DEFAULT_MODEL_MAP, MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC;
|
|
407
|
-
var init_context = __esm({
|
|
408
|
-
"src/context.ts"() {
|
|
409
|
-
"use strict";
|
|
410
|
-
init_memory();
|
|
411
|
-
init_config();
|
|
412
|
-
DEFAULT_MODEL_MAP = {
|
|
413
|
-
cheap: "haiku",
|
|
414
|
-
mid: "sonnet",
|
|
415
|
-
strong: "opus"
|
|
416
|
-
};
|
|
417
|
-
MAX_TASK_CONTEXT_PLAN = 1500;
|
|
418
|
-
MAX_TASK_CONTEXT_SPEC = 2e3;
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
// src/validators.ts
|
|
423
|
-
function validateTaskJson(content) {
|
|
424
|
-
try {
|
|
425
|
-
const parsed = JSON.parse(content);
|
|
426
|
-
for (const field of REQUIRED_TASK_FIELDS) {
|
|
427
|
-
if (!(field in parsed)) {
|
|
428
|
-
return { valid: false, error: `Missing field: ${field}` };
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
return { valid: true };
|
|
432
|
-
} catch (err) {
|
|
433
|
-
return {
|
|
434
|
-
valid: false,
|
|
435
|
-
error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
function validatePlanMd(content) {
|
|
440
|
-
if (content.length < 10) {
|
|
441
|
-
return { valid: false, error: "Plan is too short (< 10 chars)" };
|
|
442
|
-
}
|
|
443
|
-
if (!/^##\s+\w+/m.test(content)) {
|
|
444
|
-
return { valid: false, error: "Plan has no markdown h2 sections" };
|
|
445
|
-
}
|
|
446
|
-
return { valid: true };
|
|
447
|
-
}
|
|
448
|
-
function validateReviewMd(content) {
|
|
449
|
-
if (/pass/i.test(content) || /fail/i.test(content)) {
|
|
450
|
-
return { valid: true };
|
|
451
|
-
}
|
|
452
|
-
return { valid: false, error: "Review must contain 'pass' or 'fail'" };
|
|
453
|
-
}
|
|
454
|
-
var REQUIRED_TASK_FIELDS;
|
|
455
|
-
var init_validators = __esm({
|
|
456
|
-
"src/validators.ts"() {
|
|
457
|
-
"use strict";
|
|
458
|
-
REQUIRED_TASK_FIELDS = [
|
|
459
|
-
"task_type",
|
|
460
|
-
"title",
|
|
461
|
-
"description",
|
|
462
|
-
"scope",
|
|
463
|
-
"risk_level"
|
|
464
|
-
];
|
|
465
|
-
}
|
|
466
|
-
});
|
|
467
|
-
|
|
468
202
|
// src/logger.ts
|
|
469
203
|
function getLevel() {
|
|
470
204
|
const env = process.env.LOG_LEVEL;
|
|
@@ -552,11 +286,21 @@ function getCurrentBranch(cwd) {
|
|
|
552
286
|
}
|
|
553
287
|
function ensureFeatureBranch(issueNumber, title, cwd) {
|
|
554
288
|
const current = getCurrentBranch(cwd);
|
|
555
|
-
|
|
289
|
+
const branchName = deriveBranchName(issueNumber, title);
|
|
290
|
+
if (current === branchName || current.startsWith(`${issueNumber}-`)) {
|
|
556
291
|
logger.info(` Already on feature branch: ${current}`);
|
|
557
292
|
return current;
|
|
558
293
|
}
|
|
559
|
-
|
|
294
|
+
if (!BASE_BRANCHES.includes(current) && current !== "") {
|
|
295
|
+
const defaultBranch2 = getDefaultBranch(cwd);
|
|
296
|
+
logger.info(` Switching from ${current} to ${defaultBranch2} before creating ${branchName}`);
|
|
297
|
+
try {
|
|
298
|
+
git(["checkout", defaultBranch2], { cwd });
|
|
299
|
+
} catch {
|
|
300
|
+
logger.warn(` Failed to checkout ${defaultBranch2}, aborting branch creation`);
|
|
301
|
+
return current;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
560
304
|
try {
|
|
561
305
|
git(["fetch", "origin"], { cwd, timeout: 3e4 });
|
|
562
306
|
} catch {
|
|
@@ -612,7 +356,7 @@ function commitAll(message, cwd) {
|
|
|
612
356
|
if (!status) {
|
|
613
357
|
return { success: false, hash: "", message: "No changes to commit" };
|
|
614
358
|
}
|
|
615
|
-
git(["add", "
|
|
359
|
+
git(["add", "."], { cwd });
|
|
616
360
|
git(["commit", "--no-gpg-sign", "-m", message], { cwd });
|
|
617
361
|
const hash = git(["rev-parse", "HEAD"], { cwd }).slice(0, 7);
|
|
618
362
|
logger.info(` Committed: ${hash} ${message}`);
|
|
@@ -737,130 +481,314 @@ var init_github_api = __esm({
|
|
|
737
481
|
}
|
|
738
482
|
});
|
|
739
483
|
|
|
740
|
-
// src/
|
|
741
|
-
import
|
|
742
|
-
|
|
743
|
-
|
|
484
|
+
// src/pipeline/state.ts
|
|
485
|
+
import * as fs from "fs";
|
|
486
|
+
import * as path from "path";
|
|
487
|
+
function loadState(taskId, taskDir) {
|
|
488
|
+
const p = path.join(taskDir, "status.json");
|
|
489
|
+
if (!fs.existsSync(p)) return null;
|
|
744
490
|
try {
|
|
745
|
-
const
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
env: { ...process.env, FORCE_COLOR: "0" }
|
|
751
|
-
});
|
|
752
|
-
return { success: true, output: output ?? "", timedOut: false };
|
|
753
|
-
} catch (err) {
|
|
754
|
-
const e = err;
|
|
755
|
-
const output = `${e.stdout ?? ""}${e.stderr ?? ""}`;
|
|
756
|
-
return { success: false, output, timedOut: !!e.killed };
|
|
491
|
+
const raw = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
492
|
+
if (raw.taskId === taskId) return raw;
|
|
493
|
+
return null;
|
|
494
|
+
} catch {
|
|
495
|
+
return null;
|
|
757
496
|
}
|
|
758
497
|
}
|
|
759
|
-
function
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
498
|
+
function writeState(state, taskDir) {
|
|
499
|
+
const updated = {
|
|
500
|
+
...state,
|
|
501
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
502
|
+
};
|
|
503
|
+
const target = path.join(taskDir, "status.json");
|
|
504
|
+
const tmp = target + ".tmp";
|
|
505
|
+
fs.writeFileSync(tmp, JSON.stringify(updated, null, 2));
|
|
506
|
+
fs.renameSync(tmp, target);
|
|
507
|
+
state.updatedAt = updated.updatedAt;
|
|
508
|
+
}
|
|
509
|
+
function initState(taskId) {
|
|
510
|
+
const stages = {};
|
|
511
|
+
for (const stage of STAGES) {
|
|
512
|
+
stages[stage.name] = { state: "pending", retries: 0 };
|
|
765
513
|
}
|
|
766
|
-
|
|
514
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
515
|
+
return { taskId, state: "running", stages, createdAt: now, updatedAt: now };
|
|
767
516
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
517
|
+
var init_state = __esm({
|
|
518
|
+
"src/pipeline/state.ts"() {
|
|
519
|
+
"use strict";
|
|
520
|
+
init_definitions();
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// src/pipeline/complexity.ts
|
|
525
|
+
function filterByComplexity(stages, complexity) {
|
|
526
|
+
const skip = COMPLEXITY_SKIP[complexity] ?? [];
|
|
527
|
+
return stages.filter((s) => !skip.includes(s.name));
|
|
772
528
|
}
|
|
773
|
-
function
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
529
|
+
function isValidComplexity(value) {
|
|
530
|
+
return value in COMPLEXITY_SKIP;
|
|
531
|
+
}
|
|
532
|
+
var COMPLEXITY_SKIP;
|
|
533
|
+
var init_complexity = __esm({
|
|
534
|
+
"src/pipeline/complexity.ts"() {
|
|
535
|
+
"use strict";
|
|
536
|
+
COMPLEXITY_SKIP = {
|
|
537
|
+
low: ["plan", "review", "review-fix"],
|
|
538
|
+
medium: ["review-fix"],
|
|
539
|
+
high: []
|
|
540
|
+
};
|
|
785
541
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// src/memory.ts
|
|
545
|
+
import * as fs2 from "fs";
|
|
546
|
+
import * as path2 from "path";
|
|
547
|
+
function readProjectMemory(projectDir) {
|
|
548
|
+
const memoryDir = path2.join(projectDir, ".kody", "memory");
|
|
549
|
+
if (!fs2.existsSync(memoryDir)) return "";
|
|
550
|
+
const files = fs2.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
551
|
+
if (files.length === 0) return "";
|
|
552
|
+
const sections = [];
|
|
553
|
+
for (const file of files) {
|
|
554
|
+
const content = fs2.readFileSync(path2.join(memoryDir, file), "utf-8").trim();
|
|
555
|
+
if (content) {
|
|
556
|
+
sections.push(`## ${file.replace(".md", "")}
|
|
557
|
+
${content}`);
|
|
794
558
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
559
|
+
}
|
|
560
|
+
if (sections.length === 0) return "";
|
|
561
|
+
return `# Project Memory
|
|
562
|
+
|
|
563
|
+
${sections.join("\n\n")}
|
|
564
|
+
`;
|
|
565
|
+
}
|
|
566
|
+
var init_memory = __esm({
|
|
567
|
+
"src/memory.ts"() {
|
|
568
|
+
"use strict";
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// src/config.ts
|
|
573
|
+
import * as fs3 from "fs";
|
|
574
|
+
import * as path3 from "path";
|
|
575
|
+
function setConfigDir(dir) {
|
|
576
|
+
_configDir = dir;
|
|
577
|
+
_config = null;
|
|
578
|
+
}
|
|
579
|
+
function getProjectConfig() {
|
|
580
|
+
if (_config) return _config;
|
|
581
|
+
const configPath = path3.join(_configDir ?? process.cwd(), "kody.config.json");
|
|
582
|
+
if (fs3.existsSync(configPath)) {
|
|
583
|
+
try {
|
|
584
|
+
const raw = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
|
|
585
|
+
_config = {
|
|
586
|
+
quality: { ...DEFAULT_CONFIG.quality, ...raw.quality },
|
|
587
|
+
git: { ...DEFAULT_CONFIG.git, ...raw.git },
|
|
588
|
+
github: { ...DEFAULT_CONFIG.github, ...raw.github },
|
|
589
|
+
paths: { ...DEFAULT_CONFIG.paths, ...raw.paths },
|
|
590
|
+
agent: { ...DEFAULT_CONFIG.agent, ...raw.agent }
|
|
591
|
+
};
|
|
592
|
+
} catch {
|
|
593
|
+
logger.warn("kody.config.json is invalid JSON \u2014 using defaults");
|
|
594
|
+
_config = { ...DEFAULT_CONFIG };
|
|
799
595
|
}
|
|
800
|
-
|
|
596
|
+
} else {
|
|
597
|
+
_config = { ...DEFAULT_CONFIG };
|
|
801
598
|
}
|
|
802
|
-
return
|
|
599
|
+
return _config;
|
|
803
600
|
}
|
|
804
|
-
var
|
|
805
|
-
|
|
601
|
+
var DEFAULT_CONFIG, VERIFY_COMMAND_TIMEOUT_MS, FIX_COMMAND_TIMEOUT_MS, _config, _configDir;
|
|
602
|
+
var init_config = __esm({
|
|
603
|
+
"src/config.ts"() {
|
|
806
604
|
"use strict";
|
|
807
|
-
init_config();
|
|
808
605
|
init_logger();
|
|
606
|
+
DEFAULT_CONFIG = {
|
|
607
|
+
quality: {
|
|
608
|
+
typecheck: "pnpm -s tsc --noEmit",
|
|
609
|
+
lint: "pnpm -s lint",
|
|
610
|
+
lintFix: "pnpm lint:fix",
|
|
611
|
+
format: "pnpm -s format:check",
|
|
612
|
+
formatFix: "pnpm format:fix",
|
|
613
|
+
testUnit: "pnpm -s test"
|
|
614
|
+
},
|
|
615
|
+
git: {
|
|
616
|
+
defaultBranch: "dev"
|
|
617
|
+
},
|
|
618
|
+
github: {
|
|
619
|
+
owner: "",
|
|
620
|
+
repo: ""
|
|
621
|
+
},
|
|
622
|
+
paths: {
|
|
623
|
+
taskDir: ".tasks"
|
|
624
|
+
},
|
|
625
|
+
agent: {
|
|
626
|
+
runner: "claude-code",
|
|
627
|
+
defaultRunner: "claude",
|
|
628
|
+
modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
VERIFY_COMMAND_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
632
|
+
FIX_COMMAND_TIMEOUT_MS = 2 * 60 * 1e3;
|
|
633
|
+
_config = null;
|
|
634
|
+
_configDir = null;
|
|
809
635
|
}
|
|
810
636
|
});
|
|
811
637
|
|
|
812
|
-
// src/
|
|
638
|
+
// src/context.ts
|
|
813
639
|
import * as fs4 from "fs";
|
|
814
640
|
import * as path4 from "path";
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
const
|
|
818
|
-
|
|
641
|
+
function readPromptFile(stageName) {
|
|
642
|
+
const scriptDir = new URL(".", import.meta.url).pathname;
|
|
643
|
+
const candidates = [
|
|
644
|
+
path4.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
|
|
645
|
+
path4.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
|
|
646
|
+
];
|
|
647
|
+
for (const candidate of candidates) {
|
|
648
|
+
if (fs4.existsSync(candidate)) {
|
|
649
|
+
return fs4.readFileSync(candidate, "utf-8");
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
|
|
819
653
|
}
|
|
820
|
-
function
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
${
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
654
|
+
function injectTaskContext(prompt, taskId, taskDir, feedback) {
|
|
655
|
+
let context = `## Task Context
|
|
656
|
+
`;
|
|
657
|
+
context += `Task ID: ${taskId}
|
|
658
|
+
`;
|
|
659
|
+
context += `Task Directory: ${taskDir}
|
|
660
|
+
`;
|
|
661
|
+
const taskMdPath = path4.join(taskDir, "task.md");
|
|
662
|
+
if (fs4.existsSync(taskMdPath)) {
|
|
663
|
+
const taskMd = fs4.readFileSync(taskMdPath, "utf-8");
|
|
664
|
+
context += `
|
|
665
|
+
## Task Description
|
|
666
|
+
${taskMd}
|
|
667
|
+
`;
|
|
668
|
+
}
|
|
669
|
+
const taskJsonPath = path4.join(taskDir, "task.json");
|
|
670
|
+
if (fs4.existsSync(taskJsonPath)) {
|
|
671
|
+
try {
|
|
672
|
+
const taskDef = JSON.parse(fs4.readFileSync(taskJsonPath, "utf-8"));
|
|
673
|
+
context += `
|
|
674
|
+
## Task Classification
|
|
675
|
+
`;
|
|
676
|
+
context += `Type: ${taskDef.task_type ?? "unknown"}
|
|
677
|
+
`;
|
|
678
|
+
context += `Title: ${taskDef.title ?? "unknown"}
|
|
679
|
+
`;
|
|
680
|
+
context += `Risk: ${taskDef.risk_level ?? "unknown"}
|
|
681
|
+
`;
|
|
682
|
+
} catch {
|
|
839
683
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
684
|
+
}
|
|
685
|
+
const specPath = path4.join(taskDir, "spec.md");
|
|
686
|
+
if (fs4.existsSync(specPath)) {
|
|
687
|
+
const spec = fs4.readFileSync(specPath, "utf-8");
|
|
688
|
+
const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
|
|
689
|
+
context += `
|
|
690
|
+
## Spec Summary
|
|
691
|
+
${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
|
|
692
|
+
`;
|
|
693
|
+
}
|
|
694
|
+
const planPath = path4.join(taskDir, "plan.md");
|
|
695
|
+
if (fs4.existsSync(planPath)) {
|
|
696
|
+
const plan = fs4.readFileSync(planPath, "utf-8");
|
|
697
|
+
const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
|
|
698
|
+
context += `
|
|
699
|
+
## Plan Summary
|
|
700
|
+
${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
|
|
701
|
+
`;
|
|
702
|
+
}
|
|
703
|
+
if (feedback) {
|
|
704
|
+
context += `
|
|
705
|
+
## Human Feedback
|
|
706
|
+
${feedback}
|
|
707
|
+
`;
|
|
708
|
+
}
|
|
709
|
+
return prompt.replace("{{TASK_CONTEXT}}", context);
|
|
710
|
+
}
|
|
711
|
+
function buildFullPrompt(stageName, taskId, taskDir, projectDir, feedback) {
|
|
712
|
+
const memory = readProjectMemory(projectDir);
|
|
713
|
+
const promptTemplate = readPromptFile(stageName);
|
|
714
|
+
const prompt = injectTaskContext(promptTemplate, taskId, taskDir, feedback);
|
|
715
|
+
return memory ? `${memory}
|
|
716
|
+
---
|
|
850
717
|
|
|
851
|
-
${
|
|
718
|
+
${prompt}` : prompt;
|
|
719
|
+
}
|
|
720
|
+
function resolveModel(modelTier, stageName) {
|
|
721
|
+
const config = getProjectConfig();
|
|
722
|
+
if (config.agent.usePerStageRouting && stageName) {
|
|
723
|
+
return stageName;
|
|
724
|
+
}
|
|
725
|
+
const mapped = config.agent.modelMap[modelTier];
|
|
726
|
+
if (mapped) return mapped;
|
|
727
|
+
return DEFAULT_MODEL_MAP[modelTier] ?? "sonnet";
|
|
728
|
+
}
|
|
729
|
+
var DEFAULT_MODEL_MAP, MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC;
|
|
730
|
+
var init_context = __esm({
|
|
731
|
+
"src/context.ts"() {
|
|
732
|
+
"use strict";
|
|
733
|
+
init_memory();
|
|
734
|
+
init_config();
|
|
735
|
+
DEFAULT_MODEL_MAP = {
|
|
736
|
+
cheap: "haiku",
|
|
737
|
+
mid: "sonnet",
|
|
738
|
+
strong: "opus"
|
|
739
|
+
};
|
|
740
|
+
MAX_TASK_CONTEXT_PLAN = 1500;
|
|
741
|
+
MAX_TASK_CONTEXT_SPEC = 2e3;
|
|
742
|
+
}
|
|
743
|
+
});
|
|
852
744
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
745
|
+
// src/validators.ts
|
|
746
|
+
function validateTaskJson(content) {
|
|
747
|
+
try {
|
|
748
|
+
const parsed = JSON.parse(content);
|
|
749
|
+
for (const field of REQUIRED_TASK_FIELDS) {
|
|
750
|
+
if (!(field in parsed)) {
|
|
751
|
+
return { valid: false, error: `Missing field: ${field}` };
|
|
858
752
|
}
|
|
859
753
|
}
|
|
860
|
-
|
|
754
|
+
return { valid: true };
|
|
755
|
+
} catch (err) {
|
|
756
|
+
return {
|
|
757
|
+
valid: false,
|
|
758
|
+
error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
759
|
+
};
|
|
861
760
|
}
|
|
862
|
-
return false;
|
|
863
761
|
}
|
|
762
|
+
function validatePlanMd(content) {
|
|
763
|
+
if (content.length < 10) {
|
|
764
|
+
return { valid: false, error: "Plan is too short (< 10 chars)" };
|
|
765
|
+
}
|
|
766
|
+
if (!/^##\s+\w+/m.test(content)) {
|
|
767
|
+
return { valid: false, error: "Plan has no markdown h2 sections" };
|
|
768
|
+
}
|
|
769
|
+
return { valid: true };
|
|
770
|
+
}
|
|
771
|
+
function validateReviewMd(content) {
|
|
772
|
+
if (/pass/i.test(content) || /fail/i.test(content)) {
|
|
773
|
+
return { valid: true };
|
|
774
|
+
}
|
|
775
|
+
return { valid: false, error: "Review must contain 'pass' or 'fail'" };
|
|
776
|
+
}
|
|
777
|
+
var REQUIRED_TASK_FIELDS;
|
|
778
|
+
var init_validators = __esm({
|
|
779
|
+
"src/validators.ts"() {
|
|
780
|
+
"use strict";
|
|
781
|
+
REQUIRED_TASK_FIELDS = [
|
|
782
|
+
"task_type",
|
|
783
|
+
"title",
|
|
784
|
+
"description",
|
|
785
|
+
"scope",
|
|
786
|
+
"risk_level"
|
|
787
|
+
];
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
// src/pipeline/runner-selection.ts
|
|
864
792
|
function getRunnerForStage(ctx, stageName) {
|
|
865
793
|
const config = getProjectConfig();
|
|
866
794
|
const runnerName = config.agent.stageRunners?.[stageName] ?? config.agent.defaultRunner ?? Object.keys(ctx.runners)[0] ?? "claude";
|
|
@@ -872,32 +800,16 @@ function getRunnerForStage(ctx, stageName) {
|
|
|
872
800
|
}
|
|
873
801
|
return runner;
|
|
874
802
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
const raw = JSON.parse(fs4.readFileSync(p, "utf-8"));
|
|
880
|
-
if (raw.taskId === taskId) return raw;
|
|
881
|
-
return null;
|
|
882
|
-
} catch {
|
|
883
|
-
return null;
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
function writeState(state, taskDir) {
|
|
887
|
-
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
888
|
-
fs4.writeFileSync(
|
|
889
|
-
path4.join(taskDir, "status.json"),
|
|
890
|
-
JSON.stringify(state, null, 2)
|
|
891
|
-
);
|
|
892
|
-
}
|
|
893
|
-
function initState(taskId) {
|
|
894
|
-
const stages = {};
|
|
895
|
-
for (const stage of STAGES) {
|
|
896
|
-
stages[stage.name] = { state: "pending", retries: 0 };
|
|
803
|
+
var init_runner_selection = __esm({
|
|
804
|
+
"src/pipeline/runner-selection.ts"() {
|
|
805
|
+
"use strict";
|
|
806
|
+
init_config();
|
|
897
807
|
}
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
// src/stages/agent.ts
|
|
811
|
+
import * as fs5 from "fs";
|
|
812
|
+
import * as path5 from "path";
|
|
901
813
|
function validateStageOutput(stageName, content) {
|
|
902
814
|
switch (stageName) {
|
|
903
815
|
case "taskify":
|
|
@@ -933,35 +845,246 @@ async function executeAgentStage(ctx, def) {
|
|
|
933
845
|
return { outcome: result.outcome, error: result.error, retries: 0 };
|
|
934
846
|
}
|
|
935
847
|
if (def.outputFile && result.output) {
|
|
936
|
-
|
|
848
|
+
fs5.writeFileSync(path5.join(ctx.taskDir, def.outputFile), result.output);
|
|
937
849
|
}
|
|
938
850
|
if (def.outputFile) {
|
|
939
|
-
const outputPath =
|
|
940
|
-
if (!
|
|
941
|
-
const ext =
|
|
942
|
-
const base =
|
|
943
|
-
const files =
|
|
851
|
+
const outputPath = path5.join(ctx.taskDir, def.outputFile);
|
|
852
|
+
if (!fs5.existsSync(outputPath)) {
|
|
853
|
+
const ext = path5.extname(def.outputFile);
|
|
854
|
+
const base = path5.basename(def.outputFile, ext);
|
|
855
|
+
const files = fs5.readdirSync(ctx.taskDir);
|
|
944
856
|
const variant = files.find(
|
|
945
857
|
(f) => f.startsWith(base + "-") && f.endsWith(ext)
|
|
946
858
|
);
|
|
947
859
|
if (variant) {
|
|
948
|
-
|
|
860
|
+
fs5.renameSync(path5.join(ctx.taskDir, variant), outputPath);
|
|
949
861
|
logger.info(` Renamed variant ${variant} \u2192 ${def.outputFile}`);
|
|
950
862
|
}
|
|
951
863
|
}
|
|
952
864
|
}
|
|
953
865
|
if (def.outputFile) {
|
|
954
|
-
const outputPath =
|
|
955
|
-
if (
|
|
956
|
-
const content =
|
|
866
|
+
const outputPath = path5.join(ctx.taskDir, def.outputFile);
|
|
867
|
+
if (fs5.existsSync(outputPath)) {
|
|
868
|
+
const content = fs5.readFileSync(outputPath, "utf-8");
|
|
957
869
|
const validation = validateStageOutput(def.name, content);
|
|
958
870
|
if (!validation.valid) {
|
|
959
871
|
logger.warn(` validation warning: ${validation.error}`);
|
|
960
872
|
}
|
|
961
873
|
}
|
|
962
874
|
}
|
|
963
|
-
return { outcome: "completed", outputFile: def.outputFile, retries: 0 };
|
|
875
|
+
return { outcome: "completed", outputFile: def.outputFile, retries: 0 };
|
|
876
|
+
}
|
|
877
|
+
var init_agent = __esm({
|
|
878
|
+
"src/stages/agent.ts"() {
|
|
879
|
+
"use strict";
|
|
880
|
+
init_context();
|
|
881
|
+
init_validators();
|
|
882
|
+
init_config();
|
|
883
|
+
init_runner_selection();
|
|
884
|
+
init_logger();
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
// src/verify-runner.ts
|
|
889
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
890
|
+
function isExecError(err) {
|
|
891
|
+
return typeof err === "object" && err !== null;
|
|
892
|
+
}
|
|
893
|
+
function parseCommand(cmd) {
|
|
894
|
+
const parts = [];
|
|
895
|
+
let current = "";
|
|
896
|
+
let inQuote = null;
|
|
897
|
+
for (const ch of cmd) {
|
|
898
|
+
if (inQuote) {
|
|
899
|
+
if (ch === inQuote) {
|
|
900
|
+
inQuote = null;
|
|
901
|
+
} else {
|
|
902
|
+
current += ch;
|
|
903
|
+
}
|
|
904
|
+
} else if (ch === '"' || ch === "'") {
|
|
905
|
+
inQuote = ch;
|
|
906
|
+
} else if (/\s/.test(ch)) {
|
|
907
|
+
if (current) {
|
|
908
|
+
parts.push(current);
|
|
909
|
+
current = "";
|
|
910
|
+
}
|
|
911
|
+
} else {
|
|
912
|
+
current += ch;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
if (current) parts.push(current);
|
|
916
|
+
if (inQuote) logger.warn(`Unclosed quote in command: ${cmd}`);
|
|
917
|
+
return parts;
|
|
918
|
+
}
|
|
919
|
+
function runCommand(cmd, cwd, timeout) {
|
|
920
|
+
const parts = parseCommand(cmd);
|
|
921
|
+
if (parts.length === 0) {
|
|
922
|
+
return { success: true, output: "", timedOut: false };
|
|
923
|
+
}
|
|
924
|
+
try {
|
|
925
|
+
const output = execFileSync4(parts[0], parts.slice(1), {
|
|
926
|
+
cwd,
|
|
927
|
+
timeout,
|
|
928
|
+
encoding: "utf-8",
|
|
929
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
930
|
+
env: { ...process.env, FORCE_COLOR: "0" }
|
|
931
|
+
});
|
|
932
|
+
return { success: true, output: output ?? "", timedOut: false };
|
|
933
|
+
} catch (err) {
|
|
934
|
+
const stdout = isExecError(err) ? err.stdout ?? "" : "";
|
|
935
|
+
const stderr = isExecError(err) ? err.stderr ?? "" : "";
|
|
936
|
+
const killed = isExecError(err) ? !!err.killed : false;
|
|
937
|
+
return { success: false, output: `${stdout}${stderr}`, timedOut: killed };
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
function parseErrors(output) {
|
|
941
|
+
const errors = [];
|
|
942
|
+
for (const line of output.split("\n")) {
|
|
943
|
+
if (/error|Error|ERROR|failed|Failed|FAIL|warning:|Warning:|WARN/i.test(line)) {
|
|
944
|
+
errors.push(line.slice(0, 500));
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return errors;
|
|
948
|
+
}
|
|
949
|
+
function extractSummary(output, cmdName) {
|
|
950
|
+
const summaryPatterns = /Test Suites|Tests|Coverage|ERRORS|FAILURES|success|completed/i;
|
|
951
|
+
const lines = output.split("\n").filter((l) => summaryPatterns.test(l));
|
|
952
|
+
return lines.slice(-3).map((l) => `[${cmdName}] ${l.trim()}`);
|
|
953
|
+
}
|
|
954
|
+
function runQualityGates(taskDir, projectRoot) {
|
|
955
|
+
const config = getProjectConfig();
|
|
956
|
+
const cwd = projectRoot ?? process.cwd();
|
|
957
|
+
const allErrors = [];
|
|
958
|
+
const allSummary = [];
|
|
959
|
+
let allPass = true;
|
|
960
|
+
const commands = [
|
|
961
|
+
{ name: "typecheck", cmd: config.quality.typecheck },
|
|
962
|
+
{ name: "test", cmd: config.quality.testUnit }
|
|
963
|
+
];
|
|
964
|
+
if (config.quality.lint) {
|
|
965
|
+
commands.push({ name: "lint", cmd: config.quality.lint });
|
|
966
|
+
}
|
|
967
|
+
for (const { name, cmd } of commands) {
|
|
968
|
+
if (!cmd) continue;
|
|
969
|
+
logger.info(` Running ${name}: ${cmd}`);
|
|
970
|
+
const result = runCommand(cmd, cwd, VERIFY_COMMAND_TIMEOUT_MS);
|
|
971
|
+
if (result.timedOut) {
|
|
972
|
+
allErrors.push(`${name}: timed out after ${VERIFY_COMMAND_TIMEOUT_MS / 1e3}s`);
|
|
973
|
+
allPass = false;
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
if (!result.success) {
|
|
977
|
+
allPass = false;
|
|
978
|
+
const errors = parseErrors(result.output);
|
|
979
|
+
allErrors.push(...errors.map((e) => `[${name}] ${e}`));
|
|
980
|
+
}
|
|
981
|
+
allSummary.push(...extractSummary(result.output, name));
|
|
982
|
+
}
|
|
983
|
+
return { pass: allPass, errors: allErrors, summary: allSummary };
|
|
984
|
+
}
|
|
985
|
+
var init_verify_runner = __esm({
|
|
986
|
+
"src/verify-runner.ts"() {
|
|
987
|
+
"use strict";
|
|
988
|
+
init_config();
|
|
989
|
+
init_logger();
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
// src/observer.ts
|
|
994
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
995
|
+
async function diagnoseFailure(stageName, errorOutput, modifiedFiles, runner, model) {
|
|
996
|
+
const context = [
|
|
997
|
+
`Stage: ${stageName}`,
|
|
998
|
+
``,
|
|
999
|
+
`Error output:`,
|
|
1000
|
+
errorOutput.slice(-2e3),
|
|
1001
|
+
// Last 2000 chars of error
|
|
1002
|
+
``,
|
|
1003
|
+
modifiedFiles.length > 0 ? `Files modified by build stage:
|
|
1004
|
+
${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (build may not have run yet)."
|
|
1005
|
+
].join("\n");
|
|
1006
|
+
const prompt = DIAGNOSIS_PROMPT + context;
|
|
1007
|
+
try {
|
|
1008
|
+
const result = await runner.run(
|
|
1009
|
+
"diagnosis",
|
|
1010
|
+
prompt,
|
|
1011
|
+
model,
|
|
1012
|
+
3e4,
|
|
1013
|
+
// 30s timeout — this should be fast
|
|
1014
|
+
""
|
|
1015
|
+
);
|
|
1016
|
+
if (result.outcome === "completed" && result.output) {
|
|
1017
|
+
const cleaned = result.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
|
|
1018
|
+
const parsed = JSON.parse(cleaned);
|
|
1019
|
+
const validClassifications = [
|
|
1020
|
+
"fixable",
|
|
1021
|
+
"infrastructure",
|
|
1022
|
+
"pre-existing",
|
|
1023
|
+
"retry",
|
|
1024
|
+
"abort"
|
|
1025
|
+
];
|
|
1026
|
+
if (validClassifications.includes(parsed.classification)) {
|
|
1027
|
+
logger.info(` Diagnosis: ${parsed.classification} \u2014 ${parsed.reason}`);
|
|
1028
|
+
return {
|
|
1029
|
+
classification: parsed.classification,
|
|
1030
|
+
reason: parsed.reason ?? "Unknown reason",
|
|
1031
|
+
resolution: parsed.resolution ?? ""
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
} catch (err) {
|
|
1036
|
+
logger.warn(` Diagnosis error: ${err instanceof Error ? err.message : err}`);
|
|
1037
|
+
}
|
|
1038
|
+
logger.warn(" Diagnosis failed \u2014 defaulting to fixable");
|
|
1039
|
+
return {
|
|
1040
|
+
classification: "fixable",
|
|
1041
|
+
reason: "Could not diagnose failure",
|
|
1042
|
+
resolution: errorOutput.slice(-500)
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
function getModifiedFiles(projectDir) {
|
|
1046
|
+
try {
|
|
1047
|
+
const output = execFileSync5("git", ["diff", "--name-only", "HEAD~1"], {
|
|
1048
|
+
encoding: "utf-8",
|
|
1049
|
+
cwd: projectDir,
|
|
1050
|
+
timeout: 5e3,
|
|
1051
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1052
|
+
}).trim();
|
|
1053
|
+
return output ? output.split("\n").filter(Boolean) : [];
|
|
1054
|
+
} catch {
|
|
1055
|
+
return [];
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
var DIAGNOSIS_PROMPT;
|
|
1059
|
+
var init_observer = __esm({
|
|
1060
|
+
"src/observer.ts"() {
|
|
1061
|
+
"use strict";
|
|
1062
|
+
init_logger();
|
|
1063
|
+
DIAGNOSIS_PROMPT = `You are a pipeline failure diagnosis agent. Analyze the error and classify it.
|
|
1064
|
+
|
|
1065
|
+
Output ONLY valid JSON. No markdown fences. No explanation.
|
|
1066
|
+
|
|
1067
|
+
{
|
|
1068
|
+
"classification": "fixable | infrastructure | pre-existing | retry | abort",
|
|
1069
|
+
"reason": "One sentence explaining what went wrong",
|
|
1070
|
+
"resolution": "Specific instructions for fixing (if fixable) or what the user needs to do (if infrastructure)"
|
|
964
1071
|
}
|
|
1072
|
+
|
|
1073
|
+
Classification rules:
|
|
1074
|
+
- fixable: Error is in code that was just written/modified. The resolution should describe exactly what to change.
|
|
1075
|
+
- infrastructure: External dependency not available (database, API, service). The resolution should say what the user needs to set up.
|
|
1076
|
+
- pre-existing: Error exists in code that was NOT modified. Safe to skip. The resolution should note which files.
|
|
1077
|
+
- retry: Transient error (network timeout, rate limit, flaky test). Worth retrying once.
|
|
1078
|
+
- abort: Unrecoverable error (permission denied, corrupted state, out of disk). Pipeline should stop.
|
|
1079
|
+
|
|
1080
|
+
Error context:
|
|
1081
|
+
`;
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
// src/stages/gate.ts
|
|
1086
|
+
import * as fs6 from "fs";
|
|
1087
|
+
import * as path6 from "path";
|
|
965
1088
|
function executeGateStage(ctx, def) {
|
|
966
1089
|
if (ctx.input.dryRun) {
|
|
967
1090
|
logger.info(` [dry-run] skipping ${def.name}`);
|
|
@@ -992,12 +1115,24 @@ function executeGateStage(ctx, def) {
|
|
|
992
1115
|
`);
|
|
993
1116
|
}
|
|
994
1117
|
}
|
|
995
|
-
|
|
1118
|
+
fs6.writeFileSync(path6.join(ctx.taskDir, "verify.md"), lines.join(""));
|
|
996
1119
|
return {
|
|
997
1120
|
outcome: verifyResult.pass ? "completed" : "failed",
|
|
998
1121
|
retries: 0
|
|
999
1122
|
};
|
|
1000
1123
|
}
|
|
1124
|
+
var init_gate = __esm({
|
|
1125
|
+
"src/stages/gate.ts"() {
|
|
1126
|
+
"use strict";
|
|
1127
|
+
init_verify_runner();
|
|
1128
|
+
init_logger();
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
// src/stages/verify.ts
|
|
1133
|
+
import * as fs7 from "fs";
|
|
1134
|
+
import * as path7 from "path";
|
|
1135
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
1001
1136
|
async function executeVerifyWithAutofix(ctx, def) {
|
|
1002
1137
|
const maxAttempts = def.maxRetries ?? 2;
|
|
1003
1138
|
for (let attempt = 0; attempt <= maxAttempts; attempt++) {
|
|
@@ -1007,13 +1142,45 @@ async function executeVerifyWithAutofix(ctx, def) {
|
|
|
1007
1142
|
return { ...gateResult, retries: attempt };
|
|
1008
1143
|
}
|
|
1009
1144
|
if (attempt < maxAttempts) {
|
|
1010
|
-
|
|
1145
|
+
const verifyPath = path7.join(ctx.taskDir, "verify.md");
|
|
1146
|
+
const errorOutput = fs7.existsSync(verifyPath) ? fs7.readFileSync(verifyPath, "utf-8") : "Unknown error";
|
|
1147
|
+
const modifiedFiles = getModifiedFiles(ctx.projectDir);
|
|
1148
|
+
const defaultRunner = getRunnerForStage(ctx, "taskify");
|
|
1149
|
+
const diagnosis = await diagnoseFailure(
|
|
1150
|
+
"verify",
|
|
1151
|
+
errorOutput,
|
|
1152
|
+
modifiedFiles,
|
|
1153
|
+
defaultRunner,
|
|
1154
|
+
resolveModel("cheap")
|
|
1155
|
+
);
|
|
1156
|
+
if (diagnosis.classification === "infrastructure") {
|
|
1157
|
+
logger.warn(` Infrastructure issue: ${diagnosis.reason}`);
|
|
1158
|
+
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
1159
|
+
try {
|
|
1160
|
+
postComment(ctx.input.issueNumber, `\u26A0\uFE0F **Infrastructure issue detected:** ${diagnosis.reason}
|
|
1161
|
+
|
|
1162
|
+
${diagnosis.resolution}`);
|
|
1163
|
+
} catch {
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
return { outcome: "completed", retries: attempt, error: `Skipped: ${diagnosis.reason}` };
|
|
1167
|
+
}
|
|
1168
|
+
if (diagnosis.classification === "pre-existing") {
|
|
1169
|
+
logger.warn(` Pre-existing issue: ${diagnosis.reason}`);
|
|
1170
|
+
return { outcome: "completed", retries: attempt, error: `Skipped: ${diagnosis.reason}` };
|
|
1171
|
+
}
|
|
1172
|
+
if (diagnosis.classification === "abort") {
|
|
1173
|
+
logger.error(` Unrecoverable: ${diagnosis.reason}`);
|
|
1174
|
+
return { outcome: "failed", retries: attempt, error: diagnosis.reason };
|
|
1175
|
+
}
|
|
1176
|
+
logger.info(` Diagnosis: ${diagnosis.classification} \u2014 ${diagnosis.reason}`);
|
|
1011
1177
|
const config = getProjectConfig();
|
|
1012
1178
|
const runFix = (cmd) => {
|
|
1013
1179
|
if (!cmd) return;
|
|
1014
|
-
const parts = cmd
|
|
1180
|
+
const parts = parseCommand(cmd);
|
|
1181
|
+
if (parts.length === 0) return;
|
|
1015
1182
|
try {
|
|
1016
|
-
|
|
1183
|
+
execFileSync6(parts[0], parts.slice(1), {
|
|
1017
1184
|
stdio: "pipe",
|
|
1018
1185
|
timeout: FIX_COMMAND_TIMEOUT_MS
|
|
1019
1186
|
});
|
|
@@ -1023,8 +1190,17 @@ async function executeVerifyWithAutofix(ctx, def) {
|
|
|
1023
1190
|
runFix(config.quality.lintFix);
|
|
1024
1191
|
runFix(config.quality.formatFix);
|
|
1025
1192
|
if (def.retryWithAgent) {
|
|
1026
|
-
|
|
1027
|
-
|
|
1193
|
+
const autofixCtx = {
|
|
1194
|
+
...ctx,
|
|
1195
|
+
input: {
|
|
1196
|
+
...ctx.input,
|
|
1197
|
+
feedback: `${diagnosis.resolution}
|
|
1198
|
+
|
|
1199
|
+
${ctx.input.feedback ?? ""}`.trim()
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1202
|
+
logger.info(` running ${def.retryWithAgent} agent with diagnosis guidance...`);
|
|
1203
|
+
await executeAgentStage(autofixCtx, {
|
|
1028
1204
|
...def,
|
|
1029
1205
|
name: def.retryWithAgent,
|
|
1030
1206
|
type: "agent",
|
|
@@ -1041,6 +1217,24 @@ async function executeVerifyWithAutofix(ctx, def) {
|
|
|
1041
1217
|
error: "Verification failed after autofix attempts"
|
|
1042
1218
|
};
|
|
1043
1219
|
}
|
|
1220
|
+
var init_verify = __esm({
|
|
1221
|
+
"src/stages/verify.ts"() {
|
|
1222
|
+
"use strict";
|
|
1223
|
+
init_context();
|
|
1224
|
+
init_config();
|
|
1225
|
+
init_verify_runner();
|
|
1226
|
+
init_runner_selection();
|
|
1227
|
+
init_github_api();
|
|
1228
|
+
init_observer();
|
|
1229
|
+
init_logger();
|
|
1230
|
+
init_agent();
|
|
1231
|
+
init_gate();
|
|
1232
|
+
}
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
// src/stages/review.ts
|
|
1236
|
+
import * as fs8 from "fs";
|
|
1237
|
+
import * as path8 from "path";
|
|
1044
1238
|
async function executeReviewWithFix(ctx, def) {
|
|
1045
1239
|
if (ctx.input.dryRun) {
|
|
1046
1240
|
return { outcome: "completed", retries: 0 };
|
|
@@ -1051,11 +1245,11 @@ async function executeReviewWithFix(ctx, def) {
|
|
|
1051
1245
|
if (reviewResult.outcome !== "completed") {
|
|
1052
1246
|
return reviewResult;
|
|
1053
1247
|
}
|
|
1054
|
-
const reviewFile =
|
|
1055
|
-
if (!
|
|
1248
|
+
const reviewFile = path8.join(ctx.taskDir, "review.md");
|
|
1249
|
+
if (!fs8.existsSync(reviewFile)) {
|
|
1056
1250
|
return { outcome: "failed", retries: 0, error: "review.md not found" };
|
|
1057
1251
|
}
|
|
1058
|
-
const content =
|
|
1252
|
+
const content = fs8.readFileSync(reviewFile, "utf-8");
|
|
1059
1253
|
const hasIssues = /\bfail\b/i.test(content) && !/pass/i.test(content);
|
|
1060
1254
|
if (!hasIssues) {
|
|
1061
1255
|
return reviewResult;
|
|
@@ -1068,12 +1262,25 @@ async function executeReviewWithFix(ctx, def) {
|
|
|
1068
1262
|
logger.info(` re-running review after fix...`);
|
|
1069
1263
|
return executeAgentStage(ctx, reviewDef);
|
|
1070
1264
|
}
|
|
1265
|
+
var init_review = __esm({
|
|
1266
|
+
"src/stages/review.ts"() {
|
|
1267
|
+
"use strict";
|
|
1268
|
+
init_definitions();
|
|
1269
|
+
init_logger();
|
|
1270
|
+
init_agent();
|
|
1271
|
+
}
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
// src/stages/ship.ts
|
|
1275
|
+
import * as fs9 from "fs";
|
|
1276
|
+
import * as path9 from "path";
|
|
1277
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
1071
1278
|
function buildPrBody(ctx) {
|
|
1072
1279
|
const sections = [];
|
|
1073
|
-
const taskJsonPath =
|
|
1074
|
-
if (
|
|
1280
|
+
const taskJsonPath = path9.join(ctx.taskDir, "task.json");
|
|
1281
|
+
if (fs9.existsSync(taskJsonPath)) {
|
|
1075
1282
|
try {
|
|
1076
|
-
const raw =
|
|
1283
|
+
const raw = fs9.readFileSync(taskJsonPath, "utf-8");
|
|
1077
1284
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
1078
1285
|
const task = JSON.parse(cleaned);
|
|
1079
1286
|
if (task.description) {
|
|
@@ -1092,9 +1299,9 @@ ${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
|
|
|
1092
1299
|
} catch {
|
|
1093
1300
|
}
|
|
1094
1301
|
}
|
|
1095
|
-
const reviewPath =
|
|
1096
|
-
if (
|
|
1097
|
-
const review =
|
|
1302
|
+
const reviewPath = path9.join(ctx.taskDir, "review.md");
|
|
1303
|
+
if (fs9.existsSync(reviewPath)) {
|
|
1304
|
+
const review = fs9.readFileSync(reviewPath, "utf-8");
|
|
1098
1305
|
const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
1099
1306
|
if (summaryMatch) {
|
|
1100
1307
|
const summary = summaryMatch[1].trim();
|
|
@@ -1111,14 +1318,14 @@ ${summary}`);
|
|
|
1111
1318
|
**Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
|
|
1112
1319
|
}
|
|
1113
1320
|
}
|
|
1114
|
-
const verifyPath =
|
|
1115
|
-
if (
|
|
1116
|
-
const verify =
|
|
1321
|
+
const verifyPath = path9.join(ctx.taskDir, "verify.md");
|
|
1322
|
+
if (fs9.existsSync(verifyPath)) {
|
|
1323
|
+
const verify = fs9.readFileSync(verifyPath, "utf-8");
|
|
1117
1324
|
if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
|
|
1118
1325
|
}
|
|
1119
|
-
const planPath =
|
|
1120
|
-
if (
|
|
1121
|
-
const plan =
|
|
1326
|
+
const planPath = path9.join(ctx.taskDir, "plan.md");
|
|
1327
|
+
if (fs9.existsSync(planPath)) {
|
|
1328
|
+
const plan = fs9.readFileSync(planPath, "utf-8").trim();
|
|
1122
1329
|
if (plan) {
|
|
1123
1330
|
const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
|
|
1124
1331
|
sections.push(`
|
|
@@ -1138,13 +1345,13 @@ Closes #${ctx.input.issueNumber}`);
|
|
|
1138
1345
|
return sections.join("\n");
|
|
1139
1346
|
}
|
|
1140
1347
|
function executeShipStage(ctx, _def) {
|
|
1141
|
-
const shipPath =
|
|
1348
|
+
const shipPath = path9.join(ctx.taskDir, "ship.md");
|
|
1142
1349
|
if (ctx.input.dryRun) {
|
|
1143
|
-
|
|
1350
|
+
fs9.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
|
|
1144
1351
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
1145
1352
|
}
|
|
1146
1353
|
if (ctx.input.local && !ctx.input.issueNumber) {
|
|
1147
|
-
|
|
1354
|
+
fs9.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
|
|
1148
1355
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
1149
1356
|
}
|
|
1150
1357
|
try {
|
|
@@ -1156,7 +1363,7 @@ function executeShipStage(ctx, _def) {
|
|
|
1156
1363
|
let repo = config.github?.repo;
|
|
1157
1364
|
if (!owner || !repo) {
|
|
1158
1365
|
try {
|
|
1159
|
-
const remoteUrl =
|
|
1366
|
+
const remoteUrl = execFileSync7("git", ["remote", "get-url", "origin"], {
|
|
1160
1367
|
encoding: "utf-8",
|
|
1161
1368
|
cwd: ctx.projectDir
|
|
1162
1369
|
}).trim();
|
|
@@ -1176,10 +1383,10 @@ function executeShipStage(ctx, _def) {
|
|
|
1176
1383
|
docs: "docs",
|
|
1177
1384
|
chore: "chore"
|
|
1178
1385
|
};
|
|
1179
|
-
const taskJsonPath =
|
|
1180
|
-
if (
|
|
1386
|
+
const taskJsonPath = path9.join(ctx.taskDir, "task.json");
|
|
1387
|
+
if (fs9.existsSync(taskJsonPath)) {
|
|
1181
1388
|
try {
|
|
1182
|
-
const raw =
|
|
1389
|
+
const raw = fs9.readFileSync(taskJsonPath, "utf-8");
|
|
1183
1390
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
1184
1391
|
const task = JSON.parse(cleaned);
|
|
1185
1392
|
const prefix = TYPE_PREFIX[task.task_type] ?? "chore";
|
|
@@ -1188,42 +1395,396 @@ function executeShipStage(ctx, _def) {
|
|
|
1188
1395
|
} catch {
|
|
1189
1396
|
}
|
|
1190
1397
|
}
|
|
1191
|
-
if (title === "Update") {
|
|
1192
|
-
const taskMdPath =
|
|
1193
|
-
if (
|
|
1194
|
-
const content =
|
|
1195
|
-
const firstLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith("*"));
|
|
1196
|
-
if (firstLine) title = `chore: ${firstLine.trim()}`.slice(0, 72);
|
|
1197
|
-
}
|
|
1398
|
+
if (title === "Update") {
|
|
1399
|
+
const taskMdPath = path9.join(ctx.taskDir, "task.md");
|
|
1400
|
+
if (fs9.existsSync(taskMdPath)) {
|
|
1401
|
+
const content = fs9.readFileSync(taskMdPath, "utf-8");
|
|
1402
|
+
const firstLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith("*"));
|
|
1403
|
+
if (firstLine) title = `chore: ${firstLine.trim()}`.slice(0, 72);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
const body = buildPrBody(ctx);
|
|
1407
|
+
const pr = createPR(head, base, title, body);
|
|
1408
|
+
if (pr) {
|
|
1409
|
+
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
1410
|
+
try {
|
|
1411
|
+
postComment(ctx.input.issueNumber, `\u{1F389} PR created: ${pr.url}`);
|
|
1412
|
+
} catch {
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
fs9.writeFileSync(shipPath, `# Ship
|
|
1416
|
+
|
|
1417
|
+
PR created: ${pr.url}
|
|
1418
|
+
PR #${pr.number}
|
|
1419
|
+
`);
|
|
1420
|
+
} else {
|
|
1421
|
+
fs9.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
|
|
1422
|
+
}
|
|
1423
|
+
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
1424
|
+
} catch (err) {
|
|
1425
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1426
|
+
fs9.writeFileSync(shipPath, `# Ship
|
|
1427
|
+
|
|
1428
|
+
Failed: ${msg}
|
|
1429
|
+
`);
|
|
1430
|
+
return { outcome: "failed", retries: 0, error: msg };
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
var init_ship = __esm({
|
|
1434
|
+
"src/stages/ship.ts"() {
|
|
1435
|
+
"use strict";
|
|
1436
|
+
init_git_utils();
|
|
1437
|
+
init_github_api();
|
|
1438
|
+
init_config();
|
|
1439
|
+
}
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
// src/pipeline/executor-registry.ts
|
|
1443
|
+
function getExecutor(name) {
|
|
1444
|
+
const executor = EXECUTOR_REGISTRY[name];
|
|
1445
|
+
if (!executor) {
|
|
1446
|
+
throw new Error(`No executor registered for stage: ${name}`);
|
|
1447
|
+
}
|
|
1448
|
+
return executor;
|
|
1449
|
+
}
|
|
1450
|
+
var EXECUTOR_REGISTRY;
|
|
1451
|
+
var init_executor_registry = __esm({
|
|
1452
|
+
"src/pipeline/executor-registry.ts"() {
|
|
1453
|
+
"use strict";
|
|
1454
|
+
init_agent();
|
|
1455
|
+
init_verify();
|
|
1456
|
+
init_review();
|
|
1457
|
+
init_ship();
|
|
1458
|
+
EXECUTOR_REGISTRY = {
|
|
1459
|
+
taskify: executeAgentStage,
|
|
1460
|
+
plan: executeAgentStage,
|
|
1461
|
+
build: executeAgentStage,
|
|
1462
|
+
verify: executeVerifyWithAutofix,
|
|
1463
|
+
review: executeReviewWithFix,
|
|
1464
|
+
"review-fix": executeAgentStage,
|
|
1465
|
+
ship: executeShipStage
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
// src/pipeline/questions.ts
|
|
1471
|
+
import * as fs10 from "fs";
|
|
1472
|
+
import * as path10 from "path";
|
|
1473
|
+
function checkForQuestions(ctx, stageName) {
|
|
1474
|
+
if (ctx.input.local || !ctx.input.issueNumber) return false;
|
|
1475
|
+
try {
|
|
1476
|
+
if (stageName === "taskify") {
|
|
1477
|
+
const taskJsonPath = path10.join(ctx.taskDir, "task.json");
|
|
1478
|
+
if (!fs10.existsSync(taskJsonPath)) return false;
|
|
1479
|
+
const raw = fs10.readFileSync(taskJsonPath, "utf-8");
|
|
1480
|
+
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
1481
|
+
const taskJson = JSON.parse(cleaned);
|
|
1482
|
+
if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
|
|
1483
|
+
const body = `\u{1F914} **Kody has questions before proceeding:**
|
|
1484
|
+
|
|
1485
|
+
${taskJson.questions.map((q, i) => `${i + 1}. ${q}`).join("\n")}
|
|
1486
|
+
|
|
1487
|
+
Reply with \`@kody approve\` and your answers in the comment body.`;
|
|
1488
|
+
postComment(ctx.input.issueNumber, body);
|
|
1489
|
+
setLifecycleLabel(ctx.input.issueNumber, "waiting");
|
|
1490
|
+
return true;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
if (stageName === "plan") {
|
|
1494
|
+
const planPath = path10.join(ctx.taskDir, "plan.md");
|
|
1495
|
+
if (!fs10.existsSync(planPath)) return false;
|
|
1496
|
+
const plan = fs10.readFileSync(planPath, "utf-8");
|
|
1497
|
+
const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
1498
|
+
if (questionsMatch) {
|
|
1499
|
+
const questionsText = questionsMatch[1].trim();
|
|
1500
|
+
const questions = questionsText.split("\n").filter((l) => l.startsWith("- ")).map((l) => l.slice(2));
|
|
1501
|
+
if (questions.length > 0) {
|
|
1502
|
+
const body = `\u{1F3D7}\uFE0F **Kody has architecture questions:**
|
|
1503
|
+
|
|
1504
|
+
${questions.map((q, i) => `${i + 1}. ${q}`).join("\n")}
|
|
1505
|
+
|
|
1506
|
+
Reply with \`@kody approve\` and your answers in the comment body.`;
|
|
1507
|
+
postComment(ctx.input.issueNumber, body);
|
|
1508
|
+
setLifecycleLabel(ctx.input.issueNumber, "waiting");
|
|
1509
|
+
return true;
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
} catch {
|
|
1514
|
+
}
|
|
1515
|
+
return false;
|
|
1516
|
+
}
|
|
1517
|
+
var init_questions = __esm({
|
|
1518
|
+
"src/pipeline/questions.ts"() {
|
|
1519
|
+
"use strict";
|
|
1520
|
+
init_github_api();
|
|
1521
|
+
}
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
// src/pipeline/hooks.ts
|
|
1525
|
+
import * as fs11 from "fs";
|
|
1526
|
+
import * as path11 from "path";
|
|
1527
|
+
function applyPreStageLabel(ctx, def) {
|
|
1528
|
+
if (!ctx.input.issueNumber || ctx.input.local) return;
|
|
1529
|
+
if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
|
|
1530
|
+
if (def.name === "review") setLifecycleLabel(ctx.input.issueNumber, "review");
|
|
1531
|
+
}
|
|
1532
|
+
function checkQuestionsAfterStage(ctx, def, state) {
|
|
1533
|
+
if (def.name !== "taskify" && def.name !== "plan") return null;
|
|
1534
|
+
if (ctx.input.dryRun) return null;
|
|
1535
|
+
const paused = checkForQuestions(ctx, def.name);
|
|
1536
|
+
if (!paused) return null;
|
|
1537
|
+
state.state = "failed";
|
|
1538
|
+
state.stages[def.name] = {
|
|
1539
|
+
...state.stages[def.name],
|
|
1540
|
+
state: "completed",
|
|
1541
|
+
error: "paused: waiting for answers"
|
|
1542
|
+
};
|
|
1543
|
+
writeState(state, ctx.taskDir);
|
|
1544
|
+
logger.info(` Pipeline paused \u2014 questions posted on issue`);
|
|
1545
|
+
return state;
|
|
1546
|
+
}
|
|
1547
|
+
function autoDetectComplexity(ctx, def) {
|
|
1548
|
+
if (def.name !== "taskify") return null;
|
|
1549
|
+
if (ctx.input.complexity) return null;
|
|
1550
|
+
try {
|
|
1551
|
+
const taskJsonPath = path11.join(ctx.taskDir, "task.json");
|
|
1552
|
+
if (!fs11.existsSync(taskJsonPath)) return null;
|
|
1553
|
+
const raw = fs11.readFileSync(taskJsonPath, "utf-8");
|
|
1554
|
+
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
1555
|
+
const taskJson = JSON.parse(cleaned);
|
|
1556
|
+
if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
|
|
1557
|
+
const complexity = taskJson.risk_level;
|
|
1558
|
+
const activeStages = filterByComplexity(STAGES, complexity);
|
|
1559
|
+
logger.info(` Complexity auto-detected: ${complexity} (${activeStages.map((s) => s.name).join(" \u2192 ")})`);
|
|
1560
|
+
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
1561
|
+
try {
|
|
1562
|
+
setLifecycleLabel(ctx.input.issueNumber, complexity);
|
|
1563
|
+
} catch {
|
|
1564
|
+
}
|
|
1565
|
+
if (taskJson.task_type) {
|
|
1566
|
+
try {
|
|
1567
|
+
setLabel(ctx.input.issueNumber, `kody:${taskJson.task_type}`);
|
|
1568
|
+
} catch {
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
return { complexity, activeStages };
|
|
1573
|
+
} catch {
|
|
1574
|
+
return null;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
function commitAfterStage(ctx, def) {
|
|
1578
|
+
if (ctx.input.dryRun || !ctx.input.issueNumber) return;
|
|
1579
|
+
if (def.name === "build") {
|
|
1580
|
+
try {
|
|
1581
|
+
commitAll(`feat(${ctx.taskId}): implement task`, ctx.projectDir);
|
|
1582
|
+
} catch {
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
if (def.name === "review-fix") {
|
|
1586
|
+
try {
|
|
1587
|
+
commitAll(`fix(${ctx.taskId}): address review`, ctx.projectDir);
|
|
1588
|
+
} catch {
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
function postSkippedStagesComment(ctx, complexity, activeStages) {
|
|
1593
|
+
if (!ctx.input.issueNumber || ctx.input.local || ctx.input.dryRun) return;
|
|
1594
|
+
const skipped = STAGES.filter((s) => !activeStages.find((a) => a.name === s.name)).map((s) => s.name);
|
|
1595
|
+
if (skipped.length === 0) return;
|
|
1596
|
+
try {
|
|
1597
|
+
postComment(
|
|
1598
|
+
ctx.input.issueNumber,
|
|
1599
|
+
`\u26A1 **Complexity: ${complexity}** \u2014 skipping ${skipped.join(", ")} (not needed for ${complexity}-risk tasks)`
|
|
1600
|
+
);
|
|
1601
|
+
} catch {
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
var init_hooks = __esm({
|
|
1605
|
+
"src/pipeline/hooks.ts"() {
|
|
1606
|
+
"use strict";
|
|
1607
|
+
init_definitions();
|
|
1608
|
+
init_github_api();
|
|
1609
|
+
init_git_utils();
|
|
1610
|
+
init_questions();
|
|
1611
|
+
init_complexity();
|
|
1612
|
+
init_state();
|
|
1613
|
+
init_logger();
|
|
1614
|
+
}
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
// src/learning/auto-learn.ts
|
|
1618
|
+
import * as fs12 from "fs";
|
|
1619
|
+
import * as path12 from "path";
|
|
1620
|
+
function stripAnsi(str) {
|
|
1621
|
+
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
1622
|
+
}
|
|
1623
|
+
function autoLearn(ctx) {
|
|
1624
|
+
try {
|
|
1625
|
+
const memoryDir = path12.join(ctx.projectDir, ".kody", "memory");
|
|
1626
|
+
if (!fs12.existsSync(memoryDir)) {
|
|
1627
|
+
fs12.mkdirSync(memoryDir, { recursive: true });
|
|
1628
|
+
}
|
|
1629
|
+
const learnings = [];
|
|
1630
|
+
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1631
|
+
const verifyPath = path12.join(ctx.taskDir, "verify.md");
|
|
1632
|
+
if (fs12.existsSync(verifyPath)) {
|
|
1633
|
+
const verify = stripAnsi(fs12.readFileSync(verifyPath, "utf-8"));
|
|
1634
|
+
if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
|
|
1635
|
+
if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
|
|
1636
|
+
if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
|
|
1637
|
+
if (/prettier/i.test(verify)) learnings.push("- Uses prettier for formatting");
|
|
1638
|
+
if (/tsc\b/i.test(verify)) learnings.push("- Uses TypeScript (tsc)");
|
|
1639
|
+
if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
|
|
1640
|
+
if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
|
|
1641
|
+
}
|
|
1642
|
+
const reviewPath = path12.join(ctx.taskDir, "review.md");
|
|
1643
|
+
if (fs12.existsSync(reviewPath)) {
|
|
1644
|
+
const review = fs12.readFileSync(reviewPath, "utf-8");
|
|
1645
|
+
if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
|
|
1646
|
+
if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
|
|
1647
|
+
if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
|
|
1648
|
+
if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
|
|
1649
|
+
}
|
|
1650
|
+
const taskJsonPath = path12.join(ctx.taskDir, "task.json");
|
|
1651
|
+
if (fs12.existsSync(taskJsonPath)) {
|
|
1652
|
+
try {
|
|
1653
|
+
const raw = stripAnsi(fs12.readFileSync(taskJsonPath, "utf-8"));
|
|
1654
|
+
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
1655
|
+
const task = JSON.parse(cleaned);
|
|
1656
|
+
if (task.scope && Array.isArray(task.scope)) {
|
|
1657
|
+
const dirs = [...new Set(task.scope.map((s) => s.split("/").slice(0, -1).join("/")).filter(Boolean))];
|
|
1658
|
+
if (dirs.length > 0) learnings.push(`- Active directories: ${dirs.join(", ")}`);
|
|
1659
|
+
}
|
|
1660
|
+
} catch {
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
if (learnings.length > 0) {
|
|
1664
|
+
const conventionsPath = path12.join(memoryDir, "conventions.md");
|
|
1665
|
+
const entry = `
|
|
1666
|
+
## Learned ${timestamp2} (task: ${ctx.taskId})
|
|
1667
|
+
${learnings.join("\n")}
|
|
1668
|
+
`;
|
|
1669
|
+
fs12.appendFileSync(conventionsPath, entry);
|
|
1670
|
+
logger.info(`Auto-learned ${learnings.length} convention(s)`);
|
|
1671
|
+
}
|
|
1672
|
+
autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
|
|
1673
|
+
} catch {
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
1677
|
+
const archPath = path12.join(memoryDir, "architecture.md");
|
|
1678
|
+
if (fs12.existsSync(archPath)) return;
|
|
1679
|
+
const detected = [];
|
|
1680
|
+
const pkgPath = path12.join(projectDir, "package.json");
|
|
1681
|
+
if (fs12.existsSync(pkgPath)) {
|
|
1682
|
+
try {
|
|
1683
|
+
const pkg = JSON.parse(fs12.readFileSync(pkgPath, "utf-8"));
|
|
1684
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1685
|
+
if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
|
|
1686
|
+
else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
|
|
1687
|
+
else if (allDeps.express) detected.push(`- Framework: Express ${allDeps.express}`);
|
|
1688
|
+
else if (allDeps.fastify) detected.push(`- Framework: Fastify ${allDeps.fastify}`);
|
|
1689
|
+
if (allDeps.typescript) detected.push(`- Language: TypeScript ${allDeps.typescript}`);
|
|
1690
|
+
if (allDeps.vitest) detected.push(`- Testing: vitest ${allDeps.vitest}`);
|
|
1691
|
+
else if (allDeps.jest) detected.push(`- Testing: jest ${allDeps.jest}`);
|
|
1692
|
+
if (allDeps.eslint) detected.push(`- Linting: eslint ${allDeps.eslint}`);
|
|
1693
|
+
if (allDeps.prisma || allDeps["@prisma/client"]) detected.push("- Database: Prisma ORM");
|
|
1694
|
+
if (allDeps.drizzle || allDeps["drizzle-orm"]) detected.push("- Database: Drizzle ORM");
|
|
1695
|
+
if (allDeps.pg || allDeps.postgres) detected.push("- Database: PostgreSQL");
|
|
1696
|
+
if (allDeps.payload || allDeps["@payloadcms/next"]) detected.push(`- CMS: Payload CMS`);
|
|
1697
|
+
if (pkg.type === "module") detected.push("- Module system: ESM");
|
|
1698
|
+
else detected.push("- Module system: CommonJS");
|
|
1699
|
+
if (fs12.existsSync(path12.join(projectDir, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
|
|
1700
|
+
else if (fs12.existsSync(path12.join(projectDir, "yarn.lock"))) detected.push("- Package manager: yarn");
|
|
1701
|
+
else if (fs12.existsSync(path12.join(projectDir, "package-lock.json"))) detected.push("- Package manager: npm");
|
|
1702
|
+
} catch {
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
const topDirs = [];
|
|
1706
|
+
try {
|
|
1707
|
+
const entries = fs12.readdirSync(projectDir, { withFileTypes: true });
|
|
1708
|
+
for (const entry of entries) {
|
|
1709
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
1710
|
+
topDirs.push(entry.name);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
if (topDirs.length > 0) detected.push(`- Top-level directories: ${topDirs.join(", ")}`);
|
|
1714
|
+
} catch {
|
|
1715
|
+
}
|
|
1716
|
+
const srcDir = path12.join(projectDir, "src");
|
|
1717
|
+
if (fs12.existsSync(srcDir)) {
|
|
1718
|
+
try {
|
|
1719
|
+
const srcEntries = fs12.readdirSync(srcDir, { withFileTypes: true });
|
|
1720
|
+
const srcDirs = srcEntries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1721
|
+
if (srcDirs.length > 0) detected.push(`- src/ structure: ${srcDirs.join(", ")}`);
|
|
1722
|
+
} catch {
|
|
1198
1723
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
1203
|
-
try {
|
|
1204
|
-
postComment(ctx.input.issueNumber, `\u{1F389} PR created: ${pr.url}`);
|
|
1205
|
-
} catch {
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
fs4.writeFileSync(shipPath, `# Ship
|
|
1724
|
+
}
|
|
1725
|
+
if (detected.length > 0) {
|
|
1726
|
+
const content = `# Architecture (auto-detected ${timestamp2})
|
|
1209
1727
|
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1728
|
+
## Overview
|
|
1729
|
+
${detected.join("\n")}
|
|
1730
|
+
`;
|
|
1731
|
+
fs12.writeFileSync(archPath, content);
|
|
1732
|
+
logger.info(`Auto-detected architecture (${detected.length} items)`);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
var init_auto_learn = __esm({
|
|
1736
|
+
"src/learning/auto-learn.ts"() {
|
|
1737
|
+
"use strict";
|
|
1738
|
+
init_logger();
|
|
1739
|
+
}
|
|
1740
|
+
});
|
|
1220
1741
|
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1742
|
+
// src/pipeline.ts
|
|
1743
|
+
import * as fs13 from "fs";
|
|
1744
|
+
import * as path13 from "path";
|
|
1745
|
+
function ensureFeatureBranchIfNeeded(ctx) {
|
|
1746
|
+
if (!ctx.input.issueNumber || ctx.input.dryRun) return;
|
|
1747
|
+
try {
|
|
1748
|
+
const taskMdPath = path13.join(ctx.taskDir, "task.md");
|
|
1749
|
+
const title = fs13.existsSync(taskMdPath) ? fs13.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
|
|
1750
|
+
ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
|
|
1751
|
+
syncWithDefault(ctx.projectDir);
|
|
1752
|
+
} catch (err) {
|
|
1753
|
+
logger.warn(` Failed to create/sync feature branch: ${err}`);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
function acquireLock(taskDir) {
|
|
1757
|
+
const lockPath = path13.join(taskDir, ".lock");
|
|
1758
|
+
if (fs13.existsSync(lockPath)) {
|
|
1759
|
+
try {
|
|
1760
|
+
const pid = parseInt(fs13.readFileSync(lockPath, "utf-8").trim(), 10);
|
|
1761
|
+
try {
|
|
1762
|
+
process.kill(pid, 0);
|
|
1763
|
+
throw new Error(`Pipeline already running (PID ${pid})`);
|
|
1764
|
+
} catch (e) {
|
|
1765
|
+
if (e.code !== "ESRCH") throw e;
|
|
1766
|
+
}
|
|
1767
|
+
} catch (e) {
|
|
1768
|
+
if (e instanceof Error && e.message.startsWith("Pipeline already")) throw e;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
fs13.writeFileSync(lockPath, String(process.pid));
|
|
1772
|
+
}
|
|
1773
|
+
function releaseLock(taskDir) {
|
|
1774
|
+
try {
|
|
1775
|
+
fs13.unlinkSync(path13.join(taskDir, ".lock"));
|
|
1776
|
+
} catch {
|
|
1224
1777
|
}
|
|
1225
1778
|
}
|
|
1226
1779
|
async function runPipeline(ctx) {
|
|
1780
|
+
acquireLock(ctx.taskDir);
|
|
1781
|
+
try {
|
|
1782
|
+
return await runPipelineInner(ctx);
|
|
1783
|
+
} finally {
|
|
1784
|
+
releaseLock(ctx.taskDir);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
async function runPipelineInner(ctx) {
|
|
1227
1788
|
let state = loadState(ctx.taskId, ctx.taskDir);
|
|
1228
1789
|
if (!state) {
|
|
1229
1790
|
state = initState(ctx.taskId);
|
|
@@ -1248,16 +1809,7 @@ async function runPipeline(ctx) {
|
|
|
1248
1809
|
const initialPhase = ctx.input.mode === "rerun" ? "building" : "planning";
|
|
1249
1810
|
setLifecycleLabel(ctx.input.issueNumber, initialPhase);
|
|
1250
1811
|
}
|
|
1251
|
-
|
|
1252
|
-
try {
|
|
1253
|
-
const taskMdPath = path4.join(ctx.taskDir, "task.md");
|
|
1254
|
-
const title = fs4.existsSync(taskMdPath) ? fs4.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
|
|
1255
|
-
ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
|
|
1256
|
-
syncWithDefault(ctx.projectDir);
|
|
1257
|
-
} catch (err) {
|
|
1258
|
-
logger.warn(` Failed to create/sync feature branch: ${err}`);
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1812
|
+
ensureFeatureBranchIfNeeded(ctx);
|
|
1261
1813
|
let complexity = ctx.input.complexity ?? "high";
|
|
1262
1814
|
let activeStages = filterByComplexity(STAGES, complexity);
|
|
1263
1815
|
let skippedStagesCommentPosted = false;
|
|
@@ -1277,50 +1829,20 @@ async function runPipeline(ctx) {
|
|
|
1277
1829
|
logger.info(`[${def.name}] skipped (complexity: ${complexity})`);
|
|
1278
1830
|
state.stages[def.name] = { state: "completed", retries: 0, outputFile: void 0 };
|
|
1279
1831
|
writeState(state, ctx.taskDir);
|
|
1280
|
-
if (!skippedStagesCommentPosted
|
|
1281
|
-
|
|
1282
|
-
try {
|
|
1283
|
-
postComment(
|
|
1284
|
-
ctx.input.issueNumber,
|
|
1285
|
-
`\u26A1 **Complexity: ${complexity}** \u2014 skipping ${skipped.join(", ")} (not needed for ${complexity}-risk tasks)`
|
|
1286
|
-
);
|
|
1287
|
-
} catch {
|
|
1288
|
-
}
|
|
1832
|
+
if (!skippedStagesCommentPosted) {
|
|
1833
|
+
postSkippedStagesComment(ctx, complexity, activeStages);
|
|
1289
1834
|
skippedStagesCommentPosted = true;
|
|
1290
1835
|
}
|
|
1291
1836
|
continue;
|
|
1292
1837
|
}
|
|
1293
1838
|
ciGroup(`Stage: ${def.name}`);
|
|
1294
|
-
state.stages[def.name] = {
|
|
1295
|
-
state: "running",
|
|
1296
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1297
|
-
retries: 0
|
|
1298
|
-
};
|
|
1839
|
+
state.stages[def.name] = { state: "running", startedAt: (/* @__PURE__ */ new Date()).toISOString(), retries: 0 };
|
|
1299
1840
|
writeState(state, ctx.taskDir);
|
|
1300
1841
|
logger.info(`[${def.name}] starting...`);
|
|
1301
|
-
|
|
1302
|
-
if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
|
|
1303
|
-
if (def.name === "review") setLifecycleLabel(ctx.input.issueNumber, "review");
|
|
1304
|
-
}
|
|
1842
|
+
applyPreStageLabel(ctx, def);
|
|
1305
1843
|
let result;
|
|
1306
1844
|
try {
|
|
1307
|
-
|
|
1308
|
-
if (def.name === "review") {
|
|
1309
|
-
result = await executeReviewWithFix(ctx, def);
|
|
1310
|
-
} else {
|
|
1311
|
-
result = await executeAgentStage(ctx, def);
|
|
1312
|
-
}
|
|
1313
|
-
} else if (def.type === "gate") {
|
|
1314
|
-
if (def.name === "verify") {
|
|
1315
|
-
result = await executeVerifyWithAutofix(ctx, def);
|
|
1316
|
-
} else {
|
|
1317
|
-
result = executeGateStage(ctx, def);
|
|
1318
|
-
}
|
|
1319
|
-
} else if (def.type === "deterministic") {
|
|
1320
|
-
result = executeShipStage(ctx, def);
|
|
1321
|
-
} else {
|
|
1322
|
-
result = { outcome: "failed", retries: 0, error: `Unknown stage type: ${def.type}` };
|
|
1323
|
-
}
|
|
1845
|
+
result = await getExecutor(def.name)(ctx, def);
|
|
1324
1846
|
} catch (error) {
|
|
1325
1847
|
result = {
|
|
1326
1848
|
outcome: "failed",
|
|
@@ -1337,84 +1859,24 @@ async function runPipeline(ctx) {
|
|
|
1337
1859
|
outputFile: result.outputFile
|
|
1338
1860
|
};
|
|
1339
1861
|
logger.info(`[${def.name}] \u2713 completed`);
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
state: "completed",
|
|
1347
|
-
error: "paused: waiting for answers"
|
|
1348
|
-
};
|
|
1349
|
-
writeState(state, ctx.taskDir);
|
|
1350
|
-
logger.info(` Pipeline paused \u2014 questions posted on issue`);
|
|
1351
|
-
return state;
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
if (def.name === "taskify" && !ctx.input.complexity) {
|
|
1355
|
-
try {
|
|
1356
|
-
const taskJsonPath = path4.join(ctx.taskDir, "task.json");
|
|
1357
|
-
if (fs4.existsSync(taskJsonPath)) {
|
|
1358
|
-
const raw = fs4.readFileSync(taskJsonPath, "utf-8");
|
|
1359
|
-
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
1360
|
-
const taskJson = JSON.parse(cleaned);
|
|
1361
|
-
if (taskJson.risk_level && COMPLEXITY_SKIP[taskJson.risk_level]) {
|
|
1362
|
-
complexity = taskJson.risk_level;
|
|
1363
|
-
activeStages = filterByComplexity(STAGES, complexity);
|
|
1364
|
-
logger.info(` Complexity auto-detected: ${complexity} (${activeStages.map((s) => s.name).join(" \u2192 ")})`);
|
|
1365
|
-
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
1366
|
-
try {
|
|
1367
|
-
setLifecycleLabel(ctx.input.issueNumber, complexity);
|
|
1368
|
-
} catch {
|
|
1369
|
-
}
|
|
1370
|
-
if (taskJson.task_type) {
|
|
1371
|
-
try {
|
|
1372
|
-
setLabel(ctx.input.issueNumber, `kody:${taskJson.task_type}`);
|
|
1373
|
-
} catch {
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
} catch {
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
if (!ctx.input.dryRun && ctx.input.issueNumber) {
|
|
1383
|
-
if (def.name === "build") {
|
|
1384
|
-
try {
|
|
1385
|
-
commitAll(`feat(${ctx.taskId}): implement task`, ctx.projectDir);
|
|
1386
|
-
} catch {
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
if (def.name === "review-fix") {
|
|
1390
|
-
try {
|
|
1391
|
-
commitAll(`fix(${ctx.taskId}): address review`, ctx.projectDir);
|
|
1392
|
-
} catch {
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
} else if (result.outcome === "timed_out") {
|
|
1397
|
-
state.stages[def.name] = {
|
|
1398
|
-
state: "timeout",
|
|
1399
|
-
retries: result.retries,
|
|
1400
|
-
error: "Stage timed out"
|
|
1401
|
-
};
|
|
1402
|
-
state.state = "failed";
|
|
1403
|
-
writeState(state, ctx.taskDir);
|
|
1404
|
-
logger.error(`[${def.name}] \u23F1 timed out`);
|
|
1405
|
-
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
1406
|
-
setLifecycleLabel(ctx.input.issueNumber, "failed");
|
|
1862
|
+
const paused = checkQuestionsAfterStage(ctx, def, state);
|
|
1863
|
+
if (paused) return paused;
|
|
1864
|
+
const detected = autoDetectComplexity(ctx, def);
|
|
1865
|
+
if (detected) {
|
|
1866
|
+
complexity = detected.complexity;
|
|
1867
|
+
activeStages = detected.activeStages;
|
|
1407
1868
|
}
|
|
1408
|
-
|
|
1869
|
+
commitAfterStage(ctx, def);
|
|
1409
1870
|
} else {
|
|
1871
|
+
const isTimeout = result.outcome === "timed_out";
|
|
1410
1872
|
state.stages[def.name] = {
|
|
1411
|
-
state: "failed",
|
|
1873
|
+
state: isTimeout ? "timeout" : "failed",
|
|
1412
1874
|
retries: result.retries,
|
|
1413
|
-
error: result.error ?? "Stage failed"
|
|
1875
|
+
error: isTimeout ? "Stage timed out" : result.error ?? "Stage failed"
|
|
1414
1876
|
};
|
|
1415
1877
|
state.state = "failed";
|
|
1416
1878
|
writeState(state, ctx.taskDir);
|
|
1417
|
-
logger.error(`[${def.name}] \u2717 failed: ${result.error}`);
|
|
1879
|
+
logger.error(`[${def.name}] ${isTimeout ? "\u23F1 timed out" : `\u2717 failed: ${result.error}`}`);
|
|
1418
1880
|
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
1419
1881
|
setLifecycleLabel(ctx.input.issueNumber, "failed");
|
|
1420
1882
|
}
|
|
@@ -1422,9 +1884,7 @@ async function runPipeline(ctx) {
|
|
|
1422
1884
|
}
|
|
1423
1885
|
writeState(state, ctx.taskDir);
|
|
1424
1886
|
}
|
|
1425
|
-
const allCompleted = STAGES.every(
|
|
1426
|
-
(s) => state.stages[s.name].state === "completed"
|
|
1427
|
-
);
|
|
1887
|
+
const allCompleted = STAGES.every((s) => state.stages[s.name].state === "completed");
|
|
1428
1888
|
if (allCompleted) {
|
|
1429
1889
|
state.state = "completed";
|
|
1430
1890
|
writeState(state, ctx.taskDir);
|
|
@@ -1436,121 +1896,6 @@ async function runPipeline(ctx) {
|
|
|
1436
1896
|
}
|
|
1437
1897
|
return state;
|
|
1438
1898
|
}
|
|
1439
|
-
function stripAnsi(str) {
|
|
1440
|
-
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
1441
|
-
}
|
|
1442
|
-
function autoLearn(ctx) {
|
|
1443
|
-
try {
|
|
1444
|
-
const memoryDir = path4.join(ctx.projectDir, ".kody", "memory");
|
|
1445
|
-
if (!fs4.existsSync(memoryDir)) {
|
|
1446
|
-
fs4.mkdirSync(memoryDir, { recursive: true });
|
|
1447
|
-
}
|
|
1448
|
-
const learnings = [];
|
|
1449
|
-
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1450
|
-
const verifyPath = path4.join(ctx.taskDir, "verify.md");
|
|
1451
|
-
if (fs4.existsSync(verifyPath)) {
|
|
1452
|
-
const verify = stripAnsi(fs4.readFileSync(verifyPath, "utf-8"));
|
|
1453
|
-
if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
|
|
1454
|
-
if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
|
|
1455
|
-
if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
|
|
1456
|
-
if (/prettier/i.test(verify)) learnings.push("- Uses prettier for formatting");
|
|
1457
|
-
if (/tsc\b/i.test(verify)) learnings.push("- Uses TypeScript (tsc)");
|
|
1458
|
-
if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
|
|
1459
|
-
if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
|
|
1460
|
-
}
|
|
1461
|
-
const reviewPath = path4.join(ctx.taskDir, "review.md");
|
|
1462
|
-
if (fs4.existsSync(reviewPath)) {
|
|
1463
|
-
const review = fs4.readFileSync(reviewPath, "utf-8");
|
|
1464
|
-
if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
|
|
1465
|
-
if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
|
|
1466
|
-
if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
|
|
1467
|
-
if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
|
|
1468
|
-
}
|
|
1469
|
-
const taskJsonPath = path4.join(ctx.taskDir, "task.json");
|
|
1470
|
-
if (fs4.existsSync(taskJsonPath)) {
|
|
1471
|
-
try {
|
|
1472
|
-
const raw = stripAnsi(fs4.readFileSync(taskJsonPath, "utf-8"));
|
|
1473
|
-
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
1474
|
-
const task = JSON.parse(cleaned);
|
|
1475
|
-
if (task.scope && Array.isArray(task.scope)) {
|
|
1476
|
-
const dirs = [...new Set(task.scope.map((s) => s.split("/").slice(0, -1).join("/")).filter(Boolean))];
|
|
1477
|
-
if (dirs.length > 0) learnings.push(`- Active directories: ${dirs.join(", ")}`);
|
|
1478
|
-
}
|
|
1479
|
-
} catch {
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
if (learnings.length > 0) {
|
|
1483
|
-
const conventionsPath = path4.join(memoryDir, "conventions.md");
|
|
1484
|
-
const entry = `
|
|
1485
|
-
## Learned ${timestamp2} (task: ${ctx.taskId})
|
|
1486
|
-
${learnings.join("\n")}
|
|
1487
|
-
`;
|
|
1488
|
-
fs4.appendFileSync(conventionsPath, entry);
|
|
1489
|
-
logger.info(`Auto-learned ${learnings.length} convention(s)`);
|
|
1490
|
-
}
|
|
1491
|
-
autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
|
|
1492
|
-
} catch {
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1495
|
-
function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
1496
|
-
const archPath = path4.join(memoryDir, "architecture.md");
|
|
1497
|
-
if (fs4.existsSync(archPath)) return;
|
|
1498
|
-
const detected = [];
|
|
1499
|
-
const pkgPath = path4.join(projectDir, "package.json");
|
|
1500
|
-
if (fs4.existsSync(pkgPath)) {
|
|
1501
|
-
try {
|
|
1502
|
-
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
1503
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1504
|
-
if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
|
|
1505
|
-
else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
|
|
1506
|
-
else if (allDeps.express) detected.push(`- Framework: Express ${allDeps.express}`);
|
|
1507
|
-
else if (allDeps.fastify) detected.push(`- Framework: Fastify ${allDeps.fastify}`);
|
|
1508
|
-
if (allDeps.typescript) detected.push(`- Language: TypeScript ${allDeps.typescript}`);
|
|
1509
|
-
if (allDeps.vitest) detected.push(`- Testing: vitest ${allDeps.vitest}`);
|
|
1510
|
-
else if (allDeps.jest) detected.push(`- Testing: jest ${allDeps.jest}`);
|
|
1511
|
-
if (allDeps.eslint) detected.push(`- Linting: eslint ${allDeps.eslint}`);
|
|
1512
|
-
if (allDeps.prisma || allDeps["@prisma/client"]) detected.push("- Database: Prisma ORM");
|
|
1513
|
-
if (allDeps.drizzle || allDeps["drizzle-orm"]) detected.push("- Database: Drizzle ORM");
|
|
1514
|
-
if (allDeps.pg || allDeps.postgres) detected.push("- Database: PostgreSQL");
|
|
1515
|
-
if (allDeps.payload || allDeps["@payloadcms/next"]) detected.push(`- CMS: Payload CMS`);
|
|
1516
|
-
if (pkg.type === "module") detected.push("- Module system: ESM");
|
|
1517
|
-
else detected.push("- Module system: CommonJS");
|
|
1518
|
-
if (fs4.existsSync(path4.join(projectDir, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
|
|
1519
|
-
else if (fs4.existsSync(path4.join(projectDir, "yarn.lock"))) detected.push("- Package manager: yarn");
|
|
1520
|
-
else if (fs4.existsSync(path4.join(projectDir, "package-lock.json"))) detected.push("- Package manager: npm");
|
|
1521
|
-
} catch {
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
const topDirs = [];
|
|
1525
|
-
try {
|
|
1526
|
-
const entries = fs4.readdirSync(projectDir, { withFileTypes: true });
|
|
1527
|
-
for (const entry of entries) {
|
|
1528
|
-
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
1529
|
-
topDirs.push(entry.name);
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
if (topDirs.length > 0) detected.push(`- Top-level directories: ${topDirs.join(", ")}`);
|
|
1533
|
-
} catch {
|
|
1534
|
-
}
|
|
1535
|
-
const srcDir = path4.join(projectDir, "src");
|
|
1536
|
-
if (fs4.existsSync(srcDir)) {
|
|
1537
|
-
try {
|
|
1538
|
-
const srcEntries = fs4.readdirSync(srcDir, { withFileTypes: true });
|
|
1539
|
-
const srcDirs = srcEntries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1540
|
-
if (srcDirs.length > 0) detected.push(`- src/ structure: ${srcDirs.join(", ")}`);
|
|
1541
|
-
} catch {
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
if (detected.length > 0) {
|
|
1545
|
-
const content = `# Architecture (auto-detected ${timestamp2})
|
|
1546
|
-
|
|
1547
|
-
## Overview
|
|
1548
|
-
${detected.join("\n")}
|
|
1549
|
-
`;
|
|
1550
|
-
fs4.writeFileSync(archPath, content);
|
|
1551
|
-
logger.info(`Auto-detected architecture (${detected.length} items)`);
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
1899
|
function printStatus(taskId, taskDir) {
|
|
1555
1900
|
const state = loadState(taskId, taskDir);
|
|
1556
1901
|
if (!state) {
|
|
@@ -1570,29 +1915,24 @@ Task: ${state.taskId}`);
|
|
|
1570
1915
|
console.log(` ${icon} ${stage.name}: ${s.state}${extra}`);
|
|
1571
1916
|
}
|
|
1572
1917
|
}
|
|
1573
|
-
var
|
|
1574
|
-
|
|
1575
|
-
"src/state-machine.ts"() {
|
|
1918
|
+
var init_pipeline = __esm({
|
|
1919
|
+
"src/pipeline.ts"() {
|
|
1576
1920
|
"use strict";
|
|
1577
1921
|
init_definitions();
|
|
1578
|
-
init_context();
|
|
1579
|
-
init_validators();
|
|
1580
1922
|
init_git_utils();
|
|
1581
1923
|
init_github_api();
|
|
1582
|
-
init_verify_runner();
|
|
1583
|
-
init_config();
|
|
1584
1924
|
init_logger();
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1925
|
+
init_state();
|
|
1926
|
+
init_complexity();
|
|
1927
|
+
init_executor_registry();
|
|
1928
|
+
init_hooks();
|
|
1929
|
+
init_auto_learn();
|
|
1590
1930
|
}
|
|
1591
1931
|
});
|
|
1592
1932
|
|
|
1593
1933
|
// src/preflight.ts
|
|
1594
|
-
import { execFileSync as
|
|
1595
|
-
import * as
|
|
1934
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
1935
|
+
import * as fs14 from "fs";
|
|
1596
1936
|
function check(name, fn) {
|
|
1597
1937
|
try {
|
|
1598
1938
|
const detail = fn() ?? void 0;
|
|
@@ -1604,7 +1944,7 @@ function check(name, fn) {
|
|
|
1604
1944
|
function runPreflight() {
|
|
1605
1945
|
const checks = [
|
|
1606
1946
|
check("claude CLI", () => {
|
|
1607
|
-
const v =
|
|
1947
|
+
const v = execFileSync8("claude", ["--version"], {
|
|
1608
1948
|
encoding: "utf-8",
|
|
1609
1949
|
timeout: 1e4,
|
|
1610
1950
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1612,14 +1952,14 @@ function runPreflight() {
|
|
|
1612
1952
|
return v;
|
|
1613
1953
|
}),
|
|
1614
1954
|
check("git repo", () => {
|
|
1615
|
-
|
|
1955
|
+
execFileSync8("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
1616
1956
|
encoding: "utf-8",
|
|
1617
1957
|
timeout: 5e3,
|
|
1618
1958
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1619
1959
|
});
|
|
1620
1960
|
}),
|
|
1621
1961
|
check("pnpm", () => {
|
|
1622
|
-
const v =
|
|
1962
|
+
const v = execFileSync8("pnpm", ["--version"], {
|
|
1623
1963
|
encoding: "utf-8",
|
|
1624
1964
|
timeout: 5e3,
|
|
1625
1965
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1627,7 +1967,7 @@ function runPreflight() {
|
|
|
1627
1967
|
return v;
|
|
1628
1968
|
}),
|
|
1629
1969
|
check("node >= 18", () => {
|
|
1630
|
-
const v =
|
|
1970
|
+
const v = execFileSync8("node", ["--version"], {
|
|
1631
1971
|
encoding: "utf-8",
|
|
1632
1972
|
timeout: 5e3,
|
|
1633
1973
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1637,7 +1977,7 @@ function runPreflight() {
|
|
|
1637
1977
|
return v;
|
|
1638
1978
|
}),
|
|
1639
1979
|
check("gh CLI", () => {
|
|
1640
|
-
const v =
|
|
1980
|
+
const v = execFileSync8("gh", ["--version"], {
|
|
1641
1981
|
encoding: "utf-8",
|
|
1642
1982
|
timeout: 5e3,
|
|
1643
1983
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1645,7 +1985,7 @@ function runPreflight() {
|
|
|
1645
1985
|
return v;
|
|
1646
1986
|
}),
|
|
1647
1987
|
check("package.json", () => {
|
|
1648
|
-
if (!
|
|
1988
|
+
if (!fs14.existsSync("package.json")) throw new Error("not found");
|
|
1649
1989
|
})
|
|
1650
1990
|
];
|
|
1651
1991
|
const failed = checks.filter((c) => !c.ok);
|
|
@@ -1665,11 +2005,7 @@ var init_preflight = __esm({
|
|
|
1665
2005
|
}
|
|
1666
2006
|
});
|
|
1667
2007
|
|
|
1668
|
-
// src/
|
|
1669
|
-
var entry_exports = {};
|
|
1670
|
-
import * as fs6 from "fs";
|
|
1671
|
-
import * as path5 from "path";
|
|
1672
|
-
import { execFileSync as execFileSync7 } from "child_process";
|
|
2008
|
+
// src/cli/args.ts
|
|
1673
2009
|
function getArg(args2, flag) {
|
|
1674
2010
|
const idx = args2.indexOf(flag);
|
|
1675
2011
|
if (idx !== -1 && args2[idx + 1] && !args2[idx + 1].startsWith("--")) {
|
|
@@ -1711,15 +2047,92 @@ function parseArgs() {
|
|
|
1711
2047
|
complexity: getArg(args2, "--complexity") ?? process.env.COMPLEXITY
|
|
1712
2048
|
};
|
|
1713
2049
|
}
|
|
2050
|
+
var isCI2;
|
|
2051
|
+
var init_args = __esm({
|
|
2052
|
+
"src/cli/args.ts"() {
|
|
2053
|
+
"use strict";
|
|
2054
|
+
isCI2 = !!process.env.GITHUB_ACTIONS;
|
|
2055
|
+
}
|
|
2056
|
+
});
|
|
2057
|
+
|
|
2058
|
+
// src/cli/litellm.ts
|
|
2059
|
+
import * as fs15 from "fs";
|
|
2060
|
+
import * as path14 from "path";
|
|
2061
|
+
import { execFileSync as execFileSync9 } from "child_process";
|
|
2062
|
+
async function checkLitellmHealth(url) {
|
|
2063
|
+
try {
|
|
2064
|
+
const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
|
|
2065
|
+
return response.ok;
|
|
2066
|
+
} catch {
|
|
2067
|
+
return false;
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
async function tryStartLitellm(url, projectDir) {
|
|
2071
|
+
const configPath = path14.join(projectDir, "litellm-config.yaml");
|
|
2072
|
+
if (!fs15.existsSync(configPath)) {
|
|
2073
|
+
logger.warn("litellm-config.yaml not found \u2014 cannot start proxy");
|
|
2074
|
+
return null;
|
|
2075
|
+
}
|
|
2076
|
+
const portMatch = url.match(/:(\d+)/);
|
|
2077
|
+
const port = portMatch ? portMatch[1] : "4000";
|
|
2078
|
+
try {
|
|
2079
|
+
execFileSync9("litellm", ["--version"], { timeout: 5e3, stdio: "pipe" });
|
|
2080
|
+
} catch {
|
|
2081
|
+
try {
|
|
2082
|
+
execFileSync9("python3", ["-m", "litellm", "--version"], { timeout: 5e3, stdio: "pipe" });
|
|
2083
|
+
} catch {
|
|
2084
|
+
logger.warn("litellm not installed (pip install 'litellm[proxy]')");
|
|
2085
|
+
return null;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
logger.info(`Starting LiteLLM proxy on port ${port}...`);
|
|
2089
|
+
let cmd;
|
|
2090
|
+
let args2;
|
|
2091
|
+
try {
|
|
2092
|
+
execFileSync9("litellm", ["--version"], { timeout: 5e3, stdio: "pipe" });
|
|
2093
|
+
cmd = "litellm";
|
|
2094
|
+
args2 = ["--config", configPath, "--port", port];
|
|
2095
|
+
} catch {
|
|
2096
|
+
cmd = "python3";
|
|
2097
|
+
args2 = ["-m", "litellm", "--config", configPath, "--port", port];
|
|
2098
|
+
}
|
|
2099
|
+
const { spawn: spawn2 } = await import("child_process");
|
|
2100
|
+
const child = spawn2(cmd, args2, {
|
|
2101
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2102
|
+
detached: true,
|
|
2103
|
+
env: process.env
|
|
2104
|
+
});
|
|
2105
|
+
for (let i = 0; i < 30; i++) {
|
|
2106
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
2107
|
+
if (await checkLitellmHealth(url)) {
|
|
2108
|
+
logger.info(`LiteLLM proxy ready at ${url}`);
|
|
2109
|
+
return child;
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
logger.warn("LiteLLM proxy failed to start within 60s");
|
|
2113
|
+
child.kill();
|
|
2114
|
+
return null;
|
|
2115
|
+
}
|
|
2116
|
+
var init_litellm = __esm({
|
|
2117
|
+
"src/cli/litellm.ts"() {
|
|
2118
|
+
"use strict";
|
|
2119
|
+
init_logger();
|
|
2120
|
+
}
|
|
2121
|
+
});
|
|
2122
|
+
|
|
2123
|
+
// src/cli/task-resolution.ts
|
|
2124
|
+
import * as fs16 from "fs";
|
|
2125
|
+
import * as path15 from "path";
|
|
2126
|
+
import { execFileSync as execFileSync10 } from "child_process";
|
|
1714
2127
|
function findLatestTaskForIssue(issueNumber, projectDir) {
|
|
1715
|
-
const tasksDir =
|
|
1716
|
-
if (!
|
|
1717
|
-
const allDirs =
|
|
2128
|
+
const tasksDir = path15.join(projectDir, ".tasks");
|
|
2129
|
+
if (!fs16.existsSync(tasksDir)) return null;
|
|
2130
|
+
const allDirs = fs16.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
|
|
1718
2131
|
const prefix = `${issueNumber}-`;
|
|
1719
2132
|
const direct = allDirs.find((d) => d.startsWith(prefix));
|
|
1720
2133
|
if (direct) return direct;
|
|
1721
2134
|
try {
|
|
1722
|
-
const branch =
|
|
2135
|
+
const branch = execFileSync10("git", ["branch", "--show-current"], {
|
|
1723
2136
|
encoding: "utf-8",
|
|
1724
2137
|
cwd: projectDir,
|
|
1725
2138
|
timeout: 5e3,
|
|
@@ -1741,11 +2154,21 @@ function generateTaskId() {
|
|
|
1741
2154
|
const pad = (n) => String(n).padStart(2, "0");
|
|
1742
2155
|
return `${String(now.getFullYear()).slice(2)}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
1743
2156
|
}
|
|
2157
|
+
var init_task_resolution = __esm({
|
|
2158
|
+
"src/cli/task-resolution.ts"() {
|
|
2159
|
+
"use strict";
|
|
2160
|
+
}
|
|
2161
|
+
});
|
|
2162
|
+
|
|
2163
|
+
// src/entry.ts
|
|
2164
|
+
var entry_exports = {};
|
|
2165
|
+
import * as fs17 from "fs";
|
|
2166
|
+
import * as path16 from "path";
|
|
1744
2167
|
async function main() {
|
|
1745
2168
|
const input = parseArgs();
|
|
1746
|
-
const projectDir = input.cwd ?
|
|
2169
|
+
const projectDir = input.cwd ? path16.resolve(input.cwd) : process.cwd();
|
|
1747
2170
|
if (input.cwd) {
|
|
1748
|
-
if (!
|
|
2171
|
+
if (!fs17.existsSync(projectDir)) {
|
|
1749
2172
|
console.error(`--cwd path does not exist: ${projectDir}`);
|
|
1750
2173
|
process.exit(1);
|
|
1751
2174
|
}
|
|
@@ -1772,8 +2195,8 @@ async function main() {
|
|
|
1772
2195
|
process.exit(1);
|
|
1773
2196
|
}
|
|
1774
2197
|
}
|
|
1775
|
-
const taskDir =
|
|
1776
|
-
|
|
2198
|
+
const taskDir = path16.join(projectDir, ".tasks", taskId);
|
|
2199
|
+
fs17.mkdirSync(taskDir, { recursive: true });
|
|
1777
2200
|
if (input.command === "status") {
|
|
1778
2201
|
printStatus(taskId, taskDir);
|
|
1779
2202
|
return;
|
|
@@ -1781,22 +2204,22 @@ async function main() {
|
|
|
1781
2204
|
logger.info("Preflight checks:");
|
|
1782
2205
|
runPreflight();
|
|
1783
2206
|
if (input.task) {
|
|
1784
|
-
|
|
2207
|
+
fs17.writeFileSync(path16.join(taskDir, "task.md"), input.task);
|
|
1785
2208
|
}
|
|
1786
|
-
const taskMdPath =
|
|
1787
|
-
if (!
|
|
2209
|
+
const taskMdPath = path16.join(taskDir, "task.md");
|
|
2210
|
+
if (!fs17.existsSync(taskMdPath) && input.issueNumber) {
|
|
1788
2211
|
logger.info(`Fetching issue #${input.issueNumber} body as task...`);
|
|
1789
2212
|
const issue = getIssue(input.issueNumber);
|
|
1790
2213
|
if (issue) {
|
|
1791
2214
|
const taskContent = `# ${issue.title}
|
|
1792
2215
|
|
|
1793
2216
|
${issue.body ?? ""}`;
|
|
1794
|
-
|
|
2217
|
+
fs17.writeFileSync(taskMdPath, taskContent);
|
|
1795
2218
|
logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
|
|
1796
2219
|
}
|
|
1797
2220
|
}
|
|
1798
2221
|
if (input.command === "run") {
|
|
1799
|
-
if (!
|
|
2222
|
+
if (!fs17.existsSync(taskMdPath)) {
|
|
1800
2223
|
console.error("No task.md found. Provide --task, --issue-number, or ensure .tasks/<id>/task.md exists.");
|
|
1801
2224
|
process.exit(1);
|
|
1802
2225
|
}
|
|
@@ -1805,10 +2228,10 @@ ${issue.body ?? ""}`;
|
|
|
1805
2228
|
input.fromStage = "build";
|
|
1806
2229
|
}
|
|
1807
2230
|
if (input.command === "rerun" && !input.fromStage) {
|
|
1808
|
-
const statusPath =
|
|
1809
|
-
if (
|
|
2231
|
+
const statusPath = path16.join(taskDir, "status.json");
|
|
2232
|
+
if (fs17.existsSync(statusPath)) {
|
|
1810
2233
|
try {
|
|
1811
|
-
const status = JSON.parse(
|
|
2234
|
+
const status = JSON.parse(fs17.readFileSync(statusPath, "utf-8"));
|
|
1812
2235
|
const stageNames = ["taskify", "plan", "build", "verify", "review", "review-fix", "ship"];
|
|
1813
2236
|
let foundPaused = false;
|
|
1814
2237
|
for (const name of stageNames) {
|
|
@@ -1843,6 +2266,34 @@ ${issue.body ?? ""}`;
|
|
|
1843
2266
|
}
|
|
1844
2267
|
}
|
|
1845
2268
|
const config = getProjectConfig();
|
|
2269
|
+
let litellmProcess = null;
|
|
2270
|
+
const cleanupLitellm = () => {
|
|
2271
|
+
if (litellmProcess) {
|
|
2272
|
+
litellmProcess.kill();
|
|
2273
|
+
litellmProcess = null;
|
|
2274
|
+
}
|
|
2275
|
+
};
|
|
2276
|
+
process.on("exit", cleanupLitellm);
|
|
2277
|
+
process.on("SIGINT", () => {
|
|
2278
|
+
cleanupLitellm();
|
|
2279
|
+
process.exit(130);
|
|
2280
|
+
});
|
|
2281
|
+
process.on("SIGTERM", () => {
|
|
2282
|
+
cleanupLitellm();
|
|
2283
|
+
process.exit(143);
|
|
2284
|
+
});
|
|
2285
|
+
if (config.agent.litellmUrl) {
|
|
2286
|
+
const proxyRunning = await checkLitellmHealth(config.agent.litellmUrl);
|
|
2287
|
+
if (!proxyRunning) {
|
|
2288
|
+
litellmProcess = await tryStartLitellm(config.agent.litellmUrl, projectDir);
|
|
2289
|
+
if (!litellmProcess) {
|
|
2290
|
+
logger.warn("LiteLLM not available \u2014 falling back to Anthropic models");
|
|
2291
|
+
config.agent.litellmUrl = void 0;
|
|
2292
|
+
}
|
|
2293
|
+
} else {
|
|
2294
|
+
logger.info(`LiteLLM proxy already running at ${config.agent.litellmUrl}`);
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
1846
2297
|
const runners = createRunners(config);
|
|
1847
2298
|
const defaultRunnerName = config.agent.defaultRunner ?? Object.keys(runners)[0] ?? "claude";
|
|
1848
2299
|
const defaultRunner = runners[defaultRunnerName];
|
|
@@ -1887,7 +2338,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
|
|
|
1887
2338
|
}
|
|
1888
2339
|
}
|
|
1889
2340
|
const state = await runPipeline(ctx);
|
|
1890
|
-
const files =
|
|
2341
|
+
const files = fs17.readdirSync(taskDir);
|
|
1891
2342
|
console.log(`
|
|
1892
2343
|
Artifacts in ${taskDir}:`);
|
|
1893
2344
|
for (const f of files) {
|
|
@@ -1895,7 +2346,7 @@ Artifacts in ${taskDir}:`);
|
|
|
1895
2346
|
}
|
|
1896
2347
|
if (state.state === "failed") {
|
|
1897
2348
|
const isPaused = Object.values(state.stages).some(
|
|
1898
|
-
(s) =>
|
|
2349
|
+
(s) => s.error?.includes("paused") ?? false
|
|
1899
2350
|
);
|
|
1900
2351
|
if (isPaused) {
|
|
1901
2352
|
process.exit(0);
|
|
@@ -1917,17 +2368,18 @@ Artifacts in ${taskDir}:`);
|
|
|
1917
2368
|
process.exit(1);
|
|
1918
2369
|
}
|
|
1919
2370
|
}
|
|
1920
|
-
var isCI2;
|
|
1921
2371
|
var init_entry = __esm({
|
|
1922
2372
|
"src/entry.ts"() {
|
|
1923
2373
|
"use strict";
|
|
1924
2374
|
init_agent_runner();
|
|
1925
|
-
|
|
2375
|
+
init_pipeline();
|
|
1926
2376
|
init_preflight();
|
|
1927
2377
|
init_config();
|
|
1928
2378
|
init_github_api();
|
|
1929
2379
|
init_logger();
|
|
1930
|
-
|
|
2380
|
+
init_args();
|
|
2381
|
+
init_litellm();
|
|
2382
|
+
init_task_resolution();
|
|
1931
2383
|
main().catch(async (err) => {
|
|
1932
2384
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1933
2385
|
console.error(msg);
|
|
@@ -1945,20 +2397,20 @@ var init_entry = __esm({
|
|
|
1945
2397
|
});
|
|
1946
2398
|
|
|
1947
2399
|
// src/bin/cli.ts
|
|
1948
|
-
import * as
|
|
1949
|
-
import * as
|
|
1950
|
-
import { execFileSync as
|
|
2400
|
+
import * as fs18 from "fs";
|
|
2401
|
+
import * as path17 from "path";
|
|
2402
|
+
import { execFileSync as execFileSync11 } from "child_process";
|
|
1951
2403
|
import { fileURLToPath } from "url";
|
|
1952
|
-
var __dirname =
|
|
1953
|
-
var PKG_ROOT =
|
|
2404
|
+
var __dirname = path17.dirname(fileURLToPath(import.meta.url));
|
|
2405
|
+
var PKG_ROOT = path17.resolve(__dirname, "..", "..");
|
|
1954
2406
|
function getVersion() {
|
|
1955
|
-
const pkgPath =
|
|
1956
|
-
const pkg = JSON.parse(
|
|
2407
|
+
const pkgPath = path17.join(PKG_ROOT, "package.json");
|
|
2408
|
+
const pkg = JSON.parse(fs18.readFileSync(pkgPath, "utf-8"));
|
|
1957
2409
|
return pkg.version;
|
|
1958
2410
|
}
|
|
1959
2411
|
function checkCommand2(name, args2, fix) {
|
|
1960
2412
|
try {
|
|
1961
|
-
const output =
|
|
2413
|
+
const output = execFileSync11(name, args2, {
|
|
1962
2414
|
encoding: "utf-8",
|
|
1963
2415
|
timeout: 1e4,
|
|
1964
2416
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1969,14 +2421,14 @@ function checkCommand2(name, args2, fix) {
|
|
|
1969
2421
|
}
|
|
1970
2422
|
}
|
|
1971
2423
|
function checkFile(filePath, description, fix) {
|
|
1972
|
-
if (
|
|
2424
|
+
if (fs18.existsSync(filePath)) {
|
|
1973
2425
|
return { name: description, ok: true, detail: filePath };
|
|
1974
2426
|
}
|
|
1975
2427
|
return { name: description, ok: false, fix };
|
|
1976
2428
|
}
|
|
1977
2429
|
function checkGhAuth(cwd) {
|
|
1978
2430
|
try {
|
|
1979
|
-
const output =
|
|
2431
|
+
const output = execFileSync11("gh", ["auth", "status"], {
|
|
1980
2432
|
encoding: "utf-8",
|
|
1981
2433
|
timeout: 1e4,
|
|
1982
2434
|
cwd,
|
|
@@ -1985,7 +2437,7 @@ function checkGhAuth(cwd) {
|
|
|
1985
2437
|
const account = output.match(/Logged in to .* account (\S+)/)?.[1];
|
|
1986
2438
|
return { name: "gh auth", ok: true, detail: account ?? "authenticated" };
|
|
1987
2439
|
} catch (err) {
|
|
1988
|
-
const stderr = err.stderr ?? "";
|
|
2440
|
+
const stderr = err instanceof Error && "stderr" in err ? String(err.stderr ?? "") : "";
|
|
1989
2441
|
if (stderr.includes("not logged")) {
|
|
1990
2442
|
return { name: "gh auth", ok: false, fix: "Run: gh auth login" };
|
|
1991
2443
|
}
|
|
@@ -1994,7 +2446,7 @@ function checkGhAuth(cwd) {
|
|
|
1994
2446
|
}
|
|
1995
2447
|
function checkGhRepoAccess(cwd) {
|
|
1996
2448
|
try {
|
|
1997
|
-
const remote =
|
|
2449
|
+
const remote = execFileSync11("git", ["remote", "get-url", "origin"], {
|
|
1998
2450
|
encoding: "utf-8",
|
|
1999
2451
|
timeout: 5e3,
|
|
2000
2452
|
cwd,
|
|
@@ -2005,7 +2457,7 @@ function checkGhRepoAccess(cwd) {
|
|
|
2005
2457
|
return { name: "GitHub repo", ok: false, fix: "Set git remote origin to a GitHub URL" };
|
|
2006
2458
|
}
|
|
2007
2459
|
const repoSlug = `${match[1]}/${match[2]}`;
|
|
2008
|
-
|
|
2460
|
+
execFileSync11("gh", ["repo", "view", repoSlug, "--json", "name"], {
|
|
2009
2461
|
encoding: "utf-8",
|
|
2010
2462
|
timeout: 1e4,
|
|
2011
2463
|
cwd,
|
|
@@ -2018,7 +2470,7 @@ function checkGhRepoAccess(cwd) {
|
|
|
2018
2470
|
}
|
|
2019
2471
|
function checkGhSecret(repoSlug, secretName) {
|
|
2020
2472
|
try {
|
|
2021
|
-
const output =
|
|
2473
|
+
const output = execFileSync11("gh", ["secret", "list", "--repo", repoSlug], {
|
|
2022
2474
|
encoding: "utf-8",
|
|
2023
2475
|
timeout: 1e4,
|
|
2024
2476
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -2041,10 +2493,10 @@ function checkGhSecret(repoSlug, secretName) {
|
|
|
2041
2493
|
}
|
|
2042
2494
|
function detectArchitecture(cwd) {
|
|
2043
2495
|
const detected = [];
|
|
2044
|
-
const pkgPath =
|
|
2045
|
-
if (
|
|
2496
|
+
const pkgPath = path17.join(cwd, "package.json");
|
|
2497
|
+
if (fs18.existsSync(pkgPath)) {
|
|
2046
2498
|
try {
|
|
2047
|
-
const pkg = JSON.parse(
|
|
2499
|
+
const pkg = JSON.parse(fs18.readFileSync(pkgPath, "utf-8"));
|
|
2048
2500
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2049
2501
|
if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
|
|
2050
2502
|
else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
|
|
@@ -2067,44 +2519,44 @@ function detectArchitecture(cwd) {
|
|
|
2067
2519
|
if (allDeps.tailwindcss) detected.push(`- CSS: Tailwind CSS ${allDeps.tailwindcss}`);
|
|
2068
2520
|
if (pkg.type === "module") detected.push("- Module system: ESM");
|
|
2069
2521
|
else detected.push("- Module system: CommonJS");
|
|
2070
|
-
if (
|
|
2071
|
-
else if (
|
|
2072
|
-
else if (
|
|
2073
|
-
else if (
|
|
2522
|
+
if (fs18.existsSync(path17.join(cwd, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
|
|
2523
|
+
else if (fs18.existsSync(path17.join(cwd, "yarn.lock"))) detected.push("- Package manager: yarn");
|
|
2524
|
+
else if (fs18.existsSync(path17.join(cwd, "bun.lockb"))) detected.push("- Package manager: bun");
|
|
2525
|
+
else if (fs18.existsSync(path17.join(cwd, "package-lock.json"))) detected.push("- Package manager: npm");
|
|
2074
2526
|
} catch {
|
|
2075
2527
|
}
|
|
2076
2528
|
}
|
|
2077
2529
|
try {
|
|
2078
|
-
const entries =
|
|
2530
|
+
const entries = fs18.readdirSync(cwd, { withFileTypes: true });
|
|
2079
2531
|
const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
|
|
2080
2532
|
if (dirs.length > 0) detected.push(`- Top-level directories: ${dirs.join(", ")}`);
|
|
2081
2533
|
} catch {
|
|
2082
2534
|
}
|
|
2083
|
-
const srcDir =
|
|
2084
|
-
if (
|
|
2535
|
+
const srcDir = path17.join(cwd, "src");
|
|
2536
|
+
if (fs18.existsSync(srcDir)) {
|
|
2085
2537
|
try {
|
|
2086
|
-
const srcEntries =
|
|
2538
|
+
const srcEntries = fs18.readdirSync(srcDir, { withFileTypes: true });
|
|
2087
2539
|
const srcDirs = srcEntries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
2088
2540
|
if (srcDirs.length > 0) detected.push(`- src/ structure: ${srcDirs.join(", ")}`);
|
|
2089
2541
|
} catch {
|
|
2090
2542
|
}
|
|
2091
2543
|
}
|
|
2092
2544
|
const configs = [];
|
|
2093
|
-
if (
|
|
2094
|
-
if (
|
|
2095
|
-
if (
|
|
2096
|
-
if (
|
|
2545
|
+
if (fs18.existsSync(path17.join(cwd, "tsconfig.json"))) configs.push("tsconfig.json");
|
|
2546
|
+
if (fs18.existsSync(path17.join(cwd, "docker-compose.yml")) || fs18.existsSync(path17.join(cwd, "docker-compose.yaml"))) configs.push("docker-compose");
|
|
2547
|
+
if (fs18.existsSync(path17.join(cwd, "Dockerfile"))) configs.push("Dockerfile");
|
|
2548
|
+
if (fs18.existsSync(path17.join(cwd, ".env")) || fs18.existsSync(path17.join(cwd, ".env.local"))) configs.push(".env");
|
|
2097
2549
|
if (configs.length > 0) detected.push(`- Config files: ${configs.join(", ")}`);
|
|
2098
2550
|
return detected;
|
|
2099
2551
|
}
|
|
2100
2552
|
function detectBasicConfig(cwd) {
|
|
2101
2553
|
let pm = "pnpm";
|
|
2102
|
-
if (
|
|
2103
|
-
else if (
|
|
2104
|
-
else if (!
|
|
2554
|
+
if (fs18.existsSync(path17.join(cwd, "yarn.lock"))) pm = "yarn";
|
|
2555
|
+
else if (fs18.existsSync(path17.join(cwd, "bun.lockb"))) pm = "bun";
|
|
2556
|
+
else if (!fs18.existsSync(path17.join(cwd, "pnpm-lock.yaml")) && fs18.existsSync(path17.join(cwd, "package-lock.json"))) pm = "npm";
|
|
2105
2557
|
let defaultBranch = "main";
|
|
2106
2558
|
try {
|
|
2107
|
-
const ref =
|
|
2559
|
+
const ref = execFileSync11("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
|
|
2108
2560
|
encoding: "utf-8",
|
|
2109
2561
|
timeout: 5e3,
|
|
2110
2562
|
cwd,
|
|
@@ -2113,7 +2565,7 @@ function detectBasicConfig(cwd) {
|
|
|
2113
2565
|
defaultBranch = ref.replace("refs/remotes/origin/", "");
|
|
2114
2566
|
} catch {
|
|
2115
2567
|
try {
|
|
2116
|
-
|
|
2568
|
+
execFileSync11("git", ["rev-parse", "--verify", "origin/dev"], {
|
|
2117
2569
|
encoding: "utf-8",
|
|
2118
2570
|
timeout: 5e3,
|
|
2119
2571
|
cwd,
|
|
@@ -2126,7 +2578,7 @@ function detectBasicConfig(cwd) {
|
|
|
2126
2578
|
let owner = "";
|
|
2127
2579
|
let repo = "";
|
|
2128
2580
|
try {
|
|
2129
|
-
const remote =
|
|
2581
|
+
const remote = execFileSync11("git", ["remote", "get-url", "origin"], {
|
|
2130
2582
|
encoding: "utf-8",
|
|
2131
2583
|
timeout: 5e3,
|
|
2132
2584
|
cwd,
|
|
@@ -2139,16 +2591,15 @@ function detectBasicConfig(cwd) {
|
|
|
2139
2591
|
}
|
|
2140
2592
|
} catch {
|
|
2141
2593
|
}
|
|
2142
|
-
|
|
2143
|
-
return { defaultBranch, owner, repo, pm, hasOpenCode };
|
|
2594
|
+
return { defaultBranch, owner, repo, pm };
|
|
2144
2595
|
}
|
|
2145
2596
|
function smartInit(cwd) {
|
|
2146
2597
|
const basic = detectBasicConfig(cwd);
|
|
2147
2598
|
let context = "";
|
|
2148
2599
|
const readIfExists = (rel, maxChars = 3e3) => {
|
|
2149
|
-
const p =
|
|
2150
|
-
if (
|
|
2151
|
-
const content =
|
|
2600
|
+
const p = path17.join(cwd, rel);
|
|
2601
|
+
if (fs18.existsSync(p)) {
|
|
2602
|
+
const content = fs18.readFileSync(p, "utf-8");
|
|
2152
2603
|
return content.slice(0, maxChars);
|
|
2153
2604
|
}
|
|
2154
2605
|
return null;
|
|
@@ -2174,14 +2625,14 @@ ${claudeMd}
|
|
|
2174
2625
|
|
|
2175
2626
|
`;
|
|
2176
2627
|
try {
|
|
2177
|
-
const topDirs =
|
|
2628
|
+
const topDirs = fs18.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
|
|
2178
2629
|
context += `## Top-level directories
|
|
2179
2630
|
${topDirs.join(", ")}
|
|
2180
2631
|
|
|
2181
2632
|
`;
|
|
2182
|
-
const srcDir =
|
|
2183
|
-
if (
|
|
2184
|
-
const srcDirs =
|
|
2633
|
+
const srcDir = path17.join(cwd, "src");
|
|
2634
|
+
if (fs18.existsSync(srcDir)) {
|
|
2635
|
+
const srcDirs = fs18.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
2185
2636
|
context += `## src/ subdirectories
|
|
2186
2637
|
${srcDirs.join(", ")}
|
|
2187
2638
|
|
|
@@ -2190,14 +2641,14 @@ ${srcDirs.join(", ")}
|
|
|
2190
2641
|
} catch {
|
|
2191
2642
|
}
|
|
2192
2643
|
const existingFiles = [];
|
|
2193
|
-
for (const f of [".env.example", "CLAUDE.md", ".ai-docs", "
|
|
2194
|
-
if (
|
|
2644
|
+
for (const f of [".env.example", "CLAUDE.md", ".ai-docs", "vitest.config.ts", "vitest.config.mts", "jest.config.ts", "playwright.config.ts", ".eslintrc.js", "eslint.config.mjs", ".prettierrc"]) {
|
|
2645
|
+
if (fs18.existsSync(path17.join(cwd, f))) existingFiles.push(f);
|
|
2195
2646
|
}
|
|
2196
2647
|
if (existingFiles.length) context += `## Config files present
|
|
2197
2648
|
${existingFiles.join(", ")}
|
|
2198
2649
|
|
|
2199
2650
|
`;
|
|
2200
|
-
context += `## Detected: package manager=${basic.pm}, default branch=${basic.defaultBranch}, github=${basic.owner}/${basic.repo}
|
|
2651
|
+
context += `## Detected: package manager=${basic.pm}, default branch=${basic.defaultBranch}, github=${basic.owner}/${basic.repo}
|
|
2201
2652
|
`;
|
|
2202
2653
|
const prompt = `You are analyzing a project to configure Kody (an autonomous SDLC pipeline).
|
|
2203
2654
|
|
|
@@ -2217,8 +2668,8 @@ Given this project context, output ONLY a JSON object with EXACTLY this structur
|
|
|
2217
2668
|
"github": { "owner": "${basic.owner}", "repo": "${basic.repo}" },
|
|
2218
2669
|
"paths": { "taskDir": ".tasks" },
|
|
2219
2670
|
"agent": {
|
|
2220
|
-
"runner": "${
|
|
2221
|
-
"defaultRunner": "${
|
|
2671
|
+
"runner": "${"claude-code"}",
|
|
2672
|
+
"defaultRunner": "${"claude"}",
|
|
2222
2673
|
"modelMap": { "cheap": "haiku", "mid": "sonnet", "strong": "opus" }
|
|
2223
2674
|
}
|
|
2224
2675
|
},
|
|
@@ -2250,7 +2701,7 @@ Output ONLY valid JSON. No markdown fences. No explanation before or after.
|
|
|
2250
2701
|
${context}`;
|
|
2251
2702
|
console.log(" \u23F3 Analyzing project with Claude Code...");
|
|
2252
2703
|
try {
|
|
2253
|
-
const output =
|
|
2704
|
+
const output = execFileSync11("claude", [
|
|
2254
2705
|
"--print",
|
|
2255
2706
|
"--model",
|
|
2256
2707
|
"haiku",
|
|
@@ -2269,12 +2720,13 @@ ${context}`;
|
|
|
2269
2720
|
if (!config.github) config.github = {};
|
|
2270
2721
|
if (!config.paths) config.paths = {};
|
|
2271
2722
|
if (!config.agent) config.agent = {};
|
|
2723
|
+
config["$schema"] = "https://raw.githubusercontent.com/aharonyaircohen/Kody-Engine-Lite/main/kody.config.schema.json";
|
|
2272
2724
|
config.git.defaultBranch = config.git.defaultBranch || basic.defaultBranch;
|
|
2273
2725
|
config.github.owner = config.github.owner || basic.owner;
|
|
2274
2726
|
config.github.repo = config.github.repo || basic.repo;
|
|
2275
2727
|
config.paths.taskDir = config.paths.taskDir || ".tasks";
|
|
2276
|
-
config.agent.runner = config.agent.runner ||
|
|
2277
|
-
config.agent.defaultRunner = config.agent.defaultRunner ||
|
|
2728
|
+
config.agent.runner = config.agent.runner || "claude-code";
|
|
2729
|
+
config.agent.defaultRunner = config.agent.defaultRunner || "claude";
|
|
2278
2730
|
if (!config.agent.modelMap) {
|
|
2279
2731
|
config.agent.modelMap = { cheap: "haiku", mid: "sonnet", strong: "opus" };
|
|
2280
2732
|
}
|
|
@@ -2296,7 +2748,7 @@ ${context}`;
|
|
|
2296
2748
|
function validateQualityCommands(cwd, config, pm) {
|
|
2297
2749
|
let scripts = {};
|
|
2298
2750
|
try {
|
|
2299
|
-
const pkg = JSON.parse(
|
|
2751
|
+
const pkg = JSON.parse(fs18.readFileSync(path17.join(cwd, "package.json"), "utf-8"));
|
|
2300
2752
|
scripts = pkg.scripts ?? {};
|
|
2301
2753
|
} catch {
|
|
2302
2754
|
return;
|
|
@@ -2330,7 +2782,7 @@ function validateQualityCommands(cwd, config, pm) {
|
|
|
2330
2782
|
function buildFallbackConfig(cwd, basic) {
|
|
2331
2783
|
const pkg = (() => {
|
|
2332
2784
|
try {
|
|
2333
|
-
return JSON.parse(
|
|
2785
|
+
return JSON.parse(fs18.readFileSync(path17.join(cwd, "package.json"), "utf-8"));
|
|
2334
2786
|
} catch {
|
|
2335
2787
|
return {};
|
|
2336
2788
|
}
|
|
@@ -2343,6 +2795,7 @@ function buildFallbackConfig(cwd, basic) {
|
|
|
2343
2795
|
return "";
|
|
2344
2796
|
};
|
|
2345
2797
|
return {
|
|
2798
|
+
"$schema": "https://raw.githubusercontent.com/aharonyaircohen/Kody-Engine-Lite/main/kody.config.schema.json",
|
|
2346
2799
|
quality: {
|
|
2347
2800
|
typecheck: find("typecheck", "type-check") || (pkg.devDependencies?.typescript ? `${basic.pm} tsc --noEmit` : ""),
|
|
2348
2801
|
lint: find("lint"),
|
|
@@ -2355,8 +2808,8 @@ function buildFallbackConfig(cwd, basic) {
|
|
|
2355
2808
|
github: { owner: basic.owner, repo: basic.repo },
|
|
2356
2809
|
paths: { taskDir: ".tasks" },
|
|
2357
2810
|
agent: {
|
|
2358
|
-
runner:
|
|
2359
|
-
defaultRunner:
|
|
2811
|
+
runner: "claude-code",
|
|
2812
|
+
defaultRunner: "claude",
|
|
2360
2813
|
modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
|
|
2361
2814
|
}
|
|
2362
2815
|
};
|
|
@@ -2369,34 +2822,34 @@ function initCommand(opts) {
|
|
|
2369
2822
|
console.log(`Project: ${cwd}
|
|
2370
2823
|
`);
|
|
2371
2824
|
console.log("\u2500\u2500 Files \u2500\u2500");
|
|
2372
|
-
const templatesDir =
|
|
2373
|
-
const workflowSrc =
|
|
2374
|
-
const workflowDest =
|
|
2375
|
-
if (!
|
|
2825
|
+
const templatesDir = path17.join(PKG_ROOT, "templates");
|
|
2826
|
+
const workflowSrc = path17.join(templatesDir, "kody.yml");
|
|
2827
|
+
const workflowDest = path17.join(cwd, ".github", "workflows", "kody.yml");
|
|
2828
|
+
if (!fs18.existsSync(workflowSrc)) {
|
|
2376
2829
|
console.error(" \u2717 Template kody.yml not found in package");
|
|
2377
2830
|
process.exit(1);
|
|
2378
2831
|
}
|
|
2379
|
-
if (
|
|
2832
|
+
if (fs18.existsSync(workflowDest) && !opts.force) {
|
|
2380
2833
|
console.log(" \u25CB .github/workflows/kody.yml (exists, use --force to overwrite)");
|
|
2381
2834
|
} else {
|
|
2382
|
-
|
|
2383
|
-
|
|
2835
|
+
fs18.mkdirSync(path17.dirname(workflowDest), { recursive: true });
|
|
2836
|
+
fs18.copyFileSync(workflowSrc, workflowDest);
|
|
2384
2837
|
console.log(" \u2713 .github/workflows/kody.yml");
|
|
2385
2838
|
}
|
|
2386
|
-
const configDest =
|
|
2839
|
+
const configDest = path17.join(cwd, "kody.config.json");
|
|
2387
2840
|
let smartResult = null;
|
|
2388
|
-
if (!
|
|
2841
|
+
if (!fs18.existsSync(configDest) || opts.force) {
|
|
2389
2842
|
smartResult = smartInit(cwd);
|
|
2390
|
-
|
|
2843
|
+
fs18.writeFileSync(configDest, JSON.stringify(smartResult.config, null, 2) + "\n");
|
|
2391
2844
|
console.log(" \u2713 kody.config.json (auto-configured)");
|
|
2392
2845
|
} else {
|
|
2393
2846
|
console.log(" \u25CB kody.config.json (exists)");
|
|
2394
2847
|
}
|
|
2395
|
-
const gitignorePath =
|
|
2396
|
-
if (
|
|
2397
|
-
const content =
|
|
2848
|
+
const gitignorePath = path17.join(cwd, ".gitignore");
|
|
2849
|
+
if (fs18.existsSync(gitignorePath)) {
|
|
2850
|
+
const content = fs18.readFileSync(gitignorePath, "utf-8");
|
|
2398
2851
|
if (!content.includes(".tasks/")) {
|
|
2399
|
-
|
|
2852
|
+
fs18.appendFileSync(gitignorePath, "\n.tasks/\n");
|
|
2400
2853
|
console.log(" \u2713 .gitignore (added .tasks/)");
|
|
2401
2854
|
} else {
|
|
2402
2855
|
console.log(" \u25CB .gitignore (.tasks/ already present)");
|
|
@@ -2409,7 +2862,7 @@ function initCommand(opts) {
|
|
|
2409
2862
|
checkCommand2("git", ["--version"], "Install git"),
|
|
2410
2863
|
checkCommand2("node", ["--version"], "Install Node.js >= 22"),
|
|
2411
2864
|
checkCommand2("pnpm", ["--version"], "Install: npm i -g pnpm"),
|
|
2412
|
-
checkFile(
|
|
2865
|
+
checkFile(path17.join(cwd, "package.json"), "package.json", "Run: pnpm init")
|
|
2413
2866
|
];
|
|
2414
2867
|
for (const c of checks) {
|
|
2415
2868
|
if (c.ok) {
|
|
@@ -2454,7 +2907,7 @@ function initCommand(opts) {
|
|
|
2454
2907
|
console.log("\n\u2500\u2500 Labels \u2500\u2500");
|
|
2455
2908
|
for (const label of labels) {
|
|
2456
2909
|
try {
|
|
2457
|
-
|
|
2910
|
+
execFileSync11("gh", [
|
|
2458
2911
|
"label",
|
|
2459
2912
|
"create",
|
|
2460
2913
|
label.name,
|
|
@@ -2473,7 +2926,7 @@ function initCommand(opts) {
|
|
|
2473
2926
|
console.log(` \u2713 ${label.name}`);
|
|
2474
2927
|
} catch {
|
|
2475
2928
|
try {
|
|
2476
|
-
|
|
2929
|
+
execFileSync11("gh", ["label", "list", "--repo", repoSlug, "--search", label.name], {
|
|
2477
2930
|
encoding: "utf-8",
|
|
2478
2931
|
timeout: 1e4,
|
|
2479
2932
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -2486,9 +2939,9 @@ function initCommand(opts) {
|
|
|
2486
2939
|
}
|
|
2487
2940
|
}
|
|
2488
2941
|
console.log("\n\u2500\u2500 Config \u2500\u2500");
|
|
2489
|
-
if (
|
|
2942
|
+
if (fs18.existsSync(configDest)) {
|
|
2490
2943
|
try {
|
|
2491
|
-
const config = JSON.parse(
|
|
2944
|
+
const config = JSON.parse(fs18.readFileSync(configDest, "utf-8"));
|
|
2492
2945
|
const configChecks = [];
|
|
2493
2946
|
if (config.github?.owner && config.github?.repo) {
|
|
2494
2947
|
configChecks.push({ name: "github.owner/repo", ok: true, detail: `${config.github.owner}/${config.github.repo}` });
|
|
@@ -2515,21 +2968,21 @@ function initCommand(opts) {
|
|
|
2515
2968
|
}
|
|
2516
2969
|
}
|
|
2517
2970
|
console.log("\n\u2500\u2500 Project Memory \u2500\u2500");
|
|
2518
|
-
const memoryDir =
|
|
2519
|
-
|
|
2520
|
-
const archPath =
|
|
2521
|
-
const conventionsPath =
|
|
2522
|
-
if (
|
|
2971
|
+
const memoryDir = path17.join(cwd, ".kody", "memory");
|
|
2972
|
+
fs18.mkdirSync(memoryDir, { recursive: true });
|
|
2973
|
+
const archPath = path17.join(memoryDir, "architecture.md");
|
|
2974
|
+
const conventionsPath = path17.join(memoryDir, "conventions.md");
|
|
2975
|
+
if (fs18.existsSync(archPath) && !opts.force) {
|
|
2523
2976
|
console.log(" \u25CB .kody/memory/architecture.md (exists, use --force to regenerate)");
|
|
2524
2977
|
} else if (smartResult?.architecture) {
|
|
2525
|
-
|
|
2978
|
+
fs18.writeFileSync(archPath, smartResult.architecture);
|
|
2526
2979
|
const lineCount = smartResult.architecture.split("\n").length;
|
|
2527
2980
|
console.log(` \u2713 .kody/memory/architecture.md (${lineCount} lines, LLM-generated)`);
|
|
2528
2981
|
} else {
|
|
2529
2982
|
const archItems = detectArchitecture(cwd);
|
|
2530
2983
|
if (archItems.length > 0) {
|
|
2531
2984
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2532
|
-
|
|
2985
|
+
fs18.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
|
|
2533
2986
|
|
|
2534
2987
|
## Overview
|
|
2535
2988
|
${archItems.join("\n")}
|
|
@@ -2539,14 +2992,14 @@ ${archItems.join("\n")}
|
|
|
2539
2992
|
console.log(" \u25CB No architecture detected");
|
|
2540
2993
|
}
|
|
2541
2994
|
}
|
|
2542
|
-
if (
|
|
2995
|
+
if (fs18.existsSync(conventionsPath) && !opts.force) {
|
|
2543
2996
|
console.log(" \u25CB .kody/memory/conventions.md (exists, use --force to regenerate)");
|
|
2544
2997
|
} else if (smartResult?.conventions) {
|
|
2545
|
-
|
|
2998
|
+
fs18.writeFileSync(conventionsPath, smartResult.conventions);
|
|
2546
2999
|
const lineCount = smartResult.conventions.split("\n").length;
|
|
2547
3000
|
console.log(` \u2713 .kody/memory/conventions.md (${lineCount} lines, LLM-generated)`);
|
|
2548
3001
|
} else {
|
|
2549
|
-
|
|
3002
|
+
fs18.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
|
|
2550
3003
|
console.log(" \u2713 .kody/memory/conventions.md (seed)");
|
|
2551
3004
|
}
|
|
2552
3005
|
const allChecks = [...checks, ghAuth, ghRepo];
|