@kody-ade/kody-engine 0.4.201 → 0.4.202
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/kody.js +1122 -1210
- package/dist/executables/goal-scheduler/scheduler.sh +0 -0
- package/dist/executables/release-deploy/deploy.sh +0 -0
- package/dist/executables/release-prepare/prepare.sh +0 -0
- package/dist/executables/release-publish/publish.sh +0 -0
- package/dist/executables/resolve/apply-prefer.sh +0 -0
- package/dist/executables/revert/revert.sh +0 -0
- package/package.json +21 -22
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
|
|
23
|
-
import * as
|
|
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 =
|
|
53
|
-
|
|
54
|
-
|
|
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 =
|
|
61
|
-
if (!
|
|
62
|
-
const lines =
|
|
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 =
|
|
76
|
-
if (!
|
|
77
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
}
|
|
@@ -621,18 +623,7 @@ function readLedger(label) {
|
|
|
621
623
|
const startTag = `<!-- ${label}:start -->`;
|
|
622
624
|
const endTag = `<!-- ${label}:end -->`;
|
|
623
625
|
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
|
-
]);
|
|
626
|
+
const raw = gh(["issue", "list", "--state", "open", "--label", label, "--limit", "5", "--json", "number,body"]);
|
|
636
627
|
const issues = JSON.parse(raw);
|
|
637
628
|
if (issues.length === 0) return { found: false, payload: null };
|
|
638
629
|
const issue = issues.sort((a, b) => a.number - b.number)[0];
|
|
@@ -805,9 +796,13 @@ function buildDutyMcpServer(opts) {
|
|
|
805
796
|
"ensure_issue",
|
|
806
797
|
"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
798
|
{
|
|
808
|
-
key: z3.string().min(1).describe(
|
|
799
|
+
key: z3.string().min(1).describe(
|
|
800
|
+
"Stable dedup identity for this finding (e.g. 'dev-ci-red', 'docs-drift:<feature>'). Same key across ticks = same issue."
|
|
801
|
+
),
|
|
809
802
|
title: z3.string().min(1).describe("Issue title (used only on first creation)."),
|
|
810
|
-
body: z3.string().min(1).describe(
|
|
803
|
+
body: z3.string().min(1).describe(
|
|
804
|
+
"Issue body markdown (used only on first creation). Include the operator mention verbatim if the duty body has one."
|
|
805
|
+
)
|
|
811
806
|
},
|
|
812
807
|
async (args) => {
|
|
813
808
|
const result = ensureIssue(opts.repoSlug, args.key, args.title, args.body);
|
|
@@ -886,15 +881,15 @@ var init_dutyMcp = __esm({
|
|
|
886
881
|
|
|
887
882
|
// src/repoWorkspace.ts
|
|
888
883
|
import { spawn as spawn2, spawnSync } from "child_process";
|
|
889
|
-
import * as
|
|
890
|
-
import * as
|
|
884
|
+
import * as fs5 from "fs";
|
|
885
|
+
import * as path5 from "path";
|
|
891
886
|
async function resolveAndClone(reposRoot, repo, repoToken, cloneRepo) {
|
|
892
887
|
const name = repo?.trim();
|
|
893
888
|
if (!name || !REPO_RE.test(name)) return null;
|
|
894
|
-
const root =
|
|
895
|
-
const dir =
|
|
896
|
-
if (dir !== root && !dir.startsWith(root +
|
|
897
|
-
if (
|
|
889
|
+
const root = path5.resolve(reposRoot);
|
|
890
|
+
const dir = path5.resolve(root, name);
|
|
891
|
+
if (dir !== root && !dir.startsWith(root + path5.sep)) return null;
|
|
892
|
+
if (fs5.existsSync(path5.join(dir, ".git"))) return dir;
|
|
898
893
|
const inflight = repoClones.get(dir);
|
|
899
894
|
if (inflight) {
|
|
900
895
|
await inflight;
|
|
@@ -908,25 +903,13 @@ async function resolveAndClone(reposRoot, repo, repoToken, cloneRepo) {
|
|
|
908
903
|
return dir;
|
|
909
904
|
}
|
|
910
905
|
async function ensureRepoCwd(opts) {
|
|
911
|
-
const dir = await resolveAndClone(
|
|
912
|
-
opts.reposRoot,
|
|
913
|
-
opts.repo,
|
|
914
|
-
opts.repoToken,
|
|
915
|
-
opts.cloneRepo
|
|
916
|
-
);
|
|
906
|
+
const dir = await resolveAndClone(opts.reposRoot, opts.repo, opts.repoToken, opts.cloneRepo);
|
|
917
907
|
return dir ?? opts.baseCwd;
|
|
918
908
|
}
|
|
919
909
|
async function fetchRepo(opts) {
|
|
920
|
-
const dir = await resolveAndClone(
|
|
921
|
-
opts.reposRoot,
|
|
922
|
-
opts.repo,
|
|
923
|
-
opts.repoToken,
|
|
924
|
-
opts.cloneRepo ?? defaultCloneRepo
|
|
925
|
-
);
|
|
910
|
+
const dir = await resolveAndClone(opts.reposRoot, opts.repo, opts.repoToken, opts.cloneRepo ?? defaultCloneRepo);
|
|
926
911
|
if (!dir) {
|
|
927
|
-
throw new Error(
|
|
928
|
-
`invalid repo "${opts.repo}" \u2014 expected "owner/name" with no path escapes`
|
|
929
|
-
);
|
|
912
|
+
throw new Error(`invalid repo "${opts.repo}" \u2014 expected "owner/name" with no path escapes`);
|
|
930
913
|
}
|
|
931
914
|
return dir;
|
|
932
915
|
}
|
|
@@ -937,7 +920,7 @@ var init_repoWorkspace = __esm({
|
|
|
937
920
|
REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
938
921
|
repoClones = /* @__PURE__ */ new Map();
|
|
939
922
|
defaultCloneRepo = (repo, token, dir) => {
|
|
940
|
-
|
|
923
|
+
fs5.mkdirSync(path5.dirname(dir), { recursive: true });
|
|
941
924
|
const authUrl = token ? `https://x-access-token:${token}@github.com/${repo}.git` : `https://github.com/${repo}.git`;
|
|
942
925
|
return new Promise((resolve6, reject) => {
|
|
943
926
|
const child = spawn2("git", ["clone", "--depth=1", authUrl, dir], {
|
|
@@ -968,10 +951,7 @@ var fetchRepoMcp_exports = {};
|
|
|
968
951
|
__export(fetchRepoMcp_exports, {
|
|
969
952
|
buildFetchRepoMcpServer: () => buildFetchRepoMcpServer
|
|
970
953
|
});
|
|
971
|
-
import {
|
|
972
|
-
createSdkMcpServer as createSdkMcpServer4,
|
|
973
|
-
tool as tool4
|
|
974
|
-
} from "@anthropic-ai/claude-agent-sdk";
|
|
954
|
+
import { createSdkMcpServer as createSdkMcpServer4, tool as tool4 } from "@anthropic-ai/claude-agent-sdk";
|
|
975
955
|
import { z as z4 } from "zod";
|
|
976
956
|
function buildFetchRepoMcpServer(opts) {
|
|
977
957
|
const fetchTool = tool4(
|
|
@@ -1431,7 +1411,7 @@ var init_loadCoverageRules = __esm({
|
|
|
1431
1411
|
// package.json
|
|
1432
1412
|
var package_default = {
|
|
1433
1413
|
name: "@kody-ade/kody-engine",
|
|
1434
|
-
version: "0.4.
|
|
1414
|
+
version: "0.4.202",
|
|
1435
1415
|
description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
|
|
1436
1416
|
license: "MIT",
|
|
1437
1417
|
type: "module",
|
|
@@ -1562,104 +1542,16 @@ function makeRunId(sessionId, suffix) {
|
|
|
1562
1542
|
import * as fs11 from "fs";
|
|
1563
1543
|
import * as path11 from "path";
|
|
1564
1544
|
|
|
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
1545
|
// src/agent.ts
|
|
1654
|
-
import * as
|
|
1655
|
-
import * as
|
|
1546
|
+
import * as fs6 from "fs";
|
|
1547
|
+
import * as path6 from "path";
|
|
1656
1548
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
1657
1549
|
|
|
1658
1550
|
// src/claudeBinary.ts
|
|
1659
|
-
import * as
|
|
1551
|
+
import * as fs2 from "fs";
|
|
1660
1552
|
import { createRequire } from "module";
|
|
1661
1553
|
import * as os from "os";
|
|
1662
|
-
import * as
|
|
1554
|
+
import * as path2 from "path";
|
|
1663
1555
|
var SDK_PKG = "@anthropic-ai/claude-agent-sdk";
|
|
1664
1556
|
function candidateSpecs(platform, arch) {
|
|
1665
1557
|
const ext = platform === "win32" ? ".exe" : "";
|
|
@@ -1669,8 +1561,8 @@ function candidateSpecs(platform, arch) {
|
|
|
1669
1561
|
function readSdkVersion(req) {
|
|
1670
1562
|
try {
|
|
1671
1563
|
const entry = req.resolve(SDK_PKG);
|
|
1672
|
-
const pkgDir =
|
|
1673
|
-
const raw =
|
|
1564
|
+
const pkgDir = path2.dirname(entry);
|
|
1565
|
+
const raw = fs2.readFileSync(path2.join(pkgDir, "package.json"), "utf8");
|
|
1674
1566
|
const v = JSON.parse(raw)?.version;
|
|
1675
1567
|
return typeof v === "string" && v.length > 0 ? v : "unknown";
|
|
1676
1568
|
} catch {
|
|
@@ -1692,24 +1584,24 @@ function ensureStableClaudeBinary() {
|
|
|
1692
1584
|
} catch {
|
|
1693
1585
|
}
|
|
1694
1586
|
}
|
|
1695
|
-
if (!source || !
|
|
1587
|
+
if (!source || !fs2.existsSync(source)) {
|
|
1696
1588
|
cached = null;
|
|
1697
1589
|
return cached;
|
|
1698
1590
|
}
|
|
1699
1591
|
const ext = process.platform === "win32" ? ".exe" : "";
|
|
1700
1592
|
const version = readSdkVersion(req);
|
|
1701
|
-
const destDir =
|
|
1702
|
-
const dest =
|
|
1703
|
-
const srcSize =
|
|
1704
|
-
if (
|
|
1593
|
+
const destDir = path2.join(os.tmpdir(), "kody-claude-sdk", version);
|
|
1594
|
+
const dest = path2.join(destDir, `claude${ext}`);
|
|
1595
|
+
const srcSize = fs2.statSync(source).size;
|
|
1596
|
+
if (fs2.existsSync(dest) && fs2.statSync(dest).size === srcSize) {
|
|
1705
1597
|
cached = dest;
|
|
1706
1598
|
return cached;
|
|
1707
1599
|
}
|
|
1708
|
-
|
|
1709
|
-
const tmp =
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1600
|
+
fs2.mkdirSync(destDir, { recursive: true });
|
|
1601
|
+
const tmp = path2.join(destDir, `.claude.${process.pid}.${Date.now()}.tmp`);
|
|
1602
|
+
fs2.copyFileSync(source, tmp);
|
|
1603
|
+
fs2.chmodSync(tmp, 493);
|
|
1604
|
+
fs2.renameSync(tmp, dest);
|
|
1713
1605
|
cached = dest;
|
|
1714
1606
|
return cached;
|
|
1715
1607
|
} catch {
|
|
@@ -1719,8 +1611,8 @@ function ensureStableClaudeBinary() {
|
|
|
1719
1611
|
}
|
|
1720
1612
|
|
|
1721
1613
|
// src/config.ts
|
|
1722
|
-
import * as
|
|
1723
|
-
import * as
|
|
1614
|
+
import * as fs3 from "fs";
|
|
1615
|
+
import * as path3 from "path";
|
|
1724
1616
|
var LITELLM_DEFAULT_PORT = 4e3;
|
|
1725
1617
|
var LITELLM_DEFAULT_URL = `http://localhost:${LITELLM_DEFAULT_PORT}`;
|
|
1726
1618
|
function parseProviderModel(s) {
|
|
@@ -1738,13 +1630,13 @@ function needsLitellmProxy(model) {
|
|
|
1738
1630
|
return model.provider !== "claude" && model.provider !== "anthropic";
|
|
1739
1631
|
}
|
|
1740
1632
|
function loadConfig(projectDir = process.cwd()) {
|
|
1741
|
-
const configPath =
|
|
1742
|
-
if (!
|
|
1633
|
+
const configPath = path3.join(projectDir, "kody.config.json");
|
|
1634
|
+
if (!fs3.existsSync(configPath)) {
|
|
1743
1635
|
throw new Error(`kody.config.json not found at ${configPath}`);
|
|
1744
1636
|
}
|
|
1745
1637
|
let raw;
|
|
1746
1638
|
try {
|
|
1747
|
-
raw = JSON.parse(
|
|
1639
|
+
raw = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
|
|
1748
1640
|
} catch (err) {
|
|
1749
1641
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1750
1642
|
throw new Error(`kody.config.json is invalid JSON: ${msg}`);
|
|
@@ -1781,6 +1673,7 @@ function loadConfig(projectDir = process.cwd()) {
|
|
|
1781
1673
|
testRequirements: parseTestRequirements(raw.testRequirements),
|
|
1782
1674
|
defaultExecutable: typeof raw.defaultExecutable === "string" && raw.defaultExecutable.length > 0 ? raw.defaultExecutable : "run",
|
|
1783
1675
|
defaultPrExecutable: typeof raw.defaultPrExecutable === "string" && raw.defaultPrExecutable.length > 0 ? raw.defaultPrExecutable : "fix",
|
|
1676
|
+
onPullRequest: typeof raw.onPullRequest === "string" && raw.onPullRequest.length > 0 ? raw.onPullRequest : void 0,
|
|
1784
1677
|
aliases: mergeAliases(raw.aliases),
|
|
1785
1678
|
classify: parseClassifyConfig(raw.classify),
|
|
1786
1679
|
release: parseReleaseConfig(raw.release),
|
|
@@ -2062,9 +1955,9 @@ function toolMayMutate(name, input) {
|
|
|
2062
1955
|
return false;
|
|
2063
1956
|
}
|
|
2064
1957
|
async function runAgent(opts) {
|
|
2065
|
-
const ndjsonDir = opts.ndjsonDir ??
|
|
2066
|
-
|
|
2067
|
-
const ndjsonPath =
|
|
1958
|
+
const ndjsonDir = opts.ndjsonDir ?? path6.join(opts.cwd, ".kody");
|
|
1959
|
+
fs6.mkdirSync(ndjsonDir, { recursive: true });
|
|
1960
|
+
const ndjsonPath = path6.join(ndjsonDir, "last-run.jsonl");
|
|
2068
1961
|
const env = {
|
|
2069
1962
|
...process.env,
|
|
2070
1963
|
SKIP_HOOKS: "1",
|
|
@@ -2093,7 +1986,7 @@ async function runAgent(opts) {
|
|
|
2093
1986
|
let finalText = "";
|
|
2094
1987
|
let getSubmitted;
|
|
2095
1988
|
for (let attempt = 0; ; attempt++) {
|
|
2096
|
-
const fullLog =
|
|
1989
|
+
const fullLog = fs6.createWriteStream(ndjsonPath, { flags: "w" });
|
|
2097
1990
|
const resultTexts = [];
|
|
2098
1991
|
outcome = "failed";
|
|
2099
1992
|
outcomeKind = "generic_failed";
|
|
@@ -2390,48 +2283,48 @@ async function runAgent(opts) {
|
|
|
2390
2283
|
}
|
|
2391
2284
|
|
|
2392
2285
|
// src/registry.ts
|
|
2393
|
-
import * as
|
|
2394
|
-
import * as
|
|
2286
|
+
import * as fs7 from "fs";
|
|
2287
|
+
import * as path7 from "path";
|
|
2395
2288
|
function getExecutablesRoot() {
|
|
2396
|
-
const here =
|
|
2289
|
+
const here = path7.dirname(new URL(import.meta.url).pathname);
|
|
2397
2290
|
const candidates = [
|
|
2398
|
-
|
|
2291
|
+
path7.join(here, "executables"),
|
|
2399
2292
|
// dev: src/
|
|
2400
|
-
|
|
2293
|
+
path7.join(here, "..", "executables"),
|
|
2401
2294
|
// built: dist/bin → dist/executables
|
|
2402
|
-
|
|
2295
|
+
path7.join(here, "..", "src", "executables")
|
|
2403
2296
|
// fallback
|
|
2404
2297
|
];
|
|
2405
2298
|
for (const c of candidates) {
|
|
2406
|
-
if (
|
|
2299
|
+
if (fs7.existsSync(c) && fs7.statSync(c).isDirectory()) return c;
|
|
2407
2300
|
}
|
|
2408
2301
|
return candidates[0];
|
|
2409
2302
|
}
|
|
2410
2303
|
function getProjectExecutablesRoot() {
|
|
2411
|
-
return
|
|
2304
|
+
return path7.join(process.cwd(), ".kody", "executables");
|
|
2412
2305
|
}
|
|
2413
2306
|
function getBuiltinJobsRoot() {
|
|
2414
|
-
const here =
|
|
2307
|
+
const here = path7.dirname(new URL(import.meta.url).pathname);
|
|
2415
2308
|
const candidates = [
|
|
2416
|
-
|
|
2309
|
+
path7.join(here, "jobs"),
|
|
2417
2310
|
// dev: src/
|
|
2418
|
-
|
|
2311
|
+
path7.join(here, "..", "jobs"),
|
|
2419
2312
|
// built: dist/bin → dist/jobs
|
|
2420
|
-
|
|
2313
|
+
path7.join(here, "..", "src", "jobs")
|
|
2421
2314
|
// fallback
|
|
2422
2315
|
];
|
|
2423
2316
|
for (const c of candidates) {
|
|
2424
|
-
if (
|
|
2317
|
+
if (fs7.existsSync(c) && fs7.statSync(c).isDirectory()) return c;
|
|
2425
2318
|
}
|
|
2426
2319
|
return candidates[0];
|
|
2427
2320
|
}
|
|
2428
2321
|
function listBuiltinJobs(root = getBuiltinJobsRoot()) {
|
|
2429
|
-
if (!
|
|
2322
|
+
if (!fs7.existsSync(root) || !fs7.statSync(root).isDirectory()) return [];
|
|
2430
2323
|
const out = [];
|
|
2431
|
-
for (const ent of
|
|
2324
|
+
for (const ent of fs7.readdirSync(root, { withFileTypes: true })) {
|
|
2432
2325
|
if (!ent.isFile() || !ent.name.endsWith(".md")) continue;
|
|
2433
2326
|
const slug = ent.name.slice(0, -3);
|
|
2434
|
-
out.push({ slug, filePath:
|
|
2327
|
+
out.push({ slug, filePath: path7.join(root, ent.name) });
|
|
2435
2328
|
}
|
|
2436
2329
|
out.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
2437
2330
|
return out;
|
|
@@ -2444,13 +2337,13 @@ function listExecutables(roots = getExecutableRoots()) {
|
|
|
2444
2337
|
const seen = /* @__PURE__ */ new Set();
|
|
2445
2338
|
const out = [];
|
|
2446
2339
|
for (const root of rootList) {
|
|
2447
|
-
if (!
|
|
2448
|
-
const entries =
|
|
2340
|
+
if (!fs7.existsSync(root)) continue;
|
|
2341
|
+
const entries = fs7.readdirSync(root, { withFileTypes: true });
|
|
2449
2342
|
for (const ent of entries) {
|
|
2450
2343
|
if (!ent.isDirectory()) continue;
|
|
2451
2344
|
if (seen.has(ent.name)) continue;
|
|
2452
|
-
const profilePath =
|
|
2453
|
-
if (
|
|
2345
|
+
const profilePath = path7.join(root, ent.name, "profile.json");
|
|
2346
|
+
if (fs7.existsSync(profilePath) && fs7.statSync(profilePath).isFile()) {
|
|
2454
2347
|
out.push({ name: ent.name, profilePath });
|
|
2455
2348
|
seen.add(ent.name);
|
|
2456
2349
|
}
|
|
@@ -2462,8 +2355,8 @@ function resolveExecutable(name, roots = getExecutableRoots()) {
|
|
|
2462
2355
|
if (!isSafeName(name)) return null;
|
|
2463
2356
|
const rootList = typeof roots === "string" ? [roots] : roots;
|
|
2464
2357
|
for (const root of rootList) {
|
|
2465
|
-
const profilePath =
|
|
2466
|
-
if (
|
|
2358
|
+
const profilePath = path7.join(root, name, "profile.json");
|
|
2359
|
+
if (fs7.existsSync(profilePath) && fs7.statSync(profilePath).isFile()) {
|
|
2467
2360
|
return profilePath;
|
|
2468
2361
|
}
|
|
2469
2362
|
}
|
|
@@ -2479,7 +2372,7 @@ function getProfileInputs(name, roots = getExecutableRoots()) {
|
|
|
2479
2372
|
const profilePath = resolveExecutable(name, roots);
|
|
2480
2373
|
if (!profilePath) return null;
|
|
2481
2374
|
try {
|
|
2482
|
-
const raw = JSON.parse(
|
|
2375
|
+
const raw = JSON.parse(fs7.readFileSync(profilePath, "utf-8"));
|
|
2483
2376
|
if (!raw || typeof raw !== "object" || !Array.isArray(raw.inputs)) return [];
|
|
2484
2377
|
return raw.inputs;
|
|
2485
2378
|
} catch {
|
|
@@ -2497,7 +2390,11 @@ function parseGenericFlags(argv) {
|
|
|
2497
2390
|
}
|
|
2498
2391
|
const key = arg.slice(2);
|
|
2499
2392
|
const next = argv[i + 1];
|
|
2500
|
-
|
|
2393
|
+
let value = true;
|
|
2394
|
+
if (next !== void 0 && !next.startsWith("--")) {
|
|
2395
|
+
value = next;
|
|
2396
|
+
i++;
|
|
2397
|
+
}
|
|
2501
2398
|
args[key] = value;
|
|
2502
2399
|
if (key.includes("-")) {
|
|
2503
2400
|
const camel = key.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
@@ -2508,70 +2405,92 @@ function parseGenericFlags(argv) {
|
|
|
2508
2405
|
return args;
|
|
2509
2406
|
}
|
|
2510
2407
|
|
|
2511
|
-
// src/
|
|
2512
|
-
import
|
|
2513
|
-
import
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
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
|
-
}
|
|
2408
|
+
// src/task-artifacts.ts
|
|
2409
|
+
import fs8 from "fs";
|
|
2410
|
+
import path8 from "path";
|
|
2411
|
+
var TASK_ARTIFACT_FILES = ["context.json", "memory-recs.json", "followups.json", "handoff-notes.md"];
|
|
2412
|
+
function prepareTaskArtifactsDir(cwd, taskId) {
|
|
2413
|
+
const safeId = String(taskId).replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
2414
|
+
const relDir = path8.join(".kody", "tasks", safeId);
|
|
2415
|
+
const absDir = path8.join(cwd, relDir);
|
|
2416
|
+
fs8.mkdirSync(absDir, { recursive: true });
|
|
2417
|
+
return { taskId: safeId, absDir, relDir };
|
|
2530
2418
|
}
|
|
2531
|
-
function
|
|
2532
|
-
|
|
2533
|
-
const
|
|
2534
|
-
|
|
2535
|
-
const turns = [];
|
|
2536
|
-
for (const line of raw.split("\n")) {
|
|
2537
|
-
if (!line.trim()) continue;
|
|
2419
|
+
function verifyTaskArtifacts(absDir) {
|
|
2420
|
+
const missing = [];
|
|
2421
|
+
for (const name of TASK_ARTIFACT_FILES) {
|
|
2422
|
+
const full = path8.join(absDir, name);
|
|
2538
2423
|
try {
|
|
2539
|
-
const
|
|
2540
|
-
if (
|
|
2541
|
-
if (typeof parsed.content !== "string") continue;
|
|
2542
|
-
turns.push(parsed);
|
|
2424
|
+
const stat = fs8.statSync(full);
|
|
2425
|
+
if (!stat.isFile() || stat.size === 0) missing.push(name);
|
|
2543
2426
|
} catch {
|
|
2427
|
+
missing.push(name);
|
|
2544
2428
|
}
|
|
2545
2429
|
}
|
|
2546
|
-
return
|
|
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
|
-
`);
|
|
2430
|
+
return missing;
|
|
2558
2431
|
}
|
|
2559
|
-
function
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2432
|
+
function taskArtifactsPromptAddendum(opts) {
|
|
2433
|
+
return [
|
|
2434
|
+
"## Per-task artifacts (REQUIRED before your final response)",
|
|
2435
|
+
"",
|
|
2436
|
+
`Before you finish, write these four files into \`${opts.relDir}/\`:`,
|
|
2437
|
+
"",
|
|
2438
|
+
`1. **context.json** \u2014 task header. Shape:`,
|
|
2439
|
+
" ```json",
|
|
2440
|
+
" {",
|
|
2441
|
+
` "taskId": "${opts.taskId}",`,
|
|
2442
|
+
` "taskType": "${opts.taskType}",`,
|
|
2443
|
+
` "target": "<issue/PR number, session id, or job slug>",`,
|
|
2444
|
+
` "outcome": "success" | "failure" | "partial",`,
|
|
2445
|
+
` "exitCode": <number>,`,
|
|
2446
|
+
` "reason": "<one-line summary of why you exited>",`,
|
|
2447
|
+
` "prUrl": "<url or null>",`,
|
|
2448
|
+
` "runUrl": "<url or null>",`,
|
|
2449
|
+
` "filesTouched": ["path/from/repo/root.ts", ...],`,
|
|
2450
|
+
` "sessionLog": ".kody/sessions/<id>.jsonl",`,
|
|
2451
|
+
` "startedAt": "<ISO>",`,
|
|
2452
|
+
` "finishedAt": "<ISO>"`,
|
|
2453
|
+
" }",
|
|
2454
|
+
" ```",
|
|
2455
|
+
"",
|
|
2456
|
+
`2. **memory-recs.json** \u2014 array of sticky-note candidates worth promoting`,
|
|
2457
|
+
` to long-term \`.kody/memory/\`. Each item:`,
|
|
2458
|
+
" ```json",
|
|
2459
|
+
" {",
|
|
2460
|
+
` "type": "preference" | "decision" | "lesson",`,
|
|
2461
|
+
` "name": "kebab-case-slug",`,
|
|
2462
|
+
` "hook": "one-line summary for INDEX.md",`,
|
|
2463
|
+
` "body": "markdown body \u2014 explain the rule plus a Why: line",`,
|
|
2464
|
+
` "why": "the load-bearing reason a future session needs this",`,
|
|
2465
|
+
` "confidence": 0.0 to 1.0`,
|
|
2466
|
+
" }",
|
|
2467
|
+
" ```",
|
|
2468
|
+
` Use \`[]\` if nothing in this task is worth remembering. Forced`,
|
|
2469
|
+
` filler is worse than nothing \u2014 only record what would be lost`,
|
|
2470
|
+
` otherwise.`,
|
|
2471
|
+
"",
|
|
2472
|
+
`3. **followups.json** \u2014 array of TODOs uncovered but not fixed.`,
|
|
2473
|
+
" ```json",
|
|
2474
|
+
" {",
|
|
2475
|
+
` "title": "short summary",`,
|
|
2476
|
+
` "body": "what the operator should do, and where",`,
|
|
2477
|
+
` "rationale": "why this matters",`,
|
|
2478
|
+
` "priority": "low" | "medium" | "high"`,
|
|
2479
|
+
" }",
|
|
2480
|
+
" ```",
|
|
2481
|
+
` Use \`[]\` if nothing surfaced.`,
|
|
2482
|
+
"",
|
|
2483
|
+
`4. **handoff-notes.md** \u2014 short prose (\u2264200 words), no frontmatter:`,
|
|
2484
|
+
` what you did and why, so the next person/agent can pick up cold.`,
|
|
2485
|
+
"",
|
|
2486
|
+
"Skipping any of the four files is an error. Empty arrays are fine;",
|
|
2487
|
+
"skipping the file is not."
|
|
2488
|
+
].join("\n");
|
|
2570
2489
|
}
|
|
2571
2490
|
|
|
2572
2491
|
// src/chat/attachments.ts
|
|
2573
|
-
import * as
|
|
2574
|
-
import * as
|
|
2492
|
+
import * as fs9 from "fs";
|
|
2493
|
+
import * as path9 from "path";
|
|
2575
2494
|
var INLINE_ATTACHMENT_RE = /(?:\[(?:Image|File): ([^\]]*)\]\n)?data:([\w.+-]+\/[\w.+-]+);base64,([A-Za-z0-9+/=]+)/g;
|
|
2576
2495
|
var EXT_BY_MIME = {
|
|
2577
2496
|
"image/png": "png",
|
|
@@ -2587,7 +2506,7 @@ function extFor(mime) {
|
|
|
2587
2506
|
return EXT_BY_MIME[mime.toLowerCase()] ?? mime.split("/")[1]?.replace(/[^\w]/g, "") ?? "bin";
|
|
2588
2507
|
}
|
|
2589
2508
|
function attachmentsDir(cwd, sessionId) {
|
|
2590
|
-
return
|
|
2509
|
+
return path9.join(cwd, ".kody", "tmp", "attachments", sessionId);
|
|
2591
2510
|
}
|
|
2592
2511
|
function prepareAttachments(turns, cwd, sessionId) {
|
|
2593
2512
|
const imagePaths = [];
|
|
@@ -2604,11 +2523,11 @@ function prepareAttachments(turns, cwd, sessionId) {
|
|
|
2604
2523
|
if (!isImage) return `[File: ${name}]`;
|
|
2605
2524
|
try {
|
|
2606
2525
|
if (!dirEnsured) {
|
|
2607
|
-
|
|
2526
|
+
fs9.mkdirSync(dir, { recursive: true });
|
|
2608
2527
|
dirEnsured = true;
|
|
2609
2528
|
}
|
|
2610
|
-
const filePath =
|
|
2611
|
-
|
|
2529
|
+
const filePath = path9.join(dir, `${imageCounter}.${extFor(mime)}`);
|
|
2530
|
+
fs9.writeFileSync(filePath, Buffer.from(data, "base64"));
|
|
2612
2531
|
imageCounter += 1;
|
|
2613
2532
|
imagePaths.push(filePath);
|
|
2614
2533
|
return `[Image "${name}" is attached \u2014 saved to ${filePath}. Use the Read tool on that exact path to view it.]`;
|
|
@@ -2623,42 +2542,103 @@ function prepareAttachments(turns, cwd, sessionId) {
|
|
|
2623
2542
|
return { turns: rewritten, imagePaths };
|
|
2624
2543
|
}
|
|
2625
2544
|
|
|
2626
|
-
// src/chat/
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
""
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2545
|
+
// src/chat/session.ts
|
|
2546
|
+
import * as fs10 from "fs";
|
|
2547
|
+
import * as path10 from "path";
|
|
2548
|
+
function sessionFilePath(cwd, sessionId) {
|
|
2549
|
+
return path10.join(cwd, ".kody", "sessions", `${sessionId}.jsonl`);
|
|
2550
|
+
}
|
|
2551
|
+
function readMeta(file) {
|
|
2552
|
+
if (!fs10.existsSync(file)) return null;
|
|
2553
|
+
const raw = fs10.readFileSync(file, "utf-8");
|
|
2554
|
+
const firstLine2 = raw.split("\n", 1)[0]?.trim();
|
|
2555
|
+
if (!firstLine2) return null;
|
|
2556
|
+
try {
|
|
2557
|
+
const parsed = JSON.parse(firstLine2);
|
|
2558
|
+
if (parsed.type !== "meta") return null;
|
|
2559
|
+
if (parsed.mode !== "one-shot" && parsed.mode !== "interactive") return null;
|
|
2560
|
+
return parsed;
|
|
2561
|
+
} catch {
|
|
2562
|
+
return null;
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
function readSession(file) {
|
|
2566
|
+
if (!fs10.existsSync(file)) return [];
|
|
2567
|
+
const raw = fs10.readFileSync(file, "utf-8").trim();
|
|
2568
|
+
if (!raw) return [];
|
|
2569
|
+
const turns = [];
|
|
2570
|
+
for (const line of raw.split("\n")) {
|
|
2571
|
+
if (!line.trim()) continue;
|
|
2572
|
+
try {
|
|
2573
|
+
const parsed = JSON.parse(line);
|
|
2574
|
+
if (parsed.role !== "user" && parsed.role !== "assistant") continue;
|
|
2575
|
+
if (typeof parsed.content !== "string") continue;
|
|
2576
|
+
turns.push(parsed);
|
|
2577
|
+
} catch {
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
return turns;
|
|
2581
|
+
}
|
|
2582
|
+
function appendTurn(file, turn) {
|
|
2583
|
+
fs10.mkdirSync(path10.dirname(file), { recursive: true });
|
|
2584
|
+
const line = JSON.stringify({
|
|
2585
|
+
role: turn.role,
|
|
2586
|
+
content: turn.content,
|
|
2587
|
+
timestamp: turn.timestamp,
|
|
2588
|
+
toolCalls: turn.toolCalls ?? []
|
|
2589
|
+
});
|
|
2590
|
+
fs10.appendFileSync(file, `${line}
|
|
2591
|
+
`);
|
|
2592
|
+
}
|
|
2593
|
+
function seedInitialMessage(file, message) {
|
|
2594
|
+
if (!message.trim()) return false;
|
|
2595
|
+
const turns = readSession(file);
|
|
2596
|
+
const lastUser = [...turns].reverse().find((t) => t.role === "user");
|
|
2597
|
+
if (lastUser && lastUser.content === message) return false;
|
|
2598
|
+
appendTurn(file, {
|
|
2599
|
+
role: "user",
|
|
2600
|
+
content: message,
|
|
2601
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2602
|
+
});
|
|
2603
|
+
return true;
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
// src/chat/loop.ts
|
|
2607
|
+
var CHAT_SYSTEM_PROMPT = [
|
|
2608
|
+
"You are Kody, an AI assistant for the Kody Operations Dashboard. Reply to the",
|
|
2609
|
+
"user's latest message using the full conversation below as context. Keep replies",
|
|
2610
|
+
"short and simple. Prefer one-liners and short paragraphs. Use plain terms, not jargon.",
|
|
2611
|
+
"When you diagnose something, answer in this shape: a few words on the issue, a",
|
|
2612
|
+
"few words on the fix, then a single question asking whether to proceed. Do not",
|
|
2613
|
+
"pad it with preamble, restated context, or a trailing summary.",
|
|
2614
|
+
"",
|
|
2615
|
+
"# Your environment and capabilities",
|
|
2616
|
+
"You run inside a sandboxed runner with a full clone of the user's repository",
|
|
2617
|
+
"checked out at the current working directory. The runtime varies \u2014 it may be a",
|
|
2618
|
+
"GitHub Actions job, a Fly Machine, or another container \u2014 but the tools and",
|
|
2619
|
+
"capabilities below are identical across runtimes. Use the actual environment",
|
|
2620
|
+
"(e.g. `uname`, `pwd`, `env`) to verify before claiming where you run. You have",
|
|
2621
|
+
"real tools \u2014 use them before claiming you cannot do something. Never tell the",
|
|
2622
|
+
"user you lack repo, filesystem, or GitHub access; you have all three.",
|
|
2623
|
+
"",
|
|
2624
|
+
"Tools you can call:",
|
|
2625
|
+
"- Read, Edit, Write \u2014 full read/write access to every file in the repo (permission",
|
|
2626
|
+
" mode is acceptEdits, so writes do not require confirmation).",
|
|
2627
|
+
"- Glob, Grep \u2014 search the repo by filename pattern or content.",
|
|
2628
|
+
"- Bash \u2014 run any shell command in the repo. The runner has:",
|
|
2629
|
+
" - `git` (the repo is a real git checkout \u2014 `git log`, `git diff`,",
|
|
2630
|
+
" `git show`, `git blame`, `git branch`, etc. all work).",
|
|
2631
|
+
" - `gh` authenticated against this repository's GitHub via a `GITHUB_TOKEN`",
|
|
2632
|
+
" env var (read issues, PRs, workflows, runs, comments; query the API",
|
|
2633
|
+
" with `gh api`).",
|
|
2634
|
+
" - the repo's package manager and test/build/lint tooling (npm/pnpm/yarn,",
|
|
2635
|
+
" pytest, go test, cargo, etc., whatever the project uses).",
|
|
2636
|
+
" - standard Unix utilities (curl, jq, sed, awk, find, etc.).",
|
|
2637
|
+
"",
|
|
2638
|
+
"The repo's configured secrets are in the environment \u2014 check `env` before",
|
|
2639
|
+
"claiming you lack a credential. Never print a secret's value.",
|
|
2640
|
+
"",
|
|
2641
|
+
"# Clarify before you act (HARD RULE)",
|
|
2662
2642
|
"If the user's request is ambiguous or under-specified \u2014 you can read it two",
|
|
2663
2643
|
"plausible ways, or you'd have to guess what they actually want \u2014 ask",
|
|
2664
2644
|
"clarifying questions and stop. Ask as many as you genuinely need; do NOT",
|
|
@@ -2934,7 +2914,9 @@ ${content}`);
|
|
|
2934
2914
|
}
|
|
2935
2915
|
const joined = sections.join("\n\n").trim();
|
|
2936
2916
|
if (!joined) return "";
|
|
2937
|
-
const body = joined.length > MAX_CONTEXT_BYTES ? joined.slice(0, MAX_CONTEXT_BYTES)
|
|
2917
|
+
const body = joined.length > MAX_CONTEXT_BYTES ? `${joined.slice(0, MAX_CONTEXT_BYTES)}
|
|
2918
|
+
|
|
2919
|
+
_\u2026 (context truncated; see \`.kody/context/\` for the full text)_` : joined;
|
|
2938
2920
|
return [
|
|
2939
2921
|
"# Context (`.kody/context/`) \u2014 your default frame",
|
|
2940
2922
|
"",
|
|
@@ -2955,7 +2937,9 @@ function readInstructionsBlock(cwd) {
|
|
|
2955
2937
|
}
|
|
2956
2938
|
const trimmed = raw.trim();
|
|
2957
2939
|
if (!trimmed) return "";
|
|
2958
|
-
const body = trimmed.length > MAX_INSTRUCTIONS_BYTES ? trimmed.slice(0, MAX_INSTRUCTIONS_BYTES)
|
|
2940
|
+
const body = trimmed.length > MAX_INSTRUCTIONS_BYTES ? `${trimmed.slice(0, MAX_INSTRUCTIONS_BYTES)}
|
|
2941
|
+
|
|
2942
|
+
_\u2026 (instructions truncated)_` : trimmed;
|
|
2959
2943
|
return [
|
|
2960
2944
|
"# User instructions for this repo (`.kody/instructions.md`)",
|
|
2961
2945
|
"",
|
|
@@ -3172,7 +3156,8 @@ function putJsonlViaContents(repository, branch, repoPath, localText, sessionId,
|
|
|
3172
3156
|
const localLines = jsonlLines(localText);
|
|
3173
3157
|
const localSet = new Set(localLines);
|
|
3174
3158
|
const extra = remote.lines.filter((l) => !localSet.has(l));
|
|
3175
|
-
if (extra.length > 0) body = [...localLines, ...extra].join("\n")
|
|
3159
|
+
if (extra.length > 0) body = `${[...localLines, ...extra].join("\n")}
|
|
3160
|
+
`;
|
|
3176
3161
|
}
|
|
3177
3162
|
const payload = {
|
|
3178
3163
|
message: `chat: interactive turn for ${sessionId}`,
|
|
@@ -3372,18 +3357,7 @@ function cronMatchesInWindow(spec, end, windowSec) {
|
|
|
3372
3357
|
}
|
|
3373
3358
|
|
|
3374
3359
|
// 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
|
-
]);
|
|
3360
|
+
var POLITE_WORDS = /* @__PURE__ */ new Set(["please", "kindly", "hi", "hey", "hello", "thanks", "thank", "plz", "pls", "yo"]);
|
|
3387
3361
|
function primaryNumericInputName(executable) {
|
|
3388
3362
|
const inputs = getProfileInputs(executable);
|
|
3389
3363
|
if (!inputs) return null;
|
|
@@ -3417,7 +3391,18 @@ function autoDispatch(opts) {
|
|
|
3417
3391
|
return null;
|
|
3418
3392
|
}
|
|
3419
3393
|
if (eventName === "schedule") return null;
|
|
3420
|
-
if (eventName === "pull_request")
|
|
3394
|
+
if (eventName === "pull_request") {
|
|
3395
|
+
const exe = opts?.config?.onPullRequest?.trim();
|
|
3396
|
+
const action = String(event.action ?? "");
|
|
3397
|
+
if (exe && (action === "opened" || action === "synchronize" || action === "reopened")) {
|
|
3398
|
+
const prNum = Number(event.pull_request?.number ?? event.number ?? 0);
|
|
3399
|
+
if (prNum > 0) {
|
|
3400
|
+
const targetKey = primaryNumericInputName(exe) ?? "pr";
|
|
3401
|
+
return { executable: exe, cliArgs: { [targetKey]: prNum }, target: prNum };
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
return null;
|
|
3405
|
+
}
|
|
3421
3406
|
if (eventName !== "issue_comment") return null;
|
|
3422
3407
|
const rawBody = String(event.comment?.body ?? "");
|
|
3423
3408
|
const authorLogin = String(event.comment?.user?.login ?? "");
|
|
@@ -3519,7 +3504,10 @@ function autoDispatchTyped(opts) {
|
|
|
3519
3504
|
const afterTag = extractAfterTag(rawBody.toLowerCase());
|
|
3520
3505
|
const tokenRaw = extractSubcommand(afterTag) ?? "";
|
|
3521
3506
|
if (!tokenRaw || POLITE_WORDS.has(tokenRaw)) {
|
|
3522
|
-
return {
|
|
3507
|
+
return {
|
|
3508
|
+
kind: "silent",
|
|
3509
|
+
reason: tokenRaw ? `polite-word lead-in '${tokenRaw}', no default executable configured` : "no subcommand token, no default executable configured"
|
|
3510
|
+
};
|
|
3523
3511
|
}
|
|
3524
3512
|
const available = listExecutables().map((e) => e.name).filter((n) => !n.startsWith("goal-") && !n.startsWith("job-")).sort();
|
|
3525
3513
|
return { kind: "unrecognized", token: tokenRaw, target: targetNum, isPr, available };
|
|
@@ -3745,7 +3733,10 @@ function validateConfig(raw, profilePath) {
|
|
|
3745
3733
|
const lbl = label;
|
|
3746
3734
|
for (const k of ["name", "color", "description"]) {
|
|
3747
3735
|
if (typeof lbl[k] !== "string" || lbl[k].length === 0) {
|
|
3748
|
-
throw new ProfileError(
|
|
3736
|
+
throw new ProfileError(
|
|
3737
|
+
profilePath,
|
|
3738
|
+
`lifecycle "pr-branch": lifecycleConfig.label.${k} must be a non-empty string`
|
|
3739
|
+
);
|
|
3749
3740
|
}
|
|
3750
3741
|
}
|
|
3751
3742
|
const context = raw.context === void 0 ? "task" : raw.context;
|
|
@@ -4256,7 +4247,9 @@ function parseStateComment(body) {
|
|
|
4256
4247
|
try {
|
|
4257
4248
|
parsed = JSON.parse(jsonStr);
|
|
4258
4249
|
} catch (err) {
|
|
4259
|
-
throw new CorruptStateError(
|
|
4250
|
+
throw new CorruptStateError(
|
|
4251
|
+
`state JSON unparseable (truncated comment?): ${err instanceof Error ? err.message : String(err)}`
|
|
4252
|
+
);
|
|
4260
4253
|
}
|
|
4261
4254
|
if (parsed?.schemaVersion !== 1) {
|
|
4262
4255
|
throw new CorruptStateError(`unexpected schemaVersion: ${JSON.stringify(parsed?.schemaVersion)}`);
|
|
@@ -4757,7 +4750,7 @@ function extractLabelSpec(entry) {
|
|
|
4757
4750
|
const w = entry.with;
|
|
4758
4751
|
if (!w) return null;
|
|
4759
4752
|
const label = typeof w.label === "string" ? w.label : null;
|
|
4760
|
-
if (!label
|
|
4753
|
+
if (!label?.startsWith(KODY_NAMESPACE)) return null;
|
|
4761
4754
|
return {
|
|
4762
4755
|
label,
|
|
4763
4756
|
color: typeof w.color === "string" ? w.color : void 0,
|
|
@@ -5001,139 +4994,6 @@ function stripBlockingEnv(env) {
|
|
|
5001
4994
|
return out;
|
|
5002
4995
|
}
|
|
5003
4996
|
|
|
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
4997
|
// src/commit.ts
|
|
5138
4998
|
import { execFileSync as execFileSync8 } from "child_process";
|
|
5139
4999
|
|
|
@@ -5164,7 +5024,7 @@ function runGit(args, cwd) {
|
|
|
5164
5024
|
}
|
|
5165
5025
|
}
|
|
5166
5026
|
function resolveBranch(cwd, explicit) {
|
|
5167
|
-
if (explicit
|
|
5027
|
+
if (explicit?.trim()) return explicit.trim();
|
|
5168
5028
|
const r = runGit(["symbolic-ref", "--short", "HEAD"], cwd);
|
|
5169
5029
|
return r.ok ? r.stdout.trim() : "";
|
|
5170
5030
|
}
|
|
@@ -5214,8 +5074,8 @@ function pushWithRetry(opts = {}) {
|
|
|
5214
5074
|
}
|
|
5215
5075
|
|
|
5216
5076
|
// src/commit.ts
|
|
5217
|
-
import * as
|
|
5218
|
-
import * as
|
|
5077
|
+
import * as fs19 from "fs";
|
|
5078
|
+
import * as path17 from "path";
|
|
5219
5079
|
var FORBIDDEN_PATH_PREFIXES = [
|
|
5220
5080
|
".kody/",
|
|
5221
5081
|
".kody-engine/",
|
|
@@ -5276,18 +5136,18 @@ function tryGit(args, cwd) {
|
|
|
5276
5136
|
}
|
|
5277
5137
|
function abortUnfinishedGitOps(cwd) {
|
|
5278
5138
|
const aborted = [];
|
|
5279
|
-
const gitDir =
|
|
5280
|
-
if (!
|
|
5281
|
-
if (
|
|
5139
|
+
const gitDir = path17.join(cwd ?? process.cwd(), ".git");
|
|
5140
|
+
if (!fs19.existsSync(gitDir)) return aborted;
|
|
5141
|
+
if (fs19.existsSync(path17.join(gitDir, "MERGE_HEAD"))) {
|
|
5282
5142
|
if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
|
|
5283
5143
|
}
|
|
5284
|
-
if (
|
|
5144
|
+
if (fs19.existsSync(path17.join(gitDir, "CHERRY_PICK_HEAD"))) {
|
|
5285
5145
|
if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
|
|
5286
5146
|
}
|
|
5287
|
-
if (
|
|
5147
|
+
if (fs19.existsSync(path17.join(gitDir, "REVERT_HEAD"))) {
|
|
5288
5148
|
if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
|
|
5289
5149
|
}
|
|
5290
|
-
if (
|
|
5150
|
+
if (fs19.existsSync(path17.join(gitDir, "rebase-merge")) || fs19.existsSync(path17.join(gitDir, "rebase-apply"))) {
|
|
5291
5151
|
if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
|
|
5292
5152
|
}
|
|
5293
5153
|
try {
|
|
@@ -5343,7 +5203,7 @@ function normalizeCommitMessage(raw) {
|
|
|
5343
5203
|
function commitAndPush(branch, agentMessage, cwd) {
|
|
5344
5204
|
const allChanged = listChangedFiles(cwd);
|
|
5345
5205
|
const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
|
|
5346
|
-
const mergeHeadExists =
|
|
5206
|
+
const mergeHeadExists = fs19.existsSync(path17.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
|
|
5347
5207
|
if (allowedFiles.length === 0 && !mergeHeadExists) {
|
|
5348
5208
|
return { committed: false, pushed: false, sha: "", message: "" };
|
|
5349
5209
|
}
|
|
@@ -5473,49 +5333,216 @@ var advanceFlow = async (ctx, profile) => {
|
|
|
5473
5333
|
ctx.output.nextDispatch = { executable: flow.name, cliArgs: { issue: flow.issueNumber } };
|
|
5474
5334
|
};
|
|
5475
5335
|
|
|
5476
|
-
// src/
|
|
5477
|
-
|
|
5478
|
-
import
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
var live = /* @__PURE__ */ new Map();
|
|
5486
|
-
function eventsPath(dir, chatId) {
|
|
5487
|
-
return path20.join(dir, ".kody", "brain-events", `${chatId}.jsonl`);
|
|
5336
|
+
// src/gha.ts
|
|
5337
|
+
import { execFileSync as execFileSync10 } from "child_process";
|
|
5338
|
+
import * as fs20 from "fs";
|
|
5339
|
+
function getRunUrl() {
|
|
5340
|
+
const server = process.env.GITHUB_SERVER_URL;
|
|
5341
|
+
const repo = process.env.GITHUB_REPOSITORY;
|
|
5342
|
+
const runId = process.env.GITHUB_RUN_ID;
|
|
5343
|
+
if (!server || !repo || !runId) return "";
|
|
5344
|
+
return `${server}/${repo}/actions/runs/${runId}`;
|
|
5488
5345
|
}
|
|
5489
|
-
function
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5346
|
+
function reactToTriggerComment(cwd) {
|
|
5347
|
+
if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
|
|
5348
|
+
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
5349
|
+
if (!eventPath || !fs20.existsSync(eventPath)) return;
|
|
5350
|
+
let event = null;
|
|
5494
5351
|
try {
|
|
5495
|
-
|
|
5352
|
+
event = JSON.parse(fs20.readFileSync(eventPath, "utf-8"));
|
|
5496
5353
|
} catch {
|
|
5497
|
-
return
|
|
5498
|
-
}
|
|
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;
|
|
5506
|
-
try {
|
|
5507
|
-
const rec = JSON.parse(line);
|
|
5508
|
-
if (rec.seq > since) out.push(rec);
|
|
5509
|
-
} catch {
|
|
5510
|
-
}
|
|
5354
|
+
return;
|
|
5511
5355
|
}
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5356
|
+
const commentId = event?.comment?.id;
|
|
5357
|
+
const repo = process.env.GITHUB_REPOSITORY;
|
|
5358
|
+
if (!commentId || !repo) return;
|
|
5359
|
+
const token = process.env.KODY_TOKEN?.trim() || process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
|
5360
|
+
const args = [
|
|
5361
|
+
"api",
|
|
5362
|
+
"-X",
|
|
5363
|
+
"POST",
|
|
5364
|
+
"-H",
|
|
5365
|
+
"Accept: application/vnd.github+json",
|
|
5366
|
+
`/repos/${repo}/issues/comments/${commentId}/reactions`,
|
|
5367
|
+
"-f",
|
|
5368
|
+
"content=eyes"
|
|
5369
|
+
];
|
|
5370
|
+
const opts = {
|
|
5371
|
+
cwd,
|
|
5372
|
+
env: { ...process.env, GH_TOKEN: token ?? process.env.GH_TOKEN ?? "" },
|
|
5373
|
+
stdio: "pipe",
|
|
5374
|
+
timeout: 15e3
|
|
5375
|
+
};
|
|
5376
|
+
let lastErr = null;
|
|
5377
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
5378
|
+
if (attempt > 0) sleepMs(attempt === 1 ? 500 : 1500);
|
|
5379
|
+
try {
|
|
5380
|
+
execFileSync10("gh", args, opts);
|
|
5381
|
+
return;
|
|
5382
|
+
} catch (err) {
|
|
5383
|
+
lastErr = err;
|
|
5384
|
+
}
|
|
5385
|
+
}
|
|
5386
|
+
process.stderr.write(
|
|
5387
|
+
`[kody] \u{1F440} reaction failed after 3 attempts on comment ${commentId}: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}
|
|
5388
|
+
`
|
|
5389
|
+
);
|
|
5390
|
+
}
|
|
5391
|
+
function sleepMs(ms) {
|
|
5392
|
+
try {
|
|
5393
|
+
execFileSync10("sleep", [(ms / 1e3).toString()], { stdio: "ignore", timeout: ms + 1e3 });
|
|
5394
|
+
} catch {
|
|
5395
|
+
}
|
|
5396
|
+
}
|
|
5397
|
+
|
|
5398
|
+
// src/scripts/appendCompanyActivity.ts
|
|
5399
|
+
init_issue();
|
|
5400
|
+
|
|
5401
|
+
// src/stateBranch.ts
|
|
5402
|
+
init_issue();
|
|
5403
|
+
var STATE_BRANCH = "kody-state";
|
|
5404
|
+
function is404(err) {
|
|
5405
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5406
|
+
return /HTTP 404/i.test(msg) || /Not Found/i.test(msg);
|
|
5407
|
+
}
|
|
5408
|
+
function ensureStateBranch(owner, repo, cwd) {
|
|
5409
|
+
try {
|
|
5410
|
+
gh(["api", `/repos/${owner}/${repo}/git/ref/heads/${STATE_BRANCH}`], { cwd });
|
|
5411
|
+
return;
|
|
5412
|
+
} catch (err) {
|
|
5413
|
+
if (!is404(err)) throw err;
|
|
5414
|
+
}
|
|
5415
|
+
const repoInfo = JSON.parse(gh(["api", `/repos/${owner}/${repo}`], { cwd }));
|
|
5416
|
+
const defaultBranch2 = repoInfo.default_branch;
|
|
5417
|
+
if (!defaultBranch2) {
|
|
5418
|
+
throw new Error(`ensureStateBranch: could not resolve default branch for ${owner}/${repo}`);
|
|
5419
|
+
}
|
|
5420
|
+
const headRef = JSON.parse(gh(["api", `/repos/${owner}/${repo}/git/ref/heads/${defaultBranch2}`], { cwd }));
|
|
5421
|
+
const sha = headRef.object?.sha;
|
|
5422
|
+
if (!sha) {
|
|
5423
|
+
throw new Error(`ensureStateBranch: could not resolve head sha for ${owner}/${repo}@${defaultBranch2}`);
|
|
5424
|
+
}
|
|
5425
|
+
try {
|
|
5426
|
+
gh(["api", "--method", "POST", `/repos/${owner}/${repo}/git/refs`, "--input", "-"], {
|
|
5427
|
+
cwd,
|
|
5428
|
+
input: JSON.stringify({ ref: `refs/heads/${STATE_BRANCH}`, sha })
|
|
5429
|
+
});
|
|
5430
|
+
} catch (err) {
|
|
5431
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5432
|
+
if (/already exists/i.test(msg) || /HTTP 422/i.test(msg)) return;
|
|
5433
|
+
throw err;
|
|
5434
|
+
}
|
|
5435
|
+
}
|
|
5436
|
+
|
|
5437
|
+
// src/scripts/appendCompanyActivity.ts
|
|
5438
|
+
function resolveTrigger(force) {
|
|
5439
|
+
const event = process.env.GITHUB_EVENT_NAME ?? "";
|
|
5440
|
+
if (event === "schedule") return "schedule";
|
|
5441
|
+
if (force || event === "issue_comment" || event === "workflow_dispatch") return "manual";
|
|
5442
|
+
return "event";
|
|
5443
|
+
}
|
|
5444
|
+
function appendLine(owner, repo, cwd, record) {
|
|
5445
|
+
const filePath = `.kody/activity/${record.ts.slice(0, 10)}.jsonl`;
|
|
5446
|
+
let existing = "";
|
|
5447
|
+
let sha;
|
|
5448
|
+
try {
|
|
5449
|
+
const out = gh(["api", `/repos/${owner}/${repo}/contents/${filePath}?ref=${STATE_BRANCH}`], { cwd });
|
|
5450
|
+
const json = JSON.parse(out);
|
|
5451
|
+
if (json.sha) sha = json.sha;
|
|
5452
|
+
if (json.content) existing = Buffer.from(json.content, "base64").toString("utf-8");
|
|
5453
|
+
} catch {
|
|
5454
|
+
}
|
|
5455
|
+
const body = `${existing}${JSON.stringify(record)}
|
|
5456
|
+
`;
|
|
5457
|
+
const payload = {
|
|
5458
|
+
message: `chore(activity): ${record.action}`,
|
|
5459
|
+
content: Buffer.from(body, "utf-8").toString("base64"),
|
|
5460
|
+
// Keep this high-frequency feed off the default branch.
|
|
5461
|
+
branch: STATE_BRANCH
|
|
5462
|
+
};
|
|
5463
|
+
if (sha) payload.sha = sha;
|
|
5464
|
+
ensureStateBranch(owner, repo, cwd);
|
|
5465
|
+
gh(["api", "--method", "PUT", `/repos/${owner}/${repo}/contents/${filePath}`, "--input", "-"], {
|
|
5466
|
+
cwd,
|
|
5467
|
+
input: JSON.stringify(payload)
|
|
5468
|
+
});
|
|
5469
|
+
}
|
|
5470
|
+
var appendCompanyActivity = async (ctx, _profile, agentResult) => {
|
|
5471
|
+
try {
|
|
5472
|
+
const owner = ctx.config?.github?.owner;
|
|
5473
|
+
const repo = ctx.config?.github?.repo;
|
|
5474
|
+
const duty = String(ctx.data.jobSlug ?? ctx.args?.job ?? "").trim();
|
|
5475
|
+
if (!owner || !repo || !duty) return;
|
|
5476
|
+
const dutyTitle = ctx.data.jobTitle ?? null;
|
|
5477
|
+
const staff = ctx.data.workerSlug || null;
|
|
5478
|
+
const staffTitle = ctx.data.workerTitle || null;
|
|
5479
|
+
const force = ctx.args?.force === true;
|
|
5480
|
+
const record = {
|
|
5481
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5482
|
+
action: `Ran duty: ${dutyTitle ?? duty}`,
|
|
5483
|
+
duty,
|
|
5484
|
+
dutyTitle,
|
|
5485
|
+
staff,
|
|
5486
|
+
staffTitle,
|
|
5487
|
+
trigger: resolveTrigger(force),
|
|
5488
|
+
outcome: agentResult?.outcome ?? "unknown",
|
|
5489
|
+
outcomeKind: agentResult?.outcomeKind ?? null,
|
|
5490
|
+
reason: agentResult?.error ?? null,
|
|
5491
|
+
durationMs: agentResult?.durationMs ?? null,
|
|
5492
|
+
runUrl: getRunUrl() || null
|
|
5493
|
+
};
|
|
5494
|
+
appendLine(owner, repo, ctx.cwd, record);
|
|
5495
|
+
} catch (err) {
|
|
5496
|
+
process.stderr.write(
|
|
5497
|
+
`[activity] company-activity append failed: ${err instanceof Error ? err.message : String(err)}
|
|
5498
|
+
`
|
|
5499
|
+
);
|
|
5500
|
+
}
|
|
5501
|
+
};
|
|
5502
|
+
|
|
5503
|
+
// src/scripts/brainServe.ts
|
|
5504
|
+
import * as fs22 from "fs";
|
|
5505
|
+
import { createServer } from "http";
|
|
5506
|
+
import * as path19 from "path";
|
|
5507
|
+
init_repoWorkspace();
|
|
5508
|
+
|
|
5509
|
+
// src/scripts/brainTurnLog.ts
|
|
5510
|
+
import * as fs21 from "fs";
|
|
5511
|
+
import * as path18 from "path";
|
|
5512
|
+
var live = /* @__PURE__ */ new Map();
|
|
5513
|
+
function eventsPath(dir, chatId) {
|
|
5514
|
+
return path18.join(dir, ".kody", "brain-events", `${chatId}.jsonl`);
|
|
5515
|
+
}
|
|
5516
|
+
function lastPersistedSeq(dir, chatId) {
|
|
5517
|
+
const p = eventsPath(dir, chatId);
|
|
5518
|
+
if (!fs21.existsSync(p)) return 0;
|
|
5519
|
+
const lines = fs21.readFileSync(p, "utf-8").split("\n").filter(Boolean);
|
|
5520
|
+
if (lines.length === 0) return 0;
|
|
5521
|
+
try {
|
|
5522
|
+
return JSON.parse(lines[lines.length - 1]).seq || 0;
|
|
5523
|
+
} catch {
|
|
5524
|
+
return 0;
|
|
5525
|
+
}
|
|
5526
|
+
}
|
|
5527
|
+
function readSince(dir, chatId, since) {
|
|
5528
|
+
const p = eventsPath(dir, chatId);
|
|
5529
|
+
if (!fs21.existsSync(p)) return [];
|
|
5530
|
+
const out = [];
|
|
5531
|
+
for (const line of fs21.readFileSync(p, "utf-8").split("\n")) {
|
|
5532
|
+
if (!line) continue;
|
|
5533
|
+
try {
|
|
5534
|
+
const rec = JSON.parse(line);
|
|
5535
|
+
if (rec.seq > since) out.push(rec);
|
|
5536
|
+
} catch {
|
|
5537
|
+
}
|
|
5538
|
+
}
|
|
5539
|
+
return out;
|
|
5540
|
+
}
|
|
5541
|
+
function isTerminal(event) {
|
|
5542
|
+
return event.type === "done" || event.type === "error";
|
|
5543
|
+
}
|
|
5544
|
+
function beginTurn(dir, chatId) {
|
|
5545
|
+
const existing = live.get(chatId);
|
|
5519
5546
|
const seqFloor = existing ? existing.seq : lastPersistedSeq(dir, chatId);
|
|
5520
5547
|
const turn = (existing?.turn ?? 0) + 1;
|
|
5521
5548
|
const state = {
|
|
@@ -5527,12 +5554,13 @@ function beginTurn(dir, chatId) {
|
|
|
5527
5554
|
};
|
|
5528
5555
|
live.set(chatId, state);
|
|
5529
5556
|
const p = eventsPath(dir, chatId);
|
|
5530
|
-
|
|
5557
|
+
fs21.mkdirSync(path18.dirname(p), { recursive: true });
|
|
5531
5558
|
return (event) => {
|
|
5532
5559
|
state.seq += 1;
|
|
5533
5560
|
const rec = { seq: state.seq, turn, ts: Date.now(), event };
|
|
5534
5561
|
try {
|
|
5535
|
-
|
|
5562
|
+
fs21.appendFileSync(p, `${JSON.stringify(rec)}
|
|
5563
|
+
`);
|
|
5536
5564
|
} catch (err) {
|
|
5537
5565
|
process.stderr.write(
|
|
5538
5566
|
`[brain-turn-log] append failed for ${chatId}: ${err instanceof Error ? err.message : String(err)}
|
|
@@ -5570,7 +5598,8 @@ function endTurnIfUnterminated(dir, chatId, errMessage) {
|
|
|
5570
5598
|
event: { type: "error", error: errMessage || "turn ended unexpectedly", chatId }
|
|
5571
5599
|
};
|
|
5572
5600
|
try {
|
|
5573
|
-
|
|
5601
|
+
fs21.appendFileSync(eventsPath(dir, chatId), `${JSON.stringify(rec)}
|
|
5602
|
+
`);
|
|
5574
5603
|
} catch {
|
|
5575
5604
|
}
|
|
5576
5605
|
state.status = "ended";
|
|
@@ -5642,9 +5671,7 @@ var DEFAULT_PORT = 8080;
|
|
|
5642
5671
|
function getApiKey() {
|
|
5643
5672
|
const key = (process.env.BRAIN_API_KEY ?? "").trim();
|
|
5644
5673
|
if (!key) {
|
|
5645
|
-
throw new Error(
|
|
5646
|
-
"BRAIN_API_KEY env var is required \u2014 set it on the Fly machine before boot."
|
|
5647
|
-
);
|
|
5674
|
+
throw new Error("BRAIN_API_KEY env var is required \u2014 set it on the Fly machine before boot.");
|
|
5648
5675
|
}
|
|
5649
5676
|
return key;
|
|
5650
5677
|
}
|
|
@@ -5657,8 +5684,8 @@ function isSafeChatId(id) {
|
|
|
5657
5684
|
function authOk(req, expected) {
|
|
5658
5685
|
const xApiKey = req.headers["x-api-key"]?.trim();
|
|
5659
5686
|
if (xApiKey && xApiKey === expected) return true;
|
|
5660
|
-
const auth = req.headers
|
|
5661
|
-
if (auth
|
|
5687
|
+
const auth = req.headers.authorization?.trim();
|
|
5688
|
+
if (auth?.toLowerCase().startsWith("bearer ")) {
|
|
5662
5689
|
return auth.slice(7).trim() === expected;
|
|
5663
5690
|
}
|
|
5664
5691
|
return false;
|
|
@@ -5802,7 +5829,7 @@ async function handleChatTurn(req, res, chatId, opts) {
|
|
|
5802
5829
|
const repo = strField(body, "repo");
|
|
5803
5830
|
const repoToken = strField(body, "repoToken");
|
|
5804
5831
|
const sessionFile = sessionFilePath(opts.cwd, chatId);
|
|
5805
|
-
|
|
5832
|
+
fs22.mkdirSync(path19.dirname(sessionFile), { recursive: true });
|
|
5806
5833
|
appendTurn(sessionFile, {
|
|
5807
5834
|
role: "user",
|
|
5808
5835
|
content: message,
|
|
@@ -5849,7 +5876,7 @@ async function handleChatTurn(req, res, chatId, opts) {
|
|
|
5849
5876
|
function buildServer(opts) {
|
|
5850
5877
|
const runTurn = opts.runTurn ?? runChatTurn;
|
|
5851
5878
|
const cloneRepo = opts.cloneRepo ?? defaultCloneRepo;
|
|
5852
|
-
const reposRoot = opts.reposRoot ??
|
|
5879
|
+
const reposRoot = opts.reposRoot ?? path19.join(path19.dirname(path19.resolve(opts.cwd)), "repos");
|
|
5853
5880
|
return createServer(async (req, res) => {
|
|
5854
5881
|
if (!req.method || !req.url) {
|
|
5855
5882
|
sendJson(res, 400, { error: "bad request" });
|
|
@@ -5900,10 +5927,8 @@ var brainServe = async (ctx) => {
|
|
|
5900
5927
|
ctx.skipAgent = true;
|
|
5901
5928
|
const unpacked = unpackAllSecrets();
|
|
5902
5929
|
if (unpacked > 0) {
|
|
5903
|
-
process.stdout.write(
|
|
5904
|
-
|
|
5905
|
-
`
|
|
5906
|
-
);
|
|
5930
|
+
process.stdout.write(`[brain-serve] unpacked ${unpacked} secret(s) from ALL_SECRETS
|
|
5931
|
+
`);
|
|
5907
5932
|
}
|
|
5908
5933
|
const apiKey = getApiKey();
|
|
5909
5934
|
const port = Number(process.env.PORT ?? DEFAULT_PORT);
|
|
@@ -5911,15 +5936,11 @@ var brainServe = async (ctx) => {
|
|
|
5911
5936
|
const usesProxy = needsLitellmProxy(model);
|
|
5912
5937
|
let handle = null;
|
|
5913
5938
|
if (usesProxy) {
|
|
5914
|
-
process.stdout.write(
|
|
5915
|
-
|
|
5916
|
-
`
|
|
5917
|
-
);
|
|
5939
|
+
process.stdout.write(`[brain-serve] starting LiteLLM proxy for ${model.provider}/${model.model}...
|
|
5940
|
+
`);
|
|
5918
5941
|
handle = await startLitellmIfNeeded(model, ctx.cwd);
|
|
5919
|
-
process.stdout.write(
|
|
5920
|
-
|
|
5921
|
-
`
|
|
5922
|
-
);
|
|
5942
|
+
process.stdout.write(`[brain-serve] LiteLLM ready at ${handle?.url ?? LITELLM_DEFAULT_URL}
|
|
5943
|
+
`);
|
|
5923
5944
|
}
|
|
5924
5945
|
const litellmUrl = usesProxy ? handle?.url ?? LITELLM_DEFAULT_URL : null;
|
|
5925
5946
|
const server = buildServer({
|
|
@@ -5932,10 +5953,8 @@ var brainServe = async (ctx) => {
|
|
|
5932
5953
|
});
|
|
5933
5954
|
await new Promise((resolve6) => {
|
|
5934
5955
|
server.listen(port, "0.0.0.0", () => {
|
|
5935
|
-
process.stdout.write(
|
|
5936
|
-
|
|
5937
|
-
`
|
|
5938
|
-
);
|
|
5956
|
+
process.stdout.write(`[brain-serve] listening on 0.0.0.0:${port} (cwd=${ctx.cwd})
|
|
5957
|
+
`);
|
|
5939
5958
|
resolve6();
|
|
5940
5959
|
});
|
|
5941
5960
|
});
|
|
@@ -5958,8 +5977,95 @@ var brainServe = async (ctx) => {
|
|
|
5958
5977
|
});
|
|
5959
5978
|
};
|
|
5960
5979
|
|
|
5980
|
+
// src/scripts/buildSyntheticPlugin.ts
|
|
5981
|
+
import * as fs23 from "fs";
|
|
5982
|
+
import * as os3 from "os";
|
|
5983
|
+
import * as path20 from "path";
|
|
5984
|
+
function getPluginsCatalogRoot() {
|
|
5985
|
+
const here = path20.dirname(new URL(import.meta.url).pathname);
|
|
5986
|
+
const candidates = [
|
|
5987
|
+
path20.join(here, "..", "plugins"),
|
|
5988
|
+
// dev: src/scripts → src/plugins
|
|
5989
|
+
path20.join(here, "..", "..", "plugins"),
|
|
5990
|
+
// built: dist/scripts → dist/plugins
|
|
5991
|
+
path20.join(here, "..", "..", "src", "plugins")
|
|
5992
|
+
// fallback
|
|
5993
|
+
];
|
|
5994
|
+
for (const c of candidates) {
|
|
5995
|
+
if (fs23.existsSync(c) && fs23.statSync(c).isDirectory()) return c;
|
|
5996
|
+
}
|
|
5997
|
+
return candidates[0];
|
|
5998
|
+
}
|
|
5999
|
+
var buildSyntheticPlugin = async (ctx, profile) => {
|
|
6000
|
+
const cc = profile.claudeCode;
|
|
6001
|
+
const needsSynthetic = cc.skills.length > 0 || cc.commands.length > 0 || cc.hooks.length > 0;
|
|
6002
|
+
if (!needsSynthetic) return;
|
|
6003
|
+
const catalog = getPluginsCatalogRoot();
|
|
6004
|
+
const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
6005
|
+
const root = path20.join(os3.tmpdir(), `kody-synth-${runId}`);
|
|
6006
|
+
fs23.mkdirSync(path20.join(root, ".claude-plugin"), { recursive: true });
|
|
6007
|
+
const resolvePart = (bucket, entry) => {
|
|
6008
|
+
const local = path20.join(profile.dir, bucket, entry);
|
|
6009
|
+
if (fs23.existsSync(local)) return local;
|
|
6010
|
+
const central = path20.join(catalog, bucket, entry);
|
|
6011
|
+
if (fs23.existsSync(central)) return central;
|
|
6012
|
+
throw new Error(
|
|
6013
|
+
`buildSyntheticPlugin: ${bucket} entry '${entry}' not found in executable dir (${profile.dir}/${bucket}/) or catalog (${catalog}/${bucket}/)`
|
|
6014
|
+
);
|
|
6015
|
+
};
|
|
6016
|
+
if (cc.skills.length > 0) {
|
|
6017
|
+
const dst = path20.join(root, "skills");
|
|
6018
|
+
fs23.mkdirSync(dst, { recursive: true });
|
|
6019
|
+
for (const name of cc.skills) {
|
|
6020
|
+
copyDir(resolvePart("skills", name), path20.join(dst, name));
|
|
6021
|
+
}
|
|
6022
|
+
}
|
|
6023
|
+
if (cc.commands.length > 0) {
|
|
6024
|
+
const dst = path20.join(root, "commands");
|
|
6025
|
+
fs23.mkdirSync(dst, { recursive: true });
|
|
6026
|
+
for (const name of cc.commands) {
|
|
6027
|
+
fs23.copyFileSync(resolvePart("commands", `${name}.md`), path20.join(dst, `${name}.md`));
|
|
6028
|
+
}
|
|
6029
|
+
}
|
|
6030
|
+
if (cc.hooks.length > 0) {
|
|
6031
|
+
const dst = path20.join(root, "hooks");
|
|
6032
|
+
fs23.mkdirSync(dst, { recursive: true });
|
|
6033
|
+
const merged = { hooks: {} };
|
|
6034
|
+
for (const name of cc.hooks) {
|
|
6035
|
+
const src = resolvePart("hooks", `${name}.json`);
|
|
6036
|
+
const parsed = JSON.parse(fs23.readFileSync(src, "utf-8"));
|
|
6037
|
+
for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
|
|
6038
|
+
if (!Array.isArray(entries)) continue;
|
|
6039
|
+
if (!merged.hooks[event]) merged.hooks[event] = [];
|
|
6040
|
+
merged.hooks[event].push(...entries);
|
|
6041
|
+
}
|
|
6042
|
+
}
|
|
6043
|
+
fs23.writeFileSync(path20.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
|
|
6044
|
+
`);
|
|
6045
|
+
}
|
|
6046
|
+
const manifest = {
|
|
6047
|
+
name: `kody-synth-${profile.name}`,
|
|
6048
|
+
version: "1.0.0",
|
|
6049
|
+
description: `Synthetic plugin assembled by Kody for profile '${profile.name}' at runtime.`
|
|
6050
|
+
};
|
|
6051
|
+
if (cc.skills.length > 0) manifest.skills = ["./skills/"];
|
|
6052
|
+
if (cc.commands.length > 0) manifest.commands = ["./commands/"];
|
|
6053
|
+
fs23.writeFileSync(path20.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
|
|
6054
|
+
`);
|
|
6055
|
+
ctx.data.syntheticPluginPath = root;
|
|
6056
|
+
};
|
|
6057
|
+
function copyDir(src, dst) {
|
|
6058
|
+
fs23.mkdirSync(dst, { recursive: true });
|
|
6059
|
+
for (const ent of fs23.readdirSync(src, { withFileTypes: true })) {
|
|
6060
|
+
const s = path20.join(src, ent.name);
|
|
6061
|
+
const d = path20.join(dst, ent.name);
|
|
6062
|
+
if (ent.isDirectory()) copyDir(s, d);
|
|
6063
|
+
else if (ent.isFile()) fs23.copyFileSync(s, d);
|
|
6064
|
+
}
|
|
6065
|
+
}
|
|
6066
|
+
|
|
5961
6067
|
// src/coverage.ts
|
|
5962
|
-
import { execFileSync as
|
|
6068
|
+
import { execFileSync as execFileSync11 } from "child_process";
|
|
5963
6069
|
function patternToRegex(pattern) {
|
|
5964
6070
|
let s = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
5965
6071
|
s = s.replace(/\*\*\//g, "\xA7S").replace(/\*\*/g, "\xA7A").replace(/\*/g, "[^/]*");
|
|
@@ -5977,7 +6083,7 @@ function renderSiblingPath(file, requireSibling) {
|
|
|
5977
6083
|
}
|
|
5978
6084
|
function safeGit(args, cwd) {
|
|
5979
6085
|
try {
|
|
5980
|
-
return
|
|
6086
|
+
return execFileSync11("git", args, { encoding: "utf-8", cwd, env: { ...process.env, HUSKY: "0" } }).trim();
|
|
5981
6087
|
} catch {
|
|
5982
6088
|
return "";
|
|
5983
6089
|
}
|
|
@@ -6053,8 +6159,10 @@ ${formatMissesForFeedback(misses)}`;
|
|
|
6053
6159
|
retry = await invoker(retryPrompt);
|
|
6054
6160
|
} catch (err) {
|
|
6055
6161
|
const msg = err instanceof Error ? err.message : String(err);
|
|
6056
|
-
process.stderr.write(
|
|
6057
|
-
`);
|
|
6162
|
+
process.stderr.write(
|
|
6163
|
+
`[kody] coverage retry agent failed (${msg}); keeping ${misses.length} miss(es) \u2014 PR will draft
|
|
6164
|
+
`
|
|
6165
|
+
);
|
|
6058
6166
|
ctx.data.coverageMisses = misses;
|
|
6059
6167
|
return;
|
|
6060
6168
|
}
|
|
@@ -6105,12 +6213,12 @@ function defaultLabelMap() {
|
|
|
6105
6213
|
|
|
6106
6214
|
// src/scripts/commitAndPush.ts
|
|
6107
6215
|
import * as fs24 from "fs";
|
|
6108
|
-
import * as
|
|
6216
|
+
import * as path21 from "path";
|
|
6109
6217
|
init_events();
|
|
6110
6218
|
var DEFAULT_COMMIT_MESSAGE = "chore: kody changes";
|
|
6111
6219
|
function sentinelPathForStage(cwd, profileName) {
|
|
6112
6220
|
const runId = resolveRunId();
|
|
6113
|
-
return
|
|
6221
|
+
return path21.join(cwd, ".kody", "runs", runId, `commit-${profileName}.lock`);
|
|
6114
6222
|
}
|
|
6115
6223
|
var commitAndPush2 = async (ctx, profile) => {
|
|
6116
6224
|
const branch = ctx.data.branch;
|
|
@@ -6175,7 +6283,7 @@ var commitAndPush2 = async (ctx, profile) => {
|
|
|
6175
6283
|
const result = ctx.data.commitResult;
|
|
6176
6284
|
if (sentinel && result?.committed) {
|
|
6177
6285
|
try {
|
|
6178
|
-
fs24.mkdirSync(
|
|
6286
|
+
fs24.mkdirSync(path21.dirname(sentinel), { recursive: true });
|
|
6179
6287
|
fs24.writeFileSync(
|
|
6180
6288
|
sentinel,
|
|
6181
6289
|
JSON.stringify(
|
|
@@ -6198,47 +6306,9 @@ var commitAndPush2 = async (ctx, profile) => {
|
|
|
6198
6306
|
// src/goal/stateStore.ts
|
|
6199
6307
|
init_issue();
|
|
6200
6308
|
|
|
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
6309
|
// src/goal/state.ts
|
|
6240
6310
|
import * as fs25 from "fs";
|
|
6241
|
-
import * as
|
|
6311
|
+
import * as path22 from "path";
|
|
6242
6312
|
var VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "awaiting-merge", "done"]);
|
|
6243
6313
|
var GoalStateError = class extends Error {
|
|
6244
6314
|
constructor(path41, message) {
|
|
@@ -6385,15 +6455,15 @@ function describeCommitMessage(goal) {
|
|
|
6385
6455
|
|
|
6386
6456
|
// src/scripts/composePrompt.ts
|
|
6387
6457
|
import * as fs26 from "fs";
|
|
6388
|
-
import * as
|
|
6458
|
+
import * as path23 from "path";
|
|
6389
6459
|
var MUSTACHE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
|
|
6390
6460
|
var composePrompt = async (ctx, profile) => {
|
|
6391
6461
|
const explicit = ctx.data.promptTemplate;
|
|
6392
6462
|
const mode = ctx.args.mode;
|
|
6393
6463
|
const candidates = [
|
|
6394
|
-
explicit ?
|
|
6395
|
-
mode ?
|
|
6396
|
-
|
|
6464
|
+
explicit ? path23.join(profile.dir, explicit) : null,
|
|
6465
|
+
mode ? path23.join(profile.dir, "prompts", `${mode}.md`) : null,
|
|
6466
|
+
path23.join(profile.dir, "prompt.md")
|
|
6397
6467
|
].filter(Boolean);
|
|
6398
6468
|
let templatePath = "";
|
|
6399
6469
|
let template = "";
|
|
@@ -6809,12 +6879,7 @@ QA_REPORT_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.gith
|
|
|
6809
6879
|
ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
|
|
6810
6880
|
return;
|
|
6811
6881
|
}
|
|
6812
|
-
await promoteReportToGoal(
|
|
6813
|
-
ctx,
|
|
6814
|
-
finalText,
|
|
6815
|
-
ctx.args.scope,
|
|
6816
|
-
ctx.args.goal
|
|
6817
|
-
);
|
|
6882
|
+
await promoteReportToGoal(ctx, finalText, ctx.args.scope, ctx.args.goal);
|
|
6818
6883
|
};
|
|
6819
6884
|
async function promoteReportToGoal(ctx, finalText, scopeArg, explicitGoalArg) {
|
|
6820
6885
|
const { markdown, data, jsonError } = splitReport(finalText);
|
|
@@ -6830,10 +6895,10 @@ async function promoteReportToGoal(ctx, finalText, scopeArg, explicitGoalArg) {
|
|
|
6830
6895
|
const title = `QA [${verdict}]: ${scope2?.trim() || "smoke"} \u2014 ${todayIso()}`.slice(0, 240);
|
|
6831
6896
|
let url = "";
|
|
6832
6897
|
try {
|
|
6833
|
-
const out = gh(
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
);
|
|
6898
|
+
const out = gh(["issue", "create", "--title", title, "--label", FINDING_LABEL, "--body-file", "-"], {
|
|
6899
|
+
input: finalText,
|
|
6900
|
+
cwd: ctx.cwd
|
|
6901
|
+
});
|
|
6837
6902
|
url = out.split("\n").map((l) => l.trim()).filter(Boolean).pop() ?? "";
|
|
6838
6903
|
} catch (err) {
|
|
6839
6904
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -6866,13 +6931,9 @@ QA_REPORT_POSTED=${url} (verdict: ${verdict})
|
|
|
6866
6931
|
if (manifestRead.number !== null) {
|
|
6867
6932
|
manifestIssueNumber = manifestRead.number;
|
|
6868
6933
|
try {
|
|
6869
|
-
postIssueComment(
|
|
6870
|
-
manifestRead.number,
|
|
6871
|
-
`## QA \u2014 ${verdict} \xB7 goal \`${goalId}\`
|
|
6934
|
+
postIssueComment(manifestRead.number, `## QA \u2014 ${verdict} \xB7 goal \`${goalId}\`
|
|
6872
6935
|
|
|
6873
|
-
${markdown}`,
|
|
6874
|
-
ctx.cwd
|
|
6875
|
-
);
|
|
6936
|
+
${markdown}`, ctx.cwd);
|
|
6876
6937
|
} catch (err) {
|
|
6877
6938
|
const reason = err instanceof Error ? err.message : String(err);
|
|
6878
6939
|
process.stderr.write(`[createQaGoal] could not comment on manifest issue: ${reason.slice(0, 300)}
|
|
@@ -6925,7 +6986,14 @@ ${markdown}`,
|
|
|
6925
6986
|
const now = nowIso();
|
|
6926
6987
|
const goalState = { state: "active", startedAt: now, updatedAt: now, extra: { version: 1 } };
|
|
6927
6988
|
try {
|
|
6928
|
-
putGoalState(
|
|
6989
|
+
putGoalState(
|
|
6990
|
+
ctx.config.github.owner,
|
|
6991
|
+
ctx.config.github.repo,
|
|
6992
|
+
goalId,
|
|
6993
|
+
goalState,
|
|
6994
|
+
`chore(goals): activate ${goalId}`,
|
|
6995
|
+
ctx.cwd
|
|
6996
|
+
);
|
|
6929
6997
|
} catch (err) {
|
|
6930
6998
|
process.stderr.write(
|
|
6931
6999
|
`[createQaGoal] failed to persist goal state to kody-state: ${err instanceof Error ? err.message : String(err)}
|
|
@@ -7000,16 +7068,7 @@ function listGoalIssues(goalId, cwd) {
|
|
|
7000
7068
|
function listOpenPrs(cwd) {
|
|
7001
7069
|
try {
|
|
7002
7070
|
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
|
-
],
|
|
7071
|
+
["pr", "list", "--state", "open", "--limit", "200", "--json", "number,url,isDraft,headRefName,baseRefName,body"],
|
|
7013
7072
|
{ cwd }
|
|
7014
7073
|
);
|
|
7015
7074
|
return { ok: true, value: JSON.parse(out) };
|
|
@@ -7116,10 +7175,7 @@ function mergePrSquash(prNumber, cwd) {
|
|
|
7116
7175
|
}
|
|
7117
7176
|
function editPrBase(prNumber, baseBranch, cwd) {
|
|
7118
7177
|
try {
|
|
7119
|
-
gh(
|
|
7120
|
-
["api", "--method", "PATCH", `repos/{owner}/{repo}/pulls/${prNumber}`, "-f", `base=${baseBranch}`],
|
|
7121
|
-
{ cwd }
|
|
7122
|
-
);
|
|
7178
|
+
gh(["api", "--method", "PATCH", `repos/{owner}/{repo}/pulls/${prNumber}`, "-f", `base=${baseBranch}`], { cwd });
|
|
7123
7179
|
return { ok: true };
|
|
7124
7180
|
} catch (err) {
|
|
7125
7181
|
return fail(err);
|
|
@@ -7195,7 +7251,10 @@ var deriveGoalPhase = async (ctx) => {
|
|
|
7195
7251
|
goal.phase = "idle";
|
|
7196
7252
|
return;
|
|
7197
7253
|
}
|
|
7198
|
-
const taskPrs = filterGoalTaskPrs(
|
|
7254
|
+
const taskPrs = filterGoalTaskPrs(
|
|
7255
|
+
allPrs.value ?? [],
|
|
7256
|
+
rawIssues.map((i) => i.number)
|
|
7257
|
+
);
|
|
7199
7258
|
goal.openTaskPrs = taskPrs;
|
|
7200
7259
|
goal.leafPr = pickLeafPr(taskPrs);
|
|
7201
7260
|
goal.childTasks = pairIssuesWithPrs(rawIssues, taskPrs);
|
|
@@ -7252,13 +7311,13 @@ var deriveQaScopeFromIssue = async (ctx) => {
|
|
|
7252
7311
|
};
|
|
7253
7312
|
|
|
7254
7313
|
// src/scripts/diagMcp.ts
|
|
7255
|
-
import { execFileSync as
|
|
7314
|
+
import { execFileSync as execFileSync12 } from "child_process";
|
|
7256
7315
|
import * as fs27 from "fs";
|
|
7257
7316
|
import * as os4 from "os";
|
|
7258
|
-
import * as
|
|
7317
|
+
import * as path24 from "path";
|
|
7259
7318
|
var diagMcp = async (_ctx) => {
|
|
7260
7319
|
const home = os4.homedir();
|
|
7261
|
-
const cacheDir =
|
|
7320
|
+
const cacheDir = path24.join(home, ".cache", "ms-playwright");
|
|
7262
7321
|
let entries = [];
|
|
7263
7322
|
try {
|
|
7264
7323
|
entries = fs27.readdirSync(cacheDir);
|
|
@@ -7272,7 +7331,7 @@ var diagMcp = async (_ctx) => {
|
|
|
7272
7331
|
process.stderr.write(`[kody diag] chromium present: ${hasChromium ? "yes" : "no"}
|
|
7273
7332
|
`);
|
|
7274
7333
|
try {
|
|
7275
|
-
const v =
|
|
7334
|
+
const v = execFileSync12("npx", ["-y", "--package=@playwright/mcp@latest", "--", "playwright-mcp", "--version"], {
|
|
7276
7335
|
stdio: "pipe",
|
|
7277
7336
|
timeout: 6e4,
|
|
7278
7337
|
encoding: "utf8"
|
|
@@ -7288,16 +7347,16 @@ var diagMcp = async (_ctx) => {
|
|
|
7288
7347
|
|
|
7289
7348
|
// src/scripts/discoverQaContext.ts
|
|
7290
7349
|
import * as fs29 from "fs";
|
|
7291
|
-
import * as
|
|
7350
|
+
import * as path26 from "path";
|
|
7292
7351
|
|
|
7293
7352
|
// src/scripts/frameworkDetectors.ts
|
|
7294
7353
|
import * as fs28 from "fs";
|
|
7295
|
-
import * as
|
|
7354
|
+
import * as path25 from "path";
|
|
7296
7355
|
function detectFrameworks(cwd) {
|
|
7297
7356
|
const out = [];
|
|
7298
7357
|
let deps = {};
|
|
7299
7358
|
try {
|
|
7300
|
-
const pkg = JSON.parse(fs28.readFileSync(
|
|
7359
|
+
const pkg = JSON.parse(fs28.readFileSync(path25.join(cwd, "package.json"), "utf-8"));
|
|
7301
7360
|
deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
7302
7361
|
} catch {
|
|
7303
7362
|
return out;
|
|
@@ -7334,7 +7393,7 @@ function detectFrameworks(cwd) {
|
|
|
7334
7393
|
}
|
|
7335
7394
|
function findFile(cwd, candidates) {
|
|
7336
7395
|
for (const c of candidates) {
|
|
7337
|
-
if (fs28.existsSync(
|
|
7396
|
+
if (fs28.existsSync(path25.join(cwd, c))) return c;
|
|
7338
7397
|
}
|
|
7339
7398
|
return null;
|
|
7340
7399
|
}
|
|
@@ -7347,7 +7406,7 @@ var COLLECTION_DIRS = [
|
|
|
7347
7406
|
function discoverPayloadCollections(cwd) {
|
|
7348
7407
|
const out = [];
|
|
7349
7408
|
for (const dir of COLLECTION_DIRS) {
|
|
7350
|
-
const full =
|
|
7409
|
+
const full = path25.join(cwd, dir);
|
|
7351
7410
|
if (!fs28.existsSync(full)) continue;
|
|
7352
7411
|
let files;
|
|
7353
7412
|
try {
|
|
@@ -7357,7 +7416,7 @@ function discoverPayloadCollections(cwd) {
|
|
|
7357
7416
|
}
|
|
7358
7417
|
for (const file of files) {
|
|
7359
7418
|
try {
|
|
7360
|
-
const filePath =
|
|
7419
|
+
const filePath = path25.join(full, file);
|
|
7361
7420
|
const content = fs28.readFileSync(filePath, "utf-8").slice(0, 1e4);
|
|
7362
7421
|
const slugMatch = content.match(/slug:\s*['"]([a-z0-9-]+)['"]/);
|
|
7363
7422
|
if (!slugMatch) continue;
|
|
@@ -7372,7 +7431,7 @@ function discoverPayloadCollections(cwd) {
|
|
|
7372
7431
|
out.push({
|
|
7373
7432
|
name,
|
|
7374
7433
|
slug,
|
|
7375
|
-
filePath:
|
|
7434
|
+
filePath: path25.relative(cwd, filePath),
|
|
7376
7435
|
fields: fields.slice(0, 20),
|
|
7377
7436
|
hasAdmin
|
|
7378
7437
|
});
|
|
@@ -7386,7 +7445,7 @@ var ADMIN_COMPONENT_DIRS = ["src/ui/admin", "src/admin/components", "src/compone
|
|
|
7386
7445
|
function discoverAdminComponents(cwd, collections) {
|
|
7387
7446
|
const out = [];
|
|
7388
7447
|
for (const dir of ADMIN_COMPONENT_DIRS) {
|
|
7389
|
-
const full =
|
|
7448
|
+
const full = path25.join(cwd, dir);
|
|
7390
7449
|
if (!fs28.existsSync(full)) continue;
|
|
7391
7450
|
let entries;
|
|
7392
7451
|
try {
|
|
@@ -7395,19 +7454,19 @@ function discoverAdminComponents(cwd, collections) {
|
|
|
7395
7454
|
continue;
|
|
7396
7455
|
}
|
|
7397
7456
|
for (const entry of entries) {
|
|
7398
|
-
const entryPath =
|
|
7457
|
+
const entryPath = path25.join(full, entry.name);
|
|
7399
7458
|
let name;
|
|
7400
7459
|
let filePath;
|
|
7401
7460
|
if (entry.isDirectory()) {
|
|
7402
7461
|
const indexFile = ["index.tsx", "index.ts", "index.jsx", "index.js"].find(
|
|
7403
|
-
(f) => fs28.existsSync(
|
|
7462
|
+
(f) => fs28.existsSync(path25.join(entryPath, f))
|
|
7404
7463
|
);
|
|
7405
7464
|
if (!indexFile) continue;
|
|
7406
7465
|
name = entry.name;
|
|
7407
|
-
filePath =
|
|
7466
|
+
filePath = path25.relative(cwd, path25.join(entryPath, indexFile));
|
|
7408
7467
|
} else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
|
|
7409
7468
|
name = entry.name.replace(/\.(tsx?|jsx?)$/, "");
|
|
7410
|
-
filePath =
|
|
7469
|
+
filePath = path25.relative(cwd, entryPath);
|
|
7411
7470
|
} else {
|
|
7412
7471
|
continue;
|
|
7413
7472
|
}
|
|
@@ -7415,7 +7474,7 @@ function discoverAdminComponents(cwd, collections) {
|
|
|
7415
7474
|
if (collections) {
|
|
7416
7475
|
for (const col of collections) {
|
|
7417
7476
|
try {
|
|
7418
|
-
const colContent = fs28.readFileSync(
|
|
7477
|
+
const colContent = fs28.readFileSync(path25.join(cwd, col.filePath), "utf-8");
|
|
7419
7478
|
if (colContent.includes(name)) {
|
|
7420
7479
|
usedInCollection = col.slug;
|
|
7421
7480
|
break;
|
|
@@ -7434,7 +7493,7 @@ function scanApiRoutes(cwd) {
|
|
|
7434
7493
|
const out = [];
|
|
7435
7494
|
const appDirs = ["src/app", "app"];
|
|
7436
7495
|
for (const appDir of appDirs) {
|
|
7437
|
-
const apiDir =
|
|
7496
|
+
const apiDir = path25.join(cwd, appDir, "api");
|
|
7438
7497
|
if (!fs28.existsSync(apiDir)) continue;
|
|
7439
7498
|
walkApiRoutes(apiDir, "/api", cwd, out);
|
|
7440
7499
|
break;
|
|
@@ -7451,7 +7510,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
|
|
|
7451
7510
|
const routeFile = entries.find((e) => e.isFile() && /^route\.(ts|js|tsx|jsx)$/.test(e.name));
|
|
7452
7511
|
if (routeFile) {
|
|
7453
7512
|
try {
|
|
7454
|
-
const content = fs28.readFileSync(
|
|
7513
|
+
const content = fs28.readFileSync(path25.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
|
|
7455
7514
|
const methods = HTTP_METHODS.filter(
|
|
7456
7515
|
(m) => new RegExp(`export\\s+(?:async\\s+)?function\\s+${m}\\b`).test(content)
|
|
7457
7516
|
);
|
|
@@ -7459,7 +7518,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
|
|
|
7459
7518
|
out.push({
|
|
7460
7519
|
path: prefix,
|
|
7461
7520
|
methods,
|
|
7462
|
-
filePath:
|
|
7521
|
+
filePath: path25.relative(cwd, path25.join(dir, routeFile.name))
|
|
7463
7522
|
});
|
|
7464
7523
|
}
|
|
7465
7524
|
} catch {
|
|
@@ -7470,7 +7529,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
|
|
|
7470
7529
|
if (entry.name === "node_modules" || entry.name === ".next") continue;
|
|
7471
7530
|
let segment = entry.name;
|
|
7472
7531
|
if (segment.startsWith("(") && segment.endsWith(")")) {
|
|
7473
|
-
walkApiRoutes(
|
|
7532
|
+
walkApiRoutes(path25.join(dir, entry.name), prefix, cwd, out);
|
|
7474
7533
|
continue;
|
|
7475
7534
|
}
|
|
7476
7535
|
if (segment.startsWith("[[") && segment.endsWith("]]")) {
|
|
@@ -7478,7 +7537,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
|
|
|
7478
7537
|
} else if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
7479
7538
|
segment = `:${segment.slice(1, -1)}`;
|
|
7480
7539
|
}
|
|
7481
|
-
walkApiRoutes(
|
|
7540
|
+
walkApiRoutes(path25.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
|
|
7482
7541
|
}
|
|
7483
7542
|
}
|
|
7484
7543
|
var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
|
|
@@ -7498,7 +7557,7 @@ var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
|
|
|
7498
7557
|
function scanEnvVars(cwd) {
|
|
7499
7558
|
const candidates = [".env.example", ".env.local.example", ".env.template"];
|
|
7500
7559
|
for (const envFile of candidates) {
|
|
7501
|
-
const envPath =
|
|
7560
|
+
const envPath = path25.join(cwd, envFile);
|
|
7502
7561
|
if (!fs28.existsSync(envPath)) continue;
|
|
7503
7562
|
try {
|
|
7504
7563
|
const content = fs28.readFileSync(envPath, "utf-8");
|
|
@@ -7549,9 +7608,9 @@ function runQaDiscovery(cwd) {
|
|
|
7549
7608
|
}
|
|
7550
7609
|
function detectDevServer(cwd, out) {
|
|
7551
7610
|
try {
|
|
7552
|
-
const pkg = JSON.parse(fs29.readFileSync(
|
|
7611
|
+
const pkg = JSON.parse(fs29.readFileSync(path26.join(cwd, "package.json"), "utf-8"));
|
|
7553
7612
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
7554
|
-
const pm = fs29.existsSync(
|
|
7613
|
+
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
7614
|
if (pkg.scripts?.dev) out.devCommand = `${pm} dev`;
|
|
7556
7615
|
if (allDeps.next || allDeps.nuxt) out.devPort = 3e3;
|
|
7557
7616
|
else if (allDeps.vite) out.devPort = 5173;
|
|
@@ -7561,7 +7620,7 @@ function detectDevServer(cwd, out) {
|
|
|
7561
7620
|
function scanFrontendRoutes(cwd, out) {
|
|
7562
7621
|
const appDirs = ["src/app", "app"];
|
|
7563
7622
|
for (const appDir of appDirs) {
|
|
7564
|
-
const full =
|
|
7623
|
+
const full = path26.join(cwd, appDir);
|
|
7565
7624
|
if (!fs29.existsSync(full)) continue;
|
|
7566
7625
|
walkFrontendRoutes(full, "", out);
|
|
7567
7626
|
break;
|
|
@@ -7587,7 +7646,7 @@ function walkFrontendRoutes(dir, prefix, out) {
|
|
|
7587
7646
|
if (entry.name === "node_modules" || entry.name === ".next") continue;
|
|
7588
7647
|
let segment = entry.name;
|
|
7589
7648
|
if (segment.startsWith("(") && segment.endsWith(")")) {
|
|
7590
|
-
walkFrontendRoutes(
|
|
7649
|
+
walkFrontendRoutes(path26.join(dir, entry.name), prefix, out);
|
|
7591
7650
|
continue;
|
|
7592
7651
|
}
|
|
7593
7652
|
if (segment.startsWith("[[") && segment.endsWith("]]")) {
|
|
@@ -7595,7 +7654,7 @@ function walkFrontendRoutes(dir, prefix, out) {
|
|
|
7595
7654
|
} else if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
7596
7655
|
segment = `:${segment.slice(1, -1)}`;
|
|
7597
7656
|
}
|
|
7598
|
-
walkFrontendRoutes(
|
|
7657
|
+
walkFrontendRoutes(path26.join(dir, entry.name), `${prefix}/${segment}`, out);
|
|
7599
7658
|
}
|
|
7600
7659
|
}
|
|
7601
7660
|
function detectAuthFiles(cwd, out) {
|
|
@@ -7612,13 +7671,13 @@ function detectAuthFiles(cwd, out) {
|
|
|
7612
7671
|
"src/app/api/oauth"
|
|
7613
7672
|
];
|
|
7614
7673
|
for (const c of candidates) {
|
|
7615
|
-
if (fs29.existsSync(
|
|
7674
|
+
if (fs29.existsSync(path26.join(cwd, c))) out.authFiles.push(c);
|
|
7616
7675
|
}
|
|
7617
7676
|
}
|
|
7618
7677
|
function detectRoles(cwd, out) {
|
|
7619
7678
|
const rolePaths = ["src/types", "src/lib", "src/utils", "src/constants", "src/access", "src/collections"];
|
|
7620
7679
|
for (const rp of rolePaths) {
|
|
7621
|
-
const dir =
|
|
7680
|
+
const dir = path26.join(cwd, rp);
|
|
7622
7681
|
if (!fs29.existsSync(dir)) continue;
|
|
7623
7682
|
let files;
|
|
7624
7683
|
try {
|
|
@@ -7628,7 +7687,7 @@ function detectRoles(cwd, out) {
|
|
|
7628
7687
|
}
|
|
7629
7688
|
for (const f of files) {
|
|
7630
7689
|
try {
|
|
7631
|
-
const content = fs29.readFileSync(
|
|
7690
|
+
const content = fs29.readFileSync(path26.join(dir, f), "utf-8").slice(0, 5e3);
|
|
7632
7691
|
const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
|
|
7633
7692
|
if (roleMatches) {
|
|
7634
7693
|
for (const m of roleMatches) {
|
|
@@ -7701,7 +7760,8 @@ Required env vars: ${d.envVars.join(", ")}`);
|
|
|
7701
7760
|
let result = sections.join("\n");
|
|
7702
7761
|
if (result.length > MAX_SERIALIZED_LENGTH) {
|
|
7703
7762
|
const cutoff = result.lastIndexOf("\n", MAX_SERIALIZED_LENGTH - 20);
|
|
7704
|
-
result = result.slice(0, cutoff > 0 ? cutoff : MAX_SERIALIZED_LENGTH - 20)
|
|
7763
|
+
result = `${result.slice(0, cutoff > 0 ? cutoff : MAX_SERIALIZED_LENGTH - 20)}
|
|
7764
|
+
... (truncated)`;
|
|
7705
7765
|
}
|
|
7706
7766
|
return result;
|
|
7707
7767
|
}
|
|
@@ -7756,7 +7816,7 @@ function parsePr(url) {
|
|
|
7756
7816
|
}
|
|
7757
7817
|
|
|
7758
7818
|
// src/scripts/dispatchClassified.ts
|
|
7759
|
-
import { execFileSync as
|
|
7819
|
+
import { execFileSync as execFileSync13 } from "child_process";
|
|
7760
7820
|
var API_TIMEOUT_MS4 = 3e4;
|
|
7761
7821
|
var VALID_CLASSES2 = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
|
|
7762
7822
|
var dispatchClassified = async (ctx) => {
|
|
@@ -7779,14 +7839,18 @@ ${stateBody}`;
|
|
|
7779
7839
|
try {
|
|
7780
7840
|
const existing = findStateComment("issue", issueNumber, ctx.cwd);
|
|
7781
7841
|
if (existing) {
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7842
|
+
execFileSync13(
|
|
7843
|
+
"gh",
|
|
7844
|
+
["api", `repos/{owner}/{repo}/issues/comments/${existing.id}`, "-X", "PATCH", "-F", "body=@-"],
|
|
7845
|
+
{
|
|
7846
|
+
cwd: ctx.cwd,
|
|
7847
|
+
timeout: API_TIMEOUT_MS4,
|
|
7848
|
+
input: body,
|
|
7849
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
7850
|
+
}
|
|
7851
|
+
);
|
|
7788
7852
|
} else {
|
|
7789
|
-
|
|
7853
|
+
execFileSync13("gh", ["issue", "comment", String(issueNumber), "--body", body], {
|
|
7790
7854
|
cwd: ctx.cwd,
|
|
7791
7855
|
timeout: API_TIMEOUT_MS4,
|
|
7792
7856
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -7807,7 +7871,7 @@ ${stateBody}`;
|
|
|
7807
7871
|
|
|
7808
7872
|
// src/scripts/dispatchJobFileTicks.ts
|
|
7809
7873
|
import * as fs31 from "fs";
|
|
7810
|
-
import * as
|
|
7874
|
+
import * as path28 from "path";
|
|
7811
7875
|
|
|
7812
7876
|
// src/scripts/jobFrontmatter.ts
|
|
7813
7877
|
var SCHEDULE_EVERY_VALUES = [
|
|
@@ -7823,7 +7887,7 @@ var SCHEDULE_EVERY_VALUES = [
|
|
|
7823
7887
|
"manual"
|
|
7824
7888
|
];
|
|
7825
7889
|
var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
7826
|
-
function
|
|
7890
|
+
function splitFrontmatter(raw) {
|
|
7827
7891
|
const match = FRONTMATTER_RE.exec(raw);
|
|
7828
7892
|
if (!match) return { frontmatter: {}, body: raw };
|
|
7829
7893
|
const inner = match[1] ?? "";
|
|
@@ -8061,7 +8125,8 @@ var ContentsApiBackend = class {
|
|
|
8061
8125
|
return false;
|
|
8062
8126
|
}
|
|
8063
8127
|
const slug = slugFromStateFilePath(loaded.path);
|
|
8064
|
-
const body = JSON.stringify(next, null, 2)
|
|
8128
|
+
const body = `${JSON.stringify(next, null, 2)}
|
|
8129
|
+
`;
|
|
8065
8130
|
const payload = {
|
|
8066
8131
|
message: `chore(jobs): update state for ${slug} (rev ${next.rev})`,
|
|
8067
8132
|
content: Buffer.from(body, "utf-8").toString("base64"),
|
|
@@ -8100,7 +8165,7 @@ function isShaConflict(err) {
|
|
|
8100
8165
|
|
|
8101
8166
|
// src/scripts/jobState/localFileBackend.ts
|
|
8102
8167
|
import * as fs30 from "fs";
|
|
8103
|
-
import * as
|
|
8168
|
+
import * as path27 from "path";
|
|
8104
8169
|
var LocalFileBackend = class {
|
|
8105
8170
|
name = "local-file";
|
|
8106
8171
|
cwd;
|
|
@@ -8115,7 +8180,7 @@ var LocalFileBackend = class {
|
|
|
8115
8180
|
if (!opts.owner || !opts.repo) throw new Error("LocalFileBackend: owner and repo are required");
|
|
8116
8181
|
this.cwd = opts.cwd;
|
|
8117
8182
|
this.jobsDir = opts.jobsDir;
|
|
8118
|
-
this.absDir =
|
|
8183
|
+
this.absDir = path27.join(opts.cwd, opts.jobsDir);
|
|
8119
8184
|
this.owner = opts.owner;
|
|
8120
8185
|
this.repo = opts.repo;
|
|
8121
8186
|
this.cache = opts.cache ?? defaultCacheAdapter();
|
|
@@ -8175,7 +8240,7 @@ var LocalFileBackend = class {
|
|
|
8175
8240
|
}
|
|
8176
8241
|
load(slug) {
|
|
8177
8242
|
const relPath = stateFilePath(this.jobsDir, slug);
|
|
8178
|
-
const absPath =
|
|
8243
|
+
const absPath = path27.join(this.cwd, relPath);
|
|
8179
8244
|
if (!fs30.existsSync(absPath)) {
|
|
8180
8245
|
return { path: relPath, handle: null, state: initialStateEnvelope("seed"), created: true };
|
|
8181
8246
|
}
|
|
@@ -8196,9 +8261,10 @@ var LocalFileBackend = class {
|
|
|
8196
8261
|
if (!loaded.created && isStateUnchanged(loaded.state, next)) {
|
|
8197
8262
|
return false;
|
|
8198
8263
|
}
|
|
8199
|
-
const absPath =
|
|
8200
|
-
fs30.mkdirSync(
|
|
8201
|
-
const body = JSON.stringify(next, null, 2)
|
|
8264
|
+
const absPath = path27.join(this.cwd, loaded.path);
|
|
8265
|
+
fs30.mkdirSync(path27.dirname(absPath), { recursive: true });
|
|
8266
|
+
const body = `${JSON.stringify(next, null, 2)}
|
|
8267
|
+
`;
|
|
8202
8268
|
const tmpPath = `${absPath}.${process.pid}.tmp`;
|
|
8203
8269
|
fs30.writeFileSync(tmpPath, body, "utf-8");
|
|
8204
8270
|
fs30.renameSync(tmpPath, absPath);
|
|
@@ -8279,7 +8345,7 @@ var dispatchJobFileTicks = async (ctx, _profile, args) => {
|
|
|
8279
8345
|
await backend.hydrate();
|
|
8280
8346
|
}
|
|
8281
8347
|
try {
|
|
8282
|
-
const slugs = listJobSlugs(
|
|
8348
|
+
const slugs = listJobSlugs(path28.join(ctx.cwd, jobsDir));
|
|
8283
8349
|
ctx.data.jobSlugCount = slugs.length;
|
|
8284
8350
|
if (slugs.length === 0) {
|
|
8285
8351
|
process.stdout.write(`[jobs] no job files in ${jobsDir}
|
|
@@ -8299,10 +8365,8 @@ var dispatchJobFileTicks = async (ctx, _profile, args) => {
|
|
|
8299
8365
|
continue;
|
|
8300
8366
|
}
|
|
8301
8367
|
if (!frontmatter.staff || frontmatter.staff.trim().length === 0) {
|
|
8302
|
-
process.stderr.write(
|
|
8303
|
-
|
|
8304
|
-
`
|
|
8305
|
-
);
|
|
8368
|
+
process.stderr.write(`[jobs] \u23ED skip ${slug}: no staff assigned (add 'staff: <slug>' frontmatter)
|
|
8369
|
+
`);
|
|
8306
8370
|
results.push({ slug, exitCode: 0, skipped: true, reason: "no staff assigned" });
|
|
8307
8371
|
continue;
|
|
8308
8372
|
}
|
|
@@ -8392,8 +8456,8 @@ function formatAgo(ms) {
|
|
|
8392
8456
|
}
|
|
8393
8457
|
function readJobFrontmatter(cwd, jobsDir, slug) {
|
|
8394
8458
|
try {
|
|
8395
|
-
const raw = fs31.readFileSync(
|
|
8396
|
-
return
|
|
8459
|
+
const raw = fs31.readFileSync(path28.join(cwd, jobsDir, `${slug}.md`), "utf-8");
|
|
8460
|
+
return splitFrontmatter(raw).frontmatter;
|
|
8397
8461
|
} catch {
|
|
8398
8462
|
return {};
|
|
8399
8463
|
}
|
|
@@ -8499,13 +8563,10 @@ var dispatchNextTask = async (ctx) => {
|
|
|
8499
8563
|
|
|
8500
8564
|
// src/pr.ts
|
|
8501
8565
|
init_issue();
|
|
8502
|
-
import { execFileSync as
|
|
8566
|
+
import { execFileSync as execFileSync14 } from "child_process";
|
|
8503
8567
|
function prMergeStatus(prNumber, cwd) {
|
|
8504
8568
|
try {
|
|
8505
|
-
const out = gh(
|
|
8506
|
-
["pr", "view", String(prNumber), "--json", "mergeable,mergeStateStatus"],
|
|
8507
|
-
{ cwd }
|
|
8508
|
-
);
|
|
8569
|
+
const out = gh(["pr", "view", String(prNumber), "--json", "mergeable,mergeStateStatus"], { cwd });
|
|
8509
8570
|
const parsed = JSON.parse(out);
|
|
8510
8571
|
const mergeable = parsed.mergeable ?? "UNKNOWN";
|
|
8511
8572
|
const mergeStateStatus = parsed.mergeStateStatus ?? "UNKNOWN";
|
|
@@ -8625,7 +8686,7 @@ function isAlreadyExistsError(err) {
|
|
|
8625
8686
|
return ALREADY_EXISTS_RE.test(msg);
|
|
8626
8687
|
}
|
|
8627
8688
|
function git2(args, cwd) {
|
|
8628
|
-
return
|
|
8689
|
+
return execFileSync14("git", args, {
|
|
8629
8690
|
encoding: "utf-8",
|
|
8630
8691
|
timeout: 3e4,
|
|
8631
8692
|
cwd,
|
|
@@ -8890,8 +8951,10 @@ var finalizeGoal = async (ctx) => {
|
|
|
8890
8951
|
);
|
|
8891
8952
|
continue;
|
|
8892
8953
|
}
|
|
8893
|
-
process.stdout.write(
|
|
8894
|
-
`)
|
|
8954
|
+
process.stdout.write(
|
|
8955
|
+
`[goal-tick] closing task issue #${t.number} (goal finalized \u2014 carried by PR #${leaf.number})
|
|
8956
|
+
`
|
|
8957
|
+
);
|
|
8895
8958
|
const closed = closeIssue(
|
|
8896
8959
|
t.number,
|
|
8897
8960
|
{
|
|
@@ -8956,7 +9019,7 @@ var finalizeTerminal = async (ctx) => {
|
|
|
8956
9019
|
|
|
8957
9020
|
// src/scripts/finishFlow.ts
|
|
8958
9021
|
init_issue();
|
|
8959
|
-
import { execFileSync as
|
|
9022
|
+
import { execFileSync as execFileSync15 } from "child_process";
|
|
8960
9023
|
var TERMINAL_PHASE = {
|
|
8961
9024
|
"review-passed": { phase: "shipped", status: "succeeded" },
|
|
8962
9025
|
"fix-applied": { phase: "shipped", status: "succeeded" },
|
|
@@ -8978,7 +9041,7 @@ var finishFlow = async (ctx, profile, _agentResult, args) => {
|
|
|
8978
9041
|
if (state) state.flow = void 0;
|
|
8979
9042
|
if (!issueNumber) return;
|
|
8980
9043
|
const label = typeof args?.label === "string" ? args.label : void 0;
|
|
8981
|
-
if (label
|
|
9044
|
+
if (label?.startsWith(KODY_NAMESPACE)) {
|
|
8982
9045
|
const spec = {
|
|
8983
9046
|
label,
|
|
8984
9047
|
color: typeof args?.color === "string" ? args.color : void 0,
|
|
@@ -8996,7 +9059,7 @@ var finishFlow = async (ctx, profile, _agentResult, args) => {
|
|
|
8996
9059
|
**PR:** ${state.core.prUrl}` : "";
|
|
8997
9060
|
const body = `${icon} kody flow \`${flowName}\` finished \u2014 \`${reason}\`${prSuffix}`;
|
|
8998
9061
|
try {
|
|
8999
|
-
|
|
9062
|
+
execFileSync15("gh", ["issue", "comment", String(issueNumber), "--body", body], {
|
|
9000
9063
|
timeout: API_TIMEOUT_MS5,
|
|
9001
9064
|
cwd: ctx.cwd,
|
|
9002
9065
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -9026,9 +9089,9 @@ var finishFlow = async (ctx, profile, _agentResult, args) => {
|
|
|
9026
9089
|
};
|
|
9027
9090
|
|
|
9028
9091
|
// src/branch.ts
|
|
9029
|
-
import { execFileSync as
|
|
9092
|
+
import { execFileSync as execFileSync16 } from "child_process";
|
|
9030
9093
|
function git3(args, cwd) {
|
|
9031
|
-
return
|
|
9094
|
+
return execFileSync16("git", args, {
|
|
9032
9095
|
encoding: "utf-8",
|
|
9033
9096
|
timeout: 3e4,
|
|
9034
9097
|
cwd,
|
|
@@ -9045,11 +9108,11 @@ function getCurrentBranch(cwd) {
|
|
|
9045
9108
|
}
|
|
9046
9109
|
function resetWorkingTree2(cwd) {
|
|
9047
9110
|
try {
|
|
9048
|
-
|
|
9111
|
+
execFileSync16("git", ["reset", "--hard", "HEAD"], { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
|
|
9049
9112
|
} catch {
|
|
9050
9113
|
}
|
|
9051
9114
|
try {
|
|
9052
|
-
|
|
9115
|
+
execFileSync16("git", ["clean", "-fd", "-e", ".kody"], { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
|
|
9053
9116
|
} catch {
|
|
9054
9117
|
}
|
|
9055
9118
|
}
|
|
@@ -9061,14 +9124,19 @@ function checkoutPrBranch(prNumber, cwd) {
|
|
|
9061
9124
|
GH_TOKEN: process.env.GH_PAT?.trim() || process.env.GH_TOKEN || ""
|
|
9062
9125
|
};
|
|
9063
9126
|
try {
|
|
9064
|
-
|
|
9127
|
+
execFileSync16("git", ["reset", "--hard", "HEAD"], { cwd, env, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
|
|
9065
9128
|
} catch {
|
|
9066
9129
|
}
|
|
9067
9130
|
try {
|
|
9068
|
-
|
|
9131
|
+
execFileSync16("git", ["clean", "-fd", "-e", ".kody"], {
|
|
9132
|
+
cwd,
|
|
9133
|
+
env,
|
|
9134
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
9135
|
+
timeout: 3e4
|
|
9136
|
+
});
|
|
9069
9137
|
} catch {
|
|
9070
9138
|
}
|
|
9071
|
-
|
|
9139
|
+
execFileSync16("gh", ["pr", "checkout", String(prNumber)], {
|
|
9072
9140
|
cwd,
|
|
9073
9141
|
env,
|
|
9074
9142
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -9101,7 +9169,11 @@ function mergeBase(baseBranch, cwd) {
|
|
|
9101
9169
|
}
|
|
9102
9170
|
function restoreKodyAssets(cwd) {
|
|
9103
9171
|
try {
|
|
9104
|
-
|
|
9172
|
+
execFileSync16("git", ["checkout", "HEAD", "--", ".kody"], {
|
|
9173
|
+
cwd,
|
|
9174
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
9175
|
+
timeout: 3e4
|
|
9176
|
+
});
|
|
9105
9177
|
} catch {
|
|
9106
9178
|
}
|
|
9107
9179
|
}
|
|
@@ -9205,68 +9277,6 @@ function ensureFeatureBranchInner(issueNumber, title, defaultBranch2, cwd, baseB
|
|
|
9205
9277
|
return { branch: branchName, created: true };
|
|
9206
9278
|
}
|
|
9207
9279
|
|
|
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
9280
|
// src/scripts/fixCiFlow.ts
|
|
9271
9281
|
init_issue();
|
|
9272
9282
|
|
|
@@ -9515,12 +9525,12 @@ var handleAbandonedGoal = async (ctx) => {
|
|
|
9515
9525
|
|
|
9516
9526
|
// src/scripts/initFlow.ts
|
|
9517
9527
|
import { execFileSync as execFileSync18 } from "child_process";
|
|
9518
|
-
import * as
|
|
9519
|
-
import * as
|
|
9528
|
+
import * as fs32 from "fs";
|
|
9529
|
+
import * as path29 from "path";
|
|
9520
9530
|
function detectPackageManager(cwd) {
|
|
9521
|
-
if (
|
|
9522
|
-
if (
|
|
9523
|
-
if (
|
|
9531
|
+
if (fs32.existsSync(path29.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
9532
|
+
if (fs32.existsSync(path29.join(cwd, "yarn.lock"))) return "yarn";
|
|
9533
|
+
if (fs32.existsSync(path29.join(cwd, "bun.lockb"))) return "bun";
|
|
9524
9534
|
return "npm";
|
|
9525
9535
|
}
|
|
9526
9536
|
function qualityCommandsFor(pm) {
|
|
@@ -9649,36 +9659,36 @@ function performInit(cwd, force) {
|
|
|
9649
9659
|
const pm = detectPackageManager(cwd);
|
|
9650
9660
|
const ownerRepo = detectOwnerRepo(cwd);
|
|
9651
9661
|
const defaultBranch2 = defaultBranchFromGit(cwd);
|
|
9652
|
-
const configPath =
|
|
9653
|
-
if (
|
|
9662
|
+
const configPath = path29.join(cwd, "kody.config.json");
|
|
9663
|
+
if (fs32.existsSync(configPath) && !force) {
|
|
9654
9664
|
skipped.push("kody.config.json");
|
|
9655
9665
|
} else {
|
|
9656
9666
|
const cfg = makeConfig(pm, ownerRepo, defaultBranch2);
|
|
9657
|
-
|
|
9667
|
+
fs32.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
|
|
9658
9668
|
`);
|
|
9659
9669
|
wrote.push("kody.config.json");
|
|
9660
9670
|
}
|
|
9661
|
-
const workflowDir =
|
|
9662
|
-
const workflowPath =
|
|
9663
|
-
if (
|
|
9671
|
+
const workflowDir = path29.join(cwd, ".github", "workflows");
|
|
9672
|
+
const workflowPath = path29.join(workflowDir, "kody.yml");
|
|
9673
|
+
if (fs32.existsSync(workflowPath) && !force) {
|
|
9664
9674
|
skipped.push(".github/workflows/kody.yml");
|
|
9665
9675
|
} else {
|
|
9666
|
-
|
|
9667
|
-
|
|
9676
|
+
fs32.mkdirSync(workflowDir, { recursive: true });
|
|
9677
|
+
fs32.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
|
|
9668
9678
|
wrote.push(".github/workflows/kody.yml");
|
|
9669
9679
|
}
|
|
9670
9680
|
const builtinJobs = listBuiltinJobs();
|
|
9671
9681
|
if (builtinJobs.length > 0) {
|
|
9672
|
-
const jobsDir =
|
|
9673
|
-
|
|
9682
|
+
const jobsDir = path29.join(cwd, ".kody", "duties");
|
|
9683
|
+
fs32.mkdirSync(jobsDir, { recursive: true });
|
|
9674
9684
|
for (const job of builtinJobs) {
|
|
9675
|
-
const rel =
|
|
9676
|
-
const target =
|
|
9677
|
-
if (
|
|
9685
|
+
const rel = path29.join(".kody", "duties", `${job.slug}.md`);
|
|
9686
|
+
const target = path29.join(cwd, rel);
|
|
9687
|
+
if (fs32.existsSync(target) && !force) {
|
|
9678
9688
|
skipped.push(rel);
|
|
9679
9689
|
continue;
|
|
9680
9690
|
}
|
|
9681
|
-
|
|
9691
|
+
fs32.writeFileSync(target, fs32.readFileSync(job.filePath, "utf-8"));
|
|
9682
9692
|
wrote.push(rel);
|
|
9683
9693
|
}
|
|
9684
9694
|
}
|
|
@@ -9690,12 +9700,12 @@ function performInit(cwd, force) {
|
|
|
9690
9700
|
continue;
|
|
9691
9701
|
}
|
|
9692
9702
|
if (profile.kind !== "scheduled" || !profile.schedule) continue;
|
|
9693
|
-
const target =
|
|
9694
|
-
if (
|
|
9703
|
+
const target = path29.join(workflowDir, `kody-${exe.name}.yml`);
|
|
9704
|
+
if (fs32.existsSync(target) && !force) {
|
|
9695
9705
|
skipped.push(`.github/workflows/kody-${exe.name}.yml`);
|
|
9696
9706
|
continue;
|
|
9697
9707
|
}
|
|
9698
|
-
|
|
9708
|
+
fs32.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
|
|
9699
9709
|
wrote.push(`.github/workflows/kody-${exe.name}.yml`);
|
|
9700
9710
|
}
|
|
9701
9711
|
let labels;
|
|
@@ -9880,8 +9890,8 @@ var loadIssueStateComment = async (ctx, _profile, args) => {
|
|
|
9880
9890
|
|
|
9881
9891
|
// src/scripts/loadJobFromFile.ts
|
|
9882
9892
|
init_dutyMcp();
|
|
9883
|
-
import * as
|
|
9884
|
-
import * as
|
|
9893
|
+
import * as fs33 from "fs";
|
|
9894
|
+
import * as path30 from "path";
|
|
9885
9895
|
var DUTY_TOOL_PALETTE = new Set(DUTY_MCP_TOOL_NAMES);
|
|
9886
9896
|
var loadJobFromFile = async (ctx, profile, args) => {
|
|
9887
9897
|
const jobsDir = String(args?.jobsDir ?? ".kody/duties");
|
|
@@ -9891,25 +9901,23 @@ var loadJobFromFile = async (ctx, profile, args) => {
|
|
|
9891
9901
|
if (!slug) {
|
|
9892
9902
|
throw new Error(`loadJobFromFile: ctx.args.${slugArg} must be a non-empty slug`);
|
|
9893
9903
|
}
|
|
9894
|
-
const absPath =
|
|
9895
|
-
if (!
|
|
9904
|
+
const absPath = path30.join(ctx.cwd, jobsDir, `${slug}.md`);
|
|
9905
|
+
if (!fs33.existsSync(absPath)) {
|
|
9896
9906
|
throw new Error(`loadJobFromFile: job file not found: ${absPath}`);
|
|
9897
9907
|
}
|
|
9898
|
-
const raw =
|
|
9908
|
+
const raw = fs33.readFileSync(absPath, "utf-8");
|
|
9899
9909
|
const { title, body } = parseJobFile(raw, slug);
|
|
9900
|
-
const frontmatter =
|
|
9910
|
+
const frontmatter = splitFrontmatter(raw).frontmatter;
|
|
9901
9911
|
const mentions = (frontmatter.mentions ?? []).map((login) => `@${login}`).join(" ");
|
|
9902
9912
|
const workerSlug = (frontmatter.staff ?? "").trim();
|
|
9903
9913
|
let workerTitle = "";
|
|
9904
9914
|
let workerPersona = "";
|
|
9905
9915
|
if (workerSlug) {
|
|
9906
|
-
const workerPath =
|
|
9907
|
-
if (!
|
|
9908
|
-
throw new Error(
|
|
9909
|
-
`loadJobFromFile: duty '${slug}' declares staff '${workerSlug}' but ${workerPath} does not exist`
|
|
9910
|
-
);
|
|
9916
|
+
const workerPath = path30.join(ctx.cwd, workersDir, `${workerSlug}.md`);
|
|
9917
|
+
if (!fs33.existsSync(workerPath)) {
|
|
9918
|
+
throw new Error(`loadJobFromFile: duty '${slug}' declares staff '${workerSlug}' but ${workerPath} does not exist`);
|
|
9911
9919
|
}
|
|
9912
|
-
const workerRaw =
|
|
9920
|
+
const workerRaw = fs33.readFileSync(workerPath, "utf-8");
|
|
9913
9921
|
const parsed = parseJobFile(workerRaw, workerSlug);
|
|
9914
9922
|
workerTitle = parsed.title;
|
|
9915
9923
|
workerPersona = parsed.body;
|
|
@@ -9993,18 +10001,18 @@ init_loadMemoryContext();
|
|
|
9993
10001
|
init_loadPriorArt();
|
|
9994
10002
|
|
|
9995
10003
|
// src/scripts/loadQaContext.ts
|
|
9996
|
-
import * as fs36 from "fs";
|
|
9997
|
-
import * as path33 from "path";
|
|
9998
|
-
|
|
9999
|
-
// src/scripts/kodyVariables.ts
|
|
10000
10004
|
import * as fs35 from "fs";
|
|
10001
10005
|
import * as path32 from "path";
|
|
10006
|
+
|
|
10007
|
+
// src/scripts/kodyVariables.ts
|
|
10008
|
+
import * as fs34 from "fs";
|
|
10009
|
+
import * as path31 from "path";
|
|
10002
10010
|
var KODY_VARIABLES_REL_PATH = ".kody/variables.json";
|
|
10003
10011
|
function readKodyVariables(cwd) {
|
|
10004
|
-
const full =
|
|
10012
|
+
const full = path31.join(cwd, KODY_VARIABLES_REL_PATH);
|
|
10005
10013
|
let raw;
|
|
10006
10014
|
try {
|
|
10007
|
-
raw =
|
|
10015
|
+
raw = fs34.readFileSync(full, "utf-8");
|
|
10008
10016
|
} catch {
|
|
10009
10017
|
return {};
|
|
10010
10018
|
}
|
|
@@ -10029,7 +10037,9 @@ var LEGACY_AUDIENCE_TO_STAFF = { chat: "kody", qa: QA_STAFF };
|
|
|
10029
10037
|
var FRONTMATTER_RE2 = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
10030
10038
|
function parseSlugList(value) {
|
|
10031
10039
|
const inner = value.startsWith("[") && value.endsWith("]") ? value.slice(1, -1) : value;
|
|
10032
|
-
return inner.split(",").map(
|
|
10040
|
+
return inner.split(",").map(
|
|
10041
|
+
(s) => s.trim().replace(/^["']|["']$/g, "").toLowerCase()
|
|
10042
|
+
).filter(Boolean);
|
|
10033
10043
|
}
|
|
10034
10044
|
function readProfileStaff(raw) {
|
|
10035
10045
|
const m = FRONTMATTER_RE2.exec(raw);
|
|
@@ -10053,18 +10063,18 @@ function readProfileStaff(raw) {
|
|
|
10053
10063
|
return { staff: staff ?? legacy ?? ["kody"], body };
|
|
10054
10064
|
}
|
|
10055
10065
|
function readProfile(cwd) {
|
|
10056
|
-
const dir =
|
|
10057
|
-
if (!
|
|
10066
|
+
const dir = path32.join(cwd, CONTEXT_DIR_REL_PATH);
|
|
10067
|
+
if (!fs35.existsSync(dir)) return "";
|
|
10058
10068
|
let entries;
|
|
10059
10069
|
try {
|
|
10060
|
-
entries =
|
|
10070
|
+
entries = fs35.readdirSync(dir).filter((f) => f.endsWith(".md")).sort();
|
|
10061
10071
|
} catch {
|
|
10062
10072
|
return "";
|
|
10063
10073
|
}
|
|
10064
10074
|
const blocks = [];
|
|
10065
10075
|
for (const file of entries) {
|
|
10066
10076
|
try {
|
|
10067
|
-
const raw =
|
|
10077
|
+
const raw = fs35.readFileSync(path32.join(dir, file), "utf-8");
|
|
10068
10078
|
const { staff, body } = readProfileStaff(raw);
|
|
10069
10079
|
if (!staff.includes(QA_STAFF) && !staff.includes(ALL_STAFF)) continue;
|
|
10070
10080
|
blocks.push(`## ${file}
|
|
@@ -10101,8 +10111,8 @@ var loadQaContext = async (ctx) => {
|
|
|
10101
10111
|
init_events();
|
|
10102
10112
|
|
|
10103
10113
|
// src/taskContext.ts
|
|
10104
|
-
import * as
|
|
10105
|
-
import * as
|
|
10114
|
+
import * as fs36 from "fs";
|
|
10115
|
+
import * as path33 from "path";
|
|
10106
10116
|
var TASK_CONTEXT_SCHEMA_VERSION = 1;
|
|
10107
10117
|
function buildTaskContext(args) {
|
|
10108
10118
|
return {
|
|
@@ -10118,10 +10128,10 @@ function buildTaskContext(args) {
|
|
|
10118
10128
|
}
|
|
10119
10129
|
function persistTaskContext(cwd, ctx) {
|
|
10120
10130
|
try {
|
|
10121
|
-
const dir =
|
|
10122
|
-
|
|
10123
|
-
const file =
|
|
10124
|
-
|
|
10131
|
+
const dir = path33.join(cwd, ".kody", "runs", ctx.runId);
|
|
10132
|
+
fs36.mkdirSync(dir, { recursive: true });
|
|
10133
|
+
const file = path33.join(dir, "task-context.json");
|
|
10134
|
+
fs36.writeFileSync(file, `${JSON.stringify(ctx, null, 2)}
|
|
10125
10135
|
`);
|
|
10126
10136
|
return file;
|
|
10127
10137
|
} catch (err) {
|
|
@@ -10187,19 +10197,19 @@ var loadTaskState = async (ctx) => {
|
|
|
10187
10197
|
};
|
|
10188
10198
|
|
|
10189
10199
|
// src/scripts/loadWorkerAdhoc.ts
|
|
10190
|
-
import * as
|
|
10191
|
-
import * as
|
|
10200
|
+
import * as fs37 from "fs";
|
|
10201
|
+
import * as path34 from "path";
|
|
10192
10202
|
var loadWorkerAdhoc = async (ctx, _profile, args) => {
|
|
10193
10203
|
const workersDir = String(args?.workersDir ?? ".kody/staff");
|
|
10194
10204
|
const workerSlug = String(ctx.args.worker ?? "").trim();
|
|
10195
10205
|
if (!workerSlug) {
|
|
10196
10206
|
throw new Error("loadWorkerAdhoc: ctx.args.worker must be a non-empty slug");
|
|
10197
10207
|
}
|
|
10198
|
-
const workerPath =
|
|
10199
|
-
if (!
|
|
10208
|
+
const workerPath = path34.join(ctx.cwd, workersDir, `${workerSlug}.md`);
|
|
10209
|
+
if (!fs37.existsSync(workerPath)) {
|
|
10200
10210
|
throw new Error(`loadWorkerAdhoc: worker persona not found: ${workerPath}`);
|
|
10201
10211
|
}
|
|
10202
|
-
const { title, body } = parsePersona(
|
|
10212
|
+
const { title, body } = parsePersona(fs37.readFileSync(workerPath, "utf-8"), workerSlug);
|
|
10203
10213
|
const message = resolveMessage(ctx.args.message);
|
|
10204
10214
|
if (!message) {
|
|
10205
10215
|
throw new Error(
|
|
@@ -10219,9 +10229,9 @@ function resolveMessage(messageArg) {
|
|
|
10219
10229
|
}
|
|
10220
10230
|
function readCommentBody() {
|
|
10221
10231
|
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
10222
|
-
if (!eventPath || !
|
|
10232
|
+
if (!eventPath || !fs37.existsSync(eventPath)) return "";
|
|
10223
10233
|
try {
|
|
10224
|
-
const event = JSON.parse(
|
|
10234
|
+
const event = JSON.parse(fs37.readFileSync(eventPath, "utf-8"));
|
|
10225
10235
|
return String(event.comment?.body ?? "");
|
|
10226
10236
|
} catch {
|
|
10227
10237
|
return "";
|
|
@@ -10245,7 +10255,7 @@ function stripDirective(body) {
|
|
|
10245
10255
|
return lines.slice(start).join("\n").trim();
|
|
10246
10256
|
}
|
|
10247
10257
|
function parsePersona(raw, slug) {
|
|
10248
|
-
const stripped =
|
|
10258
|
+
const stripped = splitFrontmatter(raw).body;
|
|
10249
10259
|
const trimmed = stripped.trim();
|
|
10250
10260
|
const firstLine2 = trimmed.split("\n", 1)[0] ?? "";
|
|
10251
10261
|
const h1 = /^#\s+(.+?)\s*$/.exec(firstLine2);
|
|
@@ -10270,16 +10280,9 @@ var markFlowSuccess = async (ctx) => {
|
|
|
10270
10280
|
// src/scripts/mergeFlow.ts
|
|
10271
10281
|
init_issue();
|
|
10272
10282
|
function readPr(prNumber, cwd) {
|
|
10273
|
-
const out = gh(
|
|
10274
|
-
|
|
10275
|
-
|
|
10276
|
-
"view",
|
|
10277
|
-
String(prNumber),
|
|
10278
|
-
"--json",
|
|
10279
|
-
"state,isDraft,mergeable,mergeStateStatus,title,url"
|
|
10280
|
-
],
|
|
10281
|
-
{ cwd }
|
|
10282
|
-
);
|
|
10283
|
+
const out = gh(["pr", "view", String(prNumber), "--json", "state,isDraft,mergeable,mergeStateStatus,title,url"], {
|
|
10284
|
+
cwd
|
|
10285
|
+
});
|
|
10283
10286
|
const p = JSON.parse(out);
|
|
10284
10287
|
return {
|
|
10285
10288
|
state: p.state ?? "UNKNOWN",
|
|
@@ -10356,11 +10359,7 @@ var mergeFlow = async (ctx) => {
|
|
|
10356
10359
|
`);
|
|
10357
10360
|
ctx.data.mergeAction = verdict.action;
|
|
10358
10361
|
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
|
-
);
|
|
10362
|
+
commentOnIssue(prNumber, `\u{1F6A6} _Auto-merge held: ${verdict.reason} Kody will retry once the PR is CLEAN._`, ctx.cwd);
|
|
10364
10363
|
}
|
|
10365
10364
|
return;
|
|
10366
10365
|
}
|
|
@@ -10766,10 +10765,7 @@ var parseReproOutput = async (ctx, _profile, agentResult) => {
|
|
|
10766
10765
|
}
|
|
10767
10766
|
}
|
|
10768
10767
|
if (!signature) {
|
|
10769
|
-
downgrade(
|
|
10770
|
-
ctx,
|
|
10771
|
-
"reproduce missing or malformed FAILURE_SIGNATURE JSON (must contain errorType + messageContains)"
|
|
10772
|
-
);
|
|
10768
|
+
downgrade(ctx, "reproduce missing or malformed FAILURE_SIGNATURE JSON (must contain errorType + messageContains)");
|
|
10773
10769
|
return;
|
|
10774
10770
|
}
|
|
10775
10771
|
ctx.data.reproTestPath = testPath;
|
|
@@ -10796,7 +10792,7 @@ function stripMarkdownEmphasis2(s) {
|
|
|
10796
10792
|
}
|
|
10797
10793
|
function downgrade(ctx, reason) {
|
|
10798
10794
|
const action = ctx.data.action;
|
|
10799
|
-
if (action
|
|
10795
|
+
if (action?.type.endsWith("_COMPLETED")) {
|
|
10800
10796
|
ctx.data.action = {
|
|
10801
10797
|
type: action.type.replace(/_COMPLETED$/, "_FAILED"),
|
|
10802
10798
|
payload: { reason, downgradedFrom: action.type },
|
|
@@ -10855,6 +10851,102 @@ var persistFlowState = async (ctx) => {
|
|
|
10855
10851
|
import { spawn as spawn4 } from "child_process";
|
|
10856
10852
|
import { createServer as createServer2 } from "http";
|
|
10857
10853
|
|
|
10854
|
+
// src/github-health.ts
|
|
10855
|
+
var STATUS_URL = "https://www.githubstatus.com/api/v2/components.json";
|
|
10856
|
+
var STATUS_CACHE_TTL_MS = 3e4;
|
|
10857
|
+
var statusCache = null;
|
|
10858
|
+
async function probeActionsStatus(fetchImpl = fetch) {
|
|
10859
|
+
if (statusCache && statusCache.expiresAt > Date.now()) return statusCache.probe;
|
|
10860
|
+
try {
|
|
10861
|
+
const res = await fetchImpl(STATUS_URL, { headers: { "User-Agent": "kody-engine" } });
|
|
10862
|
+
if (!res.ok) return { degraded: false, label: `http_${res.status}` };
|
|
10863
|
+
const body = await res.json();
|
|
10864
|
+
const actions = (body.components ?? []).find((c) => (c.name ?? "").trim().toLowerCase() === "actions");
|
|
10865
|
+
const label = actions?.status ?? "unknown";
|
|
10866
|
+
const degraded = !!actions && label !== "operational";
|
|
10867
|
+
const probe = { degraded, label };
|
|
10868
|
+
statusCache = { probe, expiresAt: Date.now() + STATUS_CACHE_TTL_MS };
|
|
10869
|
+
return probe;
|
|
10870
|
+
} catch {
|
|
10871
|
+
return { degraded: false, label: "probe_error" };
|
|
10872
|
+
}
|
|
10873
|
+
}
|
|
10874
|
+
async function gitHubActionsDegraded(fetchImpl = fetch) {
|
|
10875
|
+
return (await probeActionsStatus(fetchImpl)).degraded;
|
|
10876
|
+
}
|
|
10877
|
+
|
|
10878
|
+
// src/pool/duty-fallback-tick.ts
|
|
10879
|
+
async function runDutyFallbackTick(deps) {
|
|
10880
|
+
if (!await deps.isDegraded()) {
|
|
10881
|
+
return { ran: false, claimed: 0 };
|
|
10882
|
+
}
|
|
10883
|
+
const repos = deps.activeRepos();
|
|
10884
|
+
if (repos.length === 0) {
|
|
10885
|
+
deps.log("GitHub Actions degraded but no active repo pools \u2014 nothing to tick");
|
|
10886
|
+
return { ran: true, claimed: 0 };
|
|
10887
|
+
}
|
|
10888
|
+
deps.log(`GitHub Actions degraded \u2014 running scheduled fan-out on Fly for ${repos.length} repo(s)`);
|
|
10889
|
+
const clock = deps.now ?? Date.now;
|
|
10890
|
+
let claimed = 0;
|
|
10891
|
+
for (const tag of repos) {
|
|
10892
|
+
const [owner, repo] = tag.split("/");
|
|
10893
|
+
if (!owner || !repo) continue;
|
|
10894
|
+
try {
|
|
10895
|
+
const res = await deps.claim(owner, repo, {
|
|
10896
|
+
jobId: `sched-${owner}-${repo}-${clock()}`,
|
|
10897
|
+
repo: tag,
|
|
10898
|
+
mode: "scheduled"
|
|
10899
|
+
});
|
|
10900
|
+
if (res.ok) {
|
|
10901
|
+
claimed++;
|
|
10902
|
+
deps.log(`[${tag}] scheduled fan-out claimed ${res.machineId}`);
|
|
10903
|
+
} else {
|
|
10904
|
+
deps.log(`[${tag}] scheduled fan-out skipped: ${res.reason ?? "pool unavailable"}`);
|
|
10905
|
+
}
|
|
10906
|
+
} catch (err) {
|
|
10907
|
+
deps.log(`[${tag}] scheduled fan-out error: ${err instanceof Error ? err.message : String(err)}`);
|
|
10908
|
+
}
|
|
10909
|
+
}
|
|
10910
|
+
return { ran: true, claimed };
|
|
10911
|
+
}
|
|
10912
|
+
|
|
10913
|
+
// src/pool/keys.ts
|
|
10914
|
+
import { hkdfSync } from "crypto";
|
|
10915
|
+
var POOL_API_KEY_INFO = "kody-pool-api:v1";
|
|
10916
|
+
var RUNNER_API_KEY_INFO = "kody-runner-api:v1";
|
|
10917
|
+
function masterKeyBytes(raw) {
|
|
10918
|
+
const v = raw.trim();
|
|
10919
|
+
if (!v) throw new Error("KODY_MASTER_KEY is empty");
|
|
10920
|
+
if (/^[0-9a-fA-F]+$/.test(v) && v.length === 64) {
|
|
10921
|
+
return Buffer.from(v, "hex");
|
|
10922
|
+
}
|
|
10923
|
+
return Buffer.from(v.replace(/-/g, "+").replace(/_/g, "/"), "base64");
|
|
10924
|
+
}
|
|
10925
|
+
function deriveKey(master, info, length = 32) {
|
|
10926
|
+
return Buffer.from(hkdfSync("sha256", master, Buffer.alloc(0), info, length)).toString("hex");
|
|
10927
|
+
}
|
|
10928
|
+
function derivePoolApiKey(master) {
|
|
10929
|
+
return deriveKey(master, POOL_API_KEY_INFO);
|
|
10930
|
+
}
|
|
10931
|
+
function deriveRunnerApiKey(master) {
|
|
10932
|
+
return deriveKey(master, RUNNER_API_KEY_INFO);
|
|
10933
|
+
}
|
|
10934
|
+
function bearerOk(headerAuth, xApiKey, expected) {
|
|
10935
|
+
const x = (xApiKey ?? "").trim();
|
|
10936
|
+
if (x && timingEqual(x, expected)) return true;
|
|
10937
|
+
const a = (headerAuth ?? "").trim();
|
|
10938
|
+
if (a.toLowerCase().startsWith("bearer ")) {
|
|
10939
|
+
return timingEqual(a.slice(7).trim(), expected);
|
|
10940
|
+
}
|
|
10941
|
+
return false;
|
|
10942
|
+
}
|
|
10943
|
+
function timingEqual(a, b) {
|
|
10944
|
+
if (a.length !== b.length) return false;
|
|
10945
|
+
let diff = 0;
|
|
10946
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
10947
|
+
return diff === 0;
|
|
10948
|
+
}
|
|
10949
|
+
|
|
10858
10950
|
// src/pool/fly.ts
|
|
10859
10951
|
var FLY_API_BASE = "https://api.machines.dev/v1";
|
|
10860
10952
|
var POOL_METADATA_KEY = "kody_pool";
|
|
@@ -11231,7 +11323,7 @@ async function readVaultSecrets(opts) {
|
|
|
11231
11323
|
async function readRepoSecret(opts) {
|
|
11232
11324
|
const secrets = await readVaultSecrets(opts);
|
|
11233
11325
|
const v = secrets[opts.name];
|
|
11234
|
-
return v
|
|
11326
|
+
return v?.trim() ? v : null;
|
|
11235
11327
|
}
|
|
11236
11328
|
async function readRepoSecrets(opts) {
|
|
11237
11329
|
return readVaultSecrets(opts);
|
|
@@ -11297,7 +11389,9 @@ var PoolRegistry = class {
|
|
|
11297
11389
|
try {
|
|
11298
11390
|
min = await this.resolvePoolMin(owner, repo);
|
|
11299
11391
|
} catch (err) {
|
|
11300
|
-
this.log(
|
|
11392
|
+
this.log(
|
|
11393
|
+
`registry: pool-min read failed for ${repoTag}, using default ${min}: ${err instanceof Error ? err.message : String(err)}`
|
|
11394
|
+
);
|
|
11301
11395
|
}
|
|
11302
11396
|
const fly = new FlyClient({ token: flyToken, app: this.cfg.base.app });
|
|
11303
11397
|
const pm = new PoolManager({
|
|
@@ -11325,7 +11419,9 @@ var PoolRegistry = class {
|
|
|
11325
11419
|
Object.entries(vault).filter(([k]) => k !== "FLY_API_TOKEN" && k !== POOL_MIN_VAULT_KEY)
|
|
11326
11420
|
);
|
|
11327
11421
|
} catch (err) {
|
|
11328
|
-
this.log(
|
|
11422
|
+
this.log(
|
|
11423
|
+
`[${this.key(owner, repo)}] vault secrets read failed: ${err instanceof Error ? err.message : String(err)}`
|
|
11424
|
+
);
|
|
11329
11425
|
}
|
|
11330
11426
|
const job = {
|
|
11331
11427
|
jobId: req.jobId,
|
|
@@ -11366,102 +11462,6 @@ var PoolRegistry = class {
|
|
|
11366
11462
|
}
|
|
11367
11463
|
};
|
|
11368
11464
|
|
|
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
11465
|
// src/scripts/poolServe.ts
|
|
11466
11466
|
var PERF_GUEST = {
|
|
11467
11467
|
low: { cpu_kind: "shared", cpus: 2, memory_mb: 2048 },
|
|
@@ -11609,7 +11609,7 @@ var poolServe = async (ctx) => {
|
|
|
11609
11609
|
});
|
|
11610
11610
|
}
|
|
11611
11611
|
const authed = bearerOk(
|
|
11612
|
-
req.headers
|
|
11612
|
+
req.headers.authorization,
|
|
11613
11613
|
req.headers["x-api-key"],
|
|
11614
11614
|
poolApiKey
|
|
11615
11615
|
);
|
|
@@ -11839,21 +11839,56 @@ ${plan}`;
|
|
|
11839
11839
|
---
|
|
11840
11840
|
_Orchestrator will advance to the next step automatically._`;
|
|
11841
11841
|
}
|
|
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 });
|
|
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 });
|
|
11851
|
+
};
|
|
11852
|
+
function renderResearchComment(issueNumber, body) {
|
|
11853
|
+
return `## Research for issue #${issueNumber}
|
|
11854
|
+
|
|
11855
|
+
${body}`;
|
|
11856
|
+
}
|
|
11857
|
+
|
|
11858
|
+
// src/scripts/promoteQaGoal.ts
|
|
11859
|
+
init_issue();
|
|
11860
|
+
var REPORT_JSON_OPEN2 = "<!-- KODY_QA_REPORT_JSON";
|
|
11861
|
+
var promoteQaGoal = async (ctx) => {
|
|
11862
|
+
ctx.skipAgent = true;
|
|
11863
|
+
const issueNum = ctx.args.issue;
|
|
11864
|
+
if (typeof issueNum !== "number" || issueNum <= 0) {
|
|
11865
|
+
ctx.output.exitCode = 2;
|
|
11866
|
+
ctx.output.reason = "qa-goal requires --issue <n>";
|
|
11867
|
+
process.stderr.write("[qa-goal] missing --issue\n");
|
|
11868
|
+
return;
|
|
11869
|
+
}
|
|
11870
|
+
let report;
|
|
11871
|
+
try {
|
|
11872
|
+
const issue = getIssue(issueNum, ctx.cwd);
|
|
11873
|
+
const reportComment = [...issue.comments].reverse().find((c) => c.body.includes(REPORT_JSON_OPEN2));
|
|
11874
|
+
if (!reportComment) {
|
|
11875
|
+
ctx.output.exitCode = 3;
|
|
11876
|
+
ctx.output.reason = `no QA report (${REPORT_JSON_OPEN2} \u2026) found on issue #${issueNum}`;
|
|
11877
|
+
process.stderr.write(`[qa-goal] ${ctx.output.reason}
|
|
11878
|
+
`);
|
|
11879
|
+
return;
|
|
11880
|
+
}
|
|
11881
|
+
report = reportComment.body;
|
|
11882
|
+
} catch (err) {
|
|
11883
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
11884
|
+
ctx.output.exitCode = 3;
|
|
11885
|
+
ctx.output.reason = `failed to read issue #${issueNum}: ${msg}`;
|
|
11886
|
+
process.stderr.write(`[qa-goal] ${ctx.output.reason}
|
|
11887
|
+
`);
|
|
11888
|
+
return;
|
|
11889
|
+
}
|
|
11890
|
+
await promoteReportToGoal(ctx, report, ctx.args.scope, ctx.args.goal);
|
|
11851
11891
|
};
|
|
11852
|
-
function renderResearchComment(issueNumber, body) {
|
|
11853
|
-
return `## Research for issue #${issueNumber}
|
|
11854
|
-
|
|
11855
|
-
${body}`;
|
|
11856
|
-
}
|
|
11857
11892
|
|
|
11858
11893
|
// src/scripts/recordClassification.ts
|
|
11859
11894
|
import { execFileSync as execFileSync20 } from "child_process";
|
|
@@ -11962,7 +11997,7 @@ function countActionItems(block) {
|
|
|
11962
11997
|
}
|
|
11963
11998
|
|
|
11964
11999
|
// src/scripts/requirePlanDeviations.ts
|
|
11965
|
-
var requirePlanDeviations = async (ctx,
|
|
12000
|
+
var requirePlanDeviations = async (ctx, _profile) => {
|
|
11966
12001
|
if (!ctx.data.agentDone) return;
|
|
11967
12002
|
const artifacts = ctx.data.artifacts ?? {};
|
|
11968
12003
|
const planContent = (artifacts.plan ?? "").trim();
|
|
@@ -12167,46 +12202,6 @@ function pushEmptyCommit(branch, cwd) {
|
|
|
12167
12202
|
}
|
|
12168
12203
|
}
|
|
12169
12204
|
|
|
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
12205
|
// src/deployments.ts
|
|
12211
12206
|
init_issue();
|
|
12212
12207
|
function findPreviewDeploymentUrl(prNumber, cwd) {
|
|
@@ -12527,7 +12522,13 @@ var runFlow = async (ctx) => {
|
|
|
12527
12522
|
process.stderr.write(`[kody runFlow] resolved base branch: ${base} (from --base)
|
|
12528
12523
|
`);
|
|
12529
12524
|
}
|
|
12530
|
-
const branchInfo = ensureFeatureBranch(
|
|
12525
|
+
const branchInfo = ensureFeatureBranch(
|
|
12526
|
+
issueNumber,
|
|
12527
|
+
issue.title,
|
|
12528
|
+
ctx.config.git.defaultBranch,
|
|
12529
|
+
ctx.cwd,
|
|
12530
|
+
base ?? void 0
|
|
12531
|
+
);
|
|
12531
12532
|
ctx.data.branch = branchInfo.branch;
|
|
12532
12533
|
const runUrl = getRunUrl();
|
|
12533
12534
|
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 +12550,22 @@ function resolveBaseOverride(value) {
|
|
|
12549
12550
|
|
|
12550
12551
|
// src/scripts/runnerServe.ts
|
|
12551
12552
|
import { spawn as spawn5 } from "child_process";
|
|
12553
|
+
import * as fs38 from "fs";
|
|
12552
12554
|
import { createServer as createServer3 } from "http";
|
|
12553
|
-
import * as fs39 from "fs";
|
|
12554
12555
|
var DEFAULT_PORT2 = 8080;
|
|
12555
12556
|
var DEFAULT_WORKDIR = "/workspace/repo";
|
|
12556
12557
|
function getApiKey2() {
|
|
12557
12558
|
const key = (process.env.RUNNER_API_KEY ?? "").trim();
|
|
12558
12559
|
if (!key) {
|
|
12559
|
-
throw new Error(
|
|
12560
|
-
"RUNNER_API_KEY env var is required \u2014 set it on the pooled machine before boot."
|
|
12561
|
-
);
|
|
12560
|
+
throw new Error("RUNNER_API_KEY env var is required \u2014 set it on the pooled machine before boot.");
|
|
12562
12561
|
}
|
|
12563
12562
|
return key;
|
|
12564
12563
|
}
|
|
12565
12564
|
function authOk2(req, expected) {
|
|
12566
12565
|
const xApiKey = req.headers["x-api-key"]?.trim();
|
|
12567
12566
|
if (xApiKey && xApiKey === expected) return true;
|
|
12568
|
-
const auth = req.headers
|
|
12569
|
-
if (auth
|
|
12567
|
+
const auth = req.headers.authorization?.trim();
|
|
12568
|
+
if (auth?.toLowerCase().startsWith("bearer ")) {
|
|
12570
12569
|
return auth.slice(7).trim() === expected;
|
|
12571
12570
|
}
|
|
12572
12571
|
return false;
|
|
@@ -12631,8 +12630,8 @@ async function defaultRunJob(job) {
|
|
|
12631
12630
|
const workdir = process.env.RUNNER_WORKDIR ?? DEFAULT_WORKDIR;
|
|
12632
12631
|
const branch = job.ref ?? "main";
|
|
12633
12632
|
const authUrl = `https://x-access-token:${job.githubToken}@github.com/${job.repo}.git`;
|
|
12634
|
-
|
|
12635
|
-
|
|
12633
|
+
fs38.rmSync(workdir, { recursive: true, force: true });
|
|
12634
|
+
fs38.mkdirSync(workdir, { recursive: true });
|
|
12636
12635
|
const allSecrets = typeof job.allSecrets === "string" ? job.allSecrets : JSON.stringify(job.allSecrets ?? {});
|
|
12637
12636
|
const interactive = job.mode === "interactive";
|
|
12638
12637
|
const scheduled = job.mode === "scheduled";
|
|
@@ -12672,15 +12671,7 @@ async function defaultRunJob(job) {
|
|
|
12672
12671
|
});
|
|
12673
12672
|
process.stdout.write(`[runner-serve] job ${job.jobId}: cloning ${job.repo}@${branch}
|
|
12674
12673
|
`);
|
|
12675
|
-
const cloneCode = await run("git", [
|
|
12676
|
-
"clone",
|
|
12677
|
-
"--depth=1",
|
|
12678
|
-
"--single-branch",
|
|
12679
|
-
"--branch",
|
|
12680
|
-
branch,
|
|
12681
|
-
authUrl,
|
|
12682
|
-
workdir
|
|
12683
|
-
]);
|
|
12674
|
+
const cloneCode = await run("git", ["clone", "--depth=1", "--single-branch", "--branch", branch, authUrl, workdir]);
|
|
12684
12675
|
if (cloneCode !== 0) {
|
|
12685
12676
|
process.stderr.write(`[runner-serve] job ${job.jobId}: clone failed (${cloneCode})
|
|
12686
12677
|
`);
|
|
@@ -12773,7 +12764,7 @@ var runnerServe = async (ctx) => {
|
|
|
12773
12764
|
|
|
12774
12765
|
// src/scripts/runPreviewBuild.ts
|
|
12775
12766
|
import { copyFile, writeFile } from "fs/promises";
|
|
12776
|
-
import * as
|
|
12767
|
+
import * as path35 from "path";
|
|
12777
12768
|
import { fileURLToPath } from "url";
|
|
12778
12769
|
|
|
12779
12770
|
// src/scripts/previewBuildHelpers.ts
|
|
@@ -12901,29 +12892,12 @@ async function setupNamespaceBuilder(opts) {
|
|
|
12901
12892
|
await runCmd("bash", ["-c", NSC_INSTALL]);
|
|
12902
12893
|
const jwt = await fetchGithubOidcToken(NSC_OIDC_AUDIENCE);
|
|
12903
12894
|
if (!jwt) {
|
|
12904
|
-
console.warn(
|
|
12905
|
-
"[preview-build] no GitHub OIDC token (id-token: write missing?) \u2014 local docker build"
|
|
12906
|
-
);
|
|
12895
|
+
console.warn("[preview-build] no GitHub OIDC token (id-token: write missing?) \u2014 local docker build");
|
|
12907
12896
|
return null;
|
|
12908
12897
|
}
|
|
12909
|
-
await runCmd("nsc", [
|
|
12910
|
-
|
|
12911
|
-
|
|
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
|
-
);
|
|
12898
|
+
await runCmd("nsc", ["auth", "exchange-oidc-token", "--tenant_id", opts.tenantId, "--token", jwt]);
|
|
12899
|
+
await runCmd("nsc", ["docker", "buildx", "setup", "--name", opts.builderName]);
|
|
12900
|
+
console.log(`[preview-build] Namespace remote builder ready (${opts.builderName})`);
|
|
12927
12901
|
return opts.builderName;
|
|
12928
12902
|
} catch (err) {
|
|
12929
12903
|
console.warn(
|
|
@@ -12939,9 +12913,9 @@ var FLY_MACHINES = "https://api.machines.dev/v1";
|
|
|
12939
12913
|
var FLY_GRAPHQL = "https://api.fly.io/graphql";
|
|
12940
12914
|
var REQ_TIMEOUT_MS2 = 3e4;
|
|
12941
12915
|
function bundledDockerfilePath(mode) {
|
|
12942
|
-
const here =
|
|
12916
|
+
const here = path35.dirname(fileURLToPath(import.meta.url));
|
|
12943
12917
|
const file = mode === "dev" ? "default-Dockerfile.preview.dev" : "default-Dockerfile.preview.prod";
|
|
12944
|
-
return
|
|
12918
|
+
return path35.join(here, "preview-build-templates", file);
|
|
12945
12919
|
}
|
|
12946
12920
|
function required(name) {
|
|
12947
12921
|
const v = (process.env[name] ?? "").trim();
|
|
@@ -13026,13 +13000,10 @@ async function flyAllocateSharedIps(appName, token) {
|
|
|
13026
13000
|
}
|
|
13027
13001
|
}
|
|
13028
13002
|
async function flyListMachines(appName, token) {
|
|
13029
|
-
const res = await fetch(
|
|
13030
|
-
|
|
13031
|
-
|
|
13032
|
-
|
|
13033
|
-
signal: AbortSignal.timeout(REQ_TIMEOUT_MS2)
|
|
13034
|
-
}
|
|
13035
|
-
);
|
|
13003
|
+
const res = await fetch(`${FLY_MACHINES}/apps/${encodeURIComponent(appName)}/machines`, {
|
|
13004
|
+
headers: flyHeaders(token),
|
|
13005
|
+
signal: AbortSignal.timeout(REQ_TIMEOUT_MS2)
|
|
13006
|
+
});
|
|
13036
13007
|
if (res.status === 404) return [];
|
|
13037
13008
|
if (!res.ok) {
|
|
13038
13009
|
throw new Error(`listMachines ${appName}: ${res.status}`);
|
|
@@ -13041,14 +13012,11 @@ async function flyListMachines(appName, token) {
|
|
|
13041
13012
|
return data.map((m) => ({ id: m.id, state: m.state }));
|
|
13042
13013
|
}
|
|
13043
13014
|
async function flyDestroyMachine(appName, machineId, token) {
|
|
13044
|
-
await fetch(
|
|
13045
|
-
|
|
13046
|
-
|
|
13047
|
-
|
|
13048
|
-
|
|
13049
|
-
signal: AbortSignal.timeout(REQ_TIMEOUT_MS2)
|
|
13050
|
-
}
|
|
13051
|
-
).catch(() => void 0);
|
|
13015
|
+
await fetch(`${FLY_MACHINES}/apps/${encodeURIComponent(appName)}/machines/${encodeURIComponent(machineId)}/stop`, {
|
|
13016
|
+
method: "POST",
|
|
13017
|
+
headers: flyHeaders(token),
|
|
13018
|
+
signal: AbortSignal.timeout(REQ_TIMEOUT_MS2)
|
|
13019
|
+
}).catch(() => void 0);
|
|
13052
13020
|
const res = await fetch(
|
|
13053
13021
|
`${FLY_MACHINES}/apps/${encodeURIComponent(appName)}/machines/${encodeURIComponent(machineId)}?force=true`,
|
|
13054
13022
|
{
|
|
@@ -13101,23 +13069,18 @@ async function flyCreatePreviewMachine(args, token) {
|
|
|
13101
13069
|
};
|
|
13102
13070
|
let lastErr = null;
|
|
13103
13071
|
for (let attempt = 0; attempt < 6; attempt++) {
|
|
13104
|
-
const res = await fetch(
|
|
13105
|
-
|
|
13106
|
-
|
|
13107
|
-
|
|
13108
|
-
|
|
13109
|
-
|
|
13110
|
-
signal: AbortSignal.timeout(REQ_TIMEOUT_MS2)
|
|
13111
|
-
}
|
|
13112
|
-
);
|
|
13072
|
+
const res = await fetch(`${FLY_MACHINES}/apps/${encodeURIComponent(args.appName)}/machines`, {
|
|
13073
|
+
method: "POST",
|
|
13074
|
+
headers: flyHeaders(token),
|
|
13075
|
+
body: JSON.stringify(body),
|
|
13076
|
+
signal: AbortSignal.timeout(REQ_TIMEOUT_MS2)
|
|
13077
|
+
});
|
|
13113
13078
|
if (res.ok) {
|
|
13114
13079
|
const { id } = await res.json();
|
|
13115
13080
|
return id;
|
|
13116
13081
|
}
|
|
13117
13082
|
const text = await res.text().catch(() => "");
|
|
13118
|
-
lastErr = new Error(
|
|
13119
|
-
`createPreviewMachine ${res.status}: ${text.slice(0, 300)}`
|
|
13120
|
-
);
|
|
13083
|
+
lastErr = new Error(`createPreviewMachine ${res.status}: ${text.slice(0, 300)}`);
|
|
13121
13084
|
if (!/MANIFEST_UNKNOWN|manifest unknown/i.test(text)) break;
|
|
13122
13085
|
await new Promise((r) => setTimeout(r, 2e3 * (attempt + 1)));
|
|
13123
13086
|
}
|
|
@@ -13137,21 +13100,18 @@ async function postOrUpdatePreviewComment(args) {
|
|
|
13137
13100
|
signal: AbortSignal.timeout(REQ_TIMEOUT_MS2)
|
|
13138
13101
|
}).catch(() => null);
|
|
13139
13102
|
let existingId = null;
|
|
13140
|
-
if (listRes
|
|
13103
|
+
if (listRes?.ok) {
|
|
13141
13104
|
const comments = await listRes.json().catch(() => []);
|
|
13142
13105
|
const hit = comments.find((c) => (c.body ?? "").includes(MARKER));
|
|
13143
13106
|
if (hit) existingId = hit.id;
|
|
13144
13107
|
}
|
|
13145
13108
|
if (existingId) {
|
|
13146
|
-
await fetch(
|
|
13147
|
-
|
|
13148
|
-
|
|
13149
|
-
|
|
13150
|
-
|
|
13151
|
-
|
|
13152
|
-
signal: AbortSignal.timeout(REQ_TIMEOUT_MS2)
|
|
13153
|
-
}
|
|
13154
|
-
);
|
|
13109
|
+
await fetch(`https://api.github.com/repos/${args.repo}/issues/comments/${existingId}`, {
|
|
13110
|
+
method: "PATCH",
|
|
13111
|
+
headers,
|
|
13112
|
+
body: JSON.stringify({ body: args.body }),
|
|
13113
|
+
signal: AbortSignal.timeout(REQ_TIMEOUT_MS2)
|
|
13114
|
+
});
|
|
13155
13115
|
return;
|
|
13156
13116
|
}
|
|
13157
13117
|
await fetch(base, {
|
|
@@ -13179,9 +13139,7 @@ var runPreviewBuild = async (ctx, _profile, _args) => {
|
|
|
13179
13139
|
masterKey = required("KODY_MASTER_KEY");
|
|
13180
13140
|
ghToken4 = (process.env.KODY_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? process.env.GH_PAT ?? "").trim();
|
|
13181
13141
|
if (!ghToken4) {
|
|
13182
|
-
throw new Error(
|
|
13183
|
-
"GitHub auth token missing (KODY_TOKEN / GH_TOKEN / GITHUB_TOKEN / GH_PAT all empty)"
|
|
13184
|
-
);
|
|
13142
|
+
throw new Error("GitHub auth token missing (KODY_TOKEN / GH_TOKEN / GITHUB_TOKEN / GH_PAT all empty)");
|
|
13185
13143
|
}
|
|
13186
13144
|
} catch (err) {
|
|
13187
13145
|
ctx.output.exitCode = 99;
|
|
@@ -13203,20 +13161,13 @@ var runPreviewBuild = async (ctx, _profile, _args) => {
|
|
|
13203
13161
|
const orgSlug = doc.secrets?.FLY_ORG_SLUG?.value?.trim() || (process.env.FLY_ORG_SLUG ?? "personal").trim();
|
|
13204
13162
|
const region = doc.secrets?.FLY_DEFAULT_REGION?.value?.trim() || (process.env.FLY_REGION ?? "fra").trim();
|
|
13205
13163
|
const nscTenantId = doc.secrets?.NSC_TENANT_ID?.value?.trim() || "";
|
|
13206
|
-
console.log(
|
|
13207
|
-
`[preview-build] vault: ${Object.keys(buildEnv).length} secrets, mode=${buildMode}`
|
|
13208
|
-
);
|
|
13164
|
+
console.log(`[preview-build] vault: ${Object.keys(buildEnv).length} secrets, mode=${buildMode}`);
|
|
13209
13165
|
if (Object.keys(buildEnv).length > 0) {
|
|
13210
|
-
const lines = Object.entries(buildEnv).map(
|
|
13211
|
-
|
|
13212
|
-
|
|
13213
|
-
await writeFile(
|
|
13214
|
-
path36.join(ctx.cwd, ".env.production.local"),
|
|
13215
|
-
lines.join("\n") + "\n",
|
|
13216
|
-
"utf8"
|
|
13217
|
-
);
|
|
13166
|
+
const lines = Object.entries(buildEnv).map(([k, v]) => `${k}=${JSON.stringify(v)}`);
|
|
13167
|
+
await writeFile(path35.join(ctx.cwd, ".env.production.local"), `${lines.join("\n")}
|
|
13168
|
+
`, "utf8");
|
|
13218
13169
|
}
|
|
13219
|
-
const consumerDockerfile =
|
|
13170
|
+
const consumerDockerfile = path35.join(ctx.cwd, "Dockerfile.preview");
|
|
13220
13171
|
const { stat } = await import("fs/promises");
|
|
13221
13172
|
let hasConsumerDockerfile = false;
|
|
13222
13173
|
try {
|
|
@@ -13228,19 +13179,16 @@ var runPreviewBuild = async (ctx, _profile, _args) => {
|
|
|
13228
13179
|
if (!hasConsumerDockerfile) {
|
|
13229
13180
|
const bundled = bundledDockerfilePath(buildMode);
|
|
13230
13181
|
await copyFile(bundled, consumerDockerfile);
|
|
13231
|
-
console.log(
|
|
13232
|
-
`[preview-build] using bundled Dockerfile.preview.${buildMode} (from ${bundled})`
|
|
13233
|
-
);
|
|
13182
|
+
console.log(`[preview-build] using bundled Dockerfile.preview.${buildMode} (from ${bundled})`);
|
|
13234
13183
|
} else {
|
|
13235
13184
|
console.log("[preview-build] using repo Dockerfile.preview");
|
|
13236
13185
|
}
|
|
13237
13186
|
let baseImage = null;
|
|
13238
13187
|
if (ghcrOwner) {
|
|
13239
13188
|
const baseRef = `${ghcrOwner.toLowerCase()}/${basePreviewAppName(repo)}`;
|
|
13240
|
-
const tok = await fetch(
|
|
13241
|
-
|
|
13242
|
-
|
|
13243
|
-
).catch(() => null);
|
|
13189
|
+
const tok = await fetch(`https://ghcr.io/token?scope=repository:${baseRef}:pull&service=ghcr.io`, {
|
|
13190
|
+
signal: AbortSignal.timeout(15e3)
|
|
13191
|
+
}).catch(() => null);
|
|
13244
13192
|
if (tok?.ok) {
|
|
13245
13193
|
const { token: bearer } = await tok.json();
|
|
13246
13194
|
const probe = await fetch(`https://ghcr.io/v2/${baseRef}/manifests/latest`, {
|
|
@@ -13261,39 +13209,22 @@ var runPreviewBuild = async (ctx, _profile, _args) => {
|
|
|
13261
13209
|
await flyCreateApp(appName, orgSlug, flyToken);
|
|
13262
13210
|
}
|
|
13263
13211
|
await flyAllocateSharedIps(appName, flyToken);
|
|
13264
|
-
await runCmd(
|
|
13265
|
-
|
|
13266
|
-
|
|
13267
|
-
|
|
13268
|
-
);
|
|
13212
|
+
await runCmd("docker", ["login", "registry.fly.io", "-u", "x", "--password-stdin"], {
|
|
13213
|
+
input: flyToken,
|
|
13214
|
+
cwd: ctx.cwd
|
|
13215
|
+
});
|
|
13269
13216
|
const imageRef = `registry.fly.io/${appName}:${tag}`;
|
|
13270
13217
|
const nsBuilder = nscTenantId ? await setupNamespaceBuilder({
|
|
13271
13218
|
tenantId: nscTenantId,
|
|
13272
13219
|
builderName: `kody-preview-${pr}`
|
|
13273
13220
|
}) : null;
|
|
13274
13221
|
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
|
-
];
|
|
13222
|
+
const a = ["buildx", "build", "--builder", nsBuilder, "-f", "Dockerfile.preview", "-t", imageRef, "--push"];
|
|
13286
13223
|
if (baseImage) a.push("--build-arg", `BASE_IMAGE=${baseImage}`);
|
|
13287
13224
|
a.push(".");
|
|
13288
13225
|
await runCmd("docker", a, { cwd: ctx.cwd });
|
|
13289
13226
|
} else {
|
|
13290
|
-
const buildArgs = [
|
|
13291
|
-
"build",
|
|
13292
|
-
"-f",
|
|
13293
|
-
"Dockerfile.preview",
|
|
13294
|
-
"-t",
|
|
13295
|
-
imageRef
|
|
13296
|
-
];
|
|
13227
|
+
const buildArgs = ["build", "-f", "Dockerfile.preview", "-t", imageRef];
|
|
13297
13228
|
if (baseImage) buildArgs.push("--build-arg", `BASE_IMAGE=${baseImage}`);
|
|
13298
13229
|
buildArgs.push(".");
|
|
13299
13230
|
await runCmd("docker", buildArgs, {
|
|
@@ -13315,9 +13246,7 @@ var runPreviewBuild = async (ctx, _profile, _args) => {
|
|
|
13315
13246
|
},
|
|
13316
13247
|
flyToken
|
|
13317
13248
|
);
|
|
13318
|
-
console.log(
|
|
13319
|
-
`[preview-build] done \u2014 machine ${machineId} at https://${appName}.fly.dev`
|
|
13320
|
-
);
|
|
13249
|
+
console.log(`[preview-build] done \u2014 machine ${machineId} at https://${appName}.fly.dev`);
|
|
13321
13250
|
await postOrUpdatePreviewComment({
|
|
13322
13251
|
repo,
|
|
13323
13252
|
pr,
|
|
@@ -13338,8 +13267,8 @@ var runPreviewBuild = async (ctx, _profile, _args) => {
|
|
|
13338
13267
|
|
|
13339
13268
|
// src/scripts/runTickScript.ts
|
|
13340
13269
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
13341
|
-
import * as
|
|
13342
|
-
import * as
|
|
13270
|
+
import * as fs39 from "fs";
|
|
13271
|
+
import * as path36 from "path";
|
|
13343
13272
|
var runTickScript = async (ctx, _profile, args) => {
|
|
13344
13273
|
ctx.skipAgent = true;
|
|
13345
13274
|
const jobsDir = String(args?.jobsDir ?? ".kody/duties");
|
|
@@ -13351,22 +13280,22 @@ var runTickScript = async (ctx, _profile, args) => {
|
|
|
13351
13280
|
ctx.output.reason = `runTickScript: ctx.args.${slugArg} must be a non-empty slug`;
|
|
13352
13281
|
return;
|
|
13353
13282
|
}
|
|
13354
|
-
const jobPath =
|
|
13355
|
-
if (!
|
|
13283
|
+
const jobPath = path36.join(ctx.cwd, jobsDir, `${slug}.md`);
|
|
13284
|
+
if (!fs39.existsSync(jobPath)) {
|
|
13356
13285
|
ctx.output.exitCode = 99;
|
|
13357
13286
|
ctx.output.reason = `runTickScript: job file not found: ${jobPath}`;
|
|
13358
13287
|
return;
|
|
13359
13288
|
}
|
|
13360
|
-
const raw =
|
|
13361
|
-
const { frontmatter } =
|
|
13289
|
+
const raw = fs39.readFileSync(jobPath, "utf-8");
|
|
13290
|
+
const { frontmatter } = splitFrontmatter(raw);
|
|
13362
13291
|
const tickScript = frontmatter.tickScript;
|
|
13363
13292
|
if (!tickScript) {
|
|
13364
13293
|
ctx.output.exitCode = 99;
|
|
13365
13294
|
ctx.output.reason = `runTickScript: job ${slug} has no \`tickScript:\` frontmatter \u2014 route via job-tick instead`;
|
|
13366
13295
|
return;
|
|
13367
13296
|
}
|
|
13368
|
-
const scriptPath =
|
|
13369
|
-
if (!
|
|
13297
|
+
const scriptPath = path36.isAbsolute(tickScript) ? tickScript : path36.join(ctx.cwd, tickScript);
|
|
13298
|
+
if (!fs39.existsSync(scriptPath)) {
|
|
13370
13299
|
ctx.output.exitCode = 99;
|
|
13371
13300
|
ctx.output.reason = `runTickScript: tickScript not found: ${scriptPath}`;
|
|
13372
13301
|
return;
|
|
@@ -13543,8 +13472,10 @@ var serveFlow = async (ctx) => {
|
|
|
13543
13472
|
process.stdout.write(`[kody serve] LiteLLM ready at ${handle?.url ?? LITELLM_DEFAULT_URL}
|
|
13544
13473
|
`);
|
|
13545
13474
|
} else {
|
|
13546
|
-
process.stdout.write(
|
|
13547
|
-
`
|
|
13475
|
+
process.stdout.write(
|
|
13476
|
+
`[kody serve] model ${model.provider}/${model.model} routes to Anthropic directly \u2014 no proxy needed
|
|
13477
|
+
`
|
|
13478
|
+
);
|
|
13548
13479
|
}
|
|
13549
13480
|
const url = handle?.url ?? LITELLM_DEFAULT_URL;
|
|
13550
13481
|
const editorEnv = usesProxy ? buildProxyEnv(url) : { ...process.env };
|
|
@@ -13589,8 +13520,10 @@ var serveFlow = async (ctx) => {
|
|
|
13589
13520
|
code.on("error", (err) => {
|
|
13590
13521
|
process.stderr.write(`[kody serve] failed to launch VS Code: ${err.message}
|
|
13591
13522
|
`);
|
|
13592
|
-
process.stderr.write(
|
|
13593
|
-
`
|
|
13523
|
+
process.stderr.write(
|
|
13524
|
+
` Install the 'code' CLI: VS Code \u2192 Command Palette \u2192 "Shell Command: Install 'code' command in PATH"
|
|
13525
|
+
`
|
|
13526
|
+
);
|
|
13594
13527
|
});
|
|
13595
13528
|
code.unref();
|
|
13596
13529
|
} catch (err) {
|
|
@@ -13817,10 +13750,8 @@ var verify = async (ctx) => {
|
|
|
13817
13750
|
ctx.data.verifyReason = result.ok ? "" : summarizeFailure(result);
|
|
13818
13751
|
ctx.data.verifyRecovered = result.recovered ?? [];
|
|
13819
13752
|
if (result.recovered && result.recovered.length > 0) {
|
|
13820
|
-
process.stderr.write(
|
|
13821
|
-
|
|
13822
|
-
`
|
|
13823
|
-
);
|
|
13753
|
+
process.stderr.write(`[kody verify] caught flake on: ${result.recovered.join(", ")} (passed on retry)
|
|
13754
|
+
`);
|
|
13824
13755
|
}
|
|
13825
13756
|
} catch (err) {
|
|
13826
13757
|
ctx.data.verifyOk = false;
|
|
@@ -13828,7 +13759,7 @@ var verify = async (ctx) => {
|
|
|
13828
13759
|
}
|
|
13829
13760
|
if (ctx.data.verifyOk === false) {
|
|
13830
13761
|
const action = ctx.data.action;
|
|
13831
|
-
if (action
|
|
13762
|
+
if (action?.type.endsWith("_COMPLETED")) {
|
|
13832
13763
|
const reason = ctx.data.verifyReason || "verify failed";
|
|
13833
13764
|
ctx.data.action = {
|
|
13834
13765
|
type: action.type.replace(/_COMPLETED$/, "_FAILED"),
|
|
@@ -13945,7 +13876,7 @@ function runCommand2(command, cwd) {
|
|
|
13945
13876
|
}
|
|
13946
13877
|
function downgrade2(ctx, reason) {
|
|
13947
13878
|
const action = ctx.data.action;
|
|
13948
|
-
if (action
|
|
13879
|
+
if (action?.type.endsWith("_COMPLETED")) {
|
|
13949
13880
|
ctx.data.action = {
|
|
13950
13881
|
type: action.type.replace(/_COMPLETED$/, "_FAILED"),
|
|
13951
13882
|
payload: { reason, downgradedFrom: action.type },
|
|
@@ -13965,10 +13896,8 @@ async function runVerify(ctx) {
|
|
|
13965
13896
|
ctx.data.verifyReason = result.ok ? "" : summarizeFailure(result);
|
|
13966
13897
|
ctx.data.verifyRecovered = result.recovered ?? [];
|
|
13967
13898
|
if (result.recovered && result.recovered.length > 0) {
|
|
13968
|
-
process.stderr.write(
|
|
13969
|
-
|
|
13970
|
-
`
|
|
13971
|
-
);
|
|
13899
|
+
process.stderr.write(`[kody verify] caught flake on: ${result.recovered.join(", ")} (passed on retry)
|
|
13900
|
+
`);
|
|
13972
13901
|
}
|
|
13973
13902
|
} catch (err) {
|
|
13974
13903
|
ctx.data.verifyOk = false;
|
|
@@ -13978,7 +13907,7 @@ async function runVerify(ctx) {
|
|
|
13978
13907
|
function downgradeActionOnFailure(ctx) {
|
|
13979
13908
|
if (ctx.data.verifyOk !== false) return;
|
|
13980
13909
|
const action = ctx.data.action;
|
|
13981
|
-
if (!action
|
|
13910
|
+
if (!action?.type.endsWith("_COMPLETED")) return;
|
|
13982
13911
|
const reason = ctx.data.verifyReason || "verify failed";
|
|
13983
13912
|
ctx.data.action = {
|
|
13984
13913
|
type: action.type.replace(/_COMPLETED$/, "_FAILED"),
|
|
@@ -13989,9 +13918,9 @@ function downgradeActionOnFailure(ctx) {
|
|
|
13989
13918
|
function upgradeActionOnPass(ctx) {
|
|
13990
13919
|
if (ctx.data.verifyOk !== true) return;
|
|
13991
13920
|
const action = ctx.data.action;
|
|
13992
|
-
if (!action
|
|
13921
|
+
if (!action?.type.endsWith("_FAILED")) return;
|
|
13993
13922
|
const downgradedFrom = action.payload?.downgradedFrom;
|
|
13994
|
-
if (!downgradedFrom
|
|
13923
|
+
if (!downgradedFrom?.endsWith("_COMPLETED")) return;
|
|
13995
13924
|
ctx.data.action = {
|
|
13996
13925
|
type: downgradedFrom,
|
|
13997
13926
|
payload: {},
|
|
@@ -14171,74 +14100,6 @@ function sleep3(ms) {
|
|
|
14171
14100
|
return new Promise((res) => setTimeout(res, ms));
|
|
14172
14101
|
}
|
|
14173
14102
|
|
|
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
14103
|
// src/scripts/warmupMcp.ts
|
|
14243
14104
|
import { spawn as spawn9 } from "child_process";
|
|
14244
14105
|
var PER_SERVER_TIMEOUT_MS = 6e4;
|
|
@@ -14276,12 +14137,14 @@ async function warmupOne(command, args, env) {
|
|
|
14276
14137
|
let nextId = 1;
|
|
14277
14138
|
const send = (method, params) => {
|
|
14278
14139
|
const id = nextId++;
|
|
14279
|
-
const payload = JSON.stringify({ jsonrpc: "2.0", id, method, params })
|
|
14140
|
+
const payload = `${JSON.stringify({ jsonrpc: "2.0", id, method, params })}
|
|
14141
|
+
`;
|
|
14280
14142
|
child.stdin.write(payload);
|
|
14281
14143
|
return id;
|
|
14282
14144
|
};
|
|
14283
14145
|
const notify = (method, params) => {
|
|
14284
|
-
const payload = JSON.stringify({ jsonrpc: "2.0", method, params })
|
|
14146
|
+
const payload = `${JSON.stringify({ jsonrpc: "2.0", method, params })}
|
|
14147
|
+
`;
|
|
14285
14148
|
child.stdin.write(payload);
|
|
14286
14149
|
};
|
|
14287
14150
|
const awaitResponse = async (id) => {
|
|
@@ -14346,11 +14209,12 @@ function lineStream(stream) {
|
|
|
14346
14209
|
};
|
|
14347
14210
|
stream.on("data", (chunk) => {
|
|
14348
14211
|
buf += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
14349
|
-
let idx;
|
|
14350
|
-
while (
|
|
14212
|
+
let idx = buf.indexOf("\n");
|
|
14213
|
+
while (idx >= 0) {
|
|
14351
14214
|
const line = buf.slice(0, idx).replace(/\r$/, "");
|
|
14352
14215
|
buf = buf.slice(idx + 1);
|
|
14353
14216
|
if (line.length > 0) queue.push(line);
|
|
14217
|
+
idx = buf.indexOf("\n");
|
|
14354
14218
|
}
|
|
14355
14219
|
tryDeliver();
|
|
14356
14220
|
});
|
|
@@ -14373,12 +14237,15 @@ function lineStream(stream) {
|
|
|
14373
14237
|
return;
|
|
14374
14238
|
}
|
|
14375
14239
|
waiter = resolve6;
|
|
14376
|
-
const t = setTimeout(
|
|
14377
|
-
|
|
14378
|
-
waiter
|
|
14379
|
-
|
|
14380
|
-
|
|
14381
|
-
|
|
14240
|
+
const t = setTimeout(
|
|
14241
|
+
() => {
|
|
14242
|
+
if (waiter === resolve6) {
|
|
14243
|
+
waiter = null;
|
|
14244
|
+
resolve6(null);
|
|
14245
|
+
}
|
|
14246
|
+
},
|
|
14247
|
+
Math.max(0, timeoutMs)
|
|
14248
|
+
);
|
|
14382
14249
|
t.unref?.();
|
|
14383
14250
|
})
|
|
14384
14251
|
};
|
|
@@ -14474,7 +14341,7 @@ var writeJobStateFile = async (ctx, _profile, agentResult, args) => {
|
|
|
14474
14341
|
};
|
|
14475
14342
|
|
|
14476
14343
|
// src/scripts/writeRunSummary.ts
|
|
14477
|
-
import * as
|
|
14344
|
+
import * as fs40 from "fs";
|
|
14478
14345
|
var writeRunSummary = async (ctx, profile) => {
|
|
14479
14346
|
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
14480
14347
|
if (!summaryPath) return;
|
|
@@ -14496,7 +14363,7 @@ var writeRunSummary = async (ctx, profile) => {
|
|
|
14496
14363
|
if (reason) lines.push(`- **Reason:** ${reason}`);
|
|
14497
14364
|
lines.push("");
|
|
14498
14365
|
try {
|
|
14499
|
-
|
|
14366
|
+
fs40.appendFileSync(summaryPath, `${lines.join("\n")}
|
|
14500
14367
|
`);
|
|
14501
14368
|
} catch {
|
|
14502
14369
|
}
|
|
@@ -14603,6 +14470,48 @@ var allScriptNames = /* @__PURE__ */ new Set([
|
|
|
14603
14470
|
...Object.keys(postflightScripts)
|
|
14604
14471
|
]);
|
|
14605
14472
|
|
|
14473
|
+
// src/subagents.ts
|
|
14474
|
+
import * as fs41 from "fs";
|
|
14475
|
+
import * as path37 from "path";
|
|
14476
|
+
function splitFrontmatter2(raw) {
|
|
14477
|
+
const match = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(raw);
|
|
14478
|
+
if (!match) return { fm: {}, body: raw.trim() };
|
|
14479
|
+
const fm = {};
|
|
14480
|
+
for (const line of match[1].split("\n")) {
|
|
14481
|
+
const idx = line.indexOf(":");
|
|
14482
|
+
if (idx === -1) continue;
|
|
14483
|
+
fm[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
14484
|
+
}
|
|
14485
|
+
return { fm, body: (match[2] ?? "").trim() };
|
|
14486
|
+
}
|
|
14487
|
+
function resolveAgentFile(profileDir, name) {
|
|
14488
|
+
const local = path37.join(profileDir, "agents", `${name}.md`);
|
|
14489
|
+
if (fs41.existsSync(local)) return local;
|
|
14490
|
+
const central = path37.join(getPluginsCatalogRoot(), "agents", `${name}.md`);
|
|
14491
|
+
if (fs41.existsSync(central)) return central;
|
|
14492
|
+
throw new Error(`loadSubagents: agent '${name}' not found in ${profileDir}/agents/ or shared catalog`);
|
|
14493
|
+
}
|
|
14494
|
+
function loadSubagents(profile) {
|
|
14495
|
+
const names = profile.claudeCode.subagents;
|
|
14496
|
+
if (!names || names.length === 0) return void 0;
|
|
14497
|
+
const agents = {};
|
|
14498
|
+
for (const name of names) {
|
|
14499
|
+
const { fm, body } = splitFrontmatter2(fs41.readFileSync(resolveAgentFile(profile.dir, name), "utf-8"));
|
|
14500
|
+
if (!body) throw new Error(`loadSubagents: agent '${name}' has an empty prompt body`);
|
|
14501
|
+
const def = {
|
|
14502
|
+
description: fm.description ?? `Subagent ${name}`,
|
|
14503
|
+
prompt: body
|
|
14504
|
+
};
|
|
14505
|
+
if (fm.tools) {
|
|
14506
|
+
const tools = fm.tools.split(",").map((t) => t.trim()).filter(Boolean);
|
|
14507
|
+
if (tools.length > 0) def.tools = tools;
|
|
14508
|
+
}
|
|
14509
|
+
if (fm.model) def.model = fm.model;
|
|
14510
|
+
agents[fm.name || name] = def;
|
|
14511
|
+
}
|
|
14512
|
+
return agents;
|
|
14513
|
+
}
|
|
14514
|
+
|
|
14606
14515
|
// src/tools.ts
|
|
14607
14516
|
import { execFileSync as execFileSync27 } from "child_process";
|
|
14608
14517
|
function verifyCliTools(tools, cwd) {
|
|
@@ -15036,7 +14945,7 @@ function clearStampedLifecycleLabels(profile, ctx) {
|
|
|
15036
14945
|
for (const entry of profile.scripts.preflight) {
|
|
15037
14946
|
if (entry.script !== "setLifecycleLabel") continue;
|
|
15038
14947
|
const label = typeof entry.with?.label === "string" ? entry.with.label : void 0;
|
|
15039
|
-
if (!label
|
|
14948
|
+
if (!label?.startsWith(KODY_NAMESPACE)) continue;
|
|
15040
14949
|
try {
|
|
15041
14950
|
removeLabel(target, label, ctx.cwd);
|
|
15042
14951
|
} catch {
|
|
@@ -15600,10 +15509,8 @@ ${CI_HELP}`);
|
|
|
15600
15509
|
`);
|
|
15601
15510
|
const buildOnly = dispatch2.executable === "preview-build";
|
|
15602
15511
|
if (args.skipInstall || buildOnly) {
|
|
15603
|
-
process.stdout.write(
|
|
15604
|
-
|
|
15605
|
-
`
|
|
15606
|
-
);
|
|
15512
|
+
process.stdout.write(`\u2192 kody: skipping dep install (${buildOnly ? "build-only executable" : "--skip-install"})
|
|
15513
|
+
`);
|
|
15607
15514
|
} else {
|
|
15608
15515
|
const code = installDeps(pm, cwd);
|
|
15609
15516
|
if (code !== 0) {
|
|
@@ -16093,15 +16000,19 @@ Kody run statistics \u2014 ${totalRuns} runs
|
|
|
16093
16000
|
const totalIn = runs.reduce((s, r) => s + r.totalInputTokens, 0);
|
|
16094
16001
|
const totalOut = runs.reduce((s, r) => s + r.totalOutputTokens, 0);
|
|
16095
16002
|
const totalCacheR = runs.reduce((s, r) => s + r.totalCacheReadTokens, 0);
|
|
16096
|
-
process.stdout.write(
|
|
16097
|
-
`)
|
|
16003
|
+
process.stdout.write(
|
|
16004
|
+
` total tokens : ${totalIn.toLocaleString()} in / ${totalOut.toLocaleString()} out / ${totalCacheR.toLocaleString()} cache-read
|
|
16005
|
+
`
|
|
16006
|
+
);
|
|
16098
16007
|
process.stdout.write(`
|
|
16099
16008
|
Per-executable (stage_end events)
|
|
16100
16009
|
`);
|
|
16101
16010
|
const headers = ["executable", "runs", "ok", "failed", "p50", "p95", "mean", "tok-in", "tok-out", "cache-r"];
|
|
16102
16011
|
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("")
|
|
16104
|
-
|
|
16012
|
+
process.stdout.write(`${headers.map((h, i) => h.padEnd(widths[i])).join("")}
|
|
16013
|
+
`);
|
|
16014
|
+
process.stdout.write(`${widths.map((w) => `${"-".repeat(w - 1)} `).join("")}
|
|
16015
|
+
`);
|
|
16105
16016
|
for (const r of rollups) {
|
|
16106
16017
|
const row = [
|
|
16107
16018
|
r.executable,
|
|
@@ -16115,7 +16026,8 @@ Per-executable (stage_end events)
|
|
|
16115
16026
|
r.totalOutputTokens.toLocaleString(),
|
|
16116
16027
|
r.totalCacheReadTokens.toLocaleString()
|
|
16117
16028
|
];
|
|
16118
|
-
process.stdout.write(row.map((c, i) => c.padEnd(widths[i])).join("")
|
|
16029
|
+
process.stdout.write(`${row.map((c, i) => c.padEnd(widths[i])).join("")}
|
|
16030
|
+
`);
|
|
16119
16031
|
}
|
|
16120
16032
|
process.stdout.write("\n");
|
|
16121
16033
|
}
|