@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 +87 -8
- package/dist/bin/cli.js +383 -73
- package/package.json +1 -1
- package/prompts/plan.md +19 -1
- package/prompts/taskify.md +13 -1
- package/templates/kody.yml +30 -5
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
|
|
156
|
-
@kody
|
|
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 |
|
|
166
|
-
| build |
|
|
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 |
|
|
169
|
-
| review-fix |
|
|
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
|
|
204
|
-
| `agent.modelMap.strong` | Model for
|
|
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(
|
|
53
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
1480
|
-
const healthy = await runner.healthCheck();
|
|
1766
|
+
const healthy = await defaultRunner.healthCheck();
|
|
1481
1767
|
if (!healthy) {
|
|
1482
|
-
console.error("
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
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
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
|
|
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}}
|
package/prompts/taskify.md
CHANGED
|
@@ -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")
|
package/templates/kody.yml
CHANGED
|
@@ -103,16 +103,41 @@ jobs:
|
|
|
103
103
|
|
|
104
104
|
# Validate mode
|
|
105
105
|
case "$MODE" in
|
|
106
|
-
full|rerun|status) ;;
|
|
107
|
-
*)
|
|
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=$
|
|
114
|
-
|
|
115
|
-
|
|
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:
|