@kody-ade/kody-engine 0.3.86 → 0.4.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 +7 -7
- package/dist/bin/kody.js +97 -95
- package/dist/executables/bug/profile.json +51 -19
- package/dist/executables/bug/prompt.md +4 -5
- package/dist/executables/{mission-scheduler → job-scheduler}/profile.json +6 -6
- package/dist/executables/{mission-tick → job-tick}/profile.json +11 -11
- package/dist/executables/job-tick/prompt.md +52 -0
- package/kody.config.schema.json +3 -3
- package/package.json +1 -1
- package/templates/kody.yml +2 -2
- package/dist/executables/bug-container/profile.json +0 -121
- package/dist/executables/bug-container/prompt.md +0 -6
- package/dist/executables/mission-tick/prompt.md +0 -52
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ npx -y -p @kody-ade/kody-engine@latest kody init
|
|
|
34
34
|
|
|
35
35
|
Required repo secrets: at least one model provider key (e.g. `MINIMAX_API_KEY`, `ANTHROPIC_API_KEY`). Recommended: `KODY_TOKEN` PAT so kody's commits trigger downstream CI and can modify `.github/workflows/*`.
|
|
36
36
|
|
|
37
|
-
The consumer workflow listens on three triggers: `issue_comment` (for `@kody …` dispatch), `workflow_dispatch` (manual runs, chat mode,
|
|
37
|
+
The consumer workflow listens on three triggers: `issue_comment` (for `@kody …` dispatch), `workflow_dispatch` (manual runs, chat mode, job wake), and `pull_request: [closed]` (auto-finalizes a merged `release/vX.Y.Z` PR).
|
|
38
38
|
|
|
39
39
|
## Commands
|
|
40
40
|
|
|
@@ -58,9 +58,9 @@ kody bug --issue <N> # plan → run → review
|
|
|
58
58
|
kody spec --issue <N> # research → plan (no code, terminates at plan)
|
|
59
59
|
kody chore --issue <N> # run → review (→ fix)
|
|
60
60
|
|
|
61
|
-
#
|
|
62
|
-
kody
|
|
63
|
-
kody
|
|
61
|
+
# jobs & watches (scheduled, coordinate work via issue state)
|
|
62
|
+
kody job-scheduler # fans out to per-issue job-tick
|
|
63
|
+
kody job-tick --issue <N> # one tick of a kody:job issue
|
|
64
64
|
kody watch-stale-prs # weekly stale-PR report
|
|
65
65
|
kody memorize # daily vault wiki update from recent PRs
|
|
66
66
|
|
|
@@ -78,11 +78,11 @@ kody chat [--session <id>] # dashboard-driven chat s
|
|
|
78
78
|
|
|
79
79
|
Each flow (`feature`, `bug`, `spec`, `chore`) is a declarative transition table: postflight entries dispatch the next executable based on `data.taskState.core.lastOutcome.type` via `runWhen`. No engine changes to add a new flow — drop a new `src/executables/<flow-name>/` with a different table. `classify` picks the flow for an unlabeled issue.
|
|
80
80
|
|
|
81
|
-
###
|
|
81
|
+
### Jobs
|
|
82
82
|
|
|
83
|
-
A **
|
|
83
|
+
A **job** is a stateful, bounded goal expressed as a labeled GitHub issue (`kody:job`). A **watch** is a stateless repeating loop. A **manager** is a job whose job happens to be overseeing other jobs. All three run on the same scheduled-executable substrate.
|
|
84
84
|
|
|
85
|
-
`
|
|
85
|
+
`job-scheduler` wakes on cron (default `*/5 * * * *`) or empty `workflow_dispatch`, finds every open `kody:job` issue, and calls `job-tick` once per issue. The tick agent reads the issue body (human-owned prose) and a dedicated state comment (bot-owned JSON), decides the next step, and emits a fenced `kody-job-next-state` block the postflight persists. Children are spawned via `gh workflow run kody.yml` (not `@kody` comments — the default `GITHUB_TOKEN` can dispatch workflows but can't post auto-triggering comments).
|
|
86
86
|
|
|
87
87
|
### `ui-review`
|
|
88
88
|
|
package/dist/bin/kody.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// package.json
|
|
4
4
|
var package_default = {
|
|
5
5
|
name: "@kody-ade/kody-engine",
|
|
6
|
-
version: "0.
|
|
6
|
+
version: "0.4.0",
|
|
7
7
|
description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
|
|
8
8
|
license: "MIT",
|
|
9
9
|
type: "module",
|
|
@@ -188,10 +188,10 @@ function loadConfig(projectDir = process.cwd()) {
|
|
|
188
188
|
aliases: mergeAliases(raw.aliases),
|
|
189
189
|
classify: parseClassifyConfig(raw.classify),
|
|
190
190
|
release: parseReleaseConfig(raw.release),
|
|
191
|
-
|
|
191
|
+
jobs: parseJobsConfig(raw.jobs)
|
|
192
192
|
};
|
|
193
193
|
}
|
|
194
|
-
function
|
|
194
|
+
function parseJobsConfig(raw) {
|
|
195
195
|
if (!raw || typeof raw !== "object") return void 0;
|
|
196
196
|
const r = raw;
|
|
197
197
|
const out = {};
|
|
@@ -199,7 +199,7 @@ function parseMissionsConfig(raw) {
|
|
|
199
199
|
out.stateBackend = r.stateBackend;
|
|
200
200
|
} else if (typeof r.stateBackend === "string") {
|
|
201
201
|
throw new Error(
|
|
202
|
-
`kody.config.json:
|
|
202
|
+
`kody.config.json: jobs.stateBackend must be "contents-api" or "local-file", got "${r.stateBackend}"`
|
|
203
203
|
);
|
|
204
204
|
}
|
|
205
205
|
return Object.keys(out).length > 0 ? out : void 0;
|
|
@@ -2318,7 +2318,9 @@ function parseAgentResult(finalText) {
|
|
|
2318
2318
|
}
|
|
2319
2319
|
const hasDoneMarker = DONE_RE.test(text);
|
|
2320
2320
|
const hasCommitMsg = /^[\s>*_#`~-]*COMMIT_MSG\s*:/im.test(text);
|
|
2321
|
-
|
|
2321
|
+
const hasPrSummary = /^[\s>*_#`~-]*PR_SUMMARY\s*:/im.test(text);
|
|
2322
|
+
if (!hasDoneMarker && !hasCommitMsg && !hasPrSummary) {
|
|
2323
|
+
const tail = text.length > 400 ? `\u2026${text.slice(-400)}` : text;
|
|
2322
2324
|
return {
|
|
2323
2325
|
done: false,
|
|
2324
2326
|
commitMessage: "",
|
|
@@ -2326,7 +2328,7 @@ function parseAgentResult(finalText) {
|
|
|
2326
2328
|
feedbackActions: "",
|
|
2327
2329
|
planDeviations: "",
|
|
2328
2330
|
priorArt: "",
|
|
2329
|
-
failureReason:
|
|
2331
|
+
failureReason: `no DONE or FAILED marker in agent output \u2014 agent tail: ${tail}`
|
|
2330
2332
|
};
|
|
2331
2333
|
}
|
|
2332
2334
|
const commitMatch = text.match(/^[\s>*_#`~-]*COMMIT_MSG[\s>*_#`~-]*\s*:\s*(.+)$/im);
|
|
@@ -3186,7 +3188,7 @@ function failedAction(reason) {
|
|
|
3186
3188
|
return { type: "CLASSIFY_FAILED", payload: { reason }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3187
3189
|
}
|
|
3188
3190
|
|
|
3189
|
-
// src/scripts/
|
|
3191
|
+
// src/scripts/dispatchJobFileTicks.ts
|
|
3190
3192
|
import * as fs18 from "fs";
|
|
3191
3193
|
import * as path17 from "path";
|
|
3192
3194
|
|
|
@@ -3414,26 +3416,26 @@ function minimizeComment(nodeId, cwd) {
|
|
|
3414
3416
|
gh2(["api", "graphql", "-f", `query=${mutation}`, "-f", `id=${nodeId}`], { cwd });
|
|
3415
3417
|
}
|
|
3416
3418
|
|
|
3417
|
-
// src/scripts/
|
|
3419
|
+
// src/scripts/jobState/backend.ts
|
|
3418
3420
|
function isStateUnchanged(prev, next) {
|
|
3419
3421
|
if (prev.cursor !== next.cursor) return false;
|
|
3420
3422
|
if (prev.done !== next.done) return false;
|
|
3421
3423
|
return JSON.stringify(prev.data) === JSON.stringify(next.data);
|
|
3422
3424
|
}
|
|
3423
|
-
function stateFilePath(
|
|
3424
|
-
return `${
|
|
3425
|
+
function stateFilePath(jobsDir, slug) {
|
|
3426
|
+
return `${jobsDir.replace(/\/+$/, "")}/${slug}.state.json`;
|
|
3425
3427
|
}
|
|
3426
3428
|
function slugFromStateFilePath(filePath) {
|
|
3427
3429
|
const last = filePath.split("/").pop() ?? filePath;
|
|
3428
3430
|
return last.replace(/\.state\.json$/i, "");
|
|
3429
3431
|
}
|
|
3430
3432
|
|
|
3431
|
-
// src/scripts/
|
|
3433
|
+
// src/scripts/jobState/contentsApiBackend.ts
|
|
3432
3434
|
var ContentsApiBackend = class {
|
|
3433
3435
|
name = "contents-api";
|
|
3434
3436
|
owner;
|
|
3435
3437
|
repo;
|
|
3436
|
-
|
|
3438
|
+
jobsDir;
|
|
3437
3439
|
cwd;
|
|
3438
3440
|
constructor(opts) {
|
|
3439
3441
|
if (!opts.owner || !opts.repo) {
|
|
@@ -3441,11 +3443,11 @@ var ContentsApiBackend = class {
|
|
|
3441
3443
|
}
|
|
3442
3444
|
this.owner = opts.owner;
|
|
3443
3445
|
this.repo = opts.repo;
|
|
3444
|
-
this.
|
|
3446
|
+
this.jobsDir = opts.jobsDir;
|
|
3445
3447
|
this.cwd = opts.cwd;
|
|
3446
3448
|
}
|
|
3447
3449
|
load(slug) {
|
|
3448
|
-
const filePath = stateFilePath(this.
|
|
3450
|
+
const filePath = stateFilePath(this.jobsDir, slug);
|
|
3449
3451
|
let raw = "";
|
|
3450
3452
|
try {
|
|
3451
3453
|
raw = gh2(["api", `/repos/${this.owner}/${this.repo}/contents/${filePath}`], { cwd: this.cwd });
|
|
@@ -3488,7 +3490,7 @@ var ContentsApiBackend = class {
|
|
|
3488
3490
|
const slug = slugFromStateFilePath(loaded.path);
|
|
3489
3491
|
const body = JSON.stringify(next, null, 2) + "\n";
|
|
3490
3492
|
const payload = {
|
|
3491
|
-
message: `chore(
|
|
3493
|
+
message: `chore(jobs): update state for ${slug} (rev ${next.rev})`,
|
|
3492
3494
|
content: Buffer.from(body, "utf-8").toString("base64")
|
|
3493
3495
|
};
|
|
3494
3496
|
if (typeof loaded.handle === "string") payload.sha = loaded.handle;
|
|
@@ -3500,35 +3502,35 @@ var ContentsApiBackend = class {
|
|
|
3500
3502
|
}
|
|
3501
3503
|
};
|
|
3502
3504
|
|
|
3503
|
-
// src/scripts/
|
|
3505
|
+
// src/scripts/jobState/localFileBackend.ts
|
|
3504
3506
|
import * as fs17 from "fs";
|
|
3505
3507
|
import * as path16 from "path";
|
|
3506
3508
|
var LocalFileBackend = class {
|
|
3507
3509
|
name = "local-file";
|
|
3508
3510
|
cwd;
|
|
3509
|
-
|
|
3511
|
+
jobsDir;
|
|
3510
3512
|
absDir;
|
|
3511
3513
|
owner;
|
|
3512
3514
|
repo;
|
|
3513
3515
|
cache;
|
|
3514
3516
|
constructor(opts) {
|
|
3515
3517
|
if (!opts.cwd) throw new Error("LocalFileBackend: cwd is required");
|
|
3516
|
-
if (!opts.
|
|
3518
|
+
if (!opts.jobsDir) throw new Error("LocalFileBackend: jobsDir is required");
|
|
3517
3519
|
if (!opts.owner || !opts.repo) throw new Error("LocalFileBackend: owner and repo are required");
|
|
3518
3520
|
this.cwd = opts.cwd;
|
|
3519
|
-
this.
|
|
3520
|
-
this.absDir = path16.join(opts.cwd, opts.
|
|
3521
|
+
this.jobsDir = opts.jobsDir;
|
|
3522
|
+
this.absDir = path16.join(opts.cwd, opts.jobsDir);
|
|
3521
3523
|
this.owner = opts.owner;
|
|
3522
3524
|
this.repo = opts.repo;
|
|
3523
3525
|
this.cache = opts.cache ?? defaultCacheAdapter();
|
|
3524
3526
|
}
|
|
3525
3527
|
/**
|
|
3526
|
-
* Restore the
|
|
3528
|
+
* Restore the job directory from the most recent Actions cache entry
|
|
3527
3529
|
* for this repo. No-op when not running in Actions or when no cache exists.
|
|
3528
3530
|
*/
|
|
3529
3531
|
async hydrate() {
|
|
3530
3532
|
if (!this.cache.isAvailable()) {
|
|
3531
|
-
process.stdout.write(`[
|
|
3533
|
+
process.stdout.write(`[jobs/state] hydrate skipped: actions cache unavailable
|
|
3532
3534
|
`);
|
|
3533
3535
|
return;
|
|
3534
3536
|
}
|
|
@@ -3538,26 +3540,26 @@ var LocalFileBackend = class {
|
|
|
3538
3540
|
try {
|
|
3539
3541
|
const matched = await this.cache.restore([this.absDir], probeKey, [prefix]);
|
|
3540
3542
|
if (matched) {
|
|
3541
|
-
process.stdout.write(`[
|
|
3543
|
+
process.stdout.write(`[jobs/state] hydrate hit: ${matched}
|
|
3542
3544
|
`);
|
|
3543
3545
|
} else {
|
|
3544
|
-
process.stdout.write(`[
|
|
3546
|
+
process.stdout.write(`[jobs/state] hydrate miss (cold start)
|
|
3545
3547
|
`);
|
|
3546
3548
|
}
|
|
3547
3549
|
} catch (err) {
|
|
3548
3550
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3549
|
-
process.stderr.write(`[
|
|
3551
|
+
process.stderr.write(`[jobs/state] hydrate failed (continuing): ${msg}
|
|
3550
3552
|
`);
|
|
3551
3553
|
}
|
|
3552
3554
|
}
|
|
3553
3555
|
/**
|
|
3554
|
-
* Save the
|
|
3556
|
+
* Save the job directory to the Actions cache under a unique key.
|
|
3555
3557
|
* No-op when not running in Actions. Errors are logged, never thrown —
|
|
3556
3558
|
* callers run this in a finally block and must not swallow real errors.
|
|
3557
3559
|
*/
|
|
3558
3560
|
async persist() {
|
|
3559
3561
|
if (!this.cache.isAvailable()) {
|
|
3560
|
-
process.stdout.write(`[
|
|
3562
|
+
process.stdout.write(`[jobs/state] persist skipped: actions cache unavailable
|
|
3561
3563
|
`);
|
|
3562
3564
|
return;
|
|
3563
3565
|
}
|
|
@@ -3567,16 +3569,16 @@ var LocalFileBackend = class {
|
|
|
3567
3569
|
const key = `${this.cacheKeyPrefix()}${process.env.GITHUB_RUN_ID ?? "norunid"}-${Date.now()}`;
|
|
3568
3570
|
try {
|
|
3569
3571
|
await this.cache.save([this.absDir], key);
|
|
3570
|
-
process.stdout.write(`[
|
|
3572
|
+
process.stdout.write(`[jobs/state] persist saved: ${key}
|
|
3571
3573
|
`);
|
|
3572
3574
|
} catch (err) {
|
|
3573
3575
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3574
|
-
process.stderr.write(`[
|
|
3576
|
+
process.stderr.write(`[jobs/state] persist failed (continuing): ${msg}
|
|
3575
3577
|
`);
|
|
3576
3578
|
}
|
|
3577
3579
|
}
|
|
3578
3580
|
load(slug) {
|
|
3579
|
-
const relPath = stateFilePath(this.
|
|
3581
|
+
const relPath = stateFilePath(this.jobsDir, slug);
|
|
3580
3582
|
const absPath = path16.join(this.cwd, relPath);
|
|
3581
3583
|
if (!fs17.existsSync(absPath)) {
|
|
3582
3584
|
return { path: relPath, handle: null, state: initialStateEnvelope("seed"), created: true };
|
|
@@ -3605,7 +3607,7 @@ var LocalFileBackend = class {
|
|
|
3605
3607
|
return true;
|
|
3606
3608
|
}
|
|
3607
3609
|
cacheKeyPrefix() {
|
|
3608
|
-
return `kody-
|
|
3610
|
+
return `kody-job-state-${sanitizeKey(this.owner)}-${sanitizeKey(this.repo)}-`;
|
|
3609
3611
|
}
|
|
3610
3612
|
};
|
|
3611
3613
|
function sanitizeKey(s) {
|
|
@@ -3644,19 +3646,19 @@ function defaultCacheAdapter() {
|
|
|
3644
3646
|
};
|
|
3645
3647
|
}
|
|
3646
3648
|
|
|
3647
|
-
// src/scripts/
|
|
3649
|
+
// src/scripts/jobState/index.ts
|
|
3648
3650
|
function resolveBackend(opts) {
|
|
3649
3651
|
const owner = opts.config.github?.owner;
|
|
3650
3652
|
const repo = opts.config.github?.repo;
|
|
3651
3653
|
if (!owner || !repo) {
|
|
3652
3654
|
throw new Error("resolveBackend: config.github.owner and config.github.repo must be set");
|
|
3653
3655
|
}
|
|
3654
|
-
const requested = opts.config.
|
|
3656
|
+
const requested = opts.config.jobs?.stateBackend ?? "contents-api";
|
|
3655
3657
|
switch (requested) {
|
|
3656
3658
|
case "contents-api":
|
|
3657
|
-
return new ContentsApiBackend({ owner, repo,
|
|
3659
|
+
return new ContentsApiBackend({ owner, repo, jobsDir: opts.jobsDir, cwd: opts.cwd });
|
|
3658
3660
|
case "local-file":
|
|
3659
|
-
return new LocalFileBackend({ cwd: opts.cwd,
|
|
3661
|
+
return new LocalFileBackend({ cwd: opts.cwd, jobsDir: opts.jobsDir, owner, repo });
|
|
3660
3662
|
default: {
|
|
3661
3663
|
const _exhaustive = requested;
|
|
3662
3664
|
throw new Error(`resolveBackend: unknown stateBackend "${String(_exhaustive)}"`);
|
|
@@ -3664,32 +3666,32 @@ function resolveBackend(opts) {
|
|
|
3664
3666
|
}
|
|
3665
3667
|
}
|
|
3666
3668
|
|
|
3667
|
-
// src/scripts/
|
|
3668
|
-
var
|
|
3669
|
+
// src/scripts/dispatchJobFileTicks.ts
|
|
3670
|
+
var dispatchJobFileTicks = async (ctx, _profile, args) => {
|
|
3669
3671
|
ctx.skipAgent = true;
|
|
3670
3672
|
const targetExecutable = String(args?.targetExecutable ?? "");
|
|
3671
3673
|
if (!targetExecutable) {
|
|
3672
|
-
throw new Error("
|
|
3674
|
+
throw new Error("dispatchJobFileTicks: `with.targetExecutable` is required");
|
|
3673
3675
|
}
|
|
3674
|
-
const
|
|
3675
|
-
const slugArg = String(args?.slugArg ?? "
|
|
3676
|
-
const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd,
|
|
3676
|
+
const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
|
|
3677
|
+
const slugArg = String(args?.slugArg ?? "job");
|
|
3678
|
+
const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, jobsDir });
|
|
3677
3679
|
if (backend.hydrate) {
|
|
3678
3680
|
await backend.hydrate();
|
|
3679
3681
|
}
|
|
3680
3682
|
try {
|
|
3681
|
-
const slugs =
|
|
3682
|
-
ctx.data.
|
|
3683
|
+
const slugs = listJobSlugs(path17.join(ctx.cwd, jobsDir));
|
|
3684
|
+
ctx.data.jobSlugCount = slugs.length;
|
|
3683
3685
|
if (slugs.length === 0) {
|
|
3684
|
-
process.stdout.write(`[
|
|
3686
|
+
process.stdout.write(`[jobs] no job files in ${jobsDir}
|
|
3685
3687
|
`);
|
|
3686
3688
|
return;
|
|
3687
3689
|
}
|
|
3688
|
-
process.stdout.write(`[
|
|
3690
|
+
process.stdout.write(`[jobs] ticking ${slugs.length} job(s) via ${targetExecutable}
|
|
3689
3691
|
`);
|
|
3690
3692
|
const results = [];
|
|
3691
3693
|
for (const slug of slugs) {
|
|
3692
|
-
process.stdout.write(`[
|
|
3694
|
+
process.stdout.write(`[jobs] \u2192 tick ${slug}
|
|
3693
3695
|
`);
|
|
3694
3696
|
try {
|
|
3695
3697
|
const out = await runExecutable(targetExecutable, {
|
|
@@ -3701,17 +3703,17 @@ var dispatchMissionFileTicks = async (ctx, _profile, args) => {
|
|
|
3701
3703
|
});
|
|
3702
3704
|
results.push({ slug, exitCode: out.exitCode, reason: out.reason });
|
|
3703
3705
|
if (out.exitCode !== 0) {
|
|
3704
|
-
process.stderr.write(`[
|
|
3706
|
+
process.stderr.write(`[jobs] tick ${slug} failed (exit ${out.exitCode}): ${out.reason ?? ""}
|
|
3705
3707
|
`);
|
|
3706
3708
|
}
|
|
3707
3709
|
} catch (err) {
|
|
3708
3710
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3709
|
-
process.stderr.write(`[
|
|
3711
|
+
process.stderr.write(`[jobs] tick ${slug} crashed: ${msg}
|
|
3710
3712
|
`);
|
|
3711
3713
|
results.push({ slug, exitCode: 99, reason: msg });
|
|
3712
3714
|
}
|
|
3713
3715
|
}
|
|
3714
|
-
ctx.data.
|
|
3716
|
+
ctx.data.jobTickResults = results;
|
|
3715
3717
|
ctx.output.exitCode = 0;
|
|
3716
3718
|
} finally {
|
|
3717
3719
|
if (backend.persist) {
|
|
@@ -3719,13 +3721,13 @@ var dispatchMissionFileTicks = async (ctx, _profile, args) => {
|
|
|
3719
3721
|
await backend.persist();
|
|
3720
3722
|
} catch (err) {
|
|
3721
3723
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3722
|
-
process.stderr.write(`[
|
|
3724
|
+
process.stderr.write(`[jobs] backend persist failed: ${msg}
|
|
3723
3725
|
`);
|
|
3724
3726
|
}
|
|
3725
3727
|
}
|
|
3726
3728
|
}
|
|
3727
3729
|
};
|
|
3728
|
-
function
|
|
3730
|
+
function listJobSlugs(absDir) {
|
|
3729
3731
|
if (!fs18.existsSync(absDir)) return [];
|
|
3730
3732
|
let entries;
|
|
3731
3733
|
try {
|
|
@@ -3736,26 +3738,26 @@ function listMissionSlugs(absDir) {
|
|
|
3736
3738
|
return entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name.replace(/\.md$/, "")).filter((slug) => slug.length > 0 && !slug.startsWith("_") && !slug.startsWith(".")).sort();
|
|
3737
3739
|
}
|
|
3738
3740
|
|
|
3739
|
-
// src/scripts/
|
|
3740
|
-
var
|
|
3741
|
+
// src/scripts/dispatchJobTicks.ts
|
|
3742
|
+
var dispatchJobTicks = async (ctx, _profile, args) => {
|
|
3741
3743
|
ctx.skipAgent = true;
|
|
3742
3744
|
const label = String(args?.label ?? "");
|
|
3743
3745
|
const targetExecutable = String(args?.targetExecutable ?? "");
|
|
3744
|
-
if (!label) throw new Error("
|
|
3745
|
-
if (!targetExecutable) throw new Error("
|
|
3746
|
+
if (!label) throw new Error("dispatchJobTicks: `with.label` is required");
|
|
3747
|
+
if (!targetExecutable) throw new Error("dispatchJobTicks: `with.targetExecutable` is required");
|
|
3746
3748
|
const issueArg = String(args?.issueArg ?? "issue");
|
|
3747
3749
|
const issues = listIssuesByLabel(label, ctx.cwd);
|
|
3748
|
-
ctx.data.
|
|
3750
|
+
ctx.data.jobIssueCount = issues.length;
|
|
3749
3751
|
if (issues.length === 0) {
|
|
3750
|
-
process.stdout.write(`[
|
|
3752
|
+
process.stdout.write(`[jobs] no open issues with label "${label}"
|
|
3751
3753
|
`);
|
|
3752
3754
|
return;
|
|
3753
3755
|
}
|
|
3754
|
-
process.stdout.write(`[
|
|
3756
|
+
process.stdout.write(`[jobs] ticking ${issues.length} issue(s) via ${targetExecutable}
|
|
3755
3757
|
`);
|
|
3756
3758
|
const results = [];
|
|
3757
3759
|
for (const issue of issues) {
|
|
3758
|
-
process.stdout.write(`[
|
|
3760
|
+
process.stdout.write(`[jobs] \u2192 tick #${issue.number}: ${issue.title}
|
|
3759
3761
|
`);
|
|
3760
3762
|
try {
|
|
3761
3763
|
const out = await runExecutable(targetExecutable, {
|
|
@@ -3767,17 +3769,17 @@ var dispatchMissionTicks = async (ctx, _profile, args) => {
|
|
|
3767
3769
|
});
|
|
3768
3770
|
results.push({ issue: issue.number, exitCode: out.exitCode, reason: out.reason });
|
|
3769
3771
|
if (out.exitCode !== 0) {
|
|
3770
|
-
process.stderr.write(`[
|
|
3772
|
+
process.stderr.write(`[jobs] tick #${issue.number} failed (exit ${out.exitCode}): ${out.reason ?? ""}
|
|
3771
3773
|
`);
|
|
3772
3774
|
}
|
|
3773
3775
|
} catch (err) {
|
|
3774
3776
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3775
|
-
process.stderr.write(`[
|
|
3777
|
+
process.stderr.write(`[jobs] tick #${issue.number} crashed: ${msg}
|
|
3776
3778
|
`);
|
|
3777
3779
|
results.push({ issue: issue.number, exitCode: 99, reason: msg });
|
|
3778
3780
|
}
|
|
3779
3781
|
}
|
|
3780
|
-
ctx.data.
|
|
3782
|
+
ctx.data.jobTickResults = results;
|
|
3781
3783
|
ctx.output.exitCode = 0;
|
|
3782
3784
|
};
|
|
3783
3785
|
function listIssuesByLabel(label, cwd) {
|
|
@@ -4951,31 +4953,31 @@ var loadIssueStateComment = async (ctx, _profile, args) => {
|
|
|
4951
4953
|
ctx.data.issueStateJson = loaded ? JSON.stringify(loaded.state, null, 2) : "null";
|
|
4952
4954
|
};
|
|
4953
4955
|
|
|
4954
|
-
// src/scripts/
|
|
4956
|
+
// src/scripts/loadJobFromFile.ts
|
|
4955
4957
|
import * as fs22 from "fs";
|
|
4956
4958
|
import * as path20 from "path";
|
|
4957
|
-
var
|
|
4958
|
-
const
|
|
4959
|
-
const slugArg = String(args?.slugArg ?? "
|
|
4959
|
+
var loadJobFromFile = async (ctx, _profile, args) => {
|
|
4960
|
+
const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
|
|
4961
|
+
const slugArg = String(args?.slugArg ?? "job");
|
|
4960
4962
|
const slug = String(ctx.args[slugArg] ?? "").trim();
|
|
4961
4963
|
if (!slug) {
|
|
4962
|
-
throw new Error(`
|
|
4964
|
+
throw new Error(`loadJobFromFile: ctx.args.${slugArg} must be a non-empty slug`);
|
|
4963
4965
|
}
|
|
4964
|
-
const absPath = path20.join(ctx.cwd,
|
|
4966
|
+
const absPath = path20.join(ctx.cwd, jobsDir, `${slug}.md`);
|
|
4965
4967
|
if (!fs22.existsSync(absPath)) {
|
|
4966
|
-
throw new Error(`
|
|
4968
|
+
throw new Error(`loadJobFromFile: job file not found: ${absPath}`);
|
|
4967
4969
|
}
|
|
4968
4970
|
const raw = fs22.readFileSync(absPath, "utf-8");
|
|
4969
|
-
const { title, body } =
|
|
4970
|
-
const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd,
|
|
4971
|
+
const { title, body } = parseJobFile(raw, slug);
|
|
4972
|
+
const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, jobsDir });
|
|
4971
4973
|
const loaded = await backend.load(slug);
|
|
4972
|
-
ctx.data.
|
|
4973
|
-
ctx.data.
|
|
4974
|
-
ctx.data.
|
|
4975
|
-
ctx.data.
|
|
4976
|
-
ctx.data.
|
|
4974
|
+
ctx.data.jobSlug = slug;
|
|
4975
|
+
ctx.data.jobTitle = title;
|
|
4976
|
+
ctx.data.jobIntent = body;
|
|
4977
|
+
ctx.data.jobState = loaded;
|
|
4978
|
+
ctx.data.jobStateJson = JSON.stringify(loaded.state, null, 2);
|
|
4977
4979
|
};
|
|
4978
|
-
function
|
|
4980
|
+
function parseJobFile(raw, slug) {
|
|
4979
4981
|
let stripped = raw;
|
|
4980
4982
|
if (stripped.startsWith("---\n")) {
|
|
4981
4983
|
const end = stripped.indexOf("\n---\n", 4);
|
|
@@ -5637,16 +5639,16 @@ function escapeRegex(s) {
|
|
|
5637
5639
|
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
5638
5640
|
}
|
|
5639
5641
|
|
|
5640
|
-
// src/scripts/
|
|
5642
|
+
// src/scripts/parseJobStateFromAgentResult.ts
|
|
5641
5643
|
function isPartialEnvelope2(x) {
|
|
5642
5644
|
if (x === null || typeof x !== "object") return false;
|
|
5643
5645
|
const o = x;
|
|
5644
5646
|
return typeof o.cursor === "string" && o.cursor.length > 0 && typeof o.done === "boolean" && o.data !== null && typeof o.data === "object" && !Array.isArray(o.data);
|
|
5645
5647
|
}
|
|
5646
|
-
var
|
|
5648
|
+
var parseJobStateFromAgentResult = async (ctx, _profile, agentResult, args) => {
|
|
5647
5649
|
const fenceLabel = String(args?.fenceLabel ?? "");
|
|
5648
5650
|
if (!fenceLabel) {
|
|
5649
|
-
throw new Error("
|
|
5651
|
+
throw new Error("parseJobStateFromAgentResult: `with.fenceLabel` is required");
|
|
5650
5652
|
}
|
|
5651
5653
|
if (!agentResult) {
|
|
5652
5654
|
ctx.data.nextStateParseError = "agent did not run";
|
|
@@ -5669,7 +5671,7 @@ var parseMissionStateFromAgentResult = async (ctx, _profile, agentResult, args)
|
|
|
5669
5671
|
ctx.data.nextStateParseError = "state must be an object with string `cursor`, object `data`, and boolean `done`";
|
|
5670
5672
|
return;
|
|
5671
5673
|
}
|
|
5672
|
-
const loaded = ctx.data.
|
|
5674
|
+
const loaded = ctx.data.jobState;
|
|
5673
5675
|
const prevRev = loaded?.state.rev ?? 0;
|
|
5674
5676
|
const next = {
|
|
5675
5677
|
version: 1,
|
|
@@ -5678,7 +5680,7 @@ var parseMissionStateFromAgentResult = async (ctx, _profile, agentResult, args)
|
|
|
5678
5680
|
data: parsed.data,
|
|
5679
5681
|
done: parsed.done
|
|
5680
5682
|
};
|
|
5681
|
-
ctx.data.
|
|
5683
|
+
ctx.data.nextJobState = next;
|
|
5682
5684
|
};
|
|
5683
5685
|
function escapeRegex2(s) {
|
|
5684
5686
|
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
@@ -7201,26 +7203,26 @@ var writeIssueStateComment = async (ctx, _profile, _agentResult, args) => {
|
|
|
7201
7203
|
}
|
|
7202
7204
|
};
|
|
7203
7205
|
|
|
7204
|
-
// src/scripts/
|
|
7205
|
-
var
|
|
7206
|
+
// src/scripts/writeJobStateFile.ts
|
|
7207
|
+
var writeJobStateFile = async (ctx, _profile, _agentResult, args) => {
|
|
7206
7208
|
const parseError = ctx.data.nextStateParseError;
|
|
7207
7209
|
if (parseError) {
|
|
7208
|
-
process.stderr.write(`[kody]
|
|
7210
|
+
process.stderr.write(`[kody] job state write skipped: ${parseError}
|
|
7209
7211
|
`);
|
|
7210
7212
|
if (ctx.output.exitCode === 0) ctx.output.exitCode = 1;
|
|
7211
7213
|
if (!ctx.output.reason) ctx.output.reason = `next-state parse failed: ${parseError}`;
|
|
7212
7214
|
return;
|
|
7213
7215
|
}
|
|
7214
|
-
const next = ctx.data.
|
|
7216
|
+
const next = ctx.data.nextJobState;
|
|
7215
7217
|
if (!next) {
|
|
7216
7218
|
return;
|
|
7217
7219
|
}
|
|
7218
|
-
const loaded = ctx.data.
|
|
7220
|
+
const loaded = ctx.data.jobState;
|
|
7219
7221
|
if (!loaded) {
|
|
7220
|
-
throw new Error("
|
|
7222
|
+
throw new Error("writeJobStateFile: ctx.data.jobState missing \u2014 preflight must run first");
|
|
7221
7223
|
}
|
|
7222
|
-
const
|
|
7223
|
-
const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd,
|
|
7224
|
+
const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
|
|
7225
|
+
const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, jobsDir });
|
|
7224
7226
|
await backend.save(loaded, next);
|
|
7225
7227
|
};
|
|
7226
7228
|
|
|
@@ -7269,7 +7271,7 @@ var preflightScripts = {
|
|
|
7269
7271
|
loadVaultContext,
|
|
7270
7272
|
loadIssueContext,
|
|
7271
7273
|
loadIssueStateComment,
|
|
7272
|
-
|
|
7274
|
+
loadJobFromFile,
|
|
7273
7275
|
loadConventions,
|
|
7274
7276
|
loadCoverageRules,
|
|
7275
7277
|
loadPriorArt,
|
|
@@ -7284,16 +7286,16 @@ var preflightScripts = {
|
|
|
7284
7286
|
skipAgent,
|
|
7285
7287
|
classifyByLabel,
|
|
7286
7288
|
diagMcp,
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
+
dispatchJobTicks,
|
|
7290
|
+
dispatchJobFileTicks
|
|
7289
7291
|
};
|
|
7290
7292
|
var postflightScripts = {
|
|
7291
7293
|
parseAgentResult: parseAgentResult2,
|
|
7292
7294
|
parseIssueStateFromAgentResult,
|
|
7293
|
-
|
|
7295
|
+
parseJobStateFromAgentResult,
|
|
7294
7296
|
parseReproOutput,
|
|
7295
7297
|
writeIssueStateComment,
|
|
7296
|
-
|
|
7298
|
+
writeJobStateFile,
|
|
7297
7299
|
requireFeedbackActions,
|
|
7298
7300
|
requirePlanDeviations,
|
|
7299
7301
|
verify,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bug",
|
|
3
|
-
"role": "
|
|
4
|
-
"describe": "
|
|
3
|
+
"role": "container",
|
|
4
|
+
"describe": "Bug / enhancement flow — reproduce → plan → run → review (→ fix on concerns/fail). Children run sequentially in one process via the container loop.",
|
|
5
5
|
"inputs": [
|
|
6
6
|
{
|
|
7
7
|
"name": "issue",
|
|
@@ -45,33 +45,21 @@
|
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
47
|
{ "script": "loadIssueContext" },
|
|
48
|
-
{ "script": "loadTaskState" }
|
|
49
|
-
{ "script": "skipAgent" }
|
|
48
|
+
{ "script": "loadTaskState" }
|
|
50
49
|
],
|
|
51
50
|
"postflight": [
|
|
52
|
-
{ "script": "startFlow", "with": { "entry": "plan", "target": "issue" } },
|
|
53
|
-
|
|
54
|
-
{ "script": "dispatch", "with": { "next": "run", "target": "issue" },
|
|
55
|
-
"runWhen": { "data.taskState.core.lastOutcome.type": "PLAN_COMPLETED" } },
|
|
56
|
-
|
|
57
|
-
{ "script": "dispatch", "with": { "next": "review", "target": "pr" },
|
|
58
|
-
"runWhen": { "data.taskState.core.lastOutcome.type": "RUN_COMPLETED" } },
|
|
59
|
-
|
|
60
51
|
{ "script": "finishFlow",
|
|
61
52
|
"with": { "reason": "review-passed", "label": "kody:done", "color": "0e8a16", "description": "kody: PR ready for human review/merge" },
|
|
62
53
|
"runWhen": { "data.taskState.core.lastOutcome.type": "REVIEW_PASS" } },
|
|
63
54
|
|
|
64
|
-
{ "script": "
|
|
65
|
-
"
|
|
55
|
+
{ "script": "finishFlow",
|
|
56
|
+
"with": { "reason": "fix-applied", "label": "kody:done", "color": "0e8a16", "description": "kody: PR ready for human review/merge" },
|
|
57
|
+
"runWhen": { "data.taskState.core.lastOutcome.type": "FIX_COMPLETED" } },
|
|
66
58
|
|
|
67
59
|
{ "script": "finishFlow",
|
|
68
60
|
"with": { "reason": "review-failed", "label": "kody:failed", "color": "e11d21", "description": "kody: flow failed" },
|
|
69
61
|
"runWhen": { "data.taskState.core.lastOutcome.type": "REVIEW_FAILED" } },
|
|
70
62
|
|
|
71
|
-
{ "script": "finishFlow",
|
|
72
|
-
"with": { "reason": "fix-applied", "label": "kody:done", "color": "0e8a16", "description": "kody: PR ready for human review/merge" },
|
|
73
|
-
"runWhen": { "data.taskState.core.lastOutcome.type": "FIX_COMPLETED" } },
|
|
74
|
-
|
|
75
63
|
{ "script": "finishFlow",
|
|
76
64
|
"with": { "reason": "aborted", "label": "kody:failed", "color": "e11d21", "description": "kody: flow failed" },
|
|
77
65
|
"runWhen": { "data.taskState.core.lastOutcome.type": ["PLAN_FAILED", "RUN_FAILED", "FIX_FAILED", "AGENT_NOT_RUN"] } },
|
|
@@ -79,9 +67,53 @@
|
|
|
79
67
|
{ "script": "persistFlowState" }
|
|
80
68
|
]
|
|
81
69
|
},
|
|
70
|
+
"children": [
|
|
71
|
+
{
|
|
72
|
+
"exec": "reproduce",
|
|
73
|
+
"target": "issue",
|
|
74
|
+
"next": {
|
|
75
|
+
"REPRODUCE_COMPLETED": "plan",
|
|
76
|
+
"REPRODUCE_FAILED": "plan",
|
|
77
|
+
"*": "abort"
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"exec": "plan",
|
|
82
|
+
"target": "issue",
|
|
83
|
+
"next": {
|
|
84
|
+
"PLAN_COMPLETED": "run",
|
|
85
|
+
"*": "abort"
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"exec": "run",
|
|
90
|
+
"target": "issue",
|
|
91
|
+
"next": {
|
|
92
|
+
"RUN_COMPLETED": "review",
|
|
93
|
+
"*": "abort"
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"exec": "review",
|
|
98
|
+
"target": "pr",
|
|
99
|
+
"next": {
|
|
100
|
+
"REVIEW_PASS": "done",
|
|
101
|
+
"REVIEW_CONCERNS": "fix",
|
|
102
|
+
"REVIEW_FAIL": "fix",
|
|
103
|
+
"*": "abort"
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"exec": "fix",
|
|
108
|
+
"target": "pr",
|
|
109
|
+
"next": {
|
|
110
|
+
"FIX_COMPLETED": "done",
|
|
111
|
+
"*": "abort"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
],
|
|
82
115
|
"output": {
|
|
83
116
|
"actionTypes": [
|
|
84
|
-
"FLOW_STARTED",
|
|
85
117
|
"FLOW_COMPLETED",
|
|
86
118
|
"FLOW_ABORTED"
|
|
87
119
|
]
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
profile.json's postflight entries.
|
|
2
|
+
Container role: no agent runs. The transition logic lives entirely in
|
|
3
|
+
profile.json's `children[].next` map (driven by the container loop in
|
|
4
|
+
src/executor.ts:runContainerLoop). This file exists only because the
|
|
5
|
+
profile loader expects a prompt.md sibling.
|
|
7
6
|
-->
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "job-scheduler",
|
|
3
3
|
"role": "watch",
|
|
4
|
-
"describe": "Scheduled: for every
|
|
4
|
+
"describe": "Scheduled: for every job file under .kody/jobs/, invoke job-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": "dispatchJobFileTicks",
|
|
40
40
|
"with": {
|
|
41
|
-
"
|
|
42
|
-
"targetExecutable": "
|
|
43
|
-
"slugArg": "
|
|
41
|
+
"jobsDir": ".kody/jobs",
|
|
42
|
+
"targetExecutable": "job-tick",
|
|
43
|
+
"slugArg": "job"
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
],
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "job-tick",
|
|
3
3
|
"role": "primitive",
|
|
4
|
-
"describe": "One classifier tick for one
|
|
4
|
+
"describe": "One classifier tick for one job 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": "job",
|
|
9
|
+
"flag": "--job",
|
|
10
10
|
"type": "string",
|
|
11
11
|
"required": true,
|
|
12
|
-
"describe": "
|
|
12
|
+
"describe": "Job slug — basename (without .md) of the file under .kody/jobs/."
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"claudeCode": {
|
|
@@ -43,10 +43,10 @@
|
|
|
43
43
|
"scripts": {
|
|
44
44
|
"preflight": [
|
|
45
45
|
{
|
|
46
|
-
"script": "
|
|
46
|
+
"script": "loadJobFromFile",
|
|
47
47
|
"with": {
|
|
48
|
-
"
|
|
49
|
-
"slugArg": "
|
|
48
|
+
"jobsDir": ".kody/jobs",
|
|
49
|
+
"slugArg": "job"
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
{
|
|
@@ -55,13 +55,13 @@
|
|
|
55
55
|
],
|
|
56
56
|
"postflight": [
|
|
57
57
|
{
|
|
58
|
-
"script": "
|
|
58
|
+
"script": "parseJobStateFromAgentResult",
|
|
59
59
|
"with": {
|
|
60
|
-
"fenceLabel": "kody-
|
|
60
|
+
"fenceLabel": "kody-job-next-state"
|
|
61
61
|
}
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
|
-
"script": "
|
|
64
|
+
"script": "writeJobStateFile"
|
|
65
65
|
}
|
|
66
66
|
]
|
|
67
67
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
You are **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
|
+
## The job
|
|
4
|
+
|
|
5
|
+
Slug **`{{jobSlug}}`** — *{{jobTitle}}*. The job body below is authoritative: it states what success looks like, allowed commands, and restrictions. The job file is human-edited — re-read it every tick.
|
|
6
|
+
|
|
7
|
+
### Job body
|
|
8
|
+
|
|
9
|
+
{{jobIntent}}
|
|
10
|
+
|
|
11
|
+
## Current state
|
|
12
|
+
|
|
13
|
+
This is the state you wrote at the end of the previous tick (or `null` if this is the first tick):
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{{jobStateJson}}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`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`.
|
|
20
|
+
|
|
21
|
+
## What to do on this tick
|
|
22
|
+
|
|
23
|
+
1. **Check `done`.** If the prior state has `done: true`, emit the same state back unchanged and exit without any action.
|
|
24
|
+
2. **Re-read the job body.** It may have changed since the last tick.
|
|
25
|
+
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`.
|
|
26
|
+
4. **Optionally post a short narration** wherever the job tells you to (typically a PR comment alongside the action). Keep it terse.
|
|
27
|
+
5. **Emit the new state** at the very end of your response using the fenced block below. Do not include `version` or `rev` — the postflight script manages those.
|
|
28
|
+
|
|
29
|
+
## Output contract (MANDATORY, exactly once, at the end)
|
|
30
|
+
|
|
31
|
+
End your response with a single fenced block using the `kody-job-next-state` language tag:
|
|
32
|
+
|
|
33
|
+
````
|
|
34
|
+
```kody-job-next-state
|
|
35
|
+
{
|
|
36
|
+
"cursor": "<your-next-cursor>",
|
|
37
|
+
"data": { ... },
|
|
38
|
+
"done": <true|false>
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
````
|
|
42
|
+
|
|
43
|
+
If you fail to emit this block, or the JSON is invalid, the tick fails and the gist state is NOT updated. On the next wake you'll see the same prior state and can retry.
|
|
44
|
+
|
|
45
|
+
## Rules
|
|
46
|
+
|
|
47
|
+
- Never edit, create, or delete files in the working tree.
|
|
48
|
+
- Never commit or push.
|
|
49
|
+
- Only shell calls allowed: `gh`. Everything must go through it.
|
|
50
|
+
- Keep each tick focused: do one action per candidate per wake. The cron will call you again.
|
|
51
|
+
- If state says you're waiting on something, just check and re-emit — don't spawn a duplicate.
|
|
52
|
+
- Honour the job body's `## Restrictions` over any inferred shortcut.
|
package/kody.config.schema.json
CHANGED
|
@@ -383,14 +383,14 @@
|
|
|
383
383
|
},
|
|
384
384
|
"additionalProperties": false
|
|
385
385
|
},
|
|
386
|
-
"
|
|
386
|
+
"jobs": {
|
|
387
387
|
"type": "object",
|
|
388
|
-
"description": "File-based
|
|
388
|
+
"description": "File-based job configuration. Jobs are long-running scheduled executables defined under .kody/jobs/<slug>.md.",
|
|
389
389
|
"properties": {
|
|
390
390
|
"stateBackend": {
|
|
391
391
|
"type": "string",
|
|
392
392
|
"enum": ["contents-api", "local-file"],
|
|
393
|
-
"description": "Storage backend for
|
|
393
|
+
"description": "Storage backend for job state. \"contents-api\" (default) commits state to a tracked file via the GitHub Contents API — durable across runs but creates a commit per change. \"local-file\" stores state on disk and snapshots it to the GitHub Actions cache between workflow runs — no commit churn, but bound to cache eviction (7-day idle).",
|
|
394
394
|
"default": "contents-api"
|
|
395
395
|
}
|
|
396
396
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kody-ade/kody-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|
package/templates/kody.yml
CHANGED
|
@@ -49,12 +49,12 @@ on:
|
|
|
49
49
|
types: [closed]
|
|
50
50
|
schedule:
|
|
51
51
|
# Wakes every 30 minutes; kody fans out to whichever scheduled executables
|
|
52
|
-
# (
|
|
52
|
+
# (job-scheduler, memorize, watch-stale-prs, …) match this tick.
|
|
53
53
|
#
|
|
54
54
|
# `memorize` writes to `.kody/vault/` and opens a daily PR. If your
|
|
55
55
|
# `.gitignore` ignores `.kody/*`, add `!.kody/vault/` and `!.kody/vault/**`
|
|
56
56
|
# so memorize's pages are tracked.
|
|
57
|
-
- cron: "*/
|
|
57
|
+
- cron: "*/15 * * * *"
|
|
58
58
|
|
|
59
59
|
jobs:
|
|
60
60
|
run:
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "bug-container",
|
|
3
|
-
"role": "container",
|
|
4
|
-
"describe": "Container variant of `bug` — same plan → run → review (→ fix) flow, but children run sequentially in one process instead of as N comment-dispatched GHA runs. Strictly additive; the comment-driven `bug` orchestrator remains the default until classify is flipped.",
|
|
5
|
-
"inputs": [
|
|
6
|
-
{
|
|
7
|
-
"name": "issue",
|
|
8
|
-
"flag": "--issue",
|
|
9
|
-
"type": "int",
|
|
10
|
-
"required": true,
|
|
11
|
-
"describe": "GitHub issue number to drive the flow on."
|
|
12
|
-
}
|
|
13
|
-
],
|
|
14
|
-
"claudeCode": {
|
|
15
|
-
"model": "inherit",
|
|
16
|
-
"permissionMode": "default",
|
|
17
|
-
"maxTurns": 0,
|
|
18
|
-
"maxThinkingTokens": null,
|
|
19
|
-
"systemPromptAppend": null,
|
|
20
|
-
"tools": [],
|
|
21
|
-
"hooks": [],
|
|
22
|
-
"skills": [],
|
|
23
|
-
"commands": [],
|
|
24
|
-
"subagents": [],
|
|
25
|
-
"plugins": [],
|
|
26
|
-
"mcpServers": []
|
|
27
|
-
},
|
|
28
|
-
"cliTools": [],
|
|
29
|
-
"scripts": {
|
|
30
|
-
"preflight": [
|
|
31
|
-
{
|
|
32
|
-
"script": "setLifecycleLabel",
|
|
33
|
-
"with": {
|
|
34
|
-
"label": "kody-flow:bug",
|
|
35
|
-
"color": "d73a4a",
|
|
36
|
-
"description": "kody flow: bug / enhancement"
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
"script": "setLifecycleLabel",
|
|
41
|
-
"with": {
|
|
42
|
-
"label": "kody:orchestrating",
|
|
43
|
-
"color": "1d76db",
|
|
44
|
-
"description": "kody: orchestrating a multi-stage flow"
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
{ "script": "loadIssueContext" },
|
|
48
|
-
{ "script": "loadTaskState" }
|
|
49
|
-
],
|
|
50
|
-
"postflight": [
|
|
51
|
-
{ "script": "finishFlow",
|
|
52
|
-
"with": { "reason": "review-passed", "label": "kody:done", "color": "0e8a16", "description": "kody: PR ready for human review/merge" },
|
|
53
|
-
"runWhen": { "data.taskState.core.lastOutcome.type": "REVIEW_PASS" } },
|
|
54
|
-
|
|
55
|
-
{ "script": "finishFlow",
|
|
56
|
-
"with": { "reason": "fix-applied", "label": "kody:done", "color": "0e8a16", "description": "kody: PR ready for human review/merge" },
|
|
57
|
-
"runWhen": { "data.taskState.core.lastOutcome.type": "FIX_COMPLETED" } },
|
|
58
|
-
|
|
59
|
-
{ "script": "finishFlow",
|
|
60
|
-
"with": { "reason": "review-failed", "label": "kody:failed", "color": "e11d21", "description": "kody: flow failed" },
|
|
61
|
-
"runWhen": { "data.taskState.core.lastOutcome.type": "REVIEW_FAILED" } },
|
|
62
|
-
|
|
63
|
-
{ "script": "finishFlow",
|
|
64
|
-
"with": { "reason": "aborted", "label": "kody:failed", "color": "e11d21", "description": "kody: flow failed" },
|
|
65
|
-
"runWhen": { "data.taskState.core.lastOutcome.type": ["PLAN_FAILED", "RUN_FAILED", "FIX_FAILED", "AGENT_NOT_RUN"] } },
|
|
66
|
-
|
|
67
|
-
{ "script": "persistFlowState" }
|
|
68
|
-
]
|
|
69
|
-
},
|
|
70
|
-
"children": [
|
|
71
|
-
{
|
|
72
|
-
"exec": "reproduce",
|
|
73
|
-
"target": "issue",
|
|
74
|
-
"next": {
|
|
75
|
-
"REPRODUCE_COMPLETED": "plan",
|
|
76
|
-
"REPRODUCE_FAILED": "plan",
|
|
77
|
-
"*": "abort"
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
"exec": "plan",
|
|
82
|
-
"target": "issue",
|
|
83
|
-
"next": {
|
|
84
|
-
"PLAN_COMPLETED": "run",
|
|
85
|
-
"*": "abort"
|
|
86
|
-
}
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
"exec": "run",
|
|
90
|
-
"target": "issue",
|
|
91
|
-
"next": {
|
|
92
|
-
"RUN_COMPLETED": "review",
|
|
93
|
-
"*": "abort"
|
|
94
|
-
}
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
"exec": "review",
|
|
98
|
-
"target": "pr",
|
|
99
|
-
"next": {
|
|
100
|
-
"REVIEW_PASS": "done",
|
|
101
|
-
"REVIEW_CONCERNS": "fix",
|
|
102
|
-
"REVIEW_FAIL": "fix",
|
|
103
|
-
"*": "abort"
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
"exec": "fix",
|
|
108
|
-
"target": "pr",
|
|
109
|
-
"next": {
|
|
110
|
-
"FIX_COMPLETED": "done",
|
|
111
|
-
"*": "abort"
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
],
|
|
115
|
-
"output": {
|
|
116
|
-
"actionTypes": [
|
|
117
|
-
"FLOW_COMPLETED",
|
|
118
|
-
"FLOW_ABORTED"
|
|
119
|
-
]
|
|
120
|
-
}
|
|
121
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
Container role: no agent runs. The transition logic lives entirely in
|
|
3
|
-
profile.json's `children[].next` map (driven by the container loop in
|
|
4
|
-
src/executor.ts:runContainerLoop). This file exists only because the
|
|
5
|
-
profile loader expects a prompt.md sibling.
|
|
6
|
-
-->
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
You are **kody mission-tick**, the coordinator for one file-based mission. 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
|
-
## The mission
|
|
4
|
-
|
|
5
|
-
Slug **`{{missionSlug}}`** — *{{missionTitle}}*. The mission body below is authoritative: it states what success looks like, allowed commands, and restrictions. The mission file is human-edited — re-read it every tick.
|
|
6
|
-
|
|
7
|
-
### Mission body
|
|
8
|
-
|
|
9
|
-
{{missionIntent}}
|
|
10
|
-
|
|
11
|
-
## Current state
|
|
12
|
-
|
|
13
|
-
This is the state you wrote at the end of the previous tick (or `null` if this is the first tick):
|
|
14
|
-
|
|
15
|
-
```json
|
|
16
|
-
{{missionStateJson}}
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
`cursor` is *your* enum — pick whatever labels map cleanly to your mission'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 mission is permanently over — for evergreen missions this should always remain `false`.
|
|
20
|
-
|
|
21
|
-
## What to do on this tick
|
|
22
|
-
|
|
23
|
-
1. **Check `done`.** If the prior state has `done: true`, emit the same state back unchanged and exit without any action.
|
|
24
|
-
2. **Re-read the mission body.** It may have changed since the last tick.
|
|
25
|
-
3. **Execute exactly the work the body's `## Mission` section describes**, subject to its `## Allowed Commands` and `## Restrictions`. Use the `## State` section to interpret and update `data`.
|
|
26
|
-
4. **Optionally post a short narration** wherever the mission tells you to (typically a PR comment alongside the action). Keep it terse.
|
|
27
|
-
5. **Emit the new state** at the very end of your response using the fenced block below. Do not include `version` or `rev` — the postflight script manages those.
|
|
28
|
-
|
|
29
|
-
## Output contract (MANDATORY, exactly once, at the end)
|
|
30
|
-
|
|
31
|
-
End your response with a single fenced block using the `kody-mission-next-state` language tag:
|
|
32
|
-
|
|
33
|
-
````
|
|
34
|
-
```kody-mission-next-state
|
|
35
|
-
{
|
|
36
|
-
"cursor": "<your-next-cursor>",
|
|
37
|
-
"data": { ... },
|
|
38
|
-
"done": <true|false>
|
|
39
|
-
}
|
|
40
|
-
```
|
|
41
|
-
````
|
|
42
|
-
|
|
43
|
-
If you fail to emit this block, or the JSON is invalid, the tick fails and the gist state is NOT updated. On the next wake you'll see the same prior state and can retry.
|
|
44
|
-
|
|
45
|
-
## Rules
|
|
46
|
-
|
|
47
|
-
- Never edit, create, or delete files in the working tree.
|
|
48
|
-
- Never commit or push.
|
|
49
|
-
- Only shell calls allowed: `gh`. Everything must go through it.
|
|
50
|
-
- Keep each tick focused: do one action per candidate per wake. The cron will call you again.
|
|
51
|
-
- If state says you're waiting on something, just check and re-emit — don't spawn a duplicate.
|
|
52
|
-
- Honour the mission body's `## Restrictions` over any inferred shortcut.
|