@kody-ade/kody-engine-lite 0.1.135 → 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 +1709 -344
- 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
|
}
|
|
@@ -2872,97 +2876,1282 @@ function parseCommentInputs() {
|
|
|
2872
2876
|
feedback = bodyAfterCommand;
|
|
2873
2877
|
}
|
|
2874
2878
|
}
|
|
2875
|
-
if (mode === "fix-ci") {
|
|
2876
|
-
if (bodyAfterCommand) {
|
|
2877
|
-
feedback = bodyAfterCommand;
|
|
2878
|
-
const runIdFromBody = bodyAfterCommand.match(/Run ID:\s*(\d+)/);
|
|
2879
|
-
if (runIdFromBody) {
|
|
2880
|
-
ciRunId = runIdFromBody[1];
|
|
2879
|
+
if (mode === "fix-ci") {
|
|
2880
|
+
if (bodyAfterCommand) {
|
|
2881
|
+
feedback = bodyAfterCommand;
|
|
2882
|
+
const runIdFromBody = bodyAfterCommand.match(/Run ID:\s*(\d+)/);
|
|
2883
|
+
if (runIdFromBody) {
|
|
2884
|
+
ciRunId = runIdFromBody[1];
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
if (mode === "bootstrap") {
|
|
2889
|
+
taskId = `bootstrap-${generateTimestamp()}`;
|
|
2890
|
+
}
|
|
2891
|
+
if (mode === "taskify") {
|
|
2892
|
+
taskId = `taskify-${issueNumber}-${generateTimestamp()}`;
|
|
2893
|
+
}
|
|
2894
|
+
const prNumber = isPR ? issueNumber : "";
|
|
2895
|
+
if (mode === "review" && prNumber) {
|
|
2896
|
+
taskId = `review-pr-${prNumber}-${generateTimestamp()}`;
|
|
2897
|
+
}
|
|
2898
|
+
if (!taskId && mode === "full") {
|
|
2899
|
+
taskId = `${issueNumber}-${generateTimestamp()}`;
|
|
2900
|
+
}
|
|
2901
|
+
const modesWithoutTaskId = ["fix", "fix-ci", "status", "review", "resolve", "rerun"];
|
|
2902
|
+
const valid = !!taskId || modesWithoutTaskId.includes(mode);
|
|
2903
|
+
return {
|
|
2904
|
+
task_id: taskId,
|
|
2905
|
+
mode,
|
|
2906
|
+
from_stage: fromStage,
|
|
2907
|
+
issue_number: issueNumber,
|
|
2908
|
+
pr_number: prNumber,
|
|
2909
|
+
feedback,
|
|
2910
|
+
complexity,
|
|
2911
|
+
ci_run_id: ciRunId,
|
|
2912
|
+
ticket_id: ticketId,
|
|
2913
|
+
prd_file: prdFile,
|
|
2914
|
+
dry_run: dryRun,
|
|
2915
|
+
valid,
|
|
2916
|
+
trigger_type: "comment"
|
|
2917
|
+
};
|
|
2918
|
+
}
|
|
2919
|
+
function writeOutputs(result2) {
|
|
2920
|
+
const outputFile = process.env.GITHUB_OUTPUT;
|
|
2921
|
+
function output(key, value) {
|
|
2922
|
+
if (outputFile) {
|
|
2923
|
+
if (value.includes("\n")) {
|
|
2924
|
+
fs14.appendFileSync(outputFile, `${key}<<KODY_EOF
|
|
2925
|
+
${value}
|
|
2926
|
+
KODY_EOF
|
|
2927
|
+
`);
|
|
2928
|
+
} else {
|
|
2929
|
+
fs14.appendFileSync(outputFile, `${key}=${value}
|
|
2930
|
+
`);
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
const display = value.includes("\n") ? value.split("\n")[0] + "..." : value;
|
|
2934
|
+
console.log(`${key}=${display}`);
|
|
2935
|
+
}
|
|
2936
|
+
output("task_id", result2.task_id);
|
|
2937
|
+
output("mode", result2.mode);
|
|
2938
|
+
output("from_stage", result2.from_stage);
|
|
2939
|
+
output("issue_number", result2.issue_number);
|
|
2940
|
+
output("pr_number", result2.pr_number);
|
|
2941
|
+
output("feedback", result2.feedback);
|
|
2942
|
+
output("complexity", result2.complexity);
|
|
2943
|
+
output("ci_run_id", result2.ci_run_id);
|
|
2944
|
+
output("ticket_id", result2.ticket_id);
|
|
2945
|
+
output("prd_file", result2.prd_file);
|
|
2946
|
+
output("dry_run", result2.dry_run ? "true" : "false");
|
|
2947
|
+
output("valid", result2.valid ? "true" : "false");
|
|
2948
|
+
output("trigger_type", result2.trigger_type);
|
|
2949
|
+
}
|
|
2950
|
+
function runCiParse() {
|
|
2951
|
+
const result2 = parseCommentInputs();
|
|
2952
|
+
writeOutputs(result2);
|
|
2953
|
+
}
|
|
2954
|
+
var VALID_MODES;
|
|
2955
|
+
var init_parse_inputs = __esm({
|
|
2956
|
+
"src/ci/parse-inputs.ts"() {
|
|
2957
|
+
"use strict";
|
|
2958
|
+
VALID_MODES = [
|
|
2959
|
+
"full",
|
|
2960
|
+
"rerun",
|
|
2961
|
+
"fix",
|
|
2962
|
+
"fix-ci",
|
|
2963
|
+
"status",
|
|
2964
|
+
"approve",
|
|
2965
|
+
"review",
|
|
2966
|
+
"resolve",
|
|
2967
|
+
"bootstrap",
|
|
2968
|
+
"taskify"
|
|
2969
|
+
];
|
|
2970
|
+
}
|
|
2971
|
+
});
|
|
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;
|
|
2881
4077
|
}
|
|
2882
|
-
}
|
|
4078
|
+
};
|
|
2883
4079
|
}
|
|
2884
|
-
|
|
2885
|
-
|
|
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
|
+
}
|
|
2886
4105
|
}
|
|
2887
|
-
if (
|
|
2888
|
-
|
|
4106
|
+
if (process.env.WATCH_DIGEST_ISSUE) {
|
|
4107
|
+
digestIssue = parseInt(process.env.WATCH_DIGEST_ISSUE, 10) || void 0;
|
|
2889
4108
|
}
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
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);
|
|
2893
4112
|
}
|
|
2894
|
-
if (!
|
|
2895
|
-
|
|
4113
|
+
if (!process.env.GH_TOKEN && !process.env.GITHUB_TOKEN) {
|
|
4114
|
+
console.error("Missing GH_TOKEN or GITHUB_TOKEN");
|
|
4115
|
+
process.exit(1);
|
|
2896
4116
|
}
|
|
2897
|
-
const
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
ci_run_id: ciRunId,
|
|
2908
|
-
ticket_id: ticketId,
|
|
2909
|
-
prd_file: prdFile,
|
|
2910
|
-
dry_run: dryRun,
|
|
2911
|
-
valid,
|
|
2912
|
-
trigger_type: "comment"
|
|
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
|
|
2913
4127
|
};
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
const outputFile = process.env.GITHUB_OUTPUT;
|
|
2917
|
-
function output(key, value) {
|
|
2918
|
-
if (outputFile) {
|
|
2919
|
-
if (value.includes("\n")) {
|
|
2920
|
-
fs14.appendFileSync(outputFile, `${key}<<KODY_EOF
|
|
2921
|
-
${value}
|
|
2922
|
-
KODY_EOF
|
|
2923
|
-
`);
|
|
2924
|
-
} else {
|
|
2925
|
-
fs14.appendFileSync(outputFile, `${key}=${value}
|
|
4128
|
+
console.log(`
|
|
4129
|
+
Kody Watch \u2014 repo: ${repo}, dry-run: ${opts.dryRun}
|
|
2926
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}`);
|
|
2927
4136
|
}
|
|
2928
4137
|
}
|
|
2929
|
-
|
|
2930
|
-
|
|
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);
|
|
2931
4145
|
}
|
|
2932
|
-
output("task_id", result2.task_id);
|
|
2933
|
-
output("mode", result2.mode);
|
|
2934
|
-
output("from_stage", result2.from_stage);
|
|
2935
|
-
output("issue_number", result2.issue_number);
|
|
2936
|
-
output("pr_number", result2.pr_number);
|
|
2937
|
-
output("feedback", result2.feedback);
|
|
2938
|
-
output("complexity", result2.complexity);
|
|
2939
|
-
output("ci_run_id", result2.ci_run_id);
|
|
2940
|
-
output("ticket_id", result2.ticket_id);
|
|
2941
|
-
output("prd_file", result2.prd_file);
|
|
2942
|
-
output("dry_run", result2.dry_run ? "true" : "false");
|
|
2943
|
-
output("valid", result2.valid ? "true" : "false");
|
|
2944
|
-
output("trigger_type", result2.trigger_type);
|
|
2945
|
-
}
|
|
2946
|
-
function runCiParse() {
|
|
2947
|
-
const result2 = parseCommentInputs();
|
|
2948
|
-
writeOutputs(result2);
|
|
2949
4146
|
}
|
|
2950
|
-
var
|
|
2951
|
-
|
|
2952
|
-
"src/ci/parse-inputs.ts"() {
|
|
4147
|
+
var init_watch2 = __esm({
|
|
4148
|
+
"src/watch/index.ts"() {
|
|
2953
4149
|
"use strict";
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
"status",
|
|
2960
|
-
"approve",
|
|
2961
|
-
"review",
|
|
2962
|
-
"resolve",
|
|
2963
|
-
"bootstrap",
|
|
2964
|
-
"taskify"
|
|
2965
|
-
];
|
|
4150
|
+
init_watch();
|
|
4151
|
+
init_registry();
|
|
4152
|
+
init_pipeline_health();
|
|
4153
|
+
init_security_scan();
|
|
4154
|
+
init_config_health();
|
|
2966
4155
|
}
|
|
2967
4156
|
});
|
|
2968
4157
|
|
|
@@ -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,25 +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
|
-
|
|
6124
|
+
const memoryDir = path27.join(ctx.projectDir, ".kody", "memory");
|
|
6125
|
+
const addPaths = [ctx.taskDir];
|
|
6126
|
+
if (fs29.existsSync(memoryDir)) addPaths.push(memoryDir);
|
|
6127
|
+
execFileSync18("git", ["add", ...addPaths], {
|
|
4936
6128
|
cwd: ctx.projectDir,
|
|
4937
6129
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
4938
6130
|
stdio: "pipe"
|
|
4939
6131
|
});
|
|
4940
|
-
|
|
6132
|
+
execFileSync18("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
|
|
4941
6133
|
cwd: ctx.projectDir,
|
|
4942
6134
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
4943
6135
|
stdio: "pipe"
|
|
@@ -4951,7 +6143,7 @@ function executeShipStage(ctx, _def) {
|
|
|
4951
6143
|
let repo = config.github?.repo;
|
|
4952
6144
|
if (!owner || !repo) {
|
|
4953
6145
|
try {
|
|
4954
|
-
const remoteUrl =
|
|
6146
|
+
const remoteUrl = execFileSync18("git", ["remote", "get-url", "origin"], {
|
|
4955
6147
|
encoding: "utf-8",
|
|
4956
6148
|
cwd: ctx.projectDir
|
|
4957
6149
|
}).trim();
|
|
@@ -4972,28 +6164,28 @@ function executeShipStage(ctx, _def) {
|
|
|
4972
6164
|
chore: "chore"
|
|
4973
6165
|
};
|
|
4974
6166
|
let prefix = "chore";
|
|
4975
|
-
const taskJsonPath =
|
|
4976
|
-
if (
|
|
6167
|
+
const taskJsonPath = path27.join(ctx.taskDir, "task.json");
|
|
6168
|
+
if (fs29.existsSync(taskJsonPath)) {
|
|
4977
6169
|
try {
|
|
4978
|
-
const raw =
|
|
6170
|
+
const raw = fs29.readFileSync(taskJsonPath, "utf-8");
|
|
4979
6171
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
4980
6172
|
const task = JSON.parse(cleaned);
|
|
4981
6173
|
prefix = TYPE_PREFIX[task.task_type] ?? "chore";
|
|
4982
6174
|
} catch {
|
|
4983
6175
|
}
|
|
4984
6176
|
}
|
|
4985
|
-
const taskMdPath =
|
|
4986
|
-
if (
|
|
4987
|
-
const content =
|
|
6177
|
+
const taskMdPath = path27.join(ctx.taskDir, "task.md");
|
|
6178
|
+
if (fs29.existsSync(taskMdPath)) {
|
|
6179
|
+
const content = fs29.readFileSync(taskMdPath, "utf-8");
|
|
4988
6180
|
const heading = content.split("\n").find((l) => l.startsWith("# "));
|
|
4989
6181
|
if (heading) {
|
|
4990
6182
|
title = `${prefix}: ${heading.replace(/^#\s*/, "").trim()}`.slice(0, 72);
|
|
4991
6183
|
}
|
|
4992
6184
|
}
|
|
4993
6185
|
if (title === "Update") {
|
|
4994
|
-
if (
|
|
6186
|
+
if (fs29.existsSync(taskJsonPath)) {
|
|
4995
6187
|
try {
|
|
4996
|
-
const raw =
|
|
6188
|
+
const raw = fs29.readFileSync(taskJsonPath, "utf-8");
|
|
4997
6189
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
4998
6190
|
const task = JSON.parse(cleaned);
|
|
4999
6191
|
if (task.title) title = `${prefix}: ${task.title}`.slice(0, 72);
|
|
@@ -5016,7 +6208,7 @@ function executeShipStage(ctx, _def) {
|
|
|
5016
6208
|
} catch {
|
|
5017
6209
|
}
|
|
5018
6210
|
}
|
|
5019
|
-
|
|
6211
|
+
fs29.writeFileSync(shipPath, `# Ship
|
|
5020
6212
|
|
|
5021
6213
|
Updated existing PR: ${existingPr.url}
|
|
5022
6214
|
PR #${existingPr.number}
|
|
@@ -5037,20 +6229,20 @@ PR #${existingPr.number}
|
|
|
5037
6229
|
} catch {
|
|
5038
6230
|
}
|
|
5039
6231
|
}
|
|
5040
|
-
|
|
6232
|
+
fs29.writeFileSync(shipPath, `# Ship
|
|
5041
6233
|
|
|
5042
6234
|
PR created: ${pr.url}
|
|
5043
6235
|
PR #${pr.number}
|
|
5044
6236
|
`);
|
|
5045
6237
|
} else {
|
|
5046
|
-
|
|
6238
|
+
fs29.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
|
|
5047
6239
|
}
|
|
5048
6240
|
}
|
|
5049
6241
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
5050
6242
|
} catch (err) {
|
|
5051
6243
|
const msg = err instanceof Error ? err.message : String(err);
|
|
5052
6244
|
try {
|
|
5053
|
-
|
|
6245
|
+
fs29.writeFileSync(shipPath, `# Ship
|
|
5054
6246
|
|
|
5055
6247
|
Failed: ${msg}
|
|
5056
6248
|
`);
|
|
@@ -5099,15 +6291,15 @@ var init_executor_registry = __esm({
|
|
|
5099
6291
|
});
|
|
5100
6292
|
|
|
5101
6293
|
// src/pipeline/questions.ts
|
|
5102
|
-
import * as
|
|
5103
|
-
import * as
|
|
6294
|
+
import * as fs30 from "fs";
|
|
6295
|
+
import * as path28 from "path";
|
|
5104
6296
|
function checkForQuestions(ctx, stageName) {
|
|
5105
6297
|
if (ctx.input.local || !ctx.input.issueNumber) return false;
|
|
5106
6298
|
try {
|
|
5107
6299
|
if (stageName === "taskify") {
|
|
5108
|
-
const taskJsonPath =
|
|
5109
|
-
if (!
|
|
5110
|
-
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");
|
|
5111
6303
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
5112
6304
|
const taskJson = JSON.parse(cleaned);
|
|
5113
6305
|
if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
|
|
@@ -5122,9 +6314,9 @@ Reply with \`@kody approve\` and your answers in the comment body.`;
|
|
|
5122
6314
|
}
|
|
5123
6315
|
}
|
|
5124
6316
|
if (stageName === "plan") {
|
|
5125
|
-
const planPath =
|
|
5126
|
-
if (!
|
|
5127
|
-
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");
|
|
5128
6320
|
const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
5129
6321
|
if (questionsMatch) {
|
|
5130
6322
|
const questionsText = questionsMatch[1].trim();
|
|
@@ -5153,8 +6345,8 @@ var init_questions = __esm({
|
|
|
5153
6345
|
});
|
|
5154
6346
|
|
|
5155
6347
|
// src/pipeline/hooks.ts
|
|
5156
|
-
import * as
|
|
5157
|
-
import * as
|
|
6348
|
+
import * as fs31 from "fs";
|
|
6349
|
+
import * as path29 from "path";
|
|
5158
6350
|
function applyPreStageLabel(ctx, def) {
|
|
5159
6351
|
if (!ctx.input.issueNumber || ctx.input.local) return;
|
|
5160
6352
|
if (def.name === "plan") setLifecycleLabel(ctx.input.issueNumber, "planning");
|
|
@@ -5195,9 +6387,9 @@ function autoDetectComplexity(ctx, def) {
|
|
|
5195
6387
|
return { complexity, activeStages };
|
|
5196
6388
|
}
|
|
5197
6389
|
try {
|
|
5198
|
-
const taskJsonPath =
|
|
5199
|
-
if (!
|
|
5200
|
-
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");
|
|
5201
6393
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
5202
6394
|
const taskJson = JSON.parse(cleaned);
|
|
5203
6395
|
if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
|
|
@@ -5227,8 +6419,8 @@ function checkRiskGate(ctx, def, state, complexity) {
|
|
|
5227
6419
|
if (ctx.input.dryRun || ctx.input.local) return null;
|
|
5228
6420
|
if (ctx.input.mode === "rerun") return null;
|
|
5229
6421
|
if (!ctx.input.issueNumber) return null;
|
|
5230
|
-
const planPath =
|
|
5231
|
-
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)";
|
|
5232
6424
|
try {
|
|
5233
6425
|
postComment(
|
|
5234
6426
|
ctx.input.issueNumber,
|
|
@@ -5289,28 +6481,28 @@ var init_hooks = __esm({
|
|
|
5289
6481
|
init_git_utils();
|
|
5290
6482
|
init_questions();
|
|
5291
6483
|
init_complexity();
|
|
5292
|
-
|
|
6484
|
+
init_state2();
|
|
5293
6485
|
init_logger();
|
|
5294
6486
|
}
|
|
5295
6487
|
});
|
|
5296
6488
|
|
|
5297
6489
|
// src/learning/auto-learn.ts
|
|
5298
|
-
import * as
|
|
5299
|
-
import * as
|
|
6490
|
+
import * as fs32 from "fs";
|
|
6491
|
+
import * as path30 from "path";
|
|
5300
6492
|
function stripAnsi(str) {
|
|
5301
6493
|
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
5302
6494
|
}
|
|
5303
6495
|
function autoLearn(ctx) {
|
|
5304
6496
|
try {
|
|
5305
|
-
const memoryDir =
|
|
5306
|
-
if (!
|
|
5307
|
-
|
|
6497
|
+
const memoryDir = path30.join(ctx.projectDir, ".kody", "memory");
|
|
6498
|
+
if (!fs32.existsSync(memoryDir)) {
|
|
6499
|
+
fs32.mkdirSync(memoryDir, { recursive: true });
|
|
5308
6500
|
}
|
|
5309
6501
|
const learnings = [];
|
|
5310
6502
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
5311
|
-
const verifyPath =
|
|
5312
|
-
if (
|
|
5313
|
-
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"));
|
|
5314
6506
|
if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
|
|
5315
6507
|
if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
|
|
5316
6508
|
if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
|
|
@@ -5319,18 +6511,18 @@ function autoLearn(ctx) {
|
|
|
5319
6511
|
if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
|
|
5320
6512
|
if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
|
|
5321
6513
|
}
|
|
5322
|
-
const reviewPath =
|
|
5323
|
-
if (
|
|
5324
|
-
const review =
|
|
6514
|
+
const reviewPath = path30.join(ctx.taskDir, "review.md");
|
|
6515
|
+
if (fs32.existsSync(reviewPath)) {
|
|
6516
|
+
const review = fs32.readFileSync(reviewPath, "utf-8");
|
|
5325
6517
|
if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
|
|
5326
6518
|
if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
|
|
5327
6519
|
if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
|
|
5328
6520
|
if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
|
|
5329
6521
|
}
|
|
5330
|
-
const taskJsonPath =
|
|
5331
|
-
if (
|
|
6522
|
+
const taskJsonPath = path30.join(ctx.taskDir, "task.json");
|
|
6523
|
+
if (fs32.existsSync(taskJsonPath)) {
|
|
5332
6524
|
try {
|
|
5333
|
-
const raw = stripAnsi(
|
|
6525
|
+
const raw = stripAnsi(fs32.readFileSync(taskJsonPath, "utf-8"));
|
|
5334
6526
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
5335
6527
|
const task = JSON.parse(cleaned);
|
|
5336
6528
|
if (task.scope && Array.isArray(task.scope)) {
|
|
@@ -5341,12 +6533,12 @@ function autoLearn(ctx) {
|
|
|
5341
6533
|
}
|
|
5342
6534
|
}
|
|
5343
6535
|
if (learnings.length > 0) {
|
|
5344
|
-
const conventionsPath =
|
|
6536
|
+
const conventionsPath = path30.join(memoryDir, "conventions.md");
|
|
5345
6537
|
const entry = `
|
|
5346
6538
|
## Learned ${timestamp2} (task: ${ctx.taskId})
|
|
5347
6539
|
${learnings.join("\n")}
|
|
5348
6540
|
`;
|
|
5349
|
-
|
|
6541
|
+
fs32.appendFileSync(conventionsPath, entry);
|
|
5350
6542
|
logger.info(`Auto-learned ${learnings.length} convention(s)`);
|
|
5351
6543
|
}
|
|
5352
6544
|
autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
|
|
@@ -5354,8 +6546,8 @@ ${learnings.join("\n")}
|
|
|
5354
6546
|
}
|
|
5355
6547
|
}
|
|
5356
6548
|
function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
5357
|
-
const archPath =
|
|
5358
|
-
if (
|
|
6549
|
+
const archPath = path30.join(memoryDir, "architecture.md");
|
|
6550
|
+
if (fs32.existsSync(archPath)) return;
|
|
5359
6551
|
const detected = detectArchitectureBasic(projectDir);
|
|
5360
6552
|
if (detected.length > 0) {
|
|
5361
6553
|
const content = `# Architecture (auto-detected ${timestamp2})
|
|
@@ -5363,7 +6555,7 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
|
5363
6555
|
## Overview
|
|
5364
6556
|
${detected.join("\n")}
|
|
5365
6557
|
`;
|
|
5366
|
-
|
|
6558
|
+
fs32.writeFileSync(archPath, content);
|
|
5367
6559
|
logger.info(`Auto-detected architecture (${detected.length} items)`);
|
|
5368
6560
|
}
|
|
5369
6561
|
}
|
|
@@ -5376,13 +6568,13 @@ var init_auto_learn = __esm({
|
|
|
5376
6568
|
});
|
|
5377
6569
|
|
|
5378
6570
|
// src/retrospective.ts
|
|
5379
|
-
import * as
|
|
5380
|
-
import * as
|
|
6571
|
+
import * as fs33 from "fs";
|
|
6572
|
+
import * as path31 from "path";
|
|
5381
6573
|
function readArtifact(taskDir, filename, maxChars) {
|
|
5382
|
-
const p =
|
|
5383
|
-
if (!
|
|
6574
|
+
const p = path31.join(taskDir, filename);
|
|
6575
|
+
if (!fs33.existsSync(p)) return null;
|
|
5384
6576
|
try {
|
|
5385
|
-
const content =
|
|
6577
|
+
const content = fs33.readFileSync(p, "utf-8");
|
|
5386
6578
|
return content.length > maxChars ? content.slice(0, maxChars) + "\n...(truncated)" : content;
|
|
5387
6579
|
} catch {
|
|
5388
6580
|
return null;
|
|
@@ -5435,13 +6627,13 @@ function collectRunContext(ctx, state, pipelineStartTime) {
|
|
|
5435
6627
|
return lines.join("\n");
|
|
5436
6628
|
}
|
|
5437
6629
|
function getLogPath(projectDir) {
|
|
5438
|
-
return
|
|
6630
|
+
return path31.join(projectDir, ".kody", "memory", "observer-log.jsonl");
|
|
5439
6631
|
}
|
|
5440
6632
|
function readPreviousRetrospectives(projectDir, limit = 10) {
|
|
5441
6633
|
const logPath = getLogPath(projectDir);
|
|
5442
|
-
if (!
|
|
6634
|
+
if (!fs33.existsSync(logPath)) return [];
|
|
5443
6635
|
try {
|
|
5444
|
-
const content =
|
|
6636
|
+
const content = fs33.readFileSync(logPath, "utf-8");
|
|
5445
6637
|
const lines = content.split("\n").filter(Boolean);
|
|
5446
6638
|
const entries = [];
|
|
5447
6639
|
const start = Math.max(0, lines.length - limit);
|
|
@@ -5468,11 +6660,11 @@ function formatPreviousEntries(entries) {
|
|
|
5468
6660
|
}
|
|
5469
6661
|
function appendRetrospectiveEntry(projectDir, entry) {
|
|
5470
6662
|
const logPath = getLogPath(projectDir);
|
|
5471
|
-
const dir =
|
|
5472
|
-
if (!
|
|
5473
|
-
|
|
6663
|
+
const dir = path31.dirname(logPath);
|
|
6664
|
+
if (!fs33.existsSync(dir)) {
|
|
6665
|
+
fs33.mkdirSync(dir, { recursive: true });
|
|
5474
6666
|
}
|
|
5475
|
-
|
|
6667
|
+
fs33.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
5476
6668
|
}
|
|
5477
6669
|
async function runRetrospective(ctx, state, pipelineStartTime) {
|
|
5478
6670
|
if (ctx.input.dryRun) return;
|
|
@@ -5640,15 +6832,15 @@ var init_summary = __esm({
|
|
|
5640
6832
|
});
|
|
5641
6833
|
|
|
5642
6834
|
// src/tools.ts
|
|
5643
|
-
import * as
|
|
5644
|
-
import * as
|
|
6835
|
+
import * as fs34 from "fs";
|
|
6836
|
+
import * as path32 from "path";
|
|
5645
6837
|
import { execSync as execSync3 } from "child_process";
|
|
5646
6838
|
import { parse as parseYaml } from "yaml";
|
|
5647
6839
|
function loadToolDeclarations(projectDir) {
|
|
5648
|
-
const toolsPath =
|
|
5649
|
-
if (!
|
|
6840
|
+
const toolsPath = path32.join(projectDir, ".kody", "tools.yml");
|
|
6841
|
+
if (!fs34.existsSync(toolsPath)) return [];
|
|
5650
6842
|
try {
|
|
5651
|
-
const raw =
|
|
6843
|
+
const raw = fs34.readFileSync(toolsPath, "utf-8");
|
|
5652
6844
|
const parsed = parseYaml(raw);
|
|
5653
6845
|
if (!parsed || typeof parsed !== "object") return [];
|
|
5654
6846
|
return Object.entries(parsed).map(([name, value]) => {
|
|
@@ -5669,7 +6861,7 @@ function loadToolDeclarations(projectDir) {
|
|
|
5669
6861
|
function detectTools(declarations, projectDir) {
|
|
5670
6862
|
const resolved = [];
|
|
5671
6863
|
for (const decl of declarations) {
|
|
5672
|
-
const detected = decl.detect.some((pattern) =>
|
|
6864
|
+
const detected = decl.detect.some((pattern) => fs34.existsSync(path32.join(projectDir, pattern)));
|
|
5673
6865
|
if (!detected) continue;
|
|
5674
6866
|
resolved.push({
|
|
5675
6867
|
name: decl.name,
|
|
@@ -5710,8 +6902,8 @@ var init_tools = __esm({
|
|
|
5710
6902
|
});
|
|
5711
6903
|
|
|
5712
6904
|
// src/pipeline.ts
|
|
5713
|
-
import * as
|
|
5714
|
-
import * as
|
|
6905
|
+
import * as fs35 from "fs";
|
|
6906
|
+
import * as path33 from "path";
|
|
5715
6907
|
function ensureFeatureBranchIfNeeded(ctx) {
|
|
5716
6908
|
if (ctx.input.dryRun) return;
|
|
5717
6909
|
if (ctx.input.prNumber) {
|
|
@@ -5724,8 +6916,8 @@ function ensureFeatureBranchIfNeeded(ctx) {
|
|
|
5724
6916
|
}
|
|
5725
6917
|
if (!ctx.input.issueNumber) return;
|
|
5726
6918
|
try {
|
|
5727
|
-
const taskMdPath =
|
|
5728
|
-
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;
|
|
5729
6921
|
ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
|
|
5730
6922
|
syncWithDefault(ctx.projectDir);
|
|
5731
6923
|
} catch (err) {
|
|
@@ -5739,10 +6931,10 @@ function ensureFeatureBranchIfNeeded(ctx) {
|
|
|
5739
6931
|
}
|
|
5740
6932
|
}
|
|
5741
6933
|
function acquireLock(taskDir) {
|
|
5742
|
-
const lockPath =
|
|
5743
|
-
if (
|
|
6934
|
+
const lockPath = path33.join(taskDir, ".lock");
|
|
6935
|
+
if (fs35.existsSync(lockPath)) {
|
|
5744
6936
|
try {
|
|
5745
|
-
const pid = parseInt(
|
|
6937
|
+
const pid = parseInt(fs35.readFileSync(lockPath, "utf-8").trim(), 10);
|
|
5746
6938
|
if (!isNaN(pid)) {
|
|
5747
6939
|
try {
|
|
5748
6940
|
process.kill(pid, 0);
|
|
@@ -5759,14 +6951,14 @@ function acquireLock(taskDir) {
|
|
|
5759
6951
|
logger.warn(` Corrupt lock file \u2014 overwriting`);
|
|
5760
6952
|
}
|
|
5761
6953
|
try {
|
|
5762
|
-
|
|
6954
|
+
fs35.unlinkSync(lockPath);
|
|
5763
6955
|
} catch {
|
|
5764
6956
|
}
|
|
5765
6957
|
}
|
|
5766
6958
|
try {
|
|
5767
|
-
const fd =
|
|
5768
|
-
|
|
5769
|
-
|
|
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);
|
|
5770
6962
|
} catch (err) {
|
|
5771
6963
|
if (err.code === "EEXIST") {
|
|
5772
6964
|
throw new Error("Pipeline already running (lock acquired by another process)");
|
|
@@ -5776,7 +6968,7 @@ function acquireLock(taskDir) {
|
|
|
5776
6968
|
}
|
|
5777
6969
|
function releaseLock(taskDir) {
|
|
5778
6970
|
try {
|
|
5779
|
-
|
|
6971
|
+
fs35.unlinkSync(path33.join(taskDir, ".lock"));
|
|
5780
6972
|
} catch {
|
|
5781
6973
|
}
|
|
5782
6974
|
}
|
|
@@ -5865,6 +7057,9 @@ async function runPipelineInner(ctx) {
|
|
|
5865
7057
|
}
|
|
5866
7058
|
continue;
|
|
5867
7059
|
}
|
|
7060
|
+
if (def.name === "ship") {
|
|
7061
|
+
autoLearn(ctx);
|
|
7062
|
+
}
|
|
5868
7063
|
ciGroup(`Stage: ${def.name}`);
|
|
5869
7064
|
state.stages[def.name] = { state: "running", startedAt: (/* @__PURE__ */ new Date()).toISOString(), retries: 0 };
|
|
5870
7065
|
writeState(state, ctx.taskDir);
|
|
@@ -5926,7 +7121,6 @@ async function runPipelineInner(ctx) {
|
|
|
5926
7121
|
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
5927
7122
|
setLifecycleLabel(ctx.input.issueNumber, "done");
|
|
5928
7123
|
}
|
|
5929
|
-
autoLearn(ctx);
|
|
5930
7124
|
}
|
|
5931
7125
|
await runRetrospective(ctx, state, pipelineStartTime).catch((err) => {
|
|
5932
7126
|
logger.warn(` Retrospective failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -5977,7 +7171,7 @@ var init_pipeline = __esm({
|
|
|
5977
7171
|
init_git_utils();
|
|
5978
7172
|
init_github_api();
|
|
5979
7173
|
init_logger();
|
|
5980
|
-
|
|
7174
|
+
init_state2();
|
|
5981
7175
|
init_complexity();
|
|
5982
7176
|
init_executor_registry();
|
|
5983
7177
|
init_hooks();
|
|
@@ -5990,8 +7184,8 @@ var init_pipeline = __esm({
|
|
|
5990
7184
|
});
|
|
5991
7185
|
|
|
5992
7186
|
// src/preflight.ts
|
|
5993
|
-
import { execFileSync as
|
|
5994
|
-
import * as
|
|
7187
|
+
import { execFileSync as execFileSync19 } from "child_process";
|
|
7188
|
+
import * as fs36 from "fs";
|
|
5995
7189
|
function check(name, fn) {
|
|
5996
7190
|
try {
|
|
5997
7191
|
const detail = fn() ?? void 0;
|
|
@@ -6003,7 +7197,7 @@ function check(name, fn) {
|
|
|
6003
7197
|
function runPreflight() {
|
|
6004
7198
|
const checks = [
|
|
6005
7199
|
check("claude CLI", () => {
|
|
6006
|
-
const v =
|
|
7200
|
+
const v = execFileSync19("claude", ["--version"], {
|
|
6007
7201
|
encoding: "utf-8",
|
|
6008
7202
|
timeout: 1e4,
|
|
6009
7203
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6011,14 +7205,14 @@ function runPreflight() {
|
|
|
6011
7205
|
return v;
|
|
6012
7206
|
}),
|
|
6013
7207
|
check("git repo", () => {
|
|
6014
|
-
|
|
7208
|
+
execFileSync19("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
6015
7209
|
encoding: "utf-8",
|
|
6016
7210
|
timeout: 5e3,
|
|
6017
7211
|
stdio: ["pipe", "pipe", "pipe"]
|
|
6018
7212
|
});
|
|
6019
7213
|
}),
|
|
6020
7214
|
check("pnpm", () => {
|
|
6021
|
-
const v =
|
|
7215
|
+
const v = execFileSync19("pnpm", ["--version"], {
|
|
6022
7216
|
encoding: "utf-8",
|
|
6023
7217
|
timeout: 5e3,
|
|
6024
7218
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6026,7 +7220,7 @@ function runPreflight() {
|
|
|
6026
7220
|
return v;
|
|
6027
7221
|
}),
|
|
6028
7222
|
check("node >= 18", () => {
|
|
6029
|
-
const v =
|
|
7223
|
+
const v = execFileSync19("node", ["--version"], {
|
|
6030
7224
|
encoding: "utf-8",
|
|
6031
7225
|
timeout: 5e3,
|
|
6032
7226
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6036,7 +7230,7 @@ function runPreflight() {
|
|
|
6036
7230
|
return v;
|
|
6037
7231
|
}),
|
|
6038
7232
|
check("gh CLI", () => {
|
|
6039
|
-
const v =
|
|
7233
|
+
const v = execFileSync19("gh", ["--version"], {
|
|
6040
7234
|
encoding: "utf-8",
|
|
6041
7235
|
timeout: 5e3,
|
|
6042
7236
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6044,7 +7238,7 @@ function runPreflight() {
|
|
|
6044
7238
|
return v;
|
|
6045
7239
|
}),
|
|
6046
7240
|
check("package.json", () => {
|
|
6047
|
-
if (!
|
|
7241
|
+
if (!fs36.existsSync("package.json")) throw new Error("not found");
|
|
6048
7242
|
})
|
|
6049
7243
|
];
|
|
6050
7244
|
const failed = checks.filter((c) => !c.ok);
|
|
@@ -6121,8 +7315,8 @@ var init_args = __esm({
|
|
|
6121
7315
|
});
|
|
6122
7316
|
|
|
6123
7317
|
// src/cli/task-state.ts
|
|
6124
|
-
import * as
|
|
6125
|
-
import * as
|
|
7318
|
+
import * as fs37 from "fs";
|
|
7319
|
+
import * as path34 from "path";
|
|
6126
7320
|
function resolveTaskAction(issueNumber, existingTaskId, existingState) {
|
|
6127
7321
|
if (!existingTaskId || !existingState) {
|
|
6128
7322
|
return { action: "start-fresh", taskId: `${issueNumber}-${generateTaskId()}` };
|
|
@@ -6154,11 +7348,11 @@ function resolveTaskAction(issueNumber, existingTaskId, existingState) {
|
|
|
6154
7348
|
function resolveForIssue(issueNumber, projectDir) {
|
|
6155
7349
|
const existingTaskId = findLatestTaskForIssue(issueNumber, projectDir);
|
|
6156
7350
|
if (existingTaskId) {
|
|
6157
|
-
const statusPath =
|
|
7351
|
+
const statusPath = path34.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
|
|
6158
7352
|
let existingState = null;
|
|
6159
|
-
if (
|
|
7353
|
+
if (fs37.existsSync(statusPath)) {
|
|
6160
7354
|
try {
|
|
6161
|
-
existingState = JSON.parse(
|
|
7355
|
+
existingState = JSON.parse(fs37.readFileSync(statusPath, "utf-8"));
|
|
6162
7356
|
} catch {
|
|
6163
7357
|
}
|
|
6164
7358
|
}
|
|
@@ -6191,12 +7385,12 @@ var resolve_exports = {};
|
|
|
6191
7385
|
__export(resolve_exports, {
|
|
6192
7386
|
runResolve: () => runResolve
|
|
6193
7387
|
});
|
|
6194
|
-
import { execFileSync as
|
|
7388
|
+
import { execFileSync as execFileSync20 } from "child_process";
|
|
6195
7389
|
function getConflictContext(cwd, files) {
|
|
6196
7390
|
const parts = [];
|
|
6197
7391
|
for (const file of files.slice(0, 10)) {
|
|
6198
7392
|
try {
|
|
6199
|
-
const content =
|
|
7393
|
+
const content = execFileSync20("git", ["diff", file], {
|
|
6200
7394
|
cwd,
|
|
6201
7395
|
encoding: "utf-8",
|
|
6202
7396
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6218,18 +7412,36 @@ async function runResolve(options) {
|
|
|
6218
7412
|
logger.info(`Resolving PR #${prNumber} \u2014 merging ${defaultBranch}...`);
|
|
6219
7413
|
const mergeResult = mergeDefault(projectDir);
|
|
6220
7414
|
if (mergeResult === "error") {
|
|
6221
|
-
|
|
7415
|
+
const error = "Failed to merge default branch";
|
|
7416
|
+
if (!local) {
|
|
7417
|
+
try {
|
|
7418
|
+
postPRComment(prNumber, `\u274C **Resolve failed:** ${error}`);
|
|
7419
|
+
} catch {
|
|
7420
|
+
}
|
|
7421
|
+
}
|
|
7422
|
+
return { outcome: "failed", error };
|
|
6222
7423
|
}
|
|
6223
7424
|
if (mergeResult === "clean") {
|
|
6224
7425
|
logger.info(" Clean merge \u2014 no conflicts");
|
|
6225
7426
|
if (!local) {
|
|
6226
7427
|
pushBranch(projectDir);
|
|
7428
|
+
try {
|
|
7429
|
+
postPRComment(prNumber, `\u2705 **Clean merge** \u2014 synced with \`${defaultBranch}\`, no conflicts.`);
|
|
7430
|
+
} catch {
|
|
7431
|
+
}
|
|
6227
7432
|
}
|
|
6228
7433
|
return { outcome: "merged" };
|
|
6229
7434
|
}
|
|
6230
7435
|
const conflictedFiles = getConflictedFiles(projectDir);
|
|
6231
7436
|
if (conflictedFiles.length === 0) {
|
|
6232
|
-
|
|
7437
|
+
const error = "Merge reported conflict but no conflicted files found";
|
|
7438
|
+
if (!local) {
|
|
7439
|
+
try {
|
|
7440
|
+
postPRComment(prNumber, `\u274C **Resolve failed:** ${error}`);
|
|
7441
|
+
} catch {
|
|
7442
|
+
}
|
|
7443
|
+
}
|
|
7444
|
+
return { outcome: "failed", error };
|
|
6233
7445
|
}
|
|
6234
7446
|
logger.info(` ${conflictedFiles.length} conflicted file(s): ${conflictedFiles.join(", ")}`);
|
|
6235
7447
|
const conflictContext = getConflictContext(projectDir, conflictedFiles);
|
|
@@ -6238,7 +7450,14 @@ async function runResolve(options) {
|
|
|
6238
7450
|
const runnerName = config.agent.defaultRunner ?? Object.keys(runners)[0] ?? "claude";
|
|
6239
7451
|
const runner = runners[runnerName];
|
|
6240
7452
|
if (!runner) {
|
|
6241
|
-
|
|
7453
|
+
const error = `Runner "${runnerName}" not found`;
|
|
7454
|
+
if (!local) {
|
|
7455
|
+
try {
|
|
7456
|
+
postPRComment(prNumber, `\u274C **Resolve failed:** ${error}`);
|
|
7457
|
+
} catch {
|
|
7458
|
+
}
|
|
7459
|
+
}
|
|
7460
|
+
return { outcome: "failed", error };
|
|
6242
7461
|
}
|
|
6243
7462
|
const model = resolveModel("mid");
|
|
6244
7463
|
const extraEnv = {};
|
|
@@ -6251,7 +7470,14 @@ async function runResolve(options) {
|
|
|
6251
7470
|
env: extraEnv
|
|
6252
7471
|
});
|
|
6253
7472
|
if (result2.outcome !== "completed") {
|
|
6254
|
-
|
|
7473
|
+
const error = `Agent failed: ${result2.error}`;
|
|
7474
|
+
if (!local) {
|
|
7475
|
+
try {
|
|
7476
|
+
postPRComment(prNumber, `\u274C **Resolve failed:** ${error}`);
|
|
7477
|
+
} catch {
|
|
7478
|
+
}
|
|
7479
|
+
}
|
|
7480
|
+
return { outcome: "failed", error };
|
|
6255
7481
|
}
|
|
6256
7482
|
logger.info(" Verifying resolution...");
|
|
6257
7483
|
const verify = runQualityGates(projectDir, projectDir, { onlyFailOnFiles: conflictedFiles });
|
|
@@ -6259,8 +7485,15 @@ async function runResolve(options) {
|
|
|
6259
7485
|
const errorSummary = verify.errors.slice(0, 5).join("\n");
|
|
6260
7486
|
logger.error(` Verification failed:
|
|
6261
7487
|
${errorSummary}`);
|
|
6262
|
-
|
|
6263
|
-
${errorSummary}
|
|
7488
|
+
const error = `Conflict resolution failed verification:
|
|
7489
|
+
${errorSummary}`;
|
|
7490
|
+
if (!local) {
|
|
7491
|
+
try {
|
|
7492
|
+
postPRComment(prNumber, `\u274C **Resolve failed:** ${error}`);
|
|
7493
|
+
} catch {
|
|
7494
|
+
}
|
|
7495
|
+
}
|
|
7496
|
+
return { outcome: "failed", error };
|
|
6264
7497
|
}
|
|
6265
7498
|
logger.info(" Verification passed");
|
|
6266
7499
|
commitAll(`chore: resolve merge conflicts with ${defaultBranch}`, projectDir);
|
|
@@ -6315,8 +7548,8 @@ var init_resolve = __esm({
|
|
|
6315
7548
|
|
|
6316
7549
|
// src/entry.ts
|
|
6317
7550
|
var entry_exports = {};
|
|
6318
|
-
import * as
|
|
6319
|
-
import * as
|
|
7551
|
+
import * as fs38 from "fs";
|
|
7552
|
+
import * as path35 from "path";
|
|
6320
7553
|
async function ensureLitellmProxy(config, projectDir) {
|
|
6321
7554
|
if (!anyStageNeedsProxy(config)) return null;
|
|
6322
7555
|
const litellmUrl = getLitellmUrl();
|
|
@@ -6371,9 +7604,9 @@ async function runModelHealthCheck(config) {
|
|
|
6371
7604
|
}
|
|
6372
7605
|
async function main() {
|
|
6373
7606
|
const input = parseArgs();
|
|
6374
|
-
const projectDir = input.cwd ?
|
|
7607
|
+
const projectDir = input.cwd ? path35.resolve(input.cwd) : process.cwd();
|
|
6375
7608
|
if (input.cwd) {
|
|
6376
|
-
if (!
|
|
7609
|
+
if (!fs38.existsSync(projectDir)) {
|
|
6377
7610
|
console.error(`--cwd path does not exist: ${projectDir}`);
|
|
6378
7611
|
process.exit(1);
|
|
6379
7612
|
}
|
|
@@ -6433,8 +7666,8 @@ async function main() {
|
|
|
6433
7666
|
process.exit(1);
|
|
6434
7667
|
}
|
|
6435
7668
|
}
|
|
6436
|
-
const taskDir =
|
|
6437
|
-
|
|
7669
|
+
const taskDir = path35.join(projectDir, ".kody", "tasks", taskId);
|
|
7670
|
+
fs38.mkdirSync(taskDir, { recursive: true });
|
|
6438
7671
|
if (input.command === "rerun" && isTaskifyRun(taskDir)) {
|
|
6439
7672
|
const marker = readTaskifyMarker(taskDir);
|
|
6440
7673
|
if (marker) {
|
|
@@ -6566,31 +7799,31 @@ async function main() {
|
|
|
6566
7799
|
logger.info("Preflight checks:");
|
|
6567
7800
|
runPreflight();
|
|
6568
7801
|
if (input.task) {
|
|
6569
|
-
|
|
7802
|
+
fs38.writeFileSync(path35.join(taskDir, "task.md"), input.task);
|
|
6570
7803
|
}
|
|
6571
|
-
const taskMdPath =
|
|
6572
|
-
if (!
|
|
7804
|
+
const taskMdPath = path35.join(taskDir, "task.md");
|
|
7805
|
+
if (!fs38.existsSync(taskMdPath) && isPRFix && input.prNumber) {
|
|
6573
7806
|
logger.info(`Fetching PR #${input.prNumber} details as task context...`);
|
|
6574
7807
|
const prDetails = getPRDetails(input.prNumber);
|
|
6575
7808
|
if (prDetails) {
|
|
6576
7809
|
const taskContent = `# ${prDetails.title}
|
|
6577
7810
|
|
|
6578
7811
|
${prDetails.body ?? ""}`;
|
|
6579
|
-
|
|
7812
|
+
fs38.writeFileSync(taskMdPath, taskContent);
|
|
6580
7813
|
logger.info(` Task loaded from PR #${input.prNumber}: ${prDetails.title}`);
|
|
6581
7814
|
}
|
|
6582
|
-
} else if (!
|
|
7815
|
+
} else if (!fs38.existsSync(taskMdPath) && input.issueNumber) {
|
|
6583
7816
|
logger.info(`Fetching issue #${input.issueNumber} body as task...`);
|
|
6584
7817
|
const issue = getIssue(input.issueNumber);
|
|
6585
7818
|
if (issue) {
|
|
6586
7819
|
const taskContent = `# ${issue.title}
|
|
6587
7820
|
|
|
6588
7821
|
${issue.body ?? ""}`;
|
|
6589
|
-
|
|
7822
|
+
fs38.writeFileSync(taskMdPath, taskContent);
|
|
6590
7823
|
logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
|
|
6591
7824
|
}
|
|
6592
7825
|
}
|
|
6593
|
-
if (!
|
|
7826
|
+
if (!fs38.existsSync(taskMdPath)) {
|
|
6594
7827
|
console.error("No task.md found. Provide --task, --issue-number, or ensure .kody/tasks/<id>/task.md exists.");
|
|
6595
7828
|
process.exit(1);
|
|
6596
7829
|
}
|
|
@@ -6734,7 +7967,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
|
|
|
6734
7967
|
}
|
|
6735
7968
|
}
|
|
6736
7969
|
const state = await runPipeline(ctx);
|
|
6737
|
-
const files =
|
|
7970
|
+
const files = fs38.readdirSync(taskDir);
|
|
6738
7971
|
console.log(`
|
|
6739
7972
|
Artifacts in ${taskDir}:`);
|
|
6740
7973
|
for (const f of files) {
|
|
@@ -6800,8 +8033,8 @@ var init_entry = __esm({
|
|
|
6800
8033
|
});
|
|
6801
8034
|
|
|
6802
8035
|
// src/bin/cli.ts
|
|
6803
|
-
import * as
|
|
6804
|
-
import * as
|
|
8036
|
+
import * as fs39 from "fs";
|
|
8037
|
+
import * as path36 from "path";
|
|
6805
8038
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6806
8039
|
|
|
6807
8040
|
// src/bin/commands/init.ts
|
|
@@ -7103,9 +8336,37 @@ function initCommand(opts, pkgRoot) {
|
|
|
7103
8336
|
console.log(" \u2717 kody.config.json \u2014 invalid JSON");
|
|
7104
8337
|
}
|
|
7105
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
|
+
}
|
|
7106
8366
|
console.log("\n\u2500\u2500 Git \u2500\u2500");
|
|
7107
8367
|
const filesToCommit = [
|
|
7108
8368
|
".github/workflows/kody.yml",
|
|
8369
|
+
".github/workflows/kody-watch.yml",
|
|
7109
8370
|
"kody.config.json"
|
|
7110
8371
|
].filter((f) => fs3.existsSync(path2.join(cwd, f)));
|
|
7111
8372
|
if (filesToCommit.length > 0) {
|
|
@@ -7841,8 +9102,8 @@ function detectProjectKeywords(cwd) {
|
|
|
7841
9102
|
}
|
|
7842
9103
|
}
|
|
7843
9104
|
function searchSkills(keywords, exclude, limit) {
|
|
7844
|
-
const allResults = [];
|
|
7845
9105
|
const seen = /* @__PURE__ */ new Set();
|
|
9106
|
+
const perKeyword = [];
|
|
7846
9107
|
for (const keyword of keywords) {
|
|
7847
9108
|
try {
|
|
7848
9109
|
const output = execFileSync4("npx", ["skills", "find", keyword], {
|
|
@@ -7850,16 +9111,34 @@ function searchSkills(keywords, exclude, limit) {
|
|
|
7850
9111
|
timeout: 15e3,
|
|
7851
9112
|
stdio: ["pipe", "pipe", "pipe"]
|
|
7852
9113
|
});
|
|
9114
|
+
const results = [];
|
|
7853
9115
|
for (const skill of parseSkillsSearchOutput(output)) {
|
|
7854
|
-
if (!
|
|
7855
|
-
|
|
7856
|
-
allResults.push(skill);
|
|
9116
|
+
if (!exclude.has(skill.name)) {
|
|
9117
|
+
results.push(skill);
|
|
7857
9118
|
}
|
|
7858
9119
|
}
|
|
9120
|
+
perKeyword.push(results);
|
|
7859
9121
|
} catch {
|
|
7860
9122
|
}
|
|
7861
9123
|
}
|
|
7862
|
-
|
|
9124
|
+
const selected = [];
|
|
9125
|
+
let round = 0;
|
|
9126
|
+
while (selected.length < limit) {
|
|
9127
|
+
let added = false;
|
|
9128
|
+
for (const results of perKeyword) {
|
|
9129
|
+
if (selected.length >= limit) break;
|
|
9130
|
+
if (round >= results.length) continue;
|
|
9131
|
+
const candidate = results[round];
|
|
9132
|
+
if (!seen.has(candidate.ref)) {
|
|
9133
|
+
seen.add(candidate.ref);
|
|
9134
|
+
selected.push(candidate);
|
|
9135
|
+
added = true;
|
|
9136
|
+
}
|
|
9137
|
+
}
|
|
9138
|
+
if (!added) break;
|
|
9139
|
+
round++;
|
|
9140
|
+
}
|
|
9141
|
+
return selected;
|
|
7863
9142
|
}
|
|
7864
9143
|
function collectSkillPaths(cwd, skillName, paths) {
|
|
7865
9144
|
for (const dir of [".claude/skills", ".agents/skills"]) {
|
|
@@ -8215,6 +9494,87 @@ Command and URL.
|
|
|
8215
9494
|
} else {
|
|
8216
9495
|
console.log(" \u25CB No routes or collections detected \u2014 skipping QA guide");
|
|
8217
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
|
+
}
|
|
8218
9578
|
console.log("\n\u2500\u2500 Labels \u2500\u2500");
|
|
8219
9579
|
try {
|
|
8220
9580
|
let repoSlug = "";
|
|
@@ -8395,6 +9755,7 @@ ${entries}
|
|
|
8395
9755
|
".kody/memory/conventions.md",
|
|
8396
9756
|
".kody/qa-guide.md",
|
|
8397
9757
|
".kody/tools.yml",
|
|
9758
|
+
"kody.config.json",
|
|
8398
9759
|
...installedSkillPaths
|
|
8399
9760
|
].filter((f) => fs8.existsSync(path7.join(cwd, f)));
|
|
8400
9761
|
for (const stage of STEP_STAGES) {
|
|
@@ -8508,11 +9869,11 @@ Create it manually.`, cwd);
|
|
|
8508
9869
|
|
|
8509
9870
|
// src/bin/cli.ts
|
|
8510
9871
|
init_architecture_detection();
|
|
8511
|
-
var __dirname2 =
|
|
8512
|
-
var PKG_ROOT =
|
|
9872
|
+
var __dirname2 = path36.dirname(fileURLToPath2(import.meta.url));
|
|
9873
|
+
var PKG_ROOT = path36.resolve(__dirname2, "..", "..");
|
|
8513
9874
|
function getVersion() {
|
|
8514
|
-
const pkgPath =
|
|
8515
|
-
const pkg = JSON.parse(
|
|
9875
|
+
const pkgPath = path36.join(PKG_ROOT, "package.json");
|
|
9876
|
+
const pkg = JSON.parse(fs39.readFileSync(pkgPath, "utf-8"));
|
|
8516
9877
|
return pkg.version;
|
|
8517
9878
|
}
|
|
8518
9879
|
var args = process.argv.slice(2);
|
|
@@ -8527,6 +9888,10 @@ if (command === "init") {
|
|
|
8527
9888
|
Promise.resolve().then(() => (init_test_model_command(), test_model_command_exports)).then(({ runTestModelCommand: runTestModelCommand2 }) => runTestModelCommand2());
|
|
8528
9889
|
} else if (command === "ci-parse") {
|
|
8529
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
|
+
);
|
|
8530
9895
|
} else if (command === "version" || command === "--version" || command === "-v") {
|
|
8531
9896
|
console.log(getVersion());
|
|
8532
9897
|
} else {
|