@kody-ade/kody-engine 0.3.87 → 0.4.1
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 +93 -93
- package/dist/executables/bug/profile.json +51 -19
- package/dist/executables/bug/prompt.md +4 -5
- package/dist/executables/goal-scheduler/scheduler.sh +0 -0
- package/dist/executables/goal-tick/tick.sh +0 -0
- 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/dist/executables/release-deploy/deploy.sh +0 -0
- package/dist/executables/release-prepare/prepare.sh +0 -0
- package/dist/executables/release-publish/publish.sh +0 -0
- package/dist/executables/resolve/apply-prefer.sh +0 -0
- package/dist/executables/revert/revert.sh +0 -0
- package/kody.config.schema.json +3 -3
- package/package.json +14 -15
- package/templates/kody.yml +1 -1
- 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.1",
|
|
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;
|
|
@@ -3188,7 +3188,7 @@ function failedAction(reason) {
|
|
|
3188
3188
|
return { type: "CLASSIFY_FAILED", payload: { reason }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3189
3189
|
}
|
|
3190
3190
|
|
|
3191
|
-
// src/scripts/
|
|
3191
|
+
// src/scripts/dispatchJobFileTicks.ts
|
|
3192
3192
|
import * as fs18 from "fs";
|
|
3193
3193
|
import * as path17 from "path";
|
|
3194
3194
|
|
|
@@ -3416,26 +3416,26 @@ function minimizeComment(nodeId, cwd) {
|
|
|
3416
3416
|
gh2(["api", "graphql", "-f", `query=${mutation}`, "-f", `id=${nodeId}`], { cwd });
|
|
3417
3417
|
}
|
|
3418
3418
|
|
|
3419
|
-
// src/scripts/
|
|
3419
|
+
// src/scripts/jobState/backend.ts
|
|
3420
3420
|
function isStateUnchanged(prev, next) {
|
|
3421
3421
|
if (prev.cursor !== next.cursor) return false;
|
|
3422
3422
|
if (prev.done !== next.done) return false;
|
|
3423
3423
|
return JSON.stringify(prev.data) === JSON.stringify(next.data);
|
|
3424
3424
|
}
|
|
3425
|
-
function stateFilePath(
|
|
3426
|
-
return `${
|
|
3425
|
+
function stateFilePath(jobsDir, slug) {
|
|
3426
|
+
return `${jobsDir.replace(/\/+$/, "")}/${slug}.state.json`;
|
|
3427
3427
|
}
|
|
3428
3428
|
function slugFromStateFilePath(filePath) {
|
|
3429
3429
|
const last = filePath.split("/").pop() ?? filePath;
|
|
3430
3430
|
return last.replace(/\.state\.json$/i, "");
|
|
3431
3431
|
}
|
|
3432
3432
|
|
|
3433
|
-
// src/scripts/
|
|
3433
|
+
// src/scripts/jobState/contentsApiBackend.ts
|
|
3434
3434
|
var ContentsApiBackend = class {
|
|
3435
3435
|
name = "contents-api";
|
|
3436
3436
|
owner;
|
|
3437
3437
|
repo;
|
|
3438
|
-
|
|
3438
|
+
jobsDir;
|
|
3439
3439
|
cwd;
|
|
3440
3440
|
constructor(opts) {
|
|
3441
3441
|
if (!opts.owner || !opts.repo) {
|
|
@@ -3443,11 +3443,11 @@ var ContentsApiBackend = class {
|
|
|
3443
3443
|
}
|
|
3444
3444
|
this.owner = opts.owner;
|
|
3445
3445
|
this.repo = opts.repo;
|
|
3446
|
-
this.
|
|
3446
|
+
this.jobsDir = opts.jobsDir;
|
|
3447
3447
|
this.cwd = opts.cwd;
|
|
3448
3448
|
}
|
|
3449
3449
|
load(slug) {
|
|
3450
|
-
const filePath = stateFilePath(this.
|
|
3450
|
+
const filePath = stateFilePath(this.jobsDir, slug);
|
|
3451
3451
|
let raw = "";
|
|
3452
3452
|
try {
|
|
3453
3453
|
raw = gh2(["api", `/repos/${this.owner}/${this.repo}/contents/${filePath}`], { cwd: this.cwd });
|
|
@@ -3490,7 +3490,7 @@ var ContentsApiBackend = class {
|
|
|
3490
3490
|
const slug = slugFromStateFilePath(loaded.path);
|
|
3491
3491
|
const body = JSON.stringify(next, null, 2) + "\n";
|
|
3492
3492
|
const payload = {
|
|
3493
|
-
message: `chore(
|
|
3493
|
+
message: `chore(jobs): update state for ${slug} (rev ${next.rev})`,
|
|
3494
3494
|
content: Buffer.from(body, "utf-8").toString("base64")
|
|
3495
3495
|
};
|
|
3496
3496
|
if (typeof loaded.handle === "string") payload.sha = loaded.handle;
|
|
@@ -3502,35 +3502,35 @@ var ContentsApiBackend = class {
|
|
|
3502
3502
|
}
|
|
3503
3503
|
};
|
|
3504
3504
|
|
|
3505
|
-
// src/scripts/
|
|
3505
|
+
// src/scripts/jobState/localFileBackend.ts
|
|
3506
3506
|
import * as fs17 from "fs";
|
|
3507
3507
|
import * as path16 from "path";
|
|
3508
3508
|
var LocalFileBackend = class {
|
|
3509
3509
|
name = "local-file";
|
|
3510
3510
|
cwd;
|
|
3511
|
-
|
|
3511
|
+
jobsDir;
|
|
3512
3512
|
absDir;
|
|
3513
3513
|
owner;
|
|
3514
3514
|
repo;
|
|
3515
3515
|
cache;
|
|
3516
3516
|
constructor(opts) {
|
|
3517
3517
|
if (!opts.cwd) throw new Error("LocalFileBackend: cwd is required");
|
|
3518
|
-
if (!opts.
|
|
3518
|
+
if (!opts.jobsDir) throw new Error("LocalFileBackend: jobsDir is required");
|
|
3519
3519
|
if (!opts.owner || !opts.repo) throw new Error("LocalFileBackend: owner and repo are required");
|
|
3520
3520
|
this.cwd = opts.cwd;
|
|
3521
|
-
this.
|
|
3522
|
-
this.absDir = path16.join(opts.cwd, opts.
|
|
3521
|
+
this.jobsDir = opts.jobsDir;
|
|
3522
|
+
this.absDir = path16.join(opts.cwd, opts.jobsDir);
|
|
3523
3523
|
this.owner = opts.owner;
|
|
3524
3524
|
this.repo = opts.repo;
|
|
3525
3525
|
this.cache = opts.cache ?? defaultCacheAdapter();
|
|
3526
3526
|
}
|
|
3527
3527
|
/**
|
|
3528
|
-
* Restore the
|
|
3528
|
+
* Restore the job directory from the most recent Actions cache entry
|
|
3529
3529
|
* for this repo. No-op when not running in Actions or when no cache exists.
|
|
3530
3530
|
*/
|
|
3531
3531
|
async hydrate() {
|
|
3532
3532
|
if (!this.cache.isAvailable()) {
|
|
3533
|
-
process.stdout.write(`[
|
|
3533
|
+
process.stdout.write(`[jobs/state] hydrate skipped: actions cache unavailable
|
|
3534
3534
|
`);
|
|
3535
3535
|
return;
|
|
3536
3536
|
}
|
|
@@ -3540,26 +3540,26 @@ var LocalFileBackend = class {
|
|
|
3540
3540
|
try {
|
|
3541
3541
|
const matched = await this.cache.restore([this.absDir], probeKey, [prefix]);
|
|
3542
3542
|
if (matched) {
|
|
3543
|
-
process.stdout.write(`[
|
|
3543
|
+
process.stdout.write(`[jobs/state] hydrate hit: ${matched}
|
|
3544
3544
|
`);
|
|
3545
3545
|
} else {
|
|
3546
|
-
process.stdout.write(`[
|
|
3546
|
+
process.stdout.write(`[jobs/state] hydrate miss (cold start)
|
|
3547
3547
|
`);
|
|
3548
3548
|
}
|
|
3549
3549
|
} catch (err) {
|
|
3550
3550
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3551
|
-
process.stderr.write(`[
|
|
3551
|
+
process.stderr.write(`[jobs/state] hydrate failed (continuing): ${msg}
|
|
3552
3552
|
`);
|
|
3553
3553
|
}
|
|
3554
3554
|
}
|
|
3555
3555
|
/**
|
|
3556
|
-
* Save the
|
|
3556
|
+
* Save the job directory to the Actions cache under a unique key.
|
|
3557
3557
|
* No-op when not running in Actions. Errors are logged, never thrown —
|
|
3558
3558
|
* callers run this in a finally block and must not swallow real errors.
|
|
3559
3559
|
*/
|
|
3560
3560
|
async persist() {
|
|
3561
3561
|
if (!this.cache.isAvailable()) {
|
|
3562
|
-
process.stdout.write(`[
|
|
3562
|
+
process.stdout.write(`[jobs/state] persist skipped: actions cache unavailable
|
|
3563
3563
|
`);
|
|
3564
3564
|
return;
|
|
3565
3565
|
}
|
|
@@ -3569,16 +3569,16 @@ var LocalFileBackend = class {
|
|
|
3569
3569
|
const key = `${this.cacheKeyPrefix()}${process.env.GITHUB_RUN_ID ?? "norunid"}-${Date.now()}`;
|
|
3570
3570
|
try {
|
|
3571
3571
|
await this.cache.save([this.absDir], key);
|
|
3572
|
-
process.stdout.write(`[
|
|
3572
|
+
process.stdout.write(`[jobs/state] persist saved: ${key}
|
|
3573
3573
|
`);
|
|
3574
3574
|
} catch (err) {
|
|
3575
3575
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3576
|
-
process.stderr.write(`[
|
|
3576
|
+
process.stderr.write(`[jobs/state] persist failed (continuing): ${msg}
|
|
3577
3577
|
`);
|
|
3578
3578
|
}
|
|
3579
3579
|
}
|
|
3580
3580
|
load(slug) {
|
|
3581
|
-
const relPath = stateFilePath(this.
|
|
3581
|
+
const relPath = stateFilePath(this.jobsDir, slug);
|
|
3582
3582
|
const absPath = path16.join(this.cwd, relPath);
|
|
3583
3583
|
if (!fs17.existsSync(absPath)) {
|
|
3584
3584
|
return { path: relPath, handle: null, state: initialStateEnvelope("seed"), created: true };
|
|
@@ -3607,7 +3607,7 @@ var LocalFileBackend = class {
|
|
|
3607
3607
|
return true;
|
|
3608
3608
|
}
|
|
3609
3609
|
cacheKeyPrefix() {
|
|
3610
|
-
return `kody-
|
|
3610
|
+
return `kody-job-state-${sanitizeKey(this.owner)}-${sanitizeKey(this.repo)}-`;
|
|
3611
3611
|
}
|
|
3612
3612
|
};
|
|
3613
3613
|
function sanitizeKey(s) {
|
|
@@ -3646,19 +3646,19 @@ function defaultCacheAdapter() {
|
|
|
3646
3646
|
};
|
|
3647
3647
|
}
|
|
3648
3648
|
|
|
3649
|
-
// src/scripts/
|
|
3649
|
+
// src/scripts/jobState/index.ts
|
|
3650
3650
|
function resolveBackend(opts) {
|
|
3651
3651
|
const owner = opts.config.github?.owner;
|
|
3652
3652
|
const repo = opts.config.github?.repo;
|
|
3653
3653
|
if (!owner || !repo) {
|
|
3654
3654
|
throw new Error("resolveBackend: config.github.owner and config.github.repo must be set");
|
|
3655
3655
|
}
|
|
3656
|
-
const requested = opts.config.
|
|
3656
|
+
const requested = opts.config.jobs?.stateBackend ?? "contents-api";
|
|
3657
3657
|
switch (requested) {
|
|
3658
3658
|
case "contents-api":
|
|
3659
|
-
return new ContentsApiBackend({ owner, repo,
|
|
3659
|
+
return new ContentsApiBackend({ owner, repo, jobsDir: opts.jobsDir, cwd: opts.cwd });
|
|
3660
3660
|
case "local-file":
|
|
3661
|
-
return new LocalFileBackend({ cwd: opts.cwd,
|
|
3661
|
+
return new LocalFileBackend({ cwd: opts.cwd, jobsDir: opts.jobsDir, owner, repo });
|
|
3662
3662
|
default: {
|
|
3663
3663
|
const _exhaustive = requested;
|
|
3664
3664
|
throw new Error(`resolveBackend: unknown stateBackend "${String(_exhaustive)}"`);
|
|
@@ -3666,32 +3666,32 @@ function resolveBackend(opts) {
|
|
|
3666
3666
|
}
|
|
3667
3667
|
}
|
|
3668
3668
|
|
|
3669
|
-
// src/scripts/
|
|
3670
|
-
var
|
|
3669
|
+
// src/scripts/dispatchJobFileTicks.ts
|
|
3670
|
+
var dispatchJobFileTicks = async (ctx, _profile, args) => {
|
|
3671
3671
|
ctx.skipAgent = true;
|
|
3672
3672
|
const targetExecutable = String(args?.targetExecutable ?? "");
|
|
3673
3673
|
if (!targetExecutable) {
|
|
3674
|
-
throw new Error("
|
|
3674
|
+
throw new Error("dispatchJobFileTicks: `with.targetExecutable` is required");
|
|
3675
3675
|
}
|
|
3676
|
-
const
|
|
3677
|
-
const slugArg = String(args?.slugArg ?? "
|
|
3678
|
-
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 });
|
|
3679
3679
|
if (backend.hydrate) {
|
|
3680
3680
|
await backend.hydrate();
|
|
3681
3681
|
}
|
|
3682
3682
|
try {
|
|
3683
|
-
const slugs =
|
|
3684
|
-
ctx.data.
|
|
3683
|
+
const slugs = listJobSlugs(path17.join(ctx.cwd, jobsDir));
|
|
3684
|
+
ctx.data.jobSlugCount = slugs.length;
|
|
3685
3685
|
if (slugs.length === 0) {
|
|
3686
|
-
process.stdout.write(`[
|
|
3686
|
+
process.stdout.write(`[jobs] no job files in ${jobsDir}
|
|
3687
3687
|
`);
|
|
3688
3688
|
return;
|
|
3689
3689
|
}
|
|
3690
|
-
process.stdout.write(`[
|
|
3690
|
+
process.stdout.write(`[jobs] ticking ${slugs.length} job(s) via ${targetExecutable}
|
|
3691
3691
|
`);
|
|
3692
3692
|
const results = [];
|
|
3693
3693
|
for (const slug of slugs) {
|
|
3694
|
-
process.stdout.write(`[
|
|
3694
|
+
process.stdout.write(`[jobs] \u2192 tick ${slug}
|
|
3695
3695
|
`);
|
|
3696
3696
|
try {
|
|
3697
3697
|
const out = await runExecutable(targetExecutable, {
|
|
@@ -3703,17 +3703,17 @@ var dispatchMissionFileTicks = async (ctx, _profile, args) => {
|
|
|
3703
3703
|
});
|
|
3704
3704
|
results.push({ slug, exitCode: out.exitCode, reason: out.reason });
|
|
3705
3705
|
if (out.exitCode !== 0) {
|
|
3706
|
-
process.stderr.write(`[
|
|
3706
|
+
process.stderr.write(`[jobs] tick ${slug} failed (exit ${out.exitCode}): ${out.reason ?? ""}
|
|
3707
3707
|
`);
|
|
3708
3708
|
}
|
|
3709
3709
|
} catch (err) {
|
|
3710
3710
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3711
|
-
process.stderr.write(`[
|
|
3711
|
+
process.stderr.write(`[jobs] tick ${slug} crashed: ${msg}
|
|
3712
3712
|
`);
|
|
3713
3713
|
results.push({ slug, exitCode: 99, reason: msg });
|
|
3714
3714
|
}
|
|
3715
3715
|
}
|
|
3716
|
-
ctx.data.
|
|
3716
|
+
ctx.data.jobTickResults = results;
|
|
3717
3717
|
ctx.output.exitCode = 0;
|
|
3718
3718
|
} finally {
|
|
3719
3719
|
if (backend.persist) {
|
|
@@ -3721,13 +3721,13 @@ var dispatchMissionFileTicks = async (ctx, _profile, args) => {
|
|
|
3721
3721
|
await backend.persist();
|
|
3722
3722
|
} catch (err) {
|
|
3723
3723
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3724
|
-
process.stderr.write(`[
|
|
3724
|
+
process.stderr.write(`[jobs] backend persist failed: ${msg}
|
|
3725
3725
|
`);
|
|
3726
3726
|
}
|
|
3727
3727
|
}
|
|
3728
3728
|
}
|
|
3729
3729
|
};
|
|
3730
|
-
function
|
|
3730
|
+
function listJobSlugs(absDir) {
|
|
3731
3731
|
if (!fs18.existsSync(absDir)) return [];
|
|
3732
3732
|
let entries;
|
|
3733
3733
|
try {
|
|
@@ -3738,26 +3738,26 @@ function listMissionSlugs(absDir) {
|
|
|
3738
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();
|
|
3739
3739
|
}
|
|
3740
3740
|
|
|
3741
|
-
// src/scripts/
|
|
3742
|
-
var
|
|
3741
|
+
// src/scripts/dispatchJobTicks.ts
|
|
3742
|
+
var dispatchJobTicks = async (ctx, _profile, args) => {
|
|
3743
3743
|
ctx.skipAgent = true;
|
|
3744
3744
|
const label = String(args?.label ?? "");
|
|
3745
3745
|
const targetExecutable = String(args?.targetExecutable ?? "");
|
|
3746
|
-
if (!label) throw new Error("
|
|
3747
|
-
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");
|
|
3748
3748
|
const issueArg = String(args?.issueArg ?? "issue");
|
|
3749
3749
|
const issues = listIssuesByLabel(label, ctx.cwd);
|
|
3750
|
-
ctx.data.
|
|
3750
|
+
ctx.data.jobIssueCount = issues.length;
|
|
3751
3751
|
if (issues.length === 0) {
|
|
3752
|
-
process.stdout.write(`[
|
|
3752
|
+
process.stdout.write(`[jobs] no open issues with label "${label}"
|
|
3753
3753
|
`);
|
|
3754
3754
|
return;
|
|
3755
3755
|
}
|
|
3756
|
-
process.stdout.write(`[
|
|
3756
|
+
process.stdout.write(`[jobs] ticking ${issues.length} issue(s) via ${targetExecutable}
|
|
3757
3757
|
`);
|
|
3758
3758
|
const results = [];
|
|
3759
3759
|
for (const issue of issues) {
|
|
3760
|
-
process.stdout.write(`[
|
|
3760
|
+
process.stdout.write(`[jobs] \u2192 tick #${issue.number}: ${issue.title}
|
|
3761
3761
|
`);
|
|
3762
3762
|
try {
|
|
3763
3763
|
const out = await runExecutable(targetExecutable, {
|
|
@@ -3769,17 +3769,17 @@ var dispatchMissionTicks = async (ctx, _profile, args) => {
|
|
|
3769
3769
|
});
|
|
3770
3770
|
results.push({ issue: issue.number, exitCode: out.exitCode, reason: out.reason });
|
|
3771
3771
|
if (out.exitCode !== 0) {
|
|
3772
|
-
process.stderr.write(`[
|
|
3772
|
+
process.stderr.write(`[jobs] tick #${issue.number} failed (exit ${out.exitCode}): ${out.reason ?? ""}
|
|
3773
3773
|
`);
|
|
3774
3774
|
}
|
|
3775
3775
|
} catch (err) {
|
|
3776
3776
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3777
|
-
process.stderr.write(`[
|
|
3777
|
+
process.stderr.write(`[jobs] tick #${issue.number} crashed: ${msg}
|
|
3778
3778
|
`);
|
|
3779
3779
|
results.push({ issue: issue.number, exitCode: 99, reason: msg });
|
|
3780
3780
|
}
|
|
3781
3781
|
}
|
|
3782
|
-
ctx.data.
|
|
3782
|
+
ctx.data.jobTickResults = results;
|
|
3783
3783
|
ctx.output.exitCode = 0;
|
|
3784
3784
|
};
|
|
3785
3785
|
function listIssuesByLabel(label, cwd) {
|
|
@@ -4953,31 +4953,31 @@ var loadIssueStateComment = async (ctx, _profile, args) => {
|
|
|
4953
4953
|
ctx.data.issueStateJson = loaded ? JSON.stringify(loaded.state, null, 2) : "null";
|
|
4954
4954
|
};
|
|
4955
4955
|
|
|
4956
|
-
// src/scripts/
|
|
4956
|
+
// src/scripts/loadJobFromFile.ts
|
|
4957
4957
|
import * as fs22 from "fs";
|
|
4958
4958
|
import * as path20 from "path";
|
|
4959
|
-
var
|
|
4960
|
-
const
|
|
4961
|
-
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");
|
|
4962
4962
|
const slug = String(ctx.args[slugArg] ?? "").trim();
|
|
4963
4963
|
if (!slug) {
|
|
4964
|
-
throw new Error(`
|
|
4964
|
+
throw new Error(`loadJobFromFile: ctx.args.${slugArg} must be a non-empty slug`);
|
|
4965
4965
|
}
|
|
4966
|
-
const absPath = path20.join(ctx.cwd,
|
|
4966
|
+
const absPath = path20.join(ctx.cwd, jobsDir, `${slug}.md`);
|
|
4967
4967
|
if (!fs22.existsSync(absPath)) {
|
|
4968
|
-
throw new Error(`
|
|
4968
|
+
throw new Error(`loadJobFromFile: job file not found: ${absPath}`);
|
|
4969
4969
|
}
|
|
4970
4970
|
const raw = fs22.readFileSync(absPath, "utf-8");
|
|
4971
|
-
const { title, body } =
|
|
4972
|
-
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 });
|
|
4973
4973
|
const loaded = await backend.load(slug);
|
|
4974
|
-
ctx.data.
|
|
4975
|
-
ctx.data.
|
|
4976
|
-
ctx.data.
|
|
4977
|
-
ctx.data.
|
|
4978
|
-
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);
|
|
4979
4979
|
};
|
|
4980
|
-
function
|
|
4980
|
+
function parseJobFile(raw, slug) {
|
|
4981
4981
|
let stripped = raw;
|
|
4982
4982
|
if (stripped.startsWith("---\n")) {
|
|
4983
4983
|
const end = stripped.indexOf("\n---\n", 4);
|
|
@@ -5639,16 +5639,16 @@ function escapeRegex(s) {
|
|
|
5639
5639
|
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
5640
5640
|
}
|
|
5641
5641
|
|
|
5642
|
-
// src/scripts/
|
|
5642
|
+
// src/scripts/parseJobStateFromAgentResult.ts
|
|
5643
5643
|
function isPartialEnvelope2(x) {
|
|
5644
5644
|
if (x === null || typeof x !== "object") return false;
|
|
5645
5645
|
const o = x;
|
|
5646
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);
|
|
5647
5647
|
}
|
|
5648
|
-
var
|
|
5648
|
+
var parseJobStateFromAgentResult = async (ctx, _profile, agentResult, args) => {
|
|
5649
5649
|
const fenceLabel = String(args?.fenceLabel ?? "");
|
|
5650
5650
|
if (!fenceLabel) {
|
|
5651
|
-
throw new Error("
|
|
5651
|
+
throw new Error("parseJobStateFromAgentResult: `with.fenceLabel` is required");
|
|
5652
5652
|
}
|
|
5653
5653
|
if (!agentResult) {
|
|
5654
5654
|
ctx.data.nextStateParseError = "agent did not run";
|
|
@@ -5671,7 +5671,7 @@ var parseMissionStateFromAgentResult = async (ctx, _profile, agentResult, args)
|
|
|
5671
5671
|
ctx.data.nextStateParseError = "state must be an object with string `cursor`, object `data`, and boolean `done`";
|
|
5672
5672
|
return;
|
|
5673
5673
|
}
|
|
5674
|
-
const loaded = ctx.data.
|
|
5674
|
+
const loaded = ctx.data.jobState;
|
|
5675
5675
|
const prevRev = loaded?.state.rev ?? 0;
|
|
5676
5676
|
const next = {
|
|
5677
5677
|
version: 1,
|
|
@@ -5680,7 +5680,7 @@ var parseMissionStateFromAgentResult = async (ctx, _profile, agentResult, args)
|
|
|
5680
5680
|
data: parsed.data,
|
|
5681
5681
|
done: parsed.done
|
|
5682
5682
|
};
|
|
5683
|
-
ctx.data.
|
|
5683
|
+
ctx.data.nextJobState = next;
|
|
5684
5684
|
};
|
|
5685
5685
|
function escapeRegex2(s) {
|
|
5686
5686
|
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
@@ -7203,26 +7203,26 @@ var writeIssueStateComment = async (ctx, _profile, _agentResult, args) => {
|
|
|
7203
7203
|
}
|
|
7204
7204
|
};
|
|
7205
7205
|
|
|
7206
|
-
// src/scripts/
|
|
7207
|
-
var
|
|
7206
|
+
// src/scripts/writeJobStateFile.ts
|
|
7207
|
+
var writeJobStateFile = async (ctx, _profile, _agentResult, args) => {
|
|
7208
7208
|
const parseError = ctx.data.nextStateParseError;
|
|
7209
7209
|
if (parseError) {
|
|
7210
|
-
process.stderr.write(`[kody]
|
|
7210
|
+
process.stderr.write(`[kody] job state write skipped: ${parseError}
|
|
7211
7211
|
`);
|
|
7212
7212
|
if (ctx.output.exitCode === 0) ctx.output.exitCode = 1;
|
|
7213
7213
|
if (!ctx.output.reason) ctx.output.reason = `next-state parse failed: ${parseError}`;
|
|
7214
7214
|
return;
|
|
7215
7215
|
}
|
|
7216
|
-
const next = ctx.data.
|
|
7216
|
+
const next = ctx.data.nextJobState;
|
|
7217
7217
|
if (!next) {
|
|
7218
7218
|
return;
|
|
7219
7219
|
}
|
|
7220
|
-
const loaded = ctx.data.
|
|
7220
|
+
const loaded = ctx.data.jobState;
|
|
7221
7221
|
if (!loaded) {
|
|
7222
|
-
throw new Error("
|
|
7222
|
+
throw new Error("writeJobStateFile: ctx.data.jobState missing \u2014 preflight must run first");
|
|
7223
7223
|
}
|
|
7224
|
-
const
|
|
7225
|
-
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 });
|
|
7226
7226
|
await backend.save(loaded, next);
|
|
7227
7227
|
};
|
|
7228
7228
|
|
|
@@ -7271,7 +7271,7 @@ var preflightScripts = {
|
|
|
7271
7271
|
loadVaultContext,
|
|
7272
7272
|
loadIssueContext,
|
|
7273
7273
|
loadIssueStateComment,
|
|
7274
|
-
|
|
7274
|
+
loadJobFromFile,
|
|
7275
7275
|
loadConventions,
|
|
7276
7276
|
loadCoverageRules,
|
|
7277
7277
|
loadPriorArt,
|
|
@@ -7286,16 +7286,16 @@ var preflightScripts = {
|
|
|
7286
7286
|
skipAgent,
|
|
7287
7287
|
classifyByLabel,
|
|
7288
7288
|
diagMcp,
|
|
7289
|
-
|
|
7290
|
-
|
|
7289
|
+
dispatchJobTicks,
|
|
7290
|
+
dispatchJobFileTicks
|
|
7291
7291
|
};
|
|
7292
7292
|
var postflightScripts = {
|
|
7293
7293
|
parseAgentResult: parseAgentResult2,
|
|
7294
7294
|
parseIssueStateFromAgentResult,
|
|
7295
|
-
|
|
7295
|
+
parseJobStateFromAgentResult,
|
|
7296
7296
|
parseReproOutput,
|
|
7297
7297
|
writeIssueStateComment,
|
|
7298
|
-
|
|
7298
|
+
writeJobStateFile,
|
|
7299
7299
|
requireFeedbackActions,
|
|
7300
7300
|
requirePlanDeviations,
|
|
7301
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
|
-->
|
|
File without changes
|
|
File without changes
|
|
@@ -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.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
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.1",
|
|
4
4
|
"description": "kody — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -12,18 +12,6 @@
|
|
|
12
12
|
"templates",
|
|
13
13
|
"kody.config.schema.json"
|
|
14
14
|
],
|
|
15
|
-
"scripts": {
|
|
16
|
-
"kody": "tsx bin/kody.ts",
|
|
17
|
-
"build": "tsup && node scripts/copy-assets.cjs",
|
|
18
|
-
"test": "vitest run tests/unit tests/int --no-coverage",
|
|
19
|
-
"test:e2e": "vitest run tests/e2e --no-coverage",
|
|
20
|
-
"test:all": "vitest run tests --no-coverage",
|
|
21
|
-
"typecheck": "tsc --noEmit",
|
|
22
|
-
"lint": "biome check",
|
|
23
|
-
"lint:fix": "biome check --write",
|
|
24
|
-
"format": "biome format --write",
|
|
25
|
-
"prepublishOnly": "pnpm build"
|
|
26
|
-
},
|
|
27
15
|
"dependencies": {
|
|
28
16
|
"@actions/cache": "^6.0.0",
|
|
29
17
|
"@anthropic-ai/claude-agent-sdk": "0.2.119"
|
|
@@ -44,5 +32,16 @@
|
|
|
44
32
|
"url": "git+https://github.com/aharonyaircohen/kody-engine.git"
|
|
45
33
|
},
|
|
46
34
|
"homepage": "https://github.com/aharonyaircohen/kody-engine",
|
|
47
|
-
"bugs": "https://github.com/aharonyaircohen/kody-engine/issues"
|
|
48
|
-
|
|
35
|
+
"bugs": "https://github.com/aharonyaircohen/kody-engine/issues",
|
|
36
|
+
"scripts": {
|
|
37
|
+
"kody": "tsx bin/kody.ts",
|
|
38
|
+
"build": "tsup && node scripts/copy-assets.cjs",
|
|
39
|
+
"test": "vitest run tests/unit tests/int --no-coverage",
|
|
40
|
+
"test:e2e": "vitest run tests/e2e --no-coverage",
|
|
41
|
+
"test:all": "vitest run tests --no-coverage",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"lint": "biome check",
|
|
44
|
+
"lint:fix": "biome check --write",
|
|
45
|
+
"format": "biome format --write"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/templates/kody.yml
CHANGED
|
@@ -49,7 +49,7 @@ 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/**`
|
|
@@ -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.
|