@kody-ade/kody-engine 0.2.47 → 0.2.49
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 +660 -103
- package/dist/executables/ui-review/profile.json +75 -0
- package/dist/executables/ui-review/prompt.md +103 -0
- package/package.json +1 -1
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.49",
|
|
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 };
|
|
@@ -3029,19 +3556,27 @@ var postPlanComment = async (ctx) => {
|
|
|
3029
3556
|
const targetNumber = Number(ctx.data.commentTargetNumber ?? 0);
|
|
3030
3557
|
const plan = ctx.data.prSummary?.trim();
|
|
3031
3558
|
if (targetType !== "issue" || !targetNumber || !plan) return;
|
|
3032
|
-
const
|
|
3559
|
+
const flowActive = Boolean(ctx.data.taskState?.flow);
|
|
3560
|
+
const body = renderPlanComment(targetNumber, plan, { flowActive });
|
|
3033
3561
|
try {
|
|
3034
3562
|
postIssueComment(targetNumber, body, ctx.cwd);
|
|
3035
3563
|
} catch {
|
|
3036
3564
|
}
|
|
3037
3565
|
};
|
|
3038
|
-
function renderPlanComment(issueNumber, plan) {
|
|
3039
|
-
|
|
3566
|
+
function renderPlanComment(issueNumber, plan, opts) {
|
|
3567
|
+
const head = `## Plan for issue #${issueNumber}
|
|
3040
3568
|
|
|
3041
|
-
${plan}
|
|
3569
|
+
${plan}`;
|
|
3570
|
+
if (opts?.flowActive) {
|
|
3571
|
+
return `${head}
|
|
3042
3572
|
|
|
3043
3573
|
---
|
|
3044
|
-
|
|
3574
|
+
_Orchestrator will advance to the next step automatically._`;
|
|
3575
|
+
}
|
|
3576
|
+
return `${head}
|
|
3577
|
+
|
|
3578
|
+
---
|
|
3579
|
+
Comment \`kody2 run\` (prefixed with \`@\`) to execute this plan.`;
|
|
3045
3580
|
}
|
|
3046
3581
|
|
|
3047
3582
|
// src/scripts/postResearchComment.ts
|
|
@@ -3128,8 +3663,8 @@ REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.
|
|
|
3128
3663
|
|
|
3129
3664
|
// src/scripts/releaseFlow.ts
|
|
3130
3665
|
import { execFileSync as execFileSync14, spawnSync } from "child_process";
|
|
3131
|
-
import * as
|
|
3132
|
-
import * as
|
|
3666
|
+
import * as fs18 from "fs";
|
|
3667
|
+
import * as path16 from "path";
|
|
3133
3668
|
function bumpVersion(current, bump) {
|
|
3134
3669
|
const m = current.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
|
|
3135
3670
|
if (!m) throw new Error(`cannot parse version '${current}' (expected x.y.z[-suffix])`);
|
|
@@ -3145,12 +3680,12 @@ function bumpVersion(current, bump) {
|
|
|
3145
3680
|
return `${major}.${minor}.${patch}`;
|
|
3146
3681
|
}
|
|
3147
3682
|
function updateVersionInFile(file, newVersion, cwd) {
|
|
3148
|
-
const abs =
|
|
3149
|
-
if (!
|
|
3150
|
-
const content =
|
|
3683
|
+
const abs = path16.join(cwd, file);
|
|
3684
|
+
if (!fs18.existsSync(abs)) return false;
|
|
3685
|
+
const content = fs18.readFileSync(abs, "utf-8");
|
|
3151
3686
|
const updated = content.replace(/"version"\s*:\s*"[^"]+"/, `"version": "${newVersion}"`);
|
|
3152
3687
|
if (updated === content) return false;
|
|
3153
|
-
|
|
3688
|
+
fs18.writeFileSync(abs, updated);
|
|
3154
3689
|
return true;
|
|
3155
3690
|
}
|
|
3156
3691
|
function generateChangelog(cwd, newVersion, lastTag) {
|
|
@@ -3198,19 +3733,19 @@ function generateChangelog(cwd, newVersion, lastTag) {
|
|
|
3198
3733
|
return parts.join("\n");
|
|
3199
3734
|
}
|
|
3200
3735
|
function prependChangelog(cwd, entry) {
|
|
3201
|
-
const p =
|
|
3736
|
+
const p = path16.join(cwd, "CHANGELOG.md");
|
|
3202
3737
|
const header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n";
|
|
3203
|
-
if (
|
|
3204
|
-
const prior =
|
|
3738
|
+
if (fs18.existsSync(p)) {
|
|
3739
|
+
const prior = fs18.readFileSync(p, "utf-8");
|
|
3205
3740
|
if (/^#\s*Changelog\b/m.test(prior)) {
|
|
3206
3741
|
const idx = prior.indexOf("\n", prior.indexOf("# Changelog"));
|
|
3207
|
-
|
|
3742
|
+
fs18.writeFileSync(p, `${prior.slice(0, idx + 1)}
|
|
3208
3743
|
${entry}${prior.slice(idx + 1)}`);
|
|
3209
3744
|
} else {
|
|
3210
|
-
|
|
3745
|
+
fs18.writeFileSync(p, `${header}${entry}${prior}`);
|
|
3211
3746
|
}
|
|
3212
3747
|
} else {
|
|
3213
|
-
|
|
3748
|
+
fs18.writeFileSync(p, `${header}${entry}`);
|
|
3214
3749
|
}
|
|
3215
3750
|
}
|
|
3216
3751
|
function git3(args, cwd, timeout = 6e4) {
|
|
@@ -3261,13 +3796,13 @@ var releaseFlow = async (ctx) => {
|
|
|
3261
3796
|
};
|
|
3262
3797
|
async function runPrepare(args) {
|
|
3263
3798
|
const { cwd, bump, dryRun, versionFiles, ctx } = args;
|
|
3264
|
-
const pkgPath =
|
|
3265
|
-
if (!
|
|
3799
|
+
const pkgPath = path16.join(cwd, "package.json");
|
|
3800
|
+
if (!fs18.existsSync(pkgPath)) {
|
|
3266
3801
|
ctx.output.exitCode = 99;
|
|
3267
3802
|
ctx.output.reason = "release prepare: package.json not found";
|
|
3268
3803
|
return;
|
|
3269
3804
|
}
|
|
3270
|
-
const pkg = JSON.parse(
|
|
3805
|
+
const pkg = JSON.parse(fs18.readFileSync(pkgPath, "utf-8"));
|
|
3271
3806
|
if (typeof pkg.version !== "string") {
|
|
3272
3807
|
ctx.output.exitCode = 99;
|
|
3273
3808
|
ctx.output.reason = "release prepare: package.json has no version";
|
|
@@ -3338,8 +3873,8 @@ Merge this and then run \`kody2 release --mode finalize\`.`;
|
|
|
3338
3873
|
}
|
|
3339
3874
|
async function runFinalize(args) {
|
|
3340
3875
|
const { cwd, dryRun, timeoutMs, releaseCfg, ctx } = args;
|
|
3341
|
-
const pkgPath =
|
|
3342
|
-
const pkg = JSON.parse(
|
|
3876
|
+
const pkgPath = path16.join(cwd, "package.json");
|
|
3877
|
+
const pkg = JSON.parse(fs18.readFileSync(pkgPath, "utf-8"));
|
|
3343
3878
|
if (typeof pkg.version !== "string") {
|
|
3344
3879
|
ctx.output.exitCode = 99;
|
|
3345
3880
|
ctx.output.reason = "release finalize: package.json has no version";
|
|
@@ -3606,6 +4141,25 @@ function tryPostPr3(prNumber, body, cwd) {
|
|
|
3606
4141
|
}
|
|
3607
4142
|
}
|
|
3608
4143
|
|
|
4144
|
+
// src/scripts/resolvePreviewUrl.ts
|
|
4145
|
+
var DEFAULT_PREVIEW_URL = "http://localhost:3000";
|
|
4146
|
+
var resolvePreviewUrl = async (ctx) => {
|
|
4147
|
+
const fromFlag = typeof ctx.args.previewUrl === "string" ? ctx.args.previewUrl.trim() : "";
|
|
4148
|
+
if (fromFlag.length > 0) {
|
|
4149
|
+
ctx.data.previewUrl = fromFlag;
|
|
4150
|
+
ctx.data.previewUrlSource = "flag";
|
|
4151
|
+
return;
|
|
4152
|
+
}
|
|
4153
|
+
const fromEnv = (process.env.PREVIEW_URL ?? "").trim();
|
|
4154
|
+
if (fromEnv.length > 0) {
|
|
4155
|
+
ctx.data.previewUrl = fromEnv;
|
|
4156
|
+
ctx.data.previewUrlSource = "env";
|
|
4157
|
+
return;
|
|
4158
|
+
}
|
|
4159
|
+
ctx.data.previewUrl = DEFAULT_PREVIEW_URL;
|
|
4160
|
+
ctx.data.previewUrlSource = "default";
|
|
4161
|
+
};
|
|
4162
|
+
|
|
3609
4163
|
// src/scripts/reviewFlow.ts
|
|
3610
4164
|
var reviewFlow = async (ctx) => {
|
|
3611
4165
|
const prNumber = ctx.args.pr;
|
|
@@ -3990,7 +4544,7 @@ var watchStalePrsFlow = async (ctx) => {
|
|
|
3990
4544
|
};
|
|
3991
4545
|
|
|
3992
4546
|
// src/scripts/writeRunSummary.ts
|
|
3993
|
-
import * as
|
|
4547
|
+
import * as fs19 from "fs";
|
|
3994
4548
|
var writeRunSummary = async (ctx, profile) => {
|
|
3995
4549
|
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
3996
4550
|
if (!summaryPath) return;
|
|
@@ -4012,7 +4566,7 @@ var writeRunSummary = async (ctx, profile) => {
|
|
|
4012
4566
|
if (reason) lines.push(`- **Reason:** ${reason}`);
|
|
4013
4567
|
lines.push("");
|
|
4014
4568
|
try {
|
|
4015
|
-
|
|
4569
|
+
fs19.appendFileSync(summaryPath, `${lines.join("\n")}
|
|
4016
4570
|
`);
|
|
4017
4571
|
} catch {
|
|
4018
4572
|
}
|
|
@@ -4033,8 +4587,11 @@ var preflightScripts = {
|
|
|
4033
4587
|
loadIssueContext,
|
|
4034
4588
|
loadConventions,
|
|
4035
4589
|
loadCoverageRules,
|
|
4590
|
+
loadQaGuide,
|
|
4036
4591
|
buildSyntheticPlugin,
|
|
4037
4592
|
resolveArtifacts,
|
|
4593
|
+
discoverQaContext,
|
|
4594
|
+
resolvePreviewUrl,
|
|
4038
4595
|
composePrompt,
|
|
4039
4596
|
skipAgent
|
|
4040
4597
|
};
|
|
@@ -4167,9 +4724,9 @@ async function runExecutable(profileName, input) {
|
|
|
4167
4724
|
data: {},
|
|
4168
4725
|
output: { exitCode: 0 }
|
|
4169
4726
|
};
|
|
4170
|
-
const ndjsonDir =
|
|
4727
|
+
const ndjsonDir = path17.join(input.cwd, ".kody2");
|
|
4171
4728
|
const invokeAgent = async (prompt) => {
|
|
4172
|
-
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) =>
|
|
4729
|
+
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path17.isAbsolute(p) ? p : path17.resolve(profile.dir, p)).filter((p) => p.length > 0);
|
|
4173
4730
|
const syntheticPath = ctx.data.syntheticPluginPath;
|
|
4174
4731
|
const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
|
|
4175
4732
|
return runAgent({
|
|
@@ -4236,17 +4793,17 @@ async function runExecutable(profileName, input) {
|
|
|
4236
4793
|
}
|
|
4237
4794
|
}
|
|
4238
4795
|
function resolveProfilePath(profileName) {
|
|
4239
|
-
const here =
|
|
4796
|
+
const here = path17.dirname(new URL(import.meta.url).pathname);
|
|
4240
4797
|
const candidates = [
|
|
4241
|
-
|
|
4798
|
+
path17.join(here, "executables", profileName, "profile.json"),
|
|
4242
4799
|
// same-dir sibling (dev)
|
|
4243
|
-
|
|
4800
|
+
path17.join(here, "..", "executables", profileName, "profile.json"),
|
|
4244
4801
|
// up one (prod: dist/bin → dist/executables)
|
|
4245
|
-
|
|
4802
|
+
path17.join(here, "..", "src", "executables", profileName, "profile.json")
|
|
4246
4803
|
// fallback
|
|
4247
4804
|
];
|
|
4248
4805
|
for (const c of candidates) {
|
|
4249
|
-
if (
|
|
4806
|
+
if (fs20.existsSync(c)) return c;
|
|
4250
4807
|
}
|
|
4251
4808
|
return candidates[0];
|
|
4252
4809
|
}
|
|
@@ -4422,9 +4979,9 @@ function resolveAuthToken(env = process.env) {
|
|
|
4422
4979
|
return token;
|
|
4423
4980
|
}
|
|
4424
4981
|
function detectPackageManager2(cwd) {
|
|
4425
|
-
if (
|
|
4426
|
-
if (
|
|
4427
|
-
if (
|
|
4982
|
+
if (fs21.existsSync(path18.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
4983
|
+
if (fs21.existsSync(path18.join(cwd, "yarn.lock"))) return "yarn";
|
|
4984
|
+
if (fs21.existsSync(path18.join(cwd, "bun.lockb"))) return "bun";
|
|
4428
4985
|
return "npm";
|
|
4429
4986
|
}
|
|
4430
4987
|
function shellOut(cmd, args, cwd, stream = true) {
|
|
@@ -4504,11 +5061,11 @@ function configureGitIdentity(cwd) {
|
|
|
4504
5061
|
}
|
|
4505
5062
|
function postFailureTail(issueNumber, cwd, reason) {
|
|
4506
5063
|
if (!issueNumber) return;
|
|
4507
|
-
const logPath =
|
|
5064
|
+
const logPath = path18.join(cwd, ".kody2", "last-run.jsonl");
|
|
4508
5065
|
let tail = "";
|
|
4509
5066
|
try {
|
|
4510
|
-
if (
|
|
4511
|
-
const content =
|
|
5067
|
+
if (fs21.existsSync(logPath)) {
|
|
5068
|
+
const content = fs21.readFileSync(logPath, "utf-8");
|
|
4512
5069
|
tail = content.slice(-3e3);
|
|
4513
5070
|
}
|
|
4514
5071
|
} catch {
|
|
@@ -4533,7 +5090,7 @@ async function runCi(argv) {
|
|
|
4533
5090
|
return 0;
|
|
4534
5091
|
}
|
|
4535
5092
|
const args = parseCiArgs(argv);
|
|
4536
|
-
const cwd = args.cwd ?
|
|
5093
|
+
const cwd = args.cwd ? path18.resolve(args.cwd) : process.cwd();
|
|
4537
5094
|
let earlyConfig;
|
|
4538
5095
|
try {
|
|
4539
5096
|
earlyConfig = loadConfig(cwd);
|
|
@@ -4666,9 +5223,9 @@ function parseChatArgs(argv, env = process.env) {
|
|
|
4666
5223
|
return result;
|
|
4667
5224
|
}
|
|
4668
5225
|
function commitChatFiles(cwd, sessionId, verbose) {
|
|
4669
|
-
const sessionFile =
|
|
4670
|
-
const eventsFile =
|
|
4671
|
-
const paths = [sessionFile, eventsFile].filter((p) =>
|
|
5226
|
+
const sessionFile = path19.relative(cwd, sessionFilePath(cwd, sessionId));
|
|
5227
|
+
const eventsFile = path19.relative(cwd, eventsFilePath(cwd, sessionId));
|
|
5228
|
+
const paths = [sessionFile, eventsFile].filter((p) => fs22.existsSync(path19.join(cwd, p)));
|
|
4672
5229
|
if (paths.length === 0) return;
|
|
4673
5230
|
const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
|
|
4674
5231
|
try {
|
|
@@ -4706,7 +5263,7 @@ async function runChat(argv) {
|
|
|
4706
5263
|
${CHAT_HELP}`);
|
|
4707
5264
|
return 64;
|
|
4708
5265
|
}
|
|
4709
|
-
const cwd = args.cwd ?
|
|
5266
|
+
const cwd = args.cwd ? path19.resolve(args.cwd) : process.cwd();
|
|
4710
5267
|
const sessionId = args.sessionId;
|
|
4711
5268
|
const unpackedSecrets = unpackAllSecrets();
|
|
4712
5269
|
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.49",
|
|
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",
|