@kody-ade/kody-engine 0.3.87 → 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 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, mission wake), and `pull_request: [closed]` (auto-finalizes a merged `release/vX.Y.Z` PR).
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
- # missions & watches (scheduled, coordinate work via issue state)
62
- kody mission-scheduler # fans out to per-issue mission-tick
63
- kody mission-tick --issue <N> # one tick of a kody:mission issue
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
- ### Missions
81
+ ### Jobs
82
82
 
83
- A **mission** is a stateful, bounded goal expressed as a labeled GitHub issue (`kody:mission`). A **watch** is a stateless repeating loop. A **manager** is a mission whose job happens to be overseeing other missions. All three run on the same scheduled-executable substrate.
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
- `mission-scheduler` wakes on cron (default `*/5 * * * *`) or empty `workflow_dispatch`, finds every open `kody:mission` issue, and calls `mission-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-mission-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).
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.3.87",
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
- missions: parseMissionsConfig(raw.missions)
191
+ jobs: parseJobsConfig(raw.jobs)
192
192
  };
193
193
  }
194
- function parseMissionsConfig(raw) {
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: missions.stateBackend must be "contents-api" or "local-file", got "${r.stateBackend}"`
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/dispatchMissionFileTicks.ts
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/missionState/backend.ts
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(missionsDir, slug) {
3426
- return `${missionsDir.replace(/\/+$/, "")}/${slug}.state.json`;
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/missionState/contentsApiBackend.ts
3433
+ // src/scripts/jobState/contentsApiBackend.ts
3434
3434
  var ContentsApiBackend = class {
3435
3435
  name = "contents-api";
3436
3436
  owner;
3437
3437
  repo;
3438
- missionsDir;
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.missionsDir = opts.missionsDir;
3446
+ this.jobsDir = opts.jobsDir;
3447
3447
  this.cwd = opts.cwd;
3448
3448
  }
3449
3449
  load(slug) {
3450
- const filePath = stateFilePath(this.missionsDir, slug);
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(missions): update state for ${slug} (rev ${next.rev})`,
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/missionState/localFileBackend.ts
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
- missionsDir;
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.missionsDir) throw new Error("LocalFileBackend: missionsDir is required");
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.missionsDir = opts.missionsDir;
3522
- this.absDir = path16.join(opts.cwd, opts.missionsDir);
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 mission directory from the most recent Actions cache entry
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(`[missions/state] hydrate skipped: actions cache unavailable
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(`[missions/state] hydrate hit: ${matched}
3543
+ process.stdout.write(`[jobs/state] hydrate hit: ${matched}
3544
3544
  `);
3545
3545
  } else {
3546
- process.stdout.write(`[missions/state] hydrate miss (cold start)
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(`[missions/state] hydrate failed (continuing): ${msg}
3551
+ process.stderr.write(`[jobs/state] hydrate failed (continuing): ${msg}
3552
3552
  `);
3553
3553
  }
3554
3554
  }
3555
3555
  /**
3556
- * Save the mission directory to the Actions cache under a unique key.
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(`[missions/state] persist skipped: actions cache unavailable
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(`[missions/state] persist saved: ${key}
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(`[missions/state] persist failed (continuing): ${msg}
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.missionsDir, slug);
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-mission-state-${sanitizeKey(this.owner)}-${sanitizeKey(this.repo)}-`;
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/missionState/index.ts
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.missions?.stateBackend ?? "contents-api";
3656
+ const requested = opts.config.jobs?.stateBackend ?? "contents-api";
3657
3657
  switch (requested) {
3658
3658
  case "contents-api":
3659
- return new ContentsApiBackend({ owner, repo, missionsDir: opts.missionsDir, cwd: opts.cwd });
3659
+ return new ContentsApiBackend({ owner, repo, jobsDir: opts.jobsDir, cwd: opts.cwd });
3660
3660
  case "local-file":
3661
- return new LocalFileBackend({ cwd: opts.cwd, missionsDir: opts.missionsDir, owner, repo });
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/dispatchMissionFileTicks.ts
3670
- var dispatchMissionFileTicks = async (ctx, _profile, args) => {
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("dispatchMissionFileTicks: `with.targetExecutable` is required");
3674
+ throw new Error("dispatchJobFileTicks: `with.targetExecutable` is required");
3675
3675
  }
3676
- const missionsDir = String(args?.missionsDir ?? ".kody/missions");
3677
- const slugArg = String(args?.slugArg ?? "mission");
3678
- const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, missionsDir });
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 = listMissionSlugs(path17.join(ctx.cwd, missionsDir));
3684
- ctx.data.missionSlugCount = slugs.length;
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(`[missions] no mission files in ${missionsDir}
3686
+ process.stdout.write(`[jobs] no job files in ${jobsDir}
3687
3687
  `);
3688
3688
  return;
3689
3689
  }
3690
- process.stdout.write(`[missions] ticking ${slugs.length} mission(s) via ${targetExecutable}
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(`[missions] \u2192 tick ${slug}
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(`[missions] tick ${slug} failed (exit ${out.exitCode}): ${out.reason ?? ""}
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(`[missions] tick ${slug} crashed: ${msg}
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.missionTickResults = results;
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(`[missions] backend persist failed: ${msg}
3724
+ process.stderr.write(`[jobs] backend persist failed: ${msg}
3725
3725
  `);
3726
3726
  }
3727
3727
  }
3728
3728
  }
3729
3729
  };
3730
- function listMissionSlugs(absDir) {
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/dispatchMissionTicks.ts
3742
- var dispatchMissionTicks = async (ctx, _profile, args) => {
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("dispatchMissionTicks: `with.label` is required");
3747
- if (!targetExecutable) throw new Error("dispatchMissionTicks: `with.targetExecutable` is required");
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.missionIssueCount = issues.length;
3750
+ ctx.data.jobIssueCount = issues.length;
3751
3751
  if (issues.length === 0) {
3752
- process.stdout.write(`[missions] no open issues with label "${label}"
3752
+ process.stdout.write(`[jobs] no open issues with label "${label}"
3753
3753
  `);
3754
3754
  return;
3755
3755
  }
3756
- process.stdout.write(`[missions] ticking ${issues.length} issue(s) via ${targetExecutable}
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(`[missions] \u2192 tick #${issue.number}: ${issue.title}
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(`[missions] tick #${issue.number} failed (exit ${out.exitCode}): ${out.reason ?? ""}
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(`[missions] tick #${issue.number} crashed: ${msg}
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.missionTickResults = results;
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/loadMissionFromFile.ts
4956
+ // src/scripts/loadJobFromFile.ts
4957
4957
  import * as fs22 from "fs";
4958
4958
  import * as path20 from "path";
4959
- var loadMissionFromFile = async (ctx, _profile, args) => {
4960
- const missionsDir = String(args?.missionsDir ?? ".kody/missions");
4961
- const slugArg = String(args?.slugArg ?? "mission");
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(`loadMissionFromFile: ctx.args.${slugArg} must be a non-empty slug`);
4964
+ throw new Error(`loadJobFromFile: ctx.args.${slugArg} must be a non-empty slug`);
4965
4965
  }
4966
- const absPath = path20.join(ctx.cwd, missionsDir, `${slug}.md`);
4966
+ const absPath = path20.join(ctx.cwd, jobsDir, `${slug}.md`);
4967
4967
  if (!fs22.existsSync(absPath)) {
4968
- throw new Error(`loadMissionFromFile: mission file not found: ${absPath}`);
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 } = parseMissionFile(raw, slug);
4972
- const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, missionsDir });
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.missionSlug = slug;
4975
- ctx.data.missionTitle = title;
4976
- ctx.data.missionIntent = body;
4977
- ctx.data.missionState = loaded;
4978
- ctx.data.missionStateJson = JSON.stringify(loaded.state, null, 2);
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 parseMissionFile(raw, slug) {
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/parseMissionStateFromAgentResult.ts
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 parseMissionStateFromAgentResult = async (ctx, _profile, agentResult, args) => {
5648
+ var parseJobStateFromAgentResult = async (ctx, _profile, agentResult, args) => {
5649
5649
  const fenceLabel = String(args?.fenceLabel ?? "");
5650
5650
  if (!fenceLabel) {
5651
- throw new Error("parseMissionStateFromAgentResult: `with.fenceLabel` is required");
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.missionState;
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.nextMissionState = next;
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/writeMissionStateFile.ts
7207
- var writeMissionStateFile = async (ctx, _profile, _agentResult, args) => {
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] mission state write skipped: ${parseError}
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.nextMissionState;
7216
+ const next = ctx.data.nextJobState;
7217
7217
  if (!next) {
7218
7218
  return;
7219
7219
  }
7220
- const loaded = ctx.data.missionState;
7220
+ const loaded = ctx.data.jobState;
7221
7221
  if (!loaded) {
7222
- throw new Error("writeMissionStateFile: ctx.data.missionState missing \u2014 preflight must run first");
7222
+ throw new Error("writeJobStateFile: ctx.data.jobState missing \u2014 preflight must run first");
7223
7223
  }
7224
- const missionsDir = String(args?.missionsDir ?? ".kody/missions");
7225
- const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, missionsDir });
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
- loadMissionFromFile,
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
- dispatchMissionTicks,
7290
- dispatchMissionFileTicks
7289
+ dispatchJobTicks,
7290
+ dispatchJobFileTicks
7291
7291
  };
7292
7292
  var postflightScripts = {
7293
7293
  parseAgentResult: parseAgentResult2,
7294
7294
  parseIssueStateFromAgentResult,
7295
- parseMissionStateFromAgentResult,
7295
+ parseJobStateFromAgentResult,
7296
7296
  parseReproOutput,
7297
7297
  writeIssueStateComment,
7298
- writeMissionStateFile,
7298
+ writeJobStateFile,
7299
7299
  requireFeedbackActions,
7300
7300
  requirePlanDeviations,
7301
7301
  verify,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bug",
3
- "role": "orchestrator",
4
- "describe": "Sub-orchestrator for bug / enhancement issues — plan → run → review (→ fix on concerns/fail). No agent the postflight entries ARE the transition table, evaluated top-to-bottom via runWhen.",
3
+ "role": "container",
4
+ "describe": "Bug / enhancement flowreproduce → 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": "dispatch", "with": { "next": "fix", "target": "pr" },
65
- "runWhen": { "data.taskState.core.lastOutcome.type": ["REVIEW_CONCERNS", "REVIEW_FAIL"] } },
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
- This file exists only because the executor's profile loader expects a
3
- prompt.md sibling. The orchestrator-plan-build-review executable runs
4
- with maxTurns: 0 and a `skipAgent` preflight, so this prompt is never
5
- actually delivered to Claude. The transition logic lives entirely in
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": "mission-scheduler",
2
+ "name": "job-scheduler",
3
3
  "role": "watch",
4
- "describe": "Scheduled: for every mission file under .kody/missions/, invoke mission-tick once. No agent on the scheduler itself.",
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": "dispatchMissionFileTicks",
39
+ "script": "dispatchJobFileTicks",
40
40
  "with": {
41
- "missionsDir": ".kody/missions",
42
- "targetExecutable": "mission-tick",
43
- "slugArg": "mission"
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": "mission-tick",
2
+ "name": "job-tick",
3
3
  "role": "primitive",
4
- "describe": "One classifier tick for one mission file: read intent + state, decide and execute via gh, emit next state.",
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": "mission",
9
- "flag": "--mission",
8
+ "name": "job",
9
+ "flag": "--job",
10
10
  "type": "string",
11
11
  "required": true,
12
- "describe": "Mission slug — basename (without .md) of the file under .kody/missions/."
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": "loadMissionFromFile",
46
+ "script": "loadJobFromFile",
47
47
  "with": {
48
- "missionsDir": ".kody/missions",
49
- "slugArg": "mission"
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": "parseMissionStateFromAgentResult",
58
+ "script": "parseJobStateFromAgentResult",
59
59
  "with": {
60
- "fenceLabel": "kody-mission-next-state"
60
+ "fenceLabel": "kody-job-next-state"
61
61
  }
62
62
  },
63
63
  {
64
- "script": "writeMissionStateFile"
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.
@@ -383,14 +383,14 @@
383
383
  },
384
384
  "additionalProperties": false
385
385
  },
386
- "missions": {
386
+ "jobs": {
387
387
  "type": "object",
388
- "description": "File-based mission configuration. Missions are long-running scheduled executables defined under .kody/missions/<slug>.md.",
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 mission 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).",
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.87",
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",
@@ -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
- # (mission-scheduler, memorize, watch-stale-prs, …) match this tick.
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.