@kody-ade/kody-engine-lite 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -152,21 +152,47 @@ kody-engine-lite init --force # overwrite existing workflow
152
152
  Comment on any issue:
153
153
 
154
154
  ```
155
- @kody full <task-id> # Run full pipeline
156
- @kody rerun <task-id> --from <stage> # Resume from stage
155
+ @kody # Run full pipeline (auto-generates task-id)
156
+ @kody full <task-id> # Run with specific task-id
157
+ @kody rerun --from <stage> # Resume latest task from stage
158
+ @kody rerun <task-id> --from <stage> # Resume specific task
159
+ @kody approve # Approve + provide answers to Kody's questions
157
160
  @kody status <task-id> # Check status
158
161
  ```
159
162
 
163
+ ### Approve Flow (Question Gate)
164
+
165
+ When Kody encounters unclear requirements or architecture decisions, it pauses and posts questions:
166
+
167
+ ```
168
+ Kody: 🤔 Kody has questions before proceeding:
169
+ 1. Should the search be case-sensitive?
170
+ 2. Which users should have access?
171
+
172
+ Reply with @kody approve and your answers in the comment body.
173
+ ```
174
+
175
+ You reply:
176
+
177
+ ```
178
+ @kody approve
179
+
180
+ 1. Yes, case-sensitive
181
+ 2. Only admin users
182
+ ```
183
+
184
+ Kody resumes automatically from where it paused, with your answers injected as context.
185
+
160
186
  ## Pipeline Stages
161
187
 
162
188
  | Stage | Model | What it does |
163
189
  |-------|-------|-------------|
164
190
  | taskify | haiku | Classifies task from issue body → `task.json` |
165
- | plan | sonnet | Creates TDD implementation plan → `plan.md` |
166
- | build | opus | Implements code changes (uses Claude Code tools) |
191
+ | plan | opus | Deep reasoning: creates TDD implementation plan → `plan.md` |
192
+ | build | sonnet | Implements code changes (Claude Code tools handle execution) |
167
193
  | verify | — | Runs typecheck + tests + lint (from `kody.config.json`) |
168
- | review | sonnet | Code review → `review.md` (PASS/FAIL + findings) |
169
- | review-fix | opus | Fixes Critical/Major review findings |
194
+ | review | opus | Thorough code review → `review.md` (PASS/FAIL + findings) |
195
+ | review-fix | sonnet | Applies known fixes from review findings |
170
196
  | ship | — | Pushes branch + creates PR + comments on issue |
171
197
 
172
198
  ### Automatic Loops
@@ -200,8 +226,8 @@ Memory is prepended to every agent prompt, giving Claude Code project context.
200
226
  | `github.repo` | GitHub repo name | `""` |
201
227
  | `paths.taskDir` | Task artifacts directory | `.tasks` |
202
228
  | `agent.modelMap.cheap` | Model for taskify | `haiku` |
203
- | `agent.modelMap.mid` | Model for plan/review | `sonnet` |
204
- | `agent.modelMap.strong` | Model for build/review-fix | `opus` |
229
+ | `agent.modelMap.mid` | Model for build/review-fix/autofix | `sonnet` |
230
+ | `agent.modelMap.strong` | Model for plan/review (deep reasoning) | `opus` |
205
231
 
206
232
  ### Environment Variables
207
233
 
@@ -213,6 +239,59 @@ Memory is prepended to every agent prompt, giving Claude Code project context.
213
239
  | `LOG_LEVEL` | No | `debug`, `info`, `warn`, `error` (default: `info`) |
214
240
  | `LITELLM_BASE_URL` | No | LiteLLM proxy URL for model routing |
215
241
 
242
+ ## Multi-Runner Support
243
+
244
+ Use different agent runners per stage. For example, use OpenCode (MiniMax) for reasoning and Claude Code for code execution:
245
+
246
+ ```json
247
+ {
248
+ "agent": {
249
+ "defaultRunner": "claude",
250
+ "runners": {
251
+ "claude": { "type": "claude-code" },
252
+ "opencode": { "type": "opencode" }
253
+ },
254
+ "stageRunners": {
255
+ "taskify": "opencode",
256
+ "plan": "opencode",
257
+ "build": "claude",
258
+ "review": "opencode",
259
+ "review-fix": "claude",
260
+ "autofix": "claude"
261
+ }
262
+ }
263
+ }
264
+ ```
265
+
266
+ Available runner types:
267
+ - `claude-code` — Claude Code CLI (`claude --print`). Supports tool use (Read, Write, Edit, Bash).
268
+ - `opencode` — OpenCode CLI (`opencode github run`). Supports MiniMax, OpenAI, Anthropic, Gemini.
269
+
270
+ If no `runners`/`stageRunners` config, defaults to Claude Code for all stages.
271
+
272
+ ## Complexity-Based Stage Skipping
273
+
274
+ Skip stages based on task complexity to save time and cost:
275
+
276
+ ```bash
277
+ # Simple fix — skip plan and review
278
+ kody-engine-lite run --task-id fix-typo --task "Fix typo in README" --complexity low
279
+
280
+ # Standard feature — skip review-fix
281
+ kody-engine-lite run --task-id add-feature --task "Add search" --complexity medium
282
+
283
+ # Complex task — run all stages (default)
284
+ kody-engine-lite run --task-id refactor --task "Refactor auth" --complexity high
285
+ ```
286
+
287
+ | Complexity | Stages | Skipped |
288
+ |-----------|--------|---------|
289
+ | low | taskify → build → verify → ship | plan, review, review-fix |
290
+ | medium | taskify → plan → build → verify → review → ship | review-fix |
291
+ | high | taskify → plan → build → verify → review → review-fix → ship | none |
292
+
293
+ If `--complexity` is not provided, it's auto-detected from the taskify stage's `risk_level` output. A complexity label (`kody:low`, `kody:medium`, `kody:high`) is set on the issue.
294
+
216
295
  ## LiteLLM (Optional)
217
296
 
218
297
  For multi-provider model routing with fallback:
package/dist/bin/cli.js CHANGED
@@ -47,10 +47,47 @@ function waitForProcess(child, timeout) {
47
47
  });
48
48
  });
49
49
  }
50
+ async function runSubprocess(command2, args2, prompt, timeout, options) {
51
+ const child = spawn(command2, args2, {
52
+ cwd: options?.cwd ?? process.cwd(),
53
+ env: {
54
+ ...process.env,
55
+ SKIP_BUILD: "1",
56
+ SKIP_HOOKS: "1",
57
+ ...options?.env
58
+ },
59
+ stdio: ["pipe", "pipe", "pipe"]
60
+ });
61
+ try {
62
+ await writeStdin(child, prompt);
63
+ } catch (err) {
64
+ return {
65
+ outcome: "failed",
66
+ error: `Failed to send prompt: ${err instanceof Error ? err.message : String(err)}`
67
+ };
68
+ }
69
+ const { code, stdout, stderr } = await waitForProcess(child, timeout);
70
+ if (code === 0) {
71
+ return { outcome: "completed", output: stdout };
72
+ }
73
+ return {
74
+ outcome: code === null ? "timed_out" : "failed",
75
+ error: `Exit code ${code}
76
+ ${stderr.slice(-STDERR_TAIL_CHARS)}`
77
+ };
78
+ }
79
+ function checkCommand(command2, args2) {
80
+ try {
81
+ execFileSync(command2, args2, { timeout: 1e4, stdio: "pipe" });
82
+ return true;
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
50
87
  function createClaudeCodeRunner() {
51
88
  return {
52
- async run(stageName, prompt, model, timeout, _taskDir, options) {
53
- const child = spawn(
89
+ async run(_stageName, prompt, model, timeout, _taskDir, options) {
90
+ return runSubprocess(
54
91
  "claude",
55
92
  [
56
93
  "--print",
@@ -60,51 +97,58 @@ function createClaudeCodeRunner() {
60
97
  "--allowedTools",
61
98
  "Bash,Edit,Read,Write,Glob,Grep"
62
99
  ],
63
- {
64
- cwd: options?.cwd ?? process.cwd(),
65
- env: {
66
- ...process.env,
67
- SKIP_BUILD: "1",
68
- SKIP_HOOKS: "1",
69
- ...options?.env
70
- },
71
- stdio: ["pipe", "pipe", "pipe"]
72
- }
100
+ prompt,
101
+ timeout,
102
+ options
73
103
  );
74
- try {
75
- await writeStdin(child, prompt);
76
- } catch (err) {
77
- return {
78
- outcome: "failed",
79
- error: `Failed to send prompt: ${err instanceof Error ? err.message : String(err)}`
80
- };
81
- }
82
- const { code, stdout, stderr } = await waitForProcess(child, timeout);
83
- if (code === 0) {
84
- return { outcome: "completed", output: stdout };
85
- }
86
- return {
87
- outcome: code === null ? "timed_out" : "failed",
88
- error: `Exit code ${code}
89
- ${stderr.slice(-STDERR_TAIL_CHARS)}`
90
- };
91
104
  },
92
105
  async healthCheck() {
93
- try {
94
- execFileSync("claude", ["--version"], { timeout: 1e4, stdio: "pipe" });
95
- return true;
96
- } catch {
97
- return false;
98
- }
106
+ return checkCommand("claude", ["--version"]);
107
+ }
108
+ };
109
+ }
110
+ function createOpenCodeRunner() {
111
+ return {
112
+ async run(stageName, prompt, _model, timeout, _taskDir, options) {
113
+ return runSubprocess(
114
+ "opencode",
115
+ ["github", "run", "--agent", stageName],
116
+ prompt,
117
+ timeout,
118
+ options
119
+ );
120
+ },
121
+ async healthCheck() {
122
+ return checkCommand("opencode", ["--version"]);
99
123
  }
100
124
  };
101
125
  }
102
- var SIGKILL_GRACE_MS, STDERR_TAIL_CHARS;
126
+ function createRunners(config) {
127
+ if (config.agent.runners && Object.keys(config.agent.runners).length > 0) {
128
+ const runners = {};
129
+ for (const [name, runnerConfig] of Object.entries(config.agent.runners)) {
130
+ const factory2 = RUNNER_FACTORIES[runnerConfig.type];
131
+ if (factory2) {
132
+ runners[name] = factory2();
133
+ }
134
+ }
135
+ return runners;
136
+ }
137
+ const runnerType = config.agent.runner ?? "claude-code";
138
+ const factory = RUNNER_FACTORIES[runnerType];
139
+ const defaultName = config.agent.defaultRunner ?? "claude";
140
+ return { [defaultName]: factory ? factory() : createClaudeCodeRunner() };
141
+ }
142
+ var SIGKILL_GRACE_MS, STDERR_TAIL_CHARS, RUNNER_FACTORIES;
103
143
  var init_agent_runner = __esm({
104
144
  "src/agent-runner.ts"() {
105
145
  "use strict";
106
146
  SIGKILL_GRACE_MS = 5e3;
107
147
  STDERR_TAIL_CHARS = 500;
148
+ RUNNER_FACTORIES = {
149
+ "claude-code": createClaudeCodeRunner,
150
+ "opencode": createOpenCodeRunner
151
+ };
108
152
  }
109
153
  });
110
154
 
@@ -125,7 +169,7 @@ var init_definitions = __esm({
125
169
  {
126
170
  name: "plan",
127
171
  type: "agent",
128
- modelTier: "mid",
172
+ modelTier: "strong",
129
173
  timeout: 3e5,
130
174
  maxRetries: 1,
131
175
  outputFile: "plan.md"
@@ -133,7 +177,7 @@ var init_definitions = __esm({
133
177
  {
134
178
  name: "build",
135
179
  type: "agent",
136
- modelTier: "strong",
180
+ modelTier: "mid",
137
181
  timeout: 12e5,
138
182
  maxRetries: 1
139
183
  },
@@ -148,7 +192,7 @@ var init_definitions = __esm({
148
192
  {
149
193
  name: "review",
150
194
  type: "agent",
151
- modelTier: "mid",
195
+ modelTier: "strong",
152
196
  timeout: 3e5,
153
197
  maxRetries: 1,
154
198
  outputFile: "review.md"
@@ -156,7 +200,7 @@ var init_definitions = __esm({
156
200
  {
157
201
  name: "review-fix",
158
202
  type: "agent",
159
- modelTier: "strong",
203
+ modelTier: "mid",
160
204
  timeout: 6e5,
161
205
  maxRetries: 1
162
206
  },
@@ -253,6 +297,7 @@ var init_config = __esm({
253
297
  },
254
298
  agent: {
255
299
  runner: "claude-code",
300
+ defaultRunner: "claude",
256
301
  modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
257
302
  }
258
303
  };
@@ -663,7 +708,7 @@ var init_github_api = __esm({
663
708
  "use strict";
664
709
  init_logger();
665
710
  API_TIMEOUT_MS = 3e4;
666
- LIFECYCLE_LABELS = ["planning", "building", "review", "done", "failed"];
711
+ LIFECYCLE_LABELS = ["planning", "building", "review", "done", "failed", "waiting", "low", "medium", "high"];
667
712
  }
668
713
  });
669
714
 
@@ -743,6 +788,65 @@ var init_verify_runner = __esm({
743
788
  import * as fs4 from "fs";
744
789
  import * as path4 from "path";
745
790
  import { execFileSync as execFileSync5 } from "child_process";
791
+ function filterByComplexity(stages, complexity) {
792
+ const skip = COMPLEXITY_SKIP[complexity] ?? [];
793
+ return stages.filter((s) => !skip.includes(s.name));
794
+ }
795
+ function checkForQuestions(ctx, stageName) {
796
+ if (ctx.input.local || !ctx.input.issueNumber) return false;
797
+ try {
798
+ if (stageName === "taskify") {
799
+ const taskJsonPath = path4.join(ctx.taskDir, "task.json");
800
+ if (!fs4.existsSync(taskJsonPath)) return false;
801
+ const raw = fs4.readFileSync(taskJsonPath, "utf-8");
802
+ const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
803
+ const taskJson = JSON.parse(cleaned);
804
+ if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
805
+ const body = `\u{1F914} **Kody has questions before proceeding:**
806
+
807
+ ${taskJson.questions.map((q, i) => `${i + 1}. ${q}`).join("\n")}
808
+
809
+ Reply with \`@kody approve\` and your answers in the comment body.`;
810
+ postComment(ctx.input.issueNumber, body);
811
+ setLifecycleLabel(ctx.input.issueNumber, "waiting");
812
+ return true;
813
+ }
814
+ }
815
+ if (stageName === "plan") {
816
+ const planPath = path4.join(ctx.taskDir, "plan.md");
817
+ if (!fs4.existsSync(planPath)) return false;
818
+ const plan = fs4.readFileSync(planPath, "utf-8");
819
+ const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
820
+ if (questionsMatch) {
821
+ const questionsText = questionsMatch[1].trim();
822
+ const questions = questionsText.split("\n").filter((l) => l.startsWith("- ")).map((l) => l.slice(2));
823
+ if (questions.length > 0) {
824
+ const body = `\u{1F3D7}\uFE0F **Kody has architecture questions:**
825
+
826
+ ${questions.map((q, i) => `${i + 1}. ${q}`).join("\n")}
827
+
828
+ Reply with \`@kody approve\` and your answers in the comment body.`;
829
+ postComment(ctx.input.issueNumber, body);
830
+ setLifecycleLabel(ctx.input.issueNumber, "waiting");
831
+ return true;
832
+ }
833
+ }
834
+ }
835
+ } catch {
836
+ }
837
+ return false;
838
+ }
839
+ function getRunnerForStage(ctx, stageName) {
840
+ const config = getProjectConfig();
841
+ const runnerName = config.agent.stageRunners?.[stageName] ?? config.agent.defaultRunner ?? Object.keys(ctx.runners)[0] ?? "claude";
842
+ const runner = ctx.runners[runnerName];
843
+ if (!runner) {
844
+ throw new Error(
845
+ `Runner "${runnerName}" not found for stage ${stageName}. Available: ${Object.keys(ctx.runners).join(", ")}`
846
+ );
847
+ }
848
+ return runner;
849
+ }
746
850
  function loadState(taskId, taskDir) {
747
851
  const p = path4.join(taskDir, "status.json");
748
852
  if (!fs4.existsSync(p)) return null;
@@ -794,7 +898,8 @@ async function executeAgentStage(ctx, def) {
794
898
  if (config.agent.litellmUrl) {
795
899
  extraEnv.ANTHROPIC_BASE_URL = config.agent.litellmUrl;
796
900
  }
797
- const result = await ctx.runner.run(def.name, prompt, model, def.timeout, ctx.taskDir, {
901
+ const runner = getRunnerForStage(ctx, def.name);
902
+ const result = await runner.run(def.name, prompt, model, def.timeout, ctx.taskDir, {
798
903
  cwd: ctx.projectDir,
799
904
  env: extraEnv
800
905
  });
@@ -911,6 +1016,9 @@ async function executeVerifyWithAutofix(ctx, def) {
911
1016
  };
912
1017
  }
913
1018
  async function executeReviewWithFix(ctx, def) {
1019
+ if (ctx.input.dryRun) {
1020
+ return { outcome: "completed", retries: 0 };
1021
+ }
914
1022
  const reviewDef = STAGES.find((s) => s.name === "review");
915
1023
  const reviewFixDef = STAGES.find((s) => s.name === "review-fix");
916
1024
  const reviewResult = await executeAgentStage(ctx, reviewDef);
@@ -934,8 +1042,65 @@ async function executeReviewWithFix(ctx, def) {
934
1042
  logger.info(` re-running review after fix...`);
935
1043
  return executeAgentStage(ctx, reviewDef);
936
1044
  }
1045
+ function buildPrBody(ctx) {
1046
+ const sections = [];
1047
+ const taskJsonPath = path4.join(ctx.taskDir, "task.json");
1048
+ if (fs4.existsSync(taskJsonPath)) {
1049
+ try {
1050
+ const raw = fs4.readFileSync(taskJsonPath, "utf-8");
1051
+ const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
1052
+ const task = JSON.parse(cleaned);
1053
+ sections.push(`## Summary`);
1054
+ sections.push(`**Type:** ${task.task_type ?? "unknown"} | **Risk:** ${task.risk_level ?? "unknown"}`);
1055
+ if (task.description) sections.push(`
1056
+ ${task.description}`);
1057
+ if (task.scope?.length) sections.push(`
1058
+ **Scope:** ${task.scope.join(", ")}`);
1059
+ } catch {
1060
+ }
1061
+ }
1062
+ const planPath = path4.join(ctx.taskDir, "plan.md");
1063
+ if (fs4.existsSync(planPath)) {
1064
+ const plan = fs4.readFileSync(planPath, "utf-8").trim();
1065
+ if (plan) {
1066
+ const truncated = plan.length > 500 ? plan.slice(0, 500) + "\n..." : plan;
1067
+ sections.push(`
1068
+ ## Plan
1069
+ <details><summary>Implementation plan</summary>
1070
+
1071
+ ${truncated}
1072
+ </details>`);
1073
+ }
1074
+ }
1075
+ const reviewPath = path4.join(ctx.taskDir, "review.md");
1076
+ if (fs4.existsSync(reviewPath)) {
1077
+ const review = fs4.readFileSync(reviewPath, "utf-8");
1078
+ const verdictMatch = review.match(/## Verdict:\s*(PASS|FAIL)/i);
1079
+ if (verdictMatch) {
1080
+ sections.push(`
1081
+ **Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
1082
+ }
1083
+ }
1084
+ const verifyPath = path4.join(ctx.taskDir, "verify.md");
1085
+ if (fs4.existsSync(verifyPath)) {
1086
+ const verify = fs4.readFileSync(verifyPath, "utf-8");
1087
+ if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
1088
+ }
1089
+ if (ctx.input.issueNumber) {
1090
+ sections.push(`
1091
+ Closes #${ctx.input.issueNumber}`);
1092
+ }
1093
+ sections.push(`
1094
+ ---
1095
+ \u{1F916} Generated by Kody`);
1096
+ return sections.join("\n");
1097
+ }
937
1098
  function executeShipStage(ctx, _def) {
938
1099
  const shipPath = path4.join(ctx.taskDir, "ship.md");
1100
+ if (ctx.input.dryRun) {
1101
+ fs4.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
1102
+ return { outcome: "completed", outputFile: "ship.md", retries: 0 };
1103
+ }
939
1104
  if (ctx.input.local && !ctx.input.issueNumber) {
940
1105
  fs4.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
941
1106
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
@@ -968,13 +1133,7 @@ function executeShipStage(ctx, _def) {
968
1133
  const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
969
1134
  title = (lines[0] ?? "Update").slice(0, 72);
970
1135
  }
971
- const closesLine = ctx.input.issueNumber ? `
972
-
973
- Closes #${ctx.input.issueNumber}` : "";
974
- const body = `Generated by Kody pipeline${closesLine}
975
-
976
- ---
977
- \u{1F916} Generated by Kody`;
1136
+ const body = buildPrBody(ctx);
978
1137
  const pr = createPR(head, base, title, body);
979
1138
  if (pr) {
980
1139
  if (ctx.input.issueNumber && !ctx.input.local) {
@@ -1035,6 +1194,9 @@ async function runPipeline(ctx) {
1035
1194
  logger.warn(` Failed to create feature branch: ${err}`);
1036
1195
  }
1037
1196
  }
1197
+ let complexity = ctx.input.complexity ?? "high";
1198
+ let activeStages = filterByComplexity(STAGES, complexity);
1199
+ let skippedStagesCommentPosted = false;
1038
1200
  for (const def of STAGES) {
1039
1201
  if (!startExecution) {
1040
1202
  if (def.name === fromStage) {
@@ -1047,6 +1209,23 @@ async function runPipeline(ctx) {
1047
1209
  logger.info(`[${def.name}] already completed, skipping`);
1048
1210
  continue;
1049
1211
  }
1212
+ if (!activeStages.find((s) => s.name === def.name)) {
1213
+ logger.info(`[${def.name}] skipped (complexity: ${complexity})`);
1214
+ state.stages[def.name] = { state: "completed", retries: 0, outputFile: void 0 };
1215
+ writeState(state, ctx.taskDir);
1216
+ if (!skippedStagesCommentPosted && ctx.input.issueNumber && !ctx.input.local && !ctx.input.dryRun) {
1217
+ const skipped = STAGES.filter((s) => !activeStages.find((a) => a.name === s.name)).map((s) => s.name);
1218
+ try {
1219
+ postComment(
1220
+ ctx.input.issueNumber,
1221
+ `\u26A1 **Complexity: ${complexity}** \u2014 skipping ${skipped.join(", ")} (not needed for ${complexity}-risk tasks)`
1222
+ );
1223
+ } catch {
1224
+ }
1225
+ skippedStagesCommentPosted = true;
1226
+ }
1227
+ continue;
1228
+ }
1050
1229
  ciGroup(`Stage: ${def.name}`);
1051
1230
  state.stages[def.name] = {
1052
1231
  state: "running",
@@ -1094,6 +1273,48 @@ async function runPipeline(ctx) {
1094
1273
  outputFile: result.outputFile
1095
1274
  };
1096
1275
  logger.info(`[${def.name}] \u2713 completed`);
1276
+ if ((def.name === "taskify" || def.name === "plan") && !ctx.input.dryRun) {
1277
+ const paused = checkForQuestions(ctx, def.name);
1278
+ if (paused) {
1279
+ state.state = "failed";
1280
+ state.stages[def.name] = {
1281
+ ...state.stages[def.name],
1282
+ state: "completed",
1283
+ error: "paused: waiting for answers"
1284
+ };
1285
+ writeState(state, ctx.taskDir);
1286
+ logger.info(` Pipeline paused \u2014 questions posted on issue`);
1287
+ return state;
1288
+ }
1289
+ }
1290
+ if (def.name === "taskify" && !ctx.input.complexity) {
1291
+ try {
1292
+ const taskJsonPath = path4.join(ctx.taskDir, "task.json");
1293
+ if (fs4.existsSync(taskJsonPath)) {
1294
+ const raw = fs4.readFileSync(taskJsonPath, "utf-8");
1295
+ const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
1296
+ const taskJson = JSON.parse(cleaned);
1297
+ if (taskJson.risk_level && COMPLEXITY_SKIP[taskJson.risk_level]) {
1298
+ complexity = taskJson.risk_level;
1299
+ activeStages = filterByComplexity(STAGES, complexity);
1300
+ logger.info(` Complexity auto-detected: ${complexity} (${activeStages.map((s) => s.name).join(" \u2192 ")})`);
1301
+ if (ctx.input.issueNumber && !ctx.input.local) {
1302
+ try {
1303
+ setLifecycleLabel(ctx.input.issueNumber, complexity);
1304
+ } catch {
1305
+ }
1306
+ if (taskJson.task_type) {
1307
+ try {
1308
+ setLabel(ctx.input.issueNumber, `kody:${taskJson.task_type}`);
1309
+ } catch {
1310
+ }
1311
+ }
1312
+ }
1313
+ }
1314
+ }
1315
+ } catch {
1316
+ }
1317
+ }
1097
1318
  if (!ctx.input.dryRun && ctx.input.issueNumber) {
1098
1319
  if (def.name === "build") {
1099
1320
  try {
@@ -1285,6 +1506,7 @@ Task: ${state.taskId}`);
1285
1506
  console.log(` ${icon} ${stage.name}: ${s.state}${extra}`);
1286
1507
  }
1287
1508
  }
1509
+ var COMPLEXITY_SKIP;
1288
1510
  var init_state_machine = __esm({
1289
1511
  "src/state-machine.ts"() {
1290
1512
  "use strict";
@@ -1296,6 +1518,11 @@ var init_state_machine = __esm({
1296
1518
  init_verify_runner();
1297
1519
  init_config();
1298
1520
  init_logger();
1521
+ COMPLEXITY_SKIP = {
1522
+ low: ["plan", "review", "review-fix"],
1523
+ medium: ["review-fix"],
1524
+ high: []
1525
+ };
1299
1526
  }
1300
1527
  });
1301
1528
 
@@ -1392,7 +1619,7 @@ function parseArgs() {
1392
1619
  const args2 = process.argv.slice(2);
1393
1620
  if (hasFlag(args2, "--help") || hasFlag(args2, "-h") || args2.length === 0) {
1394
1621
  console.log(`Usage:
1395
- kody run --task-id <id> [--task "<desc>"] [--cwd <path>] [--issue-number <n>] [--feedback "<text>"] [--local] [--dry-run]
1622
+ kody run --task-id <id> [--task "<desc>"] [--cwd <path>] [--issue-number <n>] [--complexity low|medium|high] [--feedback "<text>"] [--local] [--dry-run]
1396
1623
  kody rerun --task-id <id> --from <stage> [--cwd <path>] [--issue-number <n>]
1397
1624
  kody status --task-id <id> [--cwd <path>]
1398
1625
  kody --help`);
@@ -1414,9 +1641,17 @@ function parseArgs() {
1414
1641
  cwd: getArg(args2, "--cwd"),
1415
1642
  issueNumber: issueStr ? parseInt(issueStr, 10) : void 0,
1416
1643
  feedback: getArg(args2, "--feedback") ?? process.env.FEEDBACK,
1417
- local: localFlag || !isCI2 && !hasFlag(args2, "--no-local")
1644
+ local: localFlag || !isCI2 && !hasFlag(args2, "--no-local"),
1645
+ complexity: getArg(args2, "--complexity") ?? process.env.COMPLEXITY
1418
1646
  };
1419
1647
  }
1648
+ function findLatestTaskForIssue(issueNumber, projectDir) {
1649
+ const tasksDir = path5.join(projectDir, ".tasks");
1650
+ if (!fs6.existsSync(tasksDir)) return null;
1651
+ const prefix = `${issueNumber}-`;
1652
+ const dirs = fs6.readdirSync(tasksDir).filter((d) => d.startsWith(prefix)).sort().reverse();
1653
+ return dirs[0] ?? null;
1654
+ }
1420
1655
  function generateTaskId() {
1421
1656
  const now = /* @__PURE__ */ new Date();
1422
1657
  const pad = (n) => String(n).padStart(2, "0");
@@ -1424,15 +1659,6 @@ function generateTaskId() {
1424
1659
  }
1425
1660
  async function main() {
1426
1661
  const input = parseArgs();
1427
- let taskId = input.taskId;
1428
- if (!taskId) {
1429
- if (input.command === "run" && input.task) {
1430
- taskId = generateTaskId();
1431
- } else {
1432
- console.error("--task-id is required");
1433
- process.exit(1);
1434
- }
1435
- }
1436
1662
  const projectDir = input.cwd ? path5.resolve(input.cwd) : process.cwd();
1437
1663
  if (input.cwd) {
1438
1664
  if (!fs6.existsSync(projectDir)) {
@@ -1443,6 +1669,25 @@ async function main() {
1443
1669
  setGhCwd(projectDir);
1444
1670
  logger.info(`Working directory: ${projectDir}`);
1445
1671
  }
1672
+ let taskId = input.taskId;
1673
+ if (!taskId) {
1674
+ if (input.command === "rerun" && input.issueNumber) {
1675
+ const found = findLatestTaskForIssue(input.issueNumber, projectDir);
1676
+ if (!found) {
1677
+ console.error(`No previous task found for issue #${input.issueNumber}`);
1678
+ process.exit(1);
1679
+ }
1680
+ taskId = found;
1681
+ logger.info(`Found latest task for issue #${input.issueNumber}: ${taskId}`);
1682
+ } else if (input.issueNumber) {
1683
+ taskId = `${input.issueNumber}-${generateTaskId()}`;
1684
+ } else if (input.command === "run" && input.task) {
1685
+ taskId = generateTaskId();
1686
+ } else {
1687
+ console.error("--task-id is required (or provide --issue-number to auto-generate)");
1688
+ process.exit(1);
1689
+ }
1690
+ }
1446
1691
  const taskDir = path5.join(projectDir, ".tasks", taskId);
1447
1692
  fs6.mkdirSync(taskDir, { recursive: true });
1448
1693
  if (input.command === "status") {
@@ -1473,31 +1718,87 @@ ${issue.body ?? ""}`;
1473
1718
  }
1474
1719
  }
1475
1720
  if (input.command === "rerun" && !input.fromStage) {
1476
- console.error("--from <stage> is required for rerun");
1721
+ const statusPath = path5.join(taskDir, "status.json");
1722
+ if (fs6.existsSync(statusPath)) {
1723
+ try {
1724
+ const status = JSON.parse(fs6.readFileSync(statusPath, "utf-8"));
1725
+ const stageNames = ["taskify", "plan", "build", "verify", "review", "review-fix", "ship"];
1726
+ let foundPaused = false;
1727
+ for (const name of stageNames) {
1728
+ const s = status.stages[name];
1729
+ if (s?.error?.includes("paused")) {
1730
+ const idx = stageNames.indexOf(name);
1731
+ if (idx < stageNames.length - 1) {
1732
+ input.fromStage = stageNames[idx + 1];
1733
+ foundPaused = true;
1734
+ logger.info(`Auto-detected resume from: ${input.fromStage} (after paused ${name})`);
1735
+ break;
1736
+ }
1737
+ }
1738
+ if (s?.state === "failed" || s?.state === "pending") {
1739
+ input.fromStage = name;
1740
+ foundPaused = true;
1741
+ logger.info(`Auto-detected resume from: ${input.fromStage}`);
1742
+ break;
1743
+ }
1744
+ }
1745
+ if (!foundPaused) {
1746
+ input.fromStage = "taskify";
1747
+ logger.info("No paused/failed stage found, resuming from taskify");
1748
+ }
1749
+ } catch {
1750
+ console.error("--from <stage> is required (could not read status.json)");
1751
+ process.exit(1);
1752
+ }
1753
+ } else {
1754
+ console.error("--from <stage> is required for rerun (no status.json found)");
1755
+ process.exit(1);
1756
+ }
1757
+ }
1758
+ const config = getProjectConfig();
1759
+ const runners = createRunners(config);
1760
+ const defaultRunnerName = config.agent.defaultRunner ?? Object.keys(runners)[0] ?? "claude";
1761
+ const defaultRunner = runners[defaultRunnerName];
1762
+ if (!defaultRunner) {
1763
+ console.error(`Default runner "${defaultRunnerName}" not configured`);
1477
1764
  process.exit(1);
1478
1765
  }
1479
- const runner = createClaudeCodeRunner();
1480
- const healthy = await runner.healthCheck();
1766
+ const healthy = await defaultRunner.healthCheck();
1481
1767
  if (!healthy) {
1482
- console.error("Claude Code CLI not available. Install: npm i -g @anthropic-ai/claude-code");
1768
+ console.error(`Runner "${defaultRunnerName}" health check failed`);
1483
1769
  process.exit(1);
1484
1770
  }
1485
1771
  const ctx = {
1486
1772
  taskId,
1487
1773
  taskDir,
1488
1774
  projectDir,
1489
- runner,
1775
+ runners,
1490
1776
  input: {
1491
1777
  mode: input.command === "rerun" ? "rerun" : "full",
1492
1778
  fromStage: input.fromStage,
1493
1779
  dryRun: input.dryRun,
1494
1780
  issueNumber: input.issueNumber,
1495
1781
  feedback: input.feedback,
1496
- local: input.local
1782
+ local: input.local,
1783
+ complexity: input.complexity
1497
1784
  }
1498
1785
  };
1786
+ logger.info(`Task: ${taskId}`);
1499
1787
  logger.info(`Mode: ${ctx.input.mode}${ctx.input.local ? " (local)" : " (CI)"}`);
1500
1788
  if (ctx.input.issueNumber) logger.info(`Issue: #${ctx.input.issueNumber}`);
1789
+ if (ctx.input.issueNumber && !ctx.input.local && ctx.input.mode === "full") {
1790
+ const runUrl = process.env.RUN_URL ?? "";
1791
+ const runLink = runUrl ? ` ([logs](${runUrl}))` : "";
1792
+ try {
1793
+ postComment(
1794
+ ctx.input.issueNumber,
1795
+ `\u{1F680} Kody pipeline started: \`${taskId}\`${runLink}
1796
+
1797
+ To rerun: \`@kody rerun ${taskId} --from <stage>\``
1798
+ );
1799
+ } catch {
1800
+ }
1801
+ }
1501
1802
  const state = await runPipeline(ctx);
1502
1803
  const files = fs6.readdirSync(taskDir);
1503
1804
  console.log(`
@@ -1562,7 +1863,7 @@ function getVersion() {
1562
1863
  const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
1563
1864
  return pkg.version;
1564
1865
  }
1565
- function checkCommand(name, args2, fix) {
1866
+ function checkCommand2(name, args2, fix) {
1566
1867
  try {
1567
1868
  const output = execFileSync7(name, args2, {
1568
1869
  encoding: "utf-8",
@@ -1761,11 +2062,11 @@ function initCommand(opts) {
1761
2062
  }
1762
2063
  console.log("\n\u2500\u2500 Prerequisites \u2500\u2500");
1763
2064
  const checks = [
1764
- checkCommand("claude", ["--version"], "Install: npm i -g @anthropic-ai/claude-code"),
1765
- checkCommand("gh", ["--version"], "Install: https://cli.github.com"),
1766
- checkCommand("git", ["--version"], "Install git"),
1767
- checkCommand("node", ["--version"], "Install Node.js >= 22"),
1768
- checkCommand("pnpm", ["--version"], "Install: npm i -g pnpm"),
2065
+ checkCommand2("claude", ["--version"], "Install: npm i -g @anthropic-ai/claude-code"),
2066
+ checkCommand2("gh", ["--version"], "Install: https://cli.github.com"),
2067
+ checkCommand2("git", ["--version"], "Install git"),
2068
+ checkCommand2("node", ["--version"], "Install Node.js >= 22"),
2069
+ checkCommand2("pnpm", ["--version"], "Install: npm i -g pnpm"),
1769
2070
  checkFile(path6.join(cwd, "package.json"), "package.json", "Run: pnpm init")
1770
2071
  ];
1771
2072
  for (const c of checks) {
@@ -1797,7 +2098,16 @@ function initCommand(opts) {
1797
2098
  { name: "kody:building", color: "0e8a16", description: "Kody is building code" },
1798
2099
  { name: "kody:review", color: "fbca04", description: "Kody is reviewing code" },
1799
2100
  { name: "kody:done", color: "0e8a16", description: "Kody completed successfully" },
1800
- { name: "kody:failed", color: "d93f0b", description: "Kody pipeline failed" }
2101
+ { name: "kody:failed", color: "d93f0b", description: "Kody pipeline failed" },
2102
+ { name: "kody:waiting", color: "fef2c0", description: "Kody is waiting for answers" },
2103
+ { name: "kody:low", color: "bfdadc", description: "Low complexity \u2014 skip plan/review" },
2104
+ { name: "kody:medium", color: "c5def5", description: "Medium complexity \u2014 skip review-fix" },
2105
+ { name: "kody:high", color: "d4c5f9", description: "High complexity \u2014 full pipeline" },
2106
+ { name: "kody:feature", color: "0e8a16", description: "New feature" },
2107
+ { name: "kody:bugfix", color: "d93f0b", description: "Bug fix" },
2108
+ { name: "kody:refactor", color: "fbca04", description: "Code refactoring" },
2109
+ { name: "kody:docs", color: "0075ca", description: "Documentation" },
2110
+ { name: "kody:chore", color: "e4e669", description: "Maintenance task" }
1801
2111
  ];
1802
2112
  console.log("\n\u2500\u2500 Labels \u2500\u2500");
1803
2113
  for (const label of labels) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine-lite",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Autonomous SDLC pipeline: Kody orchestration + Claude Code + LiteLLM",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/prompts/plan.md CHANGED
@@ -9,7 +9,7 @@ You are a planning agent following the Superpowers Writing Plans methodology.
9
9
 
10
10
  Before planning, examine the codebase to understand existing code structure, patterns, and conventions. Use Read, Glob, and Grep.
11
11
 
12
- Output a markdown plan with numbered steps:
12
+ Output a markdown plan. Start with the steps, then optionally add a Questions section at the end.
13
13
 
14
14
  ## Step N: <short description>
15
15
  **File:** <exact file path>
@@ -27,4 +27,22 @@ Superpowers Writing Plans rules:
27
27
  7. If modifying existing code, show the exact function/line to change
28
28
  8. Keep it simple — avoid unnecessary abstractions (YAGNI)
29
29
 
30
+ If there are architecture decisions or technical tradeoffs that need input, add a Questions section at the END of your plan:
31
+
32
+ ## Questions
33
+
34
+ - <question about architecture decision or tradeoff>
35
+
36
+ Questions rules:
37
+ - ONLY ask about significant architecture/technical decisions that affect the implementation
38
+ - Ask about: design pattern choice, database schema decisions, API contract changes, performance tradeoffs
39
+ - Recommend an approach with rationale — don't just ask open-ended questions
40
+ - Do NOT ask about requirements — those should be clear from task.json
41
+ - Do NOT ask about things you can determine from the codebase
42
+ - If no questions, omit the Questions section entirely
43
+ - Maximum 3 questions — only decisions with real impact
44
+
45
+ Good questions: "Recommend middleware pattern vs wrapper — middleware is simpler but wrapper allows caching. Approve middleware?"
46
+ Bad questions: "What should I name the function?", "Should I add tests?"
47
+
30
48
  {{TASK_CONTEXT}}
@@ -17,7 +17,8 @@ Required JSON format:
17
17
  "title": "Brief title, max 72 characters",
18
18
  "description": "Clear description of what the task requires",
19
19
  "scope": ["list", "of", "exact/file/paths", "affected"],
20
- "risk_level": "low | medium | high"
20
+ "risk_level": "low | medium | high",
21
+ "questions": []
21
22
  }
22
23
 
23
24
  Risk level heuristics:
@@ -25,6 +26,17 @@ Risk level heuristics:
25
26
  - medium: multiple files, possible side effects, API changes, new dependencies, refactoring existing logic
26
27
  - high: core business logic, data migrations, security, authentication, payment processing, database schema changes
27
28
 
29
+ Questions rules:
30
+ - ONLY ask product/requirements questions — things you CANNOT determine by reading code
31
+ - Ask about: unclear scope, missing acceptance criteria, ambiguous user behavior, missing edge case decisions
32
+ - Do NOT ask about technical implementation — that is the planner's job
33
+ - Do NOT ask about things you can find by reading the codebase (file structure, frameworks, patterns)
34
+ - If the task is clear and complete, leave questions as an empty array []
35
+ - Maximum 3 questions — only the most important ones
36
+
37
+ Good questions: "Should the search be case-sensitive?", "Which users should have access?", "Should this work offline?"
38
+ Bad questions: "What framework should I use?", "Where should I put the file?", "What's the project structure?"
39
+
28
40
  Guidelines:
29
41
  - scope must contain exact file paths (use Glob to discover them)
30
42
  - title must be actionable ("Add X", "Fix Y", "Refactor Z")
@@ -103,16 +103,41 @@ jobs:
103
103
 
104
104
  # Validate mode
105
105
  case "$MODE" in
106
- full|rerun|status) ;;
107
- *) MODE="full"; TASK_ID="$MODE" ;;
106
+ full|rerun|status|approve) ;;
107
+ *)
108
+ # If first arg isn't a mode, it might be a task-id or nothing
109
+ if [ -n "$MODE" ] && [ "$MODE" != "" ]; then
110
+ TASK_ID="$MODE"
111
+ fi
112
+ MODE="full"
113
+ ;;
108
114
  esac
109
115
 
116
+ # Auto-generate task-id if not provided
117
+ ISSUE_NUM="${{ github.event.issue.number }}"
118
+ if [ -z "$TASK_ID" ]; then
119
+ TASK_ID="${ISSUE_NUM}-$(date +%y%m%d-%H%M%S)"
120
+ fi
121
+
122
+ # For approve mode: extract answer body and convert to rerun
123
+ if [ "$MODE" = "approve" ]; then
124
+ # Everything after @kody approve [task-id] is the feedback
125
+ APPROVE_BODY=$(echo "$BODY" | sed -n '/\(@kody\|\/kody\)\s*approve/,$p' | tail -n +2)
126
+ FEEDBACK="$APPROVE_BODY"
127
+ MODE="rerun"
128
+ # FROM_STAGE will be determined by entry.ts from paused state
129
+ fi
130
+
110
131
  echo "task_id=$TASK_ID" >> $GITHUB_OUTPUT
111
132
  echo "mode=$MODE" >> $GITHUB_OUTPUT
112
133
  echo "from_stage=$FROM_STAGE" >> $GITHUB_OUTPUT
113
- echo "issue_number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT
114
- echo "feedback=$FEEDBACK" >> $GITHUB_OUTPUT
115
- echo "valid=$([ -n "$TASK_ID" ] && echo true || echo false)" >> $GITHUB_OUTPUT
134
+ echo "issue_number=$ISSUE_NUM" >> $GITHUB_OUTPUT
135
+ {
136
+ echo "feedback<<KODY_EOF"
137
+ echo "$FEEDBACK"
138
+ echo "KODY_EOF"
139
+ } >> $GITHUB_OUTPUT
140
+ echo "valid=true" >> $GITHUB_OUTPUT
116
141
 
117
142
  # ─── Orchestrate ─────────────────────────────────────────────────────────────
118
143
  orchestrate: