@kody-ade/kody-engine 0.4.155 → 0.4.157

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
@@ -1061,7 +1061,7 @@ var init_loadPriorArt = __esm({
1061
1061
  // package.json
1062
1062
  var package_default = {
1063
1063
  name: "@kody-ade/kody-engine",
1064
- version: "0.4.155",
1064
+ version: "0.4.157",
1065
1065
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
1066
1066
  license: "MIT",
1067
1067
  type: "module",
@@ -2935,7 +2935,7 @@ function autoDispatch(opts) {
2935
2935
  const authorLogin = String(event.comment?.user?.login ?? "");
2936
2936
  const authorType = String(event.comment?.user?.type ?? "");
2937
2937
  if (!rawBody.toLowerCase().includes("@kody")) return null;
2938
- if (authorLogin === "kody-bot" || authorType === "Bot") return null;
2938
+ const isBotAuthor = authorLogin === "kody-bot" || authorType === "Bot";
2939
2939
  if (!associationAllowed(event, opts?.config)) return null;
2940
2940
  const body = rawBody.toLowerCase();
2941
2941
  const targetNum = Number(event.issue?.number ?? 0);
@@ -2962,6 +2962,13 @@ function autoDispatch(opts) {
2962
2962
  if (!executable && !firstToken) {
2963
2963
  executable = isPr ? opts?.config?.defaultPrExecutable ?? "fix" : opts?.config?.defaultExecutable ?? null;
2964
2964
  }
2965
+ if (isBotAuthor && !consumedFirstToken) {
2966
+ process.stderr.write(
2967
+ `[kody] dispatch: ignoring bot comment without an explicit command (author=${authorLogin || authorType}, firstToken=${firstToken ?? "<none>"})
2968
+ `
2969
+ );
2970
+ return null;
2971
+ }
2965
2972
  if (!executable) {
2966
2973
  const profileMissing = aliased ? getProfileInputs(aliased) === null : true;
2967
2974
  process.stderr.write(
@@ -4562,40 +4569,73 @@ function writeTaskState(target, number, state, cwd) {
4562
4569
 
4563
4570
  // src/scripts/advanceFlow.ts
4564
4571
  var API_TIMEOUT_MS3 = 3e4;
4572
+ var FLOW_HOP_CAP = 25;
4573
+ function ghComment(issueNumber, body, cwd, label) {
4574
+ try {
4575
+ execFileSync8("gh", ["issue", "comment", String(issueNumber), "--body", body], {
4576
+ timeout: API_TIMEOUT_MS3,
4577
+ cwd,
4578
+ stdio: ["ignore", "pipe", "pipe"]
4579
+ });
4580
+ } catch (err) {
4581
+ process.stderr.write(
4582
+ `[kody advanceFlow] ${label} on issue #${issueNumber} failed: ${err instanceof Error ? err.message : String(err)}
4583
+ `
4584
+ );
4585
+ }
4586
+ }
4565
4587
  var advanceFlow = async (ctx, profile) => {
4566
4588
  const state = ctx.data.taskState;
4567
4589
  const flow = state?.flow;
4568
4590
  if (!flow?.issueNumber) return;
4591
+ const curState = state;
4592
+ let issueState;
4593
+ try {
4594
+ issueState = readTaskState("issue", flow.issueNumber, ctx.cwd);
4595
+ } catch {
4596
+ issueState = curState;
4597
+ }
4569
4598
  const targetType = ctx.data.commentTargetType;
4570
4599
  const action = ctx.data.action;
4600
+ let nextIssueState = issueState;
4571
4601
  if (targetType === "pr" && action) {
4602
+ nextIssueState = reduce(issueState, profile.name, action, profile.phase);
4603
+ if (state?.core.prUrl && !nextIssueState.core.prUrl) nextIssueState.core.prUrl = state.core.prUrl;
4604
+ }
4605
+ const prevHops = issueState.flow?.hops ?? flow.hops ?? 0;
4606
+ const hops = prevHops + 1;
4607
+ if (hops > FLOW_HOP_CAP) {
4608
+ nextIssueState.flow = void 0;
4572
4609
  try {
4573
- const issueState = readTaskState("issue", flow.issueNumber, ctx.cwd);
4574
- issueState.flow = flow;
4575
- const next = reduce(issueState, profile.name, action, profile.phase);
4576
- if (state?.core.prUrl && !next.core.prUrl) next.core.prUrl = state.core.prUrl;
4577
- next.flow = flow;
4578
- writeTaskState("issue", flow.issueNumber, next, ctx.cwd);
4610
+ writeTaskState("issue", flow.issueNumber, nextIssueState, ctx.cwd);
4579
4611
  } catch (err) {
4580
4612
  process.stderr.write(
4581
- `[kody advanceFlow] failed to mirror action to issue #${flow.issueNumber}: ${err instanceof Error ? err.message : String(err)}
4613
+ `[kody advanceFlow] failed to clear looping flow on issue #${flow.issueNumber}: ${err instanceof Error ? err.message : String(err)}
4582
4614
  `
4583
4615
  );
4584
4616
  }
4617
+ ghComment(
4618
+ flow.issueNumber,
4619
+ `\u26A0\uFE0F kody: flow \`${flow.name}\` stopped after ${FLOW_HOP_CAP} steps without completing (loop guard). Re-trigger manually if this was intended.`,
4620
+ ctx.cwd,
4621
+ "loop-guard notice"
4622
+ );
4623
+ process.stderr.write(
4624
+ `[kody advanceFlow] flow '${flow.name}' on issue #${flow.issueNumber} hit hop cap ${FLOW_HOP_CAP}; stopping
4625
+ `
4626
+ );
4627
+ return;
4585
4628
  }
4586
- const body = `@kody ${flow.name}`;
4629
+ nextIssueState.flow = { ...flow, hops };
4587
4630
  try {
4588
- execFileSync8("gh", ["issue", "comment", String(flow.issueNumber), "--body", body], {
4589
- timeout: API_TIMEOUT_MS3,
4590
- cwd: ctx.cwd,
4591
- stdio: ["ignore", "pipe", "pipe"]
4592
- });
4631
+ writeTaskState("issue", flow.issueNumber, nextIssueState, ctx.cwd);
4593
4632
  } catch (err) {
4594
4633
  process.stderr.write(
4595
- `[kody advanceFlow] failed to re-trigger orchestrator on issue #${flow.issueNumber}: ${err instanceof Error ? err.message : String(err)}
4634
+ `[kody advanceFlow] failed to persist hop count on issue #${flow.issueNumber}: ${err instanceof Error ? err.message : String(err)}
4596
4635
  `
4597
4636
  );
4598
4637
  }
4638
+ ghComment(flow.issueNumber, `@kody ${flow.name}`, ctx.cwd, "re-trigger orchestrator");
4599
4639
  };
4600
4640
 
4601
4641
  // src/scripts/brainServe.ts
@@ -12124,7 +12164,8 @@ var startFlow = async (ctx, profile, _agentResult, args) => {
12124
12164
  name: flowName,
12125
12165
  step: entry,
12126
12166
  issueNumber,
12127
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
12167
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
12168
+ hops: 0
12128
12169
  };
12129
12170
  }
12130
12171
  postKodyComment(target, issueNumber, state, entry, ctx.cwd);
@@ -82,6 +82,18 @@ open_deploy_pr() {
82
82
  local body
83
83
  body=$(build_pr_body "$new_version" "$changelog_section" "$default_branch" "$release_branch" "$issue_arg")
84
84
 
85
+ # GitHub rejects a PR body over 65536 chars (GraphQL createPullRequest).
86
+ # A large accumulated CHANGELOG section can blow past it, so clamp: drop the
87
+ # changelog to a budget and rebuild, then hard-truncate as a final guard.
88
+ local body_max=65000
89
+ if (( ${#body} > body_max )); then
90
+ echo "[deploy] PR body ${#body} chars > ${body_max} — truncating changelog" >&2
91
+ local budget=$(( body_max - 2000 ))
92
+ changelog_section="${changelog_section:0:budget}"$'\n\n_…changelog truncated; see CHANGELOG.md on the branch._'
93
+ body=$(build_pr_body "$new_version" "$changelog_section" "$default_branch" "$release_branch" "$issue_arg")
94
+ (( ${#body} > body_max )) && body="${body:0:body_max}"
95
+ fi
96
+
85
97
  # Idempotency: reuse an open PR for this branch pair if one exists.
86
98
  local existing pr_url
87
99
  existing=$(gh pr list --head "$default_branch" --base "$release_branch" --state open --json url --limit 1 2>/dev/null \
@@ -135,8 +135,22 @@ if [[ "$publish_status" == "failed" ]]; then
135
135
  fi
136
136
 
137
137
  # ── 5. Deploy PR (default → release branch) ───────────────────────────────
138
+ # Distinguish three outcomes: rc!=0 is a real failure (do NOT mask as no-op);
139
+ # rc==0 + empty URL is a genuine single-branch no-op; rc==0 + URL is success.
138
140
  current_step="deploy"
139
- deploy_pr_url=$(open_deploy_pr "$new_version" "$issue" || echo "")
141
+ set +e
142
+ deploy_pr_url=$(open_deploy_pr "$new_version" "$issue")
143
+ deploy_rc=$?
144
+ set -e
145
+ release_branch="${KODY_CFG_RELEASE_RELEASEBRANCH:-}"
146
+ if [[ "$deploy_rc" -ne 0 ]]; then
147
+ echo "[release] deploy step failed (rc=${deploy_rc}) — published v${new_version} but the ${default_branch}→${release_branch} promotion PR was not opened" >&2
148
+ echo "KODY_REASON=release v${new_version}: published, but the ${default_branch}→${release_branch} deploy PR failed"
149
+ echo "RELEASE_TAG=${tag}"
150
+ [[ -n "$release_url" ]] && echo "RELEASE_URL=${release_url}"
151
+ echo "RELEASE_FAILED=true"
152
+ exit 1
153
+ fi
140
154
  if [[ -z "$deploy_pr_url" ]]; then
141
155
  echo " (deploy: no-op — single-branch repo)"
142
156
  else
@@ -142,6 +142,18 @@ if [[ "$issue_arg" =~ ^[0-9]+$ && "$issue_arg" != "0" ]]; then
142
142
  fi
143
143
  body=$(build_pr_body "$tracking_line")
144
144
 
145
+ # GitHub rejects a PR body over 65536 chars (GraphQL createPullRequest). A
146
+ # large accumulated CHANGELOG section can blow past it, so clamp: drop the
147
+ # changelog to a budget and rebuild, then hard-truncate as a final guard.
148
+ body_max=65000
149
+ if (( ${#body} > body_max )); then
150
+ echo "[kody release-deploy] PR body ${#body} chars > ${body_max} — truncating changelog" >&2
151
+ budget=$(( body_max - 2000 ))
152
+ changelog_section="${changelog_section:0:budget}"$'\n\n_…changelog truncated; see CHANGELOG.md on the branch._'
153
+ body=$(build_pr_body "$tracking_line")
154
+ (( ${#body} > body_max )) && body="${body:0:body_max}"
155
+ fi
156
+
145
157
  if [[ -n "$existing" ]]; then
146
158
  echo " reusing existing deploy PR: ${existing}"
147
159
  pr_url="$existing"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.155",
3
+ "version": "0.4.157",
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",