@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 +87 -8
- package/dist/bin/cli.js +643 -107
- 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,64 @@ 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
|
-
|
|
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
|
-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
1480
|
-
const healthy = await runner.healthCheck();
|
|
1773
|
+
const healthy = await defaultRunner.healthCheck();
|
|
1481
1774
|
if (!healthy) {
|
|
1482
|
-
console.error("
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
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
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
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
|
|
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
|
-
|
|
1869
|
-
|
|
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
|
-
|
|
2411
|
+
fs7.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
|
|
1876
2412
|
|
|
1877
2413
|
## Overview
|
|
1878
2414
|
${archItems.join("\n")}
|
|
1879
|
-
|
|
1880
|
-
|
|
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
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
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
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:
|