@kody-ade/kody-engine 0.4.219 → 0.4.221

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 -665
  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.221",
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",
@@ -1283,7 +1283,7 @@ function dutyToolDefinitions(opts) {
1283
1283
  );
1284
1284
  const recommendTool = {
1285
1285
  name: "recommend_to_operator",
1286
- description: "Post ONE comment on a PR with the operator @-mention prepended. Use this when a verb is NOT graduated in the trust ledger and you want the operator to confirm via the dashboard inbox. The mention handle is substituted from kody.config.json `github.operators` \u2014 do not type it yourself.",
1286
+ description: "Post ONE comment on a PR with the operator @-mention prepended. Use this when a duty is in ASK mode and you want the operator to confirm via the dashboard inbox. The mention handle is substituted from kody.config.json `github.operators` \u2014 do not type it yourself.",
1287
1287
  inputSchema: {
1288
1288
  pr: z2.number().int().positive().describe("PR number to comment on."),
1289
1289
  body: z2.string().min(1).describe("Comment body (markdown). Do not include the operator mention \u2014 the engine prepends it.")
@@ -1298,9 +1298,9 @@ function dutyToolDefinitions(opts) {
1298
1298
  };
1299
1299
  const ledgerTool = {
1300
1300
  name: "read_ledger",
1301
- description: "Read the trust ledger (or any sentinel-fenced JSON manifest stored on a labeled issue). Returns `{found, issueNumber, payload}` where payload is the parsed JSON between `<!-- <label>:start -->` and `<!-- <label>:end -->` sentinels. Use `read_ledger({label: 'kody:cto-decisions'})` to look up per-verb graduation modes for the trust gate.",
1301
+ description: "Read any sentinel-fenced JSON manifest stored on a labeled issue. Returns `{found, issueNumber, payload}` where payload is the parsed JSON between `<!-- <label>:start -->` and `<!-- <label>:end -->` sentinels.",
1302
1302
  inputSchema: {
1303
- label: z2.string().min(1).describe("GitHub issue label that identifies the manifest issue (e.g. 'kody:cto-decisions').")
1303
+ label: z2.string().min(1).describe("GitHub issue label that identifies the manifest issue.")
1304
1304
  },
1305
1305
  handler: async (args) => {
1306
1306
  const label = String(args.label ?? "");
@@ -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,125 +2028,197 @@ 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));
2168
2205
  return out;
2169
2206
  }
2170
2207
  function getExecutableRoots() {
2171
- return [getProjectDutiesRoot(), getProjectExecutablesRoot(), getExecutablesRoot()];
2172
- }
2173
- function builtinExecutableNames() {
2174
- if (_builtinNames) return _builtinNames;
2175
- const out = /* @__PURE__ */ new Set();
2176
- const root = getExecutablesRoot();
2177
- 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);
2180
- }
2181
- } catch {
2182
- }
2183
- _builtinNames = out;
2184
- return out;
2185
- }
2186
- function isBuiltinExecutable(name) {
2187
- return builtinExecutableNames().has(name);
2208
+ return [getProjectExecutablesRoot(), getExecutablesRoot()];
2188
2209
  }
2189
2210
  function listExecutables(roots = getExecutableRoots()) {
2190
2211
  const rootList = typeof roots === "string" ? [roots] : roots;
2191
- const dutiesRoot = getProjectDutiesRoot();
2192
2212
  const seen = /* @__PURE__ */ new Set();
2193
2213
  const out = [];
2194
2214
  for (const root of rootList) {
2195
- if (!fs6.existsSync(root)) continue;
2196
- const entries = fs6.readdirSync(root, { withFileTypes: true });
2215
+ if (!fs7.existsSync(root)) continue;
2216
+ const entries = fs7.readdirSync(root, { withFileTypes: true });
2197
2217
  for (const ent of entries) {
2198
2218
  if (!ent.isDirectory()) continue;
2199
2219
  if (seen.has(ent.name)) continue;
2200
- 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()) {
2220
+ const profilePath = path7.join(root, ent.name, DUTY_PROFILE_FILE);
2221
+ if (fs7.existsSync(profilePath) && fs7.statSync(profilePath).isFile()) {
2203
2222
  out.push({ name: ent.name, profilePath });
2204
2223
  seen.add(ent.name);
2205
2224
  }
@@ -2210,11 +2229,9 @@ function listExecutables(roots = getExecutableRoots()) {
2210
2229
  function resolveExecutable(name, roots = getExecutableRoots()) {
2211
2230
  if (!isSafeName(name)) return null;
2212
2231
  const rootList = typeof roots === "string" ? [roots] : roots;
2213
- const dutiesRoot = getProjectDutiesRoot();
2214
2232
  for (const root of rootList) {
2215
- if (root === dutiesRoot && isBuiltinExecutable(name)) continue;
2216
- const profilePath = path6.join(root, name, "profile.json");
2217
- if (fs6.existsSync(profilePath) && fs6.statSync(profilePath).isFile()) {
2233
+ const profilePath = path7.join(root, name, "profile.json");
2234
+ if (fs7.existsSync(profilePath) && fs7.statSync(profilePath).isFile()) {
2218
2235
  return profilePath;
2219
2236
  }
2220
2237
  }
@@ -2233,7 +2250,6 @@ function listDutyActions(projectDutiesRoot = getProjectDutiesRoot()) {
2233
2250
  out.push(action);
2234
2251
  };
2235
2252
  for (const action of listProjectFolderDutyActions(projectDutiesRoot)) add(action);
2236
- for (const action of listProjectMarkdownDutyActions(projectDutiesRoot)) add(action);
2237
2253
  for (const action of listBuiltinDutyActions()) add(action);
2238
2254
  return out.sort((a, b) => a.action.localeCompare(b.action));
2239
2255
  }
@@ -2248,108 +2264,54 @@ function isSafeName(name) {
2248
2264
  return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
2249
2265
  }
2250
2266
  function listProjectFolderDutyActions(root) {
2251
- if (!fs6.existsSync(root) || !fs6.statSync(root).isDirectory()) return [];
2267
+ if (!fs7.existsSync(root) || !fs7.statSync(root).isDirectory()) return [];
2252
2268
  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
- }
2269
+ for (const slug of listDutyFolderSlugs(root)) {
2270
+ if (!isSafeName(slug)) continue;
2271
+ const duty = readDutyFolder(root, slug);
2272
+ if (!duty) continue;
2273
+ const action = duty.config.action ?? slug;
2274
+ const executable = duty.config.executable ?? duty.config.executables?.[0] ?? (duty.config.tickScript ? "duty-tick-scripted" : "duty-tick");
2275
+ out.push({
2276
+ action,
2277
+ duty: slug,
2278
+ executable,
2279
+ cliArgs: duty.config.executable ? {} : { duty: slug },
2280
+ source: "project-folder",
2281
+ describe: duty.config.describe ?? duty.title,
2282
+ profilePath: duty.profilePath,
2283
+ bodyPath: duty.bodyPath
2284
+ });
2274
2285
  }
2275
2286
  return out.sort((a, b) => a.action.localeCompare(b.action));
2276
2287
  }
2277
- function listProjectMarkdownDutyActions(root) {
2278
- if (!fs6.existsSync(root) || !fs6.statSync(root).isDirectory()) return [];
2288
+ function listBuiltinDutyActions(root = getBuiltinDutiesRoot()) {
2289
+ if (!fs7.existsSync(root) || !fs7.statSync(root).isDirectory()) return [];
2279
2290
  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
- }
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 ?? slug;
2297
+ out.push({
2298
+ action,
2299
+ duty: slug,
2300
+ executable,
2301
+ cliArgs: {},
2302
+ source: "builtin",
2303
+ describe: duty.config.describe ?? duty.title,
2304
+ profilePath: duty.profilePath,
2305
+ bodyPath: duty.bodyPath
2306
+ });
2301
2307
  }
2302
2308
  return out.sort((a, b) => a.action.localeCompare(b.action));
2303
2309
  }
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
2310
  function getProfileInputs(name, roots = getExecutableRoots()) {
2349
2311
  const profilePath = resolveExecutable(name, roots);
2350
2312
  if (!profilePath) return null;
2351
2313
  try {
2352
- const raw = JSON.parse(fs6.readFileSync(profilePath, "utf-8"));
2314
+ const raw = JSON.parse(fs7.readFileSync(profilePath, "utf-8"));
2353
2315
  if (!raw || typeof raw !== "object" || !Array.isArray(raw.inputs)) return [];
2354
2316
  return raw.inputs;
2355
2317
  } catch {
@@ -2381,31 +2343,29 @@ function parseGenericFlags(argv) {
2381
2343
  if (positional.length > 0) args._ = positional;
2382
2344
  return args;
2383
2345
  }
2384
- var _builtinNames;
2385
2346
  var init_registry = __esm({
2386
2347
  "src/registry.ts"() {
2387
2348
  "use strict";
2388
- init_jobFrontmatter();
2389
- _builtinNames = null;
2349
+ init_dutyFolders();
2390
2350
  }
2391
2351
  });
2392
2352
 
2393
2353
  // src/task-artifacts.ts
2394
- import fs7 from "fs";
2395
- import path7 from "path";
2354
+ import fs8 from "fs";
2355
+ import path8 from "path";
2396
2356
  function prepareTaskArtifactsDir(cwd, taskId) {
2397
2357
  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 });
2358
+ const relDir = path8.join(".kody", "tasks", safeId);
2359
+ const absDir = path8.join(cwd, relDir);
2360
+ fs8.mkdirSync(absDir, { recursive: true });
2401
2361
  return { taskId: safeId, absDir, relDir };
2402
2362
  }
2403
2363
  function verifyTaskArtifacts(absDir) {
2404
2364
  const missing = [];
2405
2365
  for (const name of TASK_ARTIFACT_FILES) {
2406
- const full = path7.join(absDir, name);
2366
+ const full = path8.join(absDir, name);
2407
2367
  try {
2408
- const stat = fs7.statSync(full);
2368
+ const stat = fs8.statSync(full);
2409
2369
  if (!stat.isFile() || stat.size === 0) missing.push(name);
2410
2370
  } catch {
2411
2371
  missing.push(name);
@@ -2639,31 +2599,31 @@ var init_lifecycles = __esm({
2639
2599
  });
2640
2600
 
2641
2601
  // src/scripts/buildSyntheticPlugin.ts
2642
- import * as fs13 from "fs";
2602
+ import * as fs14 from "fs";
2643
2603
  import * as os2 from "os";
2644
- import * as path12 from "path";
2604
+ import * as path13 from "path";
2645
2605
  function getPluginsCatalogRoot() {
2646
- const here = path12.dirname(new URL(import.meta.url).pathname);
2606
+ const here = path13.dirname(new URL(import.meta.url).pathname);
2647
2607
  const candidates = [
2648
- path12.join(here, "..", "plugins"),
2608
+ path13.join(here, "..", "plugins"),
2649
2609
  // dev: src/scripts → src/plugins
2650
- path12.join(here, "..", "..", "plugins"),
2610
+ path13.join(here, "..", "..", "plugins"),
2651
2611
  // built: dist/scripts → dist/plugins
2652
- path12.join(here, "..", "..", "src", "plugins")
2612
+ path13.join(here, "..", "..", "src", "plugins")
2653
2613
  // fallback
2654
2614
  ];
2655
2615
  for (const c of candidates) {
2656
- if (fs13.existsSync(c) && fs13.statSync(c).isDirectory()) return c;
2616
+ if (fs14.existsSync(c) && fs14.statSync(c).isDirectory()) return c;
2657
2617
  }
2658
2618
  return candidates[0];
2659
2619
  }
2660
2620
  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);
2621
+ fs14.mkdirSync(dst, { recursive: true });
2622
+ for (const ent of fs14.readdirSync(src, { withFileTypes: true })) {
2623
+ const s = path13.join(src, ent.name);
2624
+ const d = path13.join(dst, ent.name);
2665
2625
  if (ent.isDirectory()) copyDir(s, d);
2666
- else if (ent.isFile()) fs13.copyFileSync(s, d);
2626
+ else if (ent.isFile()) fs14.copyFileSync(s, d);
2667
2627
  }
2668
2628
  }
2669
2629
  var buildSyntheticPlugin;
@@ -2676,45 +2636,45 @@ var init_buildSyntheticPlugin = __esm({
2676
2636
  if (!needsSynthetic) return;
2677
2637
  const catalog = getPluginsCatalogRoot();
2678
2638
  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 });
2639
+ const root = path13.join(os2.tmpdir(), `kody-synth-${runId}`);
2640
+ fs14.mkdirSync(path13.join(root, ".claude-plugin"), { recursive: true });
2681
2641
  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;
2642
+ const local = path13.join(profile.dir, bucket, entry);
2643
+ if (fs14.existsSync(local)) return local;
2644
+ const central = path13.join(catalog, bucket, entry);
2645
+ if (fs14.existsSync(central)) return central;
2686
2646
  throw new Error(
2687
2647
  `buildSyntheticPlugin: ${bucket} entry '${entry}' not found in executable dir (${profile.dir}/${bucket}/) or catalog (${catalog}/${bucket}/)`
2688
2648
  );
2689
2649
  };
2690
2650
  if (cc.skills.length > 0) {
2691
- const dst = path12.join(root, "skills");
2692
- fs13.mkdirSync(dst, { recursive: true });
2651
+ const dst = path13.join(root, "skills");
2652
+ fs14.mkdirSync(dst, { recursive: true });
2693
2653
  for (const name of cc.skills) {
2694
- copyDir(resolvePart("skills", name), path12.join(dst, name));
2654
+ copyDir(resolvePart("skills", name), path13.join(dst, name));
2695
2655
  }
2696
2656
  }
2697
2657
  if (cc.commands.length > 0) {
2698
- const dst = path12.join(root, "commands");
2699
- fs13.mkdirSync(dst, { recursive: true });
2658
+ const dst = path13.join(root, "commands");
2659
+ fs14.mkdirSync(dst, { recursive: true });
2700
2660
  for (const name of cc.commands) {
2701
- fs13.copyFileSync(resolvePart("commands", `${name}.md`), path12.join(dst, `${name}.md`));
2661
+ fs14.copyFileSync(resolvePart("commands", `${name}.md`), path13.join(dst, `${name}.md`));
2702
2662
  }
2703
2663
  }
2704
2664
  if (cc.hooks.length > 0) {
2705
- const dst = path12.join(root, "hooks");
2706
- fs13.mkdirSync(dst, { recursive: true });
2665
+ const dst = path13.join(root, "hooks");
2666
+ fs14.mkdirSync(dst, { recursive: true });
2707
2667
  const merged = { hooks: {} };
2708
2668
  for (const name of cc.hooks) {
2709
2669
  const src = resolvePart("hooks", `${name}.json`);
2710
- const parsed = JSON.parse(fs13.readFileSync(src, "utf-8"));
2670
+ const parsed = JSON.parse(fs14.readFileSync(src, "utf-8"));
2711
2671
  for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
2712
2672
  if (!Array.isArray(entries)) continue;
2713
2673
  if (!merged.hooks[event]) merged.hooks[event] = [];
2714
2674
  merged.hooks[event].push(...entries);
2715
2675
  }
2716
2676
  }
2717
- fs13.writeFileSync(path12.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
2677
+ fs14.writeFileSync(path13.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
2718
2678
  `);
2719
2679
  }
2720
2680
  const manifest = {
@@ -2724,7 +2684,7 @@ var init_buildSyntheticPlugin = __esm({
2724
2684
  };
2725
2685
  if (cc.skills.length > 0) manifest.skills = ["./skills/"];
2726
2686
  if (cc.commands.length > 0) manifest.commands = ["./commands/"];
2727
- fs13.writeFileSync(path12.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
2687
+ fs14.writeFileSync(path13.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
2728
2688
  `);
2729
2689
  ctx.data.syntheticPluginPath = root;
2730
2690
  };
@@ -2732,9 +2692,9 @@ var init_buildSyntheticPlugin = __esm({
2732
2692
  });
2733
2693
 
2734
2694
  // src/subagents.ts
2735
- import * as fs14 from "fs";
2736
- import * as path13 from "path";
2737
- function splitFrontmatter2(raw) {
2695
+ import * as fs15 from "fs";
2696
+ import * as path14 from "path";
2697
+ function splitFrontmatter(raw) {
2738
2698
  const match = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(raw);
2739
2699
  if (!match) return { fm: {}, body: raw.trim() };
2740
2700
  const fm = {};
@@ -2746,10 +2706,10 @@ function splitFrontmatter2(raw) {
2746
2706
  return { fm, body: (match[2] ?? "").trim() };
2747
2707
  }
2748
2708
  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;
2709
+ const local = path14.join(profileDir, "agents", `${name}.md`);
2710
+ if (fs15.existsSync(local)) return local;
2711
+ const central = path14.join(getPluginsCatalogRoot(), "agents", `${name}.md`);
2712
+ if (fs15.existsSync(central)) return central;
2753
2713
  throw new Error(`loadSubagents: agent '${name}' not found in ${profileDir}/agents/ or shared catalog`);
2754
2714
  }
2755
2715
  function captureSubagentTemplates(profile) {
@@ -2758,7 +2718,7 @@ function captureSubagentTemplates(profile) {
2758
2718
  const out = {};
2759
2719
  for (const name of names) {
2760
2720
  try {
2761
- out[name] = fs14.readFileSync(resolveAgentFile(profile.dir, name), "utf-8");
2721
+ out[name] = fs15.readFileSync(resolveAgentFile(profile.dir, name), "utf-8");
2762
2722
  } catch {
2763
2723
  }
2764
2724
  }
@@ -2769,8 +2729,8 @@ function loadSubagents(profile) {
2769
2729
  if (!names || names.length === 0) return void 0;
2770
2730
  const agents = {};
2771
2731
  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);
2732
+ const raw = profile.subagentTemplates?.[name] ?? fs15.readFileSync(resolveAgentFile(profile.dir, name), "utf-8");
2733
+ const { fm, body } = splitFrontmatter(raw);
2774
2734
  if (!body) throw new Error(`loadSubagents: agent '${name}' has an empty prompt body`);
2775
2735
  const def = {
2776
2736
  description: fm.description ?? `Subagent ${name}`,
@@ -2793,15 +2753,15 @@ var init_subagents = __esm({
2793
2753
  });
2794
2754
 
2795
2755
  // src/profile.ts
2796
- import * as fs15 from "fs";
2797
- import * as path14 from "path";
2756
+ import * as fs16 from "fs";
2757
+ import * as path15 from "path";
2798
2758
  function loadProfile(profilePath) {
2799
- if (!fs15.existsSync(profilePath)) {
2759
+ if (!fs16.existsSync(profilePath)) {
2800
2760
  throw new ProfileError(profilePath, "file not found");
2801
2761
  }
2802
2762
  let raw;
2803
2763
  try {
2804
- raw = JSON.parse(fs15.readFileSync(profilePath, "utf-8"));
2764
+ raw = JSON.parse(fs16.readFileSync(profilePath, "utf-8"));
2805
2765
  } catch (err) {
2806
2766
  throw new ProfileError(profilePath, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
2807
2767
  }
@@ -2812,7 +2772,7 @@ function loadProfile(profilePath) {
2812
2772
  const unknownKeys = Object.keys(r).filter((k) => !KNOWN_PROFILE_KEYS.has(k));
2813
2773
  if (unknownKeys.length > 0) {
2814
2774
  process.stderr.write(
2815
- `[kody profile] ${path14.basename(path14.dirname(profilePath))}: unknown top-level keys ignored: ${unknownKeys.join(", ")}
2775
+ `[kody profile] ${path15.basename(path15.dirname(profilePath))}: unknown top-level keys ignored: ${unknownKeys.join(", ")}
2816
2776
  `
2817
2777
  );
2818
2778
  }
@@ -2831,7 +2791,7 @@ function loadProfile(profilePath) {
2831
2791
  describe: typeof r.describe === "string" ? r.describe : base.describe,
2832
2792
  staff: typeof r.staff === "string" && r.staff.trim() ? r.staff.trim() : base.staff,
2833
2793
  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,
2794
+ dutyTools: parseStringArray(r.dutyTools ?? r.tools) ?? base.dutyTools,
2835
2795
  mentions: Array.isArray(r.mentions) ? r.mentions.map((m) => String(m).trim()).filter(Boolean) : base.mentions
2836
2796
  };
2837
2797
  }
@@ -2877,8 +2837,8 @@ function loadProfile(profilePath) {
2877
2837
  staff: typeof r.staff === "string" && r.staff.trim() ? r.staff.trim() : void 0,
2878
2838
  // Optional recurrence cadence (scheduled duty). Blank → undefined (on-demand).
2879
2839
  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,
2840
+ // Locked-toolbox palette + mentions from folder-duty profile metadata.
2841
+ dutyTools: parseStringArray(r.dutyTools ?? r.tools),
2882
2842
  mentions: Array.isArray(r.mentions) ? r.mentions.map((m) => String(m).trim()).filter(Boolean) : void 0,
2883
2843
  role,
2884
2844
  kind,
@@ -2901,8 +2861,8 @@ function loadProfile(profilePath) {
2901
2861
  // Phase 5 in-process handoff opt-in. Default false; containers
2902
2862
  // flip to true after end-to-end verification.
2903
2863
  preloadContext: r.preloadContext === true,
2904
- dir: path14.dirname(profilePath),
2905
- promptTemplates: readPromptTemplates(path14.dirname(profilePath))
2864
+ dir: path15.dirname(profilePath),
2865
+ promptTemplates: readPromptTemplates(path15.dirname(profilePath))
2906
2866
  };
2907
2867
  if (lifecycle) {
2908
2868
  applyLifecycle(profile, profilePath);
@@ -2934,15 +2894,16 @@ function readPromptTemplates(dir) {
2934
2894
  const out = {};
2935
2895
  const read = (p) => {
2936
2896
  try {
2937
- out[p] = fs15.readFileSync(p, "utf-8");
2897
+ out[p] = fs16.readFileSync(p, "utf-8");
2938
2898
  } catch {
2939
2899
  }
2940
2900
  };
2941
- read(path14.join(dir, "prompt.md"));
2901
+ read(path15.join(dir, "prompt.md"));
2902
+ read(path15.join(dir, "duty.md"));
2942
2903
  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));
2904
+ const promptsDir = path15.join(dir, "prompts");
2905
+ for (const ent of fs16.readdirSync(promptsDir)) {
2906
+ if (ent.endsWith(".md")) read(path15.join(promptsDir, ent));
2946
2907
  }
2947
2908
  } catch {
2948
2909
  }
@@ -2962,6 +2923,11 @@ function requireString(p, r, key) {
2962
2923
  }
2963
2924
  return v;
2964
2925
  }
2926
+ function parseStringArray(raw) {
2927
+ if (!Array.isArray(raw)) return void 0;
2928
+ const values = raw.map((t) => String(t).trim()).filter(Boolean);
2929
+ return values.length > 0 ? values : void 0;
2930
+ }
2965
2931
  function parseInputs(p, raw) {
2966
2932
  if (!Array.isArray(raw)) throw new ProfileError(p, `"inputs" must be an array`);
2967
2933
  const out = [];
@@ -3213,7 +3179,11 @@ var init_profile = __esm({
3213
3179
  "staff",
3214
3180
  "every",
3215
3181
  "dutyTools",
3182
+ "tools",
3216
3183
  "mentions",
3184
+ "stage",
3185
+ "readsFrom",
3186
+ "writesTo",
3217
3187
  "describe",
3218
3188
  "role",
3219
3189
  "kind",
@@ -3610,16 +3580,16 @@ var init_state = __esm({
3610
3580
  });
3611
3581
 
3612
3582
  // src/prompt.ts
3613
- import * as fs16 from "fs";
3614
- import * as path15 from "path";
3583
+ import * as fs17 from "fs";
3584
+ import * as path16 from "path";
3615
3585
  function loadProjectConventions(projectDir) {
3616
3586
  const out = [];
3617
3587
  for (const rel of CONVENTION_FILES) {
3618
- const abs = path15.join(projectDir, rel);
3619
- if (!fs16.existsSync(abs)) continue;
3588
+ const abs = path16.join(projectDir, rel);
3589
+ if (!fs17.existsSync(abs)) continue;
3620
3590
  let content;
3621
3591
  try {
3622
- content = fs16.readFileSync(abs, "utf-8");
3592
+ content = fs17.readFileSync(abs, "utf-8");
3623
3593
  } catch {
3624
3594
  continue;
3625
3595
  }
@@ -3854,28 +3824,28 @@ var loadMemoryContext_exports = {};
3854
3824
  __export(loadMemoryContext_exports, {
3855
3825
  loadMemoryContext: () => loadMemoryContext
3856
3826
  });
3857
- import * as fs17 from "fs";
3858
- import * as path16 from "path";
3827
+ import * as fs18 from "fs";
3828
+ import * as path17 from "path";
3859
3829
  function collectPages(memoryAbs) {
3860
3830
  const out = [];
3861
3831
  walkMd(memoryAbs, (file) => {
3862
3832
  let stat;
3863
3833
  try {
3864
- stat = fs17.statSync(file);
3834
+ stat = fs18.statSync(file);
3865
3835
  } catch {
3866
3836
  return;
3867
3837
  }
3868
3838
  let raw;
3869
3839
  try {
3870
- raw = fs17.readFileSync(file, "utf-8");
3840
+ raw = fs18.readFileSync(file, "utf-8");
3871
3841
  } catch {
3872
3842
  return;
3873
3843
  }
3874
3844
  const fm = raw.match(/^---\s*\n([\s\S]*?)\n---/);
3875
- const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path16.basename(file, ".md");
3845
+ const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path17.basename(file, ".md");
3876
3846
  const updated = fm?.[1]?.match(/^updated:\s*([0-9T:.+\-Z]+)/m)?.[1]?.trim() ?? "";
3877
3847
  out.push({
3878
- relPath: path16.relative(memoryAbs, file),
3848
+ relPath: path17.relative(memoryAbs, file),
3879
3849
  title,
3880
3850
  updated,
3881
3851
  content: raw.length > PER_PAGE_MAX_BYTES ? raw.slice(0, PER_PAGE_MAX_BYTES) + TRUNCATED_SUFFIX2 : raw,
@@ -3943,16 +3913,16 @@ function walkMd(root, visit) {
3943
3913
  const dir = stack.pop();
3944
3914
  let names;
3945
3915
  try {
3946
- names = fs17.readdirSync(dir);
3916
+ names = fs18.readdirSync(dir);
3947
3917
  } catch {
3948
3918
  continue;
3949
3919
  }
3950
3920
  for (const name of names) {
3951
3921
  if (name.startsWith(".")) continue;
3952
- const full = path16.join(dir, name);
3922
+ const full = path17.join(dir, name);
3953
3923
  let stat;
3954
3924
  try {
3955
- stat = fs17.statSync(full);
3925
+ stat = fs18.statSync(full);
3956
3926
  } catch {
3957
3927
  continue;
3958
3928
  }
@@ -3975,8 +3945,8 @@ var init_loadMemoryContext = __esm({
3975
3945
  TRUNCATED_SUFFIX2 = "\n\n\u2026 (truncated)";
3976
3946
  loadMemoryContext = async (ctx) => {
3977
3947
  if (typeof ctx.data.memoryContext === "string") return;
3978
- const memoryAbs = path16.join(ctx.cwd, MEMORY_DIR_RELATIVE);
3979
- if (!fs17.existsSync(memoryAbs)) {
3948
+ const memoryAbs = path17.join(ctx.cwd, MEMORY_DIR_RELATIVE);
3949
+ if (!fs18.existsSync(memoryAbs)) {
3980
3950
  ctx.data.memoryContext = "";
3981
3951
  return;
3982
3952
  }
@@ -4020,11 +3990,11 @@ var init_loadCoverageRules = __esm({
4020
3990
 
4021
3991
  // src/container.ts
4022
3992
  import { execFileSync as execFileSync3 } from "child_process";
4023
- import * as fs18 from "fs";
3993
+ import * as fs19 from "fs";
4024
3994
  function getProfileInputsForChild(profileName, _cwd) {
4025
3995
  try {
4026
3996
  const profilePath = resolveProfilePath(profileName);
4027
- if (!fs18.existsSync(profilePath)) return null;
3997
+ if (!fs19.existsSync(profilePath)) return null;
4028
3998
  return loadProfile(profilePath).inputs;
4029
3999
  } catch {
4030
4000
  return null;
@@ -4486,9 +4456,9 @@ var init_lifecycleLabels = __esm({
4486
4456
 
4487
4457
  // src/litellm.ts
4488
4458
  import { execFileSync as execFileSync4, spawn as spawn3 } from "child_process";
4489
- import * as fs19 from "fs";
4459
+ import * as fs20 from "fs";
4490
4460
  import * as os3 from "os";
4491
- import * as path17 from "path";
4461
+ import * as path18 from "path";
4492
4462
  async function checkLitellmHealth(url) {
4493
4463
  try {
4494
4464
  const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
@@ -4573,13 +4543,13 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
4573
4543
  let child;
4574
4544
  let logPath;
4575
4545
  const spawnProxy = () => {
4576
- const configPath = path17.join(os3.tmpdir(), `kody-local-litellm-${Date.now()}.yaml`);
4577
- fs19.writeFileSync(configPath, generateLitellmConfigYaml(model));
4546
+ const configPath = path18.join(os3.tmpdir(), `kody-local-litellm-${Date.now()}.yaml`);
4547
+ fs20.writeFileSync(configPath, generateLitellmConfigYaml(model));
4578
4548
  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");
4549
+ const nextLogPath = path18.join(os3.tmpdir(), `kody-local-litellm-${Date.now()}.log`);
4550
+ const outFd = fs20.openSync(nextLogPath, "w");
4581
4551
  child = spawn3(cmd, args, { stdio: ["ignore", outFd, outFd], detached: true, env: childEnv });
4582
- fs19.closeSync(outFd);
4552
+ fs20.closeSync(outFd);
4583
4553
  logPath = nextLogPath;
4584
4554
  };
4585
4555
  const waitForHealth = async () => {
@@ -4593,7 +4563,7 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
4593
4563
  const readLogTail = () => {
4594
4564
  if (!logPath) return "";
4595
4565
  try {
4596
- return fs19.readFileSync(logPath, "utf-8").slice(-2e3);
4566
+ return fs20.readFileSync(logPath, "utf-8").slice(-2e3);
4597
4567
  } catch {
4598
4568
  return "";
4599
4569
  }
@@ -4645,10 +4615,10 @@ ${tail}`
4645
4615
  return { url, kill: killChild, isHealthy, ensureHealthy };
4646
4616
  }
4647
4617
  function readDotenvApiKeys(projectDir) {
4648
- const dotenvPath = path17.join(projectDir, ".env");
4649
- if (!fs19.existsSync(dotenvPath)) return {};
4618
+ const dotenvPath = path18.join(projectDir, ".env");
4619
+ if (!fs20.existsSync(dotenvPath)) return {};
4650
4620
  const result = {};
4651
- for (const rawLine of fs19.readFileSync(dotenvPath, "utf-8").split("\n")) {
4621
+ for (const rawLine of fs20.readFileSync(dotenvPath, "utf-8").split("\n")) {
4652
4622
  const line = rawLine.trim();
4653
4623
  if (!line || line.startsWith("#")) continue;
4654
4624
  const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
@@ -4763,8 +4733,8 @@ var init_pushWithRetry = __esm({
4763
4733
 
4764
4734
  // src/commit.ts
4765
4735
  import { execFileSync as execFileSync6 } from "child_process";
4766
- import * as fs20 from "fs";
4767
- import * as path18 from "path";
4736
+ import * as fs21 from "fs";
4737
+ import * as path19 from "path";
4768
4738
  function git(args, cwd) {
4769
4739
  try {
4770
4740
  return execFileSync6("git", args, {
@@ -4802,18 +4772,18 @@ function ensureGitIdentity(cwd) {
4802
4772
  }
4803
4773
  function abortUnfinishedGitOps(cwd) {
4804
4774
  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"))) {
4775
+ const gitDir = path19.join(cwd ?? process.cwd(), ".git");
4776
+ if (!fs21.existsSync(gitDir)) return aborted;
4777
+ if (fs21.existsSync(path19.join(gitDir, "MERGE_HEAD"))) {
4808
4778
  if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
4809
4779
  }
4810
- if (fs20.existsSync(path18.join(gitDir, "CHERRY_PICK_HEAD"))) {
4780
+ if (fs21.existsSync(path19.join(gitDir, "CHERRY_PICK_HEAD"))) {
4811
4781
  if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
4812
4782
  }
4813
- if (fs20.existsSync(path18.join(gitDir, "REVERT_HEAD"))) {
4783
+ if (fs21.existsSync(path19.join(gitDir, "REVERT_HEAD"))) {
4814
4784
  if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
4815
4785
  }
4816
- if (fs20.existsSync(path18.join(gitDir, "rebase-merge")) || fs20.existsSync(path18.join(gitDir, "rebase-apply"))) {
4786
+ if (fs21.existsSync(path19.join(gitDir, "rebase-merge")) || fs21.existsSync(path19.join(gitDir, "rebase-apply"))) {
4817
4787
  if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
4818
4788
  }
4819
4789
  try {
@@ -4869,7 +4839,7 @@ function normalizeCommitMessage(raw) {
4869
4839
  function commitAndPush(branch, agentMessage, cwd) {
4870
4840
  const allChanged = listChangedFiles(cwd);
4871
4841
  const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
4872
- const mergeHeadExists = fs20.existsSync(path18.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
4842
+ const mergeHeadExists = fs21.existsSync(path19.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
4873
4843
  if (allowedFiles.length === 0 && !mergeHeadExists) {
4874
4844
  return { committed: false, pushed: false, sha: "", message: "" };
4875
4845
  }
@@ -5106,7 +5076,7 @@ var init_advanceFlow = __esm({
5106
5076
 
5107
5077
  // src/gha.ts
5108
5078
  import { execFileSync as execFileSync8 } from "child_process";
5109
- import * as fs21 from "fs";
5079
+ import * as fs22 from "fs";
5110
5080
  function getRunUrl() {
5111
5081
  const server = process.env.GITHUB_SERVER_URL;
5112
5082
  const repo = process.env.GITHUB_REPOSITORY;
@@ -5117,10 +5087,10 @@ function getRunUrl() {
5117
5087
  function reactToTriggerComment(cwd) {
5118
5088
  if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
5119
5089
  const eventPath = process.env.GITHUB_EVENT_PATH;
5120
- if (!eventPath || !fs21.existsSync(eventPath)) return;
5090
+ if (!eventPath || !fs22.existsSync(eventPath)) return;
5121
5091
  let event = null;
5122
5092
  try {
5123
- event = JSON.parse(fs21.readFileSync(eventPath, "utf-8"));
5093
+ event = JSON.parse(fs22.readFileSync(eventPath, "utf-8"));
5124
5094
  } catch {
5125
5095
  return;
5126
5096
  }
@@ -5454,11 +5424,11 @@ var init_classifyByLabel = __esm({
5454
5424
  });
5455
5425
 
5456
5426
  // src/scripts/commitAndPush.ts
5457
- import * as fs22 from "fs";
5458
- import * as path19 from "path";
5427
+ import * as fs23 from "fs";
5428
+ import * as path20 from "path";
5459
5429
  function sentinelPathForStage(cwd, profileName) {
5460
5430
  const runId = resolveRunId();
5461
- return path19.join(cwd, ".kody", "runs", runId, `commit-${profileName}.lock`);
5431
+ return path20.join(cwd, ".kody", "runs", runId, `commit-${profileName}.lock`);
5462
5432
  }
5463
5433
  var DEFAULT_COMMIT_MESSAGE, commitAndPush2;
5464
5434
  var init_commitAndPush = __esm({
@@ -5475,9 +5445,9 @@ var init_commitAndPush = __esm({
5475
5445
  }
5476
5446
  const idempotencyEnabled = process.env.KODY_COMMIT_IDEMPOTENCY !== "0";
5477
5447
  const sentinel = idempotencyEnabled ? sentinelPathForStage(ctx.cwd, profile.name) : null;
5478
- if (sentinel && fs22.existsSync(sentinel)) {
5448
+ if (sentinel && fs23.existsSync(sentinel)) {
5479
5449
  try {
5480
- const replay = JSON.parse(fs22.readFileSync(sentinel, "utf-8"));
5450
+ const replay = JSON.parse(fs23.readFileSync(sentinel, "utf-8"));
5481
5451
  ctx.data.commitResult = replay.commitResult ?? { committed: false, pushed: false };
5482
5452
  if (Array.isArray(replay.changedFiles)) ctx.data.changedFiles = replay.changedFiles;
5483
5453
  if (typeof replay.hasCommitsAhead === "boolean") ctx.data.hasCommitsAhead = replay.hasCommitsAhead;
@@ -5530,8 +5500,8 @@ var init_commitAndPush = __esm({
5530
5500
  const result = ctx.data.commitResult;
5531
5501
  if (sentinel && result?.committed) {
5532
5502
  try {
5533
- fs22.mkdirSync(path19.dirname(sentinel), { recursive: true });
5534
- fs22.writeFileSync(
5503
+ fs23.mkdirSync(path20.dirname(sentinel), { recursive: true });
5504
+ fs23.writeFileSync(
5535
5505
  sentinel,
5536
5506
  JSON.stringify(
5537
5507
  {
@@ -5553,8 +5523,8 @@ var init_commitAndPush = __esm({
5553
5523
  });
5554
5524
 
5555
5525
  // src/goal/state.ts
5556
- import * as fs23 from "fs";
5557
- import * as path20 from "path";
5526
+ import * as fs24 from "fs";
5527
+ import * as path21 from "path";
5558
5528
  function parseGoalState(filePath, raw) {
5559
5529
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
5560
5530
  throw new GoalStateError(filePath, "must be a JSON object");
@@ -5606,10 +5576,10 @@ var init_state2 = __esm({
5606
5576
  "use strict";
5607
5577
  VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "awaiting-merge", "done"]);
5608
5578
  GoalStateError = class extends Error {
5609
- constructor(path42, message) {
5610
- super(`Invalid goal state at ${path42}:
5579
+ constructor(path43, message) {
5580
+ super(`Invalid goal state at ${path43}:
5611
5581
  ${message}`);
5612
- this.path = path42;
5582
+ this.path = path43;
5613
5583
  this.name = "GoalStateError";
5614
5584
  }
5615
5585
  path;
@@ -5721,8 +5691,8 @@ var init_commitGoalState = __esm({
5721
5691
  });
5722
5692
 
5723
5693
  // src/scripts/composePrompt.ts
5724
- import * as fs24 from "fs";
5725
- import * as path21 from "path";
5694
+ import * as fs25 from "fs";
5695
+ import * as path22 from "path";
5726
5696
  function fenceUntrusted(value) {
5727
5697
  if (value.trim().length === 0) return value;
5728
5698
  const safe = value.replace(/-{3,}\s*END UNTRUSTED INPUT\s*-{3,}/gi, "[END UNTRUSTED INPUT]");
@@ -5811,6 +5781,10 @@ function formatDutyReference(data, profileName) {
5811
5781
  if (dutySchedule) {
5812
5782
  lines.push(`- Cadence: \`${dutySchedule}\``);
5813
5783
  }
5784
+ const dutyBody = pickToken(data, "dutyIntent", "jobIntent");
5785
+ if (dutyBody) {
5786
+ lines.push("", "## Duty body", "", dutyBody);
5787
+ }
5814
5788
  if (lines.length === 2) {
5815
5789
  return "";
5816
5790
  }
@@ -5839,9 +5813,10 @@ var init_composePrompt = __esm({
5839
5813
  const explicit = ctx.data.promptTemplate;
5840
5814
  const mode = ctx.args.mode;
5841
5815
  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")
5816
+ explicit ? path22.join(profile.dir, explicit) : null,
5817
+ mode ? path22.join(profile.dir, "prompts", `${mode}.md`) : null,
5818
+ path22.join(profile.dir, "prompt.md"),
5819
+ path22.join(profile.dir, "duty.md")
5845
5820
  ].filter(Boolean);
5846
5821
  let templatePath = "";
5847
5822
  let template = "";
@@ -5854,7 +5829,7 @@ var init_composePrompt = __esm({
5854
5829
  break;
5855
5830
  }
5856
5831
  try {
5857
- template = fs24.readFileSync(c, "utf-8");
5832
+ template = fs25.readFileSync(c, "utf-8");
5858
5833
  templatePath = c;
5859
5834
  break;
5860
5835
  } catch (err) {
@@ -5865,7 +5840,7 @@ var init_composePrompt = __esm({
5865
5840
  if (!templatePath) {
5866
5841
  let dirState;
5867
5842
  try {
5868
- dirState = `dir contents: [${fs24.readdirSync(profile.dir).join(", ")}]`;
5843
+ dirState = `dir contents: [${fs25.readdirSync(profile.dir).join(", ")}]`;
5869
5844
  } catch (err) {
5870
5845
  dirState = `readdir(${profile.dir}) failed: ${err?.code ?? String(err)}`;
5871
5846
  }
@@ -6692,19 +6667,19 @@ var init_deriveQaScopeFromIssue = __esm({
6692
6667
 
6693
6668
  // src/scripts/diagMcp.ts
6694
6669
  import { execFileSync as execFileSync10 } from "child_process";
6695
- import * as fs25 from "fs";
6670
+ import * as fs26 from "fs";
6696
6671
  import * as os4 from "os";
6697
- import * as path22 from "path";
6672
+ import * as path23 from "path";
6698
6673
  var diagMcp;
6699
6674
  var init_diagMcp = __esm({
6700
6675
  "src/scripts/diagMcp.ts"() {
6701
6676
  "use strict";
6702
6677
  diagMcp = async (_ctx) => {
6703
6678
  const home = os4.homedir();
6704
- const cacheDir = path22.join(home, ".cache", "ms-playwright");
6679
+ const cacheDir = path23.join(home, ".cache", "ms-playwright");
6705
6680
  let entries = [];
6706
6681
  try {
6707
- entries = fs25.readdirSync(cacheDir);
6682
+ entries = fs26.readdirSync(cacheDir);
6708
6683
  } catch {
6709
6684
  }
6710
6685
  const hasChromium = entries.some((e) => e.startsWith("chromium"));
@@ -6732,13 +6707,13 @@ var init_diagMcp = __esm({
6732
6707
  });
6733
6708
 
6734
6709
  // src/scripts/frameworkDetectors.ts
6735
- import * as fs26 from "fs";
6736
- import * as path23 from "path";
6710
+ import * as fs27 from "fs";
6711
+ import * as path24 from "path";
6737
6712
  function detectFrameworks(cwd) {
6738
6713
  const out = [];
6739
6714
  let deps = {};
6740
6715
  try {
6741
- const pkg = JSON.parse(fs26.readFileSync(path23.join(cwd, "package.json"), "utf-8"));
6716
+ const pkg = JSON.parse(fs27.readFileSync(path24.join(cwd, "package.json"), "utf-8"));
6742
6717
  deps = { ...pkg.dependencies, ...pkg.devDependencies };
6743
6718
  } catch {
6744
6719
  return out;
@@ -6775,25 +6750,25 @@ function detectFrameworks(cwd) {
6775
6750
  }
6776
6751
  function findFile(cwd, candidates) {
6777
6752
  for (const c of candidates) {
6778
- if (fs26.existsSync(path23.join(cwd, c))) return c;
6753
+ if (fs27.existsSync(path24.join(cwd, c))) return c;
6779
6754
  }
6780
6755
  return null;
6781
6756
  }
6782
6757
  function discoverPayloadCollections(cwd) {
6783
6758
  const out = [];
6784
6759
  for (const dir of COLLECTION_DIRS) {
6785
- const full = path23.join(cwd, dir);
6786
- if (!fs26.existsSync(full)) continue;
6760
+ const full = path24.join(cwd, dir);
6761
+ if (!fs27.existsSync(full)) continue;
6787
6762
  let files;
6788
6763
  try {
6789
- files = fs26.readdirSync(full).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
6764
+ files = fs27.readdirSync(full).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
6790
6765
  } catch {
6791
6766
  continue;
6792
6767
  }
6793
6768
  for (const file of files) {
6794
6769
  try {
6795
- const filePath = path23.join(full, file);
6796
- const content = fs26.readFileSync(filePath, "utf-8").slice(0, 1e4);
6770
+ const filePath = path24.join(full, file);
6771
+ const content = fs27.readFileSync(filePath, "utf-8").slice(0, 1e4);
6797
6772
  const slugMatch = content.match(/slug:\s*['"]([a-z0-9-]+)['"]/);
6798
6773
  if (!slugMatch) continue;
6799
6774
  const slug = slugMatch[1];
@@ -6807,7 +6782,7 @@ function discoverPayloadCollections(cwd) {
6807
6782
  out.push({
6808
6783
  name,
6809
6784
  slug,
6810
- filePath: path23.relative(cwd, filePath),
6785
+ filePath: path24.relative(cwd, filePath),
6811
6786
  fields: fields.slice(0, 20),
6812
6787
  hasAdmin
6813
6788
  });
@@ -6820,28 +6795,28 @@ function discoverPayloadCollections(cwd) {
6820
6795
  function discoverAdminComponents(cwd, collections) {
6821
6796
  const out = [];
6822
6797
  for (const dir of ADMIN_COMPONENT_DIRS) {
6823
- const full = path23.join(cwd, dir);
6824
- if (!fs26.existsSync(full)) continue;
6798
+ const full = path24.join(cwd, dir);
6799
+ if (!fs27.existsSync(full)) continue;
6825
6800
  let entries;
6826
6801
  try {
6827
- entries = fs26.readdirSync(full, { withFileTypes: true });
6802
+ entries = fs27.readdirSync(full, { withFileTypes: true });
6828
6803
  } catch {
6829
6804
  continue;
6830
6805
  }
6831
6806
  for (const entry of entries) {
6832
- const entryPath = path23.join(full, entry.name);
6807
+ const entryPath = path24.join(full, entry.name);
6833
6808
  let name;
6834
6809
  let filePath;
6835
6810
  if (entry.isDirectory()) {
6836
6811
  const indexFile = ["index.tsx", "index.ts", "index.jsx", "index.js"].find(
6837
- (f) => fs26.existsSync(path23.join(entryPath, f))
6812
+ (f) => fs27.existsSync(path24.join(entryPath, f))
6838
6813
  );
6839
6814
  if (!indexFile) continue;
6840
6815
  name = entry.name;
6841
- filePath = path23.relative(cwd, path23.join(entryPath, indexFile));
6816
+ filePath = path24.relative(cwd, path24.join(entryPath, indexFile));
6842
6817
  } else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
6843
6818
  name = entry.name.replace(/\.(tsx?|jsx?)$/, "");
6844
- filePath = path23.relative(cwd, entryPath);
6819
+ filePath = path24.relative(cwd, entryPath);
6845
6820
  } else {
6846
6821
  continue;
6847
6822
  }
@@ -6849,7 +6824,7 @@ function discoverAdminComponents(cwd, collections) {
6849
6824
  if (collections) {
6850
6825
  for (const col of collections) {
6851
6826
  try {
6852
- const colContent = fs26.readFileSync(path23.join(cwd, col.filePath), "utf-8");
6827
+ const colContent = fs27.readFileSync(path24.join(cwd, col.filePath), "utf-8");
6853
6828
  if (colContent.includes(name)) {
6854
6829
  usedInCollection = col.slug;
6855
6830
  break;
@@ -6867,8 +6842,8 @@ function scanApiRoutes(cwd) {
6867
6842
  const out = [];
6868
6843
  const appDirs = ["src/app", "app"];
6869
6844
  for (const appDir of appDirs) {
6870
- const apiDir = path23.join(cwd, appDir, "api");
6871
- if (!fs26.existsSync(apiDir)) continue;
6845
+ const apiDir = path24.join(cwd, appDir, "api");
6846
+ if (!fs27.existsSync(apiDir)) continue;
6872
6847
  walkApiRoutes(apiDir, "/api", cwd, out);
6873
6848
  break;
6874
6849
  }
@@ -6877,14 +6852,14 @@ function scanApiRoutes(cwd) {
6877
6852
  function walkApiRoutes(dir, prefix, cwd, out) {
6878
6853
  let entries;
6879
6854
  try {
6880
- entries = fs26.readdirSync(dir, { withFileTypes: true });
6855
+ entries = fs27.readdirSync(dir, { withFileTypes: true });
6881
6856
  } catch {
6882
6857
  return;
6883
6858
  }
6884
6859
  const routeFile = entries.find((e) => e.isFile() && /^route\.(ts|js|tsx|jsx)$/.test(e.name));
6885
6860
  if (routeFile) {
6886
6861
  try {
6887
- const content = fs26.readFileSync(path23.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
6862
+ const content = fs27.readFileSync(path24.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
6888
6863
  const methods = HTTP_METHODS.filter(
6889
6864
  (m) => new RegExp(`export\\s+(?:async\\s+)?function\\s+${m}\\b`).test(content)
6890
6865
  );
@@ -6892,7 +6867,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
6892
6867
  out.push({
6893
6868
  path: prefix,
6894
6869
  methods,
6895
- filePath: path23.relative(cwd, path23.join(dir, routeFile.name))
6870
+ filePath: path24.relative(cwd, path24.join(dir, routeFile.name))
6896
6871
  });
6897
6872
  }
6898
6873
  } catch {
@@ -6903,7 +6878,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
6903
6878
  if (entry.name === "node_modules" || entry.name === ".next") continue;
6904
6879
  let segment = entry.name;
6905
6880
  if (segment.startsWith("(") && segment.endsWith(")")) {
6906
- walkApiRoutes(path23.join(dir, entry.name), prefix, cwd, out);
6881
+ walkApiRoutes(path24.join(dir, entry.name), prefix, cwd, out);
6907
6882
  continue;
6908
6883
  }
6909
6884
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -6911,16 +6886,16 @@ function walkApiRoutes(dir, prefix, cwd, out) {
6911
6886
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
6912
6887
  segment = `:${segment.slice(1, -1)}`;
6913
6888
  }
6914
- walkApiRoutes(path23.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
6889
+ walkApiRoutes(path24.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
6915
6890
  }
6916
6891
  }
6917
6892
  function scanEnvVars(cwd) {
6918
6893
  const candidates = [".env.example", ".env.local.example", ".env.template"];
6919
6894
  for (const envFile of candidates) {
6920
- const envPath = path23.join(cwd, envFile);
6921
- if (!fs26.existsSync(envPath)) continue;
6895
+ const envPath = path24.join(cwd, envFile);
6896
+ if (!fs27.existsSync(envPath)) continue;
6922
6897
  try {
6923
- const content = fs26.readFileSync(envPath, "utf-8");
6898
+ const content = fs27.readFileSync(envPath, "utf-8");
6924
6899
  const vars = [];
6925
6900
  for (const line of content.split("\n")) {
6926
6901
  const trimmed = line.trim();
@@ -6965,8 +6940,8 @@ var init_frameworkDetectors = __esm({
6965
6940
  });
6966
6941
 
6967
6942
  // src/scripts/discoverQaContext.ts
6968
- import * as fs27 from "fs";
6969
- import * as path24 from "path";
6943
+ import * as fs28 from "fs";
6944
+ import * as path25 from "path";
6970
6945
  function runQaDiscovery(cwd) {
6971
6946
  const out = {
6972
6947
  routes: [],
@@ -6997,9 +6972,9 @@ function runQaDiscovery(cwd) {
6997
6972
  }
6998
6973
  function detectDevServer(cwd, out) {
6999
6974
  try {
7000
- const pkg = JSON.parse(fs27.readFileSync(path24.join(cwd, "package.json"), "utf-8"));
6975
+ const pkg = JSON.parse(fs28.readFileSync(path25.join(cwd, "package.json"), "utf-8"));
7001
6976
  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";
6977
+ 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
6978
  if (pkg.scripts?.dev) out.devCommand = `${pm} dev`;
7004
6979
  if (allDeps.next || allDeps.nuxt) out.devPort = 3e3;
7005
6980
  else if (allDeps.vite) out.devPort = 5173;
@@ -7009,8 +6984,8 @@ function detectDevServer(cwd, out) {
7009
6984
  function scanFrontendRoutes(cwd, out) {
7010
6985
  const appDirs = ["src/app", "app"];
7011
6986
  for (const appDir of appDirs) {
7012
- const full = path24.join(cwd, appDir);
7013
- if (!fs27.existsSync(full)) continue;
6987
+ const full = path25.join(cwd, appDir);
6988
+ if (!fs28.existsSync(full)) continue;
7014
6989
  walkFrontendRoutes(full, "", out);
7015
6990
  break;
7016
6991
  }
@@ -7018,7 +6993,7 @@ function scanFrontendRoutes(cwd, out) {
7018
6993
  function walkFrontendRoutes(dir, prefix, out) {
7019
6994
  let entries;
7020
6995
  try {
7021
- entries = fs27.readdirSync(dir, { withFileTypes: true });
6996
+ entries = fs28.readdirSync(dir, { withFileTypes: true });
7022
6997
  } catch {
7023
6998
  return;
7024
6999
  }
@@ -7035,7 +7010,7 @@ function walkFrontendRoutes(dir, prefix, out) {
7035
7010
  if (entry.name === "node_modules" || entry.name === ".next") continue;
7036
7011
  let segment = entry.name;
7037
7012
  if (segment.startsWith("(") && segment.endsWith(")")) {
7038
- walkFrontendRoutes(path24.join(dir, entry.name), prefix, out);
7013
+ walkFrontendRoutes(path25.join(dir, entry.name), prefix, out);
7039
7014
  continue;
7040
7015
  }
7041
7016
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -7043,7 +7018,7 @@ function walkFrontendRoutes(dir, prefix, out) {
7043
7018
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
7044
7019
  segment = `:${segment.slice(1, -1)}`;
7045
7020
  }
7046
- walkFrontendRoutes(path24.join(dir, entry.name), `${prefix}/${segment}`, out);
7021
+ walkFrontendRoutes(path25.join(dir, entry.name), `${prefix}/${segment}`, out);
7047
7022
  }
7048
7023
  }
7049
7024
  function detectAuthFiles(cwd, out) {
@@ -7060,23 +7035,23 @@ function detectAuthFiles(cwd, out) {
7060
7035
  "src/app/api/oauth"
7061
7036
  ];
7062
7037
  for (const c of candidates) {
7063
- if (fs27.existsSync(path24.join(cwd, c))) out.authFiles.push(c);
7038
+ if (fs28.existsSync(path25.join(cwd, c))) out.authFiles.push(c);
7064
7039
  }
7065
7040
  }
7066
7041
  function detectRoles(cwd, out) {
7067
7042
  const rolePaths = ["src/types", "src/lib", "src/utils", "src/constants", "src/access", "src/collections"];
7068
7043
  for (const rp of rolePaths) {
7069
- const dir = path24.join(cwd, rp);
7070
- if (!fs27.existsSync(dir)) continue;
7044
+ const dir = path25.join(cwd, rp);
7045
+ if (!fs28.existsSync(dir)) continue;
7071
7046
  let files;
7072
7047
  try {
7073
- files = fs27.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
7048
+ files = fs28.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
7074
7049
  } catch {
7075
7050
  continue;
7076
7051
  }
7077
7052
  for (const f of files) {
7078
7053
  try {
7079
- const content = fs27.readFileSync(path24.join(dir, f), "utf-8").slice(0, 5e3);
7054
+ const content = fs28.readFileSync(path25.join(dir, f), "utf-8").slice(0, 5e3);
7080
7055
  const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
7081
7056
  if (roleMatches) {
7082
7057
  for (const m of roleMatches) {
@@ -7369,6 +7344,23 @@ async function runJob(job, base) {
7369
7344
  if (executableIdentity !== void 0 && executableIdentity.length > 0)
7370
7345
  preloadedData.jobExecutable = executableIdentity;
7371
7346
  if (valid.schedule !== void 0 && valid.schedule.length > 0) preloadedData.jobSchedule = valid.schedule;
7347
+ const dutyContext = loadDutyContext(dutyIdentity ?? valid.duty);
7348
+ if (dutyContext) {
7349
+ preloadedData.dutySlug = dutyContext.slug;
7350
+ preloadedData.dutyTitle = dutyContext.title;
7351
+ preloadedData.dutyIntent = dutyContext.body;
7352
+ preloadedData.jobIntent = dutyContext.body;
7353
+ if (preloadedData.jobDuty === void 0) preloadedData.jobDuty = dutyContext.slug;
7354
+ if (dutyContext.config.staff && preloadedData.jobPersona === void 0) {
7355
+ preloadedData.jobPersona = dutyContext.config.staff;
7356
+ }
7357
+ if (dutyContext.config.every && preloadedData.jobSchedule === void 0) {
7358
+ preloadedData.jobSchedule = dutyContext.config.every;
7359
+ }
7360
+ if (dutyContext.config.mentions && dutyContext.config.mentions.length > 0) {
7361
+ preloadedData.mentions = dutyContext.config.mentions.map((login) => `@${login}`).join(" ");
7362
+ }
7363
+ }
7372
7364
  if (valid.why !== void 0 && valid.why.length > 0) preloadedData.jobWhy = valid.why;
7373
7365
  if (valid.persona !== void 0) preloadedData.jobPersona = valid.persona;
7374
7366
  const input = {
@@ -7384,6 +7376,10 @@ async function runJob(job, base) {
7384
7376
  const run = base.chain === false ? runExecutable : runExecutableChain;
7385
7377
  return run(profileName, input);
7386
7378
  }
7379
+ function loadDutyContext(slug) {
7380
+ if (!slug) return null;
7381
+ return readDutyFolder(getProjectDutiesRoot(), slug) ?? readDutyFolder(getBuiltinDutiesRoot(), slug);
7382
+ }
7387
7383
  function mintInstantJob(dispatch2, opts) {
7388
7384
  return {
7389
7385
  action: dispatch2.action,
@@ -7411,6 +7407,7 @@ var init_job = __esm({
7411
7407
  "src/job.ts"() {
7412
7408
  "use strict";
7413
7409
  init_executor();
7410
+ init_dutyFolders();
7414
7411
  init_registry();
7415
7412
  init_jobIdentity();
7416
7413
  init_jobIdentity();
@@ -7642,8 +7639,8 @@ var init_contentsApiBackend = __esm({
7642
7639
  });
7643
7640
 
7644
7641
  // src/scripts/jobState/localFileBackend.ts
7645
- import * as fs28 from "fs";
7646
- import * as path25 from "path";
7642
+ import * as fs29 from "fs";
7643
+ import * as path26 from "path";
7647
7644
  function sanitizeKey(s) {
7648
7645
  return s.replace(/[^A-Za-z0-9._-]/g, "-");
7649
7646
  }
@@ -7699,7 +7696,7 @@ var init_localFileBackend = __esm({
7699
7696
  if (!opts.owner || !opts.repo) throw new Error("LocalFileBackend: owner and repo are required");
7700
7697
  this.cwd = opts.cwd;
7701
7698
  this.jobsDir = opts.jobsDir;
7702
- this.absDir = path25.join(opts.cwd, opts.jobsDir);
7699
+ this.absDir = path26.join(opts.cwd, opts.jobsDir);
7703
7700
  this.owner = opts.owner;
7704
7701
  this.repo = opts.repo;
7705
7702
  this.cache = opts.cache ?? defaultCacheAdapter();
@@ -7714,7 +7711,7 @@ var init_localFileBackend = __esm({
7714
7711
  `);
7715
7712
  return;
7716
7713
  }
7717
- fs28.mkdirSync(this.absDir, { recursive: true });
7714
+ fs29.mkdirSync(this.absDir, { recursive: true });
7718
7715
  const prefix = this.cacheKeyPrefix();
7719
7716
  const probeKey = `${prefix}probe-${Date.now()}`;
7720
7717
  try {
@@ -7743,7 +7740,7 @@ var init_localFileBackend = __esm({
7743
7740
  `);
7744
7741
  return;
7745
7742
  }
7746
- if (!fs28.existsSync(this.absDir)) {
7743
+ if (!fs29.existsSync(this.absDir)) {
7747
7744
  return;
7748
7745
  }
7749
7746
  const key = `${this.cacheKeyPrefix()}${process.env.GITHUB_RUN_ID ?? "norunid"}-${Date.now()}`;
@@ -7759,11 +7756,11 @@ var init_localFileBackend = __esm({
7759
7756
  }
7760
7757
  load(slug) {
7761
7758
  const relPath = stateFilePath(this.jobsDir, slug);
7762
- const absPath = path25.join(this.cwd, relPath);
7763
- if (!fs28.existsSync(absPath)) {
7759
+ const absPath = path26.join(this.cwd, relPath);
7760
+ if (!fs29.existsSync(absPath)) {
7764
7761
  return { path: relPath, handle: null, state: initialStateEnvelope("seed"), created: true };
7765
7762
  }
7766
- const raw = fs28.readFileSync(absPath, "utf-8");
7763
+ const raw = fs29.readFileSync(absPath, "utf-8");
7767
7764
  let parsed;
7768
7765
  try {
7769
7766
  parsed = JSON.parse(raw);
@@ -7780,13 +7777,13 @@ var init_localFileBackend = __esm({
7780
7777
  if (!loaded.created && isStateUnchanged(loaded.state, next)) {
7781
7778
  return false;
7782
7779
  }
7783
- const absPath = path25.join(this.cwd, loaded.path);
7784
- fs28.mkdirSync(path25.dirname(absPath), { recursive: true });
7780
+ const absPath = path26.join(this.cwd, loaded.path);
7781
+ fs29.mkdirSync(path26.dirname(absPath), { recursive: true });
7785
7782
  const body = `${JSON.stringify(next, null, 2)}
7786
7783
  `;
7787
7784
  const tmpPath = `${absPath}.${process.pid}.tmp`;
7788
- fs28.writeFileSync(tmpPath, body, "utf-8");
7789
- fs28.renameSync(tmpPath, absPath);
7785
+ fs29.writeFileSync(tmpPath, body, "utf-8");
7786
+ fs29.renameSync(tmpPath, absPath);
7790
7787
  return true;
7791
7788
  }
7792
7789
  cacheKeyPrefix() {
@@ -7938,8 +7935,7 @@ var init_planTaskJobs = __esm({
7938
7935
  });
7939
7936
 
7940
7937
  // src/scripts/dispatchDutyFileTicks.ts
7941
- import * as fs29 from "fs";
7942
- import * as path26 from "path";
7938
+ import * as path27 from "path";
7943
7939
  async function decideShouldFire(every, slug, backend, now) {
7944
7940
  if (!every) return { skip: false, reason: "no schedule (every cron tick)" };
7945
7941
  if (every === "manual") {
@@ -7980,14 +7976,6 @@ function formatAgo(ms) {
7980
7976
  const day = Math.round(hr / 24);
7981
7977
  return `${day}d`;
7982
7978
  }
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
7979
  function parseDutyFilter(raw) {
7992
7980
  return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : void 0;
7993
7981
  }
@@ -7996,21 +7984,21 @@ function filterSlugs(slugs, onlyDuty) {
7996
7984
  }
7997
7985
  function createDutyTaskIssue(opts) {
7998
7986
  const title = `Duty ${opts.slug} - multi-executable task`;
7999
- const body = buildDutyTaskIssueBody(opts.slug, opts.body, opts.frontmatter);
7987
+ const body = buildDutyTaskIssueBody(opts.slug, opts.body, opts.config);
8000
7988
  const out = gh(["issue", "create", "--title", title, "--body-file", "-"], { input: body, cwd: opts.cwd });
8001
7989
  const url = out.split("\n").map((line) => line.trim()).filter(Boolean).pop() ?? "";
8002
7990
  const match = url.match(/\/issues\/(\d+)\b/);
8003
7991
  if (!match) throw new Error(`gh issue create returned unexpected output: ${out}`);
8004
7992
  return { number: Number(match[1]), url };
8005
7993
  }
8006
- function buildDutyTaskIssueBody(slug, dutyBody, frontmatter) {
8007
- const specs = (frontmatter.executables ?? []).map((executable) => ({
7994
+ function buildDutyTaskIssueBody(slug, dutyBody, config) {
7995
+ const specs = (config.executables ?? []).map((executable) => ({
8008
7996
  executable,
8009
7997
  duty: slug,
8010
- ...frontmatter.staff ? { staff: frontmatter.staff } : {},
7998
+ ...config.staff ? { staff: config.staff } : {},
8011
7999
  reason: `Duty \`${slug}\` slice for \`${executable}\`.`,
8012
8000
  flavor: "scheduled",
8013
- ...frontmatter.every ? { schedule: frontmatter.every } : {}
8001
+ ...config.every ? { schedule: config.every } : {}
8014
8002
  }));
8015
8003
  return [
8016
8004
  `# Duty task: ${slug}`,
@@ -8023,26 +8011,6 @@ function buildDutyTaskIssueBody(slug, dutyBody, frontmatter) {
8023
8011
  ""
8024
8012
  ].join("\n");
8025
8013
  }
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
8014
  async function stampFired(backend, slug, now, task) {
8047
8015
  try {
8048
8016
  const loaded = await backend.load(slug);
@@ -8061,10 +8029,10 @@ var dispatchDutyFileTicks;
8061
8029
  var init_dispatchDutyFileTicks = __esm({
8062
8030
  "src/scripts/dispatchDutyFileTicks.ts"() {
8063
8031
  "use strict";
8032
+ init_dutyFolders();
8064
8033
  init_issue();
8065
8034
  init_job();
8066
- init_profile();
8067
- init_jobFrontmatter();
8035
+ init_scheduleEvery();
8068
8036
  init_jobState();
8069
8037
  init_planTaskJobs();
8070
8038
  dispatchDutyFileTicks = async (ctx, _profile, args) => {
@@ -8082,107 +8050,54 @@ var init_dispatchDutyFileTicks = __esm({
8082
8050
  }
8083
8051
  try {
8084
8052
  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) {
8053
+ const jobsPath = path27.join(ctx.cwd, jobsDir);
8054
+ const slugs = filterSlugs(listDutyFolderSlugs(jobsPath), onlyDuty);
8055
+ ctx.data.jobSlugCount = slugs.length;
8056
+ if (slugs.length === 0) {
8090
8057
  const filter = onlyDuty ? ` matching ${onlyDuty}` : "";
8091
- process.stdout.write(`[jobs] no job files${filter} in ${jobsDir}
8058
+ process.stdout.write(`[jobs] no duty folders${filter} in ${jobsDir}
8092
8059
  `);
8093
8060
  return;
8094
8061
  }
8095
8062
  const filtered = onlyDuty ? ` matching ${onlyDuty}` : "";
8096
- process.stdout.write(
8097
- `[jobs] ticking ${slugs.length + folderSlugList.length} job(s)${filtered} via ${targetExecutable}
8098
- `
8099
- );
8063
+ process.stdout.write(`[jobs] ticking ${slugs.length} dut(y/ies)${filtered} via ${targetExecutable}
8064
+ `);
8100
8065
  const results = [];
8101
8066
  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
8067
  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)
8068
+ const duty = readDutyFolder(jobsPath, slug);
8069
+ if (!duty) {
8070
+ process.stderr.write(`[jobs] \u23ED skip ${slug}: duty folder is missing profile.json or duty.md
8155
8071
  `);
8156
- results.push({ slug, exitCode: 0, skipped: true, reason: "handled as folder-duty" });
8072
+ results.push({ slug, exitCode: 0, skipped: true, reason: "incomplete duty folder" });
8157
8073
  continue;
8158
8074
  }
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
8075
+ const config = duty.config;
8076
+ if (config.disabled === true) {
8077
+ process.stdout.write(`[jobs] \u23ED skip ${slug}: disabled in profile.json
8163
8078
  `);
8164
8079
  results.push({ slug, exitCode: 0, skipped: true, reason: "disabled" });
8165
8080
  continue;
8166
8081
  }
8167
- if (!frontmatter.staff || frontmatter.staff.trim().length === 0) {
8168
- process.stderr.write(`[jobs] \u23ED skip ${slug}: no staff assigned (add 'staff: <slug>' frontmatter)
8082
+ if (!config.staff || config.staff.trim().length === 0) {
8083
+ process.stderr.write(`[jobs] \u23ED skip ${slug}: no staff assigned (add "staff" to profile.json)
8169
8084
  `);
8170
8085
  results.push({ slug, exitCode: 0, skipped: true, reason: "no staff assigned" });
8171
8086
  continue;
8172
8087
  }
8173
- const decision = await decideShouldFire(frontmatter.every, slug, backend, now);
8088
+ const decision = await decideShouldFire(config.every, slug, backend, now);
8174
8089
  if (decision.skip) {
8175
8090
  process.stdout.write(`[jobs] \u23ED skip ${slug}: ${decision.reason}
8176
8091
  `);
8177
8092
  results.push({ slug, exitCode: 0, skipped: true, reason: decision.reason });
8178
8093
  continue;
8179
8094
  }
8180
- if (frontmatter.executables && frontmatter.executables.length > 0) {
8095
+ if (config.executables && config.executables.length > 0) {
8181
8096
  try {
8182
8097
  const task = createDutyTaskIssue({
8183
8098
  slug,
8184
- body: dutyFile.body,
8185
- frontmatter,
8099
+ body: duty.body,
8100
+ config,
8186
8101
  cwd: ctx.cwd
8187
8102
  });
8188
8103
  await stampFired(backend, slug, now, task);
@@ -8192,8 +8107,8 @@ var init_dispatchDutyFileTicks = __esm({
8192
8107
  mintScheduledJob({
8193
8108
  duty: slug,
8194
8109
  executable: "task-jobs",
8195
- schedule: frontmatter.every,
8196
- persona: frontmatter.staff,
8110
+ schedule: config.every,
8111
+ persona: config.staff,
8197
8112
  cliArgs: { issue: task.number }
8198
8113
  }),
8199
8114
  { cwd: ctx.cwd, config: ctx.config, verbose: ctx.verbose, quiet: ctx.quiet }
@@ -8211,7 +8126,8 @@ var init_dispatchDutyFileTicks = __esm({
8211
8126
  }
8212
8127
  continue;
8213
8128
  }
8214
- const slugTarget = frontmatter.tickScript ? scriptedExecutable : targetExecutable;
8129
+ const slugTarget = config.tickScript ? scriptedExecutable : config.executable ?? targetExecutable;
8130
+ const cliArgs = config.executable ? {} : { [slugArg]: slug };
8215
8131
  process.stdout.write(`[jobs] \u2192 tick ${slug} (${slugTarget})
8216
8132
  `);
8217
8133
  try {
@@ -8219,8 +8135,9 @@ var init_dispatchDutyFileTicks = __esm({
8219
8135
  mintScheduledJob({
8220
8136
  duty: slug,
8221
8137
  executable: slugTarget,
8222
- schedule: frontmatter.every,
8223
- cliArgs: { [slugArg]: slug }
8138
+ schedule: config.every,
8139
+ persona: config.staff,
8140
+ cliArgs
8224
8141
  }),
8225
8142
  { cwd: ctx.cwd, config: ctx.config, verbose: ctx.verbose, quiet: ctx.quiet, chain: false }
8226
8143
  );
@@ -9463,11 +9380,11 @@ var init_handleAbandonedGoal = __esm({
9463
9380
  // src/scripts/initFlow.ts
9464
9381
  import { execFileSync as execFileSync16 } from "child_process";
9465
9382
  import * as fs30 from "fs";
9466
- import * as path27 from "path";
9383
+ import * as path28 from "path";
9467
9384
  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";
9385
+ if (fs30.existsSync(path28.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
9386
+ if (fs30.existsSync(path28.join(cwd, "yarn.lock"))) return "yarn";
9387
+ if (fs30.existsSync(path28.join(cwd, "bun.lockb"))) return "bun";
9471
9388
  return "npm";
9472
9389
  }
9473
9390
  function qualityCommandsFor(pm) {
@@ -9539,7 +9456,7 @@ function performInit(cwd, force) {
9539
9456
  const pm = detectPackageManager(cwd);
9540
9457
  const ownerRepo = detectOwnerRepo(cwd);
9541
9458
  const defaultBranch2 = defaultBranchFromGit(cwd);
9542
- const configPath = path27.join(cwd, "kody.config.json");
9459
+ const configPath = path28.join(cwd, "kody.config.json");
9543
9460
  if (fs30.existsSync(configPath) && !force) {
9544
9461
  skipped.push("kody.config.json");
9545
9462
  } else {
@@ -9548,8 +9465,8 @@ function performInit(cwd, force) {
9548
9465
  `);
9549
9466
  wrote.push("kody.config.json");
9550
9467
  }
9551
- const workflowDir = path27.join(cwd, ".github", "workflows");
9552
- const workflowPath = path27.join(workflowDir, "kody.yml");
9468
+ const workflowDir = path28.join(cwd, ".github", "workflows");
9469
+ const workflowPath = path28.join(workflowDir, "kody.yml");
9553
9470
  if (fs30.existsSync(workflowPath) && !force) {
9554
9471
  skipped.push(".github/workflows/kody.yml");
9555
9472
  } else {
@@ -9559,37 +9476,26 @@ function performInit(cwd, force) {
9559
9476
  }
9560
9477
  const builtinJobs = listBuiltinJobs();
9561
9478
  if (builtinJobs.length > 0) {
9562
- const jobsDir = path27.join(cwd, ".kody", "duties");
9479
+ const jobsDir = path28.join(cwd, ".kody", "duties");
9563
9480
  fs30.mkdirSync(jobsDir, { recursive: true });
9564
9481
  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) {
9482
+ const targetDir = path28.join(jobsDir, job.slug);
9483
+ const relProfile = path28.join(".kody", "duties", job.slug, "profile.json");
9484
+ const relBody = path28.join(".kody", "duties", job.slug, "duty.md");
9485
+ if (fs30.existsSync(targetDir) && fs30.existsSync(path28.join(targetDir, "profile.json")) && !force) {
9580
9486
  skipped.push(relProfile);
9581
- skipped.push(relPrompt);
9487
+ skipped.push(relBody);
9582
9488
  continue;
9583
9489
  }
9584
9490
  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"));
9491
+ fs30.writeFileSync(path28.join(targetDir, "profile.json"), fs30.readFileSync(job.profilePath, "utf-8"));
9492
+ fs30.writeFileSync(path28.join(targetDir, "duty.md"), fs30.readFileSync(job.bodyPath, "utf-8"));
9587
9493
  wrote.push(relProfile);
9588
- wrote.push(relPrompt);
9494
+ wrote.push(relBody);
9589
9495
  }
9590
9496
  }
9591
- const staffDir = path27.join(cwd, ".kody", "staff");
9592
- const staffPath = path27.join(staffDir, "kody.md");
9497
+ const staffDir = path28.join(cwd, ".kody", "staff");
9498
+ const staffPath = path28.join(staffDir, "kody.md");
9593
9499
  if (fs30.existsSync(staffPath) && !force) {
9594
9500
  skipped.push(".kody/staff/kody.md");
9595
9501
  } else {
@@ -9605,7 +9511,7 @@ function performInit(cwd, force) {
9605
9511
  continue;
9606
9512
  }
9607
9513
  if (profile.kind !== "scheduled" || !profile.schedule) continue;
9608
- const target = path27.join(workflowDir, `kody-${exe.name}.yml`);
9514
+ const target = path28.join(workflowDir, `kody-${exe.name}.yml`);
9609
9515
  if (fs30.existsSync(target) && !force) {
9610
9516
  skipped.push(`.github/workflows/kody-${exe.name}.yml`);
9611
9517
  continue;
@@ -9931,7 +9837,7 @@ var init_loadIssueStateComment = __esm({
9931
9837
 
9932
9838
  // src/scripts/loadJobFromFile.ts
9933
9839
  import * as fs31 from "fs";
9934
- import * as path28 from "path";
9840
+ import * as path29 from "path";
9935
9841
  function parseJobFile(raw, slug) {
9936
9842
  let stripped = raw;
9937
9843
  if (stripped.startsWith("---\n")) {
@@ -9947,17 +9853,17 @@ function parseJobFile(raw, slug) {
9947
9853
  const rest = trimmed.slice(firstLine2.length).replace(/^\n+/, "");
9948
9854
  return { title: h1[1].trim(), body: rest };
9949
9855
  }
9950
- return { title: humanizeSlug(slug), body: trimmed };
9856
+ return { title: humanizeSlug2(slug), body: trimmed };
9951
9857
  }
9952
- function humanizeSlug(slug) {
9858
+ function humanizeSlug2(slug) {
9953
9859
  return slug.split(/[-_]+/).filter((s) => s.length > 0).map((s) => s[0].toUpperCase() + s.slice(1)).join(" ");
9954
9860
  }
9955
9861
  var DUTY_TOOL_PALETTE2, loadJobFromFile;
9956
9862
  var init_loadJobFromFile = __esm({
9957
9863
  "src/scripts/loadJobFromFile.ts"() {
9958
9864
  "use strict";
9865
+ init_dutyFolders();
9959
9866
  init_dutyMcp();
9960
- init_jobFrontmatter();
9961
9867
  init_jobState();
9962
9868
  DUTY_TOOL_PALETTE2 = new Set(DUTY_MCP_TOOL_NAMES);
9963
9869
  loadJobFromFile = async (ctx, profile, args) => {
@@ -9968,19 +9874,17 @@ var init_loadJobFromFile = __esm({
9968
9874
  if (!slug) {
9969
9875
  throw new Error(`loadJobFromFile: ctx.args.${slugArg} must be a non-empty slug`);
9970
9876
  }
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}`);
9877
+ const duty = readDutyFolder(path29.join(ctx.cwd, jobsDir), slug);
9878
+ if (!duty) {
9879
+ throw new Error(`loadJobFromFile: duty folder not found or incomplete: ${path29.join(ctx.cwd, jobsDir, slug)}`);
9974
9880
  }
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();
9881
+ const { title, body, config } = duty;
9882
+ const mentions = (config.mentions ?? []).map((login) => `@${login}`).join(" ");
9883
+ const workerSlug = (config.staff ?? "").trim();
9980
9884
  let workerTitle = "";
9981
9885
  let workerPersona = "";
9982
9886
  if (workerSlug) {
9983
- const workerPath = path28.join(ctx.cwd, workersDir, `${workerSlug}.md`);
9887
+ const workerPath = path29.join(ctx.cwd, workersDir, `${workerSlug}.md`);
9984
9888
  if (!fs31.existsSync(workerPath)) {
9985
9889
  throw new Error(`loadJobFromFile: duty '${slug}' declares staff '${workerSlug}' but ${workerPath} does not exist`);
9986
9890
  }
@@ -10005,8 +9909,8 @@ var init_loadJobFromFile = __esm({
10005
9909
  ctx.data.staffSlug = workerSlug;
10006
9910
  ctx.data.staffTitle = workerTitle;
10007
9911
  ctx.data.executableSlug = profile.name;
10008
- ctx.data.dutySchedule = "";
10009
- const declaredTools = frontmatter.tools ?? [];
9912
+ ctx.data.dutySchedule = config.every ?? "";
9913
+ const declaredTools = config.tools ?? [];
10010
9914
  if (declaredTools.length > 0) {
10011
9915
  const unknown = declaredTools.filter((name) => !DUTY_TOOL_PALETTE2.has(name));
10012
9916
  if (unknown.length > 0) {
@@ -10059,9 +9963,9 @@ ${truncate2(issue.body, FINDING_BODY_MAX_BYTES)}`;
10059
9963
 
10060
9964
  // src/scripts/kodyVariables.ts
10061
9965
  import * as fs32 from "fs";
10062
- import * as path29 from "path";
9966
+ import * as path30 from "path";
10063
9967
  function readKodyVariables(cwd) {
10064
- const full = path29.join(cwd, KODY_VARIABLES_REL_PATH);
9968
+ const full = path30.join(cwd, KODY_VARIABLES_REL_PATH);
10065
9969
  let raw;
10066
9970
  try {
10067
9971
  raw = fs32.readFileSync(full, "utf-8");
@@ -10090,7 +9994,7 @@ var init_kodyVariables = __esm({
10090
9994
 
10091
9995
  // src/scripts/loadQaContext.ts
10092
9996
  import * as fs33 from "fs";
10093
- import * as path30 from "path";
9997
+ import * as path31 from "path";
10094
9998
  function parseSlugList(value) {
10095
9999
  const inner = value.startsWith("[") && value.endsWith("]") ? value.slice(1, -1) : value;
10096
10000
  return inner.split(",").map(
@@ -10098,7 +10002,7 @@ function parseSlugList(value) {
10098
10002
  ).filter(Boolean);
10099
10003
  }
10100
10004
  function readProfileStaff(raw) {
10101
- const m = FRONTMATTER_RE2.exec(raw);
10005
+ const m = FRONTMATTER_RE.exec(raw);
10102
10006
  if (!m) return { staff: ["kody"], body: raw };
10103
10007
  const body = raw.slice(m[0].length);
10104
10008
  let staff = null;
@@ -10119,7 +10023,7 @@ function readProfileStaff(raw) {
10119
10023
  return { staff: staff ?? legacy ?? ["kody"], body };
10120
10024
  }
10121
10025
  function readProfile(cwd) {
10122
- const dir = path30.join(cwd, CONTEXT_DIR_REL_PATH);
10026
+ const dir = path31.join(cwd, CONTEXT_DIR_REL_PATH);
10123
10027
  if (!fs33.existsSync(dir)) return "";
10124
10028
  let entries;
10125
10029
  try {
@@ -10130,7 +10034,7 @@ function readProfile(cwd) {
10130
10034
  const blocks = [];
10131
10035
  for (const file of entries) {
10132
10036
  try {
10133
- const raw = fs33.readFileSync(path30.join(dir, file), "utf-8");
10037
+ const raw = fs33.readFileSync(path31.join(dir, file), "utf-8");
10134
10038
  const { staff, body } = readProfileStaff(raw);
10135
10039
  if (!staff.includes(QA_STAFF) && !staff.includes(ALL_STAFF)) continue;
10136
10040
  blocks.push(`## ${file}
@@ -10153,7 +10057,7 @@ function composeAuthBlock(authProfile, login, password) {
10153
10057
  }
10154
10058
  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
10059
  }
10156
- var CONTEXT_DIR_REL_PATH, QA_STAFF, ALL_STAFF, LEGACY_AUDIENCE_TO_STAFF, FRONTMATTER_RE2, loadQaContext;
10060
+ var CONTEXT_DIR_REL_PATH, QA_STAFF, ALL_STAFF, LEGACY_AUDIENCE_TO_STAFF, FRONTMATTER_RE, loadQaContext;
10157
10061
  var init_loadQaContext = __esm({
10158
10062
  "src/scripts/loadQaContext.ts"() {
10159
10063
  "use strict";
@@ -10162,7 +10066,7 @@ var init_loadQaContext = __esm({
10162
10066
  QA_STAFF = "qa-engineer";
10163
10067
  ALL_STAFF = "*";
10164
10068
  LEGACY_AUDIENCE_TO_STAFF = { chat: "kody", qa: QA_STAFF };
10165
- FRONTMATTER_RE2 = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
10069
+ FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
10166
10070
  loadQaContext = async (ctx) => {
10167
10071
  const vars = readKodyVariables(ctx.cwd);
10168
10072
  const login = vars.LOGIN_USER ?? "";
@@ -10177,7 +10081,7 @@ var init_loadQaContext = __esm({
10177
10081
 
10178
10082
  // src/taskContext.ts
10179
10083
  import * as fs34 from "fs";
10180
- import * as path31 from "path";
10084
+ import * as path32 from "path";
10181
10085
  function buildTaskContext(args) {
10182
10086
  return {
10183
10087
  schemaVersion: TASK_CONTEXT_SCHEMA_VERSION,
@@ -10192,9 +10096,9 @@ function buildTaskContext(args) {
10192
10096
  }
10193
10097
  function persistTaskContext(cwd, ctx) {
10194
10098
  try {
10195
- const dir = path31.join(cwd, ".kody", "runs", ctx.runId);
10099
+ const dir = path32.join(cwd, ".kody", "runs", ctx.runId);
10196
10100
  fs34.mkdirSync(dir, { recursive: true });
10197
- const file = path31.join(dir, "task-context.json");
10101
+ const file = path32.join(dir, "task-context.json");
10198
10102
  fs34.writeFileSync(file, `${JSON.stringify(ctx, null, 2)}
10199
10103
  `);
10200
10104
  return file;
@@ -10284,7 +10188,7 @@ var init_loadTaskState = __esm({
10284
10188
 
10285
10189
  // src/scripts/loadWorkerAdhoc.ts
10286
10190
  import * as fs35 from "fs";
10287
- import * as path32 from "path";
10191
+ import * as path33 from "path";
10288
10192
  function resolveMessage(messageArg) {
10289
10193
  const fromComment = readCommentBody();
10290
10194
  if (fromComment) return stripDirective(fromComment);
@@ -10318,7 +10222,7 @@ function stripDirective(body) {
10318
10222
  return lines.slice(start).join("\n").trim();
10319
10223
  }
10320
10224
  function parsePersona(raw, slug) {
10321
- const stripped = splitFrontmatter(raw).body;
10225
+ const stripped = stripLeadingFrontmatter(raw);
10322
10226
  const trimmed = stripped.trim();
10323
10227
  const firstLine2 = trimmed.split("\n", 1)[0] ?? "";
10324
10228
  const h1 = /^#\s+(.+?)\s*$/.exec(firstLine2);
@@ -10326,23 +10230,26 @@ function parsePersona(raw, slug) {
10326
10230
  const rest = trimmed.slice(firstLine2.length).replace(/^\n+/, "");
10327
10231
  return { title: h1[1].trim(), body: rest };
10328
10232
  }
10329
- return { title: humanizeSlug2(slug), body: trimmed };
10233
+ return { title: humanizeSlug3(slug), body: trimmed };
10330
10234
  }
10331
- function humanizeSlug2(slug) {
10235
+ function stripLeadingFrontmatter(raw) {
10236
+ const match = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/.exec(raw);
10237
+ return match ? raw.slice(match[0].length) : raw;
10238
+ }
10239
+ function humanizeSlug3(slug) {
10332
10240
  return slug.split(/[-_]+/).filter((s) => s.length > 0).map((s) => s[0].toUpperCase() + s.slice(1)).join(" ");
10333
10241
  }
10334
10242
  var loadWorkerAdhoc;
10335
10243
  var init_loadWorkerAdhoc = __esm({
10336
10244
  "src/scripts/loadWorkerAdhoc.ts"() {
10337
10245
  "use strict";
10338
- init_jobFrontmatter();
10339
10246
  loadWorkerAdhoc = async (ctx, _profile, args) => {
10340
10247
  const workersDir = String(args?.workersDir ?? ".kody/staff");
10341
10248
  const workerSlug = String(ctx.args.worker ?? "").trim();
10342
10249
  if (!workerSlug) {
10343
10250
  throw new Error("loadWorkerAdhoc: ctx.args.worker must be a non-empty slug");
10344
10251
  }
10345
- const workerPath = path32.join(ctx.cwd, workersDir, `${workerSlug}.md`);
10252
+ const workerPath = path33.join(ctx.cwd, workersDir, `${workerSlug}.md`);
10346
10253
  if (!fs35.existsSync(workerPath)) {
10347
10254
  throw new Error(`loadWorkerAdhoc: worker persona not found: ${workerPath}`);
10348
10255
  }
@@ -12217,12 +12124,12 @@ fi
12217
12124
 
12218
12125
  // src/scripts/runPreviewBuild.ts
12219
12126
  import { copyFile, writeFile } from "fs/promises";
12220
- import * as path33 from "path";
12127
+ import * as path34 from "path";
12221
12128
  import { fileURLToPath } from "url";
12222
12129
  function bundledDockerfilePath(mode) {
12223
- const here = path33.dirname(fileURLToPath(import.meta.url));
12130
+ const here = path34.dirname(fileURLToPath(import.meta.url));
12224
12131
  const file = mode === "dev" ? "default-Dockerfile.preview.dev" : "default-Dockerfile.preview.prod";
12225
- return path33.join(here, "preview-build-templates", file);
12132
+ return path34.join(here, "preview-build-templates", file);
12226
12133
  }
12227
12134
  function required(name) {
12228
12135
  const v = (process.env[name] ?? "").trim();
@@ -12481,10 +12388,10 @@ var init_runPreviewBuild = __esm({
12481
12388
  console.log(`[preview-build] vault: ${Object.keys(buildEnv).length} secrets, mode=${buildMode}`);
12482
12389
  if (Object.keys(buildEnv).length > 0) {
12483
12390
  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")}
12391
+ await writeFile(path34.join(ctx.cwd, ".env.production.local"), `${lines.join("\n")}
12485
12392
  `, "utf8");
12486
12393
  }
12487
- const consumerDockerfile = path33.join(ctx.cwd, "Dockerfile.preview");
12394
+ const consumerDockerfile = path34.join(ctx.cwd, "Dockerfile.preview");
12488
12395
  const { stat } = await import("fs/promises");
12489
12396
  let hasConsumerDockerfile = false;
12490
12397
  try {
@@ -12587,7 +12494,7 @@ var init_runPreviewBuild = __esm({
12587
12494
  // src/scripts/runTickScript.ts
12588
12495
  import { spawnSync as spawnSync2 } from "child_process";
12589
12496
  import * as fs36 from "fs";
12590
- import * as path34 from "path";
12497
+ import * as path35 from "path";
12591
12498
  function buildChildEnv(parent, force) {
12592
12499
  const allow = /* @__PURE__ */ new Set([
12593
12500
  "PATH",
@@ -12635,7 +12542,7 @@ var runTickScript;
12635
12542
  var init_runTickScript = __esm({
12636
12543
  "src/scripts/runTickScript.ts"() {
12637
12544
  "use strict";
12638
- init_jobFrontmatter();
12545
+ init_dutyFolders();
12639
12546
  init_jobState();
12640
12547
  init_parseJobStateFromAgentResult();
12641
12548
  runTickScript = async (ctx, _profile, args) => {
@@ -12649,21 +12556,19 @@ var init_runTickScript = __esm({
12649
12556
  ctx.output.reason = `runTickScript: ctx.args.${slugArg} must be a non-empty slug`;
12650
12557
  return;
12651
12558
  }
12652
- const jobPath = path34.join(ctx.cwd, jobsDir, `${slug}.md`);
12653
- if (!fs36.existsSync(jobPath)) {
12559
+ const duty = readDutyFolder(path35.join(ctx.cwd, jobsDir), slug);
12560
+ if (!duty) {
12654
12561
  ctx.output.exitCode = 99;
12655
- ctx.output.reason = `runTickScript: job file not found: ${jobPath}`;
12562
+ ctx.output.reason = `runTickScript: duty folder not found or incomplete: ${path35.join(ctx.cwd, jobsDir, slug)}`;
12656
12563
  return;
12657
12564
  }
12658
- const raw = fs36.readFileSync(jobPath, "utf-8");
12659
- const { frontmatter } = splitFrontmatter(raw);
12660
- const tickScript = frontmatter.tickScript;
12565
+ const tickScript = duty.config.tickScript;
12661
12566
  if (!tickScript) {
12662
12567
  ctx.output.exitCode = 99;
12663
- ctx.output.reason = `runTickScript: duty ${slug} has no \`tickScript:\` frontmatter \u2014 route via duty-tick instead`;
12568
+ ctx.output.reason = `runTickScript: duty ${slug} has no \`tickScript\` in profile.json \u2014 route via duty-tick instead`;
12664
12569
  return;
12665
12570
  }
12666
- const scriptPath = path34.isAbsolute(tickScript) ? tickScript : path34.join(ctx.cwd, tickScript);
12571
+ const scriptPath = path35.isAbsolute(tickScript) ? tickScript : path35.join(ctx.cwd, tickScript);
12667
12572
  if (!fs36.existsSync(scriptPath)) {
12668
12573
  ctx.output.exitCode = 99;
12669
12574
  ctx.output.reason = `runTickScript: tickScript not found: ${scriptPath}`;
@@ -13862,7 +13767,7 @@ var init_scripts = __esm({
13862
13767
 
13863
13768
  // src/staff.ts
13864
13769
  import * as fs38 from "fs";
13865
- import * as path35 from "path";
13770
+ import * as path36 from "path";
13866
13771
  function stripFrontmatter(raw) {
13867
13772
  const match = /^---\n[\s\S]*?\n---\n?([\s\S]*)$/.exec(raw);
13868
13773
  return (match ? match[1] : raw).trim();
@@ -13870,7 +13775,7 @@ function stripFrontmatter(raw) {
13870
13775
  function loadStaffPersona(cwd, slug, staffDir = DEFAULT_STAFF_DIR) {
13871
13776
  const trimmed = slug.trim();
13872
13777
  if (!trimmed) throw new Error("loadStaffPersona: empty staff slug");
13873
- const staffPath = path35.join(cwd, staffDir, `${trimmed}.md`);
13778
+ const staffPath = path36.join(cwd, staffDir, `${trimmed}.md`);
13874
13779
  if (fs38.existsSync(staffPath)) {
13875
13780
  const body = stripFrontmatter(fs38.readFileSync(staffPath, "utf-8"));
13876
13781
  if (body) return body;
@@ -13986,7 +13891,7 @@ var init_tools = __esm({
13986
13891
  // src/executor.ts
13987
13892
  import { spawn as spawn7 } from "child_process";
13988
13893
  import * as fs39 from "fs";
13989
- import * as path36 from "path";
13894
+ import * as path37 from "path";
13990
13895
  function isMutatingPostflight(scriptName) {
13991
13896
  return MUTATING_POSTFLIGHTS.has(scriptName ?? "");
13992
13897
  }
@@ -14137,13 +14042,13 @@ async function runExecutable(profileName, input) {
14137
14042
  })
14138
14043
  };
14139
14044
  })() : null;
14140
- const ndjsonDir = path36.join(input.cwd, ".kody");
14045
+ const ndjsonDir = path37.join(input.cwd, ".kody");
14141
14046
  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
14047
  const staffPersona = personaSlug ? framePersona(personaSlug, loadStaffPersona(input.cwd, personaSlug)) : null;
14143
14048
  const jobWhyBlock = typeof ctx.data.jobWhy === "string" ? operatorRequestBlock(ctx.data.jobWhy) : null;
14144
14049
  const jobRefBlock = jobReferenceBlock(profileName, profile, ctx.data);
14145
14050
  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);
14051
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path37.isAbsolute(p) ? p : path37.resolve(profile.dir, p)).filter((p) => p.length > 0);
14147
14052
  const syntheticPath = ctx.data.syntheticPluginPath;
14148
14053
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
14149
14054
  const agents = loadSubagents(profile);
@@ -14184,7 +14089,7 @@ async function runExecutable(profileName, input) {
14184
14089
  enableVerifyTool: profile.claudeCode.enableVerifyTool,
14185
14090
  enableSubmitTool: profile.claudeCode.enableSubmitTool,
14186
14091
  // Locked-toolbox duty mode: `loadJobFromFile` flips `ctx.data.dutyTools`
14187
- // when a duty declares `tools:` frontmatter. The executor doesn't need
14092
+ // when a duty declares `tools` in profile.json. The executor doesn't need
14188
14093
  // to know the palette — it just forwards the flag so agent.ts can spin
14189
14094
  // up the in-process `kody-duty` MCP server with the right context.
14190
14095
  enableDutyTool: Array.isArray(ctx.data.dutyTools) && ctx.data.dutyTools.length > 0,
@@ -14469,13 +14374,13 @@ function clearStampedLifecycleLabels(profile, ctx) {
14469
14374
  function resolveProfilePath(profileName) {
14470
14375
  const found = resolveExecutable(profileName);
14471
14376
  if (found) return found;
14472
- const here = path36.dirname(new URL(import.meta.url).pathname);
14377
+ const here = path37.dirname(new URL(import.meta.url).pathname);
14473
14378
  const candidates = [
14474
- path36.join(here, "executables", profileName, "profile.json"),
14379
+ path37.join(here, "executables", profileName, "profile.json"),
14475
14380
  // same-dir sibling (dev)
14476
- path36.join(here, "..", "executables", profileName, "profile.json"),
14381
+ path37.join(here, "..", "executables", profileName, "profile.json"),
14477
14382
  // up one (prod: dist/bin → dist/executables)
14478
- path36.join(here, "..", "src", "executables", profileName, "profile.json")
14383
+ path37.join(here, "..", "src", "executables", profileName, "profile.json")
14479
14384
  // fallback
14480
14385
  ];
14481
14386
  for (const c of candidates) {
@@ -14577,7 +14482,7 @@ function resolveShellTimeoutMs(entry) {
14577
14482
  }
14578
14483
  async function runShellEntry(entry, ctx, profile) {
14579
14484
  const shellName = entry.shell;
14580
- const shellPath = path36.join(profile.dir, shellName);
14485
+ const shellPath = path37.join(profile.dir, shellName);
14581
14486
  if (!fs39.existsSync(shellPath)) {
14582
14487
  ctx.skipAgent = true;
14583
14488
  ctx.output.exitCode = 99;
@@ -14834,18 +14739,18 @@ function translateOpenAISseToBrain(opts) {
14834
14739
  // src/servers/brain-serve.ts
14835
14740
  import * as fs42 from "fs";
14836
14741
  import { createServer } from "http";
14837
- import * as path39 from "path";
14742
+ import * as path40 from "path";
14838
14743
 
14839
14744
  // src/chat/loop.ts
14840
14745
  init_agent();
14841
14746
  init_registry();
14842
14747
  init_task_artifacts();
14843
- import * as fs11 from "fs";
14844
- import * as path11 from "path";
14748
+ import * as fs12 from "fs";
14749
+ import * as path12 from "path";
14845
14750
 
14846
14751
  // src/chat/attachments.ts
14847
- import * as fs8 from "fs";
14848
- import * as path8 from "path";
14752
+ import * as fs9 from "fs";
14753
+ import * as path9 from "path";
14849
14754
  var INLINE_ATTACHMENT_RE = /(?:\[(?:Image|File): ([^\]]*)\]\n)?data:([\w.+-]+\/[\w.+-]+);base64,([A-Za-z0-9+/=]+)/g;
14850
14755
  var EXT_BY_MIME = {
14851
14756
  "image/png": "png",
@@ -14861,7 +14766,7 @@ function extFor(mime) {
14861
14766
  return EXT_BY_MIME[mime.toLowerCase()] ?? mime.split("/")[1]?.replace(/[^\w]/g, "") ?? "bin";
14862
14767
  }
14863
14768
  function attachmentsDir(cwd, sessionId) {
14864
- return path8.join(cwd, ".kody", "tmp", "attachments", sessionId);
14769
+ return path9.join(cwd, ".kody", "tmp", "attachments", sessionId);
14865
14770
  }
14866
14771
  function prepareAttachments(turns, cwd, sessionId) {
14867
14772
  const imagePaths = [];
@@ -14878,11 +14783,11 @@ function prepareAttachments(turns, cwd, sessionId) {
14878
14783
  if (!isImage) return `[File: ${name}]`;
14879
14784
  try {
14880
14785
  if (!dirEnsured) {
14881
- fs8.mkdirSync(dir, { recursive: true });
14786
+ fs9.mkdirSync(dir, { recursive: true });
14882
14787
  dirEnsured = true;
14883
14788
  }
14884
- const filePath = path8.join(dir, `${imageCounter}.${extFor(mime)}`);
14885
- fs8.writeFileSync(filePath, Buffer.from(data, "base64"));
14789
+ const filePath = path9.join(dir, `${imageCounter}.${extFor(mime)}`);
14790
+ fs9.writeFileSync(filePath, Buffer.from(data, "base64"));
14886
14791
  imageCounter += 1;
14887
14792
  imagePaths.push(filePath);
14888
14793
  return `[Image "${name}" is attached \u2014 saved to ${filePath}. Use the Read tool on that exact path to view it.]`;
@@ -14898,10 +14803,10 @@ function prepareAttachments(turns, cwd, sessionId) {
14898
14803
  }
14899
14804
 
14900
14805
  // src/chat/events.ts
14901
- import * as fs9 from "fs";
14902
- import * as path9 from "path";
14806
+ import * as fs10 from "fs";
14807
+ import * as path10 from "path";
14903
14808
  function eventsFilePath(cwd, sessionId) {
14904
- return path9.join(cwd, ".kody", "events", `${sessionId}.jsonl`);
14809
+ return path10.join(cwd, ".kody", "events", `${sessionId}.jsonl`);
14905
14810
  }
14906
14811
  var FileSink = class {
14907
14812
  constructor(file) {
@@ -14909,8 +14814,8 @@ var FileSink = class {
14909
14814
  }
14910
14815
  file;
14911
14816
  async emit(event) {
14912
- fs9.mkdirSync(path9.dirname(this.file), { recursive: true });
14913
- fs9.appendFileSync(this.file, `${JSON.stringify(event)}
14817
+ fs10.mkdirSync(path10.dirname(this.file), { recursive: true });
14818
+ fs10.appendFileSync(this.file, `${JSON.stringify(event)}
14914
14819
  `);
14915
14820
  }
14916
14821
  };
@@ -14964,14 +14869,14 @@ function makeRunId(sessionId, suffix) {
14964
14869
  }
14965
14870
 
14966
14871
  // src/chat/session.ts
14967
- import * as fs10 from "fs";
14968
- import * as path10 from "path";
14872
+ import * as fs11 from "fs";
14873
+ import * as path11 from "path";
14969
14874
  function sessionFilePath(cwd, sessionId) {
14970
- return path10.join(cwd, ".kody", "sessions", `${sessionId}.jsonl`);
14875
+ return path11.join(cwd, ".kody", "sessions", `${sessionId}.jsonl`);
14971
14876
  }
14972
14877
  function readMeta(file) {
14973
- if (!fs10.existsSync(file)) return null;
14974
- const raw = fs10.readFileSync(file, "utf-8");
14878
+ if (!fs11.existsSync(file)) return null;
14879
+ const raw = fs11.readFileSync(file, "utf-8");
14975
14880
  const firstLine2 = raw.split("\n", 1)[0]?.trim();
14976
14881
  if (!firstLine2) return null;
14977
14882
  try {
@@ -14984,8 +14889,8 @@ function readMeta(file) {
14984
14889
  }
14985
14890
  }
14986
14891
  function readSession(file) {
14987
- if (!fs10.existsSync(file)) return [];
14988
- const raw = fs10.readFileSync(file, "utf-8").trim();
14892
+ if (!fs11.existsSync(file)) return [];
14893
+ const raw = fs11.readFileSync(file, "utf-8").trim();
14989
14894
  if (!raw) return [];
14990
14895
  const turns = [];
14991
14896
  for (const line of raw.split("\n")) {
@@ -15001,14 +14906,14 @@ function readSession(file) {
15001
14906
  return turns;
15002
14907
  }
15003
14908
  function appendTurn(file, turn) {
15004
- fs10.mkdirSync(path10.dirname(file), { recursive: true });
14909
+ fs11.mkdirSync(path11.dirname(file), { recursive: true });
15005
14910
  const line = JSON.stringify({
15006
14911
  role: turn.role,
15007
14912
  content: turn.content,
15008
14913
  timestamp: turn.timestamp,
15009
14914
  toolCalls: turn.toolCalls ?? []
15010
14915
  });
15011
- fs10.appendFileSync(file, `${line}
14916
+ fs11.appendFileSync(file, `${line}
15012
14917
  `);
15013
14918
  }
15014
14919
  function seedInitialMessage(file, message) {
@@ -15127,7 +15032,7 @@ function buildExecutableCatalog() {
15127
15032
  const entries = [];
15128
15033
  for (const { name, profilePath } of discovered) {
15129
15034
  try {
15130
- const raw = JSON.parse(fs11.readFileSync(profilePath, "utf-8"));
15035
+ const raw = JSON.parse(fs12.readFileSync(profilePath, "utf-8"));
15131
15036
  const describe = typeof raw.describe === "string" ? raw.describe : "";
15132
15037
  const firstSentence = describe.split(/(?<=[.!?])\s+/, 1)[0] ?? "";
15133
15038
  entries.push({ name, describe: firstSentence.trim() });
@@ -15295,10 +15200,10 @@ async function emit(sink, type, sessionId, suffix, payload) {
15295
15200
  var MEMORY_INDEX_REL = ".kody/memory/INDEX.md";
15296
15201
  var MAX_INDEX_BYTES = 8e3;
15297
15202
  function readMemoryIndexBlock(cwd) {
15298
- const indexPath = path11.join(cwd, MEMORY_INDEX_REL);
15203
+ const indexPath = path12.join(cwd, MEMORY_INDEX_REL);
15299
15204
  let raw;
15300
15205
  try {
15301
- raw = fs11.readFileSync(indexPath, "utf-8");
15206
+ raw = fs12.readFileSync(indexPath, "utf-8");
15302
15207
  } catch {
15303
15208
  return "";
15304
15209
  }
@@ -15316,17 +15221,17 @@ function readMemoryIndexBlock(cwd) {
15316
15221
  var CONTEXT_DIR_REL = ".kody/context";
15317
15222
  var MAX_CONTEXT_BYTES = 12e3;
15318
15223
  function readContextBlock(cwd) {
15319
- const dir = path11.join(cwd, CONTEXT_DIR_REL);
15224
+ const dir = path12.join(cwd, CONTEXT_DIR_REL);
15320
15225
  let files;
15321
15226
  try {
15322
- files = fs11.readdirSync(dir).filter((f) => f.endsWith(".md")).sort();
15227
+ files = fs12.readdirSync(dir).filter((f) => f.endsWith(".md")).sort();
15323
15228
  } catch {
15324
15229
  return "";
15325
15230
  }
15326
15231
  const sections = [];
15327
15232
  for (const file of files) {
15328
15233
  try {
15329
- const content = fs11.readFileSync(path11.join(dir, file), "utf-8").trim();
15234
+ const content = fs12.readFileSync(path12.join(dir, file), "utf-8").trim();
15330
15235
  if (content) sections.push(`### ${file.replace(/\.md$/, "")}
15331
15236
 
15332
15237
  ${content}`);
@@ -15349,10 +15254,10 @@ _\u2026 (context truncated; see \`.kody/context/\` for the full text)_` : joined
15349
15254
  var INSTRUCTIONS_REL = ".kody/instructions.md";
15350
15255
  var MAX_INSTRUCTIONS_BYTES = 8e3;
15351
15256
  function readInstructionsBlock(cwd) {
15352
- const instructionsPath = path11.join(cwd, INSTRUCTIONS_REL);
15257
+ const instructionsPath = path12.join(cwd, INSTRUCTIONS_REL);
15353
15258
  let raw;
15354
15259
  try {
15355
- raw = fs11.readFileSync(instructionsPath, "utf-8");
15260
+ raw = fs12.readFileSync(instructionsPath, "utf-8");
15356
15261
  } catch {
15357
15262
  return "";
15358
15263
  }
@@ -15376,7 +15281,7 @@ init_config();
15376
15281
  // src/kody-cli.ts
15377
15282
  import { execFileSync as execFileSync26 } from "child_process";
15378
15283
  import * as fs40 from "fs";
15379
- import * as path37 from "path";
15284
+ import * as path38 from "path";
15380
15285
 
15381
15286
  // src/app-auth.ts
15382
15287
  import { createSign } from "crypto";
@@ -15453,7 +15358,7 @@ init_config();
15453
15358
 
15454
15359
  // src/dispatch.ts
15455
15360
  init_config();
15456
- import * as fs12 from "fs";
15361
+ import * as fs13 from "fs";
15457
15362
 
15458
15363
  // src/cron-match.ts
15459
15364
  var FIELD_BOUNDS = [
@@ -15571,10 +15476,10 @@ function autoDispatch(opts) {
15571
15476
  }
15572
15477
  const eventName = process.env.GITHUB_EVENT_NAME;
15573
15478
  const eventPath = process.env.GITHUB_EVENT_PATH;
15574
- if (!eventName || !eventPath || !fs12.existsSync(eventPath)) return null;
15479
+ if (!eventName || !eventPath || !fs13.existsSync(eventPath)) return null;
15575
15480
  let event = {};
15576
15481
  try {
15577
- event = JSON.parse(fs12.readFileSync(eventPath, "utf-8"));
15482
+ event = JSON.parse(fs13.readFileSync(eventPath, "utf-8"));
15578
15483
  } catch {
15579
15484
  return null;
15580
15485
  }
@@ -15685,7 +15590,7 @@ function autoDispatchTyped(opts) {
15685
15590
  if (legacy) return { kind: "route", ...legacy };
15686
15591
  const eventName = process.env.GITHUB_EVENT_NAME;
15687
15592
  const eventPath = process.env.GITHUB_EVENT_PATH;
15688
- if (!eventName || !eventPath || !fs12.existsSync(eventPath)) {
15593
+ if (!eventName || !eventPath || !fs13.existsSync(eventPath)) {
15689
15594
  return { kind: "silent", reason: "no GHA event context" };
15690
15595
  }
15691
15596
  if (eventName !== "issue_comment") {
@@ -15693,7 +15598,7 @@ function autoDispatchTyped(opts) {
15693
15598
  }
15694
15599
  let event = {};
15695
15600
  try {
15696
- event = JSON.parse(fs12.readFileSync(eventPath, "utf-8"));
15601
+ event = JSON.parse(fs13.readFileSync(eventPath, "utf-8"));
15697
15602
  } catch {
15698
15603
  return { kind: "silent", reason: "GHA event payload unreadable" };
15699
15604
  }
@@ -15737,7 +15642,7 @@ function dispatchScheduledWatches(opts) {
15737
15642
  for (const exe of listExecutables()) {
15738
15643
  let raw;
15739
15644
  try {
15740
- raw = fs12.readFileSync(exe.profilePath, "utf-8");
15645
+ raw = fs13.readFileSync(exe.profilePath, "utf-8");
15741
15646
  } catch {
15742
15647
  continue;
15743
15648
  }
@@ -15980,9 +15885,9 @@ async function resolveAuthToken(env = process.env) {
15980
15885
  return void 0;
15981
15886
  }
15982
15887
  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";
15888
+ if (fs40.existsSync(path38.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
15889
+ if (fs40.existsSync(path38.join(cwd, "yarn.lock"))) return "yarn";
15890
+ if (fs40.existsSync(path38.join(cwd, "bun.lockb"))) return "bun";
15986
15891
  return "npm";
15987
15892
  }
15988
15893
  function shellOut(cmd, args, cwd, stream = true) {
@@ -16069,7 +15974,7 @@ function configureGitIdentity(cwd) {
16069
15974
  }
16070
15975
  function postFailureTail(issueNumber, cwd, reason) {
16071
15976
  if (!issueNumber) return;
16072
- const logPath = path37.join(cwd, ".kody", "last-run.jsonl");
15977
+ const logPath = path38.join(cwd, ".kody", "last-run.jsonl");
16073
15978
  let tail = "";
16074
15979
  try {
16075
15980
  if (fs40.existsSync(logPath)) {
@@ -16098,7 +16003,7 @@ async function runCi(argv) {
16098
16003
  return 0;
16099
16004
  }
16100
16005
  const args = parseCiArgs(argv);
16101
- const cwd = args.cwd ? path37.resolve(args.cwd) : process.cwd();
16006
+ const cwd = args.cwd ? path38.resolve(args.cwd) : process.cwd();
16102
16007
  let earlyConfig;
16103
16008
  let earlyConfigError;
16104
16009
  try {
@@ -16409,10 +16314,10 @@ init_repoWorkspace();
16409
16314
 
16410
16315
  // src/scripts/brainTurnLog.ts
16411
16316
  import * as fs41 from "fs";
16412
- import * as path38 from "path";
16317
+ import * as path39 from "path";
16413
16318
  var live = /* @__PURE__ */ new Map();
16414
16319
  function eventsPath(dir, chatId) {
16415
- return path38.join(dir, ".kody", "brain-events", `${chatId}.jsonl`);
16320
+ return path39.join(dir, ".kody", "brain-events", `${chatId}.jsonl`);
16416
16321
  }
16417
16322
  function lastPersistedSeq(dir, chatId) {
16418
16323
  const p = eventsPath(dir, chatId);
@@ -16455,7 +16360,7 @@ function beginTurn(dir, chatId) {
16455
16360
  };
16456
16361
  live.set(chatId, state);
16457
16362
  const p = eventsPath(dir, chatId);
16458
- fs41.mkdirSync(path38.dirname(p), { recursive: true });
16363
+ fs41.mkdirSync(path39.dirname(p), { recursive: true });
16459
16364
  return (event) => {
16460
16365
  state.seq += 1;
16461
16366
  const rec = { seq: state.seq, turn, ts: Date.now(), event };
@@ -16730,7 +16635,7 @@ async function handleChatTurn(req, res, chatId, opts) {
16730
16635
  const repo = strField(body, "repo");
16731
16636
  const repoToken = strField(body, "repoToken");
16732
16637
  const sessionFile = sessionFilePath(opts.cwd, chatId);
16733
- fs42.mkdirSync(path39.dirname(sessionFile), { recursive: true });
16638
+ fs42.mkdirSync(path40.dirname(sessionFile), { recursive: true });
16734
16639
  appendTurn(sessionFile, {
16735
16640
  role: "user",
16736
16641
  content: message,
@@ -16777,7 +16682,7 @@ async function handleChatTurn(req, res, chatId, opts) {
16777
16682
  function buildServer(opts) {
16778
16683
  const runTurn = opts.runTurn ?? runChatTurn;
16779
16684
  const cloneRepo = opts.cloneRepo ?? defaultCloneRepo;
16780
- const reposRoot = opts.reposRoot ?? path39.join(path39.dirname(path39.resolve(opts.cwd)), "repos");
16685
+ const reposRoot = opts.reposRoot ?? path40.join(path40.dirname(path40.resolve(opts.cwd)), "repos");
16781
16686
  return createServer(async (req, res) => {
16782
16687
  if (!req.method || !req.url) {
16783
16688
  sendJson(res, 400, { error: "bad request" });
@@ -17371,13 +17276,13 @@ async function loadConfigSafe() {
17371
17276
  // src/chat-cli.ts
17372
17277
  import { execFileSync as execFileSync29 } from "child_process";
17373
17278
  import * as fs44 from "fs";
17374
- import * as path41 from "path";
17279
+ import * as path42 from "path";
17375
17280
 
17376
17281
  // src/chat/modes/interactive.ts
17377
17282
  init_issue();
17378
17283
  import { execFileSync as execFileSync28 } from "child_process";
17379
17284
  import * as fs43 from "fs";
17380
- import * as path40 from "path";
17285
+ import * as path41 from "path";
17381
17286
 
17382
17287
  // src/chat/inbox.ts
17383
17288
  import { execFileSync as execFileSync27 } from "child_process";
@@ -17536,9 +17441,9 @@ function findNextUserTurn(turns, fromIdx) {
17536
17441
  return -1;
17537
17442
  }
17538
17443
  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)));
17444
+ const sessionRel = path41.relative(cwd, sessionFilePath(cwd, sessionId));
17445
+ const eventsRel = path41.relative(cwd, eventsFilePath(cwd, sessionId));
17446
+ const rels = [sessionRel, eventsRel].filter((p) => fs43.existsSync(path41.join(cwd, p)));
17542
17447
  if (rels.length === 0) return;
17543
17448
  const repository = process.env.GITHUB_REPOSITORY;
17544
17449
  if (!repository) {
@@ -17550,8 +17455,8 @@ function commitTurn(cwd, sessionId, _verbose) {
17550
17455
  }
17551
17456
  const branch = defaultBranch(cwd) ?? "main";
17552
17457
  for (const rel of rels) {
17553
- const repoPath = rel.split(path40.sep).join("/");
17554
- const localText = fs43.readFileSync(path40.join(cwd, rel), "utf-8");
17458
+ const repoPath = rel.split(path41.sep).join("/");
17459
+ const localText = fs43.readFileSync(path41.join(cwd, rel), "utf-8");
17555
17460
  putJsonlViaContents(repository, branch, repoPath, localText, sessionId, cwd);
17556
17461
  }
17557
17462
  }
@@ -17688,12 +17593,12 @@ function parseChatArgs(argv, env = process.env) {
17688
17593
  return result;
17689
17594
  }
17690
17595
  function commitChatFiles(cwd, sessionId, verbose) {
17691
- const sessionFile = path41.relative(cwd, sessionFilePath(cwd, sessionId));
17692
- const eventsFile = path41.relative(cwd, eventsFilePath(cwd, sessionId));
17596
+ const sessionFile = path42.relative(cwd, sessionFilePath(cwd, sessionId));
17597
+ const eventsFile = path42.relative(cwd, eventsFilePath(cwd, sessionId));
17693
17598
  const safeSession = sessionId.replace(/[^a-zA-Z0-9._-]/g, "_");
17694
- const tasksDir = path41.join(".kody", "tasks", safeSession);
17599
+ const tasksDir = path42.join(".kody", "tasks", safeSession);
17695
17600
  const candidatePaths = [sessionFile, eventsFile, tasksDir];
17696
- const paths = candidatePaths.filter((p) => fs44.existsSync(path41.join(cwd, p)));
17601
+ const paths = candidatePaths.filter((p) => fs44.existsSync(path42.join(cwd, p)));
17697
17602
  if (paths.length === 0) return;
17698
17603
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
17699
17604
  try {
@@ -17737,7 +17642,7 @@ async function runChat(argv) {
17737
17642
  ${CHAT_HELP}`);
17738
17643
  return 64;
17739
17644
  }
17740
- const cwd = args.cwd ? path41.resolve(args.cwd) : process.cwd();
17645
+ const cwd = args.cwd ? path42.resolve(args.cwd) : process.cwd();
17741
17646
  const sessionId = args.sessionId;
17742
17647
  const unpackedSecrets = unpackAllSecrets();
17743
17648
  if (unpackedSecrets > 0) {
@@ -17946,8 +17851,8 @@ var FlyClient = class {
17946
17851
  get fetch() {
17947
17852
  return this.opts.fetchImpl ?? fetch;
17948
17853
  }
17949
- async call(path42, init = {}) {
17950
- const res = await this.fetch(`${FLY_API_BASE}${path42}`, {
17854
+ async call(path43, init = {}) {
17855
+ const res = await this.fetch(`${FLY_API_BASE}${path43}`, {
17951
17856
  method: init.method ?? "GET",
17952
17857
  headers: {
17953
17858
  Authorization: `Bearer ${this.opts.token}`,
@@ -17958,7 +17863,7 @@ var FlyClient = class {
17958
17863
  if (res.status === 404 && init.allow404) return null;
17959
17864
  if (!res.ok) {
17960
17865
  const text = await res.text().catch(() => "");
17961
- throw new Error(`Fly API ${res.status} on ${path42}: ${text.slice(0, 200) || res.statusText}`);
17866
+ throw new Error(`Fly API ${res.status} on ${path43}: ${text.slice(0, 200) || res.statusText}`);
17962
17867
  }
17963
17868
  if (res.status === 204) return null;
17964
17869
  const raw = await res.text();