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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/cli.js CHANGED
@@ -9,8 +9,250 @@ 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
+ // src/ci/parse-inputs.ts
68
+ var parse_inputs_exports = {};
69
+ __export(parse_inputs_exports, {
70
+ parseCommentInputs: () => parseCommentInputs,
71
+ runCiParse: () => runCiParse,
72
+ writeOutputs: () => writeOutputs
73
+ });
74
+ import * as fs8 from "fs";
75
+ function generateTimestamp() {
76
+ const now = /* @__PURE__ */ new Date();
77
+ const pad = (n) => String(n).padStart(2, "0");
78
+ const y = String(now.getFullYear()).slice(2);
79
+ const m = pad(now.getMonth() + 1);
80
+ const d = pad(now.getDate());
81
+ const H = pad(now.getHours());
82
+ const M = pad(now.getMinutes());
83
+ const S = pad(now.getSeconds());
84
+ return `${y}${m}${d}-${H}${M}${S}`;
85
+ }
86
+ function parseCommentInputs() {
87
+ const triggerType = process.env.TRIGGER_TYPE ?? "dispatch";
88
+ if (triggerType === "dispatch") {
89
+ const taskId2 = process.env.INPUT_TASK_ID ?? "";
90
+ return {
91
+ task_id: taskId2,
92
+ mode: process.env.INPUT_MODE ?? "full",
93
+ from_stage: process.env.INPUT_FROM_STAGE ?? "",
94
+ issue_number: process.env.INPUT_ISSUE_NUMBER ?? "",
95
+ pr_number: "",
96
+ feedback: process.env.INPUT_FEEDBACK ?? "",
97
+ complexity: "",
98
+ ci_run_id: "",
99
+ dry_run: false,
100
+ valid: !!taskId2,
101
+ trigger_type: "dispatch"
102
+ };
103
+ }
104
+ const commentBody = (process.env.COMMENT_BODY ?? "").replace(/\r/g, "");
105
+ const issueNumber = process.env.ISSUE_NUMBER ?? "";
106
+ const isPR = !!process.env.ISSUE_IS_PR;
107
+ const kodyMatch = commentBody.match(/(?:@kody|\/kody)\s*(.*)/i);
108
+ if (!kodyMatch) {
109
+ return {
110
+ task_id: "",
111
+ mode: "full",
112
+ from_stage: "",
113
+ issue_number: issueNumber,
114
+ pr_number: "",
115
+ feedback: "",
116
+ complexity: "",
117
+ ci_run_id: "",
118
+ dry_run: false,
119
+ valid: false,
120
+ trigger_type: "comment"
121
+ };
122
+ }
123
+ const argsLine = kodyMatch[1].trim();
124
+ let fromStage = "";
125
+ let feedback = "";
126
+ let complexity = "";
127
+ let dryRun = false;
128
+ let ciRunId = "";
129
+ const fromMatch = argsLine.match(/--from\s+(\S+)/);
130
+ if (fromMatch) fromStage = fromMatch[1];
131
+ const feedbackMatch = argsLine.match(/--feedback\s+"([^"]*)"/);
132
+ if (feedbackMatch) feedback = feedbackMatch[1];
133
+ const complexityMatch = argsLine.match(/--complexity\s+(\S+)/);
134
+ if (complexityMatch) complexity = complexityMatch[1];
135
+ if (/--dry-run/.test(argsLine)) dryRun = true;
136
+ const ciRunIdMatch = argsLine.match(/--ci-run-id\s+(\S+)/);
137
+ if (ciRunIdMatch) ciRunId = ciRunIdMatch[1];
138
+ const positional = argsLine.replace(/--from\s+\S+/g, "").replace(/--feedback\s+"[^"]*"/g, "").replace(/--complexity\s+\S+/g, "").replace(/--dry-run/g, "").replace(/--ci-run-id\s+\S+/g, "").replace(/\s+/g, " ").trim();
139
+ const parts = positional ? positional.split(/\s+/) : [];
140
+ let mode = "full";
141
+ let taskId = "";
142
+ let idx = 0;
143
+ if (parts[idx] && VALID_MODES.includes(parts[idx])) {
144
+ mode = parts[idx];
145
+ idx++;
146
+ }
147
+ if (parts[idx] && !parts[idx].startsWith("--")) {
148
+ taskId = parts[idx];
149
+ idx++;
150
+ } else if (parts[0] && !VALID_MODES.includes(parts[0]) && !parts[0].startsWith("--")) {
151
+ taskId = parts[0];
152
+ }
153
+ const kodyLineIdx = commentBody.search(/(?:@kody|\/kody)/i);
154
+ const afterKodyLine = commentBody.slice(kodyLineIdx);
155
+ const newlineIdx = afterKodyLine.indexOf("\n");
156
+ const bodyAfterCommand = newlineIdx !== -1 ? afterKodyLine.slice(newlineIdx + 1) : "";
157
+ if (mode === "approve") {
158
+ mode = "rerun";
159
+ if (bodyAfterCommand) {
160
+ feedback = bodyAfterCommand;
161
+ }
162
+ }
163
+ if (mode === "fix") {
164
+ if (bodyAfterCommand) {
165
+ feedback = bodyAfterCommand;
166
+ }
167
+ }
168
+ if (mode === "fix-ci") {
169
+ if (bodyAfterCommand) {
170
+ feedback = bodyAfterCommand;
171
+ const runIdFromBody = bodyAfterCommand.match(/Run ID:\s*(\d+)/);
172
+ if (runIdFromBody) {
173
+ ciRunId = runIdFromBody[1];
174
+ }
175
+ }
176
+ }
177
+ if (mode === "bootstrap") {
178
+ taskId = `bootstrap-${generateTimestamp()}`;
179
+ }
180
+ const prNumber = isPR ? issueNumber : "";
181
+ if (mode === "review" && prNumber) {
182
+ taskId = `review-pr-${prNumber}-${generateTimestamp()}`;
183
+ }
184
+ if (!taskId && mode === "full") {
185
+ taskId = `${issueNumber}-${generateTimestamp()}`;
186
+ }
187
+ const modesWithoutTaskId = ["fix", "fix-ci", "status", "review", "resolve", "rerun"];
188
+ const valid = !!taskId || modesWithoutTaskId.includes(mode);
189
+ return {
190
+ task_id: taskId,
191
+ mode,
192
+ from_stage: fromStage,
193
+ issue_number: issueNumber,
194
+ pr_number: prNumber,
195
+ feedback,
196
+ complexity,
197
+ ci_run_id: ciRunId,
198
+ dry_run: dryRun,
199
+ valid,
200
+ trigger_type: "comment"
201
+ };
202
+ }
203
+ function writeOutputs(result) {
204
+ const outputFile = process.env.GITHUB_OUTPUT;
205
+ function output(key, value) {
206
+ if (outputFile) {
207
+ if (value.includes("\n")) {
208
+ fs8.appendFileSync(outputFile, `${key}<<KODY_EOF
209
+ ${value}
210
+ KODY_EOF
211
+ `);
212
+ } else {
213
+ fs8.appendFileSync(outputFile, `${key}=${value}
214
+ `);
215
+ }
216
+ }
217
+ const display = value.includes("\n") ? value.split("\n")[0] + "..." : value;
218
+ console.log(`${key}=${display}`);
219
+ }
220
+ output("task_id", result.task_id);
221
+ output("mode", result.mode);
222
+ output("from_stage", result.from_stage);
223
+ output("issue_number", result.issue_number);
224
+ output("pr_number", result.pr_number);
225
+ output("feedback", result.feedback);
226
+ output("complexity", result.complexity);
227
+ output("ci_run_id", result.ci_run_id);
228
+ output("dry_run", result.dry_run ? "true" : "false");
229
+ output("valid", result.valid ? "true" : "false");
230
+ output("trigger_type", result.trigger_type);
231
+ }
232
+ function runCiParse() {
233
+ const result = parseCommentInputs();
234
+ writeOutputs(result);
235
+ }
236
+ var VALID_MODES;
237
+ var init_parse_inputs = __esm({
238
+ "src/ci/parse-inputs.ts"() {
239
+ "use strict";
240
+ VALID_MODES = [
241
+ "full",
242
+ "rerun",
243
+ "fix",
244
+ "fix-ci",
245
+ "status",
246
+ "approve",
247
+ "review",
248
+ "resolve",
249
+ "bootstrap"
250
+ ];
251
+ }
252
+ });
253
+
12
254
  // src/agent-runner.ts
13
- import { spawn, execFileSync } from "child_process";
255
+ import { spawn, execFileSync as execFileSync6 } from "child_process";
14
256
  function writeStdin(child, prompt) {
15
257
  return new Promise((resolve4, reject) => {
16
258
  if (!child.stdin) {
@@ -82,9 +324,9 @@ async function runSubprocess(command2, args2, prompt, timeout, options) {
82
324
  ${errDetail}`
83
325
  };
84
326
  }
85
- function checkCommand(command2, args2) {
327
+ function checkCommand2(command2, args2) {
86
328
  try {
87
- execFileSync(command2, args2, { timeout: 1e4, stdio: "pipe" });
329
+ execFileSync6(command2, args2, { timeout: 1e4, stdio: "pipe" });
88
330
  return true;
89
331
  } catch {
90
332
  return false;
@@ -114,7 +356,7 @@ function createClaudeCodeRunner() {
114
356
  return runSubprocess("claude", args2, prompt, timeout, options);
115
357
  },
116
358
  async healthCheck() {
117
- return checkCommand("claude", ["--version"]);
359
+ return checkCommand2("claude", ["--version"]);
118
360
  }
119
361
  };
120
362
  }
@@ -271,12 +513,105 @@ var init_logger = __esm({
271
513
  }
272
514
  });
273
515
 
516
+ // src/validators.ts
517
+ function stripFences(content) {
518
+ return content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
519
+ }
520
+ function parseJsonSafe(raw, requiredFields) {
521
+ let parsed;
522
+ try {
523
+ parsed = JSON.parse(raw);
524
+ } catch (err) {
525
+ return { ok: false, error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}` };
526
+ }
527
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
528
+ return { ok: false, error: `Expected JSON object, got ${Array.isArray(parsed) ? "array" : typeof parsed}` };
529
+ }
530
+ if (requiredFields) {
531
+ for (const field of requiredFields) {
532
+ if (!(field in parsed)) {
533
+ return { ok: false, error: `Missing required field: ${field}` };
534
+ }
535
+ }
536
+ }
537
+ return { ok: true, data: parsed };
538
+ }
539
+ function validateTaskJson(content) {
540
+ try {
541
+ const parsed = JSON.parse(stripFences(content));
542
+ for (const field of REQUIRED_TASK_FIELDS) {
543
+ if (!(field in parsed)) {
544
+ return { valid: false, error: `Missing field: ${field}` };
545
+ }
546
+ }
547
+ return { valid: true };
548
+ } catch (err) {
549
+ return {
550
+ valid: false,
551
+ error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`
552
+ };
553
+ }
554
+ }
555
+ function validatePlanMd(content) {
556
+ if (content.length < 10) {
557
+ return { valid: false, error: "Plan is too short (< 10 chars)" };
558
+ }
559
+ if (!/^##\s+\w+/m.test(content)) {
560
+ return { valid: false, error: "Plan has no markdown h2 sections" };
561
+ }
562
+ return { valid: true };
563
+ }
564
+ function validateReviewMd(content) {
565
+ if (/pass/i.test(content) || /fail/i.test(content)) {
566
+ return { valid: true };
567
+ }
568
+ return { valid: false, error: "Review must contain 'pass' or 'fail'" };
569
+ }
570
+ var REQUIRED_TASK_FIELDS;
571
+ var init_validators = __esm({
572
+ "src/validators.ts"() {
573
+ "use strict";
574
+ REQUIRED_TASK_FIELDS = [
575
+ "task_type",
576
+ "title",
577
+ "description",
578
+ "scope",
579
+ "risk_level"
580
+ ];
581
+ }
582
+ });
583
+
274
584
  // src/config.ts
275
- import * as fs from "fs";
276
- import * as path from "path";
585
+ import * as fs9 from "fs";
586
+ import * as path7 from "path";
587
+ function resolveStageConfig(config, stageName, modelTier) {
588
+ const stageOverride = config.agent.stages?.[stageName];
589
+ if (stageOverride) return stageOverride;
590
+ if (config.agent.default) return config.agent.default;
591
+ const model = config.agent.modelMap[modelTier];
592
+ if (!model) {
593
+ throw new Error(`No model configured for stage '${stageName}' (tier: ${modelTier}). Set agent.stages.${stageName} or agent.default in kody.config.json`);
594
+ }
595
+ return {
596
+ provider: config.agent.provider ?? "claude",
597
+ model
598
+ };
599
+ }
277
600
  function needsLitellmProxy(config) {
278
601
  return !!(config.agent.provider && config.agent.provider !== "anthropic");
279
602
  }
603
+ function stageNeedsProxy(stageConfig) {
604
+ return stageConfig.provider !== "claude" && stageConfig.provider !== "anthropic";
605
+ }
606
+ function anyStageNeedsProxy(config) {
607
+ if (config.agent.stages) {
608
+ for (const sc of Object.values(config.agent.stages)) {
609
+ if (stageNeedsProxy(sc)) return true;
610
+ }
611
+ }
612
+ if (config.agent.default && stageNeedsProxy(config.agent.default)) return true;
613
+ return needsLitellmProxy(config);
614
+ }
280
615
  function getLitellmUrl() {
281
616
  return LITELLM_DEFAULT_URL;
282
617
  }
@@ -290,10 +625,16 @@ function setConfigDir(dir) {
290
625
  }
291
626
  function getProjectConfig() {
292
627
  if (_config) return _config;
293
- const configPath = path.join(_configDir ?? process.cwd(), "kody.config.json");
294
- if (fs.existsSync(configPath)) {
628
+ const configPath = path7.join(_configDir ?? process.cwd(), "kody.config.json");
629
+ if (fs9.existsSync(configPath)) {
295
630
  try {
296
- const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
631
+ const result = parseJsonSafe(fs9.readFileSync(configPath, "utf-8"));
632
+ if (!result.ok) {
633
+ logger.warn(`kody.config.json: ${result.error} \u2014 using defaults`);
634
+ _config = { ...DEFAULT_CONFIG };
635
+ return _config;
636
+ }
637
+ const raw = result.data;
297
638
  _config = {
298
639
  quality: { ...DEFAULT_CONFIG.quality, ...raw.quality },
299
640
  git: { ...DEFAULT_CONFIG.git, ...raw.git },
@@ -305,7 +646,13 @@ function getProjectConfig() {
305
646
  },
306
647
  timeouts: raw.timeouts ?? void 0,
307
648
  contextTiers: raw.contextTiers ? { ...DEFAULT_CONFIG.contextTiers, ...raw.contextTiers } : DEFAULT_CONFIG.contextTiers,
308
- mcp: raw.mcp ? { enabled: false, servers: {}, stages: ["build", "verify", "review", "review-fix"], ...raw.mcp } : void 0
649
+ mcp: raw.mcp ? {
650
+ servers: {},
651
+ stages: ["build", "verify", "review", "review-fix"],
652
+ ...raw.mcp,
653
+ // Auto-enable when devServer is configured (user can still set enabled: false to override)
654
+ enabled: raw.mcp.enabled ?? !!raw.mcp.devServer
655
+ } : void 0
309
656
  };
310
657
  } catch {
311
658
  logger.warn("kody.config.json is invalid JSON \u2014 using defaults");
@@ -321,6 +668,7 @@ var init_config = __esm({
321
668
  "src/config.ts"() {
322
669
  "use strict";
323
670
  init_logger();
671
+ init_validators();
324
672
  DEFAULT_CONFIG = {
325
673
  quality: {
326
674
  typecheck: "pnpm -s tsc --noEmit",
@@ -354,7 +702,7 @@ var init_config = __esm({
354
702
  });
355
703
 
356
704
  // src/git-utils.ts
357
- import { execFileSync as execFileSync2 } from "child_process";
705
+ import { execFileSync as execFileSync7 } from "child_process";
358
706
  function getHookSafeEnv() {
359
707
  if (!_hookSafeEnv) {
360
708
  _hookSafeEnv = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
@@ -362,7 +710,7 @@ function getHookSafeEnv() {
362
710
  return _hookSafeEnv;
363
711
  }
364
712
  function git(args2, options) {
365
- return execFileSync2("git", args2, {
713
+ return execFileSync7("git", args2, {
366
714
  encoding: "utf-8",
367
715
  timeout: options?.timeout ?? 3e4,
368
716
  cwd: options?.cwd,
@@ -417,8 +765,9 @@ function ensureFeatureBranch(issueNumber, title, cwd) {
417
765
  }
418
766
  try {
419
767
  git(["fetch", "origin"], { cwd, timeout: 3e4 });
420
- } catch {
421
- logger.warn(" Failed to fetch origin");
768
+ } catch (err) {
769
+ const msg = err instanceof Error ? err.message : String(err);
770
+ logger.warn(` Failed to fetch origin: ${msg}`);
422
771
  }
423
772
  try {
424
773
  git(["rev-parse", "--verify", `origin/${branchName}`], { cwd });
@@ -450,8 +799,9 @@ function syncWithDefault(cwd, branch) {
450
799
  if (current === defaultBranch) return;
451
800
  try {
452
801
  git(["fetch", "origin", defaultBranch], { cwd, timeout: 3e4 });
453
- } catch {
454
- logger.warn(" Failed to fetch latest from origin");
802
+ } catch (err) {
803
+ const msg = err instanceof Error ? err.message : String(err);
804
+ logger.warn(` Failed to fetch latest from origin: ${msg}`);
455
805
  return;
456
806
  }
457
807
  try {
@@ -460,7 +810,8 @@ function syncWithDefault(cwd, branch) {
460
810
  } catch {
461
811
  try {
462
812
  git(["merge", "--abort"], { cwd });
463
- } catch {
813
+ } catch (abortErr) {
814
+ logger.warn(` Failed to abort merge: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}`);
464
815
  }
465
816
  logger.warn(` Merge conflict with origin/${defaultBranch} \u2014 skipping sync`);
466
817
  }
@@ -471,8 +822,9 @@ function mergeDefault(cwd) {
471
822
  if (current === defaultBranch) return "clean";
472
823
  try {
473
824
  git(["fetch", "origin", defaultBranch], { cwd, timeout: 3e4 });
474
- } catch {
475
- logger.warn(" Failed to fetch latest from origin");
825
+ } catch (err) {
826
+ const msg = err instanceof Error ? err.message : String(err);
827
+ logger.warn(` Failed to fetch latest from origin: ${msg}`);
476
828
  return "error";
477
829
  }
478
830
  try {
@@ -487,7 +839,8 @@ function mergeDefault(cwd) {
487
839
  }
488
840
  try {
489
841
  git(["merge", "--abort"], { cwd });
490
- } catch {
842
+ } catch (abortErr) {
843
+ logger.warn(` Failed to abort merge: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}`);
491
844
  }
492
845
  return "error";
493
846
  }
@@ -511,6 +864,17 @@ function commitAll(message, cwd) {
511
864
  logger.info(` Committed: ${hash} ${message}`);
512
865
  return { success: true, hash, message };
513
866
  }
867
+ function getDiffFiles(baseBranch, cwd) {
868
+ try {
869
+ const output = git(["diff", "--name-only", `origin/${baseBranch}...HEAD`], { cwd });
870
+ if (!output) return [];
871
+ return output.split("\n").filter((f) => f && !f.startsWith(".kody/"));
872
+ } catch (err) {
873
+ const msg = err instanceof Error ? err.message : String(err);
874
+ logger.warn(` Failed to get diff files: ${msg}`);
875
+ return [];
876
+ }
877
+ }
514
878
  function pushBranch(cwd) {
515
879
  try {
516
880
  git(["push", "-u", "origin", "HEAD"], { cwd, timeout: 12e4 });
@@ -532,7 +896,21 @@ var init_git_utils = __esm({
532
896
  });
533
897
 
534
898
  // src/github-api.ts
535
- import { execFileSync as execFileSync3 } from "child_process";
899
+ import { execFileSync as execFileSync8 } from "child_process";
900
+ function isGhExecError(err) {
901
+ return typeof err === "object" && err !== null;
902
+ }
903
+ function ghErrorMessage(err) {
904
+ if (isGhExecError(err)) {
905
+ const stderr = err.stderr?.toString().trim();
906
+ if (stderr) return stderr;
907
+ }
908
+ return err instanceof Error ? err.message : String(err);
909
+ }
910
+ function isNotFoundError(err) {
911
+ const msg = ghErrorMessage(err).toLowerCase();
912
+ return msg.includes("not found") || msg.includes("no pull requests") || msg.includes("could not resolve");
913
+ }
536
914
  function setGhCwd(cwd) {
537
915
  _ghCwd = cwd;
538
916
  }
@@ -542,7 +920,7 @@ function ghToken() {
542
920
  function gh(args2, options) {
543
921
  const token = ghToken();
544
922
  const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
545
- return execFileSync3("gh", args2, {
923
+ return execFileSync8("gh", args2, {
546
924
  encoding: "utf-8",
547
925
  timeout: API_TIMEOUT_MS,
548
926
  cwd: _ghCwd,
@@ -560,12 +938,34 @@ function getIssue(issueNumber) {
560
938
  "--json",
561
939
  "body,title"
562
940
  ]);
563
- return JSON.parse(output);
941
+ const parsed = JSON.parse(output);
942
+ if (!parsed || typeof parsed.title !== "string") {
943
+ logger.warn(` Issue #${issueNumber}: unexpected response shape`);
944
+ return null;
945
+ }
946
+ return { body: parsed.body ?? "", title: parsed.title };
564
947
  } catch (err) {
565
- logger.error(` Failed to get issue #${issueNumber}: ${err}`);
948
+ if (isNotFoundError(err)) {
949
+ logger.info(` Issue #${issueNumber} not found`);
950
+ } else {
951
+ logger.error(` Failed to get issue #${issueNumber}: ${ghErrorMessage(err)}`);
952
+ }
566
953
  return null;
567
954
  }
568
955
  }
956
+ function getIssueComments(issueNumber) {
957
+ try {
958
+ const output = gh([
959
+ "api",
960
+ `repos/{owner}/{repo}/issues/${issueNumber}/comments`,
961
+ "--jq",
962
+ "[.[] | {body, created_at}]"
963
+ ]);
964
+ return output ? JSON.parse(output) : [];
965
+ } catch {
966
+ return [];
967
+ }
968
+ }
569
969
  function getIssueLabels(issueNumber) {
570
970
  try {
571
971
  const output = gh(["issue", "view", String(issueNumber), "--json", "labels", "--jq", ".labels[].name"]);
@@ -603,8 +1003,15 @@ function getPRForBranch(branch) {
603
1003
  "number,url"
604
1004
  ]);
605
1005
  const data = JSON.parse(output);
1006
+ if (typeof data.number !== "number" || typeof data.url !== "string") {
1007
+ logger.warn(` PR for branch ${branch}: unexpected response shape`);
1008
+ return null;
1009
+ }
606
1010
  return { number: data.number, url: data.url };
607
- } catch {
1011
+ } catch (err) {
1012
+ if (!isNotFoundError(err)) {
1013
+ logger.warn(` Failed to check PR for branch ${branch}: ${ghErrorMessage(err)}`);
1014
+ }
608
1015
  return null;
609
1016
  }
610
1017
  }
@@ -642,8 +1049,7 @@ function createPR(head, base, title, body) {
642
1049
  logger.info(` PR created: ${url}`);
643
1050
  return { number, url };
644
1051
  } catch (err) {
645
- const stderr = err?.stderr?.toString().trim();
646
- const reason = stderr || (err instanceof Error ? err.message : String(err));
1052
+ const reason = ghErrorMessage(err);
647
1053
  logger.error(` Failed to create PR: ${reason}`);
648
1054
  return null;
649
1055
  }
@@ -714,9 +1120,22 @@ function getPRDetails(prNumber) {
714
1120
  "title,body,headRefName,baseRefName"
715
1121
  ]);
716
1122
  const data = JSON.parse(output);
717
- return { title: data.title, body: data.body, headBranch: data.headRefName, baseBranch: data.baseRefName };
1123
+ if (typeof data.title !== "string" || typeof data.headRefName !== "string") {
1124
+ logger.warn(` PR #${prNumber}: unexpected response shape`);
1125
+ return null;
1126
+ }
1127
+ return {
1128
+ title: data.title,
1129
+ body: data.body ?? "",
1130
+ headBranch: data.headRefName,
1131
+ baseBranch: data.baseRefName ?? "main"
1132
+ };
718
1133
  } catch (err) {
719
- logger.error(` Failed to get PR #${prNumber}: ${err}`);
1134
+ if (isNotFoundError(err)) {
1135
+ logger.info(` PR #${prNumber} not found`);
1136
+ } else {
1137
+ logger.error(` Failed to get PR #${prNumber}: ${ghErrorMessage(err)}`);
1138
+ }
720
1139
  return null;
721
1140
  }
722
1141
  }
@@ -758,7 +1177,7 @@ function getCIFailureLogs(runId, maxLength = 8e3) {
758
1177
  const prefix = logsOutput.length > maxLength ? "...(earlier output truncated)\n" : "";
759
1178
  return `${prefix}${truncated}`;
760
1179
  } catch (err) {
761
- logger.warn(` Failed to get CI failure logs for run ${runId}: ${err}`);
1180
+ logger.warn(` Failed to get CI failure logs for run ${runId}: ${ghErrorMessage(err)}`);
762
1181
  return null;
763
1182
  }
764
1183
  }
@@ -780,7 +1199,7 @@ function getLatestFailedRunForBranch(branch) {
780
1199
  ]);
781
1200
  return output.trim() || null;
782
1201
  } catch (err) {
783
- logger.warn(` Failed to get latest failed run for branch ${branch}: ${err}`);
1202
+ logger.warn(` Failed to get latest failed run for branch ${branch}: ${ghErrorMessage(err)}`);
784
1203
  return null;
785
1204
  }
786
1205
  }
@@ -861,7 +1280,7 @@ var init_github_api = __esm({
861
1280
  "use strict";
862
1281
  init_logger();
863
1282
  API_TIMEOUT_MS = 3e4;
864
- LIFECYCLE_LABELS = ["planning", "building", "review", "done", "failed", "waiting", "low", "medium", "high"];
1283
+ LIFECYCLE_LABELS = ["planning", "building", "review", "shipping", "done", "failed", "waiting", "low", "medium", "high"];
865
1284
  KODY_MARKERS = [
866
1285
  "Kody Review",
867
1286
  "\u{1F916} Generated by Kody",
@@ -876,15 +1295,22 @@ var init_github_api = __esm({
876
1295
  });
877
1296
 
878
1297
  // src/pipeline/state.ts
879
- import * as fs2 from "fs";
880
- import * as path2 from "path";
1298
+ import * as fs10 from "fs";
1299
+ import * as path8 from "path";
881
1300
  function loadState(taskId, taskDir) {
882
- const p = path2.join(taskDir, "status.json");
883
- if (!fs2.existsSync(p)) return null;
1301
+ const p = path8.join(taskDir, "status.json");
1302
+ if (!fs10.existsSync(p)) return null;
884
1303
  try {
885
- const raw = JSON.parse(fs2.readFileSync(p, "utf-8"));
886
- if (raw.taskId === taskId) return raw;
887
- return null;
1304
+ const result = parseJsonSafe(
1305
+ fs10.readFileSync(p, "utf-8"),
1306
+ ["taskId", "state", "stages", "createdAt", "updatedAt"]
1307
+ );
1308
+ if (!result.ok) {
1309
+ logger.warn(` Corrupt status.json: ${result.error}`);
1310
+ return null;
1311
+ }
1312
+ if (result.data.taskId !== taskId) return null;
1313
+ return result.data;
888
1314
  } catch {
889
1315
  return null;
890
1316
  }
@@ -894,11 +1320,11 @@ function writeState(state, taskDir) {
894
1320
  ...state,
895
1321
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
896
1322
  };
897
- const target = path2.join(taskDir, "status.json");
1323
+ const target = path8.join(taskDir, "status.json");
898
1324
  const tmp = target + ".tmp";
899
- fs2.writeFileSync(tmp, JSON.stringify(updated, null, 2));
900
- fs2.renameSync(tmp, target);
901
- state.updatedAt = updated.updatedAt;
1325
+ fs10.writeFileSync(tmp, JSON.stringify(updated, null, 2));
1326
+ fs10.renameSync(tmp, target);
1327
+ return updated;
902
1328
  }
903
1329
  function initState(taskId) {
904
1330
  const stages = {};
@@ -912,6 +1338,8 @@ var init_state = __esm({
912
1338
  "src/pipeline/state.ts"() {
913
1339
  "use strict";
914
1340
  init_definitions();
1341
+ init_validators();
1342
+ init_logger();
915
1343
  }
916
1344
  });
917
1345
 
@@ -936,16 +1364,16 @@ var init_complexity = __esm({
936
1364
  });
937
1365
 
938
1366
  // src/memory.ts
939
- import * as fs3 from "fs";
940
- import * as path3 from "path";
1367
+ import * as fs11 from "fs";
1368
+ import * as path9 from "path";
941
1369
  function readProjectMemory(projectDir) {
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();
1370
+ const memoryDir = path9.join(projectDir, ".kody", "memory");
1371
+ if (!fs11.existsSync(memoryDir)) return "";
1372
+ const files = fs11.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
945
1373
  if (files.length === 0) return "";
946
1374
  const sections = [];
947
1375
  for (const file of files) {
948
- const content = fs3.readFileSync(path3.join(memoryDir, file), "utf-8").trim();
1376
+ const content = fs11.readFileSync(path9.join(memoryDir, file), "utf-8").trim();
949
1377
  if (content) {
950
1378
  sections.push(`## ${file.replace(".md", "")}
951
1379
  ${content}`);
@@ -964,15 +1392,11 @@ var init_memory = __esm({
964
1392
  });
965
1393
 
966
1394
  // src/context-tiers.ts
967
- import * as fs4 from "fs";
968
- import * as path4 from "path";
969
- import * as crypto2 from "crypto";
1395
+ import * as fs12 from "fs";
1396
+ import * as path10 from "path";
970
1397
  function estimateTokens(text) {
971
1398
  return Math.ceil(text.length / 4);
972
1399
  }
973
- function contentHash(content) {
974
- return crypto2.createHash("sha256").update(content).digest("hex").slice(0, 16);
975
- }
976
1400
  function resolveStagePolicy(stageName, stageOverrides) {
977
1401
  const defaults = DEFAULT_STAGE_POLICIES[stageName] ?? DEFAULT_STAGE_POLICIES.build;
978
1402
  const overrides = stageOverrides?.[stageName];
@@ -1059,61 +1483,30 @@ function generateL1Json(content) {
1059
1483
  return content.slice(0, L1_MAX_CHARS);
1060
1484
  }
1061
1485
  }
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 = {
1486
+ function getTieredContent(filePath, content) {
1487
+ const key = path10.basename(filePath);
1488
+ return {
1083
1489
  source: filePath,
1084
- hash,
1085
1490
  L0: generateL0(content, key),
1086
1491
  L1: generateL1(content, key),
1087
1492
  L2: content
1088
1493
  };
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
- }
1100
1494
  }
1101
1495
  function selectTier(tiered, tier) {
1102
1496
  return tiered[tier];
1103
1497
  }
1104
1498
  function readProjectMemoryTiered(projectDir, tier) {
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();
1499
+ const memoryDir = path10.join(projectDir, ".kody", "memory");
1500
+ if (!fs12.existsSync(memoryDir)) return "";
1501
+ const files = fs12.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
1108
1502
  if (files.length === 0) return "";
1109
- const cacheDir = path4.join(memoryDir, ".tiers");
1110
1503
  const tierLabel2 = tier === "L2" ? "full" : tier === "L1" ? "overview" : "abstract";
1111
1504
  const sections = [];
1112
1505
  for (const file of files) {
1113
- const filePath = path4.join(memoryDir, file);
1114
- const content = fs4.readFileSync(filePath, "utf-8").trim();
1506
+ const filePath = path10.join(memoryDir, file);
1507
+ const content = fs12.readFileSync(filePath, "utf-8").trim();
1115
1508
  if (!content) continue;
1116
- const tiered = getTieredContent(filePath, content, cacheDir);
1509
+ const tiered = getTieredContent(filePath, content);
1117
1510
  const selected = selectTier(tiered, tier);
1118
1511
  if (selected) {
1119
1512
  sections.push(`## ${file.replace(".md", "")}
@@ -1128,26 +1521,25 @@ ${sections.join("\n\n")}
1128
1521
  `;
1129
1522
  }
1130
1523
  function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
1131
- const cacheDir = path4.join(taskDir, ".tiers");
1132
1524
  let context = `## Task Context
1133
1525
  `;
1134
1526
  context += `Task ID: ${taskId}
1135
1527
  `;
1136
1528
  context += `Task Directory: ${taskDir}
1137
1529
  `;
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);
1530
+ const taskMdPath = path10.join(taskDir, "task.md");
1531
+ if (fs12.existsSync(taskMdPath)) {
1532
+ const content = fs12.readFileSync(taskMdPath, "utf-8");
1533
+ const selected = selectContent(taskMdPath, content, policy.taskDescription);
1142
1534
  const label = tierLabel("Task Description", policy.taskDescription);
1143
1535
  context += `
1144
1536
  ## ${label}
1145
1537
  ${selected}
1146
1538
  `;
1147
1539
  }
1148
- const taskJsonPath = path4.join(taskDir, "task.json");
1149
- if (fs4.existsSync(taskJsonPath)) {
1150
- const content = fs4.readFileSync(taskJsonPath, "utf-8");
1540
+ const taskJsonPath = path10.join(taskDir, "task.json");
1541
+ if (fs12.existsSync(taskJsonPath)) {
1542
+ const content = fs12.readFileSync(taskJsonPath, "utf-8");
1151
1543
  if (policy.taskClassification === "L2") {
1152
1544
  try {
1153
1545
  const taskDef = JSON.parse(content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, ""));
@@ -1163,7 +1555,7 @@ ${selected}
1163
1555
  } catch {
1164
1556
  }
1165
1557
  } else {
1166
- const selected = selectContent(taskJsonPath, content, cacheDir, policy.taskClassification);
1558
+ const selected = selectContent(taskJsonPath, content, policy.taskClassification);
1167
1559
  if (selected) {
1168
1560
  const label = tierLabel("Task Classification", policy.taskClassification);
1169
1561
  context += `
@@ -1173,30 +1565,30 @@ ${selected}
1173
1565
  }
1174
1566
  }
1175
1567
  }
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);
1568
+ const specPath = path10.join(taskDir, "spec.md");
1569
+ if (fs12.existsSync(specPath)) {
1570
+ const content = fs12.readFileSync(specPath, "utf-8");
1571
+ const selected = selectContent(specPath, content, policy.spec);
1180
1572
  const label = tierLabel("Spec", policy.spec);
1181
1573
  context += `
1182
1574
  ## ${label}
1183
1575
  ${selected}
1184
1576
  `;
1185
1577
  }
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);
1578
+ const planPath = path10.join(taskDir, "plan.md");
1579
+ if (fs12.existsSync(planPath)) {
1580
+ const content = fs12.readFileSync(planPath, "utf-8");
1581
+ const selected = selectContent(planPath, content, policy.plan);
1190
1582
  const label = tierLabel("Plan", policy.plan);
1191
1583
  context += `
1192
1584
  ## ${label}
1193
1585
  ${selected}
1194
1586
  `;
1195
1587
  }
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);
1588
+ const contextMdPath = path10.join(taskDir, "context.md");
1589
+ if (fs12.existsSync(contextMdPath)) {
1590
+ const content = fs12.readFileSync(contextMdPath, "utf-8");
1591
+ const selected = selectContent(contextMdPath, content, policy.accumulatedContext);
1200
1592
  const label = tierLabel("Previous Stage Context", policy.accumulatedContext);
1201
1593
  context += `
1202
1594
  ## ${label}
@@ -1211,9 +1603,9 @@ ${feedback}
1211
1603
  }
1212
1604
  return prompt.replace("{{TASK_CONTEXT}}", context);
1213
1605
  }
1214
- function selectContent(filePath, content, cacheDir, tier) {
1606
+ function selectContent(filePath, content, tier) {
1215
1607
  if (tier === "L2") return content;
1216
- const tiered = getTieredContent(filePath, content, cacheDir);
1608
+ const tiered = getTieredContent(filePath, content);
1217
1609
  return selectTier(tiered, tier);
1218
1610
  }
1219
1611
  function tierLabel(sectionName, tier) {
@@ -1281,6 +1673,20 @@ var init_context_tiers = __esm({
1281
1673
  });
1282
1674
 
1283
1675
  // src/mcp-config.ts
1676
+ function withPlaywrightIfNeeded(mcpConfig, hasUI) {
1677
+ if (!mcpConfig?.enabled || !hasUI) return mcpConfig;
1678
+ const hasPlaywright = Object.keys(mcpConfig.servers).some(
1679
+ (name) => name.toLowerCase().includes("playwright")
1680
+ );
1681
+ if (hasPlaywright) return mcpConfig;
1682
+ return {
1683
+ ...mcpConfig,
1684
+ servers: {
1685
+ ...mcpConfig.servers,
1686
+ playwright: PLAYWRIGHT_SERVER
1687
+ }
1688
+ };
1689
+ }
1284
1690
  function buildMcpConfigJson(mcpConfig) {
1285
1691
  if (!mcpConfig?.enabled) return void 0;
1286
1692
  if (Object.keys(mcpConfig.servers).length === 0) return void 0;
@@ -1297,37 +1703,40 @@ function buildMcpConfigJson(mcpConfig) {
1297
1703
  }
1298
1704
  function isMcpEnabledForStage(stageName, mcpConfig) {
1299
1705
  if (!mcpConfig?.enabled) return false;
1300
- if (Object.keys(mcpConfig.servers).length === 0) return false;
1301
1706
  const allowedStages = mcpConfig.stages ?? DEFAULT_MCP_STAGES;
1302
1707
  return allowedStages.includes(stageName);
1303
1708
  }
1304
- var DEFAULT_MCP_STAGES;
1709
+ var DEFAULT_MCP_STAGES, PLAYWRIGHT_SERVER;
1305
1710
  var init_mcp_config = __esm({
1306
1711
  "src/mcp-config.ts"() {
1307
1712
  "use strict";
1308
1713
  DEFAULT_MCP_STAGES = ["build", "verify", "review", "review-fix"];
1714
+ PLAYWRIGHT_SERVER = {
1715
+ command: "npx",
1716
+ args: ["-y", "@anthropic-ai/mcp-playwright"]
1717
+ };
1309
1718
  }
1310
1719
  });
1311
1720
 
1312
1721
  // src/context.ts
1313
- import * as fs5 from "fs";
1314
- import * as path5 from "path";
1722
+ import * as fs13 from "fs";
1723
+ import * as path11 from "path";
1315
1724
  function readPromptFile(stageName, projectDir) {
1316
1725
  if (projectDir) {
1317
- const stepFile = path5.join(projectDir, ".kody", "steps", `${stageName}.md`);
1318
- if (fs5.existsSync(stepFile)) {
1319
- return fs5.readFileSync(stepFile, "utf-8");
1726
+ const stepFile = path11.join(projectDir, ".kody", "steps", `${stageName}.md`);
1727
+ if (fs13.existsSync(stepFile)) {
1728
+ return fs13.readFileSync(stepFile, "utf-8");
1320
1729
  }
1321
1730
  console.warn(` \u26A0 No step file at ${stepFile}, falling back to engine defaults. Run 'kody-engine-lite init --force' to generate step files.`);
1322
1731
  }
1323
1732
  const scriptDir = new URL(".", import.meta.url).pathname;
1324
1733
  const candidates = [
1325
- path5.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
1326
- path5.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
1734
+ path11.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
1735
+ path11.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
1327
1736
  ];
1328
1737
  for (const candidate of candidates) {
1329
- if (fs5.existsSync(candidate)) {
1330
- return fs5.readFileSync(candidate, "utf-8");
1738
+ if (fs13.existsSync(candidate)) {
1739
+ return fs13.readFileSync(candidate, "utf-8");
1331
1740
  }
1332
1741
  }
1333
1742
  throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
@@ -1339,18 +1748,18 @@ function injectTaskContext(prompt, taskId, taskDir, feedback) {
1339
1748
  `;
1340
1749
  context += `Task Directory: ${taskDir}
1341
1750
  `;
1342
- const taskMdPath = path5.join(taskDir, "task.md");
1343
- if (fs5.existsSync(taskMdPath)) {
1344
- const taskMd = fs5.readFileSync(taskMdPath, "utf-8");
1751
+ const taskMdPath = path11.join(taskDir, "task.md");
1752
+ if (fs13.existsSync(taskMdPath)) {
1753
+ const taskMd = fs13.readFileSync(taskMdPath, "utf-8");
1345
1754
  context += `
1346
1755
  ## Task Description
1347
1756
  ${taskMd}
1348
1757
  `;
1349
1758
  }
1350
- const taskJsonPath = path5.join(taskDir, "task.json");
1351
- if (fs5.existsSync(taskJsonPath)) {
1759
+ const taskJsonPath = path11.join(taskDir, "task.json");
1760
+ if (fs13.existsSync(taskJsonPath)) {
1352
1761
  try {
1353
- const taskDef = JSON.parse(fs5.readFileSync(taskJsonPath, "utf-8"));
1762
+ const taskDef = JSON.parse(fs13.readFileSync(taskJsonPath, "utf-8"));
1354
1763
  context += `
1355
1764
  ## Task Classification
1356
1765
  `;
@@ -1363,27 +1772,27 @@ ${taskMd}
1363
1772
  } catch {
1364
1773
  }
1365
1774
  }
1366
- const specPath = path5.join(taskDir, "spec.md");
1367
- if (fs5.existsSync(specPath)) {
1368
- const spec = fs5.readFileSync(specPath, "utf-8");
1775
+ const specPath = path11.join(taskDir, "spec.md");
1776
+ if (fs13.existsSync(specPath)) {
1777
+ const spec = fs13.readFileSync(specPath, "utf-8");
1369
1778
  const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
1370
1779
  context += `
1371
1780
  ## Spec Summary
1372
1781
  ${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
1373
1782
  `;
1374
1783
  }
1375
- const planPath = path5.join(taskDir, "plan.md");
1376
- if (fs5.existsSync(planPath)) {
1377
- const plan = fs5.readFileSync(planPath, "utf-8");
1784
+ const planPath = path11.join(taskDir, "plan.md");
1785
+ if (fs13.existsSync(planPath)) {
1786
+ const plan = fs13.readFileSync(planPath, "utf-8");
1378
1787
  const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
1379
1788
  context += `
1380
1789
  ## Plan Summary
1381
1790
  ${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
1382
1791
  `;
1383
1792
  }
1384
- const contextMdPath = path5.join(taskDir, "context.md");
1385
- if (fs5.existsSync(contextMdPath)) {
1386
- const accumulated = fs5.readFileSync(contextMdPath, "utf-8");
1793
+ const contextMdPath = path11.join(taskDir, "context.md");
1794
+ if (fs13.existsSync(contextMdPath)) {
1795
+ const accumulated = fs13.readFileSync(contextMdPath, "utf-8");
1387
1796
  const truncated = accumulated.slice(-MAX_ACCUMULATED_CONTEXT);
1388
1797
  const prefix = accumulated.length > MAX_ACCUMULATED_CONTEXT ? "...(earlier context truncated)\n" : "";
1389
1798
  context += `
@@ -1399,12 +1808,22 @@ ${feedback}
1399
1808
  }
1400
1809
  return prompt.replace("{{TASK_CONTEXT}}", context);
1401
1810
  }
1811
+ function inferHasUIFromScope(scope) {
1812
+ return scope.some((filePath) => {
1813
+ const ext = path11.extname(filePath).toLowerCase();
1814
+ if (UI_EXTENSIONS.has(ext)) return true;
1815
+ const normalized = filePath.replace(/\\/g, "/");
1816
+ return UI_PATH_SEGMENTS.some((seg) => normalized.includes(seg));
1817
+ });
1818
+ }
1402
1819
  function taskHasUI(taskDir) {
1403
- const taskJsonPath = path5.join(taskDir, "task.json");
1404
- if (!fs5.existsSync(taskJsonPath)) return true;
1820
+ const taskJsonPath = path11.join(taskDir, "task.json");
1821
+ if (!fs13.existsSync(taskJsonPath)) return true;
1405
1822
  try {
1406
- const taskDef = JSON.parse(fs5.readFileSync(taskJsonPath, "utf-8"));
1407
- return taskDef.hasUI !== false;
1823
+ const taskDef = JSON.parse(fs13.readFileSync(taskJsonPath, "utf-8"));
1824
+ const scope = Array.isArray(taskDef.scope) ? taskDef.scope : [];
1825
+ if (scope.length === 0) return true;
1826
+ return inferHasUIFromScope(scope);
1408
1827
  } catch {
1409
1828
  return true;
1410
1829
  }
@@ -1523,6 +1942,11 @@ ${prompt}` : prompt;
1523
1942
  }
1524
1943
  if (isMcpEnabledForStage(stageName, config.mcp) && taskHasUI(taskDir)) {
1525
1944
  assembled = assembled + "\n\n" + getBrowserToolGuidance(stageName, taskDir);
1945
+ const qaGuidePath = path11.join(projectDir, ".kody", "qa-guide.md");
1946
+ if (fs13.existsSync(qaGuidePath)) {
1947
+ const qaGuide = fs13.readFileSync(qaGuidePath, "utf-8").trim();
1948
+ assembled = assembled + "\n\n" + qaGuide;
1949
+ }
1526
1950
  }
1527
1951
  return assembled;
1528
1952
  }
@@ -1544,13 +1968,16 @@ ${prompt}` : prompt;
1544
1968
  }
1545
1969
  return assembled;
1546
1970
  }
1971
+ function escalateModelTier(currentTier) {
1972
+ return TIER_ESCALATION[currentTier] ?? "strong";
1973
+ }
1547
1974
  function resolveModel(modelTier, stageName) {
1548
1975
  const config = getProjectConfig();
1549
1976
  const mapped = config.agent.modelMap[modelTier];
1550
1977
  if (mapped) return mapped;
1551
1978
  return DEFAULT_MODEL_MAP[modelTier] ?? "sonnet";
1552
1979
  }
1553
- var DEFAULT_MODEL_MAP, MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT;
1980
+ var DEFAULT_MODEL_MAP, MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT, UI_EXTENSIONS, UI_PATH_SEGMENTS, TIER_ESCALATION;
1554
1981
  var init_context = __esm({
1555
1982
  "src/context.ts"() {
1556
1983
  "use strict";
@@ -1566,69 +1993,43 @@ var init_context = __esm({
1566
1993
  MAX_TASK_CONTEXT_PLAN = 1500;
1567
1994
  MAX_TASK_CONTEXT_SPEC = 2e3;
1568
1995
  MAX_ACCUMULATED_CONTEXT = 4e3;
1996
+ UI_EXTENSIONS = /* @__PURE__ */ new Set([
1997
+ ".tsx",
1998
+ ".jsx",
1999
+ ".vue",
2000
+ ".svelte",
2001
+ ".css",
2002
+ ".scss",
2003
+ ".sass",
2004
+ ".less",
2005
+ ".html"
2006
+ ]);
2007
+ UI_PATH_SEGMENTS = [
2008
+ "/components/",
2009
+ "/pages/",
2010
+ "/layouts/",
2011
+ "/styles/",
2012
+ "/views/"
2013
+ ];
2014
+ TIER_ESCALATION = {
2015
+ cheap: "mid",
2016
+ mid: "strong",
2017
+ strong: "strong"
2018
+ };
1569
2019
  }
1570
2020
  });
1571
2021
 
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
-
1621
- // src/pipeline/runner-selection.ts
1622
- function getRunnerForStage(ctx, stageName) {
1623
- const config = getProjectConfig();
1624
- const runnerName = config.agent.stageRunners?.[stageName] ?? config.agent.defaultRunner ?? Object.keys(ctx.runners)[0] ?? "claude";
1625
- const runner = ctx.runners[runnerName];
1626
- if (!runner) {
1627
- throw new Error(
1628
- `Runner "${runnerName}" not found for stage ${stageName}. Available: ${Object.keys(ctx.runners).join(", ")}`
1629
- );
1630
- }
1631
- return runner;
2022
+ // src/pipeline/runner-selection.ts
2023
+ function getRunnerForStage(ctx, stageName) {
2024
+ const config = getProjectConfig();
2025
+ const runnerName = config.agent.stageRunners?.[stageName] ?? config.agent.defaultRunner ?? Object.keys(ctx.runners)[0] ?? "claude";
2026
+ const runner = ctx.runners[runnerName];
2027
+ if (!runner) {
2028
+ throw new Error(
2029
+ `Runner "${runnerName}" not found for stage ${stageName}. Available: ${Object.keys(ctx.runners).join(", ")}`
2030
+ );
2031
+ }
2032
+ return runner;
1632
2033
  }
1633
2034
  var init_runner_selection = __esm({
1634
2035
  "src/pipeline/runner-selection.ts"() {
@@ -1638,8 +2039,8 @@ var init_runner_selection = __esm({
1638
2039
  });
1639
2040
 
1640
2041
  // src/stages/agent.ts
1641
- import * as fs6 from "fs";
1642
- import * as path6 from "path";
2042
+ import * as fs14 from "fs";
2043
+ import * as path12 from "path";
1643
2044
  function getSessionInfo(stageName, sessions) {
1644
2045
  const group = SESSION_GROUP[stageName];
1645
2046
  if (!group) return void 0;
@@ -1669,15 +2070,19 @@ async function executeAgentStage(ctx, def) {
1669
2070
  return { outcome: "completed", retries: 0 };
1670
2071
  }
1671
2072
  const prompt = buildFullPrompt(def.name, ctx.taskId, ctx.taskDir, ctx.projectDir, ctx.input.feedback);
1672
- const model = resolveModel(def.modelTier, def.name);
2073
+ let currentModelTier = def.modelTier;
1673
2074
  if (ctx.input.feedback && def.name === "build") {
1674
2075
  logger.info(` feedback: ${ctx.input.feedback.slice(0, 200)}${ctx.input.feedback.length > 200 ? "..." : ""}`);
1675
2076
  }
1676
2077
  const config = getProjectConfig();
2078
+ const sc = resolveStageConfig(config, def.name, def.modelTier);
2079
+ let model = sc.model;
2080
+ const useProxy = stageNeedsProxy(sc);
2081
+ const escalateEnabled = config.agent.escalateOnTimeout !== false;
1677
2082
  const runnerName = config.agent.stageRunners?.[def.name] ?? config.agent.defaultRunner ?? Object.keys(ctx.runners)[0] ?? "claude";
1678
- logger.info(` runner=${runnerName} model=${model} timeout=${def.timeout / 1e3}s`);
2083
+ logger.info(` runner=${runnerName} provider=${sc.provider} model=${model} timeout=${def.timeout / 1e3}s`);
1679
2084
  const extraEnv = {};
1680
- if (needsLitellmProxy(config)) {
2085
+ if (useProxy) {
1681
2086
  extraEnv.ANTHROPIC_BASE_URL = getLitellmUrl();
1682
2087
  }
1683
2088
  const sessions = ctx.sessions ?? {};
@@ -1685,42 +2090,64 @@ async function executeAgentStage(ctx, def) {
1685
2090
  if (sessionInfo) {
1686
2091
  logger.info(` session: ${SESSION_GROUP[def.name]} (${sessionInfo.resumeSession ? "resume" : "new"})`);
1687
2092
  }
1688
- const mcpConfigJson = isMcpEnabledForStage(def.name, config.mcp) ? buildMcpConfigJson(config.mcp) : void 0;
2093
+ const mcpForStage = isMcpEnabledForStage(def.name, config.mcp) ? withPlaywrightIfNeeded(config.mcp, taskHasUI(ctx.taskDir)) : void 0;
2094
+ const mcpConfigJson = buildMcpConfigJson(mcpForStage);
1689
2095
  if (mcpConfigJson) {
1690
2096
  logger.info(` MCP servers enabled for ${def.name}`);
1691
2097
  }
1692
2098
  const runner = getRunnerForStage(ctx, def.name);
1693
- const result = await runner.run(def.name, prompt, model, def.timeout, ctx.taskDir, {
2099
+ const maxRetries = def.maxRetries ?? 0;
2100
+ let lastResult = await runner.run(def.name, prompt, model, def.timeout, ctx.taskDir, {
1694
2101
  cwd: ctx.projectDir,
1695
2102
  env: extraEnv,
1696
2103
  ...sessionInfo,
1697
2104
  mcpConfigJson
1698
2105
  });
1699
- if (result.outcome !== "completed") {
1700
- return { outcome: result.outcome, error: result.error, retries: 0 };
2106
+ let retries = 0;
2107
+ while (lastResult.outcome !== "completed" && retries < maxRetries) {
2108
+ retries++;
2109
+ const isTimeout = lastResult.outcome === "timed_out";
2110
+ if (isTimeout && escalateEnabled) {
2111
+ const nextTier = escalateModelTier(currentModelTier);
2112
+ if (nextTier !== currentModelTier) {
2113
+ logger.info(` Escalating model from ${currentModelTier} to ${nextTier} after timeout`);
2114
+ currentModelTier = nextTier;
2115
+ model = resolveModel(currentModelTier, def.name);
2116
+ }
2117
+ }
2118
+ logger.info(` retry ${retries}/${maxRetries} with model=${model}`);
2119
+ lastResult = await runner.run(def.name, prompt, model, def.timeout, ctx.taskDir, {
2120
+ cwd: ctx.projectDir,
2121
+ env: extraEnv,
2122
+ mcpConfigJson
2123
+ });
2124
+ }
2125
+ if (lastResult.outcome !== "completed") {
2126
+ return { outcome: lastResult.outcome, error: lastResult.error, retries };
1701
2127
  }
2128
+ const result = lastResult;
1702
2129
  if (def.outputFile && result.output) {
1703
- fs6.writeFileSync(path6.join(ctx.taskDir, def.outputFile), result.output);
2130
+ fs14.writeFileSync(path12.join(ctx.taskDir, def.outputFile), result.output);
1704
2131
  }
1705
2132
  if (def.outputFile) {
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);
2133
+ const outputPath = path12.join(ctx.taskDir, def.outputFile);
2134
+ if (!fs14.existsSync(outputPath)) {
2135
+ const ext = path12.extname(def.outputFile);
2136
+ const base = path12.basename(def.outputFile, ext);
2137
+ const files = fs14.readdirSync(ctx.taskDir);
1711
2138
  const variant = files.find(
1712
2139
  (f) => f.startsWith(base + "-") && f.endsWith(ext)
1713
2140
  );
1714
2141
  if (variant) {
1715
- fs6.renameSync(path6.join(ctx.taskDir, variant), outputPath);
2142
+ fs14.renameSync(path12.join(ctx.taskDir, variant), outputPath);
1716
2143
  logger.info(` Renamed variant ${variant} \u2192 ${def.outputFile}`);
1717
2144
  }
1718
2145
  }
1719
2146
  }
1720
2147
  if (def.outputFile) {
1721
- const outputPath = path6.join(ctx.taskDir, def.outputFile);
1722
- if (fs6.existsSync(outputPath)) {
1723
- const content = fs6.readFileSync(outputPath, "utf-8");
2148
+ const outputPath = path12.join(ctx.taskDir, def.outputFile);
2149
+ if (fs14.existsSync(outputPath)) {
2150
+ const content = fs14.readFileSync(outputPath, "utf-8");
1724
2151
  const validation = validateStageOutput(def.name, content);
1725
2152
  if (!validation.valid) {
1726
2153
  if (def.name === "taskify") {
@@ -1734,7 +2161,7 @@ async function executeAgentStage(ctx, def) {
1734
2161
  const stripped = stripFences(retryResult.output);
1735
2162
  const retryValidation = validateTaskJson(stripped);
1736
2163
  if (retryValidation.valid) {
1737
- fs6.writeFileSync(outputPath, retryResult.output);
2164
+ fs14.writeFileSync(outputPath, retryResult.output);
1738
2165
  logger.info(` taskify retry produced valid JSON`);
1739
2166
  } else {
1740
2167
  logger.warn(` taskify retry still invalid: ${retryValidation.error}`);
@@ -1745,10 +2172,9 @@ async function executeAgentStage(ctx, def) {
1745
2172
  description: plainText.slice(0, 500),
1746
2173
  scope: [],
1747
2174
  risk_level: "low",
1748
- hasUI: false,
1749
2175
  questions: []
1750
2176
  }, null, 2);
1751
- fs6.writeFileSync(outputPath, fallback);
2177
+ fs14.writeFileSync(outputPath, fallback);
1752
2178
  logger.info(` taskify fallback: generated minimal task.json (risk_level=low)`);
1753
2179
  }
1754
2180
  }
@@ -1759,10 +2185,10 @@ async function executeAgentStage(ctx, def) {
1759
2185
  }
1760
2186
  }
1761
2187
  appendStageContext(ctx.taskDir, def.name, result.output);
1762
- return { outcome: "completed", outputFile: def.outputFile, retries: 0 };
2188
+ return { outcome: "completed", outputFile: def.outputFile, retries };
1763
2189
  }
1764
2190
  function appendStageContext(taskDir, stageName, output) {
1765
- const contextPath = path6.join(taskDir, "context.md");
2191
+ const contextPath = path12.join(taskDir, "context.md");
1766
2192
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19);
1767
2193
  let summary;
1768
2194
  if (output && output.trim()) {
@@ -1775,7 +2201,7 @@ function appendStageContext(taskDir, stageName, output) {
1775
2201
  ### ${stageName} (${timestamp2})
1776
2202
  ${summary}
1777
2203
  `;
1778
- fs6.appendFileSync(contextPath, entry);
2204
+ fs14.appendFileSync(contextPath, entry);
1779
2205
  }
1780
2206
  var SESSION_GROUP;
1781
2207
  var init_agent = __esm({
@@ -1798,7 +2224,7 @@ var init_agent = __esm({
1798
2224
  });
1799
2225
 
1800
2226
  // src/verify-runner.ts
1801
- import { execFileSync as execFileSync4 } from "child_process";
2227
+ import { execFileSync as execFileSync9 } from "child_process";
1802
2228
  function isExecError(err) {
1803
2229
  return typeof err === "object" && err !== null;
1804
2230
  }
@@ -1834,7 +2260,7 @@ function runCommand(cmd, cwd, timeout) {
1834
2260
  return { success: true, output: "", timedOut: false };
1835
2261
  }
1836
2262
  try {
1837
- const output = execFileSync4(parts[0], parts.slice(1), {
2263
+ const output = execFileSync9(parts[0], parts.slice(1), {
1838
2264
  cwd,
1839
2265
  timeout,
1840
2266
  encoding: "utf-8",
@@ -1905,7 +2331,7 @@ var init_verify_runner = __esm({
1905
2331
  });
1906
2332
 
1907
2333
  // src/observer.ts
1908
- import { execFileSync as execFileSync5 } from "child_process";
2334
+ import { execFileSync as execFileSync10 } from "child_process";
1909
2335
  async function diagnoseFailure(stageName, errorOutput, modifiedFiles, runner, model, options) {
1910
2336
  const context = [
1911
2337
  `Stage: ${stageName}`,
@@ -1930,42 +2356,48 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
1930
2356
  );
1931
2357
  if (result.outcome === "completed" && result.output) {
1932
2358
  const cleaned = result.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
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
- };
2359
+ const parseResult = parseJsonSafe(cleaned, ["classification"]);
2360
+ if (parseResult.ok) {
2361
+ const { data } = parseResult;
2362
+ const validClassifications = [
2363
+ "fixable",
2364
+ "infrastructure",
2365
+ "pre-existing",
2366
+ "retry",
2367
+ "abort"
2368
+ ];
2369
+ if (validClassifications.includes(data.classification)) {
2370
+ logger.info(` Diagnosis: ${data.classification} \u2014 ${data.reason}`);
2371
+ return {
2372
+ classification: data.classification,
2373
+ reason: data.reason ?? "Unknown reason",
2374
+ resolution: data.resolution ?? ""
2375
+ };
2376
+ }
2377
+ logger.warn(` Diagnosis returned invalid classification: ${data.classification}`);
2378
+ } else {
2379
+ logger.warn(` Diagnosis JSON invalid: ${parseResult.error}`);
1948
2380
  }
1949
2381
  }
1950
2382
  } catch (err) {
1951
2383
  logger.warn(` Diagnosis error: ${err instanceof Error ? err.message : err}`);
1952
2384
  }
1953
- logger.warn(" Diagnosis failed \u2014 defaulting to fixable");
2385
+ logger.warn(" Diagnosis failed \u2014 defaulting to retry");
1954
2386
  return {
1955
- classification: "fixable",
1956
- reason: "Could not diagnose failure",
2387
+ classification: "retry",
2388
+ reason: "Could not diagnose failure \u2014 retrying gate",
1957
2389
  resolution: errorOutput.slice(-500)
1958
2390
  };
1959
2391
  }
1960
2392
  function getModifiedFiles(projectDir) {
1961
2393
  try {
1962
- const staged = execFileSync5("git", ["diff", "--name-only", "--cached"], {
2394
+ const staged = execFileSync10("git", ["diff", "--name-only", "--cached"], {
1963
2395
  encoding: "utf-8",
1964
2396
  cwd: projectDir,
1965
2397
  timeout: 5e3,
1966
2398
  stdio: ["pipe", "pipe", "pipe"]
1967
2399
  }).trim();
1968
- const unstaged = execFileSync5("git", ["diff", "--name-only"], {
2400
+ const unstaged = execFileSync10("git", ["diff", "--name-only"], {
1969
2401
  encoding: "utf-8",
1970
2402
  cwd: projectDir,
1971
2403
  timeout: 5e3,
@@ -1974,7 +2406,8 @@ function getModifiedFiles(projectDir) {
1974
2406
  const all = `${staged}
1975
2407
  ${unstaged}`.split("\n").filter(Boolean);
1976
2408
  return [...new Set(all)];
1977
- } catch {
2409
+ } catch (err) {
2410
+ logger.warn(` Failed to get modified files: ${err instanceof Error ? err.message : String(err)}`);
1978
2411
  return [];
1979
2412
  }
1980
2413
  }
@@ -1983,6 +2416,7 @@ var init_observer = __esm({
1983
2416
  "src/observer.ts"() {
1984
2417
  "use strict";
1985
2418
  init_logger();
2419
+ init_validators();
1986
2420
  DIAGNOSIS_PROMPT = `You are a pipeline failure diagnosis agent. Analyze the error and classify it.
1987
2421
 
1988
2422
  Output ONLY valid JSON. No markdown fences. No explanation.
@@ -2006,8 +2440,8 @@ Error context:
2006
2440
  });
2007
2441
 
2008
2442
  // src/stages/gate.ts
2009
- import * as fs7 from "fs";
2010
- import * as path7 from "path";
2443
+ import * as fs15 from "fs";
2444
+ import * as path13 from "path";
2011
2445
  function executeGateStage(ctx, def) {
2012
2446
  if (ctx.input.dryRun) {
2013
2447
  logger.info(` [dry-run] skipping ${def.name}`);
@@ -2050,7 +2484,7 @@ ${output}
2050
2484
  `);
2051
2485
  }
2052
2486
  }
2053
- fs7.writeFileSync(path7.join(ctx.taskDir, "verify.md"), lines.join(""));
2487
+ fs15.writeFileSync(path13.join(ctx.taskDir, "verify.md"), lines.join(""));
2054
2488
  return {
2055
2489
  outcome: verifyResult.pass ? "completed" : "failed",
2056
2490
  retries: 0
@@ -2065,9 +2499,9 @@ var init_gate = __esm({
2065
2499
  });
2066
2500
 
2067
2501
  // src/stages/verify.ts
2068
- import * as fs8 from "fs";
2069
- import * as path8 from "path";
2070
- import { execFileSync as execFileSync6 } from "child_process";
2502
+ import * as fs16 from "fs";
2503
+ import * as path14 from "path";
2504
+ import { execFileSync as execFileSync11 } from "child_process";
2071
2505
  async function executeVerifyWithAutofix(ctx, def) {
2072
2506
  const maxAttempts = def.maxRetries ?? 2;
2073
2507
  for (let attempt = 0; attempt <= maxAttempts; attempt++) {
@@ -2077,8 +2511,8 @@ async function executeVerifyWithAutofix(ctx, def) {
2077
2511
  return { ...gateResult, retries: attempt };
2078
2512
  }
2079
2513
  if (attempt < maxAttempts) {
2080
- const verifyPath = path8.join(ctx.taskDir, "verify.md");
2081
- const errorOutput = fs8.existsSync(verifyPath) ? fs8.readFileSync(verifyPath, "utf-8") : "Unknown error";
2514
+ const verifyPath = path14.join(ctx.taskDir, "verify.md");
2515
+ const errorOutput = fs16.existsSync(verifyPath) ? fs16.readFileSync(verifyPath, "utf-8") : "Unknown error";
2082
2516
  const modifiedFiles = getModifiedFiles(ctx.projectDir);
2083
2517
  const defaultRunner = getRunnerForStage(ctx, "taskify");
2084
2518
  const diagConfig = getProjectConfig();
@@ -2121,7 +2555,7 @@ ${diagnosis.resolution}`);
2121
2555
  const parts = parseCommand(cmd);
2122
2556
  if (parts.length === 0) return;
2123
2557
  try {
2124
- execFileSync6(parts[0], parts.slice(1), {
2558
+ execFileSync11(parts[0], parts.slice(1), {
2125
2559
  stdio: "pipe",
2126
2560
  timeout: FIX_COMMAND_TIMEOUT_MS
2127
2561
  });
@@ -2174,18 +2608,18 @@ var init_verify = __esm({
2174
2608
  });
2175
2609
 
2176
2610
  // src/cli/task-resolution.ts
2177
- import * as fs9 from "fs";
2178
- import * as path9 from "path";
2179
- import { execFileSync as execFileSync7 } from "child_process";
2611
+ import * as fs17 from "fs";
2612
+ import * as path15 from "path";
2613
+ import { execFileSync as execFileSync12 } from "child_process";
2180
2614
  function findLatestTaskForIssue(issueNumber, projectDir) {
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();
2615
+ const tasksDir = path15.join(projectDir, ".kody", "tasks");
2616
+ if (!fs17.existsSync(tasksDir)) return null;
2617
+ const allDirs = fs17.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
2184
2618
  const prefix = `${issueNumber}-`;
2185
2619
  const direct = allDirs.find((d) => d.startsWith(prefix));
2186
2620
  if (direct) return direct;
2187
2621
  try {
2188
- const branch = execFileSync7("git", ["branch", "--show-current"], {
2622
+ const branch = execFileSync12("git", ["branch", "--show-current"], {
2189
2623
  encoding: "utf-8",
2190
2624
  cwd: projectDir,
2191
2625
  timeout: 5e3,
@@ -2207,15 +2641,39 @@ function generateTaskId() {
2207
2641
  const pad = (n) => String(n).padStart(2, "0");
2208
2642
  return `${String(now.getFullYear()).slice(2)}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
2209
2643
  }
2644
+ function resolveTaskIdFromComments(issueNumber) {
2645
+ try {
2646
+ const comments = getIssueComments(issueNumber);
2647
+ const pattern = /pipeline started: `([^`]+)`/;
2648
+ let latestTaskId = null;
2649
+ for (const comment of comments) {
2650
+ const match = comment.body.match(pattern);
2651
+ if (match) {
2652
+ latestTaskId = match[1];
2653
+ }
2654
+ }
2655
+ return latestTaskId;
2656
+ } catch {
2657
+ return null;
2658
+ }
2659
+ }
2660
+ function resolveTaskIdForCommand(issueNumber, projectDir) {
2661
+ const fromTasks = findLatestTaskForIssue(issueNumber, projectDir);
2662
+ if (fromTasks) return fromTasks;
2663
+ const fromComments = resolveTaskIdFromComments(issueNumber);
2664
+ if (fromComments) return fromComments;
2665
+ return null;
2666
+ }
2210
2667
  var init_task_resolution = __esm({
2211
2668
  "src/cli/task-resolution.ts"() {
2212
2669
  "use strict";
2670
+ init_github_api();
2213
2671
  }
2214
2672
  });
2215
2673
 
2216
2674
  // src/review-standalone.ts
2217
- import * as fs10 from "fs";
2218
- import * as path10 from "path";
2675
+ import * as fs18 from "fs";
2676
+ import * as path16 from "path";
2219
2677
  function resolveReviewTarget(input) {
2220
2678
  if (input.prs.length === 0) {
2221
2679
  return {
@@ -2239,17 +2697,35 @@ Or comment on the specific PR: \`@kody review\``
2239
2697
  }
2240
2698
  async function runStandaloneReview(input) {
2241
2699
  const taskId = input.taskId ?? `review-${generateTaskId()}`;
2242
- const taskDir = path10.join(input.projectDir, ".kody", "tasks", taskId);
2243
- fs10.mkdirSync(taskDir, { recursive: true });
2244
- const diffInstruction = input.baseBranch ? `
2700
+ const taskDir = path16.join(input.projectDir, ".kody", "tasks", taskId);
2701
+ fs18.mkdirSync(taskDir, { recursive: true });
2702
+ let diffInstruction = "";
2703
+ let filesChangedSection = "";
2704
+ if (input.baseBranch) {
2705
+ diffInstruction = `
2245
2706
 
2246
2707
  ## Diff Command
2247
2708
  Run: \`git diff origin/${input.baseBranch}...HEAD\` to see the PR changes.
2248
- Do NOT use bare \`git diff\` \u2014 it shows only uncommitted working tree changes, not the PR diff.` : "";
2709
+ Do NOT use bare \`git diff\` \u2014 it shows only uncommitted working tree changes, not the PR diff.`;
2710
+ const diffFiles = getDiffFiles(input.baseBranch, input.projectDir);
2711
+ if (diffFiles.length > 0) {
2712
+ logger.info(`[review] Review scope: git diff origin/${input.baseBranch}...HEAD (${diffFiles.length} files)`);
2713
+ const fileList = diffFiles.map((f) => `- ${f}`).join("\n");
2714
+ filesChangedSection = `
2715
+
2716
+ ## Files Changed
2717
+ Only review the following ${diffFiles.length} files (these are the files changed in this PR):
2718
+ ${fileList}`;
2719
+ } else {
2720
+ logger.info(`[review] Review scope: git diff origin/${input.baseBranch}...HEAD (0 files)`);
2721
+ }
2722
+ } else {
2723
+ logger.warn(`[review] No baseBranch provided \u2014 reviewing all files (no diff scope)`);
2724
+ }
2249
2725
  const taskContent = `# ${input.prTitle}
2250
2726
 
2251
- ${input.prBody ?? ""}${diffInstruction}`;
2252
- fs10.writeFileSync(path10.join(taskDir, "task.md"), taskContent);
2727
+ ${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
2728
+ fs18.writeFileSync(path16.join(taskDir, "task.md"), taskContent);
2253
2729
  const reviewDef = STAGES.find((s) => s.name === "review");
2254
2730
  const ctx = {
2255
2731
  taskId,
@@ -2271,10 +2747,10 @@ ${input.prBody ?? ""}${diffInstruction}`;
2271
2747
  error: result.error ?? "Review stage failed"
2272
2748
  };
2273
2749
  }
2274
- const reviewPath = path10.join(taskDir, "review.md");
2750
+ const reviewPath = path16.join(taskDir, "review.md");
2275
2751
  let reviewContent;
2276
- if (fs10.existsSync(reviewPath)) {
2277
- reviewContent = fs10.readFileSync(reviewPath, "utf-8");
2752
+ if (fs18.existsSync(reviewPath)) {
2753
+ reviewContent = fs18.readFileSync(reviewPath, "utf-8");
2278
2754
  }
2279
2755
  return {
2280
2756
  outcome: "completed",
@@ -2309,12 +2785,13 @@ var init_review_standalone = __esm({
2309
2785
  init_agent();
2310
2786
  init_task_resolution();
2311
2787
  init_logger();
2788
+ init_git_utils();
2312
2789
  }
2313
2790
  });
2314
2791
 
2315
2792
  // src/stages/review.ts
2316
- import * as fs11 from "fs";
2317
- import * as path11 from "path";
2793
+ import * as fs19 from "fs";
2794
+ import * as path17 from "path";
2318
2795
  async function executeReviewWithFix(ctx, def) {
2319
2796
  if (ctx.input.dryRun) {
2320
2797
  return { outcome: "completed", retries: 0 };
@@ -2328,11 +2805,11 @@ async function executeReviewWithFix(ctx, def) {
2328
2805
  if (reviewResult.outcome !== "completed") {
2329
2806
  return reviewResult;
2330
2807
  }
2331
- const reviewFile = path11.join(ctx.taskDir, "review.md");
2332
- if (!fs11.existsSync(reviewFile)) {
2808
+ const reviewFile = path17.join(ctx.taskDir, "review.md");
2809
+ if (!fs19.existsSync(reviewFile)) {
2333
2810
  return { outcome: "failed", retries: iteration, error: "review.md not found" };
2334
2811
  }
2335
- const content = fs11.readFileSync(reviewFile, "utf-8");
2812
+ const content = fs19.readFileSync(reviewFile, "utf-8");
2336
2813
  if (detectReviewVerdict(content) !== "fail") {
2337
2814
  return { ...reviewResult, retries: iteration };
2338
2815
  }
@@ -2361,15 +2838,15 @@ var init_review = __esm({
2361
2838
  });
2362
2839
 
2363
2840
  // src/stages/ship.ts
2364
- import * as fs12 from "fs";
2365
- import * as path12 from "path";
2366
- import { execFileSync as execFileSync8 } from "child_process";
2841
+ import * as fs20 from "fs";
2842
+ import * as path18 from "path";
2843
+ import { execFileSync as execFileSync13 } from "child_process";
2367
2844
  function buildPrBody(ctx) {
2368
2845
  const sections = [];
2369
- const taskJsonPath = path12.join(ctx.taskDir, "task.json");
2370
- if (fs12.existsSync(taskJsonPath)) {
2846
+ const taskJsonPath = path18.join(ctx.taskDir, "task.json");
2847
+ if (fs20.existsSync(taskJsonPath)) {
2371
2848
  try {
2372
- const raw = fs12.readFileSync(taskJsonPath, "utf-8");
2849
+ const raw = fs20.readFileSync(taskJsonPath, "utf-8");
2373
2850
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2374
2851
  const task = JSON.parse(cleaned);
2375
2852
  if (task.description) {
@@ -2388,9 +2865,9 @@ ${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
2388
2865
  } catch {
2389
2866
  }
2390
2867
  }
2391
- const reviewPath = path12.join(ctx.taskDir, "review.md");
2392
- if (fs12.existsSync(reviewPath)) {
2393
- const review = fs12.readFileSync(reviewPath, "utf-8");
2868
+ const reviewPath = path18.join(ctx.taskDir, "review.md");
2869
+ if (fs20.existsSync(reviewPath)) {
2870
+ const review = fs20.readFileSync(reviewPath, "utf-8");
2394
2871
  const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
2395
2872
  if (summaryMatch) {
2396
2873
  const summary = summaryMatch[1].trim();
@@ -2407,14 +2884,14 @@ ${summary}`);
2407
2884
  **Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
2408
2885
  }
2409
2886
  }
2410
- const verifyPath = path12.join(ctx.taskDir, "verify.md");
2411
- if (fs12.existsSync(verifyPath)) {
2412
- const verify = fs12.readFileSync(verifyPath, "utf-8");
2887
+ const verifyPath = path18.join(ctx.taskDir, "verify.md");
2888
+ if (fs20.existsSync(verifyPath)) {
2889
+ const verify = fs20.readFileSync(verifyPath, "utf-8");
2413
2890
  if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
2414
2891
  }
2415
- const planPath = path12.join(ctx.taskDir, "plan.md");
2416
- if (fs12.existsSync(planPath)) {
2417
- const plan = fs12.readFileSync(planPath, "utf-8").trim();
2892
+ const planPath = path18.join(ctx.taskDir, "plan.md");
2893
+ if (fs20.existsSync(planPath)) {
2894
+ const plan = fs20.readFileSync(planPath, "utf-8").trim();
2418
2895
  if (plan) {
2419
2896
  const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
2420
2897
  sections.push(`
@@ -2434,25 +2911,25 @@ Closes #${ctx.input.issueNumber}`);
2434
2911
  return sections.join("\n");
2435
2912
  }
2436
2913
  function executeShipStage(ctx, _def) {
2437
- const shipPath = path12.join(ctx.taskDir, "ship.md");
2914
+ const shipPath = path18.join(ctx.taskDir, "ship.md");
2438
2915
  if (ctx.input.dryRun) {
2439
- fs12.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
2916
+ fs20.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
2440
2917
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
2441
2918
  }
2442
2919
  if (ctx.input.local && !ctx.input.issueNumber) {
2443
- fs12.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
2920
+ fs20.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
2444
2921
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
2445
2922
  }
2446
2923
  try {
2447
2924
  const head = getCurrentBranch(ctx.projectDir);
2448
2925
  const base = getDefaultBranch(ctx.projectDir);
2449
2926
  try {
2450
- execFileSync8("git", ["add", ctx.taskDir], {
2927
+ execFileSync13("git", ["add", ctx.taskDir], {
2451
2928
  cwd: ctx.projectDir,
2452
2929
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
2453
2930
  stdio: "pipe"
2454
2931
  });
2455
- execFileSync8("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
2932
+ execFileSync13("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
2456
2933
  cwd: ctx.projectDir,
2457
2934
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
2458
2935
  stdio: "pipe"
@@ -2466,7 +2943,7 @@ function executeShipStage(ctx, _def) {
2466
2943
  let repo = config.github?.repo;
2467
2944
  if (!owner || !repo) {
2468
2945
  try {
2469
- const remoteUrl = execFileSync8("git", ["remote", "get-url", "origin"], {
2946
+ const remoteUrl = execFileSync13("git", ["remote", "get-url", "origin"], {
2470
2947
  encoding: "utf-8",
2471
2948
  cwd: ctx.projectDir
2472
2949
  }).trim();
@@ -2487,28 +2964,28 @@ function executeShipStage(ctx, _def) {
2487
2964
  chore: "chore"
2488
2965
  };
2489
2966
  let prefix = "chore";
2490
- const taskJsonPath = path12.join(ctx.taskDir, "task.json");
2491
- if (fs12.existsSync(taskJsonPath)) {
2967
+ const taskJsonPath = path18.join(ctx.taskDir, "task.json");
2968
+ if (fs20.existsSync(taskJsonPath)) {
2492
2969
  try {
2493
- const raw = fs12.readFileSync(taskJsonPath, "utf-8");
2970
+ const raw = fs20.readFileSync(taskJsonPath, "utf-8");
2494
2971
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2495
2972
  const task = JSON.parse(cleaned);
2496
2973
  prefix = TYPE_PREFIX[task.task_type] ?? "chore";
2497
2974
  } catch {
2498
2975
  }
2499
2976
  }
2500
- const taskMdPath = path12.join(ctx.taskDir, "task.md");
2501
- if (fs12.existsSync(taskMdPath)) {
2502
- const content = fs12.readFileSync(taskMdPath, "utf-8");
2977
+ const taskMdPath = path18.join(ctx.taskDir, "task.md");
2978
+ if (fs20.existsSync(taskMdPath)) {
2979
+ const content = fs20.readFileSync(taskMdPath, "utf-8");
2503
2980
  const heading = content.split("\n").find((l) => l.startsWith("# "));
2504
2981
  if (heading) {
2505
2982
  title = `${prefix}: ${heading.replace(/^#\s*/, "").trim()}`.slice(0, 72);
2506
2983
  }
2507
2984
  }
2508
2985
  if (title === "Update") {
2509
- if (fs12.existsSync(taskJsonPath)) {
2986
+ if (fs20.existsSync(taskJsonPath)) {
2510
2987
  try {
2511
- const raw = fs12.readFileSync(taskJsonPath, "utf-8");
2988
+ const raw = fs20.readFileSync(taskJsonPath, "utf-8");
2512
2989
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2513
2990
  const task = JSON.parse(cleaned);
2514
2991
  if (task.title) title = `${prefix}: ${task.title}`.slice(0, 72);
@@ -2531,7 +3008,7 @@ function executeShipStage(ctx, _def) {
2531
3008
  } catch {
2532
3009
  }
2533
3010
  }
2534
- fs12.writeFileSync(shipPath, `# Ship
3011
+ fs20.writeFileSync(shipPath, `# Ship
2535
3012
 
2536
3013
  Updated existing PR: ${existingPr.url}
2537
3014
  PR #${existingPr.number}
@@ -2552,22 +3029,26 @@ PR #${existingPr.number}
2552
3029
  } catch {
2553
3030
  }
2554
3031
  }
2555
- fs12.writeFileSync(shipPath, `# Ship
3032
+ fs20.writeFileSync(shipPath, `# Ship
2556
3033
 
2557
3034
  PR created: ${pr.url}
2558
3035
  PR #${pr.number}
2559
3036
  `);
2560
3037
  } else {
2561
- fs12.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
3038
+ fs20.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
2562
3039
  }
2563
3040
  }
2564
3041
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
2565
3042
  } catch (err) {
2566
3043
  const msg = err instanceof Error ? err.message : String(err);
2567
- fs12.writeFileSync(shipPath, `# Ship
3044
+ try {
3045
+ fs20.writeFileSync(shipPath, `# Ship
2568
3046
 
2569
3047
  Failed: ${msg}
2570
3048
  `);
3049
+ } catch {
3050
+ logger.warn(` Failed to write ship.md artifact`);
3051
+ }
2571
3052
  return { outcome: "failed", retries: 0, error: msg };
2572
3053
  }
2573
3054
  }
@@ -2610,15 +3091,15 @@ var init_executor_registry = __esm({
2610
3091
  });
2611
3092
 
2612
3093
  // src/pipeline/questions.ts
2613
- import * as fs13 from "fs";
2614
- import * as path13 from "path";
3094
+ import * as fs21 from "fs";
3095
+ import * as path19 from "path";
2615
3096
  function checkForQuestions(ctx, stageName) {
2616
3097
  if (ctx.input.local || !ctx.input.issueNumber) return false;
2617
3098
  try {
2618
3099
  if (stageName === "taskify") {
2619
- const taskJsonPath = path13.join(ctx.taskDir, "task.json");
2620
- if (!fs13.existsSync(taskJsonPath)) return false;
2621
- const raw = fs13.readFileSync(taskJsonPath, "utf-8");
3100
+ const taskJsonPath = path19.join(ctx.taskDir, "task.json");
3101
+ if (!fs21.existsSync(taskJsonPath)) return false;
3102
+ const raw = fs21.readFileSync(taskJsonPath, "utf-8");
2622
3103
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2623
3104
  const taskJson = JSON.parse(cleaned);
2624
3105
  if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
@@ -2633,9 +3114,9 @@ Reply with \`@kody approve\` and your answers in the comment body.`;
2633
3114
  }
2634
3115
  }
2635
3116
  if (stageName === "plan") {
2636
- const planPath = path13.join(ctx.taskDir, "plan.md");
2637
- if (!fs13.existsSync(planPath)) return false;
2638
- const plan = fs13.readFileSync(planPath, "utf-8");
3117
+ const planPath = path19.join(ctx.taskDir, "plan.md");
3118
+ if (!fs21.existsSync(planPath)) return false;
3119
+ const plan = fs21.readFileSync(planPath, "utf-8");
2639
3120
  const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
2640
3121
  if (questionsMatch) {
2641
3122
  const questionsText = questionsMatch[1].trim();
@@ -2664,12 +3145,13 @@ var init_questions = __esm({
2664
3145
  });
2665
3146
 
2666
3147
  // src/pipeline/hooks.ts
2667
- import * as fs14 from "fs";
2668
- import * as path14 from "path";
3148
+ import * as fs22 from "fs";
3149
+ import * as path20 from "path";
2669
3150
  function applyPreStageLabel(ctx, def) {
2670
3151
  if (!ctx.input.issueNumber || ctx.input.local) return;
2671
3152
  if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
2672
3153
  if (def.name === "review") setLifecycleLabel(ctx.input.issueNumber, "review");
3154
+ if (def.name === "ship") setLifecycleLabel(ctx.input.issueNumber, "shipping");
2673
3155
  }
2674
3156
  function checkQuestionsAfterStage(ctx, def, state) {
2675
3157
  if (def.name !== "taskify" && def.name !== "plan") return null;
@@ -2702,9 +3184,9 @@ function autoDetectComplexity(ctx, def) {
2702
3184
  return { complexity, activeStages };
2703
3185
  }
2704
3186
  try {
2705
- const taskJsonPath = path14.join(ctx.taskDir, "task.json");
2706
- if (!fs14.existsSync(taskJsonPath)) return null;
2707
- const raw = fs14.readFileSync(taskJsonPath, "utf-8");
3187
+ const taskJsonPath = path20.join(ctx.taskDir, "task.json");
3188
+ if (!fs22.existsSync(taskJsonPath)) return null;
3189
+ const raw = fs22.readFileSync(taskJsonPath, "utf-8");
2708
3190
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2709
3191
  const taskJson = JSON.parse(cleaned);
2710
3192
  if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
@@ -2734,8 +3216,8 @@ function checkRiskGate(ctx, def, state, complexity) {
2734
3216
  if (ctx.input.dryRun || ctx.input.local) return null;
2735
3217
  if (ctx.input.mode === "rerun") return null;
2736
3218
  if (!ctx.input.issueNumber) return null;
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)";
3219
+ const planPath = path20.join(ctx.taskDir, "plan.md");
3220
+ const plan = fs22.existsSync(planPath) ? fs22.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
2739
3221
  try {
2740
3222
  postComment(
2741
3223
  ctx.input.issueNumber,
@@ -2802,22 +3284,22 @@ var init_hooks = __esm({
2802
3284
  });
2803
3285
 
2804
3286
  // src/learning/auto-learn.ts
2805
- import * as fs15 from "fs";
2806
- import * as path15 from "path";
3287
+ import * as fs23 from "fs";
3288
+ import * as path21 from "path";
2807
3289
  function stripAnsi(str) {
2808
3290
  return str.replace(/\x1b\[[0-9;]*m/g, "");
2809
3291
  }
2810
3292
  function autoLearn(ctx) {
2811
3293
  try {
2812
- const memoryDir = path15.join(ctx.projectDir, ".kody", "memory");
2813
- if (!fs15.existsSync(memoryDir)) {
2814
- fs15.mkdirSync(memoryDir, { recursive: true });
3294
+ const memoryDir = path21.join(ctx.projectDir, ".kody", "memory");
3295
+ if (!fs23.existsSync(memoryDir)) {
3296
+ fs23.mkdirSync(memoryDir, { recursive: true });
2815
3297
  }
2816
3298
  const learnings = [];
2817
3299
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2818
- const verifyPath = path15.join(ctx.taskDir, "verify.md");
2819
- if (fs15.existsSync(verifyPath)) {
2820
- const verify = stripAnsi(fs15.readFileSync(verifyPath, "utf-8"));
3300
+ const verifyPath = path21.join(ctx.taskDir, "verify.md");
3301
+ if (fs23.existsSync(verifyPath)) {
3302
+ const verify = stripAnsi(fs23.readFileSync(verifyPath, "utf-8"));
2821
3303
  if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
2822
3304
  if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
2823
3305
  if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
@@ -2826,18 +3308,18 @@ function autoLearn(ctx) {
2826
3308
  if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
2827
3309
  if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
2828
3310
  }
2829
- const reviewPath = path15.join(ctx.taskDir, "review.md");
2830
- if (fs15.existsSync(reviewPath)) {
2831
- const review = fs15.readFileSync(reviewPath, "utf-8");
3311
+ const reviewPath = path21.join(ctx.taskDir, "review.md");
3312
+ if (fs23.existsSync(reviewPath)) {
3313
+ const review = fs23.readFileSync(reviewPath, "utf-8");
2832
3314
  if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
2833
3315
  if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
2834
3316
  if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
2835
3317
  if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
2836
3318
  }
2837
- const taskJsonPath = path15.join(ctx.taskDir, "task.json");
2838
- if (fs15.existsSync(taskJsonPath)) {
3319
+ const taskJsonPath = path21.join(ctx.taskDir, "task.json");
3320
+ if (fs23.existsSync(taskJsonPath)) {
2839
3321
  try {
2840
- const raw = stripAnsi(fs15.readFileSync(taskJsonPath, "utf-8"));
3322
+ const raw = stripAnsi(fs23.readFileSync(taskJsonPath, "utf-8"));
2841
3323
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2842
3324
  const task = JSON.parse(cleaned);
2843
3325
  if (task.scope && Array.isArray(task.scope)) {
@@ -2848,135 +3330,48 @@ function autoLearn(ctx) {
2848
3330
  }
2849
3331
  }
2850
3332
  if (learnings.length > 0) {
2851
- const conventionsPath = path15.join(memoryDir, "conventions.md");
3333
+ const conventionsPath = path21.join(memoryDir, "conventions.md");
2852
3334
  const entry = `
2853
3335
  ## Learned ${timestamp2} (task: ${ctx.taskId})
2854
3336
  ${learnings.join("\n")}
2855
3337
  `;
2856
- fs15.appendFileSync(conventionsPath, entry);
2857
- invalidateCache(conventionsPath, path15.join(memoryDir, ".tiers"));
3338
+ fs23.appendFileSync(conventionsPath, entry);
2858
3339
  logger.info(`Auto-learned ${learnings.length} convention(s)`);
2859
3340
  }
2860
- autoLearnDecisions(ctx.taskDir, memoryDir, ctx.taskId, timestamp2);
2861
3341
  autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
2862
3342
  } catch {
2863
3343
  }
2864
3344
  }
2865
3345
  function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
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
- }
3346
+ const archPath = path21.join(memoryDir, "architecture.md");
3347
+ if (fs23.existsSync(archPath)) return;
3348
+ const detected = detectArchitectureBasic(projectDir);
2914
3349
  if (detected.length > 0) {
2915
3350
  const content = `# Architecture (auto-detected ${timestamp2})
2916
3351
 
2917
3352
  ## Overview
2918
3353
  ${detected.join("\n")}
2919
3354
  `;
2920
- fs15.writeFileSync(archPath, content);
2921
- invalidateCache(archPath, path15.join(memoryDir, ".tiers"));
3355
+ fs23.writeFileSync(archPath, content);
2922
3356
  logger.info(`Auto-detected architecture (${detected.length} items)`);
2923
3357
  }
2924
3358
  }
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
- }
2964
3359
  var init_auto_learn = __esm({
2965
3360
  "src/learning/auto-learn.ts"() {
2966
3361
  "use strict";
2967
3362
  init_logger();
2968
- init_context_tiers();
3363
+ init_architecture_detection();
2969
3364
  }
2970
3365
  });
2971
3366
 
2972
3367
  // src/retrospective.ts
2973
- import * as fs16 from "fs";
2974
- import * as path16 from "path";
3368
+ import * as fs24 from "fs";
3369
+ import * as path22 from "path";
2975
3370
  function readArtifact(taskDir, filename, maxChars) {
2976
- const p = path16.join(taskDir, filename);
2977
- if (!fs16.existsSync(p)) return null;
3371
+ const p = path22.join(taskDir, filename);
3372
+ if (!fs24.existsSync(p)) return null;
2978
3373
  try {
2979
- const content = fs16.readFileSync(p, "utf-8");
3374
+ const content = fs24.readFileSync(p, "utf-8");
2980
3375
  return content.length > maxChars ? content.slice(0, maxChars) + "\n...(truncated)" : content;
2981
3376
  } catch {
2982
3377
  return null;
@@ -3029,13 +3424,13 @@ function collectRunContext(ctx, state, pipelineStartTime) {
3029
3424
  return lines.join("\n");
3030
3425
  }
3031
3426
  function getLogPath(projectDir) {
3032
- return path16.join(projectDir, ".kody", "memory", "observer-log.jsonl");
3427
+ return path22.join(projectDir, ".kody", "memory", "observer-log.jsonl");
3033
3428
  }
3034
3429
  function readPreviousRetrospectives(projectDir, limit = 10) {
3035
3430
  const logPath = getLogPath(projectDir);
3036
- if (!fs16.existsSync(logPath)) return [];
3431
+ if (!fs24.existsSync(logPath)) return [];
3037
3432
  try {
3038
- const content = fs16.readFileSync(logPath, "utf-8");
3433
+ const content = fs24.readFileSync(logPath, "utf-8");
3039
3434
  const lines = content.split("\n").filter(Boolean);
3040
3435
  const entries = [];
3041
3436
  const start = Math.max(0, lines.length - limit);
@@ -3062,11 +3457,11 @@ function formatPreviousEntries(entries) {
3062
3457
  }
3063
3458
  function appendRetrospectiveEntry(projectDir, entry) {
3064
3459
  const logPath = getLogPath(projectDir);
3065
- const dir = path16.dirname(logPath);
3066
- if (!fs16.existsSync(dir)) {
3067
- fs16.mkdirSync(dir, { recursive: true });
3460
+ const dir = path22.dirname(logPath);
3461
+ if (!fs24.existsSync(dir)) {
3462
+ fs24.mkdirSync(dir, { recursive: true });
3068
3463
  }
3069
- fs16.appendFileSync(logPath, JSON.stringify(entry) + "\n");
3464
+ fs24.appendFileSync(logPath, JSON.stringify(entry) + "\n");
3070
3465
  }
3071
3466
  async function runRetrospective(ctx, state, pipelineStartTime) {
3072
3467
  if (ctx.input.dryRun) return;
@@ -3176,9 +3571,66 @@ If no pipeline flaw is detected, set "pipelineFlaw" to null.
3176
3571
  }
3177
3572
  });
3178
3573
 
3574
+ // src/pipeline/summary.ts
3575
+ function formatDuration(ms) {
3576
+ const totalSec = Math.round(ms / 1e3);
3577
+ if (totalSec < 60) return `${totalSec}s`;
3578
+ const min = Math.floor(totalSec / 60);
3579
+ const sec = totalSec % 60;
3580
+ return `${min}m ${sec}s`;
3581
+ }
3582
+ function stageDuration(stage) {
3583
+ if (!stage.startedAt) return "-";
3584
+ const start = new Date(stage.startedAt).getTime();
3585
+ const end = stage.completedAt ? new Date(stage.completedAt).getTime() : Date.now();
3586
+ if (isNaN(start) || isNaN(end)) return "-";
3587
+ return formatDuration(end - start);
3588
+ }
3589
+ function formatPipelineSummary(state, options) {
3590
+ const lines = [];
3591
+ lines.push(`## Pipeline Summary: \`${state.taskId}\``);
3592
+ lines.push("");
3593
+ lines.push("| Stage | Status | Duration | Retries |");
3594
+ lines.push("|-------|--------|----------|---------|");
3595
+ for (const def of STAGES) {
3596
+ const s = state.stages[def.name];
3597
+ if (!s) continue;
3598
+ const status = STATUS_ICONS[s.state] ?? s.state;
3599
+ const duration = stageDuration(s);
3600
+ const retries = s.retries ?? 0;
3601
+ lines.push(`| ${def.name} | ${status} | ${duration} | ${retries} |`);
3602
+ }
3603
+ const totalMs = new Date(state.updatedAt).getTime() - new Date(state.createdAt).getTime();
3604
+ const totalStr = isNaN(totalMs) || totalMs < 0 ? "-" : formatDuration(totalMs);
3605
+ lines.push("");
3606
+ const footerParts = [`**Total:** ${totalStr}`];
3607
+ if (options?.complexity) {
3608
+ footerParts.push(`**Complexity:** ${options.complexity}`);
3609
+ }
3610
+ if (options?.model) {
3611
+ footerParts.push(`**Model:** ${options.model}`);
3612
+ }
3613
+ lines.push(footerParts.join(" | "));
3614
+ return lines.join("\n");
3615
+ }
3616
+ var STATUS_ICONS;
3617
+ var init_summary = __esm({
3618
+ "src/pipeline/summary.ts"() {
3619
+ "use strict";
3620
+ init_definitions();
3621
+ STATUS_ICONS = {
3622
+ completed: "completed",
3623
+ failed: "failed",
3624
+ timeout: "timeout",
3625
+ running: "running",
3626
+ pending: "pending"
3627
+ };
3628
+ }
3629
+ });
3630
+
3179
3631
  // src/pipeline.ts
3180
- import * as fs17 from "fs";
3181
- import * as path17 from "path";
3632
+ import * as fs25 from "fs";
3633
+ import * as path23 from "path";
3182
3634
  function ensureFeatureBranchIfNeeded(ctx) {
3183
3635
  if (ctx.input.dryRun) return;
3184
3636
  if (ctx.input.prNumber) {
@@ -3191,34 +3643,59 @@ function ensureFeatureBranchIfNeeded(ctx) {
3191
3643
  }
3192
3644
  if (!ctx.input.issueNumber) return;
3193
3645
  try {
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;
3646
+ const taskMdPath = path23.join(ctx.taskDir, "task.md");
3647
+ const title = fs25.existsSync(taskMdPath) ? fs25.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
3196
3648
  ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
3197
3649
  syncWithDefault(ctx.projectDir);
3198
3650
  } catch (err) {
3199
- logger.warn(` Failed to create/sync feature branch: ${err}`);
3651
+ const msg = err instanceof Error ? err.message : String(err);
3652
+ if (msg.includes("not a git repository")) {
3653
+ logger.warn(` Not a git repository \u2014 skipping feature branch setup`);
3654
+ } else {
3655
+ logger.error(` Failed to create/sync feature branch: ${msg}`);
3656
+ throw new Error(`Feature branch setup failed: ${msg}`);
3657
+ }
3200
3658
  }
3201
3659
  }
3202
3660
  function acquireLock(taskDir) {
3203
- const lockPath = path17.join(taskDir, ".lock");
3204
- if (fs17.existsSync(lockPath)) {
3661
+ const lockPath = path23.join(taskDir, ".lock");
3662
+ if (fs25.existsSync(lockPath)) {
3205
3663
  try {
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;
3664
+ const pid = parseInt(fs25.readFileSync(lockPath, "utf-8").trim(), 10);
3665
+ if (!isNaN(pid)) {
3666
+ try {
3667
+ process.kill(pid, 0);
3668
+ throw new Error(`Pipeline already running (PID ${pid})`);
3669
+ } catch (e) {
3670
+ if (e.code !== "ESRCH") throw e;
3671
+ logger.info(` Removing stale lock (PID ${pid} no longer running)`);
3672
+ }
3673
+ } else {
3674
+ logger.warn(` Corrupt lock file (non-numeric PID) \u2014 overwriting`);
3212
3675
  }
3213
3676
  } catch (e) {
3214
3677
  if (e instanceof Error && e.message.startsWith("Pipeline already")) throw e;
3678
+ logger.warn(` Corrupt lock file \u2014 overwriting`);
3679
+ }
3680
+ try {
3681
+ fs25.unlinkSync(lockPath);
3682
+ } catch {
3683
+ }
3684
+ }
3685
+ try {
3686
+ const fd = fs25.openSync(lockPath, fs25.constants.O_WRONLY | fs25.constants.O_CREAT | fs25.constants.O_EXCL);
3687
+ fs25.writeSync(fd, String(process.pid));
3688
+ fs25.closeSync(fd);
3689
+ } catch (err) {
3690
+ if (err.code === "EEXIST") {
3691
+ throw new Error("Pipeline already running (lock acquired by another process)");
3215
3692
  }
3693
+ throw err;
3216
3694
  }
3217
- fs17.writeFileSync(lockPath, String(process.pid));
3218
3695
  }
3219
3696
  function releaseLock(taskDir) {
3220
3697
  try {
3221
- fs17.unlinkSync(path17.join(taskDir, ".lock"));
3698
+ fs25.unlinkSync(path23.join(taskDir, ".lock"));
3222
3699
  } catch {
3223
3700
  }
3224
3701
  }
@@ -3230,17 +3707,17 @@ async function runPipeline(ctx) {
3230
3707
  try {
3231
3708
  const state = loadState(ctx.taskId, ctx.taskDir);
3232
3709
  if (state && state.state === "running") {
3233
- state.state = "failed";
3710
+ const updatedStages = { ...state.stages };
3234
3711
  for (const stage of STAGES) {
3235
- if (state.stages[stage.name]?.state === "running") {
3236
- state.stages[stage.name] = {
3237
- ...state.stages[stage.name],
3712
+ if (updatedStages[stage.name]?.state === "running") {
3713
+ updatedStages[stage.name] = {
3714
+ ...updatedStages[stage.name],
3238
3715
  state: "failed",
3239
3716
  error: "Pipeline crashed unexpectedly"
3240
3717
  };
3241
3718
  }
3242
3719
  }
3243
- writeState(state, ctx.taskDir);
3720
+ writeState({ ...state, state: "failed", stages: updatedStages }, ctx.taskDir);
3244
3721
  }
3245
3722
  } catch {
3246
3723
  }
@@ -3365,8 +3842,27 @@ async function runPipelineInner(ctx) {
3365
3842
  }
3366
3843
  autoLearn(ctx);
3367
3844
  }
3368
- await runRetrospective(ctx, state, pipelineStartTime).catch(() => {
3845
+ await runRetrospective(ctx, state, pipelineStartTime).catch((err) => {
3846
+ logger.warn(` Retrospective failed: ${err instanceof Error ? err.message : String(err)}`);
3369
3847
  });
3848
+ if (ctx.input.issueNumber && !ctx.input.dryRun) {
3849
+ const config = getProjectConfig();
3850
+ const isCI3 = !!process.env.GITHUB_ACTIONS;
3851
+ const shouldPost = config.github.postSummary ?? (isCI3 ? true : false);
3852
+ if (shouldPost) {
3853
+ try {
3854
+ const summaryOpts = {};
3855
+ if (complexity) summaryOpts.complexity = complexity;
3856
+ const modelMap = config.agent?.modelMap;
3857
+ if (modelMap?.mid) summaryOpts.model = modelMap.mid;
3858
+ const summary = formatPipelineSummary(state, summaryOpts);
3859
+ postComment(ctx.input.issueNumber, summary);
3860
+ logger.info("Pipeline summary posted on issue");
3861
+ } catch (err) {
3862
+ logger.warn(` Failed to post pipeline summary: ${err instanceof Error ? err.message : String(err)}`);
3863
+ }
3864
+ }
3865
+ }
3370
3866
  return state;
3371
3867
  }
3372
3868
  function printStatus(taskId, taskDir) {
@@ -3401,12 +3897,14 @@ var init_pipeline = __esm({
3401
3897
  init_hooks();
3402
3898
  init_auto_learn();
3403
3899
  init_retrospective();
3900
+ init_summary();
3901
+ init_config();
3404
3902
  }
3405
3903
  });
3406
3904
 
3407
3905
  // src/preflight.ts
3408
- import { execFileSync as execFileSync9 } from "child_process";
3409
- import * as fs18 from "fs";
3906
+ import { execFileSync as execFileSync14 } from "child_process";
3907
+ import * as fs26 from "fs";
3410
3908
  function check(name, fn) {
3411
3909
  try {
3412
3910
  const detail = fn() ?? void 0;
@@ -3418,7 +3916,7 @@ function check(name, fn) {
3418
3916
  function runPreflight() {
3419
3917
  const checks = [
3420
3918
  check("claude CLI", () => {
3421
- const v = execFileSync9("claude", ["--version"], {
3919
+ const v = execFileSync14("claude", ["--version"], {
3422
3920
  encoding: "utf-8",
3423
3921
  timeout: 1e4,
3424
3922
  stdio: ["pipe", "pipe", "pipe"]
@@ -3426,14 +3924,14 @@ function runPreflight() {
3426
3924
  return v;
3427
3925
  }),
3428
3926
  check("git repo", () => {
3429
- execFileSync9("git", ["rev-parse", "--is-inside-work-tree"], {
3927
+ execFileSync14("git", ["rev-parse", "--is-inside-work-tree"], {
3430
3928
  encoding: "utf-8",
3431
3929
  timeout: 5e3,
3432
3930
  stdio: ["pipe", "pipe", "pipe"]
3433
3931
  });
3434
3932
  }),
3435
3933
  check("pnpm", () => {
3436
- const v = execFileSync9("pnpm", ["--version"], {
3934
+ const v = execFileSync14("pnpm", ["--version"], {
3437
3935
  encoding: "utf-8",
3438
3936
  timeout: 5e3,
3439
3937
  stdio: ["pipe", "pipe", "pipe"]
@@ -3441,7 +3939,7 @@ function runPreflight() {
3441
3939
  return v;
3442
3940
  }),
3443
3941
  check("node >= 18", () => {
3444
- const v = execFileSync9("node", ["--version"], {
3942
+ const v = execFileSync14("node", ["--version"], {
3445
3943
  encoding: "utf-8",
3446
3944
  timeout: 5e3,
3447
3945
  stdio: ["pipe", "pipe", "pipe"]
@@ -3451,7 +3949,7 @@ function runPreflight() {
3451
3949
  return v;
3452
3950
  }),
3453
3951
  check("gh CLI", () => {
3454
- const v = execFileSync9("gh", ["--version"], {
3952
+ const v = execFileSync14("gh", ["--version"], {
3455
3953
  encoding: "utf-8",
3456
3954
  timeout: 5e3,
3457
3955
  stdio: ["pipe", "pipe", "pipe"]
@@ -3459,7 +3957,7 @@ function runPreflight() {
3459
3957
  return v;
3460
3958
  }),
3461
3959
  check("package.json", () => {
3462
- if (!fs18.existsSync("package.json")) throw new Error("not found");
3960
+ if (!fs26.existsSync("package.json")) throw new Error("not found");
3463
3961
  })
3464
3962
  ];
3465
3963
  const failed = checks.filter((c) => !c.ok);
@@ -3536,10 +4034,10 @@ var init_args = __esm({
3536
4034
  });
3537
4035
 
3538
4036
  // src/cli/litellm.ts
3539
- import * as fs19 from "fs";
4037
+ import * as fs27 from "fs";
3540
4038
  import * as os from "os";
3541
- import * as path18 from "path";
3542
- import { execFileSync as execFileSync10 } from "child_process";
4039
+ import * as path24 from "path";
4040
+ import { execFileSync as execFileSync15 } from "child_process";
3543
4041
  async function checkLitellmHealth(url) {
3544
4042
  try {
3545
4043
  const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
@@ -3594,22 +4092,49 @@ function generateLitellmConfig(provider, modelMap) {
3594
4092
  }
3595
4093
  return entries.join("\n") + "\n";
3596
4094
  }
4095
+ function generateLitellmConfigFromStages(defaultConfig, stages) {
4096
+ const proxyModels = [];
4097
+ if (defaultConfig && defaultConfig.provider !== "claude" && defaultConfig.provider !== "anthropic") {
4098
+ proxyModels.push(defaultConfig);
4099
+ }
4100
+ if (stages) {
4101
+ for (const sc of Object.values(stages)) {
4102
+ if (sc.provider !== "claude" && sc.provider !== "anthropic") {
4103
+ proxyModels.push(sc);
4104
+ }
4105
+ }
4106
+ }
4107
+ if (proxyModels.length === 0) return void 0;
4108
+ const entries = ["model_list:"];
4109
+ const seen = /* @__PURE__ */ new Set();
4110
+ for (const { provider, model } of proxyModels) {
4111
+ const key = `${provider}/${model}`;
4112
+ if (seen.has(key)) continue;
4113
+ seen.add(key);
4114
+ const apiKeyVar = providerApiKeyEnvVar(provider);
4115
+ entries.push(` - model_name: ${model}`);
4116
+ entries.push(` litellm_params:`);
4117
+ entries.push(` model: ${provider}/${model}`);
4118
+ entries.push(` api_key: os.environ/${apiKeyVar}`);
4119
+ }
4120
+ return entries.join("\n") + "\n";
4121
+ }
3597
4122
  async function tryStartLitellm(url, projectDir, generatedConfig) {
3598
4123
  if (!generatedConfig) {
3599
4124
  logger.warn("No provider configured in kody.config.json \u2014 cannot start LiteLLM proxy");
3600
4125
  return null;
3601
4126
  }
3602
- const configPath = path18.join(os.tmpdir(), "kody-litellm-config.yaml");
3603
- fs19.writeFileSync(configPath, generatedConfig);
4127
+ const configPath = path24.join(os.tmpdir(), "kody-litellm-config.yaml");
4128
+ fs27.writeFileSync(configPath, generatedConfig);
3604
4129
  const portMatch = url.match(/:(\d+)/);
3605
4130
  const port = portMatch ? portMatch[1] : "4000";
3606
4131
  let litellmFound = false;
3607
4132
  try {
3608
- execFileSync10("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
4133
+ execFileSync15("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
3609
4134
  litellmFound = true;
3610
4135
  } catch {
3611
4136
  try {
3612
- execFileSync10("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
4137
+ execFileSync15("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
3613
4138
  litellmFound = true;
3614
4139
  } catch {
3615
4140
  }
@@ -3622,19 +4147,29 @@ async function tryStartLitellm(url, projectDir, generatedConfig) {
3622
4147
  let cmd;
3623
4148
  let args2;
3624
4149
  try {
3625
- execFileSync10("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
4150
+ execFileSync15("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
3626
4151
  cmd = "litellm";
3627
- args2 = ["--config", configPath, "--port", port];
4152
+ args2 = ["--config", configPath, "--port", port, "--no_db"];
3628
4153
  } catch {
3629
4154
  cmd = "python3";
3630
- args2 = ["-m", "litellm", "--config", configPath, "--port", port];
4155
+ args2 = ["-m", "litellm", "--config", configPath, "--port", port, "--no_db"];
3631
4156
  }
3632
- const dotenvPath = path18.join(projectDir, ".env");
4157
+ const dotenvPath = path24.join(projectDir, ".env");
3633
4158
  const dotenvVars = {};
3634
- if (fs19.existsSync(dotenvPath)) {
3635
- for (const line of fs19.readFileSync(dotenvPath, "utf-8").split("\n")) {
4159
+ if (fs27.existsSync(dotenvPath)) {
4160
+ for (const rawLine of fs27.readFileSync(dotenvPath, "utf-8").split("\n")) {
4161
+ const line = rawLine.trim();
4162
+ if (!line || line.startsWith("#")) continue;
3636
4163
  const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
3637
- if (match) dotenvVars[match[1]] = match[2];
4164
+ if (match) {
4165
+ let value = match[2].trim();
4166
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
4167
+ value = value.slice(1, -1);
4168
+ }
4169
+ const commentIdx = value.indexOf(" #");
4170
+ if (commentIdx !== -1) value = value.slice(0, commentIdx).trim();
4171
+ if (value) dotenvVars[match[1]] = value;
4172
+ }
3638
4173
  }
3639
4174
  if (Object.keys(dotenvVars).length > 0) {
3640
4175
  logger.info(` Loaded API keys: ${Object.keys(dotenvVars).join(", ")}`);
@@ -3673,8 +4208,8 @@ var init_litellm = __esm({
3673
4208
  });
3674
4209
 
3675
4210
  // src/cli/task-state.ts
3676
- import * as fs20 from "fs";
3677
- import * as path19 from "path";
4211
+ import * as fs28 from "fs";
4212
+ import * as path25 from "path";
3678
4213
  function resolveTaskAction(issueNumber, existingTaskId, existingState) {
3679
4214
  if (!existingTaskId || !existingState) {
3680
4215
  return { action: "start-fresh", taskId: `${issueNumber}-${generateTaskId()}` };
@@ -3706,11 +4241,11 @@ function resolveTaskAction(issueNumber, existingTaskId, existingState) {
3706
4241
  function resolveForIssue(issueNumber, projectDir) {
3707
4242
  const existingTaskId = findLatestTaskForIssue(issueNumber, projectDir);
3708
4243
  if (existingTaskId) {
3709
- const statusPath = path19.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
4244
+ const statusPath = path25.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
3710
4245
  let existingState = null;
3711
- if (fs20.existsSync(statusPath)) {
4246
+ if (fs28.existsSync(statusPath)) {
3712
4247
  try {
3713
- existingState = JSON.parse(fs20.readFileSync(statusPath, "utf-8"));
4248
+ existingState = JSON.parse(fs28.readFileSync(statusPath, "utf-8"));
3714
4249
  } catch {
3715
4250
  }
3716
4251
  }
@@ -3743,12 +4278,12 @@ var resolve_exports = {};
3743
4278
  __export(resolve_exports, {
3744
4279
  runResolve: () => runResolve
3745
4280
  });
3746
- import { execFileSync as execFileSync11 } from "child_process";
4281
+ import { execFileSync as execFileSync16 } from "child_process";
3747
4282
  function getConflictContext(cwd, files) {
3748
4283
  const parts = [];
3749
4284
  for (const file of files.slice(0, 10)) {
3750
4285
  try {
3751
- const content = execFileSync11("git", ["diff", file], {
4286
+ const content = execFileSync16("git", ["diff", file], {
3752
4287
  cwd,
3753
4288
  encoding: "utf-8",
3754
4289
  stdio: ["pipe", "pipe", "pipe"]
@@ -3867,10 +4402,10 @@ var init_resolve = __esm({
3867
4402
 
3868
4403
  // src/entry.ts
3869
4404
  var entry_exports = {};
3870
- import * as fs21 from "fs";
3871
- import * as path20 from "path";
4405
+ import * as fs29 from "fs";
4406
+ import * as path26 from "path";
3872
4407
  async function ensureLitellmProxy(config, projectDir) {
3873
- if (!needsLitellmProxy(config)) return null;
4408
+ if (!anyStageNeedsProxy(config)) return null;
3874
4409
  const litellmUrl = getLitellmUrl();
3875
4410
  const proxyRunning = await checkLitellmHealth(litellmUrl);
3876
4411
  let litellmProcess = null;
@@ -3883,7 +4418,9 @@ async function ensureLitellmProxy(config, projectDir) {
3883
4418
  }
3884
4419
  }
3885
4420
  let generatedConfig;
3886
- if (config.agent.provider && config.agent.provider !== "anthropic") {
4421
+ if (config.agent.stages || config.agent.default) {
4422
+ generatedConfig = generateLitellmConfigFromStages(config.agent.default, config.agent.stages);
4423
+ } else if (config.agent.provider && config.agent.provider !== "anthropic") {
3887
4424
  generatedConfig = generateLitellmConfig(config.agent.provider, config.agent.modelMap);
3888
4425
  }
3889
4426
  litellmProcess = await tryStartLitellm(litellmUrl, projectDir, generatedConfig);
@@ -3894,10 +4431,9 @@ async function ensureLitellmProxy(config, projectDir) {
3894
4431
  } else {
3895
4432
  logger.info(`LiteLLM proxy already running at ${litellmUrl}`);
3896
4433
  }
3897
- process.env.ANTHROPIC_BASE_URL = litellmUrl;
3898
- logger.info(`ANTHROPIC_BASE_URL set to ${litellmUrl}`);
3899
- if (!process.env.ANTHROPIC_API_KEY || !process.env.ANTHROPIC_API_KEY.startsWith("sk-ant-")) {
3900
- process.env.ANTHROPIC_API_KEY = "sk-ant-api03-litellm-proxy-key-00000000000000000000000000000000000000000000000000000000000000000000";
4434
+ logger.info(`LiteLLM proxy available at ${litellmUrl}`);
4435
+ if (!process.env.ANTHROPIC_API_KEY) {
4436
+ process.env.ANTHROPIC_API_KEY = `sk-ant-api03-${"0".repeat(64)}`;
3901
4437
  }
3902
4438
  return litellmProcess;
3903
4439
  }
@@ -3922,9 +4458,9 @@ async function runModelHealthCheck(config) {
3922
4458
  }
3923
4459
  async function main() {
3924
4460
  const input = parseArgs();
3925
- const projectDir = input.cwd ? path20.resolve(input.cwd) : process.cwd();
4461
+ const projectDir = input.cwd ? path26.resolve(input.cwd) : process.cwd();
3926
4462
  if (input.cwd) {
3927
- if (!fs21.existsSync(projectDir)) {
4463
+ if (!fs29.existsSync(projectDir)) {
3928
4464
  console.error(`--cwd path does not exist: ${projectDir}`);
3929
4465
  process.exit(1);
3930
4466
  }
@@ -3958,15 +4494,26 @@ async function main() {
3958
4494
  process.exit(0);
3959
4495
  }
3960
4496
  if (taskAction.action === "resume") {
3961
- input.taskId = taskAction.taskId;
3962
- input.fromStage = taskAction.fromStage;
3963
- input.command = "rerun";
4497
+ Object.assign(input, {
4498
+ taskId: taskAction.taskId,
4499
+ fromStage: taskAction.fromStage,
4500
+ command: "rerun"
4501
+ });
3964
4502
  logger.info(`Resuming task ${taskAction.taskId} from ${taskAction.fromStage}`);
3965
4503
  }
3966
4504
  }
3967
4505
  let taskId = input.taskId;
3968
4506
  if (!taskId) {
3969
- if (isPRFix) {
4507
+ if ((input.command === "rerun" || input.command === "status") && input.issueNumber) {
4508
+ const resolved = resolveTaskIdForCommand(input.issueNumber, projectDir);
4509
+ if (resolved) {
4510
+ taskId = resolved;
4511
+ logger.info(`Auto-resolved task-id: ${taskId} (from issue #${input.issueNumber})`);
4512
+ } else {
4513
+ console.error(`No task found for issue #${input.issueNumber}. Provide --task-id explicitly.`);
4514
+ process.exit(1);
4515
+ }
4516
+ } else if (isPRFix) {
3970
4517
  taskId = `${input.command === "fix-ci" ? "fixci" : "fix"}-pr-${input.prNumber}-${generateTaskId()}`;
3971
4518
  } else if (input.issueNumber) {
3972
4519
  taskId = `${input.issueNumber}-${generateTaskId()}`;
@@ -3979,8 +4526,8 @@ async function main() {
3979
4526
  process.exit(1);
3980
4527
  }
3981
4528
  }
3982
- const taskDir = path20.join(projectDir, ".kody", "tasks", taskId);
3983
- fs21.mkdirSync(taskDir, { recursive: true });
4529
+ const taskDir = path26.join(projectDir, ".kody", "tasks", taskId);
4530
+ fs29.mkdirSync(taskDir, { recursive: true });
3984
4531
  if (input.command === "status") {
3985
4532
  printStatus(taskId, taskDir);
3986
4533
  return;
@@ -4085,7 +4632,7 @@ async function main() {
4085
4632
  runners: runners2,
4086
4633
  local: input.local ?? true
4087
4634
  });
4088
- if (litellmProcess2) litellmProcess2.kill?.();
4635
+ if (litellmProcess2) litellmProcess2.kill();
4089
4636
  if (result.outcome === "failed") {
4090
4637
  console.error(`Resolve failed: ${result.error}`);
4091
4638
  process.exit(1);
@@ -4096,31 +4643,31 @@ async function main() {
4096
4643
  logger.info("Preflight checks:");
4097
4644
  runPreflight();
4098
4645
  if (input.task) {
4099
- fs21.writeFileSync(path20.join(taskDir, "task.md"), input.task);
4646
+ fs29.writeFileSync(path26.join(taskDir, "task.md"), input.task);
4100
4647
  }
4101
- const taskMdPath = path20.join(taskDir, "task.md");
4102
- if (!fs21.existsSync(taskMdPath) && isPRFix && input.prNumber) {
4648
+ const taskMdPath = path26.join(taskDir, "task.md");
4649
+ if (!fs29.existsSync(taskMdPath) && isPRFix && input.prNumber) {
4103
4650
  logger.info(`Fetching PR #${input.prNumber} details as task context...`);
4104
4651
  const prDetails = getPRDetails(input.prNumber);
4105
4652
  if (prDetails) {
4106
4653
  const taskContent = `# ${prDetails.title}
4107
4654
 
4108
4655
  ${prDetails.body ?? ""}`;
4109
- fs21.writeFileSync(taskMdPath, taskContent);
4656
+ fs29.writeFileSync(taskMdPath, taskContent);
4110
4657
  logger.info(` Task loaded from PR #${input.prNumber}: ${prDetails.title}`);
4111
4658
  }
4112
- } else if (!fs21.existsSync(taskMdPath) && input.issueNumber) {
4659
+ } else if (!fs29.existsSync(taskMdPath) && input.issueNumber) {
4113
4660
  logger.info(`Fetching issue #${input.issueNumber} body as task...`);
4114
4661
  const issue = getIssue(input.issueNumber);
4115
4662
  if (issue) {
4116
4663
  const taskContent = `# ${issue.title}
4117
4664
 
4118
4665
  ${issue.body ?? ""}`;
4119
- fs21.writeFileSync(taskMdPath, taskContent);
4666
+ fs29.writeFileSync(taskMdPath, taskContent);
4120
4667
  logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
4121
4668
  }
4122
4669
  }
4123
- if (!fs21.existsSync(taskMdPath)) {
4670
+ if (!fs29.existsSync(taskMdPath)) {
4124
4671
  console.error("No task.md found. Provide --task, --issue-number, or ensure .kody/tasks/<id>/task.md exists.");
4125
4672
  process.exit(1);
4126
4673
  }
@@ -4192,7 +4739,7 @@ ${input.feedback}`);
4192
4739
  await runModelHealthCheck(config);
4193
4740
  const cleanupLitellm = () => {
4194
4741
  if (litellmProcess) {
4195
- litellmProcess.kill?.();
4742
+ litellmProcess.kill();
4196
4743
  litellmProcess = null;
4197
4744
  }
4198
4745
  };
@@ -4258,7 +4805,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
4258
4805
  }
4259
4806
  }
4260
4807
  const state = await runPipeline(ctx);
4261
- const files = fs21.readdirSync(taskDir);
4808
+ const files = fs29.readdirSync(taskDir);
4262
4809
  console.log(`
4263
4810
  Artifacts in ${taskDir}:`);
4264
4811
  for (const f of files) {
@@ -4322,20 +4869,21 @@ var init_entry = __esm({
4322
4869
  });
4323
4870
 
4324
4871
  // src/bin/cli.ts
4325
- import * as fs22 from "fs";
4326
- import * as path21 from "path";
4327
- import { execFileSync as execFileSync12 } from "child_process";
4872
+ import * as fs30 from "fs";
4873
+ import * as path27 from "path";
4328
4874
  import { fileURLToPath } from "url";
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) {
4875
+
4876
+ // src/bin/commands/init.ts
4877
+ import * as fs3 from "fs";
4878
+ import * as path2 from "path";
4879
+ import { execFileSync as execFileSync3 } from "child_process";
4880
+
4881
+ // src/bin/health-checks.ts
4882
+ import * as fs from "fs";
4883
+ import { execFileSync } from "child_process";
4884
+ function checkCommand(name, args2, fix) {
4337
4885
  try {
4338
- const output = execFileSync12(name, args2, {
4886
+ const output = execFileSync(name, args2, {
4339
4887
  encoding: "utf-8",
4340
4888
  timeout: 1e4,
4341
4889
  stdio: ["pipe", "pipe", "pipe"]
@@ -4346,14 +4894,14 @@ function checkCommand2(name, args2, fix) {
4346
4894
  }
4347
4895
  }
4348
4896
  function checkFile(filePath, description, fix) {
4349
- if (fs22.existsSync(filePath)) {
4897
+ if (fs.existsSync(filePath)) {
4350
4898
  return { name: description, ok: true, detail: filePath };
4351
4899
  }
4352
4900
  return { name: description, ok: false, fix };
4353
4901
  }
4354
4902
  function checkGhAuth(cwd) {
4355
4903
  try {
4356
- const output = execFileSync12("gh", ["auth", "status"], {
4904
+ const output = execFileSync("gh", ["auth", "status"], {
4357
4905
  encoding: "utf-8",
4358
4906
  timeout: 1e4,
4359
4907
  cwd,
@@ -4371,7 +4919,7 @@ function checkGhAuth(cwd) {
4371
4919
  }
4372
4920
  function checkGhRepoAccess(cwd) {
4373
4921
  try {
4374
- const remote = execFileSync12("git", ["remote", "get-url", "origin"], {
4922
+ const remote = execFileSync("git", ["remote", "get-url", "origin"], {
4375
4923
  encoding: "utf-8",
4376
4924
  timeout: 5e3,
4377
4925
  cwd,
@@ -4382,7 +4930,7 @@ function checkGhRepoAccess(cwd) {
4382
4930
  return { name: "GitHub repo", ok: false, fix: "Set git remote origin to a GitHub URL" };
4383
4931
  }
4384
4932
  const repoSlug = `${match[1]}/${match[2]}`;
4385
- execFileSync12("gh", ["repo", "view", repoSlug, "--json", "name"], {
4933
+ execFileSync("gh", ["repo", "view", repoSlug, "--json", "name"], {
4386
4934
  encoding: "utf-8",
4387
4935
  timeout: 1e4,
4388
4936
  cwd,
@@ -4395,7 +4943,7 @@ function checkGhRepoAccess(cwd) {
4395
4943
  }
4396
4944
  function checkGhSecret(repoSlug, secretName) {
4397
4945
  try {
4398
- const output = execFileSync12("gh", ["secret", "list", "--repo", repoSlug], {
4946
+ const output = execFileSync("gh", ["secret", "list", "--repo", repoSlug], {
4399
4947
  encoding: "utf-8",
4400
4948
  timeout: 1e4,
4401
4949
  stdio: ["pipe", "pipe", "pipe"]
@@ -4416,14 +4964,20 @@ function checkGhSecret(repoSlug, secretName) {
4416
4964
  };
4417
4965
  }
4418
4966
  }
4967
+
4968
+ // src/bin/config-detection.ts
4969
+ import * as fs2 from "fs";
4970
+ import * as path from "path";
4971
+ import { execFileSync as execFileSync2 } from "child_process";
4972
+ var FRONTEND_DEPS = ["next", "react", "vue", "svelte", "nuxt", "astro", "solid-js", "angular", "@angular/core"];
4419
4973
  function detectBasicConfig(cwd) {
4420
4974
  let pm = "pnpm";
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";
4975
+ if (fs2.existsSync(path.join(cwd, "yarn.lock"))) pm = "yarn";
4976
+ else if (fs2.existsSync(path.join(cwd, "bun.lockb"))) pm = "bun";
4977
+ else if (!fs2.existsSync(path.join(cwd, "pnpm-lock.yaml")) && fs2.existsSync(path.join(cwd, "package-lock.json"))) pm = "npm";
4424
4978
  let defaultBranch = "main";
4425
4979
  try {
4426
- const ref = execFileSync12("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
4980
+ const ref = execFileSync2("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
4427
4981
  encoding: "utf-8",
4428
4982
  timeout: 5e3,
4429
4983
  cwd,
@@ -4432,7 +4986,7 @@ function detectBasicConfig(cwd) {
4432
4986
  defaultBranch = ref.replace("refs/remotes/origin/", "");
4433
4987
  } catch {
4434
4988
  try {
4435
- execFileSync12("git", ["rev-parse", "--verify", "origin/dev"], {
4989
+ execFileSync2("git", ["rev-parse", "--verify", "origin/dev"], {
4436
4990
  encoding: "utf-8",
4437
4991
  timeout: 5e3,
4438
4992
  cwd,
@@ -4445,7 +4999,7 @@ function detectBasicConfig(cwd) {
4445
4999
  let owner = "";
4446
5000
  let repo = "";
4447
5001
  try {
4448
- const remote = execFileSync12("git", ["remote", "get-url", "origin"], {
5002
+ const remote = execFileSync2("git", ["remote", "get-url", "origin"], {
4449
5003
  encoding: "utf-8",
4450
5004
  timeout: 5e3,
4451
5005
  cwd,
@@ -4463,7 +5017,7 @@ function detectBasicConfig(cwd) {
4463
5017
  function buildConfig(cwd, basic) {
4464
5018
  const pkg = (() => {
4465
5019
  try {
4466
- return JSON.parse(fs22.readFileSync(path21.join(cwd, "package.json"), "utf-8"));
5020
+ return JSON.parse(fs2.readFileSync(path.join(cwd, "package.json"), "utf-8"));
4467
5021
  } catch {
4468
5022
  return {};
4469
5023
  }
@@ -4495,7 +5049,6 @@ function buildConfig(cwd, basic) {
4495
5049
  if (mcp) config.mcp = mcp;
4496
5050
  return config;
4497
5051
  }
4498
- var FRONTEND_DEPS = ["next", "react", "vue", "svelte", "nuxt", "astro", "solid-js", "angular", "@angular/core"];
4499
5052
  function detectMcpConfig(cwd, pm, pkg) {
4500
5053
  const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
4501
5054
  const hasFrontend = FRONTEND_DEPS.some((dep) => dep in allDeps);
@@ -4507,12 +5060,7 @@ function detectMcpConfig(cwd, pm, pkg) {
4507
5060
  const defaultPort = isNext ? 3e3 : isVite ? 5173 : 3e3;
4508
5061
  const mcp = {
4509
5062
  enabled: true,
4510
- servers: {
4511
- playwright: {
4512
- command: "npx",
4513
- args: ["@playwright/mcp@latest"]
4514
- }
4515
- },
5063
+ servers: {},
4516
5064
  stages: ["build", "review"]
4517
5065
  };
4518
5066
  if (hasDevScript) {
@@ -4523,7 +5071,9 @@ function detectMcpConfig(cwd, pm, pkg) {
4523
5071
  }
4524
5072
  return mcp;
4525
5073
  }
4526
- function initCommand(opts) {
5074
+
5075
+ // src/bin/commands/init.ts
5076
+ function initCommand(opts, pkgRoot) {
4527
5077
  const cwd = process.cwd();
4528
5078
  console.log(`
4529
5079
  \u{1F527} Kody Engine Lite \u2014 Init
@@ -4531,35 +5081,35 @@ function initCommand(opts) {
4531
5081
  console.log(`Project: ${cwd}
4532
5082
  `);
4533
5083
  console.log("\u2500\u2500 Files \u2500\u2500");
4534
- const templatesDir = path21.join(PKG_ROOT, "templates");
5084
+ const templatesDir = path2.join(pkgRoot, "templates");
4535
5085
  const basic = detectBasicConfig(cwd);
4536
- const workflowSrc = path21.join(templatesDir, "kody.yml");
4537
- const workflowDest = path21.join(cwd, ".github", "workflows", "kody.yml");
4538
- if (!fs22.existsSync(workflowSrc)) {
5086
+ const workflowSrc = path2.join(templatesDir, "kody.yml");
5087
+ const workflowDest = path2.join(cwd, ".github", "workflows", "kody.yml");
5088
+ if (!fs3.existsSync(workflowSrc)) {
4539
5089
  console.error(" \u2717 Template kody.yml not found in package");
4540
5090
  process.exit(1);
4541
5091
  }
4542
- if (fs22.existsSync(workflowDest) && !opts.force) {
5092
+ if (fs3.existsSync(workflowDest) && !opts.force) {
4543
5093
  console.log(" \u25CB .github/workflows/kody.yml (exists, use --force to overwrite)");
4544
5094
  } else {
4545
- fs22.mkdirSync(path21.dirname(workflowDest), { recursive: true });
4546
- fs22.copyFileSync(workflowSrc, workflowDest);
5095
+ fs3.mkdirSync(path2.dirname(workflowDest), { recursive: true });
5096
+ fs3.copyFileSync(workflowSrc, workflowDest);
4547
5097
  console.log(" \u2713 .github/workflows/kody.yml");
4548
5098
  }
4549
- const configDest = path21.join(cwd, "kody.config.json");
4550
- if (!fs22.existsSync(configDest) || opts.force) {
5099
+ const configDest = path2.join(cwd, "kody.config.json");
5100
+ if (!fs3.existsSync(configDest) || opts.force) {
4551
5101
  const config = buildConfig(cwd, basic);
4552
- fs22.writeFileSync(configDest, JSON.stringify(config, null, 2) + "\n");
5102
+ fs3.writeFileSync(configDest, JSON.stringify(config, null, 2) + "\n");
4553
5103
  console.log(" \u2713 kody.config.json (auto-configured)");
4554
5104
  } else {
4555
5105
  console.log(" \u25CB kody.config.json (exists)");
4556
5106
  }
4557
- const gitignorePath = path21.join(cwd, ".gitignore");
4558
- if (fs22.existsSync(gitignorePath)) {
4559
- const content = fs22.readFileSync(gitignorePath, "utf-8");
5107
+ const gitignorePath = path2.join(cwd, ".gitignore");
5108
+ if (fs3.existsSync(gitignorePath)) {
5109
+ const content = fs3.readFileSync(gitignorePath, "utf-8");
4560
5110
  if (content.includes(".tasks/")) {
4561
5111
  const updated = content.replace(/\n?\.tasks\/\n?/g, "\n");
4562
- fs22.writeFileSync(gitignorePath, updated);
5112
+ fs3.writeFileSync(gitignorePath, updated);
4563
5113
  console.log(" \u2713 .gitignore (removed legacy .tasks/ \u2014 tasks now committed in .kody/tasks/)");
4564
5114
  } else {
4565
5115
  console.log(" \u25CB .gitignore (ok)");
@@ -4567,10 +5117,10 @@ function initCommand(opts) {
4567
5117
  }
4568
5118
  console.log("\n\u2500\u2500 Prerequisites \u2500\u2500");
4569
5119
  const checks = [
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`)
5120
+ checkCommand("gh", ["--version"], "Install: https://cli.github.com"),
5121
+ checkCommand("git", ["--version"], "Install git"),
5122
+ checkCommand("node", ["--version"], "Install Node.js >= 22"),
5123
+ checkFile(path2.join(cwd, "package.json"), "package.json", `Run: ${basic.pm} init`)
4574
5124
  ];
4575
5125
  for (const c of checks) {
4576
5126
  if (c.ok) {
@@ -4584,26 +5134,19 @@ function initCommand(opts) {
4584
5134
  console.log(ghAuth.ok ? ` \u2713 ${ghAuth.name} (${ghAuth.detail})` : ` \u2717 ${ghAuth.name} \u2014 ${ghAuth.fix}`);
4585
5135
  const ghRepo = checkGhRepoAccess(cwd);
4586
5136
  console.log(ghRepo.ok ? ` \u2713 ${ghRepo.name} (${ghRepo.detail})` : ` \u2717 ${ghRepo.name} \u2014 ${ghRepo.fix}`);
4587
- let repoSlug = "";
4588
5137
  if (ghRepo.ok && ghRepo.detail) {
4589
- repoSlug = ghRepo.detail;
4590
- const secretChecks = [
4591
- checkGhSecret(repoSlug, "ANTHROPIC_API_KEY")
4592
- ];
5138
+ const repoSlug = ghRepo.detail;
5139
+ const secretChecks = [checkGhSecret(repoSlug, "ANTHROPIC_API_KEY")];
4593
5140
  for (const c of secretChecks) {
4594
- if (c.ok) {
4595
- console.log(` \u2713 ${c.name}`);
4596
- } else {
4597
- console.log(` \u2717 ${c.name} \u2014 ${c.fix}`);
4598
- }
5141
+ console.log(c.ok ? ` \u2713 ${c.name}` : ` \u2717 ${c.name} \u2014 ${c.fix}`);
4599
5142
  }
4600
5143
  console.log("\n\u2500\u2500 Labels \u2500\u2500");
4601
5144
  console.log(" \u25CB Labels will be created automatically during bootstrap");
4602
5145
  }
4603
5146
  console.log("\n\u2500\u2500 Config \u2500\u2500");
4604
- if (fs22.existsSync(configDest)) {
5147
+ if (fs3.existsSync(configDest)) {
4605
5148
  try {
4606
- const config = JSON.parse(fs22.readFileSync(configDest, "utf-8"));
5149
+ const config = JSON.parse(fs3.readFileSync(configDest, "utf-8"));
4607
5150
  const configChecks = [];
4608
5151
  if (config.github?.owner && config.github?.repo) {
4609
5152
  configChecks.push({ name: "github.owner/repo", ok: true, detail: `${config.github.owner}/${config.github.repo}` });
@@ -4633,11 +5176,11 @@ function initCommand(opts) {
4633
5176
  const filesToCommit = [
4634
5177
  ".github/workflows/kody.yml",
4635
5178
  "kody.config.json"
4636
- ].filter((f) => fs22.existsSync(path21.join(cwd, f)));
5179
+ ].filter((f) => fs3.existsSync(path2.join(cwd, f)));
4637
5180
  if (filesToCommit.length > 0) {
4638
5181
  try {
4639
- const fullPaths = filesToCommit.map((f) => path21.join(cwd, f));
4640
- execFileSync12("npx", ["prettier", "--write", ...fullPaths], {
5182
+ const fullPaths = filesToCommit.map((f) => path2.join(cwd, f));
5183
+ execFileSync3("npx", ["prettier", "--write", ...fullPaths], {
4641
5184
  cwd,
4642
5185
  encoding: "utf-8",
4643
5186
  timeout: 3e4,
@@ -4648,13 +5191,13 @@ function initCommand(opts) {
4648
5191
  }
4649
5192
  if (filesToCommit.length > 0) {
4650
5193
  try {
4651
- execFileSync12("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
4652
- const staged = execFileSync12("git", ["diff", "--cached", "--name-only"], { cwd, encoding: "utf-8" }).trim();
5194
+ execFileSync3("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
5195
+ const staged = execFileSync3("git", ["diff", "--cached", "--name-only"], { cwd, encoding: "utf-8" }).trim();
4653
5196
  if (staged) {
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" });
5197
+ 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" });
4655
5198
  console.log(` \u2713 Committed: ${filesToCommit.join(", ")}`);
4656
5199
  try {
4657
- execFileSync12("git", ["push"], { cwd, stdio: "pipe", timeout: 6e4 });
5200
+ execFileSync3("git", ["push"], { cwd, stdio: "pipe", timeout: 6e4 });
4658
5201
  console.log(" \u2713 Pushed to origin");
4659
5202
  } catch {
4660
5203
  console.log(" \u25CB Push failed \u2014 run 'git push' manually");
@@ -4696,22 +5239,300 @@ function initCommand(opts) {
4696
5239
  console.log("");
4697
5240
  }
4698
5241
  }
5242
+
5243
+ // src/bin/commands/bootstrap.ts
5244
+ init_architecture_detection();
5245
+ import * as fs7 from "fs";
5246
+ import * as path6 from "path";
5247
+ import { execFileSync as execFileSync5 } from "child_process";
5248
+
5249
+ // src/bin/qa-guide.ts
5250
+ import * as fs5 from "fs";
5251
+ import * as path4 from "path";
5252
+ function discoverQaContext(cwd) {
5253
+ const result = {
5254
+ routes: [],
5255
+ authFiles: [],
5256
+ loginPage: null,
5257
+ adminPath: null,
5258
+ roles: [],
5259
+ devCommand: "",
5260
+ devPort: 3e3
5261
+ };
5262
+ try {
5263
+ const pkg = JSON.parse(fs5.readFileSync(path4.join(cwd, "package.json"), "utf-8"));
5264
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
5265
+ const pm = fs5.existsSync(path4.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs5.existsSync(path4.join(cwd, "yarn.lock")) ? "yarn" : "npm";
5266
+ if (pkg.scripts?.dev) result.devCommand = `${pm} dev`;
5267
+ if (allDeps.next || allDeps.nuxt) result.devPort = 3e3;
5268
+ else if (allDeps.vite) result.devPort = 5173;
5269
+ } catch {
5270
+ }
5271
+ const appDirs = ["src/app", "app"];
5272
+ for (const appDir of appDirs) {
5273
+ const fullAppDir = path4.join(cwd, appDir);
5274
+ if (!fs5.existsSync(fullAppDir)) continue;
5275
+ scanRoutes(fullAppDir, appDir, "", result);
5276
+ break;
5277
+ }
5278
+ const authPatterns = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
5279
+ for (const p of authPatterns) {
5280
+ if (fs5.existsSync(path4.join(cwd, p))) result.authFiles.push(p);
5281
+ }
5282
+ const authConfigGlobs = [
5283
+ "src/app/api/auth",
5284
+ "src/auth",
5285
+ "src/lib/auth",
5286
+ "auth.config.ts",
5287
+ "auth.ts",
5288
+ "src/app/api/oauth"
5289
+ ];
5290
+ for (const g of authConfigGlobs) {
5291
+ if (fs5.existsSync(path4.join(cwd, g))) result.authFiles.push(g);
5292
+ }
5293
+ try {
5294
+ const rolePaths = [
5295
+ "src/types",
5296
+ "src/lib",
5297
+ "src/utils",
5298
+ "src/constants",
5299
+ "src/access",
5300
+ "src/collections"
5301
+ ];
5302
+ for (const rp of rolePaths) {
5303
+ const dir = path4.join(cwd, rp);
5304
+ if (!fs5.existsSync(dir)) continue;
5305
+ const files = fs5.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
5306
+ for (const f of files) {
5307
+ try {
5308
+ const content = fs5.readFileSync(path4.join(dir, f), "utf-8").slice(0, 5e3);
5309
+ const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
5310
+ if (roleMatches) {
5311
+ for (const m of roleMatches) {
5312
+ const val = m.match(/['"](\w+)['"]/);
5313
+ if (val && !result.roles.includes(val[1])) result.roles.push(val[1]);
5314
+ }
5315
+ }
5316
+ const enumMatch = content.match(/(?:enum|type)\s+\w*[Rr]ole\w*\s*[={]([^}]+)/s);
5317
+ if (enumMatch) {
5318
+ const vals = enumMatch[1].match(/['"](\w+)['"]/g);
5319
+ if (vals) {
5320
+ for (const v of vals) {
5321
+ const clean = v.replace(/['"]/g, "");
5322
+ if (!result.roles.includes(clean)) result.roles.push(clean);
5323
+ }
5324
+ }
5325
+ }
5326
+ } catch {
5327
+ }
5328
+ }
5329
+ }
5330
+ } catch {
5331
+ }
5332
+ return result;
5333
+ }
5334
+ function scanRoutes(dir, baseDir, prefix, result) {
5335
+ let entries;
5336
+ try {
5337
+ entries = fs5.readdirSync(dir, { withFileTypes: true });
5338
+ } catch {
5339
+ return;
5340
+ }
5341
+ const hasPage = entries.some((e) => e.isFile() && /^page\.(tsx?|jsx?)$/.test(e.name));
5342
+ if (hasPage) {
5343
+ const routePath = prefix || "/";
5344
+ const group = prefix.startsWith("/admin") ? "admin" : prefix.includes("/login") ? "auth" : prefix.includes("/signup") ? "auth" : prefix.includes("/api") ? "api" : "frontend";
5345
+ result.routes.push({ path: routePath, group });
5346
+ if (prefix.includes("/login")) result.loginPage = routePath;
5347
+ if (prefix.startsWith("/admin") && !result.adminPath) result.adminPath = prefix;
5348
+ }
5349
+ for (const entry of entries) {
5350
+ if (!entry.isDirectory()) continue;
5351
+ if (entry.name === "node_modules" || entry.name === ".next") continue;
5352
+ let segment = entry.name;
5353
+ if (segment.startsWith("(") && segment.endsWith(")")) {
5354
+ scanRoutes(path4.join(dir, entry.name), baseDir, prefix, result);
5355
+ continue;
5356
+ }
5357
+ if (segment.startsWith("[") && segment.endsWith("]")) {
5358
+ segment = `:${segment.slice(1, -1)}`;
5359
+ }
5360
+ if (segment.startsWith("[[") && segment.endsWith("]]")) {
5361
+ segment = `:${segment.slice(2, -2)}?`;
5362
+ }
5363
+ scanRoutes(path4.join(dir, entry.name), baseDir, `${prefix}/${segment}`, result);
5364
+ }
5365
+ }
5366
+ function generateQaGuide(discovery) {
5367
+ const lines = ["# QA Guide", "", "## Authentication", ""];
5368
+ if (discovery.loginPage) {
5369
+ lines.push(`- Login page: \`${discovery.loginPage}\``);
5370
+ }
5371
+ lines.push(
5372
+ "",
5373
+ "### Test Accounts",
5374
+ "<!-- Fill in your test/preview environment credentials below -->",
5375
+ "| Role | Email | Password |",
5376
+ "|------|-------|----------|",
5377
+ "| Admin | admin@example.com | CHANGE_ME |",
5378
+ "| User | user@example.com | CHANGE_ME |",
5379
+ "",
5380
+ "### Login Steps",
5381
+ `1. Navigate to \`${discovery.loginPage ?? "/login"}\``,
5382
+ "2. Enter credentials from the test accounts table above",
5383
+ "3. Submit the login form",
5384
+ "4. Verify redirect to dashboard or home page"
5385
+ );
5386
+ if (discovery.authFiles.length > 0) {
5387
+ lines.push("", "### Auth Files");
5388
+ for (const f of discovery.authFiles) {
5389
+ lines.push(`- \`${f}\``);
5390
+ }
5391
+ }
5392
+ if (discovery.roles.length > 0) {
5393
+ lines.push("", "## Roles", "");
5394
+ for (const role of discovery.roles) {
5395
+ lines.push(`- \`${role}\``);
5396
+ }
5397
+ }
5398
+ lines.push("", "## Key Pages", "");
5399
+ const groups = {};
5400
+ for (const route of discovery.routes) {
5401
+ if (!groups[route.group]) groups[route.group] = [];
5402
+ groups[route.group].push(route.path);
5403
+ }
5404
+ for (const [group, routes] of Object.entries(groups)) {
5405
+ lines.push(`### ${group.charAt(0).toUpperCase() + group.slice(1)}`);
5406
+ const sorted = routes.sort();
5407
+ for (const r of sorted.slice(0, 20)) {
5408
+ lines.push(`- \`${r}\``);
5409
+ }
5410
+ if (sorted.length > 20) {
5411
+ lines.push(`- ... and ${sorted.length - 20} more`);
5412
+ }
5413
+ lines.push("");
5414
+ }
5415
+ lines.push(
5416
+ "## Dev Server",
5417
+ "",
5418
+ `- Command: \`${discovery.devCommand || "pnpm dev"}\``,
5419
+ `- URL: \`http://localhost:${discovery.devPort}\``,
5420
+ ""
5421
+ );
5422
+ return lines.join("\n");
5423
+ }
5424
+
5425
+ // src/bin/skills.ts
5426
+ import * as fs6 from "fs";
5427
+ import * as path5 from "path";
5428
+ import { execFileSync as execFileSync4 } from "child_process";
5429
+ var SKILL_MAPPINGS = [
5430
+ {
5431
+ detect: (deps) => "next" in deps,
5432
+ skills: [
5433
+ { package: "vercel-labs/agent-skills@vercel-react-best-practices", label: "React best practices (Vercel)" }
5434
+ ]
5435
+ },
5436
+ {
5437
+ detect: (deps) => "react" in deps && !("next" in deps),
5438
+ skills: [
5439
+ { package: "vercel-labs/agent-skills@vercel-react-best-practices", label: "React best practices (Vercel)" }
5440
+ ]
5441
+ },
5442
+ {
5443
+ detect: (deps) => FRONTEND_DEPS.some((d) => d in deps),
5444
+ skills: [
5445
+ { package: "microsoft/playwright-cli@playwright-cli", label: "Playwright browser automation" }
5446
+ ]
5447
+ }
5448
+ ];
5449
+ function detectSkillsForProject(cwd) {
5450
+ const pkgPath = path5.join(cwd, "package.json");
5451
+ if (!fs6.existsSync(pkgPath)) return [];
5452
+ try {
5453
+ const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
5454
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
5455
+ const seen = /* @__PURE__ */ new Set();
5456
+ const skills = [];
5457
+ for (const mapping of SKILL_MAPPINGS) {
5458
+ if (mapping.detect(allDeps)) {
5459
+ for (const skill of mapping.skills) {
5460
+ if (!seen.has(skill.package)) {
5461
+ seen.add(skill.package);
5462
+ skills.push(skill);
5463
+ }
5464
+ }
5465
+ }
5466
+ }
5467
+ return skills;
5468
+ } catch {
5469
+ return [];
5470
+ }
5471
+ }
5472
+ function installSkillsForProject(cwd) {
5473
+ const skills = detectSkillsForProject(cwd);
5474
+ if (skills.length === 0) {
5475
+ console.log(" \u25CB No skills to install (no frontend framework detected)");
5476
+ return [];
5477
+ }
5478
+ let installedSkills = {};
5479
+ const lockPath = path5.join(cwd, "skills-lock.json");
5480
+ if (fs6.existsSync(lockPath)) {
5481
+ try {
5482
+ const lock = JSON.parse(fs6.readFileSync(lockPath, "utf-8"));
5483
+ installedSkills = lock.skills ?? {};
5484
+ } catch {
5485
+ }
5486
+ }
5487
+ const installedPaths = [];
5488
+ for (const skill of skills) {
5489
+ const skillName = skill.package.split("@").pop() ?? "";
5490
+ if (skillName in installedSkills) {
5491
+ console.log(` \u25CB ${skill.label} \u2014 already installed`);
5492
+ const agentPath = `.agents/skills/${skillName}`;
5493
+ const claudePath = `.claude/skills/${skillName}`;
5494
+ if (fs6.existsSync(path5.join(cwd, agentPath))) installedPaths.push(agentPath);
5495
+ if (fs6.existsSync(path5.join(cwd, claudePath))) installedPaths.push(claudePath);
5496
+ continue;
5497
+ }
5498
+ try {
5499
+ console.log(` Installing: ${skill.label} (${skill.package})`);
5500
+ execFileSync4("npx", ["skills", "add", skill.package, "--yes"], {
5501
+ cwd,
5502
+ encoding: "utf-8",
5503
+ timeout: 6e4,
5504
+ stdio: ["pipe", "pipe", "pipe"]
5505
+ });
5506
+ const installedName = skill.package.split("@").pop() ?? "";
5507
+ const agentPath = `.agents/skills/${installedName}`;
5508
+ const claudePath = `.claude/skills/${installedName}`;
5509
+ if (fs6.existsSync(path5.join(cwd, agentPath))) installedPaths.push(agentPath);
5510
+ if (fs6.existsSync(path5.join(cwd, claudePath))) installedPaths.push(claudePath);
5511
+ console.log(` \u2713 ${skill.label}`);
5512
+ } catch {
5513
+ console.log(` \u2717 ${skill.label} \u2014 failed to install`);
5514
+ }
5515
+ }
5516
+ return installedPaths;
5517
+ }
5518
+
5519
+ // src/bin/commands/bootstrap.ts
4699
5520
  var STEP_STAGES = ["taskify", "plan", "build", "autofix", "review", "review-fix"];
4700
5521
  function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
4701
- const srcDir = path21.join(cwd, "src");
4702
- const baseDir = fs22.existsSync(srcDir) ? srcDir : cwd;
5522
+ const srcDir = path6.join(cwd, "src");
5523
+ const baseDir = fs7.existsSync(srcDir) ? srcDir : cwd;
4703
5524
  const results = [];
4704
5525
  function walk(dir) {
4705
5526
  const entries = [];
4706
5527
  try {
4707
- for (const entry of fs22.readdirSync(dir, { withFileTypes: true })) {
5528
+ for (const entry of fs7.readdirSync(dir, { withFileTypes: true })) {
4708
5529
  if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
4709
- const full = path21.join(dir, entry.name);
5530
+ const full = path6.join(dir, entry.name);
4710
5531
  if (entry.isDirectory()) {
4711
5532
  entries.push(...walk(full));
4712
5533
  } else if (/\.(ts|js)$/.test(entry.name) && !/\.(test|spec|config|d)\.(ts|js)$/.test(entry.name)) {
4713
5534
  try {
4714
- const stat = fs22.statSync(full);
5535
+ const stat = fs7.statSync(full);
4715
5536
  if (stat.size >= 200 && stat.size <= 5e3) {
4716
5537
  entries.push({ filePath: full, size: stat.size });
4717
5538
  }
@@ -4725,8 +5546,8 @@ function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
4725
5546
  }
4726
5547
  const files = walk(baseDir).sort((a, b) => b.size - a.size).slice(0, maxFiles);
4727
5548
  for (const { filePath } of files) {
4728
- const rel = path21.relative(cwd, filePath);
4729
- const content = fs22.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
5549
+ const rel = path6.relative(cwd, filePath);
5550
+ const content = fs7.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
4730
5551
  results.push(`### File: ${rel}
4731
5552
  \`\`\`typescript
4732
5553
  ${content}
@@ -4738,9 +5559,9 @@ function ghComment(issueNumber, body, cwd) {
4738
5559
  try {
4739
5560
  let repoSlug = "";
4740
5561
  try {
4741
- const configPath = path21.join(cwd, "kody.config.json");
4742
- if (fs22.existsSync(configPath)) {
4743
- const config = JSON.parse(fs22.readFileSync(configPath, "utf-8"));
5562
+ const configPath = path6.join(cwd, "kody.config.json");
5563
+ if (fs7.existsSync(configPath)) {
5564
+ const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
4744
5565
  if (config.github?.owner && config.github?.repo) {
4745
5566
  repoSlug = `${config.github.owner}/${config.github.repo}`;
4746
5567
  }
@@ -4748,7 +5569,7 @@ function ghComment(issueNumber, body, cwd) {
4748
5569
  } catch {
4749
5570
  }
4750
5571
  if (!repoSlug) return;
4751
- execFileSync12("gh", [
5572
+ execFileSync5("gh", [
4752
5573
  "issue",
4753
5574
  "comment",
4754
5575
  String(issueNumber),
@@ -4765,7 +5586,7 @@ function ghComment(issueNumber, body, cwd) {
4765
5586
  } catch {
4766
5587
  }
4767
5588
  }
4768
- function bootstrapCommand(opts = { force: false }) {
5589
+ function bootstrapCommand(opts, pkgRoot) {
4769
5590
  const cwd = process.cwd();
4770
5591
  const issueNumber = parseInt(process.env.ISSUE_NUMBER ?? "", 10) || 0;
4771
5592
  console.log(`
@@ -4775,8 +5596,8 @@ function bootstrapCommand(opts = { force: false }) {
4775
5596
  ghComment(issueNumber, "\u{1F527} **Bootstrap started** \u2014 analyzing project and generating configuration...", cwd);
4776
5597
  }
4777
5598
  const readIfExists = (rel, maxChars = 3e3) => {
4778
- const p = path21.join(cwd, rel);
4779
- if (fs22.existsSync(p)) return fs22.readFileSync(p, "utf-8").slice(0, maxChars);
5599
+ const p = path6.join(cwd, rel);
5600
+ if (fs7.existsSync(p)) return fs7.readFileSync(p, "utf-8").slice(0, maxChars);
4780
5601
  return null;
4781
5602
  };
4782
5603
  let repoContext = "";
@@ -4811,14 +5632,14 @@ ${sampleFiles}
4811
5632
 
4812
5633
  `;
4813
5634
  try {
4814
- const topDirs = fs22.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
5635
+ const topDirs = fs7.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
4815
5636
  repoContext += `## Top-level directories
4816
5637
  ${topDirs.join(", ")}
4817
5638
 
4818
5639
  `;
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);
5640
+ const srcDir = path6.join(cwd, "src");
5641
+ if (fs7.existsSync(srcDir)) {
5642
+ const srcDirs = fs7.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
4822
5643
  if (srcDirs.length > 0) repoContext += `## src/ subdirectories
4823
5644
  ${srcDirs.join(", ")}
4824
5645
 
@@ -4828,19 +5649,19 @@ ${srcDirs.join(", ")}
4828
5649
  }
4829
5650
  const existingFiles = [];
4830
5651
  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"]) {
4831
- if (fs22.existsSync(path21.join(cwd, f))) existingFiles.push(f);
5652
+ if (fs7.existsSync(path6.join(cwd, f))) existingFiles.push(f);
4832
5653
  }
4833
5654
  if (existingFiles.length) repoContext += `## Config files present
4834
5655
  ${existingFiles.join(", ")}
4835
5656
 
4836
5657
  `;
4837
5658
  console.log("\u2500\u2500 Project Memory \u2500\u2500");
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") : "";
5659
+ const memoryDir = path6.join(cwd, ".kody", "memory");
5660
+ fs7.mkdirSync(memoryDir, { recursive: true });
5661
+ const archPath = path6.join(memoryDir, "architecture.md");
5662
+ const conventionsPath = path6.join(memoryDir, "conventions.md");
5663
+ const existingArch = fs7.existsSync(archPath) ? fs7.readFileSync(archPath, "utf-8") : "";
5664
+ const existingConv = fs7.existsSync(conventionsPath) ? fs7.readFileSync(conventionsPath, "utf-8") : "";
4844
5665
  const hasExisting = !!(existingArch || existingConv);
4845
5666
  const extendInstruction = hasExisting && !opts.force ? `
4846
5667
  ## Existing Documentation (EXTEND, do not replace)
@@ -4881,7 +5702,7 @@ Output ONLY valid JSON. No markdown fences. No explanation.
4881
5702
  ${repoContext}`;
4882
5703
  console.log(" \u23F3 Analyzing project...");
4883
5704
  try {
4884
- const output = execFileSync12("claude", [
5705
+ const output = execFileSync5("claude", [
4885
5706
  "--print",
4886
5707
  "--model",
4887
5708
  "haiku",
@@ -4896,12 +5717,12 @@ ${repoContext}`;
4896
5717
  const cleaned = output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
4897
5718
  const parsed = JSON.parse(cleaned);
4898
5719
  if (parsed.architecture) {
4899
- fs22.writeFileSync(archPath, parsed.architecture);
5720
+ fs7.writeFileSync(archPath, parsed.architecture);
4900
5721
  const lineCount = parsed.architecture.split("\n").length;
4901
5722
  console.log(` \u2713 .kody/memory/architecture.md (${lineCount} lines)`);
4902
5723
  }
4903
5724
  if (parsed.conventions) {
4904
- fs22.writeFileSync(conventionsPath, parsed.conventions);
5725
+ fs7.writeFileSync(conventionsPath, parsed.conventions);
4905
5726
  const lineCount = parsed.conventions.split("\n").length;
4906
5727
  console.log(` \u2713 .kody/memory/conventions.md (${lineCount} lines)`);
4907
5728
  }
@@ -4910,39 +5731,39 @@ ${repoContext}`;
4910
5731
  const detected = detectArchitectureBasic(cwd);
4911
5732
  if (detected.length > 0) {
4912
5733
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
4913
- fs22.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
5734
+ fs7.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
4914
5735
 
4915
5736
  ## Overview
4916
5737
  ${detected.join("\n")}
4917
5738
  `);
4918
5739
  console.log(` \u2713 .kody/memory/architecture.md (${detected.length} items, basic detection)`);
4919
5740
  }
4920
- fs22.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
5741
+ fs7.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
4921
5742
  console.log(" \u2713 .kody/memory/conventions.md (seed)");
4922
5743
  }
4923
5744
  console.log("\n\u2500\u2500 Step Files \u2500\u2500");
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") : "";
5745
+ const stepsDir = path6.join(cwd, ".kody", "steps");
5746
+ fs7.mkdirSync(stepsDir, { recursive: true });
5747
+ const arch = fs7.existsSync(archPath) ? fs7.readFileSync(archPath, "utf-8") : "";
5748
+ const conv = fs7.existsSync(conventionsPath) ? fs7.readFileSync(conventionsPath, "utf-8") : "";
4928
5749
  console.log(" \u23F3 Customizing step files...");
4929
5750
  let stepCount = 0;
4930
5751
  for (const stage of STEP_STAGES) {
4931
- const templatePath = path21.join(PKG_ROOT, "prompts", `${stage}.md`);
4932
- if (!fs22.existsSync(templatePath)) {
5752
+ const templatePath = path6.join(pkgRoot, "prompts", `${stage}.md`);
5753
+ if (!fs7.existsSync(templatePath)) {
4933
5754
  console.log(` \u2717 ${stage}.md \u2014 template not found in engine`);
4934
5755
  continue;
4935
5756
  }
4936
- const stepOutputPath = path21.join(stepsDir, `${stage}.md`);
4937
- if (fs22.existsSync(stepOutputPath) && !opts.force) {
5757
+ const stepOutputPath = path6.join(stepsDir, `${stage}.md`);
5758
+ if (fs7.existsSync(stepOutputPath) && !opts.force) {
4938
5759
  console.log(` \u25CB ${stage}.md \u2014 already exists (use --force to regenerate)`);
4939
5760
  continue;
4940
5761
  }
4941
- const defaultPrompt = fs22.readFileSync(templatePath, "utf-8");
5762
+ const defaultPrompt = fs7.readFileSync(templatePath, "utf-8");
4942
5763
  const contextPlaceholder = "{{TASK_CONTEXT}}";
4943
5764
  const placeholderIdx = defaultPrompt.indexOf(contextPlaceholder);
4944
5765
  if (placeholderIdx === -1) {
4945
- fs22.copyFileSync(templatePath, stepOutputPath);
5766
+ fs7.copyFileSync(templatePath, stepOutputPath);
4946
5767
  stepCount++;
4947
5768
  console.log(` \u2713 ${stage}.md`);
4948
5769
  continue;
@@ -4984,7 +5805,7 @@ ${repoContext}
4984
5805
 
4985
5806
  REMINDER: Output the full prompt template first (unchanged), then your three appended sections. Do NOT include "${contextPlaceholder}".`;
4986
5807
  try {
4987
- const output = execFileSync12("claude", [
5808
+ const output = execFileSync5("claude", [
4988
5809
  "--print",
4989
5810
  "--model",
4990
5811
  "haiku",
@@ -4999,23 +5820,40 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
4999
5820
  let cleaned = output.replace(/^```(?:markdown|md)?\s*\n?/, "").replace(/\n?```\s*$/, "");
5000
5821
  cleaned = cleaned.replace(/\n*\{\{TASK_CONTEXT\}\}\s*$/, "").trimEnd();
5001
5822
  const finalPrompt = cleaned + "\n\n" + afterPlaceholder;
5002
- fs22.writeFileSync(stepOutputPath, finalPrompt);
5823
+ fs7.writeFileSync(stepOutputPath, finalPrompt);
5003
5824
  stepCount++;
5004
5825
  console.log(` \u2713 ${stage}.md`);
5005
5826
  } catch {
5006
5827
  console.log(` \u26A0 ${stage}.md \u2014 customization failed, using default template`);
5007
- fs22.copyFileSync(templatePath, stepOutputPath);
5828
+ fs7.copyFileSync(templatePath, stepOutputPath);
5008
5829
  stepCount++;
5009
5830
  }
5010
5831
  }
5011
5832
  console.log(` \u2713 Generated ${stepCount} step files in .kody/steps/`);
5833
+ console.log("\n\u2500\u2500 QA Guide \u2500\u2500");
5834
+ const qaGuidePath = path6.join(cwd, ".kody", "qa-guide.md");
5835
+ if (!fs7.existsSync(qaGuidePath) || opts.force) {
5836
+ const discovery = discoverQaContext(cwd);
5837
+ if (discovery.routes.length > 0) {
5838
+ const qaGuide = generateQaGuide(discovery);
5839
+ fs7.writeFileSync(qaGuidePath, qaGuide);
5840
+ console.log(` \u2713 .kody/qa-guide.md (${discovery.routes.length} routes, ${discovery.roles.length} roles)`);
5841
+ if (discovery.loginPage) console.log(` \u2713 Login page detected: ${discovery.loginPage}`);
5842
+ if (discovery.adminPath) console.log(` \u2713 Admin panel detected: ${discovery.adminPath}`);
5843
+ console.log(" \u2139 Add QA_ADMIN_EMAIL, QA_ADMIN_PASSWORD, QA_USER_EMAIL, QA_USER_PASSWORD as GitHub secrets");
5844
+ } else {
5845
+ console.log(" \u25CB No routes detected \u2014 skipping QA guide");
5846
+ }
5847
+ } else {
5848
+ console.log(" \u25CB .kody/qa-guide.md already exists (use --force to regenerate)");
5849
+ }
5012
5850
  console.log("\n\u2500\u2500 Labels \u2500\u2500");
5013
5851
  try {
5014
5852
  let repoSlug = "";
5015
5853
  try {
5016
- const configPath = path21.join(cwd, "kody.config.json");
5017
- if (fs22.existsSync(configPath)) {
5018
- const config = JSON.parse(fs22.readFileSync(configPath, "utf-8"));
5854
+ const configPath = path6.join(cwd, "kody.config.json");
5855
+ if (fs7.existsSync(configPath)) {
5856
+ const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
5019
5857
  if (config.github?.owner && config.github?.repo) {
5020
5858
  repoSlug = `${config.github.owner}/${config.github.repo}`;
5021
5859
  }
@@ -5027,6 +5865,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
5027
5865
  { name: "kody:planning", color: "c5def5", description: "Kody is analyzing and planning" },
5028
5866
  { name: "kody:building", color: "0e8a16", description: "Kody is building code" },
5029
5867
  { name: "kody:review", color: "fbca04", description: "Kody is reviewing code" },
5868
+ { name: "kody:shipping", color: "1d76db", description: "Kody is creating the pull request" },
5030
5869
  { name: "kody:done", color: "0e8a16", description: "Kody completed successfully" },
5031
5870
  { name: "kody:failed", color: "d93f0b", description: "Kody pipeline failed" },
5032
5871
  { name: "kody:waiting", color: "fef2c0", description: "Kody is waiting for answers" },
@@ -5041,7 +5880,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
5041
5880
  ];
5042
5881
  for (const label of labels) {
5043
5882
  try {
5044
- execFileSync12("gh", [
5883
+ execFileSync5("gh", [
5045
5884
  "label",
5046
5885
  "create",
5047
5886
  label.name,
@@ -5061,7 +5900,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
5061
5900
  console.log(` \u2713 ${label.name}`);
5062
5901
  } catch {
5063
5902
  try {
5064
- execFileSync12("gh", ["label", "list", "--repo", repoSlug, "--search", label.name], {
5903
+ execFileSync5("gh", ["label", "list", "--repo", repoSlug, "--search", label.name], {
5065
5904
  cwd,
5066
5905
  encoding: "utf-8",
5067
5906
  timeout: 1e4,
@@ -5085,22 +5924,23 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
5085
5924
  const filesToCommit = [
5086
5925
  ".kody/memory/architecture.md",
5087
5926
  ".kody/memory/conventions.md",
5927
+ ".kody/qa-guide.md",
5088
5928
  ...installedSkillPaths
5089
- ].filter((f) => fs22.existsSync(path21.join(cwd, f)));
5090
- if (fs22.existsSync(path21.join(cwd, "skills-lock.json"))) {
5929
+ ].filter((f) => fs7.existsSync(path6.join(cwd, f)));
5930
+ if (fs7.existsSync(path6.join(cwd, "skills-lock.json"))) {
5091
5931
  filesToCommit.push("skills-lock.json");
5092
5932
  }
5093
5933
  for (const stage of STEP_STAGES) {
5094
5934
  const stepFile = `.kody/steps/${stage}.md`;
5095
- if (fs22.existsSync(path21.join(cwd, stepFile))) {
5935
+ if (fs7.existsSync(path6.join(cwd, stepFile))) {
5096
5936
  filesToCommit.push(stepFile);
5097
5937
  }
5098
5938
  }
5099
5939
  if (filesToCommit.length > 0) {
5100
5940
  try {
5101
- const fullPaths = filesToCommit.map((f) => path21.join(cwd, f));
5941
+ const fullPaths = filesToCommit.map((f) => path6.join(cwd, f));
5102
5942
  for (let pass = 0; pass < 2; pass++) {
5103
- execFileSync12("npx", ["prettier", "--write", ...fullPaths], {
5943
+ execFileSync5("npx", ["prettier", "--write", ...fullPaths], {
5104
5944
  cwd,
5105
5945
  encoding: "utf-8",
5106
5946
  timeout: 3e4,
@@ -5116,24 +5956,24 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
5116
5956
  try {
5117
5957
  if (isCI3) {
5118
5958
  const branchName = `kody-bootstrap-${Date.now()}`;
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();
5959
+ execFileSync5("git", ["checkout", "-b", branchName], { cwd, stdio: "pipe" });
5960
+ execFileSync5("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
5961
+ const staged = execFileSync5("git", ["diff", "--cached", "--name-only"], { cwd, encoding: "utf-8" }).trim();
5122
5962
  if (staged) {
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 });
5963
+ 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" });
5964
+ execFileSync5("git", ["push", "-u", "origin", branchName], { cwd, stdio: "pipe", timeout: 6e4 });
5125
5965
  console.log(` \u2713 Pushed branch: ${branchName}`);
5126
5966
  let baseBranch = "main";
5127
5967
  try {
5128
- const configPath = path21.join(cwd, "kody.config.json");
5129
- if (fs22.existsSync(configPath)) {
5130
- const config = JSON.parse(fs22.readFileSync(configPath, "utf-8"));
5968
+ const configPath = path6.join(cwd, "kody.config.json");
5969
+ if (fs7.existsSync(configPath)) {
5970
+ const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
5131
5971
  baseBranch = config.git?.defaultBranch ?? "main";
5132
5972
  }
5133
5973
  } catch {
5134
5974
  }
5135
5975
  try {
5136
- const prUrl = execFileSync12("gh", [
5976
+ const prUrl = execFileSync5("gh", [
5137
5977
  "pr",
5138
5978
  "create",
5139
5979
  "--title",
@@ -5172,13 +6012,13 @@ Create it manually.`, cwd);
5172
6012
  console.log(" \u25CB No new changes to commit");
5173
6013
  }
5174
6014
  } else {
5175
- execFileSync12("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
5176
- const staged = execFileSync12("git", ["diff", "--cached", "--name-only"], { cwd, encoding: "utf-8" }).trim();
6015
+ execFileSync5("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
6016
+ const staged = execFileSync5("git", ["diff", "--cached", "--name-only"], { cwd, encoding: "utf-8" }).trim();
5177
6017
  if (staged) {
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" });
6018
+ 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" });
5179
6019
  console.log(` \u2713 Committed: ${filesToCommit.join(", ")}`);
5180
6020
  try {
5181
- execFileSync12("git", ["push"], { cwd, stdio: "pipe", timeout: 6e4 });
6021
+ execFileSync5("git", ["push"], { cwd, stdio: "pipe", timeout: 6e4 });
5182
6022
  console.log(" \u2713 Pushed to origin");
5183
6023
  } catch {
5184
6024
  console.log(" \u25CB Push failed \u2014 run 'git push' manually");
@@ -5198,131 +6038,24 @@ Create it manually.`, cwd);
5198
6038
  console.log(" \u2713 Project bootstrap complete!");
5199
6039
  console.log(" Kody now has project-specific memory and customized step files.\n");
5200
6040
  }
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;
6041
+
6042
+ // src/bin/cli.ts
6043
+ init_architecture_detection();
6044
+ var __dirname = path27.dirname(fileURLToPath(import.meta.url));
6045
+ var PKG_ROOT = path27.resolve(__dirname, "..", "..");
6046
+ function getVersion() {
6047
+ const pkgPath = path27.join(PKG_ROOT, "package.json");
6048
+ const pkg = JSON.parse(fs30.readFileSync(pkgPath, "utf-8"));
6049
+ return pkg.version;
5319
6050
  }
5320
6051
  var args = process.argv.slice(2);
5321
6052
  var command = args[0];
5322
6053
  if (command === "init") {
5323
- initCommand({ force: args.includes("--force") });
6054
+ initCommand({ force: args.includes("--force") }, PKG_ROOT);
5324
6055
  } else if (command === "bootstrap") {
5325
- bootstrapCommand({ force: args.includes("--force") });
6056
+ bootstrapCommand({ force: args.includes("--force") }, PKG_ROOT);
6057
+ } else if (command === "ci-parse") {
6058
+ Promise.resolve().then(() => (init_parse_inputs(), parse_inputs_exports)).then(({ runCiParse: runCiParse2 }) => runCiParse2());
5326
6059
  } else if (command === "version" || command === "--version" || command === "-v") {
5327
6060
  console.log(getVersion());
5328
6061
  } else {
@@ -5330,7 +6063,7 @@ if (command === "init") {
5330
6063
  }
5331
6064
  export {
5332
6065
  buildConfig,
5333
- checkCommand2 as checkCommand,
6066
+ checkCommand,
5334
6067
  checkFile,
5335
6068
  checkGhAuth,
5336
6069
  checkGhRepoAccess,