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