@kody-ade/kody-engine-lite 0.1.81 → 0.1.83
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/bin/cli.js +301 -279
- package/package.json +1 -1
package/dist/bin/cli.js
CHANGED
|
@@ -137,7 +137,7 @@ var init_agent_runner = __esm({
|
|
|
137
137
|
"src/agent-runner.ts"() {
|
|
138
138
|
"use strict";
|
|
139
139
|
SIGKILL_GRACE_MS = 5e3;
|
|
140
|
-
STDERR_TAIL_CHARS =
|
|
140
|
+
STDERR_TAIL_CHARS = 2e3;
|
|
141
141
|
RUNNER_FACTORIES = {
|
|
142
142
|
"claude-code": createClaudeCodeRunner
|
|
143
143
|
};
|
|
@@ -282,7 +282,11 @@ function getProjectConfig() {
|
|
|
282
282
|
quality: { ...DEFAULT_CONFIG.quality, ...raw.quality },
|
|
283
283
|
git: { ...DEFAULT_CONFIG.git, ...raw.git },
|
|
284
284
|
github: { ...DEFAULT_CONFIG.github, ...raw.github },
|
|
285
|
-
agent: {
|
|
285
|
+
agent: {
|
|
286
|
+
...DEFAULT_CONFIG.agent,
|
|
287
|
+
...raw.agent,
|
|
288
|
+
modelMap: { ...DEFAULT_CONFIG.agent.modelMap, ...raw.agent?.modelMap }
|
|
289
|
+
},
|
|
286
290
|
contextTiers: raw.contextTiers ? { ...DEFAULT_CONFIG.contextTiers, ...raw.contextTiers } : DEFAULT_CONFIG.contextTiers,
|
|
287
291
|
mcp: raw.mcp ? { enabled: false, servers: {}, stages: ["build", "verify", "review", "review-fix"], ...raw.mcp } : void 0
|
|
288
292
|
};
|
|
@@ -713,8 +717,10 @@ function submitPRReview(prNumber, body, event) {
|
|
|
713
717
|
{ input: body }
|
|
714
718
|
);
|
|
715
719
|
logger.info(` PR review submitted on #${prNumber}: ${event}`);
|
|
720
|
+
return true;
|
|
716
721
|
} catch (err) {
|
|
717
722
|
logger.warn(` Failed to submit PR review: ${err}`);
|
|
723
|
+
return false;
|
|
718
724
|
}
|
|
719
725
|
}
|
|
720
726
|
function getCIFailureLogs(runId, maxLength = 8e3) {
|
|
@@ -1868,8 +1874,8 @@ async function diagnoseFailure(stageName, errorOutput, modifiedFiles, runner, mo
|
|
|
1868
1874
|
`Stage: ${stageName}`,
|
|
1869
1875
|
``,
|
|
1870
1876
|
`Error output:`,
|
|
1871
|
-
errorOutput.slice(-
|
|
1872
|
-
// Last
|
|
1877
|
+
errorOutput.slice(-5e3),
|
|
1878
|
+
// Last 5000 chars of error for accurate diagnosis
|
|
1873
1879
|
``,
|
|
1874
1880
|
modifiedFiles.length > 0 ? `Files modified by build stage:
|
|
1875
1881
|
${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (build may not have run yet)."
|
|
@@ -1916,13 +1922,21 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
|
|
|
1916
1922
|
}
|
|
1917
1923
|
function getModifiedFiles(projectDir) {
|
|
1918
1924
|
try {
|
|
1919
|
-
const
|
|
1925
|
+
const staged = execFileSync5("git", ["diff", "--name-only", "--cached"], {
|
|
1926
|
+
encoding: "utf-8",
|
|
1927
|
+
cwd: projectDir,
|
|
1928
|
+
timeout: 5e3,
|
|
1929
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1930
|
+
}).trim();
|
|
1931
|
+
const unstaged = execFileSync5("git", ["diff", "--name-only"], {
|
|
1920
1932
|
encoding: "utf-8",
|
|
1921
1933
|
cwd: projectDir,
|
|
1922
1934
|
timeout: 5e3,
|
|
1923
1935
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1924
1936
|
}).trim();
|
|
1925
|
-
|
|
1937
|
+
const all = `${staged}
|
|
1938
|
+
${unstaged}`.split("\n").filter(Boolean);
|
|
1939
|
+
return [...new Set(all)];
|
|
1926
1940
|
} catch {
|
|
1927
1941
|
return [];
|
|
1928
1942
|
}
|
|
@@ -2122,55 +2136,198 @@ var init_verify = __esm({
|
|
|
2122
2136
|
}
|
|
2123
2137
|
});
|
|
2124
2138
|
|
|
2125
|
-
// src/
|
|
2139
|
+
// src/cli/task-resolution.ts
|
|
2126
2140
|
import * as fs9 from "fs";
|
|
2127
2141
|
import * as path9 from "path";
|
|
2142
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
2143
|
+
function findLatestTaskForIssue(issueNumber, projectDir) {
|
|
2144
|
+
const tasksDir = path9.join(projectDir, ".kody", "tasks");
|
|
2145
|
+
if (!fs9.existsSync(tasksDir)) return null;
|
|
2146
|
+
const allDirs = fs9.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
|
|
2147
|
+
const prefix = `${issueNumber}-`;
|
|
2148
|
+
const direct = allDirs.find((d) => d.startsWith(prefix));
|
|
2149
|
+
if (direct) return direct;
|
|
2150
|
+
try {
|
|
2151
|
+
const branch = execFileSync7("git", ["branch", "--show-current"], {
|
|
2152
|
+
encoding: "utf-8",
|
|
2153
|
+
cwd: projectDir,
|
|
2154
|
+
timeout: 5e3,
|
|
2155
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2156
|
+
}).trim();
|
|
2157
|
+
const branchIssueMatch = branch.match(/^(\d+)-/);
|
|
2158
|
+
if (branchIssueMatch) {
|
|
2159
|
+
const branchIssueNum = branchIssueMatch[1];
|
|
2160
|
+
const branchPrefix = `${branchIssueNum}-`;
|
|
2161
|
+
const fromBranch = allDirs.find((d) => d.startsWith(branchPrefix));
|
|
2162
|
+
if (fromBranch) return fromBranch;
|
|
2163
|
+
}
|
|
2164
|
+
} catch {
|
|
2165
|
+
}
|
|
2166
|
+
return null;
|
|
2167
|
+
}
|
|
2168
|
+
function generateTaskId() {
|
|
2169
|
+
const now = /* @__PURE__ */ new Date();
|
|
2170
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
2171
|
+
return `${String(now.getFullYear()).slice(2)}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
2172
|
+
}
|
|
2173
|
+
var init_task_resolution = __esm({
|
|
2174
|
+
"src/cli/task-resolution.ts"() {
|
|
2175
|
+
"use strict";
|
|
2176
|
+
}
|
|
2177
|
+
});
|
|
2178
|
+
|
|
2179
|
+
// src/review-standalone.ts
|
|
2180
|
+
import * as fs10 from "fs";
|
|
2181
|
+
import * as path10 from "path";
|
|
2182
|
+
function resolveReviewTarget(input) {
|
|
2183
|
+
if (input.prs.length === 0) {
|
|
2184
|
+
return {
|
|
2185
|
+
action: "none",
|
|
2186
|
+
message: `Issue #${input.issueNumber} has no open PRs. Nothing to review.`
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
if (input.prs.length === 1) {
|
|
2190
|
+
return { action: "review", prNumber: input.prs[0].number };
|
|
2191
|
+
}
|
|
2192
|
+
const prList = input.prs.map((pr) => ` - #${pr.number}: ${pr.title}`).join("\n");
|
|
2193
|
+
return {
|
|
2194
|
+
action: "pick",
|
|
2195
|
+
prs: input.prs,
|
|
2196
|
+
message: `\u26A0\uFE0F Issue #${input.issueNumber} has ${input.prs.length} open PRs:
|
|
2197
|
+
${prList}
|
|
2198
|
+
|
|
2199
|
+
Run: \`pnpm kody review --pr-number <n>\`
|
|
2200
|
+
Or comment on the specific PR: \`@kody review\``
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
async function runStandaloneReview(input) {
|
|
2204
|
+
const taskId = input.taskId ?? `review-${generateTaskId()}`;
|
|
2205
|
+
const taskDir = path10.join(input.projectDir, ".kody", "tasks", taskId);
|
|
2206
|
+
fs10.mkdirSync(taskDir, { recursive: true });
|
|
2207
|
+
const taskContent = `# ${input.prTitle}
|
|
2208
|
+
|
|
2209
|
+
${input.prBody ?? ""}`;
|
|
2210
|
+
fs10.writeFileSync(path10.join(taskDir, "task.md"), taskContent);
|
|
2211
|
+
const reviewDef = STAGES.find((s) => s.name === "review");
|
|
2212
|
+
const ctx = {
|
|
2213
|
+
taskId,
|
|
2214
|
+
taskDir,
|
|
2215
|
+
projectDir: input.projectDir,
|
|
2216
|
+
runners: input.runners,
|
|
2217
|
+
sessions: {},
|
|
2218
|
+
input: {
|
|
2219
|
+
mode: "full",
|
|
2220
|
+
local: input.local
|
|
2221
|
+
}
|
|
2222
|
+
};
|
|
2223
|
+
logger.info(`[review] standalone review for: ${input.prTitle}`);
|
|
2224
|
+
const result = await executeAgentStage(ctx, reviewDef);
|
|
2225
|
+
if (result.outcome !== "completed") {
|
|
2226
|
+
return {
|
|
2227
|
+
outcome: "failed",
|
|
2228
|
+
taskDir,
|
|
2229
|
+
error: result.error ?? "Review stage failed"
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
const reviewPath = path10.join(taskDir, "review.md");
|
|
2233
|
+
let reviewContent;
|
|
2234
|
+
if (fs10.existsSync(reviewPath)) {
|
|
2235
|
+
reviewContent = fs10.readFileSync(reviewPath, "utf-8");
|
|
2236
|
+
}
|
|
2237
|
+
return {
|
|
2238
|
+
outcome: "completed",
|
|
2239
|
+
reviewContent,
|
|
2240
|
+
taskDir
|
|
2241
|
+
};
|
|
2242
|
+
}
|
|
2243
|
+
function detectReviewVerdict(reviewContent) {
|
|
2244
|
+
const verdictMatch = reviewContent.match(/##\s*Verdict:\s*(PASS|FAIL)/i);
|
|
2245
|
+
if (verdictMatch) {
|
|
2246
|
+
return verdictMatch[1].toLowerCase();
|
|
2247
|
+
}
|
|
2248
|
+
const hasCritical = /###\s*Critical\s*\n(?!None\.)/i.test(reviewContent);
|
|
2249
|
+
const hasMajor = /###\s*Major\s*\n(?!None\.)/i.test(reviewContent);
|
|
2250
|
+
if (hasCritical || hasMajor) return "fail";
|
|
2251
|
+
return "pass";
|
|
2252
|
+
}
|
|
2253
|
+
function formatReviewComment(reviewContent, taskId) {
|
|
2254
|
+
const verdict = detectReviewVerdict(reviewContent);
|
|
2255
|
+
const cta = verdict === "fail" ? "\n\n> To fix these issues, comment: `@kody fix`\n> The review findings will be used automatically as context." : "";
|
|
2256
|
+
return `## \u{1F50D} Kody Review (\`${taskId}\`)
|
|
2257
|
+
|
|
2258
|
+
${reviewContent}${cta}
|
|
2259
|
+
|
|
2260
|
+
---
|
|
2261
|
+
\u{1F916} Generated by Kody`;
|
|
2262
|
+
}
|
|
2263
|
+
var init_review_standalone = __esm({
|
|
2264
|
+
"src/review-standalone.ts"() {
|
|
2265
|
+
"use strict";
|
|
2266
|
+
init_definitions();
|
|
2267
|
+
init_agent();
|
|
2268
|
+
init_task_resolution();
|
|
2269
|
+
init_logger();
|
|
2270
|
+
}
|
|
2271
|
+
});
|
|
2272
|
+
|
|
2273
|
+
// src/stages/review.ts
|
|
2274
|
+
import * as fs11 from "fs";
|
|
2275
|
+
import * as path11 from "path";
|
|
2128
2276
|
async function executeReviewWithFix(ctx, def) {
|
|
2129
2277
|
if (ctx.input.dryRun) {
|
|
2130
2278
|
return { outcome: "completed", retries: 0 };
|
|
2131
2279
|
}
|
|
2132
2280
|
const reviewDef = STAGES.find((s) => s.name === "review");
|
|
2133
2281
|
const reviewFixDef = STAGES.find((s) => s.name === "review-fix");
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2282
|
+
for (let iteration = 0; iteration <= MAX_REVIEW_FIX_ITERATIONS; iteration++) {
|
|
2283
|
+
const label = iteration === 0 ? "initial review" : `review after fix #${iteration}`;
|
|
2284
|
+
logger.info(` running ${label}...`);
|
|
2285
|
+
const reviewResult = await executeAgentStage(ctx, reviewDef);
|
|
2286
|
+
if (reviewResult.outcome !== "completed") {
|
|
2287
|
+
return reviewResult;
|
|
2288
|
+
}
|
|
2289
|
+
const reviewFile = path11.join(ctx.taskDir, "review.md");
|
|
2290
|
+
if (!fs11.existsSync(reviewFile)) {
|
|
2291
|
+
return { outcome: "failed", retries: iteration, error: "review.md not found" };
|
|
2292
|
+
}
|
|
2293
|
+
const content = fs11.readFileSync(reviewFile, "utf-8");
|
|
2294
|
+
if (detectReviewVerdict(content) !== "fail") {
|
|
2295
|
+
return { ...reviewResult, retries: iteration };
|
|
2296
|
+
}
|
|
2297
|
+
if (iteration === MAX_REVIEW_FIX_ITERATIONS) {
|
|
2298
|
+
logger.warn(` review still failing after ${MAX_REVIEW_FIX_ITERATIONS} fix attempts`);
|
|
2299
|
+
return { ...reviewResult, retries: iteration };
|
|
2300
|
+
}
|
|
2301
|
+
logger.info(` review found issues (iteration ${iteration + 1}/${MAX_REVIEW_FIX_ITERATIONS}), running review-fix...`);
|
|
2302
|
+
const fixResult = await executeAgentStage(ctx, reviewFixDef);
|
|
2303
|
+
if (fixResult.outcome !== "completed") {
|
|
2304
|
+
return fixResult;
|
|
2305
|
+
}
|
|
2151
2306
|
}
|
|
2152
|
-
|
|
2153
|
-
return executeAgentStage(ctx, reviewDef);
|
|
2307
|
+
return { outcome: "failed", retries: MAX_REVIEW_FIX_ITERATIONS, error: "Unexpected review-fix loop exit" };
|
|
2154
2308
|
}
|
|
2309
|
+
var MAX_REVIEW_FIX_ITERATIONS;
|
|
2155
2310
|
var init_review = __esm({
|
|
2156
2311
|
"src/stages/review.ts"() {
|
|
2157
2312
|
"use strict";
|
|
2158
2313
|
init_definitions();
|
|
2159
2314
|
init_logger();
|
|
2160
2315
|
init_agent();
|
|
2316
|
+
init_review_standalone();
|
|
2317
|
+
MAX_REVIEW_FIX_ITERATIONS = 2;
|
|
2161
2318
|
}
|
|
2162
2319
|
});
|
|
2163
2320
|
|
|
2164
2321
|
// src/stages/ship.ts
|
|
2165
|
-
import * as
|
|
2166
|
-
import * as
|
|
2167
|
-
import { execFileSync as
|
|
2322
|
+
import * as fs12 from "fs";
|
|
2323
|
+
import * as path12 from "path";
|
|
2324
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
2168
2325
|
function buildPrBody(ctx) {
|
|
2169
2326
|
const sections = [];
|
|
2170
|
-
const taskJsonPath =
|
|
2171
|
-
if (
|
|
2327
|
+
const taskJsonPath = path12.join(ctx.taskDir, "task.json");
|
|
2328
|
+
if (fs12.existsSync(taskJsonPath)) {
|
|
2172
2329
|
try {
|
|
2173
|
-
const raw =
|
|
2330
|
+
const raw = fs12.readFileSync(taskJsonPath, "utf-8");
|
|
2174
2331
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2175
2332
|
const task = JSON.parse(cleaned);
|
|
2176
2333
|
if (task.description) {
|
|
@@ -2189,9 +2346,9 @@ ${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
|
|
|
2189
2346
|
} catch {
|
|
2190
2347
|
}
|
|
2191
2348
|
}
|
|
2192
|
-
const reviewPath =
|
|
2193
|
-
if (
|
|
2194
|
-
const review =
|
|
2349
|
+
const reviewPath = path12.join(ctx.taskDir, "review.md");
|
|
2350
|
+
if (fs12.existsSync(reviewPath)) {
|
|
2351
|
+
const review = fs12.readFileSync(reviewPath, "utf-8");
|
|
2195
2352
|
const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
2196
2353
|
if (summaryMatch) {
|
|
2197
2354
|
const summary = summaryMatch[1].trim();
|
|
@@ -2208,14 +2365,14 @@ ${summary}`);
|
|
|
2208
2365
|
**Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
|
|
2209
2366
|
}
|
|
2210
2367
|
}
|
|
2211
|
-
const verifyPath =
|
|
2212
|
-
if (
|
|
2213
|
-
const verify =
|
|
2368
|
+
const verifyPath = path12.join(ctx.taskDir, "verify.md");
|
|
2369
|
+
if (fs12.existsSync(verifyPath)) {
|
|
2370
|
+
const verify = fs12.readFileSync(verifyPath, "utf-8");
|
|
2214
2371
|
if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
|
|
2215
2372
|
}
|
|
2216
|
-
const planPath =
|
|
2217
|
-
if (
|
|
2218
|
-
const plan =
|
|
2373
|
+
const planPath = path12.join(ctx.taskDir, "plan.md");
|
|
2374
|
+
if (fs12.existsSync(planPath)) {
|
|
2375
|
+
const plan = fs12.readFileSync(planPath, "utf-8").trim();
|
|
2219
2376
|
if (plan) {
|
|
2220
2377
|
const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
|
|
2221
2378
|
sections.push(`
|
|
@@ -2235,25 +2392,25 @@ Closes #${ctx.input.issueNumber}`);
|
|
|
2235
2392
|
return sections.join("\n");
|
|
2236
2393
|
}
|
|
2237
2394
|
function executeShipStage(ctx, _def) {
|
|
2238
|
-
const shipPath =
|
|
2395
|
+
const shipPath = path12.join(ctx.taskDir, "ship.md");
|
|
2239
2396
|
if (ctx.input.dryRun) {
|
|
2240
|
-
|
|
2397
|
+
fs12.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
|
|
2241
2398
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
2242
2399
|
}
|
|
2243
2400
|
if (ctx.input.local && !ctx.input.issueNumber) {
|
|
2244
|
-
|
|
2401
|
+
fs12.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
|
|
2245
2402
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
2246
2403
|
}
|
|
2247
2404
|
try {
|
|
2248
2405
|
const head = getCurrentBranch(ctx.projectDir);
|
|
2249
2406
|
const base = getDefaultBranch(ctx.projectDir);
|
|
2250
2407
|
try {
|
|
2251
|
-
|
|
2408
|
+
execFileSync8("git", ["add", ctx.taskDir], {
|
|
2252
2409
|
cwd: ctx.projectDir,
|
|
2253
2410
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
2254
2411
|
stdio: "pipe"
|
|
2255
2412
|
});
|
|
2256
|
-
|
|
2413
|
+
execFileSync8("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
|
|
2257
2414
|
cwd: ctx.projectDir,
|
|
2258
2415
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
2259
2416
|
stdio: "pipe"
|
|
@@ -2267,7 +2424,7 @@ function executeShipStage(ctx, _def) {
|
|
|
2267
2424
|
let repo = config.github?.repo;
|
|
2268
2425
|
if (!owner || !repo) {
|
|
2269
2426
|
try {
|
|
2270
|
-
const remoteUrl =
|
|
2427
|
+
const remoteUrl = execFileSync8("git", ["remote", "get-url", "origin"], {
|
|
2271
2428
|
encoding: "utf-8",
|
|
2272
2429
|
cwd: ctx.projectDir
|
|
2273
2430
|
}).trim();
|
|
@@ -2287,10 +2444,10 @@ function executeShipStage(ctx, _def) {
|
|
|
2287
2444
|
docs: "docs",
|
|
2288
2445
|
chore: "chore"
|
|
2289
2446
|
};
|
|
2290
|
-
const taskJsonPath =
|
|
2291
|
-
if (
|
|
2447
|
+
const taskJsonPath = path12.join(ctx.taskDir, "task.json");
|
|
2448
|
+
if (fs12.existsSync(taskJsonPath)) {
|
|
2292
2449
|
try {
|
|
2293
|
-
const raw =
|
|
2450
|
+
const raw = fs12.readFileSync(taskJsonPath, "utf-8");
|
|
2294
2451
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2295
2452
|
const task = JSON.parse(cleaned);
|
|
2296
2453
|
const prefix = TYPE_PREFIX[task.task_type] ?? "chore";
|
|
@@ -2300,9 +2457,9 @@ function executeShipStage(ctx, _def) {
|
|
|
2300
2457
|
}
|
|
2301
2458
|
}
|
|
2302
2459
|
if (title === "Update") {
|
|
2303
|
-
const taskMdPath =
|
|
2304
|
-
if (
|
|
2305
|
-
const content =
|
|
2460
|
+
const taskMdPath = path12.join(ctx.taskDir, "task.md");
|
|
2461
|
+
if (fs12.existsSync(taskMdPath)) {
|
|
2462
|
+
const content = fs12.readFileSync(taskMdPath, "utf-8");
|
|
2306
2463
|
const firstLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith("*"));
|
|
2307
2464
|
if (firstLine) title = `chore: ${firstLine.trim()}`.slice(0, 72);
|
|
2308
2465
|
}
|
|
@@ -2322,7 +2479,7 @@ function executeShipStage(ctx, _def) {
|
|
|
2322
2479
|
} catch {
|
|
2323
2480
|
}
|
|
2324
2481
|
}
|
|
2325
|
-
|
|
2482
|
+
fs12.writeFileSync(shipPath, `# Ship
|
|
2326
2483
|
|
|
2327
2484
|
Updated existing PR: ${existingPr.url}
|
|
2328
2485
|
PR #${existingPr.number}
|
|
@@ -2343,19 +2500,19 @@ PR #${existingPr.number}
|
|
|
2343
2500
|
} catch {
|
|
2344
2501
|
}
|
|
2345
2502
|
}
|
|
2346
|
-
|
|
2503
|
+
fs12.writeFileSync(shipPath, `# Ship
|
|
2347
2504
|
|
|
2348
2505
|
PR created: ${pr.url}
|
|
2349
2506
|
PR #${pr.number}
|
|
2350
2507
|
`);
|
|
2351
2508
|
} else {
|
|
2352
|
-
|
|
2509
|
+
fs12.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
|
|
2353
2510
|
}
|
|
2354
2511
|
}
|
|
2355
2512
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
2356
2513
|
} catch (err) {
|
|
2357
2514
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2358
|
-
|
|
2515
|
+
fs12.writeFileSync(shipPath, `# Ship
|
|
2359
2516
|
|
|
2360
2517
|
Failed: ${msg}
|
|
2361
2518
|
`);
|
|
@@ -2401,15 +2558,15 @@ var init_executor_registry = __esm({
|
|
|
2401
2558
|
});
|
|
2402
2559
|
|
|
2403
2560
|
// src/pipeline/questions.ts
|
|
2404
|
-
import * as
|
|
2405
|
-
import * as
|
|
2561
|
+
import * as fs13 from "fs";
|
|
2562
|
+
import * as path13 from "path";
|
|
2406
2563
|
function checkForQuestions(ctx, stageName) {
|
|
2407
2564
|
if (ctx.input.local || !ctx.input.issueNumber) return false;
|
|
2408
2565
|
try {
|
|
2409
2566
|
if (stageName === "taskify") {
|
|
2410
|
-
const taskJsonPath =
|
|
2411
|
-
if (!
|
|
2412
|
-
const raw =
|
|
2567
|
+
const taskJsonPath = path13.join(ctx.taskDir, "task.json");
|
|
2568
|
+
if (!fs13.existsSync(taskJsonPath)) return false;
|
|
2569
|
+
const raw = fs13.readFileSync(taskJsonPath, "utf-8");
|
|
2413
2570
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2414
2571
|
const taskJson = JSON.parse(cleaned);
|
|
2415
2572
|
if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
|
|
@@ -2424,9 +2581,9 @@ Reply with \`@kody approve\` and your answers in the comment body.`;
|
|
|
2424
2581
|
}
|
|
2425
2582
|
}
|
|
2426
2583
|
if (stageName === "plan") {
|
|
2427
|
-
const planPath =
|
|
2428
|
-
if (!
|
|
2429
|
-
const plan =
|
|
2584
|
+
const planPath = path13.join(ctx.taskDir, "plan.md");
|
|
2585
|
+
if (!fs13.existsSync(planPath)) return false;
|
|
2586
|
+
const plan = fs13.readFileSync(planPath, "utf-8");
|
|
2430
2587
|
const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
2431
2588
|
if (questionsMatch) {
|
|
2432
2589
|
const questionsText = questionsMatch[1].trim();
|
|
@@ -2455,8 +2612,8 @@ var init_questions = __esm({
|
|
|
2455
2612
|
});
|
|
2456
2613
|
|
|
2457
2614
|
// src/pipeline/hooks.ts
|
|
2458
|
-
import * as
|
|
2459
|
-
import * as
|
|
2615
|
+
import * as fs14 from "fs";
|
|
2616
|
+
import * as path14 from "path";
|
|
2460
2617
|
function applyPreStageLabel(ctx, def) {
|
|
2461
2618
|
if (!ctx.input.issueNumber || ctx.input.local) return;
|
|
2462
2619
|
if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
|
|
@@ -2482,9 +2639,9 @@ function autoDetectComplexity(ctx, def) {
|
|
|
2482
2639
|
if (def.name !== "taskify") return null;
|
|
2483
2640
|
if (ctx.input.complexity) return null;
|
|
2484
2641
|
try {
|
|
2485
|
-
const taskJsonPath =
|
|
2486
|
-
if (!
|
|
2487
|
-
const raw =
|
|
2642
|
+
const taskJsonPath = path14.join(ctx.taskDir, "task.json");
|
|
2643
|
+
if (!fs14.existsSync(taskJsonPath)) return null;
|
|
2644
|
+
const raw = fs14.readFileSync(taskJsonPath, "utf-8");
|
|
2488
2645
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2489
2646
|
const taskJson = JSON.parse(cleaned);
|
|
2490
2647
|
if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
|
|
@@ -2514,8 +2671,8 @@ function checkRiskGate(ctx, def, state, complexity) {
|
|
|
2514
2671
|
if (ctx.input.dryRun || ctx.input.local) return null;
|
|
2515
2672
|
if (ctx.input.mode === "rerun") return null;
|
|
2516
2673
|
if (!ctx.input.issueNumber) return null;
|
|
2517
|
-
const planPath =
|
|
2518
|
-
const plan =
|
|
2674
|
+
const planPath = path14.join(ctx.taskDir, "plan.md");
|
|
2675
|
+
const plan = fs14.existsSync(planPath) ? fs14.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
|
|
2519
2676
|
try {
|
|
2520
2677
|
postComment(
|
|
2521
2678
|
ctx.input.issueNumber,
|
|
@@ -2582,22 +2739,22 @@ var init_hooks = __esm({
|
|
|
2582
2739
|
});
|
|
2583
2740
|
|
|
2584
2741
|
// src/learning/auto-learn.ts
|
|
2585
|
-
import * as
|
|
2586
|
-
import * as
|
|
2742
|
+
import * as fs15 from "fs";
|
|
2743
|
+
import * as path15 from "path";
|
|
2587
2744
|
function stripAnsi(str) {
|
|
2588
2745
|
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
2589
2746
|
}
|
|
2590
2747
|
function autoLearn(ctx) {
|
|
2591
2748
|
try {
|
|
2592
|
-
const memoryDir =
|
|
2593
|
-
if (!
|
|
2594
|
-
|
|
2749
|
+
const memoryDir = path15.join(ctx.projectDir, ".kody", "memory");
|
|
2750
|
+
if (!fs15.existsSync(memoryDir)) {
|
|
2751
|
+
fs15.mkdirSync(memoryDir, { recursive: true });
|
|
2595
2752
|
}
|
|
2596
2753
|
const learnings = [];
|
|
2597
2754
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2598
|
-
const verifyPath =
|
|
2599
|
-
if (
|
|
2600
|
-
const verify = stripAnsi(
|
|
2755
|
+
const verifyPath = path15.join(ctx.taskDir, "verify.md");
|
|
2756
|
+
if (fs15.existsSync(verifyPath)) {
|
|
2757
|
+
const verify = stripAnsi(fs15.readFileSync(verifyPath, "utf-8"));
|
|
2601
2758
|
if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
|
|
2602
2759
|
if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
|
|
2603
2760
|
if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
|
|
@@ -2606,18 +2763,18 @@ function autoLearn(ctx) {
|
|
|
2606
2763
|
if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
|
|
2607
2764
|
if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
|
|
2608
2765
|
}
|
|
2609
|
-
const reviewPath =
|
|
2610
|
-
if (
|
|
2611
|
-
const review =
|
|
2766
|
+
const reviewPath = path15.join(ctx.taskDir, "review.md");
|
|
2767
|
+
if (fs15.existsSync(reviewPath)) {
|
|
2768
|
+
const review = fs15.readFileSync(reviewPath, "utf-8");
|
|
2612
2769
|
if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
|
|
2613
2770
|
if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
|
|
2614
2771
|
if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
|
|
2615
2772
|
if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
|
|
2616
2773
|
}
|
|
2617
|
-
const taskJsonPath =
|
|
2618
|
-
if (
|
|
2774
|
+
const taskJsonPath = path15.join(ctx.taskDir, "task.json");
|
|
2775
|
+
if (fs15.existsSync(taskJsonPath)) {
|
|
2619
2776
|
try {
|
|
2620
|
-
const raw = stripAnsi(
|
|
2777
|
+
const raw = stripAnsi(fs15.readFileSync(taskJsonPath, "utf-8"));
|
|
2621
2778
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2622
2779
|
const task = JSON.parse(cleaned);
|
|
2623
2780
|
if (task.scope && Array.isArray(task.scope)) {
|
|
@@ -2628,13 +2785,13 @@ function autoLearn(ctx) {
|
|
|
2628
2785
|
}
|
|
2629
2786
|
}
|
|
2630
2787
|
if (learnings.length > 0) {
|
|
2631
|
-
const conventionsPath =
|
|
2788
|
+
const conventionsPath = path15.join(memoryDir, "conventions.md");
|
|
2632
2789
|
const entry = `
|
|
2633
2790
|
## Learned ${timestamp2} (task: ${ctx.taskId})
|
|
2634
2791
|
${learnings.join("\n")}
|
|
2635
2792
|
`;
|
|
2636
|
-
|
|
2637
|
-
invalidateCache(conventionsPath,
|
|
2793
|
+
fs15.appendFileSync(conventionsPath, entry);
|
|
2794
|
+
invalidateCache(conventionsPath, path15.join(memoryDir, ".tiers"));
|
|
2638
2795
|
logger.info(`Auto-learned ${learnings.length} convention(s)`);
|
|
2639
2796
|
}
|
|
2640
2797
|
autoLearnDecisions(ctx.taskDir, memoryDir, ctx.taskId, timestamp2);
|
|
@@ -2643,13 +2800,13 @@ ${learnings.join("\n")}
|
|
|
2643
2800
|
}
|
|
2644
2801
|
}
|
|
2645
2802
|
function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
2646
|
-
const archPath =
|
|
2647
|
-
if (
|
|
2803
|
+
const archPath = path15.join(memoryDir, "architecture.md");
|
|
2804
|
+
if (fs15.existsSync(archPath)) return;
|
|
2648
2805
|
const detected = [];
|
|
2649
|
-
const pkgPath =
|
|
2650
|
-
if (
|
|
2806
|
+
const pkgPath = path15.join(projectDir, "package.json");
|
|
2807
|
+
if (fs15.existsSync(pkgPath)) {
|
|
2651
2808
|
try {
|
|
2652
|
-
const pkg = JSON.parse(
|
|
2809
|
+
const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf-8"));
|
|
2653
2810
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2654
2811
|
if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
|
|
2655
2812
|
else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
|
|
@@ -2665,15 +2822,15 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
|
2665
2822
|
if (allDeps.payload || allDeps["@payloadcms/next"]) detected.push(`- CMS: Payload CMS`);
|
|
2666
2823
|
if (pkg.type === "module") detected.push("- Module system: ESM");
|
|
2667
2824
|
else detected.push("- Module system: CommonJS");
|
|
2668
|
-
if (
|
|
2669
|
-
else if (
|
|
2670
|
-
else if (
|
|
2825
|
+
if (fs15.existsSync(path15.join(projectDir, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
|
|
2826
|
+
else if (fs15.existsSync(path15.join(projectDir, "yarn.lock"))) detected.push("- Package manager: yarn");
|
|
2827
|
+
else if (fs15.existsSync(path15.join(projectDir, "package-lock.json"))) detected.push("- Package manager: npm");
|
|
2671
2828
|
} catch {
|
|
2672
2829
|
}
|
|
2673
2830
|
}
|
|
2674
2831
|
const topDirs = [];
|
|
2675
2832
|
try {
|
|
2676
|
-
const entries =
|
|
2833
|
+
const entries = fs15.readdirSync(projectDir, { withFileTypes: true });
|
|
2677
2834
|
for (const entry of entries) {
|
|
2678
2835
|
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
2679
2836
|
topDirs.push(entry.name);
|
|
@@ -2682,10 +2839,10 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
|
2682
2839
|
if (topDirs.length > 0) detected.push(`- Top-level directories: ${topDirs.join(", ")}`);
|
|
2683
2840
|
} catch {
|
|
2684
2841
|
}
|
|
2685
|
-
const srcDir =
|
|
2686
|
-
if (
|
|
2842
|
+
const srcDir = path15.join(projectDir, "src");
|
|
2843
|
+
if (fs15.existsSync(srcDir)) {
|
|
2687
2844
|
try {
|
|
2688
|
-
const srcEntries =
|
|
2845
|
+
const srcEntries = fs15.readdirSync(srcDir, { withFileTypes: true });
|
|
2689
2846
|
const srcDirs = srcEntries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
2690
2847
|
if (srcDirs.length > 0) detected.push(`- src/ structure: ${srcDirs.join(", ")}`);
|
|
2691
2848
|
} catch {
|
|
@@ -2697,15 +2854,15 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
|
2697
2854
|
## Overview
|
|
2698
2855
|
${detected.join("\n")}
|
|
2699
2856
|
`;
|
|
2700
|
-
|
|
2701
|
-
invalidateCache(archPath,
|
|
2857
|
+
fs15.writeFileSync(archPath, content);
|
|
2858
|
+
invalidateCache(archPath, path15.join(memoryDir, ".tiers"));
|
|
2702
2859
|
logger.info(`Auto-detected architecture (${detected.length} items)`);
|
|
2703
2860
|
}
|
|
2704
2861
|
}
|
|
2705
2862
|
function autoLearnDecisions(taskDir, memoryDir, taskId, timestamp2) {
|
|
2706
|
-
const reviewPath =
|
|
2707
|
-
if (!
|
|
2708
|
-
const review =
|
|
2863
|
+
const reviewPath = path15.join(taskDir, "review.md");
|
|
2864
|
+
if (!fs15.existsSync(reviewPath)) return;
|
|
2865
|
+
const review = fs15.readFileSync(reviewPath, "utf-8");
|
|
2709
2866
|
const decisions = [];
|
|
2710
2867
|
const existingPatternRe = /(?:use|follow|reuse|match|adopt)\s+(?:the\s+)?existing\s+(.+?)(?:\.|$)/gim;
|
|
2711
2868
|
for (const match of review.matchAll(existingPatternRe)) {
|
|
@@ -2724,10 +2881,10 @@ function autoLearnDecisions(taskDir, memoryDir, taskId, timestamp2) {
|
|
|
2724
2881
|
decisions.push(`- Avoid ${match[1].trim()} for ${match[2].trim()}`);
|
|
2725
2882
|
}
|
|
2726
2883
|
if (decisions.length === 0) return;
|
|
2727
|
-
const decisionsPath =
|
|
2884
|
+
const decisionsPath = path15.join(memoryDir, "decisions.md");
|
|
2728
2885
|
let existing = "";
|
|
2729
|
-
if (
|
|
2730
|
-
existing =
|
|
2886
|
+
if (fs15.existsSync(decisionsPath)) {
|
|
2887
|
+
existing = fs15.readFileSync(decisionsPath, "utf-8");
|
|
2731
2888
|
} else {
|
|
2732
2889
|
existing = "# Architectural Decisions\n\nDecisions extracted from code reviews. The planning agent MUST follow these.\n";
|
|
2733
2890
|
}
|
|
@@ -2737,8 +2894,8 @@ function autoLearnDecisions(taskDir, memoryDir, taskId, timestamp2) {
|
|
|
2737
2894
|
## From task ${taskId} (${timestamp2})
|
|
2738
2895
|
${newDecisions.join("\n")}
|
|
2739
2896
|
`;
|
|
2740
|
-
|
|
2741
|
-
invalidateCache(decisionsPath,
|
|
2897
|
+
fs15.appendFileSync(decisionsPath, existing ? entry : existing + entry);
|
|
2898
|
+
invalidateCache(decisionsPath, path15.join(memoryDir, ".tiers"));
|
|
2742
2899
|
logger.info(`Auto-learned ${newDecisions.length} architectural decision(s)`);
|
|
2743
2900
|
}
|
|
2744
2901
|
var init_auto_learn = __esm({
|
|
@@ -2750,13 +2907,13 @@ var init_auto_learn = __esm({
|
|
|
2750
2907
|
});
|
|
2751
2908
|
|
|
2752
2909
|
// src/retrospective.ts
|
|
2753
|
-
import * as
|
|
2754
|
-
import * as
|
|
2910
|
+
import * as fs16 from "fs";
|
|
2911
|
+
import * as path16 from "path";
|
|
2755
2912
|
function readArtifact(taskDir, filename, maxChars) {
|
|
2756
|
-
const p =
|
|
2757
|
-
if (!
|
|
2913
|
+
const p = path16.join(taskDir, filename);
|
|
2914
|
+
if (!fs16.existsSync(p)) return null;
|
|
2758
2915
|
try {
|
|
2759
|
-
const content =
|
|
2916
|
+
const content = fs16.readFileSync(p, "utf-8");
|
|
2760
2917
|
return content.length > maxChars ? content.slice(0, maxChars) + "\n...(truncated)" : content;
|
|
2761
2918
|
} catch {
|
|
2762
2919
|
return null;
|
|
@@ -2809,13 +2966,13 @@ function collectRunContext(ctx, state, pipelineStartTime) {
|
|
|
2809
2966
|
return lines.join("\n");
|
|
2810
2967
|
}
|
|
2811
2968
|
function getLogPath(projectDir) {
|
|
2812
|
-
return
|
|
2969
|
+
return path16.join(projectDir, ".kody", "memory", "observer-log.jsonl");
|
|
2813
2970
|
}
|
|
2814
2971
|
function readPreviousRetrospectives(projectDir, limit = 10) {
|
|
2815
2972
|
const logPath = getLogPath(projectDir);
|
|
2816
|
-
if (!
|
|
2973
|
+
if (!fs16.existsSync(logPath)) return [];
|
|
2817
2974
|
try {
|
|
2818
|
-
const content =
|
|
2975
|
+
const content = fs16.readFileSync(logPath, "utf-8");
|
|
2819
2976
|
const lines = content.split("\n").filter(Boolean);
|
|
2820
2977
|
const entries = [];
|
|
2821
2978
|
const start = Math.max(0, lines.length - limit);
|
|
@@ -2842,11 +2999,11 @@ function formatPreviousEntries(entries) {
|
|
|
2842
2999
|
}
|
|
2843
3000
|
function appendRetrospectiveEntry(projectDir, entry) {
|
|
2844
3001
|
const logPath = getLogPath(projectDir);
|
|
2845
|
-
const dir =
|
|
2846
|
-
if (!
|
|
2847
|
-
|
|
3002
|
+
const dir = path16.dirname(logPath);
|
|
3003
|
+
if (!fs16.existsSync(dir)) {
|
|
3004
|
+
fs16.mkdirSync(dir, { recursive: true });
|
|
2848
3005
|
}
|
|
2849
|
-
|
|
3006
|
+
fs16.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
2850
3007
|
}
|
|
2851
3008
|
async function runRetrospective(ctx, state, pipelineStartTime) {
|
|
2852
3009
|
if (ctx.input.dryRun) return;
|
|
@@ -2957,8 +3114,8 @@ If no pipeline flaw is detected, set "pipelineFlaw" to null.
|
|
|
2957
3114
|
});
|
|
2958
3115
|
|
|
2959
3116
|
// src/pipeline.ts
|
|
2960
|
-
import * as
|
|
2961
|
-
import * as
|
|
3117
|
+
import * as fs17 from "fs";
|
|
3118
|
+
import * as path17 from "path";
|
|
2962
3119
|
function ensureFeatureBranchIfNeeded(ctx) {
|
|
2963
3120
|
if (ctx.input.dryRun) return;
|
|
2964
3121
|
if (ctx.input.prNumber) {
|
|
@@ -2971,8 +3128,8 @@ function ensureFeatureBranchIfNeeded(ctx) {
|
|
|
2971
3128
|
}
|
|
2972
3129
|
if (!ctx.input.issueNumber) return;
|
|
2973
3130
|
try {
|
|
2974
|
-
const taskMdPath =
|
|
2975
|
-
const title =
|
|
3131
|
+
const taskMdPath = path17.join(ctx.taskDir, "task.md");
|
|
3132
|
+
const title = fs17.existsSync(taskMdPath) ? fs17.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
|
|
2976
3133
|
ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
|
|
2977
3134
|
syncWithDefault(ctx.projectDir);
|
|
2978
3135
|
} catch (err) {
|
|
@@ -2980,10 +3137,10 @@ function ensureFeatureBranchIfNeeded(ctx) {
|
|
|
2980
3137
|
}
|
|
2981
3138
|
}
|
|
2982
3139
|
function acquireLock(taskDir) {
|
|
2983
|
-
const lockPath =
|
|
2984
|
-
if (
|
|
3140
|
+
const lockPath = path17.join(taskDir, ".lock");
|
|
3141
|
+
if (fs17.existsSync(lockPath)) {
|
|
2985
3142
|
try {
|
|
2986
|
-
const pid = parseInt(
|
|
3143
|
+
const pid = parseInt(fs17.readFileSync(lockPath, "utf-8").trim(), 10);
|
|
2987
3144
|
try {
|
|
2988
3145
|
process.kill(pid, 0);
|
|
2989
3146
|
throw new Error(`Pipeline already running (PID ${pid})`);
|
|
@@ -2994,11 +3151,11 @@ function acquireLock(taskDir) {
|
|
|
2994
3151
|
if (e instanceof Error && e.message.startsWith("Pipeline already")) throw e;
|
|
2995
3152
|
}
|
|
2996
3153
|
}
|
|
2997
|
-
|
|
3154
|
+
fs17.writeFileSync(lockPath, String(process.pid));
|
|
2998
3155
|
}
|
|
2999
3156
|
function releaseLock(taskDir) {
|
|
3000
3157
|
try {
|
|
3001
|
-
|
|
3158
|
+
fs17.unlinkSync(path17.join(taskDir, ".lock"));
|
|
3002
3159
|
} catch {
|
|
3003
3160
|
}
|
|
3004
3161
|
}
|
|
@@ -3166,8 +3323,8 @@ var init_pipeline = __esm({
|
|
|
3166
3323
|
});
|
|
3167
3324
|
|
|
3168
3325
|
// src/preflight.ts
|
|
3169
|
-
import { execFileSync as
|
|
3170
|
-
import * as
|
|
3326
|
+
import { execFileSync as execFileSync9 } from "child_process";
|
|
3327
|
+
import * as fs18 from "fs";
|
|
3171
3328
|
function check(name, fn) {
|
|
3172
3329
|
try {
|
|
3173
3330
|
const detail = fn() ?? void 0;
|
|
@@ -3179,7 +3336,7 @@ function check(name, fn) {
|
|
|
3179
3336
|
function runPreflight() {
|
|
3180
3337
|
const checks = [
|
|
3181
3338
|
check("claude CLI", () => {
|
|
3182
|
-
const v =
|
|
3339
|
+
const v = execFileSync9("claude", ["--version"], {
|
|
3183
3340
|
encoding: "utf-8",
|
|
3184
3341
|
timeout: 1e4,
|
|
3185
3342
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3187,14 +3344,14 @@ function runPreflight() {
|
|
|
3187
3344
|
return v;
|
|
3188
3345
|
}),
|
|
3189
3346
|
check("git repo", () => {
|
|
3190
|
-
|
|
3347
|
+
execFileSync9("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
3191
3348
|
encoding: "utf-8",
|
|
3192
3349
|
timeout: 5e3,
|
|
3193
3350
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3194
3351
|
});
|
|
3195
3352
|
}),
|
|
3196
3353
|
check("pnpm", () => {
|
|
3197
|
-
const v =
|
|
3354
|
+
const v = execFileSync9("pnpm", ["--version"], {
|
|
3198
3355
|
encoding: "utf-8",
|
|
3199
3356
|
timeout: 5e3,
|
|
3200
3357
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3202,7 +3359,7 @@ function runPreflight() {
|
|
|
3202
3359
|
return v;
|
|
3203
3360
|
}),
|
|
3204
3361
|
check("node >= 18", () => {
|
|
3205
|
-
const v =
|
|
3362
|
+
const v = execFileSync9("node", ["--version"], {
|
|
3206
3363
|
encoding: "utf-8",
|
|
3207
3364
|
timeout: 5e3,
|
|
3208
3365
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3212,7 +3369,7 @@ function runPreflight() {
|
|
|
3212
3369
|
return v;
|
|
3213
3370
|
}),
|
|
3214
3371
|
check("gh CLI", () => {
|
|
3215
|
-
const v =
|
|
3372
|
+
const v = execFileSync9("gh", ["--version"], {
|
|
3216
3373
|
encoding: "utf-8",
|
|
3217
3374
|
timeout: 5e3,
|
|
3218
3375
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3220,7 +3377,7 @@ function runPreflight() {
|
|
|
3220
3377
|
return v;
|
|
3221
3378
|
}),
|
|
3222
3379
|
check("package.json", () => {
|
|
3223
|
-
if (!
|
|
3380
|
+
if (!fs18.existsSync("package.json")) throw new Error("not found");
|
|
3224
3381
|
})
|
|
3225
3382
|
];
|
|
3226
3383
|
const failed = checks.filter((c) => !c.ok);
|
|
@@ -3240,140 +3397,6 @@ var init_preflight = __esm({
|
|
|
3240
3397
|
}
|
|
3241
3398
|
});
|
|
3242
3399
|
|
|
3243
|
-
// src/cli/task-resolution.ts
|
|
3244
|
-
import * as fs17 from "fs";
|
|
3245
|
-
import * as path16 from "path";
|
|
3246
|
-
import { execFileSync as execFileSync9 } from "child_process";
|
|
3247
|
-
function findLatestTaskForIssue(issueNumber, projectDir) {
|
|
3248
|
-
const tasksDir = path16.join(projectDir, ".kody", "tasks");
|
|
3249
|
-
if (!fs17.existsSync(tasksDir)) return null;
|
|
3250
|
-
const allDirs = fs17.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
|
|
3251
|
-
const prefix = `${issueNumber}-`;
|
|
3252
|
-
const direct = allDirs.find((d) => d.startsWith(prefix));
|
|
3253
|
-
if (direct) return direct;
|
|
3254
|
-
try {
|
|
3255
|
-
const branch = execFileSync9("git", ["branch", "--show-current"], {
|
|
3256
|
-
encoding: "utf-8",
|
|
3257
|
-
cwd: projectDir,
|
|
3258
|
-
timeout: 5e3,
|
|
3259
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
3260
|
-
}).trim();
|
|
3261
|
-
const branchIssueMatch = branch.match(/^(\d+)-/);
|
|
3262
|
-
if (branchIssueMatch) {
|
|
3263
|
-
const branchIssueNum = branchIssueMatch[1];
|
|
3264
|
-
const branchPrefix = `${branchIssueNum}-`;
|
|
3265
|
-
const fromBranch = allDirs.find((d) => d.startsWith(branchPrefix));
|
|
3266
|
-
if (fromBranch) return fromBranch;
|
|
3267
|
-
}
|
|
3268
|
-
} catch {
|
|
3269
|
-
}
|
|
3270
|
-
return null;
|
|
3271
|
-
}
|
|
3272
|
-
function generateTaskId() {
|
|
3273
|
-
const now = /* @__PURE__ */ new Date();
|
|
3274
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
3275
|
-
return `${String(now.getFullYear()).slice(2)}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
3276
|
-
}
|
|
3277
|
-
var init_task_resolution = __esm({
|
|
3278
|
-
"src/cli/task-resolution.ts"() {
|
|
3279
|
-
"use strict";
|
|
3280
|
-
}
|
|
3281
|
-
});
|
|
3282
|
-
|
|
3283
|
-
// src/review-standalone.ts
|
|
3284
|
-
import * as fs18 from "fs";
|
|
3285
|
-
import * as path17 from "path";
|
|
3286
|
-
function resolveReviewTarget(input) {
|
|
3287
|
-
if (input.prs.length === 0) {
|
|
3288
|
-
return {
|
|
3289
|
-
action: "none",
|
|
3290
|
-
message: `Issue #${input.issueNumber} has no open PRs. Nothing to review.`
|
|
3291
|
-
};
|
|
3292
|
-
}
|
|
3293
|
-
if (input.prs.length === 1) {
|
|
3294
|
-
return { action: "review", prNumber: input.prs[0].number };
|
|
3295
|
-
}
|
|
3296
|
-
const prList = input.prs.map((pr) => ` - #${pr.number}: ${pr.title}`).join("\n");
|
|
3297
|
-
return {
|
|
3298
|
-
action: "pick",
|
|
3299
|
-
prs: input.prs,
|
|
3300
|
-
message: `\u26A0\uFE0F Issue #${input.issueNumber} has ${input.prs.length} open PRs:
|
|
3301
|
-
${prList}
|
|
3302
|
-
|
|
3303
|
-
Run: \`pnpm kody review --pr-number <n>\`
|
|
3304
|
-
Or comment on the specific PR: \`@kody review\``
|
|
3305
|
-
};
|
|
3306
|
-
}
|
|
3307
|
-
async function runStandaloneReview(input) {
|
|
3308
|
-
const taskId = input.taskId ?? `review-${generateTaskId()}`;
|
|
3309
|
-
const taskDir = path17.join(input.projectDir, ".kody", "tasks", taskId);
|
|
3310
|
-
fs18.mkdirSync(taskDir, { recursive: true });
|
|
3311
|
-
const taskContent = `# ${input.prTitle}
|
|
3312
|
-
|
|
3313
|
-
${input.prBody ?? ""}`;
|
|
3314
|
-
fs18.writeFileSync(path17.join(taskDir, "task.md"), taskContent);
|
|
3315
|
-
const reviewDef = STAGES.find((s) => s.name === "review");
|
|
3316
|
-
const ctx = {
|
|
3317
|
-
taskId,
|
|
3318
|
-
taskDir,
|
|
3319
|
-
projectDir: input.projectDir,
|
|
3320
|
-
runners: input.runners,
|
|
3321
|
-
sessions: {},
|
|
3322
|
-
input: {
|
|
3323
|
-
mode: "full",
|
|
3324
|
-
local: input.local
|
|
3325
|
-
}
|
|
3326
|
-
};
|
|
3327
|
-
logger.info(`[review] standalone review for: ${input.prTitle}`);
|
|
3328
|
-
const result = await executeAgentStage(ctx, reviewDef);
|
|
3329
|
-
if (result.outcome !== "completed") {
|
|
3330
|
-
return {
|
|
3331
|
-
outcome: "failed",
|
|
3332
|
-
taskDir,
|
|
3333
|
-
error: result.error ?? "Review stage failed"
|
|
3334
|
-
};
|
|
3335
|
-
}
|
|
3336
|
-
const reviewPath = path17.join(taskDir, "review.md");
|
|
3337
|
-
let reviewContent;
|
|
3338
|
-
if (fs18.existsSync(reviewPath)) {
|
|
3339
|
-
reviewContent = fs18.readFileSync(reviewPath, "utf-8");
|
|
3340
|
-
}
|
|
3341
|
-
return {
|
|
3342
|
-
outcome: "completed",
|
|
3343
|
-
reviewContent,
|
|
3344
|
-
taskDir
|
|
3345
|
-
};
|
|
3346
|
-
}
|
|
3347
|
-
function detectReviewVerdict(reviewContent) {
|
|
3348
|
-
const verdictMatch = reviewContent.match(/##\s*Verdict:\s*(PASS|FAIL)/i);
|
|
3349
|
-
if (verdictMatch) {
|
|
3350
|
-
return verdictMatch[1].toLowerCase();
|
|
3351
|
-
}
|
|
3352
|
-
const hasCritical = /###\s*Critical\s*\n(?!None\.)/i.test(reviewContent);
|
|
3353
|
-
const hasMajor = /###\s*Major\s*\n(?!None\.)/i.test(reviewContent);
|
|
3354
|
-
if (hasCritical || hasMajor) return "fail";
|
|
3355
|
-
return "pass";
|
|
3356
|
-
}
|
|
3357
|
-
function formatReviewComment(reviewContent, taskId) {
|
|
3358
|
-
const verdict = detectReviewVerdict(reviewContent);
|
|
3359
|
-
const cta = verdict === "fail" ? "\n\n> To fix these issues, comment: `@kody fix`\n> The review findings will be used automatically as context." : "";
|
|
3360
|
-
return `## \u{1F50D} Kody Review (\`${taskId}\`)
|
|
3361
|
-
|
|
3362
|
-
${reviewContent}${cta}
|
|
3363
|
-
|
|
3364
|
-
---
|
|
3365
|
-
\u{1F916} Generated by Kody`;
|
|
3366
|
-
}
|
|
3367
|
-
var init_review_standalone = __esm({
|
|
3368
|
-
"src/review-standalone.ts"() {
|
|
3369
|
-
"use strict";
|
|
3370
|
-
init_definitions();
|
|
3371
|
-
init_agent();
|
|
3372
|
-
init_task_resolution();
|
|
3373
|
-
init_logger();
|
|
3374
|
-
}
|
|
3375
|
-
});
|
|
3376
|
-
|
|
3377
3400
|
// src/cli/args.ts
|
|
3378
3401
|
function getArg(args2, flag) {
|
|
3379
3402
|
const idx = args2.indexOf(flag);
|
|
@@ -3938,12 +3961,11 @@ async function main() {
|
|
|
3938
3961
|
console.log(result.reviewContent);
|
|
3939
3962
|
if (!input.local && prNumber) {
|
|
3940
3963
|
const comment = formatReviewComment(result.reviewContent, taskId);
|
|
3941
|
-
postPRComment(prNumber, comment);
|
|
3942
3964
|
const verdict = detectReviewVerdict(result.reviewContent);
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3965
|
+
const event = verdict === "fail" ? "request-changes" : "approve";
|
|
3966
|
+
const posted = submitPRReview(prNumber, comment, event);
|
|
3967
|
+
if (!posted) {
|
|
3968
|
+
postPRComment(prNumber, comment);
|
|
3947
3969
|
}
|
|
3948
3970
|
}
|
|
3949
3971
|
}
|