@kody-ade/kody-engine 0.2.55 → 0.2.56

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.56",
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,253 @@ 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 = ctx.data.changedFiles ?? [];
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 computeDiffStats(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", "--shortstat", ref], {
4665
+ cwd: ctx.cwd,
4666
+ encoding: "utf-8",
4667
+ stdio: ["pipe", "pipe", "pipe"]
4668
+ }).trim();
4669
+ if (out) return parseShortstat(out);
4670
+ } catch {
4671
+ }
4672
+ }
4673
+ return null;
4674
+ }
4675
+ function parseShortstat(s) {
4676
+ const ins = /(\d+)\s+insertions?/.exec(s);
4677
+ const del = /(\d+)\s+deletions?/.exec(s);
4678
+ return {
4679
+ insertions: ins ? parseInt(ins[1], 10) : 0,
4680
+ deletions: del ? parseInt(del[1], 10) : 0
4681
+ };
4682
+ }
4683
+ function listDeletedFilesInHeadCommit(cwd) {
4684
+ try {
4685
+ const out = execFileSync17("git", ["show", "--name-status", "--pretty=format:", "HEAD"], {
4686
+ cwd,
4687
+ encoding: "utf-8",
4688
+ stdio: ["pipe", "pipe", "pipe"]
4689
+ });
4690
+ return out.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("D ")).map((l) => l.slice(2).trim()).filter(Boolean);
4691
+ } catch {
4692
+ return [];
4693
+ }
4694
+ }
4695
+ function ensureApproveLabel(gate, cwd) {
4696
+ try {
4697
+ gh2(
4698
+ [
4699
+ "label",
4700
+ "create",
4701
+ `kody-approve:${gate}`,
4702
+ "--force",
4703
+ "--color",
4704
+ "0e8a16",
4705
+ "--description",
4706
+ `kody2: approve the ${gate} risk gate and resume the flow`
4707
+ ],
4708
+ { cwd }
4709
+ );
4710
+ } catch {
4711
+ }
4712
+ }
4713
+ function formatAdvisory(pending) {
4714
+ const lines = [];
4715
+ lines.push("\u23F8\uFE0F **kody2 risk gate halted the flow.**");
4716
+ lines.push("");
4717
+ lines.push("The changes were committed and pushed, but the flow will **not progress** until a human approves:");
4718
+ lines.push("");
4719
+ for (const v of pending) {
4720
+ lines.push(`- **\`${v.name}\`** _(${v.severity})_ \u2014 ${v.reason}`);
4721
+ }
4722
+ lines.push("");
4723
+ lines.push("**To approve and resume**, add one of these labels to this issue/PR:");
4724
+ const perGate = pending.map((v) => `\`kody-approve:${v.name}\``).join(", ");
4725
+ lines.push(`- ${perGate}`);
4726
+ if (pending.some((v) => v.severity === "soft")) {
4727
+ lines.push("- `kody-approve:all` \u2014 bypass **soft** gates at once (hard gates still require their specific label)");
4728
+ }
4729
+ lines.push("");
4730
+ lines.push(
4731
+ "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."
4732
+ );
4733
+ return lines.join("\n");
4734
+ }
4735
+
4489
4736
  // src/scripts/runFlow.ts
4490
4737
  var runFlow = async (ctx) => {
4491
4738
  const issueNumber = ctx.args.issue;
@@ -4582,7 +4829,7 @@ var skipAgent = async (ctx) => {
4582
4829
  };
4583
4830
 
4584
4831
  // src/scripts/startFlow.ts
4585
- import { execFileSync as execFileSync17 } from "child_process";
4832
+ import { execFileSync as execFileSync18 } from "child_process";
4586
4833
  var API_TIMEOUT_MS7 = 3e4;
4587
4834
  var startFlow = async (ctx, profile, _agentResult, args) => {
4588
4835
  const entry = args?.entry;
@@ -4616,7 +4863,7 @@ function postKody2Comment(target, issueNumber, state, next, cwd) {
4616
4863
  const sub = target === "pr" && state?.core.prUrl ? "pr" : "issue";
4617
4864
  const body = `@kody2 ${next}`;
4618
4865
  try {
4619
- execFileSync17("gh", [sub, "comment", String(targetNumber), "--body", body], {
4866
+ execFileSync18("gh", [sub, "comment", String(targetNumber), "--body", body], {
4620
4867
  timeout: API_TIMEOUT_MS7,
4621
4868
  cwd,
4622
4869
  stdio: ["ignore", "pipe", "pipe"]
@@ -4636,7 +4883,7 @@ function parsePr2(url) {
4636
4883
  }
4637
4884
 
4638
4885
  // src/scripts/syncFlow.ts
4639
- import { execFileSync as execFileSync18 } from "child_process";
4886
+ import { execFileSync as execFileSync19 } from "child_process";
4640
4887
  var syncFlow = async (ctx) => {
4641
4888
  ctx.skipAgent = true;
4642
4889
  const prNumber = ctx.args.pr;
@@ -4695,7 +4942,7 @@ function bail2(ctx, prNumber, reason) {
4695
4942
  }
4696
4943
  function revParseHead(cwd) {
4697
4944
  try {
4698
- return execFileSync18("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
4945
+ return execFileSync19("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
4699
4946
  } catch {
4700
4947
  return "";
4701
4948
  }
@@ -4703,9 +4950,9 @@ function revParseHead(cwd) {
4703
4950
  function pushBranch(branch, cwd) {
4704
4951
  const env = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
4705
4952
  try {
4706
- execFileSync18("git", ["push", "-u", "origin", branch], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
4953
+ execFileSync19("git", ["push", "-u", "origin", branch], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
4707
4954
  } catch {
4708
- execFileSync18("git", ["push", "--force-with-lease", "-u", "origin", branch], {
4955
+ execFileSync19("git", ["push", "--force-with-lease", "-u", "origin", branch], {
4709
4956
  cwd,
4710
4957
  env,
4711
4958
  stdio: ["ignore", "pipe", "pipe"]
@@ -4947,7 +5194,8 @@ var postflightScripts = {
4947
5194
  finishFlow,
4948
5195
  advanceFlow,
4949
5196
  persistFlowState,
4950
- postClassification
5197
+ postClassification,
5198
+ riskGate
4951
5199
  };
4952
5200
  var allScriptNames = /* @__PURE__ */ new Set([
4953
5201
  ...Object.keys(preflightScripts),
@@ -4955,7 +5203,7 @@ var allScriptNames = /* @__PURE__ */ new Set([
4955
5203
  ]);
4956
5204
 
4957
5205
  // src/tools.ts
4958
- import { execFileSync as execFileSync19 } from "child_process";
5206
+ import { execFileSync as execFileSync20 } from "child_process";
4959
5207
  function verifyCliTools(tools, cwd) {
4960
5208
  const out = [];
4961
5209
  for (const t of tools) out.push(verifyOne(t, cwd));
@@ -4988,7 +5236,7 @@ function verifyOne(tool, cwd) {
4988
5236
  }
4989
5237
  function runShell2(cmd, cwd, timeoutMs = 3e4) {
4990
5238
  try {
4991
- execFileSync19("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
5239
+ execFileSync20("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
4992
5240
  return true;
4993
5241
  } catch {
4994
5242
  return false;
@@ -5318,7 +5566,7 @@ function detectPackageManager2(cwd) {
5318
5566
  }
5319
5567
  function shellOut(cmd, args, cwd, stream = true) {
5320
5568
  try {
5321
- execFileSync20(cmd, args, {
5569
+ execFileSync21(cmd, args, {
5322
5570
  cwd,
5323
5571
  stdio: stream ? "inherit" : "pipe",
5324
5572
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" }
@@ -5331,7 +5579,7 @@ function shellOut(cmd, args, cwd, stream = true) {
5331
5579
  }
5332
5580
  function isOnPath(bin) {
5333
5581
  try {
5334
- execFileSync20("which", [bin], { stdio: "pipe" });
5582
+ execFileSync21("which", [bin], { stdio: "pipe" });
5335
5583
  return true;
5336
5584
  } catch {
5337
5585
  return false;
@@ -5365,7 +5613,7 @@ function installLitellmIfNeeded(cwd) {
5365
5613
  } catch {
5366
5614
  }
5367
5615
  try {
5368
- execFileSync20("python3", ["-c", "import litellm"], { stdio: "pipe" });
5616
+ execFileSync21("python3", ["-c", "import litellm"], { stdio: "pipe" });
5369
5617
  process.stdout.write("\u2192 kody2: litellm already installed\n");
5370
5618
  return 0;
5371
5619
  } catch {
@@ -5375,16 +5623,16 @@ function installLitellmIfNeeded(cwd) {
5375
5623
  }
5376
5624
  function configureGitIdentity(cwd) {
5377
5625
  try {
5378
- const name = execFileSync20("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
5626
+ const name = execFileSync21("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
5379
5627
  if (name) return;
5380
5628
  } catch {
5381
5629
  }
5382
5630
  try {
5383
- execFileSync20("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
5631
+ execFileSync21("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
5384
5632
  } catch {
5385
5633
  }
5386
5634
  try {
5387
- execFileSync20("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
5635
+ execFileSync21("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
5388
5636
  cwd,
5389
5637
  stdio: "pipe"
5390
5638
  });
@@ -5561,9 +5809,9 @@ function commitChatFiles(cwd, sessionId, verbose) {
5561
5809
  if (paths.length === 0) return;
5562
5810
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
5563
5811
  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);
5812
+ execFileSync22("git", ["add", ...paths], opts);
5813
+ execFileSync22("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
5814
+ execFileSync22("git", ["push", "--quiet", "origin", "HEAD"], opts);
5567
5815
  } catch (err) {
5568
5816
  const msg = err instanceof Error ? err.message : String(err);
5569
5817
  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.56",
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",