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

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,64 @@ 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;
106
+ return checkCommand("claude", ["--version"]);
107
+ }
108
+ };
109
+ }
110
+ function createOpenCodeRunner() {
111
+ return {
112
+ async run(stageName, prompt, model, timeout, _taskDir, options) {
113
+ const args2 = ["run"];
114
+ if (model) {
115
+ args2.push("--model", model);
98
116
  }
117
+ args2.push(prompt);
118
+ return runSubprocess(
119
+ "opencode",
120
+ args2,
121
+ "",
122
+ // opencode takes message as positional, not stdin
123
+ timeout,
124
+ options
125
+ );
126
+ },
127
+ async healthCheck() {
128
+ return checkCommand("opencode", ["--version"]);
99
129
  }
100
130
  };
101
131
  }
102
- var SIGKILL_GRACE_MS, STDERR_TAIL_CHARS;
132
+ function createRunners(config) {
133
+ if (config.agent.runners && Object.keys(config.agent.runners).length > 0) {
134
+ const runners = {};
135
+ for (const [name, runnerConfig] of Object.entries(config.agent.runners)) {
136
+ const factory2 = RUNNER_FACTORIES[runnerConfig.type];
137
+ if (factory2) {
138
+ runners[name] = factory2();
139
+ }
140
+ }
141
+ return runners;
142
+ }
143
+ const runnerType = config.agent.runner ?? "claude-code";
144
+ const factory = RUNNER_FACTORIES[runnerType];
145
+ const defaultName = config.agent.defaultRunner ?? "claude";
146
+ return { [defaultName]: factory ? factory() : createClaudeCodeRunner() };
147
+ }
148
+ var SIGKILL_GRACE_MS, STDERR_TAIL_CHARS, RUNNER_FACTORIES;
103
149
  var init_agent_runner = __esm({
104
150
  "src/agent-runner.ts"() {
105
151
  "use strict";
106
152
  SIGKILL_GRACE_MS = 5e3;
107
153
  STDERR_TAIL_CHARS = 500;
154
+ RUNNER_FACTORIES = {
155
+ "claude-code": createClaudeCodeRunner,
156
+ "opencode": createOpenCodeRunner
157
+ };
108
158
  }
109
159
  });
110
160
 
@@ -125,7 +175,7 @@ var init_definitions = __esm({
125
175
  {
126
176
  name: "plan",
127
177
  type: "agent",
128
- modelTier: "mid",
178
+ modelTier: "strong",
129
179
  timeout: 3e5,
130
180
  maxRetries: 1,
131
181
  outputFile: "plan.md"
@@ -133,7 +183,7 @@ var init_definitions = __esm({
133
183
  {
134
184
  name: "build",
135
185
  type: "agent",
136
- modelTier: "strong",
186
+ modelTier: "mid",
137
187
  timeout: 12e5,
138
188
  maxRetries: 1
139
189
  },
@@ -148,7 +198,7 @@ var init_definitions = __esm({
148
198
  {
149
199
  name: "review",
150
200
  type: "agent",
151
- modelTier: "mid",
201
+ modelTier: "strong",
152
202
  timeout: 3e5,
153
203
  maxRetries: 1,
154
204
  outputFile: "review.md"
@@ -156,7 +206,7 @@ var init_definitions = __esm({
156
206
  {
157
207
  name: "review-fix",
158
208
  type: "agent",
159
- modelTier: "strong",
209
+ modelTier: "mid",
160
210
  timeout: 6e5,
161
211
  maxRetries: 1
162
212
  },
@@ -253,6 +303,7 @@ var init_config = __esm({
253
303
  },
254
304
  agent: {
255
305
  runner: "claude-code",
306
+ defaultRunner: "claude",
256
307
  modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
257
308
  }
258
309
  };
@@ -663,7 +714,7 @@ var init_github_api = __esm({
663
714
  "use strict";
664
715
  init_logger();
665
716
  API_TIMEOUT_MS = 3e4;
666
- LIFECYCLE_LABELS = ["planning", "building", "review", "done", "failed"];
717
+ LIFECYCLE_LABELS = ["planning", "building", "review", "done", "failed", "waiting", "low", "medium", "high"];
667
718
  }
668
719
  });
669
720
 
@@ -743,6 +794,65 @@ var init_verify_runner = __esm({
743
794
  import * as fs4 from "fs";
744
795
  import * as path4 from "path";
745
796
  import { execFileSync as execFileSync5 } from "child_process";
797
+ function filterByComplexity(stages, complexity) {
798
+ const skip = COMPLEXITY_SKIP[complexity] ?? [];
799
+ return stages.filter((s) => !skip.includes(s.name));
800
+ }
801
+ function checkForQuestions(ctx, stageName) {
802
+ if (ctx.input.local || !ctx.input.issueNumber) return false;
803
+ try {
804
+ if (stageName === "taskify") {
805
+ const taskJsonPath = path4.join(ctx.taskDir, "task.json");
806
+ if (!fs4.existsSync(taskJsonPath)) return false;
807
+ const raw = fs4.readFileSync(taskJsonPath, "utf-8");
808
+ const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
809
+ const taskJson = JSON.parse(cleaned);
810
+ if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
811
+ const body = `\u{1F914} **Kody has questions before proceeding:**
812
+
813
+ ${taskJson.questions.map((q, i) => `${i + 1}. ${q}`).join("\n")}
814
+
815
+ Reply with \`@kody approve\` and your answers in the comment body.`;
816
+ postComment(ctx.input.issueNumber, body);
817
+ setLifecycleLabel(ctx.input.issueNumber, "waiting");
818
+ return true;
819
+ }
820
+ }
821
+ if (stageName === "plan") {
822
+ const planPath = path4.join(ctx.taskDir, "plan.md");
823
+ if (!fs4.existsSync(planPath)) return false;
824
+ const plan = fs4.readFileSync(planPath, "utf-8");
825
+ const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
826
+ if (questionsMatch) {
827
+ const questionsText = questionsMatch[1].trim();
828
+ const questions = questionsText.split("\n").filter((l) => l.startsWith("- ")).map((l) => l.slice(2));
829
+ if (questions.length > 0) {
830
+ const body = `\u{1F3D7}\uFE0F **Kody has architecture questions:**
831
+
832
+ ${questions.map((q, i) => `${i + 1}. ${q}`).join("\n")}
833
+
834
+ Reply with \`@kody approve\` and your answers in the comment body.`;
835
+ postComment(ctx.input.issueNumber, body);
836
+ setLifecycleLabel(ctx.input.issueNumber, "waiting");
837
+ return true;
838
+ }
839
+ }
840
+ }
841
+ } catch {
842
+ }
843
+ return false;
844
+ }
845
+ function getRunnerForStage(ctx, stageName) {
846
+ const config = getProjectConfig();
847
+ const runnerName = config.agent.stageRunners?.[stageName] ?? config.agent.defaultRunner ?? Object.keys(ctx.runners)[0] ?? "claude";
848
+ const runner = ctx.runners[runnerName];
849
+ if (!runner) {
850
+ throw new Error(
851
+ `Runner "${runnerName}" not found for stage ${stageName}. Available: ${Object.keys(ctx.runners).join(", ")}`
852
+ );
853
+ }
854
+ return runner;
855
+ }
746
856
  function loadState(taskId, taskDir) {
747
857
  const p = path4.join(taskDir, "status.json");
748
858
  if (!fs4.existsSync(p)) return null;
@@ -788,13 +898,15 @@ async function executeAgentStage(ctx, def) {
788
898
  }
789
899
  const prompt = buildFullPrompt(def.name, ctx.taskId, ctx.taskDir, ctx.projectDir, ctx.input.feedback);
790
900
  const model = resolveModel(def.modelTier, def.name);
791
- logger.info(` model=${model} timeout=${def.timeout / 1e3}s`);
792
901
  const config = getProjectConfig();
902
+ const runnerName = config.agent.stageRunners?.[def.name] ?? config.agent.defaultRunner ?? Object.keys(ctx.runners)[0] ?? "claude";
903
+ logger.info(` runner=${runnerName} model=${model} timeout=${def.timeout / 1e3}s`);
793
904
  const extraEnv = {};
794
905
  if (config.agent.litellmUrl) {
795
906
  extraEnv.ANTHROPIC_BASE_URL = config.agent.litellmUrl;
796
907
  }
797
- const result = await ctx.runner.run(def.name, prompt, model, def.timeout, ctx.taskDir, {
908
+ const runner = getRunnerForStage(ctx, def.name);
909
+ const result = await runner.run(def.name, prompt, model, def.timeout, ctx.taskDir, {
798
910
  cwd: ctx.projectDir,
799
911
  env: extraEnv
800
912
  });
@@ -911,6 +1023,9 @@ async function executeVerifyWithAutofix(ctx, def) {
911
1023
  };
912
1024
  }
913
1025
  async function executeReviewWithFix(ctx, def) {
1026
+ if (ctx.input.dryRun) {
1027
+ return { outcome: "completed", retries: 0 };
1028
+ }
914
1029
  const reviewDef = STAGES.find((s) => s.name === "review");
915
1030
  const reviewFixDef = STAGES.find((s) => s.name === "review-fix");
916
1031
  const reviewResult = await executeAgentStage(ctx, reviewDef);
@@ -934,8 +1049,65 @@ async function executeReviewWithFix(ctx, def) {
934
1049
  logger.info(` re-running review after fix...`);
935
1050
  return executeAgentStage(ctx, reviewDef);
936
1051
  }
1052
+ function buildPrBody(ctx) {
1053
+ const sections = [];
1054
+ const taskJsonPath = path4.join(ctx.taskDir, "task.json");
1055
+ if (fs4.existsSync(taskJsonPath)) {
1056
+ try {
1057
+ const raw = fs4.readFileSync(taskJsonPath, "utf-8");
1058
+ const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
1059
+ const task = JSON.parse(cleaned);
1060
+ sections.push(`## Summary`);
1061
+ sections.push(`**Type:** ${task.task_type ?? "unknown"} | **Risk:** ${task.risk_level ?? "unknown"}`);
1062
+ if (task.description) sections.push(`
1063
+ ${task.description}`);
1064
+ if (task.scope?.length) sections.push(`
1065
+ **Scope:** ${task.scope.join(", ")}`);
1066
+ } catch {
1067
+ }
1068
+ }
1069
+ const planPath = path4.join(ctx.taskDir, "plan.md");
1070
+ if (fs4.existsSync(planPath)) {
1071
+ const plan = fs4.readFileSync(planPath, "utf-8").trim();
1072
+ if (plan) {
1073
+ const truncated = plan.length > 500 ? plan.slice(0, 500) + "\n..." : plan;
1074
+ sections.push(`
1075
+ ## Plan
1076
+ <details><summary>Implementation plan</summary>
1077
+
1078
+ ${truncated}
1079
+ </details>`);
1080
+ }
1081
+ }
1082
+ const reviewPath = path4.join(ctx.taskDir, "review.md");
1083
+ if (fs4.existsSync(reviewPath)) {
1084
+ const review = fs4.readFileSync(reviewPath, "utf-8");
1085
+ const verdictMatch = review.match(/## Verdict:\s*(PASS|FAIL)/i);
1086
+ if (verdictMatch) {
1087
+ sections.push(`
1088
+ **Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
1089
+ }
1090
+ }
1091
+ const verifyPath = path4.join(ctx.taskDir, "verify.md");
1092
+ if (fs4.existsSync(verifyPath)) {
1093
+ const verify = fs4.readFileSync(verifyPath, "utf-8");
1094
+ if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
1095
+ }
1096
+ if (ctx.input.issueNumber) {
1097
+ sections.push(`
1098
+ Closes #${ctx.input.issueNumber}`);
1099
+ }
1100
+ sections.push(`
1101
+ ---
1102
+ \u{1F916} Generated by Kody`);
1103
+ return sections.join("\n");
1104
+ }
937
1105
  function executeShipStage(ctx, _def) {
938
1106
  const shipPath = path4.join(ctx.taskDir, "ship.md");
1107
+ if (ctx.input.dryRun) {
1108
+ fs4.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
1109
+ return { outcome: "completed", outputFile: "ship.md", retries: 0 };
1110
+ }
939
1111
  if (ctx.input.local && !ctx.input.issueNumber) {
940
1112
  fs4.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
941
1113
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
@@ -968,13 +1140,7 @@ function executeShipStage(ctx, _def) {
968
1140
  const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
969
1141
  title = (lines[0] ?? "Update").slice(0, 72);
970
1142
  }
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`;
1143
+ const body = buildPrBody(ctx);
978
1144
  const pr = createPR(head, base, title, body);
979
1145
  if (pr) {
980
1146
  if (ctx.input.issueNumber && !ctx.input.local) {
@@ -1035,6 +1201,9 @@ async function runPipeline(ctx) {
1035
1201
  logger.warn(` Failed to create feature branch: ${err}`);
1036
1202
  }
1037
1203
  }
1204
+ let complexity = ctx.input.complexity ?? "high";
1205
+ let activeStages = filterByComplexity(STAGES, complexity);
1206
+ let skippedStagesCommentPosted = false;
1038
1207
  for (const def of STAGES) {
1039
1208
  if (!startExecution) {
1040
1209
  if (def.name === fromStage) {
@@ -1047,6 +1216,23 @@ async function runPipeline(ctx) {
1047
1216
  logger.info(`[${def.name}] already completed, skipping`);
1048
1217
  continue;
1049
1218
  }
1219
+ if (!activeStages.find((s) => s.name === def.name)) {
1220
+ logger.info(`[${def.name}] skipped (complexity: ${complexity})`);
1221
+ state.stages[def.name] = { state: "completed", retries: 0, outputFile: void 0 };
1222
+ writeState(state, ctx.taskDir);
1223
+ if (!skippedStagesCommentPosted && ctx.input.issueNumber && !ctx.input.local && !ctx.input.dryRun) {
1224
+ const skipped = STAGES.filter((s) => !activeStages.find((a) => a.name === s.name)).map((s) => s.name);
1225
+ try {
1226
+ postComment(
1227
+ ctx.input.issueNumber,
1228
+ `\u26A1 **Complexity: ${complexity}** \u2014 skipping ${skipped.join(", ")} (not needed for ${complexity}-risk tasks)`
1229
+ );
1230
+ } catch {
1231
+ }
1232
+ skippedStagesCommentPosted = true;
1233
+ }
1234
+ continue;
1235
+ }
1050
1236
  ciGroup(`Stage: ${def.name}`);
1051
1237
  state.stages[def.name] = {
1052
1238
  state: "running",
@@ -1094,6 +1280,48 @@ async function runPipeline(ctx) {
1094
1280
  outputFile: result.outputFile
1095
1281
  };
1096
1282
  logger.info(`[${def.name}] \u2713 completed`);
1283
+ if ((def.name === "taskify" || def.name === "plan") && !ctx.input.dryRun) {
1284
+ const paused = checkForQuestions(ctx, def.name);
1285
+ if (paused) {
1286
+ state.state = "failed";
1287
+ state.stages[def.name] = {
1288
+ ...state.stages[def.name],
1289
+ state: "completed",
1290
+ error: "paused: waiting for answers"
1291
+ };
1292
+ writeState(state, ctx.taskDir);
1293
+ logger.info(` Pipeline paused \u2014 questions posted on issue`);
1294
+ return state;
1295
+ }
1296
+ }
1297
+ if (def.name === "taskify" && !ctx.input.complexity) {
1298
+ try {
1299
+ const taskJsonPath = path4.join(ctx.taskDir, "task.json");
1300
+ if (fs4.existsSync(taskJsonPath)) {
1301
+ const raw = fs4.readFileSync(taskJsonPath, "utf-8");
1302
+ const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
1303
+ const taskJson = JSON.parse(cleaned);
1304
+ if (taskJson.risk_level && COMPLEXITY_SKIP[taskJson.risk_level]) {
1305
+ complexity = taskJson.risk_level;
1306
+ activeStages = filterByComplexity(STAGES, complexity);
1307
+ logger.info(` Complexity auto-detected: ${complexity} (${activeStages.map((s) => s.name).join(" \u2192 ")})`);
1308
+ if (ctx.input.issueNumber && !ctx.input.local) {
1309
+ try {
1310
+ setLifecycleLabel(ctx.input.issueNumber, complexity);
1311
+ } catch {
1312
+ }
1313
+ if (taskJson.task_type) {
1314
+ try {
1315
+ setLabel(ctx.input.issueNumber, `kody:${taskJson.task_type}`);
1316
+ } catch {
1317
+ }
1318
+ }
1319
+ }
1320
+ }
1321
+ }
1322
+ } catch {
1323
+ }
1324
+ }
1097
1325
  if (!ctx.input.dryRun && ctx.input.issueNumber) {
1098
1326
  if (def.name === "build") {
1099
1327
  try {
@@ -1285,6 +1513,7 @@ Task: ${state.taskId}`);
1285
1513
  console.log(` ${icon} ${stage.name}: ${s.state}${extra}`);
1286
1514
  }
1287
1515
  }
1516
+ var COMPLEXITY_SKIP;
1288
1517
  var init_state_machine = __esm({
1289
1518
  "src/state-machine.ts"() {
1290
1519
  "use strict";
@@ -1296,6 +1525,11 @@ var init_state_machine = __esm({
1296
1525
  init_verify_runner();
1297
1526
  init_config();
1298
1527
  init_logger();
1528
+ COMPLEXITY_SKIP = {
1529
+ low: ["plan", "review", "review-fix"],
1530
+ medium: ["review-fix"],
1531
+ high: []
1532
+ };
1299
1533
  }
1300
1534
  });
1301
1535
 
@@ -1392,7 +1626,7 @@ function parseArgs() {
1392
1626
  const args2 = process.argv.slice(2);
1393
1627
  if (hasFlag(args2, "--help") || hasFlag(args2, "-h") || args2.length === 0) {
1394
1628
  console.log(`Usage:
1395
- kody run --task-id <id> [--task "<desc>"] [--cwd <path>] [--issue-number <n>] [--feedback "<text>"] [--local] [--dry-run]
1629
+ kody run --task-id <id> [--task "<desc>"] [--cwd <path>] [--issue-number <n>] [--complexity low|medium|high] [--feedback "<text>"] [--local] [--dry-run]
1396
1630
  kody rerun --task-id <id> --from <stage> [--cwd <path>] [--issue-number <n>]
1397
1631
  kody status --task-id <id> [--cwd <path>]
1398
1632
  kody --help`);
@@ -1414,9 +1648,17 @@ function parseArgs() {
1414
1648
  cwd: getArg(args2, "--cwd"),
1415
1649
  issueNumber: issueStr ? parseInt(issueStr, 10) : void 0,
1416
1650
  feedback: getArg(args2, "--feedback") ?? process.env.FEEDBACK,
1417
- local: localFlag || !isCI2 && !hasFlag(args2, "--no-local")
1651
+ local: localFlag || !isCI2 && !hasFlag(args2, "--no-local"),
1652
+ complexity: getArg(args2, "--complexity") ?? process.env.COMPLEXITY
1418
1653
  };
1419
1654
  }
1655
+ function findLatestTaskForIssue(issueNumber, projectDir) {
1656
+ const tasksDir = path5.join(projectDir, ".tasks");
1657
+ if (!fs6.existsSync(tasksDir)) return null;
1658
+ const prefix = `${issueNumber}-`;
1659
+ const dirs = fs6.readdirSync(tasksDir).filter((d) => d.startsWith(prefix)).sort().reverse();
1660
+ return dirs[0] ?? null;
1661
+ }
1420
1662
  function generateTaskId() {
1421
1663
  const now = /* @__PURE__ */ new Date();
1422
1664
  const pad = (n) => String(n).padStart(2, "0");
@@ -1424,15 +1666,6 @@ function generateTaskId() {
1424
1666
  }
1425
1667
  async function main() {
1426
1668
  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
1669
  const projectDir = input.cwd ? path5.resolve(input.cwd) : process.cwd();
1437
1670
  if (input.cwd) {
1438
1671
  if (!fs6.existsSync(projectDir)) {
@@ -1443,6 +1676,25 @@ async function main() {
1443
1676
  setGhCwd(projectDir);
1444
1677
  logger.info(`Working directory: ${projectDir}`);
1445
1678
  }
1679
+ let taskId = input.taskId;
1680
+ if (!taskId) {
1681
+ if (input.command === "rerun" && input.issueNumber) {
1682
+ const found = findLatestTaskForIssue(input.issueNumber, projectDir);
1683
+ if (!found) {
1684
+ console.error(`No previous task found for issue #${input.issueNumber}`);
1685
+ process.exit(1);
1686
+ }
1687
+ taskId = found;
1688
+ logger.info(`Found latest task for issue #${input.issueNumber}: ${taskId}`);
1689
+ } else if (input.issueNumber) {
1690
+ taskId = `${input.issueNumber}-${generateTaskId()}`;
1691
+ } else if (input.command === "run" && input.task) {
1692
+ taskId = generateTaskId();
1693
+ } else {
1694
+ console.error("--task-id is required (or provide --issue-number to auto-generate)");
1695
+ process.exit(1);
1696
+ }
1697
+ }
1446
1698
  const taskDir = path5.join(projectDir, ".tasks", taskId);
1447
1699
  fs6.mkdirSync(taskDir, { recursive: true });
1448
1700
  if (input.command === "status") {
@@ -1473,31 +1725,87 @@ ${issue.body ?? ""}`;
1473
1725
  }
1474
1726
  }
1475
1727
  if (input.command === "rerun" && !input.fromStage) {
1476
- console.error("--from <stage> is required for rerun");
1728
+ const statusPath = path5.join(taskDir, "status.json");
1729
+ if (fs6.existsSync(statusPath)) {
1730
+ try {
1731
+ const status = JSON.parse(fs6.readFileSync(statusPath, "utf-8"));
1732
+ const stageNames = ["taskify", "plan", "build", "verify", "review", "review-fix", "ship"];
1733
+ let foundPaused = false;
1734
+ for (const name of stageNames) {
1735
+ const s = status.stages[name];
1736
+ if (s?.error?.includes("paused")) {
1737
+ const idx = stageNames.indexOf(name);
1738
+ if (idx < stageNames.length - 1) {
1739
+ input.fromStage = stageNames[idx + 1];
1740
+ foundPaused = true;
1741
+ logger.info(`Auto-detected resume from: ${input.fromStage} (after paused ${name})`);
1742
+ break;
1743
+ }
1744
+ }
1745
+ if (s?.state === "failed" || s?.state === "pending") {
1746
+ input.fromStage = name;
1747
+ foundPaused = true;
1748
+ logger.info(`Auto-detected resume from: ${input.fromStage}`);
1749
+ break;
1750
+ }
1751
+ }
1752
+ if (!foundPaused) {
1753
+ input.fromStage = "taskify";
1754
+ logger.info("No paused/failed stage found, resuming from taskify");
1755
+ }
1756
+ } catch {
1757
+ console.error("--from <stage> is required (could not read status.json)");
1758
+ process.exit(1);
1759
+ }
1760
+ } else {
1761
+ console.error("--from <stage> is required for rerun (no status.json found)");
1762
+ process.exit(1);
1763
+ }
1764
+ }
1765
+ const config = getProjectConfig();
1766
+ const runners = createRunners(config);
1767
+ const defaultRunnerName = config.agent.defaultRunner ?? Object.keys(runners)[0] ?? "claude";
1768
+ const defaultRunner = runners[defaultRunnerName];
1769
+ if (!defaultRunner) {
1770
+ console.error(`Default runner "${defaultRunnerName}" not configured`);
1477
1771
  process.exit(1);
1478
1772
  }
1479
- const runner = createClaudeCodeRunner();
1480
- const healthy = await runner.healthCheck();
1773
+ const healthy = await defaultRunner.healthCheck();
1481
1774
  if (!healthy) {
1482
- console.error("Claude Code CLI not available. Install: npm i -g @anthropic-ai/claude-code");
1775
+ console.error(`Runner "${defaultRunnerName}" health check failed`);
1483
1776
  process.exit(1);
1484
1777
  }
1485
1778
  const ctx = {
1486
1779
  taskId,
1487
1780
  taskDir,
1488
1781
  projectDir,
1489
- runner,
1782
+ runners,
1490
1783
  input: {
1491
1784
  mode: input.command === "rerun" ? "rerun" : "full",
1492
1785
  fromStage: input.fromStage,
1493
1786
  dryRun: input.dryRun,
1494
1787
  issueNumber: input.issueNumber,
1495
1788
  feedback: input.feedback,
1496
- local: input.local
1789
+ local: input.local,
1790
+ complexity: input.complexity
1497
1791
  }
1498
1792
  };
1793
+ logger.info(`Task: ${taskId}`);
1499
1794
  logger.info(`Mode: ${ctx.input.mode}${ctx.input.local ? " (local)" : " (CI)"}`);
1500
1795
  if (ctx.input.issueNumber) logger.info(`Issue: #${ctx.input.issueNumber}`);
1796
+ if (ctx.input.issueNumber && !ctx.input.local && ctx.input.mode === "full") {
1797
+ const runUrl = process.env.RUN_URL ?? "";
1798
+ const runLink = runUrl ? ` ([logs](${runUrl}))` : "";
1799
+ try {
1800
+ postComment(
1801
+ ctx.input.issueNumber,
1802
+ `\u{1F680} Kody pipeline started: \`${taskId}\`${runLink}
1803
+
1804
+ To rerun: \`@kody rerun ${taskId} --from <stage>\``
1805
+ );
1806
+ } catch {
1807
+ }
1808
+ }
1501
1809
  const state = await runPipeline(ctx);
1502
1810
  const files = fs6.readdirSync(taskDir);
1503
1811
  console.log(`
@@ -1562,7 +1870,7 @@ function getVersion() {
1562
1870
  const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
1563
1871
  return pkg.version;
1564
1872
  }
1565
- function checkCommand(name, args2, fix) {
1873
+ function checkCommand2(name, args2, fix) {
1566
1874
  try {
1567
1875
  const output = execFileSync7(name, args2, {
1568
1876
  encoding: "utf-8",
@@ -1703,6 +2011,235 @@ function detectArchitecture(cwd) {
1703
2011
  if (configs.length > 0) detected.push(`- Config files: ${configs.join(", ")}`);
1704
2012
  return detected;
1705
2013
  }
2014
+ function detectBasicConfig(cwd) {
2015
+ let pm = "pnpm";
2016
+ if (fs7.existsSync(path6.join(cwd, "yarn.lock"))) pm = "yarn";
2017
+ else if (fs7.existsSync(path6.join(cwd, "bun.lockb"))) pm = "bun";
2018
+ else if (!fs7.existsSync(path6.join(cwd, "pnpm-lock.yaml")) && fs7.existsSync(path6.join(cwd, "package-lock.json"))) pm = "npm";
2019
+ let defaultBranch = "main";
2020
+ try {
2021
+ const ref = execFileSync7("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
2022
+ encoding: "utf-8",
2023
+ timeout: 5e3,
2024
+ cwd,
2025
+ stdio: ["pipe", "pipe", "pipe"]
2026
+ }).trim();
2027
+ defaultBranch = ref.replace("refs/remotes/origin/", "");
2028
+ } catch {
2029
+ try {
2030
+ execFileSync7("git", ["rev-parse", "--verify", "origin/dev"], {
2031
+ encoding: "utf-8",
2032
+ timeout: 5e3,
2033
+ cwd,
2034
+ stdio: ["pipe", "pipe", "pipe"]
2035
+ });
2036
+ defaultBranch = "dev";
2037
+ } catch {
2038
+ }
2039
+ }
2040
+ let owner = "";
2041
+ let repo = "";
2042
+ try {
2043
+ const remote = execFileSync7("git", ["remote", "get-url", "origin"], {
2044
+ encoding: "utf-8",
2045
+ timeout: 5e3,
2046
+ cwd,
2047
+ stdio: ["pipe", "pipe", "pipe"]
2048
+ }).trim();
2049
+ const match = remote.match(/github\.com[/:]([^/]+)\/([^/.]+)/);
2050
+ if (match) {
2051
+ owner = match[1];
2052
+ repo = match[2];
2053
+ }
2054
+ } catch {
2055
+ }
2056
+ const hasOpenCode = fs7.existsSync(path6.join(cwd, "opencode.json"));
2057
+ return { defaultBranch, owner, repo, pm, hasOpenCode };
2058
+ }
2059
+ function smartInit(cwd) {
2060
+ const basic = detectBasicConfig(cwd);
2061
+ let context = "";
2062
+ const readIfExists = (rel, maxChars = 3e3) => {
2063
+ const p = path6.join(cwd, rel);
2064
+ if (fs7.existsSync(p)) {
2065
+ const content = fs7.readFileSync(p, "utf-8");
2066
+ return content.slice(0, maxChars);
2067
+ }
2068
+ return null;
2069
+ };
2070
+ const pkgJson = readIfExists("package.json");
2071
+ if (pkgJson) context += `## package.json
2072
+ ${pkgJson}
2073
+
2074
+ `;
2075
+ const tsconfig = readIfExists("tsconfig.json", 1e3);
2076
+ if (tsconfig) context += `## tsconfig.json
2077
+ ${tsconfig}
2078
+
2079
+ `;
2080
+ const readme = readIfExists("README.md", 2e3);
2081
+ if (readme) context += `## README.md (first 2000 chars)
2082
+ ${readme}
2083
+
2084
+ `;
2085
+ const claudeMd = readIfExists("CLAUDE.md", 3e3);
2086
+ if (claudeMd) context += `## CLAUDE.md
2087
+ ${claudeMd}
2088
+
2089
+ `;
2090
+ try {
2091
+ const topDirs = fs7.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
2092
+ context += `## Top-level directories
2093
+ ${topDirs.join(", ")}
2094
+
2095
+ `;
2096
+ const srcDir = path6.join(cwd, "src");
2097
+ if (fs7.existsSync(srcDir)) {
2098
+ const srcDirs = fs7.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
2099
+ context += `## src/ subdirectories
2100
+ ${srcDirs.join(", ")}
2101
+
2102
+ `;
2103
+ }
2104
+ } catch {
2105
+ }
2106
+ const existingFiles = [];
2107
+ for (const f of [".env.example", "CLAUDE.md", ".ai-docs", "opencode.json", "vitest.config.ts", "vitest.config.mts", "jest.config.ts", "playwright.config.ts", ".eslintrc.js", "eslint.config.mjs", ".prettierrc"]) {
2108
+ if (fs7.existsSync(path6.join(cwd, f))) existingFiles.push(f);
2109
+ }
2110
+ if (existingFiles.length) context += `## Config files present
2111
+ ${existingFiles.join(", ")}
2112
+
2113
+ `;
2114
+ context += `## Detected: package manager=${basic.pm}, default branch=${basic.defaultBranch}, github=${basic.owner}/${basic.repo}, opencode=${basic.hasOpenCode}
2115
+ `;
2116
+ const prompt = `You are analyzing a project to configure Kody (an autonomous SDLC pipeline).
2117
+
2118
+ Given this project context, output ONLY a JSON object with EXACTLY this structure:
2119
+
2120
+ {
2121
+ "config": {
2122
+ "quality": {
2123
+ "typecheck": "${basic.pm} <script or command>",
2124
+ "lint": "${basic.pm} <script or command>",
2125
+ "lintFix": "${basic.pm} <script or command>",
2126
+ "format": "${basic.pm} <script or command>",
2127
+ "formatFix": "${basic.pm} <script or command>",
2128
+ "testUnit": "${basic.pm} <script or command>"
2129
+ },
2130
+ "git": { "defaultBranch": "${basic.defaultBranch}" },
2131
+ "github": { "owner": "${basic.owner}", "repo": "${basic.repo}" },
2132
+ "paths": { "taskDir": ".tasks" },
2133
+ "agent": {
2134
+ "runner": "${basic.hasOpenCode ? "opencode" : "claude-code"}",
2135
+ "defaultRunner": "${basic.hasOpenCode ? "opencode" : "claude"}",
2136
+ "modelMap": { "cheap": "haiku", "mid": "sonnet", "strong": "opus" }
2137
+ }
2138
+ },
2139
+ "architecture": "# Architecture\\n\\n<markdown content>",
2140
+ "conventions": "# Conventions\\n\\n<markdown content>"
2141
+ }
2142
+
2143
+ CRITICAL rules for config.quality:
2144
+ - Every command MUST start with "${basic.pm}" (e.g., "${basic.pm} typecheck", "${basic.pm} lint")
2145
+ - Look at the package.json "scripts" section to find the correct script names
2146
+ - testUnit must run ONLY unit tests \u2014 exclude integration and e2e tests. If there's a "test:unit" script use it. Otherwise use "test" but add exclude flags for int/e2e.
2147
+ - If a script doesn't exist and can't be inferred, set the value to ""
2148
+ - Do NOT invent commands that don't exist in package.json scripts
2149
+
2150
+ Rules for architecture (markdown string):
2151
+ - Be specific about THIS project
2152
+ - Include: framework, language, database, testing, key directories, data flow
2153
+ - Reference CLAUDE.md and .ai-docs/ if they exist
2154
+ - Keep under 50 lines
2155
+
2156
+ Rules for conventions (markdown string):
2157
+ - Extract actual patterns from the project
2158
+ - If CLAUDE.md exists, reference it
2159
+ - If .ai-docs/ exists, reference it
2160
+ - Keep under 30 lines
2161
+
2162
+ Output ONLY valid JSON. No markdown fences. No explanation before or after.
2163
+
2164
+ ${context}`;
2165
+ console.log(" \u23F3 Analyzing project with Claude Code...");
2166
+ try {
2167
+ const output = execFileSync7("claude", [
2168
+ "--print",
2169
+ "--model",
2170
+ "haiku",
2171
+ "--dangerously-skip-permissions",
2172
+ prompt
2173
+ ], {
2174
+ encoding: "utf-8",
2175
+ timeout: 12e4,
2176
+ cwd,
2177
+ stdio: ["pipe", "pipe", "pipe"]
2178
+ }).trim();
2179
+ const cleaned = output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2180
+ const parsed = JSON.parse(cleaned);
2181
+ const config = parsed.config ?? {};
2182
+ if (!config.git) config.git = {};
2183
+ if (!config.github) config.github = {};
2184
+ if (!config.paths) config.paths = {};
2185
+ if (!config.agent) config.agent = {};
2186
+ config.git.defaultBranch = config.git.defaultBranch || basic.defaultBranch;
2187
+ config.github.owner = config.github.owner || basic.owner;
2188
+ config.github.repo = config.github.repo || basic.repo;
2189
+ config.paths.taskDir = config.paths.taskDir || ".tasks";
2190
+ config.agent.runner = config.agent.runner || (basic.hasOpenCode ? "opencode" : "claude-code");
2191
+ config.agent.defaultRunner = config.agent.defaultRunner || (basic.hasOpenCode ? "opencode" : "claude");
2192
+ if (!config.agent.modelMap) {
2193
+ config.agent.modelMap = { cheap: "haiku", mid: "sonnet", strong: "opus" };
2194
+ }
2195
+ return {
2196
+ config,
2197
+ architecture: parsed.architecture ?? "",
2198
+ conventions: parsed.conventions ?? ""
2199
+ };
2200
+ } catch (err) {
2201
+ console.log(" \u26A0 Smart detection failed, falling back to basic detection");
2202
+ return {
2203
+ config: buildFallbackConfig(cwd, basic),
2204
+ architecture: "",
2205
+ conventions: ""
2206
+ };
2207
+ }
2208
+ }
2209
+ function buildFallbackConfig(cwd, basic) {
2210
+ const pkg = (() => {
2211
+ try {
2212
+ return JSON.parse(fs7.readFileSync(path6.join(cwd, "package.json"), "utf-8"));
2213
+ } catch {
2214
+ return {};
2215
+ }
2216
+ })();
2217
+ const scripts = pkg.scripts ?? {};
2218
+ const find = (...c) => {
2219
+ for (const s of c) {
2220
+ if (scripts[s]) return `${basic.pm} ${s}`;
2221
+ }
2222
+ return "";
2223
+ };
2224
+ return {
2225
+ quality: {
2226
+ typecheck: find("typecheck", "type-check") || (pkg.devDependencies?.typescript ? `${basic.pm} tsc --noEmit` : ""),
2227
+ lint: find("lint"),
2228
+ lintFix: find("lint:fix", "lint-fix"),
2229
+ format: find("format:check"),
2230
+ formatFix: find("format", "format:fix"),
2231
+ testUnit: find("test:unit", "test", "test:ci")
2232
+ },
2233
+ git: { defaultBranch: basic.defaultBranch },
2234
+ github: { owner: basic.owner, repo: basic.repo },
2235
+ paths: { taskDir: ".tasks" },
2236
+ agent: {
2237
+ runner: basic.hasOpenCode ? "opencode" : "claude-code",
2238
+ defaultRunner: basic.hasOpenCode ? "opencode" : "claude",
2239
+ modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
2240
+ }
2241
+ };
2242
+ }
1706
2243
  function initCommand(opts) {
1707
2244
  const cwd = process.cwd();
1708
2245
  console.log(`
@@ -1726,26 +2263,11 @@ function initCommand(opts) {
1726
2263
  console.log(" \u2713 .github/workflows/kody.yml");
1727
2264
  }
1728
2265
  const configDest = path6.join(cwd, "kody.config.json");
1729
- if (!fs7.existsSync(configDest)) {
1730
- const defaultConfig = {
1731
- quality: {
1732
- typecheck: "pnpm tsc --noEmit",
1733
- lint: "",
1734
- lintFix: "",
1735
- format: "",
1736
- formatFix: "",
1737
- testUnit: "pnpm test"
1738
- },
1739
- git: { defaultBranch: "main" },
1740
- github: { owner: "", repo: "" },
1741
- paths: { taskDir: ".tasks" },
1742
- agent: {
1743
- runner: "claude-code",
1744
- modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
1745
- }
1746
- };
1747
- fs7.writeFileSync(configDest, JSON.stringify(defaultConfig, null, 2) + "\n");
1748
- console.log(" \u2713 kody.config.json (created \u2014 edit github.owner and github.repo)");
2266
+ let smartResult = null;
2267
+ if (!fs7.existsSync(configDest) || opts.force) {
2268
+ smartResult = smartInit(cwd);
2269
+ fs7.writeFileSync(configDest, JSON.stringify(smartResult.config, null, 2) + "\n");
2270
+ console.log(" \u2713 kody.config.json (auto-configured)");
1749
2271
  } else {
1750
2272
  console.log(" \u25CB kody.config.json (exists)");
1751
2273
  }
@@ -1761,11 +2283,11 @@ function initCommand(opts) {
1761
2283
  }
1762
2284
  console.log("\n\u2500\u2500 Prerequisites \u2500\u2500");
1763
2285
  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"),
2286
+ checkCommand2("claude", ["--version"], "Install: npm i -g @anthropic-ai/claude-code"),
2287
+ checkCommand2("gh", ["--version"], "Install: https://cli.github.com"),
2288
+ checkCommand2("git", ["--version"], "Install git"),
2289
+ checkCommand2("node", ["--version"], "Install Node.js >= 22"),
2290
+ checkCommand2("pnpm", ["--version"], "Install: npm i -g pnpm"),
1769
2291
  checkFile(path6.join(cwd, "package.json"), "package.json", "Run: pnpm init")
1770
2292
  ];
1771
2293
  for (const c of checks) {
@@ -1797,7 +2319,16 @@ function initCommand(opts) {
1797
2319
  { name: "kody:building", color: "0e8a16", description: "Kody is building code" },
1798
2320
  { name: "kody:review", color: "fbca04", description: "Kody is reviewing code" },
1799
2321
  { name: "kody:done", color: "0e8a16", description: "Kody completed successfully" },
1800
- { name: "kody:failed", color: "d93f0b", description: "Kody pipeline failed" }
2322
+ { name: "kody:failed", color: "d93f0b", description: "Kody pipeline failed" },
2323
+ { name: "kody:waiting", color: "fef2c0", description: "Kody is waiting for answers" },
2324
+ { name: "kody:low", color: "bfdadc", description: "Low complexity \u2014 skip plan/review" },
2325
+ { name: "kody:medium", color: "c5def5", description: "Medium complexity \u2014 skip review-fix" },
2326
+ { name: "kody:high", color: "d4c5f9", description: "High complexity \u2014 full pipeline" },
2327
+ { name: "kody:feature", color: "0e8a16", description: "New feature" },
2328
+ { name: "kody:bugfix", color: "d93f0b", description: "Bug fix" },
2329
+ { name: "kody:refactor", color: "fbca04", description: "Code refactoring" },
2330
+ { name: "kody:docs", color: "0075ca", description: "Documentation" },
2331
+ { name: "kody:chore", color: "e4e669", description: "Maintenance task" }
1801
2332
  ];
1802
2333
  console.log("\n\u2500\u2500 Labels \u2500\u2500");
1803
2334
  for (const label of labels) {
@@ -1862,33 +2393,38 @@ function initCommand(opts) {
1862
2393
  console.log(" \u2717 kody.config.json \u2014 invalid JSON");
1863
2394
  }
1864
2395
  }
1865
- console.log("\n\u2500\u2500 Architecture Detection \u2500\u2500");
2396
+ console.log("\n\u2500\u2500 Project Memory \u2500\u2500");
1866
2397
  const memoryDir = path6.join(cwd, ".kody", "memory");
2398
+ fs7.mkdirSync(memoryDir, { recursive: true });
1867
2399
  const archPath = path6.join(memoryDir, "architecture.md");
1868
- if (fs7.existsSync(archPath)) {
1869
- console.log(" \u25CB .kody/memory/architecture.md (exists, not overwriting)");
2400
+ const conventionsPath = path6.join(memoryDir, "conventions.md");
2401
+ if (fs7.existsSync(archPath) && !opts.force) {
2402
+ console.log(" \u25CB .kody/memory/architecture.md (exists, use --force to regenerate)");
2403
+ } else if (smartResult?.architecture) {
2404
+ fs7.writeFileSync(archPath, smartResult.architecture);
2405
+ const lineCount = smartResult.architecture.split("\n").length;
2406
+ console.log(` \u2713 .kody/memory/architecture.md (${lineCount} lines, LLM-generated)`);
1870
2407
  } else {
1871
2408
  const archItems = detectArchitecture(cwd);
1872
2409
  if (archItems.length > 0) {
1873
- fs7.mkdirSync(memoryDir, { recursive: true });
1874
2410
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1875
- const content = `# Architecture (auto-detected ${timestamp2})
2411
+ fs7.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
1876
2412
 
1877
2413
  ## Overview
1878
2414
  ${archItems.join("\n")}
1879
- `;
1880
- fs7.writeFileSync(archPath, content);
1881
- console.log(` \u2713 .kody/memory/architecture.md (${archItems.length} items detected)`);
1882
- for (const item of archItems) {
1883
- console.log(` ${item}`);
1884
- }
2415
+ `);
2416
+ console.log(` \u2713 .kody/memory/architecture.md (${archItems.length} items, basic detection)`);
1885
2417
  } else {
1886
2418
  console.log(" \u25CB No architecture detected");
1887
2419
  }
1888
2420
  }
1889
- const conventionsPath = path6.join(memoryDir, "conventions.md");
1890
- if (!fs7.existsSync(conventionsPath)) {
1891
- fs7.mkdirSync(memoryDir, { recursive: true });
2421
+ if (fs7.existsSync(conventionsPath) && !opts.force) {
2422
+ console.log(" \u25CB .kody/memory/conventions.md (exists, use --force to regenerate)");
2423
+ } else if (smartResult?.conventions) {
2424
+ fs7.writeFileSync(conventionsPath, smartResult.conventions);
2425
+ const lineCount = smartResult.conventions.split("\n").length;
2426
+ console.log(` \u2713 .kody/memory/conventions.md (${lineCount} lines, LLM-generated)`);
2427
+ } else {
1892
2428
  fs7.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
1893
2429
  console.log(" \u2713 .kody/memory/conventions.md (seed)");
1894
2430
  }
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.8",
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: