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

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