@kody-ade/kody-engine 0.2.55 → 0.2.57

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/kody2.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.2.55",
6
+ version: "0.2.57",
7
7
  description: "kody2 \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 execFileSync21 } from "child_process";
53
+ import { execFileSync as execFileSync22 } from "child_process";
54
54
  import * as fs22 from "fs";
55
55
  import * as path19 from "path";
56
56
 
@@ -543,7 +543,7 @@ async function emit(sink, type, sessionId, suffix, payload) {
543
543
  }
544
544
 
545
545
  // src/kody2-cli.ts
546
- import { execFileSync as execFileSync20 } from "child_process";
546
+ import { execFileSync as execFileSync21 } from "child_process";
547
547
  import * as fs21 from "fs";
548
548
  import * as path18 from "path";
549
549
 
@@ -4486,6 +4486,269 @@ function tryPostPr4(prNumber, body, cwd) {
4486
4486
  }
4487
4487
  }
4488
4488
 
4489
+ // src/scripts/riskGate.ts
4490
+ import { execFileSync as execFileSync17 } from "child_process";
4491
+ var ALL_GATES = ["secrets", "workflow-edit", "large-diff", "dep-change", "test-deletion"];
4492
+ var HARD_GATES = /* @__PURE__ */ new Set(["secrets"]);
4493
+ var APPROVE_ALL = "kody-approve:all";
4494
+ var GATED_LABEL = "kody:gated";
4495
+ var DEFAULT_MAX_FILES = 20;
4496
+ var DEFAULT_MAX_DELETIONS = 500;
4497
+ var riskGate = async (ctx, profile, _agent, args) => {
4498
+ const changedFiles = collectBranchChangedFiles(ctx);
4499
+ const gatesToRun = parseGates(args?.gates);
4500
+ const violations = evaluateGates(ctx, profile.name, changedFiles, gatesToRun, args);
4501
+ if (violations.length === 0) {
4502
+ ctx.data.riskGate = { violations: [], pending: [], decision: "allow" };
4503
+ return;
4504
+ }
4505
+ const targetType = ctx.data.commentTargetType;
4506
+ const targetNumber = Number(ctx.data.commentTargetNumber ?? 0);
4507
+ const labels = targetNumber > 0 ? getIssueLabels(targetNumber, ctx.cwd) : [];
4508
+ const approveAll = labels.includes(APPROVE_ALL);
4509
+ const pending = violations.filter((v) => !isApproved(v, labels, approveAll));
4510
+ ctx.data.riskGate = {
4511
+ violations,
4512
+ pending,
4513
+ decision: pending.length === 0 ? "allow" : "halt"
4514
+ };
4515
+ if (pending.length === 0 || !targetType || targetNumber <= 0) return;
4516
+ for (const v of pending) {
4517
+ ensureApproveLabel(v.name, ctx.cwd);
4518
+ }
4519
+ try {
4520
+ setKodyLabel(
4521
+ targetNumber,
4522
+ {
4523
+ label: GATED_LABEL,
4524
+ color: "fbca04",
4525
+ description: "kody2: awaiting human approval of risk gate(s)"
4526
+ },
4527
+ ctx.cwd
4528
+ );
4529
+ } catch {
4530
+ }
4531
+ const body = formatAdvisory(pending);
4532
+ try {
4533
+ if (targetType === "issue") postIssueComment(targetNumber, body, ctx.cwd);
4534
+ else postPrReviewComment(targetNumber, body, ctx.cwd);
4535
+ } catch {
4536
+ }
4537
+ if (!ctx.output.reason) {
4538
+ ctx.output.reason = `risk gate halt: ${pending.map((p) => p.name).join(", ")}`;
4539
+ }
4540
+ };
4541
+ function evaluateGates(ctx, profileName, changedFiles, gatesToRun, args) {
4542
+ const violations = [];
4543
+ if (gatesToRun.includes("secrets")) {
4544
+ const hits = changedFiles.filter(isSecretPath);
4545
+ if (hits.length > 0) {
4546
+ violations.push({
4547
+ name: "secrets",
4548
+ severity: "hard",
4549
+ reason: `secret/credential files touched: ${preview(hits)}`
4550
+ });
4551
+ }
4552
+ }
4553
+ if (gatesToRun.includes("workflow-edit")) {
4554
+ const hits = changedFiles.filter((f) => f.startsWith(".github/workflows/"));
4555
+ if (hits.length > 0) {
4556
+ violations.push({
4557
+ name: "workflow-edit",
4558
+ severity: "soft",
4559
+ reason: `CI workflow files modified: ${preview(hits)}`
4560
+ });
4561
+ }
4562
+ }
4563
+ if (gatesToRun.includes("large-diff")) {
4564
+ const maxFiles = toPositiveInt(args?.maxFiles, DEFAULT_MAX_FILES);
4565
+ const maxDeletions = toPositiveInt(args?.maxDeletions, DEFAULT_MAX_DELETIONS);
4566
+ if (changedFiles.length > maxFiles) {
4567
+ violations.push({
4568
+ name: "large-diff",
4569
+ severity: "soft",
4570
+ reason: `${changedFiles.length} files changed (threshold: ${maxFiles})`
4571
+ });
4572
+ } else {
4573
+ const stats = computeDiffStats(ctx);
4574
+ if (stats && stats.deletions > maxDeletions) {
4575
+ violations.push({
4576
+ name: "large-diff",
4577
+ severity: "soft",
4578
+ reason: `${stats.deletions} lines deleted (threshold: ${maxDeletions})`
4579
+ });
4580
+ }
4581
+ }
4582
+ }
4583
+ if (gatesToRun.includes("dep-change") && profileName !== "chore") {
4584
+ const hits = changedFiles.filter(isDepFile);
4585
+ if (hits.length > 0) {
4586
+ violations.push({
4587
+ name: "dep-change",
4588
+ severity: "soft",
4589
+ reason: `dependency/lockfile changes outside a chore flow: ${preview(hits)}`
4590
+ });
4591
+ }
4592
+ }
4593
+ if (gatesToRun.includes("test-deletion")) {
4594
+ const deleted = listDeletedFilesInHeadCommit(ctx.cwd).filter(isTestFile);
4595
+ if (deleted.length > 0) {
4596
+ violations.push({
4597
+ name: "test-deletion",
4598
+ severity: "soft",
4599
+ reason: `test files deleted: ${preview(deleted)}`
4600
+ });
4601
+ }
4602
+ }
4603
+ return violations;
4604
+ }
4605
+ function isApproved(v, labels, approveAll) {
4606
+ if (labels.includes(`kody-approve:${v.name}`)) return true;
4607
+ if (!HARD_GATES.has(v.name) && approveAll) return true;
4608
+ return false;
4609
+ }
4610
+ function parseGates(spec) {
4611
+ if (spec === void 0 || spec === null || spec === "") return ALL_GATES;
4612
+ const list = String(spec).split(",").map((s) => s.trim()).filter(Boolean);
4613
+ const valid = ALL_GATES;
4614
+ const matched = list.filter((n) => valid.includes(n));
4615
+ return matched.length > 0 ? matched : ALL_GATES;
4616
+ }
4617
+ function toPositiveInt(v, fallback) {
4618
+ const n = typeof v === "number" ? v : parseInt(String(v ?? ""), 10);
4619
+ return Number.isFinite(n) && n > 0 ? n : fallback;
4620
+ }
4621
+ function preview(list, max = 5) {
4622
+ if (list.length <= max) return list.join(", ");
4623
+ return `${list.slice(0, max).join(", ")} (+${list.length - max} more)`;
4624
+ }
4625
+ var SECRET_PATTERNS = [
4626
+ /(^|\/)\.env(\.|$)/i,
4627
+ /\.pem$/i,
4628
+ /\.key$/i,
4629
+ /(^|\/)(id_rsa|id_ed25519|id_ecdsa)(\.|$)/i,
4630
+ /credentials?(\.|\/|$)/i,
4631
+ /(^|\/)(private|secret)[^/]*\.json$/i,
4632
+ /(^|\/)\.netrc$/i,
4633
+ /(^|\/)\.npmrc$/i
4634
+ ];
4635
+ function isSecretPath(p) {
4636
+ return SECRET_PATTERNS.some((r) => r.test(p));
4637
+ }
4638
+ var DEP_FILES = /* @__PURE__ */ new Set([
4639
+ "package.json",
4640
+ "pnpm-lock.yaml",
4641
+ "package-lock.json",
4642
+ "yarn.lock",
4643
+ "requirements.txt",
4644
+ "Pipfile",
4645
+ "Pipfile.lock",
4646
+ "poetry.lock",
4647
+ "go.mod",
4648
+ "go.sum",
4649
+ "Cargo.toml",
4650
+ "Cargo.lock",
4651
+ "Gemfile",
4652
+ "Gemfile.lock"
4653
+ ]);
4654
+ function isDepFile(p) {
4655
+ return DEP_FILES.has(p.split("/").pop() ?? "");
4656
+ }
4657
+ function isTestFile(p) {
4658
+ return /(^|\/)(tests?|__tests__|spec)\//i.test(p) || /\.(test|spec)\.[a-z0-9]+$/i.test(p);
4659
+ }
4660
+ function collectBranchChangedFiles(ctx) {
4661
+ const base = ctx.config.git.defaultBranch;
4662
+ for (const ref of [`origin/${base}...HEAD`, `${base}...HEAD`]) {
4663
+ try {
4664
+ const out = execFileSync17("git", ["diff", "--name-only", ref], {
4665
+ cwd: ctx.cwd,
4666
+ encoding: "utf-8",
4667
+ stdio: ["pipe", "pipe", "pipe"]
4668
+ });
4669
+ const files = out.split("\n").map((s) => s.trim()).filter(Boolean);
4670
+ if (files.length > 0) return files;
4671
+ } catch {
4672
+ }
4673
+ }
4674
+ return ctx.data.changedFiles ?? [];
4675
+ }
4676
+ function computeDiffStats(ctx) {
4677
+ const base = ctx.config.git.defaultBranch;
4678
+ for (const ref of [`origin/${base}...HEAD`, `${base}...HEAD`]) {
4679
+ try {
4680
+ const out = execFileSync17("git", ["diff", "--shortstat", ref], {
4681
+ cwd: ctx.cwd,
4682
+ encoding: "utf-8",
4683
+ stdio: ["pipe", "pipe", "pipe"]
4684
+ }).trim();
4685
+ if (out) return parseShortstat(out);
4686
+ } catch {
4687
+ }
4688
+ }
4689
+ return null;
4690
+ }
4691
+ function parseShortstat(s) {
4692
+ const ins = /(\d+)\s+insertions?/.exec(s);
4693
+ const del = /(\d+)\s+deletions?/.exec(s);
4694
+ return {
4695
+ insertions: ins ? parseInt(ins[1], 10) : 0,
4696
+ deletions: del ? parseInt(del[1], 10) : 0
4697
+ };
4698
+ }
4699
+ function listDeletedFilesInHeadCommit(cwd) {
4700
+ try {
4701
+ const out = execFileSync17("git", ["show", "--name-status", "--pretty=format:", "HEAD"], {
4702
+ cwd,
4703
+ encoding: "utf-8",
4704
+ stdio: ["pipe", "pipe", "pipe"]
4705
+ });
4706
+ return out.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("D ")).map((l) => l.slice(2).trim()).filter(Boolean);
4707
+ } catch {
4708
+ return [];
4709
+ }
4710
+ }
4711
+ function ensureApproveLabel(gate, cwd) {
4712
+ try {
4713
+ gh2(
4714
+ [
4715
+ "label",
4716
+ "create",
4717
+ `kody-approve:${gate}`,
4718
+ "--force",
4719
+ "--color",
4720
+ "0e8a16",
4721
+ "--description",
4722
+ `kody2: approve the ${gate} risk gate and resume the flow`
4723
+ ],
4724
+ { cwd }
4725
+ );
4726
+ } catch {
4727
+ }
4728
+ }
4729
+ function formatAdvisory(pending) {
4730
+ const lines = [];
4731
+ lines.push("\u23F8\uFE0F **kody2 risk gate halted the flow.**");
4732
+ lines.push("");
4733
+ lines.push("The changes were committed and pushed, but the flow will **not progress** until a human approves:");
4734
+ lines.push("");
4735
+ for (const v of pending) {
4736
+ lines.push(`- **\`${v.name}\`** _(${v.severity})_ \u2014 ${v.reason}`);
4737
+ }
4738
+ lines.push("");
4739
+ lines.push("**To approve and resume**, add one of these labels to this issue/PR:");
4740
+ const perGate = pending.map((v) => `\`kody-approve:${v.name}\``).join(", ");
4741
+ lines.push(`- ${perGate}`);
4742
+ if (pending.some((v) => v.severity === "soft")) {
4743
+ lines.push("- `kody-approve:all` \u2014 bypass **soft** gates at once (hard gates still require their specific label)");
4744
+ }
4745
+ lines.push("");
4746
+ lines.push(
4747
+ "Then re-trigger kody2 (e.g. post a new `@kody2 \u2026` comment). The branch already holds the committed work, so the re-run resumes from the same point."
4748
+ );
4749
+ return lines.join("\n");
4750
+ }
4751
+
4489
4752
  // src/scripts/runFlow.ts
4490
4753
  var runFlow = async (ctx) => {
4491
4754
  const issueNumber = ctx.args.issue;
@@ -4582,7 +4845,7 @@ var skipAgent = async (ctx) => {
4582
4845
  };
4583
4846
 
4584
4847
  // src/scripts/startFlow.ts
4585
- import { execFileSync as execFileSync17 } from "child_process";
4848
+ import { execFileSync as execFileSync18 } from "child_process";
4586
4849
  var API_TIMEOUT_MS7 = 3e4;
4587
4850
  var startFlow = async (ctx, profile, _agentResult, args) => {
4588
4851
  const entry = args?.entry;
@@ -4616,7 +4879,7 @@ function postKody2Comment(target, issueNumber, state, next, cwd) {
4616
4879
  const sub = target === "pr" && state?.core.prUrl ? "pr" : "issue";
4617
4880
  const body = `@kody2 ${next}`;
4618
4881
  try {
4619
- execFileSync17("gh", [sub, "comment", String(targetNumber), "--body", body], {
4882
+ execFileSync18("gh", [sub, "comment", String(targetNumber), "--body", body], {
4620
4883
  timeout: API_TIMEOUT_MS7,
4621
4884
  cwd,
4622
4885
  stdio: ["ignore", "pipe", "pipe"]
@@ -4636,7 +4899,7 @@ function parsePr2(url) {
4636
4899
  }
4637
4900
 
4638
4901
  // src/scripts/syncFlow.ts
4639
- import { execFileSync as execFileSync18 } from "child_process";
4902
+ import { execFileSync as execFileSync19 } from "child_process";
4640
4903
  var syncFlow = async (ctx) => {
4641
4904
  ctx.skipAgent = true;
4642
4905
  const prNumber = ctx.args.pr;
@@ -4695,7 +4958,7 @@ function bail2(ctx, prNumber, reason) {
4695
4958
  }
4696
4959
  function revParseHead(cwd) {
4697
4960
  try {
4698
- return execFileSync18("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
4961
+ return execFileSync19("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
4699
4962
  } catch {
4700
4963
  return "";
4701
4964
  }
@@ -4703,9 +4966,9 @@ function revParseHead(cwd) {
4703
4966
  function pushBranch(branch, cwd) {
4704
4967
  const env = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
4705
4968
  try {
4706
- execFileSync18("git", ["push", "-u", "origin", branch], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
4969
+ execFileSync19("git", ["push", "-u", "origin", branch], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
4707
4970
  } catch {
4708
- execFileSync18("git", ["push", "--force-with-lease", "-u", "origin", branch], {
4971
+ execFileSync19("git", ["push", "--force-with-lease", "-u", "origin", branch], {
4709
4972
  cwd,
4710
4973
  env,
4711
4974
  stdio: ["ignore", "pipe", "pipe"]
@@ -4947,7 +5210,8 @@ var postflightScripts = {
4947
5210
  finishFlow,
4948
5211
  advanceFlow,
4949
5212
  persistFlowState,
4950
- postClassification
5213
+ postClassification,
5214
+ riskGate
4951
5215
  };
4952
5216
  var allScriptNames = /* @__PURE__ */ new Set([
4953
5217
  ...Object.keys(preflightScripts),
@@ -4955,7 +5219,7 @@ var allScriptNames = /* @__PURE__ */ new Set([
4955
5219
  ]);
4956
5220
 
4957
5221
  // src/tools.ts
4958
- import { execFileSync as execFileSync19 } from "child_process";
5222
+ import { execFileSync as execFileSync20 } from "child_process";
4959
5223
  function verifyCliTools(tools, cwd) {
4960
5224
  const out = [];
4961
5225
  for (const t of tools) out.push(verifyOne(t, cwd));
@@ -4988,7 +5252,7 @@ function verifyOne(tool, cwd) {
4988
5252
  }
4989
5253
  function runShell2(cmd, cwd, timeoutMs = 3e4) {
4990
5254
  try {
4991
- execFileSync19("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
5255
+ execFileSync20("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
4992
5256
  return true;
4993
5257
  } catch {
4994
5258
  return false;
@@ -5318,7 +5582,7 @@ function detectPackageManager2(cwd) {
5318
5582
  }
5319
5583
  function shellOut(cmd, args, cwd, stream = true) {
5320
5584
  try {
5321
- execFileSync20(cmd, args, {
5585
+ execFileSync21(cmd, args, {
5322
5586
  cwd,
5323
5587
  stdio: stream ? "inherit" : "pipe",
5324
5588
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" }
@@ -5331,7 +5595,7 @@ function shellOut(cmd, args, cwd, stream = true) {
5331
5595
  }
5332
5596
  function isOnPath(bin) {
5333
5597
  try {
5334
- execFileSync20("which", [bin], { stdio: "pipe" });
5598
+ execFileSync21("which", [bin], { stdio: "pipe" });
5335
5599
  return true;
5336
5600
  } catch {
5337
5601
  return false;
@@ -5365,7 +5629,7 @@ function installLitellmIfNeeded(cwd) {
5365
5629
  } catch {
5366
5630
  }
5367
5631
  try {
5368
- execFileSync20("python3", ["-c", "import litellm"], { stdio: "pipe" });
5632
+ execFileSync21("python3", ["-c", "import litellm"], { stdio: "pipe" });
5369
5633
  process.stdout.write("\u2192 kody2: litellm already installed\n");
5370
5634
  return 0;
5371
5635
  } catch {
@@ -5375,16 +5639,16 @@ function installLitellmIfNeeded(cwd) {
5375
5639
  }
5376
5640
  function configureGitIdentity(cwd) {
5377
5641
  try {
5378
- const name = execFileSync20("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
5642
+ const name = execFileSync21("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
5379
5643
  if (name) return;
5380
5644
  } catch {
5381
5645
  }
5382
5646
  try {
5383
- execFileSync20("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
5647
+ execFileSync21("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
5384
5648
  } catch {
5385
5649
  }
5386
5650
  try {
5387
- execFileSync20("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
5651
+ execFileSync21("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
5388
5652
  cwd,
5389
5653
  stdio: "pipe"
5390
5654
  });
@@ -5561,9 +5825,9 @@ function commitChatFiles(cwd, sessionId, verbose) {
5561
5825
  if (paths.length === 0) return;
5562
5826
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
5563
5827
  try {
5564
- execFileSync21("git", ["add", ...paths], opts);
5565
- execFileSync21("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
5566
- execFileSync21("git", ["push", "--quiet", "origin", "HEAD"], opts);
5828
+ execFileSync22("git", ["add", ...paths], opts);
5829
+ execFileSync22("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
5830
+ execFileSync22("git", ["push", "--quiet", "origin", "HEAD"], opts);
5567
5831
  } catch (err) {
5568
5832
  const msg = err instanceof Error ? err.message : String(err);
5569
5833
  process.stderr.write(`[kody2:chat] commit/push skipped: ${msg}
@@ -94,7 +94,11 @@
94
94
  "script": "saveTaskState"
95
95
  },
96
96
  {
97
- "script": "advanceFlow"
97
+ "script": "riskGate"
98
+ },
99
+ {
100
+ "script": "advanceFlow",
101
+ "runWhen": { "data.riskGate.decision": "allow" }
98
102
  }
99
103
  ]
100
104
  },
@@ -93,7 +93,11 @@
93
93
  "script": "mirrorStateToPr"
94
94
  },
95
95
  {
96
- "script": "advanceFlow"
96
+ "script": "riskGate"
97
+ },
98
+ {
99
+ "script": "advanceFlow",
100
+ "runWhen": { "data.riskGate.decision": "allow" }
97
101
  }
98
102
  ]
99
103
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.2.55",
3
+ "version": "0.2.57",
4
4
  "description": "kody2 — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
5
5
  "license": "MIT",
6
6
  "type": "module",