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

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,1410 @@ 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 });
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;
841
+ } catch (err) {
842
+ logger.warn(` Failed to get latest failed run for branch ${branch}: ${ghErrorMessage(err)}`);
843
+ return null;
792
844
  }
793
- logger.info(` Created new branch: ${branchName}`);
794
- return branchName;
795
845
  }
796
- function syncWithDefault(cwd, branch) {
797
- const defaultBranch = branch ?? getDefaultBranch(cwd);
798
- const current = getCurrentBranch(cwd);
799
- if (current === defaultBranch) return;
846
+ function getLatestKodyReviewComment(prNumber) {
800
847
  try {
801
- 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;
802
855
  } catch (err) {
803
- const msg = err instanceof Error ? err.message : String(err);
804
- logger.warn(` Failed to fetch latest from origin: ${msg}`);
805
- return;
856
+ logger.warn(` Failed to get review comments for PR #${prNumber}: ${err}`);
857
+ return null;
806
858
  }
859
+ }
860
+ function getPRFeedbackSinceLastKodyAction(prNumber) {
807
861
  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)}`);
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;
815
885
  }
816
- logger.warn(` Merge conflict with origin/${defaultBranch} \u2014 skipping sync`);
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
+ }
893
+ }
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;
817
906
  }
818
907
  }
819
- function mergeDefault(cwd) {
820
- const defaultBranch = getDefaultBranch(cwd);
821
- const current = getCurrentBranch(cwd);
822
- if (current === defaultBranch) return "clean";
823
- try {
824
- git(["fetch", "origin", defaultBranch], { cwd, timeout: 3e4 });
825
- } 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";
908
+ function isKodyComment(comment) {
909
+ if (comment.user_type === "Bot") return true;
910
+ return KODY_MARKERS.some((marker) => comment.body.includes(marker));
911
+ }
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;
916
+ }
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
+ ];
829
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;
830
948
  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 {
839
- }
840
- try {
841
- git(["merge", "--abort"], { cwd });
842
- } catch (abortErr) {
843
- logger.warn(` Failed to abort merge: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}`);
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;
844
961
  }
845
- return "error";
962
+ } catch {
846
963
  }
964
+ return null;
847
965
  }
848
- function getConflictedFiles(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) {
849
972
  try {
850
- const output = git(["diff", "--name-only", "--diff-filter=U"], { cwd });
851
- return output ? output.split("\n") : [];
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;
852
983
  } catch {
853
- return [];
984
+ return null;
854
985
  }
855
986
  }
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" };
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"() {
996
+ "use strict";
997
+ init_github_api();
860
998
  }
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 };
999
+ });
1000
+
1001
+ // src/cli/taskify-command.ts
1002
+ var taskify_command_exports = {};
1003
+ __export(taskify_command_exports, {
1004
+ isTaskifyRun: () => isTaskifyRun,
1005
+ readTaskifyMarker: () => readTaskifyMarker,
1006
+ runTaskifyCommand: () => runTaskifyCommand,
1007
+ taskifyCommand: () => taskifyCommand,
1008
+ topoSort: () => topoSort
1009
+ });
1010
+ import * as fs10 from "fs";
1011
+ import * as path9 from "path";
1012
+ import { fileURLToPath } from "url";
1013
+ import { execSync } from "child_process";
1014
+ function topoSort(tasks) {
1015
+ const n = tasks.length;
1016
+ const inDegree = new Array(n).fill(0);
1017
+ const adj = Array.from({ length: n }, () => []);
1018
+ for (let i = 0; i < n; i++) {
1019
+ for (const dep of tasks[i].dependsOn ?? []) {
1020
+ if (dep >= 0 && dep < n && dep !== i) {
1021
+ adj[dep].push(i);
1022
+ inDegree[i]++;
1023
+ }
1024
+ }
1025
+ }
1026
+ const queue = [];
1027
+ for (let i = 0; i < n; i++) {
1028
+ if (inDegree[i] === 0) queue.push(i);
1029
+ }
1030
+ const sorted = [];
1031
+ while (queue.length > 0) {
1032
+ const node = queue.shift();
1033
+ sorted.push(tasks[node]);
1034
+ for (const neighbor of adj[node]) {
1035
+ inDegree[neighbor]--;
1036
+ if (inDegree[neighbor] === 0) queue.push(neighbor);
1037
+ }
1038
+ }
1039
+ if (sorted.length !== n) {
1040
+ logger.warn("[taskify] dependency cycle detected \u2014 falling back to original order");
1041
+ return [...tasks];
1042
+ }
1043
+ return sorted;
866
1044
  }
867
- function getDiffFiles(baseBranch, cwd) {
868
- try {
869
- const output = git(["diff", "--name-only", `origin/${baseBranch}...HEAD`], { cwd });
870
- if (!output) return [];
871
- return output.split("\n").filter((f) => f && !f.startsWith(".kody/"));
872
- } catch (err) {
873
- const msg = err instanceof Error ? err.message : String(err);
874
- logger.warn(` Failed to get diff files: ${msg}`);
875
- return [];
1045
+ function getArg(args2, flag) {
1046
+ const idx = args2.indexOf(flag);
1047
+ return idx !== -1 ? args2[idx + 1] : void 0;
1048
+ }
1049
+ function hasFlag(args2, flag) {
1050
+ return args2.includes(flag);
1051
+ }
1052
+ async function runTaskifyCommand() {
1053
+ const args2 = process.argv.slice(3);
1054
+ const cwdArg = getArg(args2, "--cwd") ?? process.cwd();
1055
+ const projectDir = path9.resolve(cwdArg);
1056
+ const ticketId = getArg(args2, "--ticket") ?? process.env.TICKET_ID;
1057
+ const prdFileArg = getArg(args2, "--file") ?? process.env.PRD_FILE;
1058
+ const prdFile = prdFileArg ? path9.resolve(projectDir, prdFileArg) : void 0;
1059
+ const issueNumberStr = getArg(args2, "--issue-number") ?? process.env.ISSUE_NUMBER ?? "";
1060
+ const issueNumber = issueNumberStr ? parseInt(issueNumberStr, 10) : void 0;
1061
+ const feedback = getArg(args2, "--feedback") ?? process.env.FEEDBACK;
1062
+ const local = hasFlag(args2, "--local") || !process.env.CI;
1063
+ const taskIdArg = getArg(args2, "--task-id") ?? process.env.TASK_ID;
1064
+ const taskId = taskIdArg ?? (issueNumber ? `taskify-${issueNumber}-${generateTaskId()}` : `taskify-${generateTaskId()}`);
1065
+ if (!ticketId && !prdFile) {
1066
+ logger.error("Usage: kody taskify --ticket <ticket-id> OR kody taskify --file <prd.md>");
1067
+ process.exit(1);
876
1068
  }
1069
+ if (prdFile && !fs10.existsSync(prdFile)) {
1070
+ logger.error(`File not found: ${prdFile}`);
1071
+ process.exit(1);
1072
+ }
1073
+ setConfigDir(projectDir);
1074
+ setGhCwd(projectDir);
1075
+ await taskifyCommand({ ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId });
877
1076
  }
878
- function pushBranch(cwd) {
879
- try {
880
- git(["push", "-u", "origin", "HEAD"], { cwd, timeout: 12e4 });
881
- } 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 });
1077
+ async function taskifyCommand(opts) {
1078
+ const { ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId } = opts;
1079
+ const config = getProjectConfig();
1080
+ const taskDir = path9.join(projectDir, ".kody", "tasks", taskId);
1081
+ fs10.mkdirSync(taskDir, { recursive: true });
1082
+ const mode = prdFile ? "file" : "ticket";
1083
+ logger.info(`[taskify] mode=${mode} source=${ticketId ?? prdFile} issue=${issueNumber ?? "none"} task=${taskId}`);
1084
+ let mcpConfigJson;
1085
+ if (mode === "ticket") {
1086
+ try {
1087
+ mcpConfigJson = buildTaskifyMcpConfigJson(config);
1088
+ } catch (err) {
1089
+ const msg = err instanceof Error ? err.message : String(err);
1090
+ logger.error(`[taskify] MCP config error: ${msg}`);
1091
+ if (issueNumber && !local) {
1092
+ postComment(
1093
+ issueNumber,
1094
+ `Kody could not start the taskify command:
1095
+
1096
+ > ${msg}
1097
+
1098
+ Add the required MCP server config to \`kody.config.json\` and try again.`
1099
+ );
1100
+ }
1101
+ process.exit(1);
1102
+ }
1103
+ }
1104
+ const sc = resolveStageConfig(config, "taskify", "strong");
1105
+ const model = sc.model;
1106
+ const fileContent = prdFile ? fs10.readFileSync(prdFile, "utf-8") : void 0;
1107
+ let projectContext;
1108
+ {
1109
+ const parts = [];
1110
+ const memoryPath = path9.join(projectDir, ".kody", "memory.md");
1111
+ if (fs10.existsSync(memoryPath)) {
1112
+ try {
1113
+ const content = fs10.readFileSync(memoryPath, "utf-8").slice(0, 2e3);
1114
+ if (content.trim()) parts.push(`### Project Memory
1115
+ ${content}`);
1116
+ } catch {
1117
+ }
1118
+ }
1119
+ try {
1120
+ const output = execSync("git ls-files", { cwd: projectDir, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
1121
+ const lines = output.split("\n").filter(Boolean).slice(0, 150);
1122
+ if (lines.length > 0) parts.push(`### File Tree
1123
+ \`\`\`
1124
+ ${lines.join("\n")}
1125
+ \`\`\``);
1126
+ } catch {
1127
+ }
1128
+ if (parts.length > 0) projectContext = parts.join("\n\n");
1129
+ }
1130
+ const prompt = buildPrompt({ ticketId, fileContent, taskDir, feedback, projectContext });
1131
+ if (issueNumber && !local) {
1132
+ const src = mode === "file" ? `file \`${path9.basename(prdFile)}\`` : `ticket **${ticketId}**`;
1133
+ postComment(issueNumber, `Kody is decomposing ${src} into tasks...`);
1134
+ setLifecycleLabel(issueNumber, "planning");
1135
+ }
1136
+ fs10.writeFileSync(path9.join(taskDir, MARKER_FILE), JSON.stringify({ ticketId, prdFile, issueNumber }));
1137
+ const runner = opts.runner ?? createClaudeCodeRunner();
1138
+ logger.info(` model=${model} timeout=${TASKIFY_TIMEOUT_MS / 1e3}s`);
1139
+ const result = await runner.run("taskify", prompt, model, TASKIFY_TIMEOUT_MS, taskDir, {
1140
+ cwd: projectDir,
1141
+ mcpConfigJson
1142
+ });
1143
+ if (result.outcome !== "completed") {
1144
+ const errMsg = result.outcome === "timed_out" ? "Taskify timed out after 5 minutes." : `Taskify failed: ${result.error}`;
1145
+ logger.error(`[taskify] ${errMsg}`);
1146
+ if (issueNumber && !local) {
1147
+ postComment(issueNumber, `Kody taskify failed:
1148
+
1149
+ > ${errMsg}`);
1150
+ setLifecycleLabel(issueNumber, "failed");
1151
+ }
1152
+ process.exit(1);
1153
+ }
1154
+ const resultPath = path9.join(taskDir, RESULT_FILE);
1155
+ if (!fs10.existsSync(resultPath)) {
1156
+ const errMsg = `Claude did not write ${RESULT_FILE}. Output:
1157
+
1158
+ ${result.output?.slice(0, 500) ?? "(none)"}`;
1159
+ logger.error(`[taskify] ${errMsg}`);
1160
+ if (issueNumber && !local) {
1161
+ postComment(issueNumber, `Kody taskify failed: result file not found.
1162
+
1163
+ ${errMsg}`);
1164
+ setLifecycleLabel(issueNumber, "failed");
1165
+ }
1166
+ process.exit(1);
1167
+ }
1168
+ let parsed;
1169
+ try {
1170
+ parsed = JSON.parse(fs10.readFileSync(resultPath, "utf-8"));
1171
+ } catch {
1172
+ const errMsg = `Could not parse ${RESULT_FILE} as JSON.`;
1173
+ logger.error(`[taskify] ${errMsg}`);
1174
+ if (issueNumber && !local) {
1175
+ postComment(issueNumber, `Kody taskify failed: ${errMsg}`);
1176
+ setLifecycleLabel(issueNumber, "failed");
1177
+ }
1178
+ process.exit(1);
1179
+ }
1180
+ const sourceLabel = ticketId ?? (prdFile ? path9.basename(prdFile) : "spec");
1181
+ if (parsed.status === "questions") {
1182
+ handleQuestions(parsed, sourceLabel, issueNumber, local ?? false);
1183
+ } else if (parsed.status === "ready") {
1184
+ await handleTasks(parsed, sourceLabel, issueNumber, local ?? false);
1185
+ } else {
1186
+ const errMsg = `Unexpected status in ${RESULT_FILE}: ${JSON.stringify(parsed)}`;
1187
+ logger.error(`[taskify] ${errMsg}`);
1188
+ if (issueNumber && !local) {
1189
+ postComment(issueNumber, `Kody taskify failed: ${errMsg}`);
1190
+ setLifecycleLabel(issueNumber, "failed");
1191
+ }
1192
+ process.exit(1);
1193
+ }
1194
+ }
1195
+ function handleQuestions(parsed, ticketId, issueNumber, local) {
1196
+ const questions = parsed.questions ?? [];
1197
+ const numbered = questions.map((q, i) => `${i + 1}. ${q}`).join("\n");
1198
+ const comment = `Kody has questions before decomposing **${ticketId}**:
1199
+
1200
+ ${numbered}
1201
+
1202
+ Reply with \`@kody approve\` and your answers to proceed.`;
1203
+ logger.info(`[taskify] posting ${questions.length} question(s)`);
1204
+ if (issueNumber && !local) {
1205
+ postComment(issueNumber, comment);
1206
+ setLifecycleLabel(issueNumber, "waiting");
1207
+ } else {
1208
+ logger.info(`[taskify] questions:
1209
+ ${comment}`);
1210
+ }
1211
+ }
1212
+ async function handleTasks(parsed, ticketId, issueNumber, local) {
1213
+ const tasks = topoSort(parsed.tasks ?? []);
1214
+ if (tasks.length === 0) {
1215
+ logger.warn("[taskify] no tasks in result \u2014 nothing to file");
1216
+ if (issueNumber && !local) {
1217
+ postComment(issueNumber, `Kody taskify completed but found no tasks to file for **${ticketId}**.`);
1218
+ setLifecycleLabel(issueNumber, "done");
1219
+ }
1220
+ return;
1221
+ }
1222
+ const tooMany = tasks.length > MAX_TASKS_GUARD;
1223
+ if (tooMany) {
1224
+ logger.warn(`[taskify] ${tasks.length} tasks exceeds MAX_TASKS_GUARD (${MAX_TASKS_GUARD}) \u2014 filing issues but skipping auto-trigger`);
1225
+ }
1226
+ logger.info(`[taskify] filing ${tasks.length} issue(s)`);
1227
+ const filed = [];
1228
+ for (const task of tasks) {
1229
+ if (local) {
1230
+ logger.info(` [local] would create issue: ${task.title}`);
1231
+ filed.push({ number: 0, url: "#", title: task.title });
1232
+ continue;
1233
+ }
1234
+ const allLabels = [...task.labels ?? [], ...task.priority ? [`priority:${task.priority}`] : []];
1235
+ const issue = createIssue(task.title, task.body, allLabels);
1236
+ if (issue) {
1237
+ filed.push({ number: issue.number, url: issue.url, title: task.title });
1238
+ } else {
1239
+ logger.warn(` failed to create issue: ${task.title}`);
1240
+ }
1241
+ }
1242
+ const autoTrigger = !tooMany && filed.length <= AUTO_TRIGGER_THRESHOLD;
1243
+ if (autoTrigger && !local) {
1244
+ for (const issue of filed) {
1245
+ if (issue.number > 0) {
1246
+ postComment(issue.number, "@kody");
1247
+ logger.info(` auto-triggered @kody on issue #${issue.number}`);
1248
+ }
1249
+ }
1250
+ }
1251
+ if (issueNumber && !local) {
1252
+ const links = filed.map((i) => `- [#${i.number}](${i.url}) \u2014 ${i.title}`).join("\n");
1253
+ const triggerNote = tooMany ? `
1254
+
1255
+ > **${tasks.length} tasks filed** \u2014 auto-trigger is disabled for large epics. Comment \`@kody\` on each issue to start the pipeline.` : autoTrigger ? `
1256
+
1257
+ > Auto-triggered \`@kody\` on each issue.` : `
1258
+
1259
+ > Comment \`@kody\` on each issue to start the pipeline.`;
1260
+ postComment(
1261
+ issueNumber,
1262
+ `Kody decomposed **${ticketId}** into ${filed.length} task(s):
1263
+
1264
+ ${links}${triggerNote}`
1265
+ );
1266
+ setLifecycleLabel(issueNumber, "done");
1267
+ } else if (local) {
1268
+ logger.info(`[taskify] local mode \u2014 would file ${filed.length} issue(s)`);
1269
+ }
1270
+ }
1271
+ function buildPrompt(opts) {
1272
+ const { ticketId, fileContent, taskDir, feedback, projectContext } = opts;
1273
+ const scriptDir = new URL(".", import.meta.url).pathname;
1274
+ const candidates = [
1275
+ path9.resolve(scriptDir, "..", "prompts", "taskify-ticket.md"),
1276
+ path9.resolve(scriptDir, "..", "..", "prompts", "taskify-ticket.md"),
1277
+ path9.resolve(__dirname, "..", "..", "prompts", "taskify-ticket.md"),
1278
+ path9.resolve(__dirname, "..", "prompts", "taskify-ticket.md")
1279
+ ];
1280
+ let template = "";
1281
+ for (const candidate of candidates) {
1282
+ if (fs10.existsSync(candidate)) {
1283
+ template = fs10.readFileSync(candidate, "utf-8");
1284
+ break;
1285
+ }
1286
+ }
1287
+ if (!template) {
1288
+ throw new Error(`Could not find prompts/taskify-ticket.md. Searched: ${candidates.join(", ")}`);
1289
+ }
1290
+ const resolveBlock = (name, value) => {
1291
+ if (value) {
1292
+ template = template.replace(new RegExp(`\\{\\{#if ${name}\\}\\}\\n?([\\s\\S]*?)\\{\\{\\/if\\}\\}`, "g"), "$1");
1293
+ template = template.replace(new RegExp(`\\{\\{${name}\\}\\}`, "g"), value);
1294
+ } else {
1295
+ template = template.replace(new RegExp(`\\{\\{#if ${name}\\}\\}\\n?[\\s\\S]*?\\{\\{\\/if\\}\\}\\n?`, "g"), "");
1296
+ }
1297
+ };
1298
+ resolveBlock("PROJECT_CONTEXT", projectContext);
1299
+ resolveBlock("TICKET_ID", ticketId);
1300
+ resolveBlock("FILE_CONTENT", fileContent);
1301
+ resolveBlock("FEEDBACK", feedback);
1302
+ template = template.replace(/\{\{TASK_DIR\}\}/g, taskDir);
1303
+ return template;
1304
+ }
1305
+ function isTaskifyRun(taskDir) {
1306
+ return fs10.existsSync(path9.join(taskDir, MARKER_FILE));
1307
+ }
1308
+ function readTaskifyMarker(taskDir) {
1309
+ const markerPath = path9.join(taskDir, MARKER_FILE);
1310
+ if (!fs10.existsSync(markerPath)) return null;
1311
+ try {
1312
+ return JSON.parse(fs10.readFileSync(markerPath, "utf-8"));
1313
+ } catch {
1314
+ return null;
1315
+ }
1316
+ }
1317
+ var __dirname, AUTO_TRIGGER_THRESHOLD, MAX_TASKS_GUARD, TASKIFY_TIMEOUT_MS, MARKER_FILE, RESULT_FILE;
1318
+ var init_taskify_command = __esm({
1319
+ "src/cli/taskify-command.ts"() {
1320
+ "use strict";
1321
+ init_config();
1322
+ init_agent_runner();
1323
+ init_mcp_config();
1324
+ init_github_api();
1325
+ init_logger();
1326
+ init_task_resolution();
1327
+ __dirname = path9.dirname(fileURLToPath(import.meta.url));
1328
+ AUTO_TRIGGER_THRESHOLD = 5;
1329
+ MAX_TASKS_GUARD = 20;
1330
+ TASKIFY_TIMEOUT_MS = 5 * 60 * 1e3;
1331
+ MARKER_FILE = "taskify.marker";
1332
+ RESULT_FILE = "taskify-result.json";
1333
+ }
1334
+ });
1335
+
1336
+ // src/ci/parse-inputs.ts
1337
+ var parse_inputs_exports = {};
1338
+ __export(parse_inputs_exports, {
1339
+ parseCommentInputs: () => parseCommentInputs,
1340
+ runCiParse: () => runCiParse,
1341
+ writeOutputs: () => writeOutputs
1342
+ });
1343
+ import * as fs11 from "fs";
1344
+ function generateTimestamp() {
1345
+ const now = /* @__PURE__ */ new Date();
1346
+ const pad = (n) => String(n).padStart(2, "0");
1347
+ const y = String(now.getFullYear()).slice(2);
1348
+ const m = pad(now.getMonth() + 1);
1349
+ const d = pad(now.getDate());
1350
+ const H = pad(now.getHours());
1351
+ const M = pad(now.getMinutes());
1352
+ const S = pad(now.getSeconds());
1353
+ return `${y}${m}${d}-${H}${M}${S}`;
1354
+ }
1355
+ function parseCommentInputs() {
1356
+ const triggerType = process.env.TRIGGER_TYPE ?? "dispatch";
1357
+ if (triggerType === "dispatch") {
1358
+ const taskId2 = process.env.INPUT_TASK_ID ?? "";
1359
+ return {
1360
+ task_id: taskId2,
1361
+ mode: process.env.INPUT_MODE ?? "full",
1362
+ from_stage: process.env.INPUT_FROM_STAGE ?? "",
1363
+ issue_number: process.env.INPUT_ISSUE_NUMBER ?? "",
1364
+ pr_number: "",
1365
+ feedback: process.env.INPUT_FEEDBACK ?? "",
1366
+ complexity: "",
1367
+ ci_run_id: "",
1368
+ ticket_id: "",
1369
+ prd_file: "",
1370
+ dry_run: false,
1371
+ valid: !!taskId2,
1372
+ trigger_type: "dispatch"
1373
+ };
1374
+ }
1375
+ const commentBody = (process.env.COMMENT_BODY ?? "").replace(/\r/g, "");
1376
+ const issueNumber = process.env.ISSUE_NUMBER ?? "";
1377
+ const isPR = !!process.env.ISSUE_IS_PR;
1378
+ const kodyMatch = commentBody.match(/(?:@kody|\/kody)\s*(.*)/i);
1379
+ if (!kodyMatch) {
1380
+ return {
1381
+ task_id: "",
1382
+ mode: "full",
1383
+ from_stage: "",
1384
+ issue_number: issueNumber,
1385
+ pr_number: "",
1386
+ feedback: "",
1387
+ complexity: "",
1388
+ ci_run_id: "",
1389
+ ticket_id: "",
1390
+ prd_file: "",
1391
+ dry_run: false,
1392
+ valid: false,
1393
+ trigger_type: "comment"
1394
+ };
1395
+ }
1396
+ const argsLine = kodyMatch[1].trim();
1397
+ let fromStage = "";
1398
+ let feedback = "";
1399
+ let complexity = "";
1400
+ let dryRun = false;
1401
+ let ciRunId = "";
1402
+ let ticketId = "";
1403
+ let prdFile = "";
1404
+ const fromMatch = argsLine.match(/--from\s+(\S+)/);
1405
+ if (fromMatch) fromStage = fromMatch[1];
1406
+ const feedbackMatch = argsLine.match(/--feedback\s+"([^"]*)"/);
1407
+ if (feedbackMatch) feedback = feedbackMatch[1];
1408
+ const complexityMatch = argsLine.match(/--complexity\s+(\S+)/);
1409
+ if (complexityMatch) complexity = complexityMatch[1];
1410
+ if (/--dry-run/.test(argsLine)) dryRun = true;
1411
+ const ciRunIdMatch = argsLine.match(/--ci-run-id\s+(\S+)/);
1412
+ if (ciRunIdMatch) ciRunId = ciRunIdMatch[1];
1413
+ const ticketMatch = argsLine.match(/--ticket\s+(\S+)/);
1414
+ if (ticketMatch) ticketId = ticketMatch[1];
1415
+ const fileMatch = argsLine.match(/--file\s+(\S+)/);
1416
+ if (fileMatch) prdFile = fileMatch[1];
1417
+ 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();
1418
+ const parts = positional ? positional.split(/\s+/) : [];
1419
+ let mode = "full";
1420
+ let taskId = "";
1421
+ let idx = 0;
1422
+ if (parts[idx] && VALID_MODES.includes(parts[idx])) {
1423
+ mode = parts[idx];
1424
+ idx++;
1425
+ }
1426
+ if (parts[idx] && !parts[idx].startsWith("--")) {
1427
+ taskId = parts[idx];
1428
+ idx++;
1429
+ } else if (parts[0] && !VALID_MODES.includes(parts[0]) && !parts[0].startsWith("--")) {
1430
+ taskId = parts[0];
1431
+ }
1432
+ const kodyLineIdx = commentBody.search(/(?:@kody|\/kody)/i);
1433
+ const afterKodyLine = commentBody.slice(kodyLineIdx);
1434
+ const newlineIdx = afterKodyLine.indexOf("\n");
1435
+ const bodyAfterCommand = newlineIdx !== -1 ? afterKodyLine.slice(newlineIdx + 1) : "";
1436
+ if (mode === "approve") {
1437
+ mode = "rerun";
1438
+ if (bodyAfterCommand) {
1439
+ feedback = bodyAfterCommand;
1440
+ }
1441
+ }
1442
+ if (mode === "fix") {
1443
+ if (bodyAfterCommand) {
1444
+ feedback = bodyAfterCommand;
1445
+ }
1446
+ }
1447
+ if (mode === "fix-ci") {
1448
+ if (bodyAfterCommand) {
1449
+ feedback = bodyAfterCommand;
1450
+ const runIdFromBody = bodyAfterCommand.match(/Run ID:\s*(\d+)/);
1451
+ if (runIdFromBody) {
1452
+ ciRunId = runIdFromBody[1];
1453
+ }
1454
+ }
1455
+ }
1456
+ if (mode === "bootstrap") {
1457
+ taskId = `bootstrap-${generateTimestamp()}`;
1458
+ }
1459
+ if (mode === "taskify") {
1460
+ taskId = `taskify-${issueNumber}-${generateTimestamp()}`;
1461
+ }
1462
+ const prNumber = isPR ? issueNumber : "";
1463
+ if (mode === "review" && prNumber) {
1464
+ taskId = `review-pr-${prNumber}-${generateTimestamp()}`;
1465
+ }
1466
+ if (!taskId && mode === "full") {
1467
+ taskId = `${issueNumber}-${generateTimestamp()}`;
1468
+ }
1469
+ const modesWithoutTaskId = ["fix", "fix-ci", "status", "review", "resolve", "rerun"];
1470
+ const valid = !!taskId || modesWithoutTaskId.includes(mode);
1471
+ if (mode === "taskify" && !ticketId && !prdFile) {
1472
+ return {
1473
+ task_id: taskId,
1474
+ mode,
1475
+ from_stage: fromStage,
1476
+ issue_number: issueNumber,
1477
+ pr_number: "",
1478
+ feedback,
1479
+ complexity,
1480
+ ci_run_id: ciRunId,
1481
+ ticket_id: "",
1482
+ prd_file: "",
1483
+ dry_run: dryRun,
1484
+ valid: false,
1485
+ trigger_type: "comment"
1486
+ };
1487
+ }
1488
+ return {
1489
+ task_id: taskId,
1490
+ mode,
1491
+ from_stage: fromStage,
1492
+ issue_number: issueNumber,
1493
+ pr_number: prNumber,
1494
+ feedback,
1495
+ complexity,
1496
+ ci_run_id: ciRunId,
1497
+ ticket_id: ticketId,
1498
+ prd_file: prdFile,
1499
+ dry_run: dryRun,
1500
+ valid,
1501
+ trigger_type: "comment"
1502
+ };
1503
+ }
1504
+ function writeOutputs(result) {
1505
+ const outputFile = process.env.GITHUB_OUTPUT;
1506
+ function output(key, value) {
1507
+ if (outputFile) {
1508
+ if (value.includes("\n")) {
1509
+ fs11.appendFileSync(outputFile, `${key}<<KODY_EOF
1510
+ ${value}
1511
+ KODY_EOF
1512
+ `);
1513
+ } else {
1514
+ fs11.appendFileSync(outputFile, `${key}=${value}
1515
+ `);
1516
+ }
1517
+ }
1518
+ const display = value.includes("\n") ? value.split("\n")[0] + "..." : value;
1519
+ console.log(`${key}=${display}`);
1520
+ }
1521
+ output("task_id", result.task_id);
1522
+ output("mode", result.mode);
1523
+ output("from_stage", result.from_stage);
1524
+ output("issue_number", result.issue_number);
1525
+ output("pr_number", result.pr_number);
1526
+ output("feedback", result.feedback);
1527
+ output("complexity", result.complexity);
1528
+ output("ci_run_id", result.ci_run_id);
1529
+ output("ticket_id", result.ticket_id);
1530
+ output("prd_file", result.prd_file);
1531
+ output("dry_run", result.dry_run ? "true" : "false");
1532
+ output("valid", result.valid ? "true" : "false");
1533
+ output("trigger_type", result.trigger_type);
1534
+ }
1535
+ function runCiParse() {
1536
+ const result = parseCommentInputs();
1537
+ writeOutputs(result);
1538
+ }
1539
+ var VALID_MODES;
1540
+ var init_parse_inputs = __esm({
1541
+ "src/ci/parse-inputs.ts"() {
1542
+ "use strict";
1543
+ VALID_MODES = [
1544
+ "full",
1545
+ "rerun",
1546
+ "fix",
1547
+ "fix-ci",
1548
+ "status",
1549
+ "approve",
1550
+ "review",
1551
+ "resolve",
1552
+ "bootstrap",
1553
+ "taskify"
1554
+ ];
1555
+ }
1556
+ });
1557
+
1558
+ // src/definitions.ts
1559
+ var definitions_exports = {};
1560
+ __export(definitions_exports, {
1561
+ STAGES: () => STAGES,
1562
+ applyTimeoutOverrides: () => applyTimeoutOverrides,
1563
+ getStage: () => getStage
1564
+ });
1565
+ function getStage(name) {
1566
+ return STAGES.find((s) => s.name === name);
1567
+ }
1568
+ function applyTimeoutOverrides(overrides) {
1569
+ for (const stage of STAGES) {
1570
+ if (overrides[stage.name] != null) {
1571
+ stage.timeout = overrides[stage.name] * 1e3;
1572
+ }
884
1573
  }
885
- logger.info(" Pushed to origin");
886
1574
  }
887
- var BASE_BRANCHES, _hookSafeEnv;
888
- var init_git_utils = __esm({
889
- "src/git-utils.ts"() {
1575
+ var STAGES;
1576
+ var init_definitions = __esm({
1577
+ "src/definitions.ts"() {
890
1578
  "use strict";
891
- init_logger();
892
- init_config();
893
- BASE_BRANCHES = ["dev", "main", "master"];
894
- _hookSafeEnv = null;
1579
+ STAGES = [
1580
+ {
1581
+ name: "taskify",
1582
+ type: "agent",
1583
+ modelTier: "cheap",
1584
+ timeout: 6e5,
1585
+ maxRetries: 1,
1586
+ outputFile: "task.json"
1587
+ },
1588
+ {
1589
+ name: "plan",
1590
+ type: "agent",
1591
+ modelTier: "strong",
1592
+ timeout: 6e5,
1593
+ maxRetries: 1,
1594
+ outputFile: "plan.md"
1595
+ },
1596
+ {
1597
+ name: "build",
1598
+ type: "agent",
1599
+ modelTier: "mid",
1600
+ timeout: 24e5,
1601
+ maxRetries: 1
1602
+ },
1603
+ {
1604
+ name: "verify",
1605
+ type: "gate",
1606
+ modelTier: "cheap",
1607
+ timeout: 3e5,
1608
+ maxRetries: 2,
1609
+ retryWithAgent: "autofix"
1610
+ },
1611
+ {
1612
+ name: "review",
1613
+ type: "agent",
1614
+ modelTier: "strong",
1615
+ timeout: 6e5,
1616
+ maxRetries: 1,
1617
+ outputFile: "review.md"
1618
+ },
1619
+ {
1620
+ name: "review-fix",
1621
+ type: "agent",
1622
+ modelTier: "mid",
1623
+ timeout: 12e5,
1624
+ maxRetries: 1
1625
+ },
1626
+ {
1627
+ name: "ship",
1628
+ type: "deterministic",
1629
+ modelTier: "cheap",
1630
+ timeout: 24e4,
1631
+ maxRetries: 1,
1632
+ outputFile: "ship.md"
1633
+ }
1634
+ ];
895
1635
  }
896
1636
  });
897
1637
 
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;
1638
+ // src/git-utils.ts
1639
+ import { execFileSync as execFileSync9 } from "child_process";
1640
+ function getHookSafeEnv() {
1641
+ if (!_hookSafeEnv) {
1642
+ _hookSafeEnv = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
907
1643
  }
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
- }
914
- function setGhCwd(cwd) {
915
- _ghCwd = cwd;
916
- }
917
- function ghToken() {
918
- return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
1644
+ return _hookSafeEnv;
919
1645
  }
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, {
1646
+ function git(args2, options) {
1647
+ return execFileSync9("git", args2, {
924
1648
  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"]
1649
+ timeout: options?.timeout ?? 3e4,
1650
+ cwd: options?.cwd,
1651
+ env: options?.env ?? getHookSafeEnv(),
1652
+ stdio: ["pipe", "pipe", "pipe"]
930
1653
  }).trim();
931
1654
  }
932
- function getIssue(issueNumber) {
933
- 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;
945
- }
946
- return { body: parsed.body ?? "", title: parsed.title };
947
- } 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)}`);
952
- }
953
- return null;
954
- }
1655
+ function deriveBranchName(issueNumber, title) {
1656
+ const slug = title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").slice(0, 50).replace(/-$/, "");
1657
+ return `${issueNumber}-${slug}`;
955
1658
  }
956
- function getIssueComments(issueNumber) {
1659
+ function getDefaultBranch(cwd) {
957
1660
  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) : [];
1661
+ const config = getProjectConfig();
1662
+ if (config.git?.defaultBranch) {
1663
+ return config.git.defaultBranch;
1664
+ }
965
1665
  } catch {
966
- return [];
967
1666
  }
968
- }
969
- function getIssueLabels(issueNumber) {
970
1667
  try {
971
- const output = gh(["issue", "view", String(issueNumber), "--json", "labels", "--jq", ".labels[].name"]);
972
- return output.split("\n").filter(Boolean);
1668
+ const ref = git(["symbolic-ref", "refs/remotes/origin/HEAD"], { cwd });
1669
+ return ref.replace("refs/remotes/origin/", "");
973
1670
  } catch {
974
- return [];
975
- }
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}`);
983
- }
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}`);
994
- }
995
- }
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;
1009
- }
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)}`);
1014
- }
1015
- return null;
1016
1671
  }
1017
- }
1018
- function updatePR(prNumber, body) {
1019
1672
  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}`);
1673
+ const output = git(["remote", "show", "origin"], { cwd, timeout: 1e4 });
1674
+ const match = output.match(/HEAD branch:\s*(\S+)/);
1675
+ if (match) return match[1];
1676
+ } catch {
1027
1677
  }
1678
+ return "dev";
1028
1679
  }
1029
- function createPR(head, base, title, body) {
1030
- 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;
1055
- }
1680
+ function getCurrentBranch(cwd) {
1681
+ return git(["branch", "--show-current"], { cwd });
1056
1682
  }
1057
- function setLifecycleLabel(issueNumber, phase) {
1058
- if (!LIFECYCLE_LABELS.includes(phase)) {
1059
- logger.warn(` Invalid lifecycle phase: ${phase}`);
1060
- return;
1683
+ function ensureFeatureBranch(issueNumber, title, cwd) {
1684
+ const current = getCurrentBranch(cwd);
1685
+ const branchName = deriveBranchName(issueNumber, title);
1686
+ if (current === branchName || current.startsWith(`${issueNumber}-`)) {
1687
+ logger.info(` Already on feature branch: ${current}`);
1688
+ return current;
1061
1689
  }
1062
- const othersToRemove = LIFECYCLE_LABELS.filter((l) => l !== phase).map((l) => `kody:${l}`).join(",");
1063
- if (othersToRemove) {
1690
+ if (!BASE_BRANCHES.includes(current) && current !== "") {
1691
+ const defaultBranch2 = getDefaultBranch(cwd);
1692
+ logger.info(` Switching from ${current} to ${defaultBranch2} before creating ${branchName}`);
1064
1693
  try {
1065
- gh(["issue", "edit", String(issueNumber), "--remove-label", othersToRemove]);
1694
+ git(["checkout", defaultBranch2], { cwd });
1066
1695
  } catch {
1696
+ logger.warn(` Failed to checkout ${defaultBranch2}, aborting branch creation`);
1697
+ return current;
1067
1698
  }
1068
1699
  }
1069
- setLabel(issueNumber, `kody:${phase}`);
1070
- }
1071
- function getPRsForIssue(issueNumber) {
1072
1700
  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;
1701
+ git(["fetch", "origin"], { cwd, timeout: 3e4 });
1108
1702
  } catch (err) {
1109
- logger.error(` Failed to get PRs for issue #${issueNumber}: ${err}`);
1110
- return [];
1703
+ const msg = err instanceof Error ? err.message : String(err);
1704
+ logger.warn(` Failed to fetch origin: ${msg}`);
1111
1705
  }
1112
- }
1113
- function getPRDetails(prNumber) {
1114
1706
  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
- };
1133
- } 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)}`);
1138
- }
1139
- return null;
1707
+ git(["rev-parse", "--verify", `origin/${branchName}`], { cwd });
1708
+ git(["checkout", branchName], { cwd });
1709
+ git(["pull", "origin", branchName], { cwd, timeout: 3e4 });
1710
+ logger.info(` Checked out existing remote branch: ${branchName}`);
1711
+ return branchName;
1712
+ } catch {
1140
1713
  }
1141
- }
1142
- function postPRComment(prNumber, body) {
1143
1714
  try {
1144
- gh(
1145
- ["pr", "comment", String(prNumber), "--body-file", "-"],
1146
- { input: body }
1147
- );
1148
- logger.info(` Comment posted on PR #${prNumber}`);
1149
- } catch (err) {
1150
- logger.warn(` Failed to post PR comment: ${err}`);
1715
+ git(["rev-parse", "--verify", branchName], { cwd });
1716
+ git(["checkout", branchName], { cwd });
1717
+ logger.info(` Checked out existing local branch: ${branchName}`);
1718
+ return branchName;
1719
+ } catch {
1151
1720
  }
1152
- }
1153
- function submitPRReview(prNumber, body, event) {
1154
- const flag = event === "approve" ? "--approve" : "--request-changes";
1721
+ const defaultBranch = getDefaultBranch(cwd);
1155
1722
  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;
1723
+ git(["checkout", "-b", branchName, `origin/${defaultBranch}`], { cwd });
1724
+ } catch {
1725
+ git(["checkout", "-b", branchName], { cwd });
1165
1726
  }
1727
+ logger.info(` Created new branch: ${branchName}`);
1728
+ return branchName;
1166
1729
  }
1167
- function getCIFailureLogs(runId, maxLength = 8e3) {
1730
+ function syncWithDefault(cwd, branch) {
1731
+ const defaultBranch = branch ?? getDefaultBranch(cwd);
1732
+ const current = getCurrentBranch(cwd);
1733
+ if (current === defaultBranch) return;
1168
1734
  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}`;
1735
+ git(["fetch", "origin", defaultBranch], { cwd, timeout: 3e4 });
1179
1736
  } catch (err) {
1180
- logger.warn(` Failed to get CI failure logs for run ${runId}: ${ghErrorMessage(err)}`);
1181
- return null;
1737
+ const msg = err instanceof Error ? err.message : String(err);
1738
+ logger.warn(` Failed to fetch latest from origin: ${msg}`);
1739
+ return;
1182
1740
  }
1183
- }
1184
- function getLatestFailedRunForBranch(branch) {
1185
1741
  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;
1742
+ git(["merge", `origin/${defaultBranch}`, "--no-edit"], { cwd, timeout: 3e4 });
1743
+ logger.info(` Synced with origin/${defaultBranch}`);
1744
+ } catch {
1745
+ try {
1746
+ git(["merge", "--abort"], { cwd });
1747
+ } catch (abortErr) {
1748
+ logger.warn(` Failed to abort merge: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}`);
1749
+ }
1750
+ logger.warn(` Merge conflict with origin/${defaultBranch} \u2014 skipping sync`);
1204
1751
  }
1205
1752
  }
1206
- function getLatestKodyReviewComment(prNumber) {
1753
+ function mergeDefault(cwd) {
1754
+ const defaultBranch = getDefaultBranch(cwd);
1755
+ const current = getCurrentBranch(cwd);
1756
+ if (current === defaultBranch) return "clean";
1207
1757
  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;
1758
+ git(["fetch", "origin", defaultBranch], { cwd, timeout: 3e4 });
1215
1759
  } catch (err) {
1216
- logger.warn(` Failed to get review comments for PR #${prNumber}: ${err}`);
1217
- return null;
1760
+ const msg = err instanceof Error ? err.message : String(err);
1761
+ logger.warn(` Failed to fetch latest from origin: ${msg}`);
1762
+ return "error";
1218
1763
  }
1219
- }
1220
- function getPRFeedbackSinceLastKodyAction(prNumber) {
1221
1764
  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
- }
1765
+ git(["merge", `origin/${defaultBranch}`, "--no-edit"], { cwd, timeout: 3e4 });
1766
+ logger.info(` Merged origin/${defaultBranch} cleanly`);
1767
+ return "clean";
1768
+ } catch {
1769
+ try {
1770
+ const unmerged = git(["diff", "--name-only", "--diff-filter=U"], { cwd });
1771
+ if (unmerged.trim()) return "conflict";
1772
+ } catch {
1253
1773
  }
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
- }
1774
+ try {
1775
+ git(["merge", "--abort"], { cwd });
1776
+ } catch (abortErr) {
1777
+ logger.warn(` Failed to abort merge: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}`);
1261
1778
  }
1262
- return parts.join("\n\n");
1779
+ return "error";
1780
+ }
1781
+ }
1782
+ function getConflictedFiles(cwd) {
1783
+ try {
1784
+ const output = git(["diff", "--name-only", "--diff-filter=U"], { cwd });
1785
+ return output ? output.split("\n") : [];
1786
+ } catch {
1787
+ return [];
1788
+ }
1789
+ }
1790
+ function commitAll(message, cwd) {
1791
+ const status = git(["status", "--porcelain"], { cwd });
1792
+ if (!status) {
1793
+ return { success: false, hash: "", message: "No changes to commit" };
1794
+ }
1795
+ git(["add", "."], { cwd });
1796
+ git(["commit", "--no-gpg-sign", "-m", message], { cwd });
1797
+ const hash = git(["rev-parse", "HEAD"], { cwd }).slice(0, 7);
1798
+ logger.info(` Committed: ${hash} ${message}`);
1799
+ return { success: true, hash, message };
1800
+ }
1801
+ function getDiffFiles(baseBranch, cwd) {
1802
+ try {
1803
+ const output = git(["diff", "--name-only", `origin/${baseBranch}...HEAD`], { cwd });
1804
+ if (!output) return [];
1805
+ return output.split("\n").filter((f) => f && !f.startsWith(".kody/"));
1263
1806
  } catch (err) {
1264
- logger.warn(` Failed to get PR feedback for #${prNumber}: ${err}`);
1265
- return null;
1807
+ const msg = err instanceof Error ? err.message : String(err);
1808
+ logger.warn(` Failed to get diff files: ${msg}`);
1809
+ return [];
1266
1810
  }
1267
1811
  }
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;
1812
+ function pushBranch(cwd) {
1813
+ try {
1814
+ git(["push", "-u", "origin", "HEAD"], { cwd, timeout: 12e4 });
1815
+ } catch {
1816
+ logger.info(" Push rejected (non-fast-forward), retrying with --force-with-lease");
1817
+ git(["push", "--force-with-lease", "-u", "origin", "HEAD"], { cwd, timeout: 12e4 });
1818
+ }
1819
+ logger.info(" Pushed to origin");
1276
1820
  }
1277
- var API_TIMEOUT_MS, LIFECYCLE_LABELS, _ghCwd, KODY_MARKERS;
1278
- var init_github_api = __esm({
1279
- "src/github-api.ts"() {
1821
+ var BASE_BRANCHES, _hookSafeEnv;
1822
+ var init_git_utils = __esm({
1823
+ "src/git-utils.ts"() {
1280
1824
  "use strict";
1281
1825
  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
- ];
1826
+ init_config();
1827
+ BASE_BRANCHES = ["dev", "main", "master"];
1828
+ _hookSafeEnv = null;
1294
1829
  }
1295
1830
  });
1296
1831
 
1297
1832
  // src/pipeline/state.ts
1298
- import * as fs10 from "fs";
1299
- import * as path8 from "path";
1833
+ import * as fs12 from "fs";
1834
+ import * as path10 from "path";
1300
1835
  function loadState(taskId, taskDir) {
1301
- const p = path8.join(taskDir, "status.json");
1302
- if (!fs10.existsSync(p)) return null;
1836
+ const p = path10.join(taskDir, "status.json");
1837
+ if (!fs12.existsSync(p)) return null;
1303
1838
  try {
1304
1839
  const result = parseJsonSafe(
1305
- fs10.readFileSync(p, "utf-8"),
1840
+ fs12.readFileSync(p, "utf-8"),
1306
1841
  ["taskId", "state", "stages", "createdAt", "updatedAt"]
1307
1842
  );
1308
1843
  if (!result.ok) {
@@ -1320,10 +1855,10 @@ function writeState(state, taskDir) {
1320
1855
  ...state,
1321
1856
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1322
1857
  };
1323
- const target = path8.join(taskDir, "status.json");
1858
+ const target = path10.join(taskDir, "status.json");
1324
1859
  const tmp = target + ".tmp";
1325
- fs10.writeFileSync(tmp, JSON.stringify(updated, null, 2));
1326
- fs10.renameSync(tmp, target);
1860
+ fs12.writeFileSync(tmp, JSON.stringify(updated, null, 2));
1861
+ fs12.renameSync(tmp, target);
1327
1862
  return updated;
1328
1863
  }
1329
1864
  function initState(taskId) {
@@ -1364,16 +1899,16 @@ var init_complexity = __esm({
1364
1899
  });
1365
1900
 
1366
1901
  // src/memory.ts
1367
- import * as fs11 from "fs";
1368
- import * as path9 from "path";
1902
+ import * as fs13 from "fs";
1903
+ import * as path11 from "path";
1369
1904
  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();
1905
+ const memoryDir = path11.join(projectDir, ".kody", "memory");
1906
+ if (!fs13.existsSync(memoryDir)) return "";
1907
+ const files = fs13.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
1373
1908
  if (files.length === 0) return "";
1374
1909
  const sections = [];
1375
1910
  for (const file of files) {
1376
- const content = fs11.readFileSync(path9.join(memoryDir, file), "utf-8").trim();
1911
+ const content = fs13.readFileSync(path11.join(memoryDir, file), "utf-8").trim();
1377
1912
  if (content) {
1378
1913
  sections.push(`## ${file.replace(".md", "")}
1379
1914
  ${content}`);
@@ -1392,8 +1927,8 @@ var init_memory = __esm({
1392
1927
  });
1393
1928
 
1394
1929
  // src/context-tiers.ts
1395
- import * as fs12 from "fs";
1396
- import * as path10 from "path";
1930
+ import * as fs14 from "fs";
1931
+ import * as path12 from "path";
1397
1932
  function estimateTokens(text) {
1398
1933
  return Math.ceil(text.length / 4);
1399
1934
  }
@@ -1484,7 +2019,7 @@ function generateL1Json(content) {
1484
2019
  }
1485
2020
  }
1486
2021
  function getTieredContent(filePath, content) {
1487
- const key = path10.basename(filePath);
2022
+ const key = path12.basename(filePath);
1488
2023
  return {
1489
2024
  source: filePath,
1490
2025
  L0: generateL0(content, key),
@@ -1496,15 +2031,15 @@ function selectTier(tiered, tier) {
1496
2031
  return tiered[tier];
1497
2032
  }
1498
2033
  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();
2034
+ const memoryDir = path12.join(projectDir, ".kody", "memory");
2035
+ if (!fs14.existsSync(memoryDir)) return "";
2036
+ const files = fs14.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
1502
2037
  if (files.length === 0) return "";
1503
2038
  const tierLabel2 = tier === "L2" ? "full" : tier === "L1" ? "overview" : "abstract";
1504
2039
  const sections = [];
1505
2040
  for (const file of files) {
1506
- const filePath = path10.join(memoryDir, file);
1507
- const content = fs12.readFileSync(filePath, "utf-8").trim();
2041
+ const filePath = path12.join(memoryDir, file);
2042
+ const content = fs14.readFileSync(filePath, "utf-8").trim();
1508
2043
  if (!content) continue;
1509
2044
  const tiered = getTieredContent(filePath, content);
1510
2045
  const selected = selectTier(tiered, tier);
@@ -1527,9 +2062,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
1527
2062
  `;
1528
2063
  context += `Task Directory: ${taskDir}
1529
2064
  `;
1530
- const taskMdPath = path10.join(taskDir, "task.md");
1531
- if (fs12.existsSync(taskMdPath)) {
1532
- const content = fs12.readFileSync(taskMdPath, "utf-8");
2065
+ const taskMdPath = path12.join(taskDir, "task.md");
2066
+ if (fs14.existsSync(taskMdPath)) {
2067
+ const content = fs14.readFileSync(taskMdPath, "utf-8");
1533
2068
  const selected = selectContent(taskMdPath, content, policy.taskDescription);
1534
2069
  const label = tierLabel("Task Description", policy.taskDescription);
1535
2070
  context += `
@@ -1537,9 +2072,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
1537
2072
  ${selected}
1538
2073
  `;
1539
2074
  }
1540
- const taskJsonPath = path10.join(taskDir, "task.json");
1541
- if (fs12.existsSync(taskJsonPath)) {
1542
- const content = fs12.readFileSync(taskJsonPath, "utf-8");
2075
+ const taskJsonPath = path12.join(taskDir, "task.json");
2076
+ if (fs14.existsSync(taskJsonPath)) {
2077
+ const content = fs14.readFileSync(taskJsonPath, "utf-8");
1543
2078
  if (policy.taskClassification === "L2") {
1544
2079
  try {
1545
2080
  const taskDef = JSON.parse(content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, ""));
@@ -1565,9 +2100,9 @@ ${selected}
1565
2100
  }
1566
2101
  }
1567
2102
  }
1568
- const specPath = path10.join(taskDir, "spec.md");
1569
- if (fs12.existsSync(specPath)) {
1570
- const content = fs12.readFileSync(specPath, "utf-8");
2103
+ const specPath = path12.join(taskDir, "spec.md");
2104
+ if (fs14.existsSync(specPath)) {
2105
+ const content = fs14.readFileSync(specPath, "utf-8");
1571
2106
  const selected = selectContent(specPath, content, policy.spec);
1572
2107
  const label = tierLabel("Spec", policy.spec);
1573
2108
  context += `
@@ -1575,9 +2110,9 @@ ${selected}
1575
2110
  ${selected}
1576
2111
  `;
1577
2112
  }
1578
- const planPath = path10.join(taskDir, "plan.md");
1579
- if (fs12.existsSync(planPath)) {
1580
- const content = fs12.readFileSync(planPath, "utf-8");
2113
+ const planPath = path12.join(taskDir, "plan.md");
2114
+ if (fs14.existsSync(planPath)) {
2115
+ const content = fs14.readFileSync(planPath, "utf-8");
1581
2116
  const selected = selectContent(planPath, content, policy.plan);
1582
2117
  const label = tierLabel("Plan", policy.plan);
1583
2118
  context += `
@@ -1585,9 +2120,9 @@ ${selected}
1585
2120
  ${selected}
1586
2121
  `;
1587
2122
  }
1588
- const contextMdPath = path10.join(taskDir, "context.md");
1589
- if (fs12.existsSync(contextMdPath)) {
1590
- const content = fs12.readFileSync(contextMdPath, "utf-8");
2123
+ const contextMdPath = path12.join(taskDir, "context.md");
2124
+ if (fs14.existsSync(contextMdPath)) {
2125
+ const content = fs14.readFileSync(contextMdPath, "utf-8");
1591
2126
  const selected = selectContent(contextMdPath, content, policy.accumulatedContext);
1592
2127
  const label = tierLabel("Previous Stage Context", policy.accumulatedContext);
1593
2128
  context += `
@@ -1672,71 +2207,25 @@ var init_context_tiers = __esm({
1672
2207
  }
1673
2208
  });
1674
2209
 
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
2210
  // src/context.ts
1722
- import * as fs13 from "fs";
1723
- import * as path11 from "path";
2211
+ import * as fs15 from "fs";
2212
+ import * as path13 from "path";
1724
2213
  function readPromptFile(stageName, projectDir) {
1725
2214
  if (projectDir) {
1726
- const stepFile = path11.join(projectDir, ".kody", "steps", `${stageName}.md`);
1727
- if (fs13.existsSync(stepFile)) {
1728
- return fs13.readFileSync(stepFile, "utf-8");
2215
+ const stepFile = path13.join(projectDir, ".kody", "steps", `${stageName}.md`);
2216
+ if (fs15.existsSync(stepFile)) {
2217
+ return fs15.readFileSync(stepFile, "utf-8");
1729
2218
  }
1730
2219
  console.warn(` \u26A0 No step file at ${stepFile}, falling back to engine defaults. Run 'kody-engine-lite init --force' to generate step files.`);
1731
2220
  }
1732
2221
  const scriptDir = new URL(".", import.meta.url).pathname;
1733
2222
  const candidates = [
1734
- path11.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
1735
- path11.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
2223
+ path13.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
2224
+ path13.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
1736
2225
  ];
1737
2226
  for (const candidate of candidates) {
1738
- if (fs13.existsSync(candidate)) {
1739
- return fs13.readFileSync(candidate, "utf-8");
2227
+ if (fs15.existsSync(candidate)) {
2228
+ return fs15.readFileSync(candidate, "utf-8");
1740
2229
  }
1741
2230
  }
1742
2231
  throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
@@ -1748,18 +2237,18 @@ function injectTaskContext(prompt, taskId, taskDir, feedback) {
1748
2237
  `;
1749
2238
  context += `Task Directory: ${taskDir}
1750
2239
  `;
1751
- const taskMdPath = path11.join(taskDir, "task.md");
1752
- if (fs13.existsSync(taskMdPath)) {
1753
- const taskMd = fs13.readFileSync(taskMdPath, "utf-8");
2240
+ const taskMdPath = path13.join(taskDir, "task.md");
2241
+ if (fs15.existsSync(taskMdPath)) {
2242
+ const taskMd = fs15.readFileSync(taskMdPath, "utf-8");
1754
2243
  context += `
1755
2244
  ## Task Description
1756
2245
  ${taskMd}
1757
2246
  `;
1758
2247
  }
1759
- const taskJsonPath = path11.join(taskDir, "task.json");
1760
- if (fs13.existsSync(taskJsonPath)) {
2248
+ const taskJsonPath = path13.join(taskDir, "task.json");
2249
+ if (fs15.existsSync(taskJsonPath)) {
1761
2250
  try {
1762
- const taskDef = JSON.parse(fs13.readFileSync(taskJsonPath, "utf-8"));
2251
+ const taskDef = JSON.parse(fs15.readFileSync(taskJsonPath, "utf-8"));
1763
2252
  context += `
1764
2253
  ## Task Classification
1765
2254
  `;
@@ -1772,27 +2261,27 @@ ${taskMd}
1772
2261
  } catch {
1773
2262
  }
1774
2263
  }
1775
- const specPath = path11.join(taskDir, "spec.md");
1776
- if (fs13.existsSync(specPath)) {
1777
- const spec = fs13.readFileSync(specPath, "utf-8");
2264
+ const specPath = path13.join(taskDir, "spec.md");
2265
+ if (fs15.existsSync(specPath)) {
2266
+ const spec = fs15.readFileSync(specPath, "utf-8");
1778
2267
  const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
1779
2268
  context += `
1780
2269
  ## Spec Summary
1781
2270
  ${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
1782
2271
  `;
1783
2272
  }
1784
- const planPath = path11.join(taskDir, "plan.md");
1785
- if (fs13.existsSync(planPath)) {
1786
- const plan = fs13.readFileSync(planPath, "utf-8");
2273
+ const planPath = path13.join(taskDir, "plan.md");
2274
+ if (fs15.existsSync(planPath)) {
2275
+ const plan = fs15.readFileSync(planPath, "utf-8");
1787
2276
  const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
1788
2277
  context += `
1789
2278
  ## Plan Summary
1790
2279
  ${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
1791
2280
  `;
1792
2281
  }
1793
- const contextMdPath = path11.join(taskDir, "context.md");
1794
- if (fs13.existsSync(contextMdPath)) {
1795
- const accumulated = fs13.readFileSync(contextMdPath, "utf-8");
2282
+ const contextMdPath = path13.join(taskDir, "context.md");
2283
+ if (fs15.existsSync(contextMdPath)) {
2284
+ const accumulated = fs15.readFileSync(contextMdPath, "utf-8");
1796
2285
  const truncated = accumulated.slice(-MAX_ACCUMULATED_CONTEXT);
1797
2286
  const prefix = accumulated.length > MAX_ACCUMULATED_CONTEXT ? "...(earlier context truncated)\n" : "";
1798
2287
  context += `
@@ -1810,17 +2299,17 @@ ${feedback}
1810
2299
  }
1811
2300
  function inferHasUIFromScope(scope) {
1812
2301
  return scope.some((filePath) => {
1813
- const ext = path11.extname(filePath).toLowerCase();
2302
+ const ext = path13.extname(filePath).toLowerCase();
1814
2303
  if (UI_EXTENSIONS.has(ext)) return true;
1815
2304
  const normalized = filePath.replace(/\\/g, "/");
1816
2305
  return UI_PATH_SEGMENTS.some((seg) => normalized.includes(seg));
1817
2306
  });
1818
2307
  }
1819
2308
  function taskHasUI(taskDir) {
1820
- const taskJsonPath = path11.join(taskDir, "task.json");
1821
- if (!fs13.existsSync(taskJsonPath)) return true;
2309
+ const taskJsonPath = path13.join(taskDir, "task.json");
2310
+ if (!fs15.existsSync(taskJsonPath)) return true;
1822
2311
  try {
1823
- const taskDef = JSON.parse(fs13.readFileSync(taskJsonPath, "utf-8"));
2312
+ const taskDef = JSON.parse(fs15.readFileSync(taskJsonPath, "utf-8"));
1824
2313
  const scope = Array.isArray(taskDef.scope) ? taskDef.scope : [];
1825
2314
  if (scope.length === 0) return true;
1826
2315
  return inferHasUIFromScope(scope);
@@ -1942,9 +2431,9 @@ ${prompt}` : prompt;
1942
2431
  }
1943
2432
  if (isMcpEnabledForStage(stageName, config.mcp) && taskHasUI(taskDir)) {
1944
2433
  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();
2434
+ const qaGuidePath = path13.join(projectDir, ".kody", "qa-guide.md");
2435
+ if (fs15.existsSync(qaGuidePath)) {
2436
+ const qaGuide = fs15.readFileSync(qaGuidePath, "utf-8").trim();
1948
2437
  assembled = assembled + "\n\n" + qaGuide;
1949
2438
  }
1950
2439
  }
@@ -1977,7 +2466,7 @@ function resolveModel(modelTier, stageName) {
1977
2466
  if (mapped) return mapped;
1978
2467
  return DEFAULT_MODEL_MAP[modelTier] ?? "sonnet";
1979
2468
  }
1980
- var DEFAULT_MODEL_MAP, MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT, UI_EXTENSIONS, UI_PATH_SEGMENTS, TIER_ESCALATION;
2469
+ var MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT, UI_EXTENSIONS, UI_PATH_SEGMENTS, TIER_ESCALATION, DEFAULT_MODEL_MAP;
1981
2470
  var init_context = __esm({
1982
2471
  "src/context.ts"() {
1983
2472
  "use strict";
@@ -1985,11 +2474,6 @@ var init_context = __esm({
1985
2474
  init_config();
1986
2475
  init_context_tiers();
1987
2476
  init_mcp_config();
1988
- DEFAULT_MODEL_MAP = {
1989
- cheap: "haiku",
1990
- mid: "sonnet",
1991
- strong: "opus"
1992
- };
1993
2477
  MAX_TASK_CONTEXT_PLAN = 1500;
1994
2478
  MAX_TASK_CONTEXT_SPEC = 2e3;
1995
2479
  MAX_ACCUMULATED_CONTEXT = 4e3;
@@ -2016,6 +2500,11 @@ var init_context = __esm({
2016
2500
  mid: "strong",
2017
2501
  strong: "strong"
2018
2502
  };
2503
+ DEFAULT_MODEL_MAP = {
2504
+ cheap: "haiku",
2505
+ mid: "sonnet",
2506
+ strong: "opus"
2507
+ };
2019
2508
  }
2020
2509
  });
2021
2510
 
@@ -2039,8 +2528,8 @@ var init_runner_selection = __esm({
2039
2528
  });
2040
2529
 
2041
2530
  // src/stages/agent.ts
2042
- import * as fs14 from "fs";
2043
- import * as path12 from "path";
2531
+ import * as fs16 from "fs";
2532
+ import * as path14 from "path";
2044
2533
  function getSessionInfo(stageName, sessions) {
2045
2534
  const group = SESSION_GROUP[stageName];
2046
2535
  if (!group) return void 0;
@@ -2127,27 +2616,27 @@ async function executeAgentStage(ctx, def) {
2127
2616
  }
2128
2617
  const result = lastResult;
2129
2618
  if (def.outputFile && result.output) {
2130
- fs14.writeFileSync(path12.join(ctx.taskDir, def.outputFile), result.output);
2619
+ fs16.writeFileSync(path14.join(ctx.taskDir, def.outputFile), result.output);
2131
2620
  }
2132
2621
  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);
2622
+ const outputPath = path14.join(ctx.taskDir, def.outputFile);
2623
+ if (!fs16.existsSync(outputPath)) {
2624
+ const ext = path14.extname(def.outputFile);
2625
+ const base = path14.basename(def.outputFile, ext);
2626
+ const files = fs16.readdirSync(ctx.taskDir);
2138
2627
  const variant = files.find(
2139
2628
  (f) => f.startsWith(base + "-") && f.endsWith(ext)
2140
2629
  );
2141
2630
  if (variant) {
2142
- fs14.renameSync(path12.join(ctx.taskDir, variant), outputPath);
2631
+ fs16.renameSync(path14.join(ctx.taskDir, variant), outputPath);
2143
2632
  logger.info(` Renamed variant ${variant} \u2192 ${def.outputFile}`);
2144
2633
  }
2145
2634
  }
2146
2635
  }
2147
2636
  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");
2637
+ const outputPath = path14.join(ctx.taskDir, def.outputFile);
2638
+ if (fs16.existsSync(outputPath)) {
2639
+ const content = fs16.readFileSync(outputPath, "utf-8");
2151
2640
  const validation = validateStageOutput(def.name, content);
2152
2641
  if (!validation.valid) {
2153
2642
  if (def.name === "taskify") {
@@ -2161,7 +2650,7 @@ async function executeAgentStage(ctx, def) {
2161
2650
  const stripped = stripFences(retryResult.output);
2162
2651
  const retryValidation = validateTaskJson(stripped);
2163
2652
  if (retryValidation.valid) {
2164
- fs14.writeFileSync(outputPath, retryResult.output);
2653
+ fs16.writeFileSync(outputPath, retryResult.output);
2165
2654
  logger.info(` taskify retry produced valid JSON`);
2166
2655
  } else {
2167
2656
  logger.warn(` taskify retry still invalid: ${retryValidation.error}`);
@@ -2174,7 +2663,7 @@ async function executeAgentStage(ctx, def) {
2174
2663
  risk_level: "low",
2175
2664
  questions: []
2176
2665
  }, null, 2);
2177
- fs14.writeFileSync(outputPath, fallback);
2666
+ fs16.writeFileSync(outputPath, fallback);
2178
2667
  logger.info(` taskify fallback: generated minimal task.json (risk_level=low)`);
2179
2668
  }
2180
2669
  }
@@ -2188,7 +2677,7 @@ async function executeAgentStage(ctx, def) {
2188
2677
  return { outcome: "completed", outputFile: def.outputFile, retries };
2189
2678
  }
2190
2679
  function appendStageContext(taskDir, stageName, output) {
2191
- const contextPath = path12.join(taskDir, "context.md");
2680
+ const contextPath = path14.join(taskDir, "context.md");
2192
2681
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19);
2193
2682
  let summary;
2194
2683
  if (output && output.trim()) {
@@ -2201,7 +2690,7 @@ function appendStageContext(taskDir, stageName, output) {
2201
2690
  ### ${stageName} (${timestamp2})
2202
2691
  ${summary}
2203
2692
  `;
2204
- fs14.appendFileSync(contextPath, entry);
2693
+ fs16.appendFileSync(contextPath, entry);
2205
2694
  }
2206
2695
  var SESSION_GROUP;
2207
2696
  var init_agent = __esm({
@@ -2224,7 +2713,7 @@ var init_agent = __esm({
2224
2713
  });
2225
2714
 
2226
2715
  // src/verify-runner.ts
2227
- import { execFileSync as execFileSync9 } from "child_process";
2716
+ import { execFileSync as execFileSync10 } from "child_process";
2228
2717
  function isExecError(err) {
2229
2718
  return typeof err === "object" && err !== null;
2230
2719
  }
@@ -2260,7 +2749,7 @@ function runCommand(cmd, cwd, timeout) {
2260
2749
  return { success: true, output: "", timedOut: false };
2261
2750
  }
2262
2751
  try {
2263
- const output = execFileSync9(parts[0], parts.slice(1), {
2752
+ const output = execFileSync10(parts[0], parts.slice(1), {
2264
2753
  cwd,
2265
2754
  timeout,
2266
2755
  encoding: "utf-8",
@@ -2331,7 +2820,7 @@ var init_verify_runner = __esm({
2331
2820
  });
2332
2821
 
2333
2822
  // src/observer.ts
2334
- import { execFileSync as execFileSync10 } from "child_process";
2823
+ import { execFileSync as execFileSync11 } from "child_process";
2335
2824
  async function diagnoseFailure(stageName, errorOutput, modifiedFiles, runner, model, options) {
2336
2825
  const context = [
2337
2826
  `Stage: ${stageName}`,
@@ -2391,13 +2880,13 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
2391
2880
  }
2392
2881
  function getModifiedFiles(projectDir) {
2393
2882
  try {
2394
- const staged = execFileSync10("git", ["diff", "--name-only", "--cached"], {
2883
+ const staged = execFileSync11("git", ["diff", "--name-only", "--cached"], {
2395
2884
  encoding: "utf-8",
2396
2885
  cwd: projectDir,
2397
2886
  timeout: 5e3,
2398
2887
  stdio: ["pipe", "pipe", "pipe"]
2399
2888
  }).trim();
2400
- const unstaged = execFileSync10("git", ["diff", "--name-only"], {
2889
+ const unstaged = execFileSync11("git", ["diff", "--name-only"], {
2401
2890
  encoding: "utf-8",
2402
2891
  cwd: projectDir,
2403
2892
  timeout: 5e3,
@@ -2440,8 +2929,8 @@ Error context:
2440
2929
  });
2441
2930
 
2442
2931
  // src/stages/gate.ts
2443
- import * as fs15 from "fs";
2444
- import * as path13 from "path";
2932
+ import * as fs17 from "fs";
2933
+ import * as path15 from "path";
2445
2934
  function executeGateStage(ctx, def) {
2446
2935
  if (ctx.input.dryRun) {
2447
2936
  logger.info(` [dry-run] skipping ${def.name}`);
@@ -2484,7 +2973,7 @@ ${output}
2484
2973
  `);
2485
2974
  }
2486
2975
  }
2487
- fs15.writeFileSync(path13.join(ctx.taskDir, "verify.md"), lines.join(""));
2976
+ fs17.writeFileSync(path15.join(ctx.taskDir, "verify.md"), lines.join(""));
2488
2977
  return {
2489
2978
  outcome: verifyResult.pass ? "completed" : "failed",
2490
2979
  retries: 0
@@ -2499,9 +2988,9 @@ var init_gate = __esm({
2499
2988
  });
2500
2989
 
2501
2990
  // src/stages/verify.ts
2502
- import * as fs16 from "fs";
2503
- import * as path14 from "path";
2504
- import { execFileSync as execFileSync11 } from "child_process";
2991
+ import * as fs18 from "fs";
2992
+ import * as path16 from "path";
2993
+ import { execFileSync as execFileSync12 } from "child_process";
2505
2994
  async function executeVerifyWithAutofix(ctx, def) {
2506
2995
  const maxAttempts = def.maxRetries ?? 2;
2507
2996
  for (let attempt = 0; attempt <= maxAttempts; attempt++) {
@@ -2511,8 +3000,8 @@ async function executeVerifyWithAutofix(ctx, def) {
2511
3000
  return { ...gateResult, retries: attempt };
2512
3001
  }
2513
3002
  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";
3003
+ const verifyPath = path16.join(ctx.taskDir, "verify.md");
3004
+ const errorOutput = fs18.existsSync(verifyPath) ? fs18.readFileSync(verifyPath, "utf-8") : "Unknown error";
2516
3005
  const modifiedFiles = getModifiedFiles(ctx.projectDir);
2517
3006
  const defaultRunner = getRunnerForStage(ctx, "taskify");
2518
3007
  const diagConfig = getProjectConfig();
@@ -2555,7 +3044,7 @@ ${diagnosis.resolution}`);
2555
3044
  const parts = parseCommand(cmd);
2556
3045
  if (parts.length === 0) return;
2557
3046
  try {
2558
- execFileSync11(parts[0], parts.slice(1), {
3047
+ execFileSync12(parts[0], parts.slice(1), {
2559
3048
  stdio: "pipe",
2560
3049
  timeout: FIX_COMMAND_TIMEOUT_MS
2561
3050
  });
@@ -2607,73 +3096,9 @@ var init_verify = __esm({
2607
3096
  }
2608
3097
  });
2609
3098
 
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];
2653
- }
2654
- }
2655
- return latestTaskId;
2656
- } catch {
2657
- return null;
2658
- }
2659
- }
2660
- function resolveTaskIdForCommand(issueNumber, projectDir) {
2661
- const fromTasks = findLatestTaskForIssue(issueNumber, projectDir);
2662
- if (fromTasks) return fromTasks;
2663
- const fromComments = resolveTaskIdFromComments(issueNumber);
2664
- if (fromComments) return fromComments;
2665
- return null;
2666
- }
2667
- var init_task_resolution = __esm({
2668
- "src/cli/task-resolution.ts"() {
2669
- "use strict";
2670
- init_github_api();
2671
- }
2672
- });
2673
-
2674
3099
  // src/review-standalone.ts
2675
- import * as fs18 from "fs";
2676
- import * as path16 from "path";
3100
+ import * as fs19 from "fs";
3101
+ import * as path17 from "path";
2677
3102
  function resolveReviewTarget(input) {
2678
3103
  if (input.prs.length === 0) {
2679
3104
  return {
@@ -2697,8 +3122,8 @@ Or comment on the specific PR: \`@kody review\``
2697
3122
  }
2698
3123
  async function runStandaloneReview(input) {
2699
3124
  const taskId = input.taskId ?? `review-${generateTaskId()}`;
2700
- const taskDir = path16.join(input.projectDir, ".kody", "tasks", taskId);
2701
- fs18.mkdirSync(taskDir, { recursive: true });
3125
+ const taskDir = path17.join(input.projectDir, ".kody", "tasks", taskId);
3126
+ fs19.mkdirSync(taskDir, { recursive: true });
2702
3127
  let diffInstruction = "";
2703
3128
  let filesChangedSection = "";
2704
3129
  if (input.baseBranch) {
@@ -2725,7 +3150,7 @@ ${fileList}`;
2725
3150
  const taskContent = `# ${input.prTitle}
2726
3151
 
2727
3152
  ${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
2728
- fs18.writeFileSync(path16.join(taskDir, "task.md"), taskContent);
3153
+ fs19.writeFileSync(path17.join(taskDir, "task.md"), taskContent);
2729
3154
  const reviewDef = STAGES.find((s) => s.name === "review");
2730
3155
  const ctx = {
2731
3156
  taskId,
@@ -2747,10 +3172,10 @@ ${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
2747
3172
  error: result.error ?? "Review stage failed"
2748
3173
  };
2749
3174
  }
2750
- const reviewPath = path16.join(taskDir, "review.md");
3175
+ const reviewPath = path17.join(taskDir, "review.md");
2751
3176
  let reviewContent;
2752
- if (fs18.existsSync(reviewPath)) {
2753
- reviewContent = fs18.readFileSync(reviewPath, "utf-8");
3177
+ if (fs19.existsSync(reviewPath)) {
3178
+ reviewContent = fs19.readFileSync(reviewPath, "utf-8");
2754
3179
  }
2755
3180
  return {
2756
3181
  outcome: "completed",
@@ -2790,8 +3215,8 @@ var init_review_standalone = __esm({
2790
3215
  });
2791
3216
 
2792
3217
  // src/stages/review.ts
2793
- import * as fs19 from "fs";
2794
- import * as path17 from "path";
3218
+ import * as fs20 from "fs";
3219
+ import * as path18 from "path";
2795
3220
  async function executeReviewWithFix(ctx, def) {
2796
3221
  if (ctx.input.dryRun) {
2797
3222
  return { outcome: "completed", retries: 0 };
@@ -2805,11 +3230,11 @@ async function executeReviewWithFix(ctx, def) {
2805
3230
  if (reviewResult.outcome !== "completed") {
2806
3231
  return reviewResult;
2807
3232
  }
2808
- const reviewFile = path17.join(ctx.taskDir, "review.md");
2809
- if (!fs19.existsSync(reviewFile)) {
3233
+ const reviewFile = path18.join(ctx.taskDir, "review.md");
3234
+ if (!fs20.existsSync(reviewFile)) {
2810
3235
  return { outcome: "failed", retries: iteration, error: "review.md not found" };
2811
3236
  }
2812
- const content = fs19.readFileSync(reviewFile, "utf-8");
3237
+ const content = fs20.readFileSync(reviewFile, "utf-8");
2813
3238
  if (detectReviewVerdict(content) !== "fail") {
2814
3239
  return { ...reviewResult, retries: iteration };
2815
3240
  }
@@ -2838,15 +3263,15 @@ var init_review = __esm({
2838
3263
  });
2839
3264
 
2840
3265
  // src/stages/ship.ts
2841
- import * as fs20 from "fs";
2842
- import * as path18 from "path";
3266
+ import * as fs21 from "fs";
3267
+ import * as path19 from "path";
2843
3268
  import { execFileSync as execFileSync13 } from "child_process";
2844
3269
  function buildPrBody(ctx) {
2845
3270
  const sections = [];
2846
- const taskJsonPath = path18.join(ctx.taskDir, "task.json");
2847
- if (fs20.existsSync(taskJsonPath)) {
3271
+ const taskJsonPath = path19.join(ctx.taskDir, "task.json");
3272
+ if (fs21.existsSync(taskJsonPath)) {
2848
3273
  try {
2849
- const raw = fs20.readFileSync(taskJsonPath, "utf-8");
3274
+ const raw = fs21.readFileSync(taskJsonPath, "utf-8");
2850
3275
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2851
3276
  const task = JSON.parse(cleaned);
2852
3277
  if (task.description) {
@@ -2865,9 +3290,9 @@ ${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
2865
3290
  } catch {
2866
3291
  }
2867
3292
  }
2868
- const reviewPath = path18.join(ctx.taskDir, "review.md");
2869
- if (fs20.existsSync(reviewPath)) {
2870
- const review = fs20.readFileSync(reviewPath, "utf-8");
3293
+ const reviewPath = path19.join(ctx.taskDir, "review.md");
3294
+ if (fs21.existsSync(reviewPath)) {
3295
+ const review = fs21.readFileSync(reviewPath, "utf-8");
2871
3296
  const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
2872
3297
  if (summaryMatch) {
2873
3298
  const summary = summaryMatch[1].trim();
@@ -2884,14 +3309,14 @@ ${summary}`);
2884
3309
  **Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
2885
3310
  }
2886
3311
  }
2887
- const verifyPath = path18.join(ctx.taskDir, "verify.md");
2888
- if (fs20.existsSync(verifyPath)) {
2889
- const verify = fs20.readFileSync(verifyPath, "utf-8");
3312
+ const verifyPath = path19.join(ctx.taskDir, "verify.md");
3313
+ if (fs21.existsSync(verifyPath)) {
3314
+ const verify = fs21.readFileSync(verifyPath, "utf-8");
2890
3315
  if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
2891
3316
  }
2892
- const planPath = path18.join(ctx.taskDir, "plan.md");
2893
- if (fs20.existsSync(planPath)) {
2894
- const plan = fs20.readFileSync(planPath, "utf-8").trim();
3317
+ const planPath = path19.join(ctx.taskDir, "plan.md");
3318
+ if (fs21.existsSync(planPath)) {
3319
+ const plan = fs21.readFileSync(planPath, "utf-8").trim();
2895
3320
  if (plan) {
2896
3321
  const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
2897
3322
  sections.push(`
@@ -2911,13 +3336,13 @@ Closes #${ctx.input.issueNumber}`);
2911
3336
  return sections.join("\n");
2912
3337
  }
2913
3338
  function executeShipStage(ctx, _def) {
2914
- const shipPath = path18.join(ctx.taskDir, "ship.md");
3339
+ const shipPath = path19.join(ctx.taskDir, "ship.md");
2915
3340
  if (ctx.input.dryRun) {
2916
- fs20.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
3341
+ fs21.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
2917
3342
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
2918
3343
  }
2919
3344
  if (ctx.input.local && !ctx.input.issueNumber) {
2920
- fs20.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
3345
+ fs21.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
2921
3346
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
2922
3347
  }
2923
3348
  try {
@@ -2964,28 +3389,28 @@ function executeShipStage(ctx, _def) {
2964
3389
  chore: "chore"
2965
3390
  };
2966
3391
  let prefix = "chore";
2967
- const taskJsonPath = path18.join(ctx.taskDir, "task.json");
2968
- if (fs20.existsSync(taskJsonPath)) {
3392
+ const taskJsonPath = path19.join(ctx.taskDir, "task.json");
3393
+ if (fs21.existsSync(taskJsonPath)) {
2969
3394
  try {
2970
- const raw = fs20.readFileSync(taskJsonPath, "utf-8");
3395
+ const raw = fs21.readFileSync(taskJsonPath, "utf-8");
2971
3396
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2972
3397
  const task = JSON.parse(cleaned);
2973
3398
  prefix = TYPE_PREFIX[task.task_type] ?? "chore";
2974
3399
  } catch {
2975
3400
  }
2976
3401
  }
2977
- const taskMdPath = path18.join(ctx.taskDir, "task.md");
2978
- if (fs20.existsSync(taskMdPath)) {
2979
- const content = fs20.readFileSync(taskMdPath, "utf-8");
3402
+ const taskMdPath = path19.join(ctx.taskDir, "task.md");
3403
+ if (fs21.existsSync(taskMdPath)) {
3404
+ const content = fs21.readFileSync(taskMdPath, "utf-8");
2980
3405
  const heading = content.split("\n").find((l) => l.startsWith("# "));
2981
3406
  if (heading) {
2982
3407
  title = `${prefix}: ${heading.replace(/^#\s*/, "").trim()}`.slice(0, 72);
2983
3408
  }
2984
3409
  }
2985
3410
  if (title === "Update") {
2986
- if (fs20.existsSync(taskJsonPath)) {
3411
+ if (fs21.existsSync(taskJsonPath)) {
2987
3412
  try {
2988
- const raw = fs20.readFileSync(taskJsonPath, "utf-8");
3413
+ const raw = fs21.readFileSync(taskJsonPath, "utf-8");
2989
3414
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2990
3415
  const task = JSON.parse(cleaned);
2991
3416
  if (task.title) title = `${prefix}: ${task.title}`.slice(0, 72);
@@ -3008,7 +3433,7 @@ function executeShipStage(ctx, _def) {
3008
3433
  } catch {
3009
3434
  }
3010
3435
  }
3011
- fs20.writeFileSync(shipPath, `# Ship
3436
+ fs21.writeFileSync(shipPath, `# Ship
3012
3437
 
3013
3438
  Updated existing PR: ${existingPr.url}
3014
3439
  PR #${existingPr.number}
@@ -3029,20 +3454,20 @@ PR #${existingPr.number}
3029
3454
  } catch {
3030
3455
  }
3031
3456
  }
3032
- fs20.writeFileSync(shipPath, `# Ship
3457
+ fs21.writeFileSync(shipPath, `# Ship
3033
3458
 
3034
3459
  PR created: ${pr.url}
3035
3460
  PR #${pr.number}
3036
3461
  `);
3037
3462
  } else {
3038
- fs20.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
3463
+ fs21.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
3039
3464
  }
3040
3465
  }
3041
3466
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
3042
3467
  } catch (err) {
3043
3468
  const msg = err instanceof Error ? err.message : String(err);
3044
3469
  try {
3045
- fs20.writeFileSync(shipPath, `# Ship
3470
+ fs21.writeFileSync(shipPath, `# Ship
3046
3471
 
3047
3472
  Failed: ${msg}
3048
3473
  `);
@@ -3091,15 +3516,15 @@ var init_executor_registry = __esm({
3091
3516
  });
3092
3517
 
3093
3518
  // src/pipeline/questions.ts
3094
- import * as fs21 from "fs";
3095
- import * as path19 from "path";
3519
+ import * as fs22 from "fs";
3520
+ import * as path20 from "path";
3096
3521
  function checkForQuestions(ctx, stageName) {
3097
3522
  if (ctx.input.local || !ctx.input.issueNumber) return false;
3098
3523
  try {
3099
3524
  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");
3525
+ const taskJsonPath = path20.join(ctx.taskDir, "task.json");
3526
+ if (!fs22.existsSync(taskJsonPath)) return false;
3527
+ const raw = fs22.readFileSync(taskJsonPath, "utf-8");
3103
3528
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3104
3529
  const taskJson = JSON.parse(cleaned);
3105
3530
  if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
@@ -3114,9 +3539,9 @@ Reply with \`@kody approve\` and your answers in the comment body.`;
3114
3539
  }
3115
3540
  }
3116
3541
  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");
3542
+ const planPath = path20.join(ctx.taskDir, "plan.md");
3543
+ if (!fs22.existsSync(planPath)) return false;
3544
+ const plan = fs22.readFileSync(planPath, "utf-8");
3120
3545
  const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
3121
3546
  if (questionsMatch) {
3122
3547
  const questionsText = questionsMatch[1].trim();
@@ -3145,8 +3570,8 @@ var init_questions = __esm({
3145
3570
  });
3146
3571
 
3147
3572
  // src/pipeline/hooks.ts
3148
- import * as fs22 from "fs";
3149
- import * as path20 from "path";
3573
+ import * as fs23 from "fs";
3574
+ import * as path21 from "path";
3150
3575
  function applyPreStageLabel(ctx, def) {
3151
3576
  if (!ctx.input.issueNumber || ctx.input.local) return;
3152
3577
  if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
@@ -3184,9 +3609,9 @@ function autoDetectComplexity(ctx, def) {
3184
3609
  return { complexity, activeStages };
3185
3610
  }
3186
3611
  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");
3612
+ const taskJsonPath = path21.join(ctx.taskDir, "task.json");
3613
+ if (!fs23.existsSync(taskJsonPath)) return null;
3614
+ const raw = fs23.readFileSync(taskJsonPath, "utf-8");
3190
3615
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3191
3616
  const taskJson = JSON.parse(cleaned);
3192
3617
  if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
@@ -3216,8 +3641,8 @@ function checkRiskGate(ctx, def, state, complexity) {
3216
3641
  if (ctx.input.dryRun || ctx.input.local) return null;
3217
3642
  if (ctx.input.mode === "rerun") return null;
3218
3643
  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)";
3644
+ const planPath = path21.join(ctx.taskDir, "plan.md");
3645
+ const plan = fs23.existsSync(planPath) ? fs23.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
3221
3646
  try {
3222
3647
  postComment(
3223
3648
  ctx.input.issueNumber,
@@ -3284,22 +3709,22 @@ var init_hooks = __esm({
3284
3709
  });
3285
3710
 
3286
3711
  // src/learning/auto-learn.ts
3287
- import * as fs23 from "fs";
3288
- import * as path21 from "path";
3712
+ import * as fs24 from "fs";
3713
+ import * as path22 from "path";
3289
3714
  function stripAnsi(str) {
3290
3715
  return str.replace(/\x1b\[[0-9;]*m/g, "");
3291
3716
  }
3292
3717
  function autoLearn(ctx) {
3293
3718
  try {
3294
- const memoryDir = path21.join(ctx.projectDir, ".kody", "memory");
3295
- if (!fs23.existsSync(memoryDir)) {
3296
- fs23.mkdirSync(memoryDir, { recursive: true });
3719
+ const memoryDir = path22.join(ctx.projectDir, ".kody", "memory");
3720
+ if (!fs24.existsSync(memoryDir)) {
3721
+ fs24.mkdirSync(memoryDir, { recursive: true });
3297
3722
  }
3298
3723
  const learnings = [];
3299
3724
  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"));
3725
+ const verifyPath = path22.join(ctx.taskDir, "verify.md");
3726
+ if (fs24.existsSync(verifyPath)) {
3727
+ const verify = stripAnsi(fs24.readFileSync(verifyPath, "utf-8"));
3303
3728
  if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
3304
3729
  if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
3305
3730
  if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
@@ -3308,18 +3733,18 @@ function autoLearn(ctx) {
3308
3733
  if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
3309
3734
  if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
3310
3735
  }
3311
- const reviewPath = path21.join(ctx.taskDir, "review.md");
3312
- if (fs23.existsSync(reviewPath)) {
3313
- const review = fs23.readFileSync(reviewPath, "utf-8");
3736
+ const reviewPath = path22.join(ctx.taskDir, "review.md");
3737
+ if (fs24.existsSync(reviewPath)) {
3738
+ const review = fs24.readFileSync(reviewPath, "utf-8");
3314
3739
  if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
3315
3740
  if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
3316
3741
  if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
3317
3742
  if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
3318
3743
  }
3319
- const taskJsonPath = path21.join(ctx.taskDir, "task.json");
3320
- if (fs23.existsSync(taskJsonPath)) {
3744
+ const taskJsonPath = path22.join(ctx.taskDir, "task.json");
3745
+ if (fs24.existsSync(taskJsonPath)) {
3321
3746
  try {
3322
- const raw = stripAnsi(fs23.readFileSync(taskJsonPath, "utf-8"));
3747
+ const raw = stripAnsi(fs24.readFileSync(taskJsonPath, "utf-8"));
3323
3748
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3324
3749
  const task = JSON.parse(cleaned);
3325
3750
  if (task.scope && Array.isArray(task.scope)) {
@@ -3330,12 +3755,12 @@ function autoLearn(ctx) {
3330
3755
  }
3331
3756
  }
3332
3757
  if (learnings.length > 0) {
3333
- const conventionsPath = path21.join(memoryDir, "conventions.md");
3758
+ const conventionsPath = path22.join(memoryDir, "conventions.md");
3334
3759
  const entry = `
3335
3760
  ## Learned ${timestamp2} (task: ${ctx.taskId})
3336
3761
  ${learnings.join("\n")}
3337
3762
  `;
3338
- fs23.appendFileSync(conventionsPath, entry);
3763
+ fs24.appendFileSync(conventionsPath, entry);
3339
3764
  logger.info(`Auto-learned ${learnings.length} convention(s)`);
3340
3765
  }
3341
3766
  autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
@@ -3343,8 +3768,8 @@ ${learnings.join("\n")}
3343
3768
  }
3344
3769
  }
3345
3770
  function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
3346
- const archPath = path21.join(memoryDir, "architecture.md");
3347
- if (fs23.existsSync(archPath)) return;
3771
+ const archPath = path22.join(memoryDir, "architecture.md");
3772
+ if (fs24.existsSync(archPath)) return;
3348
3773
  const detected = detectArchitectureBasic(projectDir);
3349
3774
  if (detected.length > 0) {
3350
3775
  const content = `# Architecture (auto-detected ${timestamp2})
@@ -3352,7 +3777,7 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
3352
3777
  ## Overview
3353
3778
  ${detected.join("\n")}
3354
3779
  `;
3355
- fs23.writeFileSync(archPath, content);
3780
+ fs24.writeFileSync(archPath, content);
3356
3781
  logger.info(`Auto-detected architecture (${detected.length} items)`);
3357
3782
  }
3358
3783
  }
@@ -3365,13 +3790,13 @@ var init_auto_learn = __esm({
3365
3790
  });
3366
3791
 
3367
3792
  // src/retrospective.ts
3368
- import * as fs24 from "fs";
3369
- import * as path22 from "path";
3793
+ import * as fs25 from "fs";
3794
+ import * as path23 from "path";
3370
3795
  function readArtifact(taskDir, filename, maxChars) {
3371
- const p = path22.join(taskDir, filename);
3372
- if (!fs24.existsSync(p)) return null;
3796
+ const p = path23.join(taskDir, filename);
3797
+ if (!fs25.existsSync(p)) return null;
3373
3798
  try {
3374
- const content = fs24.readFileSync(p, "utf-8");
3799
+ const content = fs25.readFileSync(p, "utf-8");
3375
3800
  return content.length > maxChars ? content.slice(0, maxChars) + "\n...(truncated)" : content;
3376
3801
  } catch {
3377
3802
  return null;
@@ -3424,13 +3849,13 @@ function collectRunContext(ctx, state, pipelineStartTime) {
3424
3849
  return lines.join("\n");
3425
3850
  }
3426
3851
  function getLogPath(projectDir) {
3427
- return path22.join(projectDir, ".kody", "memory", "observer-log.jsonl");
3852
+ return path23.join(projectDir, ".kody", "memory", "observer-log.jsonl");
3428
3853
  }
3429
3854
  function readPreviousRetrospectives(projectDir, limit = 10) {
3430
3855
  const logPath = getLogPath(projectDir);
3431
- if (!fs24.existsSync(logPath)) return [];
3856
+ if (!fs25.existsSync(logPath)) return [];
3432
3857
  try {
3433
- const content = fs24.readFileSync(logPath, "utf-8");
3858
+ const content = fs25.readFileSync(logPath, "utf-8");
3434
3859
  const lines = content.split("\n").filter(Boolean);
3435
3860
  const entries = [];
3436
3861
  const start = Math.max(0, lines.length - limit);
@@ -3457,11 +3882,11 @@ function formatPreviousEntries(entries) {
3457
3882
  }
3458
3883
  function appendRetrospectiveEntry(projectDir, entry) {
3459
3884
  const logPath = getLogPath(projectDir);
3460
- const dir = path22.dirname(logPath);
3461
- if (!fs24.existsSync(dir)) {
3462
- fs24.mkdirSync(dir, { recursive: true });
3885
+ const dir = path23.dirname(logPath);
3886
+ if (!fs25.existsSync(dir)) {
3887
+ fs25.mkdirSync(dir, { recursive: true });
3463
3888
  }
3464
- fs24.appendFileSync(logPath, JSON.stringify(entry) + "\n");
3889
+ fs25.appendFileSync(logPath, JSON.stringify(entry) + "\n");
3465
3890
  }
3466
3891
  async function runRetrospective(ctx, state, pipelineStartTime) {
3467
3892
  if (ctx.input.dryRun) return;
@@ -3629,8 +4054,8 @@ var init_summary = __esm({
3629
4054
  });
3630
4055
 
3631
4056
  // src/pipeline.ts
3632
- import * as fs25 from "fs";
3633
- import * as path23 from "path";
4057
+ import * as fs26 from "fs";
4058
+ import * as path24 from "path";
3634
4059
  function ensureFeatureBranchIfNeeded(ctx) {
3635
4060
  if (ctx.input.dryRun) return;
3636
4061
  if (ctx.input.prNumber) {
@@ -3643,8 +4068,8 @@ function ensureFeatureBranchIfNeeded(ctx) {
3643
4068
  }
3644
4069
  if (!ctx.input.issueNumber) return;
3645
4070
  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;
4071
+ const taskMdPath = path24.join(ctx.taskDir, "task.md");
4072
+ const title = fs26.existsSync(taskMdPath) ? fs26.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
3648
4073
  ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
3649
4074
  syncWithDefault(ctx.projectDir);
3650
4075
  } catch (err) {
@@ -3658,10 +4083,10 @@ function ensureFeatureBranchIfNeeded(ctx) {
3658
4083
  }
3659
4084
  }
3660
4085
  function acquireLock(taskDir) {
3661
- const lockPath = path23.join(taskDir, ".lock");
3662
- if (fs25.existsSync(lockPath)) {
4086
+ const lockPath = path24.join(taskDir, ".lock");
4087
+ if (fs26.existsSync(lockPath)) {
3663
4088
  try {
3664
- const pid = parseInt(fs25.readFileSync(lockPath, "utf-8").trim(), 10);
4089
+ const pid = parseInt(fs26.readFileSync(lockPath, "utf-8").trim(), 10);
3665
4090
  if (!isNaN(pid)) {
3666
4091
  try {
3667
4092
  process.kill(pid, 0);
@@ -3678,14 +4103,14 @@ function acquireLock(taskDir) {
3678
4103
  logger.warn(` Corrupt lock file \u2014 overwriting`);
3679
4104
  }
3680
4105
  try {
3681
- fs25.unlinkSync(lockPath);
4106
+ fs26.unlinkSync(lockPath);
3682
4107
  } catch {
3683
4108
  }
3684
4109
  }
3685
4110
  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);
4111
+ const fd = fs26.openSync(lockPath, fs26.constants.O_WRONLY | fs26.constants.O_CREAT | fs26.constants.O_EXCL);
4112
+ fs26.writeSync(fd, String(process.pid));
4113
+ fs26.closeSync(fd);
3689
4114
  } catch (err) {
3690
4115
  if (err.code === "EEXIST") {
3691
4116
  throw new Error("Pipeline already running (lock acquired by another process)");
@@ -3695,7 +4120,7 @@ function acquireLock(taskDir) {
3695
4120
  }
3696
4121
  function releaseLock(taskDir) {
3697
4122
  try {
3698
- fs25.unlinkSync(path23.join(taskDir, ".lock"));
4123
+ fs26.unlinkSync(path24.join(taskDir, ".lock"));
3699
4124
  } catch {
3700
4125
  }
3701
4126
  }
@@ -3904,7 +4329,7 @@ var init_pipeline = __esm({
3904
4329
 
3905
4330
  // src/preflight.ts
3906
4331
  import { execFileSync as execFileSync14 } from "child_process";
3907
- import * as fs26 from "fs";
4332
+ import * as fs27 from "fs";
3908
4333
  function check(name, fn) {
3909
4334
  try {
3910
4335
  const detail = fn() ?? void 0;
@@ -3957,7 +4382,7 @@ function runPreflight() {
3957
4382
  return v;
3958
4383
  }),
3959
4384
  check("package.json", () => {
3960
- if (!fs26.existsSync("package.json")) throw new Error("not found");
4385
+ if (!fs27.existsSync("package.json")) throw new Error("not found");
3961
4386
  })
3962
4387
  ];
3963
4388
  const failed = checks.filter((c) => !c.ok);
@@ -3978,19 +4403,19 @@ var init_preflight = __esm({
3978
4403
  });
3979
4404
 
3980
4405
  // src/cli/args.ts
3981
- function getArg(args2, flag) {
4406
+ function getArg2(args2, flag) {
3982
4407
  const idx = args2.indexOf(flag);
3983
4408
  if (idx !== -1 && args2[idx + 1] && !args2[idx + 1].startsWith("--")) {
3984
4409
  return args2[idx + 1];
3985
4410
  }
3986
4411
  return void 0;
3987
4412
  }
3988
- function hasFlag(args2, flag) {
4413
+ function hasFlag2(args2, flag) {
3989
4414
  return args2.includes(flag);
3990
4415
  }
3991
4416
  function parseArgs() {
3992
4417
  const args2 = process.argv.slice(2);
3993
- if (hasFlag(args2, "--help") || hasFlag(args2, "-h") || args2.length === 0) {
4418
+ if (hasFlag2(args2, "--help") || hasFlag2(args2, "-h") || args2.length === 0) {
3994
4419
  console.log(`Usage:
3995
4420
  kody run --task-id <id> [--task "<desc>"] [--cwd <path>] [--issue-number <n>] [--complexity low|medium|high] [--feedback "<text>"] [--local] [--dry-run]
3996
4421
  kody rerun --task-id <id> --from <stage> [--cwd <path>] [--issue-number <n>]
@@ -4007,22 +4432,22 @@ function parseArgs() {
4007
4432
  console.error(`Unknown command: ${command2}`);
4008
4433
  process.exit(1);
4009
4434
  }
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");
4435
+ const issueStr = getArg2(args2, "--issue-number") ?? process.env.ISSUE_NUMBER;
4436
+ const prStr = getArg2(args2, "--pr-number") ?? process.env.PR_NUMBER;
4437
+ const localFlag = hasFlag2(args2, "--local");
4013
4438
  return {
4014
4439
  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"),
4440
+ taskId: getArg2(args2, "--task-id") ?? process.env.TASK_ID,
4441
+ task: getArg2(args2, "--task"),
4442
+ fromStage: getArg2(args2, "--from") ?? process.env.FROM_STAGE,
4443
+ dryRun: hasFlag2(args2, "--dry-run") || process.env.DRY_RUN === "true",
4444
+ cwd: getArg2(args2, "--cwd"),
4020
4445
  issueNumber: issueStr ? parseInt(issueStr, 10) : void 0,
4021
4446
  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
4447
+ feedback: getArg2(args2, "--feedback") ?? process.env.FEEDBACK,
4448
+ local: localFlag || !isCI2 && !hasFlag2(args2, "--no-local"),
4449
+ complexity: getArg2(args2, "--complexity") ?? process.env.COMPLEXITY,
4450
+ ciRunId: getArg2(args2, "--ci-run-id") ?? process.env.CI_RUN_ID
4026
4451
  };
4027
4452
  }
4028
4453
  var isCI2;
@@ -4034,9 +4459,9 @@ var init_args = __esm({
4034
4459
  });
4035
4460
 
4036
4461
  // src/cli/litellm.ts
4037
- import * as fs27 from "fs";
4462
+ import * as fs28 from "fs";
4038
4463
  import * as os from "os";
4039
- import * as path24 from "path";
4464
+ import * as path25 from "path";
4040
4465
  import { execFileSync as execFileSync15 } from "child_process";
4041
4466
  async function checkLitellmHealth(url) {
4042
4467
  try {
@@ -4046,7 +4471,7 @@ async function checkLitellmHealth(url) {
4046
4471
  return false;
4047
4472
  }
4048
4473
  }
4049
- async function checkModelHealth(baseUrl, apiKey, model = "claude-haiku-4-5") {
4474
+ async function checkModelHealth(baseUrl, apiKey, model) {
4050
4475
  try {
4051
4476
  const res = await fetch(`${baseUrl}/v1/messages`, {
4052
4477
  method: "POST",
@@ -4124,8 +4549,8 @@ async function tryStartLitellm(url, projectDir, generatedConfig) {
4124
4549
  logger.warn("No provider configured in kody.config.json \u2014 cannot start LiteLLM proxy");
4125
4550
  return null;
4126
4551
  }
4127
- const configPath = path24.join(os.tmpdir(), "kody-litellm-config.yaml");
4128
- fs27.writeFileSync(configPath, generatedConfig);
4552
+ const configPath = path25.join(os.tmpdir(), "kody-litellm-config.yaml");
4553
+ fs28.writeFileSync(configPath, generatedConfig);
4129
4554
  const portMatch = url.match(/:(\d+)/);
4130
4555
  const port = portMatch ? portMatch[1] : "4000";
4131
4556
  let litellmFound = false;
@@ -4154,10 +4579,10 @@ async function tryStartLitellm(url, projectDir, generatedConfig) {
4154
4579
  cmd = "python3";
4155
4580
  args2 = ["-m", "litellm", "--config", configPath, "--port", port];
4156
4581
  }
4157
- const dotenvPath = path24.join(projectDir, ".env");
4582
+ const dotenvPath = path25.join(projectDir, ".env");
4158
4583
  const dotenvVars = {};
4159
- if (fs27.existsSync(dotenvPath)) {
4160
- for (const rawLine of fs27.readFileSync(dotenvPath, "utf-8").split("\n")) {
4584
+ if (fs28.existsSync(dotenvPath)) {
4585
+ for (const rawLine of fs28.readFileSync(dotenvPath, "utf-8").split("\n")) {
4161
4586
  const line = rawLine.trim();
4162
4587
  if (!line || line.startsWith("#")) continue;
4163
4588
  const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
@@ -4208,8 +4633,8 @@ var init_litellm = __esm({
4208
4633
  });
4209
4634
 
4210
4635
  // src/cli/task-state.ts
4211
- import * as fs28 from "fs";
4212
- import * as path25 from "path";
4636
+ import * as fs29 from "fs";
4637
+ import * as path26 from "path";
4213
4638
  function resolveTaskAction(issueNumber, existingTaskId, existingState) {
4214
4639
  if (!existingTaskId || !existingState) {
4215
4640
  return { action: "start-fresh", taskId: `${issueNumber}-${generateTaskId()}` };
@@ -4241,11 +4666,11 @@ function resolveTaskAction(issueNumber, existingTaskId, existingState) {
4241
4666
  function resolveForIssue(issueNumber, projectDir) {
4242
4667
  const existingTaskId = findLatestTaskForIssue(issueNumber, projectDir);
4243
4668
  if (existingTaskId) {
4244
- const statusPath = path25.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
4669
+ const statusPath = path26.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
4245
4670
  let existingState = null;
4246
- if (fs28.existsSync(statusPath)) {
4671
+ if (fs29.existsSync(statusPath)) {
4247
4672
  try {
4248
- existingState = JSON.parse(fs28.readFileSync(statusPath, "utf-8"));
4673
+ existingState = JSON.parse(fs29.readFileSync(statusPath, "utf-8"));
4249
4674
  } catch {
4250
4675
  }
4251
4676
  }
@@ -4402,8 +4827,8 @@ var init_resolve = __esm({
4402
4827
 
4403
4828
  // src/entry.ts
4404
4829
  var entry_exports = {};
4405
- import * as fs29 from "fs";
4406
- import * as path26 from "path";
4830
+ import * as fs30 from "fs";
4831
+ import * as path27 from "path";
4407
4832
  async function ensureLitellmProxy(config, projectDir) {
4408
4833
  if (!anyStageNeedsProxy(config)) return null;
4409
4834
  const litellmUrl = getLitellmUrl();
@@ -4458,9 +4883,9 @@ async function runModelHealthCheck(config) {
4458
4883
  }
4459
4884
  async function main() {
4460
4885
  const input = parseArgs();
4461
- const projectDir = input.cwd ? path26.resolve(input.cwd) : process.cwd();
4886
+ const projectDir = input.cwd ? path27.resolve(input.cwd) : process.cwd();
4462
4887
  if (input.cwd) {
4463
- if (!fs29.existsSync(projectDir)) {
4888
+ if (!fs30.existsSync(projectDir)) {
4464
4889
  console.error(`--cwd path does not exist: ${projectDir}`);
4465
4890
  process.exit(1);
4466
4891
  }
@@ -4526,8 +4951,24 @@ async function main() {
4526
4951
  process.exit(1);
4527
4952
  }
4528
4953
  }
4529
- const taskDir = path26.join(projectDir, ".kody", "tasks", taskId);
4530
- fs29.mkdirSync(taskDir, { recursive: true });
4954
+ const taskDir = path27.join(projectDir, ".kody", "tasks", taskId);
4955
+ fs30.mkdirSync(taskDir, { recursive: true });
4956
+ if (input.command === "rerun" && isTaskifyRun(taskDir)) {
4957
+ const marker = readTaskifyMarker(taskDir);
4958
+ if (marker) {
4959
+ logger.info(`Resuming taskify run for ${marker.ticketId ?? marker.prdFile} with PM feedback`);
4960
+ await taskifyCommand({
4961
+ ticketId: marker.ticketId,
4962
+ prdFile: marker.prdFile,
4963
+ issueNumber: marker.issueNumber ?? input.issueNumber,
4964
+ feedback: input.feedback,
4965
+ local: input.local,
4966
+ projectDir,
4967
+ taskId
4968
+ });
4969
+ return;
4970
+ }
4971
+ }
4531
4972
  if (input.command === "status") {
4532
4973
  printStatus(taskId, taskDir);
4533
4974
  return;
@@ -4643,31 +5084,31 @@ async function main() {
4643
5084
  logger.info("Preflight checks:");
4644
5085
  runPreflight();
4645
5086
  if (input.task) {
4646
- fs29.writeFileSync(path26.join(taskDir, "task.md"), input.task);
5087
+ fs30.writeFileSync(path27.join(taskDir, "task.md"), input.task);
4647
5088
  }
4648
- const taskMdPath = path26.join(taskDir, "task.md");
4649
- if (!fs29.existsSync(taskMdPath) && isPRFix && input.prNumber) {
5089
+ const taskMdPath = path27.join(taskDir, "task.md");
5090
+ if (!fs30.existsSync(taskMdPath) && isPRFix && input.prNumber) {
4650
5091
  logger.info(`Fetching PR #${input.prNumber} details as task context...`);
4651
5092
  const prDetails = getPRDetails(input.prNumber);
4652
5093
  if (prDetails) {
4653
5094
  const taskContent = `# ${prDetails.title}
4654
5095
 
4655
5096
  ${prDetails.body ?? ""}`;
4656
- fs29.writeFileSync(taskMdPath, taskContent);
5097
+ fs30.writeFileSync(taskMdPath, taskContent);
4657
5098
  logger.info(` Task loaded from PR #${input.prNumber}: ${prDetails.title}`);
4658
5099
  }
4659
- } else if (!fs29.existsSync(taskMdPath) && input.issueNumber) {
5100
+ } else if (!fs30.existsSync(taskMdPath) && input.issueNumber) {
4660
5101
  logger.info(`Fetching issue #${input.issueNumber} body as task...`);
4661
5102
  const issue = getIssue(input.issueNumber);
4662
5103
  if (issue) {
4663
5104
  const taskContent = `# ${issue.title}
4664
5105
 
4665
5106
  ${issue.body ?? ""}`;
4666
- fs29.writeFileSync(taskMdPath, taskContent);
5107
+ fs30.writeFileSync(taskMdPath, taskContent);
4667
5108
  logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
4668
5109
  }
4669
5110
  }
4670
- if (!fs29.existsSync(taskMdPath)) {
5111
+ if (!fs30.existsSync(taskMdPath)) {
4671
5112
  console.error("No task.md found. Provide --task, --issue-number, or ensure .kody/tasks/<id>/task.md exists.");
4672
5113
  process.exit(1);
4673
5114
  }
@@ -4805,7 +5246,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
4805
5246
  }
4806
5247
  }
4807
5248
  const state = await runPipeline(ctx);
4808
- const files = fs29.readdirSync(taskDir);
5249
+ const files = fs30.readdirSync(taskDir);
4809
5250
  console.log(`
4810
5251
  Artifacts in ${taskDir}:`);
4811
5252
  for (const f of files) {
@@ -4851,6 +5292,7 @@ var init_entry = __esm({
4851
5292
  init_litellm();
4852
5293
  init_task_resolution();
4853
5294
  init_task_state();
5295
+ init_taskify_command();
4854
5296
  init_config();
4855
5297
  main().catch(async (err) => {
4856
5298
  const msg = err instanceof Error ? err.message : String(err);
@@ -4869,9 +5311,9 @@ var init_entry = __esm({
4869
5311
  });
4870
5312
 
4871
5313
  // src/bin/cli.ts
4872
- import * as fs30 from "fs";
4873
- import * as path27 from "path";
4874
- import { fileURLToPath } from "url";
5314
+ import * as fs31 from "fs";
5315
+ import * as path28 from "path";
5316
+ import { fileURLToPath as fileURLToPath2 } from "url";
4875
5317
 
4876
5318
  // src/bin/commands/init.ts
4877
5319
  import * as fs3 from "fs";
@@ -5705,7 +6147,7 @@ ${repoContext}`;
5705
6147
  const output = execFileSync5("claude", [
5706
6148
  "--print",
5707
6149
  "--model",
5708
- "haiku",
6150
+ "claude-haiku-4-5-20251001",
5709
6151
  "--dangerously-skip-permissions",
5710
6152
  memoryPrompt
5711
6153
  ], {
@@ -5808,7 +6250,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
5808
6250
  const output = execFileSync5("claude", [
5809
6251
  "--print",
5810
6252
  "--model",
5811
- "haiku",
6253
+ "claude-haiku-4-5-20251001",
5812
6254
  "--dangerously-skip-permissions",
5813
6255
  customizationPrompt
5814
6256
  ], {
@@ -6041,11 +6483,11 @@ Create it manually.`, cwd);
6041
6483
 
6042
6484
  // src/bin/cli.ts
6043
6485
  init_architecture_detection();
6044
- var __dirname = path27.dirname(fileURLToPath(import.meta.url));
6045
- var PKG_ROOT = path27.resolve(__dirname, "..", "..");
6486
+ var __dirname2 = path28.dirname(fileURLToPath2(import.meta.url));
6487
+ var PKG_ROOT = path28.resolve(__dirname2, "..", "..");
6046
6488
  function getVersion() {
6047
- const pkgPath = path27.join(PKG_ROOT, "package.json");
6048
- const pkg = JSON.parse(fs30.readFileSync(pkgPath, "utf-8"));
6489
+ const pkgPath = path28.join(PKG_ROOT, "package.json");
6490
+ const pkg = JSON.parse(fs31.readFileSync(pkgPath, "utf-8"));
6049
6491
  return pkg.version;
6050
6492
  }
6051
6493
  var args = process.argv.slice(2);
@@ -6054,6 +6496,8 @@ if (command === "init") {
6054
6496
  initCommand({ force: args.includes("--force") }, PKG_ROOT);
6055
6497
  } else if (command === "bootstrap") {
6056
6498
  bootstrapCommand({ force: args.includes("--force") }, PKG_ROOT);
6499
+ } else if (command === "taskify") {
6500
+ Promise.resolve().then(() => (init_taskify_command(), taskify_command_exports)).then(({ runTaskifyCommand: runTaskifyCommand2 }) => runTaskifyCommand2());
6057
6501
  } else if (command === "ci-parse") {
6058
6502
  Promise.resolve().then(() => (init_parse_inputs(), parse_inputs_exports)).then(({ runCiParse: runCiParse2 }) => runCiParse2());
6059
6503
  } else if (command === "version" || command === "--version" || command === "-v") {