@kody-ade/kody-engine 0.4.233 → 0.4.235

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
@@ -15,7 +15,7 @@ var init_package = __esm({
15
15
  "package.json"() {
16
16
  package_default = {
17
17
  name: "@kody-ade/kody-engine",
18
- version: "0.4.233",
18
+ version: "0.4.235",
19
19
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
20
20
  license: "MIT",
21
21
  type: "module",
@@ -2385,21 +2385,6 @@ function getCompanyStoreExecutablesRoot() {
2385
2385
  function getCompanyStoreDutiesRoot() {
2386
2386
  return getCompanyStoreAssetRoot("duties");
2387
2387
  }
2388
- function getBuiltinJobsRoot() {
2389
- const here = path8.dirname(new URL(import.meta.url).pathname);
2390
- const candidates = [
2391
- path8.join(here, "jobs"),
2392
- // dev: src/
2393
- path8.join(here, "..", "jobs"),
2394
- // built: dist/bin → dist/jobs
2395
- path8.join(here, "..", "src", "jobs")
2396
- // fallback
2397
- ];
2398
- for (const c of candidates) {
2399
- if (fs8.existsSync(c) && fs8.statSync(c).isDirectory()) return c;
2400
- }
2401
- return candidates[0];
2402
- }
2403
2388
  function getBuiltinDutiesRoot() {
2404
2389
  const here = path8.dirname(new URL(import.meta.url).pathname);
2405
2390
  const candidates = [
@@ -2415,23 +2400,6 @@ function getBuiltinDutiesRoot() {
2415
2400
  }
2416
2401
  return candidates[0];
2417
2402
  }
2418
- function listBuiltinJobs(root = getBuiltinJobsRoot()) {
2419
- if (!fs8.existsSync(root) || !fs8.statSync(root).isDirectory()) return [];
2420
- const out = [];
2421
- for (const ent of fs8.readdirSync(root, { withFileTypes: true })) {
2422
- if (ent.name.startsWith("_") || ent.name.startsWith(".")) continue;
2423
- const full = path8.join(root, ent.name);
2424
- if (ent.isDirectory()) {
2425
- const profilePath = path8.join(full, DUTY_PROFILE_FILE);
2426
- const bodyPath = path8.join(full, DUTY_BODY_FILE);
2427
- if (!fs8.existsSync(profilePath) || !fs8.statSync(profilePath).isFile()) continue;
2428
- if (!fs8.existsSync(bodyPath) || !fs8.statSync(bodyPath).isFile()) continue;
2429
- out.push({ slug: ent.name, dir: full, profilePath, bodyPath });
2430
- }
2431
- }
2432
- out.sort((a, b) => a.slug.localeCompare(b.slug));
2433
- return out;
2434
- }
2435
2403
  function getExecutableRoots() {
2436
2404
  const storeRoot = getCompanyStoreExecutablesRoot();
2437
2405
  return [getProjectExecutablesRoot(), ...storeRoot ? [storeRoot] : [], getExecutablesRoot()];
@@ -2480,9 +2448,12 @@ function listDutyActions(projectDutiesRoot = getProjectDutiesRoot()) {
2480
2448
  out.push(action);
2481
2449
  };
2482
2450
  const roots = getDutyRoots(projectDutiesRoot);
2451
+ const executableRoots = getExecutableRoots();
2483
2452
  for (const action of listFolderDutyActions(roots[0], "project-folder")) add(action);
2453
+ for (const action of listExecutableDutyActions(executableRoots[0], "project-executable")) add(action);
2484
2454
  if (roots.length === 3) {
2485
2455
  for (const action of listFolderDutyActions(roots[1], "company-store")) add(action);
2456
+ for (const action of listExecutableDutyActions(executableRoots[1], "company-store-executable")) add(action);
2486
2457
  for (const action of listBuiltinDutyActions(roots[2])) add(action);
2487
2458
  } else {
2488
2459
  for (const action of listBuiltinDutyActions(roots[1])) add(action);
@@ -2527,6 +2498,34 @@ function executableDeclaresInput(executable, inputName) {
2527
2498
  function isSafeName(name) {
2528
2499
  return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
2529
2500
  }
2501
+ function listExecutableDutyActions(root, source) {
2502
+ if (!fs8.existsSync(root) || !fs8.statSync(root).isDirectory()) return [];
2503
+ const out = [];
2504
+ for (const ent of fs8.readdirSync(root, { withFileTypes: true })) {
2505
+ if (!ent.isDirectory() || !isSafeName(ent.name)) continue;
2506
+ const profilePath = path8.join(root, ent.name, DUTY_PROFILE_FILE);
2507
+ if (!fs8.existsSync(profilePath) || !fs8.statSync(profilePath).isFile()) continue;
2508
+ try {
2509
+ const raw = JSON.parse(fs8.readFileSync(profilePath, "utf-8"));
2510
+ const action = typeof raw.action === "string" && raw.action.trim() ? raw.action.trim() : "";
2511
+ if (!action) continue;
2512
+ if (!PUBLIC_EXECUTABLE_ACTION_ROLES.has(String(raw.role))) continue;
2513
+ if (!PUBLIC_EXECUTABLE_CAPABILITY_KINDS.has(String(raw.capabilityKind))) continue;
2514
+ if (!Array.isArray(raw.inputs)) continue;
2515
+ out.push({
2516
+ action,
2517
+ duty: ent.name,
2518
+ executable: ent.name,
2519
+ cliArgs: {},
2520
+ source,
2521
+ describe: typeof raw.describe === "string" ? raw.describe : void 0,
2522
+ profilePath
2523
+ });
2524
+ } catch {
2525
+ }
2526
+ }
2527
+ return out.sort((a, b) => a.action.localeCompare(b.action));
2528
+ }
2530
2529
  function listFolderDutyActions(root, source) {
2531
2530
  if (!fs8.existsSync(root) || !fs8.statSync(root).isDirectory()) return [];
2532
2531
  const out = [];
@@ -2607,11 +2606,14 @@ function parseGenericFlags(argv) {
2607
2606
  if (positional.length > 0) args._ = positional;
2608
2607
  return args;
2609
2608
  }
2609
+ var PUBLIC_EXECUTABLE_ACTION_ROLES, PUBLIC_EXECUTABLE_CAPABILITY_KINDS;
2610
2610
  var init_registry = __esm({
2611
2611
  "src/registry.ts"() {
2612
2612
  "use strict";
2613
2613
  init_companyStore();
2614
2614
  init_dutyFolders();
2615
+ PUBLIC_EXECUTABLE_ACTION_ROLES = /* @__PURE__ */ new Set(["primitive", "orchestrator", "container", "watch", "utility"]);
2616
+ PUBLIC_EXECUTABLE_CAPABILITY_KINDS = /* @__PURE__ */ new Set(["observe", "act", "verify"]);
2615
2617
  }
2616
2618
  });
2617
2619
 
@@ -3121,6 +3123,7 @@ function loadProfile(profilePath) {
3121
3123
  action: typeof r.action === "string" && r.action.trim() ? r.action.trim() : void 0,
3122
3124
  executable: execRef,
3123
3125
  describe: typeof r.describe === "string" ? r.describe : base.describe,
3126
+ capabilityKind: parseCapabilityKind(profilePath, r.capabilityKind) ?? base.capabilityKind,
3124
3127
  staff: typeof r.staff === "string" && r.staff.trim() ? r.staff.trim() : base.staff,
3125
3128
  every: typeof r.every === "string" && r.every.trim() ? r.every.trim() : void 0,
3126
3129
  dutyTools: parseStringArray(r.dutyTools ?? r.tools) ?? base.dutyTools,
@@ -3165,6 +3168,7 @@ function loadProfile(profilePath) {
3165
3168
  action: typeof r.action === "string" && r.action.trim() ? r.action.trim() : void 0,
3166
3169
  executable: void 0,
3167
3170
  describe: typeof r.describe === "string" ? r.describe : "",
3171
+ capabilityKind: parseCapabilityKind(profilePath, r.capabilityKind),
3168
3172
  // Optional persona to run as. Empty/blank string → undefined (no persona).
3169
3173
  staff: typeof r.staff === "string" && r.staff.trim() ? r.staff.trim() : void 0,
3170
3174
  // Optional recurrence cadence (scheduled duty). Blank → undefined (on-demand).
@@ -3255,6 +3259,13 @@ function requireString(p, r, key) {
3255
3259
  }
3256
3260
  return v;
3257
3261
  }
3262
+ function parseCapabilityKind(p, raw) {
3263
+ if (raw === void 0 || raw === null || raw === "") return void 0;
3264
+ if (typeof raw !== "string" || !VALID_CAPABILITY_KINDS.has(raw)) {
3265
+ throw new ProfileError(p, `"capabilityKind" must be one of: observe | act | verify`);
3266
+ }
3267
+ return raw;
3268
+ }
3258
3269
  function parseStringArray(raw) {
3259
3270
  if (!Array.isArray(raw)) return void 0;
3260
3271
  const values = raw.map((t) => String(t).trim()).filter(Boolean);
@@ -3331,6 +3342,17 @@ function parseCliTools(p, raw) {
3331
3342
  if (!Array.isArray(raw)) throw new ProfileError(p, `"cliTools" must be an array or absent`);
3332
3343
  const out = [];
3333
3344
  for (const [i, item] of raw.entries()) {
3345
+ if (typeof item === "string" && item.trim()) {
3346
+ const name = item.trim();
3347
+ out.push({
3348
+ name,
3349
+ install: { required: false, checkCommand: `command -v ${name}` },
3350
+ verify: `command -v ${name}`,
3351
+ usage: "",
3352
+ allowedUses: []
3353
+ });
3354
+ continue;
3355
+ }
3334
3356
  if (!item || typeof item !== "object") {
3335
3357
  throw new ProfileError(p, `cliTools[${i}] must be an object`);
3336
3358
  }
@@ -3490,7 +3512,7 @@ function parseScriptList(p, key, raw) {
3490
3512
  }
3491
3513
  return out;
3492
3514
  }
3493
- var VALID_INPUT_TYPES, VALID_PERMISSION_MODES, VALID_ROLES, VALID_CONTAINER_CHILD_TARGETS, VALID_PHASES, KNOWN_PROFILE_KEYS;
3515
+ var VALID_INPUT_TYPES, VALID_PERMISSION_MODES, VALID_ROLES, VALID_CONTAINER_CHILD_TARGETS, VALID_PHASES, VALID_CAPABILITY_KINDS, KNOWN_PROFILE_KEYS;
3494
3516
  var init_profile = __esm({
3495
3517
  "src/profile.ts"() {
3496
3518
  "use strict";
@@ -3506,6 +3528,7 @@ var init_profile = __esm({
3506
3528
  VALID_ROLES = /* @__PURE__ */ new Set(["primitive", "orchestrator", "container", "watch", "utility"]);
3507
3529
  VALID_CONTAINER_CHILD_TARGETS = /* @__PURE__ */ new Set(["issue", "pr"]);
3508
3530
  VALID_PHASES = /* @__PURE__ */ new Set(["research", "planning", "implementing", "reviewing", "shipped", "failed", "idle"]);
3531
+ VALID_CAPABILITY_KINDS = /* @__PURE__ */ new Set(["observe", "act", "verify"]);
3509
3532
  KNOWN_PROFILE_KEYS = /* @__PURE__ */ new Set([
3510
3533
  "name",
3511
3534
  "action",
@@ -3515,6 +3538,7 @@ var init_profile = __esm({
3515
3538
  "dutyTools",
3516
3539
  "tools",
3517
3540
  "mentions",
3541
+ "capabilityKind",
3518
3542
  "stage",
3519
3543
  "readsFrom",
3520
3544
  "writesTo",
@@ -4746,6 +4770,125 @@ var init_dutyReport = __esm({
4746
4770
  }
4747
4771
  });
4748
4772
 
4773
+ // src/dutyResult.ts
4774
+ function parseDutyResultsFromText(text) {
4775
+ const results = [];
4776
+ for (const match of text.matchAll(RESULT_LINE)) {
4777
+ const raw = match[1]?.trim();
4778
+ if (!raw) continue;
4779
+ try {
4780
+ const parsed = parseDutyResult(JSON.parse(raw));
4781
+ if (parsed) results.push(parsed);
4782
+ } catch {
4783
+ }
4784
+ }
4785
+ return results;
4786
+ }
4787
+ function parseDutyResult(raw) {
4788
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
4789
+ const obj = raw;
4790
+ if (obj.version !== 1) return null;
4791
+ if (!isDutyResultStatus(obj.status)) return null;
4792
+ const summary = typeof obj.summary === "string" ? obj.summary.trim() : "";
4793
+ if (!summary) return null;
4794
+ const facts = parseFacts2(obj.facts);
4795
+ if (!facts) return null;
4796
+ const artifacts = parseArtifacts(obj.artifacts);
4797
+ if (!artifacts) return null;
4798
+ return {
4799
+ version: 1,
4800
+ status: obj.status,
4801
+ summary,
4802
+ facts,
4803
+ artifacts
4804
+ };
4805
+ }
4806
+ function applyDutyResultToObjectiveState(state, result, evidenceOverride) {
4807
+ const priorFacts = parseFacts2(state.extra.facts) ?? {};
4808
+ const nextFacts = { ...priorFacts };
4809
+ for (const [key, value] of Object.entries(result.facts)) {
4810
+ if (CONTROL_FACT_KEYS2.has(key)) continue;
4811
+ nextFacts[key] = value;
4812
+ }
4813
+ const evidence = evidenceOverride || (typeof nextFacts.pendingEvidence === "string" ? nextFacts.pendingEvidence : "");
4814
+ if (evidence) {
4815
+ if (result.status === "pass") nextFacts[evidence] = true;
4816
+ if (result.status === "fail" || result.status === "blocked") nextFacts[evidence] = false;
4817
+ if ((result.status === "pass" || result.status === "fail" || result.status === "blocked") && nextFacts.pendingEvidence === evidence) {
4818
+ delete nextFacts.pendingEvidence;
4819
+ }
4820
+ }
4821
+ const blockers = parseStringArray2(state.extra.blockers) ?? [];
4822
+ if ((result.status === "fail" || result.status === "blocked") && !blockers.includes(result.summary)) {
4823
+ blockers.push(result.summary);
4824
+ }
4825
+ return {
4826
+ ...state,
4827
+ extra: {
4828
+ ...state.extra,
4829
+ facts: nextFacts,
4830
+ blockers,
4831
+ lastDutyResult: {
4832
+ status: result.status,
4833
+ summary: result.summary,
4834
+ facts: result.facts,
4835
+ artifacts: result.artifacts
4836
+ }
4837
+ }
4838
+ };
4839
+ }
4840
+ function isDutyResultStatus(value) {
4841
+ return typeof value === "string" && STATUSES.has(value);
4842
+ }
4843
+ function parseFacts2(raw) {
4844
+ if (raw === void 0) return {};
4845
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
4846
+ const facts = {};
4847
+ for (const [key, value] of Object.entries(raw)) {
4848
+ if (!key.trim()) return null;
4849
+ if (CONTROL_FACT_KEYS2.has(key)) continue;
4850
+ facts[key] = value;
4851
+ }
4852
+ return facts;
4853
+ }
4854
+ function parseStringArray2(raw) {
4855
+ if (!Array.isArray(raw)) return null;
4856
+ const out = [];
4857
+ for (const item of raw) {
4858
+ if (typeof item !== "string") return null;
4859
+ out.push(item);
4860
+ }
4861
+ return out;
4862
+ }
4863
+ function parseArtifacts(raw) {
4864
+ if (raw === void 0) return [];
4865
+ if (!Array.isArray(raw)) return null;
4866
+ const artifacts = [];
4867
+ for (const item of raw) {
4868
+ if (!item || typeof item !== "object" || Array.isArray(item)) return null;
4869
+ const rawArtifact = item;
4870
+ const label = typeof rawArtifact.label === "string" ? rawArtifact.label.trim() : "";
4871
+ const url = typeof rawArtifact.url === "string" ? rawArtifact.url.trim() : "";
4872
+ const artifactPath = typeof rawArtifact.path === "string" ? rawArtifact.path.trim() : "";
4873
+ if (!label || !url && !artifactPath) return null;
4874
+ artifacts.push({
4875
+ label,
4876
+ ...url ? { url } : {},
4877
+ ...artifactPath ? { path: artifactPath } : {}
4878
+ });
4879
+ }
4880
+ return artifacts;
4881
+ }
4882
+ var RESULT_LINE, STATUSES, CONTROL_FACT_KEYS2;
4883
+ var init_dutyResult = __esm({
4884
+ "src/dutyResult.ts"() {
4885
+ "use strict";
4886
+ RESULT_LINE = /^KODY_DUTY_RESULT=(.+)$/gm;
4887
+ STATUSES = /* @__PURE__ */ new Set(["pass", "fail", "blocked", "changed", "noop"]);
4888
+ CONTROL_FACT_KEYS2 = /* @__PURE__ */ new Set(["blockers", "destination", "duties", "route", "stage", "state"]);
4889
+ }
4890
+ });
4891
+
4749
4892
  // src/lifecycleLabels.ts
4750
4893
  function groupOf(label) {
4751
4894
  const idx = label.indexOf(":");
@@ -6738,49 +6881,88 @@ function collectReports(raw, agentResult) {
6738
6881
  const out = [];
6739
6882
  if (Array.isArray(raw)) {
6740
6883
  for (const item of raw) {
6741
- if (isDutyReport(item)) out.push(item);
6884
+ const parsed = parseDutyReport(item);
6885
+ if (parsed) out.push(parsed);
6742
6886
  }
6743
6887
  }
6744
6888
  if (agentResult?.finalText) out.push(...parseDutyReportsFromText(agentResult.finalText));
6745
6889
  return out;
6746
6890
  }
6747
- function isDutyReport(raw) {
6748
- return !!raw && typeof raw === "object" && !Array.isArray(raw) && !!raw.target && (raw.target.type === "goal" || raw.target.type === "task" || raw.target.type === "duty") && typeof raw.target.id === "string";
6891
+ function collectResults(raw, agentResult) {
6892
+ const out = [];
6893
+ if (Array.isArray(raw)) {
6894
+ for (const item of raw) {
6895
+ const parsed = parseDutyResult(item);
6896
+ if (parsed) out.push(parsed);
6897
+ }
6898
+ }
6899
+ if (agentResult?.finalText) out.push(...parseDutyResultsFromText(agentResult.finalText));
6900
+ return out;
6749
6901
  }
6750
- function describeMessage(report) {
6751
- const keys = [
6752
- ...Object.keys(report.evidence ?? {}).map((key) => `evidence:${key}`),
6753
- ...Object.keys(report.facts ?? {}).map((key) => `fact:${key}`)
6754
- ];
6755
- return `chore(goals): apply duty report for ${report.target.id}${keys.length ? ` (${keys.join(", ")})` : ""}`;
6902
+ function groupGoalReports(reports) {
6903
+ const grouped = /* @__PURE__ */ new Map();
6904
+ for (const report of reports) {
6905
+ if (report.target.type !== "goal") continue;
6906
+ const list = grouped.get(report.target.id) ?? [];
6907
+ list.push(report);
6908
+ grouped.set(report.target.id, list);
6909
+ }
6910
+ return grouped;
6911
+ }
6912
+ function describeMessage(goalId, reports, results) {
6913
+ const pieces = [];
6914
+ if (reports && reports.length > 0) pieces.push(`report=${reports.length}`);
6915
+ if (results.length > 0) pieces.push(`result=${results.map((result) => result.status).join(",")}`);
6916
+ return `Apply duty output to ${goalId}${pieces.length > 0 ? ` (${pieces.join("; ")})` : ""}`;
6756
6917
  }
6757
6918
  var applyDutyReports;
6758
6919
  var init_applyDutyReports = __esm({
6759
6920
  "src/scripts/applyDutyReports.ts"() {
6760
6921
  "use strict";
6761
6922
  init_dutyReport();
6923
+ init_dutyResult();
6762
6924
  init_state2();
6763
6925
  init_stateStore();
6764
6926
  applyDutyReports = async (ctx, _profile, agentResult) => {
6765
6927
  const reports = collectReports(ctx.data.dutyReports, agentResult);
6766
- if (reports.length === 0) return;
6928
+ const results = collectResults(ctx.data.dutyResults, agentResult);
6929
+ const resultGoalId = typeof ctx.args.goal === "string" && ctx.args.goal.length > 0 ? ctx.args.goal : null;
6930
+ if (reports.length === 0 && (results.length === 0 || !resultGoalId)) return;
6767
6931
  const owner = ctx.config.github?.owner;
6768
6932
  const repo = ctx.config.github?.repo;
6769
6933
  if (!owner || !repo) {
6770
6934
  process.stderr.write("[kody duty-report] missing github owner/repo; cannot apply reports\n");
6771
6935
  return;
6772
6936
  }
6773
- const goalReports = reports.filter((report) => report.target.type === "goal");
6774
- for (const report of goalReports) {
6775
- const prior = fetchGoalState(owner, repo, report.target.id, ctx.cwd);
6937
+ const reportsByGoal = groupGoalReports(reports);
6938
+ const goalIds = new Set(reportsByGoal.keys());
6939
+ if (results.length > 0 && resultGoalId) goalIds.add(resultGoalId);
6940
+ for (const goalId of goalIds) {
6941
+ const prior = fetchGoalState(owner, repo, goalId, ctx.cwd);
6776
6942
  if (!prior) {
6777
- process.stderr.write(`[kody duty-report] goal ${report.target.id} missing on kody-state; report skipped
6943
+ process.stderr.write(`[kody duty-report] goal ${goalId} missing on kody-state; report skipped
6778
6944
  `);
6779
6945
  continue;
6780
6946
  }
6781
- const next = applyDutyReportToGoalState(prior, report);
6947
+ let next = prior;
6948
+ for (const report of reportsByGoal.get(goalId) ?? []) {
6949
+ next = applyDutyReportToGoalState(next, report);
6950
+ }
6951
+ if (goalId === resultGoalId) {
6952
+ const evidence = typeof ctx.args.evidence === "string" && ctx.args.evidence.length > 0 ? ctx.args.evidence : void 0;
6953
+ for (const result of results) {
6954
+ next = applyDutyResultToObjectiveState(next, result, evidence);
6955
+ }
6956
+ }
6782
6957
  if (serializeGoalState(next) === serializeGoalState(prior)) continue;
6783
- putGoalState(owner, repo, report.target.id, { ...next, updatedAt: nowIso() }, describeMessage(report), ctx.cwd);
6958
+ putGoalState(
6959
+ owner,
6960
+ repo,
6961
+ goalId,
6962
+ { ...next, updatedAt: nowIso() },
6963
+ describeMessage(goalId, reportsByGoal.get(goalId), results),
6964
+ ctx.cwd
6965
+ );
6784
6966
  }
6785
6967
  };
6786
6968
  }
@@ -9900,26 +10082,6 @@ function performInit(cwd, force) {
9900
10082
  fs31.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
9901
10083
  wrote.push(".github/workflows/kody.yml");
9902
10084
  }
9903
- const builtinJobs = listBuiltinJobs();
9904
- if (builtinJobs.length > 0) {
9905
- const jobsDir = path30.join(cwd, ".kody", "duties");
9906
- fs31.mkdirSync(jobsDir, { recursive: true });
9907
- for (const job of builtinJobs) {
9908
- const targetDir = path30.join(jobsDir, job.slug);
9909
- const relProfile = path30.join(".kody", "duties", job.slug, "profile.json");
9910
- const relBody = path30.join(".kody", "duties", job.slug, "duty.md");
9911
- if (fs31.existsSync(targetDir) && fs31.existsSync(path30.join(targetDir, "profile.json")) && !force) {
9912
- skipped.push(relProfile);
9913
- skipped.push(relBody);
9914
- continue;
9915
- }
9916
- fs31.mkdirSync(targetDir, { recursive: true });
9917
- fs31.writeFileSync(path30.join(targetDir, "profile.json"), fs31.readFileSync(job.profilePath, "utf-8"));
9918
- fs31.writeFileSync(path30.join(targetDir, "duty.md"), fs31.readFileSync(job.bodyPath, "utf-8"));
9919
- wrote.push(relProfile);
9920
- wrote.push(relBody);
9921
- }
9922
- }
9923
10085
  const staffDir = path30.join(cwd, ".kody", "staff");
9924
10086
  const staffPath = path30.join(staffDir, "kody.md");
9925
10087
  if (fs31.existsSync(staffPath) && !force) {
@@ -9985,7 +10147,7 @@ jobs:
9985
10147
  python-version: "3.12"
9986
10148
  - env:
9987
10149
  GH_TOKEN: \${{ secrets.KODY_TOKEN || github.token }}
9988
- run: npx -y -p @kody-ade/kody-engine@latest kody-engine ${name}
10150
+ run: npx -y -p @kody-ade/kody-engine@latest kody-engine exec ${name}
9989
10151
  `;
9990
10152
  }
9991
10153
  var WORKFLOW_TEMPLATE, DEFAULT_STAFF_PERSONA, initFlow;
@@ -14432,6 +14594,26 @@ function isMutatingPostflight(scriptName) {
14432
14594
  function shouldBlockMutatingPostflight(scriptName, exitCode) {
14433
14595
  return isMutatingPostflight(scriptName) && (exitCode ?? 0) !== 0;
14434
14596
  }
14597
+ function collectShellSideChannels(ctx, stdout) {
14598
+ if (/^KODY_SKIP_AGENT=true\s*$/m.test(stdout)) {
14599
+ ctx.skipAgent = true;
14600
+ if (ctx.output.exitCode === void 0) ctx.output.exitCode = 0;
14601
+ }
14602
+ const prUrlMatch = stdout.match(/^KODY_PR_URL=(.+)$/m);
14603
+ if (prUrlMatch?.[1]) ctx.output.prUrl = prUrlMatch[1].trim();
14604
+ const reasonMatch = stdout.match(/^KODY_REASON=(.+)$/m);
14605
+ if (reasonMatch?.[1]) ctx.output.reason = reasonMatch[1].trim();
14606
+ const dutyReports = parseDutyReportsFromText(stdout);
14607
+ if (dutyReports.length > 0) {
14608
+ const prior = Array.isArray(ctx.data.dutyReports) ? ctx.data.dutyReports : [];
14609
+ ctx.data.dutyReports = [...prior, ...dutyReports];
14610
+ }
14611
+ const dutyResults = parseDutyResultsFromText(stdout);
14612
+ if (dutyResults.length > 0) {
14613
+ const prior = Array.isArray(ctx.data.dutyResults) ? ctx.data.dutyResults : [];
14614
+ ctx.data.dutyResults = [...prior, ...dutyResults];
14615
+ }
14616
+ }
14435
14617
  function operatorRequestBlock(why) {
14436
14618
  const text = why.trim();
14437
14619
  if (!text) return null;
@@ -15136,19 +15318,7 @@ async function runShellEntry(entry, ctx, profile) {
15136
15318
  ctx.output.reason = `shell '${shellName}' failed to spawn: ${result.spawnErr.message}`;
15137
15319
  return;
15138
15320
  }
15139
- if (/^KODY_SKIP_AGENT=true\s*$/m.test(stdout)) {
15140
- ctx.skipAgent = true;
15141
- if (ctx.output.exitCode === void 0) ctx.output.exitCode = 0;
15142
- }
15143
- const prUrlMatch = stdout.match(/^KODY_PR_URL=(.+)$/m);
15144
- if (prUrlMatch?.[1]) ctx.output.prUrl = prUrlMatch[1].trim();
15145
- const reasonMatch = stdout.match(/^KODY_REASON=(.+)$/m);
15146
- if (reasonMatch?.[1]) ctx.output.reason = reasonMatch[1].trim();
15147
- const dutyReports = parseDutyReportsFromText(stdout);
15148
- if (dutyReports.length > 0) {
15149
- const prior = Array.isArray(ctx.data.dutyReports) ? ctx.data.dutyReports : [];
15150
- ctx.data.dutyReports = [...prior, ...dutyReports];
15151
- }
15321
+ collectShellSideChannels(ctx, stdout);
15152
15322
  if (timedOut) {
15153
15323
  ctx.skipAgent = true;
15154
15324
  const seconds = Math.round(timeoutMs / 1e3);
@@ -15199,6 +15369,7 @@ var init_executor = __esm({
15199
15369
  init_container();
15200
15370
  init_discipline();
15201
15371
  init_dutyReport();
15372
+ init_dutyResult();
15202
15373
  init_events();
15203
15374
  init_lifecycleLabels();
15204
15375
  init_litellm();
@@ -15269,7 +15440,8 @@ async function runJob(job, base) {
15269
15440
  const resolvedDuty = action ? resolveDutyAction(action, projectDutiesRoot) : null;
15270
15441
  const dutyIdentity = valid.duty ?? resolvedDuty?.duty;
15271
15442
  const dutyContext = loadDutyContext(dutyIdentity, base.cwd);
15272
- if (!resolvedDuty && !dutyContext) {
15443
+ const explicitExecutableOnly = valid.executable !== void 0 && (valid.action === void 0 || valid.action === valid.executable) && (valid.duty === void 0 || valid.duty === valid.executable);
15444
+ if (!resolvedDuty && !dutyContext && !explicitExecutableOnly) {
15273
15445
  throw new InvalidJobError(`job duty not found: ${action ?? valid.duty ?? "<none>"}`);
15274
15446
  }
15275
15447
  const dutySelectedExecutable = resolvedDuty?.executable ?? dutyContext?.config.executable ?? dutyContext?.config.executables?.[0] ?? (dutyContext?.config.tickScript ? "duty-tick-scripted" : void 0);
@@ -19882,6 +20054,7 @@ Usage:
19882
20054
  kody-engine release --issue <N> [--cwd <path>] [--verbose|--quiet]
19883
20055
  kody-engine init [--cwd <path>] [--verbose|--quiet]
19884
20056
  kody-engine <action> [--cwd <path>] [--verbose|--quiet]
20057
+ kody-engine exec <executable> [--cwd <path>] [--verbose|--quiet]
19885
20058
  kody-engine ci [preflight flags \u2014 see: kody-engine ci --help]
19886
20059
  kody-engine chat [chat flags \u2014 see: kody-engine chat --help]
19887
20060
  kody-engine stats [--since 7d|--run <id>|--json|--cwd <path>]
@@ -19889,7 +20062,8 @@ Usage:
19889
20062
  kody-engine version
19890
20063
 
19891
20064
  Top-level work commands are duty actions. A duty owns the public action name
19892
- and selects an implementation executable.
20065
+ and selects an implementation executable. Use exec only for internal executable
20066
+ profiles such as scheduled helpers.
19893
20067
 
19894
20068
  Exit codes:
19895
20069
  0 success (PR opened, verify passed \u2014 or resolve produced a merge commit)
@@ -19920,6 +20094,24 @@ function parseArgs(argv) {
19920
20094
  if (cmd === "stats") {
19921
20095
  return { ...result, command: "stats", statsArgv: argv.slice(1) };
19922
20096
  }
20097
+ if (cmd === "exec") {
20098
+ const executableName = argv[1];
20099
+ if (!executableName || executableName.startsWith("-")) {
20100
+ result.errors.push("exec requires an executable name");
20101
+ return result;
20102
+ }
20103
+ if (!resolveExecutable(executableName)) {
20104
+ result.errors.push(`unknown executable: ${executableName}`);
20105
+ return result;
20106
+ }
20107
+ result.command = "__exec__";
20108
+ result.executableName = executableName;
20109
+ result.cliArgs = parseGenericFlags(argv.slice(2));
20110
+ if (typeof result.cliArgs.cwd === "string") result.cwd = result.cliArgs.cwd;
20111
+ if (result.cliArgs.verbose === true) result.verbose = true;
20112
+ if (result.cliArgs.quiet === true) result.quiet = true;
20113
+ return result;
20114
+ }
19923
20115
  const SERVER_VERBS = /* @__PURE__ */ new Set(["serve", "pool-serve", "runner-serve", "brain-serve", "brain-proxy", "mcp-http-server"]);
19924
20116
  if (SERVER_VERBS.has(cmd)) {
19925
20117
  result.command = "server";
@@ -19940,7 +20132,7 @@ function parseArgs(argv) {
19940
20132
  return result;
19941
20133
  }
19942
20134
  const discoveredActions = listDutyActions().map((e) => e.action);
19943
- const available = ["ci", "chat", "stats", "help", "version", ...discoveredActions];
20135
+ const available = ["ci", "chat", "stats", "exec", "help", "version", ...discoveredActions];
19944
20136
  result.errors.push(`unknown command: ${cmd} (available: ${available.join(", ")})`);
19945
20137
  return result;
19946
20138
  }
@@ -20072,7 +20264,44 @@ ${HELP_TEXT}`);
20072
20264
  return 99;
20073
20265
  }
20074
20266
  }
20075
- process.stderr.write("error: command did not resolve to a duty\n");
20267
+ if (args.command === "__exec__") {
20268
+ const executable = args.executableName;
20269
+ const cliArgs = args.cliArgs ?? {};
20270
+ const skipConfig = configlessCommands.has(executable);
20271
+ try {
20272
+ const result = await runJob(
20273
+ {
20274
+ action: executable,
20275
+ duty: executable,
20276
+ executable,
20277
+ cliArgs,
20278
+ target: numericTarget(cliArgs),
20279
+ flavor: "instant"
20280
+ },
20281
+ {
20282
+ cwd,
20283
+ skipConfig,
20284
+ verbose: args.verbose,
20285
+ quiet: args.quiet
20286
+ }
20287
+ );
20288
+ if (result.exitCode !== 0 && result.reason) {
20289
+ process.stderr.write(`error: ${result.reason}
20290
+ `);
20291
+ }
20292
+ return result.exitCode;
20293
+ } catch (err) {
20294
+ const msg = err instanceof Error ? err.message : String(err);
20295
+ process.stderr.write(`[kody] ${executable} crashed: ${msg}
20296
+ `);
20297
+ if (err instanceof Error && err.stack) process.stderr.write(`${err.stack}
20298
+ `);
20299
+ process.stdout.write(`PR_URL=FAILED: ${executable} crashed: ${msg}
20300
+ `);
20301
+ return 99;
20302
+ }
20303
+ }
20304
+ process.stderr.write("error: command did not resolve to a duty or executable\n");
20076
20305
  return 64;
20077
20306
  }
20078
20307
  function numericTarget(cliArgs) {
@@ -15,6 +15,8 @@ import type { Phase } from "../state.js"
15
15
  // Profile shape (mirrors the JSON on disk).
16
16
  // ────────────────────────────────────────────────────────────────────────────
17
17
 
18
+ export type CapabilityKind = "observe" | "act" | "verify"
19
+
18
20
  export interface Profile {
19
21
  name: string
20
22
  /**
@@ -33,6 +35,11 @@ export interface Profile {
33
35
  */
34
36
  staff?: string
35
37
  describe: string
38
+ /**
39
+ * Author-facing capability promise for a duty/executable. This classifies the
40
+ * shape of result it should return; it does not change executor control flow.
41
+ */
42
+ capabilityKind?: CapabilityKind
36
43
  /**
37
44
  * Semantic role — what this executable IS, not when it runs.
38
45
  * - primitive: single-step agent executor (flow → agent → verify → commit → PR).
@@ -390,6 +397,58 @@ export interface OutputContract {
390
397
  }
391
398
  }
392
399
 
400
+ export interface CapabilityAlert {
401
+ level?: "info" | "warning" | "error"
402
+ message: string
403
+ }
404
+
405
+ export interface CapabilitySuggestedAction {
406
+ action: string
407
+ args?: Record<string, unknown>
408
+ reason?: string
409
+ }
410
+
411
+ export interface CapabilityResourceRef {
412
+ type: string
413
+ id?: string | number
414
+ number?: number
415
+ url?: string
416
+ name?: string
417
+ }
418
+
419
+ export interface CapabilityEvidenceItem {
420
+ source?: string
421
+ message: string
422
+ url?: string
423
+ }
424
+
425
+ export interface ObserveResult {
426
+ kind: "observe"
427
+ facts?: Record<string, unknown>
428
+ alerts?: CapabilityAlert[]
429
+ suggestedActions?: CapabilitySuggestedAction[]
430
+ evidence?: Record<string, unknown>
431
+ }
432
+
433
+ export interface ActResult {
434
+ kind: "act"
435
+ status: "created" | "changed" | "triggered" | "skipped" | "failed"
436
+ changedResources?: CapabilityResourceRef[]
437
+ createdResources?: CapabilityResourceRef[]
438
+ actionResult?: Record<string, unknown>
439
+ evidence?: Record<string, unknown>
440
+ }
441
+
442
+ export interface VerifyResult {
443
+ kind: "verify"
444
+ passed: boolean
445
+ evidence?: CapabilityEvidenceItem[]
446
+ blockers?: string[]
447
+ facts?: Record<string, unknown>
448
+ }
449
+
450
+ export type CapabilityResult = ObserveResult | ActResult | VerifyResult
451
+
393
452
  // ────────────────────────────────────────────────────────────────────────────
394
453
  // Run-time context passed to every script.
395
454
  // ────────────────────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.233",
3
+ "version": "0.4.235",
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",
@@ -12,6 +12,26 @@
12
12
  "templates",
13
13
  "kody.config.schema.json"
14
14
  ],
15
+ "scripts": {
16
+ "kody:run": "tsx bin/kody.ts",
17
+ "serve": "tsx bin/kody.ts serve",
18
+ "serve:vscode": "tsx bin/kody.ts serve vscode",
19
+ "serve:claude": "tsx bin/kody.ts serve claude",
20
+ "build": "tsup && node scripts/copy-assets.cjs",
21
+ "check:modularity": "tsx scripts/check-script-modularity.ts",
22
+ "pretest": "pnpm check:modularity",
23
+ "test": "vitest run tests/unit tests/int --coverage",
24
+ "posttest": "tsx scripts/check-coverage-floor.ts",
25
+ "test:smoke": "vitest run tests/smoke --no-coverage",
26
+ "test:e2e": "vitest run tests/e2e --no-coverage",
27
+ "test:all": "vitest run tests --no-coverage",
28
+ "typecheck": "tsc --noEmit",
29
+ "lint": "biome check",
30
+ "lint:fix": "biome check --write",
31
+ "format": "biome format --write",
32
+ "brain:publish": "docker buildx build --platform linux/amd64 -f runner/Dockerfile.brain -t ghcr.io/${KODY_BRAIN_GHCR_OWNER:-aharonyaircohen}/kody-brain:latest --push runner",
33
+ "prepublishOnly": "pnpm typecheck && vitest run tests/unit tests/int --no-coverage && pnpm build"
34
+ },
15
35
  "dependencies": {
16
36
  "@actions/cache": "^6.0.0",
17
37
  "@anthropic-ai/claude-agent-sdk": "0.2.119",
@@ -35,24 +55,5 @@
35
55
  "url": "git+https://github.com/aharonyaircohen/kody-engine.git"
36
56
  },
37
57
  "homepage": "https://github.com/aharonyaircohen/kody-engine",
38
- "bugs": "https://github.com/aharonyaircohen/kody-engine/issues",
39
- "scripts": {
40
- "kody:run": "tsx bin/kody.ts",
41
- "serve": "tsx bin/kody.ts serve",
42
- "serve:vscode": "tsx bin/kody.ts serve vscode",
43
- "serve:claude": "tsx bin/kody.ts serve claude",
44
- "build": "tsup && node scripts/copy-assets.cjs",
45
- "check:modularity": "tsx scripts/check-script-modularity.ts",
46
- "pretest": "pnpm check:modularity",
47
- "test": "vitest run tests/unit tests/int --coverage",
48
- "posttest": "tsx scripts/check-coverage-floor.ts",
49
- "test:smoke": "vitest run tests/smoke --no-coverage",
50
- "test:e2e": "vitest run tests/e2e --no-coverage",
51
- "test:all": "vitest run tests --no-coverage",
52
- "typecheck": "tsc --noEmit",
53
- "lint": "biome check",
54
- "lint:fix": "biome check --write",
55
- "format": "biome format --write",
56
- "brain:publish": "docker buildx build --platform linux/amd64 -f runner/Dockerfile.brain -t ghcr.io/${KODY_BRAIN_GHCR_OWNER:-aharonyaircohen}/kody-brain:latest --push runner"
57
- }
58
- }
58
+ "bugs": "https://github.com/aharonyaircohen/kody-engine/issues"
59
+ }
@@ -1,95 +0,0 @@
1
- {{dutyReference}}
2
-
3
- You are **{{staffTitle}}** (staff `{{staffSlug}}`), running the **watch-stale-prs** duty — a weekly digest of open PRs that haven't been touched in a while. You do **not** touch code, do **not** commit, and do **not** edit files. You coordinate by inspecting GitHub state via `gh` and writing a single report file at `.kody/reports/{{dutySlug}}.md`.
4
-
5
- ## Who you are — staff persona (authoritative identity)
6
-
7
- The duty assigns you, staff **`{{staffSlug}}`**, as its executor. This persona defines *who* runs the duty: your authority, doctrine, voice, and hard limits. Where the persona's restrictions are stricter than the duty body, **the persona wins** — a duty can never grant you authority your staff persona withholds.
8
-
9
- {{workerPersona}}
10
-
11
- ## The duty
12
-
13
- Slug **`{{dutySlug}}`** — *{{dutyTitle}}*, assigned to staff **`{{staffSlug}}`**, running on executable **`{{executableSlug}}`**. Cadence is enforced by the engine via the `every: 7d` profile field — this duty only fires once per 7 days regardless of how often `duty-scheduler` wakes. No prose cadence guard needed.
14
-
15
- **Addressing the operator.** When the duty tells you to @-mention the operator, the exact handle(s) to use are: {{mentions}}. Copy that string **verbatim** — never invent, abbreviate, guess, or retype a GitHub username. If the line above is blank, the duty declared no operator; post without a mention.
16
-
17
- ### What "stale" means
18
-
19
- Find every open PR untouched for **≥ 7 days** and write a report listing them, sorted by staleness (oldest first). When there are no stale PRs, write a short "all clear" report so operators know the check ran.
20
-
21
- A PR is stale if:
22
-
23
- - `state` is `OPEN`, AND
24
- - `updatedAt` is more than 7 days before now.
25
-
26
- Use `gh pr list --state open --limit 100 --json number,title,url,updatedAt,author` to enumerate. Filter and sort client-side; do not call `gh` once per PR.
27
-
28
- ### Report shape
29
-
30
- Write to `.kody/reports/watch-stale-prs.md`. Overwrite each run.
31
-
32
- When stale PRs exist:
33
-
34
- ```markdown
35
- # Stale PRs — <ISO date>
36
-
37
- 🟡 <N> PR(s) untouched for > 7 days.
38
-
39
- | # | Title | Author | Days stale | Updated |
40
- |---|-------|--------|------------|---------|
41
- | [#123](url) | <title> | @user | 14 | 2026-04-25 |
42
- | ... | | | | |
43
- ```
44
-
45
- When none:
46
-
47
- ```markdown
48
- # Stale PRs — <ISO date>
49
-
50
- 🟢 No open PRs untouched for more than 7 days.
51
- ```
52
-
53
- Truncate to the 50 oldest if the list is longer; append a final line
54
- `> … and N more not shown`.
55
-
56
- ## Allowed Commands
57
-
58
- - `gh pr list --state open --limit 100 --json number,title,url,updatedAt,author`
59
- - `gh api -X GET /repos/{owner}/{repo}/contents/.kody/reports/watch-stale-prs.md`
60
- — only to fetch the existing file's `sha` for an update.
61
- - `gh api -X PUT /repos/{owner}/{repo}/contents/.kody/reports/watch-stale-prs.md`
62
- — to write the report (base64-encoded `content`, `message`, and `sha`
63
- when updating). This is the **only** permitted write path for this job.
64
-
65
- ## Restrictions
66
-
67
- - Never edit, create, or delete any other file in the working tree.
68
- - Never `git commit`, `git push`, or open a PR.
69
- - Never post comments on PRs or issues; the report file is the only
70
- output channel.
71
- - Never call `gh` per-PR — one `pr list` is enough.
72
-
73
- ## State
74
-
75
- `cursor`: always `"idle"` — this job has no phases; each fire is a
76
- one-shot report write.
77
-
78
- `data`:
79
-
80
- - `lastStaleCount` (number) — how many stale PRs were in the most recent
81
- report. Diagnostic only; the engine ignores it.
82
-
83
- (Engine-managed fields like `lastFiredAt` live under `data` automatically;
84
- do not write or rely on them from the prompt.)
85
-
86
- `done`: always `false` — this job is evergreen.
87
-
88
- ## What to do on this tick
89
-
90
- 1. **Check `done`.** If the prior state has `done: true`, emit the same state back unchanged and exit without any action.
91
- 2. **Enumerate stale PRs** with `gh pr list`.
92
- 3. **Write the report** to `.kody/reports/{{dutySlug}}.md` via `gh api -X PUT`.
93
- 4. **Submit the new state** by calling the `submit_state` tool with `cursor: "idle"`, the `lastStaleCount` in `data`, and `done: false`.
94
-
95
- The duty cadence (`every: 7d`) is enforced by the engine — do not re-arm it from the prompt.
@@ -1,46 +0,0 @@
1
- {
2
- "name": "watch-stale-prs",
3
- "role": "primitive",
4
- "describe": "Weekly digest of open PRs that haven't been touched in a while. Writes a markdown report at .kody/reports/watch-stale-prs.md (surfaced by the dashboard's /reports page).",
5
- "kind": "oneshot",
6
- "staff": "kody",
7
- "every": "7d",
8
- "inputs": [],
9
- "claudeCode": {
10
- "model": "inherit",
11
- "permissionMode": "default",
12
- "maxTurns": 100,
13
- "maxThinkingTokens": null,
14
- "systemPromptAppend": null,
15
- "enableSubmitTool": true,
16
- "tools": ["Bash", "Read", "mcp__kody-submit"],
17
- "hooks": [],
18
- "skills": [],
19
- "commands": [],
20
- "subagents": [],
21
- "plugins": [],
22
- "mcpServers": []
23
- },
24
- "cliTools": [
25
- {
26
- "name": "gh",
27
- "install": {
28
- "required": true,
29
- "checkCommand": "command -v gh"
30
- },
31
- "verify": "gh auth status",
32
- "usage": "Use `gh` for all GitHub actions: `gh pr list --state open --limit 100 --json number,title,url,updatedAt,author` to enumerate candidate PRs, `gh api -X GET /repos/{owner}/{repo}/contents/.kody/reports/watch-stale-prs.md` to fetch the existing file's `sha` for an update, `gh api -X PUT /repos/{owner}/{repo}/contents/.kody/reports/watch-stale-prs.md` to write the report (base64-encoded `content`, `message`, and `sha` when updating). NEVER edit files in the working tree.",
33
- "allowedUses": ["pr", "api"]
34
- }
35
- ],
36
- "scripts": {
37
- "preflight": [
38
- { "script": "loadDutyState" },
39
- { "script": "composePrompt" }
40
- ],
41
- "postflight": [
42
- { "script": "parseJobStateFromAgentResult", "with": { "fenceLabel": "kody-job-next-state" } },
43
- { "script": "writeJobStateFile" }
44
- ]
45
- }
46
- }