@kody-ade/kody-engine 0.4.210 → 0.4.212-live.0
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/README.md +4 -4
- package/dist/bin/kody.js +214 -34
- package/dist/executables/{job-scheduler → duty-scheduler}/profile.json +5 -5
- package/dist/executables/{job-tick → duty-tick}/profile.json +5 -5
- package/dist/executables/duty-tick/prompt.md +77 -0
- package/dist/executables/{job-tick → duty-tick}/prompts/locked.md +4 -2
- package/dist/executables/{job-tick-scripted → duty-tick-scripted}/profile.json +6 -6
- package/dist/executables/fix-ci/profile.json +71 -0
- package/dist/executables/fix-ci/prompt.md +78 -0
- package/dist/executables/types.ts +8 -6
- package/dist/jobs/watch-stale-prs.md +1 -1
- package/package.json +1 -1
- package/templates/workers/goal-manager.md +1 -1
- package/dist/executables/job-tick/prompt.md +0 -75
package/README.md
CHANGED
|
@@ -121,9 +121,9 @@ kody-engine release-publish --issue <N> [--dry-run]
|
|
|
121
121
|
kody-engine release-deploy --issue <N> [--dry-run]
|
|
122
122
|
|
|
123
123
|
# scheduled duties and goals
|
|
124
|
-
kody-engine
|
|
125
|
-
kody-engine
|
|
126
|
-
kody-engine
|
|
124
|
+
kody-engine duty-scheduler # fan out due .kody/duties/*.md files
|
|
125
|
+
kody-engine duty-tick --duty <slug> [--force] # one agent tick for one duty
|
|
126
|
+
kody-engine duty-tick-scripted --duty <slug> [--force] # one deterministic tickScript duty tick
|
|
127
127
|
kody-engine goal-scheduler # fan out active .kody/goals/* state files
|
|
128
128
|
kody-engine goal-tick --goal <id> # advance one stacked-PR goal
|
|
129
129
|
|
|
@@ -141,7 +141,7 @@ kody-engine stats # inspect run/even
|
|
|
141
141
|
|
|
142
142
|
### Duties
|
|
143
143
|
|
|
144
|
-
A **duty** is a markdown file at `.kody/duties/<slug>.md` with frontmatter such as `every:` and `staff:` plus human-owned prose. `
|
|
144
|
+
A **duty** is a markdown file at `.kody/duties/<slug>.md` with frontmatter such as `every:` and `staff:` plus human-owned prose. `duty-scheduler` wakes on cron, finds due duties, and dispatches either `duty-tick` for an agent tick or `duty-tick-scripted` for a deterministic `tickScript:` duty. `kody init` copies built-in starter duties and scaffolds `.kody/staff/kody.md`.
|
|
145
145
|
|
|
146
146
|
Locked-toolbox duties can declare `tools: [...]` to run with only the named high-level MCP intents plus `submit_state`; duties without `tools:` keep the legacy Bash/gh toolbox.
|
|
147
147
|
|
package/dist/bin/kody.js
CHANGED
|
@@ -858,7 +858,7 @@ function dutyToolDefinitions(opts) {
|
|
|
858
858
|
};
|
|
859
859
|
const checkRunsTool = {
|
|
860
860
|
name: "read_check_runs",
|
|
861
|
-
description: "Read CI for a branch or commit ref (e.g. 'dev'). Returns {sha, state, failing:[{name,conclusion,detailsUrl}], pending:[{name,status}]}. state is RED (\u22651 check has a terminal-failure conclusion: failure/timed_out/startup_failure/action_required), PENDING (none failed but some still running), or GREEN (all completed, none failed). Kody's own job check-runs (run/kody/
|
|
861
|
+
description: "Read CI for a branch or commit ref (e.g. 'dev'). Returns {sha, state, failing:[{name,conclusion,detailsUrl}], pending:[{name,status}]}. state is RED (\u22651 check has a terminal-failure conclusion: failure/timed_out/startup_failure/action_required), PENDING (none failed but some still running), or GREEN (all completed, none failed). Kody's own job check-runs (run/kody/duty-tick/\u2026) are excluded by default. This reads the commit's authoritative check-runs \u2014 use it instead of guessing CI health from a run list.",
|
|
862
862
|
inputSchema: {
|
|
863
863
|
ref: z2.string().min(1).describe("Branch name or commit SHA to read CI for (e.g. 'dev')."),
|
|
864
864
|
ignoreNames: z2.array(z2.string()).optional().describe("Check names to exclude (default: Kody's own job names).")
|
|
@@ -980,7 +980,7 @@ var init_dutyMcp = __esm({
|
|
|
980
980
|
TRUST_STATE_BRANCH = "kody-state";
|
|
981
981
|
THREAD_BODY_MAX = 4e3;
|
|
982
982
|
CHECK_FAIL_CONCLUSIONS = /* @__PURE__ */ new Set(["FAILURE", "TIMED_OUT", "STARTUP_FAILURE", "ACTION_REQUIRED"]);
|
|
983
|
-
DEFAULT_IGNORE_CHECKS = ["run", "kody", "
|
|
983
|
+
DEFAULT_IGNORE_CHECKS = ["run", "kody", "duty-tick", "goal-tick", "worker-ask", "chat"];
|
|
984
984
|
trackMarker = (key) => `<!-- kody-track:${key} -->`;
|
|
985
985
|
commentMarker = (key) => `<!-- kody-track-comment:${key} -->`;
|
|
986
986
|
GATE_EXEMPT_EXECUTABLES = /* @__PURE__ */ new Set(["qa-engineer", "ui-review"]);
|
|
@@ -1542,7 +1542,7 @@ var init_loadCoverageRules = __esm({
|
|
|
1542
1542
|
// package.json
|
|
1543
1543
|
var package_default = {
|
|
1544
1544
|
name: "@kody-ade/kody-engine",
|
|
1545
|
-
version: "0.4.
|
|
1545
|
+
version: "0.4.212-live.0",
|
|
1546
1546
|
description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
|
|
1547
1547
|
license: "MIT",
|
|
1548
1548
|
type: "module",
|
|
@@ -4406,6 +4406,7 @@ import { execFileSync as execFileSync2 } from "child_process";
|
|
|
4406
4406
|
var STATE_BEGIN = "<!-- kody:state:v1:begin -->";
|
|
4407
4407
|
var STATE_END = "<!-- kody:state:v1:end -->";
|
|
4408
4408
|
var HISTORY_MAX_ENTRIES = 20;
|
|
4409
|
+
var JOB_RUNS_MAX_ENTRIES = 20;
|
|
4409
4410
|
var API_TIMEOUT_MS2 = 3e4;
|
|
4410
4411
|
function emptyState() {
|
|
4411
4412
|
return {
|
|
@@ -4419,6 +4420,7 @@ function emptyState() {
|
|
|
4419
4420
|
},
|
|
4420
4421
|
executables: {},
|
|
4421
4422
|
artifacts: {},
|
|
4423
|
+
jobs: {},
|
|
4422
4424
|
history: []
|
|
4423
4425
|
};
|
|
4424
4426
|
}
|
|
@@ -4487,6 +4489,7 @@ function parseStateComment(body) {
|
|
|
4487
4489
|
core: { ...emptyState().core, ...parsed.core },
|
|
4488
4490
|
executables: parsed.executables ?? {},
|
|
4489
4491
|
artifacts: parsed.artifacts && typeof parsed.artifacts === "object" ? parsed.artifacts : {},
|
|
4492
|
+
jobs: normalizeJobs(parsed.jobs),
|
|
4490
4493
|
history: Array.isArray(parsed.history) ? parsed.history : [],
|
|
4491
4494
|
flow: parsed.flow
|
|
4492
4495
|
};
|
|
@@ -4512,6 +4515,7 @@ function reduce(state, executable, action, phase, staff, job) {
|
|
|
4512
4515
|
...job?.runUrl ? { runUrl: job.runUrl } : {}
|
|
4513
4516
|
};
|
|
4514
4517
|
const newHistory = [...state.history, entry].slice(-HISTORY_MAX_ENTRIES);
|
|
4518
|
+
const newJobs = reduceJobs(state.jobs ?? {}, executable, action, staff, job);
|
|
4515
4519
|
return {
|
|
4516
4520
|
schemaVersion: 1,
|
|
4517
4521
|
core: {
|
|
@@ -4525,10 +4529,47 @@ function reduce(state, executable, action, phase, staff, job) {
|
|
|
4525
4529
|
},
|
|
4526
4530
|
executables: newExecutables,
|
|
4527
4531
|
artifacts: { ...state.artifacts ?? {} },
|
|
4532
|
+
jobs: newJobs,
|
|
4528
4533
|
history: newHistory,
|
|
4529
4534
|
flow: state.flow
|
|
4530
4535
|
};
|
|
4531
4536
|
}
|
|
4537
|
+
function reduceJobs(jobs, executable, action, staff, job) {
|
|
4538
|
+
const status = statusFromAction(action);
|
|
4539
|
+
const id = job?.jobKey || job?.jobId || `legacy:${executable}`;
|
|
4540
|
+
const prior = jobs[id];
|
|
4541
|
+
const note = noteFromAction(action);
|
|
4542
|
+
const prUrl = job?.prUrl ?? prUrlFromAction(action);
|
|
4543
|
+
const run = {
|
|
4544
|
+
id: job?.jobId || `${id}:${action.timestamp}`,
|
|
4545
|
+
timestamp: action.timestamp,
|
|
4546
|
+
action: action.type,
|
|
4547
|
+
status,
|
|
4548
|
+
...note ? { note } : {},
|
|
4549
|
+
...job?.runUrl ? { runUrl: job.runUrl } : {},
|
|
4550
|
+
...prUrl ? { prUrl } : {}
|
|
4551
|
+
};
|
|
4552
|
+
const runs = [...prior?.runs ?? [], run].slice(-JOB_RUNS_MAX_ENTRIES);
|
|
4553
|
+
const ranAsStaff = typeof staff === "string" && staff.length > 0 ? staff : job?.persona;
|
|
4554
|
+
const next = {
|
|
4555
|
+
id,
|
|
4556
|
+
executable: job?.executable ?? prior?.executable ?? executable,
|
|
4557
|
+
...job?.duty ?? prior?.duty ? { duty: job?.duty ?? prior?.duty } : {},
|
|
4558
|
+
...ranAsStaff ?? prior?.staff ? { staff: ranAsStaff ?? prior?.staff } : {},
|
|
4559
|
+
...job?.flavor ?? prior?.flavor ? { flavor: job?.flavor ?? prior?.flavor } : {},
|
|
4560
|
+
...job?.schedule ?? prior?.schedule ? { schedule: job?.schedule ?? prior?.schedule } : {},
|
|
4561
|
+
...typeof job?.target === "number" ? { target: job.target } : prior?.target !== void 0 ? { target: prior.target } : {},
|
|
4562
|
+
...job?.why ?? prior?.reason ? { reason: job?.why ?? prior?.reason } : {},
|
|
4563
|
+
status,
|
|
4564
|
+
createdAt: prior?.createdAt ?? action.timestamp,
|
|
4565
|
+
updatedAt: action.timestamp,
|
|
4566
|
+
...status === "succeeded" ? { completedAt: action.timestamp } : {},
|
|
4567
|
+
...job?.runUrl ?? prior?.runUrl ? { runUrl: job?.runUrl ?? prior?.runUrl } : {},
|
|
4568
|
+
...prUrl ?? prior?.prUrl ? { prUrl: prUrl ?? prior?.prUrl } : {},
|
|
4569
|
+
runs
|
|
4570
|
+
};
|
|
4571
|
+
return { ...jobs, [id]: next };
|
|
4572
|
+
}
|
|
4532
4573
|
function statusFromAction(action) {
|
|
4533
4574
|
if (/FAILED$|ERROR$|MISSING$|REJECTED$/i.test(action.type)) return "failed";
|
|
4534
4575
|
if (/COMPLETED$|SHIPPED$|MERGED$|SUCCESS$/i.test(action.type)) return "succeeded";
|
|
@@ -4545,6 +4586,46 @@ function noteFromAction(action) {
|
|
|
4545
4586
|
if (typeof p?.commitMessage === "string") return p.commitMessage.slice(0, 120);
|
|
4546
4587
|
return void 0;
|
|
4547
4588
|
}
|
|
4589
|
+
function prUrlFromAction(action) {
|
|
4590
|
+
const p = action.payload;
|
|
4591
|
+
return typeof p?.prUrl === "string" ? p.prUrl : void 0;
|
|
4592
|
+
}
|
|
4593
|
+
function normalizeJobs(input) {
|
|
4594
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return {};
|
|
4595
|
+
const out = {};
|
|
4596
|
+
for (const [key, value] of Object.entries(input)) {
|
|
4597
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
4598
|
+
const raw = value;
|
|
4599
|
+
if (typeof raw.id !== "string" || typeof raw.executable !== "string") continue;
|
|
4600
|
+
if (!isStatus(raw.status)) continue;
|
|
4601
|
+
out[key] = {
|
|
4602
|
+
id: raw.id,
|
|
4603
|
+
executable: raw.executable,
|
|
4604
|
+
...typeof raw.duty === "string" ? { duty: raw.duty } : {},
|
|
4605
|
+
...typeof raw.staff === "string" ? { staff: raw.staff } : {},
|
|
4606
|
+
...raw.flavor === "instant" || raw.flavor === "scheduled" ? { flavor: raw.flavor } : {},
|
|
4607
|
+
...typeof raw.schedule === "string" ? { schedule: raw.schedule } : {},
|
|
4608
|
+
...typeof raw.target === "number" ? { target: raw.target } : {},
|
|
4609
|
+
...typeof raw.reason === "string" ? { reason: raw.reason } : {},
|
|
4610
|
+
status: raw.status,
|
|
4611
|
+
createdAt: typeof raw.createdAt === "string" ? raw.createdAt : "",
|
|
4612
|
+
updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : "",
|
|
4613
|
+
...typeof raw.completedAt === "string" ? { completedAt: raw.completedAt } : {},
|
|
4614
|
+
...typeof raw.runUrl === "string" ? { runUrl: raw.runUrl } : {},
|
|
4615
|
+
...typeof raw.prUrl === "string" ? { prUrl: raw.prUrl } : {},
|
|
4616
|
+
runs: Array.isArray(raw.runs) ? raw.runs.filter(isTaskJobRun).slice(-JOB_RUNS_MAX_ENTRIES) : []
|
|
4617
|
+
};
|
|
4618
|
+
}
|
|
4619
|
+
return out;
|
|
4620
|
+
}
|
|
4621
|
+
function isTaskJobRun(input) {
|
|
4622
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return false;
|
|
4623
|
+
const run = input;
|
|
4624
|
+
return typeof run.id === "string" && typeof run.timestamp === "string" && typeof run.action === "string" && isStatus(run.status);
|
|
4625
|
+
}
|
|
4626
|
+
function isStatus(input) {
|
|
4627
|
+
return input === "pending" || input === "running" || input === "succeeded" || input === "failed";
|
|
4628
|
+
}
|
|
4548
4629
|
function renderStateComment(state) {
|
|
4549
4630
|
const lines = [];
|
|
4550
4631
|
lines.push("## \u{1F4CB} kody task state");
|
|
@@ -4566,6 +4647,11 @@ function renderStateComment(state) {
|
|
|
4566
4647
|
if (attempts) lines.push(`- **Attempts:** ${attempts}`);
|
|
4567
4648
|
if (state.core.prUrl) lines.push(`- **PR:** ${state.core.prUrl}`);
|
|
4568
4649
|
if (state.core.runUrl) lines.push(`- **Run:** ${state.core.runUrl}`);
|
|
4650
|
+
const jobEntries = Object.values(state.jobs ?? {});
|
|
4651
|
+
if (jobEntries.length > 0) {
|
|
4652
|
+
const completed = jobEntries.filter((j) => j.status === "succeeded").length;
|
|
4653
|
+
lines.push(`- **Jobs:** ${completed}/${jobEntries.length} complete`);
|
|
4654
|
+
}
|
|
4569
4655
|
const artifactNames = Object.keys(state.artifacts ?? {});
|
|
4570
4656
|
if (artifactNames.length > 0) {
|
|
4571
4657
|
lines.push(`- **Artifacts:** ${artifactNames.map((n) => `\`${n}\``).join(", ")}`);
|
|
@@ -4581,6 +4667,14 @@ function renderStateComment(state) {
|
|
|
4581
4667
|
}
|
|
4582
4668
|
lines.push("");
|
|
4583
4669
|
}
|
|
4670
|
+
if (jobEntries.length > 0) {
|
|
4671
|
+
lines.push("### Jobs");
|
|
4672
|
+
lines.push("");
|
|
4673
|
+
for (const job of jobEntries) {
|
|
4674
|
+
lines.push(`- \`${job.id}\` **${job.executable}** \u2192 \`${job.status}\` (${job.runs.length} runs)`);
|
|
4675
|
+
}
|
|
4676
|
+
lines.push("");
|
|
4677
|
+
}
|
|
4584
4678
|
lines.push("<details>");
|
|
4585
4679
|
lines.push("<summary>Raw state (JSON)</summary>");
|
|
4586
4680
|
lines.push("");
|
|
@@ -4593,6 +4687,7 @@ function renderStateComment(state) {
|
|
|
4593
4687
|
schemaVersion: state.schemaVersion,
|
|
4594
4688
|
core: state.core,
|
|
4595
4689
|
artifacts: state.artifacts ?? {},
|
|
4690
|
+
jobs: state.jobs ?? {},
|
|
4596
4691
|
executables: state.executables,
|
|
4597
4692
|
history: state.history,
|
|
4598
4693
|
...state.flow ? { flow: state.flow } : {}
|
|
@@ -4906,19 +5001,7 @@ function readContainerState(ctx, child, reader) {
|
|
|
4906
5001
|
if (cached2 && typeof cached2 === "object") {
|
|
4907
5002
|
return cached2;
|
|
4908
5003
|
}
|
|
4909
|
-
return
|
|
4910
|
-
schemaVersion: 1,
|
|
4911
|
-
core: {
|
|
4912
|
-
phase: "idle",
|
|
4913
|
-
status: "pending",
|
|
4914
|
-
currentExecutable: null,
|
|
4915
|
-
lastOutcome: null,
|
|
4916
|
-
attempts: {}
|
|
4917
|
-
},
|
|
4918
|
-
executables: {},
|
|
4919
|
-
artifacts: {},
|
|
4920
|
-
history: []
|
|
4921
|
-
};
|
|
5004
|
+
return emptyState();
|
|
4922
5005
|
}
|
|
4923
5006
|
function parsePrNumber2(url) {
|
|
4924
5007
|
const m = url.match(/\/pull\/(\d+)(?:[/?#]|$)/);
|
|
@@ -5560,10 +5643,16 @@ import { execFileSync as execFileSync7 } from "child_process";
|
|
|
5560
5643
|
// src/scripts/saveTaskState.ts
|
|
5561
5644
|
function jobMetaFromData(data) {
|
|
5562
5645
|
return {
|
|
5646
|
+
jobKey: typeof data.jobKey === "string" ? data.jobKey : void 0,
|
|
5563
5647
|
jobId: typeof data.jobId === "string" ? data.jobId : void 0,
|
|
5564
5648
|
flavor: typeof data.jobFlavor === "string" ? data.jobFlavor : void 0,
|
|
5565
5649
|
schedule: typeof data.jobSchedule === "string" ? data.jobSchedule : void 0,
|
|
5566
|
-
runUrl: typeof data.runUrl === "string" ? data.runUrl : void 0
|
|
5650
|
+
runUrl: typeof data.runUrl === "string" ? data.runUrl : void 0,
|
|
5651
|
+
duty: typeof data.jobDuty === "string" ? data.jobDuty : void 0,
|
|
5652
|
+
executable: typeof data.jobExecutable === "string" ? data.jobExecutable : void 0,
|
|
5653
|
+
target: typeof data.jobTarget === "number" ? data.jobTarget : void 0,
|
|
5654
|
+
persona: typeof data.jobPersona === "string" ? data.jobPersona : void 0,
|
|
5655
|
+
why: typeof data.jobWhy === "string" ? data.jobWhy : void 0
|
|
5567
5656
|
};
|
|
5568
5657
|
}
|
|
5569
5658
|
var saveTaskState = async (ctx, profile) => {
|
|
@@ -5573,7 +5662,10 @@ var saveTaskState = async (ctx, profile) => {
|
|
|
5573
5662
|
if (!target || !number || !state) return;
|
|
5574
5663
|
const executable = profile.name;
|
|
5575
5664
|
const action = ctx.data.action ?? synthesizeAction(ctx);
|
|
5576
|
-
const next = reduce(state, executable, action, profile.phase, profile.staff,
|
|
5665
|
+
const next = reduce(state, executable, action, profile.phase, profile.staff, {
|
|
5666
|
+
...jobMetaFromData(ctx.data),
|
|
5667
|
+
...ctx.output.prUrl ? { prUrl: ctx.output.prUrl } : {}
|
|
5668
|
+
});
|
|
5577
5669
|
if (ctx.output.prUrl) next.core.prUrl = ctx.output.prUrl;
|
|
5578
5670
|
if (typeof ctx.data.runUrl === "string") next.core.runUrl = ctx.data.runUrl;
|
|
5579
5671
|
writeTaskState(target, number, next, ctx.cwd);
|
|
@@ -6289,7 +6381,19 @@ var composePrompt = async (ctx, profile) => {
|
|
|
6289
6381
|
repoOwner: ctx.config.github.owner,
|
|
6290
6382
|
repoName: ctx.config.github.repo,
|
|
6291
6383
|
defaultBranch: ctx.config.git.defaultBranch,
|
|
6292
|
-
branch: ctx.data.branch ?? ""
|
|
6384
|
+
branch: ctx.data.branch ?? "",
|
|
6385
|
+
// The `{{dutyReference}}` block is built from ctx.data.* (with legacy
|
|
6386
|
+
// jobSlug/jobTitle/workerSlug/jobSchedule fallbacks) so a duty prompt can
|
|
6387
|
+
// place a labeled summary at the top. The five underlying tokens are
|
|
6388
|
+
// also exposed individually so a template can compose them differently
|
|
6389
|
+
// (e.g. put the executable slug inline in a header).
|
|
6390
|
+
dutyReference: formatDutyReference(ctx.data, profile.name),
|
|
6391
|
+
dutySlug: pickToken(ctx.data, "dutySlug", "jobSlug"),
|
|
6392
|
+
dutyTitle: pickToken(ctx.data, "dutyTitle", "jobTitle"),
|
|
6393
|
+
executableSlug: pickToken(ctx.data, "executableSlug") || profile.name,
|
|
6394
|
+
staffSlug: pickToken(ctx.data, "staffSlug", "workerSlug"),
|
|
6395
|
+
staffTitle: pickToken(ctx.data, "staffTitle", "workerTitle"),
|
|
6396
|
+
dutySchedule: pickToken(ctx.data, "dutySchedule", "jobSchedule")
|
|
6293
6397
|
};
|
|
6294
6398
|
ctx.data.prompt = template.replace(MUSTACHE, (_, key) => {
|
|
6295
6399
|
const value = tokens[key] ?? "";
|
|
@@ -6354,6 +6458,39 @@ function formatToolsUsage(profile) {
|
|
|
6354
6458
|
}
|
|
6355
6459
|
return lines.join("\n");
|
|
6356
6460
|
}
|
|
6461
|
+
function formatDutyReference(data, profileName) {
|
|
6462
|
+
const dutySlug = pickToken(data, "dutySlug", "jobSlug");
|
|
6463
|
+
const dutyTitle = pickToken(data, "dutyTitle", "jobTitle");
|
|
6464
|
+
const executableSlug = pickToken(data, "executableSlug") || profileName;
|
|
6465
|
+
const staffSlug = pickToken(data, "staffSlug", "workerSlug");
|
|
6466
|
+
const staffTitle = pickToken(data, "staffTitle", "workerTitle");
|
|
6467
|
+
const dutySchedule = pickToken(data, "dutySchedule", "jobSchedule");
|
|
6468
|
+
const lines = ["# Duty reference", ""];
|
|
6469
|
+
if (dutySlug) {
|
|
6470
|
+
lines.push(`- Duty: \`${dutySlug}\`${dutyTitle ? ` \u2014 *${dutyTitle}*` : ""}`);
|
|
6471
|
+
}
|
|
6472
|
+
if (executableSlug) {
|
|
6473
|
+
lines.push(`- Executable: \`${executableSlug}\``);
|
|
6474
|
+
}
|
|
6475
|
+
const staffLine = staffSlug ? `\`${staffSlug}\`${staffTitle && staffTitle !== staffSlug ? ` \u2014 *${staffTitle}*` : ""}` : "";
|
|
6476
|
+
if (staffLine) {
|
|
6477
|
+
lines.push(`- Staff: ${staffLine}`);
|
|
6478
|
+
}
|
|
6479
|
+
if (dutySchedule) {
|
|
6480
|
+
lines.push(`- Cadence: \`${dutySchedule}\``);
|
|
6481
|
+
}
|
|
6482
|
+
if (lines.length === 2) {
|
|
6483
|
+
return "";
|
|
6484
|
+
}
|
|
6485
|
+
return lines.join("\n");
|
|
6486
|
+
}
|
|
6487
|
+
function pickToken(data, ...keys) {
|
|
6488
|
+
for (const k of keys) {
|
|
6489
|
+
const v = data[k];
|
|
6490
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
6491
|
+
}
|
|
6492
|
+
return "";
|
|
6493
|
+
}
|
|
6357
6494
|
|
|
6358
6495
|
// src/scripts/createQaGoal.ts
|
|
6359
6496
|
init_issue();
|
|
@@ -7654,16 +7791,32 @@ ${stateBody}`;
|
|
|
7654
7791
|
ctx.output.nextDispatch = { executable: classification, cliArgs };
|
|
7655
7792
|
};
|
|
7656
7793
|
|
|
7657
|
-
// src/scripts/
|
|
7794
|
+
// src/scripts/dispatchDutyFileTicks.ts
|
|
7658
7795
|
import * as fs29 from "fs";
|
|
7659
7796
|
import * as path26 from "path";
|
|
7660
7797
|
|
|
7661
7798
|
// src/job.ts
|
|
7662
7799
|
var DEFAULT_INSTANT_PERSONA = "kody";
|
|
7800
|
+
var localJobSeq = 0;
|
|
7663
7801
|
function newJobId(flavor) {
|
|
7664
7802
|
const runId = process.env.GITHUB_RUN_ID;
|
|
7665
7803
|
if (runId) return `gh-${runId}-${process.env.GITHUB_RUN_ATTEMPT ?? "1"}`;
|
|
7666
|
-
|
|
7804
|
+
localJobSeq += 1;
|
|
7805
|
+
return `${flavor}-${Date.now()}-${localJobSeq}`;
|
|
7806
|
+
}
|
|
7807
|
+
function stableJobKey(job) {
|
|
7808
|
+
const executable = job.executable ?? job.duty ?? "unknown";
|
|
7809
|
+
if (job.flavor === "scheduled" && job.duty) return `scheduled:${job.duty}:${executable}`;
|
|
7810
|
+
const target = typeof job.target === "number" ? job.target : targetFromCliArgs(job.cliArgs);
|
|
7811
|
+
return target === void 0 ? `${job.flavor}:${executable}` : `${job.flavor}:${executable}:${target}`;
|
|
7812
|
+
}
|
|
7813
|
+
function targetFromCliArgs(cliArgs) {
|
|
7814
|
+
if (!cliArgs) return void 0;
|
|
7815
|
+
for (const key of ["issue", "pr", "target", "issue_number"]) {
|
|
7816
|
+
const value = cliArgs[key];
|
|
7817
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
7818
|
+
}
|
|
7819
|
+
return void 0;
|
|
7667
7820
|
}
|
|
7668
7821
|
var InvalidJobError = class extends Error {
|
|
7669
7822
|
constructor(message) {
|
|
@@ -7705,7 +7858,9 @@ async function runJob(job, base) {
|
|
|
7705
7858
|
}
|
|
7706
7859
|
const preloadedData = {};
|
|
7707
7860
|
preloadedData.jobId = newJobId(valid.flavor);
|
|
7861
|
+
preloadedData.jobKey = stableJobKey(valid);
|
|
7708
7862
|
preloadedData.jobFlavor = valid.flavor;
|
|
7863
|
+
if (valid.target !== void 0) preloadedData.jobTarget = valid.target;
|
|
7709
7864
|
if (valid.duty !== void 0 && valid.duty.length > 0) preloadedData.jobDuty = valid.duty;
|
|
7710
7865
|
if (valid.executable !== void 0 && valid.executable.length > 0) preloadedData.jobExecutable = valid.executable;
|
|
7711
7866
|
if (valid.schedule !== void 0 && valid.schedule.length > 0) preloadedData.jobSchedule = valid.schedule;
|
|
@@ -8200,16 +8355,16 @@ function resolveBackend(opts) {
|
|
|
8200
8355
|
}
|
|
8201
8356
|
}
|
|
8202
8357
|
|
|
8203
|
-
// src/scripts/
|
|
8204
|
-
var
|
|
8358
|
+
// src/scripts/dispatchDutyFileTicks.ts
|
|
8359
|
+
var dispatchDutyFileTicks = async (ctx, _profile, args) => {
|
|
8205
8360
|
ctx.skipAgent = true;
|
|
8206
8361
|
const targetExecutable = String(args?.targetExecutable ?? "");
|
|
8207
8362
|
if (!targetExecutable) {
|
|
8208
|
-
throw new Error("
|
|
8363
|
+
throw new Error("dispatchDutyFileTicks: `with.targetExecutable` is required");
|
|
8209
8364
|
}
|
|
8210
8365
|
const jobsDir = String(args?.jobsDir ?? ".kody/duties");
|
|
8211
|
-
const scriptedExecutable = String(args?.scriptedExecutable ?? "
|
|
8212
|
-
const slugArg = String(args?.slugArg ?? "
|
|
8366
|
+
const scriptedExecutable = String(args?.scriptedExecutable ?? "duty-tick-scripted");
|
|
8367
|
+
const slugArg = String(args?.slugArg ?? "duty");
|
|
8213
8368
|
const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, jobsDir });
|
|
8214
8369
|
if (backend.hydrate) {
|
|
8215
8370
|
await backend.hydrate();
|
|
@@ -8422,14 +8577,14 @@ async function stampFired(backend, slug, now) {
|
|
|
8422
8577
|
}
|
|
8423
8578
|
}
|
|
8424
8579
|
|
|
8425
|
-
// src/scripts/
|
|
8580
|
+
// src/scripts/dispatchDutyTicks.ts
|
|
8426
8581
|
init_issue();
|
|
8427
|
-
var
|
|
8582
|
+
var dispatchDutyTicks = async (ctx, _profile, args) => {
|
|
8428
8583
|
ctx.skipAgent = true;
|
|
8429
8584
|
const label = String(args?.label ?? "");
|
|
8430
8585
|
const targetExecutable = String(args?.targetExecutable ?? "");
|
|
8431
|
-
if (!label) throw new Error("
|
|
8432
|
-
if (!targetExecutable) throw new Error("
|
|
8586
|
+
if (!label) throw new Error("dispatchDutyTicks: `with.label` is required");
|
|
8587
|
+
if (!targetExecutable) throw new Error("dispatchDutyTicks: `with.targetExecutable` is required");
|
|
8433
8588
|
const issueArg = String(args?.issueArg ?? "issue");
|
|
8434
8589
|
const issues = listIssuesByLabel(label, ctx.cwd);
|
|
8435
8590
|
ctx.data.jobIssueCount = issues.length;
|
|
@@ -9761,6 +9916,12 @@ var loadDutyState = async (ctx, profile, args) => {
|
|
|
9761
9916
|
ctx.data.jobSlug = slug;
|
|
9762
9917
|
ctx.data.jobState = loaded;
|
|
9763
9918
|
ctx.data.jobStateJson = JSON.stringify(loaded.state, null, 2);
|
|
9919
|
+
ctx.data.dutySlug = slug;
|
|
9920
|
+
ctx.data.dutyTitle = profile.describe;
|
|
9921
|
+
ctx.data.executableSlug = profile.executable ?? profile.name;
|
|
9922
|
+
ctx.data.staffSlug = profile.staff ?? "";
|
|
9923
|
+
ctx.data.staffTitle = "";
|
|
9924
|
+
ctx.data.dutySchedule = profile.every ?? profile.schedule ?? "";
|
|
9764
9925
|
const mentions = (profile.mentions ?? []).map((l) => `@${l}`).join(" ");
|
|
9765
9926
|
ctx.data.mentions = mentions;
|
|
9766
9927
|
const declaredTools = profile.dutyTools ?? [];
|
|
@@ -9931,6 +10092,12 @@ var loadJobFromFile = async (ctx, profile, args) => {
|
|
|
9931
10092
|
ctx.data.workerTitle = workerTitle;
|
|
9932
10093
|
ctx.data.workerPersona = workerPersona;
|
|
9933
10094
|
ctx.data.mentions = mentions;
|
|
10095
|
+
ctx.data.dutySlug = slug;
|
|
10096
|
+
ctx.data.dutyTitle = title;
|
|
10097
|
+
ctx.data.staffSlug = workerSlug;
|
|
10098
|
+
ctx.data.staffTitle = workerTitle;
|
|
10099
|
+
ctx.data.executableSlug = profile.name;
|
|
10100
|
+
ctx.data.dutySchedule = "";
|
|
9934
10101
|
const declaredTools = frontmatter.tools ?? [];
|
|
9935
10102
|
if (declaredTools.length > 0) {
|
|
9936
10103
|
const unknown = declaredTools.filter((name) => !DUTY_TOOL_PALETTE2.has(name));
|
|
@@ -10697,6 +10864,10 @@ var parseIssueStateFromAgentResult = async (ctx, _profile, agentResult, args) =>
|
|
|
10697
10864
|
};
|
|
10698
10865
|
|
|
10699
10866
|
// src/scripts/parseJobStateFromAgentResult.ts
|
|
10867
|
+
var DUTY_NEXT_STATE_FENCE_ALIASES = {
|
|
10868
|
+
"kody-job-next-state": "kody-duty-next-state",
|
|
10869
|
+
"kody-duty-next-state": "kody-job-next-state"
|
|
10870
|
+
};
|
|
10700
10871
|
var parseJobStateFromAgentResult = async (ctx, _profile, agentResult, args) => {
|
|
10701
10872
|
const fenceLabel = String(args?.fenceLabel ?? "");
|
|
10702
10873
|
if (!fenceLabel) {
|
|
@@ -10719,7 +10890,16 @@ var parseJobStateFromAgentResult = async (ctx, _profile, agentResult, args) => {
|
|
|
10719
10890
|
};
|
|
10720
10891
|
return;
|
|
10721
10892
|
}
|
|
10722
|
-
|
|
10893
|
+
let result = extractNextStateFromText(agentResult.finalText, fenceLabel, prevRev);
|
|
10894
|
+
if (result.error?.startsWith("missing `")) {
|
|
10895
|
+
const alias = DUTY_NEXT_STATE_FENCE_ALIASES[fenceLabel];
|
|
10896
|
+
if (alias) {
|
|
10897
|
+
const aliasResult = extractNextStateFromText(agentResult.finalText, alias, prevRev);
|
|
10898
|
+
if (!aliasResult.error?.startsWith("missing `")) {
|
|
10899
|
+
result = aliasResult;
|
|
10900
|
+
}
|
|
10901
|
+
}
|
|
10902
|
+
}
|
|
10723
10903
|
if (result.error) {
|
|
10724
10904
|
const cleanFinishNoBlock = result.error.startsWith("missing `") && agentResult.outcome === "completed" && loaded != null;
|
|
10725
10905
|
if (cleanFinishNoBlock) {
|
|
@@ -12266,7 +12446,7 @@ var runTickScript = async (ctx, _profile, args) => {
|
|
|
12266
12446
|
const tickScript = frontmatter.tickScript;
|
|
12267
12447
|
if (!tickScript) {
|
|
12268
12448
|
ctx.output.exitCode = 99;
|
|
12269
|
-
ctx.output.reason = `runTickScript:
|
|
12449
|
+
ctx.output.reason = `runTickScript: duty ${slug} has no \`tickScript:\` frontmatter \u2014 route via duty-tick instead`;
|
|
12270
12450
|
return;
|
|
12271
12451
|
}
|
|
12272
12452
|
const scriptPath = path34.isAbsolute(tickScript) ? tickScript : path34.join(ctx.cwd, tickScript);
|
|
@@ -13249,8 +13429,8 @@ var preflightScripts = {
|
|
|
13249
13429
|
classifyByLabel,
|
|
13250
13430
|
diagMcp,
|
|
13251
13431
|
warmupMcp,
|
|
13252
|
-
|
|
13253
|
-
|
|
13432
|
+
dispatchDutyTicks,
|
|
13433
|
+
dispatchDutyFileTicks,
|
|
13254
13434
|
runTickScript,
|
|
13255
13435
|
runPreviewBuild,
|
|
13256
13436
|
loadGoalState,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "duty-scheduler",
|
|
3
3
|
"role": "watch",
|
|
4
|
-
"describe": "Scheduled: for every duty file under .kody/duties/, invoke
|
|
4
|
+
"describe": "Scheduled: for every duty file under .kody/duties/, invoke duty-tick once. No agent on the scheduler itself.",
|
|
5
5
|
"kind": "scheduled",
|
|
6
6
|
"schedule": "*/5 * * * *",
|
|
7
7
|
"inputs": [],
|
|
@@ -36,11 +36,11 @@
|
|
|
36
36
|
"scripts": {
|
|
37
37
|
"preflight": [
|
|
38
38
|
{
|
|
39
|
-
"script": "
|
|
39
|
+
"script": "dispatchDutyFileTicks",
|
|
40
40
|
"with": {
|
|
41
41
|
"jobsDir": ".kody/duties",
|
|
42
|
-
"targetExecutable": "
|
|
43
|
-
"slugArg": "
|
|
42
|
+
"targetExecutable": "duty-tick",
|
|
43
|
+
"slugArg": "duty"
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
],
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "duty-tick",
|
|
3
3
|
"role": "primitive",
|
|
4
4
|
"describe": "One classifier tick for one duty file: read intent + state, decide and execute via gh, emit next state.",
|
|
5
5
|
"kind": "oneshot",
|
|
6
6
|
"inputs": [
|
|
7
7
|
{
|
|
8
|
-
"name": "
|
|
9
|
-
"flag": "--
|
|
8
|
+
"name": "duty",
|
|
9
|
+
"flag": "--duty",
|
|
10
10
|
"type": "string",
|
|
11
11
|
"required": true,
|
|
12
12
|
"describe": "Duty slug — basename (without .md) of the file under .kody/duties/."
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"name": "force",
|
|
16
16
|
"flag": "--force",
|
|
17
17
|
"type": "bool",
|
|
18
|
-
"describe": "When true, the agent ignores the
|
|
18
|
+
"describe": "When true, the agent ignores the duty body's cadence guard and executes the work this tick. All other body rules (allowed commands, restrictions, state schema) still apply. Used for manual triggers from the dashboard's 'Run now' button."
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"claudeCode": {
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"script": "loadJobFromFile",
|
|
54
54
|
"with": {
|
|
55
55
|
"jobsDir": ".kody/duties",
|
|
56
|
-
"slugArg": "
|
|
56
|
+
"slugArg": "duty"
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
59
|
{
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{{dutyReference}}
|
|
2
|
+
|
|
3
|
+
You are **{{staffTitle}}** (staff `{{staffSlug}}`), running through **kody duty-tick** — the coordinator for one file-based duty. You do **not** touch code, do **not** commit, and do **not** edit files. You coordinate by inspecting GitHub state and issuing Kody commands as PR comments.
|
|
4
|
+
|
|
5
|
+
## Who you are — staff persona (authoritative identity)
|
|
6
|
+
|
|
7
|
+
The duty below 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}}`**. The duty body below is authoritative for *what* to do, *when* (cadence), allowed commands, and state schema. It is human-edited — re-read it every tick. Execute it **as** the persona above.
|
|
14
|
+
|
|
15
|
+
**Addressing the operator.** When the duty body tells you to @-mention the operator (e.g. the first line of an inbox recommendation), the exact handle(s) to use are: {{mentions}}. Copy that string **verbatim** — never invent, abbreviate, guess, or retype a GitHub username. A wrong handle silently fails to route to the operator's inbox, so the recommendation is lost. If the line above is blank, the duty declared no operator; post without a mention.
|
|
16
|
+
|
|
17
|
+
### Duty body
|
|
18
|
+
|
|
19
|
+
{{jobIntent}}
|
|
20
|
+
|
|
21
|
+
## Current state
|
|
22
|
+
|
|
23
|
+
This is the state you wrote at the end of the previous tick (or `null` if this is the first tick):
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{{jobStateJson}}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`cursor` is *your* enum — pick whatever labels map cleanly to your duty's phases. `data` is where you stash anything you need on the next tick (per-PR attempt counters, last-seen SHAs, etc). `done: true` is how you signal that the duty is permanently over — for evergreen duties this should always remain `false`.
|
|
30
|
+
|
|
31
|
+
## What to do on this tick
|
|
32
|
+
|
|
33
|
+
`forceRun = {{args.force}}` — set to `true` when an operator clicked "Run now" on the dashboard. When `forceRun` is `true`, ignore the duty body's `**Cadence guard.**` paragraph (or any equivalent "skip if last run was within X" rule) and execute the work as if the guard had passed. All other body rules — allowed commands, restrictions, state schema — still apply. Force only overrides cadence.
|
|
34
|
+
|
|
35
|
+
1. **Check `done`.** If the prior state has `done: true`, emit the same state back unchanged and exit without any action.
|
|
36
|
+
2. **Re-read the duty body.** It may have changed since the last tick.
|
|
37
|
+
3. **Execute exactly the work the body's `## Duty` section describes**, subject to its `## Allowed Commands` and `## Restrictions`. Use the `## State` section to interpret and update `data`.
|
|
38
|
+
4. **Optionally post a short narration** wherever the duty tells you to (typically a PR comment alongside the action). Keep it terse.
|
|
39
|
+
5. **Submit the new state** by calling the `submit_state` tool (see contract below). Do not include `version` or `rev` — the postflight script manages those.
|
|
40
|
+
|
|
41
|
+
## Output contract (MANDATORY, exactly once, at the end)
|
|
42
|
+
|
|
43
|
+
Call the **`submit_state`** tool exactly once, as the final step, with your next state:
|
|
44
|
+
|
|
45
|
+
- `cursor` — your next cursor (string, e.g. `"idle"`).
|
|
46
|
+
- `data` — your next `data` object. Carry forward prior `data` and mutate only what you acted on this tick.
|
|
47
|
+
- `done` — `true` only if the duty is permanently finished; evergreen duties stay `false`.
|
|
48
|
+
|
|
49
|
+
This is the ONLY way your decision is saved. If you don't call it, the tick fails and the state is NOT updated — on the next wake you'll see the same prior state and can retry.
|
|
50
|
+
|
|
51
|
+
> Backstop (legacy): if the `submit_state` tool is unavailable, end your reply with the same JSON in a single fenced block tagged `kody-job-next-state` (or the new `kody-duty-next-state` alias) instead:
|
|
52
|
+
>
|
|
53
|
+
> ````
|
|
54
|
+
> ```kody-job-next-state
|
|
55
|
+
> { "cursor": "<next>", "data": { ... }, "done": <true|false> }
|
|
56
|
+
> ```
|
|
57
|
+
> ````
|
|
58
|
+
|
|
59
|
+
## Rules
|
|
60
|
+
|
|
61
|
+
- Never edit, create, or delete files in the working tree.
|
|
62
|
+
- Never commit or push via `git`. The only permitted commit path is `gh api -X PUT` against the report file (see exception below).
|
|
63
|
+
- Only shell calls allowed: `gh`. Everything must go through it.
|
|
64
|
+
- Keep each tick focused: do one action per candidate per wake. The cron will call you again.
|
|
65
|
+
- If state says you're waiting on something, just check and re-emit — don't spawn a duplicate.
|
|
66
|
+
- Honour the duty body's `## Restrictions` over any inferred shortcut.
|
|
67
|
+
|
|
68
|
+
### Single permitted write: the duty's report file
|
|
69
|
+
|
|
70
|
+
A duty MAY (optionally — only if its body asks for it) write a single
|
|
71
|
+
markdown report file at the canonical path:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
.kody/reports/{{dutySlug}}.md
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Only that exact path. Only via `gh api -X PUT /repos/<owner>/<repo>/contents/.kody/reports/{{dutySlug}}.md` (with base64 content + `sha` of the existing file when updating). All other writes — code files, other report paths, other slugs — remain forbidden. The dashboard's `/reports` page surfaces these files automatically; this is the canonical channel for a duty's diagnostic output when an issue comment isn't expressive enough.
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
{{dutyReference}}
|
|
2
|
+
|
|
3
|
+
You are **{{staffTitle}}** (staff `{{staffSlug}}`), running duty **`{{dutySlug}}`** — *{{dutyTitle}}* — in **locked-toolbox mode**.
|
|
2
4
|
|
|
3
5
|
You have NO shell. You cannot run `gh`, edit files, or post raw comments. The only actions you can take this tick are the typed tools listed below, plus `submit_state` at the end. The duty body tells you *when* to use each tool; the tools themselves do the work.
|
|
4
6
|
|
|
@@ -15,7 +17,7 @@ Anything not in that list does not exist for this tick. If the duty body asks fo
|
|
|
15
17
|
|
|
16
18
|
## The duty
|
|
17
19
|
|
|
18
|
-
Slug **`{{
|
|
20
|
+
Slug **`{{dutySlug}}`** — assigned to staff **`{{staffSlug}}`**, running on executable **`{{executableSlug}}`**. The body is authoritative for *what* and *when*; re-read it every tick.
|
|
19
21
|
|
|
20
22
|
**Operator handle.** Where the duty refers to "the operator," the `recommend_to_operator` tool already prepends this string: `{{mentions}}`. Never type it yourself.
|
|
21
23
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "duty-tick-scripted",
|
|
3
3
|
"role": "utility",
|
|
4
|
-
"describe": "Deterministic
|
|
4
|
+
"describe": "Deterministic duty tick: runs the slug's `tickScript:` (declared in duty frontmatter), parses next-state from its stdout, persists. No agent.",
|
|
5
5
|
"kind": "oneshot",
|
|
6
6
|
"inputs": [
|
|
7
7
|
{
|
|
8
|
-
"name": "
|
|
9
|
-
"flag": "--
|
|
8
|
+
"name": "duty",
|
|
9
|
+
"flag": "--duty",
|
|
10
10
|
"type": "string",
|
|
11
11
|
"required": true,
|
|
12
12
|
"describe": "Duty slug — basename (without .md) of the file under .kody/duties/."
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"name": "force",
|
|
16
16
|
"flag": "--force",
|
|
17
17
|
"type": "bool",
|
|
18
|
-
"describe": "Accepted for parity with `
|
|
18
|
+
"describe": "Accepted for parity with `duty-tick`. Scripted ticks have no agent cadence guard to bypass — the dispatcher already gated on frontmatter `every:`. Forwarded to the script via env if it cares."
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"claudeCode": {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"script": "runTickScript",
|
|
53
53
|
"with": {
|
|
54
54
|
"jobsDir": ".kody/duties",
|
|
55
|
-
"slugArg": "
|
|
55
|
+
"slugArg": "duty",
|
|
56
56
|
"fenceLabel": "kody-job-next-state"
|
|
57
57
|
}
|
|
58
58
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fix-ci",
|
|
3
|
+
"role": "primitive",
|
|
4
|
+
"describe": "Fix a failing CI workflow on an existing PR.",
|
|
5
|
+
"inputs": [
|
|
6
|
+
{
|
|
7
|
+
"name": "pr",
|
|
8
|
+
"flag": "--pr",
|
|
9
|
+
"type": "int",
|
|
10
|
+
"required": true,
|
|
11
|
+
"describe": "GitHub PR number whose CI is failing."
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "runId",
|
|
15
|
+
"flag": "--run-id",
|
|
16
|
+
"type": "string",
|
|
17
|
+
"required": false,
|
|
18
|
+
"describe": "Specific failed workflow run ID. Defaults to latest failed run on the PR branch."
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"claudeCode": {
|
|
22
|
+
"model": "inherit",
|
|
23
|
+
"permissionMode": "acceptEdits",
|
|
24
|
+
"maxTurns": null,
|
|
25
|
+
"maxTurnTimeoutSec": 1200,
|
|
26
|
+
"systemPromptAppend": null,
|
|
27
|
+
"cacheable": true,
|
|
28
|
+
"enableVerifyTool": true,
|
|
29
|
+
"verifyAttempts": 4,
|
|
30
|
+
"tools": [
|
|
31
|
+
"Read",
|
|
32
|
+
"Write",
|
|
33
|
+
"Edit",
|
|
34
|
+
"Bash",
|
|
35
|
+
"Grep",
|
|
36
|
+
"Glob",
|
|
37
|
+
"mcp__kody-verify"
|
|
38
|
+
],
|
|
39
|
+
"hooks": ["block-git"],
|
|
40
|
+
"skills": [],
|
|
41
|
+
"commands": [],
|
|
42
|
+
"subagents": [],
|
|
43
|
+
"plugins": [],
|
|
44
|
+
"mcpServers": []
|
|
45
|
+
},
|
|
46
|
+
"cliTools": [],
|
|
47
|
+
"lifecycle": "pr-branch",
|
|
48
|
+
"lifecycleConfig": {
|
|
49
|
+
"label": {
|
|
50
|
+
"name": "kody:fixing-ci",
|
|
51
|
+
"color": "e99695",
|
|
52
|
+
"description": "kody: fixing CI failures"
|
|
53
|
+
},
|
|
54
|
+
"context": "ci-fix",
|
|
55
|
+
"advance": false,
|
|
56
|
+
"finalize": true
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"preflight": [
|
|
60
|
+
{ "script": "fixCiFlow" }
|
|
61
|
+
],
|
|
62
|
+
"postflight": []
|
|
63
|
+
},
|
|
64
|
+
"output": {
|
|
65
|
+
"actionTypes": [
|
|
66
|
+
"FIX_CI_COMPLETED",
|
|
67
|
+
"FIX_CI_FAILED",
|
|
68
|
+
"AGENT_NOT_RUN"
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
You are Kody, an autonomous engineer. A CI workflow on PR #{{pr.number}} (`{{branch}}`) is failing. Read the failed-step log below and fix the root cause. The wrapper handles git/gh — you do not.
|
|
2
|
+
|
|
3
|
+
# Repo
|
|
4
|
+
- {{repoOwner}}/{{repoName}}, default branch: {{defaultBranch}}
|
|
5
|
+
|
|
6
|
+
# PR #{{pr.number}}: {{pr.title}}
|
|
7
|
+
|
|
8
|
+
# Failing workflow
|
|
9
|
+
- Workflow: {{failedWorkflowName}}
|
|
10
|
+
- Run URL: {{failedRunUrl}}
|
|
11
|
+
|
|
12
|
+
# Failed-step log (truncated, most recent ~30KB)
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
{{failedLogTail}}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
{{conventionsBlock}}{{toolsUsage}}# Current PR diff (truncated)
|
|
19
|
+
|
|
20
|
+
```diff
|
|
21
|
+
{{prDiff}}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
# Required steps
|
|
25
|
+
1. **Classify the failure.** Read the log and identify which type of failure this is. Different failure types call for different strategies; misidentifying the type usually leads to masking the symptom rather than fixing the root cause.
|
|
26
|
+
|
|
27
|
+
| Failure type | Signals in the log | Strategy |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| **Compile / type error** | `error TS…`, `cannot find module`, `undefined symbol`, `mismatched types` | Edit the code to satisfy the compiler. Don't add `any`, `// @ts-ignore`, `# type: ignore`, or weaken the type to dodge the check. |
|
|
30
|
+
| **Failing test** | `expect(...).toBe(...)`, assertion diff, "1 failed, N passed" | Read the test AND the code under test. Fix whichever has the bug — usually the code, sometimes the test if the test encodes wrong expectations. Never fix it by widening the assertion (`toBeTruthy` instead of a real check, `expect.any(Object)` instead of a real shape). |
|
|
31
|
+
| **Lint / format** | `eslint`, `prettier`, `ruff`, `gofmt`, `--check` | Run the formatter / fix the lint rule. Don't disable the rule unless it's a documented project decision. |
|
|
32
|
+
| **Missing dependency** | `Module not found`, `cannot find package`, `command not found` | Check whether the dep should be installed (add to package.json/requirements/go.mod) or whether the import path is wrong. Don't `npm install` a transitive dep that should already be inherited. |
|
|
33
|
+
| **Build / packaging** | tsup/webpack/vite/turbo errors, "out of memory", "duplicate exports" | Read the actual error. Often a real bug (circular import, wrong export shape), occasionally a config gap. |
|
|
34
|
+
| **Flaky / non-deterministic** | passes locally and on retry; race conditions; timing-sensitive assertions | See "Flaky-test escape hatch" below. Do NOT add retries, `setTimeout`, or `--retries=N` to make a real flake green. |
|
|
35
|
+
| **Environmental** | missing secret, broken runner, network failure, unreachable registry | Emit `FAILED: <explanation>`. Code can't fix infrastructure. |
|
|
36
|
+
|
|
37
|
+
2. **Make the minimum edits to fix the root cause.** Do not bundle unrelated cleanups into a CI fix.
|
|
38
|
+
|
|
39
|
+
3. **Confirm green via the `verify` tool** — call `mcp__kody-verify__verify` to run the project's typecheck/lint/test gates. If `ok: false`, read the truncated `failures`, fix the root cause, and call `verify` again. Bounded by 4 attempts; after that the tool returns `locked: true` and you must wrap up with FAILED. The postflight verifier still runs after this session as the final ratifier.
|
|
40
|
+
|
|
41
|
+
4. **Final message format** (or `FAILED: <reason>` on failure):
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
DONE
|
|
45
|
+
COMMIT_MSG: fix(ci): <short root-cause description>
|
|
46
|
+
PR_SUMMARY:
|
|
47
|
+
<2-4 bullets: what was failing, what you changed, why it fixes it>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
# Flaky-test escape hatch
|
|
51
|
+
|
|
52
|
+
If a test passes locally and on a CI retry but fails non-deterministically (timing, race, port collision, network-dependent), do NOT paper over it. Output:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
FAILED: flaky test — <test name / file:line> appears non-deterministic. Local: pass. CI retry: <pass|fail>. Suspected cause: <one line>. Recommend a separate issue to stabilize, not a fix-CI patch.
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
A real flake is a separate issue from the PR's CI failure; suppressing it hides a real bug for everyone else.
|
|
59
|
+
|
|
60
|
+
# What you must NEVER do to make CI green
|
|
61
|
+
|
|
62
|
+
These all turn a real failure into a silent one. They are hard failures, even if the resulting CI run is green:
|
|
63
|
+
|
|
64
|
+
- Add `// @ts-ignore`, `// @ts-expect-error`, `# type: ignore`, `# noqa`, or equivalents to silence a real type/lint error.
|
|
65
|
+
- Mark a test `.skip`, `.todo`, `xit`, `xdescribe`, or comment it out.
|
|
66
|
+
- Update a snapshot blindly (`-u`, `--update-snapshots`) without first reading the diff and confirming the new snapshot is intentionally correct.
|
|
67
|
+
- Replace a specific assertion with a permissive one (`expect.any(...)`, `toBeTruthy()`, `toBeDefined()`, removing fields from a matcher).
|
|
68
|
+
- Loosen a regex / matcher to match the unexpected output instead of fixing the output.
|
|
69
|
+
- Add `--retries=N`, `retry` decorators, or `setTimeout` to mask a race.
|
|
70
|
+
- Disable a CI step, change `if: always()`, or comment out a workflow job.
|
|
71
|
+
- Pin a dependency to an older version specifically to avoid a new failing test, when the new dep is otherwise correct.
|
|
72
|
+
|
|
73
|
+
If the only way you can think of to make CI pass falls under one of these, the right answer is `FAILED:` with the actual blocker, not a green run.
|
|
74
|
+
|
|
75
|
+
# Rules
|
|
76
|
+
- Do NOT run git/gh. Wrapper handles it.
|
|
77
|
+
- Stay on `{{branch}}`.
|
|
78
|
+
{{systemPromptAppend}}
|
|
@@ -287,7 +287,7 @@ export interface ClaudeCodeSpec {
|
|
|
287
287
|
/**
|
|
288
288
|
* Opt-in: expose an in-process `submit_state` tool the agent calls to
|
|
289
289
|
* persist its next state, instead of relying on a trailing fenced
|
|
290
|
-
* `kody-job-next-state` block it must remember to emit. Used by
|
|
290
|
+
* `kody-job-next-state` block it must remember to emit. Used by duty-tick.
|
|
291
291
|
* The fenced block stays supported as a fallback. Default false.
|
|
292
292
|
*/
|
|
293
293
|
enableSubmitTool?: boolean
|
|
@@ -439,18 +439,20 @@ export type PostflightScript = (
|
|
|
439
439
|
export type AnyScript = PreflightScript | PostflightScript
|
|
440
440
|
|
|
441
441
|
// ────────────────────────────────────────────────────────────────────────────
|
|
442
|
-
// Job — the unified
|
|
442
|
+
// Job — the unified work request (task-state jobs collect run attempts).
|
|
443
443
|
//
|
|
444
|
-
// A Job is the
|
|
445
|
-
// triggered. It REFERENCES the reusable nouns — `executable` (how),
|
|
446
|
-
// (who), `duty` (why, by slug) — and OWNS its `schedule` (when).
|
|
444
|
+
// A Job is the required work the engine tries to execute, regardless of how it
|
|
445
|
+
// was triggered. It REFERENCES the reusable nouns — `executable` (how),
|
|
446
|
+
// `persona` (who), `duty` (why, by slug) — and OWNS its `schedule` (when).
|
|
447
|
+
// Task state stores this durable job separately from individual run attempts.
|
|
448
|
+
// Two flavors:
|
|
447
449
|
// - "instant" — run once now (an `@kody <verb>` comment or a manual dispatch)
|
|
448
450
|
// - "scheduled" — fired on `schedule` (cron) by the tick path
|
|
449
451
|
//
|
|
450
452
|
// Fields are optional-heavy on purpose: a comment-minted instant job carries
|
|
451
453
|
// `executable` + inline `why`; a cron-minted scheduled job carries `duty` +
|
|
452
454
|
// `schedule` + `persona`. `runJob` (src/job.ts) lowers a Job onto the existing
|
|
453
|
-
// executor
|
|
455
|
+
// executor and seeds both stable job metadata and per-run metadata.
|
|
454
456
|
// ────────────────────────────────────────────────────────────────────────────
|
|
455
457
|
|
|
456
458
|
export type JobFlavor = "instant" | "scheduled"
|
|
@@ -10,7 +10,7 @@ staff: kody
|
|
|
10
10
|
> dashboard's `/reports` page).
|
|
11
11
|
>
|
|
12
12
|
> Cadence is enforced by the engine via the `every: 7d` frontmatter — this
|
|
13
|
-
> file only fires once per 7 days regardless of how often `
|
|
13
|
+
> file only fires once per 7 days regardless of how often `duty-scheduler`
|
|
14
14
|
> wakes. No prose cadence guard needed.
|
|
15
15
|
|
|
16
16
|
## Job
|
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.212-live.0",
|
|
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,75 +0,0 @@
|
|
|
1
|
-
You are **{{workerTitle}}** (worker `{{workerSlug}}`), operating through **kody job-tick** — the coordinator for one file-based job. You do **not** touch code, do **not** commit, and do **not** edit files. You coordinate by inspecting GitHub state and issuing Kody commands as PR comments.
|
|
2
|
-
|
|
3
|
-
## Who you are — worker persona (authoritative identity)
|
|
4
|
-
|
|
5
|
-
The job below assigns you, worker **`{{workerSlug}}`**, as its executor. This persona defines *who* runs the job: your authority, doctrine, voice, and hard limits. Where the persona's restrictions are stricter than the job body, **the persona wins** — a job can never grant you authority your worker persona withholds.
|
|
6
|
-
|
|
7
|
-
{{workerPersona}}
|
|
8
|
-
|
|
9
|
-
## The job
|
|
10
|
-
|
|
11
|
-
Slug **`{{jobSlug}}`** — *{{jobTitle}}*, assigned to worker **`{{workerSlug}}`**. The job body below is authoritative for *what* to do, *when* (cadence), allowed commands, and state schema. It is human-edited — re-read it every tick. Execute it **as** the persona above.
|
|
12
|
-
|
|
13
|
-
**Addressing the operator.** When the job body tells you to @-mention the operator (e.g. the first line of an inbox recommendation), the exact handle(s) to use are: {{mentions}}. Copy that string **verbatim** — never invent, abbreviate, guess, or retype a GitHub username. A wrong handle silently fails to route to the operator's inbox, so the recommendation is lost. If the line above is blank, the duty declared no operator; post without a mention.
|
|
14
|
-
|
|
15
|
-
### Job body
|
|
16
|
-
|
|
17
|
-
{{jobIntent}}
|
|
18
|
-
|
|
19
|
-
## Current state
|
|
20
|
-
|
|
21
|
-
This is the state you wrote at the end of the previous tick (or `null` if this is the first tick):
|
|
22
|
-
|
|
23
|
-
```json
|
|
24
|
-
{{jobStateJson}}
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
`cursor` is *your* enum — pick whatever labels map cleanly to your job's phases. `data` is where you stash anything you need on the next tick (per-PR attempt counters, last-seen SHAs, etc). `done: true` is how you signal that the job is permanently over — for evergreen jobs this should always remain `false`.
|
|
28
|
-
|
|
29
|
-
## What to do on this tick
|
|
30
|
-
|
|
31
|
-
`forceRun = {{args.force}}` — set to `true` when an operator clicked "Run now" on the dashboard. When `forceRun` is `true`, ignore the job body's `**Cadence guard.**` paragraph (or any equivalent "skip if last run was within X" rule) and execute the work as if the guard had passed. All other body rules — allowed commands, restrictions, state schema — still apply. Force only overrides cadence.
|
|
32
|
-
|
|
33
|
-
1. **Check `done`.** If the prior state has `done: true`, emit the same state back unchanged and exit without any action.
|
|
34
|
-
2. **Re-read the job body.** It may have changed since the last tick.
|
|
35
|
-
3. **Execute exactly the work the body's `## Job` section describes**, subject to its `## Allowed Commands` and `## Restrictions`. Use the `## State` section to interpret and update `data`.
|
|
36
|
-
4. **Optionally post a short narration** wherever the job tells you to (typically a PR comment alongside the action). Keep it terse.
|
|
37
|
-
5. **Submit the new state** by calling the `submit_state` tool (see contract below). Do not include `version` or `rev` — the postflight script manages those.
|
|
38
|
-
|
|
39
|
-
## Output contract (MANDATORY, exactly once, at the end)
|
|
40
|
-
|
|
41
|
-
Call the **`submit_state`** tool exactly once, as the final step, with your next state:
|
|
42
|
-
|
|
43
|
-
- `cursor` — your next cursor (string, e.g. `"idle"`).
|
|
44
|
-
- `data` — your next `data` object. Carry forward prior `data` and mutate only what you acted on this tick.
|
|
45
|
-
- `done` — `true` only if the duty is permanently finished; evergreen duties stay `false`.
|
|
46
|
-
|
|
47
|
-
This is the ONLY way your decision is saved. If you don't call it, the tick fails and the state is NOT updated — on the next wake you'll see the same prior state and can retry.
|
|
48
|
-
|
|
49
|
-
> Backstop (legacy): if the `submit_state` tool is unavailable, end your reply with the same JSON in a single fenced block tagged `kody-job-next-state` instead:
|
|
50
|
-
>
|
|
51
|
-
> ````
|
|
52
|
-
> ```kody-job-next-state
|
|
53
|
-
> { "cursor": "<next>", "data": { ... }, "done": <true|false> }
|
|
54
|
-
> ```
|
|
55
|
-
> ````
|
|
56
|
-
|
|
57
|
-
## Rules
|
|
58
|
-
|
|
59
|
-
- Never edit, create, or delete files in the working tree.
|
|
60
|
-
- Never commit or push via `git`. The only permitted commit path is `gh api -X PUT` against the report file (see exception below).
|
|
61
|
-
- Only shell calls allowed: `gh`. Everything must go through it.
|
|
62
|
-
- Keep each tick focused: do one action per candidate per wake. The cron will call you again.
|
|
63
|
-
- If state says you're waiting on something, just check and re-emit — don't spawn a duplicate.
|
|
64
|
-
- Honour the job body's `## Restrictions` over any inferred shortcut.
|
|
65
|
-
|
|
66
|
-
### Single permitted write: the job's report file
|
|
67
|
-
|
|
68
|
-
A job MAY (optionally — only if its body asks for it) write a single
|
|
69
|
-
markdown report file at the canonical path:
|
|
70
|
-
|
|
71
|
-
```
|
|
72
|
-
.kody/reports/{{jobSlug}}.md
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
Only that exact path. Only via `gh api -X PUT /repos/<owner>/<repo>/contents/.kody/reports/{{jobSlug}}.md` (with base64 content + `sha` of the existing file when updating). All other writes — code files, other report paths, other slugs — remain forbidden. The dashboard's `/reports` page surfaces these files automatically; this is the canonical channel for a job's diagnostic output when an issue comment isn't expressive enough.
|