@kody-ade/kody-engine 0.4.81 → 0.4.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/kody.js
CHANGED
|
@@ -460,16 +460,16 @@ var init_issue = __esm({
|
|
|
460
460
|
});
|
|
461
461
|
|
|
462
462
|
// src/prompt.ts
|
|
463
|
-
import * as
|
|
464
|
-
import * as
|
|
463
|
+
import * as fs16 from "fs";
|
|
464
|
+
import * as path14 from "path";
|
|
465
465
|
function loadProjectConventions(projectDir) {
|
|
466
466
|
const out = [];
|
|
467
467
|
for (const rel of CONVENTION_FILES) {
|
|
468
|
-
const abs =
|
|
469
|
-
if (!
|
|
468
|
+
const abs = path14.join(projectDir, rel);
|
|
469
|
+
if (!fs16.existsSync(abs)) continue;
|
|
470
470
|
let content;
|
|
471
471
|
try {
|
|
472
|
-
content =
|
|
472
|
+
content = fs16.readFileSync(abs, "utf-8");
|
|
473
473
|
} catch {
|
|
474
474
|
continue;
|
|
475
475
|
}
|
|
@@ -617,28 +617,28 @@ var loadMemoryContext_exports = {};
|
|
|
617
617
|
__export(loadMemoryContext_exports, {
|
|
618
618
|
loadMemoryContext: () => loadMemoryContext
|
|
619
619
|
});
|
|
620
|
-
import * as
|
|
621
|
-
import * as
|
|
620
|
+
import * as fs30 from "fs";
|
|
621
|
+
import * as path28 from "path";
|
|
622
622
|
function collectPages(memoryAbs) {
|
|
623
623
|
const out = [];
|
|
624
624
|
walkMd(memoryAbs, (file) => {
|
|
625
625
|
let stat;
|
|
626
626
|
try {
|
|
627
|
-
stat =
|
|
627
|
+
stat = fs30.statSync(file);
|
|
628
628
|
} catch {
|
|
629
629
|
return;
|
|
630
630
|
}
|
|
631
631
|
let raw;
|
|
632
632
|
try {
|
|
633
|
-
raw =
|
|
633
|
+
raw = fs30.readFileSync(file, "utf-8");
|
|
634
634
|
} catch {
|
|
635
635
|
return;
|
|
636
636
|
}
|
|
637
637
|
const fm = raw.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
638
|
-
const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ??
|
|
638
|
+
const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path28.basename(file, ".md");
|
|
639
639
|
const updated = fm?.[1]?.match(/^updated:\s*([0-9T:.+\-Z]+)/m)?.[1]?.trim() ?? "";
|
|
640
640
|
out.push({
|
|
641
|
-
relPath:
|
|
641
|
+
relPath: path28.relative(memoryAbs, file),
|
|
642
642
|
title,
|
|
643
643
|
updated,
|
|
644
644
|
content: raw.length > PER_PAGE_MAX_BYTES ? raw.slice(0, PER_PAGE_MAX_BYTES) + TRUNCATED_SUFFIX : raw,
|
|
@@ -706,16 +706,16 @@ function walkMd(root, visit) {
|
|
|
706
706
|
const dir = stack.pop();
|
|
707
707
|
let names;
|
|
708
708
|
try {
|
|
709
|
-
names =
|
|
709
|
+
names = fs30.readdirSync(dir);
|
|
710
710
|
} catch {
|
|
711
711
|
continue;
|
|
712
712
|
}
|
|
713
713
|
for (const name of names) {
|
|
714
714
|
if (name.startsWith(".")) continue;
|
|
715
|
-
const full =
|
|
715
|
+
const full = path28.join(dir, name);
|
|
716
716
|
let stat;
|
|
717
717
|
try {
|
|
718
|
-
stat =
|
|
718
|
+
stat = fs30.statSync(full);
|
|
719
719
|
} catch {
|
|
720
720
|
continue;
|
|
721
721
|
}
|
|
@@ -738,8 +738,8 @@ var init_loadMemoryContext = __esm({
|
|
|
738
738
|
TRUNCATED_SUFFIX = "\n\n\u2026 (truncated)";
|
|
739
739
|
loadMemoryContext = async (ctx) => {
|
|
740
740
|
if (typeof ctx.data.memoryContext === "string") return;
|
|
741
|
-
const memoryAbs =
|
|
742
|
-
if (!
|
|
741
|
+
const memoryAbs = path28.join(ctx.cwd, MEMORY_DIR_RELATIVE);
|
|
742
|
+
if (!fs30.existsSync(memoryAbs)) {
|
|
743
743
|
ctx.data.memoryContext = "";
|
|
744
744
|
return;
|
|
745
745
|
}
|
|
@@ -868,7 +868,7 @@ var init_loadPriorArt = __esm({
|
|
|
868
868
|
// package.json
|
|
869
869
|
var package_default = {
|
|
870
870
|
name: "@kody-ade/kody-engine",
|
|
871
|
-
version: "0.4.
|
|
871
|
+
version: "0.4.83",
|
|
872
872
|
description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
|
|
873
873
|
license: "MIT",
|
|
874
874
|
type: "module",
|
|
@@ -3702,529 +3702,982 @@ var advanceFlow = async (ctx, profile) => {
|
|
|
3702
3702
|
}
|
|
3703
3703
|
};
|
|
3704
3704
|
|
|
3705
|
-
// src/scripts/
|
|
3705
|
+
// src/scripts/brainServe.ts
|
|
3706
|
+
import { createServer } from "http";
|
|
3707
|
+
import * as fs14 from "fs";
|
|
3708
|
+
import * as path12 from "path";
|
|
3709
|
+
|
|
3710
|
+
// src/scripts/brainTurnLog.ts
|
|
3706
3711
|
import * as fs13 from "fs";
|
|
3707
|
-
import * as os3 from "os";
|
|
3708
3712
|
import * as path11 from "path";
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3713
|
+
var live = /* @__PURE__ */ new Map();
|
|
3714
|
+
function eventsPath(dir, chatId) {
|
|
3715
|
+
return path11.join(dir, ".kody", "brain-events", `${chatId}.jsonl`);
|
|
3716
|
+
}
|
|
3717
|
+
function lastPersistedSeq(dir, chatId) {
|
|
3718
|
+
const p = eventsPath(dir, chatId);
|
|
3719
|
+
if (!fs13.existsSync(p)) return 0;
|
|
3720
|
+
const lines = fs13.readFileSync(p, "utf-8").split("\n").filter(Boolean);
|
|
3721
|
+
if (lines.length === 0) return 0;
|
|
3722
|
+
try {
|
|
3723
|
+
return JSON.parse(lines[lines.length - 1]).seq || 0;
|
|
3724
|
+
} catch {
|
|
3725
|
+
return 0;
|
|
3721
3726
|
}
|
|
3722
|
-
return candidates[0];
|
|
3723
3727
|
}
|
|
3724
|
-
|
|
3725
|
-
const
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
const
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
if (fs13.existsSync(local)) return local;
|
|
3735
|
-
const central = path11.join(catalog, bucket, entry);
|
|
3736
|
-
if (fs13.existsSync(central)) return central;
|
|
3737
|
-
throw new Error(
|
|
3738
|
-
`buildSyntheticPlugin: ${bucket} entry '${entry}' not found in executable dir (${profile.dir}/${bucket}/) or catalog (${catalog}/${bucket}/)`
|
|
3739
|
-
);
|
|
3740
|
-
};
|
|
3741
|
-
if (cc.skills.length > 0) {
|
|
3742
|
-
const dst = path11.join(root, "skills");
|
|
3743
|
-
fs13.mkdirSync(dst, { recursive: true });
|
|
3744
|
-
for (const name of cc.skills) {
|
|
3745
|
-
copyDir(resolvePart("skills", name), path11.join(dst, name));
|
|
3728
|
+
function readSince(dir, chatId, since) {
|
|
3729
|
+
const p = eventsPath(dir, chatId);
|
|
3730
|
+
if (!fs13.existsSync(p)) return [];
|
|
3731
|
+
const out = [];
|
|
3732
|
+
for (const line of fs13.readFileSync(p, "utf-8").split("\n")) {
|
|
3733
|
+
if (!line) continue;
|
|
3734
|
+
try {
|
|
3735
|
+
const rec = JSON.parse(line);
|
|
3736
|
+
if (rec.seq > since) out.push(rec);
|
|
3737
|
+
} catch {
|
|
3746
3738
|
}
|
|
3747
3739
|
}
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3740
|
+
return out;
|
|
3741
|
+
}
|
|
3742
|
+
function isTerminal(event) {
|
|
3743
|
+
return event.type === "done" || event.type === "error";
|
|
3744
|
+
}
|
|
3745
|
+
function beginTurn(dir, chatId) {
|
|
3746
|
+
const existing = live.get(chatId);
|
|
3747
|
+
const seqFloor = existing ? existing.seq : lastPersistedSeq(dir, chatId);
|
|
3748
|
+
const turn = (existing?.turn ?? 0) + 1;
|
|
3749
|
+
const state = {
|
|
3750
|
+
seq: seqFloor,
|
|
3751
|
+
turn,
|
|
3752
|
+
status: "running",
|
|
3753
|
+
terminal: null,
|
|
3754
|
+
subscribers: /* @__PURE__ */ new Set()
|
|
3755
|
+
};
|
|
3756
|
+
live.set(chatId, state);
|
|
3757
|
+
const p = eventsPath(dir, chatId);
|
|
3758
|
+
fs13.mkdirSync(path11.dirname(p), { recursive: true });
|
|
3759
|
+
return (event) => {
|
|
3760
|
+
state.seq += 1;
|
|
3761
|
+
const rec = { seq: state.seq, turn, ts: Date.now(), event };
|
|
3762
|
+
try {
|
|
3763
|
+
fs13.appendFileSync(p, JSON.stringify(rec) + "\n");
|
|
3764
|
+
} catch (err) {
|
|
3765
|
+
process.stderr.write(
|
|
3766
|
+
`[brain-turn-log] append failed for ${chatId}: ${err instanceof Error ? err.message : String(err)}
|
|
3767
|
+
`
|
|
3768
|
+
);
|
|
3753
3769
|
}
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
fs13.copyFileSync(resolvePart("agents", `${name}.md`), path11.join(dst, `${name}.md`));
|
|
3770
|
+
for (const fn of state.subscribers) {
|
|
3771
|
+
try {
|
|
3772
|
+
fn(rec);
|
|
3773
|
+
} catch {
|
|
3774
|
+
}
|
|
3760
3775
|
}
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
if (!merged.hooks[event]) merged.hooks[event] = [];
|
|
3772
|
-
merged.hooks[event].push(...entries);
|
|
3776
|
+
if (isTerminal(event)) {
|
|
3777
|
+
state.status = "ended";
|
|
3778
|
+
state.terminal = rec;
|
|
3779
|
+
const subs = [...state.subscribers];
|
|
3780
|
+
state.subscribers.clear();
|
|
3781
|
+
for (const fn of subs) {
|
|
3782
|
+
try {
|
|
3783
|
+
fn(null);
|
|
3784
|
+
} catch {
|
|
3785
|
+
}
|
|
3773
3786
|
}
|
|
3774
3787
|
}
|
|
3775
|
-
fs13.writeFileSync(path11.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
|
|
3776
|
-
`);
|
|
3777
|
-
}
|
|
3778
|
-
const manifest = {
|
|
3779
|
-
name: `kody-synth-${profile.name}`,
|
|
3780
|
-
version: "1.0.0",
|
|
3781
|
-
description: `Synthetic plugin assembled by Kody for profile '${profile.name}' at runtime.`
|
|
3782
3788
|
};
|
|
3783
|
-
if (cc.skills.length > 0) manifest.skills = ["./skills/"];
|
|
3784
|
-
if (cc.commands.length > 0) manifest.commands = ["./commands/"];
|
|
3785
|
-
if (cc.subagents.length > 0) manifest.agents = cc.subagents.map((n) => `./agents/${n}.md`);
|
|
3786
|
-
fs13.writeFileSync(path11.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
|
|
3787
|
-
`);
|
|
3788
|
-
ctx.data.syntheticPluginPath = root;
|
|
3789
|
-
};
|
|
3790
|
-
function copyDir(src, dst) {
|
|
3791
|
-
fs13.mkdirSync(dst, { recursive: true });
|
|
3792
|
-
for (const ent of fs13.readdirSync(src, { withFileTypes: true })) {
|
|
3793
|
-
const s = path11.join(src, ent.name);
|
|
3794
|
-
const d = path11.join(dst, ent.name);
|
|
3795
|
-
if (ent.isDirectory()) copyDir(s, d);
|
|
3796
|
-
else if (ent.isFile()) fs13.copyFileSync(s, d);
|
|
3797
|
-
}
|
|
3798
|
-
}
|
|
3799
|
-
|
|
3800
|
-
// src/coverage.ts
|
|
3801
|
-
import { execFileSync as execFileSync8 } from "child_process";
|
|
3802
|
-
function patternToRegex(pattern) {
|
|
3803
|
-
let s = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
3804
|
-
s = s.replace(/\*\*\//g, "\xA7S").replace(/\*\*/g, "\xA7A").replace(/\*/g, "[^/]*");
|
|
3805
|
-
s = s.replace(/§S/g, "(?:.*/)?").replace(/§A/g, ".*");
|
|
3806
|
-
return new RegExp(`^${s}$`);
|
|
3807
|
-
}
|
|
3808
|
-
function renderSiblingPath(file, requireSibling) {
|
|
3809
|
-
const lastSlash = file.lastIndexOf("/");
|
|
3810
|
-
const dir = lastSlash === -1 ? "" : file.slice(0, lastSlash + 1);
|
|
3811
|
-
const base = lastSlash === -1 ? file : file.slice(lastSlash + 1);
|
|
3812
|
-
const name = base.replace(/\.[^.]+$/, "");
|
|
3813
|
-
const ext = base.match(/\.[^.]+$/)?.[0] ?? "";
|
|
3814
|
-
const sibling = requireSibling.replace(/\{name\}/g, name).replace(/\{ext\}/g, ext);
|
|
3815
|
-
return dir + sibling;
|
|
3816
3789
|
}
|
|
3817
|
-
function
|
|
3790
|
+
function endTurnIfUnterminated(dir, chatId, errMessage) {
|
|
3791
|
+
const state = live.get(chatId);
|
|
3792
|
+
if (!state || state.status === "ended") return;
|
|
3793
|
+
state.seq += 1;
|
|
3794
|
+
const rec = {
|
|
3795
|
+
seq: state.seq,
|
|
3796
|
+
turn: state.turn,
|
|
3797
|
+
ts: Date.now(),
|
|
3798
|
+
event: { type: "error", error: errMessage || "turn ended unexpectedly", chatId }
|
|
3799
|
+
};
|
|
3818
3800
|
try {
|
|
3819
|
-
|
|
3801
|
+
fs13.appendFileSync(eventsPath(dir, chatId), JSON.stringify(rec) + "\n");
|
|
3820
3802
|
} catch {
|
|
3821
|
-
return "";
|
|
3822
3803
|
}
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
const
|
|
3826
|
-
|
|
3827
|
-
const
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
}
|
|
3832
|
-
function checkCoverage(addedFiles, requirements) {
|
|
3833
|
-
if (requirements.length === 0) return [];
|
|
3834
|
-
const addedSet = new Set(addedFiles);
|
|
3835
|
-
const misses = [];
|
|
3836
|
-
for (const file of addedFiles) {
|
|
3837
|
-
if (/\.(test|spec)\./.test(file)) continue;
|
|
3838
|
-
for (const req of requirements) {
|
|
3839
|
-
const re = patternToRegex(req.pattern);
|
|
3840
|
-
if (!re.test(file)) continue;
|
|
3841
|
-
const expected = renderSiblingPath(file, req.requireSibling);
|
|
3842
|
-
if (!addedSet.has(expected)) {
|
|
3843
|
-
misses.push({ file, expectedTest: expected });
|
|
3844
|
-
}
|
|
3845
|
-
break;
|
|
3804
|
+
state.status = "ended";
|
|
3805
|
+
state.terminal = rec;
|
|
3806
|
+
const subs = [...state.subscribers];
|
|
3807
|
+
state.subscribers.clear();
|
|
3808
|
+
for (const fn of subs) {
|
|
3809
|
+
try {
|
|
3810
|
+
fn(rec);
|
|
3811
|
+
fn(null);
|
|
3812
|
+
} catch {
|
|
3846
3813
|
}
|
|
3847
3814
|
}
|
|
3848
|
-
return misses;
|
|
3849
|
-
}
|
|
3850
|
-
function formatMissesForFeedback(misses) {
|
|
3851
|
-
if (misses.length === 0) return "";
|
|
3852
|
-
const lines = ["The following files were added without a sibling test file:"];
|
|
3853
|
-
for (const m of misses) lines.push(`- \`${m.file}\` \u2192 expected \`${m.expectedTest}\``);
|
|
3854
|
-
lines.push("");
|
|
3855
|
-
lines.push(
|
|
3856
|
-
"Add the missing test files. Each should cover the new file's public API with at least a happy path and one failure path. Then re-emit DONE / COMMIT_MSG / PR_SUMMARY."
|
|
3857
|
-
);
|
|
3858
|
-
return lines.join("\n");
|
|
3859
3815
|
}
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
}
|
|
3869
|
-
if (!ctx.data.agentDone) {
|
|
3870
|
-
ctx.data.coverageMisses = [];
|
|
3871
|
-
return;
|
|
3816
|
+
function subscribe(dir, chatId, since, onRecord, onClose) {
|
|
3817
|
+
const backlog = readSince(dir, chatId, since);
|
|
3818
|
+
for (const rec of backlog) onRecord(rec);
|
|
3819
|
+
const lastReplayed = backlog.length ? backlog[backlog.length - 1] : null;
|
|
3820
|
+
if (lastReplayed && isTerminal(lastReplayed.event)) {
|
|
3821
|
+
onClose();
|
|
3822
|
+
return () => {
|
|
3823
|
+
};
|
|
3872
3824
|
}
|
|
3873
|
-
const
|
|
3874
|
-
if (
|
|
3875
|
-
|
|
3876
|
-
|
|
3825
|
+
const state = live.get(chatId);
|
|
3826
|
+
if (state && state.status === "running") {
|
|
3827
|
+
const fn = (rec) => {
|
|
3828
|
+
if (rec === null) {
|
|
3829
|
+
state.subscribers.delete(fn);
|
|
3830
|
+
onClose();
|
|
3831
|
+
return;
|
|
3832
|
+
}
|
|
3833
|
+
if (rec.seq > since) onRecord(rec);
|
|
3834
|
+
};
|
|
3835
|
+
state.subscribers.add(fn);
|
|
3836
|
+
return () => {
|
|
3837
|
+
state.subscribers.delete(fn);
|
|
3838
|
+
};
|
|
3877
3839
|
}
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3840
|
+
if (state && state.status === "ended" && state.terminal) {
|
|
3841
|
+
if (state.terminal.seq > since && !lastReplayed) onRecord(state.terminal);
|
|
3842
|
+
onClose();
|
|
3843
|
+
return () => {
|
|
3844
|
+
};
|
|
3883
3845
|
}
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
ctx.data.prSummary = retryParsed.prSummary || ctx.data.prSummary;
|
|
3896
|
-
}
|
|
3897
|
-
const finalMisses = checkCoverage(getAddedFiles(ctx.config.git.defaultBranch, ctx.cwd), reqs);
|
|
3898
|
-
ctx.data.coverageMisses = finalMisses;
|
|
3899
|
-
};
|
|
3900
|
-
|
|
3901
|
-
// src/scripts/classifyByLabel.ts
|
|
3902
|
-
var VALID_CLASSES = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
|
|
3903
|
-
var classifyByLabel = async (ctx) => {
|
|
3904
|
-
const issue = ctx.data.issue;
|
|
3905
|
-
const labels = issue?.labels;
|
|
3906
|
-
if (!labels || labels.length === 0) return;
|
|
3907
|
-
const cfgMap = ctx.config.classify?.labelMap;
|
|
3908
|
-
const map = cfgMap ?? defaultLabelMap();
|
|
3909
|
-
for (const label of labels) {
|
|
3910
|
-
const candidate = map[label.toLowerCase()];
|
|
3911
|
-
if (candidate && VALID_CLASSES.has(candidate)) {
|
|
3912
|
-
ctx.data.classification = candidate;
|
|
3913
|
-
ctx.data.classificationSource = "label";
|
|
3914
|
-
ctx.data.classificationReason = `label \`${label}\` \u2192 ${candidate}`;
|
|
3915
|
-
ctx.skipAgent = true;
|
|
3916
|
-
return;
|
|
3917
|
-
}
|
|
3846
|
+
if (lastReplayed) {
|
|
3847
|
+
onRecord({
|
|
3848
|
+
seq: lastReplayed.seq + 1,
|
|
3849
|
+
turn: lastReplayed.turn,
|
|
3850
|
+
ts: Date.now(),
|
|
3851
|
+
event: {
|
|
3852
|
+
type: "error",
|
|
3853
|
+
error: "stream interrupted (server restarted mid-reply) \u2014 resend your message",
|
|
3854
|
+
chatId
|
|
3855
|
+
}
|
|
3856
|
+
});
|
|
3918
3857
|
}
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
return {
|
|
3922
|
-
bug: "bug",
|
|
3923
|
-
enhancement: "bug",
|
|
3924
|
-
refactor: "feature",
|
|
3925
|
-
feature: "feature",
|
|
3926
|
-
performance: "feature",
|
|
3927
|
-
rfc: "spec",
|
|
3928
|
-
design: "spec",
|
|
3929
|
-
spec: "spec",
|
|
3930
|
-
docs: "chore",
|
|
3931
|
-
chore: "chore",
|
|
3932
|
-
dependencies: "chore"
|
|
3858
|
+
onClose();
|
|
3859
|
+
return () => {
|
|
3933
3860
|
};
|
|
3934
3861
|
}
|
|
3862
|
+
function getLastSeq(dir, chatId) {
|
|
3863
|
+
const state = live.get(chatId);
|
|
3864
|
+
if (state) return state.seq;
|
|
3865
|
+
return lastPersistedSeq(dir, chatId);
|
|
3866
|
+
}
|
|
3935
3867
|
|
|
3936
|
-
// src/scripts/
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3868
|
+
// src/scripts/brainServe.ts
|
|
3869
|
+
var DEFAULT_PORT = 8080;
|
|
3870
|
+
function getApiKey() {
|
|
3871
|
+
const key = (process.env.BRAIN_API_KEY ?? "").trim();
|
|
3872
|
+
if (!key) {
|
|
3873
|
+
throw new Error(
|
|
3874
|
+
"BRAIN_API_KEY env var is required \u2014 set it on the Fly machine before boot."
|
|
3875
|
+
);
|
|
3876
|
+
}
|
|
3877
|
+
return key;
|
|
3944
3878
|
}
|
|
3945
|
-
|
|
3946
|
-
const
|
|
3947
|
-
if (
|
|
3948
|
-
|
|
3949
|
-
|
|
3879
|
+
function authOk(req, expected) {
|
|
3880
|
+
const xApiKey = req.headers["x-api-key"]?.trim();
|
|
3881
|
+
if (xApiKey && xApiKey === expected) return true;
|
|
3882
|
+
const auth = req.headers["authorization"]?.trim();
|
|
3883
|
+
if (auth && auth.toLowerCase().startsWith("bearer ")) {
|
|
3884
|
+
return auth.slice(7).trim() === expected;
|
|
3950
3885
|
}
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
if (
|
|
3960
|
-
|
|
3961
|
-
|
|
3886
|
+
return false;
|
|
3887
|
+
}
|
|
3888
|
+
function readJsonBody(req) {
|
|
3889
|
+
return new Promise((resolve4, reject) => {
|
|
3890
|
+
const chunks = [];
|
|
3891
|
+
req.on("data", (c) => chunks.push(c));
|
|
3892
|
+
req.on("end", () => {
|
|
3893
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
3894
|
+
if (!raw.trim()) {
|
|
3895
|
+
resolve4({});
|
|
3896
|
+
return;
|
|
3897
|
+
}
|
|
3898
|
+
try {
|
|
3899
|
+
resolve4(JSON.parse(raw));
|
|
3900
|
+
} catch (err) {
|
|
3901
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
3902
|
+
}
|
|
3903
|
+
});
|
|
3904
|
+
req.on("error", reject);
|
|
3905
|
+
});
|
|
3906
|
+
}
|
|
3907
|
+
function sendJson(res, status, body) {
|
|
3908
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
3909
|
+
res.end(JSON.stringify(body));
|
|
3910
|
+
}
|
|
3911
|
+
function writeSseHeaders(res) {
|
|
3912
|
+
res.writeHead(200, {
|
|
3913
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
3914
|
+
"cache-control": "no-cache, no-transform",
|
|
3915
|
+
connection: "keep-alive",
|
|
3916
|
+
"x-accel-buffering": "no"
|
|
3917
|
+
});
|
|
3918
|
+
}
|
|
3919
|
+
function emitSse(res, event) {
|
|
3920
|
+
res.write(`data: ${JSON.stringify(event)}
|
|
3921
|
+
|
|
3962
3922
|
`);
|
|
3963
|
-
|
|
3964
|
-
|
|
3923
|
+
}
|
|
3924
|
+
function translateChatEvent(event, chatId) {
|
|
3925
|
+
switch (event.event) {
|
|
3926
|
+
case "chat.message": {
|
|
3927
|
+
const content = String(event.payload.content ?? "");
|
|
3928
|
+
if (content.length === 0) return null;
|
|
3929
|
+
return { type: "text", text: content, chatId };
|
|
3965
3930
|
}
|
|
3931
|
+
case "chat.tool": {
|
|
3932
|
+
if (event.payload.phase !== "use") return null;
|
|
3933
|
+
return {
|
|
3934
|
+
type: "tool_use",
|
|
3935
|
+
name: typeof event.payload.name === "string" ? event.payload.name : "tool",
|
|
3936
|
+
input: event.payload.input ?? {},
|
|
3937
|
+
chatId
|
|
3938
|
+
};
|
|
3939
|
+
}
|
|
3940
|
+
case "chat.done":
|
|
3941
|
+
return { type: "done", chatId };
|
|
3942
|
+
case "chat.error":
|
|
3943
|
+
return {
|
|
3944
|
+
type: "error",
|
|
3945
|
+
error: typeof event.payload.error === "string" ? event.payload.error : "agent error",
|
|
3946
|
+
chatId
|
|
3947
|
+
};
|
|
3948
|
+
default:
|
|
3949
|
+
return null;
|
|
3966
3950
|
}
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
const markerMissing = ctx.data.agentMarkerMissing === true;
|
|
3973
|
-
if (ctx.data.agentDone === false && !markerMissing) {
|
|
3974
|
-
ctx.data.commitResult = { committed: false, pushed: false, skippedReason: "agentDone=false" };
|
|
3975
|
-
ctx.data.hasCommitsAhead = hasCommitsAhead(branch, ctx.config.git.defaultBranch, ctx.cwd);
|
|
3976
|
-
return;
|
|
3951
|
+
}
|
|
3952
|
+
var BrokerSink = class {
|
|
3953
|
+
constructor(emitToLog, chatId) {
|
|
3954
|
+
this.emitToLog = emitToLog;
|
|
3955
|
+
this.chatId = chatId;
|
|
3977
3956
|
}
|
|
3978
|
-
|
|
3979
|
-
|
|
3957
|
+
emitToLog;
|
|
3958
|
+
chatId;
|
|
3959
|
+
async emit(event) {
|
|
3960
|
+
const be = translateChatEvent(event, this.chatId);
|
|
3961
|
+
if (be) this.emitToLog(be);
|
|
3980
3962
|
}
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
if (
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3963
|
+
};
|
|
3964
|
+
var chatQueues = /* @__PURE__ */ new Map();
|
|
3965
|
+
function enqueue(chatId, fn) {
|
|
3966
|
+
const prev = chatQueues.get(chatId) ?? Promise.resolve();
|
|
3967
|
+
const next = prev.catch(() => {
|
|
3968
|
+
}).then(fn);
|
|
3969
|
+
chatQueues.set(
|
|
3970
|
+
chatId,
|
|
3971
|
+
next.finally(() => {
|
|
3972
|
+
if (chatQueues.get(chatId) === next) chatQueues.delete(chatId);
|
|
3973
|
+
})
|
|
3974
|
+
);
|
|
3975
|
+
return next;
|
|
3976
|
+
}
|
|
3977
|
+
function streamToRes(res, dir, chatId, since) {
|
|
3978
|
+
writeSseHeaders(res);
|
|
3979
|
+
emitSse(res, { type: "chat", chatId });
|
|
3980
|
+
let maxSent = since;
|
|
3981
|
+
const unsubscribe = subscribe(
|
|
3982
|
+
dir,
|
|
3983
|
+
chatId,
|
|
3984
|
+
since,
|
|
3985
|
+
(rec) => {
|
|
3986
|
+
if (rec.seq <= maxSent) return;
|
|
3987
|
+
maxSent = rec.seq;
|
|
3988
|
+
if (res.writableEnded) return;
|
|
3989
|
+
res.write(`data: ${JSON.stringify({ ...rec.event, seq: rec.seq })}
|
|
3990
|
+
|
|
3995
3991
|
`);
|
|
3992
|
+
},
|
|
3993
|
+
() => {
|
|
3994
|
+
if (!res.writableEnded) {
|
|
3995
|
+
try {
|
|
3996
|
+
res.end();
|
|
3997
|
+
} catch {
|
|
3998
|
+
}
|
|
3999
|
+
}
|
|
3996
4000
|
}
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4001
|
+
);
|
|
4002
|
+
res.on("close", unsubscribe);
|
|
4003
|
+
}
|
|
4004
|
+
async function handleChatTurn(req, res, chatId, opts) {
|
|
4005
|
+
let body;
|
|
4006
|
+
try {
|
|
4007
|
+
body = await readJsonBody(req);
|
|
4008
|
+
} catch {
|
|
4009
|
+
sendJson(res, 400, { error: "invalid JSON body" });
|
|
4010
|
+
return;
|
|
4003
4011
|
}
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4012
|
+
const message = typeof body === "object" && body !== null && "message" in body ? body.message : void 0;
|
|
4013
|
+
if (typeof message !== "string" || !message.trim()) {
|
|
4014
|
+
sendJson(res, 400, { error: "message required" });
|
|
4015
|
+
return;
|
|
4016
|
+
}
|
|
4017
|
+
const sessionFile = sessionFilePath(opts.cwd, chatId);
|
|
4018
|
+
fs14.mkdirSync(path12.dirname(sessionFile), { recursive: true });
|
|
4019
|
+
appendTurn(sessionFile, {
|
|
4020
|
+
role: "user",
|
|
4021
|
+
content: message,
|
|
4022
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4023
|
+
});
|
|
4024
|
+
const sinceFloor = getLastSeq(opts.cwd, chatId);
|
|
4025
|
+
const emitToLog = beginTurn(opts.cwd, chatId);
|
|
4026
|
+
const sink = new BrokerSink(emitToLog, chatId);
|
|
4027
|
+
void enqueue(
|
|
4028
|
+
chatId,
|
|
4029
|
+
() => opts.runTurn({
|
|
4030
|
+
sessionId: chatId,
|
|
4031
|
+
sessionFile,
|
|
4032
|
+
cwd: opts.cwd,
|
|
4033
|
+
model: opts.model,
|
|
4034
|
+
litellmUrl: opts.litellmUrl,
|
|
4035
|
+
sink
|
|
4036
|
+
}).catch((err) => {
|
|
4037
|
+
const errMsg2 = err instanceof Error ? err.message : String(err);
|
|
4038
|
+
process.stderr.write(`[brain-serve] chat turn failed: ${errMsg2}
|
|
4039
|
+
`);
|
|
4040
|
+
endTurnIfUnterminated(opts.cwd, chatId, errMsg2);
|
|
4041
|
+
}).finally(() => {
|
|
4042
|
+
endTurnIfUnterminated(
|
|
4043
|
+
opts.cwd,
|
|
4044
|
+
chatId,
|
|
4045
|
+
"Brain turn ended without a reply (the machine may have restarted mid-turn) \u2014 please resend your message"
|
|
4022
4046
|
);
|
|
4023
|
-
}
|
|
4047
|
+
})
|
|
4048
|
+
);
|
|
4049
|
+
streamToRes(res, opts.cwd, chatId, sinceFloor);
|
|
4050
|
+
}
|
|
4051
|
+
function buildServer(opts) {
|
|
4052
|
+
const runTurn = opts.runTurn ?? runChatTurn;
|
|
4053
|
+
return createServer(async (req, res) => {
|
|
4054
|
+
if (!req.method || !req.url) {
|
|
4055
|
+
sendJson(res, 400, { error: "bad request" });
|
|
4056
|
+
return;
|
|
4024
4057
|
}
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4058
|
+
const url = new URL(req.url, `http://localhost`);
|
|
4059
|
+
if (req.method === "GET" && url.pathname === "/healthz") {
|
|
4060
|
+
sendJson(res, 200, { ok: true });
|
|
4061
|
+
return;
|
|
4062
|
+
}
|
|
4063
|
+
if (!authOk(req, opts.apiKey)) {
|
|
4064
|
+
sendJson(res, 401, { error: "unauthorized" });
|
|
4065
|
+
return;
|
|
4066
|
+
}
|
|
4067
|
+
const m = url.pathname.match(/^\/chats\/([^/]+)\/messages\/?$/);
|
|
4068
|
+
if (req.method === "POST" && m) {
|
|
4069
|
+
const chatId = decodeURIComponent(m[1] ?? "");
|
|
4070
|
+
if (!chatId) {
|
|
4071
|
+
sendJson(res, 400, { error: "chatId required" });
|
|
4072
|
+
return;
|
|
4073
|
+
}
|
|
4074
|
+
await handleChatTurn(req, res, chatId, {
|
|
4075
|
+
cwd: opts.cwd,
|
|
4076
|
+
model: opts.model,
|
|
4077
|
+
litellmUrl: opts.litellmUrl,
|
|
4078
|
+
runTurn
|
|
4079
|
+
});
|
|
4080
|
+
return;
|
|
4081
|
+
}
|
|
4082
|
+
const sm = url.pathname.match(/^\/chats\/([^/]+)\/stream\/?$/);
|
|
4083
|
+
if (req.method === "GET" && sm) {
|
|
4084
|
+
const chatId = decodeURIComponent(sm[1] ?? "");
|
|
4085
|
+
if (!chatId) {
|
|
4086
|
+
sendJson(res, 400, { error: "chatId required" });
|
|
4087
|
+
return;
|
|
4088
|
+
}
|
|
4089
|
+
const sinceRaw = url.searchParams.get("since");
|
|
4090
|
+
const since = Number.isFinite(Number(sinceRaw)) ? Number(sinceRaw) : 0;
|
|
4091
|
+
streamToRes(res, opts.cwd, chatId, since);
|
|
4092
|
+
return;
|
|
4093
|
+
}
|
|
4094
|
+
sendJson(res, 404, { error: "not found" });
|
|
4095
|
+
});
|
|
4096
|
+
}
|
|
4097
|
+
var brainServe = async (ctx) => {
|
|
4098
|
+
ctx.skipAgent = true;
|
|
4099
|
+
const unpacked = unpackAllSecrets();
|
|
4100
|
+
if (unpacked > 0) {
|
|
4101
|
+
process.stdout.write(
|
|
4102
|
+
`[brain-serve] unpacked ${unpacked} secret(s) from ALL_SECRETS
|
|
4040
4103
|
`
|
|
4041
4104
|
);
|
|
4042
|
-
return;
|
|
4043
|
-
}
|
|
4044
|
-
try {
|
|
4045
|
-
execFileSync9("git", ["diff", "--cached", "--quiet"], { cwd: ctx.cwd, stdio: "pipe" });
|
|
4046
|
-
return;
|
|
4047
|
-
} catch {
|
|
4048
4105
|
}
|
|
4049
|
-
const
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4106
|
+
const apiKey = getApiKey();
|
|
4107
|
+
const port = Number(process.env.PORT ?? DEFAULT_PORT);
|
|
4108
|
+
const model = parseProviderModel(ctx.config.agent.model);
|
|
4109
|
+
const usesProxy = needsLitellmProxy(model);
|
|
4110
|
+
let handle = null;
|
|
4111
|
+
if (usesProxy) {
|
|
4112
|
+
process.stdout.write(
|
|
4113
|
+
`[brain-serve] starting LiteLLM proxy for ${model.provider}/${model.model}...
|
|
4114
|
+
`
|
|
4115
|
+
);
|
|
4116
|
+
handle = await startLitellmIfNeeded(model, ctx.cwd);
|
|
4117
|
+
process.stdout.write(
|
|
4118
|
+
`[brain-serve] LiteLLM ready at ${handle?.url ?? LITELLM_DEFAULT_URL}
|
|
4055
4119
|
`
|
|
4056
4120
|
);
|
|
4057
|
-
return;
|
|
4058
|
-
}
|
|
4059
|
-
try {
|
|
4060
|
-
execFileSync9("git", ["push", "--quiet"], { cwd: ctx.cwd, stdio: "pipe" });
|
|
4061
|
-
} catch {
|
|
4062
|
-
process.stderr.write("[goal-tick] commitGoalState: push failed (will retry next tick)\n");
|
|
4063
4121
|
}
|
|
4122
|
+
const litellmUrl = usesProxy ? handle?.url ?? LITELLM_DEFAULT_URL : null;
|
|
4123
|
+
const server = buildServer({
|
|
4124
|
+
apiKey,
|
|
4125
|
+
cwd: ctx.cwd,
|
|
4126
|
+
model,
|
|
4127
|
+
litellmUrl
|
|
4128
|
+
});
|
|
4129
|
+
await new Promise((resolve4) => {
|
|
4130
|
+
server.listen(port, "0.0.0.0", () => {
|
|
4131
|
+
process.stdout.write(
|
|
4132
|
+
`[brain-serve] listening on 0.0.0.0:${port} (cwd=${ctx.cwd})
|
|
4133
|
+
`
|
|
4134
|
+
);
|
|
4135
|
+
resolve4();
|
|
4136
|
+
});
|
|
4137
|
+
});
|
|
4138
|
+
const shutdown = (signal) => {
|
|
4139
|
+
process.stdout.write(`[brain-serve] ${signal} \u2014 shutting down
|
|
4140
|
+
`);
|
|
4141
|
+
server.close(() => {
|
|
4142
|
+
if (handle) {
|
|
4143
|
+
try {
|
|
4144
|
+
handle.kill();
|
|
4145
|
+
} catch {
|
|
4146
|
+
}
|
|
4147
|
+
}
|
|
4148
|
+
process.exit(0);
|
|
4149
|
+
});
|
|
4150
|
+
};
|
|
4151
|
+
process.once("SIGINT", () => shutdown("SIGINT"));
|
|
4152
|
+
process.once("SIGTERM", () => shutdown("SIGTERM"));
|
|
4153
|
+
await new Promise(() => {
|
|
4154
|
+
});
|
|
4064
4155
|
};
|
|
4065
|
-
function describeCommitMessage(goal) {
|
|
4066
|
-
if (goal.state === "closed") return `chore(goals): abandon ${goal.id} (cleanup complete)`;
|
|
4067
|
-
if (goal.state === "done") return `chore(goals): mark ${goal.id} done`;
|
|
4068
|
-
if (goal.lastDispatchedIssue !== void 0) {
|
|
4069
|
-
return `chore(goals): dispatched #${goal.lastDispatchedIssue} for ${goal.id}`;
|
|
4070
|
-
}
|
|
4071
|
-
if (goal.phase === "in-flight") {
|
|
4072
|
-
return `chore(goals): tick ${goal.id} (waiting for in-flight task)`;
|
|
4073
|
-
}
|
|
4074
|
-
return `chore(goals): tick ${goal.id} (idle)`;
|
|
4075
|
-
}
|
|
4076
4156
|
|
|
4077
|
-
// src/scripts/
|
|
4078
|
-
import * as
|
|
4079
|
-
import * as
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
const
|
|
4083
|
-
const mode = ctx.args.mode;
|
|
4157
|
+
// src/scripts/buildSyntheticPlugin.ts
|
|
4158
|
+
import * as fs15 from "fs";
|
|
4159
|
+
import * as os3 from "os";
|
|
4160
|
+
import * as path13 from "path";
|
|
4161
|
+
function getPluginsCatalogRoot() {
|
|
4162
|
+
const here = path13.dirname(new URL(import.meta.url).pathname);
|
|
4084
4163
|
const candidates = [
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4164
|
+
path13.join(here, "..", "plugins"),
|
|
4165
|
+
// dev: src/scripts → src/plugins
|
|
4166
|
+
path13.join(here, "..", "..", "plugins"),
|
|
4167
|
+
// built: dist/scripts → dist/plugins
|
|
4168
|
+
path13.join(here, "..", "..", "src", "plugins")
|
|
4169
|
+
// fallback
|
|
4170
|
+
];
|
|
4090
4171
|
for (const c of candidates) {
|
|
4091
|
-
if (
|
|
4092
|
-
|
|
4093
|
-
|
|
4172
|
+
if (fs15.existsSync(c) && fs15.statSync(c).isDirectory()) return c;
|
|
4173
|
+
}
|
|
4174
|
+
return candidates[0];
|
|
4175
|
+
}
|
|
4176
|
+
var buildSyntheticPlugin = async (ctx, profile) => {
|
|
4177
|
+
const cc = profile.claudeCode;
|
|
4178
|
+
const needsSynthetic = cc.skills.length > 0 || cc.commands.length > 0 || cc.hooks.length > 0 || cc.subagents.length > 0;
|
|
4179
|
+
if (!needsSynthetic) return;
|
|
4180
|
+
const catalog = getPluginsCatalogRoot();
|
|
4181
|
+
const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
4182
|
+
const root = path13.join(os3.tmpdir(), `kody-synth-${runId}`);
|
|
4183
|
+
fs15.mkdirSync(path13.join(root, ".claude-plugin"), { recursive: true });
|
|
4184
|
+
const resolvePart = (bucket, entry) => {
|
|
4185
|
+
const local = path13.join(profile.dir, bucket, entry);
|
|
4186
|
+
if (fs15.existsSync(local)) return local;
|
|
4187
|
+
const central = path13.join(catalog, bucket, entry);
|
|
4188
|
+
if (fs15.existsSync(central)) return central;
|
|
4189
|
+
throw new Error(
|
|
4190
|
+
`buildSyntheticPlugin: ${bucket} entry '${entry}' not found in executable dir (${profile.dir}/${bucket}/) or catalog (${catalog}/${bucket}/)`
|
|
4191
|
+
);
|
|
4192
|
+
};
|
|
4193
|
+
if (cc.skills.length > 0) {
|
|
4194
|
+
const dst = path13.join(root, "skills");
|
|
4195
|
+
fs15.mkdirSync(dst, { recursive: true });
|
|
4196
|
+
for (const name of cc.skills) {
|
|
4197
|
+
copyDir(resolvePart("skills", name), path13.join(dst, name));
|
|
4094
4198
|
}
|
|
4095
4199
|
}
|
|
4096
|
-
if (
|
|
4097
|
-
|
|
4200
|
+
if (cc.commands.length > 0) {
|
|
4201
|
+
const dst = path13.join(root, "commands");
|
|
4202
|
+
fs15.mkdirSync(dst, { recursive: true });
|
|
4203
|
+
for (const name of cc.commands) {
|
|
4204
|
+
fs15.copyFileSync(resolvePart("commands", `${name}.md`), path13.join(dst, `${name}.md`));
|
|
4205
|
+
}
|
|
4098
4206
|
}
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
coverageBlock: formatCoverageBlock(
|
|
4105
|
-
ctx.data.coverageRules
|
|
4106
|
-
),
|
|
4107
|
-
toolsUsage: formatToolsUsage(profile),
|
|
4108
|
-
systemPromptAppend: profile.claudeCode.systemPromptAppend ?? "",
|
|
4109
|
-
repoOwner: ctx.config.github.owner,
|
|
4110
|
-
repoName: ctx.config.github.repo,
|
|
4111
|
-
defaultBranch: ctx.config.git.defaultBranch,
|
|
4112
|
-
branch: ctx.data.branch ?? ""
|
|
4113
|
-
};
|
|
4114
|
-
ctx.data.prompt = template.replace(MUSTACHE, (_, key) => tokens[key] ?? "");
|
|
4115
|
-
};
|
|
4116
|
-
function stringifyAll(source, prefix) {
|
|
4117
|
-
const out = {};
|
|
4118
|
-
for (const [k, v] of Object.entries(source)) {
|
|
4119
|
-
const key = prefix + k;
|
|
4120
|
-
if (v === null || v === void 0) continue;
|
|
4121
|
-
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
4122
|
-
out[key] = String(v);
|
|
4123
|
-
} else if (Array.isArray(v)) {
|
|
4124
|
-
out[key] = v.map((x) => typeof x === "string" ? x : JSON.stringify(x)).join("\n");
|
|
4125
|
-
} else if (typeof v === "object") {
|
|
4126
|
-
for (const [k2, v2] of Object.entries(v)) {
|
|
4127
|
-
if (typeof v2 === "string" || typeof v2 === "number" || typeof v2 === "boolean") {
|
|
4128
|
-
out[`${key}.${k2}`] = String(v2);
|
|
4129
|
-
}
|
|
4130
|
-
}
|
|
4207
|
+
if (cc.subagents.length > 0) {
|
|
4208
|
+
const dst = path13.join(root, "agents");
|
|
4209
|
+
fs15.mkdirSync(dst, { recursive: true });
|
|
4210
|
+
for (const name of cc.subagents) {
|
|
4211
|
+
fs15.copyFileSync(resolvePart("agents", `${name}.md`), path13.join(dst, `${name}.md`));
|
|
4131
4212
|
}
|
|
4132
4213
|
}
|
|
4133
|
-
|
|
4214
|
+
if (cc.hooks.length > 0) {
|
|
4215
|
+
const dst = path13.join(root, "hooks");
|
|
4216
|
+
fs15.mkdirSync(dst, { recursive: true });
|
|
4217
|
+
const merged = { hooks: {} };
|
|
4218
|
+
for (const name of cc.hooks) {
|
|
4219
|
+
const src = resolvePart("hooks", `${name}.json`);
|
|
4220
|
+
const parsed = JSON.parse(fs15.readFileSync(src, "utf-8"));
|
|
4221
|
+
for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
|
|
4222
|
+
if (!Array.isArray(entries)) continue;
|
|
4223
|
+
if (!merged.hooks[event]) merged.hooks[event] = [];
|
|
4224
|
+
merged.hooks[event].push(...entries);
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
fs15.writeFileSync(path13.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
|
|
4228
|
+
`);
|
|
4229
|
+
}
|
|
4230
|
+
const manifest = {
|
|
4231
|
+
name: `kody-synth-${profile.name}`,
|
|
4232
|
+
version: "1.0.0",
|
|
4233
|
+
description: `Synthetic plugin assembled by Kody for profile '${profile.name}' at runtime.`
|
|
4234
|
+
};
|
|
4235
|
+
if (cc.skills.length > 0) manifest.skills = ["./skills/"];
|
|
4236
|
+
if (cc.commands.length > 0) manifest.commands = ["./commands/"];
|
|
4237
|
+
if (cc.subagents.length > 0) manifest.agents = cc.subagents.map((n) => `./agents/${n}.md`);
|
|
4238
|
+
fs15.writeFileSync(path13.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
|
|
4239
|
+
`);
|
|
4240
|
+
ctx.data.syntheticPluginPath = root;
|
|
4241
|
+
};
|
|
4242
|
+
function copyDir(src, dst) {
|
|
4243
|
+
fs15.mkdirSync(dst, { recursive: true });
|
|
4244
|
+
for (const ent of fs15.readdirSync(src, { withFileTypes: true })) {
|
|
4245
|
+
const s = path13.join(src, ent.name);
|
|
4246
|
+
const d = path13.join(dst, ent.name);
|
|
4247
|
+
if (ent.isDirectory()) copyDir(s, d);
|
|
4248
|
+
else if (ent.isFile()) fs15.copyFileSync(s, d);
|
|
4249
|
+
}
|
|
4134
4250
|
}
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4251
|
+
|
|
4252
|
+
// src/coverage.ts
|
|
4253
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
4254
|
+
function patternToRegex(pattern) {
|
|
4255
|
+
let s = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
4256
|
+
s = s.replace(/\*\*\//g, "\xA7S").replace(/\*\*/g, "\xA7A").replace(/\*/g, "[^/]*");
|
|
4257
|
+
s = s.replace(/§S/g, "(?:.*/)?").replace(/§A/g, ".*");
|
|
4258
|
+
return new RegExp(`^${s}$`);
|
|
4259
|
+
}
|
|
4260
|
+
function renderSiblingPath(file, requireSibling) {
|
|
4261
|
+
const lastSlash = file.lastIndexOf("/");
|
|
4262
|
+
const dir = lastSlash === -1 ? "" : file.slice(0, lastSlash + 1);
|
|
4263
|
+
const base = lastSlash === -1 ? file : file.slice(lastSlash + 1);
|
|
4264
|
+
const name = base.replace(/\.[^.]+$/, "");
|
|
4265
|
+
const ext = base.match(/\.[^.]+$/)?.[0] ?? "";
|
|
4266
|
+
const sibling = requireSibling.replace(/\{name\}/g, name).replace(/\{ext\}/g, ext);
|
|
4267
|
+
return dir + sibling;
|
|
4268
|
+
}
|
|
4269
|
+
function safeGit(args, cwd) {
|
|
4270
|
+
try {
|
|
4271
|
+
return execFileSync8("git", args, { encoding: "utf-8", cwd, env: { ...process.env, HUSKY: "0" } }).trim();
|
|
4272
|
+
} catch {
|
|
4273
|
+
return "";
|
|
4145
4274
|
}
|
|
4146
|
-
return lines.join("\n");
|
|
4147
4275
|
}
|
|
4148
|
-
function
|
|
4149
|
-
|
|
4150
|
-
const
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
];
|
|
4156
|
-
for (const r of reqs) lines.push(`- new \`${r.pattern}\` \u2192 must include sibling \`${r.requireSibling}\``);
|
|
4157
|
-
lines.push("");
|
|
4158
|
-
return lines.join("\n");
|
|
4276
|
+
function getAddedFiles(baseBranch, cwd) {
|
|
4277
|
+
const committed = safeGit(["diff", "--name-only", "--diff-filter=A", `origin/${baseBranch}...HEAD`], cwd);
|
|
4278
|
+
const untracked = safeGit(["ls-files", "--others", "--exclude-standard"], cwd);
|
|
4279
|
+
const set = /* @__PURE__ */ new Set();
|
|
4280
|
+
for (const f of committed.split("\n")) if (f) set.add(f);
|
|
4281
|
+
for (const f of untracked.split("\n")) if (f) set.add(f);
|
|
4282
|
+
return [...set];
|
|
4159
4283
|
}
|
|
4160
|
-
function
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
const
|
|
4164
|
-
for (const
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4284
|
+
function checkCoverage(addedFiles, requirements) {
|
|
4285
|
+
if (requirements.length === 0) return [];
|
|
4286
|
+
const addedSet = new Set(addedFiles);
|
|
4287
|
+
const misses = [];
|
|
4288
|
+
for (const file of addedFiles) {
|
|
4289
|
+
if (/\.(test|spec)\./.test(file)) continue;
|
|
4290
|
+
for (const req of requirements) {
|
|
4291
|
+
const re = patternToRegex(req.pattern);
|
|
4292
|
+
if (!re.test(file)) continue;
|
|
4293
|
+
const expected = renderSiblingPath(file, req.requireSibling);
|
|
4294
|
+
if (!addedSet.has(expected)) {
|
|
4295
|
+
misses.push({ file, expectedTest: expected });
|
|
4296
|
+
}
|
|
4297
|
+
break;
|
|
4169
4298
|
}
|
|
4170
|
-
lines.push("");
|
|
4171
4299
|
}
|
|
4300
|
+
return misses;
|
|
4301
|
+
}
|
|
4302
|
+
function formatMissesForFeedback(misses) {
|
|
4303
|
+
if (misses.length === 0) return "";
|
|
4304
|
+
const lines = ["The following files were added without a sibling test file:"];
|
|
4305
|
+
for (const m of misses) lines.push(`- \`${m.file}\` \u2192 expected \`${m.expectedTest}\``);
|
|
4306
|
+
lines.push("");
|
|
4307
|
+
lines.push(
|
|
4308
|
+
"Add the missing test files. Each should cover the new file's public API with at least a happy path and one failure path. Then re-emit DONE / COMMIT_MSG / PR_SUMMARY."
|
|
4309
|
+
);
|
|
4172
4310
|
return lines.join("\n");
|
|
4173
4311
|
}
|
|
4174
4312
|
|
|
4175
|
-
// src/scripts/
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4313
|
+
// src/scripts/checkCoverageWithRetry.ts
|
|
4314
|
+
init_prompt();
|
|
4315
|
+
var checkCoverageWithRetry = async (ctx) => {
|
|
4316
|
+
const reqs = ctx.data.coverageRules ?? [];
|
|
4317
|
+
if (reqs.length === 0) {
|
|
4318
|
+
ctx.data.coverageMisses = [];
|
|
4319
|
+
return;
|
|
4320
|
+
}
|
|
4321
|
+
if (!ctx.data.agentDone) {
|
|
4322
|
+
ctx.data.coverageMisses = [];
|
|
4323
|
+
return;
|
|
4324
|
+
}
|
|
4325
|
+
const misses = checkCoverage(getAddedFiles(ctx.config.git.defaultBranch, ctx.cwd), reqs);
|
|
4326
|
+
if (misses.length === 0) {
|
|
4327
|
+
ctx.data.coverageMisses = [];
|
|
4328
|
+
return;
|
|
4329
|
+
}
|
|
4330
|
+
const invoker = ctx.data.__invokeAgent;
|
|
4331
|
+
const basePrompt = ctx.data.prompt;
|
|
4332
|
+
if (!invoker || !basePrompt) {
|
|
4333
|
+
ctx.data.coverageMisses = misses;
|
|
4334
|
+
return;
|
|
4335
|
+
}
|
|
4336
|
+
process.stderr.write(`[kody] coverage check found ${misses.length} missing test(s); retrying agent once
|
|
4337
|
+
`);
|
|
4338
|
+
const retryPrompt = `${basePrompt}
|
|
4180
4339
|
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
const
|
|
4185
|
-
if (
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4340
|
+
# Coverage failure (retry)
|
|
4341
|
+
${formatMissesForFeedback(misses)}`;
|
|
4342
|
+
const retry = await invoker(retryPrompt);
|
|
4343
|
+
const retryParsed = parseAgentResult(retry.finalText);
|
|
4344
|
+
if (retry.outcome === "completed" && retryParsed.done) {
|
|
4345
|
+
ctx.data.agentDone = true;
|
|
4346
|
+
ctx.data.commitMessage = retryParsed.commitMessage || ctx.data.commitMessage;
|
|
4347
|
+
ctx.data.prSummary = retryParsed.prSummary || ctx.data.prSummary;
|
|
4348
|
+
}
|
|
4349
|
+
const finalMisses = checkCoverage(getAddedFiles(ctx.config.git.defaultBranch, ctx.cwd), reqs);
|
|
4350
|
+
ctx.data.coverageMisses = finalMisses;
|
|
4351
|
+
};
|
|
4352
|
+
|
|
4353
|
+
// src/scripts/classifyByLabel.ts
|
|
4354
|
+
var VALID_CLASSES = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
|
|
4355
|
+
var classifyByLabel = async (ctx) => {
|
|
4356
|
+
const issue = ctx.data.issue;
|
|
4357
|
+
const labels = issue?.labels;
|
|
4358
|
+
if (!labels || labels.length === 0) return;
|
|
4359
|
+
const cfgMap = ctx.config.classify?.labelMap;
|
|
4360
|
+
const map = cfgMap ?? defaultLabelMap();
|
|
4361
|
+
for (const label of labels) {
|
|
4362
|
+
const candidate = map[label.toLowerCase()];
|
|
4363
|
+
if (candidate && VALID_CLASSES.has(candidate)) {
|
|
4364
|
+
ctx.data.classification = candidate;
|
|
4365
|
+
ctx.data.classificationSource = "label";
|
|
4366
|
+
ctx.data.classificationReason = `label \`${label}\` \u2192 ${candidate}`;
|
|
4367
|
+
ctx.skipAgent = true;
|
|
4368
|
+
return;
|
|
4369
|
+
}
|
|
4370
|
+
}
|
|
4371
|
+
};
|
|
4372
|
+
function defaultLabelMap() {
|
|
4373
|
+
return {
|
|
4374
|
+
bug: "bug",
|
|
4375
|
+
enhancement: "bug",
|
|
4376
|
+
refactor: "feature",
|
|
4377
|
+
feature: "feature",
|
|
4378
|
+
performance: "feature",
|
|
4379
|
+
rfc: "spec",
|
|
4380
|
+
design: "spec",
|
|
4381
|
+
spec: "spec",
|
|
4382
|
+
docs: "chore",
|
|
4383
|
+
chore: "chore",
|
|
4384
|
+
dependencies: "chore"
|
|
4385
|
+
};
|
|
4191
4386
|
}
|
|
4192
|
-
|
|
4193
|
-
|
|
4387
|
+
|
|
4388
|
+
// src/scripts/commitAndPush.ts
|
|
4389
|
+
import * as fs17 from "fs";
|
|
4390
|
+
import * as path15 from "path";
|
|
4391
|
+
init_events();
|
|
4392
|
+
var DEFAULT_COMMIT_MESSAGE = "chore: kody changes";
|
|
4393
|
+
function sentinelPathForStage(cwd, profileName) {
|
|
4394
|
+
const runId = resolveRunId();
|
|
4395
|
+
return path15.join(cwd, ".kody", "runs", runId, `commit-${profileName}.lock`);
|
|
4194
4396
|
}
|
|
4195
|
-
var
|
|
4196
|
-
const
|
|
4197
|
-
if (!
|
|
4198
|
-
ctx.
|
|
4199
|
-
ctx.output.reason = "review postflight: no PR number in context";
|
|
4200
|
-
ctx.data.action = failedAction(ctx.output.reason);
|
|
4397
|
+
var commitAndPush2 = async (ctx, profile) => {
|
|
4398
|
+
const branch = ctx.data.branch;
|
|
4399
|
+
if (!branch) {
|
|
4400
|
+
ctx.data.commitResult = { committed: false, pushed: false };
|
|
4201
4401
|
return;
|
|
4202
4402
|
}
|
|
4203
|
-
|
|
4204
|
-
|
|
4403
|
+
const idempotencyEnabled = process.env.KODY_COMMIT_IDEMPOTENCY !== "0";
|
|
4404
|
+
const sentinel = idempotencyEnabled ? sentinelPathForStage(ctx.cwd, profile.name) : null;
|
|
4405
|
+
if (sentinel && fs17.existsSync(sentinel)) {
|
|
4205
4406
|
try {
|
|
4206
|
-
|
|
4407
|
+
const replay = JSON.parse(fs17.readFileSync(sentinel, "utf-8"));
|
|
4408
|
+
ctx.data.commitResult = replay.commitResult ?? { committed: false, pushed: false };
|
|
4409
|
+
if (Array.isArray(replay.changedFiles)) ctx.data.changedFiles = replay.changedFiles;
|
|
4410
|
+
if (typeof replay.hasCommitsAhead === "boolean") ctx.data.hasCommitsAhead = replay.hasCommitsAhead;
|
|
4411
|
+
if (replay.salvagedFromMissingMarker) ctx.data.salvagedFromMissingMarker = true;
|
|
4412
|
+
ctx.data.commitIdempotencyReplay = true;
|
|
4413
|
+
process.stderr.write(`[kody commitAndPush] idempotency replay (sentinel ${sentinel})
|
|
4414
|
+
`);
|
|
4415
|
+
return;
|
|
4207
4416
|
} catch {
|
|
4208
4417
|
}
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
ctx.data.
|
|
4418
|
+
}
|
|
4419
|
+
if (ctx.data.verifyOk === false) {
|
|
4420
|
+
ctx.data.commitResult = { committed: false, pushed: false, skippedReason: "verifyFailed" };
|
|
4421
|
+
ctx.data.hasCommitsAhead = hasCommitsAhead(branch, ctx.config.git.defaultBranch, ctx.cwd);
|
|
4212
4422
|
return;
|
|
4213
4423
|
}
|
|
4214
|
-
const
|
|
4215
|
-
if (!
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
} catch {
|
|
4219
|
-
}
|
|
4220
|
-
ctx.output.exitCode = 1;
|
|
4221
|
-
ctx.output.reason = "empty review body";
|
|
4222
|
-
ctx.data.action = failedAction("empty review body");
|
|
4424
|
+
const markerMissing = ctx.data.agentMarkerMissing === true;
|
|
4425
|
+
if (ctx.data.agentDone === false && !markerMissing) {
|
|
4426
|
+
ctx.data.commitResult = { committed: false, pushed: false, skippedReason: "agentDone=false" };
|
|
4427
|
+
ctx.data.hasCommitsAhead = hasCommitsAhead(branch, ctx.config.git.defaultBranch, ctx.cwd);
|
|
4223
4428
|
return;
|
|
4224
4429
|
}
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
}
|
|
4430
|
+
if (ctx.data.agentDone === false && markerMissing) {
|
|
4431
|
+
ctx.data.salvagedFromMissingMarker = true;
|
|
4432
|
+
}
|
|
4433
|
+
const message = ctx.data.commitMessage || DEFAULT_COMMIT_MESSAGE;
|
|
4434
|
+
try {
|
|
4435
|
+
const result2 = commitAndPush(branch, message, ctx.cwd);
|
|
4436
|
+
ctx.data.commitResult = result2;
|
|
4437
|
+
const postCommitFiles = result2.committed ? listFilesInCommit("HEAD", ctx.cwd) : listChangedFiles(ctx.cwd);
|
|
4438
|
+
ctx.data.changedFiles = postCommitFiles.filter((f) => !isForbiddenPath(f));
|
|
4439
|
+
if (result2.committed && !result2.pushed) {
|
|
4440
|
+
const reason = result2.pushError ?? "push failed (no error detail)";
|
|
4441
|
+
ctx.data.commitCrash = reason;
|
|
4442
|
+
if (ctx.output.exitCode === void 0 || ctx.output.exitCode === 0) {
|
|
4443
|
+
ctx.output.exitCode = 4;
|
|
4444
|
+
}
|
|
4445
|
+
if (!ctx.output.reason) ctx.output.reason = reason;
|
|
4446
|
+
process.stderr.write(`[kody commitAndPush] ${reason}
|
|
4447
|
+
`);
|
|
4448
|
+
}
|
|
4449
|
+
} catch (err) {
|
|
4450
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
4451
|
+
ctx.data.commitCrash = reason;
|
|
4452
|
+
ctx.data.commitResult = { committed: false, pushed: false };
|
|
4453
|
+
process.stderr.write(`[kody commitAndPush] failed: ${reason}
|
|
4454
|
+
`);
|
|
4455
|
+
}
|
|
4456
|
+
ctx.data.hasCommitsAhead = hasCommitsAhead(branch, ctx.config.git.defaultBranch, ctx.cwd);
|
|
4457
|
+
const result = ctx.data.commitResult;
|
|
4458
|
+
if (sentinel && result?.committed) {
|
|
4459
|
+
try {
|
|
4460
|
+
fs17.mkdirSync(path15.dirname(sentinel), { recursive: true });
|
|
4461
|
+
fs17.writeFileSync(
|
|
4462
|
+
sentinel,
|
|
4463
|
+
JSON.stringify(
|
|
4464
|
+
{
|
|
4465
|
+
commitResult: ctx.data.commitResult,
|
|
4466
|
+
changedFiles: ctx.data.changedFiles,
|
|
4467
|
+
hasCommitsAhead: ctx.data.hasCommitsAhead,
|
|
4468
|
+
salvagedFromMissingMarker: ctx.data.salvagedFromMissingMarker === true,
|
|
4469
|
+
writtenAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4470
|
+
},
|
|
4471
|
+
null,
|
|
4472
|
+
2
|
|
4473
|
+
)
|
|
4474
|
+
);
|
|
4475
|
+
} catch {
|
|
4476
|
+
}
|
|
4477
|
+
}
|
|
4478
|
+
};
|
|
4479
|
+
|
|
4480
|
+
// src/scripts/commitGoalState.ts
|
|
4481
|
+
import { execFileSync as execFileSync9 } from "child_process";
|
|
4482
|
+
import * as path16 from "path";
|
|
4483
|
+
var commitGoalState = async (ctx) => {
|
|
4484
|
+
const goal = ctx.data.goal;
|
|
4485
|
+
if (!goal) return;
|
|
4486
|
+
const stateRel = path16.posix.join(".kody", "goals", goal.id, "state.json");
|
|
4487
|
+
try {
|
|
4488
|
+
execFileSync9("git", ["add", stateRel], { cwd: ctx.cwd, stdio: "pipe" });
|
|
4489
|
+
} catch (err) {
|
|
4490
|
+
process.stderr.write(
|
|
4491
|
+
`[goal-tick] commitGoalState: git add failed: ${err instanceof Error ? err.message : String(err)}
|
|
4492
|
+
`
|
|
4493
|
+
);
|
|
4494
|
+
return;
|
|
4495
|
+
}
|
|
4496
|
+
try {
|
|
4497
|
+
execFileSync9("git", ["diff", "--cached", "--quiet"], { cwd: ctx.cwd, stdio: "pipe" });
|
|
4498
|
+
return;
|
|
4499
|
+
} catch {
|
|
4500
|
+
}
|
|
4501
|
+
const msg = describeCommitMessage(goal);
|
|
4502
|
+
try {
|
|
4503
|
+
execFileSync9("git", ["commit", "-m", msg, "--quiet"], { cwd: ctx.cwd, stdio: "pipe" });
|
|
4504
|
+
} catch (err) {
|
|
4505
|
+
process.stderr.write(
|
|
4506
|
+
`[goal-tick] commitGoalState: git commit failed: ${err instanceof Error ? err.message : String(err)}
|
|
4507
|
+
`
|
|
4508
|
+
);
|
|
4509
|
+
return;
|
|
4510
|
+
}
|
|
4511
|
+
try {
|
|
4512
|
+
execFileSync9("git", ["push", "--quiet"], { cwd: ctx.cwd, stdio: "pipe" });
|
|
4513
|
+
} catch {
|
|
4514
|
+
process.stderr.write("[goal-tick] commitGoalState: push failed (will retry next tick)\n");
|
|
4515
|
+
}
|
|
4516
|
+
};
|
|
4517
|
+
function describeCommitMessage(goal) {
|
|
4518
|
+
if (goal.state === "closed") return `chore(goals): abandon ${goal.id} (cleanup complete)`;
|
|
4519
|
+
if (goal.state === "awaiting-merge") return `chore(goals): park ${goal.id} awaiting merge`;
|
|
4520
|
+
if (goal.state === "done") return `chore(goals): mark ${goal.id} done`;
|
|
4521
|
+
if (goal.lastDispatchedIssue !== void 0) {
|
|
4522
|
+
return `chore(goals): dispatched #${goal.lastDispatchedIssue} for ${goal.id}`;
|
|
4523
|
+
}
|
|
4524
|
+
if (goal.phase === "in-flight") {
|
|
4525
|
+
return `chore(goals): tick ${goal.id} (waiting for in-flight task)`;
|
|
4526
|
+
}
|
|
4527
|
+
return `chore(goals): tick ${goal.id} (idle)`;
|
|
4528
|
+
}
|
|
4529
|
+
|
|
4530
|
+
// src/scripts/composePrompt.ts
|
|
4531
|
+
import * as fs18 from "fs";
|
|
4532
|
+
import * as path17 from "path";
|
|
4533
|
+
var MUSTACHE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
|
|
4534
|
+
var composePrompt = async (ctx, profile) => {
|
|
4535
|
+
const explicit = ctx.data.promptTemplate;
|
|
4536
|
+
const mode = ctx.args.mode;
|
|
4537
|
+
const candidates = [
|
|
4538
|
+
explicit ? path17.join(profile.dir, explicit) : null,
|
|
4539
|
+
mode ? path17.join(profile.dir, "prompts", `${mode}.md`) : null,
|
|
4540
|
+
path17.join(profile.dir, "prompt.md")
|
|
4541
|
+
].filter(Boolean);
|
|
4542
|
+
let templatePath = "";
|
|
4543
|
+
for (const c of candidates) {
|
|
4544
|
+
if (fs18.existsSync(c)) {
|
|
4545
|
+
templatePath = c;
|
|
4546
|
+
break;
|
|
4547
|
+
}
|
|
4548
|
+
}
|
|
4549
|
+
if (!templatePath) {
|
|
4550
|
+
throw new Error(`profile at ${profile.dir}: no prompt template found (tried ${candidates.join(", ")})`);
|
|
4551
|
+
}
|
|
4552
|
+
const template = fs18.readFileSync(templatePath, "utf-8");
|
|
4553
|
+
const tokens = {
|
|
4554
|
+
...stringifyAll(ctx.args, "args."),
|
|
4555
|
+
...stringifyAll(ctx.data, ""),
|
|
4556
|
+
conventionsBlock: formatConventions(ctx.data.conventions),
|
|
4557
|
+
coverageBlock: formatCoverageBlock(
|
|
4558
|
+
ctx.data.coverageRules
|
|
4559
|
+
),
|
|
4560
|
+
toolsUsage: formatToolsUsage(profile),
|
|
4561
|
+
systemPromptAppend: profile.claudeCode.systemPromptAppend ?? "",
|
|
4562
|
+
repoOwner: ctx.config.github.owner,
|
|
4563
|
+
repoName: ctx.config.github.repo,
|
|
4564
|
+
defaultBranch: ctx.config.git.defaultBranch,
|
|
4565
|
+
branch: ctx.data.branch ?? ""
|
|
4566
|
+
};
|
|
4567
|
+
ctx.data.prompt = template.replace(MUSTACHE, (_, key) => tokens[key] ?? "");
|
|
4568
|
+
};
|
|
4569
|
+
function stringifyAll(source, prefix) {
|
|
4570
|
+
const out = {};
|
|
4571
|
+
for (const [k, v] of Object.entries(source)) {
|
|
4572
|
+
const key = prefix + k;
|
|
4573
|
+
if (v === null || v === void 0) continue;
|
|
4574
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
4575
|
+
out[key] = String(v);
|
|
4576
|
+
} else if (Array.isArray(v)) {
|
|
4577
|
+
out[key] = v.map((x) => typeof x === "string" ? x : JSON.stringify(x)).join("\n");
|
|
4578
|
+
} else if (typeof v === "object") {
|
|
4579
|
+
for (const [k2, v2] of Object.entries(v)) {
|
|
4580
|
+
if (typeof v2 === "string" || typeof v2 === "number" || typeof v2 === "boolean") {
|
|
4581
|
+
out[`${key}.${k2}`] = String(v2);
|
|
4582
|
+
}
|
|
4583
|
+
}
|
|
4584
|
+
}
|
|
4585
|
+
}
|
|
4586
|
+
return out;
|
|
4587
|
+
}
|
|
4588
|
+
function formatConventions(conventions) {
|
|
4589
|
+
if (!conventions || conventions.length === 0) return "";
|
|
4590
|
+
const lines = ["# Project conventions (AUTHORITATIVE \u2014 follow these over patterns you infer from code)", ""];
|
|
4591
|
+
for (const c of conventions) {
|
|
4592
|
+
lines.push(`## ${c.path}${c.truncated ? " (truncated)" : ""}`);
|
|
4593
|
+
lines.push("");
|
|
4594
|
+
lines.push("```");
|
|
4595
|
+
lines.push(c.content);
|
|
4596
|
+
lines.push("```");
|
|
4597
|
+
lines.push("");
|
|
4598
|
+
}
|
|
4599
|
+
return lines.join("\n");
|
|
4600
|
+
}
|
|
4601
|
+
function formatCoverageBlock(reqs) {
|
|
4602
|
+
if (!reqs || reqs.length === 0) return "";
|
|
4603
|
+
const lines = [
|
|
4604
|
+
"# Test coverage requirements (ENFORCED)",
|
|
4605
|
+
"",
|
|
4606
|
+
"Every newly added file matching one of these patterns MUST be accompanied by a sibling test file in the same commit. The wrapper checks this after you finish; if any sibling test is missing, the run will fail and the issue will be re-invoked with the gap as feedback.",
|
|
4607
|
+
""
|
|
4608
|
+
];
|
|
4609
|
+
for (const r of reqs) lines.push(`- new \`${r.pattern}\` \u2192 must include sibling \`${r.requireSibling}\``);
|
|
4610
|
+
lines.push("");
|
|
4611
|
+
return lines.join("\n");
|
|
4612
|
+
}
|
|
4613
|
+
function formatToolsUsage(profile) {
|
|
4614
|
+
const entries = (profile.cliTools ?? []).filter((t) => t.usage.trim().length > 0);
|
|
4615
|
+
if (entries.length === 0) return "";
|
|
4616
|
+
const lines = ["# Available CLI tools", ""];
|
|
4617
|
+
for (const t of entries) {
|
|
4618
|
+
lines.push(`## \`${t.name}\``);
|
|
4619
|
+
lines.push(t.usage);
|
|
4620
|
+
if (t.allowedUses.length > 0) {
|
|
4621
|
+
lines.push(`Allowed sub-commands: ${t.allowedUses.map((u) => `\`${u}\``).join(", ")}`);
|
|
4622
|
+
}
|
|
4623
|
+
lines.push("");
|
|
4624
|
+
}
|
|
4625
|
+
return lines.join("\n");
|
|
4626
|
+
}
|
|
4627
|
+
|
|
4628
|
+
// src/scripts/createQaGoal.ts
|
|
4629
|
+
init_issue();
|
|
4630
|
+
import { execFileSync as execFileSync10 } from "child_process";
|
|
4631
|
+
import * as fs19 from "fs";
|
|
4632
|
+
import * as path18 from "path";
|
|
4633
|
+
|
|
4634
|
+
// src/scripts/postReviewResult.ts
|
|
4635
|
+
init_issue();
|
|
4636
|
+
function detectVerdict(body) {
|
|
4637
|
+
const m = body.match(/##\s*Verdict\s*:\s*(PASS|CONCERNS|FAIL)\b/i);
|
|
4638
|
+
if (!m) return "UNKNOWN";
|
|
4639
|
+
return m[1].toUpperCase();
|
|
4640
|
+
}
|
|
4641
|
+
function reviewAction(verdict, payload) {
|
|
4642
|
+
const type = verdict === "PASS" ? "REVIEW_PASS" : verdict === "CONCERNS" ? "REVIEW_CONCERNS" : verdict === "FAIL" ? "REVIEW_FAIL" : "REVIEW_COMPLETED";
|
|
4643
|
+
return { type, payload: { verdict, ...payload }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
4644
|
+
}
|
|
4645
|
+
function failedAction(reason) {
|
|
4646
|
+
return { type: "REVIEW_FAILED", payload: { reason }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
4647
|
+
}
|
|
4648
|
+
var postReviewResult = async (ctx, _profile, agentResult) => {
|
|
4649
|
+
const prNumber = ctx.data.commentTargetNumber;
|
|
4650
|
+
if (!prNumber) {
|
|
4651
|
+
ctx.output.exitCode = 99;
|
|
4652
|
+
ctx.output.reason = "review postflight: no PR number in context";
|
|
4653
|
+
ctx.data.action = failedAction(ctx.output.reason);
|
|
4654
|
+
return;
|
|
4655
|
+
}
|
|
4656
|
+
if (!agentResult || agentResult.outcome !== "completed") {
|
|
4657
|
+
const reason = agentResult?.error ?? "agent did not complete";
|
|
4658
|
+
try {
|
|
4659
|
+
postPrReviewComment(prNumber, `\u26A0\uFE0F kody review FAILED: ${truncate2(reason, 1e3)}`, ctx.cwd);
|
|
4660
|
+
} catch {
|
|
4661
|
+
}
|
|
4662
|
+
ctx.output.exitCode = 1;
|
|
4663
|
+
ctx.output.reason = reason;
|
|
4664
|
+
ctx.data.action = failedAction(reason);
|
|
4665
|
+
return;
|
|
4666
|
+
}
|
|
4667
|
+
const reviewBody = agentResult.finalText.trim();
|
|
4668
|
+
if (!reviewBody) {
|
|
4669
|
+
try {
|
|
4670
|
+
postPrReviewComment(prNumber, `\u26A0\uFE0F kody review FAILED: agent produced no review body`, ctx.cwd);
|
|
4671
|
+
} catch {
|
|
4672
|
+
}
|
|
4673
|
+
ctx.output.exitCode = 1;
|
|
4674
|
+
ctx.output.reason = "empty review body";
|
|
4675
|
+
ctx.data.action = failedAction("empty review body");
|
|
4676
|
+
return;
|
|
4677
|
+
}
|
|
4678
|
+
try {
|
|
4679
|
+
postPrReviewComment(prNumber, reviewBody, ctx.cwd);
|
|
4680
|
+
} catch (err) {
|
|
4228
4681
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4229
4682
|
ctx.output.exitCode = 4;
|
|
4230
4683
|
ctx.output.reason = `failed to post review comment: ${msg}`;
|
|
@@ -4429,8 +4882,8 @@ function createOrUpdateManifestIssue(number, manifest, cwd) {
|
|
|
4429
4882
|
return { number: Number(m[1]), created: true };
|
|
4430
4883
|
}
|
|
4431
4884
|
function writeStateFile(cwd, goalId, lastDispatchedIssue) {
|
|
4432
|
-
const dir =
|
|
4433
|
-
|
|
4885
|
+
const dir = path18.join(cwd, ".kody", "goals", goalId);
|
|
4886
|
+
fs19.mkdirSync(dir, { recursive: true });
|
|
4434
4887
|
const state = {
|
|
4435
4888
|
version: 1,
|
|
4436
4889
|
state: "active",
|
|
@@ -4438,8 +4891,8 @@ function writeStateFile(cwd, goalId, lastDispatchedIssue) {
|
|
|
4438
4891
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4439
4892
|
...typeof lastDispatchedIssue === "number" ? { lastDispatchedIssue } : {}
|
|
4440
4893
|
};
|
|
4441
|
-
const filePath =
|
|
4442
|
-
|
|
4894
|
+
const filePath = path18.join(dir, "state.json");
|
|
4895
|
+
fs19.writeFileSync(filePath, `${JSON.stringify(state, null, 2)}
|
|
4443
4896
|
`);
|
|
4444
4897
|
return filePath;
|
|
4445
4898
|
}
|
|
@@ -4870,6 +5323,7 @@ function derivePhase(snap) {
|
|
|
4870
5323
|
if (!snap.lifecycleState) return "missing";
|
|
4871
5324
|
if (snap.lifecycleState === "abandoned") return "abandoned";
|
|
4872
5325
|
if (snap.lifecycleState === "closed" || snap.lifecycleState === "done") return "terminal";
|
|
5326
|
+
if (snap.lifecycleState === "awaiting-merge") return "awaiting-merge";
|
|
4873
5327
|
const hasInFlight = snap.childTasks.some((t) => t.state === "OPEN" && t.prState === "draft");
|
|
4874
5328
|
if (hasInFlight) return "in-flight";
|
|
4875
5329
|
if (snap.childTasks.length === 0) return "idle";
|
|
@@ -4935,15 +5389,15 @@ function filterGoalTaskPrs(prs, taskIssueNumbers) {
|
|
|
4935
5389
|
|
|
4936
5390
|
// src/scripts/diagMcp.ts
|
|
4937
5391
|
import { execFileSync as execFileSync11 } from "child_process";
|
|
4938
|
-
import * as
|
|
5392
|
+
import * as fs20 from "fs";
|
|
4939
5393
|
import * as os4 from "os";
|
|
4940
|
-
import * as
|
|
5394
|
+
import * as path19 from "path";
|
|
4941
5395
|
var diagMcp = async (_ctx) => {
|
|
4942
5396
|
const home = os4.homedir();
|
|
4943
|
-
const cacheDir =
|
|
5397
|
+
const cacheDir = path19.join(home, ".cache", "ms-playwright");
|
|
4944
5398
|
let entries = [];
|
|
4945
5399
|
try {
|
|
4946
|
-
entries =
|
|
5400
|
+
entries = fs20.readdirSync(cacheDir);
|
|
4947
5401
|
} catch {
|
|
4948
5402
|
}
|
|
4949
5403
|
const hasChromium = entries.some((e) => e.startsWith("chromium"));
|
|
@@ -4969,17 +5423,17 @@ var diagMcp = async (_ctx) => {
|
|
|
4969
5423
|
};
|
|
4970
5424
|
|
|
4971
5425
|
// src/scripts/discoverQaContext.ts
|
|
4972
|
-
import * as
|
|
4973
|
-
import * as
|
|
5426
|
+
import * as fs22 from "fs";
|
|
5427
|
+
import * as path21 from "path";
|
|
4974
5428
|
|
|
4975
5429
|
// src/scripts/frameworkDetectors.ts
|
|
4976
|
-
import * as
|
|
4977
|
-
import * as
|
|
5430
|
+
import * as fs21 from "fs";
|
|
5431
|
+
import * as path20 from "path";
|
|
4978
5432
|
function detectFrameworks(cwd) {
|
|
4979
5433
|
const out = [];
|
|
4980
5434
|
let deps = {};
|
|
4981
5435
|
try {
|
|
4982
|
-
const pkg = JSON.parse(
|
|
5436
|
+
const pkg = JSON.parse(fs21.readFileSync(path20.join(cwd, "package.json"), "utf-8"));
|
|
4983
5437
|
deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
4984
5438
|
} catch {
|
|
4985
5439
|
return out;
|
|
@@ -5016,7 +5470,7 @@ function detectFrameworks(cwd) {
|
|
|
5016
5470
|
}
|
|
5017
5471
|
function findFile(cwd, candidates) {
|
|
5018
5472
|
for (const c of candidates) {
|
|
5019
|
-
if (
|
|
5473
|
+
if (fs21.existsSync(path20.join(cwd, c))) return c;
|
|
5020
5474
|
}
|
|
5021
5475
|
return null;
|
|
5022
5476
|
}
|
|
@@ -5029,18 +5483,18 @@ var COLLECTION_DIRS = [
|
|
|
5029
5483
|
function discoverPayloadCollections(cwd) {
|
|
5030
5484
|
const out = [];
|
|
5031
5485
|
for (const dir of COLLECTION_DIRS) {
|
|
5032
|
-
const full =
|
|
5033
|
-
if (!
|
|
5486
|
+
const full = path20.join(cwd, dir);
|
|
5487
|
+
if (!fs21.existsSync(full)) continue;
|
|
5034
5488
|
let files;
|
|
5035
5489
|
try {
|
|
5036
|
-
files =
|
|
5490
|
+
files = fs21.readdirSync(full).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
|
|
5037
5491
|
} catch {
|
|
5038
5492
|
continue;
|
|
5039
5493
|
}
|
|
5040
5494
|
for (const file of files) {
|
|
5041
5495
|
try {
|
|
5042
|
-
const filePath =
|
|
5043
|
-
const content =
|
|
5496
|
+
const filePath = path20.join(full, file);
|
|
5497
|
+
const content = fs21.readFileSync(filePath, "utf-8").slice(0, 1e4);
|
|
5044
5498
|
const slugMatch = content.match(/slug:\s*['"]([a-z0-9-]+)['"]/);
|
|
5045
5499
|
if (!slugMatch) continue;
|
|
5046
5500
|
const slug = slugMatch[1];
|
|
@@ -5054,7 +5508,7 @@ function discoverPayloadCollections(cwd) {
|
|
|
5054
5508
|
out.push({
|
|
5055
5509
|
name,
|
|
5056
5510
|
slug,
|
|
5057
|
-
filePath:
|
|
5511
|
+
filePath: path20.relative(cwd, filePath),
|
|
5058
5512
|
fields: fields.slice(0, 20),
|
|
5059
5513
|
hasAdmin
|
|
5060
5514
|
});
|
|
@@ -5068,28 +5522,28 @@ var ADMIN_COMPONENT_DIRS = ["src/ui/admin", "src/admin/components", "src/compone
|
|
|
5068
5522
|
function discoverAdminComponents(cwd, collections) {
|
|
5069
5523
|
const out = [];
|
|
5070
5524
|
for (const dir of ADMIN_COMPONENT_DIRS) {
|
|
5071
|
-
const full =
|
|
5072
|
-
if (!
|
|
5525
|
+
const full = path20.join(cwd, dir);
|
|
5526
|
+
if (!fs21.existsSync(full)) continue;
|
|
5073
5527
|
let entries;
|
|
5074
5528
|
try {
|
|
5075
|
-
entries =
|
|
5529
|
+
entries = fs21.readdirSync(full, { withFileTypes: true });
|
|
5076
5530
|
} catch {
|
|
5077
5531
|
continue;
|
|
5078
5532
|
}
|
|
5079
5533
|
for (const entry of entries) {
|
|
5080
|
-
const entryPath =
|
|
5534
|
+
const entryPath = path20.join(full, entry.name);
|
|
5081
5535
|
let name;
|
|
5082
5536
|
let filePath;
|
|
5083
5537
|
if (entry.isDirectory()) {
|
|
5084
5538
|
const indexFile = ["index.tsx", "index.ts", "index.jsx", "index.js"].find(
|
|
5085
|
-
(f) =>
|
|
5539
|
+
(f) => fs21.existsSync(path20.join(entryPath, f))
|
|
5086
5540
|
);
|
|
5087
5541
|
if (!indexFile) continue;
|
|
5088
5542
|
name = entry.name;
|
|
5089
|
-
filePath =
|
|
5543
|
+
filePath = path20.relative(cwd, path20.join(entryPath, indexFile));
|
|
5090
5544
|
} else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
|
|
5091
5545
|
name = entry.name.replace(/\.(tsx?|jsx?)$/, "");
|
|
5092
|
-
filePath =
|
|
5546
|
+
filePath = path20.relative(cwd, entryPath);
|
|
5093
5547
|
} else {
|
|
5094
5548
|
continue;
|
|
5095
5549
|
}
|
|
@@ -5097,7 +5551,7 @@ function discoverAdminComponents(cwd, collections) {
|
|
|
5097
5551
|
if (collections) {
|
|
5098
5552
|
for (const col of collections) {
|
|
5099
5553
|
try {
|
|
5100
|
-
const colContent =
|
|
5554
|
+
const colContent = fs21.readFileSync(path20.join(cwd, col.filePath), "utf-8");
|
|
5101
5555
|
if (colContent.includes(name)) {
|
|
5102
5556
|
usedInCollection = col.slug;
|
|
5103
5557
|
break;
|
|
@@ -5116,8 +5570,8 @@ function scanApiRoutes(cwd) {
|
|
|
5116
5570
|
const out = [];
|
|
5117
5571
|
const appDirs = ["src/app", "app"];
|
|
5118
5572
|
for (const appDir of appDirs) {
|
|
5119
|
-
const apiDir =
|
|
5120
|
-
if (!
|
|
5573
|
+
const apiDir = path20.join(cwd, appDir, "api");
|
|
5574
|
+
if (!fs21.existsSync(apiDir)) continue;
|
|
5121
5575
|
walkApiRoutes(apiDir, "/api", cwd, out);
|
|
5122
5576
|
break;
|
|
5123
5577
|
}
|
|
@@ -5126,14 +5580,14 @@ function scanApiRoutes(cwd) {
|
|
|
5126
5580
|
function walkApiRoutes(dir, prefix, cwd, out) {
|
|
5127
5581
|
let entries;
|
|
5128
5582
|
try {
|
|
5129
|
-
entries =
|
|
5583
|
+
entries = fs21.readdirSync(dir, { withFileTypes: true });
|
|
5130
5584
|
} catch {
|
|
5131
5585
|
return;
|
|
5132
5586
|
}
|
|
5133
5587
|
const routeFile = entries.find((e) => e.isFile() && /^route\.(ts|js|tsx|jsx)$/.test(e.name));
|
|
5134
5588
|
if (routeFile) {
|
|
5135
5589
|
try {
|
|
5136
|
-
const content =
|
|
5590
|
+
const content = fs21.readFileSync(path20.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
|
|
5137
5591
|
const methods = HTTP_METHODS.filter(
|
|
5138
5592
|
(m) => new RegExp(`export\\s+(?:async\\s+)?function\\s+${m}\\b`).test(content)
|
|
5139
5593
|
);
|
|
@@ -5141,7 +5595,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
|
|
|
5141
5595
|
out.push({
|
|
5142
5596
|
path: prefix,
|
|
5143
5597
|
methods,
|
|
5144
|
-
filePath:
|
|
5598
|
+
filePath: path20.relative(cwd, path20.join(dir, routeFile.name))
|
|
5145
5599
|
});
|
|
5146
5600
|
}
|
|
5147
5601
|
} catch {
|
|
@@ -5152,7 +5606,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
|
|
|
5152
5606
|
if (entry.name === "node_modules" || entry.name === ".next") continue;
|
|
5153
5607
|
let segment = entry.name;
|
|
5154
5608
|
if (segment.startsWith("(") && segment.endsWith(")")) {
|
|
5155
|
-
walkApiRoutes(
|
|
5609
|
+
walkApiRoutes(path20.join(dir, entry.name), prefix, cwd, out);
|
|
5156
5610
|
continue;
|
|
5157
5611
|
}
|
|
5158
5612
|
if (segment.startsWith("[[") && segment.endsWith("]]")) {
|
|
@@ -5160,7 +5614,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
|
|
|
5160
5614
|
} else if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
5161
5615
|
segment = `:${segment.slice(1, -1)}`;
|
|
5162
5616
|
}
|
|
5163
|
-
walkApiRoutes(
|
|
5617
|
+
walkApiRoutes(path20.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
|
|
5164
5618
|
}
|
|
5165
5619
|
}
|
|
5166
5620
|
var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
|
|
@@ -5180,10 +5634,10 @@ var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
|
|
|
5180
5634
|
function scanEnvVars(cwd) {
|
|
5181
5635
|
const candidates = [".env.example", ".env.local.example", ".env.template"];
|
|
5182
5636
|
for (const envFile of candidates) {
|
|
5183
|
-
const envPath =
|
|
5184
|
-
if (!
|
|
5637
|
+
const envPath = path20.join(cwd, envFile);
|
|
5638
|
+
if (!fs21.existsSync(envPath)) continue;
|
|
5185
5639
|
try {
|
|
5186
|
-
const content =
|
|
5640
|
+
const content = fs21.readFileSync(envPath, "utf-8");
|
|
5187
5641
|
const vars = [];
|
|
5188
5642
|
for (const line of content.split("\n")) {
|
|
5189
5643
|
const trimmed = line.trim();
|
|
@@ -5231,9 +5685,9 @@ function runQaDiscovery(cwd) {
|
|
|
5231
5685
|
}
|
|
5232
5686
|
function detectDevServer(cwd, out) {
|
|
5233
5687
|
try {
|
|
5234
|
-
const pkg = JSON.parse(
|
|
5688
|
+
const pkg = JSON.parse(fs22.readFileSync(path21.join(cwd, "package.json"), "utf-8"));
|
|
5235
5689
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
5236
|
-
const pm =
|
|
5690
|
+
const pm = fs22.existsSync(path21.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs22.existsSync(path21.join(cwd, "yarn.lock")) ? "yarn" : fs22.existsSync(path21.join(cwd, "bun.lockb")) ? "bun" : "npm";
|
|
5237
5691
|
if (pkg.scripts?.dev) out.devCommand = `${pm} dev`;
|
|
5238
5692
|
if (allDeps.next || allDeps.nuxt) out.devPort = 3e3;
|
|
5239
5693
|
else if (allDeps.vite) out.devPort = 5173;
|
|
@@ -5243,8 +5697,8 @@ function detectDevServer(cwd, out) {
|
|
|
5243
5697
|
function scanFrontendRoutes(cwd, out) {
|
|
5244
5698
|
const appDirs = ["src/app", "app"];
|
|
5245
5699
|
for (const appDir of appDirs) {
|
|
5246
|
-
const full =
|
|
5247
|
-
if (!
|
|
5700
|
+
const full = path21.join(cwd, appDir);
|
|
5701
|
+
if (!fs22.existsSync(full)) continue;
|
|
5248
5702
|
walkFrontendRoutes(full, "", out);
|
|
5249
5703
|
break;
|
|
5250
5704
|
}
|
|
@@ -5252,7 +5706,7 @@ function scanFrontendRoutes(cwd, out) {
|
|
|
5252
5706
|
function walkFrontendRoutes(dir, prefix, out) {
|
|
5253
5707
|
let entries;
|
|
5254
5708
|
try {
|
|
5255
|
-
entries =
|
|
5709
|
+
entries = fs22.readdirSync(dir, { withFileTypes: true });
|
|
5256
5710
|
} catch {
|
|
5257
5711
|
return;
|
|
5258
5712
|
}
|
|
@@ -5269,7 +5723,7 @@ function walkFrontendRoutes(dir, prefix, out) {
|
|
|
5269
5723
|
if (entry.name === "node_modules" || entry.name === ".next") continue;
|
|
5270
5724
|
let segment = entry.name;
|
|
5271
5725
|
if (segment.startsWith("(") && segment.endsWith(")")) {
|
|
5272
|
-
walkFrontendRoutes(
|
|
5726
|
+
walkFrontendRoutes(path21.join(dir, entry.name), prefix, out);
|
|
5273
5727
|
continue;
|
|
5274
5728
|
}
|
|
5275
5729
|
if (segment.startsWith("[[") && segment.endsWith("]]")) {
|
|
@@ -5277,7 +5731,7 @@ function walkFrontendRoutes(dir, prefix, out) {
|
|
|
5277
5731
|
} else if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
5278
5732
|
segment = `:${segment.slice(1, -1)}`;
|
|
5279
5733
|
}
|
|
5280
|
-
walkFrontendRoutes(
|
|
5734
|
+
walkFrontendRoutes(path21.join(dir, entry.name), `${prefix}/${segment}`, out);
|
|
5281
5735
|
}
|
|
5282
5736
|
}
|
|
5283
5737
|
function detectAuthFiles(cwd, out) {
|
|
@@ -5294,23 +5748,23 @@ function detectAuthFiles(cwd, out) {
|
|
|
5294
5748
|
"src/app/api/oauth"
|
|
5295
5749
|
];
|
|
5296
5750
|
for (const c of candidates) {
|
|
5297
|
-
if (
|
|
5751
|
+
if (fs22.existsSync(path21.join(cwd, c))) out.authFiles.push(c);
|
|
5298
5752
|
}
|
|
5299
5753
|
}
|
|
5300
5754
|
function detectRoles(cwd, out) {
|
|
5301
5755
|
const rolePaths = ["src/types", "src/lib", "src/utils", "src/constants", "src/access", "src/collections"];
|
|
5302
5756
|
for (const rp of rolePaths) {
|
|
5303
|
-
const dir =
|
|
5304
|
-
if (!
|
|
5757
|
+
const dir = path21.join(cwd, rp);
|
|
5758
|
+
if (!fs22.existsSync(dir)) continue;
|
|
5305
5759
|
let files;
|
|
5306
5760
|
try {
|
|
5307
|
-
files =
|
|
5761
|
+
files = fs22.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
|
|
5308
5762
|
} catch {
|
|
5309
5763
|
continue;
|
|
5310
5764
|
}
|
|
5311
5765
|
for (const f of files) {
|
|
5312
5766
|
try {
|
|
5313
|
-
const content =
|
|
5767
|
+
const content = fs22.readFileSync(path21.join(dir, f), "utf-8").slice(0, 5e3);
|
|
5314
5768
|
const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
|
|
5315
5769
|
if (roleMatches) {
|
|
5316
5770
|
for (const m of roleMatches) {
|
|
@@ -5543,8 +5997,8 @@ function failedAction3(reason) {
|
|
|
5543
5997
|
}
|
|
5544
5998
|
|
|
5545
5999
|
// src/scripts/dispatchJobFileTicks.ts
|
|
5546
|
-
import * as
|
|
5547
|
-
import * as
|
|
6000
|
+
import * as fs24 from "fs";
|
|
6001
|
+
import * as path23 from "path";
|
|
5548
6002
|
|
|
5549
6003
|
// src/scripts/jobFrontmatter.ts
|
|
5550
6004
|
var SCHEDULE_EVERY_VALUES = [
|
|
@@ -5803,8 +6257,8 @@ var ContentsApiBackend = class {
|
|
|
5803
6257
|
};
|
|
5804
6258
|
|
|
5805
6259
|
// src/scripts/jobState/localFileBackend.ts
|
|
5806
|
-
import * as
|
|
5807
|
-
import * as
|
|
6260
|
+
import * as fs23 from "fs";
|
|
6261
|
+
import * as path22 from "path";
|
|
5808
6262
|
var LocalFileBackend = class {
|
|
5809
6263
|
name = "local-file";
|
|
5810
6264
|
cwd;
|
|
@@ -5819,7 +6273,7 @@ var LocalFileBackend = class {
|
|
|
5819
6273
|
if (!opts.owner || !opts.repo) throw new Error("LocalFileBackend: owner and repo are required");
|
|
5820
6274
|
this.cwd = opts.cwd;
|
|
5821
6275
|
this.jobsDir = opts.jobsDir;
|
|
5822
|
-
this.absDir =
|
|
6276
|
+
this.absDir = path22.join(opts.cwd, opts.jobsDir);
|
|
5823
6277
|
this.owner = opts.owner;
|
|
5824
6278
|
this.repo = opts.repo;
|
|
5825
6279
|
this.cache = opts.cache ?? defaultCacheAdapter();
|
|
@@ -5834,7 +6288,7 @@ var LocalFileBackend = class {
|
|
|
5834
6288
|
`);
|
|
5835
6289
|
return;
|
|
5836
6290
|
}
|
|
5837
|
-
|
|
6291
|
+
fs23.mkdirSync(this.absDir, { recursive: true });
|
|
5838
6292
|
const prefix = this.cacheKeyPrefix();
|
|
5839
6293
|
const probeKey = `${prefix}probe-${Date.now()}`;
|
|
5840
6294
|
try {
|
|
@@ -5863,7 +6317,7 @@ var LocalFileBackend = class {
|
|
|
5863
6317
|
`);
|
|
5864
6318
|
return;
|
|
5865
6319
|
}
|
|
5866
|
-
if (!
|
|
6320
|
+
if (!fs23.existsSync(this.absDir)) {
|
|
5867
6321
|
return;
|
|
5868
6322
|
}
|
|
5869
6323
|
const key = `${this.cacheKeyPrefix()}${process.env.GITHUB_RUN_ID ?? "norunid"}-${Date.now()}`;
|
|
@@ -5879,11 +6333,11 @@ var LocalFileBackend = class {
|
|
|
5879
6333
|
}
|
|
5880
6334
|
load(slug) {
|
|
5881
6335
|
const relPath = stateFilePath(this.jobsDir, slug);
|
|
5882
|
-
const absPath =
|
|
5883
|
-
if (!
|
|
6336
|
+
const absPath = path22.join(this.cwd, relPath);
|
|
6337
|
+
if (!fs23.existsSync(absPath)) {
|
|
5884
6338
|
return { path: relPath, handle: null, state: initialStateEnvelope("seed"), created: true };
|
|
5885
6339
|
}
|
|
5886
|
-
const raw =
|
|
6340
|
+
const raw = fs23.readFileSync(absPath, "utf-8");
|
|
5887
6341
|
let parsed;
|
|
5888
6342
|
try {
|
|
5889
6343
|
parsed = JSON.parse(raw);
|
|
@@ -5900,10 +6354,10 @@ var LocalFileBackend = class {
|
|
|
5900
6354
|
if (!loaded.created && isStateUnchanged(loaded.state, next)) {
|
|
5901
6355
|
return false;
|
|
5902
6356
|
}
|
|
5903
|
-
const absPath =
|
|
5904
|
-
|
|
6357
|
+
const absPath = path22.join(this.cwd, loaded.path);
|
|
6358
|
+
fs23.mkdirSync(path22.dirname(absPath), { recursive: true });
|
|
5905
6359
|
const body = JSON.stringify(next, null, 2) + "\n";
|
|
5906
|
-
|
|
6360
|
+
fs23.writeFileSync(absPath, body, "utf-8");
|
|
5907
6361
|
return true;
|
|
5908
6362
|
}
|
|
5909
6363
|
cacheKeyPrefix() {
|
|
@@ -5981,7 +6435,7 @@ var dispatchJobFileTicks = async (ctx, _profile, args) => {
|
|
|
5981
6435
|
await backend.hydrate();
|
|
5982
6436
|
}
|
|
5983
6437
|
try {
|
|
5984
|
-
const slugs = listJobSlugs(
|
|
6438
|
+
const slugs = listJobSlugs(path23.join(ctx.cwd, jobsDir));
|
|
5985
6439
|
ctx.data.jobSlugCount = slugs.length;
|
|
5986
6440
|
if (slugs.length === 0) {
|
|
5987
6441
|
process.stdout.write(`[jobs] no job files in ${jobsDir}
|
|
@@ -6086,17 +6540,17 @@ function formatAgo(ms) {
|
|
|
6086
6540
|
}
|
|
6087
6541
|
function readJobFrontmatter(cwd, jobsDir, slug) {
|
|
6088
6542
|
try {
|
|
6089
|
-
const raw =
|
|
6543
|
+
const raw = fs24.readFileSync(path23.join(cwd, jobsDir, `${slug}.md`), "utf-8");
|
|
6090
6544
|
return splitFrontmatter(raw).frontmatter;
|
|
6091
6545
|
} catch {
|
|
6092
6546
|
return {};
|
|
6093
6547
|
}
|
|
6094
6548
|
}
|
|
6095
6549
|
function listJobSlugs(absDir) {
|
|
6096
|
-
if (!
|
|
6550
|
+
if (!fs24.existsSync(absDir)) return [];
|
|
6097
6551
|
let entries;
|
|
6098
6552
|
try {
|
|
6099
|
-
entries =
|
|
6553
|
+
entries = fs24.readdirSync(absDir, { withFileTypes: true });
|
|
6100
6554
|
} catch {
|
|
6101
6555
|
return [];
|
|
6102
6556
|
}
|
|
@@ -6779,7 +7233,7 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch2, cwd, baseBranch
|
|
|
6779
7233
|
|
|
6780
7234
|
// src/gha.ts
|
|
6781
7235
|
import { execFileSync as execFileSync16 } from "child_process";
|
|
6782
|
-
import * as
|
|
7236
|
+
import * as fs25 from "fs";
|
|
6783
7237
|
function getRunUrl() {
|
|
6784
7238
|
const server = process.env.GITHUB_SERVER_URL;
|
|
6785
7239
|
const repo = process.env.GITHUB_REPOSITORY;
|
|
@@ -6790,10 +7244,10 @@ function getRunUrl() {
|
|
|
6790
7244
|
function reactToTriggerComment(cwd) {
|
|
6791
7245
|
if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
|
|
6792
7246
|
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
6793
|
-
if (!eventPath || !
|
|
7247
|
+
if (!eventPath || !fs25.existsSync(eventPath)) return;
|
|
6794
7248
|
let event = null;
|
|
6795
7249
|
try {
|
|
6796
|
-
event = JSON.parse(
|
|
7250
|
+
event = JSON.parse(fs25.readFileSync(eventPath, "utf-8"));
|
|
6797
7251
|
} catch {
|
|
6798
7252
|
return;
|
|
6799
7253
|
}
|
|
@@ -7082,22 +7536,22 @@ var handleAbandonedGoal = async (ctx) => {
|
|
|
7082
7536
|
|
|
7083
7537
|
// src/scripts/initFlow.ts
|
|
7084
7538
|
import { execFileSync as execFileSync18 } from "child_process";
|
|
7085
|
-
import * as
|
|
7086
|
-
import * as
|
|
7539
|
+
import * as fs27 from "fs";
|
|
7540
|
+
import * as path25 from "path";
|
|
7087
7541
|
|
|
7088
7542
|
// src/scripts/loadQaGuide.ts
|
|
7089
|
-
import * as
|
|
7090
|
-
import * as
|
|
7543
|
+
import * as fs26 from "fs";
|
|
7544
|
+
import * as path24 from "path";
|
|
7091
7545
|
var QA_GUIDE_REL_PATH = ".kody/qa-guide.md";
|
|
7092
7546
|
var loadQaGuide = async (ctx) => {
|
|
7093
|
-
const full =
|
|
7094
|
-
if (!
|
|
7547
|
+
const full = path24.join(ctx.cwd, QA_GUIDE_REL_PATH);
|
|
7548
|
+
if (!fs26.existsSync(full)) {
|
|
7095
7549
|
ctx.data.qaGuide = "";
|
|
7096
7550
|
ctx.data.qaGuidePath = "";
|
|
7097
7551
|
return;
|
|
7098
7552
|
}
|
|
7099
7553
|
try {
|
|
7100
|
-
ctx.data.qaGuide =
|
|
7554
|
+
ctx.data.qaGuide = fs26.readFileSync(full, "utf-8");
|
|
7101
7555
|
ctx.data.qaGuidePath = QA_GUIDE_REL_PATH;
|
|
7102
7556
|
} catch {
|
|
7103
7557
|
ctx.data.qaGuide = "";
|
|
@@ -7107,9 +7561,9 @@ var loadQaGuide = async (ctx) => {
|
|
|
7107
7561
|
|
|
7108
7562
|
// src/scripts/initFlow.ts
|
|
7109
7563
|
function detectPackageManager(cwd) {
|
|
7110
|
-
if (
|
|
7111
|
-
if (
|
|
7112
|
-
if (
|
|
7564
|
+
if (fs27.existsSync(path25.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
7565
|
+
if (fs27.existsSync(path25.join(cwd, "yarn.lock"))) return "yarn";
|
|
7566
|
+
if (fs27.existsSync(path25.join(cwd, "bun.lockb"))) return "bun";
|
|
7113
7567
|
return "npm";
|
|
7114
7568
|
}
|
|
7115
7569
|
function qualityCommandsFor(pm) {
|
|
@@ -7231,48 +7685,48 @@ function performInit(cwd, force) {
|
|
|
7231
7685
|
const pm = detectPackageManager(cwd);
|
|
7232
7686
|
const ownerRepo = detectOwnerRepo(cwd);
|
|
7233
7687
|
const defaultBranch2 = defaultBranchFromGit(cwd);
|
|
7234
|
-
const configPath =
|
|
7235
|
-
if (
|
|
7688
|
+
const configPath = path25.join(cwd, "kody.config.json");
|
|
7689
|
+
if (fs27.existsSync(configPath) && !force) {
|
|
7236
7690
|
skipped.push("kody.config.json");
|
|
7237
7691
|
} else {
|
|
7238
7692
|
const cfg = makeConfig(pm, ownerRepo, defaultBranch2);
|
|
7239
|
-
|
|
7693
|
+
fs27.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
|
|
7240
7694
|
`);
|
|
7241
7695
|
wrote.push("kody.config.json");
|
|
7242
7696
|
}
|
|
7243
|
-
const workflowDir =
|
|
7244
|
-
const workflowPath =
|
|
7245
|
-
if (
|
|
7697
|
+
const workflowDir = path25.join(cwd, ".github", "workflows");
|
|
7698
|
+
const workflowPath = path25.join(workflowDir, "kody.yml");
|
|
7699
|
+
if (fs27.existsSync(workflowPath) && !force) {
|
|
7246
7700
|
skipped.push(".github/workflows/kody.yml");
|
|
7247
7701
|
} else {
|
|
7248
|
-
|
|
7249
|
-
|
|
7702
|
+
fs27.mkdirSync(workflowDir, { recursive: true });
|
|
7703
|
+
fs27.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
|
|
7250
7704
|
wrote.push(".github/workflows/kody.yml");
|
|
7251
7705
|
}
|
|
7252
|
-
const hasUi =
|
|
7706
|
+
const hasUi = fs27.existsSync(path25.join(cwd, "src/app")) || fs27.existsSync(path25.join(cwd, "app")) || fs27.existsSync(path25.join(cwd, "pages"));
|
|
7253
7707
|
if (hasUi) {
|
|
7254
|
-
const qaGuidePath =
|
|
7255
|
-
if (
|
|
7708
|
+
const qaGuidePath = path25.join(cwd, QA_GUIDE_REL_PATH);
|
|
7709
|
+
if (fs27.existsSync(qaGuidePath) && !force) {
|
|
7256
7710
|
skipped.push(QA_GUIDE_REL_PATH);
|
|
7257
7711
|
} else {
|
|
7258
|
-
|
|
7712
|
+
fs27.mkdirSync(path25.dirname(qaGuidePath), { recursive: true });
|
|
7259
7713
|
const discovery = runQaDiscovery(cwd);
|
|
7260
|
-
|
|
7714
|
+
fs27.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
|
|
7261
7715
|
wrote.push(QA_GUIDE_REL_PATH);
|
|
7262
7716
|
}
|
|
7263
7717
|
}
|
|
7264
7718
|
const builtinJobs = listBuiltinJobs();
|
|
7265
7719
|
if (builtinJobs.length > 0) {
|
|
7266
|
-
const jobsDir =
|
|
7267
|
-
|
|
7720
|
+
const jobsDir = path25.join(cwd, ".kody", "jobs");
|
|
7721
|
+
fs27.mkdirSync(jobsDir, { recursive: true });
|
|
7268
7722
|
for (const job of builtinJobs) {
|
|
7269
|
-
const rel =
|
|
7270
|
-
const target =
|
|
7271
|
-
if (
|
|
7723
|
+
const rel = path25.join(".kody", "jobs", `${job.slug}.md`);
|
|
7724
|
+
const target = path25.join(cwd, rel);
|
|
7725
|
+
if (fs27.existsSync(target) && !force) {
|
|
7272
7726
|
skipped.push(rel);
|
|
7273
7727
|
continue;
|
|
7274
7728
|
}
|
|
7275
|
-
|
|
7729
|
+
fs27.writeFileSync(target, fs27.readFileSync(job.filePath, "utf-8"));
|
|
7276
7730
|
wrote.push(rel);
|
|
7277
7731
|
}
|
|
7278
7732
|
}
|
|
@@ -7284,12 +7738,12 @@ function performInit(cwd, force) {
|
|
|
7284
7738
|
continue;
|
|
7285
7739
|
}
|
|
7286
7740
|
if (profile.kind !== "scheduled" || !profile.schedule) continue;
|
|
7287
|
-
const target =
|
|
7288
|
-
if (
|
|
7741
|
+
const target = path25.join(workflowDir, `kody-${exe.name}.yml`);
|
|
7742
|
+
if (fs27.existsSync(target) && !force) {
|
|
7289
7743
|
skipped.push(`.github/workflows/kody-${exe.name}.yml`);
|
|
7290
7744
|
continue;
|
|
7291
7745
|
}
|
|
7292
|
-
|
|
7746
|
+
fs27.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
|
|
7293
7747
|
wrote.push(`.github/workflows/kody-${exe.name}.yml`);
|
|
7294
7748
|
}
|
|
7295
7749
|
let labels;
|
|
@@ -7367,9 +7821,9 @@ init_loadConventions();
|
|
|
7367
7821
|
init_loadCoverageRules();
|
|
7368
7822
|
|
|
7369
7823
|
// src/goal/state.ts
|
|
7370
|
-
import * as
|
|
7371
|
-
import * as
|
|
7372
|
-
var VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "done"]);
|
|
7824
|
+
import * as fs28 from "fs";
|
|
7825
|
+
import * as path26 from "path";
|
|
7826
|
+
var VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "awaiting-merge", "done"]);
|
|
7373
7827
|
var GoalStateError = class extends Error {
|
|
7374
7828
|
constructor(path34, message) {
|
|
7375
7829
|
super(`Invalid goal state at ${path34}:
|
|
@@ -7395,6 +7849,9 @@ function parseGoalState(filePath, raw) {
|
|
|
7395
7849
|
state: stateValue,
|
|
7396
7850
|
extra: {}
|
|
7397
7851
|
};
|
|
7852
|
+
if (typeof r.mergeApproved === "boolean") {
|
|
7853
|
+
parsed.mergeApproved = r.mergeApproved;
|
|
7854
|
+
}
|
|
7398
7855
|
if (typeof r.lastDispatchedIssue === "number" && Number.isFinite(r.lastDispatchedIssue)) {
|
|
7399
7856
|
parsed.lastDispatchedIssue = r.lastDispatchedIssue;
|
|
7400
7857
|
}
|
|
@@ -7402,7 +7859,7 @@ function parseGoalState(filePath, raw) {
|
|
|
7402
7859
|
const v = r[ts];
|
|
7403
7860
|
if (typeof v === "string" && v.length > 0) parsed[ts] = v;
|
|
7404
7861
|
}
|
|
7405
|
-
const known = /* @__PURE__ */ new Set(["state", "lastDispatchedIssue", "updatedAt", "createdAt", "startedAt"]);
|
|
7862
|
+
const known = /* @__PURE__ */ new Set(["state", "mergeApproved", "lastDispatchedIssue", "updatedAt", "createdAt", "startedAt"]);
|
|
7406
7863
|
for (const [k, v] of Object.entries(r)) {
|
|
7407
7864
|
if (!known.has(k)) parsed.extra[k] = v;
|
|
7408
7865
|
}
|
|
@@ -7410,6 +7867,7 @@ function parseGoalState(filePath, raw) {
|
|
|
7410
7867
|
}
|
|
7411
7868
|
function serializeGoalState(s) {
|
|
7412
7869
|
const obj = { ...s.extra, state: s.state };
|
|
7870
|
+
if (s.mergeApproved !== void 0) obj.mergeApproved = s.mergeApproved;
|
|
7413
7871
|
if (s.lastDispatchedIssue !== void 0) obj.lastDispatchedIssue = s.lastDispatchedIssue;
|
|
7414
7872
|
if (s.createdAt !== void 0) obj.createdAt = s.createdAt;
|
|
7415
7873
|
if (s.startedAt !== void 0) obj.startedAt = s.startedAt;
|
|
@@ -7418,16 +7876,16 @@ function serializeGoalState(s) {
|
|
|
7418
7876
|
`;
|
|
7419
7877
|
}
|
|
7420
7878
|
function goalStatePath(cwd, goalId) {
|
|
7421
|
-
return
|
|
7879
|
+
return path26.join(cwd, ".kody", "goals", goalId, "state.json");
|
|
7422
7880
|
}
|
|
7423
7881
|
function readGoalState(cwd, goalId) {
|
|
7424
7882
|
const file = goalStatePath(cwd, goalId);
|
|
7425
|
-
if (!
|
|
7883
|
+
if (!fs28.existsSync(file)) {
|
|
7426
7884
|
throw new GoalStateError(file, "file not found");
|
|
7427
7885
|
}
|
|
7428
7886
|
let raw;
|
|
7429
7887
|
try {
|
|
7430
|
-
raw = JSON.parse(
|
|
7888
|
+
raw = JSON.parse(fs28.readFileSync(file, "utf-8"));
|
|
7431
7889
|
} catch (err) {
|
|
7432
7890
|
throw new GoalStateError(file, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
7433
7891
|
}
|
|
@@ -7435,8 +7893,8 @@ function readGoalState(cwd, goalId) {
|
|
|
7435
7893
|
}
|
|
7436
7894
|
function writeGoalState(cwd, goalId, state) {
|
|
7437
7895
|
const file = goalStatePath(cwd, goalId);
|
|
7438
|
-
|
|
7439
|
-
|
|
7896
|
+
fs28.mkdirSync(path26.dirname(file), { recursive: true });
|
|
7897
|
+
fs28.writeFileSync(file, serializeGoalState(state), "utf-8");
|
|
7440
7898
|
}
|
|
7441
7899
|
function nowIso() {
|
|
7442
7900
|
return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
@@ -7538,8 +7996,8 @@ var loadIssueStateComment = async (ctx, _profile, args) => {
|
|
|
7538
7996
|
};
|
|
7539
7997
|
|
|
7540
7998
|
// src/scripts/loadJobFromFile.ts
|
|
7541
|
-
import * as
|
|
7542
|
-
import * as
|
|
7999
|
+
import * as fs29 from "fs";
|
|
8000
|
+
import * as path27 from "path";
|
|
7543
8001
|
var loadJobFromFile = async (ctx, _profile, args) => {
|
|
7544
8002
|
const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
|
|
7545
8003
|
const slugArg = String(args?.slugArg ?? "job");
|
|
@@ -7547,11 +8005,11 @@ var loadJobFromFile = async (ctx, _profile, args) => {
|
|
|
7547
8005
|
if (!slug) {
|
|
7548
8006
|
throw new Error(`loadJobFromFile: ctx.args.${slugArg} must be a non-empty slug`);
|
|
7549
8007
|
}
|
|
7550
|
-
const absPath =
|
|
7551
|
-
if (!
|
|
8008
|
+
const absPath = path27.join(ctx.cwd, jobsDir, `${slug}.md`);
|
|
8009
|
+
if (!fs29.existsSync(absPath)) {
|
|
7552
8010
|
throw new Error(`loadJobFromFile: job file not found: ${absPath}`);
|
|
7553
8011
|
}
|
|
7554
|
-
const raw =
|
|
8012
|
+
const raw = fs29.readFileSync(absPath, "utf-8");
|
|
7555
8013
|
const { title, body } = parseJobFile(raw, slug);
|
|
7556
8014
|
const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, jobsDir });
|
|
7557
8015
|
const loaded = await backend.load(slug);
|
|
@@ -7590,8 +8048,8 @@ init_loadPriorArt();
|
|
|
7590
8048
|
init_events();
|
|
7591
8049
|
|
|
7592
8050
|
// src/taskContext.ts
|
|
7593
|
-
import * as
|
|
7594
|
-
import * as
|
|
8051
|
+
import * as fs31 from "fs";
|
|
8052
|
+
import * as path29 from "path";
|
|
7595
8053
|
var TASK_CONTEXT_SCHEMA_VERSION = 1;
|
|
7596
8054
|
function buildTaskContext(args) {
|
|
7597
8055
|
return {
|
|
@@ -7607,10 +8065,10 @@ function buildTaskContext(args) {
|
|
|
7607
8065
|
}
|
|
7608
8066
|
function persistTaskContext(cwd, ctx) {
|
|
7609
8067
|
try {
|
|
7610
|
-
const dir =
|
|
7611
|
-
|
|
7612
|
-
const file =
|
|
7613
|
-
|
|
8068
|
+
const dir = path29.join(cwd, ".kody", "runs", ctx.runId);
|
|
8069
|
+
fs31.mkdirSync(dir, { recursive: true });
|
|
8070
|
+
const file = path29.join(dir, "task-context.json");
|
|
8071
|
+
fs31.writeFileSync(file, `${JSON.stringify(ctx, null, 2)}
|
|
7614
8072
|
`);
|
|
7615
8073
|
return file;
|
|
7616
8074
|
} catch (err) {
|
|
@@ -7888,6 +8346,23 @@ QA_REPORT_POSTED=${created.url} (verdict: ${verdict})
|
|
|
7888
8346
|
ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
|
|
7889
8347
|
};
|
|
7890
8348
|
|
|
8349
|
+
// src/scripts/parkGoalForMerge.ts
|
|
8350
|
+
var parkGoalForMerge = async (ctx) => {
|
|
8351
|
+
const goal = ctx.data.goal;
|
|
8352
|
+
if (!goal) return;
|
|
8353
|
+
const approved = goal.raw?.mergeApproved === true;
|
|
8354
|
+
if (approved) {
|
|
8355
|
+
process.stdout.write(`[goal-tick] goal ${goal.id}: merge approved \u2014 running finalize (one-shot)
|
|
8356
|
+
`);
|
|
8357
|
+
if (goal.raw) goal.raw.mergeApproved = false;
|
|
8358
|
+
return;
|
|
8359
|
+
}
|
|
8360
|
+
process.stdout.write(`[goal-tick] all task(s) done \u2014 parking goal ${goal.id} for manual merge (no auto-merge)
|
|
8361
|
+
`);
|
|
8362
|
+
goal.state = "awaiting-merge";
|
|
8363
|
+
goal.phase = "awaiting-merge";
|
|
8364
|
+
};
|
|
8365
|
+
|
|
7891
8366
|
// src/scripts/parseAgentResult.ts
|
|
7892
8367
|
init_prompt();
|
|
7893
8368
|
var parseAgentResult2 = async (ctx, profile, agentResult) => {
|
|
@@ -8858,688 +9333,289 @@ var revertFlow = async (ctx) => {
|
|
|
8858
9333
|
function buildCommitMessage(resolved) {
|
|
8859
9334
|
if (resolved.length === 1) {
|
|
8860
9335
|
const { full, subject } = resolved[0];
|
|
8861
|
-
return subject ? `revert: "${subject}" (${full.slice(0, 7)})` : `revert: ${full.slice(0, 7)}`;
|
|
8862
|
-
}
|
|
8863
|
-
const shas = resolved.map((r) => r.full.slice(0, 7)).join(", ");
|
|
8864
|
-
return `revert: ${resolved.length} commit(s) (${shas})`;
|
|
8865
|
-
}
|
|
8866
|
-
function buildPrSummary(resolved) {
|
|
8867
|
-
return resolved.map((r) => `- Reverted \`${r.full.slice(0, 7)}\`${r.subject ? ` \u2014 ${r.subject}` : ""}`).join("\n");
|
|
8868
|
-
}
|
|
8869
|
-
function git3(args, cwd) {
|
|
8870
|
-
return execFileSync23("git", args, {
|
|
8871
|
-
encoding: "utf-8",
|
|
8872
|
-
timeout: 3e4,
|
|
8873
|
-
cwd,
|
|
8874
|
-
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
8875
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
8876
|
-
}).trim();
|
|
8877
|
-
}
|
|
8878
|
-
function isAncestorOfHead(sha, cwd) {
|
|
8879
|
-
try {
|
|
8880
|
-
execFileSync23("git", ["merge-base", "--is-ancestor", sha, "HEAD"], {
|
|
8881
|
-
cwd,
|
|
8882
|
-
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
8883
|
-
stdio: ["ignore", "ignore", "ignore"]
|
|
8884
|
-
});
|
|
8885
|
-
return true;
|
|
8886
|
-
} catch {
|
|
8887
|
-
return false;
|
|
8888
|
-
}
|
|
8889
|
-
}
|
|
8890
|
-
function tryPostPr4(prNumber, body, cwd) {
|
|
8891
|
-
try {
|
|
8892
|
-
postPrReviewComment(prNumber, body, cwd);
|
|
8893
|
-
} catch (err) {
|
|
8894
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
8895
|
-
process.stderr.write(`[kody revertFlow] PR comment on #${prNumber} failed: ${msg}
|
|
8896
|
-
`);
|
|
8897
|
-
}
|
|
8898
|
-
}
|
|
8899
|
-
|
|
8900
|
-
// src/scripts/reviewFlow.ts
|
|
8901
|
-
init_issue();
|
|
8902
|
-
var reviewFlow = async (ctx) => {
|
|
8903
|
-
const prNumber = ctx.args.pr;
|
|
8904
|
-
const pr = getPr(prNumber, ctx.cwd);
|
|
8905
|
-
if (pr.state !== "OPEN") {
|
|
8906
|
-
ctx.output.exitCode = 1;
|
|
8907
|
-
ctx.output.reason = `PR #${prNumber} is not OPEN (state: ${pr.state})`;
|
|
8908
|
-
ctx.skipAgent = true;
|
|
8909
|
-
return;
|
|
8910
|
-
}
|
|
8911
|
-
ctx.data.pr = pr;
|
|
8912
|
-
ctx.data.commentTargetType = "pr";
|
|
8913
|
-
ctx.data.commentTargetNumber = prNumber;
|
|
8914
|
-
checkoutPrBranch(prNumber, ctx.cwd);
|
|
8915
|
-
ctx.data.branch = getCurrentBranch(ctx.cwd);
|
|
8916
|
-
ctx.data.prDiff = getPrDiff(prNumber, ctx.cwd);
|
|
8917
|
-
const runUrl = getRunUrl();
|
|
8918
|
-
const runSuffix = runUrl ? `, run ${runUrl}` : "";
|
|
8919
|
-
tryPostPr5(prNumber, `\u{1F440} kody review started on PR #${prNumber}${runSuffix}`, ctx.cwd);
|
|
8920
|
-
};
|
|
8921
|
-
function tryPostPr5(prNumber, body, cwd) {
|
|
8922
|
-
try {
|
|
8923
|
-
postPrReviewComment(prNumber, body, cwd);
|
|
8924
|
-
} catch {
|
|
8925
|
-
}
|
|
8926
|
-
}
|
|
8927
|
-
|
|
8928
|
-
// src/scripts/runFlow.ts
|
|
8929
|
-
init_issue();
|
|
8930
|
-
var runFlow = async (ctx) => {
|
|
8931
|
-
const issueNumber = ctx.args.issue;
|
|
8932
|
-
const issue = getIssue(issueNumber, ctx.cwd);
|
|
8933
|
-
ctx.data.issue = issue;
|
|
8934
|
-
ctx.data.commentTargetType = "issue";
|
|
8935
|
-
ctx.data.commentTargetNumber = issueNumber;
|
|
8936
|
-
const argBase = resolveBaseOverride(ctx.args.base);
|
|
8937
|
-
const baseRaw = ctx.args.base;
|
|
8938
|
-
if (baseRaw && !argBase) {
|
|
8939
|
-
process.stderr.write(`[kody runFlow] ignoring --base "${baseRaw}" (must match kody-task or goal-branch pattern)
|
|
8940
|
-
`);
|
|
8941
|
-
}
|
|
8942
|
-
const base = argBase;
|
|
8943
|
-
if (base) {
|
|
8944
|
-
ctx.data.baseBranch = base;
|
|
8945
|
-
process.stderr.write(`[kody runFlow] resolved base branch: ${base} (from --base)
|
|
8946
|
-
`);
|
|
8947
|
-
}
|
|
8948
|
-
const branchInfo = ensureFeatureBranch(issueNumber, issue.title, ctx.config.git.defaultBranch, ctx.cwd, base ?? void 0);
|
|
8949
|
-
ctx.data.branch = branchInfo.branch;
|
|
8950
|
-
const runUrl = getRunUrl();
|
|
8951
|
-
const startMsg = runUrl ? `\u2699\uFE0F kody started \u2014 branch \`${ctx.data.branch}\`, run ${runUrl}` : `\u2699\uFE0F kody started \u2014 branch \`${ctx.data.branch}\``;
|
|
8952
|
-
tryPost(issueNumber, startMsg, ctx.cwd);
|
|
8953
|
-
};
|
|
8954
|
-
function tryPost(issueNumber, body, cwd) {
|
|
8955
|
-
try {
|
|
8956
|
-
postIssueComment(issueNumber, body, cwd);
|
|
8957
|
-
} catch {
|
|
8958
|
-
}
|
|
8959
|
-
}
|
|
8960
|
-
function resolveBaseOverride(value) {
|
|
8961
|
-
if (!value) return null;
|
|
8962
|
-
if (value.length > 200) return null;
|
|
8963
|
-
if (value.includes("..")) return null;
|
|
8964
|
-
if (!/^[a-z0-9][a-z0-9/._-]*$/.test(value)) return null;
|
|
8965
|
-
return value;
|
|
8966
|
-
}
|
|
8967
|
-
|
|
8968
|
-
// src/scripts/runTickScript.ts
|
|
8969
|
-
import { spawnSync } from "child_process";
|
|
8970
|
-
import * as fs30 from "fs";
|
|
8971
|
-
import * as path28 from "path";
|
|
8972
|
-
var runTickScript = async (ctx, _profile, args) => {
|
|
8973
|
-
ctx.skipAgent = true;
|
|
8974
|
-
const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
|
|
8975
|
-
const slugArg = String(args?.slugArg ?? "job");
|
|
8976
|
-
const fenceLabel = String(args?.fenceLabel ?? "kody-job-next-state");
|
|
8977
|
-
const slug = String(ctx.args[slugArg] ?? "").trim();
|
|
8978
|
-
if (!slug) {
|
|
8979
|
-
ctx.output.exitCode = 99;
|
|
8980
|
-
ctx.output.reason = `runTickScript: ctx.args.${slugArg} must be a non-empty slug`;
|
|
8981
|
-
return;
|
|
8982
|
-
}
|
|
8983
|
-
const jobPath = path28.join(ctx.cwd, jobsDir, `${slug}.md`);
|
|
8984
|
-
if (!fs30.existsSync(jobPath)) {
|
|
8985
|
-
ctx.output.exitCode = 99;
|
|
8986
|
-
ctx.output.reason = `runTickScript: job file not found: ${jobPath}`;
|
|
8987
|
-
return;
|
|
8988
|
-
}
|
|
8989
|
-
const raw = fs30.readFileSync(jobPath, "utf-8");
|
|
8990
|
-
const { frontmatter } = splitFrontmatter(raw);
|
|
8991
|
-
const tickScript = frontmatter.tickScript;
|
|
8992
|
-
if (!tickScript) {
|
|
8993
|
-
ctx.output.exitCode = 99;
|
|
8994
|
-
ctx.output.reason = `runTickScript: job ${slug} has no \`tickScript:\` frontmatter \u2014 route via job-tick instead`;
|
|
8995
|
-
return;
|
|
8996
|
-
}
|
|
8997
|
-
const scriptPath = path28.isAbsolute(tickScript) ? tickScript : path28.join(ctx.cwd, tickScript);
|
|
8998
|
-
if (!fs30.existsSync(scriptPath)) {
|
|
8999
|
-
ctx.output.exitCode = 99;
|
|
9000
|
-
ctx.output.reason = `runTickScript: tickScript not found: ${scriptPath}`;
|
|
9001
|
-
return;
|
|
9002
|
-
}
|
|
9003
|
-
const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, jobsDir });
|
|
9004
|
-
let loaded;
|
|
9005
|
-
try {
|
|
9006
|
-
loaded = await backend.load(slug);
|
|
9007
|
-
} catch (err) {
|
|
9008
|
-
ctx.output.exitCode = 99;
|
|
9009
|
-
ctx.output.reason = `runTickScript: state load failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
9010
|
-
return;
|
|
9011
|
-
}
|
|
9012
|
-
ctx.data.jobSlug = slug;
|
|
9013
|
-
ctx.data.jobState = loaded;
|
|
9014
|
-
const childEnv = buildChildEnv(process.env, Boolean(ctx.args.force));
|
|
9015
|
-
const result = spawnSync("bash", [scriptPath], {
|
|
9016
|
-
cwd: ctx.cwd,
|
|
9017
|
-
env: childEnv,
|
|
9018
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
9019
|
-
encoding: "utf-8",
|
|
9020
|
-
timeout: 5 * 60 * 1e3,
|
|
9021
|
-
// Default maxBuffer is 1MB — a chatty `gh pr list --json …` over a
|
|
9022
|
-
// busy repo (or an accidental `set -x`) can blow that and silently
|
|
9023
|
-
// truncate stdout, which is the exact "silent state drop" failure
|
|
9024
|
-
// mode this whole executable was written to prevent. 16MB is well
|
|
9025
|
-
// above any realistic tick output.
|
|
9026
|
-
maxBuffer: 16 * 1024 * 1024
|
|
9027
|
-
});
|
|
9028
|
-
if (result.stdout) process.stdout.write(result.stdout);
|
|
9029
|
-
if (result.stderr) process.stderr.write(result.stderr);
|
|
9030
|
-
if (result.error) {
|
|
9031
|
-
ctx.output.exitCode = 99;
|
|
9032
|
-
ctx.output.reason = `runTickScript: spawn error: ${result.error.message}`;
|
|
9033
|
-
return;
|
|
9034
|
-
}
|
|
9035
|
-
if (result.signal) {
|
|
9036
|
-
ctx.output.exitCode = 124;
|
|
9037
|
-
ctx.output.reason = `runTickScript: ${tickScript} killed by ${result.signal} (likely 5min timeout)`;
|
|
9038
|
-
return;
|
|
9039
|
-
}
|
|
9040
|
-
if (result.status !== 0) {
|
|
9041
|
-
ctx.output.exitCode = result.status ?? 99;
|
|
9042
|
-
ctx.output.reason = `runTickScript: ${tickScript} exited ${result.status}`;
|
|
9043
|
-
return;
|
|
9044
|
-
}
|
|
9045
|
-
const prevRev = loaded.state.rev ?? 0;
|
|
9046
|
-
const parsed = extractNextStateFromText(result.stdout ?? "", fenceLabel, prevRev);
|
|
9047
|
-
if (parsed.error) {
|
|
9048
|
-
ctx.data.nextStateParseError = parsed.error;
|
|
9049
|
-
ctx.output.exitCode = 1;
|
|
9050
|
-
ctx.output.reason = `runTickScript: ${parsed.error}`;
|
|
9051
|
-
return;
|
|
9052
|
-
}
|
|
9053
|
-
ctx.data.nextJobState = parsed.envelope;
|
|
9054
|
-
};
|
|
9055
|
-
function buildChildEnv(parent, force) {
|
|
9056
|
-
const allow = /* @__PURE__ */ new Set([
|
|
9057
|
-
"PATH",
|
|
9058
|
-
"HOME",
|
|
9059
|
-
"USER",
|
|
9060
|
-
"LOGNAME",
|
|
9061
|
-
"SHELL",
|
|
9062
|
-
"TMPDIR",
|
|
9063
|
-
"LANG",
|
|
9064
|
-
"LC_ALL",
|
|
9065
|
-
"TERM",
|
|
9066
|
-
// GitHub auth — `gh` reads these.
|
|
9067
|
-
"GH_TOKEN",
|
|
9068
|
-
"GH_PAT",
|
|
9069
|
-
"GITHUB_TOKEN",
|
|
9070
|
-
// CI metadata commonly read by tick scripts (`gh repo view`,
|
|
9071
|
-
// workflow run links, etc.). All public values from GitHub Actions.
|
|
9072
|
-
"GITHUB_ACTIONS",
|
|
9073
|
-
"GITHUB_ACTOR",
|
|
9074
|
-
"GITHUB_REPOSITORY",
|
|
9075
|
-
"GITHUB_REPOSITORY_OWNER",
|
|
9076
|
-
"GITHUB_REF",
|
|
9077
|
-
"GITHUB_SHA",
|
|
9078
|
-
"GITHUB_RUN_ID",
|
|
9079
|
-
"GITHUB_RUN_NUMBER",
|
|
9080
|
-
"GITHUB_WORKFLOW",
|
|
9081
|
-
"GITHUB_JOB",
|
|
9082
|
-
"GITHUB_SERVER_URL",
|
|
9083
|
-
"GITHUB_API_URL",
|
|
9084
|
-
"GITHUB_EVENT_NAME",
|
|
9085
|
-
"RUNNER_OS",
|
|
9086
|
-
"RUNNER_ARCH"
|
|
9087
|
-
]);
|
|
9088
|
-
const out = {};
|
|
9089
|
-
for (const [key, value] of Object.entries(parent)) {
|
|
9090
|
-
if (value === void 0) continue;
|
|
9091
|
-
if (allow.has(key) || key.startsWith("KODY_PUBLIC_")) {
|
|
9092
|
-
out[key] = value;
|
|
9093
|
-
}
|
|
9094
|
-
}
|
|
9095
|
-
if (force) out.KODY_FORCE = "1";
|
|
9096
|
-
return out;
|
|
9097
|
-
}
|
|
9098
|
-
|
|
9099
|
-
// src/scripts/brainServe.ts
|
|
9100
|
-
import { createServer } from "http";
|
|
9101
|
-
import * as fs32 from "fs";
|
|
9102
|
-
import * as path30 from "path";
|
|
9103
|
-
|
|
9104
|
-
// src/scripts/brainTurnLog.ts
|
|
9105
|
-
import * as fs31 from "fs";
|
|
9106
|
-
import * as path29 from "path";
|
|
9107
|
-
var live = /* @__PURE__ */ new Map();
|
|
9108
|
-
function eventsPath(dir, chatId) {
|
|
9109
|
-
return path29.join(dir, ".kody", "brain-events", `${chatId}.jsonl`);
|
|
9110
|
-
}
|
|
9111
|
-
function lastPersistedSeq(dir, chatId) {
|
|
9112
|
-
const p = eventsPath(dir, chatId);
|
|
9113
|
-
if (!fs31.existsSync(p)) return 0;
|
|
9114
|
-
const lines = fs31.readFileSync(p, "utf-8").split("\n").filter(Boolean);
|
|
9115
|
-
if (lines.length === 0) return 0;
|
|
9116
|
-
try {
|
|
9117
|
-
return JSON.parse(lines[lines.length - 1]).seq || 0;
|
|
9118
|
-
} catch {
|
|
9119
|
-
return 0;
|
|
9120
|
-
}
|
|
9121
|
-
}
|
|
9122
|
-
function readSince(dir, chatId, since) {
|
|
9123
|
-
const p = eventsPath(dir, chatId);
|
|
9124
|
-
if (!fs31.existsSync(p)) return [];
|
|
9125
|
-
const out = [];
|
|
9126
|
-
for (const line of fs31.readFileSync(p, "utf-8").split("\n")) {
|
|
9127
|
-
if (!line) continue;
|
|
9128
|
-
try {
|
|
9129
|
-
const rec = JSON.parse(line);
|
|
9130
|
-
if (rec.seq > since) out.push(rec);
|
|
9131
|
-
} catch {
|
|
9132
|
-
}
|
|
9133
|
-
}
|
|
9134
|
-
return out;
|
|
9135
|
-
}
|
|
9136
|
-
function isTerminal(event) {
|
|
9137
|
-
return event.type === "done" || event.type === "error";
|
|
9138
|
-
}
|
|
9139
|
-
function beginTurn(dir, chatId) {
|
|
9140
|
-
const existing = live.get(chatId);
|
|
9141
|
-
const seqFloor = existing ? existing.seq : lastPersistedSeq(dir, chatId);
|
|
9142
|
-
const turn = (existing?.turn ?? 0) + 1;
|
|
9143
|
-
const state = {
|
|
9144
|
-
seq: seqFloor,
|
|
9145
|
-
turn,
|
|
9146
|
-
status: "running",
|
|
9147
|
-
terminal: null,
|
|
9148
|
-
subscribers: /* @__PURE__ */ new Set()
|
|
9149
|
-
};
|
|
9150
|
-
live.set(chatId, state);
|
|
9151
|
-
const p = eventsPath(dir, chatId);
|
|
9152
|
-
fs31.mkdirSync(path29.dirname(p), { recursive: true });
|
|
9153
|
-
return (event) => {
|
|
9154
|
-
state.seq += 1;
|
|
9155
|
-
const rec = { seq: state.seq, turn, ts: Date.now(), event };
|
|
9156
|
-
try {
|
|
9157
|
-
fs31.appendFileSync(p, JSON.stringify(rec) + "\n");
|
|
9158
|
-
} catch (err) {
|
|
9159
|
-
process.stderr.write(
|
|
9160
|
-
`[brain-turn-log] append failed for ${chatId}: ${err instanceof Error ? err.message : String(err)}
|
|
9161
|
-
`
|
|
9162
|
-
);
|
|
9163
|
-
}
|
|
9164
|
-
for (const fn of state.subscribers) {
|
|
9165
|
-
try {
|
|
9166
|
-
fn(rec);
|
|
9167
|
-
} catch {
|
|
9168
|
-
}
|
|
9169
|
-
}
|
|
9170
|
-
if (isTerminal(event)) {
|
|
9171
|
-
state.status = "ended";
|
|
9172
|
-
state.terminal = rec;
|
|
9173
|
-
const subs = [...state.subscribers];
|
|
9174
|
-
state.subscribers.clear();
|
|
9175
|
-
for (const fn of subs) {
|
|
9176
|
-
try {
|
|
9177
|
-
fn(null);
|
|
9178
|
-
} catch {
|
|
9179
|
-
}
|
|
9180
|
-
}
|
|
9181
|
-
}
|
|
9182
|
-
};
|
|
9183
|
-
}
|
|
9184
|
-
function endTurnIfUnterminated(dir, chatId, errMessage) {
|
|
9185
|
-
const state = live.get(chatId);
|
|
9186
|
-
if (!state || state.status === "ended") return;
|
|
9187
|
-
state.seq += 1;
|
|
9188
|
-
const rec = {
|
|
9189
|
-
seq: state.seq,
|
|
9190
|
-
turn: state.turn,
|
|
9191
|
-
ts: Date.now(),
|
|
9192
|
-
event: { type: "error", error: errMessage || "turn ended unexpectedly", chatId }
|
|
9193
|
-
};
|
|
9194
|
-
try {
|
|
9195
|
-
fs31.appendFileSync(eventsPath(dir, chatId), JSON.stringify(rec) + "\n");
|
|
9196
|
-
} catch {
|
|
9197
|
-
}
|
|
9198
|
-
state.status = "ended";
|
|
9199
|
-
state.terminal = rec;
|
|
9200
|
-
const subs = [...state.subscribers];
|
|
9201
|
-
state.subscribers.clear();
|
|
9202
|
-
for (const fn of subs) {
|
|
9203
|
-
try {
|
|
9204
|
-
fn(rec);
|
|
9205
|
-
fn(null);
|
|
9206
|
-
} catch {
|
|
9207
|
-
}
|
|
9208
|
-
}
|
|
9209
|
-
}
|
|
9210
|
-
function subscribe(dir, chatId, since, onRecord, onClose) {
|
|
9211
|
-
const backlog = readSince(dir, chatId, since);
|
|
9212
|
-
for (const rec of backlog) onRecord(rec);
|
|
9213
|
-
const lastReplayed = backlog.length ? backlog[backlog.length - 1] : null;
|
|
9214
|
-
if (lastReplayed && isTerminal(lastReplayed.event)) {
|
|
9215
|
-
onClose();
|
|
9216
|
-
return () => {
|
|
9217
|
-
};
|
|
9218
|
-
}
|
|
9219
|
-
const state = live.get(chatId);
|
|
9220
|
-
if (state && state.status === "running") {
|
|
9221
|
-
const fn = (rec) => {
|
|
9222
|
-
if (rec === null) {
|
|
9223
|
-
state.subscribers.delete(fn);
|
|
9224
|
-
onClose();
|
|
9225
|
-
return;
|
|
9226
|
-
}
|
|
9227
|
-
if (rec.seq > since) onRecord(rec);
|
|
9228
|
-
};
|
|
9229
|
-
state.subscribers.add(fn);
|
|
9230
|
-
return () => {
|
|
9231
|
-
state.subscribers.delete(fn);
|
|
9232
|
-
};
|
|
9233
|
-
}
|
|
9234
|
-
if (state && state.status === "ended" && state.terminal) {
|
|
9235
|
-
if (state.terminal.seq > since && !lastReplayed) onRecord(state.terminal);
|
|
9236
|
-
onClose();
|
|
9237
|
-
return () => {
|
|
9238
|
-
};
|
|
9239
|
-
}
|
|
9240
|
-
if (lastReplayed) {
|
|
9241
|
-
onRecord({
|
|
9242
|
-
seq: lastReplayed.seq + 1,
|
|
9243
|
-
turn: lastReplayed.turn,
|
|
9244
|
-
ts: Date.now(),
|
|
9245
|
-
event: {
|
|
9246
|
-
type: "error",
|
|
9247
|
-
error: "stream interrupted (server restarted mid-reply) \u2014 resend your message",
|
|
9248
|
-
chatId
|
|
9249
|
-
}
|
|
9250
|
-
});
|
|
9251
|
-
}
|
|
9252
|
-
onClose();
|
|
9253
|
-
return () => {
|
|
9254
|
-
};
|
|
9255
|
-
}
|
|
9256
|
-
function getLastSeq(dir, chatId) {
|
|
9257
|
-
const state = live.get(chatId);
|
|
9258
|
-
if (state) return state.seq;
|
|
9259
|
-
return lastPersistedSeq(dir, chatId);
|
|
9260
|
-
}
|
|
9261
|
-
|
|
9262
|
-
// src/scripts/brainServe.ts
|
|
9263
|
-
var DEFAULT_PORT = 8080;
|
|
9264
|
-
function getApiKey() {
|
|
9265
|
-
const key = (process.env.BRAIN_API_KEY ?? "").trim();
|
|
9266
|
-
if (!key) {
|
|
9267
|
-
throw new Error(
|
|
9268
|
-
"BRAIN_API_KEY env var is required \u2014 set it on the Fly machine before boot."
|
|
9269
|
-
);
|
|
9270
|
-
}
|
|
9271
|
-
return key;
|
|
9272
|
-
}
|
|
9273
|
-
function authOk(req, expected) {
|
|
9274
|
-
const xApiKey = req.headers["x-api-key"]?.trim();
|
|
9275
|
-
if (xApiKey && xApiKey === expected) return true;
|
|
9276
|
-
const auth = req.headers["authorization"]?.trim();
|
|
9277
|
-
if (auth && auth.toLowerCase().startsWith("bearer ")) {
|
|
9278
|
-
return auth.slice(7).trim() === expected;
|
|
9279
|
-
}
|
|
9280
|
-
return false;
|
|
9281
|
-
}
|
|
9282
|
-
function readJsonBody(req) {
|
|
9283
|
-
return new Promise((resolve4, reject) => {
|
|
9284
|
-
const chunks = [];
|
|
9285
|
-
req.on("data", (c) => chunks.push(c));
|
|
9286
|
-
req.on("end", () => {
|
|
9287
|
-
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
9288
|
-
if (!raw.trim()) {
|
|
9289
|
-
resolve4({});
|
|
9290
|
-
return;
|
|
9291
|
-
}
|
|
9292
|
-
try {
|
|
9293
|
-
resolve4(JSON.parse(raw));
|
|
9294
|
-
} catch (err) {
|
|
9295
|
-
reject(err instanceof Error ? err : new Error(String(err)));
|
|
9296
|
-
}
|
|
9297
|
-
});
|
|
9298
|
-
req.on("error", reject);
|
|
9299
|
-
});
|
|
9300
|
-
}
|
|
9301
|
-
function sendJson(res, status, body) {
|
|
9302
|
-
res.writeHead(status, { "content-type": "application/json" });
|
|
9303
|
-
res.end(JSON.stringify(body));
|
|
9304
|
-
}
|
|
9305
|
-
function writeSseHeaders(res) {
|
|
9306
|
-
res.writeHead(200, {
|
|
9307
|
-
"content-type": "text/event-stream; charset=utf-8",
|
|
9308
|
-
"cache-control": "no-cache, no-transform",
|
|
9309
|
-
connection: "keep-alive",
|
|
9310
|
-
"x-accel-buffering": "no"
|
|
9311
|
-
});
|
|
9336
|
+
return subject ? `revert: "${subject}" (${full.slice(0, 7)})` : `revert: ${full.slice(0, 7)}`;
|
|
9337
|
+
}
|
|
9338
|
+
const shas = resolved.map((r) => r.full.slice(0, 7)).join(", ");
|
|
9339
|
+
return `revert: ${resolved.length} commit(s) (${shas})`;
|
|
9312
9340
|
}
|
|
9313
|
-
function
|
|
9314
|
-
|
|
9315
|
-
|
|
9316
|
-
`);
|
|
9341
|
+
function buildPrSummary(resolved) {
|
|
9342
|
+
return resolved.map((r) => `- Reverted \`${r.full.slice(0, 7)}\`${r.subject ? ` \u2014 ${r.subject}` : ""}`).join("\n");
|
|
9317
9343
|
}
|
|
9318
|
-
function
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
|
|
9333
|
-
}
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
return {
|
|
9338
|
-
type: "error",
|
|
9339
|
-
error: typeof event.payload.error === "string" ? event.payload.error : "agent error",
|
|
9340
|
-
chatId
|
|
9341
|
-
};
|
|
9342
|
-
default:
|
|
9343
|
-
return null;
|
|
9344
|
+
function git3(args, cwd) {
|
|
9345
|
+
return execFileSync23("git", args, {
|
|
9346
|
+
encoding: "utf-8",
|
|
9347
|
+
timeout: 3e4,
|
|
9348
|
+
cwd,
|
|
9349
|
+
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
9350
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
9351
|
+
}).trim();
|
|
9352
|
+
}
|
|
9353
|
+
function isAncestorOfHead(sha, cwd) {
|
|
9354
|
+
try {
|
|
9355
|
+
execFileSync23("git", ["merge-base", "--is-ancestor", sha, "HEAD"], {
|
|
9356
|
+
cwd,
|
|
9357
|
+
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
9358
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
9359
|
+
});
|
|
9360
|
+
return true;
|
|
9361
|
+
} catch {
|
|
9362
|
+
return false;
|
|
9344
9363
|
}
|
|
9345
9364
|
}
|
|
9346
|
-
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
|
|
9365
|
+
function tryPostPr4(prNumber, body, cwd) {
|
|
9366
|
+
try {
|
|
9367
|
+
postPrReviewComment(prNumber, body, cwd);
|
|
9368
|
+
} catch (err) {
|
|
9369
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
9370
|
+
process.stderr.write(`[kody revertFlow] PR comment on #${prNumber} failed: ${msg}
|
|
9371
|
+
`);
|
|
9350
9372
|
}
|
|
9351
|
-
|
|
9352
|
-
|
|
9353
|
-
|
|
9354
|
-
|
|
9355
|
-
|
|
9373
|
+
}
|
|
9374
|
+
|
|
9375
|
+
// src/scripts/reviewFlow.ts
|
|
9376
|
+
init_issue();
|
|
9377
|
+
var reviewFlow = async (ctx) => {
|
|
9378
|
+
const prNumber = ctx.args.pr;
|
|
9379
|
+
const pr = getPr(prNumber, ctx.cwd);
|
|
9380
|
+
if (pr.state !== "OPEN") {
|
|
9381
|
+
ctx.output.exitCode = 1;
|
|
9382
|
+
ctx.output.reason = `PR #${prNumber} is not OPEN (state: ${pr.state})`;
|
|
9383
|
+
ctx.skipAgent = true;
|
|
9384
|
+
return;
|
|
9356
9385
|
}
|
|
9386
|
+
ctx.data.pr = pr;
|
|
9387
|
+
ctx.data.commentTargetType = "pr";
|
|
9388
|
+
ctx.data.commentTargetNumber = prNumber;
|
|
9389
|
+
checkoutPrBranch(prNumber, ctx.cwd);
|
|
9390
|
+
ctx.data.branch = getCurrentBranch(ctx.cwd);
|
|
9391
|
+
ctx.data.prDiff = getPrDiff(prNumber, ctx.cwd);
|
|
9392
|
+
const runUrl = getRunUrl();
|
|
9393
|
+
const runSuffix = runUrl ? `, run ${runUrl}` : "";
|
|
9394
|
+
tryPostPr5(prNumber, `\u{1F440} kody review started on PR #${prNumber}${runSuffix}`, ctx.cwd);
|
|
9357
9395
|
};
|
|
9358
|
-
|
|
9359
|
-
|
|
9360
|
-
|
|
9361
|
-
|
|
9362
|
-
}
|
|
9363
|
-
chatQueues.set(
|
|
9364
|
-
chatId,
|
|
9365
|
-
next.finally(() => {
|
|
9366
|
-
if (chatQueues.get(chatId) === next) chatQueues.delete(chatId);
|
|
9367
|
-
})
|
|
9368
|
-
);
|
|
9369
|
-
return next;
|
|
9396
|
+
function tryPostPr5(prNumber, body, cwd) {
|
|
9397
|
+
try {
|
|
9398
|
+
postPrReviewComment(prNumber, body, cwd);
|
|
9399
|
+
} catch {
|
|
9400
|
+
}
|
|
9370
9401
|
}
|
|
9371
|
-
function streamToRes(res, dir, chatId, since) {
|
|
9372
|
-
writeSseHeaders(res);
|
|
9373
|
-
emitSse(res, { type: "chat", chatId });
|
|
9374
|
-
let maxSent = since;
|
|
9375
|
-
const unsubscribe = subscribe(
|
|
9376
|
-
dir,
|
|
9377
|
-
chatId,
|
|
9378
|
-
since,
|
|
9379
|
-
(rec) => {
|
|
9380
|
-
if (rec.seq <= maxSent) return;
|
|
9381
|
-
maxSent = rec.seq;
|
|
9382
|
-
if (res.writableEnded) return;
|
|
9383
|
-
res.write(`data: ${JSON.stringify({ ...rec.event, seq: rec.seq })}
|
|
9384
9402
|
|
|
9403
|
+
// src/scripts/runFlow.ts
|
|
9404
|
+
init_issue();
|
|
9405
|
+
var runFlow = async (ctx) => {
|
|
9406
|
+
const issueNumber = ctx.args.issue;
|
|
9407
|
+
const issue = getIssue(issueNumber, ctx.cwd);
|
|
9408
|
+
ctx.data.issue = issue;
|
|
9409
|
+
ctx.data.commentTargetType = "issue";
|
|
9410
|
+
ctx.data.commentTargetNumber = issueNumber;
|
|
9411
|
+
const argBase = resolveBaseOverride(ctx.args.base);
|
|
9412
|
+
const baseRaw = ctx.args.base;
|
|
9413
|
+
if (baseRaw && !argBase) {
|
|
9414
|
+
process.stderr.write(`[kody runFlow] ignoring --base "${baseRaw}" (must match kody-task or goal-branch pattern)
|
|
9385
9415
|
`);
|
|
9386
|
-
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
|
|
9393
|
-
|
|
9394
|
-
|
|
9395
|
-
);
|
|
9396
|
-
|
|
9397
|
-
|
|
9398
|
-
|
|
9399
|
-
|
|
9416
|
+
}
|
|
9417
|
+
const base = argBase;
|
|
9418
|
+
if (base) {
|
|
9419
|
+
ctx.data.baseBranch = base;
|
|
9420
|
+
process.stderr.write(`[kody runFlow] resolved base branch: ${base} (from --base)
|
|
9421
|
+
`);
|
|
9422
|
+
}
|
|
9423
|
+
const branchInfo = ensureFeatureBranch(issueNumber, issue.title, ctx.config.git.defaultBranch, ctx.cwd, base ?? void 0);
|
|
9424
|
+
ctx.data.branch = branchInfo.branch;
|
|
9425
|
+
const runUrl = getRunUrl();
|
|
9426
|
+
const startMsg = runUrl ? `\u2699\uFE0F kody started \u2014 branch \`${ctx.data.branch}\`, run ${runUrl}` : `\u2699\uFE0F kody started \u2014 branch \`${ctx.data.branch}\``;
|
|
9427
|
+
tryPost(issueNumber, startMsg, ctx.cwd);
|
|
9428
|
+
};
|
|
9429
|
+
function tryPost(issueNumber, body, cwd) {
|
|
9400
9430
|
try {
|
|
9401
|
-
body
|
|
9431
|
+
postIssueComment(issueNumber, body, cwd);
|
|
9402
9432
|
} catch {
|
|
9403
|
-
sendJson(res, 400, { error: "invalid JSON body" });
|
|
9404
|
-
return;
|
|
9405
|
-
}
|
|
9406
|
-
const message = typeof body === "object" && body !== null && "message" in body ? body.message : void 0;
|
|
9407
|
-
if (typeof message !== "string" || !message.trim()) {
|
|
9408
|
-
sendJson(res, 400, { error: "message required" });
|
|
9409
|
-
return;
|
|
9410
9433
|
}
|
|
9411
|
-
const sessionFile = sessionFilePath(opts.cwd, chatId);
|
|
9412
|
-
fs32.mkdirSync(path30.dirname(sessionFile), { recursive: true });
|
|
9413
|
-
appendTurn(sessionFile, {
|
|
9414
|
-
role: "user",
|
|
9415
|
-
content: message,
|
|
9416
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9417
|
-
});
|
|
9418
|
-
const sinceFloor = getLastSeq(opts.cwd, chatId);
|
|
9419
|
-
const emitToLog = beginTurn(opts.cwd, chatId);
|
|
9420
|
-
const sink = new BrokerSink(emitToLog, chatId);
|
|
9421
|
-
void enqueue(
|
|
9422
|
-
chatId,
|
|
9423
|
-
() => opts.runTurn({
|
|
9424
|
-
sessionId: chatId,
|
|
9425
|
-
sessionFile,
|
|
9426
|
-
cwd: opts.cwd,
|
|
9427
|
-
model: opts.model,
|
|
9428
|
-
litellmUrl: opts.litellmUrl,
|
|
9429
|
-
sink
|
|
9430
|
-
}).catch((err) => {
|
|
9431
|
-
const errMsg2 = err instanceof Error ? err.message : String(err);
|
|
9432
|
-
process.stderr.write(`[brain-serve] chat turn failed: ${errMsg2}
|
|
9433
|
-
`);
|
|
9434
|
-
endTurnIfUnterminated(opts.cwd, chatId, errMsg2);
|
|
9435
|
-
}).finally(() => {
|
|
9436
|
-
endTurnIfUnterminated(
|
|
9437
|
-
opts.cwd,
|
|
9438
|
-
chatId,
|
|
9439
|
-
"Brain turn ended without a reply (the machine may have restarted mid-turn) \u2014 please resend your message"
|
|
9440
|
-
);
|
|
9441
|
-
})
|
|
9442
|
-
);
|
|
9443
|
-
streamToRes(res, opts.cwd, chatId, sinceFloor);
|
|
9444
9434
|
}
|
|
9445
|
-
function
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
}
|
|
9452
|
-
const url = new URL(req.url, `http://localhost`);
|
|
9453
|
-
if (req.method === "GET" && url.pathname === "/healthz") {
|
|
9454
|
-
sendJson(res, 200, { ok: true });
|
|
9455
|
-
return;
|
|
9456
|
-
}
|
|
9457
|
-
if (!authOk(req, opts.apiKey)) {
|
|
9458
|
-
sendJson(res, 401, { error: "unauthorized" });
|
|
9459
|
-
return;
|
|
9460
|
-
}
|
|
9461
|
-
const m = url.pathname.match(/^\/chats\/([^/]+)\/messages\/?$/);
|
|
9462
|
-
if (req.method === "POST" && m) {
|
|
9463
|
-
const chatId = decodeURIComponent(m[1] ?? "");
|
|
9464
|
-
if (!chatId) {
|
|
9465
|
-
sendJson(res, 400, { error: "chatId required" });
|
|
9466
|
-
return;
|
|
9467
|
-
}
|
|
9468
|
-
await handleChatTurn(req, res, chatId, {
|
|
9469
|
-
cwd: opts.cwd,
|
|
9470
|
-
model: opts.model,
|
|
9471
|
-
litellmUrl: opts.litellmUrl,
|
|
9472
|
-
runTurn
|
|
9473
|
-
});
|
|
9474
|
-
return;
|
|
9475
|
-
}
|
|
9476
|
-
const sm = url.pathname.match(/^\/chats\/([^/]+)\/stream\/?$/);
|
|
9477
|
-
if (req.method === "GET" && sm) {
|
|
9478
|
-
const chatId = decodeURIComponent(sm[1] ?? "");
|
|
9479
|
-
if (!chatId) {
|
|
9480
|
-
sendJson(res, 400, { error: "chatId required" });
|
|
9481
|
-
return;
|
|
9482
|
-
}
|
|
9483
|
-
const sinceRaw = url.searchParams.get("since");
|
|
9484
|
-
const since = Number.isFinite(Number(sinceRaw)) ? Number(sinceRaw) : 0;
|
|
9485
|
-
streamToRes(res, opts.cwd, chatId, since);
|
|
9486
|
-
return;
|
|
9487
|
-
}
|
|
9488
|
-
sendJson(res, 404, { error: "not found" });
|
|
9489
|
-
});
|
|
9435
|
+
function resolveBaseOverride(value) {
|
|
9436
|
+
if (!value) return null;
|
|
9437
|
+
if (value.length > 200) return null;
|
|
9438
|
+
if (value.includes("..")) return null;
|
|
9439
|
+
if (!/^[a-z0-9][a-z0-9/._-]*$/.test(value)) return null;
|
|
9440
|
+
return value;
|
|
9490
9441
|
}
|
|
9491
|
-
|
|
9442
|
+
|
|
9443
|
+
// src/scripts/runTickScript.ts
|
|
9444
|
+
import { spawnSync } from "child_process";
|
|
9445
|
+
import * as fs32 from "fs";
|
|
9446
|
+
import * as path30 from "path";
|
|
9447
|
+
var runTickScript = async (ctx, _profile, args) => {
|
|
9492
9448
|
ctx.skipAgent = true;
|
|
9493
|
-
const
|
|
9494
|
-
const
|
|
9495
|
-
const
|
|
9496
|
-
const
|
|
9497
|
-
|
|
9498
|
-
|
|
9499
|
-
|
|
9500
|
-
|
|
9501
|
-
`
|
|
9502
|
-
);
|
|
9503
|
-
handle = await startLitellmIfNeeded(model, ctx.cwd);
|
|
9504
|
-
process.stdout.write(
|
|
9505
|
-
`[brain-serve] LiteLLM ready at ${handle?.url ?? LITELLM_DEFAULT_URL}
|
|
9506
|
-
`
|
|
9507
|
-
);
|
|
9449
|
+
const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
|
|
9450
|
+
const slugArg = String(args?.slugArg ?? "job");
|
|
9451
|
+
const fenceLabel = String(args?.fenceLabel ?? "kody-job-next-state");
|
|
9452
|
+
const slug = String(ctx.args[slugArg] ?? "").trim();
|
|
9453
|
+
if (!slug) {
|
|
9454
|
+
ctx.output.exitCode = 99;
|
|
9455
|
+
ctx.output.reason = `runTickScript: ctx.args.${slugArg} must be a non-empty slug`;
|
|
9456
|
+
return;
|
|
9508
9457
|
}
|
|
9509
|
-
const
|
|
9510
|
-
|
|
9511
|
-
|
|
9458
|
+
const jobPath = path30.join(ctx.cwd, jobsDir, `${slug}.md`);
|
|
9459
|
+
if (!fs32.existsSync(jobPath)) {
|
|
9460
|
+
ctx.output.exitCode = 99;
|
|
9461
|
+
ctx.output.reason = `runTickScript: job file not found: ${jobPath}`;
|
|
9462
|
+
return;
|
|
9463
|
+
}
|
|
9464
|
+
const raw = fs32.readFileSync(jobPath, "utf-8");
|
|
9465
|
+
const { frontmatter } = splitFrontmatter(raw);
|
|
9466
|
+
const tickScript = frontmatter.tickScript;
|
|
9467
|
+
if (!tickScript) {
|
|
9468
|
+
ctx.output.exitCode = 99;
|
|
9469
|
+
ctx.output.reason = `runTickScript: job ${slug} has no \`tickScript:\` frontmatter \u2014 route via job-tick instead`;
|
|
9470
|
+
return;
|
|
9471
|
+
}
|
|
9472
|
+
const scriptPath = path30.isAbsolute(tickScript) ? tickScript : path30.join(ctx.cwd, tickScript);
|
|
9473
|
+
if (!fs32.existsSync(scriptPath)) {
|
|
9474
|
+
ctx.output.exitCode = 99;
|
|
9475
|
+
ctx.output.reason = `runTickScript: tickScript not found: ${scriptPath}`;
|
|
9476
|
+
return;
|
|
9477
|
+
}
|
|
9478
|
+
const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, jobsDir });
|
|
9479
|
+
let loaded;
|
|
9480
|
+
try {
|
|
9481
|
+
loaded = await backend.load(slug);
|
|
9482
|
+
} catch (err) {
|
|
9483
|
+
ctx.output.exitCode = 99;
|
|
9484
|
+
ctx.output.reason = `runTickScript: state load failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
9485
|
+
return;
|
|
9486
|
+
}
|
|
9487
|
+
ctx.data.jobSlug = slug;
|
|
9488
|
+
ctx.data.jobState = loaded;
|
|
9489
|
+
const childEnv = buildChildEnv(process.env, Boolean(ctx.args.force));
|
|
9490
|
+
const result = spawnSync("bash", [scriptPath], {
|
|
9512
9491
|
cwd: ctx.cwd,
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
});
|
|
9492
|
+
env: childEnv,
|
|
9493
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
9494
|
+
encoding: "utf-8",
|
|
9495
|
+
timeout: 5 * 60 * 1e3,
|
|
9496
|
+
// Default maxBuffer is 1MB — a chatty `gh pr list --json …` over a
|
|
9497
|
+
// busy repo (or an accidental `set -x`) can blow that and silently
|
|
9498
|
+
// truncate stdout, which is the exact "silent state drop" failure
|
|
9499
|
+
// mode this whole executable was written to prevent. 16MB is well
|
|
9500
|
+
// above any realistic tick output.
|
|
9501
|
+
maxBuffer: 16 * 1024 * 1024
|
|
9524
9502
|
});
|
|
9525
|
-
|
|
9526
|
-
|
|
9527
|
-
|
|
9528
|
-
|
|
9529
|
-
|
|
9530
|
-
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
|
|
9536
|
-
|
|
9503
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
9504
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
9505
|
+
if (result.error) {
|
|
9506
|
+
ctx.output.exitCode = 99;
|
|
9507
|
+
ctx.output.reason = `runTickScript: spawn error: ${result.error.message}`;
|
|
9508
|
+
return;
|
|
9509
|
+
}
|
|
9510
|
+
if (result.signal) {
|
|
9511
|
+
ctx.output.exitCode = 124;
|
|
9512
|
+
ctx.output.reason = `runTickScript: ${tickScript} killed by ${result.signal} (likely 5min timeout)`;
|
|
9513
|
+
return;
|
|
9514
|
+
}
|
|
9515
|
+
if (result.status !== 0) {
|
|
9516
|
+
ctx.output.exitCode = result.status ?? 99;
|
|
9517
|
+
ctx.output.reason = `runTickScript: ${tickScript} exited ${result.status}`;
|
|
9518
|
+
return;
|
|
9519
|
+
}
|
|
9520
|
+
const prevRev = loaded.state.rev ?? 0;
|
|
9521
|
+
const parsed = extractNextStateFromText(result.stdout ?? "", fenceLabel, prevRev);
|
|
9522
|
+
if (parsed.error) {
|
|
9523
|
+
ctx.data.nextStateParseError = parsed.error;
|
|
9524
|
+
ctx.output.exitCode = 1;
|
|
9525
|
+
ctx.output.reason = `runTickScript: ${parsed.error}`;
|
|
9526
|
+
return;
|
|
9527
|
+
}
|
|
9528
|
+
ctx.data.nextJobState = parsed.envelope;
|
|
9529
|
+
};
|
|
9530
|
+
function buildChildEnv(parent, force) {
|
|
9531
|
+
const allow = /* @__PURE__ */ new Set([
|
|
9532
|
+
"PATH",
|
|
9533
|
+
"HOME",
|
|
9534
|
+
"USER",
|
|
9535
|
+
"LOGNAME",
|
|
9536
|
+
"SHELL",
|
|
9537
|
+
"TMPDIR",
|
|
9538
|
+
"LANG",
|
|
9539
|
+
"LC_ALL",
|
|
9540
|
+
"TERM",
|
|
9541
|
+
// GitHub auth — `gh` reads these.
|
|
9542
|
+
"GH_TOKEN",
|
|
9543
|
+
"GH_PAT",
|
|
9544
|
+
"GITHUB_TOKEN",
|
|
9545
|
+
// CI metadata commonly read by tick scripts (`gh repo view`,
|
|
9546
|
+
// workflow run links, etc.). All public values from GitHub Actions.
|
|
9547
|
+
"GITHUB_ACTIONS",
|
|
9548
|
+
"GITHUB_ACTOR",
|
|
9549
|
+
"GITHUB_REPOSITORY",
|
|
9550
|
+
"GITHUB_REPOSITORY_OWNER",
|
|
9551
|
+
"GITHUB_REF",
|
|
9552
|
+
"GITHUB_SHA",
|
|
9553
|
+
"GITHUB_RUN_ID",
|
|
9554
|
+
"GITHUB_RUN_NUMBER",
|
|
9555
|
+
"GITHUB_WORKFLOW",
|
|
9556
|
+
"GITHUB_JOB",
|
|
9557
|
+
"GITHUB_SERVER_URL",
|
|
9558
|
+
"GITHUB_API_URL",
|
|
9559
|
+
"GITHUB_EVENT_NAME",
|
|
9560
|
+
"RUNNER_OS",
|
|
9561
|
+
"RUNNER_ARCH"
|
|
9562
|
+
]);
|
|
9563
|
+
const out = {};
|
|
9564
|
+
for (const [key, value] of Object.entries(parent)) {
|
|
9565
|
+
if (value === void 0) continue;
|
|
9566
|
+
if (allow.has(key) || key.startsWith("KODY_PUBLIC_")) {
|
|
9567
|
+
out[key] = value;
|
|
9568
|
+
}
|
|
9569
|
+
}
|
|
9570
|
+
if (force) out.KODY_FORCE = "1";
|
|
9571
|
+
return out;
|
|
9572
|
+
}
|
|
9573
|
+
|
|
9574
|
+
// src/scripts/saveGoalState.ts
|
|
9575
|
+
var saveGoalState = async (ctx) => {
|
|
9576
|
+
const goal = ctx.data.goal;
|
|
9577
|
+
if (!goal) {
|
|
9578
|
+
ctx.skipAgent = true;
|
|
9579
|
+
return;
|
|
9580
|
+
}
|
|
9581
|
+
const updated = {
|
|
9582
|
+
...goal.raw ?? { state: goal.state, extra: {} },
|
|
9583
|
+
state: goal.state,
|
|
9584
|
+
lastDispatchedIssue: goal.lastDispatchedIssue,
|
|
9585
|
+
updatedAt: nowIso()
|
|
9537
9586
|
};
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
|
|
9587
|
+
writeGoalState(ctx.cwd, goal.id, updated);
|
|
9588
|
+
ctx.skipAgent = true;
|
|
9589
|
+
};
|
|
9590
|
+
|
|
9591
|
+
// src/scripts/saveTaskState.ts
|
|
9592
|
+
var saveTaskState = async (ctx, profile) => {
|
|
9593
|
+
const target = ctx.data.commentTargetType;
|
|
9594
|
+
const number = ctx.data.commentTargetNumber;
|
|
9595
|
+
const state = ctx.data.taskState;
|
|
9596
|
+
if (!target || !number || !state) return;
|
|
9597
|
+
const executable = profile.name;
|
|
9598
|
+
const action = ctx.data.action ?? synthesizeAction(ctx);
|
|
9599
|
+
if (ctx.output.prUrl && !state.core.prUrl) state.core.prUrl = ctx.output.prUrl;
|
|
9600
|
+
if (typeof ctx.data.runUrl === "string") state.core.runUrl = ctx.data.runUrl;
|
|
9601
|
+
const next = reduce(state, executable, action, profile.phase);
|
|
9602
|
+
if (ctx.output.prUrl) next.core.prUrl = ctx.output.prUrl;
|
|
9603
|
+
if (typeof ctx.data.runUrl === "string") next.core.runUrl = ctx.data.runUrl;
|
|
9604
|
+
writeTaskState(target, number, next, ctx.cwd);
|
|
9605
|
+
ctx.data.taskStateRendered = renderStateComment(next);
|
|
9542
9606
|
};
|
|
9607
|
+
function synthesizeAction(ctx) {
|
|
9608
|
+
const ok = ctx.output.exitCode === 0;
|
|
9609
|
+
return {
|
|
9610
|
+
type: ok ? "RUN_COMPLETED" : "RUN_FAILED",
|
|
9611
|
+
payload: {
|
|
9612
|
+
exitCode: ctx.output.exitCode,
|
|
9613
|
+
reason: ctx.output.reason,
|
|
9614
|
+
prUrl: ctx.output.prUrl
|
|
9615
|
+
},
|
|
9616
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9617
|
+
};
|
|
9618
|
+
}
|
|
9543
9619
|
|
|
9544
9620
|
// src/scripts/serveFlow.ts
|
|
9545
9621
|
import { spawn as spawn3 } from "child_process";
|
|
@@ -9643,52 +9719,6 @@ var serveFlow = async (ctx) => {
|
|
|
9643
9719
|
});
|
|
9644
9720
|
};
|
|
9645
9721
|
|
|
9646
|
-
// src/scripts/saveGoalState.ts
|
|
9647
|
-
var saveGoalState = async (ctx) => {
|
|
9648
|
-
const goal = ctx.data.goal;
|
|
9649
|
-
if (!goal) {
|
|
9650
|
-
ctx.skipAgent = true;
|
|
9651
|
-
return;
|
|
9652
|
-
}
|
|
9653
|
-
const updated = {
|
|
9654
|
-
...goal.raw ?? { state: goal.state, extra: {} },
|
|
9655
|
-
state: goal.state,
|
|
9656
|
-
lastDispatchedIssue: goal.lastDispatchedIssue,
|
|
9657
|
-
updatedAt: nowIso()
|
|
9658
|
-
};
|
|
9659
|
-
writeGoalState(ctx.cwd, goal.id, updated);
|
|
9660
|
-
ctx.skipAgent = true;
|
|
9661
|
-
};
|
|
9662
|
-
|
|
9663
|
-
// src/scripts/saveTaskState.ts
|
|
9664
|
-
var saveTaskState = async (ctx, profile) => {
|
|
9665
|
-
const target = ctx.data.commentTargetType;
|
|
9666
|
-
const number = ctx.data.commentTargetNumber;
|
|
9667
|
-
const state = ctx.data.taskState;
|
|
9668
|
-
if (!target || !number || !state) return;
|
|
9669
|
-
const executable = profile.name;
|
|
9670
|
-
const action = ctx.data.action ?? synthesizeAction(ctx);
|
|
9671
|
-
if (ctx.output.prUrl && !state.core.prUrl) state.core.prUrl = ctx.output.prUrl;
|
|
9672
|
-
if (typeof ctx.data.runUrl === "string") state.core.runUrl = ctx.data.runUrl;
|
|
9673
|
-
const next = reduce(state, executable, action, profile.phase);
|
|
9674
|
-
if (ctx.output.prUrl) next.core.prUrl = ctx.output.prUrl;
|
|
9675
|
-
if (typeof ctx.data.runUrl === "string") next.core.runUrl = ctx.data.runUrl;
|
|
9676
|
-
writeTaskState(target, number, next, ctx.cwd);
|
|
9677
|
-
ctx.data.taskStateRendered = renderStateComment(next);
|
|
9678
|
-
};
|
|
9679
|
-
function synthesizeAction(ctx) {
|
|
9680
|
-
const ok = ctx.output.exitCode === 0;
|
|
9681
|
-
return {
|
|
9682
|
-
type: ok ? "RUN_COMPLETED" : "RUN_FAILED",
|
|
9683
|
-
payload: {
|
|
9684
|
-
exitCode: ctx.output.exitCode,
|
|
9685
|
-
reason: ctx.output.reason,
|
|
9686
|
-
prUrl: ctx.output.prUrl
|
|
9687
|
-
},
|
|
9688
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9689
|
-
};
|
|
9690
|
-
}
|
|
9691
|
-
|
|
9692
9722
|
// src/scripts/setCommentTarget.ts
|
|
9693
9723
|
var setCommentTarget = async (ctx, _profile, args) => {
|
|
9694
9724
|
const type = args?.type ?? "issue";
|
|
@@ -10531,6 +10561,7 @@ var preflightScripts = {
|
|
|
10531
10561
|
handleAbandonedGoal,
|
|
10532
10562
|
deriveGoalPhase,
|
|
10533
10563
|
dispatchNextTask,
|
|
10564
|
+
parkGoalForMerge,
|
|
10534
10565
|
finalizeGoal,
|
|
10535
10566
|
saveGoalState
|
|
10536
10567
|
};
|