@kody-ade/kody-engine 0.4.224 → 0.4.225
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 +2 -2
- package/dist/bin/kody.js +779 -831
- package/dist/executables/run/profile.json +1 -1
- package/dist/executables/types.ts +3 -1
- package/package.json +1 -1
- package/dist/duties/duty-scheduler/duty.md +0 -3
- package/dist/duties/duty-scheduler/profile.json +0 -6
- package/dist/duties/duty-tick/duty.md +0 -3
- package/dist/duties/duty-tick/profile.json +0 -6
- package/dist/duties/duty-tick-scripted/duty.md +0 -3
- package/dist/duties/duty-tick-scripted/profile.json +0 -6
- package/dist/duties/fix/duty.md +0 -10
- package/dist/duties/fix/profile.json +0 -6
- package/dist/duties/fix-ci/duty.md +0 -10
- package/dist/duties/fix-ci/profile.json +0 -6
- package/dist/duties/goal-scheduler/duty.md +0 -3
- package/dist/duties/goal-scheduler/profile.json +0 -6
- package/dist/duties/goal-tick/duty.md +0 -3
- package/dist/duties/goal-tick/profile.json +0 -6
- package/dist/duties/init/duty.md +0 -10
- package/dist/duties/init/profile.json +0 -6
- package/dist/duties/job-live-verify/duty.md +0 -3
- package/dist/duties/job-live-verify/profile.json +0 -6
- package/dist/duties/merge/duty.md +0 -10
- package/dist/duties/merge/profile.json +0 -6
- package/dist/duties/npm-publish/duty.md +0 -17
- package/dist/duties/npm-publish/profile.json +0 -6
- package/dist/duties/plan-verify/duty.md +0 -3
- package/dist/duties/plan-verify/profile.json +0 -6
- package/dist/duties/preview-build/duty.md +0 -10
- package/dist/duties/preview-build/profile.json +0 -6
- package/dist/duties/probe-skill/duty.md +0 -3
- package/dist/duties/probe-skill/profile.json +0 -6
- package/dist/duties/qa-goal/duty.md +0 -3
- package/dist/duties/qa-goal/profile.json +0 -6
- package/dist/duties/release/duty.md +0 -10
- package/dist/duties/release/profile.json +0 -6
- package/dist/duties/release-deploy/duty.md +0 -10
- package/dist/duties/release-deploy/profile.json +0 -6
- package/dist/duties/release-prepare/duty.md +0 -10
- package/dist/duties/release-prepare/profile.json +0 -6
- package/dist/duties/release-publish/duty.md +0 -10
- package/dist/duties/release-publish/profile.json +0 -6
- package/dist/duties/resolve/duty.md +0 -10
- package/dist/duties/resolve/profile.json +0 -6
- package/dist/duties/revert/duty.md +0 -10
- package/dist/duties/revert/profile.json +0 -6
- package/dist/duties/sync/duty.md +0 -10
- package/dist/duties/sync/profile.json +0 -6
- package/dist/duties/task-job-fail-once/duty.md +0 -3
- package/dist/duties/task-job-fail-once/profile.json +0 -6
- package/dist/duties/task-job-pass-a/duty.md +0 -3
- package/dist/duties/task-job-pass-a/profile.json +0 -6
- package/dist/duties/task-job-pass-b/duty.md +0 -3
- package/dist/duties/task-job-pass-b/profile.json +0 -6
- package/dist/duties/task-jobs/duty.md +0 -3
- package/dist/duties/task-jobs/profile.json +0 -6
- package/dist/duties/worker-ask/duty.md +0 -10
- package/dist/duties/worker-ask/profile.json +0 -6
- package/dist/executables/duty-scheduler/profile.json +0 -57
- package/dist/executables/duty-tick/profile.json +0 -78
- package/dist/executables/duty-tick/prompt.md +0 -77
- package/dist/executables/duty-tick/prompts/locked.md +0 -50
- package/dist/executables/duty-tick-scripted/profile.json +0 -72
- package/dist/executables/fix/profile.json +0 -91
- package/dist/executables/fix/prompt.md +0 -90
- package/dist/executables/fix-ci/profile.json +0 -71
- package/dist/executables/fix-ci/prompt.md +0 -78
- package/dist/executables/goal-scheduler/profile.json +0 -53
- package/dist/executables/goal-scheduler/scheduler.sh +0 -82
- package/dist/executables/goal-tick/profile.json +0 -69
- package/dist/executables/init/profile.json +0 -36
- package/dist/executables/job-live-verify/profile.json +0 -36
- package/dist/executables/job-live-verify/prompt.md +0 -42
- package/dist/executables/merge/profile.json +0 -54
- package/dist/executables/npm-publish/profile.json +0 -73
- package/dist/executables/npm-publish/publish.sh +0 -93
- package/dist/executables/plan-verify/profile.json +0 -38
- package/dist/executables/plan-verify/prompt.md +0 -72
- package/dist/executables/preview-build/profile.json +0 -61
- package/dist/executables/preview-health/profile.json +0 -48
- package/dist/executables/preview-health/tick.sh +0 -319
- package/dist/executables/probe-skill/profile.json +0 -35
- package/dist/executables/probe-skill/prompt.md +0 -31
- package/dist/executables/probe-skill/skills/probe-skill-marker/SKILL.md +0 -18
- package/dist/executables/qa-goal/profile.json +0 -65
- package/dist/executables/release/deploy.sh +0 -157
- package/dist/executables/release/prepare.sh +0 -353
- package/dist/executables/release/profile.json +0 -95
- package/dist/executables/release/publish.sh +0 -88
- package/dist/executables/release/release.sh +0 -230
- package/dist/executables/release/wait.sh +0 -88
- package/dist/executables/release-deploy/deploy.sh +0 -222
- package/dist/executables/release-deploy/profile.json +0 -53
- package/dist/executables/release-prepare/prepare.sh +0 -368
- package/dist/executables/release-prepare/profile.json +0 -70
- package/dist/executables/release-publish/profile.json +0 -53
- package/dist/executables/release-publish/publish.sh +0 -137
- package/dist/executables/resolve/apply-prefer.sh +0 -54
- package/dist/executables/resolve/profile.json +0 -107
- package/dist/executables/resolve/prompt.md +0 -51
- package/dist/executables/revert/profile.json +0 -82
- package/dist/executables/revert/revert.sh +0 -33
- package/dist/executables/sync/profile.json +0 -48
- package/dist/executables/task-job-fail-once/profile.json +0 -44
- package/dist/executables/task-job-fail-once/prompt.md +0 -1
- package/dist/executables/task-job-pass-a/profile.json +0 -43
- package/dist/executables/task-job-pass-a/prompt.md +0 -1
- package/dist/executables/task-job-pass-b/profile.json +0 -43
- package/dist/executables/task-job-pass-b/prompt.md +0 -1
- package/dist/executables/task-jobs/profile.json +0 -41
- package/dist/executables/task-jobs/prompt.md +0 -1
- package/dist/executables/worker-ask/profile.json +0 -70
- package/dist/executables/worker-ask/prompt.md +0 -60
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "preview-health",
|
|
3
|
-
"role": "watch",
|
|
4
|
-
"kind": "oneshot",
|
|
5
|
-
"describe": "Scan open PRs and dispatch mechanical preview repairs.",
|
|
6
|
-
"inputs": [],
|
|
7
|
-
"claudeCode": {
|
|
8
|
-
"model": "inherit",
|
|
9
|
-
"permissionMode": "default",
|
|
10
|
-
"maxTurns": 0,
|
|
11
|
-
"maxThinkingTokens": null,
|
|
12
|
-
"systemPromptAppend": null,
|
|
13
|
-
"tools": [],
|
|
14
|
-
"hooks": [],
|
|
15
|
-
"skills": [],
|
|
16
|
-
"commands": [],
|
|
17
|
-
"subagents": [],
|
|
18
|
-
"plugins": [],
|
|
19
|
-
"mcpServers": []
|
|
20
|
-
},
|
|
21
|
-
"cliTools": [
|
|
22
|
-
{
|
|
23
|
-
"name": "gh",
|
|
24
|
-
"install": {
|
|
25
|
-
"required": true,
|
|
26
|
-
"checkCommand": "command -v gh"
|
|
27
|
-
},
|
|
28
|
-
"verify": "gh auth status",
|
|
29
|
-
"usage": "Reads open PRs, reads the CTO trust ledger, dispatches Kody workflows, and posts PR audit/recommendation comments.",
|
|
30
|
-
"allowedUses": ["pr", "api", "issue", "workflow"]
|
|
31
|
-
}
|
|
32
|
-
],
|
|
33
|
-
"inputArtifacts": [],
|
|
34
|
-
"outputArtifacts": [],
|
|
35
|
-
"scripts": {
|
|
36
|
-
"preflight": [
|
|
37
|
-
{
|
|
38
|
-
"shell": "tick.sh",
|
|
39
|
-
"with": {
|
|
40
|
-
"jobsDir": ".kody/duties",
|
|
41
|
-
"duty": "preview-health"
|
|
42
|
-
},
|
|
43
|
-
"timeoutSec": 300
|
|
44
|
-
}
|
|
45
|
-
],
|
|
46
|
-
"postflight": []
|
|
47
|
-
}
|
|
48
|
-
}
|
|
@@ -1,319 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
jobs_dir="${1:-.kody/duties}"
|
|
5
|
-
duty="${2:-preview-health}"
|
|
6
|
-
|
|
7
|
-
export KODY_PREVIEW_HEALTH_JOBS_DIR="$jobs_dir"
|
|
8
|
-
export KODY_PREVIEW_HEALTH_DUTY="$duty"
|
|
9
|
-
|
|
10
|
-
python3 <<'PY'
|
|
11
|
-
import base64
|
|
12
|
-
import json
|
|
13
|
-
import os
|
|
14
|
-
import re
|
|
15
|
-
import subprocess
|
|
16
|
-
import sys
|
|
17
|
-
from datetime import datetime, timezone
|
|
18
|
-
|
|
19
|
-
DECISIONS_LABEL = "kody:cto-decisions"
|
|
20
|
-
LEDGER_START = "<!-- kody-cto-decisions:start -->"
|
|
21
|
-
LEDGER_END = "<!-- kody-cto-decisions:end -->"
|
|
22
|
-
VERBS = ("fix-ci", "sync", "resolve")
|
|
23
|
-
AUTO_VERBS = {"resolve"}
|
|
24
|
-
MAX_ACTIONS_PER_TICK = 5
|
|
25
|
-
FAIL_CONCLUSIONS = {"FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE"}
|
|
26
|
-
RUNNING_STATUSES = {"IN_PROGRESS", "QUEUED"}
|
|
27
|
-
STALE_THRESHOLD = 10
|
|
28
|
-
STATE_BRANCH = "kody-state"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def log(message):
|
|
32
|
-
print(f"[preview-health] {message}", file=sys.stderr)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def now_iso():
|
|
36
|
-
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def run_gh(args, input_text=None, check=True):
|
|
40
|
-
result = subprocess.run(
|
|
41
|
-
["gh", *args],
|
|
42
|
-
input=input_text,
|
|
43
|
-
text=True,
|
|
44
|
-
stdout=subprocess.PIPE,
|
|
45
|
-
stderr=subprocess.PIPE,
|
|
46
|
-
cwd=os.getcwd(),
|
|
47
|
-
)
|
|
48
|
-
if check and result.returncode != 0:
|
|
49
|
-
raise RuntimeError(result.stderr.strip() or result.stdout.strip() or f"gh {' '.join(args)} failed")
|
|
50
|
-
return result.stdout
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def repo_slug():
|
|
54
|
-
owner = os.environ.get("KODY_CFG_GITHUB_OWNER", "").strip()
|
|
55
|
-
repo = os.environ.get("KODY_CFG_GITHUB_REPO", "").strip()
|
|
56
|
-
if owner and repo:
|
|
57
|
-
return f"{owner}/{repo}"
|
|
58
|
-
return run_gh(["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"]).strip()
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def default_branch(slug):
|
|
62
|
-
configured = os.environ.get("KODY_CFG_GIT_DEFAULTBRANCH", "").strip()
|
|
63
|
-
if configured:
|
|
64
|
-
return configured
|
|
65
|
-
return run_gh(["repo", "view", slug, "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name"]).strip()
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def ensure_state_branch(slug):
|
|
69
|
-
if run_gh(["api", f"/repos/{slug}/git/ref/heads/{STATE_BRANCH}"], check=False).strip():
|
|
70
|
-
return
|
|
71
|
-
branch = default_branch(slug)
|
|
72
|
-
sha = run_gh(["api", f"/repos/{slug}/git/ref/heads/{branch}", "--jq", ".object.sha"]).strip()
|
|
73
|
-
payload = json.dumps({"ref": f"refs/heads/{STATE_BRANCH}", "sha": sha})
|
|
74
|
-
run_gh(["api", "--method", "POST", f"/repos/{slug}/git/refs", "--input", "-"], input_text=payload)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def initial_state():
|
|
78
|
-
return {"version": 1, "rev": 0, "cursor": "seed", "data": {}, "done": False}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def load_state(slug, path):
|
|
82
|
-
result = run_gh(
|
|
83
|
-
["api", f"/repos/{slug}/contents/{path}?ref={STATE_BRANCH}"],
|
|
84
|
-
check=False,
|
|
85
|
-
)
|
|
86
|
-
if not result.strip():
|
|
87
|
-
return {"path": path, "sha": None, "state": initial_state(), "created": True}
|
|
88
|
-
obj = json.loads(result)
|
|
89
|
-
raw = base64.b64decode(obj["content"]).decode("utf-8")
|
|
90
|
-
return {"path": path, "sha": obj.get("sha"), "state": json.loads(raw), "created": False}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def state_unchanged(prev, next_state):
|
|
94
|
-
return (
|
|
95
|
-
prev.get("cursor") == next_state.get("cursor")
|
|
96
|
-
and prev.get("done") == next_state.get("done")
|
|
97
|
-
and prev.get("data") == next_state.get("data")
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def save_state(slug, loaded, next_state):
|
|
102
|
-
if not loaded["created"] and state_unchanged(loaded["state"], next_state):
|
|
103
|
-
return False
|
|
104
|
-
ensure_state_branch(slug)
|
|
105
|
-
body = json.dumps(next_state, indent=2) + "\n"
|
|
106
|
-
payload = {
|
|
107
|
-
"message": f"chore(jobs): update state for {os.environ['KODY_PREVIEW_HEALTH_DUTY']} (rev {next_state['rev']})",
|
|
108
|
-
"content": base64.b64encode(body.encode("utf-8")).decode("ascii"),
|
|
109
|
-
"branch": STATE_BRANCH,
|
|
110
|
-
}
|
|
111
|
-
if loaded.get("sha"):
|
|
112
|
-
payload["sha"] = loaded["sha"]
|
|
113
|
-
result = run_gh(
|
|
114
|
-
["api", "--method", "PUT", f"/repos/{slug}/contents/{loaded['path']}", "--input", "-"],
|
|
115
|
-
input_text=json.dumps(payload),
|
|
116
|
-
check=False,
|
|
117
|
-
)
|
|
118
|
-
if result.strip():
|
|
119
|
-
return True
|
|
120
|
-
reloaded = load_state(slug, loaded["path"])
|
|
121
|
-
if not reloaded["created"] and state_unchanged(reloaded["state"], next_state):
|
|
122
|
-
return False
|
|
123
|
-
if reloaded.get("sha"):
|
|
124
|
-
payload["sha"] = reloaded["sha"]
|
|
125
|
-
else:
|
|
126
|
-
payload.pop("sha", None)
|
|
127
|
-
run_gh(["api", "--method", "PUT", f"/repos/{slug}/contents/{loaded['path']}", "--input", "-"], input_text=json.dumps(payload))
|
|
128
|
-
return True
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def read_ledger_modes():
|
|
132
|
-
modes = {verb: "ask" for verb in VERBS}
|
|
133
|
-
try:
|
|
134
|
-
raw = run_gh(["issue", "list", "--label", DECISIONS_LABEL, "--state", "open", "--limit", "20", "--json", "number,body"])
|
|
135
|
-
issues = json.loads(raw or "[]")
|
|
136
|
-
if not issues:
|
|
137
|
-
return modes
|
|
138
|
-
body = sorted(issues, key=lambda x: x.get("number", 10**9))[0].get("body", "")
|
|
139
|
-
if LEDGER_START not in body or LEDGER_END not in body:
|
|
140
|
-
return modes
|
|
141
|
-
inner = body.split(LEDGER_START, 1)[1].split(LEDGER_END, 1)[0]
|
|
142
|
-
match = re.search(r"```(?:json)?\s*(\{[\s\S]*?\})\s*```", inner)
|
|
143
|
-
if not match:
|
|
144
|
-
return modes
|
|
145
|
-
ledger = json.loads(match.group(1))
|
|
146
|
-
cto = ledger.get("staff", {}).get("cto", {})
|
|
147
|
-
for verb in VERBS:
|
|
148
|
-
if cto.get(verb, {}).get("mode") == "auto":
|
|
149
|
-
modes[verb] = "auto"
|
|
150
|
-
except Exception as err:
|
|
151
|
-
log(f"ledger read failed; treating all verbs as ask: {err}")
|
|
152
|
-
return modes
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def ci_failing(rollup):
|
|
156
|
-
if not isinstance(rollup, list):
|
|
157
|
-
return False
|
|
158
|
-
has_fail = any(str(check.get("conclusion", "")) in FAIL_CONCLUSIONS for check in rollup if isinstance(check, dict))
|
|
159
|
-
running = any(str(check.get("status", "")) in RUNNING_STATUSES for check in rollup if isinstance(check, dict))
|
|
160
|
-
return has_fail and not running
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def behind_by(slug, base, head):
|
|
164
|
-
try:
|
|
165
|
-
raw = run_gh(["api", f"repos/{slug}/compare/{base}...{head}", "--jq", ".behind_by"])
|
|
166
|
-
return int(raw.strip() or "0")
|
|
167
|
-
except Exception as err:
|
|
168
|
-
log(f"compare {base}...{head} failed: {err}")
|
|
169
|
-
return 0
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def detect_repair(slug, pr):
|
|
173
|
-
mergeable = str(pr.get("mergeable") or "").upper()
|
|
174
|
-
if mergeable == "UNKNOWN":
|
|
175
|
-
return ("defer", f"PR #{pr['number']} mergeability still UNKNOWN; retry next tick.")
|
|
176
|
-
if mergeable == "CONFLICTING":
|
|
177
|
-
return ("resolve", f"PR #{pr['number']} has merge conflicts with `{pr['baseRefName']}`.")
|
|
178
|
-
if ci_failing(pr.get("statusCheckRollup")):
|
|
179
|
-
return ("fix-ci", f"PR #{pr['number']} has failing CI checks.")
|
|
180
|
-
drift = behind_by(slug, pr["baseRefName"], pr["headRefName"])
|
|
181
|
-
if drift >= STALE_THRESHOLD:
|
|
182
|
-
return ("sync", f"PR #{pr['number']}'s branch is {drift} commits behind `{pr['baseRefName']}`.")
|
|
183
|
-
return None
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def duty_operator(jobs_dir, duty):
|
|
187
|
-
try:
|
|
188
|
-
with open(os.path.join(os.getcwd(), jobs_dir, duty, "profile.json"), "r", encoding="utf-8") as f:
|
|
189
|
-
profile = json.load(f)
|
|
190
|
-
mentions = profile.get("mentions", [])
|
|
191
|
-
return mentions[0] if isinstance(mentions, list) and mentions else ""
|
|
192
|
-
except Exception:
|
|
193
|
-
return ""
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def post_comment(pr_number, body):
|
|
197
|
-
if os.environ.get("KODY_DRY_RUN") == "1":
|
|
198
|
-
log(f"[dry-run] would comment on #{pr_number}: {body.splitlines()[0]}")
|
|
199
|
-
return True
|
|
200
|
-
try:
|
|
201
|
-
run_gh(["pr", "comment", str(pr_number), "--body", body])
|
|
202
|
-
return True
|
|
203
|
-
except Exception as err:
|
|
204
|
-
log(f"comment failed on #{pr_number}: {err}")
|
|
205
|
-
return False
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def recommend(pr_number, verb, reason, operator):
|
|
209
|
-
mention = f"@{operator} " if operator else ""
|
|
210
|
-
body = (
|
|
211
|
-
f"{mention}**CTO recommendation** - `{verb}`\n\n"
|
|
212
|
-
f"{reason} Recommended action: `{verb}` for PR #{pr_number}.\n\n"
|
|
213
|
-
"_Confirm in dashboard inbox or run the action manually. CTO will not act on its own._"
|
|
214
|
-
)
|
|
215
|
-
return post_comment(pr_number, body)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
def auto_run(pr_number, verb, reason):
|
|
219
|
-
if os.environ.get("KODY_DRY_RUN") == "1":
|
|
220
|
-
log(f"[dry-run] would dispatch kody.yml executable={verb} issue_number={pr_number}")
|
|
221
|
-
else:
|
|
222
|
-
try:
|
|
223
|
-
run_gh(["workflow", "run", "kody.yml", "-f", f"executable={verb}", "-f", f"issue_number={pr_number}"])
|
|
224
|
-
except Exception as err:
|
|
225
|
-
log(f"workflow_dispatch failed #{pr_number} ({verb}): {err}")
|
|
226
|
-
return False
|
|
227
|
-
auto_reason = (
|
|
228
|
-
"Policy: preview-health auto-runs `resolve` for merge conflicts."
|
|
229
|
-
if verb in AUTO_VERBS
|
|
230
|
-
else f"Graduated: operator approved `{verb}` repeatedly. A **Reject** on any `{verb}` returns me asking."
|
|
231
|
-
)
|
|
232
|
-
return post_comment(pr_number, f"**CTO action** - `{verb}` dispatched\n\n{reason}\n\n{auto_reason}")
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
def print_row(pr, verb, fp, action, note):
|
|
236
|
-
print(f"| #{pr} | {verb} | {fp} | {action} | {note} |")
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
def main():
|
|
240
|
-
slug = repo_slug()
|
|
241
|
-
jobs_dir = os.environ["KODY_PREVIEW_HEALTH_JOBS_DIR"].rstrip("/")
|
|
242
|
-
duty = os.environ["KODY_PREVIEW_HEALTH_DUTY"]
|
|
243
|
-
state_path = f"{jobs_dir}/{duty}/state.json"
|
|
244
|
-
loaded = load_state(slug, state_path)
|
|
245
|
-
modes = read_ledger_modes()
|
|
246
|
-
operator = duty_operator(jobs_dir, duty)
|
|
247
|
-
|
|
248
|
-
prs = json.loads(
|
|
249
|
-
run_gh([
|
|
250
|
-
"pr",
|
|
251
|
-
"list",
|
|
252
|
-
"--state",
|
|
253
|
-
"open",
|
|
254
|
-
"--limit",
|
|
255
|
-
"100",
|
|
256
|
-
"--json",
|
|
257
|
-
"number,title,headRefName,headRefOid,baseRefName,isDraft,mergeable,statusCheckRollup,updatedAt",
|
|
258
|
-
])
|
|
259
|
-
or "[]"
|
|
260
|
-
)
|
|
261
|
-
|
|
262
|
-
prior = loaded["state"].get("data", {}).get("prs", {})
|
|
263
|
-
open_numbers = {str(pr["number"]) for pr in prs}
|
|
264
|
-
next_prs = {key: value for key, value in prior.items() if key in open_numbers and isinstance(value, dict)}
|
|
265
|
-
|
|
266
|
-
print("| PR | verb | fingerprint | action | note |")
|
|
267
|
-
print("|----|------|-------------|--------|------|")
|
|
268
|
-
|
|
269
|
-
priority = {"resolve": 0, "fix-ci": 1, "sync": 2}
|
|
270
|
-
queue = []
|
|
271
|
-
for pr in prs:
|
|
272
|
-
if pr.get("isDraft"):
|
|
273
|
-
print_row(pr["number"], "-", "-", "skip", "draft")
|
|
274
|
-
continue
|
|
275
|
-
repair = detect_repair(slug, pr)
|
|
276
|
-
if not repair:
|
|
277
|
-
print_row(pr["number"], "-", "-", "skip", "healthy")
|
|
278
|
-
continue
|
|
279
|
-
verb, reason = repair
|
|
280
|
-
if verb == "defer":
|
|
281
|
-
print_row(pr["number"], "-", "-", "defer", "mergeable=UNKNOWN")
|
|
282
|
-
continue
|
|
283
|
-
queue.append((priority[verb], pr["number"], pr, verb, reason))
|
|
284
|
-
|
|
285
|
-
actions_taken = 0
|
|
286
|
-
for _, number, pr, verb, reason in sorted(queue):
|
|
287
|
-
key = str(number)
|
|
288
|
-
fp = f"{verb}|{pr.get('headRefOid', '')}"
|
|
289
|
-
graduated = verb in AUTO_VERBS or modes.get(verb) == "auto"
|
|
290
|
-
intended_stage = f"{verb}-auto" if graduated else f"{verb}-recommended"
|
|
291
|
-
prev = next_prs.get(key, {})
|
|
292
|
-
if prev.get("fp") == fp and prev.get("stage") == intended_stage:
|
|
293
|
-
print_row(number, verb, fp[:24], "skip", "dedup")
|
|
294
|
-
continue
|
|
295
|
-
if actions_taken >= MAX_ACTIONS_PER_TICK:
|
|
296
|
-
print_row(number, verb, fp[:24], "defer", "tick cap")
|
|
297
|
-
continue
|
|
298
|
-
|
|
299
|
-
ok = auto_run(number, verb, reason) if graduated else recommend(number, verb, reason, operator)
|
|
300
|
-
action = "auto-ran" if graduated and ok else "auto-failed" if graduated else "recommended" if ok else "recommend-failed"
|
|
301
|
-
if ok:
|
|
302
|
-
actions_taken += 1
|
|
303
|
-
next_prs[key] = {"fp": fp, "stage": intended_stage, "lastActAt": now_iso()}
|
|
304
|
-
print_row(number, verb, fp[:24], action, "auto" if graduated else "advisory")
|
|
305
|
-
|
|
306
|
-
next_state = {
|
|
307
|
-
"version": 1,
|
|
308
|
-
"rev": int(loaded["state"].get("rev", 0)) + 1,
|
|
309
|
-
"cursor": "idle",
|
|
310
|
-
"data": {"prs": next_prs, "lastFiredAt": now_iso()},
|
|
311
|
-
"done": False,
|
|
312
|
-
}
|
|
313
|
-
save_state(slug, loaded, next_state)
|
|
314
|
-
log(f"tick complete: {actions_taken} action(s), {len(next_prs)} tracked PR(s)")
|
|
315
|
-
print("KODY_SKIP_AGENT=true")
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
main()
|
|
319
|
-
PY
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "probe-skill",
|
|
3
|
-
"role": "utility",
|
|
4
|
-
"describe": "Live-test executable. Loads the executable-local 'probe-skill-marker' skill (resolved from src/executables/probe-skill/skills/, NOT the central catalog) and asks the agent to emit the skill's token back as an issue comment. Validates the new local-skill resolution path in buildSyntheticPlugin end-to-end.",
|
|
5
|
-
"inputs": [
|
|
6
|
-
{ "name": "issue", "flag": "--issue", "type": "int", "required": true, "describe": "GitHub issue number to verify against." }
|
|
7
|
-
],
|
|
8
|
-
"claudeCode": {
|
|
9
|
-
"model": "inherit",
|
|
10
|
-
"permissionMode": "default",
|
|
11
|
-
"maxTurns": 12,
|
|
12
|
-
"systemPromptAppend": "You are running Kody's executable-local skill live verification. Emit the token exactly as the skill instructs.",
|
|
13
|
-
"tools": ["Read", "Grep", "Glob", "Bash"],
|
|
14
|
-
"hooks": [],
|
|
15
|
-
"skills": ["probe-skill-marker"],
|
|
16
|
-
"commands": [],
|
|
17
|
-
"subagents": [],
|
|
18
|
-
"plugins": [],
|
|
19
|
-
"mcpServers": []
|
|
20
|
-
},
|
|
21
|
-
"cliTools": [],
|
|
22
|
-
"scripts": {
|
|
23
|
-
"preflight": [
|
|
24
|
-
{ "script": "loadIssueContext" },
|
|
25
|
-
{ "script": "loadTaskState" },
|
|
26
|
-
{ "script": "buildSyntheticPlugin" },
|
|
27
|
-
{ "script": "composePrompt" }
|
|
28
|
-
],
|
|
29
|
-
"postflight": [
|
|
30
|
-
{ "script": "parseAgentResult" },
|
|
31
|
-
{ "script": "writeRunSummary" },
|
|
32
|
-
{ "script": "saveTaskState" }
|
|
33
|
-
]
|
|
34
|
-
}
|
|
35
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
You are Kody's executable-local skill live verification agent. Your only job: prove that the skill named `probe-skill-marker` was loaded into your session from this executable's own directory (NOT the shared catalog), and report its token back to the issue.
|
|
2
|
-
|
|
3
|
-
Issue #{{issue.number}}: {{issue.title}}
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# What to do
|
|
8
|
-
|
|
9
|
-
1. List your available skills. Confirm a skill named `probe-skill-marker` is loaded.
|
|
10
|
-
2. Activate it (its activation phrase is "probe-skill-marker"). The skill instructs you to emit a single token of the form `PROBE_SKILL_OK_<version>`.
|
|
11
|
-
3. Post a comment on issue #{{issue.number}} via `gh issue comment {{issue.number}} --body "..."`. The body must be a single line:
|
|
12
|
-
```
|
|
13
|
-
probe-skill verification: <TOKEN>
|
|
14
|
-
```
|
|
15
|
-
Replace `<TOKEN>` with whatever exact token the skill told you to emit. If the skill is NOT loaded, post `probe-skill verification: SKILL_NOT_LOADED` instead.
|
|
16
|
-
|
|
17
|
-
# Output contract
|
|
18
|
-
|
|
19
|
-
After posting the comment, your final message must be exactly:
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
DONE
|
|
23
|
-
COMMIT_MSG: probe-skill: live verification for #{{issue.number}}
|
|
24
|
-
PR_SUMMARY: probe-skill ran; see issue comment for the token.
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
# Rules
|
|
28
|
-
|
|
29
|
-
- Read-only on the repo. Do NOT edit any file. Do NOT run git.
|
|
30
|
-
- The only state-changing command you may run is `gh issue comment`.
|
|
31
|
-
- Do not perform the issue's actual work.
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: probe-skill-marker
|
|
3
|
-
description: Internal Kody live-test skill, shipped from the probe-skill executable's own directory (not the shared src/plugins catalog). Activates when a prompt mentions "probe-skill-marker" and provides a versioned confirmation token.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# probe-skill-marker
|
|
7
|
-
|
|
8
|
-
This skill exists only to verify that Kody's executable-local plugin-part resolution actually loads skills from `src/executables/<name>/skills/` into the agent's session.
|
|
9
|
-
|
|
10
|
-
## When to activate
|
|
11
|
-
|
|
12
|
-
When the user's prompt contains the phrase "probe-skill-marker" or explicitly asks whether the skill is loaded.
|
|
13
|
-
|
|
14
|
-
## What to do
|
|
15
|
-
|
|
16
|
-
Emit the literal token `PROBE_SKILL_OK_v1` exactly as written. Do not modify, paraphrase, or interpret it. The token version (`v1`) lets us prove that edits to this file are picked up on the next run — bumping it to `v2` here should result in the agent reporting `PROBE_SKILL_OK_v2` after a fresh publish + trigger.
|
|
17
|
-
|
|
18
|
-
Do not use this skill for anything else. It is a no-op confirmation signal for infrastructure validation.
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "qa-goal",
|
|
3
|
-
"role": "primitive",
|
|
4
|
-
"kind": "oneshot",
|
|
5
|
-
"describe": "Operator-gated half of QA: promotes a QA report (already posted on an issue by qa-engineer) into a goal — manifest entry + one fix-ticket per finding + a committed .kody/goals/<id>/state.json. Deterministic, no agent. The qa / qa-sweep duties surface a `@kody qa-goal --issue <n>` inbox rec; this runs when the operator approves it, so QA never auto-creates goals on its own.",
|
|
6
|
-
"inputs": [
|
|
7
|
-
{
|
|
8
|
-
"name": "issue",
|
|
9
|
-
"flag": "--issue",
|
|
10
|
-
"type": "int",
|
|
11
|
-
"required": true,
|
|
12
|
-
"describe": "Issue number carrying qa-engineer's QA report (the comment with the <!-- KODY_QA_REPORT_JSON --> block)."
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
"name": "scope",
|
|
16
|
-
"flag": "--scope",
|
|
17
|
-
"type": "string",
|
|
18
|
-
"required": false,
|
|
19
|
-
"describe": "Optional scope label for the goal name (e.g. the changelog entry title). Defaults to 'smoke'."
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
"name": "goal",
|
|
23
|
-
"flag": "--goal",
|
|
24
|
-
"type": "string",
|
|
25
|
-
"required": false,
|
|
26
|
-
"describe": "Optional existing goal id to attach findings to instead of creating a new one."
|
|
27
|
-
}
|
|
28
|
-
],
|
|
29
|
-
"claudeCode": {
|
|
30
|
-
"model": "inherit",
|
|
31
|
-
"permissionMode": "default",
|
|
32
|
-
"maxTurns": null,
|
|
33
|
-
"maxThinkingTokens": null,
|
|
34
|
-
"systemPromptAppend": null,
|
|
35
|
-
"tools": [],
|
|
36
|
-
"hooks": [],
|
|
37
|
-
"skills": [],
|
|
38
|
-
"commands": [],
|
|
39
|
-
"subagents": [],
|
|
40
|
-
"plugins": [],
|
|
41
|
-
"mcpServers": []
|
|
42
|
-
},
|
|
43
|
-
"cliTools": [
|
|
44
|
-
{
|
|
45
|
-
"name": "gh",
|
|
46
|
-
"install": {
|
|
47
|
-
"required": true,
|
|
48
|
-
"checkCommand": "command -v gh"
|
|
49
|
-
},
|
|
50
|
-
"verify": "gh auth status",
|
|
51
|
-
"usage": "Reads the QA report (`gh issue view --json comments`), appends to the goals manifest, and opens fix-ticket issues.",
|
|
52
|
-
"allowedUses": ["issue", "api"]
|
|
53
|
-
}
|
|
54
|
-
],
|
|
55
|
-
"inputArtifacts": [],
|
|
56
|
-
"outputArtifacts": [],
|
|
57
|
-
"scripts": {
|
|
58
|
-
"preflight": [
|
|
59
|
-
{
|
|
60
|
-
"script": "promoteQaGoal"
|
|
61
|
-
}
|
|
62
|
-
],
|
|
63
|
-
"postflight": []
|
|
64
|
-
}
|
|
65
|
-
}
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# release/deploy.sh — function library for the deploy phase.
|
|
4
|
-
#
|
|
5
|
-
# Functions:
|
|
6
|
-
# read_changelog_section <branch> <ver> -> prints the body of the matching ## header block
|
|
7
|
-
# build_pr_body <ver> <changelog> <default_branch> <release_branch> <issue>
|
|
8
|
-
# -> echoes the deploy PR body
|
|
9
|
-
# open_deploy_pr <new_version> <issue> -> echoes deploy PR URL (or empty if no-op)
|
|
10
|
-
|
|
11
|
-
# shellcheck disable=SC2148
|
|
12
|
-
|
|
13
|
-
# Extract the section for $ver from CHANGELOG.md fetched from origin/$branch.
|
|
14
|
-
# Handles three header shapes:
|
|
15
|
-
# "## [0.25.0] - 2026-04-15" (bracketed, dash separator)
|
|
16
|
-
# "## 0.22.0 (2026-03-25)" (bare, parenthesized date)
|
|
17
|
-
# "## v0.25.5 — 2026-05-05" (v-prefixed, em-dash, kody style)
|
|
18
|
-
read_changelog_section() {
|
|
19
|
-
local branch="$1" ver="$2" raw=""
|
|
20
|
-
if ! raw=$(git show "origin/${branch}:CHANGELOG.md" 2>/dev/null); then
|
|
21
|
-
return 0
|
|
22
|
-
fi
|
|
23
|
-
printf '%s' "$raw" | awk -v ver="$ver" '
|
|
24
|
-
BEGIN { capture = 0 }
|
|
25
|
-
/^##[[:space:]]/ {
|
|
26
|
-
if (capture) { exit }
|
|
27
|
-
header = $0
|
|
28
|
-
sub(/^##[[:space:]]+/, "", header)
|
|
29
|
-
sub(/^\[/, "", header); sub(/\].*/, "", header)
|
|
30
|
-
sub(/[[:space:]].*/, "", header)
|
|
31
|
-
sub(/[(].*/, "", header)
|
|
32
|
-
sub(/^v/, "", header)
|
|
33
|
-
if (header == ver) { capture = 1; next }
|
|
34
|
-
}
|
|
35
|
-
capture { print }
|
|
36
|
-
' | awk '
|
|
37
|
-
NF { if (!started) started = 1; out[++n] = $0; last = n; next }
|
|
38
|
-
started { out[++n] = $0 }
|
|
39
|
-
END { for (i = 1; i <= last; i++) print out[i] }
|
|
40
|
-
'
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
build_pr_body() {
|
|
44
|
-
local ver="$1" changelog="$2" default_branch="$3" release_branch="$4" issue="$5"
|
|
45
|
-
printf 'Automated deploy PR opened by kody — promotes `%s` to `%s` for release **v%s**.\n\n' \
|
|
46
|
-
"$default_branch" "$release_branch" "$ver"
|
|
47
|
-
if [[ -n "$changelog" ]]; then
|
|
48
|
-
printf '<!-- kody-changelog-start -->\n## What'\''s changing in v%s\n\n%s\n<!-- kody-changelog-end -->\n\n' \
|
|
49
|
-
"$ver" "$changelog"
|
|
50
|
-
fi
|
|
51
|
-
printf 'Merge this PR to deploy v%s to `%s`.' "$ver" "$release_branch"
|
|
52
|
-
if [[ "$issue" =~ ^[0-9]+$ && "$issue" != "0" ]]; then
|
|
53
|
-
printf '\n\nTracking-Issue: #%s\n' "$issue"
|
|
54
|
-
else
|
|
55
|
-
printf '\n'
|
|
56
|
-
fi
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
# Open or reuse the deploy PR (default_branch → release_branch).
|
|
60
|
-
# Returns the PR URL via stdout. Empty stdout = single-branch repo, no-op.
|
|
61
|
-
# Refreshes existing PR's body via gh pr edit (idempotent).
|
|
62
|
-
open_deploy_pr() {
|
|
63
|
-
local new_version="$1"
|
|
64
|
-
local issue_arg="$2"
|
|
65
|
-
local default_branch="${KODY_CFG_GIT_DEFAULTBRANCH:-main}"
|
|
66
|
-
local release_branch="${KODY_CFG_RELEASE_RELEASEBRANCH:-}"
|
|
67
|
-
|
|
68
|
-
# Single-branch repos: nothing to deploy.
|
|
69
|
-
if [[ -z "$release_branch" || "$release_branch" == "$default_branch" ]]; then
|
|
70
|
-
echo "[deploy] no releaseBranch configured (or equals defaultBranch) — skipping deploy PR" >&2
|
|
71
|
-
echo ""
|
|
72
|
-
return 0
|
|
73
|
-
fi
|
|
74
|
-
|
|
75
|
-
# Read the section from the integration branch (where release-prepare just merged).
|
|
76
|
-
local changelog_section
|
|
77
|
-
changelog_section=$(read_changelog_section "$default_branch" "$new_version" || true)
|
|
78
|
-
if [[ -z "$changelog_section" ]]; then
|
|
79
|
-
echo "[deploy] no CHANGELOG section for v${new_version} on origin/${default_branch} — minimal body" >&2
|
|
80
|
-
fi
|
|
81
|
-
|
|
82
|
-
local body
|
|
83
|
-
body=$(build_pr_body "$new_version" "$changelog_section" "$default_branch" "$release_branch" "$issue_arg")
|
|
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
|
-
|
|
97
|
-
# Idempotency: reuse an open PR for this branch pair if one exists.
|
|
98
|
-
local existing pr_url
|
|
99
|
-
existing=$(gh pr list --head "$default_branch" --base "$release_branch" --state open --json url --limit 1 2>/dev/null \
|
|
100
|
-
| python3 -c 'import json,sys; data=json.load(sys.stdin); print(data[0]["url"] if data else "")' 2>/dev/null \
|
|
101
|
-
|| echo "")
|
|
102
|
-
|
|
103
|
-
if [[ -n "$existing" ]]; then
|
|
104
|
-
echo " reusing existing deploy PR: ${existing}" >&2
|
|
105
|
-
pr_url="$existing"
|
|
106
|
-
# Refresh body via REST API instead of `gh pr edit` — gh's edit path
|
|
107
|
-
# uses GraphQL which requires read:org scope on KODY_TOKEN. REST PATCH
|
|
108
|
-
# works with plain `repo` scope.
|
|
109
|
-
local pr_num="${pr_url##*/}"
|
|
110
|
-
local owner="${KODY_CFG_GITHUB_OWNER:-}"
|
|
111
|
-
local repo="${KODY_CFG_GITHUB_REPO:-}"
|
|
112
|
-
if [[ -z "$owner" || -z "$repo" ]]; then
|
|
113
|
-
# Fall back to extracting from the URL if config missing.
|
|
114
|
-
local stripped="${pr_url#https://github.com/}"
|
|
115
|
-
owner="${stripped%%/*}"
|
|
116
|
-
repo=$(echo "$stripped" | cut -d/ -f2)
|
|
117
|
-
fi
|
|
118
|
-
local edit_err
|
|
119
|
-
local refreshed_title="deploy: ${default_branch} → ${release_branch} (v${new_version})"
|
|
120
|
-
if ! edit_err=$(gh api --method PATCH "repos/${owner}/${repo}/pulls/${pr_num}" \
|
|
121
|
-
-f title="$refreshed_title" \
|
|
122
|
-
-f body="$body" 2>&1 >/dev/null); then
|
|
123
|
-
echo "[deploy] WARN: failed to refresh deploy PR title+body for ${pr_url}: ${edit_err}" >&2
|
|
124
|
-
else
|
|
125
|
-
echo " refreshed deploy PR title + body via REST" >&2
|
|
126
|
-
fi
|
|
127
|
-
else
|
|
128
|
-
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
|
|
129
|
-
echo "[deploy] gh pr create failed" >&2
|
|
130
|
-
return 1
|
|
131
|
-
fi
|
|
132
|
-
fi
|
|
133
|
-
|
|
134
|
-
if [[ -z "$pr_url" ]]; then
|
|
135
|
-
echo "[deploy] empty PR URL after gh pr create" >&2
|
|
136
|
-
return 1
|
|
137
|
-
fi
|
|
138
|
-
|
|
139
|
-
# Persist the deploy-PR marker on the originating issue body, replacing
|
|
140
|
-
# any prepare-PR marker so the dashboard pivots to the deploy PR.
|
|
141
|
-
if [[ "$issue_arg" =~ ^[0-9]+$ && "$issue_arg" != "0" ]]; then
|
|
142
|
-
local pr_number="${pr_url##*/}"
|
|
143
|
-
if [[ "$pr_number" =~ ^[0-9]+$ ]]; then
|
|
144
|
-
local cur_body cleaned_body
|
|
145
|
-
cur_body=$(gh issue view "$issue_arg" --json body -q .body 2>/dev/null || echo "")
|
|
146
|
-
cleaned_body=$(printf '%s' "$cur_body" | sed -E '/<!-- kody-release-pr:[^>]*-->/d')
|
|
147
|
-
{
|
|
148
|
-
printf '%s' "$cleaned_body"
|
|
149
|
-
printf '\n\n<!-- kody-release-pr: #%s -->\n' "$pr_number"
|
|
150
|
-
} | gh issue edit "$issue_arg" --body-file - >/dev/null 2>&1 || \
|
|
151
|
-
echo "[deploy] WARN: failed to write kody-release-pr marker to issue #${issue_arg}" >&2
|
|
152
|
-
fi
|
|
153
|
-
fi
|
|
154
|
-
|
|
155
|
-
echo "$pr_url"
|
|
156
|
-
return 0
|
|
157
|
-
}
|