@kody-ade/kody-engine 0.3.82 → 0.3.84

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.82",
6
+ version: "0.3.84",
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",
@@ -820,14 +820,58 @@ function commitTurn(cwd, sessionId, verbose) {
820
820
  const paths = [sessionRel, eventsRel].filter((p) => fs5.existsSync(path5.join(cwd, p)));
821
821
  if (paths.length === 0) return;
822
822
  const stdio = verbose ? "inherit" : "pipe";
823
+ const exec = (args) => execFileSync2("git", args, { cwd, stdio });
823
824
  try {
824
- execFileSync2("git", ["add", "-f", ...paths], { cwd, stdio });
825
- execFileSync2("git", ["commit", "--quiet", "-m", `chat: interactive turn for ${sessionId}`], { cwd, stdio });
826
- execFileSync2("git", ["push", "--quiet", "origin", "HEAD"], { cwd, stdio });
825
+ exec(["add", "-f", ...paths]);
826
+ exec(["commit", "--quiet", "-m", `chat: interactive turn for ${sessionId}`]);
827
827
  } catch (err) {
828
828
  const msg = err instanceof Error ? err.message : String(err);
829
- process.stderr.write(`[kody:chat:interactive] commit/push skipped: ${msg}
829
+ process.stderr.write(`[kody:chat:interactive] commit failed: ${msg}
830
830
  `);
831
+ return;
832
+ }
833
+ for (let attempt = 1; attempt <= 3; attempt++) {
834
+ try {
835
+ exec(["push", "--quiet", "origin", "HEAD"]);
836
+ return;
837
+ } catch (err) {
838
+ const msg = err instanceof Error ? err.message : String(err);
839
+ const isNonFf = /non-fast-forward|fetch first|rejected/i.test(msg);
840
+ if (!isNonFf || attempt === 3) {
841
+ process.stderr.write(`[kody:chat:interactive] push failed (attempt ${attempt}): ${msg}
842
+ `);
843
+ return;
844
+ }
845
+ process.stderr.write(`[kody:chat:interactive] push rejected (attempt ${attempt}); fetch+rebase+retry
846
+ `);
847
+ try {
848
+ exec(["fetch", "--quiet", "origin"]);
849
+ const branch = currentBranch2(cwd);
850
+ if (branch) {
851
+ exec(["rebase", "--quiet", `origin/${branch}`]);
852
+ } else {
853
+ process.stderr.write(`[kody:chat:interactive] cannot rebase: no current branch
854
+ `);
855
+ return;
856
+ }
857
+ } catch (rebaseErr) {
858
+ const rmsg = rebaseErr instanceof Error ? rebaseErr.message : String(rebaseErr);
859
+ process.stderr.write(`[kody:chat:interactive] rebase failed: ${rmsg}
860
+ `);
861
+ return;
862
+ }
863
+ }
864
+ }
865
+ }
866
+ function currentBranch2(cwd) {
867
+ try {
868
+ const out = execFileSync2("git", ["symbolic-ref", "--short", "HEAD"], {
869
+ cwd,
870
+ stdio: ["ignore", "pipe", "ignore"]
871
+ });
872
+ return out.toString("utf-8").trim() || null;
873
+ } catch {
874
+ return null;
831
875
  }
832
876
  }
833
877
  async function emitExit(opts, reason, turnsCompleted) {
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "goal-scheduler",
3
+ "role": "watch",
4
+ "describe": "Scheduled: for every active goal under .kody/goals/<id>/state.json, invoke goal-tick once. No agent on the scheduler itself.",
5
+ "kind": "scheduled",
6
+ "schedule": "*/5 * * * *",
7
+ "inputs": [],
8
+ "claudeCode": {
9
+ "model": "inherit",
10
+ "permissionMode": "default",
11
+ "maxTurns": 0,
12
+ "maxThinkingTokens": null,
13
+ "systemPromptAppend": null,
14
+ "tools": [],
15
+ "hooks": [],
16
+ "skills": [],
17
+ "commands": [],
18
+ "subagents": [],
19
+ "plugins": [],
20
+ "mcpServers": []
21
+ },
22
+ "cliTools": [
23
+ {
24
+ "name": "gh",
25
+ "install": {
26
+ "required": true,
27
+ "checkCommand": "command -v gh"
28
+ },
29
+ "verify": "gh auth status",
30
+ "usage": "",
31
+ "allowedUses": ["api", "issue", "pr"]
32
+ }
33
+ ],
34
+ "inputArtifacts": [],
35
+ "outputArtifacts": [],
36
+ "scripts": {
37
+ "preflight": [
38
+ { "shell": "scheduler.sh" },
39
+ { "script": "skipAgent" }
40
+ ],
41
+ "postflight": []
42
+ }
43
+ }
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # goal-scheduler: enumerate every goal state file under .kody/goals/ and
4
+ # dispatch goal-tick once for each whose state == "active". Runs as a
5
+ # scheduled executable (cron `*/5 * * * *`). No agent.
6
+ #
7
+ # A failed individual tick logs and continues — one stuck goal must not
8
+ # starve the rest.
9
+
10
+ set -euo pipefail
11
+
12
+ goals_dir=".kody/goals"
13
+
14
+ if [ ! -d "$goals_dir" ]; then
15
+ echo "[goal-scheduler] no $goals_dir — nothing to schedule"
16
+ echo "KODY_SKIP_AGENT=true"
17
+ exit 0
18
+ fi
19
+
20
+ shopt -s nullglob
21
+ state_files=("$goals_dir"/*/state.json)
22
+ shopt -u nullglob
23
+
24
+ if [ "${#state_files[@]}" = "0" ]; then
25
+ echo "[goal-scheduler] no goal state files yet"
26
+ echo "KODY_SKIP_AGENT=true"
27
+ exit 0
28
+ fi
29
+
30
+ active=0
31
+ for state_file in "${state_files[@]}"; do
32
+ [ -f "$state_file" ] || continue
33
+ goal_id=$(basename "$(dirname "$state_file")")
34
+
35
+ state=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('state',''))" "$state_file" 2>/dev/null || echo "")
36
+
37
+ if [ "$state" != "active" ]; then
38
+ continue
39
+ fi
40
+
41
+ active=$((active + 1))
42
+ echo "[goal-scheduler] → tick $goal_id"
43
+
44
+ # Run the tick in a subshell so a non-zero exit doesn't kill the loop.
45
+ if ! kody dispatch goal-tick --goal "$goal_id"; then
46
+ echo "[goal-scheduler] tick $goal_id failed (continuing)"
47
+ fi
48
+ done
49
+
50
+ echo "[goal-scheduler] ticked $active active goal(s) of ${#state_files[@]} total"
51
+ echo "KODY_SKIP_AGENT=true"
52
+ exit 0
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "goal-tick",
3
+ "role": "primitive",
4
+ "describe": "One deterministic tick for one goal: read .kody/goals/<id>/state.json, dispatch @kody on the next ready task (or mark goal done when all tasks closed). No agent.",
5
+ "kind": "oneshot",
6
+ "inputs": [
7
+ {
8
+ "name": "goal",
9
+ "flag": "--goal",
10
+ "type": "string",
11
+ "required": true,
12
+ "describe": "Goal id — directory name under .kody/goals/."
13
+ }
14
+ ],
15
+ "claudeCode": {
16
+ "model": "inherit",
17
+ "permissionMode": "default",
18
+ "maxTurns": 0,
19
+ "maxThinkingTokens": null,
20
+ "systemPromptAppend": null,
21
+ "tools": [],
22
+ "hooks": [],
23
+ "skills": [],
24
+ "commands": [],
25
+ "subagents": [],
26
+ "plugins": [],
27
+ "mcpServers": []
28
+ },
29
+ "cliTools": [
30
+ {
31
+ "name": "gh",
32
+ "install": {
33
+ "required": true,
34
+ "checkCommand": "command -v gh"
35
+ },
36
+ "verify": "gh auth status",
37
+ "usage": "Use `gh issue list/comment/edit` to read goal-labelled tasks and dispatch @kody.",
38
+ "allowedUses": ["issue", "pr", "api"]
39
+ }
40
+ ],
41
+ "inputArtifacts": [],
42
+ "outputArtifacts": [],
43
+ "scripts": {
44
+ "preflight": [
45
+ { "shell": "tick.sh" },
46
+ { "script": "skipAgent" }
47
+ ],
48
+ "postflight": []
49
+ }
50
+ }
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # goal-tick: one deterministic tick for one goal. No agent. Pure scripts +
4
+ # `gh` against the connected repo.
5
+ #
6
+ # Inputs (env, set by the executor):
7
+ # KODY_ARG_GOAL the goal id (directory name under .kody/goals/)
8
+ #
9
+ # What it does, per tick:
10
+ # 1. Read .kody/goals/<id>/state.json. If missing or state != "active", exit.
11
+ # 2. List issues with label `goal:<id>`.
12
+ # 3. If every such issue is closed → set state=done, completedAt=now,
13
+ # commit + push state.json, exit.
14
+ # 4. Otherwise pick the lowest-numbered open issue without label
15
+ # `goal-runner:dispatched`. Dispatch `@kody` on it (one new task per tick),
16
+ # add the label so we don't re-dispatch on the next tick.
17
+ # 5. Bump updatedAt and commit state.json (cheap, but lets the engine
18
+ # track per-tick activity in the repo history).
19
+ #
20
+ # Stdout signals:
21
+ # KODY_SKIP_AGENT=true — always; this is a no-agent flow.
22
+ # KODY_REASON=<text> — failure context (rare; gh errors etc).
23
+
24
+ set -euo pipefail
25
+
26
+ goal_id="${KODY_ARG_GOAL:-}"
27
+ if [ -z "$goal_id" ]; then
28
+ echo "KODY_REASON=missing --goal"
29
+ echo "KODY_SKIP_AGENT=true"
30
+ exit 1
31
+ fi
32
+
33
+ # Defensive: refuse path traversal in goal id.
34
+ if [[ "$goal_id" == *"/"* || "$goal_id" == *".."* ]]; then
35
+ echo "KODY_REASON=invalid goal id (no slashes or '..' allowed)"
36
+ echo "KODY_SKIP_AGENT=true"
37
+ exit 1
38
+ fi
39
+
40
+ state_dir=".kody/goals/${goal_id}"
41
+ state_file="${state_dir}/state.json"
42
+
43
+ if [ ! -f "$state_file" ]; then
44
+ echo "[goal-tick] no state file at $state_file — nothing to tick"
45
+ echo "KODY_SKIP_AGENT=true"
46
+ exit 0
47
+ fi
48
+
49
+ # Read current state. python3 is already required by other shell executables
50
+ # (e.g. release-prepare.sh), so we lean on it for JSON read/write.
51
+ state=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('state',''))" "$state_file")
52
+
53
+ if [ "$state" != "active" ]; then
54
+ echo "[goal-tick] $goal_id is '$state' — skipping"
55
+ echo "KODY_SKIP_AGENT=true"
56
+ exit 0
57
+ fi
58
+
59
+ label="goal:${goal_id}"
60
+ dispatched_label="goal-runner:dispatched"
61
+
62
+ # Fetch up to 100 goal-labelled issues. 100 is a soft ceiling — if a goal
63
+ # grows past that, we'll need pagination.
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}]')
72
+
73
+ total=$(echo "$issues_json" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))")
74
+ if [ "$total" = "0" ]; then
75
+ echo "[goal-tick] no issues with label '$label' — leaving state untouched"
76
+ echo "KODY_SKIP_AGENT=true"
77
+ exit 0
78
+ fi
79
+
80
+ 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
+
82
+ # All done? mark goal done and commit.
83
+ if [ "$open_count" = "0" ]; then
84
+ echo "[goal-tick] all $total task(s) closed — marking goal done"
85
+ python3 - "$state_file" <<'PY'
86
+ import json, sys
87
+ from datetime import datetime, timezone
88
+ path = sys.argv[1]
89
+ with open(path) as f:
90
+ s = json.load(f)
91
+ now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
92
+ s["state"] = "done"
93
+ s["completedAt"] = now
94
+ s["updatedAt"] = now
95
+ with open(path, "w") as f:
96
+ json.dump(s, f, indent=2)
97
+ f.write("\n")
98
+ PY
99
+ git add "$state_file"
100
+ if ! git diff --cached --quiet; then
101
+ git commit -m "chore(goals): mark $goal_id done" --quiet
102
+ git push --quiet || echo "[goal-tick] push failed (will retry next tick)"
103
+ fi
104
+ echo "KODY_SKIP_AGENT=true"
105
+ exit 0
106
+ fi
107
+
108
+ # Pick the lowest-numbered open issue without the dispatched marker.
109
+ next_issue=$(echo "$issues_json" | python3 -c "
110
+ import json, sys
111
+ data = json.load(sys.stdin)
112
+ opens = [
113
+ i for i in data
114
+ if i['state'] == 'OPEN'
115
+ and 'goal-runner:dispatched' not in [l['name'] for l in i.get('labels', [])]
116
+ ]
117
+ opens.sort(key=lambda x: x['number'])
118
+ print(opens[0]['number'] if opens else '')
119
+ ")
120
+
121
+ if [ -z "$next_issue" ]; then
122
+ echo "[goal-tick] all open tasks already dispatched — waiting for them to complete"
123
+ # Bump updatedAt so the dashboard can show "last ticked at" without parsing logs.
124
+ python3 - "$state_file" <<'PY'
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
140
+ echo "KODY_SKIP_AGENT=true"
141
+ exit 0
142
+ fi
143
+
144
+ echo "[goal-tick] dispatching @kody on task #$next_issue"
145
+ gh issue comment "$next_issue" --body "@kody"
146
+ gh issue edit "$next_issue" --add-label "$dispatched_label" || true
147
+
148
+ # Bump updatedAt so the file changes (cheap audit trail in git log).
149
+ python3 - "$state_file" "$next_issue" <<'PY'
150
+ import json, sys
151
+ from datetime import datetime, timezone
152
+ path = sys.argv[1]
153
+ issue = int(sys.argv[2])
154
+ with open(path) as f:
155
+ s = json.load(f)
156
+ s["updatedAt"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
157
+ s["lastDispatchedIssue"] = issue
158
+ with open(path, "w") as f:
159
+ json.dump(s, f, indent=2)
160
+ f.write("\n")
161
+ PY
162
+ git add "$state_file"
163
+ if ! git diff --cached --quiet; then
164
+ git commit -m "chore(goals): dispatched #${next_issue} for ${goal_id}" --quiet
165
+ git push --quiet || echo "[goal-tick] push failed (will retry next tick)"
166
+ fi
167
+
168
+ echo "KODY_SKIP_AGENT=true"
169
+ exit 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.3.82",
3
+ "version": "0.3.84",
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",