@kody-ade/kody-engine 0.3.70-beta.1 → 0.3.70-beta.3

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-beta.1",
6
+ version: "0.3.70-beta.3",
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",
@@ -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-beta.1",
3
+ "version": "0.3.70-beta.3",
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",