@kody-ade/kody-engine 0.2.58 → 0.2.60

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.58",
6
+ version: "0.2.60",
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 execFileSync22 } from "child_process";
53
+ import { execFileSync as execFileSync23 } 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 execFileSync21 } from "child_process";
546
+ import { execFileSync as execFileSync22 } from "child_process";
547
547
  import * as fs21 from "fs";
548
548
  import * as path18 from "path";
549
549
 
@@ -581,6 +581,9 @@ function autoDispatch(opts) {
581
581
  if (!targetNum) return null;
582
582
  const afterTag = extractAfterTag(body);
583
583
  if (isPr) {
584
+ if (/\bapprove\b/.test(afterTag)) {
585
+ return { executable: "approve", cliArgs: { pr: targetNum }, target: targetNum };
586
+ }
584
587
  if (/\bfix-ci\b/.test(afterTag)) {
585
588
  return { executable: "fix-ci", cliArgs: { pr: targetNum }, target: targetNum };
586
589
  }
@@ -754,6 +757,7 @@ import * as path6 from "path";
754
757
  var VALID_INPUT_TYPES = /* @__PURE__ */ new Set(["int", "string", "bool", "enum"]);
755
758
  var VALID_PERMISSION_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "bypassPermissions"]);
756
759
  var VALID_ROLES = /* @__PURE__ */ new Set(["primitive", "orchestrator", "watch", "utility"]);
760
+ var VALID_PHASES = /* @__PURE__ */ new Set(["research", "planning", "implementing", "reviewing", "shipped", "failed", "idle"]);
757
761
  var ProfileError = class extends Error {
758
762
  constructor(profilePath, message) {
759
763
  super(`Invalid profile at ${profilePath}:
@@ -788,12 +792,20 @@ function loadProfile(profilePath) {
788
792
  );
789
793
  }
790
794
  const role = r.role;
795
+ let phase;
796
+ if (r.phase !== void 0) {
797
+ if (typeof r.phase !== "string" || !VALID_PHASES.has(r.phase)) {
798
+ throw new ProfileError(profilePath, `"phase" must be one of: ${[...VALID_PHASES].join(" | ")}`);
799
+ }
800
+ phase = r.phase;
801
+ }
791
802
  const profile = {
792
803
  name: requireString(profilePath, r, "name"),
793
804
  describe: typeof r.describe === "string" ? r.describe : "",
794
805
  role,
795
806
  kind,
796
807
  schedule: typeof r.schedule === "string" ? r.schedule : void 0,
808
+ phase,
797
809
  inputs: parseInputs(profilePath, r.inputs),
798
810
  claudeCode: parseClaudeCode(profilePath, r.claudeCode),
799
811
  cliTools: parseCliTools(profilePath, r.cliTools),
@@ -1060,7 +1072,7 @@ function parseStateComment(body) {
1060
1072
  return emptyState();
1061
1073
  }
1062
1074
  }
1063
- function reduce(state, executable, action) {
1075
+ function reduce(state, executable, action, phase) {
1064
1076
  if (!action) return state;
1065
1077
  const newAttempts = { ...state.core.attempts, [executable]: (state.core.attempts[executable] ?? 0) + 1 };
1066
1078
  const newExecutables = {
@@ -1079,7 +1091,7 @@ function reduce(state, executable, action) {
1079
1091
  lastOutcome: action,
1080
1092
  currentExecutable: executable,
1081
1093
  status: statusFromAction(action),
1082
- phase: phaseFromAction(executable, action)
1094
+ phase: phaseFromAction(action, phase)
1083
1095
  },
1084
1096
  executables: newExecutables,
1085
1097
  artifacts: { ...state.artifacts ?? {} },
@@ -1092,12 +1104,9 @@ function statusFromAction(action) {
1092
1104
  if (/COMPLETED$|SHIPPED$|MERGED$|SUCCESS$/i.test(action.type)) return "succeeded";
1093
1105
  return "running";
1094
1106
  }
1095
- function phaseFromAction(executable, action) {
1107
+ function phaseFromAction(action, phase) {
1096
1108
  if (/FAILED$|ERROR$|REJECTED$/i.test(action.type)) return "failed";
1097
- if (executable === "build") return statusFromAction(action) === "succeeded" ? "implementing" : "implementing";
1098
- if (executable === "review") return "reviewing";
1099
- if (executable === "release") return "shipped";
1100
- return "idle";
1109
+ return phase ?? "idle";
1101
1110
  }
1102
1111
  function noteFromAction(action) {
1103
1112
  const p = action.payload;
@@ -1206,7 +1215,7 @@ var advanceFlow = async (ctx, profile) => {
1206
1215
  try {
1207
1216
  const issueState = readTaskState("issue", flow.issueNumber, ctx.cwd);
1208
1217
  issueState.flow = flow;
1209
- const next = reduce(issueState, profile.name, action);
1218
+ const next = reduce(issueState, profile.name, action, profile.phase);
1210
1219
  if (state?.core.prUrl && !next.core.prUrl) next.core.prUrl = state.core.prUrl;
1211
1220
  next.flow = flow;
1212
1221
  writeTaskState("issue", flow.issueNumber, next, ctx.cwd);
@@ -1232,6 +1241,265 @@ var advanceFlow = async (ctx, profile) => {
1232
1241
  }
1233
1242
  };
1234
1243
 
1244
+ // src/scripts/applyApprovals.ts
1245
+ import { execFileSync as execFileSync5 } from "child_process";
1246
+
1247
+ // src/issue.ts
1248
+ import { execFileSync as execFileSync4 } from "child_process";
1249
+ var API_TIMEOUT_MS3 = 3e4;
1250
+ function ghToken2() {
1251
+ return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
1252
+ }
1253
+ function gh2(args, options) {
1254
+ const token = ghToken2();
1255
+ const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
1256
+ return execFileSync4("gh", args, {
1257
+ encoding: "utf-8",
1258
+ timeout: API_TIMEOUT_MS3,
1259
+ cwd: options?.cwd,
1260
+ env,
1261
+ input: options?.input,
1262
+ stdio: options?.input ? ["pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
1263
+ }).trim();
1264
+ }
1265
+ function getIssue(issueNumber, cwd) {
1266
+ const output = gh2(["issue", "view", String(issueNumber), "--json", "number,title,body,comments,labels"], { cwd });
1267
+ const parsed = JSON.parse(output);
1268
+ if (typeof parsed?.title !== "string") {
1269
+ throw new Error(`Issue #${issueNumber}: unexpected response shape`);
1270
+ }
1271
+ return {
1272
+ number: parsed.number ?? issueNumber,
1273
+ title: parsed.title,
1274
+ body: parsed.body ?? "",
1275
+ comments: (parsed.comments ?? []).map((c) => ({
1276
+ body: c.body ?? "",
1277
+ author: c.author?.login ?? "unknown",
1278
+ createdAt: c.createdAt ?? ""
1279
+ })),
1280
+ labels: Array.isArray(parsed.labels) ? parsed.labels.map((l) => l.name ?? "").filter((n) => n.length > 0) : []
1281
+ };
1282
+ }
1283
+ function stripKody2Mentions(body) {
1284
+ return body.replace(/(@)(kody2)/gi, "$1\u200B$2");
1285
+ }
1286
+ function postIssueComment(issueNumber, body, cwd) {
1287
+ try {
1288
+ gh2(["issue", "comment", String(issueNumber), "--body-file", "-"], { input: stripKody2Mentions(body), cwd });
1289
+ } catch (err) {
1290
+ process.stderr.write(
1291
+ `[kody2] failed to post comment on #${issueNumber}: ${err instanceof Error ? err.message : String(err)}
1292
+ `
1293
+ );
1294
+ }
1295
+ }
1296
+ function truncate2(s, maxBytes) {
1297
+ if (s.length <= maxBytes) return s;
1298
+ return `${s.slice(0, maxBytes)}\u2026 (+${s.length - maxBytes} chars)`;
1299
+ }
1300
+ function getPr(prNumber, cwd) {
1301
+ const output = gh2(["pr", "view", String(prNumber), "--json", "number,title,body,headRefName,baseRefName,state"], {
1302
+ cwd
1303
+ });
1304
+ const parsed = JSON.parse(output);
1305
+ if (typeof parsed?.title !== "string") {
1306
+ throw new Error(`PR #${prNumber}: unexpected response shape`);
1307
+ }
1308
+ return {
1309
+ number: parsed.number ?? prNumber,
1310
+ title: parsed.title,
1311
+ body: parsed.body ?? "",
1312
+ headRefName: String(parsed.headRefName ?? ""),
1313
+ baseRefName: String(parsed.baseRefName ?? ""),
1314
+ state: String(parsed.state ?? "")
1315
+ };
1316
+ }
1317
+ function getPrDiff(prNumber, cwd) {
1318
+ try {
1319
+ return gh2(["pr", "diff", String(prNumber)], { cwd });
1320
+ } catch (err) {
1321
+ process.stderr.write(
1322
+ `[kody2] failed to fetch diff for PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
1323
+ `
1324
+ );
1325
+ return "";
1326
+ }
1327
+ }
1328
+ function getPrReviews(prNumber, cwd) {
1329
+ try {
1330
+ const output = gh2(["pr", "view", String(prNumber), "--json", "reviews"], { cwd });
1331
+ const parsed = JSON.parse(output);
1332
+ if (!Array.isArray(parsed?.reviews)) return [];
1333
+ return parsed.reviews.map(
1334
+ (r) => ({
1335
+ body: r.body ?? "",
1336
+ state: r.state ?? "",
1337
+ author: r.author?.login ?? "unknown",
1338
+ submittedAt: r.submittedAt ?? ""
1339
+ })
1340
+ );
1341
+ } catch {
1342
+ return [];
1343
+ }
1344
+ }
1345
+ function getPrComments(prNumber, cwd) {
1346
+ try {
1347
+ const output = gh2(["pr", "view", String(prNumber), "--json", "comments"], { cwd });
1348
+ const parsed = JSON.parse(output);
1349
+ if (!Array.isArray(parsed?.comments)) return [];
1350
+ return parsed.comments.map((c) => ({
1351
+ body: c.body ?? "",
1352
+ author: c.author?.login ?? "unknown",
1353
+ createdAt: c.createdAt ?? ""
1354
+ })).filter((c) => c.body.trim().length > 0);
1355
+ } catch {
1356
+ return [];
1357
+ }
1358
+ }
1359
+ var VERDICT_HEADING = /(^|\n)\s*#{1,6}\s*Verdict\s*:/i;
1360
+ function isReviewShaped(body) {
1361
+ return VERDICT_HEADING.test(body);
1362
+ }
1363
+ function getPrLatestReviewBody(prNumber, cwd) {
1364
+ const reviews = getPrReviews(prNumber, cwd).filter((r) => r.body.trim().length > 0).map((r) => ({ body: r.body, at: r.submittedAt }));
1365
+ const comments = getPrComments(prNumber, cwd).filter((c) => isReviewShaped(c.body)).map((c) => ({ body: c.body, at: c.createdAt }));
1366
+ const all = [...reviews, ...comments].sort((a, b) => (b.at || "").localeCompare(a.at || ""));
1367
+ if (all.length > 0) return all[0].body;
1368
+ const pr = getPr(prNumber, cwd);
1369
+ return pr.body;
1370
+ }
1371
+ function postPrReviewComment(prNumber, body, cwd) {
1372
+ try {
1373
+ gh2(["pr", "comment", String(prNumber), "--body-file", "-"], { input: stripKody2Mentions(body), cwd });
1374
+ } catch (err) {
1375
+ process.stderr.write(
1376
+ `[kody2] failed to post review comment on PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
1377
+ `
1378
+ );
1379
+ }
1380
+ }
1381
+
1382
+ // src/scripts/applyApprovals.ts
1383
+ var API_TIMEOUT_MS4 = 3e4;
1384
+ var ALL_APPROVE_LABELS = [
1385
+ { label: "kody-approve:all", description: "kody2: approve all soft risk gates" },
1386
+ { label: "kody-approve:secrets", description: "kody2: approve the secrets risk gate and resume the flow" },
1387
+ { label: "kody-approve:workflow-edit", description: "kody2: approve the workflow-edit risk gate and resume the flow" },
1388
+ { label: "kody-approve:large-diff", description: "kody2: approve the large-diff risk gate and resume the flow" },
1389
+ { label: "kody-approve:dep-change", description: "kody2: approve the dep-change risk gate and resume the flow" },
1390
+ { label: "kody-approve:test-deletion", description: "kody2: approve the test-deletion risk gate and resume the flow" }
1391
+ ];
1392
+ var applyApprovals = async (ctx) => {
1393
+ const issueArg = typeof ctx.args.issue === "number" ? ctx.args.issue : null;
1394
+ const prArg = typeof ctx.args.pr === "number" ? ctx.args.pr : null;
1395
+ const currentTarget = issueArg ? { type: "issue", number: issueArg } : prArg ? { type: "pr", number: prArg } : null;
1396
+ if (!currentTarget) {
1397
+ ctx.output.exitCode = 64;
1398
+ ctx.output.reason = "approve: must be invoked with --issue or --pr";
1399
+ return;
1400
+ }
1401
+ let state = null;
1402
+ try {
1403
+ state = readTaskState(currentTarget.type, currentTarget.number, ctx.cwd);
1404
+ } catch {
1405
+ state = null;
1406
+ }
1407
+ const issueNumber = currentTarget.type === "issue" ? currentTarget.number : state?.flow?.issueNumber ?? null;
1408
+ const prNumber = currentTarget.type === "pr" ? currentTarget.number : parsePrNumber(state?.core?.prUrl);
1409
+ const targets = uniquePairs(
1410
+ [
1411
+ { type: "issue", number: issueNumber },
1412
+ { type: "pr", number: prNumber }
1413
+ ].filter((t) => typeof t.number === "number" && t.number > 0)
1414
+ );
1415
+ for (const spec of ALL_APPROVE_LABELS) {
1416
+ ensureLabel(spec, ctx.cwd);
1417
+ }
1418
+ for (const t of targets) {
1419
+ for (const spec of ALL_APPROVE_LABELS) {
1420
+ addLabel(t.number, spec.label, ctx.cwd);
1421
+ }
1422
+ }
1423
+ const confirmation = formatConfirmation(currentTarget, targets, state);
1424
+ try {
1425
+ if (currentTarget.type === "issue") postIssueComment(currentTarget.number, confirmation, ctx.cwd);
1426
+ else postPrReviewComment(currentTarget.number, confirmation, ctx.cwd);
1427
+ } catch {
1428
+ }
1429
+ const flowName = state?.flow?.name;
1430
+ if (issueNumber && typeof flowName === "string" && flowName.length > 0) {
1431
+ try {
1432
+ execFileSync5("gh", ["issue", "comment", String(issueNumber), "--body", `@kody2 ${flowName}`], {
1433
+ timeout: API_TIMEOUT_MS4,
1434
+ cwd: ctx.cwd,
1435
+ stdio: ["ignore", "pipe", "pipe"]
1436
+ });
1437
+ } catch (err) {
1438
+ process.stderr.write(
1439
+ `[kody2 approve] failed to re-trigger flow on issue #${issueNumber}: ${err instanceof Error ? err.message : String(err)}
1440
+ `
1441
+ );
1442
+ }
1443
+ }
1444
+ ctx.output.exitCode = 0;
1445
+ };
1446
+ function parsePrNumber(url) {
1447
+ if (!url) return null;
1448
+ const m = url.match(/\/pull\/(\d+)(?:[/?#]|$)/);
1449
+ if (!m) return null;
1450
+ const n = parseInt(m[1], 10);
1451
+ return Number.isFinite(n) ? n : null;
1452
+ }
1453
+ function uniquePairs(pairs) {
1454
+ const seen = /* @__PURE__ */ new Set();
1455
+ const out = [];
1456
+ for (const p of pairs) {
1457
+ const key = `${p.type}:${p.number}`;
1458
+ if (seen.has(key)) continue;
1459
+ seen.add(key);
1460
+ out.push(p);
1461
+ }
1462
+ return out;
1463
+ }
1464
+ function ensureLabel(spec, cwd) {
1465
+ try {
1466
+ gh2(
1467
+ ["label", "create", spec.label, "--force", "--color", "0e8a16", "--description", spec.description],
1468
+ { cwd }
1469
+ );
1470
+ } catch {
1471
+ }
1472
+ }
1473
+ function addLabel(number, label, cwd) {
1474
+ try {
1475
+ gh2(["issue", "edit", String(number), "--add-label", label], { cwd });
1476
+ } catch {
1477
+ }
1478
+ }
1479
+ function formatConfirmation(current, targets, state) {
1480
+ const other = targets.find((t) => t.type !== current.type);
1481
+ const lines = [];
1482
+ lines.push("\u2705 **kody2 risk gates approved.**");
1483
+ lines.push("");
1484
+ lines.push(
1485
+ `Applied \`kody-approve:*\` labels on ${targets.map((t) => `${t.type} #${t.number}`).join(" and ")}.`
1486
+ );
1487
+ if (other && current.type === "pr") {
1488
+ lines.push(`Mirrored to the originating issue (#${other.number}) so the orchestrator also sees the approval.`);
1489
+ } else if (other && current.type === "issue") {
1490
+ lines.push(`Mirrored to PR #${other.number} so a pending \`fix\` primitive also passes the gate.`);
1491
+ }
1492
+ const flowName = state?.flow?.name;
1493
+ if (flowName) {
1494
+ lines.push("");
1495
+ lines.push(`Re-triggering the \`${flowName}\` flow now \u2014 it will resume from the existing branch/PR checkpoint.`);
1496
+ } else {
1497
+ lines.push("");
1498
+ lines.push("No active flow found in task state. Post `@kody2 <command>` to resume manually.");
1499
+ }
1500
+ return lines.join("\n");
1501
+ }
1502
+
1235
1503
  // src/scripts/buildSyntheticPlugin.ts
1236
1504
  import * as fs8 from "fs";
1237
1505
  import * as os2 from "os";
@@ -1326,7 +1594,7 @@ function copyDir(src, dst) {
1326
1594
  }
1327
1595
 
1328
1596
  // src/coverage.ts
1329
- import { execFileSync as execFileSync4 } from "child_process";
1597
+ import { execFileSync as execFileSync6 } from "child_process";
1330
1598
  function patternToRegex(pattern) {
1331
1599
  let s = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
1332
1600
  s = s.replace(/\*\*\//g, "\xA7S").replace(/\*\*/g, "\xA7A").replace(/\*/g, "[^/]*");
@@ -1344,7 +1612,7 @@ function renderSiblingPath(file, requireSibling) {
1344
1612
  }
1345
1613
  function safeGit(args, cwd) {
1346
1614
  try {
1347
- return execFileSync4("git", args, { encoding: "utf-8", cwd, env: { ...process.env, HUSKY: "0" } }).trim();
1615
+ return execFileSync6("git", args, { encoding: "utf-8", cwd, env: { ...process.env, HUSKY: "0" } }).trim();
1348
1616
  } catch {
1349
1617
  return "";
1350
1618
  }
@@ -1550,10 +1818,10 @@ function defaultLabelMap() {
1550
1818
  }
1551
1819
 
1552
1820
  // src/scripts/commitAndPush.ts
1553
- import { execFileSync as execFileSync6 } from "child_process";
1821
+ import { execFileSync as execFileSync8 } from "child_process";
1554
1822
 
1555
1823
  // src/commit.ts
1556
- import { execFileSync as execFileSync5 } from "child_process";
1824
+ import { execFileSync as execFileSync7 } from "child_process";
1557
1825
  import * as fs10 from "fs";
1558
1826
  import * as path9 from "path";
1559
1827
  var FORBIDDEN_PATH_PREFIXES = [
@@ -1583,7 +1851,7 @@ var CONVENTIONAL_PREFIXES = [
1583
1851
  ];
1584
1852
  function git(args, cwd) {
1585
1853
  try {
1586
- return execFileSync5("git", args, {
1854
+ return execFileSync7("git", args, {
1587
1855
  encoding: "utf-8",
1588
1856
  timeout: 12e4,
1589
1857
  cwd,
@@ -1641,7 +1909,7 @@ function isForbiddenPath(p) {
1641
1909
  return false;
1642
1910
  }
1643
1911
  function listChangedFiles(cwd) {
1644
- const raw = execFileSync5("git", ["status", "--porcelain=v1", "-z"], {
1912
+ const raw = execFileSync7("git", ["status", "--porcelain=v1", "-z"], {
1645
1913
  encoding: "utf-8",
1646
1914
  cwd,
1647
1915
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
@@ -1653,7 +1921,7 @@ function listChangedFiles(cwd) {
1653
1921
  }
1654
1922
  function listFilesInCommit(ref = "HEAD", cwd) {
1655
1923
  try {
1656
- const raw = execFileSync5("git", ["show", "--name-only", "--pretty=format:", "-z", ref], {
1924
+ const raw = execFileSync7("git", ["show", "--name-only", "--pretty=format:", "-z", ref], {
1657
1925
  encoding: "utf-8",
1658
1926
  cwd,
1659
1927
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
@@ -1733,7 +2001,7 @@ var commitAndPush2 = async (ctx, profile) => {
1733
2001
  const kind = profile.name;
1734
2002
  if (kind === "resolve") {
1735
2003
  try {
1736
- execFileSync6("git", ["add", "-A"], { cwd: ctx.cwd, env: { ...process.env, HUSKY: "0" }, stdio: "pipe" });
2004
+ execFileSync8("git", ["add", "-A"], { cwd: ctx.cwd, env: { ...process.env, HUSKY: "0" }, stdio: "pipe" });
1737
2005
  } catch {
1738
2006
  }
1739
2007
  } else {
@@ -2355,8 +2623,8 @@ var discoverQaContext = async (ctx) => {
2355
2623
  };
2356
2624
 
2357
2625
  // src/scripts/dispatch.ts
2358
- import { execFileSync as execFileSync7 } from "child_process";
2359
- var API_TIMEOUT_MS3 = 3e4;
2626
+ import { execFileSync as execFileSync9 } from "child_process";
2627
+ var API_TIMEOUT_MS5 = 3e4;
2360
2628
  var dispatch = async (ctx, _profile, _agentResult, args) => {
2361
2629
  const next = args?.next;
2362
2630
  if (!next) {
@@ -2378,8 +2646,8 @@ var dispatch = async (ctx, _profile, _agentResult, args) => {
2378
2646
  const sub = usePr ? "pr" : "issue";
2379
2647
  const body = `@kody2 ${next}`;
2380
2648
  try {
2381
- execFileSync7("gh", [sub, "comment", String(targetNumber), "--body", body], {
2382
- timeout: API_TIMEOUT_MS3,
2649
+ execFileSync9("gh", [sub, "comment", String(targetNumber), "--body", body], {
2650
+ timeout: API_TIMEOUT_MS5,
2383
2651
  cwd: ctx.cwd,
2384
2652
  stdio: ["ignore", "pipe", "pipe"]
2385
2653
  });
@@ -2397,141 +2665,6 @@ function parsePr(url) {
2397
2665
  return Number.isFinite(n) ? n : null;
2398
2666
  }
2399
2667
 
2400
- // src/issue.ts
2401
- import { execFileSync as execFileSync8 } from "child_process";
2402
- var API_TIMEOUT_MS4 = 3e4;
2403
- function ghToken2() {
2404
- return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
2405
- }
2406
- function gh2(args, options) {
2407
- const token = ghToken2();
2408
- const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
2409
- return execFileSync8("gh", args, {
2410
- encoding: "utf-8",
2411
- timeout: API_TIMEOUT_MS4,
2412
- cwd: options?.cwd,
2413
- env,
2414
- input: options?.input,
2415
- stdio: options?.input ? ["pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
2416
- }).trim();
2417
- }
2418
- function getIssue(issueNumber, cwd) {
2419
- const output = gh2(["issue", "view", String(issueNumber), "--json", "number,title,body,comments,labels"], { cwd });
2420
- const parsed = JSON.parse(output);
2421
- if (typeof parsed?.title !== "string") {
2422
- throw new Error(`Issue #${issueNumber}: unexpected response shape`);
2423
- }
2424
- return {
2425
- number: parsed.number ?? issueNumber,
2426
- title: parsed.title,
2427
- body: parsed.body ?? "",
2428
- comments: (parsed.comments ?? []).map((c) => ({
2429
- body: c.body ?? "",
2430
- author: c.author?.login ?? "unknown",
2431
- createdAt: c.createdAt ?? ""
2432
- })),
2433
- labels: Array.isArray(parsed.labels) ? parsed.labels.map((l) => l.name ?? "").filter((n) => n.length > 0) : []
2434
- };
2435
- }
2436
- function stripKody2Mentions(body) {
2437
- return body.replace(/(@)(kody2)/gi, "$1\u200B$2");
2438
- }
2439
- function postIssueComment(issueNumber, body, cwd) {
2440
- try {
2441
- gh2(["issue", "comment", String(issueNumber), "--body-file", "-"], { input: stripKody2Mentions(body), cwd });
2442
- } catch (err) {
2443
- process.stderr.write(
2444
- `[kody2] failed to post comment on #${issueNumber}: ${err instanceof Error ? err.message : String(err)}
2445
- `
2446
- );
2447
- }
2448
- }
2449
- function truncate2(s, maxBytes) {
2450
- if (s.length <= maxBytes) return s;
2451
- return `${s.slice(0, maxBytes)}\u2026 (+${s.length - maxBytes} chars)`;
2452
- }
2453
- function getPr(prNumber, cwd) {
2454
- const output = gh2(["pr", "view", String(prNumber), "--json", "number,title,body,headRefName,baseRefName,state"], {
2455
- cwd
2456
- });
2457
- const parsed = JSON.parse(output);
2458
- if (typeof parsed?.title !== "string") {
2459
- throw new Error(`PR #${prNumber}: unexpected response shape`);
2460
- }
2461
- return {
2462
- number: parsed.number ?? prNumber,
2463
- title: parsed.title,
2464
- body: parsed.body ?? "",
2465
- headRefName: String(parsed.headRefName ?? ""),
2466
- baseRefName: String(parsed.baseRefName ?? ""),
2467
- state: String(parsed.state ?? "")
2468
- };
2469
- }
2470
- function getPrDiff(prNumber, cwd) {
2471
- try {
2472
- return gh2(["pr", "diff", String(prNumber)], { cwd });
2473
- } catch (err) {
2474
- process.stderr.write(
2475
- `[kody2] failed to fetch diff for PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
2476
- `
2477
- );
2478
- return "";
2479
- }
2480
- }
2481
- function getPrReviews(prNumber, cwd) {
2482
- try {
2483
- const output = gh2(["pr", "view", String(prNumber), "--json", "reviews"], { cwd });
2484
- const parsed = JSON.parse(output);
2485
- if (!Array.isArray(parsed?.reviews)) return [];
2486
- return parsed.reviews.map(
2487
- (r) => ({
2488
- body: r.body ?? "",
2489
- state: r.state ?? "",
2490
- author: r.author?.login ?? "unknown",
2491
- submittedAt: r.submittedAt ?? ""
2492
- })
2493
- );
2494
- } catch {
2495
- return [];
2496
- }
2497
- }
2498
- function getPrComments(prNumber, cwd) {
2499
- try {
2500
- const output = gh2(["pr", "view", String(prNumber), "--json", "comments"], { cwd });
2501
- const parsed = JSON.parse(output);
2502
- if (!Array.isArray(parsed?.comments)) return [];
2503
- return parsed.comments.map((c) => ({
2504
- body: c.body ?? "",
2505
- author: c.author?.login ?? "unknown",
2506
- createdAt: c.createdAt ?? ""
2507
- })).filter((c) => c.body.trim().length > 0);
2508
- } catch {
2509
- return [];
2510
- }
2511
- }
2512
- var VERDICT_HEADING = /(^|\n)\s*#{1,6}\s*Verdict\s*:/i;
2513
- function isReviewShaped(body) {
2514
- return VERDICT_HEADING.test(body);
2515
- }
2516
- function getPrLatestReviewBody(prNumber, cwd) {
2517
- const reviews = getPrReviews(prNumber, cwd).filter((r) => r.body.trim().length > 0).map((r) => ({ body: r.body, at: r.submittedAt }));
2518
- const comments = getPrComments(prNumber, cwd).filter((c) => isReviewShaped(c.body)).map((c) => ({ body: c.body, at: c.createdAt }));
2519
- const all = [...reviews, ...comments].sort((a, b) => (b.at || "").localeCompare(a.at || ""));
2520
- if (all.length > 0) return all[0].body;
2521
- const pr = getPr(prNumber, cwd);
2522
- return pr.body;
2523
- }
2524
- function postPrReviewComment(prNumber, body, cwd) {
2525
- try {
2526
- gh2(["pr", "comment", String(prNumber), "--body-file", "-"], { input: stripKody2Mentions(body), cwd });
2527
- } catch (err) {
2528
- process.stderr.write(
2529
- `[kody2] failed to post review comment on PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
2530
- `
2531
- );
2532
- }
2533
- }
2534
-
2535
2668
  // src/pr.ts
2536
2669
  var TITLE_MAX = 72;
2537
2670
  function stripTitlePrefixes(raw) {
@@ -2697,7 +2830,7 @@ function computeFailureReason(ctx) {
2697
2830
  }
2698
2831
 
2699
2832
  // src/scripts/finishFlow.ts
2700
- import { execFileSync as execFileSync9 } from "child_process";
2833
+ import { execFileSync as execFileSync10 } from "child_process";
2701
2834
 
2702
2835
  // src/registry.ts
2703
2836
  import * as fs14 from "fs";
@@ -2816,7 +2949,7 @@ function getIssueLabels(issueNumber, cwd) {
2816
2949
  return [];
2817
2950
  }
2818
2951
  }
2819
- function addLabel(issueNumber, label, cwd) {
2952
+ function addLabel2(issueNumber, label, cwd) {
2820
2953
  gh2(["issue", "edit", String(issueNumber), "--add-label", label], { cwd });
2821
2954
  }
2822
2955
  function removeLabel(issueNumber, label, cwd) {
@@ -2848,12 +2981,12 @@ function setKodyLabel(issueNumber, spec, cwd) {
2848
2981
  }
2849
2982
  }
2850
2983
  try {
2851
- addLabel(issueNumber, target, cwd);
2984
+ addLabel2(issueNumber, target, cwd);
2852
2985
  } catch (err) {
2853
2986
  if (looksLikeMissingLabel(err)) {
2854
2987
  try {
2855
2988
  createLabelInRepo(spec, cwd);
2856
- addLabel(issueNumber, target, cwd);
2989
+ addLabel2(issueNumber, target, cwd);
2857
2990
  return;
2858
2991
  } catch (retryErr) {
2859
2992
  process.stderr.write(
@@ -2885,7 +3018,7 @@ function errMsg(err) {
2885
3018
  }
2886
3019
 
2887
3020
  // src/scripts/finishFlow.ts
2888
- var API_TIMEOUT_MS5 = 3e4;
3021
+ var API_TIMEOUT_MS6 = 3e4;
2889
3022
  var STATUS_ICON = {
2890
3023
  "review-passed": "\u2705",
2891
3024
  "fix-applied": "\u2705",
@@ -2917,8 +3050,8 @@ var finishFlow = async (ctx, _profile, _agentResult, args) => {
2917
3050
  **PR:** ${state.core.prUrl}` : "";
2918
3051
  const body = `${icon} kody2 flow \`${flowName}\` finished \u2014 \`${reason}\`${prSuffix}`;
2919
3052
  try {
2920
- execFileSync9("gh", ["issue", "comment", String(issueNumber), "--body", body], {
2921
- timeout: API_TIMEOUT_MS5,
3053
+ execFileSync10("gh", ["issue", "comment", String(issueNumber), "--body", body], {
3054
+ timeout: API_TIMEOUT_MS6,
2922
3055
  cwd: ctx.cwd,
2923
3056
  stdio: ["ignore", "pipe", "pipe"]
2924
3057
  });
@@ -2931,7 +3064,7 @@ var finishFlow = async (ctx, _profile, _agentResult, args) => {
2931
3064
  };
2932
3065
 
2933
3066
  // src/branch.ts
2934
- import { execFileSync as execFileSync10 } from "child_process";
3067
+ import { execFileSync as execFileSync11 } from "child_process";
2935
3068
  var UncommittedChangesError = class extends Error {
2936
3069
  constructor(branch) {
2937
3070
  super(`Uncommitted changes on branch '${branch}' \u2014 refusing to run to protect work in progress`);
@@ -2941,7 +3074,7 @@ var UncommittedChangesError = class extends Error {
2941
3074
  branch;
2942
3075
  };
2943
3076
  function git2(args, cwd) {
2944
- return execFileSync10("git", args, {
3077
+ return execFileSync11("git", args, {
2945
3078
  encoding: "utf-8",
2946
3079
  timeout: 3e4,
2947
3080
  cwd,
@@ -2966,7 +3099,7 @@ function checkoutPrBranch(prNumber, cwd) {
2966
3099
  SKIP_HOOKS: "1",
2967
3100
  GH_TOKEN: process.env.GH_PAT?.trim() || process.env.GH_TOKEN || ""
2968
3101
  };
2969
- execFileSync10("gh", ["pr", "checkout", String(prNumber)], {
3102
+ execFileSync11("gh", ["pr", "checkout", String(prNumber)], {
2970
3103
  cwd,
2971
3104
  env,
2972
3105
  stdio: ["ignore", "pipe", "pipe"],
@@ -3033,7 +3166,7 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd) {
3033
3166
  }
3034
3167
 
3035
3168
  // src/gha.ts
3036
- import { execFileSync as execFileSync11 } from "child_process";
3169
+ import { execFileSync as execFileSync12 } from "child_process";
3037
3170
  import * as fs15 from "fs";
3038
3171
  function getRunUrl() {
3039
3172
  const server = process.env.GITHUB_SERVER_URL;
@@ -3076,7 +3209,7 @@ function reactToTriggerComment(cwd) {
3076
3209
  for (let attempt = 0; attempt < 3; attempt++) {
3077
3210
  if (attempt > 0) sleepMs(attempt === 1 ? 500 : 1500);
3078
3211
  try {
3079
- execFileSync11("gh", args, opts);
3212
+ execFileSync12("gh", args, opts);
3080
3213
  return;
3081
3214
  } catch (err) {
3082
3215
  lastErr = err;
@@ -3089,13 +3222,13 @@ function reactToTriggerComment(cwd) {
3089
3222
  }
3090
3223
  function sleepMs(ms) {
3091
3224
  try {
3092
- execFileSync11("sleep", [(ms / 1e3).toString()], { stdio: "ignore", timeout: ms + 1e3 });
3225
+ execFileSync12("sleep", [(ms / 1e3).toString()], { stdio: "ignore", timeout: ms + 1e3 });
3093
3226
  } catch {
3094
3227
  }
3095
3228
  }
3096
3229
 
3097
3230
  // src/workflow.ts
3098
- import { execFileSync as execFileSync12 } from "child_process";
3231
+ import { execFileSync as execFileSync13 } from "child_process";
3099
3232
  var GH_TIMEOUT_MS = 3e4;
3100
3233
  function ghToken3() {
3101
3234
  return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
@@ -3103,7 +3236,7 @@ function ghToken3() {
3103
3236
  function gh3(args, cwd) {
3104
3237
  const token = ghToken3();
3105
3238
  const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
3106
- return execFileSync12("gh", args, {
3239
+ return execFileSync13("gh", args, {
3107
3240
  encoding: "utf-8",
3108
3241
  timeout: GH_TIMEOUT_MS,
3109
3242
  cwd,
@@ -3287,7 +3420,7 @@ function tryPostPr2(prNumber, body, cwd) {
3287
3420
  }
3288
3421
 
3289
3422
  // src/scripts/initFlow.ts
3290
- import { execFileSync as execFileSync13 } from "child_process";
3423
+ import { execFileSync as execFileSync14 } from "child_process";
3291
3424
  import * as fs17 from "fs";
3292
3425
  import * as path15 from "path";
3293
3426
 
@@ -3328,7 +3461,7 @@ function qualityCommandsFor(pm) {
3328
3461
  function detectOwnerRepo(cwd) {
3329
3462
  let url;
3330
3463
  try {
3331
- url = execFileSync13("git", ["remote", "get-url", "origin"], {
3464
+ url = execFileSync14("git", ["remote", "get-url", "origin"], {
3332
3465
  cwd,
3333
3466
  encoding: "utf-8",
3334
3467
  stdio: ["ignore", "pipe", "pipe"]
@@ -3412,7 +3545,7 @@ jobs:
3412
3545
  `;
3413
3546
  function defaultBranchFromGit(cwd) {
3414
3547
  try {
3415
- const ref = execFileSync13("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
3548
+ const ref = execFileSync14("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
3416
3549
  cwd,
3417
3550
  encoding: "utf-8",
3418
3551
  stdio: ["ignore", "pipe", "pipe"]
@@ -3420,7 +3553,7 @@ function defaultBranchFromGit(cwd) {
3420
3553
  return ref.replace("refs/remotes/origin/", "");
3421
3554
  } catch {
3422
3555
  try {
3423
- return execFileSync13("git", ["branch", "--show-current"], {
3556
+ return execFileSync14("git", ["branch", "--show-current"], {
3424
3557
  cwd,
3425
3558
  encoding: "utf-8",
3426
3559
  stdio: ["ignore", "pipe", "pipe"]
@@ -3610,7 +3743,7 @@ var mirrorStateToPr = async (ctx) => {
3610
3743
  if (!issueNumber || issueTarget !== "issue") return;
3611
3744
  const prUrl = ctx.output.prUrl ?? ctx.data.prResult?.url;
3612
3745
  if (!prUrl) return;
3613
- const prNumber = parsePrNumber(prUrl);
3746
+ const prNumber = parsePrNumber2(prUrl);
3614
3747
  if (!prNumber) return;
3615
3748
  let state;
3616
3749
  try {
@@ -3628,7 +3761,7 @@ var mirrorStateToPr = async (ctx) => {
3628
3761
  );
3629
3762
  }
3630
3763
  };
3631
- function parsePrNumber(prUrl) {
3764
+ function parsePrNumber2(prUrl) {
3632
3765
  const m = prUrl.match(/\/pull\/(\d+)(?:[/?#]|$)/);
3633
3766
  if (!m) return null;
3634
3767
  const n = parseInt(m[1], 10);
@@ -3713,8 +3846,8 @@ var persistFlowState = async (ctx) => {
3713
3846
  };
3714
3847
 
3715
3848
  // src/scripts/postClassification.ts
3716
- import { execFileSync as execFileSync14 } from "child_process";
3717
- var API_TIMEOUT_MS6 = 3e4;
3849
+ import { execFileSync as execFileSync15 } from "child_process";
3850
+ var API_TIMEOUT_MS7 = 3e4;
3718
3851
  var VALID_CLASSES2 = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
3719
3852
  var postClassification = async (ctx) => {
3720
3853
  const issueNumber = ctx.args.issue;
@@ -3743,9 +3876,9 @@ var postClassification = async (ctx) => {
3743
3876
  ctx.cwd
3744
3877
  );
3745
3878
  try {
3746
- execFileSync14("gh", ["issue", "comment", String(issueNumber), "--body", `@kody2 ${classification}`], {
3879
+ execFileSync15("gh", ["issue", "comment", String(issueNumber), "--body", `@kody2 ${classification}`], {
3747
3880
  cwd: ctx.cwd,
3748
- timeout: API_TIMEOUT_MS6,
3881
+ timeout: API_TIMEOUT_MS7,
3749
3882
  stdio: ["ignore", "pipe", "pipe"]
3750
3883
  });
3751
3884
  } catch (err) {
@@ -3777,9 +3910,9 @@ function parseClassification(prSummary) {
3777
3910
  }
3778
3911
  function tryAuditComment(issueNumber, body, cwd) {
3779
3912
  try {
3780
- execFileSync14("gh", ["issue", "comment", String(issueNumber), "--body", body], {
3913
+ execFileSync15("gh", ["issue", "comment", String(issueNumber), "--body", body], {
3781
3914
  cwd,
3782
- timeout: API_TIMEOUT_MS6,
3915
+ timeout: API_TIMEOUT_MS7,
3783
3916
  stdio: ["ignore", "pipe", "pipe"]
3784
3917
  });
3785
3918
  } catch {
@@ -3961,7 +4094,7 @@ REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.
3961
4094
  };
3962
4095
 
3963
4096
  // src/scripts/releaseFlow.ts
3964
- import { execFileSync as execFileSync15, spawnSync } from "child_process";
4097
+ import { execFileSync as execFileSync16, spawnSync } from "child_process";
3965
4098
  import * as fs18 from "fs";
3966
4099
  import * as path16 from "path";
3967
4100
  function bumpVersion(current, bump) {
@@ -3991,7 +4124,7 @@ function generateChangelog(cwd, newVersion, lastTag) {
3991
4124
  const range = lastTag ? `${lastTag}..HEAD` : "HEAD";
3992
4125
  let log = "";
3993
4126
  try {
3994
- log = execFileSync15("git", ["log", range, "--pretty=format:%s||%h", "--no-merges"], {
4127
+ log = execFileSync16("git", ["log", range, "--pretty=format:%s||%h", "--no-merges"], {
3995
4128
  cwd,
3996
4129
  encoding: "utf-8",
3997
4130
  stdio: ["ignore", "pipe", "pipe"]
@@ -4048,7 +4181,7 @@ ${entry}${prior.slice(idx + 1)}`);
4048
4181
  }
4049
4182
  }
4050
4183
  function git3(args, cwd, timeout = 6e4) {
4051
- return execFileSync15("git", args, {
4184
+ return execFileSync16("git", args, {
4052
4185
  encoding: "utf-8",
4053
4186
  timeout,
4054
4187
  cwd,
@@ -4351,7 +4484,7 @@ var resolveArtifacts = async (ctx, profile) => {
4351
4484
  };
4352
4485
 
4353
4486
  // src/scripts/resolveFlow.ts
4354
- import { execFileSync as execFileSync16 } from "child_process";
4487
+ import { execFileSync as execFileSync17 } from "child_process";
4355
4488
  var CONFLICT_DIFF_MAX_BYTES = 4e4;
4356
4489
  var resolveFlow = async (ctx) => {
4357
4490
  const prNumber = ctx.args.pr;
@@ -4403,7 +4536,7 @@ var resolveFlow = async (ctx) => {
4403
4536
  };
4404
4537
  function getConflictedFiles(cwd) {
4405
4538
  try {
4406
- const out = execFileSync16("git", ["diff", "--name-only", "--diff-filter=U"], {
4539
+ const out = execFileSync17("git", ["diff", "--name-only", "--diff-filter=U"], {
4407
4540
  encoding: "utf-8",
4408
4541
  cwd,
4409
4542
  env: { ...process.env, HUSKY: "0" }
@@ -4418,7 +4551,7 @@ function getConflictMarkersPreview(files, cwd, maxBytes = CONFLICT_DIFF_MAX_BYTE
4418
4551
  let total = 0;
4419
4552
  for (const f of files) {
4420
4553
  try {
4421
- const content = execFileSync16("cat", [f], { encoding: "utf-8", cwd }).toString();
4554
+ const content = execFileSync17("cat", [f], { encoding: "utf-8", cwd }).toString();
4422
4555
  const snippet = `### ${f}
4423
4556
 
4424
4557
  \`\`\`
@@ -4487,7 +4620,7 @@ function tryPostPr4(prNumber, body, cwd) {
4487
4620
  }
4488
4621
 
4489
4622
  // src/scripts/riskGate.ts
4490
- import { execFileSync as execFileSync17 } from "child_process";
4623
+ import { execFileSync as execFileSync18 } from "child_process";
4491
4624
  var ALL_GATES = ["secrets", "workflow-edit", "large-diff", "dep-change", "test-deletion"];
4492
4625
  var HARD_GATES = /* @__PURE__ */ new Set(["secrets"]);
4493
4626
  var APPROVE_ALL = "kody-approve:all";
@@ -4504,7 +4637,7 @@ var riskGate = async (ctx, profile, _agent, args) => {
4504
4637
  }
4505
4638
  const targetType = ctx.data.commentTargetType;
4506
4639
  const targetNumber = Number(ctx.data.commentTargetNumber ?? 0);
4507
- const labels = targetNumber > 0 ? getIssueLabels(targetNumber, ctx.cwd) : [];
4640
+ const labels = collectApprovalLabels(ctx, targetType, targetNumber);
4508
4641
  const approveAll = labels.includes(APPROVE_ALL);
4509
4642
  const pending = violations.filter((v) => !isApproved(v, labels, approveAll));
4510
4643
  ctx.data.riskGate = {
@@ -4528,7 +4661,8 @@ var riskGate = async (ctx, profile, _agent, args) => {
4528
4661
  );
4529
4662
  } catch {
4530
4663
  }
4531
- const body = formatAdvisory(pending);
4664
+ const compareUrl = computeCompareUrl(ctx);
4665
+ const body = formatAdvisory(pending, compareUrl);
4532
4666
  try {
4533
4667
  if (targetType === "issue") postIssueComment(targetNumber, body, ctx.cwd);
4534
4668
  else postPrReviewComment(targetNumber, body, ctx.cwd);
@@ -4602,6 +4736,20 @@ function evaluateGates(ctx, profileName, changedFiles, gatesToRun, args) {
4602
4736
  }
4603
4737
  return violations;
4604
4738
  }
4739
+ function collectApprovalLabels(ctx, targetType, targetNumber) {
4740
+ const seen = /* @__PURE__ */ new Set();
4741
+ if (targetNumber > 0) {
4742
+ for (const l of getIssueLabels(targetNumber, ctx.cwd)) seen.add(l);
4743
+ }
4744
+ if (targetType === "pr") {
4745
+ const state = ctx.data.taskState;
4746
+ const issueNum = state?.flow?.issueNumber;
4747
+ if (typeof issueNum === "number" && issueNum > 0 && issueNum !== targetNumber) {
4748
+ for (const l of getIssueLabels(issueNum, ctx.cwd)) seen.add(l);
4749
+ }
4750
+ }
4751
+ return [...seen];
4752
+ }
4605
4753
  function isApproved(v, labels, approveAll) {
4606
4754
  if (labels.includes(`kody-approve:${v.name}`)) return true;
4607
4755
  if (!HARD_GATES.has(v.name) && approveAll) return true;
@@ -4627,8 +4775,13 @@ var SECRET_PATTERNS = [
4627
4775
  /\.pem$/i,
4628
4776
  /\.key$/i,
4629
4777
  /(^|\/)(id_rsa|id_ed25519|id_ecdsa)(\.|$)/i,
4630
- /credentials?(\.|\/|$)/i,
4631
- /(^|\/)(private|secret)[^/]*\.json$/i,
4778
+ // Match the keyword anywhere inside the filename, as a whole word (so e.g.
4779
+ // `api-secrets.json`, `config/app.credentials.yaml`, `user-passwords.txt`
4780
+ // all trip — while false friends like `secretary.md` do not).
4781
+ /(^|\/)[^/]*\bsecrets?\b[^/]*$/i,
4782
+ /(^|\/)[^/]*\bcredentials?\b[^/]*$/i,
4783
+ /(^|\/)[^/]*\bpasswords?\b[^/]*$/i,
4784
+ /(^|\/)[^/]*\bapi[-_]keys?\b[^/]*$/i,
4632
4785
  /(^|\/)\.netrc$/i,
4633
4786
  /(^|\/)\.npmrc$/i
4634
4787
  ];
@@ -4661,7 +4814,7 @@ function collectBranchChangedFiles(ctx) {
4661
4814
  const base = ctx.config.git.defaultBranch;
4662
4815
  for (const ref of [`origin/${base}...HEAD`, `${base}...HEAD`]) {
4663
4816
  try {
4664
- const out = execFileSync17("git", ["diff", "--name-only", ref], {
4817
+ const out = execFileSync18("git", ["diff", "--name-only", ref], {
4665
4818
  cwd: ctx.cwd,
4666
4819
  encoding: "utf-8",
4667
4820
  stdio: ["pipe", "pipe", "pipe"]
@@ -4677,7 +4830,7 @@ function computeDiffStats(ctx) {
4677
4830
  const base = ctx.config.git.defaultBranch;
4678
4831
  for (const ref of [`origin/${base}...HEAD`, `${base}...HEAD`]) {
4679
4832
  try {
4680
- const out = execFileSync17("git", ["diff", "--shortstat", ref], {
4833
+ const out = execFileSync18("git", ["diff", "--shortstat", ref], {
4681
4834
  cwd: ctx.cwd,
4682
4835
  encoding: "utf-8",
4683
4836
  stdio: ["pipe", "pipe", "pipe"]
@@ -4698,7 +4851,7 @@ function parseShortstat(s) {
4698
4851
  }
4699
4852
  function listDeletedFilesInHeadCommit(cwd) {
4700
4853
  try {
4701
- const out = execFileSync17("git", ["show", "--name-status", "--pretty=format:", "HEAD"], {
4854
+ const out = execFileSync18("git", ["show", "--name-status", "--pretty=format:", "HEAD"], {
4702
4855
  cwd,
4703
4856
  encoding: "utf-8",
4704
4857
  stdio: ["pipe", "pipe", "pipe"]
@@ -4726,28 +4879,38 @@ function ensureApproveLabel(gate, cwd) {
4726
4879
  } catch {
4727
4880
  }
4728
4881
  }
4729
- function formatAdvisory(pending) {
4882
+ function formatAdvisory(pending, compareUrl) {
4730
4883
  const lines = [];
4731
4884
  lines.push("\u23F8\uFE0F **kody2 risk gate halted the flow.**");
4732
4885
  lines.push("");
4733
- lines.push("The changes were committed and pushed, but the flow will **not progress** until a human approves:");
4886
+ lines.push("The branch was pushed but **no PR was opened** \u2014 waiting for human approval:");
4734
4887
  lines.push("");
4735
4888
  for (const v of pending) {
4736
4889
  lines.push(`- **\`${v.name}\`** _(${v.severity})_ \u2014 ${v.reason}`);
4737
4890
  }
4738
4891
  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)");
4892
+ if (compareUrl) {
4893
+ lines.push(`\u{1F4CE} Review the branch diff: ${compareUrl}`);
4894
+ lines.push("");
4744
4895
  }
4896
+ lines.push("**To approve and resume**, post a comment:");
4897
+ lines.push("");
4898
+ lines.push("> `@kody2 approve`");
4745
4899
  lines.push("");
4746
4900
  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."
4901
+ "kody2 will acknowledge all currently-pending gates (soft **and** hard), open the PR, and continue the flow from this checkpoint. No re-running the agent."
4748
4902
  );
4749
4903
  return lines.join("\n");
4750
4904
  }
4905
+ function computeCompareUrl(ctx) {
4906
+ const branch = ctx.data.branch;
4907
+ if (!branch) return null;
4908
+ const owner = ctx.config.github?.owner;
4909
+ const repo = ctx.config.github?.repo;
4910
+ if (!owner || !repo) return null;
4911
+ const base = ctx.config.git.defaultBranch;
4912
+ return `https://github.com/${owner}/${repo}/compare/${base}...${branch}`;
4913
+ }
4751
4914
 
4752
4915
  // src/scripts/runFlow.ts
4753
4916
  var runFlow = async (ctx) => {
@@ -4790,7 +4953,7 @@ var saveTaskState = async (ctx, profile) => {
4790
4953
  const action = ctx.data.action ?? synthesizeAction(ctx);
4791
4954
  if (ctx.output.prUrl && !state.core.prUrl) state.core.prUrl = ctx.output.prUrl;
4792
4955
  if (typeof ctx.data.runUrl === "string") state.core.runUrl = ctx.data.runUrl;
4793
- const next = reduce(state, executable, action);
4956
+ const next = reduce(state, executable, action, profile.phase);
4794
4957
  if (ctx.output.prUrl) next.core.prUrl = ctx.output.prUrl;
4795
4958
  if (typeof ctx.data.runUrl === "string") next.core.runUrl = ctx.data.runUrl;
4796
4959
  writeTaskState(target, number, next, ctx.cwd);
@@ -4845,8 +5008,8 @@ var skipAgent = async (ctx) => {
4845
5008
  };
4846
5009
 
4847
5010
  // src/scripts/startFlow.ts
4848
- import { execFileSync as execFileSync18 } from "child_process";
4849
- var API_TIMEOUT_MS7 = 3e4;
5011
+ import { execFileSync as execFileSync19 } from "child_process";
5012
+ var API_TIMEOUT_MS8 = 3e4;
4850
5013
  var startFlow = async (ctx, profile, _agentResult, args) => {
4851
5014
  const entry = args?.entry;
4852
5015
  if (!entry) {
@@ -4879,8 +5042,8 @@ function postKody2Comment(target, issueNumber, state, next, cwd) {
4879
5042
  const sub = target === "pr" && state?.core.prUrl ? "pr" : "issue";
4880
5043
  const body = `@kody2 ${next}`;
4881
5044
  try {
4882
- execFileSync18("gh", [sub, "comment", String(targetNumber), "--body", body], {
4883
- timeout: API_TIMEOUT_MS7,
5045
+ execFileSync19("gh", [sub, "comment", String(targetNumber), "--body", body], {
5046
+ timeout: API_TIMEOUT_MS8,
4884
5047
  cwd,
4885
5048
  stdio: ["ignore", "pipe", "pipe"]
4886
5049
  });
@@ -4899,7 +5062,7 @@ function parsePr2(url) {
4899
5062
  }
4900
5063
 
4901
5064
  // src/scripts/syncFlow.ts
4902
- import { execFileSync as execFileSync19 } from "child_process";
5065
+ import { execFileSync as execFileSync20 } from "child_process";
4903
5066
  var syncFlow = async (ctx) => {
4904
5067
  ctx.skipAgent = true;
4905
5068
  const prNumber = ctx.args.pr;
@@ -4958,7 +5121,7 @@ function bail2(ctx, prNumber, reason) {
4958
5121
  }
4959
5122
  function revParseHead(cwd) {
4960
5123
  try {
4961
- return execFileSync19("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
5124
+ return execFileSync20("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
4962
5125
  } catch {
4963
5126
  return "";
4964
5127
  }
@@ -4966,9 +5129,9 @@ function revParseHead(cwd) {
4966
5129
  function pushBranch(branch, cwd) {
4967
5130
  const env = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
4968
5131
  try {
4969
- execFileSync19("git", ["push", "-u", "origin", branch], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
5132
+ execFileSync20("git", ["push", "-u", "origin", branch], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
4970
5133
  } catch {
4971
- execFileSync19("git", ["push", "--force-with-lease", "-u", "origin", branch], {
5134
+ execFileSync20("git", ["push", "--force-with-lease", "-u", "origin", branch], {
4972
5135
  cwd,
4973
5136
  env,
4974
5137
  stdio: ["ignore", "pipe", "pipe"]
@@ -5211,7 +5374,8 @@ var postflightScripts = {
5211
5374
  advanceFlow,
5212
5375
  persistFlowState,
5213
5376
  postClassification,
5214
- riskGate
5377
+ riskGate,
5378
+ applyApprovals
5215
5379
  };
5216
5380
  var allScriptNames = /* @__PURE__ */ new Set([
5217
5381
  ...Object.keys(preflightScripts),
@@ -5219,7 +5383,7 @@ var allScriptNames = /* @__PURE__ */ new Set([
5219
5383
  ]);
5220
5384
 
5221
5385
  // src/tools.ts
5222
- import { execFileSync as execFileSync20 } from "child_process";
5386
+ import { execFileSync as execFileSync21 } from "child_process";
5223
5387
  function verifyCliTools(tools, cwd) {
5224
5388
  const out = [];
5225
5389
  for (const t of tools) out.push(verifyOne(t, cwd));
@@ -5252,7 +5416,7 @@ function verifyOne(tool, cwd) {
5252
5416
  }
5253
5417
  function runShell2(cmd, cwd, timeoutMs = 3e4) {
5254
5418
  try {
5255
- execFileSync20("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
5419
+ execFileSync21("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
5256
5420
  return true;
5257
5421
  } catch {
5258
5422
  return false;
@@ -5582,7 +5746,7 @@ function detectPackageManager2(cwd) {
5582
5746
  }
5583
5747
  function shellOut(cmd, args, cwd, stream = true) {
5584
5748
  try {
5585
- execFileSync21(cmd, args, {
5749
+ execFileSync22(cmd, args, {
5586
5750
  cwd,
5587
5751
  stdio: stream ? "inherit" : "pipe",
5588
5752
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" }
@@ -5595,7 +5759,7 @@ function shellOut(cmd, args, cwd, stream = true) {
5595
5759
  }
5596
5760
  function isOnPath(bin) {
5597
5761
  try {
5598
- execFileSync21("which", [bin], { stdio: "pipe" });
5762
+ execFileSync22("which", [bin], { stdio: "pipe" });
5599
5763
  return true;
5600
5764
  } catch {
5601
5765
  return false;
@@ -5629,7 +5793,7 @@ function installLitellmIfNeeded(cwd) {
5629
5793
  } catch {
5630
5794
  }
5631
5795
  try {
5632
- execFileSync21("python3", ["-c", "import litellm"], { stdio: "pipe" });
5796
+ execFileSync22("python3", ["-c", "import litellm"], { stdio: "pipe" });
5633
5797
  process.stdout.write("\u2192 kody2: litellm already installed\n");
5634
5798
  return 0;
5635
5799
  } catch {
@@ -5639,16 +5803,16 @@ function installLitellmIfNeeded(cwd) {
5639
5803
  }
5640
5804
  function configureGitIdentity(cwd) {
5641
5805
  try {
5642
- const name = execFileSync21("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
5806
+ const name = execFileSync22("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
5643
5807
  if (name) return;
5644
5808
  } catch {
5645
5809
  }
5646
5810
  try {
5647
- execFileSync21("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
5811
+ execFileSync22("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
5648
5812
  } catch {
5649
5813
  }
5650
5814
  try {
5651
- execFileSync21("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
5815
+ execFileSync22("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
5652
5816
  cwd,
5653
5817
  stdio: "pipe"
5654
5818
  });
@@ -5825,9 +5989,9 @@ function commitChatFiles(cwd, sessionId, verbose) {
5825
5989
  if (paths.length === 0) return;
5826
5990
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
5827
5991
  try {
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);
5992
+ execFileSync23("git", ["add", ...paths], opts);
5993
+ execFileSync23("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
5994
+ execFileSync23("git", ["push", "--quiet", "origin", "HEAD"], opts);
5831
5995
  } catch (err) {
5832
5996
  const msg = err instanceof Error ? err.message : String(err);
5833
5997
  process.stderr.write(`[kody2:chat] commit/push skipped: ${msg}