@kody-ade/kody-engine-lite 0.1.20 → 0.1.22

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