@kody-ade/kody-engine 0.4.206 → 0.4.207-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,8 +8,8 @@
8
8
  **An autonomous development engine that runs in your GitHub Actions.**
9
9
 
10
10
  Comment `@kody` on an issue and it implements the change, commits, and opens a
11
- PR — all inside CI, no bot server to host. Comment on a PR to apply review
12
- feedback, fix failing CI, resolve merge conflicts, or run a UI/QA pass. It's a
11
+ PR — all inside CI, no bot server to host. Comment on a PR with explicit
12
+ commands such as `@kody resolve` or `@kody sync` for PR maintenance. It's a
13
13
  single-session Claude Code agent behind a generic executor and declarative JSON
14
14
  profiles.
15
15
 
@@ -26,8 +26,8 @@ kody: reads the issue → writes the code → runs your tests → opens a PR
26
26
 
27
27
  - **No infrastructure.** Runs on the GitHub Actions you already have. One ~20-line
28
28
  workflow file, installed via `npx`. Nothing to deploy or keep online.
29
- - **Whole PR lifecycle, not just authoring.** `run`, `fix`, `fix-ci`, `resolve`,
30
- `review`, `ui-review`, `qa-engineer`, scheduled jobs — one agent, many verbs.
29
+ - **Whole PR lifecycle, not just authoring.** `run`, `resolve`, `sync`, `merge`,
30
+ `revert`, previews, releases, and scheduled duties — one executor, many verbs.
31
31
  - **Declarative & extensible.** Every command is a folder of `profile.json` +
32
32
  `prompt.md` + shell. Add a command by dropping a folder — no engine changes.
33
33
  - **Bring your own model.** Anthropic native, or any provider via the built-in
@@ -68,7 +68,7 @@ See [SECURITY.md](SECURITY.md) to report a vulnerability.
68
68
 
69
69
  ```
70
70
  ┌─────────────────────────────────────────────┐
71
- │ Consumer repo workflow (.github/kody.yml) │ @kody comments · schedule · release PR merge
71
+ │ Consumer repo workflow (.github/kody.yml) │ @kody comments · schedule · manual dispatch
72
72
  └─────────────────────────────────────────────┘
73
73
 
74
74
  ┌─────────────────────────────────────────────┐
@@ -96,181 +96,58 @@ npx -y -p @kody-ade/kody-engine@latest kody init
96
96
 
97
97
  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/*`.
98
98
 
99
- 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).
99
+ The consumer workflow listens on `issue_comment` for `@kody ...` dispatch and `workflow_dispatch` for manual runs, chat mode, and scheduled wakeups.
100
100
 
101
101
  ## Commands
102
102
 
103
103
  ```
104
- # agent, writes code
105
- kody run --issue <N> # implement an issue end-to-end
106
- kody fix --pr <N> [--feedback ...] # apply PR review feedback
107
- kody fix-ci --pr <N> [--run-id <ID>] # fix failing CI
108
- kody resolve --pr <N> [--prefer ours|theirs] # merge default branch, resolve conflicts
109
-
110
- # agent, read-only
111
- kody plan --issue <N> # research + implementation plan
112
- kody research --issue <N> # map repo context, surface gaps
113
- kody review --pr <N> # structured diff review
114
- kody ui-review --pr <N> [--preview-url <URL>] # UI review — browses preview via Playwright MCP
115
- kody qa-engineer [--url <URL>] [--scope ...] # free-form QA — browses, opens findings as goal task issues
116
- [--goal <id>] [--issue <N>]
117
- [--auth-profile <storageState.json>]
118
- kody classify --issue <N> # pick a flow type (feature/bug/spec/chore)
119
-
120
- # flow orchestrators (no agent of their own — transition tables)
121
- kody feature --issue <N> # research → plan → run → review (→ fix)
122
- kody bug --issue <N> # plan → run → review (→ fix)
123
- kody spec --issue <N> # research → plan (no code, terminates at plan)
124
- kody chore --issue <N> # run → review (→ fix)
125
-
126
- # jobs & watches (scheduled, coordinate work via issue state)
127
- kody job-scheduler # fans out to per-issue job-tick
128
- kody job-tick --issue <N> # one tick of a kody:job issue
129
- kody watch-stale-prs # weekly stale-PR report
130
- kody memorize # daily vault wiki update from recent PRs
131
-
132
- # deterministic (no agent)
133
- kody sync --pr <N> # merge default into PR branch
134
- kody release --mode prepare|finalize [--bump patch|minor|major] [--dry-run]
135
- kody init [--force] # scaffold consumer repo
136
-
137
- # engine entrypoints
138
- kody ci # auto-dispatches from the GHA event
139
- kody chat [--session <id>] # dashboard-driven chat session
140
- ```
141
-
142
- ### Flow orchestrators
143
-
144
- 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.
145
-
146
- ### Jobs
147
-
148
- 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.
149
-
150
- `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).
151
-
152
- **Locked-toolbox jobs** (v0.4.175). A job file can add `tools: [...]` to its frontmatter to run the tick in a *locked toolbox*: the agent gets only those named tools (as `mcp__kody-duty__<name>`) plus `submit_state` — `Bash` and `Read` are revoked entirely. This removes the escape hatch where a job posted a raw `@kody <verb>` comment that the webhook silently drops for bot authors, so the job looked done while its verb never ran. The in-process kody-duty MCP server exposes high-level intents instead — `list_prs_to_repair`, `sync_pr` / `fix_ci_pr` / `resolve_pr` (each dispatches the matching `workflow_dispatch`, never a comment), `recommend_to_operator`, and `read_ledger`. Jobs without `tools:` keep the full Bash/gh toolbox unchanged.
153
-
154
- ### `ui-review`
155
-
156
- PR-bound UI review. Drives the running preview deployment via the Playwright MCP server alongside the usual diff review, posts one structured review comment.
157
-
158
- - Preview URL: `--preview-url` → `$PREVIEW_URL` → `http://localhost:3000`. Unreachable → falls back to diff-only.
159
- - Credentials: dashboard-managed, per-repo — login username from the `LOGIN_USER` variable (`.kody/variables.json`), password from the `LOGIN_PASSWORD` vault secret (`.kody/secrets.enc`). Hand-written scenarios/notes come from the company profile (`.kody/profile/*.md`).
160
- - Auto-discovery: routes, roles, login/admin paths, Payload CMS collections, API routes, env vars — fed to the agent as context.
161
-
162
- For free-form QA passes (no diff, no PR), see [`qa-engineer`](#qa-engineer) below.
104
+ # The published bin is `kody-engine` (renamed from `kody` in v0.4.77 to avoid
105
+ # shadowing a consumer repo's own `kody` bin). Install globally or via `npx`.
163
106
 
164
- ### `qa-engineer`
107
+ # issue authoring
108
+ kody-engine run --issue <N> # implement an issue end-to-end
165
109
 
166
- Free-form QA pass. Browses a running site with Playwright MCP, exercises UI states (happy / empty / error / loading / mobile / a11y), and turns findings into a kody goal whose tasks are individually triageable, severity-labelled bug issues. Read-only on the repo (no commits except the goal's own `state.json`).
110
+ # PR operations
111
+ kody-engine resolve --pr <N> [--prefer ours|theirs] # merge default branch, resolve conflicts
112
+ kody-engine sync --pr <N> # merge default into a PR branch
113
+ kody-engine merge --pr <N> # merge a green PR
114
+ kody-engine revert --pr <N> --shas <sha...> # mechanically revert PR commits
115
+ kody-engine preview-build --pr <N> # build and publish a PR preview
167
116
 
168
- ```bash
169
- # broad smoke against the project's QA_URL variable
170
- kody qa-engineer
171
-
172
- # focused pass; opens a new qa-<scope>-<date> goal + N task issues
173
- kody qa-engineer --scope "checkout flow"
174
-
175
- # attach findings to an existing goal (resolves URL from goal-<id>'s Vercel deployment)
176
- kody qa-engineer --goal admin-chat-memory-recall-ui
117
+ # release operations
118
+ kody-engine release --issue <N> [--bump patch|minor|major] [--dry-run]
119
+ kody-engine release-prepare --issue <N> [--bump patch|minor|major] [--dry-run]
120
+ kody-engine release-publish --issue <N> [--dry-run]
121
+ kody-engine release-deploy --issue <N> [--dry-run]
177
122
 
178
- # explicit URL overrides everything; useful for testing a deployed PR preview
179
- kody qa-engineer --url https://my-feature-branch.vercel.app --scope "search UX"
123
+ # scheduled duties and goals
124
+ kody-engine job-scheduler # fan out due .kody/duties/*.md files
125
+ kody-engine job-tick --job <slug> [--force] # one agent tick for one duty
126
+ kody-engine job-tick-scripted --job <slug> [--force] # one deterministic tickScript duty tick
127
+ kody-engine goal-scheduler # fan out active .kody/goals/* state files
128
+ kody-engine goal-tick --goal <id> # advance one stacked-PR goal
180
129
 
181
- # pre-authenticated session via committed Playwright storageState
182
- kody qa-engineer --scope "admin" --auth-profile .kody/qa-storage-state.json
183
-
184
- # PASS verdicts (no findings) skip goal creation; --issue routes the report to a comment
185
- kody qa-engineer --scope "smoke" --issue 1234
130
+ # setup, servers, and utilities
131
+ kody-engine init [--force] # scaffold consumer repo
132
+ kody-engine ci # auto-dispatch from the GitHub Actions event
133
+ kody-engine chat [--session <id>] # dashboard-driven chat session
134
+ kody-engine serve # LiteLLM/editor helper
135
+ kody-engine brain-serve # Brain SSE server
136
+ kody-engine pool-serve # warm-pool owner
137
+ kody-engine runner-serve # warm-pool one-shot runner
138
+ kody-engine worker-ask --worker <slug> --message "..." # ad-hoc staff persona run
139
+ kody-engine stats # inspect run/event history
186
140
  ```
187
141
 
188
- **URL resolution chain.** `resolveQaUrl` walks the chain in order; first non-empty source wins:
189
-
190
- 1. `--url <URL>` — explicit
191
- 2. `--goal <id>` → latest successful Vercel deployment for the `goal-<id>` branch (via `repos/.../deployments?ref=goal-<id>` + statuses)
192
- 3. `$PREVIEW_URL` env var
193
- 4. `QA_URL` variable in `.kody/variables.json` (per-repo, dashboard-managed)
194
- 5. error — no localhost defaults; CI has no localhost to fall back to.
195
-
196
- Configure the per-repo default via the dashboard's variables store (`.kody/variables.json`), key `QA_URL` — e.g. `https://dev.example.com`.
142
+ ### Duties
197
143
 
198
- **Output modes.**
144
+ A **duty** is a markdown file at `.kody/duties/<slug>.md` with frontmatter such as `every:` and `staff:` plus human-owned prose. `job-scheduler` wakes on cron, finds due duties, and dispatches either `job-tick` for an agent tick or `job-tick-scripted` for a deterministic `tickScript:` duty. `kody init` copies built-in starter duties and scaffolds `.kody/staff/kody.md`.
199
145
 
200
- | Trigger | What happens |
201
- |---|---|
202
- | Findings + no `--goal` | Appends a new `qa-<scope>-<date>` entry to the `kody:goals-manifest` issue (description = full report markdown). Opens N task issues, each labelled `goal:<id>` + `severity:Px` + `kody:qa-finding`. Writes `.kody/goals/<id>/state.json` (state: `active`) and pushes — `goal-scheduler` picks it up next tick. |
203
- | Findings + `--goal <id>` | Skips manifest body mutation (the existing goal owns its description). Posts the report markdown as a comment on the manifest issue. Opens N task issues with `goal:<id>` labels. |
204
- | Zero findings + `--issue <N>` | Posts the report as a comment on issue N. No goal touched. |
205
- | Zero findings + no `--issue` | Opens a single `kody:qa-finding`-labelled record issue with the full report body. |
206
-
207
- **Agent-emitted JSON contract.** The prompt requires the agent's final message to end with a machine-readable block:
208
-
209
- ```
210
- <!-- KODY_QA_REPORT_JSON
211
- ```json
212
- {
213
- "findings": [
214
- {
215
- "severity": "P0|P1|P2|P3",
216
- "title": "Short imperative — becomes the issue title",
217
- "route": "/admin/...",
218
- "steps": "1. ...\n2. ...",
219
- "expected": "...",
220
- "actual": "...",
221
- "evidence": ".kody/qa-reports/<scope>/<finding>.png"
222
- }
223
- ]
224
- }
225
- ```
226
- -->
227
- ```
228
-
229
- If the block is missing or malformed, the postflight falls back to single-issue mode and logs a warning. Severity rubric: P0 blocks core flow / data loss / security → verdict FAIL; P1 broken non-critical feature → typically FAIL; P2 degraded UX → typically CONCERNS; P3 polish → doesn't affect verdict.
230
-
231
- **GHA usage.** Trigger from any issue comment (auto-dispatched by the existing kody.yml workflow):
232
-
233
- ```
234
- @kody qa-engineer --goal add-per-user-chat-memory-recall-ui --scope "memory recall UI"
235
- ```
236
-
237
- …or via `workflow_dispatch`:
238
-
239
- ```bash
240
- gh workflow run kody.yml -F executable=qa-engineer \
241
- -F args="--goal admin-chat-memory-recall-ui --scope 'admin chat'"
242
- ```
243
-
244
- **Auto-discovery & credentials.** Same as `ui-review`: `discoverQaContext` scans the repo for routes/roles/admin path/Payload collections/env vars; `loadQaContext` reads the dashboard-managed login username (`LOGIN_USER` variable), password (`LOGIN_PASSWORD` vault secret), and hand-written scenarios/notes (`.kody/profile/*.md`). The agent only logs in if a route under test requires it.
245
-
246
- **Artifacts.** Screenshots and DOM snapshots go to `.kody/qa-reports/<scope-slug>/`; the Playwright MCP also writes to `.playwright-mcp/`. Both should be in `.gitignore` and `.prettierignore` — `kody init` doesn't yet scaffold these, add them manually:
247
-
248
- ```gitignore
249
- .kody/qa-reports/
250
- .playwright-mcp/
251
- ```
252
-
253
- ```
254
- .kody/**
255
- .playwright-mcp/**
256
- ```
257
-
258
- ### `memorize` — vault wiki
259
-
260
- A scheduled watch (cron `0 3 * * *`) that synthesizes recently merged PRs into a markdown knowledge base at `.kody/vault/` and opens a PR with the changes. Pages are entity-centric (`architecture/`, `conventions/`, `decisions/`, `components/`), not per-PR logs. Future kody runs see the relevant pages via the `loadVaultContext` preflight, which is wired into `run` / `fix` / `resolve` and exposes them as `{{vaultContext}}` in the prompt.
261
-
262
- To enable in a consumer repo: ensure `.gitignore` un-ignores the vault if `.kody/*` is otherwise ignored:
263
-
264
- ```gitignore
265
- .kody/*
266
- !.kody/vault/
267
- !.kody/vault/**
268
- ```
146
+ Locked-toolbox duties can declare `tools: [...]` to run with only the named high-level MCP intents plus `submit_state`; duties without `tools:` keep the legacy Bash/gh toolbox.
269
147
 
270
148
  ### `release`
271
149
 
272
- - `--mode prepare` bumps `package.json`, updates `CHANGELOG.md`, opens a `release/vX.Y.Z` PR. `--bump patch|minor|major` (default `patch`).
273
- - `--mode finalize` — tags, pushes, runs `prepublishOnly` + `npm publish`, creates a GH release. Runs **automatically** when a `release/vX.Y.Z` PR is merged (via `pull_request: [closed]` in the consumer workflow); manual trigger still works.
150
+ `release` is a single deterministic flow: bump version files, update `CHANGELOG.md`, open or reuse a release PR, wait for CI, merge, publish, deploy, and notify. `release-prepare`, `release-publish`, and `release-deploy` remain available as explicit stage commands.
274
151
 
275
152
  ## Profiles
276
153
 
package/dist/bin/kody.js CHANGED
@@ -1486,7 +1486,7 @@ var init_loadCoverageRules = __esm({
1486
1486
  // package.json
1487
1487
  var package_default = {
1488
1488
  name: "@kody-ade/kody-engine",
1489
- version: "0.4.206",
1489
+ version: "0.4.207-next.1",
1490
1490
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
1491
1491
  license: "MIT",
1492
1492
  type: "module",
@@ -1717,10 +1717,10 @@ function loadConfig(projectDir = process.cwd()) {
1717
1717
  const msg = err instanceof Error ? err.message : String(err);
1718
1718
  throw new Error(`kody.config.json is invalid JSON: ${msg}`);
1719
1719
  }
1720
- const quality = raw.quality ?? {};
1721
- const git5 = raw.git ?? {};
1722
- const github = raw.github ?? {};
1723
- const agent = raw.agent ?? {};
1720
+ const quality = recordValue(raw.quality) ?? {};
1721
+ const git5 = recordValue(raw.git) ?? {};
1722
+ const github = recordValue(raw.github) ?? {};
1723
+ const agent = recordValue(raw.agent) ?? {};
1724
1724
  if (!agent.model || typeof agent.model !== "string") {
1725
1725
  throw new Error(`kody.config.json: agent.model is required (e.g. "minimax/MiniMax-M2.7-highspeed")`);
1726
1726
  }
@@ -1748,7 +1748,7 @@ function loadConfig(projectDir = process.cwd()) {
1748
1748
  issueContext: parseIssueContext(raw.issueContext),
1749
1749
  testRequirements: parseTestRequirements(raw.testRequirements),
1750
1750
  defaultExecutable: typeof raw.defaultExecutable === "string" && raw.defaultExecutable.length > 0 ? raw.defaultExecutable : "run",
1751
- defaultPrExecutable: typeof raw.defaultPrExecutable === "string" && raw.defaultPrExecutable.length > 0 ? raw.defaultPrExecutable : "fix",
1751
+ defaultPrExecutable: typeof raw.defaultPrExecutable === "string" && raw.defaultPrExecutable.length > 0 ? raw.defaultPrExecutable : void 0,
1752
1752
  onPullRequest: typeof raw.onPullRequest === "string" && raw.onPullRequest.length > 0 ? raw.onPullRequest : void 0,
1753
1753
  aliases: mergeAliases(raw.aliases),
1754
1754
  classify: parseClassifyConfig(raw.classify),
@@ -1811,10 +1811,11 @@ function parseJobsConfig(raw) {
1811
1811
  }
1812
1812
  return Object.keys(out).length > 0 ? out : void 0;
1813
1813
  }
1814
+ function recordValue(raw) {
1815
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : void 0;
1816
+ }
1814
1817
  var BUILTIN_ALIASES = {
1815
- build: "run",
1816
- orchestrate: "bug",
1817
- orchestrator: "bug"
1818
+ build: "run"
1818
1819
  };
1819
1820
  function mergeAliases(raw) {
1820
1821
  const out = { ...BUILTIN_ALIASES };
@@ -3500,10 +3501,11 @@ function autoDispatch(opts) {
3500
3501
  return null;
3501
3502
  }
3502
3503
  if (eventName === "workflow_dispatch") {
3503
- const n = parseInt(String(event.inputs?.issue_number ?? ""), 10);
3504
+ const inputs2 = objectValue(event.inputs);
3505
+ const n = parseInt(String(inputs2?.issue_number ?? ""), 10);
3504
3506
  if (!Number.isNaN(n) && n > 0) {
3505
- const exe = String(event.inputs?.executable ?? "").trim() || "run";
3506
- const base = String(event.inputs?.base ?? "").trim();
3507
+ const exe = String(inputs2?.executable ?? "").trim() || "run";
3508
+ const base = String(inputs2?.base ?? "").trim();
3507
3509
  const targetKey = primaryNumericInputName(exe) ?? "issue";
3508
3510
  const cliArgs = { [targetKey]: n };
3509
3511
  if (base) cliArgs.base = base;
@@ -3516,7 +3518,8 @@ function autoDispatch(opts) {
3516
3518
  const exe = opts?.config?.onPullRequest?.trim();
3517
3519
  const action = String(event.action ?? "");
3518
3520
  if (exe && (action === "opened" || action === "synchronize" || action === "reopened")) {
3519
- const prNum = Number(event.pull_request?.number ?? event.number ?? 0);
3521
+ const pullRequest = objectValue(event.pull_request);
3522
+ const prNum = Number(pullRequest?.number ?? event.number ?? 0);
3520
3523
  if (prNum > 0) {
3521
3524
  const targetKey = primaryNumericInputName(exe) ?? "pr";
3522
3525
  return { executable: exe, cliArgs: { [targetKey]: prNum }, target: prNum };
@@ -3525,15 +3528,18 @@ function autoDispatch(opts) {
3525
3528
  return null;
3526
3529
  }
3527
3530
  if (eventName !== "issue_comment") return null;
3528
- const rawBody = String(event.comment?.body ?? "");
3529
- const authorLogin = String(event.comment?.user?.login ?? "");
3530
- const authorType = String(event.comment?.user?.type ?? "");
3531
+ const comment = objectValue(event.comment);
3532
+ const issue = objectValue(event.issue);
3533
+ const user = objectValue(comment?.user);
3534
+ const rawBody = String(comment?.body ?? "");
3535
+ const authorLogin = String(user?.login ?? "");
3536
+ const authorType = String(user?.type ?? "");
3531
3537
  if (!hasKodyMention(rawBody)) return null;
3532
3538
  const isBotAuthor = authorLogin === "kody-bot" || authorType === "Bot";
3533
3539
  if (!associationAllowed(event, opts?.config)) return null;
3534
3540
  const body = rawBody.toLowerCase();
3535
- const targetNum = Number(event.issue?.number ?? 0);
3536
- const isPr = !!event.issue?.pull_request;
3541
+ const targetNum = Number(issue?.number ?? 0);
3542
+ const isPr = !!issue?.pull_request;
3537
3543
  if (!targetNum) return null;
3538
3544
  const afterTag = extractAfterTag(body);
3539
3545
  const firstTokenRaw = extractSubcommand(afterTag);
@@ -3554,7 +3560,7 @@ function autoDispatch(opts) {
3554
3560
  }
3555
3561
  }
3556
3562
  if (!executable && !firstToken) {
3557
- executable = isPr ? opts?.config?.defaultPrExecutable ?? "fix" : opts?.config?.defaultExecutable ?? null;
3563
+ executable = isPr ? opts?.config?.defaultPrExecutable ?? null : opts?.config?.defaultExecutable ?? null;
3558
3564
  }
3559
3565
  if (isBotAuthor && !consumedFirstToken) {
3560
3566
  process.stderr.write(
@@ -3564,9 +3570,10 @@ function autoDispatch(opts) {
3564
3570
  return null;
3565
3571
  }
3566
3572
  if (!executable) {
3573
+ if (!firstToken) return null;
3567
3574
  const profileMissing = aliased ? getProfileInputs(aliased) === null : true;
3568
3575
  process.stderr.write(
3569
- `[kody] dispatch: no executable resolved for issue_comment (firstToken=${firstToken ?? "<none>"}, aliased=${aliased ?? "<none>"}, profileFound=${!profileMissing}, defaultExecutable=${opts?.config?.defaultExecutable ?? "<unset>"})
3576
+ `[kody] dispatch: no executable resolved for issue_comment (firstToken=${firstToken ?? "<none>"}, aliased=${aliased ?? "<none>"}, profileFound=${!profileMissing}, defaultExecutable=${opts?.config?.defaultExecutable ?? "<unset>"}, defaultPrExecutable=${opts?.config?.defaultPrExecutable ?? "<unset>"})
3570
3577
  `
3571
3578
  );
3572
3579
  return null;
@@ -3604,9 +3611,12 @@ function autoDispatchTyped(opts) {
3604
3611
  } catch {
3605
3612
  return { kind: "silent", reason: "GHA event payload unreadable" };
3606
3613
  }
3607
- const rawBody = String(event.comment?.body ?? "");
3608
- const authorLogin = String(event.comment?.user?.login ?? "");
3609
- const authorType = String(event.comment?.user?.type ?? "");
3614
+ const comment = objectValue(event.comment);
3615
+ const issue = objectValue(event.issue);
3616
+ const user = objectValue(comment?.user);
3617
+ const rawBody = String(comment?.body ?? "");
3618
+ const authorLogin = String(user?.login ?? "");
3619
+ const authorType = String(user?.type ?? "");
3610
3620
  if (!hasKodyMention(rawBody)) {
3611
3621
  return { kind: "silent", reason: "comment does not mention @kody" };
3612
3622
  }
@@ -3614,11 +3624,11 @@ function autoDispatchTyped(opts) {
3614
3624
  return { kind: "silent", reason: `bot-authored comment (${authorLogin || authorType})` };
3615
3625
  }
3616
3626
  if (!associationAllowed(event, opts?.config)) {
3617
- const assoc = String(event.comment?.author_association ?? "").toUpperCase() || "<none>";
3627
+ const assoc = String(comment?.author_association ?? "").toUpperCase() || "<none>";
3618
3628
  return { kind: "silent", reason: `commenter association '${assoc}' not in access.allowedAssociations` };
3619
3629
  }
3620
- const targetNum = Number(event.issue?.number ?? 0);
3621
- const isPr = !!event.issue?.pull_request;
3630
+ const targetNum = Number(issue?.number ?? 0);
3631
+ const isPr = !!issue?.pull_request;
3622
3632
  if (!targetNum) {
3623
3633
  return { kind: "silent", reason: "comment has no associated issue/PR number" };
3624
3634
  }
@@ -3674,9 +3684,13 @@ function dispatchScheduledWatches(opts) {
3674
3684
  function associationAllowed(event, config) {
3675
3685
  const allowed = config?.access?.allowedAssociations;
3676
3686
  if (!allowed || allowed.length === 0) return true;
3677
- const assoc = String(event.comment?.author_association ?? "").toUpperCase();
3687
+ const comment = objectValue(event.comment);
3688
+ const assoc = String(comment?.author_association ?? "").toUpperCase();
3678
3689
  return allowed.includes(assoc);
3679
3690
  }
3691
+ function objectValue(value) {
3692
+ return value && typeof value === "object" ? value : void 0;
3693
+ }
3680
3694
  var KODY_MENTION_RE = /(?:^|\s)@kody(?=\s|$|[^\w-])/i;
3681
3695
  function hasKodyMention(body) {
3682
3696
  return KODY_MENTION_RE.test(body);
@@ -5195,17 +5209,32 @@ function generateLitellmConfigYaml(model) {
5195
5209
  ""
5196
5210
  ].join("\n");
5197
5211
  }
5212
+ function litellmImportable() {
5213
+ try {
5214
+ execFileSync6("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
5215
+ return true;
5216
+ } catch {
5217
+ return false;
5218
+ }
5219
+ }
5198
5220
  function resolveLitellmCommand() {
5199
5221
  try {
5200
5222
  execFileSync6("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
5201
5223
  return "litellm";
5202
5224
  } catch {
5203
- try {
5204
- execFileSync6("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
5205
- return "python3";
5206
- } catch {
5207
- throw new Error("litellm not installed \u2014 run: pip install 'litellm[proxy]'");
5225
+ if (litellmImportable()) return "python3";
5226
+ process.stderr.write("\u2192 kody: litellm not found \u2014 installing (pip install 'litellm[proxy]')\n");
5227
+ let installed = false;
5228
+ for (const pip of ["pip", "pip3"]) {
5229
+ try {
5230
+ execFileSync6(pip, ["install", "litellm[proxy]"], { timeout: 3e5, stdio: "inherit" });
5231
+ installed = true;
5232
+ break;
5233
+ } catch {
5234
+ }
5208
5235
  }
5236
+ if (installed && litellmImportable()) return "python3";
5237
+ throw new Error("litellm not installed and auto-install failed \u2014 run: pip install 'litellm[proxy]'");
5209
5238
  }
5210
5239
  }
5211
5240
  async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL) {
@@ -9539,7 +9568,6 @@ jobs:
9539
9568
  if: >-
9540
9569
  \${{ github.event_name == 'workflow_dispatch' ||
9541
9570
  (github.event_name == 'issue_comment' &&
9542
- !github.event.issue.pull_request &&
9543
9571
  contains(github.event.comment.body, '@kody')) }}
9544
9572
  runs-on: ubuntu-latest
9545
9573
  timeout-minutes: 60
@@ -9564,7 +9592,14 @@ jobs:
9564
9592
 
9565
9593
  - env:
9566
9594
  ALL_SECRETS: \${{ toJSON(secrets) }}
9567
- run: npx -y -p @kody-ade/kody-engine@latest kody-engine ci --issue \${{ github.event.inputs.issue_number || github.event.issue.number }}
9595
+ run: npx -y -p @kody-ade/kody-engine@latest kody-engine ci
9596
+ `;
9597
+ var DEFAULT_STAFF_PERSONA = `# Kody
9598
+
9599
+ You are Kody, the default maintenance staff member for scheduled duties.
9600
+
9601
+ Keep actions narrow, prefer read-only inspection, and only use the tools or commands named by the duty.
9602
+ When a duty writes a report or dispatches work, keep the output factual and concise.
9568
9603
  `;
9569
9604
  function defaultBranchFromGit(cwd) {
9570
9605
  try {
@@ -9625,6 +9660,15 @@ function performInit(cwd, force) {
9625
9660
  wrote.push(rel);
9626
9661
  }
9627
9662
  }
9663
+ const staffDir = path28.join(cwd, ".kody", "staff");
9664
+ const staffPath = path28.join(staffDir, "kody.md");
9665
+ if (fs31.existsSync(staffPath) && !force) {
9666
+ skipped.push(".kody/staff/kody.md");
9667
+ } else {
9668
+ fs31.mkdirSync(staffDir, { recursive: true });
9669
+ fs31.writeFileSync(staffPath, DEFAULT_STAFF_PERSONA);
9670
+ wrote.push(".kody/staff/kody.md");
9671
+ }
9628
9672
  for (const exe of listExecutables()) {
9629
9673
  let profile;
9630
9674
  try {
@@ -9676,6 +9720,9 @@ jobs:
9676
9720
  - uses: actions/setup-node@v4
9677
9721
  with:
9678
9722
  node-version: 22
9723
+ - uses: actions/setup-python@v5
9724
+ with:
9725
+ python-version: "3.12"
9679
9726
  - env:
9680
9727
  GH_TOKEN: \${{ secrets.KODY_TOKEN || github.token }}
9681
9728
  run: npx -y -p @kody-ade/kody-engine@latest kody-engine ${name}
@@ -13306,6 +13353,23 @@ var allScriptNames = /* @__PURE__ */ new Set([
13306
13353
  import * as fs39 from "fs";
13307
13354
  import * as path36 from "path";
13308
13355
  var DEFAULT_STAFF_DIR = ".kody/staff";
13356
+ var BUILTIN_PERSONAS = {
13357
+ kody: [
13358
+ "You are **kody**, the repository's autonomous engineer.",
13359
+ "",
13360
+ "- You work the way this repo already works: follow its conventions, its",
13361
+ " existing patterns, and its tests. Read before you write.",
13362
+ "- You ship small, correct, reviewable changes. You don't refactor beyond the",
13363
+ " task, and you don't add scope nobody asked for.",
13364
+ "- You are honest about outcomes: if something failed, didn't run, or you're",
13365
+ " unsure, you say so plainly rather than papering over it.",
13366
+ "- The request that triggered you IS your authorization to do the work:",
13367
+ " opening a PR, pushing commits, and commenting are exactly what you're for \u2014",
13368
+ " do them without waiting for extra approval.",
13369
+ "- You still don't weaken security, delete work you didn't create, or take",
13370
+ " destructive actions beyond the task you were given."
13371
+ ].join("\n")
13372
+ };
13309
13373
  function stripFrontmatter(raw) {
13310
13374
  const match = /^---\n[\s\S]*?\n---\n?([\s\S]*)$/.exec(raw);
13311
13375
  return (match ? match[1] : raw).trim();
@@ -13314,12 +13378,16 @@ function loadStaffPersona(cwd, slug, staffDir = DEFAULT_STAFF_DIR) {
13314
13378
  const trimmed = slug.trim();
13315
13379
  if (!trimmed) throw new Error("loadStaffPersona: empty staff slug");
13316
13380
  const staffPath = path36.join(cwd, staffDir, `${trimmed}.md`);
13317
- if (!fs39.existsSync(staffPath)) {
13318
- throw new Error(`loadStaffPersona: staff '${trimmed}' declared but ${staffPath} does not exist`);
13381
+ if (fs39.existsSync(staffPath)) {
13382
+ const body = stripFrontmatter(fs39.readFileSync(staffPath, "utf-8"));
13383
+ if (body) return body;
13384
+ const builtinForEmpty = BUILTIN_PERSONAS[trimmed];
13385
+ if (builtinForEmpty) return builtinForEmpty;
13386
+ throw new Error(`loadStaffPersona: staff '${trimmed}' persona body is empty (${staffPath})`);
13319
13387
  }
13320
- const body = stripFrontmatter(fs39.readFileSync(staffPath, "utf-8"));
13321
- if (!body) throw new Error(`loadStaffPersona: staff '${trimmed}' persona body is empty (${staffPath})`);
13322
- return body;
13388
+ const builtin = BUILTIN_PERSONAS[trimmed];
13389
+ if (builtin) return builtin;
13390
+ throw new Error(`loadStaffPersona: staff '${trimmed}' declared but ${staffPath} does not exist`);
13323
13391
  }
13324
13392
  function framePersona(slug, persona) {
13325
13393
  return [
@@ -13507,7 +13575,8 @@ async function runExecutable(profileName, input) {
13507
13575
  };
13508
13576
  })() : null;
13509
13577
  const ndjsonDir = path37.join(input.cwd, ".kody");
13510
- const staffPersona = typeof profile.staff === "string" && profile.staff.length > 0 ? framePersona(profile.staff, loadStaffPersona(input.cwd, profile.staff)) : null;
13578
+ const personaSlug = typeof profile.staff === "string" && profile.staff.length > 0 ? profile.staff : typeof ctx.data.jobPersona === "string" && ctx.data.jobPersona.length > 0 ? ctx.data.jobPersona : null;
13579
+ const staffPersona = personaSlug ? framePersona(personaSlug, loadStaffPersona(input.cwd, personaSlug)) : null;
13511
13580
  const invokeAgent = async (prompt) => {
13512
13581
  const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path37.isAbsolute(p) ? p : path37.resolve(profile.dir, p)).filter((p) => p.length > 0);
13513
13582
  const syntheticPath = ctx.data.syntheticPluginPath;
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  every: 7d
3
+ staff: kody
3
4
  ---
4
5
 
5
6
  # watch-stale-prs
@@ -1,448 +1,176 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "title": "Kody Engine Lite Configuration",
4
- "description": "Configuration for the Kody autonomous SDLC pipeline. See https://github.com/aharonyaircohen/Kody-Engine-Lite",
3
+ "title": "Kody Engine Configuration",
4
+ "description": "Configuration for @kody-ade/kody-engine consumer repositories.",
5
5
  "type": "object",
6
+ "required": ["github", "agent"],
7
+ "additionalProperties": false,
6
8
  "properties": {
9
+ "$schema": {
10
+ "type": "string"
11
+ },
7
12
  "quality": {
8
13
  "type": "object",
9
- "description": "Quality gate commands run during the verify stage. Leave empty string to skip.",
14
+ "description": "Quality gate commands. Empty strings disable individual gates.",
15
+ "additionalProperties": false,
10
16
  "properties": {
11
- "typecheck": {
12
- "type": "string",
13
- "description": "TypeScript type checking command (e.g., 'pnpm typecheck', 'pnpm tsc --noEmit')",
14
- "default": "pnpm -s tsc --noEmit"
15
- },
16
- "lint": {
17
- "type": "string",
18
- "description": "Lint command (e.g., 'pnpm lint'). Empty to skip.",
19
- "default": ""
20
- },
21
- "lintFix": {
22
- "type": "string",
23
- "description": "Auto-fix lint command, run when verify fails (e.g., 'pnpm lint:fix')",
24
- "default": ""
25
- },
26
- "format": {
27
- "type": "string",
28
- "description": "Format check command (e.g., 'pnpm format:check', 'prettier --check .'). Empty to skip.",
29
- "default": ""
30
- },
31
- "formatFix": {
32
- "type": "string",
33
- "description": "Auto-fix format command, run when verify fails (e.g., 'pnpm format')",
34
- "default": ""
35
- },
36
- "testUnit": {
37
- "type": "string",
38
- "description": "Unit test command. Use test:unit to exclude integration/e2e tests (e.g., 'pnpm test:unit')",
39
- "default": "pnpm -s test"
40
- }
41
- },
42
- "additionalProperties": false
17
+ "typecheck": { "type": "string", "default": "" },
18
+ "lint": { "type": "string", "default": "" },
19
+ "format": { "type": "string", "default": "" },
20
+ "testUnit": { "type": "string", "default": "" }
21
+ }
43
22
  },
44
23
  "git": {
45
24
  "type": "object",
46
- "description": "Git configuration",
25
+ "additionalProperties": false,
47
26
  "properties": {
48
27
  "defaultBranch": {
49
28
  "type": "string",
50
- "description": "Default branch for PR base and branch syncing (e.g., 'main', 'dev')",
51
- "default": "dev"
29
+ "description": "Default branch for PR base and branch syncing.",
30
+ "default": "main"
52
31
  }
53
- },
54
- "additionalProperties": false
32
+ }
55
33
  },
56
34
  "github": {
57
35
  "type": "object",
58
- "description": "GitHub repository settings. Auto-detected from git remote by init.",
36
+ "required": ["owner", "repo"],
37
+ "additionalProperties": false,
59
38
  "properties": {
60
- "owner": {
61
- "type": "string",
62
- "description": "GitHub organization or username (e.g., 'my-org')"
63
- },
64
- "repo": {
65
- "type": "string",
66
- "description": "GitHub repository name (e.g., 'my-repo')"
67
- },
68
- "postSummary": {
69
- "type": "boolean",
70
- "description": "Post a structured pipeline summary comment on the issue after completion. Default: true in CI, false locally.",
71
- "default": true
72
- }
73
- },
74
- "additionalProperties": false
39
+ "owner": { "type": "string" },
40
+ "repo": { "type": "string" }
41
+ }
75
42
  },
76
- "timeouts": {
43
+ "agent": {
77
44
  "type": "object",
78
- "description": "Per-stage timeout overrides in seconds. Defaults: taskify=600, plan=600, build=2400, verify=300, review=600, review-fix=1200, ship=240",
45
+ "required": ["model"],
46
+ "additionalProperties": false,
79
47
  "properties": {
80
- "taskify": { "type": "number", "description": "Taskify stage timeout in seconds", "default": 600 },
81
- "plan": { "type": "number", "description": "Plan stage timeout in seconds", "default": 600 },
82
- "build": { "type": "number", "description": "Build stage timeout in seconds", "default": 2400 },
83
- "verify": { "type": "number", "description": "Verify stage timeout in seconds", "default": 300 },
84
- "review": { "type": "number", "description": "Review stage timeout in seconds", "default": 600 },
85
- "review-fix": { "type": "number", "description": "Review-fix stage timeout in seconds", "default": 1200 },
86
- "ship": { "type": "number", "description": "Ship stage timeout in seconds", "default": 240 }
87
- },
88
- "additionalProperties": false
48
+ "model": {
49
+ "type": "string",
50
+ "pattern": "^[^/]+/.+$",
51
+ "description": "Base provider/model string, for example minimax/MiniMax-M2.7-highspeed or claude/claude-sonnet-4-6."
52
+ },
53
+ "perExecutable": {
54
+ "type": "object",
55
+ "description": "Optional provider/model override by executable name.",
56
+ "additionalProperties": {
57
+ "type": "string",
58
+ "pattern": "^[^/]+/.+$"
59
+ }
60
+ }
61
+ }
89
62
  },
90
63
  "issueContext": {
91
64
  "type": "object",
92
- "description": "kody: how many and how much of the issue's comments the agent sees in its prompt. Higher = more context but larger prompts. Defaults: 50 comments, 10000 bytes each.",
65
+ "additionalProperties": false,
93
66
  "properties": {
94
67
  "commentLimit": {
95
68
  "type": "integer",
96
- "minimum": 1,
97
- "description": "Max number of comments (most recent first) to include in the agent prompt. Default: 50.",
98
- "default": 50
69
+ "minimum": 1
99
70
  },
100
71
  "commentMaxBytes": {
101
72
  "type": "integer",
102
- "minimum": 1,
103
- "description": "Max bytes per comment before truncation. Default: 10000.",
104
- "default": 10000
73
+ "minimum": 1
105
74
  }
106
- },
107
- "additionalProperties": false
75
+ }
108
76
  },
109
77
  "testRequirements": {
110
78
  "type": "array",
111
- "description": "kody enforces that newly added files matching `pattern` ship with a sibling test file matching `requireSibling`. Misses fail the run; the agent gets one retry with the gap as feedback. Empty array or absent = no enforcement.",
112
79
  "items": {
113
80
  "type": "object",
114
81
  "required": ["pattern", "requireSibling"],
115
82
  "additionalProperties": false,
116
83
  "properties": {
117
- "pattern": {
118
- "type": "string",
119
- "description": "Glob-style pattern (* within segment, ** across segments). Example: 'src/app/api/**/route.ts'"
120
- },
121
- "requireSibling": {
122
- "type": "string",
123
- "description": "Sibling test path template. Tokens: {name} (filename without ext), {ext} (e.g. .ts). Example: '{name}.test{ext}'"
124
- }
84
+ "pattern": { "type": "string" },
85
+ "requireSibling": { "type": "string" }
125
86
  }
126
87
  }
127
88
  },
128
- "agent": {
129
- "type": "object",
130
- "description": "Agent execution configuration",
131
- "properties": {
132
- "model": {
133
- "type": "string",
134
- "pattern": "^[^/]+/.+$",
135
- "description": "Single 'provider/model' string used by kody (single-session pipeline). Use 'claude/...' or 'anthropic/...' for direct Anthropic API; anything else routes through LiteLLM proxy.",
136
- "examples": ["claude/claude-sonnet-4-6", "minimax/MiniMax-M2.7-highspeed"]
137
- },
138
- "perExecutable": {
139
- "type": "object",
140
- "description": "Per-executable model override. Wins over agent.model for the matching stage. Example: {\"classify\": \"claude/claude-haiku-4-5-20251001\", \"plan\": \"claude/claude-opus-4-7\"}.",
141
- "additionalProperties": {
142
- "type": "string",
143
- "pattern": "^[^/]+/.+$"
144
- }
145
- },
146
- "modelMap": {
147
- "type": "object",
148
- "description": "Maps model tiers to 'provider/model' strings. Use 'claude/...' or 'anthropic/...' for direct Anthropic API; anything else routes through LiteLLM proxy.",
149
- "properties": {
150
- "cheap": {
151
- "type": "string",
152
- "pattern": "^[^/]+/.+$",
153
- "description": "Fast/cheap 'provider/model' for taskify (e.g., 'claude/claude-haiku-4-5-20251001', 'minimax/MiniMax-M2.7-highspeed')",
154
- "examples": ["claude/claude-haiku-4-5-20251001", "minimax/MiniMax-M2.7-highspeed"]
155
- },
156
- "mid": {
157
- "type": "string",
158
- "pattern": "^[^/]+/.+$",
159
- "description": "Mid-tier 'provider/model' for build, review-fix, autofix (e.g., 'claude/claude-sonnet-4-6')",
160
- "examples": ["claude/claude-sonnet-4-6", "minimax/MiniMax-M2.7-highspeed"]
161
- },
162
- "strong": {
163
- "type": "string",
164
- "pattern": "^[^/]+/.+$",
165
- "description": "Strongest 'provider/model' for plan, review — deep reasoning (e.g., 'claude/claude-opus-4-6')",
166
- "examples": ["claude/claude-opus-4-6", "minimax/MiniMax-M2.7-highspeed"]
167
- }
168
- },
169
- "additionalProperties": false
170
- },
171
- "defaultRunner": {
172
- "type": "string",
173
- "description": "Name of the default runner when multiple runners are configured (advanced)",
174
- "default": "claude"
175
- },
176
- "runners": {
177
- "type": "object",
178
- "description": "Named runner definitions (advanced)",
179
- "additionalProperties": {
180
- "type": "object",
181
- "properties": {
182
- "type": {
183
- "type": "string",
184
- "enum": ["claude-code"]
185
- }
186
- },
187
- "required": ["type"]
188
- }
189
- },
190
- "stageRunners": {
191
- "type": "object",
192
- "description": "Per-stage runner assignment (advanced). Maps stage name to runner name.",
193
- "properties": {
194
- "taskify": { "type": "string" },
195
- "plan": { "type": "string" },
196
- "build": { "type": "string" },
197
- "autofix": { "type": "string" },
198
- "review": { "type": "string" },
199
- "review-fix": { "type": "string" }
200
- },
201
- "additionalProperties": false
202
- },
203
- "default": {
204
- "type": "string",
205
- "pattern": "^[^/]+/.+$",
206
- "description": "Default 'provider/model' string applied to every stage. Use 'claude/<model>' for direct Anthropic API; anything else routes through LiteLLM proxy. Overridden by entries in 'stages'.",
207
- "examples": ["claude/claude-sonnet-4-6", "minimax/MiniMax-M2.7-highspeed", "openai/gpt-4o"]
208
- },
209
- "stages": {
210
- "type": "object",
211
- "description": "Per-stage 'provider/model' overrides. Takes precedence over 'default' and 'modelMap'.",
212
- "properties": {
213
- "taskify": { "type": "string", "pattern": "^[^/]+/.+$" },
214
- "plan": { "type": "string", "pattern": "^[^/]+/.+$" },
215
- "build": { "type": "string", "pattern": "^[^/]+/.+$" },
216
- "verify": { "type": "string", "pattern": "^[^/]+/.+$" },
217
- "review": { "type": "string", "pattern": "^[^/]+/.+$" },
218
- "review-fix": { "type": "string", "pattern": "^[^/]+/.+$" },
219
- "ship": { "type": "string", "pattern": "^[^/]+/.+$" }
220
- },
221
- "additionalProperties": false
222
- },
223
- "escalateOnTimeout": {
224
- "type": "boolean",
225
- "description": "Escalate to a stronger model tier when a stage times out and retries. Default: true.",
226
- "default": true
227
- }
228
- },
229
- "additionalProperties": false
89
+ "defaultExecutable": {
90
+ "type": "string",
91
+ "description": "Executable used for bare @kody comments on issues.",
92
+ "default": "run"
230
93
  },
231
- "watch": {
232
- "type": "object",
233
- "description": "Kody Watch periodic health monitoring. Runs every 30 minutes via GitHub Actions to check pipeline health, security, and configuration.",
234
- "properties": {
235
- "enabled": {
236
- "type": "boolean",
237
- "description": "Enable Kody Watch periodic monitoring",
238
- "default": false
239
- },
240
- "activityLog": {
241
- "type": "number",
242
- "description": "GitHub issue number for posting activity log reports. Auto-created by bootstrap."
243
- },
244
- "model": {
245
- "type": "string",
246
- "pattern": "^[^/]+/.+$",
247
- "description": "'provider/model' string for watch agents. Falls back to agent.modelMap.cheap if not set.",
248
- "examples": ["claude/claude-sonnet-4-6", "claude/claude-haiku-4-5-20251001", "minimax/MiniMax-M1"]
249
- }
250
- },
251
- "additionalProperties": false
94
+ "defaultPrExecutable": {
95
+ "type": "string",
96
+ "description": "Optional executable used for bare @kody comments on PRs. Leave unset to require explicit PR commands."
252
97
  },
253
- "decompose": {
98
+ "onPullRequest": {
99
+ "type": "string",
100
+ "description": "Optional executable to run on pull_request opened, synchronize, or reopened events."
101
+ },
102
+ "aliases": {
254
103
  "type": "object",
255
- "description": "Decompose command configuration for splitting complex tasks into parallel sub-tasks.",
256
- "properties": {
257
- "enabled": {
258
- "type": "boolean",
259
- "description": "Enable the decompose command. Default: true.",
260
- "default": true
261
- },
262
- "maxParallelSubTasks": {
263
- "type": "number",
264
- "description": "Maximum number of sub-tasks to build in parallel. Default: 3.",
265
- "default": 3
266
- },
267
- "minComplexityScore": {
268
- "type": "number",
269
- "description": "Minimum complexity score (1-10) for a task to be decomposed. Set lower to decompose more tasks. Default: 4.",
270
- "default": 4
271
- }
272
- },
273
- "additionalProperties": false
104
+ "description": "Comment subcommand aliases, mapping typed word to executable name.",
105
+ "additionalProperties": {
106
+ "type": "string"
107
+ }
274
108
  },
275
- "mcp": {
109
+ "classify": {
276
110
  "type": "object",
277
- "description": "MCP (Model Context Protocol) server configuration. Enables external tools like browser automation.",
111
+ "additionalProperties": false,
278
112
  "properties": {
279
- "enabled": {
280
- "type": "boolean",
281
- "description": "Enable MCP server integration",
282
- "default": false
283
- },
284
- "servers": {
113
+ "labelMap": {
285
114
  "type": "object",
286
- "description": "Named MCP server definitions. Each key is a server name.",
287
115
  "additionalProperties": {
288
- "type": "object",
289
- "properties": {
290
- "command": {
291
- "type": "string",
292
- "description": "Command to start the MCP server (e.g., 'npx')"
293
- },
294
- "args": {
295
- "type": "array",
296
- "items": { "type": "string" },
297
- "description": "Command arguments"
298
- },
299
- "env": {
300
- "type": "object",
301
- "additionalProperties": { "type": "string" },
302
- "description": "Environment variables for the server process"
303
- }
304
- },
305
- "required": ["command"]
116
+ "type": "string"
306
117
  }
307
- },
308
- "stages": {
309
- "type": "array",
310
- "items": { "type": "string" },
311
- "description": "Stages that can use MCP tools. Defaults to [\"build\", \"verify\", \"review\", \"review-fix\"]",
312
- "default": ["build", "verify", "review", "review-fix"]
313
- },
314
- "devServer": {
315
- "type": "object",
316
- "description": "DEPRECATED: Use top-level devServer instead. Kept for backward compatibility.",
317
- "properties": {
318
- "command": {
319
- "type": "string",
320
- "description": "Command to start the dev server (e.g., 'pnpm dev')"
321
- },
322
- "url": {
323
- "type": "string",
324
- "description": "URL where the dev server will be accessible (e.g., 'http://localhost:3000')"
325
- },
326
- "readyPattern": {
327
- "type": "string",
328
- "description": "Regex pattern to match in stdout when server is ready. Default: 'Ready in|compiled|started server|Local:'"
329
- },
330
- "readyTimeout": {
331
- "type": "number",
332
- "description": "Seconds to wait for the server to be ready. Default: 180"
333
- }
334
- },
335
- "required": ["command", "url"]
336
118
  }
337
- },
338
- "required": ["enabled", "servers"],
339
- "additionalProperties": false
119
+ }
340
120
  },
341
121
  "release": {
342
122
  "type": "object",
343
- "description": "Release automation configuration. Used by 'kody-engine release' command.",
123
+ "additionalProperties": false,
344
124
  "properties": {
345
125
  "versionFiles": {
346
126
  "type": "array",
347
- "items": { "type": "string" },
348
- "description": "Files containing version strings to update on release. Default: [\"package.json\"]",
349
- "default": ["package.json"]
350
- },
351
- "publishCommand": {
352
- "type": "string",
353
- "description": "Shell command to run after tagging for publishing. Empty = skip. NOT hardcoded to any registry.",
354
- "default": "",
355
- "examples": ["npm publish --access public", "cargo publish", "make publish"]
356
- },
357
- "notifyCommand": {
358
- "type": "string",
359
- "description": "Shell command for post-release notifications. $VERSION is interpolated at runtime. Empty = skip.",
360
- "default": "",
361
- "examples": ["curl -X POST $SLACK_WEBHOOK -d '{\"text\": \"Released v$VERSION\"}'"]
362
- },
363
- "e2eCommand": {
364
- "type": "string",
365
- "description": "Shell command to run E2E tests as a release gate. Runs before publishCommand. $VERSION is interpolated at runtime. Empty = skip (no E2E gate).",
366
- "default": "",
367
- "examples": ["pnpm test:e2e", "npm run e2e", "playwright test"]
127
+ "items": { "type": "string" }
368
128
  },
129
+ "publishCommand": { "type": "string" },
130
+ "notifyCommand": { "type": "string" },
131
+ "e2eCommand": { "type": "string" },
132
+ "draftRelease": { "type": "boolean" },
133
+ "releaseBranch": { "type": "string" },
369
134
  "timeoutMs": {
370
135
  "type": "integer",
371
- "description": "Timeout in milliseconds for e2eCommand, publishCommand, and notifyCommand. Default: 600000 (10 minutes).",
372
- "default": 600000,
373
- "examples": [600000, 1200000, 1800000]
374
- },
375
- "releaseBranch": {
376
- "type": "string",
377
- "description": "Production branch — E2E gates PRs into this branch. Release PR targets git.defaultBranch. Defaults to 'main'.",
378
- "default": "main"
379
- },
380
- "labels": {
381
- "type": "array",
382
- "items": { "type": "string" },
383
- "description": "Labels to add to the release PR. Default: [\"release\"]",
384
- "default": ["kody:release"]
385
- },
386
- "draftRelease": {
387
- "type": "boolean",
388
- "description": "Create GitHub Releases as drafts. Default: false",
389
- "default": false
136
+ "minimum": 1
390
137
  }
391
- },
392
- "additionalProperties": false
138
+ }
393
139
  },
394
140
  "jobs": {
395
141
  "type": "object",
396
- "description": "File-based job configuration. Jobs are long-running scheduled executables defined under .kody/duties/<slug>.md.",
142
+ "additionalProperties": false,
397
143
  "properties": {
398
144
  "stateBackend": {
399
145
  "type": "string",
400
146
  "enum": ["contents-api", "local-file"],
401
- "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).",
402
147
  "default": "contents-api"
403
148
  }
404
- },
405
- "additionalProperties": false
149
+ }
406
150
  },
407
151
  "access": {
408
152
  "type": "object",
409
- "description": "Who may trigger kody via an @kody comment. Gates on the GitHub comment.author_association already present on the issue_comment event (no API call, no read:org token).",
153
+ "additionalProperties": false,
410
154
  "properties": {
411
155
  "allowedAssociations": {
412
156
  "type": "array",
157
+ "description": "GitHub author_association values allowed to trigger @kody. Empty array disables the gate.",
413
158
  "items": {
414
159
  "type": "string",
415
- "enum": ["OWNER", "MEMBER", "COLLABORATOR", "CONTRIBUTOR", "FIRST_TIME_CONTRIBUTOR", "FIRST_TIMER", "MANNEQUIN", "NONE"]
160
+ "enum": [
161
+ "OWNER",
162
+ "MEMBER",
163
+ "COLLABORATOR",
164
+ "CONTRIBUTOR",
165
+ "FIRST_TIME_CONTRIBUTOR",
166
+ "FIRST_TIMER",
167
+ "MANNEQUIN",
168
+ "NONE"
169
+ ]
416
170
  },
417
- "description": "Allowlist of author_association values permitted to trigger kody. OMIT this key for the secure default — only the team (OWNER, MEMBER, COLLABORATOR) may trigger; public drive-by comments are silently ignored. A non-empty list overrides which associations are allowed. An explicit empty list ([]) DISABLES the gate (anyone may trigger). MEMBER applies only to org-owned repos; on a user-owned repo the owner is OWNER and invited people are COLLABORATOR.",
418
171
  "default": ["OWNER", "MEMBER", "COLLABORATOR"]
419
172
  }
420
- },
421
- "additionalProperties": false
422
- },
423
- "devServer": {
424
- "type": "object",
425
- "description": "Dev server configuration for browser tool verification. Works with any provider (MCP or CLI-based).",
426
- "properties": {
427
- "command": {
428
- "type": "string",
429
- "description": "Command to start the dev server (e.g., 'pnpm dev')"
430
- },
431
- "url": {
432
- "type": "string",
433
- "description": "URL where the dev server will be accessible (e.g., 'http://localhost:3000')"
434
- },
435
- "readyPattern": {
436
- "type": "string",
437
- "description": "Regex pattern to match in stdout when server is ready. Default: 'Ready in|compiled|started server|Local:'"
438
- },
439
- "readyTimeout": {
440
- "type": "number",
441
- "description": "Seconds to wait for the server to be ready. Default: 180"
442
- }
443
- },
444
- "required": ["command", "url"]
173
+ }
445
174
  }
446
- },
447
- "additionalProperties": false
175
+ }
448
176
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.206",
3
+ "version": "0.4.207-next.1",
4
4
  "description": "kody — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
5
5
  "license": "MIT",
6
6
  "type": "module",