@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 +317 -88
- package/dist/executables/types.ts +59 -0
- package/package.json +23 -22
- package/dist/jobs/watch-stale-prs/duty.md +0 -95
- package/dist/jobs/watch-stale-prs/profile.json +0 -46
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.
|
|
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
|
-
|
|
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
|
|
6748
|
-
|
|
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
|
|
6751
|
-
const
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
|
|
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
|
-
|
|
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
|
|
6774
|
-
|
|
6775
|
-
|
|
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 ${
|
|
6943
|
+
process.stderr.write(`[kody duty-report] goal ${goalId} missing on kody-state; report skipped
|
|
6778
6944
|
`);
|
|
6779
6945
|
continue;
|
|
6780
6946
|
}
|
|
6781
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
}
|