@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
package/dist/bin/kody.js CHANGED
@@ -15,7 +15,7 @@ var init_package = __esm({
15
15
  "package.json"() {
16
16
  package_default = {
17
17
  name: "@kody-ade/kody-engine",
18
- version: "0.4.221",
18
+ version: "0.4.223",
19
19
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
20
20
  license: "MIT",
21
21
  type: "module",
@@ -146,6 +146,12 @@ var init_claudeBinary = __esm({
146
146
  // src/config.ts
147
147
  import * as fs2 from "fs";
148
148
  import * as path2 from "path";
149
+ function parseReasoningEffort(raw) {
150
+ if (!raw) return null;
151
+ const v = raw.trim().toLowerCase();
152
+ if (REASONING_EFFORTS.includes(v)) return v;
153
+ return null;
154
+ }
149
155
  function parseProviderModel(s) {
150
156
  const slash = s.indexOf("/");
151
157
  if (slash <= 0 || slash === s.length - 1) {
@@ -198,7 +204,8 @@ function loadConfig(projectDir = process.cwd()) {
198
204
  },
199
205
  agent: {
200
206
  model: String(agent.model),
201
- ...parsePerExecutable(agent.perExecutable)
207
+ ...parsePerExecutable(agent.perExecutable),
208
+ ...parseAgentReasoningEffort(agent.reasoningEffort)
202
209
  },
203
210
  issueContext: parseIssueContext(raw.issueContext),
204
211
  testRequirements: parseTestRequirements(raw.testRequirements),
@@ -275,6 +282,10 @@ function parsePerExecutable(raw) {
275
282
  }
276
283
  return Object.keys(out).length > 0 ? { perExecutable: out } : {};
277
284
  }
285
+ function parseAgentReasoningEffort(raw) {
286
+ if (typeof raw !== "string") return {};
287
+ return { reasoningEffort: parseReasoningEffort(raw) ?? void 0 };
288
+ }
278
289
  function parseClassifyConfig(raw) {
279
290
  if (!raw || typeof raw !== "object") return void 0;
280
291
  const r = raw;
@@ -327,10 +338,16 @@ function parseTestRequirements(raw) {
327
338
  function getAnthropicApiKeyOrDummy() {
328
339
  return process.env.ANTHROPIC_API_KEY || `sk-ant-api03-${"0".repeat(64)}`;
329
340
  }
330
- var LITELLM_DEFAULT_PORT, LITELLM_DEFAULT_URL, GITHUB_AUTHOR_ASSOCIATIONS, DEFAULT_ALLOWED_ASSOCIATIONS, BUILTIN_ALIASES;
341
+ var REASONING_EFFORTS, REASONING_BUDGETS, LITELLM_DEFAULT_PORT, LITELLM_DEFAULT_URL, GITHUB_AUTHOR_ASSOCIATIONS, DEFAULT_ALLOWED_ASSOCIATIONS, BUILTIN_ALIASES;
331
342
  var init_config = __esm({
332
343
  "src/config.ts"() {
333
344
  "use strict";
345
+ REASONING_EFFORTS = ["off", "low", "medium", "high"];
346
+ REASONING_BUDGETS = {
347
+ low: 2048,
348
+ medium: 1e4,
349
+ high: 32e3
350
+ };
334
351
  LITELLM_DEFAULT_PORT = 4e3;
335
352
  LITELLM_DEFAULT_URL = `http://localhost:${LITELLM_DEFAULT_PORT}`;
336
353
  GITHUB_AUTHOR_ASSOCIATIONS = [
@@ -1089,8 +1106,8 @@ function listRepairCandidates(repoSlug) {
1089
1106
  };
1090
1107
  });
1091
1108
  }
1092
- function dispatchVerb(workflowFile, executable, prNumber) {
1093
- return dispatchWorkflow(workflowFile, executable, prNumber);
1109
+ function dispatchVerb(workflowFile, duty, prNumber) {
1110
+ return dispatchWorkflow(workflowFile, duty, prNumber);
1094
1111
  }
1095
1112
  function postRecommendation(prNumber, mention, message, dutySlug) {
1096
1113
  const mentioned = mention ? `${mention} ${message}` : message;
@@ -1219,17 +1236,17 @@ ${marker}` });
1219
1236
  return { error: err instanceof Error ? err.message : String(err) };
1220
1237
  }
1221
1238
  }
1222
- function dispatchWorkflow(workflowFile, executable, issueNumber) {
1239
+ function dispatchWorkflow(workflowFile, duty, issueNumber) {
1223
1240
  try {
1224
- gh(["workflow", "run", workflowFile, "-f", `executable=${executable}`, "-f", `issue_number=${issueNumber}`]);
1241
+ gh(["workflow", "run", workflowFile, "-f", `duty=${duty}`, "-f", `issue_number=${issueNumber}`]);
1225
1242
  return { ok: true };
1226
1243
  } catch (err) {
1227
1244
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
1228
1245
  }
1229
1246
  }
1230
- function isDispatchGated(executable, mode) {
1247
+ function isDispatchGated(duty, mode) {
1231
1248
  if (mode === "auto") return false;
1232
- if (executable && GATE_EXEMPT_EXECUTABLES.has(executable)) return false;
1249
+ if (duty && GATE_EXEMPT_DUTIES.has(duty)) return false;
1233
1250
  return true;
1234
1251
  }
1235
1252
  function trustRefusal(dutySlug) {
@@ -1374,19 +1391,20 @@ function dutyToolDefinitions(opts) {
1374
1391
  };
1375
1392
  const dispatchTool = {
1376
1393
  name: "dispatch_workflow",
1377
- description: "Dispatch a kody.yml workflow_dispatch run for an executable against an issue (the cross-run bot\u2192engine path; a bot `@kody` comment would be dropped). E.g. dispatch_workflow({executable:'run', issueNumber:<n>}) opens a fix PR from a tracking issue. Returns {ok} or {ok:false,error}.",
1394
+ description: "Dispatch a kody.yml workflow_dispatch run for a duty action against an issue (the cross-run bot\u2192engine path; a bot `@kody` comment would be dropped). E.g. dispatch_workflow({duty:'run', issueNumber:<n>}) opens a fix PR from a tracking issue. Returns {ok} or {ok:false,error}.",
1378
1395
  inputSchema: {
1379
- executable: z2.string().min(1).describe("Executable/stage to run (e.g. 'run')."),
1396
+ duty: z2.string().min(1).optional().describe("Duty action to run (e.g. 'run')."),
1397
+ executable: z2.string().min(1).optional().describe("Deprecated alias for duty."),
1380
1398
  issueNumber: z2.number().int().positive().describe("Issue (or PR) number forwarded as issue_number.")
1381
1399
  },
1382
1400
  handler: async (args) => {
1383
- const executable = String(args.executable ?? "");
1401
+ const duty = String(args.duty ?? args.executable ?? "");
1384
1402
  const issueNumber = Number(args.issueNumber);
1385
- if (isDispatchGated(executable, readDutyTrustMode(opts.repoSlug, opts.dutySlug))) {
1403
+ if (isDispatchGated(duty, readDutyTrustMode(opts.repoSlug, opts.dutySlug))) {
1386
1404
  return { content: [{ type: "text", text: trustRefusal(opts.dutySlug) }] };
1387
1405
  }
1388
- const result = dispatchWorkflow(workflowFile, executable, issueNumber);
1389
- const text = result.ok ? `Dispatched \`${executable}\` on #${issueNumber} via workflow_dispatch.` : `Dispatch failed for \`${executable}\` on #${issueNumber}: ${result.error}`;
1406
+ const result = dispatchWorkflow(workflowFile, duty, issueNumber);
1407
+ const text = result.ok ? `Dispatched duty \`${duty}\` on #${issueNumber} via workflow_dispatch.` : `Dispatch failed for duty \`${duty}\` on #${issueNumber}: ${result.error}`;
1390
1408
  return { content: [{ type: "text", text }] };
1391
1409
  }
1392
1410
  };
@@ -1421,7 +1439,7 @@ function buildDutyMcpServer(opts) {
1421
1439
  });
1422
1440
  return { server };
1423
1441
  }
1424
- var FAIL_CONCLUSIONS, RUNNING_STATUSES, TRUST_FILE_PATH, TRUST_STATE_BRANCH, THREAD_BODY_MAX, CHECK_FAIL_CONCLUSIONS, DEFAULT_IGNORE_CHECKS, trackMarker, commentMarker, GATE_EXEMPT_EXECUTABLES, DUTY_MCP_TOOL_NAMES;
1442
+ var FAIL_CONCLUSIONS, RUNNING_STATUSES, TRUST_FILE_PATH, TRUST_STATE_BRANCH, THREAD_BODY_MAX, CHECK_FAIL_CONCLUSIONS, DEFAULT_IGNORE_CHECKS, trackMarker, commentMarker, GATE_EXEMPT_DUTIES, DUTY_MCP_TOOL_NAMES;
1425
1443
  var init_dutyMcp = __esm({
1426
1444
  "src/dutyMcp.ts"() {
1427
1445
  "use strict";
@@ -1435,7 +1453,7 @@ var init_dutyMcp = __esm({
1435
1453
  DEFAULT_IGNORE_CHECKS = ["run", "kody", "duty-tick", "goal-tick", "worker-ask", "chat"];
1436
1454
  trackMarker = (key) => `<!-- kody-track:${key} -->`;
1437
1455
  commentMarker = (key) => `<!-- kody-track-comment:${key} -->`;
1438
- GATE_EXEMPT_EXECUTABLES = /* @__PURE__ */ new Set(["qa-engineer", "ui-review"]);
1456
+ GATE_EXEMPT_DUTIES = /* @__PURE__ */ new Set(["qa-engineer", "ui-review"]);
1439
1457
  DUTY_MCP_TOOL_NAMES = [
1440
1458
  "list_prs_to_repair",
1441
1459
  "sync_pr",
@@ -1663,15 +1681,19 @@ async function runAgent(opts) {
1663
1681
  let finalText = "";
1664
1682
  let getSubmitted;
1665
1683
  for (let attempt = 0; ; attempt++) {
1684
+ let ndjsonWriteFailed = false;
1685
+ let ndjsonWriteError;
1666
1686
  const fullLog = fs5.createWriteStream(ndjsonPath, { flags: "w" });
1687
+ fullLog.on("error", (err) => {
1688
+ ndjsonWriteFailed = true;
1689
+ ndjsonWriteError = err instanceof Error ? err.message : String(err);
1690
+ });
1667
1691
  const resultTexts = [];
1668
1692
  outcome = "failed";
1669
1693
  outcomeKind = "generic_failed";
1670
1694
  errorMessage = void 0;
1671
1695
  tokens = { input: 0, output: 0, cacheRead: 0, cacheCreate: 0 };
1672
1696
  messageCount = 0;
1673
- let ndjsonWriteFailed = false;
1674
- let ndjsonWriteError;
1675
1697
  let sawMutatingTool = false;
1676
1698
  let sawTerminalSuccess = false;
1677
1699
  let noWorkSuccess = false;
@@ -1746,7 +1768,13 @@ async function runAgent(opts) {
1746
1768
  if (typeof opts.maxTurns === "number" && opts.maxTurns > 0) {
1747
1769
  queryOptions.maxTurns = opts.maxTurns;
1748
1770
  }
1749
- if (typeof opts.maxThinkingTokens === "number" && opts.maxThinkingTokens > 0) {
1771
+ if (opts.reasoningEffort !== void 0 && opts.reasoningEffort !== null) {
1772
+ if (opts.reasoningEffort === "off") {
1773
+ } else {
1774
+ const budget = REASONING_BUDGETS[opts.reasoningEffort];
1775
+ if (budget) queryOptions.maxThinkingTokens = budget;
1776
+ }
1777
+ } else if (typeof opts.maxThinkingTokens === "number" && opts.maxThinkingTokens > 0) {
1750
1778
  queryOptions.maxThinkingTokens = opts.maxThinkingTokens;
1751
1779
  }
1752
1780
  if (typeof opts.systemPromptAppend === "string" && opts.systemPromptAppend.length > 0) {
@@ -1807,12 +1835,14 @@ async function runAgent(opts) {
1807
1835
  if (next.done) break;
1808
1836
  const msg = next.value;
1809
1837
  messageCount++;
1810
- try {
1811
- fullLog.write(`${JSON.stringify(msg)}
1838
+ if (!ndjsonWriteFailed) {
1839
+ try {
1840
+ fullLog.write(`${JSON.stringify(msg)}
1812
1841
  `);
1813
- } catch (e) {
1814
- ndjsonWriteFailed = true;
1815
- ndjsonWriteError = e instanceof Error ? e.message : String(e);
1842
+ } catch (e) {
1843
+ ndjsonWriteFailed = true;
1844
+ ndjsonWriteError = e instanceof Error ? e.message : String(e);
1845
+ }
1816
1846
  }
1817
1847
  const line = renderEvent(msg, { verbose: opts.verbose, quiet: opts.quiet });
1818
1848
  if (line) process.stdout.write(`${line}
@@ -2237,9 +2267,6 @@ function resolveExecutable(name, roots = getExecutableRoots()) {
2237
2267
  }
2238
2268
  return null;
2239
2269
  }
2240
- function hasExecutable(name, roots = getExecutableRoots()) {
2241
- return resolveExecutable(name, roots) !== null;
2242
- }
2243
2270
  function listDutyActions(projectDutiesRoot = getProjectDutiesRoot()) {
2244
2271
  const seen = /* @__PURE__ */ new Set();
2245
2272
  const out = [];
@@ -2439,6 +2466,73 @@ var init_task_artifacts = __esm({
2439
2466
  }
2440
2467
  });
2441
2468
 
2469
+ // src/gha.ts
2470
+ import { execFileSync as execFileSync2 } from "child_process";
2471
+ import * as fs14 from "fs";
2472
+ function getRunUrl() {
2473
+ const server = process.env.GITHUB_SERVER_URL;
2474
+ const repo = process.env.GITHUB_REPOSITORY;
2475
+ const runId = process.env.GITHUB_RUN_ID;
2476
+ if (!server || !repo || !runId) return "";
2477
+ return `${server}/${repo}/actions/runs/${runId}`;
2478
+ }
2479
+ function reactToTriggerComment(cwd) {
2480
+ if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
2481
+ const eventPath = process.env.GITHUB_EVENT_PATH;
2482
+ if (!eventPath || !fs14.existsSync(eventPath)) return;
2483
+ let event = null;
2484
+ try {
2485
+ event = JSON.parse(fs14.readFileSync(eventPath, "utf-8"));
2486
+ } catch {
2487
+ return;
2488
+ }
2489
+ const commentId = event?.comment?.id;
2490
+ const repo = process.env.GITHUB_REPOSITORY;
2491
+ if (!commentId || !repo) return;
2492
+ const token = process.env.KODY_TOKEN?.trim() || process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
2493
+ const args = [
2494
+ "api",
2495
+ "-X",
2496
+ "POST",
2497
+ "-H",
2498
+ "Accept: application/vnd.github+json",
2499
+ `/repos/${repo}/issues/comments/${commentId}/reactions`,
2500
+ "-f",
2501
+ "content=eyes"
2502
+ ];
2503
+ const opts = {
2504
+ cwd,
2505
+ env: { ...process.env, GH_TOKEN: token ?? process.env.GH_TOKEN ?? "" },
2506
+ stdio: "pipe",
2507
+ timeout: 15e3
2508
+ };
2509
+ let lastErr = null;
2510
+ for (let attempt = 0; attempt < 3; attempt++) {
2511
+ if (attempt > 0) sleepMs(attempt === 1 ? 500 : 1500);
2512
+ try {
2513
+ execFileSync2("gh", args, opts);
2514
+ return;
2515
+ } catch (err) {
2516
+ lastErr = err;
2517
+ }
2518
+ }
2519
+ process.stderr.write(
2520
+ `[kody] \u{1F440} reaction failed after 3 attempts on comment ${commentId}: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}
2521
+ `
2522
+ );
2523
+ }
2524
+ function sleepMs(ms) {
2525
+ try {
2526
+ execFileSync2("sleep", [(ms / 1e3).toString()], { stdio: "ignore", timeout: ms + 1e3 });
2527
+ } catch {
2528
+ }
2529
+ }
2530
+ var init_gha = __esm({
2531
+ "src/gha.ts"() {
2532
+ "use strict";
2533
+ }
2534
+ });
2535
+
2442
2536
  // src/profile-error.ts
2443
2537
  var ProfileError;
2444
2538
  var init_profile_error = __esm({
@@ -2599,7 +2693,7 @@ var init_lifecycles = __esm({
2599
2693
  });
2600
2694
 
2601
2695
  // src/scripts/buildSyntheticPlugin.ts
2602
- import * as fs14 from "fs";
2696
+ import * as fs15 from "fs";
2603
2697
  import * as os2 from "os";
2604
2698
  import * as path13 from "path";
2605
2699
  function getPluginsCatalogRoot() {
@@ -2613,17 +2707,17 @@ function getPluginsCatalogRoot() {
2613
2707
  // fallback
2614
2708
  ];
2615
2709
  for (const c of candidates) {
2616
- if (fs14.existsSync(c) && fs14.statSync(c).isDirectory()) return c;
2710
+ if (fs15.existsSync(c) && fs15.statSync(c).isDirectory()) return c;
2617
2711
  }
2618
2712
  return candidates[0];
2619
2713
  }
2620
2714
  function copyDir(src, dst) {
2621
- fs14.mkdirSync(dst, { recursive: true });
2622
- for (const ent of fs14.readdirSync(src, { withFileTypes: true })) {
2715
+ fs15.mkdirSync(dst, { recursive: true });
2716
+ for (const ent of fs15.readdirSync(src, { withFileTypes: true })) {
2623
2717
  const s = path13.join(src, ent.name);
2624
2718
  const d = path13.join(dst, ent.name);
2625
2719
  if (ent.isDirectory()) copyDir(s, d);
2626
- else if (ent.isFile()) fs14.copyFileSync(s, d);
2720
+ else if (ent.isFile()) fs15.copyFileSync(s, d);
2627
2721
  }
2628
2722
  }
2629
2723
  var buildSyntheticPlugin;
@@ -2637,44 +2731,44 @@ var init_buildSyntheticPlugin = __esm({
2637
2731
  const catalog = getPluginsCatalogRoot();
2638
2732
  const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2639
2733
  const root = path13.join(os2.tmpdir(), `kody-synth-${runId}`);
2640
- fs14.mkdirSync(path13.join(root, ".claude-plugin"), { recursive: true });
2734
+ fs15.mkdirSync(path13.join(root, ".claude-plugin"), { recursive: true });
2641
2735
  const resolvePart = (bucket, entry) => {
2642
2736
  const local = path13.join(profile.dir, bucket, entry);
2643
- if (fs14.existsSync(local)) return local;
2737
+ if (fs15.existsSync(local)) return local;
2644
2738
  const central = path13.join(catalog, bucket, entry);
2645
- if (fs14.existsSync(central)) return central;
2739
+ if (fs15.existsSync(central)) return central;
2646
2740
  throw new Error(
2647
2741
  `buildSyntheticPlugin: ${bucket} entry '${entry}' not found in executable dir (${profile.dir}/${bucket}/) or catalog (${catalog}/${bucket}/)`
2648
2742
  );
2649
2743
  };
2650
2744
  if (cc.skills.length > 0) {
2651
2745
  const dst = path13.join(root, "skills");
2652
- fs14.mkdirSync(dst, { recursive: true });
2746
+ fs15.mkdirSync(dst, { recursive: true });
2653
2747
  for (const name of cc.skills) {
2654
2748
  copyDir(resolvePart("skills", name), path13.join(dst, name));
2655
2749
  }
2656
2750
  }
2657
2751
  if (cc.commands.length > 0) {
2658
2752
  const dst = path13.join(root, "commands");
2659
- fs14.mkdirSync(dst, { recursive: true });
2753
+ fs15.mkdirSync(dst, { recursive: true });
2660
2754
  for (const name of cc.commands) {
2661
- fs14.copyFileSync(resolvePart("commands", `${name}.md`), path13.join(dst, `${name}.md`));
2755
+ fs15.copyFileSync(resolvePart("commands", `${name}.md`), path13.join(dst, `${name}.md`));
2662
2756
  }
2663
2757
  }
2664
2758
  if (cc.hooks.length > 0) {
2665
2759
  const dst = path13.join(root, "hooks");
2666
- fs14.mkdirSync(dst, { recursive: true });
2760
+ fs15.mkdirSync(dst, { recursive: true });
2667
2761
  const merged = { hooks: {} };
2668
2762
  for (const name of cc.hooks) {
2669
2763
  const src = resolvePart("hooks", `${name}.json`);
2670
- const parsed = JSON.parse(fs14.readFileSync(src, "utf-8"));
2764
+ const parsed = JSON.parse(fs15.readFileSync(src, "utf-8"));
2671
2765
  for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
2672
2766
  if (!Array.isArray(entries)) continue;
2673
2767
  if (!merged.hooks[event]) merged.hooks[event] = [];
2674
2768
  merged.hooks[event].push(...entries);
2675
2769
  }
2676
2770
  }
2677
- fs14.writeFileSync(path13.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
2771
+ fs15.writeFileSync(path13.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
2678
2772
  `);
2679
2773
  }
2680
2774
  const manifest = {
@@ -2684,7 +2778,7 @@ var init_buildSyntheticPlugin = __esm({
2684
2778
  };
2685
2779
  if (cc.skills.length > 0) manifest.skills = ["./skills/"];
2686
2780
  if (cc.commands.length > 0) manifest.commands = ["./commands/"];
2687
- fs14.writeFileSync(path13.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
2781
+ fs15.writeFileSync(path13.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
2688
2782
  `);
2689
2783
  ctx.data.syntheticPluginPath = root;
2690
2784
  };
@@ -2692,7 +2786,7 @@ var init_buildSyntheticPlugin = __esm({
2692
2786
  });
2693
2787
 
2694
2788
  // src/subagents.ts
2695
- import * as fs15 from "fs";
2789
+ import * as fs16 from "fs";
2696
2790
  import * as path14 from "path";
2697
2791
  function splitFrontmatter(raw) {
2698
2792
  const match = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(raw);
@@ -2707,9 +2801,9 @@ function splitFrontmatter(raw) {
2707
2801
  }
2708
2802
  function resolveAgentFile(profileDir, name) {
2709
2803
  const local = path14.join(profileDir, "agents", `${name}.md`);
2710
- if (fs15.existsSync(local)) return local;
2804
+ if (fs16.existsSync(local)) return local;
2711
2805
  const central = path14.join(getPluginsCatalogRoot(), "agents", `${name}.md`);
2712
- if (fs15.existsSync(central)) return central;
2806
+ if (fs16.existsSync(central)) return central;
2713
2807
  throw new Error(`loadSubagents: agent '${name}' not found in ${profileDir}/agents/ or shared catalog`);
2714
2808
  }
2715
2809
  function captureSubagentTemplates(profile) {
@@ -2718,7 +2812,7 @@ function captureSubagentTemplates(profile) {
2718
2812
  const out = {};
2719
2813
  for (const name of names) {
2720
2814
  try {
2721
- out[name] = fs15.readFileSync(resolveAgentFile(profile.dir, name), "utf-8");
2815
+ out[name] = fs16.readFileSync(resolveAgentFile(profile.dir, name), "utf-8");
2722
2816
  } catch {
2723
2817
  }
2724
2818
  }
@@ -2729,7 +2823,7 @@ function loadSubagents(profile) {
2729
2823
  if (!names || names.length === 0) return void 0;
2730
2824
  const agents = {};
2731
2825
  for (const name of names) {
2732
- const raw = profile.subagentTemplates?.[name] ?? fs15.readFileSync(resolveAgentFile(profile.dir, name), "utf-8");
2826
+ const raw = profile.subagentTemplates?.[name] ?? fs16.readFileSync(resolveAgentFile(profile.dir, name), "utf-8");
2733
2827
  const { fm, body } = splitFrontmatter(raw);
2734
2828
  if (!body) throw new Error(`loadSubagents: agent '${name}' has an empty prompt body`);
2735
2829
  const def = {
@@ -2753,15 +2847,15 @@ var init_subagents = __esm({
2753
2847
  });
2754
2848
 
2755
2849
  // src/profile.ts
2756
- import * as fs16 from "fs";
2850
+ import * as fs17 from "fs";
2757
2851
  import * as path15 from "path";
2758
2852
  function loadProfile(profilePath) {
2759
- if (!fs16.existsSync(profilePath)) {
2853
+ if (!fs17.existsSync(profilePath)) {
2760
2854
  throw new ProfileError(profilePath, "file not found");
2761
2855
  }
2762
2856
  let raw;
2763
2857
  try {
2764
- raw = JSON.parse(fs16.readFileSync(profilePath, "utf-8"));
2858
+ raw = JSON.parse(fs17.readFileSync(profilePath, "utf-8"));
2765
2859
  } catch (err) {
2766
2860
  throw new ProfileError(profilePath, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
2767
2861
  }
@@ -2880,7 +2974,7 @@ function loadProfile(profilePath) {
2880
2974
  const preNames = new Set(profile.scripts.preflight.map((e) => e.script).filter(Boolean));
2881
2975
  const postNames = profile.scripts.postflight.map((e) => e.script).filter(Boolean);
2882
2976
  const needsState = postNames.includes("writeJobStateFile") || postNames.includes("parseJobStateFromAgentResult");
2883
- const STATE_LOADERS = ["loadDutyState", "loadJobFromFile", "runTickScript"];
2977
+ const STATE_LOADERS = ["loadDutyState", "loadJobFromFile", "runTickScript", "runScheduledExecutableTick"];
2884
2978
  if (needsState && !STATE_LOADERS.some((s) => preNames.has(s))) {
2885
2979
  throw new ProfileError(
2886
2980
  profilePath,
@@ -2894,7 +2988,7 @@ function readPromptTemplates(dir) {
2894
2988
  const out = {};
2895
2989
  const read = (p) => {
2896
2990
  try {
2897
- out[p] = fs16.readFileSync(p, "utf-8");
2991
+ out[p] = fs17.readFileSync(p, "utf-8");
2898
2992
  } catch {
2899
2993
  }
2900
2994
  };
@@ -2902,7 +2996,7 @@ function readPromptTemplates(dir) {
2902
2996
  read(path15.join(dir, "duty.md"));
2903
2997
  try {
2904
2998
  const promptsDir = path15.join(dir, "prompts");
2905
- for (const ent of fs16.readdirSync(promptsDir)) {
2999
+ for (const ent of fs17.readdirSync(promptsDir)) {
2906
3000
  if (ent.endsWith(".md")) read(path15.join(promptsDir, ent));
2907
3001
  }
2908
3002
  } catch {
@@ -3210,7 +3304,7 @@ var init_profile = __esm({
3210
3304
  });
3211
3305
 
3212
3306
  // src/state.ts
3213
- import { execFileSync as execFileSync2 } from "child_process";
3307
+ import { execFileSync as execFileSync3 } from "child_process";
3214
3308
  function emptyState() {
3215
3309
  return {
3216
3310
  schemaVersion: 1,
@@ -3233,7 +3327,7 @@ function ghToken2() {
3233
3327
  function gh2(args, input, cwd) {
3234
3328
  const token = ghToken2();
3235
3329
  const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
3236
- return execFileSync2("gh", args, {
3330
+ return execFileSync3("gh", args, {
3237
3331
  encoding: "utf-8",
3238
3332
  timeout: API_TIMEOUT_MS2,
3239
3333
  cwd,
@@ -3580,16 +3674,16 @@ var init_state = __esm({
3580
3674
  });
3581
3675
 
3582
3676
  // src/prompt.ts
3583
- import * as fs17 from "fs";
3677
+ import * as fs18 from "fs";
3584
3678
  import * as path16 from "path";
3585
3679
  function loadProjectConventions(projectDir) {
3586
3680
  const out = [];
3587
3681
  for (const rel of CONVENTION_FILES) {
3588
3682
  const abs = path16.join(projectDir, rel);
3589
- if (!fs17.existsSync(abs)) continue;
3683
+ if (!fs18.existsSync(abs)) continue;
3590
3684
  let content;
3591
3685
  try {
3592
- content = fs17.readFileSync(abs, "utf-8");
3686
+ content = fs18.readFileSync(abs, "utf-8");
3593
3687
  } catch {
3594
3688
  continue;
3595
3689
  }
@@ -3824,20 +3918,20 @@ var loadMemoryContext_exports = {};
3824
3918
  __export(loadMemoryContext_exports, {
3825
3919
  loadMemoryContext: () => loadMemoryContext
3826
3920
  });
3827
- import * as fs18 from "fs";
3921
+ import * as fs19 from "fs";
3828
3922
  import * as path17 from "path";
3829
3923
  function collectPages(memoryAbs) {
3830
3924
  const out = [];
3831
3925
  walkMd(memoryAbs, (file) => {
3832
3926
  let stat;
3833
3927
  try {
3834
- stat = fs18.statSync(file);
3928
+ stat = fs19.statSync(file);
3835
3929
  } catch {
3836
3930
  return;
3837
3931
  }
3838
3932
  let raw;
3839
3933
  try {
3840
- raw = fs18.readFileSync(file, "utf-8");
3934
+ raw = fs19.readFileSync(file, "utf-8");
3841
3935
  } catch {
3842
3936
  return;
3843
3937
  }
@@ -3913,7 +4007,7 @@ function walkMd(root, visit) {
3913
4007
  const dir = stack.pop();
3914
4008
  let names;
3915
4009
  try {
3916
- names = fs18.readdirSync(dir);
4010
+ names = fs19.readdirSync(dir);
3917
4011
  } catch {
3918
4012
  continue;
3919
4013
  }
@@ -3922,7 +4016,7 @@ function walkMd(root, visit) {
3922
4016
  const full = path17.join(dir, name);
3923
4017
  let stat;
3924
4018
  try {
3925
- stat = fs18.statSync(full);
4019
+ stat = fs19.statSync(full);
3926
4020
  } catch {
3927
4021
  continue;
3928
4022
  }
@@ -3946,7 +4040,7 @@ var init_loadMemoryContext = __esm({
3946
4040
  loadMemoryContext = async (ctx) => {
3947
4041
  if (typeof ctx.data.memoryContext === "string") return;
3948
4042
  const memoryAbs = path17.join(ctx.cwd, MEMORY_DIR_RELATIVE);
3949
- if (!fs18.existsSync(memoryAbs)) {
4043
+ if (!fs19.existsSync(memoryAbs)) {
3950
4044
  ctx.data.memoryContext = "";
3951
4045
  return;
3952
4046
  }
@@ -3989,12 +4083,12 @@ var init_loadCoverageRules = __esm({
3989
4083
  });
3990
4084
 
3991
4085
  // src/container.ts
3992
- import { execFileSync as execFileSync3 } from "child_process";
3993
- import * as fs19 from "fs";
4086
+ import { execFileSync as execFileSync4 } from "child_process";
4087
+ import * as fs20 from "fs";
3994
4088
  function getProfileInputsForChild(profileName, _cwd) {
3995
4089
  try {
3996
4090
  const profilePath = resolveProfilePath(profileName);
3997
- if (!fs19.existsSync(profilePath)) return null;
4091
+ if (!fs20.existsSync(profilePath)) return null;
3998
4092
  return loadProfile(profilePath).inputs;
3999
4093
  } catch {
4000
4094
  return null;
@@ -4228,7 +4322,7 @@ async function runContainerLoop(profile, ctx, input) {
4228
4322
  }
4229
4323
  function resetWorkingTree(cwd) {
4230
4324
  try {
4231
- execFileSync3("git", ["reset", "--hard", "HEAD"], {
4325
+ execFileSync4("git", ["reset", "--hard", "HEAD"], {
4232
4326
  cwd,
4233
4327
  stdio: ["ignore", "pipe", "pipe"],
4234
4328
  timeout: 3e4
@@ -4455,8 +4549,8 @@ var init_lifecycleLabels = __esm({
4455
4549
  });
4456
4550
 
4457
4551
  // src/litellm.ts
4458
- import { execFileSync as execFileSync4, spawn as spawn3 } from "child_process";
4459
- import * as fs20 from "fs";
4552
+ import { execFileSync as execFileSync5, spawn as spawn3 } from "child_process";
4553
+ import * as fs21 from "fs";
4460
4554
  import * as os3 from "os";
4461
4555
  import * as path18 from "path";
4462
4556
  async function checkLitellmHealth(url) {
@@ -4488,7 +4582,7 @@ function generateLitellmConfigYaml(model) {
4488
4582
  }
4489
4583
  function litellmImportable() {
4490
4584
  try {
4491
- execFileSync4("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
4585
+ execFileSync5("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
4492
4586
  return true;
4493
4587
  } catch {
4494
4588
  return false;
@@ -4496,7 +4590,7 @@ function litellmImportable() {
4496
4590
  }
4497
4591
  function locateLitellmScript() {
4498
4592
  try {
4499
- const out = execFileSync4(
4593
+ const out = execFileSync5(
4500
4594
  "python3",
4501
4595
  [
4502
4596
  "-c",
@@ -4511,7 +4605,7 @@ function locateLitellmScript() {
4511
4605
  }
4512
4606
  function resolveLitellmCommand() {
4513
4607
  try {
4514
- execFileSync4("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
4608
+ execFileSync5("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
4515
4609
  return "litellm";
4516
4610
  } catch {
4517
4611
  if (!litellmImportable()) {
@@ -4519,7 +4613,7 @@ function resolveLitellmCommand() {
4519
4613
  let installed = false;
4520
4614
  for (const pip of ["pip", "pip3"]) {
4521
4615
  try {
4522
- execFileSync4(pip, ["install", "litellm[proxy]"], { timeout: 3e5, stdio: "inherit" });
4616
+ execFileSync5(pip, ["install", "litellm[proxy]"], { timeout: 3e5, stdio: "inherit" });
4523
4617
  installed = true;
4524
4618
  break;
4525
4619
  } catch {
@@ -4544,12 +4638,12 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
4544
4638
  let logPath;
4545
4639
  const spawnProxy = () => {
4546
4640
  const configPath = path18.join(os3.tmpdir(), `kody-local-litellm-${Date.now()}.yaml`);
4547
- fs20.writeFileSync(configPath, generateLitellmConfigYaml(model));
4641
+ fs21.writeFileSync(configPath, generateLitellmConfigYaml(model));
4548
4642
  const args = ["--config", configPath, "--port", port];
4549
4643
  const nextLogPath = path18.join(os3.tmpdir(), `kody-local-litellm-${Date.now()}.log`);
4550
- const outFd = fs20.openSync(nextLogPath, "w");
4644
+ const outFd = fs21.openSync(nextLogPath, "w");
4551
4645
  child = spawn3(cmd, args, { stdio: ["ignore", outFd, outFd], detached: true, env: childEnv });
4552
- fs20.closeSync(outFd);
4646
+ fs21.closeSync(outFd);
4553
4647
  logPath = nextLogPath;
4554
4648
  };
4555
4649
  const waitForHealth = async () => {
@@ -4563,7 +4657,7 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
4563
4657
  const readLogTail = () => {
4564
4658
  if (!logPath) return "";
4565
4659
  try {
4566
- return fs20.readFileSync(logPath, "utf-8").slice(-2e3);
4660
+ return fs21.readFileSync(logPath, "utf-8").slice(-2e3);
4567
4661
  } catch {
4568
4662
  return "";
4569
4663
  }
@@ -4616,9 +4710,9 @@ ${tail}`
4616
4710
  }
4617
4711
  function readDotenvApiKeys(projectDir) {
4618
4712
  const dotenvPath = path18.join(projectDir, ".env");
4619
- if (!fs20.existsSync(dotenvPath)) return {};
4713
+ if (!fs21.existsSync(dotenvPath)) return {};
4620
4714
  const result = {};
4621
- for (const rawLine of fs20.readFileSync(dotenvPath, "utf-8").split("\n")) {
4715
+ for (const rawLine of fs21.readFileSync(dotenvPath, "utf-8").split("\n")) {
4622
4716
  const line = rawLine.trim();
4623
4717
  if (!line || line.startsWith("#")) continue;
4624
4718
  const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
@@ -4650,14 +4744,14 @@ var init_litellm = __esm({
4650
4744
  });
4651
4745
 
4652
4746
  // src/pushWithRetry.ts
4653
- import { execFileSync as execFileSync5 } from "child_process";
4747
+ import { execFileSync as execFileSync6 } from "child_process";
4654
4748
  function sleepSync(ms) {
4655
4749
  if (ms <= 0) return;
4656
4750
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
4657
4751
  }
4658
4752
  function runGit(args, cwd) {
4659
4753
  try {
4660
- const stdout = execFileSync5("git", args, {
4754
+ const stdout = execFileSync6("git", args, {
4661
4755
  cwd,
4662
4756
  encoding: "utf-8",
4663
4757
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
@@ -4732,12 +4826,12 @@ var init_pushWithRetry = __esm({
4732
4826
  });
4733
4827
 
4734
4828
  // src/commit.ts
4735
- import { execFileSync as execFileSync6 } from "child_process";
4736
- import * as fs21 from "fs";
4829
+ import { execFileSync as execFileSync7 } from "child_process";
4830
+ import * as fs22 from "fs";
4737
4831
  import * as path19 from "path";
4738
4832
  function git(args, cwd) {
4739
4833
  try {
4740
- return execFileSync6("git", args, {
4834
+ return execFileSync7("git", args, {
4741
4835
  encoding: "utf-8",
4742
4836
  timeout: 12e4,
4743
4837
  cwd,
@@ -4773,17 +4867,17 @@ function ensureGitIdentity(cwd) {
4773
4867
  function abortUnfinishedGitOps(cwd) {
4774
4868
  const aborted = [];
4775
4869
  const gitDir = path19.join(cwd ?? process.cwd(), ".git");
4776
- if (!fs21.existsSync(gitDir)) return aborted;
4777
- if (fs21.existsSync(path19.join(gitDir, "MERGE_HEAD"))) {
4870
+ if (!fs22.existsSync(gitDir)) return aborted;
4871
+ if (fs22.existsSync(path19.join(gitDir, "MERGE_HEAD"))) {
4778
4872
  if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
4779
4873
  }
4780
- if (fs21.existsSync(path19.join(gitDir, "CHERRY_PICK_HEAD"))) {
4874
+ if (fs22.existsSync(path19.join(gitDir, "CHERRY_PICK_HEAD"))) {
4781
4875
  if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
4782
4876
  }
4783
- if (fs21.existsSync(path19.join(gitDir, "REVERT_HEAD"))) {
4877
+ if (fs22.existsSync(path19.join(gitDir, "REVERT_HEAD"))) {
4784
4878
  if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
4785
4879
  }
4786
- if (fs21.existsSync(path19.join(gitDir, "rebase-merge")) || fs21.existsSync(path19.join(gitDir, "rebase-apply"))) {
4880
+ if (fs22.existsSync(path19.join(gitDir, "rebase-merge")) || fs22.existsSync(path19.join(gitDir, "rebase-apply"))) {
4787
4881
  if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
4788
4882
  }
4789
4883
  try {
@@ -4804,7 +4898,7 @@ function isForbiddenPath(p) {
4804
4898
  return false;
4805
4899
  }
4806
4900
  function listChangedFiles(cwd) {
4807
- const raw = execFileSync6("git", ["status", "--porcelain=v1", "-z"], {
4901
+ const raw = execFileSync7("git", ["status", "--porcelain=v1", "-z"], {
4808
4902
  encoding: "utf-8",
4809
4903
  cwd,
4810
4904
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
@@ -4816,7 +4910,7 @@ function listChangedFiles(cwd) {
4816
4910
  }
4817
4911
  function listFilesInCommit(ref = "HEAD", cwd) {
4818
4912
  try {
4819
- const raw = execFileSync6("git", ["show", "--name-only", "--pretty=format:", "-z", ref], {
4913
+ const raw = execFileSync7("git", ["show", "--name-only", "--pretty=format:", "-z", ref], {
4820
4914
  encoding: "utf-8",
4821
4915
  cwd,
4822
4916
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
@@ -4839,7 +4933,7 @@ function normalizeCommitMessage(raw) {
4839
4933
  function commitAndPush(branch, agentMessage, cwd) {
4840
4934
  const allChanged = listChangedFiles(cwd);
4841
4935
  const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
4842
- const mergeHeadExists = fs21.existsSync(path19.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
4936
+ const mergeHeadExists = fs22.existsSync(path19.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
4843
4937
  if (allowedFiles.length === 0 && !mergeHeadExists) {
4844
4938
  return { committed: false, pushed: false, sha: "", message: "" };
4845
4939
  }
@@ -4995,10 +5089,10 @@ var init_saveTaskState = __esm({
4995
5089
  });
4996
5090
 
4997
5091
  // src/scripts/advanceFlow.ts
4998
- import { execFileSync as execFileSync7 } from "child_process";
5092
+ import { execFileSync as execFileSync8 } from "child_process";
4999
5093
  function ghComment(issueNumber, body, cwd, label) {
5000
5094
  try {
5001
- execFileSync7("gh", ["issue", "comment", String(issueNumber), "--body", body], {
5095
+ execFileSync8("gh", ["issue", "comment", String(issueNumber), "--body", body], {
5002
5096
  timeout: API_TIMEOUT_MS3,
5003
5097
  cwd,
5004
5098
  stdio: ["ignore", "pipe", "pipe"]
@@ -5069,78 +5163,11 @@ var init_advanceFlow = __esm({
5069
5163
  `
5070
5164
  );
5071
5165
  }
5072
- ctx.output.nextDispatch = { executable: flow.name, cliArgs: { issue: flow.issueNumber } };
5166
+ ctx.output.nextDispatch = { action: flow.name, cliArgs: { issue: flow.issueNumber } };
5073
5167
  };
5074
5168
  }
5075
5169
  });
5076
5170
 
5077
- // src/gha.ts
5078
- import { execFileSync as execFileSync8 } from "child_process";
5079
- import * as fs22 from "fs";
5080
- function getRunUrl() {
5081
- const server = process.env.GITHUB_SERVER_URL;
5082
- const repo = process.env.GITHUB_REPOSITORY;
5083
- const runId = process.env.GITHUB_RUN_ID;
5084
- if (!server || !repo || !runId) return "";
5085
- return `${server}/${repo}/actions/runs/${runId}`;
5086
- }
5087
- function reactToTriggerComment(cwd) {
5088
- if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
5089
- const eventPath = process.env.GITHUB_EVENT_PATH;
5090
- if (!eventPath || !fs22.existsSync(eventPath)) return;
5091
- let event = null;
5092
- try {
5093
- event = JSON.parse(fs22.readFileSync(eventPath, "utf-8"));
5094
- } catch {
5095
- return;
5096
- }
5097
- const commentId = event?.comment?.id;
5098
- const repo = process.env.GITHUB_REPOSITORY;
5099
- if (!commentId || !repo) return;
5100
- const token = process.env.KODY_TOKEN?.trim() || process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
5101
- const args = [
5102
- "api",
5103
- "-X",
5104
- "POST",
5105
- "-H",
5106
- "Accept: application/vnd.github+json",
5107
- `/repos/${repo}/issues/comments/${commentId}/reactions`,
5108
- "-f",
5109
- "content=eyes"
5110
- ];
5111
- const opts = {
5112
- cwd,
5113
- env: { ...process.env, GH_TOKEN: token ?? process.env.GH_TOKEN ?? "" },
5114
- stdio: "pipe",
5115
- timeout: 15e3
5116
- };
5117
- let lastErr = null;
5118
- for (let attempt = 0; attempt < 3; attempt++) {
5119
- if (attempt > 0) sleepMs(attempt === 1 ? 500 : 1500);
5120
- try {
5121
- execFileSync8("gh", args, opts);
5122
- return;
5123
- } catch (err) {
5124
- lastErr = err;
5125
- }
5126
- }
5127
- process.stderr.write(
5128
- `[kody] \u{1F440} reaction failed after 3 attempts on comment ${commentId}: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}
5129
- `
5130
- );
5131
- }
5132
- function sleepMs(ms) {
5133
- try {
5134
- execFileSync8("sleep", [(ms / 1e3).toString()], { stdio: "ignore", timeout: ms + 1e3 });
5135
- } catch {
5136
- }
5137
- }
5138
- var init_gha = __esm({
5139
- "src/gha.ts"() {
5140
- "use strict";
5141
- }
5142
- });
5143
-
5144
5171
  // src/stateBranch.ts
5145
5172
  function is404(err) {
5146
5173
  const msg = err instanceof Error ? err.message : String(err);
@@ -5576,10 +5603,10 @@ var init_state2 = __esm({
5576
5603
  "use strict";
5577
5604
  VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "awaiting-merge", "done"]);
5578
5605
  GoalStateError = class extends Error {
5579
- constructor(path43, message) {
5580
- super(`Invalid goal state at ${path43}:
5606
+ constructor(path45, message) {
5607
+ super(`Invalid goal state at ${path45}:
5581
5608
  ${message}`);
5582
- this.path = path43;
5609
+ this.path = path45;
5583
5610
  this.name = "GoalStateError";
5584
5611
  }
5585
5612
  path;
@@ -6460,7 +6487,7 @@ function dispatchTaskRun(issueNumber, base, cwd) {
6460
6487
  "-f",
6461
6488
  `issue_number=${issueNumber}`,
6462
6489
  "-f",
6463
- "executable=classify",
6490
+ "duty=classify",
6464
6491
  "-f",
6465
6492
  `base=${base}`
6466
6493
  ],
@@ -7186,7 +7213,7 @@ var init_dispatch = __esm({
7186
7213
  const usePr = target === "pr" && state?.core.prUrl;
7187
7214
  const targetNumber = usePr ? parsePr(state.core.prUrl) ?? issueNumber : issueNumber;
7188
7215
  ctx.output.nextDispatch = {
7189
- executable: next,
7216
+ action: next,
7190
7217
  cliArgs: usePr ? { pr: targetNumber } : { issue: targetNumber }
7191
7218
  };
7192
7219
  };
@@ -7251,228 +7278,62 @@ ${stateBody}`;
7251
7278
  if (base && getProfileInputs(classification)?.some((i) => i.name === "base")) {
7252
7279
  cliArgs.base = base;
7253
7280
  }
7254
- ctx.output.nextDispatch = { executable: classification, cliArgs };
7281
+ ctx.output.nextDispatch = { action: classification, cliArgs };
7255
7282
  };
7256
7283
  }
7257
7284
  });
7258
7285
 
7259
- // src/jobIdentity.ts
7260
- function stableJobKey(job) {
7261
- const duty = job.duty ?? job.action;
7262
- const executable = job.executable ?? duty ?? "unknown";
7263
- if (job.flavor === "scheduled" && job.duty) return `scheduled:${job.duty}:${executable}`;
7264
- const target = typeof job.target === "number" ? job.target : targetFromCliArgs(job.cliArgs);
7265
- const work = duty ?? executable;
7266
- return target === void 0 ? `${job.flavor}:${work}` : `${job.flavor}:${work}:${target}`;
7286
+ // src/scripts/issueStateComment.ts
7287
+ function isStateEnvelope(x) {
7288
+ if (x === null || typeof x !== "object") return false;
7289
+ const o = x;
7290
+ return o.version === 1 && typeof o.rev === "number" && Number.isInteger(o.rev) && o.rev >= 0 && typeof o.cursor === "string" && typeof o.done === "boolean" && o.data !== null && typeof o.data === "object" && !Array.isArray(o.data);
7267
7291
  }
7268
- function targetFromCliArgs(cliArgs) {
7269
- if (!cliArgs) return void 0;
7270
- for (const key of ["issue", "pr", "target", "issue_number"]) {
7271
- const value = cliArgs[key];
7272
- if (typeof value === "number" && Number.isFinite(value)) return value;
7273
- }
7274
- return void 0;
7292
+ function initialStateEnvelope(cursor = "seed") {
7293
+ return { version: 1, rev: 0, cursor, data: {}, done: false };
7275
7294
  }
7276
- var init_jobIdentity = __esm({
7277
- "src/jobIdentity.ts"() {
7278
- "use strict";
7279
- }
7280
- });
7295
+ function formatStateCommentBody(marker, state) {
7296
+ return `<!-- ${marker} -->
7281
7297
 
7282
- // src/job.ts
7283
- var job_exports = {};
7284
- __export(job_exports, {
7285
- DEFAULT_INSTANT_PERSONA: () => DEFAULT_INSTANT_PERSONA,
7286
- InvalidJobError: () => InvalidJobError,
7287
- mintInstantJob: () => mintInstantJob,
7288
- mintScheduledJob: () => mintScheduledJob,
7289
- newJobId: () => newJobId,
7290
- runJob: () => runJob,
7291
- stableJobKey: () => stableJobKey,
7292
- validateJob: () => validateJob
7293
- });
7294
- function newJobId(flavor) {
7295
- localJobSeq += 1;
7296
- const runId = process.env.GITHUB_RUN_ID;
7297
- if (runId) return `gh-${runId}-${process.env.GITHUB_RUN_ATTEMPT ?? "1"}-${localJobSeq}`;
7298
- return `${flavor}-${Date.now()}-${localJobSeq}`;
7298
+ \`\`\`json
7299
+ ${JSON.stringify(state, null, 2)}
7300
+ \`\`\`
7301
+ `;
7299
7302
  }
7300
- function validateJob(input) {
7301
- if (!input || typeof input !== "object") {
7302
- throw new InvalidJobError("job must be an object");
7303
- }
7304
- const j = input;
7305
- if (typeof j.executable !== "string" && typeof j.duty !== "string" && typeof j.action !== "string") {
7306
- throw new InvalidJobError("job must reference a duty action, duty, or executable");
7303
+ function parseStateCommentBody(marker, body) {
7304
+ const markerLine = `<!-- ${marker} -->`;
7305
+ if (!body.trimStart().startsWith(markerLine)) return null;
7306
+ const fenceOpen = body.indexOf("```json");
7307
+ if (fenceOpen === -1) return null;
7308
+ const after = body.slice(fenceOpen + "```json".length);
7309
+ const fenceClose = after.indexOf("```");
7310
+ if (fenceClose === -1) return null;
7311
+ const jsonText = after.slice(0, fenceClose).trim();
7312
+ let parsed;
7313
+ try {
7314
+ parsed = JSON.parse(jsonText);
7315
+ } catch {
7316
+ return null;
7307
7317
  }
7308
- if (j.flavor !== "instant" && j.flavor !== "scheduled") {
7309
- throw new InvalidJobError(`job.flavor must be "instant" or "scheduled" (got ${String(j.flavor)})`);
7318
+ return isStateEnvelope(parsed) ? parsed : null;
7319
+ }
7320
+ function listIssueComments(owner, repo, issueNumber, cwd) {
7321
+ const raw = gh(["api", "--paginate", `repos/${owner}/${repo}/issues/${issueNumber}/comments`], { cwd });
7322
+ let parsed;
7323
+ try {
7324
+ parsed = JSON.parse(raw);
7325
+ } catch {
7326
+ return [];
7310
7327
  }
7311
- if (j.cliArgs !== void 0 && (typeof j.cliArgs !== "object" || j.cliArgs === null)) {
7312
- throw new InvalidJobError("job.cliArgs must be an object when present");
7313
- }
7314
- return {
7315
- action: typeof j.action === "string" ? j.action : void 0,
7316
- executable: typeof j.executable === "string" ? j.executable : void 0,
7317
- duty: typeof j.duty === "string" ? j.duty : void 0,
7318
- why: typeof j.why === "string" ? j.why : void 0,
7319
- persona: typeof j.persona === "string" ? j.persona : void 0,
7320
- schedule: typeof j.schedule === "string" ? j.schedule : void 0,
7321
- target: typeof j.target === "number" ? j.target : void 0,
7322
- cliArgs: j.cliArgs ?? {},
7323
- flavor: j.flavor,
7324
- force: j.force === true
7325
- };
7326
- }
7327
- async function runJob(job, base) {
7328
- const valid = validateJob(job);
7329
- const action = valid.action ?? valid.duty;
7330
- const resolvedDuty = action ? resolveDutyAction(action) : null;
7331
- const profileName = valid.executable ?? resolvedDuty?.executable ?? valid.duty;
7332
- if (!profileName) {
7333
- throw new InvalidJobError("job resolves to no executable or duty");
7334
- }
7335
- const preloadedData = { ...base.preloadedData ?? {} };
7336
- preloadedData.jobId = newJobId(valid.flavor);
7337
- preloadedData.jobKey = stableJobKey(valid);
7338
- preloadedData.jobFlavor = valid.flavor;
7339
- if (valid.target !== void 0) preloadedData.jobTarget = valid.target;
7340
- if (valid.action !== void 0 && valid.action.length > 0) preloadedData.jobAction = valid.action;
7341
- const dutyIdentity = valid.duty ?? resolvedDuty?.duty;
7342
- if (dutyIdentity !== void 0 && dutyIdentity.length > 0) preloadedData.jobDuty = dutyIdentity;
7343
- const executableIdentity = valid.executable ?? resolvedDuty?.executable;
7344
- if (executableIdentity !== void 0 && executableIdentity.length > 0)
7345
- preloadedData.jobExecutable = executableIdentity;
7346
- if (valid.schedule !== void 0 && valid.schedule.length > 0) preloadedData.jobSchedule = valid.schedule;
7347
- const dutyContext = loadDutyContext(dutyIdentity ?? valid.duty);
7348
- if (dutyContext) {
7349
- preloadedData.dutySlug = dutyContext.slug;
7350
- preloadedData.dutyTitle = dutyContext.title;
7351
- preloadedData.dutyIntent = dutyContext.body;
7352
- preloadedData.jobIntent = dutyContext.body;
7353
- if (preloadedData.jobDuty === void 0) preloadedData.jobDuty = dutyContext.slug;
7354
- if (dutyContext.config.staff && preloadedData.jobPersona === void 0) {
7355
- preloadedData.jobPersona = dutyContext.config.staff;
7356
- }
7357
- if (dutyContext.config.every && preloadedData.jobSchedule === void 0) {
7358
- preloadedData.jobSchedule = dutyContext.config.every;
7359
- }
7360
- if (dutyContext.config.mentions && dutyContext.config.mentions.length > 0) {
7361
- preloadedData.mentions = dutyContext.config.mentions.map((login) => `@${login}`).join(" ");
7362
- }
7363
- }
7364
- if (valid.why !== void 0 && valid.why.length > 0) preloadedData.jobWhy = valid.why;
7365
- if (valid.persona !== void 0) preloadedData.jobPersona = valid.persona;
7366
- const input = {
7367
- cliArgs: { ...valid.cliArgs },
7368
- cwd: base.cwd,
7369
- config: base.config,
7370
- skipConfig: base.skipConfig,
7371
- verbose: base.verbose,
7372
- quiet: base.quiet,
7373
- preloadedData: Object.keys(preloadedData).length > 0 ? preloadedData : void 0
7374
- };
7375
- input.cliArgs = resolvedDuty ? { ...resolvedDuty.cliArgs, ...input.cliArgs } : input.cliArgs;
7376
- const run = base.chain === false ? runExecutable : runExecutableChain;
7377
- return run(profileName, input);
7378
- }
7379
- function loadDutyContext(slug) {
7380
- if (!slug) return null;
7381
- return readDutyFolder(getProjectDutiesRoot(), slug) ?? readDutyFolder(getBuiltinDutiesRoot(), slug);
7382
- }
7383
- function mintInstantJob(dispatch2, opts) {
7384
- return {
7385
- action: dispatch2.action,
7386
- executable: dispatch2.executable,
7387
- duty: dispatch2.duty,
7388
- why: opts?.why ?? dispatch2.why,
7389
- persona: opts?.persona ?? DEFAULT_INSTANT_PERSONA,
7390
- target: dispatch2.target,
7391
- cliArgs: dispatch2.cliArgs,
7392
- flavor: "instant"
7393
- };
7394
- }
7395
- function mintScheduledJob(input) {
7396
- return {
7397
- duty: input.duty,
7398
- executable: input.executable,
7399
- schedule: input.schedule,
7400
- persona: input.persona,
7401
- cliArgs: input.cliArgs ?? {},
7402
- flavor: "scheduled"
7403
- };
7404
- }
7405
- var DEFAULT_INSTANT_PERSONA, localJobSeq, InvalidJobError;
7406
- var init_job = __esm({
7407
- "src/job.ts"() {
7408
- "use strict";
7409
- init_executor();
7410
- init_dutyFolders();
7411
- init_registry();
7412
- init_jobIdentity();
7413
- init_jobIdentity();
7414
- DEFAULT_INSTANT_PERSONA = "kody";
7415
- localJobSeq = 0;
7416
- InvalidJobError = class extends Error {
7417
- constructor(message) {
7418
- super(message);
7419
- this.name = "InvalidJobError";
7420
- }
7421
- };
7422
- }
7423
- });
7424
-
7425
- // src/scripts/issueStateComment.ts
7426
- function isStateEnvelope(x) {
7427
- if (x === null || typeof x !== "object") return false;
7428
- const o = x;
7429
- return o.version === 1 && typeof o.rev === "number" && Number.isInteger(o.rev) && o.rev >= 0 && typeof o.cursor === "string" && typeof o.done === "boolean" && o.data !== null && typeof o.data === "object" && !Array.isArray(o.data);
7430
- }
7431
- function initialStateEnvelope(cursor = "seed") {
7432
- return { version: 1, rev: 0, cursor, data: {}, done: false };
7433
- }
7434
- function formatStateCommentBody(marker, state) {
7435
- return `<!-- ${marker} -->
7436
-
7437
- \`\`\`json
7438
- ${JSON.stringify(state, null, 2)}
7439
- \`\`\`
7440
- `;
7441
- }
7442
- function parseStateCommentBody(marker, body) {
7443
- const markerLine = `<!-- ${marker} -->`;
7444
- if (!body.trimStart().startsWith(markerLine)) return null;
7445
- const fenceOpen = body.indexOf("```json");
7446
- if (fenceOpen === -1) return null;
7447
- const after = body.slice(fenceOpen + "```json".length);
7448
- const fenceClose = after.indexOf("```");
7449
- if (fenceClose === -1) return null;
7450
- const jsonText = after.slice(0, fenceClose).trim();
7451
- let parsed;
7452
- try {
7453
- parsed = JSON.parse(jsonText);
7454
- } catch {
7455
- return null;
7456
- }
7457
- return isStateEnvelope(parsed) ? parsed : null;
7458
- }
7459
- function listIssueComments(owner, repo, issueNumber, cwd) {
7460
- const raw = gh(["api", "--paginate", `repos/${owner}/${repo}/issues/${issueNumber}/comments`], { cwd });
7461
- let parsed;
7462
- try {
7463
- parsed = JSON.parse(raw);
7464
- } catch {
7465
- return [];
7466
- }
7467
- if (!Array.isArray(parsed)) return [];
7468
- return parsed.filter((c) => typeof c.id === "number" && typeof c.node_id === "string" && typeof c.body === "string").map((c) => ({ id: c.id, node_id: c.node_id, body: c.body }));
7469
- }
7470
- function findStateComment2(owner, repo, issueNumber, marker, cwd) {
7471
- const comments = listIssueComments(owner, repo, issueNumber, cwd);
7472
- for (const c of comments) {
7473
- const state = parseStateCommentBody(marker, c.body);
7474
- if (!state) continue;
7475
- return { commentId: c.id, commentNodeId: c.node_id, state };
7328
+ if (!Array.isArray(parsed)) return [];
7329
+ return parsed.filter((c) => typeof c.id === "number" && typeof c.node_id === "string" && typeof c.body === "string").map((c) => ({ id: c.id, node_id: c.node_id, body: c.body }));
7330
+ }
7331
+ function findStateComment2(owner, repo, issueNumber, marker, cwd) {
7332
+ const comments = listIssueComments(owner, repo, issueNumber, cwd);
7333
+ for (const c of comments) {
7334
+ const state = parseStateCommentBody(marker, c.body);
7335
+ if (!state) continue;
7336
+ return { commentId: c.id, commentNodeId: c.node_id, state };
7476
7337
  }
7477
7338
  return null;
7478
7339
  }
@@ -7518,9 +7379,13 @@ function isStateUnchanged(prev, next) {
7518
7379
  return JSON.stringify(prev.data) === JSON.stringify(next.data);
7519
7380
  }
7520
7381
  function stateFilePath(jobsDir, slug) {
7521
- return `${jobsDir.replace(/\/+$/, "")}/${slug}.state.json`;
7382
+ return `${jobsDir.replace(/\/+$/, "")}/${slug}/state.json`;
7522
7383
  }
7523
7384
  function slugFromStateFilePath(filePath) {
7385
+ if (/\/state\.json$/i.test(filePath)) {
7386
+ const parts = filePath.split("/");
7387
+ return parts.at(-2) ?? filePath;
7388
+ }
7524
7389
  const last = filePath.split("/").pop() ?? filePath;
7525
7390
  return last.replace(/\.state\.json$/i, "");
7526
7391
  }
@@ -7821,6 +7686,29 @@ var init_jobState = __esm({
7821
7686
  }
7822
7687
  });
7823
7688
 
7689
+ // src/jobIdentity.ts
7690
+ function stableJobKey(job) {
7691
+ const duty = job.duty ?? job.action;
7692
+ const executable = job.executable ?? duty ?? "unknown";
7693
+ if (job.flavor === "scheduled" && job.duty) return `scheduled:${job.duty}:${executable}`;
7694
+ const target = typeof job.target === "number" ? job.target : targetFromCliArgs(job.cliArgs);
7695
+ const work = duty && executable && executable !== duty ? `${duty}:${executable}` : duty ?? executable;
7696
+ return target === void 0 ? `${job.flavor}:${work}` : `${job.flavor}:${work}:${target}`;
7697
+ }
7698
+ function targetFromCliArgs(cliArgs) {
7699
+ if (!cliArgs) return void 0;
7700
+ for (const key of ["issue", "pr", "target", "issue_number"]) {
7701
+ const value = cliArgs[key];
7702
+ if (typeof value === "number" && Number.isFinite(value)) return value;
7703
+ }
7704
+ return void 0;
7705
+ }
7706
+ var init_jobIdentity = __esm({
7707
+ "src/jobIdentity.ts"() {
7708
+ "use strict";
7709
+ }
7710
+ });
7711
+
7824
7712
  // src/scripts/planTaskJobs.ts
7825
7713
  function parseTaskJobSpecs(body) {
7826
7714
  const match = body.match(new RegExp(`<!--\\s*${TASK_JOBS_MARKER}\\s*([\\s\\S]*?)-->`));
@@ -7838,7 +7726,7 @@ function taskJobSpecToJob(spec, issueNumber) {
7838
7726
  const cliArgs = spec.cliArgs ?? { issue: issueNumber };
7839
7727
  const target = typeof spec.target === "number" ? spec.target : targetFromCliArgs(cliArgs) ?? issueNumber;
7840
7728
  return {
7841
- duty: spec.duty,
7729
+ duty: spec.duty ?? spec.executable,
7842
7730
  executable: spec.executable,
7843
7731
  why: spec.reason,
7844
7732
  persona: spec.persona ?? spec.staff,
@@ -7881,7 +7769,7 @@ function jobToPlannedTaskJob(job) {
7881
7769
  return {
7882
7770
  id: stableJobKey(job),
7883
7771
  executable: job.executable ?? job.duty ?? "unknown",
7884
- ...job.duty ? { duty: job.duty } : {},
7772
+ duty: job.duty ?? job.action ?? job.executable ?? "unknown",
7885
7773
  ...job.persona ? { staff: job.persona } : {},
7886
7774
  ...job.flavor ? { flavor: job.flavor } : {},
7887
7775
  ...job.schedule ? { schedule: job.schedule } : {},
@@ -8032,9 +7920,9 @@ var init_dispatchDutyFileTicks = __esm({
8032
7920
  init_dutyFolders();
8033
7921
  init_issue();
8034
7922
  init_job();
8035
- init_scheduleEvery();
8036
7923
  init_jobState();
8037
7924
  init_planTaskJobs();
7925
+ init_scheduleEvery();
8038
7926
  dispatchDutyFileTicks = async (ctx, _profile, args) => {
8039
7927
  ctx.skipAgent = true;
8040
7928
  const targetExecutable = String(args?.targetExecutable ?? "");
@@ -8127,7 +8015,7 @@ var init_dispatchDutyFileTicks = __esm({
8127
8015
  continue;
8128
8016
  }
8129
8017
  const slugTarget = config.tickScript ? scriptedExecutable : config.executable ?? targetExecutable;
8130
- const cliArgs = config.executable ? {} : { [slugArg]: slug };
8018
+ const cliArgs = { [slugArg]: slug };
8131
8019
  process.stdout.write(`[jobs] \u2192 tick ${slug} (${slugTarget})
8132
8020
  `);
8133
8021
  try {
@@ -8193,8 +8081,8 @@ var dispatchDutyTicks;
8193
8081
  var init_dispatchDutyTicks = __esm({
8194
8082
  "src/scripts/dispatchDutyTicks.ts"() {
8195
8083
  "use strict";
8196
- init_executor();
8197
8084
  init_issue();
8085
+ init_job();
8198
8086
  dispatchDutyTicks = async (ctx, _profile, args) => {
8199
8087
  ctx.skipAgent = true;
8200
8088
  const label = String(args?.label ?? "");
@@ -8216,13 +8104,14 @@ var init_dispatchDutyTicks = __esm({
8216
8104
  process.stdout.write(`[jobs] \u2192 tick #${issue.number}: ${issue.title}
8217
8105
  `);
8218
8106
  try {
8219
- const out = await runExecutable(targetExecutable, {
8220
- cliArgs: { [issueArg]: issue.number },
8221
- cwd: ctx.cwd,
8222
- config: ctx.config,
8223
- verbose: ctx.verbose,
8224
- quiet: ctx.quiet
8225
- });
8107
+ const out = await runJob(
8108
+ mintScheduledJob({
8109
+ duty: targetExecutable,
8110
+ executable: targetExecutable,
8111
+ cliArgs: { [issueArg]: issue.number }
8112
+ }),
8113
+ { cwd: ctx.cwd, config: ctx.config, verbose: ctx.verbose, quiet: ctx.quiet, chain: false }
8114
+ );
8226
8115
  results.push({ issue: issue.number, exitCode: out.exitCode, reason: out.reason });
8227
8116
  if (out.exitCode !== 0) {
8228
8117
  process.stderr.write(`[jobs] tick #${issue.number} failed (exit ${out.exitCode}): ${out.reason ?? ""}
@@ -8277,8 +8166,8 @@ var init_dispatchNextTask = __esm({
8277
8166
  function taskJobToJob(job, issueArg) {
8278
8167
  const target = typeof job.target === "number" ? job.target : typeof issueArg === "number" ? issueArg : void 0;
8279
8168
  return {
8169
+ duty: job.duty ?? job.executable,
8280
8170
  executable: job.executable,
8281
- ...job.duty ? { duty: job.duty } : {},
8282
8171
  ...job.reason ? { why: job.reason } : {},
8283
8172
  ...job.staff ? { persona: job.staff } : {},
8284
8173
  ...job.schedule ? { schedule: job.schedule } : {},
@@ -8289,7 +8178,7 @@ function taskJobToJob(job, issueArg) {
8289
8178
  function isJob(input) {
8290
8179
  if (!input || typeof input !== "object" || Array.isArray(input)) return false;
8291
8180
  const job = input;
8292
- return typeof job.executable === "string" && (job.flavor === "instant" || job.flavor === "scheduled") && (!job.cliArgs || typeof job.cliArgs === "object" && !Array.isArray(job.cliArgs));
8181
+ return (typeof job.duty === "string" || typeof job.action === "string") && (job.flavor === "instant" || job.flavor === "scheduled") && (!job.cliArgs || typeof job.cliArgs === "object" && !Array.isArray(job.cliArgs));
8293
8182
  }
8294
8183
  var dispatchNextTaskJob;
8295
8184
  var init_dispatchNextTaskJob = __esm({
@@ -8310,7 +8199,7 @@ var init_dispatchNextTaskJob = __esm({
8310
8199
  const plannedJobs = Array.isArray(ctx.data.plannedTaskJobs) ? ctx.data.plannedTaskJobs.filter(isJob) : [];
8311
8200
  ctx.output.nextJob = plannedJobs.find((job) => stableJobKey(job) === next.id) ?? taskJobToJob(next, ctx.args.issue);
8312
8201
  if (typeof ctx.args.issue === "number") {
8313
- ctx.output.afterNextJob = { executable: profile.name, cliArgs: { issue: ctx.args.issue } };
8202
+ ctx.output.afterNextJob = { action: profile.action ?? profile.name, cliArgs: { issue: ctx.args.issue } };
8314
8203
  }
8315
8204
  };
8316
8205
  }
@@ -8630,6 +8519,7 @@ var init_failOnceTaskJob = __esm({
8630
8519
  ctx.skipAgent = true;
8631
8520
  const issue = typeof ctx.args.issue === "number" ? ctx.args.issue : void 0;
8632
8521
  const fallbackJob = {
8522
+ duty: profile.action ?? profile.name,
8633
8523
  executable: profile.name,
8634
8524
  flavor: "instant",
8635
8525
  ...typeof issue === "number" ? { target: issue, cliArgs: { issue } } : { cliArgs: {} }
@@ -9528,9 +9418,9 @@ function performInit(cwd, force) {
9528
9418
  return { wrote, skipped, labels };
9529
9419
  }
9530
9420
  function renderScheduledWorkflow(name, cron) {
9531
- return `# Scheduled kody executable: ${name}
9421
+ return `# Scheduled kody duty: ${name}
9532
9422
  # Generated by \`kody init\`. Regenerate with \`kody init --force\`.
9533
- # Edit the cron below or the executable's profile.json#schedule.
9423
+ # Edit the cron below or the duty's implementation profile.json#schedule.
9534
9424
 
9535
9425
  name: kody ${name}
9536
9426
 
@@ -12491,11 +12381,46 @@ var init_runPreviewBuild = __esm({
12491
12381
  }
12492
12382
  });
12493
12383
 
12494
- // src/scripts/runTickScript.ts
12384
+ // src/scripts/tickShellRunner.ts
12495
12385
  import { spawnSync as spawnSync2 } from "child_process";
12496
- import * as fs36 from "fs";
12497
- import * as path35 from "path";
12498
- function buildChildEnv(parent, force) {
12386
+ function runTickShellAndParse(opts) {
12387
+ const childEnv = buildTickChildEnv(process.env, opts.force);
12388
+ const result = spawnSync2("bash", [opts.scriptPath], {
12389
+ cwd: opts.ctx.cwd,
12390
+ env: childEnv,
12391
+ stdio: ["ignore", "pipe", "pipe"],
12392
+ encoding: "utf-8",
12393
+ timeout: 5 * 60 * 1e3,
12394
+ maxBuffer: 16 * 1024 * 1024
12395
+ });
12396
+ if (result.stdout) process.stdout.write(result.stdout);
12397
+ if (result.stderr) process.stderr.write(result.stderr);
12398
+ if (result.error) {
12399
+ opts.ctx.output.exitCode = 99;
12400
+ opts.ctx.output.reason = `${opts.displayName}: spawn error: ${result.error.message}`;
12401
+ return;
12402
+ }
12403
+ if (result.signal) {
12404
+ opts.ctx.output.exitCode = 124;
12405
+ opts.ctx.output.reason = `${opts.displayName}: ${opts.reasonSubject ? `${opts.reasonSubject} ` : ""}killed by ${result.signal} (likely 5min timeout)`;
12406
+ return;
12407
+ }
12408
+ if (result.status !== 0) {
12409
+ opts.ctx.output.exitCode = result.status ?? 99;
12410
+ opts.ctx.output.reason = `${opts.displayName}: ${opts.reasonSubject ? `${opts.reasonSubject} ` : ""}exited ${result.status}`;
12411
+ return;
12412
+ }
12413
+ const prevRev = opts.loaded.state.rev ?? 0;
12414
+ const parsed = extractNextStateFromText(result.stdout ?? "", opts.fenceLabel, prevRev);
12415
+ if (parsed.error) {
12416
+ opts.ctx.data.nextStateParseError = parsed.error;
12417
+ opts.ctx.output.exitCode = 1;
12418
+ opts.ctx.output.reason = `${opts.displayName}: ${parsed.error}`;
12419
+ return;
12420
+ }
12421
+ opts.ctx.data.nextJobState = parsed.envelope;
12422
+ }
12423
+ function buildTickChildEnv(parent, force) {
12499
12424
  const allow = /* @__PURE__ */ new Set([
12500
12425
  "PATH",
12501
12426
  "HOME",
@@ -12506,45 +12431,104 @@ function buildChildEnv(parent, force) {
12506
12431
  "LANG",
12507
12432
  "LC_ALL",
12508
12433
  "TERM",
12509
- // GitHub auth — `gh` reads these.
12510
12434
  "GH_TOKEN",
12511
12435
  "GH_PAT",
12512
12436
  "GITHUB_TOKEN",
12513
- // CI metadata commonly read by tick scripts (`gh repo view`,
12514
- // workflow run links, etc.). All public values from GitHub Actions.
12515
- "GITHUB_ACTIONS",
12516
- "GITHUB_ACTOR",
12517
12437
  "GITHUB_REPOSITORY",
12518
- "GITHUB_REPOSITORY_OWNER",
12519
12438
  "GITHUB_REF",
12520
12439
  "GITHUB_SHA",
12521
12440
  "GITHUB_RUN_ID",
12522
- "GITHUB_RUN_NUMBER",
12441
+ "GITHUB_RUN_ATTEMPT",
12523
12442
  "GITHUB_WORKFLOW",
12524
- "GITHUB_JOB",
12525
- "GITHUB_SERVER_URL",
12526
- "GITHUB_API_URL",
12527
- "GITHUB_EVENT_NAME",
12528
- "RUNNER_OS",
12529
- "RUNNER_ARCH"
12443
+ "GITHUB_ACTIONS",
12444
+ "CI",
12445
+ "KODY_DRY_RUN",
12446
+ "KODY_NO_COMMIT"
12530
12447
  ]);
12531
- const out = {};
12448
+ const env = {};
12532
12449
  for (const [key, value] of Object.entries(parent)) {
12533
12450
  if (value === void 0) continue;
12534
- if (allow.has(key) || key.startsWith("KODY_PUBLIC_")) {
12535
- out[key] = value;
12536
- }
12451
+ if (allow.has(key) || key.startsWith("KODY_PUBLIC_")) env[key] = value;
12537
12452
  }
12538
- if (force) out.KODY_FORCE = "1";
12539
- return out;
12453
+ if (force) env.KODY_FORCE = "1";
12454
+ return env;
12540
12455
  }
12456
+ var init_tickShellRunner = __esm({
12457
+ "src/scripts/tickShellRunner.ts"() {
12458
+ "use strict";
12459
+ init_parseJobStateFromAgentResult();
12460
+ }
12461
+ });
12462
+
12463
+ // src/scripts/runScheduledExecutableTick.ts
12464
+ import * as fs36 from "fs";
12465
+ import * as path35 from "path";
12466
+ var runScheduledExecutableTick;
12467
+ var init_runScheduledExecutableTick = __esm({
12468
+ "src/scripts/runScheduledExecutableTick.ts"() {
12469
+ "use strict";
12470
+ init_dutyFolders();
12471
+ init_jobState();
12472
+ init_tickShellRunner();
12473
+ runScheduledExecutableTick = async (ctx, profile, args) => {
12474
+ ctx.skipAgent = true;
12475
+ const jobsDir = String(args?.jobsDir ?? ".kody/duties");
12476
+ const slugArg = String(args?.slugArg ?? "duty");
12477
+ const fenceLabel = String(args?.fenceLabel ?? "kody-job-next-state");
12478
+ const shell = String(args?.shell ?? "tick.sh");
12479
+ const slug = String(ctx.args[slugArg] ?? "").trim();
12480
+ if (!slug) {
12481
+ ctx.output.exitCode = 99;
12482
+ ctx.output.reason = `runScheduledExecutableTick: ctx.args.${slugArg} must be non-empty duty slug`;
12483
+ return;
12484
+ }
12485
+ const duty = readDutyFolder(path35.join(ctx.cwd, jobsDir), slug);
12486
+ if (!duty) {
12487
+ ctx.output.exitCode = 99;
12488
+ ctx.output.reason = `runScheduledExecutableTick: duty folder not found or incomplete: ${path35.join(jobsDir, slug)}`;
12489
+ return;
12490
+ }
12491
+ const shellPath = path35.join(profile.dir, shell);
12492
+ if (!fs36.existsSync(shellPath)) {
12493
+ ctx.output.exitCode = 99;
12494
+ ctx.output.reason = `runScheduledExecutableTick: shell not found: ${shell} (looked in ${profile.dir})`;
12495
+ return;
12496
+ }
12497
+ const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, jobsDir });
12498
+ let loaded;
12499
+ try {
12500
+ loaded = await backend.load(slug);
12501
+ } catch (err) {
12502
+ ctx.output.exitCode = 99;
12503
+ ctx.output.reason = `runScheduledExecutableTick: state load failed: ${err instanceof Error ? err.message : String(err)}`;
12504
+ return;
12505
+ }
12506
+ ctx.data.jobSlug = slug;
12507
+ ctx.data.dutySlug = slug;
12508
+ ctx.data.executableSlug = profile.name;
12509
+ ctx.data.jobState = loaded;
12510
+ runTickShellAndParse({
12511
+ ctx,
12512
+ loaded,
12513
+ scriptPath: shellPath,
12514
+ displayName: `runScheduledExecutableTick: ${shell}`,
12515
+ fenceLabel,
12516
+ force: Boolean(ctx.args.force)
12517
+ });
12518
+ };
12519
+ }
12520
+ });
12521
+
12522
+ // src/scripts/runTickScript.ts
12523
+ import * as fs37 from "fs";
12524
+ import * as path36 from "path";
12541
12525
  var runTickScript;
12542
12526
  var init_runTickScript = __esm({
12543
12527
  "src/scripts/runTickScript.ts"() {
12544
12528
  "use strict";
12545
12529
  init_dutyFolders();
12546
12530
  init_jobState();
12547
- init_parseJobStateFromAgentResult();
12531
+ init_tickShellRunner();
12548
12532
  runTickScript = async (ctx, _profile, args) => {
12549
12533
  ctx.skipAgent = true;
12550
12534
  const jobsDir = String(args?.jobsDir ?? ".kody/duties");
@@ -12556,10 +12540,10 @@ var init_runTickScript = __esm({
12556
12540
  ctx.output.reason = `runTickScript: ctx.args.${slugArg} must be a non-empty slug`;
12557
12541
  return;
12558
12542
  }
12559
- const duty = readDutyFolder(path35.join(ctx.cwd, jobsDir), slug);
12543
+ const duty = readDutyFolder(path36.join(ctx.cwd, jobsDir), slug);
12560
12544
  if (!duty) {
12561
12545
  ctx.output.exitCode = 99;
12562
- ctx.output.reason = `runTickScript: duty folder not found or incomplete: ${path35.join(ctx.cwd, jobsDir, slug)}`;
12546
+ ctx.output.reason = `runTickScript: duty folder not found or incomplete: ${path36.join(ctx.cwd, jobsDir, slug)}`;
12563
12547
  return;
12564
12548
  }
12565
12549
  const tickScript = duty.config.tickScript;
@@ -12568,8 +12552,8 @@ var init_runTickScript = __esm({
12568
12552
  ctx.output.reason = `runTickScript: duty ${slug} has no \`tickScript\` in profile.json \u2014 route via duty-tick instead`;
12569
12553
  return;
12570
12554
  }
12571
- const scriptPath = path35.isAbsolute(tickScript) ? tickScript : path35.join(ctx.cwd, tickScript);
12572
- if (!fs36.existsSync(scriptPath)) {
12555
+ const scriptPath = path36.isAbsolute(tickScript) ? tickScript : path36.join(ctx.cwd, tickScript);
12556
+ if (!fs37.existsSync(scriptPath)) {
12573
12557
  ctx.output.exitCode = 99;
12574
12558
  ctx.output.reason = `runTickScript: tickScript not found: ${scriptPath}`;
12575
12559
  return;
@@ -12585,46 +12569,15 @@ var init_runTickScript = __esm({
12585
12569
  }
12586
12570
  ctx.data.jobSlug = slug;
12587
12571
  ctx.data.jobState = loaded;
12588
- const childEnv = buildChildEnv(process.env, Boolean(ctx.args.force));
12589
- const result = spawnSync2("bash", [scriptPath], {
12590
- cwd: ctx.cwd,
12591
- env: childEnv,
12592
- stdio: ["ignore", "pipe", "pipe"],
12593
- encoding: "utf-8",
12594
- timeout: 5 * 60 * 1e3,
12595
- // Default maxBuffer is 1MB — a chatty `gh pr list --json …` over a
12596
- // busy repo (or an accidental `set -x`) can blow that and silently
12597
- // truncate stdout, which is the exact "silent state drop" failure
12598
- // mode this whole executable was written to prevent. 16MB is well
12599
- // above any realistic tick output.
12600
- maxBuffer: 16 * 1024 * 1024
12572
+ runTickShellAndParse({
12573
+ ctx,
12574
+ loaded,
12575
+ scriptPath,
12576
+ displayName: "runTickScript",
12577
+ reasonSubject: tickScript,
12578
+ fenceLabel,
12579
+ force: Boolean(ctx.args.force)
12601
12580
  });
12602
- if (result.stdout) process.stdout.write(result.stdout);
12603
- if (result.stderr) process.stderr.write(result.stderr);
12604
- if (result.error) {
12605
- ctx.output.exitCode = 99;
12606
- ctx.output.reason = `runTickScript: spawn error: ${result.error.message}`;
12607
- return;
12608
- }
12609
- if (result.signal) {
12610
- ctx.output.exitCode = 124;
12611
- ctx.output.reason = `runTickScript: ${tickScript} killed by ${result.signal} (likely 5min timeout)`;
12612
- return;
12613
- }
12614
- if (result.status !== 0) {
12615
- ctx.output.exitCode = result.status ?? 99;
12616
- ctx.output.reason = `runTickScript: ${tickScript} exited ${result.status}`;
12617
- return;
12618
- }
12619
- const prevRev = loaded.state.rev ?? 0;
12620
- const parsed = extractNextStateFromText(result.stdout ?? "", fenceLabel, prevRev);
12621
- if (parsed.error) {
12622
- ctx.data.nextStateParseError = parsed.error;
12623
- ctx.output.exitCode = 1;
12624
- ctx.output.reason = `runTickScript: ${parsed.error}`;
12625
- return;
12626
- }
12627
- ctx.data.nextJobState = parsed.envelope;
12628
12581
  };
12629
12582
  }
12630
12583
  });
@@ -12775,7 +12728,7 @@ var init_startFlow = __esm({
12775
12728
  const usePr = target === "pr" && !!state?.core.prUrl;
12776
12729
  const targetNumber = usePr ? parsePrNumber(state.core.prUrl) ?? issueNumber : issueNumber;
12777
12730
  ctx.output.nextDispatch = {
12778
- executable: entry,
12731
+ action: entry,
12779
12732
  cliArgs: usePr ? { pr: targetNumber } : { issue: targetNumber }
12780
12733
  };
12781
12734
  };
@@ -13533,7 +13486,7 @@ var init_writeJobStateFile = __esm({
13533
13486
  });
13534
13487
 
13535
13488
  // src/scripts/writeRunSummary.ts
13536
- import * as fs37 from "fs";
13489
+ import * as fs38 from "fs";
13537
13490
  var writeRunSummary;
13538
13491
  var init_writeRunSummary = __esm({
13539
13492
  "src/scripts/writeRunSummary.ts"() {
@@ -13559,7 +13512,7 @@ var init_writeRunSummary = __esm({
13559
13512
  if (reason) lines.push(`- **Reason:** ${reason}`);
13560
13513
  lines.push("");
13561
13514
  try {
13562
- fs37.appendFileSync(summaryPath, `${lines.join("\n")}
13515
+ fs38.appendFileSync(summaryPath, `${lines.join("\n")}
13563
13516
  `);
13564
13517
  } catch {
13565
13518
  }
@@ -13646,6 +13599,7 @@ var init_scripts = __esm({
13646
13599
  init_reviewFlow();
13647
13600
  init_runFlow();
13648
13601
  init_runPreviewBuild();
13602
+ init_runScheduledExecutableTick();
13649
13603
  init_runTickScript();
13650
13604
  init_saveGoalState();
13651
13605
  init_saveTaskState();
@@ -13704,6 +13658,7 @@ var init_scripts = __esm({
13704
13658
  dispatchDutyFileTicks,
13705
13659
  planTaskJobs,
13706
13660
  dispatchNextTaskJob,
13661
+ runScheduledExecutableTick,
13707
13662
  runTickScript,
13708
13663
  runPreviewBuild,
13709
13664
  loadGoalState,
@@ -13766,8 +13721,8 @@ var init_scripts = __esm({
13766
13721
  });
13767
13722
 
13768
13723
  // src/staff.ts
13769
- import * as fs38 from "fs";
13770
- import * as path36 from "path";
13724
+ import * as fs39 from "fs";
13725
+ import * as path37 from "path";
13771
13726
  function stripFrontmatter(raw) {
13772
13727
  const match = /^---\n[\s\S]*?\n---\n?([\s\S]*)$/.exec(raw);
13773
13728
  return (match ? match[1] : raw).trim();
@@ -13775,9 +13730,9 @@ function stripFrontmatter(raw) {
13775
13730
  function loadStaffPersona(cwd, slug, staffDir = DEFAULT_STAFF_DIR) {
13776
13731
  const trimmed = slug.trim();
13777
13732
  if (!trimmed) throw new Error("loadStaffPersona: empty staff slug");
13778
- const staffPath = path36.join(cwd, staffDir, `${trimmed}.md`);
13779
- if (fs38.existsSync(staffPath)) {
13780
- const body = stripFrontmatter(fs38.readFileSync(staffPath, "utf-8"));
13733
+ const staffPath = path37.join(cwd, staffDir, `${trimmed}.md`);
13734
+ if (fs39.existsSync(staffPath)) {
13735
+ const body = stripFrontmatter(fs39.readFileSync(staffPath, "utf-8"));
13781
13736
  if (body) return body;
13782
13737
  const builtinForEmpty = BUILTIN_PERSONAS[trimmed];
13783
13738
  if (builtinForEmpty) return builtinForEmpty;
@@ -13890,8 +13845,8 @@ var init_tools = __esm({
13890
13845
 
13891
13846
  // src/executor.ts
13892
13847
  import { spawn as spawn7 } from "child_process";
13893
- import * as fs39 from "fs";
13894
- import * as path37 from "path";
13848
+ import * as fs40 from "fs";
13849
+ import * as path38 from "path";
13895
13850
  function isMutatingPostflight(scriptName) {
13896
13851
  return MUTATING_POSTFLIGHTS.has(scriptName ?? "");
13897
13852
  }
@@ -14004,17 +13959,7 @@ async function runExecutable(profileName, input) {
14004
13959
  reason: `agent.model invalid: ${err instanceof Error ? err.message : String(err)}`
14005
13960
  });
14006
13961
  }
14007
- let litellm = null;
14008
- if (profileName !== "preview-build") {
14009
- try {
14010
- litellm = await startLitellmIfNeeded(model, input.cwd);
14011
- } catch (err) {
14012
- return finishAndEnd({
14013
- exitCode: 99,
14014
- reason: `litellm startup failed: ${err instanceof Error ? err.message : String(err)}`
14015
- });
14016
- }
14017
- }
13962
+ let litellm;
14018
13963
  const ctx = {
14019
13964
  args,
14020
13965
  cwd: input.cwd,
@@ -14042,16 +13987,23 @@ async function runExecutable(profileName, input) {
14042
13987
  })
14043
13988
  };
14044
13989
  })() : null;
14045
- const ndjsonDir = path37.join(input.cwd, ".kody");
13990
+ const ndjsonDir = path38.join(input.cwd, ".kody");
14046
13991
  const personaSlug = typeof profile.staff === "string" && profile.staff.length > 0 ? profile.staff : typeof ctx.data.jobPersona === "string" && ctx.data.jobPersona.length > 0 ? ctx.data.jobPersona : null;
14047
13992
  const staffPersona = personaSlug ? framePersona(personaSlug, loadStaffPersona(input.cwd, personaSlug)) : null;
14048
13993
  const jobWhyBlock = typeof ctx.data.jobWhy === "string" ? operatorRequestBlock(ctx.data.jobWhy) : null;
14049
13994
  const jobRefBlock = jobReferenceBlock(profileName, profile, ctx.data);
14050
13995
  const invokeAgent = async (prompt) => {
14051
- const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path37.isAbsolute(p) ? p : path37.resolve(profile.dir, p)).filter((p) => p.length > 0);
13996
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path38.isAbsolute(p) ? p : path38.resolve(profile.dir, p)).filter((p) => p.length > 0);
14052
13997
  const syntheticPath = ctx.data.syntheticPluginPath;
14053
13998
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
14054
13999
  const agents = loadSubagents(profile);
14000
+ if (litellm === void 0) {
14001
+ try {
14002
+ litellm = await startLitellmIfNeeded(model, input.cwd);
14003
+ } catch (err) {
14004
+ throw new Error(`litellm startup failed: ${err instanceof Error ? err.message : String(err)}`);
14005
+ }
14006
+ }
14055
14007
  const lm = litellm;
14056
14008
  return runAgent({
14057
14009
  prompt,
@@ -14159,7 +14111,14 @@ async function runExecutable(profileName, input) {
14159
14111
  });
14160
14112
  }
14161
14113
  emitEvent(input.cwd, { executable: profileName, kind: "agent_start" });
14162
- agentResult = await invokeAgent(prompt);
14114
+ try {
14115
+ agentResult = await invokeAgent(prompt);
14116
+ } catch (err) {
14117
+ return finishAndEnd({
14118
+ exitCode: 99,
14119
+ reason: err instanceof Error ? err.message : String(err)
14120
+ });
14121
+ }
14163
14122
  emitEvent(input.cwd, {
14164
14123
  executable: profileName,
14165
14124
  kind: "agent_end",
@@ -14307,8 +14266,8 @@ async function runExecutableChain(profileName, input) {
14307
14266
  process.stdout.write(`\u2192 kody: in-process job hand-off \u2192 ${label} (hop ${hops}/${MAX_CHAIN_HOPS})
14308
14267
 
14309
14268
  `);
14310
- const { runJob: runJob2 } = await Promise.resolve().then(() => (init_job(), job_exports));
14311
- const childResult = await runJob2(next2, {
14269
+ const { runJob: runJob3 } = await Promise.resolve().then(() => (init_job(), job_exports));
14270
+ const childResult = await runJob3(next2, {
14312
14271
  cwd: input.cwd,
14313
14272
  config: input.config,
14314
14273
  verbose: input.verbose,
@@ -14320,12 +14279,24 @@ async function runExecutableChain(profileName, input) {
14320
14279
  ...chainData,
14321
14280
  ...childResult.taskState ? { taskState: childResult.taskState } : {}
14322
14281
  };
14323
- process.stdout.write(`\u2192 kody: in-process return \u2192 ${after.executable} (hop ${hops}/${MAX_CHAIN_HOPS})
14282
+ const afterJob = handoffToJob(after);
14283
+ if (!afterJob) {
14284
+ return {
14285
+ exitCode: 99,
14286
+ reason: `in-process return missing duty/action for ${after.executable ?? "unknown"}`
14287
+ };
14288
+ }
14289
+ process.stdout.write(
14290
+ `\u2192 kody: in-process return \u2192 ${afterJob.action ?? afterJob.duty} (hop ${hops}/${MAX_CHAIN_HOPS})
14324
14291
 
14325
- `);
14326
- result = await runExecutable(after.executable, {
14327
- ...input,
14328
- cliArgs: after.cliArgs,
14292
+ `
14293
+ );
14294
+ const { runJob: runJob4 } = await Promise.resolve().then(() => (init_job(), job_exports));
14295
+ result = await runJob4(afterJob, {
14296
+ cwd: input.cwd,
14297
+ config: input.config,
14298
+ verbose: input.verbose,
14299
+ quiet: input.quiet,
14329
14300
  preloadedData: chainData
14330
14301
  });
14331
14302
  chainData = {
@@ -14342,10 +14313,26 @@ async function runExecutableChain(profileName, input) {
14342
14313
  continue;
14343
14314
  }
14344
14315
  const next = result.nextDispatch;
14345
- process.stdout.write(`\u2192 kody: in-process hand-off \u2192 ${next.executable} (hop ${hops}/${MAX_CHAIN_HOPS})
14316
+ const nextJob = handoffToJob(next);
14317
+ if (!nextJob) {
14318
+ return {
14319
+ exitCode: 99,
14320
+ reason: `in-process hand-off missing duty/action for ${next.executable ?? "unknown"}`
14321
+ };
14322
+ }
14323
+ process.stdout.write(
14324
+ `\u2192 kody: in-process hand-off \u2192 ${nextJob.action ?? nextJob.duty} (hop ${hops}/${MAX_CHAIN_HOPS})
14346
14325
 
14347
- `);
14348
- result = await runExecutable(next.executable, { ...input, cliArgs: next.cliArgs, preloadedData: chainData });
14326
+ `
14327
+ );
14328
+ const { runJob: runJob2 } = await Promise.resolve().then(() => (init_job(), job_exports));
14329
+ result = await runJob2(nextJob, {
14330
+ cwd: input.cwd,
14331
+ config: input.config,
14332
+ verbose: input.verbose,
14333
+ quiet: input.quiet,
14334
+ preloadedData: chainData
14335
+ });
14349
14336
  chainData = {
14350
14337
  ...chainData,
14351
14338
  ...result.taskState ? { taskState: result.taskState } : {}
@@ -14358,6 +14345,17 @@ async function runExecutableChain(profileName, input) {
14358
14345
  }
14359
14346
  return result;
14360
14347
  }
14348
+ function handoffToJob(handoff) {
14349
+ const dutyOrAction = handoff.action ?? handoff.duty;
14350
+ if (!dutyOrAction) return null;
14351
+ return {
14352
+ action: handoff.action ?? handoff.duty,
14353
+ duty: handoff.duty,
14354
+ executable: handoff.executable,
14355
+ cliArgs: handoff.cliArgs,
14356
+ flavor: "instant"
14357
+ };
14358
+ }
14361
14359
  function clearStampedLifecycleLabels(profile, ctx) {
14362
14360
  const target = ctx.args.issue ?? ctx.args.pr;
14363
14361
  if (typeof target !== "number" || !Number.isFinite(target)) return;
@@ -14374,17 +14372,17 @@ function clearStampedLifecycleLabels(profile, ctx) {
14374
14372
  function resolveProfilePath(profileName) {
14375
14373
  const found = resolveExecutable(profileName);
14376
14374
  if (found) return found;
14377
- const here = path37.dirname(new URL(import.meta.url).pathname);
14375
+ const here = path38.dirname(new URL(import.meta.url).pathname);
14378
14376
  const candidates = [
14379
- path37.join(here, "executables", profileName, "profile.json"),
14377
+ path38.join(here, "executables", profileName, "profile.json"),
14380
14378
  // same-dir sibling (dev)
14381
- path37.join(here, "..", "executables", profileName, "profile.json"),
14379
+ path38.join(here, "..", "executables", profileName, "profile.json"),
14382
14380
  // up one (prod: dist/bin → dist/executables)
14383
- path37.join(here, "..", "src", "executables", profileName, "profile.json")
14381
+ path38.join(here, "..", "src", "executables", profileName, "profile.json")
14384
14382
  // fallback
14385
14383
  ];
14386
14384
  for (const c of candidates) {
14387
- if (fs39.existsSync(c)) return c;
14385
+ if (fs40.existsSync(c)) return c;
14388
14386
  }
14389
14387
  return candidates[0];
14390
14388
  }
@@ -14482,8 +14480,8 @@ function resolveShellTimeoutMs(entry) {
14482
14480
  }
14483
14481
  async function runShellEntry(entry, ctx, profile) {
14484
14482
  const shellName = entry.shell;
14485
- const shellPath = path37.join(profile.dir, shellName);
14486
- if (!fs39.existsSync(shellPath)) {
14483
+ const shellPath = path38.join(profile.dir, shellName);
14484
+ if (!fs40.existsSync(shellPath)) {
14487
14485
  ctx.skipAgent = true;
14488
14486
  ctx.output.exitCode = 99;
14489
14487
  ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
@@ -14630,6 +14628,156 @@ var init_executor = __esm({
14630
14628
  }
14631
14629
  });
14632
14630
 
14631
+ // src/job.ts
14632
+ var job_exports = {};
14633
+ __export(job_exports, {
14634
+ DEFAULT_INSTANT_PERSONA: () => DEFAULT_INSTANT_PERSONA,
14635
+ InvalidJobError: () => InvalidJobError,
14636
+ mintInstantJob: () => mintInstantJob,
14637
+ mintScheduledJob: () => mintScheduledJob,
14638
+ newJobId: () => newJobId,
14639
+ runJob: () => runJob,
14640
+ stableJobKey: () => stableJobKey,
14641
+ validateJob: () => validateJob
14642
+ });
14643
+ import * as path39 from "path";
14644
+ function newJobId(flavor) {
14645
+ localJobSeq += 1;
14646
+ const runId = process.env.GITHUB_RUN_ID;
14647
+ if (runId) return `gh-${runId}-${process.env.GITHUB_RUN_ATTEMPT ?? "1"}-${localJobSeq}`;
14648
+ return `${flavor}-${Date.now()}-${localJobSeq}`;
14649
+ }
14650
+ function validateJob(input) {
14651
+ if (!input || typeof input !== "object") {
14652
+ throw new InvalidJobError("job must be an object");
14653
+ }
14654
+ const j = input;
14655
+ if (typeof j.duty !== "string" && typeof j.action !== "string") {
14656
+ throw new InvalidJobError("job must reference a duty action or duty");
14657
+ }
14658
+ if (j.flavor !== "instant" && j.flavor !== "scheduled") {
14659
+ throw new InvalidJobError(`job.flavor must be "instant" or "scheduled" (got ${String(j.flavor)})`);
14660
+ }
14661
+ if (j.cliArgs !== void 0 && (typeof j.cliArgs !== "object" || j.cliArgs === null)) {
14662
+ throw new InvalidJobError("job.cliArgs must be an object when present");
14663
+ }
14664
+ return {
14665
+ action: typeof j.action === "string" ? j.action : void 0,
14666
+ executable: typeof j.executable === "string" ? j.executable : void 0,
14667
+ duty: typeof j.duty === "string" ? j.duty : void 0,
14668
+ why: typeof j.why === "string" ? j.why : void 0,
14669
+ persona: typeof j.persona === "string" ? j.persona : void 0,
14670
+ schedule: typeof j.schedule === "string" ? j.schedule : void 0,
14671
+ target: typeof j.target === "number" ? j.target : void 0,
14672
+ cliArgs: j.cliArgs ?? {},
14673
+ flavor: j.flavor,
14674
+ force: j.force === true
14675
+ };
14676
+ }
14677
+ async function runJob(job, base) {
14678
+ const valid = validateJob(job);
14679
+ const action = valid.action ?? valid.duty;
14680
+ const projectDutiesRoot = path39.join(base.cwd, ".kody", "duties");
14681
+ const resolvedDuty = action ? resolveDutyAction(action, projectDutiesRoot) : null;
14682
+ const dutyIdentity = valid.duty ?? resolvedDuty?.duty;
14683
+ const dutyContext = loadDutyContext(dutyIdentity, base.cwd);
14684
+ if (!resolvedDuty && !dutyContext) {
14685
+ throw new InvalidJobError(`job duty not found: ${action ?? valid.duty ?? "<none>"}`);
14686
+ }
14687
+ const dutySelectedExecutable = resolvedDuty?.executable ?? dutyContext?.config.executable ?? dutyContext?.config.executables?.[0] ?? (dutyContext?.config.tickScript ? "duty-tick-scripted" : void 0);
14688
+ const profileName = valid.executable ?? dutySelectedExecutable;
14689
+ if (!profileName) {
14690
+ throw new InvalidJobError(`job duty resolves to no executable: ${dutyIdentity ?? action}`);
14691
+ }
14692
+ const preloadedData = { ...base.preloadedData ?? {} };
14693
+ preloadedData.jobId = newJobId(valid.flavor);
14694
+ preloadedData.jobKey = stableJobKey(valid);
14695
+ preloadedData.jobFlavor = valid.flavor;
14696
+ if (valid.target !== void 0) preloadedData.jobTarget = valid.target;
14697
+ if (valid.action !== void 0 && valid.action.length > 0) preloadedData.jobAction = valid.action;
14698
+ if (dutyIdentity !== void 0 && dutyIdentity.length > 0) preloadedData.jobDuty = dutyIdentity;
14699
+ const executableIdentity = profileName;
14700
+ if (executableIdentity !== void 0 && executableIdentity.length > 0)
14701
+ preloadedData.jobExecutable = executableIdentity;
14702
+ if (valid.schedule !== void 0 && valid.schedule.length > 0) preloadedData.jobSchedule = valid.schedule;
14703
+ if (dutyContext) {
14704
+ preloadedData.dutySlug = dutyContext.slug;
14705
+ preloadedData.dutyTitle = dutyContext.title;
14706
+ preloadedData.dutyIntent = dutyContext.body;
14707
+ preloadedData.jobIntent = dutyContext.body;
14708
+ if (preloadedData.jobDuty === void 0) preloadedData.jobDuty = dutyContext.slug;
14709
+ if (dutyContext.config.staff && preloadedData.jobPersona === void 0) {
14710
+ preloadedData.jobPersona = dutyContext.config.staff;
14711
+ }
14712
+ if (dutyContext.config.every && preloadedData.jobSchedule === void 0) {
14713
+ preloadedData.jobSchedule = dutyContext.config.every;
14714
+ }
14715
+ if (dutyContext.config.mentions && dutyContext.config.mentions.length > 0) {
14716
+ preloadedData.mentions = dutyContext.config.mentions.map((login) => `@${login}`).join(" ");
14717
+ }
14718
+ }
14719
+ if (valid.why !== void 0 && valid.why.length > 0) preloadedData.jobWhy = valid.why;
14720
+ if (valid.persona !== void 0) preloadedData.jobPersona = valid.persona;
14721
+ const input = {
14722
+ cliArgs: { ...valid.cliArgs },
14723
+ cwd: base.cwd,
14724
+ config: base.config,
14725
+ skipConfig: base.skipConfig,
14726
+ verbose: base.verbose,
14727
+ quiet: base.quiet,
14728
+ preloadedData: Object.keys(preloadedData).length > 0 ? preloadedData : void 0
14729
+ };
14730
+ input.cliArgs = resolvedDuty && profileName === resolvedDuty.executable ? { ...resolvedDuty.cliArgs, ...input.cliArgs } : input.cliArgs;
14731
+ const run = base.chain === false ? runExecutable : runExecutableChain;
14732
+ return run(profileName, input);
14733
+ }
14734
+ function loadDutyContext(slug, cwd) {
14735
+ if (!slug) return null;
14736
+ return readDutyFolder(path39.join(cwd, ".kody", "duties"), slug) ?? readDutyFolder(getProjectDutiesRoot(), slug) ?? readDutyFolder(getBuiltinDutiesRoot(), slug);
14737
+ }
14738
+ function mintInstantJob(dispatch2, opts) {
14739
+ return {
14740
+ action: dispatch2.action,
14741
+ executable: dispatch2.executable,
14742
+ duty: dispatch2.duty,
14743
+ why: opts?.why ?? dispatch2.why,
14744
+ persona: opts?.persona ?? DEFAULT_INSTANT_PERSONA,
14745
+ target: dispatch2.target,
14746
+ cliArgs: dispatch2.cliArgs,
14747
+ flavor: "instant"
14748
+ };
14749
+ }
14750
+ function mintScheduledJob(input) {
14751
+ return {
14752
+ action: input.action,
14753
+ duty: input.duty,
14754
+ executable: input.executable,
14755
+ schedule: input.schedule,
14756
+ persona: input.persona,
14757
+ cliArgs: input.cliArgs ?? {},
14758
+ flavor: "scheduled"
14759
+ };
14760
+ }
14761
+ var DEFAULT_INSTANT_PERSONA, localJobSeq, InvalidJobError;
14762
+ var init_job = __esm({
14763
+ "src/job.ts"() {
14764
+ "use strict";
14765
+ init_dutyFolders();
14766
+ init_executor();
14767
+ init_registry();
14768
+ init_jobIdentity();
14769
+ init_jobIdentity();
14770
+ DEFAULT_INSTANT_PERSONA = "kody";
14771
+ localJobSeq = 0;
14772
+ InvalidJobError = class extends Error {
14773
+ constructor(message) {
14774
+ super(message);
14775
+ this.name = "InvalidJobError";
14776
+ }
14777
+ };
14778
+ }
14779
+ });
14780
+
14633
14781
  // src/entry.ts
14634
14782
  init_package();
14635
14783
 
@@ -14737,9 +14885,9 @@ function translateOpenAISseToBrain(opts) {
14737
14885
  }
14738
14886
 
14739
14887
  // src/servers/brain-serve.ts
14740
- import * as fs42 from "fs";
14888
+ import * as fs43 from "fs";
14741
14889
  import { createServer } from "http";
14742
- import * as path40 from "path";
14890
+ import * as path42 from "path";
14743
14891
 
14744
14892
  // src/chat/loop.ts
14745
14893
  init_agent();
@@ -15108,6 +15256,7 @@ async function runChatTurn(opts) {
15108
15256
  verbose: opts.verbose,
15109
15257
  quiet: opts.quiet,
15110
15258
  systemPromptAppend: systemPrompt,
15259
+ ...opts.reasoningEffort ? { reasoningEffort: opts.reasoningEffort } : {},
15111
15260
  // Let the agent clone + work on OTHER repos mid-conversation (a
15112
15261
  // repo-less Brain serves many). Enabled whenever we know where repos
15113
15262
  // live; grants read access to that root via additionalDirectories.
@@ -15280,8 +15429,8 @@ init_config();
15280
15429
 
15281
15430
  // src/kody-cli.ts
15282
15431
  import { execFileSync as execFileSync26 } from "child_process";
15283
- import * as fs40 from "fs";
15284
- import * as path38 from "path";
15432
+ import * as fs41 from "fs";
15433
+ import * as path40 from "path";
15285
15434
 
15286
15435
  // src/app-auth.ts
15287
15436
  import { createSign } from "crypto";
@@ -15435,28 +15584,12 @@ function resolveOperatorAction(action) {
15435
15584
  return resolveDutyAction(action);
15436
15585
  }
15437
15586
  function resolveConfiguredAction(action) {
15438
- const resolved = resolveDutyAction(action);
15439
- if (resolved) return resolved;
15440
- return compatibilityDutyAction(action);
15587
+ return resolveDutyAction(action);
15441
15588
  }
15442
15589
  function requiredRoute(action) {
15443
- return resolveConfiguredAction(action) ?? {
15444
- action,
15445
- duty: action,
15446
- executable: action,
15447
- cliArgs: {},
15448
- source: "builtin"
15449
- };
15450
- }
15451
- function compatibilityDutyAction(action) {
15452
- if (!/^[a-z][a-z0-9-]*$/.test(action)) return null;
15453
- return {
15454
- action,
15455
- duty: action,
15456
- executable: action,
15457
- cliArgs: {},
15458
- source: "builtin"
15459
- };
15590
+ const route = resolveConfiguredAction(action);
15591
+ if (!route) throw new Error(`required duty action not found: ${action}`);
15592
+ return route;
15460
15593
  }
15461
15594
  function routeResult(route, cliArgs, target, why) {
15462
15595
  const result = {
@@ -15487,7 +15620,7 @@ function autoDispatch(opts) {
15487
15620
  const inputs2 = objectValue(event.inputs);
15488
15621
  const n = parseInt(String(inputs2?.issue_number ?? ""), 10);
15489
15622
  if (!Number.isNaN(n) && n > 0) {
15490
- const actionName = String(inputs2?.executable ?? "").trim() || "run";
15623
+ const actionName = String(inputs2?.duty ?? inputs2?.executable ?? "").trim() || "run";
15491
15624
  const route2 = resolveConfiguredAction(actionName);
15492
15625
  if (!route2) return null;
15493
15626
  const base = String(inputs2?.base ?? "").trim();
@@ -15628,7 +15761,7 @@ function autoDispatchTyped(opts) {
15628
15761
  if (!tokenRaw || POLITE_WORDS.has(tokenRaw)) {
15629
15762
  return {
15630
15763
  kind: "silent",
15631
- reason: tokenRaw ? `polite-word lead-in '${tokenRaw}', no default executable configured` : "no subcommand token, no default executable configured"
15764
+ reason: tokenRaw ? `polite-word lead-in '${tokenRaw}', no default duty action configured` : "no subcommand token, no default duty action configured"
15632
15765
  };
15633
15766
  }
15634
15767
  const available = listDutyActions().map((e) => e.action).filter((n) => !n.startsWith("goal-") && !n.startsWith("job-")).sort();
@@ -15668,7 +15801,15 @@ function dispatchScheduledWatches(opts) {
15668
15801
  continue;
15669
15802
  }
15670
15803
  }
15671
- out.push({ action: exe.name, duty: exe.name, executable: exe.name, cliArgs: {}, target: 0 });
15804
+ const route = resolveConfiguredAction(exe.name);
15805
+ if (!route) {
15806
+ process.stderr.write(
15807
+ `[kody] dispatchScheduledWatches: '${exe.name}' is scheduled but has no duty action; skipping
15808
+ `
15809
+ );
15810
+ continue;
15811
+ }
15812
+ out.push({ ...route, cliArgs: route.cliArgs, target: 0 });
15672
15813
  }
15673
15814
  return out;
15674
15815
  }
@@ -15770,7 +15911,6 @@ function coerceBare(spec, value) {
15770
15911
  }
15771
15912
 
15772
15913
  // src/kody-cli.ts
15773
- init_executor();
15774
15914
  init_gha();
15775
15915
  init_issue();
15776
15916
  init_job();
@@ -15885,9 +16025,9 @@ async function resolveAuthToken(env = process.env) {
15885
16025
  return void 0;
15886
16026
  }
15887
16027
  function detectPackageManager2(cwd) {
15888
- if (fs40.existsSync(path38.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
15889
- if (fs40.existsSync(path38.join(cwd, "yarn.lock"))) return "yarn";
15890
- if (fs40.existsSync(path38.join(cwd, "bun.lockb"))) return "bun";
16028
+ if (fs41.existsSync(path40.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
16029
+ if (fs41.existsSync(path40.join(cwd, "yarn.lock"))) return "yarn";
16030
+ if (fs41.existsSync(path40.join(cwd, "bun.lockb"))) return "bun";
15891
16031
  return "npm";
15892
16032
  }
15893
16033
  function shellOut(cmd, args, cwd, stream = true) {
@@ -15974,11 +16114,11 @@ function configureGitIdentity(cwd) {
15974
16114
  }
15975
16115
  function postFailureTail(issueNumber, cwd, reason) {
15976
16116
  if (!issueNumber) return;
15977
- const logPath = path38.join(cwd, ".kody", "last-run.jsonl");
16117
+ const logPath = path40.join(cwd, ".kody", "last-run.jsonl");
15978
16118
  let tail = "";
15979
16119
  try {
15980
- if (fs40.existsSync(logPath)) {
15981
- const content = fs40.readFileSync(logPath, "utf-8");
16120
+ if (fs41.existsSync(logPath)) {
16121
+ const content = fs41.readFileSync(logPath, "utf-8");
15982
16122
  tail = content.slice(-3e3);
15983
16123
  }
15984
16124
  } catch {
@@ -16003,7 +16143,7 @@ async function runCi(argv) {
16003
16143
  return 0;
16004
16144
  }
16005
16145
  const args = parseCiArgs(argv);
16006
- const cwd = args.cwd ? path38.resolve(args.cwd) : process.cwd();
16146
+ const cwd = args.cwd ? path40.resolve(args.cwd) : process.cwd();
16007
16147
  let earlyConfig;
16008
16148
  let earlyConfigError;
16009
16149
  try {
@@ -16016,14 +16156,14 @@ async function runCi(argv) {
16016
16156
  const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
16017
16157
  let manualWorkflowDispatch = false;
16018
16158
  let forceRunAction = null;
16019
- if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs40.existsSync(dispatchEventPath)) {
16159
+ if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs41.existsSync(dispatchEventPath)) {
16020
16160
  try {
16021
- const evt = JSON.parse(fs40.readFileSync(dispatchEventPath, "utf-8"));
16161
+ const evt = JSON.parse(fs41.readFileSync(dispatchEventPath, "utf-8"));
16022
16162
  const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
16023
16163
  const sessionInput = String(evt?.inputs?.sessionId ?? "");
16024
- const exeInput = String(evt?.inputs?.executable ?? "").trim();
16164
+ const dutyInput = String(evt?.inputs?.duty ?? evt?.inputs?.executable ?? "").trim();
16025
16165
  const noTarget = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
16026
- if (noTarget && exeInput) forceRunAction = exeInput;
16166
+ if (noTarget && dutyInput) forceRunAction = dutyInput;
16027
16167
  else manualWorkflowDispatch = noTarget;
16028
16168
  } catch {
16029
16169
  manualWorkflowDispatch = false;
@@ -16123,7 +16263,7 @@ async function runCi(argv) {
16123
16263
  );
16124
16264
  return 0;
16125
16265
  }
16126
- if (outcome.kind === "silent" && earlyConfigError && outcome.reason.includes("no default executable configured")) {
16266
+ if (outcome.kind === "silent" && earlyConfigError && outcome.reason.includes("no default duty action configured")) {
16127
16267
  process.stderr.write(`[kody] config error: ${earlyConfigError.message}
16128
16268
  `);
16129
16269
  return 64;
@@ -16143,11 +16283,14 @@ async function runCi(argv) {
16143
16283
  ${CI_HELP}`);
16144
16284
  return 64;
16145
16285
  }
16286
+ const runRoute = args.issueNumber ? resolveDutyAction("run") : null;
16287
+ if (!autoFallback && args.issueNumber && !runRoute) {
16288
+ process.stderr.write("[kody] required duty action 'run' not found\n");
16289
+ return 64;
16290
+ }
16146
16291
  const dispatch2 = autoFallback ?? {
16147
- action: "run",
16148
- duty: "run",
16149
- executable: "run",
16150
- cliArgs: { issue: args.issueNumber },
16292
+ ...runRoute,
16293
+ cliArgs: { ...runRoute.cliArgs, issue: args.issueNumber },
16151
16294
  target: args.issueNumber
16152
16295
  };
16153
16296
  const issueNumber = dispatch2.target;
@@ -16229,8 +16372,8 @@ async function runScheduledFanOut(cwd, args, opts) {
16229
16372
  );
16230
16373
  return 0;
16231
16374
  }
16232
- const names = matches.map((m) => m.executable).join(", ");
16233
- process.stdout.write(`\u2192 kody: scheduled wake \u2014 firing ${matches.length} watch(es): ${names}
16375
+ const names = matches.map((m) => `${m.duty}\u2192${m.executable}`).join(", ");
16376
+ process.stdout.write(`\u2192 kody: scheduled wake \u2014 firing ${matches.length} watch dut(y/ies): ${names}
16234
16377
  `);
16235
16378
  try {
16236
16379
  const n = unpackAllSecrets();
@@ -16267,19 +16410,21 @@ async function runScheduledFanOut(cwd, args, opts) {
16267
16410
  const serial = process.env.KODY_SERIAL_WATCHES === "1";
16268
16411
  const runWatch = async (match) => {
16269
16412
  process.stdout.write(`
16270
- \u2192 kody: running watch \`${match.executable}\`
16413
+ \u2192 kody: running watch duty \`${match.duty}\` (${match.executable})
16271
16414
  `);
16272
16415
  try {
16273
- const result = await runExecutable(match.executable, {
16274
- cliArgs: match.cliArgs,
16275
- cwd,
16276
- config,
16277
- verbose: args.verbose,
16278
- quiet: args.quiet
16279
- });
16416
+ const result = await runJob(
16417
+ mintScheduledJob({
16418
+ action: match.action,
16419
+ duty: match.duty,
16420
+ executable: match.executable,
16421
+ cliArgs: match.cliArgs
16422
+ }),
16423
+ { cwd, config, verbose: args.verbose, quiet: args.quiet, chain: false }
16424
+ );
16280
16425
  if (result.exitCode !== 0) {
16281
16426
  process.stderr.write(
16282
- `[kody] watch \`${match.executable}\` exited ${result.exitCode}: ${result.reason ?? "(no reason)"}
16427
+ `[kody] watch duty \`${match.duty}\` exited ${result.exitCode}: ${result.reason ?? "(no reason)"}
16283
16428
  `
16284
16429
  );
16285
16430
  return result.exitCode;
@@ -16287,7 +16432,7 @@ async function runScheduledFanOut(cwd, args, opts) {
16287
16432
  return 0;
16288
16433
  } catch (err) {
16289
16434
  const msg = err instanceof Error ? err.message : String(err);
16290
- process.stderr.write(`[kody] watch \`${match.executable}\` crashed: ${msg}
16435
+ process.stderr.write(`[kody] watch duty \`${match.duty}\` crashed: ${msg}
16291
16436
  `);
16292
16437
  return 99;
16293
16438
  }
@@ -16313,16 +16458,16 @@ init_litellm();
16313
16458
  init_repoWorkspace();
16314
16459
 
16315
16460
  // src/scripts/brainTurnLog.ts
16316
- import * as fs41 from "fs";
16317
- import * as path39 from "path";
16461
+ import * as fs42 from "fs";
16462
+ import * as path41 from "path";
16318
16463
  var live = /* @__PURE__ */ new Map();
16319
16464
  function eventsPath(dir, chatId) {
16320
- return path39.join(dir, ".kody", "brain-events", `${chatId}.jsonl`);
16465
+ return path41.join(dir, ".kody", "brain-events", `${chatId}.jsonl`);
16321
16466
  }
16322
16467
  function lastPersistedSeq(dir, chatId) {
16323
16468
  const p = eventsPath(dir, chatId);
16324
- if (!fs41.existsSync(p)) return 0;
16325
- const lines = fs41.readFileSync(p, "utf-8").split("\n").filter(Boolean);
16469
+ if (!fs42.existsSync(p)) return 0;
16470
+ const lines = fs42.readFileSync(p, "utf-8").split("\n").filter(Boolean);
16326
16471
  if (lines.length === 0) return 0;
16327
16472
  try {
16328
16473
  return JSON.parse(lines[lines.length - 1]).seq || 0;
@@ -16332,9 +16477,9 @@ function lastPersistedSeq(dir, chatId) {
16332
16477
  }
16333
16478
  function readSince(dir, chatId, since) {
16334
16479
  const p = eventsPath(dir, chatId);
16335
- if (!fs41.existsSync(p)) return [];
16480
+ if (!fs42.existsSync(p)) return [];
16336
16481
  const out = [];
16337
- for (const line of fs41.readFileSync(p, "utf-8").split("\n")) {
16482
+ for (const line of fs42.readFileSync(p, "utf-8").split("\n")) {
16338
16483
  if (!line) continue;
16339
16484
  try {
16340
16485
  const rec = JSON.parse(line);
@@ -16360,12 +16505,12 @@ function beginTurn(dir, chatId) {
16360
16505
  };
16361
16506
  live.set(chatId, state);
16362
16507
  const p = eventsPath(dir, chatId);
16363
- fs41.mkdirSync(path39.dirname(p), { recursive: true });
16508
+ fs42.mkdirSync(path41.dirname(p), { recursive: true });
16364
16509
  return (event) => {
16365
16510
  state.seq += 1;
16366
16511
  const rec = { seq: state.seq, turn, ts: Date.now(), event };
16367
16512
  try {
16368
- fs41.appendFileSync(p, `${JSON.stringify(rec)}
16513
+ fs42.appendFileSync(p, `${JSON.stringify(rec)}
16369
16514
  `);
16370
16515
  } catch (err) {
16371
16516
  process.stderr.write(
@@ -16404,7 +16549,7 @@ function endTurnIfUnterminated(dir, chatId, errMessage) {
16404
16549
  event: { type: "error", error: errMessage || "turn ended unexpectedly", chatId }
16405
16550
  };
16406
16551
  try {
16407
- fs41.appendFileSync(eventsPath(dir, chatId), `${JSON.stringify(rec)}
16552
+ fs42.appendFileSync(eventsPath(dir, chatId), `${JSON.stringify(rec)}
16408
16553
  `);
16409
16554
  } catch {
16410
16555
  }
@@ -16635,7 +16780,7 @@ async function handleChatTurn(req, res, chatId, opts) {
16635
16780
  const repo = strField(body, "repo");
16636
16781
  const repoToken = strField(body, "repoToken");
16637
16782
  const sessionFile = sessionFilePath(opts.cwd, chatId);
16638
- fs42.mkdirSync(path40.dirname(sessionFile), { recursive: true });
16783
+ fs43.mkdirSync(path42.dirname(sessionFile), { recursive: true });
16639
16784
  appendTurn(sessionFile, {
16640
16785
  role: "user",
16641
16786
  content: message,
@@ -16682,7 +16827,7 @@ async function handleChatTurn(req, res, chatId, opts) {
16682
16827
  function buildServer(opts) {
16683
16828
  const runTurn = opts.runTurn ?? runChatTurn;
16684
16829
  const cloneRepo = opts.cloneRepo ?? defaultCloneRepo;
16685
- const reposRoot = opts.reposRoot ?? path40.join(path40.dirname(path40.resolve(opts.cwd)), "repos");
16830
+ const reposRoot = opts.reposRoot ?? path42.join(path42.dirname(path42.resolve(opts.cwd)), "repos");
16686
16831
  return createServer(async (req, res) => {
16687
16832
  if (!req.method || !req.url) {
16688
16833
  sendJson(res, 400, { error: "bad request" });
@@ -17275,14 +17420,14 @@ async function loadConfigSafe() {
17275
17420
 
17276
17421
  // src/chat-cli.ts
17277
17422
  import { execFileSync as execFileSync29 } from "child_process";
17278
- import * as fs44 from "fs";
17279
- import * as path42 from "path";
17423
+ import * as fs45 from "fs";
17424
+ import * as path44 from "path";
17280
17425
 
17281
17426
  // src/chat/modes/interactive.ts
17282
17427
  init_issue();
17283
17428
  import { execFileSync as execFileSync28 } from "child_process";
17284
- import * as fs43 from "fs";
17285
- import * as path41 from "path";
17429
+ import * as fs44 from "fs";
17430
+ import * as path43 from "path";
17286
17431
 
17287
17432
  // src/chat/inbox.ts
17288
17433
  import { execFileSync as execFileSync27 } from "child_process";
@@ -17415,7 +17560,8 @@ async function runInteractiveMode(opts) {
17415
17560
  sink: opts.sink,
17416
17561
  verbose: opts.verbose,
17417
17562
  quiet: opts.quiet,
17418
- invokeAgent: opts.invokeAgent
17563
+ invokeAgent: opts.invokeAgent,
17564
+ ...opts.reasoningEffort ? { reasoningEffort: opts.reasoningEffort } : {}
17419
17565
  });
17420
17566
  } catch (err) {
17421
17567
  const msg = err instanceof Error ? err.message : String(err);
@@ -17441,9 +17587,9 @@ function findNextUserTurn(turns, fromIdx) {
17441
17587
  return -1;
17442
17588
  }
17443
17589
  function commitTurn(cwd, sessionId, _verbose) {
17444
- const sessionRel = path41.relative(cwd, sessionFilePath(cwd, sessionId));
17445
- const eventsRel = path41.relative(cwd, eventsFilePath(cwd, sessionId));
17446
- const rels = [sessionRel, eventsRel].filter((p) => fs43.existsSync(path41.join(cwd, p)));
17590
+ const sessionRel = path43.relative(cwd, sessionFilePath(cwd, sessionId));
17591
+ const eventsRel = path43.relative(cwd, eventsFilePath(cwd, sessionId));
17592
+ const rels = [sessionRel, eventsRel].filter((p) => fs44.existsSync(path43.join(cwd, p)));
17447
17593
  if (rels.length === 0) return;
17448
17594
  const repository = process.env.GITHUB_REPOSITORY;
17449
17595
  if (!repository) {
@@ -17455,8 +17601,8 @@ function commitTurn(cwd, sessionId, _verbose) {
17455
17601
  }
17456
17602
  const branch = defaultBranch(cwd) ?? "main";
17457
17603
  for (const rel of rels) {
17458
- const repoPath = rel.split(path41.sep).join("/");
17459
- const localText = fs43.readFileSync(path41.join(cwd, rel), "utf-8");
17604
+ const repoPath = rel.split(path43.sep).join("/");
17605
+ const localText = fs44.readFileSync(path43.join(cwd, rel), "utf-8");
17460
17606
  putJsonlViaContents(repository, branch, repoPath, localText, sessionId, cwd);
17461
17607
  }
17462
17608
  }
@@ -17554,11 +17700,17 @@ var CHAT_HELP = `kody chat \u2014 dashboard-driven chat session
17554
17700
 
17555
17701
  Usage:
17556
17702
  kody chat [--session <id>] [--message <text>] [--model <provider/model>]
17703
+ [--reasoning-effort <off|low|medium|high>]
17557
17704
  [--dashboard-url <url>] [--cwd <path>] [--verbose|--quiet]
17558
17705
 
17559
- All inputs may also come from env: SESSION_ID, INIT_MESSAGE, MODEL, DASHBOARD_URL.
17706
+ All inputs may also come from env: SESSION_ID, INIT_MESSAGE, MODEL, REASONING_EFFORT, DASHBOARD_URL.
17560
17707
  CLI flags take precedence over env. SESSION_ID is required.
17561
17708
 
17709
+ Thinking level maps to the Claude Agent SDK's maxThinkingTokens (Anthropic
17710
+ extended thinking). Default is unset (no thinking \u2014 cheapest). Set via the
17711
+ dashboard's chat-level thinking dropdown, the REASONING_EFFORT env var,
17712
+ or this flag.
17713
+
17562
17714
  Exit codes:
17563
17715
  0 reply emitted successfully
17564
17716
  64 bad inputs (missing session, empty history)
@@ -17571,6 +17723,7 @@ function parseChatArgs(argv, env = process.env) {
17571
17723
  if (arg === "--session") result.sessionId = argv[++i];
17572
17724
  else if (arg === "--message") result.initMessage = argv[++i];
17573
17725
  else if (arg === "--model") result.model = argv[++i];
17726
+ else if (arg === "--reasoning-effort") result.reasoningEffort = parseReasoningEffort(argv[++i]) ?? void 0;
17574
17727
  else if (arg === "--dashboard-url") result.dashboardUrl = argv[++i];
17575
17728
  else if (arg === "--cwd") result.cwd = argv[++i];
17576
17729
  else if (arg === "--verbose") result.verbose = true;
@@ -17583,22 +17736,23 @@ function parseChatArgs(argv, env = process.env) {
17583
17736
  result.initMessage = result.initMessage ?? env.INIT_MESSAGE ?? void 0;
17584
17737
  result.model = result.model ?? env.MODEL ?? void 0;
17585
17738
  result.dashboardUrl = result.dashboardUrl ?? env.DASHBOARD_URL ?? void 0;
17739
+ result.reasoningEffort = result.reasoningEffort ?? parseReasoningEffort(env.REASONING_EFFORT) ?? void 0;
17586
17740
  for (const key of ["sessionId", "initMessage", "model", "dashboardUrl"]) {
17587
17741
  const v = result[key];
17588
17742
  if (typeof v === "string" && v.trim() === "") result[key] = void 0;
17589
17743
  }
17590
17744
  if (!result.sessionId && !result.errors.includes("__HELP__")) {
17591
- result.errors.push("--session <id> (or SESSION_ID env) is required");
17745
+ result.errors.push("--session <id> (or SESSION_ID env) is required)");
17592
17746
  }
17593
17747
  return result;
17594
17748
  }
17595
17749
  function commitChatFiles(cwd, sessionId, verbose) {
17596
- const sessionFile = path42.relative(cwd, sessionFilePath(cwd, sessionId));
17597
- const eventsFile = path42.relative(cwd, eventsFilePath(cwd, sessionId));
17750
+ const sessionFile = path44.relative(cwd, sessionFilePath(cwd, sessionId));
17751
+ const eventsFile = path44.relative(cwd, eventsFilePath(cwd, sessionId));
17598
17752
  const safeSession = sessionId.replace(/[^a-zA-Z0-9._-]/g, "_");
17599
- const tasksDir = path42.join(".kody", "tasks", safeSession);
17753
+ const tasksDir = path44.join(".kody", "tasks", safeSession);
17600
17754
  const candidatePaths = [sessionFile, eventsFile, tasksDir];
17601
- const paths = candidatePaths.filter((p) => fs44.existsSync(path42.join(cwd, p)));
17755
+ const paths = candidatePaths.filter((p) => fs45.existsSync(path44.join(cwd, p)));
17602
17756
  if (paths.length === 0) return;
17603
17757
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
17604
17758
  try {
@@ -17642,7 +17796,7 @@ async function runChat(argv) {
17642
17796
  ${CHAT_HELP}`);
17643
17797
  return 64;
17644
17798
  }
17645
- const cwd = args.cwd ? path42.resolve(args.cwd) : process.cwd();
17799
+ const cwd = args.cwd ? path44.resolve(args.cwd) : process.cwd();
17646
17800
  const sessionId = args.sessionId;
17647
17801
  const unpackedSecrets = unpackAllSecrets();
17648
17802
  if (unpackedSecrets > 0) {
@@ -17653,6 +17807,7 @@ ${CHAT_HELP}`);
17653
17807
  configureGitIdentity(cwd);
17654
17808
  const config = tryLoadConfig(cwd);
17655
17809
  const modelSpec = args.model ?? config?.agent.model ?? DEFAULT_MODEL2;
17810
+ const reasoningEffort = args.reasoningEffort ?? config?.agent.reasoningEffort ?? void 0;
17656
17811
  let model;
17657
17812
  try {
17658
17813
  model = parseProviderModel(modelSpec);
@@ -17694,7 +17849,7 @@ ${CHAT_HELP}`);
17694
17849
  const sink = buildSink(cwd, sessionId, args.dashboardUrl);
17695
17850
  const meta = readMeta(sessionFile);
17696
17851
  process.stdout.write(
17697
- `\u2192 kody:chat: session file=${sessionFile} exists=${fs44.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
17852
+ `\u2192 kody:chat: session file=${sessionFile} exists=${fs45.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
17698
17853
  `
17699
17854
  );
17700
17855
  try {
@@ -17707,7 +17862,8 @@ ${CHAT_HELP}`);
17707
17862
  sink,
17708
17863
  meta,
17709
17864
  verbose: args.verbose,
17710
- quiet: args.quiet
17865
+ quiet: args.quiet,
17866
+ ...reasoningEffort ? { reasoningEffort } : {}
17711
17867
  });
17712
17868
  return result2.exitCode;
17713
17869
  }
@@ -17719,7 +17875,8 @@ ${CHAT_HELP}`);
17719
17875
  litellmUrl: litellm?.url ?? null,
17720
17876
  sink,
17721
17877
  verbose: args.verbose,
17722
- quiet: args.quiet
17878
+ quiet: args.quiet,
17879
+ ...reasoningEffort ? { reasoningEffort } : {}
17723
17880
  });
17724
17881
  commitChatFiles(cwd, sessionId, args.verbose ?? false);
17725
17882
  return result.exitCode;
@@ -17733,7 +17890,6 @@ ${CHAT_HELP}`);
17733
17890
 
17734
17891
  // src/entry.ts
17735
17892
  init_config();
17736
- init_executor();
17737
17893
  init_job();
17738
17894
  init_registry();
17739
17895
 
@@ -17851,8 +18007,8 @@ var FlyClient = class {
17851
18007
  get fetch() {
17852
18008
  return this.opts.fetchImpl ?? fetch;
17853
18009
  }
17854
- async call(path43, init = {}) {
17855
- const res = await this.fetch(`${FLY_API_BASE}${path43}`, {
18010
+ async call(path45, init = {}) {
18011
+ const res = await this.fetch(`${FLY_API_BASE}${path45}`, {
17856
18012
  method: init.method ?? "GET",
17857
18013
  headers: {
17858
18014
  Authorization: `Bearer ${this.opts.token}`,
@@ -17863,7 +18019,7 @@ var FlyClient = class {
17863
18019
  if (res.status === 404 && init.allow404) return null;
17864
18020
  if (!res.ok) {
17865
18021
  const text = await res.text().catch(() => "");
17866
- throw new Error(`Fly API ${res.status} on ${path43}: ${text.slice(0, 200) || res.statusText}`);
18022
+ throw new Error(`Fly API ${res.status} on ${path45}: ${text.slice(0, 200) || res.statusText}`);
17867
18023
  }
17868
18024
  if (res.status === 204) return null;
17869
18025
  const raw = await res.text();
@@ -18089,7 +18245,9 @@ var PoolManager = class {
18089
18245
  const destroyedSurplus = await this.destroySurplus(surplus);
18090
18246
  const destroyed = destroyedTracked + destroyedSurplus;
18091
18247
  if (pruned > 0 || adopted > 0 || destroyed > 0) {
18092
- this.log(`resync: pruned ${pruned} stale, adopted ${adopted}, destroyed ${destroyed} surplus (free=${this.free.length})`);
18248
+ this.log(
18249
+ `resync: pruned ${pruned} stale, adopted ${adopted}, destroyed ${destroyed} surplus (free=${this.free.length})`
18250
+ );
18093
18251
  }
18094
18252
  await this.refill();
18095
18253
  }
@@ -18586,7 +18744,7 @@ async function poolServe() {
18586
18744
 
18587
18745
  // src/servers/runner-serve.ts
18588
18746
  import { spawn as spawn8 } from "child_process";
18589
- import * as fs45 from "fs";
18747
+ import * as fs46 from "fs";
18590
18748
  import { createServer as createServer5 } from "http";
18591
18749
  var DEFAULT_PORT2 = 8080;
18592
18750
  var DEFAULT_WORKDIR = "/workspace/repo";
@@ -18666,8 +18824,8 @@ async function defaultRunJob(job) {
18666
18824
  const workdir = process.env.RUNNER_WORKDIR ?? DEFAULT_WORKDIR;
18667
18825
  const branch = job.ref ?? "main";
18668
18826
  const authUrl = `https://x-access-token:${job.githubToken}@github.com/${job.repo}.git`;
18669
- fs45.rmSync(workdir, { recursive: true, force: true });
18670
- fs45.mkdirSync(workdir, { recursive: true });
18827
+ fs46.rmSync(workdir, { recursive: true, force: true });
18828
+ fs46.mkdirSync(workdir, { recursive: true });
18671
18829
  const allSecrets = typeof job.allSecrets === "string" ? job.allSecrets : JSON.stringify(job.allSecrets ?? {});
18672
18830
  const interactive = job.mode === "interactive";
18673
18831
  const scheduled = job.mode === "scheduled";
@@ -19124,7 +19282,6 @@ Usage:
19124
19282
  kody-engine release --issue <N> [--cwd <path>] [--verbose|--quiet]
19125
19283
  kody-engine init [--cwd <path>] [--verbose|--quiet]
19126
19284
  kody-engine <action> [--cwd <path>] [--verbose|--quiet]
19127
- kody-engine exec <executable> [--cwd <path>] [--verbose|--quiet]
19128
19285
  kody-engine ci [preflight flags \u2014 see: kody-engine ci --help]
19129
19286
  kody-engine chat [chat flags \u2014 see: kody-engine chat --help]
19130
19287
  kody-engine stats [--since 7d|--run <id>|--json|--cwd <path>]
@@ -19132,8 +19289,7 @@ Usage:
19132
19289
  kody-engine version
19133
19290
 
19134
19291
  Top-level work commands are duty actions. A duty owns the public action name
19135
- and selects an implementation executable. \`exec <executable>\` is the low-level
19136
- debug path for engine internals and migration compatibility.
19292
+ and selects an implementation executable.
19137
19293
 
19138
19294
  Exit codes:
19139
19295
  0 success (PR opened, verify passed \u2014 or resolve produced a merge commit)
@@ -19174,24 +19330,6 @@ function parseArgs(argv) {
19174
19330
  result.serverArgs = argv.slice(1).filter((a) => !a.startsWith("-"));
19175
19331
  return result;
19176
19332
  }
19177
- if (cmd === "exec") {
19178
- const executableName = argv[1];
19179
- if (!executableName) {
19180
- result.errors.push("exec requires an executable name");
19181
- return result;
19182
- }
19183
- if (!hasExecutable(executableName)) {
19184
- result.errors.push(`unknown executable: ${executableName}`);
19185
- return result;
19186
- }
19187
- result.command = "__executable__";
19188
- result.executableName = executableName;
19189
- result.cliArgs = parseGenericFlags(argv.slice(2));
19190
- if (typeof result.cliArgs.cwd === "string") result.cwd = result.cliArgs.cwd;
19191
- if (result.cliArgs.verbose === true) result.verbose = true;
19192
- if (result.cliArgs.quiet === true) result.quiet = true;
19193
- return result;
19194
- }
19195
19333
  if (hasDutyAction(cmd)) {
19196
19334
  result.command = "__duty__";
19197
19335
  result.actionName = cmd;
@@ -19201,18 +19339,8 @@ function parseArgs(argv) {
19201
19339
  if (result.cliArgs.quiet === true) result.quiet = true;
19202
19340
  return result;
19203
19341
  }
19204
- if (hasExecutable(cmd)) {
19205
- result.command = "__executable__";
19206
- result.executableName = cmd;
19207
- result.cliArgs = parseGenericFlags(argv.slice(1));
19208
- if (typeof result.cliArgs.cwd === "string") result.cwd = result.cliArgs.cwd;
19209
- if (result.cliArgs.verbose === true) result.verbose = true;
19210
- if (result.cliArgs.quiet === true) result.quiet = true;
19211
- return result;
19212
- }
19213
19342
  const discoveredActions = listDutyActions().map((e) => e.action);
19214
- const discoveredExecutables = listExecutables().map((e) => `exec ${e.name}`);
19215
- const available = ["ci", "chat", "stats", "help", "version", ...discoveredActions, ...discoveredExecutables];
19343
+ const available = ["ci", "chat", "stats", "help", "version", ...discoveredActions];
19216
19344
  result.errors.push(`unknown command: ${cmd} (available: ${available.join(", ")})`);
19217
19345
  return result;
19218
19346
  }
@@ -19310,7 +19438,7 @@ ${HELP_TEXT}`);
19310
19438
  return 64;
19311
19439
  }
19312
19440
  const cliArgs = { ...route.cliArgs, ...args.cliArgs ?? {} };
19313
- const skipConfig2 = configlessCommands.has(route.executable);
19441
+ const skipConfig = configlessCommands.has(route.executable);
19314
19442
  try {
19315
19443
  const result = await runJob(
19316
19444
  {
@@ -19323,7 +19451,7 @@ ${HELP_TEXT}`);
19323
19451
  },
19324
19452
  {
19325
19453
  cwd,
19326
- skipConfig: skipConfig2,
19454
+ skipConfig,
19327
19455
  verbose: args.verbose,
19328
19456
  quiet: args.quiet
19329
19457
  }
@@ -19344,30 +19472,8 @@ ${HELP_TEXT}`);
19344
19472
  return 99;
19345
19473
  }
19346
19474
  }
19347
- const skipConfig = configlessCommands.has(args.executableName ?? "");
19348
- try {
19349
- const result = await runExecutableChain(args.executableName, {
19350
- cliArgs: args.cliArgs ?? {},
19351
- cwd,
19352
- skipConfig,
19353
- verbose: args.verbose,
19354
- quiet: args.quiet
19355
- });
19356
- if (result.exitCode !== 0 && result.reason) {
19357
- process.stderr.write(`error: ${result.reason}
19358
- `);
19359
- }
19360
- return result.exitCode;
19361
- } catch (err) {
19362
- const msg = err instanceof Error ? err.message : String(err);
19363
- process.stderr.write(`[kody] ${args.executableName} crashed: ${msg}
19364
- `);
19365
- if (err instanceof Error && err.stack) process.stderr.write(`${err.stack}
19366
- `);
19367
- process.stdout.write(`PR_URL=FAILED: ${args.executableName} crashed: ${msg}
19368
- `);
19369
- return 99;
19370
- }
19475
+ process.stderr.write("error: command did not resolve to a duty\n");
19476
+ return 64;
19371
19477
  }
19372
19478
  function numericTarget(cliArgs) {
19373
19479
  for (const key of ["issue", "pr"]) {