@kody-ade/kody-engine 0.2.54 → 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.54",
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 execFileSync20 } 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
 
@@ -182,9 +182,24 @@ function loadConfig(projectDir = process.cwd()) {
182
182
  issueContext: parseIssueContext(raw.issueContext),
183
183
  testRequirements: parseTestRequirements(raw.testRequirements),
184
184
  defaultExecutable: typeof raw.defaultExecutable === "string" && raw.defaultExecutable.length > 0 ? raw.defaultExecutable : void 0,
185
+ classify: parseClassifyConfig(raw.classify),
185
186
  release: parseReleaseConfig(raw.release)
186
187
  };
187
188
  }
189
+ function parseClassifyConfig(raw) {
190
+ if (!raw || typeof raw !== "object") return void 0;
191
+ const r = raw;
192
+ const out = {};
193
+ if (r.labelMap && typeof r.labelMap === "object") {
194
+ const entries = Object.entries(r.labelMap).filter(
195
+ ([, v]) => typeof v === "string" && v.length > 0
196
+ );
197
+ if (entries.length > 0) {
198
+ out.labelMap = Object.fromEntries(entries.map(([k, v]) => [k.toLowerCase(), String(v)]));
199
+ }
200
+ }
201
+ return Object.keys(out).length > 0 ? out : void 0;
202
+ }
188
203
  function parseReleaseConfig(raw) {
189
204
  if (!raw || typeof raw !== "object") return void 0;
190
205
  const r = raw;
@@ -528,7 +543,7 @@ async function emit(sink, type, sessionId, suffix, payload) {
528
543
  }
529
544
 
530
545
  // src/kody2-cli.ts
531
- import { execFileSync as execFileSync19 } from "child_process";
546
+ import { execFileSync as execFileSync21 } from "child_process";
532
547
  import * as fs21 from "fs";
533
548
  import * as path18 from "path";
534
549
 
@@ -1499,6 +1514,41 @@ ${formatMissesForFeedback(misses)}`;
1499
1514
  ctx.data.coverageMisses = finalMisses;
1500
1515
  };
1501
1516
 
1517
+ // src/scripts/classifyByLabel.ts
1518
+ var VALID_CLASSES = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
1519
+ var classifyByLabel = async (ctx) => {
1520
+ const issue = ctx.data.issue;
1521
+ const labels = issue?.labels;
1522
+ if (!labels || labels.length === 0) return;
1523
+ const cfgMap = ctx.config.classify?.labelMap;
1524
+ const map = cfgMap ?? defaultLabelMap();
1525
+ for (const label of labels) {
1526
+ const candidate = map[label.toLowerCase()];
1527
+ if (candidate && VALID_CLASSES.has(candidate)) {
1528
+ ctx.data.classification = candidate;
1529
+ ctx.data.classificationSource = "label";
1530
+ ctx.data.classificationReason = `label \`${label}\` \u2192 ${candidate}`;
1531
+ ctx.skipAgent = true;
1532
+ return;
1533
+ }
1534
+ }
1535
+ };
1536
+ function defaultLabelMap() {
1537
+ return {
1538
+ bug: "bug",
1539
+ enhancement: "bug",
1540
+ refactor: "feature",
1541
+ feature: "feature",
1542
+ performance: "feature",
1543
+ rfc: "spec",
1544
+ design: "spec",
1545
+ spec: "spec",
1546
+ docs: "chore",
1547
+ chore: "chore",
1548
+ dependencies: "chore"
1549
+ };
1550
+ }
1551
+
1502
1552
  // src/scripts/commitAndPush.ts
1503
1553
  import { execFileSync as execFileSync6 } from "child_process";
1504
1554
 
@@ -2366,7 +2416,7 @@ function gh2(args, options) {
2366
2416
  }).trim();
2367
2417
  }
2368
2418
  function getIssue(issueNumber, cwd) {
2369
- const output = gh2(["issue", "view", String(issueNumber), "--json", "number,title,body,comments"], { cwd });
2419
+ const output = gh2(["issue", "view", String(issueNumber), "--json", "number,title,body,comments,labels"], { cwd });
2370
2420
  const parsed = JSON.parse(output);
2371
2421
  if (typeof parsed?.title !== "string") {
2372
2422
  throw new Error(`Issue #${issueNumber}: unexpected response shape`);
@@ -2379,7 +2429,8 @@ function getIssue(issueNumber, cwd) {
2379
2429
  body: c.body ?? "",
2380
2430
  author: c.author?.login ?? "unknown",
2381
2431
  createdAt: c.createdAt ?? ""
2382
- }))
2432
+ })),
2433
+ labels: Array.isArray(parsed.labels) ? parsed.labels.map((l) => l.name ?? "").filter((n) => n.length > 0) : []
2383
2434
  };
2384
2435
  }
2385
2436
  function stripKody2Mentions(body) {
@@ -3534,7 +3585,9 @@ var loadIssueContext = async (ctx) => {
3534
3585
  const kept = sorted.slice(0, limit);
3535
3586
  const commentsFormatted = kept.length === 0 ? "(no comments yet)" : kept.map((c) => `- **${c.author}** (${c.createdAt}):
3536
3587
  ${truncate2(c.body, maxBytes).replace(/\n/g, "\n ")}`).join("\n\n");
3537
- ctx.data.issue = { ...issue, commentsFormatted };
3588
+ const labels = issue.labels ?? [];
3589
+ const labelsFormatted = labels.length === 0 ? "(no labels)" : labels.map((l) => `\`${l}\``).join(", ");
3590
+ ctx.data.issue = { ...issue, commentsFormatted, labelsFormatted };
3538
3591
  ctx.data.commentTargetType = "issue";
3539
3592
  ctx.data.commentTargetNumber = issueNumber;
3540
3593
  };
@@ -3659,6 +3712,86 @@ var persistFlowState = async (ctx) => {
3659
3712
  }
3660
3713
  };
3661
3714
 
3715
+ // src/scripts/postClassification.ts
3716
+ import { execFileSync as execFileSync14 } from "child_process";
3717
+ var API_TIMEOUT_MS6 = 3e4;
3718
+ var VALID_CLASSES2 = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
3719
+ var postClassification = async (ctx) => {
3720
+ const issueNumber = ctx.args.issue;
3721
+ if (!issueNumber) return;
3722
+ const presetClassification = ctx.data.classification;
3723
+ let classification = null;
3724
+ let reason = null;
3725
+ if (presetClassification && VALID_CLASSES2.has(presetClassification)) {
3726
+ classification = presetClassification;
3727
+ reason = ctx.data.classificationReason ?? "label-based match";
3728
+ } else {
3729
+ const parsed = parseClassification(ctx.data.prSummary ?? "");
3730
+ classification = parsed?.classification ?? null;
3731
+ reason = parsed?.reason ?? null;
3732
+ }
3733
+ if (!classification) {
3734
+ ctx.data.action = failedAction("classification missing or invalid");
3735
+ tryAuditComment(issueNumber, "\u26A0\uFE0F kody2 classifier could not decide \u2014 please re-run with an explicit `@kody2 <type>`.", ctx.cwd);
3736
+ ctx.output.exitCode = 1;
3737
+ ctx.output.reason = "classify: no decision";
3738
+ return;
3739
+ }
3740
+ tryAuditComment(
3741
+ issueNumber,
3742
+ `\u{1F50E} kody2 classified as \`${classification}\`${reason ? ` \u2014 ${reason}` : ""}`,
3743
+ ctx.cwd
3744
+ );
3745
+ try {
3746
+ execFileSync14("gh", ["issue", "comment", String(issueNumber), "--body", `@kody2 ${classification}`], {
3747
+ cwd: ctx.cwd,
3748
+ timeout: API_TIMEOUT_MS6,
3749
+ stdio: ["ignore", "pipe", "pipe"]
3750
+ });
3751
+ } catch (err) {
3752
+ process.stderr.write(
3753
+ `[kody2 postClassification] failed to dispatch @kody2 ${classification}: ${err instanceof Error ? err.message : String(err)}
3754
+ `
3755
+ );
3756
+ ctx.data.action = failedAction("dispatch post failed");
3757
+ ctx.output.exitCode = 1;
3758
+ ctx.output.reason = "classify: dispatch failed";
3759
+ return;
3760
+ }
3761
+ ctx.data.action = makeAction2(`CLASSIFIED_AS_${classification.toUpperCase()}`, {
3762
+ classification,
3763
+ reason: reason ?? "",
3764
+ source: ctx.data.classificationSource ?? "agent"
3765
+ });
3766
+ ctx.data.classification = classification;
3767
+ ctx.data.classificationReason = reason ?? "";
3768
+ };
3769
+ function parseClassification(prSummary) {
3770
+ if (!prSummary) return null;
3771
+ const classMatch = prSummary.match(/classification:\s*(feature|bug|spec|chore)\b/i);
3772
+ if (!classMatch) return null;
3773
+ const classification = classMatch[1].toLowerCase();
3774
+ const reasonMatch = prSummary.match(/reason:\s*(.+)$/im);
3775
+ const reason = reasonMatch ? reasonMatch[1].trim() : "";
3776
+ return { classification, reason };
3777
+ }
3778
+ function tryAuditComment(issueNumber, body, cwd) {
3779
+ try {
3780
+ execFileSync14("gh", ["issue", "comment", String(issueNumber), "--body", body], {
3781
+ cwd,
3782
+ timeout: API_TIMEOUT_MS6,
3783
+ stdio: ["ignore", "pipe", "pipe"]
3784
+ });
3785
+ } catch {
3786
+ }
3787
+ }
3788
+ function makeAction2(type, payload) {
3789
+ return { type, payload, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
3790
+ }
3791
+ function failedAction(reason) {
3792
+ return { type: "CLASSIFY_FAILED", payload: { reason }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
3793
+ }
3794
+
3662
3795
  // src/scripts/postIssueComment.ts
3663
3796
  var postIssueComment2 = async (ctx) => {
3664
3797
  if (ctx.skipAgent && ctx.output.exitCode !== void 0) return;
@@ -3773,7 +3906,7 @@ function reviewAction(verdict, payload) {
3773
3906
  const type = verdict === "PASS" ? "REVIEW_PASS" : verdict === "CONCERNS" ? "REVIEW_CONCERNS" : verdict === "FAIL" ? "REVIEW_FAIL" : "REVIEW_COMPLETED";
3774
3907
  return { type, payload: { verdict, ...payload }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
3775
3908
  }
3776
- function failedAction(reason) {
3909
+ function failedAction2(reason) {
3777
3910
  return { type: "REVIEW_FAILED", payload: { reason }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
3778
3911
  }
3779
3912
  var postReviewResult = async (ctx, _profile, agentResult) => {
@@ -3781,7 +3914,7 @@ var postReviewResult = async (ctx, _profile, agentResult) => {
3781
3914
  if (!prNumber) {
3782
3915
  ctx.output.exitCode = 99;
3783
3916
  ctx.output.reason = "review postflight: no PR number in context";
3784
- ctx.data.action = failedAction(ctx.output.reason);
3917
+ ctx.data.action = failedAction2(ctx.output.reason);
3785
3918
  return;
3786
3919
  }
3787
3920
  if (!agentResult || agentResult.outcome !== "completed") {
@@ -3792,7 +3925,7 @@ var postReviewResult = async (ctx, _profile, agentResult) => {
3792
3925
  }
3793
3926
  ctx.output.exitCode = 1;
3794
3927
  ctx.output.reason = reason;
3795
- ctx.data.action = failedAction(reason);
3928
+ ctx.data.action = failedAction2(reason);
3796
3929
  return;
3797
3930
  }
3798
3931
  const reviewBody = agentResult.finalText.trim();
@@ -3803,7 +3936,7 @@ var postReviewResult = async (ctx, _profile, agentResult) => {
3803
3936
  }
3804
3937
  ctx.output.exitCode = 1;
3805
3938
  ctx.output.reason = "empty review body";
3806
- ctx.data.action = failedAction("empty review body");
3939
+ ctx.data.action = failedAction2("empty review body");
3807
3940
  return;
3808
3941
  }
3809
3942
  try {
@@ -3812,7 +3945,7 @@ var postReviewResult = async (ctx, _profile, agentResult) => {
3812
3945
  const msg = err instanceof Error ? err.message : String(err);
3813
3946
  ctx.output.exitCode = 4;
3814
3947
  ctx.output.reason = `failed to post review comment: ${msg}`;
3815
- ctx.data.action = failedAction(ctx.output.reason);
3948
+ ctx.data.action = failedAction2(ctx.output.reason);
3816
3949
  return;
3817
3950
  }
3818
3951
  const verdict = detectVerdict(reviewBody);
@@ -3828,7 +3961,7 @@ REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.
3828
3961
  };
3829
3962
 
3830
3963
  // src/scripts/releaseFlow.ts
3831
- import { execFileSync as execFileSync14, spawnSync } from "child_process";
3964
+ import { execFileSync as execFileSync15, spawnSync } from "child_process";
3832
3965
  import * as fs18 from "fs";
3833
3966
  import * as path16 from "path";
3834
3967
  function bumpVersion(current, bump) {
@@ -3858,7 +3991,7 @@ function generateChangelog(cwd, newVersion, lastTag) {
3858
3991
  const range = lastTag ? `${lastTag}..HEAD` : "HEAD";
3859
3992
  let log = "";
3860
3993
  try {
3861
- log = execFileSync14("git", ["log", range, "--pretty=format:%s||%h", "--no-merges"], {
3994
+ log = execFileSync15("git", ["log", range, "--pretty=format:%s||%h", "--no-merges"], {
3862
3995
  cwd,
3863
3996
  encoding: "utf-8",
3864
3997
  stdio: ["ignore", "pipe", "pipe"]
@@ -3915,7 +4048,7 @@ ${entry}${prior.slice(idx + 1)}`);
3915
4048
  }
3916
4049
  }
3917
4050
  function git3(args, cwd, timeout = 6e4) {
3918
- return execFileSync14("git", args, {
4051
+ return execFileSync15("git", args, {
3919
4052
  encoding: "utf-8",
3920
4053
  timeout,
3921
4054
  cwd,
@@ -4142,12 +4275,12 @@ function fail(ctx, profile, reason) {
4142
4275
  ctx.data.agentDone = false;
4143
4276
  ctx.data.agentFailureReason = reason;
4144
4277
  const modeSeg = profile.name.replace(/-/g, "_").toUpperCase();
4145
- const failedAction2 = {
4278
+ const failedAction3 = {
4146
4279
  type: `${modeSeg}_FAILED`,
4147
4280
  payload: { reason },
4148
4281
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
4149
4282
  };
4150
- ctx.data.action = failedAction2;
4283
+ ctx.data.action = failedAction3;
4151
4284
  }
4152
4285
  function countActionItems(block) {
4153
4286
  if (!block.trim()) return 0;
@@ -4186,12 +4319,12 @@ function fail2(ctx, profile, reason) {
4186
4319
  ctx.data.agentDone = false;
4187
4320
  ctx.data.agentFailureReason = reason;
4188
4321
  const modeSeg = profile.name.replace(/-/g, "_").toUpperCase();
4189
- const failedAction2 = {
4322
+ const failedAction3 = {
4190
4323
  type: `${modeSeg}_FAILED`,
4191
4324
  payload: { reason },
4192
4325
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
4193
4326
  };
4194
- ctx.data.action = failedAction2;
4327
+ ctx.data.action = failedAction3;
4195
4328
  }
4196
4329
 
4197
4330
  // src/scripts/resolveArtifacts.ts
@@ -4218,7 +4351,7 @@ var resolveArtifacts = async (ctx, profile) => {
4218
4351
  };
4219
4352
 
4220
4353
  // src/scripts/resolveFlow.ts
4221
- import { execFileSync as execFileSync15 } from "child_process";
4354
+ import { execFileSync as execFileSync16 } from "child_process";
4222
4355
  var CONFLICT_DIFF_MAX_BYTES = 4e4;
4223
4356
  var resolveFlow = async (ctx) => {
4224
4357
  const prNumber = ctx.args.pr;
@@ -4270,7 +4403,7 @@ var resolveFlow = async (ctx) => {
4270
4403
  };
4271
4404
  function getConflictedFiles(cwd) {
4272
4405
  try {
4273
- const out = execFileSync15("git", ["diff", "--name-only", "--diff-filter=U"], {
4406
+ const out = execFileSync16("git", ["diff", "--name-only", "--diff-filter=U"], {
4274
4407
  encoding: "utf-8",
4275
4408
  cwd,
4276
4409
  env: { ...process.env, HUSKY: "0" }
@@ -4285,7 +4418,7 @@ function getConflictMarkersPreview(files, cwd, maxBytes = CONFLICT_DIFF_MAX_BYTE
4285
4418
  let total = 0;
4286
4419
  for (const f of files) {
4287
4420
  try {
4288
- const content = execFileSync15("cat", [f], { encoding: "utf-8", cwd }).toString();
4421
+ const content = execFileSync16("cat", [f], { encoding: "utf-8", cwd }).toString();
4289
4422
  const snippet = `### ${f}
4290
4423
 
4291
4424
  \`\`\`
@@ -4353,6 +4486,253 @@ function tryPostPr4(prNumber, body, cwd) {
4353
4486
  }
4354
4487
  }
4355
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
+
4356
4736
  // src/scripts/runFlow.ts
4357
4737
  var runFlow = async (ctx) => {
4358
4738
  const issueNumber = ctx.args.issue;
@@ -4449,8 +4829,8 @@ var skipAgent = async (ctx) => {
4449
4829
  };
4450
4830
 
4451
4831
  // src/scripts/startFlow.ts
4452
- import { execFileSync as execFileSync16 } from "child_process";
4453
- var API_TIMEOUT_MS6 = 3e4;
4832
+ import { execFileSync as execFileSync18 } from "child_process";
4833
+ var API_TIMEOUT_MS7 = 3e4;
4454
4834
  var startFlow = async (ctx, profile, _agentResult, args) => {
4455
4835
  const entry = args?.entry;
4456
4836
  if (!entry) {
@@ -4483,8 +4863,8 @@ function postKody2Comment(target, issueNumber, state, next, cwd) {
4483
4863
  const sub = target === "pr" && state?.core.prUrl ? "pr" : "issue";
4484
4864
  const body = `@kody2 ${next}`;
4485
4865
  try {
4486
- execFileSync16("gh", [sub, "comment", String(targetNumber), "--body", body], {
4487
- timeout: API_TIMEOUT_MS6,
4866
+ execFileSync18("gh", [sub, "comment", String(targetNumber), "--body", body], {
4867
+ timeout: API_TIMEOUT_MS7,
4488
4868
  cwd,
4489
4869
  stdio: ["ignore", "pipe", "pipe"]
4490
4870
  });
@@ -4503,7 +4883,7 @@ function parsePr2(url) {
4503
4883
  }
4504
4884
 
4505
4885
  // src/scripts/syncFlow.ts
4506
- import { execFileSync as execFileSync17 } from "child_process";
4886
+ import { execFileSync as execFileSync19 } from "child_process";
4507
4887
  var syncFlow = async (ctx) => {
4508
4888
  ctx.skipAgent = true;
4509
4889
  const prNumber = ctx.args.pr;
@@ -4562,7 +4942,7 @@ function bail2(ctx, prNumber, reason) {
4562
4942
  }
4563
4943
  function revParseHead(cwd) {
4564
4944
  try {
4565
- return execFileSync17("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();
4566
4946
  } catch {
4567
4947
  return "";
4568
4948
  }
@@ -4570,9 +4950,9 @@ function revParseHead(cwd) {
4570
4950
  function pushBranch(branch, cwd) {
4571
4951
  const env = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
4572
4952
  try {
4573
- execFileSync17("git", ["push", "-u", "origin", branch], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
4953
+ execFileSync19("git", ["push", "-u", "origin", branch], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
4574
4954
  } catch {
4575
- execFileSync17("git", ["push", "--force-with-lease", "-u", "origin", branch], {
4955
+ execFileSync19("git", ["push", "--force-with-lease", "-u", "origin", branch], {
4576
4956
  cwd,
4577
4957
  env,
4578
4958
  stdio: ["ignore", "pipe", "pipe"]
@@ -4790,7 +5170,8 @@ var preflightScripts = {
4790
5170
  resolvePreviewUrl,
4791
5171
  composePrompt,
4792
5172
  setLifecycleLabel,
4793
- skipAgent
5173
+ skipAgent,
5174
+ classifyByLabel
4794
5175
  };
4795
5176
  var postflightScripts = {
4796
5177
  parseAgentResult: parseAgentResult2,
@@ -4812,7 +5193,9 @@ var postflightScripts = {
4812
5193
  dispatch,
4813
5194
  finishFlow,
4814
5195
  advanceFlow,
4815
- persistFlowState
5196
+ persistFlowState,
5197
+ postClassification,
5198
+ riskGate
4816
5199
  };
4817
5200
  var allScriptNames = /* @__PURE__ */ new Set([
4818
5201
  ...Object.keys(preflightScripts),
@@ -4820,7 +5203,7 @@ var allScriptNames = /* @__PURE__ */ new Set([
4820
5203
  ]);
4821
5204
 
4822
5205
  // src/tools.ts
4823
- import { execFileSync as execFileSync18 } from "child_process";
5206
+ import { execFileSync as execFileSync20 } from "child_process";
4824
5207
  function verifyCliTools(tools, cwd) {
4825
5208
  const out = [];
4826
5209
  for (const t of tools) out.push(verifyOne(t, cwd));
@@ -4853,7 +5236,7 @@ function verifyOne(tool, cwd) {
4853
5236
  }
4854
5237
  function runShell2(cmd, cwd, timeoutMs = 3e4) {
4855
5238
  try {
4856
- execFileSync18("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
5239
+ execFileSync20("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
4857
5240
  return true;
4858
5241
  } catch {
4859
5242
  return false;
@@ -5183,7 +5566,7 @@ function detectPackageManager2(cwd) {
5183
5566
  }
5184
5567
  function shellOut(cmd, args, cwd, stream = true) {
5185
5568
  try {
5186
- execFileSync19(cmd, args, {
5569
+ execFileSync21(cmd, args, {
5187
5570
  cwd,
5188
5571
  stdio: stream ? "inherit" : "pipe",
5189
5572
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" }
@@ -5196,7 +5579,7 @@ function shellOut(cmd, args, cwd, stream = true) {
5196
5579
  }
5197
5580
  function isOnPath(bin) {
5198
5581
  try {
5199
- execFileSync19("which", [bin], { stdio: "pipe" });
5582
+ execFileSync21("which", [bin], { stdio: "pipe" });
5200
5583
  return true;
5201
5584
  } catch {
5202
5585
  return false;
@@ -5230,7 +5613,7 @@ function installLitellmIfNeeded(cwd) {
5230
5613
  } catch {
5231
5614
  }
5232
5615
  try {
5233
- execFileSync19("python3", ["-c", "import litellm"], { stdio: "pipe" });
5616
+ execFileSync21("python3", ["-c", "import litellm"], { stdio: "pipe" });
5234
5617
  process.stdout.write("\u2192 kody2: litellm already installed\n");
5235
5618
  return 0;
5236
5619
  } catch {
@@ -5240,16 +5623,16 @@ function installLitellmIfNeeded(cwd) {
5240
5623
  }
5241
5624
  function configureGitIdentity(cwd) {
5242
5625
  try {
5243
- const name = execFileSync19("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();
5244
5627
  if (name) return;
5245
5628
  } catch {
5246
5629
  }
5247
5630
  try {
5248
- execFileSync19("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
5631
+ execFileSync21("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
5249
5632
  } catch {
5250
5633
  }
5251
5634
  try {
5252
- execFileSync19("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"], {
5253
5636
  cwd,
5254
5637
  stdio: "pipe"
5255
5638
  });
@@ -5426,9 +5809,9 @@ function commitChatFiles(cwd, sessionId, verbose) {
5426
5809
  if (paths.length === 0) return;
5427
5810
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
5428
5811
  try {
5429
- execFileSync20("git", ["add", ...paths], opts);
5430
- execFileSync20("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
5431
- execFileSync20("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);
5432
5815
  } catch (err) {
5433
5816
  const msg = err instanceof Error ? err.message : String(err);
5434
5817
  process.stderr.write(`[kody2:chat] commit/push skipped: ${msg}
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "classify",
3
+ "role": "primitive",
4
+ "describe": "Classify an issue into one of {feature, bug, spec, chore} and dispatch the matching sub-orchestrator. Label-first fast path; LLM fallback when labels don't decide.",
5
+ "inputs": [
6
+ {
7
+ "name": "issue",
8
+ "flag": "--issue",
9
+ "type": "int",
10
+ "required": true,
11
+ "describe": "GitHub issue number to classify."
12
+ }
13
+ ],
14
+ "claudeCode": {
15
+ "model": "inherit",
16
+ "permissionMode": "default",
17
+ "maxTurns": null,
18
+ "maxThinkingTokens": null,
19
+ "systemPromptAppend": null,
20
+ "tools": [
21
+ "Read",
22
+ "Bash"
23
+ ],
24
+ "hooks": [],
25
+ "skills": [],
26
+ "commands": [],
27
+ "subagents": [],
28
+ "plugins": [],
29
+ "mcpServers": []
30
+ },
31
+ "cliTools": [],
32
+ "scripts": {
33
+ "preflight": [
34
+ {
35
+ "script": "setLifecycleLabel",
36
+ "with": {
37
+ "label": "kody:classifying",
38
+ "color": "0e8a16",
39
+ "description": "kody2: classifying the issue"
40
+ }
41
+ },
42
+ { "script": "loadIssueContext" },
43
+ { "script": "loadTaskState" },
44
+ { "script": "classifyByLabel" },
45
+ { "script": "loadConventions" },
46
+ { "script": "composePrompt" }
47
+ ],
48
+ "postflight": [
49
+ { "script": "parseAgentResult" },
50
+ { "script": "postClassification" },
51
+ { "script": "writeRunSummary" },
52
+ { "script": "saveTaskState" }
53
+ ]
54
+ },
55
+ "output": {
56
+ "actionTypes": [
57
+ "CLASSIFIED_AS_FEATURE",
58
+ "CLASSIFIED_AS_BUG",
59
+ "CLASSIFIED_AS_SPEC",
60
+ "CLASSIFIED_AS_CHORE",
61
+ "CLASSIFY_FAILED"
62
+ ]
63
+ }
64
+ }
@@ -0,0 +1,49 @@
1
+ You are Kody's issue-triage classifier. Your only job: read the issue below and pick ONE of the four flow types.
2
+
3
+ # Repo
4
+ {{repoOwner}}/{{repoName}}, default branch `{{defaultBranch}}`
5
+
6
+ # Issue #{{issue.number}}: {{issue.title}}
7
+
8
+ Labels: {{issue.labelsFormatted}}
9
+
10
+ {{issue.body}}
11
+
12
+ Recent comments (most recent first, truncated):
13
+ {{issue.commentsFormatted}}
14
+
15
+ {{conventionsBlock}}
16
+
17
+ ---
18
+
19
+ # Classification rubric
20
+
21
+ Pick **exactly one** of:
22
+
23
+ - **feature** — new user-facing capability, refactor, performance work, or anything where scope is not fully known up front. Multi-file change likely. Use when the issue opens a design space (even if small).
24
+ - **bug** — fix broken behavior, enhancement to existing feature, or any targeted change where the scope is localized and well understood. Skip research; go straight to plan.
25
+ - **spec** — produce a design doc, RFC, architecture proposal, or exploration artifact. No code changes. Terminates at the plan artifact.
26
+ - **chore** — trivial maintenance: docs tweak, dep bump, lint fix, README update. No planning needed.
27
+
28
+ **If the issue ASKS for an RFC / design doc / spec / analysis with no implementation → `spec`.** Beats everything else.
29
+ **If the issue is plainly "fix X" or "add tiny Y to existing Z" with clear boundaries → `bug`.**
30
+ **If the issue is "tweak config / bump dep / fix typo" with no real design choice → `chore`.**
31
+ **Otherwise → `feature`.**
32
+
33
+ # Required output
34
+
35
+ Your FINAL message must be exactly this shape (no extra text before or after):
36
+
37
+ ```
38
+ DONE
39
+ COMMIT_MSG: classify: <classification>
40
+ PR_SUMMARY:
41
+ classification: <feature|bug|spec|chore>
42
+ reason: <one sentence explaining the pick, grounded in the issue text>
43
+ ```
44
+
45
+ # Rules
46
+
47
+ - Read-only. Do NOT modify any file. Do NOT run git or gh.
48
+ - Output `FAILED: <reason>` if the issue is incoherent or ambiguous beyond the rubric.
49
+ - Do not over-think. This is triage, not analysis.
@@ -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.54",
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",