@kody-ade/kody-engine 0.4.95 → 0.4.96

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
@@ -626,28 +626,28 @@ var loadMemoryContext_exports = {};
626
626
  __export(loadMemoryContext_exports, {
627
627
  loadMemoryContext: () => loadMemoryContext
628
628
  });
629
- import * as fs31 from "fs";
630
- import * as path29 from "path";
629
+ import * as fs32 from "fs";
630
+ import * as path30 from "path";
631
631
  function collectPages(memoryAbs) {
632
632
  const out = [];
633
633
  walkMd(memoryAbs, (file) => {
634
634
  let stat;
635
635
  try {
636
- stat = fs31.statSync(file);
636
+ stat = fs32.statSync(file);
637
637
  } catch {
638
638
  return;
639
639
  }
640
640
  let raw;
641
641
  try {
642
- raw = fs31.readFileSync(file, "utf-8");
642
+ raw = fs32.readFileSync(file, "utf-8");
643
643
  } catch {
644
644
  return;
645
645
  }
646
646
  const fm = raw.match(/^---\s*\n([\s\S]*?)\n---/);
647
- const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path29.basename(file, ".md");
647
+ const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path30.basename(file, ".md");
648
648
  const updated = fm?.[1]?.match(/^updated:\s*([0-9T:.+\-Z]+)/m)?.[1]?.trim() ?? "";
649
649
  out.push({
650
- relPath: path29.relative(memoryAbs, file),
650
+ relPath: path30.relative(memoryAbs, file),
651
651
  title,
652
652
  updated,
653
653
  content: raw.length > PER_PAGE_MAX_BYTES ? raw.slice(0, PER_PAGE_MAX_BYTES) + TRUNCATED_SUFFIX : raw,
@@ -715,16 +715,16 @@ function walkMd(root, visit) {
715
715
  const dir = stack.pop();
716
716
  let names;
717
717
  try {
718
- names = fs31.readdirSync(dir);
718
+ names = fs32.readdirSync(dir);
719
719
  } catch {
720
720
  continue;
721
721
  }
722
722
  for (const name of names) {
723
723
  if (name.startsWith(".")) continue;
724
- const full = path29.join(dir, name);
724
+ const full = path30.join(dir, name);
725
725
  let stat;
726
726
  try {
727
- stat = fs31.statSync(full);
727
+ stat = fs32.statSync(full);
728
728
  } catch {
729
729
  continue;
730
730
  }
@@ -747,8 +747,8 @@ var init_loadMemoryContext = __esm({
747
747
  TRUNCATED_SUFFIX = "\n\n\u2026 (truncated)";
748
748
  loadMemoryContext = async (ctx) => {
749
749
  if (typeof ctx.data.memoryContext === "string") return;
750
- const memoryAbs = path29.join(ctx.cwd, MEMORY_DIR_RELATIVE);
751
- if (!fs31.existsSync(memoryAbs)) {
750
+ const memoryAbs = path30.join(ctx.cwd, MEMORY_DIR_RELATIVE);
751
+ if (!fs32.existsSync(memoryAbs)) {
752
752
  ctx.data.memoryContext = "";
753
753
  return;
754
754
  }
@@ -877,7 +877,7 @@ var init_loadPriorArt = __esm({
877
877
  // package.json
878
878
  var package_default = {
879
879
  name: "@kody-ade/kody-engine",
880
- version: "0.4.95",
880
+ version: "0.4.96",
881
881
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
882
882
  license: "MIT",
883
883
  type: "module",
@@ -932,8 +932,8 @@ var package_default = {
932
932
 
933
933
  // src/chat-cli.ts
934
934
  import { execFileSync as execFileSync31 } from "child_process";
935
- import * as fs37 from "fs";
936
- import * as path34 from "path";
935
+ import * as fs38 from "fs";
936
+ import * as path35 from "path";
937
937
 
938
938
  // src/chat/events.ts
939
939
  import * as fs from "fs";
@@ -2255,8 +2255,8 @@ async function emit2(sink, type, sessionId, suffix, payload) {
2255
2255
 
2256
2256
  // src/kody-cli.ts
2257
2257
  import { execFileSync as execFileSync30 } from "child_process";
2258
- import * as fs36 from "fs";
2259
- import * as path33 from "path";
2258
+ import * as fs37 from "fs";
2259
+ import * as path34 from "path";
2260
2260
 
2261
2261
  // src/dispatch.ts
2262
2262
  import * as fs10 from "fs";
@@ -2578,8 +2578,8 @@ init_issue();
2578
2578
 
2579
2579
  // src/executor.ts
2580
2580
  import { execFileSync as execFileSync29, spawn as spawn6 } from "child_process";
2581
- import * as fs35 from "fs";
2582
- import * as path32 from "path";
2581
+ import * as fs36 from "fs";
2582
+ import * as path33 from "path";
2583
2583
  init_events();
2584
2584
 
2585
2585
  // src/lifecycleLabels.ts
@@ -7983,10 +7983,10 @@ import * as fs29 from "fs";
7983
7983
  import * as path27 from "path";
7984
7984
  var VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "awaiting-merge", "done"]);
7985
7985
  var GoalStateError = class extends Error {
7986
- constructor(path35, message) {
7987
- super(`Invalid goal state at ${path35}:
7986
+ constructor(path36, message) {
7987
+ super(`Invalid goal state at ${path36}:
7988
7988
  ${message}`);
7989
- this.path = path35;
7989
+ this.path = path36;
7990
7990
  this.name = "GoalStateError";
7991
7991
  }
7992
7992
  path;
@@ -8212,6 +8212,79 @@ function humanizeSlug(slug) {
8212
8212
  return slug.split(/[-_]+/).filter((s) => s.length > 0).map((s) => s[0].toUpperCase() + s.slice(1)).join(" ");
8213
8213
  }
8214
8214
 
8215
+ // src/scripts/loadWorkerAdhoc.ts
8216
+ import * as fs31 from "fs";
8217
+ import * as path29 from "path";
8218
+ var loadWorkerAdhoc = async (ctx, _profile, args) => {
8219
+ const workersDir = String(args?.workersDir ?? ".kody/workers");
8220
+ const workerSlug = String(ctx.args.worker ?? "").trim();
8221
+ if (!workerSlug) {
8222
+ throw new Error("loadWorkerAdhoc: ctx.args.worker must be a non-empty slug");
8223
+ }
8224
+ const workerPath = path29.join(ctx.cwd, workersDir, `${workerSlug}.md`);
8225
+ if (!fs31.existsSync(workerPath)) {
8226
+ throw new Error(`loadWorkerAdhoc: worker persona not found: ${workerPath}`);
8227
+ }
8228
+ const { title, body } = parsePersona(fs31.readFileSync(workerPath, "utf-8"), workerSlug);
8229
+ const message = resolveMessage(ctx.args.message);
8230
+ if (!message) {
8231
+ throw new Error(
8232
+ "loadWorkerAdhoc: no message \u2014 neither the dispatching comment body nor ctx.args.message provided one"
8233
+ );
8234
+ }
8235
+ ctx.data.workerSlug = workerSlug;
8236
+ ctx.data.workerTitle = title;
8237
+ ctx.data.workerPersona = body;
8238
+ ctx.data.message = message;
8239
+ ctx.data.thread = String(ctx.args.thread ?? "").trim();
8240
+ };
8241
+ function resolveMessage(messageArg) {
8242
+ const fromComment = readCommentBody();
8243
+ if (fromComment) return stripDirective(fromComment);
8244
+ return String(messageArg ?? "").trim();
8245
+ }
8246
+ function readCommentBody() {
8247
+ const eventPath = process.env.GITHUB_EVENT_PATH;
8248
+ if (!eventPath || !fs31.existsSync(eventPath)) return "";
8249
+ try {
8250
+ const event = JSON.parse(fs31.readFileSync(eventPath, "utf-8"));
8251
+ return String(event.comment?.body ?? "");
8252
+ } catch {
8253
+ return "";
8254
+ }
8255
+ }
8256
+ function stripDirective(body) {
8257
+ const lines = body.split("\n");
8258
+ let start = 0;
8259
+ while (start < lines.length) {
8260
+ const line = lines[start].trim();
8261
+ if (line.length === 0) {
8262
+ start++;
8263
+ continue;
8264
+ }
8265
+ if (/@kody\s+worker-ask\b/i.test(line)) {
8266
+ start++;
8267
+ continue;
8268
+ }
8269
+ break;
8270
+ }
8271
+ return lines.slice(start).join("\n").trim();
8272
+ }
8273
+ function parsePersona(raw, slug) {
8274
+ const stripped = splitFrontmatter(raw).body;
8275
+ const trimmed = stripped.trim();
8276
+ const firstLine2 = trimmed.split("\n", 1)[0] ?? "";
8277
+ const h1 = /^#\s+(.+?)\s*$/.exec(firstLine2);
8278
+ if (h1) {
8279
+ const rest = trimmed.slice(firstLine2.length).replace(/^\n+/, "");
8280
+ return { title: h1[1].trim(), body: rest };
8281
+ }
8282
+ return { title: humanizeSlug2(slug), body: trimmed };
8283
+ }
8284
+ function humanizeSlug2(slug) {
8285
+ return slug.split(/[-_]+/).filter((s) => s.length > 0).map((s) => s[0].toUpperCase() + s.slice(1)).join(" ");
8286
+ }
8287
+
8215
8288
  // src/scripts/index.ts
8216
8289
  init_loadMemoryContext();
8217
8290
  init_loadPriorArt();
@@ -8220,8 +8293,8 @@ init_loadPriorArt();
8220
8293
  init_events();
8221
8294
 
8222
8295
  // src/taskContext.ts
8223
- import * as fs32 from "fs";
8224
- import * as path30 from "path";
8296
+ import * as fs33 from "fs";
8297
+ import * as path31 from "path";
8225
8298
  var TASK_CONTEXT_SCHEMA_VERSION = 1;
8226
8299
  function buildTaskContext(args) {
8227
8300
  return {
@@ -8237,10 +8310,10 @@ function buildTaskContext(args) {
8237
8310
  }
8238
8311
  function persistTaskContext(cwd, ctx) {
8239
8312
  try {
8240
- const dir = path30.join(cwd, ".kody", "runs", ctx.runId);
8241
- fs32.mkdirSync(dir, { recursive: true });
8242
- const file = path30.join(dir, "task-context.json");
8243
- fs32.writeFileSync(file, `${JSON.stringify(ctx, null, 2)}
8313
+ const dir = path31.join(cwd, ".kody", "runs", ctx.runId);
8314
+ fs33.mkdirSync(dir, { recursive: true });
8315
+ const file = path31.join(dir, "task-context.json");
8316
+ fs33.writeFileSync(file, `${JSON.stringify(ctx, null, 2)}
8244
8317
  `);
8245
8318
  return file;
8246
8319
  } catch (err) {
@@ -9603,8 +9676,8 @@ function resolveBaseOverride(value) {
9603
9676
 
9604
9677
  // src/scripts/runTickScript.ts
9605
9678
  import { spawnSync } from "child_process";
9606
- import * as fs33 from "fs";
9607
- import * as path31 from "path";
9679
+ import * as fs34 from "fs";
9680
+ import * as path32 from "path";
9608
9681
  var runTickScript = async (ctx, _profile, args) => {
9609
9682
  ctx.skipAgent = true;
9610
9683
  const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
@@ -9616,13 +9689,13 @@ var runTickScript = async (ctx, _profile, args) => {
9616
9689
  ctx.output.reason = `runTickScript: ctx.args.${slugArg} must be a non-empty slug`;
9617
9690
  return;
9618
9691
  }
9619
- const jobPath = path31.join(ctx.cwd, jobsDir, `${slug}.md`);
9620
- if (!fs33.existsSync(jobPath)) {
9692
+ const jobPath = path32.join(ctx.cwd, jobsDir, `${slug}.md`);
9693
+ if (!fs34.existsSync(jobPath)) {
9621
9694
  ctx.output.exitCode = 99;
9622
9695
  ctx.output.reason = `runTickScript: job file not found: ${jobPath}`;
9623
9696
  return;
9624
9697
  }
9625
- const raw = fs33.readFileSync(jobPath, "utf-8");
9698
+ const raw = fs34.readFileSync(jobPath, "utf-8");
9626
9699
  const { frontmatter } = splitFrontmatter(raw);
9627
9700
  const tickScript = frontmatter.tickScript;
9628
9701
  if (!tickScript) {
@@ -9630,8 +9703,8 @@ var runTickScript = async (ctx, _profile, args) => {
9630
9703
  ctx.output.reason = `runTickScript: job ${slug} has no \`tickScript:\` frontmatter \u2014 route via job-tick instead`;
9631
9704
  return;
9632
9705
  }
9633
- const scriptPath = path31.isAbsolute(tickScript) ? tickScript : path31.join(ctx.cwd, tickScript);
9634
- if (!fs33.existsSync(scriptPath)) {
9706
+ const scriptPath = path32.isAbsolute(tickScript) ? tickScript : path32.join(ctx.cwd, tickScript);
9707
+ if (!fs34.existsSync(scriptPath)) {
9635
9708
  ctx.output.exitCode = 99;
9636
9709
  ctx.output.reason = `runTickScript: tickScript not found: ${scriptPath}`;
9637
9710
  return;
@@ -10653,7 +10726,7 @@ var writeJobStateFile = async (ctx, _profile, _agentResult, args) => {
10653
10726
  };
10654
10727
 
10655
10728
  // src/scripts/writeRunSummary.ts
10656
- import * as fs34 from "fs";
10729
+ import * as fs35 from "fs";
10657
10730
  var writeRunSummary = async (ctx, profile) => {
10658
10731
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
10659
10732
  if (!summaryPath) return;
@@ -10675,7 +10748,7 @@ var writeRunSummary = async (ctx, profile) => {
10675
10748
  if (reason) lines.push(`- **Reason:** ${reason}`);
10676
10749
  lines.push("");
10677
10750
  try {
10678
- fs34.appendFileSync(summaryPath, `${lines.join("\n")}
10751
+ fs35.appendFileSync(summaryPath, `${lines.join("\n")}
10679
10752
  `);
10680
10753
  } catch {
10681
10754
  }
@@ -10696,6 +10769,7 @@ var preflightScripts = {
10696
10769
  loadIssueContext,
10697
10770
  loadIssueStateComment,
10698
10771
  loadJobFromFile,
10772
+ loadWorkerAdhoc,
10699
10773
  loadConventions,
10700
10774
  loadCoverageRules,
10701
10775
  loadMemoryContext,
@@ -10900,9 +10974,9 @@ async function runExecutable(profileName, input) {
10900
10974
  data: { ...input.preloadedData ?? {} },
10901
10975
  output: { exitCode: 0 }
10902
10976
  };
10903
- const ndjsonDir = path32.join(input.cwd, ".kody");
10977
+ const ndjsonDir = path33.join(input.cwd, ".kody");
10904
10978
  const invokeAgent = async (prompt) => {
10905
- const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path32.isAbsolute(p) ? p : path32.resolve(profile.dir, p)).filter((p) => p.length > 0);
10979
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path33.isAbsolute(p) ? p : path33.resolve(profile.dir, p)).filter((p) => p.length > 0);
10906
10980
  const syntheticPath = ctx.data.syntheticPluginPath;
10907
10981
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
10908
10982
  return runAgent({
@@ -11097,7 +11171,7 @@ function clearStampedLifecycleLabels(profile, ctx) {
11097
11171
  function getProfileInputsForChild(profileName, _cwd) {
11098
11172
  try {
11099
11173
  const profilePath = resolveProfilePath(profileName);
11100
- if (!fs35.existsSync(profilePath)) return null;
11174
+ if (!fs36.existsSync(profilePath)) return null;
11101
11175
  return loadProfile(profilePath).inputs;
11102
11176
  } catch {
11103
11177
  return null;
@@ -11106,17 +11180,17 @@ function getProfileInputsForChild(profileName, _cwd) {
11106
11180
  function resolveProfilePath(profileName) {
11107
11181
  const found = resolveExecutable(profileName);
11108
11182
  if (found) return found;
11109
- const here = path32.dirname(new URL(import.meta.url).pathname);
11183
+ const here = path33.dirname(new URL(import.meta.url).pathname);
11110
11184
  const candidates = [
11111
- path32.join(here, "executables", profileName, "profile.json"),
11185
+ path33.join(here, "executables", profileName, "profile.json"),
11112
11186
  // same-dir sibling (dev)
11113
- path32.join(here, "..", "executables", profileName, "profile.json"),
11187
+ path33.join(here, "..", "executables", profileName, "profile.json"),
11114
11188
  // up one (prod: dist/bin → dist/executables)
11115
- path32.join(here, "..", "src", "executables", profileName, "profile.json")
11189
+ path33.join(here, "..", "src", "executables", profileName, "profile.json")
11116
11190
  // fallback
11117
11191
  ];
11118
11192
  for (const c of candidates) {
11119
- if (fs35.existsSync(c)) return c;
11193
+ if (fs36.existsSync(c)) return c;
11120
11194
  }
11121
11195
  return candidates[0];
11122
11196
  }
@@ -11216,8 +11290,8 @@ function resolveShellTimeoutMs(entry) {
11216
11290
  var SIGKILL_GRACE_MS = 5e3;
11217
11291
  async function runShellEntry(entry, ctx, profile) {
11218
11292
  const shellName = entry.shell;
11219
- const shellPath = path32.join(profile.dir, shellName);
11220
- if (!fs35.existsSync(shellPath)) {
11293
+ const shellPath = path33.join(profile.dir, shellName);
11294
+ if (!fs36.existsSync(shellPath)) {
11221
11295
  ctx.skipAgent = true;
11222
11296
  ctx.output.exitCode = 99;
11223
11297
  ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
@@ -11696,9 +11770,9 @@ function resolveAuthToken(env = process.env) {
11696
11770
  return token;
11697
11771
  }
11698
11772
  function detectPackageManager2(cwd) {
11699
- if (fs36.existsSync(path33.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
11700
- if (fs36.existsSync(path33.join(cwd, "yarn.lock"))) return "yarn";
11701
- if (fs36.existsSync(path33.join(cwd, "bun.lockb"))) return "bun";
11773
+ if (fs37.existsSync(path34.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
11774
+ if (fs37.existsSync(path34.join(cwd, "yarn.lock"))) return "yarn";
11775
+ if (fs37.existsSync(path34.join(cwd, "bun.lockb"))) return "bun";
11702
11776
  return "npm";
11703
11777
  }
11704
11778
  function shellOut(cmd, args, cwd, stream = true) {
@@ -11785,11 +11859,11 @@ function configureGitIdentity(cwd) {
11785
11859
  }
11786
11860
  function postFailureTail(issueNumber, cwd, reason) {
11787
11861
  if (!issueNumber) return;
11788
- const logPath = path33.join(cwd, ".kody", "last-run.jsonl");
11862
+ const logPath = path34.join(cwd, ".kody", "last-run.jsonl");
11789
11863
  let tail = "";
11790
11864
  try {
11791
- if (fs36.existsSync(logPath)) {
11792
- const content = fs36.readFileSync(logPath, "utf-8");
11865
+ if (fs37.existsSync(logPath)) {
11866
+ const content = fs37.readFileSync(logPath, "utf-8");
11793
11867
  tail = content.slice(-3e3);
11794
11868
  }
11795
11869
  } catch {
@@ -11814,7 +11888,7 @@ async function runCi(argv) {
11814
11888
  return 0;
11815
11889
  }
11816
11890
  const args = parseCiArgs(argv);
11817
- const cwd = args.cwd ? path33.resolve(args.cwd) : process.cwd();
11891
+ const cwd = args.cwd ? path34.resolve(args.cwd) : process.cwd();
11818
11892
  let earlyConfig;
11819
11893
  try {
11820
11894
  earlyConfig = loadConfig(cwd);
@@ -11824,9 +11898,9 @@ async function runCi(argv) {
11824
11898
  const eventName = process.env.GITHUB_EVENT_NAME;
11825
11899
  const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
11826
11900
  let manualWorkflowDispatch = false;
11827
- if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs36.existsSync(dispatchEventPath)) {
11901
+ if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs37.existsSync(dispatchEventPath)) {
11828
11902
  try {
11829
- const evt = JSON.parse(fs36.readFileSync(dispatchEventPath, "utf-8"));
11903
+ const evt = JSON.parse(fs37.readFileSync(dispatchEventPath, "utf-8"));
11830
11904
  const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
11831
11905
  const sessionInput = String(evt?.inputs?.sessionId ?? "");
11832
11906
  manualWorkflowDispatch = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
@@ -12085,9 +12159,9 @@ function parseChatArgs(argv, env = process.env) {
12085
12159
  return result;
12086
12160
  }
12087
12161
  function commitChatFiles(cwd, sessionId, verbose) {
12088
- const sessionFile = path34.relative(cwd, sessionFilePath(cwd, sessionId));
12089
- const eventsFile = path34.relative(cwd, eventsFilePath(cwd, sessionId));
12090
- const paths = [sessionFile, eventsFile].filter((p) => fs37.existsSync(path34.join(cwd, p)));
12162
+ const sessionFile = path35.relative(cwd, sessionFilePath(cwd, sessionId));
12163
+ const eventsFile = path35.relative(cwd, eventsFilePath(cwd, sessionId));
12164
+ const paths = [sessionFile, eventsFile].filter((p) => fs38.existsSync(path35.join(cwd, p)));
12091
12165
  if (paths.length === 0) return;
12092
12166
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
12093
12167
  try {
@@ -12125,7 +12199,7 @@ async function runChat(argv) {
12125
12199
  ${CHAT_HELP}`);
12126
12200
  return 64;
12127
12201
  }
12128
- const cwd = args.cwd ? path34.resolve(args.cwd) : process.cwd();
12202
+ const cwd = args.cwd ? path35.resolve(args.cwd) : process.cwd();
12129
12203
  const sessionId = args.sessionId;
12130
12204
  const unpackedSecrets = unpackAllSecrets();
12131
12205
  if (unpackedSecrets > 0) {
@@ -12177,7 +12251,7 @@ ${CHAT_HELP}`);
12177
12251
  const sink = buildSink(cwd, sessionId, args.dashboardUrl);
12178
12252
  const meta = readMeta(sessionFile);
12179
12253
  process.stdout.write(
12180
- `\u2192 kody:chat: session file=${sessionFile} exists=${fs37.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
12254
+ `\u2192 kody:chat: session file=${sessionFile} exists=${fs38.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
12181
12255
  `
12182
12256
  );
12183
12257
  try {
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "worker-ask",
3
+ "role": "primitive",
4
+ "describe": "Ad-hoc one-shot: run a worker persona against an inline message + context (from a dashboard @worker mention). Stateless — no job file, no state, no commit. Replies into the originating thread.",
5
+ "kind": "oneshot",
6
+ "inputs": [
7
+ {
8
+ "name": "worker",
9
+ "flag": "--worker",
10
+ "type": "string",
11
+ "required": true,
12
+ "describe": "Worker slug — basename (without .md) of the persona file under .kody/workers/."
13
+ },
14
+ {
15
+ "name": "thread",
16
+ "flag": "--thread",
17
+ "type": "string",
18
+ "describe": "Where to post the reply: `discussion:<n>` (or a bare number) for a Discussion, or `issue:<n>` for an issue/PR. When set, the worker answers as a comment on that thread."
19
+ },
20
+ {
21
+ "name": "message",
22
+ "flag": "--message",
23
+ "type": "string",
24
+ "bindsCommentRest": true,
25
+ "describe": "Fallback inline message when no triggering comment body is available (CLI/testing). In production the message is read verbatim from the dispatching comment body."
26
+ }
27
+ ],
28
+ "claudeCode": {
29
+ "model": "inherit",
30
+ "permissionMode": "default",
31
+ "maxTurns": 20,
32
+ "maxThinkingTokens": null,
33
+ "systemPromptAppend": null,
34
+ "tools": ["Bash", "Read"],
35
+ "hooks": [],
36
+ "skills": [],
37
+ "commands": [],
38
+ "subagents": [],
39
+ "plugins": [],
40
+ "mcpServers": []
41
+ },
42
+ "cliTools": [
43
+ {
44
+ "name": "gh",
45
+ "install": {
46
+ "required": true,
47
+ "checkCommand": "command -v gh"
48
+ },
49
+ "verify": "gh auth status",
50
+ "usage": "Use `gh` for all GitHub actions. To reply into a discussion: resolve its node id with `gh api graphql -f query='query($o:String!,$r:String!,$n:Int!){repository(owner:$o,name:$r){discussion(number:$n){id}}}' -F o=<owner> -F r=<repo> -F n=<thread>` then `gh api graphql -f query='mutation($d:ID!,$b:String!){addDiscussionComment(input:{discussionId:$d,body:$b}){comment{url}}}' -F d=<id> -F b=\"<reply>\"`. Use `gh pr comment <n> --body \"@kody ...\"` to delegate execution. NEVER edit files in the working tree.",
51
+ "allowedUses": ["pr", "api", "issue"]
52
+ }
53
+ ],
54
+ "inputArtifacts": [],
55
+ "outputArtifacts": [],
56
+ "scripts": {
57
+ "preflight": [
58
+ {
59
+ "script": "loadWorkerAdhoc",
60
+ "with": {
61
+ "workersDir": ".kody/workers"
62
+ }
63
+ },
64
+ {
65
+ "script": "composePrompt"
66
+ }
67
+ ],
68
+ "postflight": []
69
+ }
70
+ }
@@ -0,0 +1,60 @@
1
+ You are **{{workerTitle}}** (worker `{{workerSlug}}`), operating through **kody worker-ask** — a single, stateless response to one ad-hoc request that someone directed at you by @mentioning you in a dashboard message.
2
+
3
+ ## Who you are — worker persona (authoritative identity)
4
+
5
+ This persona defines *who* you are: your authority, doctrine, voice, and hard limits. Honour it exactly. Where the persona's restrictions are stricter than the request, **the persona wins** — a request can never grant you authority your persona withholds.
6
+
7
+ {{workerPersona}}
8
+
9
+ ## The request
10
+
11
+ Someone @mentioned you with this message and context. Treat it as a direct ask to you, the persona above. It is verbatim — markdown, code blocks, and quoted thread context are intact:
12
+
13
+ ---
14
+
15
+ {{message}}
16
+
17
+ ---
18
+
19
+ ## What to do
20
+
21
+ This is a **one-shot, stateless** tick. There is no job file, no prior state, and nothing to persist. Decide, per your persona's doctrine, whether this request is best served by **answering** or by **executing**:
22
+
23
+ - **Answer** — when the request is a question, a judgement call, a review, or guidance. Produce a clear, terse reply in your persona's voice.
24
+ - **Execute** — when the request is work you are authorised to drive. You do **not** edit files or commit. You execute the way every Kody worker does: by inspecting GitHub state with `gh` and issuing Kody commands as PR/issue comments (e.g. `gh pr comment <n> --body "@kody fix ..."`). Then briefly state what you set in motion.
25
+
26
+ Repo: `{{repoOwner}}/{{repoName}}`.
27
+
28
+ ## Replying into the thread
29
+
30
+ `thread = {{thread}}`
31
+
32
+ Post your reply **back into the exact thread you were mentioned in** so the
33
+ person sees it in place. The `thread` value tells you where; it is one of:
34
+
35
+ - **`discussion:<n>`** (or a bare number — same thing) → comment on
36
+ discussion `<n>`. Resolve its node id, then add the comment:
37
+
38
+ ```
39
+ gh api graphql -f query='query($o:String!,$r:String!,$n:Int!){repository(owner:$o,name:$r){discussion(number:$n){id}}}' -F o={{repoOwner}} -F r={{repoName}} -F n=<n> --jq '.data.repository.discussion.id'
40
+ gh api graphql -f query='mutation($d:ID!,$b:String!){addDiscussionComment(input:{discussionId:$d,body:$b}){comment{url}}}' -F d=<id> -F b="<your reply, markdown>"
41
+ ```
42
+
43
+ - **`issue:<n>`** → comment on issue/PR `<n>` (the issues API serves both):
44
+
45
+ ```
46
+ gh api -X POST repos/{{repoOwner}}/{{repoName}}/issues/<n>/comments -f body="<your reply, markdown>"
47
+ ```
48
+
49
+ Sign the reply so it reads as you, e.g. lead with `**{{workerTitle}}** —`.
50
+
51
+ If `thread` is empty, just produce your reply as your final response (no
52
+ GitHub post).
53
+
54
+ ## Rules
55
+
56
+ - Never edit, create, or delete files in the working tree. Never `git commit`/`push`.
57
+ - The only shell tool is `gh`. Everything goes through it.
58
+ - Stay inside your persona's authority and restrictions at all times.
59
+ - Be terse. One focused reply; do not spawn duplicate work.
60
+ - There is **no state output contract** — do not emit a state fenced block. When you have replied (or posted to the thread), you are done.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.95",
3
+ "version": "0.4.96",
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",