@kody-ade/kody-engine 0.3.79 → 0.3.81

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/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.79",
6
+ version: "0.3.81",
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",
@@ -662,7 +662,7 @@ import * as path5 from "path";
662
662
 
663
663
  // src/chat/inbox.ts
664
664
  import { execFileSync } from "child_process";
665
- var DEFAULT_POLL_MS = 3e4;
665
+ var DEFAULT_POLL_MS = 3e3;
666
666
  async function waitForNextUserMessage(opts) {
667
667
  const pollMs = opts.pollIntervalMs ?? DEFAULT_POLL_MS;
668
668
  const logger = opts.logger ?? {
@@ -724,7 +724,7 @@ function currentBranch(cwd) {
724
724
  // src/chat/modes/interactive.ts
725
725
  var DEFAULT_IDLE_EXIT_MS = 5 * 6e4;
726
726
  var DEFAULT_HARD_CAP_MS = 30 * 6e4;
727
- var DEFAULT_POLL_MS2 = 3e4;
727
+ var DEFAULT_POLL_MS2 = 3e3;
728
728
  async function runInteractiveMode(opts) {
729
729
  const sessionFile = sessionFilePath(opts.cwd, opts.sessionId);
730
730
  const idleExitMs = opts.meta.idleExitMs ?? DEFAULT_IDLE_EXIT_MS;
@@ -1209,7 +1209,7 @@ function coerceBare(spec, value) {
1209
1209
  }
1210
1210
 
1211
1211
  // src/executor.ts
1212
- import { execFileSync as execFileSync27, spawn as spawn3 } from "child_process";
1212
+ import { execFileSync as execFileSync27, spawn as spawn4 } from "child_process";
1213
1213
  import * as fs26 from "fs";
1214
1214
  import * as path23 from "path";
1215
1215
 
@@ -5640,6 +5640,71 @@ function escapeRegex2(s) {
5640
5640
  return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
5641
5641
  }
5642
5642
 
5643
+ // src/scripts/parseReproOutput.ts
5644
+ var parseReproOutput = async (ctx, _profile, agentResult) => {
5645
+ if (!agentResult || ctx.data.agentDone === false) return;
5646
+ const text = agentResult.finalText ?? "";
5647
+ const testPath = extractTestPath(text);
5648
+ const signatureRaw = extractFailureSignatureBlock(text);
5649
+ if (!testPath) {
5650
+ downgrade(ctx, "reproduce missing TEST_PATH line in final message");
5651
+ return;
5652
+ }
5653
+ let signature = null;
5654
+ if (signatureRaw) {
5655
+ try {
5656
+ const parsed = JSON.parse(signatureRaw);
5657
+ if (typeof parsed.errorType === "string" && typeof parsed.messageContains === "string") {
5658
+ signature = {
5659
+ errorType: parsed.errorType,
5660
+ messageContains: parsed.messageContains,
5661
+ stackContains: typeof parsed.stackContains === "string" ? parsed.stackContains : ""
5662
+ };
5663
+ }
5664
+ } catch {
5665
+ }
5666
+ }
5667
+ if (!signature) {
5668
+ downgrade(
5669
+ ctx,
5670
+ "reproduce missing or malformed FAILURE_SIGNATURE JSON (must contain errorType + messageContains)"
5671
+ );
5672
+ return;
5673
+ }
5674
+ ctx.data.reproTestPath = testPath;
5675
+ ctx.data.reproFailureSignature = JSON.stringify(signature);
5676
+ };
5677
+ function extractTestPath(text) {
5678
+ const m = text.match(/^[\s>*_#`~-]*TEST_PATH[\s>*_#`~-]*\s*:\s*(.+?)\s*$/im);
5679
+ if (!m) return "";
5680
+ return stripMarkdownEmphasis2(m[1] ?? "");
5681
+ }
5682
+ function extractFailureSignatureBlock(text) {
5683
+ const startIdx = text.search(/(?:^|\n)[ \t]*FAILURE_SIGNATURE\s*:[ \t]*/i);
5684
+ if (startIdx === -1) return "";
5685
+ const afterMarker = text.slice(startIdx).replace(/^[\s\S]*?FAILURE_SIGNATURE\s*:[ \t]*\n?/i, "");
5686
+ const stopRe = /(?:^|\n)[ \t]*(?:COMMIT_MSG|PR_SUMMARY|TEST_PATH)\s*:/i;
5687
+ const stopIdx = afterMarker.search(stopRe);
5688
+ let block = stopIdx === -1 ? afterMarker : afterMarker.slice(0, stopIdx);
5689
+ block = block.trim();
5690
+ block = block.replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/i, "");
5691
+ return block.trim();
5692
+ }
5693
+ function stripMarkdownEmphasis2(s) {
5694
+ return s.trim().replace(/^[*_`~]+|[*_`~]+$/g, "").trim();
5695
+ }
5696
+ function downgrade(ctx, reason) {
5697
+ const action = ctx.data.action;
5698
+ if (action && action.type.endsWith("_COMPLETED")) {
5699
+ ctx.data.action = {
5700
+ type: action.type.replace(/_COMPLETED$/, "_FAILED"),
5701
+ payload: { reason, downgradedFrom: action.type },
5702
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5703
+ };
5704
+ }
5705
+ ctx.data.agentDone = false;
5706
+ }
5707
+
5643
5708
  // src/scripts/persistArtifacts.ts
5644
5709
  var persistArtifacts = async (ctx, profile) => {
5645
5710
  if (profile.outputArtifacts.length === 0) return;
@@ -6749,6 +6814,122 @@ var verify = async (ctx) => {
6749
6814
  }
6750
6815
  };
6751
6816
 
6817
+ // src/scripts/verifyReproFails.ts
6818
+ import { spawn as spawn3 } from "child_process";
6819
+ var TEST_TIMEOUT_MS = 10 * 60 * 1e3;
6820
+ var TAIL_CHARS2 = 8e3;
6821
+ var ANSI_RE2 = /\x1B\[[0-?]*[ -/]*[@-~]/g;
6822
+ var verifyReproFails = async (ctx) => {
6823
+ if (ctx.data.agentDone === false) return;
6824
+ const testPath = ctx.data.reproTestPath ?? "";
6825
+ const signatureRaw = ctx.data.reproFailureSignature ?? "";
6826
+ if (!testPath || !signatureRaw) {
6827
+ downgrade2(ctx, "verifyReproFails: missing testPath or signature in ctx.data");
6828
+ return;
6829
+ }
6830
+ let signature;
6831
+ try {
6832
+ signature = JSON.parse(signatureRaw);
6833
+ } catch {
6834
+ downgrade2(ctx, "verifyReproFails: failure signature is not valid JSON");
6835
+ return;
6836
+ }
6837
+ const baseCmd = ctx.config.quality.testUnit;
6838
+ if (!baseCmd) {
6839
+ downgrade2(ctx, "verifyReproFails: kody.config.json quality.testUnit is empty \u2014 cannot run repro test");
6840
+ return;
6841
+ }
6842
+ const cmd = composeTestCommand(baseCmd, testPath);
6843
+ const result = await runCommand2(cmd, ctx.cwd);
6844
+ const output = stripAnsi2(result.output);
6845
+ ctx.data.reproVerifyExitCode = result.exitCode;
6846
+ ctx.data.reproVerifyTail = output.slice(-TAIL_CHARS2);
6847
+ if (result.exitCode === 0) {
6848
+ downgrade2(
6849
+ ctx,
6850
+ `verifyReproFails: repro test at \`${testPath}\` exited 0 \u2014 the test should be failing because the bug is unfixed`
6851
+ );
6852
+ return;
6853
+ }
6854
+ const haystack = output.toLowerCase();
6855
+ const messageOk = signature.messageContains.length === 0 || haystack.includes(signature.messageContains.toLowerCase());
6856
+ const errorTypeOk = signature.errorType.length === 0 || haystack.includes(signature.errorType.toLowerCase());
6857
+ if (!messageOk || !errorTypeOk) {
6858
+ const missing = [];
6859
+ if (!messageOk) missing.push(`messageContains="${signature.messageContains}"`);
6860
+ if (!errorTypeOk) missing.push(`errorType="${signature.errorType}"`);
6861
+ downgrade2(
6862
+ ctx,
6863
+ `verifyReproFails: repro test failed but the failure signature did not match (missing ${missing.join(", ")}) \u2014 the test may be failing for the wrong reason (e.g. import or syntax error)`
6864
+ );
6865
+ return;
6866
+ }
6867
+ ctx.data.reproVerified = true;
6868
+ };
6869
+ function composeTestCommand(baseCmd, testPath) {
6870
+ const trimmed = baseCmd.trim();
6871
+ const lower = trimmed.toLowerCase();
6872
+ const positionalRunners = ["vitest", "jest", "pytest", "mocha", "ava", "deno test", "bun test"];
6873
+ if (positionalRunners.some((r) => lower.includes(r))) {
6874
+ return `${trimmed} ${shellQuote(testPath)}`;
6875
+ }
6876
+ return trimmed;
6877
+ }
6878
+ function shellQuote(s) {
6879
+ if (/^[A-Za-z0-9_./-]+$/.test(s)) return s;
6880
+ return `'${s.replace(/'/g, "'\\''")}'`;
6881
+ }
6882
+ function stripAnsi2(s) {
6883
+ return s.replace(ANSI_RE2, "");
6884
+ }
6885
+ function runCommand2(command, cwd) {
6886
+ return new Promise((resolve4) => {
6887
+ const child = spawn3(command, {
6888
+ cwd,
6889
+ shell: true,
6890
+ env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" },
6891
+ stdio: ["ignore", "pipe", "pipe"]
6892
+ });
6893
+ const buffers = [];
6894
+ let totalSize = 0;
6895
+ const collect = (chunk) => {
6896
+ buffers.push(chunk);
6897
+ totalSize += chunk.length;
6898
+ while (totalSize > TAIL_CHARS2 * 4 && buffers.length > 1) {
6899
+ totalSize -= buffers[0].length;
6900
+ buffers.shift();
6901
+ }
6902
+ };
6903
+ child.stdout?.on("data", collect);
6904
+ child.stderr?.on("data", collect);
6905
+ const timer = setTimeout(() => {
6906
+ child.kill("SIGTERM");
6907
+ setTimeout(() => {
6908
+ if (!child.killed) child.kill("SIGKILL");
6909
+ }, 5e3);
6910
+ }, TEST_TIMEOUT_MS);
6911
+ child.on("exit", (code) => {
6912
+ clearTimeout(timer);
6913
+ resolve4({ exitCode: code ?? -1, output: Buffer.concat(buffers).toString("utf-8") });
6914
+ });
6915
+ child.on("error", (err) => {
6916
+ clearTimeout(timer);
6917
+ resolve4({ exitCode: -1, output: err.message });
6918
+ });
6919
+ });
6920
+ }
6921
+ function downgrade2(ctx, reason) {
6922
+ const action = ctx.data.action;
6923
+ if (action && action.type.endsWith("_COMPLETED")) {
6924
+ ctx.data.action = {
6925
+ type: action.type.replace(/_COMPLETED$/, "_FAILED"),
6926
+ payload: { reason, downgradedFrom: action.type },
6927
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6928
+ };
6929
+ }
6930
+ ctx.data.agentDone = false;
6931
+ }
6932
+
6752
6933
  // src/scripts/waitForCi.ts
6753
6934
  import { execFileSync as execFileSync25 } from "child_process";
6754
6935
  var API_TIMEOUT_MS10 = 3e4;
@@ -7066,11 +7247,13 @@ var postflightScripts = {
7066
7247
  parseAgentResult: parseAgentResult2,
7067
7248
  parseIssueStateFromAgentResult,
7068
7249
  parseMissionStateFromAgentResult,
7250
+ parseReproOutput,
7069
7251
  writeIssueStateComment,
7070
7252
  writeMissionStateFile,
7071
7253
  requireFeedbackActions,
7072
7254
  requirePlanDeviations,
7073
7255
  verify,
7256
+ verifyReproFails,
7074
7257
  checkCoverageWithRetry,
7075
7258
  abortUnfinishedGitOps: abortUnfinishedGitOps2,
7076
7259
  stageMergeConflicts,
@@ -7434,7 +7617,7 @@ async function runShellEntry(entry, ctx, profile) {
7434
7617
  env[`KODY_CFG_${k}`] = v;
7435
7618
  }
7436
7619
  const timeoutMs = resolveShellTimeoutMs(entry);
7437
- const child = spawn3("bash", [shellPath, ...positional], {
7620
+ const child = spawn4("bash", [shellPath, ...positional], {
7438
7621
  cwd: ctx.cwd,
7439
7622
  env,
7440
7623
  stdio: ["pipe", "pipe", "pipe"],
@@ -68,6 +68,15 @@
68
68
  ]
69
69
  },
70
70
  "children": [
71
+ {
72
+ "exec": "reproduce",
73
+ "target": "issue",
74
+ "next": {
75
+ "REPRODUCE_COMPLETED": "plan",
76
+ "REPRODUCE_FAILED": "plan",
77
+ "*": "abort"
78
+ }
79
+ },
71
80
  {
72
81
  "exec": "plan",
73
82
  "target": "issue",
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "reproduce",
3
+ "role": "primitive",
4
+ "describe": "Write a failing test that reproduces a bug. Do NOT fix the bug — leave the test failing and capture the failure signature so subsequent fix verification can confirm the same failure mode.",
5
+ "inputs": [
6
+ {
7
+ "name": "issue",
8
+ "flag": "--issue",
9
+ "type": "int",
10
+ "required": true,
11
+ "describe": "GitHub issue number to reproduce."
12
+ }
13
+ ],
14
+ "claudeCode": {
15
+ "model": "inherit",
16
+ "permissionMode": "acceptEdits",
17
+ "maxTurns": null,
18
+ "systemPromptAppend": null,
19
+ "tools": [
20
+ "Read",
21
+ "Write",
22
+ "Edit",
23
+ "Bash",
24
+ "Grep",
25
+ "Glob"
26
+ ],
27
+ "hooks": ["block-git"],
28
+ "skills": [],
29
+ "commands": [],
30
+ "subagents": [],
31
+ "plugins": [],
32
+ "mcpServers": []
33
+ },
34
+ "cliTools": [],
35
+ "scripts": {
36
+ "preflight": [
37
+ {
38
+ "script": "setLifecycleLabel",
39
+ "with": {
40
+ "label": "kody:reproducing",
41
+ "color": "fef2c0",
42
+ "description": "kody: writing a failing test that reproduces the bug"
43
+ }
44
+ },
45
+ { "script": "runFlow" },
46
+ { "script": "loadTaskState" },
47
+ { "script": "loadConventions" },
48
+ { "script": "loadCoverageRules" },
49
+ { "script": "composePrompt" }
50
+ ],
51
+ "postflight": [
52
+ { "script": "parseAgentResult" },
53
+ { "script": "parseReproOutput" },
54
+ { "script": "verifyReproFails" },
55
+ { "script": "abortUnfinishedGitOps" },
56
+ { "script": "commitAndPush" },
57
+ { "script": "ensurePr" },
58
+ { "script": "postIssueComment" },
59
+ { "script": "persistArtifacts" },
60
+ { "script": "writeRunSummary" },
61
+ { "script": "saveTaskState" },
62
+ { "script": "mirrorStateToPr" },
63
+ { "script": "advanceFlow" }
64
+ ]
65
+ },
66
+ "output": {
67
+ "actionTypes": [
68
+ "REPRODUCE_COMPLETED",
69
+ "REPRODUCE_FAILED",
70
+ "AGENT_NOT_RUN"
71
+ ],
72
+ "artifacts": [
73
+ { "name": "repro", "format": "markdown", "from": "prSummary" },
74
+ { "name": "repro.testPath", "format": "text", "from": "reproTestPath" },
75
+ { "name": "repro.failureSignature", "format": "text", "from": "reproFailureSignature" }
76
+ ]
77
+ }
78
+ }
@@ -0,0 +1,67 @@
1
+ You are Kody, an autonomous engineer. Your job for this turn is **NOT** to fix the bug — it is to write a failing test that reproduces the bug, then confirm the test fails for the right reason. The wrapper handles git/gh — you do not.
2
+
3
+ Subsequent steps (`plan`, `run`) will design and implement the fix. The test you write here is the canonical proof the bug exists, and is the success criterion for the fix.
4
+
5
+ # Repo
6
+ - {{repoOwner}}/{{repoName}}, default branch: {{defaultBranch}}
7
+ - current branch (already checked out): {{branch}}
8
+
9
+ {{conventionsBlock}}{{coverageBlock}}{{toolsUsage}}# Issue #{{issue.number}}: {{issue.title}}
10
+ {{issue.body}}
11
+
12
+ # Required steps (all in this one session)
13
+
14
+ 1. **Understand the bug.** Read the issue carefully. Identify:
15
+ - The expected behavior (what *should* happen).
16
+ - The actual behavior (what *does* happen, the bug).
17
+ - The smallest piece of code that exhibits this gap.
18
+
19
+ 2. **Locate the right test home.** Read the existing test directory structure (`tests/`, `__tests__/`, `*.test.*` siblings — whatever this repo uses). Open the newest existing test file in the most fitting directory and copy its imports, setup, and assertion idioms **verbatim**. Do NOT introduce a new test framework or pattern when one already works in this repo.
20
+
21
+ 3. **Write a failing test.** Create or extend a single test file that asserts the **expected** (correct) behavior. The test must currently fail because the bug is unfixed. Keep it minimal — one test case is enough. Name it after the issue (e.g. `repro-issue-{{issue.number}}.test.ts`) when creating a new file, or add a clearly-labeled test case to an existing file.
22
+
23
+ - Do NOT change any production code.
24
+ - Do NOT mark the test as `skip`, `todo`, or `expect.fail` — it must run and assert.
25
+ - The assertion must fail because the bug exists, not because of an import error, missing fixture, or syntax error.
26
+
27
+ 4. **Run the test ONCE** with the project's test command (read from conventions / package.json). Capture:
28
+ - Exit code (must be non-zero).
29
+ - The error type (`AssertionError`, `TypeError`, the name of the failing matcher, etc.).
30
+ - A distinctive substring of the error message (something the fix is expected to flip).
31
+ - One stack-frame anchor pointing at the buggy production code, if visible.
32
+
33
+ 5. **If the test passes** (exit 0), the test isn't actually catching the bug — refine it and re-run. If after two refinement attempts you still cannot get a meaningful failure, output `FAILED: <reason>` instead.
34
+
35
+ 6. **If the test fails for the wrong reason** (import error, syntax error, missing module), fix that and re-run. Only when the failure is a real assertion against the buggy behavior do you proceed.
36
+
37
+ # Required output
38
+
39
+ Your FINAL message must use this exact format (or a single `FAILED: <reason>` line):
40
+
41
+ ```
42
+ DONE
43
+ TEST_PATH: <path/to/test/file relative to repo root>
44
+ FAILURE_SIGNATURE:
45
+ ```
46
+ {
47
+ "errorType": "<error class name, e.g. AssertionError>",
48
+ "messageContains": "<distinctive substring of the failure message>",
49
+ "stackContains": "<optional: substring of a stack frame in production code, or empty>"
50
+ }
51
+ ```
52
+ COMMIT_MSG: test: add failing repro for #{{issue.number}}
53
+ PR_SUMMARY:
54
+ - Test file: <path>
55
+ - What it asserts: <one sentence>
56
+ - Why it fails today: <one sentence pointing at the buggy production code>
57
+ - How to verify locally: <test command + filter>
58
+ ```
59
+
60
+ # Rules
61
+ - Do NOT fix the bug. Do NOT modify production code.
62
+ - Do NOT run `git` or `gh` commands. The wrapper handles all git/gh operations.
63
+ - Stay on the current branch (`{{branch}}`).
64
+ - Do NOT modify files under `.kody/`, `.kody-engine/`, `node_modules/`, `dist/`, `build/`, `.env`, or any `*.log`.
65
+ - Do NOT post issue comments — the wrapper handles that.
66
+ - The test you commit will stay red until the fix lands. That is correct.
67
+ {{systemPromptAppend}}
@@ -68,7 +68,10 @@
68
68
  },
69
69
  "input": {
70
70
  "artifacts": [
71
- { "name": "plan", "required": false }
71
+ { "name": "plan", "required": false },
72
+ { "name": "repro", "required": false },
73
+ { "name": "repro.testPath", "required": false },
74
+ { "name": "repro.failureSignature", "required": false }
72
75
  ]
73
76
  },
74
77
  "output": {
@@ -7,6 +7,14 @@ You are Kody, an autonomous engineer. Take a GitHub issue from spec to a tested
7
7
  {{conventionsBlock}}{{coverageBlock}}{{toolsUsage}}# Issue #{{issue.number}}: {{issue.title}}
8
8
  {{issue.body}}
9
9
 
10
+ # Failing repro test (success criterion, if present)
11
+ {{artifacts.repro}}
12
+
13
+ If the section above is non-empty, an earlier `reproduce` step has already committed a failing test on this branch. **The success criterion of your work is to make that test pass** without weakening it (do not delete the assertion, change the expected value to match the buggy output, or skip the test). Test path: `{{artifacts.repro.testPath}}`. The fix is only complete when:
14
+ 1. That specific test passes.
15
+ 2. The full quality gates (typecheck, lint, full test suite) pass — your fix has not regressed anything else.
16
+ If the repro section is empty, no failing test was pre-committed; proceed normally.
17
+
10
18
  # Existing plan (produced by `@kody plan`, if present)
11
19
  {{artifacts.plan}}
12
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.3.79",
3
+ "version": "0.3.81",
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",