@kody-ade/kody-engine 0.4.59 → 0.4.60

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/kody.js CHANGED
@@ -303,7 +303,7 @@ var init_verifyMcp = __esm({
303
303
  // package.json
304
304
  var package_default = {
305
305
  name: "@kody-ade/kody-engine",
306
- version: "0.4.59",
306
+ version: "0.4.60",
307
307
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
308
308
  license: "MIT",
309
309
  type: "module",
@@ -353,8 +353,8 @@ var package_default = {
353
353
 
354
354
  // src/chat-cli.ts
355
355
  import { execFileSync as execFileSync31 } from "child_process";
356
- import * as fs32 from "fs";
357
- import * as path30 from "path";
356
+ import * as fs33 from "fs";
357
+ import * as path31 from "path";
358
358
 
359
359
  // src/chat/events.ts
360
360
  import * as fs from "fs";
@@ -1326,8 +1326,8 @@ async function emit2(sink, type, sessionId, suffix, payload) {
1326
1326
 
1327
1327
  // src/kody-cli.ts
1328
1328
  import { execFileSync as execFileSync30 } from "child_process";
1329
- import * as fs31 from "fs";
1330
- import * as path29 from "path";
1329
+ import * as fs32 from "fs";
1330
+ import * as path30 from "path";
1331
1331
 
1332
1332
  // src/dispatch.ts
1333
1333
  import * as fs8 from "fs";
@@ -1906,8 +1906,8 @@ function postPrReviewComment(prNumber, body, cwd) {
1906
1906
 
1907
1907
  // src/executor.ts
1908
1908
  import { execFileSync as execFileSync29, spawn as spawn5 } from "child_process";
1909
- import * as fs30 from "fs";
1910
- import * as path28 from "path";
1909
+ import * as fs31 from "fs";
1910
+ import * as path29 from "path";
1911
1911
  init_events();
1912
1912
 
1913
1913
  // src/profile.ts
@@ -1972,6 +1972,10 @@ function loadProfile(profilePath) {
1972
1972
  inputArtifacts: parseInputArtifacts(profilePath, r.input),
1973
1973
  outputArtifacts: parseOutputArtifacts(profilePath, r.output),
1974
1974
  children,
1975
+ // Default true: preserves legacy bug-safe behaviour where each
1976
+ // container child sees a clean tracked tree (see executor.ts).
1977
+ // Containers opt out by setting `"resetBetweenChildren": false`.
1978
+ resetBetweenChildren: typeof r.resetBetweenChildren === "boolean" ? r.resetBetweenChildren : true,
1975
1979
  dir: path8.dirname(profilePath)
1976
1980
  };
1977
1981
  return profile;
@@ -3230,13 +3234,36 @@ function defaultLabelMap() {
3230
3234
  }
3231
3235
 
3232
3236
  // src/scripts/commitAndPush.ts
3237
+ import * as fs14 from "fs";
3238
+ import * as path13 from "path";
3239
+ init_events();
3233
3240
  var DEFAULT_COMMIT_MESSAGE = "chore: kody changes";
3234
- var commitAndPush2 = async (ctx) => {
3241
+ function sentinelPathForStage(cwd, profileName) {
3242
+ const runId = resolveRunId();
3243
+ return path13.join(cwd, ".kody", "runs", runId, `commit-${profileName}.lock`);
3244
+ }
3245
+ var commitAndPush2 = async (ctx, profile) => {
3235
3246
  const branch = ctx.data.branch;
3236
3247
  if (!branch) {
3237
3248
  ctx.data.commitResult = { committed: false, pushed: false };
3238
3249
  return;
3239
3250
  }
3251
+ const idempotencyEnabled = process.env.KODY_COMMIT_IDEMPOTENCY !== "0";
3252
+ const sentinel = idempotencyEnabled ? sentinelPathForStage(ctx.cwd, profile.name) : null;
3253
+ if (sentinel && fs14.existsSync(sentinel)) {
3254
+ try {
3255
+ const replay = JSON.parse(fs14.readFileSync(sentinel, "utf-8"));
3256
+ ctx.data.commitResult = replay.commitResult ?? { committed: false, pushed: false };
3257
+ if (Array.isArray(replay.changedFiles)) ctx.data.changedFiles = replay.changedFiles;
3258
+ if (typeof replay.hasCommitsAhead === "boolean") ctx.data.hasCommitsAhead = replay.hasCommitsAhead;
3259
+ if (replay.salvagedFromMissingMarker) ctx.data.salvagedFromMissingMarker = true;
3260
+ ctx.data.commitIdempotencyReplay = true;
3261
+ process.stderr.write(`[kody commitAndPush] idempotency replay (sentinel ${sentinel})
3262
+ `);
3263
+ return;
3264
+ } catch {
3265
+ }
3266
+ }
3240
3267
  const markerMissing = ctx.data.agentMarkerMissing === true;
3241
3268
  if (ctx.data.agentDone === false && !markerMissing) {
3242
3269
  ctx.data.commitResult = { committed: false, pushed: false, skippedReason: "agentDone=false" };
@@ -3248,12 +3275,12 @@ var commitAndPush2 = async (ctx) => {
3248
3275
  }
3249
3276
  const message = ctx.data.commitMessage || DEFAULT_COMMIT_MESSAGE;
3250
3277
  try {
3251
- const result = commitAndPush(branch, message, ctx.cwd);
3252
- ctx.data.commitResult = result;
3253
- const postCommitFiles = result.committed ? listFilesInCommit("HEAD", ctx.cwd) : listChangedFiles(ctx.cwd);
3278
+ const result2 = commitAndPush(branch, message, ctx.cwd);
3279
+ ctx.data.commitResult = result2;
3280
+ const postCommitFiles = result2.committed ? listFilesInCommit("HEAD", ctx.cwd) : listChangedFiles(ctx.cwd);
3254
3281
  ctx.data.changedFiles = postCommitFiles.filter((f) => !isForbiddenPath(f));
3255
- if (result.committed && !result.pushed) {
3256
- const reason = result.pushError ?? "push failed (no error detail)";
3282
+ if (result2.committed && !result2.pushed) {
3283
+ const reason = result2.pushError ?? "push failed (no error detail)";
3257
3284
  ctx.data.commitCrash = reason;
3258
3285
  if (ctx.output.exitCode === void 0 || ctx.output.exitCode === 0) {
3259
3286
  ctx.output.exitCode = 4;
@@ -3270,15 +3297,36 @@ var commitAndPush2 = async (ctx) => {
3270
3297
  `);
3271
3298
  }
3272
3299
  ctx.data.hasCommitsAhead = hasCommitsAhead(branch, ctx.config.git.defaultBranch, ctx.cwd);
3300
+ const result = ctx.data.commitResult;
3301
+ if (sentinel && result?.committed) {
3302
+ try {
3303
+ fs14.mkdirSync(path13.dirname(sentinel), { recursive: true });
3304
+ fs14.writeFileSync(
3305
+ sentinel,
3306
+ JSON.stringify(
3307
+ {
3308
+ commitResult: ctx.data.commitResult,
3309
+ changedFiles: ctx.data.changedFiles,
3310
+ hasCommitsAhead: ctx.data.hasCommitsAhead,
3311
+ salvagedFromMissingMarker: ctx.data.salvagedFromMissingMarker === true,
3312
+ writtenAt: (/* @__PURE__ */ new Date()).toISOString()
3313
+ },
3314
+ null,
3315
+ 2
3316
+ )
3317
+ );
3318
+ } catch {
3319
+ }
3320
+ }
3273
3321
  };
3274
3322
 
3275
3323
  // src/scripts/commitGoalState.ts
3276
3324
  import { execFileSync as execFileSync9 } from "child_process";
3277
- import * as path13 from "path";
3325
+ import * as path14 from "path";
3278
3326
  var commitGoalState = async (ctx) => {
3279
3327
  const goal = ctx.data.goal;
3280
3328
  if (!goal) return;
3281
- const stateRel = path13.posix.join(".kody", "goals", goal.id, "state.json");
3329
+ const stateRel = path14.posix.join(".kody", "goals", goal.id, "state.json");
3282
3330
  try {
3283
3331
  execFileSync9("git", ["add", stateRel], { cwd: ctx.cwd, stdio: "pipe" });
3284
3332
  } catch (err) {
@@ -3322,20 +3370,20 @@ function describeCommitMessage(goal) {
3322
3370
  }
3323
3371
 
3324
3372
  // src/scripts/composePrompt.ts
3325
- import * as fs14 from "fs";
3326
- import * as path14 from "path";
3373
+ import * as fs15 from "fs";
3374
+ import * as path15 from "path";
3327
3375
  var MUSTACHE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
3328
3376
  var composePrompt = async (ctx, profile) => {
3329
3377
  const explicit = ctx.data.promptTemplate;
3330
3378
  const mode = ctx.args.mode;
3331
3379
  const candidates = [
3332
- explicit ? path14.join(profile.dir, explicit) : null,
3333
- mode ? path14.join(profile.dir, "prompts", `${mode}.md`) : null,
3334
- path14.join(profile.dir, "prompt.md")
3380
+ explicit ? path15.join(profile.dir, explicit) : null,
3381
+ mode ? path15.join(profile.dir, "prompts", `${mode}.md`) : null,
3382
+ path15.join(profile.dir, "prompt.md")
3335
3383
  ].filter(Boolean);
3336
3384
  let templatePath = "";
3337
3385
  for (const c of candidates) {
3338
- if (fs14.existsSync(c)) {
3386
+ if (fs15.existsSync(c)) {
3339
3387
  templatePath = c;
3340
3388
  break;
3341
3389
  }
@@ -3343,7 +3391,7 @@ var composePrompt = async (ctx, profile) => {
3343
3391
  if (!templatePath) {
3344
3392
  throw new Error(`profile at ${profile.dir}: no prompt template found (tried ${candidates.join(", ")})`);
3345
3393
  }
3346
- const template = fs14.readFileSync(templatePath, "utf-8");
3394
+ const template = fs15.readFileSync(templatePath, "utf-8");
3347
3395
  const tokens = {
3348
3396
  ...stringifyAll(ctx.args, "args."),
3349
3397
  ...stringifyAll(ctx.data, ""),
@@ -3421,8 +3469,8 @@ function formatToolsUsage(profile) {
3421
3469
 
3422
3470
  // src/scripts/createQaGoal.ts
3423
3471
  import { execFileSync as execFileSync10 } from "child_process";
3424
- import * as fs15 from "fs";
3425
- import * as path15 from "path";
3472
+ import * as fs16 from "fs";
3473
+ import * as path16 from "path";
3426
3474
 
3427
3475
  // src/scripts/postReviewResult.ts
3428
3476
  function detectVerdict(body) {
@@ -3674,8 +3722,8 @@ function createOrUpdateManifestIssue(number, manifest, cwd) {
3674
3722
  return { number: Number(m[1]), created: true };
3675
3723
  }
3676
3724
  function writeStateFile(cwd, goalId, lastDispatchedIssue) {
3677
- const dir = path15.join(cwd, ".kody", "goals", goalId);
3678
- fs15.mkdirSync(dir, { recursive: true });
3725
+ const dir = path16.join(cwd, ".kody", "goals", goalId);
3726
+ fs16.mkdirSync(dir, { recursive: true });
3679
3727
  const state = {
3680
3728
  version: 1,
3681
3729
  state: "active",
@@ -3683,8 +3731,8 @@ function writeStateFile(cwd, goalId, lastDispatchedIssue) {
3683
3731
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3684
3732
  ...typeof lastDispatchedIssue === "number" ? { lastDispatchedIssue } : {}
3685
3733
  };
3686
- const filePath = path15.join(dir, "state.json");
3687
- fs15.writeFileSync(filePath, `${JSON.stringify(state, null, 2)}
3734
+ const filePath = path16.join(dir, "state.json");
3735
+ fs16.writeFileSync(filePath, `${JSON.stringify(state, null, 2)}
3688
3736
  `);
3689
3737
  return filePath;
3690
3738
  }
@@ -4089,7 +4137,10 @@ function mergePrSquash(prNumber, cwd) {
4089
4137
  }
4090
4138
  function editPrBase(prNumber, baseBranch, cwd) {
4091
4139
  try {
4092
- gh(["pr", "edit", String(prNumber), "--base", baseBranch], { cwd });
4140
+ gh(
4141
+ ["api", "--method", "PATCH", `repos/{owner}/{repo}/pulls/${prNumber}`, "-f", `base=${baseBranch}`],
4142
+ { cwd }
4143
+ );
4093
4144
  return { ok: true };
4094
4145
  } catch (err) {
4095
4146
  return fail(err);
@@ -4174,15 +4225,15 @@ function filterGoalTaskPrs(prs, taskIssueNumbers) {
4174
4225
 
4175
4226
  // src/scripts/diagMcp.ts
4176
4227
  import { execFileSync as execFileSync11 } from "child_process";
4177
- import * as fs16 from "fs";
4228
+ import * as fs17 from "fs";
4178
4229
  import * as os3 from "os";
4179
- import * as path16 from "path";
4230
+ import * as path17 from "path";
4180
4231
  var diagMcp = async (_ctx) => {
4181
4232
  const home = os3.homedir();
4182
- const cacheDir = path16.join(home, ".cache", "ms-playwright");
4233
+ const cacheDir = path17.join(home, ".cache", "ms-playwright");
4183
4234
  let entries = [];
4184
4235
  try {
4185
- entries = fs16.readdirSync(cacheDir);
4236
+ entries = fs17.readdirSync(cacheDir);
4186
4237
  } catch {
4187
4238
  }
4188
4239
  const hasChromium = entries.some((e) => e.startsWith("chromium"));
@@ -4208,17 +4259,17 @@ var diagMcp = async (_ctx) => {
4208
4259
  };
4209
4260
 
4210
4261
  // src/scripts/discoverQaContext.ts
4211
- import * as fs18 from "fs";
4212
- import * as path18 from "path";
4262
+ import * as fs19 from "fs";
4263
+ import * as path19 from "path";
4213
4264
 
4214
4265
  // src/scripts/frameworkDetectors.ts
4215
- import * as fs17 from "fs";
4216
- import * as path17 from "path";
4266
+ import * as fs18 from "fs";
4267
+ import * as path18 from "path";
4217
4268
  function detectFrameworks(cwd) {
4218
4269
  const out = [];
4219
4270
  let deps = {};
4220
4271
  try {
4221
- const pkg = JSON.parse(fs17.readFileSync(path17.join(cwd, "package.json"), "utf-8"));
4272
+ const pkg = JSON.parse(fs18.readFileSync(path18.join(cwd, "package.json"), "utf-8"));
4222
4273
  deps = { ...pkg.dependencies, ...pkg.devDependencies };
4223
4274
  } catch {
4224
4275
  return out;
@@ -4255,7 +4306,7 @@ function detectFrameworks(cwd) {
4255
4306
  }
4256
4307
  function findFile(cwd, candidates) {
4257
4308
  for (const c of candidates) {
4258
- if (fs17.existsSync(path17.join(cwd, c))) return c;
4309
+ if (fs18.existsSync(path18.join(cwd, c))) return c;
4259
4310
  }
4260
4311
  return null;
4261
4312
  }
@@ -4268,18 +4319,18 @@ var COLLECTION_DIRS = [
4268
4319
  function discoverPayloadCollections(cwd) {
4269
4320
  const out = [];
4270
4321
  for (const dir of COLLECTION_DIRS) {
4271
- const full = path17.join(cwd, dir);
4272
- if (!fs17.existsSync(full)) continue;
4322
+ const full = path18.join(cwd, dir);
4323
+ if (!fs18.existsSync(full)) continue;
4273
4324
  let files;
4274
4325
  try {
4275
- files = fs17.readdirSync(full).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
4326
+ files = fs18.readdirSync(full).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
4276
4327
  } catch {
4277
4328
  continue;
4278
4329
  }
4279
4330
  for (const file of files) {
4280
4331
  try {
4281
- const filePath = path17.join(full, file);
4282
- const content = fs17.readFileSync(filePath, "utf-8").slice(0, 1e4);
4332
+ const filePath = path18.join(full, file);
4333
+ const content = fs18.readFileSync(filePath, "utf-8").slice(0, 1e4);
4283
4334
  const slugMatch = content.match(/slug:\s*['"]([a-z0-9-]+)['"]/);
4284
4335
  if (!slugMatch) continue;
4285
4336
  const slug = slugMatch[1];
@@ -4293,7 +4344,7 @@ function discoverPayloadCollections(cwd) {
4293
4344
  out.push({
4294
4345
  name,
4295
4346
  slug,
4296
- filePath: path17.relative(cwd, filePath),
4347
+ filePath: path18.relative(cwd, filePath),
4297
4348
  fields: fields.slice(0, 20),
4298
4349
  hasAdmin
4299
4350
  });
@@ -4307,28 +4358,28 @@ var ADMIN_COMPONENT_DIRS = ["src/ui/admin", "src/admin/components", "src/compone
4307
4358
  function discoverAdminComponents(cwd, collections) {
4308
4359
  const out = [];
4309
4360
  for (const dir of ADMIN_COMPONENT_DIRS) {
4310
- const full = path17.join(cwd, dir);
4311
- if (!fs17.existsSync(full)) continue;
4361
+ const full = path18.join(cwd, dir);
4362
+ if (!fs18.existsSync(full)) continue;
4312
4363
  let entries;
4313
4364
  try {
4314
- entries = fs17.readdirSync(full, { withFileTypes: true });
4365
+ entries = fs18.readdirSync(full, { withFileTypes: true });
4315
4366
  } catch {
4316
4367
  continue;
4317
4368
  }
4318
4369
  for (const entry of entries) {
4319
- const entryPath = path17.join(full, entry.name);
4370
+ const entryPath = path18.join(full, entry.name);
4320
4371
  let name;
4321
4372
  let filePath;
4322
4373
  if (entry.isDirectory()) {
4323
4374
  const indexFile = ["index.tsx", "index.ts", "index.jsx", "index.js"].find(
4324
- (f) => fs17.existsSync(path17.join(entryPath, f))
4375
+ (f) => fs18.existsSync(path18.join(entryPath, f))
4325
4376
  );
4326
4377
  if (!indexFile) continue;
4327
4378
  name = entry.name;
4328
- filePath = path17.relative(cwd, path17.join(entryPath, indexFile));
4379
+ filePath = path18.relative(cwd, path18.join(entryPath, indexFile));
4329
4380
  } else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
4330
4381
  name = entry.name.replace(/\.(tsx?|jsx?)$/, "");
4331
- filePath = path17.relative(cwd, entryPath);
4382
+ filePath = path18.relative(cwd, entryPath);
4332
4383
  } else {
4333
4384
  continue;
4334
4385
  }
@@ -4336,7 +4387,7 @@ function discoverAdminComponents(cwd, collections) {
4336
4387
  if (collections) {
4337
4388
  for (const col of collections) {
4338
4389
  try {
4339
- const colContent = fs17.readFileSync(path17.join(cwd, col.filePath), "utf-8");
4390
+ const colContent = fs18.readFileSync(path18.join(cwd, col.filePath), "utf-8");
4340
4391
  if (colContent.includes(name)) {
4341
4392
  usedInCollection = col.slug;
4342
4393
  break;
@@ -4355,8 +4406,8 @@ function scanApiRoutes(cwd) {
4355
4406
  const out = [];
4356
4407
  const appDirs = ["src/app", "app"];
4357
4408
  for (const appDir of appDirs) {
4358
- const apiDir = path17.join(cwd, appDir, "api");
4359
- if (!fs17.existsSync(apiDir)) continue;
4409
+ const apiDir = path18.join(cwd, appDir, "api");
4410
+ if (!fs18.existsSync(apiDir)) continue;
4360
4411
  walkApiRoutes(apiDir, "/api", cwd, out);
4361
4412
  break;
4362
4413
  }
@@ -4365,14 +4416,14 @@ function scanApiRoutes(cwd) {
4365
4416
  function walkApiRoutes(dir, prefix, cwd, out) {
4366
4417
  let entries;
4367
4418
  try {
4368
- entries = fs17.readdirSync(dir, { withFileTypes: true });
4419
+ entries = fs18.readdirSync(dir, { withFileTypes: true });
4369
4420
  } catch {
4370
4421
  return;
4371
4422
  }
4372
4423
  const routeFile = entries.find((e) => e.isFile() && /^route\.(ts|js|tsx|jsx)$/.test(e.name));
4373
4424
  if (routeFile) {
4374
4425
  try {
4375
- const content = fs17.readFileSync(path17.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
4426
+ const content = fs18.readFileSync(path18.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
4376
4427
  const methods = HTTP_METHODS.filter(
4377
4428
  (m) => new RegExp(`export\\s+(?:async\\s+)?function\\s+${m}\\b`).test(content)
4378
4429
  );
@@ -4380,7 +4431,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
4380
4431
  out.push({
4381
4432
  path: prefix,
4382
4433
  methods,
4383
- filePath: path17.relative(cwd, path17.join(dir, routeFile.name))
4434
+ filePath: path18.relative(cwd, path18.join(dir, routeFile.name))
4384
4435
  });
4385
4436
  }
4386
4437
  } catch {
@@ -4391,7 +4442,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
4391
4442
  if (entry.name === "node_modules" || entry.name === ".next") continue;
4392
4443
  let segment = entry.name;
4393
4444
  if (segment.startsWith("(") && segment.endsWith(")")) {
4394
- walkApiRoutes(path17.join(dir, entry.name), prefix, cwd, out);
4445
+ walkApiRoutes(path18.join(dir, entry.name), prefix, cwd, out);
4395
4446
  continue;
4396
4447
  }
4397
4448
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -4399,7 +4450,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
4399
4450
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
4400
4451
  segment = `:${segment.slice(1, -1)}`;
4401
4452
  }
4402
- walkApiRoutes(path17.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
4453
+ walkApiRoutes(path18.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
4403
4454
  }
4404
4455
  }
4405
4456
  var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
@@ -4419,10 +4470,10 @@ var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
4419
4470
  function scanEnvVars(cwd) {
4420
4471
  const candidates = [".env.example", ".env.local.example", ".env.template"];
4421
4472
  for (const envFile of candidates) {
4422
- const envPath = path17.join(cwd, envFile);
4423
- if (!fs17.existsSync(envPath)) continue;
4473
+ const envPath = path18.join(cwd, envFile);
4474
+ if (!fs18.existsSync(envPath)) continue;
4424
4475
  try {
4425
- const content = fs17.readFileSync(envPath, "utf-8");
4476
+ const content = fs18.readFileSync(envPath, "utf-8");
4426
4477
  const vars = [];
4427
4478
  for (const line of content.split("\n")) {
4428
4479
  const trimmed = line.trim();
@@ -4470,9 +4521,9 @@ function runQaDiscovery(cwd) {
4470
4521
  }
4471
4522
  function detectDevServer(cwd, out) {
4472
4523
  try {
4473
- const pkg = JSON.parse(fs18.readFileSync(path18.join(cwd, "package.json"), "utf-8"));
4524
+ const pkg = JSON.parse(fs19.readFileSync(path19.join(cwd, "package.json"), "utf-8"));
4474
4525
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
4475
- const pm = fs18.existsSync(path18.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs18.existsSync(path18.join(cwd, "yarn.lock")) ? "yarn" : fs18.existsSync(path18.join(cwd, "bun.lockb")) ? "bun" : "npm";
4526
+ const pm = fs19.existsSync(path19.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs19.existsSync(path19.join(cwd, "yarn.lock")) ? "yarn" : fs19.existsSync(path19.join(cwd, "bun.lockb")) ? "bun" : "npm";
4476
4527
  if (pkg.scripts?.dev) out.devCommand = `${pm} dev`;
4477
4528
  if (allDeps.next || allDeps.nuxt) out.devPort = 3e3;
4478
4529
  else if (allDeps.vite) out.devPort = 5173;
@@ -4482,8 +4533,8 @@ function detectDevServer(cwd, out) {
4482
4533
  function scanFrontendRoutes(cwd, out) {
4483
4534
  const appDirs = ["src/app", "app"];
4484
4535
  for (const appDir of appDirs) {
4485
- const full = path18.join(cwd, appDir);
4486
- if (!fs18.existsSync(full)) continue;
4536
+ const full = path19.join(cwd, appDir);
4537
+ if (!fs19.existsSync(full)) continue;
4487
4538
  walkFrontendRoutes(full, "", out);
4488
4539
  break;
4489
4540
  }
@@ -4491,7 +4542,7 @@ function scanFrontendRoutes(cwd, out) {
4491
4542
  function walkFrontendRoutes(dir, prefix, out) {
4492
4543
  let entries;
4493
4544
  try {
4494
- entries = fs18.readdirSync(dir, { withFileTypes: true });
4545
+ entries = fs19.readdirSync(dir, { withFileTypes: true });
4495
4546
  } catch {
4496
4547
  return;
4497
4548
  }
@@ -4508,7 +4559,7 @@ function walkFrontendRoutes(dir, prefix, out) {
4508
4559
  if (entry.name === "node_modules" || entry.name === ".next") continue;
4509
4560
  let segment = entry.name;
4510
4561
  if (segment.startsWith("(") && segment.endsWith(")")) {
4511
- walkFrontendRoutes(path18.join(dir, entry.name), prefix, out);
4562
+ walkFrontendRoutes(path19.join(dir, entry.name), prefix, out);
4512
4563
  continue;
4513
4564
  }
4514
4565
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -4516,7 +4567,7 @@ function walkFrontendRoutes(dir, prefix, out) {
4516
4567
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
4517
4568
  segment = `:${segment.slice(1, -1)}`;
4518
4569
  }
4519
- walkFrontendRoutes(path18.join(dir, entry.name), `${prefix}/${segment}`, out);
4570
+ walkFrontendRoutes(path19.join(dir, entry.name), `${prefix}/${segment}`, out);
4520
4571
  }
4521
4572
  }
4522
4573
  function detectAuthFiles(cwd, out) {
@@ -4533,23 +4584,23 @@ function detectAuthFiles(cwd, out) {
4533
4584
  "src/app/api/oauth"
4534
4585
  ];
4535
4586
  for (const c of candidates) {
4536
- if (fs18.existsSync(path18.join(cwd, c))) out.authFiles.push(c);
4587
+ if (fs19.existsSync(path19.join(cwd, c))) out.authFiles.push(c);
4537
4588
  }
4538
4589
  }
4539
4590
  function detectRoles(cwd, out) {
4540
4591
  const rolePaths = ["src/types", "src/lib", "src/utils", "src/constants", "src/access", "src/collections"];
4541
4592
  for (const rp of rolePaths) {
4542
- const dir = path18.join(cwd, rp);
4543
- if (!fs18.existsSync(dir)) continue;
4593
+ const dir = path19.join(cwd, rp);
4594
+ if (!fs19.existsSync(dir)) continue;
4544
4595
  let files;
4545
4596
  try {
4546
- files = fs18.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
4597
+ files = fs19.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
4547
4598
  } catch {
4548
4599
  continue;
4549
4600
  }
4550
4601
  for (const f of files) {
4551
4602
  try {
4552
- const content = fs18.readFileSync(path18.join(dir, f), "utf-8").slice(0, 5e3);
4603
+ const content = fs19.readFileSync(path19.join(dir, f), "utf-8").slice(0, 5e3);
4553
4604
  const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
4554
4605
  if (roleMatches) {
4555
4606
  for (const m of roleMatches) {
@@ -4782,8 +4833,8 @@ function failedAction3(reason) {
4782
4833
  }
4783
4834
 
4784
4835
  // src/scripts/dispatchJobFileTicks.ts
4785
- import * as fs20 from "fs";
4786
- import * as path20 from "path";
4836
+ import * as fs21 from "fs";
4837
+ import * as path21 from "path";
4787
4838
 
4788
4839
  // src/scripts/jobFrontmatter.ts
4789
4840
  var SCHEDULE_EVERY_VALUES = [
@@ -5034,8 +5085,8 @@ var ContentsApiBackend = class {
5034
5085
  };
5035
5086
 
5036
5087
  // src/scripts/jobState/localFileBackend.ts
5037
- import * as fs19 from "fs";
5038
- import * as path19 from "path";
5088
+ import * as fs20 from "fs";
5089
+ import * as path20 from "path";
5039
5090
  var LocalFileBackend = class {
5040
5091
  name = "local-file";
5041
5092
  cwd;
@@ -5050,7 +5101,7 @@ var LocalFileBackend = class {
5050
5101
  if (!opts.owner || !opts.repo) throw new Error("LocalFileBackend: owner and repo are required");
5051
5102
  this.cwd = opts.cwd;
5052
5103
  this.jobsDir = opts.jobsDir;
5053
- this.absDir = path19.join(opts.cwd, opts.jobsDir);
5104
+ this.absDir = path20.join(opts.cwd, opts.jobsDir);
5054
5105
  this.owner = opts.owner;
5055
5106
  this.repo = opts.repo;
5056
5107
  this.cache = opts.cache ?? defaultCacheAdapter();
@@ -5065,7 +5116,7 @@ var LocalFileBackend = class {
5065
5116
  `);
5066
5117
  return;
5067
5118
  }
5068
- fs19.mkdirSync(this.absDir, { recursive: true });
5119
+ fs20.mkdirSync(this.absDir, { recursive: true });
5069
5120
  const prefix = this.cacheKeyPrefix();
5070
5121
  const probeKey = `${prefix}probe-${Date.now()}`;
5071
5122
  try {
@@ -5094,7 +5145,7 @@ var LocalFileBackend = class {
5094
5145
  `);
5095
5146
  return;
5096
5147
  }
5097
- if (!fs19.existsSync(this.absDir)) {
5148
+ if (!fs20.existsSync(this.absDir)) {
5098
5149
  return;
5099
5150
  }
5100
5151
  const key = `${this.cacheKeyPrefix()}${process.env.GITHUB_RUN_ID ?? "norunid"}-${Date.now()}`;
@@ -5110,11 +5161,11 @@ var LocalFileBackend = class {
5110
5161
  }
5111
5162
  load(slug) {
5112
5163
  const relPath = stateFilePath(this.jobsDir, slug);
5113
- const absPath = path19.join(this.cwd, relPath);
5114
- if (!fs19.existsSync(absPath)) {
5164
+ const absPath = path20.join(this.cwd, relPath);
5165
+ if (!fs20.existsSync(absPath)) {
5115
5166
  return { path: relPath, handle: null, state: initialStateEnvelope("seed"), created: true };
5116
5167
  }
5117
- const raw = fs19.readFileSync(absPath, "utf-8");
5168
+ const raw = fs20.readFileSync(absPath, "utf-8");
5118
5169
  let parsed;
5119
5170
  try {
5120
5171
  parsed = JSON.parse(raw);
@@ -5131,10 +5182,10 @@ var LocalFileBackend = class {
5131
5182
  if (!loaded.created && isStateUnchanged(loaded.state, next)) {
5132
5183
  return false;
5133
5184
  }
5134
- const absPath = path19.join(this.cwd, loaded.path);
5135
- fs19.mkdirSync(path19.dirname(absPath), { recursive: true });
5185
+ const absPath = path20.join(this.cwd, loaded.path);
5186
+ fs20.mkdirSync(path20.dirname(absPath), { recursive: true });
5136
5187
  const body = JSON.stringify(next, null, 2) + "\n";
5137
- fs19.writeFileSync(absPath, body, "utf-8");
5188
+ fs20.writeFileSync(absPath, body, "utf-8");
5138
5189
  return true;
5139
5190
  }
5140
5191
  cacheKeyPrefix() {
@@ -5211,7 +5262,7 @@ var dispatchJobFileTicks = async (ctx, _profile, args) => {
5211
5262
  await backend.hydrate();
5212
5263
  }
5213
5264
  try {
5214
- const slugs = listJobSlugs(path20.join(ctx.cwd, jobsDir));
5265
+ const slugs = listJobSlugs(path21.join(ctx.cwd, jobsDir));
5215
5266
  ctx.data.jobSlugCount = slugs.length;
5216
5267
  if (slugs.length === 0) {
5217
5268
  process.stdout.write(`[jobs] no job files in ${jobsDir}
@@ -5310,17 +5361,17 @@ function formatAgo(ms) {
5310
5361
  }
5311
5362
  function readJobFrontmatter(cwd, jobsDir, slug) {
5312
5363
  try {
5313
- const raw = fs20.readFileSync(path20.join(cwd, jobsDir, `${slug}.md`), "utf-8");
5364
+ const raw = fs21.readFileSync(path21.join(cwd, jobsDir, `${slug}.md`), "utf-8");
5314
5365
  return splitFrontmatter(raw).frontmatter;
5315
5366
  } catch {
5316
5367
  return {};
5317
5368
  }
5318
5369
  }
5319
5370
  function listJobSlugs(absDir) {
5320
- if (!fs20.existsSync(absDir)) return [];
5371
+ if (!fs21.existsSync(absDir)) return [];
5321
5372
  let entries;
5322
5373
  try {
5323
- entries = fs20.readdirSync(absDir, { withFileTypes: true });
5374
+ entries = fs21.readdirSync(absDir, { withFileTypes: true });
5324
5375
  } catch {
5325
5376
  return [];
5326
5377
  }
@@ -5965,7 +6016,7 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd, baseBranch)
5965
6016
 
5966
6017
  // src/gha.ts
5967
6018
  import { execFileSync as execFileSync16 } from "child_process";
5968
- import * as fs21 from "fs";
6019
+ import * as fs22 from "fs";
5969
6020
  function getRunUrl() {
5970
6021
  const server = process.env.GITHUB_SERVER_URL;
5971
6022
  const repo = process.env.GITHUB_REPOSITORY;
@@ -5976,10 +6027,10 @@ function getRunUrl() {
5976
6027
  function reactToTriggerComment(cwd) {
5977
6028
  if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
5978
6029
  const eventPath = process.env.GITHUB_EVENT_PATH;
5979
- if (!eventPath || !fs21.existsSync(eventPath)) return;
6030
+ if (!eventPath || !fs22.existsSync(eventPath)) return;
5980
6031
  let event = null;
5981
6032
  try {
5982
- event = JSON.parse(fs21.readFileSync(eventPath, "utf-8"));
6033
+ event = JSON.parse(fs22.readFileSync(eventPath, "utf-8"));
5983
6034
  } catch {
5984
6035
  return;
5985
6036
  }
@@ -6264,22 +6315,22 @@ var handleAbandonedGoal = async (ctx) => {
6264
6315
 
6265
6316
  // src/scripts/initFlow.ts
6266
6317
  import { execFileSync as execFileSync18 } from "child_process";
6267
- import * as fs23 from "fs";
6268
- import * as path22 from "path";
6318
+ import * as fs24 from "fs";
6319
+ import * as path23 from "path";
6269
6320
 
6270
6321
  // src/scripts/loadQaGuide.ts
6271
- import * as fs22 from "fs";
6272
- import * as path21 from "path";
6322
+ import * as fs23 from "fs";
6323
+ import * as path22 from "path";
6273
6324
  var QA_GUIDE_REL_PATH = ".kody/qa-guide.md";
6274
6325
  var loadQaGuide = async (ctx) => {
6275
- const full = path21.join(ctx.cwd, QA_GUIDE_REL_PATH);
6276
- if (!fs22.existsSync(full)) {
6326
+ const full = path22.join(ctx.cwd, QA_GUIDE_REL_PATH);
6327
+ if (!fs23.existsSync(full)) {
6277
6328
  ctx.data.qaGuide = "";
6278
6329
  ctx.data.qaGuidePath = "";
6279
6330
  return;
6280
6331
  }
6281
6332
  try {
6282
- ctx.data.qaGuide = fs22.readFileSync(full, "utf-8");
6333
+ ctx.data.qaGuide = fs23.readFileSync(full, "utf-8");
6283
6334
  ctx.data.qaGuidePath = QA_GUIDE_REL_PATH;
6284
6335
  } catch {
6285
6336
  ctx.data.qaGuide = "";
@@ -6289,9 +6340,9 @@ var loadQaGuide = async (ctx) => {
6289
6340
 
6290
6341
  // src/scripts/initFlow.ts
6291
6342
  function detectPackageManager(cwd) {
6292
- if (fs23.existsSync(path22.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
6293
- if (fs23.existsSync(path22.join(cwd, "yarn.lock"))) return "yarn";
6294
- if (fs23.existsSync(path22.join(cwd, "bun.lockb"))) return "bun";
6343
+ if (fs24.existsSync(path23.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
6344
+ if (fs24.existsSync(path23.join(cwd, "yarn.lock"))) return "yarn";
6345
+ if (fs24.existsSync(path23.join(cwd, "bun.lockb"))) return "bun";
6295
6346
  return "npm";
6296
6347
  }
6297
6348
  function qualityCommandsFor(pm) {
@@ -6413,48 +6464,48 @@ function performInit(cwd, force) {
6413
6464
  const pm = detectPackageManager(cwd);
6414
6465
  const ownerRepo = detectOwnerRepo(cwd);
6415
6466
  const defaultBranch = defaultBranchFromGit(cwd);
6416
- const configPath = path22.join(cwd, "kody.config.json");
6417
- if (fs23.existsSync(configPath) && !force) {
6467
+ const configPath = path23.join(cwd, "kody.config.json");
6468
+ if (fs24.existsSync(configPath) && !force) {
6418
6469
  skipped.push("kody.config.json");
6419
6470
  } else {
6420
6471
  const cfg = makeConfig(pm, ownerRepo, defaultBranch);
6421
- fs23.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
6472
+ fs24.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
6422
6473
  `);
6423
6474
  wrote.push("kody.config.json");
6424
6475
  }
6425
- const workflowDir = path22.join(cwd, ".github", "workflows");
6426
- const workflowPath = path22.join(workflowDir, "kody.yml");
6427
- if (fs23.existsSync(workflowPath) && !force) {
6476
+ const workflowDir = path23.join(cwd, ".github", "workflows");
6477
+ const workflowPath = path23.join(workflowDir, "kody.yml");
6478
+ if (fs24.existsSync(workflowPath) && !force) {
6428
6479
  skipped.push(".github/workflows/kody.yml");
6429
6480
  } else {
6430
- fs23.mkdirSync(workflowDir, { recursive: true });
6431
- fs23.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
6481
+ fs24.mkdirSync(workflowDir, { recursive: true });
6482
+ fs24.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
6432
6483
  wrote.push(".github/workflows/kody.yml");
6433
6484
  }
6434
- const hasUi = fs23.existsSync(path22.join(cwd, "src/app")) || fs23.existsSync(path22.join(cwd, "app")) || fs23.existsSync(path22.join(cwd, "pages"));
6485
+ const hasUi = fs24.existsSync(path23.join(cwd, "src/app")) || fs24.existsSync(path23.join(cwd, "app")) || fs24.existsSync(path23.join(cwd, "pages"));
6435
6486
  if (hasUi) {
6436
- const qaGuidePath = path22.join(cwd, QA_GUIDE_REL_PATH);
6437
- if (fs23.existsSync(qaGuidePath) && !force) {
6487
+ const qaGuidePath = path23.join(cwd, QA_GUIDE_REL_PATH);
6488
+ if (fs24.existsSync(qaGuidePath) && !force) {
6438
6489
  skipped.push(QA_GUIDE_REL_PATH);
6439
6490
  } else {
6440
- fs23.mkdirSync(path22.dirname(qaGuidePath), { recursive: true });
6491
+ fs24.mkdirSync(path23.dirname(qaGuidePath), { recursive: true });
6441
6492
  const discovery = runQaDiscovery(cwd);
6442
- fs23.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
6493
+ fs24.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
6443
6494
  wrote.push(QA_GUIDE_REL_PATH);
6444
6495
  }
6445
6496
  }
6446
6497
  const builtinJobs = listBuiltinJobs();
6447
6498
  if (builtinJobs.length > 0) {
6448
- const jobsDir = path22.join(cwd, ".kody", "jobs");
6449
- fs23.mkdirSync(jobsDir, { recursive: true });
6499
+ const jobsDir = path23.join(cwd, ".kody", "jobs");
6500
+ fs24.mkdirSync(jobsDir, { recursive: true });
6450
6501
  for (const job of builtinJobs) {
6451
- const rel = path22.join(".kody", "jobs", `${job.slug}.md`);
6452
- const target = path22.join(cwd, rel);
6453
- if (fs23.existsSync(target) && !force) {
6502
+ const rel = path23.join(".kody", "jobs", `${job.slug}.md`);
6503
+ const target = path23.join(cwd, rel);
6504
+ if (fs24.existsSync(target) && !force) {
6454
6505
  skipped.push(rel);
6455
6506
  continue;
6456
6507
  }
6457
- fs23.writeFileSync(target, fs23.readFileSync(job.filePath, "utf-8"));
6508
+ fs24.writeFileSync(target, fs24.readFileSync(job.filePath, "utf-8"));
6458
6509
  wrote.push(rel);
6459
6510
  }
6460
6511
  }
@@ -6466,12 +6517,12 @@ function performInit(cwd, force) {
6466
6517
  continue;
6467
6518
  }
6468
6519
  if (profile.kind !== "scheduled" || !profile.schedule) continue;
6469
- const target = path22.join(workflowDir, `kody-${exe.name}.yml`);
6470
- if (fs23.existsSync(target) && !force) {
6520
+ const target = path23.join(workflowDir, `kody-${exe.name}.yml`);
6521
+ if (fs24.existsSync(target) && !force) {
6471
6522
  skipped.push(`.github/workflows/kody-${exe.name}.yml`);
6472
6523
  continue;
6473
6524
  }
6474
- fs23.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
6525
+ fs24.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
6475
6526
  wrote.push(`.github/workflows/kody-${exe.name}.yml`);
6476
6527
  }
6477
6528
  let labels;
@@ -6560,14 +6611,14 @@ var loadCoverageRules = async (ctx) => {
6560
6611
  };
6561
6612
 
6562
6613
  // src/goal/state.ts
6563
- import * as fs24 from "fs";
6564
- import * as path23 from "path";
6614
+ import * as fs25 from "fs";
6615
+ import * as path24 from "path";
6565
6616
  var VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "done"]);
6566
6617
  var GoalStateError = class extends Error {
6567
- constructor(path31, message) {
6568
- super(`Invalid goal state at ${path31}:
6618
+ constructor(path32, message) {
6619
+ super(`Invalid goal state at ${path32}:
6569
6620
  ${message}`);
6570
- this.path = path31;
6621
+ this.path = path32;
6571
6622
  this.name = "GoalStateError";
6572
6623
  }
6573
6624
  path;
@@ -6611,16 +6662,16 @@ function serializeGoalState(s) {
6611
6662
  `;
6612
6663
  }
6613
6664
  function goalStatePath(cwd, goalId) {
6614
- return path23.join(cwd, ".kody", "goals", goalId, "state.json");
6665
+ return path24.join(cwd, ".kody", "goals", goalId, "state.json");
6615
6666
  }
6616
6667
  function readGoalState(cwd, goalId) {
6617
6668
  const file = goalStatePath(cwd, goalId);
6618
- if (!fs24.existsSync(file)) {
6669
+ if (!fs25.existsSync(file)) {
6619
6670
  throw new GoalStateError(file, "file not found");
6620
6671
  }
6621
6672
  let raw;
6622
6673
  try {
6623
- raw = JSON.parse(fs24.readFileSync(file, "utf-8"));
6674
+ raw = JSON.parse(fs25.readFileSync(file, "utf-8"));
6624
6675
  } catch (err) {
6625
6676
  throw new GoalStateError(file, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
6626
6677
  }
@@ -6628,8 +6679,8 @@ function readGoalState(cwd, goalId) {
6628
6679
  }
6629
6680
  function writeGoalState(cwd, goalId, state) {
6630
6681
  const file = goalStatePath(cwd, goalId);
6631
- fs24.mkdirSync(path23.dirname(file), { recursive: true });
6632
- fs24.writeFileSync(file, serializeGoalState(state), "utf-8");
6682
+ fs25.mkdirSync(path24.dirname(file), { recursive: true });
6683
+ fs25.writeFileSync(file, serializeGoalState(state), "utf-8");
6633
6684
  }
6634
6685
  function nowIso() {
6635
6686
  return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
@@ -6723,8 +6774,8 @@ var loadIssueStateComment = async (ctx, _profile, args) => {
6723
6774
  };
6724
6775
 
6725
6776
  // src/scripts/loadJobFromFile.ts
6726
- import * as fs25 from "fs";
6727
- import * as path24 from "path";
6777
+ import * as fs26 from "fs";
6778
+ import * as path25 from "path";
6728
6779
  var loadJobFromFile = async (ctx, _profile, args) => {
6729
6780
  const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
6730
6781
  const slugArg = String(args?.slugArg ?? "job");
@@ -6732,11 +6783,11 @@ var loadJobFromFile = async (ctx, _profile, args) => {
6732
6783
  if (!slug) {
6733
6784
  throw new Error(`loadJobFromFile: ctx.args.${slugArg} must be a non-empty slug`);
6734
6785
  }
6735
- const absPath = path24.join(ctx.cwd, jobsDir, `${slug}.md`);
6736
- if (!fs25.existsSync(absPath)) {
6786
+ const absPath = path25.join(ctx.cwd, jobsDir, `${slug}.md`);
6787
+ if (!fs26.existsSync(absPath)) {
6737
6788
  throw new Error(`loadJobFromFile: job file not found: ${absPath}`);
6738
6789
  }
6739
- const raw = fs25.readFileSync(absPath, "utf-8");
6790
+ const raw = fs26.readFileSync(absPath, "utf-8");
6740
6791
  const { title, body } = parseJobFile(raw, slug);
6741
6792
  const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, jobsDir });
6742
6793
  const loaded = await backend.load(slug);
@@ -6768,16 +6819,16 @@ function humanizeSlug(slug) {
6768
6819
  }
6769
6820
 
6770
6821
  // src/scripts/loadMemoryContext.ts
6771
- import * as fs26 from "fs";
6772
- import * as path25 from "path";
6822
+ import * as fs27 from "fs";
6823
+ import * as path26 from "path";
6773
6824
  var MEMORY_DIR_RELATIVE = ".kody/memory";
6774
6825
  var MAX_PAGES = 8;
6775
6826
  var PER_PAGE_MAX_BYTES = 4e3;
6776
6827
  var TOTAL_MAX_BYTES = 24e3;
6777
6828
  var TRUNCATED_SUFFIX = "\n\n\u2026 (truncated)";
6778
6829
  var loadMemoryContext = async (ctx) => {
6779
- const memoryAbs = path25.join(ctx.cwd, MEMORY_DIR_RELATIVE);
6780
- if (!fs26.existsSync(memoryAbs)) {
6830
+ const memoryAbs = path26.join(ctx.cwd, MEMORY_DIR_RELATIVE);
6831
+ if (!fs27.existsSync(memoryAbs)) {
6781
6832
  ctx.data.memoryContext = "";
6782
6833
  return;
6783
6834
  }
@@ -6802,21 +6853,21 @@ function collectPages(memoryAbs) {
6802
6853
  walkMd(memoryAbs, (file) => {
6803
6854
  let stat;
6804
6855
  try {
6805
- stat = fs26.statSync(file);
6856
+ stat = fs27.statSync(file);
6806
6857
  } catch {
6807
6858
  return;
6808
6859
  }
6809
6860
  let raw;
6810
6861
  try {
6811
- raw = fs26.readFileSync(file, "utf-8");
6862
+ raw = fs27.readFileSync(file, "utf-8");
6812
6863
  } catch {
6813
6864
  return;
6814
6865
  }
6815
6866
  const fm = raw.match(/^---\s*\n([\s\S]*?)\n---/);
6816
- const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path25.basename(file, ".md");
6867
+ const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path26.basename(file, ".md");
6817
6868
  const updated = fm?.[1]?.match(/^updated:\s*([0-9T:.+\-Z]+)/m)?.[1]?.trim() ?? "";
6818
6869
  out.push({
6819
- relPath: path25.relative(memoryAbs, file),
6870
+ relPath: path26.relative(memoryAbs, file),
6820
6871
  title,
6821
6872
  updated,
6822
6873
  content: raw.length > PER_PAGE_MAX_BYTES ? raw.slice(0, PER_PAGE_MAX_BYTES) + TRUNCATED_SUFFIX : raw,
@@ -6884,16 +6935,16 @@ function walkMd(root, visit) {
6884
6935
  const dir = stack.pop();
6885
6936
  let names;
6886
6937
  try {
6887
- names = fs26.readdirSync(dir);
6938
+ names = fs27.readdirSync(dir);
6888
6939
  } catch {
6889
6940
  continue;
6890
6941
  }
6891
6942
  for (const name of names) {
6892
6943
  if (name.startsWith(".")) continue;
6893
- const full = path25.join(dir, name);
6944
+ const full = path26.join(dir, name);
6894
6945
  let stat;
6895
6946
  try {
6896
- stat = fs26.statSync(full);
6947
+ stat = fs27.statSync(full);
6897
6948
  } catch {
6898
6949
  continue;
6899
6950
  }
@@ -7001,8 +7052,8 @@ function formatReviewComments(raw) {
7001
7052
  init_events();
7002
7053
 
7003
7054
  // src/taskContext.ts
7004
- import * as fs27 from "fs";
7005
- import * as path26 from "path";
7055
+ import * as fs28 from "fs";
7056
+ import * as path27 from "path";
7006
7057
  var TASK_CONTEXT_SCHEMA_VERSION = 1;
7007
7058
  function buildTaskContext(args) {
7008
7059
  return {
@@ -7018,10 +7069,10 @@ function buildTaskContext(args) {
7018
7069
  }
7019
7070
  function persistTaskContext(cwd, ctx) {
7020
7071
  try {
7021
- const dir = path26.join(cwd, ".kody", "runs", ctx.runId);
7022
- fs27.mkdirSync(dir, { recursive: true });
7023
- const file = path26.join(dir, "task-context.json");
7024
- fs27.writeFileSync(file, `${JSON.stringify(ctx, null, 2)}
7072
+ const dir = path27.join(cwd, ".kody", "runs", ctx.runId);
7073
+ fs28.mkdirSync(dir, { recursive: true });
7074
+ const file = path27.join(dir, "task-context.json");
7075
+ fs28.writeFileSync(file, `${JSON.stringify(ctx, null, 2)}
7025
7076
  `);
7026
7077
  return file;
7027
7078
  } catch (err) {
@@ -8376,8 +8427,8 @@ function resolveBaseOverride(value) {
8376
8427
 
8377
8428
  // src/scripts/runTickScript.ts
8378
8429
  import { spawnSync } from "child_process";
8379
- import * as fs28 from "fs";
8380
- import * as path27 from "path";
8430
+ import * as fs29 from "fs";
8431
+ import * as path28 from "path";
8381
8432
  var runTickScript = async (ctx, _profile, args) => {
8382
8433
  ctx.skipAgent = true;
8383
8434
  const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
@@ -8389,13 +8440,13 @@ var runTickScript = async (ctx, _profile, args) => {
8389
8440
  ctx.output.reason = `runTickScript: ctx.args.${slugArg} must be a non-empty slug`;
8390
8441
  return;
8391
8442
  }
8392
- const jobPath = path27.join(ctx.cwd, jobsDir, `${slug}.md`);
8393
- if (!fs28.existsSync(jobPath)) {
8443
+ const jobPath = path28.join(ctx.cwd, jobsDir, `${slug}.md`);
8444
+ if (!fs29.existsSync(jobPath)) {
8394
8445
  ctx.output.exitCode = 99;
8395
8446
  ctx.output.reason = `runTickScript: job file not found: ${jobPath}`;
8396
8447
  return;
8397
8448
  }
8398
- const raw = fs28.readFileSync(jobPath, "utf-8");
8449
+ const raw = fs29.readFileSync(jobPath, "utf-8");
8399
8450
  const { frontmatter } = splitFrontmatter(raw);
8400
8451
  const tickScript = frontmatter.tickScript;
8401
8452
  if (!tickScript) {
@@ -8403,8 +8454,8 @@ var runTickScript = async (ctx, _profile, args) => {
8403
8454
  ctx.output.reason = `runTickScript: job ${slug} has no \`tickScript:\` frontmatter \u2014 route via job-tick instead`;
8404
8455
  return;
8405
8456
  }
8406
- const scriptPath = path27.isAbsolute(tickScript) ? tickScript : path27.join(ctx.cwd, tickScript);
8407
- if (!fs28.existsSync(scriptPath)) {
8457
+ const scriptPath = path28.isAbsolute(tickScript) ? tickScript : path28.join(ctx.cwd, tickScript);
8458
+ if (!fs29.existsSync(scriptPath)) {
8408
8459
  ctx.output.exitCode = 99;
8409
8460
  ctx.output.reason = `runTickScript: tickScript not found: ${scriptPath}`;
8410
8461
  return;
@@ -9239,7 +9290,7 @@ var writeJobStateFile = async (ctx, _profile, _agentResult, args) => {
9239
9290
  };
9240
9291
 
9241
9292
  // src/scripts/writeRunSummary.ts
9242
- import * as fs29 from "fs";
9293
+ import * as fs30 from "fs";
9243
9294
  var writeRunSummary = async (ctx, profile) => {
9244
9295
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
9245
9296
  if (!summaryPath) return;
@@ -9261,7 +9312,7 @@ var writeRunSummary = async (ctx, profile) => {
9261
9312
  if (reason) lines.push(`- **Reason:** ${reason}`);
9262
9313
  lines.push("");
9263
9314
  try {
9264
- fs29.appendFileSync(summaryPath, `${lines.join("\n")}
9315
+ fs30.appendFileSync(summaryPath, `${lines.join("\n")}
9265
9316
  `);
9266
9317
  } catch {
9267
9318
  }
@@ -9478,9 +9529,9 @@ async function runExecutable(profileName, input) {
9478
9529
  data: {},
9479
9530
  output: { exitCode: 0 }
9480
9531
  };
9481
- const ndjsonDir = path28.join(input.cwd, ".kody");
9532
+ const ndjsonDir = path29.join(input.cwd, ".kody");
9482
9533
  const invokeAgent = async (prompt) => {
9483
- const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path28.isAbsolute(p) ? p : path28.resolve(profile.dir, p)).filter((p) => p.length > 0);
9534
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path29.isAbsolute(p) ? p : path29.resolve(profile.dir, p)).filter((p) => p.length > 0);
9484
9535
  const syntheticPath = ctx.data.syntheticPluginPath;
9485
9536
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
9486
9537
  return runAgent({
@@ -9648,7 +9699,7 @@ function clearStampedLifecycleLabels(profile, ctx) {
9648
9699
  function getProfileInputsForChild(profileName, _cwd) {
9649
9700
  try {
9650
9701
  const profilePath = resolveProfilePath(profileName);
9651
- if (!fs30.existsSync(profilePath)) return null;
9702
+ if (!fs31.existsSync(profilePath)) return null;
9652
9703
  return loadProfile(profilePath).inputs;
9653
9704
  } catch {
9654
9705
  return null;
@@ -9657,17 +9708,17 @@ function getProfileInputsForChild(profileName, _cwd) {
9657
9708
  function resolveProfilePath(profileName) {
9658
9709
  const found = resolveExecutable(profileName);
9659
9710
  if (found) return found;
9660
- const here = path28.dirname(new URL(import.meta.url).pathname);
9711
+ const here = path29.dirname(new URL(import.meta.url).pathname);
9661
9712
  const candidates = [
9662
- path28.join(here, "executables", profileName, "profile.json"),
9713
+ path29.join(here, "executables", profileName, "profile.json"),
9663
9714
  // same-dir sibling (dev)
9664
- path28.join(here, "..", "executables", profileName, "profile.json"),
9715
+ path29.join(here, "..", "executables", profileName, "profile.json"),
9665
9716
  // up one (prod: dist/bin → dist/executables)
9666
- path28.join(here, "..", "src", "executables", profileName, "profile.json")
9717
+ path29.join(here, "..", "src", "executables", profileName, "profile.json")
9667
9718
  // fallback
9668
9719
  ];
9669
9720
  for (const c of candidates) {
9670
- if (fs30.existsSync(c)) return c;
9721
+ if (fs31.existsSync(c)) return c;
9671
9722
  }
9672
9723
  return candidates[0];
9673
9724
  }
@@ -9764,8 +9815,8 @@ function resolveShellTimeoutMs(entry) {
9764
9815
  var SIGKILL_GRACE_MS = 5e3;
9765
9816
  async function runShellEntry(entry, ctx, profile) {
9766
9817
  const shellName = entry.shell;
9767
- const shellPath = path28.join(profile.dir, shellName);
9768
- if (!fs30.existsSync(shellPath)) {
9818
+ const shellPath = path29.join(profile.dir, shellName);
9819
+ if (!fs31.existsSync(shellPath)) {
9769
9820
  ctx.skipAgent = true;
9770
9821
  ctx.output.exitCode = 99;
9771
9822
  ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
@@ -9900,7 +9951,12 @@ async function runContainerLoop(profile, ctx, input) {
9900
9951
  const child = children[currentIdx];
9901
9952
  process.stderr.write(`[kody container] step ${iteration}: invoking ${child.exec}
9902
9953
  `);
9903
- resetWorkingTree(input.cwd);
9954
+ if (profile.resetBetweenChildren !== false) {
9955
+ resetWorkingTree(input.cwd);
9956
+ } else {
9957
+ process.stderr.write(`[kody container] resetBetweenChildren=false; preserving tracked tree
9958
+ `);
9959
+ }
9904
9960
  const priorState = readContainerState(ctx, child, reader);
9905
9961
  if (priorState.core?.prUrl) knownPrUrl = priorState.core.prUrl;
9906
9962
  const priorAction = priorState.executables?.[child.exec]?.lastAction;
@@ -10210,9 +10266,9 @@ function resolveAuthToken(env = process.env) {
10210
10266
  return token;
10211
10267
  }
10212
10268
  function detectPackageManager2(cwd) {
10213
- if (fs31.existsSync(path29.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
10214
- if (fs31.existsSync(path29.join(cwd, "yarn.lock"))) return "yarn";
10215
- if (fs31.existsSync(path29.join(cwd, "bun.lockb"))) return "bun";
10269
+ if (fs32.existsSync(path30.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
10270
+ if (fs32.existsSync(path30.join(cwd, "yarn.lock"))) return "yarn";
10271
+ if (fs32.existsSync(path30.join(cwd, "bun.lockb"))) return "bun";
10216
10272
  return "npm";
10217
10273
  }
10218
10274
  function shellOut(cmd, args, cwd, stream = true) {
@@ -10299,11 +10355,11 @@ function configureGitIdentity(cwd) {
10299
10355
  }
10300
10356
  function postFailureTail(issueNumber, cwd, reason) {
10301
10357
  if (!issueNumber) return;
10302
- const logPath = path29.join(cwd, ".kody", "last-run.jsonl");
10358
+ const logPath = path30.join(cwd, ".kody", "last-run.jsonl");
10303
10359
  let tail = "";
10304
10360
  try {
10305
- if (fs31.existsSync(logPath)) {
10306
- const content = fs31.readFileSync(logPath, "utf-8");
10361
+ if (fs32.existsSync(logPath)) {
10362
+ const content = fs32.readFileSync(logPath, "utf-8");
10307
10363
  tail = content.slice(-3e3);
10308
10364
  }
10309
10365
  } catch {
@@ -10328,7 +10384,7 @@ async function runCi(argv) {
10328
10384
  return 0;
10329
10385
  }
10330
10386
  const args = parseCiArgs(argv);
10331
- const cwd = args.cwd ? path29.resolve(args.cwd) : process.cwd();
10387
+ const cwd = args.cwd ? path30.resolve(args.cwd) : process.cwd();
10332
10388
  let earlyConfig;
10333
10389
  try {
10334
10390
  earlyConfig = loadConfig(cwd);
@@ -10338,9 +10394,9 @@ async function runCi(argv) {
10338
10394
  const eventName = process.env.GITHUB_EVENT_NAME;
10339
10395
  const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
10340
10396
  let manualWorkflowDispatch = false;
10341
- if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs31.existsSync(dispatchEventPath)) {
10397
+ if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs32.existsSync(dispatchEventPath)) {
10342
10398
  try {
10343
- const evt = JSON.parse(fs31.readFileSync(dispatchEventPath, "utf-8"));
10399
+ const evt = JSON.parse(fs32.readFileSync(dispatchEventPath, "utf-8"));
10344
10400
  const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
10345
10401
  const sessionInput = String(evt?.inputs?.sessionId ?? "");
10346
10402
  manualWorkflowDispatch = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
@@ -10599,9 +10655,9 @@ function parseChatArgs(argv, env = process.env) {
10599
10655
  return result;
10600
10656
  }
10601
10657
  function commitChatFiles(cwd, sessionId, verbose) {
10602
- const sessionFile = path30.relative(cwd, sessionFilePath(cwd, sessionId));
10603
- const eventsFile = path30.relative(cwd, eventsFilePath(cwd, sessionId));
10604
- const paths = [sessionFile, eventsFile].filter((p) => fs32.existsSync(path30.join(cwd, p)));
10658
+ const sessionFile = path31.relative(cwd, sessionFilePath(cwd, sessionId));
10659
+ const eventsFile = path31.relative(cwd, eventsFilePath(cwd, sessionId));
10660
+ const paths = [sessionFile, eventsFile].filter((p) => fs33.existsSync(path31.join(cwd, p)));
10605
10661
  if (paths.length === 0) return;
10606
10662
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
10607
10663
  try {
@@ -10639,7 +10695,7 @@ async function runChat(argv) {
10639
10695
  ${CHAT_HELP}`);
10640
10696
  return 64;
10641
10697
  }
10642
- const cwd = args.cwd ? path30.resolve(args.cwd) : process.cwd();
10698
+ const cwd = args.cwd ? path31.resolve(args.cwd) : process.cwd();
10643
10699
  const sessionId = args.sessionId;
10644
10700
  const unpackedSecrets = unpackAllSecrets();
10645
10701
  if (unpackedSecrets > 0) {
@@ -10691,7 +10747,7 @@ ${CHAT_HELP}`);
10691
10747
  const sink = buildSink(cwd, sessionId, args.dashboardUrl);
10692
10748
  const meta = readMeta(sessionFile);
10693
10749
  process.stdout.write(
10694
- `\u2192 kody:chat: session file=${sessionFile} exists=${fs32.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
10750
+ `\u2192 kody:chat: session file=${sessionFile} exists=${fs33.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
10695
10751
  `
10696
10752
  );
10697
10753
  try {
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -74,6 +74,16 @@ export interface Profile {
74
74
  * Defines the in-process step sequence and routing map. See ContainerChild.
75
75
  */
76
76
  children?: ContainerChild[]
77
+ /**
78
+ * Whether the container should `git reset --hard HEAD` between
79
+ * children to discard tracked-file modifications a prior child left
80
+ * behind. Default `true` (preserves the legacy bug-safe behaviour
81
+ * — see executor.ts:runContainerLoop notes). Set `false` for
82
+ * containers whose children are expected to share intermediate
83
+ * state (e.g. bug's `reproduce` writing a failing test that `run`
84
+ * then makes pass). Only honoured when `role === "container"`.
85
+ */
86
+ resetBetweenChildren?: boolean
77
87
  /** Absolute directory the profile was loaded from. Used to resolve prompt.md. */
78
88
  dir: string
79
89
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.59",
3
+ "version": "0.4.60",
4
4
  "description": "kody — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,6 +12,18 @@
12
12
  "templates",
13
13
  "kody.config.schema.json"
14
14
  ],
15
+ "scripts": {
16
+ "kody": "tsx bin/kody.ts",
17
+ "build": "tsup && node scripts/copy-assets.cjs",
18
+ "test": "vitest run tests/unit tests/int --no-coverage",
19
+ "test:e2e": "vitest run tests/e2e --no-coverage",
20
+ "test:all": "vitest run tests --no-coverage",
21
+ "typecheck": "tsc --noEmit",
22
+ "lint": "biome check",
23
+ "lint:fix": "biome check --write",
24
+ "format": "biome format --write",
25
+ "prepublishOnly": "pnpm build"
26
+ },
15
27
  "dependencies": {
16
28
  "@actions/cache": "^6.0.0",
17
29
  "@anthropic-ai/claude-agent-sdk": "0.2.119",
@@ -33,16 +45,5 @@
33
45
  "url": "git+https://github.com/aharonyaircohen/kody-engine.git"
34
46
  },
35
47
  "homepage": "https://github.com/aharonyaircohen/kody-engine",
36
- "bugs": "https://github.com/aharonyaircohen/kody-engine/issues",
37
- "scripts": {
38
- "kody": "tsx bin/kody.ts",
39
- "build": "tsup && node scripts/copy-assets.cjs",
40
- "test": "vitest run tests/unit tests/int --no-coverage",
41
- "test:e2e": "vitest run tests/e2e --no-coverage",
42
- "test:all": "vitest run tests --no-coverage",
43
- "typecheck": "tsc --noEmit",
44
- "lint": "biome check",
45
- "lint:fix": "biome check --write",
46
- "format": "biome format --write"
47
- }
48
- }
48
+ "bugs": "https://github.com/aharonyaircohen/kody-engine/issues"
49
+ }