@kody-ade/kody-engine 0.2.47 → 0.2.48
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/kody2.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// package.json
|
|
4
4
|
var package_default = {
|
|
5
5
|
name: "@kody-ade/kody-engine",
|
|
6
|
-
version: "0.2.
|
|
6
|
+
version: "0.2.48",
|
|
7
7
|
description: "kody2 \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
|
|
8
8
|
license: "MIT",
|
|
9
9
|
type: "module",
|
|
@@ -51,8 +51,8 @@ var package_default = {
|
|
|
51
51
|
|
|
52
52
|
// src/chat-cli.ts
|
|
53
53
|
import { execFileSync as execFileSync20 } from "child_process";
|
|
54
|
-
import * as
|
|
55
|
-
import * as
|
|
54
|
+
import * as fs22 from "fs";
|
|
55
|
+
import * as path19 from "path";
|
|
56
56
|
|
|
57
57
|
// src/chat/events.ts
|
|
58
58
|
import * as fs from "fs";
|
|
@@ -529,8 +529,8 @@ async function emit(sink, type, sessionId, suffix, payload) {
|
|
|
529
529
|
|
|
530
530
|
// src/kody2-cli.ts
|
|
531
531
|
import { execFileSync as execFileSync19 } from "child_process";
|
|
532
|
-
import * as
|
|
533
|
-
import * as
|
|
532
|
+
import * as fs21 from "fs";
|
|
533
|
+
import * as path18 from "path";
|
|
534
534
|
|
|
535
535
|
// src/dispatch.ts
|
|
536
536
|
import * as fs5 from "fs";
|
|
@@ -572,6 +572,9 @@ function autoDispatch(opts) {
|
|
|
572
572
|
if (/\bresolve\b/.test(afterTag)) {
|
|
573
573
|
return { executable: "resolve", cliArgs: { pr: targetNum }, target: targetNum };
|
|
574
574
|
}
|
|
575
|
+
if (/\bui-review\b/.test(afterTag)) {
|
|
576
|
+
return { executable: "ui-review", cliArgs: { pr: targetNum }, target: targetNum };
|
|
577
|
+
}
|
|
575
578
|
if (/\breview\b/.test(afterTag)) {
|
|
576
579
|
return { executable: "review", cliArgs: { pr: targetNum }, target: targetNum };
|
|
577
580
|
}
|
|
@@ -633,8 +636,8 @@ function extractFeedback(afterTag) {
|
|
|
633
636
|
}
|
|
634
637
|
|
|
635
638
|
// src/executor.ts
|
|
636
|
-
import * as
|
|
637
|
-
import * as
|
|
639
|
+
import * as fs20 from "fs";
|
|
640
|
+
import * as path17 from "path";
|
|
638
641
|
|
|
639
642
|
// src/litellm.ts
|
|
640
643
|
import { execFileSync, spawn } from "child_process";
|
|
@@ -1097,29 +1100,11 @@ function noteFromAction(action) {
|
|
|
1097
1100
|
}
|
|
1098
1101
|
function renderStateComment(state) {
|
|
1099
1102
|
const lines = [];
|
|
1100
|
-
lines.push(
|
|
1101
|
-
lines.push("");
|
|
1102
|
-
lines.push("```json");
|
|
1103
|
-
lines.push(
|
|
1104
|
-
JSON.stringify(
|
|
1105
|
-
{
|
|
1106
|
-
schemaVersion: state.schemaVersion,
|
|
1107
|
-
core: state.core,
|
|
1108
|
-
artifacts: state.artifacts ?? {},
|
|
1109
|
-
executables: state.executables,
|
|
1110
|
-
history: state.history,
|
|
1111
|
-
...state.flow ? { flow: state.flow } : {}
|
|
1112
|
-
},
|
|
1113
|
-
null,
|
|
1114
|
-
2
|
|
1115
|
-
)
|
|
1116
|
-
);
|
|
1117
|
-
lines.push("```");
|
|
1118
|
-
lines.push("");
|
|
1119
|
-
lines.push(STATE_END);
|
|
1120
|
-
lines.push("");
|
|
1121
|
-
lines.push("## kody2 task state");
|
|
1103
|
+
lines.push("## \u{1F4CB} kody2 task state");
|
|
1122
1104
|
lines.push("");
|
|
1105
|
+
if (state.flow) {
|
|
1106
|
+
lines.push(`- **Flow:** \`${state.flow.name}\` (step: \`${state.flow.step}\`)`);
|
|
1107
|
+
}
|
|
1123
1108
|
lines.push(`- **Phase:** \`${state.core.phase}\` **Status:** \`${state.core.status}\``);
|
|
1124
1109
|
if (state.core.currentExecutable) {
|
|
1125
1110
|
lines.push(`- **Last executable:** \`${state.core.currentExecutable}\``);
|
|
@@ -1146,6 +1131,31 @@ function renderStateComment(state) {
|
|
|
1146
1131
|
}
|
|
1147
1132
|
lines.push("");
|
|
1148
1133
|
}
|
|
1134
|
+
lines.push("<details>");
|
|
1135
|
+
lines.push("<summary>Raw state (JSON)</summary>");
|
|
1136
|
+
lines.push("");
|
|
1137
|
+
lines.push(STATE_BEGIN);
|
|
1138
|
+
lines.push("");
|
|
1139
|
+
lines.push("```json");
|
|
1140
|
+
lines.push(
|
|
1141
|
+
JSON.stringify(
|
|
1142
|
+
{
|
|
1143
|
+
schemaVersion: state.schemaVersion,
|
|
1144
|
+
core: state.core,
|
|
1145
|
+
artifacts: state.artifacts ?? {},
|
|
1146
|
+
executables: state.executables,
|
|
1147
|
+
history: state.history,
|
|
1148
|
+
...state.flow ? { flow: state.flow } : {}
|
|
1149
|
+
},
|
|
1150
|
+
null,
|
|
1151
|
+
2
|
|
1152
|
+
)
|
|
1153
|
+
);
|
|
1154
|
+
lines.push("```");
|
|
1155
|
+
lines.push("");
|
|
1156
|
+
lines.push(STATE_END);
|
|
1157
|
+
lines.push("");
|
|
1158
|
+
lines.push("</details>");
|
|
1149
1159
|
return lines.join("\n");
|
|
1150
1160
|
}
|
|
1151
1161
|
function readTaskState(target, number, cwd) {
|
|
@@ -1816,6 +1826,491 @@ function formatToolsUsage(profile) {
|
|
|
1816
1826
|
return lines.join("\n");
|
|
1817
1827
|
}
|
|
1818
1828
|
|
|
1829
|
+
// src/scripts/discoverQaContext.ts
|
|
1830
|
+
import * as fs13 from "fs";
|
|
1831
|
+
import * as path12 from "path";
|
|
1832
|
+
|
|
1833
|
+
// src/scripts/frameworkDetectors.ts
|
|
1834
|
+
import * as fs12 from "fs";
|
|
1835
|
+
import * as path11 from "path";
|
|
1836
|
+
function detectFrameworks(cwd) {
|
|
1837
|
+
const out = [];
|
|
1838
|
+
let deps = {};
|
|
1839
|
+
try {
|
|
1840
|
+
const pkg = JSON.parse(fs12.readFileSync(path11.join(cwd, "package.json"), "utf-8"));
|
|
1841
|
+
deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1842
|
+
} catch {
|
|
1843
|
+
return out;
|
|
1844
|
+
}
|
|
1845
|
+
if (deps.payload || deps["@payloadcms/next"]) {
|
|
1846
|
+
out.push({
|
|
1847
|
+
name: "payload-cms",
|
|
1848
|
+
version: deps.payload ?? deps["@payloadcms/next"] ?? null,
|
|
1849
|
+
configFile: findFile(cwd, ["payload.config.ts", "payload-config.ts", "src/payload.config.ts"])
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
if (deps["next-auth"]) {
|
|
1853
|
+
out.push({
|
|
1854
|
+
name: "nextauth",
|
|
1855
|
+
version: deps["next-auth"] ?? null,
|
|
1856
|
+
configFile: findFile(cwd, ["auth.ts", "auth.config.ts", "src/auth.ts", "src/auth.config.ts"])
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
if (deps.prisma || deps["@prisma/client"]) {
|
|
1860
|
+
out.push({
|
|
1861
|
+
name: "prisma",
|
|
1862
|
+
version: deps.prisma ?? deps["@prisma/client"] ?? null,
|
|
1863
|
+
configFile: findFile(cwd, ["prisma/schema.prisma"])
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
if (deps.next) {
|
|
1867
|
+
out.push({
|
|
1868
|
+
name: "nextjs",
|
|
1869
|
+
version: deps.next ?? null,
|
|
1870
|
+
configFile: findFile(cwd, ["next.config.ts", "next.config.mjs", "next.config.js"])
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
return out;
|
|
1874
|
+
}
|
|
1875
|
+
function findFile(cwd, candidates) {
|
|
1876
|
+
for (const c of candidates) {
|
|
1877
|
+
if (fs12.existsSync(path11.join(cwd, c))) return c;
|
|
1878
|
+
}
|
|
1879
|
+
return null;
|
|
1880
|
+
}
|
|
1881
|
+
var COLLECTION_DIRS = [
|
|
1882
|
+
"src/server/payload/collections",
|
|
1883
|
+
"src/payload/collections",
|
|
1884
|
+
"src/collections",
|
|
1885
|
+
"payload/collections"
|
|
1886
|
+
];
|
|
1887
|
+
function discoverPayloadCollections(cwd) {
|
|
1888
|
+
const out = [];
|
|
1889
|
+
for (const dir of COLLECTION_DIRS) {
|
|
1890
|
+
const full = path11.join(cwd, dir);
|
|
1891
|
+
if (!fs12.existsSync(full)) continue;
|
|
1892
|
+
let files;
|
|
1893
|
+
try {
|
|
1894
|
+
files = fs12.readdirSync(full).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
|
|
1895
|
+
} catch {
|
|
1896
|
+
continue;
|
|
1897
|
+
}
|
|
1898
|
+
for (const file of files) {
|
|
1899
|
+
try {
|
|
1900
|
+
const filePath = path11.join(full, file);
|
|
1901
|
+
const content = fs12.readFileSync(filePath, "utf-8").slice(0, 1e4);
|
|
1902
|
+
const slugMatch = content.match(/slug:\s*['"]([a-z0-9-]+)['"]/);
|
|
1903
|
+
if (!slugMatch) continue;
|
|
1904
|
+
const slug = slugMatch[1];
|
|
1905
|
+
const name = file.replace(/\.(ts|tsx)$/, "");
|
|
1906
|
+
const fields = [];
|
|
1907
|
+
const fieldMatches = content.matchAll(/name:\s*['"]([a-zA-Z_][a-zA-Z0-9_]*)['"]/g);
|
|
1908
|
+
for (const m of fieldMatches) {
|
|
1909
|
+
if (!fields.includes(m[1])) fields.push(m[1]);
|
|
1910
|
+
}
|
|
1911
|
+
const hasAdmin = /components:\s*\{/.test(content) || /Field:\s*['"]/.test(content) || /Cell:\s*['"]/.test(content) || /views:\s*\{/.test(content);
|
|
1912
|
+
out.push({
|
|
1913
|
+
name,
|
|
1914
|
+
slug,
|
|
1915
|
+
filePath: path11.relative(cwd, filePath),
|
|
1916
|
+
fields: fields.slice(0, 20),
|
|
1917
|
+
hasAdmin
|
|
1918
|
+
});
|
|
1919
|
+
} catch {
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
return out;
|
|
1924
|
+
}
|
|
1925
|
+
var ADMIN_COMPONENT_DIRS = ["src/ui/admin", "src/admin/components", "src/components/admin"];
|
|
1926
|
+
function discoverAdminComponents(cwd, collections) {
|
|
1927
|
+
const out = [];
|
|
1928
|
+
for (const dir of ADMIN_COMPONENT_DIRS) {
|
|
1929
|
+
const full = path11.join(cwd, dir);
|
|
1930
|
+
if (!fs12.existsSync(full)) continue;
|
|
1931
|
+
let entries;
|
|
1932
|
+
try {
|
|
1933
|
+
entries = fs12.readdirSync(full, { withFileTypes: true });
|
|
1934
|
+
} catch {
|
|
1935
|
+
continue;
|
|
1936
|
+
}
|
|
1937
|
+
for (const entry of entries) {
|
|
1938
|
+
const entryPath = path11.join(full, entry.name);
|
|
1939
|
+
let name;
|
|
1940
|
+
let filePath;
|
|
1941
|
+
if (entry.isDirectory()) {
|
|
1942
|
+
const indexFile = ["index.tsx", "index.ts", "index.jsx", "index.js"].find(
|
|
1943
|
+
(f) => fs12.existsSync(path11.join(entryPath, f))
|
|
1944
|
+
);
|
|
1945
|
+
if (!indexFile) continue;
|
|
1946
|
+
name = entry.name;
|
|
1947
|
+
filePath = path11.relative(cwd, path11.join(entryPath, indexFile));
|
|
1948
|
+
} else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
|
|
1949
|
+
name = entry.name.replace(/\.(tsx?|jsx?)$/, "");
|
|
1950
|
+
filePath = path11.relative(cwd, entryPath);
|
|
1951
|
+
} else {
|
|
1952
|
+
continue;
|
|
1953
|
+
}
|
|
1954
|
+
let usedInCollection = null;
|
|
1955
|
+
if (collections) {
|
|
1956
|
+
for (const col of collections) {
|
|
1957
|
+
try {
|
|
1958
|
+
const colContent = fs12.readFileSync(path11.join(cwd, col.filePath), "utf-8");
|
|
1959
|
+
if (colContent.includes(name)) {
|
|
1960
|
+
usedInCollection = col.slug;
|
|
1961
|
+
break;
|
|
1962
|
+
}
|
|
1963
|
+
} catch {
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
out.push({ name, filePath, usedInCollection });
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
return out;
|
|
1971
|
+
}
|
|
1972
|
+
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
1973
|
+
function scanApiRoutes(cwd) {
|
|
1974
|
+
const out = [];
|
|
1975
|
+
const appDirs = ["src/app", "app"];
|
|
1976
|
+
for (const appDir of appDirs) {
|
|
1977
|
+
const apiDir = path11.join(cwd, appDir, "api");
|
|
1978
|
+
if (!fs12.existsSync(apiDir)) continue;
|
|
1979
|
+
walkApiRoutes(apiDir, "/api", cwd, out);
|
|
1980
|
+
break;
|
|
1981
|
+
}
|
|
1982
|
+
return out;
|
|
1983
|
+
}
|
|
1984
|
+
function walkApiRoutes(dir, prefix, cwd, out) {
|
|
1985
|
+
let entries;
|
|
1986
|
+
try {
|
|
1987
|
+
entries = fs12.readdirSync(dir, { withFileTypes: true });
|
|
1988
|
+
} catch {
|
|
1989
|
+
return;
|
|
1990
|
+
}
|
|
1991
|
+
const routeFile = entries.find((e) => e.isFile() && /^route\.(ts|js|tsx|jsx)$/.test(e.name));
|
|
1992
|
+
if (routeFile) {
|
|
1993
|
+
try {
|
|
1994
|
+
const content = fs12.readFileSync(path11.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
|
|
1995
|
+
const methods = HTTP_METHODS.filter((m) => new RegExp(`export\\s+(?:async\\s+)?function\\s+${m}\\b`).test(content));
|
|
1996
|
+
if (methods.length > 0) {
|
|
1997
|
+
out.push({
|
|
1998
|
+
path: prefix,
|
|
1999
|
+
methods,
|
|
2000
|
+
filePath: path11.relative(cwd, path11.join(dir, routeFile.name))
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
} catch {
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
for (const entry of entries) {
|
|
2007
|
+
if (!entry.isDirectory()) continue;
|
|
2008
|
+
if (entry.name === "node_modules" || entry.name === ".next") continue;
|
|
2009
|
+
let segment = entry.name;
|
|
2010
|
+
if (segment.startsWith("(") && segment.endsWith(")")) {
|
|
2011
|
+
walkApiRoutes(path11.join(dir, entry.name), prefix, cwd, out);
|
|
2012
|
+
continue;
|
|
2013
|
+
}
|
|
2014
|
+
if (segment.startsWith("[[") && segment.endsWith("]]")) {
|
|
2015
|
+
segment = `:${segment.slice(2, -2)}?`;
|
|
2016
|
+
} else if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
2017
|
+
segment = `:${segment.slice(1, -1)}`;
|
|
2018
|
+
}
|
|
2019
|
+
walkApiRoutes(path11.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
|
|
2023
|
+
"NODE_ENV",
|
|
2024
|
+
"HOME",
|
|
2025
|
+
"PATH",
|
|
2026
|
+
"USER",
|
|
2027
|
+
"SHELL",
|
|
2028
|
+
"TERM",
|
|
2029
|
+
"LANG",
|
|
2030
|
+
"PWD",
|
|
2031
|
+
"HOSTNAME",
|
|
2032
|
+
"PORT",
|
|
2033
|
+
"CI",
|
|
2034
|
+
"GITHUB_ACTIONS"
|
|
2035
|
+
]);
|
|
2036
|
+
function scanEnvVars(cwd) {
|
|
2037
|
+
const candidates = [".env.example", ".env.local.example", ".env.template"];
|
|
2038
|
+
for (const envFile of candidates) {
|
|
2039
|
+
const envPath = path11.join(cwd, envFile);
|
|
2040
|
+
if (!fs12.existsSync(envPath)) continue;
|
|
2041
|
+
try {
|
|
2042
|
+
const content = fs12.readFileSync(envPath, "utf-8");
|
|
2043
|
+
const vars = [];
|
|
2044
|
+
for (const line of content.split("\n")) {
|
|
2045
|
+
const trimmed = line.trim();
|
|
2046
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2047
|
+
const match = trimmed.match(/^([A-Z][A-Z0-9_]*)=/);
|
|
2048
|
+
if (match && !BUILTIN_ENV_VARS.has(match[1])) vars.push(match[1]);
|
|
2049
|
+
}
|
|
2050
|
+
return vars;
|
|
2051
|
+
} catch {
|
|
2052
|
+
return [];
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
return [];
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
// src/scripts/discoverQaContext.ts
|
|
2059
|
+
var MAX_SERIALIZED_LENGTH = 8e3;
|
|
2060
|
+
function runQaDiscovery(cwd) {
|
|
2061
|
+
const out = {
|
|
2062
|
+
routes: [],
|
|
2063
|
+
authFiles: [],
|
|
2064
|
+
loginPage: null,
|
|
2065
|
+
adminPath: null,
|
|
2066
|
+
roles: [],
|
|
2067
|
+
devCommand: "",
|
|
2068
|
+
devPort: 3e3,
|
|
2069
|
+
frameworks: [],
|
|
2070
|
+
collections: [],
|
|
2071
|
+
adminComponents: [],
|
|
2072
|
+
apiRoutes: [],
|
|
2073
|
+
envVars: []
|
|
2074
|
+
};
|
|
2075
|
+
detectDevServer(cwd, out);
|
|
2076
|
+
scanFrontendRoutes(cwd, out);
|
|
2077
|
+
detectAuthFiles(cwd, out);
|
|
2078
|
+
detectRoles(cwd, out);
|
|
2079
|
+
out.frameworks = detectFrameworks(cwd);
|
|
2080
|
+
const hasPayload = out.frameworks.some((f) => f.name === "payload-cms");
|
|
2081
|
+
if (hasPayload) out.collections = discoverPayloadCollections(cwd);
|
|
2082
|
+
out.adminComponents = discoverAdminComponents(cwd, out.collections.length > 0 ? out.collections : void 0);
|
|
2083
|
+
out.apiRoutes = scanApiRoutes(cwd);
|
|
2084
|
+
out.envVars = scanEnvVars(cwd);
|
|
2085
|
+
if (hasPayload && !out.adminPath) out.adminPath = "/admin";
|
|
2086
|
+
return out;
|
|
2087
|
+
}
|
|
2088
|
+
function detectDevServer(cwd, out) {
|
|
2089
|
+
try {
|
|
2090
|
+
const pkg = JSON.parse(fs13.readFileSync(path12.join(cwd, "package.json"), "utf-8"));
|
|
2091
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2092
|
+
const pm = fs13.existsSync(path12.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs13.existsSync(path12.join(cwd, "yarn.lock")) ? "yarn" : fs13.existsSync(path12.join(cwd, "bun.lockb")) ? "bun" : "npm";
|
|
2093
|
+
if (pkg.scripts?.dev) out.devCommand = `${pm} dev`;
|
|
2094
|
+
if (allDeps.next || allDeps.nuxt) out.devPort = 3e3;
|
|
2095
|
+
else if (allDeps.vite) out.devPort = 5173;
|
|
2096
|
+
} catch {
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
function scanFrontendRoutes(cwd, out) {
|
|
2100
|
+
const appDirs = ["src/app", "app"];
|
|
2101
|
+
for (const appDir of appDirs) {
|
|
2102
|
+
const full = path12.join(cwd, appDir);
|
|
2103
|
+
if (!fs13.existsSync(full)) continue;
|
|
2104
|
+
walkFrontendRoutes(full, "", out);
|
|
2105
|
+
break;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
function walkFrontendRoutes(dir, prefix, out) {
|
|
2109
|
+
let entries;
|
|
2110
|
+
try {
|
|
2111
|
+
entries = fs13.readdirSync(dir, { withFileTypes: true });
|
|
2112
|
+
} catch {
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
2115
|
+
const hasPage = entries.some((e) => e.isFile() && /^page\.(tsx?|jsx?)$/.test(e.name));
|
|
2116
|
+
if (hasPage) {
|
|
2117
|
+
const routePath = prefix || "/";
|
|
2118
|
+
const group = prefix.startsWith("/admin") ? "admin" : prefix.includes("/login") || prefix.includes("/signup") ? "auth" : prefix.includes("/api") ? "api" : "frontend";
|
|
2119
|
+
out.routes.push({ path: routePath, group });
|
|
2120
|
+
if (prefix.includes("/login") && !out.loginPage) out.loginPage = routePath;
|
|
2121
|
+
if (prefix.startsWith("/admin") && !out.adminPath) out.adminPath = prefix;
|
|
2122
|
+
}
|
|
2123
|
+
for (const entry of entries) {
|
|
2124
|
+
if (!entry.isDirectory()) continue;
|
|
2125
|
+
if (entry.name === "node_modules" || entry.name === ".next") continue;
|
|
2126
|
+
let segment = entry.name;
|
|
2127
|
+
if (segment.startsWith("(") && segment.endsWith(")")) {
|
|
2128
|
+
walkFrontendRoutes(path12.join(dir, entry.name), prefix, out);
|
|
2129
|
+
continue;
|
|
2130
|
+
}
|
|
2131
|
+
if (segment.startsWith("[[") && segment.endsWith("]]")) {
|
|
2132
|
+
segment = `:${segment.slice(2, -2)}?`;
|
|
2133
|
+
} else if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
2134
|
+
segment = `:${segment.slice(1, -1)}`;
|
|
2135
|
+
}
|
|
2136
|
+
walkFrontendRoutes(path12.join(dir, entry.name), `${prefix}/${segment}`, out);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
function detectAuthFiles(cwd, out) {
|
|
2140
|
+
const candidates = [
|
|
2141
|
+
"middleware.ts",
|
|
2142
|
+
"middleware.js",
|
|
2143
|
+
"src/middleware.ts",
|
|
2144
|
+
"src/middleware.js",
|
|
2145
|
+
"src/app/api/auth",
|
|
2146
|
+
"src/auth",
|
|
2147
|
+
"src/lib/auth",
|
|
2148
|
+
"auth.config.ts",
|
|
2149
|
+
"auth.ts",
|
|
2150
|
+
"src/app/api/oauth"
|
|
2151
|
+
];
|
|
2152
|
+
for (const c of candidates) {
|
|
2153
|
+
if (fs13.existsSync(path12.join(cwd, c))) out.authFiles.push(c);
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
function detectRoles(cwd, out) {
|
|
2157
|
+
const rolePaths = ["src/types", "src/lib", "src/utils", "src/constants", "src/access", "src/collections"];
|
|
2158
|
+
for (const rp of rolePaths) {
|
|
2159
|
+
const dir = path12.join(cwd, rp);
|
|
2160
|
+
if (!fs13.existsSync(dir)) continue;
|
|
2161
|
+
let files;
|
|
2162
|
+
try {
|
|
2163
|
+
files = fs13.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
|
|
2164
|
+
} catch {
|
|
2165
|
+
continue;
|
|
2166
|
+
}
|
|
2167
|
+
for (const f of files) {
|
|
2168
|
+
try {
|
|
2169
|
+
const content = fs13.readFileSync(path12.join(dir, f), "utf-8").slice(0, 5e3);
|
|
2170
|
+
const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
|
|
2171
|
+
if (roleMatches) {
|
|
2172
|
+
for (const m of roleMatches) {
|
|
2173
|
+
const val = m.match(/['"](\w+)['"]/);
|
|
2174
|
+
if (val && !out.roles.includes(val[1])) out.roles.push(val[1]);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
const enumMatch = content.match(/(?:enum|type)\s+\w*[Rr]ole\w*\s*[={]([^}]+)/s);
|
|
2178
|
+
if (enumMatch) {
|
|
2179
|
+
const vals = enumMatch[1].match(/['"](\w+)['"]/g);
|
|
2180
|
+
if (vals) {
|
|
2181
|
+
for (const v of vals) {
|
|
2182
|
+
const clean = v.replace(/['"]/g, "");
|
|
2183
|
+
if (!out.roles.includes(clean)) out.roles.push(clean);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
} catch {
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
function serializeDiscoveryForLLM(d) {
|
|
2193
|
+
const sections = [];
|
|
2194
|
+
sections.push(`Dev server: ${d.devCommand || "pnpm dev"} at http://localhost:${d.devPort}`);
|
|
2195
|
+
if (d.loginPage) sections.push(`Login page: ${d.loginPage}`);
|
|
2196
|
+
if (d.adminPath) sections.push(`Admin panel: ${d.adminPath}`);
|
|
2197
|
+
if (d.roles.length > 0) sections.push(`Roles: ${d.roles.join(", ")}`);
|
|
2198
|
+
if (d.frameworks.length > 0) {
|
|
2199
|
+
sections.push(
|
|
2200
|
+
`
|
|
2201
|
+
Frameworks: ${d.frameworks.map((f) => `${f.name}${f.version ? ` (${f.version})` : ""}`).join(", ")}`
|
|
2202
|
+
);
|
|
2203
|
+
}
|
|
2204
|
+
if (d.collections.length > 0) {
|
|
2205
|
+
sections.push("\nCollections (Payload CMS):");
|
|
2206
|
+
for (const col of d.collections.slice(0, 15)) {
|
|
2207
|
+
const fields = col.fields.slice(0, 10).join(", ");
|
|
2208
|
+
let line = `- ${col.slug}: fields=[${fields}]`;
|
|
2209
|
+
if (col.hasAdmin) line += " (has custom admin components)";
|
|
2210
|
+
line += ` \u2014 ${col.filePath}`;
|
|
2211
|
+
sections.push(line);
|
|
2212
|
+
}
|
|
2213
|
+
if (d.collections.length > 15) sections.push(`- ... and ${d.collections.length - 15} more collections`);
|
|
2214
|
+
}
|
|
2215
|
+
if (d.adminComponents.length > 0) {
|
|
2216
|
+
sections.push("\nCustom Admin Components:");
|
|
2217
|
+
for (const comp of d.adminComponents.slice(0, 10)) {
|
|
2218
|
+
let line = `- ${comp.name} (${comp.filePath})`;
|
|
2219
|
+
if (comp.usedInCollection) line += ` \u2192 used in "${comp.usedInCollection}" collection`;
|
|
2220
|
+
sections.push(line);
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
if (d.apiRoutes.length > 0) {
|
|
2224
|
+
sections.push("\nAPI Routes:");
|
|
2225
|
+
for (const route of d.apiRoutes.slice(0, 20)) {
|
|
2226
|
+
sections.push(`- ${route.methods.join("/")} ${route.path} \u2014 ${route.filePath}`);
|
|
2227
|
+
}
|
|
2228
|
+
if (d.apiRoutes.length > 20) sections.push(`- ... and ${d.apiRoutes.length - 20} more routes`);
|
|
2229
|
+
}
|
|
2230
|
+
if (d.routes.length > 0) {
|
|
2231
|
+
sections.push("\nFrontend Routes:");
|
|
2232
|
+
for (const route of d.routes.slice(0, 30)) {
|
|
2233
|
+
sections.push(`- [${route.group}] ${route.path}`);
|
|
2234
|
+
}
|
|
2235
|
+
if (d.routes.length > 30) sections.push(`- ... and ${d.routes.length - 30} more routes`);
|
|
2236
|
+
}
|
|
2237
|
+
if (d.envVars.length > 0) sections.push(`
|
|
2238
|
+
Required env vars: ${d.envVars.join(", ")}`);
|
|
2239
|
+
let result = sections.join("\n");
|
|
2240
|
+
if (result.length > MAX_SERIALIZED_LENGTH) {
|
|
2241
|
+
const cutoff = result.lastIndexOf("\n", MAX_SERIALIZED_LENGTH - 20);
|
|
2242
|
+
result = result.slice(0, cutoff > 0 ? cutoff : MAX_SERIALIZED_LENGTH - 20) + "\n... (truncated)";
|
|
2243
|
+
}
|
|
2244
|
+
return result;
|
|
2245
|
+
}
|
|
2246
|
+
function generateQaGuideTemplate(d) {
|
|
2247
|
+
const lines = [];
|
|
2248
|
+
lines.push("# QA guide");
|
|
2249
|
+
lines.push("");
|
|
2250
|
+
lines.push("This file is read by `kody2 ui-review`. Fill in the credential placeholders");
|
|
2251
|
+
lines.push("below and commit \u2014 the agent uses them to log in to your preview deployment.");
|
|
2252
|
+
lines.push("");
|
|
2253
|
+
lines.push("## Test accounts");
|
|
2254
|
+
lines.push("");
|
|
2255
|
+
lines.push("<!-- Replace CHANGE_ME with real credentials for your preview environment.");
|
|
2256
|
+
lines.push(" Remove any role row you don't have an account for. -->");
|
|
2257
|
+
lines.push("");
|
|
2258
|
+
lines.push("| Role | Email | Password |");
|
|
2259
|
+
lines.push("|------|-------|----------|");
|
|
2260
|
+
if (d.roles.length > 0) {
|
|
2261
|
+
for (const role of d.roles) {
|
|
2262
|
+
lines.push(`| ${role} | CHANGE_ME | CHANGE_ME |`);
|
|
2263
|
+
}
|
|
2264
|
+
} else {
|
|
2265
|
+
lines.push("| admin | admin@example.com | CHANGE_ME |");
|
|
2266
|
+
lines.push("| user | user@example.com | CHANGE_ME |");
|
|
2267
|
+
}
|
|
2268
|
+
lines.push("");
|
|
2269
|
+
lines.push("## Login");
|
|
2270
|
+
lines.push("");
|
|
2271
|
+
lines.push(`- Login page: \`${d.loginPage ?? "/login"}\``);
|
|
2272
|
+
if (d.adminPath) lines.push(`- Admin panel: \`${d.adminPath}\``);
|
|
2273
|
+
lines.push("");
|
|
2274
|
+
lines.push("### Steps");
|
|
2275
|
+
lines.push(`1. Navigate to \`${d.loginPage ?? "/login"}\``);
|
|
2276
|
+
lines.push("2. Enter credentials from the table above");
|
|
2277
|
+
lines.push("3. Submit the login form");
|
|
2278
|
+
lines.push("4. Verify the redirect lands on the expected page");
|
|
2279
|
+
lines.push("");
|
|
2280
|
+
if (d.roles.length > 0) {
|
|
2281
|
+
lines.push("## Roles");
|
|
2282
|
+
lines.push("");
|
|
2283
|
+
for (const role of d.roles) lines.push(`- \`${role}\``);
|
|
2284
|
+
lines.push("");
|
|
2285
|
+
}
|
|
2286
|
+
if (d.routes.length > 0) {
|
|
2287
|
+
lines.push("## Key pages");
|
|
2288
|
+
lines.push("");
|
|
2289
|
+
const groups = {};
|
|
2290
|
+
for (const r of d.routes) {
|
|
2291
|
+
if (!groups[r.group]) groups[r.group] = [];
|
|
2292
|
+
groups[r.group].push(r.path);
|
|
2293
|
+
}
|
|
2294
|
+
for (const [group, routes] of Object.entries(groups)) {
|
|
2295
|
+
lines.push(`### ${group[0].toUpperCase()}${group.slice(1)}`);
|
|
2296
|
+
for (const r of routes.slice(0, 15).sort()) lines.push(`- \`${r}\``);
|
|
2297
|
+
if (routes.length > 15) lines.push(`- \u2026 and ${routes.length - 15} more`);
|
|
2298
|
+
lines.push("");
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
lines.push("## Notes for the reviewer");
|
|
2302
|
+
lines.push("");
|
|
2303
|
+
lines.push("<!-- Add any repo-specific quirks the UI-review agent should know:");
|
|
2304
|
+
lines.push(" seed data assumptions, feature flags, preview-only behaviors, etc. -->");
|
|
2305
|
+
lines.push("");
|
|
2306
|
+
return lines.join("\n");
|
|
2307
|
+
}
|
|
2308
|
+
var discoverQaContext = async (ctx) => {
|
|
2309
|
+
const discovery = runQaDiscovery(ctx.cwd);
|
|
2310
|
+
ctx.data.qaDiscovery = discovery;
|
|
2311
|
+
ctx.data.qaContext = serializeDiscoveryForLLM(discovery);
|
|
2312
|
+
};
|
|
2313
|
+
|
|
1819
2314
|
// src/scripts/dispatch.ts
|
|
1820
2315
|
import { execFileSync as execFileSync7 } from "child_process";
|
|
1821
2316
|
var API_TIMEOUT_MS3 = 3e4;
|
|
@@ -2293,7 +2788,7 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd) {
|
|
|
2293
2788
|
|
|
2294
2789
|
// src/gha.ts
|
|
2295
2790
|
import { execFileSync as execFileSync11 } from "child_process";
|
|
2296
|
-
import * as
|
|
2791
|
+
import * as fs14 from "fs";
|
|
2297
2792
|
function getRunUrl() {
|
|
2298
2793
|
const server = process.env.GITHUB_SERVER_URL;
|
|
2299
2794
|
const repo = process.env.GITHUB_REPOSITORY;
|
|
@@ -2304,10 +2799,10 @@ function getRunUrl() {
|
|
|
2304
2799
|
function reactToTriggerComment(cwd) {
|
|
2305
2800
|
if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
|
|
2306
2801
|
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
2307
|
-
if (!eventPath || !
|
|
2802
|
+
if (!eventPath || !fs14.existsSync(eventPath)) return;
|
|
2308
2803
|
let event = null;
|
|
2309
2804
|
try {
|
|
2310
|
-
event = JSON.parse(
|
|
2805
|
+
event = JSON.parse(fs14.readFileSync(eventPath, "utf-8"));
|
|
2311
2806
|
} catch {
|
|
2312
2807
|
return;
|
|
2313
2808
|
}
|
|
@@ -2533,35 +3028,35 @@ function tryPostPr2(prNumber, body, cwd) {
|
|
|
2533
3028
|
|
|
2534
3029
|
// src/scripts/initFlow.ts
|
|
2535
3030
|
import { execFileSync as execFileSync13 } from "child_process";
|
|
2536
|
-
import * as
|
|
2537
|
-
import * as
|
|
3031
|
+
import * as fs17 from "fs";
|
|
3032
|
+
import * as path15 from "path";
|
|
2538
3033
|
|
|
2539
3034
|
// src/registry.ts
|
|
2540
|
-
import * as
|
|
2541
|
-
import * as
|
|
3035
|
+
import * as fs15 from "fs";
|
|
3036
|
+
import * as path13 from "path";
|
|
2542
3037
|
function getExecutablesRoot() {
|
|
2543
|
-
const here =
|
|
3038
|
+
const here = path13.dirname(new URL(import.meta.url).pathname);
|
|
2544
3039
|
const candidates = [
|
|
2545
|
-
|
|
3040
|
+
path13.join(here, "executables"),
|
|
2546
3041
|
// dev: src/
|
|
2547
|
-
|
|
3042
|
+
path13.join(here, "..", "executables"),
|
|
2548
3043
|
// built: dist/bin → dist/executables
|
|
2549
|
-
|
|
3044
|
+
path13.join(here, "..", "src", "executables")
|
|
2550
3045
|
// fallback
|
|
2551
3046
|
];
|
|
2552
3047
|
for (const c of candidates) {
|
|
2553
|
-
if (
|
|
3048
|
+
if (fs15.existsSync(c) && fs15.statSync(c).isDirectory()) return c;
|
|
2554
3049
|
}
|
|
2555
3050
|
return candidates[0];
|
|
2556
3051
|
}
|
|
2557
3052
|
function listExecutables(root = getExecutablesRoot()) {
|
|
2558
|
-
if (!
|
|
2559
|
-
const entries =
|
|
3053
|
+
if (!fs15.existsSync(root)) return [];
|
|
3054
|
+
const entries = fs15.readdirSync(root, { withFileTypes: true });
|
|
2560
3055
|
const out = [];
|
|
2561
3056
|
for (const ent of entries) {
|
|
2562
3057
|
if (!ent.isDirectory()) continue;
|
|
2563
|
-
const profilePath =
|
|
2564
|
-
if (
|
|
3058
|
+
const profilePath = path13.join(root, ent.name, "profile.json");
|
|
3059
|
+
if (fs15.existsSync(profilePath) && fs15.statSync(profilePath).isFile()) {
|
|
2565
3060
|
out.push({ name: ent.name, profilePath });
|
|
2566
3061
|
}
|
|
2567
3062
|
}
|
|
@@ -2569,8 +3064,8 @@ function listExecutables(root = getExecutablesRoot()) {
|
|
|
2569
3064
|
}
|
|
2570
3065
|
function hasExecutable(name, root = getExecutablesRoot()) {
|
|
2571
3066
|
if (!isSafeName(name)) return false;
|
|
2572
|
-
const profilePath =
|
|
2573
|
-
return
|
|
3067
|
+
const profilePath = path13.join(root, name, "profile.json");
|
|
3068
|
+
return fs15.existsSync(profilePath) && fs15.statSync(profilePath).isFile();
|
|
2574
3069
|
}
|
|
2575
3070
|
function isSafeName(name) {
|
|
2576
3071
|
return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
|
|
@@ -2597,11 +3092,31 @@ function parseGenericFlags(argv) {
|
|
|
2597
3092
|
return args;
|
|
2598
3093
|
}
|
|
2599
3094
|
|
|
3095
|
+
// src/scripts/loadQaGuide.ts
|
|
3096
|
+
import * as fs16 from "fs";
|
|
3097
|
+
import * as path14 from "path";
|
|
3098
|
+
var QA_GUIDE_REL_PATH = ".kody2/qa-guide.md";
|
|
3099
|
+
var loadQaGuide = async (ctx) => {
|
|
3100
|
+
const full = path14.join(ctx.cwd, QA_GUIDE_REL_PATH);
|
|
3101
|
+
if (!fs16.existsSync(full)) {
|
|
3102
|
+
ctx.data.qaGuide = "";
|
|
3103
|
+
ctx.data.qaGuidePath = "";
|
|
3104
|
+
return;
|
|
3105
|
+
}
|
|
3106
|
+
try {
|
|
3107
|
+
ctx.data.qaGuide = fs16.readFileSync(full, "utf-8");
|
|
3108
|
+
ctx.data.qaGuidePath = QA_GUIDE_REL_PATH;
|
|
3109
|
+
} catch {
|
|
3110
|
+
ctx.data.qaGuide = "";
|
|
3111
|
+
ctx.data.qaGuidePath = "";
|
|
3112
|
+
}
|
|
3113
|
+
};
|
|
3114
|
+
|
|
2600
3115
|
// src/scripts/initFlow.ts
|
|
2601
3116
|
function detectPackageManager(cwd) {
|
|
2602
|
-
if (
|
|
2603
|
-
if (
|
|
2604
|
-
if (
|
|
3117
|
+
if (fs17.existsSync(path15.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
3118
|
+
if (fs17.existsSync(path15.join(cwd, "yarn.lock"))) return "yarn";
|
|
3119
|
+
if (fs17.existsSync(path15.join(cwd, "bun.lockb"))) return "bun";
|
|
2605
3120
|
return "npm";
|
|
2606
3121
|
}
|
|
2607
3122
|
function qualityCommandsFor(pm) {
|
|
@@ -2722,24 +3237,36 @@ function performInit(cwd, force) {
|
|
|
2722
3237
|
const pm = detectPackageManager(cwd);
|
|
2723
3238
|
const ownerRepo = detectOwnerRepo(cwd);
|
|
2724
3239
|
const defaultBranch = defaultBranchFromGit(cwd);
|
|
2725
|
-
const configPath =
|
|
2726
|
-
if (
|
|
3240
|
+
const configPath = path15.join(cwd, "kody.config.json");
|
|
3241
|
+
if (fs17.existsSync(configPath) && !force) {
|
|
2727
3242
|
skipped.push("kody.config.json");
|
|
2728
3243
|
} else {
|
|
2729
3244
|
const cfg = makeConfig(pm, ownerRepo, defaultBranch);
|
|
2730
|
-
|
|
3245
|
+
fs17.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
|
|
2731
3246
|
`);
|
|
2732
3247
|
wrote.push("kody.config.json");
|
|
2733
3248
|
}
|
|
2734
|
-
const workflowDir =
|
|
2735
|
-
const workflowPath =
|
|
2736
|
-
if (
|
|
3249
|
+
const workflowDir = path15.join(cwd, ".github", "workflows");
|
|
3250
|
+
const workflowPath = path15.join(workflowDir, "kody2.yml");
|
|
3251
|
+
if (fs17.existsSync(workflowPath) && !force) {
|
|
2737
3252
|
skipped.push(".github/workflows/kody2.yml");
|
|
2738
3253
|
} else {
|
|
2739
|
-
|
|
2740
|
-
|
|
3254
|
+
fs17.mkdirSync(workflowDir, { recursive: true });
|
|
3255
|
+
fs17.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
|
|
2741
3256
|
wrote.push(".github/workflows/kody2.yml");
|
|
2742
3257
|
}
|
|
3258
|
+
const hasUi = fs17.existsSync(path15.join(cwd, "src/app")) || fs17.existsSync(path15.join(cwd, "app")) || fs17.existsSync(path15.join(cwd, "pages"));
|
|
3259
|
+
if (hasUi) {
|
|
3260
|
+
const qaGuidePath = path15.join(cwd, QA_GUIDE_REL_PATH);
|
|
3261
|
+
if (fs17.existsSync(qaGuidePath) && !force) {
|
|
3262
|
+
skipped.push(QA_GUIDE_REL_PATH);
|
|
3263
|
+
} else {
|
|
3264
|
+
fs17.mkdirSync(path15.dirname(qaGuidePath), { recursive: true });
|
|
3265
|
+
const discovery = runQaDiscovery(cwd);
|
|
3266
|
+
fs17.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
|
|
3267
|
+
wrote.push(QA_GUIDE_REL_PATH);
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
2743
3270
|
for (const exe of listExecutables()) {
|
|
2744
3271
|
let profile;
|
|
2745
3272
|
try {
|
|
@@ -2748,12 +3275,12 @@ function performInit(cwd, force) {
|
|
|
2748
3275
|
continue;
|
|
2749
3276
|
}
|
|
2750
3277
|
if (profile.kind !== "scheduled" || !profile.schedule) continue;
|
|
2751
|
-
const target =
|
|
2752
|
-
if (
|
|
3278
|
+
const target = path15.join(workflowDir, `kody2-${exe.name}.yml`);
|
|
3279
|
+
if (fs17.existsSync(target) && !force) {
|
|
2753
3280
|
skipped.push(`.github/workflows/kody2-${exe.name}.yml`);
|
|
2754
3281
|
continue;
|
|
2755
3282
|
}
|
|
2756
|
-
|
|
3283
|
+
fs17.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
|
|
2757
3284
|
wrote.push(`.github/workflows/kody2-${exe.name}.yml`);
|
|
2758
3285
|
}
|
|
2759
3286
|
return { wrote, skipped };
|
|
@@ -3128,8 +3655,8 @@ REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.
|
|
|
3128
3655
|
|
|
3129
3656
|
// src/scripts/releaseFlow.ts
|
|
3130
3657
|
import { execFileSync as execFileSync14, spawnSync } from "child_process";
|
|
3131
|
-
import * as
|
|
3132
|
-
import * as
|
|
3658
|
+
import * as fs18 from "fs";
|
|
3659
|
+
import * as path16 from "path";
|
|
3133
3660
|
function bumpVersion(current, bump) {
|
|
3134
3661
|
const m = current.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
|
|
3135
3662
|
if (!m) throw new Error(`cannot parse version '${current}' (expected x.y.z[-suffix])`);
|
|
@@ -3145,12 +3672,12 @@ function bumpVersion(current, bump) {
|
|
|
3145
3672
|
return `${major}.${minor}.${patch}`;
|
|
3146
3673
|
}
|
|
3147
3674
|
function updateVersionInFile(file, newVersion, cwd) {
|
|
3148
|
-
const abs =
|
|
3149
|
-
if (!
|
|
3150
|
-
const content =
|
|
3675
|
+
const abs = path16.join(cwd, file);
|
|
3676
|
+
if (!fs18.existsSync(abs)) return false;
|
|
3677
|
+
const content = fs18.readFileSync(abs, "utf-8");
|
|
3151
3678
|
const updated = content.replace(/"version"\s*:\s*"[^"]+"/, `"version": "${newVersion}"`);
|
|
3152
3679
|
if (updated === content) return false;
|
|
3153
|
-
|
|
3680
|
+
fs18.writeFileSync(abs, updated);
|
|
3154
3681
|
return true;
|
|
3155
3682
|
}
|
|
3156
3683
|
function generateChangelog(cwd, newVersion, lastTag) {
|
|
@@ -3198,19 +3725,19 @@ function generateChangelog(cwd, newVersion, lastTag) {
|
|
|
3198
3725
|
return parts.join("\n");
|
|
3199
3726
|
}
|
|
3200
3727
|
function prependChangelog(cwd, entry) {
|
|
3201
|
-
const p =
|
|
3728
|
+
const p = path16.join(cwd, "CHANGELOG.md");
|
|
3202
3729
|
const header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n";
|
|
3203
|
-
if (
|
|
3204
|
-
const prior =
|
|
3730
|
+
if (fs18.existsSync(p)) {
|
|
3731
|
+
const prior = fs18.readFileSync(p, "utf-8");
|
|
3205
3732
|
if (/^#\s*Changelog\b/m.test(prior)) {
|
|
3206
3733
|
const idx = prior.indexOf("\n", prior.indexOf("# Changelog"));
|
|
3207
|
-
|
|
3734
|
+
fs18.writeFileSync(p, `${prior.slice(0, idx + 1)}
|
|
3208
3735
|
${entry}${prior.slice(idx + 1)}`);
|
|
3209
3736
|
} else {
|
|
3210
|
-
|
|
3737
|
+
fs18.writeFileSync(p, `${header}${entry}${prior}`);
|
|
3211
3738
|
}
|
|
3212
3739
|
} else {
|
|
3213
|
-
|
|
3740
|
+
fs18.writeFileSync(p, `${header}${entry}`);
|
|
3214
3741
|
}
|
|
3215
3742
|
}
|
|
3216
3743
|
function git3(args, cwd, timeout = 6e4) {
|
|
@@ -3261,13 +3788,13 @@ var releaseFlow = async (ctx) => {
|
|
|
3261
3788
|
};
|
|
3262
3789
|
async function runPrepare(args) {
|
|
3263
3790
|
const { cwd, bump, dryRun, versionFiles, ctx } = args;
|
|
3264
|
-
const pkgPath =
|
|
3265
|
-
if (!
|
|
3791
|
+
const pkgPath = path16.join(cwd, "package.json");
|
|
3792
|
+
if (!fs18.existsSync(pkgPath)) {
|
|
3266
3793
|
ctx.output.exitCode = 99;
|
|
3267
3794
|
ctx.output.reason = "release prepare: package.json not found";
|
|
3268
3795
|
return;
|
|
3269
3796
|
}
|
|
3270
|
-
const pkg = JSON.parse(
|
|
3797
|
+
const pkg = JSON.parse(fs18.readFileSync(pkgPath, "utf-8"));
|
|
3271
3798
|
if (typeof pkg.version !== "string") {
|
|
3272
3799
|
ctx.output.exitCode = 99;
|
|
3273
3800
|
ctx.output.reason = "release prepare: package.json has no version";
|
|
@@ -3338,8 +3865,8 @@ Merge this and then run \`kody2 release --mode finalize\`.`;
|
|
|
3338
3865
|
}
|
|
3339
3866
|
async function runFinalize(args) {
|
|
3340
3867
|
const { cwd, dryRun, timeoutMs, releaseCfg, ctx } = args;
|
|
3341
|
-
const pkgPath =
|
|
3342
|
-
const pkg = JSON.parse(
|
|
3868
|
+
const pkgPath = path16.join(cwd, "package.json");
|
|
3869
|
+
const pkg = JSON.parse(fs18.readFileSync(pkgPath, "utf-8"));
|
|
3343
3870
|
if (typeof pkg.version !== "string") {
|
|
3344
3871
|
ctx.output.exitCode = 99;
|
|
3345
3872
|
ctx.output.reason = "release finalize: package.json has no version";
|
|
@@ -3606,6 +4133,25 @@ function tryPostPr3(prNumber, body, cwd) {
|
|
|
3606
4133
|
}
|
|
3607
4134
|
}
|
|
3608
4135
|
|
|
4136
|
+
// src/scripts/resolvePreviewUrl.ts
|
|
4137
|
+
var DEFAULT_PREVIEW_URL = "http://localhost:3000";
|
|
4138
|
+
var resolvePreviewUrl = async (ctx) => {
|
|
4139
|
+
const fromFlag = typeof ctx.args.previewUrl === "string" ? ctx.args.previewUrl.trim() : "";
|
|
4140
|
+
if (fromFlag.length > 0) {
|
|
4141
|
+
ctx.data.previewUrl = fromFlag;
|
|
4142
|
+
ctx.data.previewUrlSource = "flag";
|
|
4143
|
+
return;
|
|
4144
|
+
}
|
|
4145
|
+
const fromEnv = (process.env.PREVIEW_URL ?? "").trim();
|
|
4146
|
+
if (fromEnv.length > 0) {
|
|
4147
|
+
ctx.data.previewUrl = fromEnv;
|
|
4148
|
+
ctx.data.previewUrlSource = "env";
|
|
4149
|
+
return;
|
|
4150
|
+
}
|
|
4151
|
+
ctx.data.previewUrl = DEFAULT_PREVIEW_URL;
|
|
4152
|
+
ctx.data.previewUrlSource = "default";
|
|
4153
|
+
};
|
|
4154
|
+
|
|
3609
4155
|
// src/scripts/reviewFlow.ts
|
|
3610
4156
|
var reviewFlow = async (ctx) => {
|
|
3611
4157
|
const prNumber = ctx.args.pr;
|
|
@@ -3990,7 +4536,7 @@ var watchStalePrsFlow = async (ctx) => {
|
|
|
3990
4536
|
};
|
|
3991
4537
|
|
|
3992
4538
|
// src/scripts/writeRunSummary.ts
|
|
3993
|
-
import * as
|
|
4539
|
+
import * as fs19 from "fs";
|
|
3994
4540
|
var writeRunSummary = async (ctx, profile) => {
|
|
3995
4541
|
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
3996
4542
|
if (!summaryPath) return;
|
|
@@ -4012,7 +4558,7 @@ var writeRunSummary = async (ctx, profile) => {
|
|
|
4012
4558
|
if (reason) lines.push(`- **Reason:** ${reason}`);
|
|
4013
4559
|
lines.push("");
|
|
4014
4560
|
try {
|
|
4015
|
-
|
|
4561
|
+
fs19.appendFileSync(summaryPath, `${lines.join("\n")}
|
|
4016
4562
|
`);
|
|
4017
4563
|
} catch {
|
|
4018
4564
|
}
|
|
@@ -4033,8 +4579,11 @@ var preflightScripts = {
|
|
|
4033
4579
|
loadIssueContext,
|
|
4034
4580
|
loadConventions,
|
|
4035
4581
|
loadCoverageRules,
|
|
4582
|
+
loadQaGuide,
|
|
4036
4583
|
buildSyntheticPlugin,
|
|
4037
4584
|
resolveArtifacts,
|
|
4585
|
+
discoverQaContext,
|
|
4586
|
+
resolvePreviewUrl,
|
|
4038
4587
|
composePrompt,
|
|
4039
4588
|
skipAgent
|
|
4040
4589
|
};
|
|
@@ -4167,9 +4716,9 @@ async function runExecutable(profileName, input) {
|
|
|
4167
4716
|
data: {},
|
|
4168
4717
|
output: { exitCode: 0 }
|
|
4169
4718
|
};
|
|
4170
|
-
const ndjsonDir =
|
|
4719
|
+
const ndjsonDir = path17.join(input.cwd, ".kody2");
|
|
4171
4720
|
const invokeAgent = async (prompt) => {
|
|
4172
|
-
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) =>
|
|
4721
|
+
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path17.isAbsolute(p) ? p : path17.resolve(profile.dir, p)).filter((p) => p.length > 0);
|
|
4173
4722
|
const syntheticPath = ctx.data.syntheticPluginPath;
|
|
4174
4723
|
const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
|
|
4175
4724
|
return runAgent({
|
|
@@ -4236,17 +4785,17 @@ async function runExecutable(profileName, input) {
|
|
|
4236
4785
|
}
|
|
4237
4786
|
}
|
|
4238
4787
|
function resolveProfilePath(profileName) {
|
|
4239
|
-
const here =
|
|
4788
|
+
const here = path17.dirname(new URL(import.meta.url).pathname);
|
|
4240
4789
|
const candidates = [
|
|
4241
|
-
|
|
4790
|
+
path17.join(here, "executables", profileName, "profile.json"),
|
|
4242
4791
|
// same-dir sibling (dev)
|
|
4243
|
-
|
|
4792
|
+
path17.join(here, "..", "executables", profileName, "profile.json"),
|
|
4244
4793
|
// up one (prod: dist/bin → dist/executables)
|
|
4245
|
-
|
|
4794
|
+
path17.join(here, "..", "src", "executables", profileName, "profile.json")
|
|
4246
4795
|
// fallback
|
|
4247
4796
|
];
|
|
4248
4797
|
for (const c of candidates) {
|
|
4249
|
-
if (
|
|
4798
|
+
if (fs20.existsSync(c)) return c;
|
|
4250
4799
|
}
|
|
4251
4800
|
return candidates[0];
|
|
4252
4801
|
}
|
|
@@ -4422,9 +4971,9 @@ function resolveAuthToken(env = process.env) {
|
|
|
4422
4971
|
return token;
|
|
4423
4972
|
}
|
|
4424
4973
|
function detectPackageManager2(cwd) {
|
|
4425
|
-
if (
|
|
4426
|
-
if (
|
|
4427
|
-
if (
|
|
4974
|
+
if (fs21.existsSync(path18.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
4975
|
+
if (fs21.existsSync(path18.join(cwd, "yarn.lock"))) return "yarn";
|
|
4976
|
+
if (fs21.existsSync(path18.join(cwd, "bun.lockb"))) return "bun";
|
|
4428
4977
|
return "npm";
|
|
4429
4978
|
}
|
|
4430
4979
|
function shellOut(cmd, args, cwd, stream = true) {
|
|
@@ -4504,11 +5053,11 @@ function configureGitIdentity(cwd) {
|
|
|
4504
5053
|
}
|
|
4505
5054
|
function postFailureTail(issueNumber, cwd, reason) {
|
|
4506
5055
|
if (!issueNumber) return;
|
|
4507
|
-
const logPath =
|
|
5056
|
+
const logPath = path18.join(cwd, ".kody2", "last-run.jsonl");
|
|
4508
5057
|
let tail = "";
|
|
4509
5058
|
try {
|
|
4510
|
-
if (
|
|
4511
|
-
const content =
|
|
5059
|
+
if (fs21.existsSync(logPath)) {
|
|
5060
|
+
const content = fs21.readFileSync(logPath, "utf-8");
|
|
4512
5061
|
tail = content.slice(-3e3);
|
|
4513
5062
|
}
|
|
4514
5063
|
} catch {
|
|
@@ -4533,7 +5082,7 @@ async function runCi(argv) {
|
|
|
4533
5082
|
return 0;
|
|
4534
5083
|
}
|
|
4535
5084
|
const args = parseCiArgs(argv);
|
|
4536
|
-
const cwd = args.cwd ?
|
|
5085
|
+
const cwd = args.cwd ? path18.resolve(args.cwd) : process.cwd();
|
|
4537
5086
|
let earlyConfig;
|
|
4538
5087
|
try {
|
|
4539
5088
|
earlyConfig = loadConfig(cwd);
|
|
@@ -4666,9 +5215,9 @@ function parseChatArgs(argv, env = process.env) {
|
|
|
4666
5215
|
return result;
|
|
4667
5216
|
}
|
|
4668
5217
|
function commitChatFiles(cwd, sessionId, verbose) {
|
|
4669
|
-
const sessionFile =
|
|
4670
|
-
const eventsFile =
|
|
4671
|
-
const paths = [sessionFile, eventsFile].filter((p) =>
|
|
5218
|
+
const sessionFile = path19.relative(cwd, sessionFilePath(cwd, sessionId));
|
|
5219
|
+
const eventsFile = path19.relative(cwd, eventsFilePath(cwd, sessionId));
|
|
5220
|
+
const paths = [sessionFile, eventsFile].filter((p) => fs22.existsSync(path19.join(cwd, p)));
|
|
4672
5221
|
if (paths.length === 0) return;
|
|
4673
5222
|
const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
|
|
4674
5223
|
try {
|
|
@@ -4706,7 +5255,7 @@ async function runChat(argv) {
|
|
|
4706
5255
|
${CHAT_HELP}`);
|
|
4707
5256
|
return 64;
|
|
4708
5257
|
}
|
|
4709
|
-
const cwd = args.cwd ?
|
|
5258
|
+
const cwd = args.cwd ? path19.resolve(args.cwd) : process.cwd();
|
|
4710
5259
|
const sessionId = args.sessionId;
|
|
4711
5260
|
const unpackedSecrets = unpackAllSecrets();
|
|
4712
5261
|
if (unpackedSecrets > 0) {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ui-review",
|
|
3
|
+
"describe": "UI/UX review of an open PR: browses the running preview with Playwright, compares behavior to diff intent, posts one structured review comment. Read-only on the repo (no commits); writes a throwaway Playwright spec under .kody2/.",
|
|
4
|
+
"kind": "oneshot",
|
|
5
|
+
"inputs": [
|
|
6
|
+
{
|
|
7
|
+
"name": "pr",
|
|
8
|
+
"flag": "--pr",
|
|
9
|
+
"type": "int",
|
|
10
|
+
"required": true,
|
|
11
|
+
"describe": "GitHub PR number to review."
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "previewUrl",
|
|
15
|
+
"flag": "--preview-url",
|
|
16
|
+
"type": "string",
|
|
17
|
+
"required": false,
|
|
18
|
+
"describe": "Base URL the agent should browse. Falls back to $PREVIEW_URL, then http://localhost:3000."
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"claudeCode": {
|
|
22
|
+
"model": "inherit",
|
|
23
|
+
"permissionMode": "acceptEdits",
|
|
24
|
+
"maxTurns": null,
|
|
25
|
+
"maxThinkingTokens": null,
|
|
26
|
+
"systemPromptAppend": null,
|
|
27
|
+
"tools": [
|
|
28
|
+
"Read",
|
|
29
|
+
"Grep",
|
|
30
|
+
"Glob",
|
|
31
|
+
"Bash",
|
|
32
|
+
"Write",
|
|
33
|
+
"Edit"
|
|
34
|
+
],
|
|
35
|
+
"hooks": [],
|
|
36
|
+
"skills": [],
|
|
37
|
+
"commands": [],
|
|
38
|
+
"subagents": [],
|
|
39
|
+
"plugins": [],
|
|
40
|
+
"mcpServers": []
|
|
41
|
+
},
|
|
42
|
+
"cliTools": [
|
|
43
|
+
{
|
|
44
|
+
"name": "playwright",
|
|
45
|
+
"install": {
|
|
46
|
+
"required": false,
|
|
47
|
+
"checkCommand": "npx --no-install playwright --version",
|
|
48
|
+
"installCommand": "npx --yes playwright install --with-deps chromium"
|
|
49
|
+
},
|
|
50
|
+
"verify": "npx --no-install playwright --version",
|
|
51
|
+
"usage": "Use `npx playwright test <file>` to run a Playwright spec. Write ad-hoc specs under `.kody2/ui-review/*.spec.ts`. If `npx playwright test` errors with `Cannot find package '@playwright/test'`, install it once with `npm install -D @playwright/test` (or the repo's package-manager equivalent) before retrying — the `playwright` browser binaries are already set up by preflight, but the per-repo test framework may not be. Prefer `page.goto(process.env.UI_REVIEW_BASE_URL)` — the base URL is injected as `UI_REVIEW_BASE_URL` at run time. Capture screenshots with `await page.screenshot({ path: '.kody2/ui-review/<name>.png' })` and reference those paths in your final review.",
|
|
52
|
+
"allowedUses": [
|
|
53
|
+
"test",
|
|
54
|
+
"--version"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
"inputArtifacts": [],
|
|
59
|
+
"outputArtifacts": [],
|
|
60
|
+
"scripts": {
|
|
61
|
+
"preflight": [
|
|
62
|
+
{ "script": "reviewFlow" },
|
|
63
|
+
{ "script": "loadTaskState" },
|
|
64
|
+
{ "script": "loadConventions" },
|
|
65
|
+
{ "script": "discoverQaContext" },
|
|
66
|
+
{ "script": "loadQaGuide" },
|
|
67
|
+
{ "script": "resolvePreviewUrl" },
|
|
68
|
+
{ "script": "composePrompt" }
|
|
69
|
+
],
|
|
70
|
+
"postflight": [
|
|
71
|
+
{ "script": "postReviewResult" },
|
|
72
|
+
{ "script": "saveTaskState" }
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
You are Kody, a senior UI/UX reviewer. Review PR #{{pr.number}} by reading the diff AND browsing the running app with Playwright. Post ONE structured review comment. Do NOT edit any tracked source files. Do NOT run any `git` or `gh` commands.
|
|
2
|
+
|
|
3
|
+
You MAY write throwaway Playwright specs and screenshots under `.kody2/ui-review/` — that directory is ignored by the repo.
|
|
4
|
+
|
|
5
|
+
# PR #{{pr.number}}: {{pr.title}}
|
|
6
|
+
|
|
7
|
+
Base: {{pr.baseRefName}} ← Head: {{pr.headRefName}}
|
|
8
|
+
|
|
9
|
+
{{pr.body}}
|
|
10
|
+
|
|
11
|
+
# Preview URL
|
|
12
|
+
|
|
13
|
+
`{{previewUrl}}` (resolved from: {{previewUrlSource}})
|
|
14
|
+
|
|
15
|
+
Before you do anything else, run:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
curl -sS -o /dev/null -w "%{http_code}\n" --max-time 10 {{previewUrl}}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
If the response is not 2xx or 3xx, the preview is unreachable. In that case, SKIP browsing, note the failure in your review under "Browsing", and base your verdict on the diff alone.
|
|
22
|
+
|
|
23
|
+
# QA context (auto-discovered from the repo)
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
{{qaContext}}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
# QA guide (committed in the repo — authoritative over the auto-discovery above)
|
|
30
|
+
|
|
31
|
+
{{qaGuide}}
|
|
32
|
+
|
|
33
|
+
# Diff
|
|
34
|
+
|
|
35
|
+
```diff
|
|
36
|
+
{{prDiff}}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
{{conventionsBlock}}
|
|
40
|
+
|
|
41
|
+
{{toolsUsage}}
|
|
42
|
+
|
|
43
|
+
# What to do
|
|
44
|
+
|
|
45
|
+
1. **Identify UI-affecting changes.** Read the diff. Which pages / components / forms / styles did this PR change? Which user-visible behavior should be verified in the browser? If the diff has no UI surface (pure backend, pure config, pure tests), say so and produce a diff-only review — do not spin up Playwright for nothing.
|
|
46
|
+
|
|
47
|
+
2. **Plan the browse session.** For each UI-affecting change, pick 1–3 routes from the QA context that exercise it. If the change requires an authenticated role, grab credentials from the QA guide above. If no credentials are available for a role the change depends on, note that as a gap and browse only public pages.
|
|
48
|
+
|
|
49
|
+
3. **Write a Playwright spec.** Create exactly one file at `.kody2/ui-review/browse.spec.ts`. Use `process.env.UI_REVIEW_BASE_URL` as the base URL. For each route you plan to check, write a test that:
|
|
50
|
+
- navigates there,
|
|
51
|
+
- performs the minimum interaction to exercise the change (click, submit, fill),
|
|
52
|
+
- takes a screenshot at `.kody2/ui-review/<slug>.png`,
|
|
53
|
+
- asserts at least one piece of visible content so the test fails loudly on a blank / error page.
|
|
54
|
+
|
|
55
|
+
Include a `playwright.config.ts` at `.kody2/ui-review/playwright.config.ts` only if you need custom config; otherwise rely on defaults (headless chromium).
|
|
56
|
+
|
|
57
|
+
4. **Run it.** Invoke:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
UI_REVIEW_BASE_URL={{previewUrl}} npx playwright test .kody2/ui-review/browse.spec.ts --reporter=line
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Capture both stdout and exit code. If Playwright is not installed, the executor will have tried to install it in preflight — if it still fails, report the install error and fall back to a diff-only review.
|
|
64
|
+
|
|
65
|
+
5. **Inspect screenshots.** Use the Read tool on each `.png` under `.kody2/ui-review/` so the visual state is in your context. Note anything that looks broken, empty, misaligned, or inconsistent with the diff's intent.
|
|
66
|
+
|
|
67
|
+
6. **Write the review.** Your FINAL MESSAGE must be the markdown review comment — no preamble, no DONE / COMMIT_MSG markers. The entire final message is posted verbatim to the PR.
|
|
68
|
+
|
|
69
|
+
# Required output format
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
## Verdict: PASS | CONCERNS | FAIL
|
|
73
|
+
|
|
74
|
+
_UI review by kody2 — browsed {{previewUrl}}_
|
|
75
|
+
|
|
76
|
+
### Summary
|
|
77
|
+
<2-3 sentences: what this PR changes in the UI, and whether the running app matches that intent>
|
|
78
|
+
|
|
79
|
+
### What I browsed
|
|
80
|
+
- `<route>` — <what was checked, with screenshot path>
|
|
81
|
+
- ... (omit this section entirely if the diff had no UI surface)
|
|
82
|
+
|
|
83
|
+
### UI findings
|
|
84
|
+
- <bullet — cite file:line for code issues; cite route + screenshot for visual issues; say "None." if truly none>
|
|
85
|
+
|
|
86
|
+
### Code findings
|
|
87
|
+
- <bullets from reading the diff — correctness, a11y, performance, component structure; say "None." if none>
|
|
88
|
+
|
|
89
|
+
### Gaps
|
|
90
|
+
- <anything you could NOT verify (missing creds, unreachable page, preview down) and why — say "None." if you verified everything relevant>
|
|
91
|
+
|
|
92
|
+
### Bottom line
|
|
93
|
+
<one sentence>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
# Rules
|
|
97
|
+
|
|
98
|
+
- No commits. No `git` / `gh` invocations. No edits to files outside `.kody2/ui-review/`.
|
|
99
|
+
- Verdict **FAIL** only for clear visual regressions, broken flows, or correctness/accessibility issues that block merge.
|
|
100
|
+
- Verdict **CONCERNS** for clarity/polish/edge-case gaps that shouldn't block.
|
|
101
|
+
- Verdict **PASS** when the PR's UI changes work as intended and nothing obvious is broken.
|
|
102
|
+
- If the preview URL is unreachable, PASS/FAIL should be based on the diff alone, and the "Gaps" section must call that out.
|
|
103
|
+
- Be specific: every finding gets a route + screenshot reference, or a file:line reference. No generic advice.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kody-ade/kody-engine",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.48",
|
|
4
4
|
"description": "kody2 — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|