@kody-ade/kody-engine 0.3.86 → 0.4.0

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