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