@kody-ade/kody-engine 0.4.203 → 0.4.204-next.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/kody.js CHANGED
@@ -551,9 +551,11 @@ __export(dutyMcp_exports, {
551
551
  dispatchWorkflow: () => dispatchWorkflow,
552
552
  ensureComment: () => ensureComment,
553
553
  ensureIssue: () => ensureIssue,
554
+ isDispatchGated: () => isDispatchGated,
554
555
  parseDutyTrustMode: () => parseDutyTrustMode,
555
556
  readCheckRuns: () => readCheckRuns,
556
- readDutyTrustMode: () => readDutyTrustMode
557
+ readDutyTrustMode: () => readDutyTrustMode,
558
+ readThread: () => readThread
557
559
  });
558
560
  import { createSdkMcpServer as createSdkMcpServer3, tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
559
561
  import { z as z3 } from "zod";
@@ -665,6 +667,24 @@ function readDutyTrustMode(repoSlug, dutySlug) {
665
667
  return "ask";
666
668
  }
667
669
  }
670
+ function readThread(repoSlug, number, limit = 10) {
671
+ const meta = JSON.parse(gh(["api", `repos/${repoSlug}/issues/${number}`]));
672
+ const rawComments = JSON.parse(
673
+ gh(["api", `repos/${repoSlug}/issues/${number}/comments?per_page=100`])
674
+ );
675
+ const comments = rawComments.slice(-Math.max(1, limit)).map((c) => ({
676
+ author: c.user?.login ?? "?",
677
+ createdAt: c.created_at ?? "",
678
+ body: (c.body ?? "").slice(0, THREAD_BODY_MAX)
679
+ }));
680
+ return {
681
+ number,
682
+ title: meta.title ?? "",
683
+ state: meta.state ?? "",
684
+ labels: (meta.labels ?? []).map((l) => l.name ?? "").filter(Boolean),
685
+ comments
686
+ };
687
+ }
668
688
  function readCheckRuns(repoSlug, ref, ignoreNames) {
669
689
  const sha = gh(["api", `repos/${repoSlug}/commits/${ref}`, "--jq", ".sha"]).trim();
670
690
  const raw = gh([
@@ -723,6 +743,11 @@ function dispatchWorkflow(workflowFile, executable, issueNumber) {
723
743
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
724
744
  }
725
745
  }
746
+ function isDispatchGated(executable, mode) {
747
+ if (mode === "auto") return false;
748
+ if (executable && GATE_EXEMPT_EXECUTABLES.has(executable)) return false;
749
+ return true;
750
+ }
726
751
  function trustRefusal(dutySlug) {
727
752
  return `Not dispatched: duty \`${dutySlug ?? "?"}\` is in ASK mode (not trusted for autonomy). Do NOT retry the dispatch. Instead notify the operator (use recommend_to_operator, or rely on the tracking issue that already @-mentions them), then submit_state. To let this duty act on its own, grant it Auto on the dashboard Trust page.`;
728
753
  }
@@ -751,7 +776,7 @@ function buildDutyMcpServer(opts) {
751
776
  pr: z3.number().int().positive().describe("PR number to repair.")
752
777
  },
753
778
  async (args) => {
754
- if (readDutyTrustMode(opts.repoSlug, opts.dutySlug) !== "auto") {
779
+ if (isDispatchGated(verb, readDutyTrustMode(opts.repoSlug, opts.dutySlug))) {
755
780
  return { content: [{ type: "text", text: trustRefusal(opts.dutySlug) }] };
756
781
  }
757
782
  const result = dispatchVerb(workflowFile, verb, args.pr);
@@ -818,6 +843,18 @@ function buildDutyMcpServer(opts) {
818
843
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
819
844
  }
820
845
  );
846
+ const readThreadTool = tool3(
847
+ "read_thread",
848
+ "Read an issue or PR's recent comments + labels + title/state. Returns {number, title, state, labels:[...], comments:[{author, createdAt, body}]} (newest last, body truncated). Use this to read a verdict a dispatched check posted back \u2014 e.g. qa-engineer's report or ui-review's PASS/CONCERNS/FAIL \u2014 on a later tick. Read-only; works for both issues and PRs.",
849
+ {
850
+ number: z3.number().int().positive().describe("Issue or PR number to read."),
851
+ limit: z3.number().int().positive().optional().describe("Max recent comments to return (default 10).")
852
+ },
853
+ async (args) => {
854
+ const result = readThread(opts.repoSlug, args.number, args.limit ?? 10);
855
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
856
+ }
857
+ );
821
858
  const ensureIssueTool = tool3(
822
859
  "ensure_issue",
823
860
  "Idempotently ensure ONE open tracking issue exists for `key`. Searches OPEN issues (issues API, not the laggy search index) for `key`'s hidden marker; if found, returns {created:false, number} and creates NOTHING; otherwise creates the issue (title + body, marker appended) and returns {created:true, number}. This is the anti-duplication primitive: use one stable `key` per recurring finding so re-ticks reuse the same issue. Only take follow-up actions (dispatch/comment) when created===true.",
@@ -856,7 +893,7 @@ function buildDutyMcpServer(opts) {
856
893
  issueNumber: z3.number().int().positive().describe("Issue (or PR) number forwarded as issue_number.")
857
894
  },
858
895
  async (args) => {
859
- if (readDutyTrustMode(opts.repoSlug, opts.dutySlug) !== "auto") {
896
+ if (isDispatchGated(args.executable, readDutyTrustMode(opts.repoSlug, opts.dutySlug))) {
860
897
  return { content: [{ type: "text", text: trustRefusal(opts.dutySlug) }] };
861
898
  }
862
899
  const result = dispatchWorkflow(workflowFile, args.executable, args.issueNumber);
@@ -875,6 +912,7 @@ function buildDutyMcpServer(opts) {
875
912
  recommendTool,
876
913
  ledgerTool,
877
914
  checkRunsTool,
915
+ readThreadTool,
878
916
  ensureIssueTool,
879
917
  ensureCommentTool,
880
918
  dispatchTool
@@ -882,7 +920,7 @@ function buildDutyMcpServer(opts) {
882
920
  });
883
921
  return { server };
884
922
  }
885
- var FAIL_CONCLUSIONS, RUNNING_STATUSES, TRUST_FILE_PATH, TRUST_STATE_BRANCH, CHECK_FAIL_CONCLUSIONS, DEFAULT_IGNORE_CHECKS, trackMarker, commentMarker, DUTY_MCP_TOOL_NAMES;
923
+ var FAIL_CONCLUSIONS, RUNNING_STATUSES, TRUST_FILE_PATH, TRUST_STATE_BRANCH, THREAD_BODY_MAX, CHECK_FAIL_CONCLUSIONS, DEFAULT_IGNORE_CHECKS, trackMarker, commentMarker, GATE_EXEMPT_EXECUTABLES, DUTY_MCP_TOOL_NAMES;
886
924
  var init_dutyMcp = __esm({
887
925
  "src/dutyMcp.ts"() {
888
926
  "use strict";
@@ -891,10 +929,12 @@ var init_dutyMcp = __esm({
891
929
  RUNNING_STATUSES = /* @__PURE__ */ new Set(["IN_PROGRESS", "QUEUED", "PENDING", "WAITING", "REQUESTED"]);
892
930
  TRUST_FILE_PATH = ".kody/state/trust.json";
893
931
  TRUST_STATE_BRANCH = "kody-state";
932
+ THREAD_BODY_MAX = 4e3;
894
933
  CHECK_FAIL_CONCLUSIONS = /* @__PURE__ */ new Set(["FAILURE", "TIMED_OUT", "STARTUP_FAILURE", "ACTION_REQUIRED"]);
895
934
  DEFAULT_IGNORE_CHECKS = ["run", "kody", "job-tick", "goal-tick", "worker-ask", "chat"];
896
935
  trackMarker = (key) => `<!-- kody-track:${key} -->`;
897
936
  commentMarker = (key) => `<!-- kody-track-comment:${key} -->`;
937
+ GATE_EXEMPT_EXECUTABLES = /* @__PURE__ */ new Set(["qa-engineer", "ui-review"]);
898
938
  DUTY_MCP_TOOL_NAMES = [
899
939
  "list_prs_to_repair",
900
940
  "sync_pr",
@@ -903,6 +943,7 @@ var init_dutyMcp = __esm({
903
943
  "recommend_to_operator",
904
944
  "read_ledger",
905
945
  "read_check_runs",
946
+ "read_thread",
906
947
  "ensure_issue",
907
948
  "ensure_comment",
908
949
  "dispatch_workflow"
@@ -1031,16 +1072,16 @@ var init_fetchRepoMcp = __esm({
1031
1072
  });
1032
1073
 
1033
1074
  // src/prompt.ts
1034
- import * as fs15 from "fs";
1035
- import * as path14 from "path";
1075
+ import * as fs17 from "fs";
1076
+ import * as path16 from "path";
1036
1077
  function loadProjectConventions(projectDir) {
1037
1078
  const out = [];
1038
1079
  for (const rel of CONVENTION_FILES) {
1039
- const abs = path14.join(projectDir, rel);
1040
- if (!fs15.existsSync(abs)) continue;
1080
+ const abs = path16.join(projectDir, rel);
1081
+ if (!fs17.existsSync(abs)) continue;
1041
1082
  let content;
1042
1083
  try {
1043
- content = fs15.readFileSync(abs, "utf-8");
1084
+ content = fs17.readFileSync(abs, "utf-8");
1044
1085
  } catch {
1045
1086
  continue;
1046
1087
  }
@@ -1275,28 +1316,28 @@ var loadMemoryContext_exports = {};
1275
1316
  __export(loadMemoryContext_exports, {
1276
1317
  loadMemoryContext: () => loadMemoryContext
1277
1318
  });
1278
- import * as fs16 from "fs";
1279
- import * as path15 from "path";
1319
+ import * as fs18 from "fs";
1320
+ import * as path17 from "path";
1280
1321
  function collectPages(memoryAbs) {
1281
1322
  const out = [];
1282
1323
  walkMd(memoryAbs, (file) => {
1283
1324
  let stat;
1284
1325
  try {
1285
- stat = fs16.statSync(file);
1326
+ stat = fs18.statSync(file);
1286
1327
  } catch {
1287
1328
  return;
1288
1329
  }
1289
1330
  let raw;
1290
1331
  try {
1291
- raw = fs16.readFileSync(file, "utf-8");
1332
+ raw = fs18.readFileSync(file, "utf-8");
1292
1333
  } catch {
1293
1334
  return;
1294
1335
  }
1295
1336
  const fm = raw.match(/^---\s*\n([\s\S]*?)\n---/);
1296
- const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path15.basename(file, ".md");
1337
+ const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path17.basename(file, ".md");
1297
1338
  const updated = fm?.[1]?.match(/^updated:\s*([0-9T:.+\-Z]+)/m)?.[1]?.trim() ?? "";
1298
1339
  out.push({
1299
- relPath: path15.relative(memoryAbs, file),
1340
+ relPath: path17.relative(memoryAbs, file),
1300
1341
  title,
1301
1342
  updated,
1302
1343
  content: raw.length > PER_PAGE_MAX_BYTES ? raw.slice(0, PER_PAGE_MAX_BYTES) + TRUNCATED_SUFFIX2 : raw,
@@ -1364,16 +1405,16 @@ function walkMd(root, visit) {
1364
1405
  const dir = stack.pop();
1365
1406
  let names;
1366
1407
  try {
1367
- names = fs16.readdirSync(dir);
1408
+ names = fs18.readdirSync(dir);
1368
1409
  } catch {
1369
1410
  continue;
1370
1411
  }
1371
1412
  for (const name of names) {
1372
1413
  if (name.startsWith(".")) continue;
1373
- const full = path15.join(dir, name);
1414
+ const full = path17.join(dir, name);
1374
1415
  let stat;
1375
1416
  try {
1376
- stat = fs16.statSync(full);
1417
+ stat = fs18.statSync(full);
1377
1418
  } catch {
1378
1419
  continue;
1379
1420
  }
@@ -1396,8 +1437,8 @@ var init_loadMemoryContext = __esm({
1396
1437
  TRUNCATED_SUFFIX2 = "\n\n\u2026 (truncated)";
1397
1438
  loadMemoryContext = async (ctx) => {
1398
1439
  if (typeof ctx.data.memoryContext === "string") return;
1399
- const memoryAbs = path15.join(ctx.cwd, MEMORY_DIR_RELATIVE);
1400
- if (!fs16.existsSync(memoryAbs)) {
1440
+ const memoryAbs = path17.join(ctx.cwd, MEMORY_DIR_RELATIVE);
1441
+ if (!fs18.existsSync(memoryAbs)) {
1401
1442
  ctx.data.memoryContext = "";
1402
1443
  return;
1403
1444
  }
@@ -1442,7 +1483,7 @@ var init_loadCoverageRules = __esm({
1442
1483
  // package.json
1443
1484
  var package_default = {
1444
1485
  name: "@kody-ade/kody-engine",
1445
- version: "0.4.203",
1486
+ version: "0.4.204-next.10",
1446
1487
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
1447
1488
  license: "MIT",
1448
1489
  type: "module",
@@ -1500,8 +1541,8 @@ var package_default = {
1500
1541
 
1501
1542
  // src/chat-cli.ts
1502
1543
  import { execFileSync as execFileSync29 } from "child_process";
1503
- import * as fs44 from "fs";
1504
- import * as path40 from "path";
1544
+ import * as fs45 from "fs";
1545
+ import * as path41 from "path";
1505
1546
 
1506
1547
  // src/chat/events.ts
1507
1548
  import * as fs from "fs";
@@ -2334,6 +2375,9 @@ function getExecutablesRoot() {
2334
2375
  function getProjectExecutablesRoot() {
2335
2376
  return path7.join(process.cwd(), ".kody", "executables");
2336
2377
  }
2378
+ function getProjectDutiesRoot() {
2379
+ return path7.join(process.cwd(), ".kody", "duties");
2380
+ }
2337
2381
  function getBuiltinJobsRoot() {
2338
2382
  const here = path7.dirname(new URL(import.meta.url).pathname);
2339
2383
  const candidates = [
@@ -2361,7 +2405,7 @@ function listBuiltinJobs(root = getBuiltinJobsRoot()) {
2361
2405
  return out;
2362
2406
  }
2363
2407
  function getExecutableRoots() {
2364
- return [getProjectExecutablesRoot(), getExecutablesRoot()];
2408
+ return [getProjectDutiesRoot(), getProjectExecutablesRoot(), getExecutablesRoot()];
2365
2409
  }
2366
2410
  function listExecutables(roots = getExecutableRoots()) {
2367
2411
  const rootList = typeof roots === "string" ? [roots] : roots;
@@ -3249,8 +3293,8 @@ async function emit2(sink, type, sessionId, suffix, payload) {
3249
3293
 
3250
3294
  // src/kody-cli.ts
3251
3295
  import { execFileSync as execFileSync28 } from "child_process";
3252
- import * as fs43 from "fs";
3253
- import * as path39 from "path";
3296
+ import * as fs44 from "fs";
3297
+ import * as path40 from "path";
3254
3298
 
3255
3299
  // src/app-auth.ts
3256
3300
  import { createSign } from "crypto";
@@ -3676,17 +3720,17 @@ function coerceBare(spec, value) {
3676
3720
 
3677
3721
  // src/executor.ts
3678
3722
  import { spawn as spawn10 } from "child_process";
3679
- import * as fs42 from "fs";
3680
- import * as path38 from "path";
3723
+ import * as fs43 from "fs";
3724
+ import * as path39 from "path";
3681
3725
 
3682
3726
  // src/container.ts
3683
3727
  init_events();
3684
3728
  import { execFileSync as execFileSync5 } from "child_process";
3685
- import * as fs17 from "fs";
3729
+ import * as fs19 from "fs";
3686
3730
 
3687
3731
  // src/profile.ts
3688
- import * as fs14 from "fs";
3689
- import * as path13 from "path";
3732
+ import * as fs16 from "fs";
3733
+ import * as path15 from "path";
3690
3734
 
3691
3735
  // src/profile-error.ts
3692
3736
  var ProfileError = class extends Error {
@@ -3826,6 +3870,150 @@ function applyLifecycle(profile, profilePath) {
3826
3870
  expander(profile, profilePath);
3827
3871
  }
3828
3872
 
3873
+ // src/subagents.ts
3874
+ import * as fs15 from "fs";
3875
+ import * as path14 from "path";
3876
+
3877
+ // src/scripts/buildSyntheticPlugin.ts
3878
+ import * as fs14 from "fs";
3879
+ import * as os2 from "os";
3880
+ import * as path13 from "path";
3881
+ function getPluginsCatalogRoot() {
3882
+ const here = path13.dirname(new URL(import.meta.url).pathname);
3883
+ const candidates = [
3884
+ path13.join(here, "..", "plugins"),
3885
+ // dev: src/scripts → src/plugins
3886
+ path13.join(here, "..", "..", "plugins"),
3887
+ // built: dist/scripts → dist/plugins
3888
+ path13.join(here, "..", "..", "src", "plugins")
3889
+ // fallback
3890
+ ];
3891
+ for (const c of candidates) {
3892
+ if (fs14.existsSync(c) && fs14.statSync(c).isDirectory()) return c;
3893
+ }
3894
+ return candidates[0];
3895
+ }
3896
+ var buildSyntheticPlugin = async (ctx, profile) => {
3897
+ const cc = profile.claudeCode;
3898
+ const needsSynthetic = cc.skills.length > 0 || cc.commands.length > 0 || cc.hooks.length > 0;
3899
+ if (!needsSynthetic) return;
3900
+ const catalog = getPluginsCatalogRoot();
3901
+ const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
3902
+ const root = path13.join(os2.tmpdir(), `kody-synth-${runId}`);
3903
+ fs14.mkdirSync(path13.join(root, ".claude-plugin"), { recursive: true });
3904
+ const resolvePart = (bucket, entry) => {
3905
+ const local = path13.join(profile.dir, bucket, entry);
3906
+ if (fs14.existsSync(local)) return local;
3907
+ const central = path13.join(catalog, bucket, entry);
3908
+ if (fs14.existsSync(central)) return central;
3909
+ throw new Error(
3910
+ `buildSyntheticPlugin: ${bucket} entry '${entry}' not found in executable dir (${profile.dir}/${bucket}/) or catalog (${catalog}/${bucket}/)`
3911
+ );
3912
+ };
3913
+ if (cc.skills.length > 0) {
3914
+ const dst = path13.join(root, "skills");
3915
+ fs14.mkdirSync(dst, { recursive: true });
3916
+ for (const name of cc.skills) {
3917
+ copyDir(resolvePart("skills", name), path13.join(dst, name));
3918
+ }
3919
+ }
3920
+ if (cc.commands.length > 0) {
3921
+ const dst = path13.join(root, "commands");
3922
+ fs14.mkdirSync(dst, { recursive: true });
3923
+ for (const name of cc.commands) {
3924
+ fs14.copyFileSync(resolvePart("commands", `${name}.md`), path13.join(dst, `${name}.md`));
3925
+ }
3926
+ }
3927
+ if (cc.hooks.length > 0) {
3928
+ const dst = path13.join(root, "hooks");
3929
+ fs14.mkdirSync(dst, { recursive: true });
3930
+ const merged = { hooks: {} };
3931
+ for (const name of cc.hooks) {
3932
+ const src = resolvePart("hooks", `${name}.json`);
3933
+ const parsed = JSON.parse(fs14.readFileSync(src, "utf-8"));
3934
+ for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
3935
+ if (!Array.isArray(entries)) continue;
3936
+ if (!merged.hooks[event]) merged.hooks[event] = [];
3937
+ merged.hooks[event].push(...entries);
3938
+ }
3939
+ }
3940
+ fs14.writeFileSync(path13.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
3941
+ `);
3942
+ }
3943
+ const manifest = {
3944
+ name: `kody-synth-${profile.name}`,
3945
+ version: "1.0.0",
3946
+ description: `Synthetic plugin assembled by Kody for profile '${profile.name}' at runtime.`
3947
+ };
3948
+ if (cc.skills.length > 0) manifest.skills = ["./skills/"];
3949
+ if (cc.commands.length > 0) manifest.commands = ["./commands/"];
3950
+ fs14.writeFileSync(path13.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
3951
+ `);
3952
+ ctx.data.syntheticPluginPath = root;
3953
+ };
3954
+ function copyDir(src, dst) {
3955
+ fs14.mkdirSync(dst, { recursive: true });
3956
+ for (const ent of fs14.readdirSync(src, { withFileTypes: true })) {
3957
+ const s = path13.join(src, ent.name);
3958
+ const d = path13.join(dst, ent.name);
3959
+ if (ent.isDirectory()) copyDir(s, d);
3960
+ else if (ent.isFile()) fs14.copyFileSync(s, d);
3961
+ }
3962
+ }
3963
+
3964
+ // src/subagents.ts
3965
+ function splitFrontmatter(raw) {
3966
+ const match = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(raw);
3967
+ if (!match) return { fm: {}, body: raw.trim() };
3968
+ const fm = {};
3969
+ for (const line of match[1].split("\n")) {
3970
+ const idx = line.indexOf(":");
3971
+ if (idx === -1) continue;
3972
+ fm[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
3973
+ }
3974
+ return { fm, body: (match[2] ?? "").trim() };
3975
+ }
3976
+ function resolveAgentFile(profileDir, name) {
3977
+ const local = path14.join(profileDir, "agents", `${name}.md`);
3978
+ if (fs15.existsSync(local)) return local;
3979
+ const central = path14.join(getPluginsCatalogRoot(), "agents", `${name}.md`);
3980
+ if (fs15.existsSync(central)) return central;
3981
+ throw new Error(`loadSubagents: agent '${name}' not found in ${profileDir}/agents/ or shared catalog`);
3982
+ }
3983
+ function captureSubagentTemplates(profile) {
3984
+ const names = profile.claudeCode.subagents;
3985
+ if (!names || names.length === 0) return {};
3986
+ const out = {};
3987
+ for (const name of names) {
3988
+ try {
3989
+ out[name] = fs15.readFileSync(resolveAgentFile(profile.dir, name), "utf-8");
3990
+ } catch {
3991
+ }
3992
+ }
3993
+ return out;
3994
+ }
3995
+ function loadSubagents(profile) {
3996
+ const names = profile.claudeCode.subagents;
3997
+ if (!names || names.length === 0) return void 0;
3998
+ const agents = {};
3999
+ for (const name of names) {
4000
+ const raw = profile.subagentTemplates?.[name] ?? fs15.readFileSync(resolveAgentFile(profile.dir, name), "utf-8");
4001
+ const { fm, body } = splitFrontmatter(raw);
4002
+ if (!body) throw new Error(`loadSubagents: agent '${name}' has an empty prompt body`);
4003
+ const def = {
4004
+ description: fm.description ?? `Subagent ${name}`,
4005
+ prompt: body
4006
+ };
4007
+ if (fm.tools) {
4008
+ const tools = fm.tools.split(",").map((t) => t.trim()).filter(Boolean);
4009
+ if (tools.length > 0) def.tools = tools;
4010
+ }
4011
+ if (fm.model) def.model = fm.model;
4012
+ agents[fm.name || name] = def;
4013
+ }
4014
+ return agents;
4015
+ }
4016
+
3829
4017
  // src/profile.ts
3830
4018
  var VALID_INPUT_TYPES = /* @__PURE__ */ new Set(["int", "string", "bool", "enum"]);
3831
4019
  var VALID_PERMISSION_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "bypassPermissions"]);
@@ -3834,6 +4022,10 @@ var VALID_CONTAINER_CHILD_TARGETS = /* @__PURE__ */ new Set(["issue", "pr"]);
3834
4022
  var VALID_PHASES = /* @__PURE__ */ new Set(["research", "planning", "implementing", "reviewing", "shipped", "failed", "idle"]);
3835
4023
  var KNOWN_PROFILE_KEYS = /* @__PURE__ */ new Set([
3836
4024
  "name",
4025
+ "staff",
4026
+ "every",
4027
+ "dutyTools",
4028
+ "mentions",
3837
4029
  "describe",
3838
4030
  "role",
3839
4031
  "kind",
@@ -3857,12 +4049,12 @@ var KNOWN_PROFILE_KEYS = /* @__PURE__ */ new Set([
3857
4049
  "preloadContext"
3858
4050
  ]);
3859
4051
  function loadProfile(profilePath) {
3860
- if (!fs14.existsSync(profilePath)) {
4052
+ if (!fs16.existsSync(profilePath)) {
3861
4053
  throw new ProfileError(profilePath, "file not found");
3862
4054
  }
3863
4055
  let raw;
3864
4056
  try {
3865
- raw = JSON.parse(fs14.readFileSync(profilePath, "utf-8"));
4057
+ raw = JSON.parse(fs16.readFileSync(profilePath, "utf-8"));
3866
4058
  } catch (err) {
3867
4059
  throw new ProfileError(profilePath, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
3868
4060
  }
@@ -3873,7 +4065,7 @@ function loadProfile(profilePath) {
3873
4065
  const unknownKeys = Object.keys(r).filter((k) => !KNOWN_PROFILE_KEYS.has(k));
3874
4066
  if (unknownKeys.length > 0) {
3875
4067
  process.stderr.write(
3876
- `[kody profile] ${path13.basename(path13.dirname(profilePath))}: unknown top-level keys ignored: ${unknownKeys.join(", ")}
4068
+ `[kody profile] ${path15.basename(path15.dirname(profilePath))}: unknown top-level keys ignored: ${unknownKeys.join(", ")}
3877
4069
  `
3878
4070
  );
3879
4071
  }
@@ -3913,6 +4105,13 @@ function loadProfile(profilePath) {
3913
4105
  const profile = {
3914
4106
  name: requireString(profilePath, r, "name"),
3915
4107
  describe: typeof r.describe === "string" ? r.describe : "",
4108
+ // Optional persona to run as. Empty/blank string → undefined (no persona).
4109
+ staff: typeof r.staff === "string" && r.staff.trim() ? r.staff.trim() : void 0,
4110
+ // Optional recurrence cadence (scheduled duty). Blank → undefined (on-demand).
4111
+ every: typeof r.every === "string" && r.every.trim() ? r.every.trim() : void 0,
4112
+ // Locked-toolbox palette + mentions (folder-duty successors to frontmatter).
4113
+ dutyTools: Array.isArray(r.dutyTools) ? r.dutyTools.map((t) => String(t).trim()).filter(Boolean) : void 0,
4114
+ mentions: Array.isArray(r.mentions) ? r.mentions.map((m) => String(m).trim()).filter(Boolean) : void 0,
3916
4115
  role,
3917
4116
  kind,
3918
4117
  schedule: typeof r.schedule === "string" ? r.schedule : void 0,
@@ -3934,27 +4133,28 @@ function loadProfile(profilePath) {
3934
4133
  // Phase 5 in-process handoff opt-in. Default false; containers
3935
4134
  // flip to true after end-to-end verification.
3936
4135
  preloadContext: r.preloadContext === true,
3937
- dir: path13.dirname(profilePath),
3938
- promptTemplates: readPromptTemplates(path13.dirname(profilePath))
4136
+ dir: path15.dirname(profilePath),
4137
+ promptTemplates: readPromptTemplates(path15.dirname(profilePath))
3939
4138
  };
3940
4139
  if (lifecycle) {
3941
4140
  applyLifecycle(profile, profilePath);
3942
4141
  }
4142
+ profile.subagentTemplates = captureSubagentTemplates(profile);
3943
4143
  return profile;
3944
4144
  }
3945
4145
  function readPromptTemplates(dir) {
3946
4146
  const out = {};
3947
4147
  const read = (p) => {
3948
4148
  try {
3949
- out[p] = fs14.readFileSync(p, "utf-8");
4149
+ out[p] = fs16.readFileSync(p, "utf-8");
3950
4150
  } catch {
3951
4151
  }
3952
4152
  };
3953
- read(path13.join(dir, "prompt.md"));
4153
+ read(path15.join(dir, "prompt.md"));
3954
4154
  try {
3955
- const promptsDir = path13.join(dir, "prompts");
3956
- for (const ent of fs14.readdirSync(promptsDir)) {
3957
- if (ent.endsWith(".md")) read(path13.join(promptsDir, ent));
4155
+ const promptsDir = path15.join(dir, "prompts");
4156
+ for (const ent of fs16.readdirSync(promptsDir)) {
4157
+ if (ent.endsWith(".md")) read(path15.join(promptsDir, ent));
3958
4158
  }
3959
4159
  } catch {
3960
4160
  }
@@ -4294,16 +4494,17 @@ function parseStateComment(body) {
4294
4494
  flow: parsed.flow
4295
4495
  };
4296
4496
  }
4297
- function reduce(state, executable, action, phase) {
4497
+ function reduce(state, executable, action, phase, staff) {
4298
4498
  if (!action) return state;
4299
4499
  const newAttempts = { ...state.core.attempts, [executable]: (state.core.attempts[executable] ?? 0) + 1 };
4300
4500
  const newExecutables = {
4301
4501
  ...state.executables,
4302
4502
  [executable]: { ...state.executables[executable] ?? { lastAction: null }, lastAction: action }
4303
4503
  };
4504
+ const ranAsStaff = typeof staff === "string" && staff.length > 0 ? staff : void 0;
4304
4505
  const newHistory = [
4305
4506
  ...state.history,
4306
- { timestamp: action.timestamp, executable, action: action.type, note: noteFromAction(action) }
4507
+ { timestamp: action.timestamp, executable, action: action.type, note: noteFromAction(action), staff: ranAsStaff }
4307
4508
  ].slice(-HISTORY_MAX_ENTRIES);
4308
4509
  return {
4309
4510
  schemaVersion: 1,
@@ -4312,6 +4513,7 @@ function reduce(state, executable, action, phase) {
4312
4513
  attempts: newAttempts,
4313
4514
  lastOutcome: action,
4314
4515
  currentExecutable: executable,
4516
+ ranAsStaff: ranAsStaff ?? null,
4315
4517
  status: statusFromAction(action),
4316
4518
  phase: phaseFromAction(action, phase)
4317
4519
  },
@@ -4348,6 +4550,9 @@ function renderStateComment(state) {
4348
4550
  if (state.core.currentExecutable) {
4349
4551
  lines.push(`- **Last executable:** \`${state.core.currentExecutable}\``);
4350
4552
  }
4553
+ if (state.core.ranAsStaff) {
4554
+ lines.push(`- **Ran as:** \`${state.core.ranAsStaff}\``);
4555
+ }
4351
4556
  if (state.core.lastOutcome) {
4352
4557
  lines.push(`- **Last action:** \`${state.core.lastOutcome.type}\``);
4353
4558
  }
@@ -4430,7 +4635,7 @@ var CONTAINER_MAX_ITERATIONS = 50;
4430
4635
  function getProfileInputsForChild(profileName, _cwd) {
4431
4636
  try {
4432
4637
  const profilePath = resolveProfilePath(profileName);
4433
- if (!fs17.existsSync(profilePath)) return null;
4638
+ if (!fs19.existsSync(profilePath)) return null;
4434
4639
  return loadProfile(profilePath).inputs;
4435
4640
  } catch {
4436
4641
  return null;
@@ -4874,9 +5079,9 @@ function errMsg(err) {
4874
5079
 
4875
5080
  // src/litellm.ts
4876
5081
  import { execFileSync as execFileSync6, spawn as spawn3 } from "child_process";
4877
- import * as fs18 from "fs";
4878
- import * as os2 from "os";
4879
- import * as path16 from "path";
5082
+ import * as fs20 from "fs";
5083
+ import * as os3 from "os";
5084
+ import * as path18 from "path";
4880
5085
  async function checkLitellmHealth(url) {
4881
5086
  try {
4882
5087
  const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
@@ -4928,13 +5133,13 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
4928
5133
  let child;
4929
5134
  let logPath;
4930
5135
  const spawnProxy = () => {
4931
- const configPath = path16.join(os2.tmpdir(), `kody-litellm-${Date.now()}.yaml`);
4932
- fs18.writeFileSync(configPath, generateLitellmConfigYaml(model));
5136
+ const configPath = path18.join(os3.tmpdir(), `kody-litellm-${Date.now()}.yaml`);
5137
+ fs20.writeFileSync(configPath, generateLitellmConfigYaml(model));
4933
5138
  const args = cmd === "litellm" ? ["--config", configPath, "--port", port] : ["-m", "litellm", "--config", configPath, "--port", port];
4934
- const nextLogPath = path16.join(os2.tmpdir(), `kody-litellm-${Date.now()}.log`);
4935
- const outFd = fs18.openSync(nextLogPath, "w");
5139
+ const nextLogPath = path18.join(os3.tmpdir(), `kody-litellm-${Date.now()}.log`);
5140
+ const outFd = fs20.openSync(nextLogPath, "w");
4936
5141
  child = spawn3(cmd, args, { stdio: ["ignore", outFd, outFd], detached: true, env: childEnv });
4937
- fs18.closeSync(outFd);
5142
+ fs20.closeSync(outFd);
4938
5143
  logPath = nextLogPath;
4939
5144
  };
4940
5145
  const waitForHealth = async () => {
@@ -4948,7 +5153,7 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
4948
5153
  const readLogTail = () => {
4949
5154
  if (!logPath) return "";
4950
5155
  try {
4951
- return fs18.readFileSync(logPath, "utf-8").slice(-2e3);
5156
+ return fs20.readFileSync(logPath, "utf-8").slice(-2e3);
4952
5157
  } catch {
4953
5158
  return "";
4954
5159
  }
@@ -5000,10 +5205,10 @@ ${tail}`
5000
5205
  return { url, kill: killChild, isHealthy, ensureHealthy };
5001
5206
  }
5002
5207
  function readDotenvApiKeys(projectDir) {
5003
- const dotenvPath = path16.join(projectDir, ".env");
5004
- if (!fs18.existsSync(dotenvPath)) return {};
5208
+ const dotenvPath = path18.join(projectDir, ".env");
5209
+ if (!fs20.existsSync(dotenvPath)) return {};
5005
5210
  const result = {};
5006
- for (const rawLine of fs18.readFileSync(dotenvPath, "utf-8").split("\n")) {
5211
+ for (const rawLine of fs20.readFileSync(dotenvPath, "utf-8").split("\n")) {
5007
5212
  const line = rawLine.trim();
5008
5213
  if (!line || line.startsWith("#")) continue;
5009
5214
  const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
@@ -5105,8 +5310,8 @@ function pushWithRetry(opts = {}) {
5105
5310
  }
5106
5311
 
5107
5312
  // src/commit.ts
5108
- import * as fs19 from "fs";
5109
- import * as path17 from "path";
5313
+ import * as fs21 from "fs";
5314
+ import * as path19 from "path";
5110
5315
  var FORBIDDEN_PATH_PREFIXES = [
5111
5316
  ".kody/",
5112
5317
  ".kody-engine/",
@@ -5167,18 +5372,18 @@ function tryGit(args, cwd) {
5167
5372
  }
5168
5373
  function abortUnfinishedGitOps(cwd) {
5169
5374
  const aborted = [];
5170
- const gitDir = path17.join(cwd ?? process.cwd(), ".git");
5171
- if (!fs19.existsSync(gitDir)) return aborted;
5172
- if (fs19.existsSync(path17.join(gitDir, "MERGE_HEAD"))) {
5375
+ const gitDir = path19.join(cwd ?? process.cwd(), ".git");
5376
+ if (!fs21.existsSync(gitDir)) return aborted;
5377
+ if (fs21.existsSync(path19.join(gitDir, "MERGE_HEAD"))) {
5173
5378
  if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
5174
5379
  }
5175
- if (fs19.existsSync(path17.join(gitDir, "CHERRY_PICK_HEAD"))) {
5380
+ if (fs21.existsSync(path19.join(gitDir, "CHERRY_PICK_HEAD"))) {
5176
5381
  if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
5177
5382
  }
5178
- if (fs19.existsSync(path17.join(gitDir, "REVERT_HEAD"))) {
5383
+ if (fs21.existsSync(path19.join(gitDir, "REVERT_HEAD"))) {
5179
5384
  if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
5180
5385
  }
5181
- if (fs19.existsSync(path17.join(gitDir, "rebase-merge")) || fs19.existsSync(path17.join(gitDir, "rebase-apply"))) {
5386
+ if (fs21.existsSync(path19.join(gitDir, "rebase-merge")) || fs21.existsSync(path19.join(gitDir, "rebase-apply"))) {
5182
5387
  if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
5183
5388
  }
5184
5389
  try {
@@ -5234,7 +5439,7 @@ function normalizeCommitMessage(raw) {
5234
5439
  function commitAndPush(branch, agentMessage, cwd) {
5235
5440
  const allChanged = listChangedFiles(cwd);
5236
5441
  const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
5237
- const mergeHeadExists = fs19.existsSync(path17.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
5442
+ const mergeHeadExists = fs21.existsSync(path19.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
5238
5443
  if (allowedFiles.length === 0 && !mergeHeadExists) {
5239
5444
  return { committed: false, pushed: false, sha: "", message: "" };
5240
5445
  }
@@ -5325,7 +5530,7 @@ var advanceFlow = async (ctx, profile) => {
5325
5530
  const action = ctx.data.action;
5326
5531
  let nextIssueState = issueState;
5327
5532
  if (targetType === "pr" && action) {
5328
- nextIssueState = reduce(issueState, profile.name, action, profile.phase);
5533
+ nextIssueState = reduce(issueState, profile.name, action, profile.phase, profile.staff);
5329
5534
  if (state?.core.prUrl && !nextIssueState.core.prUrl) nextIssueState.core.prUrl = state.core.prUrl;
5330
5535
  }
5331
5536
  const prevHops = issueState.flow?.hops ?? flow.hops ?? 0;
@@ -5366,7 +5571,7 @@ var advanceFlow = async (ctx, profile) => {
5366
5571
 
5367
5572
  // src/gha.ts
5368
5573
  import { execFileSync as execFileSync10 } from "child_process";
5369
- import * as fs20 from "fs";
5574
+ import * as fs22 from "fs";
5370
5575
  function getRunUrl() {
5371
5576
  const server = process.env.GITHUB_SERVER_URL;
5372
5577
  const repo = process.env.GITHUB_REPOSITORY;
@@ -5377,10 +5582,10 @@ function getRunUrl() {
5377
5582
  function reactToTriggerComment(cwd) {
5378
5583
  if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
5379
5584
  const eventPath = process.env.GITHUB_EVENT_PATH;
5380
- if (!eventPath || !fs20.existsSync(eventPath)) return;
5585
+ if (!eventPath || !fs22.existsSync(eventPath)) return;
5381
5586
  let event = null;
5382
5587
  try {
5383
- event = JSON.parse(fs20.readFileSync(eventPath, "utf-8"));
5588
+ event = JSON.parse(fs22.readFileSync(eventPath, "utf-8"));
5384
5589
  } catch {
5385
5590
  return;
5386
5591
  }
@@ -5532,22 +5737,22 @@ var appendCompanyActivity = async (ctx, _profile, agentResult) => {
5532
5737
  };
5533
5738
 
5534
5739
  // src/scripts/brainServe.ts
5535
- import * as fs22 from "fs";
5740
+ import * as fs24 from "fs";
5536
5741
  import { createServer } from "http";
5537
- import * as path19 from "path";
5742
+ import * as path21 from "path";
5538
5743
  init_repoWorkspace();
5539
5744
 
5540
5745
  // src/scripts/brainTurnLog.ts
5541
- import * as fs21 from "fs";
5542
- import * as path18 from "path";
5746
+ import * as fs23 from "fs";
5747
+ import * as path20 from "path";
5543
5748
  var live = /* @__PURE__ */ new Map();
5544
5749
  function eventsPath(dir, chatId) {
5545
- return path18.join(dir, ".kody", "brain-events", `${chatId}.jsonl`);
5750
+ return path20.join(dir, ".kody", "brain-events", `${chatId}.jsonl`);
5546
5751
  }
5547
5752
  function lastPersistedSeq(dir, chatId) {
5548
5753
  const p = eventsPath(dir, chatId);
5549
- if (!fs21.existsSync(p)) return 0;
5550
- const lines = fs21.readFileSync(p, "utf-8").split("\n").filter(Boolean);
5754
+ if (!fs23.existsSync(p)) return 0;
5755
+ const lines = fs23.readFileSync(p, "utf-8").split("\n").filter(Boolean);
5551
5756
  if (lines.length === 0) return 0;
5552
5757
  try {
5553
5758
  return JSON.parse(lines[lines.length - 1]).seq || 0;
@@ -5557,9 +5762,9 @@ function lastPersistedSeq(dir, chatId) {
5557
5762
  }
5558
5763
  function readSince(dir, chatId, since) {
5559
5764
  const p = eventsPath(dir, chatId);
5560
- if (!fs21.existsSync(p)) return [];
5765
+ if (!fs23.existsSync(p)) return [];
5561
5766
  const out = [];
5562
- for (const line of fs21.readFileSync(p, "utf-8").split("\n")) {
5767
+ for (const line of fs23.readFileSync(p, "utf-8").split("\n")) {
5563
5768
  if (!line) continue;
5564
5769
  try {
5565
5770
  const rec = JSON.parse(line);
@@ -5585,12 +5790,12 @@ function beginTurn(dir, chatId) {
5585
5790
  };
5586
5791
  live.set(chatId, state);
5587
5792
  const p = eventsPath(dir, chatId);
5588
- fs21.mkdirSync(path18.dirname(p), { recursive: true });
5793
+ fs23.mkdirSync(path20.dirname(p), { recursive: true });
5589
5794
  return (event) => {
5590
5795
  state.seq += 1;
5591
5796
  const rec = { seq: state.seq, turn, ts: Date.now(), event };
5592
5797
  try {
5593
- fs21.appendFileSync(p, `${JSON.stringify(rec)}
5798
+ fs23.appendFileSync(p, `${JSON.stringify(rec)}
5594
5799
  `);
5595
5800
  } catch (err) {
5596
5801
  process.stderr.write(
@@ -5629,7 +5834,7 @@ function endTurnIfUnterminated(dir, chatId, errMessage) {
5629
5834
  event: { type: "error", error: errMessage || "turn ended unexpectedly", chatId }
5630
5835
  };
5631
5836
  try {
5632
- fs21.appendFileSync(eventsPath(dir, chatId), `${JSON.stringify(rec)}
5837
+ fs23.appendFileSync(eventsPath(dir, chatId), `${JSON.stringify(rec)}
5633
5838
  `);
5634
5839
  } catch {
5635
5840
  }
@@ -5860,7 +6065,7 @@ async function handleChatTurn(req, res, chatId, opts) {
5860
6065
  const repo = strField(body, "repo");
5861
6066
  const repoToken = strField(body, "repoToken");
5862
6067
  const sessionFile = sessionFilePath(opts.cwd, chatId);
5863
- fs22.mkdirSync(path19.dirname(sessionFile), { recursive: true });
6068
+ fs24.mkdirSync(path21.dirname(sessionFile), { recursive: true });
5864
6069
  appendTurn(sessionFile, {
5865
6070
  role: "user",
5866
6071
  content: message,
@@ -5907,7 +6112,7 @@ async function handleChatTurn(req, res, chatId, opts) {
5907
6112
  function buildServer(opts) {
5908
6113
  const runTurn = opts.runTurn ?? runChatTurn;
5909
6114
  const cloneRepo = opts.cloneRepo ?? defaultCloneRepo;
5910
- const reposRoot = opts.reposRoot ?? path19.join(path19.dirname(path19.resolve(opts.cwd)), "repos");
6115
+ const reposRoot = opts.reposRoot ?? path21.join(path21.dirname(path21.resolve(opts.cwd)), "repos");
5911
6116
  return createServer(async (req, res) => {
5912
6117
  if (!req.method || !req.url) {
5913
6118
  sendJson(res, 400, { error: "bad request" });
@@ -6008,93 +6213,6 @@ var brainServe = async (ctx) => {
6008
6213
  });
6009
6214
  };
6010
6215
 
6011
- // src/scripts/buildSyntheticPlugin.ts
6012
- import * as fs23 from "fs";
6013
- import * as os3 from "os";
6014
- import * as path20 from "path";
6015
- function getPluginsCatalogRoot() {
6016
- const here = path20.dirname(new URL(import.meta.url).pathname);
6017
- const candidates = [
6018
- path20.join(here, "..", "plugins"),
6019
- // dev: src/scripts → src/plugins
6020
- path20.join(here, "..", "..", "plugins"),
6021
- // built: dist/scripts → dist/plugins
6022
- path20.join(here, "..", "..", "src", "plugins")
6023
- // fallback
6024
- ];
6025
- for (const c of candidates) {
6026
- if (fs23.existsSync(c) && fs23.statSync(c).isDirectory()) return c;
6027
- }
6028
- return candidates[0];
6029
- }
6030
- var buildSyntheticPlugin = async (ctx, profile) => {
6031
- const cc = profile.claudeCode;
6032
- const needsSynthetic = cc.skills.length > 0 || cc.commands.length > 0 || cc.hooks.length > 0;
6033
- if (!needsSynthetic) return;
6034
- const catalog = getPluginsCatalogRoot();
6035
- const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
6036
- const root = path20.join(os3.tmpdir(), `kody-synth-${runId}`);
6037
- fs23.mkdirSync(path20.join(root, ".claude-plugin"), { recursive: true });
6038
- const resolvePart = (bucket, entry) => {
6039
- const local = path20.join(profile.dir, bucket, entry);
6040
- if (fs23.existsSync(local)) return local;
6041
- const central = path20.join(catalog, bucket, entry);
6042
- if (fs23.existsSync(central)) return central;
6043
- throw new Error(
6044
- `buildSyntheticPlugin: ${bucket} entry '${entry}' not found in executable dir (${profile.dir}/${bucket}/) or catalog (${catalog}/${bucket}/)`
6045
- );
6046
- };
6047
- if (cc.skills.length > 0) {
6048
- const dst = path20.join(root, "skills");
6049
- fs23.mkdirSync(dst, { recursive: true });
6050
- for (const name of cc.skills) {
6051
- copyDir(resolvePart("skills", name), path20.join(dst, name));
6052
- }
6053
- }
6054
- if (cc.commands.length > 0) {
6055
- const dst = path20.join(root, "commands");
6056
- fs23.mkdirSync(dst, { recursive: true });
6057
- for (const name of cc.commands) {
6058
- fs23.copyFileSync(resolvePart("commands", `${name}.md`), path20.join(dst, `${name}.md`));
6059
- }
6060
- }
6061
- if (cc.hooks.length > 0) {
6062
- const dst = path20.join(root, "hooks");
6063
- fs23.mkdirSync(dst, { recursive: true });
6064
- const merged = { hooks: {} };
6065
- for (const name of cc.hooks) {
6066
- const src = resolvePart("hooks", `${name}.json`);
6067
- const parsed = JSON.parse(fs23.readFileSync(src, "utf-8"));
6068
- for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
6069
- if (!Array.isArray(entries)) continue;
6070
- if (!merged.hooks[event]) merged.hooks[event] = [];
6071
- merged.hooks[event].push(...entries);
6072
- }
6073
- }
6074
- fs23.writeFileSync(path20.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
6075
- `);
6076
- }
6077
- const manifest = {
6078
- name: `kody-synth-${profile.name}`,
6079
- version: "1.0.0",
6080
- description: `Synthetic plugin assembled by Kody for profile '${profile.name}' at runtime.`
6081
- };
6082
- if (cc.skills.length > 0) manifest.skills = ["./skills/"];
6083
- if (cc.commands.length > 0) manifest.commands = ["./commands/"];
6084
- fs23.writeFileSync(path20.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
6085
- `);
6086
- ctx.data.syntheticPluginPath = root;
6087
- };
6088
- function copyDir(src, dst) {
6089
- fs23.mkdirSync(dst, { recursive: true });
6090
- for (const ent of fs23.readdirSync(src, { withFileTypes: true })) {
6091
- const s = path20.join(src, ent.name);
6092
- const d = path20.join(dst, ent.name);
6093
- if (ent.isDirectory()) copyDir(s, d);
6094
- else if (ent.isFile()) fs23.copyFileSync(s, d);
6095
- }
6096
- }
6097
-
6098
6216
  // src/coverage.ts
6099
6217
  import { execFileSync as execFileSync11 } from "child_process";
6100
6218
  function patternToRegex(pattern) {
@@ -6243,13 +6361,13 @@ function defaultLabelMap() {
6243
6361
  }
6244
6362
 
6245
6363
  // src/scripts/commitAndPush.ts
6246
- import * as fs24 from "fs";
6247
- import * as path21 from "path";
6364
+ import * as fs25 from "fs";
6365
+ import * as path22 from "path";
6248
6366
  init_events();
6249
6367
  var DEFAULT_COMMIT_MESSAGE = "chore: kody changes";
6250
6368
  function sentinelPathForStage(cwd, profileName) {
6251
6369
  const runId = resolveRunId();
6252
- return path21.join(cwd, ".kody", "runs", runId, `commit-${profileName}.lock`);
6370
+ return path22.join(cwd, ".kody", "runs", runId, `commit-${profileName}.lock`);
6253
6371
  }
6254
6372
  var commitAndPush2 = async (ctx, profile) => {
6255
6373
  const branch = ctx.data.branch;
@@ -6259,9 +6377,9 @@ var commitAndPush2 = async (ctx, profile) => {
6259
6377
  }
6260
6378
  const idempotencyEnabled = process.env.KODY_COMMIT_IDEMPOTENCY !== "0";
6261
6379
  const sentinel = idempotencyEnabled ? sentinelPathForStage(ctx.cwd, profile.name) : null;
6262
- if (sentinel && fs24.existsSync(sentinel)) {
6380
+ if (sentinel && fs25.existsSync(sentinel)) {
6263
6381
  try {
6264
- const replay = JSON.parse(fs24.readFileSync(sentinel, "utf-8"));
6382
+ const replay = JSON.parse(fs25.readFileSync(sentinel, "utf-8"));
6265
6383
  ctx.data.commitResult = replay.commitResult ?? { committed: false, pushed: false };
6266
6384
  if (Array.isArray(replay.changedFiles)) ctx.data.changedFiles = replay.changedFiles;
6267
6385
  if (typeof replay.hasCommitsAhead === "boolean") ctx.data.hasCommitsAhead = replay.hasCommitsAhead;
@@ -6314,8 +6432,8 @@ var commitAndPush2 = async (ctx, profile) => {
6314
6432
  const result = ctx.data.commitResult;
6315
6433
  if (sentinel && result?.committed) {
6316
6434
  try {
6317
- fs24.mkdirSync(path21.dirname(sentinel), { recursive: true });
6318
- fs24.writeFileSync(
6435
+ fs25.mkdirSync(path22.dirname(sentinel), { recursive: true });
6436
+ fs25.writeFileSync(
6319
6437
  sentinel,
6320
6438
  JSON.stringify(
6321
6439
  {
@@ -6338,14 +6456,14 @@ var commitAndPush2 = async (ctx, profile) => {
6338
6456
  init_issue();
6339
6457
 
6340
6458
  // src/goal/state.ts
6341
- import * as fs25 from "fs";
6342
- import * as path22 from "path";
6459
+ import * as fs26 from "fs";
6460
+ import * as path23 from "path";
6343
6461
  var VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "awaiting-merge", "done"]);
6344
6462
  var GoalStateError = class extends Error {
6345
- constructor(path41, message) {
6346
- super(`Invalid goal state at ${path41}:
6463
+ constructor(path42, message) {
6464
+ super(`Invalid goal state at ${path42}:
6347
6465
  ${message}`);
6348
- this.path = path41;
6466
+ this.path = path42;
6349
6467
  this.name = "GoalStateError";
6350
6468
  }
6351
6469
  path;
@@ -6485,16 +6603,16 @@ function describeCommitMessage(goal) {
6485
6603
  }
6486
6604
 
6487
6605
  // src/scripts/composePrompt.ts
6488
- import * as fs26 from "fs";
6489
- import * as path23 from "path";
6606
+ import * as fs27 from "fs";
6607
+ import * as path24 from "path";
6490
6608
  var MUSTACHE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
6491
6609
  var composePrompt = async (ctx, profile) => {
6492
6610
  const explicit = ctx.data.promptTemplate;
6493
6611
  const mode = ctx.args.mode;
6494
6612
  const candidates = [
6495
- explicit ? path23.join(profile.dir, explicit) : null,
6496
- mode ? path23.join(profile.dir, "prompts", `${mode}.md`) : null,
6497
- path23.join(profile.dir, "prompt.md")
6613
+ explicit ? path24.join(profile.dir, explicit) : null,
6614
+ mode ? path24.join(profile.dir, "prompts", `${mode}.md`) : null,
6615
+ path24.join(profile.dir, "prompt.md")
6498
6616
  ].filter(Boolean);
6499
6617
  let templatePath = "";
6500
6618
  let template = "";
@@ -6507,7 +6625,7 @@ var composePrompt = async (ctx, profile) => {
6507
6625
  break;
6508
6626
  }
6509
6627
  try {
6510
- template = fs26.readFileSync(c, "utf-8");
6628
+ template = fs27.readFileSync(c, "utf-8");
6511
6629
  templatePath = c;
6512
6630
  break;
6513
6631
  } catch (err) {
@@ -6518,7 +6636,7 @@ var composePrompt = async (ctx, profile) => {
6518
6636
  if (!templatePath) {
6519
6637
  let dirState;
6520
6638
  try {
6521
- dirState = `dir contents: [${fs26.readdirSync(profile.dir).join(", ")}]`;
6639
+ dirState = `dir contents: [${fs27.readdirSync(profile.dir).join(", ")}]`;
6522
6640
  } catch (err) {
6523
6641
  dirState = `readdir(${profile.dir}) failed: ${err?.code ?? String(err)}`;
6524
6642
  }
@@ -7343,15 +7461,15 @@ var deriveQaScopeFromIssue = async (ctx) => {
7343
7461
 
7344
7462
  // src/scripts/diagMcp.ts
7345
7463
  import { execFileSync as execFileSync12 } from "child_process";
7346
- import * as fs27 from "fs";
7464
+ import * as fs28 from "fs";
7347
7465
  import * as os4 from "os";
7348
- import * as path24 from "path";
7466
+ import * as path25 from "path";
7349
7467
  var diagMcp = async (_ctx) => {
7350
7468
  const home = os4.homedir();
7351
- const cacheDir = path24.join(home, ".cache", "ms-playwright");
7469
+ const cacheDir = path25.join(home, ".cache", "ms-playwright");
7352
7470
  let entries = [];
7353
7471
  try {
7354
- entries = fs27.readdirSync(cacheDir);
7472
+ entries = fs28.readdirSync(cacheDir);
7355
7473
  } catch {
7356
7474
  }
7357
7475
  const hasChromium = entries.some((e) => e.startsWith("chromium"));
@@ -7377,17 +7495,17 @@ var diagMcp = async (_ctx) => {
7377
7495
  };
7378
7496
 
7379
7497
  // src/scripts/discoverQaContext.ts
7380
- import * as fs29 from "fs";
7381
- import * as path26 from "path";
7498
+ import * as fs30 from "fs";
7499
+ import * as path27 from "path";
7382
7500
 
7383
7501
  // src/scripts/frameworkDetectors.ts
7384
- import * as fs28 from "fs";
7385
- import * as path25 from "path";
7502
+ import * as fs29 from "fs";
7503
+ import * as path26 from "path";
7386
7504
  function detectFrameworks(cwd) {
7387
7505
  const out = [];
7388
7506
  let deps = {};
7389
7507
  try {
7390
- const pkg = JSON.parse(fs28.readFileSync(path25.join(cwd, "package.json"), "utf-8"));
7508
+ const pkg = JSON.parse(fs29.readFileSync(path26.join(cwd, "package.json"), "utf-8"));
7391
7509
  deps = { ...pkg.dependencies, ...pkg.devDependencies };
7392
7510
  } catch {
7393
7511
  return out;
@@ -7424,7 +7542,7 @@ function detectFrameworks(cwd) {
7424
7542
  }
7425
7543
  function findFile(cwd, candidates) {
7426
7544
  for (const c of candidates) {
7427
- if (fs28.existsSync(path25.join(cwd, c))) return c;
7545
+ if (fs29.existsSync(path26.join(cwd, c))) return c;
7428
7546
  }
7429
7547
  return null;
7430
7548
  }
@@ -7437,18 +7555,18 @@ var COLLECTION_DIRS = [
7437
7555
  function discoverPayloadCollections(cwd) {
7438
7556
  const out = [];
7439
7557
  for (const dir of COLLECTION_DIRS) {
7440
- const full = path25.join(cwd, dir);
7441
- if (!fs28.existsSync(full)) continue;
7558
+ const full = path26.join(cwd, dir);
7559
+ if (!fs29.existsSync(full)) continue;
7442
7560
  let files;
7443
7561
  try {
7444
- files = fs28.readdirSync(full).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
7562
+ files = fs29.readdirSync(full).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
7445
7563
  } catch {
7446
7564
  continue;
7447
7565
  }
7448
7566
  for (const file of files) {
7449
7567
  try {
7450
- const filePath = path25.join(full, file);
7451
- const content = fs28.readFileSync(filePath, "utf-8").slice(0, 1e4);
7568
+ const filePath = path26.join(full, file);
7569
+ const content = fs29.readFileSync(filePath, "utf-8").slice(0, 1e4);
7452
7570
  const slugMatch = content.match(/slug:\s*['"]([a-z0-9-]+)['"]/);
7453
7571
  if (!slugMatch) continue;
7454
7572
  const slug = slugMatch[1];
@@ -7462,7 +7580,7 @@ function discoverPayloadCollections(cwd) {
7462
7580
  out.push({
7463
7581
  name,
7464
7582
  slug,
7465
- filePath: path25.relative(cwd, filePath),
7583
+ filePath: path26.relative(cwd, filePath),
7466
7584
  fields: fields.slice(0, 20),
7467
7585
  hasAdmin
7468
7586
  });
@@ -7476,28 +7594,28 @@ var ADMIN_COMPONENT_DIRS = ["src/ui/admin", "src/admin/components", "src/compone
7476
7594
  function discoverAdminComponents(cwd, collections) {
7477
7595
  const out = [];
7478
7596
  for (const dir of ADMIN_COMPONENT_DIRS) {
7479
- const full = path25.join(cwd, dir);
7480
- if (!fs28.existsSync(full)) continue;
7597
+ const full = path26.join(cwd, dir);
7598
+ if (!fs29.existsSync(full)) continue;
7481
7599
  let entries;
7482
7600
  try {
7483
- entries = fs28.readdirSync(full, { withFileTypes: true });
7601
+ entries = fs29.readdirSync(full, { withFileTypes: true });
7484
7602
  } catch {
7485
7603
  continue;
7486
7604
  }
7487
7605
  for (const entry of entries) {
7488
- const entryPath = path25.join(full, entry.name);
7606
+ const entryPath = path26.join(full, entry.name);
7489
7607
  let name;
7490
7608
  let filePath;
7491
7609
  if (entry.isDirectory()) {
7492
7610
  const indexFile = ["index.tsx", "index.ts", "index.jsx", "index.js"].find(
7493
- (f) => fs28.existsSync(path25.join(entryPath, f))
7611
+ (f) => fs29.existsSync(path26.join(entryPath, f))
7494
7612
  );
7495
7613
  if (!indexFile) continue;
7496
7614
  name = entry.name;
7497
- filePath = path25.relative(cwd, path25.join(entryPath, indexFile));
7615
+ filePath = path26.relative(cwd, path26.join(entryPath, indexFile));
7498
7616
  } else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
7499
7617
  name = entry.name.replace(/\.(tsx?|jsx?)$/, "");
7500
- filePath = path25.relative(cwd, entryPath);
7618
+ filePath = path26.relative(cwd, entryPath);
7501
7619
  } else {
7502
7620
  continue;
7503
7621
  }
@@ -7505,7 +7623,7 @@ function discoverAdminComponents(cwd, collections) {
7505
7623
  if (collections) {
7506
7624
  for (const col of collections) {
7507
7625
  try {
7508
- const colContent = fs28.readFileSync(path25.join(cwd, col.filePath), "utf-8");
7626
+ const colContent = fs29.readFileSync(path26.join(cwd, col.filePath), "utf-8");
7509
7627
  if (colContent.includes(name)) {
7510
7628
  usedInCollection = col.slug;
7511
7629
  break;
@@ -7524,8 +7642,8 @@ function scanApiRoutes(cwd) {
7524
7642
  const out = [];
7525
7643
  const appDirs = ["src/app", "app"];
7526
7644
  for (const appDir of appDirs) {
7527
- const apiDir = path25.join(cwd, appDir, "api");
7528
- if (!fs28.existsSync(apiDir)) continue;
7645
+ const apiDir = path26.join(cwd, appDir, "api");
7646
+ if (!fs29.existsSync(apiDir)) continue;
7529
7647
  walkApiRoutes(apiDir, "/api", cwd, out);
7530
7648
  break;
7531
7649
  }
@@ -7534,14 +7652,14 @@ function scanApiRoutes(cwd) {
7534
7652
  function walkApiRoutes(dir, prefix, cwd, out) {
7535
7653
  let entries;
7536
7654
  try {
7537
- entries = fs28.readdirSync(dir, { withFileTypes: true });
7655
+ entries = fs29.readdirSync(dir, { withFileTypes: true });
7538
7656
  } catch {
7539
7657
  return;
7540
7658
  }
7541
7659
  const routeFile = entries.find((e) => e.isFile() && /^route\.(ts|js|tsx|jsx)$/.test(e.name));
7542
7660
  if (routeFile) {
7543
7661
  try {
7544
- const content = fs28.readFileSync(path25.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
7662
+ const content = fs29.readFileSync(path26.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
7545
7663
  const methods = HTTP_METHODS.filter(
7546
7664
  (m) => new RegExp(`export\\s+(?:async\\s+)?function\\s+${m}\\b`).test(content)
7547
7665
  );
@@ -7549,7 +7667,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
7549
7667
  out.push({
7550
7668
  path: prefix,
7551
7669
  methods,
7552
- filePath: path25.relative(cwd, path25.join(dir, routeFile.name))
7670
+ filePath: path26.relative(cwd, path26.join(dir, routeFile.name))
7553
7671
  });
7554
7672
  }
7555
7673
  } catch {
@@ -7560,7 +7678,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
7560
7678
  if (entry.name === "node_modules" || entry.name === ".next") continue;
7561
7679
  let segment = entry.name;
7562
7680
  if (segment.startsWith("(") && segment.endsWith(")")) {
7563
- walkApiRoutes(path25.join(dir, entry.name), prefix, cwd, out);
7681
+ walkApiRoutes(path26.join(dir, entry.name), prefix, cwd, out);
7564
7682
  continue;
7565
7683
  }
7566
7684
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -7568,7 +7686,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
7568
7686
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
7569
7687
  segment = `:${segment.slice(1, -1)}`;
7570
7688
  }
7571
- walkApiRoutes(path25.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
7689
+ walkApiRoutes(path26.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
7572
7690
  }
7573
7691
  }
7574
7692
  var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
@@ -7588,10 +7706,10 @@ var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
7588
7706
  function scanEnvVars(cwd) {
7589
7707
  const candidates = [".env.example", ".env.local.example", ".env.template"];
7590
7708
  for (const envFile of candidates) {
7591
- const envPath = path25.join(cwd, envFile);
7592
- if (!fs28.existsSync(envPath)) continue;
7709
+ const envPath = path26.join(cwd, envFile);
7710
+ if (!fs29.existsSync(envPath)) continue;
7593
7711
  try {
7594
- const content = fs28.readFileSync(envPath, "utf-8");
7712
+ const content = fs29.readFileSync(envPath, "utf-8");
7595
7713
  const vars = [];
7596
7714
  for (const line of content.split("\n")) {
7597
7715
  const trimmed = line.trim();
@@ -7639,9 +7757,9 @@ function runQaDiscovery(cwd) {
7639
7757
  }
7640
7758
  function detectDevServer(cwd, out) {
7641
7759
  try {
7642
- const pkg = JSON.parse(fs29.readFileSync(path26.join(cwd, "package.json"), "utf-8"));
7760
+ const pkg = JSON.parse(fs30.readFileSync(path27.join(cwd, "package.json"), "utf-8"));
7643
7761
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
7644
- const pm = fs29.existsSync(path26.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs29.existsSync(path26.join(cwd, "yarn.lock")) ? "yarn" : fs29.existsSync(path26.join(cwd, "bun.lockb")) ? "bun" : "npm";
7762
+ const pm = fs30.existsSync(path27.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs30.existsSync(path27.join(cwd, "yarn.lock")) ? "yarn" : fs30.existsSync(path27.join(cwd, "bun.lockb")) ? "bun" : "npm";
7645
7763
  if (pkg.scripts?.dev) out.devCommand = `${pm} dev`;
7646
7764
  if (allDeps.next || allDeps.nuxt) out.devPort = 3e3;
7647
7765
  else if (allDeps.vite) out.devPort = 5173;
@@ -7651,8 +7769,8 @@ function detectDevServer(cwd, out) {
7651
7769
  function scanFrontendRoutes(cwd, out) {
7652
7770
  const appDirs = ["src/app", "app"];
7653
7771
  for (const appDir of appDirs) {
7654
- const full = path26.join(cwd, appDir);
7655
- if (!fs29.existsSync(full)) continue;
7772
+ const full = path27.join(cwd, appDir);
7773
+ if (!fs30.existsSync(full)) continue;
7656
7774
  walkFrontendRoutes(full, "", out);
7657
7775
  break;
7658
7776
  }
@@ -7660,7 +7778,7 @@ function scanFrontendRoutes(cwd, out) {
7660
7778
  function walkFrontendRoutes(dir, prefix, out) {
7661
7779
  let entries;
7662
7780
  try {
7663
- entries = fs29.readdirSync(dir, { withFileTypes: true });
7781
+ entries = fs30.readdirSync(dir, { withFileTypes: true });
7664
7782
  } catch {
7665
7783
  return;
7666
7784
  }
@@ -7677,7 +7795,7 @@ function walkFrontendRoutes(dir, prefix, out) {
7677
7795
  if (entry.name === "node_modules" || entry.name === ".next") continue;
7678
7796
  let segment = entry.name;
7679
7797
  if (segment.startsWith("(") && segment.endsWith(")")) {
7680
- walkFrontendRoutes(path26.join(dir, entry.name), prefix, out);
7798
+ walkFrontendRoutes(path27.join(dir, entry.name), prefix, out);
7681
7799
  continue;
7682
7800
  }
7683
7801
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -7685,7 +7803,7 @@ function walkFrontendRoutes(dir, prefix, out) {
7685
7803
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
7686
7804
  segment = `:${segment.slice(1, -1)}`;
7687
7805
  }
7688
- walkFrontendRoutes(path26.join(dir, entry.name), `${prefix}/${segment}`, out);
7806
+ walkFrontendRoutes(path27.join(dir, entry.name), `${prefix}/${segment}`, out);
7689
7807
  }
7690
7808
  }
7691
7809
  function detectAuthFiles(cwd, out) {
@@ -7702,23 +7820,23 @@ function detectAuthFiles(cwd, out) {
7702
7820
  "src/app/api/oauth"
7703
7821
  ];
7704
7822
  for (const c of candidates) {
7705
- if (fs29.existsSync(path26.join(cwd, c))) out.authFiles.push(c);
7823
+ if (fs30.existsSync(path27.join(cwd, c))) out.authFiles.push(c);
7706
7824
  }
7707
7825
  }
7708
7826
  function detectRoles(cwd, out) {
7709
7827
  const rolePaths = ["src/types", "src/lib", "src/utils", "src/constants", "src/access", "src/collections"];
7710
7828
  for (const rp of rolePaths) {
7711
- const dir = path26.join(cwd, rp);
7712
- if (!fs29.existsSync(dir)) continue;
7829
+ const dir = path27.join(cwd, rp);
7830
+ if (!fs30.existsSync(dir)) continue;
7713
7831
  let files;
7714
7832
  try {
7715
- files = fs29.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
7833
+ files = fs30.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
7716
7834
  } catch {
7717
7835
  continue;
7718
7836
  }
7719
7837
  for (const f of files) {
7720
7838
  try {
7721
- const content = fs29.readFileSync(path26.join(dir, f), "utf-8").slice(0, 5e3);
7839
+ const content = fs30.readFileSync(path27.join(dir, f), "utf-8").slice(0, 5e3);
7722
7840
  const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
7723
7841
  if (roleMatches) {
7724
7842
  for (const m of roleMatches) {
@@ -7901,8 +8019,8 @@ ${stateBody}`;
7901
8019
  };
7902
8020
 
7903
8021
  // src/scripts/dispatchJobFileTicks.ts
7904
- import * as fs31 from "fs";
7905
- import * as path28 from "path";
8022
+ import * as fs32 from "fs";
8023
+ import * as path29 from "path";
7906
8024
 
7907
8025
  // src/scripts/jobFrontmatter.ts
7908
8026
  var SCHEDULE_EVERY_VALUES = [
@@ -7918,7 +8036,7 @@ var SCHEDULE_EVERY_VALUES = [
7918
8036
  "manual"
7919
8037
  ];
7920
8038
  var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
7921
- function splitFrontmatter(raw) {
8039
+ function splitFrontmatter2(raw) {
7922
8040
  const match = FRONTMATTER_RE.exec(raw);
7923
8041
  if (!match) return { frontmatter: {}, body: raw };
7924
8042
  const inner = match[1] ?? "";
@@ -8195,8 +8313,8 @@ function isShaConflict(err) {
8195
8313
  }
8196
8314
 
8197
8315
  // src/scripts/jobState/localFileBackend.ts
8198
- import * as fs30 from "fs";
8199
- import * as path27 from "path";
8316
+ import * as fs31 from "fs";
8317
+ import * as path28 from "path";
8200
8318
  var LocalFileBackend = class {
8201
8319
  name = "local-file";
8202
8320
  cwd;
@@ -8211,7 +8329,7 @@ var LocalFileBackend = class {
8211
8329
  if (!opts.owner || !opts.repo) throw new Error("LocalFileBackend: owner and repo are required");
8212
8330
  this.cwd = opts.cwd;
8213
8331
  this.jobsDir = opts.jobsDir;
8214
- this.absDir = path27.join(opts.cwd, opts.jobsDir);
8332
+ this.absDir = path28.join(opts.cwd, opts.jobsDir);
8215
8333
  this.owner = opts.owner;
8216
8334
  this.repo = opts.repo;
8217
8335
  this.cache = opts.cache ?? defaultCacheAdapter();
@@ -8226,7 +8344,7 @@ var LocalFileBackend = class {
8226
8344
  `);
8227
8345
  return;
8228
8346
  }
8229
- fs30.mkdirSync(this.absDir, { recursive: true });
8347
+ fs31.mkdirSync(this.absDir, { recursive: true });
8230
8348
  const prefix = this.cacheKeyPrefix();
8231
8349
  const probeKey = `${prefix}probe-${Date.now()}`;
8232
8350
  try {
@@ -8255,7 +8373,7 @@ var LocalFileBackend = class {
8255
8373
  `);
8256
8374
  return;
8257
8375
  }
8258
- if (!fs30.existsSync(this.absDir)) {
8376
+ if (!fs31.existsSync(this.absDir)) {
8259
8377
  return;
8260
8378
  }
8261
8379
  const key = `${this.cacheKeyPrefix()}${process.env.GITHUB_RUN_ID ?? "norunid"}-${Date.now()}`;
@@ -8271,11 +8389,11 @@ var LocalFileBackend = class {
8271
8389
  }
8272
8390
  load(slug) {
8273
8391
  const relPath = stateFilePath(this.jobsDir, slug);
8274
- const absPath = path27.join(this.cwd, relPath);
8275
- if (!fs30.existsSync(absPath)) {
8392
+ const absPath = path28.join(this.cwd, relPath);
8393
+ if (!fs31.existsSync(absPath)) {
8276
8394
  return { path: relPath, handle: null, state: initialStateEnvelope("seed"), created: true };
8277
8395
  }
8278
- const raw = fs30.readFileSync(absPath, "utf-8");
8396
+ const raw = fs31.readFileSync(absPath, "utf-8");
8279
8397
  let parsed;
8280
8398
  try {
8281
8399
  parsed = JSON.parse(raw);
@@ -8292,13 +8410,13 @@ var LocalFileBackend = class {
8292
8410
  if (!loaded.created && isStateUnchanged(loaded.state, next)) {
8293
8411
  return false;
8294
8412
  }
8295
- const absPath = path27.join(this.cwd, loaded.path);
8296
- fs30.mkdirSync(path27.dirname(absPath), { recursive: true });
8413
+ const absPath = path28.join(this.cwd, loaded.path);
8414
+ fs31.mkdirSync(path28.dirname(absPath), { recursive: true });
8297
8415
  const body = `${JSON.stringify(next, null, 2)}
8298
8416
  `;
8299
8417
  const tmpPath = `${absPath}.${process.pid}.tmp`;
8300
- fs30.writeFileSync(tmpPath, body, "utf-8");
8301
- fs30.renameSync(tmpPath, absPath);
8418
+ fs31.writeFileSync(tmpPath, body, "utf-8");
8419
+ fs31.renameSync(tmpPath, absPath);
8302
8420
  return true;
8303
8421
  }
8304
8422
  cacheKeyPrefix() {
@@ -8376,7 +8494,7 @@ var dispatchJobFileTicks = async (ctx, _profile, args) => {
8376
8494
  await backend.hydrate();
8377
8495
  }
8378
8496
  try {
8379
- const slugs = listJobSlugs(path28.join(ctx.cwd, jobsDir));
8497
+ const slugs = listJobSlugs(path29.join(ctx.cwd, jobsDir));
8380
8498
  ctx.data.jobSlugCount = slugs.length;
8381
8499
  if (slugs.length === 0) {
8382
8500
  process.stdout.write(`[jobs] no job files in ${jobsDir}
@@ -8387,6 +8505,55 @@ var dispatchJobFileTicks = async (ctx, _profile, args) => {
8387
8505
  `);
8388
8506
  const results = [];
8389
8507
  const now = Date.now();
8508
+ const scheduledDuties = listFolderDutySlugs(path29.join(ctx.cwd, jobsDir)).map((slug) => {
8509
+ try {
8510
+ const p = loadProfile(path29.join(ctx.cwd, jobsDir, slug, "profile.json"));
8511
+ return { slug, every: p.every, staff: p.staff };
8512
+ } catch (err) {
8513
+ process.stderr.write(`[jobs] \u23ED skip folder-duty ${slug}: profile load failed: ${String(err)}
8514
+ `);
8515
+ return null;
8516
+ }
8517
+ }).filter((d) => d !== null && Boolean(d.every));
8518
+ process.stdout.write(`[jobs] ${scheduledDuties.length} scheduled folder-dut(y/ies) to consider
8519
+ `);
8520
+ for (const { slug, every, staff } of scheduledDuties) {
8521
+ if (!staff || staff.trim().length === 0) {
8522
+ process.stderr.write(`[jobs] \u23ED skip ${slug}: scheduled duty has no staff
8523
+ `);
8524
+ results.push({ slug, exitCode: 0, skipped: true, reason: "no staff assigned" });
8525
+ continue;
8526
+ }
8527
+ const decision = await decideShouldFire(every, slug, backend, now);
8528
+ if (decision.skip) {
8529
+ process.stdout.write(`[jobs] \u23ED skip ${slug}: ${decision.reason}
8530
+ `);
8531
+ results.push({ slug, exitCode: 0, skipped: true, reason: decision.reason });
8532
+ continue;
8533
+ }
8534
+ await stampFired(backend, slug, now);
8535
+ process.stdout.write(`[jobs] \u2192 run scheduled duty ${slug} (one-shot, as ${staff})
8536
+ `);
8537
+ try {
8538
+ const out = await runExecutable(slug, {
8539
+ cliArgs: {},
8540
+ cwd: ctx.cwd,
8541
+ config: ctx.config,
8542
+ verbose: ctx.verbose,
8543
+ quiet: ctx.quiet
8544
+ });
8545
+ results.push({ slug, exitCode: out.exitCode, reason: out.reason });
8546
+ if (out.exitCode !== 0) {
8547
+ process.stderr.write(`[jobs] scheduled duty ${slug} failed (exit ${out.exitCode}): ${out.reason ?? ""}
8548
+ `);
8549
+ }
8550
+ } catch (err) {
8551
+ const msg = err instanceof Error ? err.message : String(err);
8552
+ process.stderr.write(`[jobs] scheduled duty ${slug} crashed: ${msg}
8553
+ `);
8554
+ results.push({ slug, exitCode: 99, reason: msg });
8555
+ }
8556
+ }
8390
8557
  for (const slug of slugs) {
8391
8558
  const frontmatter = readJobFrontmatter(ctx.cwd, jobsDir, slug);
8392
8559
  if (frontmatter.disabled === true) {
@@ -8487,22 +8654,42 @@ function formatAgo(ms) {
8487
8654
  }
8488
8655
  function readJobFrontmatter(cwd, jobsDir, slug) {
8489
8656
  try {
8490
- const raw = fs31.readFileSync(path28.join(cwd, jobsDir, `${slug}.md`), "utf-8");
8491
- return splitFrontmatter(raw).frontmatter;
8657
+ const raw = fs32.readFileSync(path29.join(cwd, jobsDir, `${slug}.md`), "utf-8");
8658
+ return splitFrontmatter2(raw).frontmatter;
8492
8659
  } catch {
8493
8660
  return {};
8494
8661
  }
8495
8662
  }
8496
8663
  function listJobSlugs(absDir) {
8497
- if (!fs31.existsSync(absDir)) return [];
8664
+ if (!fs32.existsSync(absDir)) return [];
8498
8665
  let entries;
8499
8666
  try {
8500
- entries = fs31.readdirSync(absDir, { withFileTypes: true });
8667
+ entries = fs32.readdirSync(absDir, { withFileTypes: true });
8501
8668
  } catch {
8502
8669
  return [];
8503
8670
  }
8504
8671
  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();
8505
8672
  }
8673
+ function listFolderDutySlugs(absDir) {
8674
+ if (!fs32.existsSync(absDir)) return [];
8675
+ let entries;
8676
+ try {
8677
+ entries = fs32.readdirSync(absDir, { withFileTypes: true });
8678
+ } catch {
8679
+ return [];
8680
+ }
8681
+ return entries.filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).filter((e) => fs32.existsSync(path29.join(absDir, e.name, "profile.json"))).map((e) => e.name).sort();
8682
+ }
8683
+ async function stampFired(backend, slug, now) {
8684
+ try {
8685
+ const loaded = await backend.load(slug);
8686
+ const nextData = { ...loaded.state.data ?? {}, lastFiredAt: new Date(now).toISOString() };
8687
+ await backend.save(loaded, { ...loaded.state, data: nextData });
8688
+ } catch (err) {
8689
+ process.stderr.write(`[jobs] failed to stamp lastFiredAt for ${slug}: ${String(err)}
8690
+ `);
8691
+ }
8692
+ }
8506
8693
 
8507
8694
  // src/scripts/dispatchJobTicks.ts
8508
8695
  init_issue();
@@ -9556,12 +9743,12 @@ var handleAbandonedGoal = async (ctx) => {
9556
9743
 
9557
9744
  // src/scripts/initFlow.ts
9558
9745
  import { execFileSync as execFileSync18 } from "child_process";
9559
- import * as fs32 from "fs";
9560
- import * as path29 from "path";
9746
+ import * as fs33 from "fs";
9747
+ import * as path30 from "path";
9561
9748
  function detectPackageManager(cwd) {
9562
- if (fs32.existsSync(path29.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
9563
- if (fs32.existsSync(path29.join(cwd, "yarn.lock"))) return "yarn";
9564
- if (fs32.existsSync(path29.join(cwd, "bun.lockb"))) return "bun";
9749
+ if (fs33.existsSync(path30.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
9750
+ if (fs33.existsSync(path30.join(cwd, "yarn.lock"))) return "yarn";
9751
+ if (fs33.existsSync(path30.join(cwd, "bun.lockb"))) return "bun";
9565
9752
  return "npm";
9566
9753
  }
9567
9754
  function qualityCommandsFor(pm) {
@@ -9690,36 +9877,36 @@ function performInit(cwd, force) {
9690
9877
  const pm = detectPackageManager(cwd);
9691
9878
  const ownerRepo = detectOwnerRepo(cwd);
9692
9879
  const defaultBranch2 = defaultBranchFromGit(cwd);
9693
- const configPath = path29.join(cwd, "kody.config.json");
9694
- if (fs32.existsSync(configPath) && !force) {
9880
+ const configPath = path30.join(cwd, "kody.config.json");
9881
+ if (fs33.existsSync(configPath) && !force) {
9695
9882
  skipped.push("kody.config.json");
9696
9883
  } else {
9697
9884
  const cfg = makeConfig(pm, ownerRepo, defaultBranch2);
9698
- fs32.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
9885
+ fs33.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
9699
9886
  `);
9700
9887
  wrote.push("kody.config.json");
9701
9888
  }
9702
- const workflowDir = path29.join(cwd, ".github", "workflows");
9703
- const workflowPath = path29.join(workflowDir, "kody.yml");
9704
- if (fs32.existsSync(workflowPath) && !force) {
9889
+ const workflowDir = path30.join(cwd, ".github", "workflows");
9890
+ const workflowPath = path30.join(workflowDir, "kody.yml");
9891
+ if (fs33.existsSync(workflowPath) && !force) {
9705
9892
  skipped.push(".github/workflows/kody.yml");
9706
9893
  } else {
9707
- fs32.mkdirSync(workflowDir, { recursive: true });
9708
- fs32.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
9894
+ fs33.mkdirSync(workflowDir, { recursive: true });
9895
+ fs33.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
9709
9896
  wrote.push(".github/workflows/kody.yml");
9710
9897
  }
9711
9898
  const builtinJobs = listBuiltinJobs();
9712
9899
  if (builtinJobs.length > 0) {
9713
- const jobsDir = path29.join(cwd, ".kody", "duties");
9714
- fs32.mkdirSync(jobsDir, { recursive: true });
9900
+ const jobsDir = path30.join(cwd, ".kody", "duties");
9901
+ fs33.mkdirSync(jobsDir, { recursive: true });
9715
9902
  for (const job of builtinJobs) {
9716
- const rel = path29.join(".kody", "duties", `${job.slug}.md`);
9717
- const target = path29.join(cwd, rel);
9718
- if (fs32.existsSync(target) && !force) {
9903
+ const rel = path30.join(".kody", "duties", `${job.slug}.md`);
9904
+ const target = path30.join(cwd, rel);
9905
+ if (fs33.existsSync(target) && !force) {
9719
9906
  skipped.push(rel);
9720
9907
  continue;
9721
9908
  }
9722
- fs32.writeFileSync(target, fs32.readFileSync(job.filePath, "utf-8"));
9909
+ fs33.writeFileSync(target, fs33.readFileSync(job.filePath, "utf-8"));
9723
9910
  wrote.push(rel);
9724
9911
  }
9725
9912
  }
@@ -9731,12 +9918,12 @@ function performInit(cwd, force) {
9731
9918
  continue;
9732
9919
  }
9733
9920
  if (profile.kind !== "scheduled" || !profile.schedule) continue;
9734
- const target = path29.join(workflowDir, `kody-${exe.name}.yml`);
9735
- if (fs32.existsSync(target) && !force) {
9921
+ const target = path30.join(workflowDir, `kody-${exe.name}.yml`);
9922
+ if (fs33.existsSync(target) && !force) {
9736
9923
  skipped.push(`.github/workflows/kody-${exe.name}.yml`);
9737
9924
  continue;
9738
9925
  }
9739
- fs32.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
9926
+ fs33.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
9740
9927
  wrote.push(`.github/workflows/kody-${exe.name}.yml`);
9741
9928
  }
9742
9929
  let labels;
@@ -9921,8 +10108,8 @@ var loadIssueStateComment = async (ctx, _profile, args) => {
9921
10108
 
9922
10109
  // src/scripts/loadJobFromFile.ts
9923
10110
  init_dutyMcp();
9924
- import * as fs33 from "fs";
9925
- import * as path30 from "path";
10111
+ import * as fs34 from "fs";
10112
+ import * as path31 from "path";
9926
10113
  var DUTY_TOOL_PALETTE = new Set(DUTY_MCP_TOOL_NAMES);
9927
10114
  var loadJobFromFile = async (ctx, profile, args) => {
9928
10115
  const jobsDir = String(args?.jobsDir ?? ".kody/duties");
@@ -9932,23 +10119,23 @@ var loadJobFromFile = async (ctx, profile, args) => {
9932
10119
  if (!slug) {
9933
10120
  throw new Error(`loadJobFromFile: ctx.args.${slugArg} must be a non-empty slug`);
9934
10121
  }
9935
- const absPath = path30.join(ctx.cwd, jobsDir, `${slug}.md`);
9936
- if (!fs33.existsSync(absPath)) {
10122
+ const absPath = path31.join(ctx.cwd, jobsDir, `${slug}.md`);
10123
+ if (!fs34.existsSync(absPath)) {
9937
10124
  throw new Error(`loadJobFromFile: job file not found: ${absPath}`);
9938
10125
  }
9939
- const raw = fs33.readFileSync(absPath, "utf-8");
10126
+ const raw = fs34.readFileSync(absPath, "utf-8");
9940
10127
  const { title, body } = parseJobFile(raw, slug);
9941
- const frontmatter = splitFrontmatter(raw).frontmatter;
10128
+ const frontmatter = splitFrontmatter2(raw).frontmatter;
9942
10129
  const mentions = (frontmatter.mentions ?? []).map((login) => `@${login}`).join(" ");
9943
10130
  const workerSlug = (frontmatter.staff ?? "").trim();
9944
10131
  let workerTitle = "";
9945
10132
  let workerPersona = "";
9946
10133
  if (workerSlug) {
9947
- const workerPath = path30.join(ctx.cwd, workersDir, `${workerSlug}.md`);
9948
- if (!fs33.existsSync(workerPath)) {
10134
+ const workerPath = path31.join(ctx.cwd, workersDir, `${workerSlug}.md`);
10135
+ if (!fs34.existsSync(workerPath)) {
9949
10136
  throw new Error(`loadJobFromFile: duty '${slug}' declares staff '${workerSlug}' but ${workerPath} does not exist`);
9950
10137
  }
9951
- const workerRaw = fs33.readFileSync(workerPath, "utf-8");
10138
+ const workerRaw = fs34.readFileSync(workerPath, "utf-8");
9952
10139
  const parsed = parseJobFile(workerRaw, workerSlug);
9953
10140
  workerTitle = parsed.title;
9954
10141
  workerPersona = parsed.body;
@@ -10001,6 +10188,36 @@ function humanizeSlug(slug) {
10001
10188
  return slug.split(/[-_]+/).filter((s) => s.length > 0).map((s) => s[0].toUpperCase() + s.slice(1)).join(" ");
10002
10189
  }
10003
10190
 
10191
+ // src/scripts/loadDutyState.ts
10192
+ init_dutyMcp();
10193
+ var DUTY_TOOL_PALETTE2 = new Set(DUTY_MCP_TOOL_NAMES);
10194
+ var loadDutyState = async (ctx, profile, args) => {
10195
+ const jobsDir = String(args?.jobsDir ?? ".kody/duties");
10196
+ const slug = profile.name;
10197
+ const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, jobsDir });
10198
+ if (backend.hydrate) await backend.hydrate();
10199
+ const loaded = await backend.load(slug);
10200
+ ctx.data.jobSlug = slug;
10201
+ ctx.data.jobState = loaded;
10202
+ ctx.data.jobStateJson = JSON.stringify(loaded.state, null, 2);
10203
+ const mentions = (profile.mentions ?? []).map((l) => `@${l}`).join(" ");
10204
+ ctx.data.mentions = mentions;
10205
+ const declaredTools = profile.dutyTools ?? [];
10206
+ if (declaredTools.length > 0) {
10207
+ const unknown = declaredTools.filter((name) => !DUTY_TOOL_PALETTE2.has(name));
10208
+ if (unknown.length > 0) {
10209
+ throw new Error(
10210
+ `loadDutyState: duty '${slug}' declared dutyTools not in the kody-duty palette: ${unknown.join(", ")}. Available: ${[...DUTY_MCP_TOOL_NAMES].join(", ")}`
10211
+ );
10212
+ }
10213
+ ctx.data.dutyTools = declaredTools;
10214
+ ctx.data.dutyToolsList = declaredTools.map((name) => `- \`${name}\``).join("\n");
10215
+ ctx.data.dutyOperatorMention = mentions;
10216
+ const mcpToolNames = declaredTools.map((name) => `mcp__kody-duty__${name}`);
10217
+ profile.claudeCode.tools = [...mcpToolNames, "mcp__kody-submit__submit_state"];
10218
+ }
10219
+ };
10220
+
10004
10221
  // src/scripts/loadLinkedFinding.ts
10005
10222
  init_issue();
10006
10223
  var FINDING_BODY_MAX_BYTES = 4e3;
@@ -10032,18 +10249,18 @@ init_loadMemoryContext();
10032
10249
  init_loadPriorArt();
10033
10250
 
10034
10251
  // src/scripts/loadQaContext.ts
10035
- import * as fs35 from "fs";
10036
- import * as path32 from "path";
10252
+ import * as fs36 from "fs";
10253
+ import * as path33 from "path";
10037
10254
 
10038
10255
  // src/scripts/kodyVariables.ts
10039
- import * as fs34 from "fs";
10040
- import * as path31 from "path";
10256
+ import * as fs35 from "fs";
10257
+ import * as path32 from "path";
10041
10258
  var KODY_VARIABLES_REL_PATH = ".kody/variables.json";
10042
10259
  function readKodyVariables(cwd) {
10043
- const full = path31.join(cwd, KODY_VARIABLES_REL_PATH);
10260
+ const full = path32.join(cwd, KODY_VARIABLES_REL_PATH);
10044
10261
  let raw;
10045
10262
  try {
10046
- raw = fs34.readFileSync(full, "utf-8");
10263
+ raw = fs35.readFileSync(full, "utf-8");
10047
10264
  } catch {
10048
10265
  return {};
10049
10266
  }
@@ -10094,18 +10311,18 @@ function readProfileStaff(raw) {
10094
10311
  return { staff: staff ?? legacy ?? ["kody"], body };
10095
10312
  }
10096
10313
  function readProfile(cwd) {
10097
- const dir = path32.join(cwd, CONTEXT_DIR_REL_PATH);
10098
- if (!fs35.existsSync(dir)) return "";
10314
+ const dir = path33.join(cwd, CONTEXT_DIR_REL_PATH);
10315
+ if (!fs36.existsSync(dir)) return "";
10099
10316
  let entries;
10100
10317
  try {
10101
- entries = fs35.readdirSync(dir).filter((f) => f.endsWith(".md")).sort();
10318
+ entries = fs36.readdirSync(dir).filter((f) => f.endsWith(".md")).sort();
10102
10319
  } catch {
10103
10320
  return "";
10104
10321
  }
10105
10322
  const blocks = [];
10106
10323
  for (const file of entries) {
10107
10324
  try {
10108
- const raw = fs35.readFileSync(path32.join(dir, file), "utf-8");
10325
+ const raw = fs36.readFileSync(path33.join(dir, file), "utf-8");
10109
10326
  const { staff, body } = readProfileStaff(raw);
10110
10327
  if (!staff.includes(QA_STAFF) && !staff.includes(ALL_STAFF)) continue;
10111
10328
  blocks.push(`## ${file}
@@ -10142,8 +10359,8 @@ var loadQaContext = async (ctx) => {
10142
10359
  init_events();
10143
10360
 
10144
10361
  // src/taskContext.ts
10145
- import * as fs36 from "fs";
10146
- import * as path33 from "path";
10362
+ import * as fs37 from "fs";
10363
+ import * as path34 from "path";
10147
10364
  var TASK_CONTEXT_SCHEMA_VERSION = 1;
10148
10365
  function buildTaskContext(args) {
10149
10366
  return {
@@ -10159,10 +10376,10 @@ function buildTaskContext(args) {
10159
10376
  }
10160
10377
  function persistTaskContext(cwd, ctx) {
10161
10378
  try {
10162
- const dir = path33.join(cwd, ".kody", "runs", ctx.runId);
10163
- fs36.mkdirSync(dir, { recursive: true });
10164
- const file = path33.join(dir, "task-context.json");
10165
- fs36.writeFileSync(file, `${JSON.stringify(ctx, null, 2)}
10379
+ const dir = path34.join(cwd, ".kody", "runs", ctx.runId);
10380
+ fs37.mkdirSync(dir, { recursive: true });
10381
+ const file = path34.join(dir, "task-context.json");
10382
+ fs37.writeFileSync(file, `${JSON.stringify(ctx, null, 2)}
10166
10383
  `);
10167
10384
  return file;
10168
10385
  } catch (err) {
@@ -10228,19 +10445,19 @@ var loadTaskState = async (ctx) => {
10228
10445
  };
10229
10446
 
10230
10447
  // src/scripts/loadWorkerAdhoc.ts
10231
- import * as fs37 from "fs";
10232
- import * as path34 from "path";
10448
+ import * as fs38 from "fs";
10449
+ import * as path35 from "path";
10233
10450
  var loadWorkerAdhoc = async (ctx, _profile, args) => {
10234
10451
  const workersDir = String(args?.workersDir ?? ".kody/staff");
10235
10452
  const workerSlug = String(ctx.args.worker ?? "").trim();
10236
10453
  if (!workerSlug) {
10237
10454
  throw new Error("loadWorkerAdhoc: ctx.args.worker must be a non-empty slug");
10238
10455
  }
10239
- const workerPath = path34.join(ctx.cwd, workersDir, `${workerSlug}.md`);
10240
- if (!fs37.existsSync(workerPath)) {
10456
+ const workerPath = path35.join(ctx.cwd, workersDir, `${workerSlug}.md`);
10457
+ if (!fs38.existsSync(workerPath)) {
10241
10458
  throw new Error(`loadWorkerAdhoc: worker persona not found: ${workerPath}`);
10242
10459
  }
10243
- const { title, body } = parsePersona(fs37.readFileSync(workerPath, "utf-8"), workerSlug);
10460
+ const { title, body } = parsePersona(fs38.readFileSync(workerPath, "utf-8"), workerSlug);
10244
10461
  const message = resolveMessage(ctx.args.message);
10245
10462
  if (!message) {
10246
10463
  throw new Error(
@@ -10260,9 +10477,9 @@ function resolveMessage(messageArg) {
10260
10477
  }
10261
10478
  function readCommentBody() {
10262
10479
  const eventPath = process.env.GITHUB_EVENT_PATH;
10263
- if (!eventPath || !fs37.existsSync(eventPath)) return "";
10480
+ if (!eventPath || !fs38.existsSync(eventPath)) return "";
10264
10481
  try {
10265
- const event = JSON.parse(fs37.readFileSync(eventPath, "utf-8"));
10482
+ const event = JSON.parse(fs38.readFileSync(eventPath, "utf-8"));
10266
10483
  return String(event.comment?.body ?? "");
10267
10484
  } catch {
10268
10485
  return "";
@@ -10286,7 +10503,7 @@ function stripDirective(body) {
10286
10503
  return lines.slice(start).join("\n").trim();
10287
10504
  }
10288
10505
  function parsePersona(raw, slug) {
10289
- const stripped = splitFrontmatter(raw).body;
10506
+ const stripped = splitFrontmatter2(raw).body;
10290
10507
  const trimmed = stripped.trim();
10291
10508
  const firstLine2 = trimmed.split("\n", 1)[0] ?? "";
10292
10509
  const h1 = /^#\s+(.+?)\s*$/.exec(firstLine2);
@@ -10993,8 +11210,8 @@ var FlyClient = class {
10993
11210
  get fetch() {
10994
11211
  return this.opts.fetchImpl ?? fetch;
10995
11212
  }
10996
- async call(path41, init = {}) {
10997
- const res = await this.fetch(`${FLY_API_BASE}${path41}`, {
11213
+ async call(path42, init = {}) {
11214
+ const res = await this.fetch(`${FLY_API_BASE}${path42}`, {
10998
11215
  method: init.method ?? "GET",
10999
11216
  headers: {
11000
11217
  Authorization: `Bearer ${this.opts.token}`,
@@ -11005,7 +11222,7 @@ var FlyClient = class {
11005
11222
  if (res.status === 404 && init.allow404) return null;
11006
11223
  if (!res.ok) {
11007
11224
  const text = await res.text().catch(() => "");
11008
- throw new Error(`Fly API ${res.status} on ${path41}: ${text.slice(0, 200) || res.statusText}`);
11225
+ throw new Error(`Fly API ${res.status} on ${path42}: ${text.slice(0, 200) || res.statusText}`);
11009
11226
  }
11010
11227
  if (res.status === 204) return null;
11011
11228
  const raw = await res.text();
@@ -12581,7 +12798,7 @@ function resolveBaseOverride(value) {
12581
12798
 
12582
12799
  // src/scripts/runnerServe.ts
12583
12800
  import { spawn as spawn5 } from "child_process";
12584
- import * as fs38 from "fs";
12801
+ import * as fs39 from "fs";
12585
12802
  import { createServer as createServer3 } from "http";
12586
12803
  var DEFAULT_PORT2 = 8080;
12587
12804
  var DEFAULT_WORKDIR = "/workspace/repo";
@@ -12661,8 +12878,8 @@ async function defaultRunJob(job) {
12661
12878
  const workdir = process.env.RUNNER_WORKDIR ?? DEFAULT_WORKDIR;
12662
12879
  const branch = job.ref ?? "main";
12663
12880
  const authUrl = `https://x-access-token:${job.githubToken}@github.com/${job.repo}.git`;
12664
- fs38.rmSync(workdir, { recursive: true, force: true });
12665
- fs38.mkdirSync(workdir, { recursive: true });
12881
+ fs39.rmSync(workdir, { recursive: true, force: true });
12882
+ fs39.mkdirSync(workdir, { recursive: true });
12666
12883
  const allSecrets = typeof job.allSecrets === "string" ? job.allSecrets : JSON.stringify(job.allSecrets ?? {});
12667
12884
  const interactive = job.mode === "interactive";
12668
12885
  const scheduled = job.mode === "scheduled";
@@ -12795,7 +13012,7 @@ var runnerServe = async (ctx) => {
12795
13012
 
12796
13013
  // src/scripts/runPreviewBuild.ts
12797
13014
  import { copyFile, writeFile } from "fs/promises";
12798
- import * as path35 from "path";
13015
+ import * as path36 from "path";
12799
13016
  import { fileURLToPath } from "url";
12800
13017
 
12801
13018
  // src/scripts/previewBuildHelpers.ts
@@ -12944,9 +13161,9 @@ var FLY_MACHINES = "https://api.machines.dev/v1";
12944
13161
  var FLY_GRAPHQL = "https://api.fly.io/graphql";
12945
13162
  var REQ_TIMEOUT_MS2 = 3e4;
12946
13163
  function bundledDockerfilePath(mode) {
12947
- const here = path35.dirname(fileURLToPath(import.meta.url));
13164
+ const here = path36.dirname(fileURLToPath(import.meta.url));
12948
13165
  const file = mode === "dev" ? "default-Dockerfile.preview.dev" : "default-Dockerfile.preview.prod";
12949
- return path35.join(here, "preview-build-templates", file);
13166
+ return path36.join(here, "preview-build-templates", file);
12950
13167
  }
12951
13168
  function required(name) {
12952
13169
  const v = (process.env[name] ?? "").trim();
@@ -13195,10 +13412,10 @@ var runPreviewBuild = async (ctx, _profile, _args) => {
13195
13412
  console.log(`[preview-build] vault: ${Object.keys(buildEnv).length} secrets, mode=${buildMode}`);
13196
13413
  if (Object.keys(buildEnv).length > 0) {
13197
13414
  const lines = Object.entries(buildEnv).map(([k, v]) => `${k}=${JSON.stringify(v)}`);
13198
- await writeFile(path35.join(ctx.cwd, ".env.production.local"), `${lines.join("\n")}
13415
+ await writeFile(path36.join(ctx.cwd, ".env.production.local"), `${lines.join("\n")}
13199
13416
  `, "utf8");
13200
13417
  }
13201
- const consumerDockerfile = path35.join(ctx.cwd, "Dockerfile.preview");
13418
+ const consumerDockerfile = path36.join(ctx.cwd, "Dockerfile.preview");
13202
13419
  const { stat } = await import("fs/promises");
13203
13420
  let hasConsumerDockerfile = false;
13204
13421
  try {
@@ -13298,8 +13515,8 @@ var runPreviewBuild = async (ctx, _profile, _args) => {
13298
13515
 
13299
13516
  // src/scripts/runTickScript.ts
13300
13517
  import { spawnSync as spawnSync2 } from "child_process";
13301
- import * as fs39 from "fs";
13302
- import * as path36 from "path";
13518
+ import * as fs40 from "fs";
13519
+ import * as path37 from "path";
13303
13520
  var runTickScript = async (ctx, _profile, args) => {
13304
13521
  ctx.skipAgent = true;
13305
13522
  const jobsDir = String(args?.jobsDir ?? ".kody/duties");
@@ -13311,22 +13528,22 @@ var runTickScript = async (ctx, _profile, args) => {
13311
13528
  ctx.output.reason = `runTickScript: ctx.args.${slugArg} must be a non-empty slug`;
13312
13529
  return;
13313
13530
  }
13314
- const jobPath = path36.join(ctx.cwd, jobsDir, `${slug}.md`);
13315
- if (!fs39.existsSync(jobPath)) {
13531
+ const jobPath = path37.join(ctx.cwd, jobsDir, `${slug}.md`);
13532
+ if (!fs40.existsSync(jobPath)) {
13316
13533
  ctx.output.exitCode = 99;
13317
13534
  ctx.output.reason = `runTickScript: job file not found: ${jobPath}`;
13318
13535
  return;
13319
13536
  }
13320
- const raw = fs39.readFileSync(jobPath, "utf-8");
13321
- const { frontmatter } = splitFrontmatter(raw);
13537
+ const raw = fs40.readFileSync(jobPath, "utf-8");
13538
+ const { frontmatter } = splitFrontmatter2(raw);
13322
13539
  const tickScript = frontmatter.tickScript;
13323
13540
  if (!tickScript) {
13324
13541
  ctx.output.exitCode = 99;
13325
13542
  ctx.output.reason = `runTickScript: job ${slug} has no \`tickScript:\` frontmatter \u2014 route via job-tick instead`;
13326
13543
  return;
13327
13544
  }
13328
- const scriptPath = path36.isAbsolute(tickScript) ? tickScript : path36.join(ctx.cwd, tickScript);
13329
- if (!fs39.existsSync(scriptPath)) {
13545
+ const scriptPath = path37.isAbsolute(tickScript) ? tickScript : path37.join(ctx.cwd, tickScript);
13546
+ if (!fs40.existsSync(scriptPath)) {
13330
13547
  ctx.output.exitCode = 99;
13331
13548
  ctx.output.reason = `runTickScript: tickScript not found: ${scriptPath}`;
13332
13549
  return;
@@ -13455,7 +13672,7 @@ var saveTaskState = async (ctx, profile) => {
13455
13672
  if (!target || !number || !state) return;
13456
13673
  const executable = profile.name;
13457
13674
  const action = ctx.data.action ?? synthesizeAction(ctx);
13458
- const next = reduce(state, executable, action, profile.phase);
13675
+ const next = reduce(state, executable, action, profile.phase, profile.staff);
13459
13676
  if (ctx.output.prUrl) next.core.prUrl = ctx.output.prUrl;
13460
13677
  if (typeof ctx.data.runUrl === "string") next.core.runUrl = ctx.data.runUrl;
13461
13678
  writeTaskState(target, number, next, ctx.cwd);
@@ -14372,7 +14589,7 @@ var writeJobStateFile = async (ctx, _profile, agentResult, args) => {
14372
14589
  };
14373
14590
 
14374
14591
  // src/scripts/writeRunSummary.ts
14375
- import * as fs40 from "fs";
14592
+ import * as fs41 from "fs";
14376
14593
  var writeRunSummary = async (ctx, profile) => {
14377
14594
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
14378
14595
  if (!summaryPath) return;
@@ -14394,7 +14611,7 @@ var writeRunSummary = async (ctx, profile) => {
14394
14611
  if (reason) lines.push(`- **Reason:** ${reason}`);
14395
14612
  lines.push("");
14396
14613
  try {
14397
- fs40.appendFileSync(summaryPath, `${lines.join("\n")}
14614
+ fs41.appendFileSync(summaryPath, `${lines.join("\n")}
14398
14615
  `);
14399
14616
  } catch {
14400
14617
  }
@@ -14416,6 +14633,7 @@ var preflightScripts = {
14416
14633
  loadIssueContext,
14417
14634
  loadIssueStateComment,
14418
14635
  loadJobFromFile,
14636
+ loadDutyState,
14419
14637
  loadWorkerAdhoc,
14420
14638
  loadConventions,
14421
14639
  loadCoverageRules,
@@ -14501,46 +14719,36 @@ var allScriptNames = /* @__PURE__ */ new Set([
14501
14719
  ...Object.keys(postflightScripts)
14502
14720
  ]);
14503
14721
 
14504
- // src/subagents.ts
14505
- import * as fs41 from "fs";
14506
- import * as path37 from "path";
14507
- function splitFrontmatter2(raw) {
14508
- const match = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(raw);
14509
- if (!match) return { fm: {}, body: raw.trim() };
14510
- const fm = {};
14511
- for (const line of match[1].split("\n")) {
14512
- const idx = line.indexOf(":");
14513
- if (idx === -1) continue;
14514
- fm[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
14515
- }
14516
- return { fm, body: (match[2] ?? "").trim() };
14517
- }
14518
- function resolveAgentFile(profileDir, name) {
14519
- const local = path37.join(profileDir, "agents", `${name}.md`);
14520
- if (fs41.existsSync(local)) return local;
14521
- const central = path37.join(getPluginsCatalogRoot(), "agents", `${name}.md`);
14522
- if (fs41.existsSync(central)) return central;
14523
- throw new Error(`loadSubagents: agent '${name}' not found in ${profileDir}/agents/ or shared catalog`);
14524
- }
14525
- function loadSubagents(profile) {
14526
- const names = profile.claudeCode.subagents;
14527
- if (!names || names.length === 0) return void 0;
14528
- const agents = {};
14529
- for (const name of names) {
14530
- const { fm, body } = splitFrontmatter2(fs41.readFileSync(resolveAgentFile(profile.dir, name), "utf-8"));
14531
- if (!body) throw new Error(`loadSubagents: agent '${name}' has an empty prompt body`);
14532
- const def = {
14533
- description: fm.description ?? `Subagent ${name}`,
14534
- prompt: body
14535
- };
14536
- if (fm.tools) {
14537
- const tools = fm.tools.split(",").map((t) => t.trim()).filter(Boolean);
14538
- if (tools.length > 0) def.tools = tools;
14539
- }
14540
- if (fm.model) def.model = fm.model;
14541
- agents[fm.name || name] = def;
14542
- }
14543
- return agents;
14722
+ // src/staff.ts
14723
+ import * as fs42 from "fs";
14724
+ import * as path38 from "path";
14725
+ var DEFAULT_STAFF_DIR = ".kody/staff";
14726
+ function stripFrontmatter(raw) {
14727
+ const match = /^---\n[\s\S]*?\n---\n?([\s\S]*)$/.exec(raw);
14728
+ return (match ? match[1] : raw).trim();
14729
+ }
14730
+ function loadStaffPersona(cwd, slug, staffDir = DEFAULT_STAFF_DIR) {
14731
+ const trimmed = slug.trim();
14732
+ if (!trimmed) throw new Error("loadStaffPersona: empty staff slug");
14733
+ const staffPath = path38.join(cwd, staffDir, `${trimmed}.md`);
14734
+ if (!fs42.existsSync(staffPath)) {
14735
+ throw new Error(`loadStaffPersona: staff '${trimmed}' declared but ${staffPath} does not exist`);
14736
+ }
14737
+ const body = stripFrontmatter(fs42.readFileSync(staffPath, "utf-8"));
14738
+ if (!body) throw new Error(`loadStaffPersona: staff '${trimmed}' persona body is empty (${staffPath})`);
14739
+ return body;
14740
+ }
14741
+ function framePersona(slug, persona) {
14742
+ return [
14743
+ `## Who you are \u2014 staff persona (authoritative identity)`,
14744
+ ``,
14745
+ `You are operating as staff member \`${slug}\`. This persona defines *who* you are:`,
14746
+ `your authority, doctrine, voice, and hard limits. Honour it exactly. Where the`,
14747
+ `persona's restrictions are stricter than the task, **the persona wins** \u2014 a task`,
14748
+ `can never grant you authority your persona withholds.`,
14749
+ ``,
14750
+ persona
14751
+ ].join("\n");
14544
14752
  }
14545
14753
 
14546
14754
  // src/tools.ts
@@ -14715,9 +14923,10 @@ async function runExecutable(profileName, input) {
14715
14923
  })
14716
14924
  };
14717
14925
  })() : null;
14718
- const ndjsonDir = path38.join(input.cwd, ".kody");
14926
+ const ndjsonDir = path39.join(input.cwd, ".kody");
14927
+ const staffPersona = typeof profile.staff === "string" && profile.staff.length > 0 ? framePersona(profile.staff, loadStaffPersona(input.cwd, profile.staff)) : null;
14719
14928
  const invokeAgent = async (prompt) => {
14720
- const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path38.isAbsolute(p) ? p : path38.resolve(profile.dir, p)).filter((p) => p.length > 0);
14929
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path39.isAbsolute(p) ? p : path39.resolve(profile.dir, p)).filter((p) => p.length > 0);
14721
14930
  const syntheticPath = ctx.data.syntheticPluginPath;
14722
14931
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
14723
14932
  const agents = loadSubagents(profile);
@@ -14746,7 +14955,7 @@ async function runExecutable(profileName, input) {
14746
14955
  maxTurnTimeoutMs: typeof profile.claudeCode.maxTurnTimeoutSec === "number" ? Math.floor(profile.claudeCode.maxTurnTimeoutSec * 1e3) : void 0,
14747
14956
  // DISCIPLINE leads so the stable, role-agnostic block sits at the front
14748
14957
  // of the cacheable system-prompt prefix; profile/task appends follow.
14749
- systemPromptAppend: [DISCIPLINE, profile.claudeCode.systemPromptAppend, taskArtifacts?.promptAddendum].filter((s) => typeof s === "string" && s.length > 0).join("\n\n") || void 0,
14958
+ systemPromptAppend: [DISCIPLINE, staffPersona, profile.claudeCode.systemPromptAppend, taskArtifacts?.promptAddendum].filter((s) => typeof s === "string" && s.length > 0).join("\n\n") || void 0,
14750
14959
  cacheable: profile.claudeCode.cacheable,
14751
14960
  enableVerifyTool: profile.claudeCode.enableVerifyTool,
14752
14961
  enableSubmitTool: profile.claudeCode.enableSubmitTool,
@@ -14986,17 +15195,17 @@ function clearStampedLifecycleLabels(profile, ctx) {
14986
15195
  function resolveProfilePath(profileName) {
14987
15196
  const found = resolveExecutable(profileName);
14988
15197
  if (found) return found;
14989
- const here = path38.dirname(new URL(import.meta.url).pathname);
15198
+ const here = path39.dirname(new URL(import.meta.url).pathname);
14990
15199
  const candidates = [
14991
- path38.join(here, "executables", profileName, "profile.json"),
15200
+ path39.join(here, "executables", profileName, "profile.json"),
14992
15201
  // same-dir sibling (dev)
14993
- path38.join(here, "..", "executables", profileName, "profile.json"),
15202
+ path39.join(here, "..", "executables", profileName, "profile.json"),
14994
15203
  // up one (prod: dist/bin → dist/executables)
14995
- path38.join(here, "..", "src", "executables", profileName, "profile.json")
15204
+ path39.join(here, "..", "src", "executables", profileName, "profile.json")
14996
15205
  // fallback
14997
15206
  ];
14998
15207
  for (const c of candidates) {
14999
- if (fs42.existsSync(c)) return c;
15208
+ if (fs43.existsSync(c)) return c;
15000
15209
  }
15001
15210
  return candidates[0];
15002
15211
  }
@@ -15096,8 +15305,8 @@ function resolveShellTimeoutMs(entry) {
15096
15305
  var SIGKILL_GRACE_MS = 5e3;
15097
15306
  async function runShellEntry(entry, ctx, profile) {
15098
15307
  const shellName = entry.shell;
15099
- const shellPath = path38.join(profile.dir, shellName);
15100
- if (!fs42.existsSync(shellPath)) {
15308
+ const shellPath = path39.join(profile.dir, shellName);
15309
+ if (!fs43.existsSync(shellPath)) {
15101
15310
  ctx.skipAgent = true;
15102
15311
  ctx.output.exitCode = 99;
15103
15312
  ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
@@ -15332,9 +15541,9 @@ async function resolveAuthToken(env = process.env) {
15332
15541
  return void 0;
15333
15542
  }
15334
15543
  function detectPackageManager2(cwd) {
15335
- if (fs43.existsSync(path39.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
15336
- if (fs43.existsSync(path39.join(cwd, "yarn.lock"))) return "yarn";
15337
- if (fs43.existsSync(path39.join(cwd, "bun.lockb"))) return "bun";
15544
+ if (fs44.existsSync(path40.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
15545
+ if (fs44.existsSync(path40.join(cwd, "yarn.lock"))) return "yarn";
15546
+ if (fs44.existsSync(path40.join(cwd, "bun.lockb"))) return "bun";
15338
15547
  return "npm";
15339
15548
  }
15340
15549
  function shellOut(cmd, args, cwd, stream = true) {
@@ -15421,11 +15630,11 @@ function configureGitIdentity(cwd) {
15421
15630
  }
15422
15631
  function postFailureTail(issueNumber, cwd, reason) {
15423
15632
  if (!issueNumber) return;
15424
- const logPath = path39.join(cwd, ".kody", "last-run.jsonl");
15633
+ const logPath = path40.join(cwd, ".kody", "last-run.jsonl");
15425
15634
  let tail = "";
15426
15635
  try {
15427
- if (fs43.existsSync(logPath)) {
15428
- const content = fs43.readFileSync(logPath, "utf-8");
15636
+ if (fs44.existsSync(logPath)) {
15637
+ const content = fs44.readFileSync(logPath, "utf-8");
15429
15638
  tail = content.slice(-3e3);
15430
15639
  }
15431
15640
  } catch {
@@ -15450,7 +15659,7 @@ async function runCi(argv) {
15450
15659
  return 0;
15451
15660
  }
15452
15661
  const args = parseCiArgs(argv);
15453
- const cwd = args.cwd ? path39.resolve(args.cwd) : process.cwd();
15662
+ const cwd = args.cwd ? path40.resolve(args.cwd) : process.cwd();
15454
15663
  let earlyConfig;
15455
15664
  try {
15456
15665
  earlyConfig = loadConfig(cwd);
@@ -15460,16 +15669,63 @@ async function runCi(argv) {
15460
15669
  const eventName = process.env.GITHUB_EVENT_NAME;
15461
15670
  const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
15462
15671
  let manualWorkflowDispatch = false;
15463
- if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs43.existsSync(dispatchEventPath)) {
15672
+ let forceRunDuty = null;
15673
+ if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs44.existsSync(dispatchEventPath)) {
15464
15674
  try {
15465
- const evt = JSON.parse(fs43.readFileSync(dispatchEventPath, "utf-8"));
15675
+ const evt = JSON.parse(fs44.readFileSync(dispatchEventPath, "utf-8"));
15466
15676
  const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
15467
15677
  const sessionInput = String(evt?.inputs?.sessionId ?? "");
15468
- manualWorkflowDispatch = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
15678
+ const exeInput = String(evt?.inputs?.executable ?? "").trim();
15679
+ const noTarget = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
15680
+ if (noTarget && exeInput) forceRunDuty = exeInput;
15681
+ else manualWorkflowDispatch = noTarget;
15469
15682
  } catch {
15470
15683
  manualWorkflowDispatch = false;
15471
15684
  }
15472
15685
  }
15686
+ if (forceRunDuty) {
15687
+ const config = earlyConfig ?? loadConfig(cwd);
15688
+ process.stdout.write(`\u2192 kody: manual one-shot run of duty ${forceRunDuty}
15689
+
15690
+ `);
15691
+ try {
15692
+ const n = unpackAllSecrets();
15693
+ if (n > 0) process.stdout.write(`\u2192 kody: unpacked ${n} secret(s)
15694
+ `);
15695
+ await resolveAuthToken();
15696
+ const pm = args.packageManager ?? detectPackageManager2(cwd);
15697
+ if (!args.skipInstall) {
15698
+ const code = installDeps(pm, cwd);
15699
+ if (code !== 0) {
15700
+ process.stderr.write(`[kody] dependency install failed (exit ${code})
15701
+ `);
15702
+ return 99;
15703
+ }
15704
+ }
15705
+ if (!args.skipLitellm) {
15706
+ const code = installLitellmIfNeeded(cwd);
15707
+ if (code !== 0) {
15708
+ process.stderr.write(`[kody] litellm install failed (exit ${code})
15709
+ `);
15710
+ return 99;
15711
+ }
15712
+ }
15713
+ configureGitIdentity(cwd);
15714
+ } catch (err) {
15715
+ process.stderr.write(`[kody] manual duty preflight crashed: ${String(err)}
15716
+ `);
15717
+ return 99;
15718
+ }
15719
+ const result = await runExecutableChain(forceRunDuty, {
15720
+ cliArgs: {},
15721
+ cwd,
15722
+ config,
15723
+ verbose: args.verbose,
15724
+ quiet: args.quiet
15725
+ });
15726
+ const ec = result.exitCode;
15727
+ return ec === 0 || ec === 1 || ec === 2 ? ec : 99;
15728
+ }
15473
15729
  if (!args.issueNumber && !autoFallback && (eventName === "schedule" || manualWorkflowDispatch)) {
15474
15730
  return runScheduledFanOut(cwd, args, { force: manualWorkflowDispatch });
15475
15731
  }
@@ -15728,12 +15984,12 @@ function parseChatArgs(argv, env = process.env) {
15728
15984
  return result;
15729
15985
  }
15730
15986
  function commitChatFiles(cwd, sessionId, verbose) {
15731
- const sessionFile = path40.relative(cwd, sessionFilePath(cwd, sessionId));
15732
- const eventsFile = path40.relative(cwd, eventsFilePath(cwd, sessionId));
15987
+ const sessionFile = path41.relative(cwd, sessionFilePath(cwd, sessionId));
15988
+ const eventsFile = path41.relative(cwd, eventsFilePath(cwd, sessionId));
15733
15989
  const safeSession = sessionId.replace(/[^a-zA-Z0-9._-]/g, "_");
15734
- const tasksDir = path40.join(".kody", "tasks", safeSession);
15990
+ const tasksDir = path41.join(".kody", "tasks", safeSession);
15735
15991
  const candidatePaths = [sessionFile, eventsFile, tasksDir];
15736
- const paths = candidatePaths.filter((p) => fs44.existsSync(path40.join(cwd, p)));
15992
+ const paths = candidatePaths.filter((p) => fs45.existsSync(path41.join(cwd, p)));
15737
15993
  if (paths.length === 0) return;
15738
15994
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
15739
15995
  try {
@@ -15777,7 +16033,7 @@ async function runChat(argv) {
15777
16033
  ${CHAT_HELP}`);
15778
16034
  return 64;
15779
16035
  }
15780
- const cwd = args.cwd ? path40.resolve(args.cwd) : process.cwd();
16036
+ const cwd = args.cwd ? path41.resolve(args.cwd) : process.cwd();
15781
16037
  const sessionId = args.sessionId;
15782
16038
  const unpackedSecrets = unpackAllSecrets();
15783
16039
  if (unpackedSecrets > 0) {
@@ -15829,7 +16085,7 @@ ${CHAT_HELP}`);
15829
16085
  const sink = buildSink(cwd, sessionId, args.dashboardUrl);
15830
16086
  const meta = readMeta(sessionFile);
15831
16087
  process.stdout.write(
15832
- `\u2192 kody:chat: session file=${sessionFile} exists=${fs44.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
16088
+ `\u2192 kody:chat: session file=${sessionFile} exists=${fs45.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
15833
16089
  `
15834
16090
  );
15835
16091
  try {