@kody-ade/kody-engine-lite 0.1.108 → 0.1.110

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
@@ -64,212 +64,260 @@ var init_architecture_detection = __esm({
64
64
  }
65
65
  });
66
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}`;
67
+ // src/logger.ts
68
+ function getLevel() {
69
+ const env = process.env.LOG_LEVEL;
70
+ return LEVELS[env ?? "info"] ?? LEVELS.info;
85
71
  }
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
- };
72
+ function timestamp() {
73
+ return (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
74
+ }
75
+ function log(level, msg) {
76
+ if (LEVELS[level] < getLevel()) return;
77
+ const prefix = `[${timestamp()}] ${level.toUpperCase().padEnd(5)}`;
78
+ if (level === "error") {
79
+ console.error(`${prefix} ${msg}`);
80
+ } else if (level === "warn") {
81
+ console.warn(`${prefix} ${msg}`);
82
+ } else {
83
+ console.log(`${prefix} ${msg}`);
103
84
  }
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"
85
+ }
86
+ function ciGroup(title) {
87
+ if (isCI) process.stdout.write(`::group::${title}
88
+ `);
89
+ }
90
+ function ciGroupEnd() {
91
+ if (isCI) process.stdout.write(`::endgroup::
92
+ `);
93
+ }
94
+ var isCI, LEVELS, logger;
95
+ var init_logger = __esm({
96
+ "src/logger.ts"() {
97
+ "use strict";
98
+ isCI = !!process.env.GITHUB_ACTIONS;
99
+ LEVELS = {
100
+ debug: 0,
101
+ info: 1,
102
+ warn: 2,
103
+ error: 3
104
+ };
105
+ logger = {
106
+ debug: (msg) => log("debug", msg),
107
+ info: (msg) => log("info", msg),
108
+ warn: (msg) => log("warn", msg),
109
+ error: (msg) => log("error", msg)
121
110
  };
122
111
  }
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];
112
+ });
113
+
114
+ // src/validators.ts
115
+ function stripFences(content) {
116
+ return content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
117
+ }
118
+ function parseJsonSafe(raw, requiredFields) {
119
+ let parsed;
120
+ try {
121
+ parsed = JSON.parse(raw);
122
+ } catch (err) {
123
+ return { ok: false, error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}` };
152
124
  }
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
- }
125
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
126
+ return { ok: false, error: `Expected JSON object, got ${Array.isArray(parsed) ? "array" : typeof parsed}` };
162
127
  }
163
- if (mode === "fix") {
164
- if (bodyAfterCommand) {
165
- feedback = bodyAfterCommand;
128
+ if (requiredFields) {
129
+ for (const field of requiredFields) {
130
+ if (!(field in parsed)) {
131
+ return { ok: false, error: `Missing required field: ${field}` };
132
+ }
166
133
  }
167
134
  }
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];
135
+ return { ok: true, data: parsed };
136
+ }
137
+ function validateTaskJson(content) {
138
+ try {
139
+ const parsed = JSON.parse(stripFences(content));
140
+ for (const field of REQUIRED_TASK_FIELDS) {
141
+ if (!(field in parsed)) {
142
+ return { valid: false, error: `Missing field: ${field}` };
174
143
  }
175
144
  }
145
+ return { valid: true };
146
+ } catch (err) {
147
+ return {
148
+ valid: false,
149
+ error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`
150
+ };
176
151
  }
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()}`;
152
+ }
153
+ function validatePlanMd(content) {
154
+ if (content.length < 10) {
155
+ return { valid: false, error: "Plan is too short (< 10 chars)" };
183
156
  }
184
- if (!taskId && mode === "full") {
185
- taskId = `${issueNumber}-${generateTimestamp()}`;
157
+ if (!/^##\s+\w+/m.test(content)) {
158
+ return { valid: false, error: "Plan has no markdown h2 sections" };
186
159
  }
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
- };
160
+ return { valid: true };
202
161
  }
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}`);
162
+ function validateReviewMd(content) {
163
+ if (/pass/i.test(content) || /fail/i.test(content)) {
164
+ return { valid: true };
219
165
  }
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);
166
+ return { valid: false, error: "Review must contain 'pass' or 'fail'" };
235
167
  }
236
- var VALID_MODES;
237
- var init_parse_inputs = __esm({
238
- "src/ci/parse-inputs.ts"() {
168
+ var REQUIRED_TASK_FIELDS;
169
+ var init_validators = __esm({
170
+ "src/validators.ts"() {
239
171
  "use strict";
240
- VALID_MODES = [
241
- "full",
242
- "rerun",
243
- "fix",
244
- "fix-ci",
245
- "status",
246
- "approve",
247
- "review",
248
- "resolve",
249
- "bootstrap"
172
+ REQUIRED_TASK_FIELDS = [
173
+ "task_type",
174
+ "title",
175
+ "description",
176
+ "scope",
177
+ "risk_level"
250
178
  ];
251
179
  }
252
180
  });
253
181
 
182
+ // src/config.ts
183
+ import * as fs8 from "fs";
184
+ import * as path7 from "path";
185
+ function resolveStageConfig(config, stageName, modelTier) {
186
+ const stageOverride = config.agent.stages?.[stageName];
187
+ if (stageOverride) return stageOverride;
188
+ if (config.agent.default) return config.agent.default;
189
+ const model = config.agent.modelMap[modelTier];
190
+ if (!model) {
191
+ throw new Error(`No model configured for stage '${stageName}' (tier: ${modelTier}). Set agent.stages.${stageName} or agent.default in kody.config.json`);
192
+ }
193
+ return {
194
+ provider: config.agent.provider ?? "claude",
195
+ model
196
+ };
197
+ }
198
+ function needsLitellmProxy(config) {
199
+ return !!(config.agent.provider && config.agent.provider !== "anthropic");
200
+ }
201
+ function stageNeedsProxy(stageConfig) {
202
+ return stageConfig.provider !== "claude" && stageConfig.provider !== "anthropic";
203
+ }
204
+ function anyStageNeedsProxy(config) {
205
+ if (config.agent.stages) {
206
+ for (const sc of Object.values(config.agent.stages)) {
207
+ if (stageNeedsProxy(sc)) return true;
208
+ }
209
+ }
210
+ if (config.agent.default && stageNeedsProxy(config.agent.default)) return true;
211
+ return needsLitellmProxy(config);
212
+ }
213
+ function getLitellmUrl() {
214
+ return LITELLM_DEFAULT_URL;
215
+ }
216
+ function providerApiKeyEnvVar(provider) {
217
+ if (provider === "anthropic") return "ANTHROPIC_API_KEY";
218
+ return "ANTHROPIC_COMPATIBLE_API_KEY";
219
+ }
220
+ function setConfigDir(dir) {
221
+ _configDir = dir;
222
+ _config = null;
223
+ }
224
+ function getProjectConfig() {
225
+ if (_config) return _config;
226
+ const configPath = path7.join(_configDir ?? process.cwd(), "kody.config.json");
227
+ if (fs8.existsSync(configPath)) {
228
+ try {
229
+ const result = parseJsonSafe(fs8.readFileSync(configPath, "utf-8"));
230
+ if (!result.ok) {
231
+ logger.warn(`kody.config.json: ${result.error} \u2014 using defaults`);
232
+ _config = { ...DEFAULT_CONFIG };
233
+ return _config;
234
+ }
235
+ const raw = result.data;
236
+ _config = {
237
+ quality: { ...DEFAULT_CONFIG.quality, ...raw.quality },
238
+ git: { ...DEFAULT_CONFIG.git, ...raw.git },
239
+ github: { ...DEFAULT_CONFIG.github, ...raw.github },
240
+ agent: {
241
+ ...DEFAULT_CONFIG.agent,
242
+ ...raw.agent,
243
+ modelMap: { ...DEFAULT_CONFIG.agent.modelMap, ...raw.agent?.modelMap }
244
+ },
245
+ timeouts: raw.timeouts ?? void 0,
246
+ contextTiers: raw.contextTiers ? { ...DEFAULT_CONFIG.contextTiers, ...raw.contextTiers } : DEFAULT_CONFIG.contextTiers,
247
+ mcp: raw.mcp ? {
248
+ servers: {},
249
+ stages: ["build", "verify", "review", "review-fix"],
250
+ ...raw.mcp,
251
+ // Auto-enable when devServer is configured (user can still set enabled: false to override)
252
+ enabled: raw.mcp.enabled ?? !!raw.mcp.devServer
253
+ } : void 0
254
+ };
255
+ } catch {
256
+ logger.warn("kody.config.json is invalid JSON \u2014 using defaults");
257
+ _config = { ...DEFAULT_CONFIG };
258
+ }
259
+ } else {
260
+ _config = { ...DEFAULT_CONFIG };
261
+ }
262
+ return _config;
263
+ }
264
+ var DEFAULT_CONFIG, LITELLM_DEFAULT_PORT, LITELLM_DEFAULT_URL, VERIFY_COMMAND_TIMEOUT_MS, FIX_COMMAND_TIMEOUT_MS, _config, _configDir;
265
+ var init_config = __esm({
266
+ "src/config.ts"() {
267
+ "use strict";
268
+ init_logger();
269
+ init_validators();
270
+ DEFAULT_CONFIG = {
271
+ quality: {
272
+ typecheck: "pnpm -s tsc --noEmit",
273
+ lint: "pnpm -s lint",
274
+ lintFix: "pnpm lint:fix",
275
+ formatFix: "pnpm format:fix",
276
+ testUnit: "pnpm -s test"
277
+ },
278
+ git: {
279
+ defaultBranch: "dev"
280
+ },
281
+ github: {
282
+ owner: "",
283
+ repo: ""
284
+ },
285
+ agent: {
286
+ modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
287
+ },
288
+ contextTiers: {
289
+ enabled: true,
290
+ tokenBudget: 8e3
291
+ }
292
+ };
293
+ LITELLM_DEFAULT_PORT = 4e3;
294
+ LITELLM_DEFAULT_URL = `http://localhost:${LITELLM_DEFAULT_PORT}`;
295
+ VERIFY_COMMAND_TIMEOUT_MS = 5 * 60 * 1e3;
296
+ FIX_COMMAND_TIMEOUT_MS = 2 * 60 * 1e3;
297
+ _config = null;
298
+ _configDir = null;
299
+ }
300
+ });
301
+
254
302
  // src/agent-runner.ts
255
303
  import { spawn, execFileSync as execFileSync6 } from "child_process";
256
304
  function writeStdin(child, prompt) {
257
- return new Promise((resolve4, reject) => {
305
+ return new Promise((resolve5, reject) => {
258
306
  if (!child.stdin) {
259
- resolve4();
307
+ resolve5();
260
308
  return;
261
309
  }
262
310
  child.stdin.write(prompt, (err) => {
263
311
  if (err) reject(err);
264
312
  else {
265
313
  child.stdin.end();
266
- resolve4();
314
+ resolve5();
267
315
  }
268
316
  });
269
317
  });
270
318
  }
271
319
  function waitForProcess(child, timeout) {
272
- return new Promise((resolve4) => {
320
+ return new Promise((resolve5) => {
273
321
  const stdoutChunks = [];
274
322
  const stderrChunks = [];
275
323
  child.stdout?.on("data", (chunk) => stdoutChunks.push(chunk));
@@ -282,7 +330,7 @@ function waitForProcess(child, timeout) {
282
330
  }, timeout);
283
331
  child.on("exit", (code) => {
284
332
  clearTimeout(timer);
285
- resolve4({
333
+ resolve5({
286
334
  code,
287
335
  stdout: Buffer.concat(stdoutChunks).toString(),
288
336
  stderr: Buffer.concat(stderrChunks).toString()
@@ -290,7 +338,7 @@ function waitForProcess(child, timeout) {
290
338
  });
291
339
  child.on("error", (err) => {
292
340
  clearTimeout(timer);
293
- resolve4({ code: -1, stdout: "", stderr: err.message });
341
+ resolve5({ code: -1, stdout: "", stderr: err.message });
294
342
  });
295
343
  });
296
344
  }
@@ -339,12 +387,12 @@ function createClaudeCodeRunner() {
339
387
  "--print",
340
388
  "--model",
341
389
  model,
342
- "--dangerously-skip-permissions",
343
- "--allowedTools",
344
- "Bash,Edit,Read,Write,Glob,Grep"
390
+ "--dangerously-skip-permissions"
345
391
  ];
346
392
  if (options?.mcpConfigJson) {
347
393
  args2.push("--mcp-config", options.mcpConfigJson);
394
+ } else {
395
+ args2.push("--allowedTools", "Bash,Edit,Read,Write,Glob,Grep");
348
396
  }
349
397
  if (options?.sessionId) {
350
398
  if (options.resumeSession) {
@@ -386,923 +434,1607 @@ var init_agent_runner = __esm({
386
434
  }
387
435
  });
388
436
 
389
- // src/definitions.ts
390
- var definitions_exports = {};
391
- __export(definitions_exports, {
392
- STAGES: () => STAGES,
393
- applyTimeoutOverrides: () => applyTimeoutOverrides,
394
- getStage: () => getStage
395
- });
396
- function getStage(name) {
397
- return STAGES.find((s) => s.name === name);
437
+ // src/mcp-config.ts
438
+ function withPlaywrightIfNeeded(mcpConfig, hasUI) {
439
+ if (!mcpConfig?.enabled || !hasUI) return mcpConfig;
440
+ const hasPlaywright = Object.keys(mcpConfig.servers).some(
441
+ (name) => name.toLowerCase().includes("playwright")
442
+ );
443
+ if (hasPlaywright) return mcpConfig;
444
+ return {
445
+ ...mcpConfig,
446
+ servers: {
447
+ ...mcpConfig.servers,
448
+ playwright: PLAYWRIGHT_SERVER
449
+ }
450
+ };
398
451
  }
399
- function applyTimeoutOverrides(overrides) {
400
- for (const stage of STAGES) {
401
- if (overrides[stage.name] != null) {
402
- stage.timeout = overrides[stage.name] * 1e3;
452
+ function buildMcpConfigJson(mcpConfig) {
453
+ if (!mcpConfig?.enabled) return void 0;
454
+ if (Object.keys(mcpConfig.servers).length === 0) return void 0;
455
+ const config = { mcpServers: {} };
456
+ const mcpServers = config.mcpServers;
457
+ for (const [name, server] of Object.entries(mcpConfig.servers)) {
458
+ mcpServers[name] = {
459
+ command: server.command,
460
+ args: server.args ?? [],
461
+ ...server.env ? { env: server.env } : {}
462
+ };
463
+ }
464
+ return JSON.stringify(config);
465
+ }
466
+ function resolveMcpEnvVars(servers) {
467
+ const resolved = {};
468
+ for (const [name, server] of Object.entries(servers)) {
469
+ const env = {};
470
+ for (const [k, v] of Object.entries(server.env ?? {})) {
471
+ env[k] = v.replace(/\$\{(\w+)\}/g, (_, varName) => {
472
+ const val = process.env[varName];
473
+ if (!val) {
474
+ throw new Error(
475
+ `MCP env var \${${varName}} is not set (required by MCP server '${name}'). Add it as a GitHub secret and it will be forwarded automatically.`
476
+ );
477
+ }
478
+ return val;
479
+ });
403
480
  }
481
+ resolved[name] = { ...server, ...Object.keys(env).length > 0 ? { env } : {} };
404
482
  }
483
+ return resolved;
405
484
  }
406
- var STAGES;
407
- var init_definitions = __esm({
408
- "src/definitions.ts"() {
485
+ function buildTaskifyMcpConfigJson(config) {
486
+ const servers = config.mcp?.servers ?? {};
487
+ if (Object.keys(servers).length === 0) {
488
+ throw new Error(
489
+ "kody taskify requires at least one MCP server configured in kody.config.json under mcp.servers. Add your task management tool's MCP server there."
490
+ );
491
+ }
492
+ const resolvedServers = resolveMcpEnvVars(servers);
493
+ const mcpServers = {};
494
+ for (const [name, server] of Object.entries(resolvedServers)) {
495
+ mcpServers[name] = {
496
+ command: server.command,
497
+ args: server.args ?? [],
498
+ ...server.env ? { env: server.env } : {}
499
+ };
500
+ }
501
+ return JSON.stringify({ mcpServers });
502
+ }
503
+ function isMcpEnabledForStage(stageName, mcpConfig) {
504
+ if (!mcpConfig?.enabled) return false;
505
+ const allowedStages = mcpConfig.stages ?? DEFAULT_MCP_STAGES;
506
+ return allowedStages.includes(stageName);
507
+ }
508
+ var DEFAULT_MCP_STAGES, PLAYWRIGHT_SERVER;
509
+ var init_mcp_config = __esm({
510
+ "src/mcp-config.ts"() {
409
511
  "use strict";
410
- STAGES = [
411
- {
412
- name: "taskify",
413
- type: "agent",
414
- modelTier: "cheap",
415
- timeout: 6e5,
416
- maxRetries: 1,
417
- outputFile: "task.json"
418
- },
419
- {
420
- name: "plan",
421
- type: "agent",
422
- modelTier: "strong",
423
- timeout: 6e5,
424
- maxRetries: 1,
425
- outputFile: "plan.md"
426
- },
427
- {
428
- name: "build",
429
- type: "agent",
430
- modelTier: "mid",
431
- timeout: 24e5,
432
- maxRetries: 1
433
- },
434
- {
435
- name: "verify",
436
- type: "gate",
437
- modelTier: "cheap",
438
- timeout: 3e5,
439
- maxRetries: 2,
440
- retryWithAgent: "autofix"
441
- },
442
- {
443
- name: "review",
444
- type: "agent",
445
- modelTier: "strong",
446
- timeout: 6e5,
447
- maxRetries: 1,
448
- outputFile: "review.md"
449
- },
450
- {
451
- name: "review-fix",
452
- type: "agent",
453
- modelTier: "mid",
454
- timeout: 12e5,
455
- maxRetries: 1
456
- },
457
- {
458
- name: "ship",
459
- type: "deterministic",
460
- modelTier: "cheap",
461
- timeout: 24e4,
462
- maxRetries: 1,
463
- outputFile: "ship.md"
464
- }
465
- ];
512
+ DEFAULT_MCP_STAGES = ["build", "verify", "review", "review-fix"];
513
+ PLAYWRIGHT_SERVER = {
514
+ command: "npx",
515
+ args: ["-y", "@anthropic-ai/mcp-playwright"]
516
+ };
466
517
  }
467
518
  });
468
519
 
469
- // src/logger.ts
470
- function getLevel() {
471
- const env = process.env.LOG_LEVEL;
472
- return LEVELS[env ?? "info"] ?? LEVELS.info;
473
- }
474
- function timestamp() {
475
- return (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
520
+ // src/github-api.ts
521
+ import { execFileSync as execFileSync7 } from "child_process";
522
+ function isGhExecError(err) {
523
+ return typeof err === "object" && err !== null;
476
524
  }
477
- function log(level, msg) {
478
- if (LEVELS[level] < getLevel()) return;
479
- const prefix = `[${timestamp()}] ${level.toUpperCase().padEnd(5)}`;
480
- if (level === "error") {
481
- console.error(`${prefix} ${msg}`);
482
- } else if (level === "warn") {
483
- console.warn(`${prefix} ${msg}`);
484
- } else {
485
- console.log(`${prefix} ${msg}`);
525
+ function ghErrorMessage(err) {
526
+ if (isGhExecError(err)) {
527
+ const stderr = err.stderr?.toString().trim();
528
+ if (stderr) return stderr;
486
529
  }
530
+ return err instanceof Error ? err.message : String(err);
487
531
  }
488
- function ciGroup(title) {
489
- if (isCI) process.stdout.write(`::group::${title}
490
- `);
532
+ function isNotFoundError(err) {
533
+ const msg = ghErrorMessage(err).toLowerCase();
534
+ return msg.includes("not found") || msg.includes("no pull requests") || msg.includes("could not resolve");
491
535
  }
492
- function ciGroupEnd() {
493
- if (isCI) process.stdout.write(`::endgroup::
494
- `);
536
+ function setGhCwd(cwd) {
537
+ _ghCwd = cwd;
495
538
  }
496
- var isCI, LEVELS, logger;
497
- var init_logger = __esm({
498
- "src/logger.ts"() {
499
- "use strict";
500
- isCI = !!process.env.GITHUB_ACTIONS;
501
- LEVELS = {
502
- debug: 0,
503
- info: 1,
504
- warn: 2,
505
- error: 3
506
- };
507
- logger = {
508
- debug: (msg) => log("debug", msg),
509
- info: (msg) => log("info", msg),
510
- warn: (msg) => log("warn", msg),
511
- error: (msg) => log("error", msg)
512
- };
513
- }
514
- });
515
-
516
- // src/validators.ts
517
- function stripFences(content) {
518
- return content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
539
+ function ghToken() {
540
+ return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
519
541
  }
520
- function parseJsonSafe(raw, requiredFields) {
521
- let parsed;
542
+ function gh(args2, options) {
543
+ const token = ghToken();
544
+ const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
545
+ return execFileSync7("gh", args2, {
546
+ encoding: "utf-8",
547
+ timeout: API_TIMEOUT_MS,
548
+ cwd: _ghCwd,
549
+ env,
550
+ input: options?.input,
551
+ stdio: options?.input ? ["pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
552
+ }).trim();
553
+ }
554
+ function getIssue(issueNumber) {
522
555
  try {
523
- parsed = JSON.parse(raw);
556
+ const output = gh([
557
+ "issue",
558
+ "view",
559
+ String(issueNumber),
560
+ "--json",
561
+ "body,title"
562
+ ]);
563
+ const parsed = JSON.parse(output);
564
+ if (!parsed || typeof parsed.title !== "string") {
565
+ logger.warn(` Issue #${issueNumber}: unexpected response shape`);
566
+ return null;
567
+ }
568
+ return { body: parsed.body ?? "", title: parsed.title };
524
569
  } 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
- }
570
+ if (isNotFoundError(err)) {
571
+ logger.info(` Issue #${issueNumber} not found`);
572
+ } else {
573
+ logger.error(` Failed to get issue #${issueNumber}: ${ghErrorMessage(err)}`);
535
574
  }
575
+ return null;
536
576
  }
537
- return { ok: true, data: parsed };
538
577
  }
539
- function validateTaskJson(content) {
578
+ function getIssueComments(issueNumber) {
540
579
  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
- };
580
+ const output = gh([
581
+ "api",
582
+ `repos/{owner}/{repo}/issues/${issueNumber}/comments`,
583
+ "--jq",
584
+ "[.[] | {body, created_at}]"
585
+ ]);
586
+ return output ? JSON.parse(output) : [];
587
+ } catch {
588
+ return [];
553
589
  }
554
590
  }
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" };
591
+ function getIssueLabels(issueNumber) {
592
+ try {
593
+ const output = gh(["issue", "view", String(issueNumber), "--json", "labels", "--jq", ".labels[].name"]);
594
+ return output.split("\n").filter(Boolean);
595
+ } catch {
596
+ return [];
561
597
  }
562
- return { valid: true };
563
598
  }
564
- function validateReviewMd(content) {
565
- if (/pass/i.test(content) || /fail/i.test(content)) {
566
- return { valid: true };
599
+ function setLabel(issueNumber, label) {
600
+ try {
601
+ gh(["issue", "edit", String(issueNumber), "--add-label", label]);
602
+ logger.info(` Label added: ${label}`);
603
+ } catch (err) {
604
+ logger.warn(` Failed to set label ${label}: ${err}`);
567
605
  }
568
- return { valid: false, error: "Review must contain 'pass' or 'fail'" };
569
606
  }
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
- ];
607
+ function postComment(issueNumber, body) {
608
+ try {
609
+ gh(
610
+ ["issue", "comment", String(issueNumber), "--body-file", "-"],
611
+ { input: body }
612
+ );
613
+ logger.info(` Comment posted on #${issueNumber}`);
614
+ } catch (err) {
615
+ logger.warn(` Failed to post comment: ${err}`);
581
616
  }
582
- });
583
-
584
- // src/config.ts
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`);
617
+ }
618
+ function getPRForBranch(branch) {
619
+ try {
620
+ const output = gh([
621
+ "pr",
622
+ "view",
623
+ branch,
624
+ "--json",
625
+ "number,url"
626
+ ]);
627
+ const data = JSON.parse(output);
628
+ if (typeof data.number !== "number" || typeof data.url !== "string") {
629
+ logger.warn(` PR for branch ${branch}: unexpected response shape`);
630
+ return null;
631
+ }
632
+ return { number: data.number, url: data.url };
633
+ } catch (err) {
634
+ if (!isNotFoundError(err)) {
635
+ logger.warn(` Failed to check PR for branch ${branch}: ${ghErrorMessage(err)}`);
636
+ }
637
+ return null;
594
638
  }
595
- return {
596
- provider: config.agent.provider ?? "claude",
597
- model
598
- };
599
639
  }
600
- function needsLitellmProxy(config) {
601
- return !!(config.agent.provider && config.agent.provider !== "anthropic");
640
+ function updatePR(prNumber, body) {
641
+ try {
642
+ gh(
643
+ ["pr", "edit", String(prNumber), "--body-file", "-"],
644
+ { input: body }
645
+ );
646
+ logger.info(` PR #${prNumber} body updated`);
647
+ } catch (err) {
648
+ logger.warn(` Failed to update PR #${prNumber}: ${err}`);
649
+ }
602
650
  }
603
- function stageNeedsProxy(stageConfig) {
604
- return stageConfig.provider !== "claude" && stageConfig.provider !== "anthropic";
651
+ function createPR(head, base, title, body) {
652
+ try {
653
+ const output = gh(
654
+ [
655
+ "pr",
656
+ "create",
657
+ "--head",
658
+ head,
659
+ "--base",
660
+ base,
661
+ "--title",
662
+ title,
663
+ "--body-file",
664
+ "-"
665
+ ],
666
+ { input: body }
667
+ );
668
+ const url = output.trim();
669
+ const match = url.match(/\/pull\/(\d+)$/);
670
+ const number = match ? parseInt(match[1], 10) : 0;
671
+ logger.info(` PR created: ${url}`);
672
+ return { number, url };
673
+ } catch (err) {
674
+ const reason = ghErrorMessage(err);
675
+ logger.error(` Failed to create PR: ${reason}`);
676
+ return null;
677
+ }
605
678
  }
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;
679
+ function createIssue(title, body, labels) {
680
+ try {
681
+ const args2 = ["issue", "create", "--title", title, "--body-file", "-"];
682
+ if (labels && labels.length > 0) {
683
+ args2.push("--label", labels.join(","));
610
684
  }
685
+ const output = gh(args2, { input: body });
686
+ const url = output.trim();
687
+ const match = url.match(/\/issues\/(\d+)$/);
688
+ const number = match ? parseInt(match[1], 10) : 0;
689
+ logger.info(` Issue created: ${url}`);
690
+ return { number, url };
691
+ } catch (err) {
692
+ const reason = ghErrorMessage(err);
693
+ logger.error(` Failed to create issue: ${reason}`);
694
+ return null;
611
695
  }
612
- if (config.agent.default && stageNeedsProxy(config.agent.default)) return true;
613
- return needsLitellmProxy(config);
614
- }
615
- function getLitellmUrl() {
616
- return LITELLM_DEFAULT_URL;
617
696
  }
618
- function providerApiKeyEnvVar(provider) {
619
- if (provider === "anthropic") return "ANTHROPIC_API_KEY";
620
- return "ANTHROPIC_COMPATIBLE_API_KEY";
621
- }
622
- function setConfigDir(dir) {
623
- _configDir = dir;
624
- _config = null;
625
- }
626
- function getProjectConfig() {
627
- if (_config) return _config;
628
- const configPath = path7.join(_configDir ?? process.cwd(), "kody.config.json");
629
- if (fs9.existsSync(configPath)) {
697
+ function setLifecycleLabel(issueNumber, phase) {
698
+ if (!LIFECYCLE_LABELS.includes(phase)) {
699
+ logger.warn(` Invalid lifecycle phase: ${phase}`);
700
+ return;
701
+ }
702
+ const othersToRemove = LIFECYCLE_LABELS.filter((l) => l !== phase).map((l) => `kody:${l}`).join(",");
703
+ if (othersToRemove) {
630
704
  try {
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;
638
- _config = {
639
- quality: { ...DEFAULT_CONFIG.quality, ...raw.quality },
640
- git: { ...DEFAULT_CONFIG.git, ...raw.git },
641
- github: { ...DEFAULT_CONFIG.github, ...raw.github },
642
- agent: {
643
- ...DEFAULT_CONFIG.agent,
644
- ...raw.agent,
645
- modelMap: { ...DEFAULT_CONFIG.agent.modelMap, ...raw.agent?.modelMap }
646
- },
647
- timeouts: raw.timeouts ?? void 0,
648
- contextTiers: raw.contextTiers ? { ...DEFAULT_CONFIG.contextTiers, ...raw.contextTiers } : DEFAULT_CONFIG.contextTiers,
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
656
- };
705
+ gh(["issue", "edit", String(issueNumber), "--remove-label", othersToRemove]);
657
706
  } catch {
658
- logger.warn("kody.config.json is invalid JSON \u2014 using defaults");
659
- _config = { ...DEFAULT_CONFIG };
660
707
  }
661
- } else {
662
- _config = { ...DEFAULT_CONFIG };
663
708
  }
664
- return _config;
709
+ setLabel(issueNumber, `kody:${phase}`);
665
710
  }
666
- var DEFAULT_CONFIG, LITELLM_DEFAULT_PORT, LITELLM_DEFAULT_URL, VERIFY_COMMAND_TIMEOUT_MS, FIX_COMMAND_TIMEOUT_MS, _config, _configDir;
667
- var init_config = __esm({
668
- "src/config.ts"() {
669
- "use strict";
670
- init_logger();
671
- init_validators();
672
- DEFAULT_CONFIG = {
673
- quality: {
674
- typecheck: "pnpm -s tsc --noEmit",
675
- lint: "pnpm -s lint",
676
- lintFix: "pnpm lint:fix",
677
- formatFix: "pnpm format:fix",
678
- testUnit: "pnpm -s test"
679
- },
680
- git: {
681
- defaultBranch: "dev"
682
- },
683
- github: {
684
- owner: "",
685
- repo: ""
686
- },
687
- agent: {
688
- modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
689
- },
690
- contextTiers: {
691
- enabled: true,
692
- tokenBudget: 8e3
711
+ function getPRsForIssue(issueNumber) {
712
+ try {
713
+ const output = gh([
714
+ "pr",
715
+ "list",
716
+ "--search",
717
+ `${issueNumber} in:body`,
718
+ "--json",
719
+ "number,title,url,headRefName",
720
+ "--state",
721
+ "open"
722
+ ]);
723
+ const prs = JSON.parse(output);
724
+ const branchPrs = (() => {
725
+ try {
726
+ const branchOutput = gh([
727
+ "pr",
728
+ "list",
729
+ "--json",
730
+ "number,title,url,headRefName",
731
+ "--state",
732
+ "open"
733
+ ]);
734
+ return JSON.parse(branchOutput).filter((pr) => pr.headRefName.startsWith(`${issueNumber}-`));
735
+ } catch {
736
+ return [];
693
737
  }
694
- };
695
- LITELLM_DEFAULT_PORT = 4e3;
696
- LITELLM_DEFAULT_URL = `http://localhost:${LITELLM_DEFAULT_PORT}`;
697
- VERIFY_COMMAND_TIMEOUT_MS = 5 * 60 * 1e3;
698
- FIX_COMMAND_TIMEOUT_MS = 2 * 60 * 1e3;
699
- _config = null;
700
- _configDir = null;
701
- }
702
- });
703
-
704
- // src/git-utils.ts
705
- import { execFileSync as execFileSync7 } from "child_process";
706
- function getHookSafeEnv() {
707
- if (!_hookSafeEnv) {
708
- _hookSafeEnv = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
738
+ })();
739
+ const seen = /* @__PURE__ */ new Set();
740
+ const merged = [];
741
+ for (const pr of [...prs, ...branchPrs]) {
742
+ if (!seen.has(pr.number)) {
743
+ seen.add(pr.number);
744
+ merged.push({ number: pr.number, title: pr.title, url: pr.url, headBranch: pr.headRefName });
745
+ }
746
+ }
747
+ return merged;
748
+ } catch (err) {
749
+ logger.error(` Failed to get PRs for issue #${issueNumber}: ${err}`);
750
+ return [];
709
751
  }
710
- return _hookSafeEnv;
711
- }
712
- function git(args2, options) {
713
- return execFileSync7("git", args2, {
714
- encoding: "utf-8",
715
- timeout: options?.timeout ?? 3e4,
716
- cwd: options?.cwd,
717
- env: options?.env ?? getHookSafeEnv(),
718
- stdio: ["pipe", "pipe", "pipe"]
719
- }).trim();
720
- }
721
- function deriveBranchName(issueNumber, title) {
722
- const slug = title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").slice(0, 50).replace(/-$/, "");
723
- return `${issueNumber}-${slug}`;
724
752
  }
725
- function getDefaultBranch(cwd) {
753
+ function getPRDetails(prNumber) {
726
754
  try {
727
- const config = getProjectConfig();
728
- if (config.git?.defaultBranch) {
729
- return config.git.defaultBranch;
755
+ const output = gh([
756
+ "pr",
757
+ "view",
758
+ String(prNumber),
759
+ "--json",
760
+ "title,body,headRefName,baseRefName"
761
+ ]);
762
+ const data = JSON.parse(output);
763
+ if (typeof data.title !== "string" || typeof data.headRefName !== "string") {
764
+ logger.warn(` PR #${prNumber}: unexpected response shape`);
765
+ return null;
730
766
  }
731
- } catch {
732
- }
733
- try {
734
- const ref = git(["symbolic-ref", "refs/remotes/origin/HEAD"], { cwd });
735
- return ref.replace("refs/remotes/origin/", "");
736
- } catch {
737
- }
738
- try {
739
- const output = git(["remote", "show", "origin"], { cwd, timeout: 1e4 });
740
- const match = output.match(/HEAD branch:\s*(\S+)/);
741
- if (match) return match[1];
742
- } catch {
743
- }
744
- return "dev";
745
- }
746
- function getCurrentBranch(cwd) {
747
- return git(["branch", "--show-current"], { cwd });
748
- }
749
- function ensureFeatureBranch(issueNumber, title, cwd) {
750
- const current = getCurrentBranch(cwd);
751
- const branchName = deriveBranchName(issueNumber, title);
752
- if (current === branchName || current.startsWith(`${issueNumber}-`)) {
753
- logger.info(` Already on feature branch: ${current}`);
754
- return current;
755
- }
756
- if (!BASE_BRANCHES.includes(current) && current !== "") {
757
- const defaultBranch2 = getDefaultBranch(cwd);
758
- logger.info(` Switching from ${current} to ${defaultBranch2} before creating ${branchName}`);
759
- try {
760
- git(["checkout", defaultBranch2], { cwd });
761
- } catch {
762
- logger.warn(` Failed to checkout ${defaultBranch2}, aborting branch creation`);
763
- return current;
767
+ return {
768
+ title: data.title,
769
+ body: data.body ?? "",
770
+ headBranch: data.headRefName,
771
+ baseBranch: data.baseRefName ?? "main"
772
+ };
773
+ } catch (err) {
774
+ if (isNotFoundError(err)) {
775
+ logger.info(` PR #${prNumber} not found`);
776
+ } else {
777
+ logger.error(` Failed to get PR #${prNumber}: ${ghErrorMessage(err)}`);
764
778
  }
779
+ return null;
765
780
  }
781
+ }
782
+ function postPRComment(prNumber, body) {
766
783
  try {
767
- git(["fetch", "origin"], { cwd, timeout: 3e4 });
784
+ gh(
785
+ ["pr", "comment", String(prNumber), "--body-file", "-"],
786
+ { input: body }
787
+ );
788
+ logger.info(` Comment posted on PR #${prNumber}`);
768
789
  } catch (err) {
769
- const msg = err instanceof Error ? err.message : String(err);
770
- logger.warn(` Failed to fetch origin: ${msg}`);
790
+ logger.warn(` Failed to post PR comment: ${err}`);
771
791
  }
792
+ }
793
+ function submitPRReview(prNumber, body, event) {
794
+ const flag = event === "approve" ? "--approve" : "--request-changes";
772
795
  try {
773
- git(["rev-parse", "--verify", `origin/${branchName}`], { cwd });
774
- git(["checkout", branchName], { cwd });
775
- git(["pull", "origin", branchName], { cwd, timeout: 3e4 });
776
- logger.info(` Checked out existing remote branch: ${branchName}`);
777
- return branchName;
778
- } catch {
796
+ gh(
797
+ ["pr", "review", String(prNumber), flag, "--body-file", "-"],
798
+ { input: body }
799
+ );
800
+ logger.info(` PR review submitted on #${prNumber}: ${event}`);
801
+ return true;
802
+ } catch (err) {
803
+ logger.warn(` Failed to submit PR review: ${err}`);
804
+ return false;
779
805
  }
806
+ }
807
+ function getCIFailureLogs(runId, maxLength = 8e3) {
780
808
  try {
781
- git(["rev-parse", "--verify", branchName], { cwd });
782
- git(["checkout", branchName], { cwd });
783
- logger.info(` Checked out existing local branch: ${branchName}`);
784
- return branchName;
785
- } catch {
809
+ const logsOutput = gh([
810
+ "run",
811
+ "view",
812
+ String(runId),
813
+ "--log-failed"
814
+ ]);
815
+ if (!logsOutput) return null;
816
+ const truncated = logsOutput.slice(-maxLength);
817
+ const prefix = logsOutput.length > maxLength ? "...(earlier output truncated)\n" : "";
818
+ return `${prefix}${truncated}`;
819
+ } catch (err) {
820
+ logger.warn(` Failed to get CI failure logs for run ${runId}: ${ghErrorMessage(err)}`);
821
+ return null;
786
822
  }
787
- const defaultBranch = getDefaultBranch(cwd);
823
+ }
824
+ function getLatestFailedRunForBranch(branch) {
788
825
  try {
789
- git(["checkout", "-b", branchName, `origin/${defaultBranch}`], { cwd });
790
- } catch {
791
- git(["checkout", "-b", branchName], { cwd });
792
- }
793
- logger.info(` Created new branch: ${branchName}`);
794
- return branchName;
795
- }
796
- function syncWithDefault(cwd, branch) {
797
- const defaultBranch = branch ?? getDefaultBranch(cwd);
798
- const current = getCurrentBranch(cwd);
799
- if (current === defaultBranch) return;
800
- try {
801
- git(["fetch", "origin", defaultBranch], { cwd, timeout: 3e4 });
826
+ const output = gh([
827
+ "run",
828
+ "list",
829
+ "--branch",
830
+ branch,
831
+ "--status",
832
+ "failure",
833
+ "--limit",
834
+ "1",
835
+ "--json",
836
+ "databaseId",
837
+ "--jq",
838
+ ".[0].databaseId"
839
+ ]);
840
+ return output.trim() || null;
802
841
  } catch (err) {
803
- const msg = err instanceof Error ? err.message : String(err);
804
- logger.warn(` Failed to fetch latest from origin: ${msg}`);
805
- return;
806
- }
807
- try {
808
- git(["merge", `origin/${defaultBranch}`, "--no-edit"], { cwd, timeout: 3e4 });
809
- logger.info(` Synced with origin/${defaultBranch}`);
810
- } catch {
811
- try {
812
- git(["merge", "--abort"], { cwd });
813
- } catch (abortErr) {
814
- logger.warn(` Failed to abort merge: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}`);
815
- }
816
- logger.warn(` Merge conflict with origin/${defaultBranch} \u2014 skipping sync`);
842
+ logger.warn(` Failed to get latest failed run for branch ${branch}: ${ghErrorMessage(err)}`);
843
+ return null;
817
844
  }
818
845
  }
819
- function mergeDefault(cwd) {
820
- const defaultBranch = getDefaultBranch(cwd);
821
- const current = getCurrentBranch(cwd);
822
- if (current === defaultBranch) return "clean";
846
+ function getLatestKodyReviewComment(prNumber) {
823
847
  try {
824
- git(["fetch", "origin", defaultBranch], { cwd, timeout: 3e4 });
848
+ const output = gh([
849
+ "api",
850
+ `repos/{owner}/{repo}/issues/${prNumber}/comments`,
851
+ "--jq",
852
+ '[.[] | select(.body | test("Kody Review"))] | last | .body'
853
+ ]);
854
+ return output.trim() || null;
825
855
  } catch (err) {
826
- const msg = err instanceof Error ? err.message : String(err);
827
- logger.warn(` Failed to fetch latest from origin: ${msg}`);
828
- return "error";
856
+ logger.warn(` Failed to get review comments for PR #${prNumber}: ${err}`);
857
+ return null;
829
858
  }
859
+ }
860
+ function getPRFeedbackSinceLastKodyAction(prNumber) {
830
861
  try {
831
- git(["merge", `origin/${defaultBranch}`, "--no-edit"], { cwd, timeout: 3e4 });
832
- logger.info(` Merged origin/${defaultBranch} cleanly`);
833
- return "clean";
834
- } catch {
835
- try {
836
- const unmerged = git(["diff", "--name-only", "--diff-filter=U"], { cwd });
837
- if (unmerged.trim()) return "conflict";
838
- } catch {
862
+ const issueCommentsRaw = gh([
863
+ "api",
864
+ `repos/{owner}/{repo}/issues/${prNumber}/comments`,
865
+ "--jq",
866
+ "[.[] | {body, created_at, user_login: .user.login, user_type: .user.type}]"
867
+ ]);
868
+ const issueComments = issueCommentsRaw ? JSON.parse(issueCommentsRaw) : [];
869
+ const reviewCommentsRaw = gh([
870
+ "api",
871
+ `repos/{owner}/{repo}/pulls/${prNumber}/comments`,
872
+ "--jq",
873
+ "[.[] | {body, created_at, user_login: .user.login, user_type: .user.type, path, line}]"
874
+ ]);
875
+ const reviewComments = reviewCommentsRaw ? JSON.parse(reviewCommentsRaw) : [];
876
+ const kodyTimestamp = findLastKodyActionTimestamp(issueComments);
877
+ const humanIssueComments = issueComments.filter(
878
+ (c) => !isKodyComment(c) && (!kodyTimestamp || c.created_at > kodyTimestamp)
879
+ );
880
+ const humanReviewComments = reviewComments.filter(
881
+ (c) => !isKodyComment(c) && (!kodyTimestamp || c.created_at > kodyTimestamp)
882
+ );
883
+ if (humanIssueComments.length === 0 && humanReviewComments.length === 0) {
884
+ return null;
839
885
  }
840
- try {
841
- git(["merge", "--abort"], { cwd });
842
- } catch (abortErr) {
843
- logger.warn(` Failed to abort merge: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}`);
886
+ const parts = [];
887
+ if (humanIssueComments.length > 0) {
888
+ parts.push("### PR Comments");
889
+ for (const c of humanIssueComments) {
890
+ parts.push(`**@${c.user_login}:**
891
+ ${c.body}`);
892
+ }
844
893
  }
845
- return "error";
894
+ if (humanReviewComments.length > 0) {
895
+ parts.push("### Code Review Comments");
896
+ for (const c of humanReviewComments) {
897
+ const location = c.path ? `\`${c.path}${c.line ? `:${c.line}` : ""}\`` : "";
898
+ parts.push(`**@${c.user_login}** ${location}:
899
+ ${c.body}`);
900
+ }
901
+ }
902
+ return parts.join("\n\n");
903
+ } catch (err) {
904
+ logger.warn(` Failed to get PR feedback for #${prNumber}: ${err}`);
905
+ return null;
846
906
  }
847
907
  }
848
- function getConflictedFiles(cwd) {
849
- try {
850
- const output = git(["diff", "--name-only", "--diff-filter=U"], { cwd });
851
- return output ? output.split("\n") : [];
852
- } catch {
853
- return [];
854
- }
908
+ function isKodyComment(comment) {
909
+ if (comment.user_type === "Bot") return true;
910
+ return KODY_MARKERS.some((marker) => comment.body.includes(marker));
855
911
  }
856
- function commitAll(message, cwd) {
857
- const status = git(["status", "--porcelain"], { cwd });
858
- if (!status) {
859
- return { success: false, hash: "", message: "No changes to commit" };
860
- }
861
- git(["add", "."], { cwd });
862
- git(["commit", "--no-gpg-sign", "-m", message], { cwd });
863
- const hash = git(["rev-parse", "HEAD"], { cwd }).slice(0, 7);
864
- logger.info(` Committed: ${hash} ${message}`);
865
- return { success: true, hash, message };
912
+ function findLastKodyActionTimestamp(comments) {
913
+ const kodyComments = comments.filter(isKodyComment);
914
+ if (kodyComments.length === 0) return null;
915
+ return kodyComments[kodyComments.length - 1].created_at;
866
916
  }
867
- function getDiffFiles(baseBranch, cwd) {
917
+ var API_TIMEOUT_MS, LIFECYCLE_LABELS, _ghCwd, KODY_MARKERS;
918
+ var init_github_api = __esm({
919
+ "src/github-api.ts"() {
920
+ "use strict";
921
+ init_logger();
922
+ API_TIMEOUT_MS = 3e4;
923
+ LIFECYCLE_LABELS = ["planning", "building", "review", "shipping", "done", "failed", "waiting", "low", "medium", "high"];
924
+ KODY_MARKERS = [
925
+ "Kody Review",
926
+ "\u{1F916} Generated by Kody",
927
+ "Kody pipeline started",
928
+ "Fix pushed to PR",
929
+ "PR created:",
930
+ "Pipeline failed at",
931
+ "Pipeline already running",
932
+ "already completed"
933
+ ];
934
+ }
935
+ });
936
+
937
+ // src/cli/task-resolution.ts
938
+ import * as fs9 from "fs";
939
+ import * as path8 from "path";
940
+ import { execFileSync as execFileSync8 } from "child_process";
941
+ function findLatestTaskForIssue(issueNumber, projectDir) {
942
+ const tasksDir = path8.join(projectDir, ".kody", "tasks");
943
+ if (!fs9.existsSync(tasksDir)) return null;
944
+ const allDirs = fs9.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
945
+ const prefix = `${issueNumber}-`;
946
+ const direct = allDirs.find((d) => d.startsWith(prefix));
947
+ if (direct) return direct;
868
948
  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 [];
949
+ const branch = execFileSync8("git", ["branch", "--show-current"], {
950
+ encoding: "utf-8",
951
+ cwd: projectDir,
952
+ timeout: 5e3,
953
+ stdio: ["pipe", "pipe", "pipe"]
954
+ }).trim();
955
+ const branchIssueMatch = branch.match(/^(\d+)-/);
956
+ if (branchIssueMatch) {
957
+ const branchIssueNum = branchIssueMatch[1];
958
+ const branchPrefix = `${branchIssueNum}-`;
959
+ const fromBranch = allDirs.find((d) => d.startsWith(branchPrefix));
960
+ if (fromBranch) return fromBranch;
961
+ }
962
+ } catch {
876
963
  }
964
+ return null;
877
965
  }
878
- function pushBranch(cwd) {
966
+ function generateTaskId() {
967
+ const now = /* @__PURE__ */ new Date();
968
+ const pad = (n) => String(n).padStart(2, "0");
969
+ return `${String(now.getFullYear()).slice(2)}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
970
+ }
971
+ function resolveTaskIdFromComments(issueNumber) {
879
972
  try {
880
- git(["push", "-u", "origin", "HEAD"], { cwd, timeout: 12e4 });
973
+ const comments = getIssueComments(issueNumber);
974
+ const pattern = /pipeline started: `([^`]+)`/;
975
+ let latestTaskId = null;
976
+ for (const comment of comments) {
977
+ const match = comment.body.match(pattern);
978
+ if (match) {
979
+ latestTaskId = match[1];
980
+ }
981
+ }
982
+ return latestTaskId;
881
983
  } catch {
882
- logger.info(" Push rejected (non-fast-forward), retrying with --force-with-lease");
883
- git(["push", "--force-with-lease", "-u", "origin", "HEAD"], { cwd, timeout: 12e4 });
984
+ return null;
884
985
  }
885
- logger.info(" Pushed to origin");
886
986
  }
887
- var BASE_BRANCHES, _hookSafeEnv;
888
- var init_git_utils = __esm({
889
- "src/git-utils.ts"() {
987
+ function resolveTaskIdForCommand(issueNumber, projectDir) {
988
+ const fromTasks = findLatestTaskForIssue(issueNumber, projectDir);
989
+ if (fromTasks) return fromTasks;
990
+ const fromComments = resolveTaskIdFromComments(issueNumber);
991
+ if (fromComments) return fromComments;
992
+ return null;
993
+ }
994
+ var init_task_resolution = __esm({
995
+ "src/cli/task-resolution.ts"() {
890
996
  "use strict";
891
- init_logger();
892
- init_config();
893
- BASE_BRANCHES = ["dev", "main", "master"];
894
- _hookSafeEnv = null;
997
+ init_github_api();
895
998
  }
896
999
  });
897
1000
 
898
- // src/github-api.ts
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;
1001
+ // src/cli/litellm.ts
1002
+ import * as fs10 from "fs";
1003
+ import * as os from "os";
1004
+ import * as path9 from "path";
1005
+ import { execFileSync as execFileSync9 } from "child_process";
1006
+ async function checkLitellmHealth(url) {
1007
+ try {
1008
+ const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
1009
+ return response.ok;
1010
+ } catch {
1011
+ return false;
907
1012
  }
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
1013
  }
914
- function setGhCwd(cwd) {
915
- _ghCwd = cwd;
916
- }
917
- function ghToken() {
918
- return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
919
- }
920
- function gh(args2, options) {
921
- const token = ghToken();
922
- const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
923
- return execFileSync8("gh", args2, {
924
- encoding: "utf-8",
925
- timeout: API_TIMEOUT_MS,
926
- cwd: _ghCwd,
927
- env,
928
- input: options?.input,
929
- stdio: options?.input ? ["pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
930
- }).trim();
931
- }
932
- function getIssue(issueNumber) {
1014
+ async function checkModelHealth(baseUrl, apiKey, model) {
933
1015
  try {
934
- const output = gh([
935
- "issue",
936
- "view",
937
- String(issueNumber),
938
- "--json",
939
- "body,title"
940
- ]);
941
- const parsed = JSON.parse(output);
942
- if (!parsed || typeof parsed.title !== "string") {
943
- logger.warn(` Issue #${issueNumber}: unexpected response shape`);
944
- return null;
1016
+ const res = await fetch(`${baseUrl}/v1/messages`, {
1017
+ method: "POST",
1018
+ headers: {
1019
+ "Content-Type": "application/json",
1020
+ "x-api-key": apiKey,
1021
+ "anthropic-version": "2023-06-01"
1022
+ },
1023
+ body: JSON.stringify({
1024
+ model,
1025
+ max_tokens: 4,
1026
+ messages: [{ role: "user", content: "Reply with: ok" }]
1027
+ }),
1028
+ signal: AbortSignal.timeout(3e4)
1029
+ });
1030
+ if (!res.ok) {
1031
+ const body2 = await res.text().catch(() => "");
1032
+ return { ok: false, error: `HTTP ${res.status}: ${body2.slice(0, 200)}` };
945
1033
  }
946
- return { body: parsed.body ?? "", title: parsed.title };
1034
+ const body = await res.json();
1035
+ const hasAnthropicContent = Array.isArray(body.content) && body.content.some((b) => b.type === "text");
1036
+ const hasThinkingContent = Array.isArray(body.content) && body.content.some((b) => b.type === "thinking");
1037
+ const hasOpenAIContent = !!body.choices?.[0]?.message?.content;
1038
+ if (!hasAnthropicContent && !hasThinkingContent && !hasOpenAIContent) {
1039
+ return { ok: false, error: `Unexpected response format: ${JSON.stringify(body).slice(0, 200)}` };
1040
+ }
1041
+ return { ok: true };
947
1042
  } catch (err) {
948
- if (isNotFoundError(err)) {
949
- logger.info(` Issue #${issueNumber} not found`);
950
- } else {
951
- logger.error(` Failed to get issue #${issueNumber}: ${ghErrorMessage(err)}`);
1043
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
1044
+ }
1045
+ }
1046
+ function generateLitellmConfig(provider, modelMap) {
1047
+ const apiKeyVar = providerApiKeyEnvVar(provider);
1048
+ const entries = ["model_list:"];
1049
+ const seen = /* @__PURE__ */ new Set();
1050
+ for (const providerModel of Object.values(modelMap)) {
1051
+ if (seen.has(providerModel)) continue;
1052
+ seen.add(providerModel);
1053
+ entries.push(` - model_name: ${providerModel}`);
1054
+ entries.push(` litellm_params:`);
1055
+ entries.push(` model: ${provider}/${providerModel}`);
1056
+ entries.push(` api_key: os.environ/${apiKeyVar}`);
1057
+ }
1058
+ return entries.join("\n") + "\n";
1059
+ }
1060
+ function generateLitellmConfigFromStages(defaultConfig, stages) {
1061
+ const proxyModels = [];
1062
+ if (defaultConfig && defaultConfig.provider !== "claude" && defaultConfig.provider !== "anthropic") {
1063
+ proxyModels.push(defaultConfig);
1064
+ }
1065
+ if (stages) {
1066
+ for (const sc of Object.values(stages)) {
1067
+ if (sc.provider !== "claude" && sc.provider !== "anthropic") {
1068
+ proxyModels.push(sc);
1069
+ }
952
1070
  }
953
- return null;
954
1071
  }
1072
+ if (proxyModels.length === 0) return void 0;
1073
+ const entries = ["model_list:"];
1074
+ const seen = /* @__PURE__ */ new Set();
1075
+ for (const { provider, model } of proxyModels) {
1076
+ const key = `${provider}/${model}`;
1077
+ if (seen.has(key)) continue;
1078
+ seen.add(key);
1079
+ const apiKeyVar = providerApiKeyEnvVar(provider);
1080
+ entries.push(` - model_name: ${model}`);
1081
+ entries.push(` litellm_params:`);
1082
+ entries.push(` model: ${provider}/${model}`);
1083
+ entries.push(` api_key: os.environ/${apiKeyVar}`);
1084
+ }
1085
+ return entries.join("\n") + "\n";
955
1086
  }
956
- function getIssueComments(issueNumber) {
1087
+ async function tryStartLitellm(url, projectDir, generatedConfig) {
1088
+ if (!generatedConfig) {
1089
+ logger.warn("No provider configured in kody.config.json \u2014 cannot start LiteLLM proxy");
1090
+ return null;
1091
+ }
1092
+ const configPath = path9.join(os.tmpdir(), "kody-litellm-config.yaml");
1093
+ fs10.writeFileSync(configPath, generatedConfig);
1094
+ const portMatch = url.match(/:(\d+)/);
1095
+ const port = portMatch ? portMatch[1] : "4000";
1096
+ let litellmFound = false;
957
1097
  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) : [];
1098
+ execFileSync9("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
1099
+ litellmFound = true;
965
1100
  } catch {
966
- return [];
1101
+ try {
1102
+ execFileSync9("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
1103
+ litellmFound = true;
1104
+ } catch {
1105
+ }
967
1106
  }
968
- }
969
- function getIssueLabels(issueNumber) {
1107
+ if (!litellmFound) {
1108
+ logger.warn("litellm not installed (pip install 'litellm[proxy]')");
1109
+ return null;
1110
+ }
1111
+ logger.info(`Starting LiteLLM proxy on port ${port}...`);
1112
+ let cmd;
1113
+ let args2;
970
1114
  try {
971
- const output = gh(["issue", "view", String(issueNumber), "--json", "labels", "--jq", ".labels[].name"]);
972
- return output.split("\n").filter(Boolean);
1115
+ execFileSync9("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
1116
+ cmd = "litellm";
1117
+ args2 = ["--config", configPath, "--port", port];
973
1118
  } catch {
974
- return [];
1119
+ cmd = "python3";
1120
+ args2 = ["-m", "litellm", "--config", configPath, "--port", port];
975
1121
  }
976
- }
977
- function setLabel(issueNumber, label) {
978
- try {
979
- gh(["issue", "edit", String(issueNumber), "--add-label", label]);
980
- logger.info(` Label added: ${label}`);
981
- } catch (err) {
982
- logger.warn(` Failed to set label ${label}: ${err}`);
1122
+ const dotenvPath = path9.join(projectDir, ".env");
1123
+ const dotenvVars = {};
1124
+ if (fs10.existsSync(dotenvPath)) {
1125
+ for (const rawLine of fs10.readFileSync(dotenvPath, "utf-8").split("\n")) {
1126
+ const line = rawLine.trim();
1127
+ if (!line || line.startsWith("#")) continue;
1128
+ const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
1129
+ if (match) {
1130
+ let value = match[2].trim();
1131
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1132
+ value = value.slice(1, -1);
1133
+ }
1134
+ const commentIdx = value.indexOf(" #");
1135
+ if (commentIdx !== -1) value = value.slice(0, commentIdx).trim();
1136
+ if (value) dotenvVars[match[1]] = value;
1137
+ }
1138
+ }
1139
+ if (Object.keys(dotenvVars).length > 0) {
1140
+ logger.info(` Loaded API keys: ${Object.keys(dotenvVars).join(", ")}`);
1141
+ }
983
1142
  }
984
- }
985
- function postComment(issueNumber, body) {
986
- try {
987
- gh(
988
- ["issue", "comment", String(issueNumber), "--body-file", "-"],
989
- { input: body }
990
- );
991
- logger.info(` Comment posted on #${issueNumber}`);
992
- } catch (err) {
993
- logger.warn(` Failed to post comment: ${err}`);
1143
+ const { spawn: spawn2 } = await import("child_process");
1144
+ const child = spawn2(cmd, args2, {
1145
+ stdio: ["ignore", "pipe", "pipe"],
1146
+ detached: true,
1147
+ env: { ...process.env, ...dotenvVars }
1148
+ });
1149
+ let proxyStderr = "";
1150
+ child.stderr?.on("data", (chunk) => {
1151
+ proxyStderr += chunk.toString();
1152
+ });
1153
+ for (let i = 0; i < 30; i++) {
1154
+ await new Promise((r) => setTimeout(r, 2e3));
1155
+ if (await checkLitellmHealth(url)) {
1156
+ logger.info(`LiteLLM proxy ready at ${url}`);
1157
+ return child;
1158
+ }
1159
+ }
1160
+ if (proxyStderr) {
1161
+ logger.warn(`LiteLLM stderr: ${proxyStderr.slice(-1e3)}`);
994
1162
  }
1163
+ logger.warn("LiteLLM proxy failed to start within 60s");
1164
+ child.kill();
1165
+ return null;
995
1166
  }
996
- function getPRForBranch(branch) {
997
- try {
998
- const output = gh([
999
- "pr",
1000
- "view",
1001
- branch,
1002
- "--json",
1003
- "number,url"
1004
- ]);
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;
1167
+ var init_litellm = __esm({
1168
+ "src/cli/litellm.ts"() {
1169
+ "use strict";
1170
+ init_logger();
1171
+ init_config();
1172
+ }
1173
+ });
1174
+
1175
+ // src/cli/taskify-command.ts
1176
+ var taskify_command_exports = {};
1177
+ __export(taskify_command_exports, {
1178
+ isTaskifyRun: () => isTaskifyRun,
1179
+ readTaskifyMarker: () => readTaskifyMarker,
1180
+ runTaskifyCommand: () => runTaskifyCommand,
1181
+ taskifyCommand: () => taskifyCommand,
1182
+ topoSort: () => topoSort
1183
+ });
1184
+ import * as fs11 from "fs";
1185
+ import * as path10 from "path";
1186
+ import { fileURLToPath } from "url";
1187
+ import { execSync } from "child_process";
1188
+ function topoSort(tasks) {
1189
+ const n = tasks.length;
1190
+ const inDegree = new Array(n).fill(0);
1191
+ const adj = Array.from({ length: n }, () => []);
1192
+ for (let i = 0; i < n; i++) {
1193
+ for (const dep of tasks[i].dependsOn ?? []) {
1194
+ if (dep >= 0 && dep < n && dep !== i) {
1195
+ adj[dep].push(i);
1196
+ inDegree[i]++;
1197
+ }
1009
1198
  }
1010
- return { number: data.number, url: data.url };
1011
- } catch (err) {
1012
- if (!isNotFoundError(err)) {
1013
- logger.warn(` Failed to check PR for branch ${branch}: ${ghErrorMessage(err)}`);
1199
+ }
1200
+ const queue = [];
1201
+ for (let i = 0; i < n; i++) {
1202
+ if (inDegree[i] === 0) queue.push(i);
1203
+ }
1204
+ const sorted = [];
1205
+ while (queue.length > 0) {
1206
+ const node = queue.shift();
1207
+ sorted.push(tasks[node]);
1208
+ for (const neighbor of adj[node]) {
1209
+ inDegree[neighbor]--;
1210
+ if (inDegree[neighbor] === 0) queue.push(neighbor);
1014
1211
  }
1015
- return null;
1016
1212
  }
1213
+ if (sorted.length !== n) {
1214
+ logger.warn("[taskify] dependency cycle detected \u2014 falling back to original order");
1215
+ return [...tasks];
1216
+ }
1217
+ return sorted;
1017
1218
  }
1018
- function updatePR(prNumber, body) {
1019
- try {
1020
- gh(
1021
- ["pr", "edit", String(prNumber), "--body-file", "-"],
1022
- { input: body }
1023
- );
1024
- logger.info(` PR #${prNumber} body updated`);
1025
- } catch (err) {
1026
- logger.warn(` Failed to update PR #${prNumber}: ${err}`);
1219
+ function getArg(args2, flag) {
1220
+ const idx = args2.indexOf(flag);
1221
+ return idx !== -1 ? args2[idx + 1] : void 0;
1222
+ }
1223
+ function hasFlag(args2, flag) {
1224
+ return args2.includes(flag);
1225
+ }
1226
+ async function runTaskifyCommand() {
1227
+ const args2 = process.argv.slice(3);
1228
+ const cwdArg = getArg(args2, "--cwd") ?? process.cwd();
1229
+ const projectDir = path10.resolve(cwdArg);
1230
+ const ticketId = getArg(args2, "--ticket") ?? process.env.TICKET_ID;
1231
+ const prdFileArg = getArg(args2, "--file") ?? process.env.PRD_FILE;
1232
+ const prdFile = prdFileArg ? path10.resolve(projectDir, prdFileArg) : void 0;
1233
+ const issueNumberStr = getArg(args2, "--issue-number") ?? process.env.ISSUE_NUMBER ?? "";
1234
+ const issueNumber = issueNumberStr ? parseInt(issueNumberStr, 10) : void 0;
1235
+ const feedback = getArg(args2, "--feedback") ?? process.env.FEEDBACK;
1236
+ const local = hasFlag(args2, "--local") || !process.env.CI;
1237
+ const taskIdArg = getArg(args2, "--task-id") ?? process.env.TASK_ID;
1238
+ const taskId = taskIdArg ?? (issueNumber ? `taskify-${issueNumber}-${generateTaskId()}` : `taskify-${generateTaskId()}`);
1239
+ if (!ticketId && !prdFile) {
1240
+ logger.error("Usage: kody taskify --ticket <ticket-id> OR kody taskify --file <prd.md>");
1241
+ process.exit(1);
1242
+ }
1243
+ if (prdFile && !fs11.existsSync(prdFile)) {
1244
+ logger.error(`File not found: ${prdFile}`);
1245
+ process.exit(1);
1246
+ }
1247
+ setConfigDir(projectDir);
1248
+ setGhCwd(projectDir);
1249
+ const config = getProjectConfig();
1250
+ let litellmProcess = null;
1251
+ let runnerEnv;
1252
+ if (anyStageNeedsProxy(config)) {
1253
+ const litellmUrl = getLitellmUrl();
1254
+ const proxyRunning = await checkLitellmHealth(litellmUrl);
1255
+ if (!proxyRunning) {
1256
+ let generatedConfig;
1257
+ if (config.agent.stages || config.agent.default) {
1258
+ generatedConfig = generateLitellmConfigFromStages(config.agent.default, config.agent.stages);
1259
+ } else if (config.agent.provider && config.agent.provider !== "anthropic") {
1260
+ generatedConfig = generateLitellmConfig(config.agent.provider, config.agent.modelMap);
1261
+ }
1262
+ litellmProcess = await tryStartLitellm(litellmUrl, projectDir, generatedConfig);
1263
+ }
1264
+ runnerEnv = {
1265
+ ANTHROPIC_BASE_URL: litellmUrl,
1266
+ ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || "dummy"
1267
+ };
1268
+ }
1269
+ await taskifyCommand({ ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId, runnerEnv });
1270
+ litellmProcess?.kill();
1271
+ }
1272
+ async function taskifyCommand(opts) {
1273
+ const { ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId } = opts;
1274
+ const config = getProjectConfig();
1275
+ const taskDir = path10.join(projectDir, ".kody", "tasks", taskId);
1276
+ fs11.mkdirSync(taskDir, { recursive: true });
1277
+ const mode = prdFile ? "file" : "ticket";
1278
+ logger.info(`[taskify] mode=${mode} source=${ticketId ?? prdFile} issue=${issueNumber ?? "none"} task=${taskId}`);
1279
+ let mcpConfigJson;
1280
+ if (mode === "ticket") {
1281
+ try {
1282
+ mcpConfigJson = buildTaskifyMcpConfigJson(config);
1283
+ } catch (err) {
1284
+ const msg = err instanceof Error ? err.message : String(err);
1285
+ logger.error(`[taskify] MCP config error: ${msg}`);
1286
+ if (issueNumber && !local) {
1287
+ postComment(
1288
+ issueNumber,
1289
+ `Kody could not start the taskify command:
1290
+
1291
+ > ${msg}
1292
+
1293
+ Add the required MCP server config to \`kody.config.json\` and try again.`
1294
+ );
1295
+ }
1296
+ process.exit(1);
1297
+ }
1298
+ }
1299
+ const sc = resolveStageConfig(config, "taskify", "strong");
1300
+ const model = sc.model;
1301
+ const fileContent = prdFile ? fs11.readFileSync(prdFile, "utf-8") : void 0;
1302
+ let projectContext;
1303
+ {
1304
+ const parts = [];
1305
+ const memoryPath = path10.join(projectDir, ".kody", "memory.md");
1306
+ if (fs11.existsSync(memoryPath)) {
1307
+ try {
1308
+ const content = fs11.readFileSync(memoryPath, "utf-8").slice(0, 2e3);
1309
+ if (content.trim()) parts.push(`### Project Memory
1310
+ ${content}`);
1311
+ } catch {
1312
+ }
1313
+ }
1314
+ try {
1315
+ const output = execSync("git ls-files", { cwd: projectDir, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
1316
+ const lines = output.split("\n").filter(Boolean).slice(0, 150);
1317
+ if (lines.length > 0) parts.push(`### File Tree
1318
+ \`\`\`
1319
+ ${lines.join("\n")}
1320
+ \`\`\``);
1321
+ } catch {
1322
+ }
1323
+ if (parts.length > 0) projectContext = parts.join("\n\n");
1324
+ }
1325
+ const prompt = buildPrompt({ ticketId, fileContent, taskDir, feedback, projectContext });
1326
+ if (issueNumber && !local) {
1327
+ const src = mode === "file" ? `file \`${path10.basename(prdFile)}\`` : `ticket **${ticketId}**`;
1328
+ postComment(issueNumber, `Kody is decomposing ${src} into tasks...`);
1329
+ setLifecycleLabel(issueNumber, "planning");
1330
+ }
1331
+ fs11.writeFileSync(path10.join(taskDir, MARKER_FILE), JSON.stringify({ ticketId, prdFile, issueNumber }));
1332
+ const runner = opts.runner ?? createClaudeCodeRunner();
1333
+ logger.info(` model=${model} timeout=${TASKIFY_TIMEOUT_MS / 1e3}s`);
1334
+ const result = await runner.run("taskify", prompt, model, TASKIFY_TIMEOUT_MS, taskDir, {
1335
+ cwd: projectDir,
1336
+ mcpConfigJson,
1337
+ env: opts.runnerEnv
1338
+ });
1339
+ if (result.outcome !== "completed") {
1340
+ const errMsg = result.outcome === "timed_out" ? "Taskify timed out after 5 minutes." : `Taskify failed: ${result.error}`;
1341
+ logger.error(`[taskify] ${errMsg}`);
1342
+ if (issueNumber && !local) {
1343
+ postComment(issueNumber, `Kody taskify failed:
1344
+
1345
+ > ${errMsg}`);
1346
+ setLifecycleLabel(issueNumber, "failed");
1347
+ }
1348
+ process.exit(1);
1349
+ }
1350
+ const resultPath = path10.join(taskDir, RESULT_FILE);
1351
+ if (!fs11.existsSync(resultPath)) {
1352
+ const errMsg = `Claude did not write ${RESULT_FILE}. Output:
1353
+
1354
+ ${result.output?.slice(0, 500) ?? "(none)"}`;
1355
+ logger.error(`[taskify] ${errMsg}`);
1356
+ if (issueNumber && !local) {
1357
+ postComment(issueNumber, `Kody taskify failed: result file not found.
1358
+
1359
+ ${errMsg}`);
1360
+ setLifecycleLabel(issueNumber, "failed");
1361
+ }
1362
+ process.exit(1);
1363
+ }
1364
+ let parsed;
1365
+ try {
1366
+ parsed = JSON.parse(fs11.readFileSync(resultPath, "utf-8"));
1367
+ } catch {
1368
+ const errMsg = `Could not parse ${RESULT_FILE} as JSON.`;
1369
+ logger.error(`[taskify] ${errMsg}`);
1370
+ if (issueNumber && !local) {
1371
+ postComment(issueNumber, `Kody taskify failed: ${errMsg}`);
1372
+ setLifecycleLabel(issueNumber, "failed");
1373
+ }
1374
+ process.exit(1);
1375
+ }
1376
+ const sourceLabel = ticketId ?? (prdFile ? path10.basename(prdFile) : "spec");
1377
+ if (parsed.status === "questions") {
1378
+ handleQuestions(parsed, sourceLabel, issueNumber, local ?? false);
1379
+ } else if (parsed.status === "ready") {
1380
+ await handleTasks(parsed, sourceLabel, issueNumber, local ?? false);
1381
+ } else {
1382
+ const errMsg = `Unexpected status in ${RESULT_FILE}: ${JSON.stringify(parsed)}`;
1383
+ logger.error(`[taskify] ${errMsg}`);
1384
+ if (issueNumber && !local) {
1385
+ postComment(issueNumber, `Kody taskify failed: ${errMsg}`);
1386
+ setLifecycleLabel(issueNumber, "failed");
1387
+ }
1388
+ process.exit(1);
1389
+ }
1390
+ }
1391
+ function handleQuestions(parsed, ticketId, issueNumber, local) {
1392
+ const questions = parsed.questions ?? [];
1393
+ const numbered = questions.map((q, i) => `${i + 1}. ${q}`).join("\n");
1394
+ const comment = `Kody has questions before decomposing **${ticketId}**:
1395
+
1396
+ ${numbered}
1397
+
1398
+ Reply with \`@kody approve\` and your answers to proceed.`;
1399
+ logger.info(`[taskify] posting ${questions.length} question(s)`);
1400
+ if (issueNumber && !local) {
1401
+ postComment(issueNumber, comment);
1402
+ setLifecycleLabel(issueNumber, "waiting");
1403
+ } else {
1404
+ logger.info(`[taskify] questions:
1405
+ ${comment}`);
1406
+ }
1407
+ }
1408
+ async function handleTasks(parsed, ticketId, issueNumber, local) {
1409
+ const tasks = topoSort(parsed.tasks ?? []);
1410
+ if (tasks.length === 0) {
1411
+ logger.warn("[taskify] no tasks in result \u2014 nothing to file");
1412
+ if (issueNumber && !local) {
1413
+ postComment(issueNumber, `Kody taskify completed but found no tasks to file for **${ticketId}**.`);
1414
+ setLifecycleLabel(issueNumber, "done");
1415
+ }
1416
+ return;
1417
+ }
1418
+ const tooMany = tasks.length > MAX_TASKS_GUARD;
1419
+ if (tooMany) {
1420
+ logger.warn(`[taskify] ${tasks.length} tasks exceeds MAX_TASKS_GUARD (${MAX_TASKS_GUARD}) \u2014 filing issues but skipping auto-trigger`);
1421
+ }
1422
+ logger.info(`[taskify] filing ${tasks.length} issue(s)`);
1423
+ const filed = [];
1424
+ for (const task of tasks) {
1425
+ if (local) {
1426
+ logger.info(` [local] would create issue: ${task.title}`);
1427
+ filed.push({ number: 0, url: "#", title: task.title });
1428
+ continue;
1429
+ }
1430
+ const allLabels = [...task.labels ?? [], ...task.priority ? [`priority:${task.priority}`] : []];
1431
+ const issue = createIssue(task.title, task.body, allLabels);
1432
+ if (issue) {
1433
+ filed.push({ number: issue.number, url: issue.url, title: task.title });
1434
+ } else {
1435
+ logger.warn(` failed to create issue: ${task.title}`);
1436
+ }
1437
+ }
1438
+ const autoTrigger = !tooMany && filed.length <= AUTO_TRIGGER_THRESHOLD;
1439
+ if (autoTrigger && !local) {
1440
+ for (const issue of filed) {
1441
+ if (issue.number > 0) {
1442
+ postComment(issue.number, "@kody");
1443
+ logger.info(` auto-triggered @kody on issue #${issue.number}`);
1444
+ }
1445
+ }
1446
+ }
1447
+ if (issueNumber && !local) {
1448
+ const links = filed.map((i) => `- [#${i.number}](${i.url}) \u2014 ${i.title}`).join("\n");
1449
+ const triggerNote = tooMany ? `
1450
+
1451
+ > **${tasks.length} tasks filed** \u2014 auto-trigger is disabled for large epics. Comment \`@kody\` on each issue to start the pipeline.` : autoTrigger ? `
1452
+
1453
+ > Auto-triggered \`@kody\` on each issue.` : `
1454
+
1455
+ > Comment \`@kody\` on each issue to start the pipeline.`;
1456
+ postComment(
1457
+ issueNumber,
1458
+ `Kody decomposed **${ticketId}** into ${filed.length} task(s):
1459
+
1460
+ ${links}${triggerNote}`
1461
+ );
1462
+ setLifecycleLabel(issueNumber, "done");
1463
+ } else if (local) {
1464
+ logger.info(`[taskify] local mode \u2014 would file ${filed.length} issue(s)`);
1465
+ }
1466
+ }
1467
+ function buildPrompt(opts) {
1468
+ const { ticketId, fileContent, taskDir, feedback, projectContext } = opts;
1469
+ const scriptDir = new URL(".", import.meta.url).pathname;
1470
+ const candidates = [
1471
+ path10.resolve(scriptDir, "..", "prompts", "taskify-ticket.md"),
1472
+ path10.resolve(scriptDir, "..", "..", "prompts", "taskify-ticket.md"),
1473
+ path10.resolve(__dirname, "..", "..", "prompts", "taskify-ticket.md"),
1474
+ path10.resolve(__dirname, "..", "prompts", "taskify-ticket.md")
1475
+ ];
1476
+ let template = "";
1477
+ for (const candidate of candidates) {
1478
+ if (fs11.existsSync(candidate)) {
1479
+ template = fs11.readFileSync(candidate, "utf-8");
1480
+ break;
1481
+ }
1482
+ }
1483
+ if (!template) {
1484
+ throw new Error(`Could not find prompts/taskify-ticket.md. Searched: ${candidates.join(", ")}`);
1485
+ }
1486
+ const resolveBlock = (name, value) => {
1487
+ if (value) {
1488
+ template = template.replace(new RegExp(`\\{\\{#if ${name}\\}\\}\\n?([\\s\\S]*?)\\{\\{\\/if\\}\\}`, "g"), "$1");
1489
+ template = template.replace(new RegExp(`\\{\\{${name}\\}\\}`, "g"), value);
1490
+ } else {
1491
+ template = template.replace(new RegExp(`\\{\\{#if ${name}\\}\\}\\n?[\\s\\S]*?\\{\\{\\/if\\}\\}\\n?`, "g"), "");
1492
+ }
1493
+ };
1494
+ resolveBlock("PROJECT_CONTEXT", projectContext);
1495
+ resolveBlock("TICKET_ID", ticketId);
1496
+ resolveBlock("FILE_CONTENT", fileContent);
1497
+ resolveBlock("FEEDBACK", feedback);
1498
+ template = template.replace(/\{\{TASK_DIR\}\}/g, taskDir);
1499
+ return template;
1500
+ }
1501
+ function isTaskifyRun(taskDir) {
1502
+ return fs11.existsSync(path10.join(taskDir, MARKER_FILE));
1503
+ }
1504
+ function readTaskifyMarker(taskDir) {
1505
+ const markerPath = path10.join(taskDir, MARKER_FILE);
1506
+ if (!fs11.existsSync(markerPath)) return null;
1507
+ try {
1508
+ return JSON.parse(fs11.readFileSync(markerPath, "utf-8"));
1509
+ } catch {
1510
+ return null;
1511
+ }
1512
+ }
1513
+ var __dirname, AUTO_TRIGGER_THRESHOLD, MAX_TASKS_GUARD, TASKIFY_TIMEOUT_MS, MARKER_FILE, RESULT_FILE;
1514
+ var init_taskify_command = __esm({
1515
+ "src/cli/taskify-command.ts"() {
1516
+ "use strict";
1517
+ init_config();
1518
+ init_agent_runner();
1519
+ init_mcp_config();
1520
+ init_github_api();
1521
+ init_logger();
1522
+ init_task_resolution();
1523
+ init_litellm();
1524
+ __dirname = path10.dirname(fileURLToPath(import.meta.url));
1525
+ AUTO_TRIGGER_THRESHOLD = 5;
1526
+ MAX_TASKS_GUARD = 20;
1527
+ TASKIFY_TIMEOUT_MS = 5 * 60 * 1e3;
1528
+ MARKER_FILE = "taskify.marker";
1529
+ RESULT_FILE = "taskify-result.json";
1530
+ }
1531
+ });
1532
+
1533
+ // src/ci/parse-inputs.ts
1534
+ var parse_inputs_exports = {};
1535
+ __export(parse_inputs_exports, {
1536
+ parseCommentInputs: () => parseCommentInputs,
1537
+ runCiParse: () => runCiParse,
1538
+ writeOutputs: () => writeOutputs
1539
+ });
1540
+ import * as fs12 from "fs";
1541
+ function generateTimestamp() {
1542
+ const now = /* @__PURE__ */ new Date();
1543
+ const pad = (n) => String(n).padStart(2, "0");
1544
+ const y = String(now.getFullYear()).slice(2);
1545
+ const m = pad(now.getMonth() + 1);
1546
+ const d = pad(now.getDate());
1547
+ const H = pad(now.getHours());
1548
+ const M = pad(now.getMinutes());
1549
+ const S = pad(now.getSeconds());
1550
+ return `${y}${m}${d}-${H}${M}${S}`;
1551
+ }
1552
+ function parseCommentInputs() {
1553
+ const triggerType = process.env.TRIGGER_TYPE ?? "dispatch";
1554
+ if (triggerType === "dispatch") {
1555
+ const taskId2 = process.env.INPUT_TASK_ID ?? "";
1556
+ return {
1557
+ task_id: taskId2,
1558
+ mode: process.env.INPUT_MODE ?? "full",
1559
+ from_stage: process.env.INPUT_FROM_STAGE ?? "",
1560
+ issue_number: process.env.INPUT_ISSUE_NUMBER ?? "",
1561
+ pr_number: "",
1562
+ feedback: process.env.INPUT_FEEDBACK ?? "",
1563
+ complexity: "",
1564
+ ci_run_id: "",
1565
+ ticket_id: "",
1566
+ prd_file: "",
1567
+ dry_run: false,
1568
+ valid: !!taskId2,
1569
+ trigger_type: "dispatch"
1570
+ };
1571
+ }
1572
+ const commentBody = (process.env.COMMENT_BODY ?? "").replace(/\r/g, "");
1573
+ const issueNumber = process.env.ISSUE_NUMBER ?? "";
1574
+ const isPR = !!process.env.ISSUE_IS_PR;
1575
+ const kodyMatch = commentBody.match(/(?:@kody|\/kody)\s*(.*)/i);
1576
+ if (!kodyMatch) {
1577
+ return {
1578
+ task_id: "",
1579
+ mode: "full",
1580
+ from_stage: "",
1581
+ issue_number: issueNumber,
1582
+ pr_number: "",
1583
+ feedback: "",
1584
+ complexity: "",
1585
+ ci_run_id: "",
1586
+ ticket_id: "",
1587
+ prd_file: "",
1588
+ dry_run: false,
1589
+ valid: false,
1590
+ trigger_type: "comment"
1591
+ };
1592
+ }
1593
+ const argsLine = kodyMatch[1].trim();
1594
+ let fromStage = "";
1595
+ let feedback = "";
1596
+ let complexity = "";
1597
+ let dryRun = false;
1598
+ let ciRunId = "";
1599
+ let ticketId = "";
1600
+ let prdFile = "";
1601
+ const fromMatch = argsLine.match(/--from\s+(\S+)/);
1602
+ if (fromMatch) fromStage = fromMatch[1];
1603
+ const feedbackMatch = argsLine.match(/--feedback\s+"([^"]*)"/);
1604
+ if (feedbackMatch) feedback = feedbackMatch[1];
1605
+ const complexityMatch = argsLine.match(/--complexity\s+(\S+)/);
1606
+ if (complexityMatch) complexity = complexityMatch[1];
1607
+ if (/--dry-run/.test(argsLine)) dryRun = true;
1608
+ const ciRunIdMatch = argsLine.match(/--ci-run-id\s+(\S+)/);
1609
+ if (ciRunIdMatch) ciRunId = ciRunIdMatch[1];
1610
+ const ticketMatch = argsLine.match(/--ticket\s+(\S+)/);
1611
+ if (ticketMatch) ticketId = ticketMatch[1];
1612
+ const fileMatch = argsLine.match(/--file\s+(\S+)/);
1613
+ if (fileMatch) prdFile = fileMatch[1];
1614
+ 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(/--ticket\s+\S+/g, "").replace(/--file\s+\S+/g, "").replace(/\s+/g, " ").trim();
1615
+ const parts = positional ? positional.split(/\s+/) : [];
1616
+ let mode = "full";
1617
+ let taskId = "";
1618
+ let idx = 0;
1619
+ if (parts[idx] && VALID_MODES.includes(parts[idx])) {
1620
+ mode = parts[idx];
1621
+ idx++;
1622
+ }
1623
+ if (parts[idx] && !parts[idx].startsWith("--")) {
1624
+ taskId = parts[idx];
1625
+ idx++;
1626
+ } else if (parts[0] && !VALID_MODES.includes(parts[0]) && !parts[0].startsWith("--")) {
1627
+ taskId = parts[0];
1628
+ }
1629
+ const kodyLineIdx = commentBody.search(/(?:@kody|\/kody)/i);
1630
+ const afterKodyLine = commentBody.slice(kodyLineIdx);
1631
+ const newlineIdx = afterKodyLine.indexOf("\n");
1632
+ const bodyAfterCommand = newlineIdx !== -1 ? afterKodyLine.slice(newlineIdx + 1) : "";
1633
+ if (mode === "approve") {
1634
+ mode = "rerun";
1635
+ if (bodyAfterCommand) {
1636
+ feedback = bodyAfterCommand;
1637
+ }
1638
+ }
1639
+ if (mode === "fix") {
1640
+ if (bodyAfterCommand) {
1641
+ feedback = bodyAfterCommand;
1642
+ }
1643
+ }
1644
+ if (mode === "fix-ci") {
1645
+ if (bodyAfterCommand) {
1646
+ feedback = bodyAfterCommand;
1647
+ const runIdFromBody = bodyAfterCommand.match(/Run ID:\s*(\d+)/);
1648
+ if (runIdFromBody) {
1649
+ ciRunId = runIdFromBody[1];
1650
+ }
1651
+ }
1652
+ }
1653
+ if (mode === "bootstrap") {
1654
+ taskId = `bootstrap-${generateTimestamp()}`;
1655
+ }
1656
+ if (mode === "taskify") {
1657
+ taskId = `taskify-${issueNumber}-${generateTimestamp()}`;
1658
+ }
1659
+ const prNumber = isPR ? issueNumber : "";
1660
+ if (mode === "review" && prNumber) {
1661
+ taskId = `review-pr-${prNumber}-${generateTimestamp()}`;
1662
+ }
1663
+ if (!taskId && mode === "full") {
1664
+ taskId = `${issueNumber}-${generateTimestamp()}`;
1665
+ }
1666
+ const modesWithoutTaskId = ["fix", "fix-ci", "status", "review", "resolve", "rerun"];
1667
+ const valid = !!taskId || modesWithoutTaskId.includes(mode);
1668
+ if (mode === "taskify" && !ticketId && !prdFile) {
1669
+ return {
1670
+ task_id: taskId,
1671
+ mode,
1672
+ from_stage: fromStage,
1673
+ issue_number: issueNumber,
1674
+ pr_number: "",
1675
+ feedback,
1676
+ complexity,
1677
+ ci_run_id: ciRunId,
1678
+ ticket_id: "",
1679
+ prd_file: "",
1680
+ dry_run: dryRun,
1681
+ valid: false,
1682
+ trigger_type: "comment"
1683
+ };
1684
+ }
1685
+ return {
1686
+ task_id: taskId,
1687
+ mode,
1688
+ from_stage: fromStage,
1689
+ issue_number: issueNumber,
1690
+ pr_number: prNumber,
1691
+ feedback,
1692
+ complexity,
1693
+ ci_run_id: ciRunId,
1694
+ ticket_id: ticketId,
1695
+ prd_file: prdFile,
1696
+ dry_run: dryRun,
1697
+ valid,
1698
+ trigger_type: "comment"
1699
+ };
1700
+ }
1701
+ function writeOutputs(result) {
1702
+ const outputFile = process.env.GITHUB_OUTPUT;
1703
+ function output(key, value) {
1704
+ if (outputFile) {
1705
+ if (value.includes("\n")) {
1706
+ fs12.appendFileSync(outputFile, `${key}<<KODY_EOF
1707
+ ${value}
1708
+ KODY_EOF
1709
+ `);
1710
+ } else {
1711
+ fs12.appendFileSync(outputFile, `${key}=${value}
1712
+ `);
1713
+ }
1714
+ }
1715
+ const display = value.includes("\n") ? value.split("\n")[0] + "..." : value;
1716
+ console.log(`${key}=${display}`);
1717
+ }
1718
+ output("task_id", result.task_id);
1719
+ output("mode", result.mode);
1720
+ output("from_stage", result.from_stage);
1721
+ output("issue_number", result.issue_number);
1722
+ output("pr_number", result.pr_number);
1723
+ output("feedback", result.feedback);
1724
+ output("complexity", result.complexity);
1725
+ output("ci_run_id", result.ci_run_id);
1726
+ output("ticket_id", result.ticket_id);
1727
+ output("prd_file", result.prd_file);
1728
+ output("dry_run", result.dry_run ? "true" : "false");
1729
+ output("valid", result.valid ? "true" : "false");
1730
+ output("trigger_type", result.trigger_type);
1731
+ }
1732
+ function runCiParse() {
1733
+ const result = parseCommentInputs();
1734
+ writeOutputs(result);
1735
+ }
1736
+ var VALID_MODES;
1737
+ var init_parse_inputs = __esm({
1738
+ "src/ci/parse-inputs.ts"() {
1739
+ "use strict";
1740
+ VALID_MODES = [
1741
+ "full",
1742
+ "rerun",
1743
+ "fix",
1744
+ "fix-ci",
1745
+ "status",
1746
+ "approve",
1747
+ "review",
1748
+ "resolve",
1749
+ "bootstrap",
1750
+ "taskify"
1751
+ ];
1752
+ }
1753
+ });
1754
+
1755
+ // src/definitions.ts
1756
+ var definitions_exports = {};
1757
+ __export(definitions_exports, {
1758
+ STAGES: () => STAGES,
1759
+ applyTimeoutOverrides: () => applyTimeoutOverrides,
1760
+ getStage: () => getStage
1761
+ });
1762
+ function getStage(name) {
1763
+ return STAGES.find((s) => s.name === name);
1764
+ }
1765
+ function applyTimeoutOverrides(overrides) {
1766
+ for (const stage of STAGES) {
1767
+ if (overrides[stage.name] != null) {
1768
+ stage.timeout = overrides[stage.name] * 1e3;
1769
+ }
1770
+ }
1771
+ }
1772
+ var STAGES;
1773
+ var init_definitions = __esm({
1774
+ "src/definitions.ts"() {
1775
+ "use strict";
1776
+ STAGES = [
1777
+ {
1778
+ name: "taskify",
1779
+ type: "agent",
1780
+ modelTier: "cheap",
1781
+ timeout: 6e5,
1782
+ maxRetries: 1,
1783
+ outputFile: "task.json"
1784
+ },
1785
+ {
1786
+ name: "plan",
1787
+ type: "agent",
1788
+ modelTier: "strong",
1789
+ timeout: 6e5,
1790
+ maxRetries: 1,
1791
+ outputFile: "plan.md"
1792
+ },
1793
+ {
1794
+ name: "build",
1795
+ type: "agent",
1796
+ modelTier: "mid",
1797
+ timeout: 24e5,
1798
+ maxRetries: 1
1799
+ },
1800
+ {
1801
+ name: "verify",
1802
+ type: "gate",
1803
+ modelTier: "cheap",
1804
+ timeout: 3e5,
1805
+ maxRetries: 2,
1806
+ retryWithAgent: "autofix"
1807
+ },
1808
+ {
1809
+ name: "review",
1810
+ type: "agent",
1811
+ modelTier: "strong",
1812
+ timeout: 6e5,
1813
+ maxRetries: 1,
1814
+ outputFile: "review.md"
1815
+ },
1816
+ {
1817
+ name: "review-fix",
1818
+ type: "agent",
1819
+ modelTier: "mid",
1820
+ timeout: 12e5,
1821
+ maxRetries: 1
1822
+ },
1823
+ {
1824
+ name: "ship",
1825
+ type: "deterministic",
1826
+ modelTier: "cheap",
1827
+ timeout: 24e4,
1828
+ maxRetries: 1,
1829
+ outputFile: "ship.md"
1830
+ }
1831
+ ];
1832
+ }
1833
+ });
1834
+
1835
+ // src/git-utils.ts
1836
+ import { execFileSync as execFileSync10 } from "child_process";
1837
+ function getHookSafeEnv() {
1838
+ if (!_hookSafeEnv) {
1839
+ _hookSafeEnv = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
1840
+ }
1841
+ return _hookSafeEnv;
1842
+ }
1843
+ function git(args2, options) {
1844
+ return execFileSync10("git", args2, {
1845
+ encoding: "utf-8",
1846
+ timeout: options?.timeout ?? 3e4,
1847
+ cwd: options?.cwd,
1848
+ env: options?.env ?? getHookSafeEnv(),
1849
+ stdio: ["pipe", "pipe", "pipe"]
1850
+ }).trim();
1851
+ }
1852
+ function deriveBranchName(issueNumber, title) {
1853
+ const slug = title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").slice(0, 50).replace(/-$/, "");
1854
+ return `${issueNumber}-${slug}`;
1855
+ }
1856
+ function getDefaultBranch(cwd) {
1857
+ try {
1858
+ const config = getProjectConfig();
1859
+ if (config.git?.defaultBranch) {
1860
+ return config.git.defaultBranch;
1861
+ }
1862
+ } catch {
1027
1863
  }
1028
- }
1029
- function createPR(head, base, title, body) {
1030
1864
  try {
1031
- const output = gh(
1032
- [
1033
- "pr",
1034
- "create",
1035
- "--head",
1036
- head,
1037
- "--base",
1038
- base,
1039
- "--title",
1040
- title,
1041
- "--body-file",
1042
- "-"
1043
- ],
1044
- { input: body }
1045
- );
1046
- const url = output.trim();
1047
- const match = url.match(/\/pull\/(\d+)$/);
1048
- const number = match ? parseInt(match[1], 10) : 0;
1049
- logger.info(` PR created: ${url}`);
1050
- return { number, url };
1051
- } catch (err) {
1052
- const reason = ghErrorMessage(err);
1053
- logger.error(` Failed to create PR: ${reason}`);
1054
- return null;
1865
+ const ref = git(["symbolic-ref", "refs/remotes/origin/HEAD"], { cwd });
1866
+ return ref.replace("refs/remotes/origin/", "");
1867
+ } catch {
1868
+ }
1869
+ try {
1870
+ const output = git(["remote", "show", "origin"], { cwd, timeout: 1e4 });
1871
+ const match = output.match(/HEAD branch:\s*(\S+)/);
1872
+ if (match) return match[1];
1873
+ } catch {
1055
1874
  }
1875
+ return "dev";
1056
1876
  }
1057
- function setLifecycleLabel(issueNumber, phase) {
1058
- if (!LIFECYCLE_LABELS.includes(phase)) {
1059
- logger.warn(` Invalid lifecycle phase: ${phase}`);
1060
- return;
1877
+ function getCurrentBranch(cwd) {
1878
+ return git(["branch", "--show-current"], { cwd });
1879
+ }
1880
+ function ensureFeatureBranch(issueNumber, title, cwd) {
1881
+ const current = getCurrentBranch(cwd);
1882
+ const branchName = deriveBranchName(issueNumber, title);
1883
+ if (current === branchName || current.startsWith(`${issueNumber}-`)) {
1884
+ logger.info(` Already on feature branch: ${current}`);
1885
+ return current;
1061
1886
  }
1062
- const othersToRemove = LIFECYCLE_LABELS.filter((l) => l !== phase).map((l) => `kody:${l}`).join(",");
1063
- if (othersToRemove) {
1887
+ if (!BASE_BRANCHES.includes(current) && current !== "") {
1888
+ const defaultBranch2 = getDefaultBranch(cwd);
1889
+ logger.info(` Switching from ${current} to ${defaultBranch2} before creating ${branchName}`);
1064
1890
  try {
1065
- gh(["issue", "edit", String(issueNumber), "--remove-label", othersToRemove]);
1891
+ git(["checkout", defaultBranch2], { cwd });
1066
1892
  } catch {
1893
+ logger.warn(` Failed to checkout ${defaultBranch2}, aborting branch creation`);
1894
+ return current;
1067
1895
  }
1068
1896
  }
1069
- setLabel(issueNumber, `kody:${phase}`);
1070
- }
1071
- function getPRsForIssue(issueNumber) {
1072
1897
  try {
1073
- const output = gh([
1074
- "pr",
1075
- "list",
1076
- "--search",
1077
- `${issueNumber} in:body`,
1078
- "--json",
1079
- "number,title,url,headRefName",
1080
- "--state",
1081
- "open"
1082
- ]);
1083
- const prs = JSON.parse(output);
1084
- const branchPrs = (() => {
1085
- try {
1086
- const branchOutput = gh([
1087
- "pr",
1088
- "list",
1089
- "--json",
1090
- "number,title,url,headRefName",
1091
- "--state",
1092
- "open"
1093
- ]);
1094
- return JSON.parse(branchOutput).filter((pr) => pr.headRefName.startsWith(`${issueNumber}-`));
1095
- } catch {
1096
- return [];
1097
- }
1098
- })();
1099
- const seen = /* @__PURE__ */ new Set();
1100
- const merged = [];
1101
- for (const pr of [...prs, ...branchPrs]) {
1102
- if (!seen.has(pr.number)) {
1103
- seen.add(pr.number);
1104
- merged.push({ number: pr.number, title: pr.title, url: pr.url, headBranch: pr.headRefName });
1105
- }
1106
- }
1107
- return merged;
1898
+ git(["fetch", "origin"], { cwd, timeout: 3e4 });
1108
1899
  } catch (err) {
1109
- logger.error(` Failed to get PRs for issue #${issueNumber}: ${err}`);
1110
- return [];
1900
+ const msg = err instanceof Error ? err.message : String(err);
1901
+ logger.warn(` Failed to fetch origin: ${msg}`);
1902
+ }
1903
+ try {
1904
+ git(["rev-parse", "--verify", `origin/${branchName}`], { cwd });
1905
+ git(["checkout", branchName], { cwd });
1906
+ git(["pull", "origin", branchName], { cwd, timeout: 3e4 });
1907
+ logger.info(` Checked out existing remote branch: ${branchName}`);
1908
+ return branchName;
1909
+ } catch {
1910
+ }
1911
+ try {
1912
+ git(["rev-parse", "--verify", branchName], { cwd });
1913
+ git(["checkout", branchName], { cwd });
1914
+ logger.info(` Checked out existing local branch: ${branchName}`);
1915
+ return branchName;
1916
+ } catch {
1917
+ }
1918
+ const defaultBranch = getDefaultBranch(cwd);
1919
+ try {
1920
+ git(["checkout", "-b", branchName, `origin/${defaultBranch}`], { cwd });
1921
+ } catch {
1922
+ git(["checkout", "-b", branchName], { cwd });
1111
1923
  }
1924
+ logger.info(` Created new branch: ${branchName}`);
1925
+ return branchName;
1112
1926
  }
1113
- function getPRDetails(prNumber) {
1927
+ function syncWithDefault(cwd, branch) {
1928
+ const defaultBranch = branch ?? getDefaultBranch(cwd);
1929
+ const current = getCurrentBranch(cwd);
1930
+ if (current === defaultBranch) return;
1114
1931
  try {
1115
- const output = gh([
1116
- "pr",
1117
- "view",
1118
- String(prNumber),
1119
- "--json",
1120
- "title,body,headRefName,baseRefName"
1121
- ]);
1122
- const data = JSON.parse(output);
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
- };
1932
+ git(["fetch", "origin", defaultBranch], { cwd, timeout: 3e4 });
1133
1933
  } catch (err) {
1134
- if (isNotFoundError(err)) {
1135
- logger.info(` PR #${prNumber} not found`);
1136
- } else {
1137
- logger.error(` Failed to get PR #${prNumber}: ${ghErrorMessage(err)}`);
1934
+ const msg = err instanceof Error ? err.message : String(err);
1935
+ logger.warn(` Failed to fetch latest from origin: ${msg}`);
1936
+ return;
1937
+ }
1938
+ try {
1939
+ git(["merge", `origin/${defaultBranch}`, "--no-edit"], { cwd, timeout: 3e4 });
1940
+ logger.info(` Synced with origin/${defaultBranch}`);
1941
+ } catch {
1942
+ try {
1943
+ git(["merge", "--abort"], { cwd });
1944
+ } catch (abortErr) {
1945
+ logger.warn(` Failed to abort merge: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}`);
1138
1946
  }
1139
- return null;
1947
+ logger.warn(` Merge conflict with origin/${defaultBranch} \u2014 skipping sync`);
1140
1948
  }
1141
1949
  }
1142
- function postPRComment(prNumber, body) {
1950
+ function mergeDefault(cwd) {
1951
+ const defaultBranch = getDefaultBranch(cwd);
1952
+ const current = getCurrentBranch(cwd);
1953
+ if (current === defaultBranch) return "clean";
1143
1954
  try {
1144
- gh(
1145
- ["pr", "comment", String(prNumber), "--body-file", "-"],
1146
- { input: body }
1147
- );
1148
- logger.info(` Comment posted on PR #${prNumber}`);
1955
+ git(["fetch", "origin", defaultBranch], { cwd, timeout: 3e4 });
1149
1956
  } catch (err) {
1150
- logger.warn(` Failed to post PR comment: ${err}`);
1957
+ const msg = err instanceof Error ? err.message : String(err);
1958
+ logger.warn(` Failed to fetch latest from origin: ${msg}`);
1959
+ return "error";
1151
1960
  }
1152
- }
1153
- function submitPRReview(prNumber, body, event) {
1154
- const flag = event === "approve" ? "--approve" : "--request-changes";
1155
1961
  try {
1156
- gh(
1157
- ["pr", "review", String(prNumber), flag, "--body-file", "-"],
1158
- { input: body }
1159
- );
1160
- logger.info(` PR review submitted on #${prNumber}: ${event}`);
1161
- return true;
1162
- } catch (err) {
1163
- logger.warn(` Failed to submit PR review: ${err}`);
1164
- return false;
1962
+ git(["merge", `origin/${defaultBranch}`, "--no-edit"], { cwd, timeout: 3e4 });
1963
+ logger.info(` Merged origin/${defaultBranch} cleanly`);
1964
+ return "clean";
1965
+ } catch {
1966
+ try {
1967
+ const unmerged = git(["diff", "--name-only", "--diff-filter=U"], { cwd });
1968
+ if (unmerged.trim()) return "conflict";
1969
+ } catch {
1970
+ }
1971
+ try {
1972
+ git(["merge", "--abort"], { cwd });
1973
+ } catch (abortErr) {
1974
+ logger.warn(` Failed to abort merge: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}`);
1975
+ }
1976
+ return "error";
1165
1977
  }
1166
1978
  }
1167
- function getCIFailureLogs(runId, maxLength = 8e3) {
1979
+ function getConflictedFiles(cwd) {
1168
1980
  try {
1169
- const logsOutput = gh([
1170
- "run",
1171
- "view",
1172
- String(runId),
1173
- "--log-failed"
1174
- ]);
1175
- if (!logsOutput) return null;
1176
- const truncated = logsOutput.slice(-maxLength);
1177
- const prefix = logsOutput.length > maxLength ? "...(earlier output truncated)\n" : "";
1178
- return `${prefix}${truncated}`;
1179
- } catch (err) {
1180
- logger.warn(` Failed to get CI failure logs for run ${runId}: ${ghErrorMessage(err)}`);
1181
- return null;
1981
+ const output = git(["diff", "--name-only", "--diff-filter=U"], { cwd });
1982
+ return output ? output.split("\n") : [];
1983
+ } catch {
1984
+ return [];
1182
1985
  }
1183
1986
  }
1184
- function getLatestFailedRunForBranch(branch) {
1185
- try {
1186
- const output = gh([
1187
- "run",
1188
- "list",
1189
- "--branch",
1190
- branch,
1191
- "--status",
1192
- "failure",
1193
- "--limit",
1194
- "1",
1195
- "--json",
1196
- "databaseId",
1197
- "--jq",
1198
- ".[0].databaseId"
1199
- ]);
1200
- return output.trim() || null;
1201
- } catch (err) {
1202
- logger.warn(` Failed to get latest failed run for branch ${branch}: ${ghErrorMessage(err)}`);
1203
- return null;
1987
+ function commitAll(message, cwd) {
1988
+ const status = git(["status", "--porcelain"], { cwd });
1989
+ if (!status) {
1990
+ return { success: false, hash: "", message: "No changes to commit" };
1204
1991
  }
1992
+ git(["add", "."], { cwd });
1993
+ git(["commit", "--no-gpg-sign", "-m", message], { cwd });
1994
+ const hash = git(["rev-parse", "HEAD"], { cwd }).slice(0, 7);
1995
+ logger.info(` Committed: ${hash} ${message}`);
1996
+ return { success: true, hash, message };
1205
1997
  }
1206
- function getLatestKodyReviewComment(prNumber) {
1998
+ function getDiffFiles(baseBranch, cwd) {
1207
1999
  try {
1208
- const output = gh([
1209
- "api",
1210
- `repos/{owner}/{repo}/issues/${prNumber}/comments`,
1211
- "--jq",
1212
- '[.[] | select(.body | test("Kody Review"))] | last | .body'
1213
- ]);
1214
- return output.trim() || null;
2000
+ const output = git(["diff", "--name-only", `origin/${baseBranch}...HEAD`], { cwd });
2001
+ if (!output) return [];
2002
+ return output.split("\n").filter((f) => f && !f.startsWith(".kody/"));
1215
2003
  } catch (err) {
1216
- logger.warn(` Failed to get review comments for PR #${prNumber}: ${err}`);
1217
- return null;
2004
+ const msg = err instanceof Error ? err.message : String(err);
2005
+ logger.warn(` Failed to get diff files: ${msg}`);
2006
+ return [];
1218
2007
  }
1219
2008
  }
1220
- function getPRFeedbackSinceLastKodyAction(prNumber) {
2009
+ function pushBranch(cwd) {
1221
2010
  try {
1222
- const issueCommentsRaw = gh([
1223
- "api",
1224
- `repos/{owner}/{repo}/issues/${prNumber}/comments`,
1225
- "--jq",
1226
- "[.[] | {body, created_at, user_login: .user.login, user_type: .user.type}]"
1227
- ]);
1228
- const issueComments = issueCommentsRaw ? JSON.parse(issueCommentsRaw) : [];
1229
- const reviewCommentsRaw = gh([
1230
- "api",
1231
- `repos/{owner}/{repo}/pulls/${prNumber}/comments`,
1232
- "--jq",
1233
- "[.[] | {body, created_at, user_login: .user.login, user_type: .user.type, path, line}]"
1234
- ]);
1235
- const reviewComments = reviewCommentsRaw ? JSON.parse(reviewCommentsRaw) : [];
1236
- const kodyTimestamp = findLastKodyActionTimestamp(issueComments);
1237
- const humanIssueComments = issueComments.filter(
1238
- (c) => !isKodyComment(c) && (!kodyTimestamp || c.created_at > kodyTimestamp)
1239
- );
1240
- const humanReviewComments = reviewComments.filter(
1241
- (c) => !isKodyComment(c) && (!kodyTimestamp || c.created_at > kodyTimestamp)
1242
- );
1243
- if (humanIssueComments.length === 0 && humanReviewComments.length === 0) {
1244
- return null;
1245
- }
1246
- const parts = [];
1247
- if (humanIssueComments.length > 0) {
1248
- parts.push("### PR Comments");
1249
- for (const c of humanIssueComments) {
1250
- parts.push(`**@${c.user_login}:**
1251
- ${c.body}`);
1252
- }
1253
- }
1254
- if (humanReviewComments.length > 0) {
1255
- parts.push("### Code Review Comments");
1256
- for (const c of humanReviewComments) {
1257
- const location = c.path ? `\`${c.path}${c.line ? `:${c.line}` : ""}\`` : "";
1258
- parts.push(`**@${c.user_login}** ${location}:
1259
- ${c.body}`);
1260
- }
1261
- }
1262
- return parts.join("\n\n");
1263
- } catch (err) {
1264
- logger.warn(` Failed to get PR feedback for #${prNumber}: ${err}`);
1265
- return null;
2011
+ git(["push", "-u", "origin", "HEAD"], { cwd, timeout: 12e4 });
2012
+ } catch {
2013
+ logger.info(" Push rejected (non-fast-forward), retrying with --force-with-lease");
2014
+ git(["push", "--force-with-lease", "-u", "origin", "HEAD"], { cwd, timeout: 12e4 });
1266
2015
  }
2016
+ logger.info(" Pushed to origin");
1267
2017
  }
1268
- function isKodyComment(comment) {
1269
- if (comment.user_type === "Bot") return true;
1270
- return KODY_MARKERS.some((marker) => comment.body.includes(marker));
1271
- }
1272
- function findLastKodyActionTimestamp(comments) {
1273
- const kodyComments = comments.filter(isKodyComment);
1274
- if (kodyComments.length === 0) return null;
1275
- return kodyComments[kodyComments.length - 1].created_at;
1276
- }
1277
- var API_TIMEOUT_MS, LIFECYCLE_LABELS, _ghCwd, KODY_MARKERS;
1278
- var init_github_api = __esm({
1279
- "src/github-api.ts"() {
2018
+ var BASE_BRANCHES, _hookSafeEnv;
2019
+ var init_git_utils = __esm({
2020
+ "src/git-utils.ts"() {
1280
2021
  "use strict";
1281
2022
  init_logger();
1282
- API_TIMEOUT_MS = 3e4;
1283
- LIFECYCLE_LABELS = ["planning", "building", "review", "shipping", "done", "failed", "waiting", "low", "medium", "high"];
1284
- KODY_MARKERS = [
1285
- "Kody Review",
1286
- "\u{1F916} Generated by Kody",
1287
- "Kody pipeline started",
1288
- "Fix pushed to PR",
1289
- "PR created:",
1290
- "Pipeline failed at",
1291
- "Pipeline already running",
1292
- "already completed"
1293
- ];
2023
+ init_config();
2024
+ BASE_BRANCHES = ["dev", "main", "master"];
2025
+ _hookSafeEnv = null;
1294
2026
  }
1295
2027
  });
1296
2028
 
1297
2029
  // src/pipeline/state.ts
1298
- import * as fs10 from "fs";
1299
- import * as path8 from "path";
2030
+ import * as fs13 from "fs";
2031
+ import * as path11 from "path";
1300
2032
  function loadState(taskId, taskDir) {
1301
- const p = path8.join(taskDir, "status.json");
1302
- if (!fs10.existsSync(p)) return null;
2033
+ const p = path11.join(taskDir, "status.json");
2034
+ if (!fs13.existsSync(p)) return null;
1303
2035
  try {
1304
2036
  const result = parseJsonSafe(
1305
- fs10.readFileSync(p, "utf-8"),
2037
+ fs13.readFileSync(p, "utf-8"),
1306
2038
  ["taskId", "state", "stages", "createdAt", "updatedAt"]
1307
2039
  );
1308
2040
  if (!result.ok) {
@@ -1320,10 +2052,10 @@ function writeState(state, taskDir) {
1320
2052
  ...state,
1321
2053
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1322
2054
  };
1323
- const target = path8.join(taskDir, "status.json");
2055
+ const target = path11.join(taskDir, "status.json");
1324
2056
  const tmp = target + ".tmp";
1325
- fs10.writeFileSync(tmp, JSON.stringify(updated, null, 2));
1326
- fs10.renameSync(tmp, target);
2057
+ fs13.writeFileSync(tmp, JSON.stringify(updated, null, 2));
2058
+ fs13.renameSync(tmp, target);
1327
2059
  return updated;
1328
2060
  }
1329
2061
  function initState(taskId) {
@@ -1364,16 +2096,16 @@ var init_complexity = __esm({
1364
2096
  });
1365
2097
 
1366
2098
  // src/memory.ts
1367
- import * as fs11 from "fs";
1368
- import * as path9 from "path";
2099
+ import * as fs14 from "fs";
2100
+ import * as path12 from "path";
1369
2101
  function readProjectMemory(projectDir) {
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();
2102
+ const memoryDir = path12.join(projectDir, ".kody", "memory");
2103
+ if (!fs14.existsSync(memoryDir)) return "";
2104
+ const files = fs14.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
1373
2105
  if (files.length === 0) return "";
1374
2106
  const sections = [];
1375
2107
  for (const file of files) {
1376
- const content = fs11.readFileSync(path9.join(memoryDir, file), "utf-8").trim();
2108
+ const content = fs14.readFileSync(path12.join(memoryDir, file), "utf-8").trim();
1377
2109
  if (content) {
1378
2110
  sections.push(`## ${file.replace(".md", "")}
1379
2111
  ${content}`);
@@ -1392,8 +2124,8 @@ var init_memory = __esm({
1392
2124
  });
1393
2125
 
1394
2126
  // src/context-tiers.ts
1395
- import * as fs12 from "fs";
1396
- import * as path10 from "path";
2127
+ import * as fs15 from "fs";
2128
+ import * as path13 from "path";
1397
2129
  function estimateTokens(text) {
1398
2130
  return Math.ceil(text.length / 4);
1399
2131
  }
@@ -1484,7 +2216,7 @@ function generateL1Json(content) {
1484
2216
  }
1485
2217
  }
1486
2218
  function getTieredContent(filePath, content) {
1487
- const key = path10.basename(filePath);
2219
+ const key = path13.basename(filePath);
1488
2220
  return {
1489
2221
  source: filePath,
1490
2222
  L0: generateL0(content, key),
@@ -1496,15 +2228,15 @@ function selectTier(tiered, tier) {
1496
2228
  return tiered[tier];
1497
2229
  }
1498
2230
  function readProjectMemoryTiered(projectDir, tier) {
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();
2231
+ const memoryDir = path13.join(projectDir, ".kody", "memory");
2232
+ if (!fs15.existsSync(memoryDir)) return "";
2233
+ const files = fs15.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
1502
2234
  if (files.length === 0) return "";
1503
2235
  const tierLabel2 = tier === "L2" ? "full" : tier === "L1" ? "overview" : "abstract";
1504
2236
  const sections = [];
1505
2237
  for (const file of files) {
1506
- const filePath = path10.join(memoryDir, file);
1507
- const content = fs12.readFileSync(filePath, "utf-8").trim();
2238
+ const filePath = path13.join(memoryDir, file);
2239
+ const content = fs15.readFileSync(filePath, "utf-8").trim();
1508
2240
  if (!content) continue;
1509
2241
  const tiered = getTieredContent(filePath, content);
1510
2242
  const selected = selectTier(tiered, tier);
@@ -1527,9 +2259,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
1527
2259
  `;
1528
2260
  context += `Task Directory: ${taskDir}
1529
2261
  `;
1530
- const taskMdPath = path10.join(taskDir, "task.md");
1531
- if (fs12.existsSync(taskMdPath)) {
1532
- const content = fs12.readFileSync(taskMdPath, "utf-8");
2262
+ const taskMdPath = path13.join(taskDir, "task.md");
2263
+ if (fs15.existsSync(taskMdPath)) {
2264
+ const content = fs15.readFileSync(taskMdPath, "utf-8");
1533
2265
  const selected = selectContent(taskMdPath, content, policy.taskDescription);
1534
2266
  const label = tierLabel("Task Description", policy.taskDescription);
1535
2267
  context += `
@@ -1537,9 +2269,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
1537
2269
  ${selected}
1538
2270
  `;
1539
2271
  }
1540
- const taskJsonPath = path10.join(taskDir, "task.json");
1541
- if (fs12.existsSync(taskJsonPath)) {
1542
- const content = fs12.readFileSync(taskJsonPath, "utf-8");
2272
+ const taskJsonPath = path13.join(taskDir, "task.json");
2273
+ if (fs15.existsSync(taskJsonPath)) {
2274
+ const content = fs15.readFileSync(taskJsonPath, "utf-8");
1543
2275
  if (policy.taskClassification === "L2") {
1544
2276
  try {
1545
2277
  const taskDef = JSON.parse(content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, ""));
@@ -1565,9 +2297,9 @@ ${selected}
1565
2297
  }
1566
2298
  }
1567
2299
  }
1568
- const specPath = path10.join(taskDir, "spec.md");
1569
- if (fs12.existsSync(specPath)) {
1570
- const content = fs12.readFileSync(specPath, "utf-8");
2300
+ const specPath = path13.join(taskDir, "spec.md");
2301
+ if (fs15.existsSync(specPath)) {
2302
+ const content = fs15.readFileSync(specPath, "utf-8");
1571
2303
  const selected = selectContent(specPath, content, policy.spec);
1572
2304
  const label = tierLabel("Spec", policy.spec);
1573
2305
  context += `
@@ -1575,9 +2307,9 @@ ${selected}
1575
2307
  ${selected}
1576
2308
  `;
1577
2309
  }
1578
- const planPath = path10.join(taskDir, "plan.md");
1579
- if (fs12.existsSync(planPath)) {
1580
- const content = fs12.readFileSync(planPath, "utf-8");
2310
+ const planPath = path13.join(taskDir, "plan.md");
2311
+ if (fs15.existsSync(planPath)) {
2312
+ const content = fs15.readFileSync(planPath, "utf-8");
1581
2313
  const selected = selectContent(planPath, content, policy.plan);
1582
2314
  const label = tierLabel("Plan", policy.plan);
1583
2315
  context += `
@@ -1585,9 +2317,9 @@ ${selected}
1585
2317
  ${selected}
1586
2318
  `;
1587
2319
  }
1588
- const contextMdPath = path10.join(taskDir, "context.md");
1589
- if (fs12.existsSync(contextMdPath)) {
1590
- const content = fs12.readFileSync(contextMdPath, "utf-8");
2320
+ const contextMdPath = path13.join(taskDir, "context.md");
2321
+ if (fs15.existsSync(contextMdPath)) {
2322
+ const content = fs15.readFileSync(contextMdPath, "utf-8");
1591
2323
  const selected = selectContent(contextMdPath, content, policy.accumulatedContext);
1592
2324
  const label = tierLabel("Previous Stage Context", policy.accumulatedContext);
1593
2325
  context += `
@@ -1672,71 +2404,25 @@ var init_context_tiers = __esm({
1672
2404
  }
1673
2405
  });
1674
2406
 
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
- }
1690
- function buildMcpConfigJson(mcpConfig) {
1691
- if (!mcpConfig?.enabled) return void 0;
1692
- if (Object.keys(mcpConfig.servers).length === 0) return void 0;
1693
- const config = { mcpServers: {} };
1694
- const mcpServers = config.mcpServers;
1695
- for (const [name, server] of Object.entries(mcpConfig.servers)) {
1696
- mcpServers[name] = {
1697
- command: server.command,
1698
- args: server.args ?? [],
1699
- ...server.env ? { env: server.env } : {}
1700
- };
1701
- }
1702
- return JSON.stringify(config);
1703
- }
1704
- function isMcpEnabledForStage(stageName, mcpConfig) {
1705
- if (!mcpConfig?.enabled) return false;
1706
- const allowedStages = mcpConfig.stages ?? DEFAULT_MCP_STAGES;
1707
- return allowedStages.includes(stageName);
1708
- }
1709
- var DEFAULT_MCP_STAGES, PLAYWRIGHT_SERVER;
1710
- var init_mcp_config = __esm({
1711
- "src/mcp-config.ts"() {
1712
- "use strict";
1713
- DEFAULT_MCP_STAGES = ["build", "verify", "review", "review-fix"];
1714
- PLAYWRIGHT_SERVER = {
1715
- command: "npx",
1716
- args: ["-y", "@anthropic-ai/mcp-playwright"]
1717
- };
1718
- }
1719
- });
1720
-
1721
2407
  // src/context.ts
1722
- import * as fs13 from "fs";
1723
- import * as path11 from "path";
2408
+ import * as fs16 from "fs";
2409
+ import * as path14 from "path";
1724
2410
  function readPromptFile(stageName, projectDir) {
1725
2411
  if (projectDir) {
1726
- const stepFile = path11.join(projectDir, ".kody", "steps", `${stageName}.md`);
1727
- if (fs13.existsSync(stepFile)) {
1728
- return fs13.readFileSync(stepFile, "utf-8");
2412
+ const stepFile = path14.join(projectDir, ".kody", "steps", `${stageName}.md`);
2413
+ if (fs16.existsSync(stepFile)) {
2414
+ return fs16.readFileSync(stepFile, "utf-8");
1729
2415
  }
1730
2416
  console.warn(` \u26A0 No step file at ${stepFile}, falling back to engine defaults. Run 'kody-engine-lite init --force' to generate step files.`);
1731
2417
  }
1732
2418
  const scriptDir = new URL(".", import.meta.url).pathname;
1733
2419
  const candidates = [
1734
- path11.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
1735
- path11.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
2420
+ path14.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
2421
+ path14.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
1736
2422
  ];
1737
2423
  for (const candidate of candidates) {
1738
- if (fs13.existsSync(candidate)) {
1739
- return fs13.readFileSync(candidate, "utf-8");
2424
+ if (fs16.existsSync(candidate)) {
2425
+ return fs16.readFileSync(candidate, "utf-8");
1740
2426
  }
1741
2427
  }
1742
2428
  throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
@@ -1748,18 +2434,18 @@ function injectTaskContext(prompt, taskId, taskDir, feedback) {
1748
2434
  `;
1749
2435
  context += `Task Directory: ${taskDir}
1750
2436
  `;
1751
- const taskMdPath = path11.join(taskDir, "task.md");
1752
- if (fs13.existsSync(taskMdPath)) {
1753
- const taskMd = fs13.readFileSync(taskMdPath, "utf-8");
2437
+ const taskMdPath = path14.join(taskDir, "task.md");
2438
+ if (fs16.existsSync(taskMdPath)) {
2439
+ const taskMd = fs16.readFileSync(taskMdPath, "utf-8");
1754
2440
  context += `
1755
2441
  ## Task Description
1756
2442
  ${taskMd}
1757
2443
  `;
1758
2444
  }
1759
- const taskJsonPath = path11.join(taskDir, "task.json");
1760
- if (fs13.existsSync(taskJsonPath)) {
2445
+ const taskJsonPath = path14.join(taskDir, "task.json");
2446
+ if (fs16.existsSync(taskJsonPath)) {
1761
2447
  try {
1762
- const taskDef = JSON.parse(fs13.readFileSync(taskJsonPath, "utf-8"));
2448
+ const taskDef = JSON.parse(fs16.readFileSync(taskJsonPath, "utf-8"));
1763
2449
  context += `
1764
2450
  ## Task Classification
1765
2451
  `;
@@ -1772,27 +2458,27 @@ ${taskMd}
1772
2458
  } catch {
1773
2459
  }
1774
2460
  }
1775
- const specPath = path11.join(taskDir, "spec.md");
1776
- if (fs13.existsSync(specPath)) {
1777
- const spec = fs13.readFileSync(specPath, "utf-8");
2461
+ const specPath = path14.join(taskDir, "spec.md");
2462
+ if (fs16.existsSync(specPath)) {
2463
+ const spec = fs16.readFileSync(specPath, "utf-8");
1778
2464
  const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
1779
2465
  context += `
1780
2466
  ## Spec Summary
1781
2467
  ${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
1782
2468
  `;
1783
2469
  }
1784
- const planPath = path11.join(taskDir, "plan.md");
1785
- if (fs13.existsSync(planPath)) {
1786
- const plan = fs13.readFileSync(planPath, "utf-8");
2470
+ const planPath = path14.join(taskDir, "plan.md");
2471
+ if (fs16.existsSync(planPath)) {
2472
+ const plan = fs16.readFileSync(planPath, "utf-8");
1787
2473
  const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
1788
2474
  context += `
1789
2475
  ## Plan Summary
1790
2476
  ${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
1791
2477
  `;
1792
2478
  }
1793
- const contextMdPath = path11.join(taskDir, "context.md");
1794
- if (fs13.existsSync(contextMdPath)) {
1795
- const accumulated = fs13.readFileSync(contextMdPath, "utf-8");
2479
+ const contextMdPath = path14.join(taskDir, "context.md");
2480
+ if (fs16.existsSync(contextMdPath)) {
2481
+ const accumulated = fs16.readFileSync(contextMdPath, "utf-8");
1796
2482
  const truncated = accumulated.slice(-MAX_ACCUMULATED_CONTEXT);
1797
2483
  const prefix = accumulated.length > MAX_ACCUMULATED_CONTEXT ? "...(earlier context truncated)\n" : "";
1798
2484
  context += `
@@ -1810,17 +2496,17 @@ ${feedback}
1810
2496
  }
1811
2497
  function inferHasUIFromScope(scope) {
1812
2498
  return scope.some((filePath) => {
1813
- const ext = path11.extname(filePath).toLowerCase();
2499
+ const ext = path14.extname(filePath).toLowerCase();
1814
2500
  if (UI_EXTENSIONS.has(ext)) return true;
1815
2501
  const normalized = filePath.replace(/\\/g, "/");
1816
2502
  return UI_PATH_SEGMENTS.some((seg) => normalized.includes(seg));
1817
2503
  });
1818
2504
  }
1819
2505
  function taskHasUI(taskDir) {
1820
- const taskJsonPath = path11.join(taskDir, "task.json");
1821
- if (!fs13.existsSync(taskJsonPath)) return true;
2506
+ const taskJsonPath = path14.join(taskDir, "task.json");
2507
+ if (!fs16.existsSync(taskJsonPath)) return true;
1822
2508
  try {
1823
- const taskDef = JSON.parse(fs13.readFileSync(taskJsonPath, "utf-8"));
2509
+ const taskDef = JSON.parse(fs16.readFileSync(taskJsonPath, "utf-8"));
1824
2510
  const scope = Array.isArray(taskDef.scope) ? taskDef.scope : [];
1825
2511
  if (scope.length === 0) return true;
1826
2512
  return inferHasUIFromScope(scope);
@@ -1942,9 +2628,9 @@ ${prompt}` : prompt;
1942
2628
  }
1943
2629
  if (isMcpEnabledForStage(stageName, config.mcp) && taskHasUI(taskDir)) {
1944
2630
  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();
2631
+ const qaGuidePath = path14.join(projectDir, ".kody", "qa-guide.md");
2632
+ if (fs16.existsSync(qaGuidePath)) {
2633
+ const qaGuide = fs16.readFileSync(qaGuidePath, "utf-8").trim();
1948
2634
  assembled = assembled + "\n\n" + qaGuide;
1949
2635
  }
1950
2636
  }
@@ -1977,7 +2663,7 @@ function resolveModel(modelTier, stageName) {
1977
2663
  if (mapped) return mapped;
1978
2664
  return DEFAULT_MODEL_MAP[modelTier] ?? "sonnet";
1979
2665
  }
1980
- var DEFAULT_MODEL_MAP, MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT, UI_EXTENSIONS, UI_PATH_SEGMENTS, TIER_ESCALATION;
2666
+ var MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT, UI_EXTENSIONS, UI_PATH_SEGMENTS, TIER_ESCALATION, DEFAULT_MODEL_MAP;
1981
2667
  var init_context = __esm({
1982
2668
  "src/context.ts"() {
1983
2669
  "use strict";
@@ -1985,11 +2671,6 @@ var init_context = __esm({
1985
2671
  init_config();
1986
2672
  init_context_tiers();
1987
2673
  init_mcp_config();
1988
- DEFAULT_MODEL_MAP = {
1989
- cheap: "haiku",
1990
- mid: "sonnet",
1991
- strong: "opus"
1992
- };
1993
2674
  MAX_TASK_CONTEXT_PLAN = 1500;
1994
2675
  MAX_TASK_CONTEXT_SPEC = 2e3;
1995
2676
  MAX_ACCUMULATED_CONTEXT = 4e3;
@@ -2016,6 +2697,11 @@ var init_context = __esm({
2016
2697
  mid: "strong",
2017
2698
  strong: "strong"
2018
2699
  };
2700
+ DEFAULT_MODEL_MAP = {
2701
+ cheap: "haiku",
2702
+ mid: "sonnet",
2703
+ strong: "opus"
2704
+ };
2019
2705
  }
2020
2706
  });
2021
2707
 
@@ -2039,8 +2725,8 @@ var init_runner_selection = __esm({
2039
2725
  });
2040
2726
 
2041
2727
  // src/stages/agent.ts
2042
- import * as fs14 from "fs";
2043
- import * as path12 from "path";
2728
+ import * as fs17 from "fs";
2729
+ import * as path15 from "path";
2044
2730
  function getSessionInfo(stageName, sessions) {
2045
2731
  const group = SESSION_GROUP[stageName];
2046
2732
  if (!group) return void 0;
@@ -2127,27 +2813,27 @@ async function executeAgentStage(ctx, def) {
2127
2813
  }
2128
2814
  const result = lastResult;
2129
2815
  if (def.outputFile && result.output) {
2130
- fs14.writeFileSync(path12.join(ctx.taskDir, def.outputFile), result.output);
2816
+ fs17.writeFileSync(path15.join(ctx.taskDir, def.outputFile), result.output);
2131
2817
  }
2132
2818
  if (def.outputFile) {
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);
2819
+ const outputPath = path15.join(ctx.taskDir, def.outputFile);
2820
+ if (!fs17.existsSync(outputPath)) {
2821
+ const ext = path15.extname(def.outputFile);
2822
+ const base = path15.basename(def.outputFile, ext);
2823
+ const files = fs17.readdirSync(ctx.taskDir);
2138
2824
  const variant = files.find(
2139
2825
  (f) => f.startsWith(base + "-") && f.endsWith(ext)
2140
2826
  );
2141
2827
  if (variant) {
2142
- fs14.renameSync(path12.join(ctx.taskDir, variant), outputPath);
2828
+ fs17.renameSync(path15.join(ctx.taskDir, variant), outputPath);
2143
2829
  logger.info(` Renamed variant ${variant} \u2192 ${def.outputFile}`);
2144
2830
  }
2145
2831
  }
2146
2832
  }
2147
2833
  if (def.outputFile) {
2148
- const outputPath = path12.join(ctx.taskDir, def.outputFile);
2149
- if (fs14.existsSync(outputPath)) {
2150
- const content = fs14.readFileSync(outputPath, "utf-8");
2834
+ const outputPath = path15.join(ctx.taskDir, def.outputFile);
2835
+ if (fs17.existsSync(outputPath)) {
2836
+ const content = fs17.readFileSync(outputPath, "utf-8");
2151
2837
  const validation = validateStageOutput(def.name, content);
2152
2838
  if (!validation.valid) {
2153
2839
  if (def.name === "taskify") {
@@ -2161,7 +2847,7 @@ async function executeAgentStage(ctx, def) {
2161
2847
  const stripped = stripFences(retryResult.output);
2162
2848
  const retryValidation = validateTaskJson(stripped);
2163
2849
  if (retryValidation.valid) {
2164
- fs14.writeFileSync(outputPath, retryResult.output);
2850
+ fs17.writeFileSync(outputPath, retryResult.output);
2165
2851
  logger.info(` taskify retry produced valid JSON`);
2166
2852
  } else {
2167
2853
  logger.warn(` taskify retry still invalid: ${retryValidation.error}`);
@@ -2174,7 +2860,7 @@ async function executeAgentStage(ctx, def) {
2174
2860
  risk_level: "low",
2175
2861
  questions: []
2176
2862
  }, null, 2);
2177
- fs14.writeFileSync(outputPath, fallback);
2863
+ fs17.writeFileSync(outputPath, fallback);
2178
2864
  logger.info(` taskify fallback: generated minimal task.json (risk_level=low)`);
2179
2865
  }
2180
2866
  }
@@ -2188,7 +2874,7 @@ async function executeAgentStage(ctx, def) {
2188
2874
  return { outcome: "completed", outputFile: def.outputFile, retries };
2189
2875
  }
2190
2876
  function appendStageContext(taskDir, stageName, output) {
2191
- const contextPath = path12.join(taskDir, "context.md");
2877
+ const contextPath = path15.join(taskDir, "context.md");
2192
2878
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19);
2193
2879
  let summary;
2194
2880
  if (output && output.trim()) {
@@ -2201,7 +2887,7 @@ function appendStageContext(taskDir, stageName, output) {
2201
2887
  ### ${stageName} (${timestamp2})
2202
2888
  ${summary}
2203
2889
  `;
2204
- fs14.appendFileSync(contextPath, entry);
2890
+ fs17.appendFileSync(contextPath, entry);
2205
2891
  }
2206
2892
  var SESSION_GROUP;
2207
2893
  var init_agent = __esm({
@@ -2224,7 +2910,7 @@ var init_agent = __esm({
2224
2910
  });
2225
2911
 
2226
2912
  // src/verify-runner.ts
2227
- import { execFileSync as execFileSync9 } from "child_process";
2913
+ import { execFileSync as execFileSync11 } from "child_process";
2228
2914
  function isExecError(err) {
2229
2915
  return typeof err === "object" && err !== null;
2230
2916
  }
@@ -2260,7 +2946,7 @@ function runCommand(cmd, cwd, timeout) {
2260
2946
  return { success: true, output: "", timedOut: false };
2261
2947
  }
2262
2948
  try {
2263
- const output = execFileSync9(parts[0], parts.slice(1), {
2949
+ const output = execFileSync11(parts[0], parts.slice(1), {
2264
2950
  cwd,
2265
2951
  timeout,
2266
2952
  encoding: "utf-8",
@@ -2331,7 +3017,7 @@ var init_verify_runner = __esm({
2331
3017
  });
2332
3018
 
2333
3019
  // src/observer.ts
2334
- import { execFileSync as execFileSync10 } from "child_process";
3020
+ import { execFileSync as execFileSync12 } from "child_process";
2335
3021
  async function diagnoseFailure(stageName, errorOutput, modifiedFiles, runner, model, options) {
2336
3022
  const context = [
2337
3023
  `Stage: ${stageName}`,
@@ -2391,13 +3077,13 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
2391
3077
  }
2392
3078
  function getModifiedFiles(projectDir) {
2393
3079
  try {
2394
- const staged = execFileSync10("git", ["diff", "--name-only", "--cached"], {
3080
+ const staged = execFileSync12("git", ["diff", "--name-only", "--cached"], {
2395
3081
  encoding: "utf-8",
2396
3082
  cwd: projectDir,
2397
3083
  timeout: 5e3,
2398
3084
  stdio: ["pipe", "pipe", "pipe"]
2399
3085
  }).trim();
2400
- const unstaged = execFileSync10("git", ["diff", "--name-only"], {
3086
+ const unstaged = execFileSync12("git", ["diff", "--name-only"], {
2401
3087
  encoding: "utf-8",
2402
3088
  cwd: projectDir,
2403
3089
  timeout: 5e3,
@@ -2440,8 +3126,8 @@ Error context:
2440
3126
  });
2441
3127
 
2442
3128
  // src/stages/gate.ts
2443
- import * as fs15 from "fs";
2444
- import * as path13 from "path";
3129
+ import * as fs18 from "fs";
3130
+ import * as path16 from "path";
2445
3131
  function executeGateStage(ctx, def) {
2446
3132
  if (ctx.input.dryRun) {
2447
3133
  logger.info(` [dry-run] skipping ${def.name}`);
@@ -2484,7 +3170,7 @@ ${output}
2484
3170
  `);
2485
3171
  }
2486
3172
  }
2487
- fs15.writeFileSync(path13.join(ctx.taskDir, "verify.md"), lines.join(""));
3173
+ fs18.writeFileSync(path16.join(ctx.taskDir, "verify.md"), lines.join(""));
2488
3174
  return {
2489
3175
  outcome: verifyResult.pass ? "completed" : "failed",
2490
3176
  retries: 0
@@ -2499,9 +3185,9 @@ var init_gate = __esm({
2499
3185
  });
2500
3186
 
2501
3187
  // src/stages/verify.ts
2502
- import * as fs16 from "fs";
2503
- import * as path14 from "path";
2504
- import { execFileSync as execFileSync11 } from "child_process";
3188
+ import * as fs19 from "fs";
3189
+ import * as path17 from "path";
3190
+ import { execFileSync as execFileSync13 } from "child_process";
2505
3191
  async function executeVerifyWithAutofix(ctx, def) {
2506
3192
  const maxAttempts = def.maxRetries ?? 2;
2507
3193
  for (let attempt = 0; attempt <= maxAttempts; attempt++) {
@@ -2511,8 +3197,8 @@ async function executeVerifyWithAutofix(ctx, def) {
2511
3197
  return { ...gateResult, retries: attempt };
2512
3198
  }
2513
3199
  if (attempt < maxAttempts) {
2514
- const verifyPath = path14.join(ctx.taskDir, "verify.md");
2515
- const errorOutput = fs16.existsSync(verifyPath) ? fs16.readFileSync(verifyPath, "utf-8") : "Unknown error";
3200
+ const verifyPath = path17.join(ctx.taskDir, "verify.md");
3201
+ const errorOutput = fs19.existsSync(verifyPath) ? fs19.readFileSync(verifyPath, "utf-8") : "Unknown error";
2516
3202
  const modifiedFiles = getModifiedFiles(ctx.projectDir);
2517
3203
  const defaultRunner = getRunnerForStage(ctx, "taskify");
2518
3204
  const diagConfig = getProjectConfig();
@@ -2555,7 +3241,7 @@ ${diagnosis.resolution}`);
2555
3241
  const parts = parseCommand(cmd);
2556
3242
  if (parts.length === 0) return;
2557
3243
  try {
2558
- execFileSync11(parts[0], parts.slice(1), {
3244
+ execFileSync13(parts[0], parts.slice(1), {
2559
3245
  stdio: "pipe",
2560
3246
  timeout: FIX_COMMAND_TIMEOUT_MS
2561
3247
  });
@@ -2580,100 +3266,36 @@ ${ctx.input.feedback ?? ""}`.trim()
2580
3266
  name: def.retryWithAgent,
2581
3267
  type: "agent",
2582
3268
  modelTier: "mid",
2583
- timeout: 3e5,
2584
- outputFile: void 0
2585
- });
2586
- }
2587
- }
2588
- }
2589
- return {
2590
- outcome: "failed",
2591
- retries: maxAttempts,
2592
- error: "Verification failed after autofix attempts"
2593
- };
2594
- }
2595
- var init_verify = __esm({
2596
- "src/stages/verify.ts"() {
2597
- "use strict";
2598
- init_context();
2599
- init_config();
2600
- init_verify_runner();
2601
- init_runner_selection();
2602
- init_github_api();
2603
- init_observer();
2604
- init_logger();
2605
- init_agent();
2606
- init_gate();
2607
- }
2608
- });
2609
-
2610
- // src/cli/task-resolution.ts
2611
- import * as fs17 from "fs";
2612
- import * as path15 from "path";
2613
- import { execFileSync as execFileSync12 } from "child_process";
2614
- function findLatestTaskForIssue(issueNumber, projectDir) {
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();
2618
- const prefix = `${issueNumber}-`;
2619
- const direct = allDirs.find((d) => d.startsWith(prefix));
2620
- if (direct) return direct;
2621
- try {
2622
- const branch = execFileSync12("git", ["branch", "--show-current"], {
2623
- encoding: "utf-8",
2624
- cwd: projectDir,
2625
- timeout: 5e3,
2626
- stdio: ["pipe", "pipe", "pipe"]
2627
- }).trim();
2628
- const branchIssueMatch = branch.match(/^(\d+)-/);
2629
- if (branchIssueMatch) {
2630
- const branchIssueNum = branchIssueMatch[1];
2631
- const branchPrefix = `${branchIssueNum}-`;
2632
- const fromBranch = allDirs.find((d) => d.startsWith(branchPrefix));
2633
- if (fromBranch) return fromBranch;
2634
- }
2635
- } catch {
2636
- }
2637
- return null;
2638
- }
2639
- function generateTaskId() {
2640
- const now = /* @__PURE__ */ new Date();
2641
- const pad = (n) => String(n).padStart(2, "0");
2642
- return `${String(now.getFullYear()).slice(2)}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
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];
3269
+ timeout: 3e5,
3270
+ outputFile: void 0
3271
+ });
2653
3272
  }
2654
3273
  }
2655
- return latestTaskId;
2656
- } catch {
2657
- return null;
2658
3274
  }
3275
+ return {
3276
+ outcome: "failed",
3277
+ retries: maxAttempts,
3278
+ error: "Verification failed after autofix attempts"
3279
+ };
2659
3280
  }
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
- }
2667
- var init_task_resolution = __esm({
2668
- "src/cli/task-resolution.ts"() {
3281
+ var init_verify = __esm({
3282
+ "src/stages/verify.ts"() {
2669
3283
  "use strict";
3284
+ init_context();
3285
+ init_config();
3286
+ init_verify_runner();
3287
+ init_runner_selection();
2670
3288
  init_github_api();
3289
+ init_observer();
3290
+ init_logger();
3291
+ init_agent();
3292
+ init_gate();
2671
3293
  }
2672
3294
  });
2673
3295
 
2674
3296
  // src/review-standalone.ts
2675
- import * as fs18 from "fs";
2676
- import * as path16 from "path";
3297
+ import * as fs20 from "fs";
3298
+ import * as path18 from "path";
2677
3299
  function resolveReviewTarget(input) {
2678
3300
  if (input.prs.length === 0) {
2679
3301
  return {
@@ -2697,8 +3319,8 @@ Or comment on the specific PR: \`@kody review\``
2697
3319
  }
2698
3320
  async function runStandaloneReview(input) {
2699
3321
  const taskId = input.taskId ?? `review-${generateTaskId()}`;
2700
- const taskDir = path16.join(input.projectDir, ".kody", "tasks", taskId);
2701
- fs18.mkdirSync(taskDir, { recursive: true });
3322
+ const taskDir = path18.join(input.projectDir, ".kody", "tasks", taskId);
3323
+ fs20.mkdirSync(taskDir, { recursive: true });
2702
3324
  let diffInstruction = "";
2703
3325
  let filesChangedSection = "";
2704
3326
  if (input.baseBranch) {
@@ -2725,7 +3347,7 @@ ${fileList}`;
2725
3347
  const taskContent = `# ${input.prTitle}
2726
3348
 
2727
3349
  ${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
2728
- fs18.writeFileSync(path16.join(taskDir, "task.md"), taskContent);
3350
+ fs20.writeFileSync(path18.join(taskDir, "task.md"), taskContent);
2729
3351
  const reviewDef = STAGES.find((s) => s.name === "review");
2730
3352
  const ctx = {
2731
3353
  taskId,
@@ -2747,10 +3369,10 @@ ${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
2747
3369
  error: result.error ?? "Review stage failed"
2748
3370
  };
2749
3371
  }
2750
- const reviewPath = path16.join(taskDir, "review.md");
3372
+ const reviewPath = path18.join(taskDir, "review.md");
2751
3373
  let reviewContent;
2752
- if (fs18.existsSync(reviewPath)) {
2753
- reviewContent = fs18.readFileSync(reviewPath, "utf-8");
3374
+ if (fs20.existsSync(reviewPath)) {
3375
+ reviewContent = fs20.readFileSync(reviewPath, "utf-8");
2754
3376
  }
2755
3377
  return {
2756
3378
  outcome: "completed",
@@ -2790,8 +3412,8 @@ var init_review_standalone = __esm({
2790
3412
  });
2791
3413
 
2792
3414
  // src/stages/review.ts
2793
- import * as fs19 from "fs";
2794
- import * as path17 from "path";
3415
+ import * as fs21 from "fs";
3416
+ import * as path19 from "path";
2795
3417
  async function executeReviewWithFix(ctx, def) {
2796
3418
  if (ctx.input.dryRun) {
2797
3419
  return { outcome: "completed", retries: 0 };
@@ -2805,11 +3427,11 @@ async function executeReviewWithFix(ctx, def) {
2805
3427
  if (reviewResult.outcome !== "completed") {
2806
3428
  return reviewResult;
2807
3429
  }
2808
- const reviewFile = path17.join(ctx.taskDir, "review.md");
2809
- if (!fs19.existsSync(reviewFile)) {
3430
+ const reviewFile = path19.join(ctx.taskDir, "review.md");
3431
+ if (!fs21.existsSync(reviewFile)) {
2810
3432
  return { outcome: "failed", retries: iteration, error: "review.md not found" };
2811
3433
  }
2812
- const content = fs19.readFileSync(reviewFile, "utf-8");
3434
+ const content = fs21.readFileSync(reviewFile, "utf-8");
2813
3435
  if (detectReviewVerdict(content) !== "fail") {
2814
3436
  return { ...reviewResult, retries: iteration };
2815
3437
  }
@@ -2838,15 +3460,15 @@ var init_review = __esm({
2838
3460
  });
2839
3461
 
2840
3462
  // src/stages/ship.ts
2841
- import * as fs20 from "fs";
2842
- import * as path18 from "path";
2843
- import { execFileSync as execFileSync13 } from "child_process";
3463
+ import * as fs22 from "fs";
3464
+ import * as path20 from "path";
3465
+ import { execFileSync as execFileSync14 } from "child_process";
2844
3466
  function buildPrBody(ctx) {
2845
3467
  const sections = [];
2846
- const taskJsonPath = path18.join(ctx.taskDir, "task.json");
2847
- if (fs20.existsSync(taskJsonPath)) {
3468
+ const taskJsonPath = path20.join(ctx.taskDir, "task.json");
3469
+ if (fs22.existsSync(taskJsonPath)) {
2848
3470
  try {
2849
- const raw = fs20.readFileSync(taskJsonPath, "utf-8");
3471
+ const raw = fs22.readFileSync(taskJsonPath, "utf-8");
2850
3472
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2851
3473
  const task = JSON.parse(cleaned);
2852
3474
  if (task.description) {
@@ -2865,9 +3487,9 @@ ${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
2865
3487
  } catch {
2866
3488
  }
2867
3489
  }
2868
- const reviewPath = path18.join(ctx.taskDir, "review.md");
2869
- if (fs20.existsSync(reviewPath)) {
2870
- const review = fs20.readFileSync(reviewPath, "utf-8");
3490
+ const reviewPath = path20.join(ctx.taskDir, "review.md");
3491
+ if (fs22.existsSync(reviewPath)) {
3492
+ const review = fs22.readFileSync(reviewPath, "utf-8");
2871
3493
  const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
2872
3494
  if (summaryMatch) {
2873
3495
  const summary = summaryMatch[1].trim();
@@ -2884,14 +3506,14 @@ ${summary}`);
2884
3506
  **Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
2885
3507
  }
2886
3508
  }
2887
- const verifyPath = path18.join(ctx.taskDir, "verify.md");
2888
- if (fs20.existsSync(verifyPath)) {
2889
- const verify = fs20.readFileSync(verifyPath, "utf-8");
3509
+ const verifyPath = path20.join(ctx.taskDir, "verify.md");
3510
+ if (fs22.existsSync(verifyPath)) {
3511
+ const verify = fs22.readFileSync(verifyPath, "utf-8");
2890
3512
  if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
2891
3513
  }
2892
- const planPath = path18.join(ctx.taskDir, "plan.md");
2893
- if (fs20.existsSync(planPath)) {
2894
- const plan = fs20.readFileSync(planPath, "utf-8").trim();
3514
+ const planPath = path20.join(ctx.taskDir, "plan.md");
3515
+ if (fs22.existsSync(planPath)) {
3516
+ const plan = fs22.readFileSync(planPath, "utf-8").trim();
2895
3517
  if (plan) {
2896
3518
  const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
2897
3519
  sections.push(`
@@ -2911,25 +3533,25 @@ Closes #${ctx.input.issueNumber}`);
2911
3533
  return sections.join("\n");
2912
3534
  }
2913
3535
  function executeShipStage(ctx, _def) {
2914
- const shipPath = path18.join(ctx.taskDir, "ship.md");
3536
+ const shipPath = path20.join(ctx.taskDir, "ship.md");
2915
3537
  if (ctx.input.dryRun) {
2916
- fs20.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
3538
+ fs22.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
2917
3539
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
2918
3540
  }
2919
3541
  if (ctx.input.local && !ctx.input.issueNumber) {
2920
- fs20.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
3542
+ fs22.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
2921
3543
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
2922
3544
  }
2923
3545
  try {
2924
3546
  const head = getCurrentBranch(ctx.projectDir);
2925
3547
  const base = getDefaultBranch(ctx.projectDir);
2926
3548
  try {
2927
- execFileSync13("git", ["add", ctx.taskDir], {
3549
+ execFileSync14("git", ["add", ctx.taskDir], {
2928
3550
  cwd: ctx.projectDir,
2929
3551
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
2930
3552
  stdio: "pipe"
2931
3553
  });
2932
- execFileSync13("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
3554
+ execFileSync14("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
2933
3555
  cwd: ctx.projectDir,
2934
3556
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
2935
3557
  stdio: "pipe"
@@ -2943,7 +3565,7 @@ function executeShipStage(ctx, _def) {
2943
3565
  let repo = config.github?.repo;
2944
3566
  if (!owner || !repo) {
2945
3567
  try {
2946
- const remoteUrl = execFileSync13("git", ["remote", "get-url", "origin"], {
3568
+ const remoteUrl = execFileSync14("git", ["remote", "get-url", "origin"], {
2947
3569
  encoding: "utf-8",
2948
3570
  cwd: ctx.projectDir
2949
3571
  }).trim();
@@ -2964,28 +3586,28 @@ function executeShipStage(ctx, _def) {
2964
3586
  chore: "chore"
2965
3587
  };
2966
3588
  let prefix = "chore";
2967
- const taskJsonPath = path18.join(ctx.taskDir, "task.json");
2968
- if (fs20.existsSync(taskJsonPath)) {
3589
+ const taskJsonPath = path20.join(ctx.taskDir, "task.json");
3590
+ if (fs22.existsSync(taskJsonPath)) {
2969
3591
  try {
2970
- const raw = fs20.readFileSync(taskJsonPath, "utf-8");
3592
+ const raw = fs22.readFileSync(taskJsonPath, "utf-8");
2971
3593
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2972
3594
  const task = JSON.parse(cleaned);
2973
3595
  prefix = TYPE_PREFIX[task.task_type] ?? "chore";
2974
3596
  } catch {
2975
3597
  }
2976
3598
  }
2977
- const taskMdPath = path18.join(ctx.taskDir, "task.md");
2978
- if (fs20.existsSync(taskMdPath)) {
2979
- const content = fs20.readFileSync(taskMdPath, "utf-8");
3599
+ const taskMdPath = path20.join(ctx.taskDir, "task.md");
3600
+ if (fs22.existsSync(taskMdPath)) {
3601
+ const content = fs22.readFileSync(taskMdPath, "utf-8");
2980
3602
  const heading = content.split("\n").find((l) => l.startsWith("# "));
2981
3603
  if (heading) {
2982
3604
  title = `${prefix}: ${heading.replace(/^#\s*/, "").trim()}`.slice(0, 72);
2983
3605
  }
2984
3606
  }
2985
3607
  if (title === "Update") {
2986
- if (fs20.existsSync(taskJsonPath)) {
3608
+ if (fs22.existsSync(taskJsonPath)) {
2987
3609
  try {
2988
- const raw = fs20.readFileSync(taskJsonPath, "utf-8");
3610
+ const raw = fs22.readFileSync(taskJsonPath, "utf-8");
2989
3611
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2990
3612
  const task = JSON.parse(cleaned);
2991
3613
  if (task.title) title = `${prefix}: ${task.title}`.slice(0, 72);
@@ -3008,7 +3630,7 @@ function executeShipStage(ctx, _def) {
3008
3630
  } catch {
3009
3631
  }
3010
3632
  }
3011
- fs20.writeFileSync(shipPath, `# Ship
3633
+ fs22.writeFileSync(shipPath, `# Ship
3012
3634
 
3013
3635
  Updated existing PR: ${existingPr.url}
3014
3636
  PR #${existingPr.number}
@@ -3029,20 +3651,20 @@ PR #${existingPr.number}
3029
3651
  } catch {
3030
3652
  }
3031
3653
  }
3032
- fs20.writeFileSync(shipPath, `# Ship
3654
+ fs22.writeFileSync(shipPath, `# Ship
3033
3655
 
3034
3656
  PR created: ${pr.url}
3035
3657
  PR #${pr.number}
3036
3658
  `);
3037
3659
  } else {
3038
- fs20.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
3660
+ fs22.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
3039
3661
  }
3040
3662
  }
3041
3663
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
3042
3664
  } catch (err) {
3043
3665
  const msg = err instanceof Error ? err.message : String(err);
3044
3666
  try {
3045
- fs20.writeFileSync(shipPath, `# Ship
3667
+ fs22.writeFileSync(shipPath, `# Ship
3046
3668
 
3047
3669
  Failed: ${msg}
3048
3670
  `);
@@ -3091,15 +3713,15 @@ var init_executor_registry = __esm({
3091
3713
  });
3092
3714
 
3093
3715
  // src/pipeline/questions.ts
3094
- import * as fs21 from "fs";
3095
- import * as path19 from "path";
3716
+ import * as fs23 from "fs";
3717
+ import * as path21 from "path";
3096
3718
  function checkForQuestions(ctx, stageName) {
3097
3719
  if (ctx.input.local || !ctx.input.issueNumber) return false;
3098
3720
  try {
3099
3721
  if (stageName === "taskify") {
3100
- const taskJsonPath = path19.join(ctx.taskDir, "task.json");
3101
- if (!fs21.existsSync(taskJsonPath)) return false;
3102
- const raw = fs21.readFileSync(taskJsonPath, "utf-8");
3722
+ const taskJsonPath = path21.join(ctx.taskDir, "task.json");
3723
+ if (!fs23.existsSync(taskJsonPath)) return false;
3724
+ const raw = fs23.readFileSync(taskJsonPath, "utf-8");
3103
3725
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3104
3726
  const taskJson = JSON.parse(cleaned);
3105
3727
  if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
@@ -3114,9 +3736,9 @@ Reply with \`@kody approve\` and your answers in the comment body.`;
3114
3736
  }
3115
3737
  }
3116
3738
  if (stageName === "plan") {
3117
- const planPath = path19.join(ctx.taskDir, "plan.md");
3118
- if (!fs21.existsSync(planPath)) return false;
3119
- const plan = fs21.readFileSync(planPath, "utf-8");
3739
+ const planPath = path21.join(ctx.taskDir, "plan.md");
3740
+ if (!fs23.existsSync(planPath)) return false;
3741
+ const plan = fs23.readFileSync(planPath, "utf-8");
3120
3742
  const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
3121
3743
  if (questionsMatch) {
3122
3744
  const questionsText = questionsMatch[1].trim();
@@ -3145,8 +3767,8 @@ var init_questions = __esm({
3145
3767
  });
3146
3768
 
3147
3769
  // src/pipeline/hooks.ts
3148
- import * as fs22 from "fs";
3149
- import * as path20 from "path";
3770
+ import * as fs24 from "fs";
3771
+ import * as path22 from "path";
3150
3772
  function applyPreStageLabel(ctx, def) {
3151
3773
  if (!ctx.input.issueNumber || ctx.input.local) return;
3152
3774
  if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
@@ -3184,9 +3806,9 @@ function autoDetectComplexity(ctx, def) {
3184
3806
  return { complexity, activeStages };
3185
3807
  }
3186
3808
  try {
3187
- const taskJsonPath = path20.join(ctx.taskDir, "task.json");
3188
- if (!fs22.existsSync(taskJsonPath)) return null;
3189
- const raw = fs22.readFileSync(taskJsonPath, "utf-8");
3809
+ const taskJsonPath = path22.join(ctx.taskDir, "task.json");
3810
+ if (!fs24.existsSync(taskJsonPath)) return null;
3811
+ const raw = fs24.readFileSync(taskJsonPath, "utf-8");
3190
3812
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3191
3813
  const taskJson = JSON.parse(cleaned);
3192
3814
  if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
@@ -3216,8 +3838,8 @@ function checkRiskGate(ctx, def, state, complexity) {
3216
3838
  if (ctx.input.dryRun || ctx.input.local) return null;
3217
3839
  if (ctx.input.mode === "rerun") return null;
3218
3840
  if (!ctx.input.issueNumber) return null;
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)";
3841
+ const planPath = path22.join(ctx.taskDir, "plan.md");
3842
+ const plan = fs24.existsSync(planPath) ? fs24.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
3221
3843
  try {
3222
3844
  postComment(
3223
3845
  ctx.input.issueNumber,
@@ -3284,22 +3906,22 @@ var init_hooks = __esm({
3284
3906
  });
3285
3907
 
3286
3908
  // src/learning/auto-learn.ts
3287
- import * as fs23 from "fs";
3288
- import * as path21 from "path";
3909
+ import * as fs25 from "fs";
3910
+ import * as path23 from "path";
3289
3911
  function stripAnsi(str) {
3290
3912
  return str.replace(/\x1b\[[0-9;]*m/g, "");
3291
3913
  }
3292
3914
  function autoLearn(ctx) {
3293
3915
  try {
3294
- const memoryDir = path21.join(ctx.projectDir, ".kody", "memory");
3295
- if (!fs23.existsSync(memoryDir)) {
3296
- fs23.mkdirSync(memoryDir, { recursive: true });
3916
+ const memoryDir = path23.join(ctx.projectDir, ".kody", "memory");
3917
+ if (!fs25.existsSync(memoryDir)) {
3918
+ fs25.mkdirSync(memoryDir, { recursive: true });
3297
3919
  }
3298
3920
  const learnings = [];
3299
3921
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3300
- const verifyPath = path21.join(ctx.taskDir, "verify.md");
3301
- if (fs23.existsSync(verifyPath)) {
3302
- const verify = stripAnsi(fs23.readFileSync(verifyPath, "utf-8"));
3922
+ const verifyPath = path23.join(ctx.taskDir, "verify.md");
3923
+ if (fs25.existsSync(verifyPath)) {
3924
+ const verify = stripAnsi(fs25.readFileSync(verifyPath, "utf-8"));
3303
3925
  if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
3304
3926
  if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
3305
3927
  if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
@@ -3308,18 +3930,18 @@ function autoLearn(ctx) {
3308
3930
  if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
3309
3931
  if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
3310
3932
  }
3311
- const reviewPath = path21.join(ctx.taskDir, "review.md");
3312
- if (fs23.existsSync(reviewPath)) {
3313
- const review = fs23.readFileSync(reviewPath, "utf-8");
3933
+ const reviewPath = path23.join(ctx.taskDir, "review.md");
3934
+ if (fs25.existsSync(reviewPath)) {
3935
+ const review = fs25.readFileSync(reviewPath, "utf-8");
3314
3936
  if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
3315
3937
  if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
3316
3938
  if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
3317
3939
  if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
3318
3940
  }
3319
- const taskJsonPath = path21.join(ctx.taskDir, "task.json");
3320
- if (fs23.existsSync(taskJsonPath)) {
3941
+ const taskJsonPath = path23.join(ctx.taskDir, "task.json");
3942
+ if (fs25.existsSync(taskJsonPath)) {
3321
3943
  try {
3322
- const raw = stripAnsi(fs23.readFileSync(taskJsonPath, "utf-8"));
3944
+ const raw = stripAnsi(fs25.readFileSync(taskJsonPath, "utf-8"));
3323
3945
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3324
3946
  const task = JSON.parse(cleaned);
3325
3947
  if (task.scope && Array.isArray(task.scope)) {
@@ -3330,12 +3952,12 @@ function autoLearn(ctx) {
3330
3952
  }
3331
3953
  }
3332
3954
  if (learnings.length > 0) {
3333
- const conventionsPath = path21.join(memoryDir, "conventions.md");
3955
+ const conventionsPath = path23.join(memoryDir, "conventions.md");
3334
3956
  const entry = `
3335
3957
  ## Learned ${timestamp2} (task: ${ctx.taskId})
3336
3958
  ${learnings.join("\n")}
3337
3959
  `;
3338
- fs23.appendFileSync(conventionsPath, entry);
3960
+ fs25.appendFileSync(conventionsPath, entry);
3339
3961
  logger.info(`Auto-learned ${learnings.length} convention(s)`);
3340
3962
  }
3341
3963
  autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
@@ -3343,8 +3965,8 @@ ${learnings.join("\n")}
3343
3965
  }
3344
3966
  }
3345
3967
  function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
3346
- const archPath = path21.join(memoryDir, "architecture.md");
3347
- if (fs23.existsSync(archPath)) return;
3968
+ const archPath = path23.join(memoryDir, "architecture.md");
3969
+ if (fs25.existsSync(archPath)) return;
3348
3970
  const detected = detectArchitectureBasic(projectDir);
3349
3971
  if (detected.length > 0) {
3350
3972
  const content = `# Architecture (auto-detected ${timestamp2})
@@ -3352,7 +3974,7 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
3352
3974
  ## Overview
3353
3975
  ${detected.join("\n")}
3354
3976
  `;
3355
- fs23.writeFileSync(archPath, content);
3977
+ fs25.writeFileSync(archPath, content);
3356
3978
  logger.info(`Auto-detected architecture (${detected.length} items)`);
3357
3979
  }
3358
3980
  }
@@ -3365,13 +3987,13 @@ var init_auto_learn = __esm({
3365
3987
  });
3366
3988
 
3367
3989
  // src/retrospective.ts
3368
- import * as fs24 from "fs";
3369
- import * as path22 from "path";
3990
+ import * as fs26 from "fs";
3991
+ import * as path24 from "path";
3370
3992
  function readArtifact(taskDir, filename, maxChars) {
3371
- const p = path22.join(taskDir, filename);
3372
- if (!fs24.existsSync(p)) return null;
3993
+ const p = path24.join(taskDir, filename);
3994
+ if (!fs26.existsSync(p)) return null;
3373
3995
  try {
3374
- const content = fs24.readFileSync(p, "utf-8");
3996
+ const content = fs26.readFileSync(p, "utf-8");
3375
3997
  return content.length > maxChars ? content.slice(0, maxChars) + "\n...(truncated)" : content;
3376
3998
  } catch {
3377
3999
  return null;
@@ -3424,13 +4046,13 @@ function collectRunContext(ctx, state, pipelineStartTime) {
3424
4046
  return lines.join("\n");
3425
4047
  }
3426
4048
  function getLogPath(projectDir) {
3427
- return path22.join(projectDir, ".kody", "memory", "observer-log.jsonl");
4049
+ return path24.join(projectDir, ".kody", "memory", "observer-log.jsonl");
3428
4050
  }
3429
4051
  function readPreviousRetrospectives(projectDir, limit = 10) {
3430
4052
  const logPath = getLogPath(projectDir);
3431
- if (!fs24.existsSync(logPath)) return [];
4053
+ if (!fs26.existsSync(logPath)) return [];
3432
4054
  try {
3433
- const content = fs24.readFileSync(logPath, "utf-8");
4055
+ const content = fs26.readFileSync(logPath, "utf-8");
3434
4056
  const lines = content.split("\n").filter(Boolean);
3435
4057
  const entries = [];
3436
4058
  const start = Math.max(0, lines.length - limit);
@@ -3457,11 +4079,11 @@ function formatPreviousEntries(entries) {
3457
4079
  }
3458
4080
  function appendRetrospectiveEntry(projectDir, entry) {
3459
4081
  const logPath = getLogPath(projectDir);
3460
- const dir = path22.dirname(logPath);
3461
- if (!fs24.existsSync(dir)) {
3462
- fs24.mkdirSync(dir, { recursive: true });
4082
+ const dir = path24.dirname(logPath);
4083
+ if (!fs26.existsSync(dir)) {
4084
+ fs26.mkdirSync(dir, { recursive: true });
3463
4085
  }
3464
- fs24.appendFileSync(logPath, JSON.stringify(entry) + "\n");
4086
+ fs26.appendFileSync(logPath, JSON.stringify(entry) + "\n");
3465
4087
  }
3466
4088
  async function runRetrospective(ctx, state, pipelineStartTime) {
3467
4089
  if (ctx.input.dryRun) return;
@@ -3629,8 +4251,8 @@ var init_summary = __esm({
3629
4251
  });
3630
4252
 
3631
4253
  // src/pipeline.ts
3632
- import * as fs25 from "fs";
3633
- import * as path23 from "path";
4254
+ import * as fs27 from "fs";
4255
+ import * as path25 from "path";
3634
4256
  function ensureFeatureBranchIfNeeded(ctx) {
3635
4257
  if (ctx.input.dryRun) return;
3636
4258
  if (ctx.input.prNumber) {
@@ -3643,8 +4265,8 @@ function ensureFeatureBranchIfNeeded(ctx) {
3643
4265
  }
3644
4266
  if (!ctx.input.issueNumber) return;
3645
4267
  try {
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;
4268
+ const taskMdPath = path25.join(ctx.taskDir, "task.md");
4269
+ const title = fs27.existsSync(taskMdPath) ? fs27.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
3648
4270
  ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
3649
4271
  syncWithDefault(ctx.projectDir);
3650
4272
  } catch (err) {
@@ -3658,10 +4280,10 @@ function ensureFeatureBranchIfNeeded(ctx) {
3658
4280
  }
3659
4281
  }
3660
4282
  function acquireLock(taskDir) {
3661
- const lockPath = path23.join(taskDir, ".lock");
3662
- if (fs25.existsSync(lockPath)) {
4283
+ const lockPath = path25.join(taskDir, ".lock");
4284
+ if (fs27.existsSync(lockPath)) {
3663
4285
  try {
3664
- const pid = parseInt(fs25.readFileSync(lockPath, "utf-8").trim(), 10);
4286
+ const pid = parseInt(fs27.readFileSync(lockPath, "utf-8").trim(), 10);
3665
4287
  if (!isNaN(pid)) {
3666
4288
  try {
3667
4289
  process.kill(pid, 0);
@@ -3678,14 +4300,14 @@ function acquireLock(taskDir) {
3678
4300
  logger.warn(` Corrupt lock file \u2014 overwriting`);
3679
4301
  }
3680
4302
  try {
3681
- fs25.unlinkSync(lockPath);
4303
+ fs27.unlinkSync(lockPath);
3682
4304
  } catch {
3683
4305
  }
3684
4306
  }
3685
4307
  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);
4308
+ const fd = fs27.openSync(lockPath, fs27.constants.O_WRONLY | fs27.constants.O_CREAT | fs27.constants.O_EXCL);
4309
+ fs27.writeSync(fd, String(process.pid));
4310
+ fs27.closeSync(fd);
3689
4311
  } catch (err) {
3690
4312
  if (err.code === "EEXIST") {
3691
4313
  throw new Error("Pipeline already running (lock acquired by another process)");
@@ -3695,7 +4317,7 @@ function acquireLock(taskDir) {
3695
4317
  }
3696
4318
  function releaseLock(taskDir) {
3697
4319
  try {
3698
- fs25.unlinkSync(path23.join(taskDir, ".lock"));
4320
+ fs27.unlinkSync(path25.join(taskDir, ".lock"));
3699
4321
  } catch {
3700
4322
  }
3701
4323
  }
@@ -3903,8 +4525,8 @@ var init_pipeline = __esm({
3903
4525
  });
3904
4526
 
3905
4527
  // src/preflight.ts
3906
- import { execFileSync as execFileSync14 } from "child_process";
3907
- import * as fs26 from "fs";
4528
+ import { execFileSync as execFileSync15 } from "child_process";
4529
+ import * as fs28 from "fs";
3908
4530
  function check(name, fn) {
3909
4531
  try {
3910
4532
  const detail = fn() ?? void 0;
@@ -3916,7 +4538,7 @@ function check(name, fn) {
3916
4538
  function runPreflight() {
3917
4539
  const checks = [
3918
4540
  check("claude CLI", () => {
3919
- const v = execFileSync14("claude", ["--version"], {
4541
+ const v = execFileSync15("claude", ["--version"], {
3920
4542
  encoding: "utf-8",
3921
4543
  timeout: 1e4,
3922
4544
  stdio: ["pipe", "pipe", "pipe"]
@@ -3924,14 +4546,14 @@ function runPreflight() {
3924
4546
  return v;
3925
4547
  }),
3926
4548
  check("git repo", () => {
3927
- execFileSync14("git", ["rev-parse", "--is-inside-work-tree"], {
4549
+ execFileSync15("git", ["rev-parse", "--is-inside-work-tree"], {
3928
4550
  encoding: "utf-8",
3929
4551
  timeout: 5e3,
3930
4552
  stdio: ["pipe", "pipe", "pipe"]
3931
4553
  });
3932
4554
  }),
3933
4555
  check("pnpm", () => {
3934
- const v = execFileSync14("pnpm", ["--version"], {
4556
+ const v = execFileSync15("pnpm", ["--version"], {
3935
4557
  encoding: "utf-8",
3936
4558
  timeout: 5e3,
3937
4559
  stdio: ["pipe", "pipe", "pipe"]
@@ -3939,7 +4561,7 @@ function runPreflight() {
3939
4561
  return v;
3940
4562
  }),
3941
4563
  check("node >= 18", () => {
3942
- const v = execFileSync14("node", ["--version"], {
4564
+ const v = execFileSync15("node", ["--version"], {
3943
4565
  encoding: "utf-8",
3944
4566
  timeout: 5e3,
3945
4567
  stdio: ["pipe", "pipe", "pipe"]
@@ -3949,7 +4571,7 @@ function runPreflight() {
3949
4571
  return v;
3950
4572
  }),
3951
4573
  check("gh CLI", () => {
3952
- const v = execFileSync14("gh", ["--version"], {
4574
+ const v = execFileSync15("gh", ["--version"], {
3953
4575
  encoding: "utf-8",
3954
4576
  timeout: 5e3,
3955
4577
  stdio: ["pipe", "pipe", "pipe"]
@@ -3957,7 +4579,7 @@ function runPreflight() {
3957
4579
  return v;
3958
4580
  }),
3959
4581
  check("package.json", () => {
3960
- if (!fs26.existsSync("package.json")) throw new Error("not found");
4582
+ if (!fs28.existsSync("package.json")) throw new Error("not found");
3961
4583
  })
3962
4584
  ];
3963
4585
  const failed = checks.filter((c) => !c.ok);
@@ -3978,19 +4600,19 @@ var init_preflight = __esm({
3978
4600
  });
3979
4601
 
3980
4602
  // src/cli/args.ts
3981
- function getArg(args2, flag) {
4603
+ function getArg2(args2, flag) {
3982
4604
  const idx = args2.indexOf(flag);
3983
4605
  if (idx !== -1 && args2[idx + 1] && !args2[idx + 1].startsWith("--")) {
3984
4606
  return args2[idx + 1];
3985
4607
  }
3986
4608
  return void 0;
3987
4609
  }
3988
- function hasFlag(args2, flag) {
4610
+ function hasFlag2(args2, flag) {
3989
4611
  return args2.includes(flag);
3990
4612
  }
3991
4613
  function parseArgs() {
3992
4614
  const args2 = process.argv.slice(2);
3993
- if (hasFlag(args2, "--help") || hasFlag(args2, "-h") || args2.length === 0) {
4615
+ if (hasFlag2(args2, "--help") || hasFlag2(args2, "-h") || args2.length === 0) {
3994
4616
  console.log(`Usage:
3995
4617
  kody run --task-id <id> [--task "<desc>"] [--cwd <path>] [--issue-number <n>] [--complexity low|medium|high] [--feedback "<text>"] [--local] [--dry-run]
3996
4618
  kody rerun --task-id <id> --from <stage> [--cwd <path>] [--issue-number <n>]
@@ -4007,22 +4629,22 @@ function parseArgs() {
4007
4629
  console.error(`Unknown command: ${command2}`);
4008
4630
  process.exit(1);
4009
4631
  }
4010
- const issueStr = getArg(args2, "--issue-number") ?? process.env.ISSUE_NUMBER;
4011
- const prStr = getArg(args2, "--pr-number") ?? process.env.PR_NUMBER;
4012
- const localFlag = hasFlag(args2, "--local");
4632
+ const issueStr = getArg2(args2, "--issue-number") ?? process.env.ISSUE_NUMBER;
4633
+ const prStr = getArg2(args2, "--pr-number") ?? process.env.PR_NUMBER;
4634
+ const localFlag = hasFlag2(args2, "--local");
4013
4635
  return {
4014
4636
  command: command2,
4015
- taskId: getArg(args2, "--task-id") ?? process.env.TASK_ID,
4016
- task: getArg(args2, "--task"),
4017
- fromStage: getArg(args2, "--from") ?? process.env.FROM_STAGE,
4018
- dryRun: hasFlag(args2, "--dry-run") || process.env.DRY_RUN === "true",
4019
- cwd: getArg(args2, "--cwd"),
4637
+ taskId: getArg2(args2, "--task-id") ?? process.env.TASK_ID,
4638
+ task: getArg2(args2, "--task"),
4639
+ fromStage: getArg2(args2, "--from") ?? process.env.FROM_STAGE,
4640
+ dryRun: hasFlag2(args2, "--dry-run") || process.env.DRY_RUN === "true",
4641
+ cwd: getArg2(args2, "--cwd"),
4020
4642
  issueNumber: issueStr ? parseInt(issueStr, 10) : void 0,
4021
4643
  prNumber: prStr ? parseInt(prStr, 10) : void 0,
4022
- feedback: getArg(args2, "--feedback") ?? process.env.FEEDBACK,
4023
- local: localFlag || !isCI2 && !hasFlag(args2, "--no-local"),
4024
- complexity: getArg(args2, "--complexity") ?? process.env.COMPLEXITY,
4025
- ciRunId: getArg(args2, "--ci-run-id") ?? process.env.CI_RUN_ID
4644
+ feedback: getArg2(args2, "--feedback") ?? process.env.FEEDBACK,
4645
+ local: localFlag || !isCI2 && !hasFlag2(args2, "--no-local"),
4646
+ complexity: getArg2(args2, "--complexity") ?? process.env.COMPLEXITY,
4647
+ ciRunId: getArg2(args2, "--ci-run-id") ?? process.env.CI_RUN_ID
4026
4648
  };
4027
4649
  }
4028
4650
  var isCI2;
@@ -4033,183 +4655,9 @@ var init_args = __esm({
4033
4655
  }
4034
4656
  });
4035
4657
 
4036
- // src/cli/litellm.ts
4037
- import * as fs27 from "fs";
4038
- import * as os from "os";
4039
- import * as path24 from "path";
4040
- import { execFileSync as execFileSync15 } from "child_process";
4041
- async function checkLitellmHealth(url) {
4042
- try {
4043
- const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
4044
- return response.ok;
4045
- } catch {
4046
- return false;
4047
- }
4048
- }
4049
- async function checkModelHealth(baseUrl, apiKey, model = "claude-haiku-4-5") {
4050
- try {
4051
- const res = await fetch(`${baseUrl}/v1/messages`, {
4052
- method: "POST",
4053
- headers: {
4054
- "Content-Type": "application/json",
4055
- "x-api-key": apiKey,
4056
- "anthropic-version": "2023-06-01"
4057
- },
4058
- body: JSON.stringify({
4059
- model,
4060
- max_tokens: 4,
4061
- messages: [{ role: "user", content: "Reply with: ok" }]
4062
- }),
4063
- signal: AbortSignal.timeout(3e4)
4064
- });
4065
- if (!res.ok) {
4066
- const body2 = await res.text().catch(() => "");
4067
- return { ok: false, error: `HTTP ${res.status}: ${body2.slice(0, 200)}` };
4068
- }
4069
- const body = await res.json();
4070
- const hasAnthropicContent = Array.isArray(body.content) && body.content.some((b) => b.type === "text");
4071
- const hasThinkingContent = Array.isArray(body.content) && body.content.some((b) => b.type === "thinking");
4072
- const hasOpenAIContent = !!body.choices?.[0]?.message?.content;
4073
- if (!hasAnthropicContent && !hasThinkingContent && !hasOpenAIContent) {
4074
- return { ok: false, error: `Unexpected response format: ${JSON.stringify(body).slice(0, 200)}` };
4075
- }
4076
- return { ok: true };
4077
- } catch (err) {
4078
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
4079
- }
4080
- }
4081
- function generateLitellmConfig(provider, modelMap) {
4082
- const apiKeyVar = providerApiKeyEnvVar(provider);
4083
- const entries = ["model_list:"];
4084
- const seen = /* @__PURE__ */ new Set();
4085
- for (const providerModel of Object.values(modelMap)) {
4086
- if (seen.has(providerModel)) continue;
4087
- seen.add(providerModel);
4088
- entries.push(` - model_name: ${providerModel}`);
4089
- entries.push(` litellm_params:`);
4090
- entries.push(` model: ${provider}/${providerModel}`);
4091
- entries.push(` api_key: os.environ/${apiKeyVar}`);
4092
- }
4093
- return entries.join("\n") + "\n";
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
- }
4122
- async function tryStartLitellm(url, projectDir, generatedConfig) {
4123
- if (!generatedConfig) {
4124
- logger.warn("No provider configured in kody.config.json \u2014 cannot start LiteLLM proxy");
4125
- return null;
4126
- }
4127
- const configPath = path24.join(os.tmpdir(), "kody-litellm-config.yaml");
4128
- fs27.writeFileSync(configPath, generatedConfig);
4129
- const portMatch = url.match(/:(\d+)/);
4130
- const port = portMatch ? portMatch[1] : "4000";
4131
- let litellmFound = false;
4132
- try {
4133
- execFileSync15("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
4134
- litellmFound = true;
4135
- } catch {
4136
- try {
4137
- execFileSync15("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
4138
- litellmFound = true;
4139
- } catch {
4140
- }
4141
- }
4142
- if (!litellmFound) {
4143
- logger.warn("litellm not installed (pip install 'litellm[proxy]')");
4144
- return null;
4145
- }
4146
- logger.info(`Starting LiteLLM proxy on port ${port}...`);
4147
- let cmd;
4148
- let args2;
4149
- try {
4150
- execFileSync15("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
4151
- cmd = "litellm";
4152
- args2 = ["--config", configPath, "--port", port];
4153
- } catch {
4154
- cmd = "python3";
4155
- args2 = ["-m", "litellm", "--config", configPath, "--port", port];
4156
- }
4157
- const dotenvPath = path24.join(projectDir, ".env");
4158
- const dotenvVars = {};
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;
4163
- const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
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
- }
4173
- }
4174
- if (Object.keys(dotenvVars).length > 0) {
4175
- logger.info(` Loaded API keys: ${Object.keys(dotenvVars).join(", ")}`);
4176
- }
4177
- }
4178
- const { spawn: spawn2 } = await import("child_process");
4179
- const child = spawn2(cmd, args2, {
4180
- stdio: ["ignore", "pipe", "pipe"],
4181
- detached: true,
4182
- env: { ...process.env, ...dotenvVars }
4183
- });
4184
- let proxyStderr = "";
4185
- child.stderr?.on("data", (chunk) => {
4186
- proxyStderr += chunk.toString();
4187
- });
4188
- for (let i = 0; i < 30; i++) {
4189
- await new Promise((r) => setTimeout(r, 2e3));
4190
- if (await checkLitellmHealth(url)) {
4191
- logger.info(`LiteLLM proxy ready at ${url}`);
4192
- return child;
4193
- }
4194
- }
4195
- if (proxyStderr) {
4196
- logger.warn(`LiteLLM stderr: ${proxyStderr.slice(-1e3)}`);
4197
- }
4198
- logger.warn("LiteLLM proxy failed to start within 60s");
4199
- child.kill();
4200
- return null;
4201
- }
4202
- var init_litellm = __esm({
4203
- "src/cli/litellm.ts"() {
4204
- "use strict";
4205
- init_logger();
4206
- init_config();
4207
- }
4208
- });
4209
-
4210
4658
  // src/cli/task-state.ts
4211
- import * as fs28 from "fs";
4212
- import * as path25 from "path";
4659
+ import * as fs29 from "fs";
4660
+ import * as path26 from "path";
4213
4661
  function resolveTaskAction(issueNumber, existingTaskId, existingState) {
4214
4662
  if (!existingTaskId || !existingState) {
4215
4663
  return { action: "start-fresh", taskId: `${issueNumber}-${generateTaskId()}` };
@@ -4241,11 +4689,11 @@ function resolveTaskAction(issueNumber, existingTaskId, existingState) {
4241
4689
  function resolveForIssue(issueNumber, projectDir) {
4242
4690
  const existingTaskId = findLatestTaskForIssue(issueNumber, projectDir);
4243
4691
  if (existingTaskId) {
4244
- const statusPath = path25.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
4692
+ const statusPath = path26.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
4245
4693
  let existingState = null;
4246
- if (fs28.existsSync(statusPath)) {
4694
+ if (fs29.existsSync(statusPath)) {
4247
4695
  try {
4248
- existingState = JSON.parse(fs28.readFileSync(statusPath, "utf-8"));
4696
+ existingState = JSON.parse(fs29.readFileSync(statusPath, "utf-8"));
4249
4697
  } catch {
4250
4698
  }
4251
4699
  }
@@ -4402,8 +4850,8 @@ var init_resolve = __esm({
4402
4850
 
4403
4851
  // src/entry.ts
4404
4852
  var entry_exports = {};
4405
- import * as fs29 from "fs";
4406
- import * as path26 from "path";
4853
+ import * as fs30 from "fs";
4854
+ import * as path27 from "path";
4407
4855
  async function ensureLitellmProxy(config, projectDir) {
4408
4856
  if (!anyStageNeedsProxy(config)) return null;
4409
4857
  const litellmUrl = getLitellmUrl();
@@ -4458,9 +4906,9 @@ async function runModelHealthCheck(config) {
4458
4906
  }
4459
4907
  async function main() {
4460
4908
  const input = parseArgs();
4461
- const projectDir = input.cwd ? path26.resolve(input.cwd) : process.cwd();
4909
+ const projectDir = input.cwd ? path27.resolve(input.cwd) : process.cwd();
4462
4910
  if (input.cwd) {
4463
- if (!fs29.existsSync(projectDir)) {
4911
+ if (!fs30.existsSync(projectDir)) {
4464
4912
  console.error(`--cwd path does not exist: ${projectDir}`);
4465
4913
  process.exit(1);
4466
4914
  }
@@ -4526,8 +4974,24 @@ async function main() {
4526
4974
  process.exit(1);
4527
4975
  }
4528
4976
  }
4529
- const taskDir = path26.join(projectDir, ".kody", "tasks", taskId);
4530
- fs29.mkdirSync(taskDir, { recursive: true });
4977
+ const taskDir = path27.join(projectDir, ".kody", "tasks", taskId);
4978
+ fs30.mkdirSync(taskDir, { recursive: true });
4979
+ if (input.command === "rerun" && isTaskifyRun(taskDir)) {
4980
+ const marker = readTaskifyMarker(taskDir);
4981
+ if (marker) {
4982
+ logger.info(`Resuming taskify run for ${marker.ticketId ?? marker.prdFile} with PM feedback`);
4983
+ await taskifyCommand({
4984
+ ticketId: marker.ticketId,
4985
+ prdFile: marker.prdFile,
4986
+ issueNumber: marker.issueNumber ?? input.issueNumber,
4987
+ feedback: input.feedback,
4988
+ local: input.local,
4989
+ projectDir,
4990
+ taskId
4991
+ });
4992
+ return;
4993
+ }
4994
+ }
4531
4995
  if (input.command === "status") {
4532
4996
  printStatus(taskId, taskDir);
4533
4997
  return;
@@ -4643,31 +5107,31 @@ async function main() {
4643
5107
  logger.info("Preflight checks:");
4644
5108
  runPreflight();
4645
5109
  if (input.task) {
4646
- fs29.writeFileSync(path26.join(taskDir, "task.md"), input.task);
5110
+ fs30.writeFileSync(path27.join(taskDir, "task.md"), input.task);
4647
5111
  }
4648
- const taskMdPath = path26.join(taskDir, "task.md");
4649
- if (!fs29.existsSync(taskMdPath) && isPRFix && input.prNumber) {
5112
+ const taskMdPath = path27.join(taskDir, "task.md");
5113
+ if (!fs30.existsSync(taskMdPath) && isPRFix && input.prNumber) {
4650
5114
  logger.info(`Fetching PR #${input.prNumber} details as task context...`);
4651
5115
  const prDetails = getPRDetails(input.prNumber);
4652
5116
  if (prDetails) {
4653
5117
  const taskContent = `# ${prDetails.title}
4654
5118
 
4655
5119
  ${prDetails.body ?? ""}`;
4656
- fs29.writeFileSync(taskMdPath, taskContent);
5120
+ fs30.writeFileSync(taskMdPath, taskContent);
4657
5121
  logger.info(` Task loaded from PR #${input.prNumber}: ${prDetails.title}`);
4658
5122
  }
4659
- } else if (!fs29.existsSync(taskMdPath) && input.issueNumber) {
5123
+ } else if (!fs30.existsSync(taskMdPath) && input.issueNumber) {
4660
5124
  logger.info(`Fetching issue #${input.issueNumber} body as task...`);
4661
5125
  const issue = getIssue(input.issueNumber);
4662
5126
  if (issue) {
4663
5127
  const taskContent = `# ${issue.title}
4664
5128
 
4665
5129
  ${issue.body ?? ""}`;
4666
- fs29.writeFileSync(taskMdPath, taskContent);
5130
+ fs30.writeFileSync(taskMdPath, taskContent);
4667
5131
  logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
4668
5132
  }
4669
5133
  }
4670
- if (!fs29.existsSync(taskMdPath)) {
5134
+ if (!fs30.existsSync(taskMdPath)) {
4671
5135
  console.error("No task.md found. Provide --task, --issue-number, or ensure .kody/tasks/<id>/task.md exists.");
4672
5136
  process.exit(1);
4673
5137
  }
@@ -4805,7 +5269,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
4805
5269
  }
4806
5270
  }
4807
5271
  const state = await runPipeline(ctx);
4808
- const files = fs29.readdirSync(taskDir);
5272
+ const files = fs30.readdirSync(taskDir);
4809
5273
  console.log(`
4810
5274
  Artifacts in ${taskDir}:`);
4811
5275
  for (const f of files) {
@@ -4851,6 +5315,7 @@ var init_entry = __esm({
4851
5315
  init_litellm();
4852
5316
  init_task_resolution();
4853
5317
  init_task_state();
5318
+ init_taskify_command();
4854
5319
  init_config();
4855
5320
  main().catch(async (err) => {
4856
5321
  const msg = err instanceof Error ? err.message : String(err);
@@ -4869,9 +5334,9 @@ var init_entry = __esm({
4869
5334
  });
4870
5335
 
4871
5336
  // src/bin/cli.ts
4872
- import * as fs30 from "fs";
4873
- import * as path27 from "path";
4874
- import { fileURLToPath } from "url";
5337
+ import * as fs31 from "fs";
5338
+ import * as path28 from "path";
5339
+ import { fileURLToPath as fileURLToPath2 } from "url";
4875
5340
 
4876
5341
  // src/bin/commands/init.ts
4877
5342
  import * as fs3 from "fs";
@@ -5705,7 +6170,7 @@ ${repoContext}`;
5705
6170
  const output = execFileSync5("claude", [
5706
6171
  "--print",
5707
6172
  "--model",
5708
- "haiku",
6173
+ "claude-haiku-4-5-20251001",
5709
6174
  "--dangerously-skip-permissions",
5710
6175
  memoryPrompt
5711
6176
  ], {
@@ -5808,7 +6273,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
5808
6273
  const output = execFileSync5("claude", [
5809
6274
  "--print",
5810
6275
  "--model",
5811
- "haiku",
6276
+ "claude-haiku-4-5-20251001",
5812
6277
  "--dangerously-skip-permissions",
5813
6278
  customizationPrompt
5814
6279
  ], {
@@ -6041,11 +6506,11 @@ Create it manually.`, cwd);
6041
6506
 
6042
6507
  // src/bin/cli.ts
6043
6508
  init_architecture_detection();
6044
- var __dirname = path27.dirname(fileURLToPath(import.meta.url));
6045
- var PKG_ROOT = path27.resolve(__dirname, "..", "..");
6509
+ var __dirname2 = path28.dirname(fileURLToPath2(import.meta.url));
6510
+ var PKG_ROOT = path28.resolve(__dirname2, "..", "..");
6046
6511
  function getVersion() {
6047
- const pkgPath = path27.join(PKG_ROOT, "package.json");
6048
- const pkg = JSON.parse(fs30.readFileSync(pkgPath, "utf-8"));
6512
+ const pkgPath = path28.join(PKG_ROOT, "package.json");
6513
+ const pkg = JSON.parse(fs31.readFileSync(pkgPath, "utf-8"));
6049
6514
  return pkg.version;
6050
6515
  }
6051
6516
  var args = process.argv.slice(2);
@@ -6054,6 +6519,8 @@ if (command === "init") {
6054
6519
  initCommand({ force: args.includes("--force") }, PKG_ROOT);
6055
6520
  } else if (command === "bootstrap") {
6056
6521
  bootstrapCommand({ force: args.includes("--force") }, PKG_ROOT);
6522
+ } else if (command === "taskify") {
6523
+ Promise.resolve().then(() => (init_taskify_command(), taskify_command_exports)).then(({ runTaskifyCommand: runTaskifyCommand2 }) => runTaskifyCommand2());
6057
6524
  } else if (command === "ci-parse") {
6058
6525
  Promise.resolve().then(() => (init_parse_inputs(), parse_inputs_exports)).then(({ runCiParse: runCiParse2 }) => runCiParse2());
6059
6526
  } else if (command === "version" || command === "--version" || command === "-v") {