@kody-ade/kody-engine-lite 0.1.21 → 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/cli.js +1386 -996
- package/package.json +1 -1
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,38 +481,475 @@ 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 };
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
function parseErrors(output) {
|
|
748
|
-
const errors = [];
|
|
749
|
-
for (const line of output.split("\n")) {
|
|
750
|
-
if (/error|Error|ERROR|failed|Failed|FAIL|warning:|Warning:|WARN/i.test(line)) {
|
|
751
|
-
errors.push(line.slice(0, 500));
|
|
752
|
-
}
|
|
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;
|
|
753
496
|
}
|
|
754
|
-
return errors;
|
|
755
497
|
}
|
|
756
|
-
function
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
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 };
|
|
513
|
+
}
|
|
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 validateTaskJson(content) {
|
|
747
|
+
try {
|
|
748
|
+
const parsed = JSON.parse(content);
|
|
749
|
+
for (const field of REQUIRED_TASK_FIELDS) {
|
|
750
|
+
if (!(field in parsed)) {
|
|
751
|
+
return { valid: false, error: `Missing field: ${field}` };
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return { valid: true };
|
|
755
|
+
} catch (err) {
|
|
756
|
+
return {
|
|
757
|
+
valid: false,
|
|
758
|
+
error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
function validatePlanMd(content) {
|
|
763
|
+
if (content.length < 10) {
|
|
764
|
+
return { valid: false, error: "Plan is too short (< 10 chars)" };
|
|
765
|
+
}
|
|
766
|
+
if (!/^##\s+\w+/m.test(content)) {
|
|
767
|
+
return { valid: false, error: "Plan has no markdown h2 sections" };
|
|
768
|
+
}
|
|
769
|
+
return { valid: true };
|
|
770
|
+
}
|
|
771
|
+
function validateReviewMd(content) {
|
|
772
|
+
if (/pass/i.test(content) || /fail/i.test(content)) {
|
|
773
|
+
return { valid: true };
|
|
774
|
+
}
|
|
775
|
+
return { valid: false, error: "Review must contain 'pass' or 'fail'" };
|
|
776
|
+
}
|
|
777
|
+
var REQUIRED_TASK_FIELDS;
|
|
778
|
+
var init_validators = __esm({
|
|
779
|
+
"src/validators.ts"() {
|
|
780
|
+
"use strict";
|
|
781
|
+
REQUIRED_TASK_FIELDS = [
|
|
782
|
+
"task_type",
|
|
783
|
+
"title",
|
|
784
|
+
"description",
|
|
785
|
+
"scope",
|
|
786
|
+
"risk_level"
|
|
787
|
+
];
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
// src/pipeline/runner-selection.ts
|
|
792
|
+
function getRunnerForStage(ctx, stageName) {
|
|
793
|
+
const config = getProjectConfig();
|
|
794
|
+
const runnerName = config.agent.stageRunners?.[stageName] ?? config.agent.defaultRunner ?? Object.keys(ctx.runners)[0] ?? "claude";
|
|
795
|
+
const runner = ctx.runners[runnerName];
|
|
796
|
+
if (!runner) {
|
|
797
|
+
throw new Error(
|
|
798
|
+
`Runner "${runnerName}" not found for stage ${stageName}. Available: ${Object.keys(ctx.runners).join(", ")}`
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
return runner;
|
|
802
|
+
}
|
|
803
|
+
var init_runner_selection = __esm({
|
|
804
|
+
"src/pipeline/runner-selection.ts"() {
|
|
805
|
+
"use strict";
|
|
806
|
+
init_config();
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
// src/stages/agent.ts
|
|
811
|
+
import * as fs5 from "fs";
|
|
812
|
+
import * as path5 from "path";
|
|
813
|
+
function validateStageOutput(stageName, content) {
|
|
814
|
+
switch (stageName) {
|
|
815
|
+
case "taskify":
|
|
816
|
+
return validateTaskJson(content);
|
|
817
|
+
case "plan":
|
|
818
|
+
return validatePlanMd(content);
|
|
819
|
+
case "review":
|
|
820
|
+
return validateReviewMd(content);
|
|
821
|
+
default:
|
|
822
|
+
return { valid: true };
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
async function executeAgentStage(ctx, def) {
|
|
826
|
+
if (ctx.input.dryRun) {
|
|
827
|
+
logger.info(` [dry-run] skipping ${def.name}`);
|
|
828
|
+
return { outcome: "completed", retries: 0 };
|
|
829
|
+
}
|
|
830
|
+
const prompt = buildFullPrompt(def.name, ctx.taskId, ctx.taskDir, ctx.projectDir, ctx.input.feedback);
|
|
831
|
+
const model = resolveModel(def.modelTier, def.name);
|
|
832
|
+
const config = getProjectConfig();
|
|
833
|
+
const runnerName = config.agent.stageRunners?.[def.name] ?? config.agent.defaultRunner ?? Object.keys(ctx.runners)[0] ?? "claude";
|
|
834
|
+
logger.info(` runner=${runnerName} model=${model} timeout=${def.timeout / 1e3}s`);
|
|
835
|
+
const extraEnv = {};
|
|
836
|
+
if (config.agent.litellmUrl) {
|
|
837
|
+
extraEnv.ANTHROPIC_BASE_URL = config.agent.litellmUrl;
|
|
838
|
+
}
|
|
839
|
+
const runner = getRunnerForStage(ctx, def.name);
|
|
840
|
+
const result = await runner.run(def.name, prompt, model, def.timeout, ctx.taskDir, {
|
|
841
|
+
cwd: ctx.projectDir,
|
|
842
|
+
env: extraEnv
|
|
843
|
+
});
|
|
844
|
+
if (result.outcome !== "completed") {
|
|
845
|
+
return { outcome: result.outcome, error: result.error, retries: 0 };
|
|
846
|
+
}
|
|
847
|
+
if (def.outputFile && result.output) {
|
|
848
|
+
fs5.writeFileSync(path5.join(ctx.taskDir, def.outputFile), result.output);
|
|
849
|
+
}
|
|
850
|
+
if (def.outputFile) {
|
|
851
|
+
const outputPath = path5.join(ctx.taskDir, def.outputFile);
|
|
852
|
+
if (!fs5.existsSync(outputPath)) {
|
|
853
|
+
const ext = path5.extname(def.outputFile);
|
|
854
|
+
const base = path5.basename(def.outputFile, ext);
|
|
855
|
+
const files = fs5.readdirSync(ctx.taskDir);
|
|
856
|
+
const variant = files.find(
|
|
857
|
+
(f) => f.startsWith(base + "-") && f.endsWith(ext)
|
|
858
|
+
);
|
|
859
|
+
if (variant) {
|
|
860
|
+
fs5.renameSync(path5.join(ctx.taskDir, variant), outputPath);
|
|
861
|
+
logger.info(` Renamed variant ${variant} \u2192 ${def.outputFile}`);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (def.outputFile) {
|
|
866
|
+
const outputPath = path5.join(ctx.taskDir, def.outputFile);
|
|
867
|
+
if (fs5.existsSync(outputPath)) {
|
|
868
|
+
const content = fs5.readFileSync(outputPath, "utf-8");
|
|
869
|
+
const validation = validateStageOutput(def.name, content);
|
|
870
|
+
if (!validation.valid) {
|
|
871
|
+
logger.warn(` validation warning: ${validation.error}`);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return { outcome: "completed", outputFile: def.outputFile, retries: 0 };
|
|
876
|
+
}
|
|
877
|
+
var init_agent = __esm({
|
|
878
|
+
"src/stages/agent.ts"() {
|
|
879
|
+
"use strict";
|
|
880
|
+
init_context();
|
|
881
|
+
init_validators();
|
|
882
|
+
init_config();
|
|
883
|
+
init_runner_selection();
|
|
884
|
+
init_logger();
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
// src/verify-runner.ts
|
|
889
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
890
|
+
function isExecError(err) {
|
|
891
|
+
return typeof err === "object" && err !== null;
|
|
892
|
+
}
|
|
893
|
+
function parseCommand(cmd) {
|
|
894
|
+
const parts = [];
|
|
895
|
+
let current = "";
|
|
896
|
+
let inQuote = null;
|
|
897
|
+
for (const ch of cmd) {
|
|
898
|
+
if (inQuote) {
|
|
899
|
+
if (ch === inQuote) {
|
|
900
|
+
inQuote = null;
|
|
901
|
+
} else {
|
|
902
|
+
current += ch;
|
|
903
|
+
}
|
|
904
|
+
} else if (ch === '"' || ch === "'") {
|
|
905
|
+
inQuote = ch;
|
|
906
|
+
} else if (/\s/.test(ch)) {
|
|
907
|
+
if (current) {
|
|
908
|
+
parts.push(current);
|
|
909
|
+
current = "";
|
|
910
|
+
}
|
|
911
|
+
} else {
|
|
912
|
+
current += ch;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
if (current) parts.push(current);
|
|
916
|
+
if (inQuote) logger.warn(`Unclosed quote in command: ${cmd}`);
|
|
917
|
+
return parts;
|
|
918
|
+
}
|
|
919
|
+
function runCommand(cmd, cwd, timeout) {
|
|
920
|
+
const parts = parseCommand(cmd);
|
|
921
|
+
if (parts.length === 0) {
|
|
922
|
+
return { success: true, output: "", timedOut: false };
|
|
923
|
+
}
|
|
924
|
+
try {
|
|
925
|
+
const output = execFileSync4(parts[0], parts.slice(1), {
|
|
926
|
+
cwd,
|
|
927
|
+
timeout,
|
|
928
|
+
encoding: "utf-8",
|
|
929
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
930
|
+
env: { ...process.env, FORCE_COLOR: "0" }
|
|
931
|
+
});
|
|
932
|
+
return { success: true, output: output ?? "", timedOut: false };
|
|
933
|
+
} catch (err) {
|
|
934
|
+
const stdout = isExecError(err) ? err.stdout ?? "" : "";
|
|
935
|
+
const stderr = isExecError(err) ? err.stderr ?? "" : "";
|
|
936
|
+
const killed = isExecError(err) ? !!err.killed : false;
|
|
937
|
+
return { success: false, output: `${stdout}${stderr}`, timedOut: killed };
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
function parseErrors(output) {
|
|
941
|
+
const errors = [];
|
|
942
|
+
for (const line of output.split("\n")) {
|
|
943
|
+
if (/error|Error|ERROR|failed|Failed|FAIL|warning:|Warning:|WARN/i.test(line)) {
|
|
944
|
+
errors.push(line.slice(0, 500));
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return errors;
|
|
948
|
+
}
|
|
949
|
+
function extractSummary(output, cmdName) {
|
|
950
|
+
const summaryPatterns = /Test Suites|Tests|Coverage|ERRORS|FAILURES|success|completed/i;
|
|
951
|
+
const lines = output.split("\n").filter((l) => summaryPatterns.test(l));
|
|
952
|
+
return lines.slice(-3).map((l) => `[${cmdName}] ${l.trim()}`);
|
|
760
953
|
}
|
|
761
954
|
function runQualityGates(taskDir, projectRoot) {
|
|
762
955
|
const config = getProjectConfig();
|
|
@@ -787,431 +980,811 @@ function runQualityGates(taskDir, projectRoot) {
|
|
|
787
980
|
}
|
|
788
981
|
allSummary.push(...extractSummary(result.output, name));
|
|
789
982
|
}
|
|
790
|
-
return { pass: allPass, errors: allErrors, summary: allSummary };
|
|
983
|
+
return { pass: allPass, errors: allErrors, summary: allSummary };
|
|
984
|
+
}
|
|
985
|
+
var init_verify_runner = __esm({
|
|
986
|
+
"src/verify-runner.ts"() {
|
|
987
|
+
"use strict";
|
|
988
|
+
init_config();
|
|
989
|
+
init_logger();
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
// src/observer.ts
|
|
994
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
995
|
+
async function diagnoseFailure(stageName, errorOutput, modifiedFiles, runner, model) {
|
|
996
|
+
const context = [
|
|
997
|
+
`Stage: ${stageName}`,
|
|
998
|
+
``,
|
|
999
|
+
`Error output:`,
|
|
1000
|
+
errorOutput.slice(-2e3),
|
|
1001
|
+
// Last 2000 chars of error
|
|
1002
|
+
``,
|
|
1003
|
+
modifiedFiles.length > 0 ? `Files modified by build stage:
|
|
1004
|
+
${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (build may not have run yet)."
|
|
1005
|
+
].join("\n");
|
|
1006
|
+
const prompt = DIAGNOSIS_PROMPT + context;
|
|
1007
|
+
try {
|
|
1008
|
+
const result = await runner.run(
|
|
1009
|
+
"diagnosis",
|
|
1010
|
+
prompt,
|
|
1011
|
+
model,
|
|
1012
|
+
3e4,
|
|
1013
|
+
// 30s timeout — this should be fast
|
|
1014
|
+
""
|
|
1015
|
+
);
|
|
1016
|
+
if (result.outcome === "completed" && result.output) {
|
|
1017
|
+
const cleaned = result.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
|
|
1018
|
+
const parsed = JSON.parse(cleaned);
|
|
1019
|
+
const validClassifications = [
|
|
1020
|
+
"fixable",
|
|
1021
|
+
"infrastructure",
|
|
1022
|
+
"pre-existing",
|
|
1023
|
+
"retry",
|
|
1024
|
+
"abort"
|
|
1025
|
+
];
|
|
1026
|
+
if (validClassifications.includes(parsed.classification)) {
|
|
1027
|
+
logger.info(` Diagnosis: ${parsed.classification} \u2014 ${parsed.reason}`);
|
|
1028
|
+
return {
|
|
1029
|
+
classification: parsed.classification,
|
|
1030
|
+
reason: parsed.reason ?? "Unknown reason",
|
|
1031
|
+
resolution: parsed.resolution ?? ""
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
} catch (err) {
|
|
1036
|
+
logger.warn(` Diagnosis error: ${err instanceof Error ? err.message : err}`);
|
|
1037
|
+
}
|
|
1038
|
+
logger.warn(" Diagnosis failed \u2014 defaulting to fixable");
|
|
1039
|
+
return {
|
|
1040
|
+
classification: "fixable",
|
|
1041
|
+
reason: "Could not diagnose failure",
|
|
1042
|
+
resolution: errorOutput.slice(-500)
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
function getModifiedFiles(projectDir) {
|
|
1046
|
+
try {
|
|
1047
|
+
const output = execFileSync5("git", ["diff", "--name-only", "HEAD~1"], {
|
|
1048
|
+
encoding: "utf-8",
|
|
1049
|
+
cwd: projectDir,
|
|
1050
|
+
timeout: 5e3,
|
|
1051
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1052
|
+
}).trim();
|
|
1053
|
+
return output ? output.split("\n").filter(Boolean) : [];
|
|
1054
|
+
} catch {
|
|
1055
|
+
return [];
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
var DIAGNOSIS_PROMPT;
|
|
1059
|
+
var init_observer = __esm({
|
|
1060
|
+
"src/observer.ts"() {
|
|
1061
|
+
"use strict";
|
|
1062
|
+
init_logger();
|
|
1063
|
+
DIAGNOSIS_PROMPT = `You are a pipeline failure diagnosis agent. Analyze the error and classify it.
|
|
1064
|
+
|
|
1065
|
+
Output ONLY valid JSON. No markdown fences. No explanation.
|
|
1066
|
+
|
|
1067
|
+
{
|
|
1068
|
+
"classification": "fixable | infrastructure | pre-existing | retry | abort",
|
|
1069
|
+
"reason": "One sentence explaining what went wrong",
|
|
1070
|
+
"resolution": "Specific instructions for fixing (if fixable) or what the user needs to do (if infrastructure)"
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
Classification rules:
|
|
1074
|
+
- fixable: Error is in code that was just written/modified. The resolution should describe exactly what to change.
|
|
1075
|
+
- infrastructure: External dependency not available (database, API, service). The resolution should say what the user needs to set up.
|
|
1076
|
+
- pre-existing: Error exists in code that was NOT modified. Safe to skip. The resolution should note which files.
|
|
1077
|
+
- retry: Transient error (network timeout, rate limit, flaky test). Worth retrying once.
|
|
1078
|
+
- abort: Unrecoverable error (permission denied, corrupted state, out of disk). Pipeline should stop.
|
|
1079
|
+
|
|
1080
|
+
Error context:
|
|
1081
|
+
`;
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
// src/stages/gate.ts
|
|
1086
|
+
import * as fs6 from "fs";
|
|
1087
|
+
import * as path6 from "path";
|
|
1088
|
+
function executeGateStage(ctx, def) {
|
|
1089
|
+
if (ctx.input.dryRun) {
|
|
1090
|
+
logger.info(` [dry-run] skipping ${def.name}`);
|
|
1091
|
+
return { outcome: "completed", retries: 0 };
|
|
1092
|
+
}
|
|
1093
|
+
const verifyResult = runQualityGates(ctx.taskDir, ctx.projectDir);
|
|
1094
|
+
const lines = [
|
|
1095
|
+
`# Verification Report
|
|
1096
|
+
`,
|
|
1097
|
+
`## Result: ${verifyResult.pass ? "PASS" : "FAIL"}
|
|
1098
|
+
`
|
|
1099
|
+
];
|
|
1100
|
+
if (verifyResult.errors.length > 0) {
|
|
1101
|
+
lines.push(`
|
|
1102
|
+
## Errors
|
|
1103
|
+
`);
|
|
1104
|
+
for (const e of verifyResult.errors) {
|
|
1105
|
+
lines.push(`- ${e}
|
|
1106
|
+
`);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
if (verifyResult.summary.length > 0) {
|
|
1110
|
+
lines.push(`
|
|
1111
|
+
## Summary
|
|
1112
|
+
`);
|
|
1113
|
+
for (const s of verifyResult.summary) {
|
|
1114
|
+
lines.push(`- ${s}
|
|
1115
|
+
`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
fs6.writeFileSync(path6.join(ctx.taskDir, "verify.md"), lines.join(""));
|
|
1119
|
+
return {
|
|
1120
|
+
outcome: verifyResult.pass ? "completed" : "failed",
|
|
1121
|
+
retries: 0
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
var init_gate = __esm({
|
|
1125
|
+
"src/stages/gate.ts"() {
|
|
1126
|
+
"use strict";
|
|
1127
|
+
init_verify_runner();
|
|
1128
|
+
init_logger();
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
// src/stages/verify.ts
|
|
1133
|
+
import * as fs7 from "fs";
|
|
1134
|
+
import * as path7 from "path";
|
|
1135
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
1136
|
+
async function executeVerifyWithAutofix(ctx, def) {
|
|
1137
|
+
const maxAttempts = def.maxRetries ?? 2;
|
|
1138
|
+
for (let attempt = 0; attempt <= maxAttempts; attempt++) {
|
|
1139
|
+
logger.info(` verification attempt ${attempt + 1}/${maxAttempts + 1}`);
|
|
1140
|
+
const gateResult = executeGateStage(ctx, def);
|
|
1141
|
+
if (gateResult.outcome === "completed") {
|
|
1142
|
+
return { ...gateResult, retries: attempt };
|
|
1143
|
+
}
|
|
1144
|
+
if (attempt < maxAttempts) {
|
|
1145
|
+
const verifyPath = path7.join(ctx.taskDir, "verify.md");
|
|
1146
|
+
const errorOutput = fs7.existsSync(verifyPath) ? fs7.readFileSync(verifyPath, "utf-8") : "Unknown error";
|
|
1147
|
+
const modifiedFiles = getModifiedFiles(ctx.projectDir);
|
|
1148
|
+
const defaultRunner = getRunnerForStage(ctx, "taskify");
|
|
1149
|
+
const diagnosis = await diagnoseFailure(
|
|
1150
|
+
"verify",
|
|
1151
|
+
errorOutput,
|
|
1152
|
+
modifiedFiles,
|
|
1153
|
+
defaultRunner,
|
|
1154
|
+
resolveModel("cheap")
|
|
1155
|
+
);
|
|
1156
|
+
if (diagnosis.classification === "infrastructure") {
|
|
1157
|
+
logger.warn(` Infrastructure issue: ${diagnosis.reason}`);
|
|
1158
|
+
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
1159
|
+
try {
|
|
1160
|
+
postComment(ctx.input.issueNumber, `\u26A0\uFE0F **Infrastructure issue detected:** ${diagnosis.reason}
|
|
1161
|
+
|
|
1162
|
+
${diagnosis.resolution}`);
|
|
1163
|
+
} catch {
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
return { outcome: "completed", retries: attempt, error: `Skipped: ${diagnosis.reason}` };
|
|
1167
|
+
}
|
|
1168
|
+
if (diagnosis.classification === "pre-existing") {
|
|
1169
|
+
logger.warn(` Pre-existing issue: ${diagnosis.reason}`);
|
|
1170
|
+
return { outcome: "completed", retries: attempt, error: `Skipped: ${diagnosis.reason}` };
|
|
1171
|
+
}
|
|
1172
|
+
if (diagnosis.classification === "abort") {
|
|
1173
|
+
logger.error(` Unrecoverable: ${diagnosis.reason}`);
|
|
1174
|
+
return { outcome: "failed", retries: attempt, error: diagnosis.reason };
|
|
1175
|
+
}
|
|
1176
|
+
logger.info(` Diagnosis: ${diagnosis.classification} \u2014 ${diagnosis.reason}`);
|
|
1177
|
+
const config = getProjectConfig();
|
|
1178
|
+
const runFix = (cmd) => {
|
|
1179
|
+
if (!cmd) return;
|
|
1180
|
+
const parts = parseCommand(cmd);
|
|
1181
|
+
if (parts.length === 0) return;
|
|
1182
|
+
try {
|
|
1183
|
+
execFileSync6(parts[0], parts.slice(1), {
|
|
1184
|
+
stdio: "pipe",
|
|
1185
|
+
timeout: FIX_COMMAND_TIMEOUT_MS
|
|
1186
|
+
});
|
|
1187
|
+
} catch {
|
|
1188
|
+
}
|
|
1189
|
+
};
|
|
1190
|
+
runFix(config.quality.lintFix);
|
|
1191
|
+
runFix(config.quality.formatFix);
|
|
1192
|
+
if (def.retryWithAgent) {
|
|
1193
|
+
const autofixCtx = {
|
|
1194
|
+
...ctx,
|
|
1195
|
+
input: {
|
|
1196
|
+
...ctx.input,
|
|
1197
|
+
feedback: `${diagnosis.resolution}
|
|
1198
|
+
|
|
1199
|
+
${ctx.input.feedback ?? ""}`.trim()
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1202
|
+
logger.info(` running ${def.retryWithAgent} agent with diagnosis guidance...`);
|
|
1203
|
+
await executeAgentStage(autofixCtx, {
|
|
1204
|
+
...def,
|
|
1205
|
+
name: def.retryWithAgent,
|
|
1206
|
+
type: "agent",
|
|
1207
|
+
modelTier: "mid",
|
|
1208
|
+
timeout: 3e5,
|
|
1209
|
+
outputFile: void 0
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
return {
|
|
1215
|
+
outcome: "failed",
|
|
1216
|
+
retries: maxAttempts,
|
|
1217
|
+
error: "Verification failed after autofix attempts"
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
var init_verify = __esm({
|
|
1221
|
+
"src/stages/verify.ts"() {
|
|
1222
|
+
"use strict";
|
|
1223
|
+
init_context();
|
|
1224
|
+
init_config();
|
|
1225
|
+
init_verify_runner();
|
|
1226
|
+
init_runner_selection();
|
|
1227
|
+
init_github_api();
|
|
1228
|
+
init_observer();
|
|
1229
|
+
init_logger();
|
|
1230
|
+
init_agent();
|
|
1231
|
+
init_gate();
|
|
1232
|
+
}
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
// src/stages/review.ts
|
|
1236
|
+
import * as fs8 from "fs";
|
|
1237
|
+
import * as path8 from "path";
|
|
1238
|
+
async function executeReviewWithFix(ctx, def) {
|
|
1239
|
+
if (ctx.input.dryRun) {
|
|
1240
|
+
return { outcome: "completed", retries: 0 };
|
|
1241
|
+
}
|
|
1242
|
+
const reviewDef = STAGES.find((s) => s.name === "review");
|
|
1243
|
+
const reviewFixDef = STAGES.find((s) => s.name === "review-fix");
|
|
1244
|
+
const reviewResult = await executeAgentStage(ctx, reviewDef);
|
|
1245
|
+
if (reviewResult.outcome !== "completed") {
|
|
1246
|
+
return reviewResult;
|
|
1247
|
+
}
|
|
1248
|
+
const reviewFile = path8.join(ctx.taskDir, "review.md");
|
|
1249
|
+
if (!fs8.existsSync(reviewFile)) {
|
|
1250
|
+
return { outcome: "failed", retries: 0, error: "review.md not found" };
|
|
1251
|
+
}
|
|
1252
|
+
const content = fs8.readFileSync(reviewFile, "utf-8");
|
|
1253
|
+
const hasIssues = /\bfail\b/i.test(content) && !/pass/i.test(content);
|
|
1254
|
+
if (!hasIssues) {
|
|
1255
|
+
return reviewResult;
|
|
1256
|
+
}
|
|
1257
|
+
logger.info(` review found issues, running review-fix...`);
|
|
1258
|
+
const fixResult = await executeAgentStage(ctx, reviewFixDef);
|
|
1259
|
+
if (fixResult.outcome !== "completed") {
|
|
1260
|
+
return fixResult;
|
|
1261
|
+
}
|
|
1262
|
+
logger.info(` re-running review after fix...`);
|
|
1263
|
+
return executeAgentStage(ctx, reviewDef);
|
|
791
1264
|
}
|
|
792
|
-
var
|
|
793
|
-
"src/
|
|
1265
|
+
var init_review = __esm({
|
|
1266
|
+
"src/stages/review.ts"() {
|
|
794
1267
|
"use strict";
|
|
795
|
-
|
|
1268
|
+
init_definitions();
|
|
796
1269
|
init_logger();
|
|
1270
|
+
init_agent();
|
|
797
1271
|
}
|
|
798
1272
|
});
|
|
799
1273
|
|
|
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");
|
|
1274
|
+
// src/stages/ship.ts
|
|
1275
|
+
import * as fs9 from "fs";
|
|
1276
|
+
import * as path9 from "path";
|
|
1277
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
1278
|
+
function buildPrBody(ctx) {
|
|
1279
|
+
const sections = [];
|
|
1280
|
+
const taskJsonPath = path9.join(ctx.taskDir, "task.json");
|
|
1281
|
+
if (fs9.existsSync(taskJsonPath)) {
|
|
1282
|
+
try {
|
|
1283
|
+
const raw = fs9.readFileSync(taskJsonPath, "utf-8");
|
|
815
1284
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
816
|
-
const
|
|
817
|
-
if (
|
|
818
|
-
|
|
1285
|
+
const task = JSON.parse(cleaned);
|
|
1286
|
+
if (task.description) {
|
|
1287
|
+
sections.push(`## What
|
|
819
1288
|
|
|
820
|
-
${
|
|
1289
|
+
${task.description}`);
|
|
1290
|
+
}
|
|
1291
|
+
if (task.scope?.length) {
|
|
1292
|
+
sections.push(`
|
|
1293
|
+
## Scope
|
|
821
1294
|
|
|
822
|
-
|
|
823
|
-
postComment(ctx.input.issueNumber, body);
|
|
824
|
-
setLifecycleLabel(ctx.input.issueNumber, "waiting");
|
|
825
|
-
return true;
|
|
1295
|
+
${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
|
|
826
1296
|
}
|
|
1297
|
+
sections.push(`
|
|
1298
|
+
**Type:** ${task.task_type ?? "unknown"} | **Risk:** ${task.risk_level ?? "unknown"}`);
|
|
1299
|
+
} catch {
|
|
827
1300
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
${questions.map((q, i) => `${i + 1}. ${q}`).join("\n")}
|
|
1301
|
+
}
|
|
1302
|
+
const reviewPath = path9.join(ctx.taskDir, "review.md");
|
|
1303
|
+
if (fs9.existsSync(reviewPath)) {
|
|
1304
|
+
const review = fs9.readFileSync(reviewPath, "utf-8");
|
|
1305
|
+
const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
1306
|
+
if (summaryMatch) {
|
|
1307
|
+
const summary = summaryMatch[1].trim();
|
|
1308
|
+
if (summary) {
|
|
1309
|
+
sections.push(`
|
|
1310
|
+
## Changes
|
|
840
1311
|
|
|
841
|
-
|
|
842
|
-
postComment(ctx.input.issueNumber, body);
|
|
843
|
-
setLifecycleLabel(ctx.input.issueNumber, "waiting");
|
|
844
|
-
return true;
|
|
845
|
-
}
|
|
1312
|
+
${summary}`);
|
|
846
1313
|
}
|
|
847
1314
|
}
|
|
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
|
-
);
|
|
1315
|
+
const verdictMatch = review.match(/## Verdict:\s*(PASS|FAIL)/i);
|
|
1316
|
+
if (verdictMatch) {
|
|
1317
|
+
sections.push(`
|
|
1318
|
+
**Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
|
|
1319
|
+
}
|
|
860
1320
|
}
|
|
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;
|
|
1321
|
+
const verifyPath = path9.join(ctx.taskDir, "verify.md");
|
|
1322
|
+
if (fs9.existsSync(verifyPath)) {
|
|
1323
|
+
const verify = fs9.readFileSync(verifyPath, "utf-8");
|
|
1324
|
+
if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
|
|
872
1325
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
stages[stage.name] = { state: "pending", retries: 0 };
|
|
1326
|
+
const planPath = path9.join(ctx.taskDir, "plan.md");
|
|
1327
|
+
if (fs9.existsSync(planPath)) {
|
|
1328
|
+
const plan = fs9.readFileSync(planPath, "utf-8").trim();
|
|
1329
|
+
if (plan) {
|
|
1330
|
+
const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
|
|
1331
|
+
sections.push(`
|
|
1332
|
+
<details><summary>\u{1F4CB} Implementation plan</summary>
|
|
1333
|
+
|
|
1334
|
+
${truncated}
|
|
1335
|
+
</details>`);
|
|
1336
|
+
}
|
|
885
1337
|
}
|
|
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 };
|
|
1338
|
+
if (ctx.input.issueNumber) {
|
|
1339
|
+
sections.push(`
|
|
1340
|
+
Closes #${ctx.input.issueNumber}`);
|
|
899
1341
|
}
|
|
1342
|
+
sections.push(`
|
|
1343
|
+
---
|
|
1344
|
+
\u{1F916} Generated by Kody`);
|
|
1345
|
+
return sections.join("\n");
|
|
900
1346
|
}
|
|
901
|
-
|
|
1347
|
+
function executeShipStage(ctx, _def) {
|
|
1348
|
+
const shipPath = path9.join(ctx.taskDir, "ship.md");
|
|
902
1349
|
if (ctx.input.dryRun) {
|
|
903
|
-
|
|
904
|
-
return { outcome: "completed", retries: 0 };
|
|
1350
|
+
fs9.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
|
|
1351
|
+
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
905
1352
|
}
|
|
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;
|
|
1353
|
+
if (ctx.input.local && !ctx.input.issueNumber) {
|
|
1354
|
+
fs9.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
|
|
1355
|
+
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
914
1356
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
1357
|
+
try {
|
|
1358
|
+
const head = getCurrentBranch(ctx.projectDir);
|
|
1359
|
+
const base = getDefaultBranch(ctx.projectDir);
|
|
1360
|
+
pushBranch(ctx.projectDir);
|
|
1361
|
+
const config = getProjectConfig();
|
|
1362
|
+
let owner = config.github?.owner;
|
|
1363
|
+
let repo = config.github?.repo;
|
|
1364
|
+
if (!owner || !repo) {
|
|
1365
|
+
try {
|
|
1366
|
+
const remoteUrl = execFileSync7("git", ["remote", "get-url", "origin"], {
|
|
1367
|
+
encoding: "utf-8",
|
|
1368
|
+
cwd: ctx.projectDir
|
|
1369
|
+
}).trim();
|
|
1370
|
+
const match = remoteUrl.match(/github\.com[/:]([^/]+)\/([^/.]+)/);
|
|
1371
|
+
if (match) {
|
|
1372
|
+
owner = match[1];
|
|
1373
|
+
repo = match[2];
|
|
1374
|
+
}
|
|
1375
|
+
} catch {
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
let title = "Update";
|
|
1379
|
+
const TYPE_PREFIX = {
|
|
1380
|
+
feature: "feat",
|
|
1381
|
+
bugfix: "fix",
|
|
1382
|
+
refactor: "refactor",
|
|
1383
|
+
docs: "docs",
|
|
1384
|
+
chore: "chore"
|
|
1385
|
+
};
|
|
1386
|
+
const taskJsonPath = path9.join(ctx.taskDir, "task.json");
|
|
1387
|
+
if (fs9.existsSync(taskJsonPath)) {
|
|
1388
|
+
try {
|
|
1389
|
+
const raw = fs9.readFileSync(taskJsonPath, "utf-8");
|
|
1390
|
+
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
1391
|
+
const task = JSON.parse(cleaned);
|
|
1392
|
+
const prefix = TYPE_PREFIX[task.task_type] ?? "chore";
|
|
1393
|
+
const taskTitle = task.title ?? "Update";
|
|
1394
|
+
title = `${prefix}: ${taskTitle}`.slice(0, 72);
|
|
1395
|
+
} catch {
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
if (title === "Update") {
|
|
1399
|
+
const taskMdPath = path9.join(ctx.taskDir, "task.md");
|
|
1400
|
+
if (fs9.existsSync(taskMdPath)) {
|
|
1401
|
+
const content = fs9.readFileSync(taskMdPath, "utf-8");
|
|
1402
|
+
const firstLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith("*"));
|
|
1403
|
+
if (firstLine) title = `chore: ${firstLine.trim()}`.slice(0, 72);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
const body = buildPrBody(ctx);
|
|
1407
|
+
const pr = createPR(head, base, title, body);
|
|
1408
|
+
if (pr) {
|
|
1409
|
+
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
1410
|
+
try {
|
|
1411
|
+
postComment(ctx.input.issueNumber, `\u{1F389} PR created: ${pr.url}`);
|
|
1412
|
+
} catch {
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
fs9.writeFileSync(shipPath, `# Ship
|
|
1416
|
+
|
|
1417
|
+
PR created: ${pr.url}
|
|
1418
|
+
PR #${pr.number}
|
|
1419
|
+
`);
|
|
1420
|
+
} else {
|
|
1421
|
+
fs9.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
|
|
1422
|
+
}
|
|
1423
|
+
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
1424
|
+
} catch (err) {
|
|
1425
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1426
|
+
fs9.writeFileSync(shipPath, `# Ship
|
|
1427
|
+
|
|
1428
|
+
Failed: ${msg}
|
|
1429
|
+
`);
|
|
1430
|
+
return { outcome: "failed", retries: 0, error: msg };
|
|
922
1431
|
}
|
|
923
|
-
|
|
924
|
-
|
|
1432
|
+
}
|
|
1433
|
+
var init_ship = __esm({
|
|
1434
|
+
"src/stages/ship.ts"() {
|
|
1435
|
+
"use strict";
|
|
1436
|
+
init_git_utils();
|
|
1437
|
+
init_github_api();
|
|
1438
|
+
init_config();
|
|
925
1439
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
// src/pipeline/executor-registry.ts
|
|
1443
|
+
function getExecutor(name) {
|
|
1444
|
+
const executor = EXECUTOR_REGISTRY[name];
|
|
1445
|
+
if (!executor) {
|
|
1446
|
+
throw new Error(`No executor registered for stage: ${name}`);
|
|
1447
|
+
}
|
|
1448
|
+
return executor;
|
|
1449
|
+
}
|
|
1450
|
+
var EXECUTOR_REGISTRY;
|
|
1451
|
+
var init_executor_registry = __esm({
|
|
1452
|
+
"src/pipeline/executor-registry.ts"() {
|
|
1453
|
+
"use strict";
|
|
1454
|
+
init_agent();
|
|
1455
|
+
init_verify();
|
|
1456
|
+
init_review();
|
|
1457
|
+
init_ship();
|
|
1458
|
+
EXECUTOR_REGISTRY = {
|
|
1459
|
+
taskify: executeAgentStage,
|
|
1460
|
+
plan: executeAgentStage,
|
|
1461
|
+
build: executeAgentStage,
|
|
1462
|
+
verify: executeVerifyWithAutofix,
|
|
1463
|
+
review: executeReviewWithFix,
|
|
1464
|
+
"review-fix": executeAgentStage,
|
|
1465
|
+
ship: executeShipStage
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
// src/pipeline/questions.ts
|
|
1471
|
+
import * as fs10 from "fs";
|
|
1472
|
+
import * as path10 from "path";
|
|
1473
|
+
function checkForQuestions(ctx, stageName) {
|
|
1474
|
+
if (ctx.input.local || !ctx.input.issueNumber) return false;
|
|
1475
|
+
try {
|
|
1476
|
+
if (stageName === "taskify") {
|
|
1477
|
+
const taskJsonPath = path10.join(ctx.taskDir, "task.json");
|
|
1478
|
+
if (!fs10.existsSync(taskJsonPath)) return false;
|
|
1479
|
+
const raw = fs10.readFileSync(taskJsonPath, "utf-8");
|
|
1480
|
+
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
1481
|
+
const taskJson = JSON.parse(cleaned);
|
|
1482
|
+
if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
|
|
1483
|
+
const body = `\u{1F914} **Kody has questions before proceeding:**
|
|
1484
|
+
|
|
1485
|
+
${taskJson.questions.map((q, i) => `${i + 1}. ${q}`).join("\n")}
|
|
1486
|
+
|
|
1487
|
+
Reply with \`@kody approve\` and your answers in the comment body.`;
|
|
1488
|
+
postComment(ctx.input.issueNumber, body);
|
|
1489
|
+
setLifecycleLabel(ctx.input.issueNumber, "waiting");
|
|
1490
|
+
return true;
|
|
938
1491
|
}
|
|
939
1492
|
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
const
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1493
|
+
if (stageName === "plan") {
|
|
1494
|
+
const planPath = path10.join(ctx.taskDir, "plan.md");
|
|
1495
|
+
if (!fs10.existsSync(planPath)) return false;
|
|
1496
|
+
const plan = fs10.readFileSync(planPath, "utf-8");
|
|
1497
|
+
const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
1498
|
+
if (questionsMatch) {
|
|
1499
|
+
const questionsText = questionsMatch[1].trim();
|
|
1500
|
+
const questions = questionsText.split("\n").filter((l) => l.startsWith("- ")).map((l) => l.slice(2));
|
|
1501
|
+
if (questions.length > 0) {
|
|
1502
|
+
const body = `\u{1F3D7}\uFE0F **Kody has architecture questions:**
|
|
1503
|
+
|
|
1504
|
+
${questions.map((q, i) => `${i + 1}. ${q}`).join("\n")}
|
|
1505
|
+
|
|
1506
|
+
Reply with \`@kody approve\` and your answers in the comment body.`;
|
|
1507
|
+
postComment(ctx.input.issueNumber, body);
|
|
1508
|
+
setLifecycleLabel(ctx.input.issueNumber, "waiting");
|
|
1509
|
+
return true;
|
|
1510
|
+
}
|
|
948
1511
|
}
|
|
949
1512
|
}
|
|
1513
|
+
} catch {
|
|
950
1514
|
}
|
|
951
|
-
return
|
|
1515
|
+
return false;
|
|
952
1516
|
}
|
|
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
|
-
}
|
|
1517
|
+
var init_questions = __esm({
|
|
1518
|
+
"src/pipeline/questions.ts"() {
|
|
1519
|
+
"use strict";
|
|
1520
|
+
init_github_api();
|
|
982
1521
|
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
// src/pipeline/hooks.ts
|
|
1525
|
+
import * as fs11 from "fs";
|
|
1526
|
+
import * as path11 from "path";
|
|
1527
|
+
function applyPreStageLabel(ctx, def) {
|
|
1528
|
+
if (!ctx.input.issueNumber || ctx.input.local) return;
|
|
1529
|
+
if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
|
|
1530
|
+
if (def.name === "review") setLifecycleLabel(ctx.input.issueNumber, "review");
|
|
1531
|
+
}
|
|
1532
|
+
function checkQuestionsAfterStage(ctx, def, state) {
|
|
1533
|
+
if (def.name !== "taskify" && def.name !== "plan") return null;
|
|
1534
|
+
if (ctx.input.dryRun) return null;
|
|
1535
|
+
const paused = checkForQuestions(ctx, def.name);
|
|
1536
|
+
if (!paused) return null;
|
|
1537
|
+
state.state = "failed";
|
|
1538
|
+
state.stages[def.name] = {
|
|
1539
|
+
...state.stages[def.name],
|
|
1540
|
+
state: "completed",
|
|
1541
|
+
error: "paused: waiting for answers"
|
|
987
1542
|
};
|
|
1543
|
+
writeState(state, ctx.taskDir);
|
|
1544
|
+
logger.info(` Pipeline paused \u2014 questions posted on issue`);
|
|
1545
|
+
return state;
|
|
988
1546
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
const
|
|
994
|
-
if (
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1547
|
+
function autoDetectComplexity(ctx, def) {
|
|
1548
|
+
if (def.name !== "taskify") return null;
|
|
1549
|
+
if (ctx.input.complexity) return null;
|
|
1550
|
+
try {
|
|
1551
|
+
const taskJsonPath = path11.join(ctx.taskDir, "task.json");
|
|
1552
|
+
if (!fs11.existsSync(taskJsonPath)) return null;
|
|
1553
|
+
const raw = fs11.readFileSync(taskJsonPath, "utf-8");
|
|
1554
|
+
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
1555
|
+
const taskJson = JSON.parse(cleaned);
|
|
1556
|
+
if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
|
|
1557
|
+
const complexity = taskJson.risk_level;
|
|
1558
|
+
const activeStages = filterByComplexity(STAGES, complexity);
|
|
1559
|
+
logger.info(` Complexity auto-detected: ${complexity} (${activeStages.map((s) => s.name).join(" \u2192 ")})`);
|
|
1560
|
+
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
1561
|
+
try {
|
|
1562
|
+
setLifecycleLabel(ctx.input.issueNumber, complexity);
|
|
1563
|
+
} catch {
|
|
1564
|
+
}
|
|
1565
|
+
if (taskJson.task_type) {
|
|
1003
1566
|
try {
|
|
1004
|
-
|
|
1005
|
-
stdio: "pipe",
|
|
1006
|
-
timeout: FIX_COMMAND_TIMEOUT_MS
|
|
1007
|
-
});
|
|
1567
|
+
setLabel(ctx.input.issueNumber, `kody:${taskJson.task_type}`);
|
|
1008
1568
|
} catch {
|
|
1009
1569
|
}
|
|
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
1570
|
}
|
|
1024
1571
|
}
|
|
1572
|
+
return { complexity, activeStages };
|
|
1573
|
+
} catch {
|
|
1574
|
+
return null;
|
|
1025
1575
|
}
|
|
1026
|
-
return {
|
|
1027
|
-
outcome: "failed",
|
|
1028
|
-
retries: maxAttempts,
|
|
1029
|
-
error: "Verification failed after autofix attempts"
|
|
1030
|
-
};
|
|
1031
1576
|
}
|
|
1032
|
-
|
|
1033
|
-
if (ctx.input.dryRun)
|
|
1034
|
-
|
|
1577
|
+
function commitAfterStage(ctx, def) {
|
|
1578
|
+
if (ctx.input.dryRun || !ctx.input.issueNumber) return;
|
|
1579
|
+
if (def.name === "build") {
|
|
1580
|
+
try {
|
|
1581
|
+
commitAll(`feat(${ctx.taskId}): implement task`, ctx.projectDir);
|
|
1582
|
+
} catch {
|
|
1583
|
+
}
|
|
1035
1584
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1585
|
+
if (def.name === "review-fix") {
|
|
1586
|
+
try {
|
|
1587
|
+
commitAll(`fix(${ctx.taskId}): address review`, ctx.projectDir);
|
|
1588
|
+
} catch {
|
|
1589
|
+
}
|
|
1041
1590
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1591
|
+
}
|
|
1592
|
+
function postSkippedStagesComment(ctx, complexity, activeStages) {
|
|
1593
|
+
if (!ctx.input.issueNumber || ctx.input.local || ctx.input.dryRun) return;
|
|
1594
|
+
const skipped = STAGES.filter((s) => !activeStages.find((a) => a.name === s.name)).map((s) => s.name);
|
|
1595
|
+
if (skipped.length === 0) return;
|
|
1596
|
+
try {
|
|
1597
|
+
postComment(
|
|
1598
|
+
ctx.input.issueNumber,
|
|
1599
|
+
`\u26A1 **Complexity: ${complexity}** \u2014 skipping ${skipped.join(", ")} (not needed for ${complexity}-risk tasks)`
|
|
1600
|
+
);
|
|
1601
|
+
} catch {
|
|
1045
1602
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1603
|
+
}
|
|
1604
|
+
var init_hooks = __esm({
|
|
1605
|
+
"src/pipeline/hooks.ts"() {
|
|
1606
|
+
"use strict";
|
|
1607
|
+
init_definitions();
|
|
1608
|
+
init_github_api();
|
|
1609
|
+
init_git_utils();
|
|
1610
|
+
init_questions();
|
|
1611
|
+
init_complexity();
|
|
1612
|
+
init_state();
|
|
1613
|
+
init_logger();
|
|
1050
1614
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
// src/learning/auto-learn.ts
|
|
1618
|
+
import * as fs12 from "fs";
|
|
1619
|
+
import * as path12 from "path";
|
|
1620
|
+
function stripAnsi(str) {
|
|
1621
|
+
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
1622
|
+
}
|
|
1623
|
+
function autoLearn(ctx) {
|
|
1624
|
+
try {
|
|
1625
|
+
const memoryDir = path12.join(ctx.projectDir, ".kody", "memory");
|
|
1626
|
+
if (!fs12.existsSync(memoryDir)) {
|
|
1627
|
+
fs12.mkdirSync(memoryDir, { recursive: true });
|
|
1628
|
+
}
|
|
1629
|
+
const learnings = [];
|
|
1630
|
+
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1631
|
+
const verifyPath = path12.join(ctx.taskDir, "verify.md");
|
|
1632
|
+
if (fs12.existsSync(verifyPath)) {
|
|
1633
|
+
const verify = stripAnsi(fs12.readFileSync(verifyPath, "utf-8"));
|
|
1634
|
+
if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
|
|
1635
|
+
if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
|
|
1636
|
+
if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
|
|
1637
|
+
if (/prettier/i.test(verify)) learnings.push("- Uses prettier for formatting");
|
|
1638
|
+
if (/tsc\b/i.test(verify)) learnings.push("- Uses TypeScript (tsc)");
|
|
1639
|
+
if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
|
|
1640
|
+
if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
|
|
1641
|
+
}
|
|
1642
|
+
const reviewPath = path12.join(ctx.taskDir, "review.md");
|
|
1643
|
+
if (fs12.existsSync(reviewPath)) {
|
|
1644
|
+
const review = fs12.readFileSync(reviewPath, "utf-8");
|
|
1645
|
+
if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
|
|
1646
|
+
if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
|
|
1647
|
+
if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
|
|
1648
|
+
if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
|
|
1649
|
+
}
|
|
1650
|
+
const taskJsonPath = path12.join(ctx.taskDir, "task.json");
|
|
1651
|
+
if (fs12.existsSync(taskJsonPath)) {
|
|
1652
|
+
try {
|
|
1653
|
+
const raw = stripAnsi(fs12.readFileSync(taskJsonPath, "utf-8"));
|
|
1654
|
+
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
1655
|
+
const task = JSON.parse(cleaned);
|
|
1656
|
+
if (task.scope && Array.isArray(task.scope)) {
|
|
1657
|
+
const dirs = [...new Set(task.scope.map((s) => s.split("/").slice(0, -1).join("/")).filter(Boolean))];
|
|
1658
|
+
if (dirs.length > 0) learnings.push(`- Active directories: ${dirs.join(", ")}`);
|
|
1659
|
+
}
|
|
1660
|
+
} catch {
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
if (learnings.length > 0) {
|
|
1664
|
+
const conventionsPath = path12.join(memoryDir, "conventions.md");
|
|
1665
|
+
const entry = `
|
|
1666
|
+
## Learned ${timestamp2} (task: ${ctx.taskId})
|
|
1667
|
+
${learnings.join("\n")}
|
|
1668
|
+
`;
|
|
1669
|
+
fs12.appendFileSync(conventionsPath, entry);
|
|
1670
|
+
logger.info(`Auto-learned ${learnings.length} convention(s)`);
|
|
1671
|
+
}
|
|
1672
|
+
autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
|
|
1673
|
+
} catch {
|
|
1055
1674
|
}
|
|
1056
|
-
logger.info(` re-running review after fix...`);
|
|
1057
|
-
return executeAgentStage(ctx, reviewDef);
|
|
1058
1675
|
}
|
|
1059
|
-
function
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
|
|
1676
|
+
function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
1677
|
+
const archPath = path12.join(memoryDir, "architecture.md");
|
|
1678
|
+
if (fs12.existsSync(archPath)) return;
|
|
1679
|
+
const detected = [];
|
|
1680
|
+
const pkgPath = path12.join(projectDir, "package.json");
|
|
1681
|
+
if (fs12.existsSync(pkgPath)) {
|
|
1063
1682
|
try {
|
|
1064
|
-
const
|
|
1065
|
-
const
|
|
1066
|
-
|
|
1067
|
-
if (
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
${
|
|
1071
|
-
}
|
|
1072
|
-
if (
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1683
|
+
const pkg = JSON.parse(fs12.readFileSync(pkgPath, "utf-8"));
|
|
1684
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1685
|
+
if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
|
|
1686
|
+
else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
|
|
1687
|
+
else if (allDeps.express) detected.push(`- Framework: Express ${allDeps.express}`);
|
|
1688
|
+
else if (allDeps.fastify) detected.push(`- Framework: Fastify ${allDeps.fastify}`);
|
|
1689
|
+
if (allDeps.typescript) detected.push(`- Language: TypeScript ${allDeps.typescript}`);
|
|
1690
|
+
if (allDeps.vitest) detected.push(`- Testing: vitest ${allDeps.vitest}`);
|
|
1691
|
+
else if (allDeps.jest) detected.push(`- Testing: jest ${allDeps.jest}`);
|
|
1692
|
+
if (allDeps.eslint) detected.push(`- Linting: eslint ${allDeps.eslint}`);
|
|
1693
|
+
if (allDeps.prisma || allDeps["@prisma/client"]) detected.push("- Database: Prisma ORM");
|
|
1694
|
+
if (allDeps.drizzle || allDeps["drizzle-orm"]) detected.push("- Database: Drizzle ORM");
|
|
1695
|
+
if (allDeps.pg || allDeps.postgres) detected.push("- Database: PostgreSQL");
|
|
1696
|
+
if (allDeps.payload || allDeps["@payloadcms/next"]) detected.push(`- CMS: Payload CMS`);
|
|
1697
|
+
if (pkg.type === "module") detected.push("- Module system: ESM");
|
|
1698
|
+
else detected.push("- Module system: CommonJS");
|
|
1699
|
+
if (fs12.existsSync(path12.join(projectDir, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
|
|
1700
|
+
else if (fs12.existsSync(path12.join(projectDir, "yarn.lock"))) detected.push("- Package manager: yarn");
|
|
1701
|
+
else if (fs12.existsSync(path12.join(projectDir, "package-lock.json"))) detected.push("- Package manager: npm");
|
|
1080
1702
|
} catch {
|
|
1081
1703
|
}
|
|
1082
1704
|
}
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
const
|
|
1086
|
-
const
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
if (summary) {
|
|
1090
|
-
sections.push(`
|
|
1091
|
-
## Changes
|
|
1092
|
-
|
|
1093
|
-
${summary}`);
|
|
1705
|
+
const topDirs = [];
|
|
1706
|
+
try {
|
|
1707
|
+
const entries = fs12.readdirSync(projectDir, { withFileTypes: true });
|
|
1708
|
+
for (const entry of entries) {
|
|
1709
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
1710
|
+
topDirs.push(entry.name);
|
|
1094
1711
|
}
|
|
1095
1712
|
}
|
|
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`);
|
|
1713
|
+
if (topDirs.length > 0) detected.push(`- Top-level directories: ${topDirs.join(", ")}`);
|
|
1714
|
+
} catch {
|
|
1106
1715
|
}
|
|
1107
|
-
const
|
|
1108
|
-
if (
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
const
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
${truncated}
|
|
1116
|
-
</details>`);
|
|
1716
|
+
const srcDir = path12.join(projectDir, "src");
|
|
1717
|
+
if (fs12.existsSync(srcDir)) {
|
|
1718
|
+
try {
|
|
1719
|
+
const srcEntries = fs12.readdirSync(srcDir, { withFileTypes: true });
|
|
1720
|
+
const srcDirs = srcEntries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1721
|
+
if (srcDirs.length > 0) detected.push(`- src/ structure: ${srcDirs.join(", ")}`);
|
|
1722
|
+
} catch {
|
|
1117
1723
|
}
|
|
1118
1724
|
}
|
|
1119
|
-
if (
|
|
1120
|
-
|
|
1121
|
-
|
|
1725
|
+
if (detected.length > 0) {
|
|
1726
|
+
const content = `# Architecture (auto-detected ${timestamp2})
|
|
1727
|
+
|
|
1728
|
+
## Overview
|
|
1729
|
+
${detected.join("\n")}
|
|
1730
|
+
`;
|
|
1731
|
+
fs12.writeFileSync(archPath, content);
|
|
1732
|
+
logger.info(`Auto-detected architecture (${detected.length} items)`);
|
|
1122
1733
|
}
|
|
1123
|
-
sections.push(`
|
|
1124
|
-
---
|
|
1125
|
-
\u{1F916} Generated by Kody`);
|
|
1126
|
-
return sections.join("\n");
|
|
1127
1734
|
}
|
|
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 };
|
|
1735
|
+
var init_auto_learn = __esm({
|
|
1736
|
+
"src/learning/auto-learn.ts"() {
|
|
1737
|
+
"use strict";
|
|
1738
|
+
init_logger();
|
|
1137
1739
|
}
|
|
1740
|
+
});
|
|
1741
|
+
|
|
1742
|
+
// src/pipeline.ts
|
|
1743
|
+
import * as fs13 from "fs";
|
|
1744
|
+
import * as path13 from "path";
|
|
1745
|
+
function ensureFeatureBranchIfNeeded(ctx) {
|
|
1746
|
+
if (!ctx.input.issueNumber || ctx.input.dryRun) return;
|
|
1138
1747
|
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)) {
|
|
1748
|
+
const taskMdPath = path13.join(ctx.taskDir, "task.md");
|
|
1749
|
+
const title = fs13.existsSync(taskMdPath) ? fs13.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
|
|
1750
|
+
ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
|
|
1751
|
+
syncWithDefault(ctx.projectDir);
|
|
1752
|
+
} catch (err) {
|
|
1753
|
+
logger.warn(` Failed to create/sync feature branch: ${err}`);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
function acquireLock(taskDir) {
|
|
1757
|
+
const lockPath = path13.join(taskDir, ".lock");
|
|
1758
|
+
if (fs13.existsSync(lockPath)) {
|
|
1759
|
+
try {
|
|
1760
|
+
const pid = parseInt(fs13.readFileSync(lockPath, "utf-8").trim(), 10);
|
|
1169
1761
|
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
|
-
}
|
|
1762
|
+
process.kill(pid, 0);
|
|
1763
|
+
throw new Error(`Pipeline already running (PID ${pid})`);
|
|
1764
|
+
} catch (e) {
|
|
1765
|
+
if (e.code !== "ESRCH") throw e;
|
|
1195
1766
|
}
|
|
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");
|
|
1767
|
+
} catch (e) {
|
|
1768
|
+
if (e instanceof Error && e.message.startsWith("Pipeline already")) throw e;
|
|
1203
1769
|
}
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
return { outcome: "failed", retries: 0, error: msg };
|
|
1770
|
+
}
|
|
1771
|
+
fs13.writeFileSync(lockPath, String(process.pid));
|
|
1772
|
+
}
|
|
1773
|
+
function releaseLock(taskDir) {
|
|
1774
|
+
try {
|
|
1775
|
+
fs13.unlinkSync(path13.join(taskDir, ".lock"));
|
|
1776
|
+
} catch {
|
|
1212
1777
|
}
|
|
1213
1778
|
}
|
|
1214
1779
|
async function runPipeline(ctx) {
|
|
1780
|
+
acquireLock(ctx.taskDir);
|
|
1781
|
+
try {
|
|
1782
|
+
return await runPipelineInner(ctx);
|
|
1783
|
+
} finally {
|
|
1784
|
+
releaseLock(ctx.taskDir);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
async function runPipelineInner(ctx) {
|
|
1215
1788
|
let state = loadState(ctx.taskId, ctx.taskDir);
|
|
1216
1789
|
if (!state) {
|
|
1217
1790
|
state = initState(ctx.taskId);
|
|
@@ -1236,16 +1809,7 @@ async function runPipeline(ctx) {
|
|
|
1236
1809
|
const initialPhase = ctx.input.mode === "rerun" ? "building" : "planning";
|
|
1237
1810
|
setLifecycleLabel(ctx.input.issueNumber, initialPhase);
|
|
1238
1811
|
}
|
|
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
|
-
}
|
|
1812
|
+
ensureFeatureBranchIfNeeded(ctx);
|
|
1249
1813
|
let complexity = ctx.input.complexity ?? "high";
|
|
1250
1814
|
let activeStages = filterByComplexity(STAGES, complexity);
|
|
1251
1815
|
let skippedStagesCommentPosted = false;
|
|
@@ -1265,50 +1829,20 @@ async function runPipeline(ctx) {
|
|
|
1265
1829
|
logger.info(`[${def.name}] skipped (complexity: ${complexity})`);
|
|
1266
1830
|
state.stages[def.name] = { state: "completed", retries: 0, outputFile: void 0 };
|
|
1267
1831
|
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
|
-
}
|
|
1832
|
+
if (!skippedStagesCommentPosted) {
|
|
1833
|
+
postSkippedStagesComment(ctx, complexity, activeStages);
|
|
1277
1834
|
skippedStagesCommentPosted = true;
|
|
1278
1835
|
}
|
|
1279
1836
|
continue;
|
|
1280
1837
|
}
|
|
1281
1838
|
ciGroup(`Stage: ${def.name}`);
|
|
1282
|
-
state.stages[def.name] = {
|
|
1283
|
-
state: "running",
|
|
1284
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1285
|
-
retries: 0
|
|
1286
|
-
};
|
|
1839
|
+
state.stages[def.name] = { state: "running", startedAt: (/* @__PURE__ */ new Date()).toISOString(), retries: 0 };
|
|
1287
1840
|
writeState(state, ctx.taskDir);
|
|
1288
1841
|
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
|
-
}
|
|
1842
|
+
applyPreStageLabel(ctx, def);
|
|
1293
1843
|
let result;
|
|
1294
1844
|
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
|
-
}
|
|
1845
|
+
result = await getExecutor(def.name)(ctx, def);
|
|
1312
1846
|
} catch (error) {
|
|
1313
1847
|
result = {
|
|
1314
1848
|
outcome: "failed",
|
|
@@ -1325,84 +1859,24 @@ async function runPipeline(ctx) {
|
|
|
1325
1859
|
outputFile: result.outputFile
|
|
1326
1860
|
};
|
|
1327
1861
|
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");
|
|
1862
|
+
const paused = checkQuestionsAfterStage(ctx, def, state);
|
|
1863
|
+
if (paused) return paused;
|
|
1864
|
+
const detected = autoDetectComplexity(ctx, def);
|
|
1865
|
+
if (detected) {
|
|
1866
|
+
complexity = detected.complexity;
|
|
1867
|
+
activeStages = detected.activeStages;
|
|
1395
1868
|
}
|
|
1396
|
-
|
|
1869
|
+
commitAfterStage(ctx, def);
|
|
1397
1870
|
} else {
|
|
1871
|
+
const isTimeout = result.outcome === "timed_out";
|
|
1398
1872
|
state.stages[def.name] = {
|
|
1399
|
-
state: "failed",
|
|
1873
|
+
state: isTimeout ? "timeout" : "failed",
|
|
1400
1874
|
retries: result.retries,
|
|
1401
|
-
error: result.error ?? "Stage failed"
|
|
1875
|
+
error: isTimeout ? "Stage timed out" : result.error ?? "Stage failed"
|
|
1402
1876
|
};
|
|
1403
1877
|
state.state = "failed";
|
|
1404
1878
|
writeState(state, ctx.taskDir);
|
|
1405
|
-
logger.error(`[${def.name}] \u2717 failed: ${result.error}`);
|
|
1879
|
+
logger.error(`[${def.name}] ${isTimeout ? "\u23F1 timed out" : `\u2717 failed: ${result.error}`}`);
|
|
1406
1880
|
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
1407
1881
|
setLifecycleLabel(ctx.input.issueNumber, "failed");
|
|
1408
1882
|
}
|
|
@@ -1410,9 +1884,7 @@ async function runPipeline(ctx) {
|
|
|
1410
1884
|
}
|
|
1411
1885
|
writeState(state, ctx.taskDir);
|
|
1412
1886
|
}
|
|
1413
|
-
const allCompleted = STAGES.every(
|
|
1414
|
-
(s) => state.stages[s.name].state === "completed"
|
|
1415
|
-
);
|
|
1887
|
+
const allCompleted = STAGES.every((s) => state.stages[s.name].state === "completed");
|
|
1416
1888
|
if (allCompleted) {
|
|
1417
1889
|
state.state = "completed";
|
|
1418
1890
|
writeState(state, ctx.taskDir);
|
|
@@ -1424,121 +1896,6 @@ async function runPipeline(ctx) {
|
|
|
1424
1896
|
}
|
|
1425
1897
|
return state;
|
|
1426
1898
|
}
|
|
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
1899
|
function printStatus(taskId, taskDir) {
|
|
1543
1900
|
const state = loadState(taskId, taskDir);
|
|
1544
1901
|
if (!state) {
|
|
@@ -1558,29 +1915,24 @@ Task: ${state.taskId}`);
|
|
|
1558
1915
|
console.log(` ${icon} ${stage.name}: ${s.state}${extra}`);
|
|
1559
1916
|
}
|
|
1560
1917
|
}
|
|
1561
|
-
var
|
|
1562
|
-
|
|
1563
|
-
"src/state-machine.ts"() {
|
|
1918
|
+
var init_pipeline = __esm({
|
|
1919
|
+
"src/pipeline.ts"() {
|
|
1564
1920
|
"use strict";
|
|
1565
1921
|
init_definitions();
|
|
1566
|
-
init_context();
|
|
1567
|
-
init_validators();
|
|
1568
1922
|
init_git_utils();
|
|
1569
1923
|
init_github_api();
|
|
1570
|
-
init_verify_runner();
|
|
1571
|
-
init_config();
|
|
1572
1924
|
init_logger();
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1925
|
+
init_state();
|
|
1926
|
+
init_complexity();
|
|
1927
|
+
init_executor_registry();
|
|
1928
|
+
init_hooks();
|
|
1929
|
+
init_auto_learn();
|
|
1578
1930
|
}
|
|
1579
1931
|
});
|
|
1580
1932
|
|
|
1581
1933
|
// src/preflight.ts
|
|
1582
|
-
import { execFileSync as
|
|
1583
|
-
import * as
|
|
1934
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
1935
|
+
import * as fs14 from "fs";
|
|
1584
1936
|
function check(name, fn) {
|
|
1585
1937
|
try {
|
|
1586
1938
|
const detail = fn() ?? void 0;
|
|
@@ -1592,7 +1944,7 @@ function check(name, fn) {
|
|
|
1592
1944
|
function runPreflight() {
|
|
1593
1945
|
const checks = [
|
|
1594
1946
|
check("claude CLI", () => {
|
|
1595
|
-
const v =
|
|
1947
|
+
const v = execFileSync8("claude", ["--version"], {
|
|
1596
1948
|
encoding: "utf-8",
|
|
1597
1949
|
timeout: 1e4,
|
|
1598
1950
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1600,14 +1952,14 @@ function runPreflight() {
|
|
|
1600
1952
|
return v;
|
|
1601
1953
|
}),
|
|
1602
1954
|
check("git repo", () => {
|
|
1603
|
-
|
|
1955
|
+
execFileSync8("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
1604
1956
|
encoding: "utf-8",
|
|
1605
1957
|
timeout: 5e3,
|
|
1606
1958
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1607
1959
|
});
|
|
1608
1960
|
}),
|
|
1609
1961
|
check("pnpm", () => {
|
|
1610
|
-
const v =
|
|
1962
|
+
const v = execFileSync8("pnpm", ["--version"], {
|
|
1611
1963
|
encoding: "utf-8",
|
|
1612
1964
|
timeout: 5e3,
|
|
1613
1965
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1615,7 +1967,7 @@ function runPreflight() {
|
|
|
1615
1967
|
return v;
|
|
1616
1968
|
}),
|
|
1617
1969
|
check("node >= 18", () => {
|
|
1618
|
-
const v =
|
|
1970
|
+
const v = execFileSync8("node", ["--version"], {
|
|
1619
1971
|
encoding: "utf-8",
|
|
1620
1972
|
timeout: 5e3,
|
|
1621
1973
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1625,7 +1977,7 @@ function runPreflight() {
|
|
|
1625
1977
|
return v;
|
|
1626
1978
|
}),
|
|
1627
1979
|
check("gh CLI", () => {
|
|
1628
|
-
const v =
|
|
1980
|
+
const v = execFileSync8("gh", ["--version"], {
|
|
1629
1981
|
encoding: "utf-8",
|
|
1630
1982
|
timeout: 5e3,
|
|
1631
1983
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1633,7 +1985,7 @@ function runPreflight() {
|
|
|
1633
1985
|
return v;
|
|
1634
1986
|
}),
|
|
1635
1987
|
check("package.json", () => {
|
|
1636
|
-
if (!
|
|
1988
|
+
if (!fs14.existsSync("package.json")) throw new Error("not found");
|
|
1637
1989
|
})
|
|
1638
1990
|
];
|
|
1639
1991
|
const failed = checks.filter((c) => !c.ok);
|
|
@@ -1653,11 +2005,7 @@ var init_preflight = __esm({
|
|
|
1653
2005
|
}
|
|
1654
2006
|
});
|
|
1655
2007
|
|
|
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";
|
|
2008
|
+
// src/cli/args.ts
|
|
1661
2009
|
function getArg(args2, flag) {
|
|
1662
2010
|
const idx = args2.indexOf(flag);
|
|
1663
2011
|
if (idx !== -1 && args2[idx + 1] && !args2[idx + 1].startsWith("--")) {
|
|
@@ -1699,6 +2047,18 @@ function parseArgs() {
|
|
|
1699
2047
|
complexity: getArg(args2, "--complexity") ?? process.env.COMPLEXITY
|
|
1700
2048
|
};
|
|
1701
2049
|
}
|
|
2050
|
+
var isCI2;
|
|
2051
|
+
var init_args = __esm({
|
|
2052
|
+
"src/cli/args.ts"() {
|
|
2053
|
+
"use strict";
|
|
2054
|
+
isCI2 = !!process.env.GITHUB_ACTIONS;
|
|
2055
|
+
}
|
|
2056
|
+
});
|
|
2057
|
+
|
|
2058
|
+
// src/cli/litellm.ts
|
|
2059
|
+
import * as fs15 from "fs";
|
|
2060
|
+
import * as path14 from "path";
|
|
2061
|
+
import { execFileSync as execFileSync9 } from "child_process";
|
|
1702
2062
|
async function checkLitellmHealth(url) {
|
|
1703
2063
|
try {
|
|
1704
2064
|
const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
|
|
@@ -1708,18 +2068,18 @@ async function checkLitellmHealth(url) {
|
|
|
1708
2068
|
}
|
|
1709
2069
|
}
|
|
1710
2070
|
async function tryStartLitellm(url, projectDir) {
|
|
1711
|
-
const configPath =
|
|
1712
|
-
if (!
|
|
2071
|
+
const configPath = path14.join(projectDir, "litellm-config.yaml");
|
|
2072
|
+
if (!fs15.existsSync(configPath)) {
|
|
1713
2073
|
logger.warn("litellm-config.yaml not found \u2014 cannot start proxy");
|
|
1714
2074
|
return null;
|
|
1715
2075
|
}
|
|
1716
2076
|
const portMatch = url.match(/:(\d+)/);
|
|
1717
2077
|
const port = portMatch ? portMatch[1] : "4000";
|
|
1718
2078
|
try {
|
|
1719
|
-
|
|
2079
|
+
execFileSync9("litellm", ["--version"], { timeout: 5e3, stdio: "pipe" });
|
|
1720
2080
|
} catch {
|
|
1721
2081
|
try {
|
|
1722
|
-
|
|
2082
|
+
execFileSync9("python3", ["-m", "litellm", "--version"], { timeout: 5e3, stdio: "pipe" });
|
|
1723
2083
|
} catch {
|
|
1724
2084
|
logger.warn("litellm not installed (pip install 'litellm[proxy]')");
|
|
1725
2085
|
return null;
|
|
@@ -1729,7 +2089,7 @@ async function tryStartLitellm(url, projectDir) {
|
|
|
1729
2089
|
let cmd;
|
|
1730
2090
|
let args2;
|
|
1731
2091
|
try {
|
|
1732
|
-
|
|
2092
|
+
execFileSync9("litellm", ["--version"], { timeout: 5e3, stdio: "pipe" });
|
|
1733
2093
|
cmd = "litellm";
|
|
1734
2094
|
args2 = ["--config", configPath, "--port", port];
|
|
1735
2095
|
} catch {
|
|
@@ -1753,15 +2113,26 @@ async function tryStartLitellm(url, projectDir) {
|
|
|
1753
2113
|
child.kill();
|
|
1754
2114
|
return null;
|
|
1755
2115
|
}
|
|
2116
|
+
var init_litellm = __esm({
|
|
2117
|
+
"src/cli/litellm.ts"() {
|
|
2118
|
+
"use strict";
|
|
2119
|
+
init_logger();
|
|
2120
|
+
}
|
|
2121
|
+
});
|
|
2122
|
+
|
|
2123
|
+
// src/cli/task-resolution.ts
|
|
2124
|
+
import * as fs16 from "fs";
|
|
2125
|
+
import * as path15 from "path";
|
|
2126
|
+
import { execFileSync as execFileSync10 } from "child_process";
|
|
1756
2127
|
function findLatestTaskForIssue(issueNumber, projectDir) {
|
|
1757
|
-
const tasksDir =
|
|
1758
|
-
if (!
|
|
1759
|
-
const allDirs =
|
|
2128
|
+
const tasksDir = path15.join(projectDir, ".tasks");
|
|
2129
|
+
if (!fs16.existsSync(tasksDir)) return null;
|
|
2130
|
+
const allDirs = fs16.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
|
|
1760
2131
|
const prefix = `${issueNumber}-`;
|
|
1761
2132
|
const direct = allDirs.find((d) => d.startsWith(prefix));
|
|
1762
2133
|
if (direct) return direct;
|
|
1763
2134
|
try {
|
|
1764
|
-
const branch =
|
|
2135
|
+
const branch = execFileSync10("git", ["branch", "--show-current"], {
|
|
1765
2136
|
encoding: "utf-8",
|
|
1766
2137
|
cwd: projectDir,
|
|
1767
2138
|
timeout: 5e3,
|
|
@@ -1783,11 +2154,21 @@ function generateTaskId() {
|
|
|
1783
2154
|
const pad = (n) => String(n).padStart(2, "0");
|
|
1784
2155
|
return `${String(now.getFullYear()).slice(2)}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
1785
2156
|
}
|
|
2157
|
+
var init_task_resolution = __esm({
|
|
2158
|
+
"src/cli/task-resolution.ts"() {
|
|
2159
|
+
"use strict";
|
|
2160
|
+
}
|
|
2161
|
+
});
|
|
2162
|
+
|
|
2163
|
+
// src/entry.ts
|
|
2164
|
+
var entry_exports = {};
|
|
2165
|
+
import * as fs17 from "fs";
|
|
2166
|
+
import * as path16 from "path";
|
|
1786
2167
|
async function main() {
|
|
1787
2168
|
const input = parseArgs();
|
|
1788
|
-
const projectDir = input.cwd ?
|
|
2169
|
+
const projectDir = input.cwd ? path16.resolve(input.cwd) : process.cwd();
|
|
1789
2170
|
if (input.cwd) {
|
|
1790
|
-
if (!
|
|
2171
|
+
if (!fs17.existsSync(projectDir)) {
|
|
1791
2172
|
console.error(`--cwd path does not exist: ${projectDir}`);
|
|
1792
2173
|
process.exit(1);
|
|
1793
2174
|
}
|
|
@@ -1814,8 +2195,8 @@ async function main() {
|
|
|
1814
2195
|
process.exit(1);
|
|
1815
2196
|
}
|
|
1816
2197
|
}
|
|
1817
|
-
const taskDir =
|
|
1818
|
-
|
|
2198
|
+
const taskDir = path16.join(projectDir, ".tasks", taskId);
|
|
2199
|
+
fs17.mkdirSync(taskDir, { recursive: true });
|
|
1819
2200
|
if (input.command === "status") {
|
|
1820
2201
|
printStatus(taskId, taskDir);
|
|
1821
2202
|
return;
|
|
@@ -1823,22 +2204,22 @@ async function main() {
|
|
|
1823
2204
|
logger.info("Preflight checks:");
|
|
1824
2205
|
runPreflight();
|
|
1825
2206
|
if (input.task) {
|
|
1826
|
-
|
|
2207
|
+
fs17.writeFileSync(path16.join(taskDir, "task.md"), input.task);
|
|
1827
2208
|
}
|
|
1828
|
-
const taskMdPath =
|
|
1829
|
-
if (!
|
|
2209
|
+
const taskMdPath = path16.join(taskDir, "task.md");
|
|
2210
|
+
if (!fs17.existsSync(taskMdPath) && input.issueNumber) {
|
|
1830
2211
|
logger.info(`Fetching issue #${input.issueNumber} body as task...`);
|
|
1831
2212
|
const issue = getIssue(input.issueNumber);
|
|
1832
2213
|
if (issue) {
|
|
1833
2214
|
const taskContent = `# ${issue.title}
|
|
1834
2215
|
|
|
1835
2216
|
${issue.body ?? ""}`;
|
|
1836
|
-
|
|
2217
|
+
fs17.writeFileSync(taskMdPath, taskContent);
|
|
1837
2218
|
logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
|
|
1838
2219
|
}
|
|
1839
2220
|
}
|
|
1840
2221
|
if (input.command === "run") {
|
|
1841
|
-
if (!
|
|
2222
|
+
if (!fs17.existsSync(taskMdPath)) {
|
|
1842
2223
|
console.error("No task.md found. Provide --task, --issue-number, or ensure .tasks/<id>/task.md exists.");
|
|
1843
2224
|
process.exit(1);
|
|
1844
2225
|
}
|
|
@@ -1847,10 +2228,10 @@ ${issue.body ?? ""}`;
|
|
|
1847
2228
|
input.fromStage = "build";
|
|
1848
2229
|
}
|
|
1849
2230
|
if (input.command === "rerun" && !input.fromStage) {
|
|
1850
|
-
const statusPath =
|
|
1851
|
-
if (
|
|
2231
|
+
const statusPath = path16.join(taskDir, "status.json");
|
|
2232
|
+
if (fs17.existsSync(statusPath)) {
|
|
1852
2233
|
try {
|
|
1853
|
-
const status = JSON.parse(
|
|
2234
|
+
const status = JSON.parse(fs17.readFileSync(statusPath, "utf-8"));
|
|
1854
2235
|
const stageNames = ["taskify", "plan", "build", "verify", "review", "review-fix", "ship"];
|
|
1855
2236
|
let foundPaused = false;
|
|
1856
2237
|
for (const name of stageNames) {
|
|
@@ -1893,6 +2274,14 @@ ${issue.body ?? ""}`;
|
|
|
1893
2274
|
}
|
|
1894
2275
|
};
|
|
1895
2276
|
process.on("exit", cleanupLitellm);
|
|
2277
|
+
process.on("SIGINT", () => {
|
|
2278
|
+
cleanupLitellm();
|
|
2279
|
+
process.exit(130);
|
|
2280
|
+
});
|
|
2281
|
+
process.on("SIGTERM", () => {
|
|
2282
|
+
cleanupLitellm();
|
|
2283
|
+
process.exit(143);
|
|
2284
|
+
});
|
|
1896
2285
|
if (config.agent.litellmUrl) {
|
|
1897
2286
|
const proxyRunning = await checkLitellmHealth(config.agent.litellmUrl);
|
|
1898
2287
|
if (!proxyRunning) {
|
|
@@ -1949,7 +2338,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
|
|
|
1949
2338
|
}
|
|
1950
2339
|
}
|
|
1951
2340
|
const state = await runPipeline(ctx);
|
|
1952
|
-
const files =
|
|
2341
|
+
const files = fs17.readdirSync(taskDir);
|
|
1953
2342
|
console.log(`
|
|
1954
2343
|
Artifacts in ${taskDir}:`);
|
|
1955
2344
|
for (const f of files) {
|
|
@@ -1957,7 +2346,7 @@ Artifacts in ${taskDir}:`);
|
|
|
1957
2346
|
}
|
|
1958
2347
|
if (state.state === "failed") {
|
|
1959
2348
|
const isPaused = Object.values(state.stages).some(
|
|
1960
|
-
(s) =>
|
|
2349
|
+
(s) => s.error?.includes("paused") ?? false
|
|
1961
2350
|
);
|
|
1962
2351
|
if (isPaused) {
|
|
1963
2352
|
process.exit(0);
|
|
@@ -1979,17 +2368,18 @@ Artifacts in ${taskDir}:`);
|
|
|
1979
2368
|
process.exit(1);
|
|
1980
2369
|
}
|
|
1981
2370
|
}
|
|
1982
|
-
var isCI2;
|
|
1983
2371
|
var init_entry = __esm({
|
|
1984
2372
|
"src/entry.ts"() {
|
|
1985
2373
|
"use strict";
|
|
1986
2374
|
init_agent_runner();
|
|
1987
|
-
|
|
2375
|
+
init_pipeline();
|
|
1988
2376
|
init_preflight();
|
|
1989
2377
|
init_config();
|
|
1990
2378
|
init_github_api();
|
|
1991
2379
|
init_logger();
|
|
1992
|
-
|
|
2380
|
+
init_args();
|
|
2381
|
+
init_litellm();
|
|
2382
|
+
init_task_resolution();
|
|
1993
2383
|
main().catch(async (err) => {
|
|
1994
2384
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1995
2385
|
console.error(msg);
|
|
@@ -2007,20 +2397,20 @@ var init_entry = __esm({
|
|
|
2007
2397
|
});
|
|
2008
2398
|
|
|
2009
2399
|
// src/bin/cli.ts
|
|
2010
|
-
import * as
|
|
2011
|
-
import * as
|
|
2012
|
-
import { execFileSync as
|
|
2400
|
+
import * as fs18 from "fs";
|
|
2401
|
+
import * as path17 from "path";
|
|
2402
|
+
import { execFileSync as execFileSync11 } from "child_process";
|
|
2013
2403
|
import { fileURLToPath } from "url";
|
|
2014
|
-
var __dirname =
|
|
2015
|
-
var PKG_ROOT =
|
|
2404
|
+
var __dirname = path17.dirname(fileURLToPath(import.meta.url));
|
|
2405
|
+
var PKG_ROOT = path17.resolve(__dirname, "..", "..");
|
|
2016
2406
|
function getVersion() {
|
|
2017
|
-
const pkgPath =
|
|
2018
|
-
const pkg = JSON.parse(
|
|
2407
|
+
const pkgPath = path17.join(PKG_ROOT, "package.json");
|
|
2408
|
+
const pkg = JSON.parse(fs18.readFileSync(pkgPath, "utf-8"));
|
|
2019
2409
|
return pkg.version;
|
|
2020
2410
|
}
|
|
2021
2411
|
function checkCommand2(name, args2, fix) {
|
|
2022
2412
|
try {
|
|
2023
|
-
const output =
|
|
2413
|
+
const output = execFileSync11(name, args2, {
|
|
2024
2414
|
encoding: "utf-8",
|
|
2025
2415
|
timeout: 1e4,
|
|
2026
2416
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -2031,14 +2421,14 @@ function checkCommand2(name, args2, fix) {
|
|
|
2031
2421
|
}
|
|
2032
2422
|
}
|
|
2033
2423
|
function checkFile(filePath, description, fix) {
|
|
2034
|
-
if (
|
|
2424
|
+
if (fs18.existsSync(filePath)) {
|
|
2035
2425
|
return { name: description, ok: true, detail: filePath };
|
|
2036
2426
|
}
|
|
2037
2427
|
return { name: description, ok: false, fix };
|
|
2038
2428
|
}
|
|
2039
2429
|
function checkGhAuth(cwd) {
|
|
2040
2430
|
try {
|
|
2041
|
-
const output =
|
|
2431
|
+
const output = execFileSync11("gh", ["auth", "status"], {
|
|
2042
2432
|
encoding: "utf-8",
|
|
2043
2433
|
timeout: 1e4,
|
|
2044
2434
|
cwd,
|
|
@@ -2047,7 +2437,7 @@ function checkGhAuth(cwd) {
|
|
|
2047
2437
|
const account = output.match(/Logged in to .* account (\S+)/)?.[1];
|
|
2048
2438
|
return { name: "gh auth", ok: true, detail: account ?? "authenticated" };
|
|
2049
2439
|
} catch (err) {
|
|
2050
|
-
const stderr = err.stderr ?? "";
|
|
2440
|
+
const stderr = err instanceof Error && "stderr" in err ? String(err.stderr ?? "") : "";
|
|
2051
2441
|
if (stderr.includes("not logged")) {
|
|
2052
2442
|
return { name: "gh auth", ok: false, fix: "Run: gh auth login" };
|
|
2053
2443
|
}
|
|
@@ -2056,7 +2446,7 @@ function checkGhAuth(cwd) {
|
|
|
2056
2446
|
}
|
|
2057
2447
|
function checkGhRepoAccess(cwd) {
|
|
2058
2448
|
try {
|
|
2059
|
-
const remote =
|
|
2449
|
+
const remote = execFileSync11("git", ["remote", "get-url", "origin"], {
|
|
2060
2450
|
encoding: "utf-8",
|
|
2061
2451
|
timeout: 5e3,
|
|
2062
2452
|
cwd,
|
|
@@ -2067,7 +2457,7 @@ function checkGhRepoAccess(cwd) {
|
|
|
2067
2457
|
return { name: "GitHub repo", ok: false, fix: "Set git remote origin to a GitHub URL" };
|
|
2068
2458
|
}
|
|
2069
2459
|
const repoSlug = `${match[1]}/${match[2]}`;
|
|
2070
|
-
|
|
2460
|
+
execFileSync11("gh", ["repo", "view", repoSlug, "--json", "name"], {
|
|
2071
2461
|
encoding: "utf-8",
|
|
2072
2462
|
timeout: 1e4,
|
|
2073
2463
|
cwd,
|
|
@@ -2080,7 +2470,7 @@ function checkGhRepoAccess(cwd) {
|
|
|
2080
2470
|
}
|
|
2081
2471
|
function checkGhSecret(repoSlug, secretName) {
|
|
2082
2472
|
try {
|
|
2083
|
-
const output =
|
|
2473
|
+
const output = execFileSync11("gh", ["secret", "list", "--repo", repoSlug], {
|
|
2084
2474
|
encoding: "utf-8",
|
|
2085
2475
|
timeout: 1e4,
|
|
2086
2476
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -2103,10 +2493,10 @@ function checkGhSecret(repoSlug, secretName) {
|
|
|
2103
2493
|
}
|
|
2104
2494
|
function detectArchitecture(cwd) {
|
|
2105
2495
|
const detected = [];
|
|
2106
|
-
const pkgPath =
|
|
2107
|
-
if (
|
|
2496
|
+
const pkgPath = path17.join(cwd, "package.json");
|
|
2497
|
+
if (fs18.existsSync(pkgPath)) {
|
|
2108
2498
|
try {
|
|
2109
|
-
const pkg = JSON.parse(
|
|
2499
|
+
const pkg = JSON.parse(fs18.readFileSync(pkgPath, "utf-8"));
|
|
2110
2500
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2111
2501
|
if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
|
|
2112
2502
|
else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
|
|
@@ -2129,44 +2519,44 @@ function detectArchitecture(cwd) {
|
|
|
2129
2519
|
if (allDeps.tailwindcss) detected.push(`- CSS: Tailwind CSS ${allDeps.tailwindcss}`);
|
|
2130
2520
|
if (pkg.type === "module") detected.push("- Module system: ESM");
|
|
2131
2521
|
else detected.push("- Module system: CommonJS");
|
|
2132
|
-
if (
|
|
2133
|
-
else if (
|
|
2134
|
-
else if (
|
|
2135
|
-
else if (
|
|
2522
|
+
if (fs18.existsSync(path17.join(cwd, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
|
|
2523
|
+
else if (fs18.existsSync(path17.join(cwd, "yarn.lock"))) detected.push("- Package manager: yarn");
|
|
2524
|
+
else if (fs18.existsSync(path17.join(cwd, "bun.lockb"))) detected.push("- Package manager: bun");
|
|
2525
|
+
else if (fs18.existsSync(path17.join(cwd, "package-lock.json"))) detected.push("- Package manager: npm");
|
|
2136
2526
|
} catch {
|
|
2137
2527
|
}
|
|
2138
2528
|
}
|
|
2139
2529
|
try {
|
|
2140
|
-
const entries =
|
|
2530
|
+
const entries = fs18.readdirSync(cwd, { withFileTypes: true });
|
|
2141
2531
|
const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
|
|
2142
2532
|
if (dirs.length > 0) detected.push(`- Top-level directories: ${dirs.join(", ")}`);
|
|
2143
2533
|
} catch {
|
|
2144
2534
|
}
|
|
2145
|
-
const srcDir =
|
|
2146
|
-
if (
|
|
2535
|
+
const srcDir = path17.join(cwd, "src");
|
|
2536
|
+
if (fs18.existsSync(srcDir)) {
|
|
2147
2537
|
try {
|
|
2148
|
-
const srcEntries =
|
|
2538
|
+
const srcEntries = fs18.readdirSync(srcDir, { withFileTypes: true });
|
|
2149
2539
|
const srcDirs = srcEntries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
2150
2540
|
if (srcDirs.length > 0) detected.push(`- src/ structure: ${srcDirs.join(", ")}`);
|
|
2151
2541
|
} catch {
|
|
2152
2542
|
}
|
|
2153
2543
|
}
|
|
2154
2544
|
const configs = [];
|
|
2155
|
-
if (
|
|
2156
|
-
if (
|
|
2157
|
-
if (
|
|
2158
|
-
if (
|
|
2545
|
+
if (fs18.existsSync(path17.join(cwd, "tsconfig.json"))) configs.push("tsconfig.json");
|
|
2546
|
+
if (fs18.existsSync(path17.join(cwd, "docker-compose.yml")) || fs18.existsSync(path17.join(cwd, "docker-compose.yaml"))) configs.push("docker-compose");
|
|
2547
|
+
if (fs18.existsSync(path17.join(cwd, "Dockerfile"))) configs.push("Dockerfile");
|
|
2548
|
+
if (fs18.existsSync(path17.join(cwd, ".env")) || fs18.existsSync(path17.join(cwd, ".env.local"))) configs.push(".env");
|
|
2159
2549
|
if (configs.length > 0) detected.push(`- Config files: ${configs.join(", ")}`);
|
|
2160
2550
|
return detected;
|
|
2161
2551
|
}
|
|
2162
2552
|
function detectBasicConfig(cwd) {
|
|
2163
2553
|
let pm = "pnpm";
|
|
2164
|
-
if (
|
|
2165
|
-
else if (
|
|
2166
|
-
else if (!
|
|
2554
|
+
if (fs18.existsSync(path17.join(cwd, "yarn.lock"))) pm = "yarn";
|
|
2555
|
+
else if (fs18.existsSync(path17.join(cwd, "bun.lockb"))) pm = "bun";
|
|
2556
|
+
else if (!fs18.existsSync(path17.join(cwd, "pnpm-lock.yaml")) && fs18.existsSync(path17.join(cwd, "package-lock.json"))) pm = "npm";
|
|
2167
2557
|
let defaultBranch = "main";
|
|
2168
2558
|
try {
|
|
2169
|
-
const ref =
|
|
2559
|
+
const ref = execFileSync11("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
|
|
2170
2560
|
encoding: "utf-8",
|
|
2171
2561
|
timeout: 5e3,
|
|
2172
2562
|
cwd,
|
|
@@ -2175,7 +2565,7 @@ function detectBasicConfig(cwd) {
|
|
|
2175
2565
|
defaultBranch = ref.replace("refs/remotes/origin/", "");
|
|
2176
2566
|
} catch {
|
|
2177
2567
|
try {
|
|
2178
|
-
|
|
2568
|
+
execFileSync11("git", ["rev-parse", "--verify", "origin/dev"], {
|
|
2179
2569
|
encoding: "utf-8",
|
|
2180
2570
|
timeout: 5e3,
|
|
2181
2571
|
cwd,
|
|
@@ -2188,7 +2578,7 @@ function detectBasicConfig(cwd) {
|
|
|
2188
2578
|
let owner = "";
|
|
2189
2579
|
let repo = "";
|
|
2190
2580
|
try {
|
|
2191
|
-
const remote =
|
|
2581
|
+
const remote = execFileSync11("git", ["remote", "get-url", "origin"], {
|
|
2192
2582
|
encoding: "utf-8",
|
|
2193
2583
|
timeout: 5e3,
|
|
2194
2584
|
cwd,
|
|
@@ -2207,9 +2597,9 @@ function smartInit(cwd) {
|
|
|
2207
2597
|
const basic = detectBasicConfig(cwd);
|
|
2208
2598
|
let context = "";
|
|
2209
2599
|
const readIfExists = (rel, maxChars = 3e3) => {
|
|
2210
|
-
const p =
|
|
2211
|
-
if (
|
|
2212
|
-
const content =
|
|
2600
|
+
const p = path17.join(cwd, rel);
|
|
2601
|
+
if (fs18.existsSync(p)) {
|
|
2602
|
+
const content = fs18.readFileSync(p, "utf-8");
|
|
2213
2603
|
return content.slice(0, maxChars);
|
|
2214
2604
|
}
|
|
2215
2605
|
return null;
|
|
@@ -2235,14 +2625,14 @@ ${claudeMd}
|
|
|
2235
2625
|
|
|
2236
2626
|
`;
|
|
2237
2627
|
try {
|
|
2238
|
-
const topDirs =
|
|
2628
|
+
const topDirs = fs18.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
|
|
2239
2629
|
context += `## Top-level directories
|
|
2240
2630
|
${topDirs.join(", ")}
|
|
2241
2631
|
|
|
2242
2632
|
`;
|
|
2243
|
-
const srcDir =
|
|
2244
|
-
if (
|
|
2245
|
-
const srcDirs =
|
|
2633
|
+
const srcDir = path17.join(cwd, "src");
|
|
2634
|
+
if (fs18.existsSync(srcDir)) {
|
|
2635
|
+
const srcDirs = fs18.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
2246
2636
|
context += `## src/ subdirectories
|
|
2247
2637
|
${srcDirs.join(", ")}
|
|
2248
2638
|
|
|
@@ -2252,7 +2642,7 @@ ${srcDirs.join(", ")}
|
|
|
2252
2642
|
}
|
|
2253
2643
|
const existingFiles = [];
|
|
2254
2644
|
for (const f of [".env.example", "CLAUDE.md", ".ai-docs", "vitest.config.ts", "vitest.config.mts", "jest.config.ts", "playwright.config.ts", ".eslintrc.js", "eslint.config.mjs", ".prettierrc"]) {
|
|
2255
|
-
if (
|
|
2645
|
+
if (fs18.existsSync(path17.join(cwd, f))) existingFiles.push(f);
|
|
2256
2646
|
}
|
|
2257
2647
|
if (existingFiles.length) context += `## Config files present
|
|
2258
2648
|
${existingFiles.join(", ")}
|
|
@@ -2311,7 +2701,7 @@ Output ONLY valid JSON. No markdown fences. No explanation before or after.
|
|
|
2311
2701
|
${context}`;
|
|
2312
2702
|
console.log(" \u23F3 Analyzing project with Claude Code...");
|
|
2313
2703
|
try {
|
|
2314
|
-
const output =
|
|
2704
|
+
const output = execFileSync11("claude", [
|
|
2315
2705
|
"--print",
|
|
2316
2706
|
"--model",
|
|
2317
2707
|
"haiku",
|
|
@@ -2358,7 +2748,7 @@ ${context}`;
|
|
|
2358
2748
|
function validateQualityCommands(cwd, config, pm) {
|
|
2359
2749
|
let scripts = {};
|
|
2360
2750
|
try {
|
|
2361
|
-
const pkg = JSON.parse(
|
|
2751
|
+
const pkg = JSON.parse(fs18.readFileSync(path17.join(cwd, "package.json"), "utf-8"));
|
|
2362
2752
|
scripts = pkg.scripts ?? {};
|
|
2363
2753
|
} catch {
|
|
2364
2754
|
return;
|
|
@@ -2392,7 +2782,7 @@ function validateQualityCommands(cwd, config, pm) {
|
|
|
2392
2782
|
function buildFallbackConfig(cwd, basic) {
|
|
2393
2783
|
const pkg = (() => {
|
|
2394
2784
|
try {
|
|
2395
|
-
return JSON.parse(
|
|
2785
|
+
return JSON.parse(fs18.readFileSync(path17.join(cwd, "package.json"), "utf-8"));
|
|
2396
2786
|
} catch {
|
|
2397
2787
|
return {};
|
|
2398
2788
|
}
|
|
@@ -2432,34 +2822,34 @@ function initCommand(opts) {
|
|
|
2432
2822
|
console.log(`Project: ${cwd}
|
|
2433
2823
|
`);
|
|
2434
2824
|
console.log("\u2500\u2500 Files \u2500\u2500");
|
|
2435
|
-
const templatesDir =
|
|
2436
|
-
const workflowSrc =
|
|
2437
|
-
const workflowDest =
|
|
2438
|
-
if (!
|
|
2825
|
+
const templatesDir = path17.join(PKG_ROOT, "templates");
|
|
2826
|
+
const workflowSrc = path17.join(templatesDir, "kody.yml");
|
|
2827
|
+
const workflowDest = path17.join(cwd, ".github", "workflows", "kody.yml");
|
|
2828
|
+
if (!fs18.existsSync(workflowSrc)) {
|
|
2439
2829
|
console.error(" \u2717 Template kody.yml not found in package");
|
|
2440
2830
|
process.exit(1);
|
|
2441
2831
|
}
|
|
2442
|
-
if (
|
|
2832
|
+
if (fs18.existsSync(workflowDest) && !opts.force) {
|
|
2443
2833
|
console.log(" \u25CB .github/workflows/kody.yml (exists, use --force to overwrite)");
|
|
2444
2834
|
} else {
|
|
2445
|
-
|
|
2446
|
-
|
|
2835
|
+
fs18.mkdirSync(path17.dirname(workflowDest), { recursive: true });
|
|
2836
|
+
fs18.copyFileSync(workflowSrc, workflowDest);
|
|
2447
2837
|
console.log(" \u2713 .github/workflows/kody.yml");
|
|
2448
2838
|
}
|
|
2449
|
-
const configDest =
|
|
2839
|
+
const configDest = path17.join(cwd, "kody.config.json");
|
|
2450
2840
|
let smartResult = null;
|
|
2451
|
-
if (!
|
|
2841
|
+
if (!fs18.existsSync(configDest) || opts.force) {
|
|
2452
2842
|
smartResult = smartInit(cwd);
|
|
2453
|
-
|
|
2843
|
+
fs18.writeFileSync(configDest, JSON.stringify(smartResult.config, null, 2) + "\n");
|
|
2454
2844
|
console.log(" \u2713 kody.config.json (auto-configured)");
|
|
2455
2845
|
} else {
|
|
2456
2846
|
console.log(" \u25CB kody.config.json (exists)");
|
|
2457
2847
|
}
|
|
2458
|
-
const gitignorePath =
|
|
2459
|
-
if (
|
|
2460
|
-
const content =
|
|
2848
|
+
const gitignorePath = path17.join(cwd, ".gitignore");
|
|
2849
|
+
if (fs18.existsSync(gitignorePath)) {
|
|
2850
|
+
const content = fs18.readFileSync(gitignorePath, "utf-8");
|
|
2461
2851
|
if (!content.includes(".tasks/")) {
|
|
2462
|
-
|
|
2852
|
+
fs18.appendFileSync(gitignorePath, "\n.tasks/\n");
|
|
2463
2853
|
console.log(" \u2713 .gitignore (added .tasks/)");
|
|
2464
2854
|
} else {
|
|
2465
2855
|
console.log(" \u25CB .gitignore (.tasks/ already present)");
|
|
@@ -2472,7 +2862,7 @@ function initCommand(opts) {
|
|
|
2472
2862
|
checkCommand2("git", ["--version"], "Install git"),
|
|
2473
2863
|
checkCommand2("node", ["--version"], "Install Node.js >= 22"),
|
|
2474
2864
|
checkCommand2("pnpm", ["--version"], "Install: npm i -g pnpm"),
|
|
2475
|
-
checkFile(
|
|
2865
|
+
checkFile(path17.join(cwd, "package.json"), "package.json", "Run: pnpm init")
|
|
2476
2866
|
];
|
|
2477
2867
|
for (const c of checks) {
|
|
2478
2868
|
if (c.ok) {
|
|
@@ -2517,7 +2907,7 @@ function initCommand(opts) {
|
|
|
2517
2907
|
console.log("\n\u2500\u2500 Labels \u2500\u2500");
|
|
2518
2908
|
for (const label of labels) {
|
|
2519
2909
|
try {
|
|
2520
|
-
|
|
2910
|
+
execFileSync11("gh", [
|
|
2521
2911
|
"label",
|
|
2522
2912
|
"create",
|
|
2523
2913
|
label.name,
|
|
@@ -2536,7 +2926,7 @@ function initCommand(opts) {
|
|
|
2536
2926
|
console.log(` \u2713 ${label.name}`);
|
|
2537
2927
|
} catch {
|
|
2538
2928
|
try {
|
|
2539
|
-
|
|
2929
|
+
execFileSync11("gh", ["label", "list", "--repo", repoSlug, "--search", label.name], {
|
|
2540
2930
|
encoding: "utf-8",
|
|
2541
2931
|
timeout: 1e4,
|
|
2542
2932
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -2549,9 +2939,9 @@ function initCommand(opts) {
|
|
|
2549
2939
|
}
|
|
2550
2940
|
}
|
|
2551
2941
|
console.log("\n\u2500\u2500 Config \u2500\u2500");
|
|
2552
|
-
if (
|
|
2942
|
+
if (fs18.existsSync(configDest)) {
|
|
2553
2943
|
try {
|
|
2554
|
-
const config = JSON.parse(
|
|
2944
|
+
const config = JSON.parse(fs18.readFileSync(configDest, "utf-8"));
|
|
2555
2945
|
const configChecks = [];
|
|
2556
2946
|
if (config.github?.owner && config.github?.repo) {
|
|
2557
2947
|
configChecks.push({ name: "github.owner/repo", ok: true, detail: `${config.github.owner}/${config.github.repo}` });
|
|
@@ -2578,21 +2968,21 @@ function initCommand(opts) {
|
|
|
2578
2968
|
}
|
|
2579
2969
|
}
|
|
2580
2970
|
console.log("\n\u2500\u2500 Project Memory \u2500\u2500");
|
|
2581
|
-
const memoryDir =
|
|
2582
|
-
|
|
2583
|
-
const archPath =
|
|
2584
|
-
const conventionsPath =
|
|
2585
|
-
if (
|
|
2971
|
+
const memoryDir = path17.join(cwd, ".kody", "memory");
|
|
2972
|
+
fs18.mkdirSync(memoryDir, { recursive: true });
|
|
2973
|
+
const archPath = path17.join(memoryDir, "architecture.md");
|
|
2974
|
+
const conventionsPath = path17.join(memoryDir, "conventions.md");
|
|
2975
|
+
if (fs18.existsSync(archPath) && !opts.force) {
|
|
2586
2976
|
console.log(" \u25CB .kody/memory/architecture.md (exists, use --force to regenerate)");
|
|
2587
2977
|
} else if (smartResult?.architecture) {
|
|
2588
|
-
|
|
2978
|
+
fs18.writeFileSync(archPath, smartResult.architecture);
|
|
2589
2979
|
const lineCount = smartResult.architecture.split("\n").length;
|
|
2590
2980
|
console.log(` \u2713 .kody/memory/architecture.md (${lineCount} lines, LLM-generated)`);
|
|
2591
2981
|
} else {
|
|
2592
2982
|
const archItems = detectArchitecture(cwd);
|
|
2593
2983
|
if (archItems.length > 0) {
|
|
2594
2984
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2595
|
-
|
|
2985
|
+
fs18.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
|
|
2596
2986
|
|
|
2597
2987
|
## Overview
|
|
2598
2988
|
${archItems.join("\n")}
|
|
@@ -2602,14 +2992,14 @@ ${archItems.join("\n")}
|
|
|
2602
2992
|
console.log(" \u25CB No architecture detected");
|
|
2603
2993
|
}
|
|
2604
2994
|
}
|
|
2605
|
-
if (
|
|
2995
|
+
if (fs18.existsSync(conventionsPath) && !opts.force) {
|
|
2606
2996
|
console.log(" \u25CB .kody/memory/conventions.md (exists, use --force to regenerate)");
|
|
2607
2997
|
} else if (smartResult?.conventions) {
|
|
2608
|
-
|
|
2998
|
+
fs18.writeFileSync(conventionsPath, smartResult.conventions);
|
|
2609
2999
|
const lineCount = smartResult.conventions.split("\n").length;
|
|
2610
3000
|
console.log(` \u2713 .kody/memory/conventions.md (${lineCount} lines, LLM-generated)`);
|
|
2611
3001
|
} else {
|
|
2612
|
-
|
|
3002
|
+
fs18.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
|
|
2613
3003
|
console.log(" \u2713 .kody/memory/conventions.md (seed)");
|
|
2614
3004
|
}
|
|
2615
3005
|
const allChecks = [...checks, ghAuth, ghRepo];
|