@kody-ade/kody-engine 0.4.285 → 0.4.287

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.
Files changed (2) hide show
  1. package/dist/bin/kody.js +75 -30
  2. package/package.json +1 -1
package/dist/bin/kody.js CHANGED
@@ -15,7 +15,7 @@ var init_package = __esm({
15
15
  "package.json"() {
16
16
  package_default = {
17
17
  name: "@kody-ade/kody-engine",
18
- version: "0.4.285",
18
+ version: "0.4.287",
19
19
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
20
20
  license: "MIT",
21
21
  type: "module",
@@ -443,7 +443,7 @@ var STATE_BRANCH;
443
443
  var init_stateBranch = __esm({
444
444
  "src/stateBranch.ts"() {
445
445
  "use strict";
446
- STATE_BRANCH = "kody-state";
446
+ STATE_BRANCH = "main";
447
447
  }
448
448
  });
449
449
 
@@ -497,18 +497,33 @@ function normalizeStatePath(raw, field = "statePath") {
497
497
  }
498
498
  return parts.join("/");
499
499
  }
500
+ function normalizeStateBranch(raw, field = "state.branch") {
501
+ const value = raw?.trim() || STATE_BRANCH;
502
+ if (!value) throw new Error(`kody.config.json: ${field} must not be empty`);
503
+ if (value.startsWith("/") || value.endsWith("/") || value.includes("\\") || value.includes("..") || value.includes("@{") || /[\x00-\x20\x7f~^:?*\[]/.test(value)) {
504
+ throw new Error(`kody.config.json: ${field} contains an invalid branch`);
505
+ }
506
+ for (const part of value.split("/")) {
507
+ if (!part || part === "." || part === ".." || part.startsWith(".") || part.endsWith(".lock")) {
508
+ throw new Error(`kody.config.json: ${field} contains an invalid branch`);
509
+ }
510
+ }
511
+ return value;
512
+ }
500
513
  function resolveStateRepoConfig(config) {
501
514
  if (config.state?.repo && config.state?.path) {
502
515
  parseStateRepoSlug(config.state.repo);
503
516
  return {
504
517
  repo: config.state.repo,
505
- path: normalizeStatePath(config.state.path)
518
+ path: normalizeStatePath(config.state.path),
519
+ branch: normalizeStateBranch(config.state.branch)
506
520
  };
507
521
  }
508
522
  if (config.github?.owner && config.github?.repo) {
509
523
  return {
510
524
  repo: `${config.github.owner}/kody-state`,
511
- path: normalizeStatePath(config.github.repo)
525
+ path: normalizeStatePath(config.github.repo),
526
+ branch: normalizeStateBranch(void 0)
512
527
  };
513
528
  }
514
529
  throw new Error("stateRepo: config.state or config.github owner/repo is required");
@@ -516,7 +531,7 @@ function resolveStateRepoConfig(config) {
516
531
  function parseStateRepo(config) {
517
532
  const state = resolveStateRepoConfig(config);
518
533
  const parsed = parseStateRepoSlug(state.repo);
519
- return { ...parsed, basePath: state.path };
534
+ return { ...parsed, basePath: state.path, branch: state.branch };
520
535
  }
521
536
  function stateRepoPath(config, filePath) {
522
537
  const state = resolveStateRepoConfig(config);
@@ -528,14 +543,15 @@ function apiPath(config, targetPath) {
528
543
  return `/repos/${parsed.owner}/${parsed.repo}/contents/${targetPath}`;
529
544
  }
530
545
  function branchApiPath(config, targetPath) {
531
- return `${apiPath(config, targetPath)}?ref=${encodeURIComponent(STATE_BRANCH)}`;
546
+ const state = resolveStateRepoConfig(config);
547
+ return `${apiPath(config, targetPath)}?ref=${encodeURIComponent(state.branch)}`;
532
548
  }
533
549
  function ensureStateBranch(config, cwd) {
534
550
  const parsed = parseStateRepo(config);
535
- const cacheKey3 = `${parsed.owner}/${parsed.repo}:${STATE_BRANCH}`;
551
+ const cacheKey3 = `${parsed.owner}/${parsed.repo}:${parsed.branch}`;
536
552
  if (ensuredStateBranches.has(cacheKey3)) return;
537
553
  try {
538
- gh(["api", `/repos/${parsed.owner}/${parsed.repo}/git/ref/heads/${STATE_BRANCH}`], { cwd });
554
+ gh(["api", `/repos/${parsed.owner}/${parsed.repo}/git/ref/heads/${parsed.branch}`], { cwd });
539
555
  ensuredStateBranches.add(cacheKey3);
540
556
  return;
541
557
  } catch (err) {
@@ -550,7 +566,7 @@ function ensureStateBranch(config, cwd) {
550
566
  try {
551
567
  gh(["api", "--method", "POST", `/repos/${parsed.owner}/${parsed.repo}/git/refs`, "--input", "-"], {
552
568
  cwd,
553
- input: JSON.stringify({ ref: `refs/heads/${STATE_BRANCH}`, sha })
569
+ input: JSON.stringify({ ref: `refs/heads/${parsed.branch}`, sha })
554
570
  });
555
571
  } catch (err) {
556
572
  if (!isAlreadyExists(err)) throw err;
@@ -590,7 +606,7 @@ function writeStateText(config, cwd, filePath, content, message, sha) {
590
606
  const payload = {
591
607
  message,
592
608
  content: Buffer.from(content, "utf-8").toString("base64"),
593
- branch: STATE_BRANCH
609
+ branch: resolveStateRepoConfig(config).branch
594
610
  };
595
611
  if (sha) payload.sha = sha;
596
612
  gh(["api", "--method", "PUT", apiPath(config, targetPath), "--input", "-"], {
@@ -722,12 +738,14 @@ function parseStateConfig(raw, github) {
722
738
  const nested = recordValue(raw.state) ?? {};
723
739
  const repoRaw = typeof raw.stateRepo === "string" ? raw.stateRepo : nested.repo;
724
740
  const pathRaw = typeof raw.statePath === "string" ? raw.statePath : nested.path;
741
+ const branchRaw = typeof raw.stateBranch === "string" ? raw.stateBranch : nested.branch;
725
742
  const stateRepo = typeof repoRaw === "string" && repoRaw.trim().length > 0 ? repoRaw.trim() : `https://github.com/${String(github.owner)}/kody-state`;
726
743
  parseStateRepoSlug(stateRepo);
727
744
  const statePath = typeof pathRaw === "string" && pathRaw.trim().length > 0 ? pathRaw.trim() : String(github.repo);
728
745
  return {
729
746
  repo: stateRepo,
730
- path: normalizeStatePath(statePath)
747
+ path: normalizeStatePath(statePath),
748
+ branch: typeof branchRaw === "string" && branchRaw.trim().length > 0 ? normalizeStateBranch(branchRaw) : normalizeStateBranch(void 0)
731
749
  };
732
750
  }
733
751
  function parseAccessConfig(raw) {
@@ -6878,6 +6896,7 @@ function stateRepoContext(config, goalId, logPath) {
6878
6896
  return {
6879
6897
  repo: state.repo,
6880
6898
  path: state.path,
6899
+ branch: state.branch,
6881
6900
  goalStatePath: `${state.path}/${goalStateLogPath(goalId)}`,
6882
6901
  logPath: `${state.path}/${logPath}`
6883
6902
  };
@@ -6976,16 +6995,17 @@ function linkContext(stateRepo) {
6976
6995
  const server = process.env.GITHUB_SERVER_URL?.trim() || "https://github.com";
6977
6996
  if (runId && repository) links.workflowRun = `${server}/${repository}/actions/runs/${runId}`;
6978
6997
  const repo = stringValue(stateRepo?.repo);
6998
+ const branch = stringValue(stateRepo?.branch);
6979
6999
  const goalStatePath2 = stringValue(stateRepo?.goalStatePath);
6980
7000
  const logPath = stringValue(stateRepo?.logPath);
6981
- if (repo && goalStatePath2) links.goalState = githubBlobUrl(repo, goalStatePath2);
6982
- if (repo && logPath) links.log = githubBlobUrl(repo, logPath);
7001
+ if (repo && branch && goalStatePath2) links.goalState = githubBlobUrl(repo, branch, goalStatePath2);
7002
+ if (repo && branch && logPath) links.log = githubBlobUrl(repo, branch, logPath);
6983
7003
  return Object.keys(links).length > 0 ? links : void 0;
6984
7004
  }
6985
- function githubBlobUrl(repo, filePath) {
7005
+ function githubBlobUrl(repo, branch, filePath) {
6986
7006
  try {
6987
7007
  const parsed = parseStateRepoSlug(repo);
6988
- return `https://github.com/${parsed.owner}/${parsed.repo}/blob/${STATE_BRANCH}/${filePath}`;
7008
+ return `https://github.com/${parsed.owner}/${parsed.repo}/blob/${encodeURIComponent(branch)}/${filePath}`;
6989
7009
  } catch {
6990
7010
  return void 0;
6991
7011
  }
@@ -7068,7 +7088,6 @@ var LOGS_KEY, LOG_RUN_KEY, LOG_STARTED_KEY;
7068
7088
  var init_runLog = __esm({
7069
7089
  "src/goal/runLog.ts"() {
7070
7090
  "use strict";
7071
- init_stateBranch();
7072
7091
  init_stateRepo();
7073
7092
  init_state2();
7074
7093
  LOGS_KEY = "__goalRunLogs";
@@ -7270,12 +7289,10 @@ var init_stateStore = __esm({
7270
7289
  // src/goal/targetLoopResolution.ts
7271
7290
  import * as fs26 from "fs";
7272
7291
  import * as path24 from "path";
7273
- function resolveGoalLoopTarget(config, cwd, loopGoalId, loopGoal, now) {
7292
+ function resolveActiveGoalLoopTarget(config, cwd, loopGoalId, loopGoal) {
7274
7293
  const targetId = loopGoal.loopTarget?.id.trim() ?? "";
7275
7294
  assertSafeGoalId(targetId, "loop target");
7276
- if (!hasExplicitStateRepo(config)) {
7277
- return { targetId, templateId: targetId, reason: "literal target; state repo not configured" };
7278
- }
7295
+ if (!hasExplicitStateRepo(config)) return null;
7279
7296
  const activeInstance = findActiveTargetInstance(config, cwd, loopGoalId, targetId);
7280
7297
  if (activeInstance) {
7281
7298
  return {
@@ -7288,6 +7305,17 @@ function resolveGoalLoopTarget(config, cwd, loopGoalId, loopGoal, now) {
7288
7305
  if (directTarget?.state === "active") {
7289
7306
  return { targetId, templateId: targetId, reason: "active target goal" };
7290
7307
  }
7308
+ return null;
7309
+ }
7310
+ function resolveGoalLoopTarget(config, cwd, loopGoalId, loopGoal, now) {
7311
+ const targetId = loopGoal.loopTarget?.id.trim() ?? "";
7312
+ assertSafeGoalId(targetId, "loop target");
7313
+ if (!hasExplicitStateRepo(config)) {
7314
+ return { targetId, templateId: targetId, reason: "literal target; state repo not configured" };
7315
+ }
7316
+ const activeTarget = resolveActiveGoalLoopTarget(config, cwd, loopGoalId, loopGoal);
7317
+ if (activeTarget) return activeTarget;
7318
+ const directTarget = fetchGoalState(config, targetId, cwd);
7291
7319
  const template = loadGoalTemplate(cwd, targetId);
7292
7320
  if (!template) {
7293
7321
  if (directTarget) {
@@ -7961,7 +7989,9 @@ function planTargetLoopSchedule(opts) {
7961
7989
  }
7962
7990
  const preferred = opts.goal.preferredRunTime;
7963
7991
  if (preferred) {
7964
- const gate = preferredRunTimeGate(preferred, now, opts.previousScheduleState);
7992
+ const gate = preferredRunTimeGate(preferred, now, opts.previousScheduleState, {
7993
+ allowRepeatAfterCompletedTarget: opts.allowRepeatAfterCompletedTarget === true
7994
+ });
7965
7995
  if (!gate.ok) return targetLoopDecision("idle", gate.reason, at);
7966
7996
  }
7967
7997
  const dispatchTargetId = target.type === "goal" && opts.resolvedGoalTargetId?.trim() ? opts.resolvedGoalTargetId.trim() : targetId;
@@ -8123,7 +8153,7 @@ function targetLoopDecision(kind, reason, at) {
8123
8153
  }
8124
8154
  };
8125
8155
  }
8126
- function preferredRunTimeGate(preferred, now, previous) {
8156
+ function preferredRunTimeGate(preferred, now, previous, opts) {
8127
8157
  const current = zonedTimeParts(now, preferred.timezone);
8128
8158
  if (!current) return { ok: false, reason: `invalid preferred timezone: ${preferred.timezone}` };
8129
8159
  const preferredMinute = preferredTimeToMinute(preferred.time);
@@ -8135,7 +8165,7 @@ function preferredRunTimeGate(preferred, now, previous) {
8135
8165
  const lastDispatchAt = previous?.lastDecision.kind === "dispatch" ? previous.lastDecision.at : void 0;
8136
8166
  if (lastDispatchAt) {
8137
8167
  const last = zonedTimeParts(new Date(lastDispatchAt), preferred.timezone);
8138
- if (last?.date === current.date) {
8168
+ if (last?.date === current.date && opts?.allowRepeatAfterCompletedTarget !== true) {
8139
8169
  return { ok: false, reason: `already dispatched today at preferred time ${preferred.time} ${preferred.timezone}` };
8140
8170
  }
8141
8171
  }
@@ -8269,6 +8299,14 @@ function readSimpleGoalTaskSummary(goalId, cwd) {
8269
8299
  const open = issues.filter((issue) => String(issue.state ?? "").toLowerCase() === "open").length;
8270
8300
  return { total, open };
8271
8301
  }
8302
+ function previousDispatchWasTargetInstance(managed, previousScheduleState) {
8303
+ const targetId = managed.loopTarget?.id.trim();
8304
+ const previous = previousScheduleState?.lastDecision;
8305
+ if (!targetId || previous?.kind !== "dispatch" || !("targetType" in previous) || previous.targetType !== "goal") {
8306
+ return false;
8307
+ }
8308
+ return previous.targetId === targetId || previous.targetId.startsWith(`${targetId}-`);
8309
+ }
8272
8310
  function ensureIssueFactIfNeeded(goal, goalId, cwd) {
8273
8311
  if (!routeNeedsIssueFact(goal)) return;
8274
8312
  const existing = normalizeIssueNumber(goal.facts.issue);
@@ -8401,7 +8439,14 @@ var init_advanceManagedGoal = __esm({
8401
8439
  const beforeSnapshot = goalRunLogSnapshot(goal.id, goal.state, managed);
8402
8440
  const previousScheduleState = goal.raw.extra.scheduleState && typeof goal.raw.extra.scheduleState === "object" ? goal.raw.extra.scheduleState : void 0;
8403
8441
  const now = goalLoopNow();
8404
- let decision2 = planTargetLoopSchedule({ goal: managed, previousScheduleState, now });
8442
+ const activeTarget = isGoalTargetLoop(managed) ? resolveActiveGoalLoopTarget(ctx.config, ctx.cwd, goal.id, managed) : null;
8443
+ const allowRepeatAfterCompletedTarget = isGoalTargetLoop(managed) && !activeTarget && previousDispatchWasTargetInstance(managed, previousScheduleState);
8444
+ let decision2 = planTargetLoopSchedule({
8445
+ goal: managed,
8446
+ previousScheduleState,
8447
+ now,
8448
+ allowRepeatAfterCompletedTarget
8449
+ });
8405
8450
  let targetResolution;
8406
8451
  if (decision2.kind === "dispatch" && decision2.dispatch && isGoalTargetLoop(managed)) {
8407
8452
  targetResolution = resolveGoalLoopTarget(ctx.config, ctx.cwd, goal.id, managed, now);
@@ -8409,7 +8454,8 @@ var init_advanceManagedGoal = __esm({
8409
8454
  goal: managed,
8410
8455
  previousScheduleState,
8411
8456
  now,
8412
- resolvedGoalTargetId: targetResolution.targetId
8457
+ resolvedGoalTargetId: targetResolution.targetId,
8458
+ allowRepeatAfterCompletedTarget
8413
8459
  });
8414
8460
  }
8415
8461
  restoreGoalIdFact();
@@ -17509,7 +17555,7 @@ function overlayDirectoryChildren(cwd, sourceDir, localDir) {
17509
17555
  function hydrateStateWorkspace(config, cwd) {
17510
17556
  if (process.env.VITEST && process.env[TEST_FETCH_ENV] !== "1") return;
17511
17557
  const parsed = parseStateRepo(config);
17512
- const hydrateKey = `${path41.resolve(cwd)}|${parsed.owner}/${parsed.repo}|${parsed.basePath}|${STATE_BRANCH}`;
17558
+ const hydrateKey = `${path41.resolve(cwd)}|${parsed.owner}/${parsed.repo}|${parsed.basePath}|${parsed.branch}`;
17513
17559
  if (hydratedWorkspaces.has(hydrateKey)) return;
17514
17560
  const snapshotRoot = fetchStateSnapshot(parsed);
17515
17561
  for (const mapping of DIR_MAPPINGS) {
@@ -17533,7 +17579,7 @@ function fetchStateSnapshot(parsed) {
17533
17579
  runGit3(["clone", "--no-checkout", "--filter=blob:none", url, cacheDir]);
17534
17580
  }
17535
17581
  runGit3(["-C", cacheDir, "remote", "set-url", "origin", url]);
17536
- runGit3(["-C", cacheDir, "fetch", "--depth=1", "origin", STATE_BRANCH]);
17582
+ runGit3(["-C", cacheDir, "fetch", "--depth=1", "origin", parsed.branch]);
17537
17583
  runGit3(["-C", cacheDir, "sparse-checkout", "init", "--cone"]);
17538
17584
  runGit3(["-C", cacheDir, "sparse-checkout", "set", parsed.basePath]);
17539
17585
  runGit3(["-C", cacheDir, "checkout", "--force", "--detach", "FETCH_HEAD"]);
@@ -17541,7 +17587,7 @@ function fetchStateSnapshot(parsed) {
17541
17587
  } catch (err) {
17542
17588
  const msg = err instanceof Error ? err.message : String(err);
17543
17589
  throw new Error(
17544
- `stateWorkspace: failed to fetch ${parsed.owner}/${parsed.repo}:${parsed.basePath}@${STATE_BRANCH}: ${msg}`
17590
+ `stateWorkspace: failed to fetch ${parsed.owner}/${parsed.repo}:${parsed.basePath}@${parsed.branch}: ${msg}`
17545
17591
  );
17546
17592
  }
17547
17593
  return path41.join(cacheDir, parsed.basePath);
@@ -17550,7 +17596,7 @@ function cacheRoot2() {
17550
17596
  return process.env[CACHE_ENV2]?.trim() || path41.join(os7.homedir(), ".cache", "kody", "state-repo");
17551
17597
  }
17552
17598
  function cacheKey2(parsed) {
17553
- return crypto3.createHash("sha256").update(`${parsed.owner}/${parsed.repo}#${STATE_BRANCH}#${parsed.basePath}`).digest("hex").slice(0, 24);
17599
+ return crypto3.createHash("sha256").update(`${parsed.owner}/${parsed.repo}#${parsed.branch}#${parsed.basePath}`).digest("hex").slice(0, 24);
17554
17600
  }
17555
17601
  function runGit3(args) {
17556
17602
  try {
@@ -17585,7 +17631,6 @@ var DIR_MAPPINGS, FILE_MAPPINGS, CACHE_ENV2, TEST_FETCH_ENV, hydratedWorkspaces;
17585
17631
  var init_stateWorkspace = __esm({
17586
17632
  "src/stateWorkspace.ts"() {
17587
17633
  "use strict";
17588
- init_stateBranch();
17589
17634
  init_stateRepo();
17590
17635
  DIR_MAPPINGS = [
17591
17636
  { stateDir: "executables", localDir: path41.join(".kody", "executables") },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.285",
3
+ "version": "0.4.287",
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",