@kody-ade/kody-engine 0.4.622 → 0.4.624

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -109,6 +109,16 @@ fans out scheduled capabilities at runtime, so capability schedules never
109
109
  generate additional GitHub workflow files. Idempotent — pass `--force` to
110
110
  overwrite.
111
111
 
112
+ Kody also discovers conventional package scripts (`typecheck`, `lint`,
113
+ `test:unit` or `test`, and non-mutating format-check scripts). This means a
114
+ repository that adds its application after Kody onboarding still receives a
115
+ real verification gate. Explicit `quality` commands in `kody.config.json`
116
+ always override discovery. See [Repository quality gates](docs/quality-gates.md).
117
+
118
+ Lifecycle labels stay attached to both the task and its pull request. A green
119
+ delivery is `kody:reviewing`; after that PR merges, Kody changes the PR and each
120
+ linked closing issue to `kody:done`. See [Pull request lifecycle](docs/pull-request-lifecycle.md).
121
+
112
122
  Store model provider keys (for example `MINIMAX_API_KEY`) in the connected
113
123
  repository's Kody vault. GitHub Actions uses OIDC to request only the selected
114
124
  model key at runtime; user secrets are not copied into Actions. `KODY_TOKEN`
package/dist/bin/kody.js CHANGED
@@ -15,7 +15,7 @@ var init_package = __esm({
15
15
  "package.json"() {
16
16
  package_default = {
17
17
  name: "@kody-ade/kody-engine",
18
- version: "0.4.622",
18
+ version: "0.4.624",
19
19
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative implementation profiles.",
20
20
  license: "MIT",
21
21
  repository: {
@@ -183,6 +183,32 @@ var init_completionGuard = __esm({
183
183
  // src/config.ts
184
184
  import * as fs2 from "fs";
185
185
  import * as path3 from "path";
186
+ function packageManagerFor(projectDir) {
187
+ if (fs2.existsSync(path3.join(projectDir, "pnpm-lock.yaml"))) return "pnpm";
188
+ if (fs2.existsSync(path3.join(projectDir, "yarn.lock"))) return "yarn";
189
+ if (fs2.existsSync(path3.join(projectDir, "bun.lockb")) || fs2.existsSync(path3.join(projectDir, "bun.lock"))) return "bun";
190
+ return "npm";
191
+ }
192
+ function inferQualityCommands(projectDir) {
193
+ const empty = { typecheck: "", lint: "", format: "", testUnit: "" };
194
+ try {
195
+ const pkg = JSON.parse(fs2.readFileSync(path3.join(projectDir, "package.json"), "utf-8"));
196
+ const scripts = pkg.scripts ?? {};
197
+ const pm = packageManagerFor(projectDir);
198
+ const command = (names) => {
199
+ const name = names.find((candidate) => typeof scripts[candidate] === "string");
200
+ return name ? `${pm} run ${name}` : "";
201
+ };
202
+ return {
203
+ typecheck: command(["typecheck", "type-check", "check:types"]),
204
+ lint: command(["lint"]),
205
+ format: command(["format:check", "format-check", "prettier:check"]),
206
+ testUnit: command(["test:unit", "test"])
207
+ };
208
+ } catch {
209
+ return empty;
210
+ }
211
+ }
186
212
  function parseReasoningEffort(raw) {
187
213
  if (!raw) return null;
188
214
  const v = raw.trim().toLowerCase();
@@ -285,6 +311,7 @@ function loadConfig(projectDir = process.cwd()) {
285
311
  throw new Error(`kody.config.json is invalid JSON: ${msg}`);
286
312
  }
287
313
  const quality = recordValue(raw.quality) ?? {};
314
+ const inferredQuality = inferQualityCommands(projectDir);
288
315
  const git5 = recordValue(raw.git) ?? {};
289
316
  const github = recordValue(raw.github) ?? {};
290
317
  const agent = recordValue(raw.agent) ?? {};
@@ -300,10 +327,10 @@ function loadConfig(projectDir = process.cwd()) {
300
327
  }
301
328
  return {
302
329
  quality: {
303
- typecheck: typeof quality.typecheck === "string" ? quality.typecheck : "",
304
- lint: typeof quality.lint === "string" ? quality.lint : "",
305
- format: typeof quality.format === "string" ? quality.format : "",
306
- testUnit: typeof quality.testUnit === "string" ? quality.testUnit : "",
330
+ typecheck: typeof quality.typecheck === "string" ? quality.typecheck : inferredQuality.typecheck,
331
+ lint: typeof quality.lint === "string" ? quality.lint : inferredQuality.lint,
332
+ format: typeof quality.format === "string" ? quality.format : inferredQuality.format,
333
+ testUnit: typeof quality.testUnit === "string" ? quality.testUnit : inferredQuality.testUnit,
307
334
  coverage: typeof quality.coverage === "string" ? quality.coverage : ""
308
335
  },
309
336
  git: {
@@ -15810,19 +15837,6 @@ var init_workflow_template = __esm({
15810
15837
  import { execFileSync as execFileSync14 } from "child_process";
15811
15838
  import * as fs40 from "fs";
15812
15839
  import * as path39 from "path";
15813
- function detectPackageManager(cwd) {
15814
- if (fs40.existsSync(path39.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
15815
- if (fs40.existsSync(path39.join(cwd, "yarn.lock"))) return "yarn";
15816
- if (fs40.existsSync(path39.join(cwd, "bun.lockb"))) return "bun";
15817
- return "npm";
15818
- }
15819
- function qualityCommandsFor(pm) {
15820
- return {
15821
- typecheck: `${pm} tsc --noEmit`,
15822
- lint: "",
15823
- testUnit: `${pm} test`
15824
- };
15825
- }
15826
15840
  function schemaUrlFromPkg() {
15827
15841
  const fallback = "https://raw.githubusercontent.com/aharonyaircohen/kody-engine/main/kody.config.schema.json";
15828
15842
  const repoUrl = package_default.repository?.url;
@@ -15845,10 +15859,10 @@ function detectOwnerRepo(cwd) {
15845
15859
  if (!m) return null;
15846
15860
  return { owner: m[1], repo: m[2] };
15847
15861
  }
15848
- function makeConfig(pm, ownerRepo, defaultBranch) {
15862
+ function makeConfig(cwd, ownerRepo, defaultBranch) {
15849
15863
  return {
15850
15864
  $schema: schemaUrlFromPkg(),
15851
- quality: qualityCommandsFor(pm),
15865
+ quality: inferQualityCommands(cwd),
15852
15866
  git: { defaultBranch },
15853
15867
  github: {
15854
15868
  owner: ownerRepo?.owner ?? "OWNER",
@@ -15882,14 +15896,13 @@ function defaultBranchFromGit(cwd) {
15882
15896
  function performInit(cwd, force) {
15883
15897
  const wrote = [];
15884
15898
  const skipped = [];
15885
- const pm = detectPackageManager(cwd);
15886
15899
  const ownerRepo = detectOwnerRepo(cwd);
15887
15900
  const defaultBranch = defaultBranchFromGit(cwd);
15888
15901
  const configPath = path39.join(cwd, "kody.config.json");
15889
15902
  if (fs40.existsSync(configPath) && !force) {
15890
15903
  skipped.push("kody.config.json");
15891
15904
  } else {
15892
- const cfg = makeConfig(pm, ownerRepo, defaultBranch);
15905
+ const cfg = makeConfig(cwd, ownerRepo, defaultBranch);
15893
15906
  fs40.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
15894
15907
  `);
15895
15908
  wrote.push("kody.config.json");
@@ -15916,6 +15929,7 @@ var init_initFlow = __esm({
15916
15929
  "src/scripts/initFlow.ts"() {
15917
15930
  "use strict";
15918
15931
  init_package();
15932
+ init_config();
15919
15933
  init_lifecycleLabels();
15920
15934
  init_workflow_template();
15921
15935
  initFlow = async (ctx) => {
@@ -26419,7 +26433,7 @@ async function hydrateDefinitionsFromEnv(cwd = process.cwd(), env = process.env)
26419
26433
 
26420
26434
  // src/kody-cli.ts
26421
26435
  import { execFileSync as execFileSync24 } from "child_process";
26422
- import * as fs56 from "fs";
26436
+ import * as fs57 from "fs";
26423
26437
  import * as path53 from "path";
26424
26438
 
26425
26439
  // src/app-auth.ts
@@ -26966,6 +26980,55 @@ function readRunRequestFromEnv(env = process.env) {
26966
26980
  // src/kody-cli.ts
26967
26981
  init_runtimePaths();
26968
26982
  init_stateWorkspace();
26983
+
26984
+ // src/mergedPrLifecycle.ts
26985
+ init_lifecycleLabels();
26986
+ import * as fs56 from "fs";
26987
+ var DONE3 = {
26988
+ label: "kody:done",
26989
+ color: "0e8a16",
26990
+ description: "kody: work complete"
26991
+ };
26992
+ function mergedKodyPullRequestTargets(event) {
26993
+ if (!event || typeof event !== "object" || Array.isArray(event)) return null;
26994
+ const root = event;
26995
+ if (root.action !== "closed") return null;
26996
+ const pr = root.pull_request;
26997
+ if (!pr || typeof pr !== "object" || Array.isArray(pr)) return null;
26998
+ const record2 = pr;
26999
+ if (record2.merged !== true) return null;
27000
+ const labels = Array.isArray(record2.labels) ? record2.labels : [];
27001
+ const hasKodyLifecycleLabel = labels.some((label) => {
27002
+ if (!label || typeof label !== "object" || Array.isArray(label)) return false;
27003
+ const name = label.name;
27004
+ return typeof name === "string" && name.startsWith("kody:");
27005
+ });
27006
+ if (!hasKodyLifecycleLabel) return null;
27007
+ const prNumber = Number(record2.number ?? root.number ?? 0);
27008
+ if (!Number.isInteger(prNumber) || prNumber <= 0) return null;
27009
+ const body = typeof record2.body === "string" ? record2.body : "";
27010
+ const issues = /* @__PURE__ */ new Set();
27011
+ const closingReference = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)\b/gi;
27012
+ for (const match of body.matchAll(closingReference)) {
27013
+ const issue2 = Number(match[1]);
27014
+ if (Number.isInteger(issue2) && issue2 > 0 && issue2 !== prNumber) issues.add(issue2);
27015
+ }
27016
+ return { pr: prNumber, issues: [...issues] };
27017
+ }
27018
+ function finalizeMergedPullRequestEvent(event, cwd, writeLabel = setKodyLabel) {
27019
+ const targets = mergedKodyPullRequestTargets(event);
27020
+ if (!targets) return null;
27021
+ writeLabel(targets.pr, DONE3, cwd);
27022
+ for (const issue2 of targets.issues) writeLabel(issue2, DONE3, cwd);
27023
+ return targets;
27024
+ }
27025
+ function readGitHubEvent(env = process.env) {
27026
+ const eventPath = env.GITHUB_EVENT_PATH;
27027
+ if (!eventPath || !fs56.existsSync(eventPath)) return null;
27028
+ return JSON.parse(fs56.readFileSync(eventPath, "utf-8"));
27029
+ }
27030
+
27031
+ // src/kody-cli.ts
26969
27032
  init_workflowDefinitions();
26970
27033
  var FAILED_DISPATCH_LABEL = {
26971
27034
  label: "kody:failed",
@@ -27172,10 +27235,10 @@ async function resolveAuthToken(env = process.env) {
27172
27235
  );
27173
27236
  return void 0;
27174
27237
  }
27175
- function detectPackageManager2(cwd) {
27176
- if (fs56.existsSync(path53.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
27177
- if (fs56.existsSync(path53.join(cwd, "yarn.lock"))) return "yarn";
27178
- if (fs56.existsSync(path53.join(cwd, "bun.lockb"))) return "bun";
27238
+ function detectPackageManager(cwd) {
27239
+ if (fs57.existsSync(path53.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
27240
+ if (fs57.existsSync(path53.join(cwd, "yarn.lock"))) return "yarn";
27241
+ if (fs57.existsSync(path53.join(cwd, "bun.lockb"))) return "bun";
27179
27242
  return "npm";
27180
27243
  }
27181
27244
  function shouldChainScheduledWatch(match) {
@@ -27216,7 +27279,7 @@ function ensurePackageManagerInstalled(pm, cwd) {
27216
27279
  return shellOut("npm", ["install", "-g", spec], cwd);
27217
27280
  }
27218
27281
  function installDeps(pm, cwd) {
27219
- if (!fs56.existsSync(path53.join(cwd, "package.json"))) {
27282
+ if (!fs57.existsSync(path53.join(cwd, "package.json"))) {
27220
27283
  process.stdout.write("\u2192 kody: no package.json found \u2014 skipping consumer dependency install\n");
27221
27284
  return 0;
27222
27285
  }
@@ -27282,8 +27345,8 @@ function postFailureTail(issueNumber, cwd, reason) {
27282
27345
  const logPath = lastRunLogPath(cwd);
27283
27346
  let tail = "";
27284
27347
  try {
27285
- if (fs56.existsSync(logPath)) {
27286
- const content = fs56.readFileSync(logPath, "utf-8");
27348
+ if (fs57.existsSync(logPath)) {
27349
+ const content = fs57.readFileSync(logPath, "utf-8");
27287
27350
  tail = content.slice(-3e3);
27288
27351
  }
27289
27352
  } catch {
@@ -27330,6 +27393,24 @@ async function runCi(argv) {
27330
27393
  } catch (err) {
27331
27394
  earlyConfigError = err instanceof Error ? err : new Error(String(err));
27332
27395
  }
27396
+ if (process.env.GITHUB_EVENT_NAME === "pull_request") {
27397
+ try {
27398
+ const finalized = finalizeMergedPullRequestEvent(readGitHubEvent(), cwd);
27399
+ if (finalized) {
27400
+ process.stdout.write(
27401
+ `\u2192 kody: merged PR #${finalized.pr} finalized as done${finalized.issues.length > 0 ? ` (issues ${finalized.issues.map((n) => `#${n}`).join(", ")})` : ""}
27402
+ `
27403
+ );
27404
+ return 0;
27405
+ }
27406
+ } catch (err) {
27407
+ process.stderr.write(
27408
+ `[kody] failed to finalize merged PR lifecycle: ${err instanceof Error ? err.message : String(err)}
27409
+ `
27410
+ );
27411
+ return 99;
27412
+ }
27413
+ }
27333
27414
  const dispatchCapabilitiesRoot = capabilitiesRoot(cwd);
27334
27415
  const autoFallback = !args.issueNumber ? autoDispatch({ config: earlyConfig, projectCapabilitiesRoot: dispatchCapabilitiesRoot }) : null;
27335
27416
  const eventName = process.env.GITHUB_EVENT_NAME;
@@ -27382,9 +27463,9 @@ async function runCi(argv) {
27382
27463
  forceRunCliArgs = { goal: envForceMessage };
27383
27464
  }
27384
27465
  }
27385
- if (!args.issueNumber && !autoFallback && !forceRunAction && !runRequestFanOut && eventName === "workflow_dispatch" && dispatchEventPath && fs56.existsSync(dispatchEventPath)) {
27466
+ if (!args.issueNumber && !autoFallback && !forceRunAction && !runRequestFanOut && eventName === "workflow_dispatch" && dispatchEventPath && fs57.existsSync(dispatchEventPath)) {
27386
27467
  try {
27387
- const evt = JSON.parse(fs56.readFileSync(dispatchEventPath, "utf-8"));
27468
+ const evt = JSON.parse(fs57.readFileSync(dispatchEventPath, "utf-8"));
27388
27469
  const inputs = objectValue2(evt.inputs);
27389
27470
  const issueInput = parseInt(String(inputs?.issue_number ?? ""), 10);
27390
27471
  const sessionInput = String(inputs?.sessionId ?? "");
@@ -27472,7 +27553,7 @@ async function runCi(argv) {
27472
27553
  if (n > 0) process.stdout.write(`\u2192 kody: unpacked ${n} secret(s)
27473
27554
  `);
27474
27555
  await resolveAuthToken();
27475
- const pm = args.packageManager ?? detectPackageManager2(cwd);
27556
+ const pm = args.packageManager ?? detectPackageManager(cwd);
27476
27557
  if (!args.skipInstall) {
27477
27558
  const code = installDeps(pm, cwd);
27478
27559
  if (code !== 0) {
@@ -27633,7 +27714,7 @@ ${CI_HELP}`);
27633
27714
  `);
27634
27715
  await resolveAuthToken();
27635
27716
  reactToTriggerComment(cwd);
27636
- const pm = args.packageManager ?? detectPackageManager2(cwd);
27717
+ const pm = args.packageManager ?? detectPackageManager(cwd);
27637
27718
  process.stdout.write(`\u2192 kody: package manager = ${pm}
27638
27719
  `);
27639
27720
  const buildOnly = dispatch2.implementation === "preview-build";
@@ -27714,7 +27795,7 @@ async function runScheduledFanOut(cwd, args, opts) {
27714
27795
  if (n > 0) process.stdout.write(`\u2192 kody: unpacked ${n} secret(s) from ALL_SECRETS
27715
27796
  `);
27716
27797
  await resolveAuthToken();
27717
- const pm = args.packageManager ?? detectPackageManager2(cwd);
27798
+ const pm = args.packageManager ?? detectPackageManager(cwd);
27718
27799
  process.stdout.write(`\u2192 kody: package manager = ${pm}
27719
27800
  `);
27720
27801
  if (!args.skipInstall) {
@@ -27800,7 +27881,7 @@ init_repoWorkspace();
27800
27881
 
27801
27882
  // src/scripts/brainTurnLog.ts
27802
27883
  init_runtimePaths();
27803
- import * as fs57 from "fs";
27884
+ import * as fs58 from "fs";
27804
27885
  import * as path54 from "path";
27805
27886
  import posixPath4 from "path/posix";
27806
27887
  var live = /* @__PURE__ */ new Map();
@@ -27809,8 +27890,8 @@ function brainEventsFilePath(dir, chatId) {
27809
27890
  }
27810
27891
  function lastPersistedSeq(dir, chatId) {
27811
27892
  const p = brainEventsFilePath(dir, chatId);
27812
- if (!fs57.existsSync(p)) return 0;
27813
- const lines = fs57.readFileSync(p, "utf-8").split("\n").filter(Boolean);
27893
+ if (!fs58.existsSync(p)) return 0;
27894
+ const lines = fs58.readFileSync(p, "utf-8").split("\n").filter(Boolean);
27814
27895
  if (lines.length === 0) return 0;
27815
27896
  try {
27816
27897
  return JSON.parse(lines[lines.length - 1]).seq || 0;
@@ -27820,9 +27901,9 @@ function lastPersistedSeq(dir, chatId) {
27820
27901
  }
27821
27902
  function readSince(dir, chatId, since) {
27822
27903
  const p = brainEventsFilePath(dir, chatId);
27823
- if (!fs57.existsSync(p)) return [];
27904
+ if (!fs58.existsSync(p)) return [];
27824
27905
  const out = [];
27825
- for (const line of fs57.readFileSync(p, "utf-8").split("\n")) {
27906
+ for (const line of fs58.readFileSync(p, "utf-8").split("\n")) {
27826
27907
  if (!line) continue;
27827
27908
  try {
27828
27909
  const rec = JSON.parse(line);
@@ -27848,12 +27929,12 @@ function beginTurn(dir, chatId) {
27848
27929
  };
27849
27930
  live.set(chatId, state);
27850
27931
  const p = brainEventsFilePath(dir, chatId);
27851
- fs57.mkdirSync(path54.dirname(p), { recursive: true });
27932
+ fs58.mkdirSync(path54.dirname(p), { recursive: true });
27852
27933
  return (event) => {
27853
27934
  state.seq += 1;
27854
27935
  const rec = { seq: state.seq, turn, ts: Date.now(), event };
27855
27936
  try {
27856
- fs57.appendFileSync(p, `${JSON.stringify(rec)}
27937
+ fs58.appendFileSync(p, `${JSON.stringify(rec)}
27857
27938
  `);
27858
27939
  } catch (err) {
27859
27940
  process.stderr.write(
@@ -27892,7 +27973,7 @@ function endTurnIfUnterminated(dir, chatId, errMessage) {
27892
27973
  event: { type: "error", error: errMessage || "turn ended unexpectedly", chatId }
27893
27974
  };
27894
27975
  try {
27895
- fs57.appendFileSync(brainEventsFilePath(dir, chatId), `${JSON.stringify(rec)}
27976
+ fs58.appendFileSync(brainEventsFilePath(dir, chatId), `${JSON.stringify(rec)}
27896
27977
  `);
27897
27978
  } catch {
27898
27979
  }
@@ -30556,7 +30637,7 @@ async function poolServe() {
30556
30637
 
30557
30638
  // src/servers/runner-serve.ts
30558
30639
  import { spawn as spawn10 } from "child_process";
30559
- import * as fs58 from "fs";
30640
+ import * as fs59 from "fs";
30560
30641
  import { createServer as createServer6 } from "http";
30561
30642
  var DEFAULT_PORT2 = 8080;
30562
30643
  var DEFAULT_WORKDIR = "/workspace/repo";
@@ -30632,8 +30713,8 @@ async function defaultRunJob(job) {
30632
30713
  const workdir = process.env.RUNNER_WORKDIR ?? DEFAULT_WORKDIR;
30633
30714
  const branch = job.ref ?? "main";
30634
30715
  const authUrl = `https://x-access-token:${job.githubToken}@github.com/${job.repo}.git`;
30635
- fs58.rmSync(workdir, { recursive: true, force: true });
30636
- fs58.mkdirSync(workdir, { recursive: true });
30716
+ fs59.rmSync(workdir, { recursive: true, force: true });
30717
+ fs59.mkdirSync(workdir, { recursive: true });
30637
30718
  const allSecrets = typeof job.allSecrets === "string" ? job.allSecrets : JSON.stringify(job.allSecrets ?? {});
30638
30719
  const target = job.runRequest.target;
30639
30720
  const interactive = target.type === "chat";
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "quality": {
13
13
  "type": "object",
14
- "description": "Quality gate commands. Empty strings disable individual gates.",
14
+ "description": "Quality gate commands. Missing commands are inferred from conventional package.json scripts; explicit empty strings disable individual gates.",
15
15
  "additionalProperties": false,
16
16
  "properties": {
17
17
  "typecheck": { "type": "string", "default": "" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.622",
3
+ "version": "0.4.624",
4
4
  "description": "kody — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative implementation profiles.",
5
5
  "license": "MIT",
6
6
  "repository": {