@kody-ade/kody-engine 0.4.234 → 0.4.236

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.234",
18
+ version: "0.4.236",
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
 
@@ -4768,6 +4770,125 @@ var init_dutyReport = __esm({
4768
4770
  }
4769
4771
  });
4770
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
+
4771
4892
  // src/lifecycleLabels.ts
4772
4893
  function groupOf(label) {
4773
4894
  const idx = label.indexOf(":");
@@ -6760,49 +6881,88 @@ function collectReports(raw, agentResult) {
6760
6881
  const out = [];
6761
6882
  if (Array.isArray(raw)) {
6762
6883
  for (const item of raw) {
6763
- if (isDutyReport(item)) out.push(item);
6884
+ const parsed = parseDutyReport(item);
6885
+ if (parsed) out.push(parsed);
6764
6886
  }
6765
6887
  }
6766
6888
  if (agentResult?.finalText) out.push(...parseDutyReportsFromText(agentResult.finalText));
6767
6889
  return out;
6768
6890
  }
6769
- function isDutyReport(raw) {
6770
- 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;
6771
6901
  }
6772
- function describeMessage(report) {
6773
- const keys = [
6774
- ...Object.keys(report.evidence ?? {}).map((key) => `evidence:${key}`),
6775
- ...Object.keys(report.facts ?? {}).map((key) => `fact:${key}`)
6776
- ];
6777
- 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("; ")})` : ""}`;
6778
6917
  }
6779
6918
  var applyDutyReports;
6780
6919
  var init_applyDutyReports = __esm({
6781
6920
  "src/scripts/applyDutyReports.ts"() {
6782
6921
  "use strict";
6783
6922
  init_dutyReport();
6923
+ init_dutyResult();
6784
6924
  init_state2();
6785
6925
  init_stateStore();
6786
6926
  applyDutyReports = async (ctx, _profile, agentResult) => {
6787
6927
  const reports = collectReports(ctx.data.dutyReports, agentResult);
6788
- 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;
6789
6931
  const owner = ctx.config.github?.owner;
6790
6932
  const repo = ctx.config.github?.repo;
6791
6933
  if (!owner || !repo) {
6792
6934
  process.stderr.write("[kody duty-report] missing github owner/repo; cannot apply reports\n");
6793
6935
  return;
6794
6936
  }
6795
- const goalReports = reports.filter((report) => report.target.type === "goal");
6796
- for (const report of goalReports) {
6797
- 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);
6798
6942
  if (!prior) {
6799
- 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
6800
6944
  `);
6801
6945
  continue;
6802
6946
  }
6803
- 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
+ }
6804
6957
  if (serializeGoalState(next) === serializeGoalState(prior)) continue;
6805
- 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
+ );
6806
6966
  }
6807
6967
  };
6808
6968
  }
@@ -9922,26 +10082,6 @@ function performInit(cwd, force) {
9922
10082
  fs31.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
9923
10083
  wrote.push(".github/workflows/kody.yml");
9924
10084
  }
9925
- const builtinJobs = listBuiltinJobs();
9926
- if (builtinJobs.length > 0) {
9927
- const jobsDir = path30.join(cwd, ".kody", "duties");
9928
- fs31.mkdirSync(jobsDir, { recursive: true });
9929
- for (const job of builtinJobs) {
9930
- const targetDir = path30.join(jobsDir, job.slug);
9931
- const relProfile = path30.join(".kody", "duties", job.slug, "profile.json");
9932
- const relBody = path30.join(".kody", "duties", job.slug, "duty.md");
9933
- if (fs31.existsSync(targetDir) && fs31.existsSync(path30.join(targetDir, "profile.json")) && !force) {
9934
- skipped.push(relProfile);
9935
- skipped.push(relBody);
9936
- continue;
9937
- }
9938
- fs31.mkdirSync(targetDir, { recursive: true });
9939
- fs31.writeFileSync(path30.join(targetDir, "profile.json"), fs31.readFileSync(job.profilePath, "utf-8"));
9940
- fs31.writeFileSync(path30.join(targetDir, "duty.md"), fs31.readFileSync(job.bodyPath, "utf-8"));
9941
- wrote.push(relProfile);
9942
- wrote.push(relBody);
9943
- }
9944
- }
9945
10085
  const staffDir = path30.join(cwd, ".kody", "staff");
9946
10086
  const staffPath = path30.join(staffDir, "kody.md");
9947
10087
  if (fs31.existsSync(staffPath) && !force) {
@@ -10007,7 +10147,7 @@ jobs:
10007
10147
  python-version: "3.12"
10008
10148
  - env:
10009
10149
  GH_TOKEN: \${{ secrets.KODY_TOKEN || github.token }}
10010
- 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}
10011
10151
  `;
10012
10152
  }
10013
10153
  var WORKFLOW_TEMPLATE, DEFAULT_STAFF_PERSONA, initFlow;
@@ -14454,6 +14594,26 @@ function isMutatingPostflight(scriptName) {
14454
14594
  function shouldBlockMutatingPostflight(scriptName, exitCode) {
14455
14595
  return isMutatingPostflight(scriptName) && (exitCode ?? 0) !== 0;
14456
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
+ }
14457
14617
  function operatorRequestBlock(why) {
14458
14618
  const text = why.trim();
14459
14619
  if (!text) return null;
@@ -15158,19 +15318,7 @@ async function runShellEntry(entry, ctx, profile) {
15158
15318
  ctx.output.reason = `shell '${shellName}' failed to spawn: ${result.spawnErr.message}`;
15159
15319
  return;
15160
15320
  }
15161
- if (/^KODY_SKIP_AGENT=true\s*$/m.test(stdout)) {
15162
- ctx.skipAgent = true;
15163
- if (ctx.output.exitCode === void 0) ctx.output.exitCode = 0;
15164
- }
15165
- const prUrlMatch = stdout.match(/^KODY_PR_URL=(.+)$/m);
15166
- if (prUrlMatch?.[1]) ctx.output.prUrl = prUrlMatch[1].trim();
15167
- const reasonMatch = stdout.match(/^KODY_REASON=(.+)$/m);
15168
- if (reasonMatch?.[1]) ctx.output.reason = reasonMatch[1].trim();
15169
- const dutyReports = parseDutyReportsFromText(stdout);
15170
- if (dutyReports.length > 0) {
15171
- const prior = Array.isArray(ctx.data.dutyReports) ? ctx.data.dutyReports : [];
15172
- ctx.data.dutyReports = [...prior, ...dutyReports];
15173
- }
15321
+ collectShellSideChannels(ctx, stdout);
15174
15322
  if (timedOut) {
15175
15323
  ctx.skipAgent = true;
15176
15324
  const seconds = Math.round(timeoutMs / 1e3);
@@ -15221,6 +15369,7 @@ var init_executor = __esm({
15221
15369
  init_container();
15222
15370
  init_discipline();
15223
15371
  init_dutyReport();
15372
+ init_dutyResult();
15224
15373
  init_events();
15225
15374
  init_lifecycleLabels();
15226
15375
  init_litellm();
@@ -15291,7 +15440,8 @@ async function runJob(job, base) {
15291
15440
  const resolvedDuty = action ? resolveDutyAction(action, projectDutiesRoot) : null;
15292
15441
  const dutyIdentity = valid.duty ?? resolvedDuty?.duty;
15293
15442
  const dutyContext = loadDutyContext(dutyIdentity, base.cwd);
15294
- 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) {
15295
15445
  throw new InvalidJobError(`job duty not found: ${action ?? valid.duty ?? "<none>"}`);
15296
15446
  }
15297
15447
  const dutySelectedExecutable = resolvedDuty?.executable ?? dutyContext?.config.executable ?? dutyContext?.config.executables?.[0] ?? (dutyContext?.config.tickScript ? "duty-tick-scripted" : void 0);
@@ -16789,7 +16939,13 @@ async function runCi(argv) {
16789
16939
  }
16790
16940
  if (forceRunAction) {
16791
16941
  const config = earlyConfig ?? loadConfig(cwd);
16792
- const route = resolveDutyAction(forceRunAction);
16942
+ const manualGoalManager = forceRunAction === "goal-manager";
16943
+ const route = manualGoalManager ? {
16944
+ action: "goal-manager",
16945
+ duty: "goal-manager",
16946
+ executable: "goal-manager",
16947
+ cliArgs: forceRunCliArgs
16948
+ } : resolveDutyAction(forceRunAction);
16793
16949
  if (!route) {
16794
16950
  process.stderr.write(`[kody] manual one-shot action '${forceRunAction}' has no duty action
16795
16951
  `);
@@ -19904,6 +20060,7 @@ Usage:
19904
20060
  kody-engine release --issue <N> [--cwd <path>] [--verbose|--quiet]
19905
20061
  kody-engine init [--cwd <path>] [--verbose|--quiet]
19906
20062
  kody-engine <action> [--cwd <path>] [--verbose|--quiet]
20063
+ kody-engine exec <executable> [--cwd <path>] [--verbose|--quiet]
19907
20064
  kody-engine ci [preflight flags \u2014 see: kody-engine ci --help]
19908
20065
  kody-engine chat [chat flags \u2014 see: kody-engine chat --help]
19909
20066
  kody-engine stats [--since 7d|--run <id>|--json|--cwd <path>]
@@ -19911,7 +20068,8 @@ Usage:
19911
20068
  kody-engine version
19912
20069
 
19913
20070
  Top-level work commands are duty actions. A duty owns the public action name
19914
- and selects an implementation executable.
20071
+ and selects an implementation executable. Use exec only for internal executable
20072
+ profiles such as scheduled helpers.
19915
20073
 
19916
20074
  Exit codes:
19917
20075
  0 success (PR opened, verify passed \u2014 or resolve produced a merge commit)
@@ -19942,6 +20100,24 @@ function parseArgs(argv) {
19942
20100
  if (cmd === "stats") {
19943
20101
  return { ...result, command: "stats", statsArgv: argv.slice(1) };
19944
20102
  }
20103
+ if (cmd === "exec") {
20104
+ const executableName = argv[1];
20105
+ if (!executableName || executableName.startsWith("-")) {
20106
+ result.errors.push("exec requires an executable name");
20107
+ return result;
20108
+ }
20109
+ if (!resolveExecutable(executableName)) {
20110
+ result.errors.push(`unknown executable: ${executableName}`);
20111
+ return result;
20112
+ }
20113
+ result.command = "__exec__";
20114
+ result.executableName = executableName;
20115
+ result.cliArgs = parseGenericFlags(argv.slice(2));
20116
+ if (typeof result.cliArgs.cwd === "string") result.cwd = result.cliArgs.cwd;
20117
+ if (result.cliArgs.verbose === true) result.verbose = true;
20118
+ if (result.cliArgs.quiet === true) result.quiet = true;
20119
+ return result;
20120
+ }
19945
20121
  const SERVER_VERBS = /* @__PURE__ */ new Set(["serve", "pool-serve", "runner-serve", "brain-serve", "brain-proxy", "mcp-http-server"]);
19946
20122
  if (SERVER_VERBS.has(cmd)) {
19947
20123
  result.command = "server";
@@ -19962,7 +20138,7 @@ function parseArgs(argv) {
19962
20138
  return result;
19963
20139
  }
19964
20140
  const discoveredActions = listDutyActions().map((e) => e.action);
19965
- const available = ["ci", "chat", "stats", "help", "version", ...discoveredActions];
20141
+ const available = ["ci", "chat", "stats", "exec", "help", "version", ...discoveredActions];
19966
20142
  result.errors.push(`unknown command: ${cmd} (available: ${available.join(", ")})`);
19967
20143
  return result;
19968
20144
  }
@@ -20094,7 +20270,44 @@ ${HELP_TEXT}`);
20094
20270
  return 99;
20095
20271
  }
20096
20272
  }
20097
- process.stderr.write("error: command did not resolve to a duty\n");
20273
+ if (args.command === "__exec__") {
20274
+ const executable = args.executableName;
20275
+ const cliArgs = args.cliArgs ?? {};
20276
+ const skipConfig = configlessCommands.has(executable);
20277
+ try {
20278
+ const result = await runJob(
20279
+ {
20280
+ action: executable,
20281
+ duty: executable,
20282
+ executable,
20283
+ cliArgs,
20284
+ target: numericTarget(cliArgs),
20285
+ flavor: "instant"
20286
+ },
20287
+ {
20288
+ cwd,
20289
+ skipConfig,
20290
+ verbose: args.verbose,
20291
+ quiet: args.quiet
20292
+ }
20293
+ );
20294
+ if (result.exitCode !== 0 && result.reason) {
20295
+ process.stderr.write(`error: ${result.reason}
20296
+ `);
20297
+ }
20298
+ return result.exitCode;
20299
+ } catch (err) {
20300
+ const msg = err instanceof Error ? err.message : String(err);
20301
+ process.stderr.write(`[kody] ${executable} crashed: ${msg}
20302
+ `);
20303
+ if (err instanceof Error && err.stack) process.stderr.write(`${err.stack}
20304
+ `);
20305
+ process.stdout.write(`PR_URL=FAILED: ${executable} crashed: ${msg}
20306
+ `);
20307
+ return 99;
20308
+ }
20309
+ }
20310
+ process.stderr.write("error: command did not resolve to a duty or executable\n");
20098
20311
  return 64;
20099
20312
  }
20100
20313
  function numericTarget(cliArgs) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.234",
3
+ "version": "0.4.236",
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",
@@ -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
- }