@kody-ade/kody-engine 0.4.201 → 0.4.203

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.
Files changed (2) hide show
  1. package/dist/bin/kody.js +1132 -1189
  2. package/package.json +1 -1
package/dist/bin/kody.js CHANGED
@@ -19,8 +19,8 @@ __export(events_exports, {
19
19
  resolveRunId: () => resolveRunId
20
20
  });
21
21
  import * as crypto from "crypto";
22
- import * as fs5 from "fs";
23
- import * as path5 from "path";
22
+ import * as fs4 from "fs";
23
+ import * as path4 from "path";
24
24
  function resolveRunId() {
25
25
  if (cachedRunId) return cachedRunId;
26
26
  if (process.env.KODY_RUN_ID) {
@@ -49,17 +49,17 @@ function emitEvent(cwd, ev) {
49
49
  runId,
50
50
  ...ev
51
51
  };
52
- const eventsPath2 = path5.join(cwd, ".kody", "runs", runId, "events.jsonl");
53
- fs5.mkdirSync(path5.dirname(eventsPath2), { recursive: true });
54
- fs5.appendFileSync(eventsPath2, `${JSON.stringify(fullEvent)}
52
+ const eventsPath2 = path4.join(cwd, ".kody", "runs", runId, "events.jsonl");
53
+ fs4.mkdirSync(path4.dirname(eventsPath2), { recursive: true });
54
+ fs4.appendFileSync(eventsPath2, `${JSON.stringify(fullEvent)}
55
55
  `);
56
56
  } catch {
57
57
  }
58
58
  }
59
59
  function readEvents(cwd, runId) {
60
- const eventsPath2 = path5.join(cwd, ".kody", "runs", runId, "events.jsonl");
61
- if (!fs5.existsSync(eventsPath2)) return [];
62
- const lines = fs5.readFileSync(eventsPath2, "utf-8").split("\n");
60
+ const eventsPath2 = path4.join(cwd, ".kody", "runs", runId, "events.jsonl");
61
+ if (!fs4.existsSync(eventsPath2)) return [];
62
+ const lines = fs4.readFileSync(eventsPath2, "utf-8").split("\n");
63
63
  const out = [];
64
64
  for (const line of lines) {
65
65
  const trimmed = line.trim();
@@ -72,11 +72,11 @@ function readEvents(cwd, runId) {
72
72
  return out;
73
73
  }
74
74
  function listRuns(cwd) {
75
- const runsDir = path5.join(cwd, ".kody", "runs");
76
- if (!fs5.existsSync(runsDir)) return [];
77
- return fs5.readdirSync(runsDir).filter((name) => {
75
+ const runsDir = path4.join(cwd, ".kody", "runs");
76
+ if (!fs4.existsSync(runsDir)) return [];
77
+ return fs4.readdirSync(runsDir).filter((name) => {
78
78
  try {
79
- return fs5.statSync(path5.join(runsDir, name)).isDirectory();
79
+ return fs4.statSync(path4.join(runsDir, name)).isDirectory();
80
80
  } catch {
81
81
  return false;
82
82
  }
@@ -181,8 +181,10 @@ function summarizeFailure(result) {
181
181
  for (let attempt = 1; ; attempt++) {
182
182
  const retry = result.details[`${name} (retry ${attempt})`];
183
183
  if (!retry) break;
184
- lines.push(`
185
- --- ${name} (retry ${attempt}: exit ${retry.exitCode}, ${(retry.durationMs / 1e3).toFixed(1)}s) ---`);
184
+ lines.push(
185
+ `
186
+ --- ${name} (retry ${attempt}: exit ${retry.exitCode}, ${(retry.durationMs / 1e3).toFixed(1)}s) ---`
187
+ );
186
188
  lines.push(stripAnsi(retry.tail));
187
189
  }
188
190
  }
@@ -549,7 +551,9 @@ __export(dutyMcp_exports, {
549
551
  dispatchWorkflow: () => dispatchWorkflow,
550
552
  ensureComment: () => ensureComment,
551
553
  ensureIssue: () => ensureIssue,
552
- readCheckRuns: () => readCheckRuns
554
+ parseDutyTrustMode: () => parseDutyTrustMode,
555
+ readCheckRuns: () => readCheckRuns,
556
+ readDutyTrustMode: () => readDutyTrustMode
553
557
  });
554
558
  import { createSdkMcpServer as createSdkMcpServer3, tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
555
559
  import { z as z3 } from "zod";
@@ -621,18 +625,7 @@ function readLedger(label) {
621
625
  const startTag = `<!-- ${label}:start -->`;
622
626
  const endTag = `<!-- ${label}:end -->`;
623
627
  try {
624
- const raw = gh([
625
- "issue",
626
- "list",
627
- "--state",
628
- "open",
629
- "--label",
630
- label,
631
- "--limit",
632
- "5",
633
- "--json",
634
- "number,body"
635
- ]);
628
+ const raw = gh(["issue", "list", "--state", "open", "--label", label, "--limit", "5", "--json", "number,body"]);
636
629
  const issues = JSON.parse(raw);
637
630
  if (issues.length === 0) return { found: false, payload: null };
638
631
  const issue = issues.sort((a, b) => a.number - b.number)[0];
@@ -654,6 +647,24 @@ function readLedger(label) {
654
647
  return { found: false, payload: { error: err instanceof Error ? err.message : String(err) } };
655
648
  }
656
649
  }
650
+ function parseDutyTrustMode(rawJson, dutySlug) {
651
+ try {
652
+ const parsed = JSON.parse(rawJson);
653
+ return parsed?.duties?.[dutySlug]?.mode === "auto" ? "auto" : "ask";
654
+ } catch {
655
+ return "ask";
656
+ }
657
+ }
658
+ function readDutyTrustMode(repoSlug, dutySlug) {
659
+ if (!dutySlug) return "ask";
660
+ try {
661
+ const b64 = gh(["api", `repos/${repoSlug}/contents/${TRUST_FILE_PATH}?ref=${TRUST_STATE_BRANCH}`, "--jq", ".content"]);
662
+ const json = Buffer.from(b64.trim(), "base64").toString("utf-8");
663
+ return parseDutyTrustMode(json, dutySlug);
664
+ } catch {
665
+ return "ask";
666
+ }
667
+ }
657
668
  function readCheckRuns(repoSlug, ref, ignoreNames) {
658
669
  const sha = gh(["api", `repos/${repoSlug}/commits/${ref}`, "--jq", ".sha"]).trim();
659
670
  const raw = gh([
@@ -712,6 +723,9 @@ function dispatchWorkflow(workflowFile, executable, issueNumber) {
712
723
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
713
724
  }
714
725
  }
726
+ function trustRefusal(dutySlug) {
727
+ 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
+ }
715
729
  function buildDutyMcpServer(opts) {
716
730
  const workflowFile = opts.workflowFile ?? "kody.yml";
717
731
  const listTool = tool3(
@@ -737,6 +751,9 @@ function buildDutyMcpServer(opts) {
737
751
  pr: z3.number().int().positive().describe("PR number to repair.")
738
752
  },
739
753
  async (args) => {
754
+ if (readDutyTrustMode(opts.repoSlug, opts.dutySlug) !== "auto") {
755
+ return { content: [{ type: "text", text: trustRefusal(opts.dutySlug) }] };
756
+ }
740
757
  const result = dispatchVerb(workflowFile, verb, args.pr);
741
758
  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}`;
742
759
  return {
@@ -805,9 +822,13 @@ function buildDutyMcpServer(opts) {
805
822
  "ensure_issue",
806
823
  "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.",
807
824
  {
808
- key: z3.string().min(1).describe("Stable dedup identity for this finding (e.g. 'dev-ci-red', 'docs-drift:<feature>'). Same key across ticks = same issue."),
825
+ key: z3.string().min(1).describe(
826
+ "Stable dedup identity for this finding (e.g. 'dev-ci-red', 'docs-drift:<feature>'). Same key across ticks = same issue."
827
+ ),
809
828
  title: z3.string().min(1).describe("Issue title (used only on first creation)."),
810
- body: z3.string().min(1).describe("Issue body markdown (used only on first creation). Include the operator mention verbatim if the duty body has one.")
829
+ body: z3.string().min(1).describe(
830
+ "Issue body markdown (used only on first creation). Include the operator mention verbatim if the duty body has one."
831
+ )
811
832
  },
812
833
  async (args) => {
813
834
  const result = ensureIssue(opts.repoSlug, args.key, args.title, args.body);
@@ -835,6 +856,9 @@ function buildDutyMcpServer(opts) {
835
856
  issueNumber: z3.number().int().positive().describe("Issue (or PR) number forwarded as issue_number.")
836
857
  },
837
858
  async (args) => {
859
+ if (readDutyTrustMode(opts.repoSlug, opts.dutySlug) !== "auto") {
860
+ return { content: [{ type: "text", text: trustRefusal(opts.dutySlug) }] };
861
+ }
838
862
  const result = dispatchWorkflow(workflowFile, args.executable, args.issueNumber);
839
863
  const text = result.ok ? `Dispatched \`${args.executable}\` on #${args.issueNumber} via workflow_dispatch.` : `Dispatch failed for \`${args.executable}\` on #${args.issueNumber}: ${result.error}`;
840
864
  return { content: [{ type: "text", text }] };
@@ -858,13 +882,15 @@ function buildDutyMcpServer(opts) {
858
882
  });
859
883
  return { server };
860
884
  }
861
- var FAIL_CONCLUSIONS, RUNNING_STATUSES, CHECK_FAIL_CONCLUSIONS, DEFAULT_IGNORE_CHECKS, trackMarker, commentMarker, DUTY_MCP_TOOL_NAMES;
885
+ var FAIL_CONCLUSIONS, RUNNING_STATUSES, TRUST_FILE_PATH, TRUST_STATE_BRANCH, CHECK_FAIL_CONCLUSIONS, DEFAULT_IGNORE_CHECKS, trackMarker, commentMarker, DUTY_MCP_TOOL_NAMES;
862
886
  var init_dutyMcp = __esm({
863
887
  "src/dutyMcp.ts"() {
864
888
  "use strict";
865
889
  init_issue();
866
890
  FAIL_CONCLUSIONS = /* @__PURE__ */ new Set(["FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE", "CANCELLED"]);
867
891
  RUNNING_STATUSES = /* @__PURE__ */ new Set(["IN_PROGRESS", "QUEUED", "PENDING", "WAITING", "REQUESTED"]);
892
+ TRUST_FILE_PATH = ".kody/state/trust.json";
893
+ TRUST_STATE_BRANCH = "kody-state";
868
894
  CHECK_FAIL_CONCLUSIONS = /* @__PURE__ */ new Set(["FAILURE", "TIMED_OUT", "STARTUP_FAILURE", "ACTION_REQUIRED"]);
869
895
  DEFAULT_IGNORE_CHECKS = ["run", "kody", "job-tick", "goal-tick", "worker-ask", "chat"];
870
896
  trackMarker = (key) => `<!-- kody-track:${key} -->`;
@@ -886,15 +912,15 @@ var init_dutyMcp = __esm({
886
912
 
887
913
  // src/repoWorkspace.ts
888
914
  import { spawn as spawn2, spawnSync } from "child_process";
889
- import * as fs6 from "fs";
890
- import * as path6 from "path";
915
+ import * as fs5 from "fs";
916
+ import * as path5 from "path";
891
917
  async function resolveAndClone(reposRoot, repo, repoToken, cloneRepo) {
892
918
  const name = repo?.trim();
893
919
  if (!name || !REPO_RE.test(name)) return null;
894
- const root = path6.resolve(reposRoot);
895
- const dir = path6.resolve(root, name);
896
- if (dir !== root && !dir.startsWith(root + path6.sep)) return null;
897
- if (fs6.existsSync(path6.join(dir, ".git"))) return dir;
920
+ const root = path5.resolve(reposRoot);
921
+ const dir = path5.resolve(root, name);
922
+ if (dir !== root && !dir.startsWith(root + path5.sep)) return null;
923
+ if (fs5.existsSync(path5.join(dir, ".git"))) return dir;
898
924
  const inflight = repoClones.get(dir);
899
925
  if (inflight) {
900
926
  await inflight;
@@ -908,25 +934,13 @@ async function resolveAndClone(reposRoot, repo, repoToken, cloneRepo) {
908
934
  return dir;
909
935
  }
910
936
  async function ensureRepoCwd(opts) {
911
- const dir = await resolveAndClone(
912
- opts.reposRoot,
913
- opts.repo,
914
- opts.repoToken,
915
- opts.cloneRepo
916
- );
937
+ const dir = await resolveAndClone(opts.reposRoot, opts.repo, opts.repoToken, opts.cloneRepo);
917
938
  return dir ?? opts.baseCwd;
918
939
  }
919
940
  async function fetchRepo(opts) {
920
- const dir = await resolveAndClone(
921
- opts.reposRoot,
922
- opts.repo,
923
- opts.repoToken,
924
- opts.cloneRepo ?? defaultCloneRepo
925
- );
941
+ const dir = await resolveAndClone(opts.reposRoot, opts.repo, opts.repoToken, opts.cloneRepo ?? defaultCloneRepo);
926
942
  if (!dir) {
927
- throw new Error(
928
- `invalid repo "${opts.repo}" \u2014 expected "owner/name" with no path escapes`
929
- );
943
+ throw new Error(`invalid repo "${opts.repo}" \u2014 expected "owner/name" with no path escapes`);
930
944
  }
931
945
  return dir;
932
946
  }
@@ -937,7 +951,7 @@ var init_repoWorkspace = __esm({
937
951
  REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
938
952
  repoClones = /* @__PURE__ */ new Map();
939
953
  defaultCloneRepo = (repo, token, dir) => {
940
- fs6.mkdirSync(path6.dirname(dir), { recursive: true });
954
+ fs5.mkdirSync(path5.dirname(dir), { recursive: true });
941
955
  const authUrl = token ? `https://x-access-token:${token}@github.com/${repo}.git` : `https://github.com/${repo}.git`;
942
956
  return new Promise((resolve6, reject) => {
943
957
  const child = spawn2("git", ["clone", "--depth=1", authUrl, dir], {
@@ -968,10 +982,7 @@ var fetchRepoMcp_exports = {};
968
982
  __export(fetchRepoMcp_exports, {
969
983
  buildFetchRepoMcpServer: () => buildFetchRepoMcpServer
970
984
  });
971
- import {
972
- createSdkMcpServer as createSdkMcpServer4,
973
- tool as tool4
974
- } from "@anthropic-ai/claude-agent-sdk";
985
+ import { createSdkMcpServer as createSdkMcpServer4, tool as tool4 } from "@anthropic-ai/claude-agent-sdk";
975
986
  import { z as z4 } from "zod";
976
987
  function buildFetchRepoMcpServer(opts) {
977
988
  const fetchTool = tool4(
@@ -1431,7 +1442,7 @@ var init_loadCoverageRules = __esm({
1431
1442
  // package.json
1432
1443
  var package_default = {
1433
1444
  name: "@kody-ade/kody-engine",
1434
- version: "0.4.201",
1445
+ version: "0.4.203",
1435
1446
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
1436
1447
  license: "MIT",
1437
1448
  type: "module",
@@ -1562,104 +1573,16 @@ function makeRunId(sessionId, suffix) {
1562
1573
  import * as fs11 from "fs";
1563
1574
  import * as path11 from "path";
1564
1575
 
1565
- // src/task-artifacts.ts
1566
- import fs2 from "fs";
1567
- import path2 from "path";
1568
- var TASK_ARTIFACT_FILES = [
1569
- "context.json",
1570
- "memory-recs.json",
1571
- "followups.json",
1572
- "handoff-notes.md"
1573
- ];
1574
- function prepareTaskArtifactsDir(cwd, taskId) {
1575
- const safeId = String(taskId).replace(/[^a-zA-Z0-9._-]/g, "_");
1576
- const relDir = path2.join(".kody", "tasks", safeId);
1577
- const absDir = path2.join(cwd, relDir);
1578
- fs2.mkdirSync(absDir, { recursive: true });
1579
- return { taskId: safeId, absDir, relDir };
1580
- }
1581
- function verifyTaskArtifacts(absDir) {
1582
- const missing = [];
1583
- for (const name of TASK_ARTIFACT_FILES) {
1584
- const full = path2.join(absDir, name);
1585
- try {
1586
- const stat = fs2.statSync(full);
1587
- if (!stat.isFile() || stat.size === 0) missing.push(name);
1588
- } catch {
1589
- missing.push(name);
1590
- }
1591
- }
1592
- return missing;
1593
- }
1594
- function taskArtifactsPromptAddendum(opts) {
1595
- return [
1596
- "## Per-task artifacts (REQUIRED before your final response)",
1597
- "",
1598
- `Before you finish, write these four files into \`${opts.relDir}/\`:`,
1599
- "",
1600
- `1. **context.json** \u2014 task header. Shape:`,
1601
- " ```json",
1602
- " {",
1603
- ` "taskId": "${opts.taskId}",`,
1604
- ` "taskType": "${opts.taskType}",`,
1605
- ` "target": "<issue/PR number, session id, or job slug>",`,
1606
- ` "outcome": "success" | "failure" | "partial",`,
1607
- ` "exitCode": <number>,`,
1608
- ` "reason": "<one-line summary of why you exited>",`,
1609
- ` "prUrl": "<url or null>",`,
1610
- ` "runUrl": "<url or null>",`,
1611
- ` "filesTouched": ["path/from/repo/root.ts", ...],`,
1612
- ` "sessionLog": ".kody/sessions/<id>.jsonl",`,
1613
- ` "startedAt": "<ISO>",`,
1614
- ` "finishedAt": "<ISO>"`,
1615
- " }",
1616
- " ```",
1617
- "",
1618
- `2. **memory-recs.json** \u2014 array of sticky-note candidates worth promoting`,
1619
- ` to long-term \`.kody/memory/\`. Each item:`,
1620
- " ```json",
1621
- " {",
1622
- ` "type": "preference" | "decision" | "lesson",`,
1623
- ` "name": "kebab-case-slug",`,
1624
- ` "hook": "one-line summary for INDEX.md",`,
1625
- ` "body": "markdown body \u2014 explain the rule plus a Why: line",`,
1626
- ` "why": "the load-bearing reason a future session needs this",`,
1627
- ` "confidence": 0.0 to 1.0`,
1628
- " }",
1629
- " ```",
1630
- ` Use \`[]\` if nothing in this task is worth remembering. Forced`,
1631
- ` filler is worse than nothing \u2014 only record what would be lost`,
1632
- ` otherwise.`,
1633
- "",
1634
- `3. **followups.json** \u2014 array of TODOs uncovered but not fixed.`,
1635
- " ```json",
1636
- " {",
1637
- ` "title": "short summary",`,
1638
- ` "body": "what the operator should do, and where",`,
1639
- ` "rationale": "why this matters",`,
1640
- ` "priority": "low" | "medium" | "high"`,
1641
- " }",
1642
- " ```",
1643
- ` Use \`[]\` if nothing surfaced.`,
1644
- "",
1645
- `4. **handoff-notes.md** \u2014 short prose (\u2264200 words), no frontmatter:`,
1646
- ` what you did and why, so the next person/agent can pick up cold.`,
1647
- "",
1648
- "Skipping any of the four files is an error. Empty arrays are fine;",
1649
- "skipping the file is not."
1650
- ].join("\n");
1651
- }
1652
-
1653
1576
  // src/agent.ts
1654
- import * as fs7 from "fs";
1655
- import * as path7 from "path";
1577
+ import * as fs6 from "fs";
1578
+ import * as path6 from "path";
1656
1579
  import { query } from "@anthropic-ai/claude-agent-sdk";
1657
1580
 
1658
1581
  // src/claudeBinary.ts
1659
- import * as fs3 from "fs";
1582
+ import * as fs2 from "fs";
1660
1583
  import { createRequire } from "module";
1661
1584
  import * as os from "os";
1662
- import * as path3 from "path";
1585
+ import * as path2 from "path";
1663
1586
  var SDK_PKG = "@anthropic-ai/claude-agent-sdk";
1664
1587
  function candidateSpecs(platform, arch) {
1665
1588
  const ext = platform === "win32" ? ".exe" : "";
@@ -1669,8 +1592,8 @@ function candidateSpecs(platform, arch) {
1669
1592
  function readSdkVersion(req) {
1670
1593
  try {
1671
1594
  const entry = req.resolve(SDK_PKG);
1672
- const pkgDir = path3.dirname(entry);
1673
- const raw = fs3.readFileSync(path3.join(pkgDir, "package.json"), "utf8");
1595
+ const pkgDir = path2.dirname(entry);
1596
+ const raw = fs2.readFileSync(path2.join(pkgDir, "package.json"), "utf8");
1674
1597
  const v = JSON.parse(raw)?.version;
1675
1598
  return typeof v === "string" && v.length > 0 ? v : "unknown";
1676
1599
  } catch {
@@ -1692,24 +1615,24 @@ function ensureStableClaudeBinary() {
1692
1615
  } catch {
1693
1616
  }
1694
1617
  }
1695
- if (!source || !fs3.existsSync(source)) {
1618
+ if (!source || !fs2.existsSync(source)) {
1696
1619
  cached = null;
1697
1620
  return cached;
1698
1621
  }
1699
1622
  const ext = process.platform === "win32" ? ".exe" : "";
1700
1623
  const version = readSdkVersion(req);
1701
- const destDir = path3.join(os.tmpdir(), "kody-claude-sdk", version);
1702
- const dest = path3.join(destDir, `claude${ext}`);
1703
- const srcSize = fs3.statSync(source).size;
1704
- if (fs3.existsSync(dest) && fs3.statSync(dest).size === srcSize) {
1624
+ const destDir = path2.join(os.tmpdir(), "kody-claude-sdk", version);
1625
+ const dest = path2.join(destDir, `claude${ext}`);
1626
+ const srcSize = fs2.statSync(source).size;
1627
+ if (fs2.existsSync(dest) && fs2.statSync(dest).size === srcSize) {
1705
1628
  cached = dest;
1706
1629
  return cached;
1707
1630
  }
1708
- fs3.mkdirSync(destDir, { recursive: true });
1709
- const tmp = path3.join(destDir, `.claude.${process.pid}.${Date.now()}.tmp`);
1710
- fs3.copyFileSync(source, tmp);
1711
- fs3.chmodSync(tmp, 493);
1712
- fs3.renameSync(tmp, dest);
1631
+ fs2.mkdirSync(destDir, { recursive: true });
1632
+ const tmp = path2.join(destDir, `.claude.${process.pid}.${Date.now()}.tmp`);
1633
+ fs2.copyFileSync(source, tmp);
1634
+ fs2.chmodSync(tmp, 493);
1635
+ fs2.renameSync(tmp, dest);
1713
1636
  cached = dest;
1714
1637
  return cached;
1715
1638
  } catch {
@@ -1719,8 +1642,8 @@ function ensureStableClaudeBinary() {
1719
1642
  }
1720
1643
 
1721
1644
  // src/config.ts
1722
- import * as fs4 from "fs";
1723
- import * as path4 from "path";
1645
+ import * as fs3 from "fs";
1646
+ import * as path3 from "path";
1724
1647
  var LITELLM_DEFAULT_PORT = 4e3;
1725
1648
  var LITELLM_DEFAULT_URL = `http://localhost:${LITELLM_DEFAULT_PORT}`;
1726
1649
  function parseProviderModel(s) {
@@ -1738,13 +1661,13 @@ function needsLitellmProxy(model) {
1738
1661
  return model.provider !== "claude" && model.provider !== "anthropic";
1739
1662
  }
1740
1663
  function loadConfig(projectDir = process.cwd()) {
1741
- const configPath = path4.join(projectDir, "kody.config.json");
1742
- if (!fs4.existsSync(configPath)) {
1664
+ const configPath = path3.join(projectDir, "kody.config.json");
1665
+ if (!fs3.existsSync(configPath)) {
1743
1666
  throw new Error(`kody.config.json not found at ${configPath}`);
1744
1667
  }
1745
1668
  let raw;
1746
1669
  try {
1747
- raw = JSON.parse(fs4.readFileSync(configPath, "utf-8"));
1670
+ raw = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
1748
1671
  } catch (err) {
1749
1672
  const msg = err instanceof Error ? err.message : String(err);
1750
1673
  throw new Error(`kody.config.json is invalid JSON: ${msg}`);
@@ -1781,6 +1704,7 @@ function loadConfig(projectDir = process.cwd()) {
1781
1704
  testRequirements: parseTestRequirements(raw.testRequirements),
1782
1705
  defaultExecutable: typeof raw.defaultExecutable === "string" && raw.defaultExecutable.length > 0 ? raw.defaultExecutable : "run",
1783
1706
  defaultPrExecutable: typeof raw.defaultPrExecutable === "string" && raw.defaultPrExecutable.length > 0 ? raw.defaultPrExecutable : "fix",
1707
+ onPullRequest: typeof raw.onPullRequest === "string" && raw.onPullRequest.length > 0 ? raw.onPullRequest : void 0,
1784
1708
  aliases: mergeAliases(raw.aliases),
1785
1709
  classify: parseClassifyConfig(raw.classify),
1786
1710
  release: parseReleaseConfig(raw.release),
@@ -2062,9 +1986,9 @@ function toolMayMutate(name, input) {
2062
1986
  return false;
2063
1987
  }
2064
1988
  async function runAgent(opts) {
2065
- const ndjsonDir = opts.ndjsonDir ?? path7.join(opts.cwd, ".kody");
2066
- fs7.mkdirSync(ndjsonDir, { recursive: true });
2067
- const ndjsonPath = path7.join(ndjsonDir, "last-run.jsonl");
1989
+ const ndjsonDir = opts.ndjsonDir ?? path6.join(opts.cwd, ".kody");
1990
+ fs6.mkdirSync(ndjsonDir, { recursive: true });
1991
+ const ndjsonPath = path6.join(ndjsonDir, "last-run.jsonl");
2068
1992
  const env = {
2069
1993
  ...process.env,
2070
1994
  SKIP_HOOKS: "1",
@@ -2093,7 +2017,7 @@ async function runAgent(opts) {
2093
2017
  let finalText = "";
2094
2018
  let getSubmitted;
2095
2019
  for (let attempt = 0; ; attempt++) {
2096
- const fullLog = fs7.createWriteStream(ndjsonPath, { flags: "w" });
2020
+ const fullLog = fs6.createWriteStream(ndjsonPath, { flags: "w" });
2097
2021
  const resultTexts = [];
2098
2022
  outcome = "failed";
2099
2023
  outcomeKind = "generic_failed";
@@ -2390,48 +2314,48 @@ async function runAgent(opts) {
2390
2314
  }
2391
2315
 
2392
2316
  // src/registry.ts
2393
- import * as fs8 from "fs";
2394
- import * as path8 from "path";
2317
+ import * as fs7 from "fs";
2318
+ import * as path7 from "path";
2395
2319
  function getExecutablesRoot() {
2396
- const here = path8.dirname(new URL(import.meta.url).pathname);
2320
+ const here = path7.dirname(new URL(import.meta.url).pathname);
2397
2321
  const candidates = [
2398
- path8.join(here, "executables"),
2322
+ path7.join(here, "executables"),
2399
2323
  // dev: src/
2400
- path8.join(here, "..", "executables"),
2324
+ path7.join(here, "..", "executables"),
2401
2325
  // built: dist/bin → dist/executables
2402
- path8.join(here, "..", "src", "executables")
2326
+ path7.join(here, "..", "src", "executables")
2403
2327
  // fallback
2404
2328
  ];
2405
2329
  for (const c of candidates) {
2406
- if (fs8.existsSync(c) && fs8.statSync(c).isDirectory()) return c;
2330
+ if (fs7.existsSync(c) && fs7.statSync(c).isDirectory()) return c;
2407
2331
  }
2408
2332
  return candidates[0];
2409
2333
  }
2410
2334
  function getProjectExecutablesRoot() {
2411
- return path8.join(process.cwd(), ".kody", "executables");
2335
+ return path7.join(process.cwd(), ".kody", "executables");
2412
2336
  }
2413
2337
  function getBuiltinJobsRoot() {
2414
- const here = path8.dirname(new URL(import.meta.url).pathname);
2338
+ const here = path7.dirname(new URL(import.meta.url).pathname);
2415
2339
  const candidates = [
2416
- path8.join(here, "jobs"),
2340
+ path7.join(here, "jobs"),
2417
2341
  // dev: src/
2418
- path8.join(here, "..", "jobs"),
2342
+ path7.join(here, "..", "jobs"),
2419
2343
  // built: dist/bin → dist/jobs
2420
- path8.join(here, "..", "src", "jobs")
2344
+ path7.join(here, "..", "src", "jobs")
2421
2345
  // fallback
2422
2346
  ];
2423
2347
  for (const c of candidates) {
2424
- if (fs8.existsSync(c) && fs8.statSync(c).isDirectory()) return c;
2348
+ if (fs7.existsSync(c) && fs7.statSync(c).isDirectory()) return c;
2425
2349
  }
2426
2350
  return candidates[0];
2427
2351
  }
2428
2352
  function listBuiltinJobs(root = getBuiltinJobsRoot()) {
2429
- if (!fs8.existsSync(root) || !fs8.statSync(root).isDirectory()) return [];
2353
+ if (!fs7.existsSync(root) || !fs7.statSync(root).isDirectory()) return [];
2430
2354
  const out = [];
2431
- for (const ent of fs8.readdirSync(root, { withFileTypes: true })) {
2355
+ for (const ent of fs7.readdirSync(root, { withFileTypes: true })) {
2432
2356
  if (!ent.isFile() || !ent.name.endsWith(".md")) continue;
2433
2357
  const slug = ent.name.slice(0, -3);
2434
- out.push({ slug, filePath: path8.join(root, ent.name) });
2358
+ out.push({ slug, filePath: path7.join(root, ent.name) });
2435
2359
  }
2436
2360
  out.sort((a, b) => a.slug.localeCompare(b.slug));
2437
2361
  return out;
@@ -2444,13 +2368,13 @@ function listExecutables(roots = getExecutableRoots()) {
2444
2368
  const seen = /* @__PURE__ */ new Set();
2445
2369
  const out = [];
2446
2370
  for (const root of rootList) {
2447
- if (!fs8.existsSync(root)) continue;
2448
- const entries = fs8.readdirSync(root, { withFileTypes: true });
2371
+ if (!fs7.existsSync(root)) continue;
2372
+ const entries = fs7.readdirSync(root, { withFileTypes: true });
2449
2373
  for (const ent of entries) {
2450
2374
  if (!ent.isDirectory()) continue;
2451
2375
  if (seen.has(ent.name)) continue;
2452
- const profilePath = path8.join(root, ent.name, "profile.json");
2453
- if (fs8.existsSync(profilePath) && fs8.statSync(profilePath).isFile()) {
2376
+ const profilePath = path7.join(root, ent.name, "profile.json");
2377
+ if (fs7.existsSync(profilePath) && fs7.statSync(profilePath).isFile()) {
2454
2378
  out.push({ name: ent.name, profilePath });
2455
2379
  seen.add(ent.name);
2456
2380
  }
@@ -2462,8 +2386,8 @@ function resolveExecutable(name, roots = getExecutableRoots()) {
2462
2386
  if (!isSafeName(name)) return null;
2463
2387
  const rootList = typeof roots === "string" ? [roots] : roots;
2464
2388
  for (const root of rootList) {
2465
- const profilePath = path8.join(root, name, "profile.json");
2466
- if (fs8.existsSync(profilePath) && fs8.statSync(profilePath).isFile()) {
2389
+ const profilePath = path7.join(root, name, "profile.json");
2390
+ if (fs7.existsSync(profilePath) && fs7.statSync(profilePath).isFile()) {
2467
2391
  return profilePath;
2468
2392
  }
2469
2393
  }
@@ -2479,7 +2403,7 @@ function getProfileInputs(name, roots = getExecutableRoots()) {
2479
2403
  const profilePath = resolveExecutable(name, roots);
2480
2404
  if (!profilePath) return null;
2481
2405
  try {
2482
- const raw = JSON.parse(fs8.readFileSync(profilePath, "utf-8"));
2406
+ const raw = JSON.parse(fs7.readFileSync(profilePath, "utf-8"));
2483
2407
  if (!raw || typeof raw !== "object" || !Array.isArray(raw.inputs)) return [];
2484
2408
  return raw.inputs;
2485
2409
  } catch {
@@ -2497,7 +2421,11 @@ function parseGenericFlags(argv) {
2497
2421
  }
2498
2422
  const key = arg.slice(2);
2499
2423
  const next = argv[i + 1];
2500
- const value = next !== void 0 && !next.startsWith("--") ? (i++, next) : true;
2424
+ let value = true;
2425
+ if (next !== void 0 && !next.startsWith("--")) {
2426
+ value = next;
2427
+ i++;
2428
+ }
2501
2429
  args[key] = value;
2502
2430
  if (key.includes("-")) {
2503
2431
  const camel = key.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
@@ -2508,70 +2436,92 @@ function parseGenericFlags(argv) {
2508
2436
  return args;
2509
2437
  }
2510
2438
 
2511
- // src/chat/session.ts
2512
- import * as fs9 from "fs";
2513
- import * as path9 from "path";
2514
- function sessionFilePath(cwd, sessionId) {
2515
- return path9.join(cwd, ".kody", "sessions", `${sessionId}.jsonl`);
2516
- }
2517
- function readMeta(file) {
2518
- if (!fs9.existsSync(file)) return null;
2519
- const raw = fs9.readFileSync(file, "utf-8");
2520
- const firstLine2 = raw.split("\n", 1)[0]?.trim();
2521
- if (!firstLine2) return null;
2522
- try {
2523
- const parsed = JSON.parse(firstLine2);
2524
- if (parsed.type !== "meta") return null;
2525
- if (parsed.mode !== "one-shot" && parsed.mode !== "interactive") return null;
2526
- return parsed;
2527
- } catch {
2528
- return null;
2529
- }
2439
+ // src/task-artifacts.ts
2440
+ import fs8 from "fs";
2441
+ import path8 from "path";
2442
+ var TASK_ARTIFACT_FILES = ["context.json", "memory-recs.json", "followups.json", "handoff-notes.md"];
2443
+ function prepareTaskArtifactsDir(cwd, taskId) {
2444
+ const safeId = String(taskId).replace(/[^a-zA-Z0-9._-]/g, "_");
2445
+ const relDir = path8.join(".kody", "tasks", safeId);
2446
+ const absDir = path8.join(cwd, relDir);
2447
+ fs8.mkdirSync(absDir, { recursive: true });
2448
+ return { taskId: safeId, absDir, relDir };
2530
2449
  }
2531
- function readSession(file) {
2532
- if (!fs9.existsSync(file)) return [];
2533
- const raw = fs9.readFileSync(file, "utf-8").trim();
2534
- if (!raw) return [];
2535
- const turns = [];
2536
- for (const line of raw.split("\n")) {
2537
- if (!line.trim()) continue;
2450
+ function verifyTaskArtifacts(absDir) {
2451
+ const missing = [];
2452
+ for (const name of TASK_ARTIFACT_FILES) {
2453
+ const full = path8.join(absDir, name);
2538
2454
  try {
2539
- const parsed = JSON.parse(line);
2540
- if (parsed.role !== "user" && parsed.role !== "assistant") continue;
2541
- if (typeof parsed.content !== "string") continue;
2542
- turns.push(parsed);
2455
+ const stat = fs8.statSync(full);
2456
+ if (!stat.isFile() || stat.size === 0) missing.push(name);
2543
2457
  } catch {
2458
+ missing.push(name);
2544
2459
  }
2545
2460
  }
2546
- return turns;
2547
- }
2548
- function appendTurn(file, turn) {
2549
- fs9.mkdirSync(path9.dirname(file), { recursive: true });
2550
- const line = JSON.stringify({
2551
- role: turn.role,
2552
- content: turn.content,
2553
- timestamp: turn.timestamp,
2554
- toolCalls: turn.toolCalls ?? []
2555
- });
2556
- fs9.appendFileSync(file, `${line}
2557
- `);
2461
+ return missing;
2558
2462
  }
2559
- function seedInitialMessage(file, message) {
2560
- if (!message.trim()) return false;
2561
- const turns = readSession(file);
2562
- const lastUser = [...turns].reverse().find((t) => t.role === "user");
2563
- if (lastUser && lastUser.content === message) return false;
2564
- appendTurn(file, {
2565
- role: "user",
2566
- content: message,
2567
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2568
- });
2569
- return true;
2463
+ function taskArtifactsPromptAddendum(opts) {
2464
+ return [
2465
+ "## Per-task artifacts (REQUIRED before your final response)",
2466
+ "",
2467
+ `Before you finish, write these four files into \`${opts.relDir}/\`:`,
2468
+ "",
2469
+ `1. **context.json** \u2014 task header. Shape:`,
2470
+ " ```json",
2471
+ " {",
2472
+ ` "taskId": "${opts.taskId}",`,
2473
+ ` "taskType": "${opts.taskType}",`,
2474
+ ` "target": "<issue/PR number, session id, or job slug>",`,
2475
+ ` "outcome": "success" | "failure" | "partial",`,
2476
+ ` "exitCode": <number>,`,
2477
+ ` "reason": "<one-line summary of why you exited>",`,
2478
+ ` "prUrl": "<url or null>",`,
2479
+ ` "runUrl": "<url or null>",`,
2480
+ ` "filesTouched": ["path/from/repo/root.ts", ...],`,
2481
+ ` "sessionLog": ".kody/sessions/<id>.jsonl",`,
2482
+ ` "startedAt": "<ISO>",`,
2483
+ ` "finishedAt": "<ISO>"`,
2484
+ " }",
2485
+ " ```",
2486
+ "",
2487
+ `2. **memory-recs.json** \u2014 array of sticky-note candidates worth promoting`,
2488
+ ` to long-term \`.kody/memory/\`. Each item:`,
2489
+ " ```json",
2490
+ " {",
2491
+ ` "type": "preference" | "decision" | "lesson",`,
2492
+ ` "name": "kebab-case-slug",`,
2493
+ ` "hook": "one-line summary for INDEX.md",`,
2494
+ ` "body": "markdown body \u2014 explain the rule plus a Why: line",`,
2495
+ ` "why": "the load-bearing reason a future session needs this",`,
2496
+ ` "confidence": 0.0 to 1.0`,
2497
+ " }",
2498
+ " ```",
2499
+ ` Use \`[]\` if nothing in this task is worth remembering. Forced`,
2500
+ ` filler is worse than nothing \u2014 only record what would be lost`,
2501
+ ` otherwise.`,
2502
+ "",
2503
+ `3. **followups.json** \u2014 array of TODOs uncovered but not fixed.`,
2504
+ " ```json",
2505
+ " {",
2506
+ ` "title": "short summary",`,
2507
+ ` "body": "what the operator should do, and where",`,
2508
+ ` "rationale": "why this matters",`,
2509
+ ` "priority": "low" | "medium" | "high"`,
2510
+ " }",
2511
+ " ```",
2512
+ ` Use \`[]\` if nothing surfaced.`,
2513
+ "",
2514
+ `4. **handoff-notes.md** \u2014 short prose (\u2264200 words), no frontmatter:`,
2515
+ ` what you did and why, so the next person/agent can pick up cold.`,
2516
+ "",
2517
+ "Skipping any of the four files is an error. Empty arrays are fine;",
2518
+ "skipping the file is not."
2519
+ ].join("\n");
2570
2520
  }
2571
2521
 
2572
2522
  // src/chat/attachments.ts
2573
- import * as fs10 from "fs";
2574
- import * as path10 from "path";
2523
+ import * as fs9 from "fs";
2524
+ import * as path9 from "path";
2575
2525
  var INLINE_ATTACHMENT_RE = /(?:\[(?:Image|File): ([^\]]*)\]\n)?data:([\w.+-]+\/[\w.+-]+);base64,([A-Za-z0-9+/=]+)/g;
2576
2526
  var EXT_BY_MIME = {
2577
2527
  "image/png": "png",
@@ -2587,7 +2537,7 @@ function extFor(mime) {
2587
2537
  return EXT_BY_MIME[mime.toLowerCase()] ?? mime.split("/")[1]?.replace(/[^\w]/g, "") ?? "bin";
2588
2538
  }
2589
2539
  function attachmentsDir(cwd, sessionId) {
2590
- return path10.join(cwd, ".kody", "tmp", "attachments", sessionId);
2540
+ return path9.join(cwd, ".kody", "tmp", "attachments", sessionId);
2591
2541
  }
2592
2542
  function prepareAttachments(turns, cwd, sessionId) {
2593
2543
  const imagePaths = [];
@@ -2604,11 +2554,11 @@ function prepareAttachments(turns, cwd, sessionId) {
2604
2554
  if (!isImage) return `[File: ${name}]`;
2605
2555
  try {
2606
2556
  if (!dirEnsured) {
2607
- fs10.mkdirSync(dir, { recursive: true });
2557
+ fs9.mkdirSync(dir, { recursive: true });
2608
2558
  dirEnsured = true;
2609
2559
  }
2610
- const filePath = path10.join(dir, `${imageCounter}.${extFor(mime)}`);
2611
- fs10.writeFileSync(filePath, Buffer.from(data, "base64"));
2560
+ const filePath = path9.join(dir, `${imageCounter}.${extFor(mime)}`);
2561
+ fs9.writeFileSync(filePath, Buffer.from(data, "base64"));
2612
2562
  imageCounter += 1;
2613
2563
  imagePaths.push(filePath);
2614
2564
  return `[Image "${name}" is attached \u2014 saved to ${filePath}. Use the Read tool on that exact path to view it.]`;
@@ -2623,31 +2573,92 @@ function prepareAttachments(turns, cwd, sessionId) {
2623
2573
  return { turns: rewritten, imagePaths };
2624
2574
  }
2625
2575
 
2626
- // src/chat/loop.ts
2627
- var CHAT_SYSTEM_PROMPT = [
2628
- "You are Kody, an AI assistant for the Kody Operations Dashboard. Reply to the",
2629
- "user's latest message using the full conversation below as context. Keep replies",
2630
- "short and simple. Prefer one-liners and short paragraphs. Use plain terms, not jargon.",
2631
- "When you diagnose something, answer in this shape: a few words on the issue, a",
2632
- "few words on the fix, then a single question asking whether to proceed. Do not",
2633
- "pad it with preamble, restated context, or a trailing summary.",
2634
- "",
2635
- "# Your environment and capabilities",
2636
- "You run inside a sandboxed runner with a full clone of the user's repository",
2637
- "checked out at the current working directory. The runtime varies \u2014 it may be a",
2638
- "GitHub Actions job, a Fly Machine, or another container \u2014 but the tools and",
2639
- "capabilities below are identical across runtimes. Use the actual environment",
2640
- "(e.g. `uname`, `pwd`, `env`) to verify before claiming where you run. You have",
2641
- "real tools \u2014 use them before claiming you cannot do something. Never tell the",
2642
- "user you lack repo, filesystem, or GitHub access; you have all three.",
2643
- "",
2644
- "Tools you can call:",
2645
- "- Read, Edit, Write \u2014 full read/write access to every file in the repo (permission",
2646
- " mode is acceptEdits, so writes do not require confirmation).",
2647
- "- Glob, Grep \u2014 search the repo by filename pattern or content.",
2648
- "- Bash \u2014 run any shell command in the repo. The runner has:",
2649
- " - `git` (the repo is a real git checkout \u2014 `git log`, `git diff`,",
2650
- " `git show`, `git blame`, `git branch`, etc. all work).",
2576
+ // src/chat/session.ts
2577
+ import * as fs10 from "fs";
2578
+ import * as path10 from "path";
2579
+ function sessionFilePath(cwd, sessionId) {
2580
+ return path10.join(cwd, ".kody", "sessions", `${sessionId}.jsonl`);
2581
+ }
2582
+ function readMeta(file) {
2583
+ if (!fs10.existsSync(file)) return null;
2584
+ const raw = fs10.readFileSync(file, "utf-8");
2585
+ const firstLine2 = raw.split("\n", 1)[0]?.trim();
2586
+ if (!firstLine2) return null;
2587
+ try {
2588
+ const parsed = JSON.parse(firstLine2);
2589
+ if (parsed.type !== "meta") return null;
2590
+ if (parsed.mode !== "one-shot" && parsed.mode !== "interactive") return null;
2591
+ return parsed;
2592
+ } catch {
2593
+ return null;
2594
+ }
2595
+ }
2596
+ function readSession(file) {
2597
+ if (!fs10.existsSync(file)) return [];
2598
+ const raw = fs10.readFileSync(file, "utf-8").trim();
2599
+ if (!raw) return [];
2600
+ const turns = [];
2601
+ for (const line of raw.split("\n")) {
2602
+ if (!line.trim()) continue;
2603
+ try {
2604
+ const parsed = JSON.parse(line);
2605
+ if (parsed.role !== "user" && parsed.role !== "assistant") continue;
2606
+ if (typeof parsed.content !== "string") continue;
2607
+ turns.push(parsed);
2608
+ } catch {
2609
+ }
2610
+ }
2611
+ return turns;
2612
+ }
2613
+ function appendTurn(file, turn) {
2614
+ fs10.mkdirSync(path10.dirname(file), { recursive: true });
2615
+ const line = JSON.stringify({
2616
+ role: turn.role,
2617
+ content: turn.content,
2618
+ timestamp: turn.timestamp,
2619
+ toolCalls: turn.toolCalls ?? []
2620
+ });
2621
+ fs10.appendFileSync(file, `${line}
2622
+ `);
2623
+ }
2624
+ function seedInitialMessage(file, message) {
2625
+ if (!message.trim()) return false;
2626
+ const turns = readSession(file);
2627
+ const lastUser = [...turns].reverse().find((t) => t.role === "user");
2628
+ if (lastUser && lastUser.content === message) return false;
2629
+ appendTurn(file, {
2630
+ role: "user",
2631
+ content: message,
2632
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2633
+ });
2634
+ return true;
2635
+ }
2636
+
2637
+ // src/chat/loop.ts
2638
+ var CHAT_SYSTEM_PROMPT = [
2639
+ "You are Kody, an AI assistant for the Kody Operations Dashboard. Reply to the",
2640
+ "user's latest message using the full conversation below as context. Keep replies",
2641
+ "short and simple. Prefer one-liners and short paragraphs. Use plain terms, not jargon.",
2642
+ "When you diagnose something, answer in this shape: a few words on the issue, a",
2643
+ "few words on the fix, then a single question asking whether to proceed. Do not",
2644
+ "pad it with preamble, restated context, or a trailing summary.",
2645
+ "",
2646
+ "# Your environment and capabilities",
2647
+ "You run inside a sandboxed runner with a full clone of the user's repository",
2648
+ "checked out at the current working directory. The runtime varies \u2014 it may be a",
2649
+ "GitHub Actions job, a Fly Machine, or another container \u2014 but the tools and",
2650
+ "capabilities below are identical across runtimes. Use the actual environment",
2651
+ "(e.g. `uname`, `pwd`, `env`) to verify before claiming where you run. You have",
2652
+ "real tools \u2014 use them before claiming you cannot do something. Never tell the",
2653
+ "user you lack repo, filesystem, or GitHub access; you have all three.",
2654
+ "",
2655
+ "Tools you can call:",
2656
+ "- Read, Edit, Write \u2014 full read/write access to every file in the repo (permission",
2657
+ " mode is acceptEdits, so writes do not require confirmation).",
2658
+ "- Glob, Grep \u2014 search the repo by filename pattern or content.",
2659
+ "- Bash \u2014 run any shell command in the repo. The runner has:",
2660
+ " - `git` (the repo is a real git checkout \u2014 `git log`, `git diff`,",
2661
+ " `git show`, `git blame`, `git branch`, etc. all work).",
2651
2662
  " - `gh` authenticated against this repository's GitHub via a `GITHUB_TOKEN`",
2652
2663
  " env var (read issues, PRs, workflows, runs, comments; query the API",
2653
2664
  " with `gh api`).",
@@ -2934,7 +2945,9 @@ ${content}`);
2934
2945
  }
2935
2946
  const joined = sections.join("\n\n").trim();
2936
2947
  if (!joined) return "";
2937
- const body = joined.length > MAX_CONTEXT_BYTES ? joined.slice(0, MAX_CONTEXT_BYTES) + "\n\n_\u2026 (context truncated; see `.kody/context/` for the full text)_" : joined;
2948
+ const body = joined.length > MAX_CONTEXT_BYTES ? `${joined.slice(0, MAX_CONTEXT_BYTES)}
2949
+
2950
+ _\u2026 (context truncated; see \`.kody/context/\` for the full text)_` : joined;
2938
2951
  return [
2939
2952
  "# Context (`.kody/context/`) \u2014 your default frame",
2940
2953
  "",
@@ -2955,7 +2968,9 @@ function readInstructionsBlock(cwd) {
2955
2968
  }
2956
2969
  const trimmed = raw.trim();
2957
2970
  if (!trimmed) return "";
2958
- const body = trimmed.length > MAX_INSTRUCTIONS_BYTES ? trimmed.slice(0, MAX_INSTRUCTIONS_BYTES) + "\n\n_\u2026 (instructions truncated)_" : trimmed;
2971
+ const body = trimmed.length > MAX_INSTRUCTIONS_BYTES ? `${trimmed.slice(0, MAX_INSTRUCTIONS_BYTES)}
2972
+
2973
+ _\u2026 (instructions truncated)_` : trimmed;
2959
2974
  return [
2960
2975
  "# User instructions for this repo (`.kody/instructions.md`)",
2961
2976
  "",
@@ -3172,7 +3187,8 @@ function putJsonlViaContents(repository, branch, repoPath, localText, sessionId,
3172
3187
  const localLines = jsonlLines(localText);
3173
3188
  const localSet = new Set(localLines);
3174
3189
  const extra = remote.lines.filter((l) => !localSet.has(l));
3175
- if (extra.length > 0) body = [...localLines, ...extra].join("\n") + "\n";
3190
+ if (extra.length > 0) body = `${[...localLines, ...extra].join("\n")}
3191
+ `;
3176
3192
  }
3177
3193
  const payload = {
3178
3194
  message: `chat: interactive turn for ${sessionId}`,
@@ -3372,18 +3388,7 @@ function cronMatchesInWindow(spec, end, windowSec) {
3372
3388
  }
3373
3389
 
3374
3390
  // src/dispatch.ts
3375
- var POLITE_WORDS = /* @__PURE__ */ new Set([
3376
- "please",
3377
- "kindly",
3378
- "hi",
3379
- "hey",
3380
- "hello",
3381
- "thanks",
3382
- "thank",
3383
- "plz",
3384
- "pls",
3385
- "yo"
3386
- ]);
3391
+ var POLITE_WORDS = /* @__PURE__ */ new Set(["please", "kindly", "hi", "hey", "hello", "thanks", "thank", "plz", "pls", "yo"]);
3387
3392
  function primaryNumericInputName(executable) {
3388
3393
  const inputs = getProfileInputs(executable);
3389
3394
  if (!inputs) return null;
@@ -3417,7 +3422,18 @@ function autoDispatch(opts) {
3417
3422
  return null;
3418
3423
  }
3419
3424
  if (eventName === "schedule") return null;
3420
- if (eventName === "pull_request") return null;
3425
+ if (eventName === "pull_request") {
3426
+ const exe = opts?.config?.onPullRequest?.trim();
3427
+ const action = String(event.action ?? "");
3428
+ if (exe && (action === "opened" || action === "synchronize" || action === "reopened")) {
3429
+ const prNum = Number(event.pull_request?.number ?? event.number ?? 0);
3430
+ if (prNum > 0) {
3431
+ const targetKey = primaryNumericInputName(exe) ?? "pr";
3432
+ return { executable: exe, cliArgs: { [targetKey]: prNum }, target: prNum };
3433
+ }
3434
+ }
3435
+ return null;
3436
+ }
3421
3437
  if (eventName !== "issue_comment") return null;
3422
3438
  const rawBody = String(event.comment?.body ?? "");
3423
3439
  const authorLogin = String(event.comment?.user?.login ?? "");
@@ -3519,7 +3535,10 @@ function autoDispatchTyped(opts) {
3519
3535
  const afterTag = extractAfterTag(rawBody.toLowerCase());
3520
3536
  const tokenRaw = extractSubcommand(afterTag) ?? "";
3521
3537
  if (!tokenRaw || POLITE_WORDS.has(tokenRaw)) {
3522
- return { kind: "silent", reason: tokenRaw ? `polite-word lead-in '${tokenRaw}', no default executable configured` : "no subcommand token, no default executable configured" };
3538
+ return {
3539
+ kind: "silent",
3540
+ reason: tokenRaw ? `polite-word lead-in '${tokenRaw}', no default executable configured` : "no subcommand token, no default executable configured"
3541
+ };
3523
3542
  }
3524
3543
  const available = listExecutables().map((e) => e.name).filter((n) => !n.startsWith("goal-") && !n.startsWith("job-")).sort();
3525
3544
  return { kind: "unrecognized", token: tokenRaw, target: targetNum, isPr, available };
@@ -3745,7 +3764,10 @@ function validateConfig(raw, profilePath) {
3745
3764
  const lbl = label;
3746
3765
  for (const k of ["name", "color", "description"]) {
3747
3766
  if (typeof lbl[k] !== "string" || lbl[k].length === 0) {
3748
- throw new ProfileError(profilePath, `lifecycle "pr-branch": lifecycleConfig.label.${k} must be a non-empty string`);
3767
+ throw new ProfileError(
3768
+ profilePath,
3769
+ `lifecycle "pr-branch": lifecycleConfig.label.${k} must be a non-empty string`
3770
+ );
3749
3771
  }
3750
3772
  }
3751
3773
  const context = raw.context === void 0 ? "task" : raw.context;
@@ -4256,7 +4278,9 @@ function parseStateComment(body) {
4256
4278
  try {
4257
4279
  parsed = JSON.parse(jsonStr);
4258
4280
  } catch (err) {
4259
- throw new CorruptStateError(`state JSON unparseable (truncated comment?): ${err instanceof Error ? err.message : String(err)}`);
4281
+ throw new CorruptStateError(
4282
+ `state JSON unparseable (truncated comment?): ${err instanceof Error ? err.message : String(err)}`
4283
+ );
4260
4284
  }
4261
4285
  if (parsed?.schemaVersion !== 1) {
4262
4286
  throw new CorruptStateError(`unexpected schemaVersion: ${JSON.stringify(parsed?.schemaVersion)}`);
@@ -4757,7 +4781,7 @@ function extractLabelSpec(entry) {
4757
4781
  const w = entry.with;
4758
4782
  if (!w) return null;
4759
4783
  const label = typeof w.label === "string" ? w.label : null;
4760
- if (!label || !label.startsWith(KODY_NAMESPACE)) return null;
4784
+ if (!label?.startsWith(KODY_NAMESPACE)) return null;
4761
4785
  return {
4762
4786
  label,
4763
4787
  color: typeof w.color === "string" ? w.color : void 0,
@@ -5001,139 +5025,6 @@ function stripBlockingEnv(env) {
5001
5025
  return out;
5002
5026
  }
5003
5027
 
5004
- // src/subagents.ts
5005
- import * as fs20 from "fs";
5006
- import * as path18 from "path";
5007
-
5008
- // src/scripts/buildSyntheticPlugin.ts
5009
- import * as fs19 from "fs";
5010
- import * as os3 from "os";
5011
- import * as path17 from "path";
5012
- function getPluginsCatalogRoot() {
5013
- const here = path17.dirname(new URL(import.meta.url).pathname);
5014
- const candidates = [
5015
- path17.join(here, "..", "plugins"),
5016
- // dev: src/scripts → src/plugins
5017
- path17.join(here, "..", "..", "plugins"),
5018
- // built: dist/scripts → dist/plugins
5019
- path17.join(here, "..", "..", "src", "plugins")
5020
- // fallback
5021
- ];
5022
- for (const c of candidates) {
5023
- if (fs19.existsSync(c) && fs19.statSync(c).isDirectory()) return c;
5024
- }
5025
- return candidates[0];
5026
- }
5027
- var buildSyntheticPlugin = async (ctx, profile) => {
5028
- const cc = profile.claudeCode;
5029
- const needsSynthetic = cc.skills.length > 0 || cc.commands.length > 0 || cc.hooks.length > 0;
5030
- if (!needsSynthetic) return;
5031
- const catalog = getPluginsCatalogRoot();
5032
- const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
5033
- const root = path17.join(os3.tmpdir(), `kody-synth-${runId}`);
5034
- fs19.mkdirSync(path17.join(root, ".claude-plugin"), { recursive: true });
5035
- const resolvePart = (bucket, entry) => {
5036
- const local = path17.join(profile.dir, bucket, entry);
5037
- if (fs19.existsSync(local)) return local;
5038
- const central = path17.join(catalog, bucket, entry);
5039
- if (fs19.existsSync(central)) return central;
5040
- throw new Error(
5041
- `buildSyntheticPlugin: ${bucket} entry '${entry}' not found in executable dir (${profile.dir}/${bucket}/) or catalog (${catalog}/${bucket}/)`
5042
- );
5043
- };
5044
- if (cc.skills.length > 0) {
5045
- const dst = path17.join(root, "skills");
5046
- fs19.mkdirSync(dst, { recursive: true });
5047
- for (const name of cc.skills) {
5048
- copyDir(resolvePart("skills", name), path17.join(dst, name));
5049
- }
5050
- }
5051
- if (cc.commands.length > 0) {
5052
- const dst = path17.join(root, "commands");
5053
- fs19.mkdirSync(dst, { recursive: true });
5054
- for (const name of cc.commands) {
5055
- fs19.copyFileSync(resolvePart("commands", `${name}.md`), path17.join(dst, `${name}.md`));
5056
- }
5057
- }
5058
- if (cc.hooks.length > 0) {
5059
- const dst = path17.join(root, "hooks");
5060
- fs19.mkdirSync(dst, { recursive: true });
5061
- const merged = { hooks: {} };
5062
- for (const name of cc.hooks) {
5063
- const src = resolvePart("hooks", `${name}.json`);
5064
- const parsed = JSON.parse(fs19.readFileSync(src, "utf-8"));
5065
- for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
5066
- if (!Array.isArray(entries)) continue;
5067
- if (!merged.hooks[event]) merged.hooks[event] = [];
5068
- merged.hooks[event].push(...entries);
5069
- }
5070
- }
5071
- fs19.writeFileSync(path17.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
5072
- `);
5073
- }
5074
- const manifest = {
5075
- name: `kody-synth-${profile.name}`,
5076
- version: "1.0.0",
5077
- description: `Synthetic plugin assembled by Kody for profile '${profile.name}' at runtime.`
5078
- };
5079
- if (cc.skills.length > 0) manifest.skills = ["./skills/"];
5080
- if (cc.commands.length > 0) manifest.commands = ["./commands/"];
5081
- fs19.writeFileSync(path17.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
5082
- `);
5083
- ctx.data.syntheticPluginPath = root;
5084
- };
5085
- function copyDir(src, dst) {
5086
- fs19.mkdirSync(dst, { recursive: true });
5087
- for (const ent of fs19.readdirSync(src, { withFileTypes: true })) {
5088
- const s = path17.join(src, ent.name);
5089
- const d = path17.join(dst, ent.name);
5090
- if (ent.isDirectory()) copyDir(s, d);
5091
- else if (ent.isFile()) fs19.copyFileSync(s, d);
5092
- }
5093
- }
5094
-
5095
- // src/subagents.ts
5096
- function splitFrontmatter(raw) {
5097
- const match = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(raw);
5098
- if (!match) return { fm: {}, body: raw.trim() };
5099
- const fm = {};
5100
- for (const line of match[1].split("\n")) {
5101
- const idx = line.indexOf(":");
5102
- if (idx === -1) continue;
5103
- fm[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
5104
- }
5105
- return { fm, body: (match[2] ?? "").trim() };
5106
- }
5107
- function resolveAgentFile(profileDir, name) {
5108
- const local = path18.join(profileDir, "agents", `${name}.md`);
5109
- if (fs20.existsSync(local)) return local;
5110
- const central = path18.join(getPluginsCatalogRoot(), "agents", `${name}.md`);
5111
- if (fs20.existsSync(central)) return central;
5112
- throw new Error(
5113
- `loadSubagents: agent '${name}' not found in ${profileDir}/agents/ or shared catalog`
5114
- );
5115
- }
5116
- function loadSubagents(profile) {
5117
- const names = profile.claudeCode.subagents;
5118
- if (!names || names.length === 0) return void 0;
5119
- const agents = {};
5120
- for (const name of names) {
5121
- const { fm, body } = splitFrontmatter(fs20.readFileSync(resolveAgentFile(profile.dir, name), "utf-8"));
5122
- if (!body) throw new Error(`loadSubagents: agent '${name}' has an empty prompt body`);
5123
- const def = {
5124
- description: fm.description ?? `Subagent ${name}`,
5125
- prompt: body
5126
- };
5127
- if (fm.tools) {
5128
- const tools = fm.tools.split(",").map((t) => t.trim()).filter(Boolean);
5129
- if (tools.length > 0) def.tools = tools;
5130
- }
5131
- if (fm.model) def.model = fm.model;
5132
- agents[fm.name || name] = def;
5133
- }
5134
- return agents;
5135
- }
5136
-
5137
5028
  // src/commit.ts
5138
5029
  import { execFileSync as execFileSync8 } from "child_process";
5139
5030
 
@@ -5164,7 +5055,7 @@ function runGit(args, cwd) {
5164
5055
  }
5165
5056
  }
5166
5057
  function resolveBranch(cwd, explicit) {
5167
- if (explicit && explicit.trim()) return explicit.trim();
5058
+ if (explicit?.trim()) return explicit.trim();
5168
5059
  const r = runGit(["symbolic-ref", "--short", "HEAD"], cwd);
5169
5060
  return r.ok ? r.stdout.trim() : "";
5170
5061
  }
@@ -5214,8 +5105,8 @@ function pushWithRetry(opts = {}) {
5214
5105
  }
5215
5106
 
5216
5107
  // src/commit.ts
5217
- import * as fs21 from "fs";
5218
- import * as path19 from "path";
5108
+ import * as fs19 from "fs";
5109
+ import * as path17 from "path";
5219
5110
  var FORBIDDEN_PATH_PREFIXES = [
5220
5111
  ".kody/",
5221
5112
  ".kody-engine/",
@@ -5276,18 +5167,18 @@ function tryGit(args, cwd) {
5276
5167
  }
5277
5168
  function abortUnfinishedGitOps(cwd) {
5278
5169
  const aborted = [];
5279
- const gitDir = path19.join(cwd ?? process.cwd(), ".git");
5280
- if (!fs21.existsSync(gitDir)) return aborted;
5281
- if (fs21.existsSync(path19.join(gitDir, "MERGE_HEAD"))) {
5170
+ const gitDir = path17.join(cwd ?? process.cwd(), ".git");
5171
+ if (!fs19.existsSync(gitDir)) return aborted;
5172
+ if (fs19.existsSync(path17.join(gitDir, "MERGE_HEAD"))) {
5282
5173
  if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
5283
5174
  }
5284
- if (fs21.existsSync(path19.join(gitDir, "CHERRY_PICK_HEAD"))) {
5175
+ if (fs19.existsSync(path17.join(gitDir, "CHERRY_PICK_HEAD"))) {
5285
5176
  if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
5286
5177
  }
5287
- if (fs21.existsSync(path19.join(gitDir, "REVERT_HEAD"))) {
5178
+ if (fs19.existsSync(path17.join(gitDir, "REVERT_HEAD"))) {
5288
5179
  if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
5289
5180
  }
5290
- if (fs21.existsSync(path19.join(gitDir, "rebase-merge")) || fs21.existsSync(path19.join(gitDir, "rebase-apply"))) {
5181
+ if (fs19.existsSync(path17.join(gitDir, "rebase-merge")) || fs19.existsSync(path17.join(gitDir, "rebase-apply"))) {
5291
5182
  if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
5292
5183
  }
5293
5184
  try {
@@ -5343,7 +5234,7 @@ function normalizeCommitMessage(raw) {
5343
5234
  function commitAndPush(branch, agentMessage, cwd) {
5344
5235
  const allChanged = listChangedFiles(cwd);
5345
5236
  const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
5346
- const mergeHeadExists = fs21.existsSync(path19.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
5237
+ const mergeHeadExists = fs19.existsSync(path17.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
5347
5238
  if (allowedFiles.length === 0 && !mergeHeadExists) {
5348
5239
  return { committed: false, pushed: false, sha: "", message: "" };
5349
5240
  }
@@ -5473,38 +5364,205 @@ var advanceFlow = async (ctx, profile) => {
5473
5364
  ctx.output.nextDispatch = { executable: flow.name, cliArgs: { issue: flow.issueNumber } };
5474
5365
  };
5475
5366
 
5476
- // src/scripts/brainServe.ts
5477
- init_repoWorkspace();
5478
- import { createServer } from "http";
5479
- import * as fs23 from "fs";
5480
- import * as path21 from "path";
5481
-
5482
- // src/scripts/brainTurnLog.ts
5483
- import * as fs22 from "fs";
5484
- import * as path20 from "path";
5485
- var live = /* @__PURE__ */ new Map();
5486
- function eventsPath(dir, chatId) {
5487
- return path20.join(dir, ".kody", "brain-events", `${chatId}.jsonl`);
5367
+ // src/gha.ts
5368
+ import { execFileSync as execFileSync10 } from "child_process";
5369
+ import * as fs20 from "fs";
5370
+ function getRunUrl() {
5371
+ const server = process.env.GITHUB_SERVER_URL;
5372
+ const repo = process.env.GITHUB_REPOSITORY;
5373
+ const runId = process.env.GITHUB_RUN_ID;
5374
+ if (!server || !repo || !runId) return "";
5375
+ return `${server}/${repo}/actions/runs/${runId}`;
5488
5376
  }
5489
- function lastPersistedSeq(dir, chatId) {
5490
- const p = eventsPath(dir, chatId);
5491
- if (!fs22.existsSync(p)) return 0;
5492
- const lines = fs22.readFileSync(p, "utf-8").split("\n").filter(Boolean);
5493
- if (lines.length === 0) return 0;
5377
+ function reactToTriggerComment(cwd) {
5378
+ if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
5379
+ const eventPath = process.env.GITHUB_EVENT_PATH;
5380
+ if (!eventPath || !fs20.existsSync(eventPath)) return;
5381
+ let event = null;
5494
5382
  try {
5495
- return JSON.parse(lines[lines.length - 1]).seq || 0;
5383
+ event = JSON.parse(fs20.readFileSync(eventPath, "utf-8"));
5496
5384
  } catch {
5497
- return 0;
5385
+ return;
5498
5386
  }
5499
- }
5500
- function readSince(dir, chatId, since) {
5501
- const p = eventsPath(dir, chatId);
5502
- if (!fs22.existsSync(p)) return [];
5503
- const out = [];
5504
- for (const line of fs22.readFileSync(p, "utf-8").split("\n")) {
5505
- if (!line) continue;
5387
+ const commentId = event?.comment?.id;
5388
+ const repo = process.env.GITHUB_REPOSITORY;
5389
+ if (!commentId || !repo) return;
5390
+ const token = process.env.KODY_TOKEN?.trim() || process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
5391
+ const args = [
5392
+ "api",
5393
+ "-X",
5394
+ "POST",
5395
+ "-H",
5396
+ "Accept: application/vnd.github+json",
5397
+ `/repos/${repo}/issues/comments/${commentId}/reactions`,
5398
+ "-f",
5399
+ "content=eyes"
5400
+ ];
5401
+ const opts = {
5402
+ cwd,
5403
+ env: { ...process.env, GH_TOKEN: token ?? process.env.GH_TOKEN ?? "" },
5404
+ stdio: "pipe",
5405
+ timeout: 15e3
5406
+ };
5407
+ let lastErr = null;
5408
+ for (let attempt = 0; attempt < 3; attempt++) {
5409
+ if (attempt > 0) sleepMs(attempt === 1 ? 500 : 1500);
5506
5410
  try {
5507
- const rec = JSON.parse(line);
5411
+ execFileSync10("gh", args, opts);
5412
+ return;
5413
+ } catch (err) {
5414
+ lastErr = err;
5415
+ }
5416
+ }
5417
+ process.stderr.write(
5418
+ `[kody] \u{1F440} reaction failed after 3 attempts on comment ${commentId}: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}
5419
+ `
5420
+ );
5421
+ }
5422
+ function sleepMs(ms) {
5423
+ try {
5424
+ execFileSync10("sleep", [(ms / 1e3).toString()], { stdio: "ignore", timeout: ms + 1e3 });
5425
+ } catch {
5426
+ }
5427
+ }
5428
+
5429
+ // src/scripts/appendCompanyActivity.ts
5430
+ init_issue();
5431
+
5432
+ // src/stateBranch.ts
5433
+ init_issue();
5434
+ var STATE_BRANCH = "kody-state";
5435
+ function is404(err) {
5436
+ const msg = err instanceof Error ? err.message : String(err);
5437
+ return /HTTP 404/i.test(msg) || /Not Found/i.test(msg);
5438
+ }
5439
+ function ensureStateBranch(owner, repo, cwd) {
5440
+ try {
5441
+ gh(["api", `/repos/${owner}/${repo}/git/ref/heads/${STATE_BRANCH}`], { cwd });
5442
+ return;
5443
+ } catch (err) {
5444
+ if (!is404(err)) throw err;
5445
+ }
5446
+ const repoInfo = JSON.parse(gh(["api", `/repos/${owner}/${repo}`], { cwd }));
5447
+ const defaultBranch2 = repoInfo.default_branch;
5448
+ if (!defaultBranch2) {
5449
+ throw new Error(`ensureStateBranch: could not resolve default branch for ${owner}/${repo}`);
5450
+ }
5451
+ const headRef = JSON.parse(gh(["api", `/repos/${owner}/${repo}/git/ref/heads/${defaultBranch2}`], { cwd }));
5452
+ const sha = headRef.object?.sha;
5453
+ if (!sha) {
5454
+ throw new Error(`ensureStateBranch: could not resolve head sha for ${owner}/${repo}@${defaultBranch2}`);
5455
+ }
5456
+ try {
5457
+ gh(["api", "--method", "POST", `/repos/${owner}/${repo}/git/refs`, "--input", "-"], {
5458
+ cwd,
5459
+ input: JSON.stringify({ ref: `refs/heads/${STATE_BRANCH}`, sha })
5460
+ });
5461
+ } catch (err) {
5462
+ const msg = err instanceof Error ? err.message : String(err);
5463
+ if (/already exists/i.test(msg) || /HTTP 422/i.test(msg)) return;
5464
+ throw err;
5465
+ }
5466
+ }
5467
+
5468
+ // src/scripts/appendCompanyActivity.ts
5469
+ function resolveTrigger(force) {
5470
+ const event = process.env.GITHUB_EVENT_NAME ?? "";
5471
+ if (event === "schedule") return "schedule";
5472
+ if (force || event === "issue_comment" || event === "workflow_dispatch") return "manual";
5473
+ return "event";
5474
+ }
5475
+ function appendLine(owner, repo, cwd, record) {
5476
+ const filePath = `.kody/activity/${record.ts.slice(0, 10)}.jsonl`;
5477
+ let existing = "";
5478
+ let sha;
5479
+ try {
5480
+ const out = gh(["api", `/repos/${owner}/${repo}/contents/${filePath}?ref=${STATE_BRANCH}`], { cwd });
5481
+ const json = JSON.parse(out);
5482
+ if (json.sha) sha = json.sha;
5483
+ if (json.content) existing = Buffer.from(json.content, "base64").toString("utf-8");
5484
+ } catch {
5485
+ }
5486
+ const body = `${existing}${JSON.stringify(record)}
5487
+ `;
5488
+ const payload = {
5489
+ message: `chore(activity): ${record.action}`,
5490
+ content: Buffer.from(body, "utf-8").toString("base64"),
5491
+ // Keep this high-frequency feed off the default branch.
5492
+ branch: STATE_BRANCH
5493
+ };
5494
+ if (sha) payload.sha = sha;
5495
+ ensureStateBranch(owner, repo, cwd);
5496
+ gh(["api", "--method", "PUT", `/repos/${owner}/${repo}/contents/${filePath}`, "--input", "-"], {
5497
+ cwd,
5498
+ input: JSON.stringify(payload)
5499
+ });
5500
+ }
5501
+ var appendCompanyActivity = async (ctx, _profile, agentResult) => {
5502
+ try {
5503
+ const owner = ctx.config?.github?.owner;
5504
+ const repo = ctx.config?.github?.repo;
5505
+ const duty = String(ctx.data.jobSlug ?? ctx.args?.job ?? "").trim();
5506
+ if (!owner || !repo || !duty) return;
5507
+ const dutyTitle = ctx.data.jobTitle ?? null;
5508
+ const staff = ctx.data.workerSlug || null;
5509
+ const staffTitle = ctx.data.workerTitle || null;
5510
+ const force = ctx.args?.force === true;
5511
+ const record = {
5512
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
5513
+ action: `Ran duty: ${dutyTitle ?? duty}`,
5514
+ duty,
5515
+ dutyTitle,
5516
+ staff,
5517
+ staffTitle,
5518
+ trigger: resolveTrigger(force),
5519
+ outcome: agentResult?.outcome ?? "unknown",
5520
+ outcomeKind: agentResult?.outcomeKind ?? null,
5521
+ reason: agentResult?.error ?? null,
5522
+ durationMs: agentResult?.durationMs ?? null,
5523
+ runUrl: getRunUrl() || null
5524
+ };
5525
+ appendLine(owner, repo, ctx.cwd, record);
5526
+ } catch (err) {
5527
+ process.stderr.write(
5528
+ `[activity] company-activity append failed: ${err instanceof Error ? err.message : String(err)}
5529
+ `
5530
+ );
5531
+ }
5532
+ };
5533
+
5534
+ // src/scripts/brainServe.ts
5535
+ import * as fs22 from "fs";
5536
+ import { createServer } from "http";
5537
+ import * as path19 from "path";
5538
+ init_repoWorkspace();
5539
+
5540
+ // src/scripts/brainTurnLog.ts
5541
+ import * as fs21 from "fs";
5542
+ import * as path18 from "path";
5543
+ var live = /* @__PURE__ */ new Map();
5544
+ function eventsPath(dir, chatId) {
5545
+ return path18.join(dir, ".kody", "brain-events", `${chatId}.jsonl`);
5546
+ }
5547
+ function lastPersistedSeq(dir, chatId) {
5548
+ const p = eventsPath(dir, chatId);
5549
+ if (!fs21.existsSync(p)) return 0;
5550
+ const lines = fs21.readFileSync(p, "utf-8").split("\n").filter(Boolean);
5551
+ if (lines.length === 0) return 0;
5552
+ try {
5553
+ return JSON.parse(lines[lines.length - 1]).seq || 0;
5554
+ } catch {
5555
+ return 0;
5556
+ }
5557
+ }
5558
+ function readSince(dir, chatId, since) {
5559
+ const p = eventsPath(dir, chatId);
5560
+ if (!fs21.existsSync(p)) return [];
5561
+ const out = [];
5562
+ for (const line of fs21.readFileSync(p, "utf-8").split("\n")) {
5563
+ if (!line) continue;
5564
+ try {
5565
+ const rec = JSON.parse(line);
5508
5566
  if (rec.seq > since) out.push(rec);
5509
5567
  } catch {
5510
5568
  }
@@ -5527,12 +5585,13 @@ function beginTurn(dir, chatId) {
5527
5585
  };
5528
5586
  live.set(chatId, state);
5529
5587
  const p = eventsPath(dir, chatId);
5530
- fs22.mkdirSync(path20.dirname(p), { recursive: true });
5588
+ fs21.mkdirSync(path18.dirname(p), { recursive: true });
5531
5589
  return (event) => {
5532
5590
  state.seq += 1;
5533
5591
  const rec = { seq: state.seq, turn, ts: Date.now(), event };
5534
5592
  try {
5535
- fs22.appendFileSync(p, JSON.stringify(rec) + "\n");
5593
+ fs21.appendFileSync(p, `${JSON.stringify(rec)}
5594
+ `);
5536
5595
  } catch (err) {
5537
5596
  process.stderr.write(
5538
5597
  `[brain-turn-log] append failed for ${chatId}: ${err instanceof Error ? err.message : String(err)}
@@ -5570,7 +5629,8 @@ function endTurnIfUnterminated(dir, chatId, errMessage) {
5570
5629
  event: { type: "error", error: errMessage || "turn ended unexpectedly", chatId }
5571
5630
  };
5572
5631
  try {
5573
- fs22.appendFileSync(eventsPath(dir, chatId), JSON.stringify(rec) + "\n");
5632
+ fs21.appendFileSync(eventsPath(dir, chatId), `${JSON.stringify(rec)}
5633
+ `);
5574
5634
  } catch {
5575
5635
  }
5576
5636
  state.status = "ended";
@@ -5642,9 +5702,7 @@ var DEFAULT_PORT = 8080;
5642
5702
  function getApiKey() {
5643
5703
  const key = (process.env.BRAIN_API_KEY ?? "").trim();
5644
5704
  if (!key) {
5645
- throw new Error(
5646
- "BRAIN_API_KEY env var is required \u2014 set it on the Fly machine before boot."
5647
- );
5705
+ throw new Error("BRAIN_API_KEY env var is required \u2014 set it on the Fly machine before boot.");
5648
5706
  }
5649
5707
  return key;
5650
5708
  }
@@ -5657,8 +5715,8 @@ function isSafeChatId(id) {
5657
5715
  function authOk(req, expected) {
5658
5716
  const xApiKey = req.headers["x-api-key"]?.trim();
5659
5717
  if (xApiKey && xApiKey === expected) return true;
5660
- const auth = req.headers["authorization"]?.trim();
5661
- if (auth && auth.toLowerCase().startsWith("bearer ")) {
5718
+ const auth = req.headers.authorization?.trim();
5719
+ if (auth?.toLowerCase().startsWith("bearer ")) {
5662
5720
  return auth.slice(7).trim() === expected;
5663
5721
  }
5664
5722
  return false;
@@ -5802,7 +5860,7 @@ async function handleChatTurn(req, res, chatId, opts) {
5802
5860
  const repo = strField(body, "repo");
5803
5861
  const repoToken = strField(body, "repoToken");
5804
5862
  const sessionFile = sessionFilePath(opts.cwd, chatId);
5805
- fs23.mkdirSync(path21.dirname(sessionFile), { recursive: true });
5863
+ fs22.mkdirSync(path19.dirname(sessionFile), { recursive: true });
5806
5864
  appendTurn(sessionFile, {
5807
5865
  role: "user",
5808
5866
  content: message,
@@ -5849,7 +5907,7 @@ async function handleChatTurn(req, res, chatId, opts) {
5849
5907
  function buildServer(opts) {
5850
5908
  const runTurn = opts.runTurn ?? runChatTurn;
5851
5909
  const cloneRepo = opts.cloneRepo ?? defaultCloneRepo;
5852
- const reposRoot = opts.reposRoot ?? path21.join(path21.dirname(path21.resolve(opts.cwd)), "repos");
5910
+ const reposRoot = opts.reposRoot ?? path19.join(path19.dirname(path19.resolve(opts.cwd)), "repos");
5853
5911
  return createServer(async (req, res) => {
5854
5912
  if (!req.method || !req.url) {
5855
5913
  sendJson(res, 400, { error: "bad request" });
@@ -5900,10 +5958,8 @@ var brainServe = async (ctx) => {
5900
5958
  ctx.skipAgent = true;
5901
5959
  const unpacked = unpackAllSecrets();
5902
5960
  if (unpacked > 0) {
5903
- process.stdout.write(
5904
- `[brain-serve] unpacked ${unpacked} secret(s) from ALL_SECRETS
5905
- `
5906
- );
5961
+ process.stdout.write(`[brain-serve] unpacked ${unpacked} secret(s) from ALL_SECRETS
5962
+ `);
5907
5963
  }
5908
5964
  const apiKey = getApiKey();
5909
5965
  const port = Number(process.env.PORT ?? DEFAULT_PORT);
@@ -5911,15 +5967,11 @@ var brainServe = async (ctx) => {
5911
5967
  const usesProxy = needsLitellmProxy(model);
5912
5968
  let handle = null;
5913
5969
  if (usesProxy) {
5914
- process.stdout.write(
5915
- `[brain-serve] starting LiteLLM proxy for ${model.provider}/${model.model}...
5916
- `
5917
- );
5970
+ process.stdout.write(`[brain-serve] starting LiteLLM proxy for ${model.provider}/${model.model}...
5971
+ `);
5918
5972
  handle = await startLitellmIfNeeded(model, ctx.cwd);
5919
- process.stdout.write(
5920
- `[brain-serve] LiteLLM ready at ${handle?.url ?? LITELLM_DEFAULT_URL}
5921
- `
5922
- );
5973
+ process.stdout.write(`[brain-serve] LiteLLM ready at ${handle?.url ?? LITELLM_DEFAULT_URL}
5974
+ `);
5923
5975
  }
5924
5976
  const litellmUrl = usesProxy ? handle?.url ?? LITELLM_DEFAULT_URL : null;
5925
5977
  const server = buildServer({
@@ -5932,10 +5984,8 @@ var brainServe = async (ctx) => {
5932
5984
  });
5933
5985
  await new Promise((resolve6) => {
5934
5986
  server.listen(port, "0.0.0.0", () => {
5935
- process.stdout.write(
5936
- `[brain-serve] listening on 0.0.0.0:${port} (cwd=${ctx.cwd})
5937
- `
5938
- );
5987
+ process.stdout.write(`[brain-serve] listening on 0.0.0.0:${port} (cwd=${ctx.cwd})
5988
+ `);
5939
5989
  resolve6();
5940
5990
  });
5941
5991
  });
@@ -5958,8 +6008,95 @@ var brainServe = async (ctx) => {
5958
6008
  });
5959
6009
  };
5960
6010
 
6011
+ // src/scripts/buildSyntheticPlugin.ts
6012
+ import * as fs23 from "fs";
6013
+ import * as os3 from "os";
6014
+ import * as path20 from "path";
6015
+ function getPluginsCatalogRoot() {
6016
+ const here = path20.dirname(new URL(import.meta.url).pathname);
6017
+ const candidates = [
6018
+ path20.join(here, "..", "plugins"),
6019
+ // dev: src/scripts → src/plugins
6020
+ path20.join(here, "..", "..", "plugins"),
6021
+ // built: dist/scripts → dist/plugins
6022
+ path20.join(here, "..", "..", "src", "plugins")
6023
+ // fallback
6024
+ ];
6025
+ for (const c of candidates) {
6026
+ if (fs23.existsSync(c) && fs23.statSync(c).isDirectory()) return c;
6027
+ }
6028
+ return candidates[0];
6029
+ }
6030
+ var buildSyntheticPlugin = async (ctx, profile) => {
6031
+ const cc = profile.claudeCode;
6032
+ const needsSynthetic = cc.skills.length > 0 || cc.commands.length > 0 || cc.hooks.length > 0;
6033
+ if (!needsSynthetic) return;
6034
+ const catalog = getPluginsCatalogRoot();
6035
+ const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
6036
+ const root = path20.join(os3.tmpdir(), `kody-synth-${runId}`);
6037
+ fs23.mkdirSync(path20.join(root, ".claude-plugin"), { recursive: true });
6038
+ const resolvePart = (bucket, entry) => {
6039
+ const local = path20.join(profile.dir, bucket, entry);
6040
+ if (fs23.existsSync(local)) return local;
6041
+ const central = path20.join(catalog, bucket, entry);
6042
+ if (fs23.existsSync(central)) return central;
6043
+ throw new Error(
6044
+ `buildSyntheticPlugin: ${bucket} entry '${entry}' not found in executable dir (${profile.dir}/${bucket}/) or catalog (${catalog}/${bucket}/)`
6045
+ );
6046
+ };
6047
+ if (cc.skills.length > 0) {
6048
+ const dst = path20.join(root, "skills");
6049
+ fs23.mkdirSync(dst, { recursive: true });
6050
+ for (const name of cc.skills) {
6051
+ copyDir(resolvePart("skills", name), path20.join(dst, name));
6052
+ }
6053
+ }
6054
+ if (cc.commands.length > 0) {
6055
+ const dst = path20.join(root, "commands");
6056
+ fs23.mkdirSync(dst, { recursive: true });
6057
+ for (const name of cc.commands) {
6058
+ fs23.copyFileSync(resolvePart("commands", `${name}.md`), path20.join(dst, `${name}.md`));
6059
+ }
6060
+ }
6061
+ if (cc.hooks.length > 0) {
6062
+ const dst = path20.join(root, "hooks");
6063
+ fs23.mkdirSync(dst, { recursive: true });
6064
+ const merged = { hooks: {} };
6065
+ for (const name of cc.hooks) {
6066
+ const src = resolvePart("hooks", `${name}.json`);
6067
+ const parsed = JSON.parse(fs23.readFileSync(src, "utf-8"));
6068
+ for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
6069
+ if (!Array.isArray(entries)) continue;
6070
+ if (!merged.hooks[event]) merged.hooks[event] = [];
6071
+ merged.hooks[event].push(...entries);
6072
+ }
6073
+ }
6074
+ fs23.writeFileSync(path20.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
6075
+ `);
6076
+ }
6077
+ const manifest = {
6078
+ name: `kody-synth-${profile.name}`,
6079
+ version: "1.0.0",
6080
+ description: `Synthetic plugin assembled by Kody for profile '${profile.name}' at runtime.`
6081
+ };
6082
+ if (cc.skills.length > 0) manifest.skills = ["./skills/"];
6083
+ if (cc.commands.length > 0) manifest.commands = ["./commands/"];
6084
+ fs23.writeFileSync(path20.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
6085
+ `);
6086
+ ctx.data.syntheticPluginPath = root;
6087
+ };
6088
+ function copyDir(src, dst) {
6089
+ fs23.mkdirSync(dst, { recursive: true });
6090
+ for (const ent of fs23.readdirSync(src, { withFileTypes: true })) {
6091
+ const s = path20.join(src, ent.name);
6092
+ const d = path20.join(dst, ent.name);
6093
+ if (ent.isDirectory()) copyDir(s, d);
6094
+ else if (ent.isFile()) fs23.copyFileSync(s, d);
6095
+ }
6096
+ }
6097
+
5961
6098
  // src/coverage.ts
5962
- import { execFileSync as execFileSync10 } from "child_process";
6099
+ import { execFileSync as execFileSync11 } from "child_process";
5963
6100
  function patternToRegex(pattern) {
5964
6101
  let s = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
5965
6102
  s = s.replace(/\*\*\//g, "\xA7S").replace(/\*\*/g, "\xA7A").replace(/\*/g, "[^/]*");
@@ -5977,7 +6114,7 @@ function renderSiblingPath(file, requireSibling) {
5977
6114
  }
5978
6115
  function safeGit(args, cwd) {
5979
6116
  try {
5980
- return execFileSync10("git", args, { encoding: "utf-8", cwd, env: { ...process.env, HUSKY: "0" } }).trim();
6117
+ return execFileSync11("git", args, { encoding: "utf-8", cwd, env: { ...process.env, HUSKY: "0" } }).trim();
5981
6118
  } catch {
5982
6119
  return "";
5983
6120
  }
@@ -6053,8 +6190,10 @@ ${formatMissesForFeedback(misses)}`;
6053
6190
  retry = await invoker(retryPrompt);
6054
6191
  } catch (err) {
6055
6192
  const msg = err instanceof Error ? err.message : String(err);
6056
- process.stderr.write(`[kody] coverage retry agent failed (${msg}); keeping ${misses.length} miss(es) \u2014 PR will draft
6057
- `);
6193
+ process.stderr.write(
6194
+ `[kody] coverage retry agent failed (${msg}); keeping ${misses.length} miss(es) \u2014 PR will draft
6195
+ `
6196
+ );
6058
6197
  ctx.data.coverageMisses = misses;
6059
6198
  return;
6060
6199
  }
@@ -6105,12 +6244,12 @@ function defaultLabelMap() {
6105
6244
 
6106
6245
  // src/scripts/commitAndPush.ts
6107
6246
  import * as fs24 from "fs";
6108
- import * as path22 from "path";
6247
+ import * as path21 from "path";
6109
6248
  init_events();
6110
6249
  var DEFAULT_COMMIT_MESSAGE = "chore: kody changes";
6111
6250
  function sentinelPathForStage(cwd, profileName) {
6112
6251
  const runId = resolveRunId();
6113
- return path22.join(cwd, ".kody", "runs", runId, `commit-${profileName}.lock`);
6252
+ return path21.join(cwd, ".kody", "runs", runId, `commit-${profileName}.lock`);
6114
6253
  }
6115
6254
  var commitAndPush2 = async (ctx, profile) => {
6116
6255
  const branch = ctx.data.branch;
@@ -6175,7 +6314,7 @@ var commitAndPush2 = async (ctx, profile) => {
6175
6314
  const result = ctx.data.commitResult;
6176
6315
  if (sentinel && result?.committed) {
6177
6316
  try {
6178
- fs24.mkdirSync(path22.dirname(sentinel), { recursive: true });
6317
+ fs24.mkdirSync(path21.dirname(sentinel), { recursive: true });
6179
6318
  fs24.writeFileSync(
6180
6319
  sentinel,
6181
6320
  JSON.stringify(
@@ -6198,47 +6337,9 @@ var commitAndPush2 = async (ctx, profile) => {
6198
6337
  // src/goal/stateStore.ts
6199
6338
  init_issue();
6200
6339
 
6201
- // src/stateBranch.ts
6202
- init_issue();
6203
- var STATE_BRANCH = "kody-state";
6204
- function is404(err) {
6205
- const msg = err instanceof Error ? err.message : String(err);
6206
- return /HTTP 404/i.test(msg) || /Not Found/i.test(msg);
6207
- }
6208
- function ensureStateBranch(owner, repo, cwd) {
6209
- try {
6210
- gh(["api", `/repos/${owner}/${repo}/git/ref/heads/${STATE_BRANCH}`], { cwd });
6211
- return;
6212
- } catch (err) {
6213
- if (!is404(err)) throw err;
6214
- }
6215
- const repoInfo = JSON.parse(gh(["api", `/repos/${owner}/${repo}`], { cwd }));
6216
- const defaultBranch2 = repoInfo.default_branch;
6217
- if (!defaultBranch2) {
6218
- throw new Error(`ensureStateBranch: could not resolve default branch for ${owner}/${repo}`);
6219
- }
6220
- const headRef = JSON.parse(
6221
- gh(["api", `/repos/${owner}/${repo}/git/ref/heads/${defaultBranch2}`], { cwd })
6222
- );
6223
- const sha = headRef.object?.sha;
6224
- if (!sha) {
6225
- throw new Error(`ensureStateBranch: could not resolve head sha for ${owner}/${repo}@${defaultBranch2}`);
6226
- }
6227
- try {
6228
- gh(["api", "--method", "POST", `/repos/${owner}/${repo}/git/refs`, "--input", "-"], {
6229
- cwd,
6230
- input: JSON.stringify({ ref: `refs/heads/${STATE_BRANCH}`, sha })
6231
- });
6232
- } catch (err) {
6233
- const msg = err instanceof Error ? err.message : String(err);
6234
- if (/already exists/i.test(msg) || /HTTP 422/i.test(msg)) return;
6235
- throw err;
6236
- }
6237
- }
6238
-
6239
6340
  // src/goal/state.ts
6240
6341
  import * as fs25 from "fs";
6241
- import * as path23 from "path";
6342
+ import * as path22 from "path";
6242
6343
  var VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "awaiting-merge", "done"]);
6243
6344
  var GoalStateError = class extends Error {
6244
6345
  constructor(path41, message) {
@@ -6385,15 +6486,15 @@ function describeCommitMessage(goal) {
6385
6486
 
6386
6487
  // src/scripts/composePrompt.ts
6387
6488
  import * as fs26 from "fs";
6388
- import * as path24 from "path";
6489
+ import * as path23 from "path";
6389
6490
  var MUSTACHE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
6390
6491
  var composePrompt = async (ctx, profile) => {
6391
6492
  const explicit = ctx.data.promptTemplate;
6392
6493
  const mode = ctx.args.mode;
6393
6494
  const candidates = [
6394
- explicit ? path24.join(profile.dir, explicit) : null,
6395
- mode ? path24.join(profile.dir, "prompts", `${mode}.md`) : null,
6396
- path24.join(profile.dir, "prompt.md")
6495
+ explicit ? path23.join(profile.dir, explicit) : null,
6496
+ mode ? path23.join(profile.dir, "prompts", `${mode}.md`) : null,
6497
+ path23.join(profile.dir, "prompt.md")
6397
6498
  ].filter(Boolean);
6398
6499
  let templatePath = "";
6399
6500
  let template = "";
@@ -6809,12 +6910,7 @@ QA_REPORT_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.gith
6809
6910
  ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
6810
6911
  return;
6811
6912
  }
6812
- await promoteReportToGoal(
6813
- ctx,
6814
- finalText,
6815
- ctx.args.scope,
6816
- ctx.args.goal
6817
- );
6913
+ await promoteReportToGoal(ctx, finalText, ctx.args.scope, ctx.args.goal);
6818
6914
  };
6819
6915
  async function promoteReportToGoal(ctx, finalText, scopeArg, explicitGoalArg) {
6820
6916
  const { markdown, data, jsonError } = splitReport(finalText);
@@ -6830,10 +6926,10 @@ async function promoteReportToGoal(ctx, finalText, scopeArg, explicitGoalArg) {
6830
6926
  const title = `QA [${verdict}]: ${scope2?.trim() || "smoke"} \u2014 ${todayIso()}`.slice(0, 240);
6831
6927
  let url = "";
6832
6928
  try {
6833
- const out = gh(
6834
- ["issue", "create", "--title", title, "--label", FINDING_LABEL, "--body-file", "-"],
6835
- { input: finalText, cwd: ctx.cwd }
6836
- );
6929
+ const out = gh(["issue", "create", "--title", title, "--label", FINDING_LABEL, "--body-file", "-"], {
6930
+ input: finalText,
6931
+ cwd: ctx.cwd
6932
+ });
6837
6933
  url = out.split("\n").map((l) => l.trim()).filter(Boolean).pop() ?? "";
6838
6934
  } catch (err) {
6839
6935
  const msg = err instanceof Error ? err.message : String(err);
@@ -6866,13 +6962,9 @@ QA_REPORT_POSTED=${url} (verdict: ${verdict})
6866
6962
  if (manifestRead.number !== null) {
6867
6963
  manifestIssueNumber = manifestRead.number;
6868
6964
  try {
6869
- postIssueComment(
6870
- manifestRead.number,
6871
- `## QA \u2014 ${verdict} \xB7 goal \`${goalId}\`
6965
+ postIssueComment(manifestRead.number, `## QA \u2014 ${verdict} \xB7 goal \`${goalId}\`
6872
6966
 
6873
- ${markdown}`,
6874
- ctx.cwd
6875
- );
6967
+ ${markdown}`, ctx.cwd);
6876
6968
  } catch (err) {
6877
6969
  const reason = err instanceof Error ? err.message : String(err);
6878
6970
  process.stderr.write(`[createQaGoal] could not comment on manifest issue: ${reason.slice(0, 300)}
@@ -6925,7 +7017,14 @@ ${markdown}`,
6925
7017
  const now = nowIso();
6926
7018
  const goalState = { state: "active", startedAt: now, updatedAt: now, extra: { version: 1 } };
6927
7019
  try {
6928
- putGoalState(ctx.config.github.owner, ctx.config.github.repo, goalId, goalState, `chore(goals): activate ${goalId}`, ctx.cwd);
7020
+ putGoalState(
7021
+ ctx.config.github.owner,
7022
+ ctx.config.github.repo,
7023
+ goalId,
7024
+ goalState,
7025
+ `chore(goals): activate ${goalId}`,
7026
+ ctx.cwd
7027
+ );
6929
7028
  } catch (err) {
6930
7029
  process.stderr.write(
6931
7030
  `[createQaGoal] failed to persist goal state to kody-state: ${err instanceof Error ? err.message : String(err)}
@@ -7000,16 +7099,7 @@ function listGoalIssues(goalId, cwd) {
7000
7099
  function listOpenPrs(cwd) {
7001
7100
  try {
7002
7101
  const out = gh(
7003
- [
7004
- "pr",
7005
- "list",
7006
- "--state",
7007
- "open",
7008
- "--limit",
7009
- "200",
7010
- "--json",
7011
- "number,url,isDraft,headRefName,baseRefName,body"
7012
- ],
7102
+ ["pr", "list", "--state", "open", "--limit", "200", "--json", "number,url,isDraft,headRefName,baseRefName,body"],
7013
7103
  { cwd }
7014
7104
  );
7015
7105
  return { ok: true, value: JSON.parse(out) };
@@ -7116,10 +7206,7 @@ function mergePrSquash(prNumber, cwd) {
7116
7206
  }
7117
7207
  function editPrBase(prNumber, baseBranch, cwd) {
7118
7208
  try {
7119
- gh(
7120
- ["api", "--method", "PATCH", `repos/{owner}/{repo}/pulls/${prNumber}`, "-f", `base=${baseBranch}`],
7121
- { cwd }
7122
- );
7209
+ gh(["api", "--method", "PATCH", `repos/{owner}/{repo}/pulls/${prNumber}`, "-f", `base=${baseBranch}`], { cwd });
7123
7210
  return { ok: true };
7124
7211
  } catch (err) {
7125
7212
  return fail(err);
@@ -7195,7 +7282,10 @@ var deriveGoalPhase = async (ctx) => {
7195
7282
  goal.phase = "idle";
7196
7283
  return;
7197
7284
  }
7198
- const taskPrs = filterGoalTaskPrs(allPrs.value ?? [], rawIssues.map((i) => i.number));
7285
+ const taskPrs = filterGoalTaskPrs(
7286
+ allPrs.value ?? [],
7287
+ rawIssues.map((i) => i.number)
7288
+ );
7199
7289
  goal.openTaskPrs = taskPrs;
7200
7290
  goal.leafPr = pickLeafPr(taskPrs);
7201
7291
  goal.childTasks = pairIssuesWithPrs(rawIssues, taskPrs);
@@ -7252,13 +7342,13 @@ var deriveQaScopeFromIssue = async (ctx) => {
7252
7342
  };
7253
7343
 
7254
7344
  // src/scripts/diagMcp.ts
7255
- import { execFileSync as execFileSync11 } from "child_process";
7345
+ import { execFileSync as execFileSync12 } from "child_process";
7256
7346
  import * as fs27 from "fs";
7257
7347
  import * as os4 from "os";
7258
- import * as path25 from "path";
7348
+ import * as path24 from "path";
7259
7349
  var diagMcp = async (_ctx) => {
7260
7350
  const home = os4.homedir();
7261
- const cacheDir = path25.join(home, ".cache", "ms-playwright");
7351
+ const cacheDir = path24.join(home, ".cache", "ms-playwright");
7262
7352
  let entries = [];
7263
7353
  try {
7264
7354
  entries = fs27.readdirSync(cacheDir);
@@ -7272,7 +7362,7 @@ var diagMcp = async (_ctx) => {
7272
7362
  process.stderr.write(`[kody diag] chromium present: ${hasChromium ? "yes" : "no"}
7273
7363
  `);
7274
7364
  try {
7275
- const v = execFileSync11("npx", ["-y", "--package=@playwright/mcp@latest", "--", "playwright-mcp", "--version"], {
7365
+ const v = execFileSync12("npx", ["-y", "--package=@playwright/mcp@latest", "--", "playwright-mcp", "--version"], {
7276
7366
  stdio: "pipe",
7277
7367
  timeout: 6e4,
7278
7368
  encoding: "utf8"
@@ -7288,16 +7378,16 @@ var diagMcp = async (_ctx) => {
7288
7378
 
7289
7379
  // src/scripts/discoverQaContext.ts
7290
7380
  import * as fs29 from "fs";
7291
- import * as path27 from "path";
7381
+ import * as path26 from "path";
7292
7382
 
7293
7383
  // src/scripts/frameworkDetectors.ts
7294
7384
  import * as fs28 from "fs";
7295
- import * as path26 from "path";
7385
+ import * as path25 from "path";
7296
7386
  function detectFrameworks(cwd) {
7297
7387
  const out = [];
7298
7388
  let deps = {};
7299
7389
  try {
7300
- const pkg = JSON.parse(fs28.readFileSync(path26.join(cwd, "package.json"), "utf-8"));
7390
+ const pkg = JSON.parse(fs28.readFileSync(path25.join(cwd, "package.json"), "utf-8"));
7301
7391
  deps = { ...pkg.dependencies, ...pkg.devDependencies };
7302
7392
  } catch {
7303
7393
  return out;
@@ -7334,7 +7424,7 @@ function detectFrameworks(cwd) {
7334
7424
  }
7335
7425
  function findFile(cwd, candidates) {
7336
7426
  for (const c of candidates) {
7337
- if (fs28.existsSync(path26.join(cwd, c))) return c;
7427
+ if (fs28.existsSync(path25.join(cwd, c))) return c;
7338
7428
  }
7339
7429
  return null;
7340
7430
  }
@@ -7347,7 +7437,7 @@ var COLLECTION_DIRS = [
7347
7437
  function discoverPayloadCollections(cwd) {
7348
7438
  const out = [];
7349
7439
  for (const dir of COLLECTION_DIRS) {
7350
- const full = path26.join(cwd, dir);
7440
+ const full = path25.join(cwd, dir);
7351
7441
  if (!fs28.existsSync(full)) continue;
7352
7442
  let files;
7353
7443
  try {
@@ -7357,7 +7447,7 @@ function discoverPayloadCollections(cwd) {
7357
7447
  }
7358
7448
  for (const file of files) {
7359
7449
  try {
7360
- const filePath = path26.join(full, file);
7450
+ const filePath = path25.join(full, file);
7361
7451
  const content = fs28.readFileSync(filePath, "utf-8").slice(0, 1e4);
7362
7452
  const slugMatch = content.match(/slug:\s*['"]([a-z0-9-]+)['"]/);
7363
7453
  if (!slugMatch) continue;
@@ -7372,7 +7462,7 @@ function discoverPayloadCollections(cwd) {
7372
7462
  out.push({
7373
7463
  name,
7374
7464
  slug,
7375
- filePath: path26.relative(cwd, filePath),
7465
+ filePath: path25.relative(cwd, filePath),
7376
7466
  fields: fields.slice(0, 20),
7377
7467
  hasAdmin
7378
7468
  });
@@ -7386,7 +7476,7 @@ var ADMIN_COMPONENT_DIRS = ["src/ui/admin", "src/admin/components", "src/compone
7386
7476
  function discoverAdminComponents(cwd, collections) {
7387
7477
  const out = [];
7388
7478
  for (const dir of ADMIN_COMPONENT_DIRS) {
7389
- const full = path26.join(cwd, dir);
7479
+ const full = path25.join(cwd, dir);
7390
7480
  if (!fs28.existsSync(full)) continue;
7391
7481
  let entries;
7392
7482
  try {
@@ -7395,19 +7485,19 @@ function discoverAdminComponents(cwd, collections) {
7395
7485
  continue;
7396
7486
  }
7397
7487
  for (const entry of entries) {
7398
- const entryPath = path26.join(full, entry.name);
7488
+ const entryPath = path25.join(full, entry.name);
7399
7489
  let name;
7400
7490
  let filePath;
7401
7491
  if (entry.isDirectory()) {
7402
7492
  const indexFile = ["index.tsx", "index.ts", "index.jsx", "index.js"].find(
7403
- (f) => fs28.existsSync(path26.join(entryPath, f))
7493
+ (f) => fs28.existsSync(path25.join(entryPath, f))
7404
7494
  );
7405
7495
  if (!indexFile) continue;
7406
7496
  name = entry.name;
7407
- filePath = path26.relative(cwd, path26.join(entryPath, indexFile));
7497
+ filePath = path25.relative(cwd, path25.join(entryPath, indexFile));
7408
7498
  } else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
7409
7499
  name = entry.name.replace(/\.(tsx?|jsx?)$/, "");
7410
- filePath = path26.relative(cwd, entryPath);
7500
+ filePath = path25.relative(cwd, entryPath);
7411
7501
  } else {
7412
7502
  continue;
7413
7503
  }
@@ -7415,7 +7505,7 @@ function discoverAdminComponents(cwd, collections) {
7415
7505
  if (collections) {
7416
7506
  for (const col of collections) {
7417
7507
  try {
7418
- const colContent = fs28.readFileSync(path26.join(cwd, col.filePath), "utf-8");
7508
+ const colContent = fs28.readFileSync(path25.join(cwd, col.filePath), "utf-8");
7419
7509
  if (colContent.includes(name)) {
7420
7510
  usedInCollection = col.slug;
7421
7511
  break;
@@ -7434,7 +7524,7 @@ function scanApiRoutes(cwd) {
7434
7524
  const out = [];
7435
7525
  const appDirs = ["src/app", "app"];
7436
7526
  for (const appDir of appDirs) {
7437
- const apiDir = path26.join(cwd, appDir, "api");
7527
+ const apiDir = path25.join(cwd, appDir, "api");
7438
7528
  if (!fs28.existsSync(apiDir)) continue;
7439
7529
  walkApiRoutes(apiDir, "/api", cwd, out);
7440
7530
  break;
@@ -7451,7 +7541,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
7451
7541
  const routeFile = entries.find((e) => e.isFile() && /^route\.(ts|js|tsx|jsx)$/.test(e.name));
7452
7542
  if (routeFile) {
7453
7543
  try {
7454
- const content = fs28.readFileSync(path26.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
7544
+ const content = fs28.readFileSync(path25.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
7455
7545
  const methods = HTTP_METHODS.filter(
7456
7546
  (m) => new RegExp(`export\\s+(?:async\\s+)?function\\s+${m}\\b`).test(content)
7457
7547
  );
@@ -7459,7 +7549,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
7459
7549
  out.push({
7460
7550
  path: prefix,
7461
7551
  methods,
7462
- filePath: path26.relative(cwd, path26.join(dir, routeFile.name))
7552
+ filePath: path25.relative(cwd, path25.join(dir, routeFile.name))
7463
7553
  });
7464
7554
  }
7465
7555
  } catch {
@@ -7470,7 +7560,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
7470
7560
  if (entry.name === "node_modules" || entry.name === ".next") continue;
7471
7561
  let segment = entry.name;
7472
7562
  if (segment.startsWith("(") && segment.endsWith(")")) {
7473
- walkApiRoutes(path26.join(dir, entry.name), prefix, cwd, out);
7563
+ walkApiRoutes(path25.join(dir, entry.name), prefix, cwd, out);
7474
7564
  continue;
7475
7565
  }
7476
7566
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -7478,7 +7568,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
7478
7568
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
7479
7569
  segment = `:${segment.slice(1, -1)}`;
7480
7570
  }
7481
- walkApiRoutes(path26.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
7571
+ walkApiRoutes(path25.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
7482
7572
  }
7483
7573
  }
7484
7574
  var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
@@ -7498,7 +7588,7 @@ var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
7498
7588
  function scanEnvVars(cwd) {
7499
7589
  const candidates = [".env.example", ".env.local.example", ".env.template"];
7500
7590
  for (const envFile of candidates) {
7501
- const envPath = path26.join(cwd, envFile);
7591
+ const envPath = path25.join(cwd, envFile);
7502
7592
  if (!fs28.existsSync(envPath)) continue;
7503
7593
  try {
7504
7594
  const content = fs28.readFileSync(envPath, "utf-8");
@@ -7549,9 +7639,9 @@ function runQaDiscovery(cwd) {
7549
7639
  }
7550
7640
  function detectDevServer(cwd, out) {
7551
7641
  try {
7552
- const pkg = JSON.parse(fs29.readFileSync(path27.join(cwd, "package.json"), "utf-8"));
7642
+ const pkg = JSON.parse(fs29.readFileSync(path26.join(cwd, "package.json"), "utf-8"));
7553
7643
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
7554
- const pm = fs29.existsSync(path27.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs29.existsSync(path27.join(cwd, "yarn.lock")) ? "yarn" : fs29.existsSync(path27.join(cwd, "bun.lockb")) ? "bun" : "npm";
7644
+ const pm = fs29.existsSync(path26.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs29.existsSync(path26.join(cwd, "yarn.lock")) ? "yarn" : fs29.existsSync(path26.join(cwd, "bun.lockb")) ? "bun" : "npm";
7555
7645
  if (pkg.scripts?.dev) out.devCommand = `${pm} dev`;
7556
7646
  if (allDeps.next || allDeps.nuxt) out.devPort = 3e3;
7557
7647
  else if (allDeps.vite) out.devPort = 5173;
@@ -7561,7 +7651,7 @@ function detectDevServer(cwd, out) {
7561
7651
  function scanFrontendRoutes(cwd, out) {
7562
7652
  const appDirs = ["src/app", "app"];
7563
7653
  for (const appDir of appDirs) {
7564
- const full = path27.join(cwd, appDir);
7654
+ const full = path26.join(cwd, appDir);
7565
7655
  if (!fs29.existsSync(full)) continue;
7566
7656
  walkFrontendRoutes(full, "", out);
7567
7657
  break;
@@ -7587,7 +7677,7 @@ function walkFrontendRoutes(dir, prefix, out) {
7587
7677
  if (entry.name === "node_modules" || entry.name === ".next") continue;
7588
7678
  let segment = entry.name;
7589
7679
  if (segment.startsWith("(") && segment.endsWith(")")) {
7590
- walkFrontendRoutes(path27.join(dir, entry.name), prefix, out);
7680
+ walkFrontendRoutes(path26.join(dir, entry.name), prefix, out);
7591
7681
  continue;
7592
7682
  }
7593
7683
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -7595,7 +7685,7 @@ function walkFrontendRoutes(dir, prefix, out) {
7595
7685
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
7596
7686
  segment = `:${segment.slice(1, -1)}`;
7597
7687
  }
7598
- walkFrontendRoutes(path27.join(dir, entry.name), `${prefix}/${segment}`, out);
7688
+ walkFrontendRoutes(path26.join(dir, entry.name), `${prefix}/${segment}`, out);
7599
7689
  }
7600
7690
  }
7601
7691
  function detectAuthFiles(cwd, out) {
@@ -7612,13 +7702,13 @@ function detectAuthFiles(cwd, out) {
7612
7702
  "src/app/api/oauth"
7613
7703
  ];
7614
7704
  for (const c of candidates) {
7615
- if (fs29.existsSync(path27.join(cwd, c))) out.authFiles.push(c);
7705
+ if (fs29.existsSync(path26.join(cwd, c))) out.authFiles.push(c);
7616
7706
  }
7617
7707
  }
7618
7708
  function detectRoles(cwd, out) {
7619
7709
  const rolePaths = ["src/types", "src/lib", "src/utils", "src/constants", "src/access", "src/collections"];
7620
7710
  for (const rp of rolePaths) {
7621
- const dir = path27.join(cwd, rp);
7711
+ const dir = path26.join(cwd, rp);
7622
7712
  if (!fs29.existsSync(dir)) continue;
7623
7713
  let files;
7624
7714
  try {
@@ -7628,7 +7718,7 @@ function detectRoles(cwd, out) {
7628
7718
  }
7629
7719
  for (const f of files) {
7630
7720
  try {
7631
- const content = fs29.readFileSync(path27.join(dir, f), "utf-8").slice(0, 5e3);
7721
+ const content = fs29.readFileSync(path26.join(dir, f), "utf-8").slice(0, 5e3);
7632
7722
  const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
7633
7723
  if (roleMatches) {
7634
7724
  for (const m of roleMatches) {
@@ -7701,7 +7791,8 @@ Required env vars: ${d.envVars.join(", ")}`);
7701
7791
  let result = sections.join("\n");
7702
7792
  if (result.length > MAX_SERIALIZED_LENGTH) {
7703
7793
  const cutoff = result.lastIndexOf("\n", MAX_SERIALIZED_LENGTH - 20);
7704
- result = result.slice(0, cutoff > 0 ? cutoff : MAX_SERIALIZED_LENGTH - 20) + "\n... (truncated)";
7794
+ result = `${result.slice(0, cutoff > 0 ? cutoff : MAX_SERIALIZED_LENGTH - 20)}
7795
+ ... (truncated)`;
7705
7796
  }
7706
7797
  return result;
7707
7798
  }
@@ -7756,7 +7847,7 @@ function parsePr(url) {
7756
7847
  }
7757
7848
 
7758
7849
  // src/scripts/dispatchClassified.ts
7759
- import { execFileSync as execFileSync12 } from "child_process";
7850
+ import { execFileSync as execFileSync13 } from "child_process";
7760
7851
  var API_TIMEOUT_MS4 = 3e4;
7761
7852
  var VALID_CLASSES2 = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
7762
7853
  var dispatchClassified = async (ctx) => {
@@ -7779,14 +7870,18 @@ ${stateBody}`;
7779
7870
  try {
7780
7871
  const existing = findStateComment("issue", issueNumber, ctx.cwd);
7781
7872
  if (existing) {
7782
- execFileSync12("gh", ["api", `repos/{owner}/{repo}/issues/comments/${existing.id}`, "-X", "PATCH", "-F", "body=@-"], {
7783
- cwd: ctx.cwd,
7784
- timeout: API_TIMEOUT_MS4,
7785
- input: body,
7786
- stdio: ["pipe", "pipe", "pipe"]
7787
- });
7873
+ execFileSync13(
7874
+ "gh",
7875
+ ["api", `repos/{owner}/{repo}/issues/comments/${existing.id}`, "-X", "PATCH", "-F", "body=@-"],
7876
+ {
7877
+ cwd: ctx.cwd,
7878
+ timeout: API_TIMEOUT_MS4,
7879
+ input: body,
7880
+ stdio: ["pipe", "pipe", "pipe"]
7881
+ }
7882
+ );
7788
7883
  } else {
7789
- execFileSync12("gh", ["issue", "comment", String(issueNumber), "--body", body], {
7884
+ execFileSync13("gh", ["issue", "comment", String(issueNumber), "--body", body], {
7790
7885
  cwd: ctx.cwd,
7791
7886
  timeout: API_TIMEOUT_MS4,
7792
7887
  stdio: ["ignore", "pipe", "pipe"]
@@ -7807,7 +7902,7 @@ ${stateBody}`;
7807
7902
 
7808
7903
  // src/scripts/dispatchJobFileTicks.ts
7809
7904
  import * as fs31 from "fs";
7810
- import * as path29 from "path";
7905
+ import * as path28 from "path";
7811
7906
 
7812
7907
  // src/scripts/jobFrontmatter.ts
7813
7908
  var SCHEDULE_EVERY_VALUES = [
@@ -7823,7 +7918,7 @@ var SCHEDULE_EVERY_VALUES = [
7823
7918
  "manual"
7824
7919
  ];
7825
7920
  var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
7826
- function splitFrontmatter2(raw) {
7921
+ function splitFrontmatter(raw) {
7827
7922
  const match = FRONTMATTER_RE.exec(raw);
7828
7923
  if (!match) return { frontmatter: {}, body: raw };
7829
7924
  const inner = match[1] ?? "";
@@ -8061,7 +8156,8 @@ var ContentsApiBackend = class {
8061
8156
  return false;
8062
8157
  }
8063
8158
  const slug = slugFromStateFilePath(loaded.path);
8064
- const body = JSON.stringify(next, null, 2) + "\n";
8159
+ const body = `${JSON.stringify(next, null, 2)}
8160
+ `;
8065
8161
  const payload = {
8066
8162
  message: `chore(jobs): update state for ${slug} (rev ${next.rev})`,
8067
8163
  content: Buffer.from(body, "utf-8").toString("base64"),
@@ -8100,7 +8196,7 @@ function isShaConflict(err) {
8100
8196
 
8101
8197
  // src/scripts/jobState/localFileBackend.ts
8102
8198
  import * as fs30 from "fs";
8103
- import * as path28 from "path";
8199
+ import * as path27 from "path";
8104
8200
  var LocalFileBackend = class {
8105
8201
  name = "local-file";
8106
8202
  cwd;
@@ -8115,7 +8211,7 @@ var LocalFileBackend = class {
8115
8211
  if (!opts.owner || !opts.repo) throw new Error("LocalFileBackend: owner and repo are required");
8116
8212
  this.cwd = opts.cwd;
8117
8213
  this.jobsDir = opts.jobsDir;
8118
- this.absDir = path28.join(opts.cwd, opts.jobsDir);
8214
+ this.absDir = path27.join(opts.cwd, opts.jobsDir);
8119
8215
  this.owner = opts.owner;
8120
8216
  this.repo = opts.repo;
8121
8217
  this.cache = opts.cache ?? defaultCacheAdapter();
@@ -8175,7 +8271,7 @@ var LocalFileBackend = class {
8175
8271
  }
8176
8272
  load(slug) {
8177
8273
  const relPath = stateFilePath(this.jobsDir, slug);
8178
- const absPath = path28.join(this.cwd, relPath);
8274
+ const absPath = path27.join(this.cwd, relPath);
8179
8275
  if (!fs30.existsSync(absPath)) {
8180
8276
  return { path: relPath, handle: null, state: initialStateEnvelope("seed"), created: true };
8181
8277
  }
@@ -8196,9 +8292,10 @@ var LocalFileBackend = class {
8196
8292
  if (!loaded.created && isStateUnchanged(loaded.state, next)) {
8197
8293
  return false;
8198
8294
  }
8199
- const absPath = path28.join(this.cwd, loaded.path);
8200
- fs30.mkdirSync(path28.dirname(absPath), { recursive: true });
8201
- const body = JSON.stringify(next, null, 2) + "\n";
8295
+ const absPath = path27.join(this.cwd, loaded.path);
8296
+ fs30.mkdirSync(path27.dirname(absPath), { recursive: true });
8297
+ const body = `${JSON.stringify(next, null, 2)}
8298
+ `;
8202
8299
  const tmpPath = `${absPath}.${process.pid}.tmp`;
8203
8300
  fs30.writeFileSync(tmpPath, body, "utf-8");
8204
8301
  fs30.renameSync(tmpPath, absPath);
@@ -8279,7 +8376,7 @@ var dispatchJobFileTicks = async (ctx, _profile, args) => {
8279
8376
  await backend.hydrate();
8280
8377
  }
8281
8378
  try {
8282
- const slugs = listJobSlugs(path29.join(ctx.cwd, jobsDir));
8379
+ const slugs = listJobSlugs(path28.join(ctx.cwd, jobsDir));
8283
8380
  ctx.data.jobSlugCount = slugs.length;
8284
8381
  if (slugs.length === 0) {
8285
8382
  process.stdout.write(`[jobs] no job files in ${jobsDir}
@@ -8299,10 +8396,8 @@ var dispatchJobFileTicks = async (ctx, _profile, args) => {
8299
8396
  continue;
8300
8397
  }
8301
8398
  if (!frontmatter.staff || frontmatter.staff.trim().length === 0) {
8302
- process.stderr.write(
8303
- `[jobs] \u23ED skip ${slug}: no staff assigned (add 'staff: <slug>' frontmatter)
8304
- `
8305
- );
8399
+ process.stderr.write(`[jobs] \u23ED skip ${slug}: no staff assigned (add 'staff: <slug>' frontmatter)
8400
+ `);
8306
8401
  results.push({ slug, exitCode: 0, skipped: true, reason: "no staff assigned" });
8307
8402
  continue;
8308
8403
  }
@@ -8392,8 +8487,8 @@ function formatAgo(ms) {
8392
8487
  }
8393
8488
  function readJobFrontmatter(cwd, jobsDir, slug) {
8394
8489
  try {
8395
- const raw = fs31.readFileSync(path29.join(cwd, jobsDir, `${slug}.md`), "utf-8");
8396
- return splitFrontmatter2(raw).frontmatter;
8490
+ const raw = fs31.readFileSync(path28.join(cwd, jobsDir, `${slug}.md`), "utf-8");
8491
+ return splitFrontmatter(raw).frontmatter;
8397
8492
  } catch {
8398
8493
  return {};
8399
8494
  }
@@ -8499,13 +8594,10 @@ var dispatchNextTask = async (ctx) => {
8499
8594
 
8500
8595
  // src/pr.ts
8501
8596
  init_issue();
8502
- import { execFileSync as execFileSync13 } from "child_process";
8597
+ import { execFileSync as execFileSync14 } from "child_process";
8503
8598
  function prMergeStatus(prNumber, cwd) {
8504
8599
  try {
8505
- const out = gh(
8506
- ["pr", "view", String(prNumber), "--json", "mergeable,mergeStateStatus"],
8507
- { cwd }
8508
- );
8600
+ const out = gh(["pr", "view", String(prNumber), "--json", "mergeable,mergeStateStatus"], { cwd });
8509
8601
  const parsed = JSON.parse(out);
8510
8602
  const mergeable = parsed.mergeable ?? "UNKNOWN";
8511
8603
  const mergeStateStatus = parsed.mergeStateStatus ?? "UNKNOWN";
@@ -8625,7 +8717,7 @@ function isAlreadyExistsError(err) {
8625
8717
  return ALREADY_EXISTS_RE.test(msg);
8626
8718
  }
8627
8719
  function git2(args, cwd) {
8628
- return execFileSync13("git", args, {
8720
+ return execFileSync14("git", args, {
8629
8721
  encoding: "utf-8",
8630
8722
  timeout: 3e4,
8631
8723
  cwd,
@@ -8890,8 +8982,10 @@ var finalizeGoal = async (ctx) => {
8890
8982
  );
8891
8983
  continue;
8892
8984
  }
8893
- process.stdout.write(`[goal-tick] closing task issue #${t.number} (goal finalized \u2014 carried by PR #${leaf.number})
8894
- `);
8985
+ process.stdout.write(
8986
+ `[goal-tick] closing task issue #${t.number} (goal finalized \u2014 carried by PR #${leaf.number})
8987
+ `
8988
+ );
8895
8989
  const closed = closeIssue(
8896
8990
  t.number,
8897
8991
  {
@@ -8956,7 +9050,7 @@ var finalizeTerminal = async (ctx) => {
8956
9050
 
8957
9051
  // src/scripts/finishFlow.ts
8958
9052
  init_issue();
8959
- import { execFileSync as execFileSync14 } from "child_process";
9053
+ import { execFileSync as execFileSync15 } from "child_process";
8960
9054
  var TERMINAL_PHASE = {
8961
9055
  "review-passed": { phase: "shipped", status: "succeeded" },
8962
9056
  "fix-applied": { phase: "shipped", status: "succeeded" },
@@ -8978,7 +9072,7 @@ var finishFlow = async (ctx, profile, _agentResult, args) => {
8978
9072
  if (state) state.flow = void 0;
8979
9073
  if (!issueNumber) return;
8980
9074
  const label = typeof args?.label === "string" ? args.label : void 0;
8981
- if (label && label.startsWith(KODY_NAMESPACE)) {
9075
+ if (label?.startsWith(KODY_NAMESPACE)) {
8982
9076
  const spec = {
8983
9077
  label,
8984
9078
  color: typeof args?.color === "string" ? args.color : void 0,
@@ -8996,7 +9090,7 @@ var finishFlow = async (ctx, profile, _agentResult, args) => {
8996
9090
  **PR:** ${state.core.prUrl}` : "";
8997
9091
  const body = `${icon} kody flow \`${flowName}\` finished \u2014 \`${reason}\`${prSuffix}`;
8998
9092
  try {
8999
- execFileSync14("gh", ["issue", "comment", String(issueNumber), "--body", body], {
9093
+ execFileSync15("gh", ["issue", "comment", String(issueNumber), "--body", body], {
9000
9094
  timeout: API_TIMEOUT_MS5,
9001
9095
  cwd: ctx.cwd,
9002
9096
  stdio: ["ignore", "pipe", "pipe"]
@@ -9026,9 +9120,9 @@ var finishFlow = async (ctx, profile, _agentResult, args) => {
9026
9120
  };
9027
9121
 
9028
9122
  // src/branch.ts
9029
- import { execFileSync as execFileSync15 } from "child_process";
9123
+ import { execFileSync as execFileSync16 } from "child_process";
9030
9124
  function git3(args, cwd) {
9031
- return execFileSync15("git", args, {
9125
+ return execFileSync16("git", args, {
9032
9126
  encoding: "utf-8",
9033
9127
  timeout: 3e4,
9034
9128
  cwd,
@@ -9045,11 +9139,11 @@ function getCurrentBranch(cwd) {
9045
9139
  }
9046
9140
  function resetWorkingTree2(cwd) {
9047
9141
  try {
9048
- execFileSync15("git", ["reset", "--hard", "HEAD"], { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
9142
+ execFileSync16("git", ["reset", "--hard", "HEAD"], { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
9049
9143
  } catch {
9050
9144
  }
9051
9145
  try {
9052
- execFileSync15("git", ["clean", "-fd", "-e", ".kody"], { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
9146
+ execFileSync16("git", ["clean", "-fd", "-e", ".kody"], { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
9053
9147
  } catch {
9054
9148
  }
9055
9149
  }
@@ -9061,14 +9155,19 @@ function checkoutPrBranch(prNumber, cwd) {
9061
9155
  GH_TOKEN: process.env.GH_PAT?.trim() || process.env.GH_TOKEN || ""
9062
9156
  };
9063
9157
  try {
9064
- execFileSync15("git", ["reset", "--hard", "HEAD"], { cwd, env, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
9158
+ execFileSync16("git", ["reset", "--hard", "HEAD"], { cwd, env, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
9065
9159
  } catch {
9066
9160
  }
9067
9161
  try {
9068
- execFileSync15("git", ["clean", "-fd", "-e", ".kody"], { cwd, env, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
9162
+ execFileSync16("git", ["clean", "-fd", "-e", ".kody"], {
9163
+ cwd,
9164
+ env,
9165
+ stdio: ["ignore", "pipe", "pipe"],
9166
+ timeout: 3e4
9167
+ });
9069
9168
  } catch {
9070
9169
  }
9071
- execFileSync15("gh", ["pr", "checkout", String(prNumber)], {
9170
+ execFileSync16("gh", ["pr", "checkout", String(prNumber)], {
9072
9171
  cwd,
9073
9172
  env,
9074
9173
  stdio: ["ignore", "pipe", "pipe"],
@@ -9101,7 +9200,11 @@ function mergeBase(baseBranch, cwd) {
9101
9200
  }
9102
9201
  function restoreKodyAssets(cwd) {
9103
9202
  try {
9104
- execFileSync15("git", ["checkout", "HEAD", "--", ".kody"], { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
9203
+ execFileSync16("git", ["checkout", "HEAD", "--", ".kody"], {
9204
+ cwd,
9205
+ stdio: ["ignore", "pipe", "pipe"],
9206
+ timeout: 3e4
9207
+ });
9105
9208
  } catch {
9106
9209
  }
9107
9210
  }
@@ -9205,68 +9308,6 @@ function ensureFeatureBranchInner(issueNumber, title, defaultBranch2, cwd, baseB
9205
9308
  return { branch: branchName, created: true };
9206
9309
  }
9207
9310
 
9208
- // src/gha.ts
9209
- import { execFileSync as execFileSync16 } from "child_process";
9210
- import * as fs32 from "fs";
9211
- function getRunUrl() {
9212
- const server = process.env.GITHUB_SERVER_URL;
9213
- const repo = process.env.GITHUB_REPOSITORY;
9214
- const runId = process.env.GITHUB_RUN_ID;
9215
- if (!server || !repo || !runId) return "";
9216
- return `${server}/${repo}/actions/runs/${runId}`;
9217
- }
9218
- function reactToTriggerComment(cwd) {
9219
- if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
9220
- const eventPath = process.env.GITHUB_EVENT_PATH;
9221
- if (!eventPath || !fs32.existsSync(eventPath)) return;
9222
- let event = null;
9223
- try {
9224
- event = JSON.parse(fs32.readFileSync(eventPath, "utf-8"));
9225
- } catch {
9226
- return;
9227
- }
9228
- const commentId = event?.comment?.id;
9229
- const repo = process.env.GITHUB_REPOSITORY;
9230
- if (!commentId || !repo) return;
9231
- const token = process.env.KODY_TOKEN?.trim() || process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
9232
- const args = [
9233
- "api",
9234
- "-X",
9235
- "POST",
9236
- "-H",
9237
- "Accept: application/vnd.github+json",
9238
- `/repos/${repo}/issues/comments/${commentId}/reactions`,
9239
- "-f",
9240
- "content=eyes"
9241
- ];
9242
- const opts = {
9243
- cwd,
9244
- env: { ...process.env, GH_TOKEN: token ?? process.env.GH_TOKEN ?? "" },
9245
- stdio: "pipe",
9246
- timeout: 15e3
9247
- };
9248
- let lastErr = null;
9249
- for (let attempt = 0; attempt < 3; attempt++) {
9250
- if (attempt > 0) sleepMs(attempt === 1 ? 500 : 1500);
9251
- try {
9252
- execFileSync16("gh", args, opts);
9253
- return;
9254
- } catch (err) {
9255
- lastErr = err;
9256
- }
9257
- }
9258
- process.stderr.write(
9259
- `[kody] \u{1F440} reaction failed after 3 attempts on comment ${commentId}: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}
9260
- `
9261
- );
9262
- }
9263
- function sleepMs(ms) {
9264
- try {
9265
- execFileSync16("sleep", [(ms / 1e3).toString()], { stdio: "ignore", timeout: ms + 1e3 });
9266
- } catch {
9267
- }
9268
- }
9269
-
9270
9311
  // src/scripts/fixCiFlow.ts
9271
9312
  init_issue();
9272
9313
 
@@ -9515,12 +9556,12 @@ var handleAbandonedGoal = async (ctx) => {
9515
9556
 
9516
9557
  // src/scripts/initFlow.ts
9517
9558
  import { execFileSync as execFileSync18 } from "child_process";
9518
- import * as fs33 from "fs";
9519
- import * as path30 from "path";
9559
+ import * as fs32 from "fs";
9560
+ import * as path29 from "path";
9520
9561
  function detectPackageManager(cwd) {
9521
- if (fs33.existsSync(path30.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
9522
- if (fs33.existsSync(path30.join(cwd, "yarn.lock"))) return "yarn";
9523
- if (fs33.existsSync(path30.join(cwd, "bun.lockb"))) return "bun";
9562
+ if (fs32.existsSync(path29.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
9563
+ if (fs32.existsSync(path29.join(cwd, "yarn.lock"))) return "yarn";
9564
+ if (fs32.existsSync(path29.join(cwd, "bun.lockb"))) return "bun";
9524
9565
  return "npm";
9525
9566
  }
9526
9567
  function qualityCommandsFor(pm) {
@@ -9649,36 +9690,36 @@ function performInit(cwd, force) {
9649
9690
  const pm = detectPackageManager(cwd);
9650
9691
  const ownerRepo = detectOwnerRepo(cwd);
9651
9692
  const defaultBranch2 = defaultBranchFromGit(cwd);
9652
- const configPath = path30.join(cwd, "kody.config.json");
9653
- if (fs33.existsSync(configPath) && !force) {
9693
+ const configPath = path29.join(cwd, "kody.config.json");
9694
+ if (fs32.existsSync(configPath) && !force) {
9654
9695
  skipped.push("kody.config.json");
9655
9696
  } else {
9656
9697
  const cfg = makeConfig(pm, ownerRepo, defaultBranch2);
9657
- fs33.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
9698
+ fs32.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
9658
9699
  `);
9659
9700
  wrote.push("kody.config.json");
9660
9701
  }
9661
- const workflowDir = path30.join(cwd, ".github", "workflows");
9662
- const workflowPath = path30.join(workflowDir, "kody.yml");
9663
- if (fs33.existsSync(workflowPath) && !force) {
9702
+ const workflowDir = path29.join(cwd, ".github", "workflows");
9703
+ const workflowPath = path29.join(workflowDir, "kody.yml");
9704
+ if (fs32.existsSync(workflowPath) && !force) {
9664
9705
  skipped.push(".github/workflows/kody.yml");
9665
9706
  } else {
9666
- fs33.mkdirSync(workflowDir, { recursive: true });
9667
- fs33.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
9707
+ fs32.mkdirSync(workflowDir, { recursive: true });
9708
+ fs32.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
9668
9709
  wrote.push(".github/workflows/kody.yml");
9669
9710
  }
9670
9711
  const builtinJobs = listBuiltinJobs();
9671
9712
  if (builtinJobs.length > 0) {
9672
- const jobsDir = path30.join(cwd, ".kody", "duties");
9673
- fs33.mkdirSync(jobsDir, { recursive: true });
9713
+ const jobsDir = path29.join(cwd, ".kody", "duties");
9714
+ fs32.mkdirSync(jobsDir, { recursive: true });
9674
9715
  for (const job of builtinJobs) {
9675
- const rel = path30.join(".kody", "duties", `${job.slug}.md`);
9676
- const target = path30.join(cwd, rel);
9677
- if (fs33.existsSync(target) && !force) {
9716
+ const rel = path29.join(".kody", "duties", `${job.slug}.md`);
9717
+ const target = path29.join(cwd, rel);
9718
+ if (fs32.existsSync(target) && !force) {
9678
9719
  skipped.push(rel);
9679
9720
  continue;
9680
9721
  }
9681
- fs33.writeFileSync(target, fs33.readFileSync(job.filePath, "utf-8"));
9722
+ fs32.writeFileSync(target, fs32.readFileSync(job.filePath, "utf-8"));
9682
9723
  wrote.push(rel);
9683
9724
  }
9684
9725
  }
@@ -9690,12 +9731,12 @@ function performInit(cwd, force) {
9690
9731
  continue;
9691
9732
  }
9692
9733
  if (profile.kind !== "scheduled" || !profile.schedule) continue;
9693
- const target = path30.join(workflowDir, `kody-${exe.name}.yml`);
9694
- if (fs33.existsSync(target) && !force) {
9734
+ const target = path29.join(workflowDir, `kody-${exe.name}.yml`);
9735
+ if (fs32.existsSync(target) && !force) {
9695
9736
  skipped.push(`.github/workflows/kody-${exe.name}.yml`);
9696
9737
  continue;
9697
9738
  }
9698
- fs33.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
9739
+ fs32.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
9699
9740
  wrote.push(`.github/workflows/kody-${exe.name}.yml`);
9700
9741
  }
9701
9742
  let labels;
@@ -9880,8 +9921,8 @@ var loadIssueStateComment = async (ctx, _profile, args) => {
9880
9921
 
9881
9922
  // src/scripts/loadJobFromFile.ts
9882
9923
  init_dutyMcp();
9883
- import * as fs34 from "fs";
9884
- import * as path31 from "path";
9924
+ import * as fs33 from "fs";
9925
+ import * as path30 from "path";
9885
9926
  var DUTY_TOOL_PALETTE = new Set(DUTY_MCP_TOOL_NAMES);
9886
9927
  var loadJobFromFile = async (ctx, profile, args) => {
9887
9928
  const jobsDir = String(args?.jobsDir ?? ".kody/duties");
@@ -9891,25 +9932,23 @@ var loadJobFromFile = async (ctx, profile, args) => {
9891
9932
  if (!slug) {
9892
9933
  throw new Error(`loadJobFromFile: ctx.args.${slugArg} must be a non-empty slug`);
9893
9934
  }
9894
- const absPath = path31.join(ctx.cwd, jobsDir, `${slug}.md`);
9895
- if (!fs34.existsSync(absPath)) {
9935
+ const absPath = path30.join(ctx.cwd, jobsDir, `${slug}.md`);
9936
+ if (!fs33.existsSync(absPath)) {
9896
9937
  throw new Error(`loadJobFromFile: job file not found: ${absPath}`);
9897
9938
  }
9898
- const raw = fs34.readFileSync(absPath, "utf-8");
9939
+ const raw = fs33.readFileSync(absPath, "utf-8");
9899
9940
  const { title, body } = parseJobFile(raw, slug);
9900
- const frontmatter = splitFrontmatter2(raw).frontmatter;
9941
+ const frontmatter = splitFrontmatter(raw).frontmatter;
9901
9942
  const mentions = (frontmatter.mentions ?? []).map((login) => `@${login}`).join(" ");
9902
9943
  const workerSlug = (frontmatter.staff ?? "").trim();
9903
9944
  let workerTitle = "";
9904
9945
  let workerPersona = "";
9905
9946
  if (workerSlug) {
9906
- const workerPath = path31.join(ctx.cwd, workersDir, `${workerSlug}.md`);
9907
- if (!fs34.existsSync(workerPath)) {
9908
- throw new Error(
9909
- `loadJobFromFile: duty '${slug}' declares staff '${workerSlug}' but ${workerPath} does not exist`
9910
- );
9947
+ const workerPath = path30.join(ctx.cwd, workersDir, `${workerSlug}.md`);
9948
+ if (!fs33.existsSync(workerPath)) {
9949
+ throw new Error(`loadJobFromFile: duty '${slug}' declares staff '${workerSlug}' but ${workerPath} does not exist`);
9911
9950
  }
9912
- const workerRaw = fs34.readFileSync(workerPath, "utf-8");
9951
+ const workerRaw = fs33.readFileSync(workerPath, "utf-8");
9913
9952
  const parsed = parseJobFile(workerRaw, workerSlug);
9914
9953
  workerTitle = parsed.title;
9915
9954
  workerPersona = parsed.body;
@@ -9993,18 +10032,18 @@ init_loadMemoryContext();
9993
10032
  init_loadPriorArt();
9994
10033
 
9995
10034
  // src/scripts/loadQaContext.ts
9996
- import * as fs36 from "fs";
9997
- import * as path33 from "path";
9998
-
9999
- // src/scripts/kodyVariables.ts
10000
10035
  import * as fs35 from "fs";
10001
10036
  import * as path32 from "path";
10037
+
10038
+ // src/scripts/kodyVariables.ts
10039
+ import * as fs34 from "fs";
10040
+ import * as path31 from "path";
10002
10041
  var KODY_VARIABLES_REL_PATH = ".kody/variables.json";
10003
10042
  function readKodyVariables(cwd) {
10004
- const full = path32.join(cwd, KODY_VARIABLES_REL_PATH);
10043
+ const full = path31.join(cwd, KODY_VARIABLES_REL_PATH);
10005
10044
  let raw;
10006
10045
  try {
10007
- raw = fs35.readFileSync(full, "utf-8");
10046
+ raw = fs34.readFileSync(full, "utf-8");
10008
10047
  } catch {
10009
10048
  return {};
10010
10049
  }
@@ -10029,7 +10068,9 @@ var LEGACY_AUDIENCE_TO_STAFF = { chat: "kody", qa: QA_STAFF };
10029
10068
  var FRONTMATTER_RE2 = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
10030
10069
  function parseSlugList(value) {
10031
10070
  const inner = value.startsWith("[") && value.endsWith("]") ? value.slice(1, -1) : value;
10032
- return inner.split(",").map((s) => s.trim().replace(/^["']|["']$/g, "").toLowerCase()).filter(Boolean);
10071
+ return inner.split(",").map(
10072
+ (s) => s.trim().replace(/^["']|["']$/g, "").toLowerCase()
10073
+ ).filter(Boolean);
10033
10074
  }
10034
10075
  function readProfileStaff(raw) {
10035
10076
  const m = FRONTMATTER_RE2.exec(raw);
@@ -10053,18 +10094,18 @@ function readProfileStaff(raw) {
10053
10094
  return { staff: staff ?? legacy ?? ["kody"], body };
10054
10095
  }
10055
10096
  function readProfile(cwd) {
10056
- const dir = path33.join(cwd, CONTEXT_DIR_REL_PATH);
10057
- if (!fs36.existsSync(dir)) return "";
10097
+ const dir = path32.join(cwd, CONTEXT_DIR_REL_PATH);
10098
+ if (!fs35.existsSync(dir)) return "";
10058
10099
  let entries;
10059
10100
  try {
10060
- entries = fs36.readdirSync(dir).filter((f) => f.endsWith(".md")).sort();
10101
+ entries = fs35.readdirSync(dir).filter((f) => f.endsWith(".md")).sort();
10061
10102
  } catch {
10062
10103
  return "";
10063
10104
  }
10064
10105
  const blocks = [];
10065
10106
  for (const file of entries) {
10066
10107
  try {
10067
- const raw = fs36.readFileSync(path33.join(dir, file), "utf-8");
10108
+ const raw = fs35.readFileSync(path32.join(dir, file), "utf-8");
10068
10109
  const { staff, body } = readProfileStaff(raw);
10069
10110
  if (!staff.includes(QA_STAFF) && !staff.includes(ALL_STAFF)) continue;
10070
10111
  blocks.push(`## ${file}
@@ -10101,8 +10142,8 @@ var loadQaContext = async (ctx) => {
10101
10142
  init_events();
10102
10143
 
10103
10144
  // src/taskContext.ts
10104
- import * as fs37 from "fs";
10105
- import * as path34 from "path";
10145
+ import * as fs36 from "fs";
10146
+ import * as path33 from "path";
10106
10147
  var TASK_CONTEXT_SCHEMA_VERSION = 1;
10107
10148
  function buildTaskContext(args) {
10108
10149
  return {
@@ -10118,10 +10159,10 @@ function buildTaskContext(args) {
10118
10159
  }
10119
10160
  function persistTaskContext(cwd, ctx) {
10120
10161
  try {
10121
- const dir = path34.join(cwd, ".kody", "runs", ctx.runId);
10122
- fs37.mkdirSync(dir, { recursive: true });
10123
- const file = path34.join(dir, "task-context.json");
10124
- fs37.writeFileSync(file, `${JSON.stringify(ctx, null, 2)}
10162
+ const dir = path33.join(cwd, ".kody", "runs", ctx.runId);
10163
+ fs36.mkdirSync(dir, { recursive: true });
10164
+ const file = path33.join(dir, "task-context.json");
10165
+ fs36.writeFileSync(file, `${JSON.stringify(ctx, null, 2)}
10125
10166
  `);
10126
10167
  return file;
10127
10168
  } catch (err) {
@@ -10187,19 +10228,19 @@ var loadTaskState = async (ctx) => {
10187
10228
  };
10188
10229
 
10189
10230
  // src/scripts/loadWorkerAdhoc.ts
10190
- import * as fs38 from "fs";
10191
- import * as path35 from "path";
10231
+ import * as fs37 from "fs";
10232
+ import * as path34 from "path";
10192
10233
  var loadWorkerAdhoc = async (ctx, _profile, args) => {
10193
10234
  const workersDir = String(args?.workersDir ?? ".kody/staff");
10194
10235
  const workerSlug = String(ctx.args.worker ?? "").trim();
10195
10236
  if (!workerSlug) {
10196
10237
  throw new Error("loadWorkerAdhoc: ctx.args.worker must be a non-empty slug");
10197
10238
  }
10198
- const workerPath = path35.join(ctx.cwd, workersDir, `${workerSlug}.md`);
10199
- if (!fs38.existsSync(workerPath)) {
10239
+ const workerPath = path34.join(ctx.cwd, workersDir, `${workerSlug}.md`);
10240
+ if (!fs37.existsSync(workerPath)) {
10200
10241
  throw new Error(`loadWorkerAdhoc: worker persona not found: ${workerPath}`);
10201
10242
  }
10202
- const { title, body } = parsePersona(fs38.readFileSync(workerPath, "utf-8"), workerSlug);
10243
+ const { title, body } = parsePersona(fs37.readFileSync(workerPath, "utf-8"), workerSlug);
10203
10244
  const message = resolveMessage(ctx.args.message);
10204
10245
  if (!message) {
10205
10246
  throw new Error(
@@ -10219,9 +10260,9 @@ function resolveMessage(messageArg) {
10219
10260
  }
10220
10261
  function readCommentBody() {
10221
10262
  const eventPath = process.env.GITHUB_EVENT_PATH;
10222
- if (!eventPath || !fs38.existsSync(eventPath)) return "";
10263
+ if (!eventPath || !fs37.existsSync(eventPath)) return "";
10223
10264
  try {
10224
- const event = JSON.parse(fs38.readFileSync(eventPath, "utf-8"));
10265
+ const event = JSON.parse(fs37.readFileSync(eventPath, "utf-8"));
10225
10266
  return String(event.comment?.body ?? "");
10226
10267
  } catch {
10227
10268
  return "";
@@ -10245,7 +10286,7 @@ function stripDirective(body) {
10245
10286
  return lines.slice(start).join("\n").trim();
10246
10287
  }
10247
10288
  function parsePersona(raw, slug) {
10248
- const stripped = splitFrontmatter2(raw).body;
10289
+ const stripped = splitFrontmatter(raw).body;
10249
10290
  const trimmed = stripped.trim();
10250
10291
  const firstLine2 = trimmed.split("\n", 1)[0] ?? "";
10251
10292
  const h1 = /^#\s+(.+?)\s*$/.exec(firstLine2);
@@ -10270,16 +10311,9 @@ var markFlowSuccess = async (ctx) => {
10270
10311
  // src/scripts/mergeFlow.ts
10271
10312
  init_issue();
10272
10313
  function readPr(prNumber, cwd) {
10273
- const out = gh(
10274
- [
10275
- "pr",
10276
- "view",
10277
- String(prNumber),
10278
- "--json",
10279
- "state,isDraft,mergeable,mergeStateStatus,title,url"
10280
- ],
10281
- { cwd }
10282
- );
10314
+ const out = gh(["pr", "view", String(prNumber), "--json", "state,isDraft,mergeable,mergeStateStatus,title,url"], {
10315
+ cwd
10316
+ });
10283
10317
  const p = JSON.parse(out);
10284
10318
  return {
10285
10319
  state: p.state ?? "UNKNOWN",
@@ -10356,11 +10390,7 @@ var mergeFlow = async (ctx) => {
10356
10390
  `);
10357
10391
  ctx.data.mergeAction = verdict.action;
10358
10392
  if (verdict.action === "MERGE_BLOCKED") {
10359
- commentOnIssue(
10360
- prNumber,
10361
- `\u{1F6A6} _Auto-merge held: ${verdict.reason} Kody will retry once the PR is CLEAN._`,
10362
- ctx.cwd
10363
- );
10393
+ commentOnIssue(prNumber, `\u{1F6A6} _Auto-merge held: ${verdict.reason} Kody will retry once the PR is CLEAN._`, ctx.cwd);
10364
10394
  }
10365
10395
  return;
10366
10396
  }
@@ -10766,10 +10796,7 @@ var parseReproOutput = async (ctx, _profile, agentResult) => {
10766
10796
  }
10767
10797
  }
10768
10798
  if (!signature) {
10769
- downgrade(
10770
- ctx,
10771
- "reproduce missing or malformed FAILURE_SIGNATURE JSON (must contain errorType + messageContains)"
10772
- );
10799
+ downgrade(ctx, "reproduce missing or malformed FAILURE_SIGNATURE JSON (must contain errorType + messageContains)");
10773
10800
  return;
10774
10801
  }
10775
10802
  ctx.data.reproTestPath = testPath;
@@ -10796,7 +10823,7 @@ function stripMarkdownEmphasis2(s) {
10796
10823
  }
10797
10824
  function downgrade(ctx, reason) {
10798
10825
  const action = ctx.data.action;
10799
- if (action && action.type.endsWith("_COMPLETED")) {
10826
+ if (action?.type.endsWith("_COMPLETED")) {
10800
10827
  ctx.data.action = {
10801
10828
  type: action.type.replace(/_COMPLETED$/, "_FAILED"),
10802
10829
  payload: { reason, downgradedFrom: action.type },
@@ -10855,6 +10882,102 @@ var persistFlowState = async (ctx) => {
10855
10882
  import { spawn as spawn4 } from "child_process";
10856
10883
  import { createServer as createServer2 } from "http";
10857
10884
 
10885
+ // src/github-health.ts
10886
+ var STATUS_URL = "https://www.githubstatus.com/api/v2/components.json";
10887
+ var STATUS_CACHE_TTL_MS = 3e4;
10888
+ var statusCache = null;
10889
+ async function probeActionsStatus(fetchImpl = fetch) {
10890
+ if (statusCache && statusCache.expiresAt > Date.now()) return statusCache.probe;
10891
+ try {
10892
+ const res = await fetchImpl(STATUS_URL, { headers: { "User-Agent": "kody-engine" } });
10893
+ if (!res.ok) return { degraded: false, label: `http_${res.status}` };
10894
+ const body = await res.json();
10895
+ const actions = (body.components ?? []).find((c) => (c.name ?? "").trim().toLowerCase() === "actions");
10896
+ const label = actions?.status ?? "unknown";
10897
+ const degraded = !!actions && label !== "operational";
10898
+ const probe = { degraded, label };
10899
+ statusCache = { probe, expiresAt: Date.now() + STATUS_CACHE_TTL_MS };
10900
+ return probe;
10901
+ } catch {
10902
+ return { degraded: false, label: "probe_error" };
10903
+ }
10904
+ }
10905
+ async function gitHubActionsDegraded(fetchImpl = fetch) {
10906
+ return (await probeActionsStatus(fetchImpl)).degraded;
10907
+ }
10908
+
10909
+ // src/pool/duty-fallback-tick.ts
10910
+ async function runDutyFallbackTick(deps) {
10911
+ if (!await deps.isDegraded()) {
10912
+ return { ran: false, claimed: 0 };
10913
+ }
10914
+ const repos = deps.activeRepos();
10915
+ if (repos.length === 0) {
10916
+ deps.log("GitHub Actions degraded but no active repo pools \u2014 nothing to tick");
10917
+ return { ran: true, claimed: 0 };
10918
+ }
10919
+ deps.log(`GitHub Actions degraded \u2014 running scheduled fan-out on Fly for ${repos.length} repo(s)`);
10920
+ const clock = deps.now ?? Date.now;
10921
+ let claimed = 0;
10922
+ for (const tag of repos) {
10923
+ const [owner, repo] = tag.split("/");
10924
+ if (!owner || !repo) continue;
10925
+ try {
10926
+ const res = await deps.claim(owner, repo, {
10927
+ jobId: `sched-${owner}-${repo}-${clock()}`,
10928
+ repo: tag,
10929
+ mode: "scheduled"
10930
+ });
10931
+ if (res.ok) {
10932
+ claimed++;
10933
+ deps.log(`[${tag}] scheduled fan-out claimed ${res.machineId}`);
10934
+ } else {
10935
+ deps.log(`[${tag}] scheduled fan-out skipped: ${res.reason ?? "pool unavailable"}`);
10936
+ }
10937
+ } catch (err) {
10938
+ deps.log(`[${tag}] scheduled fan-out error: ${err instanceof Error ? err.message : String(err)}`);
10939
+ }
10940
+ }
10941
+ return { ran: true, claimed };
10942
+ }
10943
+
10944
+ // src/pool/keys.ts
10945
+ import { hkdfSync } from "crypto";
10946
+ var POOL_API_KEY_INFO = "kody-pool-api:v1";
10947
+ var RUNNER_API_KEY_INFO = "kody-runner-api:v1";
10948
+ function masterKeyBytes(raw) {
10949
+ const v = raw.trim();
10950
+ if (!v) throw new Error("KODY_MASTER_KEY is empty");
10951
+ if (/^[0-9a-fA-F]+$/.test(v) && v.length === 64) {
10952
+ return Buffer.from(v, "hex");
10953
+ }
10954
+ return Buffer.from(v.replace(/-/g, "+").replace(/_/g, "/"), "base64");
10955
+ }
10956
+ function deriveKey(master, info, length = 32) {
10957
+ return Buffer.from(hkdfSync("sha256", master, Buffer.alloc(0), info, length)).toString("hex");
10958
+ }
10959
+ function derivePoolApiKey(master) {
10960
+ return deriveKey(master, POOL_API_KEY_INFO);
10961
+ }
10962
+ function deriveRunnerApiKey(master) {
10963
+ return deriveKey(master, RUNNER_API_KEY_INFO);
10964
+ }
10965
+ function bearerOk(headerAuth, xApiKey, expected) {
10966
+ const x = (xApiKey ?? "").trim();
10967
+ if (x && timingEqual(x, expected)) return true;
10968
+ const a = (headerAuth ?? "").trim();
10969
+ if (a.toLowerCase().startsWith("bearer ")) {
10970
+ return timingEqual(a.slice(7).trim(), expected);
10971
+ }
10972
+ return false;
10973
+ }
10974
+ function timingEqual(a, b) {
10975
+ if (a.length !== b.length) return false;
10976
+ let diff = 0;
10977
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
10978
+ return diff === 0;
10979
+ }
10980
+
10858
10981
  // src/pool/fly.ts
10859
10982
  var FLY_API_BASE = "https://api.machines.dev/v1";
10860
10983
  var POOL_METADATA_KEY = "kody_pool";
@@ -11231,7 +11354,7 @@ async function readVaultSecrets(opts) {
11231
11354
  async function readRepoSecret(opts) {
11232
11355
  const secrets = await readVaultSecrets(opts);
11233
11356
  const v = secrets[opts.name];
11234
- return v && v.trim() ? v : null;
11357
+ return v?.trim() ? v : null;
11235
11358
  }
11236
11359
  async function readRepoSecrets(opts) {
11237
11360
  return readVaultSecrets(opts);
@@ -11297,7 +11420,9 @@ var PoolRegistry = class {
11297
11420
  try {
11298
11421
  min = await this.resolvePoolMin(owner, repo);
11299
11422
  } catch (err) {
11300
- this.log(`registry: pool-min read failed for ${repoTag}, using default ${min}: ${err instanceof Error ? err.message : String(err)}`);
11423
+ this.log(
11424
+ `registry: pool-min read failed for ${repoTag}, using default ${min}: ${err instanceof Error ? err.message : String(err)}`
11425
+ );
11301
11426
  }
11302
11427
  const fly = new FlyClient({ token: flyToken, app: this.cfg.base.app });
11303
11428
  const pm = new PoolManager({
@@ -11325,7 +11450,9 @@ var PoolRegistry = class {
11325
11450
  Object.entries(vault).filter(([k]) => k !== "FLY_API_TOKEN" && k !== POOL_MIN_VAULT_KEY)
11326
11451
  );
11327
11452
  } catch (err) {
11328
- this.log(`[${this.key(owner, repo)}] vault secrets read failed: ${err instanceof Error ? err.message : String(err)}`);
11453
+ this.log(
11454
+ `[${this.key(owner, repo)}] vault secrets read failed: ${err instanceof Error ? err.message : String(err)}`
11455
+ );
11329
11456
  }
11330
11457
  const job = {
11331
11458
  jobId: req.jobId,
@@ -11366,102 +11493,6 @@ var PoolRegistry = class {
11366
11493
  }
11367
11494
  };
11368
11495
 
11369
- // src/pool/keys.ts
11370
- import { hkdfSync } from "crypto";
11371
- var POOL_API_KEY_INFO = "kody-pool-api:v1";
11372
- var RUNNER_API_KEY_INFO = "kody-runner-api:v1";
11373
- function masterKeyBytes(raw) {
11374
- const v = raw.trim();
11375
- if (!v) throw new Error("KODY_MASTER_KEY is empty");
11376
- if (/^[0-9a-fA-F]+$/.test(v) && v.length === 64) {
11377
- return Buffer.from(v, "hex");
11378
- }
11379
- return Buffer.from(v.replace(/-/g, "+").replace(/_/g, "/"), "base64");
11380
- }
11381
- function deriveKey(master, info, length = 32) {
11382
- return Buffer.from(hkdfSync("sha256", master, Buffer.alloc(0), info, length)).toString("hex");
11383
- }
11384
- function derivePoolApiKey(master) {
11385
- return deriveKey(master, POOL_API_KEY_INFO);
11386
- }
11387
- function deriveRunnerApiKey(master) {
11388
- return deriveKey(master, RUNNER_API_KEY_INFO);
11389
- }
11390
- function bearerOk(headerAuth, xApiKey, expected) {
11391
- const x = (xApiKey ?? "").trim();
11392
- if (x && timingEqual(x, expected)) return true;
11393
- const a = (headerAuth ?? "").trim();
11394
- if (a.toLowerCase().startsWith("bearer ")) {
11395
- return timingEqual(a.slice(7).trim(), expected);
11396
- }
11397
- return false;
11398
- }
11399
- function timingEqual(a, b) {
11400
- if (a.length !== b.length) return false;
11401
- let diff = 0;
11402
- for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
11403
- return diff === 0;
11404
- }
11405
-
11406
- // src/pool/duty-fallback-tick.ts
11407
- async function runDutyFallbackTick(deps) {
11408
- if (!await deps.isDegraded()) {
11409
- return { ran: false, claimed: 0 };
11410
- }
11411
- const repos = deps.activeRepos();
11412
- if (repos.length === 0) {
11413
- deps.log("GitHub Actions degraded but no active repo pools \u2014 nothing to tick");
11414
- return { ran: true, claimed: 0 };
11415
- }
11416
- deps.log(`GitHub Actions degraded \u2014 running scheduled fan-out on Fly for ${repos.length} repo(s)`);
11417
- const clock = deps.now ?? Date.now;
11418
- let claimed = 0;
11419
- for (const tag of repos) {
11420
- const [owner, repo] = tag.split("/");
11421
- if (!owner || !repo) continue;
11422
- try {
11423
- const res = await deps.claim(owner, repo, {
11424
- jobId: `sched-${owner}-${repo}-${clock()}`,
11425
- repo: tag,
11426
- mode: "scheduled"
11427
- });
11428
- if (res.ok) {
11429
- claimed++;
11430
- deps.log(`[${tag}] scheduled fan-out claimed ${res.machineId}`);
11431
- } else {
11432
- deps.log(`[${tag}] scheduled fan-out skipped: ${res.reason ?? "pool unavailable"}`);
11433
- }
11434
- } catch (err) {
11435
- deps.log(`[${tag}] scheduled fan-out error: ${err instanceof Error ? err.message : String(err)}`);
11436
- }
11437
- }
11438
- return { ran: true, claimed };
11439
- }
11440
-
11441
- // src/github-health.ts
11442
- var STATUS_URL = "https://www.githubstatus.com/api/v2/components.json";
11443
- var STATUS_CACHE_TTL_MS = 3e4;
11444
- var statusCache = null;
11445
- async function probeActionsStatus(fetchImpl = fetch) {
11446
- if (statusCache && statusCache.expiresAt > Date.now()) return statusCache.probe;
11447
- try {
11448
- const res = await fetchImpl(STATUS_URL, { headers: { "User-Agent": "kody-engine" } });
11449
- if (!res.ok) return { degraded: false, label: `http_${res.status}` };
11450
- const body = await res.json();
11451
- const actions = (body.components ?? []).find((c) => (c.name ?? "").trim().toLowerCase() === "actions");
11452
- const label = actions?.status ?? "unknown";
11453
- const degraded = !!actions && label !== "operational";
11454
- const probe = { degraded, label };
11455
- statusCache = { probe, expiresAt: Date.now() + STATUS_CACHE_TTL_MS };
11456
- return probe;
11457
- } catch {
11458
- return { degraded: false, label: "probe_error" };
11459
- }
11460
- }
11461
- async function gitHubActionsDegraded(fetchImpl = fetch) {
11462
- return (await probeActionsStatus(fetchImpl)).degraded;
11463
- }
11464
-
11465
11496
  // src/scripts/poolServe.ts
11466
11497
  var PERF_GUEST = {
11467
11498
  low: { cpu_kind: "shared", cpus: 2, memory_mb: 2048 },
@@ -11609,7 +11640,7 @@ var poolServe = async (ctx) => {
11609
11640
  });
11610
11641
  }
11611
11642
  const authed = bearerOk(
11612
- req.headers["authorization"],
11643
+ req.headers.authorization,
11613
11644
  req.headers["x-api-key"],
11614
11645
  poolApiKey
11615
11646
  );
@@ -11839,21 +11870,56 @@ ${plan}`;
11839
11870
  ---
11840
11871
  _Orchestrator will advance to the next step automatically._`;
11841
11872
  }
11842
- return `${head}
11843
-
11844
- ---
11845
- Comment \`kody run\` (prefixed with \`@\`) to execute this plan.`;
11846
- }
11847
-
11848
- // src/scripts/postResearchComment.ts
11849
- var postResearchComment = async (ctx) => {
11850
- postAgentSummaryComment(ctx, { issueOnly: true, render: renderResearchComment });
11873
+ return `${head}
11874
+
11875
+ ---
11876
+ Comment \`kody run\` (prefixed with \`@\`) to execute this plan.`;
11877
+ }
11878
+
11879
+ // src/scripts/postResearchComment.ts
11880
+ var postResearchComment = async (ctx) => {
11881
+ postAgentSummaryComment(ctx, { issueOnly: true, render: renderResearchComment });
11882
+ };
11883
+ function renderResearchComment(issueNumber, body) {
11884
+ return `## Research for issue #${issueNumber}
11885
+
11886
+ ${body}`;
11887
+ }
11888
+
11889
+ // src/scripts/promoteQaGoal.ts
11890
+ init_issue();
11891
+ var REPORT_JSON_OPEN2 = "<!-- KODY_QA_REPORT_JSON";
11892
+ var promoteQaGoal = async (ctx) => {
11893
+ ctx.skipAgent = true;
11894
+ const issueNum = ctx.args.issue;
11895
+ if (typeof issueNum !== "number" || issueNum <= 0) {
11896
+ ctx.output.exitCode = 2;
11897
+ ctx.output.reason = "qa-goal requires --issue <n>";
11898
+ process.stderr.write("[qa-goal] missing --issue\n");
11899
+ return;
11900
+ }
11901
+ let report;
11902
+ try {
11903
+ const issue = getIssue(issueNum, ctx.cwd);
11904
+ const reportComment = [...issue.comments].reverse().find((c) => c.body.includes(REPORT_JSON_OPEN2));
11905
+ if (!reportComment) {
11906
+ ctx.output.exitCode = 3;
11907
+ ctx.output.reason = `no QA report (${REPORT_JSON_OPEN2} \u2026) found on issue #${issueNum}`;
11908
+ process.stderr.write(`[qa-goal] ${ctx.output.reason}
11909
+ `);
11910
+ return;
11911
+ }
11912
+ report = reportComment.body;
11913
+ } catch (err) {
11914
+ const msg = err instanceof Error ? err.message : String(err);
11915
+ ctx.output.exitCode = 3;
11916
+ ctx.output.reason = `failed to read issue #${issueNum}: ${msg}`;
11917
+ process.stderr.write(`[qa-goal] ${ctx.output.reason}
11918
+ `);
11919
+ return;
11920
+ }
11921
+ await promoteReportToGoal(ctx, report, ctx.args.scope, ctx.args.goal);
11851
11922
  };
11852
- function renderResearchComment(issueNumber, body) {
11853
- return `## Research for issue #${issueNumber}
11854
-
11855
- ${body}`;
11856
- }
11857
11923
 
11858
11924
  // src/scripts/recordClassification.ts
11859
11925
  import { execFileSync as execFileSync20 } from "child_process";
@@ -11962,7 +12028,7 @@ function countActionItems(block) {
11962
12028
  }
11963
12029
 
11964
12030
  // src/scripts/requirePlanDeviations.ts
11965
- var requirePlanDeviations = async (ctx, profile) => {
12031
+ var requirePlanDeviations = async (ctx, _profile) => {
11966
12032
  if (!ctx.data.agentDone) return;
11967
12033
  const artifacts = ctx.data.artifacts ?? {};
11968
12034
  const planContent = (artifacts.plan ?? "").trim();
@@ -12167,46 +12233,6 @@ function pushEmptyCommit(branch, cwd) {
12167
12233
  }
12168
12234
  }
12169
12235
 
12170
- // src/scripts/promoteQaGoal.ts
12171
- init_issue();
12172
- var REPORT_JSON_OPEN2 = "<!-- KODY_QA_REPORT_JSON";
12173
- var promoteQaGoal = async (ctx) => {
12174
- ctx.skipAgent = true;
12175
- const issueNum = ctx.args.issue;
12176
- if (typeof issueNum !== "number" || issueNum <= 0) {
12177
- ctx.output.exitCode = 2;
12178
- ctx.output.reason = "qa-goal requires --issue <n>";
12179
- process.stderr.write("[qa-goal] missing --issue\n");
12180
- return;
12181
- }
12182
- let report;
12183
- try {
12184
- const issue = getIssue(issueNum, ctx.cwd);
12185
- const reportComment = [...issue.comments].reverse().find((c) => c.body.includes(REPORT_JSON_OPEN2));
12186
- if (!reportComment) {
12187
- ctx.output.exitCode = 3;
12188
- ctx.output.reason = `no QA report (${REPORT_JSON_OPEN2} \u2026) found on issue #${issueNum}`;
12189
- process.stderr.write(`[qa-goal] ${ctx.output.reason}
12190
- `);
12191
- return;
12192
- }
12193
- report = reportComment.body;
12194
- } catch (err) {
12195
- const msg = err instanceof Error ? err.message : String(err);
12196
- ctx.output.exitCode = 3;
12197
- ctx.output.reason = `failed to read issue #${issueNum}: ${msg}`;
12198
- process.stderr.write(`[qa-goal] ${ctx.output.reason}
12199
- `);
12200
- return;
12201
- }
12202
- await promoteReportToGoal(
12203
- ctx,
12204
- report,
12205
- ctx.args.scope,
12206
- ctx.args.goal
12207
- );
12208
- };
12209
-
12210
12236
  // src/deployments.ts
12211
12237
  init_issue();
12212
12238
  function findPreviewDeploymentUrl(prNumber, cwd) {
@@ -12527,7 +12553,13 @@ var runFlow = async (ctx) => {
12527
12553
  process.stderr.write(`[kody runFlow] resolved base branch: ${base} (from --base)
12528
12554
  `);
12529
12555
  }
12530
- const branchInfo = ensureFeatureBranch(issueNumber, issue.title, ctx.config.git.defaultBranch, ctx.cwd, base ?? void 0);
12556
+ const branchInfo = ensureFeatureBranch(
12557
+ issueNumber,
12558
+ issue.title,
12559
+ ctx.config.git.defaultBranch,
12560
+ ctx.cwd,
12561
+ base ?? void 0
12562
+ );
12531
12563
  ctx.data.branch = branchInfo.branch;
12532
12564
  const runUrl = getRunUrl();
12533
12565
  const startMsg = runUrl ? `\u2699\uFE0F kody started \u2014 branch \`${ctx.data.branch}\`, run ${runUrl}` : `\u2699\uFE0F kody started \u2014 branch \`${ctx.data.branch}\``;
@@ -12549,24 +12581,22 @@ function resolveBaseOverride(value) {
12549
12581
 
12550
12582
  // src/scripts/runnerServe.ts
12551
12583
  import { spawn as spawn5 } from "child_process";
12584
+ import * as fs38 from "fs";
12552
12585
  import { createServer as createServer3 } from "http";
12553
- import * as fs39 from "fs";
12554
12586
  var DEFAULT_PORT2 = 8080;
12555
12587
  var DEFAULT_WORKDIR = "/workspace/repo";
12556
12588
  function getApiKey2() {
12557
12589
  const key = (process.env.RUNNER_API_KEY ?? "").trim();
12558
12590
  if (!key) {
12559
- throw new Error(
12560
- "RUNNER_API_KEY env var is required \u2014 set it on the pooled machine before boot."
12561
- );
12591
+ throw new Error("RUNNER_API_KEY env var is required \u2014 set it on the pooled machine before boot.");
12562
12592
  }
12563
12593
  return key;
12564
12594
  }
12565
12595
  function authOk2(req, expected) {
12566
12596
  const xApiKey = req.headers["x-api-key"]?.trim();
12567
12597
  if (xApiKey && xApiKey === expected) return true;
12568
- const auth = req.headers["authorization"]?.trim();
12569
- if (auth && auth.toLowerCase().startsWith("bearer ")) {
12598
+ const auth = req.headers.authorization?.trim();
12599
+ if (auth?.toLowerCase().startsWith("bearer ")) {
12570
12600
  return auth.slice(7).trim() === expected;
12571
12601
  }
12572
12602
  return false;
@@ -12631,8 +12661,8 @@ async function defaultRunJob(job) {
12631
12661
  const workdir = process.env.RUNNER_WORKDIR ?? DEFAULT_WORKDIR;
12632
12662
  const branch = job.ref ?? "main";
12633
12663
  const authUrl = `https://x-access-token:${job.githubToken}@github.com/${job.repo}.git`;
12634
- fs39.rmSync(workdir, { recursive: true, force: true });
12635
- fs39.mkdirSync(workdir, { recursive: true });
12664
+ fs38.rmSync(workdir, { recursive: true, force: true });
12665
+ fs38.mkdirSync(workdir, { recursive: true });
12636
12666
  const allSecrets = typeof job.allSecrets === "string" ? job.allSecrets : JSON.stringify(job.allSecrets ?? {});
12637
12667
  const interactive = job.mode === "interactive";
12638
12668
  const scheduled = job.mode === "scheduled";
@@ -12672,15 +12702,7 @@ async function defaultRunJob(job) {
12672
12702
  });
12673
12703
  process.stdout.write(`[runner-serve] job ${job.jobId}: cloning ${job.repo}@${branch}
12674
12704
  `);
12675
- const cloneCode = await run("git", [
12676
- "clone",
12677
- "--depth=1",
12678
- "--single-branch",
12679
- "--branch",
12680
- branch,
12681
- authUrl,
12682
- workdir
12683
- ]);
12705
+ const cloneCode = await run("git", ["clone", "--depth=1", "--single-branch", "--branch", branch, authUrl, workdir]);
12684
12706
  if (cloneCode !== 0) {
12685
12707
  process.stderr.write(`[runner-serve] job ${job.jobId}: clone failed (${cloneCode})
12686
12708
  `);
@@ -12773,7 +12795,7 @@ var runnerServe = async (ctx) => {
12773
12795
 
12774
12796
  // src/scripts/runPreviewBuild.ts
12775
12797
  import { copyFile, writeFile } from "fs/promises";
12776
- import * as path36 from "path";
12798
+ import * as path35 from "path";
12777
12799
  import { fileURLToPath } from "url";
12778
12800
 
12779
12801
  // src/scripts/previewBuildHelpers.ts
@@ -12901,29 +12923,12 @@ async function setupNamespaceBuilder(opts) {
12901
12923
  await runCmd("bash", ["-c", NSC_INSTALL]);
12902
12924
  const jwt = await fetchGithubOidcToken(NSC_OIDC_AUDIENCE);
12903
12925
  if (!jwt) {
12904
- console.warn(
12905
- "[preview-build] no GitHub OIDC token (id-token: write missing?) \u2014 local docker build"
12906
- );
12926
+ console.warn("[preview-build] no GitHub OIDC token (id-token: write missing?) \u2014 local docker build");
12907
12927
  return null;
12908
12928
  }
12909
- await runCmd("nsc", [
12910
- "auth",
12911
- "exchange-oidc-token",
12912
- "--tenant_id",
12913
- opts.tenantId,
12914
- "--token",
12915
- jwt
12916
- ]);
12917
- await runCmd("nsc", [
12918
- "docker",
12919
- "buildx",
12920
- "setup",
12921
- "--name",
12922
- opts.builderName
12923
- ]);
12924
- console.log(
12925
- `[preview-build] Namespace remote builder ready (${opts.builderName})`
12926
- );
12929
+ await runCmd("nsc", ["auth", "exchange-oidc-token", "--tenant_id", opts.tenantId, "--token", jwt]);
12930
+ await runCmd("nsc", ["docker", "buildx", "setup", "--name", opts.builderName]);
12931
+ console.log(`[preview-build] Namespace remote builder ready (${opts.builderName})`);
12927
12932
  return opts.builderName;
12928
12933
  } catch (err) {
12929
12934
  console.warn(
@@ -12939,9 +12944,9 @@ var FLY_MACHINES = "https://api.machines.dev/v1";
12939
12944
  var FLY_GRAPHQL = "https://api.fly.io/graphql";
12940
12945
  var REQ_TIMEOUT_MS2 = 3e4;
12941
12946
  function bundledDockerfilePath(mode) {
12942
- const here = path36.dirname(fileURLToPath(import.meta.url));
12947
+ const here = path35.dirname(fileURLToPath(import.meta.url));
12943
12948
  const file = mode === "dev" ? "default-Dockerfile.preview.dev" : "default-Dockerfile.preview.prod";
12944
- return path36.join(here, "preview-build-templates", file);
12949
+ return path35.join(here, "preview-build-templates", file);
12945
12950
  }
12946
12951
  function required(name) {
12947
12952
  const v = (process.env[name] ?? "").trim();
@@ -13026,13 +13031,10 @@ async function flyAllocateSharedIps(appName, token) {
13026
13031
  }
13027
13032
  }
13028
13033
  async function flyListMachines(appName, token) {
13029
- const res = await fetch(
13030
- `${FLY_MACHINES}/apps/${encodeURIComponent(appName)}/machines`,
13031
- {
13032
- headers: flyHeaders(token),
13033
- signal: AbortSignal.timeout(REQ_TIMEOUT_MS2)
13034
- }
13035
- );
13034
+ const res = await fetch(`${FLY_MACHINES}/apps/${encodeURIComponent(appName)}/machines`, {
13035
+ headers: flyHeaders(token),
13036
+ signal: AbortSignal.timeout(REQ_TIMEOUT_MS2)
13037
+ });
13036
13038
  if (res.status === 404) return [];
13037
13039
  if (!res.ok) {
13038
13040
  throw new Error(`listMachines ${appName}: ${res.status}`);
@@ -13041,14 +13043,11 @@ async function flyListMachines(appName, token) {
13041
13043
  return data.map((m) => ({ id: m.id, state: m.state }));
13042
13044
  }
13043
13045
  async function flyDestroyMachine(appName, machineId, token) {
13044
- await fetch(
13045
- `${FLY_MACHINES}/apps/${encodeURIComponent(appName)}/machines/${encodeURIComponent(machineId)}/stop`,
13046
- {
13047
- method: "POST",
13048
- headers: flyHeaders(token),
13049
- signal: AbortSignal.timeout(REQ_TIMEOUT_MS2)
13050
- }
13051
- ).catch(() => void 0);
13046
+ await fetch(`${FLY_MACHINES}/apps/${encodeURIComponent(appName)}/machines/${encodeURIComponent(machineId)}/stop`, {
13047
+ method: "POST",
13048
+ headers: flyHeaders(token),
13049
+ signal: AbortSignal.timeout(REQ_TIMEOUT_MS2)
13050
+ }).catch(() => void 0);
13052
13051
  const res = await fetch(
13053
13052
  `${FLY_MACHINES}/apps/${encodeURIComponent(appName)}/machines/${encodeURIComponent(machineId)}?force=true`,
13054
13053
  {
@@ -13101,23 +13100,18 @@ async function flyCreatePreviewMachine(args, token) {
13101
13100
  };
13102
13101
  let lastErr = null;
13103
13102
  for (let attempt = 0; attempt < 6; attempt++) {
13104
- const res = await fetch(
13105
- `${FLY_MACHINES}/apps/${encodeURIComponent(args.appName)}/machines`,
13106
- {
13107
- method: "POST",
13108
- headers: flyHeaders(token),
13109
- body: JSON.stringify(body),
13110
- signal: AbortSignal.timeout(REQ_TIMEOUT_MS2)
13111
- }
13112
- );
13103
+ const res = await fetch(`${FLY_MACHINES}/apps/${encodeURIComponent(args.appName)}/machines`, {
13104
+ method: "POST",
13105
+ headers: flyHeaders(token),
13106
+ body: JSON.stringify(body),
13107
+ signal: AbortSignal.timeout(REQ_TIMEOUT_MS2)
13108
+ });
13113
13109
  if (res.ok) {
13114
13110
  const { id } = await res.json();
13115
13111
  return id;
13116
13112
  }
13117
13113
  const text = await res.text().catch(() => "");
13118
- lastErr = new Error(
13119
- `createPreviewMachine ${res.status}: ${text.slice(0, 300)}`
13120
- );
13114
+ lastErr = new Error(`createPreviewMachine ${res.status}: ${text.slice(0, 300)}`);
13121
13115
  if (!/MANIFEST_UNKNOWN|manifest unknown/i.test(text)) break;
13122
13116
  await new Promise((r) => setTimeout(r, 2e3 * (attempt + 1)));
13123
13117
  }
@@ -13137,21 +13131,18 @@ async function postOrUpdatePreviewComment(args) {
13137
13131
  signal: AbortSignal.timeout(REQ_TIMEOUT_MS2)
13138
13132
  }).catch(() => null);
13139
13133
  let existingId = null;
13140
- if (listRes && listRes.ok) {
13134
+ if (listRes?.ok) {
13141
13135
  const comments = await listRes.json().catch(() => []);
13142
13136
  const hit = comments.find((c) => (c.body ?? "").includes(MARKER));
13143
13137
  if (hit) existingId = hit.id;
13144
13138
  }
13145
13139
  if (existingId) {
13146
- await fetch(
13147
- `https://api.github.com/repos/${args.repo}/issues/comments/${existingId}`,
13148
- {
13149
- method: "PATCH",
13150
- headers,
13151
- body: JSON.stringify({ body: args.body }),
13152
- signal: AbortSignal.timeout(REQ_TIMEOUT_MS2)
13153
- }
13154
- );
13140
+ await fetch(`https://api.github.com/repos/${args.repo}/issues/comments/${existingId}`, {
13141
+ method: "PATCH",
13142
+ headers,
13143
+ body: JSON.stringify({ body: args.body }),
13144
+ signal: AbortSignal.timeout(REQ_TIMEOUT_MS2)
13145
+ });
13155
13146
  return;
13156
13147
  }
13157
13148
  await fetch(base, {
@@ -13179,9 +13170,7 @@ var runPreviewBuild = async (ctx, _profile, _args) => {
13179
13170
  masterKey = required("KODY_MASTER_KEY");
13180
13171
  ghToken4 = (process.env.KODY_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? process.env.GH_PAT ?? "").trim();
13181
13172
  if (!ghToken4) {
13182
- throw new Error(
13183
- "GitHub auth token missing (KODY_TOKEN / GH_TOKEN / GITHUB_TOKEN / GH_PAT all empty)"
13184
- );
13173
+ throw new Error("GitHub auth token missing (KODY_TOKEN / GH_TOKEN / GITHUB_TOKEN / GH_PAT all empty)");
13185
13174
  }
13186
13175
  } catch (err) {
13187
13176
  ctx.output.exitCode = 99;
@@ -13203,20 +13192,13 @@ var runPreviewBuild = async (ctx, _profile, _args) => {
13203
13192
  const orgSlug = doc.secrets?.FLY_ORG_SLUG?.value?.trim() || (process.env.FLY_ORG_SLUG ?? "personal").trim();
13204
13193
  const region = doc.secrets?.FLY_DEFAULT_REGION?.value?.trim() || (process.env.FLY_REGION ?? "fra").trim();
13205
13194
  const nscTenantId = doc.secrets?.NSC_TENANT_ID?.value?.trim() || "";
13206
- console.log(
13207
- `[preview-build] vault: ${Object.keys(buildEnv).length} secrets, mode=${buildMode}`
13208
- );
13195
+ console.log(`[preview-build] vault: ${Object.keys(buildEnv).length} secrets, mode=${buildMode}`);
13209
13196
  if (Object.keys(buildEnv).length > 0) {
13210
- const lines = Object.entries(buildEnv).map(
13211
- ([k, v]) => `${k}=${JSON.stringify(v)}`
13212
- );
13213
- await writeFile(
13214
- path36.join(ctx.cwd, ".env.production.local"),
13215
- lines.join("\n") + "\n",
13216
- "utf8"
13217
- );
13197
+ const lines = Object.entries(buildEnv).map(([k, v]) => `${k}=${JSON.stringify(v)}`);
13198
+ await writeFile(path35.join(ctx.cwd, ".env.production.local"), `${lines.join("\n")}
13199
+ `, "utf8");
13218
13200
  }
13219
- const consumerDockerfile = path36.join(ctx.cwd, "Dockerfile.preview");
13201
+ const consumerDockerfile = path35.join(ctx.cwd, "Dockerfile.preview");
13220
13202
  const { stat } = await import("fs/promises");
13221
13203
  let hasConsumerDockerfile = false;
13222
13204
  try {
@@ -13228,19 +13210,16 @@ var runPreviewBuild = async (ctx, _profile, _args) => {
13228
13210
  if (!hasConsumerDockerfile) {
13229
13211
  const bundled = bundledDockerfilePath(buildMode);
13230
13212
  await copyFile(bundled, consumerDockerfile);
13231
- console.log(
13232
- `[preview-build] using bundled Dockerfile.preview.${buildMode} (from ${bundled})`
13233
- );
13213
+ console.log(`[preview-build] using bundled Dockerfile.preview.${buildMode} (from ${bundled})`);
13234
13214
  } else {
13235
13215
  console.log("[preview-build] using repo Dockerfile.preview");
13236
13216
  }
13237
13217
  let baseImage = null;
13238
13218
  if (ghcrOwner) {
13239
13219
  const baseRef = `${ghcrOwner.toLowerCase()}/${basePreviewAppName(repo)}`;
13240
- const tok = await fetch(
13241
- `https://ghcr.io/token?scope=repository:${baseRef}:pull&service=ghcr.io`,
13242
- { signal: AbortSignal.timeout(15e3) }
13243
- ).catch(() => null);
13220
+ const tok = await fetch(`https://ghcr.io/token?scope=repository:${baseRef}:pull&service=ghcr.io`, {
13221
+ signal: AbortSignal.timeout(15e3)
13222
+ }).catch(() => null);
13244
13223
  if (tok?.ok) {
13245
13224
  const { token: bearer } = await tok.json();
13246
13225
  const probe = await fetch(`https://ghcr.io/v2/${baseRef}/manifests/latest`, {
@@ -13261,39 +13240,22 @@ var runPreviewBuild = async (ctx, _profile, _args) => {
13261
13240
  await flyCreateApp(appName, orgSlug, flyToken);
13262
13241
  }
13263
13242
  await flyAllocateSharedIps(appName, flyToken);
13264
- await runCmd(
13265
- "docker",
13266
- ["login", "registry.fly.io", "-u", "x", "--password-stdin"],
13267
- { input: flyToken, cwd: ctx.cwd }
13268
- );
13243
+ await runCmd("docker", ["login", "registry.fly.io", "-u", "x", "--password-stdin"], {
13244
+ input: flyToken,
13245
+ cwd: ctx.cwd
13246
+ });
13269
13247
  const imageRef = `registry.fly.io/${appName}:${tag}`;
13270
13248
  const nsBuilder = nscTenantId ? await setupNamespaceBuilder({
13271
13249
  tenantId: nscTenantId,
13272
13250
  builderName: `kody-preview-${pr}`
13273
13251
  }) : null;
13274
13252
  if (nsBuilder) {
13275
- const a = [
13276
- "buildx",
13277
- "build",
13278
- "--builder",
13279
- nsBuilder,
13280
- "-f",
13281
- "Dockerfile.preview",
13282
- "-t",
13283
- imageRef,
13284
- "--push"
13285
- ];
13253
+ const a = ["buildx", "build", "--builder", nsBuilder, "-f", "Dockerfile.preview", "-t", imageRef, "--push"];
13286
13254
  if (baseImage) a.push("--build-arg", `BASE_IMAGE=${baseImage}`);
13287
13255
  a.push(".");
13288
13256
  await runCmd("docker", a, { cwd: ctx.cwd });
13289
13257
  } else {
13290
- const buildArgs = [
13291
- "build",
13292
- "-f",
13293
- "Dockerfile.preview",
13294
- "-t",
13295
- imageRef
13296
- ];
13258
+ const buildArgs = ["build", "-f", "Dockerfile.preview", "-t", imageRef];
13297
13259
  if (baseImage) buildArgs.push("--build-arg", `BASE_IMAGE=${baseImage}`);
13298
13260
  buildArgs.push(".");
13299
13261
  await runCmd("docker", buildArgs, {
@@ -13315,9 +13277,7 @@ var runPreviewBuild = async (ctx, _profile, _args) => {
13315
13277
  },
13316
13278
  flyToken
13317
13279
  );
13318
- console.log(
13319
- `[preview-build] done \u2014 machine ${machineId} at https://${appName}.fly.dev`
13320
- );
13280
+ console.log(`[preview-build] done \u2014 machine ${machineId} at https://${appName}.fly.dev`);
13321
13281
  await postOrUpdatePreviewComment({
13322
13282
  repo,
13323
13283
  pr,
@@ -13338,8 +13298,8 @@ var runPreviewBuild = async (ctx, _profile, _args) => {
13338
13298
 
13339
13299
  // src/scripts/runTickScript.ts
13340
13300
  import { spawnSync as spawnSync2 } from "child_process";
13341
- import * as fs40 from "fs";
13342
- import * as path37 from "path";
13301
+ import * as fs39 from "fs";
13302
+ import * as path36 from "path";
13343
13303
  var runTickScript = async (ctx, _profile, args) => {
13344
13304
  ctx.skipAgent = true;
13345
13305
  const jobsDir = String(args?.jobsDir ?? ".kody/duties");
@@ -13351,22 +13311,22 @@ var runTickScript = async (ctx, _profile, args) => {
13351
13311
  ctx.output.reason = `runTickScript: ctx.args.${slugArg} must be a non-empty slug`;
13352
13312
  return;
13353
13313
  }
13354
- const jobPath = path37.join(ctx.cwd, jobsDir, `${slug}.md`);
13355
- if (!fs40.existsSync(jobPath)) {
13314
+ const jobPath = path36.join(ctx.cwd, jobsDir, `${slug}.md`);
13315
+ if (!fs39.existsSync(jobPath)) {
13356
13316
  ctx.output.exitCode = 99;
13357
13317
  ctx.output.reason = `runTickScript: job file not found: ${jobPath}`;
13358
13318
  return;
13359
13319
  }
13360
- const raw = fs40.readFileSync(jobPath, "utf-8");
13361
- const { frontmatter } = splitFrontmatter2(raw);
13320
+ const raw = fs39.readFileSync(jobPath, "utf-8");
13321
+ const { frontmatter } = splitFrontmatter(raw);
13362
13322
  const tickScript = frontmatter.tickScript;
13363
13323
  if (!tickScript) {
13364
13324
  ctx.output.exitCode = 99;
13365
13325
  ctx.output.reason = `runTickScript: job ${slug} has no \`tickScript:\` frontmatter \u2014 route via job-tick instead`;
13366
13326
  return;
13367
13327
  }
13368
- const scriptPath = path37.isAbsolute(tickScript) ? tickScript : path37.join(ctx.cwd, tickScript);
13369
- if (!fs40.existsSync(scriptPath)) {
13328
+ const scriptPath = path36.isAbsolute(tickScript) ? tickScript : path36.join(ctx.cwd, tickScript);
13329
+ if (!fs39.existsSync(scriptPath)) {
13370
13330
  ctx.output.exitCode = 99;
13371
13331
  ctx.output.reason = `runTickScript: tickScript not found: ${scriptPath}`;
13372
13332
  return;
@@ -13543,8 +13503,10 @@ var serveFlow = async (ctx) => {
13543
13503
  process.stdout.write(`[kody serve] LiteLLM ready at ${handle?.url ?? LITELLM_DEFAULT_URL}
13544
13504
  `);
13545
13505
  } else {
13546
- process.stdout.write(`[kody serve] model ${model.provider}/${model.model} routes to Anthropic directly \u2014 no proxy needed
13547
- `);
13506
+ process.stdout.write(
13507
+ `[kody serve] model ${model.provider}/${model.model} routes to Anthropic directly \u2014 no proxy needed
13508
+ `
13509
+ );
13548
13510
  }
13549
13511
  const url = handle?.url ?? LITELLM_DEFAULT_URL;
13550
13512
  const editorEnv = usesProxy ? buildProxyEnv(url) : { ...process.env };
@@ -13589,8 +13551,10 @@ var serveFlow = async (ctx) => {
13589
13551
  code.on("error", (err) => {
13590
13552
  process.stderr.write(`[kody serve] failed to launch VS Code: ${err.message}
13591
13553
  `);
13592
- process.stderr.write(` Install the 'code' CLI: VS Code \u2192 Command Palette \u2192 "Shell Command: Install 'code' command in PATH"
13593
- `);
13554
+ process.stderr.write(
13555
+ ` Install the 'code' CLI: VS Code \u2192 Command Palette \u2192 "Shell Command: Install 'code' command in PATH"
13556
+ `
13557
+ );
13594
13558
  });
13595
13559
  code.unref();
13596
13560
  } catch (err) {
@@ -13817,10 +13781,8 @@ var verify = async (ctx) => {
13817
13781
  ctx.data.verifyReason = result.ok ? "" : summarizeFailure(result);
13818
13782
  ctx.data.verifyRecovered = result.recovered ?? [];
13819
13783
  if (result.recovered && result.recovered.length > 0) {
13820
- process.stderr.write(
13821
- `[kody verify] caught flake on: ${result.recovered.join(", ")} (passed on retry)
13822
- `
13823
- );
13784
+ process.stderr.write(`[kody verify] caught flake on: ${result.recovered.join(", ")} (passed on retry)
13785
+ `);
13824
13786
  }
13825
13787
  } catch (err) {
13826
13788
  ctx.data.verifyOk = false;
@@ -13828,7 +13790,7 @@ var verify = async (ctx) => {
13828
13790
  }
13829
13791
  if (ctx.data.verifyOk === false) {
13830
13792
  const action = ctx.data.action;
13831
- if (action && action.type.endsWith("_COMPLETED")) {
13793
+ if (action?.type.endsWith("_COMPLETED")) {
13832
13794
  const reason = ctx.data.verifyReason || "verify failed";
13833
13795
  ctx.data.action = {
13834
13796
  type: action.type.replace(/_COMPLETED$/, "_FAILED"),
@@ -13945,7 +13907,7 @@ function runCommand2(command, cwd) {
13945
13907
  }
13946
13908
  function downgrade2(ctx, reason) {
13947
13909
  const action = ctx.data.action;
13948
- if (action && action.type.endsWith("_COMPLETED")) {
13910
+ if (action?.type.endsWith("_COMPLETED")) {
13949
13911
  ctx.data.action = {
13950
13912
  type: action.type.replace(/_COMPLETED$/, "_FAILED"),
13951
13913
  payload: { reason, downgradedFrom: action.type },
@@ -13965,10 +13927,8 @@ async function runVerify(ctx) {
13965
13927
  ctx.data.verifyReason = result.ok ? "" : summarizeFailure(result);
13966
13928
  ctx.data.verifyRecovered = result.recovered ?? [];
13967
13929
  if (result.recovered && result.recovered.length > 0) {
13968
- process.stderr.write(
13969
- `[kody verify] caught flake on: ${result.recovered.join(", ")} (passed on retry)
13970
- `
13971
- );
13930
+ process.stderr.write(`[kody verify] caught flake on: ${result.recovered.join(", ")} (passed on retry)
13931
+ `);
13972
13932
  }
13973
13933
  } catch (err) {
13974
13934
  ctx.data.verifyOk = false;
@@ -13978,7 +13938,7 @@ async function runVerify(ctx) {
13978
13938
  function downgradeActionOnFailure(ctx) {
13979
13939
  if (ctx.data.verifyOk !== false) return;
13980
13940
  const action = ctx.data.action;
13981
- if (!action || !action.type.endsWith("_COMPLETED")) return;
13941
+ if (!action?.type.endsWith("_COMPLETED")) return;
13982
13942
  const reason = ctx.data.verifyReason || "verify failed";
13983
13943
  ctx.data.action = {
13984
13944
  type: action.type.replace(/_COMPLETED$/, "_FAILED"),
@@ -13989,9 +13949,9 @@ function downgradeActionOnFailure(ctx) {
13989
13949
  function upgradeActionOnPass(ctx) {
13990
13950
  if (ctx.data.verifyOk !== true) return;
13991
13951
  const action = ctx.data.action;
13992
- if (!action || !action.type.endsWith("_FAILED")) return;
13952
+ if (!action?.type.endsWith("_FAILED")) return;
13993
13953
  const downgradedFrom = action.payload?.downgradedFrom;
13994
- if (!downgradedFrom || !downgradedFrom.endsWith("_COMPLETED")) return;
13954
+ if (!downgradedFrom?.endsWith("_COMPLETED")) return;
13995
13955
  ctx.data.action = {
13996
13956
  type: downgradedFrom,
13997
13957
  payload: {},
@@ -14171,74 +14131,6 @@ function sleep3(ms) {
14171
14131
  return new Promise((res) => setTimeout(res, ms));
14172
14132
  }
14173
14133
 
14174
- // src/scripts/appendCompanyActivity.ts
14175
- init_issue();
14176
- function resolveTrigger(force) {
14177
- const event = process.env.GITHUB_EVENT_NAME ?? "";
14178
- if (event === "schedule") return "schedule";
14179
- if (force || event === "issue_comment" || event === "workflow_dispatch")
14180
- return "manual";
14181
- return "event";
14182
- }
14183
- function appendLine(owner, repo, cwd, record) {
14184
- const filePath = `.kody/activity/${record.ts.slice(0, 10)}.jsonl`;
14185
- let existing = "";
14186
- let sha;
14187
- try {
14188
- const out = gh(["api", `/repos/${owner}/${repo}/contents/${filePath}?ref=${STATE_BRANCH}`], { cwd });
14189
- const json = JSON.parse(out);
14190
- if (json.sha) sha = json.sha;
14191
- if (json.content) existing = Buffer.from(json.content, "base64").toString("utf-8");
14192
- } catch {
14193
- }
14194
- const body = `${existing}${JSON.stringify(record)}
14195
- `;
14196
- const payload = {
14197
- message: `chore(activity): ${record.action}`,
14198
- content: Buffer.from(body, "utf-8").toString("base64"),
14199
- // Keep this high-frequency feed off the default branch.
14200
- branch: STATE_BRANCH
14201
- };
14202
- if (sha) payload.sha = sha;
14203
- ensureStateBranch(owner, repo, cwd);
14204
- gh(
14205
- ["api", "--method", "PUT", `/repos/${owner}/${repo}/contents/${filePath}`, "--input", "-"],
14206
- { cwd, input: JSON.stringify(payload) }
14207
- );
14208
- }
14209
- var appendCompanyActivity = async (ctx, _profile, agentResult) => {
14210
- try {
14211
- const owner = ctx.config?.github?.owner;
14212
- const repo = ctx.config?.github?.repo;
14213
- const duty = String(ctx.data.jobSlug ?? ctx.args?.job ?? "").trim();
14214
- if (!owner || !repo || !duty) return;
14215
- const dutyTitle = ctx.data.jobTitle ?? null;
14216
- const staff = ctx.data.workerSlug || null;
14217
- const staffTitle = ctx.data.workerTitle || null;
14218
- const force = ctx.args?.force === true;
14219
- const record = {
14220
- ts: (/* @__PURE__ */ new Date()).toISOString(),
14221
- action: `Ran duty: ${dutyTitle ?? duty}`,
14222
- duty,
14223
- dutyTitle,
14224
- staff,
14225
- staffTitle,
14226
- trigger: resolveTrigger(force),
14227
- outcome: agentResult?.outcome ?? "unknown",
14228
- outcomeKind: agentResult?.outcomeKind ?? null,
14229
- reason: agentResult?.error ?? null,
14230
- durationMs: agentResult?.durationMs ?? null,
14231
- runUrl: getRunUrl() || null
14232
- };
14233
- appendLine(owner, repo, ctx.cwd, record);
14234
- } catch (err) {
14235
- process.stderr.write(
14236
- `[activity] company-activity append failed: ${err instanceof Error ? err.message : String(err)}
14237
- `
14238
- );
14239
- }
14240
- };
14241
-
14242
14134
  // src/scripts/warmupMcp.ts
14243
14135
  import { spawn as spawn9 } from "child_process";
14244
14136
  var PER_SERVER_TIMEOUT_MS = 6e4;
@@ -14276,12 +14168,14 @@ async function warmupOne(command, args, env) {
14276
14168
  let nextId = 1;
14277
14169
  const send = (method, params) => {
14278
14170
  const id = nextId++;
14279
- const payload = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
14171
+ const payload = `${JSON.stringify({ jsonrpc: "2.0", id, method, params })}
14172
+ `;
14280
14173
  child.stdin.write(payload);
14281
14174
  return id;
14282
14175
  };
14283
14176
  const notify = (method, params) => {
14284
- const payload = JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n";
14177
+ const payload = `${JSON.stringify({ jsonrpc: "2.0", method, params })}
14178
+ `;
14285
14179
  child.stdin.write(payload);
14286
14180
  };
14287
14181
  const awaitResponse = async (id) => {
@@ -14346,11 +14240,12 @@ function lineStream(stream) {
14346
14240
  };
14347
14241
  stream.on("data", (chunk) => {
14348
14242
  buf += typeof chunk === "string" ? chunk : chunk.toString("utf8");
14349
- let idx;
14350
- while ((idx = buf.indexOf("\n")) >= 0) {
14243
+ let idx = buf.indexOf("\n");
14244
+ while (idx >= 0) {
14351
14245
  const line = buf.slice(0, idx).replace(/\r$/, "");
14352
14246
  buf = buf.slice(idx + 1);
14353
14247
  if (line.length > 0) queue.push(line);
14248
+ idx = buf.indexOf("\n");
14354
14249
  }
14355
14250
  tryDeliver();
14356
14251
  });
@@ -14373,12 +14268,15 @@ function lineStream(stream) {
14373
14268
  return;
14374
14269
  }
14375
14270
  waiter = resolve6;
14376
- const t = setTimeout(() => {
14377
- if (waiter === resolve6) {
14378
- waiter = null;
14379
- resolve6(null);
14380
- }
14381
- }, Math.max(0, timeoutMs));
14271
+ const t = setTimeout(
14272
+ () => {
14273
+ if (waiter === resolve6) {
14274
+ waiter = null;
14275
+ resolve6(null);
14276
+ }
14277
+ },
14278
+ Math.max(0, timeoutMs)
14279
+ );
14382
14280
  t.unref?.();
14383
14281
  })
14384
14282
  };
@@ -14474,7 +14372,7 @@ var writeJobStateFile = async (ctx, _profile, agentResult, args) => {
14474
14372
  };
14475
14373
 
14476
14374
  // src/scripts/writeRunSummary.ts
14477
- import * as fs41 from "fs";
14375
+ import * as fs40 from "fs";
14478
14376
  var writeRunSummary = async (ctx, profile) => {
14479
14377
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
14480
14378
  if (!summaryPath) return;
@@ -14496,7 +14394,7 @@ var writeRunSummary = async (ctx, profile) => {
14496
14394
  if (reason) lines.push(`- **Reason:** ${reason}`);
14497
14395
  lines.push("");
14498
14396
  try {
14499
- fs41.appendFileSync(summaryPath, `${lines.join("\n")}
14397
+ fs40.appendFileSync(summaryPath, `${lines.join("\n")}
14500
14398
  `);
14501
14399
  } catch {
14502
14400
  }
@@ -14603,6 +14501,48 @@ var allScriptNames = /* @__PURE__ */ new Set([
14603
14501
  ...Object.keys(postflightScripts)
14604
14502
  ]);
14605
14503
 
14504
+ // src/subagents.ts
14505
+ import * as fs41 from "fs";
14506
+ import * as path37 from "path";
14507
+ function splitFrontmatter2(raw) {
14508
+ const match = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(raw);
14509
+ if (!match) return { fm: {}, body: raw.trim() };
14510
+ const fm = {};
14511
+ for (const line of match[1].split("\n")) {
14512
+ const idx = line.indexOf(":");
14513
+ if (idx === -1) continue;
14514
+ fm[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
14515
+ }
14516
+ return { fm, body: (match[2] ?? "").trim() };
14517
+ }
14518
+ function resolveAgentFile(profileDir, name) {
14519
+ const local = path37.join(profileDir, "agents", `${name}.md`);
14520
+ if (fs41.existsSync(local)) return local;
14521
+ const central = path37.join(getPluginsCatalogRoot(), "agents", `${name}.md`);
14522
+ if (fs41.existsSync(central)) return central;
14523
+ throw new Error(`loadSubagents: agent '${name}' not found in ${profileDir}/agents/ or shared catalog`);
14524
+ }
14525
+ function loadSubagents(profile) {
14526
+ const names = profile.claudeCode.subagents;
14527
+ if (!names || names.length === 0) return void 0;
14528
+ const agents = {};
14529
+ for (const name of names) {
14530
+ const { fm, body } = splitFrontmatter2(fs41.readFileSync(resolveAgentFile(profile.dir, name), "utf-8"));
14531
+ if (!body) throw new Error(`loadSubagents: agent '${name}' has an empty prompt body`);
14532
+ const def = {
14533
+ description: fm.description ?? `Subagent ${name}`,
14534
+ prompt: body
14535
+ };
14536
+ if (fm.tools) {
14537
+ const tools = fm.tools.split(",").map((t) => t.trim()).filter(Boolean);
14538
+ if (tools.length > 0) def.tools = tools;
14539
+ }
14540
+ if (fm.model) def.model = fm.model;
14541
+ agents[fm.name || name] = def;
14542
+ }
14543
+ return agents;
14544
+ }
14545
+
14606
14546
  // src/tools.ts
14607
14547
  import { execFileSync as execFileSync27 } from "child_process";
14608
14548
  function verifyCliTools(tools, cwd) {
@@ -15036,7 +14976,7 @@ function clearStampedLifecycleLabels(profile, ctx) {
15036
14976
  for (const entry of profile.scripts.preflight) {
15037
14977
  if (entry.script !== "setLifecycleLabel") continue;
15038
14978
  const label = typeof entry.with?.label === "string" ? entry.with.label : void 0;
15039
- if (!label || !label.startsWith(KODY_NAMESPACE)) continue;
14979
+ if (!label?.startsWith(KODY_NAMESPACE)) continue;
15040
14980
  try {
15041
14981
  removeLabel(target, label, ctx.cwd);
15042
14982
  } catch {
@@ -15600,10 +15540,8 @@ ${CI_HELP}`);
15600
15540
  `);
15601
15541
  const buildOnly = dispatch2.executable === "preview-build";
15602
15542
  if (args.skipInstall || buildOnly) {
15603
- process.stdout.write(
15604
- `\u2192 kody: skipping dep install (${buildOnly ? "build-only executable" : "--skip-install"})
15605
- `
15606
- );
15543
+ process.stdout.write(`\u2192 kody: skipping dep install (${buildOnly ? "build-only executable" : "--skip-install"})
15544
+ `);
15607
15545
  } else {
15608
15546
  const code = installDeps(pm, cwd);
15609
15547
  if (code !== 0) {
@@ -16093,15 +16031,19 @@ Kody run statistics \u2014 ${totalRuns} runs
16093
16031
  const totalIn = runs.reduce((s, r) => s + r.totalInputTokens, 0);
16094
16032
  const totalOut = runs.reduce((s, r) => s + r.totalOutputTokens, 0);
16095
16033
  const totalCacheR = runs.reduce((s, r) => s + r.totalCacheReadTokens, 0);
16096
- process.stdout.write(` total tokens : ${totalIn.toLocaleString()} in / ${totalOut.toLocaleString()} out / ${totalCacheR.toLocaleString()} cache-read
16097
- `);
16034
+ process.stdout.write(
16035
+ ` total tokens : ${totalIn.toLocaleString()} in / ${totalOut.toLocaleString()} out / ${totalCacheR.toLocaleString()} cache-read
16036
+ `
16037
+ );
16098
16038
  process.stdout.write(`
16099
16039
  Per-executable (stage_end events)
16100
16040
  `);
16101
16041
  const headers = ["executable", "runs", "ok", "failed", "p50", "p95", "mean", "tok-in", "tok-out", "cache-r"];
16102
16042
  const widths = [22, 6, 6, 7, 9, 9, 9, 10, 10, 10];
16103
- process.stdout.write(headers.map((h, i) => h.padEnd(widths[i])).join("") + "\n");
16104
- process.stdout.write(widths.map((w) => "-".repeat(w - 1) + " ").join("") + "\n");
16043
+ process.stdout.write(`${headers.map((h, i) => h.padEnd(widths[i])).join("")}
16044
+ `);
16045
+ process.stdout.write(`${widths.map((w) => `${"-".repeat(w - 1)} `).join("")}
16046
+ `);
16105
16047
  for (const r of rollups) {
16106
16048
  const row = [
16107
16049
  r.executable,
@@ -16115,7 +16057,8 @@ Per-executable (stage_end events)
16115
16057
  r.totalOutputTokens.toLocaleString(),
16116
16058
  r.totalCacheReadTokens.toLocaleString()
16117
16059
  ];
16118
- process.stdout.write(row.map((c, i) => c.padEnd(widths[i])).join("") + "\n");
16060
+ process.stdout.write(`${row.map((c, i) => c.padEnd(widths[i])).join("")}
16061
+ `);
16119
16062
  }
16120
16063
  process.stdout.write("\n");
16121
16064
  }