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

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