@kody-ade/kody-engine-lite 0.1.104 → 0.1.105

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/cli.js +759 -1037
  3. package/package.json +1 -1
  4. package/prompts/autofix.md +20 -33
  5. package/prompts/review-fix.md +8 -12
  6. package/prompts/taskify.md +2 -19
  7. package/templates/kody.yml +26 -17
  8. package/dist/agent-runner.d.ts +0 -4
  9. package/dist/agent-runner.js +0 -122
  10. package/dist/ci/parse-inputs.d.ts +0 -6
  11. package/dist/ci/parse-inputs.js +0 -76
  12. package/dist/ci/parse-safety.d.ts +0 -6
  13. package/dist/ci/parse-safety.js +0 -22
  14. package/dist/cli/args.d.ts +0 -13
  15. package/dist/cli/args.js +0 -42
  16. package/dist/cli/litellm.d.ts +0 -2
  17. package/dist/cli/litellm.js +0 -85
  18. package/dist/cli/task-resolution.d.ts +0 -2
  19. package/dist/cli/task-resolution.js +0 -41
  20. package/dist/config.d.ts +0 -49
  21. package/dist/config.js +0 -72
  22. package/dist/context.d.ts +0 -4
  23. package/dist/context.js +0 -83
  24. package/dist/definitions.d.ts +0 -3
  25. package/dist/definitions.js +0 -59
  26. package/dist/entry.d.ts +0 -1
  27. package/dist/entry.js +0 -236
  28. package/dist/git-utils.d.ts +0 -13
  29. package/dist/git-utils.js +0 -174
  30. package/dist/github-api.d.ts +0 -14
  31. package/dist/github-api.js +0 -114
  32. package/dist/kody-utils.d.ts +0 -1
  33. package/dist/kody-utils.js +0 -9
  34. package/dist/learning/auto-learn.d.ts +0 -2
  35. package/dist/learning/auto-learn.js +0 -169
  36. package/dist/logger.d.ts +0 -14
  37. package/dist/logger.js +0 -51
  38. package/dist/memory.d.ts +0 -1
  39. package/dist/memory.js +0 -20
  40. package/dist/observer.d.ts +0 -9
  41. package/dist/observer.js +0 -80
  42. package/dist/pipeline/complexity.d.ts +0 -3
  43. package/dist/pipeline/complexity.js +0 -12
  44. package/dist/pipeline/executor-registry.d.ts +0 -3
  45. package/dist/pipeline/executor-registry.js +0 -20
  46. package/dist/pipeline/hooks.d.ts +0 -17
  47. package/dist/pipeline/hooks.js +0 -110
  48. package/dist/pipeline/questions.d.ts +0 -2
  49. package/dist/pipeline/questions.js +0 -44
  50. package/dist/pipeline/runner-selection.d.ts +0 -2
  51. package/dist/pipeline/runner-selection.js +0 -13
  52. package/dist/pipeline/state.d.ts +0 -4
  53. package/dist/pipeline/state.js +0 -37
  54. package/dist/pipeline.d.ts +0 -3
  55. package/dist/pipeline.js +0 -213
  56. package/dist/preflight.d.ts +0 -1
  57. package/dist/preflight.js +0 -69
  58. package/dist/retrospective.d.ts +0 -26
  59. package/dist/retrospective.js +0 -211
  60. package/dist/stages/agent.d.ts +0 -2
  61. package/dist/stages/agent.js +0 -94
  62. package/dist/stages/gate.d.ts +0 -2
  63. package/dist/stages/gate.js +0 -32
  64. package/dist/stages/review.d.ts +0 -2
  65. package/dist/stages/review.js +0 -32
  66. package/dist/stages/ship.d.ts +0 -3
  67. package/dist/stages/ship.js +0 -154
  68. package/dist/stages/verify.d.ts +0 -2
  69. package/dist/stages/verify.js +0 -94
  70. package/dist/types.d.ts +0 -61
  71. package/dist/types.js +0 -1
  72. package/dist/validators.d.ts +0 -8
  73. package/dist/validators.js +0 -42
  74. package/dist/verify-runner.d.ts +0 -11
  75. package/dist/verify-runner.js +0 -110
package/dist/bin/cli.js CHANGED
@@ -9,63 +9,8 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
- // src/bin/architecture-detection.ts
13
- import * as fs4 from "fs";
14
- import * as path3 from "path";
15
- function detectArchitectureBasic(cwd) {
16
- const detected = [];
17
- const pkgPath = path3.join(cwd, "package.json");
18
- if (fs4.existsSync(pkgPath)) {
19
- try {
20
- const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
21
- const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
22
- if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
23
- else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
24
- else if (allDeps.express) detected.push(`- Framework: Express ${allDeps.express}`);
25
- else if (allDeps.fastify) detected.push(`- Framework: Fastify ${allDeps.fastify}`);
26
- else if (allDeps.hono) detected.push(`- Framework: Hono ${allDeps.hono}`);
27
- if (allDeps.typescript) detected.push(`- Language: TypeScript ${allDeps.typescript}`);
28
- if (allDeps.vitest) detected.push(`- Testing: vitest ${allDeps.vitest}`);
29
- else if (allDeps.jest) detected.push(`- Testing: jest ${allDeps.jest}`);
30
- if (allDeps.eslint) detected.push(`- Linting: eslint ${allDeps.eslint}`);
31
- if (allDeps.prettier) detected.push(`- Formatting: prettier ${allDeps.prettier}`);
32
- if (allDeps.prisma || allDeps["@prisma/client"]) detected.push("- ORM: Prisma");
33
- if (allDeps["drizzle-orm"]) detected.push("- ORM: Drizzle");
34
- if (allDeps.payload || allDeps["@payloadcms/next"]) detected.push("- CMS: Payload CMS");
35
- if (allDeps.tailwindcss) detected.push(`- CSS: Tailwind CSS ${allDeps.tailwindcss}`);
36
- if (fs4.existsSync(path3.join(cwd, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
37
- else if (fs4.existsSync(path3.join(cwd, "yarn.lock"))) detected.push("- Package manager: yarn");
38
- else if (fs4.existsSync(path3.join(cwd, "bun.lockb"))) detected.push("- Package manager: bun");
39
- else if (fs4.existsSync(path3.join(cwd, "package-lock.json"))) detected.push("- Package manager: npm");
40
- if (pkg.type === "module") detected.push("- Module system: ESM");
41
- else detected.push("- Module system: CommonJS");
42
- if (allDeps.pg || allDeps.postgres) detected.push("- Database: PostgreSQL");
43
- } catch {
44
- }
45
- }
46
- try {
47
- const topDirs = fs4.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
48
- if (topDirs.length > 0) detected.push(`- Top-level directories: ${topDirs.join(", ")}`);
49
- } catch {
50
- }
51
- const srcDir = path3.join(cwd, "src");
52
- if (fs4.existsSync(srcDir)) {
53
- try {
54
- const srcDirs = fs4.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
55
- if (srcDirs.length > 0) detected.push(`- src/ structure: ${srcDirs.join(", ")}`);
56
- } catch {
57
- }
58
- }
59
- return detected;
60
- }
61
- var init_architecture_detection = __esm({
62
- "src/bin/architecture-detection.ts"() {
63
- "use strict";
64
- }
65
- });
66
-
67
12
  // src/agent-runner.ts
68
- import { spawn, execFileSync as execFileSync6 } from "child_process";
13
+ import { spawn, execFileSync } from "child_process";
69
14
  function writeStdin(child, prompt) {
70
15
  return new Promise((resolve4, reject) => {
71
16
  if (!child.stdin) {
@@ -137,9 +82,9 @@ async function runSubprocess(command2, args2, prompt, timeout, options) {
137
82
  ${errDetail}`
138
83
  };
139
84
  }
140
- function checkCommand2(command2, args2) {
85
+ function checkCommand(command2, args2) {
141
86
  try {
142
- execFileSync6(command2, args2, { timeout: 1e4, stdio: "pipe" });
87
+ execFileSync(command2, args2, { timeout: 1e4, stdio: "pipe" });
143
88
  return true;
144
89
  } catch {
145
90
  return false;
@@ -169,7 +114,7 @@ function createClaudeCodeRunner() {
169
114
  return runSubprocess("claude", args2, prompt, timeout, options);
170
115
  },
171
116
  async healthCheck() {
172
- return checkCommand2("claude", ["--version"]);
117
+ return checkCommand("claude", ["--version"]);
173
118
  }
174
119
  };
175
120
  }
@@ -326,77 +271,9 @@ var init_logger = __esm({
326
271
  }
327
272
  });
328
273
 
329
- // src/validators.ts
330
- function stripFences(content) {
331
- return content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
332
- }
333
- function parseJsonSafe(raw, requiredFields) {
334
- let parsed;
335
- try {
336
- parsed = JSON.parse(raw);
337
- } catch (err) {
338
- return { ok: false, error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}` };
339
- }
340
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
341
- return { ok: false, error: `Expected JSON object, got ${Array.isArray(parsed) ? "array" : typeof parsed}` };
342
- }
343
- if (requiredFields) {
344
- for (const field of requiredFields) {
345
- if (!(field in parsed)) {
346
- return { ok: false, error: `Missing required field: ${field}` };
347
- }
348
- }
349
- }
350
- return { ok: true, data: parsed };
351
- }
352
- function validateTaskJson(content) {
353
- try {
354
- const parsed = JSON.parse(stripFences(content));
355
- for (const field of REQUIRED_TASK_FIELDS) {
356
- if (!(field in parsed)) {
357
- return { valid: false, error: `Missing field: ${field}` };
358
- }
359
- }
360
- return { valid: true };
361
- } catch (err) {
362
- return {
363
- valid: false,
364
- error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`
365
- };
366
- }
367
- }
368
- function validatePlanMd(content) {
369
- if (content.length < 10) {
370
- return { valid: false, error: "Plan is too short (< 10 chars)" };
371
- }
372
- if (!/^##\s+\w+/m.test(content)) {
373
- return { valid: false, error: "Plan has no markdown h2 sections" };
374
- }
375
- return { valid: true };
376
- }
377
- function validateReviewMd(content) {
378
- if (/pass/i.test(content) || /fail/i.test(content)) {
379
- return { valid: true };
380
- }
381
- return { valid: false, error: "Review must contain 'pass' or 'fail'" };
382
- }
383
- var REQUIRED_TASK_FIELDS;
384
- var init_validators = __esm({
385
- "src/validators.ts"() {
386
- "use strict";
387
- REQUIRED_TASK_FIELDS = [
388
- "task_type",
389
- "title",
390
- "description",
391
- "scope",
392
- "risk_level"
393
- ];
394
- }
395
- });
396
-
397
274
  // src/config.ts
398
- import * as fs8 from "fs";
399
- import * as path7 from "path";
275
+ import * as fs from "fs";
276
+ import * as path from "path";
400
277
  function needsLitellmProxy(config) {
401
278
  return !!(config.agent.provider && config.agent.provider !== "anthropic");
402
279
  }
@@ -413,16 +290,10 @@ function setConfigDir(dir) {
413
290
  }
414
291
  function getProjectConfig() {
415
292
  if (_config) return _config;
416
- const configPath = path7.join(_configDir ?? process.cwd(), "kody.config.json");
417
- if (fs8.existsSync(configPath)) {
293
+ const configPath = path.join(_configDir ?? process.cwd(), "kody.config.json");
294
+ if (fs.existsSync(configPath)) {
418
295
  try {
419
- const result = parseJsonSafe(fs8.readFileSync(configPath, "utf-8"));
420
- if (!result.ok) {
421
- logger.warn(`kody.config.json: ${result.error} \u2014 using defaults`);
422
- _config = { ...DEFAULT_CONFIG };
423
- return _config;
424
- }
425
- const raw = result.data;
296
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
426
297
  _config = {
427
298
  quality: { ...DEFAULT_CONFIG.quality, ...raw.quality },
428
299
  git: { ...DEFAULT_CONFIG.git, ...raw.git },
@@ -434,13 +305,7 @@ function getProjectConfig() {
434
305
  },
435
306
  timeouts: raw.timeouts ?? void 0,
436
307
  contextTiers: raw.contextTiers ? { ...DEFAULT_CONFIG.contextTiers, ...raw.contextTiers } : DEFAULT_CONFIG.contextTiers,
437
- mcp: raw.mcp ? {
438
- servers: {},
439
- stages: ["build", "verify", "review", "review-fix"],
440
- ...raw.mcp,
441
- // Auto-enable when devServer is configured (user can still set enabled: false to override)
442
- enabled: raw.mcp.enabled ?? !!raw.mcp.devServer
443
- } : void 0
308
+ mcp: raw.mcp ? { enabled: false, servers: {}, stages: ["build", "verify", "review", "review-fix"], ...raw.mcp } : void 0
444
309
  };
445
310
  } catch {
446
311
  logger.warn("kody.config.json is invalid JSON \u2014 using defaults");
@@ -456,7 +321,6 @@ var init_config = __esm({
456
321
  "src/config.ts"() {
457
322
  "use strict";
458
323
  init_logger();
459
- init_validators();
460
324
  DEFAULT_CONFIG = {
461
325
  quality: {
462
326
  typecheck: "pnpm -s tsc --noEmit",
@@ -490,7 +354,7 @@ var init_config = __esm({
490
354
  });
491
355
 
492
356
  // src/git-utils.ts
493
- import { execFileSync as execFileSync7 } from "child_process";
357
+ import { execFileSync as execFileSync2 } from "child_process";
494
358
  function getHookSafeEnv() {
495
359
  if (!_hookSafeEnv) {
496
360
  _hookSafeEnv = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
@@ -498,7 +362,7 @@ function getHookSafeEnv() {
498
362
  return _hookSafeEnv;
499
363
  }
500
364
  function git(args2, options) {
501
- return execFileSync7("git", args2, {
365
+ return execFileSync2("git", args2, {
502
366
  encoding: "utf-8",
503
367
  timeout: options?.timeout ?? 3e4,
504
368
  cwd: options?.cwd,
@@ -553,9 +417,8 @@ function ensureFeatureBranch(issueNumber, title, cwd) {
553
417
  }
554
418
  try {
555
419
  git(["fetch", "origin"], { cwd, timeout: 3e4 });
556
- } catch (err) {
557
- const msg = err instanceof Error ? err.message : String(err);
558
- logger.warn(` Failed to fetch origin: ${msg}`);
420
+ } catch {
421
+ logger.warn(" Failed to fetch origin");
559
422
  }
560
423
  try {
561
424
  git(["rev-parse", "--verify", `origin/${branchName}`], { cwd });
@@ -587,9 +450,8 @@ function syncWithDefault(cwd, branch) {
587
450
  if (current === defaultBranch) return;
588
451
  try {
589
452
  git(["fetch", "origin", defaultBranch], { cwd, timeout: 3e4 });
590
- } catch (err) {
591
- const msg = err instanceof Error ? err.message : String(err);
592
- logger.warn(` Failed to fetch latest from origin: ${msg}`);
453
+ } catch {
454
+ logger.warn(" Failed to fetch latest from origin");
593
455
  return;
594
456
  }
595
457
  try {
@@ -598,8 +460,7 @@ function syncWithDefault(cwd, branch) {
598
460
  } catch {
599
461
  try {
600
462
  git(["merge", "--abort"], { cwd });
601
- } catch (abortErr) {
602
- logger.warn(` Failed to abort merge: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}`);
463
+ } catch {
603
464
  }
604
465
  logger.warn(` Merge conflict with origin/${defaultBranch} \u2014 skipping sync`);
605
466
  }
@@ -610,9 +471,8 @@ function mergeDefault(cwd) {
610
471
  if (current === defaultBranch) return "clean";
611
472
  try {
612
473
  git(["fetch", "origin", defaultBranch], { cwd, timeout: 3e4 });
613
- } catch (err) {
614
- const msg = err instanceof Error ? err.message : String(err);
615
- logger.warn(` Failed to fetch latest from origin: ${msg}`);
474
+ } catch {
475
+ logger.warn(" Failed to fetch latest from origin");
616
476
  return "error";
617
477
  }
618
478
  try {
@@ -627,8 +487,7 @@ function mergeDefault(cwd) {
627
487
  }
628
488
  try {
629
489
  git(["merge", "--abort"], { cwd });
630
- } catch (abortErr) {
631
- logger.warn(` Failed to abort merge: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}`);
490
+ } catch {
632
491
  }
633
492
  return "error";
634
493
  }
@@ -673,21 +532,7 @@ var init_git_utils = __esm({
673
532
  });
674
533
 
675
534
  // src/github-api.ts
676
- import { execFileSync as execFileSync8 } from "child_process";
677
- function isGhExecError(err) {
678
- return typeof err === "object" && err !== null;
679
- }
680
- function ghErrorMessage(err) {
681
- if (isGhExecError(err)) {
682
- const stderr = err.stderr?.toString().trim();
683
- if (stderr) return stderr;
684
- }
685
- return err instanceof Error ? err.message : String(err);
686
- }
687
- function isNotFoundError(err) {
688
- const msg = ghErrorMessage(err).toLowerCase();
689
- return msg.includes("not found") || msg.includes("no pull requests") || msg.includes("could not resolve");
690
- }
535
+ import { execFileSync as execFileSync3 } from "child_process";
691
536
  function setGhCwd(cwd) {
692
537
  _ghCwd = cwd;
693
538
  }
@@ -697,7 +542,7 @@ function ghToken() {
697
542
  function gh(args2, options) {
698
543
  const token = ghToken();
699
544
  const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
700
- return execFileSync8("gh", args2, {
545
+ return execFileSync3("gh", args2, {
701
546
  encoding: "utf-8",
702
547
  timeout: API_TIMEOUT_MS,
703
548
  cwd: _ghCwd,
@@ -715,18 +560,9 @@ function getIssue(issueNumber) {
715
560
  "--json",
716
561
  "body,title"
717
562
  ]);
718
- const parsed = JSON.parse(output);
719
- if (!parsed || typeof parsed.title !== "string") {
720
- logger.warn(` Issue #${issueNumber}: unexpected response shape`);
721
- return null;
722
- }
723
- return { body: parsed.body ?? "", title: parsed.title };
563
+ return JSON.parse(output);
724
564
  } catch (err) {
725
- if (isNotFoundError(err)) {
726
- logger.info(` Issue #${issueNumber} not found`);
727
- } else {
728
- logger.error(` Failed to get issue #${issueNumber}: ${ghErrorMessage(err)}`);
729
- }
565
+ logger.error(` Failed to get issue #${issueNumber}: ${err}`);
730
566
  return null;
731
567
  }
732
568
  }
@@ -767,15 +603,8 @@ function getPRForBranch(branch) {
767
603
  "number,url"
768
604
  ]);
769
605
  const data = JSON.parse(output);
770
- if (typeof data.number !== "number" || typeof data.url !== "string") {
771
- logger.warn(` PR for branch ${branch}: unexpected response shape`);
772
- return null;
773
- }
774
606
  return { number: data.number, url: data.url };
775
- } catch (err) {
776
- if (!isNotFoundError(err)) {
777
- logger.warn(` Failed to check PR for branch ${branch}: ${ghErrorMessage(err)}`);
778
- }
607
+ } catch {
779
608
  return null;
780
609
  }
781
610
  }
@@ -813,7 +642,8 @@ function createPR(head, base, title, body) {
813
642
  logger.info(` PR created: ${url}`);
814
643
  return { number, url };
815
644
  } catch (err) {
816
- const reason = ghErrorMessage(err);
645
+ const stderr = err?.stderr?.toString().trim();
646
+ const reason = stderr || (err instanceof Error ? err.message : String(err));
817
647
  logger.error(` Failed to create PR: ${reason}`);
818
648
  return null;
819
649
  }
@@ -884,22 +714,9 @@ function getPRDetails(prNumber) {
884
714
  "title,body,headRefName,baseRefName"
885
715
  ]);
886
716
  const data = JSON.parse(output);
887
- if (typeof data.title !== "string" || typeof data.headRefName !== "string") {
888
- logger.warn(` PR #${prNumber}: unexpected response shape`);
889
- return null;
890
- }
891
- return {
892
- title: data.title,
893
- body: data.body ?? "",
894
- headBranch: data.headRefName,
895
- baseBranch: data.baseRefName ?? "main"
896
- };
717
+ return { title: data.title, body: data.body, headBranch: data.headRefName, baseBranch: data.baseRefName };
897
718
  } catch (err) {
898
- if (isNotFoundError(err)) {
899
- logger.info(` PR #${prNumber} not found`);
900
- } else {
901
- logger.error(` Failed to get PR #${prNumber}: ${ghErrorMessage(err)}`);
902
- }
719
+ logger.error(` Failed to get PR #${prNumber}: ${err}`);
903
720
  return null;
904
721
  }
905
722
  }
@@ -941,7 +758,7 @@ function getCIFailureLogs(runId, maxLength = 8e3) {
941
758
  const prefix = logsOutput.length > maxLength ? "...(earlier output truncated)\n" : "";
942
759
  return `${prefix}${truncated}`;
943
760
  } catch (err) {
944
- logger.warn(` Failed to get CI failure logs for run ${runId}: ${ghErrorMessage(err)}`);
761
+ logger.warn(` Failed to get CI failure logs for run ${runId}: ${err}`);
945
762
  return null;
946
763
  }
947
764
  }
@@ -963,7 +780,7 @@ function getLatestFailedRunForBranch(branch) {
963
780
  ]);
964
781
  return output.trim() || null;
965
782
  } catch (err) {
966
- logger.warn(` Failed to get latest failed run for branch ${branch}: ${ghErrorMessage(err)}`);
783
+ logger.warn(` Failed to get latest failed run for branch ${branch}: ${err}`);
967
784
  return null;
968
785
  }
969
786
  }
@@ -1044,7 +861,7 @@ var init_github_api = __esm({
1044
861
  "use strict";
1045
862
  init_logger();
1046
863
  API_TIMEOUT_MS = 3e4;
1047
- LIFECYCLE_LABELS = ["planning", "building", "review", "shipping", "done", "failed", "waiting", "low", "medium", "high"];
864
+ LIFECYCLE_LABELS = ["planning", "building", "review", "done", "failed", "waiting", "low", "medium", "high"];
1048
865
  KODY_MARKERS = [
1049
866
  "Kody Review",
1050
867
  "\u{1F916} Generated by Kody",
@@ -1059,22 +876,15 @@ var init_github_api = __esm({
1059
876
  });
1060
877
 
1061
878
  // src/pipeline/state.ts
1062
- import * as fs9 from "fs";
1063
- import * as path8 from "path";
879
+ import * as fs2 from "fs";
880
+ import * as path2 from "path";
1064
881
  function loadState(taskId, taskDir) {
1065
- const p = path8.join(taskDir, "status.json");
1066
- if (!fs9.existsSync(p)) return null;
882
+ const p = path2.join(taskDir, "status.json");
883
+ if (!fs2.existsSync(p)) return null;
1067
884
  try {
1068
- const result = parseJsonSafe(
1069
- fs9.readFileSync(p, "utf-8"),
1070
- ["taskId", "state", "stages", "createdAt", "updatedAt"]
1071
- );
1072
- if (!result.ok) {
1073
- logger.warn(` Corrupt status.json: ${result.error}`);
1074
- return null;
1075
- }
1076
- if (result.data.taskId !== taskId) return null;
1077
- return result.data;
885
+ const raw = JSON.parse(fs2.readFileSync(p, "utf-8"));
886
+ if (raw.taskId === taskId) return raw;
887
+ return null;
1078
888
  } catch {
1079
889
  return null;
1080
890
  }
@@ -1084,11 +894,11 @@ function writeState(state, taskDir) {
1084
894
  ...state,
1085
895
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1086
896
  };
1087
- const target = path8.join(taskDir, "status.json");
897
+ const target = path2.join(taskDir, "status.json");
1088
898
  const tmp = target + ".tmp";
1089
- fs9.writeFileSync(tmp, JSON.stringify(updated, null, 2));
1090
- fs9.renameSync(tmp, target);
1091
- return updated;
899
+ fs2.writeFileSync(tmp, JSON.stringify(updated, null, 2));
900
+ fs2.renameSync(tmp, target);
901
+ state.updatedAt = updated.updatedAt;
1092
902
  }
1093
903
  function initState(taskId) {
1094
904
  const stages = {};
@@ -1102,8 +912,6 @@ var init_state = __esm({
1102
912
  "src/pipeline/state.ts"() {
1103
913
  "use strict";
1104
914
  init_definitions();
1105
- init_validators();
1106
- init_logger();
1107
915
  }
1108
916
  });
1109
917
 
@@ -1128,16 +936,16 @@ var init_complexity = __esm({
1128
936
  });
1129
937
 
1130
938
  // src/memory.ts
1131
- import * as fs10 from "fs";
1132
- import * as path9 from "path";
939
+ import * as fs3 from "fs";
940
+ import * as path3 from "path";
1133
941
  function readProjectMemory(projectDir) {
1134
- const memoryDir = path9.join(projectDir, ".kody", "memory");
1135
- if (!fs10.existsSync(memoryDir)) return "";
1136
- const files = fs10.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
942
+ const memoryDir = path3.join(projectDir, ".kody", "memory");
943
+ if (!fs3.existsSync(memoryDir)) return "";
944
+ const files = fs3.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
1137
945
  if (files.length === 0) return "";
1138
946
  const sections = [];
1139
947
  for (const file of files) {
1140
- const content = fs10.readFileSync(path9.join(memoryDir, file), "utf-8").trim();
948
+ const content = fs3.readFileSync(path3.join(memoryDir, file), "utf-8").trim();
1141
949
  if (content) {
1142
950
  sections.push(`## ${file.replace(".md", "")}
1143
951
  ${content}`);
@@ -1156,11 +964,15 @@ var init_memory = __esm({
1156
964
  });
1157
965
 
1158
966
  // src/context-tiers.ts
1159
- import * as fs11 from "fs";
1160
- import * as path10 from "path";
967
+ import * as fs4 from "fs";
968
+ import * as path4 from "path";
969
+ import * as crypto2 from "crypto";
1161
970
  function estimateTokens(text) {
1162
971
  return Math.ceil(text.length / 4);
1163
972
  }
973
+ function contentHash(content) {
974
+ return crypto2.createHash("sha256").update(content).digest("hex").slice(0, 16);
975
+ }
1164
976
  function resolveStagePolicy(stageName, stageOverrides) {
1165
977
  const defaults = DEFAULT_STAGE_POLICIES[stageName] ?? DEFAULT_STAGE_POLICIES.build;
1166
978
  const overrides = stageOverrides?.[stageName];
@@ -1247,30 +1059,61 @@ function generateL1Json(content) {
1247
1059
  return content.slice(0, L1_MAX_CHARS);
1248
1060
  }
1249
1061
  }
1250
- function getTieredContent(filePath, content) {
1251
- const key = path10.basename(filePath);
1252
- return {
1062
+ function readCache(cacheDir) {
1063
+ const cachePath = path4.join(cacheDir, "tier-cache.json");
1064
+ if (!fs4.existsSync(cachePath)) return { version: 1, entries: {} };
1065
+ try {
1066
+ return JSON.parse(fs4.readFileSync(cachePath, "utf-8"));
1067
+ } catch {
1068
+ return { version: 1, entries: {} };
1069
+ }
1070
+ }
1071
+ function writeCache(cacheDir, cache) {
1072
+ fs4.mkdirSync(cacheDir, { recursive: true });
1073
+ fs4.writeFileSync(path4.join(cacheDir, "tier-cache.json"), JSON.stringify(cache, null, 2));
1074
+ }
1075
+ function getTieredContent(filePath, content, cacheDir) {
1076
+ const hash = contentHash(content);
1077
+ const key = path4.basename(filePath);
1078
+ const cache = readCache(cacheDir);
1079
+ if (cache.entries[key] && cache.entries[key].hash === hash) {
1080
+ return cache.entries[key];
1081
+ }
1082
+ const tiered = {
1253
1083
  source: filePath,
1084
+ hash,
1254
1085
  L0: generateL0(content, key),
1255
1086
  L1: generateL1(content, key),
1256
1087
  L2: content
1257
1088
  };
1089
+ cache.entries[key] = tiered;
1090
+ writeCache(cacheDir, cache);
1091
+ return tiered;
1092
+ }
1093
+ function invalidateCache(filePath, cacheDir) {
1094
+ const key = path4.basename(filePath);
1095
+ const cache = readCache(cacheDir);
1096
+ if (cache.entries[key]) {
1097
+ delete cache.entries[key];
1098
+ writeCache(cacheDir, cache);
1099
+ }
1258
1100
  }
1259
1101
  function selectTier(tiered, tier) {
1260
1102
  return tiered[tier];
1261
1103
  }
1262
1104
  function readProjectMemoryTiered(projectDir, tier) {
1263
- const memoryDir = path10.join(projectDir, ".kody", "memory");
1264
- if (!fs11.existsSync(memoryDir)) return "";
1265
- const files = fs11.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
1105
+ const memoryDir = path4.join(projectDir, ".kody", "memory");
1106
+ if (!fs4.existsSync(memoryDir)) return "";
1107
+ const files = fs4.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
1266
1108
  if (files.length === 0) return "";
1109
+ const cacheDir = path4.join(memoryDir, ".tiers");
1267
1110
  const tierLabel2 = tier === "L2" ? "full" : tier === "L1" ? "overview" : "abstract";
1268
1111
  const sections = [];
1269
1112
  for (const file of files) {
1270
- const filePath = path10.join(memoryDir, file);
1271
- const content = fs11.readFileSync(filePath, "utf-8").trim();
1113
+ const filePath = path4.join(memoryDir, file);
1114
+ const content = fs4.readFileSync(filePath, "utf-8").trim();
1272
1115
  if (!content) continue;
1273
- const tiered = getTieredContent(filePath, content);
1116
+ const tiered = getTieredContent(filePath, content, cacheDir);
1274
1117
  const selected = selectTier(tiered, tier);
1275
1118
  if (selected) {
1276
1119
  sections.push(`## ${file.replace(".md", "")}
@@ -1285,25 +1128,26 @@ ${sections.join("\n\n")}
1285
1128
  `;
1286
1129
  }
1287
1130
  function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
1131
+ const cacheDir = path4.join(taskDir, ".tiers");
1288
1132
  let context = `## Task Context
1289
1133
  `;
1290
1134
  context += `Task ID: ${taskId}
1291
1135
  `;
1292
1136
  context += `Task Directory: ${taskDir}
1293
1137
  `;
1294
- const taskMdPath = path10.join(taskDir, "task.md");
1295
- if (fs11.existsSync(taskMdPath)) {
1296
- const content = fs11.readFileSync(taskMdPath, "utf-8");
1297
- const selected = selectContent(taskMdPath, content, policy.taskDescription);
1138
+ const taskMdPath = path4.join(taskDir, "task.md");
1139
+ if (fs4.existsSync(taskMdPath)) {
1140
+ const content = fs4.readFileSync(taskMdPath, "utf-8");
1141
+ const selected = selectContent(taskMdPath, content, cacheDir, policy.taskDescription);
1298
1142
  const label = tierLabel("Task Description", policy.taskDescription);
1299
1143
  context += `
1300
1144
  ## ${label}
1301
1145
  ${selected}
1302
1146
  `;
1303
1147
  }
1304
- const taskJsonPath = path10.join(taskDir, "task.json");
1305
- if (fs11.existsSync(taskJsonPath)) {
1306
- const content = fs11.readFileSync(taskJsonPath, "utf-8");
1148
+ const taskJsonPath = path4.join(taskDir, "task.json");
1149
+ if (fs4.existsSync(taskJsonPath)) {
1150
+ const content = fs4.readFileSync(taskJsonPath, "utf-8");
1307
1151
  if (policy.taskClassification === "L2") {
1308
1152
  try {
1309
1153
  const taskDef = JSON.parse(content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, ""));
@@ -1319,7 +1163,7 @@ ${selected}
1319
1163
  } catch {
1320
1164
  }
1321
1165
  } else {
1322
- const selected = selectContent(taskJsonPath, content, policy.taskClassification);
1166
+ const selected = selectContent(taskJsonPath, content, cacheDir, policy.taskClassification);
1323
1167
  if (selected) {
1324
1168
  const label = tierLabel("Task Classification", policy.taskClassification);
1325
1169
  context += `
@@ -1329,30 +1173,30 @@ ${selected}
1329
1173
  }
1330
1174
  }
1331
1175
  }
1332
- const specPath = path10.join(taskDir, "spec.md");
1333
- if (fs11.existsSync(specPath)) {
1334
- const content = fs11.readFileSync(specPath, "utf-8");
1335
- const selected = selectContent(specPath, content, policy.spec);
1176
+ const specPath = path4.join(taskDir, "spec.md");
1177
+ if (fs4.existsSync(specPath)) {
1178
+ const content = fs4.readFileSync(specPath, "utf-8");
1179
+ const selected = selectContent(specPath, content, cacheDir, policy.spec);
1336
1180
  const label = tierLabel("Spec", policy.spec);
1337
1181
  context += `
1338
1182
  ## ${label}
1339
1183
  ${selected}
1340
1184
  `;
1341
1185
  }
1342
- const planPath = path10.join(taskDir, "plan.md");
1343
- if (fs11.existsSync(planPath)) {
1344
- const content = fs11.readFileSync(planPath, "utf-8");
1345
- const selected = selectContent(planPath, content, policy.plan);
1186
+ const planPath = path4.join(taskDir, "plan.md");
1187
+ if (fs4.existsSync(planPath)) {
1188
+ const content = fs4.readFileSync(planPath, "utf-8");
1189
+ const selected = selectContent(planPath, content, cacheDir, policy.plan);
1346
1190
  const label = tierLabel("Plan", policy.plan);
1347
1191
  context += `
1348
1192
  ## ${label}
1349
1193
  ${selected}
1350
1194
  `;
1351
1195
  }
1352
- const contextMdPath = path10.join(taskDir, "context.md");
1353
- if (fs11.existsSync(contextMdPath)) {
1354
- const content = fs11.readFileSync(contextMdPath, "utf-8");
1355
- const selected = selectContent(contextMdPath, content, policy.accumulatedContext);
1196
+ const contextMdPath = path4.join(taskDir, "context.md");
1197
+ if (fs4.existsSync(contextMdPath)) {
1198
+ const content = fs4.readFileSync(contextMdPath, "utf-8");
1199
+ const selected = selectContent(contextMdPath, content, cacheDir, policy.accumulatedContext);
1356
1200
  const label = tierLabel("Previous Stage Context", policy.accumulatedContext);
1357
1201
  context += `
1358
1202
  ## ${label}
@@ -1367,9 +1211,9 @@ ${feedback}
1367
1211
  }
1368
1212
  return prompt.replace("{{TASK_CONTEXT}}", context);
1369
1213
  }
1370
- function selectContent(filePath, content, tier) {
1214
+ function selectContent(filePath, content, cacheDir, tier) {
1371
1215
  if (tier === "L2") return content;
1372
- const tiered = getTieredContent(filePath, content);
1216
+ const tiered = getTieredContent(filePath, content, cacheDir);
1373
1217
  return selectTier(tiered, tier);
1374
1218
  }
1375
1219
  function tierLabel(sectionName, tier) {
@@ -1437,20 +1281,6 @@ var init_context_tiers = __esm({
1437
1281
  });
1438
1282
 
1439
1283
  // src/mcp-config.ts
1440
- function withPlaywrightIfNeeded(mcpConfig, hasUI) {
1441
- if (!mcpConfig?.enabled || !hasUI) return mcpConfig;
1442
- const hasPlaywright = Object.keys(mcpConfig.servers).some(
1443
- (name) => name.toLowerCase().includes("playwright")
1444
- );
1445
- if (hasPlaywright) return mcpConfig;
1446
- return {
1447
- ...mcpConfig,
1448
- servers: {
1449
- ...mcpConfig.servers,
1450
- playwright: PLAYWRIGHT_SERVER
1451
- }
1452
- };
1453
- }
1454
1284
  function buildMcpConfigJson(mcpConfig) {
1455
1285
  if (!mcpConfig?.enabled) return void 0;
1456
1286
  if (Object.keys(mcpConfig.servers).length === 0) return void 0;
@@ -1467,40 +1297,37 @@ function buildMcpConfigJson(mcpConfig) {
1467
1297
  }
1468
1298
  function isMcpEnabledForStage(stageName, mcpConfig) {
1469
1299
  if (!mcpConfig?.enabled) return false;
1300
+ if (Object.keys(mcpConfig.servers).length === 0) return false;
1470
1301
  const allowedStages = mcpConfig.stages ?? DEFAULT_MCP_STAGES;
1471
1302
  return allowedStages.includes(stageName);
1472
1303
  }
1473
- var DEFAULT_MCP_STAGES, PLAYWRIGHT_SERVER;
1304
+ var DEFAULT_MCP_STAGES;
1474
1305
  var init_mcp_config = __esm({
1475
1306
  "src/mcp-config.ts"() {
1476
1307
  "use strict";
1477
1308
  DEFAULT_MCP_STAGES = ["build", "verify", "review", "review-fix"];
1478
- PLAYWRIGHT_SERVER = {
1479
- command: "npx",
1480
- args: ["-y", "@anthropic-ai/mcp-playwright"]
1481
- };
1482
1309
  }
1483
1310
  });
1484
1311
 
1485
1312
  // src/context.ts
1486
- import * as fs12 from "fs";
1487
- import * as path11 from "path";
1313
+ import * as fs5 from "fs";
1314
+ import * as path5 from "path";
1488
1315
  function readPromptFile(stageName, projectDir) {
1489
1316
  if (projectDir) {
1490
- const stepFile = path11.join(projectDir, ".kody", "steps", `${stageName}.md`);
1491
- if (fs12.existsSync(stepFile)) {
1492
- return fs12.readFileSync(stepFile, "utf-8");
1317
+ const stepFile = path5.join(projectDir, ".kody", "steps", `${stageName}.md`);
1318
+ if (fs5.existsSync(stepFile)) {
1319
+ return fs5.readFileSync(stepFile, "utf-8");
1493
1320
  }
1494
1321
  console.warn(` \u26A0 No step file at ${stepFile}, falling back to engine defaults. Run 'kody-engine-lite init --force' to generate step files.`);
1495
1322
  }
1496
1323
  const scriptDir = new URL(".", import.meta.url).pathname;
1497
1324
  const candidates = [
1498
- path11.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
1499
- path11.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
1325
+ path5.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
1326
+ path5.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
1500
1327
  ];
1501
1328
  for (const candidate of candidates) {
1502
- if (fs12.existsSync(candidate)) {
1503
- return fs12.readFileSync(candidate, "utf-8");
1329
+ if (fs5.existsSync(candidate)) {
1330
+ return fs5.readFileSync(candidate, "utf-8");
1504
1331
  }
1505
1332
  }
1506
1333
  throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
@@ -1512,18 +1339,18 @@ function injectTaskContext(prompt, taskId, taskDir, feedback) {
1512
1339
  `;
1513
1340
  context += `Task Directory: ${taskDir}
1514
1341
  `;
1515
- const taskMdPath = path11.join(taskDir, "task.md");
1516
- if (fs12.existsSync(taskMdPath)) {
1517
- const taskMd = fs12.readFileSync(taskMdPath, "utf-8");
1342
+ const taskMdPath = path5.join(taskDir, "task.md");
1343
+ if (fs5.existsSync(taskMdPath)) {
1344
+ const taskMd = fs5.readFileSync(taskMdPath, "utf-8");
1518
1345
  context += `
1519
1346
  ## Task Description
1520
1347
  ${taskMd}
1521
1348
  `;
1522
1349
  }
1523
- const taskJsonPath = path11.join(taskDir, "task.json");
1524
- if (fs12.existsSync(taskJsonPath)) {
1350
+ const taskJsonPath = path5.join(taskDir, "task.json");
1351
+ if (fs5.existsSync(taskJsonPath)) {
1525
1352
  try {
1526
- const taskDef = JSON.parse(fs12.readFileSync(taskJsonPath, "utf-8"));
1353
+ const taskDef = JSON.parse(fs5.readFileSync(taskJsonPath, "utf-8"));
1527
1354
  context += `
1528
1355
  ## Task Classification
1529
1356
  `;
@@ -1536,27 +1363,27 @@ ${taskMd}
1536
1363
  } catch {
1537
1364
  }
1538
1365
  }
1539
- const specPath = path11.join(taskDir, "spec.md");
1540
- if (fs12.existsSync(specPath)) {
1541
- const spec = fs12.readFileSync(specPath, "utf-8");
1366
+ const specPath = path5.join(taskDir, "spec.md");
1367
+ if (fs5.existsSync(specPath)) {
1368
+ const spec = fs5.readFileSync(specPath, "utf-8");
1542
1369
  const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
1543
1370
  context += `
1544
1371
  ## Spec Summary
1545
1372
  ${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
1546
1373
  `;
1547
1374
  }
1548
- const planPath = path11.join(taskDir, "plan.md");
1549
- if (fs12.existsSync(planPath)) {
1550
- const plan = fs12.readFileSync(planPath, "utf-8");
1375
+ const planPath = path5.join(taskDir, "plan.md");
1376
+ if (fs5.existsSync(planPath)) {
1377
+ const plan = fs5.readFileSync(planPath, "utf-8");
1551
1378
  const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
1552
1379
  context += `
1553
1380
  ## Plan Summary
1554
1381
  ${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
1555
1382
  `;
1556
1383
  }
1557
- const contextMdPath = path11.join(taskDir, "context.md");
1558
- if (fs12.existsSync(contextMdPath)) {
1559
- const accumulated = fs12.readFileSync(contextMdPath, "utf-8");
1384
+ const contextMdPath = path5.join(taskDir, "context.md");
1385
+ if (fs5.existsSync(contextMdPath)) {
1386
+ const accumulated = fs5.readFileSync(contextMdPath, "utf-8");
1560
1387
  const truncated = accumulated.slice(-MAX_ACCUMULATED_CONTEXT);
1561
1388
  const prefix = accumulated.length > MAX_ACCUMULATED_CONTEXT ? "...(earlier context truncated)\n" : "";
1562
1389
  context += `
@@ -1573,10 +1400,10 @@ ${feedback}
1573
1400
  return prompt.replace("{{TASK_CONTEXT}}", context);
1574
1401
  }
1575
1402
  function taskHasUI(taskDir) {
1576
- const taskJsonPath = path11.join(taskDir, "task.json");
1577
- if (!fs12.existsSync(taskJsonPath)) return true;
1403
+ const taskJsonPath = path5.join(taskDir, "task.json");
1404
+ if (!fs5.existsSync(taskJsonPath)) return true;
1578
1405
  try {
1579
- const taskDef = JSON.parse(fs12.readFileSync(taskJsonPath, "utf-8"));
1406
+ const taskDef = JSON.parse(fs5.readFileSync(taskJsonPath, "utf-8"));
1580
1407
  return taskDef.hasUI !== false;
1581
1408
  } catch {
1582
1409
  return true;
@@ -1696,11 +1523,6 @@ ${prompt}` : prompt;
1696
1523
  }
1697
1524
  if (isMcpEnabledForStage(stageName, config.mcp) && taskHasUI(taskDir)) {
1698
1525
  assembled = assembled + "\n\n" + getBrowserToolGuidance(stageName, taskDir);
1699
- const qaGuidePath = path11.join(projectDir, ".kody", "qa-guide.md");
1700
- if (fs12.existsSync(qaGuidePath)) {
1701
- const qaGuide = fs12.readFileSync(qaGuidePath, "utf-8").trim();
1702
- assembled = assembled + "\n\n" + qaGuide;
1703
- }
1704
1526
  }
1705
1527
  return assembled;
1706
1528
  }
@@ -1747,6 +1569,55 @@ var init_context = __esm({
1747
1569
  }
1748
1570
  });
1749
1571
 
1572
+ // src/validators.ts
1573
+ function stripFences(content) {
1574
+ return content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
1575
+ }
1576
+ function validateTaskJson(content) {
1577
+ try {
1578
+ const parsed = JSON.parse(stripFences(content));
1579
+ for (const field of REQUIRED_TASK_FIELDS) {
1580
+ if (!(field in parsed)) {
1581
+ return { valid: false, error: `Missing field: ${field}` };
1582
+ }
1583
+ }
1584
+ return { valid: true };
1585
+ } catch (err) {
1586
+ return {
1587
+ valid: false,
1588
+ error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`
1589
+ };
1590
+ }
1591
+ }
1592
+ function validatePlanMd(content) {
1593
+ if (content.length < 10) {
1594
+ return { valid: false, error: "Plan is too short (< 10 chars)" };
1595
+ }
1596
+ if (!/^##\s+\w+/m.test(content)) {
1597
+ return { valid: false, error: "Plan has no markdown h2 sections" };
1598
+ }
1599
+ return { valid: true };
1600
+ }
1601
+ function validateReviewMd(content) {
1602
+ if (/pass/i.test(content) || /fail/i.test(content)) {
1603
+ return { valid: true };
1604
+ }
1605
+ return { valid: false, error: "Review must contain 'pass' or 'fail'" };
1606
+ }
1607
+ var REQUIRED_TASK_FIELDS;
1608
+ var init_validators = __esm({
1609
+ "src/validators.ts"() {
1610
+ "use strict";
1611
+ REQUIRED_TASK_FIELDS = [
1612
+ "task_type",
1613
+ "title",
1614
+ "description",
1615
+ "scope",
1616
+ "risk_level"
1617
+ ];
1618
+ }
1619
+ });
1620
+
1750
1621
  // src/pipeline/runner-selection.ts
1751
1622
  function getRunnerForStage(ctx, stageName) {
1752
1623
  const config = getProjectConfig();
@@ -1767,8 +1638,8 @@ var init_runner_selection = __esm({
1767
1638
  });
1768
1639
 
1769
1640
  // src/stages/agent.ts
1770
- import * as fs13 from "fs";
1771
- import * as path12 from "path";
1641
+ import * as fs6 from "fs";
1642
+ import * as path6 from "path";
1772
1643
  function getSessionInfo(stageName, sessions) {
1773
1644
  const group = SESSION_GROUP[stageName];
1774
1645
  if (!group) return void 0;
@@ -1814,8 +1685,7 @@ async function executeAgentStage(ctx, def) {
1814
1685
  if (sessionInfo) {
1815
1686
  logger.info(` session: ${SESSION_GROUP[def.name]} (${sessionInfo.resumeSession ? "resume" : "new"})`);
1816
1687
  }
1817
- const mcpForStage = isMcpEnabledForStage(def.name, config.mcp) ? withPlaywrightIfNeeded(config.mcp, taskHasUI(ctx.taskDir)) : void 0;
1818
- const mcpConfigJson = buildMcpConfigJson(mcpForStage);
1688
+ const mcpConfigJson = isMcpEnabledForStage(def.name, config.mcp) ? buildMcpConfigJson(config.mcp) : void 0;
1819
1689
  if (mcpConfigJson) {
1820
1690
  logger.info(` MCP servers enabled for ${def.name}`);
1821
1691
  }
@@ -1830,27 +1700,27 @@ async function executeAgentStage(ctx, def) {
1830
1700
  return { outcome: result.outcome, error: result.error, retries: 0 };
1831
1701
  }
1832
1702
  if (def.outputFile && result.output) {
1833
- fs13.writeFileSync(path12.join(ctx.taskDir, def.outputFile), result.output);
1703
+ fs6.writeFileSync(path6.join(ctx.taskDir, def.outputFile), result.output);
1834
1704
  }
1835
1705
  if (def.outputFile) {
1836
- const outputPath = path12.join(ctx.taskDir, def.outputFile);
1837
- if (!fs13.existsSync(outputPath)) {
1838
- const ext = path12.extname(def.outputFile);
1839
- const base = path12.basename(def.outputFile, ext);
1840
- const files = fs13.readdirSync(ctx.taskDir);
1706
+ const outputPath = path6.join(ctx.taskDir, def.outputFile);
1707
+ if (!fs6.existsSync(outputPath)) {
1708
+ const ext = path6.extname(def.outputFile);
1709
+ const base = path6.basename(def.outputFile, ext);
1710
+ const files = fs6.readdirSync(ctx.taskDir);
1841
1711
  const variant = files.find(
1842
1712
  (f) => f.startsWith(base + "-") && f.endsWith(ext)
1843
1713
  );
1844
1714
  if (variant) {
1845
- fs13.renameSync(path12.join(ctx.taskDir, variant), outputPath);
1715
+ fs6.renameSync(path6.join(ctx.taskDir, variant), outputPath);
1846
1716
  logger.info(` Renamed variant ${variant} \u2192 ${def.outputFile}`);
1847
1717
  }
1848
1718
  }
1849
1719
  }
1850
1720
  if (def.outputFile) {
1851
- const outputPath = path12.join(ctx.taskDir, def.outputFile);
1852
- if (fs13.existsSync(outputPath)) {
1853
- const content = fs13.readFileSync(outputPath, "utf-8");
1721
+ const outputPath = path6.join(ctx.taskDir, def.outputFile);
1722
+ if (fs6.existsSync(outputPath)) {
1723
+ const content = fs6.readFileSync(outputPath, "utf-8");
1854
1724
  const validation = validateStageOutput(def.name, content);
1855
1725
  if (!validation.valid) {
1856
1726
  if (def.name === "taskify") {
@@ -1864,7 +1734,7 @@ async function executeAgentStage(ctx, def) {
1864
1734
  const stripped = stripFences(retryResult.output);
1865
1735
  const retryValidation = validateTaskJson(stripped);
1866
1736
  if (retryValidation.valid) {
1867
- fs13.writeFileSync(outputPath, retryResult.output);
1737
+ fs6.writeFileSync(outputPath, retryResult.output);
1868
1738
  logger.info(` taskify retry produced valid JSON`);
1869
1739
  } else {
1870
1740
  logger.warn(` taskify retry still invalid: ${retryValidation.error}`);
@@ -1875,10 +1745,10 @@ async function executeAgentStage(ctx, def) {
1875
1745
  description: plainText.slice(0, 500),
1876
1746
  scope: [],
1877
1747
  risk_level: "low",
1878
- hasUI: true,
1748
+ hasUI: false,
1879
1749
  questions: []
1880
1750
  }, null, 2);
1881
- fs13.writeFileSync(outputPath, fallback);
1751
+ fs6.writeFileSync(outputPath, fallback);
1882
1752
  logger.info(` taskify fallback: generated minimal task.json (risk_level=low)`);
1883
1753
  }
1884
1754
  }
@@ -1892,7 +1762,7 @@ async function executeAgentStage(ctx, def) {
1892
1762
  return { outcome: "completed", outputFile: def.outputFile, retries: 0 };
1893
1763
  }
1894
1764
  function appendStageContext(taskDir, stageName, output) {
1895
- const contextPath = path12.join(taskDir, "context.md");
1765
+ const contextPath = path6.join(taskDir, "context.md");
1896
1766
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19);
1897
1767
  let summary;
1898
1768
  if (output && output.trim()) {
@@ -1905,7 +1775,7 @@ function appendStageContext(taskDir, stageName, output) {
1905
1775
  ### ${stageName} (${timestamp2})
1906
1776
  ${summary}
1907
1777
  `;
1908
- fs13.appendFileSync(contextPath, entry);
1778
+ fs6.appendFileSync(contextPath, entry);
1909
1779
  }
1910
1780
  var SESSION_GROUP;
1911
1781
  var init_agent = __esm({
@@ -1928,7 +1798,7 @@ var init_agent = __esm({
1928
1798
  });
1929
1799
 
1930
1800
  // src/verify-runner.ts
1931
- import { execFileSync as execFileSync9 } from "child_process";
1801
+ import { execFileSync as execFileSync4 } from "child_process";
1932
1802
  function isExecError(err) {
1933
1803
  return typeof err === "object" && err !== null;
1934
1804
  }
@@ -1964,7 +1834,7 @@ function runCommand(cmd, cwd, timeout) {
1964
1834
  return { success: true, output: "", timedOut: false };
1965
1835
  }
1966
1836
  try {
1967
- const output = execFileSync9(parts[0], parts.slice(1), {
1837
+ const output = execFileSync4(parts[0], parts.slice(1), {
1968
1838
  cwd,
1969
1839
  timeout,
1970
1840
  encoding: "utf-8",
@@ -2035,7 +1905,7 @@ var init_verify_runner = __esm({
2035
1905
  });
2036
1906
 
2037
1907
  // src/observer.ts
2038
- import { execFileSync as execFileSync10 } from "child_process";
1908
+ import { execFileSync as execFileSync5 } from "child_process";
2039
1909
  async function diagnoseFailure(stageName, errorOutput, modifiedFiles, runner, model, options) {
2040
1910
  const context = [
2041
1911
  `Stage: ${stageName}`,
@@ -2060,48 +1930,42 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
2060
1930
  );
2061
1931
  if (result.outcome === "completed" && result.output) {
2062
1932
  const cleaned = result.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
2063
- const parseResult = parseJsonSafe(cleaned, ["classification"]);
2064
- if (parseResult.ok) {
2065
- const { data } = parseResult;
2066
- const validClassifications = [
2067
- "fixable",
2068
- "infrastructure",
2069
- "pre-existing",
2070
- "retry",
2071
- "abort"
2072
- ];
2073
- if (validClassifications.includes(data.classification)) {
2074
- logger.info(` Diagnosis: ${data.classification} \u2014 ${data.reason}`);
2075
- return {
2076
- classification: data.classification,
2077
- reason: data.reason ?? "Unknown reason",
2078
- resolution: data.resolution ?? ""
2079
- };
2080
- }
2081
- logger.warn(` Diagnosis returned invalid classification: ${data.classification}`);
2082
- } else {
2083
- logger.warn(` Diagnosis JSON invalid: ${parseResult.error}`);
1933
+ const parsed = JSON.parse(cleaned);
1934
+ const validClassifications = [
1935
+ "fixable",
1936
+ "infrastructure",
1937
+ "pre-existing",
1938
+ "retry",
1939
+ "abort"
1940
+ ];
1941
+ if (validClassifications.includes(parsed.classification)) {
1942
+ logger.info(` Diagnosis: ${parsed.classification} \u2014 ${parsed.reason}`);
1943
+ return {
1944
+ classification: parsed.classification,
1945
+ reason: parsed.reason ?? "Unknown reason",
1946
+ resolution: parsed.resolution ?? ""
1947
+ };
2084
1948
  }
2085
1949
  }
2086
1950
  } catch (err) {
2087
1951
  logger.warn(` Diagnosis error: ${err instanceof Error ? err.message : err}`);
2088
1952
  }
2089
- logger.warn(" Diagnosis failed \u2014 defaulting to retry");
1953
+ logger.warn(" Diagnosis failed \u2014 defaulting to fixable");
2090
1954
  return {
2091
- classification: "retry",
2092
- reason: "Could not diagnose failure \u2014 retrying gate",
1955
+ classification: "fixable",
1956
+ reason: "Could not diagnose failure",
2093
1957
  resolution: errorOutput.slice(-500)
2094
1958
  };
2095
1959
  }
2096
1960
  function getModifiedFiles(projectDir) {
2097
1961
  try {
2098
- const staged = execFileSync10("git", ["diff", "--name-only", "--cached"], {
1962
+ const staged = execFileSync5("git", ["diff", "--name-only", "--cached"], {
2099
1963
  encoding: "utf-8",
2100
1964
  cwd: projectDir,
2101
1965
  timeout: 5e3,
2102
1966
  stdio: ["pipe", "pipe", "pipe"]
2103
1967
  }).trim();
2104
- const unstaged = execFileSync10("git", ["diff", "--name-only"], {
1968
+ const unstaged = execFileSync5("git", ["diff", "--name-only"], {
2105
1969
  encoding: "utf-8",
2106
1970
  cwd: projectDir,
2107
1971
  timeout: 5e3,
@@ -2110,8 +1974,7 @@ function getModifiedFiles(projectDir) {
2110
1974
  const all = `${staged}
2111
1975
  ${unstaged}`.split("\n").filter(Boolean);
2112
1976
  return [...new Set(all)];
2113
- } catch (err) {
2114
- logger.warn(` Failed to get modified files: ${err instanceof Error ? err.message : String(err)}`);
1977
+ } catch {
2115
1978
  return [];
2116
1979
  }
2117
1980
  }
@@ -2120,7 +1983,6 @@ var init_observer = __esm({
2120
1983
  "src/observer.ts"() {
2121
1984
  "use strict";
2122
1985
  init_logger();
2123
- init_validators();
2124
1986
  DIAGNOSIS_PROMPT = `You are a pipeline failure diagnosis agent. Analyze the error and classify it.
2125
1987
 
2126
1988
  Output ONLY valid JSON. No markdown fences. No explanation.
@@ -2144,8 +2006,8 @@ Error context:
2144
2006
  });
2145
2007
 
2146
2008
  // src/stages/gate.ts
2147
- import * as fs14 from "fs";
2148
- import * as path13 from "path";
2009
+ import * as fs7 from "fs";
2010
+ import * as path7 from "path";
2149
2011
  function executeGateStage(ctx, def) {
2150
2012
  if (ctx.input.dryRun) {
2151
2013
  logger.info(` [dry-run] skipping ${def.name}`);
@@ -2188,7 +2050,7 @@ ${output}
2188
2050
  `);
2189
2051
  }
2190
2052
  }
2191
- fs14.writeFileSync(path13.join(ctx.taskDir, "verify.md"), lines.join(""));
2053
+ fs7.writeFileSync(path7.join(ctx.taskDir, "verify.md"), lines.join(""));
2192
2054
  return {
2193
2055
  outcome: verifyResult.pass ? "completed" : "failed",
2194
2056
  retries: 0
@@ -2203,9 +2065,9 @@ var init_gate = __esm({
2203
2065
  });
2204
2066
 
2205
2067
  // src/stages/verify.ts
2206
- import * as fs15 from "fs";
2207
- import * as path14 from "path";
2208
- import { execFileSync as execFileSync11 } from "child_process";
2068
+ import * as fs8 from "fs";
2069
+ import * as path8 from "path";
2070
+ import { execFileSync as execFileSync6 } from "child_process";
2209
2071
  async function executeVerifyWithAutofix(ctx, def) {
2210
2072
  const maxAttempts = def.maxRetries ?? 2;
2211
2073
  for (let attempt = 0; attempt <= maxAttempts; attempt++) {
@@ -2215,8 +2077,8 @@ async function executeVerifyWithAutofix(ctx, def) {
2215
2077
  return { ...gateResult, retries: attempt };
2216
2078
  }
2217
2079
  if (attempt < maxAttempts) {
2218
- const verifyPath = path14.join(ctx.taskDir, "verify.md");
2219
- const errorOutput = fs15.existsSync(verifyPath) ? fs15.readFileSync(verifyPath, "utf-8") : "Unknown error";
2080
+ const verifyPath = path8.join(ctx.taskDir, "verify.md");
2081
+ const errorOutput = fs8.existsSync(verifyPath) ? fs8.readFileSync(verifyPath, "utf-8") : "Unknown error";
2220
2082
  const modifiedFiles = getModifiedFiles(ctx.projectDir);
2221
2083
  const defaultRunner = getRunnerForStage(ctx, "taskify");
2222
2084
  const diagConfig = getProjectConfig();
@@ -2259,7 +2121,7 @@ ${diagnosis.resolution}`);
2259
2121
  const parts = parseCommand(cmd);
2260
2122
  if (parts.length === 0) return;
2261
2123
  try {
2262
- execFileSync11(parts[0], parts.slice(1), {
2124
+ execFileSync6(parts[0], parts.slice(1), {
2263
2125
  stdio: "pipe",
2264
2126
  timeout: FIX_COMMAND_TIMEOUT_MS
2265
2127
  });
@@ -2312,18 +2174,18 @@ var init_verify = __esm({
2312
2174
  });
2313
2175
 
2314
2176
  // src/cli/task-resolution.ts
2315
- import * as fs16 from "fs";
2316
- import * as path15 from "path";
2317
- import { execFileSync as execFileSync12 } from "child_process";
2177
+ import * as fs9 from "fs";
2178
+ import * as path9 from "path";
2179
+ import { execFileSync as execFileSync7 } from "child_process";
2318
2180
  function findLatestTaskForIssue(issueNumber, projectDir) {
2319
- const tasksDir = path15.join(projectDir, ".kody", "tasks");
2320
- if (!fs16.existsSync(tasksDir)) return null;
2321
- const allDirs = fs16.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
2181
+ const tasksDir = path9.join(projectDir, ".kody", "tasks");
2182
+ if (!fs9.existsSync(tasksDir)) return null;
2183
+ const allDirs = fs9.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
2322
2184
  const prefix = `${issueNumber}-`;
2323
2185
  const direct = allDirs.find((d) => d.startsWith(prefix));
2324
2186
  if (direct) return direct;
2325
2187
  try {
2326
- const branch = execFileSync12("git", ["branch", "--show-current"], {
2188
+ const branch = execFileSync7("git", ["branch", "--show-current"], {
2327
2189
  encoding: "utf-8",
2328
2190
  cwd: projectDir,
2329
2191
  timeout: 5e3,
@@ -2352,8 +2214,8 @@ var init_task_resolution = __esm({
2352
2214
  });
2353
2215
 
2354
2216
  // src/review-standalone.ts
2355
- import * as fs17 from "fs";
2356
- import * as path16 from "path";
2217
+ import * as fs10 from "fs";
2218
+ import * as path10 from "path";
2357
2219
  function resolveReviewTarget(input) {
2358
2220
  if (input.prs.length === 0) {
2359
2221
  return {
@@ -2377,8 +2239,8 @@ Or comment on the specific PR: \`@kody review\``
2377
2239
  }
2378
2240
  async function runStandaloneReview(input) {
2379
2241
  const taskId = input.taskId ?? `review-${generateTaskId()}`;
2380
- const taskDir = path16.join(input.projectDir, ".kody", "tasks", taskId);
2381
- fs17.mkdirSync(taskDir, { recursive: true });
2242
+ const taskDir = path10.join(input.projectDir, ".kody", "tasks", taskId);
2243
+ fs10.mkdirSync(taskDir, { recursive: true });
2382
2244
  const diffInstruction = input.baseBranch ? `
2383
2245
 
2384
2246
  ## Diff Command
@@ -2387,7 +2249,7 @@ Do NOT use bare \`git diff\` \u2014 it shows only uncommitted working tree chang
2387
2249
  const taskContent = `# ${input.prTitle}
2388
2250
 
2389
2251
  ${input.prBody ?? ""}${diffInstruction}`;
2390
- fs17.writeFileSync(path16.join(taskDir, "task.md"), taskContent);
2252
+ fs10.writeFileSync(path10.join(taskDir, "task.md"), taskContent);
2391
2253
  const reviewDef = STAGES.find((s) => s.name === "review");
2392
2254
  const ctx = {
2393
2255
  taskId,
@@ -2409,10 +2271,10 @@ ${input.prBody ?? ""}${diffInstruction}`;
2409
2271
  error: result.error ?? "Review stage failed"
2410
2272
  };
2411
2273
  }
2412
- const reviewPath = path16.join(taskDir, "review.md");
2274
+ const reviewPath = path10.join(taskDir, "review.md");
2413
2275
  let reviewContent;
2414
- if (fs17.existsSync(reviewPath)) {
2415
- reviewContent = fs17.readFileSync(reviewPath, "utf-8");
2276
+ if (fs10.existsSync(reviewPath)) {
2277
+ reviewContent = fs10.readFileSync(reviewPath, "utf-8");
2416
2278
  }
2417
2279
  return {
2418
2280
  outcome: "completed",
@@ -2451,8 +2313,8 @@ var init_review_standalone = __esm({
2451
2313
  });
2452
2314
 
2453
2315
  // src/stages/review.ts
2454
- import * as fs18 from "fs";
2455
- import * as path17 from "path";
2316
+ import * as fs11 from "fs";
2317
+ import * as path11 from "path";
2456
2318
  async function executeReviewWithFix(ctx, def) {
2457
2319
  if (ctx.input.dryRun) {
2458
2320
  return { outcome: "completed", retries: 0 };
@@ -2466,11 +2328,11 @@ async function executeReviewWithFix(ctx, def) {
2466
2328
  if (reviewResult.outcome !== "completed") {
2467
2329
  return reviewResult;
2468
2330
  }
2469
- const reviewFile = path17.join(ctx.taskDir, "review.md");
2470
- if (!fs18.existsSync(reviewFile)) {
2331
+ const reviewFile = path11.join(ctx.taskDir, "review.md");
2332
+ if (!fs11.existsSync(reviewFile)) {
2471
2333
  return { outcome: "failed", retries: iteration, error: "review.md not found" };
2472
2334
  }
2473
- const content = fs18.readFileSync(reviewFile, "utf-8");
2335
+ const content = fs11.readFileSync(reviewFile, "utf-8");
2474
2336
  if (detectReviewVerdict(content) !== "fail") {
2475
2337
  return { ...reviewResult, retries: iteration };
2476
2338
  }
@@ -2499,15 +2361,15 @@ var init_review = __esm({
2499
2361
  });
2500
2362
 
2501
2363
  // src/stages/ship.ts
2502
- import * as fs19 from "fs";
2503
- import * as path18 from "path";
2504
- import { execFileSync as execFileSync13 } from "child_process";
2364
+ import * as fs12 from "fs";
2365
+ import * as path12 from "path";
2366
+ import { execFileSync as execFileSync8 } from "child_process";
2505
2367
  function buildPrBody(ctx) {
2506
2368
  const sections = [];
2507
- const taskJsonPath = path18.join(ctx.taskDir, "task.json");
2508
- if (fs19.existsSync(taskJsonPath)) {
2369
+ const taskJsonPath = path12.join(ctx.taskDir, "task.json");
2370
+ if (fs12.existsSync(taskJsonPath)) {
2509
2371
  try {
2510
- const raw = fs19.readFileSync(taskJsonPath, "utf-8");
2372
+ const raw = fs12.readFileSync(taskJsonPath, "utf-8");
2511
2373
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2512
2374
  const task = JSON.parse(cleaned);
2513
2375
  if (task.description) {
@@ -2526,9 +2388,9 @@ ${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
2526
2388
  } catch {
2527
2389
  }
2528
2390
  }
2529
- const reviewPath = path18.join(ctx.taskDir, "review.md");
2530
- if (fs19.existsSync(reviewPath)) {
2531
- const review = fs19.readFileSync(reviewPath, "utf-8");
2391
+ const reviewPath = path12.join(ctx.taskDir, "review.md");
2392
+ if (fs12.existsSync(reviewPath)) {
2393
+ const review = fs12.readFileSync(reviewPath, "utf-8");
2532
2394
  const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
2533
2395
  if (summaryMatch) {
2534
2396
  const summary = summaryMatch[1].trim();
@@ -2545,14 +2407,14 @@ ${summary}`);
2545
2407
  **Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
2546
2408
  }
2547
2409
  }
2548
- const verifyPath = path18.join(ctx.taskDir, "verify.md");
2549
- if (fs19.existsSync(verifyPath)) {
2550
- const verify = fs19.readFileSync(verifyPath, "utf-8");
2410
+ const verifyPath = path12.join(ctx.taskDir, "verify.md");
2411
+ if (fs12.existsSync(verifyPath)) {
2412
+ const verify = fs12.readFileSync(verifyPath, "utf-8");
2551
2413
  if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
2552
2414
  }
2553
- const planPath = path18.join(ctx.taskDir, "plan.md");
2554
- if (fs19.existsSync(planPath)) {
2555
- const plan = fs19.readFileSync(planPath, "utf-8").trim();
2415
+ const planPath = path12.join(ctx.taskDir, "plan.md");
2416
+ if (fs12.existsSync(planPath)) {
2417
+ const plan = fs12.readFileSync(planPath, "utf-8").trim();
2556
2418
  if (plan) {
2557
2419
  const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
2558
2420
  sections.push(`
@@ -2572,25 +2434,25 @@ Closes #${ctx.input.issueNumber}`);
2572
2434
  return sections.join("\n");
2573
2435
  }
2574
2436
  function executeShipStage(ctx, _def) {
2575
- const shipPath = path18.join(ctx.taskDir, "ship.md");
2437
+ const shipPath = path12.join(ctx.taskDir, "ship.md");
2576
2438
  if (ctx.input.dryRun) {
2577
- fs19.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
2439
+ fs12.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
2578
2440
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
2579
2441
  }
2580
2442
  if (ctx.input.local && !ctx.input.issueNumber) {
2581
- fs19.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
2443
+ fs12.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
2582
2444
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
2583
2445
  }
2584
2446
  try {
2585
2447
  const head = getCurrentBranch(ctx.projectDir);
2586
2448
  const base = getDefaultBranch(ctx.projectDir);
2587
2449
  try {
2588
- execFileSync13("git", ["add", ctx.taskDir], {
2450
+ execFileSync8("git", ["add", ctx.taskDir], {
2589
2451
  cwd: ctx.projectDir,
2590
2452
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
2591
2453
  stdio: "pipe"
2592
2454
  });
2593
- execFileSync13("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
2455
+ execFileSync8("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
2594
2456
  cwd: ctx.projectDir,
2595
2457
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
2596
2458
  stdio: "pipe"
@@ -2604,7 +2466,7 @@ function executeShipStage(ctx, _def) {
2604
2466
  let repo = config.github?.repo;
2605
2467
  if (!owner || !repo) {
2606
2468
  try {
2607
- const remoteUrl = execFileSync13("git", ["remote", "get-url", "origin"], {
2469
+ const remoteUrl = execFileSync8("git", ["remote", "get-url", "origin"], {
2608
2470
  encoding: "utf-8",
2609
2471
  cwd: ctx.projectDir
2610
2472
  }).trim();
@@ -2625,28 +2487,28 @@ function executeShipStage(ctx, _def) {
2625
2487
  chore: "chore"
2626
2488
  };
2627
2489
  let prefix = "chore";
2628
- const taskJsonPath = path18.join(ctx.taskDir, "task.json");
2629
- if (fs19.existsSync(taskJsonPath)) {
2490
+ const taskJsonPath = path12.join(ctx.taskDir, "task.json");
2491
+ if (fs12.existsSync(taskJsonPath)) {
2630
2492
  try {
2631
- const raw = fs19.readFileSync(taskJsonPath, "utf-8");
2493
+ const raw = fs12.readFileSync(taskJsonPath, "utf-8");
2632
2494
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2633
2495
  const task = JSON.parse(cleaned);
2634
2496
  prefix = TYPE_PREFIX[task.task_type] ?? "chore";
2635
2497
  } catch {
2636
2498
  }
2637
2499
  }
2638
- const taskMdPath = path18.join(ctx.taskDir, "task.md");
2639
- if (fs19.existsSync(taskMdPath)) {
2640
- const content = fs19.readFileSync(taskMdPath, "utf-8");
2500
+ const taskMdPath = path12.join(ctx.taskDir, "task.md");
2501
+ if (fs12.existsSync(taskMdPath)) {
2502
+ const content = fs12.readFileSync(taskMdPath, "utf-8");
2641
2503
  const heading = content.split("\n").find((l) => l.startsWith("# "));
2642
2504
  if (heading) {
2643
2505
  title = `${prefix}: ${heading.replace(/^#\s*/, "").trim()}`.slice(0, 72);
2644
2506
  }
2645
2507
  }
2646
2508
  if (title === "Update") {
2647
- if (fs19.existsSync(taskJsonPath)) {
2509
+ if (fs12.existsSync(taskJsonPath)) {
2648
2510
  try {
2649
- const raw = fs19.readFileSync(taskJsonPath, "utf-8");
2511
+ const raw = fs12.readFileSync(taskJsonPath, "utf-8");
2650
2512
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2651
2513
  const task = JSON.parse(cleaned);
2652
2514
  if (task.title) title = `${prefix}: ${task.title}`.slice(0, 72);
@@ -2669,7 +2531,7 @@ function executeShipStage(ctx, _def) {
2669
2531
  } catch {
2670
2532
  }
2671
2533
  }
2672
- fs19.writeFileSync(shipPath, `# Ship
2534
+ fs12.writeFileSync(shipPath, `# Ship
2673
2535
 
2674
2536
  Updated existing PR: ${existingPr.url}
2675
2537
  PR #${existingPr.number}
@@ -2690,26 +2552,22 @@ PR #${existingPr.number}
2690
2552
  } catch {
2691
2553
  }
2692
2554
  }
2693
- fs19.writeFileSync(shipPath, `# Ship
2555
+ fs12.writeFileSync(shipPath, `# Ship
2694
2556
 
2695
2557
  PR created: ${pr.url}
2696
2558
  PR #${pr.number}
2697
2559
  `);
2698
2560
  } else {
2699
- fs19.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
2561
+ fs12.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
2700
2562
  }
2701
2563
  }
2702
2564
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
2703
2565
  } catch (err) {
2704
2566
  const msg = err instanceof Error ? err.message : String(err);
2705
- try {
2706
- fs19.writeFileSync(shipPath, `# Ship
2567
+ fs12.writeFileSync(shipPath, `# Ship
2707
2568
 
2708
2569
  Failed: ${msg}
2709
2570
  `);
2710
- } catch {
2711
- logger.warn(` Failed to write ship.md artifact`);
2712
- }
2713
2571
  return { outcome: "failed", retries: 0, error: msg };
2714
2572
  }
2715
2573
  }
@@ -2752,15 +2610,15 @@ var init_executor_registry = __esm({
2752
2610
  });
2753
2611
 
2754
2612
  // src/pipeline/questions.ts
2755
- import * as fs20 from "fs";
2756
- import * as path19 from "path";
2613
+ import * as fs13 from "fs";
2614
+ import * as path13 from "path";
2757
2615
  function checkForQuestions(ctx, stageName) {
2758
2616
  if (ctx.input.local || !ctx.input.issueNumber) return false;
2759
2617
  try {
2760
2618
  if (stageName === "taskify") {
2761
- const taskJsonPath = path19.join(ctx.taskDir, "task.json");
2762
- if (!fs20.existsSync(taskJsonPath)) return false;
2763
- const raw = fs20.readFileSync(taskJsonPath, "utf-8");
2619
+ const taskJsonPath = path13.join(ctx.taskDir, "task.json");
2620
+ if (!fs13.existsSync(taskJsonPath)) return false;
2621
+ const raw = fs13.readFileSync(taskJsonPath, "utf-8");
2764
2622
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2765
2623
  const taskJson = JSON.parse(cleaned);
2766
2624
  if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
@@ -2775,9 +2633,9 @@ Reply with \`@kody approve\` and your answers in the comment body.`;
2775
2633
  }
2776
2634
  }
2777
2635
  if (stageName === "plan") {
2778
- const planPath = path19.join(ctx.taskDir, "plan.md");
2779
- if (!fs20.existsSync(planPath)) return false;
2780
- const plan = fs20.readFileSync(planPath, "utf-8");
2636
+ const planPath = path13.join(ctx.taskDir, "plan.md");
2637
+ if (!fs13.existsSync(planPath)) return false;
2638
+ const plan = fs13.readFileSync(planPath, "utf-8");
2781
2639
  const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
2782
2640
  if (questionsMatch) {
2783
2641
  const questionsText = questionsMatch[1].trim();
@@ -2806,13 +2664,12 @@ var init_questions = __esm({
2806
2664
  });
2807
2665
 
2808
2666
  // src/pipeline/hooks.ts
2809
- import * as fs21 from "fs";
2810
- import * as path20 from "path";
2667
+ import * as fs14 from "fs";
2668
+ import * as path14 from "path";
2811
2669
  function applyPreStageLabel(ctx, def) {
2812
2670
  if (!ctx.input.issueNumber || ctx.input.local) return;
2813
2671
  if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
2814
2672
  if (def.name === "review") setLifecycleLabel(ctx.input.issueNumber, "review");
2815
- if (def.name === "ship") setLifecycleLabel(ctx.input.issueNumber, "shipping");
2816
2673
  }
2817
2674
  function checkQuestionsAfterStage(ctx, def, state) {
2818
2675
  if (def.name !== "taskify" && def.name !== "plan") return null;
@@ -2845,9 +2702,9 @@ function autoDetectComplexity(ctx, def) {
2845
2702
  return { complexity, activeStages };
2846
2703
  }
2847
2704
  try {
2848
- const taskJsonPath = path20.join(ctx.taskDir, "task.json");
2849
- if (!fs21.existsSync(taskJsonPath)) return null;
2850
- const raw = fs21.readFileSync(taskJsonPath, "utf-8");
2705
+ const taskJsonPath = path14.join(ctx.taskDir, "task.json");
2706
+ if (!fs14.existsSync(taskJsonPath)) return null;
2707
+ const raw = fs14.readFileSync(taskJsonPath, "utf-8");
2851
2708
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2852
2709
  const taskJson = JSON.parse(cleaned);
2853
2710
  if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
@@ -2877,8 +2734,8 @@ function checkRiskGate(ctx, def, state, complexity) {
2877
2734
  if (ctx.input.dryRun || ctx.input.local) return null;
2878
2735
  if (ctx.input.mode === "rerun") return null;
2879
2736
  if (!ctx.input.issueNumber) return null;
2880
- const planPath = path20.join(ctx.taskDir, "plan.md");
2881
- const plan = fs21.existsSync(planPath) ? fs21.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
2737
+ const planPath = path14.join(ctx.taskDir, "plan.md");
2738
+ const plan = fs14.existsSync(planPath) ? fs14.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
2882
2739
  try {
2883
2740
  postComment(
2884
2741
  ctx.input.issueNumber,
@@ -2945,22 +2802,22 @@ var init_hooks = __esm({
2945
2802
  });
2946
2803
 
2947
2804
  // src/learning/auto-learn.ts
2948
- import * as fs22 from "fs";
2949
- import * as path21 from "path";
2805
+ import * as fs15 from "fs";
2806
+ import * as path15 from "path";
2950
2807
  function stripAnsi(str) {
2951
2808
  return str.replace(/\x1b\[[0-9;]*m/g, "");
2952
2809
  }
2953
2810
  function autoLearn(ctx) {
2954
2811
  try {
2955
- const memoryDir = path21.join(ctx.projectDir, ".kody", "memory");
2956
- if (!fs22.existsSync(memoryDir)) {
2957
- fs22.mkdirSync(memoryDir, { recursive: true });
2812
+ const memoryDir = path15.join(ctx.projectDir, ".kody", "memory");
2813
+ if (!fs15.existsSync(memoryDir)) {
2814
+ fs15.mkdirSync(memoryDir, { recursive: true });
2958
2815
  }
2959
2816
  const learnings = [];
2960
2817
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2961
- const verifyPath = path21.join(ctx.taskDir, "verify.md");
2962
- if (fs22.existsSync(verifyPath)) {
2963
- const verify = stripAnsi(fs22.readFileSync(verifyPath, "utf-8"));
2818
+ const verifyPath = path15.join(ctx.taskDir, "verify.md");
2819
+ if (fs15.existsSync(verifyPath)) {
2820
+ const verify = stripAnsi(fs15.readFileSync(verifyPath, "utf-8"));
2964
2821
  if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
2965
2822
  if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
2966
2823
  if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
@@ -2969,18 +2826,18 @@ function autoLearn(ctx) {
2969
2826
  if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
2970
2827
  if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
2971
2828
  }
2972
- const reviewPath = path21.join(ctx.taskDir, "review.md");
2973
- if (fs22.existsSync(reviewPath)) {
2974
- const review = fs22.readFileSync(reviewPath, "utf-8");
2829
+ const reviewPath = path15.join(ctx.taskDir, "review.md");
2830
+ if (fs15.existsSync(reviewPath)) {
2831
+ const review = fs15.readFileSync(reviewPath, "utf-8");
2975
2832
  if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
2976
2833
  if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
2977
2834
  if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
2978
2835
  if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
2979
2836
  }
2980
- const taskJsonPath = path21.join(ctx.taskDir, "task.json");
2981
- if (fs22.existsSync(taskJsonPath)) {
2837
+ const taskJsonPath = path15.join(ctx.taskDir, "task.json");
2838
+ if (fs15.existsSync(taskJsonPath)) {
2982
2839
  try {
2983
- const raw = stripAnsi(fs22.readFileSync(taskJsonPath, "utf-8"));
2840
+ const raw = stripAnsi(fs15.readFileSync(taskJsonPath, "utf-8"));
2984
2841
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2985
2842
  const task = JSON.parse(cleaned);
2986
2843
  if (task.scope && Array.isArray(task.scope)) {
@@ -2991,48 +2848,135 @@ function autoLearn(ctx) {
2991
2848
  }
2992
2849
  }
2993
2850
  if (learnings.length > 0) {
2994
- const conventionsPath = path21.join(memoryDir, "conventions.md");
2851
+ const conventionsPath = path15.join(memoryDir, "conventions.md");
2995
2852
  const entry = `
2996
2853
  ## Learned ${timestamp2} (task: ${ctx.taskId})
2997
2854
  ${learnings.join("\n")}
2998
2855
  `;
2999
- fs22.appendFileSync(conventionsPath, entry);
2856
+ fs15.appendFileSync(conventionsPath, entry);
2857
+ invalidateCache(conventionsPath, path15.join(memoryDir, ".tiers"));
3000
2858
  logger.info(`Auto-learned ${learnings.length} convention(s)`);
3001
2859
  }
2860
+ autoLearnDecisions(ctx.taskDir, memoryDir, ctx.taskId, timestamp2);
3002
2861
  autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
3003
2862
  } catch {
3004
2863
  }
3005
2864
  }
3006
2865
  function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
3007
- const archPath = path21.join(memoryDir, "architecture.md");
3008
- if (fs22.existsSync(archPath)) return;
3009
- const detected = detectArchitectureBasic(projectDir);
2866
+ const archPath = path15.join(memoryDir, "architecture.md");
2867
+ if (fs15.existsSync(archPath)) return;
2868
+ const detected = [];
2869
+ const pkgPath = path15.join(projectDir, "package.json");
2870
+ if (fs15.existsSync(pkgPath)) {
2871
+ try {
2872
+ const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf-8"));
2873
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
2874
+ if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
2875
+ else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
2876
+ else if (allDeps.express) detected.push(`- Framework: Express ${allDeps.express}`);
2877
+ else if (allDeps.fastify) detected.push(`- Framework: Fastify ${allDeps.fastify}`);
2878
+ if (allDeps.typescript) detected.push(`- Language: TypeScript ${allDeps.typescript}`);
2879
+ if (allDeps.vitest) detected.push(`- Testing: vitest ${allDeps.vitest}`);
2880
+ else if (allDeps.jest) detected.push(`- Testing: jest ${allDeps.jest}`);
2881
+ if (allDeps.eslint) detected.push(`- Linting: eslint ${allDeps.eslint}`);
2882
+ if (allDeps.prisma || allDeps["@prisma/client"]) detected.push("- Database: Prisma ORM");
2883
+ if (allDeps.drizzle || allDeps["drizzle-orm"]) detected.push("- Database: Drizzle ORM");
2884
+ if (allDeps.pg || allDeps.postgres) detected.push("- Database: PostgreSQL");
2885
+ if (allDeps.payload || allDeps["@payloadcms/next"]) detected.push(`- CMS: Payload CMS`);
2886
+ if (pkg.type === "module") detected.push("- Module system: ESM");
2887
+ else detected.push("- Module system: CommonJS");
2888
+ if (fs15.existsSync(path15.join(projectDir, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
2889
+ else if (fs15.existsSync(path15.join(projectDir, "yarn.lock"))) detected.push("- Package manager: yarn");
2890
+ else if (fs15.existsSync(path15.join(projectDir, "package-lock.json"))) detected.push("- Package manager: npm");
2891
+ } catch {
2892
+ }
2893
+ }
2894
+ const topDirs = [];
2895
+ try {
2896
+ const entries = fs15.readdirSync(projectDir, { withFileTypes: true });
2897
+ for (const entry of entries) {
2898
+ if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
2899
+ topDirs.push(entry.name);
2900
+ }
2901
+ }
2902
+ if (topDirs.length > 0) detected.push(`- Top-level directories: ${topDirs.join(", ")}`);
2903
+ } catch {
2904
+ }
2905
+ const srcDir = path15.join(projectDir, "src");
2906
+ if (fs15.existsSync(srcDir)) {
2907
+ try {
2908
+ const srcEntries = fs15.readdirSync(srcDir, { withFileTypes: true });
2909
+ const srcDirs = srcEntries.filter((e) => e.isDirectory()).map((e) => e.name);
2910
+ if (srcDirs.length > 0) detected.push(`- src/ structure: ${srcDirs.join(", ")}`);
2911
+ } catch {
2912
+ }
2913
+ }
3010
2914
  if (detected.length > 0) {
3011
2915
  const content = `# Architecture (auto-detected ${timestamp2})
3012
2916
 
3013
2917
  ## Overview
3014
2918
  ${detected.join("\n")}
3015
2919
  `;
3016
- fs22.writeFileSync(archPath, content);
2920
+ fs15.writeFileSync(archPath, content);
2921
+ invalidateCache(archPath, path15.join(memoryDir, ".tiers"));
3017
2922
  logger.info(`Auto-detected architecture (${detected.length} items)`);
3018
2923
  }
3019
2924
  }
2925
+ function autoLearnDecisions(taskDir, memoryDir, taskId, timestamp2) {
2926
+ const reviewPath = path15.join(taskDir, "review.md");
2927
+ if (!fs15.existsSync(reviewPath)) return;
2928
+ const review = fs15.readFileSync(reviewPath, "utf-8");
2929
+ const decisions = [];
2930
+ const existingPatternRe = /(?:use|follow|reuse|match|adopt)\s+(?:the\s+)?existing\s+(.+?)(?:\.|$)/gim;
2931
+ for (const match of review.matchAll(existingPatternRe)) {
2932
+ decisions.push(`- Use existing ${match[1].trim()}`);
2933
+ }
2934
+ const insteadOfRe = /instead\s+of\s+(.+?),?\s+(?:use|prefer|adopt)\s+(.+?)(?:\.|$)/gim;
2935
+ for (const match of review.matchAll(insteadOfRe)) {
2936
+ decisions.push(`- Prefer ${match[2].trim()} over ${match[1].trim()}`);
2937
+ }
2938
+ const consistentRe = /(?:consistent\s+with|same\s+pattern\s+as|follow\s+the\s+pattern\s+(?:in|from))\s+(.+?)(?:\.|$)/gim;
2939
+ for (const match of review.matchAll(consistentRe)) {
2940
+ decisions.push(`- Follow pattern from ${match[1].trim()}`);
2941
+ }
2942
+ const avoidRe = /(?:don't|do\s+not|never|avoid)\s+(?:use\s+)?(.+?)\s+(?:for|when|in)\s+(.+?)(?:\.|$)/gim;
2943
+ for (const match of review.matchAll(avoidRe)) {
2944
+ decisions.push(`- Avoid ${match[1].trim()} for ${match[2].trim()}`);
2945
+ }
2946
+ if (decisions.length === 0) return;
2947
+ const decisionsPath = path15.join(memoryDir, "decisions.md");
2948
+ let existing = "";
2949
+ if (fs15.existsSync(decisionsPath)) {
2950
+ existing = fs15.readFileSync(decisionsPath, "utf-8");
2951
+ } else {
2952
+ existing = "# Architectural Decisions\n\nDecisions extracted from code reviews. The planning agent MUST follow these.\n";
2953
+ }
2954
+ const newDecisions = decisions.filter((d) => !existing.includes(d));
2955
+ if (newDecisions.length === 0) return;
2956
+ const entry = `
2957
+ ## From task ${taskId} (${timestamp2})
2958
+ ${newDecisions.join("\n")}
2959
+ `;
2960
+ fs15.appendFileSync(decisionsPath, existing ? entry : existing + entry);
2961
+ invalidateCache(decisionsPath, path15.join(memoryDir, ".tiers"));
2962
+ logger.info(`Auto-learned ${newDecisions.length} architectural decision(s)`);
2963
+ }
3020
2964
  var init_auto_learn = __esm({
3021
2965
  "src/learning/auto-learn.ts"() {
3022
2966
  "use strict";
3023
2967
  init_logger();
3024
- init_architecture_detection();
2968
+ init_context_tiers();
3025
2969
  }
3026
2970
  });
3027
2971
 
3028
2972
  // src/retrospective.ts
3029
- import * as fs23 from "fs";
3030
- import * as path22 from "path";
2973
+ import * as fs16 from "fs";
2974
+ import * as path16 from "path";
3031
2975
  function readArtifact(taskDir, filename, maxChars) {
3032
- const p = path22.join(taskDir, filename);
3033
- if (!fs23.existsSync(p)) return null;
2976
+ const p = path16.join(taskDir, filename);
2977
+ if (!fs16.existsSync(p)) return null;
3034
2978
  try {
3035
- const content = fs23.readFileSync(p, "utf-8");
2979
+ const content = fs16.readFileSync(p, "utf-8");
3036
2980
  return content.length > maxChars ? content.slice(0, maxChars) + "\n...(truncated)" : content;
3037
2981
  } catch {
3038
2982
  return null;
@@ -3085,13 +3029,13 @@ function collectRunContext(ctx, state, pipelineStartTime) {
3085
3029
  return lines.join("\n");
3086
3030
  }
3087
3031
  function getLogPath(projectDir) {
3088
- return path22.join(projectDir, ".kody", "memory", "observer-log.jsonl");
3032
+ return path16.join(projectDir, ".kody", "memory", "observer-log.jsonl");
3089
3033
  }
3090
3034
  function readPreviousRetrospectives(projectDir, limit = 10) {
3091
3035
  const logPath = getLogPath(projectDir);
3092
- if (!fs23.existsSync(logPath)) return [];
3036
+ if (!fs16.existsSync(logPath)) return [];
3093
3037
  try {
3094
- const content = fs23.readFileSync(logPath, "utf-8");
3038
+ const content = fs16.readFileSync(logPath, "utf-8");
3095
3039
  const lines = content.split("\n").filter(Boolean);
3096
3040
  const entries = [];
3097
3041
  const start = Math.max(0, lines.length - limit);
@@ -3118,11 +3062,11 @@ function formatPreviousEntries(entries) {
3118
3062
  }
3119
3063
  function appendRetrospectiveEntry(projectDir, entry) {
3120
3064
  const logPath = getLogPath(projectDir);
3121
- const dir = path22.dirname(logPath);
3122
- if (!fs23.existsSync(dir)) {
3123
- fs23.mkdirSync(dir, { recursive: true });
3065
+ const dir = path16.dirname(logPath);
3066
+ if (!fs16.existsSync(dir)) {
3067
+ fs16.mkdirSync(dir, { recursive: true });
3124
3068
  }
3125
- fs23.appendFileSync(logPath, JSON.stringify(entry) + "\n");
3069
+ fs16.appendFileSync(logPath, JSON.stringify(entry) + "\n");
3126
3070
  }
3127
3071
  async function runRetrospective(ctx, state, pipelineStartTime) {
3128
3072
  if (ctx.input.dryRun) return;
@@ -3233,8 +3177,8 @@ If no pipeline flaw is detected, set "pipelineFlaw" to null.
3233
3177
  });
3234
3178
 
3235
3179
  // src/pipeline.ts
3236
- import * as fs24 from "fs";
3237
- import * as path23 from "path";
3180
+ import * as fs17 from "fs";
3181
+ import * as path17 from "path";
3238
3182
  function ensureFeatureBranchIfNeeded(ctx) {
3239
3183
  if (ctx.input.dryRun) return;
3240
3184
  if (ctx.input.prNumber) {
@@ -3247,59 +3191,34 @@ function ensureFeatureBranchIfNeeded(ctx) {
3247
3191
  }
3248
3192
  if (!ctx.input.issueNumber) return;
3249
3193
  try {
3250
- const taskMdPath = path23.join(ctx.taskDir, "task.md");
3251
- const title = fs24.existsSync(taskMdPath) ? fs24.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
3194
+ const taskMdPath = path17.join(ctx.taskDir, "task.md");
3195
+ const title = fs17.existsSync(taskMdPath) ? fs17.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
3252
3196
  ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
3253
3197
  syncWithDefault(ctx.projectDir);
3254
3198
  } catch (err) {
3255
- const msg = err instanceof Error ? err.message : String(err);
3256
- if (msg.includes("not a git repository")) {
3257
- logger.warn(` Not a git repository \u2014 skipping feature branch setup`);
3258
- } else {
3259
- logger.error(` Failed to create/sync feature branch: ${msg}`);
3260
- throw new Error(`Feature branch setup failed: ${msg}`);
3261
- }
3199
+ logger.warn(` Failed to create/sync feature branch: ${err}`);
3262
3200
  }
3263
3201
  }
3264
3202
  function acquireLock(taskDir) {
3265
- const lockPath = path23.join(taskDir, ".lock");
3266
- if (fs24.existsSync(lockPath)) {
3203
+ const lockPath = path17.join(taskDir, ".lock");
3204
+ if (fs17.existsSync(lockPath)) {
3267
3205
  try {
3268
- const pid = parseInt(fs24.readFileSync(lockPath, "utf-8").trim(), 10);
3269
- if (!isNaN(pid)) {
3270
- try {
3271
- process.kill(pid, 0);
3272
- throw new Error(`Pipeline already running (PID ${pid})`);
3273
- } catch (e) {
3274
- if (e.code !== "ESRCH") throw e;
3275
- logger.info(` Removing stale lock (PID ${pid} no longer running)`);
3276
- }
3277
- } else {
3278
- logger.warn(` Corrupt lock file (non-numeric PID) \u2014 overwriting`);
3206
+ const pid = parseInt(fs17.readFileSync(lockPath, "utf-8").trim(), 10);
3207
+ try {
3208
+ process.kill(pid, 0);
3209
+ throw new Error(`Pipeline already running (PID ${pid})`);
3210
+ } catch (e) {
3211
+ if (e.code !== "ESRCH") throw e;
3279
3212
  }
3280
3213
  } catch (e) {
3281
3214
  if (e instanceof Error && e.message.startsWith("Pipeline already")) throw e;
3282
- logger.warn(` Corrupt lock file \u2014 overwriting`);
3283
- }
3284
- try {
3285
- fs24.unlinkSync(lockPath);
3286
- } catch {
3287
3215
  }
3288
3216
  }
3289
- try {
3290
- const fd = fs24.openSync(lockPath, fs24.constants.O_WRONLY | fs24.constants.O_CREAT | fs24.constants.O_EXCL);
3291
- fs24.writeSync(fd, String(process.pid));
3292
- fs24.closeSync(fd);
3293
- } catch (err) {
3294
- if (err.code === "EEXIST") {
3295
- throw new Error("Pipeline already running (lock acquired by another process)");
3296
- }
3297
- throw err;
3298
- }
3217
+ fs17.writeFileSync(lockPath, String(process.pid));
3299
3218
  }
3300
3219
  function releaseLock(taskDir) {
3301
3220
  try {
3302
- fs24.unlinkSync(path23.join(taskDir, ".lock"));
3221
+ fs17.unlinkSync(path17.join(taskDir, ".lock"));
3303
3222
  } catch {
3304
3223
  }
3305
3224
  }
@@ -3311,17 +3230,17 @@ async function runPipeline(ctx) {
3311
3230
  try {
3312
3231
  const state = loadState(ctx.taskId, ctx.taskDir);
3313
3232
  if (state && state.state === "running") {
3314
- const updatedStages = { ...state.stages };
3233
+ state.state = "failed";
3315
3234
  for (const stage of STAGES) {
3316
- if (updatedStages[stage.name]?.state === "running") {
3317
- updatedStages[stage.name] = {
3318
- ...updatedStages[stage.name],
3235
+ if (state.stages[stage.name]?.state === "running") {
3236
+ state.stages[stage.name] = {
3237
+ ...state.stages[stage.name],
3319
3238
  state: "failed",
3320
3239
  error: "Pipeline crashed unexpectedly"
3321
3240
  };
3322
3241
  }
3323
3242
  }
3324
- writeState({ ...state, state: "failed", stages: updatedStages }, ctx.taskDir);
3243
+ writeState(state, ctx.taskDir);
3325
3244
  }
3326
3245
  } catch {
3327
3246
  }
@@ -3446,8 +3365,7 @@ async function runPipelineInner(ctx) {
3446
3365
  }
3447
3366
  autoLearn(ctx);
3448
3367
  }
3449
- await runRetrospective(ctx, state, pipelineStartTime).catch((err) => {
3450
- logger.warn(` Retrospective failed: ${err instanceof Error ? err.message : String(err)}`);
3368
+ await runRetrospective(ctx, state, pipelineStartTime).catch(() => {
3451
3369
  });
3452
3370
  return state;
3453
3371
  }
@@ -3487,8 +3405,8 @@ var init_pipeline = __esm({
3487
3405
  });
3488
3406
 
3489
3407
  // src/preflight.ts
3490
- import { execFileSync as execFileSync14 } from "child_process";
3491
- import * as fs25 from "fs";
3408
+ import { execFileSync as execFileSync9 } from "child_process";
3409
+ import * as fs18 from "fs";
3492
3410
  function check(name, fn) {
3493
3411
  try {
3494
3412
  const detail = fn() ?? void 0;
@@ -3500,7 +3418,7 @@ function check(name, fn) {
3500
3418
  function runPreflight() {
3501
3419
  const checks = [
3502
3420
  check("claude CLI", () => {
3503
- const v = execFileSync14("claude", ["--version"], {
3421
+ const v = execFileSync9("claude", ["--version"], {
3504
3422
  encoding: "utf-8",
3505
3423
  timeout: 1e4,
3506
3424
  stdio: ["pipe", "pipe", "pipe"]
@@ -3508,14 +3426,14 @@ function runPreflight() {
3508
3426
  return v;
3509
3427
  }),
3510
3428
  check("git repo", () => {
3511
- execFileSync14("git", ["rev-parse", "--is-inside-work-tree"], {
3429
+ execFileSync9("git", ["rev-parse", "--is-inside-work-tree"], {
3512
3430
  encoding: "utf-8",
3513
3431
  timeout: 5e3,
3514
3432
  stdio: ["pipe", "pipe", "pipe"]
3515
3433
  });
3516
3434
  }),
3517
3435
  check("pnpm", () => {
3518
- const v = execFileSync14("pnpm", ["--version"], {
3436
+ const v = execFileSync9("pnpm", ["--version"], {
3519
3437
  encoding: "utf-8",
3520
3438
  timeout: 5e3,
3521
3439
  stdio: ["pipe", "pipe", "pipe"]
@@ -3523,7 +3441,7 @@ function runPreflight() {
3523
3441
  return v;
3524
3442
  }),
3525
3443
  check("node >= 18", () => {
3526
- const v = execFileSync14("node", ["--version"], {
3444
+ const v = execFileSync9("node", ["--version"], {
3527
3445
  encoding: "utf-8",
3528
3446
  timeout: 5e3,
3529
3447
  stdio: ["pipe", "pipe", "pipe"]
@@ -3533,7 +3451,7 @@ function runPreflight() {
3533
3451
  return v;
3534
3452
  }),
3535
3453
  check("gh CLI", () => {
3536
- const v = execFileSync14("gh", ["--version"], {
3454
+ const v = execFileSync9("gh", ["--version"], {
3537
3455
  encoding: "utf-8",
3538
3456
  timeout: 5e3,
3539
3457
  stdio: ["pipe", "pipe", "pipe"]
@@ -3541,7 +3459,7 @@ function runPreflight() {
3541
3459
  return v;
3542
3460
  }),
3543
3461
  check("package.json", () => {
3544
- if (!fs25.existsSync("package.json")) throw new Error("not found");
3462
+ if (!fs18.existsSync("package.json")) throw new Error("not found");
3545
3463
  })
3546
3464
  ];
3547
3465
  const failed = checks.filter((c) => !c.ok);
@@ -3618,10 +3536,10 @@ var init_args = __esm({
3618
3536
  });
3619
3537
 
3620
3538
  // src/cli/litellm.ts
3621
- import * as fs26 from "fs";
3539
+ import * as fs19 from "fs";
3622
3540
  import * as os from "os";
3623
- import * as path24 from "path";
3624
- import { execFileSync as execFileSync15 } from "child_process";
3541
+ import * as path18 from "path";
3542
+ import { execFileSync as execFileSync10 } from "child_process";
3625
3543
  async function checkLitellmHealth(url) {
3626
3544
  try {
3627
3545
  const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
@@ -3681,17 +3599,17 @@ async function tryStartLitellm(url, projectDir, generatedConfig) {
3681
3599
  logger.warn("No provider configured in kody.config.json \u2014 cannot start LiteLLM proxy");
3682
3600
  return null;
3683
3601
  }
3684
- const configPath = path24.join(os.tmpdir(), "kody-litellm-config.yaml");
3685
- fs26.writeFileSync(configPath, generatedConfig);
3602
+ const configPath = path18.join(os.tmpdir(), "kody-litellm-config.yaml");
3603
+ fs19.writeFileSync(configPath, generatedConfig);
3686
3604
  const portMatch = url.match(/:(\d+)/);
3687
3605
  const port = portMatch ? portMatch[1] : "4000";
3688
3606
  let litellmFound = false;
3689
3607
  try {
3690
- execFileSync15("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
3608
+ execFileSync10("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
3691
3609
  litellmFound = true;
3692
3610
  } catch {
3693
3611
  try {
3694
- execFileSync15("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
3612
+ execFileSync10("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
3695
3613
  litellmFound = true;
3696
3614
  } catch {
3697
3615
  }
@@ -3704,29 +3622,19 @@ async function tryStartLitellm(url, projectDir, generatedConfig) {
3704
3622
  let cmd;
3705
3623
  let args2;
3706
3624
  try {
3707
- execFileSync15("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
3625
+ execFileSync10("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
3708
3626
  cmd = "litellm";
3709
3627
  args2 = ["--config", configPath, "--port", port];
3710
3628
  } catch {
3711
3629
  cmd = "python3";
3712
3630
  args2 = ["-m", "litellm", "--config", configPath, "--port", port];
3713
3631
  }
3714
- const dotenvPath = path24.join(projectDir, ".env");
3632
+ const dotenvPath = path18.join(projectDir, ".env");
3715
3633
  const dotenvVars = {};
3716
- if (fs26.existsSync(dotenvPath)) {
3717
- for (const rawLine of fs26.readFileSync(dotenvPath, "utf-8").split("\n")) {
3718
- const line = rawLine.trim();
3719
- if (!line || line.startsWith("#")) continue;
3634
+ if (fs19.existsSync(dotenvPath)) {
3635
+ for (const line of fs19.readFileSync(dotenvPath, "utf-8").split("\n")) {
3720
3636
  const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
3721
- if (match) {
3722
- let value = match[2].trim();
3723
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
3724
- value = value.slice(1, -1);
3725
- }
3726
- const commentIdx = value.indexOf(" #");
3727
- if (commentIdx !== -1) value = value.slice(0, commentIdx).trim();
3728
- if (value) dotenvVars[match[1]] = value;
3729
- }
3637
+ if (match) dotenvVars[match[1]] = match[2];
3730
3638
  }
3731
3639
  if (Object.keys(dotenvVars).length > 0) {
3732
3640
  logger.info(` Loaded API keys: ${Object.keys(dotenvVars).join(", ")}`);
@@ -3765,8 +3673,8 @@ var init_litellm = __esm({
3765
3673
  });
3766
3674
 
3767
3675
  // src/cli/task-state.ts
3768
- import * as fs27 from "fs";
3769
- import * as path25 from "path";
3676
+ import * as fs20 from "fs";
3677
+ import * as path19 from "path";
3770
3678
  function resolveTaskAction(issueNumber, existingTaskId, existingState) {
3771
3679
  if (!existingTaskId || !existingState) {
3772
3680
  return { action: "start-fresh", taskId: `${issueNumber}-${generateTaskId()}` };
@@ -3798,11 +3706,11 @@ function resolveTaskAction(issueNumber, existingTaskId, existingState) {
3798
3706
  function resolveForIssue(issueNumber, projectDir) {
3799
3707
  const existingTaskId = findLatestTaskForIssue(issueNumber, projectDir);
3800
3708
  if (existingTaskId) {
3801
- const statusPath = path25.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
3709
+ const statusPath = path19.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
3802
3710
  let existingState = null;
3803
- if (fs27.existsSync(statusPath)) {
3711
+ if (fs20.existsSync(statusPath)) {
3804
3712
  try {
3805
- existingState = JSON.parse(fs27.readFileSync(statusPath, "utf-8"));
3713
+ existingState = JSON.parse(fs20.readFileSync(statusPath, "utf-8"));
3806
3714
  } catch {
3807
3715
  }
3808
3716
  }
@@ -3835,12 +3743,12 @@ var resolve_exports = {};
3835
3743
  __export(resolve_exports, {
3836
3744
  runResolve: () => runResolve
3837
3745
  });
3838
- import { execFileSync as execFileSync16 } from "child_process";
3746
+ import { execFileSync as execFileSync11 } from "child_process";
3839
3747
  function getConflictContext(cwd, files) {
3840
3748
  const parts = [];
3841
3749
  for (const file of files.slice(0, 10)) {
3842
3750
  try {
3843
- const content = execFileSync16("git", ["diff", file], {
3751
+ const content = execFileSync11("git", ["diff", file], {
3844
3752
  cwd,
3845
3753
  encoding: "utf-8",
3846
3754
  stdio: ["pipe", "pipe", "pipe"]
@@ -3959,8 +3867,8 @@ var init_resolve = __esm({
3959
3867
 
3960
3868
  // src/entry.ts
3961
3869
  var entry_exports = {};
3962
- import * as fs28 from "fs";
3963
- import * as path26 from "path";
3870
+ import * as fs21 from "fs";
3871
+ import * as path20 from "path";
3964
3872
  async function ensureLitellmProxy(config, projectDir) {
3965
3873
  if (!needsLitellmProxy(config)) return null;
3966
3874
  const litellmUrl = getLitellmUrl();
@@ -3989,7 +3897,7 @@ async function ensureLitellmProxy(config, projectDir) {
3989
3897
  process.env.ANTHROPIC_BASE_URL = litellmUrl;
3990
3898
  logger.info(`ANTHROPIC_BASE_URL set to ${litellmUrl}`);
3991
3899
  if (!process.env.ANTHROPIC_API_KEY || !process.env.ANTHROPIC_API_KEY.startsWith("sk-ant-")) {
3992
- process.env.ANTHROPIC_API_KEY = `sk-ant-api03-${"0".repeat(64)}`;
3900
+ process.env.ANTHROPIC_API_KEY = "sk-ant-api03-litellm-proxy-key-00000000000000000000000000000000000000000000000000000000000000000000";
3993
3901
  }
3994
3902
  return litellmProcess;
3995
3903
  }
@@ -4014,9 +3922,9 @@ async function runModelHealthCheck(config) {
4014
3922
  }
4015
3923
  async function main() {
4016
3924
  const input = parseArgs();
4017
- const projectDir = input.cwd ? path26.resolve(input.cwd) : process.cwd();
3925
+ const projectDir = input.cwd ? path20.resolve(input.cwd) : process.cwd();
4018
3926
  if (input.cwd) {
4019
- if (!fs28.existsSync(projectDir)) {
3927
+ if (!fs21.existsSync(projectDir)) {
4020
3928
  console.error(`--cwd path does not exist: ${projectDir}`);
4021
3929
  process.exit(1);
4022
3930
  }
@@ -4050,11 +3958,9 @@ async function main() {
4050
3958
  process.exit(0);
4051
3959
  }
4052
3960
  if (taskAction.action === "resume") {
4053
- Object.assign(input, {
4054
- taskId: taskAction.taskId,
4055
- fromStage: taskAction.fromStage,
4056
- command: "rerun"
4057
- });
3961
+ input.taskId = taskAction.taskId;
3962
+ input.fromStage = taskAction.fromStage;
3963
+ input.command = "rerun";
4058
3964
  logger.info(`Resuming task ${taskAction.taskId} from ${taskAction.fromStage}`);
4059
3965
  }
4060
3966
  }
@@ -4073,8 +3979,8 @@ async function main() {
4073
3979
  process.exit(1);
4074
3980
  }
4075
3981
  }
4076
- const taskDir = path26.join(projectDir, ".kody", "tasks", taskId);
4077
- fs28.mkdirSync(taskDir, { recursive: true });
3982
+ const taskDir = path20.join(projectDir, ".kody", "tasks", taskId);
3983
+ fs21.mkdirSync(taskDir, { recursive: true });
4078
3984
  if (input.command === "status") {
4079
3985
  printStatus(taskId, taskDir);
4080
3986
  return;
@@ -4179,7 +4085,7 @@ async function main() {
4179
4085
  runners: runners2,
4180
4086
  local: input.local ?? true
4181
4087
  });
4182
- if (litellmProcess2) litellmProcess2.kill();
4088
+ if (litellmProcess2) litellmProcess2.kill?.();
4183
4089
  if (result.outcome === "failed") {
4184
4090
  console.error(`Resolve failed: ${result.error}`);
4185
4091
  process.exit(1);
@@ -4190,31 +4096,31 @@ async function main() {
4190
4096
  logger.info("Preflight checks:");
4191
4097
  runPreflight();
4192
4098
  if (input.task) {
4193
- fs28.writeFileSync(path26.join(taskDir, "task.md"), input.task);
4099
+ fs21.writeFileSync(path20.join(taskDir, "task.md"), input.task);
4194
4100
  }
4195
- const taskMdPath = path26.join(taskDir, "task.md");
4196
- if (!fs28.existsSync(taskMdPath) && isPRFix && input.prNumber) {
4101
+ const taskMdPath = path20.join(taskDir, "task.md");
4102
+ if (!fs21.existsSync(taskMdPath) && isPRFix && input.prNumber) {
4197
4103
  logger.info(`Fetching PR #${input.prNumber} details as task context...`);
4198
4104
  const prDetails = getPRDetails(input.prNumber);
4199
4105
  if (prDetails) {
4200
4106
  const taskContent = `# ${prDetails.title}
4201
4107
 
4202
4108
  ${prDetails.body ?? ""}`;
4203
- fs28.writeFileSync(taskMdPath, taskContent);
4109
+ fs21.writeFileSync(taskMdPath, taskContent);
4204
4110
  logger.info(` Task loaded from PR #${input.prNumber}: ${prDetails.title}`);
4205
4111
  }
4206
- } else if (!fs28.existsSync(taskMdPath) && input.issueNumber) {
4112
+ } else if (!fs21.existsSync(taskMdPath) && input.issueNumber) {
4207
4113
  logger.info(`Fetching issue #${input.issueNumber} body as task...`);
4208
4114
  const issue = getIssue(input.issueNumber);
4209
4115
  if (issue) {
4210
4116
  const taskContent = `# ${issue.title}
4211
4117
 
4212
4118
  ${issue.body ?? ""}`;
4213
- fs28.writeFileSync(taskMdPath, taskContent);
4119
+ fs21.writeFileSync(taskMdPath, taskContent);
4214
4120
  logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
4215
4121
  }
4216
4122
  }
4217
- if (!fs28.existsSync(taskMdPath)) {
4123
+ if (!fs21.existsSync(taskMdPath)) {
4218
4124
  console.error("No task.md found. Provide --task, --issue-number, or ensure .kody/tasks/<id>/task.md exists.");
4219
4125
  process.exit(1);
4220
4126
  }
@@ -4286,7 +4192,7 @@ ${input.feedback}`);
4286
4192
  await runModelHealthCheck(config);
4287
4193
  const cleanupLitellm = () => {
4288
4194
  if (litellmProcess) {
4289
- litellmProcess.kill();
4195
+ litellmProcess.kill?.();
4290
4196
  litellmProcess = null;
4291
4197
  }
4292
4198
  };
@@ -4352,7 +4258,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
4352
4258
  }
4353
4259
  }
4354
4260
  const state = await runPipeline(ctx);
4355
- const files = fs28.readdirSync(taskDir);
4261
+ const files = fs21.readdirSync(taskDir);
4356
4262
  console.log(`
4357
4263
  Artifacts in ${taskDir}:`);
4358
4264
  for (const f of files) {
@@ -4416,21 +4322,20 @@ var init_entry = __esm({
4416
4322
  });
4417
4323
 
4418
4324
  // src/bin/cli.ts
4419
- import * as fs29 from "fs";
4420
- import * as path27 from "path";
4325
+ import * as fs22 from "fs";
4326
+ import * as path21 from "path";
4327
+ import { execFileSync as execFileSync12 } from "child_process";
4421
4328
  import { fileURLToPath } from "url";
4422
-
4423
- // src/bin/commands/init.ts
4424
- import * as fs3 from "fs";
4425
- import * as path2 from "path";
4426
- import { execFileSync as execFileSync3 } from "child_process";
4427
-
4428
- // src/bin/health-checks.ts
4429
- import * as fs from "fs";
4430
- import { execFileSync } from "child_process";
4431
- function checkCommand(name, args2, fix) {
4329
+ var __dirname = path21.dirname(fileURLToPath(import.meta.url));
4330
+ var PKG_ROOT = path21.resolve(__dirname, "..", "..");
4331
+ function getVersion() {
4332
+ const pkgPath = path21.join(PKG_ROOT, "package.json");
4333
+ const pkg = JSON.parse(fs22.readFileSync(pkgPath, "utf-8"));
4334
+ return pkg.version;
4335
+ }
4336
+ function checkCommand2(name, args2, fix) {
4432
4337
  try {
4433
- const output = execFileSync(name, args2, {
4338
+ const output = execFileSync12(name, args2, {
4434
4339
  encoding: "utf-8",
4435
4340
  timeout: 1e4,
4436
4341
  stdio: ["pipe", "pipe", "pipe"]
@@ -4441,14 +4346,14 @@ function checkCommand(name, args2, fix) {
4441
4346
  }
4442
4347
  }
4443
4348
  function checkFile(filePath, description, fix) {
4444
- if (fs.existsSync(filePath)) {
4349
+ if (fs22.existsSync(filePath)) {
4445
4350
  return { name: description, ok: true, detail: filePath };
4446
4351
  }
4447
4352
  return { name: description, ok: false, fix };
4448
4353
  }
4449
4354
  function checkGhAuth(cwd) {
4450
4355
  try {
4451
- const output = execFileSync("gh", ["auth", "status"], {
4356
+ const output = execFileSync12("gh", ["auth", "status"], {
4452
4357
  encoding: "utf-8",
4453
4358
  timeout: 1e4,
4454
4359
  cwd,
@@ -4466,7 +4371,7 @@ function checkGhAuth(cwd) {
4466
4371
  }
4467
4372
  function checkGhRepoAccess(cwd) {
4468
4373
  try {
4469
- const remote = execFileSync("git", ["remote", "get-url", "origin"], {
4374
+ const remote = execFileSync12("git", ["remote", "get-url", "origin"], {
4470
4375
  encoding: "utf-8",
4471
4376
  timeout: 5e3,
4472
4377
  cwd,
@@ -4477,7 +4382,7 @@ function checkGhRepoAccess(cwd) {
4477
4382
  return { name: "GitHub repo", ok: false, fix: "Set git remote origin to a GitHub URL" };
4478
4383
  }
4479
4384
  const repoSlug = `${match[1]}/${match[2]}`;
4480
- execFileSync("gh", ["repo", "view", repoSlug, "--json", "name"], {
4385
+ execFileSync12("gh", ["repo", "view", repoSlug, "--json", "name"], {
4481
4386
  encoding: "utf-8",
4482
4387
  timeout: 1e4,
4483
4388
  cwd,
@@ -4490,7 +4395,7 @@ function checkGhRepoAccess(cwd) {
4490
4395
  }
4491
4396
  function checkGhSecret(repoSlug, secretName) {
4492
4397
  try {
4493
- const output = execFileSync("gh", ["secret", "list", "--repo", repoSlug], {
4398
+ const output = execFileSync12("gh", ["secret", "list", "--repo", repoSlug], {
4494
4399
  encoding: "utf-8",
4495
4400
  timeout: 1e4,
4496
4401
  stdio: ["pipe", "pipe", "pipe"]
@@ -4511,20 +4416,14 @@ function checkGhSecret(repoSlug, secretName) {
4511
4416
  };
4512
4417
  }
4513
4418
  }
4514
-
4515
- // src/bin/config-detection.ts
4516
- import * as fs2 from "fs";
4517
- import * as path from "path";
4518
- import { execFileSync as execFileSync2 } from "child_process";
4519
- var FRONTEND_DEPS = ["next", "react", "vue", "svelte", "nuxt", "astro", "solid-js", "angular", "@angular/core"];
4520
4419
  function detectBasicConfig(cwd) {
4521
4420
  let pm = "pnpm";
4522
- if (fs2.existsSync(path.join(cwd, "yarn.lock"))) pm = "yarn";
4523
- else if (fs2.existsSync(path.join(cwd, "bun.lockb"))) pm = "bun";
4524
- else if (!fs2.existsSync(path.join(cwd, "pnpm-lock.yaml")) && fs2.existsSync(path.join(cwd, "package-lock.json"))) pm = "npm";
4421
+ if (fs22.existsSync(path21.join(cwd, "yarn.lock"))) pm = "yarn";
4422
+ else if (fs22.existsSync(path21.join(cwd, "bun.lockb"))) pm = "bun";
4423
+ else if (!fs22.existsSync(path21.join(cwd, "pnpm-lock.yaml")) && fs22.existsSync(path21.join(cwd, "package-lock.json"))) pm = "npm";
4525
4424
  let defaultBranch = "main";
4526
4425
  try {
4527
- const ref = execFileSync2("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
4426
+ const ref = execFileSync12("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
4528
4427
  encoding: "utf-8",
4529
4428
  timeout: 5e3,
4530
4429
  cwd,
@@ -4533,7 +4432,7 @@ function detectBasicConfig(cwd) {
4533
4432
  defaultBranch = ref.replace("refs/remotes/origin/", "");
4534
4433
  } catch {
4535
4434
  try {
4536
- execFileSync2("git", ["rev-parse", "--verify", "origin/dev"], {
4435
+ execFileSync12("git", ["rev-parse", "--verify", "origin/dev"], {
4537
4436
  encoding: "utf-8",
4538
4437
  timeout: 5e3,
4539
4438
  cwd,
@@ -4546,7 +4445,7 @@ function detectBasicConfig(cwd) {
4546
4445
  let owner = "";
4547
4446
  let repo = "";
4548
4447
  try {
4549
- const remote = execFileSync2("git", ["remote", "get-url", "origin"], {
4448
+ const remote = execFileSync12("git", ["remote", "get-url", "origin"], {
4550
4449
  encoding: "utf-8",
4551
4450
  timeout: 5e3,
4552
4451
  cwd,
@@ -4564,7 +4463,7 @@ function detectBasicConfig(cwd) {
4564
4463
  function buildConfig(cwd, basic) {
4565
4464
  const pkg = (() => {
4566
4465
  try {
4567
- return JSON.parse(fs2.readFileSync(path.join(cwd, "package.json"), "utf-8"));
4466
+ return JSON.parse(fs22.readFileSync(path21.join(cwd, "package.json"), "utf-8"));
4568
4467
  } catch {
4569
4468
  return {};
4570
4469
  }
@@ -4596,6 +4495,7 @@ function buildConfig(cwd, basic) {
4596
4495
  if (mcp) config.mcp = mcp;
4597
4496
  return config;
4598
4497
  }
4498
+ var FRONTEND_DEPS = ["next", "react", "vue", "svelte", "nuxt", "astro", "solid-js", "angular", "@angular/core"];
4599
4499
  function detectMcpConfig(cwd, pm, pkg) {
4600
4500
  const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
4601
4501
  const hasFrontend = FRONTEND_DEPS.some((dep) => dep in allDeps);
@@ -4607,7 +4507,12 @@ function detectMcpConfig(cwd, pm, pkg) {
4607
4507
  const defaultPort = isNext ? 3e3 : isVite ? 5173 : 3e3;
4608
4508
  const mcp = {
4609
4509
  enabled: true,
4610
- servers: {},
4510
+ servers: {
4511
+ playwright: {
4512
+ command: "npx",
4513
+ args: ["@playwright/mcp@latest"]
4514
+ }
4515
+ },
4611
4516
  stages: ["build", "review"]
4612
4517
  };
4613
4518
  if (hasDevScript) {
@@ -4618,9 +4523,7 @@ function detectMcpConfig(cwd, pm, pkg) {
4618
4523
  }
4619
4524
  return mcp;
4620
4525
  }
4621
-
4622
- // src/bin/commands/init.ts
4623
- function initCommand(opts, pkgRoot) {
4526
+ function initCommand(opts) {
4624
4527
  const cwd = process.cwd();
4625
4528
  console.log(`
4626
4529
  \u{1F527} Kody Engine Lite \u2014 Init
@@ -4628,35 +4531,35 @@ function initCommand(opts, pkgRoot) {
4628
4531
  console.log(`Project: ${cwd}
4629
4532
  `);
4630
4533
  console.log("\u2500\u2500 Files \u2500\u2500");
4631
- const templatesDir = path2.join(pkgRoot, "templates");
4534
+ const templatesDir = path21.join(PKG_ROOT, "templates");
4632
4535
  const basic = detectBasicConfig(cwd);
4633
- const workflowSrc = path2.join(templatesDir, "kody.yml");
4634
- const workflowDest = path2.join(cwd, ".github", "workflows", "kody.yml");
4635
- if (!fs3.existsSync(workflowSrc)) {
4536
+ const workflowSrc = path21.join(templatesDir, "kody.yml");
4537
+ const workflowDest = path21.join(cwd, ".github", "workflows", "kody.yml");
4538
+ if (!fs22.existsSync(workflowSrc)) {
4636
4539
  console.error(" \u2717 Template kody.yml not found in package");
4637
4540
  process.exit(1);
4638
4541
  }
4639
- if (fs3.existsSync(workflowDest) && !opts.force) {
4542
+ if (fs22.existsSync(workflowDest) && !opts.force) {
4640
4543
  console.log(" \u25CB .github/workflows/kody.yml (exists, use --force to overwrite)");
4641
4544
  } else {
4642
- fs3.mkdirSync(path2.dirname(workflowDest), { recursive: true });
4643
- fs3.copyFileSync(workflowSrc, workflowDest);
4545
+ fs22.mkdirSync(path21.dirname(workflowDest), { recursive: true });
4546
+ fs22.copyFileSync(workflowSrc, workflowDest);
4644
4547
  console.log(" \u2713 .github/workflows/kody.yml");
4645
4548
  }
4646
- const configDest = path2.join(cwd, "kody.config.json");
4647
- if (!fs3.existsSync(configDest) || opts.force) {
4549
+ const configDest = path21.join(cwd, "kody.config.json");
4550
+ if (!fs22.existsSync(configDest) || opts.force) {
4648
4551
  const config = buildConfig(cwd, basic);
4649
- fs3.writeFileSync(configDest, JSON.stringify(config, null, 2) + "\n");
4552
+ fs22.writeFileSync(configDest, JSON.stringify(config, null, 2) + "\n");
4650
4553
  console.log(" \u2713 kody.config.json (auto-configured)");
4651
4554
  } else {
4652
4555
  console.log(" \u25CB kody.config.json (exists)");
4653
4556
  }
4654
- const gitignorePath = path2.join(cwd, ".gitignore");
4655
- if (fs3.existsSync(gitignorePath)) {
4656
- const content = fs3.readFileSync(gitignorePath, "utf-8");
4557
+ const gitignorePath = path21.join(cwd, ".gitignore");
4558
+ if (fs22.existsSync(gitignorePath)) {
4559
+ const content = fs22.readFileSync(gitignorePath, "utf-8");
4657
4560
  if (content.includes(".tasks/")) {
4658
4561
  const updated = content.replace(/\n?\.tasks\/\n?/g, "\n");
4659
- fs3.writeFileSync(gitignorePath, updated);
4562
+ fs22.writeFileSync(gitignorePath, updated);
4660
4563
  console.log(" \u2713 .gitignore (removed legacy .tasks/ \u2014 tasks now committed in .kody/tasks/)");
4661
4564
  } else {
4662
4565
  console.log(" \u25CB .gitignore (ok)");
@@ -4664,10 +4567,10 @@ function initCommand(opts, pkgRoot) {
4664
4567
  }
4665
4568
  console.log("\n\u2500\u2500 Prerequisites \u2500\u2500");
4666
4569
  const checks = [
4667
- checkCommand("gh", ["--version"], "Install: https://cli.github.com"),
4668
- checkCommand("git", ["--version"], "Install git"),
4669
- checkCommand("node", ["--version"], "Install Node.js >= 22"),
4670
- checkFile(path2.join(cwd, "package.json"), "package.json", `Run: ${basic.pm} init`)
4570
+ checkCommand2("gh", ["--version"], "Install: https://cli.github.com"),
4571
+ checkCommand2("git", ["--version"], "Install git"),
4572
+ checkCommand2("node", ["--version"], "Install Node.js >= 22"),
4573
+ checkFile(path21.join(cwd, "package.json"), "package.json", `Run: ${basic.pm} init`)
4671
4574
  ];
4672
4575
  for (const c of checks) {
4673
4576
  if (c.ok) {
@@ -4681,19 +4584,26 @@ function initCommand(opts, pkgRoot) {
4681
4584
  console.log(ghAuth.ok ? ` \u2713 ${ghAuth.name} (${ghAuth.detail})` : ` \u2717 ${ghAuth.name} \u2014 ${ghAuth.fix}`);
4682
4585
  const ghRepo = checkGhRepoAccess(cwd);
4683
4586
  console.log(ghRepo.ok ? ` \u2713 ${ghRepo.name} (${ghRepo.detail})` : ` \u2717 ${ghRepo.name} \u2014 ${ghRepo.fix}`);
4587
+ let repoSlug = "";
4684
4588
  if (ghRepo.ok && ghRepo.detail) {
4685
- const repoSlug = ghRepo.detail;
4686
- const secretChecks = [checkGhSecret(repoSlug, "ANTHROPIC_API_KEY")];
4589
+ repoSlug = ghRepo.detail;
4590
+ const secretChecks = [
4591
+ checkGhSecret(repoSlug, "ANTHROPIC_API_KEY")
4592
+ ];
4687
4593
  for (const c of secretChecks) {
4688
- console.log(c.ok ? ` \u2713 ${c.name}` : ` \u2717 ${c.name} \u2014 ${c.fix}`);
4594
+ if (c.ok) {
4595
+ console.log(` \u2713 ${c.name}`);
4596
+ } else {
4597
+ console.log(` \u2717 ${c.name} \u2014 ${c.fix}`);
4598
+ }
4689
4599
  }
4690
4600
  console.log("\n\u2500\u2500 Labels \u2500\u2500");
4691
4601
  console.log(" \u25CB Labels will be created automatically during bootstrap");
4692
4602
  }
4693
4603
  console.log("\n\u2500\u2500 Config \u2500\u2500");
4694
- if (fs3.existsSync(configDest)) {
4604
+ if (fs22.existsSync(configDest)) {
4695
4605
  try {
4696
- const config = JSON.parse(fs3.readFileSync(configDest, "utf-8"));
4606
+ const config = JSON.parse(fs22.readFileSync(configDest, "utf-8"));
4697
4607
  const configChecks = [];
4698
4608
  if (config.github?.owner && config.github?.repo) {
4699
4609
  configChecks.push({ name: "github.owner/repo", ok: true, detail: `${config.github.owner}/${config.github.repo}` });
@@ -4723,11 +4633,11 @@ function initCommand(opts, pkgRoot) {
4723
4633
  const filesToCommit = [
4724
4634
  ".github/workflows/kody.yml",
4725
4635
  "kody.config.json"
4726
- ].filter((f) => fs3.existsSync(path2.join(cwd, f)));
4636
+ ].filter((f) => fs22.existsSync(path21.join(cwd, f)));
4727
4637
  if (filesToCommit.length > 0) {
4728
4638
  try {
4729
- const fullPaths = filesToCommit.map((f) => path2.join(cwd, f));
4730
- execFileSync3("npx", ["prettier", "--write", ...fullPaths], {
4639
+ const fullPaths = filesToCommit.map((f) => path21.join(cwd, f));
4640
+ execFileSync12("npx", ["prettier", "--write", ...fullPaths], {
4731
4641
  cwd,
4732
4642
  encoding: "utf-8",
4733
4643
  timeout: 3e4,
@@ -4738,13 +4648,13 @@ function initCommand(opts, pkgRoot) {
4738
4648
  }
4739
4649
  if (filesToCommit.length > 0) {
4740
4650
  try {
4741
- execFileSync3("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
4742
- const staged = execFileSync3("git", ["diff", "--cached", "--name-only"], { cwd, encoding: "utf-8" }).trim();
4651
+ execFileSync12("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
4652
+ const staged = execFileSync12("git", ["diff", "--cached", "--name-only"], { cwd, encoding: "utf-8" }).trim();
4743
4653
  if (staged) {
4744
- execFileSync3("git", ["commit", "-m", "chore: Add Kody Engine workflow and config\n\nAdd GitHub Actions workflow and auto-detected configuration for Kody Engine Lite."], { cwd, stdio: "pipe" });
4654
+ execFileSync12("git", ["commit", "-m", "chore: Add Kody Engine workflow and config\n\nAdd GitHub Actions workflow and auto-detected configuration for Kody Engine Lite."], { cwd, stdio: "pipe" });
4745
4655
  console.log(` \u2713 Committed: ${filesToCommit.join(", ")}`);
4746
4656
  try {
4747
- execFileSync3("git", ["push"], { cwd, stdio: "pipe", timeout: 6e4 });
4657
+ execFileSync12("git", ["push"], { cwd, stdio: "pipe", timeout: 6e4 });
4748
4658
  console.log(" \u2713 Pushed to origin");
4749
4659
  } catch {
4750
4660
  console.log(" \u25CB Push failed \u2014 run 'git push' manually");
@@ -4786,300 +4696,22 @@ function initCommand(opts, pkgRoot) {
4786
4696
  console.log("");
4787
4697
  }
4788
4698
  }
4789
-
4790
- // src/bin/commands/bootstrap.ts
4791
- init_architecture_detection();
4792
- import * as fs7 from "fs";
4793
- import * as path6 from "path";
4794
- import { execFileSync as execFileSync5 } from "child_process";
4795
-
4796
- // src/bin/qa-guide.ts
4797
- import * as fs5 from "fs";
4798
- import * as path4 from "path";
4799
- function discoverQaContext(cwd) {
4800
- const result = {
4801
- routes: [],
4802
- authFiles: [],
4803
- loginPage: null,
4804
- adminPath: null,
4805
- roles: [],
4806
- devCommand: "",
4807
- devPort: 3e3
4808
- };
4809
- try {
4810
- const pkg = JSON.parse(fs5.readFileSync(path4.join(cwd, "package.json"), "utf-8"));
4811
- const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
4812
- const pm = fs5.existsSync(path4.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs5.existsSync(path4.join(cwd, "yarn.lock")) ? "yarn" : "npm";
4813
- if (pkg.scripts?.dev) result.devCommand = `${pm} dev`;
4814
- if (allDeps.next || allDeps.nuxt) result.devPort = 3e3;
4815
- else if (allDeps.vite) result.devPort = 5173;
4816
- } catch {
4817
- }
4818
- const appDirs = ["src/app", "app"];
4819
- for (const appDir of appDirs) {
4820
- const fullAppDir = path4.join(cwd, appDir);
4821
- if (!fs5.existsSync(fullAppDir)) continue;
4822
- scanRoutes(fullAppDir, appDir, "", result);
4823
- break;
4824
- }
4825
- const authPatterns = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
4826
- for (const p of authPatterns) {
4827
- if (fs5.existsSync(path4.join(cwd, p))) result.authFiles.push(p);
4828
- }
4829
- const authConfigGlobs = [
4830
- "src/app/api/auth",
4831
- "src/auth",
4832
- "src/lib/auth",
4833
- "auth.config.ts",
4834
- "auth.ts",
4835
- "src/app/api/oauth"
4836
- ];
4837
- for (const g of authConfigGlobs) {
4838
- if (fs5.existsSync(path4.join(cwd, g))) result.authFiles.push(g);
4839
- }
4840
- try {
4841
- const rolePaths = [
4842
- "src/types",
4843
- "src/lib",
4844
- "src/utils",
4845
- "src/constants",
4846
- "src/access",
4847
- "src/collections"
4848
- ];
4849
- for (const rp of rolePaths) {
4850
- const dir = path4.join(cwd, rp);
4851
- if (!fs5.existsSync(dir)) continue;
4852
- const files = fs5.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
4853
- for (const f of files) {
4854
- try {
4855
- const content = fs5.readFileSync(path4.join(dir, f), "utf-8").slice(0, 5e3);
4856
- const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
4857
- if (roleMatches) {
4858
- for (const m of roleMatches) {
4859
- const val = m.match(/['"](\w+)['"]/);
4860
- if (val && !result.roles.includes(val[1])) result.roles.push(val[1]);
4861
- }
4862
- }
4863
- const enumMatch = content.match(/(?:enum|type)\s+\w*[Rr]ole\w*\s*[={]([^}]+)/s);
4864
- if (enumMatch) {
4865
- const vals = enumMatch[1].match(/['"](\w+)['"]/g);
4866
- if (vals) {
4867
- for (const v of vals) {
4868
- const clean = v.replace(/['"]/g, "");
4869
- if (!result.roles.includes(clean)) result.roles.push(clean);
4870
- }
4871
- }
4872
- }
4873
- } catch {
4874
- }
4875
- }
4876
- }
4877
- } catch {
4878
- }
4879
- return result;
4880
- }
4881
- function scanRoutes(dir, baseDir, prefix, result) {
4882
- let entries;
4883
- try {
4884
- entries = fs5.readdirSync(dir, { withFileTypes: true });
4885
- } catch {
4886
- return;
4887
- }
4888
- const hasPage = entries.some((e) => e.isFile() && /^page\.(tsx?|jsx?)$/.test(e.name));
4889
- if (hasPage) {
4890
- const routePath = prefix || "/";
4891
- const group = prefix.startsWith("/admin") ? "admin" : prefix.includes("/login") ? "auth" : prefix.includes("/signup") ? "auth" : prefix.includes("/api") ? "api" : "frontend";
4892
- result.routes.push({ path: routePath, group });
4893
- if (prefix.includes("/login")) result.loginPage = routePath;
4894
- if (prefix.startsWith("/admin") && !result.adminPath) result.adminPath = prefix;
4895
- }
4896
- for (const entry of entries) {
4897
- if (!entry.isDirectory()) continue;
4898
- if (entry.name === "node_modules" || entry.name === ".next") continue;
4899
- let segment = entry.name;
4900
- if (segment.startsWith("(") && segment.endsWith(")")) {
4901
- scanRoutes(path4.join(dir, entry.name), baseDir, prefix, result);
4902
- continue;
4903
- }
4904
- if (segment.startsWith("[") && segment.endsWith("]")) {
4905
- segment = `:${segment.slice(1, -1)}`;
4906
- }
4907
- if (segment.startsWith("[[") && segment.endsWith("]]")) {
4908
- segment = `:${segment.slice(2, -2)}?`;
4909
- }
4910
- scanRoutes(path4.join(dir, entry.name), baseDir, `${prefix}/${segment}`, result);
4911
- }
4912
- }
4913
- function generateQaGuide(discovery) {
4914
- const lines = ["# QA Guide", "", "## Authentication", ""];
4915
- if (discovery.loginPage) {
4916
- lines.push(`- Login page: \`${discovery.loginPage}\``);
4917
- }
4918
- lines.push(
4919
- "",
4920
- "### Test Accounts",
4921
- "<!-- Fill in your test/preview environment credentials below -->",
4922
- "| Role | Email | Password |",
4923
- "|------|-------|----------|",
4924
- "| Admin | admin@example.com | CHANGE_ME |",
4925
- "| User | user@example.com | CHANGE_ME |",
4926
- "",
4927
- "### Login Steps",
4928
- `1. Navigate to \`${discovery.loginPage ?? "/login"}\``,
4929
- "2. Enter credentials from the test accounts table above",
4930
- "3. Submit the login form",
4931
- "4. Verify redirect to dashboard or home page"
4932
- );
4933
- if (discovery.authFiles.length > 0) {
4934
- lines.push("", "### Auth Files");
4935
- for (const f of discovery.authFiles) {
4936
- lines.push(`- \`${f}\``);
4937
- }
4938
- }
4939
- if (discovery.roles.length > 0) {
4940
- lines.push("", "## Roles", "");
4941
- for (const role of discovery.roles) {
4942
- lines.push(`- \`${role}\``);
4943
- }
4944
- }
4945
- lines.push("", "## Key Pages", "");
4946
- const groups = {};
4947
- for (const route of discovery.routes) {
4948
- if (!groups[route.group]) groups[route.group] = [];
4949
- groups[route.group].push(route.path);
4950
- }
4951
- for (const [group, routes] of Object.entries(groups)) {
4952
- lines.push(`### ${group.charAt(0).toUpperCase() + group.slice(1)}`);
4953
- const sorted = routes.sort();
4954
- for (const r of sorted.slice(0, 20)) {
4955
- lines.push(`- \`${r}\``);
4956
- }
4957
- if (sorted.length > 20) {
4958
- lines.push(`- ... and ${sorted.length - 20} more`);
4959
- }
4960
- lines.push("");
4961
- }
4962
- lines.push(
4963
- "## Dev Server",
4964
- "",
4965
- `- Command: \`${discovery.devCommand || "pnpm dev"}\``,
4966
- `- URL: \`http://localhost:${discovery.devPort}\``,
4967
- ""
4968
- );
4969
- return lines.join("\n");
4970
- }
4971
-
4972
- // src/bin/skills.ts
4973
- import * as fs6 from "fs";
4974
- import * as path5 from "path";
4975
- import { execFileSync as execFileSync4 } from "child_process";
4976
- var SKILL_MAPPINGS = [
4977
- {
4978
- detect: (deps) => "next" in deps,
4979
- skills: [
4980
- { package: "vercel-labs/agent-skills@vercel-react-best-practices", label: "React best practices (Vercel)" }
4981
- ]
4982
- },
4983
- {
4984
- detect: (deps) => "react" in deps && !("next" in deps),
4985
- skills: [
4986
- { package: "vercel-labs/agent-skills@vercel-react-best-practices", label: "React best practices (Vercel)" }
4987
- ]
4988
- },
4989
- {
4990
- detect: (deps) => FRONTEND_DEPS.some((d) => d in deps),
4991
- skills: [
4992
- { package: "microsoft/playwright-cli@playwright-cli", label: "Playwright browser automation" }
4993
- ]
4994
- }
4995
- ];
4996
- function detectSkillsForProject(cwd) {
4997
- const pkgPath = path5.join(cwd, "package.json");
4998
- if (!fs6.existsSync(pkgPath)) return [];
4999
- try {
5000
- const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
5001
- const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
5002
- const seen = /* @__PURE__ */ new Set();
5003
- const skills = [];
5004
- for (const mapping of SKILL_MAPPINGS) {
5005
- if (mapping.detect(allDeps)) {
5006
- for (const skill of mapping.skills) {
5007
- if (!seen.has(skill.package)) {
5008
- seen.add(skill.package);
5009
- skills.push(skill);
5010
- }
5011
- }
5012
- }
5013
- }
5014
- return skills;
5015
- } catch {
5016
- return [];
5017
- }
5018
- }
5019
- function installSkillsForProject(cwd) {
5020
- const skills = detectSkillsForProject(cwd);
5021
- if (skills.length === 0) {
5022
- console.log(" \u25CB No skills to install (no frontend framework detected)");
5023
- return [];
5024
- }
5025
- let installedSkills = {};
5026
- const lockPath = path5.join(cwd, "skills-lock.json");
5027
- if (fs6.existsSync(lockPath)) {
5028
- try {
5029
- const lock = JSON.parse(fs6.readFileSync(lockPath, "utf-8"));
5030
- installedSkills = lock.skills ?? {};
5031
- } catch {
5032
- }
5033
- }
5034
- const installedPaths = [];
5035
- for (const skill of skills) {
5036
- const skillName = skill.package.split("@").pop() ?? "";
5037
- if (skillName in installedSkills) {
5038
- console.log(` \u25CB ${skill.label} \u2014 already installed`);
5039
- const agentPath = `.agents/skills/${skillName}`;
5040
- const claudePath = `.claude/skills/${skillName}`;
5041
- if (fs6.existsSync(path5.join(cwd, agentPath))) installedPaths.push(agentPath);
5042
- if (fs6.existsSync(path5.join(cwd, claudePath))) installedPaths.push(claudePath);
5043
- continue;
5044
- }
5045
- try {
5046
- console.log(` Installing: ${skill.label} (${skill.package})`);
5047
- execFileSync4("npx", ["skills", "add", skill.package, "--yes"], {
5048
- cwd,
5049
- encoding: "utf-8",
5050
- timeout: 6e4,
5051
- stdio: ["pipe", "pipe", "pipe"]
5052
- });
5053
- const installedName = skill.package.split("@").pop() ?? "";
5054
- const agentPath = `.agents/skills/${installedName}`;
5055
- const claudePath = `.claude/skills/${installedName}`;
5056
- if (fs6.existsSync(path5.join(cwd, agentPath))) installedPaths.push(agentPath);
5057
- if (fs6.existsSync(path5.join(cwd, claudePath))) installedPaths.push(claudePath);
5058
- console.log(` \u2713 ${skill.label}`);
5059
- } catch {
5060
- console.log(` \u2717 ${skill.label} \u2014 failed to install`);
5061
- }
5062
- }
5063
- return installedPaths;
5064
- }
5065
-
5066
- // src/bin/commands/bootstrap.ts
5067
4699
  var STEP_STAGES = ["taskify", "plan", "build", "autofix", "review", "review-fix"];
5068
4700
  function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
5069
- const srcDir = path6.join(cwd, "src");
5070
- const baseDir = fs7.existsSync(srcDir) ? srcDir : cwd;
4701
+ const srcDir = path21.join(cwd, "src");
4702
+ const baseDir = fs22.existsSync(srcDir) ? srcDir : cwd;
5071
4703
  const results = [];
5072
4704
  function walk(dir) {
5073
4705
  const entries = [];
5074
4706
  try {
5075
- for (const entry of fs7.readdirSync(dir, { withFileTypes: true })) {
4707
+ for (const entry of fs22.readdirSync(dir, { withFileTypes: true })) {
5076
4708
  if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
5077
- const full = path6.join(dir, entry.name);
4709
+ const full = path21.join(dir, entry.name);
5078
4710
  if (entry.isDirectory()) {
5079
4711
  entries.push(...walk(full));
5080
4712
  } else if (/\.(ts|js)$/.test(entry.name) && !/\.(test|spec|config|d)\.(ts|js)$/.test(entry.name)) {
5081
4713
  try {
5082
- const stat = fs7.statSync(full);
4714
+ const stat = fs22.statSync(full);
5083
4715
  if (stat.size >= 200 && stat.size <= 5e3) {
5084
4716
  entries.push({ filePath: full, size: stat.size });
5085
4717
  }
@@ -5093,8 +4725,8 @@ function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
5093
4725
  }
5094
4726
  const files = walk(baseDir).sort((a, b) => b.size - a.size).slice(0, maxFiles);
5095
4727
  for (const { filePath } of files) {
5096
- const rel = path6.relative(cwd, filePath);
5097
- const content = fs7.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
4728
+ const rel = path21.relative(cwd, filePath);
4729
+ const content = fs22.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
5098
4730
  results.push(`### File: ${rel}
5099
4731
  \`\`\`typescript
5100
4732
  ${content}
@@ -5106,9 +4738,9 @@ function ghComment(issueNumber, body, cwd) {
5106
4738
  try {
5107
4739
  let repoSlug = "";
5108
4740
  try {
5109
- const configPath = path6.join(cwd, "kody.config.json");
5110
- if (fs7.existsSync(configPath)) {
5111
- const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
4741
+ const configPath = path21.join(cwd, "kody.config.json");
4742
+ if (fs22.existsSync(configPath)) {
4743
+ const config = JSON.parse(fs22.readFileSync(configPath, "utf-8"));
5112
4744
  if (config.github?.owner && config.github?.repo) {
5113
4745
  repoSlug = `${config.github.owner}/${config.github.repo}`;
5114
4746
  }
@@ -5116,7 +4748,7 @@ function ghComment(issueNumber, body, cwd) {
5116
4748
  } catch {
5117
4749
  }
5118
4750
  if (!repoSlug) return;
5119
- execFileSync5("gh", [
4751
+ execFileSync12("gh", [
5120
4752
  "issue",
5121
4753
  "comment",
5122
4754
  String(issueNumber),
@@ -5133,7 +4765,7 @@ function ghComment(issueNumber, body, cwd) {
5133
4765
  } catch {
5134
4766
  }
5135
4767
  }
5136
- function bootstrapCommand(opts, pkgRoot) {
4768
+ function bootstrapCommand(opts = { force: false }) {
5137
4769
  const cwd = process.cwd();
5138
4770
  const issueNumber = parseInt(process.env.ISSUE_NUMBER ?? "", 10) || 0;
5139
4771
  console.log(`
@@ -5143,8 +4775,8 @@ function bootstrapCommand(opts, pkgRoot) {
5143
4775
  ghComment(issueNumber, "\u{1F527} **Bootstrap started** \u2014 analyzing project and generating configuration...", cwd);
5144
4776
  }
5145
4777
  const readIfExists = (rel, maxChars = 3e3) => {
5146
- const p = path6.join(cwd, rel);
5147
- if (fs7.existsSync(p)) return fs7.readFileSync(p, "utf-8").slice(0, maxChars);
4778
+ const p = path21.join(cwd, rel);
4779
+ if (fs22.existsSync(p)) return fs22.readFileSync(p, "utf-8").slice(0, maxChars);
5148
4780
  return null;
5149
4781
  };
5150
4782
  let repoContext = "";
@@ -5179,14 +4811,14 @@ ${sampleFiles}
5179
4811
 
5180
4812
  `;
5181
4813
  try {
5182
- const topDirs = fs7.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
4814
+ const topDirs = fs22.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
5183
4815
  repoContext += `## Top-level directories
5184
4816
  ${topDirs.join(", ")}
5185
4817
 
5186
4818
  `;
5187
- const srcDir = path6.join(cwd, "src");
5188
- if (fs7.existsSync(srcDir)) {
5189
- const srcDirs = fs7.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
4819
+ const srcDir = path21.join(cwd, "src");
4820
+ if (fs22.existsSync(srcDir)) {
4821
+ const srcDirs = fs22.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
5190
4822
  if (srcDirs.length > 0) repoContext += `## src/ subdirectories
5191
4823
  ${srcDirs.join(", ")}
5192
4824
 
@@ -5196,19 +4828,19 @@ ${srcDirs.join(", ")}
5196
4828
  }
5197
4829
  const existingFiles = [];
5198
4830
  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"]) {
5199
- if (fs7.existsSync(path6.join(cwd, f))) existingFiles.push(f);
4831
+ if (fs22.existsSync(path21.join(cwd, f))) existingFiles.push(f);
5200
4832
  }
5201
4833
  if (existingFiles.length) repoContext += `## Config files present
5202
4834
  ${existingFiles.join(", ")}
5203
4835
 
5204
4836
  `;
5205
4837
  console.log("\u2500\u2500 Project Memory \u2500\u2500");
5206
- const memoryDir = path6.join(cwd, ".kody", "memory");
5207
- fs7.mkdirSync(memoryDir, { recursive: true });
5208
- const archPath = path6.join(memoryDir, "architecture.md");
5209
- const conventionsPath = path6.join(memoryDir, "conventions.md");
5210
- const existingArch = fs7.existsSync(archPath) ? fs7.readFileSync(archPath, "utf-8") : "";
5211
- const existingConv = fs7.existsSync(conventionsPath) ? fs7.readFileSync(conventionsPath, "utf-8") : "";
4838
+ const memoryDir = path21.join(cwd, ".kody", "memory");
4839
+ fs22.mkdirSync(memoryDir, { recursive: true });
4840
+ const archPath = path21.join(memoryDir, "architecture.md");
4841
+ const conventionsPath = path21.join(memoryDir, "conventions.md");
4842
+ const existingArch = fs22.existsSync(archPath) ? fs22.readFileSync(archPath, "utf-8") : "";
4843
+ const existingConv = fs22.existsSync(conventionsPath) ? fs22.readFileSync(conventionsPath, "utf-8") : "";
5212
4844
  const hasExisting = !!(existingArch || existingConv);
5213
4845
  const extendInstruction = hasExisting && !opts.force ? `
5214
4846
  ## Existing Documentation (EXTEND, do not replace)
@@ -5249,7 +4881,7 @@ Output ONLY valid JSON. No markdown fences. No explanation.
5249
4881
  ${repoContext}`;
5250
4882
  console.log(" \u23F3 Analyzing project...");
5251
4883
  try {
5252
- const output = execFileSync5("claude", [
4884
+ const output = execFileSync12("claude", [
5253
4885
  "--print",
5254
4886
  "--model",
5255
4887
  "haiku",
@@ -5264,12 +4896,12 @@ ${repoContext}`;
5264
4896
  const cleaned = output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
5265
4897
  const parsed = JSON.parse(cleaned);
5266
4898
  if (parsed.architecture) {
5267
- fs7.writeFileSync(archPath, parsed.architecture);
4899
+ fs22.writeFileSync(archPath, parsed.architecture);
5268
4900
  const lineCount = parsed.architecture.split("\n").length;
5269
4901
  console.log(` \u2713 .kody/memory/architecture.md (${lineCount} lines)`);
5270
4902
  }
5271
4903
  if (parsed.conventions) {
5272
- fs7.writeFileSync(conventionsPath, parsed.conventions);
4904
+ fs22.writeFileSync(conventionsPath, parsed.conventions);
5273
4905
  const lineCount = parsed.conventions.split("\n").length;
5274
4906
  console.log(` \u2713 .kody/memory/conventions.md (${lineCount} lines)`);
5275
4907
  }
@@ -5278,39 +4910,39 @@ ${repoContext}`;
5278
4910
  const detected = detectArchitectureBasic(cwd);
5279
4911
  if (detected.length > 0) {
5280
4912
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5281
- fs7.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
4913
+ fs22.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
5282
4914
 
5283
4915
  ## Overview
5284
4916
  ${detected.join("\n")}
5285
4917
  `);
5286
4918
  console.log(` \u2713 .kody/memory/architecture.md (${detected.length} items, basic detection)`);
5287
4919
  }
5288
- fs7.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
4920
+ fs22.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
5289
4921
  console.log(" \u2713 .kody/memory/conventions.md (seed)");
5290
4922
  }
5291
4923
  console.log("\n\u2500\u2500 Step Files \u2500\u2500");
5292
- const stepsDir = path6.join(cwd, ".kody", "steps");
5293
- fs7.mkdirSync(stepsDir, { recursive: true });
5294
- const arch = fs7.existsSync(archPath) ? fs7.readFileSync(archPath, "utf-8") : "";
5295
- const conv = fs7.existsSync(conventionsPath) ? fs7.readFileSync(conventionsPath, "utf-8") : "";
4924
+ const stepsDir = path21.join(cwd, ".kody", "steps");
4925
+ fs22.mkdirSync(stepsDir, { recursive: true });
4926
+ const arch = fs22.existsSync(archPath) ? fs22.readFileSync(archPath, "utf-8") : "";
4927
+ const conv = fs22.existsSync(conventionsPath) ? fs22.readFileSync(conventionsPath, "utf-8") : "";
5296
4928
  console.log(" \u23F3 Customizing step files...");
5297
4929
  let stepCount = 0;
5298
4930
  for (const stage of STEP_STAGES) {
5299
- const templatePath = path6.join(pkgRoot, "prompts", `${stage}.md`);
5300
- if (!fs7.existsSync(templatePath)) {
4931
+ const templatePath = path21.join(PKG_ROOT, "prompts", `${stage}.md`);
4932
+ if (!fs22.existsSync(templatePath)) {
5301
4933
  console.log(` \u2717 ${stage}.md \u2014 template not found in engine`);
5302
4934
  continue;
5303
4935
  }
5304
- const stepOutputPath = path6.join(stepsDir, `${stage}.md`);
5305
- if (fs7.existsSync(stepOutputPath) && !opts.force) {
4936
+ const stepOutputPath = path21.join(stepsDir, `${stage}.md`);
4937
+ if (fs22.existsSync(stepOutputPath) && !opts.force) {
5306
4938
  console.log(` \u25CB ${stage}.md \u2014 already exists (use --force to regenerate)`);
5307
4939
  continue;
5308
4940
  }
5309
- const defaultPrompt = fs7.readFileSync(templatePath, "utf-8");
4941
+ const defaultPrompt = fs22.readFileSync(templatePath, "utf-8");
5310
4942
  const contextPlaceholder = "{{TASK_CONTEXT}}";
5311
4943
  const placeholderIdx = defaultPrompt.indexOf(contextPlaceholder);
5312
4944
  if (placeholderIdx === -1) {
5313
- fs7.copyFileSync(templatePath, stepOutputPath);
4945
+ fs22.copyFileSync(templatePath, stepOutputPath);
5314
4946
  stepCount++;
5315
4947
  console.log(` \u2713 ${stage}.md`);
5316
4948
  continue;
@@ -5352,7 +4984,7 @@ ${repoContext}
5352
4984
 
5353
4985
  REMINDER: Output the full prompt template first (unchanged), then your three appended sections. Do NOT include "${contextPlaceholder}".`;
5354
4986
  try {
5355
- const output = execFileSync5("claude", [
4987
+ const output = execFileSync12("claude", [
5356
4988
  "--print",
5357
4989
  "--model",
5358
4990
  "haiku",
@@ -5367,40 +4999,23 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
5367
4999
  let cleaned = output.replace(/^```(?:markdown|md)?\s*\n?/, "").replace(/\n?```\s*$/, "");
5368
5000
  cleaned = cleaned.replace(/\n*\{\{TASK_CONTEXT\}\}\s*$/, "").trimEnd();
5369
5001
  const finalPrompt = cleaned + "\n\n" + afterPlaceholder;
5370
- fs7.writeFileSync(stepOutputPath, finalPrompt);
5002
+ fs22.writeFileSync(stepOutputPath, finalPrompt);
5371
5003
  stepCount++;
5372
5004
  console.log(` \u2713 ${stage}.md`);
5373
5005
  } catch {
5374
5006
  console.log(` \u26A0 ${stage}.md \u2014 customization failed, using default template`);
5375
- fs7.copyFileSync(templatePath, stepOutputPath);
5007
+ fs22.copyFileSync(templatePath, stepOutputPath);
5376
5008
  stepCount++;
5377
5009
  }
5378
5010
  }
5379
5011
  console.log(` \u2713 Generated ${stepCount} step files in .kody/steps/`);
5380
- console.log("\n\u2500\u2500 QA Guide \u2500\u2500");
5381
- const qaGuidePath = path6.join(cwd, ".kody", "qa-guide.md");
5382
- if (!fs7.existsSync(qaGuidePath) || opts.force) {
5383
- const discovery = discoverQaContext(cwd);
5384
- if (discovery.routes.length > 0) {
5385
- const qaGuide = generateQaGuide(discovery);
5386
- fs7.writeFileSync(qaGuidePath, qaGuide);
5387
- console.log(` \u2713 .kody/qa-guide.md (${discovery.routes.length} routes, ${discovery.roles.length} roles)`);
5388
- if (discovery.loginPage) console.log(` \u2713 Login page detected: ${discovery.loginPage}`);
5389
- if (discovery.adminPath) console.log(` \u2713 Admin panel detected: ${discovery.adminPath}`);
5390
- console.log(" \u2139 Add QA_ADMIN_EMAIL, QA_ADMIN_PASSWORD, QA_USER_EMAIL, QA_USER_PASSWORD as GitHub secrets");
5391
- } else {
5392
- console.log(" \u25CB No routes detected \u2014 skipping QA guide");
5393
- }
5394
- } else {
5395
- console.log(" \u25CB .kody/qa-guide.md already exists (use --force to regenerate)");
5396
- }
5397
5012
  console.log("\n\u2500\u2500 Labels \u2500\u2500");
5398
5013
  try {
5399
5014
  let repoSlug = "";
5400
5015
  try {
5401
- const configPath = path6.join(cwd, "kody.config.json");
5402
- if (fs7.existsSync(configPath)) {
5403
- const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
5016
+ const configPath = path21.join(cwd, "kody.config.json");
5017
+ if (fs22.existsSync(configPath)) {
5018
+ const config = JSON.parse(fs22.readFileSync(configPath, "utf-8"));
5404
5019
  if (config.github?.owner && config.github?.repo) {
5405
5020
  repoSlug = `${config.github.owner}/${config.github.repo}`;
5406
5021
  }
@@ -5412,7 +5027,6 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
5412
5027
  { name: "kody:planning", color: "c5def5", description: "Kody is analyzing and planning" },
5413
5028
  { name: "kody:building", color: "0e8a16", description: "Kody is building code" },
5414
5029
  { name: "kody:review", color: "fbca04", description: "Kody is reviewing code" },
5415
- { name: "kody:shipping", color: "1d76db", description: "Kody is creating the pull request" },
5416
5030
  { name: "kody:done", color: "0e8a16", description: "Kody completed successfully" },
5417
5031
  { name: "kody:failed", color: "d93f0b", description: "Kody pipeline failed" },
5418
5032
  { name: "kody:waiting", color: "fef2c0", description: "Kody is waiting for answers" },
@@ -5427,7 +5041,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
5427
5041
  ];
5428
5042
  for (const label of labels) {
5429
5043
  try {
5430
- execFileSync5("gh", [
5044
+ execFileSync12("gh", [
5431
5045
  "label",
5432
5046
  "create",
5433
5047
  label.name,
@@ -5447,7 +5061,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
5447
5061
  console.log(` \u2713 ${label.name}`);
5448
5062
  } catch {
5449
5063
  try {
5450
- execFileSync5("gh", ["label", "list", "--repo", repoSlug, "--search", label.name], {
5064
+ execFileSync12("gh", ["label", "list", "--repo", repoSlug, "--search", label.name], {
5451
5065
  cwd,
5452
5066
  encoding: "utf-8",
5453
5067
  timeout: 1e4,
@@ -5471,23 +5085,22 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
5471
5085
  const filesToCommit = [
5472
5086
  ".kody/memory/architecture.md",
5473
5087
  ".kody/memory/conventions.md",
5474
- ".kody/qa-guide.md",
5475
5088
  ...installedSkillPaths
5476
- ].filter((f) => fs7.existsSync(path6.join(cwd, f)));
5477
- if (fs7.existsSync(path6.join(cwd, "skills-lock.json"))) {
5089
+ ].filter((f) => fs22.existsSync(path21.join(cwd, f)));
5090
+ if (fs22.existsSync(path21.join(cwd, "skills-lock.json"))) {
5478
5091
  filesToCommit.push("skills-lock.json");
5479
5092
  }
5480
5093
  for (const stage of STEP_STAGES) {
5481
5094
  const stepFile = `.kody/steps/${stage}.md`;
5482
- if (fs7.existsSync(path6.join(cwd, stepFile))) {
5095
+ if (fs22.existsSync(path21.join(cwd, stepFile))) {
5483
5096
  filesToCommit.push(stepFile);
5484
5097
  }
5485
5098
  }
5486
5099
  if (filesToCommit.length > 0) {
5487
5100
  try {
5488
- const fullPaths = filesToCommit.map((f) => path6.join(cwd, f));
5101
+ const fullPaths = filesToCommit.map((f) => path21.join(cwd, f));
5489
5102
  for (let pass = 0; pass < 2; pass++) {
5490
- execFileSync5("npx", ["prettier", "--write", ...fullPaths], {
5103
+ execFileSync12("npx", ["prettier", "--write", ...fullPaths], {
5491
5104
  cwd,
5492
5105
  encoding: "utf-8",
5493
5106
  timeout: 3e4,
@@ -5503,24 +5116,24 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
5503
5116
  try {
5504
5117
  if (isCI3) {
5505
5118
  const branchName = `kody-bootstrap-${Date.now()}`;
5506
- execFileSync5("git", ["checkout", "-b", branchName], { cwd, stdio: "pipe" });
5507
- execFileSync5("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
5508
- const staged = execFileSync5("git", ["diff", "--cached", "--name-only"], { cwd, encoding: "utf-8" }).trim();
5119
+ execFileSync12("git", ["checkout", "-b", branchName], { cwd, stdio: "pipe" });
5120
+ execFileSync12("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
5121
+ const staged = execFileSync12("git", ["diff", "--cached", "--name-only"], { cwd, encoding: "utf-8" }).trim();
5509
5122
  if (staged) {
5510
- execFileSync5("git", ["commit", "-m", "chore: Add Kody project memory and step files\n\nBootstrap Kody Engine with project-specific architecture, conventions, and pipeline step files."], { cwd, stdio: "pipe" });
5511
- execFileSync5("git", ["push", "-u", "origin", branchName], { cwd, stdio: "pipe", timeout: 6e4 });
5123
+ execFileSync12("git", ["commit", "-m", "chore: Add Kody project memory and step files\n\nBootstrap Kody Engine with project-specific architecture, conventions, and pipeline step files."], { cwd, stdio: "pipe" });
5124
+ execFileSync12("git", ["push", "-u", "origin", branchName], { cwd, stdio: "pipe", timeout: 6e4 });
5512
5125
  console.log(` \u2713 Pushed branch: ${branchName}`);
5513
5126
  let baseBranch = "main";
5514
5127
  try {
5515
- const configPath = path6.join(cwd, "kody.config.json");
5516
- if (fs7.existsSync(configPath)) {
5517
- const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
5128
+ const configPath = path21.join(cwd, "kody.config.json");
5129
+ if (fs22.existsSync(configPath)) {
5130
+ const config = JSON.parse(fs22.readFileSync(configPath, "utf-8"));
5518
5131
  baseBranch = config.git?.defaultBranch ?? "main";
5519
5132
  }
5520
5133
  } catch {
5521
5134
  }
5522
5135
  try {
5523
- const prUrl = execFileSync5("gh", [
5136
+ const prUrl = execFileSync12("gh", [
5524
5137
  "pr",
5525
5138
  "create",
5526
5139
  "--title",
@@ -5559,13 +5172,13 @@ Create it manually.`, cwd);
5559
5172
  console.log(" \u25CB No new changes to commit");
5560
5173
  }
5561
5174
  } else {
5562
- execFileSync5("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
5563
- const staged = execFileSync5("git", ["diff", "--cached", "--name-only"], { cwd, encoding: "utf-8" }).trim();
5175
+ execFileSync12("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
5176
+ const staged = execFileSync12("git", ["diff", "--cached", "--name-only"], { cwd, encoding: "utf-8" }).trim();
5564
5177
  if (staged) {
5565
- execFileSync5("git", ["commit", "-m", "chore: Add Kody project memory and step files\n\nBootstrap Kody Engine with project-specific architecture, conventions, and pipeline step files."], { cwd, stdio: "pipe" });
5178
+ execFileSync12("git", ["commit", "-m", "chore: Add Kody project memory and step files\n\nBootstrap Kody Engine with project-specific architecture, conventions, and pipeline step files."], { cwd, stdio: "pipe" });
5566
5179
  console.log(` \u2713 Committed: ${filesToCommit.join(", ")}`);
5567
5180
  try {
5568
- execFileSync5("git", ["push"], { cwd, stdio: "pipe", timeout: 6e4 });
5181
+ execFileSync12("git", ["push"], { cwd, stdio: "pipe", timeout: 6e4 });
5569
5182
  console.log(" \u2713 Pushed to origin");
5570
5183
  } catch {
5571
5184
  console.log(" \u25CB Push failed \u2014 run 'git push' manually");
@@ -5585,22 +5198,131 @@ Create it manually.`, cwd);
5585
5198
  console.log(" \u2713 Project bootstrap complete!");
5586
5199
  console.log(" Kody now has project-specific memory and customized step files.\n");
5587
5200
  }
5588
-
5589
- // src/bin/cli.ts
5590
- init_architecture_detection();
5591
- var __dirname = path27.dirname(fileURLToPath(import.meta.url));
5592
- var PKG_ROOT = path27.resolve(__dirname, "..", "..");
5593
- function getVersion() {
5594
- const pkgPath = path27.join(PKG_ROOT, "package.json");
5595
- const pkg = JSON.parse(fs29.readFileSync(pkgPath, "utf-8"));
5596
- return pkg.version;
5201
+ function detectArchitectureBasic(cwd) {
5202
+ const detected = [];
5203
+ const pkgPath = path21.join(cwd, "package.json");
5204
+ if (fs22.existsSync(pkgPath)) {
5205
+ try {
5206
+ const pkg = JSON.parse(fs22.readFileSync(pkgPath, "utf-8"));
5207
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
5208
+ if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
5209
+ else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
5210
+ else if (allDeps.express) detected.push(`- Framework: Express ${allDeps.express}`);
5211
+ else if (allDeps.fastify) detected.push(`- Framework: Fastify ${allDeps.fastify}`);
5212
+ else if (allDeps.hono) detected.push(`- Framework: Hono ${allDeps.hono}`);
5213
+ if (allDeps.typescript) detected.push(`- Language: TypeScript ${allDeps.typescript}`);
5214
+ if (allDeps.vitest) detected.push(`- Testing: vitest ${allDeps.vitest}`);
5215
+ else if (allDeps.jest) detected.push(`- Testing: jest ${allDeps.jest}`);
5216
+ if (allDeps.eslint) detected.push(`- Linting: eslint ${allDeps.eslint}`);
5217
+ if (allDeps.prettier) detected.push(`- Formatting: prettier ${allDeps.prettier}`);
5218
+ if (allDeps.prisma || allDeps["@prisma/client"]) detected.push("- ORM: Prisma");
5219
+ if (allDeps["drizzle-orm"]) detected.push("- ORM: Drizzle");
5220
+ if (allDeps.payload || allDeps["@payloadcms/next"]) detected.push("- CMS: Payload CMS");
5221
+ if (allDeps.tailwindcss) detected.push(`- CSS: Tailwind CSS ${allDeps.tailwindcss}`);
5222
+ if (fs22.existsSync(path21.join(cwd, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
5223
+ else if (fs22.existsSync(path21.join(cwd, "yarn.lock"))) detected.push("- Package manager: yarn");
5224
+ else if (fs22.existsSync(path21.join(cwd, "bun.lockb"))) detected.push("- Package manager: bun");
5225
+ else if (fs22.existsSync(path21.join(cwd, "package-lock.json"))) detected.push("- Package manager: npm");
5226
+ } catch {
5227
+ }
5228
+ }
5229
+ return detected;
5230
+ }
5231
+ var SKILL_MAPPINGS = [
5232
+ {
5233
+ detect: (deps) => "next" in deps,
5234
+ skills: [
5235
+ { package: "vercel-labs/agent-skills@vercel-react-best-practices", label: "React best practices (Vercel)" }
5236
+ ]
5237
+ },
5238
+ {
5239
+ detect: (deps) => "react" in deps && !("next" in deps),
5240
+ skills: [
5241
+ { package: "vercel-labs/agent-skills@vercel-react-best-practices", label: "React best practices (Vercel)" }
5242
+ ]
5243
+ },
5244
+ {
5245
+ detect: (deps) => FRONTEND_DEPS.some((d) => d in deps),
5246
+ skills: [
5247
+ { package: "microsoft/playwright-cli@playwright-cli", label: "Playwright browser automation" }
5248
+ ]
5249
+ }
5250
+ ];
5251
+ function detectSkillsForProject(cwd) {
5252
+ const pkgPath = path21.join(cwd, "package.json");
5253
+ if (!fs22.existsSync(pkgPath)) return [];
5254
+ try {
5255
+ const pkg = JSON.parse(fs22.readFileSync(pkgPath, "utf-8"));
5256
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
5257
+ const seen = /* @__PURE__ */ new Set();
5258
+ const skills = [];
5259
+ for (const mapping of SKILL_MAPPINGS) {
5260
+ if (mapping.detect(allDeps)) {
5261
+ for (const skill of mapping.skills) {
5262
+ if (!seen.has(skill.package)) {
5263
+ seen.add(skill.package);
5264
+ skills.push(skill);
5265
+ }
5266
+ }
5267
+ }
5268
+ }
5269
+ return skills;
5270
+ } catch {
5271
+ return [];
5272
+ }
5273
+ }
5274
+ function installSkillsForProject(cwd) {
5275
+ const skills = detectSkillsForProject(cwd);
5276
+ if (skills.length === 0) {
5277
+ console.log(" \u25CB No skills to install (no frontend framework detected)");
5278
+ return [];
5279
+ }
5280
+ let installedSkills = {};
5281
+ const lockPath = path21.join(cwd, "skills-lock.json");
5282
+ if (fs22.existsSync(lockPath)) {
5283
+ try {
5284
+ const lock = JSON.parse(fs22.readFileSync(lockPath, "utf-8"));
5285
+ installedSkills = lock.skills ?? {};
5286
+ } catch {
5287
+ }
5288
+ }
5289
+ const installedPaths = [];
5290
+ for (const skill of skills) {
5291
+ const skillName = skill.package.split("@").pop() ?? "";
5292
+ if (skillName in installedSkills) {
5293
+ console.log(` \u25CB ${skill.label} \u2014 already installed`);
5294
+ const agentPath = `.agents/skills/${skillName}`;
5295
+ const claudePath = `.claude/skills/${skillName}`;
5296
+ if (fs22.existsSync(path21.join(cwd, agentPath))) installedPaths.push(agentPath);
5297
+ if (fs22.existsSync(path21.join(cwd, claudePath))) installedPaths.push(claudePath);
5298
+ continue;
5299
+ }
5300
+ try {
5301
+ console.log(` Installing: ${skill.label} (${skill.package})`);
5302
+ execFileSync12("npx", ["skills", "add", skill.package, "--yes"], {
5303
+ cwd,
5304
+ encoding: "utf-8",
5305
+ timeout: 6e4,
5306
+ stdio: ["pipe", "pipe", "pipe"]
5307
+ });
5308
+ const skillName2 = skill.package.split("@").pop() ?? "";
5309
+ const agentPath = `.agents/skills/${skillName2}`;
5310
+ const claudePath = `.claude/skills/${skillName2}`;
5311
+ if (fs22.existsSync(path21.join(cwd, agentPath))) installedPaths.push(agentPath);
5312
+ if (fs22.existsSync(path21.join(cwd, claudePath))) installedPaths.push(claudePath);
5313
+ console.log(` \u2713 ${skill.label}`);
5314
+ } catch (err) {
5315
+ console.log(` \u2717 ${skill.label} \u2014 failed to install`);
5316
+ }
5317
+ }
5318
+ return installedPaths;
5597
5319
  }
5598
5320
  var args = process.argv.slice(2);
5599
5321
  var command = args[0];
5600
5322
  if (command === "init") {
5601
- initCommand({ force: args.includes("--force") }, PKG_ROOT);
5323
+ initCommand({ force: args.includes("--force") });
5602
5324
  } else if (command === "bootstrap") {
5603
- bootstrapCommand({ force: args.includes("--force") }, PKG_ROOT);
5325
+ bootstrapCommand({ force: args.includes("--force") });
5604
5326
  } else if (command === "version" || command === "--version" || command === "-v") {
5605
5327
  console.log(getVersion());
5606
5328
  } else {
@@ -5608,7 +5330,7 @@ if (command === "init") {
5608
5330
  }
5609
5331
  export {
5610
5332
  buildConfig,
5611
- checkCommand,
5333
+ checkCommand2 as checkCommand,
5612
5334
  checkFile,
5613
5335
  checkGhAuth,
5614
5336
  checkGhRepoAccess,