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

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