@kody-ade/kody-engine 0.3.70 → 0.3.72

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.70",
6
+ version: "0.3.72",
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",
@@ -91,8 +91,26 @@ open_deploy_pr() {
91
91
  if [[ -n "$existing" ]]; then
92
92
  echo " reusing existing deploy PR: ${existing}" >&2
93
93
  pr_url="$existing"
94
- if ! printf '%s' "$body" | gh pr edit "$pr_url" --body-file - >/dev/null 2>&1; then
95
- echo "[deploy] WARN: failed to refresh deploy PR body" >&2
94
+ # Refresh body via REST API instead of `gh pr edit` gh's edit path
95
+ # uses GraphQL which requires read:org scope on KODY_TOKEN. REST PATCH
96
+ # works with plain `repo` scope.
97
+ local pr_num="${pr_url##*/}"
98
+ local owner="${KODY_CFG_GITHUB_OWNER:-}"
99
+ local repo="${KODY_CFG_GITHUB_REPO:-}"
100
+ if [[ -z "$owner" || -z "$repo" ]]; then
101
+ # Fall back to extracting from the URL if config missing.
102
+ local stripped="${pr_url#https://github.com/}"
103
+ owner="${stripped%%/*}"
104
+ repo=$(echo "$stripped" | cut -d/ -f2)
105
+ fi
106
+ local edit_err
107
+ local refreshed_title="deploy: ${default_branch} → ${release_branch} (v${new_version})"
108
+ if ! edit_err=$(gh api --method PATCH "repos/${owner}/${repo}/pulls/${pr_num}" \
109
+ -f title="$refreshed_title" \
110
+ -f body="$body" 2>&1 >/dev/null); then
111
+ echo "[deploy] WARN: failed to refresh deploy PR title+body for ${pr_url}: ${edit_err}" >&2
112
+ else
113
+ echo " refreshed deploy PR title + body via REST" >&2
96
114
  fi
97
115
  else
98
116
  if ! pr_url=$(printf '%s' "$body" | gh pr create --head "$default_branch" --base "$release_branch" --title "deploy: ${default_branch} → ${release_branch} (v${new_version})" --body-file -); then
@@ -107,12 +107,18 @@ generate_changelog() {
107
107
 
108
108
  format_changelog() {
109
109
  local new_version="$1"
110
- local date_str
110
+ local date_str raw
111
111
  date_str=$(date -u +%Y-%m-%d)
112
- python3 - "$new_version" "$date_str" <<'PY'
113
- import sys, re
114
- new_version, date_str = sys.argv[1], sys.argv[2]
115
- raw = sys.stdin.read()
112
+ # Capture stdin BEFORE invoking python — the python heredoc below
113
+ # redirects sys.stdin to the heredoc text itself, so we can't rely on
114
+ # `sys.stdin.read()` to see the piped `subject||sha` lines. Pass the
115
+ # raw log through an env var instead.
116
+ raw=$(cat)
117
+ RAW_CHANGELOG="$raw" NEW_VER="$new_version" DATE_STR="$date_str" python3 - <<'PY'
118
+ import os, re, sys
119
+ new_version = os.environ["NEW_VER"]
120
+ date_str = os.environ["DATE_STR"]
121
+ raw = os.environ.get("RAW_CHANGELOG", "")
116
122
  buckets = {k: [] for k in ("feat", "fix", "perf", "refactor", "docs", "chore", "other")}
117
123
  for line in raw.splitlines():
118
124
  line = line.strip()
@@ -64,7 +64,7 @@
64
64
  },
65
65
  { "script": "loadIssueContext" },
66
66
  { "script": "loadTaskState" },
67
- { "shell": "release.sh" },
67
+ { "shell": "release.sh", "timeoutSec": 5400 },
68
68
  { "script": "skipAgent" }
69
69
  ],
70
70
  "postflight": [
@@ -15,14 +15,32 @@ tag_and_publish() {
15
15
  local timeout_s=$((timeout_ms / 1000))
16
16
  local tag="v${new_version}"
17
17
 
18
- # Refuse if the tag already exists locally (left over from a prior failed run).
19
- if git rev-parse --verify "$tag" >/dev/null 2>&1; then
20
- echo "[publish] tag ${tag} already exists locally" >&2
21
- return 1
18
+ # Idempotent tagging: if the tag already exists and points at HEAD,
19
+ # treat it as already-published. If it points elsewhere, fail loudly —
20
+ # something is inconsistent and a human should look.
21
+ local remote_sha local_sha head_sha
22
+ head_sha=$(git rev-parse HEAD)
23
+ if local_sha=$(git rev-parse --verify "$tag" 2>/dev/null); then
24
+ if [[ "$local_sha" == "$head_sha" ]]; then
25
+ echo " tag ${tag} already exists locally at HEAD — skipping create" >&2
26
+ else
27
+ echo "[publish] tag ${tag} exists locally at ${local_sha} but HEAD is ${head_sha}" >&2
28
+ return 1
29
+ fi
30
+ else
31
+ git tag -a "$tag" -m "Release ${tag}"
32
+ fi
33
+ # Push the tag if it isn't already on origin (or push always; gh will
34
+ # no-op on existing remote tag at the same sha).
35
+ if remote_sha=$(git ls-remote --tags origin "refs/tags/${tag}" 2>/dev/null | awk '{print $1}'); then
36
+ if [[ -z "$remote_sha" ]]; then
37
+ git push origin "$tag"
38
+ elif [[ "$remote_sha" != "$head_sha" ]]; then
39
+ echo " WARN: remote tag ${tag} points at ${remote_sha}, HEAD is ${head_sha}" >&2
40
+ fi
41
+ else
42
+ git push origin "$tag" || true
22
43
  fi
23
-
24
- git tag -a "$tag" -m "Release ${tag}"
25
- git push origin "$tag"
26
44
 
27
45
  # publishCommand (optional). Failure here is recorded but does not abort —
28
46
  # we still want the GH release entry so the tag is discoverable.
@@ -48,6 +66,16 @@ create_gh_release() {
48
66
  local draft_flag=""
49
67
  [[ "$draft" == "true" ]] && draft_flag="--draft"
50
68
 
69
+ # Idempotent: if a release for this tag already exists, reuse it.
70
+ local existing_url
71
+ if existing_url=$(gh release view "$tag" --json url -q .url 2>/dev/null); then
72
+ if [[ -n "$existing_url" ]]; then
73
+ echo " GH release for ${tag} already exists: ${existing_url}" >&2
74
+ echo "$existing_url"
75
+ return 0
76
+ fi
77
+ fi
78
+
51
79
  local release_url=""
52
80
  if release_url=$(gh release create "$tag" --title "$tag" --notes "Release ${tag} — automated by kody." $draft_flag 2>&1); then
53
81
  echo "$release_url"
@@ -3,10 +3,12 @@
3
3
  # release/wait.sh — wait_for_ci function: poll a PR's check rollup
4
4
  # until all non-skipped checks pass, or timeout.
5
5
  #
6
- # Function: wait_for_ci <pr_number> <timeout_minutes>
7
- # Exits 0 on CI_PASSED, exits 1 on CI_FAILED/CI_TIMEOUT.
6
+ # Function: wait_for_ci <pr_number> <timeout_minutes> [poll_seconds] [initial_wait]
7
+ # Returns 0 on CI passed, 1 on CI failed/timeout.
8
8
  #
9
- # Reads gh pr checks <N> output. Treats SKIPPED as pass.
9
+ # Special-case: if a PR has zero registered checks (no CI configured),
10
+ # treats that as PASSED after a short stabilization window — so a Tester
11
+ # repo without checks doesn't loop forever.
10
12
 
11
13
  # shellcheck disable=SC2148
12
14
 
@@ -24,43 +26,50 @@ wait_for_ci() {
24
26
  local deadline=$(( $(date +%s) + timeout_minutes * 60 ))
25
27
  echo "→ wait_for_ci: PR #${pr_number}, timeout=${timeout_minutes}m"
26
28
 
27
- # Initial wait — gives GHA time to register checks.
28
29
  sleep "$initial_wait"
29
30
 
31
+ # Track consecutive "no checks" results so we don't bail prematurely
32
+ # on a PR that's just slow to register checks.
33
+ local empty_count=0
34
+ local empty_threshold=4 # ~2 minutes (4 * poll_seconds=30s) before treating as no-CI
35
+
30
36
  while (( $(date +%s) < deadline )); do
31
- # gh pr checks <N> --json prints an array of {state, name, ...}.
32
37
  local raw
33
- if ! raw=$(gh pr checks "$pr_number" --json state,name 2>/dev/null); then
34
- echo " [wait_for_ci] gh pr checks failed; retrying in ${poll_seconds}s" >&2
38
+ if ! raw=$(gh pr checks "$pr_number" --json state 2>/dev/null); then
39
+ # gh exits non-zero when there are no checks treat as no-CI.
40
+ empty_count=$((empty_count + 1))
41
+ echo " [wait_for_ci] gh pr checks returned non-zero (count=${empty_count})"
42
+ if (( empty_count >= empty_threshold )); then
43
+ echo "→ wait_for_ci: PR #${pr_number} has no CI checks configured — treating as passed"
44
+ return 0
45
+ fi
35
46
  sleep "$poll_seconds"
36
47
  continue
37
48
  fi
38
49
 
39
- # Tally states. Anything that's still PENDING/IN_PROGRESS/QUEUED → keep waiting.
40
- # Anything FAILURE/CANCELLED/TIMED_OUT/ACTION_REQUIRED → fail fast.
41
- # SUCCESS/SKIPPED/NEUTRAL pass.
42
- local summary
43
- summary=$(printf '%s' "$raw" | python3 -c '
44
- import json, sys
45
- data = json.load(sys.stdin)
46
- buckets = {"pending": 0, "passed": 0, "failed": 0, "failed_names": []}
47
- for r in data:
48
- s = (r.get("state") or "").upper()
49
- name = r.get("name") or "?"
50
- if s in ("PENDING", "IN_PROGRESS", "QUEUED", ""):
51
- buckets["pending"] += 1
52
- elif s in ("FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE"):
53
- buckets["failed"] += 1
54
- buckets["failed_names"].append(name)
55
- else: # SUCCESS, SKIPPED, NEUTRAL, etc.
56
- buckets["passed"] += 1
57
- print(f"{buckets[\"pending\"]}|{buckets[\"passed\"]}|{buckets[\"failed\"]}|{','.join(buckets[\"failed_names\"][:5])}")
58
- ')
50
+ # Empty array? Same path.
51
+ local total
52
+ total=$(printf '%s' "$raw" | jq 'length' 2>/dev/null || echo "0")
53
+ if [[ "$total" == "0" ]]; then
54
+ empty_count=$((empty_count + 1))
55
+ echo " [wait_for_ci] no checks registered yet (count=${empty_count})"
56
+ if (( empty_count >= empty_threshold )); then
57
+ echo "→ wait_for_ci: PR #${pr_number} has no CI checks configured — treating as passed"
58
+ return 0
59
+ fi
60
+ sleep "$poll_seconds"
61
+ continue
62
+ fi
63
+ empty_count=0
59
64
 
60
- local pending passed failed failed_names
61
- IFS='|' read -r pending passed failed failed_names <<< "$summary"
65
+ # Tally states via jq.
66
+ local pending failed passed failed_names
67
+ pending=$(printf '%s' "$raw" | jq '[.[] | select((.state // "") | IN("PENDING","IN_PROGRESS","QUEUED",""))] | length')
68
+ failed=$(printf '%s' "$raw" | jq '[.[] | select((.state // "") | IN("FAILURE","CANCELLED","TIMED_OUT","ACTION_REQUIRED","STARTUP_FAILURE"))] | length')
69
+ passed=$(printf '%s' "$raw" | jq '[.[] | select((.state // "") | IN("SUCCESS","SKIPPED","NEUTRAL"))] | length')
70
+ failed_names=$(printf '%s' "$raw" | jq -r '[.[] | select((.state // "") | IN("FAILURE","CANCELLED","TIMED_OUT","ACTION_REQUIRED","STARTUP_FAILURE")) | .name] | join(",")')
62
71
 
63
- echo " [wait_for_ci] pending=${pending} passed=${passed} failed=${failed}"
72
+ echo " [wait_for_ci] pending=${pending} passed=${passed} failed=${failed} (total=${total})"
64
73
 
65
74
  if [[ "$failed" -gt 0 ]]; then
66
75
  echo "[wait_for_ci] CI failed on PR #${pr_number}: ${failed_names}" >&2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.3.70",
3
+ "version": "0.3.72",
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",