@kody-ade/kody-engine 0.4.173 → 0.4.176
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/README.md +73 -2
- package/dist/bin/kody.js +914 -186
- package/dist/executables/job-tick/prompts/locked.md +48 -0
- package/dist/executables/preview-build/profile.json +61 -0
- package/dist/scripts/preview-build-templates/default-Dockerfile.preview.dev +43 -0
- package/dist/scripts/preview-build-templates/default-Dockerfile.preview.prod +40 -0
- package/package.json +1 -1
package/dist/bin/kody.js
CHANGED
|
@@ -358,141 +358,6 @@ var init_submitMcp = __esm({
|
|
|
358
358
|
}
|
|
359
359
|
});
|
|
360
360
|
|
|
361
|
-
// src/repoWorkspace.ts
|
|
362
|
-
import { spawn as spawn2, spawnSync } from "child_process";
|
|
363
|
-
import * as fs6 from "fs";
|
|
364
|
-
import * as path6 from "path";
|
|
365
|
-
async function resolveAndClone(reposRoot, repo, repoToken, cloneRepo) {
|
|
366
|
-
const name = repo?.trim();
|
|
367
|
-
if (!name || !REPO_RE.test(name)) return null;
|
|
368
|
-
const root = path6.resolve(reposRoot);
|
|
369
|
-
const dir = path6.resolve(root, name);
|
|
370
|
-
if (dir !== root && !dir.startsWith(root + path6.sep)) return null;
|
|
371
|
-
if (fs6.existsSync(path6.join(dir, ".git"))) return dir;
|
|
372
|
-
const inflight = repoClones.get(dir);
|
|
373
|
-
if (inflight) {
|
|
374
|
-
await inflight;
|
|
375
|
-
return dir;
|
|
376
|
-
}
|
|
377
|
-
const p = cloneRepo(name, repoToken, dir).finally(() => {
|
|
378
|
-
if (repoClones.get(dir) === p) repoClones.delete(dir);
|
|
379
|
-
});
|
|
380
|
-
repoClones.set(dir, p);
|
|
381
|
-
await p;
|
|
382
|
-
return dir;
|
|
383
|
-
}
|
|
384
|
-
async function ensureRepoCwd(opts) {
|
|
385
|
-
const dir = await resolveAndClone(
|
|
386
|
-
opts.reposRoot,
|
|
387
|
-
opts.repo,
|
|
388
|
-
opts.repoToken,
|
|
389
|
-
opts.cloneRepo
|
|
390
|
-
);
|
|
391
|
-
return dir ?? opts.baseCwd;
|
|
392
|
-
}
|
|
393
|
-
async function fetchRepo(opts) {
|
|
394
|
-
const dir = await resolveAndClone(
|
|
395
|
-
opts.reposRoot,
|
|
396
|
-
opts.repo,
|
|
397
|
-
opts.repoToken,
|
|
398
|
-
opts.cloneRepo ?? defaultCloneRepo
|
|
399
|
-
);
|
|
400
|
-
if (!dir) {
|
|
401
|
-
throw new Error(
|
|
402
|
-
`invalid repo "${opts.repo}" \u2014 expected "owner/name" with no path escapes`
|
|
403
|
-
);
|
|
404
|
-
}
|
|
405
|
-
return dir;
|
|
406
|
-
}
|
|
407
|
-
var REPO_RE, repoClones, defaultCloneRepo;
|
|
408
|
-
var init_repoWorkspace = __esm({
|
|
409
|
-
"src/repoWorkspace.ts"() {
|
|
410
|
-
"use strict";
|
|
411
|
-
REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
412
|
-
repoClones = /* @__PURE__ */ new Map();
|
|
413
|
-
defaultCloneRepo = (repo, token, dir) => {
|
|
414
|
-
fs6.mkdirSync(path6.dirname(dir), { recursive: true });
|
|
415
|
-
const authUrl = token ? `https://x-access-token:${token}@github.com/${repo}.git` : `https://github.com/${repo}.git`;
|
|
416
|
-
return new Promise((resolve6, reject) => {
|
|
417
|
-
const child = spawn2("git", ["clone", "--depth=1", authUrl, dir], {
|
|
418
|
-
stdio: "inherit"
|
|
419
|
-
});
|
|
420
|
-
child.on("exit", (code) => {
|
|
421
|
-
if (code !== 0) {
|
|
422
|
-
reject(new Error(`git clone ${repo} failed (exit ${code})`));
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
try {
|
|
426
|
-
const name = process.env.GIT_AUTHOR_NAME ?? "Kody Bot";
|
|
427
|
-
const email = process.env.GIT_AUTHOR_EMAIL ?? "kody-bot@users.noreply.github.com";
|
|
428
|
-
spawnSync("git", ["-C", dir, "config", "user.name", name]);
|
|
429
|
-
spawnSync("git", ["-C", dir, "config", "user.email", email]);
|
|
430
|
-
} catch {
|
|
431
|
-
}
|
|
432
|
-
resolve6();
|
|
433
|
-
});
|
|
434
|
-
child.on("error", reject);
|
|
435
|
-
});
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
// src/fetchRepoMcp.ts
|
|
441
|
-
var fetchRepoMcp_exports = {};
|
|
442
|
-
__export(fetchRepoMcp_exports, {
|
|
443
|
-
buildFetchRepoMcpServer: () => buildFetchRepoMcpServer
|
|
444
|
-
});
|
|
445
|
-
import {
|
|
446
|
-
createSdkMcpServer as createSdkMcpServer3,
|
|
447
|
-
tool as tool3
|
|
448
|
-
} from "@anthropic-ai/claude-agent-sdk";
|
|
449
|
-
import { z as z3 } from "zod";
|
|
450
|
-
function buildFetchRepoMcpServer(opts) {
|
|
451
|
-
const fetchTool = tool3(
|
|
452
|
-
"fetch_repo",
|
|
453
|
-
'Clone another GitHub repository into your workspace so you can read and work on it. Pass `repo` as "owner/name" (e.g. "A-Guy-educ/A-Guy"). Returns the absolute path of the clone \u2014 then use your Read/Grep/Glob/Bash tools at that path to inspect it. Already-fetched repos are reused instantly. Use this whenever the user asks about a repository other than your current one \u2014 you are NOT limited to a single repo.',
|
|
454
|
-
{
|
|
455
|
-
repo: z3.string().describe('GitHub repository as "owner/name", e.g. "A-Guy-educ/A-Guy".')
|
|
456
|
-
},
|
|
457
|
-
async (args) => {
|
|
458
|
-
const repo = String(args.repo ?? "").trim();
|
|
459
|
-
try {
|
|
460
|
-
const dir = await fetchRepo({
|
|
461
|
-
reposRoot: opts.reposRoot,
|
|
462
|
-
repo,
|
|
463
|
-
repoToken: opts.repoToken
|
|
464
|
-
});
|
|
465
|
-
return {
|
|
466
|
-
content: [
|
|
467
|
-
{
|
|
468
|
-
type: "text",
|
|
469
|
-
text: `Cloned ${repo} \u2192 ${dir}
|
|
470
|
-
Use Read/Grep/Glob/Bash at that absolute path to explore it. It now lives in your workspace alongside any other repos you've fetched.`
|
|
471
|
-
}
|
|
472
|
-
]
|
|
473
|
-
};
|
|
474
|
-
} catch (err) {
|
|
475
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
476
|
-
return {
|
|
477
|
-
content: [{ type: "text", text: `Could not fetch ${repo}: ${msg}` }],
|
|
478
|
-
isError: true
|
|
479
|
-
};
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
);
|
|
483
|
-
return createSdkMcpServer3({
|
|
484
|
-
name: "kody-fetch-repo",
|
|
485
|
-
version: "0.1.0",
|
|
486
|
-
tools: [fetchTool]
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
|
-
var init_fetchRepoMcp = __esm({
|
|
490
|
-
"src/fetchRepoMcp.ts"() {
|
|
491
|
-
"use strict";
|
|
492
|
-
init_repoWorkspace();
|
|
493
|
-
}
|
|
494
|
-
});
|
|
495
|
-
|
|
496
361
|
// src/issue.ts
|
|
497
362
|
import { execFileSync } from "child_process";
|
|
498
363
|
function ghToken() {
|
|
@@ -676,6 +541,362 @@ var init_issue = __esm({
|
|
|
676
541
|
}
|
|
677
542
|
});
|
|
678
543
|
|
|
544
|
+
// src/dutyMcp.ts
|
|
545
|
+
var dutyMcp_exports = {};
|
|
546
|
+
__export(dutyMcp_exports, {
|
|
547
|
+
DUTY_MCP_TOOL_NAMES: () => DUTY_MCP_TOOL_NAMES,
|
|
548
|
+
buildDutyMcpServer: () => buildDutyMcpServer
|
|
549
|
+
});
|
|
550
|
+
import { createSdkMcpServer as createSdkMcpServer3, tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
|
|
551
|
+
import { z as z3 } from "zod";
|
|
552
|
+
function summarizeCiStatus(rollup) {
|
|
553
|
+
if (!Array.isArray(rollup) || rollup.length === 0) return "UNKNOWN";
|
|
554
|
+
let hasRunning = false;
|
|
555
|
+
for (const check of rollup) {
|
|
556
|
+
const status = String(check.status ?? "").toUpperCase();
|
|
557
|
+
const conclusion = String(check.conclusion ?? "").toUpperCase();
|
|
558
|
+
if (FAIL_CONCLUSIONS.has(conclusion)) return "FAILING";
|
|
559
|
+
if (!conclusion && RUNNING_STATUSES.has(status)) hasRunning = true;
|
|
560
|
+
}
|
|
561
|
+
return hasRunning ? "RUNNING" : "PASSING";
|
|
562
|
+
}
|
|
563
|
+
function computeBehindBy(repoSlug, base, head) {
|
|
564
|
+
try {
|
|
565
|
+
const raw = gh(["api", `repos/${repoSlug}/compare/${base}...${head}`, "--jq", ".behind_by"]);
|
|
566
|
+
const n = Number(raw.trim());
|
|
567
|
+
return Number.isFinite(n) ? n : -1;
|
|
568
|
+
} catch {
|
|
569
|
+
return -1;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
function listRepairCandidates(repoSlug) {
|
|
573
|
+
const raw = gh([
|
|
574
|
+
"pr",
|
|
575
|
+
"list",
|
|
576
|
+
"--state",
|
|
577
|
+
"open",
|
|
578
|
+
"--limit",
|
|
579
|
+
"100",
|
|
580
|
+
"--json",
|
|
581
|
+
"number,title,headRefName,headRefOid,baseRefName,isDraft,mergeable,statusCheckRollup,updatedAt"
|
|
582
|
+
]);
|
|
583
|
+
const prs = JSON.parse(raw);
|
|
584
|
+
return prs.filter((p) => !p.isDraft).map((p) => {
|
|
585
|
+
const ciStatus = summarizeCiStatus(p.statusCheckRollup);
|
|
586
|
+
const mergeable = String(p.mergeable ?? "UNKNOWN").toUpperCase();
|
|
587
|
+
const behindBy = mergeable === "CONFLICTING" || ciStatus === "FAILING" ? 0 : computeBehindBy(repoSlug, p.baseRefName, p.headRefName);
|
|
588
|
+
return {
|
|
589
|
+
number: p.number,
|
|
590
|
+
title: p.title,
|
|
591
|
+
headSha: p.headRefOid,
|
|
592
|
+
baseRef: p.baseRefName,
|
|
593
|
+
isDraft: false,
|
|
594
|
+
mergeable,
|
|
595
|
+
ciStatus,
|
|
596
|
+
behindBy,
|
|
597
|
+
updatedAt: p.updatedAt
|
|
598
|
+
};
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
function dispatchVerb(workflowFile, executable, prNumber) {
|
|
602
|
+
try {
|
|
603
|
+
gh([
|
|
604
|
+
"workflow",
|
|
605
|
+
"run",
|
|
606
|
+
workflowFile,
|
|
607
|
+
"-f",
|
|
608
|
+
`executable=${executable}`,
|
|
609
|
+
"-f",
|
|
610
|
+
`issue_number=${prNumber}`
|
|
611
|
+
]);
|
|
612
|
+
return { ok: true };
|
|
613
|
+
} catch (err) {
|
|
614
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function postRecommendation(prNumber, mention, message) {
|
|
618
|
+
const body = mention ? `${mention} ${message}` : message;
|
|
619
|
+
try {
|
|
620
|
+
gh(["pr", "comment", String(prNumber), "--body", body]);
|
|
621
|
+
return { ok: true };
|
|
622
|
+
} catch (err) {
|
|
623
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
function readLedger(label) {
|
|
627
|
+
const startTag = `<!-- ${label}:start -->`;
|
|
628
|
+
const endTag = `<!-- ${label}:end -->`;
|
|
629
|
+
try {
|
|
630
|
+
const raw = gh([
|
|
631
|
+
"issue",
|
|
632
|
+
"list",
|
|
633
|
+
"--state",
|
|
634
|
+
"open",
|
|
635
|
+
"--label",
|
|
636
|
+
label,
|
|
637
|
+
"--limit",
|
|
638
|
+
"5",
|
|
639
|
+
"--json",
|
|
640
|
+
"number,body"
|
|
641
|
+
]);
|
|
642
|
+
const issues = JSON.parse(raw);
|
|
643
|
+
if (issues.length === 0) return { found: false, payload: null };
|
|
644
|
+
const issue = issues.sort((a, b) => a.number - b.number)[0];
|
|
645
|
+
const body = issue?.body ?? "";
|
|
646
|
+
const startIdx = body.indexOf(startTag);
|
|
647
|
+
const endIdx = body.indexOf(endTag);
|
|
648
|
+
if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) {
|
|
649
|
+
return { found: true, issueNumber: issue?.number, payload: null };
|
|
650
|
+
}
|
|
651
|
+
const between = body.slice(startIdx + startTag.length, endIdx);
|
|
652
|
+
const fenceMatch = between.match(/```json\s*([\s\S]*?)```/);
|
|
653
|
+
if (!fenceMatch) return { found: true, issueNumber: issue?.number, payload: null };
|
|
654
|
+
try {
|
|
655
|
+
return { found: true, issueNumber: issue?.number, payload: JSON.parse(fenceMatch[1]) };
|
|
656
|
+
} catch {
|
|
657
|
+
return { found: true, issueNumber: issue?.number, payload: null };
|
|
658
|
+
}
|
|
659
|
+
} catch (err) {
|
|
660
|
+
return { found: false, payload: { error: err instanceof Error ? err.message : String(err) } };
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
function buildDutyMcpServer(opts) {
|
|
664
|
+
const workflowFile = opts.workflowFile ?? "kody.yml";
|
|
665
|
+
const listTool = tool3(
|
|
666
|
+
"list_prs_to_repair",
|
|
667
|
+
"Return open non-draft PRs with the signals you need to pick a repair: number, title, headSha, baseRef, mergeable (CONFLICTING/MERGEABLE/UNKNOWN), ciStatus (PASSING/FAILING/RUNNING/UNKNOWN), behindBy (commits behind base; 0 for PRs that already match conflicts or CI-failure rules), updatedAt. Drafts are excluded. One call returns everything \u2014 do not iterate or paginate.",
|
|
668
|
+
{},
|
|
669
|
+
async () => {
|
|
670
|
+
const candidates = listRepairCandidates(opts.repoSlug);
|
|
671
|
+
return {
|
|
672
|
+
content: [
|
|
673
|
+
{
|
|
674
|
+
type: "text",
|
|
675
|
+
text: JSON.stringify({ prs: candidates }, null, 2)
|
|
676
|
+
}
|
|
677
|
+
]
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
);
|
|
681
|
+
const makeDispatch = (verb, describe) => tool3(
|
|
682
|
+
`${verb.replace("-", "_")}_pr`,
|
|
683
|
+
describe,
|
|
684
|
+
{
|
|
685
|
+
pr: z3.number().int().positive().describe("PR number to repair.")
|
|
686
|
+
},
|
|
687
|
+
async (args) => {
|
|
688
|
+
const result = dispatchVerb(workflowFile, verb, args.pr);
|
|
689
|
+
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}`;
|
|
690
|
+
return {
|
|
691
|
+
content: [{ type: "text", text }]
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
);
|
|
695
|
+
const syncTool = makeDispatch(
|
|
696
|
+
"sync",
|
|
697
|
+
"Bring a stale PR up to date with its base branch (merges base \u2192 head + pushes). Use when behindBy > 10 AND mergeable !== CONFLICTING AND ciStatus !== FAILING. Returns immediately \u2014 the actual merge runs in a separate workflow."
|
|
698
|
+
);
|
|
699
|
+
const fixCiTool = makeDispatch(
|
|
700
|
+
"fix-ci",
|
|
701
|
+
"Repair a PR whose CI is failing. Use when ciStatus === FAILING. The repair runs in a separate workflow."
|
|
702
|
+
);
|
|
703
|
+
const resolveTool = makeDispatch(
|
|
704
|
+
"resolve",
|
|
705
|
+
"Resolve merge conflicts on a PR. Use when mergeable === CONFLICTING. The repair runs in a separate workflow."
|
|
706
|
+
);
|
|
707
|
+
const recommendTool = tool3(
|
|
708
|
+
"recommend_to_operator",
|
|
709
|
+
"Post ONE comment on a PR with the operator @-mention prepended. Use this when a verb is NOT graduated in the trust ledger and you want the operator to confirm via the dashboard inbox. The mention handle is substituted from kody.config.json `github.operators` \u2014 do not type it yourself.",
|
|
710
|
+
{
|
|
711
|
+
pr: z3.number().int().positive().describe("PR number to comment on."),
|
|
712
|
+
body: z3.string().min(1).describe("Comment body (markdown). Do not include the operator mention \u2014 the engine prepends it.")
|
|
713
|
+
},
|
|
714
|
+
async (args) => {
|
|
715
|
+
const result = postRecommendation(args.pr, opts.operatorMention, args.body);
|
|
716
|
+
const text = result.ok ? `Recommendation posted on PR #${args.pr}.` : `Recommendation failed on PR #${args.pr}: ${result.error}`;
|
|
717
|
+
return {
|
|
718
|
+
content: [{ type: "text", text }]
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
);
|
|
722
|
+
const ledgerTool = tool3(
|
|
723
|
+
"read_ledger",
|
|
724
|
+
"Read the trust ledger (or any sentinel-fenced JSON manifest stored on a labeled issue). Returns `{found, issueNumber, payload}` where payload is the parsed JSON between `<!-- <label>:start -->` and `<!-- <label>:end -->` sentinels. Use `read_ledger({label: 'kody:cto-decisions'})` to look up per-verb graduation modes for the trust gate.",
|
|
725
|
+
{
|
|
726
|
+
label: z3.string().min(1).describe("GitHub issue label that identifies the manifest issue (e.g. 'kody:cto-decisions').")
|
|
727
|
+
},
|
|
728
|
+
async (args) => {
|
|
729
|
+
const result = readLedger(args.label);
|
|
730
|
+
return {
|
|
731
|
+
content: [
|
|
732
|
+
{
|
|
733
|
+
type: "text",
|
|
734
|
+
text: JSON.stringify(result, null, 2)
|
|
735
|
+
}
|
|
736
|
+
]
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
);
|
|
740
|
+
const server = createSdkMcpServer3({
|
|
741
|
+
name: "kody-duty",
|
|
742
|
+
version: "0.1.0",
|
|
743
|
+
tools: [listTool, syncTool, fixCiTool, resolveTool, recommendTool, ledgerTool]
|
|
744
|
+
});
|
|
745
|
+
return { server };
|
|
746
|
+
}
|
|
747
|
+
var FAIL_CONCLUSIONS, RUNNING_STATUSES, DUTY_MCP_TOOL_NAMES;
|
|
748
|
+
var init_dutyMcp = __esm({
|
|
749
|
+
"src/dutyMcp.ts"() {
|
|
750
|
+
"use strict";
|
|
751
|
+
init_issue();
|
|
752
|
+
FAIL_CONCLUSIONS = /* @__PURE__ */ new Set(["FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE", "CANCELLED"]);
|
|
753
|
+
RUNNING_STATUSES = /* @__PURE__ */ new Set(["IN_PROGRESS", "QUEUED", "PENDING", "WAITING", "REQUESTED"]);
|
|
754
|
+
DUTY_MCP_TOOL_NAMES = [
|
|
755
|
+
"list_prs_to_repair",
|
|
756
|
+
"sync_pr",
|
|
757
|
+
"fix_ci_pr",
|
|
758
|
+
"resolve_pr",
|
|
759
|
+
"recommend_to_operator",
|
|
760
|
+
"read_ledger"
|
|
761
|
+
];
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
// src/repoWorkspace.ts
|
|
766
|
+
import { spawn as spawn2, spawnSync } from "child_process";
|
|
767
|
+
import * as fs6 from "fs";
|
|
768
|
+
import * as path6 from "path";
|
|
769
|
+
async function resolveAndClone(reposRoot, repo, repoToken, cloneRepo) {
|
|
770
|
+
const name = repo?.trim();
|
|
771
|
+
if (!name || !REPO_RE.test(name)) return null;
|
|
772
|
+
const root = path6.resolve(reposRoot);
|
|
773
|
+
const dir = path6.resolve(root, name);
|
|
774
|
+
if (dir !== root && !dir.startsWith(root + path6.sep)) return null;
|
|
775
|
+
if (fs6.existsSync(path6.join(dir, ".git"))) return dir;
|
|
776
|
+
const inflight = repoClones.get(dir);
|
|
777
|
+
if (inflight) {
|
|
778
|
+
await inflight;
|
|
779
|
+
return dir;
|
|
780
|
+
}
|
|
781
|
+
const p = cloneRepo(name, repoToken, dir).finally(() => {
|
|
782
|
+
if (repoClones.get(dir) === p) repoClones.delete(dir);
|
|
783
|
+
});
|
|
784
|
+
repoClones.set(dir, p);
|
|
785
|
+
await p;
|
|
786
|
+
return dir;
|
|
787
|
+
}
|
|
788
|
+
async function ensureRepoCwd(opts) {
|
|
789
|
+
const dir = await resolveAndClone(
|
|
790
|
+
opts.reposRoot,
|
|
791
|
+
opts.repo,
|
|
792
|
+
opts.repoToken,
|
|
793
|
+
opts.cloneRepo
|
|
794
|
+
);
|
|
795
|
+
return dir ?? opts.baseCwd;
|
|
796
|
+
}
|
|
797
|
+
async function fetchRepo(opts) {
|
|
798
|
+
const dir = await resolveAndClone(
|
|
799
|
+
opts.reposRoot,
|
|
800
|
+
opts.repo,
|
|
801
|
+
opts.repoToken,
|
|
802
|
+
opts.cloneRepo ?? defaultCloneRepo
|
|
803
|
+
);
|
|
804
|
+
if (!dir) {
|
|
805
|
+
throw new Error(
|
|
806
|
+
`invalid repo "${opts.repo}" \u2014 expected "owner/name" with no path escapes`
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
return dir;
|
|
810
|
+
}
|
|
811
|
+
var REPO_RE, repoClones, defaultCloneRepo;
|
|
812
|
+
var init_repoWorkspace = __esm({
|
|
813
|
+
"src/repoWorkspace.ts"() {
|
|
814
|
+
"use strict";
|
|
815
|
+
REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
816
|
+
repoClones = /* @__PURE__ */ new Map();
|
|
817
|
+
defaultCloneRepo = (repo, token, dir) => {
|
|
818
|
+
fs6.mkdirSync(path6.dirname(dir), { recursive: true });
|
|
819
|
+
const authUrl = token ? `https://x-access-token:${token}@github.com/${repo}.git` : `https://github.com/${repo}.git`;
|
|
820
|
+
return new Promise((resolve6, reject) => {
|
|
821
|
+
const child = spawn2("git", ["clone", "--depth=1", authUrl, dir], {
|
|
822
|
+
stdio: "inherit"
|
|
823
|
+
});
|
|
824
|
+
child.on("exit", (code) => {
|
|
825
|
+
if (code !== 0) {
|
|
826
|
+
reject(new Error(`git clone ${repo} failed (exit ${code})`));
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
try {
|
|
830
|
+
const name = process.env.GIT_AUTHOR_NAME ?? "Kody Bot";
|
|
831
|
+
const email = process.env.GIT_AUTHOR_EMAIL ?? "kody-bot@users.noreply.github.com";
|
|
832
|
+
spawnSync("git", ["-C", dir, "config", "user.name", name]);
|
|
833
|
+
spawnSync("git", ["-C", dir, "config", "user.email", email]);
|
|
834
|
+
} catch {
|
|
835
|
+
}
|
|
836
|
+
resolve6();
|
|
837
|
+
});
|
|
838
|
+
child.on("error", reject);
|
|
839
|
+
});
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
// src/fetchRepoMcp.ts
|
|
845
|
+
var fetchRepoMcp_exports = {};
|
|
846
|
+
__export(fetchRepoMcp_exports, {
|
|
847
|
+
buildFetchRepoMcpServer: () => buildFetchRepoMcpServer
|
|
848
|
+
});
|
|
849
|
+
import {
|
|
850
|
+
createSdkMcpServer as createSdkMcpServer4,
|
|
851
|
+
tool as tool4
|
|
852
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
853
|
+
import { z as z4 } from "zod";
|
|
854
|
+
function buildFetchRepoMcpServer(opts) {
|
|
855
|
+
const fetchTool = tool4(
|
|
856
|
+
"fetch_repo",
|
|
857
|
+
'Clone another GitHub repository into your workspace so you can read and work on it. Pass `repo` as "owner/name" (e.g. "A-Guy-educ/A-Guy"). Returns the absolute path of the clone \u2014 then use your Read/Grep/Glob/Bash tools at that path to inspect it. Already-fetched repos are reused instantly. Use this whenever the user asks about a repository other than your current one \u2014 you are NOT limited to a single repo.',
|
|
858
|
+
{
|
|
859
|
+
repo: z4.string().describe('GitHub repository as "owner/name", e.g. "A-Guy-educ/A-Guy".')
|
|
860
|
+
},
|
|
861
|
+
async (args) => {
|
|
862
|
+
const repo = String(args.repo ?? "").trim();
|
|
863
|
+
try {
|
|
864
|
+
const dir = await fetchRepo({
|
|
865
|
+
reposRoot: opts.reposRoot,
|
|
866
|
+
repo,
|
|
867
|
+
repoToken: opts.repoToken
|
|
868
|
+
});
|
|
869
|
+
return {
|
|
870
|
+
content: [
|
|
871
|
+
{
|
|
872
|
+
type: "text",
|
|
873
|
+
text: `Cloned ${repo} \u2192 ${dir}
|
|
874
|
+
Use Read/Grep/Glob/Bash at that absolute path to explore it. It now lives in your workspace alongside any other repos you've fetched.`
|
|
875
|
+
}
|
|
876
|
+
]
|
|
877
|
+
};
|
|
878
|
+
} catch (err) {
|
|
879
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
880
|
+
return {
|
|
881
|
+
content: [{ type: "text", text: `Could not fetch ${repo}: ${msg}` }],
|
|
882
|
+
isError: true
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
);
|
|
887
|
+
return createSdkMcpServer4({
|
|
888
|
+
name: "kody-fetch-repo",
|
|
889
|
+
version: "0.1.0",
|
|
890
|
+
tools: [fetchTool]
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
var init_fetchRepoMcp = __esm({
|
|
894
|
+
"src/fetchRepoMcp.ts"() {
|
|
895
|
+
"use strict";
|
|
896
|
+
init_repoWorkspace();
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
|
|
679
900
|
// src/prompt.ts
|
|
680
901
|
import * as fs21 from "fs";
|
|
681
902
|
import * as path20 from "path";
|
|
@@ -1088,7 +1309,7 @@ var init_loadPriorArt = __esm({
|
|
|
1088
1309
|
// package.json
|
|
1089
1310
|
var package_default = {
|
|
1090
1311
|
name: "@kody-ade/kody-engine",
|
|
1091
|
-
version: "0.4.
|
|
1312
|
+
version: "0.4.176",
|
|
1092
1313
|
description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
|
|
1093
1314
|
license: "MIT",
|
|
1094
1315
|
type: "module",
|
|
@@ -1146,7 +1367,7 @@ var package_default = {
|
|
|
1146
1367
|
// src/chat-cli.ts
|
|
1147
1368
|
import { execFileSync as execFileSync29 } from "child_process";
|
|
1148
1369
|
import * as fs43 from "fs";
|
|
1149
|
-
import * as
|
|
1370
|
+
import * as path40 from "path";
|
|
1150
1371
|
|
|
1151
1372
|
// src/chat/events.ts
|
|
1152
1373
|
import * as fs from "fs";
|
|
@@ -1702,7 +1923,6 @@ var BASH_WRITE_VERB = /\b(git\s+(commit|push|merge|rebase|tag|reset|cherry-pick)
|
|
|
1702
1923
|
function toolMayMutate(name, input) {
|
|
1703
1924
|
if (!name) return false;
|
|
1704
1925
|
if (MUTATING_FILE_TOOLS.has(name)) return true;
|
|
1705
|
-
if (name.startsWith("mcp__kody-submit__")) return true;
|
|
1706
1926
|
if (name === "Bash") return BASH_WRITE_VERB.test(String(input?.command ?? ""));
|
|
1707
1927
|
return false;
|
|
1708
1928
|
}
|
|
@@ -1783,6 +2003,19 @@ async function runAgent(opts) {
|
|
|
1783
2003
|
getSubmitted = submitHandle.getSubmitted;
|
|
1784
2004
|
mcpEntries.push(["kody-submit", submitHandle.server]);
|
|
1785
2005
|
}
|
|
2006
|
+
if (opts.enableDutyTool) {
|
|
2007
|
+
const { buildDutyMcpServer: buildDutyMcpServer2 } = await Promise.resolve().then(() => (init_dutyMcp(), dutyMcp_exports));
|
|
2008
|
+
if (!opts.dutyRepoSlug) {
|
|
2009
|
+
throw new Error(
|
|
2010
|
+
"enableDutyTool requires dutyRepoSlug (owner/name) \u2014 set kody.config.json github.{owner,repo} or GITHUB_REPOSITORY env var"
|
|
2011
|
+
);
|
|
2012
|
+
}
|
|
2013
|
+
const dutyHandle = buildDutyMcpServer2({
|
|
2014
|
+
repoSlug: opts.dutyRepoSlug,
|
|
2015
|
+
operatorMention: opts.dutyOperatorMention ?? ""
|
|
2016
|
+
});
|
|
2017
|
+
mcpEntries.push(["kody-duty", dutyHandle.server]);
|
|
2018
|
+
}
|
|
1786
2019
|
if (opts.enableFetchRepoTool && opts.reposRoot) {
|
|
1787
2020
|
const { buildFetchRepoMcpServer: buildFetchRepoMcpServer2 } = await Promise.resolve().then(() => (init_fetchRepoMcp(), fetchRepoMcp_exports));
|
|
1788
2021
|
const fetchServer = buildFetchRepoMcpServer2({
|
|
@@ -2848,7 +3081,7 @@ async function emit2(sink, type, sessionId, suffix, payload) {
|
|
|
2848
3081
|
// src/kody-cli.ts
|
|
2849
3082
|
import { execFileSync as execFileSync28 } from "child_process";
|
|
2850
3083
|
import * as fs42 from "fs";
|
|
2851
|
-
import * as
|
|
3084
|
+
import * as path39 from "path";
|
|
2852
3085
|
|
|
2853
3086
|
// src/app-auth.ts
|
|
2854
3087
|
import { createSign } from "crypto";
|
|
@@ -2998,6 +3231,12 @@ var POLITE_WORDS = /* @__PURE__ */ new Set([
|
|
|
2998
3231
|
"pls",
|
|
2999
3232
|
"yo"
|
|
3000
3233
|
]);
|
|
3234
|
+
function primaryNumericInputName(executable) {
|
|
3235
|
+
const inputs = getProfileInputs(executable);
|
|
3236
|
+
if (!inputs) return null;
|
|
3237
|
+
const intInput = inputs.find((i) => i.type === "int" && i.required);
|
|
3238
|
+
return intInput?.name ?? null;
|
|
3239
|
+
}
|
|
3001
3240
|
function autoDispatch(opts) {
|
|
3002
3241
|
const explicit = opts?.explicit;
|
|
3003
3242
|
if (explicit?.issueNumber && explicit.issueNumber > 0) {
|
|
@@ -3017,7 +3256,8 @@ function autoDispatch(opts) {
|
|
|
3017
3256
|
if (!Number.isNaN(n) && n > 0) {
|
|
3018
3257
|
const exe = String(event.inputs?.executable ?? "").trim() || "run";
|
|
3019
3258
|
const base = String(event.inputs?.base ?? "").trim();
|
|
3020
|
-
const
|
|
3259
|
+
const targetKey = primaryNumericInputName(exe) ?? "issue";
|
|
3260
|
+
const cliArgs = { [targetKey]: n };
|
|
3021
3261
|
if (base) cliArgs.base = base;
|
|
3022
3262
|
return { executable: exe, cliArgs, target: n };
|
|
3023
3263
|
}
|
|
@@ -3258,9 +3498,9 @@ function coerceBare(spec, value) {
|
|
|
3258
3498
|
}
|
|
3259
3499
|
|
|
3260
3500
|
// src/executor.ts
|
|
3261
|
-
import { execFileSync as execFileSync27, spawn as
|
|
3501
|
+
import { execFileSync as execFileSync27, spawn as spawn10 } from "child_process";
|
|
3262
3502
|
import * as fs41 from "fs";
|
|
3263
|
-
import * as
|
|
3503
|
+
import * as path38 from "path";
|
|
3264
3504
|
|
|
3265
3505
|
// src/discipline.ts
|
|
3266
3506
|
var DISCIPLINE = `# Working discipline (applies to this entire task)
|
|
@@ -5495,10 +5735,10 @@ import * as fs23 from "fs";
|
|
|
5495
5735
|
import * as path22 from "path";
|
|
5496
5736
|
var VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "awaiting-merge", "done"]);
|
|
5497
5737
|
var GoalStateError = class extends Error {
|
|
5498
|
-
constructor(
|
|
5499
|
-
super(`Invalid goal state at ${
|
|
5738
|
+
constructor(path41, message) {
|
|
5739
|
+
super(`Invalid goal state at ${path41}:
|
|
5500
5740
|
${message}`);
|
|
5501
|
-
this.path =
|
|
5741
|
+
this.path = path41;
|
|
5502
5742
|
this.name = "GoalStateError";
|
|
5503
5743
|
}
|
|
5504
5744
|
path;
|
|
@@ -7117,6 +7357,9 @@ function parseFlatYaml(text) {
|
|
|
7117
7357
|
} else if (key === "mentions") {
|
|
7118
7358
|
const logins = value.split(",").map((s) => s.trim().replace(/^@/, "")).filter(Boolean);
|
|
7119
7359
|
if (logins.length > 0) out.mentions = logins;
|
|
7360
|
+
} else if (key === "tools") {
|
|
7361
|
+
const names = value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
7362
|
+
if (names.length > 0) out.tools = names;
|
|
7120
7363
|
}
|
|
7121
7364
|
}
|
|
7122
7365
|
return out;
|
|
@@ -9077,9 +9320,11 @@ var loadIssueStateComment = async (ctx, _profile, args) => {
|
|
|
9077
9320
|
};
|
|
9078
9321
|
|
|
9079
9322
|
// src/scripts/loadJobFromFile.ts
|
|
9323
|
+
init_dutyMcp();
|
|
9080
9324
|
import * as fs32 from "fs";
|
|
9081
9325
|
import * as path30 from "path";
|
|
9082
|
-
var
|
|
9326
|
+
var DUTY_TOOL_PALETTE = new Set(DUTY_MCP_TOOL_NAMES);
|
|
9327
|
+
var loadJobFromFile = async (ctx, profile, args) => {
|
|
9083
9328
|
const jobsDir = String(args?.jobsDir ?? ".kody/duties");
|
|
9084
9329
|
const workersDir = String(args?.workersDir ?? ".kody/staff");
|
|
9085
9330
|
const slugArg = String(args?.slugArg ?? "job");
|
|
@@ -9121,6 +9366,21 @@ var loadJobFromFile = async (ctx, _profile, args) => {
|
|
|
9121
9366
|
ctx.data.workerTitle = workerTitle;
|
|
9122
9367
|
ctx.data.workerPersona = workerPersona;
|
|
9123
9368
|
ctx.data.mentions = mentions;
|
|
9369
|
+
const declaredTools = frontmatter.tools ?? [];
|
|
9370
|
+
if (declaredTools.length > 0) {
|
|
9371
|
+
const unknown = declaredTools.filter((name) => !DUTY_TOOL_PALETTE.has(name));
|
|
9372
|
+
if (unknown.length > 0) {
|
|
9373
|
+
throw new Error(
|
|
9374
|
+
`loadJobFromFile: duty '${slug}' declared tools not in the kody-duty palette: ${unknown.join(", ")}. Available: ${[...DUTY_MCP_TOOL_NAMES].join(", ")}`
|
|
9375
|
+
);
|
|
9376
|
+
}
|
|
9377
|
+
const mcpToolNames = declaredTools.map((name) => `mcp__kody-duty__${name}`);
|
|
9378
|
+
profile.claudeCode.tools = [...mcpToolNames, "mcp__kody-submit__submit_state"];
|
|
9379
|
+
ctx.data.dutyTools = declaredTools;
|
|
9380
|
+
ctx.data.dutyOperatorMention = mentions;
|
|
9381
|
+
ctx.data.promptTemplate = "prompts/locked.md";
|
|
9382
|
+
ctx.data.dutyToolsList = declaredTools.map((name) => `- \`${name}\``).join("\n");
|
|
9383
|
+
}
|
|
9124
9384
|
};
|
|
9125
9385
|
function parseJobFile(raw, slug) {
|
|
9126
9386
|
let stripped = raw;
|
|
@@ -10054,8 +10314,8 @@ var FlyClient = class {
|
|
|
10054
10314
|
get fetch() {
|
|
10055
10315
|
return this.opts.fetchImpl ?? fetch;
|
|
10056
10316
|
}
|
|
10057
|
-
async call(
|
|
10058
|
-
const res = await this.fetch(`${FLY_API_BASE}${
|
|
10317
|
+
async call(path41, init = {}) {
|
|
10318
|
+
const res = await this.fetch(`${FLY_API_BASE}${path41}`, {
|
|
10059
10319
|
method: init.method ?? "GET",
|
|
10060
10320
|
headers: {
|
|
10061
10321
|
Authorization: `Bearer ${this.opts.token}`,
|
|
@@ -10066,7 +10326,7 @@ var FlyClient = class {
|
|
|
10066
10326
|
if (res.status === 404 && init.allow404) return null;
|
|
10067
10327
|
if (!res.ok) {
|
|
10068
10328
|
const text = await res.text().catch(() => "");
|
|
10069
|
-
throw new Error(`Fly API ${res.status} on ${
|
|
10329
|
+
throw new Error(`Fly API ${res.status} on ${path41}: ${text.slice(0, 200) || res.statusText}`);
|
|
10070
10330
|
}
|
|
10071
10331
|
if (res.status === 204) return null;
|
|
10072
10332
|
const raw = await res.text();
|
|
@@ -11963,10 +12223,467 @@ var runnerServe = async (ctx) => {
|
|
|
11963
12223
|
});
|
|
11964
12224
|
};
|
|
11965
12225
|
|
|
12226
|
+
// src/scripts/runPreviewBuild.ts
|
|
12227
|
+
import { copyFile, writeFile } from "fs/promises";
|
|
12228
|
+
import * as path36 from "path";
|
|
12229
|
+
import { fileURLToPath } from "url";
|
|
12230
|
+
|
|
12231
|
+
// src/scripts/previewBuildHelpers.ts
|
|
12232
|
+
import { createDecipheriv as createDecipheriv2, createHash } from "crypto";
|
|
12233
|
+
var NEVER_PASS_TO_BUILD = /* @__PURE__ */ new Set([
|
|
12234
|
+
"FLY_API_TOKEN",
|
|
12235
|
+
"FLY_ORG_SLUG",
|
|
12236
|
+
"FLY_DEFAULT_REGION",
|
|
12237
|
+
"KODY_MASTER_KEY",
|
|
12238
|
+
// Preview-config knob; consumed by the dispatcher before spawn.
|
|
12239
|
+
"KODY_PREVIEW_BUILD_MODE"
|
|
12240
|
+
]);
|
|
12241
|
+
function shortHash(s) {
|
|
12242
|
+
return createHash("sha256").update(s).digest("hex").slice(0, 6);
|
|
12243
|
+
}
|
|
12244
|
+
function previewAppName(repo, pr) {
|
|
12245
|
+
const [owner, name] = repo.split("/");
|
|
12246
|
+
if (!owner || !name) {
|
|
12247
|
+
throw new Error(`invalid repo "${repo}", expected "owner/name"`);
|
|
12248
|
+
}
|
|
12249
|
+
return `kp-${shortHash(owner)}-${shortHash(name)}-pr-${pr}`;
|
|
12250
|
+
}
|
|
12251
|
+
function basePreviewAppName(repo) {
|
|
12252
|
+
const [owner, name] = repo.split("/");
|
|
12253
|
+
if (!owner || !name) {
|
|
12254
|
+
throw new Error(`invalid repo "${repo}", expected "owner/name"`);
|
|
12255
|
+
}
|
|
12256
|
+
return `kp-${shortHash(owner)}-${shortHash(name)}-base`;
|
|
12257
|
+
}
|
|
12258
|
+
function decryptVaultPayload(payload, keyRaw) {
|
|
12259
|
+
const parts = payload.split(":");
|
|
12260
|
+
if (parts.length !== 4 || parts[0] !== "v1") {
|
|
12261
|
+
throw new Error("invalid vault payload format");
|
|
12262
|
+
}
|
|
12263
|
+
const [, ivB64, ctB64, tagB64] = parts;
|
|
12264
|
+
const key = /^[0-9a-fA-F]{64}$/.test(keyRaw) ? Buffer.from(keyRaw, "hex") : Buffer.from(keyRaw, "base64");
|
|
12265
|
+
if (key.length !== 32) {
|
|
12266
|
+
throw new Error("KODY_MASTER_KEY must decode to 32 bytes");
|
|
12267
|
+
}
|
|
12268
|
+
const iv = Buffer.from(ivB64, "base64");
|
|
12269
|
+
const ct = Buffer.from(ctB64, "base64");
|
|
12270
|
+
const tag = Buffer.from(tagB64, "base64");
|
|
12271
|
+
const decipher = createDecipheriv2("aes-256-gcm", key, iv);
|
|
12272
|
+
decipher.setAuthTag(tag);
|
|
12273
|
+
return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf8");
|
|
12274
|
+
}
|
|
12275
|
+
function buildEnvFromVault(doc) {
|
|
12276
|
+
const buildEnv = {};
|
|
12277
|
+
for (const [name, entry] of Object.entries(doc.secrets ?? {})) {
|
|
12278
|
+
if (!entry?.value) continue;
|
|
12279
|
+
if (NEVER_PASS_TO_BUILD.has(name)) continue;
|
|
12280
|
+
buildEnv[name] = entry.value;
|
|
12281
|
+
}
|
|
12282
|
+
const raw = doc.secrets?.KODY_PREVIEW_BUILD_MODE?.value;
|
|
12283
|
+
const buildMode = raw?.toLowerCase().trim() === "dev" ? "dev" : "prod";
|
|
12284
|
+
return { buildEnv, buildMode };
|
|
12285
|
+
}
|
|
12286
|
+
function formatPreviewComment(args) {
|
|
12287
|
+
const url = `https://${args.appName}.fly.dev`;
|
|
12288
|
+
return [
|
|
12289
|
+
"<!-- kody-fly-preview -->",
|
|
12290
|
+
`\u2705 **Preview ready:** ${url}`,
|
|
12291
|
+
"",
|
|
12292
|
+
`<sub>App: \`${args.appName}\` \xB7 Commit: \`${args.ref.slice(0, 7)}\` \xB7 Updated: ${args.nowIso}</sub>`
|
|
12293
|
+
].join("\n");
|
|
12294
|
+
}
|
|
12295
|
+
function defaultImageTag(repo, ref) {
|
|
12296
|
+
return createHash("sha256").update(`${repo}@${ref}`).digest("hex").slice(0, 12);
|
|
12297
|
+
}
|
|
12298
|
+
|
|
12299
|
+
// src/scripts/previewBuildRun.ts
|
|
12300
|
+
import { spawn as spawn6 } from "child_process";
|
|
12301
|
+
async function runCmd(cmd, args, opts = {}) {
|
|
12302
|
+
await new Promise((resolve6, reject) => {
|
|
12303
|
+
const child = spawn6(cmd, args, {
|
|
12304
|
+
cwd: opts.cwd,
|
|
12305
|
+
env: { ...process.env, ...opts.env ?? {} },
|
|
12306
|
+
stdio: opts.input ? ["pipe", "inherit", "inherit"] : "inherit"
|
|
12307
|
+
});
|
|
12308
|
+
if (opts.input && child.stdin) {
|
|
12309
|
+
child.stdin.write(opts.input);
|
|
12310
|
+
child.stdin.end();
|
|
12311
|
+
}
|
|
12312
|
+
child.on("error", reject);
|
|
12313
|
+
child.on("close", (code) => {
|
|
12314
|
+
if (code === 0) resolve6();
|
|
12315
|
+
else reject(new Error(`${cmd} ${args.join(" ")} exited ${code}`));
|
|
12316
|
+
});
|
|
12317
|
+
});
|
|
12318
|
+
}
|
|
12319
|
+
|
|
12320
|
+
// src/scripts/runPreviewBuild.ts
|
|
12321
|
+
var FLY_MACHINES = "https://api.machines.dev/v1";
|
|
12322
|
+
var FLY_GRAPHQL = "https://api.fly.io/graphql";
|
|
12323
|
+
var REQ_TIMEOUT_MS = 3e4;
|
|
12324
|
+
function bundledDockerfilePath(mode) {
|
|
12325
|
+
const here = path36.dirname(fileURLToPath(import.meta.url));
|
|
12326
|
+
const file = mode === "dev" ? "default-Dockerfile.preview.dev" : "default-Dockerfile.preview.prod";
|
|
12327
|
+
return path36.join(here, "preview-build-templates", file);
|
|
12328
|
+
}
|
|
12329
|
+
function required(name) {
|
|
12330
|
+
const v = (process.env[name] ?? "").trim();
|
|
12331
|
+
if (!v) throw new Error(`${name} is required`);
|
|
12332
|
+
return v;
|
|
12333
|
+
}
|
|
12334
|
+
function flyHeaders(token) {
|
|
12335
|
+
return {
|
|
12336
|
+
Authorization: `Bearer ${token}`,
|
|
12337
|
+
"Content-Type": "application/json"
|
|
12338
|
+
};
|
|
12339
|
+
}
|
|
12340
|
+
async function ghJSON(url, token) {
|
|
12341
|
+
const res = await fetch(url, {
|
|
12342
|
+
headers: {
|
|
12343
|
+
Authorization: `Bearer ${token}`,
|
|
12344
|
+
Accept: "application/vnd.github+json",
|
|
12345
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
12346
|
+
},
|
|
12347
|
+
signal: AbortSignal.timeout(REQ_TIMEOUT_MS)
|
|
12348
|
+
});
|
|
12349
|
+
if (!res.ok) {
|
|
12350
|
+
throw new Error(`GitHub ${url}: ${res.status} ${res.statusText}`);
|
|
12351
|
+
}
|
|
12352
|
+
return await res.json();
|
|
12353
|
+
}
|
|
12354
|
+
async function fetchVaultDoc(repo, ghToken4, masterKey) {
|
|
12355
|
+
const meta = await ghJSON(
|
|
12356
|
+
`https://api.github.com/repos/${repo}/contents/.kody/secrets.enc`,
|
|
12357
|
+
ghToken4
|
|
12358
|
+
);
|
|
12359
|
+
const payload = Buffer.from(meta.content, "base64").toString("utf8");
|
|
12360
|
+
const plaintext = decryptVaultPayload(payload, masterKey);
|
|
12361
|
+
return JSON.parse(plaintext);
|
|
12362
|
+
}
|
|
12363
|
+
async function flyAppExists(name, token) {
|
|
12364
|
+
const res = await fetch(`${FLY_MACHINES}/apps/${encodeURIComponent(name)}`, {
|
|
12365
|
+
headers: flyHeaders(token),
|
|
12366
|
+
signal: AbortSignal.timeout(REQ_TIMEOUT_MS)
|
|
12367
|
+
});
|
|
12368
|
+
if (res.status === 404) return false;
|
|
12369
|
+
if (!res.ok) {
|
|
12370
|
+
throw new Error(`appExists ${name}: ${res.status} ${res.statusText}`);
|
|
12371
|
+
}
|
|
12372
|
+
return true;
|
|
12373
|
+
}
|
|
12374
|
+
async function flyCreateApp(name, orgSlug, token) {
|
|
12375
|
+
const res = await fetch(`${FLY_MACHINES}/apps`, {
|
|
12376
|
+
method: "POST",
|
|
12377
|
+
headers: flyHeaders(token),
|
|
12378
|
+
body: JSON.stringify({ app_name: name, org_slug: orgSlug }),
|
|
12379
|
+
signal: AbortSignal.timeout(REQ_TIMEOUT_MS)
|
|
12380
|
+
});
|
|
12381
|
+
if (res.status === 422) return;
|
|
12382
|
+
if (!res.ok) {
|
|
12383
|
+
const text = await res.text().catch(() => "");
|
|
12384
|
+
throw new Error(`createApp ${name}: ${res.status} ${text.slice(0, 200)}`);
|
|
12385
|
+
}
|
|
12386
|
+
}
|
|
12387
|
+
async function flyAllocateSharedIps(appName, token) {
|
|
12388
|
+
const mutation = `
|
|
12389
|
+
mutation AllocateIps($appId: ID!) {
|
|
12390
|
+
v4: allocateIpAddress(input: { appId: $appId, type: shared_v4 }) { ipAddress { address } }
|
|
12391
|
+
v6: allocateIpAddress(input: { appId: $appId, type: v6 }) { ipAddress { address } }
|
|
12392
|
+
}
|
|
12393
|
+
`;
|
|
12394
|
+
const res = await fetch(FLY_GRAPHQL, {
|
|
12395
|
+
method: "POST",
|
|
12396
|
+
headers: flyHeaders(token),
|
|
12397
|
+
body: JSON.stringify({ query: mutation, variables: { appId: appName } }),
|
|
12398
|
+
signal: AbortSignal.timeout(REQ_TIMEOUT_MS)
|
|
12399
|
+
});
|
|
12400
|
+
if (!res.ok) {
|
|
12401
|
+
throw new Error(`allocateSharedIps ${appName}: ${res.status}`);
|
|
12402
|
+
}
|
|
12403
|
+
const data = await res.json();
|
|
12404
|
+
if (data.errors?.length) {
|
|
12405
|
+
const msgs = data.errors.map((e) => e.message).join("; ");
|
|
12406
|
+
if (!/already|exists/i.test(msgs)) {
|
|
12407
|
+
throw new Error(`allocateSharedIps: ${msgs}`);
|
|
12408
|
+
}
|
|
12409
|
+
}
|
|
12410
|
+
}
|
|
12411
|
+
async function flyListMachines(appName, token) {
|
|
12412
|
+
const res = await fetch(
|
|
12413
|
+
`${FLY_MACHINES}/apps/${encodeURIComponent(appName)}/machines`,
|
|
12414
|
+
{
|
|
12415
|
+
headers: flyHeaders(token),
|
|
12416
|
+
signal: AbortSignal.timeout(REQ_TIMEOUT_MS)
|
|
12417
|
+
}
|
|
12418
|
+
);
|
|
12419
|
+
if (res.status === 404) return [];
|
|
12420
|
+
if (!res.ok) {
|
|
12421
|
+
throw new Error(`listMachines ${appName}: ${res.status}`);
|
|
12422
|
+
}
|
|
12423
|
+
const data = await res.json();
|
|
12424
|
+
return data.map((m) => ({ id: m.id, state: m.state }));
|
|
12425
|
+
}
|
|
12426
|
+
async function flyDestroyMachine(appName, machineId, token) {
|
|
12427
|
+
await fetch(
|
|
12428
|
+
`${FLY_MACHINES}/apps/${encodeURIComponent(appName)}/machines/${encodeURIComponent(machineId)}/stop`,
|
|
12429
|
+
{
|
|
12430
|
+
method: "POST",
|
|
12431
|
+
headers: flyHeaders(token),
|
|
12432
|
+
signal: AbortSignal.timeout(REQ_TIMEOUT_MS)
|
|
12433
|
+
}
|
|
12434
|
+
).catch(() => void 0);
|
|
12435
|
+
const res = await fetch(
|
|
12436
|
+
`${FLY_MACHINES}/apps/${encodeURIComponent(appName)}/machines/${encodeURIComponent(machineId)}?force=true`,
|
|
12437
|
+
{
|
|
12438
|
+
method: "DELETE",
|
|
12439
|
+
headers: flyHeaders(token),
|
|
12440
|
+
signal: AbortSignal.timeout(REQ_TIMEOUT_MS)
|
|
12441
|
+
}
|
|
12442
|
+
);
|
|
12443
|
+
if (res.status === 404) return;
|
|
12444
|
+
if (!res.ok) {
|
|
12445
|
+
throw new Error(`destroyMachine ${machineId}: ${res.status}`);
|
|
12446
|
+
}
|
|
12447
|
+
}
|
|
12448
|
+
async function flyCreatePreviewMachine(args, token) {
|
|
12449
|
+
const body = {
|
|
12450
|
+
region: args.region,
|
|
12451
|
+
config: {
|
|
12452
|
+
image: args.image,
|
|
12453
|
+
env: args.env,
|
|
12454
|
+
auto_destroy: false,
|
|
12455
|
+
restart: { policy: "always" },
|
|
12456
|
+
// 4 GB / 2 CPU is the floor that compiles A-Guy-class pages
|
|
12457
|
+
// without OOM when something forces a runtime recompile.
|
|
12458
|
+
guest: { cpu_kind: "shared", cpus: 2, memory_mb: 4096 },
|
|
12459
|
+
services: [
|
|
12460
|
+
{
|
|
12461
|
+
ports: [
|
|
12462
|
+
{ port: 443, handlers: ["tls", "http"], force_https: false },
|
|
12463
|
+
{ port: 80, handlers: ["http"] }
|
|
12464
|
+
],
|
|
12465
|
+
protocol: "tcp",
|
|
12466
|
+
internal_port: 8080,
|
|
12467
|
+
auto_stop_machines: "suspend",
|
|
12468
|
+
auto_start_machines: true,
|
|
12469
|
+
min_machines_running: 0
|
|
12470
|
+
}
|
|
12471
|
+
],
|
|
12472
|
+
checks: {
|
|
12473
|
+
httpget: {
|
|
12474
|
+
type: "http",
|
|
12475
|
+
port: 8080,
|
|
12476
|
+
method: "GET",
|
|
12477
|
+
path: "/",
|
|
12478
|
+
interval: "15s",
|
|
12479
|
+
timeout: "10s",
|
|
12480
|
+
grace_period: "30s"
|
|
12481
|
+
}
|
|
12482
|
+
}
|
|
12483
|
+
}
|
|
12484
|
+
};
|
|
12485
|
+
let lastErr = null;
|
|
12486
|
+
for (let attempt = 0; attempt < 6; attempt++) {
|
|
12487
|
+
const res = await fetch(
|
|
12488
|
+
`${FLY_MACHINES}/apps/${encodeURIComponent(args.appName)}/machines`,
|
|
12489
|
+
{
|
|
12490
|
+
method: "POST",
|
|
12491
|
+
headers: flyHeaders(token),
|
|
12492
|
+
body: JSON.stringify(body),
|
|
12493
|
+
signal: AbortSignal.timeout(REQ_TIMEOUT_MS)
|
|
12494
|
+
}
|
|
12495
|
+
);
|
|
12496
|
+
if (res.ok) {
|
|
12497
|
+
const { id } = await res.json();
|
|
12498
|
+
return id;
|
|
12499
|
+
}
|
|
12500
|
+
const text = await res.text().catch(() => "");
|
|
12501
|
+
lastErr = new Error(
|
|
12502
|
+
`createPreviewMachine ${res.status}: ${text.slice(0, 300)}`
|
|
12503
|
+
);
|
|
12504
|
+
if (!/MANIFEST_UNKNOWN|manifest unknown/i.test(text)) break;
|
|
12505
|
+
await new Promise((r) => setTimeout(r, 2e3 * (attempt + 1)));
|
|
12506
|
+
}
|
|
12507
|
+
throw lastErr ?? new Error("createPreviewMachine failed (unknown)");
|
|
12508
|
+
}
|
|
12509
|
+
async function postOrUpdatePreviewComment(args) {
|
|
12510
|
+
const MARKER = "<!-- kody-fly-preview -->";
|
|
12511
|
+
const base = `https://api.github.com/repos/${args.repo}/issues/${args.pr}/comments`;
|
|
12512
|
+
const headers = {
|
|
12513
|
+
Authorization: `Bearer ${args.token}`,
|
|
12514
|
+
Accept: "application/vnd.github+json",
|
|
12515
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
12516
|
+
"Content-Type": "application/json"
|
|
12517
|
+
};
|
|
12518
|
+
const listRes = await fetch(`${base}?per_page=100`, {
|
|
12519
|
+
headers,
|
|
12520
|
+
signal: AbortSignal.timeout(REQ_TIMEOUT_MS)
|
|
12521
|
+
}).catch(() => null);
|
|
12522
|
+
let existingId = null;
|
|
12523
|
+
if (listRes && listRes.ok) {
|
|
12524
|
+
const comments = await listRes.json().catch(() => []);
|
|
12525
|
+
const hit = comments.find((c) => (c.body ?? "").includes(MARKER));
|
|
12526
|
+
if (hit) existingId = hit.id;
|
|
12527
|
+
}
|
|
12528
|
+
if (existingId) {
|
|
12529
|
+
await fetch(
|
|
12530
|
+
`https://api.github.com/repos/${args.repo}/issues/comments/${existingId}`,
|
|
12531
|
+
{
|
|
12532
|
+
method: "PATCH",
|
|
12533
|
+
headers,
|
|
12534
|
+
body: JSON.stringify({ body: args.body }),
|
|
12535
|
+
signal: AbortSignal.timeout(REQ_TIMEOUT_MS)
|
|
12536
|
+
}
|
|
12537
|
+
);
|
|
12538
|
+
return;
|
|
12539
|
+
}
|
|
12540
|
+
await fetch(base, {
|
|
12541
|
+
method: "POST",
|
|
12542
|
+
headers,
|
|
12543
|
+
body: JSON.stringify({ body: args.body }),
|
|
12544
|
+
signal: AbortSignal.timeout(REQ_TIMEOUT_MS)
|
|
12545
|
+
});
|
|
12546
|
+
}
|
|
12547
|
+
var runPreviewBuild = async (ctx, _profile, _args) => {
|
|
12548
|
+
ctx.skipAgent = true;
|
|
12549
|
+
const pr = Number(ctx.args.pr);
|
|
12550
|
+
if (!Number.isFinite(pr) || pr <= 0) {
|
|
12551
|
+
ctx.output.exitCode = 99;
|
|
12552
|
+
ctx.output.reason = `runPreviewBuild: invalid pr arg "${ctx.args.pr}"`;
|
|
12553
|
+
return;
|
|
12554
|
+
}
|
|
12555
|
+
let repo;
|
|
12556
|
+
let ref;
|
|
12557
|
+
let flyToken;
|
|
12558
|
+
let masterKey;
|
|
12559
|
+
let ghToken4;
|
|
12560
|
+
try {
|
|
12561
|
+
repo = required("GITHUB_REPOSITORY");
|
|
12562
|
+
ref = required("GITHUB_SHA");
|
|
12563
|
+
flyToken = required("FLY_API_TOKEN");
|
|
12564
|
+
masterKey = required("KODY_MASTER_KEY");
|
|
12565
|
+
ghToken4 = required("GITHUB_TOKEN");
|
|
12566
|
+
} catch (err) {
|
|
12567
|
+
ctx.output.exitCode = 99;
|
|
12568
|
+
ctx.output.reason = `runPreviewBuild: ${err instanceof Error ? err.message : String(err)}`;
|
|
12569
|
+
return;
|
|
12570
|
+
}
|
|
12571
|
+
const orgSlug = (process.env.FLY_ORG_SLUG ?? "personal").trim();
|
|
12572
|
+
const region = (process.env.FLY_REGION ?? "fra").trim();
|
|
12573
|
+
const ghcrOwner = process.env.KODY_PREVIEW_GHCR_OWNER?.trim() || "";
|
|
12574
|
+
const appName = previewAppName(repo, pr);
|
|
12575
|
+
const tag = defaultImageTag(repo, ref);
|
|
12576
|
+
try {
|
|
12577
|
+
const doc = await fetchVaultDoc(repo, ghToken4, masterKey);
|
|
12578
|
+
const { buildEnv, buildMode } = buildEnvFromVault(doc);
|
|
12579
|
+
console.log(
|
|
12580
|
+
`[preview-build] vault: ${Object.keys(buildEnv).length} secrets, mode=${buildMode}`
|
|
12581
|
+
);
|
|
12582
|
+
if (Object.keys(buildEnv).length > 0) {
|
|
12583
|
+
const lines = Object.entries(buildEnv).map(
|
|
12584
|
+
([k, v]) => `${k}=${JSON.stringify(v)}`
|
|
12585
|
+
);
|
|
12586
|
+
await writeFile(
|
|
12587
|
+
path36.join(ctx.cwd, ".env.production.local"),
|
|
12588
|
+
lines.join("\n") + "\n",
|
|
12589
|
+
"utf8"
|
|
12590
|
+
);
|
|
12591
|
+
}
|
|
12592
|
+
const consumerDockerfile = path36.join(ctx.cwd, "Dockerfile.preview");
|
|
12593
|
+
try {
|
|
12594
|
+
await copyFile(bundledDockerfilePath(buildMode), consumerDockerfile);
|
|
12595
|
+
console.log(`[preview-build] using bundled Dockerfile.preview.${buildMode}`);
|
|
12596
|
+
} catch (err) {
|
|
12597
|
+
console.warn(
|
|
12598
|
+
`[preview-build] failed to drop bundled Dockerfile: ${err instanceof Error ? err.message : err}`
|
|
12599
|
+
);
|
|
12600
|
+
}
|
|
12601
|
+
let baseImage = null;
|
|
12602
|
+
if (ghcrOwner) {
|
|
12603
|
+
const baseRef = `${ghcrOwner.toLowerCase()}/${basePreviewAppName(repo)}`;
|
|
12604
|
+
const tok = await fetch(
|
|
12605
|
+
`https://ghcr.io/token?scope=repository:${baseRef}:pull&service=ghcr.io`,
|
|
12606
|
+
{ signal: AbortSignal.timeout(15e3) }
|
|
12607
|
+
).catch(() => null);
|
|
12608
|
+
if (tok?.ok) {
|
|
12609
|
+
const { token: bearer } = await tok.json();
|
|
12610
|
+
const probe = await fetch(`https://ghcr.io/v2/${baseRef}/manifests/latest`, {
|
|
12611
|
+
method: "HEAD",
|
|
12612
|
+
headers: {
|
|
12613
|
+
Authorization: `Bearer ${bearer}`,
|
|
12614
|
+
Accept: "application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json"
|
|
12615
|
+
},
|
|
12616
|
+
signal: AbortSignal.timeout(15e3)
|
|
12617
|
+
}).catch(() => null);
|
|
12618
|
+
if (probe?.status === 200) {
|
|
12619
|
+
baseImage = `ghcr.io/${baseRef}:latest`;
|
|
12620
|
+
console.log(`[preview-build] inheriting from base ${baseImage}`);
|
|
12621
|
+
}
|
|
12622
|
+
}
|
|
12623
|
+
}
|
|
12624
|
+
if (!await flyAppExists(appName, flyToken)) {
|
|
12625
|
+
await flyCreateApp(appName, orgSlug, flyToken);
|
|
12626
|
+
}
|
|
12627
|
+
await flyAllocateSharedIps(appName, flyToken);
|
|
12628
|
+
await runCmd(
|
|
12629
|
+
"docker",
|
|
12630
|
+
["login", "registry.fly.io", "-u", "x", "--password-stdin"],
|
|
12631
|
+
{ input: flyToken, cwd: ctx.cwd }
|
|
12632
|
+
);
|
|
12633
|
+
const buildArgs = [
|
|
12634
|
+
"build",
|
|
12635
|
+
"-f",
|
|
12636
|
+
"Dockerfile.preview",
|
|
12637
|
+
"-t",
|
|
12638
|
+
`registry.fly.io/${appName}:${tag}`
|
|
12639
|
+
];
|
|
12640
|
+
if (baseImage) buildArgs.push("--build-arg", `BASE_IMAGE=${baseImage}`);
|
|
12641
|
+
buildArgs.push(".");
|
|
12642
|
+
await runCmd("docker", buildArgs, {
|
|
12643
|
+
cwd: ctx.cwd,
|
|
12644
|
+
env: { DOCKER_BUILDKIT: "1" }
|
|
12645
|
+
});
|
|
12646
|
+
await runCmd("docker", ["push", `registry.fly.io/${appName}:${tag}`], {
|
|
12647
|
+
cwd: ctx.cwd
|
|
12648
|
+
});
|
|
12649
|
+
const stale = await flyListMachines(appName, flyToken);
|
|
12650
|
+
for (const m of stale) {
|
|
12651
|
+
await flyDestroyMachine(appName, m.id, flyToken).catch(() => void 0);
|
|
12652
|
+
}
|
|
12653
|
+
const machineId = await flyCreatePreviewMachine(
|
|
12654
|
+
{
|
|
12655
|
+
appName,
|
|
12656
|
+
region,
|
|
12657
|
+
image: `registry.fly.io/${appName}:${tag}`,
|
|
12658
|
+
env: buildEnv
|
|
12659
|
+
},
|
|
12660
|
+
flyToken
|
|
12661
|
+
);
|
|
12662
|
+
console.log(
|
|
12663
|
+
`[preview-build] done \u2014 machine ${machineId} at https://${appName}.fly.dev`
|
|
12664
|
+
);
|
|
12665
|
+
await postOrUpdatePreviewComment({
|
|
12666
|
+
repo,
|
|
12667
|
+
pr,
|
|
12668
|
+
body: formatPreviewComment({
|
|
12669
|
+
appName,
|
|
12670
|
+
ref,
|
|
12671
|
+
nowIso: (/* @__PURE__ */ new Date()).toISOString()
|
|
12672
|
+
}),
|
|
12673
|
+
token: ghToken4
|
|
12674
|
+
});
|
|
12675
|
+
} catch (err) {
|
|
12676
|
+
ctx.output.exitCode = 1;
|
|
12677
|
+
ctx.output.reason = `runPreviewBuild: ${err instanceof Error ? err.message : String(err)}`;
|
|
12678
|
+
console.error("[preview-build] failed:", err);
|
|
12679
|
+
return;
|
|
12680
|
+
}
|
|
12681
|
+
};
|
|
12682
|
+
|
|
11966
12683
|
// src/scripts/runTickScript.ts
|
|
11967
12684
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
11968
12685
|
import * as fs39 from "fs";
|
|
11969
|
-
import * as
|
|
12686
|
+
import * as path37 from "path";
|
|
11970
12687
|
var runTickScript = async (ctx, _profile, args) => {
|
|
11971
12688
|
ctx.skipAgent = true;
|
|
11972
12689
|
const jobsDir = String(args?.jobsDir ?? ".kody/duties");
|
|
@@ -11978,7 +12695,7 @@ var runTickScript = async (ctx, _profile, args) => {
|
|
|
11978
12695
|
ctx.output.reason = `runTickScript: ctx.args.${slugArg} must be a non-empty slug`;
|
|
11979
12696
|
return;
|
|
11980
12697
|
}
|
|
11981
|
-
const jobPath =
|
|
12698
|
+
const jobPath = path37.join(ctx.cwd, jobsDir, `${slug}.md`);
|
|
11982
12699
|
if (!fs39.existsSync(jobPath)) {
|
|
11983
12700
|
ctx.output.exitCode = 99;
|
|
11984
12701
|
ctx.output.reason = `runTickScript: job file not found: ${jobPath}`;
|
|
@@ -11992,7 +12709,7 @@ var runTickScript = async (ctx, _profile, args) => {
|
|
|
11992
12709
|
ctx.output.reason = `runTickScript: job ${slug} has no \`tickScript:\` frontmatter \u2014 route via job-tick instead`;
|
|
11993
12710
|
return;
|
|
11994
12711
|
}
|
|
11995
|
-
const scriptPath =
|
|
12712
|
+
const scriptPath = path37.isAbsolute(tickScript) ? tickScript : path37.join(ctx.cwd, tickScript);
|
|
11996
12713
|
if (!fs39.existsSync(scriptPath)) {
|
|
11997
12714
|
ctx.output.exitCode = 99;
|
|
11998
12715
|
ctx.output.reason = `runTickScript: tickScript not found: ${scriptPath}`;
|
|
@@ -12144,7 +12861,7 @@ function synthesizeAction(ctx) {
|
|
|
12144
12861
|
}
|
|
12145
12862
|
|
|
12146
12863
|
// src/scripts/serveFlow.ts
|
|
12147
|
-
import { spawn as
|
|
12864
|
+
import { spawn as spawn7 } from "child_process";
|
|
12148
12865
|
function parseTarget(positional) {
|
|
12149
12866
|
if (!Array.isArray(positional) || positional.length === 0) return "none";
|
|
12150
12867
|
const first = String(positional[0]).toLowerCase();
|
|
@@ -12193,7 +12910,7 @@ var serveFlow = async (ctx) => {
|
|
|
12193
12910
|
if (usesProxy) process.stdout.write(` ANTHROPIC_BASE_URL=${url}
|
|
12194
12911
|
`);
|
|
12195
12912
|
const args = ["--dangerously-skip-permissions", "--model", model.model];
|
|
12196
|
-
const child =
|
|
12913
|
+
const child = spawn7("claude", args, { stdio: "inherit", env: editorEnv, cwd: ctx.cwd });
|
|
12197
12914
|
const exitCode = await new Promise((resolve6) => {
|
|
12198
12915
|
child.on("exit", (code) => resolve6(code ?? 0));
|
|
12199
12916
|
child.on("error", (err) => {
|
|
@@ -12214,7 +12931,7 @@ var serveFlow = async (ctx) => {
|
|
|
12214
12931
|
if (usesProxy) process.stdout.write(` ANTHROPIC_BASE_URL=${url}
|
|
12215
12932
|
`);
|
|
12216
12933
|
try {
|
|
12217
|
-
const code =
|
|
12934
|
+
const code = spawn7("code", [ctx.cwd], { stdio: "inherit", env: editorEnv, detached: true });
|
|
12218
12935
|
code.on("error", (err) => {
|
|
12219
12936
|
process.stderr.write(`[kody serve] failed to launch VS Code: ${err.message}
|
|
12220
12937
|
`);
|
|
@@ -12469,7 +13186,7 @@ var verify = async (ctx) => {
|
|
|
12469
13186
|
};
|
|
12470
13187
|
|
|
12471
13188
|
// src/scripts/verifyReproFails.ts
|
|
12472
|
-
import { spawn as
|
|
13189
|
+
import { spawn as spawn8 } from "child_process";
|
|
12473
13190
|
var TEST_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
12474
13191
|
var TAIL_CHARS2 = 8e3;
|
|
12475
13192
|
var ANSI_RE2 = /\x1B\[[0-?]*[ -/]*[@-~]/g;
|
|
@@ -12538,7 +13255,7 @@ function stripAnsi2(s) {
|
|
|
12538
13255
|
}
|
|
12539
13256
|
function runCommand2(command, cwd) {
|
|
12540
13257
|
return new Promise((resolve6) => {
|
|
12541
|
-
const child =
|
|
13258
|
+
const child = spawn8(command, {
|
|
12542
13259
|
cwd,
|
|
12543
13260
|
shell: true,
|
|
12544
13261
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" },
|
|
@@ -12869,7 +13586,7 @@ var appendCompanyActivity = async (ctx, _profile, agentResult) => {
|
|
|
12869
13586
|
};
|
|
12870
13587
|
|
|
12871
13588
|
// src/scripts/warmupMcp.ts
|
|
12872
|
-
import { spawn as
|
|
13589
|
+
import { spawn as spawn9 } from "child_process";
|
|
12873
13590
|
var PER_SERVER_TIMEOUT_MS = 6e4;
|
|
12874
13591
|
var PER_REQUEST_TIMEOUT_MS = 2e4;
|
|
12875
13592
|
var warmupMcp = async (_ctx, profile) => {
|
|
@@ -12891,7 +13608,7 @@ var warmupMcp = async (_ctx, profile) => {
|
|
|
12891
13608
|
}
|
|
12892
13609
|
};
|
|
12893
13610
|
async function warmupOne(command, args, env) {
|
|
12894
|
-
const child =
|
|
13611
|
+
const child = spawn9(command, args, {
|
|
12895
13612
|
stdio: ["pipe", "pipe", "pipe"],
|
|
12896
13613
|
env: env ? { ...process.env, ...env } : process.env
|
|
12897
13614
|
});
|
|
@@ -13171,6 +13888,7 @@ var preflightScripts = {
|
|
|
13171
13888
|
dispatchJobTicks,
|
|
13172
13889
|
dispatchJobFileTicks,
|
|
13173
13890
|
runTickScript,
|
|
13891
|
+
runPreviewBuild,
|
|
13174
13892
|
serveFlow,
|
|
13175
13893
|
brainServe,
|
|
13176
13894
|
runnerServe,
|
|
@@ -13246,24 +13964,24 @@ function firstRequiredFailure(results, tools) {
|
|
|
13246
13964
|
}
|
|
13247
13965
|
return null;
|
|
13248
13966
|
}
|
|
13249
|
-
function verifyOne(
|
|
13250
|
-
const result = { name:
|
|
13251
|
-
const checkRes = runShell(
|
|
13967
|
+
function verifyOne(tool5, cwd) {
|
|
13968
|
+
const result = { name: tool5.name, present: false, verified: false };
|
|
13969
|
+
const checkRes = runShell(tool5.install.checkCommand, cwd);
|
|
13252
13970
|
let present = checkRes.ok;
|
|
13253
|
-
if (!present &&
|
|
13254
|
-
runShell(
|
|
13255
|
-
present = runShell(
|
|
13971
|
+
if (!present && tool5.install.installCommand) {
|
|
13972
|
+
runShell(tool5.install.installCommand, cwd, 12e4);
|
|
13973
|
+
present = runShell(tool5.install.checkCommand, cwd).ok;
|
|
13256
13974
|
}
|
|
13257
13975
|
result.present = present;
|
|
13258
13976
|
if (!present) {
|
|
13259
|
-
result.error = `tool "${
|
|
13977
|
+
result.error = `tool "${tool5.name}" not on PATH (check: ${tool5.install.checkCommand})`;
|
|
13260
13978
|
return result;
|
|
13261
13979
|
}
|
|
13262
|
-
const verifyRes = runShell(
|
|
13980
|
+
const verifyRes = runShell(tool5.verify, cwd);
|
|
13263
13981
|
result.verified = verifyRes.ok;
|
|
13264
13982
|
if (!verifyRes.ok) {
|
|
13265
13983
|
const tail = formatStderrTail(verifyRes.stderr, verifyRes.stdout);
|
|
13266
|
-
result.error = `tool "${
|
|
13984
|
+
result.error = `tool "${tool5.name}" failed verify: ${tool5.verify}${tail ? ` \u2014 ${tail}` : ""}`;
|
|
13267
13985
|
}
|
|
13268
13986
|
return result;
|
|
13269
13987
|
}
|
|
@@ -13395,9 +14113,9 @@ async function runExecutable(profileName, input) {
|
|
|
13395
14113
|
})
|
|
13396
14114
|
};
|
|
13397
14115
|
})() : null;
|
|
13398
|
-
const ndjsonDir =
|
|
14116
|
+
const ndjsonDir = path38.join(input.cwd, ".kody");
|
|
13399
14117
|
const invokeAgent = async (prompt) => {
|
|
13400
|
-
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) =>
|
|
14118
|
+
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path38.isAbsolute(p) ? p : path38.resolve(profile.dir, p)).filter((p) => p.length > 0);
|
|
13401
14119
|
const syntheticPath = ctx.data.syntheticPluginPath;
|
|
13402
14120
|
const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
|
|
13403
14121
|
const agents = loadSubagents(profile);
|
|
@@ -13427,6 +14145,16 @@ async function runExecutable(profileName, input) {
|
|
|
13427
14145
|
cacheable: profile.claudeCode.cacheable,
|
|
13428
14146
|
enableVerifyTool: profile.claudeCode.enableVerifyTool,
|
|
13429
14147
|
enableSubmitTool: profile.claudeCode.enableSubmitTool,
|
|
14148
|
+
// Locked-toolbox duty mode: `loadJobFromFile` flips `ctx.data.dutyTools`
|
|
14149
|
+
// when a duty declares `tools:` frontmatter. The executor doesn't need
|
|
14150
|
+
// to know the palette — it just forwards the flag so agent.ts can spin
|
|
14151
|
+
// up the in-process `kody-duty` MCP server with the right context.
|
|
14152
|
+
enableDutyTool: Array.isArray(ctx.data.dutyTools) && ctx.data.dutyTools.length > 0,
|
|
14153
|
+
dutyOperatorMention: typeof ctx.data.dutyOperatorMention === "string" ? ctx.data.dutyOperatorMention : void 0,
|
|
14154
|
+
// owner/repo from kody.config.json; envelope falls back to GITHUB_REPOSITORY
|
|
14155
|
+
// for tester repos that don't set config.github (the file isn't always
|
|
14156
|
+
// checked in). Either way, dutyMcp needs "owner/name" to hit the compare API.
|
|
14157
|
+
dutyRepoSlug: config.github?.owner && config.github?.repo ? `${config.github.owner}/${config.github.repo}` : process.env.GITHUB_REPOSITORY?.trim() || void 0,
|
|
13430
14158
|
verifyToolMaxAttempts: profile.claudeCode.verifyAttempts ?? null,
|
|
13431
14159
|
verifyConfig: profile.claudeCode.enableVerifyTool ? config : void 0,
|
|
13432
14160
|
executableName: profileName,
|
|
@@ -13639,13 +14367,13 @@ function getProfileInputsForChild(profileName, _cwd) {
|
|
|
13639
14367
|
function resolveProfilePath(profileName) {
|
|
13640
14368
|
const found = resolveExecutable(profileName);
|
|
13641
14369
|
if (found) return found;
|
|
13642
|
-
const here =
|
|
14370
|
+
const here = path38.dirname(new URL(import.meta.url).pathname);
|
|
13643
14371
|
const candidates = [
|
|
13644
|
-
|
|
14372
|
+
path38.join(here, "executables", profileName, "profile.json"),
|
|
13645
14373
|
// same-dir sibling (dev)
|
|
13646
|
-
|
|
14374
|
+
path38.join(here, "..", "executables", profileName, "profile.json"),
|
|
13647
14375
|
// up one (prod: dist/bin → dist/executables)
|
|
13648
|
-
|
|
14376
|
+
path38.join(here, "..", "src", "executables", profileName, "profile.json")
|
|
13649
14377
|
// fallback
|
|
13650
14378
|
];
|
|
13651
14379
|
for (const c of candidates) {
|
|
@@ -13749,7 +14477,7 @@ function resolveShellTimeoutMs(entry) {
|
|
|
13749
14477
|
var SIGKILL_GRACE_MS = 5e3;
|
|
13750
14478
|
async function runShellEntry(entry, ctx, profile) {
|
|
13751
14479
|
const shellName = entry.shell;
|
|
13752
|
-
const shellPath =
|
|
14480
|
+
const shellPath = path38.join(profile.dir, shellName);
|
|
13753
14481
|
if (!fs41.existsSync(shellPath)) {
|
|
13754
14482
|
ctx.skipAgent = true;
|
|
13755
14483
|
ctx.output.exitCode = 99;
|
|
@@ -13766,7 +14494,7 @@ async function runShellEntry(entry, ctx, profile) {
|
|
|
13766
14494
|
env[`KODY_CFG_${k}`] = v;
|
|
13767
14495
|
}
|
|
13768
14496
|
const timeoutMs = resolveShellTimeoutMs(entry);
|
|
13769
|
-
const child =
|
|
14497
|
+
const child = spawn10("bash", [shellPath, ...positional], {
|
|
13770
14498
|
cwd: ctx.cwd,
|
|
13771
14499
|
env,
|
|
13772
14500
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -14256,9 +14984,9 @@ async function resolveAuthToken(env = process.env) {
|
|
|
14256
14984
|
return void 0;
|
|
14257
14985
|
}
|
|
14258
14986
|
function detectPackageManager2(cwd) {
|
|
14259
|
-
if (fs42.existsSync(
|
|
14260
|
-
if (fs42.existsSync(
|
|
14261
|
-
if (fs42.existsSync(
|
|
14987
|
+
if (fs42.existsSync(path39.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
14988
|
+
if (fs42.existsSync(path39.join(cwd, "yarn.lock"))) return "yarn";
|
|
14989
|
+
if (fs42.existsSync(path39.join(cwd, "bun.lockb"))) return "bun";
|
|
14262
14990
|
return "npm";
|
|
14263
14991
|
}
|
|
14264
14992
|
function shellOut(cmd, args, cwd, stream = true) {
|
|
@@ -14345,7 +15073,7 @@ function configureGitIdentity(cwd) {
|
|
|
14345
15073
|
}
|
|
14346
15074
|
function postFailureTail(issueNumber, cwd, reason) {
|
|
14347
15075
|
if (!issueNumber) return;
|
|
14348
|
-
const logPath =
|
|
15076
|
+
const logPath = path39.join(cwd, ".kody", "last-run.jsonl");
|
|
14349
15077
|
let tail = "";
|
|
14350
15078
|
try {
|
|
14351
15079
|
if (fs42.existsSync(logPath)) {
|
|
@@ -14374,7 +15102,7 @@ async function runCi(argv) {
|
|
|
14374
15102
|
return 0;
|
|
14375
15103
|
}
|
|
14376
15104
|
const args = parseCiArgs(argv);
|
|
14377
|
-
const cwd = args.cwd ?
|
|
15105
|
+
const cwd = args.cwd ? path39.resolve(args.cwd) : process.cwd();
|
|
14378
15106
|
let earlyConfig;
|
|
14379
15107
|
try {
|
|
14380
15108
|
earlyConfig = loadConfig(cwd);
|
|
@@ -14647,12 +15375,12 @@ function parseChatArgs(argv, env = process.env) {
|
|
|
14647
15375
|
return result;
|
|
14648
15376
|
}
|
|
14649
15377
|
function commitChatFiles(cwd, sessionId, verbose) {
|
|
14650
|
-
const sessionFile =
|
|
14651
|
-
const eventsFile =
|
|
15378
|
+
const sessionFile = path40.relative(cwd, sessionFilePath(cwd, sessionId));
|
|
15379
|
+
const eventsFile = path40.relative(cwd, eventsFilePath(cwd, sessionId));
|
|
14652
15380
|
const safeSession = sessionId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
14653
|
-
const tasksDir =
|
|
15381
|
+
const tasksDir = path40.join(".kody", "tasks", safeSession);
|
|
14654
15382
|
const candidatePaths = [sessionFile, eventsFile, tasksDir];
|
|
14655
|
-
const paths = candidatePaths.filter((p) => fs43.existsSync(
|
|
15383
|
+
const paths = candidatePaths.filter((p) => fs43.existsSync(path40.join(cwd, p)));
|
|
14656
15384
|
if (paths.length === 0) return;
|
|
14657
15385
|
const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
|
|
14658
15386
|
try {
|
|
@@ -14696,7 +15424,7 @@ async function runChat(argv) {
|
|
|
14696
15424
|
${CHAT_HELP}`);
|
|
14697
15425
|
return 64;
|
|
14698
15426
|
}
|
|
14699
|
-
const cwd = args.cwd ?
|
|
15427
|
+
const cwd = args.cwd ? path40.resolve(args.cwd) : process.cwd();
|
|
14700
15428
|
const sessionId = args.sessionId;
|
|
14701
15429
|
const unpackedSecrets = unpackAllSecrets();
|
|
14702
15430
|
if (unpackedSecrets > 0) {
|