@kody-ade/kody-engine 0.4.218 → 0.4.220

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +3 -3
  2. package/dist/bin/kody.js +860 -632
  3. package/dist/duties/fix/duty.md +10 -0
  4. package/dist/duties/fix/profile.json +6 -0
  5. package/dist/duties/fix-ci/duty.md +10 -0
  6. package/dist/duties/fix-ci/profile.json +6 -0
  7. package/dist/duties/init/duty.md +10 -0
  8. package/dist/duties/init/profile.json +6 -0
  9. package/dist/duties/merge/duty.md +10 -0
  10. package/dist/duties/merge/profile.json +6 -0
  11. package/dist/duties/preview-build/duty.md +10 -0
  12. package/dist/duties/preview-build/profile.json +6 -0
  13. package/dist/duties/release/duty.md +10 -0
  14. package/dist/duties/release/profile.json +6 -0
  15. package/dist/duties/release-deploy/duty.md +10 -0
  16. package/dist/duties/release-deploy/profile.json +6 -0
  17. package/dist/duties/release-prepare/duty.md +10 -0
  18. package/dist/duties/release-prepare/profile.json +6 -0
  19. package/dist/duties/release-publish/duty.md +10 -0
  20. package/dist/duties/release-publish/profile.json +6 -0
  21. package/dist/duties/resolve/duty.md +10 -0
  22. package/dist/duties/resolve/profile.json +6 -0
  23. package/dist/duties/revert/duty.md +10 -0
  24. package/dist/duties/revert/profile.json +6 -0
  25. package/dist/duties/run/duty.md +10 -0
  26. package/dist/duties/run/profile.json +6 -0
  27. package/dist/duties/sync/duty.md +10 -0
  28. package/dist/duties/sync/profile.json +6 -0
  29. package/dist/duties/worker-ask/duty.md +10 -0
  30. package/dist/duties/worker-ask/profile.json +6 -0
  31. package/dist/executables/duty-scheduler/profile.json +1 -1
  32. package/dist/executables/duty-tick/profile.json +2 -2
  33. package/dist/executables/duty-tick-scripted/profile.json +3 -3
  34. package/dist/executables/goal-scheduler/scheduler.sh +0 -0
  35. package/dist/executables/release-deploy/deploy.sh +0 -0
  36. package/dist/executables/release-prepare/prepare.sh +0 -0
  37. package/dist/executables/release-publish/publish.sh +0 -0
  38. package/dist/executables/resolve/apply-prefer.sh +0 -0
  39. package/dist/executables/revert/revert.sh +0 -0
  40. package/dist/executables/types.ts +11 -3
  41. package/dist/executables/worker-ask/profile.json +1 -1
  42. package/package.json +22 -23
  43. /package/dist/jobs/watch-stale-prs/{prompt.md → duty.md} +0 -0
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.218",
18
+ version: "0.4.220",
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,68 +1981,224 @@ var init_agent = __esm({
1981
1981
  }
1982
1982
  });
1983
1983
 
1984
- // src/registry.ts
1984
+ // src/scripts/scheduleEvery.ts
1985
+ function isScheduleEvery(value) {
1986
+ return typeof value === "string" && SCHEDULE_EVERY_VALUES.includes(value);
1987
+ }
1988
+ function scheduleEveryToMs(every) {
1989
+ const MIN = 60 * 1e3;
1990
+ const HOUR = 60 * MIN;
1991
+ const DAY = 24 * HOUR;
1992
+ switch (every) {
1993
+ case "15m":
1994
+ return 15 * MIN;
1995
+ case "30m":
1996
+ return 30 * MIN;
1997
+ case "1h":
1998
+ return HOUR;
1999
+ case "2h":
2000
+ return 2 * HOUR;
2001
+ case "6h":
2002
+ return 6 * HOUR;
2003
+ case "12h":
2004
+ return 12 * HOUR;
2005
+ case "1d":
2006
+ return DAY;
2007
+ case "3d":
2008
+ return 3 * DAY;
2009
+ case "7d":
2010
+ return 7 * DAY;
2011
+ case "manual":
2012
+ return Number.POSITIVE_INFINITY;
2013
+ }
2014
+ }
2015
+ var SCHEDULE_EVERY_VALUES;
2016
+ var init_scheduleEvery = __esm({
2017
+ "src/scripts/scheduleEvery.ts"() {
2018
+ "use strict";
2019
+ SCHEDULE_EVERY_VALUES = [
2020
+ "15m",
2021
+ "30m",
2022
+ "1h",
2023
+ "2h",
2024
+ "6h",
2025
+ "12h",
2026
+ "1d",
2027
+ "3d",
2028
+ "7d",
2029
+ "manual"
2030
+ ];
2031
+ }
2032
+ });
2033
+
2034
+ // src/dutyFolders.ts
1985
2035
  import * as fs6 from "fs";
1986
2036
  import * as path6 from "path";
2037
+ function listDutyFolderSlugs(absDir) {
2038
+ if (!fs6.existsSync(absDir)) return [];
2039
+ let entries;
2040
+ try {
2041
+ entries = fs6.readdirSync(absDir, { withFileTypes: true });
2042
+ } catch {
2043
+ return [];
2044
+ }
2045
+ return entries.filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).filter((e) => isDutyFolder(path6.join(absDir, e.name))).map((e) => e.name).sort();
2046
+ }
2047
+ function isDutyFolder(dir) {
2048
+ return fs6.existsSync(path6.join(dir, DUTY_PROFILE_FILE)) && fs6.existsSync(path6.join(dir, DUTY_BODY_FILE));
2049
+ }
2050
+ function readDutyFolder(root, slug) {
2051
+ const dir = path6.join(root, slug);
2052
+ const profilePath = path6.join(dir, DUTY_PROFILE_FILE);
2053
+ const bodyPath = path6.join(dir, DUTY_BODY_FILE);
2054
+ if (!fs6.existsSync(profilePath) || !fs6.statSync(profilePath).isFile()) return null;
2055
+ if (!fs6.existsSync(bodyPath) || !fs6.statSync(bodyPath).isFile()) return null;
2056
+ try {
2057
+ const rawProfile = JSON.parse(fs6.readFileSync(profilePath, "utf-8"));
2058
+ const rawBody = fs6.readFileSync(bodyPath, "utf-8");
2059
+ const { title, body } = parseDutyBody(rawBody, slug);
2060
+ return {
2061
+ slug,
2062
+ dir,
2063
+ profilePath,
2064
+ bodyPath,
2065
+ title,
2066
+ body,
2067
+ rawBody,
2068
+ config: parseDutyConfig(rawProfile),
2069
+ rawProfile
2070
+ };
2071
+ } catch {
2072
+ return null;
2073
+ }
2074
+ }
2075
+ function parseDutyConfig(raw) {
2076
+ const tools = stringList(raw.tools ?? raw.dutyTools);
2077
+ return {
2078
+ action: stringField(raw.action),
2079
+ executable: stringField(raw.executable),
2080
+ every: isScheduleEvery(raw.every) ? raw.every : void 0,
2081
+ tickScript: stringField(raw.tickScript),
2082
+ disabled: typeof raw.disabled === "boolean" ? raw.disabled : void 0,
2083
+ staff: stringField(raw.staff),
2084
+ mentions: stringList(raw.mentions).map((m) => m.replace(/^@/, "")),
2085
+ tools,
2086
+ executables: stringList(raw.executables),
2087
+ describe: stringField(raw.describe),
2088
+ stage: stringField(raw.stage),
2089
+ readsFrom: stringList(raw.readsFrom ?? raw.reads_from),
2090
+ writesTo: stringList(raw.writesTo ?? raw.writes_to)
2091
+ };
2092
+ }
2093
+ function parseDutyBody(raw, slug) {
2094
+ const trimmed = raw.trim();
2095
+ const firstLine2 = trimmed.split("\n", 1)[0] ?? "";
2096
+ const h1 = /^#\s+(.+?)\s*$/.exec(firstLine2);
2097
+ const title = h1 ? h1[1].trim() : humanizeSlug(slug);
2098
+ const body = stripLeadingH1(raw);
2099
+ return { title, body };
2100
+ }
2101
+ function stripLeadingH1(raw) {
2102
+ const lines = raw.replace(/^\uFEFF/, "").split("\n");
2103
+ let i = 0;
2104
+ for (; ; ) {
2105
+ while (i < lines.length && lines[i].trim() === "") i++;
2106
+ if (i < lines.length && /^#\s+.+/.test(lines[i])) i++;
2107
+ else break;
2108
+ }
2109
+ return lines.slice(i).join("\n");
2110
+ }
2111
+ function humanizeSlug(slug) {
2112
+ return slug.split(/[-_]+/).filter(Boolean).map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
2113
+ }
2114
+ function stringField(value) {
2115
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
2116
+ }
2117
+ function stringList(value) {
2118
+ if (Array.isArray(value)) {
2119
+ return value.map((v) => String(v).trim()).filter(Boolean);
2120
+ }
2121
+ if (typeof value === "string") {
2122
+ return value.split(",").map((v) => v.trim()).filter(Boolean);
2123
+ }
2124
+ return [];
2125
+ }
2126
+ var DUTY_PROFILE_FILE, DUTY_BODY_FILE;
2127
+ var init_dutyFolders = __esm({
2128
+ "src/dutyFolders.ts"() {
2129
+ "use strict";
2130
+ init_scheduleEvery();
2131
+ DUTY_PROFILE_FILE = "profile.json";
2132
+ DUTY_BODY_FILE = "duty.md";
2133
+ }
2134
+ });
2135
+
2136
+ // src/registry.ts
2137
+ import * as fs7 from "fs";
2138
+ import * as path7 from "path";
1987
2139
  function getExecutablesRoot() {
1988
- const here = path6.dirname(new URL(import.meta.url).pathname);
2140
+ const here = path7.dirname(new URL(import.meta.url).pathname);
1989
2141
  const candidates = [
1990
- path6.join(here, "executables"),
2142
+ path7.join(here, "executables"),
1991
2143
  // dev: src/
1992
- path6.join(here, "..", "executables"),
2144
+ path7.join(here, "..", "executables"),
1993
2145
  // built: dist/bin → dist/executables
1994
- path6.join(here, "..", "src", "executables")
2146
+ path7.join(here, "..", "src", "executables")
1995
2147
  // fallback
1996
2148
  ];
1997
2149
  for (const c of candidates) {
1998
- if (fs6.existsSync(c) && fs6.statSync(c).isDirectory()) return c;
2150
+ if (fs7.existsSync(c) && fs7.statSync(c).isDirectory()) return c;
1999
2151
  }
2000
2152
  return candidates[0];
2001
2153
  }
2002
2154
  function getProjectExecutablesRoot() {
2003
- return path6.join(process.cwd(), ".kody", "executables");
2155
+ return path7.join(process.cwd(), ".kody", "executables");
2004
2156
  }
2005
2157
  function getProjectDutiesRoot() {
2006
- return path6.join(process.cwd(), ".kody", "duties");
2158
+ return path7.join(process.cwd(), ".kody", "duties");
2007
2159
  }
2008
2160
  function getBuiltinJobsRoot() {
2009
- const here = path6.dirname(new URL(import.meta.url).pathname);
2161
+ const here = path7.dirname(new URL(import.meta.url).pathname);
2010
2162
  const candidates = [
2011
- path6.join(here, "jobs"),
2163
+ path7.join(here, "jobs"),
2012
2164
  // dev: src/
2013
- path6.join(here, "..", "jobs"),
2165
+ path7.join(here, "..", "jobs"),
2014
2166
  // built: dist/bin → dist/jobs
2015
- path6.join(here, "..", "src", "jobs")
2167
+ path7.join(here, "..", "src", "jobs")
2168
+ // fallback
2169
+ ];
2170
+ for (const c of candidates) {
2171
+ if (fs7.existsSync(c) && fs7.statSync(c).isDirectory()) return c;
2172
+ }
2173
+ return candidates[0];
2174
+ }
2175
+ function getBuiltinDutiesRoot() {
2176
+ const here = path7.dirname(new URL(import.meta.url).pathname);
2177
+ const candidates = [
2178
+ path7.join(here, "duties"),
2179
+ // dev: src/
2180
+ path7.join(here, "..", "duties"),
2181
+ // built: dist/bin → dist/duties
2182
+ path7.join(here, "..", "src", "duties")
2016
2183
  // fallback
2017
2184
  ];
2018
2185
  for (const c of candidates) {
2019
- if (fs6.existsSync(c) && fs6.statSync(c).isDirectory()) return c;
2186
+ if (fs7.existsSync(c) && fs7.statSync(c).isDirectory()) return c;
2020
2187
  }
2021
2188
  return candidates[0];
2022
2189
  }
2023
2190
  function listBuiltinJobs(root = getBuiltinJobsRoot()) {
2024
- if (!fs6.existsSync(root) || !fs6.statSync(root).isDirectory()) return [];
2191
+ if (!fs7.existsSync(root) || !fs7.statSync(root).isDirectory()) return [];
2025
2192
  const out = [];
2026
- for (const ent of fs6.readdirSync(root, { withFileTypes: true })) {
2193
+ for (const ent of fs7.readdirSync(root, { withFileTypes: true })) {
2027
2194
  if (ent.name.startsWith("_") || ent.name.startsWith(".")) continue;
2028
- const full = path6.join(root, ent.name);
2195
+ const full = path7.join(root, ent.name);
2029
2196
  if (ent.isDirectory()) {
2030
- const profilePath = path6.join(full, "profile.json");
2031
- const promptPath = path6.join(full, "prompt.md");
2032
- if (!fs6.existsSync(profilePath) || !fs6.statSync(profilePath).isFile()) continue;
2033
- if (!fs6.existsSync(promptPath) || !fs6.statSync(promptPath).isFile()) continue;
2034
- out.push({ slug: ent.name, dir: full, profilePath, promptPath });
2035
- continue;
2036
- }
2037
- if (ent.isFile() && ent.name.endsWith(".md")) {
2038
- const slug = ent.name.slice(0, -3);
2039
- out.push({
2040
- slug,
2041
- dir: full,
2042
- profilePath: "",
2043
- promptPath: "",
2044
- filePath: full
2045
- });
2197
+ const profilePath = path7.join(full, DUTY_PROFILE_FILE);
2198
+ const bodyPath = path7.join(full, DUTY_BODY_FILE);
2199
+ if (!fs7.existsSync(profilePath) || !fs7.statSync(profilePath).isFile()) continue;
2200
+ if (!fs7.existsSync(bodyPath) || !fs7.statSync(bodyPath).isFile()) continue;
2201
+ out.push({ slug: ent.name, dir: full, profilePath, bodyPath });
2046
2202
  }
2047
2203
  }
2048
2204
  out.sort((a, b) => a.slug.localeCompare(b.slug));
@@ -2056,8 +2212,8 @@ function builtinExecutableNames() {
2056
2212
  const out = /* @__PURE__ */ new Set();
2057
2213
  const root = getExecutablesRoot();
2058
2214
  try {
2059
- for (const ent of fs6.readdirSync(root, { withFileTypes: true })) {
2060
- if (ent.isDirectory() && fs6.existsSync(path6.join(root, ent.name, "profile.json"))) out.add(ent.name);
2215
+ for (const ent of fs7.readdirSync(root, { withFileTypes: true })) {
2216
+ if (ent.isDirectory() && fs7.existsSync(path7.join(root, ent.name, "profile.json"))) out.add(ent.name);
2061
2217
  }
2062
2218
  } catch {
2063
2219
  }
@@ -2073,14 +2229,15 @@ function listExecutables(roots = getExecutableRoots()) {
2073
2229
  const seen = /* @__PURE__ */ new Set();
2074
2230
  const out = [];
2075
2231
  for (const root of rootList) {
2076
- if (!fs6.existsSync(root)) continue;
2077
- const entries = fs6.readdirSync(root, { withFileTypes: true });
2232
+ if (!fs7.existsSync(root)) continue;
2233
+ const entries = fs7.readdirSync(root, { withFileTypes: true });
2078
2234
  for (const ent of entries) {
2079
2235
  if (!ent.isDirectory()) continue;
2080
2236
  if (seen.has(ent.name)) continue;
2081
2237
  if (root === dutiesRoot && isBuiltinExecutable(ent.name)) continue;
2082
- const profilePath = path6.join(root, ent.name, "profile.json");
2083
- if (fs6.existsSync(profilePath) && fs6.statSync(profilePath).isFile()) {
2238
+ const profilePath = path7.join(root, ent.name, DUTY_PROFILE_FILE);
2239
+ if (root === dutiesRoot && !fs7.existsSync(path7.join(root, ent.name, DUTY_BODY_FILE))) continue;
2240
+ if (fs7.existsSync(profilePath) && fs7.statSync(profilePath).isFile()) {
2084
2241
  out.push({ name: ent.name, profilePath });
2085
2242
  seen.add(ent.name);
2086
2243
  }
@@ -2094,8 +2251,9 @@ function resolveExecutable(name, roots = getExecutableRoots()) {
2094
2251
  const dutiesRoot = getProjectDutiesRoot();
2095
2252
  for (const root of rootList) {
2096
2253
  if (root === dutiesRoot && isBuiltinExecutable(name)) continue;
2097
- const profilePath = path6.join(root, name, "profile.json");
2098
- if (fs6.existsSync(profilePath) && fs6.statSync(profilePath).isFile()) {
2254
+ if (root === dutiesRoot && !fs7.existsSync(path7.join(root, name, DUTY_BODY_FILE))) continue;
2255
+ const profilePath = path7.join(root, name, "profile.json");
2256
+ if (fs7.existsSync(profilePath) && fs7.statSync(profilePath).isFile()) {
2099
2257
  return profilePath;
2100
2258
  }
2101
2259
  }
@@ -2104,14 +2262,78 @@ function resolveExecutable(name, roots = getExecutableRoots()) {
2104
2262
  function hasExecutable(name, roots = getExecutableRoots()) {
2105
2263
  return resolveExecutable(name, roots) !== null;
2106
2264
  }
2265
+ function listDutyActions(projectDutiesRoot = getProjectDutiesRoot()) {
2266
+ const seen = /* @__PURE__ */ new Set();
2267
+ const out = [];
2268
+ const add = (action) => {
2269
+ if (!isSafeName(action.action) || !isSafeName(action.duty) || !isSafeName(action.executable)) return;
2270
+ if (seen.has(action.action)) return;
2271
+ seen.add(action.action);
2272
+ out.push(action);
2273
+ };
2274
+ for (const action of listProjectFolderDutyActions(projectDutiesRoot)) add(action);
2275
+ for (const action of listBuiltinDutyActions()) add(action);
2276
+ return out.sort((a, b) => a.action.localeCompare(b.action));
2277
+ }
2278
+ function resolveDutyAction(action, projectDutiesRoot = getProjectDutiesRoot()) {
2279
+ if (!isSafeName(action)) return null;
2280
+ return listDutyActions(projectDutiesRoot).find((d) => d.action === action) ?? null;
2281
+ }
2282
+ function hasDutyAction(action, projectDutiesRoot = getProjectDutiesRoot()) {
2283
+ return resolveDutyAction(action, projectDutiesRoot) !== null;
2284
+ }
2107
2285
  function isSafeName(name) {
2108
2286
  return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
2109
2287
  }
2288
+ function listProjectFolderDutyActions(root) {
2289
+ if (!fs7.existsSync(root) || !fs7.statSync(root).isDirectory()) return [];
2290
+ const out = [];
2291
+ for (const slug of listDutyFolderSlugs(root)) {
2292
+ if (!isSafeName(slug)) continue;
2293
+ const duty = readDutyFolder(root, slug);
2294
+ if (!duty) continue;
2295
+ const action = duty.config.action ?? slug;
2296
+ const executable = duty.config.executable ?? duty.config.executables?.[0] ?? (duty.config.tickScript ? "duty-tick-scripted" : "duty-tick");
2297
+ out.push({
2298
+ action,
2299
+ duty: slug,
2300
+ executable,
2301
+ cliArgs: duty.config.executable ? {} : { duty: slug },
2302
+ source: "project-folder",
2303
+ describe: duty.config.describe ?? duty.title,
2304
+ profilePath: duty.profilePath,
2305
+ bodyPath: duty.bodyPath
2306
+ });
2307
+ }
2308
+ return out.sort((a, b) => a.action.localeCompare(b.action));
2309
+ }
2310
+ function listBuiltinDutyActions(root = getBuiltinDutiesRoot()) {
2311
+ if (!fs7.existsSync(root) || !fs7.statSync(root).isDirectory()) return [];
2312
+ const out = [];
2313
+ for (const slug of listDutyFolderSlugs(root)) {
2314
+ if (!isSafeName(slug)) continue;
2315
+ const duty = readDutyFolder(root, slug);
2316
+ if (!duty) continue;
2317
+ const action = duty.config.action ?? slug;
2318
+ const executable = duty.config.executable ?? slug;
2319
+ out.push({
2320
+ action,
2321
+ duty: slug,
2322
+ executable,
2323
+ cliArgs: {},
2324
+ source: "builtin",
2325
+ describe: duty.config.describe ?? duty.title,
2326
+ profilePath: duty.profilePath,
2327
+ bodyPath: duty.bodyPath
2328
+ });
2329
+ }
2330
+ return out.sort((a, b) => a.action.localeCompare(b.action));
2331
+ }
2110
2332
  function getProfileInputs(name, roots = getExecutableRoots()) {
2111
2333
  const profilePath = resolveExecutable(name, roots);
2112
2334
  if (!profilePath) return null;
2113
2335
  try {
2114
- const raw = JSON.parse(fs6.readFileSync(profilePath, "utf-8"));
2336
+ const raw = JSON.parse(fs7.readFileSync(profilePath, "utf-8"));
2115
2337
  if (!raw || typeof raw !== "object" || !Array.isArray(raw.inputs)) return [];
2116
2338
  return raw.inputs;
2117
2339
  } catch {
@@ -2147,26 +2369,27 @@ var _builtinNames;
2147
2369
  var init_registry = __esm({
2148
2370
  "src/registry.ts"() {
2149
2371
  "use strict";
2372
+ init_dutyFolders();
2150
2373
  _builtinNames = null;
2151
2374
  }
2152
2375
  });
2153
2376
 
2154
2377
  // src/task-artifacts.ts
2155
- import fs7 from "fs";
2156
- import path7 from "path";
2378
+ import fs8 from "fs";
2379
+ import path8 from "path";
2157
2380
  function prepareTaskArtifactsDir(cwd, taskId) {
2158
2381
  const safeId = String(taskId).replace(/[^a-zA-Z0-9._-]/g, "_");
2159
- const relDir = path7.join(".kody", "tasks", safeId);
2160
- const absDir = path7.join(cwd, relDir);
2161
- fs7.mkdirSync(absDir, { recursive: true });
2382
+ const relDir = path8.join(".kody", "tasks", safeId);
2383
+ const absDir = path8.join(cwd, relDir);
2384
+ fs8.mkdirSync(absDir, { recursive: true });
2162
2385
  return { taskId: safeId, absDir, relDir };
2163
2386
  }
2164
2387
  function verifyTaskArtifacts(absDir) {
2165
2388
  const missing = [];
2166
2389
  for (const name of TASK_ARTIFACT_FILES) {
2167
- const full = path7.join(absDir, name);
2390
+ const full = path8.join(absDir, name);
2168
2391
  try {
2169
- const stat = fs7.statSync(full);
2392
+ const stat = fs8.statSync(full);
2170
2393
  if (!stat.isFile() || stat.size === 0) missing.push(name);
2171
2394
  } catch {
2172
2395
  missing.push(name);
@@ -2400,31 +2623,31 @@ var init_lifecycles = __esm({
2400
2623
  });
2401
2624
 
2402
2625
  // src/scripts/buildSyntheticPlugin.ts
2403
- import * as fs13 from "fs";
2626
+ import * as fs14 from "fs";
2404
2627
  import * as os2 from "os";
2405
- import * as path12 from "path";
2628
+ import * as path13 from "path";
2406
2629
  function getPluginsCatalogRoot() {
2407
- const here = path12.dirname(new URL(import.meta.url).pathname);
2630
+ const here = path13.dirname(new URL(import.meta.url).pathname);
2408
2631
  const candidates = [
2409
- path12.join(here, "..", "plugins"),
2632
+ path13.join(here, "..", "plugins"),
2410
2633
  // dev: src/scripts → src/plugins
2411
- path12.join(here, "..", "..", "plugins"),
2634
+ path13.join(here, "..", "..", "plugins"),
2412
2635
  // built: dist/scripts → dist/plugins
2413
- path12.join(here, "..", "..", "src", "plugins")
2636
+ path13.join(here, "..", "..", "src", "plugins")
2414
2637
  // fallback
2415
2638
  ];
2416
2639
  for (const c of candidates) {
2417
- if (fs13.existsSync(c) && fs13.statSync(c).isDirectory()) return c;
2640
+ if (fs14.existsSync(c) && fs14.statSync(c).isDirectory()) return c;
2418
2641
  }
2419
2642
  return candidates[0];
2420
2643
  }
2421
2644
  function copyDir(src, dst) {
2422
- fs13.mkdirSync(dst, { recursive: true });
2423
- for (const ent of fs13.readdirSync(src, { withFileTypes: true })) {
2424
- const s = path12.join(src, ent.name);
2425
- const d = path12.join(dst, ent.name);
2645
+ fs14.mkdirSync(dst, { recursive: true });
2646
+ for (const ent of fs14.readdirSync(src, { withFileTypes: true })) {
2647
+ const s = path13.join(src, ent.name);
2648
+ const d = path13.join(dst, ent.name);
2426
2649
  if (ent.isDirectory()) copyDir(s, d);
2427
- else if (ent.isFile()) fs13.copyFileSync(s, d);
2650
+ else if (ent.isFile()) fs14.copyFileSync(s, d);
2428
2651
  }
2429
2652
  }
2430
2653
  var buildSyntheticPlugin;
@@ -2437,45 +2660,45 @@ var init_buildSyntheticPlugin = __esm({
2437
2660
  if (!needsSynthetic) return;
2438
2661
  const catalog = getPluginsCatalogRoot();
2439
2662
  const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2440
- const root = path12.join(os2.tmpdir(), `kody-synth-${runId}`);
2441
- fs13.mkdirSync(path12.join(root, ".claude-plugin"), { recursive: true });
2663
+ const root = path13.join(os2.tmpdir(), `kody-synth-${runId}`);
2664
+ fs14.mkdirSync(path13.join(root, ".claude-plugin"), { recursive: true });
2442
2665
  const resolvePart = (bucket, entry) => {
2443
- const local = path12.join(profile.dir, bucket, entry);
2444
- if (fs13.existsSync(local)) return local;
2445
- const central = path12.join(catalog, bucket, entry);
2446
- if (fs13.existsSync(central)) return central;
2666
+ const local = path13.join(profile.dir, bucket, entry);
2667
+ if (fs14.existsSync(local)) return local;
2668
+ const central = path13.join(catalog, bucket, entry);
2669
+ if (fs14.existsSync(central)) return central;
2447
2670
  throw new Error(
2448
2671
  `buildSyntheticPlugin: ${bucket} entry '${entry}' not found in executable dir (${profile.dir}/${bucket}/) or catalog (${catalog}/${bucket}/)`
2449
2672
  );
2450
2673
  };
2451
2674
  if (cc.skills.length > 0) {
2452
- const dst = path12.join(root, "skills");
2453
- fs13.mkdirSync(dst, { recursive: true });
2675
+ const dst = path13.join(root, "skills");
2676
+ fs14.mkdirSync(dst, { recursive: true });
2454
2677
  for (const name of cc.skills) {
2455
- copyDir(resolvePart("skills", name), path12.join(dst, name));
2678
+ copyDir(resolvePart("skills", name), path13.join(dst, name));
2456
2679
  }
2457
2680
  }
2458
2681
  if (cc.commands.length > 0) {
2459
- const dst = path12.join(root, "commands");
2460
- fs13.mkdirSync(dst, { recursive: true });
2682
+ const dst = path13.join(root, "commands");
2683
+ fs14.mkdirSync(dst, { recursive: true });
2461
2684
  for (const name of cc.commands) {
2462
- fs13.copyFileSync(resolvePart("commands", `${name}.md`), path12.join(dst, `${name}.md`));
2685
+ fs14.copyFileSync(resolvePart("commands", `${name}.md`), path13.join(dst, `${name}.md`));
2463
2686
  }
2464
2687
  }
2465
2688
  if (cc.hooks.length > 0) {
2466
- const dst = path12.join(root, "hooks");
2467
- fs13.mkdirSync(dst, { recursive: true });
2689
+ const dst = path13.join(root, "hooks");
2690
+ fs14.mkdirSync(dst, { recursive: true });
2468
2691
  const merged = { hooks: {} };
2469
2692
  for (const name of cc.hooks) {
2470
2693
  const src = resolvePart("hooks", `${name}.json`);
2471
- const parsed = JSON.parse(fs13.readFileSync(src, "utf-8"));
2694
+ const parsed = JSON.parse(fs14.readFileSync(src, "utf-8"));
2472
2695
  for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
2473
2696
  if (!Array.isArray(entries)) continue;
2474
2697
  if (!merged.hooks[event]) merged.hooks[event] = [];
2475
2698
  merged.hooks[event].push(...entries);
2476
2699
  }
2477
2700
  }
2478
- fs13.writeFileSync(path12.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
2701
+ fs14.writeFileSync(path13.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
2479
2702
  `);
2480
2703
  }
2481
2704
  const manifest = {
@@ -2485,7 +2708,7 @@ var init_buildSyntheticPlugin = __esm({
2485
2708
  };
2486
2709
  if (cc.skills.length > 0) manifest.skills = ["./skills/"];
2487
2710
  if (cc.commands.length > 0) manifest.commands = ["./commands/"];
2488
- fs13.writeFileSync(path12.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
2711
+ fs14.writeFileSync(path13.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
2489
2712
  `);
2490
2713
  ctx.data.syntheticPluginPath = root;
2491
2714
  };
@@ -2493,8 +2716,8 @@ var init_buildSyntheticPlugin = __esm({
2493
2716
  });
2494
2717
 
2495
2718
  // src/subagents.ts
2496
- import * as fs14 from "fs";
2497
- import * as path13 from "path";
2719
+ import * as fs15 from "fs";
2720
+ import * as path14 from "path";
2498
2721
  function splitFrontmatter(raw) {
2499
2722
  const match = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(raw);
2500
2723
  if (!match) return { fm: {}, body: raw.trim() };
@@ -2507,10 +2730,10 @@ function splitFrontmatter(raw) {
2507
2730
  return { fm, body: (match[2] ?? "").trim() };
2508
2731
  }
2509
2732
  function resolveAgentFile(profileDir, name) {
2510
- const local = path13.join(profileDir, "agents", `${name}.md`);
2511
- if (fs14.existsSync(local)) return local;
2512
- const central = path13.join(getPluginsCatalogRoot(), "agents", `${name}.md`);
2513
- if (fs14.existsSync(central)) return central;
2733
+ const local = path14.join(profileDir, "agents", `${name}.md`);
2734
+ if (fs15.existsSync(local)) return local;
2735
+ const central = path14.join(getPluginsCatalogRoot(), "agents", `${name}.md`);
2736
+ if (fs15.existsSync(central)) return central;
2514
2737
  throw new Error(`loadSubagents: agent '${name}' not found in ${profileDir}/agents/ or shared catalog`);
2515
2738
  }
2516
2739
  function captureSubagentTemplates(profile) {
@@ -2519,7 +2742,7 @@ function captureSubagentTemplates(profile) {
2519
2742
  const out = {};
2520
2743
  for (const name of names) {
2521
2744
  try {
2522
- out[name] = fs14.readFileSync(resolveAgentFile(profile.dir, name), "utf-8");
2745
+ out[name] = fs15.readFileSync(resolveAgentFile(profile.dir, name), "utf-8");
2523
2746
  } catch {
2524
2747
  }
2525
2748
  }
@@ -2530,7 +2753,7 @@ function loadSubagents(profile) {
2530
2753
  if (!names || names.length === 0) return void 0;
2531
2754
  const agents = {};
2532
2755
  for (const name of names) {
2533
- const raw = profile.subagentTemplates?.[name] ?? fs14.readFileSync(resolveAgentFile(profile.dir, name), "utf-8");
2756
+ const raw = profile.subagentTemplates?.[name] ?? fs15.readFileSync(resolveAgentFile(profile.dir, name), "utf-8");
2534
2757
  const { fm, body } = splitFrontmatter(raw);
2535
2758
  if (!body) throw new Error(`loadSubagents: agent '${name}' has an empty prompt body`);
2536
2759
  const def = {
@@ -2554,15 +2777,15 @@ var init_subagents = __esm({
2554
2777
  });
2555
2778
 
2556
2779
  // src/profile.ts
2557
- import * as fs15 from "fs";
2558
- import * as path14 from "path";
2780
+ import * as fs16 from "fs";
2781
+ import * as path15 from "path";
2559
2782
  function loadProfile(profilePath) {
2560
- if (!fs15.existsSync(profilePath)) {
2783
+ if (!fs16.existsSync(profilePath)) {
2561
2784
  throw new ProfileError(profilePath, "file not found");
2562
2785
  }
2563
2786
  let raw;
2564
2787
  try {
2565
- raw = JSON.parse(fs15.readFileSync(profilePath, "utf-8"));
2788
+ raw = JSON.parse(fs16.readFileSync(profilePath, "utf-8"));
2566
2789
  } catch (err) {
2567
2790
  throw new ProfileError(profilePath, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
2568
2791
  }
@@ -2573,7 +2796,7 @@ function loadProfile(profilePath) {
2573
2796
  const unknownKeys = Object.keys(r).filter((k) => !KNOWN_PROFILE_KEYS.has(k));
2574
2797
  if (unknownKeys.length > 0) {
2575
2798
  process.stderr.write(
2576
- `[kody profile] ${path14.basename(path14.dirname(profilePath))}: unknown top-level keys ignored: ${unknownKeys.join(", ")}
2799
+ `[kody profile] ${path15.basename(path15.dirname(profilePath))}: unknown top-level keys ignored: ${unknownKeys.join(", ")}
2577
2800
  `
2578
2801
  );
2579
2802
  }
@@ -2587,11 +2810,12 @@ function loadProfile(profilePath) {
2587
2810
  return {
2588
2811
  ...base,
2589
2812
  name: requireString(profilePath, r, "name"),
2813
+ action: typeof r.action === "string" && r.action.trim() ? r.action.trim() : void 0,
2590
2814
  executable: execRef,
2591
2815
  describe: typeof r.describe === "string" ? r.describe : base.describe,
2592
2816
  staff: typeof r.staff === "string" && r.staff.trim() ? r.staff.trim() : base.staff,
2593
2817
  every: typeof r.every === "string" && r.every.trim() ? r.every.trim() : void 0,
2594
- dutyTools: Array.isArray(r.dutyTools) ? r.dutyTools.map((t) => String(t).trim()).filter(Boolean) : base.dutyTools,
2818
+ dutyTools: parseStringArray(r.dutyTools ?? r.tools) ?? base.dutyTools,
2595
2819
  mentions: Array.isArray(r.mentions) ? r.mentions.map((m) => String(m).trim()).filter(Boolean) : base.mentions
2596
2820
  };
2597
2821
  }
@@ -2630,14 +2854,15 @@ function loadProfile(profilePath) {
2630
2854
  }
2631
2855
  const profile = {
2632
2856
  name: requireString(profilePath, r, "name"),
2857
+ action: typeof r.action === "string" && r.action.trim() ? r.action.trim() : void 0,
2633
2858
  executable: void 0,
2634
2859
  describe: typeof r.describe === "string" ? r.describe : "",
2635
2860
  // Optional persona to run as. Empty/blank string → undefined (no persona).
2636
2861
  staff: typeof r.staff === "string" && r.staff.trim() ? r.staff.trim() : void 0,
2637
2862
  // Optional recurrence cadence (scheduled duty). Blank → undefined (on-demand).
2638
2863
  every: typeof r.every === "string" && r.every.trim() ? r.every.trim() : void 0,
2639
- // Locked-toolbox palette + mentions (folder-duty successors to frontmatter).
2640
- dutyTools: Array.isArray(r.dutyTools) ? r.dutyTools.map((t) => String(t).trim()).filter(Boolean) : void 0,
2864
+ // Locked-toolbox palette + mentions from folder-duty profile metadata.
2865
+ dutyTools: parseStringArray(r.dutyTools ?? r.tools),
2641
2866
  mentions: Array.isArray(r.mentions) ? r.mentions.map((m) => String(m).trim()).filter(Boolean) : void 0,
2642
2867
  role,
2643
2868
  kind,
@@ -2660,8 +2885,8 @@ function loadProfile(profilePath) {
2660
2885
  // Phase 5 in-process handoff opt-in. Default false; containers
2661
2886
  // flip to true after end-to-end verification.
2662
2887
  preloadContext: r.preloadContext === true,
2663
- dir: path14.dirname(profilePath),
2664
- promptTemplates: readPromptTemplates(path14.dirname(profilePath))
2888
+ dir: path15.dirname(profilePath),
2889
+ promptTemplates: readPromptTemplates(path15.dirname(profilePath))
2665
2890
  };
2666
2891
  if (lifecycle) {
2667
2892
  applyLifecycle(profile, profilePath);
@@ -2693,15 +2918,16 @@ function readPromptTemplates(dir) {
2693
2918
  const out = {};
2694
2919
  const read = (p) => {
2695
2920
  try {
2696
- out[p] = fs15.readFileSync(p, "utf-8");
2921
+ out[p] = fs16.readFileSync(p, "utf-8");
2697
2922
  } catch {
2698
2923
  }
2699
2924
  };
2700
- read(path14.join(dir, "prompt.md"));
2925
+ read(path15.join(dir, "prompt.md"));
2926
+ read(path15.join(dir, "duty.md"));
2701
2927
  try {
2702
- const promptsDir = path14.join(dir, "prompts");
2703
- for (const ent of fs15.readdirSync(promptsDir)) {
2704
- if (ent.endsWith(".md")) read(path14.join(promptsDir, ent));
2928
+ const promptsDir = path15.join(dir, "prompts");
2929
+ for (const ent of fs16.readdirSync(promptsDir)) {
2930
+ if (ent.endsWith(".md")) read(path15.join(promptsDir, ent));
2705
2931
  }
2706
2932
  } catch {
2707
2933
  }
@@ -2721,6 +2947,11 @@ function requireString(p, r, key) {
2721
2947
  }
2722
2948
  return v;
2723
2949
  }
2950
+ function parseStringArray(raw) {
2951
+ if (!Array.isArray(raw)) return void 0;
2952
+ const values = raw.map((t) => String(t).trim()).filter(Boolean);
2953
+ return values.length > 0 ? values : void 0;
2954
+ }
2724
2955
  function parseInputs(p, raw) {
2725
2956
  if (!Array.isArray(raw)) throw new ProfileError(p, `"inputs" must be an array`);
2726
2957
  const out = [];
@@ -2967,11 +3198,16 @@ var init_profile = __esm({
2967
3198
  VALID_PHASES = /* @__PURE__ */ new Set(["research", "planning", "implementing", "reviewing", "shipped", "failed", "idle"]);
2968
3199
  KNOWN_PROFILE_KEYS = /* @__PURE__ */ new Set([
2969
3200
  "name",
3201
+ "action",
2970
3202
  "executable",
2971
3203
  "staff",
2972
3204
  "every",
2973
3205
  "dutyTools",
3206
+ "tools",
2974
3207
  "mentions",
3208
+ "stage",
3209
+ "readsFrom",
3210
+ "writesTo",
2975
3211
  "describe",
2976
3212
  "role",
2977
3213
  "kind",
@@ -3368,16 +3604,16 @@ var init_state = __esm({
3368
3604
  });
3369
3605
 
3370
3606
  // src/prompt.ts
3371
- import * as fs16 from "fs";
3372
- import * as path15 from "path";
3607
+ import * as fs17 from "fs";
3608
+ import * as path16 from "path";
3373
3609
  function loadProjectConventions(projectDir) {
3374
3610
  const out = [];
3375
3611
  for (const rel of CONVENTION_FILES) {
3376
- const abs = path15.join(projectDir, rel);
3377
- if (!fs16.existsSync(abs)) continue;
3612
+ const abs = path16.join(projectDir, rel);
3613
+ if (!fs17.existsSync(abs)) continue;
3378
3614
  let content;
3379
3615
  try {
3380
- content = fs16.readFileSync(abs, "utf-8");
3616
+ content = fs17.readFileSync(abs, "utf-8");
3381
3617
  } catch {
3382
3618
  continue;
3383
3619
  }
@@ -3612,28 +3848,28 @@ var loadMemoryContext_exports = {};
3612
3848
  __export(loadMemoryContext_exports, {
3613
3849
  loadMemoryContext: () => loadMemoryContext
3614
3850
  });
3615
- import * as fs17 from "fs";
3616
- import * as path16 from "path";
3851
+ import * as fs18 from "fs";
3852
+ import * as path17 from "path";
3617
3853
  function collectPages(memoryAbs) {
3618
3854
  const out = [];
3619
3855
  walkMd(memoryAbs, (file) => {
3620
3856
  let stat;
3621
3857
  try {
3622
- stat = fs17.statSync(file);
3858
+ stat = fs18.statSync(file);
3623
3859
  } catch {
3624
3860
  return;
3625
3861
  }
3626
3862
  let raw;
3627
3863
  try {
3628
- raw = fs17.readFileSync(file, "utf-8");
3864
+ raw = fs18.readFileSync(file, "utf-8");
3629
3865
  } catch {
3630
3866
  return;
3631
3867
  }
3632
3868
  const fm = raw.match(/^---\s*\n([\s\S]*?)\n---/);
3633
- const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path16.basename(file, ".md");
3869
+ const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path17.basename(file, ".md");
3634
3870
  const updated = fm?.[1]?.match(/^updated:\s*([0-9T:.+\-Z]+)/m)?.[1]?.trim() ?? "";
3635
3871
  out.push({
3636
- relPath: path16.relative(memoryAbs, file),
3872
+ relPath: path17.relative(memoryAbs, file),
3637
3873
  title,
3638
3874
  updated,
3639
3875
  content: raw.length > PER_PAGE_MAX_BYTES ? raw.slice(0, PER_PAGE_MAX_BYTES) + TRUNCATED_SUFFIX2 : raw,
@@ -3701,16 +3937,16 @@ function walkMd(root, visit) {
3701
3937
  const dir = stack.pop();
3702
3938
  let names;
3703
3939
  try {
3704
- names = fs17.readdirSync(dir);
3940
+ names = fs18.readdirSync(dir);
3705
3941
  } catch {
3706
3942
  continue;
3707
3943
  }
3708
3944
  for (const name of names) {
3709
3945
  if (name.startsWith(".")) continue;
3710
- const full = path16.join(dir, name);
3946
+ const full = path17.join(dir, name);
3711
3947
  let stat;
3712
3948
  try {
3713
- stat = fs17.statSync(full);
3949
+ stat = fs18.statSync(full);
3714
3950
  } catch {
3715
3951
  continue;
3716
3952
  }
@@ -3733,8 +3969,8 @@ var init_loadMemoryContext = __esm({
3733
3969
  TRUNCATED_SUFFIX2 = "\n\n\u2026 (truncated)";
3734
3970
  loadMemoryContext = async (ctx) => {
3735
3971
  if (typeof ctx.data.memoryContext === "string") return;
3736
- const memoryAbs = path16.join(ctx.cwd, MEMORY_DIR_RELATIVE);
3737
- if (!fs17.existsSync(memoryAbs)) {
3972
+ const memoryAbs = path17.join(ctx.cwd, MEMORY_DIR_RELATIVE);
3973
+ if (!fs18.existsSync(memoryAbs)) {
3738
3974
  ctx.data.memoryContext = "";
3739
3975
  return;
3740
3976
  }
@@ -3778,11 +4014,11 @@ var init_loadCoverageRules = __esm({
3778
4014
 
3779
4015
  // src/container.ts
3780
4016
  import { execFileSync as execFileSync3 } from "child_process";
3781
- import * as fs18 from "fs";
4017
+ import * as fs19 from "fs";
3782
4018
  function getProfileInputsForChild(profileName, _cwd) {
3783
4019
  try {
3784
4020
  const profilePath = resolveProfilePath(profileName);
3785
- if (!fs18.existsSync(profilePath)) return null;
4021
+ if (!fs19.existsSync(profilePath)) return null;
3786
4022
  return loadProfile(profilePath).inputs;
3787
4023
  } catch {
3788
4024
  return null;
@@ -4244,9 +4480,9 @@ var init_lifecycleLabels = __esm({
4244
4480
 
4245
4481
  // src/litellm.ts
4246
4482
  import { execFileSync as execFileSync4, spawn as spawn3 } from "child_process";
4247
- import * as fs19 from "fs";
4483
+ import * as fs20 from "fs";
4248
4484
  import * as os3 from "os";
4249
- import * as path17 from "path";
4485
+ import * as path18 from "path";
4250
4486
  async function checkLitellmHealth(url) {
4251
4487
  try {
4252
4488
  const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
@@ -4331,13 +4567,13 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
4331
4567
  let child;
4332
4568
  let logPath;
4333
4569
  const spawnProxy = () => {
4334
- const configPath = path17.join(os3.tmpdir(), `kody-local-litellm-${Date.now()}.yaml`);
4335
- fs19.writeFileSync(configPath, generateLitellmConfigYaml(model));
4570
+ const configPath = path18.join(os3.tmpdir(), `kody-local-litellm-${Date.now()}.yaml`);
4571
+ fs20.writeFileSync(configPath, generateLitellmConfigYaml(model));
4336
4572
  const args = ["--config", configPath, "--port", port];
4337
- const nextLogPath = path17.join(os3.tmpdir(), `kody-local-litellm-${Date.now()}.log`);
4338
- const outFd = fs19.openSync(nextLogPath, "w");
4573
+ const nextLogPath = path18.join(os3.tmpdir(), `kody-local-litellm-${Date.now()}.log`);
4574
+ const outFd = fs20.openSync(nextLogPath, "w");
4339
4575
  child = spawn3(cmd, args, { stdio: ["ignore", outFd, outFd], detached: true, env: childEnv });
4340
- fs19.closeSync(outFd);
4576
+ fs20.closeSync(outFd);
4341
4577
  logPath = nextLogPath;
4342
4578
  };
4343
4579
  const waitForHealth = async () => {
@@ -4351,7 +4587,7 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
4351
4587
  const readLogTail = () => {
4352
4588
  if (!logPath) return "";
4353
4589
  try {
4354
- return fs19.readFileSync(logPath, "utf-8").slice(-2e3);
4590
+ return fs20.readFileSync(logPath, "utf-8").slice(-2e3);
4355
4591
  } catch {
4356
4592
  return "";
4357
4593
  }
@@ -4403,10 +4639,10 @@ ${tail}`
4403
4639
  return { url, kill: killChild, isHealthy, ensureHealthy };
4404
4640
  }
4405
4641
  function readDotenvApiKeys(projectDir) {
4406
- const dotenvPath = path17.join(projectDir, ".env");
4407
- if (!fs19.existsSync(dotenvPath)) return {};
4642
+ const dotenvPath = path18.join(projectDir, ".env");
4643
+ if (!fs20.existsSync(dotenvPath)) return {};
4408
4644
  const result = {};
4409
- for (const rawLine of fs19.readFileSync(dotenvPath, "utf-8").split("\n")) {
4645
+ for (const rawLine of fs20.readFileSync(dotenvPath, "utf-8").split("\n")) {
4410
4646
  const line = rawLine.trim();
4411
4647
  if (!line || line.startsWith("#")) continue;
4412
4648
  const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
@@ -4521,8 +4757,8 @@ var init_pushWithRetry = __esm({
4521
4757
 
4522
4758
  // src/commit.ts
4523
4759
  import { execFileSync as execFileSync6 } from "child_process";
4524
- import * as fs20 from "fs";
4525
- import * as path18 from "path";
4760
+ import * as fs21 from "fs";
4761
+ import * as path19 from "path";
4526
4762
  function git(args, cwd) {
4527
4763
  try {
4528
4764
  return execFileSync6("git", args, {
@@ -4560,18 +4796,18 @@ function ensureGitIdentity(cwd) {
4560
4796
  }
4561
4797
  function abortUnfinishedGitOps(cwd) {
4562
4798
  const aborted = [];
4563
- const gitDir = path18.join(cwd ?? process.cwd(), ".git");
4564
- if (!fs20.existsSync(gitDir)) return aborted;
4565
- if (fs20.existsSync(path18.join(gitDir, "MERGE_HEAD"))) {
4799
+ const gitDir = path19.join(cwd ?? process.cwd(), ".git");
4800
+ if (!fs21.existsSync(gitDir)) return aborted;
4801
+ if (fs21.existsSync(path19.join(gitDir, "MERGE_HEAD"))) {
4566
4802
  if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
4567
4803
  }
4568
- if (fs20.existsSync(path18.join(gitDir, "CHERRY_PICK_HEAD"))) {
4804
+ if (fs21.existsSync(path19.join(gitDir, "CHERRY_PICK_HEAD"))) {
4569
4805
  if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
4570
4806
  }
4571
- if (fs20.existsSync(path18.join(gitDir, "REVERT_HEAD"))) {
4807
+ if (fs21.existsSync(path19.join(gitDir, "REVERT_HEAD"))) {
4572
4808
  if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
4573
4809
  }
4574
- if (fs20.existsSync(path18.join(gitDir, "rebase-merge")) || fs20.existsSync(path18.join(gitDir, "rebase-apply"))) {
4810
+ if (fs21.existsSync(path19.join(gitDir, "rebase-merge")) || fs21.existsSync(path19.join(gitDir, "rebase-apply"))) {
4575
4811
  if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
4576
4812
  }
4577
4813
  try {
@@ -4627,7 +4863,7 @@ function normalizeCommitMessage(raw) {
4627
4863
  function commitAndPush(branch, agentMessage, cwd) {
4628
4864
  const allChanged = listChangedFiles(cwd);
4629
4865
  const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
4630
- const mergeHeadExists = fs20.existsSync(path18.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
4866
+ const mergeHeadExists = fs21.existsSync(path19.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
4631
4867
  if (allowedFiles.length === 0 && !mergeHeadExists) {
4632
4868
  return { committed: false, pushed: false, sha: "", message: "" };
4633
4869
  }
@@ -4864,7 +5100,7 @@ var init_advanceFlow = __esm({
4864
5100
 
4865
5101
  // src/gha.ts
4866
5102
  import { execFileSync as execFileSync8 } from "child_process";
4867
- import * as fs21 from "fs";
5103
+ import * as fs22 from "fs";
4868
5104
  function getRunUrl() {
4869
5105
  const server = process.env.GITHUB_SERVER_URL;
4870
5106
  const repo = process.env.GITHUB_REPOSITORY;
@@ -4875,10 +5111,10 @@ function getRunUrl() {
4875
5111
  function reactToTriggerComment(cwd) {
4876
5112
  if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
4877
5113
  const eventPath = process.env.GITHUB_EVENT_PATH;
4878
- if (!eventPath || !fs21.existsSync(eventPath)) return;
5114
+ if (!eventPath || !fs22.existsSync(eventPath)) return;
4879
5115
  let event = null;
4880
5116
  try {
4881
- event = JSON.parse(fs21.readFileSync(eventPath, "utf-8"));
5117
+ event = JSON.parse(fs22.readFileSync(eventPath, "utf-8"));
4882
5118
  } catch {
4883
5119
  return;
4884
5120
  }
@@ -5212,11 +5448,11 @@ var init_classifyByLabel = __esm({
5212
5448
  });
5213
5449
 
5214
5450
  // src/scripts/commitAndPush.ts
5215
- import * as fs22 from "fs";
5216
- import * as path19 from "path";
5451
+ import * as fs23 from "fs";
5452
+ import * as path20 from "path";
5217
5453
  function sentinelPathForStage(cwd, profileName) {
5218
5454
  const runId = resolveRunId();
5219
- return path19.join(cwd, ".kody", "runs", runId, `commit-${profileName}.lock`);
5455
+ return path20.join(cwd, ".kody", "runs", runId, `commit-${profileName}.lock`);
5220
5456
  }
5221
5457
  var DEFAULT_COMMIT_MESSAGE, commitAndPush2;
5222
5458
  var init_commitAndPush = __esm({
@@ -5233,9 +5469,9 @@ var init_commitAndPush = __esm({
5233
5469
  }
5234
5470
  const idempotencyEnabled = process.env.KODY_COMMIT_IDEMPOTENCY !== "0";
5235
5471
  const sentinel = idempotencyEnabled ? sentinelPathForStage(ctx.cwd, profile.name) : null;
5236
- if (sentinel && fs22.existsSync(sentinel)) {
5472
+ if (sentinel && fs23.existsSync(sentinel)) {
5237
5473
  try {
5238
- const replay = JSON.parse(fs22.readFileSync(sentinel, "utf-8"));
5474
+ const replay = JSON.parse(fs23.readFileSync(sentinel, "utf-8"));
5239
5475
  ctx.data.commitResult = replay.commitResult ?? { committed: false, pushed: false };
5240
5476
  if (Array.isArray(replay.changedFiles)) ctx.data.changedFiles = replay.changedFiles;
5241
5477
  if (typeof replay.hasCommitsAhead === "boolean") ctx.data.hasCommitsAhead = replay.hasCommitsAhead;
@@ -5288,8 +5524,8 @@ var init_commitAndPush = __esm({
5288
5524
  const result = ctx.data.commitResult;
5289
5525
  if (sentinel && result?.committed) {
5290
5526
  try {
5291
- fs22.mkdirSync(path19.dirname(sentinel), { recursive: true });
5292
- fs22.writeFileSync(
5527
+ fs23.mkdirSync(path20.dirname(sentinel), { recursive: true });
5528
+ fs23.writeFileSync(
5293
5529
  sentinel,
5294
5530
  JSON.stringify(
5295
5531
  {
@@ -5311,8 +5547,8 @@ var init_commitAndPush = __esm({
5311
5547
  });
5312
5548
 
5313
5549
  // src/goal/state.ts
5314
- import * as fs23 from "fs";
5315
- import * as path20 from "path";
5550
+ import * as fs24 from "fs";
5551
+ import * as path21 from "path";
5316
5552
  function parseGoalState(filePath, raw) {
5317
5553
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
5318
5554
  throw new GoalStateError(filePath, "must be a JSON object");
@@ -5364,10 +5600,10 @@ var init_state2 = __esm({
5364
5600
  "use strict";
5365
5601
  VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "awaiting-merge", "done"]);
5366
5602
  GoalStateError = class extends Error {
5367
- constructor(path42, message) {
5368
- super(`Invalid goal state at ${path42}:
5603
+ constructor(path43, message) {
5604
+ super(`Invalid goal state at ${path43}:
5369
5605
  ${message}`);
5370
- this.path = path42;
5606
+ this.path = path43;
5371
5607
  this.name = "GoalStateError";
5372
5608
  }
5373
5609
  path;
@@ -5479,8 +5715,8 @@ var init_commitGoalState = __esm({
5479
5715
  });
5480
5716
 
5481
5717
  // src/scripts/composePrompt.ts
5482
- import * as fs24 from "fs";
5483
- import * as path21 from "path";
5718
+ import * as fs25 from "fs";
5719
+ import * as path22 from "path";
5484
5720
  function fenceUntrusted(value) {
5485
5721
  if (value.trim().length === 0) return value;
5486
5722
  const safe = value.replace(/-{3,}\s*END UNTRUSTED INPUT\s*-{3,}/gi, "[END UNTRUSTED INPUT]");
@@ -5569,6 +5805,10 @@ function formatDutyReference(data, profileName) {
5569
5805
  if (dutySchedule) {
5570
5806
  lines.push(`- Cadence: \`${dutySchedule}\``);
5571
5807
  }
5808
+ const dutyBody = pickToken(data, "dutyIntent", "jobIntent");
5809
+ if (dutyBody) {
5810
+ lines.push("", "## Duty body", "", dutyBody);
5811
+ }
5572
5812
  if (lines.length === 2) {
5573
5813
  return "";
5574
5814
  }
@@ -5597,9 +5837,10 @@ var init_composePrompt = __esm({
5597
5837
  const explicit = ctx.data.promptTemplate;
5598
5838
  const mode = ctx.args.mode;
5599
5839
  const candidates = [
5600
- explicit ? path21.join(profile.dir, explicit) : null,
5601
- mode ? path21.join(profile.dir, "prompts", `${mode}.md`) : null,
5602
- path21.join(profile.dir, "prompt.md")
5840
+ explicit ? path22.join(profile.dir, explicit) : null,
5841
+ mode ? path22.join(profile.dir, "prompts", `${mode}.md`) : null,
5842
+ path22.join(profile.dir, "prompt.md"),
5843
+ path22.join(profile.dir, "duty.md")
5603
5844
  ].filter(Boolean);
5604
5845
  let templatePath = "";
5605
5846
  let template = "";
@@ -5612,7 +5853,7 @@ var init_composePrompt = __esm({
5612
5853
  break;
5613
5854
  }
5614
5855
  try {
5615
- template = fs24.readFileSync(c, "utf-8");
5856
+ template = fs25.readFileSync(c, "utf-8");
5616
5857
  templatePath = c;
5617
5858
  break;
5618
5859
  } catch (err) {
@@ -5623,7 +5864,7 @@ var init_composePrompt = __esm({
5623
5864
  if (!templatePath) {
5624
5865
  let dirState;
5625
5866
  try {
5626
- dirState = `dir contents: [${fs24.readdirSync(profile.dir).join(", ")}]`;
5867
+ dirState = `dir contents: [${fs25.readdirSync(profile.dir).join(", ")}]`;
5627
5868
  } catch (err) {
5628
5869
  dirState = `readdir(${profile.dir}) failed: ${err?.code ?? String(err)}`;
5629
5870
  }
@@ -6450,19 +6691,19 @@ var init_deriveQaScopeFromIssue = __esm({
6450
6691
 
6451
6692
  // src/scripts/diagMcp.ts
6452
6693
  import { execFileSync as execFileSync10 } from "child_process";
6453
- import * as fs25 from "fs";
6694
+ import * as fs26 from "fs";
6454
6695
  import * as os4 from "os";
6455
- import * as path22 from "path";
6696
+ import * as path23 from "path";
6456
6697
  var diagMcp;
6457
6698
  var init_diagMcp = __esm({
6458
6699
  "src/scripts/diagMcp.ts"() {
6459
6700
  "use strict";
6460
6701
  diagMcp = async (_ctx) => {
6461
6702
  const home = os4.homedir();
6462
- const cacheDir = path22.join(home, ".cache", "ms-playwright");
6703
+ const cacheDir = path23.join(home, ".cache", "ms-playwright");
6463
6704
  let entries = [];
6464
6705
  try {
6465
- entries = fs25.readdirSync(cacheDir);
6706
+ entries = fs26.readdirSync(cacheDir);
6466
6707
  } catch {
6467
6708
  }
6468
6709
  const hasChromium = entries.some((e) => e.startsWith("chromium"));
@@ -6490,13 +6731,13 @@ var init_diagMcp = __esm({
6490
6731
  });
6491
6732
 
6492
6733
  // src/scripts/frameworkDetectors.ts
6493
- import * as fs26 from "fs";
6494
- import * as path23 from "path";
6734
+ import * as fs27 from "fs";
6735
+ import * as path24 from "path";
6495
6736
  function detectFrameworks(cwd) {
6496
6737
  const out = [];
6497
6738
  let deps = {};
6498
6739
  try {
6499
- const pkg = JSON.parse(fs26.readFileSync(path23.join(cwd, "package.json"), "utf-8"));
6740
+ const pkg = JSON.parse(fs27.readFileSync(path24.join(cwd, "package.json"), "utf-8"));
6500
6741
  deps = { ...pkg.dependencies, ...pkg.devDependencies };
6501
6742
  } catch {
6502
6743
  return out;
@@ -6533,25 +6774,25 @@ function detectFrameworks(cwd) {
6533
6774
  }
6534
6775
  function findFile(cwd, candidates) {
6535
6776
  for (const c of candidates) {
6536
- if (fs26.existsSync(path23.join(cwd, c))) return c;
6777
+ if (fs27.existsSync(path24.join(cwd, c))) return c;
6537
6778
  }
6538
6779
  return null;
6539
6780
  }
6540
6781
  function discoverPayloadCollections(cwd) {
6541
6782
  const out = [];
6542
6783
  for (const dir of COLLECTION_DIRS) {
6543
- const full = path23.join(cwd, dir);
6544
- if (!fs26.existsSync(full)) continue;
6784
+ const full = path24.join(cwd, dir);
6785
+ if (!fs27.existsSync(full)) continue;
6545
6786
  let files;
6546
6787
  try {
6547
- files = fs26.readdirSync(full).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
6788
+ files = fs27.readdirSync(full).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
6548
6789
  } catch {
6549
6790
  continue;
6550
6791
  }
6551
6792
  for (const file of files) {
6552
6793
  try {
6553
- const filePath = path23.join(full, file);
6554
- const content = fs26.readFileSync(filePath, "utf-8").slice(0, 1e4);
6794
+ const filePath = path24.join(full, file);
6795
+ const content = fs27.readFileSync(filePath, "utf-8").slice(0, 1e4);
6555
6796
  const slugMatch = content.match(/slug:\s*['"]([a-z0-9-]+)['"]/);
6556
6797
  if (!slugMatch) continue;
6557
6798
  const slug = slugMatch[1];
@@ -6565,7 +6806,7 @@ function discoverPayloadCollections(cwd) {
6565
6806
  out.push({
6566
6807
  name,
6567
6808
  slug,
6568
- filePath: path23.relative(cwd, filePath),
6809
+ filePath: path24.relative(cwd, filePath),
6569
6810
  fields: fields.slice(0, 20),
6570
6811
  hasAdmin
6571
6812
  });
@@ -6578,28 +6819,28 @@ function discoverPayloadCollections(cwd) {
6578
6819
  function discoverAdminComponents(cwd, collections) {
6579
6820
  const out = [];
6580
6821
  for (const dir of ADMIN_COMPONENT_DIRS) {
6581
- const full = path23.join(cwd, dir);
6582
- if (!fs26.existsSync(full)) continue;
6822
+ const full = path24.join(cwd, dir);
6823
+ if (!fs27.existsSync(full)) continue;
6583
6824
  let entries;
6584
6825
  try {
6585
- entries = fs26.readdirSync(full, { withFileTypes: true });
6826
+ entries = fs27.readdirSync(full, { withFileTypes: true });
6586
6827
  } catch {
6587
6828
  continue;
6588
6829
  }
6589
6830
  for (const entry of entries) {
6590
- const entryPath = path23.join(full, entry.name);
6831
+ const entryPath = path24.join(full, entry.name);
6591
6832
  let name;
6592
6833
  let filePath;
6593
6834
  if (entry.isDirectory()) {
6594
6835
  const indexFile = ["index.tsx", "index.ts", "index.jsx", "index.js"].find(
6595
- (f) => fs26.existsSync(path23.join(entryPath, f))
6836
+ (f) => fs27.existsSync(path24.join(entryPath, f))
6596
6837
  );
6597
6838
  if (!indexFile) continue;
6598
6839
  name = entry.name;
6599
- filePath = path23.relative(cwd, path23.join(entryPath, indexFile));
6840
+ filePath = path24.relative(cwd, path24.join(entryPath, indexFile));
6600
6841
  } else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
6601
6842
  name = entry.name.replace(/\.(tsx?|jsx?)$/, "");
6602
- filePath = path23.relative(cwd, entryPath);
6843
+ filePath = path24.relative(cwd, entryPath);
6603
6844
  } else {
6604
6845
  continue;
6605
6846
  }
@@ -6607,7 +6848,7 @@ function discoverAdminComponents(cwd, collections) {
6607
6848
  if (collections) {
6608
6849
  for (const col of collections) {
6609
6850
  try {
6610
- const colContent = fs26.readFileSync(path23.join(cwd, col.filePath), "utf-8");
6851
+ const colContent = fs27.readFileSync(path24.join(cwd, col.filePath), "utf-8");
6611
6852
  if (colContent.includes(name)) {
6612
6853
  usedInCollection = col.slug;
6613
6854
  break;
@@ -6625,8 +6866,8 @@ function scanApiRoutes(cwd) {
6625
6866
  const out = [];
6626
6867
  const appDirs = ["src/app", "app"];
6627
6868
  for (const appDir of appDirs) {
6628
- const apiDir = path23.join(cwd, appDir, "api");
6629
- if (!fs26.existsSync(apiDir)) continue;
6869
+ const apiDir = path24.join(cwd, appDir, "api");
6870
+ if (!fs27.existsSync(apiDir)) continue;
6630
6871
  walkApiRoutes(apiDir, "/api", cwd, out);
6631
6872
  break;
6632
6873
  }
@@ -6635,14 +6876,14 @@ function scanApiRoutes(cwd) {
6635
6876
  function walkApiRoutes(dir, prefix, cwd, out) {
6636
6877
  let entries;
6637
6878
  try {
6638
- entries = fs26.readdirSync(dir, { withFileTypes: true });
6879
+ entries = fs27.readdirSync(dir, { withFileTypes: true });
6639
6880
  } catch {
6640
6881
  return;
6641
6882
  }
6642
6883
  const routeFile = entries.find((e) => e.isFile() && /^route\.(ts|js|tsx|jsx)$/.test(e.name));
6643
6884
  if (routeFile) {
6644
6885
  try {
6645
- const content = fs26.readFileSync(path23.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
6886
+ const content = fs27.readFileSync(path24.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
6646
6887
  const methods = HTTP_METHODS.filter(
6647
6888
  (m) => new RegExp(`export\\s+(?:async\\s+)?function\\s+${m}\\b`).test(content)
6648
6889
  );
@@ -6650,7 +6891,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
6650
6891
  out.push({
6651
6892
  path: prefix,
6652
6893
  methods,
6653
- filePath: path23.relative(cwd, path23.join(dir, routeFile.name))
6894
+ filePath: path24.relative(cwd, path24.join(dir, routeFile.name))
6654
6895
  });
6655
6896
  }
6656
6897
  } catch {
@@ -6661,7 +6902,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
6661
6902
  if (entry.name === "node_modules" || entry.name === ".next") continue;
6662
6903
  let segment = entry.name;
6663
6904
  if (segment.startsWith("(") && segment.endsWith(")")) {
6664
- walkApiRoutes(path23.join(dir, entry.name), prefix, cwd, out);
6905
+ walkApiRoutes(path24.join(dir, entry.name), prefix, cwd, out);
6665
6906
  continue;
6666
6907
  }
6667
6908
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -6669,16 +6910,16 @@ function walkApiRoutes(dir, prefix, cwd, out) {
6669
6910
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
6670
6911
  segment = `:${segment.slice(1, -1)}`;
6671
6912
  }
6672
- walkApiRoutes(path23.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
6913
+ walkApiRoutes(path24.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
6673
6914
  }
6674
6915
  }
6675
6916
  function scanEnvVars(cwd) {
6676
6917
  const candidates = [".env.example", ".env.local.example", ".env.template"];
6677
6918
  for (const envFile of candidates) {
6678
- const envPath = path23.join(cwd, envFile);
6679
- if (!fs26.existsSync(envPath)) continue;
6919
+ const envPath = path24.join(cwd, envFile);
6920
+ if (!fs27.existsSync(envPath)) continue;
6680
6921
  try {
6681
- const content = fs26.readFileSync(envPath, "utf-8");
6922
+ const content = fs27.readFileSync(envPath, "utf-8");
6682
6923
  const vars = [];
6683
6924
  for (const line of content.split("\n")) {
6684
6925
  const trimmed = line.trim();
@@ -6723,8 +6964,8 @@ var init_frameworkDetectors = __esm({
6723
6964
  });
6724
6965
 
6725
6966
  // src/scripts/discoverQaContext.ts
6726
- import * as fs27 from "fs";
6727
- import * as path24 from "path";
6967
+ import * as fs28 from "fs";
6968
+ import * as path25 from "path";
6728
6969
  function runQaDiscovery(cwd) {
6729
6970
  const out = {
6730
6971
  routes: [],
@@ -6755,9 +6996,9 @@ function runQaDiscovery(cwd) {
6755
6996
  }
6756
6997
  function detectDevServer(cwd, out) {
6757
6998
  try {
6758
- const pkg = JSON.parse(fs27.readFileSync(path24.join(cwd, "package.json"), "utf-8"));
6999
+ const pkg = JSON.parse(fs28.readFileSync(path25.join(cwd, "package.json"), "utf-8"));
6759
7000
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
6760
- const pm = fs27.existsSync(path24.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs27.existsSync(path24.join(cwd, "yarn.lock")) ? "yarn" : fs27.existsSync(path24.join(cwd, "bun.lockb")) ? "bun" : "npm";
7001
+ const pm = fs28.existsSync(path25.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs28.existsSync(path25.join(cwd, "yarn.lock")) ? "yarn" : fs28.existsSync(path25.join(cwd, "bun.lockb")) ? "bun" : "npm";
6761
7002
  if (pkg.scripts?.dev) out.devCommand = `${pm} dev`;
6762
7003
  if (allDeps.next || allDeps.nuxt) out.devPort = 3e3;
6763
7004
  else if (allDeps.vite) out.devPort = 5173;
@@ -6767,8 +7008,8 @@ function detectDevServer(cwd, out) {
6767
7008
  function scanFrontendRoutes(cwd, out) {
6768
7009
  const appDirs = ["src/app", "app"];
6769
7010
  for (const appDir of appDirs) {
6770
- const full = path24.join(cwd, appDir);
6771
- if (!fs27.existsSync(full)) continue;
7011
+ const full = path25.join(cwd, appDir);
7012
+ if (!fs28.existsSync(full)) continue;
6772
7013
  walkFrontendRoutes(full, "", out);
6773
7014
  break;
6774
7015
  }
@@ -6776,7 +7017,7 @@ function scanFrontendRoutes(cwd, out) {
6776
7017
  function walkFrontendRoutes(dir, prefix, out) {
6777
7018
  let entries;
6778
7019
  try {
6779
- entries = fs27.readdirSync(dir, { withFileTypes: true });
7020
+ entries = fs28.readdirSync(dir, { withFileTypes: true });
6780
7021
  } catch {
6781
7022
  return;
6782
7023
  }
@@ -6793,7 +7034,7 @@ function walkFrontendRoutes(dir, prefix, out) {
6793
7034
  if (entry.name === "node_modules" || entry.name === ".next") continue;
6794
7035
  let segment = entry.name;
6795
7036
  if (segment.startsWith("(") && segment.endsWith(")")) {
6796
- walkFrontendRoutes(path24.join(dir, entry.name), prefix, out);
7037
+ walkFrontendRoutes(path25.join(dir, entry.name), prefix, out);
6797
7038
  continue;
6798
7039
  }
6799
7040
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -6801,7 +7042,7 @@ function walkFrontendRoutes(dir, prefix, out) {
6801
7042
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
6802
7043
  segment = `:${segment.slice(1, -1)}`;
6803
7044
  }
6804
- walkFrontendRoutes(path24.join(dir, entry.name), `${prefix}/${segment}`, out);
7045
+ walkFrontendRoutes(path25.join(dir, entry.name), `${prefix}/${segment}`, out);
6805
7046
  }
6806
7047
  }
6807
7048
  function detectAuthFiles(cwd, out) {
@@ -6818,23 +7059,23 @@ function detectAuthFiles(cwd, out) {
6818
7059
  "src/app/api/oauth"
6819
7060
  ];
6820
7061
  for (const c of candidates) {
6821
- if (fs27.existsSync(path24.join(cwd, c))) out.authFiles.push(c);
7062
+ if (fs28.existsSync(path25.join(cwd, c))) out.authFiles.push(c);
6822
7063
  }
6823
7064
  }
6824
7065
  function detectRoles(cwd, out) {
6825
7066
  const rolePaths = ["src/types", "src/lib", "src/utils", "src/constants", "src/access", "src/collections"];
6826
7067
  for (const rp of rolePaths) {
6827
- const dir = path24.join(cwd, rp);
6828
- if (!fs27.existsSync(dir)) continue;
7068
+ const dir = path25.join(cwd, rp);
7069
+ if (!fs28.existsSync(dir)) continue;
6829
7070
  let files;
6830
7071
  try {
6831
- files = fs27.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
7072
+ files = fs28.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
6832
7073
  } catch {
6833
7074
  continue;
6834
7075
  }
6835
7076
  for (const f of files) {
6836
7077
  try {
6837
- const content = fs27.readFileSync(path24.join(dir, f), "utf-8").slice(0, 5e3);
7078
+ const content = fs28.readFileSync(path25.join(dir, f), "utf-8").slice(0, 5e3);
6838
7079
  const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
6839
7080
  if (roleMatches) {
6840
7081
  for (const m of roleMatches) {
@@ -7041,10 +7282,12 @@ ${stateBody}`;
7041
7282
 
7042
7283
  // src/jobIdentity.ts
7043
7284
  function stableJobKey(job) {
7044
- const executable = job.executable ?? job.duty ?? "unknown";
7285
+ const duty = job.duty ?? job.action;
7286
+ const executable = job.executable ?? duty ?? "unknown";
7045
7287
  if (job.flavor === "scheduled" && job.duty) return `scheduled:${job.duty}:${executable}`;
7046
7288
  const target = typeof job.target === "number" ? job.target : targetFromCliArgs(job.cliArgs);
7047
- return target === void 0 ? `${job.flavor}:${executable}` : `${job.flavor}:${executable}:${target}`;
7289
+ const work = duty ?? executable;
7290
+ return target === void 0 ? `${job.flavor}:${work}` : `${job.flavor}:${work}:${target}`;
7048
7291
  }
7049
7292
  function targetFromCliArgs(cliArgs) {
7050
7293
  if (!cliArgs) return void 0;
@@ -7083,8 +7326,8 @@ function validateJob(input) {
7083
7326
  throw new InvalidJobError("job must be an object");
7084
7327
  }
7085
7328
  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");
7329
+ if (typeof j.executable !== "string" && typeof j.duty !== "string" && typeof j.action !== "string") {
7330
+ throw new InvalidJobError("job must reference a duty action, duty, or executable");
7088
7331
  }
7089
7332
  if (j.flavor !== "instant" && j.flavor !== "scheduled") {
7090
7333
  throw new InvalidJobError(`job.flavor must be "instant" or "scheduled" (got ${String(j.flavor)})`);
@@ -7093,6 +7336,7 @@ function validateJob(input) {
7093
7336
  throw new InvalidJobError("job.cliArgs must be an object when present");
7094
7337
  }
7095
7338
  return {
7339
+ action: typeof j.action === "string" ? j.action : void 0,
7096
7340
  executable: typeof j.executable === "string" ? j.executable : void 0,
7097
7341
  duty: typeof j.duty === "string" ? j.duty : void 0,
7098
7342
  why: typeof j.why === "string" ? j.why : void 0,
@@ -7106,7 +7350,9 @@ function validateJob(input) {
7106
7350
  }
7107
7351
  async function runJob(job, base) {
7108
7352
  const valid = validateJob(job);
7109
- const profileName = valid.executable ?? valid.duty;
7353
+ const action = valid.action ?? valid.duty;
7354
+ const resolvedDuty = action ? resolveDutyAction(action) : null;
7355
+ const profileName = valid.executable ?? resolvedDuty?.executable ?? valid.duty;
7110
7356
  if (!profileName) {
7111
7357
  throw new InvalidJobError("job resolves to no executable or duty");
7112
7358
  }
@@ -7115,25 +7361,54 @@ async function runJob(job, base) {
7115
7361
  preloadedData.jobKey = stableJobKey(valid);
7116
7362
  preloadedData.jobFlavor = valid.flavor;
7117
7363
  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;
7364
+ if (valid.action !== void 0 && valid.action.length > 0) preloadedData.jobAction = valid.action;
7365
+ const dutyIdentity = valid.duty ?? resolvedDuty?.duty;
7366
+ if (dutyIdentity !== void 0 && dutyIdentity.length > 0) preloadedData.jobDuty = dutyIdentity;
7367
+ const executableIdentity = valid.executable ?? resolvedDuty?.executable;
7368
+ if (executableIdentity !== void 0 && executableIdentity.length > 0)
7369
+ preloadedData.jobExecutable = executableIdentity;
7120
7370
  if (valid.schedule !== void 0 && valid.schedule.length > 0) preloadedData.jobSchedule = valid.schedule;
7371
+ const dutyContext = loadDutyContext(dutyIdentity ?? valid.duty);
7372
+ if (dutyContext) {
7373
+ preloadedData.dutySlug = dutyContext.slug;
7374
+ preloadedData.dutyTitle = dutyContext.title;
7375
+ preloadedData.dutyIntent = dutyContext.body;
7376
+ preloadedData.jobIntent = dutyContext.body;
7377
+ if (preloadedData.jobDuty === void 0) preloadedData.jobDuty = dutyContext.slug;
7378
+ if (dutyContext.config.staff && preloadedData.jobPersona === void 0) {
7379
+ preloadedData.jobPersona = dutyContext.config.staff;
7380
+ }
7381
+ if (dutyContext.config.every && preloadedData.jobSchedule === void 0) {
7382
+ preloadedData.jobSchedule = dutyContext.config.every;
7383
+ }
7384
+ if (dutyContext.config.mentions && dutyContext.config.mentions.length > 0) {
7385
+ preloadedData.mentions = dutyContext.config.mentions.map((login) => `@${login}`).join(" ");
7386
+ }
7387
+ }
7121
7388
  if (valid.why !== void 0 && valid.why.length > 0) preloadedData.jobWhy = valid.why;
7122
7389
  if (valid.persona !== void 0) preloadedData.jobPersona = valid.persona;
7123
7390
  const input = {
7124
7391
  cliArgs: { ...valid.cliArgs },
7125
7392
  cwd: base.cwd,
7126
7393
  config: base.config,
7394
+ skipConfig: base.skipConfig,
7127
7395
  verbose: base.verbose,
7128
7396
  quiet: base.quiet,
7129
7397
  preloadedData: Object.keys(preloadedData).length > 0 ? preloadedData : void 0
7130
7398
  };
7399
+ input.cliArgs = resolvedDuty ? { ...resolvedDuty.cliArgs, ...input.cliArgs } : input.cliArgs;
7131
7400
  const run = base.chain === false ? runExecutable : runExecutableChain;
7132
7401
  return run(profileName, input);
7133
7402
  }
7403
+ function loadDutyContext(slug) {
7404
+ if (!slug) return null;
7405
+ return readDutyFolder(getProjectDutiesRoot(), slug) ?? readDutyFolder(getBuiltinDutiesRoot(), slug);
7406
+ }
7134
7407
  function mintInstantJob(dispatch2, opts) {
7135
7408
  return {
7409
+ action: dispatch2.action,
7136
7410
  executable: dispatch2.executable,
7411
+ duty: dispatch2.duty,
7137
7412
  why: opts?.why ?? dispatch2.why,
7138
7413
  persona: opts?.persona ?? DEFAULT_INSTANT_PERSONA,
7139
7414
  target: dispatch2.target,
@@ -7156,6 +7431,8 @@ var init_job = __esm({
7156
7431
  "src/job.ts"() {
7157
7432
  "use strict";
7158
7433
  init_executor();
7434
+ init_dutyFolders();
7435
+ init_registry();
7159
7436
  init_jobIdentity();
7160
7437
  init_jobIdentity();
7161
7438
  DEFAULT_INSTANT_PERSONA = "kody";
@@ -7169,106 +7446,6 @@ var init_job = __esm({
7169
7446
  }
7170
7447
  });
7171
7448
 
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
7449
  // src/scripts/issueStateComment.ts
7273
7450
  function isStateEnvelope(x) {
7274
7451
  if (x === null || typeof x !== "object") return false;
@@ -7486,8 +7663,8 @@ var init_contentsApiBackend = __esm({
7486
7663
  });
7487
7664
 
7488
7665
  // src/scripts/jobState/localFileBackend.ts
7489
- import * as fs28 from "fs";
7490
- import * as path25 from "path";
7666
+ import * as fs29 from "fs";
7667
+ import * as path26 from "path";
7491
7668
  function sanitizeKey(s) {
7492
7669
  return s.replace(/[^A-Za-z0-9._-]/g, "-");
7493
7670
  }
@@ -7543,7 +7720,7 @@ var init_localFileBackend = __esm({
7543
7720
  if (!opts.owner || !opts.repo) throw new Error("LocalFileBackend: owner and repo are required");
7544
7721
  this.cwd = opts.cwd;
7545
7722
  this.jobsDir = opts.jobsDir;
7546
- this.absDir = path25.join(opts.cwd, opts.jobsDir);
7723
+ this.absDir = path26.join(opts.cwd, opts.jobsDir);
7547
7724
  this.owner = opts.owner;
7548
7725
  this.repo = opts.repo;
7549
7726
  this.cache = opts.cache ?? defaultCacheAdapter();
@@ -7558,7 +7735,7 @@ var init_localFileBackend = __esm({
7558
7735
  `);
7559
7736
  return;
7560
7737
  }
7561
- fs28.mkdirSync(this.absDir, { recursive: true });
7738
+ fs29.mkdirSync(this.absDir, { recursive: true });
7562
7739
  const prefix = this.cacheKeyPrefix();
7563
7740
  const probeKey = `${prefix}probe-${Date.now()}`;
7564
7741
  try {
@@ -7587,7 +7764,7 @@ var init_localFileBackend = __esm({
7587
7764
  `);
7588
7765
  return;
7589
7766
  }
7590
- if (!fs28.existsSync(this.absDir)) {
7767
+ if (!fs29.existsSync(this.absDir)) {
7591
7768
  return;
7592
7769
  }
7593
7770
  const key = `${this.cacheKeyPrefix()}${process.env.GITHUB_RUN_ID ?? "norunid"}-${Date.now()}`;
@@ -7603,11 +7780,11 @@ var init_localFileBackend = __esm({
7603
7780
  }
7604
7781
  load(slug) {
7605
7782
  const relPath = stateFilePath(this.jobsDir, slug);
7606
- const absPath = path25.join(this.cwd, relPath);
7607
- if (!fs28.existsSync(absPath)) {
7783
+ const absPath = path26.join(this.cwd, relPath);
7784
+ if (!fs29.existsSync(absPath)) {
7608
7785
  return { path: relPath, handle: null, state: initialStateEnvelope("seed"), created: true };
7609
7786
  }
7610
- const raw = fs28.readFileSync(absPath, "utf-8");
7787
+ const raw = fs29.readFileSync(absPath, "utf-8");
7611
7788
  let parsed;
7612
7789
  try {
7613
7790
  parsed = JSON.parse(raw);
@@ -7624,13 +7801,13 @@ var init_localFileBackend = __esm({
7624
7801
  if (!loaded.created && isStateUnchanged(loaded.state, next)) {
7625
7802
  return false;
7626
7803
  }
7627
- const absPath = path25.join(this.cwd, loaded.path);
7628
- fs28.mkdirSync(path25.dirname(absPath), { recursive: true });
7804
+ const absPath = path26.join(this.cwd, loaded.path);
7805
+ fs29.mkdirSync(path26.dirname(absPath), { recursive: true });
7629
7806
  const body = `${JSON.stringify(next, null, 2)}
7630
7807
  `;
7631
7808
  const tmpPath = `${absPath}.${process.pid}.tmp`;
7632
- fs28.writeFileSync(tmpPath, body, "utf-8");
7633
- fs28.renameSync(tmpPath, absPath);
7809
+ fs29.writeFileSync(tmpPath, body, "utf-8");
7810
+ fs29.renameSync(tmpPath, absPath);
7634
7811
  return true;
7635
7812
  }
7636
7813
  cacheKeyPrefix() {
@@ -7782,8 +7959,7 @@ var init_planTaskJobs = __esm({
7782
7959
  });
7783
7960
 
7784
7961
  // src/scripts/dispatchDutyFileTicks.ts
7785
- import * as fs29 from "fs";
7786
- import * as path26 from "path";
7962
+ import * as path27 from "path";
7787
7963
  async function decideShouldFire(every, slug, backend, now) {
7788
7964
  if (!every) return { skip: false, reason: "no schedule (every cron tick)" };
7789
7965
  if (every === "manual") {
@@ -7824,14 +8000,6 @@ function formatAgo(ms) {
7824
8000
  const day = Math.round(hr / 24);
7825
8001
  return `${day}d`;
7826
8002
  }
7827
- function readJobFile(cwd, jobsDir, slug) {
7828
- try {
7829
- const raw = fs29.readFileSync(path26.join(cwd, jobsDir, `${slug}.md`), "utf-8");
7830
- return splitFrontmatter2(raw);
7831
- } catch {
7832
- return { frontmatter: {}, body: "" };
7833
- }
7834
- }
7835
8003
  function parseDutyFilter(raw) {
7836
8004
  return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : void 0;
7837
8005
  }
@@ -7840,21 +8008,21 @@ function filterSlugs(slugs, onlyDuty) {
7840
8008
  }
7841
8009
  function createDutyTaskIssue(opts) {
7842
8010
  const title = `Duty ${opts.slug} - multi-executable task`;
7843
- const body = buildDutyTaskIssueBody(opts.slug, opts.body, opts.frontmatter);
8011
+ const body = buildDutyTaskIssueBody(opts.slug, opts.body, opts.config);
7844
8012
  const out = gh(["issue", "create", "--title", title, "--body-file", "-"], { input: body, cwd: opts.cwd });
7845
8013
  const url = out.split("\n").map((line) => line.trim()).filter(Boolean).pop() ?? "";
7846
8014
  const match = url.match(/\/issues\/(\d+)\b/);
7847
8015
  if (!match) throw new Error(`gh issue create returned unexpected output: ${out}`);
7848
8016
  return { number: Number(match[1]), url };
7849
8017
  }
7850
- function buildDutyTaskIssueBody(slug, dutyBody, frontmatter) {
7851
- const specs = (frontmatter.executables ?? []).map((executable) => ({
8018
+ function buildDutyTaskIssueBody(slug, dutyBody, config) {
8019
+ const specs = (config.executables ?? []).map((executable) => ({
7852
8020
  executable,
7853
8021
  duty: slug,
7854
- ...frontmatter.staff ? { staff: frontmatter.staff } : {},
8022
+ ...config.staff ? { staff: config.staff } : {},
7855
8023
  reason: `Duty \`${slug}\` slice for \`${executable}\`.`,
7856
8024
  flavor: "scheduled",
7857
- ...frontmatter.every ? { schedule: frontmatter.every } : {}
8025
+ ...config.every ? { schedule: config.every } : {}
7858
8026
  }));
7859
8027
  return [
7860
8028
  `# Duty task: ${slug}`,
@@ -7867,26 +8035,6 @@ function buildDutyTaskIssueBody(slug, dutyBody, frontmatter) {
7867
8035
  ""
7868
8036
  ].join("\n");
7869
8037
  }
7870
- function listJobSlugs(absDir) {
7871
- if (!fs29.existsSync(absDir)) return [];
7872
- let entries;
7873
- try {
7874
- entries = fs29.readdirSync(absDir, { withFileTypes: true });
7875
- } catch {
7876
- return [];
7877
- }
7878
- return entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name.replace(/\.md$/, "")).filter((slug) => slug.length > 0 && !slug.startsWith("_") && !slug.startsWith(".")).sort();
7879
- }
7880
- function listFolderDutySlugs(absDir) {
7881
- if (!fs29.existsSync(absDir)) return [];
7882
- let entries;
7883
- try {
7884
- entries = fs29.readdirSync(absDir, { withFileTypes: true });
7885
- } catch {
7886
- return [];
7887
- }
7888
- return entries.filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).filter((e) => fs29.existsSync(path26.join(absDir, e.name, "profile.json"))).map((e) => e.name).sort();
7889
- }
7890
8038
  async function stampFired(backend, slug, now, task) {
7891
8039
  try {
7892
8040
  const loaded = await backend.load(slug);
@@ -7905,10 +8053,10 @@ var dispatchDutyFileTicks;
7905
8053
  var init_dispatchDutyFileTicks = __esm({
7906
8054
  "src/scripts/dispatchDutyFileTicks.ts"() {
7907
8055
  "use strict";
8056
+ init_dutyFolders();
7908
8057
  init_issue();
7909
8058
  init_job();
7910
- init_profile();
7911
- init_jobFrontmatter();
8059
+ init_scheduleEvery();
7912
8060
  init_jobState();
7913
8061
  init_planTaskJobs();
7914
8062
  dispatchDutyFileTicks = async (ctx, _profile, args) => {
@@ -7926,107 +8074,54 @@ var init_dispatchDutyFileTicks = __esm({
7926
8074
  }
7927
8075
  try {
7928
8076
  const onlyDuty = parseDutyFilter(ctx.args.duty);
7929
- const jobsPath = path26.join(ctx.cwd, jobsDir);
7930
- const slugs = filterSlugs(listJobSlugs(jobsPath), onlyDuty);
7931
- const folderSlugList = filterSlugs(listFolderDutySlugs(jobsPath), onlyDuty);
7932
- ctx.data.jobSlugCount = slugs.length + folderSlugList.length;
7933
- if (slugs.length === 0 && folderSlugList.length === 0) {
8077
+ const jobsPath = path27.join(ctx.cwd, jobsDir);
8078
+ const slugs = filterSlugs(listDutyFolderSlugs(jobsPath), onlyDuty);
8079
+ ctx.data.jobSlugCount = slugs.length;
8080
+ if (slugs.length === 0) {
7934
8081
  const filter = onlyDuty ? ` matching ${onlyDuty}` : "";
7935
- process.stdout.write(`[jobs] no job files${filter} in ${jobsDir}
8082
+ process.stdout.write(`[jobs] no duty folders${filter} in ${jobsDir}
7936
8083
  `);
7937
8084
  return;
7938
8085
  }
7939
8086
  const filtered = onlyDuty ? ` matching ${onlyDuty}` : "";
7940
- process.stdout.write(
7941
- `[jobs] ticking ${slugs.length + folderSlugList.length} job(s)${filtered} via ${targetExecutable}
7942
- `
7943
- );
8087
+ process.stdout.write(`[jobs] ticking ${slugs.length} dut(y/ies)${filtered} via ${targetExecutable}
8088
+ `);
7944
8089
  const results = [];
7945
8090
  const now = Date.now();
7946
- const folderDutySlugs = new Set(folderSlugList);
7947
- const scheduledDuties = folderSlugList.map((slug) => {
7948
- try {
7949
- const p = loadProfile(path26.join(ctx.cwd, jobsDir, slug, "profile.json"));
7950
- return { slug, every: p.every, staff: p.staff };
7951
- } catch (err) {
7952
- process.stderr.write(`[jobs] \u23ED skip folder-duty ${slug}: profile load failed: ${String(err)}
7953
- `);
7954
- return null;
7955
- }
7956
- }).filter((d) => d !== null && Boolean(d.every));
7957
- process.stdout.write(`[jobs] ${scheduledDuties.length} scheduled folder-dut(y/ies) to consider
7958
- `);
7959
- for (const { slug, every, staff } of scheduledDuties) {
7960
- if (!staff || staff.trim().length === 0) {
7961
- process.stderr.write(`[jobs] \u23ED skip ${slug}: scheduled duty has no staff
7962
- `);
7963
- results.push({ slug, exitCode: 0, skipped: true, reason: "no staff assigned" });
7964
- continue;
7965
- }
7966
- const decision = await decideShouldFire(every, slug, backend, now);
7967
- if (decision.skip) {
7968
- process.stdout.write(`[jobs] \u23ED skip ${slug}: ${decision.reason}
7969
- `);
7970
- results.push({ slug, exitCode: 0, skipped: true, reason: decision.reason });
7971
- continue;
7972
- }
7973
- await stampFired(backend, slug, now);
7974
- process.stdout.write(`[jobs] \u2192 run scheduled duty ${slug} (one-shot, as ${staff})
7975
- `);
7976
- try {
7977
- const out = await runJob(mintScheduledJob({ duty: slug, executable: slug, schedule: every }), {
7978
- cwd: ctx.cwd,
7979
- config: ctx.config,
7980
- verbose: ctx.verbose,
7981
- quiet: ctx.quiet,
7982
- chain: false
7983
- });
7984
- results.push({ slug, exitCode: out.exitCode, reason: out.reason });
7985
- if (out.exitCode !== 0) {
7986
- process.stderr.write(`[jobs] scheduled duty ${slug} failed (exit ${out.exitCode}): ${out.reason ?? ""}
7987
- `);
7988
- }
7989
- } catch (err) {
7990
- const msg = err instanceof Error ? err.message : String(err);
7991
- process.stderr.write(`[jobs] scheduled duty ${slug} crashed: ${msg}
7992
- `);
7993
- results.push({ slug, exitCode: 99, reason: msg });
7994
- }
7995
- }
7996
8091
  for (const slug of slugs) {
7997
- if (folderDutySlugs.has(slug)) {
7998
- process.stdout.write(`[jobs] \u23ED skip ${slug}: handled as folder-duty (folder wins over .md)
8092
+ const duty = readDutyFolder(jobsPath, slug);
8093
+ if (!duty) {
8094
+ process.stderr.write(`[jobs] \u23ED skip ${slug}: duty folder is missing profile.json or duty.md
7999
8095
  `);
8000
- results.push({ slug, exitCode: 0, skipped: true, reason: "handled as folder-duty" });
8096
+ results.push({ slug, exitCode: 0, skipped: true, reason: "incomplete duty folder" });
8001
8097
  continue;
8002
8098
  }
8003
- const dutyFile = readJobFile(ctx.cwd, jobsDir, slug);
8004
- const frontmatter = dutyFile.frontmatter;
8005
- if (frontmatter.disabled === true) {
8006
- process.stdout.write(`[jobs] \u23ED skip ${slug}: disabled in frontmatter
8099
+ const config = duty.config;
8100
+ if (config.disabled === true) {
8101
+ process.stdout.write(`[jobs] \u23ED skip ${slug}: disabled in profile.json
8007
8102
  `);
8008
8103
  results.push({ slug, exitCode: 0, skipped: true, reason: "disabled" });
8009
8104
  continue;
8010
8105
  }
8011
- if (!frontmatter.staff || frontmatter.staff.trim().length === 0) {
8012
- process.stderr.write(`[jobs] \u23ED skip ${slug}: no staff assigned (add 'staff: <slug>' frontmatter)
8106
+ if (!config.staff || config.staff.trim().length === 0) {
8107
+ process.stderr.write(`[jobs] \u23ED skip ${slug}: no staff assigned (add "staff" to profile.json)
8013
8108
  `);
8014
8109
  results.push({ slug, exitCode: 0, skipped: true, reason: "no staff assigned" });
8015
8110
  continue;
8016
8111
  }
8017
- const decision = await decideShouldFire(frontmatter.every, slug, backend, now);
8112
+ const decision = await decideShouldFire(config.every, slug, backend, now);
8018
8113
  if (decision.skip) {
8019
8114
  process.stdout.write(`[jobs] \u23ED skip ${slug}: ${decision.reason}
8020
8115
  `);
8021
8116
  results.push({ slug, exitCode: 0, skipped: true, reason: decision.reason });
8022
8117
  continue;
8023
8118
  }
8024
- if (frontmatter.executables && frontmatter.executables.length > 0) {
8119
+ if (config.executables && config.executables.length > 0) {
8025
8120
  try {
8026
8121
  const task = createDutyTaskIssue({
8027
8122
  slug,
8028
- body: dutyFile.body,
8029
- frontmatter,
8123
+ body: duty.body,
8124
+ config,
8030
8125
  cwd: ctx.cwd
8031
8126
  });
8032
8127
  await stampFired(backend, slug, now, task);
@@ -8036,8 +8131,8 @@ var init_dispatchDutyFileTicks = __esm({
8036
8131
  mintScheduledJob({
8037
8132
  duty: slug,
8038
8133
  executable: "task-jobs",
8039
- schedule: frontmatter.every,
8040
- persona: frontmatter.staff,
8134
+ schedule: config.every,
8135
+ persona: config.staff,
8041
8136
  cliArgs: { issue: task.number }
8042
8137
  }),
8043
8138
  { cwd: ctx.cwd, config: ctx.config, verbose: ctx.verbose, quiet: ctx.quiet }
@@ -8055,7 +8150,8 @@ var init_dispatchDutyFileTicks = __esm({
8055
8150
  }
8056
8151
  continue;
8057
8152
  }
8058
- const slugTarget = frontmatter.tickScript ? scriptedExecutable : targetExecutable;
8153
+ const slugTarget = config.tickScript ? scriptedExecutable : config.executable ?? targetExecutable;
8154
+ const cliArgs = config.executable ? {} : { [slugArg]: slug };
8059
8155
  process.stdout.write(`[jobs] \u2192 tick ${slug} (${slugTarget})
8060
8156
  `);
8061
8157
  try {
@@ -8063,8 +8159,9 @@ var init_dispatchDutyFileTicks = __esm({
8063
8159
  mintScheduledJob({
8064
8160
  duty: slug,
8065
8161
  executable: slugTarget,
8066
- schedule: frontmatter.every,
8067
- cliArgs: { [slugArg]: slug }
8162
+ schedule: config.every,
8163
+ persona: config.staff,
8164
+ cliArgs
8068
8165
  }),
8069
8166
  { cwd: ctx.cwd, config: ctx.config, verbose: ctx.verbose, quiet: ctx.quiet, chain: false }
8070
8167
  );
@@ -9307,11 +9404,11 @@ var init_handleAbandonedGoal = __esm({
9307
9404
  // src/scripts/initFlow.ts
9308
9405
  import { execFileSync as execFileSync16 } from "child_process";
9309
9406
  import * as fs30 from "fs";
9310
- import * as path27 from "path";
9407
+ import * as path28 from "path";
9311
9408
  function detectPackageManager(cwd) {
9312
- if (fs30.existsSync(path27.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
9313
- if (fs30.existsSync(path27.join(cwd, "yarn.lock"))) return "yarn";
9314
- if (fs30.existsSync(path27.join(cwd, "bun.lockb"))) return "bun";
9409
+ if (fs30.existsSync(path28.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
9410
+ if (fs30.existsSync(path28.join(cwd, "yarn.lock"))) return "yarn";
9411
+ if (fs30.existsSync(path28.join(cwd, "bun.lockb"))) return "bun";
9315
9412
  return "npm";
9316
9413
  }
9317
9414
  function qualityCommandsFor(pm) {
@@ -9383,7 +9480,7 @@ function performInit(cwd, force) {
9383
9480
  const pm = detectPackageManager(cwd);
9384
9481
  const ownerRepo = detectOwnerRepo(cwd);
9385
9482
  const defaultBranch2 = defaultBranchFromGit(cwd);
9386
- const configPath = path27.join(cwd, "kody.config.json");
9483
+ const configPath = path28.join(cwd, "kody.config.json");
9387
9484
  if (fs30.existsSync(configPath) && !force) {
9388
9485
  skipped.push("kody.config.json");
9389
9486
  } else {
@@ -9392,8 +9489,8 @@ function performInit(cwd, force) {
9392
9489
  `);
9393
9490
  wrote.push("kody.config.json");
9394
9491
  }
9395
- const workflowDir = path27.join(cwd, ".github", "workflows");
9396
- const workflowPath = path27.join(workflowDir, "kody.yml");
9492
+ const workflowDir = path28.join(cwd, ".github", "workflows");
9493
+ const workflowPath = path28.join(workflowDir, "kody.yml");
9397
9494
  if (fs30.existsSync(workflowPath) && !force) {
9398
9495
  skipped.push(".github/workflows/kody.yml");
9399
9496
  } else {
@@ -9403,37 +9500,26 @@ function performInit(cwd, force) {
9403
9500
  }
9404
9501
  const builtinJobs = listBuiltinJobs();
9405
9502
  if (builtinJobs.length > 0) {
9406
- const jobsDir = path27.join(cwd, ".kody", "duties");
9503
+ const jobsDir = path28.join(cwd, ".kody", "duties");
9407
9504
  fs30.mkdirSync(jobsDir, { recursive: true });
9408
9505
  for (const job of builtinJobs) {
9409
- if (job.filePath && !job.profilePath) {
9410
- const rel = path27.join(".kody", "duties", `${job.slug}.md`);
9411
- const target = path27.join(cwd, rel);
9412
- if (fs30.existsSync(target) && !force) {
9413
- skipped.push(rel);
9414
- continue;
9415
- }
9416
- fs30.writeFileSync(target, fs30.readFileSync(job.filePath, "utf-8"));
9417
- wrote.push(rel);
9418
- continue;
9419
- }
9420
- const targetDir = path27.join(jobsDir, job.slug);
9421
- const relProfile = path27.join(".kody", "duties", job.slug, "profile.json");
9422
- const relPrompt = path27.join(".kody", "duties", job.slug, "prompt.md");
9423
- if (fs30.existsSync(targetDir) && fs30.existsSync(path27.join(targetDir, "profile.json")) && !force) {
9506
+ const targetDir = path28.join(jobsDir, job.slug);
9507
+ const relProfile = path28.join(".kody", "duties", job.slug, "profile.json");
9508
+ const relBody = path28.join(".kody", "duties", job.slug, "duty.md");
9509
+ if (fs30.existsSync(targetDir) && fs30.existsSync(path28.join(targetDir, "profile.json")) && !force) {
9424
9510
  skipped.push(relProfile);
9425
- skipped.push(relPrompt);
9511
+ skipped.push(relBody);
9426
9512
  continue;
9427
9513
  }
9428
9514
  fs30.mkdirSync(targetDir, { recursive: true });
9429
- fs30.writeFileSync(path27.join(targetDir, "profile.json"), fs30.readFileSync(job.profilePath, "utf-8"));
9430
- fs30.writeFileSync(path27.join(targetDir, "prompt.md"), fs30.readFileSync(job.promptPath, "utf-8"));
9515
+ fs30.writeFileSync(path28.join(targetDir, "profile.json"), fs30.readFileSync(job.profilePath, "utf-8"));
9516
+ fs30.writeFileSync(path28.join(targetDir, "duty.md"), fs30.readFileSync(job.bodyPath, "utf-8"));
9431
9517
  wrote.push(relProfile);
9432
- wrote.push(relPrompt);
9518
+ wrote.push(relBody);
9433
9519
  }
9434
9520
  }
9435
- const staffDir = path27.join(cwd, ".kody", "staff");
9436
- const staffPath = path27.join(staffDir, "kody.md");
9521
+ const staffDir = path28.join(cwd, ".kody", "staff");
9522
+ const staffPath = path28.join(staffDir, "kody.md");
9437
9523
  if (fs30.existsSync(staffPath) && !force) {
9438
9524
  skipped.push(".kody/staff/kody.md");
9439
9525
  } else {
@@ -9449,7 +9535,7 @@ function performInit(cwd, force) {
9449
9535
  continue;
9450
9536
  }
9451
9537
  if (profile.kind !== "scheduled" || !profile.schedule) continue;
9452
- const target = path27.join(workflowDir, `kody-${exe.name}.yml`);
9538
+ const target = path28.join(workflowDir, `kody-${exe.name}.yml`);
9453
9539
  if (fs30.existsSync(target) && !force) {
9454
9540
  skipped.push(`.github/workflows/kody-${exe.name}.yml`);
9455
9541
  continue;
@@ -9775,7 +9861,7 @@ var init_loadIssueStateComment = __esm({
9775
9861
 
9776
9862
  // src/scripts/loadJobFromFile.ts
9777
9863
  import * as fs31 from "fs";
9778
- import * as path28 from "path";
9864
+ import * as path29 from "path";
9779
9865
  function parseJobFile(raw, slug) {
9780
9866
  let stripped = raw;
9781
9867
  if (stripped.startsWith("---\n")) {
@@ -9791,17 +9877,17 @@ function parseJobFile(raw, slug) {
9791
9877
  const rest = trimmed.slice(firstLine2.length).replace(/^\n+/, "");
9792
9878
  return { title: h1[1].trim(), body: rest };
9793
9879
  }
9794
- return { title: humanizeSlug(slug), body: trimmed };
9880
+ return { title: humanizeSlug2(slug), body: trimmed };
9795
9881
  }
9796
- function humanizeSlug(slug) {
9882
+ function humanizeSlug2(slug) {
9797
9883
  return slug.split(/[-_]+/).filter((s) => s.length > 0).map((s) => s[0].toUpperCase() + s.slice(1)).join(" ");
9798
9884
  }
9799
9885
  var DUTY_TOOL_PALETTE2, loadJobFromFile;
9800
9886
  var init_loadJobFromFile = __esm({
9801
9887
  "src/scripts/loadJobFromFile.ts"() {
9802
9888
  "use strict";
9889
+ init_dutyFolders();
9803
9890
  init_dutyMcp();
9804
- init_jobFrontmatter();
9805
9891
  init_jobState();
9806
9892
  DUTY_TOOL_PALETTE2 = new Set(DUTY_MCP_TOOL_NAMES);
9807
9893
  loadJobFromFile = async (ctx, profile, args) => {
@@ -9812,19 +9898,17 @@ var init_loadJobFromFile = __esm({
9812
9898
  if (!slug) {
9813
9899
  throw new Error(`loadJobFromFile: ctx.args.${slugArg} must be a non-empty slug`);
9814
9900
  }
9815
- const absPath = path28.join(ctx.cwd, jobsDir, `${slug}.md`);
9816
- if (!fs31.existsSync(absPath)) {
9817
- throw new Error(`loadJobFromFile: job file not found: ${absPath}`);
9901
+ const duty = readDutyFolder(path29.join(ctx.cwd, jobsDir), slug);
9902
+ if (!duty) {
9903
+ throw new Error(`loadJobFromFile: duty folder not found or incomplete: ${path29.join(ctx.cwd, jobsDir, slug)}`);
9818
9904
  }
9819
- const raw = fs31.readFileSync(absPath, "utf-8");
9820
- const { title, body } = parseJobFile(raw, slug);
9821
- const frontmatter = splitFrontmatter2(raw).frontmatter;
9822
- const mentions = (frontmatter.mentions ?? []).map((login) => `@${login}`).join(" ");
9823
- const workerSlug = (frontmatter.staff ?? "").trim();
9905
+ const { title, body, config } = duty;
9906
+ const mentions = (config.mentions ?? []).map((login) => `@${login}`).join(" ");
9907
+ const workerSlug = (config.staff ?? "").trim();
9824
9908
  let workerTitle = "";
9825
9909
  let workerPersona = "";
9826
9910
  if (workerSlug) {
9827
- const workerPath = path28.join(ctx.cwd, workersDir, `${workerSlug}.md`);
9911
+ const workerPath = path29.join(ctx.cwd, workersDir, `${workerSlug}.md`);
9828
9912
  if (!fs31.existsSync(workerPath)) {
9829
9913
  throw new Error(`loadJobFromFile: duty '${slug}' declares staff '${workerSlug}' but ${workerPath} does not exist`);
9830
9914
  }
@@ -9849,8 +9933,8 @@ var init_loadJobFromFile = __esm({
9849
9933
  ctx.data.staffSlug = workerSlug;
9850
9934
  ctx.data.staffTitle = workerTitle;
9851
9935
  ctx.data.executableSlug = profile.name;
9852
- ctx.data.dutySchedule = "";
9853
- const declaredTools = frontmatter.tools ?? [];
9936
+ ctx.data.dutySchedule = config.every ?? "";
9937
+ const declaredTools = config.tools ?? [];
9854
9938
  if (declaredTools.length > 0) {
9855
9939
  const unknown = declaredTools.filter((name) => !DUTY_TOOL_PALETTE2.has(name));
9856
9940
  if (unknown.length > 0) {
@@ -9903,9 +9987,9 @@ ${truncate2(issue.body, FINDING_BODY_MAX_BYTES)}`;
9903
9987
 
9904
9988
  // src/scripts/kodyVariables.ts
9905
9989
  import * as fs32 from "fs";
9906
- import * as path29 from "path";
9990
+ import * as path30 from "path";
9907
9991
  function readKodyVariables(cwd) {
9908
- const full = path29.join(cwd, KODY_VARIABLES_REL_PATH);
9992
+ const full = path30.join(cwd, KODY_VARIABLES_REL_PATH);
9909
9993
  let raw;
9910
9994
  try {
9911
9995
  raw = fs32.readFileSync(full, "utf-8");
@@ -9934,7 +10018,7 @@ var init_kodyVariables = __esm({
9934
10018
 
9935
10019
  // src/scripts/loadQaContext.ts
9936
10020
  import * as fs33 from "fs";
9937
- import * as path30 from "path";
10021
+ import * as path31 from "path";
9938
10022
  function parseSlugList(value) {
9939
10023
  const inner = value.startsWith("[") && value.endsWith("]") ? value.slice(1, -1) : value;
9940
10024
  return inner.split(",").map(
@@ -9942,7 +10026,7 @@ function parseSlugList(value) {
9942
10026
  ).filter(Boolean);
9943
10027
  }
9944
10028
  function readProfileStaff(raw) {
9945
- const m = FRONTMATTER_RE2.exec(raw);
10029
+ const m = FRONTMATTER_RE.exec(raw);
9946
10030
  if (!m) return { staff: ["kody"], body: raw };
9947
10031
  const body = raw.slice(m[0].length);
9948
10032
  let staff = null;
@@ -9963,7 +10047,7 @@ function readProfileStaff(raw) {
9963
10047
  return { staff: staff ?? legacy ?? ["kody"], body };
9964
10048
  }
9965
10049
  function readProfile(cwd) {
9966
- const dir = path30.join(cwd, CONTEXT_DIR_REL_PATH);
10050
+ const dir = path31.join(cwd, CONTEXT_DIR_REL_PATH);
9967
10051
  if (!fs33.existsSync(dir)) return "";
9968
10052
  let entries;
9969
10053
  try {
@@ -9974,7 +10058,7 @@ function readProfile(cwd) {
9974
10058
  const blocks = [];
9975
10059
  for (const file of entries) {
9976
10060
  try {
9977
- const raw = fs33.readFileSync(path30.join(dir, file), "utf-8");
10061
+ const raw = fs33.readFileSync(path31.join(dir, file), "utf-8");
9978
10062
  const { staff, body } = readProfileStaff(raw);
9979
10063
  if (!staff.includes(QA_STAFF) && !staff.includes(ALL_STAFF)) continue;
9980
10064
  blocks.push(`## ${file}
@@ -9997,7 +10081,7 @@ function composeAuthBlock(authProfile, login, password) {
9997
10081
  }
9998
10082
  return "Auth: no QA credentials configured (set the `LOGIN_USER` variable and the `LOGIN_PASSWORD` vault secret). Browse public routes only; note auth-gated surfaces as gaps.";
9999
10083
  }
10000
- var CONTEXT_DIR_REL_PATH, QA_STAFF, ALL_STAFF, LEGACY_AUDIENCE_TO_STAFF, FRONTMATTER_RE2, loadQaContext;
10084
+ var CONTEXT_DIR_REL_PATH, QA_STAFF, ALL_STAFF, LEGACY_AUDIENCE_TO_STAFF, FRONTMATTER_RE, loadQaContext;
10001
10085
  var init_loadQaContext = __esm({
10002
10086
  "src/scripts/loadQaContext.ts"() {
10003
10087
  "use strict";
@@ -10006,7 +10090,7 @@ var init_loadQaContext = __esm({
10006
10090
  QA_STAFF = "qa-engineer";
10007
10091
  ALL_STAFF = "*";
10008
10092
  LEGACY_AUDIENCE_TO_STAFF = { chat: "kody", qa: QA_STAFF };
10009
- FRONTMATTER_RE2 = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
10093
+ FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
10010
10094
  loadQaContext = async (ctx) => {
10011
10095
  const vars = readKodyVariables(ctx.cwd);
10012
10096
  const login = vars.LOGIN_USER ?? "";
@@ -10021,7 +10105,7 @@ var init_loadQaContext = __esm({
10021
10105
 
10022
10106
  // src/taskContext.ts
10023
10107
  import * as fs34 from "fs";
10024
- import * as path31 from "path";
10108
+ import * as path32 from "path";
10025
10109
  function buildTaskContext(args) {
10026
10110
  return {
10027
10111
  schemaVersion: TASK_CONTEXT_SCHEMA_VERSION,
@@ -10036,9 +10120,9 @@ function buildTaskContext(args) {
10036
10120
  }
10037
10121
  function persistTaskContext(cwd, ctx) {
10038
10122
  try {
10039
- const dir = path31.join(cwd, ".kody", "runs", ctx.runId);
10123
+ const dir = path32.join(cwd, ".kody", "runs", ctx.runId);
10040
10124
  fs34.mkdirSync(dir, { recursive: true });
10041
- const file = path31.join(dir, "task-context.json");
10125
+ const file = path32.join(dir, "task-context.json");
10042
10126
  fs34.writeFileSync(file, `${JSON.stringify(ctx, null, 2)}
10043
10127
  `);
10044
10128
  return file;
@@ -10128,7 +10212,7 @@ var init_loadTaskState = __esm({
10128
10212
 
10129
10213
  // src/scripts/loadWorkerAdhoc.ts
10130
10214
  import * as fs35 from "fs";
10131
- import * as path32 from "path";
10215
+ import * as path33 from "path";
10132
10216
  function resolveMessage(messageArg) {
10133
10217
  const fromComment = readCommentBody();
10134
10218
  if (fromComment) return stripDirective(fromComment);
@@ -10162,7 +10246,7 @@ function stripDirective(body) {
10162
10246
  return lines.slice(start).join("\n").trim();
10163
10247
  }
10164
10248
  function parsePersona(raw, slug) {
10165
- const stripped = splitFrontmatter2(raw).body;
10249
+ const stripped = stripLeadingFrontmatter(raw);
10166
10250
  const trimmed = stripped.trim();
10167
10251
  const firstLine2 = trimmed.split("\n", 1)[0] ?? "";
10168
10252
  const h1 = /^#\s+(.+?)\s*$/.exec(firstLine2);
@@ -10170,23 +10254,26 @@ function parsePersona(raw, slug) {
10170
10254
  const rest = trimmed.slice(firstLine2.length).replace(/^\n+/, "");
10171
10255
  return { title: h1[1].trim(), body: rest };
10172
10256
  }
10173
- return { title: humanizeSlug2(slug), body: trimmed };
10257
+ return { title: humanizeSlug3(slug), body: trimmed };
10174
10258
  }
10175
- function humanizeSlug2(slug) {
10259
+ function stripLeadingFrontmatter(raw) {
10260
+ const match = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/.exec(raw);
10261
+ return match ? raw.slice(match[0].length) : raw;
10262
+ }
10263
+ function humanizeSlug3(slug) {
10176
10264
  return slug.split(/[-_]+/).filter((s) => s.length > 0).map((s) => s[0].toUpperCase() + s.slice(1)).join(" ");
10177
10265
  }
10178
10266
  var loadWorkerAdhoc;
10179
10267
  var init_loadWorkerAdhoc = __esm({
10180
10268
  "src/scripts/loadWorkerAdhoc.ts"() {
10181
10269
  "use strict";
10182
- init_jobFrontmatter();
10183
10270
  loadWorkerAdhoc = async (ctx, _profile, args) => {
10184
10271
  const workersDir = String(args?.workersDir ?? ".kody/staff");
10185
10272
  const workerSlug = String(ctx.args.worker ?? "").trim();
10186
10273
  if (!workerSlug) {
10187
10274
  throw new Error("loadWorkerAdhoc: ctx.args.worker must be a non-empty slug");
10188
10275
  }
10189
- const workerPath = path32.join(ctx.cwd, workersDir, `${workerSlug}.md`);
10276
+ const workerPath = path33.join(ctx.cwd, workersDir, `${workerSlug}.md`);
10190
10277
  if (!fs35.existsSync(workerPath)) {
10191
10278
  throw new Error(`loadWorkerAdhoc: worker persona not found: ${workerPath}`);
10192
10279
  }
@@ -12061,12 +12148,12 @@ fi
12061
12148
 
12062
12149
  // src/scripts/runPreviewBuild.ts
12063
12150
  import { copyFile, writeFile } from "fs/promises";
12064
- import * as path33 from "path";
12151
+ import * as path34 from "path";
12065
12152
  import { fileURLToPath } from "url";
12066
12153
  function bundledDockerfilePath(mode) {
12067
- const here = path33.dirname(fileURLToPath(import.meta.url));
12154
+ const here = path34.dirname(fileURLToPath(import.meta.url));
12068
12155
  const file = mode === "dev" ? "default-Dockerfile.preview.dev" : "default-Dockerfile.preview.prod";
12069
- return path33.join(here, "preview-build-templates", file);
12156
+ return path34.join(here, "preview-build-templates", file);
12070
12157
  }
12071
12158
  function required(name) {
12072
12159
  const v = (process.env[name] ?? "").trim();
@@ -12325,10 +12412,10 @@ var init_runPreviewBuild = __esm({
12325
12412
  console.log(`[preview-build] vault: ${Object.keys(buildEnv).length} secrets, mode=${buildMode}`);
12326
12413
  if (Object.keys(buildEnv).length > 0) {
12327
12414
  const lines = Object.entries(buildEnv).map(([k, v]) => `${k}=${JSON.stringify(v)}`);
12328
- await writeFile(path33.join(ctx.cwd, ".env.production.local"), `${lines.join("\n")}
12415
+ await writeFile(path34.join(ctx.cwd, ".env.production.local"), `${lines.join("\n")}
12329
12416
  `, "utf8");
12330
12417
  }
12331
- const consumerDockerfile = path33.join(ctx.cwd, "Dockerfile.preview");
12418
+ const consumerDockerfile = path34.join(ctx.cwd, "Dockerfile.preview");
12332
12419
  const { stat } = await import("fs/promises");
12333
12420
  let hasConsumerDockerfile = false;
12334
12421
  try {
@@ -12431,7 +12518,7 @@ var init_runPreviewBuild = __esm({
12431
12518
  // src/scripts/runTickScript.ts
12432
12519
  import { spawnSync as spawnSync2 } from "child_process";
12433
12520
  import * as fs36 from "fs";
12434
- import * as path34 from "path";
12521
+ import * as path35 from "path";
12435
12522
  function buildChildEnv(parent, force) {
12436
12523
  const allow = /* @__PURE__ */ new Set([
12437
12524
  "PATH",
@@ -12479,7 +12566,7 @@ var runTickScript;
12479
12566
  var init_runTickScript = __esm({
12480
12567
  "src/scripts/runTickScript.ts"() {
12481
12568
  "use strict";
12482
- init_jobFrontmatter();
12569
+ init_dutyFolders();
12483
12570
  init_jobState();
12484
12571
  init_parseJobStateFromAgentResult();
12485
12572
  runTickScript = async (ctx, _profile, args) => {
@@ -12493,21 +12580,19 @@ var init_runTickScript = __esm({
12493
12580
  ctx.output.reason = `runTickScript: ctx.args.${slugArg} must be a non-empty slug`;
12494
12581
  return;
12495
12582
  }
12496
- const jobPath = path34.join(ctx.cwd, jobsDir, `${slug}.md`);
12497
- if (!fs36.existsSync(jobPath)) {
12583
+ const duty = readDutyFolder(path35.join(ctx.cwd, jobsDir), slug);
12584
+ if (!duty) {
12498
12585
  ctx.output.exitCode = 99;
12499
- ctx.output.reason = `runTickScript: job file not found: ${jobPath}`;
12586
+ ctx.output.reason = `runTickScript: duty folder not found or incomplete: ${path35.join(ctx.cwd, jobsDir, slug)}`;
12500
12587
  return;
12501
12588
  }
12502
- const raw = fs36.readFileSync(jobPath, "utf-8");
12503
- const { frontmatter } = splitFrontmatter2(raw);
12504
- const tickScript = frontmatter.tickScript;
12589
+ const tickScript = duty.config.tickScript;
12505
12590
  if (!tickScript) {
12506
12591
  ctx.output.exitCode = 99;
12507
- ctx.output.reason = `runTickScript: duty ${slug} has no \`tickScript:\` frontmatter \u2014 route via duty-tick instead`;
12592
+ ctx.output.reason = `runTickScript: duty ${slug} has no \`tickScript\` in profile.json \u2014 route via duty-tick instead`;
12508
12593
  return;
12509
12594
  }
12510
- const scriptPath = path34.isAbsolute(tickScript) ? tickScript : path34.join(ctx.cwd, tickScript);
12595
+ const scriptPath = path35.isAbsolute(tickScript) ? tickScript : path35.join(ctx.cwd, tickScript);
12511
12596
  if (!fs36.existsSync(scriptPath)) {
12512
12597
  ctx.output.exitCode = 99;
12513
12598
  ctx.output.reason = `runTickScript: tickScript not found: ${scriptPath}`;
@@ -13706,7 +13791,7 @@ var init_scripts = __esm({
13706
13791
 
13707
13792
  // src/staff.ts
13708
13793
  import * as fs38 from "fs";
13709
- import * as path35 from "path";
13794
+ import * as path36 from "path";
13710
13795
  function stripFrontmatter(raw) {
13711
13796
  const match = /^---\n[\s\S]*?\n---\n?([\s\S]*)$/.exec(raw);
13712
13797
  return (match ? match[1] : raw).trim();
@@ -13714,7 +13799,7 @@ function stripFrontmatter(raw) {
13714
13799
  function loadStaffPersona(cwd, slug, staffDir = DEFAULT_STAFF_DIR) {
13715
13800
  const trimmed = slug.trim();
13716
13801
  if (!trimmed) throw new Error("loadStaffPersona: empty staff slug");
13717
- const staffPath = path35.join(cwd, staffDir, `${trimmed}.md`);
13802
+ const staffPath = path36.join(cwd, staffDir, `${trimmed}.md`);
13718
13803
  if (fs38.existsSync(staffPath)) {
13719
13804
  const body = stripFrontmatter(fs38.readFileSync(staffPath, "utf-8"));
13720
13805
  if (body) return body;
@@ -13830,7 +13915,7 @@ var init_tools = __esm({
13830
13915
  // src/executor.ts
13831
13916
  import { spawn as spawn7 } from "child_process";
13832
13917
  import * as fs39 from "fs";
13833
- import * as path36 from "path";
13918
+ import * as path37 from "path";
13834
13919
  function isMutatingPostflight(scriptName) {
13835
13920
  return MUTATING_POSTFLIGHTS.has(scriptName ?? "");
13836
13921
  }
@@ -13981,13 +14066,13 @@ async function runExecutable(profileName, input) {
13981
14066
  })
13982
14067
  };
13983
14068
  })() : null;
13984
- const ndjsonDir = path36.join(input.cwd, ".kody");
14069
+ const ndjsonDir = path37.join(input.cwd, ".kody");
13985
14070
  const personaSlug = typeof profile.staff === "string" && profile.staff.length > 0 ? profile.staff : typeof ctx.data.jobPersona === "string" && ctx.data.jobPersona.length > 0 ? ctx.data.jobPersona : null;
13986
14071
  const staffPersona = personaSlug ? framePersona(personaSlug, loadStaffPersona(input.cwd, personaSlug)) : null;
13987
14072
  const jobWhyBlock = typeof ctx.data.jobWhy === "string" ? operatorRequestBlock(ctx.data.jobWhy) : null;
13988
14073
  const jobRefBlock = jobReferenceBlock(profileName, profile, ctx.data);
13989
14074
  const invokeAgent = async (prompt) => {
13990
- const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path36.isAbsolute(p) ? p : path36.resolve(profile.dir, p)).filter((p) => p.length > 0);
14075
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path37.isAbsolute(p) ? p : path37.resolve(profile.dir, p)).filter((p) => p.length > 0);
13991
14076
  const syntheticPath = ctx.data.syntheticPluginPath;
13992
14077
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
13993
14078
  const agents = loadSubagents(profile);
@@ -14028,7 +14113,7 @@ async function runExecutable(profileName, input) {
14028
14113
  enableVerifyTool: profile.claudeCode.enableVerifyTool,
14029
14114
  enableSubmitTool: profile.claudeCode.enableSubmitTool,
14030
14115
  // Locked-toolbox duty mode: `loadJobFromFile` flips `ctx.data.dutyTools`
14031
- // when a duty declares `tools:` frontmatter. The executor doesn't need
14116
+ // when a duty declares `tools` in profile.json. The executor doesn't need
14032
14117
  // to know the palette — it just forwards the flag so agent.ts can spin
14033
14118
  // up the in-process `kody-duty` MCP server with the right context.
14034
14119
  enableDutyTool: Array.isArray(ctx.data.dutyTools) && ctx.data.dutyTools.length > 0,
@@ -14313,13 +14398,13 @@ function clearStampedLifecycleLabels(profile, ctx) {
14313
14398
  function resolveProfilePath(profileName) {
14314
14399
  const found = resolveExecutable(profileName);
14315
14400
  if (found) return found;
14316
- const here = path36.dirname(new URL(import.meta.url).pathname);
14401
+ const here = path37.dirname(new URL(import.meta.url).pathname);
14317
14402
  const candidates = [
14318
- path36.join(here, "executables", profileName, "profile.json"),
14403
+ path37.join(here, "executables", profileName, "profile.json"),
14319
14404
  // same-dir sibling (dev)
14320
- path36.join(here, "..", "executables", profileName, "profile.json"),
14405
+ path37.join(here, "..", "executables", profileName, "profile.json"),
14321
14406
  // up one (prod: dist/bin → dist/executables)
14322
- path36.join(here, "..", "src", "executables", profileName, "profile.json")
14407
+ path37.join(here, "..", "src", "executables", profileName, "profile.json")
14323
14408
  // fallback
14324
14409
  ];
14325
14410
  for (const c of candidates) {
@@ -14421,7 +14506,7 @@ function resolveShellTimeoutMs(entry) {
14421
14506
  }
14422
14507
  async function runShellEntry(entry, ctx, profile) {
14423
14508
  const shellName = entry.shell;
14424
- const shellPath = path36.join(profile.dir, shellName);
14509
+ const shellPath = path37.join(profile.dir, shellName);
14425
14510
  if (!fs39.existsSync(shellPath)) {
14426
14511
  ctx.skipAgent = true;
14427
14512
  ctx.output.exitCode = 99;
@@ -14678,18 +14763,18 @@ function translateOpenAISseToBrain(opts) {
14678
14763
  // src/servers/brain-serve.ts
14679
14764
  import * as fs42 from "fs";
14680
14765
  import { createServer } from "http";
14681
- import * as path39 from "path";
14766
+ import * as path40 from "path";
14682
14767
 
14683
14768
  // src/chat/loop.ts
14684
14769
  init_agent();
14685
14770
  init_registry();
14686
14771
  init_task_artifacts();
14687
- import * as fs11 from "fs";
14688
- import * as path11 from "path";
14772
+ import * as fs12 from "fs";
14773
+ import * as path12 from "path";
14689
14774
 
14690
14775
  // src/chat/attachments.ts
14691
- import * as fs8 from "fs";
14692
- import * as path8 from "path";
14776
+ import * as fs9 from "fs";
14777
+ import * as path9 from "path";
14693
14778
  var INLINE_ATTACHMENT_RE = /(?:\[(?:Image|File): ([^\]]*)\]\n)?data:([\w.+-]+\/[\w.+-]+);base64,([A-Za-z0-9+/=]+)/g;
14694
14779
  var EXT_BY_MIME = {
14695
14780
  "image/png": "png",
@@ -14705,7 +14790,7 @@ function extFor(mime) {
14705
14790
  return EXT_BY_MIME[mime.toLowerCase()] ?? mime.split("/")[1]?.replace(/[^\w]/g, "") ?? "bin";
14706
14791
  }
14707
14792
  function attachmentsDir(cwd, sessionId) {
14708
- return path8.join(cwd, ".kody", "tmp", "attachments", sessionId);
14793
+ return path9.join(cwd, ".kody", "tmp", "attachments", sessionId);
14709
14794
  }
14710
14795
  function prepareAttachments(turns, cwd, sessionId) {
14711
14796
  const imagePaths = [];
@@ -14722,11 +14807,11 @@ function prepareAttachments(turns, cwd, sessionId) {
14722
14807
  if (!isImage) return `[File: ${name}]`;
14723
14808
  try {
14724
14809
  if (!dirEnsured) {
14725
- fs8.mkdirSync(dir, { recursive: true });
14810
+ fs9.mkdirSync(dir, { recursive: true });
14726
14811
  dirEnsured = true;
14727
14812
  }
14728
- const filePath = path8.join(dir, `${imageCounter}.${extFor(mime)}`);
14729
- fs8.writeFileSync(filePath, Buffer.from(data, "base64"));
14813
+ const filePath = path9.join(dir, `${imageCounter}.${extFor(mime)}`);
14814
+ fs9.writeFileSync(filePath, Buffer.from(data, "base64"));
14730
14815
  imageCounter += 1;
14731
14816
  imagePaths.push(filePath);
14732
14817
  return `[Image "${name}" is attached \u2014 saved to ${filePath}. Use the Read tool on that exact path to view it.]`;
@@ -14742,10 +14827,10 @@ function prepareAttachments(turns, cwd, sessionId) {
14742
14827
  }
14743
14828
 
14744
14829
  // src/chat/events.ts
14745
- import * as fs9 from "fs";
14746
- import * as path9 from "path";
14830
+ import * as fs10 from "fs";
14831
+ import * as path10 from "path";
14747
14832
  function eventsFilePath(cwd, sessionId) {
14748
- return path9.join(cwd, ".kody", "events", `${sessionId}.jsonl`);
14833
+ return path10.join(cwd, ".kody", "events", `${sessionId}.jsonl`);
14749
14834
  }
14750
14835
  var FileSink = class {
14751
14836
  constructor(file) {
@@ -14753,8 +14838,8 @@ var FileSink = class {
14753
14838
  }
14754
14839
  file;
14755
14840
  async emit(event) {
14756
- fs9.mkdirSync(path9.dirname(this.file), { recursive: true });
14757
- fs9.appendFileSync(this.file, `${JSON.stringify(event)}
14841
+ fs10.mkdirSync(path10.dirname(this.file), { recursive: true });
14842
+ fs10.appendFileSync(this.file, `${JSON.stringify(event)}
14758
14843
  `);
14759
14844
  }
14760
14845
  };
@@ -14808,14 +14893,14 @@ function makeRunId(sessionId, suffix) {
14808
14893
  }
14809
14894
 
14810
14895
  // src/chat/session.ts
14811
- import * as fs10 from "fs";
14812
- import * as path10 from "path";
14896
+ import * as fs11 from "fs";
14897
+ import * as path11 from "path";
14813
14898
  function sessionFilePath(cwd, sessionId) {
14814
- return path10.join(cwd, ".kody", "sessions", `${sessionId}.jsonl`);
14899
+ return path11.join(cwd, ".kody", "sessions", `${sessionId}.jsonl`);
14815
14900
  }
14816
14901
  function readMeta(file) {
14817
- if (!fs10.existsSync(file)) return null;
14818
- const raw = fs10.readFileSync(file, "utf-8");
14902
+ if (!fs11.existsSync(file)) return null;
14903
+ const raw = fs11.readFileSync(file, "utf-8");
14819
14904
  const firstLine2 = raw.split("\n", 1)[0]?.trim();
14820
14905
  if (!firstLine2) return null;
14821
14906
  try {
@@ -14828,8 +14913,8 @@ function readMeta(file) {
14828
14913
  }
14829
14914
  }
14830
14915
  function readSession(file) {
14831
- if (!fs10.existsSync(file)) return [];
14832
- const raw = fs10.readFileSync(file, "utf-8").trim();
14916
+ if (!fs11.existsSync(file)) return [];
14917
+ const raw = fs11.readFileSync(file, "utf-8").trim();
14833
14918
  if (!raw) return [];
14834
14919
  const turns = [];
14835
14920
  for (const line of raw.split("\n")) {
@@ -14845,14 +14930,14 @@ function readSession(file) {
14845
14930
  return turns;
14846
14931
  }
14847
14932
  function appendTurn(file, turn) {
14848
- fs10.mkdirSync(path10.dirname(file), { recursive: true });
14933
+ fs11.mkdirSync(path11.dirname(file), { recursive: true });
14849
14934
  const line = JSON.stringify({
14850
14935
  role: turn.role,
14851
14936
  content: turn.content,
14852
14937
  timestamp: turn.timestamp,
14853
14938
  toolCalls: turn.toolCalls ?? []
14854
14939
  });
14855
- fs10.appendFileSync(file, `${line}
14940
+ fs11.appendFileSync(file, `${line}
14856
14941
  `);
14857
14942
  }
14858
14943
  function seedInitialMessage(file, message) {
@@ -14971,7 +15056,7 @@ function buildExecutableCatalog() {
14971
15056
  const entries = [];
14972
15057
  for (const { name, profilePath } of discovered) {
14973
15058
  try {
14974
- const raw = JSON.parse(fs11.readFileSync(profilePath, "utf-8"));
15059
+ const raw = JSON.parse(fs12.readFileSync(profilePath, "utf-8"));
14975
15060
  const describe = typeof raw.describe === "string" ? raw.describe : "";
14976
15061
  const firstSentence = describe.split(/(?<=[.!?])\s+/, 1)[0] ?? "";
14977
15062
  entries.push({ name, describe: firstSentence.trim() });
@@ -15139,10 +15224,10 @@ async function emit(sink, type, sessionId, suffix, payload) {
15139
15224
  var MEMORY_INDEX_REL = ".kody/memory/INDEX.md";
15140
15225
  var MAX_INDEX_BYTES = 8e3;
15141
15226
  function readMemoryIndexBlock(cwd) {
15142
- const indexPath = path11.join(cwd, MEMORY_INDEX_REL);
15227
+ const indexPath = path12.join(cwd, MEMORY_INDEX_REL);
15143
15228
  let raw;
15144
15229
  try {
15145
- raw = fs11.readFileSync(indexPath, "utf-8");
15230
+ raw = fs12.readFileSync(indexPath, "utf-8");
15146
15231
  } catch {
15147
15232
  return "";
15148
15233
  }
@@ -15160,17 +15245,17 @@ function readMemoryIndexBlock(cwd) {
15160
15245
  var CONTEXT_DIR_REL = ".kody/context";
15161
15246
  var MAX_CONTEXT_BYTES = 12e3;
15162
15247
  function readContextBlock(cwd) {
15163
- const dir = path11.join(cwd, CONTEXT_DIR_REL);
15248
+ const dir = path12.join(cwd, CONTEXT_DIR_REL);
15164
15249
  let files;
15165
15250
  try {
15166
- files = fs11.readdirSync(dir).filter((f) => f.endsWith(".md")).sort();
15251
+ files = fs12.readdirSync(dir).filter((f) => f.endsWith(".md")).sort();
15167
15252
  } catch {
15168
15253
  return "";
15169
15254
  }
15170
15255
  const sections = [];
15171
15256
  for (const file of files) {
15172
15257
  try {
15173
- const content = fs11.readFileSync(path11.join(dir, file), "utf-8").trim();
15258
+ const content = fs12.readFileSync(path12.join(dir, file), "utf-8").trim();
15174
15259
  if (content) sections.push(`### ${file.replace(/\.md$/, "")}
15175
15260
 
15176
15261
  ${content}`);
@@ -15193,10 +15278,10 @@ _\u2026 (context truncated; see \`.kody/context/\` for the full text)_` : joined
15193
15278
  var INSTRUCTIONS_REL = ".kody/instructions.md";
15194
15279
  var MAX_INSTRUCTIONS_BYTES = 8e3;
15195
15280
  function readInstructionsBlock(cwd) {
15196
- const instructionsPath = path11.join(cwd, INSTRUCTIONS_REL);
15281
+ const instructionsPath = path12.join(cwd, INSTRUCTIONS_REL);
15197
15282
  let raw;
15198
15283
  try {
15199
- raw = fs11.readFileSync(instructionsPath, "utf-8");
15284
+ raw = fs12.readFileSync(instructionsPath, "utf-8");
15200
15285
  } catch {
15201
15286
  return "";
15202
15287
  }
@@ -15220,7 +15305,7 @@ init_config();
15220
15305
  // src/kody-cli.ts
15221
15306
  import { execFileSync as execFileSync26 } from "child_process";
15222
15307
  import * as fs40 from "fs";
15223
- import * as path37 from "path";
15308
+ import * as path38 from "path";
15224
15309
 
15225
15310
  // src/app-auth.ts
15226
15311
  import { createSign } from "crypto";
@@ -15297,7 +15382,7 @@ init_config();
15297
15382
 
15298
15383
  // src/dispatch.ts
15299
15384
  init_config();
15300
- import * as fs12 from "fs";
15385
+ import * as fs13 from "fs";
15301
15386
 
15302
15387
  // src/cron-match.ts
15303
15388
  var FIELD_BOUNDS = [
@@ -15370,17 +15455,55 @@ function primaryNumericInputName(executable) {
15370
15455
  const intInput = inputs.find((i) => i.type === "int" && i.required);
15371
15456
  return intInput?.name ?? null;
15372
15457
  }
15458
+ function resolveOperatorAction(action) {
15459
+ return resolveDutyAction(action);
15460
+ }
15461
+ function resolveConfiguredAction(action) {
15462
+ const resolved = resolveDutyAction(action);
15463
+ if (resolved) return resolved;
15464
+ return compatibilityDutyAction(action);
15465
+ }
15466
+ function requiredRoute(action) {
15467
+ return resolveConfiguredAction(action) ?? {
15468
+ action,
15469
+ duty: action,
15470
+ executable: action,
15471
+ cliArgs: {},
15472
+ source: "builtin"
15473
+ };
15474
+ }
15475
+ function compatibilityDutyAction(action) {
15476
+ if (!/^[a-z][a-z0-9-]*$/.test(action)) return null;
15477
+ return {
15478
+ action,
15479
+ duty: action,
15480
+ executable: action,
15481
+ cliArgs: {},
15482
+ source: "builtin"
15483
+ };
15484
+ }
15485
+ function routeResult(route, cliArgs, target, why) {
15486
+ const result = {
15487
+ action: route.action,
15488
+ duty: route.duty,
15489
+ executable: route.executable,
15490
+ cliArgs: { ...route.cliArgs, ...cliArgs },
15491
+ target
15492
+ };
15493
+ if (why !== void 0 && why.length > 0) result.why = why;
15494
+ return result;
15495
+ }
15373
15496
  function autoDispatch(opts) {
15374
15497
  const explicit = opts?.explicit;
15375
15498
  if (explicit?.issueNumber && explicit.issueNumber > 0) {
15376
- return { executable: "run", cliArgs: { issue: explicit.issueNumber }, target: explicit.issueNumber };
15499
+ return routeResult(requiredRoute("run"), { issue: explicit.issueNumber }, explicit.issueNumber);
15377
15500
  }
15378
15501
  const eventName = process.env.GITHUB_EVENT_NAME;
15379
15502
  const eventPath = process.env.GITHUB_EVENT_PATH;
15380
- if (!eventName || !eventPath || !fs12.existsSync(eventPath)) return null;
15503
+ if (!eventName || !eventPath || !fs13.existsSync(eventPath)) return null;
15381
15504
  let event = {};
15382
15505
  try {
15383
- event = JSON.parse(fs12.readFileSync(eventPath, "utf-8"));
15506
+ event = JSON.parse(fs13.readFileSync(eventPath, "utf-8"));
15384
15507
  } catch {
15385
15508
  return null;
15386
15509
  }
@@ -15388,25 +15511,29 @@ function autoDispatch(opts) {
15388
15511
  const inputs2 = objectValue(event.inputs);
15389
15512
  const n = parseInt(String(inputs2?.issue_number ?? ""), 10);
15390
15513
  if (!Number.isNaN(n) && n > 0) {
15391
- const exe = String(inputs2?.executable ?? "").trim() || "run";
15514
+ const actionName = String(inputs2?.executable ?? "").trim() || "run";
15515
+ const route2 = resolveConfiguredAction(actionName);
15516
+ if (!route2) return null;
15392
15517
  const base = String(inputs2?.base ?? "").trim();
15393
- const targetKey = primaryNumericInputName(exe) ?? "issue";
15518
+ const targetKey = primaryNumericInputName(route2.executable) ?? "issue";
15394
15519
  const cliArgs = { [targetKey]: n };
15395
15520
  if (base) cliArgs.base = base;
15396
- return { executable: exe, cliArgs, target: n };
15521
+ return routeResult(route2, cliArgs, n);
15397
15522
  }
15398
15523
  return null;
15399
15524
  }
15400
15525
  if (eventName === "schedule") return null;
15401
15526
  if (eventName === "pull_request") {
15402
- const exe = opts?.config?.onPullRequest?.trim();
15527
+ const actionName = opts?.config?.onPullRequest?.trim();
15403
15528
  const action = String(event.action ?? "");
15404
- if (exe && (action === "opened" || action === "synchronize" || action === "reopened")) {
15529
+ if (actionName && (action === "opened" || action === "synchronize" || action === "reopened")) {
15530
+ const route2 = resolveConfiguredAction(actionName);
15531
+ if (!route2) return null;
15405
15532
  const pullRequest = objectValue(event.pull_request);
15406
15533
  const prNum = Number(pullRequest?.number ?? event.number ?? 0);
15407
15534
  if (prNum > 0) {
15408
- const targetKey = primaryNumericInputName(exe) ?? "pr";
15409
- return { executable: exe, cliArgs: { [targetKey]: prNum }, target: prNum };
15535
+ const targetKey = primaryNumericInputName(route2.executable) ?? "pr";
15536
+ return routeResult(route2, { [targetKey]: prNum }, prNum);
15410
15537
  }
15411
15538
  }
15412
15539
  return null;
@@ -15430,21 +15557,22 @@ function autoDispatch(opts) {
15430
15557
  const firstToken = firstTokenRaw && POLITE_WORDS.has(firstTokenRaw) ? null : firstTokenRaw;
15431
15558
  const aliases = opts?.config?.aliases ?? BUILTIN_ALIASES;
15432
15559
  const aliased = firstToken ? aliases[firstToken] ?? firstToken : null;
15433
- let executable = null;
15560
+ let route = null;
15434
15561
  let consumedFirstToken = false;
15435
15562
  if (aliased) {
15436
- if (getProfileInputs(aliased) !== null) {
15437
- executable = aliased;
15563
+ route = resolveOperatorAction(aliased);
15564
+ if (route) {
15438
15565
  consumedFirstToken = true;
15439
15566
  } else if (firstToken && aliases[firstToken] && aliases[firstToken] === aliased) {
15440
15567
  process.stderr.write(
15441
- `[kody] dispatch: alias '${firstToken}' \u2192 '${aliased}' has no matching executable; falling back to default
15568
+ `[kody] dispatch: alias '${firstToken}' \u2192 '${aliased}' has no matching duty action; falling back to default
15442
15569
  `
15443
15570
  );
15444
15571
  }
15445
15572
  }
15446
- if (!executable && !firstToken) {
15447
- executable = isPr ? opts?.config?.defaultPrExecutable ?? null : opts?.config?.defaultExecutable ?? null;
15573
+ if (!route && !firstToken) {
15574
+ const defaultAction = isPr ? opts?.config?.defaultPrExecutable ?? null : opts?.config?.defaultExecutable ?? null;
15575
+ route = defaultAction ? resolveConfiguredAction(defaultAction) : null;
15448
15576
  }
15449
15577
  if (isBotAuthor && !consumedFirstToken) {
15450
15578
  process.stderr.write(
@@ -15453,16 +15581,16 @@ function autoDispatch(opts) {
15453
15581
  );
15454
15582
  return null;
15455
15583
  }
15456
- if (!executable) {
15584
+ if (!route) {
15457
15585
  if (!firstToken) return null;
15458
- const profileMissing = aliased ? getProfileInputs(aliased) === null : true;
15586
+ const profileMissing = aliased ? resolveOperatorAction(aliased) === null : true;
15459
15587
  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>"})
15588
+ `[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
15589
  `
15462
15590
  );
15463
15591
  return null;
15464
15592
  }
15465
- const inputs = getProfileInputs(executable);
15593
+ const inputs = getProfileInputs(route.executable);
15466
15594
  const effectiveInputs = inputs ?? [];
15467
15595
  const unknownProfile = inputs === null;
15468
15596
  const rest = extractCommentRest(afterTag, consumedFirstToken ? firstToken : null);
@@ -15479,14 +15607,14 @@ function autoDispatch(opts) {
15479
15607
  } else if (leftover.length > 0) {
15480
15608
  why = leftover;
15481
15609
  }
15482
- return { executable, cliArgs: args, target: targetNum, why };
15610
+ return routeResult(route, args, targetNum, why);
15483
15611
  }
15484
15612
  function autoDispatchTyped(opts) {
15485
15613
  const legacy = autoDispatch(opts);
15486
15614
  if (legacy) return { kind: "route", ...legacy };
15487
15615
  const eventName = process.env.GITHUB_EVENT_NAME;
15488
15616
  const eventPath = process.env.GITHUB_EVENT_PATH;
15489
- if (!eventName || !eventPath || !fs12.existsSync(eventPath)) {
15617
+ if (!eventName || !eventPath || !fs13.existsSync(eventPath)) {
15490
15618
  return { kind: "silent", reason: "no GHA event context" };
15491
15619
  }
15492
15620
  if (eventName !== "issue_comment") {
@@ -15494,7 +15622,7 @@ function autoDispatchTyped(opts) {
15494
15622
  }
15495
15623
  let event = {};
15496
15624
  try {
15497
- event = JSON.parse(fs12.readFileSync(eventPath, "utf-8"));
15625
+ event = JSON.parse(fs13.readFileSync(eventPath, "utf-8"));
15498
15626
  } catch {
15499
15627
  return { kind: "silent", reason: "GHA event payload unreadable" };
15500
15628
  }
@@ -15527,7 +15655,7 @@ function autoDispatchTyped(opts) {
15527
15655
  reason: tokenRaw ? `polite-word lead-in '${tokenRaw}', no default executable configured` : "no subcommand token, no default executable configured"
15528
15656
  };
15529
15657
  }
15530
- const available = listExecutables().map((e) => e.name).filter((n) => !n.startsWith("goal-") && !n.startsWith("job-")).sort();
15658
+ const available = listDutyActions().map((e) => e.action).filter((n) => !n.startsWith("goal-") && !n.startsWith("job-")).sort();
15531
15659
  return { kind: "unrecognized", token: tokenRaw, target: targetNum, isPr, available };
15532
15660
  }
15533
15661
  function dispatchScheduledWatches(opts) {
@@ -15538,7 +15666,7 @@ function dispatchScheduledWatches(opts) {
15538
15666
  for (const exe of listExecutables()) {
15539
15667
  let raw;
15540
15668
  try {
15541
- raw = fs12.readFileSync(exe.profilePath, "utf-8");
15669
+ raw = fs13.readFileSync(exe.profilePath, "utf-8");
15542
15670
  } catch {
15543
15671
  continue;
15544
15672
  }
@@ -15564,7 +15692,7 @@ function dispatchScheduledWatches(opts) {
15564
15692
  continue;
15565
15693
  }
15566
15694
  }
15567
- out.push({ executable: exe.name, cliArgs: {}, target: 0 });
15695
+ out.push({ action: exe.name, duty: exe.name, executable: exe.name, cliArgs: {}, target: 0 });
15568
15696
  }
15569
15697
  return out;
15570
15698
  }
@@ -15670,6 +15798,7 @@ init_executor();
15670
15798
  init_gha();
15671
15799
  init_issue();
15672
15800
  init_job();
15801
+ init_registry();
15673
15802
  var CI_HELP = `kody ci \u2014 minimal-YAML autonomous engineer (CI preflight + run)
15674
15803
 
15675
15804
  Usage:
@@ -15780,9 +15909,9 @@ async function resolveAuthToken(env = process.env) {
15780
15909
  return void 0;
15781
15910
  }
15782
15911
  function detectPackageManager2(cwd) {
15783
- if (fs40.existsSync(path37.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
15784
- if (fs40.existsSync(path37.join(cwd, "yarn.lock"))) return "yarn";
15785
- if (fs40.existsSync(path37.join(cwd, "bun.lockb"))) return "bun";
15912
+ if (fs40.existsSync(path38.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
15913
+ if (fs40.existsSync(path38.join(cwd, "yarn.lock"))) return "yarn";
15914
+ if (fs40.existsSync(path38.join(cwd, "bun.lockb"))) return "bun";
15786
15915
  return "npm";
15787
15916
  }
15788
15917
  function shellOut(cmd, args, cwd, stream = true) {
@@ -15869,7 +15998,7 @@ function configureGitIdentity(cwd) {
15869
15998
  }
15870
15999
  function postFailureTail(issueNumber, cwd, reason) {
15871
16000
  if (!issueNumber) return;
15872
- const logPath = path37.join(cwd, ".kody", "last-run.jsonl");
16001
+ const logPath = path38.join(cwd, ".kody", "last-run.jsonl");
15873
16002
  let tail = "";
15874
16003
  try {
15875
16004
  if (fs40.existsSync(logPath)) {
@@ -15898,7 +16027,7 @@ async function runCi(argv) {
15898
16027
  return 0;
15899
16028
  }
15900
16029
  const args = parseCiArgs(argv);
15901
- const cwd = args.cwd ? path37.resolve(args.cwd) : process.cwd();
16030
+ const cwd = args.cwd ? path38.resolve(args.cwd) : process.cwd();
15902
16031
  let earlyConfig;
15903
16032
  let earlyConfigError;
15904
16033
  try {
@@ -15910,7 +16039,7 @@ async function runCi(argv) {
15910
16039
  const eventName = process.env.GITHUB_EVENT_NAME;
15911
16040
  const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
15912
16041
  let manualWorkflowDispatch = false;
15913
- let forceRunDuty = null;
16042
+ let forceRunAction = null;
15914
16043
  if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs40.existsSync(dispatchEventPath)) {
15915
16044
  try {
15916
16045
  const evt = JSON.parse(fs40.readFileSync(dispatchEventPath, "utf-8"));
@@ -15918,15 +16047,21 @@ async function runCi(argv) {
15918
16047
  const sessionInput = String(evt?.inputs?.sessionId ?? "");
15919
16048
  const exeInput = String(evt?.inputs?.executable ?? "").trim();
15920
16049
  const noTarget = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
15921
- if (noTarget && exeInput) forceRunDuty = exeInput;
16050
+ if (noTarget && exeInput) forceRunAction = exeInput;
15922
16051
  else manualWorkflowDispatch = noTarget;
15923
16052
  } catch {
15924
16053
  manualWorkflowDispatch = false;
15925
16054
  }
15926
16055
  }
15927
- if (forceRunDuty) {
16056
+ if (forceRunAction) {
15928
16057
  const config = earlyConfig ?? loadConfig(cwd);
15929
- process.stdout.write(`\u2192 kody: manual one-shot run of duty ${forceRunDuty}
16058
+ const route = resolveDutyAction(forceRunAction);
16059
+ if (!route) {
16060
+ process.stderr.write(`[kody] manual one-shot action '${forceRunAction}' has no duty action
16061
+ `);
16062
+ return 64;
16063
+ }
16064
+ process.stdout.write(`\u2192 kody: manual one-shot run of duty action ${route.action} (${route.duty})
15930
16065
 
15931
16066
  `);
15932
16067
  try {
@@ -15957,13 +16092,22 @@ async function runCi(argv) {
15957
16092
  `);
15958
16093
  return 99;
15959
16094
  }
15960
- const result = await runExecutableChain(forceRunDuty, {
15961
- cliArgs: {},
15962
- cwd,
15963
- config,
15964
- verbose: args.verbose,
15965
- quiet: args.quiet
15966
- });
16095
+ const result = await runJob(
16096
+ {
16097
+ action: route.action,
16098
+ duty: route.duty,
16099
+ executable: route.executable,
16100
+ cliArgs: route.cliArgs,
16101
+ flavor: "instant",
16102
+ force: true
16103
+ },
16104
+ {
16105
+ cwd,
16106
+ config,
16107
+ verbose: args.verbose,
16108
+ quiet: args.quiet
16109
+ }
16110
+ );
15967
16111
  const ec = result.exitCode;
15968
16112
  return ec === 0 || ec === 1 || ec === 2 ? ec : 99;
15969
16113
  }
@@ -16024,13 +16168,17 @@ ${CI_HELP}`);
16024
16168
  return 64;
16025
16169
  }
16026
16170
  const dispatch2 = autoFallback ?? {
16171
+ action: "run",
16172
+ duty: "run",
16027
16173
  executable: "run",
16028
16174
  cliArgs: { issue: args.issueNumber },
16029
16175
  target: args.issueNumber
16030
16176
  };
16031
16177
  const issueNumber = dispatch2.target;
16032
- process.stdout.write(`\u2192 kody preflight (cwd=${cwd}, executable=${dispatch2.executable}, target=${issueNumber})
16033
- `);
16178
+ process.stdout.write(
16179
+ `\u2192 kody preflight (cwd=${cwd}, action=${dispatch2.action}, duty=${dispatch2.duty}, executable=${dispatch2.executable}, target=${issueNumber})
16180
+ `
16181
+ );
16034
16182
  try {
16035
16183
  const n = unpackAllSecrets();
16036
16184
  if (n > 0) process.stdout.write(`\u2192 kody: unpacked ${n} secret(s) from ALL_SECRETS
@@ -16190,10 +16338,10 @@ init_repoWorkspace();
16190
16338
 
16191
16339
  // src/scripts/brainTurnLog.ts
16192
16340
  import * as fs41 from "fs";
16193
- import * as path38 from "path";
16341
+ import * as path39 from "path";
16194
16342
  var live = /* @__PURE__ */ new Map();
16195
16343
  function eventsPath(dir, chatId) {
16196
- return path38.join(dir, ".kody", "brain-events", `${chatId}.jsonl`);
16344
+ return path39.join(dir, ".kody", "brain-events", `${chatId}.jsonl`);
16197
16345
  }
16198
16346
  function lastPersistedSeq(dir, chatId) {
16199
16347
  const p = eventsPath(dir, chatId);
@@ -16236,7 +16384,7 @@ function beginTurn(dir, chatId) {
16236
16384
  };
16237
16385
  live.set(chatId, state);
16238
16386
  const p = eventsPath(dir, chatId);
16239
- fs41.mkdirSync(path38.dirname(p), { recursive: true });
16387
+ fs41.mkdirSync(path39.dirname(p), { recursive: true });
16240
16388
  return (event) => {
16241
16389
  state.seq += 1;
16242
16390
  const rec = { seq: state.seq, turn, ts: Date.now(), event };
@@ -16511,7 +16659,7 @@ async function handleChatTurn(req, res, chatId, opts) {
16511
16659
  const repo = strField(body, "repo");
16512
16660
  const repoToken = strField(body, "repoToken");
16513
16661
  const sessionFile = sessionFilePath(opts.cwd, chatId);
16514
- fs42.mkdirSync(path39.dirname(sessionFile), { recursive: true });
16662
+ fs42.mkdirSync(path40.dirname(sessionFile), { recursive: true });
16515
16663
  appendTurn(sessionFile, {
16516
16664
  role: "user",
16517
16665
  content: message,
@@ -16558,7 +16706,7 @@ async function handleChatTurn(req, res, chatId, opts) {
16558
16706
  function buildServer(opts) {
16559
16707
  const runTurn = opts.runTurn ?? runChatTurn;
16560
16708
  const cloneRepo = opts.cloneRepo ?? defaultCloneRepo;
16561
- const reposRoot = opts.reposRoot ?? path39.join(path39.dirname(path39.resolve(opts.cwd)), "repos");
16709
+ const reposRoot = opts.reposRoot ?? path40.join(path40.dirname(path40.resolve(opts.cwd)), "repos");
16562
16710
  return createServer(async (req, res) => {
16563
16711
  if (!req.method || !req.url) {
16564
16712
  sendJson(res, 400, { error: "bad request" });
@@ -17152,13 +17300,13 @@ async function loadConfigSafe() {
17152
17300
  // src/chat-cli.ts
17153
17301
  import { execFileSync as execFileSync29 } from "child_process";
17154
17302
  import * as fs44 from "fs";
17155
- import * as path41 from "path";
17303
+ import * as path42 from "path";
17156
17304
 
17157
17305
  // src/chat/modes/interactive.ts
17158
17306
  init_issue();
17159
17307
  import { execFileSync as execFileSync28 } from "child_process";
17160
17308
  import * as fs43 from "fs";
17161
- import * as path40 from "path";
17309
+ import * as path41 from "path";
17162
17310
 
17163
17311
  // src/chat/inbox.ts
17164
17312
  import { execFileSync as execFileSync27 } from "child_process";
@@ -17317,9 +17465,9 @@ function findNextUserTurn(turns, fromIdx) {
17317
17465
  return -1;
17318
17466
  }
17319
17467
  function commitTurn(cwd, sessionId, _verbose) {
17320
- const sessionRel = path40.relative(cwd, sessionFilePath(cwd, sessionId));
17321
- const eventsRel = path40.relative(cwd, eventsFilePath(cwd, sessionId));
17322
- const rels = [sessionRel, eventsRel].filter((p) => fs43.existsSync(path40.join(cwd, p)));
17468
+ const sessionRel = path41.relative(cwd, sessionFilePath(cwd, sessionId));
17469
+ const eventsRel = path41.relative(cwd, eventsFilePath(cwd, sessionId));
17470
+ const rels = [sessionRel, eventsRel].filter((p) => fs43.existsSync(path41.join(cwd, p)));
17323
17471
  if (rels.length === 0) return;
17324
17472
  const repository = process.env.GITHUB_REPOSITORY;
17325
17473
  if (!repository) {
@@ -17331,8 +17479,8 @@ function commitTurn(cwd, sessionId, _verbose) {
17331
17479
  }
17332
17480
  const branch = defaultBranch(cwd) ?? "main";
17333
17481
  for (const rel of rels) {
17334
- const repoPath = rel.split(path40.sep).join("/");
17335
- const localText = fs43.readFileSync(path40.join(cwd, rel), "utf-8");
17482
+ const repoPath = rel.split(path41.sep).join("/");
17483
+ const localText = fs43.readFileSync(path41.join(cwd, rel), "utf-8");
17336
17484
  putJsonlViaContents(repository, branch, repoPath, localText, sessionId, cwd);
17337
17485
  }
17338
17486
  }
@@ -17469,12 +17617,12 @@ function parseChatArgs(argv, env = process.env) {
17469
17617
  return result;
17470
17618
  }
17471
17619
  function commitChatFiles(cwd, sessionId, verbose) {
17472
- const sessionFile = path41.relative(cwd, sessionFilePath(cwd, sessionId));
17473
- const eventsFile = path41.relative(cwd, eventsFilePath(cwd, sessionId));
17620
+ const sessionFile = path42.relative(cwd, sessionFilePath(cwd, sessionId));
17621
+ const eventsFile = path42.relative(cwd, eventsFilePath(cwd, sessionId));
17474
17622
  const safeSession = sessionId.replace(/[^a-zA-Z0-9._-]/g, "_");
17475
- const tasksDir = path41.join(".kody", "tasks", safeSession);
17623
+ const tasksDir = path42.join(".kody", "tasks", safeSession);
17476
17624
  const candidatePaths = [sessionFile, eventsFile, tasksDir];
17477
- const paths = candidatePaths.filter((p) => fs44.existsSync(path41.join(cwd, p)));
17625
+ const paths = candidatePaths.filter((p) => fs44.existsSync(path42.join(cwd, p)));
17478
17626
  if (paths.length === 0) return;
17479
17627
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
17480
17628
  try {
@@ -17518,7 +17666,7 @@ async function runChat(argv) {
17518
17666
  ${CHAT_HELP}`);
17519
17667
  return 64;
17520
17668
  }
17521
- const cwd = args.cwd ? path41.resolve(args.cwd) : process.cwd();
17669
+ const cwd = args.cwd ? path42.resolve(args.cwd) : process.cwd();
17522
17670
  const sessionId = args.sessionId;
17523
17671
  const unpackedSecrets = unpackAllSecrets();
17524
17672
  if (unpackedSecrets > 0) {
@@ -17610,6 +17758,7 @@ ${CHAT_HELP}`);
17610
17758
  // src/entry.ts
17611
17759
  init_config();
17612
17760
  init_executor();
17761
+ init_job();
17613
17762
  init_registry();
17614
17763
 
17615
17764
  // src/servers/pool-serve.ts
@@ -17726,8 +17875,8 @@ var FlyClient = class {
17726
17875
  get fetch() {
17727
17876
  return this.opts.fetchImpl ?? fetch;
17728
17877
  }
17729
- async call(path42, init = {}) {
17730
- const res = await this.fetch(`${FLY_API_BASE}${path42}`, {
17878
+ async call(path43, init = {}) {
17879
+ const res = await this.fetch(`${FLY_API_BASE}${path43}`, {
17731
17880
  method: init.method ?? "GET",
17732
17881
  headers: {
17733
17882
  Authorization: `Bearer ${this.opts.token}`,
@@ -17738,7 +17887,7 @@ var FlyClient = class {
17738
17887
  if (res.status === 404 && init.allow404) return null;
17739
17888
  if (!res.ok) {
17740
17889
  const text = await res.text().catch(() => "");
17741
- throw new Error(`Fly API ${res.status} on ${path42}: ${text.slice(0, 200) || res.statusText}`);
17890
+ throw new Error(`Fly API ${res.status} on ${path43}: ${text.slice(0, 200) || res.statusText}`);
17742
17891
  }
17743
17892
  if (res.status === 204) return null;
17744
17893
  const raw = await res.text();
@@ -18998,16 +19147,17 @@ Usage:
18998
19147
  kody-engine preview-build --pr <N> [--cwd <path>] [--verbose|--quiet]
18999
19148
  kody-engine release --issue <N> [--cwd <path>] [--verbose|--quiet]
19000
19149
  kody-engine init [--cwd <path>] [--verbose|--quiet]
19001
- kody-engine <executable> [--cwd <path>] [--verbose|--quiet]
19150
+ kody-engine <action> [--cwd <path>] [--verbose|--quiet]
19151
+ kody-engine exec <executable> [--cwd <path>] [--verbose|--quiet]
19002
19152
  kody-engine ci [preflight flags \u2014 see: kody-engine ci --help]
19003
19153
  kody-engine chat [chat flags \u2014 see: kody-engine chat --help]
19004
19154
  kody-engine stats [--since 7d|--run <id>|--json|--cwd <path>]
19005
19155
  kody-engine help
19006
19156
  kody-engine version
19007
19157
 
19008
- Each top-level command is a discovered executable under
19009
- \`src/executables/<name>/profile.json\`. Drop in a new directory to add a new
19010
- command; consumer repos can also provide their own executable profiles.
19158
+ Top-level work commands are duty actions. A duty owns the public action name
19159
+ and selects an implementation executable. \`exec <executable>\` is the low-level
19160
+ debug path for engine internals and migration compatibility.
19011
19161
 
19012
19162
  Exit codes:
19013
19163
  0 success (PR opened, verify passed \u2014 or resolve produced a merge commit)
@@ -19048,6 +19198,33 @@ function parseArgs(argv) {
19048
19198
  result.serverArgs = argv.slice(1).filter((a) => !a.startsWith("-"));
19049
19199
  return result;
19050
19200
  }
19201
+ if (cmd === "exec") {
19202
+ const executableName = argv[1];
19203
+ if (!executableName) {
19204
+ result.errors.push("exec requires an executable name");
19205
+ return result;
19206
+ }
19207
+ if (!hasExecutable(executableName)) {
19208
+ result.errors.push(`unknown executable: ${executableName}`);
19209
+ return result;
19210
+ }
19211
+ result.command = "__executable__";
19212
+ result.executableName = executableName;
19213
+ result.cliArgs = parseGenericFlags(argv.slice(2));
19214
+ if (typeof result.cliArgs.cwd === "string") result.cwd = result.cliArgs.cwd;
19215
+ if (result.cliArgs.verbose === true) result.verbose = true;
19216
+ if (result.cliArgs.quiet === true) result.quiet = true;
19217
+ return result;
19218
+ }
19219
+ if (hasDutyAction(cmd)) {
19220
+ result.command = "__duty__";
19221
+ result.actionName = cmd;
19222
+ result.cliArgs = parseGenericFlags(argv.slice(1));
19223
+ if (typeof result.cliArgs.cwd === "string") result.cwd = result.cliArgs.cwd;
19224
+ if (result.cliArgs.verbose === true) result.verbose = true;
19225
+ if (result.cliArgs.quiet === true) result.quiet = true;
19226
+ return result;
19227
+ }
19051
19228
  if (hasExecutable(cmd)) {
19052
19229
  result.command = "__executable__";
19053
19230
  result.executableName = cmd;
@@ -19057,8 +19234,9 @@ function parseArgs(argv) {
19057
19234
  if (result.cliArgs.quiet === true) result.quiet = true;
19058
19235
  return result;
19059
19236
  }
19060
- const discovered = listExecutables().map((e) => e.name);
19061
- const available = ["ci", "chat", "stats", "help", "version", ...discovered];
19237
+ const discoveredActions = listDutyActions().map((e) => e.action);
19238
+ const discoveredExecutables = listExecutables().map((e) => `exec ${e.name}`);
19239
+ const available = ["ci", "chat", "stats", "help", "version", ...discoveredActions, ...discoveredExecutables];
19062
19240
  result.errors.push(`unknown command: ${cmd} (available: ${available.join(", ")})`);
19063
19241
  return result;
19064
19242
  }
@@ -19148,6 +19326,48 @@ ${HELP_TEXT}`);
19148
19326
  }
19149
19327
  const cwd = args.cwd ?? process.cwd();
19150
19328
  const configlessCommands = /* @__PURE__ */ new Set(["init", "goal-scheduler"]);
19329
+ if (args.command === "__duty__") {
19330
+ const route = resolveDutyAction(args.actionName);
19331
+ if (!route) {
19332
+ process.stderr.write(`error: unknown duty action '${args.actionName}'
19333
+ `);
19334
+ return 64;
19335
+ }
19336
+ const cliArgs = { ...route.cliArgs, ...args.cliArgs ?? {} };
19337
+ const skipConfig2 = configlessCommands.has(route.executable);
19338
+ try {
19339
+ const result = await runJob(
19340
+ {
19341
+ action: route.action,
19342
+ duty: route.duty,
19343
+ executable: route.executable,
19344
+ cliArgs,
19345
+ target: numericTarget(cliArgs),
19346
+ flavor: "instant"
19347
+ },
19348
+ {
19349
+ cwd,
19350
+ skipConfig: skipConfig2,
19351
+ verbose: args.verbose,
19352
+ quiet: args.quiet
19353
+ }
19354
+ );
19355
+ if (result.exitCode !== 0 && result.reason) {
19356
+ process.stderr.write(`error: ${result.reason}
19357
+ `);
19358
+ }
19359
+ return result.exitCode;
19360
+ } catch (err) {
19361
+ const msg = err instanceof Error ? err.message : String(err);
19362
+ process.stderr.write(`[kody] ${args.actionName} crashed: ${msg}
19363
+ `);
19364
+ if (err instanceof Error && err.stack) process.stderr.write(`${err.stack}
19365
+ `);
19366
+ process.stdout.write(`PR_URL=FAILED: ${args.actionName} crashed: ${msg}
19367
+ `);
19368
+ return 99;
19369
+ }
19370
+ }
19151
19371
  const skipConfig = configlessCommands.has(args.executableName ?? "");
19152
19372
  try {
19153
19373
  const result = await runExecutableChain(args.executableName, {
@@ -19173,6 +19393,14 @@ ${HELP_TEXT}`);
19173
19393
  return 99;
19174
19394
  }
19175
19395
  }
19396
+ function numericTarget(cliArgs) {
19397
+ for (const key of ["issue", "pr"]) {
19398
+ const raw = cliArgs[key];
19399
+ const n = typeof raw === "number" ? raw : typeof raw === "string" ? parseInt(raw, 10) : Number.NaN;
19400
+ if (Number.isFinite(n) && n > 0) return n;
19401
+ }
19402
+ return void 0;
19403
+ }
19176
19404
 
19177
19405
  // bin/kody.ts
19178
19406
  main().then((code) => {