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

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.2",
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,8 +3720,8 @@ 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();
@@ -3834,6 +3878,7 @@ var VALID_CONTAINER_CHILD_TARGETS = /* @__PURE__ */ new Set(["issue", "pr"]);
3834
3878
  var VALID_PHASES = /* @__PURE__ */ new Set(["research", "planning", "implementing", "reviewing", "shipped", "failed", "idle"]);
3835
3879
  var KNOWN_PROFILE_KEYS = /* @__PURE__ */ new Set([
3836
3880
  "name",
3881
+ "staff",
3837
3882
  "describe",
3838
3883
  "role",
3839
3884
  "kind",
@@ -3913,6 +3958,8 @@ function loadProfile(profilePath) {
3913
3958
  const profile = {
3914
3959
  name: requireString(profilePath, r, "name"),
3915
3960
  describe: typeof r.describe === "string" ? r.describe : "",
3961
+ // Optional persona to run as. Empty/blank string → undefined (no persona).
3962
+ staff: typeof r.staff === "string" && r.staff.trim() ? r.staff.trim() : void 0,
3916
3963
  role,
3917
3964
  kind,
3918
3965
  schedule: typeof r.schedule === "string" ? r.schedule : void 0,
@@ -4294,16 +4341,17 @@ function parseStateComment(body) {
4294
4341
  flow: parsed.flow
4295
4342
  };
4296
4343
  }
4297
- function reduce(state, executable, action, phase) {
4344
+ function reduce(state, executable, action, phase, staff) {
4298
4345
  if (!action) return state;
4299
4346
  const newAttempts = { ...state.core.attempts, [executable]: (state.core.attempts[executable] ?? 0) + 1 };
4300
4347
  const newExecutables = {
4301
4348
  ...state.executables,
4302
4349
  [executable]: { ...state.executables[executable] ?? { lastAction: null }, lastAction: action }
4303
4350
  };
4351
+ const ranAsStaff = typeof staff === "string" && staff.length > 0 ? staff : void 0;
4304
4352
  const newHistory = [
4305
4353
  ...state.history,
4306
- { timestamp: action.timestamp, executable, action: action.type, note: noteFromAction(action) }
4354
+ { timestamp: action.timestamp, executable, action: action.type, note: noteFromAction(action), staff: ranAsStaff }
4307
4355
  ].slice(-HISTORY_MAX_ENTRIES);
4308
4356
  return {
4309
4357
  schemaVersion: 1,
@@ -4312,6 +4360,7 @@ function reduce(state, executable, action, phase) {
4312
4360
  attempts: newAttempts,
4313
4361
  lastOutcome: action,
4314
4362
  currentExecutable: executable,
4363
+ ranAsStaff: ranAsStaff ?? null,
4315
4364
  status: statusFromAction(action),
4316
4365
  phase: phaseFromAction(action, phase)
4317
4366
  },
@@ -4348,6 +4397,9 @@ function renderStateComment(state) {
4348
4397
  if (state.core.currentExecutable) {
4349
4398
  lines.push(`- **Last executable:** \`${state.core.currentExecutable}\``);
4350
4399
  }
4400
+ if (state.core.ranAsStaff) {
4401
+ lines.push(`- **Ran as:** \`${state.core.ranAsStaff}\``);
4402
+ }
4351
4403
  if (state.core.lastOutcome) {
4352
4404
  lines.push(`- **Last action:** \`${state.core.lastOutcome.type}\``);
4353
4405
  }
@@ -5325,7 +5377,7 @@ var advanceFlow = async (ctx, profile) => {
5325
5377
  const action = ctx.data.action;
5326
5378
  let nextIssueState = issueState;
5327
5379
  if (targetType === "pr" && action) {
5328
- nextIssueState = reduce(issueState, profile.name, action, profile.phase);
5380
+ nextIssueState = reduce(issueState, profile.name, action, profile.phase, profile.staff);
5329
5381
  if (state?.core.prUrl && !nextIssueState.core.prUrl) nextIssueState.core.prUrl = state.core.prUrl;
5330
5382
  }
5331
5383
  const prevHops = issueState.flow?.hops ?? flow.hops ?? 0;
@@ -6342,10 +6394,10 @@ import * as fs25 from "fs";
6342
6394
  import * as path22 from "path";
6343
6395
  var VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "awaiting-merge", "done"]);
6344
6396
  var GoalStateError = class extends Error {
6345
- constructor(path41, message) {
6346
- super(`Invalid goal state at ${path41}:
6397
+ constructor(path42, message) {
6398
+ super(`Invalid goal state at ${path42}:
6347
6399
  ${message}`);
6348
- this.path = path41;
6400
+ this.path = path42;
6349
6401
  this.name = "GoalStateError";
6350
6402
  }
6351
6403
  path;
@@ -10993,8 +11045,8 @@ var FlyClient = class {
10993
11045
  get fetch() {
10994
11046
  return this.opts.fetchImpl ?? fetch;
10995
11047
  }
10996
- async call(path41, init = {}) {
10997
- const res = await this.fetch(`${FLY_API_BASE}${path41}`, {
11048
+ async call(path42, init = {}) {
11049
+ const res = await this.fetch(`${FLY_API_BASE}${path42}`, {
10998
11050
  method: init.method ?? "GET",
10999
11051
  headers: {
11000
11052
  Authorization: `Bearer ${this.opts.token}`,
@@ -11005,7 +11057,7 @@ var FlyClient = class {
11005
11057
  if (res.status === 404 && init.allow404) return null;
11006
11058
  if (!res.ok) {
11007
11059
  const text = await res.text().catch(() => "");
11008
- throw new Error(`Fly API ${res.status} on ${path41}: ${text.slice(0, 200) || res.statusText}`);
11060
+ throw new Error(`Fly API ${res.status} on ${path42}: ${text.slice(0, 200) || res.statusText}`);
11009
11061
  }
11010
11062
  if (res.status === 204) return null;
11011
11063
  const raw = await res.text();
@@ -13455,7 +13507,7 @@ var saveTaskState = async (ctx, profile) => {
13455
13507
  if (!target || !number || !state) return;
13456
13508
  const executable = profile.name;
13457
13509
  const action = ctx.data.action ?? synthesizeAction(ctx);
13458
- const next = reduce(state, executable, action, profile.phase);
13510
+ const next = reduce(state, executable, action, profile.phase, profile.staff);
13459
13511
  if (ctx.output.prUrl) next.core.prUrl = ctx.output.prUrl;
13460
13512
  if (typeof ctx.data.runUrl === "string") next.core.runUrl = ctx.data.runUrl;
13461
13513
  writeTaskState(target, number, next, ctx.cwd);
@@ -14501,9 +14553,41 @@ var allScriptNames = /* @__PURE__ */ new Set([
14501
14553
  ...Object.keys(postflightScripts)
14502
14554
  ]);
14503
14555
 
14504
- // src/subagents.ts
14556
+ // src/staff.ts
14505
14557
  import * as fs41 from "fs";
14506
14558
  import * as path37 from "path";
14559
+ var DEFAULT_STAFF_DIR = ".kody/staff";
14560
+ function stripFrontmatter(raw) {
14561
+ const match = /^---\n[\s\S]*?\n---\n?([\s\S]*)$/.exec(raw);
14562
+ return (match ? match[1] : raw).trim();
14563
+ }
14564
+ function loadStaffPersona(cwd, slug, staffDir = DEFAULT_STAFF_DIR) {
14565
+ const trimmed = slug.trim();
14566
+ if (!trimmed) throw new Error("loadStaffPersona: empty staff slug");
14567
+ const staffPath = path37.join(cwd, staffDir, `${trimmed}.md`);
14568
+ if (!fs41.existsSync(staffPath)) {
14569
+ throw new Error(`loadStaffPersona: staff '${trimmed}' declared but ${staffPath} does not exist`);
14570
+ }
14571
+ const body = stripFrontmatter(fs41.readFileSync(staffPath, "utf-8"));
14572
+ if (!body) throw new Error(`loadStaffPersona: staff '${trimmed}' persona body is empty (${staffPath})`);
14573
+ return body;
14574
+ }
14575
+ function framePersona(slug, persona) {
14576
+ return [
14577
+ `## Who you are \u2014 staff persona (authoritative identity)`,
14578
+ ``,
14579
+ `You are operating as staff member \`${slug}\`. This persona defines *who* you are:`,
14580
+ `your authority, doctrine, voice, and hard limits. Honour it exactly. Where the`,
14581
+ `persona's restrictions are stricter than the task, **the persona wins** \u2014 a task`,
14582
+ `can never grant you authority your persona withholds.`,
14583
+ ``,
14584
+ persona
14585
+ ].join("\n");
14586
+ }
14587
+
14588
+ // src/subagents.ts
14589
+ import * as fs42 from "fs";
14590
+ import * as path38 from "path";
14507
14591
  function splitFrontmatter2(raw) {
14508
14592
  const match = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(raw);
14509
14593
  if (!match) return { fm: {}, body: raw.trim() };
@@ -14516,10 +14600,10 @@ function splitFrontmatter2(raw) {
14516
14600
  return { fm, body: (match[2] ?? "").trim() };
14517
14601
  }
14518
14602
  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;
14603
+ const local = path38.join(profileDir, "agents", `${name}.md`);
14604
+ if (fs42.existsSync(local)) return local;
14605
+ const central = path38.join(getPluginsCatalogRoot(), "agents", `${name}.md`);
14606
+ if (fs42.existsSync(central)) return central;
14523
14607
  throw new Error(`loadSubagents: agent '${name}' not found in ${profileDir}/agents/ or shared catalog`);
14524
14608
  }
14525
14609
  function loadSubagents(profile) {
@@ -14527,7 +14611,7 @@ function loadSubagents(profile) {
14527
14611
  if (!names || names.length === 0) return void 0;
14528
14612
  const agents = {};
14529
14613
  for (const name of names) {
14530
- const { fm, body } = splitFrontmatter2(fs41.readFileSync(resolveAgentFile(profile.dir, name), "utf-8"));
14614
+ const { fm, body } = splitFrontmatter2(fs42.readFileSync(resolveAgentFile(profile.dir, name), "utf-8"));
14531
14615
  if (!body) throw new Error(`loadSubagents: agent '${name}' has an empty prompt body`);
14532
14616
  const def = {
14533
14617
  description: fm.description ?? `Subagent ${name}`,
@@ -14715,9 +14799,10 @@ async function runExecutable(profileName, input) {
14715
14799
  })
14716
14800
  };
14717
14801
  })() : null;
14718
- const ndjsonDir = path38.join(input.cwd, ".kody");
14802
+ const ndjsonDir = path39.join(input.cwd, ".kody");
14803
+ const staffPersona = typeof profile.staff === "string" && profile.staff.length > 0 ? framePersona(profile.staff, loadStaffPersona(input.cwd, profile.staff)) : null;
14719
14804
  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);
14805
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path39.isAbsolute(p) ? p : path39.resolve(profile.dir, p)).filter((p) => p.length > 0);
14721
14806
  const syntheticPath = ctx.data.syntheticPluginPath;
14722
14807
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
14723
14808
  const agents = loadSubagents(profile);
@@ -14746,7 +14831,7 @@ async function runExecutable(profileName, input) {
14746
14831
  maxTurnTimeoutMs: typeof profile.claudeCode.maxTurnTimeoutSec === "number" ? Math.floor(profile.claudeCode.maxTurnTimeoutSec * 1e3) : void 0,
14747
14832
  // DISCIPLINE leads so the stable, role-agnostic block sits at the front
14748
14833
  // 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,
14834
+ systemPromptAppend: [DISCIPLINE, staffPersona, profile.claudeCode.systemPromptAppend, taskArtifacts?.promptAddendum].filter((s) => typeof s === "string" && s.length > 0).join("\n\n") || void 0,
14750
14835
  cacheable: profile.claudeCode.cacheable,
14751
14836
  enableVerifyTool: profile.claudeCode.enableVerifyTool,
14752
14837
  enableSubmitTool: profile.claudeCode.enableSubmitTool,
@@ -14986,17 +15071,17 @@ function clearStampedLifecycleLabels(profile, ctx) {
14986
15071
  function resolveProfilePath(profileName) {
14987
15072
  const found = resolveExecutable(profileName);
14988
15073
  if (found) return found;
14989
- const here = path38.dirname(new URL(import.meta.url).pathname);
15074
+ const here = path39.dirname(new URL(import.meta.url).pathname);
14990
15075
  const candidates = [
14991
- path38.join(here, "executables", profileName, "profile.json"),
15076
+ path39.join(here, "executables", profileName, "profile.json"),
14992
15077
  // same-dir sibling (dev)
14993
- path38.join(here, "..", "executables", profileName, "profile.json"),
15078
+ path39.join(here, "..", "executables", profileName, "profile.json"),
14994
15079
  // up one (prod: dist/bin → dist/executables)
14995
- path38.join(here, "..", "src", "executables", profileName, "profile.json")
15080
+ path39.join(here, "..", "src", "executables", profileName, "profile.json")
14996
15081
  // fallback
14997
15082
  ];
14998
15083
  for (const c of candidates) {
14999
- if (fs42.existsSync(c)) return c;
15084
+ if (fs43.existsSync(c)) return c;
15000
15085
  }
15001
15086
  return candidates[0];
15002
15087
  }
@@ -15096,8 +15181,8 @@ function resolveShellTimeoutMs(entry) {
15096
15181
  var SIGKILL_GRACE_MS = 5e3;
15097
15182
  async function runShellEntry(entry, ctx, profile) {
15098
15183
  const shellName = entry.shell;
15099
- const shellPath = path38.join(profile.dir, shellName);
15100
- if (!fs42.existsSync(shellPath)) {
15184
+ const shellPath = path39.join(profile.dir, shellName);
15185
+ if (!fs43.existsSync(shellPath)) {
15101
15186
  ctx.skipAgent = true;
15102
15187
  ctx.output.exitCode = 99;
15103
15188
  ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
@@ -15332,9 +15417,9 @@ async function resolveAuthToken(env = process.env) {
15332
15417
  return void 0;
15333
15418
  }
15334
15419
  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";
15420
+ if (fs44.existsSync(path40.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
15421
+ if (fs44.existsSync(path40.join(cwd, "yarn.lock"))) return "yarn";
15422
+ if (fs44.existsSync(path40.join(cwd, "bun.lockb"))) return "bun";
15338
15423
  return "npm";
15339
15424
  }
15340
15425
  function shellOut(cmd, args, cwd, stream = true) {
@@ -15421,11 +15506,11 @@ function configureGitIdentity(cwd) {
15421
15506
  }
15422
15507
  function postFailureTail(issueNumber, cwd, reason) {
15423
15508
  if (!issueNumber) return;
15424
- const logPath = path39.join(cwd, ".kody", "last-run.jsonl");
15509
+ const logPath = path40.join(cwd, ".kody", "last-run.jsonl");
15425
15510
  let tail = "";
15426
15511
  try {
15427
- if (fs43.existsSync(logPath)) {
15428
- const content = fs43.readFileSync(logPath, "utf-8");
15512
+ if (fs44.existsSync(logPath)) {
15513
+ const content = fs44.readFileSync(logPath, "utf-8");
15429
15514
  tail = content.slice(-3e3);
15430
15515
  }
15431
15516
  } catch {
@@ -15450,7 +15535,7 @@ async function runCi(argv) {
15450
15535
  return 0;
15451
15536
  }
15452
15537
  const args = parseCiArgs(argv);
15453
- const cwd = args.cwd ? path39.resolve(args.cwd) : process.cwd();
15538
+ const cwd = args.cwd ? path40.resolve(args.cwd) : process.cwd();
15454
15539
  let earlyConfig;
15455
15540
  try {
15456
15541
  earlyConfig = loadConfig(cwd);
@@ -15460,9 +15545,9 @@ async function runCi(argv) {
15460
15545
  const eventName = process.env.GITHUB_EVENT_NAME;
15461
15546
  const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
15462
15547
  let manualWorkflowDispatch = false;
15463
- if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs43.existsSync(dispatchEventPath)) {
15548
+ if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs44.existsSync(dispatchEventPath)) {
15464
15549
  try {
15465
- const evt = JSON.parse(fs43.readFileSync(dispatchEventPath, "utf-8"));
15550
+ const evt = JSON.parse(fs44.readFileSync(dispatchEventPath, "utf-8"));
15466
15551
  const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
15467
15552
  const sessionInput = String(evt?.inputs?.sessionId ?? "");
15468
15553
  manualWorkflowDispatch = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
@@ -15728,12 +15813,12 @@ function parseChatArgs(argv, env = process.env) {
15728
15813
  return result;
15729
15814
  }
15730
15815
  function commitChatFiles(cwd, sessionId, verbose) {
15731
- const sessionFile = path40.relative(cwd, sessionFilePath(cwd, sessionId));
15732
- const eventsFile = path40.relative(cwd, eventsFilePath(cwd, sessionId));
15816
+ const sessionFile = path41.relative(cwd, sessionFilePath(cwd, sessionId));
15817
+ const eventsFile = path41.relative(cwd, eventsFilePath(cwd, sessionId));
15733
15818
  const safeSession = sessionId.replace(/[^a-zA-Z0-9._-]/g, "_");
15734
- const tasksDir = path40.join(".kody", "tasks", safeSession);
15819
+ const tasksDir = path41.join(".kody", "tasks", safeSession);
15735
15820
  const candidatePaths = [sessionFile, eventsFile, tasksDir];
15736
- const paths = candidatePaths.filter((p) => fs44.existsSync(path40.join(cwd, p)));
15821
+ const paths = candidatePaths.filter((p) => fs45.existsSync(path41.join(cwd, p)));
15737
15822
  if (paths.length === 0) return;
15738
15823
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
15739
15824
  try {
@@ -15777,7 +15862,7 @@ async function runChat(argv) {
15777
15862
  ${CHAT_HELP}`);
15778
15863
  return 64;
15779
15864
  }
15780
- const cwd = args.cwd ? path40.resolve(args.cwd) : process.cwd();
15865
+ const cwd = args.cwd ? path41.resolve(args.cwd) : process.cwd();
15781
15866
  const sessionId = args.sessionId;
15782
15867
  const unpackedSecrets = unpackAllSecrets();
15783
15868
  if (unpackedSecrets > 0) {
@@ -15829,7 +15914,7 @@ ${CHAT_HELP}`);
15829
15914
  const sink = buildSink(cwd, sessionId, args.dashboardUrl);
15830
15915
  const meta = readMeta(sessionFile);
15831
15916
  process.stdout.write(
15832
- `\u2192 kody:chat: session file=${sessionFile} exists=${fs44.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
15917
+ `\u2192 kody:chat: session file=${sessionFile} exists=${fs45.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
15833
15918
  `
15834
15919
  );
15835
15920
  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.2",
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",