@kody-ade/kody-engine 0.4.132 → 0.4.134

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/kody.js CHANGED
@@ -880,7 +880,7 @@ var init_loadPriorArt = __esm({
880
880
  // package.json
881
881
  var package_default = {
882
882
  name: "@kody-ade/kody-engine",
883
- version: "0.4.132",
883
+ version: "0.4.134",
884
884
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
885
885
  license: "MIT",
886
886
  type: "module",
@@ -5736,6 +5736,14 @@ function closePr(prNumber, comment, cwd) {
5736
5736
  return fail(err);
5737
5737
  }
5738
5738
  }
5739
+ function mergePrSquash(prNumber, cwd) {
5740
+ try {
5741
+ gh(["pr", "merge", String(prNumber), "--squash", "--delete-branch"], { cwd });
5742
+ return { ok: true };
5743
+ } catch (err) {
5744
+ return fail(err);
5745
+ }
5746
+ }
5739
5747
  function editPrBase(prNumber, baseBranch, cwd) {
5740
5748
  try {
5741
5749
  gh(
@@ -8616,23 +8624,34 @@ function readKodyVariables(cwd) {
8616
8624
 
8617
8625
  // src/scripts/loadQaContext.ts
8618
8626
  var PROFILE_DIR_REL_PATH = ".kody/profile";
8627
+ var QA_STAFF = "qa-engineer";
8628
+ var ALL_STAFF = "*";
8629
+ var LEGACY_AUDIENCE_TO_STAFF = { chat: "kody", qa: QA_STAFF };
8619
8630
  var FRONTMATTER_RE2 = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
8620
- function readProfileAudience(raw) {
8631
+ function parseSlugList(value) {
8632
+ const inner = value.startsWith("[") && value.endsWith("]") ? value.slice(1, -1) : value;
8633
+ return inner.split(",").map((s) => s.trim().replace(/^["']|["']$/g, "").toLowerCase()).filter(Boolean);
8634
+ }
8635
+ function readProfileStaff(raw) {
8621
8636
  const m = FRONTMATTER_RE2.exec(raw);
8622
- if (!m) return { audience: ["chat"], body: raw };
8637
+ if (!m) return { staff: ["kody"], body: raw };
8623
8638
  const body = raw.slice(m[0].length);
8639
+ let staff = null;
8640
+ let legacy = null;
8624
8641
  for (const line of (m[1] ?? "").split(/\r?\n/)) {
8625
8642
  const t = line.trim();
8626
8643
  const c = t.indexOf(":");
8627
8644
  if (c < 0) continue;
8628
- if (t.slice(0, c).trim() === "audience") {
8629
- const v = t.slice(c + 1).trim();
8630
- const inner = v.startsWith("[") && v.endsWith("]") ? v.slice(1, -1) : v;
8631
- const audience = inner.split(",").map((s) => s.trim().replace(/^["']|["']$/g, "").toLowerCase()).filter(Boolean);
8632
- return { audience: audience.length > 0 ? audience : ["chat"], body };
8645
+ const key = t.slice(0, c).trim();
8646
+ const value = t.slice(c + 1).trim();
8647
+ if (key === "staff") {
8648
+ staff = parseSlugList(value);
8649
+ } else if (key === "audience" || key === "for") {
8650
+ const mapped = parseSlugList(value).map((tok) => LEGACY_AUDIENCE_TO_STAFF[tok]).filter(Boolean);
8651
+ if (mapped.length > 0) legacy = mapped;
8633
8652
  }
8634
8653
  }
8635
- return { audience: ["chat"], body };
8654
+ return { staff: staff ?? legacy ?? ["kody"], body };
8636
8655
  }
8637
8656
  function readProfile(cwd) {
8638
8657
  const dir = path33.join(cwd, PROFILE_DIR_REL_PATH);
@@ -8647,8 +8666,8 @@ function readProfile(cwd) {
8647
8666
  for (const file of entries) {
8648
8667
  try {
8649
8668
  const raw = fs34.readFileSync(path33.join(dir, file), "utf-8");
8650
- const { audience, body } = readProfileAudience(raw);
8651
- if (!audience.includes("qa")) continue;
8669
+ const { staff, body } = readProfileStaff(raw);
8670
+ if (!staff.includes(QA_STAFF) && !staff.includes(ALL_STAFF)) continue;
8652
8671
  blocks.push(`## ${file}
8653
8672
 
8654
8673
  ${body.trim()}`);
@@ -8831,6 +8850,122 @@ var markFlowSuccess = async (ctx) => {
8831
8850
  }
8832
8851
  };
8833
8852
 
8853
+ // src/scripts/mergeFlow.ts
8854
+ init_issue();
8855
+ function readPr(prNumber, cwd) {
8856
+ const out = gh(
8857
+ [
8858
+ "pr",
8859
+ "view",
8860
+ String(prNumber),
8861
+ "--json",
8862
+ "state,isDraft,mergeable,mergeStateStatus,title,url"
8863
+ ],
8864
+ { cwd }
8865
+ );
8866
+ const p = JSON.parse(out);
8867
+ return {
8868
+ state: p.state ?? "UNKNOWN",
8869
+ isDraft: p.isDraft ?? false,
8870
+ mergeable: p.mergeable ?? "UNKNOWN",
8871
+ mergeStateStatus: p.mergeStateStatus ?? "UNKNOWN",
8872
+ title: p.title ?? "",
8873
+ url: p.url ?? ""
8874
+ };
8875
+ }
8876
+ function evaluateMergeGate(pr) {
8877
+ if (pr.state !== "OPEN") {
8878
+ return {
8879
+ ok: false,
8880
+ action: "MERGE_SKIPPED",
8881
+ reason: `PR is ${pr.state.toLowerCase()}, not open \u2014 nothing to merge.`
8882
+ };
8883
+ }
8884
+ if (pr.isDraft) {
8885
+ return { ok: false, action: "MERGE_BLOCKED", reason: "PR is still a draft." };
8886
+ }
8887
+ if (pr.mergeable === "CONFLICTING" || pr.mergeStateStatus === "DIRTY") {
8888
+ return {
8889
+ ok: false,
8890
+ action: "MERGE_BLOCKED",
8891
+ reason: "PR has conflicts with its base branch \u2014 resolve them first."
8892
+ };
8893
+ }
8894
+ if (pr.mergeable === "UNKNOWN" || pr.mergeStateStatus === "UNKNOWN") {
8895
+ return {
8896
+ ok: false,
8897
+ action: "MERGE_BLOCKED",
8898
+ reason: "GitHub is still computing mergeability \u2014 will retry next tick."
8899
+ };
8900
+ }
8901
+ if (pr.mergeStateStatus === "BEHIND") {
8902
+ return {
8903
+ ok: false,
8904
+ action: "MERGE_BLOCKED",
8905
+ reason: "PR branch is behind its base \u2014 update it before merging."
8906
+ };
8907
+ }
8908
+ if (pr.mergeStateStatus !== "CLEAN") {
8909
+ return {
8910
+ ok: false,
8911
+ action: "MERGE_BLOCKED",
8912
+ reason: `PR is not green (status: ${pr.mergeStateStatus}) \u2014 required checks or reviews are not satisfied.`
8913
+ };
8914
+ }
8915
+ return { ok: true };
8916
+ }
8917
+ var mergeFlow = async (ctx) => {
8918
+ ctx.skipAgent = true;
8919
+ const prNumber = Number(ctx.args.pr);
8920
+ if (!Number.isInteger(prNumber) || prNumber <= 0) {
8921
+ process.stderr.write(`[merge] invalid --pr value: ${String(ctx.args.pr)}
8922
+ `);
8923
+ ctx.data.mergeAction = "MERGE_BLOCKED";
8924
+ return;
8925
+ }
8926
+ let pr;
8927
+ try {
8928
+ pr = readPr(prNumber, ctx.cwd);
8929
+ } catch (err) {
8930
+ const msg = err instanceof Error ? err.message : String(err);
8931
+ process.stderr.write(`[merge] could not read PR #${prNumber}: ${msg}
8932
+ `);
8933
+ ctx.data.mergeAction = "MERGE_BLOCKED";
8934
+ return;
8935
+ }
8936
+ const verdict = evaluateMergeGate(pr);
8937
+ if (!verdict.ok) {
8938
+ process.stdout.write(`[merge] PR #${prNumber} not merged \u2014 ${verdict.reason}
8939
+ `);
8940
+ ctx.data.mergeAction = verdict.action;
8941
+ if (verdict.action === "MERGE_BLOCKED") {
8942
+ commentOnIssue(
8943
+ prNumber,
8944
+ `\u{1F6A6} _Auto-merge held: ${verdict.reason} Kody will retry once the PR is CLEAN._`,
8945
+ ctx.cwd
8946
+ );
8947
+ }
8948
+ return;
8949
+ }
8950
+ process.stdout.write(`[merge] PR #${prNumber} is CLEAN \u2014 squash-merging
8951
+ `);
8952
+ const merged = mergePrSquash(prNumber, ctx.cwd);
8953
+ if (!merged.ok) {
8954
+ process.stderr.write(`[merge] squash-merge of PR #${prNumber} failed: ${merged.error}
8955
+ `);
8956
+ ctx.data.mergeAction = "MERGE_BLOCKED";
8957
+ commentOnIssue(
8958
+ prNumber,
8959
+ `\u{1F6A6} _Auto-merge attempted but failed: ${merged.error}. Kody will retry next tick._`,
8960
+ ctx.cwd
8961
+ );
8962
+ return;
8963
+ }
8964
+ process.stdout.write(`[merge] PR #${prNumber} merged \u2705
8965
+ `);
8966
+ ctx.data.mergeAction = "MERGE_COMPLETED";
8967
+ };
8968
+
8834
8969
  // src/scripts/mergeReleasePr.ts
8835
8970
  import { execFileSync as execFileSync20 } from "child_process";
8836
8971
  var API_TIMEOUT_MS7 = 6e4;
@@ -11950,7 +12085,7 @@ function appendLine(owner, repo, cwd, record) {
11950
12085
  let existing = "";
11951
12086
  let sha;
11952
12087
  try {
11953
- const out = gh(["api", `/repos/${owner}/${repo}/contents/${filePath}`], { cwd });
12088
+ const out = gh(["api", `/repos/${owner}/${repo}/contents/${filePath}?ref=${STATE_BRANCH}`], { cwd });
11954
12089
  const json = JSON.parse(out);
11955
12090
  if (json.sha) sha = json.sha;
11956
12091
  if (json.content) existing = Buffer.from(json.content, "base64").toString("utf-8");
@@ -11960,9 +12095,12 @@ function appendLine(owner, repo, cwd, record) {
11960
12095
  `;
11961
12096
  const payload = {
11962
12097
  message: `chore(activity): ${record.action}`,
11963
- content: Buffer.from(body, "utf-8").toString("base64")
12098
+ content: Buffer.from(body, "utf-8").toString("base64"),
12099
+ // Keep this high-frequency feed off the default branch.
12100
+ branch: STATE_BRANCH
11964
12101
  };
11965
12102
  if (sha) payload.sha = sha;
12103
+ ensureStateBranch(owner, repo, cwd);
11966
12104
  gh(
11967
12105
  ["api", "--method", "PUT", `/repos/${owner}/${repo}/contents/${filePath}`, "--input", "-"],
11968
12106
  { cwd, input: JSON.stringify(payload) }
@@ -12252,6 +12390,7 @@ var preflightScripts = {
12252
12390
  revertFlow,
12253
12391
  reviewFlow,
12254
12392
  syncFlow,
12393
+ mergeFlow,
12255
12394
  initFlow,
12256
12395
  loadTaskState,
12257
12396
  loadTaskContext,
@@ -14028,6 +14167,7 @@ Usage:
14028
14167
  kody-engine fix --pr <N> [--feedback "..."] [--cwd <path>] [--verbose|--quiet]
14029
14168
  kody-engine fix-ci --pr <N> [--run-id <ID>] [--cwd <path>] [--verbose|--quiet]
14030
14169
  kody-engine resolve --pr <N> [--cwd <path>] [--verbose|--quiet]
14170
+ kody-engine merge --pr <N> [--cwd <path>] [--verbose|--quiet]
14031
14171
  kody-engine review --pr <N> [--cwd <path>] [--verbose|--quiet]
14032
14172
  kody-engine <other> [--cwd <path>] [--verbose|--quiet]
14033
14173
  kody-engine ci --issue <N> [preflight flags \u2014 see: kody-engine ci --help]
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "merge",
3
+ "role": "primitive",
4
+ "kind": "oneshot",
5
+ "describe": "Self-gating squash-merge of an open PR into its base. Deterministic, no agent: merges only when GitHub reports the PR CLEAN (mergeable + required checks/reviews satisfied); otherwise refuses and comments why. Used by the CTO qa-merge duty once a QA-goal PR has graduated.",
6
+ "inputs": [
7
+ {
8
+ "name": "pr",
9
+ "flag": "--pr",
10
+ "type": "int",
11
+ "required": true,
12
+ "describe": "GitHub PR number to merge. No-op (MERGE_SKIPPED) if already closed/merged; refuses (MERGE_BLOCKED) if draft, conflicting, or not yet CLEAN."
13
+ }
14
+ ],
15
+ "claudeCode": {
16
+ "model": "inherit",
17
+ "permissionMode": "default",
18
+ "maxTurns": null,
19
+ "maxThinkingTokens": null,
20
+ "systemPromptAppend": null,
21
+ "tools": [],
22
+ "hooks": [],
23
+ "skills": [],
24
+ "commands": [],
25
+ "subagents": [],
26
+ "plugins": [],
27
+ "mcpServers": []
28
+ },
29
+ "cliTools": [
30
+ {
31
+ "name": "gh",
32
+ "install": {
33
+ "required": true,
34
+ "checkCommand": "command -v gh"
35
+ },
36
+ "verify": "gh auth status",
37
+ "usage": "Reads PR mergeability (`gh pr view --json`) and merges (`gh pr merge --squash --delete-branch`).",
38
+ "allowedUses": ["pr", "api"]
39
+ }
40
+ ],
41
+ "inputArtifacts": [],
42
+ "outputArtifacts": [],
43
+ "scripts": {
44
+ "preflight": [
45
+ {
46
+ "script": "mergeFlow"
47
+ }
48
+ ],
49
+ "postflight": []
50
+ },
51
+ "output": {
52
+ "actionTypes": ["MERGE_COMPLETED", "MERGE_BLOCKED", "MERGE_SKIPPED"]
53
+ }
54
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.132",
3
+ "version": "0.4.134",
4
4
  "description": "kody — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
5
5
  "license": "MIT",
6
6
  "type": "module",