@kody-ade/kody-engine-lite 0.1.112 → 0.1.113
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-runner.d.ts +4 -0
- package/dist/agent-runner.js +122 -0
- package/dist/bin/cli.js +98 -86
- package/dist/ci/parse-inputs.d.ts +6 -0
- package/dist/ci/parse-inputs.js +76 -0
- package/dist/ci/parse-safety.d.ts +6 -0
- package/dist/ci/parse-safety.js +22 -0
- package/dist/cli/args.d.ts +13 -0
- package/dist/cli/args.js +42 -0
- package/dist/cli/litellm.d.ts +2 -0
- package/dist/cli/litellm.js +85 -0
- package/dist/cli/task-resolution.d.ts +2 -0
- package/dist/cli/task-resolution.js +41 -0
- package/dist/config.d.ts +49 -0
- package/dist/config.js +72 -0
- package/dist/context.d.ts +4 -0
- package/dist/context.js +83 -0
- package/dist/definitions.d.ts +3 -0
- package/dist/definitions.js +59 -0
- package/dist/entry.d.ts +1 -0
- package/dist/entry.js +236 -0
- package/dist/git-utils.d.ts +13 -0
- package/dist/git-utils.js +174 -0
- package/dist/github-api.d.ts +14 -0
- package/dist/github-api.js +114 -0
- package/dist/kody-utils.d.ts +1 -0
- package/dist/kody-utils.js +9 -0
- package/dist/learning/auto-learn.d.ts +2 -0
- package/dist/learning/auto-learn.js +169 -0
- package/dist/logger.d.ts +14 -0
- package/dist/logger.js +51 -0
- package/dist/memory.d.ts +1 -0
- package/dist/memory.js +20 -0
- package/dist/observer.d.ts +9 -0
- package/dist/observer.js +80 -0
- package/dist/pipeline/complexity.d.ts +3 -0
- package/dist/pipeline/complexity.js +12 -0
- package/dist/pipeline/executor-registry.d.ts +3 -0
- package/dist/pipeline/executor-registry.js +20 -0
- package/dist/pipeline/hooks.d.ts +17 -0
- package/dist/pipeline/hooks.js +110 -0
- package/dist/pipeline/questions.d.ts +2 -0
- package/dist/pipeline/questions.js +44 -0
- package/dist/pipeline/runner-selection.d.ts +2 -0
- package/dist/pipeline/runner-selection.js +13 -0
- package/dist/pipeline/state.d.ts +4 -0
- package/dist/pipeline/state.js +37 -0
- package/dist/pipeline.d.ts +3 -0
- package/dist/pipeline.js +213 -0
- package/dist/preflight.d.ts +1 -0
- package/dist/preflight.js +69 -0
- package/dist/retrospective.d.ts +26 -0
- package/dist/retrospective.js +211 -0
- package/dist/stages/agent.d.ts +2 -0
- package/dist/stages/agent.js +94 -0
- package/dist/stages/gate.d.ts +2 -0
- package/dist/stages/gate.js +32 -0
- package/dist/stages/review.d.ts +2 -0
- package/dist/stages/review.js +32 -0
- package/dist/stages/ship.d.ts +3 -0
- package/dist/stages/ship.js +154 -0
- package/dist/stages/verify.d.ts +2 -0
- package/dist/stages/verify.js +94 -0
- package/dist/types.d.ts +61 -0
- package/dist/types.js +1 -0
- package/dist/validators.d.ts +8 -0
- package/dist/validators.js +42 -0
- package/dist/verify-runner.d.ts +11 -0
- package/dist/verify-runner.js +110 -0
- package/kody.config.schema.json +31 -0
- package/package.json +1 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { spawn, execFileSync } from "child_process";
|
|
2
|
+
const SIGKILL_GRACE_MS = 5000;
|
|
3
|
+
const STDERR_TAIL_CHARS = 500;
|
|
4
|
+
function writeStdin(child, prompt) {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
if (!child.stdin) {
|
|
7
|
+
resolve();
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
child.stdin.write(prompt, (err) => {
|
|
11
|
+
if (err)
|
|
12
|
+
reject(err);
|
|
13
|
+
else {
|
|
14
|
+
child.stdin.end();
|
|
15
|
+
resolve();
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function waitForProcess(child, timeout) {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const stdoutChunks = [];
|
|
23
|
+
const stderrChunks = [];
|
|
24
|
+
child.stdout?.on("data", (chunk) => stdoutChunks.push(chunk));
|
|
25
|
+
child.stderr?.on("data", (chunk) => stderrChunks.push(chunk));
|
|
26
|
+
const timer = setTimeout(() => {
|
|
27
|
+
child.kill("SIGTERM");
|
|
28
|
+
setTimeout(() => {
|
|
29
|
+
if (!child.killed)
|
|
30
|
+
child.kill("SIGKILL");
|
|
31
|
+
}, SIGKILL_GRACE_MS);
|
|
32
|
+
}, timeout);
|
|
33
|
+
child.on("exit", (code) => {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
resolve({
|
|
36
|
+
code,
|
|
37
|
+
stdout: Buffer.concat(stdoutChunks).toString(),
|
|
38
|
+
stderr: Buffer.concat(stderrChunks).toString(),
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
child.on("error", (err) => {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
resolve({ code: -1, stdout: "", stderr: err.message });
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async function runSubprocess(command, args, prompt, timeout, options) {
|
|
48
|
+
const child = spawn(command, args, {
|
|
49
|
+
cwd: options?.cwd ?? process.cwd(),
|
|
50
|
+
env: {
|
|
51
|
+
...process.env,
|
|
52
|
+
SKIP_BUILD: "1",
|
|
53
|
+
SKIP_HOOKS: "1",
|
|
54
|
+
...options?.env,
|
|
55
|
+
},
|
|
56
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
57
|
+
});
|
|
58
|
+
try {
|
|
59
|
+
await writeStdin(child, prompt);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
return {
|
|
63
|
+
outcome: "failed",
|
|
64
|
+
error: `Failed to send prompt: ${err instanceof Error ? err.message : String(err)}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const { code, stdout, stderr } = await waitForProcess(child, timeout);
|
|
68
|
+
if (code === 0) {
|
|
69
|
+
return { outcome: "completed", output: stdout };
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
outcome: code === null ? "timed_out" : "failed",
|
|
73
|
+
error: `Exit code ${code}\n${stderr.slice(-STDERR_TAIL_CHARS)}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function checkCommand(command, args) {
|
|
77
|
+
try {
|
|
78
|
+
execFileSync(command, args, { timeout: 10_000, stdio: "pipe" });
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// ─── Claude Code Runner ──────────────────────────────────────────────────────
|
|
86
|
+
export function createClaudeCodeRunner() {
|
|
87
|
+
return {
|
|
88
|
+
async run(_stageName, prompt, model, timeout, _taskDir, options) {
|
|
89
|
+
return runSubprocess("claude", [
|
|
90
|
+
"--print",
|
|
91
|
+
"--model", model,
|
|
92
|
+
"--dangerously-skip-permissions",
|
|
93
|
+
"--allowedTools", "Bash,Edit,Read,Write,Glob,Grep",
|
|
94
|
+
], prompt, timeout, options);
|
|
95
|
+
},
|
|
96
|
+
async healthCheck() {
|
|
97
|
+
return checkCommand("claude", ["--version"]);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// ─── Runner Factory ──────────────────────────────────────────────────────────
|
|
102
|
+
const RUNNER_FACTORIES = {
|
|
103
|
+
"claude-code": createClaudeCodeRunner,
|
|
104
|
+
};
|
|
105
|
+
export function createRunners(config) {
|
|
106
|
+
// New multi-runner config
|
|
107
|
+
if (config.agent.runners && Object.keys(config.agent.runners).length > 0) {
|
|
108
|
+
const runners = {};
|
|
109
|
+
for (const [name, runnerConfig] of Object.entries(config.agent.runners)) {
|
|
110
|
+
const factory = RUNNER_FACTORIES[runnerConfig.type];
|
|
111
|
+
if (factory) {
|
|
112
|
+
runners[name] = factory();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return runners;
|
|
116
|
+
}
|
|
117
|
+
// Legacy single-runner fallback
|
|
118
|
+
const runnerType = config.agent.runner ?? "claude-code";
|
|
119
|
+
const factory = RUNNER_FACTORIES[runnerType];
|
|
120
|
+
const defaultName = config.agent.defaultRunner ?? "claude";
|
|
121
|
+
return { [defaultName]: factory ? factory() : createClaudeCodeRunner() };
|
|
122
|
+
}
|
package/dist/bin/cli.js
CHANGED
|
@@ -180,8 +180,8 @@ var init_validators = __esm({
|
|
|
180
180
|
});
|
|
181
181
|
|
|
182
182
|
// src/config.ts
|
|
183
|
-
import * as
|
|
184
|
-
import * as
|
|
183
|
+
import * as fs7 from "fs";
|
|
184
|
+
import * as path6 from "path";
|
|
185
185
|
function resolveStageConfig(config, stageName, modelTier) {
|
|
186
186
|
const stageOverride = config.agent.stages?.[stageName];
|
|
187
187
|
if (stageOverride) return stageOverride;
|
|
@@ -223,10 +223,10 @@ function setConfigDir(dir) {
|
|
|
223
223
|
}
|
|
224
224
|
function getProjectConfig() {
|
|
225
225
|
if (_config) return _config;
|
|
226
|
-
const configPath =
|
|
227
|
-
if (
|
|
226
|
+
const configPath = path6.join(_configDir ?? process.cwd(), "kody.config.json");
|
|
227
|
+
if (fs7.existsSync(configPath)) {
|
|
228
228
|
try {
|
|
229
|
-
const result = parseJsonSafe(
|
|
229
|
+
const result = parseJsonSafe(fs7.readFileSync(configPath, "utf-8"));
|
|
230
230
|
if (!result.ok) {
|
|
231
231
|
logger.warn(`kody.config.json: ${result.error} \u2014 using defaults`);
|
|
232
232
|
_config = { ...DEFAULT_CONFIG };
|
|
@@ -283,7 +283,7 @@ var init_config = __esm({
|
|
|
283
283
|
repo: ""
|
|
284
284
|
},
|
|
285
285
|
agent: {
|
|
286
|
-
modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
|
|
286
|
+
modelMap: { cheap: "claude-haiku-4-5-20251001", mid: "claude-sonnet-4-6", strong: "claude-opus-4-6" }
|
|
287
287
|
},
|
|
288
288
|
contextTiers: {
|
|
289
289
|
enabled: true,
|
|
@@ -389,10 +389,11 @@ function createClaudeCodeRunner() {
|
|
|
389
389
|
model,
|
|
390
390
|
"--dangerously-skip-permissions"
|
|
391
391
|
];
|
|
392
|
+
const baseTools = "Bash,Edit,Read,Write,Glob,Grep";
|
|
392
393
|
if (options?.mcpConfigJson) {
|
|
393
394
|
args2.push("--mcp-config", options.mcpConfigJson);
|
|
394
395
|
} else {
|
|
395
|
-
args2.push("--allowedTools",
|
|
396
|
+
args2.push("--allowedTools", baseTools);
|
|
396
397
|
}
|
|
397
398
|
if (options?.sessionId) {
|
|
398
399
|
if (options.resumeSession) {
|
|
@@ -1192,6 +1193,7 @@ var init_litellm = __esm({
|
|
|
1192
1193
|
// src/cli/taskify-command.ts
|
|
1193
1194
|
var taskify_command_exports = {};
|
|
1194
1195
|
__export(taskify_command_exports, {
|
|
1196
|
+
TaskifyError: () => TaskifyError,
|
|
1195
1197
|
isTaskifyRun: () => isTaskifyRun,
|
|
1196
1198
|
readTaskifyMarker: () => readTaskifyMarker,
|
|
1197
1199
|
runTaskifyCommand: () => runTaskifyCommand,
|
|
@@ -1283,8 +1285,17 @@ async function runTaskifyCommand() {
|
|
|
1283
1285
|
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || "dummy"
|
|
1284
1286
|
};
|
|
1285
1287
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
+
try {
|
|
1289
|
+
await taskifyCommand({ ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId, runnerEnv });
|
|
1290
|
+
} catch (err) {
|
|
1291
|
+
if (err instanceof TaskifyError) {
|
|
1292
|
+
logger.error(`[taskify] ${err.message}`);
|
|
1293
|
+
process.exit(1);
|
|
1294
|
+
}
|
|
1295
|
+
throw err;
|
|
1296
|
+
} finally {
|
|
1297
|
+
litellmProcess?.kill();
|
|
1298
|
+
}
|
|
1288
1299
|
}
|
|
1289
1300
|
async function taskifyCommand(opts) {
|
|
1290
1301
|
const { ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId } = opts;
|
|
@@ -1299,7 +1310,6 @@ async function taskifyCommand(opts) {
|
|
|
1299
1310
|
mcpConfigJson = buildTaskifyMcpConfigJson(config);
|
|
1300
1311
|
} catch (err) {
|
|
1301
1312
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1302
|
-
logger.error(`[taskify] MCP config error: ${msg}`);
|
|
1303
1313
|
if (issueNumber && !local) {
|
|
1304
1314
|
postComment(
|
|
1305
1315
|
issueNumber,
|
|
@@ -1310,7 +1320,7 @@ async function taskifyCommand(opts) {
|
|
|
1310
1320
|
Add the required MCP server config to \`kody.config.json\` and try again.`
|
|
1311
1321
|
);
|
|
1312
1322
|
}
|
|
1313
|
-
|
|
1323
|
+
throw new TaskifyError(`MCP config error: ${msg}`);
|
|
1314
1324
|
}
|
|
1315
1325
|
}
|
|
1316
1326
|
const sc = resolveStageConfig(config, "taskify", "strong");
|
|
@@ -1358,40 +1368,37 @@ Kody is decomposing ${src} into tasks...`);
|
|
|
1358
1368
|
});
|
|
1359
1369
|
if (result.outcome !== "completed") {
|
|
1360
1370
|
const errMsg = result.outcome === "timed_out" ? "Taskify timed out after 5 minutes." : `Taskify failed: ${result.error}`;
|
|
1361
|
-
logger.error(`[taskify] ${errMsg}`);
|
|
1362
1371
|
if (issueNumber && !local) {
|
|
1363
1372
|
postComment(issueNumber, `Kody taskify failed:
|
|
1364
1373
|
|
|
1365
1374
|
> ${errMsg}`);
|
|
1366
1375
|
setLifecycleLabel(issueNumber, "failed");
|
|
1367
1376
|
}
|
|
1368
|
-
|
|
1377
|
+
throw new TaskifyError(errMsg);
|
|
1369
1378
|
}
|
|
1370
1379
|
const resultPath = path10.join(taskDir, RESULT_FILE);
|
|
1371
1380
|
if (!fs11.existsSync(resultPath)) {
|
|
1372
1381
|
const errMsg = `Claude did not write ${RESULT_FILE}. Output:
|
|
1373
1382
|
|
|
1374
1383
|
${result.output?.slice(0, 500) ?? "(none)"}`;
|
|
1375
|
-
logger.error(`[taskify] ${errMsg}`);
|
|
1376
1384
|
if (issueNumber && !local) {
|
|
1377
1385
|
postComment(issueNumber, `Kody taskify failed: result file not found.
|
|
1378
1386
|
|
|
1379
1387
|
${errMsg}`);
|
|
1380
1388
|
setLifecycleLabel(issueNumber, "failed");
|
|
1381
1389
|
}
|
|
1382
|
-
|
|
1390
|
+
throw new TaskifyError(errMsg);
|
|
1383
1391
|
}
|
|
1384
1392
|
let parsed;
|
|
1385
1393
|
try {
|
|
1386
1394
|
parsed = JSON.parse(fs11.readFileSync(resultPath, "utf-8"));
|
|
1387
1395
|
} catch {
|
|
1388
1396
|
const errMsg = `Could not parse ${RESULT_FILE} as JSON.`;
|
|
1389
|
-
logger.error(`[taskify] ${errMsg}`);
|
|
1390
1397
|
if (issueNumber && !local) {
|
|
1391
1398
|
postComment(issueNumber, `Kody taskify failed: ${errMsg}`);
|
|
1392
1399
|
setLifecycleLabel(issueNumber, "failed");
|
|
1393
1400
|
}
|
|
1394
|
-
|
|
1401
|
+
throw new TaskifyError(errMsg);
|
|
1395
1402
|
}
|
|
1396
1403
|
const sourceLabel = ticketId ?? (prdFile ? path10.basename(prdFile) : "spec");
|
|
1397
1404
|
if (parsed.status === "questions") {
|
|
@@ -1400,12 +1407,11 @@ ${errMsg}`);
|
|
|
1400
1407
|
await handleTasks(parsed, sourceLabel, issueNumber, local ?? false);
|
|
1401
1408
|
} else {
|
|
1402
1409
|
const errMsg = `Unexpected status in ${RESULT_FILE}: ${JSON.stringify(parsed)}`;
|
|
1403
|
-
logger.error(`[taskify] ${errMsg}`);
|
|
1404
1410
|
if (issueNumber && !local) {
|
|
1405
1411
|
postComment(issueNumber, `Kody taskify failed: ${errMsg}`);
|
|
1406
1412
|
setLifecycleLabel(issueNumber, "failed");
|
|
1407
1413
|
}
|
|
1408
|
-
|
|
1414
|
+
throw new TaskifyError(errMsg);
|
|
1409
1415
|
}
|
|
1410
1416
|
}
|
|
1411
1417
|
function handleQuestions(parsed, ticketId, issueNumber, local) {
|
|
@@ -1536,7 +1542,7 @@ function readTaskifyMarker(taskDir) {
|
|
|
1536
1542
|
return null;
|
|
1537
1543
|
}
|
|
1538
1544
|
}
|
|
1539
|
-
var __dirname, AUTO_TRIGGER_THRESHOLD, MAX_TASKS_GUARD, TASKIFY_TIMEOUT_MS, MARKER_FILE, RESULT_FILE;
|
|
1545
|
+
var __dirname, TaskifyError, AUTO_TRIGGER_THRESHOLD, MAX_TASKS_GUARD, TASKIFY_TIMEOUT_MS, MARKER_FILE, RESULT_FILE;
|
|
1540
1546
|
var init_taskify_command = __esm({
|
|
1541
1547
|
"src/cli/taskify-command.ts"() {
|
|
1542
1548
|
"use strict";
|
|
@@ -1548,6 +1554,12 @@ var init_taskify_command = __esm({
|
|
|
1548
1554
|
init_task_resolution();
|
|
1549
1555
|
init_litellm();
|
|
1550
1556
|
__dirname = path10.dirname(fileURLToPath(import.meta.url));
|
|
1557
|
+
TaskifyError = class extends Error {
|
|
1558
|
+
constructor(message) {
|
|
1559
|
+
super(message);
|
|
1560
|
+
this.name = "TaskifyError";
|
|
1561
|
+
}
|
|
1562
|
+
};
|
|
1551
1563
|
AUTO_TRIGGER_THRESHOLD = 5;
|
|
1552
1564
|
MAX_TASKS_GUARD = 20;
|
|
1553
1565
|
TASKIFY_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
@@ -2686,10 +2698,12 @@ function escalateModelTier(currentTier) {
|
|
|
2686
2698
|
function resolveModel(modelTier, stageName) {
|
|
2687
2699
|
const config = getProjectConfig();
|
|
2688
2700
|
const mapped = config.agent.modelMap[modelTier];
|
|
2689
|
-
if (mapped)
|
|
2690
|
-
|
|
2701
|
+
if (!mapped) {
|
|
2702
|
+
throw new Error(`No model configured for tier '${modelTier}'. Set agent.modelMap.${modelTier} in kody.config.json`);
|
|
2703
|
+
}
|
|
2704
|
+
return mapped;
|
|
2691
2705
|
}
|
|
2692
|
-
var MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT, UI_EXTENSIONS, UI_PATH_SEGMENTS, TIER_ESCALATION
|
|
2706
|
+
var MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT, UI_EXTENSIONS, UI_PATH_SEGMENTS, TIER_ESCALATION;
|
|
2693
2707
|
var init_context = __esm({
|
|
2694
2708
|
"src/context.ts"() {
|
|
2695
2709
|
"use strict";
|
|
@@ -2723,11 +2737,6 @@ var init_context = __esm({
|
|
|
2723
2737
|
mid: "strong",
|
|
2724
2738
|
strong: "strong"
|
|
2725
2739
|
};
|
|
2726
|
-
DEFAULT_MODEL_MAP = {
|
|
2727
|
-
cheap: "haiku",
|
|
2728
|
-
mid: "sonnet",
|
|
2729
|
-
strong: "opus"
|
|
2730
|
-
};
|
|
2731
2740
|
}
|
|
2732
2741
|
});
|
|
2733
2742
|
|
|
@@ -4912,7 +4921,7 @@ async function ensureLitellmProxy(config, projectDir) {
|
|
|
4912
4921
|
return litellmProcess;
|
|
4913
4922
|
}
|
|
4914
4923
|
async function runModelHealthCheck(config) {
|
|
4915
|
-
const usesProxy =
|
|
4924
|
+
const usesProxy = anyStageNeedsProxy(config);
|
|
4916
4925
|
const baseUrl = usesProxy ? getLitellmUrl() : "https://api.anthropic.com";
|
|
4917
4926
|
const apiKey = usesProxy ? process.env.ANTHROPIC_COMPATIBLE_API_KEY : process.env.ANTHROPIC_API_KEY;
|
|
4918
4927
|
if (!apiKey) {
|
|
@@ -5533,7 +5542,7 @@ function buildConfig(cwd, basic) {
|
|
|
5533
5542
|
github: { owner: basic.owner, repo: basic.repo },
|
|
5534
5543
|
agent: {
|
|
5535
5544
|
provider: "anthropic",
|
|
5536
|
-
modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
|
|
5545
|
+
modelMap: { cheap: "claude-haiku-4-5-20251001", mid: "claude-sonnet-4-6", strong: "claude-opus-4-6" }
|
|
5537
5546
|
}
|
|
5538
5547
|
};
|
|
5539
5548
|
const mcp = detectMcpConfig(cwd, basic.pm, pkg);
|
|
@@ -5733,8 +5742,8 @@ function initCommand(opts, pkgRoot) {
|
|
|
5733
5742
|
|
|
5734
5743
|
// src/bin/commands/bootstrap.ts
|
|
5735
5744
|
init_architecture_detection();
|
|
5736
|
-
import * as
|
|
5737
|
-
import * as
|
|
5745
|
+
import * as fs8 from "fs";
|
|
5746
|
+
import * as path7 from "path";
|
|
5738
5747
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
5739
5748
|
|
|
5740
5749
|
// src/bin/qa-guide.ts
|
|
@@ -6008,22 +6017,23 @@ function installSkillsForProject(cwd) {
|
|
|
6008
6017
|
}
|
|
6009
6018
|
|
|
6010
6019
|
// src/bin/commands/bootstrap.ts
|
|
6020
|
+
init_config();
|
|
6011
6021
|
var STEP_STAGES = ["taskify", "plan", "build", "autofix", "review", "review-fix"];
|
|
6012
6022
|
function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
|
|
6013
|
-
const srcDir =
|
|
6014
|
-
const baseDir =
|
|
6023
|
+
const srcDir = path7.join(cwd, "src");
|
|
6024
|
+
const baseDir = fs8.existsSync(srcDir) ? srcDir : cwd;
|
|
6015
6025
|
const results = [];
|
|
6016
6026
|
function walk(dir) {
|
|
6017
6027
|
const entries = [];
|
|
6018
6028
|
try {
|
|
6019
|
-
for (const entry of
|
|
6029
|
+
for (const entry of fs8.readdirSync(dir, { withFileTypes: true })) {
|
|
6020
6030
|
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
6021
|
-
const full =
|
|
6031
|
+
const full = path7.join(dir, entry.name);
|
|
6022
6032
|
if (entry.isDirectory()) {
|
|
6023
6033
|
entries.push(...walk(full));
|
|
6024
6034
|
} else if (/\.(ts|js)$/.test(entry.name) && !/\.(test|spec|config|d)\.(ts|js)$/.test(entry.name)) {
|
|
6025
6035
|
try {
|
|
6026
|
-
const stat =
|
|
6036
|
+
const stat = fs8.statSync(full);
|
|
6027
6037
|
if (stat.size >= 200 && stat.size <= 5e3) {
|
|
6028
6038
|
entries.push({ filePath: full, size: stat.size });
|
|
6029
6039
|
}
|
|
@@ -6037,8 +6047,8 @@ function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
|
|
|
6037
6047
|
}
|
|
6038
6048
|
const files = walk(baseDir).sort((a, b) => b.size - a.size).slice(0, maxFiles);
|
|
6039
6049
|
for (const { filePath } of files) {
|
|
6040
|
-
const rel =
|
|
6041
|
-
const content =
|
|
6050
|
+
const rel = path7.relative(cwd, filePath);
|
|
6051
|
+
const content = fs8.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
|
|
6042
6052
|
results.push(`### File: ${rel}
|
|
6043
6053
|
\`\`\`typescript
|
|
6044
6054
|
${content}
|
|
@@ -6050,9 +6060,9 @@ function ghComment(issueNumber, body, cwd) {
|
|
|
6050
6060
|
try {
|
|
6051
6061
|
let repoSlug = "";
|
|
6052
6062
|
try {
|
|
6053
|
-
const configPath =
|
|
6054
|
-
if (
|
|
6055
|
-
const config = JSON.parse(
|
|
6063
|
+
const configPath = path7.join(cwd, "kody.config.json");
|
|
6064
|
+
if (fs8.existsSync(configPath)) {
|
|
6065
|
+
const config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
|
|
6056
6066
|
if (config.github?.owner && config.github?.repo) {
|
|
6057
6067
|
repoSlug = `${config.github.owner}/${config.github.repo}`;
|
|
6058
6068
|
}
|
|
@@ -6079,7 +6089,9 @@ function ghComment(issueNumber, body, cwd) {
|
|
|
6079
6089
|
}
|
|
6080
6090
|
function bootstrapCommand(opts, pkgRoot) {
|
|
6081
6091
|
const cwd = process.cwd();
|
|
6092
|
+
setConfigDir(cwd);
|
|
6082
6093
|
const issueNumber = parseInt(process.env.ISSUE_NUMBER ?? "", 10) || 0;
|
|
6094
|
+
const bootstrapModel = resolveStageConfig(getProjectConfig(), "bootstrap", "cheap").model;
|
|
6083
6095
|
console.log(`
|
|
6084
6096
|
\u{1F527} Kody Bootstrap \u2014 Generating project memory + step files
|
|
6085
6097
|
`);
|
|
@@ -6087,8 +6099,8 @@ function bootstrapCommand(opts, pkgRoot) {
|
|
|
6087
6099
|
ghComment(issueNumber, "\u{1F527} **Bootstrap started** \u2014 analyzing project and generating configuration...", cwd);
|
|
6088
6100
|
}
|
|
6089
6101
|
const readIfExists = (rel, maxChars = 3e3) => {
|
|
6090
|
-
const p =
|
|
6091
|
-
if (
|
|
6102
|
+
const p = path7.join(cwd, rel);
|
|
6103
|
+
if (fs8.existsSync(p)) return fs8.readFileSync(p, "utf-8").slice(0, maxChars);
|
|
6092
6104
|
return null;
|
|
6093
6105
|
};
|
|
6094
6106
|
let repoContext = "";
|
|
@@ -6123,14 +6135,14 @@ ${sampleFiles}
|
|
|
6123
6135
|
|
|
6124
6136
|
`;
|
|
6125
6137
|
try {
|
|
6126
|
-
const topDirs =
|
|
6138
|
+
const topDirs = fs8.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
|
|
6127
6139
|
repoContext += `## Top-level directories
|
|
6128
6140
|
${topDirs.join(", ")}
|
|
6129
6141
|
|
|
6130
6142
|
`;
|
|
6131
|
-
const srcDir =
|
|
6132
|
-
if (
|
|
6133
|
-
const srcDirs =
|
|
6143
|
+
const srcDir = path7.join(cwd, "src");
|
|
6144
|
+
if (fs8.existsSync(srcDir)) {
|
|
6145
|
+
const srcDirs = fs8.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
6134
6146
|
if (srcDirs.length > 0) repoContext += `## src/ subdirectories
|
|
6135
6147
|
${srcDirs.join(", ")}
|
|
6136
6148
|
|
|
@@ -6140,19 +6152,19 @@ ${srcDirs.join(", ")}
|
|
|
6140
6152
|
}
|
|
6141
6153
|
const existingFiles = [];
|
|
6142
6154
|
for (const f of [".env.example", "CLAUDE.md", ".ai-docs", "vitest.config.ts", "vitest.config.mts", "jest.config.ts", "playwright.config.ts", ".eslintrc.js", "eslint.config.mjs", ".prettierrc"]) {
|
|
6143
|
-
if (
|
|
6155
|
+
if (fs8.existsSync(path7.join(cwd, f))) existingFiles.push(f);
|
|
6144
6156
|
}
|
|
6145
6157
|
if (existingFiles.length) repoContext += `## Config files present
|
|
6146
6158
|
${existingFiles.join(", ")}
|
|
6147
6159
|
|
|
6148
6160
|
`;
|
|
6149
6161
|
console.log("\u2500\u2500 Project Memory \u2500\u2500");
|
|
6150
|
-
const memoryDir =
|
|
6151
|
-
|
|
6152
|
-
const archPath =
|
|
6153
|
-
const conventionsPath =
|
|
6154
|
-
const existingArch =
|
|
6155
|
-
const existingConv =
|
|
6162
|
+
const memoryDir = path7.join(cwd, ".kody", "memory");
|
|
6163
|
+
fs8.mkdirSync(memoryDir, { recursive: true });
|
|
6164
|
+
const archPath = path7.join(memoryDir, "architecture.md");
|
|
6165
|
+
const conventionsPath = path7.join(memoryDir, "conventions.md");
|
|
6166
|
+
const existingArch = fs8.existsSync(archPath) ? fs8.readFileSync(archPath, "utf-8") : "";
|
|
6167
|
+
const existingConv = fs8.existsSync(conventionsPath) ? fs8.readFileSync(conventionsPath, "utf-8") : "";
|
|
6156
6168
|
const hasExisting = !!(existingArch || existingConv);
|
|
6157
6169
|
const extendInstruction = hasExisting && !opts.force ? `
|
|
6158
6170
|
## Existing Documentation (EXTEND, do not replace)
|
|
@@ -6196,7 +6208,7 @@ ${repoContext}`;
|
|
|
6196
6208
|
const output = execFileSync5("claude", [
|
|
6197
6209
|
"--print",
|
|
6198
6210
|
"--model",
|
|
6199
|
-
|
|
6211
|
+
bootstrapModel,
|
|
6200
6212
|
"--dangerously-skip-permissions",
|
|
6201
6213
|
memoryPrompt
|
|
6202
6214
|
], {
|
|
@@ -6208,12 +6220,12 @@ ${repoContext}`;
|
|
|
6208
6220
|
const cleaned = output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
6209
6221
|
const parsed = JSON.parse(cleaned);
|
|
6210
6222
|
if (parsed.architecture) {
|
|
6211
|
-
|
|
6223
|
+
fs8.writeFileSync(archPath, parsed.architecture);
|
|
6212
6224
|
const lineCount = parsed.architecture.split("\n").length;
|
|
6213
6225
|
console.log(` \u2713 .kody/memory/architecture.md (${lineCount} lines)`);
|
|
6214
6226
|
}
|
|
6215
6227
|
if (parsed.conventions) {
|
|
6216
|
-
|
|
6228
|
+
fs8.writeFileSync(conventionsPath, parsed.conventions);
|
|
6217
6229
|
const lineCount = parsed.conventions.split("\n").length;
|
|
6218
6230
|
console.log(` \u2713 .kody/memory/conventions.md (${lineCount} lines)`);
|
|
6219
6231
|
}
|
|
@@ -6222,39 +6234,39 @@ ${repoContext}`;
|
|
|
6222
6234
|
const detected = detectArchitectureBasic(cwd);
|
|
6223
6235
|
if (detected.length > 0) {
|
|
6224
6236
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6225
|
-
|
|
6237
|
+
fs8.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
|
|
6226
6238
|
|
|
6227
6239
|
## Overview
|
|
6228
6240
|
${detected.join("\n")}
|
|
6229
6241
|
`);
|
|
6230
6242
|
console.log(` \u2713 .kody/memory/architecture.md (${detected.length} items, basic detection)`);
|
|
6231
6243
|
}
|
|
6232
|
-
|
|
6244
|
+
fs8.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
|
|
6233
6245
|
console.log(" \u2713 .kody/memory/conventions.md (seed)");
|
|
6234
6246
|
}
|
|
6235
6247
|
console.log("\n\u2500\u2500 Step Files \u2500\u2500");
|
|
6236
|
-
const stepsDir =
|
|
6237
|
-
|
|
6238
|
-
const arch =
|
|
6239
|
-
const conv =
|
|
6248
|
+
const stepsDir = path7.join(cwd, ".kody", "steps");
|
|
6249
|
+
fs8.mkdirSync(stepsDir, { recursive: true });
|
|
6250
|
+
const arch = fs8.existsSync(archPath) ? fs8.readFileSync(archPath, "utf-8") : "";
|
|
6251
|
+
const conv = fs8.existsSync(conventionsPath) ? fs8.readFileSync(conventionsPath, "utf-8") : "";
|
|
6240
6252
|
console.log(" \u23F3 Customizing step files...");
|
|
6241
6253
|
let stepCount = 0;
|
|
6242
6254
|
for (const stage of STEP_STAGES) {
|
|
6243
|
-
const templatePath =
|
|
6244
|
-
if (!
|
|
6255
|
+
const templatePath = path7.join(pkgRoot, "prompts", `${stage}.md`);
|
|
6256
|
+
if (!fs8.existsSync(templatePath)) {
|
|
6245
6257
|
console.log(` \u2717 ${stage}.md \u2014 template not found in engine`);
|
|
6246
6258
|
continue;
|
|
6247
6259
|
}
|
|
6248
|
-
const stepOutputPath =
|
|
6249
|
-
if (
|
|
6260
|
+
const stepOutputPath = path7.join(stepsDir, `${stage}.md`);
|
|
6261
|
+
if (fs8.existsSync(stepOutputPath) && !opts.force) {
|
|
6250
6262
|
console.log(` \u25CB ${stage}.md \u2014 already exists (use --force to regenerate)`);
|
|
6251
6263
|
continue;
|
|
6252
6264
|
}
|
|
6253
|
-
const defaultPrompt =
|
|
6265
|
+
const defaultPrompt = fs8.readFileSync(templatePath, "utf-8");
|
|
6254
6266
|
const contextPlaceholder = "{{TASK_CONTEXT}}";
|
|
6255
6267
|
const placeholderIdx = defaultPrompt.indexOf(contextPlaceholder);
|
|
6256
6268
|
if (placeholderIdx === -1) {
|
|
6257
|
-
|
|
6269
|
+
fs8.copyFileSync(templatePath, stepOutputPath);
|
|
6258
6270
|
stepCount++;
|
|
6259
6271
|
console.log(` \u2713 ${stage}.md`);
|
|
6260
6272
|
continue;
|
|
@@ -6299,7 +6311,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
6299
6311
|
const output = execFileSync5("claude", [
|
|
6300
6312
|
"--print",
|
|
6301
6313
|
"--model",
|
|
6302
|
-
|
|
6314
|
+
bootstrapModel,
|
|
6303
6315
|
"--dangerously-skip-permissions",
|
|
6304
6316
|
customizationPrompt
|
|
6305
6317
|
], {
|
|
@@ -6311,23 +6323,23 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
6311
6323
|
let cleaned = output.replace(/^```(?:markdown|md)?\s*\n?/, "").replace(/\n?```\s*$/, "");
|
|
6312
6324
|
cleaned = cleaned.replace(/\n*\{\{TASK_CONTEXT\}\}\s*$/, "").trimEnd();
|
|
6313
6325
|
const finalPrompt = cleaned + "\n\n" + afterPlaceholder;
|
|
6314
|
-
|
|
6326
|
+
fs8.writeFileSync(stepOutputPath, finalPrompt);
|
|
6315
6327
|
stepCount++;
|
|
6316
6328
|
console.log(` \u2713 ${stage}.md`);
|
|
6317
6329
|
} catch {
|
|
6318
6330
|
console.log(` \u26A0 ${stage}.md \u2014 customization failed, using default template`);
|
|
6319
|
-
|
|
6331
|
+
fs8.copyFileSync(templatePath, stepOutputPath);
|
|
6320
6332
|
stepCount++;
|
|
6321
6333
|
}
|
|
6322
6334
|
}
|
|
6323
6335
|
console.log(` \u2713 Generated ${stepCount} step files in .kody/steps/`);
|
|
6324
6336
|
console.log("\n\u2500\u2500 QA Guide \u2500\u2500");
|
|
6325
|
-
const qaGuidePath =
|
|
6326
|
-
if (!
|
|
6337
|
+
const qaGuidePath = path7.join(cwd, ".kody", "qa-guide.md");
|
|
6338
|
+
if (!fs8.existsSync(qaGuidePath) || opts.force) {
|
|
6327
6339
|
const discovery = discoverQaContext(cwd);
|
|
6328
6340
|
if (discovery.routes.length > 0) {
|
|
6329
6341
|
const qaGuide = generateQaGuide(discovery);
|
|
6330
|
-
|
|
6342
|
+
fs8.writeFileSync(qaGuidePath, qaGuide);
|
|
6331
6343
|
console.log(` \u2713 .kody/qa-guide.md (${discovery.routes.length} routes, ${discovery.roles.length} roles)`);
|
|
6332
6344
|
if (discovery.loginPage) console.log(` \u2713 Login page detected: ${discovery.loginPage}`);
|
|
6333
6345
|
if (discovery.adminPath) console.log(` \u2713 Admin panel detected: ${discovery.adminPath}`);
|
|
@@ -6342,9 +6354,9 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
6342
6354
|
try {
|
|
6343
6355
|
let repoSlug = "";
|
|
6344
6356
|
try {
|
|
6345
|
-
const configPath =
|
|
6346
|
-
if (
|
|
6347
|
-
const config = JSON.parse(
|
|
6357
|
+
const configPath = path7.join(cwd, "kody.config.json");
|
|
6358
|
+
if (fs8.existsSync(configPath)) {
|
|
6359
|
+
const config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
|
|
6348
6360
|
if (config.github?.owner && config.github?.repo) {
|
|
6349
6361
|
repoSlug = `${config.github.owner}/${config.github.repo}`;
|
|
6350
6362
|
}
|
|
@@ -6417,19 +6429,19 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
6417
6429
|
".kody/memory/conventions.md",
|
|
6418
6430
|
".kody/qa-guide.md",
|
|
6419
6431
|
...installedSkillPaths
|
|
6420
|
-
].filter((f) =>
|
|
6421
|
-
if (
|
|
6432
|
+
].filter((f) => fs8.existsSync(path7.join(cwd, f)));
|
|
6433
|
+
if (fs8.existsSync(path7.join(cwd, "skills-lock.json"))) {
|
|
6422
6434
|
filesToCommit.push("skills-lock.json");
|
|
6423
6435
|
}
|
|
6424
6436
|
for (const stage of STEP_STAGES) {
|
|
6425
6437
|
const stepFile = `.kody/steps/${stage}.md`;
|
|
6426
|
-
if (
|
|
6438
|
+
if (fs8.existsSync(path7.join(cwd, stepFile))) {
|
|
6427
6439
|
filesToCommit.push(stepFile);
|
|
6428
6440
|
}
|
|
6429
6441
|
}
|
|
6430
6442
|
if (filesToCommit.length > 0) {
|
|
6431
6443
|
try {
|
|
6432
|
-
const fullPaths = filesToCommit.map((f) =>
|
|
6444
|
+
const fullPaths = filesToCommit.map((f) => path7.join(cwd, f));
|
|
6433
6445
|
for (let pass = 0; pass < 2; pass++) {
|
|
6434
6446
|
execFileSync5("npx", ["prettier", "--write", ...fullPaths], {
|
|
6435
6447
|
cwd,
|
|
@@ -6456,9 +6468,9 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
6456
6468
|
console.log(` \u2713 Pushed branch: ${branchName}`);
|
|
6457
6469
|
let baseBranch = "main";
|
|
6458
6470
|
try {
|
|
6459
|
-
const configPath =
|
|
6460
|
-
if (
|
|
6461
|
-
const config = JSON.parse(
|
|
6471
|
+
const configPath = path7.join(cwd, "kody.config.json");
|
|
6472
|
+
if (fs8.existsSync(configPath)) {
|
|
6473
|
+
const config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
|
|
6462
6474
|
baseBranch = config.git?.defaultBranch ?? "main";
|
|
6463
6475
|
}
|
|
6464
6476
|
} catch {
|