@kody-ade/kody-engine 0.3.29 → 0.3.32

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/kody.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.3.29",
6
+ version: "0.3.32",
7
7
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
8
8
  license: "MIT",
9
9
  type: "module",
@@ -50,7 +50,7 @@ var package_default = {
50
50
  };
51
51
 
52
52
  // src/chat-cli.ts
53
- import { execFileSync as execFileSync22 } from "child_process";
53
+ import { execFileSync as execFileSync23 } from "child_process";
54
54
  import * as fs22 from "fs";
55
55
  import * as path19 from "path";
56
56
 
@@ -577,7 +577,7 @@ async function emit(sink, type, sessionId, suffix, payload) {
577
577
  }
578
578
 
579
579
  // src/kody-cli.ts
580
- import { execFileSync as execFileSync21 } from "child_process";
580
+ import { execFileSync as execFileSync22 } from "child_process";
581
581
  import * as fs21 from "fs";
582
582
  import * as path18 from "path";
583
583
 
@@ -2580,6 +2580,19 @@ var dispatch = async (ctx, _profile, _agentResult, args) => {
2580
2580
  return;
2581
2581
  }
2582
2582
  const state = ctx.data.taskState;
2583
+ if (target === "pr" && !state?.core.prUrl) {
2584
+ const reason = `cannot dispatch @kody ${next}: target=pr but state.core.prUrl is not set`;
2585
+ process.stderr.write(`[kody dispatch] ${reason}
2586
+ `);
2587
+ const action = {
2588
+ type: "AGENT_NOT_RUN",
2589
+ payload: { reason, dispatchTarget: "pr", next },
2590
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2591
+ };
2592
+ ctx.data.action = action;
2593
+ if (state) state.core.lastOutcome = action;
2594
+ return;
2595
+ }
2583
2596
  if (state?.flow) {
2584
2597
  state.flow.step = next;
2585
2598
  }
@@ -2880,11 +2893,15 @@ function firstLine(s) {
2880
2893
  }
2881
2894
  function findExistingPr(branch, cwd) {
2882
2895
  try {
2883
- const output = gh2(["pr", "view", branch, "--json", "number,url,body"], { cwd });
2884
- const parsed = JSON.parse(output);
2885
- if (typeof parsed?.number === "number" && typeof parsed?.url === "string") {
2886
- const body = typeof parsed.body === "string" ? parsed.body : "";
2887
- return { number: parsed.number, url: parsed.url, body };
2896
+ const output = gh2(
2897
+ ["pr", "list", "--head", branch, "--state", "open", "--json", "number,url,body", "--limit", "1"],
2898
+ { cwd }
2899
+ );
2900
+ const arr = JSON.parse(output);
2901
+ const first = Array.isArray(arr) ? arr[0] : null;
2902
+ if (first && typeof first.number === "number" && typeof first.url === "string") {
2903
+ const body = typeof first.body === "string" ? first.body : "";
2904
+ return { number: first.number, url: first.url, body };
2888
2905
  }
2889
2906
  return null;
2890
2907
  } catch {
@@ -5128,6 +5145,135 @@ var verify = async (ctx) => {
5128
5145
  }
5129
5146
  };
5130
5147
 
5148
+ // src/scripts/waitForCi.ts
5149
+ import { execFileSync as execFileSync20 } from "child_process";
5150
+ var API_TIMEOUT_MS9 = 3e4;
5151
+ var waitForCi = async (ctx, _profile, _agentResult, args) => {
5152
+ const timeoutMinutes = numArg(args, "timeoutMinutes", 30);
5153
+ const pollSeconds = numArg(args, "pollSeconds", 30);
5154
+ const initialWaitSeconds = numArg(args, "initialWaitSeconds", 15);
5155
+ const maxFixCiAttempts = numArg(args, "maxFixCiAttempts", 3);
5156
+ const state = ctx.data.taskState;
5157
+ const prUrl = state?.core.prUrl;
5158
+ const prNumber = prUrl ? parsePrNumber(prUrl) : null;
5159
+ if (!prNumber) {
5160
+ ctx.data.action = mkAction("CI_GIVEUP", { reason: "no PR url in state \u2014 nothing to wait for" });
5161
+ return;
5162
+ }
5163
+ const fixCiAttempts = state?.core.attempts?.["fix-ci"] ?? 0;
5164
+ await sleep(initialWaitSeconds * 1e3);
5165
+ const deadline = Date.now() + timeoutMinutes * 6e4;
5166
+ let lastSummary = "";
5167
+ while (Date.now() < deadline) {
5168
+ const rows = fetchChecks(prNumber, ctx.cwd);
5169
+ if (rows === null) {
5170
+ await sleep(pollSeconds * 1e3);
5171
+ continue;
5172
+ }
5173
+ if (rows.length === 0) {
5174
+ await sleep(pollSeconds * 1e3);
5175
+ continue;
5176
+ }
5177
+ const summary = summarize(rows);
5178
+ if (summary !== lastSummary) {
5179
+ lastSummary = summary;
5180
+ tryPostPr6(prNumber, `\u23F3 kody waitForCi: ${summary}`, ctx.cwd);
5181
+ }
5182
+ const failed = rows.filter((r) => r.bucket === "fail" || r.bucket === "cancel");
5183
+ const pending = rows.filter((r) => r.bucket === "pending");
5184
+ if (failed.length > 0) {
5185
+ const detail = failed.slice(0, 5).map((r) => `${r.workflow ?? "?"} / ${r.name ?? "?"}${r.link ? ` (${r.link})` : ""}`).join("; ");
5186
+ if (fixCiAttempts >= maxFixCiAttempts) {
5187
+ ctx.data.action = mkAction("CI_GIVEUP", {
5188
+ reason: `fix-ci attempts (${fixCiAttempts}) hit cap (${maxFixCiAttempts})`,
5189
+ failedChecks: detail,
5190
+ prUrl
5191
+ });
5192
+ tryPostPr6(
5193
+ prNumber,
5194
+ `\u{1F6D1} kody waitForCi: giving up after ${fixCiAttempts} fix-ci attempts. Failed: ${detail}`,
5195
+ ctx.cwd
5196
+ );
5197
+ } else {
5198
+ ctx.data.action = mkAction("CI_FAILED", {
5199
+ failedChecks: detail,
5200
+ attempt: fixCiAttempts + 1,
5201
+ maxAttempts: maxFixCiAttempts,
5202
+ prUrl
5203
+ });
5204
+ tryPostPr6(
5205
+ prNumber,
5206
+ `\u274C kody waitForCi: CI failed (attempt ${fixCiAttempts + 1}/${maxFixCiAttempts}). Dispatching fix-ci. Failed: ${detail}`,
5207
+ ctx.cwd
5208
+ );
5209
+ }
5210
+ return;
5211
+ }
5212
+ if (pending.length === 0) {
5213
+ ctx.data.action = mkAction("CI_PASSED", { checks: rows.length, prUrl });
5214
+ tryPostPr6(prNumber, `\u2705 kody waitForCi: all ${rows.length} checks green on PR #${prNumber}`, ctx.cwd);
5215
+ return;
5216
+ }
5217
+ await sleep(pollSeconds * 1e3);
5218
+ }
5219
+ ctx.data.action = mkAction("CI_TIMEOUT", {
5220
+ reason: `CI did not complete within ${timeoutMinutes} minutes`,
5221
+ prUrl
5222
+ });
5223
+ tryPostPr6(prNumber, `\u231B kody waitForCi: timed out after ${timeoutMinutes} minutes`, ctx.cwd);
5224
+ };
5225
+ function fetchChecks(prNumber, cwd) {
5226
+ try {
5227
+ const raw = execFileSync20(
5228
+ "gh",
5229
+ ["pr", "checks", String(prNumber), "--json", "bucket,state,name,workflow,link"],
5230
+ {
5231
+ encoding: "utf-8",
5232
+ timeout: API_TIMEOUT_MS9,
5233
+ cwd,
5234
+ stdio: ["ignore", "pipe", "pipe"]
5235
+ }
5236
+ );
5237
+ const parsed = JSON.parse(raw);
5238
+ return Array.isArray(parsed) ? parsed : [];
5239
+ } catch (err) {
5240
+ process.stderr.write(
5241
+ `[kody waitForCi] gh pr checks #${prNumber} failed: ${err instanceof Error ? err.message : String(err)}
5242
+ `
5243
+ );
5244
+ return null;
5245
+ }
5246
+ }
5247
+ function summarize(rows) {
5248
+ const counts = {};
5249
+ for (const r of rows) {
5250
+ const k = r.bucket ?? "unknown";
5251
+ counts[k] = (counts[k] ?? 0) + 1;
5252
+ }
5253
+ return Object.entries(counts).map(([k, v]) => `${k}:${v}`).join(" ");
5254
+ }
5255
+ function mkAction(type, payload) {
5256
+ return { type, payload, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
5257
+ }
5258
+ function numArg(args, key, fallback) {
5259
+ const v = args?.[key];
5260
+ if (typeof v === "number" && Number.isFinite(v) && v >= 0) return v;
5261
+ if (typeof v === "string") {
5262
+ const n = Number(v);
5263
+ if (Number.isFinite(n) && n >= 0) return n;
5264
+ }
5265
+ return fallback;
5266
+ }
5267
+ function tryPostPr6(prNumber, body, cwd) {
5268
+ try {
5269
+ postPrReviewComment(prNumber, body, cwd);
5270
+ } catch {
5271
+ }
5272
+ }
5273
+ function sleep(ms) {
5274
+ return new Promise((res) => setTimeout(res, ms));
5275
+ }
5276
+
5131
5277
  // src/scripts/watchStalePrsFlow.ts
5132
5278
  function readWatchConfig(ctx) {
5133
5279
  const cfg = ctx.config.watch;
@@ -5319,7 +5465,8 @@ var postflightScripts = {
5319
5465
  postClassification,
5320
5466
  notifyTerminal,
5321
5467
  recordOutcome,
5322
- mergeReleasePr
5468
+ mergeReleasePr,
5469
+ waitForCi
5323
5470
  };
5324
5471
  var allScriptNames = /* @__PURE__ */ new Set([
5325
5472
  ...Object.keys(preflightScripts),
@@ -5327,7 +5474,7 @@ var allScriptNames = /* @__PURE__ */ new Set([
5327
5474
  ]);
5328
5475
 
5329
5476
  // src/tools.ts
5330
- import { execFileSync as execFileSync20 } from "child_process";
5477
+ import { execFileSync as execFileSync21 } from "child_process";
5331
5478
  function verifyCliTools(tools, cwd) {
5332
5479
  const out = [];
5333
5480
  for (const t of tools) out.push(verifyOne(t, cwd));
@@ -5360,7 +5507,7 @@ function verifyOne(tool, cwd) {
5360
5507
  }
5361
5508
  function runShell(cmd, cwd, timeoutMs = 3e4) {
5362
5509
  try {
5363
- execFileSync20("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
5510
+ execFileSync21("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
5364
5511
  return true;
5365
5512
  } catch {
5366
5513
  return false;
@@ -5767,7 +5914,7 @@ function detectPackageManager2(cwd) {
5767
5914
  }
5768
5915
  function shellOut(cmd, args, cwd, stream = true) {
5769
5916
  try {
5770
- execFileSync21(cmd, args, {
5917
+ execFileSync22(cmd, args, {
5771
5918
  cwd,
5772
5919
  stdio: stream ? "inherit" : "pipe",
5773
5920
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" }
@@ -5780,7 +5927,7 @@ function shellOut(cmd, args, cwd, stream = true) {
5780
5927
  }
5781
5928
  function isOnPath(bin) {
5782
5929
  try {
5783
- execFileSync21("which", [bin], { stdio: "pipe" });
5930
+ execFileSync22("which", [bin], { stdio: "pipe" });
5784
5931
  return true;
5785
5932
  } catch {
5786
5933
  return false;
@@ -5814,7 +5961,7 @@ function installLitellmIfNeeded(cwd) {
5814
5961
  } catch {
5815
5962
  }
5816
5963
  try {
5817
- execFileSync21("python3", ["-c", "import litellm"], { stdio: "pipe" });
5964
+ execFileSync22("python3", ["-c", "import litellm"], { stdio: "pipe" });
5818
5965
  process.stdout.write("\u2192 kody: litellm already installed\n");
5819
5966
  return 0;
5820
5967
  } catch {
@@ -5824,16 +5971,16 @@ function installLitellmIfNeeded(cwd) {
5824
5971
  }
5825
5972
  function configureGitIdentity(cwd) {
5826
5973
  try {
5827
- const name = execFileSync21("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
5974
+ const name = execFileSync22("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
5828
5975
  if (name) return;
5829
5976
  } catch {
5830
5977
  }
5831
5978
  try {
5832
- execFileSync21("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
5979
+ execFileSync22("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
5833
5980
  } catch {
5834
5981
  }
5835
5982
  try {
5836
- execFileSync21("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
5983
+ execFileSync22("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
5837
5984
  cwd,
5838
5985
  stdio: "pipe"
5839
5986
  });
@@ -6015,9 +6162,9 @@ function commitChatFiles(cwd, sessionId, verbose) {
6015
6162
  if (paths.length === 0) return;
6016
6163
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
6017
6164
  try {
6018
- execFileSync22("git", ["add", ...paths], opts);
6019
- execFileSync22("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
6020
- execFileSync22("git", ["push", "--quiet", "origin", "HEAD"], opts);
6165
+ execFileSync23("git", ["add", ...paths], opts);
6166
+ execFileSync23("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
6167
+ execFileSync23("git", ["push", "--quiet", "origin", "HEAD"], opts);
6021
6168
  } catch (err) {
6022
6169
  const msg = err instanceof Error ? err.message : String(err);
6023
6170
  process.stderr.write(`[kody:chat] commit/push skipped: ${msg}
@@ -62,13 +62,24 @@
62
62
  { "script": "dispatch", "with": { "next": "release-deploy", "target": "issue" },
63
63
  "runWhen": { "data.taskState.core.lastOutcome.type": "RELEASE_PUBLISH_COMPLETED" } },
64
64
 
65
+ { "script": "waitForCi",
66
+ "with": { "timeoutMinutes": 30, "pollSeconds": 30, "initialWaitSeconds": 15, "maxFixCiAttempts": 3 },
67
+ "runWhen": { "data.taskState.core.lastOutcome.type": ["RELEASE_DEPLOY_COMPLETED", "FIX_CI_COMPLETED"] } },
68
+
69
+ { "script": "dispatch", "with": { "next": "fix-ci", "target": "pr" },
70
+ "runWhen": { "data.action.type": "CI_FAILED" } },
71
+
65
72
  { "script": "finishFlow",
66
73
  "with": { "reason": "release-completed", "label": "kody:done", "color": "0e8a16", "description": "kody: release complete" },
67
- "runWhen": { "data.taskState.core.lastOutcome.type": "RELEASE_DEPLOY_COMPLETED" } },
74
+ "runWhen": { "data.action.type": "CI_PASSED" } },
75
+
76
+ { "script": "finishFlow",
77
+ "with": { "reason": "release-failed", "label": "kody:failed", "color": "e11d21", "description": "kody: release flow failed" },
78
+ "runWhen": { "data.action.type": ["CI_GIVEUP", "CI_TIMEOUT"] } },
68
79
 
69
80
  { "script": "finishFlow",
70
81
  "with": { "reason": "release-failed", "label": "kody:failed", "color": "e11d21", "description": "kody: release flow failed" },
71
- "runWhen": { "data.taskState.core.lastOutcome.type": ["RELEASE_PREPARE_FAILED", "RELEASE_MERGE_FAILED", "RELEASE_PUBLISH_FAILED", "RELEASE_DEPLOY_FAILED"] } },
82
+ "runWhen": { "data.taskState.core.lastOutcome.type": ["RELEASE_PREPARE_FAILED", "RELEASE_MERGE_FAILED", "RELEASE_PUBLISH_FAILED", "RELEASE_DEPLOY_FAILED", "FIX_CI_FAILED"] } },
72
83
 
73
84
  { "script": "persistFlowState" }
74
85
  ]
@@ -74,13 +74,24 @@ existing=$(gh pr list --head "$default_branch" --base "$release_branch" --state
74
74
  | python3 -c 'import json,sys; data=json.load(sys.stdin); print(data[0]["url"] if data else "")' 2>/dev/null \
75
75
  || echo "")
76
76
 
77
+ # Hoisted so the kody-release-pr marker write below also runs in the
78
+ # reuse-existing-PR path.
79
+ issue_arg="${KODY_ARG_ISSUE:-}"
80
+
77
81
  if [[ -n "$existing" ]]; then
78
82
  echo " reusing existing deploy PR: ${existing}"
79
83
  pr_url="$existing"
80
84
  else
85
+ # Same Tracking-Issue marker as release-prepare — non-closing reference
86
+ # so the originating release issue stays open through the deploy step
87
+ # while the Kody Dashboard can still link this PR to the task for preview.
88
+ tracking_line=""
89
+ if [[ "$issue_arg" =~ ^[0-9]+$ && "$issue_arg" != "0" ]]; then
90
+ tracking_line=$'\n\nTracking-Issue: #'"${issue_arg}"
91
+ fi
81
92
  body="Automated deploy PR opened by kody — promotes \`${default_branch}\` to \`${release_branch}\` for release **v${version}**.
82
93
 
83
- Merge this PR to deploy v${version} to \`${release_branch}\`."
94
+ Merge this PR to deploy v${version} to \`${release_branch}\`.${tracking_line}"
84
95
  if ! pr_url=$(printf '%s' "$body" | gh pr create --head "$default_branch" --base "$release_branch" --title "deploy: ${default_branch} → ${release_branch} (v${version})" --body-file -); then
85
96
  echo "KODY_REASON=release deploy: gh pr create failed"
86
97
  echo "KODY_SKIP_AGENT=true"
@@ -94,6 +105,24 @@ if [[ -z "$pr_url" ]]; then
94
105
  exit 1
95
106
  fi
96
107
 
108
+ # Persist the deploy-PR marker on the originating issue body. Mirrors the
109
+ # release-prepare path — the issue body is owned by the orchestrator, so
110
+ # this signal survives any @kody fix that overwrites the PR body. The
111
+ # marker replaces the prepare-PR ref so the dashboard pivots to the deploy
112
+ # PR (the now-current task) automatically.
113
+ if [[ "${issue_arg:-}" =~ ^[0-9]+$ && "${issue_arg:-0}" != "0" ]]; then
114
+ pr_number="${pr_url##*/}"
115
+ if [[ "$pr_number" =~ ^[0-9]+$ ]]; then
116
+ cur_body=$(gh issue view "$issue_arg" --json body -q .body 2>/dev/null || echo "")
117
+ cleaned_body=$(printf '%s' "$cur_body" | sed -E '/<!-- kody-release-pr:[^>]*-->/d')
118
+ {
119
+ printf '%s' "$cleaned_body"
120
+ printf '\n\n<!-- kody-release-pr: #%s -->\n' "$pr_number"
121
+ } | gh issue edit "$issue_arg" --body-file - >/dev/null 2>&1 || \
122
+ echo "[kody release-deploy] WARN: failed to write kody-release-pr marker to issue #${issue_arg}"
123
+ fi
124
+ fi
125
+
97
126
  echo "RELEASE_DEPLOY_PR=${pr_url}"
98
127
  echo "KODY_PR_URL=${pr_url}"
99
128
 
@@ -313,7 +313,19 @@ _… truncated; see CHANGELOG.md_"
313
313
  else
314
314
  body_entry="$entry"
315
315
  fi
316
- body=$'Automated release PR opened by kody.\n\n'"$body_entry"$'\n\nThe release orchestrator will merge this into `'"${default_branch}"$'` and continue to publish + deploy.'
316
+ # Link the PR back to the originating release issue WITHOUT auto-closing
317
+ # it. The orchestrator continues to publish + deploy after merge using the
318
+ # issue as the dispatch target, so the issue must stay open for the
319
+ # dashboard to keep showing the task as in-progress until finishFlow
320
+ # applies kody:done. We use `Tracking-Issue: #N` (not `Closes #N`) — GitHub
321
+ # treats this as plain text, the dashboard parses it as a fallback link
322
+ # signal, and no auto-close fires on PR merge.
323
+ issue_arg="${KODY_ARG_ISSUE:-}"
324
+ tracking_line=""
325
+ if [[ "$issue_arg" =~ ^[0-9]+$ && "$issue_arg" != "0" ]]; then
326
+ tracking_line=$'\n\nTracking-Issue: #'"${issue_arg}"
327
+ fi
328
+ body=$'Automated release PR opened by kody.\n\n'"$body_entry"$'\n\nThe release orchestrator will merge this into `'"${default_branch}"$'` and continue to publish + deploy.'"${tracking_line}"
317
329
  pr_url=$(printf '%s' "$body" | gh pr create --head "$release_branch" --base "$default_branch" --title "chore: release ${tag}" --body-file -)
318
330
  fi
319
331
 
@@ -321,6 +333,24 @@ if [[ -z "$pr_url" ]]; then
321
333
  fail "release prepare: gh pr create returned empty URL" 4
322
334
  fi
323
335
 
336
+ # Persist a release-PR marker on the originating issue body so the Kody
337
+ # Dashboard can link the issue → PR even if @kody fix later overwrites the
338
+ # PR body. Idempotent: any existing marker line is stripped before append,
339
+ # so re-runs replace the previous PR ref. The issue body is owned by the
340
+ # orchestrator (no @kody fix touches it), so this signal is durable.
341
+ if [[ "${issue_arg:-}" =~ ^[0-9]+$ && "${issue_arg:-0}" != "0" ]]; then
342
+ pr_number="${pr_url##*/}"
343
+ if [[ "$pr_number" =~ ^[0-9]+$ ]]; then
344
+ cur_body=$(gh issue view "$issue_arg" --json body -q .body 2>/dev/null || echo "")
345
+ cleaned_body=$(printf '%s' "$cur_body" | sed -E '/<!-- kody-release-pr:[^>]*-->/d')
346
+ {
347
+ printf '%s' "$cleaned_body"
348
+ printf '\n\n<!-- kody-release-pr: #%s -->\n' "$pr_number"
349
+ } | gh issue edit "$issue_arg" --body-file - >/dev/null 2>&1 || \
350
+ echo "[kody release-prepare] WARN: failed to write kody-release-pr marker to issue #${issue_arg}"
351
+ fi
352
+ fi
353
+
324
354
  echo "RELEASE_PR=${pr_url}"
325
355
  echo "KODY_PR_URL=${pr_url}"
326
356
  echo "KODY_REASON=opened release PR for ${tag}"
@@ -57,17 +57,8 @@ documented in `AGENTS.md` / `CLAUDE.md` — reference those files by path
57
57
  cite it; don't copy it.
58
58
 
59
59
  ## Clarifying questions
60
- Numbered list. Each question must include a one-line "Why:" naming the
61
- concrete implementation decision the answer would unblock not "why it
62
- matters" in general, but which fork in the road it picks.
63
-
64
- Default to asking. Only skip this section when every entry in
65
- "Gaps & assumptions" is genuinely safe to proceed on without confirmation;
66
- if you skip, justify it in one line under the heading. Prefer a few sharp
67
- questions over many soft ones — a question that wouldn't change the
68
- implementation is noise. Address the questions to the issue author / a
69
- human reviewer; they'll be answered in a follow-up comment and picked up
70
- on the next research run via delta mode.
60
+ Numbered list. Each question must include a one-line "Why:" explaining why
61
+ the answer changes the implementation. Skip if there are genuinely none.
71
62
 
72
63
  ## Gaps & assumptions
73
64
  What is unknown, and — for each gap — what assumption the implementer would
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.3.29",
3
+ "version": "0.3.32",
4
4
  "description": "kody — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
5
5
  "license": "MIT",
6
6
  "type": "module",