@kody-ade/kody-engine 0.4.219 → 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 (44) hide show
  1. package/README.md +3 -3
  2. package/dist/bin/kody.js +570 -641
  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 +3 -3
  41. package/dist/executables/worker-ask/profile.json +1 -1
  42. package/package.json +22 -23
  43. package/dist/duties/public-actions.json +0 -86
  44. /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.219",
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,14 +1981,7 @@ var init_agent = __esm({
1981
1981
  }
1982
1982
  });
1983
1983
 
1984
- // src/scripts/jobFrontmatter.ts
1985
- function splitFrontmatter(raw) {
1986
- const match = FRONTMATTER_RE.exec(raw);
1987
- if (!match) return { frontmatter: {}, body: raw };
1988
- const inner = match[1] ?? "";
1989
- const body = raw.slice(match[0].length);
1990
- return { frontmatter: parseFlatYaml(inner), body };
1991
- }
1984
+ // src/scripts/scheduleEvery.ts
1992
1985
  function isScheduleEvery(value) {
1993
1986
  return typeof value === "string" && SCHEDULE_EVERY_VALUES.includes(value);
1994
1987
  }
@@ -2019,55 +2012,9 @@ function scheduleEveryToMs(every) {
2019
2012
  return Number.POSITIVE_INFINITY;
2020
2013
  }
2021
2014
  }
2022
- function parseFlatYaml(text) {
2023
- const out = {};
2024
- for (const rawLine of text.split(/\r?\n/)) {
2025
- const line = rawLine.trim();
2026
- if (!line || line.startsWith("#")) continue;
2027
- const colon = line.indexOf(":");
2028
- if (colon < 0) continue;
2029
- const key = line.slice(0, colon).trim();
2030
- const value = stripQuotes(line.slice(colon + 1).trim());
2031
- if (key === "action" && value.length > 0) {
2032
- out.action = value;
2033
- } else if (key === "executable" && value.length > 0) {
2034
- out.executable = value;
2035
- } else if (key === "every" && isScheduleEvery(value)) {
2036
- out.every = value;
2037
- } else if (key === "tickScript" && value.length > 0) {
2038
- out.tickScript = value;
2039
- } else if (key === "disabled") {
2040
- const lower = value.toLowerCase();
2041
- if (lower === "true") out.disabled = true;
2042
- else if (lower === "false") out.disabled = false;
2043
- } else if (key === "staff" && value.length > 0) {
2044
- out.staff = value;
2045
- } else if (key === "mentions") {
2046
- const logins = value.split(",").map((s) => s.trim().replace(/^@/, "")).filter(Boolean);
2047
- if (logins.length > 0) out.mentions = logins;
2048
- } else if (key === "tools") {
2049
- const names = value.split(",").map((s) => s.trim()).filter(Boolean);
2050
- if (names.length > 0) out.tools = names;
2051
- } else if (key === "executables") {
2052
- const names = value.split(",").map((s) => s.trim()).filter(Boolean);
2053
- if (names.length > 0) out.executables = names;
2054
- }
2055
- }
2056
- return out;
2057
- }
2058
- function stripQuotes(value) {
2059
- if (value.length >= 2) {
2060
- const first = value[0];
2061
- const last = value[value.length - 1];
2062
- if (first === '"' && last === '"' || first === "'" && last === "'") {
2063
- return value.slice(1, -1);
2064
- }
2065
- }
2066
- return value;
2067
- }
2068
- var SCHEDULE_EVERY_VALUES, FRONTMATTER_RE;
2069
- var init_jobFrontmatter = __esm({
2070
- "src/scripts/jobFrontmatter.ts"() {
2015
+ var SCHEDULE_EVERY_VALUES;
2016
+ var init_scheduleEvery = __esm({
2017
+ "src/scripts/scheduleEvery.ts"() {
2071
2018
  "use strict";
2072
2019
  SCHEDULE_EVERY_VALUES = [
2073
2020
  "15m",
@@ -2081,87 +2028,177 @@ var init_jobFrontmatter = __esm({
2081
2028
  "7d",
2082
2029
  "manual"
2083
2030
  ];
2084
- FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
2085
2031
  }
2086
2032
  });
2087
2033
 
2088
- // src/registry.ts
2034
+ // src/dutyFolders.ts
2089
2035
  import * as fs6 from "fs";
2090
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";
2091
2139
  function getExecutablesRoot() {
2092
- const here = path6.dirname(new URL(import.meta.url).pathname);
2140
+ const here = path7.dirname(new URL(import.meta.url).pathname);
2093
2141
  const candidates = [
2094
- path6.join(here, "executables"),
2142
+ path7.join(here, "executables"),
2095
2143
  // dev: src/
2096
- path6.join(here, "..", "executables"),
2144
+ path7.join(here, "..", "executables"),
2097
2145
  // built: dist/bin → dist/executables
2098
- path6.join(here, "..", "src", "executables")
2146
+ path7.join(here, "..", "src", "executables")
2099
2147
  // fallback
2100
2148
  ];
2101
2149
  for (const c of candidates) {
2102
- if (fs6.existsSync(c) && fs6.statSync(c).isDirectory()) return c;
2150
+ if (fs7.existsSync(c) && fs7.statSync(c).isDirectory()) return c;
2103
2151
  }
2104
2152
  return candidates[0];
2105
2153
  }
2106
2154
  function getProjectExecutablesRoot() {
2107
- return path6.join(process.cwd(), ".kody", "executables");
2155
+ return path7.join(process.cwd(), ".kody", "executables");
2108
2156
  }
2109
2157
  function getProjectDutiesRoot() {
2110
- return path6.join(process.cwd(), ".kody", "duties");
2158
+ return path7.join(process.cwd(), ".kody", "duties");
2111
2159
  }
2112
2160
  function getBuiltinJobsRoot() {
2113
- const here = path6.dirname(new URL(import.meta.url).pathname);
2161
+ const here = path7.dirname(new URL(import.meta.url).pathname);
2114
2162
  const candidates = [
2115
- path6.join(here, "jobs"),
2163
+ path7.join(here, "jobs"),
2116
2164
  // dev: src/
2117
- path6.join(here, "..", "jobs"),
2165
+ path7.join(here, "..", "jobs"),
2118
2166
  // built: dist/bin → dist/jobs
2119
- path6.join(here, "..", "src", "jobs")
2167
+ path7.join(here, "..", "src", "jobs")
2120
2168
  // fallback
2121
2169
  ];
2122
2170
  for (const c of candidates) {
2123
- if (fs6.existsSync(c) && fs6.statSync(c).isDirectory()) return c;
2171
+ if (fs7.existsSync(c) && fs7.statSync(c).isDirectory()) return c;
2124
2172
  }
2125
2173
  return candidates[0];
2126
2174
  }
2127
2175
  function getBuiltinDutiesRoot() {
2128
- const here = path6.dirname(new URL(import.meta.url).pathname);
2176
+ const here = path7.dirname(new URL(import.meta.url).pathname);
2129
2177
  const candidates = [
2130
- path6.join(here, "duties"),
2178
+ path7.join(here, "duties"),
2131
2179
  // dev: src/
2132
- path6.join(here, "..", "duties"),
2180
+ path7.join(here, "..", "duties"),
2133
2181
  // built: dist/bin → dist/duties
2134
- path6.join(here, "..", "src", "duties")
2182
+ path7.join(here, "..", "src", "duties")
2135
2183
  // fallback
2136
2184
  ];
2137
2185
  for (const c of candidates) {
2138
- if (fs6.existsSync(c) && fs6.statSync(c).isDirectory()) return c;
2186
+ if (fs7.existsSync(c) && fs7.statSync(c).isDirectory()) return c;
2139
2187
  }
2140
2188
  return candidates[0];
2141
2189
  }
2142
2190
  function listBuiltinJobs(root = getBuiltinJobsRoot()) {
2143
- if (!fs6.existsSync(root) || !fs6.statSync(root).isDirectory()) return [];
2191
+ if (!fs7.existsSync(root) || !fs7.statSync(root).isDirectory()) return [];
2144
2192
  const out = [];
2145
- for (const ent of fs6.readdirSync(root, { withFileTypes: true })) {
2193
+ for (const ent of fs7.readdirSync(root, { withFileTypes: true })) {
2146
2194
  if (ent.name.startsWith("_") || ent.name.startsWith(".")) continue;
2147
- const full = path6.join(root, ent.name);
2195
+ const full = path7.join(root, ent.name);
2148
2196
  if (ent.isDirectory()) {
2149
- const profilePath = path6.join(full, "profile.json");
2150
- const promptPath = path6.join(full, "prompt.md");
2151
- if (!fs6.existsSync(profilePath) || !fs6.statSync(profilePath).isFile()) continue;
2152
- if (!fs6.existsSync(promptPath) || !fs6.statSync(promptPath).isFile()) continue;
2153
- out.push({ slug: ent.name, dir: full, profilePath, promptPath });
2154
- continue;
2155
- }
2156
- if (ent.isFile() && ent.name.endsWith(".md")) {
2157
- const slug = ent.name.slice(0, -3);
2158
- out.push({
2159
- slug,
2160
- dir: full,
2161
- profilePath: "",
2162
- promptPath: "",
2163
- filePath: full
2164
- });
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 });
2165
2202
  }
2166
2203
  }
2167
2204
  out.sort((a, b) => a.slug.localeCompare(b.slug));
@@ -2175,8 +2212,8 @@ function builtinExecutableNames() {
2175
2212
  const out = /* @__PURE__ */ new Set();
2176
2213
  const root = getExecutablesRoot();
2177
2214
  try {
2178
- for (const ent of fs6.readdirSync(root, { withFileTypes: true })) {
2179
- 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);
2180
2217
  }
2181
2218
  } catch {
2182
2219
  }
@@ -2192,14 +2229,15 @@ function listExecutables(roots = getExecutableRoots()) {
2192
2229
  const seen = /* @__PURE__ */ new Set();
2193
2230
  const out = [];
2194
2231
  for (const root of rootList) {
2195
- if (!fs6.existsSync(root)) continue;
2196
- const entries = fs6.readdirSync(root, { withFileTypes: true });
2232
+ if (!fs7.existsSync(root)) continue;
2233
+ const entries = fs7.readdirSync(root, { withFileTypes: true });
2197
2234
  for (const ent of entries) {
2198
2235
  if (!ent.isDirectory()) continue;
2199
2236
  if (seen.has(ent.name)) continue;
2200
2237
  if (root === dutiesRoot && isBuiltinExecutable(ent.name)) continue;
2201
- const profilePath = path6.join(root, ent.name, "profile.json");
2202
- 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()) {
2203
2241
  out.push({ name: ent.name, profilePath });
2204
2242
  seen.add(ent.name);
2205
2243
  }
@@ -2213,8 +2251,9 @@ function resolveExecutable(name, roots = getExecutableRoots()) {
2213
2251
  const dutiesRoot = getProjectDutiesRoot();
2214
2252
  for (const root of rootList) {
2215
2253
  if (root === dutiesRoot && isBuiltinExecutable(name)) continue;
2216
- const profilePath = path6.join(root, name, "profile.json");
2217
- 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()) {
2218
2257
  return profilePath;
2219
2258
  }
2220
2259
  }
@@ -2233,7 +2272,6 @@ function listDutyActions(projectDutiesRoot = getProjectDutiesRoot()) {
2233
2272
  out.push(action);
2234
2273
  };
2235
2274
  for (const action of listProjectFolderDutyActions(projectDutiesRoot)) add(action);
2236
- for (const action of listProjectMarkdownDutyActions(projectDutiesRoot)) add(action);
2237
2275
  for (const action of listBuiltinDutyActions()) add(action);
2238
2276
  return out.sort((a, b) => a.action.localeCompare(b.action));
2239
2277
  }
@@ -2248,108 +2286,54 @@ function isSafeName(name) {
2248
2286
  return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
2249
2287
  }
2250
2288
  function listProjectFolderDutyActions(root) {
2251
- if (!fs6.existsSync(root) || !fs6.statSync(root).isDirectory()) return [];
2289
+ if (!fs7.existsSync(root) || !fs7.statSync(root).isDirectory()) return [];
2252
2290
  const out = [];
2253
- for (const ent of fs6.readdirSync(root, { withFileTypes: true })) {
2254
- if (!ent.isDirectory() || ent.name.startsWith(".") || ent.name.startsWith("_")) continue;
2255
- if (!isSafeName(ent.name)) continue;
2256
- const profilePath = path6.join(root, ent.name, "profile.json");
2257
- if (!fs6.existsSync(profilePath) || !fs6.statSync(profilePath).isFile()) continue;
2258
- try {
2259
- const raw = JSON.parse(fs6.readFileSync(profilePath, "utf-8"));
2260
- const action = stringOr(raw.action, ent.name);
2261
- const executable = stringOr(raw.executable, ent.name);
2262
- out.push({
2263
- action,
2264
- duty: ent.name,
2265
- executable,
2266
- cliArgs: {},
2267
- source: "project-folder",
2268
- describe: typeof raw.describe === "string" ? raw.describe : void 0,
2269
- profilePath
2270
- });
2271
- } catch {
2272
- continue;
2273
- }
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
+ });
2274
2307
  }
2275
2308
  return out.sort((a, b) => a.action.localeCompare(b.action));
2276
2309
  }
2277
- function listProjectMarkdownDutyActions(root) {
2278
- if (!fs6.existsSync(root) || !fs6.statSync(root).isDirectory()) return [];
2310
+ function listBuiltinDutyActions(root = getBuiltinDutiesRoot()) {
2311
+ if (!fs7.existsSync(root) || !fs7.statSync(root).isDirectory()) return [];
2279
2312
  const out = [];
2280
- for (const ent of fs6.readdirSync(root, { withFileTypes: true })) {
2281
- if (!ent.isFile() || !ent.name.endsWith(".md")) continue;
2282
- const duty = ent.name.slice(0, -3);
2283
- if (!isSafeName(duty)) continue;
2284
- const filePath = path6.join(root, ent.name);
2285
- try {
2286
- const raw = fs6.readFileSync(filePath, "utf-8");
2287
- const { frontmatter } = splitFrontmatter(raw);
2288
- const action = frontmatter.action?.trim() || duty;
2289
- const implementation = markdownDutyImplementation(duty, frontmatter);
2290
- out.push({
2291
- action,
2292
- duty,
2293
- executable: implementation.executable,
2294
- cliArgs: implementation.cliArgs,
2295
- source: "project-markdown",
2296
- filePath
2297
- });
2298
- } catch {
2299
- continue;
2300
- }
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
+ });
2301
2329
  }
2302
2330
  return out.sort((a, b) => a.action.localeCompare(b.action));
2303
2331
  }
2304
- function markdownDutyImplementation(duty, frontmatter) {
2305
- if (frontmatter.executable?.trim()) {
2306
- return { executable: frontmatter.executable.trim(), cliArgs: {} };
2307
- }
2308
- if (frontmatter.executables?.length === 1 && frontmatter.executables[0]?.trim()) {
2309
- return { executable: frontmatter.executables[0].trim(), cliArgs: {} };
2310
- }
2311
- if (frontmatter.tickScript?.trim()) {
2312
- return { executable: "duty-tick-scripted", cliArgs: { duty } };
2313
- }
2314
- return { executable: "duty-tick", cliArgs: { duty } };
2315
- }
2316
- function listBuiltinDutyActions(root = getBuiltinDutiesRoot()) {
2317
- const filePath = path6.join(root, "public-actions.json");
2318
- if (!fs6.existsSync(filePath) || !fs6.statSync(filePath).isFile()) return [];
2319
- try {
2320
- const raw = JSON.parse(fs6.readFileSync(filePath, "utf-8"));
2321
- if (!Array.isArray(raw)) return [];
2322
- const out = [];
2323
- for (const item of raw) {
2324
- if (!item || typeof item !== "object") continue;
2325
- const r = item;
2326
- const duty = stringOr(r.duty, stringOr(r.name, ""));
2327
- const action = stringOr(r.action, duty);
2328
- const executable = stringOr(r.executable, duty);
2329
- if (!duty || !action || !executable) continue;
2330
- out.push({
2331
- action,
2332
- duty,
2333
- executable,
2334
- cliArgs: {},
2335
- source: "builtin",
2336
- describe: typeof r.describe === "string" ? r.describe : void 0,
2337
- filePath
2338
- });
2339
- }
2340
- return out.sort((a, b) => a.action.localeCompare(b.action));
2341
- } catch {
2342
- return [];
2343
- }
2344
- }
2345
- function stringOr(value, fallback) {
2346
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : fallback;
2347
- }
2348
2332
  function getProfileInputs(name, roots = getExecutableRoots()) {
2349
2333
  const profilePath = resolveExecutable(name, roots);
2350
2334
  if (!profilePath) return null;
2351
2335
  try {
2352
- const raw = JSON.parse(fs6.readFileSync(profilePath, "utf-8"));
2336
+ const raw = JSON.parse(fs7.readFileSync(profilePath, "utf-8"));
2353
2337
  if (!raw || typeof raw !== "object" || !Array.isArray(raw.inputs)) return [];
2354
2338
  return raw.inputs;
2355
2339
  } catch {
@@ -2385,27 +2369,27 @@ var _builtinNames;
2385
2369
  var init_registry = __esm({
2386
2370
  "src/registry.ts"() {
2387
2371
  "use strict";
2388
- init_jobFrontmatter();
2372
+ init_dutyFolders();
2389
2373
  _builtinNames = null;
2390
2374
  }
2391
2375
  });
2392
2376
 
2393
2377
  // src/task-artifacts.ts
2394
- import fs7 from "fs";
2395
- import path7 from "path";
2378
+ import fs8 from "fs";
2379
+ import path8 from "path";
2396
2380
  function prepareTaskArtifactsDir(cwd, taskId) {
2397
2381
  const safeId = String(taskId).replace(/[^a-zA-Z0-9._-]/g, "_");
2398
- const relDir = path7.join(".kody", "tasks", safeId);
2399
- const absDir = path7.join(cwd, relDir);
2400
- 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 });
2401
2385
  return { taskId: safeId, absDir, relDir };
2402
2386
  }
2403
2387
  function verifyTaskArtifacts(absDir) {
2404
2388
  const missing = [];
2405
2389
  for (const name of TASK_ARTIFACT_FILES) {
2406
- const full = path7.join(absDir, name);
2390
+ const full = path8.join(absDir, name);
2407
2391
  try {
2408
- const stat = fs7.statSync(full);
2392
+ const stat = fs8.statSync(full);
2409
2393
  if (!stat.isFile() || stat.size === 0) missing.push(name);
2410
2394
  } catch {
2411
2395
  missing.push(name);
@@ -2639,31 +2623,31 @@ var init_lifecycles = __esm({
2639
2623
  });
2640
2624
 
2641
2625
  // src/scripts/buildSyntheticPlugin.ts
2642
- import * as fs13 from "fs";
2626
+ import * as fs14 from "fs";
2643
2627
  import * as os2 from "os";
2644
- import * as path12 from "path";
2628
+ import * as path13 from "path";
2645
2629
  function getPluginsCatalogRoot() {
2646
- const here = path12.dirname(new URL(import.meta.url).pathname);
2630
+ const here = path13.dirname(new URL(import.meta.url).pathname);
2647
2631
  const candidates = [
2648
- path12.join(here, "..", "plugins"),
2632
+ path13.join(here, "..", "plugins"),
2649
2633
  // dev: src/scripts → src/plugins
2650
- path12.join(here, "..", "..", "plugins"),
2634
+ path13.join(here, "..", "..", "plugins"),
2651
2635
  // built: dist/scripts → dist/plugins
2652
- path12.join(here, "..", "..", "src", "plugins")
2636
+ path13.join(here, "..", "..", "src", "plugins")
2653
2637
  // fallback
2654
2638
  ];
2655
2639
  for (const c of candidates) {
2656
- if (fs13.existsSync(c) && fs13.statSync(c).isDirectory()) return c;
2640
+ if (fs14.existsSync(c) && fs14.statSync(c).isDirectory()) return c;
2657
2641
  }
2658
2642
  return candidates[0];
2659
2643
  }
2660
2644
  function copyDir(src, dst) {
2661
- fs13.mkdirSync(dst, { recursive: true });
2662
- for (const ent of fs13.readdirSync(src, { withFileTypes: true })) {
2663
- const s = path12.join(src, ent.name);
2664
- 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);
2665
2649
  if (ent.isDirectory()) copyDir(s, d);
2666
- else if (ent.isFile()) fs13.copyFileSync(s, d);
2650
+ else if (ent.isFile()) fs14.copyFileSync(s, d);
2667
2651
  }
2668
2652
  }
2669
2653
  var buildSyntheticPlugin;
@@ -2676,45 +2660,45 @@ var init_buildSyntheticPlugin = __esm({
2676
2660
  if (!needsSynthetic) return;
2677
2661
  const catalog = getPluginsCatalogRoot();
2678
2662
  const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2679
- const root = path12.join(os2.tmpdir(), `kody-synth-${runId}`);
2680
- 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 });
2681
2665
  const resolvePart = (bucket, entry) => {
2682
- const local = path12.join(profile.dir, bucket, entry);
2683
- if (fs13.existsSync(local)) return local;
2684
- const central = path12.join(catalog, bucket, entry);
2685
- 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;
2686
2670
  throw new Error(
2687
2671
  `buildSyntheticPlugin: ${bucket} entry '${entry}' not found in executable dir (${profile.dir}/${bucket}/) or catalog (${catalog}/${bucket}/)`
2688
2672
  );
2689
2673
  };
2690
2674
  if (cc.skills.length > 0) {
2691
- const dst = path12.join(root, "skills");
2692
- fs13.mkdirSync(dst, { recursive: true });
2675
+ const dst = path13.join(root, "skills");
2676
+ fs14.mkdirSync(dst, { recursive: true });
2693
2677
  for (const name of cc.skills) {
2694
- copyDir(resolvePart("skills", name), path12.join(dst, name));
2678
+ copyDir(resolvePart("skills", name), path13.join(dst, name));
2695
2679
  }
2696
2680
  }
2697
2681
  if (cc.commands.length > 0) {
2698
- const dst = path12.join(root, "commands");
2699
- fs13.mkdirSync(dst, { recursive: true });
2682
+ const dst = path13.join(root, "commands");
2683
+ fs14.mkdirSync(dst, { recursive: true });
2700
2684
  for (const name of cc.commands) {
2701
- fs13.copyFileSync(resolvePart("commands", `${name}.md`), path12.join(dst, `${name}.md`));
2685
+ fs14.copyFileSync(resolvePart("commands", `${name}.md`), path13.join(dst, `${name}.md`));
2702
2686
  }
2703
2687
  }
2704
2688
  if (cc.hooks.length > 0) {
2705
- const dst = path12.join(root, "hooks");
2706
- fs13.mkdirSync(dst, { recursive: true });
2689
+ const dst = path13.join(root, "hooks");
2690
+ fs14.mkdirSync(dst, { recursive: true });
2707
2691
  const merged = { hooks: {} };
2708
2692
  for (const name of cc.hooks) {
2709
2693
  const src = resolvePart("hooks", `${name}.json`);
2710
- const parsed = JSON.parse(fs13.readFileSync(src, "utf-8"));
2694
+ const parsed = JSON.parse(fs14.readFileSync(src, "utf-8"));
2711
2695
  for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
2712
2696
  if (!Array.isArray(entries)) continue;
2713
2697
  if (!merged.hooks[event]) merged.hooks[event] = [];
2714
2698
  merged.hooks[event].push(...entries);
2715
2699
  }
2716
2700
  }
2717
- 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)}
2718
2702
  `);
2719
2703
  }
2720
2704
  const manifest = {
@@ -2724,7 +2708,7 @@ var init_buildSyntheticPlugin = __esm({
2724
2708
  };
2725
2709
  if (cc.skills.length > 0) manifest.skills = ["./skills/"];
2726
2710
  if (cc.commands.length > 0) manifest.commands = ["./commands/"];
2727
- 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)}
2728
2712
  `);
2729
2713
  ctx.data.syntheticPluginPath = root;
2730
2714
  };
@@ -2732,9 +2716,9 @@ var init_buildSyntheticPlugin = __esm({
2732
2716
  });
2733
2717
 
2734
2718
  // src/subagents.ts
2735
- import * as fs14 from "fs";
2736
- import * as path13 from "path";
2737
- function splitFrontmatter2(raw) {
2719
+ import * as fs15 from "fs";
2720
+ import * as path14 from "path";
2721
+ function splitFrontmatter(raw) {
2738
2722
  const match = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(raw);
2739
2723
  if (!match) return { fm: {}, body: raw.trim() };
2740
2724
  const fm = {};
@@ -2746,10 +2730,10 @@ function splitFrontmatter2(raw) {
2746
2730
  return { fm, body: (match[2] ?? "").trim() };
2747
2731
  }
2748
2732
  function resolveAgentFile(profileDir, name) {
2749
- const local = path13.join(profileDir, "agents", `${name}.md`);
2750
- if (fs14.existsSync(local)) return local;
2751
- const central = path13.join(getPluginsCatalogRoot(), "agents", `${name}.md`);
2752
- 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;
2753
2737
  throw new Error(`loadSubagents: agent '${name}' not found in ${profileDir}/agents/ or shared catalog`);
2754
2738
  }
2755
2739
  function captureSubagentTemplates(profile) {
@@ -2758,7 +2742,7 @@ function captureSubagentTemplates(profile) {
2758
2742
  const out = {};
2759
2743
  for (const name of names) {
2760
2744
  try {
2761
- out[name] = fs14.readFileSync(resolveAgentFile(profile.dir, name), "utf-8");
2745
+ out[name] = fs15.readFileSync(resolveAgentFile(profile.dir, name), "utf-8");
2762
2746
  } catch {
2763
2747
  }
2764
2748
  }
@@ -2769,8 +2753,8 @@ function loadSubagents(profile) {
2769
2753
  if (!names || names.length === 0) return void 0;
2770
2754
  const agents = {};
2771
2755
  for (const name of names) {
2772
- const raw = profile.subagentTemplates?.[name] ?? fs14.readFileSync(resolveAgentFile(profile.dir, name), "utf-8");
2773
- const { fm, body } = splitFrontmatter2(raw);
2756
+ const raw = profile.subagentTemplates?.[name] ?? fs15.readFileSync(resolveAgentFile(profile.dir, name), "utf-8");
2757
+ const { fm, body } = splitFrontmatter(raw);
2774
2758
  if (!body) throw new Error(`loadSubagents: agent '${name}' has an empty prompt body`);
2775
2759
  const def = {
2776
2760
  description: fm.description ?? `Subagent ${name}`,
@@ -2793,15 +2777,15 @@ var init_subagents = __esm({
2793
2777
  });
2794
2778
 
2795
2779
  // src/profile.ts
2796
- import * as fs15 from "fs";
2797
- import * as path14 from "path";
2780
+ import * as fs16 from "fs";
2781
+ import * as path15 from "path";
2798
2782
  function loadProfile(profilePath) {
2799
- if (!fs15.existsSync(profilePath)) {
2783
+ if (!fs16.existsSync(profilePath)) {
2800
2784
  throw new ProfileError(profilePath, "file not found");
2801
2785
  }
2802
2786
  let raw;
2803
2787
  try {
2804
- raw = JSON.parse(fs15.readFileSync(profilePath, "utf-8"));
2788
+ raw = JSON.parse(fs16.readFileSync(profilePath, "utf-8"));
2805
2789
  } catch (err) {
2806
2790
  throw new ProfileError(profilePath, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
2807
2791
  }
@@ -2812,7 +2796,7 @@ function loadProfile(profilePath) {
2812
2796
  const unknownKeys = Object.keys(r).filter((k) => !KNOWN_PROFILE_KEYS.has(k));
2813
2797
  if (unknownKeys.length > 0) {
2814
2798
  process.stderr.write(
2815
- `[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(", ")}
2816
2800
  `
2817
2801
  );
2818
2802
  }
@@ -2831,7 +2815,7 @@ function loadProfile(profilePath) {
2831
2815
  describe: typeof r.describe === "string" ? r.describe : base.describe,
2832
2816
  staff: typeof r.staff === "string" && r.staff.trim() ? r.staff.trim() : base.staff,
2833
2817
  every: typeof r.every === "string" && r.every.trim() ? r.every.trim() : void 0,
2834
- 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,
2835
2819
  mentions: Array.isArray(r.mentions) ? r.mentions.map((m) => String(m).trim()).filter(Boolean) : base.mentions
2836
2820
  };
2837
2821
  }
@@ -2877,8 +2861,8 @@ function loadProfile(profilePath) {
2877
2861
  staff: typeof r.staff === "string" && r.staff.trim() ? r.staff.trim() : void 0,
2878
2862
  // Optional recurrence cadence (scheduled duty). Blank → undefined (on-demand).
2879
2863
  every: typeof r.every === "string" && r.every.trim() ? r.every.trim() : void 0,
2880
- // Locked-toolbox palette + mentions (folder-duty successors to frontmatter).
2881
- 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),
2882
2866
  mentions: Array.isArray(r.mentions) ? r.mentions.map((m) => String(m).trim()).filter(Boolean) : void 0,
2883
2867
  role,
2884
2868
  kind,
@@ -2901,8 +2885,8 @@ function loadProfile(profilePath) {
2901
2885
  // Phase 5 in-process handoff opt-in. Default false; containers
2902
2886
  // flip to true after end-to-end verification.
2903
2887
  preloadContext: r.preloadContext === true,
2904
- dir: path14.dirname(profilePath),
2905
- promptTemplates: readPromptTemplates(path14.dirname(profilePath))
2888
+ dir: path15.dirname(profilePath),
2889
+ promptTemplates: readPromptTemplates(path15.dirname(profilePath))
2906
2890
  };
2907
2891
  if (lifecycle) {
2908
2892
  applyLifecycle(profile, profilePath);
@@ -2934,15 +2918,16 @@ function readPromptTemplates(dir) {
2934
2918
  const out = {};
2935
2919
  const read = (p) => {
2936
2920
  try {
2937
- out[p] = fs15.readFileSync(p, "utf-8");
2921
+ out[p] = fs16.readFileSync(p, "utf-8");
2938
2922
  } catch {
2939
2923
  }
2940
2924
  };
2941
- read(path14.join(dir, "prompt.md"));
2925
+ read(path15.join(dir, "prompt.md"));
2926
+ read(path15.join(dir, "duty.md"));
2942
2927
  try {
2943
- const promptsDir = path14.join(dir, "prompts");
2944
- for (const ent of fs15.readdirSync(promptsDir)) {
2945
- 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));
2946
2931
  }
2947
2932
  } catch {
2948
2933
  }
@@ -2962,6 +2947,11 @@ function requireString(p, r, key) {
2962
2947
  }
2963
2948
  return v;
2964
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
+ }
2965
2955
  function parseInputs(p, raw) {
2966
2956
  if (!Array.isArray(raw)) throw new ProfileError(p, `"inputs" must be an array`);
2967
2957
  const out = [];
@@ -3213,7 +3203,11 @@ var init_profile = __esm({
3213
3203
  "staff",
3214
3204
  "every",
3215
3205
  "dutyTools",
3206
+ "tools",
3216
3207
  "mentions",
3208
+ "stage",
3209
+ "readsFrom",
3210
+ "writesTo",
3217
3211
  "describe",
3218
3212
  "role",
3219
3213
  "kind",
@@ -3610,16 +3604,16 @@ var init_state = __esm({
3610
3604
  });
3611
3605
 
3612
3606
  // src/prompt.ts
3613
- import * as fs16 from "fs";
3614
- import * as path15 from "path";
3607
+ import * as fs17 from "fs";
3608
+ import * as path16 from "path";
3615
3609
  function loadProjectConventions(projectDir) {
3616
3610
  const out = [];
3617
3611
  for (const rel of CONVENTION_FILES) {
3618
- const abs = path15.join(projectDir, rel);
3619
- if (!fs16.existsSync(abs)) continue;
3612
+ const abs = path16.join(projectDir, rel);
3613
+ if (!fs17.existsSync(abs)) continue;
3620
3614
  let content;
3621
3615
  try {
3622
- content = fs16.readFileSync(abs, "utf-8");
3616
+ content = fs17.readFileSync(abs, "utf-8");
3623
3617
  } catch {
3624
3618
  continue;
3625
3619
  }
@@ -3854,28 +3848,28 @@ var loadMemoryContext_exports = {};
3854
3848
  __export(loadMemoryContext_exports, {
3855
3849
  loadMemoryContext: () => loadMemoryContext
3856
3850
  });
3857
- import * as fs17 from "fs";
3858
- import * as path16 from "path";
3851
+ import * as fs18 from "fs";
3852
+ import * as path17 from "path";
3859
3853
  function collectPages(memoryAbs) {
3860
3854
  const out = [];
3861
3855
  walkMd(memoryAbs, (file) => {
3862
3856
  let stat;
3863
3857
  try {
3864
- stat = fs17.statSync(file);
3858
+ stat = fs18.statSync(file);
3865
3859
  } catch {
3866
3860
  return;
3867
3861
  }
3868
3862
  let raw;
3869
3863
  try {
3870
- raw = fs17.readFileSync(file, "utf-8");
3864
+ raw = fs18.readFileSync(file, "utf-8");
3871
3865
  } catch {
3872
3866
  return;
3873
3867
  }
3874
3868
  const fm = raw.match(/^---\s*\n([\s\S]*?)\n---/);
3875
- 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");
3876
3870
  const updated = fm?.[1]?.match(/^updated:\s*([0-9T:.+\-Z]+)/m)?.[1]?.trim() ?? "";
3877
3871
  out.push({
3878
- relPath: path16.relative(memoryAbs, file),
3872
+ relPath: path17.relative(memoryAbs, file),
3879
3873
  title,
3880
3874
  updated,
3881
3875
  content: raw.length > PER_PAGE_MAX_BYTES ? raw.slice(0, PER_PAGE_MAX_BYTES) + TRUNCATED_SUFFIX2 : raw,
@@ -3943,16 +3937,16 @@ function walkMd(root, visit) {
3943
3937
  const dir = stack.pop();
3944
3938
  let names;
3945
3939
  try {
3946
- names = fs17.readdirSync(dir);
3940
+ names = fs18.readdirSync(dir);
3947
3941
  } catch {
3948
3942
  continue;
3949
3943
  }
3950
3944
  for (const name of names) {
3951
3945
  if (name.startsWith(".")) continue;
3952
- const full = path16.join(dir, name);
3946
+ const full = path17.join(dir, name);
3953
3947
  let stat;
3954
3948
  try {
3955
- stat = fs17.statSync(full);
3949
+ stat = fs18.statSync(full);
3956
3950
  } catch {
3957
3951
  continue;
3958
3952
  }
@@ -3975,8 +3969,8 @@ var init_loadMemoryContext = __esm({
3975
3969
  TRUNCATED_SUFFIX2 = "\n\n\u2026 (truncated)";
3976
3970
  loadMemoryContext = async (ctx) => {
3977
3971
  if (typeof ctx.data.memoryContext === "string") return;
3978
- const memoryAbs = path16.join(ctx.cwd, MEMORY_DIR_RELATIVE);
3979
- if (!fs17.existsSync(memoryAbs)) {
3972
+ const memoryAbs = path17.join(ctx.cwd, MEMORY_DIR_RELATIVE);
3973
+ if (!fs18.existsSync(memoryAbs)) {
3980
3974
  ctx.data.memoryContext = "";
3981
3975
  return;
3982
3976
  }
@@ -4020,11 +4014,11 @@ var init_loadCoverageRules = __esm({
4020
4014
 
4021
4015
  // src/container.ts
4022
4016
  import { execFileSync as execFileSync3 } from "child_process";
4023
- import * as fs18 from "fs";
4017
+ import * as fs19 from "fs";
4024
4018
  function getProfileInputsForChild(profileName, _cwd) {
4025
4019
  try {
4026
4020
  const profilePath = resolveProfilePath(profileName);
4027
- if (!fs18.existsSync(profilePath)) return null;
4021
+ if (!fs19.existsSync(profilePath)) return null;
4028
4022
  return loadProfile(profilePath).inputs;
4029
4023
  } catch {
4030
4024
  return null;
@@ -4486,9 +4480,9 @@ var init_lifecycleLabels = __esm({
4486
4480
 
4487
4481
  // src/litellm.ts
4488
4482
  import { execFileSync as execFileSync4, spawn as spawn3 } from "child_process";
4489
- import * as fs19 from "fs";
4483
+ import * as fs20 from "fs";
4490
4484
  import * as os3 from "os";
4491
- import * as path17 from "path";
4485
+ import * as path18 from "path";
4492
4486
  async function checkLitellmHealth(url) {
4493
4487
  try {
4494
4488
  const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
@@ -4573,13 +4567,13 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
4573
4567
  let child;
4574
4568
  let logPath;
4575
4569
  const spawnProxy = () => {
4576
- const configPath = path17.join(os3.tmpdir(), `kody-local-litellm-${Date.now()}.yaml`);
4577
- fs19.writeFileSync(configPath, generateLitellmConfigYaml(model));
4570
+ const configPath = path18.join(os3.tmpdir(), `kody-local-litellm-${Date.now()}.yaml`);
4571
+ fs20.writeFileSync(configPath, generateLitellmConfigYaml(model));
4578
4572
  const args = ["--config", configPath, "--port", port];
4579
- const nextLogPath = path17.join(os3.tmpdir(), `kody-local-litellm-${Date.now()}.log`);
4580
- 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");
4581
4575
  child = spawn3(cmd, args, { stdio: ["ignore", outFd, outFd], detached: true, env: childEnv });
4582
- fs19.closeSync(outFd);
4576
+ fs20.closeSync(outFd);
4583
4577
  logPath = nextLogPath;
4584
4578
  };
4585
4579
  const waitForHealth = async () => {
@@ -4593,7 +4587,7 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
4593
4587
  const readLogTail = () => {
4594
4588
  if (!logPath) return "";
4595
4589
  try {
4596
- return fs19.readFileSync(logPath, "utf-8").slice(-2e3);
4590
+ return fs20.readFileSync(logPath, "utf-8").slice(-2e3);
4597
4591
  } catch {
4598
4592
  return "";
4599
4593
  }
@@ -4645,10 +4639,10 @@ ${tail}`
4645
4639
  return { url, kill: killChild, isHealthy, ensureHealthy };
4646
4640
  }
4647
4641
  function readDotenvApiKeys(projectDir) {
4648
- const dotenvPath = path17.join(projectDir, ".env");
4649
- if (!fs19.existsSync(dotenvPath)) return {};
4642
+ const dotenvPath = path18.join(projectDir, ".env");
4643
+ if (!fs20.existsSync(dotenvPath)) return {};
4650
4644
  const result = {};
4651
- for (const rawLine of fs19.readFileSync(dotenvPath, "utf-8").split("\n")) {
4645
+ for (const rawLine of fs20.readFileSync(dotenvPath, "utf-8").split("\n")) {
4652
4646
  const line = rawLine.trim();
4653
4647
  if (!line || line.startsWith("#")) continue;
4654
4648
  const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
@@ -4763,8 +4757,8 @@ var init_pushWithRetry = __esm({
4763
4757
 
4764
4758
  // src/commit.ts
4765
4759
  import { execFileSync as execFileSync6 } from "child_process";
4766
- import * as fs20 from "fs";
4767
- import * as path18 from "path";
4760
+ import * as fs21 from "fs";
4761
+ import * as path19 from "path";
4768
4762
  function git(args, cwd) {
4769
4763
  try {
4770
4764
  return execFileSync6("git", args, {
@@ -4802,18 +4796,18 @@ function ensureGitIdentity(cwd) {
4802
4796
  }
4803
4797
  function abortUnfinishedGitOps(cwd) {
4804
4798
  const aborted = [];
4805
- const gitDir = path18.join(cwd ?? process.cwd(), ".git");
4806
- if (!fs20.existsSync(gitDir)) return aborted;
4807
- 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"))) {
4808
4802
  if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
4809
4803
  }
4810
- if (fs20.existsSync(path18.join(gitDir, "CHERRY_PICK_HEAD"))) {
4804
+ if (fs21.existsSync(path19.join(gitDir, "CHERRY_PICK_HEAD"))) {
4811
4805
  if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
4812
4806
  }
4813
- if (fs20.existsSync(path18.join(gitDir, "REVERT_HEAD"))) {
4807
+ if (fs21.existsSync(path19.join(gitDir, "REVERT_HEAD"))) {
4814
4808
  if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
4815
4809
  }
4816
- 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"))) {
4817
4811
  if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
4818
4812
  }
4819
4813
  try {
@@ -4869,7 +4863,7 @@ function normalizeCommitMessage(raw) {
4869
4863
  function commitAndPush(branch, agentMessage, cwd) {
4870
4864
  const allChanged = listChangedFiles(cwd);
4871
4865
  const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
4872
- 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"));
4873
4867
  if (allowedFiles.length === 0 && !mergeHeadExists) {
4874
4868
  return { committed: false, pushed: false, sha: "", message: "" };
4875
4869
  }
@@ -5106,7 +5100,7 @@ var init_advanceFlow = __esm({
5106
5100
 
5107
5101
  // src/gha.ts
5108
5102
  import { execFileSync as execFileSync8 } from "child_process";
5109
- import * as fs21 from "fs";
5103
+ import * as fs22 from "fs";
5110
5104
  function getRunUrl() {
5111
5105
  const server = process.env.GITHUB_SERVER_URL;
5112
5106
  const repo = process.env.GITHUB_REPOSITORY;
@@ -5117,10 +5111,10 @@ function getRunUrl() {
5117
5111
  function reactToTriggerComment(cwd) {
5118
5112
  if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
5119
5113
  const eventPath = process.env.GITHUB_EVENT_PATH;
5120
- if (!eventPath || !fs21.existsSync(eventPath)) return;
5114
+ if (!eventPath || !fs22.existsSync(eventPath)) return;
5121
5115
  let event = null;
5122
5116
  try {
5123
- event = JSON.parse(fs21.readFileSync(eventPath, "utf-8"));
5117
+ event = JSON.parse(fs22.readFileSync(eventPath, "utf-8"));
5124
5118
  } catch {
5125
5119
  return;
5126
5120
  }
@@ -5454,11 +5448,11 @@ var init_classifyByLabel = __esm({
5454
5448
  });
5455
5449
 
5456
5450
  // src/scripts/commitAndPush.ts
5457
- import * as fs22 from "fs";
5458
- import * as path19 from "path";
5451
+ import * as fs23 from "fs";
5452
+ import * as path20 from "path";
5459
5453
  function sentinelPathForStage(cwd, profileName) {
5460
5454
  const runId = resolveRunId();
5461
- return path19.join(cwd, ".kody", "runs", runId, `commit-${profileName}.lock`);
5455
+ return path20.join(cwd, ".kody", "runs", runId, `commit-${profileName}.lock`);
5462
5456
  }
5463
5457
  var DEFAULT_COMMIT_MESSAGE, commitAndPush2;
5464
5458
  var init_commitAndPush = __esm({
@@ -5475,9 +5469,9 @@ var init_commitAndPush = __esm({
5475
5469
  }
5476
5470
  const idempotencyEnabled = process.env.KODY_COMMIT_IDEMPOTENCY !== "0";
5477
5471
  const sentinel = idempotencyEnabled ? sentinelPathForStage(ctx.cwd, profile.name) : null;
5478
- if (sentinel && fs22.existsSync(sentinel)) {
5472
+ if (sentinel && fs23.existsSync(sentinel)) {
5479
5473
  try {
5480
- const replay = JSON.parse(fs22.readFileSync(sentinel, "utf-8"));
5474
+ const replay = JSON.parse(fs23.readFileSync(sentinel, "utf-8"));
5481
5475
  ctx.data.commitResult = replay.commitResult ?? { committed: false, pushed: false };
5482
5476
  if (Array.isArray(replay.changedFiles)) ctx.data.changedFiles = replay.changedFiles;
5483
5477
  if (typeof replay.hasCommitsAhead === "boolean") ctx.data.hasCommitsAhead = replay.hasCommitsAhead;
@@ -5530,8 +5524,8 @@ var init_commitAndPush = __esm({
5530
5524
  const result = ctx.data.commitResult;
5531
5525
  if (sentinel && result?.committed) {
5532
5526
  try {
5533
- fs22.mkdirSync(path19.dirname(sentinel), { recursive: true });
5534
- fs22.writeFileSync(
5527
+ fs23.mkdirSync(path20.dirname(sentinel), { recursive: true });
5528
+ fs23.writeFileSync(
5535
5529
  sentinel,
5536
5530
  JSON.stringify(
5537
5531
  {
@@ -5553,8 +5547,8 @@ var init_commitAndPush = __esm({
5553
5547
  });
5554
5548
 
5555
5549
  // src/goal/state.ts
5556
- import * as fs23 from "fs";
5557
- import * as path20 from "path";
5550
+ import * as fs24 from "fs";
5551
+ import * as path21 from "path";
5558
5552
  function parseGoalState(filePath, raw) {
5559
5553
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
5560
5554
  throw new GoalStateError(filePath, "must be a JSON object");
@@ -5606,10 +5600,10 @@ var init_state2 = __esm({
5606
5600
  "use strict";
5607
5601
  VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "awaiting-merge", "done"]);
5608
5602
  GoalStateError = class extends Error {
5609
- constructor(path42, message) {
5610
- super(`Invalid goal state at ${path42}:
5603
+ constructor(path43, message) {
5604
+ super(`Invalid goal state at ${path43}:
5611
5605
  ${message}`);
5612
- this.path = path42;
5606
+ this.path = path43;
5613
5607
  this.name = "GoalStateError";
5614
5608
  }
5615
5609
  path;
@@ -5721,8 +5715,8 @@ var init_commitGoalState = __esm({
5721
5715
  });
5722
5716
 
5723
5717
  // src/scripts/composePrompt.ts
5724
- import * as fs24 from "fs";
5725
- import * as path21 from "path";
5718
+ import * as fs25 from "fs";
5719
+ import * as path22 from "path";
5726
5720
  function fenceUntrusted(value) {
5727
5721
  if (value.trim().length === 0) return value;
5728
5722
  const safe = value.replace(/-{3,}\s*END UNTRUSTED INPUT\s*-{3,}/gi, "[END UNTRUSTED INPUT]");
@@ -5811,6 +5805,10 @@ function formatDutyReference(data, profileName) {
5811
5805
  if (dutySchedule) {
5812
5806
  lines.push(`- Cadence: \`${dutySchedule}\``);
5813
5807
  }
5808
+ const dutyBody = pickToken(data, "dutyIntent", "jobIntent");
5809
+ if (dutyBody) {
5810
+ lines.push("", "## Duty body", "", dutyBody);
5811
+ }
5814
5812
  if (lines.length === 2) {
5815
5813
  return "";
5816
5814
  }
@@ -5839,9 +5837,10 @@ var init_composePrompt = __esm({
5839
5837
  const explicit = ctx.data.promptTemplate;
5840
5838
  const mode = ctx.args.mode;
5841
5839
  const candidates = [
5842
- explicit ? path21.join(profile.dir, explicit) : null,
5843
- mode ? path21.join(profile.dir, "prompts", `${mode}.md`) : null,
5844
- 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")
5845
5844
  ].filter(Boolean);
5846
5845
  let templatePath = "";
5847
5846
  let template = "";
@@ -5854,7 +5853,7 @@ var init_composePrompt = __esm({
5854
5853
  break;
5855
5854
  }
5856
5855
  try {
5857
- template = fs24.readFileSync(c, "utf-8");
5856
+ template = fs25.readFileSync(c, "utf-8");
5858
5857
  templatePath = c;
5859
5858
  break;
5860
5859
  } catch (err) {
@@ -5865,7 +5864,7 @@ var init_composePrompt = __esm({
5865
5864
  if (!templatePath) {
5866
5865
  let dirState;
5867
5866
  try {
5868
- dirState = `dir contents: [${fs24.readdirSync(profile.dir).join(", ")}]`;
5867
+ dirState = `dir contents: [${fs25.readdirSync(profile.dir).join(", ")}]`;
5869
5868
  } catch (err) {
5870
5869
  dirState = `readdir(${profile.dir}) failed: ${err?.code ?? String(err)}`;
5871
5870
  }
@@ -6692,19 +6691,19 @@ var init_deriveQaScopeFromIssue = __esm({
6692
6691
 
6693
6692
  // src/scripts/diagMcp.ts
6694
6693
  import { execFileSync as execFileSync10 } from "child_process";
6695
- import * as fs25 from "fs";
6694
+ import * as fs26 from "fs";
6696
6695
  import * as os4 from "os";
6697
- import * as path22 from "path";
6696
+ import * as path23 from "path";
6698
6697
  var diagMcp;
6699
6698
  var init_diagMcp = __esm({
6700
6699
  "src/scripts/diagMcp.ts"() {
6701
6700
  "use strict";
6702
6701
  diagMcp = async (_ctx) => {
6703
6702
  const home = os4.homedir();
6704
- const cacheDir = path22.join(home, ".cache", "ms-playwright");
6703
+ const cacheDir = path23.join(home, ".cache", "ms-playwright");
6705
6704
  let entries = [];
6706
6705
  try {
6707
- entries = fs25.readdirSync(cacheDir);
6706
+ entries = fs26.readdirSync(cacheDir);
6708
6707
  } catch {
6709
6708
  }
6710
6709
  const hasChromium = entries.some((e) => e.startsWith("chromium"));
@@ -6732,13 +6731,13 @@ var init_diagMcp = __esm({
6732
6731
  });
6733
6732
 
6734
6733
  // src/scripts/frameworkDetectors.ts
6735
- import * as fs26 from "fs";
6736
- import * as path23 from "path";
6734
+ import * as fs27 from "fs";
6735
+ import * as path24 from "path";
6737
6736
  function detectFrameworks(cwd) {
6738
6737
  const out = [];
6739
6738
  let deps = {};
6740
6739
  try {
6741
- 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"));
6742
6741
  deps = { ...pkg.dependencies, ...pkg.devDependencies };
6743
6742
  } catch {
6744
6743
  return out;
@@ -6775,25 +6774,25 @@ function detectFrameworks(cwd) {
6775
6774
  }
6776
6775
  function findFile(cwd, candidates) {
6777
6776
  for (const c of candidates) {
6778
- if (fs26.existsSync(path23.join(cwd, c))) return c;
6777
+ if (fs27.existsSync(path24.join(cwd, c))) return c;
6779
6778
  }
6780
6779
  return null;
6781
6780
  }
6782
6781
  function discoverPayloadCollections(cwd) {
6783
6782
  const out = [];
6784
6783
  for (const dir of COLLECTION_DIRS) {
6785
- const full = path23.join(cwd, dir);
6786
- if (!fs26.existsSync(full)) continue;
6784
+ const full = path24.join(cwd, dir);
6785
+ if (!fs27.existsSync(full)) continue;
6787
6786
  let files;
6788
6787
  try {
6789
- 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"));
6790
6789
  } catch {
6791
6790
  continue;
6792
6791
  }
6793
6792
  for (const file of files) {
6794
6793
  try {
6795
- const filePath = path23.join(full, file);
6796
- 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);
6797
6796
  const slugMatch = content.match(/slug:\s*['"]([a-z0-9-]+)['"]/);
6798
6797
  if (!slugMatch) continue;
6799
6798
  const slug = slugMatch[1];
@@ -6807,7 +6806,7 @@ function discoverPayloadCollections(cwd) {
6807
6806
  out.push({
6808
6807
  name,
6809
6808
  slug,
6810
- filePath: path23.relative(cwd, filePath),
6809
+ filePath: path24.relative(cwd, filePath),
6811
6810
  fields: fields.slice(0, 20),
6812
6811
  hasAdmin
6813
6812
  });
@@ -6820,28 +6819,28 @@ function discoverPayloadCollections(cwd) {
6820
6819
  function discoverAdminComponents(cwd, collections) {
6821
6820
  const out = [];
6822
6821
  for (const dir of ADMIN_COMPONENT_DIRS) {
6823
- const full = path23.join(cwd, dir);
6824
- if (!fs26.existsSync(full)) continue;
6822
+ const full = path24.join(cwd, dir);
6823
+ if (!fs27.existsSync(full)) continue;
6825
6824
  let entries;
6826
6825
  try {
6827
- entries = fs26.readdirSync(full, { withFileTypes: true });
6826
+ entries = fs27.readdirSync(full, { withFileTypes: true });
6828
6827
  } catch {
6829
6828
  continue;
6830
6829
  }
6831
6830
  for (const entry of entries) {
6832
- const entryPath = path23.join(full, entry.name);
6831
+ const entryPath = path24.join(full, entry.name);
6833
6832
  let name;
6834
6833
  let filePath;
6835
6834
  if (entry.isDirectory()) {
6836
6835
  const indexFile = ["index.tsx", "index.ts", "index.jsx", "index.js"].find(
6837
- (f) => fs26.existsSync(path23.join(entryPath, f))
6836
+ (f) => fs27.existsSync(path24.join(entryPath, f))
6838
6837
  );
6839
6838
  if (!indexFile) continue;
6840
6839
  name = entry.name;
6841
- filePath = path23.relative(cwd, path23.join(entryPath, indexFile));
6840
+ filePath = path24.relative(cwd, path24.join(entryPath, indexFile));
6842
6841
  } else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
6843
6842
  name = entry.name.replace(/\.(tsx?|jsx?)$/, "");
6844
- filePath = path23.relative(cwd, entryPath);
6843
+ filePath = path24.relative(cwd, entryPath);
6845
6844
  } else {
6846
6845
  continue;
6847
6846
  }
@@ -6849,7 +6848,7 @@ function discoverAdminComponents(cwd, collections) {
6849
6848
  if (collections) {
6850
6849
  for (const col of collections) {
6851
6850
  try {
6852
- const colContent = fs26.readFileSync(path23.join(cwd, col.filePath), "utf-8");
6851
+ const colContent = fs27.readFileSync(path24.join(cwd, col.filePath), "utf-8");
6853
6852
  if (colContent.includes(name)) {
6854
6853
  usedInCollection = col.slug;
6855
6854
  break;
@@ -6867,8 +6866,8 @@ function scanApiRoutes(cwd) {
6867
6866
  const out = [];
6868
6867
  const appDirs = ["src/app", "app"];
6869
6868
  for (const appDir of appDirs) {
6870
- const apiDir = path23.join(cwd, appDir, "api");
6871
- if (!fs26.existsSync(apiDir)) continue;
6869
+ const apiDir = path24.join(cwd, appDir, "api");
6870
+ if (!fs27.existsSync(apiDir)) continue;
6872
6871
  walkApiRoutes(apiDir, "/api", cwd, out);
6873
6872
  break;
6874
6873
  }
@@ -6877,14 +6876,14 @@ function scanApiRoutes(cwd) {
6877
6876
  function walkApiRoutes(dir, prefix, cwd, out) {
6878
6877
  let entries;
6879
6878
  try {
6880
- entries = fs26.readdirSync(dir, { withFileTypes: true });
6879
+ entries = fs27.readdirSync(dir, { withFileTypes: true });
6881
6880
  } catch {
6882
6881
  return;
6883
6882
  }
6884
6883
  const routeFile = entries.find((e) => e.isFile() && /^route\.(ts|js|tsx|jsx)$/.test(e.name));
6885
6884
  if (routeFile) {
6886
6885
  try {
6887
- 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);
6888
6887
  const methods = HTTP_METHODS.filter(
6889
6888
  (m) => new RegExp(`export\\s+(?:async\\s+)?function\\s+${m}\\b`).test(content)
6890
6889
  );
@@ -6892,7 +6891,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
6892
6891
  out.push({
6893
6892
  path: prefix,
6894
6893
  methods,
6895
- filePath: path23.relative(cwd, path23.join(dir, routeFile.name))
6894
+ filePath: path24.relative(cwd, path24.join(dir, routeFile.name))
6896
6895
  });
6897
6896
  }
6898
6897
  } catch {
@@ -6903,7 +6902,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
6903
6902
  if (entry.name === "node_modules" || entry.name === ".next") continue;
6904
6903
  let segment = entry.name;
6905
6904
  if (segment.startsWith("(") && segment.endsWith(")")) {
6906
- walkApiRoutes(path23.join(dir, entry.name), prefix, cwd, out);
6905
+ walkApiRoutes(path24.join(dir, entry.name), prefix, cwd, out);
6907
6906
  continue;
6908
6907
  }
6909
6908
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -6911,16 +6910,16 @@ function walkApiRoutes(dir, prefix, cwd, out) {
6911
6910
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
6912
6911
  segment = `:${segment.slice(1, -1)}`;
6913
6912
  }
6914
- walkApiRoutes(path23.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
6913
+ walkApiRoutes(path24.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
6915
6914
  }
6916
6915
  }
6917
6916
  function scanEnvVars(cwd) {
6918
6917
  const candidates = [".env.example", ".env.local.example", ".env.template"];
6919
6918
  for (const envFile of candidates) {
6920
- const envPath = path23.join(cwd, envFile);
6921
- if (!fs26.existsSync(envPath)) continue;
6919
+ const envPath = path24.join(cwd, envFile);
6920
+ if (!fs27.existsSync(envPath)) continue;
6922
6921
  try {
6923
- const content = fs26.readFileSync(envPath, "utf-8");
6922
+ const content = fs27.readFileSync(envPath, "utf-8");
6924
6923
  const vars = [];
6925
6924
  for (const line of content.split("\n")) {
6926
6925
  const trimmed = line.trim();
@@ -6965,8 +6964,8 @@ var init_frameworkDetectors = __esm({
6965
6964
  });
6966
6965
 
6967
6966
  // src/scripts/discoverQaContext.ts
6968
- import * as fs27 from "fs";
6969
- import * as path24 from "path";
6967
+ import * as fs28 from "fs";
6968
+ import * as path25 from "path";
6970
6969
  function runQaDiscovery(cwd) {
6971
6970
  const out = {
6972
6971
  routes: [],
@@ -6997,9 +6996,9 @@ function runQaDiscovery(cwd) {
6997
6996
  }
6998
6997
  function detectDevServer(cwd, out) {
6999
6998
  try {
7000
- 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"));
7001
7000
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
7002
- 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";
7003
7002
  if (pkg.scripts?.dev) out.devCommand = `${pm} dev`;
7004
7003
  if (allDeps.next || allDeps.nuxt) out.devPort = 3e3;
7005
7004
  else if (allDeps.vite) out.devPort = 5173;
@@ -7009,8 +7008,8 @@ function detectDevServer(cwd, out) {
7009
7008
  function scanFrontendRoutes(cwd, out) {
7010
7009
  const appDirs = ["src/app", "app"];
7011
7010
  for (const appDir of appDirs) {
7012
- const full = path24.join(cwd, appDir);
7013
- if (!fs27.existsSync(full)) continue;
7011
+ const full = path25.join(cwd, appDir);
7012
+ if (!fs28.existsSync(full)) continue;
7014
7013
  walkFrontendRoutes(full, "", out);
7015
7014
  break;
7016
7015
  }
@@ -7018,7 +7017,7 @@ function scanFrontendRoutes(cwd, out) {
7018
7017
  function walkFrontendRoutes(dir, prefix, out) {
7019
7018
  let entries;
7020
7019
  try {
7021
- entries = fs27.readdirSync(dir, { withFileTypes: true });
7020
+ entries = fs28.readdirSync(dir, { withFileTypes: true });
7022
7021
  } catch {
7023
7022
  return;
7024
7023
  }
@@ -7035,7 +7034,7 @@ function walkFrontendRoutes(dir, prefix, out) {
7035
7034
  if (entry.name === "node_modules" || entry.name === ".next") continue;
7036
7035
  let segment = entry.name;
7037
7036
  if (segment.startsWith("(") && segment.endsWith(")")) {
7038
- walkFrontendRoutes(path24.join(dir, entry.name), prefix, out);
7037
+ walkFrontendRoutes(path25.join(dir, entry.name), prefix, out);
7039
7038
  continue;
7040
7039
  }
7041
7040
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -7043,7 +7042,7 @@ function walkFrontendRoutes(dir, prefix, out) {
7043
7042
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
7044
7043
  segment = `:${segment.slice(1, -1)}`;
7045
7044
  }
7046
- walkFrontendRoutes(path24.join(dir, entry.name), `${prefix}/${segment}`, out);
7045
+ walkFrontendRoutes(path25.join(dir, entry.name), `${prefix}/${segment}`, out);
7047
7046
  }
7048
7047
  }
7049
7048
  function detectAuthFiles(cwd, out) {
@@ -7060,23 +7059,23 @@ function detectAuthFiles(cwd, out) {
7060
7059
  "src/app/api/oauth"
7061
7060
  ];
7062
7061
  for (const c of candidates) {
7063
- if (fs27.existsSync(path24.join(cwd, c))) out.authFiles.push(c);
7062
+ if (fs28.existsSync(path25.join(cwd, c))) out.authFiles.push(c);
7064
7063
  }
7065
7064
  }
7066
7065
  function detectRoles(cwd, out) {
7067
7066
  const rolePaths = ["src/types", "src/lib", "src/utils", "src/constants", "src/access", "src/collections"];
7068
7067
  for (const rp of rolePaths) {
7069
- const dir = path24.join(cwd, rp);
7070
- if (!fs27.existsSync(dir)) continue;
7068
+ const dir = path25.join(cwd, rp);
7069
+ if (!fs28.existsSync(dir)) continue;
7071
7070
  let files;
7072
7071
  try {
7073
- 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"));
7074
7073
  } catch {
7075
7074
  continue;
7076
7075
  }
7077
7076
  for (const f of files) {
7078
7077
  try {
7079
- 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);
7080
7079
  const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
7081
7080
  if (roleMatches) {
7082
7081
  for (const m of roleMatches) {
@@ -7369,6 +7368,23 @@ async function runJob(job, base) {
7369
7368
  if (executableIdentity !== void 0 && executableIdentity.length > 0)
7370
7369
  preloadedData.jobExecutable = executableIdentity;
7371
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
+ }
7372
7388
  if (valid.why !== void 0 && valid.why.length > 0) preloadedData.jobWhy = valid.why;
7373
7389
  if (valid.persona !== void 0) preloadedData.jobPersona = valid.persona;
7374
7390
  const input = {
@@ -7384,6 +7400,10 @@ async function runJob(job, base) {
7384
7400
  const run = base.chain === false ? runExecutable : runExecutableChain;
7385
7401
  return run(profileName, input);
7386
7402
  }
7403
+ function loadDutyContext(slug) {
7404
+ if (!slug) return null;
7405
+ return readDutyFolder(getProjectDutiesRoot(), slug) ?? readDutyFolder(getBuiltinDutiesRoot(), slug);
7406
+ }
7387
7407
  function mintInstantJob(dispatch2, opts) {
7388
7408
  return {
7389
7409
  action: dispatch2.action,
@@ -7411,6 +7431,7 @@ var init_job = __esm({
7411
7431
  "src/job.ts"() {
7412
7432
  "use strict";
7413
7433
  init_executor();
7434
+ init_dutyFolders();
7414
7435
  init_registry();
7415
7436
  init_jobIdentity();
7416
7437
  init_jobIdentity();
@@ -7642,8 +7663,8 @@ var init_contentsApiBackend = __esm({
7642
7663
  });
7643
7664
 
7644
7665
  // src/scripts/jobState/localFileBackend.ts
7645
- import * as fs28 from "fs";
7646
- import * as path25 from "path";
7666
+ import * as fs29 from "fs";
7667
+ import * as path26 from "path";
7647
7668
  function sanitizeKey(s) {
7648
7669
  return s.replace(/[^A-Za-z0-9._-]/g, "-");
7649
7670
  }
@@ -7699,7 +7720,7 @@ var init_localFileBackend = __esm({
7699
7720
  if (!opts.owner || !opts.repo) throw new Error("LocalFileBackend: owner and repo are required");
7700
7721
  this.cwd = opts.cwd;
7701
7722
  this.jobsDir = opts.jobsDir;
7702
- this.absDir = path25.join(opts.cwd, opts.jobsDir);
7723
+ this.absDir = path26.join(opts.cwd, opts.jobsDir);
7703
7724
  this.owner = opts.owner;
7704
7725
  this.repo = opts.repo;
7705
7726
  this.cache = opts.cache ?? defaultCacheAdapter();
@@ -7714,7 +7735,7 @@ var init_localFileBackend = __esm({
7714
7735
  `);
7715
7736
  return;
7716
7737
  }
7717
- fs28.mkdirSync(this.absDir, { recursive: true });
7738
+ fs29.mkdirSync(this.absDir, { recursive: true });
7718
7739
  const prefix = this.cacheKeyPrefix();
7719
7740
  const probeKey = `${prefix}probe-${Date.now()}`;
7720
7741
  try {
@@ -7743,7 +7764,7 @@ var init_localFileBackend = __esm({
7743
7764
  `);
7744
7765
  return;
7745
7766
  }
7746
- if (!fs28.existsSync(this.absDir)) {
7767
+ if (!fs29.existsSync(this.absDir)) {
7747
7768
  return;
7748
7769
  }
7749
7770
  const key = `${this.cacheKeyPrefix()}${process.env.GITHUB_RUN_ID ?? "norunid"}-${Date.now()}`;
@@ -7759,11 +7780,11 @@ var init_localFileBackend = __esm({
7759
7780
  }
7760
7781
  load(slug) {
7761
7782
  const relPath = stateFilePath(this.jobsDir, slug);
7762
- const absPath = path25.join(this.cwd, relPath);
7763
- if (!fs28.existsSync(absPath)) {
7783
+ const absPath = path26.join(this.cwd, relPath);
7784
+ if (!fs29.existsSync(absPath)) {
7764
7785
  return { path: relPath, handle: null, state: initialStateEnvelope("seed"), created: true };
7765
7786
  }
7766
- const raw = fs28.readFileSync(absPath, "utf-8");
7787
+ const raw = fs29.readFileSync(absPath, "utf-8");
7767
7788
  let parsed;
7768
7789
  try {
7769
7790
  parsed = JSON.parse(raw);
@@ -7780,13 +7801,13 @@ var init_localFileBackend = __esm({
7780
7801
  if (!loaded.created && isStateUnchanged(loaded.state, next)) {
7781
7802
  return false;
7782
7803
  }
7783
- const absPath = path25.join(this.cwd, loaded.path);
7784
- fs28.mkdirSync(path25.dirname(absPath), { recursive: true });
7804
+ const absPath = path26.join(this.cwd, loaded.path);
7805
+ fs29.mkdirSync(path26.dirname(absPath), { recursive: true });
7785
7806
  const body = `${JSON.stringify(next, null, 2)}
7786
7807
  `;
7787
7808
  const tmpPath = `${absPath}.${process.pid}.tmp`;
7788
- fs28.writeFileSync(tmpPath, body, "utf-8");
7789
- fs28.renameSync(tmpPath, absPath);
7809
+ fs29.writeFileSync(tmpPath, body, "utf-8");
7810
+ fs29.renameSync(tmpPath, absPath);
7790
7811
  return true;
7791
7812
  }
7792
7813
  cacheKeyPrefix() {
@@ -7938,8 +7959,7 @@ var init_planTaskJobs = __esm({
7938
7959
  });
7939
7960
 
7940
7961
  // src/scripts/dispatchDutyFileTicks.ts
7941
- import * as fs29 from "fs";
7942
- import * as path26 from "path";
7962
+ import * as path27 from "path";
7943
7963
  async function decideShouldFire(every, slug, backend, now) {
7944
7964
  if (!every) return { skip: false, reason: "no schedule (every cron tick)" };
7945
7965
  if (every === "manual") {
@@ -7980,14 +8000,6 @@ function formatAgo(ms) {
7980
8000
  const day = Math.round(hr / 24);
7981
8001
  return `${day}d`;
7982
8002
  }
7983
- function readJobFile(cwd, jobsDir, slug) {
7984
- try {
7985
- const raw = fs29.readFileSync(path26.join(cwd, jobsDir, `${slug}.md`), "utf-8");
7986
- return splitFrontmatter(raw);
7987
- } catch {
7988
- return { frontmatter: {}, body: "" };
7989
- }
7990
- }
7991
8003
  function parseDutyFilter(raw) {
7992
8004
  return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : void 0;
7993
8005
  }
@@ -7996,21 +8008,21 @@ function filterSlugs(slugs, onlyDuty) {
7996
8008
  }
7997
8009
  function createDutyTaskIssue(opts) {
7998
8010
  const title = `Duty ${opts.slug} - multi-executable task`;
7999
- const body = buildDutyTaskIssueBody(opts.slug, opts.body, opts.frontmatter);
8011
+ const body = buildDutyTaskIssueBody(opts.slug, opts.body, opts.config);
8000
8012
  const out = gh(["issue", "create", "--title", title, "--body-file", "-"], { input: body, cwd: opts.cwd });
8001
8013
  const url = out.split("\n").map((line) => line.trim()).filter(Boolean).pop() ?? "";
8002
8014
  const match = url.match(/\/issues\/(\d+)\b/);
8003
8015
  if (!match) throw new Error(`gh issue create returned unexpected output: ${out}`);
8004
8016
  return { number: Number(match[1]), url };
8005
8017
  }
8006
- function buildDutyTaskIssueBody(slug, dutyBody, frontmatter) {
8007
- const specs = (frontmatter.executables ?? []).map((executable) => ({
8018
+ function buildDutyTaskIssueBody(slug, dutyBody, config) {
8019
+ const specs = (config.executables ?? []).map((executable) => ({
8008
8020
  executable,
8009
8021
  duty: slug,
8010
- ...frontmatter.staff ? { staff: frontmatter.staff } : {},
8022
+ ...config.staff ? { staff: config.staff } : {},
8011
8023
  reason: `Duty \`${slug}\` slice for \`${executable}\`.`,
8012
8024
  flavor: "scheduled",
8013
- ...frontmatter.every ? { schedule: frontmatter.every } : {}
8025
+ ...config.every ? { schedule: config.every } : {}
8014
8026
  }));
8015
8027
  return [
8016
8028
  `# Duty task: ${slug}`,
@@ -8023,26 +8035,6 @@ function buildDutyTaskIssueBody(slug, dutyBody, frontmatter) {
8023
8035
  ""
8024
8036
  ].join("\n");
8025
8037
  }
8026
- function listJobSlugs(absDir) {
8027
- if (!fs29.existsSync(absDir)) return [];
8028
- let entries;
8029
- try {
8030
- entries = fs29.readdirSync(absDir, { withFileTypes: true });
8031
- } catch {
8032
- return [];
8033
- }
8034
- 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();
8035
- }
8036
- function listFolderDutySlugs(absDir) {
8037
- if (!fs29.existsSync(absDir)) return [];
8038
- let entries;
8039
- try {
8040
- entries = fs29.readdirSync(absDir, { withFileTypes: true });
8041
- } catch {
8042
- return [];
8043
- }
8044
- 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();
8045
- }
8046
8038
  async function stampFired(backend, slug, now, task) {
8047
8039
  try {
8048
8040
  const loaded = await backend.load(slug);
@@ -8061,10 +8053,10 @@ var dispatchDutyFileTicks;
8061
8053
  var init_dispatchDutyFileTicks = __esm({
8062
8054
  "src/scripts/dispatchDutyFileTicks.ts"() {
8063
8055
  "use strict";
8056
+ init_dutyFolders();
8064
8057
  init_issue();
8065
8058
  init_job();
8066
- init_profile();
8067
- init_jobFrontmatter();
8059
+ init_scheduleEvery();
8068
8060
  init_jobState();
8069
8061
  init_planTaskJobs();
8070
8062
  dispatchDutyFileTicks = async (ctx, _profile, args) => {
@@ -8082,107 +8074,54 @@ var init_dispatchDutyFileTicks = __esm({
8082
8074
  }
8083
8075
  try {
8084
8076
  const onlyDuty = parseDutyFilter(ctx.args.duty);
8085
- const jobsPath = path26.join(ctx.cwd, jobsDir);
8086
- const slugs = filterSlugs(listJobSlugs(jobsPath), onlyDuty);
8087
- const folderSlugList = filterSlugs(listFolderDutySlugs(jobsPath), onlyDuty);
8088
- ctx.data.jobSlugCount = slugs.length + folderSlugList.length;
8089
- 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) {
8090
8081
  const filter = onlyDuty ? ` matching ${onlyDuty}` : "";
8091
- process.stdout.write(`[jobs] no job files${filter} in ${jobsDir}
8082
+ process.stdout.write(`[jobs] no duty folders${filter} in ${jobsDir}
8092
8083
  `);
8093
8084
  return;
8094
8085
  }
8095
8086
  const filtered = onlyDuty ? ` matching ${onlyDuty}` : "";
8096
- process.stdout.write(
8097
- `[jobs] ticking ${slugs.length + folderSlugList.length} job(s)${filtered} via ${targetExecutable}
8098
- `
8099
- );
8087
+ process.stdout.write(`[jobs] ticking ${slugs.length} dut(y/ies)${filtered} via ${targetExecutable}
8088
+ `);
8100
8089
  const results = [];
8101
8090
  const now = Date.now();
8102
- const folderDutySlugs = new Set(folderSlugList);
8103
- const scheduledDuties = folderSlugList.map((slug) => {
8104
- try {
8105
- const p = loadProfile(path26.join(ctx.cwd, jobsDir, slug, "profile.json"));
8106
- return { slug, every: p.every, staff: p.staff };
8107
- } catch (err) {
8108
- process.stderr.write(`[jobs] \u23ED skip folder-duty ${slug}: profile load failed: ${String(err)}
8109
- `);
8110
- return null;
8111
- }
8112
- }).filter((d) => d !== null && Boolean(d.every));
8113
- process.stdout.write(`[jobs] ${scheduledDuties.length} scheduled folder-dut(y/ies) to consider
8114
- `);
8115
- for (const { slug, every, staff } of scheduledDuties) {
8116
- if (!staff || staff.trim().length === 0) {
8117
- process.stderr.write(`[jobs] \u23ED skip ${slug}: scheduled duty has no staff
8118
- `);
8119
- results.push({ slug, exitCode: 0, skipped: true, reason: "no staff assigned" });
8120
- continue;
8121
- }
8122
- const decision = await decideShouldFire(every, slug, backend, now);
8123
- if (decision.skip) {
8124
- process.stdout.write(`[jobs] \u23ED skip ${slug}: ${decision.reason}
8125
- `);
8126
- results.push({ slug, exitCode: 0, skipped: true, reason: decision.reason });
8127
- continue;
8128
- }
8129
- await stampFired(backend, slug, now);
8130
- process.stdout.write(`[jobs] \u2192 run scheduled duty ${slug} (one-shot, as ${staff})
8131
- `);
8132
- try {
8133
- const out = await runJob(mintScheduledJob({ duty: slug, executable: slug, schedule: every }), {
8134
- cwd: ctx.cwd,
8135
- config: ctx.config,
8136
- verbose: ctx.verbose,
8137
- quiet: ctx.quiet,
8138
- chain: false
8139
- });
8140
- results.push({ slug, exitCode: out.exitCode, reason: out.reason });
8141
- if (out.exitCode !== 0) {
8142
- process.stderr.write(`[jobs] scheduled duty ${slug} failed (exit ${out.exitCode}): ${out.reason ?? ""}
8143
- `);
8144
- }
8145
- } catch (err) {
8146
- const msg = err instanceof Error ? err.message : String(err);
8147
- process.stderr.write(`[jobs] scheduled duty ${slug} crashed: ${msg}
8148
- `);
8149
- results.push({ slug, exitCode: 99, reason: msg });
8150
- }
8151
- }
8152
8091
  for (const slug of slugs) {
8153
- if (folderDutySlugs.has(slug)) {
8154
- 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
8155
8095
  `);
8156
- results.push({ slug, exitCode: 0, skipped: true, reason: "handled as folder-duty" });
8096
+ results.push({ slug, exitCode: 0, skipped: true, reason: "incomplete duty folder" });
8157
8097
  continue;
8158
8098
  }
8159
- const dutyFile = readJobFile(ctx.cwd, jobsDir, slug);
8160
- const frontmatter = dutyFile.frontmatter;
8161
- if (frontmatter.disabled === true) {
8162
- 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
8163
8102
  `);
8164
8103
  results.push({ slug, exitCode: 0, skipped: true, reason: "disabled" });
8165
8104
  continue;
8166
8105
  }
8167
- if (!frontmatter.staff || frontmatter.staff.trim().length === 0) {
8168
- 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)
8169
8108
  `);
8170
8109
  results.push({ slug, exitCode: 0, skipped: true, reason: "no staff assigned" });
8171
8110
  continue;
8172
8111
  }
8173
- const decision = await decideShouldFire(frontmatter.every, slug, backend, now);
8112
+ const decision = await decideShouldFire(config.every, slug, backend, now);
8174
8113
  if (decision.skip) {
8175
8114
  process.stdout.write(`[jobs] \u23ED skip ${slug}: ${decision.reason}
8176
8115
  `);
8177
8116
  results.push({ slug, exitCode: 0, skipped: true, reason: decision.reason });
8178
8117
  continue;
8179
8118
  }
8180
- if (frontmatter.executables && frontmatter.executables.length > 0) {
8119
+ if (config.executables && config.executables.length > 0) {
8181
8120
  try {
8182
8121
  const task = createDutyTaskIssue({
8183
8122
  slug,
8184
- body: dutyFile.body,
8185
- frontmatter,
8123
+ body: duty.body,
8124
+ config,
8186
8125
  cwd: ctx.cwd
8187
8126
  });
8188
8127
  await stampFired(backend, slug, now, task);
@@ -8192,8 +8131,8 @@ var init_dispatchDutyFileTicks = __esm({
8192
8131
  mintScheduledJob({
8193
8132
  duty: slug,
8194
8133
  executable: "task-jobs",
8195
- schedule: frontmatter.every,
8196
- persona: frontmatter.staff,
8134
+ schedule: config.every,
8135
+ persona: config.staff,
8197
8136
  cliArgs: { issue: task.number }
8198
8137
  }),
8199
8138
  { cwd: ctx.cwd, config: ctx.config, verbose: ctx.verbose, quiet: ctx.quiet }
@@ -8211,7 +8150,8 @@ var init_dispatchDutyFileTicks = __esm({
8211
8150
  }
8212
8151
  continue;
8213
8152
  }
8214
- const slugTarget = frontmatter.tickScript ? scriptedExecutable : targetExecutable;
8153
+ const slugTarget = config.tickScript ? scriptedExecutable : config.executable ?? targetExecutable;
8154
+ const cliArgs = config.executable ? {} : { [slugArg]: slug };
8215
8155
  process.stdout.write(`[jobs] \u2192 tick ${slug} (${slugTarget})
8216
8156
  `);
8217
8157
  try {
@@ -8219,8 +8159,9 @@ var init_dispatchDutyFileTicks = __esm({
8219
8159
  mintScheduledJob({
8220
8160
  duty: slug,
8221
8161
  executable: slugTarget,
8222
- schedule: frontmatter.every,
8223
- cliArgs: { [slugArg]: slug }
8162
+ schedule: config.every,
8163
+ persona: config.staff,
8164
+ cliArgs
8224
8165
  }),
8225
8166
  { cwd: ctx.cwd, config: ctx.config, verbose: ctx.verbose, quiet: ctx.quiet, chain: false }
8226
8167
  );
@@ -9463,11 +9404,11 @@ var init_handleAbandonedGoal = __esm({
9463
9404
  // src/scripts/initFlow.ts
9464
9405
  import { execFileSync as execFileSync16 } from "child_process";
9465
9406
  import * as fs30 from "fs";
9466
- import * as path27 from "path";
9407
+ import * as path28 from "path";
9467
9408
  function detectPackageManager(cwd) {
9468
- if (fs30.existsSync(path27.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
9469
- if (fs30.existsSync(path27.join(cwd, "yarn.lock"))) return "yarn";
9470
- 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";
9471
9412
  return "npm";
9472
9413
  }
9473
9414
  function qualityCommandsFor(pm) {
@@ -9539,7 +9480,7 @@ function performInit(cwd, force) {
9539
9480
  const pm = detectPackageManager(cwd);
9540
9481
  const ownerRepo = detectOwnerRepo(cwd);
9541
9482
  const defaultBranch2 = defaultBranchFromGit(cwd);
9542
- const configPath = path27.join(cwd, "kody.config.json");
9483
+ const configPath = path28.join(cwd, "kody.config.json");
9543
9484
  if (fs30.existsSync(configPath) && !force) {
9544
9485
  skipped.push("kody.config.json");
9545
9486
  } else {
@@ -9548,8 +9489,8 @@ function performInit(cwd, force) {
9548
9489
  `);
9549
9490
  wrote.push("kody.config.json");
9550
9491
  }
9551
- const workflowDir = path27.join(cwd, ".github", "workflows");
9552
- const workflowPath = path27.join(workflowDir, "kody.yml");
9492
+ const workflowDir = path28.join(cwd, ".github", "workflows");
9493
+ const workflowPath = path28.join(workflowDir, "kody.yml");
9553
9494
  if (fs30.existsSync(workflowPath) && !force) {
9554
9495
  skipped.push(".github/workflows/kody.yml");
9555
9496
  } else {
@@ -9559,37 +9500,26 @@ function performInit(cwd, force) {
9559
9500
  }
9560
9501
  const builtinJobs = listBuiltinJobs();
9561
9502
  if (builtinJobs.length > 0) {
9562
- const jobsDir = path27.join(cwd, ".kody", "duties");
9503
+ const jobsDir = path28.join(cwd, ".kody", "duties");
9563
9504
  fs30.mkdirSync(jobsDir, { recursive: true });
9564
9505
  for (const job of builtinJobs) {
9565
- if (job.filePath && !job.profilePath) {
9566
- const rel = path27.join(".kody", "duties", `${job.slug}.md`);
9567
- const target = path27.join(cwd, rel);
9568
- if (fs30.existsSync(target) && !force) {
9569
- skipped.push(rel);
9570
- continue;
9571
- }
9572
- fs30.writeFileSync(target, fs30.readFileSync(job.filePath, "utf-8"));
9573
- wrote.push(rel);
9574
- continue;
9575
- }
9576
- const targetDir = path27.join(jobsDir, job.slug);
9577
- const relProfile = path27.join(".kody", "duties", job.slug, "profile.json");
9578
- const relPrompt = path27.join(".kody", "duties", job.slug, "prompt.md");
9579
- 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) {
9580
9510
  skipped.push(relProfile);
9581
- skipped.push(relPrompt);
9511
+ skipped.push(relBody);
9582
9512
  continue;
9583
9513
  }
9584
9514
  fs30.mkdirSync(targetDir, { recursive: true });
9585
- fs30.writeFileSync(path27.join(targetDir, "profile.json"), fs30.readFileSync(job.profilePath, "utf-8"));
9586
- 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"));
9587
9517
  wrote.push(relProfile);
9588
- wrote.push(relPrompt);
9518
+ wrote.push(relBody);
9589
9519
  }
9590
9520
  }
9591
- const staffDir = path27.join(cwd, ".kody", "staff");
9592
- const staffPath = path27.join(staffDir, "kody.md");
9521
+ const staffDir = path28.join(cwd, ".kody", "staff");
9522
+ const staffPath = path28.join(staffDir, "kody.md");
9593
9523
  if (fs30.existsSync(staffPath) && !force) {
9594
9524
  skipped.push(".kody/staff/kody.md");
9595
9525
  } else {
@@ -9605,7 +9535,7 @@ function performInit(cwd, force) {
9605
9535
  continue;
9606
9536
  }
9607
9537
  if (profile.kind !== "scheduled" || !profile.schedule) continue;
9608
- const target = path27.join(workflowDir, `kody-${exe.name}.yml`);
9538
+ const target = path28.join(workflowDir, `kody-${exe.name}.yml`);
9609
9539
  if (fs30.existsSync(target) && !force) {
9610
9540
  skipped.push(`.github/workflows/kody-${exe.name}.yml`);
9611
9541
  continue;
@@ -9931,7 +9861,7 @@ var init_loadIssueStateComment = __esm({
9931
9861
 
9932
9862
  // src/scripts/loadJobFromFile.ts
9933
9863
  import * as fs31 from "fs";
9934
- import * as path28 from "path";
9864
+ import * as path29 from "path";
9935
9865
  function parseJobFile(raw, slug) {
9936
9866
  let stripped = raw;
9937
9867
  if (stripped.startsWith("---\n")) {
@@ -9947,17 +9877,17 @@ function parseJobFile(raw, slug) {
9947
9877
  const rest = trimmed.slice(firstLine2.length).replace(/^\n+/, "");
9948
9878
  return { title: h1[1].trim(), body: rest };
9949
9879
  }
9950
- return { title: humanizeSlug(slug), body: trimmed };
9880
+ return { title: humanizeSlug2(slug), body: trimmed };
9951
9881
  }
9952
- function humanizeSlug(slug) {
9882
+ function humanizeSlug2(slug) {
9953
9883
  return slug.split(/[-_]+/).filter((s) => s.length > 0).map((s) => s[0].toUpperCase() + s.slice(1)).join(" ");
9954
9884
  }
9955
9885
  var DUTY_TOOL_PALETTE2, loadJobFromFile;
9956
9886
  var init_loadJobFromFile = __esm({
9957
9887
  "src/scripts/loadJobFromFile.ts"() {
9958
9888
  "use strict";
9889
+ init_dutyFolders();
9959
9890
  init_dutyMcp();
9960
- init_jobFrontmatter();
9961
9891
  init_jobState();
9962
9892
  DUTY_TOOL_PALETTE2 = new Set(DUTY_MCP_TOOL_NAMES);
9963
9893
  loadJobFromFile = async (ctx, profile, args) => {
@@ -9968,19 +9898,17 @@ var init_loadJobFromFile = __esm({
9968
9898
  if (!slug) {
9969
9899
  throw new Error(`loadJobFromFile: ctx.args.${slugArg} must be a non-empty slug`);
9970
9900
  }
9971
- const absPath = path28.join(ctx.cwd, jobsDir, `${slug}.md`);
9972
- if (!fs31.existsSync(absPath)) {
9973
- 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)}`);
9974
9904
  }
9975
- const raw = fs31.readFileSync(absPath, "utf-8");
9976
- const { title, body } = parseJobFile(raw, slug);
9977
- const frontmatter = splitFrontmatter(raw).frontmatter;
9978
- const mentions = (frontmatter.mentions ?? []).map((login) => `@${login}`).join(" ");
9979
- 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();
9980
9908
  let workerTitle = "";
9981
9909
  let workerPersona = "";
9982
9910
  if (workerSlug) {
9983
- const workerPath = path28.join(ctx.cwd, workersDir, `${workerSlug}.md`);
9911
+ const workerPath = path29.join(ctx.cwd, workersDir, `${workerSlug}.md`);
9984
9912
  if (!fs31.existsSync(workerPath)) {
9985
9913
  throw new Error(`loadJobFromFile: duty '${slug}' declares staff '${workerSlug}' but ${workerPath} does not exist`);
9986
9914
  }
@@ -10005,8 +9933,8 @@ var init_loadJobFromFile = __esm({
10005
9933
  ctx.data.staffSlug = workerSlug;
10006
9934
  ctx.data.staffTitle = workerTitle;
10007
9935
  ctx.data.executableSlug = profile.name;
10008
- ctx.data.dutySchedule = "";
10009
- const declaredTools = frontmatter.tools ?? [];
9936
+ ctx.data.dutySchedule = config.every ?? "";
9937
+ const declaredTools = config.tools ?? [];
10010
9938
  if (declaredTools.length > 0) {
10011
9939
  const unknown = declaredTools.filter((name) => !DUTY_TOOL_PALETTE2.has(name));
10012
9940
  if (unknown.length > 0) {
@@ -10059,9 +9987,9 @@ ${truncate2(issue.body, FINDING_BODY_MAX_BYTES)}`;
10059
9987
 
10060
9988
  // src/scripts/kodyVariables.ts
10061
9989
  import * as fs32 from "fs";
10062
- import * as path29 from "path";
9990
+ import * as path30 from "path";
10063
9991
  function readKodyVariables(cwd) {
10064
- const full = path29.join(cwd, KODY_VARIABLES_REL_PATH);
9992
+ const full = path30.join(cwd, KODY_VARIABLES_REL_PATH);
10065
9993
  let raw;
10066
9994
  try {
10067
9995
  raw = fs32.readFileSync(full, "utf-8");
@@ -10090,7 +10018,7 @@ var init_kodyVariables = __esm({
10090
10018
 
10091
10019
  // src/scripts/loadQaContext.ts
10092
10020
  import * as fs33 from "fs";
10093
- import * as path30 from "path";
10021
+ import * as path31 from "path";
10094
10022
  function parseSlugList(value) {
10095
10023
  const inner = value.startsWith("[") && value.endsWith("]") ? value.slice(1, -1) : value;
10096
10024
  return inner.split(",").map(
@@ -10098,7 +10026,7 @@ function parseSlugList(value) {
10098
10026
  ).filter(Boolean);
10099
10027
  }
10100
10028
  function readProfileStaff(raw) {
10101
- const m = FRONTMATTER_RE2.exec(raw);
10029
+ const m = FRONTMATTER_RE.exec(raw);
10102
10030
  if (!m) return { staff: ["kody"], body: raw };
10103
10031
  const body = raw.slice(m[0].length);
10104
10032
  let staff = null;
@@ -10119,7 +10047,7 @@ function readProfileStaff(raw) {
10119
10047
  return { staff: staff ?? legacy ?? ["kody"], body };
10120
10048
  }
10121
10049
  function readProfile(cwd) {
10122
- const dir = path30.join(cwd, CONTEXT_DIR_REL_PATH);
10050
+ const dir = path31.join(cwd, CONTEXT_DIR_REL_PATH);
10123
10051
  if (!fs33.existsSync(dir)) return "";
10124
10052
  let entries;
10125
10053
  try {
@@ -10130,7 +10058,7 @@ function readProfile(cwd) {
10130
10058
  const blocks = [];
10131
10059
  for (const file of entries) {
10132
10060
  try {
10133
- const raw = fs33.readFileSync(path30.join(dir, file), "utf-8");
10061
+ const raw = fs33.readFileSync(path31.join(dir, file), "utf-8");
10134
10062
  const { staff, body } = readProfileStaff(raw);
10135
10063
  if (!staff.includes(QA_STAFF) && !staff.includes(ALL_STAFF)) continue;
10136
10064
  blocks.push(`## ${file}
@@ -10153,7 +10081,7 @@ function composeAuthBlock(authProfile, login, password) {
10153
10081
  }
10154
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.";
10155
10083
  }
10156
- 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;
10157
10085
  var init_loadQaContext = __esm({
10158
10086
  "src/scripts/loadQaContext.ts"() {
10159
10087
  "use strict";
@@ -10162,7 +10090,7 @@ var init_loadQaContext = __esm({
10162
10090
  QA_STAFF = "qa-engineer";
10163
10091
  ALL_STAFF = "*";
10164
10092
  LEGACY_AUDIENCE_TO_STAFF = { chat: "kody", qa: QA_STAFF };
10165
- FRONTMATTER_RE2 = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
10093
+ FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
10166
10094
  loadQaContext = async (ctx) => {
10167
10095
  const vars = readKodyVariables(ctx.cwd);
10168
10096
  const login = vars.LOGIN_USER ?? "";
@@ -10177,7 +10105,7 @@ var init_loadQaContext = __esm({
10177
10105
 
10178
10106
  // src/taskContext.ts
10179
10107
  import * as fs34 from "fs";
10180
- import * as path31 from "path";
10108
+ import * as path32 from "path";
10181
10109
  function buildTaskContext(args) {
10182
10110
  return {
10183
10111
  schemaVersion: TASK_CONTEXT_SCHEMA_VERSION,
@@ -10192,9 +10120,9 @@ function buildTaskContext(args) {
10192
10120
  }
10193
10121
  function persistTaskContext(cwd, ctx) {
10194
10122
  try {
10195
- const dir = path31.join(cwd, ".kody", "runs", ctx.runId);
10123
+ const dir = path32.join(cwd, ".kody", "runs", ctx.runId);
10196
10124
  fs34.mkdirSync(dir, { recursive: true });
10197
- const file = path31.join(dir, "task-context.json");
10125
+ const file = path32.join(dir, "task-context.json");
10198
10126
  fs34.writeFileSync(file, `${JSON.stringify(ctx, null, 2)}
10199
10127
  `);
10200
10128
  return file;
@@ -10284,7 +10212,7 @@ var init_loadTaskState = __esm({
10284
10212
 
10285
10213
  // src/scripts/loadWorkerAdhoc.ts
10286
10214
  import * as fs35 from "fs";
10287
- import * as path32 from "path";
10215
+ import * as path33 from "path";
10288
10216
  function resolveMessage(messageArg) {
10289
10217
  const fromComment = readCommentBody();
10290
10218
  if (fromComment) return stripDirective(fromComment);
@@ -10318,7 +10246,7 @@ function stripDirective(body) {
10318
10246
  return lines.slice(start).join("\n").trim();
10319
10247
  }
10320
10248
  function parsePersona(raw, slug) {
10321
- const stripped = splitFrontmatter(raw).body;
10249
+ const stripped = stripLeadingFrontmatter(raw);
10322
10250
  const trimmed = stripped.trim();
10323
10251
  const firstLine2 = trimmed.split("\n", 1)[0] ?? "";
10324
10252
  const h1 = /^#\s+(.+?)\s*$/.exec(firstLine2);
@@ -10326,23 +10254,26 @@ function parsePersona(raw, slug) {
10326
10254
  const rest = trimmed.slice(firstLine2.length).replace(/^\n+/, "");
10327
10255
  return { title: h1[1].trim(), body: rest };
10328
10256
  }
10329
- return { title: humanizeSlug2(slug), body: trimmed };
10257
+ return { title: humanizeSlug3(slug), body: trimmed };
10330
10258
  }
10331
- 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) {
10332
10264
  return slug.split(/[-_]+/).filter((s) => s.length > 0).map((s) => s[0].toUpperCase() + s.slice(1)).join(" ");
10333
10265
  }
10334
10266
  var loadWorkerAdhoc;
10335
10267
  var init_loadWorkerAdhoc = __esm({
10336
10268
  "src/scripts/loadWorkerAdhoc.ts"() {
10337
10269
  "use strict";
10338
- init_jobFrontmatter();
10339
10270
  loadWorkerAdhoc = async (ctx, _profile, args) => {
10340
10271
  const workersDir = String(args?.workersDir ?? ".kody/staff");
10341
10272
  const workerSlug = String(ctx.args.worker ?? "").trim();
10342
10273
  if (!workerSlug) {
10343
10274
  throw new Error("loadWorkerAdhoc: ctx.args.worker must be a non-empty slug");
10344
10275
  }
10345
- const workerPath = path32.join(ctx.cwd, workersDir, `${workerSlug}.md`);
10276
+ const workerPath = path33.join(ctx.cwd, workersDir, `${workerSlug}.md`);
10346
10277
  if (!fs35.existsSync(workerPath)) {
10347
10278
  throw new Error(`loadWorkerAdhoc: worker persona not found: ${workerPath}`);
10348
10279
  }
@@ -12217,12 +12148,12 @@ fi
12217
12148
 
12218
12149
  // src/scripts/runPreviewBuild.ts
12219
12150
  import { copyFile, writeFile } from "fs/promises";
12220
- import * as path33 from "path";
12151
+ import * as path34 from "path";
12221
12152
  import { fileURLToPath } from "url";
12222
12153
  function bundledDockerfilePath(mode) {
12223
- const here = path33.dirname(fileURLToPath(import.meta.url));
12154
+ const here = path34.dirname(fileURLToPath(import.meta.url));
12224
12155
  const file = mode === "dev" ? "default-Dockerfile.preview.dev" : "default-Dockerfile.preview.prod";
12225
- return path33.join(here, "preview-build-templates", file);
12156
+ return path34.join(here, "preview-build-templates", file);
12226
12157
  }
12227
12158
  function required(name) {
12228
12159
  const v = (process.env[name] ?? "").trim();
@@ -12481,10 +12412,10 @@ var init_runPreviewBuild = __esm({
12481
12412
  console.log(`[preview-build] vault: ${Object.keys(buildEnv).length} secrets, mode=${buildMode}`);
12482
12413
  if (Object.keys(buildEnv).length > 0) {
12483
12414
  const lines = Object.entries(buildEnv).map(([k, v]) => `${k}=${JSON.stringify(v)}`);
12484
- 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")}
12485
12416
  `, "utf8");
12486
12417
  }
12487
- const consumerDockerfile = path33.join(ctx.cwd, "Dockerfile.preview");
12418
+ const consumerDockerfile = path34.join(ctx.cwd, "Dockerfile.preview");
12488
12419
  const { stat } = await import("fs/promises");
12489
12420
  let hasConsumerDockerfile = false;
12490
12421
  try {
@@ -12587,7 +12518,7 @@ var init_runPreviewBuild = __esm({
12587
12518
  // src/scripts/runTickScript.ts
12588
12519
  import { spawnSync as spawnSync2 } from "child_process";
12589
12520
  import * as fs36 from "fs";
12590
- import * as path34 from "path";
12521
+ import * as path35 from "path";
12591
12522
  function buildChildEnv(parent, force) {
12592
12523
  const allow = /* @__PURE__ */ new Set([
12593
12524
  "PATH",
@@ -12635,7 +12566,7 @@ var runTickScript;
12635
12566
  var init_runTickScript = __esm({
12636
12567
  "src/scripts/runTickScript.ts"() {
12637
12568
  "use strict";
12638
- init_jobFrontmatter();
12569
+ init_dutyFolders();
12639
12570
  init_jobState();
12640
12571
  init_parseJobStateFromAgentResult();
12641
12572
  runTickScript = async (ctx, _profile, args) => {
@@ -12649,21 +12580,19 @@ var init_runTickScript = __esm({
12649
12580
  ctx.output.reason = `runTickScript: ctx.args.${slugArg} must be a non-empty slug`;
12650
12581
  return;
12651
12582
  }
12652
- const jobPath = path34.join(ctx.cwd, jobsDir, `${slug}.md`);
12653
- if (!fs36.existsSync(jobPath)) {
12583
+ const duty = readDutyFolder(path35.join(ctx.cwd, jobsDir), slug);
12584
+ if (!duty) {
12654
12585
  ctx.output.exitCode = 99;
12655
- 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)}`;
12656
12587
  return;
12657
12588
  }
12658
- const raw = fs36.readFileSync(jobPath, "utf-8");
12659
- const { frontmatter } = splitFrontmatter(raw);
12660
- const tickScript = frontmatter.tickScript;
12589
+ const tickScript = duty.config.tickScript;
12661
12590
  if (!tickScript) {
12662
12591
  ctx.output.exitCode = 99;
12663
- 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`;
12664
12593
  return;
12665
12594
  }
12666
- const scriptPath = path34.isAbsolute(tickScript) ? tickScript : path34.join(ctx.cwd, tickScript);
12595
+ const scriptPath = path35.isAbsolute(tickScript) ? tickScript : path35.join(ctx.cwd, tickScript);
12667
12596
  if (!fs36.existsSync(scriptPath)) {
12668
12597
  ctx.output.exitCode = 99;
12669
12598
  ctx.output.reason = `runTickScript: tickScript not found: ${scriptPath}`;
@@ -13862,7 +13791,7 @@ var init_scripts = __esm({
13862
13791
 
13863
13792
  // src/staff.ts
13864
13793
  import * as fs38 from "fs";
13865
- import * as path35 from "path";
13794
+ import * as path36 from "path";
13866
13795
  function stripFrontmatter(raw) {
13867
13796
  const match = /^---\n[\s\S]*?\n---\n?([\s\S]*)$/.exec(raw);
13868
13797
  return (match ? match[1] : raw).trim();
@@ -13870,7 +13799,7 @@ function stripFrontmatter(raw) {
13870
13799
  function loadStaffPersona(cwd, slug, staffDir = DEFAULT_STAFF_DIR) {
13871
13800
  const trimmed = slug.trim();
13872
13801
  if (!trimmed) throw new Error("loadStaffPersona: empty staff slug");
13873
- const staffPath = path35.join(cwd, staffDir, `${trimmed}.md`);
13802
+ const staffPath = path36.join(cwd, staffDir, `${trimmed}.md`);
13874
13803
  if (fs38.existsSync(staffPath)) {
13875
13804
  const body = stripFrontmatter(fs38.readFileSync(staffPath, "utf-8"));
13876
13805
  if (body) return body;
@@ -13986,7 +13915,7 @@ var init_tools = __esm({
13986
13915
  // src/executor.ts
13987
13916
  import { spawn as spawn7 } from "child_process";
13988
13917
  import * as fs39 from "fs";
13989
- import * as path36 from "path";
13918
+ import * as path37 from "path";
13990
13919
  function isMutatingPostflight(scriptName) {
13991
13920
  return MUTATING_POSTFLIGHTS.has(scriptName ?? "");
13992
13921
  }
@@ -14137,13 +14066,13 @@ async function runExecutable(profileName, input) {
14137
14066
  })
14138
14067
  };
14139
14068
  })() : null;
14140
- const ndjsonDir = path36.join(input.cwd, ".kody");
14069
+ const ndjsonDir = path37.join(input.cwd, ".kody");
14141
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;
14142
14071
  const staffPersona = personaSlug ? framePersona(personaSlug, loadStaffPersona(input.cwd, personaSlug)) : null;
14143
14072
  const jobWhyBlock = typeof ctx.data.jobWhy === "string" ? operatorRequestBlock(ctx.data.jobWhy) : null;
14144
14073
  const jobRefBlock = jobReferenceBlock(profileName, profile, ctx.data);
14145
14074
  const invokeAgent = async (prompt) => {
14146
- 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);
14147
14076
  const syntheticPath = ctx.data.syntheticPluginPath;
14148
14077
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
14149
14078
  const agents = loadSubagents(profile);
@@ -14184,7 +14113,7 @@ async function runExecutable(profileName, input) {
14184
14113
  enableVerifyTool: profile.claudeCode.enableVerifyTool,
14185
14114
  enableSubmitTool: profile.claudeCode.enableSubmitTool,
14186
14115
  // Locked-toolbox duty mode: `loadJobFromFile` flips `ctx.data.dutyTools`
14187
- // 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
14188
14117
  // to know the palette — it just forwards the flag so agent.ts can spin
14189
14118
  // up the in-process `kody-duty` MCP server with the right context.
14190
14119
  enableDutyTool: Array.isArray(ctx.data.dutyTools) && ctx.data.dutyTools.length > 0,
@@ -14469,13 +14398,13 @@ function clearStampedLifecycleLabels(profile, ctx) {
14469
14398
  function resolveProfilePath(profileName) {
14470
14399
  const found = resolveExecutable(profileName);
14471
14400
  if (found) return found;
14472
- const here = path36.dirname(new URL(import.meta.url).pathname);
14401
+ const here = path37.dirname(new URL(import.meta.url).pathname);
14473
14402
  const candidates = [
14474
- path36.join(here, "executables", profileName, "profile.json"),
14403
+ path37.join(here, "executables", profileName, "profile.json"),
14475
14404
  // same-dir sibling (dev)
14476
- path36.join(here, "..", "executables", profileName, "profile.json"),
14405
+ path37.join(here, "..", "executables", profileName, "profile.json"),
14477
14406
  // up one (prod: dist/bin → dist/executables)
14478
- path36.join(here, "..", "src", "executables", profileName, "profile.json")
14407
+ path37.join(here, "..", "src", "executables", profileName, "profile.json")
14479
14408
  // fallback
14480
14409
  ];
14481
14410
  for (const c of candidates) {
@@ -14577,7 +14506,7 @@ function resolveShellTimeoutMs(entry) {
14577
14506
  }
14578
14507
  async function runShellEntry(entry, ctx, profile) {
14579
14508
  const shellName = entry.shell;
14580
- const shellPath = path36.join(profile.dir, shellName);
14509
+ const shellPath = path37.join(profile.dir, shellName);
14581
14510
  if (!fs39.existsSync(shellPath)) {
14582
14511
  ctx.skipAgent = true;
14583
14512
  ctx.output.exitCode = 99;
@@ -14834,18 +14763,18 @@ function translateOpenAISseToBrain(opts) {
14834
14763
  // src/servers/brain-serve.ts
14835
14764
  import * as fs42 from "fs";
14836
14765
  import { createServer } from "http";
14837
- import * as path39 from "path";
14766
+ import * as path40 from "path";
14838
14767
 
14839
14768
  // src/chat/loop.ts
14840
14769
  init_agent();
14841
14770
  init_registry();
14842
14771
  init_task_artifacts();
14843
- import * as fs11 from "fs";
14844
- import * as path11 from "path";
14772
+ import * as fs12 from "fs";
14773
+ import * as path12 from "path";
14845
14774
 
14846
14775
  // src/chat/attachments.ts
14847
- import * as fs8 from "fs";
14848
- import * as path8 from "path";
14776
+ import * as fs9 from "fs";
14777
+ import * as path9 from "path";
14849
14778
  var INLINE_ATTACHMENT_RE = /(?:\[(?:Image|File): ([^\]]*)\]\n)?data:([\w.+-]+\/[\w.+-]+);base64,([A-Za-z0-9+/=]+)/g;
14850
14779
  var EXT_BY_MIME = {
14851
14780
  "image/png": "png",
@@ -14861,7 +14790,7 @@ function extFor(mime) {
14861
14790
  return EXT_BY_MIME[mime.toLowerCase()] ?? mime.split("/")[1]?.replace(/[^\w]/g, "") ?? "bin";
14862
14791
  }
14863
14792
  function attachmentsDir(cwd, sessionId) {
14864
- return path8.join(cwd, ".kody", "tmp", "attachments", sessionId);
14793
+ return path9.join(cwd, ".kody", "tmp", "attachments", sessionId);
14865
14794
  }
14866
14795
  function prepareAttachments(turns, cwd, sessionId) {
14867
14796
  const imagePaths = [];
@@ -14878,11 +14807,11 @@ function prepareAttachments(turns, cwd, sessionId) {
14878
14807
  if (!isImage) return `[File: ${name}]`;
14879
14808
  try {
14880
14809
  if (!dirEnsured) {
14881
- fs8.mkdirSync(dir, { recursive: true });
14810
+ fs9.mkdirSync(dir, { recursive: true });
14882
14811
  dirEnsured = true;
14883
14812
  }
14884
- const filePath = path8.join(dir, `${imageCounter}.${extFor(mime)}`);
14885
- fs8.writeFileSync(filePath, Buffer.from(data, "base64"));
14813
+ const filePath = path9.join(dir, `${imageCounter}.${extFor(mime)}`);
14814
+ fs9.writeFileSync(filePath, Buffer.from(data, "base64"));
14886
14815
  imageCounter += 1;
14887
14816
  imagePaths.push(filePath);
14888
14817
  return `[Image "${name}" is attached \u2014 saved to ${filePath}. Use the Read tool on that exact path to view it.]`;
@@ -14898,10 +14827,10 @@ function prepareAttachments(turns, cwd, sessionId) {
14898
14827
  }
14899
14828
 
14900
14829
  // src/chat/events.ts
14901
- import * as fs9 from "fs";
14902
- import * as path9 from "path";
14830
+ import * as fs10 from "fs";
14831
+ import * as path10 from "path";
14903
14832
  function eventsFilePath(cwd, sessionId) {
14904
- return path9.join(cwd, ".kody", "events", `${sessionId}.jsonl`);
14833
+ return path10.join(cwd, ".kody", "events", `${sessionId}.jsonl`);
14905
14834
  }
14906
14835
  var FileSink = class {
14907
14836
  constructor(file) {
@@ -14909,8 +14838,8 @@ var FileSink = class {
14909
14838
  }
14910
14839
  file;
14911
14840
  async emit(event) {
14912
- fs9.mkdirSync(path9.dirname(this.file), { recursive: true });
14913
- fs9.appendFileSync(this.file, `${JSON.stringify(event)}
14841
+ fs10.mkdirSync(path10.dirname(this.file), { recursive: true });
14842
+ fs10.appendFileSync(this.file, `${JSON.stringify(event)}
14914
14843
  `);
14915
14844
  }
14916
14845
  };
@@ -14964,14 +14893,14 @@ function makeRunId(sessionId, suffix) {
14964
14893
  }
14965
14894
 
14966
14895
  // src/chat/session.ts
14967
- import * as fs10 from "fs";
14968
- import * as path10 from "path";
14896
+ import * as fs11 from "fs";
14897
+ import * as path11 from "path";
14969
14898
  function sessionFilePath(cwd, sessionId) {
14970
- return path10.join(cwd, ".kody", "sessions", `${sessionId}.jsonl`);
14899
+ return path11.join(cwd, ".kody", "sessions", `${sessionId}.jsonl`);
14971
14900
  }
14972
14901
  function readMeta(file) {
14973
- if (!fs10.existsSync(file)) return null;
14974
- const raw = fs10.readFileSync(file, "utf-8");
14902
+ if (!fs11.existsSync(file)) return null;
14903
+ const raw = fs11.readFileSync(file, "utf-8");
14975
14904
  const firstLine2 = raw.split("\n", 1)[0]?.trim();
14976
14905
  if (!firstLine2) return null;
14977
14906
  try {
@@ -14984,8 +14913,8 @@ function readMeta(file) {
14984
14913
  }
14985
14914
  }
14986
14915
  function readSession(file) {
14987
- if (!fs10.existsSync(file)) return [];
14988
- const raw = fs10.readFileSync(file, "utf-8").trim();
14916
+ if (!fs11.existsSync(file)) return [];
14917
+ const raw = fs11.readFileSync(file, "utf-8").trim();
14989
14918
  if (!raw) return [];
14990
14919
  const turns = [];
14991
14920
  for (const line of raw.split("\n")) {
@@ -15001,14 +14930,14 @@ function readSession(file) {
15001
14930
  return turns;
15002
14931
  }
15003
14932
  function appendTurn(file, turn) {
15004
- fs10.mkdirSync(path10.dirname(file), { recursive: true });
14933
+ fs11.mkdirSync(path11.dirname(file), { recursive: true });
15005
14934
  const line = JSON.stringify({
15006
14935
  role: turn.role,
15007
14936
  content: turn.content,
15008
14937
  timestamp: turn.timestamp,
15009
14938
  toolCalls: turn.toolCalls ?? []
15010
14939
  });
15011
- fs10.appendFileSync(file, `${line}
14940
+ fs11.appendFileSync(file, `${line}
15012
14941
  `);
15013
14942
  }
15014
14943
  function seedInitialMessage(file, message) {
@@ -15127,7 +15056,7 @@ function buildExecutableCatalog() {
15127
15056
  const entries = [];
15128
15057
  for (const { name, profilePath } of discovered) {
15129
15058
  try {
15130
- const raw = JSON.parse(fs11.readFileSync(profilePath, "utf-8"));
15059
+ const raw = JSON.parse(fs12.readFileSync(profilePath, "utf-8"));
15131
15060
  const describe = typeof raw.describe === "string" ? raw.describe : "";
15132
15061
  const firstSentence = describe.split(/(?<=[.!?])\s+/, 1)[0] ?? "";
15133
15062
  entries.push({ name, describe: firstSentence.trim() });
@@ -15295,10 +15224,10 @@ async function emit(sink, type, sessionId, suffix, payload) {
15295
15224
  var MEMORY_INDEX_REL = ".kody/memory/INDEX.md";
15296
15225
  var MAX_INDEX_BYTES = 8e3;
15297
15226
  function readMemoryIndexBlock(cwd) {
15298
- const indexPath = path11.join(cwd, MEMORY_INDEX_REL);
15227
+ const indexPath = path12.join(cwd, MEMORY_INDEX_REL);
15299
15228
  let raw;
15300
15229
  try {
15301
- raw = fs11.readFileSync(indexPath, "utf-8");
15230
+ raw = fs12.readFileSync(indexPath, "utf-8");
15302
15231
  } catch {
15303
15232
  return "";
15304
15233
  }
@@ -15316,17 +15245,17 @@ function readMemoryIndexBlock(cwd) {
15316
15245
  var CONTEXT_DIR_REL = ".kody/context";
15317
15246
  var MAX_CONTEXT_BYTES = 12e3;
15318
15247
  function readContextBlock(cwd) {
15319
- const dir = path11.join(cwd, CONTEXT_DIR_REL);
15248
+ const dir = path12.join(cwd, CONTEXT_DIR_REL);
15320
15249
  let files;
15321
15250
  try {
15322
- files = fs11.readdirSync(dir).filter((f) => f.endsWith(".md")).sort();
15251
+ files = fs12.readdirSync(dir).filter((f) => f.endsWith(".md")).sort();
15323
15252
  } catch {
15324
15253
  return "";
15325
15254
  }
15326
15255
  const sections = [];
15327
15256
  for (const file of files) {
15328
15257
  try {
15329
- const content = fs11.readFileSync(path11.join(dir, file), "utf-8").trim();
15258
+ const content = fs12.readFileSync(path12.join(dir, file), "utf-8").trim();
15330
15259
  if (content) sections.push(`### ${file.replace(/\.md$/, "")}
15331
15260
 
15332
15261
  ${content}`);
@@ -15349,10 +15278,10 @@ _\u2026 (context truncated; see \`.kody/context/\` for the full text)_` : joined
15349
15278
  var INSTRUCTIONS_REL = ".kody/instructions.md";
15350
15279
  var MAX_INSTRUCTIONS_BYTES = 8e3;
15351
15280
  function readInstructionsBlock(cwd) {
15352
- const instructionsPath = path11.join(cwd, INSTRUCTIONS_REL);
15281
+ const instructionsPath = path12.join(cwd, INSTRUCTIONS_REL);
15353
15282
  let raw;
15354
15283
  try {
15355
- raw = fs11.readFileSync(instructionsPath, "utf-8");
15284
+ raw = fs12.readFileSync(instructionsPath, "utf-8");
15356
15285
  } catch {
15357
15286
  return "";
15358
15287
  }
@@ -15376,7 +15305,7 @@ init_config();
15376
15305
  // src/kody-cli.ts
15377
15306
  import { execFileSync as execFileSync26 } from "child_process";
15378
15307
  import * as fs40 from "fs";
15379
- import * as path37 from "path";
15308
+ import * as path38 from "path";
15380
15309
 
15381
15310
  // src/app-auth.ts
15382
15311
  import { createSign } from "crypto";
@@ -15453,7 +15382,7 @@ init_config();
15453
15382
 
15454
15383
  // src/dispatch.ts
15455
15384
  init_config();
15456
- import * as fs12 from "fs";
15385
+ import * as fs13 from "fs";
15457
15386
 
15458
15387
  // src/cron-match.ts
15459
15388
  var FIELD_BOUNDS = [
@@ -15571,10 +15500,10 @@ function autoDispatch(opts) {
15571
15500
  }
15572
15501
  const eventName = process.env.GITHUB_EVENT_NAME;
15573
15502
  const eventPath = process.env.GITHUB_EVENT_PATH;
15574
- if (!eventName || !eventPath || !fs12.existsSync(eventPath)) return null;
15503
+ if (!eventName || !eventPath || !fs13.existsSync(eventPath)) return null;
15575
15504
  let event = {};
15576
15505
  try {
15577
- event = JSON.parse(fs12.readFileSync(eventPath, "utf-8"));
15506
+ event = JSON.parse(fs13.readFileSync(eventPath, "utf-8"));
15578
15507
  } catch {
15579
15508
  return null;
15580
15509
  }
@@ -15685,7 +15614,7 @@ function autoDispatchTyped(opts) {
15685
15614
  if (legacy) return { kind: "route", ...legacy };
15686
15615
  const eventName = process.env.GITHUB_EVENT_NAME;
15687
15616
  const eventPath = process.env.GITHUB_EVENT_PATH;
15688
- if (!eventName || !eventPath || !fs12.existsSync(eventPath)) {
15617
+ if (!eventName || !eventPath || !fs13.existsSync(eventPath)) {
15689
15618
  return { kind: "silent", reason: "no GHA event context" };
15690
15619
  }
15691
15620
  if (eventName !== "issue_comment") {
@@ -15693,7 +15622,7 @@ function autoDispatchTyped(opts) {
15693
15622
  }
15694
15623
  let event = {};
15695
15624
  try {
15696
- event = JSON.parse(fs12.readFileSync(eventPath, "utf-8"));
15625
+ event = JSON.parse(fs13.readFileSync(eventPath, "utf-8"));
15697
15626
  } catch {
15698
15627
  return { kind: "silent", reason: "GHA event payload unreadable" };
15699
15628
  }
@@ -15737,7 +15666,7 @@ function dispatchScheduledWatches(opts) {
15737
15666
  for (const exe of listExecutables()) {
15738
15667
  let raw;
15739
15668
  try {
15740
- raw = fs12.readFileSync(exe.profilePath, "utf-8");
15669
+ raw = fs13.readFileSync(exe.profilePath, "utf-8");
15741
15670
  } catch {
15742
15671
  continue;
15743
15672
  }
@@ -15980,9 +15909,9 @@ async function resolveAuthToken(env = process.env) {
15980
15909
  return void 0;
15981
15910
  }
15982
15911
  function detectPackageManager2(cwd) {
15983
- if (fs40.existsSync(path37.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
15984
- if (fs40.existsSync(path37.join(cwd, "yarn.lock"))) return "yarn";
15985
- 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";
15986
15915
  return "npm";
15987
15916
  }
15988
15917
  function shellOut(cmd, args, cwd, stream = true) {
@@ -16069,7 +15998,7 @@ function configureGitIdentity(cwd) {
16069
15998
  }
16070
15999
  function postFailureTail(issueNumber, cwd, reason) {
16071
16000
  if (!issueNumber) return;
16072
- const logPath = path37.join(cwd, ".kody", "last-run.jsonl");
16001
+ const logPath = path38.join(cwd, ".kody", "last-run.jsonl");
16073
16002
  let tail = "";
16074
16003
  try {
16075
16004
  if (fs40.existsSync(logPath)) {
@@ -16098,7 +16027,7 @@ async function runCi(argv) {
16098
16027
  return 0;
16099
16028
  }
16100
16029
  const args = parseCiArgs(argv);
16101
- const cwd = args.cwd ? path37.resolve(args.cwd) : process.cwd();
16030
+ const cwd = args.cwd ? path38.resolve(args.cwd) : process.cwd();
16102
16031
  let earlyConfig;
16103
16032
  let earlyConfigError;
16104
16033
  try {
@@ -16409,10 +16338,10 @@ init_repoWorkspace();
16409
16338
 
16410
16339
  // src/scripts/brainTurnLog.ts
16411
16340
  import * as fs41 from "fs";
16412
- import * as path38 from "path";
16341
+ import * as path39 from "path";
16413
16342
  var live = /* @__PURE__ */ new Map();
16414
16343
  function eventsPath(dir, chatId) {
16415
- return path38.join(dir, ".kody", "brain-events", `${chatId}.jsonl`);
16344
+ return path39.join(dir, ".kody", "brain-events", `${chatId}.jsonl`);
16416
16345
  }
16417
16346
  function lastPersistedSeq(dir, chatId) {
16418
16347
  const p = eventsPath(dir, chatId);
@@ -16455,7 +16384,7 @@ function beginTurn(dir, chatId) {
16455
16384
  };
16456
16385
  live.set(chatId, state);
16457
16386
  const p = eventsPath(dir, chatId);
16458
- fs41.mkdirSync(path38.dirname(p), { recursive: true });
16387
+ fs41.mkdirSync(path39.dirname(p), { recursive: true });
16459
16388
  return (event) => {
16460
16389
  state.seq += 1;
16461
16390
  const rec = { seq: state.seq, turn, ts: Date.now(), event };
@@ -16730,7 +16659,7 @@ async function handleChatTurn(req, res, chatId, opts) {
16730
16659
  const repo = strField(body, "repo");
16731
16660
  const repoToken = strField(body, "repoToken");
16732
16661
  const sessionFile = sessionFilePath(opts.cwd, chatId);
16733
- fs42.mkdirSync(path39.dirname(sessionFile), { recursive: true });
16662
+ fs42.mkdirSync(path40.dirname(sessionFile), { recursive: true });
16734
16663
  appendTurn(sessionFile, {
16735
16664
  role: "user",
16736
16665
  content: message,
@@ -16777,7 +16706,7 @@ async function handleChatTurn(req, res, chatId, opts) {
16777
16706
  function buildServer(opts) {
16778
16707
  const runTurn = opts.runTurn ?? runChatTurn;
16779
16708
  const cloneRepo = opts.cloneRepo ?? defaultCloneRepo;
16780
- 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");
16781
16710
  return createServer(async (req, res) => {
16782
16711
  if (!req.method || !req.url) {
16783
16712
  sendJson(res, 400, { error: "bad request" });
@@ -17371,13 +17300,13 @@ async function loadConfigSafe() {
17371
17300
  // src/chat-cli.ts
17372
17301
  import { execFileSync as execFileSync29 } from "child_process";
17373
17302
  import * as fs44 from "fs";
17374
- import * as path41 from "path";
17303
+ import * as path42 from "path";
17375
17304
 
17376
17305
  // src/chat/modes/interactive.ts
17377
17306
  init_issue();
17378
17307
  import { execFileSync as execFileSync28 } from "child_process";
17379
17308
  import * as fs43 from "fs";
17380
- import * as path40 from "path";
17309
+ import * as path41 from "path";
17381
17310
 
17382
17311
  // src/chat/inbox.ts
17383
17312
  import { execFileSync as execFileSync27 } from "child_process";
@@ -17536,9 +17465,9 @@ function findNextUserTurn(turns, fromIdx) {
17536
17465
  return -1;
17537
17466
  }
17538
17467
  function commitTurn(cwd, sessionId, _verbose) {
17539
- const sessionRel = path40.relative(cwd, sessionFilePath(cwd, sessionId));
17540
- const eventsRel = path40.relative(cwd, eventsFilePath(cwd, sessionId));
17541
- 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)));
17542
17471
  if (rels.length === 0) return;
17543
17472
  const repository = process.env.GITHUB_REPOSITORY;
17544
17473
  if (!repository) {
@@ -17550,8 +17479,8 @@ function commitTurn(cwd, sessionId, _verbose) {
17550
17479
  }
17551
17480
  const branch = defaultBranch(cwd) ?? "main";
17552
17481
  for (const rel of rels) {
17553
- const repoPath = rel.split(path40.sep).join("/");
17554
- 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");
17555
17484
  putJsonlViaContents(repository, branch, repoPath, localText, sessionId, cwd);
17556
17485
  }
17557
17486
  }
@@ -17688,12 +17617,12 @@ function parseChatArgs(argv, env = process.env) {
17688
17617
  return result;
17689
17618
  }
17690
17619
  function commitChatFiles(cwd, sessionId, verbose) {
17691
- const sessionFile = path41.relative(cwd, sessionFilePath(cwd, sessionId));
17692
- 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));
17693
17622
  const safeSession = sessionId.replace(/[^a-zA-Z0-9._-]/g, "_");
17694
- const tasksDir = path41.join(".kody", "tasks", safeSession);
17623
+ const tasksDir = path42.join(".kody", "tasks", safeSession);
17695
17624
  const candidatePaths = [sessionFile, eventsFile, tasksDir];
17696
- const paths = candidatePaths.filter((p) => fs44.existsSync(path41.join(cwd, p)));
17625
+ const paths = candidatePaths.filter((p) => fs44.existsSync(path42.join(cwd, p)));
17697
17626
  if (paths.length === 0) return;
17698
17627
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
17699
17628
  try {
@@ -17737,7 +17666,7 @@ async function runChat(argv) {
17737
17666
  ${CHAT_HELP}`);
17738
17667
  return 64;
17739
17668
  }
17740
- const cwd = args.cwd ? path41.resolve(args.cwd) : process.cwd();
17669
+ const cwd = args.cwd ? path42.resolve(args.cwd) : process.cwd();
17741
17670
  const sessionId = args.sessionId;
17742
17671
  const unpackedSecrets = unpackAllSecrets();
17743
17672
  if (unpackedSecrets > 0) {
@@ -17946,8 +17875,8 @@ var FlyClient = class {
17946
17875
  get fetch() {
17947
17876
  return this.opts.fetchImpl ?? fetch;
17948
17877
  }
17949
- async call(path42, init = {}) {
17950
- const res = await this.fetch(`${FLY_API_BASE}${path42}`, {
17878
+ async call(path43, init = {}) {
17879
+ const res = await this.fetch(`${FLY_API_BASE}${path43}`, {
17951
17880
  method: init.method ?? "GET",
17952
17881
  headers: {
17953
17882
  Authorization: `Bearer ${this.opts.token}`,
@@ -17958,7 +17887,7 @@ var FlyClient = class {
17958
17887
  if (res.status === 404 && init.allow404) return null;
17959
17888
  if (!res.ok) {
17960
17889
  const text = await res.text().catch(() => "");
17961
- 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}`);
17962
17891
  }
17963
17892
  if (res.status === 204) return null;
17964
17893
  const raw = await res.text();