@kody-ade/kody-engine-lite 0.1.136 → 0.1.138
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/cli.js +1560 -256
- package/kody.config.schema.json +16 -0
- package/package.json +1 -1
- package/templates/kody-watch.yml +41 -0
package/dist/bin/cli.js
CHANGED
|
@@ -618,6 +618,12 @@ function setLabel(issueNumber, label) {
|
|
|
618
618
|
logger.warn(` Failed to set label ${label}: ${err}`);
|
|
619
619
|
}
|
|
620
620
|
}
|
|
621
|
+
function removeLabel(issueNumber, label) {
|
|
622
|
+
try {
|
|
623
|
+
gh(["issue", "edit", String(issueNumber), "--remove-label", label]);
|
|
624
|
+
} catch {
|
|
625
|
+
}
|
|
626
|
+
}
|
|
621
627
|
function postComment(issueNumber, body) {
|
|
622
628
|
try {
|
|
623
629
|
gh(
|
|
@@ -713,12 +719,10 @@ function setLifecycleLabel(issueNumber, phase) {
|
|
|
713
719
|
logger.warn(` Invalid lifecycle phase: ${phase}`);
|
|
714
720
|
return;
|
|
715
721
|
}
|
|
716
|
-
const
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
} catch {
|
|
721
|
-
}
|
|
722
|
+
const currentLabels = getIssueLabels(issueNumber);
|
|
723
|
+
const toRemove = LIFECYCLE_LABELS.filter((l) => l !== phase).map((l) => `kody:${l}`).filter((l) => currentLabels.includes(l));
|
|
724
|
+
for (const label of toRemove) {
|
|
725
|
+
removeLabel(issueNumber, label);
|
|
722
726
|
}
|
|
723
727
|
setLabel(issueNumber, `kody:${phase}`);
|
|
724
728
|
}
|
|
@@ -2966,6 +2970,1192 @@ var init_parse_inputs = __esm({
|
|
|
2966
2970
|
}
|
|
2967
2971
|
});
|
|
2968
2972
|
|
|
2973
|
+
// src/watch/core/state.ts
|
|
2974
|
+
import * as fs15 from "fs";
|
|
2975
|
+
import * as path13 from "path";
|
|
2976
|
+
import { execFileSync as execFileSync10 } from "child_process";
|
|
2977
|
+
function createStateStore(repo, localFilePath) {
|
|
2978
|
+
if (process.env.GITHUB_ACTIONS === "true") {
|
|
2979
|
+
return new GhVariableStateStore(repo);
|
|
2980
|
+
}
|
|
2981
|
+
return new JsonStateStore(localFilePath);
|
|
2982
|
+
}
|
|
2983
|
+
var JsonStateStore, GH_VARIABLE_NAME, GhVariableStateStore;
|
|
2984
|
+
var init_state = __esm({
|
|
2985
|
+
"src/watch/core/state.ts"() {
|
|
2986
|
+
"use strict";
|
|
2987
|
+
JsonStateStore = class {
|
|
2988
|
+
data = {};
|
|
2989
|
+
filePath;
|
|
2990
|
+
dirty = false;
|
|
2991
|
+
constructor(filePath) {
|
|
2992
|
+
this.filePath = filePath;
|
|
2993
|
+
this.load();
|
|
2994
|
+
}
|
|
2995
|
+
load() {
|
|
2996
|
+
try {
|
|
2997
|
+
if (fs15.existsSync(this.filePath)) {
|
|
2998
|
+
const content = fs15.readFileSync(this.filePath, "utf-8");
|
|
2999
|
+
const parsed = JSON.parse(content);
|
|
3000
|
+
if (parsed && typeof parsed === "object") {
|
|
3001
|
+
this.data = parsed;
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
} catch {
|
|
3005
|
+
this.data = {};
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
get(key) {
|
|
3009
|
+
return this.data[key];
|
|
3010
|
+
}
|
|
3011
|
+
set(key, value) {
|
|
3012
|
+
this.data[key] = value;
|
|
3013
|
+
this.dirty = true;
|
|
3014
|
+
}
|
|
3015
|
+
save() {
|
|
3016
|
+
if (!this.dirty) return;
|
|
3017
|
+
const dir = path13.dirname(this.filePath);
|
|
3018
|
+
if (!fs15.existsSync(dir)) {
|
|
3019
|
+
fs15.mkdirSync(dir, { recursive: true });
|
|
3020
|
+
}
|
|
3021
|
+
const tempPath = `${this.filePath}.tmp`;
|
|
3022
|
+
const json = JSON.stringify(this.data, null, 2);
|
|
3023
|
+
try {
|
|
3024
|
+
fs15.writeFileSync(tempPath, json, "utf-8");
|
|
3025
|
+
fs15.renameSync(tempPath, this.filePath);
|
|
3026
|
+
this.dirty = false;
|
|
3027
|
+
} catch (error) {
|
|
3028
|
+
if (fs15.existsSync(tempPath)) {
|
|
3029
|
+
fs15.unlinkSync(tempPath);
|
|
3030
|
+
}
|
|
3031
|
+
throw error;
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
};
|
|
3035
|
+
GH_VARIABLE_NAME = "KODY_WATCH_STATE";
|
|
3036
|
+
GhVariableStateStore = class {
|
|
3037
|
+
data = {};
|
|
3038
|
+
dirty = false;
|
|
3039
|
+
repo;
|
|
3040
|
+
constructor(repo) {
|
|
3041
|
+
this.repo = repo;
|
|
3042
|
+
this.loadFromGh();
|
|
3043
|
+
}
|
|
3044
|
+
loadFromGh() {
|
|
3045
|
+
try {
|
|
3046
|
+
const output = execFileSync10(
|
|
3047
|
+
"gh",
|
|
3048
|
+
["variable", "get", GH_VARIABLE_NAME, "--repo", this.repo],
|
|
3049
|
+
{
|
|
3050
|
+
encoding: "utf-8",
|
|
3051
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3052
|
+
env: { ...process.env, GH_TOKEN: process.env.GH_PAT || process.env.GH_TOKEN || "" }
|
|
3053
|
+
}
|
|
3054
|
+
).trim();
|
|
3055
|
+
if (output) {
|
|
3056
|
+
const parsed = JSON.parse(output);
|
|
3057
|
+
if (parsed && typeof parsed === "object") {
|
|
3058
|
+
this.data = parsed;
|
|
3059
|
+
return;
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
} catch (error) {
|
|
3063
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
3064
|
+
if (!msg.includes("HTTP 404") && !msg.includes("variable not found")) {
|
|
3065
|
+
console.warn(`[KodyWatch] Failed to load state: ${msg} \u2014 starting fresh`);
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
this.data = {};
|
|
3069
|
+
}
|
|
3070
|
+
get(key) {
|
|
3071
|
+
return this.data[key];
|
|
3072
|
+
}
|
|
3073
|
+
set(key, value) {
|
|
3074
|
+
this.data[key] = value;
|
|
3075
|
+
this.dirty = true;
|
|
3076
|
+
}
|
|
3077
|
+
save() {
|
|
3078
|
+
if (!this.dirty) return;
|
|
3079
|
+
const json = JSON.stringify(this.data);
|
|
3080
|
+
try {
|
|
3081
|
+
execFileSync10(
|
|
3082
|
+
"gh",
|
|
3083
|
+
["variable", "set", GH_VARIABLE_NAME, "--repo", this.repo, "--body", json],
|
|
3084
|
+
{
|
|
3085
|
+
encoding: "utf-8",
|
|
3086
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3087
|
+
env: { ...process.env, GH_TOKEN: process.env.GH_PAT || process.env.GH_TOKEN || "" }
|
|
3088
|
+
}
|
|
3089
|
+
);
|
|
3090
|
+
this.dirty = false;
|
|
3091
|
+
} catch (error) {
|
|
3092
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
3093
|
+
console.error(`[KodyWatch] Failed to save state: ${msg}`);
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
};
|
|
3097
|
+
}
|
|
3098
|
+
});
|
|
3099
|
+
|
|
3100
|
+
// src/watch/core/dedup.ts
|
|
3101
|
+
function shouldDedup(action, ctx) {
|
|
3102
|
+
if (!action.dedupKey) return false;
|
|
3103
|
+
const windowMs = (action.dedupWindowMinutes ?? 60) * 60 * 1e3;
|
|
3104
|
+
const dedupKey = `dedup:${action.plugin}:${action.dedupKey}`;
|
|
3105
|
+
const dedupEntries = ctx.state.get("watch:dedupEntries") || {};
|
|
3106
|
+
const lastExecuted = dedupEntries[dedupKey];
|
|
3107
|
+
if (!lastExecuted) return false;
|
|
3108
|
+
const lastTime = parseInt(lastExecuted, 10);
|
|
3109
|
+
if (Number.isNaN(lastTime)) return false;
|
|
3110
|
+
return Date.now() - lastTime < windowMs;
|
|
3111
|
+
}
|
|
3112
|
+
function markExecuted(action, ctx) {
|
|
3113
|
+
if (!action.dedupKey) return;
|
|
3114
|
+
const dedupKey = `dedup:${action.plugin}:${action.dedupKey}`;
|
|
3115
|
+
const dedupEntries = ctx.state.get("watch:dedupEntries") || {};
|
|
3116
|
+
dedupEntries[dedupKey] = String(Date.now());
|
|
3117
|
+
ctx.state.set("watch:dedupEntries", dedupEntries);
|
|
3118
|
+
}
|
|
3119
|
+
function cleanupExpiredDedup(ctx, maxAgeMs = 24 * 60 * 60 * 1e3) {
|
|
3120
|
+
const dedupEntries = ctx.state.get("watch:dedupEntries") || {};
|
|
3121
|
+
const now = Date.now();
|
|
3122
|
+
let cleaned = 0;
|
|
3123
|
+
const updated = {};
|
|
3124
|
+
for (const [key, timestamp2] of Object.entries(dedupEntries)) {
|
|
3125
|
+
const time = parseInt(timestamp2, 10);
|
|
3126
|
+
if (Number.isNaN(time) || now - time > maxAgeMs) {
|
|
3127
|
+
cleaned++;
|
|
3128
|
+
continue;
|
|
3129
|
+
}
|
|
3130
|
+
updated[key] = timestamp2;
|
|
3131
|
+
}
|
|
3132
|
+
ctx.state.set("watch:dedupEntries", updated);
|
|
3133
|
+
return cleaned;
|
|
3134
|
+
}
|
|
3135
|
+
var init_dedup = __esm({
|
|
3136
|
+
"src/watch/core/dedup.ts"() {
|
|
3137
|
+
"use strict";
|
|
3138
|
+
}
|
|
3139
|
+
});
|
|
3140
|
+
|
|
3141
|
+
// src/watch/clients/github.ts
|
|
3142
|
+
import { execFileSync as execFileSync11 } from "child_process";
|
|
3143
|
+
function createGitHubClient(repo, token) {
|
|
3144
|
+
const gh2 = (args2, input) => {
|
|
3145
|
+
try {
|
|
3146
|
+
return execFileSync11("gh", args2, {
|
|
3147
|
+
input,
|
|
3148
|
+
encoding: "utf-8",
|
|
3149
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
3150
|
+
env: { ...process.env, GH_TOKEN: token }
|
|
3151
|
+
}).trim();
|
|
3152
|
+
} catch {
|
|
3153
|
+
return "";
|
|
3154
|
+
}
|
|
3155
|
+
};
|
|
3156
|
+
return {
|
|
3157
|
+
postComment(issueNumber, body) {
|
|
3158
|
+
gh2(["issue", "comment", String(issueNumber), "--repo", repo, "--body-file", "-"], body);
|
|
3159
|
+
},
|
|
3160
|
+
getIssue(issueNumber) {
|
|
3161
|
+
const output = gh2([
|
|
3162
|
+
"api",
|
|
3163
|
+
`repos/${repo}/issues/${issueNumber}`,
|
|
3164
|
+
"--jq",
|
|
3165
|
+
"{body: .body, title: .title}"
|
|
3166
|
+
]);
|
|
3167
|
+
if (!output) return { body: null, title: null };
|
|
3168
|
+
try {
|
|
3169
|
+
return JSON.parse(output);
|
|
3170
|
+
} catch {
|
|
3171
|
+
return { body: null, title: null };
|
|
3172
|
+
}
|
|
3173
|
+
},
|
|
3174
|
+
getOpenIssues(labels) {
|
|
3175
|
+
let query = `repos/${repo}/issues`;
|
|
3176
|
+
if (labels && labels.length > 0) {
|
|
3177
|
+
query += `?labels=${labels.join(",")}`;
|
|
3178
|
+
}
|
|
3179
|
+
const output = gh2([
|
|
3180
|
+
"api",
|
|
3181
|
+
query,
|
|
3182
|
+
"--paginate",
|
|
3183
|
+
"--jq",
|
|
3184
|
+
'[.[] | select(.state == "open") | select(.pull_request == null) | {number: .number, title: .title, labels: [.labels[].name], updatedAt: .updated_at}]'
|
|
3185
|
+
]);
|
|
3186
|
+
if (!output) return [];
|
|
3187
|
+
return output.split("\n").filter(Boolean).flatMap((line) => {
|
|
3188
|
+
try {
|
|
3189
|
+
return JSON.parse(line);
|
|
3190
|
+
} catch {
|
|
3191
|
+
return [];
|
|
3192
|
+
}
|
|
3193
|
+
});
|
|
3194
|
+
},
|
|
3195
|
+
createIssue(title, body, labels) {
|
|
3196
|
+
const args2 = ["issue", "create", "--repo", repo, "--title", title, "--body-file", "-"];
|
|
3197
|
+
const output = gh2(args2, body);
|
|
3198
|
+
if (!output) return null;
|
|
3199
|
+
const match = output.match(/\/issues\/(\d+)/);
|
|
3200
|
+
const issueNumber = match ? parseInt(match[1], 10) : null;
|
|
3201
|
+
if (issueNumber && labels.length > 0) {
|
|
3202
|
+
gh2(["issue", "edit", String(issueNumber), "--repo", repo, "--add-label", labels.join(",")]);
|
|
3203
|
+
}
|
|
3204
|
+
return issueNumber;
|
|
3205
|
+
},
|
|
3206
|
+
searchIssues(query) {
|
|
3207
|
+
const output = gh2([
|
|
3208
|
+
"api",
|
|
3209
|
+
`search/issues?q=${encodeURIComponent(query + ` repo:${repo}`)}&per_page=30`,
|
|
3210
|
+
"--jq",
|
|
3211
|
+
"[.items[] | {number: .number, title: .title, labels: [.labels[].name], updatedAt: .updated_at}]"
|
|
3212
|
+
]);
|
|
3213
|
+
if (!output) return [];
|
|
3214
|
+
try {
|
|
3215
|
+
return JSON.parse(output);
|
|
3216
|
+
} catch {
|
|
3217
|
+
return [];
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
};
|
|
3221
|
+
}
|
|
3222
|
+
var init_github = __esm({
|
|
3223
|
+
"src/watch/clients/github.ts"() {
|
|
3224
|
+
"use strict";
|
|
3225
|
+
}
|
|
3226
|
+
});
|
|
3227
|
+
|
|
3228
|
+
// src/watch/clients/logger.ts
|
|
3229
|
+
function createConsoleLogger() {
|
|
3230
|
+
const format = (level, first, second) => {
|
|
3231
|
+
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
|
|
3232
|
+
if (typeof first === "string") {
|
|
3233
|
+
return `[${timestamp2}] ${level}: ${first}`;
|
|
3234
|
+
}
|
|
3235
|
+
const ctx = JSON.stringify(first);
|
|
3236
|
+
return `[${timestamp2}] ${level}: ${second} ${ctx}`;
|
|
3237
|
+
};
|
|
3238
|
+
return {
|
|
3239
|
+
debug(first, second) {
|
|
3240
|
+
if (process.env.LOG_LEVEL === "debug") {
|
|
3241
|
+
console.debug(format("DEBUG", first, second));
|
|
3242
|
+
}
|
|
3243
|
+
},
|
|
3244
|
+
info(first, second) {
|
|
3245
|
+
console.info(format("INFO", first, second));
|
|
3246
|
+
},
|
|
3247
|
+
warn(first, second) {
|
|
3248
|
+
console.warn(format("WARN", first, second));
|
|
3249
|
+
},
|
|
3250
|
+
error(first, second) {
|
|
3251
|
+
console.error(format("ERROR", first, second));
|
|
3252
|
+
}
|
|
3253
|
+
};
|
|
3254
|
+
}
|
|
3255
|
+
var init_logger2 = __esm({
|
|
3256
|
+
"src/watch/clients/logger.ts"() {
|
|
3257
|
+
"use strict";
|
|
3258
|
+
}
|
|
3259
|
+
});
|
|
3260
|
+
|
|
3261
|
+
// src/watch/core/watch.ts
|
|
3262
|
+
async function runWatch(config) {
|
|
3263
|
+
const { repo, dryRun, stateFile, plugins } = config;
|
|
3264
|
+
const state = createStateStore(repo, stateFile);
|
|
3265
|
+
const cycleNumber = (state.get("system:cycleNumber") || 0) + 1;
|
|
3266
|
+
state.set("system:cycleNumber", cycleNumber);
|
|
3267
|
+
const token = process.env.GH_TOKEN || "";
|
|
3268
|
+
const github = createGitHubClient(repo, token);
|
|
3269
|
+
const log2 = createConsoleLogger();
|
|
3270
|
+
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
3271
|
+
const ctx = {
|
|
3272
|
+
repo,
|
|
3273
|
+
dryRun,
|
|
3274
|
+
state,
|
|
3275
|
+
github,
|
|
3276
|
+
log: log2,
|
|
3277
|
+
runTimestamp: timestamp2,
|
|
3278
|
+
cycleNumber,
|
|
3279
|
+
digestIssue: config.digestIssue
|
|
3280
|
+
};
|
|
3281
|
+
const cleaned = cleanupExpiredDedup(ctx);
|
|
3282
|
+
if (cleaned > 0) {
|
|
3283
|
+
log2.debug({ cleaned }, "Cleaned up expired dedup entries");
|
|
3284
|
+
}
|
|
3285
|
+
const errors = [];
|
|
3286
|
+
const allActions = [];
|
|
3287
|
+
const scheduledPlugins = plugins.filter((plugin) => {
|
|
3288
|
+
if (!plugin.schedule || !plugin.schedule.every) return true;
|
|
3289
|
+
return cycleNumber % plugin.schedule.every === 0;
|
|
3290
|
+
});
|
|
3291
|
+
log2.info(
|
|
3292
|
+
{ cycle: cycleNumber, pluginsTotal: plugins.length, pluginsScheduled: scheduledPlugins.length },
|
|
3293
|
+
"Watch cycle started"
|
|
3294
|
+
);
|
|
3295
|
+
for (const plugin of scheduledPlugins) {
|
|
3296
|
+
try {
|
|
3297
|
+
log2.debug({ plugin: plugin.name }, "Running plugin");
|
|
3298
|
+
const actions = await plugin.run(ctx);
|
|
3299
|
+
allActions.push(...actions);
|
|
3300
|
+
log2.debug({ plugin: plugin.name, actionCount: actions.length }, "Plugin completed");
|
|
3301
|
+
} catch (error) {
|
|
3302
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3303
|
+
errors.push(`Plugin ${plugin.name}: ${message}`);
|
|
3304
|
+
log2.error({ plugin: plugin.name, error: message }, "Plugin failed");
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
const dedupedActions = [];
|
|
3308
|
+
let actionsDeduplicated = 0;
|
|
3309
|
+
for (const action of allActions) {
|
|
3310
|
+
if (shouldDedup(action, ctx)) {
|
|
3311
|
+
actionsDeduplicated++;
|
|
3312
|
+
log2.debug(
|
|
3313
|
+
{ plugin: action.plugin, type: action.type, dedupKey: action.dedupKey },
|
|
3314
|
+
"Action deduplicated"
|
|
3315
|
+
);
|
|
3316
|
+
continue;
|
|
3317
|
+
}
|
|
3318
|
+
dedupedActions.push(action);
|
|
3319
|
+
}
|
|
3320
|
+
let actionsExecuted = 0;
|
|
3321
|
+
if (!dryRun) {
|
|
3322
|
+
for (const action of dedupedActions) {
|
|
3323
|
+
try {
|
|
3324
|
+
log2.info(
|
|
3325
|
+
{ plugin: action.plugin, type: action.type, target: action.target, urgency: action.urgency },
|
|
3326
|
+
"Executing action"
|
|
3327
|
+
);
|
|
3328
|
+
const result3 = await action.execute(ctx);
|
|
3329
|
+
if (result3.success) {
|
|
3330
|
+
actionsExecuted++;
|
|
3331
|
+
markExecuted(action, ctx);
|
|
3332
|
+
} else {
|
|
3333
|
+
log2.warn({ plugin: action.plugin, type: action.type, message: result3.message }, "Action failed");
|
|
3334
|
+
}
|
|
3335
|
+
} catch (error) {
|
|
3336
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3337
|
+
errors.push(`Action ${action.plugin}/${action.type}: ${message}`);
|
|
3338
|
+
log2.error({ plugin: action.plugin, type: action.type, error: message }, "Action error");
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
} else {
|
|
3342
|
+
log2.info({ actionCount: dedupedActions.length }, "Dry run \u2014 skipping action execution");
|
|
3343
|
+
}
|
|
3344
|
+
state.save();
|
|
3345
|
+
const result2 = {
|
|
3346
|
+
cycleNumber,
|
|
3347
|
+
pluginsRun: scheduledPlugins.length,
|
|
3348
|
+
actionsProduced: allActions.length,
|
|
3349
|
+
actionsExecuted,
|
|
3350
|
+
actionsDeduplicated,
|
|
3351
|
+
errors
|
|
3352
|
+
};
|
|
3353
|
+
log2.info(
|
|
3354
|
+
{
|
|
3355
|
+
cycle: cycleNumber,
|
|
3356
|
+
pluginsRun: result2.pluginsRun,
|
|
3357
|
+
actionsProduced: result2.actionsProduced,
|
|
3358
|
+
actionsExecuted: result2.actionsExecuted,
|
|
3359
|
+
actionsDeduplicated: result2.actionsDeduplicated,
|
|
3360
|
+
errors: result2.errors.length
|
|
3361
|
+
},
|
|
3362
|
+
"Watch cycle completed"
|
|
3363
|
+
);
|
|
3364
|
+
return result2;
|
|
3365
|
+
}
|
|
3366
|
+
var init_watch = __esm({
|
|
3367
|
+
"src/watch/core/watch.ts"() {
|
|
3368
|
+
"use strict";
|
|
3369
|
+
init_state();
|
|
3370
|
+
init_dedup();
|
|
3371
|
+
init_github();
|
|
3372
|
+
init_logger2();
|
|
3373
|
+
}
|
|
3374
|
+
});
|
|
3375
|
+
|
|
3376
|
+
// src/watch/plugins/registry.ts
|
|
3377
|
+
function createPluginRegistry() {
|
|
3378
|
+
return new PluginRegistry();
|
|
3379
|
+
}
|
|
3380
|
+
var PluginRegistry;
|
|
3381
|
+
var init_registry = __esm({
|
|
3382
|
+
"src/watch/plugins/registry.ts"() {
|
|
3383
|
+
"use strict";
|
|
3384
|
+
PluginRegistry = class {
|
|
3385
|
+
plugins = [];
|
|
3386
|
+
register(plugin) {
|
|
3387
|
+
const existing = this.plugins.find((p) => p.name === plugin.name);
|
|
3388
|
+
if (existing) {
|
|
3389
|
+
throw new Error(`Plugin already registered: ${plugin.name}`);
|
|
3390
|
+
}
|
|
3391
|
+
this.plugins.push(plugin);
|
|
3392
|
+
}
|
|
3393
|
+
getAll() {
|
|
3394
|
+
return [...this.plugins];
|
|
3395
|
+
}
|
|
3396
|
+
clear() {
|
|
3397
|
+
this.plugins = [];
|
|
3398
|
+
}
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3401
|
+
});
|
|
3402
|
+
|
|
3403
|
+
// src/watch/plugins/pipeline-health/index.ts
|
|
3404
|
+
import * as fs16 from "fs";
|
|
3405
|
+
import * as path14 from "path";
|
|
3406
|
+
function discoverTasks(cwd) {
|
|
3407
|
+
const tasksDir = path14.join(cwd, ".kody", "tasks");
|
|
3408
|
+
if (!fs16.existsSync(tasksDir)) return [];
|
|
3409
|
+
const tasks = [];
|
|
3410
|
+
try {
|
|
3411
|
+
for (const entry of fs16.readdirSync(tasksDir, { withFileTypes: true })) {
|
|
3412
|
+
if (!entry.isDirectory()) continue;
|
|
3413
|
+
const statusPath = path14.join(tasksDir, entry.name, "status.json");
|
|
3414
|
+
if (!fs16.existsSync(statusPath)) continue;
|
|
3415
|
+
try {
|
|
3416
|
+
const content = fs16.readFileSync(statusPath, "utf-8");
|
|
3417
|
+
const status = JSON.parse(content);
|
|
3418
|
+
tasks.push({ taskId: entry.name, ...status });
|
|
3419
|
+
} catch {
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
} catch {
|
|
3423
|
+
}
|
|
3424
|
+
return tasks;
|
|
3425
|
+
}
|
|
3426
|
+
function evaluateHealth(task) {
|
|
3427
|
+
const now = Date.now();
|
|
3428
|
+
if (task.state === "failed") {
|
|
3429
|
+
const failedStage = Object.entries(task.stages || {}).find(([, s]) => s.state === "failed");
|
|
3430
|
+
return {
|
|
3431
|
+
taskId: task.taskId,
|
|
3432
|
+
status: task.state,
|
|
3433
|
+
health: "failed",
|
|
3434
|
+
detail: failedStage ? `Failed at stage '${failedStage[0]}': ${failedStage[1].error || "unknown error"}` : "Pipeline failed",
|
|
3435
|
+
failedStage: failedStage?.[0]
|
|
3436
|
+
};
|
|
3437
|
+
}
|
|
3438
|
+
if (task.state === "running" || task.state === "in-progress") {
|
|
3439
|
+
const startedAt = task.startedAt ? new Date(task.startedAt).getTime() : 0;
|
|
3440
|
+
if (startedAt > 0) {
|
|
3441
|
+
const durationMinutes = Math.round((now - startedAt) / 6e4);
|
|
3442
|
+
if (durationMinutes > STALL_THRESHOLD_MINUTES) {
|
|
3443
|
+
const runningStage = Object.entries(task.stages || {}).find(
|
|
3444
|
+
([, s]) => s.state === "running" || s.state === "in-progress"
|
|
3445
|
+
);
|
|
3446
|
+
return {
|
|
3447
|
+
taskId: task.taskId,
|
|
3448
|
+
status: task.state,
|
|
3449
|
+
health: "stalled",
|
|
3450
|
+
detail: runningStage ? `Stalled at stage '${runningStage[0]}' for ${durationMinutes} min` : `Running for ${durationMinutes} min without progress`,
|
|
3451
|
+
durationMinutes
|
|
3452
|
+
};
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
return {
|
|
3456
|
+
taskId: task.taskId,
|
|
3457
|
+
status: task.state,
|
|
3458
|
+
health: "healthy",
|
|
3459
|
+
detail: "Running normally"
|
|
3460
|
+
};
|
|
3461
|
+
}
|
|
3462
|
+
return {
|
|
3463
|
+
taskId: task.taskId,
|
|
3464
|
+
status: task.state,
|
|
3465
|
+
health: "healthy",
|
|
3466
|
+
detail: task.state === "completed" ? "Completed successfully" : `Status: ${task.state}`
|
|
3467
|
+
};
|
|
3468
|
+
}
|
|
3469
|
+
function formatDigestMarkdown(evaluations, cycleNumber) {
|
|
3470
|
+
const unhealthy = evaluations.filter((e) => e.health !== "healthy");
|
|
3471
|
+
if (unhealthy.length === 0) return "";
|
|
3472
|
+
let md = `## Pipeline Health \u2014 Cycle #${cycleNumber}
|
|
3473
|
+
|
|
3474
|
+
`;
|
|
3475
|
+
md += `| Task | Status | Health | Detail |
|
|
3476
|
+
`;
|
|
3477
|
+
md += `|------|--------|--------|--------|
|
|
3478
|
+
`;
|
|
3479
|
+
for (const e of unhealthy) {
|
|
3480
|
+
const icon = e.health === "failed" ? "\u{1F534}" : e.health === "stalled" ? "\u{1F7E1}" : "\u{1F7E0}";
|
|
3481
|
+
md += `| \`${e.taskId}\` | ${e.status} | ${icon} ${e.health} | ${e.detail} |
|
|
3482
|
+
`;
|
|
3483
|
+
}
|
|
3484
|
+
md += `
|
|
3485
|
+
_Generated by Kody Watch on ${(/* @__PURE__ */ new Date()).toISOString()}_`;
|
|
3486
|
+
return md;
|
|
3487
|
+
}
|
|
3488
|
+
var STALL_THRESHOLD_MINUTES, pipelineHealthPlugin;
|
|
3489
|
+
var init_pipeline_health = __esm({
|
|
3490
|
+
"src/watch/plugins/pipeline-health/index.ts"() {
|
|
3491
|
+
"use strict";
|
|
3492
|
+
STALL_THRESHOLD_MINUTES = 30;
|
|
3493
|
+
pipelineHealthPlugin = {
|
|
3494
|
+
name: "pipeline-health",
|
|
3495
|
+
description: "Monitor .kody/tasks/ for stalled, failed, or stuck pipeline runs",
|
|
3496
|
+
domain: "pipeline",
|
|
3497
|
+
schedule: { every: 1 },
|
|
3498
|
+
async run(ctx) {
|
|
3499
|
+
const tasks = discoverTasks(process.cwd());
|
|
3500
|
+
if (tasks.length === 0) {
|
|
3501
|
+
ctx.log.info("No tasks found \u2014 skipping pipeline-health");
|
|
3502
|
+
return [];
|
|
3503
|
+
}
|
|
3504
|
+
const evaluations = tasks.map(evaluateHealth);
|
|
3505
|
+
const unhealthy = evaluations.filter((e) => e.health !== "healthy");
|
|
3506
|
+
ctx.log.info(
|
|
3507
|
+
{ total: tasks.length, unhealthy: unhealthy.length },
|
|
3508
|
+
"Pipeline health scan complete"
|
|
3509
|
+
);
|
|
3510
|
+
if (unhealthy.length === 0) return [];
|
|
3511
|
+
const actions = [];
|
|
3512
|
+
if (ctx.digestIssue) {
|
|
3513
|
+
actions.push({
|
|
3514
|
+
plugin: "pipeline-health",
|
|
3515
|
+
type: "digest",
|
|
3516
|
+
urgency: "warning",
|
|
3517
|
+
title: "Pipeline Health Report",
|
|
3518
|
+
detail: `${unhealthy.length} unhealthy task(s)`,
|
|
3519
|
+
dedupKey: "pipeline-health:digest",
|
|
3520
|
+
dedupWindowMinutes: 25,
|
|
3521
|
+
// Slightly less than 30 min cycle
|
|
3522
|
+
async execute(execCtx) {
|
|
3523
|
+
if (!execCtx.digestIssue) return { success: false, message: "No digest issue" };
|
|
3524
|
+
const markdown = formatDigestMarkdown(evaluations, execCtx.cycleNumber);
|
|
3525
|
+
if (!markdown) return { success: true, message: "No unhealthy tasks" };
|
|
3526
|
+
execCtx.github.postComment(execCtx.digestIssue, markdown);
|
|
3527
|
+
return { success: true, message: `Reported ${unhealthy.length} unhealthy task(s)` };
|
|
3528
|
+
}
|
|
3529
|
+
});
|
|
3530
|
+
}
|
|
3531
|
+
return actions;
|
|
3532
|
+
}
|
|
3533
|
+
};
|
|
3534
|
+
}
|
|
3535
|
+
});
|
|
3536
|
+
|
|
3537
|
+
// src/watch/plugins/security-scan/rules.ts
|
|
3538
|
+
var SECRET_PATTERNS, SECRET_SCAN_EXCLUDES, UNSAFE_PATTERNS, ENV_FILE_PATTERNS;
|
|
3539
|
+
var init_rules = __esm({
|
|
3540
|
+
"src/watch/plugins/security-scan/rules.ts"() {
|
|
3541
|
+
"use strict";
|
|
3542
|
+
SECRET_PATTERNS = [
|
|
3543
|
+
{ label: "AWS access key", pattern: /['"]AKIA[0-9A-Z]{16}['"]/ },
|
|
3544
|
+
{ label: "Generic API key assignment", pattern: /(?:api[_-]?key|apikey|api_secret)\s*[:=]\s*['"][a-zA-Z0-9_\-]{20,}['"]/i },
|
|
3545
|
+
{ label: "Private key block", pattern: /-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----/ },
|
|
3546
|
+
{ label: "JWT token", pattern: /['"]eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}['"]/ }
|
|
3547
|
+
];
|
|
3548
|
+
SECRET_SCAN_EXCLUDES = [
|
|
3549
|
+
"node_modules/",
|
|
3550
|
+
".next/",
|
|
3551
|
+
".git/",
|
|
3552
|
+
"dist/",
|
|
3553
|
+
"build/",
|
|
3554
|
+
"tests/",
|
|
3555
|
+
"watch/",
|
|
3556
|
+
".env",
|
|
3557
|
+
"pnpm-lock.yaml",
|
|
3558
|
+
"package-lock.json",
|
|
3559
|
+
"*.test.ts",
|
|
3560
|
+
"*.spec.ts",
|
|
3561
|
+
"*.md",
|
|
3562
|
+
"*.json"
|
|
3563
|
+
];
|
|
3564
|
+
UNSAFE_PATTERNS = [
|
|
3565
|
+
{ label: "eval() usage", pattern: /\beval\s*\(/, severity: "high" },
|
|
3566
|
+
{ label: "innerHTML assignment", pattern: /\.innerHTML\s*=/, severity: "medium" },
|
|
3567
|
+
{ label: "Unsanitized exec", pattern: /exec\s*\(\s*`/, severity: "high" },
|
|
3568
|
+
{ label: "Unsanitized execSync", pattern: /execSync\s*\(\s*`/, severity: "high" }
|
|
3569
|
+
];
|
|
3570
|
+
ENV_FILE_PATTERNS = [
|
|
3571
|
+
".env",
|
|
3572
|
+
".env.local",
|
|
3573
|
+
".env.production",
|
|
3574
|
+
".env.staging"
|
|
3575
|
+
];
|
|
3576
|
+
}
|
|
3577
|
+
});
|
|
3578
|
+
|
|
3579
|
+
// src/watch/plugins/security-scan/scanner.ts
|
|
3580
|
+
import * as fs17 from "fs";
|
|
3581
|
+
import * as path15 from "path";
|
|
3582
|
+
import { execFileSync as execFileSync12 } from "child_process";
|
|
3583
|
+
function findFiles(dir, pattern, exclude = []) {
|
|
3584
|
+
const results = [];
|
|
3585
|
+
if (!fs17.existsSync(dir)) return results;
|
|
3586
|
+
let entries;
|
|
3587
|
+
try {
|
|
3588
|
+
entries = fs17.readdirSync(dir, { withFileTypes: true });
|
|
3589
|
+
} catch {
|
|
3590
|
+
return results;
|
|
3591
|
+
}
|
|
3592
|
+
if (!entries || !Array.isArray(entries)) return results;
|
|
3593
|
+
for (const entry of entries) {
|
|
3594
|
+
const fullPath = path15.join(dir, entry.name);
|
|
3595
|
+
const shouldExclude = exclude.some((ex) => {
|
|
3596
|
+
if (ex.endsWith("/")) return fullPath.includes(ex);
|
|
3597
|
+
if (ex.startsWith("*")) return entry.name.endsWith(ex.slice(1));
|
|
3598
|
+
return entry.name === ex || fullPath.endsWith(ex);
|
|
3599
|
+
});
|
|
3600
|
+
if (shouldExclude) continue;
|
|
3601
|
+
if (entry.isDirectory()) {
|
|
3602
|
+
results.push(...findFiles(fullPath, pattern, exclude));
|
|
3603
|
+
} else if (pattern.test(entry.name)) {
|
|
3604
|
+
results.push(fullPath);
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
return results;
|
|
3608
|
+
}
|
|
3609
|
+
function scanForHardcodedSecrets(rootDir) {
|
|
3610
|
+
const findings = [];
|
|
3611
|
+
const srcDir = path15.join(rootDir, "src");
|
|
3612
|
+
if (!fs17.existsSync(srcDir)) return findings;
|
|
3613
|
+
const sourceFiles = findFiles(srcDir, /\.(ts|tsx|js|jsx)$/, SECRET_SCAN_EXCLUDES);
|
|
3614
|
+
for (const filePath of sourceFiles) {
|
|
3615
|
+
const relativePath = path15.relative(rootDir, filePath);
|
|
3616
|
+
const content = fs17.readFileSync(filePath, "utf-8");
|
|
3617
|
+
const lines = content.split("\n");
|
|
3618
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3619
|
+
const line = lines[i];
|
|
3620
|
+
for (const secretDef of SECRET_PATTERNS) {
|
|
3621
|
+
if (secretDef.pattern.test(line)) {
|
|
3622
|
+
findings.push({
|
|
3623
|
+
rule: "hardcoded-secret",
|
|
3624
|
+
severity: "critical",
|
|
3625
|
+
file: relativePath,
|
|
3626
|
+
line: i + 1,
|
|
3627
|
+
message: `Potential hardcoded secret: ${secretDef.label}`,
|
|
3628
|
+
detail: `Line ${i + 1}: ${line.trim().substring(0, 80)}...`
|
|
3629
|
+
});
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
return findings;
|
|
3635
|
+
}
|
|
3636
|
+
function scanForUnsafePatterns(rootDir) {
|
|
3637
|
+
const findings = [];
|
|
3638
|
+
const srcDir = path15.join(rootDir, "src");
|
|
3639
|
+
if (!fs17.existsSync(srcDir)) return findings;
|
|
3640
|
+
const sourceFiles = findFiles(srcDir, /\.(ts|tsx|js|jsx)$/, SECRET_SCAN_EXCLUDES);
|
|
3641
|
+
for (const filePath of sourceFiles) {
|
|
3642
|
+
const relativePath = path15.relative(rootDir, filePath);
|
|
3643
|
+
const content = fs17.readFileSync(filePath, "utf-8");
|
|
3644
|
+
const lines = content.split("\n");
|
|
3645
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3646
|
+
const line = lines[i];
|
|
3647
|
+
for (const unsafeDef of UNSAFE_PATTERNS) {
|
|
3648
|
+
if (unsafeDef.pattern.test(line)) {
|
|
3649
|
+
findings.push({
|
|
3650
|
+
rule: `unsafe-pattern:${unsafeDef.label.toLowerCase().replace(/\s+/g, "-")}`,
|
|
3651
|
+
severity: unsafeDef.severity,
|
|
3652
|
+
file: relativePath,
|
|
3653
|
+
line: i + 1,
|
|
3654
|
+
message: `Unsafe pattern: ${unsafeDef.label}`,
|
|
3655
|
+
detail: `Line ${i + 1}: ${line.trim().substring(0, 80)}`
|
|
3656
|
+
});
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
return findings;
|
|
3662
|
+
}
|
|
3663
|
+
function scanForCommittedEnvFiles(rootDir) {
|
|
3664
|
+
const findings = [];
|
|
3665
|
+
for (const envFile of ENV_FILE_PATTERNS) {
|
|
3666
|
+
const envPath = path15.join(rootDir, envFile);
|
|
3667
|
+
if (!fs17.existsSync(envPath)) continue;
|
|
3668
|
+
try {
|
|
3669
|
+
execFileSync12("git", ["ls-files", "--error-unmatch", envFile], {
|
|
3670
|
+
cwd: rootDir,
|
|
3671
|
+
encoding: "utf-8",
|
|
3672
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3673
|
+
});
|
|
3674
|
+
findings.push({
|
|
3675
|
+
rule: "committed-env-file",
|
|
3676
|
+
severity: "critical",
|
|
3677
|
+
file: envFile,
|
|
3678
|
+
message: `Environment file committed to git: ${envFile}`,
|
|
3679
|
+
detail: `${envFile} is tracked by git and may contain secrets`
|
|
3680
|
+
});
|
|
3681
|
+
} catch {
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3684
|
+
return findings;
|
|
3685
|
+
}
|
|
3686
|
+
function scanDependencyVulnerabilities(rootDir) {
|
|
3687
|
+
const findings = [];
|
|
3688
|
+
const hasYarn = fs17.existsSync(path15.join(rootDir, "yarn.lock"));
|
|
3689
|
+
const hasPnpm = fs17.existsSync(path15.join(rootDir, "pnpm-lock.yaml"));
|
|
3690
|
+
const hasNpm = fs17.existsSync(path15.join(rootDir, "package-lock.json"));
|
|
3691
|
+
const auditCmd = hasPnpm ? "pnpm" : hasYarn ? "yarn" : hasNpm ? "npm" : null;
|
|
3692
|
+
if (!auditCmd) return findings;
|
|
3693
|
+
try {
|
|
3694
|
+
const output = execFileSync12(auditCmd, ["audit", "--json"], {
|
|
3695
|
+
cwd: rootDir,
|
|
3696
|
+
encoding: "utf-8",
|
|
3697
|
+
timeout: 6e4,
|
|
3698
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3699
|
+
});
|
|
3700
|
+
if (hasPnpm || hasNpm) {
|
|
3701
|
+
try {
|
|
3702
|
+
const audit = JSON.parse(output);
|
|
3703
|
+
const vulnerabilities = audit.vulnerabilities || audit.advisories || {};
|
|
3704
|
+
for (const [name, vuln] of Object.entries(vulnerabilities)) {
|
|
3705
|
+
const v = vuln;
|
|
3706
|
+
const severity = v.severity || "medium";
|
|
3707
|
+
if (severity === "low" || severity === "info") continue;
|
|
3708
|
+
findings.push({
|
|
3709
|
+
rule: "dependency-vulnerability",
|
|
3710
|
+
severity: severity === "critical" ? "critical" : severity === "high" ? "high" : "medium",
|
|
3711
|
+
file: "package.json",
|
|
3712
|
+
message: `Vulnerable dependency: ${name} (${severity})`,
|
|
3713
|
+
detail: v.title || v.overview || `${name} has a ${severity} vulnerability`
|
|
3714
|
+
});
|
|
3715
|
+
}
|
|
3716
|
+
} catch {
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
} catch (error) {
|
|
3720
|
+
const stderr = error instanceof Error ? error.stdout || "" : "";
|
|
3721
|
+
if (stderr) {
|
|
3722
|
+
try {
|
|
3723
|
+
const audit = JSON.parse(stderr);
|
|
3724
|
+
const meta = audit.metadata || {};
|
|
3725
|
+
const total = (meta.vulnerabilities?.critical || 0) + (meta.vulnerabilities?.high || 0);
|
|
3726
|
+
if (total > 0) {
|
|
3727
|
+
findings.push({
|
|
3728
|
+
rule: "dependency-vulnerability",
|
|
3729
|
+
severity: "high",
|
|
3730
|
+
file: "package.json",
|
|
3731
|
+
message: `${total} critical/high dependency vulnerabilities found`,
|
|
3732
|
+
detail: `Run '${auditCmd} audit' for details`
|
|
3733
|
+
});
|
|
3734
|
+
}
|
|
3735
|
+
} catch {
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
return findings;
|
|
3740
|
+
}
|
|
3741
|
+
function runAllScans(rootDir) {
|
|
3742
|
+
const allFindings = [];
|
|
3743
|
+
allFindings.push(...scanForHardcodedSecrets(rootDir));
|
|
3744
|
+
allFindings.push(...scanForUnsafePatterns(rootDir));
|
|
3745
|
+
allFindings.push(...scanForCommittedEnvFiles(rootDir));
|
|
3746
|
+
allFindings.push(...scanDependencyVulnerabilities(rootDir));
|
|
3747
|
+
const severityOrder = {
|
|
3748
|
+
critical: 0,
|
|
3749
|
+
high: 1,
|
|
3750
|
+
medium: 2,
|
|
3751
|
+
low: 3
|
|
3752
|
+
};
|
|
3753
|
+
allFindings.sort((a, b) => (severityOrder[a.severity] ?? 3) - (severityOrder[b.severity] ?? 3));
|
|
3754
|
+
return allFindings;
|
|
3755
|
+
}
|
|
3756
|
+
var init_scanner = __esm({
|
|
3757
|
+
"src/watch/plugins/security-scan/scanner.ts"() {
|
|
3758
|
+
"use strict";
|
|
3759
|
+
init_rules();
|
|
3760
|
+
}
|
|
3761
|
+
});
|
|
3762
|
+
|
|
3763
|
+
// src/watch/plugins/security-scan/index.ts
|
|
3764
|
+
function groupFindingsByRule(findings) {
|
|
3765
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3766
|
+
for (const finding of findings) {
|
|
3767
|
+
const existing = groups.get(finding.rule) || [];
|
|
3768
|
+
existing.push(finding);
|
|
3769
|
+
groups.set(finding.rule, existing);
|
|
3770
|
+
}
|
|
3771
|
+
return groups;
|
|
3772
|
+
}
|
|
3773
|
+
function buildBatchedTitle(rule, findings) {
|
|
3774
|
+
if (findings.length === 1) {
|
|
3775
|
+
return `[Security] ${findings[0].message}`;
|
|
3776
|
+
}
|
|
3777
|
+
const firstMessage = findings[0].message;
|
|
3778
|
+
return `[Security] ${findings.length}x: ${firstMessage.split(":")[0]}`;
|
|
3779
|
+
}
|
|
3780
|
+
function buildBatchedBody(rule, findings) {
|
|
3781
|
+
let body = `## Security Finding
|
|
3782
|
+
|
|
3783
|
+
`;
|
|
3784
|
+
body += `**Rule:** \`${rule}\`
|
|
3785
|
+
`;
|
|
3786
|
+
body += `**Findings:** ${findings.length}
|
|
3787
|
+
|
|
3788
|
+
`;
|
|
3789
|
+
body += `### Affected Files
|
|
3790
|
+
|
|
3791
|
+
`;
|
|
3792
|
+
for (const f of findings) {
|
|
3793
|
+
body += `- \`${f.file}${f.line ? `:${f.line}` : ""}\` \u2014 ${f.message}
|
|
3794
|
+
`;
|
|
3795
|
+
}
|
|
3796
|
+
body += `
|
|
3797
|
+
### Details
|
|
3798
|
+
|
|
3799
|
+
`;
|
|
3800
|
+
body += "```\n" + findings[0].detail + "\n```\n\n";
|
|
3801
|
+
if (findings.length > 1) {
|
|
3802
|
+
body += `_${findings.length - 1} additional files have the same issue._
|
|
3803
|
+
|
|
3804
|
+
`;
|
|
3805
|
+
}
|
|
3806
|
+
body += `_Auto-generated by Kody Watch security-scan on ${(/* @__PURE__ */ new Date()).toISOString()}_`;
|
|
3807
|
+
return body;
|
|
3808
|
+
}
|
|
3809
|
+
function formatDigestMarkdown2(findings, cycleNumber) {
|
|
3810
|
+
const counts = {
|
|
3811
|
+
critical: findings.filter((f) => f.severity === "critical").length,
|
|
3812
|
+
high: findings.filter((f) => f.severity === "high").length,
|
|
3813
|
+
medium: findings.filter((f) => f.severity === "medium").length,
|
|
3814
|
+
low: findings.filter((f) => f.severity === "low").length
|
|
3815
|
+
};
|
|
3816
|
+
let md = `## Security Scan \u2014 Cycle #${cycleNumber}
|
|
3817
|
+
|
|
3818
|
+
`;
|
|
3819
|
+
md += `| Severity | Count |
|
|
3820
|
+
|----------|-------|
|
|
3821
|
+
`;
|
|
3822
|
+
md += `| Critical | ${counts.critical} |
|
|
3823
|
+
`;
|
|
3824
|
+
md += `| High | ${counts.high} |
|
|
3825
|
+
`;
|
|
3826
|
+
md += `| Medium | ${counts.medium} |
|
|
3827
|
+
`;
|
|
3828
|
+
md += `| Low | ${counts.low} |
|
|
3829
|
+
|
|
3830
|
+
`;
|
|
3831
|
+
if (findings.length > 0) {
|
|
3832
|
+
md += `### Findings
|
|
3833
|
+
|
|
3834
|
+
`;
|
|
3835
|
+
md += `| Severity | File | Message |
|
|
3836
|
+
|----------|------|---------|
|
|
3837
|
+
`;
|
|
3838
|
+
for (const f of findings.slice(0, 20)) {
|
|
3839
|
+
md += `| ${f.severity} | \`${f.file}${f.line ? `:${f.line}` : ""}\` | ${f.message} |
|
|
3840
|
+
`;
|
|
3841
|
+
}
|
|
3842
|
+
if (findings.length > 20) {
|
|
3843
|
+
md += `
|
|
3844
|
+
_... and ${findings.length - 20} more findings_
|
|
3845
|
+
`;
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
md += `
|
|
3849
|
+
_Generated by Kody Watch on ${(/* @__PURE__ */ new Date()).toISOString()}_`;
|
|
3850
|
+
return md;
|
|
3851
|
+
}
|
|
3852
|
+
var DEDUP_WINDOW_MINUTES, SECURITY_LABEL, MAX_ISSUES_PER_CYCLE, securityScanPlugin;
|
|
3853
|
+
var init_security_scan = __esm({
|
|
3854
|
+
"src/watch/plugins/security-scan/index.ts"() {
|
|
3855
|
+
"use strict";
|
|
3856
|
+
init_scanner();
|
|
3857
|
+
DEDUP_WINDOW_MINUTES = 23 * 60;
|
|
3858
|
+
SECURITY_LABEL = "kody:security";
|
|
3859
|
+
MAX_ISSUES_PER_CYCLE = 3;
|
|
3860
|
+
securityScanPlugin = {
|
|
3861
|
+
name: "security-scan",
|
|
3862
|
+
description: "Broad security audit: secrets, dependency CVEs, unsafe patterns, committed env files",
|
|
3863
|
+
domain: "security",
|
|
3864
|
+
schedule: { every: 48 },
|
|
3865
|
+
async run(ctx) {
|
|
3866
|
+
ctx.log.debug("Running security-scan plugin");
|
|
3867
|
+
const findings = runAllScans(process.cwd());
|
|
3868
|
+
if (findings.length === 0) {
|
|
3869
|
+
ctx.log.info("No security findings");
|
|
3870
|
+
return [];
|
|
3871
|
+
}
|
|
3872
|
+
ctx.log.info(
|
|
3873
|
+
{
|
|
3874
|
+
total: findings.length,
|
|
3875
|
+
critical: findings.filter((f) => f.severity === "critical").length,
|
|
3876
|
+
high: findings.filter((f) => f.severity === "high").length
|
|
3877
|
+
},
|
|
3878
|
+
"Security scan complete"
|
|
3879
|
+
);
|
|
3880
|
+
const actions = [];
|
|
3881
|
+
if (ctx.digestIssue) {
|
|
3882
|
+
actions.push({
|
|
3883
|
+
plugin: "security-scan",
|
|
3884
|
+
type: "digest",
|
|
3885
|
+
urgency: "info",
|
|
3886
|
+
title: "Security Scan Report",
|
|
3887
|
+
detail: `${findings.length} findings`,
|
|
3888
|
+
dedupKey: "security-scan:digest-daily",
|
|
3889
|
+
dedupWindowMinutes: DEDUP_WINDOW_MINUTES,
|
|
3890
|
+
async execute(execCtx) {
|
|
3891
|
+
if (!execCtx.digestIssue) return { success: false, message: "No digest issue" };
|
|
3892
|
+
const markdown = formatDigestMarkdown2(findings, execCtx.cycleNumber);
|
|
3893
|
+
execCtx.github.postComment(execCtx.digestIssue, markdown);
|
|
3894
|
+
return { success: true, message: "Digest posted" };
|
|
3895
|
+
}
|
|
3896
|
+
});
|
|
3897
|
+
}
|
|
3898
|
+
const criticalFindings = findings.filter((f) => f.severity === "critical");
|
|
3899
|
+
const grouped = groupFindingsByRule(criticalFindings);
|
|
3900
|
+
let issueCount = 0;
|
|
3901
|
+
for (const [rule, ruleFindings] of grouped) {
|
|
3902
|
+
if (issueCount >= MAX_ISSUES_PER_CYCLE) break;
|
|
3903
|
+
const dedupKey = `security-scan:issue:${rule}`;
|
|
3904
|
+
const title = buildBatchedTitle(rule, ruleFindings);
|
|
3905
|
+
actions.push({
|
|
3906
|
+
plugin: "security-scan",
|
|
3907
|
+
type: "create-issue",
|
|
3908
|
+
urgency: "critical",
|
|
3909
|
+
title,
|
|
3910
|
+
detail: `${ruleFindings.length} finding(s) for rule ${rule}`,
|
|
3911
|
+
dedupKey,
|
|
3912
|
+
dedupWindowMinutes: DEDUP_WINDOW_MINUTES,
|
|
3913
|
+
async execute(execCtx) {
|
|
3914
|
+
const existing = execCtx.github.searchIssues(`"[Security]" label:${SECURITY_LABEL} is:open "${rule}"`);
|
|
3915
|
+
if (existing.length > 0) {
|
|
3916
|
+
return { success: true, message: `Issue already exists (#${existing[0].number})` };
|
|
3917
|
+
}
|
|
3918
|
+
const body = buildBatchedBody(rule, ruleFindings);
|
|
3919
|
+
const issueNumber = execCtx.github.createIssue(title, body, [SECURITY_LABEL]);
|
|
3920
|
+
return issueNumber ? { success: true, message: `Created issue #${issueNumber}` } : { success: false, message: "Failed to create issue" };
|
|
3921
|
+
}
|
|
3922
|
+
});
|
|
3923
|
+
issueCount++;
|
|
3924
|
+
}
|
|
3925
|
+
return actions;
|
|
3926
|
+
}
|
|
3927
|
+
};
|
|
3928
|
+
}
|
|
3929
|
+
});
|
|
3930
|
+
|
|
3931
|
+
// src/watch/plugins/config-health/index.ts
|
|
3932
|
+
import * as fs18 from "fs";
|
|
3933
|
+
import * as path16 from "path";
|
|
3934
|
+
import { execFileSync as execFileSync13 } from "child_process";
|
|
3935
|
+
function validateConfig(cwd, repo) {
|
|
3936
|
+
const findings = [];
|
|
3937
|
+
const configPath = path16.join(cwd, "kody.config.json");
|
|
3938
|
+
if (!fs18.existsSync(configPath)) {
|
|
3939
|
+
findings.push({
|
|
3940
|
+
check: "config-exists",
|
|
3941
|
+
severity: "error",
|
|
3942
|
+
message: "kody.config.json not found"
|
|
3943
|
+
});
|
|
3944
|
+
return findings;
|
|
3945
|
+
}
|
|
3946
|
+
let config;
|
|
3947
|
+
try {
|
|
3948
|
+
config = JSON.parse(fs18.readFileSync(configPath, "utf-8"));
|
|
3949
|
+
} catch {
|
|
3950
|
+
findings.push({
|
|
3951
|
+
check: "config-valid-json",
|
|
3952
|
+
severity: "error",
|
|
3953
|
+
message: "kody.config.json is not valid JSON"
|
|
3954
|
+
});
|
|
3955
|
+
return findings;
|
|
3956
|
+
}
|
|
3957
|
+
const github = config.github;
|
|
3958
|
+
if (!github?.owner || !github?.repo) {
|
|
3959
|
+
findings.push({
|
|
3960
|
+
check: "config-github",
|
|
3961
|
+
severity: "error",
|
|
3962
|
+
message: "Missing github.owner or github.repo in kody.config.json"
|
|
3963
|
+
});
|
|
3964
|
+
}
|
|
3965
|
+
const quality = config.quality;
|
|
3966
|
+
if (!quality?.testUnit) {
|
|
3967
|
+
findings.push({
|
|
3968
|
+
check: "config-test-command",
|
|
3969
|
+
severity: "warning",
|
|
3970
|
+
message: "No quality.testUnit command configured \u2014 Kody won't run tests"
|
|
3971
|
+
});
|
|
3972
|
+
}
|
|
3973
|
+
if (quality) {
|
|
3974
|
+
const pkgPath = path16.join(cwd, "package.json");
|
|
3975
|
+
if (fs18.existsSync(pkgPath)) {
|
|
3976
|
+
try {
|
|
3977
|
+
const pkg = JSON.parse(fs18.readFileSync(pkgPath, "utf-8"));
|
|
3978
|
+
const scripts = pkg.scripts || {};
|
|
3979
|
+
for (const [key, cmd] of Object.entries(quality)) {
|
|
3980
|
+
if (typeof cmd !== "string" || !cmd) continue;
|
|
3981
|
+
const parts = String(cmd).split(/\s+/);
|
|
3982
|
+
const scriptName = parts.length > 1 ? parts[1] : parts[0];
|
|
3983
|
+
if (scriptName && !scriptName.startsWith("-") && !(scriptName in scripts)) {
|
|
3984
|
+
if (["pnpm", "npm", "yarn"].includes(parts[0]) && !(scriptName in scripts)) {
|
|
3985
|
+
findings.push({
|
|
3986
|
+
check: `config-quality-${key}`,
|
|
3987
|
+
severity: "warning",
|
|
3988
|
+
message: `quality.${key} references script '${scriptName}' which doesn't exist in package.json`
|
|
3989
|
+
});
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
}
|
|
3993
|
+
} catch {
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
const kodyDir = path16.join(cwd, ".kody");
|
|
3998
|
+
if (!fs18.existsSync(kodyDir)) {
|
|
3999
|
+
findings.push({
|
|
4000
|
+
check: "kody-dir",
|
|
4001
|
+
severity: "warning",
|
|
4002
|
+
message: ".kody/ directory missing \u2014 run bootstrap to initialize"
|
|
4003
|
+
});
|
|
4004
|
+
}
|
|
4005
|
+
if (repo) {
|
|
4006
|
+
try {
|
|
4007
|
+
const output = execFileSync13("gh", ["secret", "list", "--repo", repo], {
|
|
4008
|
+
encoding: "utf-8",
|
|
4009
|
+
timeout: 1e4,
|
|
4010
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
4011
|
+
});
|
|
4012
|
+
if (!output.includes("ANTHROPIC_API_KEY")) {
|
|
4013
|
+
findings.push({
|
|
4014
|
+
check: "secret-anthropic",
|
|
4015
|
+
severity: "error",
|
|
4016
|
+
message: "ANTHROPIC_API_KEY secret not found \u2014 Kody pipeline will fail"
|
|
4017
|
+
});
|
|
4018
|
+
}
|
|
4019
|
+
} catch {
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
4022
|
+
return findings;
|
|
4023
|
+
}
|
|
4024
|
+
function formatDigestMarkdown3(findings, cycleNumber) {
|
|
4025
|
+
let md = `## Config Health \u2014 Cycle #${cycleNumber}
|
|
4026
|
+
|
|
4027
|
+
`;
|
|
4028
|
+
md += `| Check | Severity | Message |
|
|
4029
|
+
|-------|----------|---------|
|
|
4030
|
+
`;
|
|
4031
|
+
for (const f of findings) {
|
|
4032
|
+
const icon = f.severity === "error" ? "\u{1F534}" : "\u{1F7E1}";
|
|
4033
|
+
md += `| ${f.check} | ${icon} ${f.severity} | ${f.message} |
|
|
4034
|
+
`;
|
|
4035
|
+
}
|
|
4036
|
+
md += `
|
|
4037
|
+
_Generated by Kody Watch on ${(/* @__PURE__ */ new Date()).toISOString()}_`;
|
|
4038
|
+
return md;
|
|
4039
|
+
}
|
|
4040
|
+
var configHealthPlugin;
|
|
4041
|
+
var init_config_health = __esm({
|
|
4042
|
+
"src/watch/plugins/config-health/index.ts"() {
|
|
4043
|
+
"use strict";
|
|
4044
|
+
configHealthPlugin = {
|
|
4045
|
+
name: "config-health",
|
|
4046
|
+
description: "Validate kody.config.json, secrets, quality commands, and .kody/ integrity",
|
|
4047
|
+
domain: "config",
|
|
4048
|
+
schedule: { every: 48 },
|
|
4049
|
+
async run(ctx) {
|
|
4050
|
+
const findings = validateConfig(process.cwd(), ctx.repo);
|
|
4051
|
+
if (findings.length === 0) {
|
|
4052
|
+
ctx.log.info("Config health check passed");
|
|
4053
|
+
return [];
|
|
4054
|
+
}
|
|
4055
|
+
ctx.log.info(
|
|
4056
|
+
{ total: findings.length, errors: findings.filter((f) => f.severity === "error").length },
|
|
4057
|
+
"Config health issues found"
|
|
4058
|
+
);
|
|
4059
|
+
const actions = [];
|
|
4060
|
+
if (ctx.digestIssue) {
|
|
4061
|
+
actions.push({
|
|
4062
|
+
plugin: "config-health",
|
|
4063
|
+
type: "digest",
|
|
4064
|
+
urgency: "warning",
|
|
4065
|
+
title: "Config Health Report",
|
|
4066
|
+
detail: `${findings.length} issue(s) found`,
|
|
4067
|
+
dedupKey: "config-health:digest-daily",
|
|
4068
|
+
dedupWindowMinutes: 23 * 60,
|
|
4069
|
+
async execute(execCtx) {
|
|
4070
|
+
if (!execCtx.digestIssue) return { success: false, message: "No digest issue" };
|
|
4071
|
+
const markdown = formatDigestMarkdown3(findings, execCtx.cycleNumber);
|
|
4072
|
+
execCtx.github.postComment(execCtx.digestIssue, markdown);
|
|
4073
|
+
return { success: true, message: `Reported ${findings.length} config issue(s)` };
|
|
4074
|
+
}
|
|
4075
|
+
});
|
|
4076
|
+
}
|
|
4077
|
+
return actions;
|
|
4078
|
+
}
|
|
4079
|
+
};
|
|
4080
|
+
}
|
|
4081
|
+
});
|
|
4082
|
+
|
|
4083
|
+
// src/watch/index.ts
|
|
4084
|
+
var watch_exports = {};
|
|
4085
|
+
__export(watch_exports, {
|
|
4086
|
+
runWatchCommand: () => runWatchCommand
|
|
4087
|
+
});
|
|
4088
|
+
import * as fs19 from "fs";
|
|
4089
|
+
import * as path17 from "path";
|
|
4090
|
+
async function runWatchCommand(opts) {
|
|
4091
|
+
const cwd = process.cwd();
|
|
4092
|
+
const configPath = path17.join(cwd, "kody.config.json");
|
|
4093
|
+
let repo = process.env.REPO || "";
|
|
4094
|
+
let digestIssue;
|
|
4095
|
+
if (!repo && fs19.existsSync(configPath)) {
|
|
4096
|
+
try {
|
|
4097
|
+
const config2 = JSON.parse(fs19.readFileSync(configPath, "utf-8"));
|
|
4098
|
+
if (config2.github?.owner && config2.github?.repo) {
|
|
4099
|
+
repo = `${config2.github.owner}/${config2.github.repo}`;
|
|
4100
|
+
}
|
|
4101
|
+
if (config2.watch?.digestIssue) {
|
|
4102
|
+
digestIssue = config2.watch.digestIssue;
|
|
4103
|
+
}
|
|
4104
|
+
} catch {
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
if (process.env.WATCH_DIGEST_ISSUE) {
|
|
4108
|
+
digestIssue = parseInt(process.env.WATCH_DIGEST_ISSUE, 10) || void 0;
|
|
4109
|
+
}
|
|
4110
|
+
if (!repo) {
|
|
4111
|
+
console.error("Missing repo \u2014 set REPO env var or configure github.owner/repo in kody.config.json");
|
|
4112
|
+
process.exit(1);
|
|
4113
|
+
}
|
|
4114
|
+
if (!process.env.GH_TOKEN && !process.env.GITHUB_TOKEN) {
|
|
4115
|
+
console.error("Missing GH_TOKEN or GITHUB_TOKEN");
|
|
4116
|
+
process.exit(1);
|
|
4117
|
+
}
|
|
4118
|
+
const registry = createPluginRegistry();
|
|
4119
|
+
registry.register(pipelineHealthPlugin);
|
|
4120
|
+
registry.register(securityScanPlugin);
|
|
4121
|
+
registry.register(configHealthPlugin);
|
|
4122
|
+
const config = {
|
|
4123
|
+
repo,
|
|
4124
|
+
dryRun: opts.dryRun,
|
|
4125
|
+
stateFile: path17.join(cwd, ".kody", "watch-state.json"),
|
|
4126
|
+
plugins: registry.getAll(),
|
|
4127
|
+
digestIssue
|
|
4128
|
+
};
|
|
4129
|
+
console.log(`
|
|
4130
|
+
Kody Watch \u2014 repo: ${repo}, dry-run: ${opts.dryRun}
|
|
4131
|
+
`);
|
|
4132
|
+
try {
|
|
4133
|
+
const result2 = await runWatch(config);
|
|
4134
|
+
if (result2.errors.length > 0) {
|
|
4135
|
+
for (const error of result2.errors) {
|
|
4136
|
+
console.warn(` Warning: ${error}`);
|
|
4137
|
+
}
|
|
4138
|
+
}
|
|
4139
|
+
console.log(`
|
|
4140
|
+
Cycle #${result2.cycleNumber} complete: ${result2.pluginsRun} plugins, ${result2.actionsExecuted} actions, ${result2.actionsDeduplicated} deduplicated`);
|
|
4141
|
+
process.exit(0);
|
|
4142
|
+
} catch (error) {
|
|
4143
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4144
|
+
console.error(`Watch failed: ${message}`);
|
|
4145
|
+
process.exit(1);
|
|
4146
|
+
}
|
|
4147
|
+
}
|
|
4148
|
+
var init_watch2 = __esm({
|
|
4149
|
+
"src/watch/index.ts"() {
|
|
4150
|
+
"use strict";
|
|
4151
|
+
init_watch();
|
|
4152
|
+
init_registry();
|
|
4153
|
+
init_pipeline_health();
|
|
4154
|
+
init_security_scan();
|
|
4155
|
+
init_config_health();
|
|
4156
|
+
}
|
|
4157
|
+
});
|
|
4158
|
+
|
|
2969
4159
|
// src/definitions.ts
|
|
2970
4160
|
var definitions_exports = {};
|
|
2971
4161
|
__export(definitions_exports, {
|
|
@@ -3047,7 +4237,7 @@ var init_definitions = __esm({
|
|
|
3047
4237
|
});
|
|
3048
4238
|
|
|
3049
4239
|
// src/git-utils.ts
|
|
3050
|
-
import { execFileSync as
|
|
4240
|
+
import { execFileSync as execFileSync14 } from "child_process";
|
|
3051
4241
|
function getHookSafeEnv() {
|
|
3052
4242
|
if (!_hookSafeEnv) {
|
|
3053
4243
|
_hookSafeEnv = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
|
|
@@ -3055,7 +4245,7 @@ function getHookSafeEnv() {
|
|
|
3055
4245
|
return _hookSafeEnv;
|
|
3056
4246
|
}
|
|
3057
4247
|
function git(args2, options) {
|
|
3058
|
-
return
|
|
4248
|
+
return execFileSync14("git", args2, {
|
|
3059
4249
|
encoding: "utf-8",
|
|
3060
4250
|
timeout: options?.timeout ?? 3e4,
|
|
3061
4251
|
cwd: options?.cwd,
|
|
@@ -3241,14 +4431,14 @@ var init_git_utils = __esm({
|
|
|
3241
4431
|
});
|
|
3242
4432
|
|
|
3243
4433
|
// src/pipeline/state.ts
|
|
3244
|
-
import * as
|
|
3245
|
-
import * as
|
|
4434
|
+
import * as fs20 from "fs";
|
|
4435
|
+
import * as path18 from "path";
|
|
3246
4436
|
function loadState(taskId, taskDir) {
|
|
3247
|
-
const p =
|
|
3248
|
-
if (!
|
|
4437
|
+
const p = path18.join(taskDir, "status.json");
|
|
4438
|
+
if (!fs20.existsSync(p)) return null;
|
|
3249
4439
|
try {
|
|
3250
4440
|
const result2 = parseJsonSafe(
|
|
3251
|
-
|
|
4441
|
+
fs20.readFileSync(p, "utf-8"),
|
|
3252
4442
|
["taskId", "state", "stages", "createdAt", "updatedAt"]
|
|
3253
4443
|
);
|
|
3254
4444
|
if (!result2.ok) {
|
|
@@ -3266,10 +4456,10 @@ function writeState(state, taskDir) {
|
|
|
3266
4456
|
...state,
|
|
3267
4457
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3268
4458
|
};
|
|
3269
|
-
const target =
|
|
4459
|
+
const target = path18.join(taskDir, "status.json");
|
|
3270
4460
|
const tmp = target + ".tmp";
|
|
3271
|
-
|
|
3272
|
-
|
|
4461
|
+
fs20.writeFileSync(tmp, JSON.stringify(updated, null, 2));
|
|
4462
|
+
fs20.renameSync(tmp, target);
|
|
3273
4463
|
return updated;
|
|
3274
4464
|
}
|
|
3275
4465
|
function initState(taskId) {
|
|
@@ -3280,7 +4470,7 @@ function initState(taskId) {
|
|
|
3280
4470
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3281
4471
|
return { taskId, state: "running", stages, createdAt: now, updatedAt: now };
|
|
3282
4472
|
}
|
|
3283
|
-
var
|
|
4473
|
+
var init_state2 = __esm({
|
|
3284
4474
|
"src/pipeline/state.ts"() {
|
|
3285
4475
|
"use strict";
|
|
3286
4476
|
init_definitions();
|
|
@@ -3310,16 +4500,16 @@ var init_complexity = __esm({
|
|
|
3310
4500
|
});
|
|
3311
4501
|
|
|
3312
4502
|
// src/memory.ts
|
|
3313
|
-
import * as
|
|
3314
|
-
import * as
|
|
4503
|
+
import * as fs21 from "fs";
|
|
4504
|
+
import * as path19 from "path";
|
|
3315
4505
|
function readProjectMemory(projectDir) {
|
|
3316
|
-
const memoryDir =
|
|
3317
|
-
if (!
|
|
3318
|
-
const files =
|
|
4506
|
+
const memoryDir = path19.join(projectDir, ".kody", "memory");
|
|
4507
|
+
if (!fs21.existsSync(memoryDir)) return "";
|
|
4508
|
+
const files = fs21.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
3319
4509
|
if (files.length === 0) return "";
|
|
3320
4510
|
const sections = [];
|
|
3321
4511
|
for (const file of files) {
|
|
3322
|
-
const content =
|
|
4512
|
+
const content = fs21.readFileSync(path19.join(memoryDir, file), "utf-8").trim();
|
|
3323
4513
|
if (content) {
|
|
3324
4514
|
sections.push(`## ${file.replace(".md", "")}
|
|
3325
4515
|
${content}`);
|
|
@@ -3338,8 +4528,8 @@ var init_memory = __esm({
|
|
|
3338
4528
|
});
|
|
3339
4529
|
|
|
3340
4530
|
// src/context-tiers.ts
|
|
3341
|
-
import * as
|
|
3342
|
-
import * as
|
|
4531
|
+
import * as fs22 from "fs";
|
|
4532
|
+
import * as path20 from "path";
|
|
3343
4533
|
function estimateTokens(text) {
|
|
3344
4534
|
return Math.ceil(text.length / 4);
|
|
3345
4535
|
}
|
|
@@ -3430,7 +4620,7 @@ function generateL1Json(content) {
|
|
|
3430
4620
|
}
|
|
3431
4621
|
}
|
|
3432
4622
|
function getTieredContent(filePath, content) {
|
|
3433
|
-
const key =
|
|
4623
|
+
const key = path20.basename(filePath);
|
|
3434
4624
|
return {
|
|
3435
4625
|
source: filePath,
|
|
3436
4626
|
L0: generateL0(content, key),
|
|
@@ -3442,15 +4632,15 @@ function selectTier(tiered, tier) {
|
|
|
3442
4632
|
return tiered[tier];
|
|
3443
4633
|
}
|
|
3444
4634
|
function readProjectMemoryTiered(projectDir, tier) {
|
|
3445
|
-
const memoryDir =
|
|
3446
|
-
if (!
|
|
3447
|
-
const files =
|
|
4635
|
+
const memoryDir = path20.join(projectDir, ".kody", "memory");
|
|
4636
|
+
if (!fs22.existsSync(memoryDir)) return "";
|
|
4637
|
+
const files = fs22.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
3448
4638
|
if (files.length === 0) return "";
|
|
3449
4639
|
const tierLabel2 = tier === "L2" ? "full" : tier === "L1" ? "overview" : "abstract";
|
|
3450
4640
|
const sections = [];
|
|
3451
4641
|
for (const file of files) {
|
|
3452
|
-
const filePath =
|
|
3453
|
-
const content =
|
|
4642
|
+
const filePath = path20.join(memoryDir, file);
|
|
4643
|
+
const content = fs22.readFileSync(filePath, "utf-8").trim();
|
|
3454
4644
|
if (!content) continue;
|
|
3455
4645
|
const tiered = getTieredContent(filePath, content);
|
|
3456
4646
|
const selected = selectTier(tiered, tier);
|
|
@@ -3473,9 +4663,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
|
|
|
3473
4663
|
`;
|
|
3474
4664
|
context += `Task Directory: ${taskDir}
|
|
3475
4665
|
`;
|
|
3476
|
-
const taskMdPath =
|
|
3477
|
-
if (
|
|
3478
|
-
const content =
|
|
4666
|
+
const taskMdPath = path20.join(taskDir, "task.md");
|
|
4667
|
+
if (fs22.existsSync(taskMdPath)) {
|
|
4668
|
+
const content = fs22.readFileSync(taskMdPath, "utf-8");
|
|
3479
4669
|
const selected = selectContent(taskMdPath, content, policy.taskDescription);
|
|
3480
4670
|
const label = tierLabel("Task Description", policy.taskDescription);
|
|
3481
4671
|
context += `
|
|
@@ -3483,9 +4673,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
|
|
|
3483
4673
|
${selected}
|
|
3484
4674
|
`;
|
|
3485
4675
|
}
|
|
3486
|
-
const taskJsonPath =
|
|
3487
|
-
if (
|
|
3488
|
-
const content =
|
|
4676
|
+
const taskJsonPath = path20.join(taskDir, "task.json");
|
|
4677
|
+
if (fs22.existsSync(taskJsonPath)) {
|
|
4678
|
+
const content = fs22.readFileSync(taskJsonPath, "utf-8");
|
|
3489
4679
|
if (policy.taskClassification === "L2") {
|
|
3490
4680
|
try {
|
|
3491
4681
|
const taskDef = JSON.parse(content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, ""));
|
|
@@ -3511,9 +4701,9 @@ ${selected}
|
|
|
3511
4701
|
}
|
|
3512
4702
|
}
|
|
3513
4703
|
}
|
|
3514
|
-
const specPath =
|
|
3515
|
-
if (
|
|
3516
|
-
const content =
|
|
4704
|
+
const specPath = path20.join(taskDir, "spec.md");
|
|
4705
|
+
if (fs22.existsSync(specPath)) {
|
|
4706
|
+
const content = fs22.readFileSync(specPath, "utf-8");
|
|
3517
4707
|
const selected = selectContent(specPath, content, policy.spec);
|
|
3518
4708
|
const label = tierLabel("Spec", policy.spec);
|
|
3519
4709
|
context += `
|
|
@@ -3521,9 +4711,9 @@ ${selected}
|
|
|
3521
4711
|
${selected}
|
|
3522
4712
|
`;
|
|
3523
4713
|
}
|
|
3524
|
-
const planPath =
|
|
3525
|
-
if (
|
|
3526
|
-
const content =
|
|
4714
|
+
const planPath = path20.join(taskDir, "plan.md");
|
|
4715
|
+
if (fs22.existsSync(planPath)) {
|
|
4716
|
+
const content = fs22.readFileSync(planPath, "utf-8");
|
|
3527
4717
|
const selected = selectContent(planPath, content, policy.plan);
|
|
3528
4718
|
const label = tierLabel("Plan", policy.plan);
|
|
3529
4719
|
context += `
|
|
@@ -3531,9 +4721,9 @@ ${selected}
|
|
|
3531
4721
|
${selected}
|
|
3532
4722
|
`;
|
|
3533
4723
|
}
|
|
3534
|
-
const contextMdPath =
|
|
3535
|
-
if (
|
|
3536
|
-
const content =
|
|
4724
|
+
const contextMdPath = path20.join(taskDir, "context.md");
|
|
4725
|
+
if (fs22.existsSync(contextMdPath)) {
|
|
4726
|
+
const content = fs22.readFileSync(contextMdPath, "utf-8");
|
|
3537
4727
|
const selected = selectContent(contextMdPath, content, policy.accumulatedContext);
|
|
3538
4728
|
const label = tierLabel("Previous Stage Context", policy.accumulatedContext);
|
|
3539
4729
|
context += `
|
|
@@ -3619,24 +4809,24 @@ var init_context_tiers = __esm({
|
|
|
3619
4809
|
});
|
|
3620
4810
|
|
|
3621
4811
|
// src/context.ts
|
|
3622
|
-
import * as
|
|
3623
|
-
import * as
|
|
4812
|
+
import * as fs23 from "fs";
|
|
4813
|
+
import * as path21 from "path";
|
|
3624
4814
|
function readPromptFile(stageName, projectDir) {
|
|
3625
4815
|
if (projectDir) {
|
|
3626
|
-
const stepFile =
|
|
3627
|
-
if (
|
|
3628
|
-
return
|
|
4816
|
+
const stepFile = path21.join(projectDir, ".kody", "steps", `${stageName}.md`);
|
|
4817
|
+
if (fs23.existsSync(stepFile)) {
|
|
4818
|
+
return fs23.readFileSync(stepFile, "utf-8");
|
|
3629
4819
|
}
|
|
3630
4820
|
console.warn(` \u26A0 No step file at ${stepFile}, falling back to engine defaults. Run 'kody-engine-lite init --force' to generate step files.`);
|
|
3631
4821
|
}
|
|
3632
4822
|
const scriptDir = new URL(".", import.meta.url).pathname;
|
|
3633
4823
|
const candidates = [
|
|
3634
|
-
|
|
3635
|
-
|
|
4824
|
+
path21.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
|
|
4825
|
+
path21.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
|
|
3636
4826
|
];
|
|
3637
4827
|
for (const candidate of candidates) {
|
|
3638
|
-
if (
|
|
3639
|
-
return
|
|
4828
|
+
if (fs23.existsSync(candidate)) {
|
|
4829
|
+
return fs23.readFileSync(candidate, "utf-8");
|
|
3640
4830
|
}
|
|
3641
4831
|
}
|
|
3642
4832
|
throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
|
|
@@ -3648,18 +4838,18 @@ function injectTaskContext(prompt, taskId, taskDir, feedback) {
|
|
|
3648
4838
|
`;
|
|
3649
4839
|
context += `Task Directory: ${taskDir}
|
|
3650
4840
|
`;
|
|
3651
|
-
const taskMdPath =
|
|
3652
|
-
if (
|
|
3653
|
-
const taskMd =
|
|
4841
|
+
const taskMdPath = path21.join(taskDir, "task.md");
|
|
4842
|
+
if (fs23.existsSync(taskMdPath)) {
|
|
4843
|
+
const taskMd = fs23.readFileSync(taskMdPath, "utf-8");
|
|
3654
4844
|
context += `
|
|
3655
4845
|
## Task Description
|
|
3656
4846
|
${taskMd}
|
|
3657
4847
|
`;
|
|
3658
4848
|
}
|
|
3659
|
-
const taskJsonPath =
|
|
3660
|
-
if (
|
|
4849
|
+
const taskJsonPath = path21.join(taskDir, "task.json");
|
|
4850
|
+
if (fs23.existsSync(taskJsonPath)) {
|
|
3661
4851
|
try {
|
|
3662
|
-
const taskDef = JSON.parse(
|
|
4852
|
+
const taskDef = JSON.parse(fs23.readFileSync(taskJsonPath, "utf-8"));
|
|
3663
4853
|
context += `
|
|
3664
4854
|
## Task Classification
|
|
3665
4855
|
`;
|
|
@@ -3672,27 +4862,27 @@ ${taskMd}
|
|
|
3672
4862
|
} catch {
|
|
3673
4863
|
}
|
|
3674
4864
|
}
|
|
3675
|
-
const specPath =
|
|
3676
|
-
if (
|
|
3677
|
-
const spec =
|
|
4865
|
+
const specPath = path21.join(taskDir, "spec.md");
|
|
4866
|
+
if (fs23.existsSync(specPath)) {
|
|
4867
|
+
const spec = fs23.readFileSync(specPath, "utf-8");
|
|
3678
4868
|
const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
|
|
3679
4869
|
context += `
|
|
3680
4870
|
## Spec Summary
|
|
3681
4871
|
${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
|
|
3682
4872
|
`;
|
|
3683
4873
|
}
|
|
3684
|
-
const planPath =
|
|
3685
|
-
if (
|
|
3686
|
-
const plan =
|
|
4874
|
+
const planPath = path21.join(taskDir, "plan.md");
|
|
4875
|
+
if (fs23.existsSync(planPath)) {
|
|
4876
|
+
const plan = fs23.readFileSync(planPath, "utf-8");
|
|
3687
4877
|
const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
|
|
3688
4878
|
context += `
|
|
3689
4879
|
## Plan Summary
|
|
3690
4880
|
${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
|
|
3691
4881
|
`;
|
|
3692
4882
|
}
|
|
3693
|
-
const contextMdPath =
|
|
3694
|
-
if (
|
|
3695
|
-
const accumulated =
|
|
4883
|
+
const contextMdPath = path21.join(taskDir, "context.md");
|
|
4884
|
+
if (fs23.existsSync(contextMdPath)) {
|
|
4885
|
+
const accumulated = fs23.readFileSync(contextMdPath, "utf-8");
|
|
3696
4886
|
const truncated = accumulated.slice(-MAX_ACCUMULATED_CONTEXT);
|
|
3697
4887
|
const prefix = accumulated.length > MAX_ACCUMULATED_CONTEXT ? "...(earlier context truncated)\n" : "";
|
|
3698
4888
|
context += `
|
|
@@ -3710,17 +4900,17 @@ ${feedback}
|
|
|
3710
4900
|
}
|
|
3711
4901
|
function inferHasUIFromScope(scope) {
|
|
3712
4902
|
return scope.some((filePath) => {
|
|
3713
|
-
const ext =
|
|
4903
|
+
const ext = path21.extname(filePath).toLowerCase();
|
|
3714
4904
|
if (UI_EXTENSIONS.has(ext)) return true;
|
|
3715
4905
|
const normalized = filePath.replace(/\\/g, "/");
|
|
3716
4906
|
return UI_PATH_SEGMENTS.some((seg) => normalized.includes(seg));
|
|
3717
4907
|
});
|
|
3718
4908
|
}
|
|
3719
4909
|
function taskHasUI(taskDir) {
|
|
3720
|
-
const taskJsonPath =
|
|
3721
|
-
if (!
|
|
4910
|
+
const taskJsonPath = path21.join(taskDir, "task.json");
|
|
4911
|
+
if (!fs23.existsSync(taskJsonPath)) return true;
|
|
3722
4912
|
try {
|
|
3723
|
-
const taskDef = JSON.parse(
|
|
4913
|
+
const taskDef = JSON.parse(fs23.readFileSync(taskJsonPath, "utf-8"));
|
|
3724
4914
|
const scope = Array.isArray(taskDef.scope) ? taskDef.scope : [];
|
|
3725
4915
|
if (scope.length === 0) return true;
|
|
3726
4916
|
return inferHasUIFromScope(scope);
|
|
@@ -3878,9 +5068,9 @@ ${prompt}` : prompt;
|
|
|
3878
5068
|
}
|
|
3879
5069
|
if (isMcpEnabledForStage(stageName, config.mcp) && taskHasUI(taskDir)) {
|
|
3880
5070
|
assembled = assembled + "\n\n" + getBrowserToolGuidance(stageName, taskDir);
|
|
3881
|
-
const qaGuidePath =
|
|
3882
|
-
if (
|
|
3883
|
-
const qaGuide =
|
|
5071
|
+
const qaGuidePath = path21.join(projectDir, ".kody", "qa-guide.md");
|
|
5072
|
+
if (fs23.existsSync(qaGuidePath)) {
|
|
5073
|
+
const qaGuide = fs23.readFileSync(qaGuidePath, "utf-8").trim();
|
|
3884
5074
|
assembled = assembled + "\n\n" + qaGuide;
|
|
3885
5075
|
}
|
|
3886
5076
|
}
|
|
@@ -4042,8 +5232,8 @@ var init_dev_server = __esm({
|
|
|
4042
5232
|
});
|
|
4043
5233
|
|
|
4044
5234
|
// src/stages/agent.ts
|
|
4045
|
-
import * as
|
|
4046
|
-
import * as
|
|
5235
|
+
import * as fs24 from "fs";
|
|
5236
|
+
import * as path22 from "path";
|
|
4047
5237
|
function getSessionInfo(stageName, sessions) {
|
|
4048
5238
|
const group = SESSION_GROUP[stageName];
|
|
4049
5239
|
if (!group) return void 0;
|
|
@@ -4157,27 +5347,27 @@ async function executeAgentStage(ctx, def) {
|
|
|
4157
5347
|
}
|
|
4158
5348
|
const result2 = lastResult;
|
|
4159
5349
|
if (def.outputFile && result2.output) {
|
|
4160
|
-
|
|
5350
|
+
fs24.writeFileSync(path22.join(ctx.taskDir, def.outputFile), result2.output);
|
|
4161
5351
|
}
|
|
4162
5352
|
if (def.outputFile) {
|
|
4163
|
-
const outputPath =
|
|
4164
|
-
if (!
|
|
4165
|
-
const ext =
|
|
4166
|
-
const base =
|
|
4167
|
-
const files =
|
|
5353
|
+
const outputPath = path22.join(ctx.taskDir, def.outputFile);
|
|
5354
|
+
if (!fs24.existsSync(outputPath)) {
|
|
5355
|
+
const ext = path22.extname(def.outputFile);
|
|
5356
|
+
const base = path22.basename(def.outputFile, ext);
|
|
5357
|
+
const files = fs24.readdirSync(ctx.taskDir);
|
|
4168
5358
|
const variant = files.find(
|
|
4169
5359
|
(f) => f.startsWith(base + "-") && f.endsWith(ext)
|
|
4170
5360
|
);
|
|
4171
5361
|
if (variant) {
|
|
4172
|
-
|
|
5362
|
+
fs24.renameSync(path22.join(ctx.taskDir, variant), outputPath);
|
|
4173
5363
|
logger.info(` Renamed variant ${variant} \u2192 ${def.outputFile}`);
|
|
4174
5364
|
}
|
|
4175
5365
|
}
|
|
4176
5366
|
}
|
|
4177
5367
|
if (def.outputFile) {
|
|
4178
|
-
const outputPath =
|
|
4179
|
-
if (
|
|
4180
|
-
const content =
|
|
5368
|
+
const outputPath = path22.join(ctx.taskDir, def.outputFile);
|
|
5369
|
+
if (fs24.existsSync(outputPath)) {
|
|
5370
|
+
const content = fs24.readFileSync(outputPath, "utf-8");
|
|
4181
5371
|
const validation = validateStageOutput(def.name, content);
|
|
4182
5372
|
if (!validation.valid) {
|
|
4183
5373
|
if (def.name === "taskify") {
|
|
@@ -4191,7 +5381,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
4191
5381
|
const stripped = stripFences(retryResult.output);
|
|
4192
5382
|
const retryValidation = validateTaskJson(stripped);
|
|
4193
5383
|
if (retryValidation.valid) {
|
|
4194
|
-
|
|
5384
|
+
fs24.writeFileSync(outputPath, retryResult.output);
|
|
4195
5385
|
logger.info(` taskify retry produced valid JSON`);
|
|
4196
5386
|
} else {
|
|
4197
5387
|
logger.warn(` taskify retry still invalid: ${retryValidation.error}`);
|
|
@@ -4204,7 +5394,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
4204
5394
|
risk_level: "low",
|
|
4205
5395
|
questions: []
|
|
4206
5396
|
}, null, 2);
|
|
4207
|
-
|
|
5397
|
+
fs24.writeFileSync(outputPath, fallback);
|
|
4208
5398
|
logger.info(` taskify fallback: generated minimal task.json (risk_level=low)`);
|
|
4209
5399
|
}
|
|
4210
5400
|
}
|
|
@@ -4218,7 +5408,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
4218
5408
|
return { outcome: "completed", outputFile: def.outputFile, retries };
|
|
4219
5409
|
}
|
|
4220
5410
|
function appendStageContext(taskDir, stageName, output) {
|
|
4221
|
-
const contextPath =
|
|
5411
|
+
const contextPath = path22.join(taskDir, "context.md");
|
|
4222
5412
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19);
|
|
4223
5413
|
let summary;
|
|
4224
5414
|
if (output && output.trim()) {
|
|
@@ -4231,7 +5421,7 @@ function appendStageContext(taskDir, stageName, output) {
|
|
|
4231
5421
|
### ${stageName} (${timestamp2})
|
|
4232
5422
|
${summary}
|
|
4233
5423
|
`;
|
|
4234
|
-
|
|
5424
|
+
fs24.appendFileSync(contextPath, entry);
|
|
4235
5425
|
}
|
|
4236
5426
|
var SESSION_GROUP;
|
|
4237
5427
|
var init_agent = __esm({
|
|
@@ -4255,7 +5445,7 @@ var init_agent = __esm({
|
|
|
4255
5445
|
});
|
|
4256
5446
|
|
|
4257
5447
|
// src/verify-runner.ts
|
|
4258
|
-
import { execFileSync as
|
|
5448
|
+
import { execFileSync as execFileSync15 } from "child_process";
|
|
4259
5449
|
function isExecError(err) {
|
|
4260
5450
|
return typeof err === "object" && err !== null;
|
|
4261
5451
|
}
|
|
@@ -4291,7 +5481,7 @@ function runCommand(cmd, cwd, timeout) {
|
|
|
4291
5481
|
return { success: true, output: "", timedOut: false };
|
|
4292
5482
|
}
|
|
4293
5483
|
try {
|
|
4294
|
-
const output =
|
|
5484
|
+
const output = execFileSync15(parts[0], parts.slice(1), {
|
|
4295
5485
|
cwd,
|
|
4296
5486
|
timeout,
|
|
4297
5487
|
encoding: "utf-8",
|
|
@@ -4380,7 +5570,7 @@ var init_verify_runner = __esm({
|
|
|
4380
5570
|
});
|
|
4381
5571
|
|
|
4382
5572
|
// src/observer.ts
|
|
4383
|
-
import { execFileSync as
|
|
5573
|
+
import { execFileSync as execFileSync16 } from "child_process";
|
|
4384
5574
|
async function diagnoseFailure(stageName, errorOutput, modifiedFiles, runner, model, options) {
|
|
4385
5575
|
const context = [
|
|
4386
5576
|
`Stage: ${stageName}`,
|
|
@@ -4463,13 +5653,13 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
|
|
|
4463
5653
|
}
|
|
4464
5654
|
function getModifiedFiles(projectDir) {
|
|
4465
5655
|
try {
|
|
4466
|
-
const staged =
|
|
5656
|
+
const staged = execFileSync16("git", ["diff", "--name-only", "--cached"], {
|
|
4467
5657
|
encoding: "utf-8",
|
|
4468
5658
|
cwd: projectDir,
|
|
4469
5659
|
timeout: 5e3,
|
|
4470
5660
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4471
5661
|
}).trim();
|
|
4472
|
-
const unstaged =
|
|
5662
|
+
const unstaged = execFileSync16("git", ["diff", "--name-only"], {
|
|
4473
5663
|
encoding: "utf-8",
|
|
4474
5664
|
cwd: projectDir,
|
|
4475
5665
|
timeout: 5e3,
|
|
@@ -4512,8 +5702,8 @@ Error context:
|
|
|
4512
5702
|
});
|
|
4513
5703
|
|
|
4514
5704
|
// src/stages/gate.ts
|
|
4515
|
-
import * as
|
|
4516
|
-
import * as
|
|
5705
|
+
import * as fs25 from "fs";
|
|
5706
|
+
import * as path23 from "path";
|
|
4517
5707
|
function executeGateStage(ctx, def) {
|
|
4518
5708
|
if (ctx.input.dryRun) {
|
|
4519
5709
|
logger.info(` [dry-run] skipping ${def.name}`);
|
|
@@ -4556,7 +5746,7 @@ ${output}
|
|
|
4556
5746
|
`);
|
|
4557
5747
|
}
|
|
4558
5748
|
}
|
|
4559
|
-
|
|
5749
|
+
fs25.writeFileSync(path23.join(ctx.taskDir, "verify.md"), lines.join(""));
|
|
4560
5750
|
return {
|
|
4561
5751
|
outcome: verifyResult.pass ? "completed" : "failed",
|
|
4562
5752
|
retries: 0
|
|
@@ -4571,9 +5761,9 @@ var init_gate = __esm({
|
|
|
4571
5761
|
});
|
|
4572
5762
|
|
|
4573
5763
|
// src/stages/verify.ts
|
|
4574
|
-
import * as
|
|
4575
|
-
import * as
|
|
4576
|
-
import { execFileSync as
|
|
5764
|
+
import * as fs26 from "fs";
|
|
5765
|
+
import * as path24 from "path";
|
|
5766
|
+
import { execFileSync as execFileSync17 } from "child_process";
|
|
4577
5767
|
async function executeVerifyWithAutofix(ctx, def) {
|
|
4578
5768
|
const maxAttempts = def.maxRetries ?? 2;
|
|
4579
5769
|
for (let attempt = 0; attempt <= maxAttempts; attempt++) {
|
|
@@ -4583,8 +5773,8 @@ async function executeVerifyWithAutofix(ctx, def) {
|
|
|
4583
5773
|
return { ...gateResult, retries: attempt };
|
|
4584
5774
|
}
|
|
4585
5775
|
if (attempt < maxAttempts) {
|
|
4586
|
-
const verifyPath =
|
|
4587
|
-
const errorOutput =
|
|
5776
|
+
const verifyPath = path24.join(ctx.taskDir, "verify.md");
|
|
5777
|
+
const errorOutput = fs26.existsSync(verifyPath) ? fs26.readFileSync(verifyPath, "utf-8") : "Unknown error";
|
|
4588
5778
|
const modifiedFiles = getModifiedFiles(ctx.projectDir);
|
|
4589
5779
|
const defaultRunner = getRunnerForStage(ctx, "taskify");
|
|
4590
5780
|
const diagConfig = getProjectConfig();
|
|
@@ -4627,7 +5817,7 @@ ${diagnosis.resolution}`);
|
|
|
4627
5817
|
const parts = parseCommand(cmd);
|
|
4628
5818
|
if (parts.length === 0) return;
|
|
4629
5819
|
try {
|
|
4630
|
-
|
|
5820
|
+
execFileSync17(parts[0], parts.slice(1), {
|
|
4631
5821
|
stdio: "pipe",
|
|
4632
5822
|
timeout: FIX_COMMAND_TIMEOUT_MS
|
|
4633
5823
|
});
|
|
@@ -4680,8 +5870,8 @@ var init_verify = __esm({
|
|
|
4680
5870
|
});
|
|
4681
5871
|
|
|
4682
5872
|
// src/review-standalone.ts
|
|
4683
|
-
import * as
|
|
4684
|
-
import * as
|
|
5873
|
+
import * as fs27 from "fs";
|
|
5874
|
+
import * as path25 from "path";
|
|
4685
5875
|
function resolveReviewTarget(input) {
|
|
4686
5876
|
if (input.prs.length === 0) {
|
|
4687
5877
|
return {
|
|
@@ -4705,8 +5895,8 @@ Or comment on the specific PR: \`@kody review\``
|
|
|
4705
5895
|
}
|
|
4706
5896
|
async function runStandaloneReview(input) {
|
|
4707
5897
|
const taskId = input.taskId ?? `review-${generateTaskId()}`;
|
|
4708
|
-
const taskDir =
|
|
4709
|
-
|
|
5898
|
+
const taskDir = path25.join(input.projectDir, ".kody", "tasks", taskId);
|
|
5899
|
+
fs27.mkdirSync(taskDir, { recursive: true });
|
|
4710
5900
|
let diffInstruction = "";
|
|
4711
5901
|
let filesChangedSection = "";
|
|
4712
5902
|
if (input.baseBranch) {
|
|
@@ -4733,7 +5923,7 @@ ${fileList}`;
|
|
|
4733
5923
|
const taskContent = `# ${input.prTitle}
|
|
4734
5924
|
|
|
4735
5925
|
${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
|
|
4736
|
-
|
|
5926
|
+
fs27.writeFileSync(path25.join(taskDir, "task.md"), taskContent);
|
|
4737
5927
|
const reviewDef = STAGES.find((s) => s.name === "review");
|
|
4738
5928
|
const ctx = {
|
|
4739
5929
|
taskId,
|
|
@@ -4755,10 +5945,10 @@ ${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
|
|
|
4755
5945
|
error: result2.error ?? "Review stage failed"
|
|
4756
5946
|
};
|
|
4757
5947
|
}
|
|
4758
|
-
const reviewPath =
|
|
5948
|
+
const reviewPath = path25.join(taskDir, "review.md");
|
|
4759
5949
|
let reviewContent;
|
|
4760
|
-
if (
|
|
4761
|
-
reviewContent =
|
|
5950
|
+
if (fs27.existsSync(reviewPath)) {
|
|
5951
|
+
reviewContent = fs27.readFileSync(reviewPath, "utf-8");
|
|
4762
5952
|
}
|
|
4763
5953
|
return {
|
|
4764
5954
|
outcome: "completed",
|
|
@@ -4798,8 +5988,8 @@ var init_review_standalone = __esm({
|
|
|
4798
5988
|
});
|
|
4799
5989
|
|
|
4800
5990
|
// src/stages/review.ts
|
|
4801
|
-
import * as
|
|
4802
|
-
import * as
|
|
5991
|
+
import * as fs28 from "fs";
|
|
5992
|
+
import * as path26 from "path";
|
|
4803
5993
|
async function executeReviewWithFix(ctx, def) {
|
|
4804
5994
|
if (ctx.input.dryRun) {
|
|
4805
5995
|
return { outcome: "completed", retries: 0 };
|
|
@@ -4813,11 +6003,11 @@ async function executeReviewWithFix(ctx, def) {
|
|
|
4813
6003
|
if (reviewResult.outcome !== "completed") {
|
|
4814
6004
|
return reviewResult;
|
|
4815
6005
|
}
|
|
4816
|
-
const reviewFile =
|
|
4817
|
-
if (!
|
|
6006
|
+
const reviewFile = path26.join(ctx.taskDir, "review.md");
|
|
6007
|
+
if (!fs28.existsSync(reviewFile)) {
|
|
4818
6008
|
return { outcome: "failed", retries: iteration, error: "review.md not found" };
|
|
4819
6009
|
}
|
|
4820
|
-
const content =
|
|
6010
|
+
const content = fs28.readFileSync(reviewFile, "utf-8");
|
|
4821
6011
|
if (detectReviewVerdict(content) !== "fail") {
|
|
4822
6012
|
return { ...reviewResult, retries: iteration };
|
|
4823
6013
|
}
|
|
@@ -4846,15 +6036,15 @@ var init_review = __esm({
|
|
|
4846
6036
|
});
|
|
4847
6037
|
|
|
4848
6038
|
// src/stages/ship.ts
|
|
4849
|
-
import * as
|
|
4850
|
-
import * as
|
|
4851
|
-
import { execFileSync as
|
|
6039
|
+
import * as fs29 from "fs";
|
|
6040
|
+
import * as path27 from "path";
|
|
6041
|
+
import { execFileSync as execFileSync18 } from "child_process";
|
|
4852
6042
|
function buildPrBody(ctx) {
|
|
4853
6043
|
const sections = [];
|
|
4854
|
-
const taskJsonPath =
|
|
4855
|
-
if (
|
|
6044
|
+
const taskJsonPath = path27.join(ctx.taskDir, "task.json");
|
|
6045
|
+
if (fs29.existsSync(taskJsonPath)) {
|
|
4856
6046
|
try {
|
|
4857
|
-
const raw =
|
|
6047
|
+
const raw = fs29.readFileSync(taskJsonPath, "utf-8");
|
|
4858
6048
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
4859
6049
|
const task = JSON.parse(cleaned);
|
|
4860
6050
|
if (task.description) {
|
|
@@ -4873,9 +6063,9 @@ ${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
|
|
|
4873
6063
|
} catch {
|
|
4874
6064
|
}
|
|
4875
6065
|
}
|
|
4876
|
-
const reviewPath =
|
|
4877
|
-
if (
|
|
4878
|
-
const review =
|
|
6066
|
+
const reviewPath = path27.join(ctx.taskDir, "review.md");
|
|
6067
|
+
if (fs29.existsSync(reviewPath)) {
|
|
6068
|
+
const review = fs29.readFileSync(reviewPath, "utf-8");
|
|
4879
6069
|
const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
4880
6070
|
if (summaryMatch) {
|
|
4881
6071
|
const summary = summaryMatch[1].trim();
|
|
@@ -4892,14 +6082,14 @@ ${summary}`);
|
|
|
4892
6082
|
**Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
|
|
4893
6083
|
}
|
|
4894
6084
|
}
|
|
4895
|
-
const verifyPath =
|
|
4896
|
-
if (
|
|
4897
|
-
const verify =
|
|
6085
|
+
const verifyPath = path27.join(ctx.taskDir, "verify.md");
|
|
6086
|
+
if (fs29.existsSync(verifyPath)) {
|
|
6087
|
+
const verify = fs29.readFileSync(verifyPath, "utf-8");
|
|
4898
6088
|
if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
|
|
4899
6089
|
}
|
|
4900
|
-
const planPath =
|
|
4901
|
-
if (
|
|
4902
|
-
const plan =
|
|
6090
|
+
const planPath = path27.join(ctx.taskDir, "plan.md");
|
|
6091
|
+
if (fs29.existsSync(planPath)) {
|
|
6092
|
+
const plan = fs29.readFileSync(planPath, "utf-8").trim();
|
|
4903
6093
|
if (plan) {
|
|
4904
6094
|
const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
|
|
4905
6095
|
sections.push(`
|
|
@@ -4919,28 +6109,28 @@ Closes #${ctx.input.issueNumber}`);
|
|
|
4919
6109
|
return sections.join("\n");
|
|
4920
6110
|
}
|
|
4921
6111
|
function executeShipStage(ctx, _def) {
|
|
4922
|
-
const shipPath =
|
|
6112
|
+
const shipPath = path27.join(ctx.taskDir, "ship.md");
|
|
4923
6113
|
if (ctx.input.dryRun) {
|
|
4924
|
-
|
|
6114
|
+
fs29.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
|
|
4925
6115
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
4926
6116
|
}
|
|
4927
6117
|
if (ctx.input.local && !ctx.input.issueNumber) {
|
|
4928
|
-
|
|
6118
|
+
fs29.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
|
|
4929
6119
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
4930
6120
|
}
|
|
4931
6121
|
try {
|
|
4932
6122
|
const head = getCurrentBranch(ctx.projectDir);
|
|
4933
6123
|
const base = getDefaultBranch(ctx.projectDir);
|
|
4934
6124
|
try {
|
|
4935
|
-
const memoryDir =
|
|
6125
|
+
const memoryDir = path27.join(ctx.projectDir, ".kody", "memory");
|
|
4936
6126
|
const addPaths = [ctx.taskDir];
|
|
4937
|
-
if (
|
|
4938
|
-
|
|
6127
|
+
if (fs29.existsSync(memoryDir)) addPaths.push(memoryDir);
|
|
6128
|
+
execFileSync18("git", ["add", ...addPaths], {
|
|
4939
6129
|
cwd: ctx.projectDir,
|
|
4940
6130
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
4941
6131
|
stdio: "pipe"
|
|
4942
6132
|
});
|
|
4943
|
-
|
|
6133
|
+
execFileSync18("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
|
|
4944
6134
|
cwd: ctx.projectDir,
|
|
4945
6135
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
4946
6136
|
stdio: "pipe"
|
|
@@ -4954,7 +6144,7 @@ function executeShipStage(ctx, _def) {
|
|
|
4954
6144
|
let repo = config.github?.repo;
|
|
4955
6145
|
if (!owner || !repo) {
|
|
4956
6146
|
try {
|
|
4957
|
-
const remoteUrl =
|
|
6147
|
+
const remoteUrl = execFileSync18("git", ["remote", "get-url", "origin"], {
|
|
4958
6148
|
encoding: "utf-8",
|
|
4959
6149
|
cwd: ctx.projectDir
|
|
4960
6150
|
}).trim();
|
|
@@ -4975,28 +6165,28 @@ function executeShipStage(ctx, _def) {
|
|
|
4975
6165
|
chore: "chore"
|
|
4976
6166
|
};
|
|
4977
6167
|
let prefix = "chore";
|
|
4978
|
-
const taskJsonPath =
|
|
4979
|
-
if (
|
|
6168
|
+
const taskJsonPath = path27.join(ctx.taskDir, "task.json");
|
|
6169
|
+
if (fs29.existsSync(taskJsonPath)) {
|
|
4980
6170
|
try {
|
|
4981
|
-
const raw =
|
|
6171
|
+
const raw = fs29.readFileSync(taskJsonPath, "utf-8");
|
|
4982
6172
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
4983
6173
|
const task = JSON.parse(cleaned);
|
|
4984
6174
|
prefix = TYPE_PREFIX[task.task_type] ?? "chore";
|
|
4985
6175
|
} catch {
|
|
4986
6176
|
}
|
|
4987
6177
|
}
|
|
4988
|
-
const taskMdPath =
|
|
4989
|
-
if (
|
|
4990
|
-
const content =
|
|
6178
|
+
const taskMdPath = path27.join(ctx.taskDir, "task.md");
|
|
6179
|
+
if (fs29.existsSync(taskMdPath)) {
|
|
6180
|
+
const content = fs29.readFileSync(taskMdPath, "utf-8");
|
|
4991
6181
|
const heading = content.split("\n").find((l) => l.startsWith("# "));
|
|
4992
6182
|
if (heading) {
|
|
4993
6183
|
title = `${prefix}: ${heading.replace(/^#\s*/, "").trim()}`.slice(0, 72);
|
|
4994
6184
|
}
|
|
4995
6185
|
}
|
|
4996
6186
|
if (title === "Update") {
|
|
4997
|
-
if (
|
|
6187
|
+
if (fs29.existsSync(taskJsonPath)) {
|
|
4998
6188
|
try {
|
|
4999
|
-
const raw =
|
|
6189
|
+
const raw = fs29.readFileSync(taskJsonPath, "utf-8");
|
|
5000
6190
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
5001
6191
|
const task = JSON.parse(cleaned);
|
|
5002
6192
|
if (task.title) title = `${prefix}: ${task.title}`.slice(0, 72);
|
|
@@ -5019,7 +6209,7 @@ function executeShipStage(ctx, _def) {
|
|
|
5019
6209
|
} catch {
|
|
5020
6210
|
}
|
|
5021
6211
|
}
|
|
5022
|
-
|
|
6212
|
+
fs29.writeFileSync(shipPath, `# Ship
|
|
5023
6213
|
|
|
5024
6214
|
Updated existing PR: ${existingPr.url}
|
|
5025
6215
|
PR #${existingPr.number}
|
|
@@ -5040,20 +6230,20 @@ PR #${existingPr.number}
|
|
|
5040
6230
|
} catch {
|
|
5041
6231
|
}
|
|
5042
6232
|
}
|
|
5043
|
-
|
|
6233
|
+
fs29.writeFileSync(shipPath, `# Ship
|
|
5044
6234
|
|
|
5045
6235
|
PR created: ${pr.url}
|
|
5046
6236
|
PR #${pr.number}
|
|
5047
6237
|
`);
|
|
5048
6238
|
} else {
|
|
5049
|
-
|
|
6239
|
+
fs29.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
|
|
5050
6240
|
}
|
|
5051
6241
|
}
|
|
5052
6242
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
5053
6243
|
} catch (err) {
|
|
5054
6244
|
const msg = err instanceof Error ? err.message : String(err);
|
|
5055
6245
|
try {
|
|
5056
|
-
|
|
6246
|
+
fs29.writeFileSync(shipPath, `# Ship
|
|
5057
6247
|
|
|
5058
6248
|
Failed: ${msg}
|
|
5059
6249
|
`);
|
|
@@ -5102,15 +6292,15 @@ var init_executor_registry = __esm({
|
|
|
5102
6292
|
});
|
|
5103
6293
|
|
|
5104
6294
|
// src/pipeline/questions.ts
|
|
5105
|
-
import * as
|
|
5106
|
-
import * as
|
|
6295
|
+
import * as fs30 from "fs";
|
|
6296
|
+
import * as path28 from "path";
|
|
5107
6297
|
function checkForQuestions(ctx, stageName) {
|
|
5108
6298
|
if (ctx.input.local || !ctx.input.issueNumber) return false;
|
|
5109
6299
|
try {
|
|
5110
6300
|
if (stageName === "taskify") {
|
|
5111
|
-
const taskJsonPath =
|
|
5112
|
-
if (!
|
|
5113
|
-
const raw =
|
|
6301
|
+
const taskJsonPath = path28.join(ctx.taskDir, "task.json");
|
|
6302
|
+
if (!fs30.existsSync(taskJsonPath)) return false;
|
|
6303
|
+
const raw = fs30.readFileSync(taskJsonPath, "utf-8");
|
|
5114
6304
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
5115
6305
|
const taskJson = JSON.parse(cleaned);
|
|
5116
6306
|
if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
|
|
@@ -5125,9 +6315,9 @@ Reply with \`@kody approve\` and your answers in the comment body.`;
|
|
|
5125
6315
|
}
|
|
5126
6316
|
}
|
|
5127
6317
|
if (stageName === "plan") {
|
|
5128
|
-
const planPath =
|
|
5129
|
-
if (!
|
|
5130
|
-
const plan =
|
|
6318
|
+
const planPath = path28.join(ctx.taskDir, "plan.md");
|
|
6319
|
+
if (!fs30.existsSync(planPath)) return false;
|
|
6320
|
+
const plan = fs30.readFileSync(planPath, "utf-8");
|
|
5131
6321
|
const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
5132
6322
|
if (questionsMatch) {
|
|
5133
6323
|
const questionsText = questionsMatch[1].trim();
|
|
@@ -5156,8 +6346,8 @@ var init_questions = __esm({
|
|
|
5156
6346
|
});
|
|
5157
6347
|
|
|
5158
6348
|
// src/pipeline/hooks.ts
|
|
5159
|
-
import * as
|
|
5160
|
-
import * as
|
|
6349
|
+
import * as fs31 from "fs";
|
|
6350
|
+
import * as path29 from "path";
|
|
5161
6351
|
function applyPreStageLabel(ctx, def) {
|
|
5162
6352
|
if (!ctx.input.issueNumber || ctx.input.local) return;
|
|
5163
6353
|
if (def.name === "plan") setLifecycleLabel(ctx.input.issueNumber, "planning");
|
|
@@ -5198,9 +6388,9 @@ function autoDetectComplexity(ctx, def) {
|
|
|
5198
6388
|
return { complexity, activeStages };
|
|
5199
6389
|
}
|
|
5200
6390
|
try {
|
|
5201
|
-
const taskJsonPath =
|
|
5202
|
-
if (!
|
|
5203
|
-
const raw =
|
|
6391
|
+
const taskJsonPath = path29.join(ctx.taskDir, "task.json");
|
|
6392
|
+
if (!fs31.existsSync(taskJsonPath)) return null;
|
|
6393
|
+
const raw = fs31.readFileSync(taskJsonPath, "utf-8");
|
|
5204
6394
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
5205
6395
|
const taskJson = JSON.parse(cleaned);
|
|
5206
6396
|
if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
|
|
@@ -5230,8 +6420,8 @@ function checkRiskGate(ctx, def, state, complexity) {
|
|
|
5230
6420
|
if (ctx.input.dryRun || ctx.input.local) return null;
|
|
5231
6421
|
if (ctx.input.mode === "rerun") return null;
|
|
5232
6422
|
if (!ctx.input.issueNumber) return null;
|
|
5233
|
-
const planPath =
|
|
5234
|
-
const plan =
|
|
6423
|
+
const planPath = path29.join(ctx.taskDir, "plan.md");
|
|
6424
|
+
const plan = fs31.existsSync(planPath) ? fs31.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
|
|
5235
6425
|
try {
|
|
5236
6426
|
postComment(
|
|
5237
6427
|
ctx.input.issueNumber,
|
|
@@ -5292,28 +6482,28 @@ var init_hooks = __esm({
|
|
|
5292
6482
|
init_git_utils();
|
|
5293
6483
|
init_questions();
|
|
5294
6484
|
init_complexity();
|
|
5295
|
-
|
|
6485
|
+
init_state2();
|
|
5296
6486
|
init_logger();
|
|
5297
6487
|
}
|
|
5298
6488
|
});
|
|
5299
6489
|
|
|
5300
6490
|
// src/learning/auto-learn.ts
|
|
5301
|
-
import * as
|
|
5302
|
-
import * as
|
|
6491
|
+
import * as fs32 from "fs";
|
|
6492
|
+
import * as path30 from "path";
|
|
5303
6493
|
function stripAnsi(str) {
|
|
5304
6494
|
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
5305
6495
|
}
|
|
5306
6496
|
function autoLearn(ctx) {
|
|
5307
6497
|
try {
|
|
5308
|
-
const memoryDir =
|
|
5309
|
-
if (!
|
|
5310
|
-
|
|
6498
|
+
const memoryDir = path30.join(ctx.projectDir, ".kody", "memory");
|
|
6499
|
+
if (!fs32.existsSync(memoryDir)) {
|
|
6500
|
+
fs32.mkdirSync(memoryDir, { recursive: true });
|
|
5311
6501
|
}
|
|
5312
6502
|
const learnings = [];
|
|
5313
6503
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
5314
|
-
const verifyPath =
|
|
5315
|
-
if (
|
|
5316
|
-
const verify = stripAnsi(
|
|
6504
|
+
const verifyPath = path30.join(ctx.taskDir, "verify.md");
|
|
6505
|
+
if (fs32.existsSync(verifyPath)) {
|
|
6506
|
+
const verify = stripAnsi(fs32.readFileSync(verifyPath, "utf-8"));
|
|
5317
6507
|
if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
|
|
5318
6508
|
if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
|
|
5319
6509
|
if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
|
|
@@ -5322,18 +6512,18 @@ function autoLearn(ctx) {
|
|
|
5322
6512
|
if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
|
|
5323
6513
|
if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
|
|
5324
6514
|
}
|
|
5325
|
-
const reviewPath =
|
|
5326
|
-
if (
|
|
5327
|
-
const review =
|
|
6515
|
+
const reviewPath = path30.join(ctx.taskDir, "review.md");
|
|
6516
|
+
if (fs32.existsSync(reviewPath)) {
|
|
6517
|
+
const review = fs32.readFileSync(reviewPath, "utf-8");
|
|
5328
6518
|
if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
|
|
5329
6519
|
if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
|
|
5330
6520
|
if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
|
|
5331
6521
|
if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
|
|
5332
6522
|
}
|
|
5333
|
-
const taskJsonPath =
|
|
5334
|
-
if (
|
|
6523
|
+
const taskJsonPath = path30.join(ctx.taskDir, "task.json");
|
|
6524
|
+
if (fs32.existsSync(taskJsonPath)) {
|
|
5335
6525
|
try {
|
|
5336
|
-
const raw = stripAnsi(
|
|
6526
|
+
const raw = stripAnsi(fs32.readFileSync(taskJsonPath, "utf-8"));
|
|
5337
6527
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
5338
6528
|
const task = JSON.parse(cleaned);
|
|
5339
6529
|
if (task.scope && Array.isArray(task.scope)) {
|
|
@@ -5344,12 +6534,12 @@ function autoLearn(ctx) {
|
|
|
5344
6534
|
}
|
|
5345
6535
|
}
|
|
5346
6536
|
if (learnings.length > 0) {
|
|
5347
|
-
const conventionsPath =
|
|
6537
|
+
const conventionsPath = path30.join(memoryDir, "conventions.md");
|
|
5348
6538
|
const entry = `
|
|
5349
6539
|
## Learned ${timestamp2} (task: ${ctx.taskId})
|
|
5350
6540
|
${learnings.join("\n")}
|
|
5351
6541
|
`;
|
|
5352
|
-
|
|
6542
|
+
fs32.appendFileSync(conventionsPath, entry);
|
|
5353
6543
|
logger.info(`Auto-learned ${learnings.length} convention(s)`);
|
|
5354
6544
|
}
|
|
5355
6545
|
autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
|
|
@@ -5357,8 +6547,8 @@ ${learnings.join("\n")}
|
|
|
5357
6547
|
}
|
|
5358
6548
|
}
|
|
5359
6549
|
function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
5360
|
-
const archPath =
|
|
5361
|
-
if (
|
|
6550
|
+
const archPath = path30.join(memoryDir, "architecture.md");
|
|
6551
|
+
if (fs32.existsSync(archPath)) return;
|
|
5362
6552
|
const detected = detectArchitectureBasic(projectDir);
|
|
5363
6553
|
if (detected.length > 0) {
|
|
5364
6554
|
const content = `# Architecture (auto-detected ${timestamp2})
|
|
@@ -5366,7 +6556,7 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
|
5366
6556
|
## Overview
|
|
5367
6557
|
${detected.join("\n")}
|
|
5368
6558
|
`;
|
|
5369
|
-
|
|
6559
|
+
fs32.writeFileSync(archPath, content);
|
|
5370
6560
|
logger.info(`Auto-detected architecture (${detected.length} items)`);
|
|
5371
6561
|
}
|
|
5372
6562
|
}
|
|
@@ -5379,13 +6569,13 @@ var init_auto_learn = __esm({
|
|
|
5379
6569
|
});
|
|
5380
6570
|
|
|
5381
6571
|
// src/retrospective.ts
|
|
5382
|
-
import * as
|
|
5383
|
-
import * as
|
|
6572
|
+
import * as fs33 from "fs";
|
|
6573
|
+
import * as path31 from "path";
|
|
5384
6574
|
function readArtifact(taskDir, filename, maxChars) {
|
|
5385
|
-
const p =
|
|
5386
|
-
if (!
|
|
6575
|
+
const p = path31.join(taskDir, filename);
|
|
6576
|
+
if (!fs33.existsSync(p)) return null;
|
|
5387
6577
|
try {
|
|
5388
|
-
const content =
|
|
6578
|
+
const content = fs33.readFileSync(p, "utf-8");
|
|
5389
6579
|
return content.length > maxChars ? content.slice(0, maxChars) + "\n...(truncated)" : content;
|
|
5390
6580
|
} catch {
|
|
5391
6581
|
return null;
|
|
@@ -5438,13 +6628,13 @@ function collectRunContext(ctx, state, pipelineStartTime) {
|
|
|
5438
6628
|
return lines.join("\n");
|
|
5439
6629
|
}
|
|
5440
6630
|
function getLogPath(projectDir) {
|
|
5441
|
-
return
|
|
6631
|
+
return path31.join(projectDir, ".kody", "memory", "observer-log.jsonl");
|
|
5442
6632
|
}
|
|
5443
6633
|
function readPreviousRetrospectives(projectDir, limit = 10) {
|
|
5444
6634
|
const logPath = getLogPath(projectDir);
|
|
5445
|
-
if (!
|
|
6635
|
+
if (!fs33.existsSync(logPath)) return [];
|
|
5446
6636
|
try {
|
|
5447
|
-
const content =
|
|
6637
|
+
const content = fs33.readFileSync(logPath, "utf-8");
|
|
5448
6638
|
const lines = content.split("\n").filter(Boolean);
|
|
5449
6639
|
const entries = [];
|
|
5450
6640
|
const start = Math.max(0, lines.length - limit);
|
|
@@ -5471,11 +6661,11 @@ function formatPreviousEntries(entries) {
|
|
|
5471
6661
|
}
|
|
5472
6662
|
function appendRetrospectiveEntry(projectDir, entry) {
|
|
5473
6663
|
const logPath = getLogPath(projectDir);
|
|
5474
|
-
const dir =
|
|
5475
|
-
if (!
|
|
5476
|
-
|
|
6664
|
+
const dir = path31.dirname(logPath);
|
|
6665
|
+
if (!fs33.existsSync(dir)) {
|
|
6666
|
+
fs33.mkdirSync(dir, { recursive: true });
|
|
5477
6667
|
}
|
|
5478
|
-
|
|
6668
|
+
fs33.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
5479
6669
|
}
|
|
5480
6670
|
async function runRetrospective(ctx, state, pipelineStartTime) {
|
|
5481
6671
|
if (ctx.input.dryRun) return;
|
|
@@ -5643,15 +6833,15 @@ var init_summary = __esm({
|
|
|
5643
6833
|
});
|
|
5644
6834
|
|
|
5645
6835
|
// src/tools.ts
|
|
5646
|
-
import * as
|
|
5647
|
-
import * as
|
|
6836
|
+
import * as fs34 from "fs";
|
|
6837
|
+
import * as path32 from "path";
|
|
5648
6838
|
import { execSync as execSync3 } from "child_process";
|
|
5649
6839
|
import { parse as parseYaml } from "yaml";
|
|
5650
6840
|
function loadToolDeclarations(projectDir) {
|
|
5651
|
-
const toolsPath =
|
|
5652
|
-
if (!
|
|
6841
|
+
const toolsPath = path32.join(projectDir, ".kody", "tools.yml");
|
|
6842
|
+
if (!fs34.existsSync(toolsPath)) return [];
|
|
5653
6843
|
try {
|
|
5654
|
-
const raw =
|
|
6844
|
+
const raw = fs34.readFileSync(toolsPath, "utf-8");
|
|
5655
6845
|
const parsed = parseYaml(raw);
|
|
5656
6846
|
if (!parsed || typeof parsed !== "object") return [];
|
|
5657
6847
|
return Object.entries(parsed).map(([name, value]) => {
|
|
@@ -5672,7 +6862,7 @@ function loadToolDeclarations(projectDir) {
|
|
|
5672
6862
|
function detectTools(declarations, projectDir) {
|
|
5673
6863
|
const resolved = [];
|
|
5674
6864
|
for (const decl of declarations) {
|
|
5675
|
-
const detected = decl.detect.some((pattern) =>
|
|
6865
|
+
const detected = decl.detect.some((pattern) => fs34.existsSync(path32.join(projectDir, pattern)));
|
|
5676
6866
|
if (!detected) continue;
|
|
5677
6867
|
resolved.push({
|
|
5678
6868
|
name: decl.name,
|
|
@@ -5713,8 +6903,8 @@ var init_tools = __esm({
|
|
|
5713
6903
|
});
|
|
5714
6904
|
|
|
5715
6905
|
// src/pipeline.ts
|
|
5716
|
-
import * as
|
|
5717
|
-
import * as
|
|
6906
|
+
import * as fs35 from "fs";
|
|
6907
|
+
import * as path33 from "path";
|
|
5718
6908
|
function ensureFeatureBranchIfNeeded(ctx) {
|
|
5719
6909
|
if (ctx.input.dryRun) return;
|
|
5720
6910
|
if (ctx.input.prNumber) {
|
|
@@ -5727,8 +6917,8 @@ function ensureFeatureBranchIfNeeded(ctx) {
|
|
|
5727
6917
|
}
|
|
5728
6918
|
if (!ctx.input.issueNumber) return;
|
|
5729
6919
|
try {
|
|
5730
|
-
const taskMdPath =
|
|
5731
|
-
const title =
|
|
6920
|
+
const taskMdPath = path33.join(ctx.taskDir, "task.md");
|
|
6921
|
+
const title = fs35.existsSync(taskMdPath) ? fs35.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
|
|
5732
6922
|
ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
|
|
5733
6923
|
syncWithDefault(ctx.projectDir);
|
|
5734
6924
|
} catch (err) {
|
|
@@ -5742,10 +6932,10 @@ function ensureFeatureBranchIfNeeded(ctx) {
|
|
|
5742
6932
|
}
|
|
5743
6933
|
}
|
|
5744
6934
|
function acquireLock(taskDir) {
|
|
5745
|
-
const lockPath =
|
|
5746
|
-
if (
|
|
6935
|
+
const lockPath = path33.join(taskDir, ".lock");
|
|
6936
|
+
if (fs35.existsSync(lockPath)) {
|
|
5747
6937
|
try {
|
|
5748
|
-
const pid = parseInt(
|
|
6938
|
+
const pid = parseInt(fs35.readFileSync(lockPath, "utf-8").trim(), 10);
|
|
5749
6939
|
if (!isNaN(pid)) {
|
|
5750
6940
|
try {
|
|
5751
6941
|
process.kill(pid, 0);
|
|
@@ -5762,14 +6952,14 @@ function acquireLock(taskDir) {
|
|
|
5762
6952
|
logger.warn(` Corrupt lock file \u2014 overwriting`);
|
|
5763
6953
|
}
|
|
5764
6954
|
try {
|
|
5765
|
-
|
|
6955
|
+
fs35.unlinkSync(lockPath);
|
|
5766
6956
|
} catch {
|
|
5767
6957
|
}
|
|
5768
6958
|
}
|
|
5769
6959
|
try {
|
|
5770
|
-
const fd =
|
|
5771
|
-
|
|
5772
|
-
|
|
6960
|
+
const fd = fs35.openSync(lockPath, fs35.constants.O_WRONLY | fs35.constants.O_CREAT | fs35.constants.O_EXCL);
|
|
6961
|
+
fs35.writeSync(fd, String(process.pid));
|
|
6962
|
+
fs35.closeSync(fd);
|
|
5773
6963
|
} catch (err) {
|
|
5774
6964
|
if (err.code === "EEXIST") {
|
|
5775
6965
|
throw new Error("Pipeline already running (lock acquired by another process)");
|
|
@@ -5779,7 +6969,7 @@ function acquireLock(taskDir) {
|
|
|
5779
6969
|
}
|
|
5780
6970
|
function releaseLock(taskDir) {
|
|
5781
6971
|
try {
|
|
5782
|
-
|
|
6972
|
+
fs35.unlinkSync(path33.join(taskDir, ".lock"));
|
|
5783
6973
|
} catch {
|
|
5784
6974
|
}
|
|
5785
6975
|
}
|
|
@@ -5982,7 +7172,7 @@ var init_pipeline = __esm({
|
|
|
5982
7172
|
init_git_utils();
|
|
5983
7173
|
init_github_api();
|
|
5984
7174
|
init_logger();
|
|
5985
|
-
|
|
7175
|
+
init_state2();
|
|
5986
7176
|
init_complexity();
|
|
5987
7177
|
init_executor_registry();
|
|
5988
7178
|
init_hooks();
|
|
@@ -5995,8 +7185,8 @@ var init_pipeline = __esm({
|
|
|
5995
7185
|
});
|
|
5996
7186
|
|
|
5997
7187
|
// src/preflight.ts
|
|
5998
|
-
import { execFileSync as
|
|
5999
|
-
import * as
|
|
7188
|
+
import { execFileSync as execFileSync19 } from "child_process";
|
|
7189
|
+
import * as fs36 from "fs";
|
|
6000
7190
|
function check(name, fn) {
|
|
6001
7191
|
try {
|
|
6002
7192
|
const detail = fn() ?? void 0;
|
|
@@ -6008,7 +7198,7 @@ function check(name, fn) {
|
|
|
6008
7198
|
function runPreflight() {
|
|
6009
7199
|
const checks = [
|
|
6010
7200
|
check("claude CLI", () => {
|
|
6011
|
-
const v =
|
|
7201
|
+
const v = execFileSync19("claude", ["--version"], {
|
|
6012
7202
|
encoding: "utf-8",
|
|
6013
7203
|
timeout: 1e4,
|
|
6014
7204
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6016,14 +7206,14 @@ function runPreflight() {
|
|
|
6016
7206
|
return v;
|
|
6017
7207
|
}),
|
|
6018
7208
|
check("git repo", () => {
|
|
6019
|
-
|
|
7209
|
+
execFileSync19("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
6020
7210
|
encoding: "utf-8",
|
|
6021
7211
|
timeout: 5e3,
|
|
6022
7212
|
stdio: ["pipe", "pipe", "pipe"]
|
|
6023
7213
|
});
|
|
6024
7214
|
}),
|
|
6025
7215
|
check("pnpm", () => {
|
|
6026
|
-
const v =
|
|
7216
|
+
const v = execFileSync19("pnpm", ["--version"], {
|
|
6027
7217
|
encoding: "utf-8",
|
|
6028
7218
|
timeout: 5e3,
|
|
6029
7219
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6031,7 +7221,7 @@ function runPreflight() {
|
|
|
6031
7221
|
return v;
|
|
6032
7222
|
}),
|
|
6033
7223
|
check("node >= 18", () => {
|
|
6034
|
-
const v =
|
|
7224
|
+
const v = execFileSync19("node", ["--version"], {
|
|
6035
7225
|
encoding: "utf-8",
|
|
6036
7226
|
timeout: 5e3,
|
|
6037
7227
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6041,7 +7231,7 @@ function runPreflight() {
|
|
|
6041
7231
|
return v;
|
|
6042
7232
|
}),
|
|
6043
7233
|
check("gh CLI", () => {
|
|
6044
|
-
const v =
|
|
7234
|
+
const v = execFileSync19("gh", ["--version"], {
|
|
6045
7235
|
encoding: "utf-8",
|
|
6046
7236
|
timeout: 5e3,
|
|
6047
7237
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6049,7 +7239,7 @@ function runPreflight() {
|
|
|
6049
7239
|
return v;
|
|
6050
7240
|
}),
|
|
6051
7241
|
check("package.json", () => {
|
|
6052
|
-
if (!
|
|
7242
|
+
if (!fs36.existsSync("package.json")) throw new Error("not found");
|
|
6053
7243
|
})
|
|
6054
7244
|
];
|
|
6055
7245
|
const failed = checks.filter((c) => !c.ok);
|
|
@@ -6126,8 +7316,8 @@ var init_args = __esm({
|
|
|
6126
7316
|
});
|
|
6127
7317
|
|
|
6128
7318
|
// src/cli/task-state.ts
|
|
6129
|
-
import * as
|
|
6130
|
-
import * as
|
|
7319
|
+
import * as fs37 from "fs";
|
|
7320
|
+
import * as path34 from "path";
|
|
6131
7321
|
function resolveTaskAction(issueNumber, existingTaskId, existingState) {
|
|
6132
7322
|
if (!existingTaskId || !existingState) {
|
|
6133
7323
|
return { action: "start-fresh", taskId: `${issueNumber}-${generateTaskId()}` };
|
|
@@ -6159,11 +7349,11 @@ function resolveTaskAction(issueNumber, existingTaskId, existingState) {
|
|
|
6159
7349
|
function resolveForIssue(issueNumber, projectDir) {
|
|
6160
7350
|
const existingTaskId = findLatestTaskForIssue(issueNumber, projectDir);
|
|
6161
7351
|
if (existingTaskId) {
|
|
6162
|
-
const statusPath =
|
|
7352
|
+
const statusPath = path34.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
|
|
6163
7353
|
let existingState = null;
|
|
6164
|
-
if (
|
|
7354
|
+
if (fs37.existsSync(statusPath)) {
|
|
6165
7355
|
try {
|
|
6166
|
-
existingState = JSON.parse(
|
|
7356
|
+
existingState = JSON.parse(fs37.readFileSync(statusPath, "utf-8"));
|
|
6167
7357
|
} catch {
|
|
6168
7358
|
}
|
|
6169
7359
|
}
|
|
@@ -6196,12 +7386,12 @@ var resolve_exports = {};
|
|
|
6196
7386
|
__export(resolve_exports, {
|
|
6197
7387
|
runResolve: () => runResolve
|
|
6198
7388
|
});
|
|
6199
|
-
import { execFileSync as
|
|
7389
|
+
import { execFileSync as execFileSync20 } from "child_process";
|
|
6200
7390
|
function getConflictContext(cwd, files) {
|
|
6201
7391
|
const parts = [];
|
|
6202
7392
|
for (const file of files.slice(0, 10)) {
|
|
6203
7393
|
try {
|
|
6204
|
-
const content =
|
|
7394
|
+
const content = execFileSync20("git", ["diff", file], {
|
|
6205
7395
|
cwd,
|
|
6206
7396
|
encoding: "utf-8",
|
|
6207
7397
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6359,8 +7549,8 @@ var init_resolve = __esm({
|
|
|
6359
7549
|
|
|
6360
7550
|
// src/entry.ts
|
|
6361
7551
|
var entry_exports = {};
|
|
6362
|
-
import * as
|
|
6363
|
-
import * as
|
|
7552
|
+
import * as fs38 from "fs";
|
|
7553
|
+
import * as path35 from "path";
|
|
6364
7554
|
async function ensureLitellmProxy(config, projectDir) {
|
|
6365
7555
|
if (!anyStageNeedsProxy(config)) return null;
|
|
6366
7556
|
const litellmUrl = getLitellmUrl();
|
|
@@ -6415,9 +7605,9 @@ async function runModelHealthCheck(config) {
|
|
|
6415
7605
|
}
|
|
6416
7606
|
async function main() {
|
|
6417
7607
|
const input = parseArgs();
|
|
6418
|
-
const projectDir = input.cwd ?
|
|
7608
|
+
const projectDir = input.cwd ? path35.resolve(input.cwd) : process.cwd();
|
|
6419
7609
|
if (input.cwd) {
|
|
6420
|
-
if (!
|
|
7610
|
+
if (!fs38.existsSync(projectDir)) {
|
|
6421
7611
|
console.error(`--cwd path does not exist: ${projectDir}`);
|
|
6422
7612
|
process.exit(1);
|
|
6423
7613
|
}
|
|
@@ -6477,8 +7667,8 @@ async function main() {
|
|
|
6477
7667
|
process.exit(1);
|
|
6478
7668
|
}
|
|
6479
7669
|
}
|
|
6480
|
-
const taskDir =
|
|
6481
|
-
|
|
7670
|
+
const taskDir = path35.join(projectDir, ".kody", "tasks", taskId);
|
|
7671
|
+
fs38.mkdirSync(taskDir, { recursive: true });
|
|
6482
7672
|
if (input.command === "rerun" && isTaskifyRun(taskDir)) {
|
|
6483
7673
|
const marker = readTaskifyMarker(taskDir);
|
|
6484
7674
|
if (marker) {
|
|
@@ -6610,31 +7800,31 @@ async function main() {
|
|
|
6610
7800
|
logger.info("Preflight checks:");
|
|
6611
7801
|
runPreflight();
|
|
6612
7802
|
if (input.task) {
|
|
6613
|
-
|
|
7803
|
+
fs38.writeFileSync(path35.join(taskDir, "task.md"), input.task);
|
|
6614
7804
|
}
|
|
6615
|
-
const taskMdPath =
|
|
6616
|
-
if (!
|
|
7805
|
+
const taskMdPath = path35.join(taskDir, "task.md");
|
|
7806
|
+
if (!fs38.existsSync(taskMdPath) && isPRFix && input.prNumber) {
|
|
6617
7807
|
logger.info(`Fetching PR #${input.prNumber} details as task context...`);
|
|
6618
7808
|
const prDetails = getPRDetails(input.prNumber);
|
|
6619
7809
|
if (prDetails) {
|
|
6620
7810
|
const taskContent = `# ${prDetails.title}
|
|
6621
7811
|
|
|
6622
7812
|
${prDetails.body ?? ""}`;
|
|
6623
|
-
|
|
7813
|
+
fs38.writeFileSync(taskMdPath, taskContent);
|
|
6624
7814
|
logger.info(` Task loaded from PR #${input.prNumber}: ${prDetails.title}`);
|
|
6625
7815
|
}
|
|
6626
|
-
} else if (!
|
|
7816
|
+
} else if (!fs38.existsSync(taskMdPath) && input.issueNumber) {
|
|
6627
7817
|
logger.info(`Fetching issue #${input.issueNumber} body as task...`);
|
|
6628
7818
|
const issue = getIssue(input.issueNumber);
|
|
6629
7819
|
if (issue) {
|
|
6630
7820
|
const taskContent = `# ${issue.title}
|
|
6631
7821
|
|
|
6632
7822
|
${issue.body ?? ""}`;
|
|
6633
|
-
|
|
7823
|
+
fs38.writeFileSync(taskMdPath, taskContent);
|
|
6634
7824
|
logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
|
|
6635
7825
|
}
|
|
6636
7826
|
}
|
|
6637
|
-
if (!
|
|
7827
|
+
if (!fs38.existsSync(taskMdPath)) {
|
|
6638
7828
|
console.error("No task.md found. Provide --task, --issue-number, or ensure .kody/tasks/<id>/task.md exists.");
|
|
6639
7829
|
process.exit(1);
|
|
6640
7830
|
}
|
|
@@ -6778,7 +7968,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
|
|
|
6778
7968
|
}
|
|
6779
7969
|
}
|
|
6780
7970
|
const state = await runPipeline(ctx);
|
|
6781
|
-
const files =
|
|
7971
|
+
const files = fs38.readdirSync(taskDir);
|
|
6782
7972
|
console.log(`
|
|
6783
7973
|
Artifacts in ${taskDir}:`);
|
|
6784
7974
|
for (const f of files) {
|
|
@@ -6844,8 +8034,8 @@ var init_entry = __esm({
|
|
|
6844
8034
|
});
|
|
6845
8035
|
|
|
6846
8036
|
// src/bin/cli.ts
|
|
6847
|
-
import * as
|
|
6848
|
-
import * as
|
|
8037
|
+
import * as fs39 from "fs";
|
|
8038
|
+
import * as path36 from "path";
|
|
6849
8039
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6850
8040
|
|
|
6851
8041
|
// src/bin/commands/init.ts
|
|
@@ -7147,9 +8337,37 @@ function initCommand(opts, pkgRoot) {
|
|
|
7147
8337
|
console.log(" \u2717 kody.config.json \u2014 invalid JSON");
|
|
7148
8338
|
}
|
|
7149
8339
|
}
|
|
8340
|
+
console.log("\n\u2500\u2500 Kody Watch \u2500\u2500");
|
|
8341
|
+
const watchWorkflowSrc = path2.join(templatesDir, "kody-watch.yml");
|
|
8342
|
+
const watchWorkflowDest = path2.join(cwd, ".github", "workflows", "kody-watch.yml");
|
|
8343
|
+
if (fs3.existsSync(watchWorkflowSrc)) {
|
|
8344
|
+
if (fs3.existsSync(watchWorkflowDest) && !opts.force) {
|
|
8345
|
+
console.log(" \u25CB .github/workflows/kody-watch.yml (exists)");
|
|
8346
|
+
} else {
|
|
8347
|
+
fs3.mkdirSync(path2.dirname(watchWorkflowDest), { recursive: true });
|
|
8348
|
+
fs3.copyFileSync(watchWorkflowSrc, watchWorkflowDest);
|
|
8349
|
+
console.log(" \u2713 .github/workflows/kody-watch.yml");
|
|
8350
|
+
if (fs3.existsSync(configDest)) {
|
|
8351
|
+
try {
|
|
8352
|
+
const config = JSON.parse(fs3.readFileSync(configDest, "utf-8"));
|
|
8353
|
+
if (!config.watch) {
|
|
8354
|
+
config.watch = { enabled: true };
|
|
8355
|
+
fs3.writeFileSync(configDest, JSON.stringify(config, null, 2) + "\n");
|
|
8356
|
+
console.log(" \u2713 Added watch config to kody.config.json");
|
|
8357
|
+
}
|
|
8358
|
+
} catch {
|
|
8359
|
+
}
|
|
8360
|
+
}
|
|
8361
|
+
console.log(" \u2139 Kody Watch will monitor pipeline health every 30 minutes");
|
|
8362
|
+
console.log(" \u2139 Digest issue will be created during bootstrap");
|
|
8363
|
+
}
|
|
8364
|
+
} else {
|
|
8365
|
+
console.log(" \u25CB kody-watch.yml template not found \u2014 skipping");
|
|
8366
|
+
}
|
|
7150
8367
|
console.log("\n\u2500\u2500 Git \u2500\u2500");
|
|
7151
8368
|
const filesToCommit = [
|
|
7152
8369
|
".github/workflows/kody.yml",
|
|
8370
|
+
".github/workflows/kody-watch.yml",
|
|
7153
8371
|
"kody.config.json"
|
|
7154
8372
|
].filter((f) => fs3.existsSync(path2.join(cwd, f)));
|
|
7155
8373
|
if (filesToCommit.length > 0) {
|
|
@@ -8277,6 +9495,87 @@ Command and URL.
|
|
|
8277
9495
|
} else {
|
|
8278
9496
|
console.log(" \u25CB No routes or collections detected \u2014 skipping QA guide");
|
|
8279
9497
|
}
|
|
9498
|
+
console.log("\n\u2500\u2500 Kody Watch \u2500\u2500");
|
|
9499
|
+
const kodyConfigPath = path7.join(cwd, "kody.config.json");
|
|
9500
|
+
if (fs8.existsSync(kodyConfigPath)) {
|
|
9501
|
+
try {
|
|
9502
|
+
const config = JSON.parse(fs8.readFileSync(kodyConfigPath, "utf-8"));
|
|
9503
|
+
if (config.watch?.enabled && !config.watch?.digestIssue) {
|
|
9504
|
+
let watchRepoSlug = "";
|
|
9505
|
+
if (config.github?.owner && config.github?.repo) {
|
|
9506
|
+
watchRepoSlug = `${config.github.owner}/${config.github.repo}`;
|
|
9507
|
+
}
|
|
9508
|
+
if (watchRepoSlug) {
|
|
9509
|
+
console.log(" \u23F3 Creating Watch digest issue...");
|
|
9510
|
+
try {
|
|
9511
|
+
const issueUrl = execFileSync4("gh", [
|
|
9512
|
+
"issue",
|
|
9513
|
+
"create",
|
|
9514
|
+
"--repo",
|
|
9515
|
+
watchRepoSlug,
|
|
9516
|
+
"--title",
|
|
9517
|
+
"[Kody Watch] Health Digest",
|
|
9518
|
+
"--body",
|
|
9519
|
+
"This issue receives periodic health reports from Kody Watch.\n\n**Plugins:** pipeline-health, security-scan, config-health\n\n_Do not close this issue \u2014 Kody Watch posts digest comments here._"
|
|
9520
|
+
], {
|
|
9521
|
+
cwd,
|
|
9522
|
+
encoding: "utf-8",
|
|
9523
|
+
timeout: 15e3,
|
|
9524
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
9525
|
+
}).trim();
|
|
9526
|
+
const digestMatch = issueUrl.match(/\/issues\/(\d+)/);
|
|
9527
|
+
if (digestMatch) {
|
|
9528
|
+
const digestIssueNumber = parseInt(digestMatch[1], 10);
|
|
9529
|
+
config.watch.digestIssue = digestIssueNumber;
|
|
9530
|
+
fs8.writeFileSync(kodyConfigPath, JSON.stringify(config, null, 2) + "\n");
|
|
9531
|
+
console.log(` \u2713 Created digest issue #${digestIssueNumber}`);
|
|
9532
|
+
try {
|
|
9533
|
+
execFileSync4("gh", [
|
|
9534
|
+
"issue",
|
|
9535
|
+
"pin",
|
|
9536
|
+
String(digestIssueNumber),
|
|
9537
|
+
"--repo",
|
|
9538
|
+
watchRepoSlug
|
|
9539
|
+
], {
|
|
9540
|
+
cwd,
|
|
9541
|
+
encoding: "utf-8",
|
|
9542
|
+
timeout: 1e4,
|
|
9543
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
9544
|
+
});
|
|
9545
|
+
console.log(` \u2713 Pinned digest issue`);
|
|
9546
|
+
} catch {
|
|
9547
|
+
}
|
|
9548
|
+
try {
|
|
9549
|
+
execFileSync4("gh", [
|
|
9550
|
+
"variable",
|
|
9551
|
+
"set",
|
|
9552
|
+
"WATCH_DIGEST_ISSUE",
|
|
9553
|
+
"--repo",
|
|
9554
|
+
watchRepoSlug,
|
|
9555
|
+
"--body",
|
|
9556
|
+
String(digestIssueNumber)
|
|
9557
|
+
], {
|
|
9558
|
+
cwd,
|
|
9559
|
+
encoding: "utf-8",
|
|
9560
|
+
timeout: 1e4,
|
|
9561
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
9562
|
+
});
|
|
9563
|
+
console.log(` \u2713 Set WATCH_DIGEST_ISSUE variable`);
|
|
9564
|
+
} catch {
|
|
9565
|
+
}
|
|
9566
|
+
}
|
|
9567
|
+
} catch (err) {
|
|
9568
|
+
console.log(` \u26A0 Failed to create digest issue: ${err instanceof Error ? err.message : err}`);
|
|
9569
|
+
}
|
|
9570
|
+
}
|
|
9571
|
+
} else if (config.watch?.digestIssue) {
|
|
9572
|
+
console.log(` \u25CB Digest issue already set: #${config.watch.digestIssue}`);
|
|
9573
|
+
} else {
|
|
9574
|
+
console.log(" \u25CB Watch not enabled \u2014 skipping digest issue setup");
|
|
9575
|
+
}
|
|
9576
|
+
} catch {
|
|
9577
|
+
}
|
|
9578
|
+
}
|
|
8280
9579
|
console.log("\n\u2500\u2500 Labels \u2500\u2500");
|
|
8281
9580
|
try {
|
|
8282
9581
|
let repoSlug = "";
|
|
@@ -8457,6 +9756,7 @@ ${entries}
|
|
|
8457
9756
|
".kody/memory/conventions.md",
|
|
8458
9757
|
".kody/qa-guide.md",
|
|
8459
9758
|
".kody/tools.yml",
|
|
9759
|
+
"kody.config.json",
|
|
8460
9760
|
...installedSkillPaths
|
|
8461
9761
|
].filter((f) => fs8.existsSync(path7.join(cwd, f)));
|
|
8462
9762
|
for (const stage of STEP_STAGES) {
|
|
@@ -8570,11 +9870,11 @@ Create it manually.`, cwd);
|
|
|
8570
9870
|
|
|
8571
9871
|
// src/bin/cli.ts
|
|
8572
9872
|
init_architecture_detection();
|
|
8573
|
-
var __dirname2 =
|
|
8574
|
-
var PKG_ROOT =
|
|
9873
|
+
var __dirname2 = path36.dirname(fileURLToPath2(import.meta.url));
|
|
9874
|
+
var PKG_ROOT = path36.resolve(__dirname2, "..", "..");
|
|
8575
9875
|
function getVersion() {
|
|
8576
|
-
const pkgPath =
|
|
8577
|
-
const pkg = JSON.parse(
|
|
9876
|
+
const pkgPath = path36.join(PKG_ROOT, "package.json");
|
|
9877
|
+
const pkg = JSON.parse(fs39.readFileSync(pkgPath, "utf-8"));
|
|
8578
9878
|
return pkg.version;
|
|
8579
9879
|
}
|
|
8580
9880
|
var args = process.argv.slice(2);
|
|
@@ -8589,6 +9889,10 @@ if (command === "init") {
|
|
|
8589
9889
|
Promise.resolve().then(() => (init_test_model_command(), test_model_command_exports)).then(({ runTestModelCommand: runTestModelCommand2 }) => runTestModelCommand2());
|
|
8590
9890
|
} else if (command === "ci-parse") {
|
|
8591
9891
|
Promise.resolve().then(() => (init_parse_inputs(), parse_inputs_exports)).then(({ runCiParse: runCiParse2 }) => runCiParse2());
|
|
9892
|
+
} else if (command === "watch") {
|
|
9893
|
+
Promise.resolve().then(() => (init_watch2(), watch_exports)).then(
|
|
9894
|
+
({ runWatchCommand: runWatchCommand2 }) => runWatchCommand2({ dryRun: args.includes("--dry-run") })
|
|
9895
|
+
);
|
|
8592
9896
|
} else if (command === "version" || command === "--version" || command === "-v") {
|
|
8593
9897
|
console.log(getVersion());
|
|
8594
9898
|
} else {
|