@kody-ade/kody-engine 0.4.202 → 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,7 +551,11 @@ __export(dutyMcp_exports, {
551
551
  dispatchWorkflow: () => dispatchWorkflow,
552
552
  ensureComment: () => ensureComment,
553
553
  ensureIssue: () => ensureIssue,
554
- readCheckRuns: () => readCheckRuns
554
+ isDispatchGated: () => isDispatchGated,
555
+ parseDutyTrustMode: () => parseDutyTrustMode,
556
+ readCheckRuns: () => readCheckRuns,
557
+ readDutyTrustMode: () => readDutyTrustMode,
558
+ readThread: () => readThread
555
559
  });
556
560
  import { createSdkMcpServer as createSdkMcpServer3, tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
557
561
  import { z as z3 } from "zod";
@@ -645,6 +649,42 @@ function readLedger(label) {
645
649
  return { found: false, payload: { error: err instanceof Error ? err.message : String(err) } };
646
650
  }
647
651
  }
652
+ function parseDutyTrustMode(rawJson, dutySlug) {
653
+ try {
654
+ const parsed = JSON.parse(rawJson);
655
+ return parsed?.duties?.[dutySlug]?.mode === "auto" ? "auto" : "ask";
656
+ } catch {
657
+ return "ask";
658
+ }
659
+ }
660
+ function readDutyTrustMode(repoSlug, dutySlug) {
661
+ if (!dutySlug) return "ask";
662
+ try {
663
+ const b64 = gh(["api", `repos/${repoSlug}/contents/${TRUST_FILE_PATH}?ref=${TRUST_STATE_BRANCH}`, "--jq", ".content"]);
664
+ const json = Buffer.from(b64.trim(), "base64").toString("utf-8");
665
+ return parseDutyTrustMode(json, dutySlug);
666
+ } catch {
667
+ return "ask";
668
+ }
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
+ }
648
688
  function readCheckRuns(repoSlug, ref, ignoreNames) {
649
689
  const sha = gh(["api", `repos/${repoSlug}/commits/${ref}`, "--jq", ".sha"]).trim();
650
690
  const raw = gh([
@@ -703,6 +743,14 @@ function dispatchWorkflow(workflowFile, executable, issueNumber) {
703
743
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
704
744
  }
705
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
+ }
751
+ function trustRefusal(dutySlug) {
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.`;
753
+ }
706
754
  function buildDutyMcpServer(opts) {
707
755
  const workflowFile = opts.workflowFile ?? "kody.yml";
708
756
  const listTool = tool3(
@@ -728,6 +776,9 @@ function buildDutyMcpServer(opts) {
728
776
  pr: z3.number().int().positive().describe("PR number to repair.")
729
777
  },
730
778
  async (args) => {
779
+ if (isDispatchGated(verb, readDutyTrustMode(opts.repoSlug, opts.dutySlug))) {
780
+ return { content: [{ type: "text", text: trustRefusal(opts.dutySlug) }] };
781
+ }
731
782
  const result = dispatchVerb(workflowFile, verb, args.pr);
732
783
  const text = result.ok ? `Dispatched \`${verb}\` on PR #${args.pr}. The repair runs in its own workflow_dispatch \u2014 wait for the next tick to see the new headSha.` : `Dispatch failed for \`${verb}\` on PR #${args.pr}: ${result.error}`;
733
784
  return {
@@ -792,6 +843,18 @@ function buildDutyMcpServer(opts) {
792
843
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
793
844
  }
794
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
+ );
795
858
  const ensureIssueTool = tool3(
796
859
  "ensure_issue",
797
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.",
@@ -830,6 +893,9 @@ function buildDutyMcpServer(opts) {
830
893
  issueNumber: z3.number().int().positive().describe("Issue (or PR) number forwarded as issue_number.")
831
894
  },
832
895
  async (args) => {
896
+ if (isDispatchGated(args.executable, readDutyTrustMode(opts.repoSlug, opts.dutySlug))) {
897
+ return { content: [{ type: "text", text: trustRefusal(opts.dutySlug) }] };
898
+ }
833
899
  const result = dispatchWorkflow(workflowFile, args.executable, args.issueNumber);
834
900
  const text = result.ok ? `Dispatched \`${args.executable}\` on #${args.issueNumber} via workflow_dispatch.` : `Dispatch failed for \`${args.executable}\` on #${args.issueNumber}: ${result.error}`;
835
901
  return { content: [{ type: "text", text }] };
@@ -846,6 +912,7 @@ function buildDutyMcpServer(opts) {
846
912
  recommendTool,
847
913
  ledgerTool,
848
914
  checkRunsTool,
915
+ readThreadTool,
849
916
  ensureIssueTool,
850
917
  ensureCommentTool,
851
918
  dispatchTool
@@ -853,17 +920,21 @@ function buildDutyMcpServer(opts) {
853
920
  });
854
921
  return { server };
855
922
  }
856
- var FAIL_CONCLUSIONS, RUNNING_STATUSES, 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;
857
924
  var init_dutyMcp = __esm({
858
925
  "src/dutyMcp.ts"() {
859
926
  "use strict";
860
927
  init_issue();
861
928
  FAIL_CONCLUSIONS = /* @__PURE__ */ new Set(["FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE", "CANCELLED"]);
862
929
  RUNNING_STATUSES = /* @__PURE__ */ new Set(["IN_PROGRESS", "QUEUED", "PENDING", "WAITING", "REQUESTED"]);
930
+ TRUST_FILE_PATH = ".kody/state/trust.json";
931
+ TRUST_STATE_BRANCH = "kody-state";
932
+ THREAD_BODY_MAX = 4e3;
863
933
  CHECK_FAIL_CONCLUSIONS = /* @__PURE__ */ new Set(["FAILURE", "TIMED_OUT", "STARTUP_FAILURE", "ACTION_REQUIRED"]);
864
934
  DEFAULT_IGNORE_CHECKS = ["run", "kody", "job-tick", "goal-tick", "worker-ask", "chat"];
865
935
  trackMarker = (key) => `<!-- kody-track:${key} -->`;
866
936
  commentMarker = (key) => `<!-- kody-track-comment:${key} -->`;
937
+ GATE_EXEMPT_EXECUTABLES = /* @__PURE__ */ new Set(["qa-engineer", "ui-review"]);
867
938
  DUTY_MCP_TOOL_NAMES = [
868
939
  "list_prs_to_repair",
869
940
  "sync_pr",
@@ -872,6 +943,7 @@ var init_dutyMcp = __esm({
872
943
  "recommend_to_operator",
873
944
  "read_ledger",
874
945
  "read_check_runs",
946
+ "read_thread",
875
947
  "ensure_issue",
876
948
  "ensure_comment",
877
949
  "dispatch_workflow"
@@ -1411,7 +1483,7 @@ var init_loadCoverageRules = __esm({
1411
1483
  // package.json
1412
1484
  var package_default = {
1413
1485
  name: "@kody-ade/kody-engine",
1414
- version: "0.4.202",
1486
+ version: "0.4.204-next.0",
1415
1487
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
1416
1488
  license: "MIT",
1417
1489
  type: "module",
@@ -1469,8 +1541,8 @@ var package_default = {
1469
1541
 
1470
1542
  // src/chat-cli.ts
1471
1543
  import { execFileSync as execFileSync29 } from "child_process";
1472
- import * as fs44 from "fs";
1473
- import * as path40 from "path";
1544
+ import * as fs45 from "fs";
1545
+ import * as path41 from "path";
1474
1546
 
1475
1547
  // src/chat/events.ts
1476
1548
  import * as fs from "fs";
@@ -3218,8 +3290,8 @@ async function emit2(sink, type, sessionId, suffix, payload) {
3218
3290
 
3219
3291
  // src/kody-cli.ts
3220
3292
  import { execFileSync as execFileSync28 } from "child_process";
3221
- import * as fs43 from "fs";
3222
- import * as path39 from "path";
3293
+ import * as fs44 from "fs";
3294
+ import * as path40 from "path";
3223
3295
 
3224
3296
  // src/app-auth.ts
3225
3297
  import { createSign } from "crypto";
@@ -3645,8 +3717,8 @@ function coerceBare(spec, value) {
3645
3717
 
3646
3718
  // src/executor.ts
3647
3719
  import { spawn as spawn10 } from "child_process";
3648
- import * as fs42 from "fs";
3649
- import * as path38 from "path";
3720
+ import * as fs43 from "fs";
3721
+ import * as path39 from "path";
3650
3722
 
3651
3723
  // src/container.ts
3652
3724
  init_events();
@@ -3882,6 +3954,8 @@ function loadProfile(profilePath) {
3882
3954
  const profile = {
3883
3955
  name: requireString(profilePath, r, "name"),
3884
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,
3885
3959
  role,
3886
3960
  kind,
3887
3961
  schedule: typeof r.schedule === "string" ? r.schedule : void 0,
@@ -6311,10 +6385,10 @@ import * as fs25 from "fs";
6311
6385
  import * as path22 from "path";
6312
6386
  var VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "awaiting-merge", "done"]);
6313
6387
  var GoalStateError = class extends Error {
6314
- constructor(path41, message) {
6315
- super(`Invalid goal state at ${path41}:
6388
+ constructor(path42, message) {
6389
+ super(`Invalid goal state at ${path42}:
6316
6390
  ${message}`);
6317
- this.path = path41;
6391
+ this.path = path42;
6318
6392
  this.name = "GoalStateError";
6319
6393
  }
6320
6394
  path;
@@ -10962,8 +11036,8 @@ var FlyClient = class {
10962
11036
  get fetch() {
10963
11037
  return this.opts.fetchImpl ?? fetch;
10964
11038
  }
10965
- async call(path41, init = {}) {
10966
- const res = await this.fetch(`${FLY_API_BASE}${path41}`, {
11039
+ async call(path42, init = {}) {
11040
+ const res = await this.fetch(`${FLY_API_BASE}${path42}`, {
10967
11041
  method: init.method ?? "GET",
10968
11042
  headers: {
10969
11043
  Authorization: `Bearer ${this.opts.token}`,
@@ -10974,7 +11048,7 @@ var FlyClient = class {
10974
11048
  if (res.status === 404 && init.allow404) return null;
10975
11049
  if (!res.ok) {
10976
11050
  const text = await res.text().catch(() => "");
10977
- 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}`);
10978
11052
  }
10979
11053
  if (res.status === 204) return null;
10980
11054
  const raw = await res.text();
@@ -14470,9 +14544,41 @@ var allScriptNames = /* @__PURE__ */ new Set([
14470
14544
  ...Object.keys(postflightScripts)
14471
14545
  ]);
14472
14546
 
14473
- // src/subagents.ts
14547
+ // src/staff.ts
14474
14548
  import * as fs41 from "fs";
14475
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";
14476
14582
  function splitFrontmatter2(raw) {
14477
14583
  const match = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(raw);
14478
14584
  if (!match) return { fm: {}, body: raw.trim() };
@@ -14485,10 +14591,10 @@ function splitFrontmatter2(raw) {
14485
14591
  return { fm, body: (match[2] ?? "").trim() };
14486
14592
  }
14487
14593
  function resolveAgentFile(profileDir, name) {
14488
- const local = path37.join(profileDir, "agents", `${name}.md`);
14489
- if (fs41.existsSync(local)) return local;
14490
- const central = path37.join(getPluginsCatalogRoot(), "agents", `${name}.md`);
14491
- 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;
14492
14598
  throw new Error(`loadSubagents: agent '${name}' not found in ${profileDir}/agents/ or shared catalog`);
14493
14599
  }
14494
14600
  function loadSubagents(profile) {
@@ -14496,7 +14602,7 @@ function loadSubagents(profile) {
14496
14602
  if (!names || names.length === 0) return void 0;
14497
14603
  const agents = {};
14498
14604
  for (const name of names) {
14499
- 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"));
14500
14606
  if (!body) throw new Error(`loadSubagents: agent '${name}' has an empty prompt body`);
14501
14607
  const def = {
14502
14608
  description: fm.description ?? `Subagent ${name}`,
@@ -14684,9 +14790,10 @@ async function runExecutable(profileName, input) {
14684
14790
  })
14685
14791
  };
14686
14792
  })() : null;
14687
- 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;
14688
14795
  const invokeAgent = async (prompt) => {
14689
- 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);
14690
14797
  const syntheticPath = ctx.data.syntheticPluginPath;
14691
14798
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
14692
14799
  const agents = loadSubagents(profile);
@@ -14715,7 +14822,7 @@ async function runExecutable(profileName, input) {
14715
14822
  maxTurnTimeoutMs: typeof profile.claudeCode.maxTurnTimeoutSec === "number" ? Math.floor(profile.claudeCode.maxTurnTimeoutSec * 1e3) : void 0,
14716
14823
  // DISCIPLINE leads so the stable, role-agnostic block sits at the front
14717
14824
  // of the cacheable system-prompt prefix; profile/task appends follow.
14718
- 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,
14719
14826
  cacheable: profile.claudeCode.cacheable,
14720
14827
  enableVerifyTool: profile.claudeCode.enableVerifyTool,
14721
14828
  enableSubmitTool: profile.claudeCode.enableSubmitTool,
@@ -14955,17 +15062,17 @@ function clearStampedLifecycleLabels(profile, ctx) {
14955
15062
  function resolveProfilePath(profileName) {
14956
15063
  const found = resolveExecutable(profileName);
14957
15064
  if (found) return found;
14958
- const here = path38.dirname(new URL(import.meta.url).pathname);
15065
+ const here = path39.dirname(new URL(import.meta.url).pathname);
14959
15066
  const candidates = [
14960
- path38.join(here, "executables", profileName, "profile.json"),
15067
+ path39.join(here, "executables", profileName, "profile.json"),
14961
15068
  // same-dir sibling (dev)
14962
- path38.join(here, "..", "executables", profileName, "profile.json"),
15069
+ path39.join(here, "..", "executables", profileName, "profile.json"),
14963
15070
  // up one (prod: dist/bin → dist/executables)
14964
- path38.join(here, "..", "src", "executables", profileName, "profile.json")
15071
+ path39.join(here, "..", "src", "executables", profileName, "profile.json")
14965
15072
  // fallback
14966
15073
  ];
14967
15074
  for (const c of candidates) {
14968
- if (fs42.existsSync(c)) return c;
15075
+ if (fs43.existsSync(c)) return c;
14969
15076
  }
14970
15077
  return candidates[0];
14971
15078
  }
@@ -15065,8 +15172,8 @@ function resolveShellTimeoutMs(entry) {
15065
15172
  var SIGKILL_GRACE_MS = 5e3;
15066
15173
  async function runShellEntry(entry, ctx, profile) {
15067
15174
  const shellName = entry.shell;
15068
- const shellPath = path38.join(profile.dir, shellName);
15069
- if (!fs42.existsSync(shellPath)) {
15175
+ const shellPath = path39.join(profile.dir, shellName);
15176
+ if (!fs43.existsSync(shellPath)) {
15070
15177
  ctx.skipAgent = true;
15071
15178
  ctx.output.exitCode = 99;
15072
15179
  ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
@@ -15301,9 +15408,9 @@ async function resolveAuthToken(env = process.env) {
15301
15408
  return void 0;
15302
15409
  }
15303
15410
  function detectPackageManager2(cwd) {
15304
- if (fs43.existsSync(path39.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
15305
- if (fs43.existsSync(path39.join(cwd, "yarn.lock"))) return "yarn";
15306
- 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";
15307
15414
  return "npm";
15308
15415
  }
15309
15416
  function shellOut(cmd, args, cwd, stream = true) {
@@ -15390,11 +15497,11 @@ function configureGitIdentity(cwd) {
15390
15497
  }
15391
15498
  function postFailureTail(issueNumber, cwd, reason) {
15392
15499
  if (!issueNumber) return;
15393
- const logPath = path39.join(cwd, ".kody", "last-run.jsonl");
15500
+ const logPath = path40.join(cwd, ".kody", "last-run.jsonl");
15394
15501
  let tail = "";
15395
15502
  try {
15396
- if (fs43.existsSync(logPath)) {
15397
- const content = fs43.readFileSync(logPath, "utf-8");
15503
+ if (fs44.existsSync(logPath)) {
15504
+ const content = fs44.readFileSync(logPath, "utf-8");
15398
15505
  tail = content.slice(-3e3);
15399
15506
  }
15400
15507
  } catch {
@@ -15419,7 +15526,7 @@ async function runCi(argv) {
15419
15526
  return 0;
15420
15527
  }
15421
15528
  const args = parseCiArgs(argv);
15422
- const cwd = args.cwd ? path39.resolve(args.cwd) : process.cwd();
15529
+ const cwd = args.cwd ? path40.resolve(args.cwd) : process.cwd();
15423
15530
  let earlyConfig;
15424
15531
  try {
15425
15532
  earlyConfig = loadConfig(cwd);
@@ -15429,9 +15536,9 @@ async function runCi(argv) {
15429
15536
  const eventName = process.env.GITHUB_EVENT_NAME;
15430
15537
  const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
15431
15538
  let manualWorkflowDispatch = false;
15432
- if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs43.existsSync(dispatchEventPath)) {
15539
+ if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs44.existsSync(dispatchEventPath)) {
15433
15540
  try {
15434
- const evt = JSON.parse(fs43.readFileSync(dispatchEventPath, "utf-8"));
15541
+ const evt = JSON.parse(fs44.readFileSync(dispatchEventPath, "utf-8"));
15435
15542
  const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
15436
15543
  const sessionInput = String(evt?.inputs?.sessionId ?? "");
15437
15544
  manualWorkflowDispatch = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
@@ -15697,12 +15804,12 @@ function parseChatArgs(argv, env = process.env) {
15697
15804
  return result;
15698
15805
  }
15699
15806
  function commitChatFiles(cwd, sessionId, verbose) {
15700
- const sessionFile = path40.relative(cwd, sessionFilePath(cwd, sessionId));
15701
- 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));
15702
15809
  const safeSession = sessionId.replace(/[^a-zA-Z0-9._-]/g, "_");
15703
- const tasksDir = path40.join(".kody", "tasks", safeSession);
15810
+ const tasksDir = path41.join(".kody", "tasks", safeSession);
15704
15811
  const candidatePaths = [sessionFile, eventsFile, tasksDir];
15705
- const paths = candidatePaths.filter((p) => fs44.existsSync(path40.join(cwd, p)));
15812
+ const paths = candidatePaths.filter((p) => fs45.existsSync(path41.join(cwd, p)));
15706
15813
  if (paths.length === 0) return;
15707
15814
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
15708
15815
  try {
@@ -15746,7 +15853,7 @@ async function runChat(argv) {
15746
15853
  ${CHAT_HELP}`);
15747
15854
  return 64;
15748
15855
  }
15749
- const cwd = args.cwd ? path40.resolve(args.cwd) : process.cwd();
15856
+ const cwd = args.cwd ? path41.resolve(args.cwd) : process.cwd();
15750
15857
  const sessionId = args.sessionId;
15751
15858
  const unpackedSecrets = unpackAllSecrets();
15752
15859
  if (unpackedSecrets > 0) {
@@ -15798,7 +15905,7 @@ ${CHAT_HELP}`);
15798
15905
  const sink = buildSink(cwd, sessionId, args.dashboardUrl);
15799
15906
  const meta = readMeta(sessionFile);
15800
15907
  process.stdout.write(
15801
- `\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"}
15802
15909
  `
15803
15910
  );
15804
15911
  try {
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -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.202",
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",
@@ -12,6 +12,25 @@
12
12
  "templates",
13
13
  "kody.config.schema.json"
14
14
  ],
15
+ "scripts": {
16
+ "kody:run": "tsx bin/kody.ts",
17
+ "serve": "tsx bin/kody.ts serve",
18
+ "serve:vscode": "tsx bin/kody.ts serve vscode",
19
+ "serve:claude": "tsx bin/kody.ts serve claude",
20
+ "build": "tsup && node scripts/copy-assets.cjs",
21
+ "check:modularity": "tsx scripts/check-script-modularity.ts",
22
+ "pretest": "pnpm check:modularity",
23
+ "test": "vitest run tests/unit tests/int --coverage",
24
+ "test:smoke": "vitest run tests/smoke --no-coverage",
25
+ "test:e2e": "vitest run tests/e2e --no-coverage",
26
+ "test:all": "vitest run tests --no-coverage",
27
+ "typecheck": "tsc --noEmit",
28
+ "lint": "biome check",
29
+ "lint:fix": "biome check --write",
30
+ "format": "biome format --write",
31
+ "brain:publish": "docker buildx build --platform linux/amd64 -f runner/Dockerfile.brain -t ghcr.io/${KODY_BRAIN_GHCR_OWNER:-aharonyaircohen}/kody-brain:latest --push runner",
32
+ "prepublishOnly": "pnpm typecheck && vitest run tests/unit tests/int --no-coverage && pnpm build"
33
+ },
15
34
  "dependencies": {
16
35
  "@actions/cache": "^6.0.0",
17
36
  "@anthropic-ai/claude-agent-sdk": "0.2.119",
@@ -34,23 +53,5 @@
34
53
  "url": "git+https://github.com/aharonyaircohen/kody-engine.git"
35
54
  },
36
55
  "homepage": "https://github.com/aharonyaircohen/kody-engine",
37
- "bugs": "https://github.com/aharonyaircohen/kody-engine/issues",
38
- "scripts": {
39
- "kody:run": "tsx bin/kody.ts",
40
- "serve": "tsx bin/kody.ts serve",
41
- "serve:vscode": "tsx bin/kody.ts serve vscode",
42
- "serve:claude": "tsx bin/kody.ts serve claude",
43
- "build": "tsup && node scripts/copy-assets.cjs",
44
- "check:modularity": "tsx scripts/check-script-modularity.ts",
45
- "pretest": "pnpm check:modularity",
46
- "test": "vitest run tests/unit tests/int --coverage",
47
- "test:smoke": "vitest run tests/smoke --no-coverage",
48
- "test:e2e": "vitest run tests/e2e --no-coverage",
49
- "test:all": "vitest run tests --no-coverage",
50
- "typecheck": "tsc --noEmit",
51
- "lint": "biome check",
52
- "lint:fix": "biome check --write",
53
- "format": "biome format --write",
54
- "brain:publish": "docker buildx build --platform linux/amd64 -f runner/Dockerfile.brain -t ghcr.io/${KODY_BRAIN_GHCR_OWNER:-aharonyaircohen}/kody-brain:latest --push runner"
55
- }
56
- }
56
+ "bugs": "https://github.com/aharonyaircohen/kody-engine/issues"
57
+ }