@kody-ade/kody-engine-lite 0.1.100 → 0.1.102

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/README.md CHANGED
@@ -154,7 +154,7 @@ kody-engine-lite rerun --issue-number 42 --from verify
154
154
 
155
155
  ## Documentation
156
156
 
157
- **Understand Kody:** [About](docs/ABOUT.md) · [Features](docs/FEATURES.md) · [Pipeline](docs/PIPELINE.md) · [Comparison](docs/COMPARISON.md)
157
+ **Understand Kody:** [About](docs/ABOUT.md) · [Tech Stack](docs/TECH-STACK.md) · [Features](docs/FEATURES.md) · [Pipeline](docs/PIPELINE.md) · [Comparison](docs/COMPARISON.md)
158
158
 
159
159
  **Set up & use:** [CLI](docs/CLI.md) · [Configuration](docs/CONFIGURATION.md) · [Bootstrap](docs/BOOTSTRAP.md) · [LiteLLM](docs/LITELLM.md)
160
160
 
package/dist/bin/cli.js CHANGED
@@ -305,7 +305,13 @@ function getProjectConfig() {
305
305
  },
306
306
  timeouts: raw.timeouts ?? void 0,
307
307
  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
308
+ mcp: raw.mcp ? {
309
+ servers: {},
310
+ stages: ["build", "verify", "review", "review-fix"],
311
+ ...raw.mcp,
312
+ // Auto-enable when devServer is configured (user can still set enabled: false to override)
313
+ enabled: raw.mcp.enabled ?? !!raw.mcp.devServer
314
+ } : void 0
309
315
  };
310
316
  } catch {
311
317
  logger.warn("kody.config.json is invalid JSON \u2014 using defaults");
@@ -861,7 +867,7 @@ var init_github_api = __esm({
861
867
  "use strict";
862
868
  init_logger();
863
869
  API_TIMEOUT_MS = 3e4;
864
- LIFECYCLE_LABELS = ["planning", "building", "review", "done", "failed", "waiting", "low", "medium", "high"];
870
+ LIFECYCLE_LABELS = ["planning", "building", "review", "shipping", "done", "failed", "waiting", "low", "medium", "high"];
865
871
  KODY_MARKERS = [
866
872
  "Kody Review",
867
873
  "\u{1F916} Generated by Kody",
@@ -1281,6 +1287,20 @@ var init_context_tiers = __esm({
1281
1287
  });
1282
1288
 
1283
1289
  // src/mcp-config.ts
1290
+ function withPlaywrightIfNeeded(mcpConfig, hasUI) {
1291
+ if (!mcpConfig?.enabled || !hasUI) return mcpConfig;
1292
+ const hasPlaywright = Object.keys(mcpConfig.servers).some(
1293
+ (name) => name.toLowerCase().includes("playwright")
1294
+ );
1295
+ if (hasPlaywright) return mcpConfig;
1296
+ return {
1297
+ ...mcpConfig,
1298
+ servers: {
1299
+ ...mcpConfig.servers,
1300
+ playwright: PLAYWRIGHT_SERVER
1301
+ }
1302
+ };
1303
+ }
1284
1304
  function buildMcpConfigJson(mcpConfig) {
1285
1305
  if (!mcpConfig?.enabled) return void 0;
1286
1306
  if (Object.keys(mcpConfig.servers).length === 0) return void 0;
@@ -1297,15 +1317,18 @@ function buildMcpConfigJson(mcpConfig) {
1297
1317
  }
1298
1318
  function isMcpEnabledForStage(stageName, mcpConfig) {
1299
1319
  if (!mcpConfig?.enabled) return false;
1300
- if (Object.keys(mcpConfig.servers).length === 0) return false;
1301
1320
  const allowedStages = mcpConfig.stages ?? DEFAULT_MCP_STAGES;
1302
1321
  return allowedStages.includes(stageName);
1303
1322
  }
1304
- var DEFAULT_MCP_STAGES;
1323
+ var DEFAULT_MCP_STAGES, PLAYWRIGHT_SERVER;
1305
1324
  var init_mcp_config = __esm({
1306
1325
  "src/mcp-config.ts"() {
1307
1326
  "use strict";
1308
1327
  DEFAULT_MCP_STAGES = ["build", "verify", "review", "review-fix"];
1328
+ PLAYWRIGHT_SERVER = {
1329
+ command: "npx",
1330
+ args: ["-y", "@anthropic-ai/mcp-playwright"]
1331
+ };
1309
1332
  }
1310
1333
  });
1311
1334
 
@@ -1523,6 +1546,11 @@ ${prompt}` : prompt;
1523
1546
  }
1524
1547
  if (isMcpEnabledForStage(stageName, config.mcp) && taskHasUI(taskDir)) {
1525
1548
  assembled = assembled + "\n\n" + getBrowserToolGuidance(stageName, taskDir);
1549
+ const qaGuidePath = path5.join(projectDir, ".kody", "qa-guide.md");
1550
+ if (fs5.existsSync(qaGuidePath)) {
1551
+ const qaGuide = fs5.readFileSync(qaGuidePath, "utf-8").trim();
1552
+ assembled = assembled + "\n\n" + qaGuide;
1553
+ }
1526
1554
  }
1527
1555
  return assembled;
1528
1556
  }
@@ -1685,7 +1713,8 @@ async function executeAgentStage(ctx, def) {
1685
1713
  if (sessionInfo) {
1686
1714
  logger.info(` session: ${SESSION_GROUP[def.name]} (${sessionInfo.resumeSession ? "resume" : "new"})`);
1687
1715
  }
1688
- const mcpConfigJson = isMcpEnabledForStage(def.name, config.mcp) ? buildMcpConfigJson(config.mcp) : void 0;
1716
+ const mcpForStage = isMcpEnabledForStage(def.name, config.mcp) ? withPlaywrightIfNeeded(config.mcp, taskHasUI(ctx.taskDir)) : void 0;
1717
+ const mcpConfigJson = buildMcpConfigJson(mcpForStage);
1689
1718
  if (mcpConfigJson) {
1690
1719
  logger.info(` MCP servers enabled for ${def.name}`);
1691
1720
  }
@@ -2670,6 +2699,7 @@ function applyPreStageLabel(ctx, def) {
2670
2699
  if (!ctx.input.issueNumber || ctx.input.local) return;
2671
2700
  if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
2672
2701
  if (def.name === "review") setLifecycleLabel(ctx.input.issueNumber, "review");
2702
+ if (def.name === "ship") setLifecycleLabel(ctx.input.issueNumber, "shipping");
2673
2703
  }
2674
2704
  function checkQuestionsAfterStage(ctx, def, state) {
2675
2705
  if (def.name !== "taskify" && def.name !== "plan") return null;
@@ -4507,12 +4537,7 @@ function detectMcpConfig(cwd, pm, pkg) {
4507
4537
  const defaultPort = isNext ? 3e3 : isVite ? 5173 : 3e3;
4508
4538
  const mcp = {
4509
4539
  enabled: true,
4510
- servers: {
4511
- playwright: {
4512
- command: "npx",
4513
- args: ["@playwright/mcp@latest"]
4514
- }
4515
- },
4540
+ servers: {},
4516
4541
  stages: ["build", "review"]
4517
4542
  };
4518
4543
  if (hasDevScript) {
@@ -5009,6 +5034,23 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
5009
5034
  }
5010
5035
  }
5011
5036
  console.log(` \u2713 Generated ${stepCount} step files in .kody/steps/`);
5037
+ console.log("\n\u2500\u2500 QA Guide \u2500\u2500");
5038
+ const qaGuidePath = path21.join(cwd, ".kody", "qa-guide.md");
5039
+ if (!fs22.existsSync(qaGuidePath) || opts.force) {
5040
+ const discovery = discoverQaContext(cwd);
5041
+ if (discovery.routes.length > 0) {
5042
+ const qaGuide = generateQaGuide(discovery);
5043
+ fs22.writeFileSync(qaGuidePath, qaGuide);
5044
+ console.log(` \u2713 .kody/qa-guide.md (${discovery.routes.length} routes, ${discovery.roles.length} roles)`);
5045
+ if (discovery.loginPage) console.log(` \u2713 Login page detected: ${discovery.loginPage}`);
5046
+ if (discovery.adminPath) console.log(` \u2713 Admin panel detected: ${discovery.adminPath}`);
5047
+ console.log(" \u2139 Add QA_ADMIN_EMAIL, QA_ADMIN_PASSWORD, QA_USER_EMAIL, QA_USER_PASSWORD as GitHub secrets");
5048
+ } else {
5049
+ console.log(" \u25CB No routes detected \u2014 skipping QA guide");
5050
+ }
5051
+ } else {
5052
+ console.log(" \u25CB .kody/qa-guide.md already exists (use --force to regenerate)");
5053
+ }
5012
5054
  console.log("\n\u2500\u2500 Labels \u2500\u2500");
5013
5055
  try {
5014
5056
  let repoSlug = "";
@@ -5027,6 +5069,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
5027
5069
  { name: "kody:planning", color: "c5def5", description: "Kody is analyzing and planning" },
5028
5070
  { name: "kody:building", color: "0e8a16", description: "Kody is building code" },
5029
5071
  { name: "kody:review", color: "fbca04", description: "Kody is reviewing code" },
5072
+ { name: "kody:shipping", color: "1d76db", description: "Kody is creating the pull request" },
5030
5073
  { name: "kody:done", color: "0e8a16", description: "Kody completed successfully" },
5031
5074
  { name: "kody:failed", color: "d93f0b", description: "Kody pipeline failed" },
5032
5075
  { name: "kody:waiting", color: "fef2c0", description: "Kody is waiting for answers" },
@@ -5085,6 +5128,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
5085
5128
  const filesToCommit = [
5086
5129
  ".kody/memory/architecture.md",
5087
5130
  ".kody/memory/conventions.md",
5131
+ ".kody/qa-guide.md",
5088
5132
  ...installedSkillPaths
5089
5133
  ].filter((f) => fs22.existsSync(path21.join(cwd, f)));
5090
5134
  if (fs22.existsSync(path21.join(cwd, "skills-lock.json"))) {
@@ -5198,6 +5242,178 @@ Create it manually.`, cwd);
5198
5242
  console.log(" \u2713 Project bootstrap complete!");
5199
5243
  console.log(" Kody now has project-specific memory and customized step files.\n");
5200
5244
  }
5245
+ function discoverQaContext(cwd) {
5246
+ const result = {
5247
+ routes: [],
5248
+ authFiles: [],
5249
+ loginPage: null,
5250
+ adminPath: null,
5251
+ roles: [],
5252
+ devCommand: "",
5253
+ devPort: 3e3
5254
+ };
5255
+ try {
5256
+ const pkg = JSON.parse(fs22.readFileSync(path21.join(cwd, "package.json"), "utf-8"));
5257
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
5258
+ const pm = fs22.existsSync(path21.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs22.existsSync(path21.join(cwd, "yarn.lock")) ? "yarn" : "npm";
5259
+ if (pkg.scripts?.dev) result.devCommand = `${pm} dev`;
5260
+ if (allDeps.next || allDeps.nuxt) result.devPort = 3e3;
5261
+ else if (allDeps.vite) result.devPort = 5173;
5262
+ } catch {
5263
+ }
5264
+ const appDirs = ["src/app", "app"];
5265
+ for (const appDir of appDirs) {
5266
+ const fullAppDir = path21.join(cwd, appDir);
5267
+ if (!fs22.existsSync(fullAppDir)) continue;
5268
+ scanRoutes(fullAppDir, appDir, "", result);
5269
+ break;
5270
+ }
5271
+ const authPatterns = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
5272
+ for (const p of authPatterns) {
5273
+ if (fs22.existsSync(path21.join(cwd, p))) result.authFiles.push(p);
5274
+ }
5275
+ const authConfigGlobs = [
5276
+ "src/app/api/auth",
5277
+ "src/auth",
5278
+ "src/lib/auth",
5279
+ "auth.config.ts",
5280
+ "auth.ts",
5281
+ "src/app/api/oauth"
5282
+ ];
5283
+ for (const g of authConfigGlobs) {
5284
+ if (fs22.existsSync(path21.join(cwd, g))) result.authFiles.push(g);
5285
+ }
5286
+ try {
5287
+ const rolePaths = [
5288
+ "src/types",
5289
+ "src/lib",
5290
+ "src/utils",
5291
+ "src/constants",
5292
+ "src/access",
5293
+ "src/collections"
5294
+ ];
5295
+ for (const rp of rolePaths) {
5296
+ const dir = path21.join(cwd, rp);
5297
+ if (!fs22.existsSync(dir)) continue;
5298
+ const files = fs22.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
5299
+ for (const f of files) {
5300
+ try {
5301
+ const content = fs22.readFileSync(path21.join(dir, f), "utf-8").slice(0, 5e3);
5302
+ const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
5303
+ if (roleMatches) {
5304
+ for (const m of roleMatches) {
5305
+ const val = m.match(/['"](\w+)['"]/);
5306
+ if (val && !result.roles.includes(val[1])) result.roles.push(val[1]);
5307
+ }
5308
+ }
5309
+ const enumMatch = content.match(/(?:enum|type)\s+\w*[Rr]ole\w*\s*[={]([^}]+)/s);
5310
+ if (enumMatch) {
5311
+ const vals = enumMatch[1].match(/['"](\w+)['"]/g);
5312
+ if (vals) {
5313
+ for (const v of vals) {
5314
+ const clean = v.replace(/['"]/g, "");
5315
+ if (!result.roles.includes(clean)) result.roles.push(clean);
5316
+ }
5317
+ }
5318
+ }
5319
+ } catch {
5320
+ }
5321
+ }
5322
+ }
5323
+ } catch {
5324
+ }
5325
+ return result;
5326
+ }
5327
+ function scanRoutes(dir, baseDir, prefix, result) {
5328
+ let entries;
5329
+ try {
5330
+ entries = fs22.readdirSync(dir, { withFileTypes: true });
5331
+ } catch {
5332
+ return;
5333
+ }
5334
+ const hasPage = entries.some((e) => e.isFile() && /^page\.(tsx?|jsx?)$/.test(e.name));
5335
+ if (hasPage) {
5336
+ const routePath = prefix || "/";
5337
+ const group = prefix.startsWith("/admin") ? "admin" : prefix.includes("/login") ? "auth" : prefix.includes("/signup") ? "auth" : prefix.includes("/api") ? "api" : "frontend";
5338
+ result.routes.push({ path: routePath, group });
5339
+ if (prefix.includes("/login")) result.loginPage = routePath;
5340
+ if (prefix.startsWith("/admin") && !result.adminPath) result.adminPath = prefix;
5341
+ }
5342
+ for (const entry of entries) {
5343
+ if (!entry.isDirectory()) continue;
5344
+ if (entry.name === "node_modules" || entry.name === ".next") continue;
5345
+ let segment = entry.name;
5346
+ if (segment.startsWith("(") && segment.endsWith(")")) {
5347
+ scanRoutes(path21.join(dir, entry.name), baseDir, prefix, result);
5348
+ continue;
5349
+ }
5350
+ if (segment.startsWith("[") && segment.endsWith("]")) {
5351
+ segment = `:${segment.slice(1, -1)}`;
5352
+ }
5353
+ if (segment.startsWith("[[") && segment.endsWith("]]")) {
5354
+ segment = `:${segment.slice(2, -2)}?`;
5355
+ }
5356
+ scanRoutes(path21.join(dir, entry.name), baseDir, `${prefix}/${segment}`, result);
5357
+ }
5358
+ }
5359
+ function generateQaGuide(discovery) {
5360
+ const lines = ["# QA Guide", "", "## Authentication", ""];
5361
+ if (discovery.loginPage) {
5362
+ lines.push(`- Login page: \`${discovery.loginPage}\``);
5363
+ }
5364
+ lines.push(
5365
+ "",
5366
+ "### Test Accounts",
5367
+ "<!-- Fill in your test/preview environment credentials below -->",
5368
+ "| Role | Email | Password |",
5369
+ "|------|-------|----------|",
5370
+ "| Admin | admin@example.com | CHANGE_ME |",
5371
+ "| User | user@example.com | CHANGE_ME |",
5372
+ "",
5373
+ "### Login Steps",
5374
+ `1. Navigate to \`${discovery.loginPage ?? "/login"}\``,
5375
+ "2. Enter credentials from the test accounts table above",
5376
+ "3. Submit the login form",
5377
+ "4. Verify redirect to dashboard or home page"
5378
+ );
5379
+ if (discovery.authFiles.length > 0) {
5380
+ lines.push("", "### Auth Files");
5381
+ for (const f of discovery.authFiles) {
5382
+ lines.push(`- \`${f}\``);
5383
+ }
5384
+ }
5385
+ if (discovery.roles.length > 0) {
5386
+ lines.push("", "## Roles", "");
5387
+ for (const role of discovery.roles) {
5388
+ lines.push(`- \`${role}\``);
5389
+ }
5390
+ }
5391
+ lines.push("", "## Key Pages", "");
5392
+ const groups = {};
5393
+ for (const route of discovery.routes) {
5394
+ if (!groups[route.group]) groups[route.group] = [];
5395
+ groups[route.group].push(route.path);
5396
+ }
5397
+ for (const [group, routes] of Object.entries(groups)) {
5398
+ lines.push(`### ${group.charAt(0).toUpperCase() + group.slice(1)}`);
5399
+ const sorted = routes.sort();
5400
+ for (const r of sorted.slice(0, 20)) {
5401
+ lines.push(`- \`${r}\``);
5402
+ }
5403
+ if (sorted.length > 20) {
5404
+ lines.push(`- ... and ${sorted.length - 20} more`);
5405
+ }
5406
+ lines.push("");
5407
+ }
5408
+ lines.push(
5409
+ "## Dev Server",
5410
+ "",
5411
+ `- Command: \`${discovery.devCommand || "pnpm dev"}\``,
5412
+ `- URL: \`http://localhost:${discovery.devPort}\``,
5413
+ ""
5414
+ );
5415
+ return lines.join("\n");
5416
+ }
5201
5417
  function detectArchitectureBasic(cwd) {
5202
5418
  const detected = [];
5203
5419
  const pkgPath = path21.join(cwd, "package.json");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine-lite",
3
- "version": "0.1.100",
3
+ "version": "0.1.102",
4
4
  "description": "Autonomous SDLC pipeline: Kody orchestration + Claude Code + LiteLLM",
5
5
  "license": "MIT",
6
6
  "type": "module",