@kody-ade/kody-engine-lite 0.1.21 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/cli.js +1402 -990
- package/package.json +1 -1
- package/prompts/review.md +2 -0
package/dist/bin/cli.js
CHANGED
|
@@ -199,251 +199,6 @@ var init_definitions = __esm({
|
|
|
199
199
|
}
|
|
200
200
|
});
|
|
201
201
|
|
|
202
|
-
// src/memory.ts
|
|
203
|
-
import * as fs from "fs";
|
|
204
|
-
import * as path from "path";
|
|
205
|
-
function readProjectMemory(projectDir) {
|
|
206
|
-
const memoryDir = path.join(projectDir, ".kody", "memory");
|
|
207
|
-
if (!fs.existsSync(memoryDir)) return "";
|
|
208
|
-
const files = fs.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
209
|
-
if (files.length === 0) return "";
|
|
210
|
-
const sections = [];
|
|
211
|
-
for (const file of files) {
|
|
212
|
-
const content = fs.readFileSync(path.join(memoryDir, file), "utf-8").trim();
|
|
213
|
-
if (content) {
|
|
214
|
-
sections.push(`## ${file.replace(".md", "")}
|
|
215
|
-
${content}`);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
if (sections.length === 0) return "";
|
|
219
|
-
return `# Project Memory
|
|
220
|
-
|
|
221
|
-
${sections.join("\n\n")}
|
|
222
|
-
`;
|
|
223
|
-
}
|
|
224
|
-
var init_memory = __esm({
|
|
225
|
-
"src/memory.ts"() {
|
|
226
|
-
"use strict";
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
// src/config.ts
|
|
231
|
-
import * as fs2 from "fs";
|
|
232
|
-
import * as path2 from "path";
|
|
233
|
-
function setConfigDir(dir) {
|
|
234
|
-
_configDir = dir;
|
|
235
|
-
_config = null;
|
|
236
|
-
}
|
|
237
|
-
function getProjectConfig() {
|
|
238
|
-
if (_config) return _config;
|
|
239
|
-
const configPath = path2.join(_configDir ?? process.cwd(), "kody.config.json");
|
|
240
|
-
if (fs2.existsSync(configPath)) {
|
|
241
|
-
try {
|
|
242
|
-
const raw = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
|
|
243
|
-
_config = {
|
|
244
|
-
quality: { ...DEFAULT_CONFIG.quality, ...raw.quality },
|
|
245
|
-
git: { ...DEFAULT_CONFIG.git, ...raw.git },
|
|
246
|
-
github: { ...DEFAULT_CONFIG.github, ...raw.github },
|
|
247
|
-
paths: { ...DEFAULT_CONFIG.paths, ...raw.paths },
|
|
248
|
-
agent: { ...DEFAULT_CONFIG.agent, ...raw.agent }
|
|
249
|
-
};
|
|
250
|
-
} catch {
|
|
251
|
-
_config = { ...DEFAULT_CONFIG };
|
|
252
|
-
}
|
|
253
|
-
} else {
|
|
254
|
-
_config = { ...DEFAULT_CONFIG };
|
|
255
|
-
}
|
|
256
|
-
return _config;
|
|
257
|
-
}
|
|
258
|
-
var DEFAULT_CONFIG, VERIFY_COMMAND_TIMEOUT_MS, FIX_COMMAND_TIMEOUT_MS, _config, _configDir;
|
|
259
|
-
var init_config = __esm({
|
|
260
|
-
"src/config.ts"() {
|
|
261
|
-
"use strict";
|
|
262
|
-
DEFAULT_CONFIG = {
|
|
263
|
-
quality: {
|
|
264
|
-
typecheck: "pnpm -s tsc --noEmit",
|
|
265
|
-
lint: "pnpm -s lint",
|
|
266
|
-
lintFix: "pnpm lint:fix",
|
|
267
|
-
format: "pnpm -s format:check",
|
|
268
|
-
formatFix: "pnpm format:fix",
|
|
269
|
-
testUnit: "pnpm -s test"
|
|
270
|
-
},
|
|
271
|
-
git: {
|
|
272
|
-
defaultBranch: "dev"
|
|
273
|
-
},
|
|
274
|
-
github: {
|
|
275
|
-
owner: "",
|
|
276
|
-
repo: ""
|
|
277
|
-
},
|
|
278
|
-
paths: {
|
|
279
|
-
taskDir: ".tasks"
|
|
280
|
-
},
|
|
281
|
-
agent: {
|
|
282
|
-
runner: "claude-code",
|
|
283
|
-
defaultRunner: "claude",
|
|
284
|
-
modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
|
|
285
|
-
}
|
|
286
|
-
};
|
|
287
|
-
VERIFY_COMMAND_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
288
|
-
FIX_COMMAND_TIMEOUT_MS = 2 * 60 * 1e3;
|
|
289
|
-
_config = null;
|
|
290
|
-
_configDir = null;
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
// src/context.ts
|
|
295
|
-
import * as fs3 from "fs";
|
|
296
|
-
import * as path3 from "path";
|
|
297
|
-
function readPromptFile(stageName) {
|
|
298
|
-
const scriptDir = new URL(".", import.meta.url).pathname;
|
|
299
|
-
const candidates = [
|
|
300
|
-
path3.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
|
|
301
|
-
path3.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
|
|
302
|
-
];
|
|
303
|
-
for (const candidate of candidates) {
|
|
304
|
-
if (fs3.existsSync(candidate)) {
|
|
305
|
-
return fs3.readFileSync(candidate, "utf-8");
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
|
|
309
|
-
}
|
|
310
|
-
function injectTaskContext(prompt, taskId, taskDir, feedback) {
|
|
311
|
-
let context = `## Task Context
|
|
312
|
-
`;
|
|
313
|
-
context += `Task ID: ${taskId}
|
|
314
|
-
`;
|
|
315
|
-
context += `Task Directory: ${taskDir}
|
|
316
|
-
`;
|
|
317
|
-
const taskMdPath = path3.join(taskDir, "task.md");
|
|
318
|
-
if (fs3.existsSync(taskMdPath)) {
|
|
319
|
-
const taskMd = fs3.readFileSync(taskMdPath, "utf-8");
|
|
320
|
-
context += `
|
|
321
|
-
## Task Description
|
|
322
|
-
${taskMd}
|
|
323
|
-
`;
|
|
324
|
-
}
|
|
325
|
-
const taskJsonPath = path3.join(taskDir, "task.json");
|
|
326
|
-
if (fs3.existsSync(taskJsonPath)) {
|
|
327
|
-
try {
|
|
328
|
-
const taskDef = JSON.parse(fs3.readFileSync(taskJsonPath, "utf-8"));
|
|
329
|
-
context += `
|
|
330
|
-
## Task Classification
|
|
331
|
-
`;
|
|
332
|
-
context += `Type: ${taskDef.task_type ?? "unknown"}
|
|
333
|
-
`;
|
|
334
|
-
context += `Title: ${taskDef.title ?? "unknown"}
|
|
335
|
-
`;
|
|
336
|
-
context += `Risk: ${taskDef.risk_level ?? "unknown"}
|
|
337
|
-
`;
|
|
338
|
-
} catch {
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
const specPath = path3.join(taskDir, "spec.md");
|
|
342
|
-
if (fs3.existsSync(specPath)) {
|
|
343
|
-
const spec = fs3.readFileSync(specPath, "utf-8");
|
|
344
|
-
const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
|
|
345
|
-
context += `
|
|
346
|
-
## Spec Summary
|
|
347
|
-
${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
|
|
348
|
-
`;
|
|
349
|
-
}
|
|
350
|
-
const planPath = path3.join(taskDir, "plan.md");
|
|
351
|
-
if (fs3.existsSync(planPath)) {
|
|
352
|
-
const plan = fs3.readFileSync(planPath, "utf-8");
|
|
353
|
-
const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
|
|
354
|
-
context += `
|
|
355
|
-
## Plan Summary
|
|
356
|
-
${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
|
|
357
|
-
`;
|
|
358
|
-
}
|
|
359
|
-
if (feedback) {
|
|
360
|
-
context += `
|
|
361
|
-
## Human Feedback
|
|
362
|
-
${feedback}
|
|
363
|
-
`;
|
|
364
|
-
}
|
|
365
|
-
return prompt.replace("{{TASK_CONTEXT}}", context);
|
|
366
|
-
}
|
|
367
|
-
function buildFullPrompt(stageName, taskId, taskDir, projectDir, feedback) {
|
|
368
|
-
const memory = readProjectMemory(projectDir);
|
|
369
|
-
const promptTemplate = readPromptFile(stageName);
|
|
370
|
-
const prompt = injectTaskContext(promptTemplate, taskId, taskDir, feedback);
|
|
371
|
-
return memory ? `${memory}
|
|
372
|
-
---
|
|
373
|
-
|
|
374
|
-
${prompt}` : prompt;
|
|
375
|
-
}
|
|
376
|
-
function resolveModel(modelTier, stageName) {
|
|
377
|
-
const config = getProjectConfig();
|
|
378
|
-
if (config.agent.usePerStageRouting && stageName) {
|
|
379
|
-
return stageName;
|
|
380
|
-
}
|
|
381
|
-
const mapped = config.agent.modelMap[modelTier];
|
|
382
|
-
if (mapped) return mapped;
|
|
383
|
-
return DEFAULT_MODEL_MAP[modelTier] ?? "sonnet";
|
|
384
|
-
}
|
|
385
|
-
var DEFAULT_MODEL_MAP, MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC;
|
|
386
|
-
var init_context = __esm({
|
|
387
|
-
"src/context.ts"() {
|
|
388
|
-
"use strict";
|
|
389
|
-
init_memory();
|
|
390
|
-
init_config();
|
|
391
|
-
DEFAULT_MODEL_MAP = {
|
|
392
|
-
cheap: "haiku",
|
|
393
|
-
mid: "sonnet",
|
|
394
|
-
strong: "opus"
|
|
395
|
-
};
|
|
396
|
-
MAX_TASK_CONTEXT_PLAN = 1500;
|
|
397
|
-
MAX_TASK_CONTEXT_SPEC = 2e3;
|
|
398
|
-
}
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
// src/validators.ts
|
|
402
|
-
function validateTaskJson(content) {
|
|
403
|
-
try {
|
|
404
|
-
const parsed = JSON.parse(content);
|
|
405
|
-
for (const field of REQUIRED_TASK_FIELDS) {
|
|
406
|
-
if (!(field in parsed)) {
|
|
407
|
-
return { valid: false, error: `Missing field: ${field}` };
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
return { valid: true };
|
|
411
|
-
} catch (err) {
|
|
412
|
-
return {
|
|
413
|
-
valid: false,
|
|
414
|
-
error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
function validatePlanMd(content) {
|
|
419
|
-
if (content.length < 10) {
|
|
420
|
-
return { valid: false, error: "Plan is too short (< 10 chars)" };
|
|
421
|
-
}
|
|
422
|
-
if (!/^##\s+\w+/m.test(content)) {
|
|
423
|
-
return { valid: false, error: "Plan has no markdown h2 sections" };
|
|
424
|
-
}
|
|
425
|
-
return { valid: true };
|
|
426
|
-
}
|
|
427
|
-
function validateReviewMd(content) {
|
|
428
|
-
if (/pass/i.test(content) || /fail/i.test(content)) {
|
|
429
|
-
return { valid: true };
|
|
430
|
-
}
|
|
431
|
-
return { valid: false, error: "Review must contain 'pass' or 'fail'" };
|
|
432
|
-
}
|
|
433
|
-
var REQUIRED_TASK_FIELDS;
|
|
434
|
-
var init_validators = __esm({
|
|
435
|
-
"src/validators.ts"() {
|
|
436
|
-
"use strict";
|
|
437
|
-
REQUIRED_TASK_FIELDS = [
|
|
438
|
-
"task_type",
|
|
439
|
-
"title",
|
|
440
|
-
"description",
|
|
441
|
-
"scope",
|
|
442
|
-
"risk_level"
|
|
443
|
-
];
|
|
444
|
-
}
|
|
445
|
-
});
|
|
446
|
-
|
|
447
202
|
// src/logger.ts
|
|
448
203
|
function getLevel() {
|
|
449
204
|
const env = process.env.LOG_LEVEL;
|
|
@@ -542,7 +297,8 @@ function ensureFeatureBranch(issueNumber, title, cwd) {
|
|
|
542
297
|
try {
|
|
543
298
|
git(["checkout", defaultBranch2], { cwd });
|
|
544
299
|
} catch {
|
|
545
|
-
logger.warn(` Failed to checkout ${defaultBranch2}`);
|
|
300
|
+
logger.warn(` Failed to checkout ${defaultBranch2}, aborting branch creation`);
|
|
301
|
+
return current;
|
|
546
302
|
}
|
|
547
303
|
}
|
|
548
304
|
try {
|
|
@@ -600,7 +356,7 @@ function commitAll(message, cwd) {
|
|
|
600
356
|
if (!status) {
|
|
601
357
|
return { success: false, hash: "", message: "No changes to commit" };
|
|
602
358
|
}
|
|
603
|
-
git(["add", "
|
|
359
|
+
git(["add", "."], { cwd });
|
|
604
360
|
git(["commit", "--no-gpg-sign", "-m", message], { cwd });
|
|
605
361
|
const hash = git(["rev-parse", "HEAD"], { cwd }).slice(0, 7);
|
|
606
362
|
logger.info(` Committed: ${hash} ${message}`);
|
|
@@ -725,33 +481,492 @@ var init_github_api = __esm({
|
|
|
725
481
|
}
|
|
726
482
|
});
|
|
727
483
|
|
|
728
|
-
// src/
|
|
729
|
-
import
|
|
730
|
-
|
|
731
|
-
|
|
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;
|
|
732
490
|
try {
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
env: { ...process.env, FORCE_COLOR: "0" }
|
|
739
|
-
});
|
|
740
|
-
return { success: true, output: output ?? "", timedOut: false };
|
|
741
|
-
} catch (err) {
|
|
742
|
-
const e = err;
|
|
743
|
-
const output = `${e.stdout ?? ""}${e.stderr ?? ""}`;
|
|
744
|
-
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;
|
|
745
496
|
}
|
|
746
497
|
}
|
|
747
|
-
function
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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 };
|
|
753
513
|
}
|
|
754
|
-
|
|
514
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
515
|
+
return { taskId, state: "running", stages, createdAt: now, updatedAt: now };
|
|
516
|
+
}
|
|
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));
|
|
528
|
+
}
|
|
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
|
+
};
|
|
541
|
+
}
|
|
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}`);
|
|
558
|
+
}
|
|
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 };
|
|
595
|
+
}
|
|
596
|
+
} else {
|
|
597
|
+
_config = { ...DEFAULT_CONFIG };
|
|
598
|
+
}
|
|
599
|
+
return _config;
|
|
600
|
+
}
|
|
601
|
+
var DEFAULT_CONFIG, VERIFY_COMMAND_TIMEOUT_MS, FIX_COMMAND_TIMEOUT_MS, _config, _configDir;
|
|
602
|
+
var init_config = __esm({
|
|
603
|
+
"src/config.ts"() {
|
|
604
|
+
"use strict";
|
|
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;
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
// src/context.ts
|
|
639
|
+
import * as fs4 from "fs";
|
|
640
|
+
import * as path4 from "path";
|
|
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(", ")}`);
|
|
653
|
+
}
|
|
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 {
|
|
683
|
+
}
|
|
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
|
+
---
|
|
717
|
+
|
|
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
|
+
});
|
|
744
|
+
|
|
745
|
+
// src/validators.ts
|
|
746
|
+
function stripFences(content) {
|
|
747
|
+
return content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
748
|
+
}
|
|
749
|
+
function validateTaskJson(content) {
|
|
750
|
+
try {
|
|
751
|
+
const parsed = JSON.parse(stripFences(content));
|
|
752
|
+
for (const field of REQUIRED_TASK_FIELDS) {
|
|
753
|
+
if (!(field in parsed)) {
|
|
754
|
+
return { valid: false, error: `Missing field: ${field}` };
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return { valid: true };
|
|
758
|
+
} catch (err) {
|
|
759
|
+
return {
|
|
760
|
+
valid: false,
|
|
761
|
+
error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
function validatePlanMd(content) {
|
|
766
|
+
if (content.length < 10) {
|
|
767
|
+
return { valid: false, error: "Plan is too short (< 10 chars)" };
|
|
768
|
+
}
|
|
769
|
+
if (!/^##\s+\w+/m.test(content)) {
|
|
770
|
+
return { valid: false, error: "Plan has no markdown h2 sections" };
|
|
771
|
+
}
|
|
772
|
+
return { valid: true };
|
|
773
|
+
}
|
|
774
|
+
function validateReviewMd(content) {
|
|
775
|
+
if (/pass/i.test(content) || /fail/i.test(content)) {
|
|
776
|
+
return { valid: true };
|
|
777
|
+
}
|
|
778
|
+
return { valid: false, error: "Review must contain 'pass' or 'fail'" };
|
|
779
|
+
}
|
|
780
|
+
var REQUIRED_TASK_FIELDS;
|
|
781
|
+
var init_validators = __esm({
|
|
782
|
+
"src/validators.ts"() {
|
|
783
|
+
"use strict";
|
|
784
|
+
REQUIRED_TASK_FIELDS = [
|
|
785
|
+
"task_type",
|
|
786
|
+
"title",
|
|
787
|
+
"description",
|
|
788
|
+
"scope",
|
|
789
|
+
"risk_level"
|
|
790
|
+
];
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
// src/pipeline/runner-selection.ts
|
|
795
|
+
function getRunnerForStage(ctx, stageName) {
|
|
796
|
+
const config = getProjectConfig();
|
|
797
|
+
const runnerName = config.agent.stageRunners?.[stageName] ?? config.agent.defaultRunner ?? Object.keys(ctx.runners)[0] ?? "claude";
|
|
798
|
+
const runner = ctx.runners[runnerName];
|
|
799
|
+
if (!runner) {
|
|
800
|
+
throw new Error(
|
|
801
|
+
`Runner "${runnerName}" not found for stage ${stageName}. Available: ${Object.keys(ctx.runners).join(", ")}`
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
return runner;
|
|
805
|
+
}
|
|
806
|
+
var init_runner_selection = __esm({
|
|
807
|
+
"src/pipeline/runner-selection.ts"() {
|
|
808
|
+
"use strict";
|
|
809
|
+
init_config();
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
// src/stages/agent.ts
|
|
814
|
+
import * as fs5 from "fs";
|
|
815
|
+
import * as path5 from "path";
|
|
816
|
+
function validateStageOutput(stageName, content) {
|
|
817
|
+
switch (stageName) {
|
|
818
|
+
case "taskify":
|
|
819
|
+
return validateTaskJson(content);
|
|
820
|
+
case "plan":
|
|
821
|
+
return validatePlanMd(content);
|
|
822
|
+
case "review":
|
|
823
|
+
return validateReviewMd(content);
|
|
824
|
+
default:
|
|
825
|
+
return { valid: true };
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
async function executeAgentStage(ctx, def) {
|
|
829
|
+
if (ctx.input.dryRun) {
|
|
830
|
+
logger.info(` [dry-run] skipping ${def.name}`);
|
|
831
|
+
return { outcome: "completed", retries: 0 };
|
|
832
|
+
}
|
|
833
|
+
const prompt = buildFullPrompt(def.name, ctx.taskId, ctx.taskDir, ctx.projectDir, ctx.input.feedback);
|
|
834
|
+
const model = resolveModel(def.modelTier, def.name);
|
|
835
|
+
const config = getProjectConfig();
|
|
836
|
+
const runnerName = config.agent.stageRunners?.[def.name] ?? config.agent.defaultRunner ?? Object.keys(ctx.runners)[0] ?? "claude";
|
|
837
|
+
logger.info(` runner=${runnerName} model=${model} timeout=${def.timeout / 1e3}s`);
|
|
838
|
+
const extraEnv = {};
|
|
839
|
+
if (config.agent.litellmUrl) {
|
|
840
|
+
extraEnv.ANTHROPIC_BASE_URL = config.agent.litellmUrl;
|
|
841
|
+
}
|
|
842
|
+
const runner = getRunnerForStage(ctx, def.name);
|
|
843
|
+
const result = await runner.run(def.name, prompt, model, def.timeout, ctx.taskDir, {
|
|
844
|
+
cwd: ctx.projectDir,
|
|
845
|
+
env: extraEnv
|
|
846
|
+
});
|
|
847
|
+
if (result.outcome !== "completed") {
|
|
848
|
+
return { outcome: result.outcome, error: result.error, retries: 0 };
|
|
849
|
+
}
|
|
850
|
+
if (def.outputFile && result.output) {
|
|
851
|
+
fs5.writeFileSync(path5.join(ctx.taskDir, def.outputFile), result.output);
|
|
852
|
+
}
|
|
853
|
+
if (def.outputFile) {
|
|
854
|
+
const outputPath = path5.join(ctx.taskDir, def.outputFile);
|
|
855
|
+
if (!fs5.existsSync(outputPath)) {
|
|
856
|
+
const ext = path5.extname(def.outputFile);
|
|
857
|
+
const base = path5.basename(def.outputFile, ext);
|
|
858
|
+
const files = fs5.readdirSync(ctx.taskDir);
|
|
859
|
+
const variant = files.find(
|
|
860
|
+
(f) => f.startsWith(base + "-") && f.endsWith(ext)
|
|
861
|
+
);
|
|
862
|
+
if (variant) {
|
|
863
|
+
fs5.renameSync(path5.join(ctx.taskDir, variant), outputPath);
|
|
864
|
+
logger.info(` Renamed variant ${variant} \u2192 ${def.outputFile}`);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
if (def.outputFile) {
|
|
869
|
+
const outputPath = path5.join(ctx.taskDir, def.outputFile);
|
|
870
|
+
if (fs5.existsSync(outputPath)) {
|
|
871
|
+
const content = fs5.readFileSync(outputPath, "utf-8");
|
|
872
|
+
const validation = validateStageOutput(def.name, content);
|
|
873
|
+
if (!validation.valid) {
|
|
874
|
+
if (def.name === "taskify") {
|
|
875
|
+
logger.warn(` taskify output invalid (${validation.error}), retrying...`);
|
|
876
|
+
const retryPrompt = prompt + "\n\nIMPORTANT: Your previous output was not valid JSON. Output ONLY the raw JSON object. No markdown, no fences, no explanation.";
|
|
877
|
+
const retryResult = await runner.run(def.name, retryPrompt, model, def.timeout, ctx.taskDir, {
|
|
878
|
+
cwd: ctx.projectDir,
|
|
879
|
+
env: extraEnv
|
|
880
|
+
});
|
|
881
|
+
if (retryResult.outcome === "completed" && retryResult.output) {
|
|
882
|
+
const stripped = stripFences(retryResult.output);
|
|
883
|
+
const retryValidation = validateTaskJson(stripped);
|
|
884
|
+
if (retryValidation.valid) {
|
|
885
|
+
fs5.writeFileSync(outputPath, retryResult.output);
|
|
886
|
+
logger.info(` taskify retry produced valid JSON`);
|
|
887
|
+
} else {
|
|
888
|
+
logger.warn(` taskify retry still invalid: ${retryValidation.error}`);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
} else {
|
|
892
|
+
logger.warn(` validation warning: ${validation.error}`);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return { outcome: "completed", outputFile: def.outputFile, retries: 0 };
|
|
898
|
+
}
|
|
899
|
+
var init_agent = __esm({
|
|
900
|
+
"src/stages/agent.ts"() {
|
|
901
|
+
"use strict";
|
|
902
|
+
init_context();
|
|
903
|
+
init_validators();
|
|
904
|
+
init_config();
|
|
905
|
+
init_runner_selection();
|
|
906
|
+
init_logger();
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
// src/verify-runner.ts
|
|
911
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
912
|
+
function isExecError(err) {
|
|
913
|
+
return typeof err === "object" && err !== null;
|
|
914
|
+
}
|
|
915
|
+
function parseCommand(cmd) {
|
|
916
|
+
const parts = [];
|
|
917
|
+
let current = "";
|
|
918
|
+
let inQuote = null;
|
|
919
|
+
for (const ch of cmd) {
|
|
920
|
+
if (inQuote) {
|
|
921
|
+
if (ch === inQuote) {
|
|
922
|
+
inQuote = null;
|
|
923
|
+
} else {
|
|
924
|
+
current += ch;
|
|
925
|
+
}
|
|
926
|
+
} else if (ch === '"' || ch === "'") {
|
|
927
|
+
inQuote = ch;
|
|
928
|
+
} else if (/\s/.test(ch)) {
|
|
929
|
+
if (current) {
|
|
930
|
+
parts.push(current);
|
|
931
|
+
current = "";
|
|
932
|
+
}
|
|
933
|
+
} else {
|
|
934
|
+
current += ch;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
if (current) parts.push(current);
|
|
938
|
+
if (inQuote) logger.warn(`Unclosed quote in command: ${cmd}`);
|
|
939
|
+
return parts;
|
|
940
|
+
}
|
|
941
|
+
function runCommand(cmd, cwd, timeout) {
|
|
942
|
+
const parts = parseCommand(cmd);
|
|
943
|
+
if (parts.length === 0) {
|
|
944
|
+
return { success: true, output: "", timedOut: false };
|
|
945
|
+
}
|
|
946
|
+
try {
|
|
947
|
+
const output = execFileSync4(parts[0], parts.slice(1), {
|
|
948
|
+
cwd,
|
|
949
|
+
timeout,
|
|
950
|
+
encoding: "utf-8",
|
|
951
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
952
|
+
env: { ...process.env, FORCE_COLOR: "0" }
|
|
953
|
+
});
|
|
954
|
+
return { success: true, output: output ?? "", timedOut: false };
|
|
955
|
+
} catch (err) {
|
|
956
|
+
const stdout = isExecError(err) ? err.stdout ?? "" : "";
|
|
957
|
+
const stderr = isExecError(err) ? err.stderr ?? "" : "";
|
|
958
|
+
const killed = isExecError(err) ? !!err.killed : false;
|
|
959
|
+
return { success: false, output: `${stdout}${stderr}`, timedOut: killed };
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
function parseErrors(output) {
|
|
963
|
+
const errors = [];
|
|
964
|
+
for (const line of output.split("\n")) {
|
|
965
|
+
if (/error|Error|ERROR|failed|Failed|FAIL|warning:|Warning:|WARN/i.test(line)) {
|
|
966
|
+
errors.push(line.slice(0, 500));
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return errors;
|
|
755
970
|
}
|
|
756
971
|
function extractSummary(output, cmdName) {
|
|
757
972
|
const summaryPatterns = /Test Suites|Tests|Coverage|ERRORS|FAILURES|success|completed/i;
|
|
@@ -787,431 +1002,811 @@ function runQualityGates(taskDir, projectRoot) {
|
|
|
787
1002
|
}
|
|
788
1003
|
allSummary.push(...extractSummary(result.output, name));
|
|
789
1004
|
}
|
|
790
|
-
return { pass: allPass, errors: allErrors, summary: allSummary };
|
|
1005
|
+
return { pass: allPass, errors: allErrors, summary: allSummary };
|
|
1006
|
+
}
|
|
1007
|
+
var init_verify_runner = __esm({
|
|
1008
|
+
"src/verify-runner.ts"() {
|
|
1009
|
+
"use strict";
|
|
1010
|
+
init_config();
|
|
1011
|
+
init_logger();
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
// src/observer.ts
|
|
1016
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
1017
|
+
async function diagnoseFailure(stageName, errorOutput, modifiedFiles, runner, model) {
|
|
1018
|
+
const context = [
|
|
1019
|
+
`Stage: ${stageName}`,
|
|
1020
|
+
``,
|
|
1021
|
+
`Error output:`,
|
|
1022
|
+
errorOutput.slice(-2e3),
|
|
1023
|
+
// Last 2000 chars of error
|
|
1024
|
+
``,
|
|
1025
|
+
modifiedFiles.length > 0 ? `Files modified by build stage:
|
|
1026
|
+
${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (build may not have run yet)."
|
|
1027
|
+
].join("\n");
|
|
1028
|
+
const prompt = DIAGNOSIS_PROMPT + context;
|
|
1029
|
+
try {
|
|
1030
|
+
const result = await runner.run(
|
|
1031
|
+
"diagnosis",
|
|
1032
|
+
prompt,
|
|
1033
|
+
model,
|
|
1034
|
+
3e4,
|
|
1035
|
+
// 30s timeout — this should be fast
|
|
1036
|
+
""
|
|
1037
|
+
);
|
|
1038
|
+
if (result.outcome === "completed" && result.output) {
|
|
1039
|
+
const cleaned = result.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
|
|
1040
|
+
const parsed = JSON.parse(cleaned);
|
|
1041
|
+
const validClassifications = [
|
|
1042
|
+
"fixable",
|
|
1043
|
+
"infrastructure",
|
|
1044
|
+
"pre-existing",
|
|
1045
|
+
"retry",
|
|
1046
|
+
"abort"
|
|
1047
|
+
];
|
|
1048
|
+
if (validClassifications.includes(parsed.classification)) {
|
|
1049
|
+
logger.info(` Diagnosis: ${parsed.classification} \u2014 ${parsed.reason}`);
|
|
1050
|
+
return {
|
|
1051
|
+
classification: parsed.classification,
|
|
1052
|
+
reason: parsed.reason ?? "Unknown reason",
|
|
1053
|
+
resolution: parsed.resolution ?? ""
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
logger.warn(` Diagnosis error: ${err instanceof Error ? err.message : err}`);
|
|
1059
|
+
}
|
|
1060
|
+
logger.warn(" Diagnosis failed \u2014 defaulting to fixable");
|
|
1061
|
+
return {
|
|
1062
|
+
classification: "fixable",
|
|
1063
|
+
reason: "Could not diagnose failure",
|
|
1064
|
+
resolution: errorOutput.slice(-500)
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
function getModifiedFiles(projectDir) {
|
|
1068
|
+
try {
|
|
1069
|
+
const output = execFileSync5("git", ["diff", "--name-only", "HEAD~1"], {
|
|
1070
|
+
encoding: "utf-8",
|
|
1071
|
+
cwd: projectDir,
|
|
1072
|
+
timeout: 5e3,
|
|
1073
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1074
|
+
}).trim();
|
|
1075
|
+
return output ? output.split("\n").filter(Boolean) : [];
|
|
1076
|
+
} catch {
|
|
1077
|
+
return [];
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
var DIAGNOSIS_PROMPT;
|
|
1081
|
+
var init_observer = __esm({
|
|
1082
|
+
"src/observer.ts"() {
|
|
1083
|
+
"use strict";
|
|
1084
|
+
init_logger();
|
|
1085
|
+
DIAGNOSIS_PROMPT = `You are a pipeline failure diagnosis agent. Analyze the error and classify it.
|
|
1086
|
+
|
|
1087
|
+
Output ONLY valid JSON. No markdown fences. No explanation.
|
|
1088
|
+
|
|
1089
|
+
{
|
|
1090
|
+
"classification": "fixable | infrastructure | pre-existing | retry | abort",
|
|
1091
|
+
"reason": "One sentence explaining what went wrong",
|
|
1092
|
+
"resolution": "Specific instructions for fixing (if fixable) or what the user needs to do (if infrastructure)"
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
Classification rules:
|
|
1096
|
+
- fixable: Error is in code that was just written/modified. The resolution should describe exactly what to change.
|
|
1097
|
+
- infrastructure: External dependency not available (database, API, service). The resolution should say what the user needs to set up.
|
|
1098
|
+
- pre-existing: Error exists in code that was NOT modified. Safe to skip. The resolution should note which files.
|
|
1099
|
+
- retry: Transient error (network timeout, rate limit, flaky test). Worth retrying once.
|
|
1100
|
+
- abort: Unrecoverable error (permission denied, corrupted state, out of disk). Pipeline should stop.
|
|
1101
|
+
|
|
1102
|
+
Error context:
|
|
1103
|
+
`;
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
// src/stages/gate.ts
|
|
1108
|
+
import * as fs6 from "fs";
|
|
1109
|
+
import * as path6 from "path";
|
|
1110
|
+
function executeGateStage(ctx, def) {
|
|
1111
|
+
if (ctx.input.dryRun) {
|
|
1112
|
+
logger.info(` [dry-run] skipping ${def.name}`);
|
|
1113
|
+
return { outcome: "completed", retries: 0 };
|
|
1114
|
+
}
|
|
1115
|
+
const verifyResult = runQualityGates(ctx.taskDir, ctx.projectDir);
|
|
1116
|
+
const lines = [
|
|
1117
|
+
`# Verification Report
|
|
1118
|
+
`,
|
|
1119
|
+
`## Result: ${verifyResult.pass ? "PASS" : "FAIL"}
|
|
1120
|
+
`
|
|
1121
|
+
];
|
|
1122
|
+
if (verifyResult.errors.length > 0) {
|
|
1123
|
+
lines.push(`
|
|
1124
|
+
## Errors
|
|
1125
|
+
`);
|
|
1126
|
+
for (const e of verifyResult.errors) {
|
|
1127
|
+
lines.push(`- ${e}
|
|
1128
|
+
`);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
if (verifyResult.summary.length > 0) {
|
|
1132
|
+
lines.push(`
|
|
1133
|
+
## Summary
|
|
1134
|
+
`);
|
|
1135
|
+
for (const s of verifyResult.summary) {
|
|
1136
|
+
lines.push(`- ${s}
|
|
1137
|
+
`);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
fs6.writeFileSync(path6.join(ctx.taskDir, "verify.md"), lines.join(""));
|
|
1141
|
+
return {
|
|
1142
|
+
outcome: verifyResult.pass ? "completed" : "failed",
|
|
1143
|
+
retries: 0
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
var init_gate = __esm({
|
|
1147
|
+
"src/stages/gate.ts"() {
|
|
1148
|
+
"use strict";
|
|
1149
|
+
init_verify_runner();
|
|
1150
|
+
init_logger();
|
|
1151
|
+
}
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
// src/stages/verify.ts
|
|
1155
|
+
import * as fs7 from "fs";
|
|
1156
|
+
import * as path7 from "path";
|
|
1157
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
1158
|
+
async function executeVerifyWithAutofix(ctx, def) {
|
|
1159
|
+
const maxAttempts = def.maxRetries ?? 2;
|
|
1160
|
+
for (let attempt = 0; attempt <= maxAttempts; attempt++) {
|
|
1161
|
+
logger.info(` verification attempt ${attempt + 1}/${maxAttempts + 1}`);
|
|
1162
|
+
const gateResult = executeGateStage(ctx, def);
|
|
1163
|
+
if (gateResult.outcome === "completed") {
|
|
1164
|
+
return { ...gateResult, retries: attempt };
|
|
1165
|
+
}
|
|
1166
|
+
if (attempt < maxAttempts) {
|
|
1167
|
+
const verifyPath = path7.join(ctx.taskDir, "verify.md");
|
|
1168
|
+
const errorOutput = fs7.existsSync(verifyPath) ? fs7.readFileSync(verifyPath, "utf-8") : "Unknown error";
|
|
1169
|
+
const modifiedFiles = getModifiedFiles(ctx.projectDir);
|
|
1170
|
+
const defaultRunner = getRunnerForStage(ctx, "taskify");
|
|
1171
|
+
const diagnosis = await diagnoseFailure(
|
|
1172
|
+
"verify",
|
|
1173
|
+
errorOutput,
|
|
1174
|
+
modifiedFiles,
|
|
1175
|
+
defaultRunner,
|
|
1176
|
+
resolveModel("cheap")
|
|
1177
|
+
);
|
|
1178
|
+
if (diagnosis.classification === "infrastructure") {
|
|
1179
|
+
logger.warn(` Infrastructure issue: ${diagnosis.reason}`);
|
|
1180
|
+
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
1181
|
+
try {
|
|
1182
|
+
postComment(ctx.input.issueNumber, `\u26A0\uFE0F **Infrastructure issue detected:** ${diagnosis.reason}
|
|
1183
|
+
|
|
1184
|
+
${diagnosis.resolution}`);
|
|
1185
|
+
} catch {
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
return { outcome: "completed", retries: attempt, error: `Skipped: ${diagnosis.reason}` };
|
|
1189
|
+
}
|
|
1190
|
+
if (diagnosis.classification === "pre-existing") {
|
|
1191
|
+
logger.warn(` Pre-existing issue: ${diagnosis.reason}`);
|
|
1192
|
+
return { outcome: "completed", retries: attempt, error: `Skipped: ${diagnosis.reason}` };
|
|
1193
|
+
}
|
|
1194
|
+
if (diagnosis.classification === "abort") {
|
|
1195
|
+
logger.error(` Unrecoverable: ${diagnosis.reason}`);
|
|
1196
|
+
return { outcome: "failed", retries: attempt, error: diagnosis.reason };
|
|
1197
|
+
}
|
|
1198
|
+
logger.info(` Diagnosis: ${diagnosis.classification} \u2014 ${diagnosis.reason}`);
|
|
1199
|
+
const config = getProjectConfig();
|
|
1200
|
+
const runFix = (cmd) => {
|
|
1201
|
+
if (!cmd) return;
|
|
1202
|
+
const parts = parseCommand(cmd);
|
|
1203
|
+
if (parts.length === 0) return;
|
|
1204
|
+
try {
|
|
1205
|
+
execFileSync6(parts[0], parts.slice(1), {
|
|
1206
|
+
stdio: "pipe",
|
|
1207
|
+
timeout: FIX_COMMAND_TIMEOUT_MS
|
|
1208
|
+
});
|
|
1209
|
+
} catch {
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
runFix(config.quality.lintFix);
|
|
1213
|
+
runFix(config.quality.formatFix);
|
|
1214
|
+
if (def.retryWithAgent) {
|
|
1215
|
+
const autofixCtx = {
|
|
1216
|
+
...ctx,
|
|
1217
|
+
input: {
|
|
1218
|
+
...ctx.input,
|
|
1219
|
+
feedback: `${diagnosis.resolution}
|
|
1220
|
+
|
|
1221
|
+
${ctx.input.feedback ?? ""}`.trim()
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
logger.info(` running ${def.retryWithAgent} agent with diagnosis guidance...`);
|
|
1225
|
+
await executeAgentStage(autofixCtx, {
|
|
1226
|
+
...def,
|
|
1227
|
+
name: def.retryWithAgent,
|
|
1228
|
+
type: "agent",
|
|
1229
|
+
modelTier: "mid",
|
|
1230
|
+
timeout: 3e5,
|
|
1231
|
+
outputFile: void 0
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
return {
|
|
1237
|
+
outcome: "failed",
|
|
1238
|
+
retries: maxAttempts,
|
|
1239
|
+
error: "Verification failed after autofix attempts"
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
var init_verify = __esm({
|
|
1243
|
+
"src/stages/verify.ts"() {
|
|
1244
|
+
"use strict";
|
|
1245
|
+
init_context();
|
|
1246
|
+
init_config();
|
|
1247
|
+
init_verify_runner();
|
|
1248
|
+
init_runner_selection();
|
|
1249
|
+
init_github_api();
|
|
1250
|
+
init_observer();
|
|
1251
|
+
init_logger();
|
|
1252
|
+
init_agent();
|
|
1253
|
+
init_gate();
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
// src/stages/review.ts
|
|
1258
|
+
import * as fs8 from "fs";
|
|
1259
|
+
import * as path8 from "path";
|
|
1260
|
+
async function executeReviewWithFix(ctx, def) {
|
|
1261
|
+
if (ctx.input.dryRun) {
|
|
1262
|
+
return { outcome: "completed", retries: 0 };
|
|
1263
|
+
}
|
|
1264
|
+
const reviewDef = STAGES.find((s) => s.name === "review");
|
|
1265
|
+
const reviewFixDef = STAGES.find((s) => s.name === "review-fix");
|
|
1266
|
+
const reviewResult = await executeAgentStage(ctx, reviewDef);
|
|
1267
|
+
if (reviewResult.outcome !== "completed") {
|
|
1268
|
+
return reviewResult;
|
|
1269
|
+
}
|
|
1270
|
+
const reviewFile = path8.join(ctx.taskDir, "review.md");
|
|
1271
|
+
if (!fs8.existsSync(reviewFile)) {
|
|
1272
|
+
return { outcome: "failed", retries: 0, error: "review.md not found" };
|
|
1273
|
+
}
|
|
1274
|
+
const content = fs8.readFileSync(reviewFile, "utf-8");
|
|
1275
|
+
const hasIssues = /\bfail\b/i.test(content) && !/pass/i.test(content);
|
|
1276
|
+
if (!hasIssues) {
|
|
1277
|
+
return reviewResult;
|
|
1278
|
+
}
|
|
1279
|
+
logger.info(` review found issues, running review-fix...`);
|
|
1280
|
+
const fixResult = await executeAgentStage(ctx, reviewFixDef);
|
|
1281
|
+
if (fixResult.outcome !== "completed") {
|
|
1282
|
+
return fixResult;
|
|
1283
|
+
}
|
|
1284
|
+
logger.info(` re-running review after fix...`);
|
|
1285
|
+
return executeAgentStage(ctx, reviewDef);
|
|
791
1286
|
}
|
|
792
|
-
var
|
|
793
|
-
"src/
|
|
1287
|
+
var init_review = __esm({
|
|
1288
|
+
"src/stages/review.ts"() {
|
|
794
1289
|
"use strict";
|
|
795
|
-
|
|
1290
|
+
init_definitions();
|
|
796
1291
|
init_logger();
|
|
1292
|
+
init_agent();
|
|
797
1293
|
}
|
|
798
1294
|
});
|
|
799
1295
|
|
|
800
|
-
// src/
|
|
801
|
-
import * as
|
|
802
|
-
import * as
|
|
803
|
-
import { execFileSync as
|
|
804
|
-
function
|
|
805
|
-
const
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
try {
|
|
811
|
-
if (stageName === "taskify") {
|
|
812
|
-
const taskJsonPath = path4.join(ctx.taskDir, "task.json");
|
|
813
|
-
if (!fs4.existsSync(taskJsonPath)) return false;
|
|
814
|
-
const raw = fs4.readFileSync(taskJsonPath, "utf-8");
|
|
1296
|
+
// src/stages/ship.ts
|
|
1297
|
+
import * as fs9 from "fs";
|
|
1298
|
+
import * as path9 from "path";
|
|
1299
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
1300
|
+
function buildPrBody(ctx) {
|
|
1301
|
+
const sections = [];
|
|
1302
|
+
const taskJsonPath = path9.join(ctx.taskDir, "task.json");
|
|
1303
|
+
if (fs9.existsSync(taskJsonPath)) {
|
|
1304
|
+
try {
|
|
1305
|
+
const raw = fs9.readFileSync(taskJsonPath, "utf-8");
|
|
815
1306
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
816
|
-
const
|
|
817
|
-
if (
|
|
818
|
-
|
|
1307
|
+
const task = JSON.parse(cleaned);
|
|
1308
|
+
if (task.description) {
|
|
1309
|
+
sections.push(`## What
|
|
819
1310
|
|
|
820
|
-
${
|
|
1311
|
+
${task.description}`);
|
|
1312
|
+
}
|
|
1313
|
+
if (task.scope?.length) {
|
|
1314
|
+
sections.push(`
|
|
1315
|
+
## Scope
|
|
821
1316
|
|
|
822
|
-
|
|
823
|
-
postComment(ctx.input.issueNumber, body);
|
|
824
|
-
setLifecycleLabel(ctx.input.issueNumber, "waiting");
|
|
825
|
-
return true;
|
|
1317
|
+
${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
|
|
826
1318
|
}
|
|
1319
|
+
sections.push(`
|
|
1320
|
+
**Type:** ${task.task_type ?? "unknown"} | **Risk:** ${task.risk_level ?? "unknown"}`);
|
|
1321
|
+
} catch {
|
|
827
1322
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
${questions.map((q, i) => `${i + 1}. ${q}`).join("\n")}
|
|
1323
|
+
}
|
|
1324
|
+
const reviewPath = path9.join(ctx.taskDir, "review.md");
|
|
1325
|
+
if (fs9.existsSync(reviewPath)) {
|
|
1326
|
+
const review = fs9.readFileSync(reviewPath, "utf-8");
|
|
1327
|
+
const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
1328
|
+
if (summaryMatch) {
|
|
1329
|
+
const summary = summaryMatch[1].trim();
|
|
1330
|
+
if (summary) {
|
|
1331
|
+
sections.push(`
|
|
1332
|
+
## Changes
|
|
840
1333
|
|
|
841
|
-
|
|
842
|
-
postComment(ctx.input.issueNumber, body);
|
|
843
|
-
setLifecycleLabel(ctx.input.issueNumber, "waiting");
|
|
844
|
-
return true;
|
|
845
|
-
}
|
|
1334
|
+
${summary}`);
|
|
846
1335
|
}
|
|
847
1336
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
const config = getProjectConfig();
|
|
854
|
-
const runnerName = config.agent.stageRunners?.[stageName] ?? config.agent.defaultRunner ?? Object.keys(ctx.runners)[0] ?? "claude";
|
|
855
|
-
const runner = ctx.runners[runnerName];
|
|
856
|
-
if (!runner) {
|
|
857
|
-
throw new Error(
|
|
858
|
-
`Runner "${runnerName}" not found for stage ${stageName}. Available: ${Object.keys(ctx.runners).join(", ")}`
|
|
859
|
-
);
|
|
1337
|
+
const verdictMatch = review.match(/## Verdict:\s*(PASS|FAIL)/i);
|
|
1338
|
+
if (verdictMatch) {
|
|
1339
|
+
sections.push(`
|
|
1340
|
+
**Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
|
|
1341
|
+
}
|
|
860
1342
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
if (!fs4.existsSync(p)) return null;
|
|
866
|
-
try {
|
|
867
|
-
const raw = JSON.parse(fs4.readFileSync(p, "utf-8"));
|
|
868
|
-
if (raw.taskId === taskId) return raw;
|
|
869
|
-
return null;
|
|
870
|
-
} catch {
|
|
871
|
-
return null;
|
|
1343
|
+
const verifyPath = path9.join(ctx.taskDir, "verify.md");
|
|
1344
|
+
if (fs9.existsSync(verifyPath)) {
|
|
1345
|
+
const verify = fs9.readFileSync(verifyPath, "utf-8");
|
|
1346
|
+
if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
|
|
872
1347
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
stages[stage.name] = { state: "pending", retries: 0 };
|
|
1348
|
+
const planPath = path9.join(ctx.taskDir, "plan.md");
|
|
1349
|
+
if (fs9.existsSync(planPath)) {
|
|
1350
|
+
const plan = fs9.readFileSync(planPath, "utf-8").trim();
|
|
1351
|
+
if (plan) {
|
|
1352
|
+
const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
|
|
1353
|
+
sections.push(`
|
|
1354
|
+
<details><summary>\u{1F4CB} Implementation plan</summary>
|
|
1355
|
+
|
|
1356
|
+
${truncated}
|
|
1357
|
+
</details>`);
|
|
1358
|
+
}
|
|
885
1359
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
}
|
|
889
|
-
function validateStageOutput(stageName, content) {
|
|
890
|
-
switch (stageName) {
|
|
891
|
-
case "taskify":
|
|
892
|
-
return validateTaskJson(content);
|
|
893
|
-
case "plan":
|
|
894
|
-
return validatePlanMd(content);
|
|
895
|
-
case "review":
|
|
896
|
-
return validateReviewMd(content);
|
|
897
|
-
default:
|
|
898
|
-
return { valid: true };
|
|
1360
|
+
if (ctx.input.issueNumber) {
|
|
1361
|
+
sections.push(`
|
|
1362
|
+
Closes #${ctx.input.issueNumber}`);
|
|
899
1363
|
}
|
|
1364
|
+
sections.push(`
|
|
1365
|
+
---
|
|
1366
|
+
\u{1F916} Generated by Kody`);
|
|
1367
|
+
return sections.join("\n");
|
|
900
1368
|
}
|
|
901
|
-
|
|
1369
|
+
function executeShipStage(ctx, _def) {
|
|
1370
|
+
const shipPath = path9.join(ctx.taskDir, "ship.md");
|
|
902
1371
|
if (ctx.input.dryRun) {
|
|
903
|
-
|
|
904
|
-
return { outcome: "completed", retries: 0 };
|
|
1372
|
+
fs9.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
|
|
1373
|
+
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
905
1374
|
}
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
const runnerName = config.agent.stageRunners?.[def.name] ?? config.agent.defaultRunner ?? Object.keys(ctx.runners)[0] ?? "claude";
|
|
910
|
-
logger.info(` runner=${runnerName} model=${model} timeout=${def.timeout / 1e3}s`);
|
|
911
|
-
const extraEnv = {};
|
|
912
|
-
if (config.agent.litellmUrl) {
|
|
913
|
-
extraEnv.ANTHROPIC_BASE_URL = config.agent.litellmUrl;
|
|
1375
|
+
if (ctx.input.local && !ctx.input.issueNumber) {
|
|
1376
|
+
fs9.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
|
|
1377
|
+
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
914
1378
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
1379
|
+
try {
|
|
1380
|
+
const head = getCurrentBranch(ctx.projectDir);
|
|
1381
|
+
const base = getDefaultBranch(ctx.projectDir);
|
|
1382
|
+
pushBranch(ctx.projectDir);
|
|
1383
|
+
const config = getProjectConfig();
|
|
1384
|
+
let owner = config.github?.owner;
|
|
1385
|
+
let repo = config.github?.repo;
|
|
1386
|
+
if (!owner || !repo) {
|
|
1387
|
+
try {
|
|
1388
|
+
const remoteUrl = execFileSync7("git", ["remote", "get-url", "origin"], {
|
|
1389
|
+
encoding: "utf-8",
|
|
1390
|
+
cwd: ctx.projectDir
|
|
1391
|
+
}).trim();
|
|
1392
|
+
const match = remoteUrl.match(/github\.com[/:]([^/]+)\/([^/.]+)/);
|
|
1393
|
+
if (match) {
|
|
1394
|
+
owner = match[1];
|
|
1395
|
+
repo = match[2];
|
|
1396
|
+
}
|
|
1397
|
+
} catch {
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
let title = "Update";
|
|
1401
|
+
const TYPE_PREFIX = {
|
|
1402
|
+
feature: "feat",
|
|
1403
|
+
bugfix: "fix",
|
|
1404
|
+
refactor: "refactor",
|
|
1405
|
+
docs: "docs",
|
|
1406
|
+
chore: "chore"
|
|
1407
|
+
};
|
|
1408
|
+
const taskJsonPath = path9.join(ctx.taskDir, "task.json");
|
|
1409
|
+
if (fs9.existsSync(taskJsonPath)) {
|
|
1410
|
+
try {
|
|
1411
|
+
const raw = fs9.readFileSync(taskJsonPath, "utf-8");
|
|
1412
|
+
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
1413
|
+
const task = JSON.parse(cleaned);
|
|
1414
|
+
const prefix = TYPE_PREFIX[task.task_type] ?? "chore";
|
|
1415
|
+
const taskTitle = task.title ?? "Update";
|
|
1416
|
+
title = `${prefix}: ${taskTitle}`.slice(0, 72);
|
|
1417
|
+
} catch {
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
if (title === "Update") {
|
|
1421
|
+
const taskMdPath = path9.join(ctx.taskDir, "task.md");
|
|
1422
|
+
if (fs9.existsSync(taskMdPath)) {
|
|
1423
|
+
const content = fs9.readFileSync(taskMdPath, "utf-8");
|
|
1424
|
+
const firstLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith("*"));
|
|
1425
|
+
if (firstLine) title = `chore: ${firstLine.trim()}`.slice(0, 72);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
const body = buildPrBody(ctx);
|
|
1429
|
+
const pr = createPR(head, base, title, body);
|
|
1430
|
+
if (pr) {
|
|
1431
|
+
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
1432
|
+
try {
|
|
1433
|
+
postComment(ctx.input.issueNumber, `\u{1F389} PR created: ${pr.url}`);
|
|
1434
|
+
} catch {
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
fs9.writeFileSync(shipPath, `# Ship
|
|
1438
|
+
|
|
1439
|
+
PR created: ${pr.url}
|
|
1440
|
+
PR #${pr.number}
|
|
1441
|
+
`);
|
|
1442
|
+
} else {
|
|
1443
|
+
fs9.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
|
|
1444
|
+
}
|
|
1445
|
+
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
1446
|
+
} catch (err) {
|
|
1447
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1448
|
+
fs9.writeFileSync(shipPath, `# Ship
|
|
1449
|
+
|
|
1450
|
+
Failed: ${msg}
|
|
1451
|
+
`);
|
|
1452
|
+
return { outcome: "failed", retries: 0, error: msg };
|
|
922
1453
|
}
|
|
923
|
-
|
|
924
|
-
|
|
1454
|
+
}
|
|
1455
|
+
var init_ship = __esm({
|
|
1456
|
+
"src/stages/ship.ts"() {
|
|
1457
|
+
"use strict";
|
|
1458
|
+
init_git_utils();
|
|
1459
|
+
init_github_api();
|
|
1460
|
+
init_config();
|
|
925
1461
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
// src/pipeline/executor-registry.ts
|
|
1465
|
+
function getExecutor(name) {
|
|
1466
|
+
const executor = EXECUTOR_REGISTRY[name];
|
|
1467
|
+
if (!executor) {
|
|
1468
|
+
throw new Error(`No executor registered for stage: ${name}`);
|
|
1469
|
+
}
|
|
1470
|
+
return executor;
|
|
1471
|
+
}
|
|
1472
|
+
var EXECUTOR_REGISTRY;
|
|
1473
|
+
var init_executor_registry = __esm({
|
|
1474
|
+
"src/pipeline/executor-registry.ts"() {
|
|
1475
|
+
"use strict";
|
|
1476
|
+
init_agent();
|
|
1477
|
+
init_verify();
|
|
1478
|
+
init_review();
|
|
1479
|
+
init_ship();
|
|
1480
|
+
EXECUTOR_REGISTRY = {
|
|
1481
|
+
taskify: executeAgentStage,
|
|
1482
|
+
plan: executeAgentStage,
|
|
1483
|
+
build: executeAgentStage,
|
|
1484
|
+
verify: executeVerifyWithAutofix,
|
|
1485
|
+
review: executeReviewWithFix,
|
|
1486
|
+
"review-fix": executeAgentStage,
|
|
1487
|
+
ship: executeShipStage
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
// src/pipeline/questions.ts
|
|
1493
|
+
import * as fs10 from "fs";
|
|
1494
|
+
import * as path10 from "path";
|
|
1495
|
+
function checkForQuestions(ctx, stageName) {
|
|
1496
|
+
if (ctx.input.local || !ctx.input.issueNumber) return false;
|
|
1497
|
+
try {
|
|
1498
|
+
if (stageName === "taskify") {
|
|
1499
|
+
const taskJsonPath = path10.join(ctx.taskDir, "task.json");
|
|
1500
|
+
if (!fs10.existsSync(taskJsonPath)) return false;
|
|
1501
|
+
const raw = fs10.readFileSync(taskJsonPath, "utf-8");
|
|
1502
|
+
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
1503
|
+
const taskJson = JSON.parse(cleaned);
|
|
1504
|
+
if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
|
|
1505
|
+
const body = `\u{1F914} **Kody has questions before proceeding:**
|
|
1506
|
+
|
|
1507
|
+
${taskJson.questions.map((q, i) => `${i + 1}. ${q}`).join("\n")}
|
|
1508
|
+
|
|
1509
|
+
Reply with \`@kody approve\` and your answers in the comment body.`;
|
|
1510
|
+
postComment(ctx.input.issueNumber, body);
|
|
1511
|
+
setLifecycleLabel(ctx.input.issueNumber, "waiting");
|
|
1512
|
+
return true;
|
|
938
1513
|
}
|
|
939
1514
|
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
const
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1515
|
+
if (stageName === "plan") {
|
|
1516
|
+
const planPath = path10.join(ctx.taskDir, "plan.md");
|
|
1517
|
+
if (!fs10.existsSync(planPath)) return false;
|
|
1518
|
+
const plan = fs10.readFileSync(planPath, "utf-8");
|
|
1519
|
+
const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
1520
|
+
if (questionsMatch) {
|
|
1521
|
+
const questionsText = questionsMatch[1].trim();
|
|
1522
|
+
const questions = questionsText.split("\n").filter((l) => l.startsWith("- ")).map((l) => l.slice(2));
|
|
1523
|
+
if (questions.length > 0) {
|
|
1524
|
+
const body = `\u{1F3D7}\uFE0F **Kody has architecture questions:**
|
|
1525
|
+
|
|
1526
|
+
${questions.map((q, i) => `${i + 1}. ${q}`).join("\n")}
|
|
1527
|
+
|
|
1528
|
+
Reply with \`@kody approve\` and your answers in the comment body.`;
|
|
1529
|
+
postComment(ctx.input.issueNumber, body);
|
|
1530
|
+
setLifecycleLabel(ctx.input.issueNumber, "waiting");
|
|
1531
|
+
return true;
|
|
1532
|
+
}
|
|
948
1533
|
}
|
|
949
1534
|
}
|
|
1535
|
+
} catch {
|
|
950
1536
|
}
|
|
951
|
-
return
|
|
1537
|
+
return false;
|
|
952
1538
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
}
|
|
958
|
-
const verifyResult = runQualityGates(ctx.taskDir, ctx.projectDir);
|
|
959
|
-
const lines = [
|
|
960
|
-
`# Verification Report
|
|
961
|
-
`,
|
|
962
|
-
`## Result: ${verifyResult.pass ? "PASS" : "FAIL"}
|
|
963
|
-
`
|
|
964
|
-
];
|
|
965
|
-
if (verifyResult.errors.length > 0) {
|
|
966
|
-
lines.push(`
|
|
967
|
-
## Errors
|
|
968
|
-
`);
|
|
969
|
-
for (const e of verifyResult.errors) {
|
|
970
|
-
lines.push(`- ${e}
|
|
971
|
-
`);
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
if (verifyResult.summary.length > 0) {
|
|
975
|
-
lines.push(`
|
|
976
|
-
## Summary
|
|
977
|
-
`);
|
|
978
|
-
for (const s of verifyResult.summary) {
|
|
979
|
-
lines.push(`- ${s}
|
|
980
|
-
`);
|
|
981
|
-
}
|
|
1539
|
+
var init_questions = __esm({
|
|
1540
|
+
"src/pipeline/questions.ts"() {
|
|
1541
|
+
"use strict";
|
|
1542
|
+
init_github_api();
|
|
982
1543
|
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1544
|
+
});
|
|
1545
|
+
|
|
1546
|
+
// src/pipeline/hooks.ts
|
|
1547
|
+
import * as fs11 from "fs";
|
|
1548
|
+
import * as path11 from "path";
|
|
1549
|
+
function applyPreStageLabel(ctx, def) {
|
|
1550
|
+
if (!ctx.input.issueNumber || ctx.input.local) return;
|
|
1551
|
+
if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
|
|
1552
|
+
if (def.name === "review") setLifecycleLabel(ctx.input.issueNumber, "review");
|
|
1553
|
+
}
|
|
1554
|
+
function checkQuestionsAfterStage(ctx, def, state) {
|
|
1555
|
+
if (def.name !== "taskify" && def.name !== "plan") return null;
|
|
1556
|
+
if (ctx.input.dryRun) return null;
|
|
1557
|
+
const paused = checkForQuestions(ctx, def.name);
|
|
1558
|
+
if (!paused) return null;
|
|
1559
|
+
state.state = "failed";
|
|
1560
|
+
state.stages[def.name] = {
|
|
1561
|
+
...state.stages[def.name],
|
|
1562
|
+
state: "completed",
|
|
1563
|
+
error: "paused: waiting for answers"
|
|
987
1564
|
};
|
|
1565
|
+
writeState(state, ctx.taskDir);
|
|
1566
|
+
logger.info(` Pipeline paused \u2014 questions posted on issue`);
|
|
1567
|
+
return state;
|
|
988
1568
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
const
|
|
994
|
-
if (
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1569
|
+
function autoDetectComplexity(ctx, def) {
|
|
1570
|
+
if (def.name !== "taskify") return null;
|
|
1571
|
+
if (ctx.input.complexity) return null;
|
|
1572
|
+
try {
|
|
1573
|
+
const taskJsonPath = path11.join(ctx.taskDir, "task.json");
|
|
1574
|
+
if (!fs11.existsSync(taskJsonPath)) return null;
|
|
1575
|
+
const raw = fs11.readFileSync(taskJsonPath, "utf-8");
|
|
1576
|
+
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
1577
|
+
const taskJson = JSON.parse(cleaned);
|
|
1578
|
+
if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
|
|
1579
|
+
const complexity = taskJson.risk_level;
|
|
1580
|
+
const activeStages = filterByComplexity(STAGES, complexity);
|
|
1581
|
+
logger.info(` Complexity auto-detected: ${complexity} (${activeStages.map((s) => s.name).join(" \u2192 ")})`);
|
|
1582
|
+
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
1583
|
+
try {
|
|
1584
|
+
setLifecycleLabel(ctx.input.issueNumber, complexity);
|
|
1585
|
+
} catch {
|
|
1586
|
+
}
|
|
1587
|
+
if (taskJson.task_type) {
|
|
1003
1588
|
try {
|
|
1004
|
-
|
|
1005
|
-
stdio: "pipe",
|
|
1006
|
-
timeout: FIX_COMMAND_TIMEOUT_MS
|
|
1007
|
-
});
|
|
1589
|
+
setLabel(ctx.input.issueNumber, `kody:${taskJson.task_type}`);
|
|
1008
1590
|
} catch {
|
|
1009
1591
|
}
|
|
1010
|
-
};
|
|
1011
|
-
runFix(config.quality.lintFix);
|
|
1012
|
-
runFix(config.quality.formatFix);
|
|
1013
|
-
if (def.retryWithAgent) {
|
|
1014
|
-
logger.info(` running ${def.retryWithAgent} agent...`);
|
|
1015
|
-
await executeAgentStage(ctx, {
|
|
1016
|
-
...def,
|
|
1017
|
-
name: def.retryWithAgent,
|
|
1018
|
-
type: "agent",
|
|
1019
|
-
modelTier: "mid",
|
|
1020
|
-
timeout: 3e5,
|
|
1021
|
-
outputFile: void 0
|
|
1022
|
-
});
|
|
1023
1592
|
}
|
|
1024
1593
|
}
|
|
1594
|
+
return { complexity, activeStages };
|
|
1595
|
+
} catch {
|
|
1596
|
+
return null;
|
|
1025
1597
|
}
|
|
1026
|
-
return {
|
|
1027
|
-
outcome: "failed",
|
|
1028
|
-
retries: maxAttempts,
|
|
1029
|
-
error: "Verification failed after autofix attempts"
|
|
1030
|
-
};
|
|
1031
1598
|
}
|
|
1032
|
-
|
|
1033
|
-
if (ctx.input.dryRun)
|
|
1034
|
-
|
|
1599
|
+
function commitAfterStage(ctx, def) {
|
|
1600
|
+
if (ctx.input.dryRun || !ctx.input.issueNumber) return;
|
|
1601
|
+
if (def.name === "build") {
|
|
1602
|
+
try {
|
|
1603
|
+
commitAll(`feat(${ctx.taskId}): implement task`, ctx.projectDir);
|
|
1604
|
+
} catch {
|
|
1605
|
+
}
|
|
1035
1606
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1607
|
+
if (def.name === "review-fix") {
|
|
1608
|
+
try {
|
|
1609
|
+
commitAll(`fix(${ctx.taskId}): address review`, ctx.projectDir);
|
|
1610
|
+
} catch {
|
|
1611
|
+
}
|
|
1041
1612
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1613
|
+
}
|
|
1614
|
+
function postSkippedStagesComment(ctx, complexity, activeStages) {
|
|
1615
|
+
if (!ctx.input.issueNumber || ctx.input.local || ctx.input.dryRun) return;
|
|
1616
|
+
const skipped = STAGES.filter((s) => !activeStages.find((a) => a.name === s.name)).map((s) => s.name);
|
|
1617
|
+
if (skipped.length === 0) return;
|
|
1618
|
+
try {
|
|
1619
|
+
postComment(
|
|
1620
|
+
ctx.input.issueNumber,
|
|
1621
|
+
`\u26A1 **Complexity: ${complexity}** \u2014 skipping ${skipped.join(", ")} (not needed for ${complexity}-risk tasks)`
|
|
1622
|
+
);
|
|
1623
|
+
} catch {
|
|
1045
1624
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1625
|
+
}
|
|
1626
|
+
var init_hooks = __esm({
|
|
1627
|
+
"src/pipeline/hooks.ts"() {
|
|
1628
|
+
"use strict";
|
|
1629
|
+
init_definitions();
|
|
1630
|
+
init_github_api();
|
|
1631
|
+
init_git_utils();
|
|
1632
|
+
init_questions();
|
|
1633
|
+
init_complexity();
|
|
1634
|
+
init_state();
|
|
1635
|
+
init_logger();
|
|
1050
1636
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1637
|
+
});
|
|
1638
|
+
|
|
1639
|
+
// src/learning/auto-learn.ts
|
|
1640
|
+
import * as fs12 from "fs";
|
|
1641
|
+
import * as path12 from "path";
|
|
1642
|
+
function stripAnsi(str) {
|
|
1643
|
+
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
1644
|
+
}
|
|
1645
|
+
function autoLearn(ctx) {
|
|
1646
|
+
try {
|
|
1647
|
+
const memoryDir = path12.join(ctx.projectDir, ".kody", "memory");
|
|
1648
|
+
if (!fs12.existsSync(memoryDir)) {
|
|
1649
|
+
fs12.mkdirSync(memoryDir, { recursive: true });
|
|
1650
|
+
}
|
|
1651
|
+
const learnings = [];
|
|
1652
|
+
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1653
|
+
const verifyPath = path12.join(ctx.taskDir, "verify.md");
|
|
1654
|
+
if (fs12.existsSync(verifyPath)) {
|
|
1655
|
+
const verify = stripAnsi(fs12.readFileSync(verifyPath, "utf-8"));
|
|
1656
|
+
if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
|
|
1657
|
+
if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
|
|
1658
|
+
if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
|
|
1659
|
+
if (/prettier/i.test(verify)) learnings.push("- Uses prettier for formatting");
|
|
1660
|
+
if (/tsc\b/i.test(verify)) learnings.push("- Uses TypeScript (tsc)");
|
|
1661
|
+
if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
|
|
1662
|
+
if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
|
|
1663
|
+
}
|
|
1664
|
+
const reviewPath = path12.join(ctx.taskDir, "review.md");
|
|
1665
|
+
if (fs12.existsSync(reviewPath)) {
|
|
1666
|
+
const review = fs12.readFileSync(reviewPath, "utf-8");
|
|
1667
|
+
if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
|
|
1668
|
+
if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
|
|
1669
|
+
if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
|
|
1670
|
+
if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
|
|
1671
|
+
}
|
|
1672
|
+
const taskJsonPath = path12.join(ctx.taskDir, "task.json");
|
|
1673
|
+
if (fs12.existsSync(taskJsonPath)) {
|
|
1674
|
+
try {
|
|
1675
|
+
const raw = stripAnsi(fs12.readFileSync(taskJsonPath, "utf-8"));
|
|
1676
|
+
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
1677
|
+
const task = JSON.parse(cleaned);
|
|
1678
|
+
if (task.scope && Array.isArray(task.scope)) {
|
|
1679
|
+
const dirs = [...new Set(task.scope.map((s) => s.split("/").slice(0, -1).join("/")).filter(Boolean))];
|
|
1680
|
+
if (dirs.length > 0) learnings.push(`- Active directories: ${dirs.join(", ")}`);
|
|
1681
|
+
}
|
|
1682
|
+
} catch {
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
if (learnings.length > 0) {
|
|
1686
|
+
const conventionsPath = path12.join(memoryDir, "conventions.md");
|
|
1687
|
+
const entry = `
|
|
1688
|
+
## Learned ${timestamp2} (task: ${ctx.taskId})
|
|
1689
|
+
${learnings.join("\n")}
|
|
1690
|
+
`;
|
|
1691
|
+
fs12.appendFileSync(conventionsPath, entry);
|
|
1692
|
+
logger.info(`Auto-learned ${learnings.length} convention(s)`);
|
|
1693
|
+
}
|
|
1694
|
+
autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
|
|
1695
|
+
} catch {
|
|
1055
1696
|
}
|
|
1056
|
-
logger.info(` re-running review after fix...`);
|
|
1057
|
-
return executeAgentStage(ctx, reviewDef);
|
|
1058
1697
|
}
|
|
1059
|
-
function
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
|
|
1698
|
+
function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
1699
|
+
const archPath = path12.join(memoryDir, "architecture.md");
|
|
1700
|
+
if (fs12.existsSync(archPath)) return;
|
|
1701
|
+
const detected = [];
|
|
1702
|
+
const pkgPath = path12.join(projectDir, "package.json");
|
|
1703
|
+
if (fs12.existsSync(pkgPath)) {
|
|
1063
1704
|
try {
|
|
1064
|
-
const
|
|
1065
|
-
const
|
|
1066
|
-
|
|
1067
|
-
if (
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
${
|
|
1071
|
-
}
|
|
1072
|
-
if (
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1705
|
+
const pkg = JSON.parse(fs12.readFileSync(pkgPath, "utf-8"));
|
|
1706
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1707
|
+
if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
|
|
1708
|
+
else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
|
|
1709
|
+
else if (allDeps.express) detected.push(`- Framework: Express ${allDeps.express}`);
|
|
1710
|
+
else if (allDeps.fastify) detected.push(`- Framework: Fastify ${allDeps.fastify}`);
|
|
1711
|
+
if (allDeps.typescript) detected.push(`- Language: TypeScript ${allDeps.typescript}`);
|
|
1712
|
+
if (allDeps.vitest) detected.push(`- Testing: vitest ${allDeps.vitest}`);
|
|
1713
|
+
else if (allDeps.jest) detected.push(`- Testing: jest ${allDeps.jest}`);
|
|
1714
|
+
if (allDeps.eslint) detected.push(`- Linting: eslint ${allDeps.eslint}`);
|
|
1715
|
+
if (allDeps.prisma || allDeps["@prisma/client"]) detected.push("- Database: Prisma ORM");
|
|
1716
|
+
if (allDeps.drizzle || allDeps["drizzle-orm"]) detected.push("- Database: Drizzle ORM");
|
|
1717
|
+
if (allDeps.pg || allDeps.postgres) detected.push("- Database: PostgreSQL");
|
|
1718
|
+
if (allDeps.payload || allDeps["@payloadcms/next"]) detected.push(`- CMS: Payload CMS`);
|
|
1719
|
+
if (pkg.type === "module") detected.push("- Module system: ESM");
|
|
1720
|
+
else detected.push("- Module system: CommonJS");
|
|
1721
|
+
if (fs12.existsSync(path12.join(projectDir, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
|
|
1722
|
+
else if (fs12.existsSync(path12.join(projectDir, "yarn.lock"))) detected.push("- Package manager: yarn");
|
|
1723
|
+
else if (fs12.existsSync(path12.join(projectDir, "package-lock.json"))) detected.push("- Package manager: npm");
|
|
1080
1724
|
} catch {
|
|
1081
1725
|
}
|
|
1082
1726
|
}
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
const
|
|
1086
|
-
const
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
if (summary) {
|
|
1090
|
-
sections.push(`
|
|
1091
|
-
## Changes
|
|
1092
|
-
|
|
1093
|
-
${summary}`);
|
|
1727
|
+
const topDirs = [];
|
|
1728
|
+
try {
|
|
1729
|
+
const entries = fs12.readdirSync(projectDir, { withFileTypes: true });
|
|
1730
|
+
for (const entry of entries) {
|
|
1731
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
1732
|
+
topDirs.push(entry.name);
|
|
1094
1733
|
}
|
|
1095
1734
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
sections.push(`
|
|
1099
|
-
**Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
const verifyPath = path4.join(ctx.taskDir, "verify.md");
|
|
1103
|
-
if (fs4.existsSync(verifyPath)) {
|
|
1104
|
-
const verify = fs4.readFileSync(verifyPath, "utf-8");
|
|
1105
|
-
if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
|
|
1735
|
+
if (topDirs.length > 0) detected.push(`- Top-level directories: ${topDirs.join(", ")}`);
|
|
1736
|
+
} catch {
|
|
1106
1737
|
}
|
|
1107
|
-
const
|
|
1108
|
-
if (
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
const
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
${truncated}
|
|
1116
|
-
</details>`);
|
|
1738
|
+
const srcDir = path12.join(projectDir, "src");
|
|
1739
|
+
if (fs12.existsSync(srcDir)) {
|
|
1740
|
+
try {
|
|
1741
|
+
const srcEntries = fs12.readdirSync(srcDir, { withFileTypes: true });
|
|
1742
|
+
const srcDirs = srcEntries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1743
|
+
if (srcDirs.length > 0) detected.push(`- src/ structure: ${srcDirs.join(", ")}`);
|
|
1744
|
+
} catch {
|
|
1117
1745
|
}
|
|
1118
1746
|
}
|
|
1119
|
-
if (
|
|
1120
|
-
|
|
1121
|
-
|
|
1747
|
+
if (detected.length > 0) {
|
|
1748
|
+
const content = `# Architecture (auto-detected ${timestamp2})
|
|
1749
|
+
|
|
1750
|
+
## Overview
|
|
1751
|
+
${detected.join("\n")}
|
|
1752
|
+
`;
|
|
1753
|
+
fs12.writeFileSync(archPath, content);
|
|
1754
|
+
logger.info(`Auto-detected architecture (${detected.length} items)`);
|
|
1122
1755
|
}
|
|
1123
|
-
sections.push(`
|
|
1124
|
-
---
|
|
1125
|
-
\u{1F916} Generated by Kody`);
|
|
1126
|
-
return sections.join("\n");
|
|
1127
1756
|
}
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
1133
|
-
}
|
|
1134
|
-
if (ctx.input.local && !ctx.input.issueNumber) {
|
|
1135
|
-
fs4.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
|
|
1136
|
-
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
1757
|
+
var init_auto_learn = __esm({
|
|
1758
|
+
"src/learning/auto-learn.ts"() {
|
|
1759
|
+
"use strict";
|
|
1760
|
+
init_logger();
|
|
1137
1761
|
}
|
|
1762
|
+
});
|
|
1763
|
+
|
|
1764
|
+
// src/pipeline.ts
|
|
1765
|
+
import * as fs13 from "fs";
|
|
1766
|
+
import * as path13 from "path";
|
|
1767
|
+
function ensureFeatureBranchIfNeeded(ctx) {
|
|
1768
|
+
if (!ctx.input.issueNumber || ctx.input.dryRun) return;
|
|
1138
1769
|
try {
|
|
1139
|
-
const
|
|
1140
|
-
const
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
if (match) {
|
|
1153
|
-
owner = match[1];
|
|
1154
|
-
repo = match[2];
|
|
1155
|
-
}
|
|
1156
|
-
} catch {
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
let title = "Update";
|
|
1160
|
-
const TYPE_PREFIX = {
|
|
1161
|
-
feature: "feat",
|
|
1162
|
-
bugfix: "fix",
|
|
1163
|
-
refactor: "refactor",
|
|
1164
|
-
docs: "docs",
|
|
1165
|
-
chore: "chore"
|
|
1166
|
-
};
|
|
1167
|
-
const taskJsonPath = path4.join(ctx.taskDir, "task.json");
|
|
1168
|
-
if (fs4.existsSync(taskJsonPath)) {
|
|
1770
|
+
const taskMdPath = path13.join(ctx.taskDir, "task.md");
|
|
1771
|
+
const title = fs13.existsSync(taskMdPath) ? fs13.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
|
|
1772
|
+
ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
|
|
1773
|
+
syncWithDefault(ctx.projectDir);
|
|
1774
|
+
} catch (err) {
|
|
1775
|
+
logger.warn(` Failed to create/sync feature branch: ${err}`);
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
function acquireLock(taskDir) {
|
|
1779
|
+
const lockPath = path13.join(taskDir, ".lock");
|
|
1780
|
+
if (fs13.existsSync(lockPath)) {
|
|
1781
|
+
try {
|
|
1782
|
+
const pid = parseInt(fs13.readFileSync(lockPath, "utf-8").trim(), 10);
|
|
1169
1783
|
try {
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
const taskTitle = task.title ?? "Update";
|
|
1175
|
-
title = `${prefix}: ${taskTitle}`.slice(0, 72);
|
|
1176
|
-
} catch {
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
if (title === "Update") {
|
|
1180
|
-
const taskMdPath = path4.join(ctx.taskDir, "task.md");
|
|
1181
|
-
if (fs4.existsSync(taskMdPath)) {
|
|
1182
|
-
const content = fs4.readFileSync(taskMdPath, "utf-8");
|
|
1183
|
-
const firstLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith("*"));
|
|
1184
|
-
if (firstLine) title = `chore: ${firstLine.trim()}`.slice(0, 72);
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
const body = buildPrBody(ctx);
|
|
1188
|
-
const pr = createPR(head, base, title, body);
|
|
1189
|
-
if (pr) {
|
|
1190
|
-
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
1191
|
-
try {
|
|
1192
|
-
postComment(ctx.input.issueNumber, `\u{1F389} PR created: ${pr.url}`);
|
|
1193
|
-
} catch {
|
|
1194
|
-
}
|
|
1784
|
+
process.kill(pid, 0);
|
|
1785
|
+
throw new Error(`Pipeline already running (PID ${pid})`);
|
|
1786
|
+
} catch (e) {
|
|
1787
|
+
if (e.code !== "ESRCH") throw e;
|
|
1195
1788
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
PR created: ${pr.url}
|
|
1199
|
-
PR #${pr.number}
|
|
1200
|
-
`);
|
|
1201
|
-
} else {
|
|
1202
|
-
fs4.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
|
|
1789
|
+
} catch (e) {
|
|
1790
|
+
if (e instanceof Error && e.message.startsWith("Pipeline already")) throw e;
|
|
1203
1791
|
}
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
return { outcome: "failed", retries: 0, error: msg };
|
|
1792
|
+
}
|
|
1793
|
+
fs13.writeFileSync(lockPath, String(process.pid));
|
|
1794
|
+
}
|
|
1795
|
+
function releaseLock(taskDir) {
|
|
1796
|
+
try {
|
|
1797
|
+
fs13.unlinkSync(path13.join(taskDir, ".lock"));
|
|
1798
|
+
} catch {
|
|
1212
1799
|
}
|
|
1213
1800
|
}
|
|
1214
1801
|
async function runPipeline(ctx) {
|
|
1802
|
+
acquireLock(ctx.taskDir);
|
|
1803
|
+
try {
|
|
1804
|
+
return await runPipelineInner(ctx);
|
|
1805
|
+
} finally {
|
|
1806
|
+
releaseLock(ctx.taskDir);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
async function runPipelineInner(ctx) {
|
|
1215
1810
|
let state = loadState(ctx.taskId, ctx.taskDir);
|
|
1216
1811
|
if (!state) {
|
|
1217
1812
|
state = initState(ctx.taskId);
|
|
@@ -1236,16 +1831,7 @@ async function runPipeline(ctx) {
|
|
|
1236
1831
|
const initialPhase = ctx.input.mode === "rerun" ? "building" : "planning";
|
|
1237
1832
|
setLifecycleLabel(ctx.input.issueNumber, initialPhase);
|
|
1238
1833
|
}
|
|
1239
|
-
|
|
1240
|
-
try {
|
|
1241
|
-
const taskMdPath = path4.join(ctx.taskDir, "task.md");
|
|
1242
|
-
const title = fs4.existsSync(taskMdPath) ? fs4.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
|
|
1243
|
-
ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
|
|
1244
|
-
syncWithDefault(ctx.projectDir);
|
|
1245
|
-
} catch (err) {
|
|
1246
|
-
logger.warn(` Failed to create/sync feature branch: ${err}`);
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1834
|
+
ensureFeatureBranchIfNeeded(ctx);
|
|
1249
1835
|
let complexity = ctx.input.complexity ?? "high";
|
|
1250
1836
|
let activeStages = filterByComplexity(STAGES, complexity);
|
|
1251
1837
|
let skippedStagesCommentPosted = false;
|
|
@@ -1265,50 +1851,20 @@ async function runPipeline(ctx) {
|
|
|
1265
1851
|
logger.info(`[${def.name}] skipped (complexity: ${complexity})`);
|
|
1266
1852
|
state.stages[def.name] = { state: "completed", retries: 0, outputFile: void 0 };
|
|
1267
1853
|
writeState(state, ctx.taskDir);
|
|
1268
|
-
if (!skippedStagesCommentPosted
|
|
1269
|
-
|
|
1270
|
-
try {
|
|
1271
|
-
postComment(
|
|
1272
|
-
ctx.input.issueNumber,
|
|
1273
|
-
`\u26A1 **Complexity: ${complexity}** \u2014 skipping ${skipped.join(", ")} (not needed for ${complexity}-risk tasks)`
|
|
1274
|
-
);
|
|
1275
|
-
} catch {
|
|
1276
|
-
}
|
|
1854
|
+
if (!skippedStagesCommentPosted) {
|
|
1855
|
+
postSkippedStagesComment(ctx, complexity, activeStages);
|
|
1277
1856
|
skippedStagesCommentPosted = true;
|
|
1278
1857
|
}
|
|
1279
1858
|
continue;
|
|
1280
1859
|
}
|
|
1281
1860
|
ciGroup(`Stage: ${def.name}`);
|
|
1282
|
-
state.stages[def.name] = {
|
|
1283
|
-
state: "running",
|
|
1284
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1285
|
-
retries: 0
|
|
1286
|
-
};
|
|
1861
|
+
state.stages[def.name] = { state: "running", startedAt: (/* @__PURE__ */ new Date()).toISOString(), retries: 0 };
|
|
1287
1862
|
writeState(state, ctx.taskDir);
|
|
1288
1863
|
logger.info(`[${def.name}] starting...`);
|
|
1289
|
-
|
|
1290
|
-
if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
|
|
1291
|
-
if (def.name === "review") setLifecycleLabel(ctx.input.issueNumber, "review");
|
|
1292
|
-
}
|
|
1864
|
+
applyPreStageLabel(ctx, def);
|
|
1293
1865
|
let result;
|
|
1294
1866
|
try {
|
|
1295
|
-
|
|
1296
|
-
if (def.name === "review") {
|
|
1297
|
-
result = await executeReviewWithFix(ctx, def);
|
|
1298
|
-
} else {
|
|
1299
|
-
result = await executeAgentStage(ctx, def);
|
|
1300
|
-
}
|
|
1301
|
-
} else if (def.type === "gate") {
|
|
1302
|
-
if (def.name === "verify") {
|
|
1303
|
-
result = await executeVerifyWithAutofix(ctx, def);
|
|
1304
|
-
} else {
|
|
1305
|
-
result = executeGateStage(ctx, def);
|
|
1306
|
-
}
|
|
1307
|
-
} else if (def.type === "deterministic") {
|
|
1308
|
-
result = executeShipStage(ctx, def);
|
|
1309
|
-
} else {
|
|
1310
|
-
result = { outcome: "failed", retries: 0, error: `Unknown stage type: ${def.type}` };
|
|
1311
|
-
}
|
|
1867
|
+
result = await getExecutor(def.name)(ctx, def);
|
|
1312
1868
|
} catch (error) {
|
|
1313
1869
|
result = {
|
|
1314
1870
|
outcome: "failed",
|
|
@@ -1325,84 +1881,24 @@ async function runPipeline(ctx) {
|
|
|
1325
1881
|
outputFile: result.outputFile
|
|
1326
1882
|
};
|
|
1327
1883
|
logger.info(`[${def.name}] \u2713 completed`);
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
state: "completed",
|
|
1335
|
-
error: "paused: waiting for answers"
|
|
1336
|
-
};
|
|
1337
|
-
writeState(state, ctx.taskDir);
|
|
1338
|
-
logger.info(` Pipeline paused \u2014 questions posted on issue`);
|
|
1339
|
-
return state;
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
if (def.name === "taskify" && !ctx.input.complexity) {
|
|
1343
|
-
try {
|
|
1344
|
-
const taskJsonPath = path4.join(ctx.taskDir, "task.json");
|
|
1345
|
-
if (fs4.existsSync(taskJsonPath)) {
|
|
1346
|
-
const raw = fs4.readFileSync(taskJsonPath, "utf-8");
|
|
1347
|
-
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
1348
|
-
const taskJson = JSON.parse(cleaned);
|
|
1349
|
-
if (taskJson.risk_level && COMPLEXITY_SKIP[taskJson.risk_level]) {
|
|
1350
|
-
complexity = taskJson.risk_level;
|
|
1351
|
-
activeStages = filterByComplexity(STAGES, complexity);
|
|
1352
|
-
logger.info(` Complexity auto-detected: ${complexity} (${activeStages.map((s) => s.name).join(" \u2192 ")})`);
|
|
1353
|
-
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
1354
|
-
try {
|
|
1355
|
-
setLifecycleLabel(ctx.input.issueNumber, complexity);
|
|
1356
|
-
} catch {
|
|
1357
|
-
}
|
|
1358
|
-
if (taskJson.task_type) {
|
|
1359
|
-
try {
|
|
1360
|
-
setLabel(ctx.input.issueNumber, `kody:${taskJson.task_type}`);
|
|
1361
|
-
} catch {
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
} catch {
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
if (!ctx.input.dryRun && ctx.input.issueNumber) {
|
|
1371
|
-
if (def.name === "build") {
|
|
1372
|
-
try {
|
|
1373
|
-
commitAll(`feat(${ctx.taskId}): implement task`, ctx.projectDir);
|
|
1374
|
-
} catch {
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
if (def.name === "review-fix") {
|
|
1378
|
-
try {
|
|
1379
|
-
commitAll(`fix(${ctx.taskId}): address review`, ctx.projectDir);
|
|
1380
|
-
} catch {
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
} else if (result.outcome === "timed_out") {
|
|
1385
|
-
state.stages[def.name] = {
|
|
1386
|
-
state: "timeout",
|
|
1387
|
-
retries: result.retries,
|
|
1388
|
-
error: "Stage timed out"
|
|
1389
|
-
};
|
|
1390
|
-
state.state = "failed";
|
|
1391
|
-
writeState(state, ctx.taskDir);
|
|
1392
|
-
logger.error(`[${def.name}] \u23F1 timed out`);
|
|
1393
|
-
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
1394
|
-
setLifecycleLabel(ctx.input.issueNumber, "failed");
|
|
1884
|
+
const paused = checkQuestionsAfterStage(ctx, def, state);
|
|
1885
|
+
if (paused) return paused;
|
|
1886
|
+
const detected = autoDetectComplexity(ctx, def);
|
|
1887
|
+
if (detected) {
|
|
1888
|
+
complexity = detected.complexity;
|
|
1889
|
+
activeStages = detected.activeStages;
|
|
1395
1890
|
}
|
|
1396
|
-
|
|
1891
|
+
commitAfterStage(ctx, def);
|
|
1397
1892
|
} else {
|
|
1893
|
+
const isTimeout = result.outcome === "timed_out";
|
|
1398
1894
|
state.stages[def.name] = {
|
|
1399
|
-
state: "failed",
|
|
1895
|
+
state: isTimeout ? "timeout" : "failed",
|
|
1400
1896
|
retries: result.retries,
|
|
1401
|
-
error: result.error ?? "Stage failed"
|
|
1897
|
+
error: isTimeout ? "Stage timed out" : result.error ?? "Stage failed"
|
|
1402
1898
|
};
|
|
1403
1899
|
state.state = "failed";
|
|
1404
1900
|
writeState(state, ctx.taskDir);
|
|
1405
|
-
logger.error(`[${def.name}] \u2717 failed: ${result.error}`);
|
|
1901
|
+
logger.error(`[${def.name}] ${isTimeout ? "\u23F1 timed out" : `\u2717 failed: ${result.error}`}`);
|
|
1406
1902
|
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
1407
1903
|
setLifecycleLabel(ctx.input.issueNumber, "failed");
|
|
1408
1904
|
}
|
|
@@ -1410,9 +1906,7 @@ async function runPipeline(ctx) {
|
|
|
1410
1906
|
}
|
|
1411
1907
|
writeState(state, ctx.taskDir);
|
|
1412
1908
|
}
|
|
1413
|
-
const allCompleted = STAGES.every(
|
|
1414
|
-
(s) => state.stages[s.name].state === "completed"
|
|
1415
|
-
);
|
|
1909
|
+
const allCompleted = STAGES.every((s) => state.stages[s.name].state === "completed");
|
|
1416
1910
|
if (allCompleted) {
|
|
1417
1911
|
state.state = "completed";
|
|
1418
1912
|
writeState(state, ctx.taskDir);
|
|
@@ -1424,121 +1918,6 @@ async function runPipeline(ctx) {
|
|
|
1424
1918
|
}
|
|
1425
1919
|
return state;
|
|
1426
1920
|
}
|
|
1427
|
-
function stripAnsi(str) {
|
|
1428
|
-
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
1429
|
-
}
|
|
1430
|
-
function autoLearn(ctx) {
|
|
1431
|
-
try {
|
|
1432
|
-
const memoryDir = path4.join(ctx.projectDir, ".kody", "memory");
|
|
1433
|
-
if (!fs4.existsSync(memoryDir)) {
|
|
1434
|
-
fs4.mkdirSync(memoryDir, { recursive: true });
|
|
1435
|
-
}
|
|
1436
|
-
const learnings = [];
|
|
1437
|
-
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1438
|
-
const verifyPath = path4.join(ctx.taskDir, "verify.md");
|
|
1439
|
-
if (fs4.existsSync(verifyPath)) {
|
|
1440
|
-
const verify = stripAnsi(fs4.readFileSync(verifyPath, "utf-8"));
|
|
1441
|
-
if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
|
|
1442
|
-
if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
|
|
1443
|
-
if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
|
|
1444
|
-
if (/prettier/i.test(verify)) learnings.push("- Uses prettier for formatting");
|
|
1445
|
-
if (/tsc\b/i.test(verify)) learnings.push("- Uses TypeScript (tsc)");
|
|
1446
|
-
if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
|
|
1447
|
-
if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
|
|
1448
|
-
}
|
|
1449
|
-
const reviewPath = path4.join(ctx.taskDir, "review.md");
|
|
1450
|
-
if (fs4.existsSync(reviewPath)) {
|
|
1451
|
-
const review = fs4.readFileSync(reviewPath, "utf-8");
|
|
1452
|
-
if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
|
|
1453
|
-
if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
|
|
1454
|
-
if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
|
|
1455
|
-
if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
|
|
1456
|
-
}
|
|
1457
|
-
const taskJsonPath = path4.join(ctx.taskDir, "task.json");
|
|
1458
|
-
if (fs4.existsSync(taskJsonPath)) {
|
|
1459
|
-
try {
|
|
1460
|
-
const raw = stripAnsi(fs4.readFileSync(taskJsonPath, "utf-8"));
|
|
1461
|
-
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
1462
|
-
const task = JSON.parse(cleaned);
|
|
1463
|
-
if (task.scope && Array.isArray(task.scope)) {
|
|
1464
|
-
const dirs = [...new Set(task.scope.map((s) => s.split("/").slice(0, -1).join("/")).filter(Boolean))];
|
|
1465
|
-
if (dirs.length > 0) learnings.push(`- Active directories: ${dirs.join(", ")}`);
|
|
1466
|
-
}
|
|
1467
|
-
} catch {
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
if (learnings.length > 0) {
|
|
1471
|
-
const conventionsPath = path4.join(memoryDir, "conventions.md");
|
|
1472
|
-
const entry = `
|
|
1473
|
-
## Learned ${timestamp2} (task: ${ctx.taskId})
|
|
1474
|
-
${learnings.join("\n")}
|
|
1475
|
-
`;
|
|
1476
|
-
fs4.appendFileSync(conventionsPath, entry);
|
|
1477
|
-
logger.info(`Auto-learned ${learnings.length} convention(s)`);
|
|
1478
|
-
}
|
|
1479
|
-
autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
|
|
1480
|
-
} catch {
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
1484
|
-
const archPath = path4.join(memoryDir, "architecture.md");
|
|
1485
|
-
if (fs4.existsSync(archPath)) return;
|
|
1486
|
-
const detected = [];
|
|
1487
|
-
const pkgPath = path4.join(projectDir, "package.json");
|
|
1488
|
-
if (fs4.existsSync(pkgPath)) {
|
|
1489
|
-
try {
|
|
1490
|
-
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
1491
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1492
|
-
if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
|
|
1493
|
-
else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
|
|
1494
|
-
else if (allDeps.express) detected.push(`- Framework: Express ${allDeps.express}`);
|
|
1495
|
-
else if (allDeps.fastify) detected.push(`- Framework: Fastify ${allDeps.fastify}`);
|
|
1496
|
-
if (allDeps.typescript) detected.push(`- Language: TypeScript ${allDeps.typescript}`);
|
|
1497
|
-
if (allDeps.vitest) detected.push(`- Testing: vitest ${allDeps.vitest}`);
|
|
1498
|
-
else if (allDeps.jest) detected.push(`- Testing: jest ${allDeps.jest}`);
|
|
1499
|
-
if (allDeps.eslint) detected.push(`- Linting: eslint ${allDeps.eslint}`);
|
|
1500
|
-
if (allDeps.prisma || allDeps["@prisma/client"]) detected.push("- Database: Prisma ORM");
|
|
1501
|
-
if (allDeps.drizzle || allDeps["drizzle-orm"]) detected.push("- Database: Drizzle ORM");
|
|
1502
|
-
if (allDeps.pg || allDeps.postgres) detected.push("- Database: PostgreSQL");
|
|
1503
|
-
if (allDeps.payload || allDeps["@payloadcms/next"]) detected.push(`- CMS: Payload CMS`);
|
|
1504
|
-
if (pkg.type === "module") detected.push("- Module system: ESM");
|
|
1505
|
-
else detected.push("- Module system: CommonJS");
|
|
1506
|
-
if (fs4.existsSync(path4.join(projectDir, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
|
|
1507
|
-
else if (fs4.existsSync(path4.join(projectDir, "yarn.lock"))) detected.push("- Package manager: yarn");
|
|
1508
|
-
else if (fs4.existsSync(path4.join(projectDir, "package-lock.json"))) detected.push("- Package manager: npm");
|
|
1509
|
-
} catch {
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1512
|
-
const topDirs = [];
|
|
1513
|
-
try {
|
|
1514
|
-
const entries = fs4.readdirSync(projectDir, { withFileTypes: true });
|
|
1515
|
-
for (const entry of entries) {
|
|
1516
|
-
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
1517
|
-
topDirs.push(entry.name);
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
if (topDirs.length > 0) detected.push(`- Top-level directories: ${topDirs.join(", ")}`);
|
|
1521
|
-
} catch {
|
|
1522
|
-
}
|
|
1523
|
-
const srcDir = path4.join(projectDir, "src");
|
|
1524
|
-
if (fs4.existsSync(srcDir)) {
|
|
1525
|
-
try {
|
|
1526
|
-
const srcEntries = fs4.readdirSync(srcDir, { withFileTypes: true });
|
|
1527
|
-
const srcDirs = srcEntries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1528
|
-
if (srcDirs.length > 0) detected.push(`- src/ structure: ${srcDirs.join(", ")}`);
|
|
1529
|
-
} catch {
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
if (detected.length > 0) {
|
|
1533
|
-
const content = `# Architecture (auto-detected ${timestamp2})
|
|
1534
|
-
|
|
1535
|
-
## Overview
|
|
1536
|
-
${detected.join("\n")}
|
|
1537
|
-
`;
|
|
1538
|
-
fs4.writeFileSync(archPath, content);
|
|
1539
|
-
logger.info(`Auto-detected architecture (${detected.length} items)`);
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
1921
|
function printStatus(taskId, taskDir) {
|
|
1543
1922
|
const state = loadState(taskId, taskDir);
|
|
1544
1923
|
if (!state) {
|
|
@@ -1558,29 +1937,24 @@ Task: ${state.taskId}`);
|
|
|
1558
1937
|
console.log(` ${icon} ${stage.name}: ${s.state}${extra}`);
|
|
1559
1938
|
}
|
|
1560
1939
|
}
|
|
1561
|
-
var
|
|
1562
|
-
|
|
1563
|
-
"src/state-machine.ts"() {
|
|
1940
|
+
var init_pipeline = __esm({
|
|
1941
|
+
"src/pipeline.ts"() {
|
|
1564
1942
|
"use strict";
|
|
1565
1943
|
init_definitions();
|
|
1566
|
-
init_context();
|
|
1567
|
-
init_validators();
|
|
1568
1944
|
init_git_utils();
|
|
1569
1945
|
init_github_api();
|
|
1570
|
-
init_verify_runner();
|
|
1571
|
-
init_config();
|
|
1572
1946
|
init_logger();
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1947
|
+
init_state();
|
|
1948
|
+
init_complexity();
|
|
1949
|
+
init_executor_registry();
|
|
1950
|
+
init_hooks();
|
|
1951
|
+
init_auto_learn();
|
|
1578
1952
|
}
|
|
1579
1953
|
});
|
|
1580
1954
|
|
|
1581
1955
|
// src/preflight.ts
|
|
1582
|
-
import { execFileSync as
|
|
1583
|
-
import * as
|
|
1956
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
1957
|
+
import * as fs14 from "fs";
|
|
1584
1958
|
function check(name, fn) {
|
|
1585
1959
|
try {
|
|
1586
1960
|
const detail = fn() ?? void 0;
|
|
@@ -1592,7 +1966,7 @@ function check(name, fn) {
|
|
|
1592
1966
|
function runPreflight() {
|
|
1593
1967
|
const checks = [
|
|
1594
1968
|
check("claude CLI", () => {
|
|
1595
|
-
const v =
|
|
1969
|
+
const v = execFileSync8("claude", ["--version"], {
|
|
1596
1970
|
encoding: "utf-8",
|
|
1597
1971
|
timeout: 1e4,
|
|
1598
1972
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1600,14 +1974,14 @@ function runPreflight() {
|
|
|
1600
1974
|
return v;
|
|
1601
1975
|
}),
|
|
1602
1976
|
check("git repo", () => {
|
|
1603
|
-
|
|
1977
|
+
execFileSync8("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
1604
1978
|
encoding: "utf-8",
|
|
1605
1979
|
timeout: 5e3,
|
|
1606
1980
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1607
1981
|
});
|
|
1608
1982
|
}),
|
|
1609
1983
|
check("pnpm", () => {
|
|
1610
|
-
const v =
|
|
1984
|
+
const v = execFileSync8("pnpm", ["--version"], {
|
|
1611
1985
|
encoding: "utf-8",
|
|
1612
1986
|
timeout: 5e3,
|
|
1613
1987
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1615,7 +1989,7 @@ function runPreflight() {
|
|
|
1615
1989
|
return v;
|
|
1616
1990
|
}),
|
|
1617
1991
|
check("node >= 18", () => {
|
|
1618
|
-
const v =
|
|
1992
|
+
const v = execFileSync8("node", ["--version"], {
|
|
1619
1993
|
encoding: "utf-8",
|
|
1620
1994
|
timeout: 5e3,
|
|
1621
1995
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1625,7 +1999,7 @@ function runPreflight() {
|
|
|
1625
1999
|
return v;
|
|
1626
2000
|
}),
|
|
1627
2001
|
check("gh CLI", () => {
|
|
1628
|
-
const v =
|
|
2002
|
+
const v = execFileSync8("gh", ["--version"], {
|
|
1629
2003
|
encoding: "utf-8",
|
|
1630
2004
|
timeout: 5e3,
|
|
1631
2005
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1633,7 +2007,7 @@ function runPreflight() {
|
|
|
1633
2007
|
return v;
|
|
1634
2008
|
}),
|
|
1635
2009
|
check("package.json", () => {
|
|
1636
|
-
if (!
|
|
2010
|
+
if (!fs14.existsSync("package.json")) throw new Error("not found");
|
|
1637
2011
|
})
|
|
1638
2012
|
];
|
|
1639
2013
|
const failed = checks.filter((c) => !c.ok);
|
|
@@ -1653,11 +2027,7 @@ var init_preflight = __esm({
|
|
|
1653
2027
|
}
|
|
1654
2028
|
});
|
|
1655
2029
|
|
|
1656
|
-
// src/
|
|
1657
|
-
var entry_exports = {};
|
|
1658
|
-
import * as fs6 from "fs";
|
|
1659
|
-
import * as path5 from "path";
|
|
1660
|
-
import { execFileSync as execFileSync7 } from "child_process";
|
|
2030
|
+
// src/cli/args.ts
|
|
1661
2031
|
function getArg(args2, flag) {
|
|
1662
2032
|
const idx = args2.indexOf(flag);
|
|
1663
2033
|
if (idx !== -1 && args2[idx + 1] && !args2[idx + 1].startsWith("--")) {
|
|
@@ -1699,6 +2069,18 @@ function parseArgs() {
|
|
|
1699
2069
|
complexity: getArg(args2, "--complexity") ?? process.env.COMPLEXITY
|
|
1700
2070
|
};
|
|
1701
2071
|
}
|
|
2072
|
+
var isCI2;
|
|
2073
|
+
var init_args = __esm({
|
|
2074
|
+
"src/cli/args.ts"() {
|
|
2075
|
+
"use strict";
|
|
2076
|
+
isCI2 = !!process.env.GITHUB_ACTIONS;
|
|
2077
|
+
}
|
|
2078
|
+
});
|
|
2079
|
+
|
|
2080
|
+
// src/cli/litellm.ts
|
|
2081
|
+
import * as fs15 from "fs";
|
|
2082
|
+
import * as path14 from "path";
|
|
2083
|
+
import { execFileSync as execFileSync9 } from "child_process";
|
|
1702
2084
|
async function checkLitellmHealth(url) {
|
|
1703
2085
|
try {
|
|
1704
2086
|
const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
|
|
@@ -1708,18 +2090,18 @@ async function checkLitellmHealth(url) {
|
|
|
1708
2090
|
}
|
|
1709
2091
|
}
|
|
1710
2092
|
async function tryStartLitellm(url, projectDir) {
|
|
1711
|
-
const configPath =
|
|
1712
|
-
if (!
|
|
2093
|
+
const configPath = path14.join(projectDir, "litellm-config.yaml");
|
|
2094
|
+
if (!fs15.existsSync(configPath)) {
|
|
1713
2095
|
logger.warn("litellm-config.yaml not found \u2014 cannot start proxy");
|
|
1714
2096
|
return null;
|
|
1715
2097
|
}
|
|
1716
2098
|
const portMatch = url.match(/:(\d+)/);
|
|
1717
2099
|
const port = portMatch ? portMatch[1] : "4000";
|
|
1718
2100
|
try {
|
|
1719
|
-
|
|
2101
|
+
execFileSync9("litellm", ["--version"], { timeout: 5e3, stdio: "pipe" });
|
|
1720
2102
|
} catch {
|
|
1721
2103
|
try {
|
|
1722
|
-
|
|
2104
|
+
execFileSync9("python3", ["-m", "litellm", "--version"], { timeout: 5e3, stdio: "pipe" });
|
|
1723
2105
|
} catch {
|
|
1724
2106
|
logger.warn("litellm not installed (pip install 'litellm[proxy]')");
|
|
1725
2107
|
return null;
|
|
@@ -1729,7 +2111,7 @@ async function tryStartLitellm(url, projectDir) {
|
|
|
1729
2111
|
let cmd;
|
|
1730
2112
|
let args2;
|
|
1731
2113
|
try {
|
|
1732
|
-
|
|
2114
|
+
execFileSync9("litellm", ["--version"], { timeout: 5e3, stdio: "pipe" });
|
|
1733
2115
|
cmd = "litellm";
|
|
1734
2116
|
args2 = ["--config", configPath, "--port", port];
|
|
1735
2117
|
} catch {
|
|
@@ -1753,15 +2135,26 @@ async function tryStartLitellm(url, projectDir) {
|
|
|
1753
2135
|
child.kill();
|
|
1754
2136
|
return null;
|
|
1755
2137
|
}
|
|
2138
|
+
var init_litellm = __esm({
|
|
2139
|
+
"src/cli/litellm.ts"() {
|
|
2140
|
+
"use strict";
|
|
2141
|
+
init_logger();
|
|
2142
|
+
}
|
|
2143
|
+
});
|
|
2144
|
+
|
|
2145
|
+
// src/cli/task-resolution.ts
|
|
2146
|
+
import * as fs16 from "fs";
|
|
2147
|
+
import * as path15 from "path";
|
|
2148
|
+
import { execFileSync as execFileSync10 } from "child_process";
|
|
1756
2149
|
function findLatestTaskForIssue(issueNumber, projectDir) {
|
|
1757
|
-
const tasksDir =
|
|
1758
|
-
if (!
|
|
1759
|
-
const allDirs =
|
|
2150
|
+
const tasksDir = path15.join(projectDir, ".tasks");
|
|
2151
|
+
if (!fs16.existsSync(tasksDir)) return null;
|
|
2152
|
+
const allDirs = fs16.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
|
|
1760
2153
|
const prefix = `${issueNumber}-`;
|
|
1761
2154
|
const direct = allDirs.find((d) => d.startsWith(prefix));
|
|
1762
2155
|
if (direct) return direct;
|
|
1763
2156
|
try {
|
|
1764
|
-
const branch =
|
|
2157
|
+
const branch = execFileSync10("git", ["branch", "--show-current"], {
|
|
1765
2158
|
encoding: "utf-8",
|
|
1766
2159
|
cwd: projectDir,
|
|
1767
2160
|
timeout: 5e3,
|
|
@@ -1783,11 +2176,21 @@ function generateTaskId() {
|
|
|
1783
2176
|
const pad = (n) => String(n).padStart(2, "0");
|
|
1784
2177
|
return `${String(now.getFullYear()).slice(2)}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
1785
2178
|
}
|
|
2179
|
+
var init_task_resolution = __esm({
|
|
2180
|
+
"src/cli/task-resolution.ts"() {
|
|
2181
|
+
"use strict";
|
|
2182
|
+
}
|
|
2183
|
+
});
|
|
2184
|
+
|
|
2185
|
+
// src/entry.ts
|
|
2186
|
+
var entry_exports = {};
|
|
2187
|
+
import * as fs17 from "fs";
|
|
2188
|
+
import * as path16 from "path";
|
|
1786
2189
|
async function main() {
|
|
1787
2190
|
const input = parseArgs();
|
|
1788
|
-
const projectDir = input.cwd ?
|
|
2191
|
+
const projectDir = input.cwd ? path16.resolve(input.cwd) : process.cwd();
|
|
1789
2192
|
if (input.cwd) {
|
|
1790
|
-
if (!
|
|
2193
|
+
if (!fs17.existsSync(projectDir)) {
|
|
1791
2194
|
console.error(`--cwd path does not exist: ${projectDir}`);
|
|
1792
2195
|
process.exit(1);
|
|
1793
2196
|
}
|
|
@@ -1814,8 +2217,8 @@ async function main() {
|
|
|
1814
2217
|
process.exit(1);
|
|
1815
2218
|
}
|
|
1816
2219
|
}
|
|
1817
|
-
const taskDir =
|
|
1818
|
-
|
|
2220
|
+
const taskDir = path16.join(projectDir, ".tasks", taskId);
|
|
2221
|
+
fs17.mkdirSync(taskDir, { recursive: true });
|
|
1819
2222
|
if (input.command === "status") {
|
|
1820
2223
|
printStatus(taskId, taskDir);
|
|
1821
2224
|
return;
|
|
@@ -1823,22 +2226,22 @@ async function main() {
|
|
|
1823
2226
|
logger.info("Preflight checks:");
|
|
1824
2227
|
runPreflight();
|
|
1825
2228
|
if (input.task) {
|
|
1826
|
-
|
|
2229
|
+
fs17.writeFileSync(path16.join(taskDir, "task.md"), input.task);
|
|
1827
2230
|
}
|
|
1828
|
-
const taskMdPath =
|
|
1829
|
-
if (!
|
|
2231
|
+
const taskMdPath = path16.join(taskDir, "task.md");
|
|
2232
|
+
if (!fs17.existsSync(taskMdPath) && input.issueNumber) {
|
|
1830
2233
|
logger.info(`Fetching issue #${input.issueNumber} body as task...`);
|
|
1831
2234
|
const issue = getIssue(input.issueNumber);
|
|
1832
2235
|
if (issue) {
|
|
1833
2236
|
const taskContent = `# ${issue.title}
|
|
1834
2237
|
|
|
1835
2238
|
${issue.body ?? ""}`;
|
|
1836
|
-
|
|
2239
|
+
fs17.writeFileSync(taskMdPath, taskContent);
|
|
1837
2240
|
logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
|
|
1838
2241
|
}
|
|
1839
2242
|
}
|
|
1840
2243
|
if (input.command === "run") {
|
|
1841
|
-
if (!
|
|
2244
|
+
if (!fs17.existsSync(taskMdPath)) {
|
|
1842
2245
|
console.error("No task.md found. Provide --task, --issue-number, or ensure .tasks/<id>/task.md exists.");
|
|
1843
2246
|
process.exit(1);
|
|
1844
2247
|
}
|
|
@@ -1847,10 +2250,10 @@ ${issue.body ?? ""}`;
|
|
|
1847
2250
|
input.fromStage = "build";
|
|
1848
2251
|
}
|
|
1849
2252
|
if (input.command === "rerun" && !input.fromStage) {
|
|
1850
|
-
const statusPath =
|
|
1851
|
-
if (
|
|
2253
|
+
const statusPath = path16.join(taskDir, "status.json");
|
|
2254
|
+
if (fs17.existsSync(statusPath)) {
|
|
1852
2255
|
try {
|
|
1853
|
-
const status = JSON.parse(
|
|
2256
|
+
const status = JSON.parse(fs17.readFileSync(statusPath, "utf-8"));
|
|
1854
2257
|
const stageNames = ["taskify", "plan", "build", "verify", "review", "review-fix", "ship"];
|
|
1855
2258
|
let foundPaused = false;
|
|
1856
2259
|
for (const name of stageNames) {
|
|
@@ -1893,6 +2296,14 @@ ${issue.body ?? ""}`;
|
|
|
1893
2296
|
}
|
|
1894
2297
|
};
|
|
1895
2298
|
process.on("exit", cleanupLitellm);
|
|
2299
|
+
process.on("SIGINT", () => {
|
|
2300
|
+
cleanupLitellm();
|
|
2301
|
+
process.exit(130);
|
|
2302
|
+
});
|
|
2303
|
+
process.on("SIGTERM", () => {
|
|
2304
|
+
cleanupLitellm();
|
|
2305
|
+
process.exit(143);
|
|
2306
|
+
});
|
|
1896
2307
|
if (config.agent.litellmUrl) {
|
|
1897
2308
|
const proxyRunning = await checkLitellmHealth(config.agent.litellmUrl);
|
|
1898
2309
|
if (!proxyRunning) {
|
|
@@ -1949,7 +2360,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
|
|
|
1949
2360
|
}
|
|
1950
2361
|
}
|
|
1951
2362
|
const state = await runPipeline(ctx);
|
|
1952
|
-
const files =
|
|
2363
|
+
const files = fs17.readdirSync(taskDir);
|
|
1953
2364
|
console.log(`
|
|
1954
2365
|
Artifacts in ${taskDir}:`);
|
|
1955
2366
|
for (const f of files) {
|
|
@@ -1957,7 +2368,7 @@ Artifacts in ${taskDir}:`);
|
|
|
1957
2368
|
}
|
|
1958
2369
|
if (state.state === "failed") {
|
|
1959
2370
|
const isPaused = Object.values(state.stages).some(
|
|
1960
|
-
(s) =>
|
|
2371
|
+
(s) => s.error?.includes("paused") ?? false
|
|
1961
2372
|
);
|
|
1962
2373
|
if (isPaused) {
|
|
1963
2374
|
process.exit(0);
|
|
@@ -1979,17 +2390,18 @@ Artifacts in ${taskDir}:`);
|
|
|
1979
2390
|
process.exit(1);
|
|
1980
2391
|
}
|
|
1981
2392
|
}
|
|
1982
|
-
var isCI2;
|
|
1983
2393
|
var init_entry = __esm({
|
|
1984
2394
|
"src/entry.ts"() {
|
|
1985
2395
|
"use strict";
|
|
1986
2396
|
init_agent_runner();
|
|
1987
|
-
|
|
2397
|
+
init_pipeline();
|
|
1988
2398
|
init_preflight();
|
|
1989
2399
|
init_config();
|
|
1990
2400
|
init_github_api();
|
|
1991
2401
|
init_logger();
|
|
1992
|
-
|
|
2402
|
+
init_args();
|
|
2403
|
+
init_litellm();
|
|
2404
|
+
init_task_resolution();
|
|
1993
2405
|
main().catch(async (err) => {
|
|
1994
2406
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1995
2407
|
console.error(msg);
|
|
@@ -2007,20 +2419,20 @@ var init_entry = __esm({
|
|
|
2007
2419
|
});
|
|
2008
2420
|
|
|
2009
2421
|
// src/bin/cli.ts
|
|
2010
|
-
import * as
|
|
2011
|
-
import * as
|
|
2012
|
-
import { execFileSync as
|
|
2422
|
+
import * as fs18 from "fs";
|
|
2423
|
+
import * as path17 from "path";
|
|
2424
|
+
import { execFileSync as execFileSync11 } from "child_process";
|
|
2013
2425
|
import { fileURLToPath } from "url";
|
|
2014
|
-
var __dirname =
|
|
2015
|
-
var PKG_ROOT =
|
|
2426
|
+
var __dirname = path17.dirname(fileURLToPath(import.meta.url));
|
|
2427
|
+
var PKG_ROOT = path17.resolve(__dirname, "..", "..");
|
|
2016
2428
|
function getVersion() {
|
|
2017
|
-
const pkgPath =
|
|
2018
|
-
const pkg = JSON.parse(
|
|
2429
|
+
const pkgPath = path17.join(PKG_ROOT, "package.json");
|
|
2430
|
+
const pkg = JSON.parse(fs18.readFileSync(pkgPath, "utf-8"));
|
|
2019
2431
|
return pkg.version;
|
|
2020
2432
|
}
|
|
2021
2433
|
function checkCommand2(name, args2, fix) {
|
|
2022
2434
|
try {
|
|
2023
|
-
const output =
|
|
2435
|
+
const output = execFileSync11(name, args2, {
|
|
2024
2436
|
encoding: "utf-8",
|
|
2025
2437
|
timeout: 1e4,
|
|
2026
2438
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -2031,14 +2443,14 @@ function checkCommand2(name, args2, fix) {
|
|
|
2031
2443
|
}
|
|
2032
2444
|
}
|
|
2033
2445
|
function checkFile(filePath, description, fix) {
|
|
2034
|
-
if (
|
|
2446
|
+
if (fs18.existsSync(filePath)) {
|
|
2035
2447
|
return { name: description, ok: true, detail: filePath };
|
|
2036
2448
|
}
|
|
2037
2449
|
return { name: description, ok: false, fix };
|
|
2038
2450
|
}
|
|
2039
2451
|
function checkGhAuth(cwd) {
|
|
2040
2452
|
try {
|
|
2041
|
-
const output =
|
|
2453
|
+
const output = execFileSync11("gh", ["auth", "status"], {
|
|
2042
2454
|
encoding: "utf-8",
|
|
2043
2455
|
timeout: 1e4,
|
|
2044
2456
|
cwd,
|
|
@@ -2047,7 +2459,7 @@ function checkGhAuth(cwd) {
|
|
|
2047
2459
|
const account = output.match(/Logged in to .* account (\S+)/)?.[1];
|
|
2048
2460
|
return { name: "gh auth", ok: true, detail: account ?? "authenticated" };
|
|
2049
2461
|
} catch (err) {
|
|
2050
|
-
const stderr = err.stderr ?? "";
|
|
2462
|
+
const stderr = err instanceof Error && "stderr" in err ? String(err.stderr ?? "") : "";
|
|
2051
2463
|
if (stderr.includes("not logged")) {
|
|
2052
2464
|
return { name: "gh auth", ok: false, fix: "Run: gh auth login" };
|
|
2053
2465
|
}
|
|
@@ -2056,7 +2468,7 @@ function checkGhAuth(cwd) {
|
|
|
2056
2468
|
}
|
|
2057
2469
|
function checkGhRepoAccess(cwd) {
|
|
2058
2470
|
try {
|
|
2059
|
-
const remote =
|
|
2471
|
+
const remote = execFileSync11("git", ["remote", "get-url", "origin"], {
|
|
2060
2472
|
encoding: "utf-8",
|
|
2061
2473
|
timeout: 5e3,
|
|
2062
2474
|
cwd,
|
|
@@ -2067,7 +2479,7 @@ function checkGhRepoAccess(cwd) {
|
|
|
2067
2479
|
return { name: "GitHub repo", ok: false, fix: "Set git remote origin to a GitHub URL" };
|
|
2068
2480
|
}
|
|
2069
2481
|
const repoSlug = `${match[1]}/${match[2]}`;
|
|
2070
|
-
|
|
2482
|
+
execFileSync11("gh", ["repo", "view", repoSlug, "--json", "name"], {
|
|
2071
2483
|
encoding: "utf-8",
|
|
2072
2484
|
timeout: 1e4,
|
|
2073
2485
|
cwd,
|
|
@@ -2080,7 +2492,7 @@ function checkGhRepoAccess(cwd) {
|
|
|
2080
2492
|
}
|
|
2081
2493
|
function checkGhSecret(repoSlug, secretName) {
|
|
2082
2494
|
try {
|
|
2083
|
-
const output =
|
|
2495
|
+
const output = execFileSync11("gh", ["secret", "list", "--repo", repoSlug], {
|
|
2084
2496
|
encoding: "utf-8",
|
|
2085
2497
|
timeout: 1e4,
|
|
2086
2498
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -2103,10 +2515,10 @@ function checkGhSecret(repoSlug, secretName) {
|
|
|
2103
2515
|
}
|
|
2104
2516
|
function detectArchitecture(cwd) {
|
|
2105
2517
|
const detected = [];
|
|
2106
|
-
const pkgPath =
|
|
2107
|
-
if (
|
|
2518
|
+
const pkgPath = path17.join(cwd, "package.json");
|
|
2519
|
+
if (fs18.existsSync(pkgPath)) {
|
|
2108
2520
|
try {
|
|
2109
|
-
const pkg = JSON.parse(
|
|
2521
|
+
const pkg = JSON.parse(fs18.readFileSync(pkgPath, "utf-8"));
|
|
2110
2522
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2111
2523
|
if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
|
|
2112
2524
|
else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
|
|
@@ -2129,44 +2541,44 @@ function detectArchitecture(cwd) {
|
|
|
2129
2541
|
if (allDeps.tailwindcss) detected.push(`- CSS: Tailwind CSS ${allDeps.tailwindcss}`);
|
|
2130
2542
|
if (pkg.type === "module") detected.push("- Module system: ESM");
|
|
2131
2543
|
else detected.push("- Module system: CommonJS");
|
|
2132
|
-
if (
|
|
2133
|
-
else if (
|
|
2134
|
-
else if (
|
|
2135
|
-
else if (
|
|
2544
|
+
if (fs18.existsSync(path17.join(cwd, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
|
|
2545
|
+
else if (fs18.existsSync(path17.join(cwd, "yarn.lock"))) detected.push("- Package manager: yarn");
|
|
2546
|
+
else if (fs18.existsSync(path17.join(cwd, "bun.lockb"))) detected.push("- Package manager: bun");
|
|
2547
|
+
else if (fs18.existsSync(path17.join(cwd, "package-lock.json"))) detected.push("- Package manager: npm");
|
|
2136
2548
|
} catch {
|
|
2137
2549
|
}
|
|
2138
2550
|
}
|
|
2139
2551
|
try {
|
|
2140
|
-
const entries =
|
|
2552
|
+
const entries = fs18.readdirSync(cwd, { withFileTypes: true });
|
|
2141
2553
|
const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
|
|
2142
2554
|
if (dirs.length > 0) detected.push(`- Top-level directories: ${dirs.join(", ")}`);
|
|
2143
2555
|
} catch {
|
|
2144
2556
|
}
|
|
2145
|
-
const srcDir =
|
|
2146
|
-
if (
|
|
2557
|
+
const srcDir = path17.join(cwd, "src");
|
|
2558
|
+
if (fs18.existsSync(srcDir)) {
|
|
2147
2559
|
try {
|
|
2148
|
-
const srcEntries =
|
|
2560
|
+
const srcEntries = fs18.readdirSync(srcDir, { withFileTypes: true });
|
|
2149
2561
|
const srcDirs = srcEntries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
2150
2562
|
if (srcDirs.length > 0) detected.push(`- src/ structure: ${srcDirs.join(", ")}`);
|
|
2151
2563
|
} catch {
|
|
2152
2564
|
}
|
|
2153
2565
|
}
|
|
2154
2566
|
const configs = [];
|
|
2155
|
-
if (
|
|
2156
|
-
if (
|
|
2157
|
-
if (
|
|
2158
|
-
if (
|
|
2567
|
+
if (fs18.existsSync(path17.join(cwd, "tsconfig.json"))) configs.push("tsconfig.json");
|
|
2568
|
+
if (fs18.existsSync(path17.join(cwd, "docker-compose.yml")) || fs18.existsSync(path17.join(cwd, "docker-compose.yaml"))) configs.push("docker-compose");
|
|
2569
|
+
if (fs18.existsSync(path17.join(cwd, "Dockerfile"))) configs.push("Dockerfile");
|
|
2570
|
+
if (fs18.existsSync(path17.join(cwd, ".env")) || fs18.existsSync(path17.join(cwd, ".env.local"))) configs.push(".env");
|
|
2159
2571
|
if (configs.length > 0) detected.push(`- Config files: ${configs.join(", ")}`);
|
|
2160
2572
|
return detected;
|
|
2161
2573
|
}
|
|
2162
2574
|
function detectBasicConfig(cwd) {
|
|
2163
2575
|
let pm = "pnpm";
|
|
2164
|
-
if (
|
|
2165
|
-
else if (
|
|
2166
|
-
else if (!
|
|
2576
|
+
if (fs18.existsSync(path17.join(cwd, "yarn.lock"))) pm = "yarn";
|
|
2577
|
+
else if (fs18.existsSync(path17.join(cwd, "bun.lockb"))) pm = "bun";
|
|
2578
|
+
else if (!fs18.existsSync(path17.join(cwd, "pnpm-lock.yaml")) && fs18.existsSync(path17.join(cwd, "package-lock.json"))) pm = "npm";
|
|
2167
2579
|
let defaultBranch = "main";
|
|
2168
2580
|
try {
|
|
2169
|
-
const ref =
|
|
2581
|
+
const ref = execFileSync11("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
|
|
2170
2582
|
encoding: "utf-8",
|
|
2171
2583
|
timeout: 5e3,
|
|
2172
2584
|
cwd,
|
|
@@ -2175,7 +2587,7 @@ function detectBasicConfig(cwd) {
|
|
|
2175
2587
|
defaultBranch = ref.replace("refs/remotes/origin/", "");
|
|
2176
2588
|
} catch {
|
|
2177
2589
|
try {
|
|
2178
|
-
|
|
2590
|
+
execFileSync11("git", ["rev-parse", "--verify", "origin/dev"], {
|
|
2179
2591
|
encoding: "utf-8",
|
|
2180
2592
|
timeout: 5e3,
|
|
2181
2593
|
cwd,
|
|
@@ -2188,7 +2600,7 @@ function detectBasicConfig(cwd) {
|
|
|
2188
2600
|
let owner = "";
|
|
2189
2601
|
let repo = "";
|
|
2190
2602
|
try {
|
|
2191
|
-
const remote =
|
|
2603
|
+
const remote = execFileSync11("git", ["remote", "get-url", "origin"], {
|
|
2192
2604
|
encoding: "utf-8",
|
|
2193
2605
|
timeout: 5e3,
|
|
2194
2606
|
cwd,
|
|
@@ -2207,9 +2619,9 @@ function smartInit(cwd) {
|
|
|
2207
2619
|
const basic = detectBasicConfig(cwd);
|
|
2208
2620
|
let context = "";
|
|
2209
2621
|
const readIfExists = (rel, maxChars = 3e3) => {
|
|
2210
|
-
const p =
|
|
2211
|
-
if (
|
|
2212
|
-
const content =
|
|
2622
|
+
const p = path17.join(cwd, rel);
|
|
2623
|
+
if (fs18.existsSync(p)) {
|
|
2624
|
+
const content = fs18.readFileSync(p, "utf-8");
|
|
2213
2625
|
return content.slice(0, maxChars);
|
|
2214
2626
|
}
|
|
2215
2627
|
return null;
|
|
@@ -2235,14 +2647,14 @@ ${claudeMd}
|
|
|
2235
2647
|
|
|
2236
2648
|
`;
|
|
2237
2649
|
try {
|
|
2238
|
-
const topDirs =
|
|
2650
|
+
const topDirs = fs18.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
|
|
2239
2651
|
context += `## Top-level directories
|
|
2240
2652
|
${topDirs.join(", ")}
|
|
2241
2653
|
|
|
2242
2654
|
`;
|
|
2243
|
-
const srcDir =
|
|
2244
|
-
if (
|
|
2245
|
-
const srcDirs =
|
|
2655
|
+
const srcDir = path17.join(cwd, "src");
|
|
2656
|
+
if (fs18.existsSync(srcDir)) {
|
|
2657
|
+
const srcDirs = fs18.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
2246
2658
|
context += `## src/ subdirectories
|
|
2247
2659
|
${srcDirs.join(", ")}
|
|
2248
2660
|
|
|
@@ -2252,7 +2664,7 @@ ${srcDirs.join(", ")}
|
|
|
2252
2664
|
}
|
|
2253
2665
|
const existingFiles = [];
|
|
2254
2666
|
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"]) {
|
|
2255
|
-
if (
|
|
2667
|
+
if (fs18.existsSync(path17.join(cwd, f))) existingFiles.push(f);
|
|
2256
2668
|
}
|
|
2257
2669
|
if (existingFiles.length) context += `## Config files present
|
|
2258
2670
|
${existingFiles.join(", ")}
|
|
@@ -2311,7 +2723,7 @@ Output ONLY valid JSON. No markdown fences. No explanation before or after.
|
|
|
2311
2723
|
${context}`;
|
|
2312
2724
|
console.log(" \u23F3 Analyzing project with Claude Code...");
|
|
2313
2725
|
try {
|
|
2314
|
-
const output =
|
|
2726
|
+
const output = execFileSync11("claude", [
|
|
2315
2727
|
"--print",
|
|
2316
2728
|
"--model",
|
|
2317
2729
|
"haiku",
|
|
@@ -2358,7 +2770,7 @@ ${context}`;
|
|
|
2358
2770
|
function validateQualityCommands(cwd, config, pm) {
|
|
2359
2771
|
let scripts = {};
|
|
2360
2772
|
try {
|
|
2361
|
-
const pkg = JSON.parse(
|
|
2773
|
+
const pkg = JSON.parse(fs18.readFileSync(path17.join(cwd, "package.json"), "utf-8"));
|
|
2362
2774
|
scripts = pkg.scripts ?? {};
|
|
2363
2775
|
} catch {
|
|
2364
2776
|
return;
|
|
@@ -2392,7 +2804,7 @@ function validateQualityCommands(cwd, config, pm) {
|
|
|
2392
2804
|
function buildFallbackConfig(cwd, basic) {
|
|
2393
2805
|
const pkg = (() => {
|
|
2394
2806
|
try {
|
|
2395
|
-
return JSON.parse(
|
|
2807
|
+
return JSON.parse(fs18.readFileSync(path17.join(cwd, "package.json"), "utf-8"));
|
|
2396
2808
|
} catch {
|
|
2397
2809
|
return {};
|
|
2398
2810
|
}
|
|
@@ -2432,34 +2844,34 @@ function initCommand(opts) {
|
|
|
2432
2844
|
console.log(`Project: ${cwd}
|
|
2433
2845
|
`);
|
|
2434
2846
|
console.log("\u2500\u2500 Files \u2500\u2500");
|
|
2435
|
-
const templatesDir =
|
|
2436
|
-
const workflowSrc =
|
|
2437
|
-
const workflowDest =
|
|
2438
|
-
if (!
|
|
2847
|
+
const templatesDir = path17.join(PKG_ROOT, "templates");
|
|
2848
|
+
const workflowSrc = path17.join(templatesDir, "kody.yml");
|
|
2849
|
+
const workflowDest = path17.join(cwd, ".github", "workflows", "kody.yml");
|
|
2850
|
+
if (!fs18.existsSync(workflowSrc)) {
|
|
2439
2851
|
console.error(" \u2717 Template kody.yml not found in package");
|
|
2440
2852
|
process.exit(1);
|
|
2441
2853
|
}
|
|
2442
|
-
if (
|
|
2854
|
+
if (fs18.existsSync(workflowDest) && !opts.force) {
|
|
2443
2855
|
console.log(" \u25CB .github/workflows/kody.yml (exists, use --force to overwrite)");
|
|
2444
2856
|
} else {
|
|
2445
|
-
|
|
2446
|
-
|
|
2857
|
+
fs18.mkdirSync(path17.dirname(workflowDest), { recursive: true });
|
|
2858
|
+
fs18.copyFileSync(workflowSrc, workflowDest);
|
|
2447
2859
|
console.log(" \u2713 .github/workflows/kody.yml");
|
|
2448
2860
|
}
|
|
2449
|
-
const configDest =
|
|
2861
|
+
const configDest = path17.join(cwd, "kody.config.json");
|
|
2450
2862
|
let smartResult = null;
|
|
2451
|
-
if (!
|
|
2863
|
+
if (!fs18.existsSync(configDest) || opts.force) {
|
|
2452
2864
|
smartResult = smartInit(cwd);
|
|
2453
|
-
|
|
2865
|
+
fs18.writeFileSync(configDest, JSON.stringify(smartResult.config, null, 2) + "\n");
|
|
2454
2866
|
console.log(" \u2713 kody.config.json (auto-configured)");
|
|
2455
2867
|
} else {
|
|
2456
2868
|
console.log(" \u25CB kody.config.json (exists)");
|
|
2457
2869
|
}
|
|
2458
|
-
const gitignorePath =
|
|
2459
|
-
if (
|
|
2460
|
-
const content =
|
|
2870
|
+
const gitignorePath = path17.join(cwd, ".gitignore");
|
|
2871
|
+
if (fs18.existsSync(gitignorePath)) {
|
|
2872
|
+
const content = fs18.readFileSync(gitignorePath, "utf-8");
|
|
2461
2873
|
if (!content.includes(".tasks/")) {
|
|
2462
|
-
|
|
2874
|
+
fs18.appendFileSync(gitignorePath, "\n.tasks/\n");
|
|
2463
2875
|
console.log(" \u2713 .gitignore (added .tasks/)");
|
|
2464
2876
|
} else {
|
|
2465
2877
|
console.log(" \u25CB .gitignore (.tasks/ already present)");
|
|
@@ -2472,7 +2884,7 @@ function initCommand(opts) {
|
|
|
2472
2884
|
checkCommand2("git", ["--version"], "Install git"),
|
|
2473
2885
|
checkCommand2("node", ["--version"], "Install Node.js >= 22"),
|
|
2474
2886
|
checkCommand2("pnpm", ["--version"], "Install: npm i -g pnpm"),
|
|
2475
|
-
checkFile(
|
|
2887
|
+
checkFile(path17.join(cwd, "package.json"), "package.json", "Run: pnpm init")
|
|
2476
2888
|
];
|
|
2477
2889
|
for (const c of checks) {
|
|
2478
2890
|
if (c.ok) {
|
|
@@ -2517,7 +2929,7 @@ function initCommand(opts) {
|
|
|
2517
2929
|
console.log("\n\u2500\u2500 Labels \u2500\u2500");
|
|
2518
2930
|
for (const label of labels) {
|
|
2519
2931
|
try {
|
|
2520
|
-
|
|
2932
|
+
execFileSync11("gh", [
|
|
2521
2933
|
"label",
|
|
2522
2934
|
"create",
|
|
2523
2935
|
label.name,
|
|
@@ -2536,7 +2948,7 @@ function initCommand(opts) {
|
|
|
2536
2948
|
console.log(` \u2713 ${label.name}`);
|
|
2537
2949
|
} catch {
|
|
2538
2950
|
try {
|
|
2539
|
-
|
|
2951
|
+
execFileSync11("gh", ["label", "list", "--repo", repoSlug, "--search", label.name], {
|
|
2540
2952
|
encoding: "utf-8",
|
|
2541
2953
|
timeout: 1e4,
|
|
2542
2954
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -2549,9 +2961,9 @@ function initCommand(opts) {
|
|
|
2549
2961
|
}
|
|
2550
2962
|
}
|
|
2551
2963
|
console.log("\n\u2500\u2500 Config \u2500\u2500");
|
|
2552
|
-
if (
|
|
2964
|
+
if (fs18.existsSync(configDest)) {
|
|
2553
2965
|
try {
|
|
2554
|
-
const config = JSON.parse(
|
|
2966
|
+
const config = JSON.parse(fs18.readFileSync(configDest, "utf-8"));
|
|
2555
2967
|
const configChecks = [];
|
|
2556
2968
|
if (config.github?.owner && config.github?.repo) {
|
|
2557
2969
|
configChecks.push({ name: "github.owner/repo", ok: true, detail: `${config.github.owner}/${config.github.repo}` });
|
|
@@ -2578,21 +2990,21 @@ function initCommand(opts) {
|
|
|
2578
2990
|
}
|
|
2579
2991
|
}
|
|
2580
2992
|
console.log("\n\u2500\u2500 Project Memory \u2500\u2500");
|
|
2581
|
-
const memoryDir =
|
|
2582
|
-
|
|
2583
|
-
const archPath =
|
|
2584
|
-
const conventionsPath =
|
|
2585
|
-
if (
|
|
2993
|
+
const memoryDir = path17.join(cwd, ".kody", "memory");
|
|
2994
|
+
fs18.mkdirSync(memoryDir, { recursive: true });
|
|
2995
|
+
const archPath = path17.join(memoryDir, "architecture.md");
|
|
2996
|
+
const conventionsPath = path17.join(memoryDir, "conventions.md");
|
|
2997
|
+
if (fs18.existsSync(archPath) && !opts.force) {
|
|
2586
2998
|
console.log(" \u25CB .kody/memory/architecture.md (exists, use --force to regenerate)");
|
|
2587
2999
|
} else if (smartResult?.architecture) {
|
|
2588
|
-
|
|
3000
|
+
fs18.writeFileSync(archPath, smartResult.architecture);
|
|
2589
3001
|
const lineCount = smartResult.architecture.split("\n").length;
|
|
2590
3002
|
console.log(` \u2713 .kody/memory/architecture.md (${lineCount} lines, LLM-generated)`);
|
|
2591
3003
|
} else {
|
|
2592
3004
|
const archItems = detectArchitecture(cwd);
|
|
2593
3005
|
if (archItems.length > 0) {
|
|
2594
3006
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2595
|
-
|
|
3007
|
+
fs18.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
|
|
2596
3008
|
|
|
2597
3009
|
## Overview
|
|
2598
3010
|
${archItems.join("\n")}
|
|
@@ -2602,14 +3014,14 @@ ${archItems.join("\n")}
|
|
|
2602
3014
|
console.log(" \u25CB No architecture detected");
|
|
2603
3015
|
}
|
|
2604
3016
|
}
|
|
2605
|
-
if (
|
|
3017
|
+
if (fs18.existsSync(conventionsPath) && !opts.force) {
|
|
2606
3018
|
console.log(" \u25CB .kody/memory/conventions.md (exists, use --force to regenerate)");
|
|
2607
3019
|
} else if (smartResult?.conventions) {
|
|
2608
|
-
|
|
3020
|
+
fs18.writeFileSync(conventionsPath, smartResult.conventions);
|
|
2609
3021
|
const lineCount = smartResult.conventions.split("\n").length;
|
|
2610
3022
|
console.log(` \u2713 .kody/memory/conventions.md (${lineCount} lines, LLM-generated)`);
|
|
2611
3023
|
} else {
|
|
2612
|
-
|
|
3024
|
+
fs18.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
|
|
2613
3025
|
console.log(" \u2713 .kody/memory/conventions.md (seed)");
|
|
2614
3026
|
}
|
|
2615
3027
|
const allChecks = [...checks, ghAuth, ghRepo];
|