@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.
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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.
|
|
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",
|
package/templates/kody.yml
CHANGED
|
@@ -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]" >
|
|
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:
|
|
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) }}
|