@kody-ade/kody-engine 0.4.6 → 0.4.8
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.4.
|
|
6
|
+
version: "0.4.8",
|
|
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",
|
|
@@ -3911,13 +3911,14 @@ function ensurePr(opts) {
|
|
|
3911
3911
|
}
|
|
3912
3912
|
return { url: existing.url, number: existing.number, draft: opts.draft, action: "updated" };
|
|
3913
3913
|
}
|
|
3914
|
+
const base = opts.baseBranch && opts.baseBranch.length > 0 ? opts.baseBranch : opts.defaultBranch;
|
|
3914
3915
|
const args = [
|
|
3915
3916
|
"pr",
|
|
3916
3917
|
"create",
|
|
3917
3918
|
"--head",
|
|
3918
3919
|
opts.branch,
|
|
3919
3920
|
"--base",
|
|
3920
|
-
|
|
3921
|
+
base,
|
|
3921
3922
|
"--title",
|
|
3922
3923
|
title,
|
|
3923
3924
|
"--body-file",
|
|
@@ -4045,6 +4046,7 @@ var ensurePr2 = async (ctx) => {
|
|
|
4045
4046
|
const pr = ctx.data.pr;
|
|
4046
4047
|
const targetNumber = Number(ctx.data.commentTargetNumber ?? 0);
|
|
4047
4048
|
const title = issue?.title ?? pr?.title ?? `kody changes`;
|
|
4049
|
+
const baseBranch = ctx.data.baseBranch;
|
|
4048
4050
|
try {
|
|
4049
4051
|
const result = ensurePr({
|
|
4050
4052
|
branch,
|
|
@@ -4055,6 +4057,7 @@ var ensurePr2 = async (ctx) => {
|
|
|
4055
4057
|
failureReason: isFailure ? failureReason : void 0,
|
|
4056
4058
|
changedFiles,
|
|
4057
4059
|
agentSummary: ctx.data.prSummary,
|
|
4060
|
+
baseBranch,
|
|
4058
4061
|
cwd: ctx.cwd
|
|
4059
4062
|
});
|
|
4060
4063
|
ctx.output.prUrl = result.url;
|
|
@@ -4332,7 +4335,7 @@ function mergeBase(baseBranch, cwd) {
|
|
|
4332
4335
|
return "error";
|
|
4333
4336
|
}
|
|
4334
4337
|
}
|
|
4335
|
-
function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd) {
|
|
4338
|
+
function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd, baseBranch) {
|
|
4336
4339
|
const branchName = deriveBranchName(issueNumber, title);
|
|
4337
4340
|
const current = getCurrentBranch(cwd);
|
|
4338
4341
|
if (current === branchName) {
|
|
@@ -4360,8 +4363,16 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd) {
|
|
|
4360
4363
|
return { branch: branchName, created: false };
|
|
4361
4364
|
} catch {
|
|
4362
4365
|
}
|
|
4366
|
+
let forkPoint = defaultBranch;
|
|
4367
|
+
if (baseBranch && baseBranch !== defaultBranch) {
|
|
4368
|
+
try {
|
|
4369
|
+
git2(["rev-parse", "--verify", `origin/${baseBranch}`], cwd);
|
|
4370
|
+
forkPoint = baseBranch;
|
|
4371
|
+
} catch {
|
|
4372
|
+
}
|
|
4373
|
+
}
|
|
4363
4374
|
try {
|
|
4364
|
-
git2(["checkout", "-b", branchName, `origin/${
|
|
4375
|
+
git2(["checkout", "-b", branchName, `origin/${forkPoint}`], cwd);
|
|
4365
4376
|
} catch {
|
|
4366
4377
|
git2(["checkout", "-b", branchName], cwd);
|
|
4367
4378
|
}
|
|
@@ -6507,8 +6518,17 @@ var runFlow = async (ctx) => {
|
|
|
6507
6518
|
ctx.data.issue = issue;
|
|
6508
6519
|
ctx.data.commentTargetType = "issue";
|
|
6509
6520
|
ctx.data.commentTargetNumber = issueNumber;
|
|
6521
|
+
const baseRaw = ctx.args.base;
|
|
6522
|
+
const base = resolveBaseOverride(baseRaw);
|
|
6523
|
+
if (baseRaw && !base) {
|
|
6524
|
+
process.stderr.write(`[kody runFlow] ignoring --base "${baseRaw}" (must match /^goal-[a-z0-9-]+$/)
|
|
6525
|
+
`);
|
|
6526
|
+
}
|
|
6527
|
+
if (base) {
|
|
6528
|
+
ctx.data.baseBranch = base;
|
|
6529
|
+
}
|
|
6510
6530
|
try {
|
|
6511
|
-
const branchInfo = ensureFeatureBranch(issueNumber, issue.title, ctx.config.git.defaultBranch, ctx.cwd);
|
|
6531
|
+
const branchInfo = ensureFeatureBranch(issueNumber, issue.title, ctx.config.git.defaultBranch, ctx.cwd, base ?? void 0);
|
|
6512
6532
|
ctx.data.branch = branchInfo.branch;
|
|
6513
6533
|
} catch (err) {
|
|
6514
6534
|
if (err instanceof UncommittedChangesError) {
|
|
@@ -6530,6 +6550,10 @@ function tryPost(issueNumber, body, cwd) {
|
|
|
6530
6550
|
} catch {
|
|
6531
6551
|
}
|
|
6532
6552
|
}
|
|
6553
|
+
function resolveBaseOverride(value) {
|
|
6554
|
+
if (!value) return null;
|
|
6555
|
+
return /^goal-[a-z0-9-]+$/.test(value) ? value : null;
|
|
6556
|
+
}
|
|
6533
6557
|
|
|
6534
6558
|
// src/scripts/saveTaskState.ts
|
|
6535
6559
|
var saveTaskState = async (ctx, profile) => {
|
|
@@ -26,7 +26,7 @@ If a prior-art block is present above, scan it before editing — those are earl
|
|
|
26
26
|
# Required steps
|
|
27
27
|
1. **Extract** every actionable item from the feedback. A structured review uses headings like `### Concerns`, `### Suggestions`, and `### Bugs`; each bullet under those headings is a distinct item. `### Strengths`, `### Summary`, and `### Bottom line` are NOT items — skip them. If the feedback has no headings (plain inline feedback), treat the whole feedback as one item.
|
|
28
28
|
2. **Number each item** internally (Item 1, Item 2, …). You will account for every one of them in your final message below.
|
|
29
|
-
3. **Research** — read only what's needed to act on the items. Make the minimum edits required to implement each one. If the feedback or PR body links to external URLs (reproduction sites, bug recordings, spec pages), use the **Playwright MCP** tools (`mcp__playwright__browser_navigate`, `mcp__playwright__browser_snapshot`) to load them — do not rely on your interpretation of the URL alone.
|
|
29
|
+
3. **Research** — read only what's needed to act on the items. Make the minimum edits required to implement each one. If the feedback or PR body links to external **non-GitHub** URLs (reproduction sites, bug recordings, spec pages), use the **Playwright MCP** tools (`mcp__playwright__browser_navigate`, `mcp__playwright__browser_snapshot`) to load them — do not rely on your interpretation of the URL alone. Do NOT open GitHub URLs (issues, PRs, repo files, gists) in Playwright — the browser session is anonymous and will 404 on private repos. Issue/PR context is already in this prompt; for anything else on GitHub, use the `gh` CLI via Bash.
|
|
30
30
|
|
|
31
31
|
**Research floor (MUST be met before any Edit/Write):**
|
|
32
32
|
- Read the **full** contents of every file you intend to change.
|
|
@@ -41,6 +41,35 @@ for state_file in "${state_files[@]}"; do
|
|
|
41
41
|
active=$((active + 1))
|
|
42
42
|
echo "[goal-scheduler] → tick $goal_id"
|
|
43
43
|
|
|
44
|
+
# Ensure the shared goal branch exists on origin before we tick. This is
|
|
45
|
+
# the integration target for every task PR under this goal. Idempotent:
|
|
46
|
+
# if origin/goal-<id> already exists we skip. We deliberately create from
|
|
47
|
+
# the latest origin/<defaultBranch> so the goal branch starts from a
|
|
48
|
+
# known-clean base; subsequent task PRs build on top.
|
|
49
|
+
goal_branch="goal-${goal_id}"
|
|
50
|
+
default_branch="${KODY_CFG_GIT_DEFAULTBRANCH:-main}"
|
|
51
|
+
|
|
52
|
+
# Best-effort fetch so origin refs are fresh.
|
|
53
|
+
git fetch origin --quiet 2>/dev/null || true
|
|
54
|
+
|
|
55
|
+
if git rev-parse --verify --quiet "refs/remotes/origin/${goal_branch}" >/dev/null 2>&1; then
|
|
56
|
+
echo "[goal-scheduler] origin/${goal_branch} already exists — leaving as-is"
|
|
57
|
+
else
|
|
58
|
+
if ! git rev-parse --verify --quiet "refs/remotes/origin/${default_branch}" >/dev/null 2>&1; then
|
|
59
|
+
echo "[goal-scheduler] cannot create goal branch: origin/${default_branch} missing"
|
|
60
|
+
else
|
|
61
|
+
echo "[goal-scheduler] creating origin/${goal_branch} from origin/${default_branch}"
|
|
62
|
+
# Push a new ref directly without checking it out — avoids touching the
|
|
63
|
+
# working tree, which other ticks/scripts in this same scheduler run
|
|
64
|
+
# may rely on. Failures here are logged and we proceed; goal-tick will
|
|
65
|
+
# still dispatch task issues, and ensureFeatureBranch will fall back to
|
|
66
|
+
# forking from defaultBranch when origin/goal-<id> is absent.
|
|
67
|
+
if ! git push origin "refs/remotes/origin/${default_branch}:refs/heads/${goal_branch}" --quiet 2>&1; then
|
|
68
|
+
echo "[goal-scheduler] push of ${goal_branch} failed (will retry next tick)"
|
|
69
|
+
fi
|
|
70
|
+
fi
|
|
71
|
+
fi
|
|
72
|
+
|
|
44
73
|
# Run the tick. Top-level kody invocation is `kody <executable>` —
|
|
45
74
|
# there's no `dispatch` subcommand. A non-zero exit logs and continues
|
|
46
75
|
# so one stuck goal doesn't starve the rest of the schedule.
|
|
@@ -5,17 +5,24 @@
|
|
|
5
5
|
#
|
|
6
6
|
# Inputs (env, set by the executor):
|
|
7
7
|
# KODY_ARG_GOAL the goal id (directory name under .kody/goals/)
|
|
8
|
+
# KODY_CFG_GIT_DEFAULTBRANCH the repo's default branch (usually main)
|
|
8
9
|
#
|
|
9
10
|
# What it does, per tick:
|
|
10
|
-
# 1. Read .kody/goals/<id>/state.json. If missing or state
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# 4.
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
11
|
+
# 1. Read .kody/goals/<id>/state.json. If missing or state == "abandoned",
|
|
12
|
+
# run cleanup (close goal PR, close open tasks) and set state=closed.
|
|
13
|
+
# 2. If state != "active", exit.
|
|
14
|
+
# 3. List issues with label `goal:<id>`.
|
|
15
|
+
# 4. If every such issue is closed → set state=done, open final goal-<id> →
|
|
16
|
+
# default-branch PR (if not already open), commit + push state.json.
|
|
17
|
+
# 5. SERIALIZE: if any open issue still has the `goal-runner:dispatched`
|
|
18
|
+
# label, stay idle. We dispatch the next task only after the previous
|
|
19
|
+
# one has merged (issue closure follows PR merge via `Closes #N`).
|
|
20
|
+
# 6. If any issue carries `goal-runner:failed`, stay idle (a human must
|
|
21
|
+
# unblock by removing the label or closing the issue).
|
|
22
|
+
# 7. Otherwise pick the lowest-numbered open issue without
|
|
23
|
+
# `goal-runner:dispatched`. Comment `@kody --base goal-<id>` on it
|
|
24
|
+
# and add the label so we don't re-dispatch on the next tick.
|
|
25
|
+
# 8. Bump updatedAt and commit state.json (cheap audit trail).
|
|
19
26
|
#
|
|
20
27
|
# Stdout signals:
|
|
21
28
|
# KODY_SKIP_AGENT=true — always; this is a no-agent flow.
|
|
@@ -24,6 +31,8 @@
|
|
|
24
31
|
set -euo pipefail
|
|
25
32
|
|
|
26
33
|
goal_id="${KODY_ARG_GOAL:-}"
|
|
34
|
+
default_branch="${KODY_CFG_GIT_DEFAULTBRANCH:-main}"
|
|
35
|
+
|
|
27
36
|
if [ -z "$goal_id" ]; then
|
|
28
37
|
echo "KODY_REASON=missing --goal"
|
|
29
38
|
echo "KODY_SKIP_AGENT=true"
|
|
@@ -39,6 +48,10 @@ fi
|
|
|
39
48
|
|
|
40
49
|
state_dir=".kody/goals/${goal_id}"
|
|
41
50
|
state_file="${state_dir}/state.json"
|
|
51
|
+
goal_branch="goal-${goal_id}"
|
|
52
|
+
label="goal:${goal_id}"
|
|
53
|
+
dispatched_label="goal-runner:dispatched"
|
|
54
|
+
failed_label="goal-runner:failed"
|
|
42
55
|
|
|
43
56
|
if [ ! -f "$state_file" ]; then
|
|
44
57
|
echo "[goal-tick] no state file at $state_file — nothing to tick"
|
|
@@ -46,30 +59,106 @@ if [ ! -f "$state_file" ]; then
|
|
|
46
59
|
exit 0
|
|
47
60
|
fi
|
|
48
61
|
|
|
49
|
-
# Read current state.
|
|
50
|
-
# (e.g. release-prepare.sh), so we lean on it for JSON read/write.
|
|
62
|
+
# Read current state.
|
|
51
63
|
state=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('state',''))" "$state_file")
|
|
52
64
|
|
|
65
|
+
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
set_state_field() {
|
|
68
|
+
# set_state_field <key> <value> — value is treated as a JSON string.
|
|
69
|
+
python3 - "$state_file" "$1" "$2" <<'PY'
|
|
70
|
+
import json, sys
|
|
71
|
+
from datetime import datetime, timezone
|
|
72
|
+
path, key, value = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
73
|
+
with open(path) as f:
|
|
74
|
+
s = json.load(f)
|
|
75
|
+
s[key] = value
|
|
76
|
+
s["updatedAt"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
77
|
+
with open(path, "w") as f:
|
|
78
|
+
json.dump(s, f, indent=2)
|
|
79
|
+
f.write("\n")
|
|
80
|
+
PY
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
bump_updated_at() {
|
|
84
|
+
python3 - "$state_file" <<'PY'
|
|
85
|
+
import json, sys
|
|
86
|
+
from datetime import datetime, timezone
|
|
87
|
+
path = sys.argv[1]
|
|
88
|
+
with open(path) as f:
|
|
89
|
+
s = json.load(f)
|
|
90
|
+
s["updatedAt"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
91
|
+
with open(path, "w") as f:
|
|
92
|
+
json.dump(s, f, indent=2)
|
|
93
|
+
f.write("\n")
|
|
94
|
+
PY
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
commit_state() {
|
|
98
|
+
# commit_state <message> — best-effort commit + push.
|
|
99
|
+
git add "$state_file"
|
|
100
|
+
if ! git diff --cached --quiet; then
|
|
101
|
+
git commit -m "$1" --quiet
|
|
102
|
+
git push --quiet || echo "[goal-tick] push failed (will retry next tick)"
|
|
103
|
+
fi
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
ensure_label() {
|
|
107
|
+
# ensure_label <name> <color> <description> — best-effort, never fails the tick.
|
|
108
|
+
gh label create "$1" --color "$2" --description "$3" --force >/dev/null 2>&1 || true
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
list_goal_issues() {
|
|
112
|
+
# Up to 100 goal-labelled issues. PRs filtered out.
|
|
113
|
+
gh api \
|
|
114
|
+
"repos/{owner}/{repo}/issues?labels=${label}&state=all&per_page=100" \
|
|
115
|
+
--jq '[.[] | select(.pull_request == null) | {number, state: (.state | ascii_upcase), labels: [.labels[].name]}]'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# ── Cleanup path: state == abandoned ──────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
if [ "$state" = "abandoned" ]; then
|
|
121
|
+
echo "[goal-tick] $goal_id is abandoned — running cleanup"
|
|
122
|
+
|
|
123
|
+
# Close any open task issues with a brief note.
|
|
124
|
+
issues_json=$(list_goal_issues)
|
|
125
|
+
echo "$issues_json" | python3 -c "
|
|
126
|
+
import json, sys
|
|
127
|
+
data = json.load(sys.stdin)
|
|
128
|
+
for i in data:
|
|
129
|
+
if i['state'] == 'OPEN':
|
|
130
|
+
print(i['number'])
|
|
131
|
+
" | while read -r num; do
|
|
132
|
+
[ -n "$num" ] || continue
|
|
133
|
+
gh issue comment "$num" --body "_Goal abandoned — closing this task without dispatch._" >/dev/null 2>&1 || true
|
|
134
|
+
gh issue close "$num" --reason "not planned" >/dev/null 2>&1 || true
|
|
135
|
+
done
|
|
136
|
+
|
|
137
|
+
# Close the goal PR if one is open.
|
|
138
|
+
goal_pr=$(gh pr list --head "$goal_branch" --state open --json number --jq '.[0].number // empty' 2>/dev/null || echo "")
|
|
139
|
+
if [ -n "$goal_pr" ]; then
|
|
140
|
+
gh pr close "$goal_pr" --comment "_Goal abandoned by operator — closing without merge._" >/dev/null 2>&1 || true
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
set_state_field "state" "closed"
|
|
144
|
+
commit_state "chore(goals): abandon ${goal_id} (cleanup complete)"
|
|
145
|
+
echo "KODY_SKIP_AGENT=true"
|
|
146
|
+
exit 0
|
|
147
|
+
fi
|
|
148
|
+
|
|
53
149
|
if [ "$state" != "active" ]; then
|
|
54
150
|
echo "[goal-tick] $goal_id is '$state' — skipping"
|
|
55
151
|
echo "KODY_SKIP_AGENT=true"
|
|
56
152
|
exit 0
|
|
57
153
|
fi
|
|
58
154
|
|
|
59
|
-
|
|
60
|
-
dispatched_label="goal-runner:dispatched"
|
|
155
|
+
# ── Active path ───────────────────────────────────────────────────────────────
|
|
61
156
|
|
|
62
|
-
#
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
# Use `gh api` (not `gh issue list --label`) because the latter chokes on
|
|
66
|
-
# colons in label names, which is exactly the convention used here
|
|
67
|
-
# (`goal:<id>` etc). `gh api` also lets us filter PRs out cleanly via the
|
|
68
|
-
# `pull_request` field GitHub attaches to issue payloads.
|
|
69
|
-
issues_json=$(gh api \
|
|
70
|
-
"repos/{owner}/{repo}/issues?labels=${label}&state=all&per_page=100" \
|
|
71
|
-
--jq '[.[] | select(.pull_request == null) | {number, state: (.state | ascii_upcase), labels: .labels}]')
|
|
157
|
+
# Make sure the dedup labels exist before we read/write them.
|
|
158
|
+
ensure_label "$dispatched_label" "ededed" "kody goal-runner: already dispatched this tick"
|
|
159
|
+
ensure_label "$failed_label" "b60205" "kody goal-runner: task failed; needs human attention"
|
|
72
160
|
|
|
161
|
+
issues_json=$(list_goal_issues)
|
|
73
162
|
total=$(echo "$issues_json" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))")
|
|
74
163
|
if [ "$total" = "0" ]; then
|
|
75
164
|
echo "[goal-tick] no issues with label '$label' — leaving state untouched"
|
|
@@ -77,30 +166,81 @@ if [ "$total" = "0" ]; then
|
|
|
77
166
|
exit 0
|
|
78
167
|
fi
|
|
79
168
|
|
|
169
|
+
# Counts.
|
|
80
170
|
open_count=$(echo "$issues_json" | python3 -c "import json,sys; print(sum(1 for i in json.load(sys.stdin) if i['state']=='OPEN'))")
|
|
81
171
|
|
|
82
|
-
# All
|
|
172
|
+
# All tasks closed → goal is done. Open the final goal-<id> → default-branch PR.
|
|
83
173
|
if [ "$open_count" = "0" ]; then
|
|
84
|
-
echo "[goal-tick] all $total task(s) closed —
|
|
85
|
-
|
|
174
|
+
echo "[goal-tick] all $total task(s) closed — finalising goal"
|
|
175
|
+
|
|
176
|
+
# Open the goal PR if it doesn't already exist. We only care about origin/<goal_branch>:
|
|
177
|
+
# the goal branch should exist (created by goal-scheduler). If not, log and skip
|
|
178
|
+
# PR creation but still mark state=done so the goal moves out of `active`.
|
|
179
|
+
goal_pr_url=""
|
|
180
|
+
if git ls-remote --exit-code --heads origin "$goal_branch" >/dev/null 2>&1; then
|
|
181
|
+
existing_pr=$(gh pr list --head "$goal_branch" --state open --json number,url --jq '.[0]' 2>/dev/null || echo "")
|
|
182
|
+
if [ -z "$existing_pr" ] || [ "$existing_pr" = "null" ]; then
|
|
183
|
+
title="goal: ${goal_id}"
|
|
184
|
+
body=$(printf "Final integration PR for goal **%s**.\n\nAll task issues are closed and merged into \`%s\`. Ready for review.\n" "$goal_id" "$goal_branch")
|
|
185
|
+
goal_pr_url=$(gh pr create \
|
|
186
|
+
--head "$goal_branch" \
|
|
187
|
+
--base "$default_branch" \
|
|
188
|
+
--title "$title" \
|
|
189
|
+
--body "$body" 2>/dev/null || echo "")
|
|
190
|
+
else
|
|
191
|
+
goal_pr_url=$(echo "$existing_pr" | python3 -c "import json,sys; print(json.load(sys.stdin).get('url',''))")
|
|
192
|
+
fi
|
|
193
|
+
else
|
|
194
|
+
echo "[goal-tick] goal branch ${goal_branch} not found on origin — skipping final PR"
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
python3 - "$state_file" "$goal_pr_url" <<'PY'
|
|
86
198
|
import json, sys
|
|
87
199
|
from datetime import datetime, timezone
|
|
88
|
-
path = sys.argv[1]
|
|
200
|
+
path, pr_url = sys.argv[1], sys.argv[2]
|
|
89
201
|
with open(path) as f:
|
|
90
202
|
s = json.load(f)
|
|
91
203
|
now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
92
204
|
s["state"] = "done"
|
|
93
205
|
s["completedAt"] = now
|
|
94
206
|
s["updatedAt"] = now
|
|
207
|
+
if pr_url:
|
|
208
|
+
s["goalPrUrl"] = pr_url
|
|
95
209
|
with open(path, "w") as f:
|
|
96
210
|
json.dump(s, f, indent=2)
|
|
97
211
|
f.write("\n")
|
|
98
212
|
PY
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
213
|
+
commit_state "chore(goals): mark $goal_id done"
|
|
214
|
+
echo "KODY_SKIP_AGENT=true"
|
|
215
|
+
exit 0
|
|
216
|
+
fi
|
|
217
|
+
|
|
218
|
+
# Failure gate: any task carrying the failed label means a human must intervene.
|
|
219
|
+
failed_count=$(echo "$issues_json" | python3 -c "
|
|
220
|
+
import json, sys
|
|
221
|
+
data = json.load(sys.stdin)
|
|
222
|
+
print(sum(1 for i in data if 'goal-runner:failed' in i['labels']))
|
|
223
|
+
")
|
|
224
|
+
if [ "$failed_count" != "0" ]; then
|
|
225
|
+
echo "[goal-tick] $failed_count failed task(s) — staying idle until cleared"
|
|
226
|
+
bump_updated_at
|
|
227
|
+
commit_state "chore(goals): tick $goal_id (blocked by failed task)"
|
|
228
|
+
echo "KODY_SKIP_AGENT=true"
|
|
229
|
+
exit 0
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
# Serialize: if any dispatched task is still open, wait. The previous task is
|
|
233
|
+
# still merging. We dispatch the next one only after closure (which happens on
|
|
234
|
+
# PR merge into the goal branch via `Closes #N`).
|
|
235
|
+
in_flight=$(echo "$issues_json" | python3 -c "
|
|
236
|
+
import json, sys
|
|
237
|
+
data = json.load(sys.stdin)
|
|
238
|
+
print(sum(1 for i in data if i['state'] == 'OPEN' and 'goal-runner:dispatched' in i['labels']))
|
|
239
|
+
")
|
|
240
|
+
if [ "$in_flight" != "0" ]; then
|
|
241
|
+
echo "[goal-tick] $in_flight task(s) in flight — waiting for current task to merge into ${goal_branch}"
|
|
242
|
+
bump_updated_at
|
|
243
|
+
commit_state "chore(goals): tick $goal_id (waiting for in-flight task)"
|
|
104
244
|
echo "KODY_SKIP_AGENT=true"
|
|
105
245
|
exit 0
|
|
106
246
|
fi
|
|
@@ -112,44 +252,25 @@ data = json.load(sys.stdin)
|
|
|
112
252
|
opens = [
|
|
113
253
|
i for i in data
|
|
114
254
|
if i['state'] == 'OPEN'
|
|
115
|
-
and 'goal-runner:dispatched' not in [
|
|
255
|
+
and 'goal-runner:dispatched' not in i['labels']
|
|
116
256
|
]
|
|
117
257
|
opens.sort(key=lambda x: x['number'])
|
|
118
258
|
print(opens[0]['number'] if opens else '')
|
|
119
259
|
")
|
|
120
260
|
|
|
121
261
|
if [ -z "$next_issue" ]; then
|
|
122
|
-
echo "[goal-tick]
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
import json, sys
|
|
126
|
-
from datetime import datetime, timezone
|
|
127
|
-
path = sys.argv[1]
|
|
128
|
-
with open(path) as f:
|
|
129
|
-
s = json.load(f)
|
|
130
|
-
s["updatedAt"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
131
|
-
with open(path, "w") as f:
|
|
132
|
-
json.dump(s, f, indent=2)
|
|
133
|
-
f.write("\n")
|
|
134
|
-
PY
|
|
135
|
-
git add "$state_file"
|
|
136
|
-
if ! git diff --cached --quiet; then
|
|
137
|
-
git commit -m "chore(goals): tick $goal_id (idle)" --quiet
|
|
138
|
-
git push --quiet || echo "[goal-tick] push failed (will retry next tick)"
|
|
139
|
-
fi
|
|
262
|
+
echo "[goal-tick] no undispatched open task — idle"
|
|
263
|
+
bump_updated_at
|
|
264
|
+
commit_state "chore(goals): tick $goal_id (idle)"
|
|
140
265
|
echo "KODY_SKIP_AGENT=true"
|
|
141
266
|
exit 0
|
|
142
267
|
fi
|
|
143
268
|
|
|
144
|
-
echo "[goal-tick] dispatching @kody on task #$next_issue"
|
|
145
|
-
gh issue comment "$next_issue" --body "@kody"
|
|
146
|
-
# Ensure the dedup label exists in the repo. It lives outside the `kody:`
|
|
147
|
-
# namespace so `ensureLabels` doesn't create it at init time, and a missing
|
|
148
|
-
# label silently swallowed here would re-dispatch the same issue every tick.
|
|
149
|
-
gh label create "$dispatched_label" --color ededed --description "kody goal-runner: already dispatched this tick" --force >/dev/null 2>&1 || true
|
|
269
|
+
echo "[goal-tick] dispatching @kody on task #$next_issue (--base $goal_branch)"
|
|
270
|
+
gh issue comment "$next_issue" --body "@kody --base ${goal_branch}"
|
|
150
271
|
gh issue edit "$next_issue" --add-label "$dispatched_label"
|
|
151
272
|
|
|
152
|
-
# Bump updatedAt
|
|
273
|
+
# Bump updatedAt + record last dispatched issue.
|
|
153
274
|
python3 - "$state_file" "$next_issue" <<'PY'
|
|
154
275
|
import json, sys
|
|
155
276
|
from datetime import datetime, timezone
|
|
@@ -163,11 +284,7 @@ with open(path, "w") as f:
|
|
|
163
284
|
json.dump(s, f, indent=2)
|
|
164
285
|
f.write("\n")
|
|
165
286
|
PY
|
|
166
|
-
|
|
167
|
-
if ! git diff --cached --quiet; then
|
|
168
|
-
git commit -m "chore(goals): dispatched #${next_issue} for ${goal_id}" --quiet
|
|
169
|
-
git push --quiet || echo "[goal-tick] push failed (will retry next tick)"
|
|
170
|
-
fi
|
|
287
|
+
commit_state "chore(goals): dispatched #${next_issue} for ${goal_id}"
|
|
171
288
|
|
|
172
289
|
echo "KODY_SKIP_AGENT=true"
|
|
173
290
|
exit 0
|
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
"type": "int",
|
|
10
10
|
"required": true,
|
|
11
11
|
"describe": "GitHub issue number to implement."
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "base",
|
|
15
|
+
"flag": "--base",
|
|
16
|
+
"type": "string",
|
|
17
|
+
"required": false,
|
|
18
|
+
"describe": "Optional base branch override. Must match /^goal-[a-z0-9-]+$/; used by goal-tick so the feature branch forks from the shared goal branch and the PR targets it."
|
|
12
19
|
}
|
|
13
20
|
],
|
|
14
21
|
"claudeCode": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kody-ade/kody-engine",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
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",
|