@kody-ade/kody-engine 0.3.46 → 0.3.48

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/README.md CHANGED
@@ -62,6 +62,7 @@ kody chore --issue <N> # run → review (→ fix
62
62
  kody mission-scheduler # fans out to per-issue mission-tick
63
63
  kody mission-tick --issue <N> # one tick of a kody:mission issue
64
64
  kody watch-stale-prs # weekly stale-PR report
65
+ kody memorize # daily vault wiki update from recent PRs
65
66
 
66
67
  # deterministic (no agent)
67
68
  kody sync --pr <N> # merge default into PR branch
@@ -91,6 +92,18 @@ Drives the running preview deployment via the Playwright MCP server alongside th
91
92
  - Credentials: `.kody/qa-guide.md` (committed, scaffolded by `kody init` with `CHANGE_ME` placeholders).
92
93
  - Auto-discovery: routes, roles, login/admin paths, Payload CMS collections, API routes, env vars — fed to the agent as context.
93
94
 
95
+ ### `memorize` — vault wiki
96
+
97
+ A scheduled watch (cron `0 3 * * *`) that synthesizes recently merged PRs into a markdown knowledge base at `.kody/vault/` and opens a PR with the changes. Pages are entity-centric (`architecture/`, `conventions/`, `decisions/`, `components/`), not per-PR logs. Future kody runs see the relevant pages via the `loadVaultContext` preflight, which is wired into `run` / `fix` / `resolve` and exposes them as `{{vaultContext}}` in the prompt.
98
+
99
+ To enable in a consumer repo: ensure `.gitignore` un-ignores the vault if `.kody/*` is otherwise ignored:
100
+
101
+ ```gitignore
102
+ .kody/*
103
+ !.kody/vault/
104
+ !.kody/vault/**
105
+ ```
106
+
94
107
  ### `release`
95
108
 
96
109
  - `--mode prepare` — bumps `package.json`, updates `CHANGELOG.md`, opens a `release/vX.Y.Z` PR. `--bump patch|minor|major` (default `patch`).
package/dist/bin/kody.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@kody-ade/kody-engine",
6
- version: "0.3.46",
6
+ version: "0.3.48",
7
7
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
8
8
  license: "MIT",
9
9
  type: "module",
@@ -50,7 +50,7 @@ var package_default = {
50
50
  };
51
51
 
52
52
  // src/chat-cli.ts
53
- import { execFileSync as execFileSync24 } from "child_process";
53
+ import { execFileSync as execFileSync25 } from "child_process";
54
54
  import * as fs26 from "fs";
55
55
  import * as path23 from "path";
56
56
 
@@ -605,7 +605,7 @@ async function emit(sink, type, sessionId, suffix, payload) {
605
605
  }
606
606
 
607
607
  // src/kody-cli.ts
608
- import { execFileSync as execFileSync23 } from "child_process";
608
+ import { execFileSync as execFileSync24 } from "child_process";
609
609
  import * as fs25 from "fs";
610
610
  import * as path22 from "path";
611
611
 
@@ -2774,6 +2774,35 @@ function parsePr(url) {
2774
2774
  return Number.isFinite(n) ? n : null;
2775
2775
  }
2776
2776
 
2777
+ // src/scripts/dispatchClassified.ts
2778
+ import { execFileSync as execFileSync8 } from "child_process";
2779
+ var API_TIMEOUT_MS4 = 3e4;
2780
+ var VALID_CLASSES2 = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
2781
+ var dispatchClassified = async (ctx) => {
2782
+ const issueNumber = ctx.args.issue;
2783
+ if (!issueNumber) return;
2784
+ const classification = ctx.data.classification;
2785
+ if (!classification || !VALID_CLASSES2.has(classification)) return;
2786
+ try {
2787
+ execFileSync8("gh", ["issue", "comment", String(issueNumber), "--body", `@kody ${classification}`], {
2788
+ cwd: ctx.cwd,
2789
+ timeout: API_TIMEOUT_MS4,
2790
+ stdio: ["ignore", "pipe", "pipe"]
2791
+ });
2792
+ } catch (err) {
2793
+ process.stderr.write(
2794
+ `[kody dispatchClassified] failed to dispatch @kody ${classification}: ${err instanceof Error ? err.message : String(err)}
2795
+ `
2796
+ );
2797
+ ctx.data.action = failedAction("dispatch post failed");
2798
+ ctx.output.exitCode = 1;
2799
+ ctx.output.reason = "classify: dispatch failed";
2800
+ }
2801
+ };
2802
+ function failedAction(reason) {
2803
+ return { type: "CLASSIFY_FAILED", payload: { reason }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
2804
+ }
2805
+
2777
2806
  // src/scripts/dispatchMissionFileTicks.ts
2778
2807
  import * as fs16 from "fs";
2779
2808
  import * as path15 from "path";
@@ -2833,17 +2862,17 @@ function listMissionSlugs(absDir) {
2833
2862
  }
2834
2863
 
2835
2864
  // src/issue.ts
2836
- import { execFileSync as execFileSync8 } from "child_process";
2837
- var API_TIMEOUT_MS4 = 3e4;
2865
+ import { execFileSync as execFileSync9 } from "child_process";
2866
+ var API_TIMEOUT_MS5 = 3e4;
2838
2867
  function ghToken2() {
2839
2868
  return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
2840
2869
  }
2841
2870
  function gh2(args, options) {
2842
2871
  const token = ghToken2();
2843
2872
  const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
2844
- return execFileSync8("gh", args, {
2873
+ return execFileSync9("gh", args, {
2845
2874
  encoding: "utf-8",
2846
- timeout: API_TIMEOUT_MS4,
2875
+ timeout: API_TIMEOUT_MS5,
2847
2876
  cwd: options?.cwd,
2848
2877
  env,
2849
2878
  input: options?.input,
@@ -3214,18 +3243,7 @@ var ensureMemorizePr = async (ctx) => {
3214
3243
  }
3215
3244
  try {
3216
3245
  const output = gh2(
3217
- [
3218
- "pr",
3219
- "create",
3220
- "--head",
3221
- branch,
3222
- "--base",
3223
- ctx.config.git.defaultBranch,
3224
- "--title",
3225
- title,
3226
- "--body-file",
3227
- "-"
3228
- ],
3246
+ ["pr", "create", "--head", branch, "--base", ctx.config.git.defaultBranch, "--title", title, "--body-file", "-"],
3229
3247
  { input: body, cwd: ctx.cwd }
3230
3248
  );
3231
3249
  const url = output.trim();
@@ -3321,7 +3339,7 @@ function computeFailureReason(ctx) {
3321
3339
  }
3322
3340
 
3323
3341
  // src/scripts/finishFlow.ts
3324
- import { execFileSync as execFileSync9 } from "child_process";
3342
+ import { execFileSync as execFileSync10 } from "child_process";
3325
3343
 
3326
3344
  // src/lifecycleLabels.ts
3327
3345
  var KODY_NAMESPACE = "kody";
@@ -3441,7 +3459,7 @@ function errMsg(err) {
3441
3459
  }
3442
3460
 
3443
3461
  // src/scripts/finishFlow.ts
3444
- var API_TIMEOUT_MS5 = 3e4;
3462
+ var API_TIMEOUT_MS6 = 3e4;
3445
3463
  var STATUS_ICON = {
3446
3464
  "review-passed": "\u2705",
3447
3465
  "fix-applied": "\u2705",
@@ -3474,8 +3492,8 @@ var finishFlow = async (ctx, _profile, _agentResult, args) => {
3474
3492
  **PR:** ${state.core.prUrl}` : "";
3475
3493
  const body = `${icon} kody flow \`${flowName}\` finished \u2014 \`${reason}\`${prSuffix}`;
3476
3494
  try {
3477
- execFileSync9("gh", ["issue", "comment", String(issueNumber), "--body", body], {
3478
- timeout: API_TIMEOUT_MS5,
3495
+ execFileSync10("gh", ["issue", "comment", String(issueNumber), "--body", body], {
3496
+ timeout: API_TIMEOUT_MS6,
3479
3497
  cwd: ctx.cwd,
3480
3498
  stdio: ["ignore", "pipe", "pipe"]
3481
3499
  });
@@ -3488,7 +3506,7 @@ var finishFlow = async (ctx, _profile, _agentResult, args) => {
3488
3506
  };
3489
3507
 
3490
3508
  // src/branch.ts
3491
- import { execFileSync as execFileSync10 } from "child_process";
3509
+ import { execFileSync as execFileSync11 } from "child_process";
3492
3510
  var UncommittedChangesError = class extends Error {
3493
3511
  constructor(branch) {
3494
3512
  super(`Uncommitted changes on branch '${branch}' \u2014 refusing to run to protect work in progress`);
@@ -3498,7 +3516,7 @@ var UncommittedChangesError = class extends Error {
3498
3516
  branch;
3499
3517
  };
3500
3518
  function git2(args, cwd) {
3501
- return execFileSync10("git", args, {
3519
+ return execFileSync11("git", args, {
3502
3520
  encoding: "utf-8",
3503
3521
  timeout: 3e4,
3504
3522
  cwd,
@@ -3523,7 +3541,7 @@ function checkoutPrBranch(prNumber, cwd) {
3523
3541
  SKIP_HOOKS: "1",
3524
3542
  GH_TOKEN: process.env.GH_PAT?.trim() || process.env.GH_TOKEN || ""
3525
3543
  };
3526
- execFileSync10("gh", ["pr", "checkout", String(prNumber)], {
3544
+ execFileSync11("gh", ["pr", "checkout", String(prNumber)], {
3527
3545
  cwd,
3528
3546
  env,
3529
3547
  stdio: ["ignore", "pipe", "pipe"],
@@ -3590,7 +3608,7 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd) {
3590
3608
  }
3591
3609
 
3592
3610
  // src/gha.ts
3593
- import { execFileSync as execFileSync11 } from "child_process";
3611
+ import { execFileSync as execFileSync12 } from "child_process";
3594
3612
  import * as fs17 from "fs";
3595
3613
  function getRunUrl() {
3596
3614
  const server = process.env.GITHUB_SERVER_URL;
@@ -3633,7 +3651,7 @@ function reactToTriggerComment(cwd) {
3633
3651
  for (let attempt = 0; attempt < 3; attempt++) {
3634
3652
  if (attempt > 0) sleepMs(attempt === 1 ? 500 : 1500);
3635
3653
  try {
3636
- execFileSync11("gh", args, opts);
3654
+ execFileSync12("gh", args, opts);
3637
3655
  return;
3638
3656
  } catch (err) {
3639
3657
  lastErr = err;
@@ -3646,13 +3664,13 @@ function reactToTriggerComment(cwd) {
3646
3664
  }
3647
3665
  function sleepMs(ms) {
3648
3666
  try {
3649
- execFileSync11("sleep", [(ms / 1e3).toString()], { stdio: "ignore", timeout: ms + 1e3 });
3667
+ execFileSync12("sleep", [(ms / 1e3).toString()], { stdio: "ignore", timeout: ms + 1e3 });
3650
3668
  } catch {
3651
3669
  }
3652
3670
  }
3653
3671
 
3654
3672
  // src/workflow.ts
3655
- import { execFileSync as execFileSync12 } from "child_process";
3673
+ import { execFileSync as execFileSync13 } from "child_process";
3656
3674
  var GH_TIMEOUT_MS = 3e4;
3657
3675
  function ghToken3() {
3658
3676
  return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
@@ -3660,7 +3678,7 @@ function ghToken3() {
3660
3678
  function gh3(args, cwd) {
3661
3679
  const token = ghToken3();
3662
3680
  const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
3663
- return execFileSync12("gh", args, {
3681
+ return execFileSync13("gh", args, {
3664
3682
  encoding: "utf-8",
3665
3683
  timeout: GH_TIMEOUT_MS,
3666
3684
  cwd,
@@ -3844,7 +3862,7 @@ function tryPostPr2(prNumber, body, cwd) {
3844
3862
  }
3845
3863
 
3846
3864
  // src/scripts/initFlow.ts
3847
- import { execFileSync as execFileSync13 } from "child_process";
3865
+ import { execFileSync as execFileSync14 } from "child_process";
3848
3866
  import * as fs19 from "fs";
3849
3867
  import * as path17 from "path";
3850
3868
 
@@ -3885,7 +3903,7 @@ function qualityCommandsFor(pm) {
3885
3903
  function detectOwnerRepo(cwd) {
3886
3904
  let url;
3887
3905
  try {
3888
- url = execFileSync13("git", ["remote", "get-url", "origin"], {
3906
+ url = execFileSync14("git", ["remote", "get-url", "origin"], {
3889
3907
  cwd,
3890
3908
  encoding: "utf-8",
3891
3909
  stdio: ["ignore", "pipe", "pipe"]
@@ -3970,7 +3988,7 @@ jobs:
3970
3988
  `;
3971
3989
  function defaultBranchFromGit(cwd) {
3972
3990
  try {
3973
- const ref = execFileSync13("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
3991
+ const ref = execFileSync14("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
3974
3992
  cwd,
3975
3993
  encoding: "utf-8",
3976
3994
  stdio: ["ignore", "pipe", "pipe"]
@@ -3978,7 +3996,7 @@ function defaultBranchFromGit(cwd) {
3978
3996
  return ref.replace("refs/remotes/origin/", "");
3979
3997
  } catch {
3980
3998
  try {
3981
- return execFileSync13("git", ["branch", "--show-current"], {
3999
+ return execFileSync14("git", ["branch", "--show-current"], {
3982
4000
  cwd,
3983
4001
  encoding: "utf-8",
3984
4002
  stdio: ["ignore", "pipe", "pipe"]
@@ -4567,7 +4585,12 @@ function sortByRecency(pages) {
4567
4585
  }
4568
4586
  function formatBlock(pages) {
4569
4587
  if (pages.length === 0) return "";
4570
- const lines = ["# Project memory (`.kody/vault/`)", "", "Pages from prior memorize ticks. Treat as advisory context \u2014 confirm against the codebase before acting.", ""];
4588
+ const lines = [
4589
+ "# Project memory (`.kody/vault/`)",
4590
+ "",
4591
+ "Pages from prior memorize ticks. Treat as advisory context \u2014 confirm against the codebase before acting.",
4592
+ ""
4593
+ ];
4571
4594
  let total = 0;
4572
4595
  for (const p of pages) {
4573
4596
  const block = [`## ${p.title} \u2014 \`${p.relPath}\``, "", p.content].join("\n");
@@ -4582,7 +4605,7 @@ function formatBlock(pages) {
4582
4605
  return lines.join("\n");
4583
4606
  }
4584
4607
  function walkMd(root, visit) {
4585
- let stack = [root];
4608
+ const stack = [root];
4586
4609
  while (stack.length > 0) {
4587
4610
  const dir = stack.pop();
4588
4611
  let names;
@@ -4610,7 +4633,7 @@ function walkMd(root, visit) {
4610
4633
  }
4611
4634
 
4612
4635
  // src/scripts/memorizeFlow.ts
4613
- import { execFileSync as execFileSync14 } from "child_process";
4636
+ import { execFileSync as execFileSync15 } from "child_process";
4614
4637
  import * as fs22 from "fs";
4615
4638
  import * as path20 from "path";
4616
4639
  var VAULT_DIR_RELATIVE2 = ".kody/vault";
@@ -4637,8 +4660,10 @@ var memorizeFlow = async (ctx) => {
4637
4660
  process.stdout.write(`[kody memorize] no merged PRs since ${sinceIso}; agent may emit no changes
4638
4661
  `);
4639
4662
  } else {
4640
- process.stdout.write(`[kody memorize] ${recent.length} merged PR(s) since ${sinceIso} \u2192 branch ${ctx.data.branch}
4641
- `);
4663
+ process.stdout.write(
4664
+ `[kody memorize] ${recent.length} merged PR(s) since ${sinceIso} \u2192 branch ${ctx.data.branch}
4665
+ `
4666
+ );
4642
4667
  }
4643
4668
  };
4644
4669
  function ensureBranch(ctx, vaultAbs) {
@@ -4764,7 +4789,7 @@ function formatVaultIndex(vaultAbs) {
4764
4789
  }
4765
4790
  function walkMd2(root, visit) {
4766
4791
  if (!fs22.existsSync(root)) return;
4767
- let stack = [root];
4792
+ const stack = [root];
4768
4793
  while (stack.length > 0) {
4769
4794
  const dir = stack.pop();
4770
4795
  let names;
@@ -4791,7 +4816,7 @@ function walkMd2(root, visit) {
4791
4816
  }
4792
4817
  }
4793
4818
  function git3(args, cwd) {
4794
- return execFileSync14("git", args, {
4819
+ return execFileSync15("git", args, {
4795
4820
  encoding: "utf-8",
4796
4821
  timeout: 3e4,
4797
4822
  cwd,
@@ -4801,8 +4826,8 @@ function git3(args, cwd) {
4801
4826
  }
4802
4827
 
4803
4828
  // src/scripts/mergeReleasePr.ts
4804
- import { execFileSync as execFileSync15 } from "child_process";
4805
- var API_TIMEOUT_MS6 = 6e4;
4829
+ import { execFileSync as execFileSync16 } from "child_process";
4830
+ var API_TIMEOUT_MS7 = 6e4;
4806
4831
  var mergeReleasePr = async (ctx) => {
4807
4832
  const state = ctx.data.taskState;
4808
4833
  const prUrl = state?.core.prUrl;
@@ -4818,8 +4843,8 @@ var mergeReleasePr = async (ctx) => {
4818
4843
  return;
4819
4844
  }
4820
4845
  try {
4821
- execFileSync15("gh", ["pr", "merge", String(prNumber), "--merge"], {
4822
- timeout: API_TIMEOUT_MS6,
4846
+ execFileSync16("gh", ["pr", "merge", String(prNumber), "--merge"], {
4847
+ timeout: API_TIMEOUT_MS7,
4823
4848
  cwd: ctx.cwd,
4824
4849
  stdio: ["ignore", "pipe", "pipe"]
4825
4850
  });
@@ -5082,86 +5107,6 @@ var persistFlowState = async (ctx) => {
5082
5107
  }
5083
5108
  };
5084
5109
 
5085
- // src/scripts/postClassification.ts
5086
- import { execFileSync as execFileSync16 } from "child_process";
5087
- var API_TIMEOUT_MS7 = 3e4;
5088
- var VALID_CLASSES2 = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
5089
- var postClassification = async (ctx) => {
5090
- const issueNumber = ctx.args.issue;
5091
- if (!issueNumber) return;
5092
- const presetClassification = ctx.data.classification;
5093
- let classification = null;
5094
- let reason = null;
5095
- if (presetClassification && VALID_CLASSES2.has(presetClassification)) {
5096
- classification = presetClassification;
5097
- reason = ctx.data.classificationReason ?? "label-based match";
5098
- } else {
5099
- const parsed = parseClassification(ctx.data.prSummary ?? "");
5100
- classification = parsed?.classification ?? null;
5101
- reason = parsed?.reason ?? null;
5102
- }
5103
- if (!classification) {
5104
- ctx.data.action = failedAction("classification missing or invalid");
5105
- tryAuditComment(
5106
- issueNumber,
5107
- "\u26A0\uFE0F kody classifier could not decide \u2014 please re-run with an explicit `@kody <type>`.",
5108
- ctx.cwd
5109
- );
5110
- ctx.output.exitCode = 1;
5111
- ctx.output.reason = "classify: no decision";
5112
- return;
5113
- }
5114
- tryAuditComment(issueNumber, `\u{1F50E} kody classified as \`${classification}\`${reason ? ` \u2014 ${reason}` : ""}`, ctx.cwd);
5115
- try {
5116
- execFileSync16("gh", ["issue", "comment", String(issueNumber), "--body", `@kody ${classification}`], {
5117
- cwd: ctx.cwd,
5118
- timeout: API_TIMEOUT_MS7,
5119
- stdio: ["ignore", "pipe", "pipe"]
5120
- });
5121
- } catch (err) {
5122
- process.stderr.write(
5123
- `[kody postClassification] failed to dispatch @kody ${classification}: ${err instanceof Error ? err.message : String(err)}
5124
- `
5125
- );
5126
- ctx.data.action = failedAction("dispatch post failed");
5127
- ctx.output.exitCode = 1;
5128
- ctx.output.reason = "classify: dispatch failed";
5129
- return;
5130
- }
5131
- ctx.data.action = makeAction3(`CLASSIFIED_AS_${classification.toUpperCase()}`, {
5132
- classification,
5133
- reason: reason ?? "",
5134
- source: ctx.data.classificationSource ?? "agent"
5135
- });
5136
- ctx.data.classification = classification;
5137
- ctx.data.classificationReason = reason ?? "";
5138
- };
5139
- function parseClassification(prSummary) {
5140
- if (!prSummary) return null;
5141
- const classMatch = prSummary.match(/classification:\s*(feature|bug|spec|chore)\b/i);
5142
- if (!classMatch) return null;
5143
- const classification = classMatch[1].toLowerCase();
5144
- const reasonMatch = prSummary.match(/reason:\s*(.+)$/im);
5145
- const reason = reasonMatch ? reasonMatch[1].trim() : "";
5146
- return { classification, reason };
5147
- }
5148
- function tryAuditComment(issueNumber, body, cwd) {
5149
- try {
5150
- execFileSync16("gh", ["issue", "comment", String(issueNumber), "--body", body], {
5151
- cwd,
5152
- timeout: API_TIMEOUT_MS7,
5153
- stdio: ["ignore", "pipe", "pipe"]
5154
- });
5155
- } catch {
5156
- }
5157
- }
5158
- function makeAction3(type, payload) {
5159
- return { type, payload, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
5160
- }
5161
- function failedAction(reason) {
5162
- return { type: "CLASSIFY_FAILED", payload: { reason }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
5163
- }
5164
-
5165
5110
  // src/scripts/postIssueComment.ts
5166
5111
  var postIssueComment2 = async (ctx) => {
5167
5112
  if (ctx.skipAgent && ctx.output.exitCode !== void 0) return;
@@ -5330,6 +5275,70 @@ REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.
5330
5275
  );
5331
5276
  };
5332
5277
 
5278
+ // src/scripts/recordClassification.ts
5279
+ import { execFileSync as execFileSync17 } from "child_process";
5280
+ var API_TIMEOUT_MS8 = 3e4;
5281
+ var VALID_CLASSES3 = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
5282
+ var recordClassification = async (ctx) => {
5283
+ const issueNumber = ctx.args.issue;
5284
+ if (!issueNumber) return;
5285
+ const presetClassification = ctx.data.classification;
5286
+ let classification = null;
5287
+ let reason = null;
5288
+ if (presetClassification && VALID_CLASSES3.has(presetClassification)) {
5289
+ classification = presetClassification;
5290
+ reason = ctx.data.classificationReason ?? "label-based match";
5291
+ } else {
5292
+ const parsed = parseClassification(ctx.data.prSummary ?? "");
5293
+ classification = parsed?.classification ?? null;
5294
+ reason = parsed?.reason ?? null;
5295
+ }
5296
+ if (!classification) {
5297
+ ctx.data.action = failedAction3("classification missing or invalid");
5298
+ tryAuditComment(
5299
+ issueNumber,
5300
+ "\u26A0\uFE0F kody classifier could not decide \u2014 please re-run with an explicit `@kody <type>`.",
5301
+ ctx.cwd
5302
+ );
5303
+ ctx.output.exitCode = 1;
5304
+ ctx.output.reason = "classify: no decision";
5305
+ return;
5306
+ }
5307
+ tryAuditComment(issueNumber, `\u{1F50E} kody classified as \`${classification}\`${reason ? ` \u2014 ${reason}` : ""}`, ctx.cwd);
5308
+ ctx.data.action = makeAction3(`CLASSIFIED_AS_${classification.toUpperCase()}`, {
5309
+ classification,
5310
+ reason: reason ?? "",
5311
+ source: ctx.data.classificationSource ?? "agent"
5312
+ });
5313
+ ctx.data.classification = classification;
5314
+ ctx.data.classificationReason = reason ?? "";
5315
+ };
5316
+ function parseClassification(prSummary) {
5317
+ if (!prSummary) return null;
5318
+ const classMatch = prSummary.match(/classification:\s*(feature|bug|spec|chore)\b/i);
5319
+ if (!classMatch) return null;
5320
+ const classification = classMatch[1].toLowerCase();
5321
+ const reasonMatch = prSummary.match(/reason:\s*(.+)$/im);
5322
+ const reason = reasonMatch ? reasonMatch[1].trim() : "";
5323
+ return { classification, reason };
5324
+ }
5325
+ function tryAuditComment(issueNumber, body, cwd) {
5326
+ try {
5327
+ execFileSync17("gh", ["issue", "comment", String(issueNumber), "--body", body], {
5328
+ cwd,
5329
+ timeout: API_TIMEOUT_MS8,
5330
+ stdio: ["ignore", "pipe", "pipe"]
5331
+ });
5332
+ } catch {
5333
+ }
5334
+ }
5335
+ function makeAction3(type, payload) {
5336
+ return { type, payload, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
5337
+ }
5338
+ function failedAction3(reason) {
5339
+ return { type: "CLASSIFY_FAILED", payload: { reason }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
5340
+ }
5341
+
5333
5342
  // src/scripts/recordOutcome.ts
5334
5343
  var recordOutcome = async (ctx, profile) => {
5335
5344
  const seg = profile.name.replace(/-/g, "_").toUpperCase();
@@ -5365,12 +5374,12 @@ function fail(ctx, profile, reason) {
5365
5374
  ctx.data.agentDone = false;
5366
5375
  ctx.data.agentFailureReason = reason;
5367
5376
  const modeSeg = profile.name.replace(/-/g, "_").toUpperCase();
5368
- const failedAction3 = {
5377
+ const failedAction4 = {
5369
5378
  type: `${modeSeg}_FAILED`,
5370
5379
  payload: { reason },
5371
5380
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5372
5381
  };
5373
- ctx.data.action = failedAction3;
5382
+ ctx.data.action = failedAction4;
5374
5383
  }
5375
5384
  function countActionItems(block) {
5376
5385
  if (!block.trim()) return 0;
@@ -5411,12 +5420,12 @@ function fail2(ctx, profile, reason) {
5411
5420
  ctx.data.agentDone = false;
5412
5421
  ctx.data.agentFailureReason = reason;
5413
5422
  const modeSeg = profile.name.replace(/-/g, "_").toUpperCase();
5414
- const failedAction3 = {
5423
+ const failedAction4 = {
5415
5424
  type: `${modeSeg}_FAILED`,
5416
5425
  payload: { reason },
5417
5426
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5418
5427
  };
5419
- ctx.data.action = failedAction3;
5428
+ ctx.data.action = failedAction4;
5420
5429
  }
5421
5430
 
5422
5431
  // src/scripts/resolveArtifacts.ts
@@ -5443,7 +5452,7 @@ var resolveArtifacts = async (ctx, profile) => {
5443
5452
  };
5444
5453
 
5445
5454
  // src/scripts/resolveFlow.ts
5446
- import { execFileSync as execFileSync17 } from "child_process";
5455
+ import { execFileSync as execFileSync18 } from "child_process";
5447
5456
  var CONFLICT_DIFF_MAX_BYTES = 4e4;
5448
5457
  var resolveFlow = async (ctx) => {
5449
5458
  const prNumber = ctx.args.pr;
@@ -5513,7 +5522,7 @@ function buildPreferBlock(prefer, baseBranch) {
5513
5522
  }
5514
5523
  function getConflictedFiles(cwd) {
5515
5524
  try {
5516
- const out = execFileSync17("git", ["diff", "--name-only", "--diff-filter=U"], {
5525
+ const out = execFileSync18("git", ["diff", "--name-only", "--diff-filter=U"], {
5517
5526
  encoding: "utf-8",
5518
5527
  cwd,
5519
5528
  env: { ...process.env, HUSKY: "0" }
@@ -5528,7 +5537,7 @@ function getConflictMarkersPreview(files, cwd, maxBytes = CONFLICT_DIFF_MAX_BYTE
5528
5537
  let total = 0;
5529
5538
  for (const f of files) {
5530
5539
  try {
5531
- const content = execFileSync17("cat", [f], { encoding: "utf-8", cwd }).toString();
5540
+ const content = execFileSync18("cat", [f], { encoding: "utf-8", cwd }).toString();
5532
5541
  const snippet = `### ${f}
5533
5542
 
5534
5543
  \`\`\`
@@ -5702,11 +5711,11 @@ var skipAgent = async (ctx) => {
5702
5711
  };
5703
5712
 
5704
5713
  // src/scripts/stageMergeConflicts.ts
5705
- import { execFileSync as execFileSync18 } from "child_process";
5714
+ import { execFileSync as execFileSync19 } from "child_process";
5706
5715
  var stageMergeConflicts = async (ctx) => {
5707
5716
  if (ctx.data.agentDone === false) return;
5708
5717
  try {
5709
- execFileSync18("git", ["add", "-A"], {
5718
+ execFileSync19("git", ["add", "-A"], {
5710
5719
  cwd: ctx.cwd,
5711
5720
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
5712
5721
  stdio: "pipe"
@@ -5716,8 +5725,8 @@ var stageMergeConflicts = async (ctx) => {
5716
5725
  };
5717
5726
 
5718
5727
  // src/scripts/startFlow.ts
5719
- import { execFileSync as execFileSync19 } from "child_process";
5720
- var API_TIMEOUT_MS8 = 3e4;
5728
+ import { execFileSync as execFileSync20 } from "child_process";
5729
+ var API_TIMEOUT_MS9 = 3e4;
5721
5730
  var startFlow = async (ctx, profile, _agentResult, args) => {
5722
5731
  const entry = args?.entry;
5723
5732
  if (!entry) {
@@ -5750,8 +5759,8 @@ function postKodyComment(target, issueNumber, state, next, cwd) {
5750
5759
  const sub = target === "pr" && state?.core.prUrl ? "pr" : "issue";
5751
5760
  const body = `@kody ${next}`;
5752
5761
  try {
5753
- execFileSync19("gh", [sub, "comment", String(targetNumber), "--body", body], {
5754
- timeout: API_TIMEOUT_MS8,
5762
+ execFileSync20("gh", [sub, "comment", String(targetNumber), "--body", body], {
5763
+ timeout: API_TIMEOUT_MS9,
5755
5764
  cwd,
5756
5765
  stdio: ["ignore", "pipe", "pipe"]
5757
5766
  });
@@ -5764,7 +5773,7 @@ function postKodyComment(target, issueNumber, state, next, cwd) {
5764
5773
  }
5765
5774
 
5766
5775
  // src/scripts/syncFlow.ts
5767
- import { execFileSync as execFileSync20 } from "child_process";
5776
+ import { execFileSync as execFileSync21 } from "child_process";
5768
5777
  var syncFlow = async (ctx, _profile, args) => {
5769
5778
  const announceOnSuccess = Boolean(args?.announceOnSuccess);
5770
5779
  const prNumber = ctx.args.pr;
@@ -5836,7 +5845,7 @@ function bail2(ctx, prNumber, reason) {
5836
5845
  }
5837
5846
  function revParseHead(cwd) {
5838
5847
  try {
5839
- return execFileSync20("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
5848
+ return execFileSync21("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
5840
5849
  } catch {
5841
5850
  return "";
5842
5851
  }
@@ -5844,9 +5853,9 @@ function revParseHead(cwd) {
5844
5853
  function pushBranch(branch, cwd) {
5845
5854
  const env = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
5846
5855
  try {
5847
- execFileSync20("git", ["push", "-u", "origin", branch], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
5856
+ execFileSync21("git", ["push", "-u", "origin", branch], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
5848
5857
  } catch {
5849
- execFileSync20("git", ["push", "--force-with-lease", "-u", "origin", branch], {
5858
+ execFileSync21("git", ["push", "--force-with-lease", "-u", "origin", branch], {
5850
5859
  cwd,
5851
5860
  env,
5852
5861
  stdio: ["ignore", "pipe", "pipe"]
@@ -5957,8 +5966,8 @@ var verify = async (ctx) => {
5957
5966
  };
5958
5967
 
5959
5968
  // src/scripts/waitForCi.ts
5960
- import { execFileSync as execFileSync21 } from "child_process";
5961
- var API_TIMEOUT_MS9 = 3e4;
5969
+ import { execFileSync as execFileSync22 } from "child_process";
5970
+ var API_TIMEOUT_MS10 = 3e4;
5962
5971
  var waitForCi = async (ctx, _profile, _agentResult, args) => {
5963
5972
  const timeoutMinutes = numArg(args, "timeoutMinutes", 30);
5964
5973
  const pollSeconds = numArg(args, "pollSeconds", 30);
@@ -6035,9 +6044,9 @@ var waitForCi = async (ctx, _profile, _agentResult, args) => {
6035
6044
  };
6036
6045
  function fetchChecks(prNumber, cwd) {
6037
6046
  try {
6038
- const raw = execFileSync21("gh", ["pr", "checks", String(prNumber), "--json", "bucket,state,name,workflow,link"], {
6047
+ const raw = execFileSync22("gh", ["pr", "checks", String(prNumber), "--json", "bucket,state,name,workflow,link"], {
6039
6048
  encoding: "utf-8",
6040
- timeout: API_TIMEOUT_MS9,
6049
+ timeout: API_TIMEOUT_MS10,
6041
6050
  cwd,
6042
6051
  stdio: ["ignore", "pipe", "pipe"]
6043
6052
  });
@@ -6299,7 +6308,8 @@ var postflightScripts = {
6299
6308
  finishFlow,
6300
6309
  advanceFlow,
6301
6310
  persistFlowState,
6302
- postClassification,
6311
+ recordClassification,
6312
+ dispatchClassified,
6303
6313
  notifyTerminal,
6304
6314
  recordOutcome,
6305
6315
  mergeReleasePr,
@@ -6311,7 +6321,7 @@ var allScriptNames = /* @__PURE__ */ new Set([
6311
6321
  ]);
6312
6322
 
6313
6323
  // src/tools.ts
6314
- import { execFileSync as execFileSync22 } from "child_process";
6324
+ import { execFileSync as execFileSync23 } from "child_process";
6315
6325
  function verifyCliTools(tools, cwd) {
6316
6326
  const out = [];
6317
6327
  for (const t of tools) out.push(verifyOne(t, cwd));
@@ -6344,7 +6354,7 @@ function verifyOne(tool, cwd) {
6344
6354
  }
6345
6355
  function runShell(cmd, cwd, timeoutMs = 3e4) {
6346
6356
  try {
6347
- execFileSync22("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
6357
+ execFileSync23("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
6348
6358
  return true;
6349
6359
  } catch {
6350
6360
  return false;
@@ -6753,7 +6763,7 @@ function detectPackageManager2(cwd) {
6753
6763
  }
6754
6764
  function shellOut(cmd, args, cwd, stream = true) {
6755
6765
  try {
6756
- execFileSync23(cmd, args, {
6766
+ execFileSync24(cmd, args, {
6757
6767
  cwd,
6758
6768
  stdio: stream ? "inherit" : "pipe",
6759
6769
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" }
@@ -6766,7 +6776,7 @@ function shellOut(cmd, args, cwd, stream = true) {
6766
6776
  }
6767
6777
  function isOnPath(bin) {
6768
6778
  try {
6769
- execFileSync23("which", [bin], { stdio: "pipe" });
6779
+ execFileSync24("which", [bin], { stdio: "pipe" });
6770
6780
  return true;
6771
6781
  } catch {
6772
6782
  return false;
@@ -6800,7 +6810,7 @@ function installLitellmIfNeeded(cwd) {
6800
6810
  } catch {
6801
6811
  }
6802
6812
  try {
6803
- execFileSync23("python3", ["-c", "import litellm"], { stdio: "pipe" });
6813
+ execFileSync24("python3", ["-c", "import litellm"], { stdio: "pipe" });
6804
6814
  process.stdout.write("\u2192 kody: litellm already installed\n");
6805
6815
  return 0;
6806
6816
  } catch {
@@ -6810,16 +6820,16 @@ function installLitellmIfNeeded(cwd) {
6810
6820
  }
6811
6821
  function configureGitIdentity(cwd) {
6812
6822
  try {
6813
- const name = execFileSync23("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
6823
+ const name = execFileSync24("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
6814
6824
  if (name) return;
6815
6825
  } catch {
6816
6826
  }
6817
6827
  try {
6818
- execFileSync23("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
6828
+ execFileSync24("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
6819
6829
  } catch {
6820
6830
  }
6821
6831
  try {
6822
- execFileSync23("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
6832
+ execFileSync24("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
6823
6833
  cwd,
6824
6834
  stdio: "pipe"
6825
6835
  });
@@ -7090,9 +7100,9 @@ function commitChatFiles(cwd, sessionId, verbose) {
7090
7100
  if (paths.length === 0) return;
7091
7101
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
7092
7102
  try {
7093
- execFileSync24("git", ["add", ...paths], opts);
7094
- execFileSync24("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
7095
- execFileSync24("git", ["push", "--quiet", "origin", "HEAD"], opts);
7103
+ execFileSync25("git", ["add", ...paths], opts);
7104
+ execFileSync25("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
7105
+ execFileSync25("git", ["push", "--quiet", "origin", "HEAD"], opts);
7096
7106
  } catch (err) {
7097
7107
  const msg = err instanceof Error ? err.message : String(err);
7098
7108
  process.stderr.write(`[kody:chat] commit/push skipped: ${msg}
@@ -47,9 +47,10 @@
47
47
  ],
48
48
  "postflight": [
49
49
  { "script": "parseAgentResult" },
50
- { "script": "postClassification" },
50
+ { "script": "recordClassification" },
51
51
  { "script": "writeRunSummary" },
52
- { "script": "saveTaskState" }
52
+ { "script": "saveTaskState" },
53
+ { "script": "dispatchClassified" }
53
54
  ]
54
55
  },
55
56
  "output": {
File without changes
File without changes
File without changes
File without changes
@@ -26,10 +26,11 @@ If a prior-art block is present above, READ THE DIFFS — those are failed or su
26
26
  - Read the **full** contents of every file you intend to change (not just a grep hit).
27
27
  - Read the tests for each of those files, if tests exist for the module.
28
28
  - Read at least one sibling module that already implements the same pattern you're about to follow — your edits should mirror an existing convention unless you can name why a new one is needed.
29
+ - If your change requires writing or modifying a test, also check for repo-level testing guidance: `tests/README.md`, `TESTING.md`, or a "Testing"/"Tests" section in `AGENTS.md`/`CLAUDE.md`. If one exists, treat its patterns (auth setup, fixture creation, what NOT to do) as authoritative — they override anything you might infer from grepping individual files.
29
30
  - If a file you need to read does not exist, say so explicitly in your plan (step 2). Do not guess at its contents.
30
31
  2. **Plan** — before any Edit/Write, output a short plan (5–10 lines): what files you'll change, the approach, what could go wrong. No fluff.
31
32
  3. **Build** — Edit/Write to implement the change. Stay within the plan; if you discover the plan was wrong, briefly say so and adjust.
32
- 4. **Test** — for every new module you added and every behavior you changed, write or update tests. If the plan above contains a "Test plan" section, treat it as authoritative: every item there must produce a corresponding test. Match the repo's existing test layout (look at `tests/` or sibling `*.test.ts` files in the codebase to see the convention). Cover at least one happy path and one failure path per change. Skipping tests is a hard failure. A change may only be declared untestable if you can name the specific blocker (e.g., "no fake exists for the X SDK and stubbing it would mock the entire call surface"); vague "this is just config" claims are rejected. Untestable changes go in `PLAN_DEVIATIONS:` with the named blocker.
33
+ 4. **Test** — for every new module you added and every behavior you changed, write or update tests. If the plan above contains a "Test plan" section, treat it as authoritative: every item there must produce a corresponding test. Before writing a test, open the newest existing file in the same test directory (`tests/int/`, `tests/unit/`, `tests/e2e/`, or sibling `*.test.ts`) and copy its imports, setup hooks, and auth pattern **verbatim**. Do NOT introduce a new test infrastructure (own testcontainers, `fetch` against relative URLs, alternate auth headers) when a working pattern already exists in that directory — divergence from the established pattern is a hard failure even if the test passes locally. Cover at least one happy path and one failure path per change. Skipping tests is a hard failure. A change may only be declared untestable if you can name the specific blocker (e.g., "no fake exists for the X SDK and stubbing it would mock the entire call surface"); vague "this is just config" claims are rejected. Untestable changes go in `PLAN_DEVIATIONS:` with the named blocker.
33
34
  5. **Verify** — run each quality command with Bash. On failure, fix the root cause and re-run. When reporting that a command passed, you MUST have just run it and seen exit code 0 in this session — do not paraphrase prior output.
34
35
  6. Your FINAL message must use this exact format (or a single `FAILED: <reason>` line on failure). The `PLAN_DEVIATIONS:` block is REQUIRED whenever a plan was provided.
35
36
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.3.46",
3
+ "version": "0.3.48",
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
  "@anthropic-ai/claude-agent-sdk": "0.2.119"
17
29
  },
@@ -31,16 +43,5 @@
31
43
  "url": "git+https://github.com/aharonyaircohen/kody-engine.git"
32
44
  },
33
45
  "homepage": "https://github.com/aharonyaircohen/kody-engine",
34
- "bugs": "https://github.com/aharonyaircohen/kody-engine/issues",
35
- "scripts": {
36
- "kody": "tsx bin/kody.ts",
37
- "build": "tsup && node scripts/copy-assets.cjs",
38
- "test": "vitest run tests/unit tests/int --no-coverage",
39
- "test:e2e": "vitest run tests/e2e --no-coverage",
40
- "test:all": "vitest run tests --no-coverage",
41
- "typecheck": "tsc --noEmit",
42
- "lint": "biome check",
43
- "lint:fix": "biome check --write",
44
- "format": "biome format --write"
45
- }
46
- }
46
+ "bugs": "https://github.com/aharonyaircohen/kody-engine/issues"
47
+ }
@@ -48,9 +48,13 @@ on:
48
48
  pull_request:
49
49
  types: [closed]
50
50
  schedule:
51
- # Wakes every 5 minutes; kody fans out to whichever scheduled executables
52
- # (mission-scheduler, memorize, watch-stale-prs, …) match this minute.
53
- - cron: "*/5 * * * *"
51
+ # Wakes every 30 minutes; kody fans out to whichever scheduled executables
52
+ # (mission-scheduler, memorize, watch-stale-prs, …) match this tick.
53
+ #
54
+ # `memorize` writes to `.kody/vault/` and opens a daily PR. If your
55
+ # `.gitignore` ignores `.kody/*`, add `!.kody/vault/` and `!.kody/vault/**`
56
+ # so memorize's pages are tracked.
57
+ - cron: "*/30 * * * *"
54
58
 
55
59
  jobs:
56
60
  run: