@kody-ade/kody-engine 0.4.221 → 0.4.222

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/kody.js CHANGED
@@ -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.222",
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",
@@ -1089,8 +1089,8 @@ function listRepairCandidates(repoSlug) {
1089
1089
  };
1090
1090
  });
1091
1091
  }
1092
- function dispatchVerb(workflowFile, executable, prNumber) {
1093
- return dispatchWorkflow(workflowFile, executable, prNumber);
1092
+ function dispatchVerb(workflowFile, duty, prNumber) {
1093
+ return dispatchWorkflow(workflowFile, duty, prNumber);
1094
1094
  }
1095
1095
  function postRecommendation(prNumber, mention, message, dutySlug) {
1096
1096
  const mentioned = mention ? `${mention} ${message}` : message;
@@ -1219,17 +1219,17 @@ ${marker}` });
1219
1219
  return { error: err instanceof Error ? err.message : String(err) };
1220
1220
  }
1221
1221
  }
1222
- function dispatchWorkflow(workflowFile, executable, issueNumber) {
1222
+ function dispatchWorkflow(workflowFile, duty, issueNumber) {
1223
1223
  try {
1224
- gh(["workflow", "run", workflowFile, "-f", `executable=${executable}`, "-f", `issue_number=${issueNumber}`]);
1224
+ gh(["workflow", "run", workflowFile, "-f", `duty=${duty}`, "-f", `issue_number=${issueNumber}`]);
1225
1225
  return { ok: true };
1226
1226
  } catch (err) {
1227
1227
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
1228
1228
  }
1229
1229
  }
1230
- function isDispatchGated(executable, mode) {
1230
+ function isDispatchGated(duty, mode) {
1231
1231
  if (mode === "auto") return false;
1232
- if (executable && GATE_EXEMPT_EXECUTABLES.has(executable)) return false;
1232
+ if (duty && GATE_EXEMPT_DUTIES.has(duty)) return false;
1233
1233
  return true;
1234
1234
  }
1235
1235
  function trustRefusal(dutySlug) {
@@ -1374,19 +1374,20 @@ function dutyToolDefinitions(opts) {
1374
1374
  };
1375
1375
  const dispatchTool = {
1376
1376
  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}.",
1377
+ 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
1378
  inputSchema: {
1379
- executable: z2.string().min(1).describe("Executable/stage to run (e.g. 'run')."),
1379
+ duty: z2.string().min(1).optional().describe("Duty action to run (e.g. 'run')."),
1380
+ executable: z2.string().min(1).optional().describe("Deprecated alias for duty."),
1380
1381
  issueNumber: z2.number().int().positive().describe("Issue (or PR) number forwarded as issue_number.")
1381
1382
  },
1382
1383
  handler: async (args) => {
1383
- const executable = String(args.executable ?? "");
1384
+ const duty = String(args.duty ?? args.executable ?? "");
1384
1385
  const issueNumber = Number(args.issueNumber);
1385
- if (isDispatchGated(executable, readDutyTrustMode(opts.repoSlug, opts.dutySlug))) {
1386
+ if (isDispatchGated(duty, readDutyTrustMode(opts.repoSlug, opts.dutySlug))) {
1386
1387
  return { content: [{ type: "text", text: trustRefusal(opts.dutySlug) }] };
1387
1388
  }
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}`;
1389
+ const result = dispatchWorkflow(workflowFile, duty, issueNumber);
1390
+ const text = result.ok ? `Dispatched duty \`${duty}\` on #${issueNumber} via workflow_dispatch.` : `Dispatch failed for duty \`${duty}\` on #${issueNumber}: ${result.error}`;
1390
1391
  return { content: [{ type: "text", text }] };
1391
1392
  }
1392
1393
  };
@@ -1421,7 +1422,7 @@ function buildDutyMcpServer(opts) {
1421
1422
  });
1422
1423
  return { server };
1423
1424
  }
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;
1425
+ 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
1426
  var init_dutyMcp = __esm({
1426
1427
  "src/dutyMcp.ts"() {
1427
1428
  "use strict";
@@ -1435,7 +1436,7 @@ var init_dutyMcp = __esm({
1435
1436
  DEFAULT_IGNORE_CHECKS = ["run", "kody", "duty-tick", "goal-tick", "worker-ask", "chat"];
1436
1437
  trackMarker = (key) => `<!-- kody-track:${key} -->`;
1437
1438
  commentMarker = (key) => `<!-- kody-track-comment:${key} -->`;
1438
- GATE_EXEMPT_EXECUTABLES = /* @__PURE__ */ new Set(["qa-engineer", "ui-review"]);
1439
+ GATE_EXEMPT_DUTIES = /* @__PURE__ */ new Set(["qa-engineer", "ui-review"]);
1439
1440
  DUTY_MCP_TOOL_NAMES = [
1440
1441
  "list_prs_to_repair",
1441
1442
  "sync_pr",
@@ -2237,9 +2238,6 @@ function resolveExecutable(name, roots = getExecutableRoots()) {
2237
2238
  }
2238
2239
  return null;
2239
2240
  }
2240
- function hasExecutable(name, roots = getExecutableRoots()) {
2241
- return resolveExecutable(name, roots) !== null;
2242
- }
2243
2241
  function listDutyActions(projectDutiesRoot = getProjectDutiesRoot()) {
2244
2242
  const seen = /* @__PURE__ */ new Set();
2245
2243
  const out = [];
@@ -2439,6 +2437,73 @@ var init_task_artifacts = __esm({
2439
2437
  }
2440
2438
  });
2441
2439
 
2440
+ // src/gha.ts
2441
+ import { execFileSync as execFileSync2 } from "child_process";
2442
+ import * as fs14 from "fs";
2443
+ function getRunUrl() {
2444
+ const server = process.env.GITHUB_SERVER_URL;
2445
+ const repo = process.env.GITHUB_REPOSITORY;
2446
+ const runId = process.env.GITHUB_RUN_ID;
2447
+ if (!server || !repo || !runId) return "";
2448
+ return `${server}/${repo}/actions/runs/${runId}`;
2449
+ }
2450
+ function reactToTriggerComment(cwd) {
2451
+ if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
2452
+ const eventPath = process.env.GITHUB_EVENT_PATH;
2453
+ if (!eventPath || !fs14.existsSync(eventPath)) return;
2454
+ let event = null;
2455
+ try {
2456
+ event = JSON.parse(fs14.readFileSync(eventPath, "utf-8"));
2457
+ } catch {
2458
+ return;
2459
+ }
2460
+ const commentId = event?.comment?.id;
2461
+ const repo = process.env.GITHUB_REPOSITORY;
2462
+ if (!commentId || !repo) return;
2463
+ const token = process.env.KODY_TOKEN?.trim() || process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
2464
+ const args = [
2465
+ "api",
2466
+ "-X",
2467
+ "POST",
2468
+ "-H",
2469
+ "Accept: application/vnd.github+json",
2470
+ `/repos/${repo}/issues/comments/${commentId}/reactions`,
2471
+ "-f",
2472
+ "content=eyes"
2473
+ ];
2474
+ const opts = {
2475
+ cwd,
2476
+ env: { ...process.env, GH_TOKEN: token ?? process.env.GH_TOKEN ?? "" },
2477
+ stdio: "pipe",
2478
+ timeout: 15e3
2479
+ };
2480
+ let lastErr = null;
2481
+ for (let attempt = 0; attempt < 3; attempt++) {
2482
+ if (attempt > 0) sleepMs(attempt === 1 ? 500 : 1500);
2483
+ try {
2484
+ execFileSync2("gh", args, opts);
2485
+ return;
2486
+ } catch (err) {
2487
+ lastErr = err;
2488
+ }
2489
+ }
2490
+ process.stderr.write(
2491
+ `[kody] \u{1F440} reaction failed after 3 attempts on comment ${commentId}: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}
2492
+ `
2493
+ );
2494
+ }
2495
+ function sleepMs(ms) {
2496
+ try {
2497
+ execFileSync2("sleep", [(ms / 1e3).toString()], { stdio: "ignore", timeout: ms + 1e3 });
2498
+ } catch {
2499
+ }
2500
+ }
2501
+ var init_gha = __esm({
2502
+ "src/gha.ts"() {
2503
+ "use strict";
2504
+ }
2505
+ });
2506
+
2442
2507
  // src/profile-error.ts
2443
2508
  var ProfileError;
2444
2509
  var init_profile_error = __esm({
@@ -2599,7 +2664,7 @@ var init_lifecycles = __esm({
2599
2664
  });
2600
2665
 
2601
2666
  // src/scripts/buildSyntheticPlugin.ts
2602
- import * as fs14 from "fs";
2667
+ import * as fs15 from "fs";
2603
2668
  import * as os2 from "os";
2604
2669
  import * as path13 from "path";
2605
2670
  function getPluginsCatalogRoot() {
@@ -2613,17 +2678,17 @@ function getPluginsCatalogRoot() {
2613
2678
  // fallback
2614
2679
  ];
2615
2680
  for (const c of candidates) {
2616
- if (fs14.existsSync(c) && fs14.statSync(c).isDirectory()) return c;
2681
+ if (fs15.existsSync(c) && fs15.statSync(c).isDirectory()) return c;
2617
2682
  }
2618
2683
  return candidates[0];
2619
2684
  }
2620
2685
  function copyDir(src, dst) {
2621
- fs14.mkdirSync(dst, { recursive: true });
2622
- for (const ent of fs14.readdirSync(src, { withFileTypes: true })) {
2686
+ fs15.mkdirSync(dst, { recursive: true });
2687
+ for (const ent of fs15.readdirSync(src, { withFileTypes: true })) {
2623
2688
  const s = path13.join(src, ent.name);
2624
2689
  const d = path13.join(dst, ent.name);
2625
2690
  if (ent.isDirectory()) copyDir(s, d);
2626
- else if (ent.isFile()) fs14.copyFileSync(s, d);
2691
+ else if (ent.isFile()) fs15.copyFileSync(s, d);
2627
2692
  }
2628
2693
  }
2629
2694
  var buildSyntheticPlugin;
@@ -2637,44 +2702,44 @@ var init_buildSyntheticPlugin = __esm({
2637
2702
  const catalog = getPluginsCatalogRoot();
2638
2703
  const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2639
2704
  const root = path13.join(os2.tmpdir(), `kody-synth-${runId}`);
2640
- fs14.mkdirSync(path13.join(root, ".claude-plugin"), { recursive: true });
2705
+ fs15.mkdirSync(path13.join(root, ".claude-plugin"), { recursive: true });
2641
2706
  const resolvePart = (bucket, entry) => {
2642
2707
  const local = path13.join(profile.dir, bucket, entry);
2643
- if (fs14.existsSync(local)) return local;
2708
+ if (fs15.existsSync(local)) return local;
2644
2709
  const central = path13.join(catalog, bucket, entry);
2645
- if (fs14.existsSync(central)) return central;
2710
+ if (fs15.existsSync(central)) return central;
2646
2711
  throw new Error(
2647
2712
  `buildSyntheticPlugin: ${bucket} entry '${entry}' not found in executable dir (${profile.dir}/${bucket}/) or catalog (${catalog}/${bucket}/)`
2648
2713
  );
2649
2714
  };
2650
2715
  if (cc.skills.length > 0) {
2651
2716
  const dst = path13.join(root, "skills");
2652
- fs14.mkdirSync(dst, { recursive: true });
2717
+ fs15.mkdirSync(dst, { recursive: true });
2653
2718
  for (const name of cc.skills) {
2654
2719
  copyDir(resolvePart("skills", name), path13.join(dst, name));
2655
2720
  }
2656
2721
  }
2657
2722
  if (cc.commands.length > 0) {
2658
2723
  const dst = path13.join(root, "commands");
2659
- fs14.mkdirSync(dst, { recursive: true });
2724
+ fs15.mkdirSync(dst, { recursive: true });
2660
2725
  for (const name of cc.commands) {
2661
- fs14.copyFileSync(resolvePart("commands", `${name}.md`), path13.join(dst, `${name}.md`));
2726
+ fs15.copyFileSync(resolvePart("commands", `${name}.md`), path13.join(dst, `${name}.md`));
2662
2727
  }
2663
2728
  }
2664
2729
  if (cc.hooks.length > 0) {
2665
2730
  const dst = path13.join(root, "hooks");
2666
- fs14.mkdirSync(dst, { recursive: true });
2731
+ fs15.mkdirSync(dst, { recursive: true });
2667
2732
  const merged = { hooks: {} };
2668
2733
  for (const name of cc.hooks) {
2669
2734
  const src = resolvePart("hooks", `${name}.json`);
2670
- const parsed = JSON.parse(fs14.readFileSync(src, "utf-8"));
2735
+ const parsed = JSON.parse(fs15.readFileSync(src, "utf-8"));
2671
2736
  for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
2672
2737
  if (!Array.isArray(entries)) continue;
2673
2738
  if (!merged.hooks[event]) merged.hooks[event] = [];
2674
2739
  merged.hooks[event].push(...entries);
2675
2740
  }
2676
2741
  }
2677
- fs14.writeFileSync(path13.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
2742
+ fs15.writeFileSync(path13.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
2678
2743
  `);
2679
2744
  }
2680
2745
  const manifest = {
@@ -2684,7 +2749,7 @@ var init_buildSyntheticPlugin = __esm({
2684
2749
  };
2685
2750
  if (cc.skills.length > 0) manifest.skills = ["./skills/"];
2686
2751
  if (cc.commands.length > 0) manifest.commands = ["./commands/"];
2687
- fs14.writeFileSync(path13.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
2752
+ fs15.writeFileSync(path13.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
2688
2753
  `);
2689
2754
  ctx.data.syntheticPluginPath = root;
2690
2755
  };
@@ -2692,7 +2757,7 @@ var init_buildSyntheticPlugin = __esm({
2692
2757
  });
2693
2758
 
2694
2759
  // src/subagents.ts
2695
- import * as fs15 from "fs";
2760
+ import * as fs16 from "fs";
2696
2761
  import * as path14 from "path";
2697
2762
  function splitFrontmatter(raw) {
2698
2763
  const match = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(raw);
@@ -2707,9 +2772,9 @@ function splitFrontmatter(raw) {
2707
2772
  }
2708
2773
  function resolveAgentFile(profileDir, name) {
2709
2774
  const local = path14.join(profileDir, "agents", `${name}.md`);
2710
- if (fs15.existsSync(local)) return local;
2775
+ if (fs16.existsSync(local)) return local;
2711
2776
  const central = path14.join(getPluginsCatalogRoot(), "agents", `${name}.md`);
2712
- if (fs15.existsSync(central)) return central;
2777
+ if (fs16.existsSync(central)) return central;
2713
2778
  throw new Error(`loadSubagents: agent '${name}' not found in ${profileDir}/agents/ or shared catalog`);
2714
2779
  }
2715
2780
  function captureSubagentTemplates(profile) {
@@ -2718,7 +2783,7 @@ function captureSubagentTemplates(profile) {
2718
2783
  const out = {};
2719
2784
  for (const name of names) {
2720
2785
  try {
2721
- out[name] = fs15.readFileSync(resolveAgentFile(profile.dir, name), "utf-8");
2786
+ out[name] = fs16.readFileSync(resolveAgentFile(profile.dir, name), "utf-8");
2722
2787
  } catch {
2723
2788
  }
2724
2789
  }
@@ -2729,7 +2794,7 @@ function loadSubagents(profile) {
2729
2794
  if (!names || names.length === 0) return void 0;
2730
2795
  const agents = {};
2731
2796
  for (const name of names) {
2732
- const raw = profile.subagentTemplates?.[name] ?? fs15.readFileSync(resolveAgentFile(profile.dir, name), "utf-8");
2797
+ const raw = profile.subagentTemplates?.[name] ?? fs16.readFileSync(resolveAgentFile(profile.dir, name), "utf-8");
2733
2798
  const { fm, body } = splitFrontmatter(raw);
2734
2799
  if (!body) throw new Error(`loadSubagents: agent '${name}' has an empty prompt body`);
2735
2800
  const def = {
@@ -2753,15 +2818,15 @@ var init_subagents = __esm({
2753
2818
  });
2754
2819
 
2755
2820
  // src/profile.ts
2756
- import * as fs16 from "fs";
2821
+ import * as fs17 from "fs";
2757
2822
  import * as path15 from "path";
2758
2823
  function loadProfile(profilePath) {
2759
- if (!fs16.existsSync(profilePath)) {
2824
+ if (!fs17.existsSync(profilePath)) {
2760
2825
  throw new ProfileError(profilePath, "file not found");
2761
2826
  }
2762
2827
  let raw;
2763
2828
  try {
2764
- raw = JSON.parse(fs16.readFileSync(profilePath, "utf-8"));
2829
+ raw = JSON.parse(fs17.readFileSync(profilePath, "utf-8"));
2765
2830
  } catch (err) {
2766
2831
  throw new ProfileError(profilePath, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
2767
2832
  }
@@ -2894,7 +2959,7 @@ function readPromptTemplates(dir) {
2894
2959
  const out = {};
2895
2960
  const read = (p) => {
2896
2961
  try {
2897
- out[p] = fs16.readFileSync(p, "utf-8");
2962
+ out[p] = fs17.readFileSync(p, "utf-8");
2898
2963
  } catch {
2899
2964
  }
2900
2965
  };
@@ -2902,7 +2967,7 @@ function readPromptTemplates(dir) {
2902
2967
  read(path15.join(dir, "duty.md"));
2903
2968
  try {
2904
2969
  const promptsDir = path15.join(dir, "prompts");
2905
- for (const ent of fs16.readdirSync(promptsDir)) {
2970
+ for (const ent of fs17.readdirSync(promptsDir)) {
2906
2971
  if (ent.endsWith(".md")) read(path15.join(promptsDir, ent));
2907
2972
  }
2908
2973
  } catch {
@@ -3210,7 +3275,7 @@ var init_profile = __esm({
3210
3275
  });
3211
3276
 
3212
3277
  // src/state.ts
3213
- import { execFileSync as execFileSync2 } from "child_process";
3278
+ import { execFileSync as execFileSync3 } from "child_process";
3214
3279
  function emptyState() {
3215
3280
  return {
3216
3281
  schemaVersion: 1,
@@ -3233,7 +3298,7 @@ function ghToken2() {
3233
3298
  function gh2(args, input, cwd) {
3234
3299
  const token = ghToken2();
3235
3300
  const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
3236
- return execFileSync2("gh", args, {
3301
+ return execFileSync3("gh", args, {
3237
3302
  encoding: "utf-8",
3238
3303
  timeout: API_TIMEOUT_MS2,
3239
3304
  cwd,
@@ -3580,16 +3645,16 @@ var init_state = __esm({
3580
3645
  });
3581
3646
 
3582
3647
  // src/prompt.ts
3583
- import * as fs17 from "fs";
3648
+ import * as fs18 from "fs";
3584
3649
  import * as path16 from "path";
3585
3650
  function loadProjectConventions(projectDir) {
3586
3651
  const out = [];
3587
3652
  for (const rel of CONVENTION_FILES) {
3588
3653
  const abs = path16.join(projectDir, rel);
3589
- if (!fs17.existsSync(abs)) continue;
3654
+ if (!fs18.existsSync(abs)) continue;
3590
3655
  let content;
3591
3656
  try {
3592
- content = fs17.readFileSync(abs, "utf-8");
3657
+ content = fs18.readFileSync(abs, "utf-8");
3593
3658
  } catch {
3594
3659
  continue;
3595
3660
  }
@@ -3824,20 +3889,20 @@ var loadMemoryContext_exports = {};
3824
3889
  __export(loadMemoryContext_exports, {
3825
3890
  loadMemoryContext: () => loadMemoryContext
3826
3891
  });
3827
- import * as fs18 from "fs";
3892
+ import * as fs19 from "fs";
3828
3893
  import * as path17 from "path";
3829
3894
  function collectPages(memoryAbs) {
3830
3895
  const out = [];
3831
3896
  walkMd(memoryAbs, (file) => {
3832
3897
  let stat;
3833
3898
  try {
3834
- stat = fs18.statSync(file);
3899
+ stat = fs19.statSync(file);
3835
3900
  } catch {
3836
3901
  return;
3837
3902
  }
3838
3903
  let raw;
3839
3904
  try {
3840
- raw = fs18.readFileSync(file, "utf-8");
3905
+ raw = fs19.readFileSync(file, "utf-8");
3841
3906
  } catch {
3842
3907
  return;
3843
3908
  }
@@ -3913,7 +3978,7 @@ function walkMd(root, visit) {
3913
3978
  const dir = stack.pop();
3914
3979
  let names;
3915
3980
  try {
3916
- names = fs18.readdirSync(dir);
3981
+ names = fs19.readdirSync(dir);
3917
3982
  } catch {
3918
3983
  continue;
3919
3984
  }
@@ -3922,7 +3987,7 @@ function walkMd(root, visit) {
3922
3987
  const full = path17.join(dir, name);
3923
3988
  let stat;
3924
3989
  try {
3925
- stat = fs18.statSync(full);
3990
+ stat = fs19.statSync(full);
3926
3991
  } catch {
3927
3992
  continue;
3928
3993
  }
@@ -3946,7 +4011,7 @@ var init_loadMemoryContext = __esm({
3946
4011
  loadMemoryContext = async (ctx) => {
3947
4012
  if (typeof ctx.data.memoryContext === "string") return;
3948
4013
  const memoryAbs = path17.join(ctx.cwd, MEMORY_DIR_RELATIVE);
3949
- if (!fs18.existsSync(memoryAbs)) {
4014
+ if (!fs19.existsSync(memoryAbs)) {
3950
4015
  ctx.data.memoryContext = "";
3951
4016
  return;
3952
4017
  }
@@ -3989,12 +4054,12 @@ var init_loadCoverageRules = __esm({
3989
4054
  });
3990
4055
 
3991
4056
  // src/container.ts
3992
- import { execFileSync as execFileSync3 } from "child_process";
3993
- import * as fs19 from "fs";
4057
+ import { execFileSync as execFileSync4 } from "child_process";
4058
+ import * as fs20 from "fs";
3994
4059
  function getProfileInputsForChild(profileName, _cwd) {
3995
4060
  try {
3996
4061
  const profilePath = resolveProfilePath(profileName);
3997
- if (!fs19.existsSync(profilePath)) return null;
4062
+ if (!fs20.existsSync(profilePath)) return null;
3998
4063
  return loadProfile(profilePath).inputs;
3999
4064
  } catch {
4000
4065
  return null;
@@ -4228,7 +4293,7 @@ async function runContainerLoop(profile, ctx, input) {
4228
4293
  }
4229
4294
  function resetWorkingTree(cwd) {
4230
4295
  try {
4231
- execFileSync3("git", ["reset", "--hard", "HEAD"], {
4296
+ execFileSync4("git", ["reset", "--hard", "HEAD"], {
4232
4297
  cwd,
4233
4298
  stdio: ["ignore", "pipe", "pipe"],
4234
4299
  timeout: 3e4
@@ -4455,8 +4520,8 @@ var init_lifecycleLabels = __esm({
4455
4520
  });
4456
4521
 
4457
4522
  // src/litellm.ts
4458
- import { execFileSync as execFileSync4, spawn as spawn3 } from "child_process";
4459
- import * as fs20 from "fs";
4523
+ import { execFileSync as execFileSync5, spawn as spawn3 } from "child_process";
4524
+ import * as fs21 from "fs";
4460
4525
  import * as os3 from "os";
4461
4526
  import * as path18 from "path";
4462
4527
  async function checkLitellmHealth(url) {
@@ -4488,7 +4553,7 @@ function generateLitellmConfigYaml(model) {
4488
4553
  }
4489
4554
  function litellmImportable() {
4490
4555
  try {
4491
- execFileSync4("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
4556
+ execFileSync5("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
4492
4557
  return true;
4493
4558
  } catch {
4494
4559
  return false;
@@ -4496,7 +4561,7 @@ function litellmImportable() {
4496
4561
  }
4497
4562
  function locateLitellmScript() {
4498
4563
  try {
4499
- const out = execFileSync4(
4564
+ const out = execFileSync5(
4500
4565
  "python3",
4501
4566
  [
4502
4567
  "-c",
@@ -4511,7 +4576,7 @@ function locateLitellmScript() {
4511
4576
  }
4512
4577
  function resolveLitellmCommand() {
4513
4578
  try {
4514
- execFileSync4("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
4579
+ execFileSync5("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
4515
4580
  return "litellm";
4516
4581
  } catch {
4517
4582
  if (!litellmImportable()) {
@@ -4519,7 +4584,7 @@ function resolveLitellmCommand() {
4519
4584
  let installed = false;
4520
4585
  for (const pip of ["pip", "pip3"]) {
4521
4586
  try {
4522
- execFileSync4(pip, ["install", "litellm[proxy]"], { timeout: 3e5, stdio: "inherit" });
4587
+ execFileSync5(pip, ["install", "litellm[proxy]"], { timeout: 3e5, stdio: "inherit" });
4523
4588
  installed = true;
4524
4589
  break;
4525
4590
  } catch {
@@ -4544,12 +4609,12 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
4544
4609
  let logPath;
4545
4610
  const spawnProxy = () => {
4546
4611
  const configPath = path18.join(os3.tmpdir(), `kody-local-litellm-${Date.now()}.yaml`);
4547
- fs20.writeFileSync(configPath, generateLitellmConfigYaml(model));
4612
+ fs21.writeFileSync(configPath, generateLitellmConfigYaml(model));
4548
4613
  const args = ["--config", configPath, "--port", port];
4549
4614
  const nextLogPath = path18.join(os3.tmpdir(), `kody-local-litellm-${Date.now()}.log`);
4550
- const outFd = fs20.openSync(nextLogPath, "w");
4615
+ const outFd = fs21.openSync(nextLogPath, "w");
4551
4616
  child = spawn3(cmd, args, { stdio: ["ignore", outFd, outFd], detached: true, env: childEnv });
4552
- fs20.closeSync(outFd);
4617
+ fs21.closeSync(outFd);
4553
4618
  logPath = nextLogPath;
4554
4619
  };
4555
4620
  const waitForHealth = async () => {
@@ -4563,7 +4628,7 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
4563
4628
  const readLogTail = () => {
4564
4629
  if (!logPath) return "";
4565
4630
  try {
4566
- return fs20.readFileSync(logPath, "utf-8").slice(-2e3);
4631
+ return fs21.readFileSync(logPath, "utf-8").slice(-2e3);
4567
4632
  } catch {
4568
4633
  return "";
4569
4634
  }
@@ -4616,9 +4681,9 @@ ${tail}`
4616
4681
  }
4617
4682
  function readDotenvApiKeys(projectDir) {
4618
4683
  const dotenvPath = path18.join(projectDir, ".env");
4619
- if (!fs20.existsSync(dotenvPath)) return {};
4684
+ if (!fs21.existsSync(dotenvPath)) return {};
4620
4685
  const result = {};
4621
- for (const rawLine of fs20.readFileSync(dotenvPath, "utf-8").split("\n")) {
4686
+ for (const rawLine of fs21.readFileSync(dotenvPath, "utf-8").split("\n")) {
4622
4687
  const line = rawLine.trim();
4623
4688
  if (!line || line.startsWith("#")) continue;
4624
4689
  const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
@@ -4650,14 +4715,14 @@ var init_litellm = __esm({
4650
4715
  });
4651
4716
 
4652
4717
  // src/pushWithRetry.ts
4653
- import { execFileSync as execFileSync5 } from "child_process";
4718
+ import { execFileSync as execFileSync6 } from "child_process";
4654
4719
  function sleepSync(ms) {
4655
4720
  if (ms <= 0) return;
4656
4721
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
4657
4722
  }
4658
4723
  function runGit(args, cwd) {
4659
4724
  try {
4660
- const stdout = execFileSync5("git", args, {
4725
+ const stdout = execFileSync6("git", args, {
4661
4726
  cwd,
4662
4727
  encoding: "utf-8",
4663
4728
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
@@ -4732,12 +4797,12 @@ var init_pushWithRetry = __esm({
4732
4797
  });
4733
4798
 
4734
4799
  // src/commit.ts
4735
- import { execFileSync as execFileSync6 } from "child_process";
4736
- import * as fs21 from "fs";
4800
+ import { execFileSync as execFileSync7 } from "child_process";
4801
+ import * as fs22 from "fs";
4737
4802
  import * as path19 from "path";
4738
4803
  function git(args, cwd) {
4739
4804
  try {
4740
- return execFileSync6("git", args, {
4805
+ return execFileSync7("git", args, {
4741
4806
  encoding: "utf-8",
4742
4807
  timeout: 12e4,
4743
4808
  cwd,
@@ -4773,17 +4838,17 @@ function ensureGitIdentity(cwd) {
4773
4838
  function abortUnfinishedGitOps(cwd) {
4774
4839
  const aborted = [];
4775
4840
  const gitDir = path19.join(cwd ?? process.cwd(), ".git");
4776
- if (!fs21.existsSync(gitDir)) return aborted;
4777
- if (fs21.existsSync(path19.join(gitDir, "MERGE_HEAD"))) {
4841
+ if (!fs22.existsSync(gitDir)) return aborted;
4842
+ if (fs22.existsSync(path19.join(gitDir, "MERGE_HEAD"))) {
4778
4843
  if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
4779
4844
  }
4780
- if (fs21.existsSync(path19.join(gitDir, "CHERRY_PICK_HEAD"))) {
4845
+ if (fs22.existsSync(path19.join(gitDir, "CHERRY_PICK_HEAD"))) {
4781
4846
  if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
4782
4847
  }
4783
- if (fs21.existsSync(path19.join(gitDir, "REVERT_HEAD"))) {
4848
+ if (fs22.existsSync(path19.join(gitDir, "REVERT_HEAD"))) {
4784
4849
  if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
4785
4850
  }
4786
- if (fs21.existsSync(path19.join(gitDir, "rebase-merge")) || fs21.existsSync(path19.join(gitDir, "rebase-apply"))) {
4851
+ if (fs22.existsSync(path19.join(gitDir, "rebase-merge")) || fs22.existsSync(path19.join(gitDir, "rebase-apply"))) {
4787
4852
  if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
4788
4853
  }
4789
4854
  try {
@@ -4804,7 +4869,7 @@ function isForbiddenPath(p) {
4804
4869
  return false;
4805
4870
  }
4806
4871
  function listChangedFiles(cwd) {
4807
- const raw = execFileSync6("git", ["status", "--porcelain=v1", "-z"], {
4872
+ const raw = execFileSync7("git", ["status", "--porcelain=v1", "-z"], {
4808
4873
  encoding: "utf-8",
4809
4874
  cwd,
4810
4875
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
@@ -4816,7 +4881,7 @@ function listChangedFiles(cwd) {
4816
4881
  }
4817
4882
  function listFilesInCommit(ref = "HEAD", cwd) {
4818
4883
  try {
4819
- const raw = execFileSync6("git", ["show", "--name-only", "--pretty=format:", "-z", ref], {
4884
+ const raw = execFileSync7("git", ["show", "--name-only", "--pretty=format:", "-z", ref], {
4820
4885
  encoding: "utf-8",
4821
4886
  cwd,
4822
4887
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
@@ -4839,7 +4904,7 @@ function normalizeCommitMessage(raw) {
4839
4904
  function commitAndPush(branch, agentMessage, cwd) {
4840
4905
  const allChanged = listChangedFiles(cwd);
4841
4906
  const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
4842
- const mergeHeadExists = fs21.existsSync(path19.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
4907
+ const mergeHeadExists = fs22.existsSync(path19.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
4843
4908
  if (allowedFiles.length === 0 && !mergeHeadExists) {
4844
4909
  return { committed: false, pushed: false, sha: "", message: "" };
4845
4910
  }
@@ -4995,10 +5060,10 @@ var init_saveTaskState = __esm({
4995
5060
  });
4996
5061
 
4997
5062
  // src/scripts/advanceFlow.ts
4998
- import { execFileSync as execFileSync7 } from "child_process";
5063
+ import { execFileSync as execFileSync8 } from "child_process";
4999
5064
  function ghComment(issueNumber, body, cwd, label) {
5000
5065
  try {
5001
- execFileSync7("gh", ["issue", "comment", String(issueNumber), "--body", body], {
5066
+ execFileSync8("gh", ["issue", "comment", String(issueNumber), "--body", body], {
5002
5067
  timeout: API_TIMEOUT_MS3,
5003
5068
  cwd,
5004
5069
  stdio: ["ignore", "pipe", "pipe"]
@@ -5069,78 +5134,11 @@ var init_advanceFlow = __esm({
5069
5134
  `
5070
5135
  );
5071
5136
  }
5072
- ctx.output.nextDispatch = { executable: flow.name, cliArgs: { issue: flow.issueNumber } };
5137
+ ctx.output.nextDispatch = { action: flow.name, cliArgs: { issue: flow.issueNumber } };
5073
5138
  };
5074
5139
  }
5075
5140
  });
5076
5141
 
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
5142
  // src/stateBranch.ts
5145
5143
  function is404(err) {
5146
5144
  const msg = err instanceof Error ? err.message : String(err);
@@ -5576,10 +5574,10 @@ var init_state2 = __esm({
5576
5574
  "use strict";
5577
5575
  VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "awaiting-merge", "done"]);
5578
5576
  GoalStateError = class extends Error {
5579
- constructor(path43, message) {
5580
- super(`Invalid goal state at ${path43}:
5577
+ constructor(path44, message) {
5578
+ super(`Invalid goal state at ${path44}:
5581
5579
  ${message}`);
5582
- this.path = path43;
5580
+ this.path = path44;
5583
5581
  this.name = "GoalStateError";
5584
5582
  }
5585
5583
  path;
@@ -6460,7 +6458,7 @@ function dispatchTaskRun(issueNumber, base, cwd) {
6460
6458
  "-f",
6461
6459
  `issue_number=${issueNumber}`,
6462
6460
  "-f",
6463
- "executable=classify",
6461
+ "duty=classify",
6464
6462
  "-f",
6465
6463
  `base=${base}`
6466
6464
  ],
@@ -7186,7 +7184,7 @@ var init_dispatch = __esm({
7186
7184
  const usePr = target === "pr" && state?.core.prUrl;
7187
7185
  const targetNumber = usePr ? parsePr(state.core.prUrl) ?? issueNumber : issueNumber;
7188
7186
  ctx.output.nextDispatch = {
7189
- executable: next,
7187
+ action: next,
7190
7188
  cliArgs: usePr ? { pr: targetNumber } : { issue: targetNumber }
7191
7189
  };
7192
7190
  };
@@ -7242,182 +7240,16 @@ ${stateBody}`;
7242
7240
  });
7243
7241
  }
7244
7242
  } catch (err) {
7245
- process.stderr.write(
7246
- `[kody dispatchClassified] failed to post state comment for #${issueNumber}: ${err instanceof Error ? err.message : String(err)}
7247
- `
7248
- );
7249
- }
7250
- const cliArgs = { issue: issueNumber };
7251
- if (base && getProfileInputs(classification)?.some((i) => i.name === "base")) {
7252
- cliArgs.base = base;
7253
- }
7254
- ctx.output.nextDispatch = { executable: classification, cliArgs };
7255
- };
7256
- }
7257
- });
7258
-
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}`;
7267
- }
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;
7275
- }
7276
- var init_jobIdentity = __esm({
7277
- "src/jobIdentity.ts"() {
7278
- "use strict";
7279
- }
7280
- });
7281
-
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}`;
7299
- }
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");
7307
- }
7308
- if (j.flavor !== "instant" && j.flavor !== "scheduled") {
7309
- throw new InvalidJobError(`job.flavor must be "instant" or "scheduled" (got ${String(j.flavor)})`);
7310
- }
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";
7243
+ process.stderr.write(
7244
+ `[kody dispatchClassified] failed to post state comment for #${issueNumber}: ${err instanceof Error ? err.message : String(err)}
7245
+ `
7246
+ );
7247
+ }
7248
+ const cliArgs = { issue: issueNumber };
7249
+ if (base && getProfileInputs(classification)?.some((i) => i.name === "base")) {
7250
+ cliArgs.base = base;
7420
7251
  }
7252
+ ctx.output.nextDispatch = { action: classification, cliArgs };
7421
7253
  };
7422
7254
  }
7423
7255
  });
@@ -7518,9 +7350,13 @@ function isStateUnchanged(prev, next) {
7518
7350
  return JSON.stringify(prev.data) === JSON.stringify(next.data);
7519
7351
  }
7520
7352
  function stateFilePath(jobsDir, slug) {
7521
- return `${jobsDir.replace(/\/+$/, "")}/${slug}.state.json`;
7353
+ return `${jobsDir.replace(/\/+$/, "")}/${slug}/state.json`;
7522
7354
  }
7523
7355
  function slugFromStateFilePath(filePath) {
7356
+ if (/\/state\.json$/i.test(filePath)) {
7357
+ const parts = filePath.split("/");
7358
+ return parts.at(-2) ?? filePath;
7359
+ }
7524
7360
  const last = filePath.split("/").pop() ?? filePath;
7525
7361
  return last.replace(/\.state\.json$/i, "");
7526
7362
  }
@@ -7821,6 +7657,29 @@ var init_jobState = __esm({
7821
7657
  }
7822
7658
  });
7823
7659
 
7660
+ // src/jobIdentity.ts
7661
+ function stableJobKey(job) {
7662
+ const duty = job.duty ?? job.action;
7663
+ const executable = job.executable ?? duty ?? "unknown";
7664
+ if (job.flavor === "scheduled" && job.duty) return `scheduled:${job.duty}:${executable}`;
7665
+ const target = typeof job.target === "number" ? job.target : targetFromCliArgs(job.cliArgs);
7666
+ const work = duty && executable && executable !== duty ? `${duty}:${executable}` : duty ?? executable;
7667
+ return target === void 0 ? `${job.flavor}:${work}` : `${job.flavor}:${work}:${target}`;
7668
+ }
7669
+ function targetFromCliArgs(cliArgs) {
7670
+ if (!cliArgs) return void 0;
7671
+ for (const key of ["issue", "pr", "target", "issue_number"]) {
7672
+ const value = cliArgs[key];
7673
+ if (typeof value === "number" && Number.isFinite(value)) return value;
7674
+ }
7675
+ return void 0;
7676
+ }
7677
+ var init_jobIdentity = __esm({
7678
+ "src/jobIdentity.ts"() {
7679
+ "use strict";
7680
+ }
7681
+ });
7682
+
7824
7683
  // src/scripts/planTaskJobs.ts
7825
7684
  function parseTaskJobSpecs(body) {
7826
7685
  const match = body.match(new RegExp(`<!--\\s*${TASK_JOBS_MARKER}\\s*([\\s\\S]*?)-->`));
@@ -7838,7 +7697,7 @@ function taskJobSpecToJob(spec, issueNumber) {
7838
7697
  const cliArgs = spec.cliArgs ?? { issue: issueNumber };
7839
7698
  const target = typeof spec.target === "number" ? spec.target : targetFromCliArgs(cliArgs) ?? issueNumber;
7840
7699
  return {
7841
- duty: spec.duty,
7700
+ duty: spec.duty ?? spec.executable,
7842
7701
  executable: spec.executable,
7843
7702
  why: spec.reason,
7844
7703
  persona: spec.persona ?? spec.staff,
@@ -7881,7 +7740,7 @@ function jobToPlannedTaskJob(job) {
7881
7740
  return {
7882
7741
  id: stableJobKey(job),
7883
7742
  executable: job.executable ?? job.duty ?? "unknown",
7884
- ...job.duty ? { duty: job.duty } : {},
7743
+ duty: job.duty ?? job.action ?? job.executable ?? "unknown",
7885
7744
  ...job.persona ? { staff: job.persona } : {},
7886
7745
  ...job.flavor ? { flavor: job.flavor } : {},
7887
7746
  ...job.schedule ? { schedule: job.schedule } : {},
@@ -8193,8 +8052,8 @@ var dispatchDutyTicks;
8193
8052
  var init_dispatchDutyTicks = __esm({
8194
8053
  "src/scripts/dispatchDutyTicks.ts"() {
8195
8054
  "use strict";
8196
- init_executor();
8197
8055
  init_issue();
8056
+ init_job();
8198
8057
  dispatchDutyTicks = async (ctx, _profile, args) => {
8199
8058
  ctx.skipAgent = true;
8200
8059
  const label = String(args?.label ?? "");
@@ -8216,13 +8075,14 @@ var init_dispatchDutyTicks = __esm({
8216
8075
  process.stdout.write(`[jobs] \u2192 tick #${issue.number}: ${issue.title}
8217
8076
  `);
8218
8077
  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
- });
8078
+ const out = await runJob(
8079
+ mintScheduledJob({
8080
+ duty: targetExecutable,
8081
+ executable: targetExecutable,
8082
+ cliArgs: { [issueArg]: issue.number }
8083
+ }),
8084
+ { cwd: ctx.cwd, config: ctx.config, verbose: ctx.verbose, quiet: ctx.quiet, chain: false }
8085
+ );
8226
8086
  results.push({ issue: issue.number, exitCode: out.exitCode, reason: out.reason });
8227
8087
  if (out.exitCode !== 0) {
8228
8088
  process.stderr.write(`[jobs] tick #${issue.number} failed (exit ${out.exitCode}): ${out.reason ?? ""}
@@ -8277,8 +8137,8 @@ var init_dispatchNextTask = __esm({
8277
8137
  function taskJobToJob(job, issueArg) {
8278
8138
  const target = typeof job.target === "number" ? job.target : typeof issueArg === "number" ? issueArg : void 0;
8279
8139
  return {
8140
+ duty: job.duty ?? job.executable,
8280
8141
  executable: job.executable,
8281
- ...job.duty ? { duty: job.duty } : {},
8282
8142
  ...job.reason ? { why: job.reason } : {},
8283
8143
  ...job.staff ? { persona: job.staff } : {},
8284
8144
  ...job.schedule ? { schedule: job.schedule } : {},
@@ -8289,7 +8149,7 @@ function taskJobToJob(job, issueArg) {
8289
8149
  function isJob(input) {
8290
8150
  if (!input || typeof input !== "object" || Array.isArray(input)) return false;
8291
8151
  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));
8152
+ 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
8153
  }
8294
8154
  var dispatchNextTaskJob;
8295
8155
  var init_dispatchNextTaskJob = __esm({
@@ -8310,7 +8170,7 @@ var init_dispatchNextTaskJob = __esm({
8310
8170
  const plannedJobs = Array.isArray(ctx.data.plannedTaskJobs) ? ctx.data.plannedTaskJobs.filter(isJob) : [];
8311
8171
  ctx.output.nextJob = plannedJobs.find((job) => stableJobKey(job) === next.id) ?? taskJobToJob(next, ctx.args.issue);
8312
8172
  if (typeof ctx.args.issue === "number") {
8313
- ctx.output.afterNextJob = { executable: profile.name, cliArgs: { issue: ctx.args.issue } };
8173
+ ctx.output.afterNextJob = { action: profile.action ?? profile.name, cliArgs: { issue: ctx.args.issue } };
8314
8174
  }
8315
8175
  };
8316
8176
  }
@@ -8630,6 +8490,7 @@ var init_failOnceTaskJob = __esm({
8630
8490
  ctx.skipAgent = true;
8631
8491
  const issue = typeof ctx.args.issue === "number" ? ctx.args.issue : void 0;
8632
8492
  const fallbackJob = {
8493
+ duty: profile.action ?? profile.name,
8633
8494
  executable: profile.name,
8634
8495
  flavor: "instant",
8635
8496
  ...typeof issue === "number" ? { target: issue, cliArgs: { issue } } : { cliArgs: {} }
@@ -9528,9 +9389,9 @@ function performInit(cwd, force) {
9528
9389
  return { wrote, skipped, labels };
9529
9390
  }
9530
9391
  function renderScheduledWorkflow(name, cron) {
9531
- return `# Scheduled kody executable: ${name}
9392
+ return `# Scheduled kody duty: ${name}
9532
9393
  # Generated by \`kody init\`. Regenerate with \`kody init --force\`.
9533
- # Edit the cron below or the executable's profile.json#schedule.
9394
+ # Edit the cron below or the duty's implementation profile.json#schedule.
9534
9395
 
9535
9396
  name: kody ${name}
9536
9397
 
@@ -12775,7 +12636,7 @@ var init_startFlow = __esm({
12775
12636
  const usePr = target === "pr" && !!state?.core.prUrl;
12776
12637
  const targetNumber = usePr ? parsePrNumber(state.core.prUrl) ?? issueNumber : issueNumber;
12777
12638
  ctx.output.nextDispatch = {
12778
- executable: entry,
12639
+ action: entry,
12779
12640
  cliArgs: usePr ? { pr: targetNumber } : { issue: targetNumber }
12780
12641
  };
12781
12642
  };
@@ -14307,8 +14168,8 @@ async function runExecutableChain(profileName, input) {
14307
14168
  process.stdout.write(`\u2192 kody: in-process job hand-off \u2192 ${label} (hop ${hops}/${MAX_CHAIN_HOPS})
14308
14169
 
14309
14170
  `);
14310
- const { runJob: runJob2 } = await Promise.resolve().then(() => (init_job(), job_exports));
14311
- const childResult = await runJob2(next2, {
14171
+ const { runJob: runJob3 } = await Promise.resolve().then(() => (init_job(), job_exports));
14172
+ const childResult = await runJob3(next2, {
14312
14173
  cwd: input.cwd,
14313
14174
  config: input.config,
14314
14175
  verbose: input.verbose,
@@ -14320,12 +14181,24 @@ async function runExecutableChain(profileName, input) {
14320
14181
  ...chainData,
14321
14182
  ...childResult.taskState ? { taskState: childResult.taskState } : {}
14322
14183
  };
14323
- process.stdout.write(`\u2192 kody: in-process return \u2192 ${after.executable} (hop ${hops}/${MAX_CHAIN_HOPS})
14184
+ const afterJob = handoffToJob(after);
14185
+ if (!afterJob) {
14186
+ return {
14187
+ exitCode: 99,
14188
+ reason: `in-process return missing duty/action for ${after.executable ?? "unknown"}`
14189
+ };
14190
+ }
14191
+ process.stdout.write(
14192
+ `\u2192 kody: in-process return \u2192 ${afterJob.action ?? afterJob.duty} (hop ${hops}/${MAX_CHAIN_HOPS})
14324
14193
 
14325
- `);
14326
- result = await runExecutable(after.executable, {
14327
- ...input,
14328
- cliArgs: after.cliArgs,
14194
+ `
14195
+ );
14196
+ const { runJob: runJob4 } = await Promise.resolve().then(() => (init_job(), job_exports));
14197
+ result = await runJob4(afterJob, {
14198
+ cwd: input.cwd,
14199
+ config: input.config,
14200
+ verbose: input.verbose,
14201
+ quiet: input.quiet,
14329
14202
  preloadedData: chainData
14330
14203
  });
14331
14204
  chainData = {
@@ -14342,10 +14215,26 @@ async function runExecutableChain(profileName, input) {
14342
14215
  continue;
14343
14216
  }
14344
14217
  const next = result.nextDispatch;
14345
- process.stdout.write(`\u2192 kody: in-process hand-off \u2192 ${next.executable} (hop ${hops}/${MAX_CHAIN_HOPS})
14218
+ const nextJob = handoffToJob(next);
14219
+ if (!nextJob) {
14220
+ return {
14221
+ exitCode: 99,
14222
+ reason: `in-process hand-off missing duty/action for ${next.executable ?? "unknown"}`
14223
+ };
14224
+ }
14225
+ process.stdout.write(
14226
+ `\u2192 kody: in-process hand-off \u2192 ${nextJob.action ?? nextJob.duty} (hop ${hops}/${MAX_CHAIN_HOPS})
14346
14227
 
14347
- `);
14348
- result = await runExecutable(next.executable, { ...input, cliArgs: next.cliArgs, preloadedData: chainData });
14228
+ `
14229
+ );
14230
+ const { runJob: runJob2 } = await Promise.resolve().then(() => (init_job(), job_exports));
14231
+ result = await runJob2(nextJob, {
14232
+ cwd: input.cwd,
14233
+ config: input.config,
14234
+ verbose: input.verbose,
14235
+ quiet: input.quiet,
14236
+ preloadedData: chainData
14237
+ });
14349
14238
  chainData = {
14350
14239
  ...chainData,
14351
14240
  ...result.taskState ? { taskState: result.taskState } : {}
@@ -14358,6 +14247,17 @@ async function runExecutableChain(profileName, input) {
14358
14247
  }
14359
14248
  return result;
14360
14249
  }
14250
+ function handoffToJob(handoff) {
14251
+ const dutyOrAction = handoff.action ?? handoff.duty;
14252
+ if (!dutyOrAction) return null;
14253
+ return {
14254
+ action: handoff.action ?? handoff.duty,
14255
+ duty: handoff.duty,
14256
+ executable: handoff.executable,
14257
+ cliArgs: handoff.cliArgs,
14258
+ flavor: "instant"
14259
+ };
14260
+ }
14361
14261
  function clearStampedLifecycleLabels(profile, ctx) {
14362
14262
  const target = ctx.args.issue ?? ctx.args.pr;
14363
14263
  if (typeof target !== "number" || !Number.isFinite(target)) return;
@@ -14630,6 +14530,156 @@ var init_executor = __esm({
14630
14530
  }
14631
14531
  });
14632
14532
 
14533
+ // src/job.ts
14534
+ var job_exports = {};
14535
+ __export(job_exports, {
14536
+ DEFAULT_INSTANT_PERSONA: () => DEFAULT_INSTANT_PERSONA,
14537
+ InvalidJobError: () => InvalidJobError,
14538
+ mintInstantJob: () => mintInstantJob,
14539
+ mintScheduledJob: () => mintScheduledJob,
14540
+ newJobId: () => newJobId,
14541
+ runJob: () => runJob,
14542
+ stableJobKey: () => stableJobKey,
14543
+ validateJob: () => validateJob
14544
+ });
14545
+ import * as path38 from "path";
14546
+ function newJobId(flavor) {
14547
+ localJobSeq += 1;
14548
+ const runId = process.env.GITHUB_RUN_ID;
14549
+ if (runId) return `gh-${runId}-${process.env.GITHUB_RUN_ATTEMPT ?? "1"}-${localJobSeq}`;
14550
+ return `${flavor}-${Date.now()}-${localJobSeq}`;
14551
+ }
14552
+ function validateJob(input) {
14553
+ if (!input || typeof input !== "object") {
14554
+ throw new InvalidJobError("job must be an object");
14555
+ }
14556
+ const j = input;
14557
+ if (typeof j.duty !== "string" && typeof j.action !== "string") {
14558
+ throw new InvalidJobError("job must reference a duty action or duty");
14559
+ }
14560
+ if (j.flavor !== "instant" && j.flavor !== "scheduled") {
14561
+ throw new InvalidJobError(`job.flavor must be "instant" or "scheduled" (got ${String(j.flavor)})`);
14562
+ }
14563
+ if (j.cliArgs !== void 0 && (typeof j.cliArgs !== "object" || j.cliArgs === null)) {
14564
+ throw new InvalidJobError("job.cliArgs must be an object when present");
14565
+ }
14566
+ return {
14567
+ action: typeof j.action === "string" ? j.action : void 0,
14568
+ executable: typeof j.executable === "string" ? j.executable : void 0,
14569
+ duty: typeof j.duty === "string" ? j.duty : void 0,
14570
+ why: typeof j.why === "string" ? j.why : void 0,
14571
+ persona: typeof j.persona === "string" ? j.persona : void 0,
14572
+ schedule: typeof j.schedule === "string" ? j.schedule : void 0,
14573
+ target: typeof j.target === "number" ? j.target : void 0,
14574
+ cliArgs: j.cliArgs ?? {},
14575
+ flavor: j.flavor,
14576
+ force: j.force === true
14577
+ };
14578
+ }
14579
+ async function runJob(job, base) {
14580
+ const valid = validateJob(job);
14581
+ const action = valid.action ?? valid.duty;
14582
+ const projectDutiesRoot = path38.join(base.cwd, ".kody", "duties");
14583
+ const resolvedDuty = action ? resolveDutyAction(action, projectDutiesRoot) : null;
14584
+ const dutyIdentity = valid.duty ?? resolvedDuty?.duty;
14585
+ const dutyContext = loadDutyContext(dutyIdentity, base.cwd);
14586
+ if (!resolvedDuty && !dutyContext) {
14587
+ throw new InvalidJobError(`job duty not found: ${action ?? valid.duty ?? "<none>"}`);
14588
+ }
14589
+ const dutySelectedExecutable = resolvedDuty?.executable ?? dutyContext?.config.executable ?? dutyContext?.config.executables?.[0] ?? (dutyContext?.config.tickScript ? "duty-tick-scripted" : void 0);
14590
+ const profileName = valid.executable ?? dutySelectedExecutable;
14591
+ if (!profileName) {
14592
+ throw new InvalidJobError(`job duty resolves to no executable: ${dutyIdentity ?? action}`);
14593
+ }
14594
+ const preloadedData = { ...base.preloadedData ?? {} };
14595
+ preloadedData.jobId = newJobId(valid.flavor);
14596
+ preloadedData.jobKey = stableJobKey(valid);
14597
+ preloadedData.jobFlavor = valid.flavor;
14598
+ if (valid.target !== void 0) preloadedData.jobTarget = valid.target;
14599
+ if (valid.action !== void 0 && valid.action.length > 0) preloadedData.jobAction = valid.action;
14600
+ if (dutyIdentity !== void 0 && dutyIdentity.length > 0) preloadedData.jobDuty = dutyIdentity;
14601
+ const executableIdentity = profileName;
14602
+ if (executableIdentity !== void 0 && executableIdentity.length > 0)
14603
+ preloadedData.jobExecutable = executableIdentity;
14604
+ if (valid.schedule !== void 0 && valid.schedule.length > 0) preloadedData.jobSchedule = valid.schedule;
14605
+ if (dutyContext) {
14606
+ preloadedData.dutySlug = dutyContext.slug;
14607
+ preloadedData.dutyTitle = dutyContext.title;
14608
+ preloadedData.dutyIntent = dutyContext.body;
14609
+ preloadedData.jobIntent = dutyContext.body;
14610
+ if (preloadedData.jobDuty === void 0) preloadedData.jobDuty = dutyContext.slug;
14611
+ if (dutyContext.config.staff && preloadedData.jobPersona === void 0) {
14612
+ preloadedData.jobPersona = dutyContext.config.staff;
14613
+ }
14614
+ if (dutyContext.config.every && preloadedData.jobSchedule === void 0) {
14615
+ preloadedData.jobSchedule = dutyContext.config.every;
14616
+ }
14617
+ if (dutyContext.config.mentions && dutyContext.config.mentions.length > 0) {
14618
+ preloadedData.mentions = dutyContext.config.mentions.map((login) => `@${login}`).join(" ");
14619
+ }
14620
+ }
14621
+ if (valid.why !== void 0 && valid.why.length > 0) preloadedData.jobWhy = valid.why;
14622
+ if (valid.persona !== void 0) preloadedData.jobPersona = valid.persona;
14623
+ const input = {
14624
+ cliArgs: { ...valid.cliArgs },
14625
+ cwd: base.cwd,
14626
+ config: base.config,
14627
+ skipConfig: base.skipConfig,
14628
+ verbose: base.verbose,
14629
+ quiet: base.quiet,
14630
+ preloadedData: Object.keys(preloadedData).length > 0 ? preloadedData : void 0
14631
+ };
14632
+ input.cliArgs = resolvedDuty && profileName === resolvedDuty.executable ? { ...resolvedDuty.cliArgs, ...input.cliArgs } : input.cliArgs;
14633
+ const run = base.chain === false ? runExecutable : runExecutableChain;
14634
+ return run(profileName, input);
14635
+ }
14636
+ function loadDutyContext(slug, cwd) {
14637
+ if (!slug) return null;
14638
+ return readDutyFolder(path38.join(cwd, ".kody", "duties"), slug) ?? readDutyFolder(getProjectDutiesRoot(), slug) ?? readDutyFolder(getBuiltinDutiesRoot(), slug);
14639
+ }
14640
+ function mintInstantJob(dispatch2, opts) {
14641
+ return {
14642
+ action: dispatch2.action,
14643
+ executable: dispatch2.executable,
14644
+ duty: dispatch2.duty,
14645
+ why: opts?.why ?? dispatch2.why,
14646
+ persona: opts?.persona ?? DEFAULT_INSTANT_PERSONA,
14647
+ target: dispatch2.target,
14648
+ cliArgs: dispatch2.cliArgs,
14649
+ flavor: "instant"
14650
+ };
14651
+ }
14652
+ function mintScheduledJob(input) {
14653
+ return {
14654
+ action: input.action,
14655
+ duty: input.duty,
14656
+ executable: input.executable,
14657
+ schedule: input.schedule,
14658
+ persona: input.persona,
14659
+ cliArgs: input.cliArgs ?? {},
14660
+ flavor: "scheduled"
14661
+ };
14662
+ }
14663
+ var DEFAULT_INSTANT_PERSONA, localJobSeq, InvalidJobError;
14664
+ var init_job = __esm({
14665
+ "src/job.ts"() {
14666
+ "use strict";
14667
+ init_dutyFolders();
14668
+ init_executor();
14669
+ init_registry();
14670
+ init_jobIdentity();
14671
+ init_jobIdentity();
14672
+ DEFAULT_INSTANT_PERSONA = "kody";
14673
+ localJobSeq = 0;
14674
+ InvalidJobError = class extends Error {
14675
+ constructor(message) {
14676
+ super(message);
14677
+ this.name = "InvalidJobError";
14678
+ }
14679
+ };
14680
+ }
14681
+ });
14682
+
14633
14683
  // src/entry.ts
14634
14684
  init_package();
14635
14685
 
@@ -14739,7 +14789,7 @@ function translateOpenAISseToBrain(opts) {
14739
14789
  // src/servers/brain-serve.ts
14740
14790
  import * as fs42 from "fs";
14741
14791
  import { createServer } from "http";
14742
- import * as path40 from "path";
14792
+ import * as path41 from "path";
14743
14793
 
14744
14794
  // src/chat/loop.ts
14745
14795
  init_agent();
@@ -15281,7 +15331,7 @@ init_config();
15281
15331
  // src/kody-cli.ts
15282
15332
  import { execFileSync as execFileSync26 } from "child_process";
15283
15333
  import * as fs40 from "fs";
15284
- import * as path38 from "path";
15334
+ import * as path39 from "path";
15285
15335
 
15286
15336
  // src/app-auth.ts
15287
15337
  import { createSign } from "crypto";
@@ -15435,28 +15485,12 @@ function resolveOperatorAction(action) {
15435
15485
  return resolveDutyAction(action);
15436
15486
  }
15437
15487
  function resolveConfiguredAction(action) {
15438
- const resolved = resolveDutyAction(action);
15439
- if (resolved) return resolved;
15440
- return compatibilityDutyAction(action);
15488
+ return resolveDutyAction(action);
15441
15489
  }
15442
15490
  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
- };
15491
+ const route = resolveConfiguredAction(action);
15492
+ if (!route) throw new Error(`required duty action not found: ${action}`);
15493
+ return route;
15460
15494
  }
15461
15495
  function routeResult(route, cliArgs, target, why) {
15462
15496
  const result = {
@@ -15487,7 +15521,7 @@ function autoDispatch(opts) {
15487
15521
  const inputs2 = objectValue(event.inputs);
15488
15522
  const n = parseInt(String(inputs2?.issue_number ?? ""), 10);
15489
15523
  if (!Number.isNaN(n) && n > 0) {
15490
- const actionName = String(inputs2?.executable ?? "").trim() || "run";
15524
+ const actionName = String(inputs2?.duty ?? inputs2?.executable ?? "").trim() || "run";
15491
15525
  const route2 = resolveConfiguredAction(actionName);
15492
15526
  if (!route2) return null;
15493
15527
  const base = String(inputs2?.base ?? "").trim();
@@ -15628,7 +15662,7 @@ function autoDispatchTyped(opts) {
15628
15662
  if (!tokenRaw || POLITE_WORDS.has(tokenRaw)) {
15629
15663
  return {
15630
15664
  kind: "silent",
15631
- reason: tokenRaw ? `polite-word lead-in '${tokenRaw}', no default executable configured` : "no subcommand token, no default executable configured"
15665
+ reason: tokenRaw ? `polite-word lead-in '${tokenRaw}', no default duty action configured` : "no subcommand token, no default duty action configured"
15632
15666
  };
15633
15667
  }
15634
15668
  const available = listDutyActions().map((e) => e.action).filter((n) => !n.startsWith("goal-") && !n.startsWith("job-")).sort();
@@ -15668,7 +15702,15 @@ function dispatchScheduledWatches(opts) {
15668
15702
  continue;
15669
15703
  }
15670
15704
  }
15671
- out.push({ action: exe.name, duty: exe.name, executable: exe.name, cliArgs: {}, target: 0 });
15705
+ const route = resolveConfiguredAction(exe.name);
15706
+ if (!route) {
15707
+ process.stderr.write(
15708
+ `[kody] dispatchScheduledWatches: '${exe.name}' is scheduled but has no duty action; skipping
15709
+ `
15710
+ );
15711
+ continue;
15712
+ }
15713
+ out.push({ ...route, cliArgs: route.cliArgs, target: 0 });
15672
15714
  }
15673
15715
  return out;
15674
15716
  }
@@ -15770,7 +15812,6 @@ function coerceBare(spec, value) {
15770
15812
  }
15771
15813
 
15772
15814
  // src/kody-cli.ts
15773
- init_executor();
15774
15815
  init_gha();
15775
15816
  init_issue();
15776
15817
  init_job();
@@ -15885,9 +15926,9 @@ async function resolveAuthToken(env = process.env) {
15885
15926
  return void 0;
15886
15927
  }
15887
15928
  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";
15929
+ if (fs40.existsSync(path39.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
15930
+ if (fs40.existsSync(path39.join(cwd, "yarn.lock"))) return "yarn";
15931
+ if (fs40.existsSync(path39.join(cwd, "bun.lockb"))) return "bun";
15891
15932
  return "npm";
15892
15933
  }
15893
15934
  function shellOut(cmd, args, cwd, stream = true) {
@@ -15974,7 +16015,7 @@ function configureGitIdentity(cwd) {
15974
16015
  }
15975
16016
  function postFailureTail(issueNumber, cwd, reason) {
15976
16017
  if (!issueNumber) return;
15977
- const logPath = path38.join(cwd, ".kody", "last-run.jsonl");
16018
+ const logPath = path39.join(cwd, ".kody", "last-run.jsonl");
15978
16019
  let tail = "";
15979
16020
  try {
15980
16021
  if (fs40.existsSync(logPath)) {
@@ -16003,7 +16044,7 @@ async function runCi(argv) {
16003
16044
  return 0;
16004
16045
  }
16005
16046
  const args = parseCiArgs(argv);
16006
- const cwd = args.cwd ? path38.resolve(args.cwd) : process.cwd();
16047
+ const cwd = args.cwd ? path39.resolve(args.cwd) : process.cwd();
16007
16048
  let earlyConfig;
16008
16049
  let earlyConfigError;
16009
16050
  try {
@@ -16021,9 +16062,9 @@ async function runCi(argv) {
16021
16062
  const evt = JSON.parse(fs40.readFileSync(dispatchEventPath, "utf-8"));
16022
16063
  const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
16023
16064
  const sessionInput = String(evt?.inputs?.sessionId ?? "");
16024
- const exeInput = String(evt?.inputs?.executable ?? "").trim();
16065
+ const dutyInput = String(evt?.inputs?.duty ?? evt?.inputs?.executable ?? "").trim();
16025
16066
  const noTarget = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
16026
- if (noTarget && exeInput) forceRunAction = exeInput;
16067
+ if (noTarget && dutyInput) forceRunAction = dutyInput;
16027
16068
  else manualWorkflowDispatch = noTarget;
16028
16069
  } catch {
16029
16070
  manualWorkflowDispatch = false;
@@ -16123,7 +16164,7 @@ async function runCi(argv) {
16123
16164
  );
16124
16165
  return 0;
16125
16166
  }
16126
- if (outcome.kind === "silent" && earlyConfigError && outcome.reason.includes("no default executable configured")) {
16167
+ if (outcome.kind === "silent" && earlyConfigError && outcome.reason.includes("no default duty action configured")) {
16127
16168
  process.stderr.write(`[kody] config error: ${earlyConfigError.message}
16128
16169
  `);
16129
16170
  return 64;
@@ -16143,11 +16184,14 @@ async function runCi(argv) {
16143
16184
  ${CI_HELP}`);
16144
16185
  return 64;
16145
16186
  }
16187
+ const runRoute = args.issueNumber ? resolveDutyAction("run") : null;
16188
+ if (!autoFallback && args.issueNumber && !runRoute) {
16189
+ process.stderr.write("[kody] required duty action 'run' not found\n");
16190
+ return 64;
16191
+ }
16146
16192
  const dispatch2 = autoFallback ?? {
16147
- action: "run",
16148
- duty: "run",
16149
- executable: "run",
16150
- cliArgs: { issue: args.issueNumber },
16193
+ ...runRoute,
16194
+ cliArgs: { ...runRoute.cliArgs, issue: args.issueNumber },
16151
16195
  target: args.issueNumber
16152
16196
  };
16153
16197
  const issueNumber = dispatch2.target;
@@ -16229,8 +16273,8 @@ async function runScheduledFanOut(cwd, args, opts) {
16229
16273
  );
16230
16274
  return 0;
16231
16275
  }
16232
- const names = matches.map((m) => m.executable).join(", ");
16233
- process.stdout.write(`\u2192 kody: scheduled wake \u2014 firing ${matches.length} watch(es): ${names}
16276
+ const names = matches.map((m) => `${m.duty}\u2192${m.executable}`).join(", ");
16277
+ process.stdout.write(`\u2192 kody: scheduled wake \u2014 firing ${matches.length} watch dut(y/ies): ${names}
16234
16278
  `);
16235
16279
  try {
16236
16280
  const n = unpackAllSecrets();
@@ -16267,19 +16311,21 @@ async function runScheduledFanOut(cwd, args, opts) {
16267
16311
  const serial = process.env.KODY_SERIAL_WATCHES === "1";
16268
16312
  const runWatch = async (match) => {
16269
16313
  process.stdout.write(`
16270
- \u2192 kody: running watch \`${match.executable}\`
16314
+ \u2192 kody: running watch duty \`${match.duty}\` (${match.executable})
16271
16315
  `);
16272
16316
  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
- });
16317
+ const result = await runJob(
16318
+ mintScheduledJob({
16319
+ action: match.action,
16320
+ duty: match.duty,
16321
+ executable: match.executable,
16322
+ cliArgs: match.cliArgs
16323
+ }),
16324
+ { cwd, config, verbose: args.verbose, quiet: args.quiet, chain: false }
16325
+ );
16280
16326
  if (result.exitCode !== 0) {
16281
16327
  process.stderr.write(
16282
- `[kody] watch \`${match.executable}\` exited ${result.exitCode}: ${result.reason ?? "(no reason)"}
16328
+ `[kody] watch duty \`${match.duty}\` exited ${result.exitCode}: ${result.reason ?? "(no reason)"}
16283
16329
  `
16284
16330
  );
16285
16331
  return result.exitCode;
@@ -16287,7 +16333,7 @@ async function runScheduledFanOut(cwd, args, opts) {
16287
16333
  return 0;
16288
16334
  } catch (err) {
16289
16335
  const msg = err instanceof Error ? err.message : String(err);
16290
- process.stderr.write(`[kody] watch \`${match.executable}\` crashed: ${msg}
16336
+ process.stderr.write(`[kody] watch duty \`${match.duty}\` crashed: ${msg}
16291
16337
  `);
16292
16338
  return 99;
16293
16339
  }
@@ -16314,10 +16360,10 @@ init_repoWorkspace();
16314
16360
 
16315
16361
  // src/scripts/brainTurnLog.ts
16316
16362
  import * as fs41 from "fs";
16317
- import * as path39 from "path";
16363
+ import * as path40 from "path";
16318
16364
  var live = /* @__PURE__ */ new Map();
16319
16365
  function eventsPath(dir, chatId) {
16320
- return path39.join(dir, ".kody", "brain-events", `${chatId}.jsonl`);
16366
+ return path40.join(dir, ".kody", "brain-events", `${chatId}.jsonl`);
16321
16367
  }
16322
16368
  function lastPersistedSeq(dir, chatId) {
16323
16369
  const p = eventsPath(dir, chatId);
@@ -16360,7 +16406,7 @@ function beginTurn(dir, chatId) {
16360
16406
  };
16361
16407
  live.set(chatId, state);
16362
16408
  const p = eventsPath(dir, chatId);
16363
- fs41.mkdirSync(path39.dirname(p), { recursive: true });
16409
+ fs41.mkdirSync(path40.dirname(p), { recursive: true });
16364
16410
  return (event) => {
16365
16411
  state.seq += 1;
16366
16412
  const rec = { seq: state.seq, turn, ts: Date.now(), event };
@@ -16635,7 +16681,7 @@ async function handleChatTurn(req, res, chatId, opts) {
16635
16681
  const repo = strField(body, "repo");
16636
16682
  const repoToken = strField(body, "repoToken");
16637
16683
  const sessionFile = sessionFilePath(opts.cwd, chatId);
16638
- fs42.mkdirSync(path40.dirname(sessionFile), { recursive: true });
16684
+ fs42.mkdirSync(path41.dirname(sessionFile), { recursive: true });
16639
16685
  appendTurn(sessionFile, {
16640
16686
  role: "user",
16641
16687
  content: message,
@@ -16682,7 +16728,7 @@ async function handleChatTurn(req, res, chatId, opts) {
16682
16728
  function buildServer(opts) {
16683
16729
  const runTurn = opts.runTurn ?? runChatTurn;
16684
16730
  const cloneRepo = opts.cloneRepo ?? defaultCloneRepo;
16685
- const reposRoot = opts.reposRoot ?? path40.join(path40.dirname(path40.resolve(opts.cwd)), "repos");
16731
+ const reposRoot = opts.reposRoot ?? path41.join(path41.dirname(path41.resolve(opts.cwd)), "repos");
16686
16732
  return createServer(async (req, res) => {
16687
16733
  if (!req.method || !req.url) {
16688
16734
  sendJson(res, 400, { error: "bad request" });
@@ -17276,13 +17322,13 @@ async function loadConfigSafe() {
17276
17322
  // src/chat-cli.ts
17277
17323
  import { execFileSync as execFileSync29 } from "child_process";
17278
17324
  import * as fs44 from "fs";
17279
- import * as path42 from "path";
17325
+ import * as path43 from "path";
17280
17326
 
17281
17327
  // src/chat/modes/interactive.ts
17282
17328
  init_issue();
17283
17329
  import { execFileSync as execFileSync28 } from "child_process";
17284
17330
  import * as fs43 from "fs";
17285
- import * as path41 from "path";
17331
+ import * as path42 from "path";
17286
17332
 
17287
17333
  // src/chat/inbox.ts
17288
17334
  import { execFileSync as execFileSync27 } from "child_process";
@@ -17441,9 +17487,9 @@ function findNextUserTurn(turns, fromIdx) {
17441
17487
  return -1;
17442
17488
  }
17443
17489
  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)));
17490
+ const sessionRel = path42.relative(cwd, sessionFilePath(cwd, sessionId));
17491
+ const eventsRel = path42.relative(cwd, eventsFilePath(cwd, sessionId));
17492
+ const rels = [sessionRel, eventsRel].filter((p) => fs43.existsSync(path42.join(cwd, p)));
17447
17493
  if (rels.length === 0) return;
17448
17494
  const repository = process.env.GITHUB_REPOSITORY;
17449
17495
  if (!repository) {
@@ -17455,8 +17501,8 @@ function commitTurn(cwd, sessionId, _verbose) {
17455
17501
  }
17456
17502
  const branch = defaultBranch(cwd) ?? "main";
17457
17503
  for (const rel of rels) {
17458
- const repoPath = rel.split(path41.sep).join("/");
17459
- const localText = fs43.readFileSync(path41.join(cwd, rel), "utf-8");
17504
+ const repoPath = rel.split(path42.sep).join("/");
17505
+ const localText = fs43.readFileSync(path42.join(cwd, rel), "utf-8");
17460
17506
  putJsonlViaContents(repository, branch, repoPath, localText, sessionId, cwd);
17461
17507
  }
17462
17508
  }
@@ -17593,12 +17639,12 @@ function parseChatArgs(argv, env = process.env) {
17593
17639
  return result;
17594
17640
  }
17595
17641
  function commitChatFiles(cwd, sessionId, verbose) {
17596
- const sessionFile = path42.relative(cwd, sessionFilePath(cwd, sessionId));
17597
- const eventsFile = path42.relative(cwd, eventsFilePath(cwd, sessionId));
17642
+ const sessionFile = path43.relative(cwd, sessionFilePath(cwd, sessionId));
17643
+ const eventsFile = path43.relative(cwd, eventsFilePath(cwd, sessionId));
17598
17644
  const safeSession = sessionId.replace(/[^a-zA-Z0-9._-]/g, "_");
17599
- const tasksDir = path42.join(".kody", "tasks", safeSession);
17645
+ const tasksDir = path43.join(".kody", "tasks", safeSession);
17600
17646
  const candidatePaths = [sessionFile, eventsFile, tasksDir];
17601
- const paths = candidatePaths.filter((p) => fs44.existsSync(path42.join(cwd, p)));
17647
+ const paths = candidatePaths.filter((p) => fs44.existsSync(path43.join(cwd, p)));
17602
17648
  if (paths.length === 0) return;
17603
17649
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
17604
17650
  try {
@@ -17642,7 +17688,7 @@ async function runChat(argv) {
17642
17688
  ${CHAT_HELP}`);
17643
17689
  return 64;
17644
17690
  }
17645
- const cwd = args.cwd ? path42.resolve(args.cwd) : process.cwd();
17691
+ const cwd = args.cwd ? path43.resolve(args.cwd) : process.cwd();
17646
17692
  const sessionId = args.sessionId;
17647
17693
  const unpackedSecrets = unpackAllSecrets();
17648
17694
  if (unpackedSecrets > 0) {
@@ -17733,7 +17779,6 @@ ${CHAT_HELP}`);
17733
17779
 
17734
17780
  // src/entry.ts
17735
17781
  init_config();
17736
- init_executor();
17737
17782
  init_job();
17738
17783
  init_registry();
17739
17784
 
@@ -17851,8 +17896,8 @@ var FlyClient = class {
17851
17896
  get fetch() {
17852
17897
  return this.opts.fetchImpl ?? fetch;
17853
17898
  }
17854
- async call(path43, init = {}) {
17855
- const res = await this.fetch(`${FLY_API_BASE}${path43}`, {
17899
+ async call(path44, init = {}) {
17900
+ const res = await this.fetch(`${FLY_API_BASE}${path44}`, {
17856
17901
  method: init.method ?? "GET",
17857
17902
  headers: {
17858
17903
  Authorization: `Bearer ${this.opts.token}`,
@@ -17863,7 +17908,7 @@ var FlyClient = class {
17863
17908
  if (res.status === 404 && init.allow404) return null;
17864
17909
  if (!res.ok) {
17865
17910
  const text = await res.text().catch(() => "");
17866
- throw new Error(`Fly API ${res.status} on ${path43}: ${text.slice(0, 200) || res.statusText}`);
17911
+ throw new Error(`Fly API ${res.status} on ${path44}: ${text.slice(0, 200) || res.statusText}`);
17867
17912
  }
17868
17913
  if (res.status === 204) return null;
17869
17914
  const raw = await res.text();
@@ -19124,7 +19169,6 @@ Usage:
19124
19169
  kody-engine release --issue <N> [--cwd <path>] [--verbose|--quiet]
19125
19170
  kody-engine init [--cwd <path>] [--verbose|--quiet]
19126
19171
  kody-engine <action> [--cwd <path>] [--verbose|--quiet]
19127
- kody-engine exec <executable> [--cwd <path>] [--verbose|--quiet]
19128
19172
  kody-engine ci [preflight flags \u2014 see: kody-engine ci --help]
19129
19173
  kody-engine chat [chat flags \u2014 see: kody-engine chat --help]
19130
19174
  kody-engine stats [--since 7d|--run <id>|--json|--cwd <path>]
@@ -19132,8 +19176,7 @@ Usage:
19132
19176
  kody-engine version
19133
19177
 
19134
19178
  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.
19179
+ and selects an implementation executable.
19137
19180
 
19138
19181
  Exit codes:
19139
19182
  0 success (PR opened, verify passed \u2014 or resolve produced a merge commit)
@@ -19174,24 +19217,6 @@ function parseArgs(argv) {
19174
19217
  result.serverArgs = argv.slice(1).filter((a) => !a.startsWith("-"));
19175
19218
  return result;
19176
19219
  }
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
19220
  if (hasDutyAction(cmd)) {
19196
19221
  result.command = "__duty__";
19197
19222
  result.actionName = cmd;
@@ -19201,18 +19226,8 @@ function parseArgs(argv) {
19201
19226
  if (result.cliArgs.quiet === true) result.quiet = true;
19202
19227
  return result;
19203
19228
  }
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
19229
  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];
19230
+ const available = ["ci", "chat", "stats", "help", "version", ...discoveredActions];
19216
19231
  result.errors.push(`unknown command: ${cmd} (available: ${available.join(", ")})`);
19217
19232
  return result;
19218
19233
  }
@@ -19310,7 +19325,7 @@ ${HELP_TEXT}`);
19310
19325
  return 64;
19311
19326
  }
19312
19327
  const cliArgs = { ...route.cliArgs, ...args.cliArgs ?? {} };
19313
- const skipConfig2 = configlessCommands.has(route.executable);
19328
+ const skipConfig = configlessCommands.has(route.executable);
19314
19329
  try {
19315
19330
  const result = await runJob(
19316
19331
  {
@@ -19323,7 +19338,7 @@ ${HELP_TEXT}`);
19323
19338
  },
19324
19339
  {
19325
19340
  cwd,
19326
- skipConfig: skipConfig2,
19341
+ skipConfig,
19327
19342
  verbose: args.verbose,
19328
19343
  quiet: args.quiet
19329
19344
  }
@@ -19344,30 +19359,8 @@ ${HELP_TEXT}`);
19344
19359
  return 99;
19345
19360
  }
19346
19361
  }
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
- }
19362
+ process.stderr.write("error: command did not resolve to a duty\n");
19363
+ return 64;
19371
19364
  }
19372
19365
  function numericTarget(cliArgs) {
19373
19366
  for (const key of ["issue", "pr"]) {