@kody-ade/kody-engine 0.4.10 → 0.4.12

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.4.10",
6
+ version: "0.4.12",
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",
@@ -4347,15 +4347,54 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd, baseBranch)
4347
4347
  git2(["fetch", "origin"], cwd);
4348
4348
  } catch {
4349
4349
  }
4350
+ let originBranchExists = false;
4350
4351
  try {
4351
4352
  git2(["rev-parse", "--verify", `origin/${branchName}`], cwd);
4353
+ originBranchExists = true;
4354
+ } catch {
4355
+ }
4356
+ if (originBranchExists && baseBranch && baseBranch !== defaultBranch) {
4357
+ let baseExists = false;
4358
+ try {
4359
+ git2(["rev-parse", "--verify", `origin/${baseBranch}`], cwd);
4360
+ baseExists = true;
4361
+ } catch {
4362
+ }
4363
+ if (baseExists) {
4364
+ let descendsFromBase = false;
4365
+ try {
4366
+ git2(["merge-base", "--is-ancestor", `origin/${baseBranch}`, `origin/${branchName}`], cwd);
4367
+ descendsFromBase = true;
4368
+ } catch {
4369
+ }
4370
+ if (!descendsFromBase) {
4371
+ process.stderr.write(
4372
+ `[kody branch] origin/${branchName} does not descend from origin/${baseBranch} \u2014 recreating from base
4373
+ `
4374
+ );
4375
+ try {
4376
+ git2(["push", "origin", "--delete", branchName], cwd);
4377
+ } catch {
4378
+ }
4379
+ try {
4380
+ git2(["update-ref", "-d", `refs/remotes/origin/${branchName}`], cwd);
4381
+ } catch {
4382
+ }
4383
+ try {
4384
+ git2(["branch", "-D", branchName], cwd);
4385
+ } catch {
4386
+ }
4387
+ originBranchExists = false;
4388
+ }
4389
+ }
4390
+ }
4391
+ if (originBranchExists) {
4352
4392
  git2(["checkout", branchName], cwd);
4353
4393
  try {
4354
4394
  git2(["pull", "origin", branchName], cwd);
4355
4395
  } catch {
4356
4396
  }
4357
4397
  return { branch: branchName, created: false };
4358
- } catch {
4359
4398
  }
4360
4399
  try {
4361
4400
  git2(["rev-parse", "--verify", branchName], cwd);
@@ -5571,6 +5610,171 @@ function composeBody({ label, exit, prUrl, reason, dryRun }) {
5571
5610
  return `\u2705 kody ${label} complete`;
5572
5611
  }
5573
5612
 
5613
+ // src/scripts/postReviewResult.ts
5614
+ function detectVerdict(body) {
5615
+ const m = body.match(/##\s*Verdict\s*:\s*(PASS|CONCERNS|FAIL)\b/i);
5616
+ if (!m) return "UNKNOWN";
5617
+ return m[1].toUpperCase();
5618
+ }
5619
+ function reviewAction(verdict, payload) {
5620
+ const type = verdict === "PASS" ? "REVIEW_PASS" : verdict === "CONCERNS" ? "REVIEW_CONCERNS" : verdict === "FAIL" ? "REVIEW_FAIL" : "REVIEW_COMPLETED";
5621
+ return { type, payload: { verdict, ...payload }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
5622
+ }
5623
+ function failedAction2(reason) {
5624
+ return { type: "REVIEW_FAILED", payload: { reason }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
5625
+ }
5626
+ var postReviewResult = async (ctx, _profile, agentResult) => {
5627
+ const prNumber = ctx.data.commentTargetNumber;
5628
+ if (!prNumber) {
5629
+ ctx.output.exitCode = 99;
5630
+ ctx.output.reason = "review postflight: no PR number in context";
5631
+ ctx.data.action = failedAction2(ctx.output.reason);
5632
+ return;
5633
+ }
5634
+ if (!agentResult || agentResult.outcome !== "completed") {
5635
+ const reason = agentResult?.error ?? "agent did not complete";
5636
+ try {
5637
+ postPrReviewComment(prNumber, `\u26A0\uFE0F kody review FAILED: ${truncate2(reason, 1e3)}`, ctx.cwd);
5638
+ } catch {
5639
+ }
5640
+ ctx.output.exitCode = 1;
5641
+ ctx.output.reason = reason;
5642
+ ctx.data.action = failedAction2(reason);
5643
+ return;
5644
+ }
5645
+ const reviewBody = agentResult.finalText.trim();
5646
+ if (!reviewBody) {
5647
+ try {
5648
+ postPrReviewComment(prNumber, `\u26A0\uFE0F kody review FAILED: agent produced no review body`, ctx.cwd);
5649
+ } catch {
5650
+ }
5651
+ ctx.output.exitCode = 1;
5652
+ ctx.output.reason = "empty review body";
5653
+ ctx.data.action = failedAction2("empty review body");
5654
+ return;
5655
+ }
5656
+ try {
5657
+ postPrReviewComment(prNumber, reviewBody, ctx.cwd);
5658
+ } catch (err) {
5659
+ const msg = err instanceof Error ? err.message : String(err);
5660
+ ctx.output.exitCode = 4;
5661
+ ctx.output.reason = `failed to post review comment: ${msg}`;
5662
+ ctx.data.action = failedAction2(ctx.output.reason);
5663
+ return;
5664
+ }
5665
+ const verdict = detectVerdict(reviewBody);
5666
+ ctx.data.reviewVerdict = verdict;
5667
+ ctx.data.reviewBody = reviewBody;
5668
+ ctx.data.action = reviewAction(verdict, { bodyPreview: truncate2(reviewBody, 500) });
5669
+ ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
5670
+ process.stdout.write(
5671
+ `
5672
+ REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.repo}/pull/${prNumber} (verdict: ${verdict})
5673
+ `
5674
+ );
5675
+ };
5676
+
5677
+ // src/scripts/openQaIssue.ts
5678
+ var QA_LABEL = "kody:qa-report";
5679
+ function qaAction(verdict, payload) {
5680
+ const type = verdict === "PASS" ? "QA_PASS" : verdict === "CONCERNS" ? "QA_CONCERNS" : verdict === "FAIL" ? "QA_FAIL" : "QA_COMPLETED";
5681
+ return { type, payload: { verdict, ...payload }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
5682
+ }
5683
+ function failedAction3(reason) {
5684
+ return { type: "QA_FAILED", payload: { reason }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
5685
+ }
5686
+ function slugifyScope(scope) {
5687
+ return scope.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
5688
+ }
5689
+ function buildIssueTitle(scope, verdict) {
5690
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5691
+ const focus = scope?.trim() ? scope.trim() : "smoke";
5692
+ const verdictTag = verdict === "UNKNOWN" ? "REPORT" : verdict;
5693
+ return `QA [${verdictTag}]: ${focus} \u2014 ${date}`.slice(0, 240);
5694
+ }
5695
+ function ensureLabel(cwd) {
5696
+ try {
5697
+ gh2(["label", "create", QA_LABEL, "--color", "8b5cf6", "--description", "kody: QA report", "--force"], { cwd });
5698
+ return true;
5699
+ } catch {
5700
+ return false;
5701
+ }
5702
+ }
5703
+ function createQaIssue(title, body, hasLabel, cwd) {
5704
+ const args = ["issue", "create", "--title", title, "--body-file", "-"];
5705
+ if (hasLabel) args.push("--label", QA_LABEL);
5706
+ const out = gh2(args, { input: body, cwd });
5707
+ const url = out.split("\n").map((l) => l.trim()).filter(Boolean).pop() ?? "";
5708
+ const m = url.match(/\/issues\/(\d+)\b/);
5709
+ if (!m) throw new Error(`gh issue create returned unexpected output: ${out}`);
5710
+ return { number: Number(m[1]), url };
5711
+ }
5712
+ var openQaIssue = async (ctx, _profile, agentResult) => {
5713
+ if (!agentResult || agentResult.outcome !== "completed") {
5714
+ const reason = agentResult?.error ?? "agent did not complete";
5715
+ process.stderr.write(`qa-engineer: ${reason}
5716
+ `);
5717
+ ctx.output.exitCode = 1;
5718
+ ctx.output.reason = reason;
5719
+ ctx.data.action = failedAction3(reason);
5720
+ return;
5721
+ }
5722
+ const reportBody = agentResult.finalText.trim();
5723
+ if (!reportBody) {
5724
+ process.stderr.write("qa-engineer: agent produced no report body\n");
5725
+ ctx.output.exitCode = 1;
5726
+ ctx.output.reason = "empty report body";
5727
+ ctx.data.action = failedAction3("empty report body");
5728
+ return;
5729
+ }
5730
+ const verdict = detectVerdict(reportBody);
5731
+ ctx.data.qaVerdict = verdict;
5732
+ ctx.data.qaReport = reportBody;
5733
+ const existingIssue = ctx.args.issue;
5734
+ if (typeof existingIssue === "number" && Number.isFinite(existingIssue) && existingIssue > 0) {
5735
+ try {
5736
+ postIssueComment(existingIssue, reportBody, ctx.cwd);
5737
+ } catch (err) {
5738
+ const msg = err instanceof Error ? err.message : String(err);
5739
+ ctx.output.exitCode = 4;
5740
+ ctx.output.reason = `failed to comment on issue #${existingIssue}: ${msg}`;
5741
+ ctx.data.action = failedAction3(ctx.output.reason);
5742
+ return;
5743
+ }
5744
+ process.stdout.write(
5745
+ `
5746
+ QA_REPORT_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.repo}/issues/${existingIssue} (verdict: ${verdict})
5747
+ `
5748
+ );
5749
+ ctx.data.action = qaAction(verdict, { issueNumber: existingIssue, mode: "comment" });
5750
+ ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
5751
+ return;
5752
+ }
5753
+ const scope = ctx.args.scope;
5754
+ const title = buildIssueTitle(scope, verdict);
5755
+ const hasLabel = ensureLabel(ctx.cwd);
5756
+ let created;
5757
+ try {
5758
+ created = createQaIssue(title, reportBody, hasLabel, ctx.cwd);
5759
+ } catch (err) {
5760
+ const msg = err instanceof Error ? err.message : String(err);
5761
+ ctx.output.exitCode = 4;
5762
+ ctx.output.reason = `failed to open QA issue: ${truncate2(msg, 1e3)}`;
5763
+ ctx.data.action = failedAction3(ctx.output.reason);
5764
+ return;
5765
+ }
5766
+ process.stdout.write(`
5767
+ QA_REPORT_POSTED=${created.url} (verdict: ${verdict})
5768
+ `);
5769
+ ctx.data.action = qaAction(verdict, {
5770
+ issueNumber: created.number,
5771
+ issueUrl: created.url,
5772
+ titleSlug: scope ? slugifyScope(scope) : "smoke",
5773
+ mode: "create"
5774
+ });
5775
+ ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
5776
+ };
5777
+
5574
5778
  // src/scripts/parseAgentResult.ts
5575
5779
  var parseAgentResult2 = async (ctx, profile, agentResult) => {
5576
5780
  if (!agentResult) {
@@ -5945,70 +6149,6 @@ function renderResearchComment(issueNumber, body) {
5945
6149
  ${body}`;
5946
6150
  }
5947
6151
 
5948
- // src/scripts/postReviewResult.ts
5949
- function detectVerdict(body) {
5950
- const m = body.match(/##\s*Verdict\s*:\s*(PASS|CONCERNS|FAIL)\b/i);
5951
- if (!m) return "UNKNOWN";
5952
- return m[1].toUpperCase();
5953
- }
5954
- function reviewAction(verdict, payload) {
5955
- const type = verdict === "PASS" ? "REVIEW_PASS" : verdict === "CONCERNS" ? "REVIEW_CONCERNS" : verdict === "FAIL" ? "REVIEW_FAIL" : "REVIEW_COMPLETED";
5956
- return { type, payload: { verdict, ...payload }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
5957
- }
5958
- function failedAction2(reason) {
5959
- return { type: "REVIEW_FAILED", payload: { reason }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
5960
- }
5961
- var postReviewResult = async (ctx, _profile, agentResult) => {
5962
- const prNumber = ctx.data.commentTargetNumber;
5963
- if (!prNumber) {
5964
- ctx.output.exitCode = 99;
5965
- ctx.output.reason = "review postflight: no PR number in context";
5966
- ctx.data.action = failedAction2(ctx.output.reason);
5967
- return;
5968
- }
5969
- if (!agentResult || agentResult.outcome !== "completed") {
5970
- const reason = agentResult?.error ?? "agent did not complete";
5971
- try {
5972
- postPrReviewComment(prNumber, `\u26A0\uFE0F kody review FAILED: ${truncate2(reason, 1e3)}`, ctx.cwd);
5973
- } catch {
5974
- }
5975
- ctx.output.exitCode = 1;
5976
- ctx.output.reason = reason;
5977
- ctx.data.action = failedAction2(reason);
5978
- return;
5979
- }
5980
- const reviewBody = agentResult.finalText.trim();
5981
- if (!reviewBody) {
5982
- try {
5983
- postPrReviewComment(prNumber, `\u26A0\uFE0F kody review FAILED: agent produced no review body`, ctx.cwd);
5984
- } catch {
5985
- }
5986
- ctx.output.exitCode = 1;
5987
- ctx.output.reason = "empty review body";
5988
- ctx.data.action = failedAction2("empty review body");
5989
- return;
5990
- }
5991
- try {
5992
- postPrReviewComment(prNumber, reviewBody, ctx.cwd);
5993
- } catch (err) {
5994
- const msg = err instanceof Error ? err.message : String(err);
5995
- ctx.output.exitCode = 4;
5996
- ctx.output.reason = `failed to post review comment: ${msg}`;
5997
- ctx.data.action = failedAction2(ctx.output.reason);
5998
- return;
5999
- }
6000
- const verdict = detectVerdict(reviewBody);
6001
- ctx.data.reviewVerdict = verdict;
6002
- ctx.data.reviewBody = reviewBody;
6003
- ctx.data.action = reviewAction(verdict, { bodyPreview: truncate2(reviewBody, 500) });
6004
- ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
6005
- process.stdout.write(
6006
- `
6007
- REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.repo}/pull/${prNumber} (verdict: ${verdict})
6008
- `
6009
- );
6010
- };
6011
-
6012
6152
  // src/scripts/recordClassification.ts
6013
6153
  import { execFileSync as execFileSync19 } from "child_process";
6014
6154
  var API_TIMEOUT_MS8 = 3e4;
@@ -6028,7 +6168,7 @@ var recordClassification = async (ctx) => {
6028
6168
  reason = parsed?.reason ?? null;
6029
6169
  }
6030
6170
  if (!classification) {
6031
- ctx.data.action = failedAction3("classification missing or invalid");
6171
+ ctx.data.action = failedAction4("classification missing or invalid");
6032
6172
  tryAuditComment(
6033
6173
  issueNumber,
6034
6174
  "\u26A0\uFE0F kody classifier could not decide \u2014 please re-run with an explicit `@kody <type>`.",
@@ -6069,7 +6209,7 @@ function tryAuditComment(issueNumber, body, cwd) {
6069
6209
  function makeAction3(type, payload) {
6070
6210
  return { type, payload, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
6071
6211
  }
6072
- function failedAction3(reason) {
6212
+ function failedAction4(reason) {
6073
6213
  return { type: "CLASSIFY_FAILED", payload: { reason }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
6074
6214
  }
6075
6215
 
@@ -6108,12 +6248,12 @@ function fail(ctx, profile, reason) {
6108
6248
  ctx.data.agentDone = false;
6109
6249
  ctx.data.agentFailureReason = reason;
6110
6250
  const modeSeg = profile.name.replace(/-/g, "_").toUpperCase();
6111
- const failedAction4 = {
6251
+ const failedAction5 = {
6112
6252
  type: `${modeSeg}_FAILED`,
6113
6253
  payload: { reason },
6114
6254
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
6115
6255
  };
6116
- ctx.data.action = failedAction4;
6256
+ ctx.data.action = failedAction5;
6117
6257
  }
6118
6258
  function countActionItems(block) {
6119
6259
  if (!block.trim()) return 0;
@@ -6154,12 +6294,12 @@ function fail2(ctx, profile, reason) {
6154
6294
  ctx.data.agentDone = false;
6155
6295
  ctx.data.agentFailureReason = reason;
6156
6296
  const modeSeg = profile.name.replace(/-/g, "_").toUpperCase();
6157
- const failedAction4 = {
6297
+ const failedAction5 = {
6158
6298
  type: `${modeSeg}_FAILED`,
6159
6299
  payload: { reason },
6160
6300
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
6161
6301
  };
6162
- ctx.data.action = failedAction4;
6302
+ ctx.data.action = failedAction5;
6163
6303
  }
6164
6304
 
6165
6305
  // src/scripts/resolveArtifacts.ts
@@ -7358,6 +7498,7 @@ var postflightScripts = {
7358
7498
  recordClassification,
7359
7499
  dispatchClassified,
7360
7500
  notifyTerminal,
7501
+ openQaIssue,
7361
7502
  recordOutcome,
7362
7503
  mergeReleasePr,
7363
7504
  waitForCi,
@@ -0,0 +1,90 @@
1
+ {
2
+ "name": "qa-engineer",
3
+ "role": "primitive",
4
+ "describe": "Free-form QA: browses a running site with Playwright MCP, explores routes, exercises UI states, posts a structured QA report. Opens a new issue per run by default; pass --issue <N> to comment on an existing one. Read-only on the repo.",
5
+ "kind": "oneshot",
6
+ "inputs": [
7
+ {
8
+ "name": "url",
9
+ "flag": "--url",
10
+ "type": "string",
11
+ "required": true,
12
+ "describe": "Base URL the agent should browse (e.g. http://localhost:3000)."
13
+ },
14
+ {
15
+ "name": "scope",
16
+ "flag": "--scope",
17
+ "type": "string",
18
+ "required": false,
19
+ "describe": "Optional feature focus (e.g. 'admin chat memory recall'). Without a scope the agent does a broad smoke pass over discovered routes."
20
+ },
21
+ {
22
+ "name": "issue",
23
+ "flag": "--issue",
24
+ "type": "int",
25
+ "required": false,
26
+ "describe": "Optional: comment the QA report on this existing issue instead of opening a new one."
27
+ },
28
+ {
29
+ "name": "authProfile",
30
+ "flag": "--auth-profile",
31
+ "type": "string",
32
+ "required": false,
33
+ "describe": "Path to a Playwright storageState.json for pre-authenticated sessions (skips manual login)."
34
+ }
35
+ ],
36
+ "claudeCode": {
37
+ "model": "inherit",
38
+ "permissionMode": "acceptEdits",
39
+ "maxTurns": null,
40
+ "maxThinkingTokens": null,
41
+ "systemPromptAppend": null,
42
+ "tools": [
43
+ "Read",
44
+ "Grep",
45
+ "Glob",
46
+ "Bash",
47
+ "Write",
48
+ "Edit",
49
+ "mcp__playwright"
50
+ ],
51
+ "hooks": ["block-git"],
52
+ "skills": [],
53
+ "commands": [],
54
+ "subagents": [],
55
+ "plugins": [],
56
+ "mcpServers": [
57
+ {
58
+ "name": "playwright",
59
+ "command": "npx",
60
+ "args": ["-y", "--package=@playwright/mcp@latest", "--", "playwright-mcp"]
61
+ }
62
+ ]
63
+ },
64
+ "cliTools": [
65
+ {
66
+ "name": "playwright",
67
+ "install": {
68
+ "required": false,
69
+ "checkCommand": "ls \"$HOME/.cache/ms-playwright\" 2>/dev/null | grep -q '^chromium' || ls \"$HOME/Library/Caches/ms-playwright\" 2>/dev/null | grep -q '^chromium'",
70
+ "installCommand": "npx --yes playwright install chromium"
71
+ },
72
+ "verify": "ls \"$HOME/.cache/ms-playwright\" 2>/dev/null | grep -q '^chromium' || ls \"$HOME/Library/Caches/ms-playwright\" 2>/dev/null | grep -q '^chromium'",
73
+ "usage": "The Playwright MCP server uses Chromium under the hood. Preflight ensures it is installed. Save screenshots under `.kody/qa-reports/<run>/` if you take any — that directory is gitignored.",
74
+ "allowedUses": ["--version"]
75
+ }
76
+ ],
77
+ "inputArtifacts": [],
78
+ "outputArtifacts": [],
79
+ "scripts": {
80
+ "preflight": [
81
+ { "script": "discoverQaContext" },
82
+ { "script": "loadQaGuide" },
83
+ { "script": "loadConventions" },
84
+ { "script": "composePrompt" }
85
+ ],
86
+ "postflight": [
87
+ { "script": "openQaIssue" }
88
+ ]
89
+ }
90
+ }
@@ -0,0 +1,103 @@
1
+ You are Kody, a senior QA engineer. Your job is to **browse the running app like a real user**, exercise the UI broadly and intentionally, and produce one structured QA report. You do NOT fix bugs. You do NOT touch tracked source files. You do NOT run `git` or `gh`.
2
+
3
+ You may write throwaway artifacts (screenshots, ad-hoc Playwright specs) under `.kody/qa-reports/` — that path is gitignored.
4
+
5
+ # Target
6
+
7
+ Base URL: `{{args.url}}`
8
+ {{#args.scope}}Focus: **{{args.scope}}**{{/args.scope}}
9
+ {{^args.scope}}Focus: broad smoke across discovered routes.{{/args.scope}}
10
+ {{#args.authProfile}}Auth: a saved Playwright `storageState.json` is available at `{{args.authProfile}}`. Pass it to `mcp__playwright__browser_navigate` via the `storageState` parameter so the session starts pre-authenticated.{{/args.authProfile}}
11
+ {{^args.authProfile}}Auth: log in fresh using credentials from the QA guide if needed.{{/args.authProfile}}
12
+
13
+ Report destination: {{#args.issue}}existing issue #{{args.issue}} (postflight will comment on it){{/args.issue}}{{^args.issue}}a new issue (postflight will open one and label it `kody:qa-report`){{/args.issue}}.
14
+
15
+ # How to browse
16
+
17
+ You have the **Playwright MCP** tools (`mcp__playwright__browser_navigate`, `mcp__playwright__browser_snapshot`, `mcp__playwright__browser_click`, `mcp__playwright__browser_type`, `mcp__playwright__browser_take_screenshot`, etc.). These return structured accessibility snapshots — prefer them over raw screenshots when you need to reason about the DOM. Reach for screenshots when something *looks* wrong rather than *is* wrong.
18
+
19
+ Before anything else, navigate to the base URL:
20
+
21
+ ```
22
+ mcp__playwright__browser_navigate({ url: "{{args.url}}" })
23
+ ```
24
+
25
+ If that errors (timeout, DNS, connection refused), the app is unreachable. STOP browsing, write a short report explaining the failure, and exit. Don't fabricate findings.
26
+
27
+ # QA context (auto-discovered from the repo)
28
+
29
+ ```
30
+ {{qaContext}}
31
+ ```
32
+
33
+ # QA guide (committed in the repo — authoritative over the auto-discovery above)
34
+
35
+ {{qaGuide}}
36
+
37
+ {{conventionsBlock}}
38
+
39
+ {{toolsUsage}}
40
+
41
+ # What to do
42
+
43
+ 1. **Plan the session.** From the QA context, the QA guide, and the focus, build a short test matrix. For each candidate UI surface, list the user-visible behaviors worth verifying. Skip surfaces unrelated to the focus.
44
+
45
+ 2. **Authenticate if required.** If a route under test needs a role and you have credentials (in the QA guide or via `--auth-profile`), log in once. If credentials for a needed role are missing, note it as a gap and browse only what you can.
46
+
47
+ 3. **Exercise each surface.** For every UI surface in your matrix, run through the relevant states. Don't pad — apply the checklist where it actually matters:
48
+ - **Happy path.** The user-visible behavior the surface exists to support, end to end.
49
+ - **Empty state.** Zero items, no rows, no results. Is the screen meaningfully empty or just confusingly blank?
50
+ - **Loading.** What renders before data resolves? Skeletons? Layout shift?
51
+ - **Error.** Force a failure where you reasonably can — invalid input, broken nav, network throttle. Is the error visible and actionable?
52
+ - **Validation.** Submit forms with invalid / boundary / empty inputs. What's the feedback?
53
+ - **Mobile / narrow viewport.** Resize to ~375px wide. Anything cut off, overlapping, illegible?
54
+ - **Keyboard nav.** Tab through. Is focus visible at every step? Can a keyboard-only user reach every interactive element? Does Enter/Space activate the right control?
55
+ - **Destructive action.** If present (delete, archive, sign out), confirm it's gated behind a confirmation and the gate works.
56
+
57
+ 4. **Capture evidence.** Save screenshots that show the bug or the verified-good state under `.kody/qa-reports/<scope-slug>/<finding-slug>.png`. Reference them by relative path in the report. Don't screenshot every step — only what you need to back a finding.
58
+
59
+ 5. **Write the report.** Your FINAL MESSAGE must be **the entire QA report markdown, verbatim** — no preamble, no `DONE` marker, no `COMMIT_MSG` marker. The postflight reads your final message and posts it.
60
+
61
+ # Required output format
62
+
63
+ ```
64
+ ## Verdict: PASS | CONCERNS | FAIL
65
+
66
+ _QA by kody — browsed `{{args.url}}`{{#args.scope}} (focus: {{args.scope}}){{/args.scope}}_
67
+
68
+ ### Summary
69
+ <2–3 sentences: what you covered and what the running app actually does>
70
+
71
+ ### What I browsed
72
+ - `<route>` — <surface checked, states exercised, screenshot path if any>
73
+ - ...
74
+
75
+ ### Findings
76
+ - **[P0 | P1 | P2 | P3] <short title>** — `<route>`
77
+ - **Steps:** 1) … 2) … 3) …
78
+ - **Expected:** …
79
+ - **Actual:** …
80
+ - **Evidence:** `.kody/qa-reports/.../shot.png` (if applicable)
81
+ - ...
82
+ - (write "None." if you found no defects)
83
+
84
+ ### Gaps
85
+ - <anything you could NOT verify and why — missing creds, unreachable surface, no test data — say "None." if you covered everything in your matrix>
86
+
87
+ ### Bottom line
88
+ <one sentence>
89
+ ```
90
+
91
+ # Severity rubric
92
+
93
+ - **P0** — blocks core flow, data loss, security exposure, total breakage on a critical path. Verdict must be FAIL if any P0 lands.
94
+ - **P1** — broken feature on a non-critical path, or a P0-class issue with a workaround. Verdict typically FAIL.
95
+ - **P2** — degraded UX (visual bugs, minor a11y, confusing copy, edge-case handling). Verdict typically CONCERNS.
96
+ - **P3** — polish (alignment, micro-copy, non-blocking inconsistency). Doesn't affect verdict on its own.
97
+
98
+ # Rules
99
+
100
+ - No commits. No `git` / `gh`. No edits outside `.kody/qa-reports/`.
101
+ - Verdict **PASS** only when every UI surface you exercised behaved as the user would expect.
102
+ - Be specific in every finding: route + concrete steps + screenshot path (or DOM snapshot reference). No "consider improving X" advice.
103
+ - If the base URL was unreachable, the report should still be valid markdown — just say so under "Bottom line" and "Gaps", and use verdict **CONCERNS** (not FAIL — there's no defect, only an unreachable target).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.10",
3
+ "version": "0.4.12",
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",
@@ -81,13 +81,16 @@ jobs:
81
81
  node-version: 22
82
82
 
83
83
  - name: Write pip cache key for LiteLLM
84
- run: echo "litellm[proxy]" > "${{ runner.temp }}/kody-pip-requirements.txt"
84
+ run: echo "litellm[proxy]" > .kody-pip-requirements.txt
85
85
 
86
86
  - uses: actions/setup-python@v5
87
87
  with:
88
88
  python-version: "3.12"
89
89
  cache: "pip"
90
- cache-dependency-path: ${{ runner.temp }}/kody-pip-requirements.txt
90
+ cache-dependency-path: .kody-pip-requirements.txt
91
+
92
+ - name: Remove pip cache key file (avoid blocking branch switches)
93
+ run: rm -f .kody-pip-requirements.txt
91
94
 
92
95
  - env:
93
96
  ALL_SECRETS: ${{ toJSON(secrets) }}