@kody-ade/kody-engine 0.4.217 → 0.4.219

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.217",
18
+ version: "0.4.219",
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",
@@ -1981,6 +1981,110 @@ var init_agent = __esm({
1981
1981
  }
1982
1982
  });
1983
1983
 
1984
+ // src/scripts/jobFrontmatter.ts
1985
+ function splitFrontmatter(raw) {
1986
+ const match = FRONTMATTER_RE.exec(raw);
1987
+ if (!match) return { frontmatter: {}, body: raw };
1988
+ const inner = match[1] ?? "";
1989
+ const body = raw.slice(match[0].length);
1990
+ return { frontmatter: parseFlatYaml(inner), body };
1991
+ }
1992
+ function isScheduleEvery(value) {
1993
+ return typeof value === "string" && SCHEDULE_EVERY_VALUES.includes(value);
1994
+ }
1995
+ function scheduleEveryToMs(every) {
1996
+ const MIN = 60 * 1e3;
1997
+ const HOUR = 60 * MIN;
1998
+ const DAY = 24 * HOUR;
1999
+ switch (every) {
2000
+ case "15m":
2001
+ return 15 * MIN;
2002
+ case "30m":
2003
+ return 30 * MIN;
2004
+ case "1h":
2005
+ return HOUR;
2006
+ case "2h":
2007
+ return 2 * HOUR;
2008
+ case "6h":
2009
+ return 6 * HOUR;
2010
+ case "12h":
2011
+ return 12 * HOUR;
2012
+ case "1d":
2013
+ return DAY;
2014
+ case "3d":
2015
+ return 3 * DAY;
2016
+ case "7d":
2017
+ return 7 * DAY;
2018
+ case "manual":
2019
+ return Number.POSITIVE_INFINITY;
2020
+ }
2021
+ }
2022
+ function parseFlatYaml(text) {
2023
+ const out = {};
2024
+ for (const rawLine of text.split(/\r?\n/)) {
2025
+ const line = rawLine.trim();
2026
+ if (!line || line.startsWith("#")) continue;
2027
+ const colon = line.indexOf(":");
2028
+ if (colon < 0) continue;
2029
+ const key = line.slice(0, colon).trim();
2030
+ const value = stripQuotes(line.slice(colon + 1).trim());
2031
+ if (key === "action" && value.length > 0) {
2032
+ out.action = value;
2033
+ } else if (key === "executable" && value.length > 0) {
2034
+ out.executable = value;
2035
+ } else if (key === "every" && isScheduleEvery(value)) {
2036
+ out.every = value;
2037
+ } else if (key === "tickScript" && value.length > 0) {
2038
+ out.tickScript = value;
2039
+ } else if (key === "disabled") {
2040
+ const lower = value.toLowerCase();
2041
+ if (lower === "true") out.disabled = true;
2042
+ else if (lower === "false") out.disabled = false;
2043
+ } else if (key === "staff" && value.length > 0) {
2044
+ out.staff = value;
2045
+ } else if (key === "mentions") {
2046
+ const logins = value.split(",").map((s) => s.trim().replace(/^@/, "")).filter(Boolean);
2047
+ if (logins.length > 0) out.mentions = logins;
2048
+ } else if (key === "tools") {
2049
+ const names = value.split(",").map((s) => s.trim()).filter(Boolean);
2050
+ if (names.length > 0) out.tools = names;
2051
+ } else if (key === "executables") {
2052
+ const names = value.split(",").map((s) => s.trim()).filter(Boolean);
2053
+ if (names.length > 0) out.executables = names;
2054
+ }
2055
+ }
2056
+ return out;
2057
+ }
2058
+ function stripQuotes(value) {
2059
+ if (value.length >= 2) {
2060
+ const first = value[0];
2061
+ const last = value[value.length - 1];
2062
+ if (first === '"' && last === '"' || first === "'" && last === "'") {
2063
+ return value.slice(1, -1);
2064
+ }
2065
+ }
2066
+ return value;
2067
+ }
2068
+ var SCHEDULE_EVERY_VALUES, FRONTMATTER_RE;
2069
+ var init_jobFrontmatter = __esm({
2070
+ "src/scripts/jobFrontmatter.ts"() {
2071
+ "use strict";
2072
+ SCHEDULE_EVERY_VALUES = [
2073
+ "15m",
2074
+ "30m",
2075
+ "1h",
2076
+ "2h",
2077
+ "6h",
2078
+ "12h",
2079
+ "1d",
2080
+ "3d",
2081
+ "7d",
2082
+ "manual"
2083
+ ];
2084
+ FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
2085
+ }
2086
+ });
2087
+
1984
2088
  // src/registry.ts
1985
2089
  import * as fs6 from "fs";
1986
2090
  import * as path6 from "path";
@@ -2020,6 +2124,21 @@ function getBuiltinJobsRoot() {
2020
2124
  }
2021
2125
  return candidates[0];
2022
2126
  }
2127
+ function getBuiltinDutiesRoot() {
2128
+ const here = path6.dirname(new URL(import.meta.url).pathname);
2129
+ const candidates = [
2130
+ path6.join(here, "duties"),
2131
+ // dev: src/
2132
+ path6.join(here, "..", "duties"),
2133
+ // built: dist/bin → dist/duties
2134
+ path6.join(here, "..", "src", "duties")
2135
+ // fallback
2136
+ ];
2137
+ for (const c of candidates) {
2138
+ if (fs6.existsSync(c) && fs6.statSync(c).isDirectory()) return c;
2139
+ }
2140
+ return candidates[0];
2141
+ }
2023
2142
  function listBuiltinJobs(root = getBuiltinJobsRoot()) {
2024
2143
  if (!fs6.existsSync(root) || !fs6.statSync(root).isDirectory()) return [];
2025
2144
  const out = [];
@@ -2104,9 +2223,128 @@ function resolveExecutable(name, roots = getExecutableRoots()) {
2104
2223
  function hasExecutable(name, roots = getExecutableRoots()) {
2105
2224
  return resolveExecutable(name, roots) !== null;
2106
2225
  }
2226
+ function listDutyActions(projectDutiesRoot = getProjectDutiesRoot()) {
2227
+ const seen = /* @__PURE__ */ new Set();
2228
+ const out = [];
2229
+ const add = (action) => {
2230
+ if (!isSafeName(action.action) || !isSafeName(action.duty) || !isSafeName(action.executable)) return;
2231
+ if (seen.has(action.action)) return;
2232
+ seen.add(action.action);
2233
+ out.push(action);
2234
+ };
2235
+ for (const action of listProjectFolderDutyActions(projectDutiesRoot)) add(action);
2236
+ for (const action of listProjectMarkdownDutyActions(projectDutiesRoot)) add(action);
2237
+ for (const action of listBuiltinDutyActions()) add(action);
2238
+ return out.sort((a, b) => a.action.localeCompare(b.action));
2239
+ }
2240
+ function resolveDutyAction(action, projectDutiesRoot = getProjectDutiesRoot()) {
2241
+ if (!isSafeName(action)) return null;
2242
+ return listDutyActions(projectDutiesRoot).find((d) => d.action === action) ?? null;
2243
+ }
2244
+ function hasDutyAction(action, projectDutiesRoot = getProjectDutiesRoot()) {
2245
+ return resolveDutyAction(action, projectDutiesRoot) !== null;
2246
+ }
2107
2247
  function isSafeName(name) {
2108
2248
  return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
2109
2249
  }
2250
+ function listProjectFolderDutyActions(root) {
2251
+ if (!fs6.existsSync(root) || !fs6.statSync(root).isDirectory()) return [];
2252
+ const out = [];
2253
+ for (const ent of fs6.readdirSync(root, { withFileTypes: true })) {
2254
+ if (!ent.isDirectory() || ent.name.startsWith(".") || ent.name.startsWith("_")) continue;
2255
+ if (!isSafeName(ent.name)) continue;
2256
+ const profilePath = path6.join(root, ent.name, "profile.json");
2257
+ if (!fs6.existsSync(profilePath) || !fs6.statSync(profilePath).isFile()) continue;
2258
+ try {
2259
+ const raw = JSON.parse(fs6.readFileSync(profilePath, "utf-8"));
2260
+ const action = stringOr(raw.action, ent.name);
2261
+ const executable = stringOr(raw.executable, ent.name);
2262
+ out.push({
2263
+ action,
2264
+ duty: ent.name,
2265
+ executable,
2266
+ cliArgs: {},
2267
+ source: "project-folder",
2268
+ describe: typeof raw.describe === "string" ? raw.describe : void 0,
2269
+ profilePath
2270
+ });
2271
+ } catch {
2272
+ continue;
2273
+ }
2274
+ }
2275
+ return out.sort((a, b) => a.action.localeCompare(b.action));
2276
+ }
2277
+ function listProjectMarkdownDutyActions(root) {
2278
+ if (!fs6.existsSync(root) || !fs6.statSync(root).isDirectory()) return [];
2279
+ const out = [];
2280
+ for (const ent of fs6.readdirSync(root, { withFileTypes: true })) {
2281
+ if (!ent.isFile() || !ent.name.endsWith(".md")) continue;
2282
+ const duty = ent.name.slice(0, -3);
2283
+ if (!isSafeName(duty)) continue;
2284
+ const filePath = path6.join(root, ent.name);
2285
+ try {
2286
+ const raw = fs6.readFileSync(filePath, "utf-8");
2287
+ const { frontmatter } = splitFrontmatter(raw);
2288
+ const action = frontmatter.action?.trim() || duty;
2289
+ const implementation = markdownDutyImplementation(duty, frontmatter);
2290
+ out.push({
2291
+ action,
2292
+ duty,
2293
+ executable: implementation.executable,
2294
+ cliArgs: implementation.cliArgs,
2295
+ source: "project-markdown",
2296
+ filePath
2297
+ });
2298
+ } catch {
2299
+ continue;
2300
+ }
2301
+ }
2302
+ return out.sort((a, b) => a.action.localeCompare(b.action));
2303
+ }
2304
+ function markdownDutyImplementation(duty, frontmatter) {
2305
+ if (frontmatter.executable?.trim()) {
2306
+ return { executable: frontmatter.executable.trim(), cliArgs: {} };
2307
+ }
2308
+ if (frontmatter.executables?.length === 1 && frontmatter.executables[0]?.trim()) {
2309
+ return { executable: frontmatter.executables[0].trim(), cliArgs: {} };
2310
+ }
2311
+ if (frontmatter.tickScript?.trim()) {
2312
+ return { executable: "duty-tick-scripted", cliArgs: { duty } };
2313
+ }
2314
+ return { executable: "duty-tick", cliArgs: { duty } };
2315
+ }
2316
+ function listBuiltinDutyActions(root = getBuiltinDutiesRoot()) {
2317
+ const filePath = path6.join(root, "public-actions.json");
2318
+ if (!fs6.existsSync(filePath) || !fs6.statSync(filePath).isFile()) return [];
2319
+ try {
2320
+ const raw = JSON.parse(fs6.readFileSync(filePath, "utf-8"));
2321
+ if (!Array.isArray(raw)) return [];
2322
+ const out = [];
2323
+ for (const item of raw) {
2324
+ if (!item || typeof item !== "object") continue;
2325
+ const r = item;
2326
+ const duty = stringOr(r.duty, stringOr(r.name, ""));
2327
+ const action = stringOr(r.action, duty);
2328
+ const executable = stringOr(r.executable, duty);
2329
+ if (!duty || !action || !executable) continue;
2330
+ out.push({
2331
+ action,
2332
+ duty,
2333
+ executable,
2334
+ cliArgs: {},
2335
+ source: "builtin",
2336
+ describe: typeof r.describe === "string" ? r.describe : void 0,
2337
+ filePath
2338
+ });
2339
+ }
2340
+ return out.sort((a, b) => a.action.localeCompare(b.action));
2341
+ } catch {
2342
+ return [];
2343
+ }
2344
+ }
2345
+ function stringOr(value, fallback) {
2346
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : fallback;
2347
+ }
2110
2348
  function getProfileInputs(name, roots = getExecutableRoots()) {
2111
2349
  const profilePath = resolveExecutable(name, roots);
2112
2350
  if (!profilePath) return null;
@@ -2147,6 +2385,7 @@ var _builtinNames;
2147
2385
  var init_registry = __esm({
2148
2386
  "src/registry.ts"() {
2149
2387
  "use strict";
2388
+ init_jobFrontmatter();
2150
2389
  _builtinNames = null;
2151
2390
  }
2152
2391
  });
@@ -2495,7 +2734,7 @@ var init_buildSyntheticPlugin = __esm({
2495
2734
  // src/subagents.ts
2496
2735
  import * as fs14 from "fs";
2497
2736
  import * as path13 from "path";
2498
- function splitFrontmatter(raw) {
2737
+ function splitFrontmatter2(raw) {
2499
2738
  const match = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(raw);
2500
2739
  if (!match) return { fm: {}, body: raw.trim() };
2501
2740
  const fm = {};
@@ -2531,7 +2770,7 @@ function loadSubagents(profile) {
2531
2770
  const agents = {};
2532
2771
  for (const name of names) {
2533
2772
  const raw = profile.subagentTemplates?.[name] ?? fs14.readFileSync(resolveAgentFile(profile.dir, name), "utf-8");
2534
- const { fm, body } = splitFrontmatter(raw);
2773
+ const { fm, body } = splitFrontmatter2(raw);
2535
2774
  if (!body) throw new Error(`loadSubagents: agent '${name}' has an empty prompt body`);
2536
2775
  const def = {
2537
2776
  description: fm.description ?? `Subagent ${name}`,
@@ -2587,6 +2826,7 @@ function loadProfile(profilePath) {
2587
2826
  return {
2588
2827
  ...base,
2589
2828
  name: requireString(profilePath, r, "name"),
2829
+ action: typeof r.action === "string" && r.action.trim() ? r.action.trim() : void 0,
2590
2830
  executable: execRef,
2591
2831
  describe: typeof r.describe === "string" ? r.describe : base.describe,
2592
2832
  staff: typeof r.staff === "string" && r.staff.trim() ? r.staff.trim() : base.staff,
@@ -2630,6 +2870,7 @@ function loadProfile(profilePath) {
2630
2870
  }
2631
2871
  const profile = {
2632
2872
  name: requireString(profilePath, r, "name"),
2873
+ action: typeof r.action === "string" && r.action.trim() ? r.action.trim() : void 0,
2633
2874
  executable: void 0,
2634
2875
  describe: typeof r.describe === "string" ? r.describe : "",
2635
2876
  // Optional persona to run as. Empty/blank string → undefined (no persona).
@@ -2967,6 +3208,7 @@ var init_profile = __esm({
2967
3208
  VALID_PHASES = /* @__PURE__ */ new Set(["research", "planning", "implementing", "reviewing", "shipped", "failed", "idle"]);
2968
3209
  KNOWN_PROFILE_KEYS = /* @__PURE__ */ new Set([
2969
3210
  "name",
3211
+ "action",
2970
3212
  "executable",
2971
3213
  "staff",
2972
3214
  "every",
@@ -7041,10 +7283,12 @@ ${stateBody}`;
7041
7283
 
7042
7284
  // src/jobIdentity.ts
7043
7285
  function stableJobKey(job) {
7044
- const executable = job.executable ?? job.duty ?? "unknown";
7286
+ const duty = job.duty ?? job.action;
7287
+ const executable = job.executable ?? duty ?? "unknown";
7045
7288
  if (job.flavor === "scheduled" && job.duty) return `scheduled:${job.duty}:${executable}`;
7046
7289
  const target = typeof job.target === "number" ? job.target : targetFromCliArgs(job.cliArgs);
7047
- return target === void 0 ? `${job.flavor}:${executable}` : `${job.flavor}:${executable}:${target}`;
7290
+ const work = duty ?? executable;
7291
+ return target === void 0 ? `${job.flavor}:${work}` : `${job.flavor}:${work}:${target}`;
7048
7292
  }
7049
7293
  function targetFromCliArgs(cliArgs) {
7050
7294
  if (!cliArgs) return void 0;
@@ -7083,8 +7327,8 @@ function validateJob(input) {
7083
7327
  throw new InvalidJobError("job must be an object");
7084
7328
  }
7085
7329
  const j = input;
7086
- if (typeof j.executable !== "string" && typeof j.duty !== "string") {
7087
- throw new InvalidJobError("job must reference an executable or a duty");
7330
+ if (typeof j.executable !== "string" && typeof j.duty !== "string" && typeof j.action !== "string") {
7331
+ throw new InvalidJobError("job must reference a duty action, duty, or executable");
7088
7332
  }
7089
7333
  if (j.flavor !== "instant" && j.flavor !== "scheduled") {
7090
7334
  throw new InvalidJobError(`job.flavor must be "instant" or "scheduled" (got ${String(j.flavor)})`);
@@ -7093,6 +7337,7 @@ function validateJob(input) {
7093
7337
  throw new InvalidJobError("job.cliArgs must be an object when present");
7094
7338
  }
7095
7339
  return {
7340
+ action: typeof j.action === "string" ? j.action : void 0,
7096
7341
  executable: typeof j.executable === "string" ? j.executable : void 0,
7097
7342
  duty: typeof j.duty === "string" ? j.duty : void 0,
7098
7343
  why: typeof j.why === "string" ? j.why : void 0,
@@ -7106,7 +7351,9 @@ function validateJob(input) {
7106
7351
  }
7107
7352
  async function runJob(job, base) {
7108
7353
  const valid = validateJob(job);
7109
- const profileName = valid.executable ?? valid.duty;
7354
+ const action = valid.action ?? valid.duty;
7355
+ const resolvedDuty = action ? resolveDutyAction(action) : null;
7356
+ const profileName = valid.executable ?? resolvedDuty?.executable ?? valid.duty;
7110
7357
  if (!profileName) {
7111
7358
  throw new InvalidJobError("job resolves to no executable or duty");
7112
7359
  }
@@ -7115,8 +7362,12 @@ async function runJob(job, base) {
7115
7362
  preloadedData.jobKey = stableJobKey(valid);
7116
7363
  preloadedData.jobFlavor = valid.flavor;
7117
7364
  if (valid.target !== void 0) preloadedData.jobTarget = valid.target;
7118
- if (valid.duty !== void 0 && valid.duty.length > 0) preloadedData.jobDuty = valid.duty;
7119
- if (valid.executable !== void 0 && valid.executable.length > 0) preloadedData.jobExecutable = valid.executable;
7365
+ if (valid.action !== void 0 && valid.action.length > 0) preloadedData.jobAction = valid.action;
7366
+ const dutyIdentity = valid.duty ?? resolvedDuty?.duty;
7367
+ if (dutyIdentity !== void 0 && dutyIdentity.length > 0) preloadedData.jobDuty = dutyIdentity;
7368
+ const executableIdentity = valid.executable ?? resolvedDuty?.executable;
7369
+ if (executableIdentity !== void 0 && executableIdentity.length > 0)
7370
+ preloadedData.jobExecutable = executableIdentity;
7120
7371
  if (valid.schedule !== void 0 && valid.schedule.length > 0) preloadedData.jobSchedule = valid.schedule;
7121
7372
  if (valid.why !== void 0 && valid.why.length > 0) preloadedData.jobWhy = valid.why;
7122
7373
  if (valid.persona !== void 0) preloadedData.jobPersona = valid.persona;
@@ -7124,16 +7375,20 @@ async function runJob(job, base) {
7124
7375
  cliArgs: { ...valid.cliArgs },
7125
7376
  cwd: base.cwd,
7126
7377
  config: base.config,
7378
+ skipConfig: base.skipConfig,
7127
7379
  verbose: base.verbose,
7128
7380
  quiet: base.quiet,
7129
7381
  preloadedData: Object.keys(preloadedData).length > 0 ? preloadedData : void 0
7130
7382
  };
7383
+ input.cliArgs = resolvedDuty ? { ...resolvedDuty.cliArgs, ...input.cliArgs } : input.cliArgs;
7131
7384
  const run = base.chain === false ? runExecutable : runExecutableChain;
7132
7385
  return run(profileName, input);
7133
7386
  }
7134
7387
  function mintInstantJob(dispatch2, opts) {
7135
7388
  return {
7389
+ action: dispatch2.action,
7136
7390
  executable: dispatch2.executable,
7391
+ duty: dispatch2.duty,
7137
7392
  why: opts?.why ?? dispatch2.why,
7138
7393
  persona: opts?.persona ?? DEFAULT_INSTANT_PERSONA,
7139
7394
  target: dispatch2.target,
@@ -7156,6 +7411,7 @@ var init_job = __esm({
7156
7411
  "src/job.ts"() {
7157
7412
  "use strict";
7158
7413
  init_executor();
7414
+ init_registry();
7159
7415
  init_jobIdentity();
7160
7416
  init_jobIdentity();
7161
7417
  DEFAULT_INSTANT_PERSONA = "kody";
@@ -7169,106 +7425,6 @@ var init_job = __esm({
7169
7425
  }
7170
7426
  });
7171
7427
 
7172
- // src/scripts/jobFrontmatter.ts
7173
- function splitFrontmatter2(raw) {
7174
- const match = FRONTMATTER_RE.exec(raw);
7175
- if (!match) return { frontmatter: {}, body: raw };
7176
- const inner = match[1] ?? "";
7177
- const body = raw.slice(match[0].length);
7178
- return { frontmatter: parseFlatYaml(inner), body };
7179
- }
7180
- function isScheduleEvery(value) {
7181
- return typeof value === "string" && SCHEDULE_EVERY_VALUES.includes(value);
7182
- }
7183
- function scheduleEveryToMs(every) {
7184
- const MIN = 60 * 1e3;
7185
- const HOUR = 60 * MIN;
7186
- const DAY = 24 * HOUR;
7187
- switch (every) {
7188
- case "15m":
7189
- return 15 * MIN;
7190
- case "30m":
7191
- return 30 * MIN;
7192
- case "1h":
7193
- return HOUR;
7194
- case "2h":
7195
- return 2 * HOUR;
7196
- case "6h":
7197
- return 6 * HOUR;
7198
- case "12h":
7199
- return 12 * HOUR;
7200
- case "1d":
7201
- return DAY;
7202
- case "3d":
7203
- return 3 * DAY;
7204
- case "7d":
7205
- return 7 * DAY;
7206
- case "manual":
7207
- return Number.POSITIVE_INFINITY;
7208
- }
7209
- }
7210
- function parseFlatYaml(text) {
7211
- const out = {};
7212
- for (const rawLine of text.split(/\r?\n/)) {
7213
- const line = rawLine.trim();
7214
- if (!line || line.startsWith("#")) continue;
7215
- const colon = line.indexOf(":");
7216
- if (colon < 0) continue;
7217
- const key = line.slice(0, colon).trim();
7218
- const value = stripQuotes(line.slice(colon + 1).trim());
7219
- if (key === "every" && isScheduleEvery(value)) {
7220
- out.every = value;
7221
- } else if (key === "tickScript" && value.length > 0) {
7222
- out.tickScript = value;
7223
- } else if (key === "disabled") {
7224
- const lower = value.toLowerCase();
7225
- if (lower === "true") out.disabled = true;
7226
- else if (lower === "false") out.disabled = false;
7227
- } else if (key === "staff" && value.length > 0) {
7228
- out.staff = value;
7229
- } else if (key === "mentions") {
7230
- const logins = value.split(",").map((s) => s.trim().replace(/^@/, "")).filter(Boolean);
7231
- if (logins.length > 0) out.mentions = logins;
7232
- } else if (key === "tools") {
7233
- const names = value.split(",").map((s) => s.trim()).filter(Boolean);
7234
- if (names.length > 0) out.tools = names;
7235
- } else if (key === "executables") {
7236
- const names = value.split(",").map((s) => s.trim()).filter(Boolean);
7237
- if (names.length > 0) out.executables = names;
7238
- }
7239
- }
7240
- return out;
7241
- }
7242
- function stripQuotes(value) {
7243
- if (value.length >= 2) {
7244
- const first = value[0];
7245
- const last = value[value.length - 1];
7246
- if (first === '"' && last === '"' || first === "'" && last === "'") {
7247
- return value.slice(1, -1);
7248
- }
7249
- }
7250
- return value;
7251
- }
7252
- var SCHEDULE_EVERY_VALUES, FRONTMATTER_RE;
7253
- var init_jobFrontmatter = __esm({
7254
- "src/scripts/jobFrontmatter.ts"() {
7255
- "use strict";
7256
- SCHEDULE_EVERY_VALUES = [
7257
- "15m",
7258
- "30m",
7259
- "1h",
7260
- "2h",
7261
- "6h",
7262
- "12h",
7263
- "1d",
7264
- "3d",
7265
- "7d",
7266
- "manual"
7267
- ];
7268
- FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
7269
- }
7270
- });
7271
-
7272
7428
  // src/scripts/issueStateComment.ts
7273
7429
  function isStateEnvelope(x) {
7274
7430
  if (x === null || typeof x !== "object") return false;
@@ -7827,7 +7983,7 @@ function formatAgo(ms) {
7827
7983
  function readJobFile(cwd, jobsDir, slug) {
7828
7984
  try {
7829
7985
  const raw = fs29.readFileSync(path26.join(cwd, jobsDir, `${slug}.md`), "utf-8");
7830
- return splitFrontmatter2(raw);
7986
+ return splitFrontmatter(raw);
7831
7987
  } catch {
7832
7988
  return { frontmatter: {}, body: "" };
7833
7989
  }
@@ -9818,7 +9974,7 @@ var init_loadJobFromFile = __esm({
9818
9974
  }
9819
9975
  const raw = fs31.readFileSync(absPath, "utf-8");
9820
9976
  const { title, body } = parseJobFile(raw, slug);
9821
- const frontmatter = splitFrontmatter2(raw).frontmatter;
9977
+ const frontmatter = splitFrontmatter(raw).frontmatter;
9822
9978
  const mentions = (frontmatter.mentions ?? []).map((login) => `@${login}`).join(" ");
9823
9979
  const workerSlug = (frontmatter.staff ?? "").trim();
9824
9980
  let workerTitle = "";
@@ -10162,7 +10318,7 @@ function stripDirective(body) {
10162
10318
  return lines.slice(start).join("\n").trim();
10163
10319
  }
10164
10320
  function parsePersona(raw, slug) {
10165
- const stripped = splitFrontmatter2(raw).body;
10321
+ const stripped = splitFrontmatter(raw).body;
10166
10322
  const trimmed = stripped.trim();
10167
10323
  const firstLine2 = trimmed.split("\n", 1)[0] ?? "";
10168
10324
  const h1 = /^#\s+(.+?)\s*$/.exec(firstLine2);
@@ -12500,7 +12656,7 @@ var init_runTickScript = __esm({
12500
12656
  return;
12501
12657
  }
12502
12658
  const raw = fs36.readFileSync(jobPath, "utf-8");
12503
- const { frontmatter } = splitFrontmatter2(raw);
12659
+ const { frontmatter } = splitFrontmatter(raw);
12504
12660
  const tickScript = frontmatter.tickScript;
12505
12661
  if (!tickScript) {
12506
12662
  ctx.output.exitCode = 99;
@@ -15370,10 +15526,48 @@ function primaryNumericInputName(executable) {
15370
15526
  const intInput = inputs.find((i) => i.type === "int" && i.required);
15371
15527
  return intInput?.name ?? null;
15372
15528
  }
15529
+ function resolveOperatorAction(action) {
15530
+ return resolveDutyAction(action);
15531
+ }
15532
+ function resolveConfiguredAction(action) {
15533
+ const resolved = resolveDutyAction(action);
15534
+ if (resolved) return resolved;
15535
+ return compatibilityDutyAction(action);
15536
+ }
15537
+ function requiredRoute(action) {
15538
+ return resolveConfiguredAction(action) ?? {
15539
+ action,
15540
+ duty: action,
15541
+ executable: action,
15542
+ cliArgs: {},
15543
+ source: "builtin"
15544
+ };
15545
+ }
15546
+ function compatibilityDutyAction(action) {
15547
+ if (!/^[a-z][a-z0-9-]*$/.test(action)) return null;
15548
+ return {
15549
+ action,
15550
+ duty: action,
15551
+ executable: action,
15552
+ cliArgs: {},
15553
+ source: "builtin"
15554
+ };
15555
+ }
15556
+ function routeResult(route, cliArgs, target, why) {
15557
+ const result = {
15558
+ action: route.action,
15559
+ duty: route.duty,
15560
+ executable: route.executable,
15561
+ cliArgs: { ...route.cliArgs, ...cliArgs },
15562
+ target
15563
+ };
15564
+ if (why !== void 0 && why.length > 0) result.why = why;
15565
+ return result;
15566
+ }
15373
15567
  function autoDispatch(opts) {
15374
15568
  const explicit = opts?.explicit;
15375
15569
  if (explicit?.issueNumber && explicit.issueNumber > 0) {
15376
- return { executable: "run", cliArgs: { issue: explicit.issueNumber }, target: explicit.issueNumber };
15570
+ return routeResult(requiredRoute("run"), { issue: explicit.issueNumber }, explicit.issueNumber);
15377
15571
  }
15378
15572
  const eventName = process.env.GITHUB_EVENT_NAME;
15379
15573
  const eventPath = process.env.GITHUB_EVENT_PATH;
@@ -15388,25 +15582,29 @@ function autoDispatch(opts) {
15388
15582
  const inputs2 = objectValue(event.inputs);
15389
15583
  const n = parseInt(String(inputs2?.issue_number ?? ""), 10);
15390
15584
  if (!Number.isNaN(n) && n > 0) {
15391
- const exe = String(inputs2?.executable ?? "").trim() || "run";
15585
+ const actionName = String(inputs2?.executable ?? "").trim() || "run";
15586
+ const route2 = resolveConfiguredAction(actionName);
15587
+ if (!route2) return null;
15392
15588
  const base = String(inputs2?.base ?? "").trim();
15393
- const targetKey = primaryNumericInputName(exe) ?? "issue";
15589
+ const targetKey = primaryNumericInputName(route2.executable) ?? "issue";
15394
15590
  const cliArgs = { [targetKey]: n };
15395
15591
  if (base) cliArgs.base = base;
15396
- return { executable: exe, cliArgs, target: n };
15592
+ return routeResult(route2, cliArgs, n);
15397
15593
  }
15398
15594
  return null;
15399
15595
  }
15400
15596
  if (eventName === "schedule") return null;
15401
15597
  if (eventName === "pull_request") {
15402
- const exe = opts?.config?.onPullRequest?.trim();
15598
+ const actionName = opts?.config?.onPullRequest?.trim();
15403
15599
  const action = String(event.action ?? "");
15404
- if (exe && (action === "opened" || action === "synchronize" || action === "reopened")) {
15600
+ if (actionName && (action === "opened" || action === "synchronize" || action === "reopened")) {
15601
+ const route2 = resolveConfiguredAction(actionName);
15602
+ if (!route2) return null;
15405
15603
  const pullRequest = objectValue(event.pull_request);
15406
15604
  const prNum = Number(pullRequest?.number ?? event.number ?? 0);
15407
15605
  if (prNum > 0) {
15408
- const targetKey = primaryNumericInputName(exe) ?? "pr";
15409
- return { executable: exe, cliArgs: { [targetKey]: prNum }, target: prNum };
15606
+ const targetKey = primaryNumericInputName(route2.executable) ?? "pr";
15607
+ return routeResult(route2, { [targetKey]: prNum }, prNum);
15410
15608
  }
15411
15609
  }
15412
15610
  return null;
@@ -15430,21 +15628,22 @@ function autoDispatch(opts) {
15430
15628
  const firstToken = firstTokenRaw && POLITE_WORDS.has(firstTokenRaw) ? null : firstTokenRaw;
15431
15629
  const aliases = opts?.config?.aliases ?? BUILTIN_ALIASES;
15432
15630
  const aliased = firstToken ? aliases[firstToken] ?? firstToken : null;
15433
- let executable = null;
15631
+ let route = null;
15434
15632
  let consumedFirstToken = false;
15435
15633
  if (aliased) {
15436
- if (getProfileInputs(aliased) !== null) {
15437
- executable = aliased;
15634
+ route = resolveOperatorAction(aliased);
15635
+ if (route) {
15438
15636
  consumedFirstToken = true;
15439
15637
  } else if (firstToken && aliases[firstToken] && aliases[firstToken] === aliased) {
15440
15638
  process.stderr.write(
15441
- `[kody] dispatch: alias '${firstToken}' \u2192 '${aliased}' has no matching executable; falling back to default
15639
+ `[kody] dispatch: alias '${firstToken}' \u2192 '${aliased}' has no matching duty action; falling back to default
15442
15640
  `
15443
15641
  );
15444
15642
  }
15445
15643
  }
15446
- if (!executable && !firstToken) {
15447
- executable = isPr ? opts?.config?.defaultPrExecutable ?? null : opts?.config?.defaultExecutable ?? null;
15644
+ if (!route && !firstToken) {
15645
+ const defaultAction = isPr ? opts?.config?.defaultPrExecutable ?? null : opts?.config?.defaultExecutable ?? null;
15646
+ route = defaultAction ? resolveConfiguredAction(defaultAction) : null;
15448
15647
  }
15449
15648
  if (isBotAuthor && !consumedFirstToken) {
15450
15649
  process.stderr.write(
@@ -15453,16 +15652,16 @@ function autoDispatch(opts) {
15453
15652
  );
15454
15653
  return null;
15455
15654
  }
15456
- if (!executable) {
15655
+ if (!route) {
15457
15656
  if (!firstToken) return null;
15458
- const profileMissing = aliased ? getProfileInputs(aliased) === null : true;
15657
+ const profileMissing = aliased ? resolveOperatorAction(aliased) === null : true;
15459
15658
  process.stderr.write(
15460
- `[kody] dispatch: no executable resolved for issue_comment (firstToken=${firstToken ?? "<none>"}, aliased=${aliased ?? "<none>"}, profileFound=${!profileMissing}, defaultExecutable=${opts?.config?.defaultExecutable ?? "<unset>"}, defaultPrExecutable=${opts?.config?.defaultPrExecutable ?? "<unset>"})
15659
+ `[kody] dispatch: no duty action resolved for issue_comment (firstToken=${firstToken ?? "<none>"}, aliased=${aliased ?? "<none>"}, actionFound=${!profileMissing}, defaultExecutable=${opts?.config?.defaultExecutable ?? "<unset>"}, defaultPrExecutable=${opts?.config?.defaultPrExecutable ?? "<unset>"})
15461
15660
  `
15462
15661
  );
15463
15662
  return null;
15464
15663
  }
15465
- const inputs = getProfileInputs(executable);
15664
+ const inputs = getProfileInputs(route.executable);
15466
15665
  const effectiveInputs = inputs ?? [];
15467
15666
  const unknownProfile = inputs === null;
15468
15667
  const rest = extractCommentRest(afterTag, consumedFirstToken ? firstToken : null);
@@ -15479,7 +15678,7 @@ function autoDispatch(opts) {
15479
15678
  } else if (leftover.length > 0) {
15480
15679
  why = leftover;
15481
15680
  }
15482
- return { executable, cliArgs: args, target: targetNum, why };
15681
+ return routeResult(route, args, targetNum, why);
15483
15682
  }
15484
15683
  function autoDispatchTyped(opts) {
15485
15684
  const legacy = autoDispatch(opts);
@@ -15527,7 +15726,7 @@ function autoDispatchTyped(opts) {
15527
15726
  reason: tokenRaw ? `polite-word lead-in '${tokenRaw}', no default executable configured` : "no subcommand token, no default executable configured"
15528
15727
  };
15529
15728
  }
15530
- const available = listExecutables().map((e) => e.name).filter((n) => !n.startsWith("goal-") && !n.startsWith("job-")).sort();
15729
+ const available = listDutyActions().map((e) => e.action).filter((n) => !n.startsWith("goal-") && !n.startsWith("job-")).sort();
15531
15730
  return { kind: "unrecognized", token: tokenRaw, target: targetNum, isPr, available };
15532
15731
  }
15533
15732
  function dispatchScheduledWatches(opts) {
@@ -15564,7 +15763,7 @@ function dispatchScheduledWatches(opts) {
15564
15763
  continue;
15565
15764
  }
15566
15765
  }
15567
- out.push({ executable: exe.name, cliArgs: {}, target: 0 });
15766
+ out.push({ action: exe.name, duty: exe.name, executable: exe.name, cliArgs: {}, target: 0 });
15568
15767
  }
15569
15768
  return out;
15570
15769
  }
@@ -15670,6 +15869,7 @@ init_executor();
15670
15869
  init_gha();
15671
15870
  init_issue();
15672
15871
  init_job();
15872
+ init_registry();
15673
15873
  var CI_HELP = `kody ci \u2014 minimal-YAML autonomous engineer (CI preflight + run)
15674
15874
 
15675
15875
  Usage:
@@ -15910,7 +16110,7 @@ async function runCi(argv) {
15910
16110
  const eventName = process.env.GITHUB_EVENT_NAME;
15911
16111
  const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
15912
16112
  let manualWorkflowDispatch = false;
15913
- let forceRunDuty = null;
16113
+ let forceRunAction = null;
15914
16114
  if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs40.existsSync(dispatchEventPath)) {
15915
16115
  try {
15916
16116
  const evt = JSON.parse(fs40.readFileSync(dispatchEventPath, "utf-8"));
@@ -15918,15 +16118,21 @@ async function runCi(argv) {
15918
16118
  const sessionInput = String(evt?.inputs?.sessionId ?? "");
15919
16119
  const exeInput = String(evt?.inputs?.executable ?? "").trim();
15920
16120
  const noTarget = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
15921
- if (noTarget && exeInput) forceRunDuty = exeInput;
16121
+ if (noTarget && exeInput) forceRunAction = exeInput;
15922
16122
  else manualWorkflowDispatch = noTarget;
15923
16123
  } catch {
15924
16124
  manualWorkflowDispatch = false;
15925
16125
  }
15926
16126
  }
15927
- if (forceRunDuty) {
16127
+ if (forceRunAction) {
15928
16128
  const config = earlyConfig ?? loadConfig(cwd);
15929
- process.stdout.write(`\u2192 kody: manual one-shot run of duty ${forceRunDuty}
16129
+ const route = resolveDutyAction(forceRunAction);
16130
+ if (!route) {
16131
+ process.stderr.write(`[kody] manual one-shot action '${forceRunAction}' has no duty action
16132
+ `);
16133
+ return 64;
16134
+ }
16135
+ process.stdout.write(`\u2192 kody: manual one-shot run of duty action ${route.action} (${route.duty})
15930
16136
 
15931
16137
  `);
15932
16138
  try {
@@ -15957,13 +16163,22 @@ async function runCi(argv) {
15957
16163
  `);
15958
16164
  return 99;
15959
16165
  }
15960
- const result = await runExecutableChain(forceRunDuty, {
15961
- cliArgs: {},
15962
- cwd,
15963
- config,
15964
- verbose: args.verbose,
15965
- quiet: args.quiet
15966
- });
16166
+ const result = await runJob(
16167
+ {
16168
+ action: route.action,
16169
+ duty: route.duty,
16170
+ executable: route.executable,
16171
+ cliArgs: route.cliArgs,
16172
+ flavor: "instant",
16173
+ force: true
16174
+ },
16175
+ {
16176
+ cwd,
16177
+ config,
16178
+ verbose: args.verbose,
16179
+ quiet: args.quiet
16180
+ }
16181
+ );
15967
16182
  const ec = result.exitCode;
15968
16183
  return ec === 0 || ec === 1 || ec === 2 ? ec : 99;
15969
16184
  }
@@ -16024,13 +16239,17 @@ ${CI_HELP}`);
16024
16239
  return 64;
16025
16240
  }
16026
16241
  const dispatch2 = autoFallback ?? {
16242
+ action: "run",
16243
+ duty: "run",
16027
16244
  executable: "run",
16028
16245
  cliArgs: { issue: args.issueNumber },
16029
16246
  target: args.issueNumber
16030
16247
  };
16031
16248
  const issueNumber = dispatch2.target;
16032
- process.stdout.write(`\u2192 kody preflight (cwd=${cwd}, executable=${dispatch2.executable}, target=${issueNumber})
16033
- `);
16249
+ process.stdout.write(
16250
+ `\u2192 kody preflight (cwd=${cwd}, action=${dispatch2.action}, duty=${dispatch2.duty}, executable=${dispatch2.executable}, target=${issueNumber})
16251
+ `
16252
+ );
16034
16253
  try {
16035
16254
  const n = unpackAllSecrets();
16036
16255
  if (n > 0) process.stdout.write(`\u2192 kody: unpacked ${n} secret(s) from ALL_SECRETS
@@ -17610,6 +17829,7 @@ ${CHAT_HELP}`);
17610
17829
  // src/entry.ts
17611
17830
  init_config();
17612
17831
  init_executor();
17832
+ init_job();
17613
17833
  init_registry();
17614
17834
 
17615
17835
  // src/servers/pool-serve.ts
@@ -17831,6 +18051,7 @@ var PoolManager = class {
17831
18051
  }
17832
18052
  deps;
17833
18053
  free = [];
18054
+ claimed = /* @__PURE__ */ new Set();
17834
18055
  booting = 0;
17835
18056
  claimsInFlight = 0;
17836
18057
  refilling = false;
@@ -17847,16 +18068,15 @@ var PoolManager = class {
17847
18068
  }
17848
18069
  /**
17849
18070
  * Resize the warm target at runtime (per-repo, sourced from the repo's vault
17850
- * POOL_MIN). Raising it warms up immediately via refill; lowering it just
17851
- * stops topping up surplus machines drain as they're claimed/auto-destroyed,
17852
- * never force-killed. No-op when unchanged or given a bad value.
18071
+ * POOL_MIN). Raising it warms immediately; lowering it prunes idle surplus
18072
+ * warm machines. No-op when unchanged or given a bad value.
17853
18073
  */
17854
18074
  setMin(min) {
17855
18075
  if (!Number.isInteger(min) || min < 0) return;
17856
18076
  if (min === this.deps.config.min) return;
17857
18077
  this.deps.config.min = min;
17858
18078
  this.log(`min set to ${min}`);
17859
- void this.refill();
18079
+ void this.rebalance("setMin");
17860
18080
  }
17861
18081
  /**
17862
18082
  * Adopt existing pooled machines on owner (re)start: suspended ones become
@@ -17865,12 +18085,17 @@ var PoolManager = class {
17865
18085
  async reconcile() {
17866
18086
  const machines = await this.deps.fly.listPooled(this.deps.config.repoTag);
17867
18087
  this.free = [];
18088
+ const surplus = [];
17868
18089
  for (const m of machines) {
17869
- if ((m.state === "suspended" || m.state === "suspending") && m.private_ip) {
18090
+ if (!isSuspendedWithIp(m)) continue;
18091
+ if (this.warmCapacity() < this.deps.config.min) {
17870
18092
  this.free.push({ id: m.id, privateIp: m.private_ip });
18093
+ } else {
18094
+ surplus.push(m.id);
17871
18095
  }
17872
18096
  }
17873
- this.log(`reconcile: adopted ${this.free.length} suspended machine(s)`);
18097
+ const destroyed = await this.destroySurplus(surplus);
18098
+ this.log(`reconcile: adopted ${this.free.length} suspended machine(s), destroyed ${destroyed} surplus`);
17874
18099
  await this.refill();
17875
18100
  }
17876
18101
  /**
@@ -17887,6 +18112,7 @@ var PoolManager = class {
17887
18112
  const machine = this.free.shift();
17888
18113
  if (!machine) break;
17889
18114
  this.claimsInFlight++;
18115
+ this.claimed.add(machine.id);
17890
18116
  try {
17891
18117
  await this.deps.fly.start(machine.id);
17892
18118
  const healthy = await this.deps.fly.waitHealthy(this.baseUrl(machine), {
@@ -17913,6 +18139,7 @@ var PoolManager = class {
17913
18139
  await this.safeDestroy(machine.id);
17914
18140
  lastReason = errMsg2(err);
17915
18141
  } finally {
18142
+ this.claimed.delete(machine.id);
17916
18143
  this.claimsInFlight--;
17917
18144
  }
17918
18145
  }
@@ -17940,16 +18167,24 @@ var PoolManager = class {
17940
18167
  const before = this.free.length;
17941
18168
  this.free = this.free.filter((f) => liveIds.has(f.id));
17942
18169
  const pruned = before - this.free.length;
17943
- const tracked = new Set(this.free.map((f) => f.id));
18170
+ const destroyedTracked = await this.trimFreeSurplus("resync");
18171
+ const tracked = /* @__PURE__ */ new Set([...this.free.map((f) => f.id), ...this.claimed]);
17944
18172
  let adopted = 0;
18173
+ const surplus = [];
17945
18174
  for (const m of machines) {
17946
- if ((m.state === "suspended" || m.state === "suspending") && m.private_ip && !tracked.has(m.id)) {
18175
+ if (!isSuspendedWithIp(m) || tracked.has(m.id)) continue;
18176
+ if (this.warmCapacity() < this.deps.config.min) {
17947
18177
  this.free.push({ id: m.id, privateIp: m.private_ip });
18178
+ tracked.add(m.id);
17948
18179
  adopted++;
18180
+ } else if (this.booting === 0) {
18181
+ surplus.push(m.id);
17949
18182
  }
17950
18183
  }
17951
- if (pruned > 0 || adopted > 0) {
17952
- this.log(`resync: pruned ${pruned} stale, adopted ${adopted} (free=${this.free.length})`);
18184
+ const destroyedSurplus = await this.destroySurplus(surplus);
18185
+ const destroyed = destroyedTracked + destroyedSurplus;
18186
+ if (pruned > 0 || adopted > 0 || destroyed > 0) {
18187
+ this.log(`resync: pruned ${pruned} stale, adopted ${adopted}, destroyed ${destroyed} surplus (free=${this.free.length})`);
17953
18188
  }
17954
18189
  await this.refill();
17955
18190
  }
@@ -18004,11 +18239,37 @@ var PoolManager = class {
18004
18239
  baseUrl(m) {
18005
18240
  return `http://[${m.privateIp}]:${this.deps.config.port}`;
18006
18241
  }
18242
+ warmCapacity() {
18243
+ return this.free.length + this.booting;
18244
+ }
18245
+ async rebalance(reason) {
18246
+ await this.trimFreeSurplus(reason);
18247
+ await this.refill();
18248
+ }
18249
+ async trimFreeSurplus(reason) {
18250
+ const surplus = [];
18251
+ while (this.free.length > this.deps.config.min) {
18252
+ const machine = this.free.pop();
18253
+ if (machine) surplus.push(machine.id);
18254
+ }
18255
+ const destroyed = await this.destroySurplus(surplus);
18256
+ if (destroyed > 0) this.log(`${reason}: destroyed ${destroyed} surplus free machine(s)`);
18257
+ return destroyed;
18258
+ }
18259
+ async destroySurplus(ids) {
18260
+ let destroyed = 0;
18261
+ for (const id of ids) {
18262
+ if (await this.safeDestroy(id)) destroyed++;
18263
+ }
18264
+ return destroyed;
18265
+ }
18007
18266
  async safeDestroy(id) {
18008
18267
  try {
18009
18268
  await this.deps.fly.destroy(id);
18269
+ return true;
18010
18270
  } catch (err) {
18011
18271
  this.log(`destroy ${id} failed: ${errMsg2(err)}`);
18272
+ return false;
18012
18273
  }
18013
18274
  }
18014
18275
  };
@@ -18027,6 +18288,9 @@ async function defaultPostRun(m, job, cfg) {
18027
18288
  function errMsg2(err) {
18028
18289
  return err instanceof Error ? err.message : String(err);
18029
18290
  }
18291
+ function isSuspendedWithIp(m) {
18292
+ return (m.state === "suspended" || m.state === "suspending") && !!m.private_ip;
18293
+ }
18030
18294
 
18031
18295
  // src/pool/vault.ts
18032
18296
  import { createDecipheriv as createDecipheriv2 } from "crypto";
@@ -18954,16 +19218,17 @@ Usage:
18954
19218
  kody-engine preview-build --pr <N> [--cwd <path>] [--verbose|--quiet]
18955
19219
  kody-engine release --issue <N> [--cwd <path>] [--verbose|--quiet]
18956
19220
  kody-engine init [--cwd <path>] [--verbose|--quiet]
18957
- kody-engine <executable> [--cwd <path>] [--verbose|--quiet]
19221
+ kody-engine <action> [--cwd <path>] [--verbose|--quiet]
19222
+ kody-engine exec <executable> [--cwd <path>] [--verbose|--quiet]
18958
19223
  kody-engine ci [preflight flags \u2014 see: kody-engine ci --help]
18959
19224
  kody-engine chat [chat flags \u2014 see: kody-engine chat --help]
18960
19225
  kody-engine stats [--since 7d|--run <id>|--json|--cwd <path>]
18961
19226
  kody-engine help
18962
19227
  kody-engine version
18963
19228
 
18964
- Each top-level command is a discovered executable under
18965
- \`src/executables/<name>/profile.json\`. Drop in a new directory to add a new
18966
- command; consumer repos can also provide their own executable profiles.
19229
+ Top-level work commands are duty actions. A duty owns the public action name
19230
+ and selects an implementation executable. \`exec <executable>\` is the low-level
19231
+ debug path for engine internals and migration compatibility.
18967
19232
 
18968
19233
  Exit codes:
18969
19234
  0 success (PR opened, verify passed \u2014 or resolve produced a merge commit)
@@ -19004,6 +19269,33 @@ function parseArgs(argv) {
19004
19269
  result.serverArgs = argv.slice(1).filter((a) => !a.startsWith("-"));
19005
19270
  return result;
19006
19271
  }
19272
+ if (cmd === "exec") {
19273
+ const executableName = argv[1];
19274
+ if (!executableName) {
19275
+ result.errors.push("exec requires an executable name");
19276
+ return result;
19277
+ }
19278
+ if (!hasExecutable(executableName)) {
19279
+ result.errors.push(`unknown executable: ${executableName}`);
19280
+ return result;
19281
+ }
19282
+ result.command = "__executable__";
19283
+ result.executableName = executableName;
19284
+ result.cliArgs = parseGenericFlags(argv.slice(2));
19285
+ if (typeof result.cliArgs.cwd === "string") result.cwd = result.cliArgs.cwd;
19286
+ if (result.cliArgs.verbose === true) result.verbose = true;
19287
+ if (result.cliArgs.quiet === true) result.quiet = true;
19288
+ return result;
19289
+ }
19290
+ if (hasDutyAction(cmd)) {
19291
+ result.command = "__duty__";
19292
+ result.actionName = cmd;
19293
+ result.cliArgs = parseGenericFlags(argv.slice(1));
19294
+ if (typeof result.cliArgs.cwd === "string") result.cwd = result.cliArgs.cwd;
19295
+ if (result.cliArgs.verbose === true) result.verbose = true;
19296
+ if (result.cliArgs.quiet === true) result.quiet = true;
19297
+ return result;
19298
+ }
19007
19299
  if (hasExecutable(cmd)) {
19008
19300
  result.command = "__executable__";
19009
19301
  result.executableName = cmd;
@@ -19013,8 +19305,9 @@ function parseArgs(argv) {
19013
19305
  if (result.cliArgs.quiet === true) result.quiet = true;
19014
19306
  return result;
19015
19307
  }
19016
- const discovered = listExecutables().map((e) => e.name);
19017
- const available = ["ci", "chat", "stats", "help", "version", ...discovered];
19308
+ const discoveredActions = listDutyActions().map((e) => e.action);
19309
+ const discoveredExecutables = listExecutables().map((e) => `exec ${e.name}`);
19310
+ const available = ["ci", "chat", "stats", "help", "version", ...discoveredActions, ...discoveredExecutables];
19018
19311
  result.errors.push(`unknown command: ${cmd} (available: ${available.join(", ")})`);
19019
19312
  return result;
19020
19313
  }
@@ -19104,6 +19397,48 @@ ${HELP_TEXT}`);
19104
19397
  }
19105
19398
  const cwd = args.cwd ?? process.cwd();
19106
19399
  const configlessCommands = /* @__PURE__ */ new Set(["init", "goal-scheduler"]);
19400
+ if (args.command === "__duty__") {
19401
+ const route = resolveDutyAction(args.actionName);
19402
+ if (!route) {
19403
+ process.stderr.write(`error: unknown duty action '${args.actionName}'
19404
+ `);
19405
+ return 64;
19406
+ }
19407
+ const cliArgs = { ...route.cliArgs, ...args.cliArgs ?? {} };
19408
+ const skipConfig2 = configlessCommands.has(route.executable);
19409
+ try {
19410
+ const result = await runJob(
19411
+ {
19412
+ action: route.action,
19413
+ duty: route.duty,
19414
+ executable: route.executable,
19415
+ cliArgs,
19416
+ target: numericTarget(cliArgs),
19417
+ flavor: "instant"
19418
+ },
19419
+ {
19420
+ cwd,
19421
+ skipConfig: skipConfig2,
19422
+ verbose: args.verbose,
19423
+ quiet: args.quiet
19424
+ }
19425
+ );
19426
+ if (result.exitCode !== 0 && result.reason) {
19427
+ process.stderr.write(`error: ${result.reason}
19428
+ `);
19429
+ }
19430
+ return result.exitCode;
19431
+ } catch (err) {
19432
+ const msg = err instanceof Error ? err.message : String(err);
19433
+ process.stderr.write(`[kody] ${args.actionName} crashed: ${msg}
19434
+ `);
19435
+ if (err instanceof Error && err.stack) process.stderr.write(`${err.stack}
19436
+ `);
19437
+ process.stdout.write(`PR_URL=FAILED: ${args.actionName} crashed: ${msg}
19438
+ `);
19439
+ return 99;
19440
+ }
19441
+ }
19107
19442
  const skipConfig = configlessCommands.has(args.executableName ?? "");
19108
19443
  try {
19109
19444
  const result = await runExecutableChain(args.executableName, {
@@ -19129,6 +19464,14 @@ ${HELP_TEXT}`);
19129
19464
  return 99;
19130
19465
  }
19131
19466
  }
19467
+ function numericTarget(cliArgs) {
19468
+ for (const key of ["issue", "pr"]) {
19469
+ const raw = cliArgs[key];
19470
+ const n = typeof raw === "number" ? raw : typeof raw === "string" ? parseInt(raw, 10) : Number.NaN;
19471
+ if (Number.isFinite(n) && n > 0) return n;
19472
+ }
19473
+ return void 0;
19474
+ }
19132
19475
 
19133
19476
  // bin/kody.ts
19134
19477
  main().then((code) => {
@@ -0,0 +1,86 @@
1
+ [
2
+ {
3
+ "name": "run",
4
+ "action": "run",
5
+ "executable": "run",
6
+ "describe": "Implement a GitHub issue end-to-end."
7
+ },
8
+ {
9
+ "name": "fix",
10
+ "action": "fix",
11
+ "executable": "fix",
12
+ "describe": "Apply review feedback to an existing PR."
13
+ },
14
+ {
15
+ "name": "fix-ci",
16
+ "action": "fix-ci",
17
+ "executable": "fix-ci",
18
+ "describe": "Fix a failing CI workflow on an existing PR."
19
+ },
20
+ {
21
+ "name": "resolve",
22
+ "action": "resolve",
23
+ "executable": "resolve",
24
+ "describe": "Resolve merge conflicts on an existing PR."
25
+ },
26
+ {
27
+ "name": "sync",
28
+ "action": "sync",
29
+ "executable": "sync",
30
+ "describe": "Merge the base branch into a PR branch."
31
+ },
32
+ {
33
+ "name": "merge",
34
+ "action": "merge",
35
+ "executable": "merge",
36
+ "describe": "Self-gating squash merge of a PR."
37
+ },
38
+ {
39
+ "name": "revert",
40
+ "action": "revert",
41
+ "executable": "revert",
42
+ "describe": "Revert one or more commits on a PR branch."
43
+ },
44
+ {
45
+ "name": "preview-build",
46
+ "action": "preview-build",
47
+ "executable": "preview-build",
48
+ "describe": "Build and publish a per-PR preview."
49
+ },
50
+ {
51
+ "name": "release",
52
+ "action": "release",
53
+ "executable": "release",
54
+ "describe": "Run the release flow."
55
+ },
56
+ {
57
+ "name": "release-prepare",
58
+ "action": "release-prepare",
59
+ "executable": "release-prepare",
60
+ "describe": "Prepare a release PR."
61
+ },
62
+ {
63
+ "name": "release-publish",
64
+ "action": "release-publish",
65
+ "executable": "release-publish",
66
+ "describe": "Publish a prepared release."
67
+ },
68
+ {
69
+ "name": "release-deploy",
70
+ "action": "release-deploy",
71
+ "executable": "release-deploy",
72
+ "describe": "Deploy or promote a release."
73
+ },
74
+ {
75
+ "name": "init",
76
+ "action": "init",
77
+ "executable": "init",
78
+ "describe": "Install Kody engine files in a repo."
79
+ },
80
+ {
81
+ "name": "worker-ask",
82
+ "action": "worker-ask",
83
+ "executable": "worker-ask",
84
+ "describe": "Run a staff member once against an inline request."
85
+ }
86
+ ]
@@ -17,6 +17,12 @@ import type { Phase } from "../state.js"
17
17
 
18
18
  export interface Profile {
19
19
  name: string
20
+ /**
21
+ * Public action name owned by a duty. A user may type `@kody <action>`;
22
+ * dispatch resolves that action to the duty, then the duty selects the
23
+ * implementation executable. Absent → the duty slug/name is the action.
24
+ */
25
+ action?: string
20
26
  /**
21
27
  * Optional staff member this executable runs *as*. When set, the executor
22
28
  * loads `.kody/staff/<staff>.md` and injects that persona (authoritative
@@ -462,6 +468,8 @@ export type AnyScript = PreflightScript | PostflightScript
462
468
  export type JobFlavor = "instant" | "scheduled"
463
469
 
464
470
  export interface Job {
471
+ /** Public action the user/operator invoked. Mirrors the duty action. */
472
+ action?: string
465
473
  /** How: executable (profile) name to run. 0–1; omitted when intent is
466
474
  * agent-only with no specific verb. */
467
475
  executable?: string
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.217",
3
+ "version": "0.4.219",
4
4
  "description": "kody — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
5
5
  "license": "MIT",
6
6
  "type": "module",