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

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"
@@ -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.0",
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";
@@ -3249,8 +3290,8 @@ async function emit2(sink, type, sessionId, suffix, payload) {
3249
3290
 
3250
3291
  // src/kody-cli.ts
3251
3292
  import { execFileSync as execFileSync28 } from "child_process";
3252
- import * as fs43 from "fs";
3253
- import * as path39 from "path";
3293
+ import * as fs44 from "fs";
3294
+ import * as path40 from "path";
3254
3295
 
3255
3296
  // src/app-auth.ts
3256
3297
  import { createSign } from "crypto";
@@ -3676,8 +3717,8 @@ function coerceBare(spec, value) {
3676
3717
 
3677
3718
  // src/executor.ts
3678
3719
  import { spawn as spawn10 } from "child_process";
3679
- import * as fs42 from "fs";
3680
- import * as path38 from "path";
3720
+ import * as fs43 from "fs";
3721
+ import * as path39 from "path";
3681
3722
 
3682
3723
  // src/container.ts
3683
3724
  init_events();
@@ -3913,6 +3954,8 @@ function loadProfile(profilePath) {
3913
3954
  const profile = {
3914
3955
  name: requireString(profilePath, r, "name"),
3915
3956
  describe: typeof r.describe === "string" ? r.describe : "",
3957
+ // Optional persona to run as. Empty/blank string → undefined (no persona).
3958
+ staff: typeof r.staff === "string" && r.staff.trim() ? r.staff.trim() : void 0,
3916
3959
  role,
3917
3960
  kind,
3918
3961
  schedule: typeof r.schedule === "string" ? r.schedule : void 0,
@@ -6342,10 +6385,10 @@ import * as fs25 from "fs";
6342
6385
  import * as path22 from "path";
6343
6386
  var VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "awaiting-merge", "done"]);
6344
6387
  var GoalStateError = class extends Error {
6345
- constructor(path41, message) {
6346
- super(`Invalid goal state at ${path41}:
6388
+ constructor(path42, message) {
6389
+ super(`Invalid goal state at ${path42}:
6347
6390
  ${message}`);
6348
- this.path = path41;
6391
+ this.path = path42;
6349
6392
  this.name = "GoalStateError";
6350
6393
  }
6351
6394
  path;
@@ -10993,8 +11036,8 @@ var FlyClient = class {
10993
11036
  get fetch() {
10994
11037
  return this.opts.fetchImpl ?? fetch;
10995
11038
  }
10996
- async call(path41, init = {}) {
10997
- const res = await this.fetch(`${FLY_API_BASE}${path41}`, {
11039
+ async call(path42, init = {}) {
11040
+ const res = await this.fetch(`${FLY_API_BASE}${path42}`, {
10998
11041
  method: init.method ?? "GET",
10999
11042
  headers: {
11000
11043
  Authorization: `Bearer ${this.opts.token}`,
@@ -11005,7 +11048,7 @@ var FlyClient = class {
11005
11048
  if (res.status === 404 && init.allow404) return null;
11006
11049
  if (!res.ok) {
11007
11050
  const text = await res.text().catch(() => "");
11008
- throw new Error(`Fly API ${res.status} on ${path41}: ${text.slice(0, 200) || res.statusText}`);
11051
+ throw new Error(`Fly API ${res.status} on ${path42}: ${text.slice(0, 200) || res.statusText}`);
11009
11052
  }
11010
11053
  if (res.status === 204) return null;
11011
11054
  const raw = await res.text();
@@ -14501,9 +14544,41 @@ var allScriptNames = /* @__PURE__ */ new Set([
14501
14544
  ...Object.keys(postflightScripts)
14502
14545
  ]);
14503
14546
 
14504
- // src/subagents.ts
14547
+ // src/staff.ts
14505
14548
  import * as fs41 from "fs";
14506
14549
  import * as path37 from "path";
14550
+ var DEFAULT_STAFF_DIR = ".kody/staff";
14551
+ function stripFrontmatter(raw) {
14552
+ const match = /^---\n[\s\S]*?\n---\n?([\s\S]*)$/.exec(raw);
14553
+ return (match ? match[1] : raw).trim();
14554
+ }
14555
+ function loadStaffPersona(cwd, slug, staffDir = DEFAULT_STAFF_DIR) {
14556
+ const trimmed = slug.trim();
14557
+ if (!trimmed) throw new Error("loadStaffPersona: empty staff slug");
14558
+ const staffPath = path37.join(cwd, staffDir, `${trimmed}.md`);
14559
+ if (!fs41.existsSync(staffPath)) {
14560
+ throw new Error(`loadStaffPersona: staff '${trimmed}' declared but ${staffPath} does not exist`);
14561
+ }
14562
+ const body = stripFrontmatter(fs41.readFileSync(staffPath, "utf-8"));
14563
+ if (!body) throw new Error(`loadStaffPersona: staff '${trimmed}' persona body is empty (${staffPath})`);
14564
+ return body;
14565
+ }
14566
+ function framePersona(slug, persona) {
14567
+ return [
14568
+ `## Who you are \u2014 staff persona (authoritative identity)`,
14569
+ ``,
14570
+ `You are operating as staff member \`${slug}\`. This persona defines *who* you are:`,
14571
+ `your authority, doctrine, voice, and hard limits. Honour it exactly. Where the`,
14572
+ `persona's restrictions are stricter than the task, **the persona wins** \u2014 a task`,
14573
+ `can never grant you authority your persona withholds.`,
14574
+ ``,
14575
+ persona
14576
+ ].join("\n");
14577
+ }
14578
+
14579
+ // src/subagents.ts
14580
+ import * as fs42 from "fs";
14581
+ import * as path38 from "path";
14507
14582
  function splitFrontmatter2(raw) {
14508
14583
  const match = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(raw);
14509
14584
  if (!match) return { fm: {}, body: raw.trim() };
@@ -14516,10 +14591,10 @@ function splitFrontmatter2(raw) {
14516
14591
  return { fm, body: (match[2] ?? "").trim() };
14517
14592
  }
14518
14593
  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;
14594
+ const local = path38.join(profileDir, "agents", `${name}.md`);
14595
+ if (fs42.existsSync(local)) return local;
14596
+ const central = path38.join(getPluginsCatalogRoot(), "agents", `${name}.md`);
14597
+ if (fs42.existsSync(central)) return central;
14523
14598
  throw new Error(`loadSubagents: agent '${name}' not found in ${profileDir}/agents/ or shared catalog`);
14524
14599
  }
14525
14600
  function loadSubagents(profile) {
@@ -14527,7 +14602,7 @@ function loadSubagents(profile) {
14527
14602
  if (!names || names.length === 0) return void 0;
14528
14603
  const agents = {};
14529
14604
  for (const name of names) {
14530
- const { fm, body } = splitFrontmatter2(fs41.readFileSync(resolveAgentFile(profile.dir, name), "utf-8"));
14605
+ const { fm, body } = splitFrontmatter2(fs42.readFileSync(resolveAgentFile(profile.dir, name), "utf-8"));
14531
14606
  if (!body) throw new Error(`loadSubagents: agent '${name}' has an empty prompt body`);
14532
14607
  const def = {
14533
14608
  description: fm.description ?? `Subagent ${name}`,
@@ -14715,9 +14790,10 @@ async function runExecutable(profileName, input) {
14715
14790
  })
14716
14791
  };
14717
14792
  })() : null;
14718
- const ndjsonDir = path38.join(input.cwd, ".kody");
14793
+ const ndjsonDir = path39.join(input.cwd, ".kody");
14794
+ const staffPersona = typeof profile.staff === "string" && profile.staff.length > 0 ? framePersona(profile.staff, loadStaffPersona(input.cwd, profile.staff)) : null;
14719
14795
  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);
14796
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path39.isAbsolute(p) ? p : path39.resolve(profile.dir, p)).filter((p) => p.length > 0);
14721
14797
  const syntheticPath = ctx.data.syntheticPluginPath;
14722
14798
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
14723
14799
  const agents = loadSubagents(profile);
@@ -14746,7 +14822,7 @@ async function runExecutable(profileName, input) {
14746
14822
  maxTurnTimeoutMs: typeof profile.claudeCode.maxTurnTimeoutSec === "number" ? Math.floor(profile.claudeCode.maxTurnTimeoutSec * 1e3) : void 0,
14747
14823
  // DISCIPLINE leads so the stable, role-agnostic block sits at the front
14748
14824
  // 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,
14825
+ systemPromptAppend: [DISCIPLINE, staffPersona, profile.claudeCode.systemPromptAppend, taskArtifacts?.promptAddendum].filter((s) => typeof s === "string" && s.length > 0).join("\n\n") || void 0,
14750
14826
  cacheable: profile.claudeCode.cacheable,
14751
14827
  enableVerifyTool: profile.claudeCode.enableVerifyTool,
14752
14828
  enableSubmitTool: profile.claudeCode.enableSubmitTool,
@@ -14986,17 +15062,17 @@ function clearStampedLifecycleLabels(profile, ctx) {
14986
15062
  function resolveProfilePath(profileName) {
14987
15063
  const found = resolveExecutable(profileName);
14988
15064
  if (found) return found;
14989
- const here = path38.dirname(new URL(import.meta.url).pathname);
15065
+ const here = path39.dirname(new URL(import.meta.url).pathname);
14990
15066
  const candidates = [
14991
- path38.join(here, "executables", profileName, "profile.json"),
15067
+ path39.join(here, "executables", profileName, "profile.json"),
14992
15068
  // same-dir sibling (dev)
14993
- path38.join(here, "..", "executables", profileName, "profile.json"),
15069
+ path39.join(here, "..", "executables", profileName, "profile.json"),
14994
15070
  // up one (prod: dist/bin → dist/executables)
14995
- path38.join(here, "..", "src", "executables", profileName, "profile.json")
15071
+ path39.join(here, "..", "src", "executables", profileName, "profile.json")
14996
15072
  // fallback
14997
15073
  ];
14998
15074
  for (const c of candidates) {
14999
- if (fs42.existsSync(c)) return c;
15075
+ if (fs43.existsSync(c)) return c;
15000
15076
  }
15001
15077
  return candidates[0];
15002
15078
  }
@@ -15096,8 +15172,8 @@ function resolveShellTimeoutMs(entry) {
15096
15172
  var SIGKILL_GRACE_MS = 5e3;
15097
15173
  async function runShellEntry(entry, ctx, profile) {
15098
15174
  const shellName = entry.shell;
15099
- const shellPath = path38.join(profile.dir, shellName);
15100
- if (!fs42.existsSync(shellPath)) {
15175
+ const shellPath = path39.join(profile.dir, shellName);
15176
+ if (!fs43.existsSync(shellPath)) {
15101
15177
  ctx.skipAgent = true;
15102
15178
  ctx.output.exitCode = 99;
15103
15179
  ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
@@ -15332,9 +15408,9 @@ async function resolveAuthToken(env = process.env) {
15332
15408
  return void 0;
15333
15409
  }
15334
15410
  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";
15411
+ if (fs44.existsSync(path40.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
15412
+ if (fs44.existsSync(path40.join(cwd, "yarn.lock"))) return "yarn";
15413
+ if (fs44.existsSync(path40.join(cwd, "bun.lockb"))) return "bun";
15338
15414
  return "npm";
15339
15415
  }
15340
15416
  function shellOut(cmd, args, cwd, stream = true) {
@@ -15421,11 +15497,11 @@ function configureGitIdentity(cwd) {
15421
15497
  }
15422
15498
  function postFailureTail(issueNumber, cwd, reason) {
15423
15499
  if (!issueNumber) return;
15424
- const logPath = path39.join(cwd, ".kody", "last-run.jsonl");
15500
+ const logPath = path40.join(cwd, ".kody", "last-run.jsonl");
15425
15501
  let tail = "";
15426
15502
  try {
15427
- if (fs43.existsSync(logPath)) {
15428
- const content = fs43.readFileSync(logPath, "utf-8");
15503
+ if (fs44.existsSync(logPath)) {
15504
+ const content = fs44.readFileSync(logPath, "utf-8");
15429
15505
  tail = content.slice(-3e3);
15430
15506
  }
15431
15507
  } catch {
@@ -15450,7 +15526,7 @@ async function runCi(argv) {
15450
15526
  return 0;
15451
15527
  }
15452
15528
  const args = parseCiArgs(argv);
15453
- const cwd = args.cwd ? path39.resolve(args.cwd) : process.cwd();
15529
+ const cwd = args.cwd ? path40.resolve(args.cwd) : process.cwd();
15454
15530
  let earlyConfig;
15455
15531
  try {
15456
15532
  earlyConfig = loadConfig(cwd);
@@ -15460,9 +15536,9 @@ async function runCi(argv) {
15460
15536
  const eventName = process.env.GITHUB_EVENT_NAME;
15461
15537
  const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
15462
15538
  let manualWorkflowDispatch = false;
15463
- if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs43.existsSync(dispatchEventPath)) {
15539
+ if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs44.existsSync(dispatchEventPath)) {
15464
15540
  try {
15465
- const evt = JSON.parse(fs43.readFileSync(dispatchEventPath, "utf-8"));
15541
+ const evt = JSON.parse(fs44.readFileSync(dispatchEventPath, "utf-8"));
15466
15542
  const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
15467
15543
  const sessionInput = String(evt?.inputs?.sessionId ?? "");
15468
15544
  manualWorkflowDispatch = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
@@ -15728,12 +15804,12 @@ function parseChatArgs(argv, env = process.env) {
15728
15804
  return result;
15729
15805
  }
15730
15806
  function commitChatFiles(cwd, sessionId, verbose) {
15731
- const sessionFile = path40.relative(cwd, sessionFilePath(cwd, sessionId));
15732
- const eventsFile = path40.relative(cwd, eventsFilePath(cwd, sessionId));
15807
+ const sessionFile = path41.relative(cwd, sessionFilePath(cwd, sessionId));
15808
+ const eventsFile = path41.relative(cwd, eventsFilePath(cwd, sessionId));
15733
15809
  const safeSession = sessionId.replace(/[^a-zA-Z0-9._-]/g, "_");
15734
- const tasksDir = path40.join(".kody", "tasks", safeSession);
15810
+ const tasksDir = path41.join(".kody", "tasks", safeSession);
15735
15811
  const candidatePaths = [sessionFile, eventsFile, tasksDir];
15736
- const paths = candidatePaths.filter((p) => fs44.existsSync(path40.join(cwd, p)));
15812
+ const paths = candidatePaths.filter((p) => fs45.existsSync(path41.join(cwd, p)));
15737
15813
  if (paths.length === 0) return;
15738
15814
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
15739
15815
  try {
@@ -15777,7 +15853,7 @@ async function runChat(argv) {
15777
15853
  ${CHAT_HELP}`);
15778
15854
  return 64;
15779
15855
  }
15780
- const cwd = args.cwd ? path40.resolve(args.cwd) : process.cwd();
15856
+ const cwd = args.cwd ? path41.resolve(args.cwd) : process.cwd();
15781
15857
  const sessionId = args.sessionId;
15782
15858
  const unpackedSecrets = unpackAllSecrets();
15783
15859
  if (unpackedSecrets > 0) {
@@ -15829,7 +15905,7 @@ ${CHAT_HELP}`);
15829
15905
  const sink = buildSink(cwd, sessionId, args.dashboardUrl);
15830
15906
  const meta = readMeta(sessionFile);
15831
15907
  process.stdout.write(
15832
- `\u2192 kody:chat: session file=${sessionFile} exists=${fs44.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
15908
+ `\u2192 kody:chat: session file=${sessionFile} exists=${fs45.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
15833
15909
  `
15834
15910
  );
15835
15911
  try {
@@ -17,6 +17,15 @@ import type { Phase } from "../state.js"
17
17
 
18
18
  export interface Profile {
19
19
  name: string
20
+ /**
21
+ * Optional staff member this executable runs *as*. When set, the executor
22
+ * loads `.kody/staff/<staff>.md` and injects that persona (authoritative
23
+ * identity) ahead of the executable's own system-prompt append. This is the
24
+ * unification hook: a "duty" is just an executable + a staff member. Absent →
25
+ * runs with no persona (unchanged legacy behaviour). A declared-but-missing
26
+ * staff file is fatal at run time (see src/staff.ts).
27
+ */
28
+ staff?: string
20
29
  describe: string
21
30
  /**
22
31
  * Semantic role — what this executable IS, not when it runs.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.203",
3
+ "version": "0.4.204-next.0",
4
4
  "description": "kody — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
5
5
  "license": "MIT",
6
6
  "type": "module",