@kody-ade/kody-engine-lite 0.1.136 → 0.1.137

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