@kody-ade/kody-engine 0.2.53 → 0.2.55

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.53",
6
+ version: "0.2.55",
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 execFileSync21 } 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 execFileSync20 } from "child_process";
532
547
  import * as fs21 from "fs";
533
548
  import * as path18 from "path";
534
549
 
@@ -738,6 +753,7 @@ import * as fs7 from "fs";
738
753
  import * as path6 from "path";
739
754
  var VALID_INPUT_TYPES = /* @__PURE__ */ new Set(["int", "string", "bool", "enum"]);
740
755
  var VALID_PERMISSION_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "bypassPermissions"]);
756
+ var VALID_ROLES = /* @__PURE__ */ new Set(["primitive", "orchestrator", "watch", "utility"]);
741
757
  var ProfileError = class extends Error {
742
758
  constructor(profilePath, message) {
743
759
  super(`Invalid profile at ${profilePath}:
@@ -765,9 +781,17 @@ function loadProfile(profilePath) {
765
781
  if (kind === "scheduled" && typeof r.schedule !== "string") {
766
782
  throw new ProfileError(profilePath, `kind: "scheduled" requires a "schedule" cron string`);
767
783
  }
784
+ if (typeof r.role !== "string" || !VALID_ROLES.has(r.role)) {
785
+ throw new ProfileError(
786
+ profilePath,
787
+ `"role" is required and must be one of: ${[...VALID_ROLES].join(" | ")}`
788
+ );
789
+ }
790
+ const role = r.role;
768
791
  const profile = {
769
792
  name: requireString(profilePath, r, "name"),
770
793
  describe: typeof r.describe === "string" ? r.describe : "",
794
+ role,
771
795
  kind,
772
796
  schedule: typeof r.schedule === "string" ? r.schedule : void 0,
773
797
  inputs: parseInputs(profilePath, r.inputs),
@@ -1490,6 +1514,41 @@ ${formatMissesForFeedback(misses)}`;
1490
1514
  ctx.data.coverageMisses = finalMisses;
1491
1515
  };
1492
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
+
1493
1552
  // src/scripts/commitAndPush.ts
1494
1553
  import { execFileSync as execFileSync6 } from "child_process";
1495
1554
 
@@ -2357,7 +2416,7 @@ function gh2(args, options) {
2357
2416
  }).trim();
2358
2417
  }
2359
2418
  function getIssue(issueNumber, cwd) {
2360
- 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 });
2361
2420
  const parsed = JSON.parse(output);
2362
2421
  if (typeof parsed?.title !== "string") {
2363
2422
  throw new Error(`Issue #${issueNumber}: unexpected response shape`);
@@ -2370,12 +2429,16 @@ function getIssue(issueNumber, cwd) {
2370
2429
  body: c.body ?? "",
2371
2430
  author: c.author?.login ?? "unknown",
2372
2431
  createdAt: c.createdAt ?? ""
2373
- }))
2432
+ })),
2433
+ labels: Array.isArray(parsed.labels) ? parsed.labels.map((l) => l.name ?? "").filter((n) => n.length > 0) : []
2374
2434
  };
2375
2435
  }
2436
+ function stripKody2Mentions(body) {
2437
+ return body.replace(/(@)(kody2)/gi, "$1\u200B$2");
2438
+ }
2376
2439
  function postIssueComment(issueNumber, body, cwd) {
2377
2440
  try {
2378
- gh2(["issue", "comment", String(issueNumber), "--body-file", "-"], { input: body, cwd });
2441
+ gh2(["issue", "comment", String(issueNumber), "--body-file", "-"], { input: stripKody2Mentions(body), cwd });
2379
2442
  } catch (err) {
2380
2443
  process.stderr.write(
2381
2444
  `[kody2] failed to post comment on #${issueNumber}: ${err instanceof Error ? err.message : String(err)}
@@ -2460,7 +2523,7 @@ function getPrLatestReviewBody(prNumber, cwd) {
2460
2523
  }
2461
2524
  function postPrReviewComment(prNumber, body, cwd) {
2462
2525
  try {
2463
- gh2(["pr", "comment", String(prNumber), "--body-file", "-"], { input: body, cwd });
2526
+ gh2(["pr", "comment", String(prNumber), "--body-file", "-"], { input: stripKody2Mentions(body), cwd });
2464
2527
  } catch (err) {
2465
2528
  process.stderr.write(
2466
2529
  `[kody2] failed to post review comment on PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
@@ -2698,7 +2761,11 @@ function parseGenericFlags(argv) {
2698
2761
  }
2699
2762
 
2700
2763
  // src/lifecycleLabels.ts
2701
- var KODY_LABEL_PREFIX = "kody:";
2764
+ var KODY_NAMESPACE = "kody";
2765
+ function groupOf(label) {
2766
+ const idx = label.indexOf(":");
2767
+ return idx === -1 ? label : label.slice(0, idx + 1);
2768
+ }
2702
2769
  function collectProfileLabels() {
2703
2770
  const byLabel = /* @__PURE__ */ new Map();
2704
2771
  for (const exe of listExecutables()) {
@@ -2719,7 +2786,7 @@ function extractLabelSpec(entry) {
2719
2786
  const w = entry.with;
2720
2787
  if (!w) return null;
2721
2788
  const label = typeof w.label === "string" ? w.label : null;
2722
- if (!label || !label.startsWith(KODY_LABEL_PREFIX)) return null;
2789
+ if (!label || !label.startsWith(KODY_NAMESPACE)) return null;
2723
2790
  return {
2724
2791
  label,
2725
2792
  color: typeof w.color === "string" ? w.color : void 0,
@@ -2766,16 +2833,17 @@ function createLabelInRepo(spec, cwd) {
2766
2833
  }
2767
2834
  function setKodyLabel(issueNumber, spec, cwd) {
2768
2835
  const target = spec.label;
2769
- if (!target.startsWith(KODY_LABEL_PREFIX)) {
2836
+ if (!target.startsWith(KODY_NAMESPACE)) {
2770
2837
  process.stderr.write(
2771
2838
  `[kody2] setKodyLabel: refusing to set non-kody label "${target}"
2772
2839
  `
2773
2840
  );
2774
2841
  return;
2775
2842
  }
2843
+ const targetGroup = groupOf(target);
2776
2844
  const present = getIssueLabels(issueNumber, cwd);
2777
2845
  for (const label of present) {
2778
- if (label !== target && label.startsWith(KODY_LABEL_PREFIX)) {
2846
+ if (label !== target && label.startsWith(KODY_NAMESPACE) && groupOf(label) === targetGroup) {
2779
2847
  removeLabel(issueNumber, label, cwd);
2780
2848
  }
2781
2849
  }
@@ -2832,7 +2900,7 @@ var finishFlow = async (ctx, _profile, _agentResult, args) => {
2832
2900
  if (state) state.flow = void 0;
2833
2901
  if (!issueNumber) return;
2834
2902
  const label = typeof args?.label === "string" ? args.label : void 0;
2835
- if (label && label.startsWith(KODY_LABEL_PREFIX)) {
2903
+ if (label && label.startsWith(KODY_NAMESPACE)) {
2836
2904
  setKodyLabel(
2837
2905
  issueNumber,
2838
2906
  {
@@ -3517,7 +3585,9 @@ var loadIssueContext = async (ctx) => {
3517
3585
  const kept = sorted.slice(0, limit);
3518
3586
  const commentsFormatted = kept.length === 0 ? "(no comments yet)" : kept.map((c) => `- **${c.author}** (${c.createdAt}):
3519
3587
  ${truncate2(c.body, maxBytes).replace(/\n/g, "\n ")}`).join("\n\n");
3520
- 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 };
3521
3591
  ctx.data.commentTargetType = "issue";
3522
3592
  ctx.data.commentTargetNumber = issueNumber;
3523
3593
  };
@@ -3642,6 +3712,86 @@ var persistFlowState = async (ctx) => {
3642
3712
  }
3643
3713
  };
3644
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
+
3645
3795
  // src/scripts/postIssueComment.ts
3646
3796
  var postIssueComment2 = async (ctx) => {
3647
3797
  if (ctx.skipAgent && ctx.output.exitCode !== void 0) return;
@@ -3756,7 +3906,7 @@ function reviewAction(verdict, payload) {
3756
3906
  const type = verdict === "PASS" ? "REVIEW_PASS" : verdict === "CONCERNS" ? "REVIEW_CONCERNS" : verdict === "FAIL" ? "REVIEW_FAIL" : "REVIEW_COMPLETED";
3757
3907
  return { type, payload: { verdict, ...payload }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
3758
3908
  }
3759
- function failedAction(reason) {
3909
+ function failedAction2(reason) {
3760
3910
  return { type: "REVIEW_FAILED", payload: { reason }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
3761
3911
  }
3762
3912
  var postReviewResult = async (ctx, _profile, agentResult) => {
@@ -3764,7 +3914,7 @@ var postReviewResult = async (ctx, _profile, agentResult) => {
3764
3914
  if (!prNumber) {
3765
3915
  ctx.output.exitCode = 99;
3766
3916
  ctx.output.reason = "review postflight: no PR number in context";
3767
- ctx.data.action = failedAction(ctx.output.reason);
3917
+ ctx.data.action = failedAction2(ctx.output.reason);
3768
3918
  return;
3769
3919
  }
3770
3920
  if (!agentResult || agentResult.outcome !== "completed") {
@@ -3775,7 +3925,7 @@ var postReviewResult = async (ctx, _profile, agentResult) => {
3775
3925
  }
3776
3926
  ctx.output.exitCode = 1;
3777
3927
  ctx.output.reason = reason;
3778
- ctx.data.action = failedAction(reason);
3928
+ ctx.data.action = failedAction2(reason);
3779
3929
  return;
3780
3930
  }
3781
3931
  const reviewBody = agentResult.finalText.trim();
@@ -3786,7 +3936,7 @@ var postReviewResult = async (ctx, _profile, agentResult) => {
3786
3936
  }
3787
3937
  ctx.output.exitCode = 1;
3788
3938
  ctx.output.reason = "empty review body";
3789
- ctx.data.action = failedAction("empty review body");
3939
+ ctx.data.action = failedAction2("empty review body");
3790
3940
  return;
3791
3941
  }
3792
3942
  try {
@@ -3795,7 +3945,7 @@ var postReviewResult = async (ctx, _profile, agentResult) => {
3795
3945
  const msg = err instanceof Error ? err.message : String(err);
3796
3946
  ctx.output.exitCode = 4;
3797
3947
  ctx.output.reason = `failed to post review comment: ${msg}`;
3798
- ctx.data.action = failedAction(ctx.output.reason);
3948
+ ctx.data.action = failedAction2(ctx.output.reason);
3799
3949
  return;
3800
3950
  }
3801
3951
  const verdict = detectVerdict(reviewBody);
@@ -3811,7 +3961,7 @@ REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.
3811
3961
  };
3812
3962
 
3813
3963
  // src/scripts/releaseFlow.ts
3814
- import { execFileSync as execFileSync14, spawnSync } from "child_process";
3964
+ import { execFileSync as execFileSync15, spawnSync } from "child_process";
3815
3965
  import * as fs18 from "fs";
3816
3966
  import * as path16 from "path";
3817
3967
  function bumpVersion(current, bump) {
@@ -3841,7 +3991,7 @@ function generateChangelog(cwd, newVersion, lastTag) {
3841
3991
  const range = lastTag ? `${lastTag}..HEAD` : "HEAD";
3842
3992
  let log = "";
3843
3993
  try {
3844
- log = execFileSync14("git", ["log", range, "--pretty=format:%s||%h", "--no-merges"], {
3994
+ log = execFileSync15("git", ["log", range, "--pretty=format:%s||%h", "--no-merges"], {
3845
3995
  cwd,
3846
3996
  encoding: "utf-8",
3847
3997
  stdio: ["ignore", "pipe", "pipe"]
@@ -3898,7 +4048,7 @@ ${entry}${prior.slice(idx + 1)}`);
3898
4048
  }
3899
4049
  }
3900
4050
  function git3(args, cwd, timeout = 6e4) {
3901
- return execFileSync14("git", args, {
4051
+ return execFileSync15("git", args, {
3902
4052
  encoding: "utf-8",
3903
4053
  timeout,
3904
4054
  cwd,
@@ -4125,12 +4275,12 @@ function fail(ctx, profile, reason) {
4125
4275
  ctx.data.agentDone = false;
4126
4276
  ctx.data.agentFailureReason = reason;
4127
4277
  const modeSeg = profile.name.replace(/-/g, "_").toUpperCase();
4128
- const failedAction2 = {
4278
+ const failedAction3 = {
4129
4279
  type: `${modeSeg}_FAILED`,
4130
4280
  payload: { reason },
4131
4281
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
4132
4282
  };
4133
- ctx.data.action = failedAction2;
4283
+ ctx.data.action = failedAction3;
4134
4284
  }
4135
4285
  function countActionItems(block) {
4136
4286
  if (!block.trim()) return 0;
@@ -4169,12 +4319,12 @@ function fail2(ctx, profile, reason) {
4169
4319
  ctx.data.agentDone = false;
4170
4320
  ctx.data.agentFailureReason = reason;
4171
4321
  const modeSeg = profile.name.replace(/-/g, "_").toUpperCase();
4172
- const failedAction2 = {
4322
+ const failedAction3 = {
4173
4323
  type: `${modeSeg}_FAILED`,
4174
4324
  payload: { reason },
4175
4325
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
4176
4326
  };
4177
- ctx.data.action = failedAction2;
4327
+ ctx.data.action = failedAction3;
4178
4328
  }
4179
4329
 
4180
4330
  // src/scripts/resolveArtifacts.ts
@@ -4201,7 +4351,7 @@ var resolveArtifacts = async (ctx, profile) => {
4201
4351
  };
4202
4352
 
4203
4353
  // src/scripts/resolveFlow.ts
4204
- import { execFileSync as execFileSync15 } from "child_process";
4354
+ import { execFileSync as execFileSync16 } from "child_process";
4205
4355
  var CONFLICT_DIFF_MAX_BYTES = 4e4;
4206
4356
  var resolveFlow = async (ctx) => {
4207
4357
  const prNumber = ctx.args.pr;
@@ -4253,7 +4403,7 @@ var resolveFlow = async (ctx) => {
4253
4403
  };
4254
4404
  function getConflictedFiles(cwd) {
4255
4405
  try {
4256
- const out = execFileSync15("git", ["diff", "--name-only", "--diff-filter=U"], {
4406
+ const out = execFileSync16("git", ["diff", "--name-only", "--diff-filter=U"], {
4257
4407
  encoding: "utf-8",
4258
4408
  cwd,
4259
4409
  env: { ...process.env, HUSKY: "0" }
@@ -4268,7 +4418,7 @@ function getConflictMarkersPreview(files, cwd, maxBytes = CONFLICT_DIFF_MAX_BYTE
4268
4418
  let total = 0;
4269
4419
  for (const f of files) {
4270
4420
  try {
4271
- const content = execFileSync15("cat", [f], { encoding: "utf-8", cwd }).toString();
4421
+ const content = execFileSync16("cat", [f], { encoding: "utf-8", cwd }).toString();
4272
4422
  const snippet = `### ${f}
4273
4423
 
4274
4424
  \`\`\`
@@ -4399,9 +4549,9 @@ function synthesizeAction(ctx) {
4399
4549
  // src/scripts/setLifecycleLabel.ts
4400
4550
  var setLifecycleLabel = async (ctx, _profile, args) => {
4401
4551
  const label = args?.label;
4402
- if (typeof label !== "string" || !label.startsWith(KODY_LABEL_PREFIX)) {
4552
+ if (typeof label !== "string" || !label.startsWith(KODY_NAMESPACE)) {
4403
4553
  process.stderr.write(
4404
- `[kody2] setLifecycleLabel: missing or invalid "label" arg (must start with "${KODY_LABEL_PREFIX}"): ${String(label)}
4554
+ `[kody2] setLifecycleLabel: missing or invalid "label" arg (must start with "${KODY_NAMESPACE}"): ${String(label)}
4405
4555
  `
4406
4556
  );
4407
4557
  return;
@@ -4432,8 +4582,8 @@ var skipAgent = async (ctx) => {
4432
4582
  };
4433
4583
 
4434
4584
  // src/scripts/startFlow.ts
4435
- import { execFileSync as execFileSync16 } from "child_process";
4436
- var API_TIMEOUT_MS6 = 3e4;
4585
+ import { execFileSync as execFileSync17 } from "child_process";
4586
+ var API_TIMEOUT_MS7 = 3e4;
4437
4587
  var startFlow = async (ctx, profile, _agentResult, args) => {
4438
4588
  const entry = args?.entry;
4439
4589
  if (!entry) {
@@ -4466,8 +4616,8 @@ function postKody2Comment(target, issueNumber, state, next, cwd) {
4466
4616
  const sub = target === "pr" && state?.core.prUrl ? "pr" : "issue";
4467
4617
  const body = `@kody2 ${next}`;
4468
4618
  try {
4469
- execFileSync16("gh", [sub, "comment", String(targetNumber), "--body", body], {
4470
- timeout: API_TIMEOUT_MS6,
4619
+ execFileSync17("gh", [sub, "comment", String(targetNumber), "--body", body], {
4620
+ timeout: API_TIMEOUT_MS7,
4471
4621
  cwd,
4472
4622
  stdio: ["ignore", "pipe", "pipe"]
4473
4623
  });
@@ -4486,7 +4636,7 @@ function parsePr2(url) {
4486
4636
  }
4487
4637
 
4488
4638
  // src/scripts/syncFlow.ts
4489
- import { execFileSync as execFileSync17 } from "child_process";
4639
+ import { execFileSync as execFileSync18 } from "child_process";
4490
4640
  var syncFlow = async (ctx) => {
4491
4641
  ctx.skipAgent = true;
4492
4642
  const prNumber = ctx.args.pr;
@@ -4545,7 +4695,7 @@ function bail2(ctx, prNumber, reason) {
4545
4695
  }
4546
4696
  function revParseHead(cwd) {
4547
4697
  try {
4548
- return execFileSync17("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
4698
+ return execFileSync18("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
4549
4699
  } catch {
4550
4700
  return "";
4551
4701
  }
@@ -4553,9 +4703,9 @@ function revParseHead(cwd) {
4553
4703
  function pushBranch(branch, cwd) {
4554
4704
  const env = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
4555
4705
  try {
4556
- execFileSync17("git", ["push", "-u", "origin", branch], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
4706
+ execFileSync18("git", ["push", "-u", "origin", branch], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
4557
4707
  } catch {
4558
- execFileSync17("git", ["push", "--force-with-lease", "-u", "origin", branch], {
4708
+ execFileSync18("git", ["push", "--force-with-lease", "-u", "origin", branch], {
4559
4709
  cwd,
4560
4710
  env,
4561
4711
  stdio: ["ignore", "pipe", "pipe"]
@@ -4773,7 +4923,8 @@ var preflightScripts = {
4773
4923
  resolvePreviewUrl,
4774
4924
  composePrompt,
4775
4925
  setLifecycleLabel,
4776
- skipAgent
4926
+ skipAgent,
4927
+ classifyByLabel
4777
4928
  };
4778
4929
  var postflightScripts = {
4779
4930
  parseAgentResult: parseAgentResult2,
@@ -4795,7 +4946,8 @@ var postflightScripts = {
4795
4946
  dispatch,
4796
4947
  finishFlow,
4797
4948
  advanceFlow,
4798
- persistFlowState
4949
+ persistFlowState,
4950
+ postClassification
4799
4951
  };
4800
4952
  var allScriptNames = /* @__PURE__ */ new Set([
4801
4953
  ...Object.keys(preflightScripts),
@@ -4803,7 +4955,7 @@ var allScriptNames = /* @__PURE__ */ new Set([
4803
4955
  ]);
4804
4956
 
4805
4957
  // src/tools.ts
4806
- import { execFileSync as execFileSync18 } from "child_process";
4958
+ import { execFileSync as execFileSync19 } from "child_process";
4807
4959
  function verifyCliTools(tools, cwd) {
4808
4960
  const out = [];
4809
4961
  for (const t of tools) out.push(verifyOne(t, cwd));
@@ -4836,7 +4988,7 @@ function verifyOne(tool, cwd) {
4836
4988
  }
4837
4989
  function runShell2(cmd, cwd, timeoutMs = 3e4) {
4838
4990
  try {
4839
- execFileSync18("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
4991
+ execFileSync19("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
4840
4992
  return true;
4841
4993
  } catch {
4842
4994
  return false;
@@ -5166,7 +5318,7 @@ function detectPackageManager2(cwd) {
5166
5318
  }
5167
5319
  function shellOut(cmd, args, cwd, stream = true) {
5168
5320
  try {
5169
- execFileSync19(cmd, args, {
5321
+ execFileSync20(cmd, args, {
5170
5322
  cwd,
5171
5323
  stdio: stream ? "inherit" : "pipe",
5172
5324
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" }
@@ -5179,7 +5331,7 @@ function shellOut(cmd, args, cwd, stream = true) {
5179
5331
  }
5180
5332
  function isOnPath(bin) {
5181
5333
  try {
5182
- execFileSync19("which", [bin], { stdio: "pipe" });
5334
+ execFileSync20("which", [bin], { stdio: "pipe" });
5183
5335
  return true;
5184
5336
  } catch {
5185
5337
  return false;
@@ -5213,7 +5365,7 @@ function installLitellmIfNeeded(cwd) {
5213
5365
  } catch {
5214
5366
  }
5215
5367
  try {
5216
- execFileSync19("python3", ["-c", "import litellm"], { stdio: "pipe" });
5368
+ execFileSync20("python3", ["-c", "import litellm"], { stdio: "pipe" });
5217
5369
  process.stdout.write("\u2192 kody2: litellm already installed\n");
5218
5370
  return 0;
5219
5371
  } catch {
@@ -5223,16 +5375,16 @@ function installLitellmIfNeeded(cwd) {
5223
5375
  }
5224
5376
  function configureGitIdentity(cwd) {
5225
5377
  try {
5226
- const name = execFileSync19("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
5378
+ const name = execFileSync20("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
5227
5379
  if (name) return;
5228
5380
  } catch {
5229
5381
  }
5230
5382
  try {
5231
- execFileSync19("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
5383
+ execFileSync20("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
5232
5384
  } catch {
5233
5385
  }
5234
5386
  try {
5235
- execFileSync19("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
5387
+ execFileSync20("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
5236
5388
  cwd,
5237
5389
  stdio: "pipe"
5238
5390
  });
@@ -5409,9 +5561,9 @@ function commitChatFiles(cwd, sessionId, verbose) {
5409
5561
  if (paths.length === 0) return;
5410
5562
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
5411
5563
  try {
5412
- execFileSync20("git", ["add", ...paths], opts);
5413
- execFileSync20("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
5414
- execFileSync20("git", ["push", "--quiet", "origin", "HEAD"], opts);
5564
+ execFileSync21("git", ["add", ...paths], opts);
5565
+ execFileSync21("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
5566
+ execFileSync21("git", ["push", "--quiet", "origin", "HEAD"], opts);
5415
5567
  } catch (err) {
5416
5568
  const msg = err instanceof Error ? err.message : String(err);
5417
5569
  process.stderr.write(`[kody2:chat] commit/push skipped: ${msg}
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "name": "bug",
3
+ "role": "orchestrator",
3
4
  "describe": "Sub-orchestrator for bug / enhancement issues — plan → run → review (→ fix on concerns/fail). No agent — the postflight entries ARE the transition table, evaluated top-to-bottom via runWhen.",
4
5
  "inputs": [
5
6
  {
@@ -27,6 +28,14 @@
27
28
  "cliTools": [],
28
29
  "scripts": {
29
30
  "preflight": [
31
+ {
32
+ "script": "setLifecycleLabel",
33
+ "with": {
34
+ "label": "kody-flow:bug",
35
+ "color": "d73a4a",
36
+ "description": "kody2 flow: bug / enhancement"
37
+ }
38
+ },
30
39
  {
31
40
  "script": "setLifecycleLabel",
32
41
  "with": {
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "name": "chore",
3
+ "role": "orchestrator",
3
4
  "describe": "Sub-orchestrator for chore / docs / dep-bump issues — run → review (→ fix on concerns/fail). Skips planning entirely. No agent.",
4
5
  "inputs": [
5
6
  {
@@ -27,6 +28,14 @@
27
28
  "cliTools": [],
28
29
  "scripts": {
29
30
  "preflight": [
31
+ {
32
+ "script": "setLifecycleLabel",
33
+ "with": {
34
+ "label": "kody-flow:chore",
35
+ "color": "c5def5",
36
+ "description": "kody2 flow: chore / docs / dep-bump"
37
+ }
38
+ },
30
39
  {
31
40
  "script": "setLifecycleLabel",
32
41
  "with": {
@@ -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.
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "name": "feature",
3
+ "role": "orchestrator",
3
4
  "describe": "Sub-orchestrator for feature / refactor issues — research → plan → run → review (→ fix on concerns/fail). No agent — the postflight entries ARE the transition table, evaluated top-to-bottom via runWhen.",
4
5
  "inputs": [
5
6
  {
@@ -27,6 +28,14 @@
27
28
  "cliTools": [],
28
29
  "scripts": {
29
30
  "preflight": [
31
+ {
32
+ "script": "setLifecycleLabel",
33
+ "with": {
34
+ "label": "kody-flow:feature",
35
+ "color": "a2eeef",
36
+ "description": "kody2 flow: feature / refactor"
37
+ }
38
+ },
30
39
  {
31
40
  "script": "setLifecycleLabel",
32
41
  "with": {
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "name": "fix",
3
+ "role": "primitive",
3
4
  "describe": "Apply review feedback to an existing PR branch.",
4
5
  "inputs": [
5
6
  {
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "name": "fix-ci",
3
+ "role": "primitive",
3
4
  "describe": "Fix a failing CI workflow on an existing PR.",
4
5
  "inputs": [
5
6
  {
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "name": "init",
3
+ "role": "utility",
3
4
  "describe": "Scaffold a consumer repo with kody.config.json and the @kody2 workflow. No agent.",
4
5
  "inputs": [
5
6
  {
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "name": "plan",
3
+ "role": "primitive",
3
4
  "describe": "Research an issue and produce a concrete implementation plan as a comment. Read-only \u2014 no branches, no commits.",
4
5
  "inputs": [
5
6
  {
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "name": "plan-verify",
3
+ "role": "utility",
3
4
  "describe": "Live-test executable. Loads kody-live-marker skill, /kody-live-probe command, kody-live-trace hook, and the bundled test-plugin. Asks the agent to emit confirmation tokens for each feature so Kody2 can validate plugin wiring end-to-end.",
4
5
  "inputs": [
5
6
  { "name": "issue", "flag": "--issue", "type": "int", "required": true, "describe": "GitHub issue number to verify against." }
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "name": "release",
3
+ "role": "utility",
3
4
  "describe": "Version bump + changelog + release PR (prepare), or tag + publish + GH release (finalize). No agent.",
4
5
  "inputs": [
5
6
  {
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "name": "research",
3
+ "role": "primitive",
3
4
  "describe": "Research an issue: understand the ask, map relevant repo context, and surface clarifying questions + gaps. Read-only — no branches, no commits, no prescribed next steps.",
4
5
  "inputs": [
5
6
  {
@@ -67,6 +68,9 @@
67
68
  },
68
69
  {
69
70
  "script": "saveTaskState"
71
+ },
72
+ {
73
+ "script": "advanceFlow"
70
74
  }
71
75
  ]
72
76
  },
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "name": "resolve",
3
+ "role": "primitive",
3
4
  "describe": "Resolve merge conflicts between a PR branch and the default branch.",
4
5
  "inputs": [
5
6
  {
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "name": "review",
3
+ "role": "primitive",
3
4
  "describe": "Read-only structured review of an open PR. Posts one comment, never commits.",
4
5
  "inputs": [
5
6
  {
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "name": "run",
3
+ "role": "primitive",
3
4
  "describe": "Implement a GitHub issue end-to-end: branch, code, commit, open PR.",
4
5
  "inputs": [
5
6
  {
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "name": "spec",
3
+ "role": "orchestrator",
3
4
  "describe": "Sub-orchestrator for spec / RFC / design-doc issues — research → plan (stop). Terminates at the plan artifact; no run, no PR. No agent.",
4
5
  "inputs": [
5
6
  {
@@ -27,6 +28,14 @@
27
28
  "cliTools": [],
28
29
  "scripts": {
29
30
  "preflight": [
31
+ {
32
+ "script": "setLifecycleLabel",
33
+ "with": {
34
+ "label": "kody-flow:spec",
35
+ "color": "7057ff",
36
+ "description": "kody2 flow: spec / RFC / design-doc"
37
+ }
38
+ },
30
39
  {
31
40
  "script": "setLifecycleLabel",
32
41
  "with": {
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "name": "sync",
3
+ "role": "primitive",
3
4
  "describe": "Merge the default (base) branch into a PR branch and push. No agent.",
4
5
  "inputs": [
5
6
  {
@@ -18,7 +18,19 @@ export interface Profile {
18
18
  name: string
19
19
  describe: string
20
20
  /**
21
- * Execution model. `oneshot` (default): single invocation on demand.
21
+ * Semantic role what this executable IS, not when it runs.
22
+ * - primitive: single-step agent executor (flow → agent → verify → commit → PR).
23
+ * - orchestrator: no-agent, drives primitives via a postflight transition table.
24
+ * - watch: scheduled observer that inspects repo state and may trigger other executables.
25
+ * - utility: no-agent, one-off administrative work (scaffolding, release, etc.).
26
+ *
27
+ * Roles enforce shape at profile-load time and let help/dispatch treat
28
+ * executables differently by category.
29
+ */
30
+ role: "primitive" | "orchestrator" | "watch" | "utility"
31
+ /**
32
+ * Execution model — orthogonal to `role`.
33
+ * `oneshot` (default): single invocation on demand.
22
34
  * `scheduled`: fires periodically via an external cron (typically GHA
23
35
  * `schedule:`). Scheduled profiles must declare a `schedule` cron string.
24
36
  */
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "name": "ui-review",
3
+ "role": "primitive",
3
4
  "describe": "UI/UX review of an open PR: browses the running preview with Playwright, compares behavior to diff intent, posts one structured review comment. Read-only on the repo (no commits); writes a throwaway Playwright spec under .kody2/.",
4
5
  "kind": "oneshot",
5
6
  "inputs": [
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "name": "watch-stale-prs",
3
+ "role": "watch",
3
4
  "describe": "Scheduled: list open PRs untouched for N days and report. No agent invocation.",
4
5
  "kind": "scheduled",
5
6
  "schedule": "0 8 * * MON",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.2.53",
3
+ "version": "0.2.55",
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",