@kody-ade/kody-engine 0.2.7 → 0.2.9

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/kody2.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@kody-ade/kody-engine",
6
- version: "0.2.7",
6
+ version: "0.2.9",
7
7
  description: "kody2 \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
8
8
  license: "MIT",
9
9
  type: "module",
@@ -108,6 +108,7 @@ function loadConfig(projectDir = process.cwd()) {
108
108
  },
109
109
  issueContext: parseIssueContext(raw.issueContext),
110
110
  testRequirements: parseTestRequirements(raw.testRequirements),
111
+ defaultExecutable: typeof raw.defaultExecutable === "string" && raw.defaultExecutable.length > 0 ? raw.defaultExecutable : void 0,
111
112
  release: parseReleaseConfig(raw.release)
112
113
  };
113
114
  }
@@ -1962,10 +1963,201 @@ var loadCoverageRules = async (ctx) => {
1962
1963
  ctx.data.coverageRules = ctx.config.testRequirements ?? [];
1963
1964
  };
1964
1965
 
1966
+ // src/state.ts
1967
+ import { execFileSync as execFileSync10 } from "child_process";
1968
+ var STATE_BEGIN = "<!-- kody2:state:v1:begin -->";
1969
+ var STATE_END = "<!-- kody2:state:v1:end -->";
1970
+ var HISTORY_MAX_ENTRIES = 20;
1971
+ var API_TIMEOUT_MS2 = 3e4;
1972
+ function emptyState() {
1973
+ return {
1974
+ schemaVersion: 1,
1975
+ core: {
1976
+ phase: "idle",
1977
+ status: "pending",
1978
+ currentExecutable: null,
1979
+ lastOutcome: null,
1980
+ attempts: {}
1981
+ },
1982
+ executables: {},
1983
+ history: []
1984
+ };
1985
+ }
1986
+ function ghToken3() {
1987
+ return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
1988
+ }
1989
+ function gh3(args, input, cwd) {
1990
+ const token = ghToken3();
1991
+ const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
1992
+ return execFileSync10("gh", args, {
1993
+ encoding: "utf-8",
1994
+ timeout: API_TIMEOUT_MS2,
1995
+ cwd,
1996
+ env,
1997
+ input,
1998
+ stdio: input ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"]
1999
+ }).trim();
2000
+ }
2001
+ function findStateComment(target, number, cwd) {
2002
+ const apiPath = target === "issue" ? `repos/{owner}/{repo}/issues/${number}/comments` : `repos/{owner}/{repo}/issues/${number}/comments`;
2003
+ try {
2004
+ const raw = gh3(["api", "--paginate", apiPath], void 0, cwd);
2005
+ const list = JSON.parse(raw);
2006
+ for (const c of list) {
2007
+ if (c.body?.includes(STATE_BEGIN)) {
2008
+ return { id: String(c.id), body: c.body };
2009
+ }
2010
+ }
2011
+ } catch {
2012
+ }
2013
+ return null;
2014
+ }
2015
+ function parseStateComment(body) {
2016
+ const beginIdx = body.indexOf(STATE_BEGIN);
2017
+ const endIdx = body.indexOf(STATE_END, beginIdx + 1);
2018
+ if (beginIdx < 0 || endIdx < 0) return emptyState();
2019
+ const between = body.slice(beginIdx + STATE_BEGIN.length, endIdx);
2020
+ const fenceMatch = between.match(/```json\s*([\s\S]*?)\s*```/);
2021
+ if (!fenceMatch) return emptyState();
2022
+ try {
2023
+ const parsed = JSON.parse(fenceMatch[1]);
2024
+ if (parsed?.schemaVersion !== 1) return emptyState();
2025
+ return {
2026
+ schemaVersion: 1,
2027
+ core: { ...emptyState().core, ...parsed.core },
2028
+ executables: parsed.executables ?? {},
2029
+ history: Array.isArray(parsed.history) ? parsed.history : []
2030
+ };
2031
+ } catch {
2032
+ return emptyState();
2033
+ }
2034
+ }
2035
+ function reduce(state, executable, action) {
2036
+ if (!action) return state;
2037
+ const newAttempts = { ...state.core.attempts, [executable]: (state.core.attempts[executable] ?? 0) + 1 };
2038
+ const newExecutables = {
2039
+ ...state.executables,
2040
+ [executable]: { ...state.executables[executable] ?? { lastAction: null }, lastAction: action }
2041
+ };
2042
+ const newHistory = [
2043
+ ...state.history,
2044
+ { timestamp: action.timestamp, executable, action: action.type, note: noteFromAction(action) }
2045
+ ].slice(-HISTORY_MAX_ENTRIES);
2046
+ return {
2047
+ schemaVersion: 1,
2048
+ core: {
2049
+ ...state.core,
2050
+ attempts: newAttempts,
2051
+ lastOutcome: action,
2052
+ currentExecutable: executable,
2053
+ status: statusFromAction(action),
2054
+ phase: phaseFromAction(executable, action)
2055
+ },
2056
+ executables: newExecutables,
2057
+ history: newHistory
2058
+ };
2059
+ }
2060
+ function statusFromAction(action) {
2061
+ if (/FAILED$|ERROR$|MISSING$|REJECTED$/i.test(action.type)) return "failed";
2062
+ if (/COMPLETED$|SHIPPED$|MERGED$|SUCCESS$/i.test(action.type)) return "succeeded";
2063
+ return "running";
2064
+ }
2065
+ function phaseFromAction(executable, action) {
2066
+ if (/FAILED$|ERROR$|REJECTED$/i.test(action.type)) return "failed";
2067
+ if (executable === "build") return statusFromAction(action) === "succeeded" ? "implementing" : "implementing";
2068
+ if (executable === "review") return "reviewing";
2069
+ if (executable === "release") return "shipped";
2070
+ return "idle";
2071
+ }
2072
+ function noteFromAction(action) {
2073
+ const p = action.payload;
2074
+ if (typeof p?.prUrl === "string") return p.prUrl;
2075
+ if (typeof p?.reason === "string") return p.reason.slice(0, 120);
2076
+ if (typeof p?.commitMessage === "string") return p.commitMessage.slice(0, 120);
2077
+ return void 0;
2078
+ }
2079
+ function renderStateComment(state) {
2080
+ const lines = [];
2081
+ lines.push(STATE_BEGIN);
2082
+ lines.push("");
2083
+ lines.push("```json");
2084
+ lines.push(JSON.stringify(
2085
+ { schemaVersion: state.schemaVersion, core: state.core, executables: state.executables, history: state.history },
2086
+ null,
2087
+ 2
2088
+ ));
2089
+ lines.push("```");
2090
+ lines.push("");
2091
+ lines.push(STATE_END);
2092
+ lines.push("");
2093
+ lines.push("## kody2 task state");
2094
+ lines.push("");
2095
+ lines.push(`- **Phase:** \`${state.core.phase}\` **Status:** \`${state.core.status}\``);
2096
+ if (state.core.currentExecutable) {
2097
+ lines.push(`- **Last executable:** \`${state.core.currentExecutable}\``);
2098
+ }
2099
+ if (state.core.lastOutcome) {
2100
+ lines.push(`- **Last action:** \`${state.core.lastOutcome.type}\``);
2101
+ }
2102
+ const attempts = Object.entries(state.core.attempts).map(([k, v]) => `${k}:${v}`).join(", ");
2103
+ if (attempts) lines.push(`- **Attempts:** ${attempts}`);
2104
+ if (state.core.prUrl) lines.push(`- **PR:** ${state.core.prUrl}`);
2105
+ if (state.core.runUrl) lines.push(`- **Run:** ${state.core.runUrl}`);
2106
+ lines.push("");
2107
+ if (state.history.length > 0) {
2108
+ lines.push("### Recent history");
2109
+ lines.push("");
2110
+ const recent = state.history.slice(-10).reverse();
2111
+ for (const h of recent) {
2112
+ const note = h.note ? ` \u2014 ${h.note}` : "";
2113
+ lines.push(`- \`${h.timestamp}\` **${h.executable}** \u2192 \`${h.action}\`${note}`);
2114
+ }
2115
+ lines.push("");
2116
+ }
2117
+ return lines.join("\n");
2118
+ }
2119
+ function readTaskState(target, number, cwd) {
2120
+ const existing = findStateComment(target, number, cwd);
2121
+ return existing ? parseStateComment(existing.body) : emptyState();
2122
+ }
2123
+ function writeTaskState(target, number, state, cwd) {
2124
+ const body = renderStateComment(state);
2125
+ const existing = findStateComment(target, number, cwd);
2126
+ try {
2127
+ if (existing) {
2128
+ gh3(
2129
+ ["api", `repos/{owner}/{repo}/issues/comments/${existing.id}`, "-X", "PATCH", "-F", "body=@-"],
2130
+ body,
2131
+ cwd
2132
+ );
2133
+ } else {
2134
+ const sub = target === "issue" ? "issue" : "pr";
2135
+ gh3([sub, "comment", String(number), "--body-file", "-"], body, cwd);
2136
+ }
2137
+ } catch (err) {
2138
+ process.stderr.write(
2139
+ `[kody2 state] failed to write state on ${target} #${number}: ${err instanceof Error ? err.message : String(err)}
2140
+ `
2141
+ );
2142
+ }
2143
+ }
2144
+
2145
+ // src/scripts/loadTaskState.ts
2146
+ var loadTaskState = async (ctx) => {
2147
+ const target = ctx.data.commentTargetType;
2148
+ const number = ctx.data.commentTargetNumber;
2149
+ if (!target || !number) {
2150
+ ctx.data.taskState = emptyState();
2151
+ return;
2152
+ }
2153
+ ctx.data.taskState = readTaskState(target, number, ctx.cwd);
2154
+ };
2155
+
1965
2156
  // src/scripts/parseAgentResult.ts
1966
- var parseAgentResult2 = async (ctx, _profile, agentResult) => {
2157
+ var parseAgentResult2 = async (ctx, profile, agentResult) => {
1967
2158
  if (!agentResult) {
1968
2159
  ctx.data.agentDone = false;
2160
+ ctx.data.action = makeAction("AGENT_NOT_RUN", { reason: "no agent result" });
1969
2161
  return;
1970
2162
  }
1971
2163
  const parsed = parseAgentResult(agentResult.finalText);
@@ -1975,7 +2167,21 @@ var parseAgentResult2 = async (ctx, _profile, agentResult) => {
1975
2167
  ctx.data.agentFailureReason = parsed.failureReason;
1976
2168
  ctx.data.agentOutcome = agentResult.outcome;
1977
2169
  ctx.data.agentError = agentResult.error;
2170
+ const modeSeg = (ctx.args.mode ?? profile.name).replace(/-/g, "_").toUpperCase();
2171
+ if (parsed.done) {
2172
+ ctx.data.action = makeAction(`${modeSeg}_COMPLETED`, {
2173
+ commitMessage: parsed.commitMessage,
2174
+ prSummary: parsed.prSummary
2175
+ });
2176
+ } else {
2177
+ ctx.data.action = makeAction(`${modeSeg}_FAILED`, {
2178
+ reason: parsed.failureReason || agentResult.error || "unknown failure"
2179
+ });
2180
+ }
1978
2181
  };
2182
+ function makeAction(type, payload) {
2183
+ return { type, payload, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
2184
+ }
1979
2185
 
1980
2186
  // src/scripts/postIssueComment.ts
1981
2187
  var postIssueComment2 = async (ctx) => {
@@ -2079,7 +2285,7 @@ REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.
2079
2285
  };
2080
2286
 
2081
2287
  // src/scripts/releaseFlow.ts
2082
- import { execFileSync as execFileSync10, spawnSync } from "child_process";
2288
+ import { execFileSync as execFileSync11, spawnSync } from "child_process";
2083
2289
  import * as fs11 from "fs";
2084
2290
  import * as path10 from "path";
2085
2291
  function bumpVersion(current, bump) {
@@ -2109,7 +2315,7 @@ function generateChangelog(cwd, newVersion, lastTag) {
2109
2315
  const range = lastTag ? `${lastTag}..HEAD` : "HEAD";
2110
2316
  let log = "";
2111
2317
  try {
2112
- log = execFileSync10("git", ["log", range, "--pretty=format:%s||%h", "--no-merges"], {
2318
+ log = execFileSync11("git", ["log", range, "--pretty=format:%s||%h", "--no-merges"], {
2113
2319
  cwd,
2114
2320
  encoding: "utf-8",
2115
2321
  stdio: ["ignore", "pipe", "pipe"]
@@ -2166,7 +2372,7 @@ ${entry}${prior.slice(idx + 1)}`);
2166
2372
  }
2167
2373
  }
2168
2374
  function git3(args, cwd, timeout = 6e4) {
2169
- return execFileSync10("git", args, {
2375
+ return execFileSync11("git", args, {
2170
2376
  encoding: "utf-8",
2171
2377
  timeout,
2172
2378
  cwd,
@@ -2381,7 +2587,7 @@ ${truncate2(r.stderr, 2e3)}
2381
2587
  }
2382
2588
 
2383
2589
  // src/scripts/resolveFlow.ts
2384
- import { execFileSync as execFileSync11 } from "child_process";
2590
+ import { execFileSync as execFileSync12 } from "child_process";
2385
2591
  var CONFLICT_DIFF_MAX_BYTES = 4e4;
2386
2592
  var resolveFlow = async (ctx) => {
2387
2593
  const prNumber = ctx.args.pr;
@@ -2433,7 +2639,7 @@ var resolveFlow = async (ctx) => {
2433
2639
  };
2434
2640
  function getConflictedFiles(cwd) {
2435
2641
  try {
2436
- const out = execFileSync11("git", ["diff", "--name-only", "--diff-filter=U"], {
2642
+ const out = execFileSync12("git", ["diff", "--name-only", "--diff-filter=U"], {
2437
2643
  encoding: "utf-8",
2438
2644
  cwd,
2439
2645
  env: { ...process.env, HUSKY: "0" }
@@ -2448,7 +2654,7 @@ function getConflictMarkersPreview(files, cwd, maxBytes = CONFLICT_DIFF_MAX_BYTE
2448
2654
  let total = 0;
2449
2655
  for (const f of files) {
2450
2656
  try {
2451
- const content = execFileSync11("cat", [f], { encoding: "utf-8", cwd }).toString();
2657
+ const content = execFileSync12("cat", [f], { encoding: "utf-8", cwd }).toString();
2452
2658
  const snippet = `### ${f}
2453
2659
 
2454
2660
  \`\`\`
@@ -2528,6 +2734,35 @@ function tryPost(issueNumber, body, cwd) {
2528
2734
  }
2529
2735
  }
2530
2736
 
2737
+ // src/scripts/saveTaskState.ts
2738
+ var saveTaskState = async (ctx, profile) => {
2739
+ const target = ctx.data.commentTargetType;
2740
+ const number = ctx.data.commentTargetNumber;
2741
+ const state = ctx.data.taskState;
2742
+ if (!target || !number || !state) return;
2743
+ const executable = profile.name;
2744
+ const action = ctx.data.action ?? synthesizeAction(ctx);
2745
+ if (ctx.output.prUrl && !state.core.prUrl) state.core.prUrl = ctx.output.prUrl;
2746
+ if (typeof ctx.data.runUrl === "string") state.core.runUrl = ctx.data.runUrl;
2747
+ const next = reduce(state, executable, action);
2748
+ if (ctx.output.prUrl) next.core.prUrl = ctx.output.prUrl;
2749
+ if (typeof ctx.data.runUrl === "string") next.core.runUrl = ctx.data.runUrl;
2750
+ writeTaskState(target, number, next, ctx.cwd);
2751
+ ctx.data.taskStateRendered = renderStateComment(next);
2752
+ };
2753
+ function synthesizeAction(ctx) {
2754
+ const ok = ctx.output.exitCode === 0;
2755
+ return {
2756
+ type: ok ? "RUN_COMPLETED" : "RUN_FAILED",
2757
+ payload: {
2758
+ exitCode: ctx.output.exitCode,
2759
+ reason: ctx.output.reason,
2760
+ prUrl: ctx.output.prUrl
2761
+ },
2762
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2763
+ };
2764
+ }
2765
+
2531
2766
  // src/verify.ts
2532
2767
  import { spawn as spawn2 } from "child_process";
2533
2768
  var TAIL_CHARS = 4e3;
@@ -2730,6 +2965,7 @@ var preflightScripts = {
2730
2965
  initFlow,
2731
2966
  releaseFlow,
2732
2967
  watchStalePrsFlow,
2968
+ loadTaskState,
2733
2969
  loadConventions,
2734
2970
  loadCoverageRules,
2735
2971
  composePrompt
@@ -2742,7 +2978,8 @@ var postflightScripts = {
2742
2978
  ensurePr: ensurePr2,
2743
2979
  postIssueComment: postIssueComment2,
2744
2980
  postReviewResult,
2745
- writeRunSummary
2981
+ writeRunSummary,
2982
+ saveTaskState
2746
2983
  };
2747
2984
  var allScriptNames = /* @__PURE__ */ new Set([
2748
2985
  ...Object.keys(preflightScripts),
@@ -2750,7 +2987,7 @@ var allScriptNames = /* @__PURE__ */ new Set([
2750
2987
  ]);
2751
2988
 
2752
2989
  // src/tools.ts
2753
- import { execFileSync as execFileSync12 } from "child_process";
2990
+ import { execFileSync as execFileSync13 } from "child_process";
2754
2991
  function verifyCliTools(tools, cwd) {
2755
2992
  const out = [];
2756
2993
  for (const t of tools) out.push(verifyOne(t, cwd));
@@ -2783,7 +3020,7 @@ function verifyOne(tool, cwd) {
2783
3020
  }
2784
3021
  function runShell2(cmd, cwd, timeoutMs = 3e4) {
2785
3022
  try {
2786
- execFileSync12("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
3023
+ execFileSync13("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
2787
3024
  return true;
2788
3025
  } catch {
2789
3026
  return false;
@@ -2981,17 +3218,19 @@ function finish(out) {
2981
3218
  }
2982
3219
 
2983
3220
  // src/kody2-cli.ts
2984
- import { execFileSync as execFileSync13 } from "child_process";
3221
+ import { execFileSync as execFileSync14 } from "child_process";
2985
3222
  import * as fs15 from "fs";
2986
3223
  import * as path12 from "path";
2987
3224
 
2988
3225
  // src/dispatch.ts
2989
3226
  import * as fs14 from "fs";
2990
- function autoDispatch(explicit) {
2991
- if (explicit?.mode && explicit.target) {
3227
+ function autoDispatch(opts) {
3228
+ const explicit = opts?.explicit;
3229
+ if (explicit?.issueNumber && explicit.issueNumber > 0) {
2992
3230
  return {
2993
- mode: explicit.mode,
2994
- target: explicit.target
3231
+ executable: "build",
3232
+ cliArgs: { mode: "run", issue: explicit.issueNumber },
3233
+ target: explicit.issueNumber
2995
3234
  };
2996
3235
  }
2997
3236
  const eventName = process.env.GITHUB_EVENT_NAME;
@@ -3005,30 +3244,60 @@ function autoDispatch(explicit) {
3005
3244
  }
3006
3245
  if (eventName === "workflow_dispatch") {
3007
3246
  const n = parseInt(String(event.inputs?.issue_number ?? ""), 10);
3008
- if (!Number.isNaN(n) && n > 0) return { mode: "run", target: n };
3247
+ if (!Number.isNaN(n) && n > 0) {
3248
+ return { executable: "build", cliArgs: { mode: "run", issue: n }, target: n };
3249
+ }
3009
3250
  return null;
3010
3251
  }
3011
- if (eventName === "issue_comment") {
3012
- const body = String(event.comment?.body ?? "").toLowerCase();
3013
- const issueNum = Number(event.issue?.number ?? 0);
3014
- const isPr = !!event.issue?.pull_request;
3015
- if (!issueNum) return null;
3016
- const afterTag = extractAfterTag(body);
3017
- if (isPr) {
3018
- if (/\bfix-ci\b/.test(afterTag)) return { mode: "fix-ci", target: issueNum };
3019
- if (/\bresolve\b/.test(afterTag)) return { mode: "resolve", target: issueNum };
3020
- const feedbackText = extractFeedback(afterTag);
3021
- return { mode: "fix", target: issueNum, feedback: feedbackText };
3252
+ if (eventName !== "issue_comment") return null;
3253
+ const body = String(event.comment?.body ?? "").toLowerCase();
3254
+ const targetNum = Number(event.issue?.number ?? 0);
3255
+ const isPr = !!event.issue?.pull_request;
3256
+ if (!targetNum) return null;
3257
+ const afterTag = extractAfterTag(body);
3258
+ if (isPr) {
3259
+ if (/\bfix-ci\b/.test(afterTag)) {
3260
+ return { executable: "build", cliArgs: { mode: "fix-ci", pr: targetNum }, target: targetNum };
3022
3261
  }
3023
- return { mode: "run", target: issueNum };
3262
+ if (/\bresolve\b/.test(afterTag)) {
3263
+ return { executable: "build", cliArgs: { mode: "resolve", pr: targetNum }, target: targetNum };
3264
+ }
3265
+ const feedback = extractFeedback(afterTag);
3266
+ return {
3267
+ executable: "build",
3268
+ cliArgs: { mode: "fix", pr: targetNum, ...feedback ? { feedback } : {} },
3269
+ target: targetNum
3270
+ };
3024
3271
  }
3025
- return null;
3272
+ const sub = extractSubcommand(afterTag);
3273
+ const defaultExec = opts?.config?.defaultExecutable ?? "build";
3274
+ if (!sub) {
3275
+ return asDispatch(defaultExec, targetNum);
3276
+ }
3277
+ if (sub === "build") {
3278
+ return { executable: "build", cliArgs: { mode: "run", issue: targetNum }, target: targetNum };
3279
+ }
3280
+ if (sub === "orchestrate" || sub === "orchestrator") {
3281
+ return { executable: "orchestrator", cliArgs: { issue: targetNum }, target: targetNum };
3282
+ }
3283
+ return asDispatch(sub, targetNum);
3284
+ }
3285
+ function asDispatch(executable, target) {
3286
+ if (executable === "build") {
3287
+ return { executable, cliArgs: { mode: "run", issue: target }, target };
3288
+ }
3289
+ return { executable, cliArgs: { issue: target }, target };
3026
3290
  }
3027
3291
  function extractAfterTag(body) {
3028
3292
  const idx = body.indexOf("@kody2");
3029
3293
  if (idx === -1) return body;
3030
3294
  return body.slice(idx + "@kody2".length).trim();
3031
3295
  }
3296
+ function extractSubcommand(afterTag) {
3297
+ const match = afterTag.match(/^([a-z][a-z0-9-]{1,40})\b/);
3298
+ if (!match) return null;
3299
+ return match[1];
3300
+ }
3032
3301
  function extractFeedback(afterTag) {
3033
3302
  const cleaned = afterTag.replace(/^(fix|please|kindly)[\s:,.-]+/i, "").trim();
3034
3303
  return cleaned.length > 0 ? cleaned : void 0;
@@ -3126,7 +3395,7 @@ function detectPackageManager2(cwd) {
3126
3395
  }
3127
3396
  function shellOut(cmd, args, cwd, stream = true) {
3128
3397
  try {
3129
- execFileSync13(cmd, args, {
3398
+ execFileSync14(cmd, args, {
3130
3399
  cwd,
3131
3400
  stdio: stream ? "inherit" : "pipe",
3132
3401
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" }
@@ -3139,7 +3408,7 @@ function shellOut(cmd, args, cwd, stream = true) {
3139
3408
  }
3140
3409
  function isOnPath(bin) {
3141
3410
  try {
3142
- execFileSync13("which", [bin], { stdio: "pipe" });
3411
+ execFileSync14("which", [bin], { stdio: "pipe" });
3143
3412
  return true;
3144
3413
  } catch {
3145
3414
  return false;
@@ -3173,7 +3442,7 @@ function installLitellmIfNeeded(cwd) {
3173
3442
  } catch {
3174
3443
  }
3175
3444
  try {
3176
- execFileSync13("python3", ["-c", "import litellm"], { stdio: "pipe" });
3445
+ execFileSync14("python3", ["-c", "import litellm"], { stdio: "pipe" });
3177
3446
  process.stdout.write("\u2192 kody2: litellm already installed\n");
3178
3447
  return 0;
3179
3448
  } catch {
@@ -3183,16 +3452,16 @@ function installLitellmIfNeeded(cwd) {
3183
3452
  }
3184
3453
  function configureGitIdentity(cwd) {
3185
3454
  try {
3186
- const name = execFileSync13("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
3455
+ const name = execFileSync14("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
3187
3456
  if (name) return;
3188
3457
  } catch {
3189
3458
  }
3190
3459
  try {
3191
- execFileSync13("git", ["config", "user.name", "kody2-bot"], { cwd, stdio: "pipe" });
3460
+ execFileSync14("git", ["config", "user.name", "kody2-bot"], { cwd, stdio: "pipe" });
3192
3461
  } catch {
3193
3462
  }
3194
3463
  try {
3195
- execFileSync13("git", ["config", "user.email", "kody2-bot@users.noreply.github.com"], { cwd, stdio: "pipe" });
3464
+ execFileSync14("git", ["config", "user.email", "kody2-bot@users.noreply.github.com"], { cwd, stdio: "pipe" });
3196
3465
  } catch {
3197
3466
  }
3198
3467
  }
@@ -3227,7 +3496,13 @@ async function runCi(argv) {
3227
3496
  return 0;
3228
3497
  }
3229
3498
  const args = parseCiArgs(argv);
3230
- const autoFallback = !args.issueNumber ? autoDispatch() : null;
3499
+ const cwd = args.cwd ? path12.resolve(args.cwd) : process.cwd();
3500
+ let earlyConfig;
3501
+ try {
3502
+ earlyConfig = loadConfig(cwd);
3503
+ } catch {
3504
+ }
3505
+ const autoFallback = !args.issueNumber ? autoDispatch({ config: earlyConfig }) : null;
3231
3506
  if (!args.issueNumber && !autoFallback) {
3232
3507
  } else {
3233
3508
  args.errors = args.errors.filter((e) => !e.includes("--issue"));
@@ -3239,14 +3514,13 @@ async function runCi(argv) {
3239
3514
  ${CI_HELP}`);
3240
3515
  return 64;
3241
3516
  }
3242
- const cwd = args.cwd ? path12.resolve(args.cwd) : process.cwd();
3243
3517
  const dispatch = autoFallback ?? {
3244
- mode: "run",
3245
- target: args.issueNumber,
3246
- feedback: void 0
3518
+ executable: "build",
3519
+ cliArgs: { mode: "run", issue: args.issueNumber },
3520
+ target: args.issueNumber
3247
3521
  };
3248
3522
  const issueNumber = dispatch.target;
3249
- process.stdout.write(`\u2192 kody2 preflight (cwd=${cwd}, mode=${dispatch.mode}, target=${issueNumber})
3523
+ process.stdout.write(`\u2192 kody2 preflight (cwd=${cwd}, executable=${dispatch.executable}, target=${issueNumber})
3250
3524
  `);
3251
3525
  try {
3252
3526
  const n = unpackAllSecrets();
@@ -3283,17 +3557,13 @@ ${CI_HELP}`);
3283
3557
  postFailureTail(issueNumber, cwd, `preflight crashed: ${msg}`);
3284
3558
  return 99;
3285
3559
  }
3286
- process.stdout.write(`\u2192 kody2: preflight done, handing off to kody2 ${dispatch.mode}
3560
+ process.stdout.write(`\u2192 kody2: preflight done, handing off to kody2 ${dispatch.executable}
3287
3561
 
3288
3562
  `);
3289
3563
  try {
3290
- const config = loadConfig(cwd);
3291
- const cliArgs = { mode: dispatch.mode };
3292
- if (dispatch.mode === "run") cliArgs.issue = issueNumber;
3293
- else cliArgs.pr = issueNumber;
3294
- if (dispatch.feedback) cliArgs.feedback = dispatch.feedback;
3295
- const result = await runExecutable("build", {
3296
- cliArgs,
3564
+ const config = earlyConfig ?? loadConfig(cwd);
3565
+ const result = await runExecutable(dispatch.executable, {
3566
+ cliArgs: dispatch.cliArgs,
3297
3567
  cwd,
3298
3568
  config,
3299
3569
  verbose: args.verbose,
@@ -67,6 +67,7 @@
67
67
  { "script": "fixFlow", "runWhen": { "args.mode": "fix" } },
68
68
  { "script": "fixCiFlow", "runWhen": { "args.mode": "fix-ci" } },
69
69
  { "script": "resolveFlow", "runWhen": { "args.mode": "resolve" } },
70
+ { "script": "loadTaskState" },
70
71
  { "script": "loadConventions" },
71
72
  { "script": "loadCoverageRules" },
72
73
  { "script": "composePrompt" }
@@ -78,7 +79,21 @@
78
79
  { "script": "commitAndPush" },
79
80
  { "script": "ensurePr" },
80
81
  { "script": "postIssueComment" },
81
- { "script": "writeRunSummary" }
82
+ { "script": "writeRunSummary" },
83
+ { "script": "saveTaskState" }
84
+ ]
85
+ },
86
+ "output": {
87
+ "actionTypes": [
88
+ "RUN_COMPLETED",
89
+ "RUN_FAILED",
90
+ "FIX_COMPLETED",
91
+ "FIX_FAILED",
92
+ "FIX_CI_COMPLETED",
93
+ "FIX_CI_FAILED",
94
+ "RESOLVE_COMPLETED",
95
+ "RESOLVE_FAILED",
96
+ "AGENT_NOT_RUN"
82
97
  ]
83
98
  }
84
99
  }
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "orchestrator",
3
+ "describe": "Drive a chain of kody2 executables by posting @kody2 subcommand comments and polling the task-state comment. Atomic — one orchestrator run fires one flow.",
4
+
5
+ "inputs": [
6
+ {
7
+ "name": "issue",
8
+ "flag": "--issue",
9
+ "type": "int",
10
+ "required": true,
11
+ "describe": "GitHub issue number to drive."
12
+ },
13
+ {
14
+ "name": "flow",
15
+ "flag": "--flow",
16
+ "type": "string",
17
+ "required": false,
18
+ "describe": "Named flow to run (e.g. 'plan-then-build'). Defaults to plan-then-build."
19
+ }
20
+ ],
21
+
22
+ "claudeCode": {
23
+ "model": "inherit",
24
+ "permissionMode": "default",
25
+ "maxTurns": null,
26
+ "systemPromptAppend": null,
27
+ "tools": ["Bash", "Read"],
28
+ "hooks": { "PreToolUse": [], "PostToolUse": [], "Stop": [] },
29
+ "skills": [],
30
+ "commands": [],
31
+ "subagents": [],
32
+ "plugins": [],
33
+ "mcpServers": []
34
+ },
35
+
36
+ "cliTools": [],
37
+
38
+ "scripts": {
39
+ "preflight": [
40
+ { "script": "runFlow" },
41
+ { "script": "loadTaskState" },
42
+ { "script": "composePrompt" }
43
+ ],
44
+ "postflight": [
45
+ { "script": "parseAgentResult" },
46
+ { "script": "postIssueComment" },
47
+ { "script": "writeRunSummary" },
48
+ { "script": "saveTaskState" }
49
+ ]
50
+ },
51
+
52
+ "output": {
53
+ "actionTypes": ["ORCHESTRATION_COMPLETED", "ORCHESTRATION_FAILED"]
54
+ }
55
+ }
@@ -0,0 +1,56 @@
1
+ You are the **kody2 orchestrator** for issue #{{issue.number}} on {{repoOwner}}/{{repoName}}.
2
+
3
+ Your job: drive a 2-step flow **plan → build** by posting `@kody2 <subcommand>` comments on the issue and watching the state-comment for completion signals. You do NOT edit files. You do NOT run git. You use `gh` (via Bash) only to post comments and read the state-comment.
4
+
5
+ ---
6
+
7
+ # Issue #{{issue.number}}: {{issue.title}}
8
+
9
+ {{issue.body}}
10
+
11
+ # Required flow (plan-then-build)
12
+
13
+ 1. **Kick off plan.** Post an issue comment with EXACTLY this body:
14
+ ```
15
+ @kody2 plan
16
+ ```
17
+ Use: `gh issue comment {{issue.number}} --body "@kody2 plan"` (in the cwd).
18
+ 2. **Wait for plan to complete.** Poll the issue's state-comment every ~30s. The state-comment is the one whose body starts with `<!-- kody2:state:v1:begin -->`. Fetch it with:
19
+ ```
20
+ gh api repos/{{repoOwner}}/{{repoName}}/issues/{{issue.number}}/comments --paginate --jq '.[] | select(.body | contains("kody2:state:v1:begin")) | .body'
21
+ ```
22
+ Parse the JSON block inside the sentinels. Look for `core.lastOutcome.type == "PLAN_COMPLETED"`.
23
+ If `core.lastOutcome.type == "PLAN_FAILED"` OR if 10 minutes pass without completion → abort with:
24
+ ```
25
+ FAILED: plan did not complete (<reason from state or "timeout">)
26
+ ```
27
+ 3. **Kick off build.** Post:
28
+ ```
29
+ @kody2 build
30
+ ```
31
+ Same `gh issue comment` command.
32
+ 4. **Wait for build to complete.** Same poll technique. Look for `core.lastOutcome.type == "RUN_COMPLETED"` (build's success marker) or `RUN_FAILED`. If `RUN_FAILED` or 30 minutes pass → abort with `FAILED: build did not complete (...)`.
33
+ 5. **Emit final summary.**
34
+
35
+ # Required final output
36
+
37
+ On success:
38
+
39
+ ```
40
+ DONE
41
+ COMMIT_MSG: chore(orchestrator): plan-then-build for #{{issue.number}}
42
+ PR_SUMMARY:
43
+ - Posted `@kody2 plan` and observed PLAN_COMPLETED.
44
+ - Posted `@kody2 build` and observed RUN_COMPLETED.
45
+ - Final PR: <prUrl from state>
46
+ ```
47
+
48
+ On failure, a single line: `FAILED: <concrete reason>`.
49
+
50
+ # Rules
51
+
52
+ - NEVER edit files. Read-only flow.
53
+ - NEVER run git. Only `gh` via Bash for comment posting and state polling.
54
+ - Between polls, sleep ~30 seconds. Do NOT poll faster than once every 30 seconds.
55
+ - Hard cap: 40 turns total across the whole flow. If you're approaching the cap, fail early with `FAILED: turn budget exhausted`.
56
+ - If you post an `@kody2` comment and the state-comment does NOT update within the poll window, the child executable likely didn't run — check the GitHub Actions runs tab URL via `gh run list --limit 5 --json conclusion,status,url` to diagnose, then fail with a concrete reason.
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "plan",
3
+ "describe": "Research an issue and produce a concrete implementation plan as a comment. Read-only — no branches, no commits.",
4
+
5
+ "inputs": [
6
+ {
7
+ "name": "issue",
8
+ "flag": "--issue",
9
+ "type": "int",
10
+ "required": true,
11
+ "describe": "GitHub issue number to plan."
12
+ }
13
+ ],
14
+
15
+ "claudeCode": {
16
+ "model": "inherit",
17
+ "permissionMode": "default",
18
+ "maxTurns": null,
19
+ "systemPromptAppend": null,
20
+ "tools": ["Read", "Grep", "Glob", "Bash"],
21
+ "hooks": { "PreToolUse": [], "PostToolUse": [], "Stop": [] },
22
+ "skills": [],
23
+ "commands": [],
24
+ "subagents": [],
25
+ "plugins": [],
26
+ "mcpServers": []
27
+ },
28
+
29
+ "cliTools": [],
30
+
31
+ "scripts": {
32
+ "preflight": [
33
+ { "script": "runFlow" },
34
+ { "script": "loadTaskState" },
35
+ { "script": "loadConventions" },
36
+ { "script": "composePrompt" }
37
+ ],
38
+ "postflight": [
39
+ { "script": "parseAgentResult" },
40
+ { "script": "postIssueComment" },
41
+ { "script": "writeRunSummary" },
42
+ { "script": "saveTaskState" }
43
+ ]
44
+ },
45
+
46
+ "output": {
47
+ "actionTypes": ["PLAN_COMPLETED", "PLAN_FAILED"]
48
+ }
49
+ }
@@ -0,0 +1,42 @@
1
+ You are a senior engineer producing an **implementation plan** for the GitHub issue below. You will NOT write code. You will NOT run git or gh commands. You will NOT modify files. Your only outputs are:
2
+
3
+ 1. Use Read / Grep / Glob / Bash (read-only) to study the codebase as much as needed.
4
+ 2. Emit a final message with the plan wrapped in the required markers (see "Required output").
5
+
6
+ ---
7
+
8
+ # Repo
9
+ - {{repoOwner}}/{{repoName}}, default branch: {{defaultBranch}}
10
+
11
+ # Issue #{{issue.number}}: {{issue.title}}
12
+
13
+ {{issue.body}}
14
+
15
+ Recent comments (most recent first, truncated):
16
+ {{issue.commentsFormatted}}
17
+
18
+ {{conventionsBlock}}
19
+
20
+ ---
21
+
22
+ # Required output
23
+
24
+ Your FINAL message must be exactly this shape (no extra text before or after):
25
+
26
+ ```
27
+ DONE
28
+ COMMIT_MSG: plan: <very short title>
29
+ PR_SUMMARY:
30
+ <A concrete implementation plan in markdown. Include:
31
+ - Files to change (with paths), and the change in each.
32
+ - New files to create, with their purpose and rough shape.
33
+ - Any ambiguities that need the human to resolve first.
34
+ - Verification checklist (typecheck / tests / lint expectations).
35
+ Keep to ~60 lines or less. No filler. No marketing language.>
36
+ ```
37
+
38
+ # Rules
39
+ - Read-only. Do NOT modify any file.
40
+ - Do NOT run git or gh commands.
41
+ - No speculative scope — plan only what the issue asks for.
42
+ - If the issue is ambiguous and you cannot make progress without input, output `FAILED: <what's unclear>` instead of a plan.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "kody2 — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
5
5
  "license": "MIT",
6
6
  "type": "module",