@kody-ade/kody-engine 0.4.221 → 0.4.223

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.
Files changed (34) hide show
  1. package/README.md +7 -5
  2. package/dist/bin/kody.js +767 -661
  3. package/dist/duties/duty-scheduler/duty.md +3 -0
  4. package/dist/duties/duty-scheduler/profile.json +6 -0
  5. package/dist/duties/duty-tick/duty.md +3 -0
  6. package/dist/duties/duty-tick/profile.json +6 -0
  7. package/dist/duties/duty-tick-scripted/duty.md +3 -0
  8. package/dist/duties/duty-tick-scripted/profile.json +6 -0
  9. package/dist/duties/goal-scheduler/duty.md +3 -0
  10. package/dist/duties/goal-scheduler/profile.json +6 -0
  11. package/dist/duties/goal-tick/duty.md +3 -0
  12. package/dist/duties/goal-tick/profile.json +6 -0
  13. package/dist/duties/job-live-verify/duty.md +3 -0
  14. package/dist/duties/job-live-verify/profile.json +6 -0
  15. package/dist/duties/plan-verify/duty.md +3 -0
  16. package/dist/duties/plan-verify/profile.json +6 -0
  17. package/dist/duties/probe-skill/duty.md +3 -0
  18. package/dist/duties/probe-skill/profile.json +6 -0
  19. package/dist/duties/qa-goal/duty.md +3 -0
  20. package/dist/duties/qa-goal/profile.json +6 -0
  21. package/dist/duties/task-job-fail-once/duty.md +3 -0
  22. package/dist/duties/task-job-fail-once/profile.json +6 -0
  23. package/dist/duties/task-job-pass-a/duty.md +3 -0
  24. package/dist/duties/task-job-pass-a/profile.json +6 -0
  25. package/dist/duties/task-job-pass-b/duty.md +3 -0
  26. package/dist/duties/task-job-pass-b/profile.json +6 -0
  27. package/dist/duties/task-jobs/duty.md +3 -0
  28. package/dist/duties/task-jobs/profile.json +6 -0
  29. package/dist/executables/preview-health/profile.json +48 -0
  30. package/dist/executables/preview-health/tick.sh +319 -0
  31. package/dist/executables/release/prepare.sh +4 -2
  32. package/dist/executables/types.ts +7 -10
  33. package/kody.config.schema.json +5 -0
  34. package/package.json +1 -1
@@ -0,0 +1,3 @@
1
+ # Duty Scheduler
2
+
3
+ Scan `.kody/duties/` for due duty folders and run each due duty through its declared executable.
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "duty-scheduler",
3
+ "action": "duty-scheduler",
4
+ "executable": "duty-scheduler",
5
+ "describe": "Schedule due duty folders."
6
+ }
@@ -0,0 +1,3 @@
1
+ # Duty Tick
2
+
3
+ Run one scheduled duty folder using its duty prompt, staff persona, tools, and state.
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "duty-tick",
3
+ "action": "duty-tick",
4
+ "executable": "duty-tick",
5
+ "describe": "Run one folder duty tick."
6
+ }
@@ -0,0 +1,3 @@
1
+ # Duty Tick Scripted
2
+
3
+ Run one scheduled duty folder through its deterministic tick script and persist the next duty state.
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "duty-tick-scripted",
3
+ "action": "duty-tick-scripted",
4
+ "executable": "duty-tick-scripted",
5
+ "describe": "Run one scripted folder duty tick."
6
+ }
@@ -0,0 +1,3 @@
1
+ # Goal Scheduler
2
+
3
+ Scan active goal state and run one goal tick per active goal.
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "goal-scheduler",
3
+ "action": "goal-scheduler",
4
+ "executable": "goal-scheduler",
5
+ "describe": "Schedule active goals."
6
+ }
@@ -0,0 +1,3 @@
1
+ # Goal Tick
2
+
3
+ Read one goal state file, dispatch the next ready task, or finalize the goal when all tasks are complete.
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "goal-tick",
3
+ "action": "goal-tick",
4
+ "executable": "goal-tick",
5
+ "describe": "Advance one goal."
6
+ }
@@ -0,0 +1,3 @@
1
+ # Job Live Verify
2
+
3
+ Verify live job reference, staff, duty, executable, skill, and state wiring.
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "job-live-verify",
3
+ "action": "job-live-verify",
4
+ "executable": "job-live-verify",
5
+ "describe": "Verify live job wiring."
6
+ }
@@ -0,0 +1,3 @@
1
+ # Plan Verify
2
+
3
+ Verify one planned task slice.
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "plan-verify",
3
+ "action": "plan-verify",
4
+ "executable": "plan-verify",
5
+ "describe": "Verify a planned task slice."
6
+ }
@@ -0,0 +1,3 @@
1
+ # Probe Skill
2
+
3
+ Run the probe skill test executable.
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "probe-skill",
3
+ "action": "probe-skill",
4
+ "executable": "probe-skill",
5
+ "describe": "Run the probe skill test executable."
6
+ }
@@ -0,0 +1,3 @@
1
+ # QA Goal
2
+
3
+ Run one QA goal workflow.
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "qa-goal",
3
+ "action": "qa-goal",
4
+ "executable": "qa-goal",
5
+ "describe": "Run a QA goal."
6
+ }
@@ -0,0 +1,3 @@
1
+ # Task Job Fail Once
2
+
3
+ Fail the first run and pass after a recorded failed attempt.
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "task-job-fail-once",
3
+ "action": "task-job-fail-once",
4
+ "executable": "task-job-fail-once",
5
+ "describe": "Fail once for task job retry verification."
6
+ }
@@ -0,0 +1,3 @@
1
+ # Task Job Pass A
2
+
3
+ Run task job pass test A.
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "task-job-pass-a",
3
+ "action": "task-job-pass-a",
4
+ "executable": "task-job-pass-a",
5
+ "describe": "Pass task job test A."
6
+ }
@@ -0,0 +1,3 @@
1
+ # Task Job Pass B
2
+
3
+ Run task job pass test B.
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "task-job-pass-b",
3
+ "action": "task-job-pass-b",
4
+ "executable": "task-job-pass-b",
5
+ "describe": "Pass task job test B."
6
+ }
@@ -0,0 +1,3 @@
1
+ # Task Jobs
2
+
3
+ Drain the next pending planned task job and return until the plan is complete.
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "task-jobs",
3
+ "action": "task-jobs",
4
+ "executable": "task-jobs",
5
+ "describe": "Run planned task jobs."
6
+ }
@@ -0,0 +1,48 @@
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
+ }
@@ -0,0 +1,319 @@
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
@@ -261,8 +261,10 @@ open_prepare_pr() {
261
261
  fi
262
262
 
263
263
  # Bump version files.
264
- local files
265
- mapfile -t files < <(resolve_version_files)
264
+ local files=()
265
+ while IFS= read -r f; do
266
+ files+=("$f")
267
+ done < <(resolve_version_files)
266
268
  local touched=()
267
269
  for f in "${files[@]}"; do
268
270
  local res
@@ -416,11 +416,11 @@ export interface Context {
416
416
  * `@kody <next>` comment, which is silently ignored when Kody comments as
417
417
  * a GitHub App (bot author), stalling the pipeline at classify.
418
418
  */
419
- nextDispatch?: { executable: string; cliArgs: Record<string, unknown> }
419
+ nextDispatch?: { action?: string; duty?: string; executable?: string; cliArgs: Record<string, unknown> }
420
420
  /** In-process hand-off to a full Job, preserving job identity in task state. */
421
421
  nextJob?: Job
422
422
  /** Where to return after nextJob succeeds. Used by task-jobs to keep draining pending work. */
423
- afterNextJob?: { executable: string; cliArgs: Record<string, unknown> }
423
+ afterNextJob?: { action?: string; duty?: string; executable?: string; cliArgs: Record<string, unknown> }
424
424
  }
425
425
  /**
426
426
  * If a preflight script sets this to true, the executor skips the agent
@@ -452,17 +452,15 @@ export type AnyScript = PreflightScript | PostflightScript
452
452
  // Job — the unified work request (task-state jobs collect run attempts).
453
453
  //
454
454
  // A Job is the required work the engine tries to execute, regardless of how it
455
- // was triggered. It REFERENCES the reusable nouns `executable` (how),
456
- // `persona` (who), `duty` (why, by slug) and OWNS its `schedule` (when).
455
+ // was triggered. It must reference a duty/action (why). The executable is only
456
+ // the duty-selected implementation detail (how), never a standalone request.
457
457
  // Task state stores this durable job separately from individual run attempts.
458
458
  // Two flavors:
459
459
  // - "instant" — run once now (an `@kody <verb>` comment or a manual dispatch)
460
460
  // - "scheduled" — fired on `schedule` (cron) by the tick path
461
461
  //
462
- // Fields are optional-heavy on purpose: a comment-minted instant job carries
463
- // `executable` + inline `why`; a cron-minted scheduled job carries `duty` +
464
- // `schedule` + `persona`. `runJob` (src/job.ts) lowers a Job onto the existing
465
- // executor and seeds both stable job metadata and per-run metadata.
462
+ // `runJob` (src/job.ts) lowers a Job onto the private executor after resolving
463
+ // the duty, and seeds both stable job metadata and per-run metadata.
466
464
  // ────────────────────────────────────────────────────────────────────────────
467
465
 
468
466
  export type JobFlavor = "instant" | "scheduled"
@@ -470,8 +468,7 @@ export type JobFlavor = "instant" | "scheduled"
470
468
  export interface Job {
471
469
  /** Public action the user/operator invoked. Mirrors the duty action. */
472
470
  action?: string
473
- /** How: executable (profile) name to run. 0–1; omitted when intent is
474
- * agent-only with no specific verb. */
471
+ /** How: implementation profile selected by the duty. Not valid by itself. */
475
472
  executable?: string
476
473
  /** Why (referenced): a duty slug whose intent drives the run. */
477
474
  duty?: string
@@ -63,6 +63,11 @@
63
63
  "pattern": "^[^/]+/.+$",
64
64
  "description": "Base provider/model string, for example minimax/MiniMax-M3 or claude/claude-sonnet-4-6."
65
65
  },
66
+ "reasoningEffort": {
67
+ "type": "string",
68
+ "enum": ["off", "low", "medium", "high"],
69
+ "description": "Default thinking effort for the Claude Agent SDK. Maps to maxThinkingTokens. 'off' (default) skips the thinking block entirely — no reasoning preamble, no extra tokens. 'low'/'medium'/'high' enable extended thinking with budgets 2_048/10_000/32_000. Overridable per-session via the REASONING_EFFORT env var or the dashboard's chat-level thinking dropdown."
70
+ },
66
71
  "perExecutable": {
67
72
  "type": "object",
68
73
  "description": "Optional provider/model override by executable name.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.221",
3
+ "version": "0.4.223",
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",